<?php

/**
 * SQL Parser from: http://code.google.com/p/php-sql-parser/
 * License: New BSD
 */
#[\AllowDynamicProperties]
class PHPSQLParser
{
    public $reserved = array();
    public $functions = array();
    public function __construct($sql = false)
    {
        #LOAD THE LIST OF RESERVED WORDS
        $this->load_reserved_words();
        if ($sql) {
            $this->parse($sql);
        }
    }

    public function parse($sql)
    {
        $sql = trim($sql);

        #lex the SQL statement
        $in = $this->split_sql($sql);

        #sometimes the parser needs to skip ahead until a particular
        #token is found
        $skip_until = false;

        #this is the output tree which is being parsed
        $out = array();

        #This is the last type of union used (UNION or UNION ALL)
        #indicates a) presence of at least one union in this query
        #          b) the type of union if this is the first or last query
        $union = false;

        #Sometimes a "query" consists of more than one query (like a UNION query)
        #this array holds all the queries
        $queries=array();

        #This is the highest level lexical analysis.  This is the part of the
        #code which finds UNION and UNION ALL query parts
        foreach ($in as $key => $token) {
            $token=trim($token);

            if ($skip_until) {
                if ($token) {
                    if (strtoupper($token) == $skip_until) {
                        $skip_until = false;
                        continue;
                    }
                } else {
                    continue;
                }
            }

            if (strtoupper($token) == "UNION") {
                $union = 'UNION';
                $inCount = count($in);
                for ($i=$key+1;$i<$inCount;++$i) {
                    if (trim($in[$i]) == '') {
                        continue;
                    }
                    if (strtoupper($in[$i]) == 'ALL') {
                        $skip_until = 'ALL';
                        $union = 'UNION ALL';
                        continue ;
                    }
                    break;
                }

                $queries[$union][] = $out;
                $out = array();
            } else {
                $out[]=$token;
            }
        }

        if (!empty($out)) {
            if ($union) {
                $queries[$union][] = $out;
            } else {
                $queries[] = $out;
            }
        }


        /*MySQL supports a special form of UNION:
        (select ...)
        union
        (select ...)

        This block handles this query syntax.  Only one such subquery
        is supported in each UNION block.  (select)(select)union(select) is not legal.
        The extra queries will be silently ignored.
        */
        $union_types = array('UNION','UNION ALL');
        foreach ($union_types as $union_type) {
            if (!empty($queries[$union_type])) {
                foreach ($queries[$union_type] as $i => $tok_list) {
                    foreach ($tok_list as $z => $tok) {
                        $tok = trim($tok);
                        if (!$tok) {
                            continue;
                        }
                        if (preg_match('/^\\(\\s*select\\s*/i', $tok)) {
                            $queries[$union_type][$i] = $this->parse(substr($tok, 1, -1));
                            break;
                        }
                        $queries[$union_type][$i] = $this->process_sql($queries[$union_type][$i]);
                        break;
                    }
                }
            }
        }


        /* If there was no UNION or UNION ALL in the query, then the query is
        stored at $queries[0].
        */
        if (!empty($queries[0])) {
            $queries[0] = $this->process_sql($queries[0]);
        }

        if (count($queries) == 1 && !$union) {
            $queries = $queries[0];
        }

        $this->parsed = $queries;
        return $this->parsed;
    }

    #This function counts open and close parenthesis and
    #returns their location.  This might be faster as a regex
    private function count_paren($token, $chars=array('(',')'))
    {
        $len = strlen((string) $token);
        $open=array();
        $close=array();
        for ($i=0;$i<$len;++$i) {
            if ($token[$i] == $chars[0]) {
                $open[] = $i;
            } elseif ($token[$i] == $chars[1]) {
                $close[] = $i;
            }
        }
        return array('open' => $open, 'close' => $close, 'balanced' =>(count($close) - count($open)));
    }

    #This function counts open and close parenthesis and
    #returns their location.  This might be faster as a regex
    private function count_backtick($token)
    {
        $len = strlen((string) $token);
        $cnt=0;
        for ($i=0;$i<$len;++$i) {
            if ($token[$i] == '`') {
                ++$cnt;
            }
        }
        return $cnt;
    }

    #This is the lexer
    #this function splits up a SQL statement into easy to "parse"
    #tokens for the SQL processor
    private function split_sql($sql)
    {
        if (!is_string($sql)) {
            return false;
        }

        $sql = str_replace(array('\\\'','\\"',"\r\n","\n","()"), array("''",'""'," "," "," "), $sql);
        $regex=<<<EOREGEX
/(`(?:[^`]|``)`|[@A-Za-z0-9_.`-]+(?:\(\s*\)){0,1})
|(\+|-|\*|\/|!=|>=|<=|<>|>|<|&&|\|\||=|\^)
|(\(.*?\))   # Match FUNCTION(...) OR BAREWORDS
|('(?:[^']|'')*'+)
|("(?:[^"]|"")*"+)
|([^ ,]+)
/ix
EOREGEX
;

        $tokens = preg_split($regex, $sql, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE);
        $token_count = is_countable($tokens) ? count($tokens) : 0;

        /* The above regex has one problem, because the parenthetical match is not greedy.
           Thus, when matching grouped expressions such as ( (a and b) or c) the
           tokenizer will produce "( (a and b)", " ", "or", " " , "c,")"

           This block detects the number of open/close parentheses in the given token.  If the parentheses are balanced
           (balanced == 0) then we don't need to do anything.

           otherwise, we need to balance the expression.
                */
        $reset = false;
        for ($i=0;$i<$token_count;++$i) {
            if (empty($tokens[$i])) {
                continue;
            }

            $token = $tokens[$i];
            $trim = trim($token);
            if ($trim) {
                if ($trim[0] != '('
                        && substr($trim, -1) == ')') {
                    $trim=trim(substr(
                        $trim,
                        0,
                        strpos($trim, '(')
                    ));
                }
                $tokens[$i]=$trim;
                $token=$trim;
            }

            if ($token && $token[0] == '(') {
                $info = $this->count_paren($token);
                if ($info['balanced'] == 0) {
                    continue;
                }

                // We need to find this many closing parentheses.
                $needed = abs($info['balanced']);
                $n = $i;
                while ($needed > 0 && $n <$token_count-1) {
                    ++$n;
                    #echo "LOOKING FORWARD TO $n [ " . $tokens[$n] . "]\n";
                    $token2 = $tokens[$n];
                    $info2 = $this->count_paren($token2);
                    $closes = is_countable($info2['close']) ? count($info2['close']) : 0;
                    if ($closes != $needed) {
                        $tokens[$i] .= $tokens[$n];
                        unset($tokens[$n]);
                        $reset = true;
                        $info2 = $this->count_paren($tokens[$i]);
                        $needed = abs($info2['balanced']);
                    #	echo "CLOSES LESS THAN NEEDED (still need $needed)\n";
                    } else {
                        /*get the string pos of the last close parenthesis we need*/
                        $pos = $info2['close'][(is_countable($info2['close']) ? count($info2['close']) : 0)-1];
                        $str1 = $str2 = "";
                        if ($pos == 0) {
                            $str1 = ')';
                        } else {
                            $str1 = substr($tokens[$n], 0, $pos) . ')';
                            $str2 = substr($tokens[$n], $pos+1);
                        }
                        #echo "CLOSES FOUND AT $n, offset:$pos  [$str1] [$str2]\n";
                        if (strlen($str2) > 0) {
                            $tokens[$n] = $str2;
                        } else {
                            unset($tokens[$n]);
                            $reset = true;
                        }
                        $tokens[$i] .= $str1;
                        $info2 = $this->count_paren($tokens[$i]);
                        $needed = abs($info2['balanced']);
                    }
                }
            }
        }

        #the same problem appears with backticks :(

        /* reset the array if we deleted any tokens above */
        if ($reset) {
            $tokens = array_values($tokens);
        }

        $token_count=is_countable($tokens) ? count($tokens) : 0;
        for ($i=0;$i<$token_count;++$i) {
            if (empty($tokens[$i])) {
                continue;
            }
            $token=$tokens[$i];
            $needed=true;
            $reset=false;
            if ($needed && $token && strpos((string) $token, '`') !== false) {
                $info = $this->count_backtick($token);
                if ($info %2 == 0) { #even number of backticks means we are balanced
                    continue;
                }
                $needed=1;

                $n = $i;
                while ($needed && $n <$token_count-1) {
                    $reset=true;
                    #echo "BACKTICK COUNT[$i]: $info old: {$tokens[$i]}, new: ($token)\n";
                    ++$n;
                    $token .= $tokens[$n];
                    unset($tokens[$n]);
                    $needed = $this->count_backtick($token) % 2;
                }
            }
            if ($reset) {
                $tokens[$i] = $token;
            }
        }
        /* reset the array if we deleted any tokens above */
        $tokens = array_values($tokens);

        return $tokens;
    }

    /* This function breaks up the SQL statement into logical sections.
       Some sections are then further handled by specialized functions.
    */
    private function process_sql(&$tokens, $start_at = 0, $stop_at = false)
    {
        $prev_category = "";
        $start = microtime(true);
        $token_category = "";

        $skip_next=false;
        $token_count = is_countable($tokens) ? count($tokens) : 0;

        if (!$stop_at) {
            $stop_at = $token_count;
        }

        $out = [];

        for ($token_number = $start_at;$token_number<$stop_at;++$token_number) {
            $token = trim($tokens[$token_number]);
            if ($token && $token[0] == '(' && $token_category == "") {
                $token_category = 'SELECT';
            }

            /* If it isn't obvious, when $skip_next is set, then we ignore the next real
                token, that is we ignore whitespace.
                    */
            if ($skip_next) {
                #whitespace does not count as a next token
                if ($token == "") {
                    continue;
                }

                #to skip the token we replace it with whitespace
                $new_token = "";
                $skip_next = false;
            }

            $upper = strtoupper($token);
            switch ($upper) {

                    /* Tokens that get their own sections. These keywords have subclauses. */
                    case 'SELECT':
                    case 'ORDER':
                    case 'LIMIT':
                    case 'SET':
                    case 'DUPLICATE':
                    case 'VALUES':
                    case 'GROUP':
                    case 'ORDER':
                    case 'HAVING':
                    case 'INTO':
                    case 'WHERE':
                    case 'RENAME':
                    case 'CALL':
                    case 'PROCEDURE':
                    case 'FUNCTION':
                    case 'DATABASE':
                    case 'SERVER':
                    case 'LOGFILE':
                    case 'DEFINER':
                    case 'RETURNS':
                    case 'EVENT':
                    case 'TABLESPACE':
                    case 'VIEW':
                    case 'TRIGGER':
                    case 'DATA':
                    case 'DO':
                    case 'PASSWORD':
                    case 'USER':
                    case 'PLUGIN':
                    case 'FROM':
                    case 'FLUSH':
                    case 'KILL':
                    case 'RESET':
                    case 'START':
                    case 'STOP':
                    case 'PURGE':
                    case 'EXECUTE':
                    case 'PREPARE':
                    case 'DEALLOCATE':
                        if ($token == 'DEALLOCATE') {
                            $skip_next = true;
                        }
                        /* this FROM is different from FROM in other DML (not join related) */
                        if ($token_category == 'PREPARE' && $upper == 'FROM') {
                            continue 2;
                        }

                        $token_category = $upper;
                        #$join_type = 'JOIN';
                        if ($upper == 'FROM' && $token_category == 'FROM') {
                            /* DO NOTHING*/
                        } else {
                            continue 2;
                        }
                    break;

                    /* These tokens get their own section, but have no subclauses.
                                   These tokens identify the statement but have no specific subclauses of their own. */
                    case 'DELETE':
                    case 'ALTER':
                    case 'INSERT':
                    case 'REPLACE':
                    case 'TRUNCATE':
                    case 'CREATE':
                    case 'TRUNCATE':
                    case 'OPTIMIZE':
                    case 'GRANT':
                    case 'REVOKE':
                    case 'SHOW':
                    case 'HANDLER':
                    case 'LOAD':
                    case 'ROLLBACK':
                    case 'SAVEPOINT':
                    case 'UNLOCK':
                    case 'INSTALL':
                    case 'UNINSTALL':
                    case 'ANALZYE':
                    case 'BACKUP':
                    case 'CHECK':
                    case 'CHECKSUM':
                    case 'REPAIR':
                    case 'RESTORE':
                    case 'CACHE':
                    case 'DESCRIBE':
                    case 'EXPLAIN':
                    case 'USE':
                    case 'HELP':
                        $token_category = $upper; /* set the category in case these get subclauses
                                          in a future version of MySQL */
                        if (!isset($out[$upper])) {
                            $out[$upper] = [];
                        }
                        $out[$upper][0] = $upper;
                        continue 2;
                    break;

                    /* This is either LOCK TABLES or SELECT ... LOCK IN SHARE MODE*/
                    case 'LOCK':
                        if ($token_category == "") {
                            $token_category = $upper;
                            if (!isset($out[$upper])) {
                                $out[$upper] = [];
                            }
                            $out[$upper][0] = $upper;
                        } else {
                            $token = 'LOCK IN SHARE MODE';
                            $skip_next=true;
                            if (!isset($out['OPTIONS'])) {
                                $out['OPTIONS'] = [];
                            }
                            $out['OPTIONS'][] = $token;
                        }
                        continue 2;
                    break;

                    case 'USING':
                        /* USING in FROM clause is different from USING w/ prepared statement*/
                        if ($token_category == 'EXECUTE') {
                            $token_category=$upper;
                            continue 2;
                        }
                        if ($token_category == 'FROM' && !empty($out['DELETE'])) {
                            $token_category=$upper;
                            continue 2;
                        }
                    break;

                    /* DROP TABLE is different from ALTER TABLE DROP ... */
                    case 'DROP':
                        if ($token_category != 'ALTER') {
                            $token_category = $upper;
                            if (!isset($out[$upper])) {
                                $out[$upper] = [];
                            }
                            $out[$upper][0] = $upper;
                            continue 2;
                        }
                    break;

                    case 'FOR':
                        $skip_next=true;
                        if (!isset($out['OPTIONS'])) {
                            $out['OPTIONS'] = [];
                        }
                        $out['OPTIONS'][] = 'FOR UPDATE';
                        continue 2;
                    break;


                    case 'UPDATE':
                        if ($token_category == "") {
                            $token_category = $upper;
                            continue 2;
                        }
                        if ($token_category == 'DUPLICATE') {
                            continue 2;
                        }
                        break;
                    break;

                    case 'START':
                        $token = "BEGIN";
                        if (!isset($out[$upper])) {
                            $out[$upper] = [];
                        }
                        $out[$upper][0] = $upper;
                        $skip_next = true;
                    break;

                    /* These tokens are ignored. */
                    case 'BY':
                    case 'ALL':
                    case 'SHARE':
                    case 'MODE':
                    case 'TO':

                    case ';':
                        continue 2;
                        break;

                    case 'KEY':
                        if ($token_category == 'DUPLICATE') {
                            continue 2;
                        }
                    break;

                    /* These tokens set particular options for the statement.  They never stand alone.*/
                    case 'DISTINCTROW':
                        $token='DISTINCT';
                        // no break
                    case 'DISTINCT':
                    case 'HIGH_PRIORITY':
                    case 'LOW_PRIORITY':
                    case 'DELAYED':
                    case 'IGNORE':
                    case 'FORCE':
                    case 'STRAIGHT_JOIN':
                    case 'SQL_SMALL_RESULT':
                    case 'SQL_BIG_RESULT':
                    case 'QUICK':
                    case 'SQL_BUFFER_RESULT':
                    case 'SQL_CACHE':
                    case 'SQL_NO_CACHE':
                    case 'SQL_CALC_FOUND_ROWS':
                        if (!isset($out['OPTIONS'])) {
                            $out['OPTIONS'] = [];
                        }
                        $out['OPTIONS'][] = $upper;
                        continue 2;
                    break;

                    case 'WITH':
                        if ($token_category == 'GROUP') {
                            $skip_next=true;
                            if (!isset($out['OPTIONS'])) {
                                $out['OPTIONS'] = [];
                            }
                            $out['OPTIONS'][] = 'WITH ROLLUP';
                            continue 2;
                        }
                    break;


                    case 'AS':
                    break;

                    case '':
                    case ',':
                    case ';':
                        break;

                    default:
                        break;
                }

            if ($prev_category === $token_category) {
                if (!isset($out[$token_category])) {
                    $out[$token_category] = [];
                }
                $out[$token_category][] = $token;
            }

            $prev_category = $token_category;
        }

        if (empty($out)) {
            return false;
        }


        #process the SELECT clause
        if (!empty($out['SELECT'])) {
            $out['SELECT'] = $this->process_select($out['SELECT']);
        }

        if (!empty($out['FROM'])) {
            $out['FROM'] = $this->process_from($out['FROM']);
        }
        if (!empty($out['USING'])) {
            $out['USING'] = $this->process_from($out['USING']);
        }
        if (!empty($out['UPDATE'])) {
            $out['UPDATE'] = $this->process_from($out['UPDATE']);
        }

        if (!empty($out['GROUP'])) {
            $out['GROUP'] = $this->process_group($out['GROUP'], $out['SELECT']);
        }
        if (!empty($out['ORDER'])) {
            $out['ORDER'] = $this->process_group($out['ORDER'], $out['SELECT']);
        }

        if (!empty($out['LIMIT'])) {
            $out['LIMIT'] = $this->process_limit($out['LIMIT']);
        }

        if (!empty($out['WHERE'])) {
            $out['WHERE'] = $this->process_expr_list($out['WHERE']);
        }
        if (!empty($out['HAVING'])) {
            $out['HAVING'] = $this->process_expr_list($out['HAVING']);
        }
        if (!empty($out['SET'])) {
            $out['SET'] = $this->process_set_list($out['SET']);
        }
        if (!empty($out['DUPLICATE'])) {
            $out['ON DUPLICATE KEY UPDATE'] = $this->process_set_list($out['DUPLICATE']);
            unset($out['DUPLICATE']);
        }
        if (!empty($out['INSERT'])) {
            $out = $this->process_insert($out);
        }
        if (!empty($out['REPLACE'])) {
            $out = $this->process_insert($out, 'REPLACE');
        }
        if (!empty($out['DELETE'])) {
            $out = $this->process_delete($out);
        }

        return $out;
    }

    /* A SET list is simply a list of key = value expressions separated by comma (,).
    This function produces a list of the key/value expressions.
    */
    private function process_set_list($tokens)
    {
        $column="";
        $expression="";
        $expr=array();
        foreach ($tokens as $token) {
            $token=trim($token);
            if (!$column) {
                if ($token === false || empty($token)) {
                    continue;
                }
                $column .= $token;
                continue;
            }

            if ($token == '=') {
                continue;
            }

            if ($token == ',') {
                $expr[] = array('column' => trim($column), 'expr' => trim($expression));
                $expression = $column = "";
                continue;
            }

            $expression .= $token;
        }
        if ($expression) {
            $expr[] = array('column' => trim($column), 'expr' => trim($expression));
        }

        return $expr;
    }

    /* This function processes the LIMIT section.
       start,end are set.  If only end is provided in the query
       then start is set to 0.
    */
    private function process_limit($tokens)
    {
        $start = 0;
        $end = 0;

        if ($pos = array_search(',', $tokens, true)) {
            for ($i=0;$i<$pos;++$i) {
                if ($tokens[$i] != '') {
                    $start = $tokens[$i];
                    break;
                }
            }
            $pos = $pos + 1;
        } else {
            $pos = 0;
        }
        $tokensCount = count($tokens);

        for ($i=$pos;$i<$tokensCount;++$i) {
            if ($tokens[$i] != '') {
                $end = $tokens[$i];
                break;
            }
        }

        return array('start' => $start, 'end' => $end);
    }

    /* This function processes the SELECT section.  It splits the clauses at the commas.
       Each clause is then processed by process_select_expr() and the results are added to
       the expression list.

       Finally, at the end, the expression list is returned.
    */
    private function process_select(&$tokens)
    {
        $expression = "";
        $expr = array();
        foreach ($tokens as $token) {
            if ($token == ',') {
                $expr[] = $this->process_select_expr(trim($expression));
                $expression = "";
            } else {
                if ($token === "" || $token===false) {
                    $token=" ";
                }
                $expression .= $token ;
            }
        }
        $expression = trim($expression);
        if ($expression) {
            $expr[] = $this->process_select_expr($expression);
        }
        return $expr;
    }

    /* This function processes each SELECT clause.  We determine what (if any) alias
       is provided, and we set the type of expression.
    */
    private function process_select_expr($expression)
    {
        if (empty($expression)) {
            return '';
        }

        $capture = false;
        $alias = "";
        $base_expression = $expression;
        $upper = trim(strtoupper($expression));
        #if necessary, unpack the expression
        if ($upper[0] == '(') {
            #$expression = substr($expression,1,-1);
            $base_expression = $expression;
        }

        $tokens = $this->split_sql($expression);
        $token_count = is_countable($tokens) ? count($tokens) : 0;

        /* Determine if there is an explicit alias after the AS clause.
        If AS is found, then the next non-whitespace token is captured as the alias.
        The tokens after (and including) the AS are removed.
        */
        $base_expr = "";
        $stripped=array();
        $capture=false;
        $alias = "";
        $processed=false;
        for ($i=0;$i<$token_count;++$i) {
            $token = strtoupper($tokens[$i]);
            if (trim($token)) {
                $stripped[] = $tokens[$i];
            }

            if ($token == 'AS') {
                unset($tokens[$i]);
                $capture = true;
                continue;
            }

            if ($capture) {
                if (trim($token)) {
                    $alias .= $tokens[$i];
                }
                unset($tokens[$i]);
                continue;
            }
            $base_expr .= $tokens[$i];
        }

        $stripped = $this->process_expr_list($stripped);
        $last = array_pop($stripped);
        if (!$alias && $last['expr_type'] == 'colref') {
            $prev = array_pop($stripped);
            if (!empty($prev) && 
                ($prev['expr_type'] == 'operator' ||
                $prev['expr_type'] == 'const' ||
                $prev['expr_type'] == 'function' ||
                $prev['expr_type'] == 'expression' ||
                #$prev['expr_type'] == 'aggregate_function' ||
                $prev['expr_type'] == 'subquery' ||
                $prev['expr_type'] == 'colref')) {
                $alias = $last['base_expr'];

                #remove the last token
                array_pop($tokens);

                $base_expr = implode("", $tokens);
            }
        }

        if (!$alias) {
            $base_expr=implode("", $tokens);
            $alias = $base_expr;
        }

        /* Properly escape the alias if it is not escaped */
        if ($alias[0] != '`') {
            $alias = '`' . str_replace('`', '``', (string) $alias) . '`';
        }
        $processed = false;
        $type='expression';

        if (substr(trim($base_expr), 0, 1) == '(') {
            $base_expr = substr((string) $expression, 1, -1);
            if (preg_match('/^sel/i', $base_expr)) {
                $type='subquery';
                $processed = $this->parse($base_expr);
            }
        }
        if (!$processed) {
            $processed = $this->process_expr_list($tokens);
        }

        if ((is_countable($processed) ? count($processed) : 0) == 1) {
            $type = $processed[0]['expr_type'];
            $processed = false;
        }

        return array('expr_type'=>$type,'alias' => $alias, 'base_expr' => $base_expr, 'sub_tree' => $processed);
    }

    private function trimSubquery($sq)
    {
        $sq = trim($sq);
        if (empty($sq)) {
            return '';
        }
        while ($sq[0] == '(' && substr($sq, -1) == ')') {
            $sq = substr($sq, 1, -1);
        }
        return $sq;
    }

    private function process_from(&$tokens)
    {
        $expression = "";
        $expr = array();
        $token_count=0;
        $table = "";
        $alias = "";

        $skip_next=false;
        $i=0;
        $join_type = '';
        $ref_type="";
        $ref_expr="";
        $base_expr="";
        $sub_tree = false;
        $subquery = "";

        $first_join=true;
        $modifier="";
        $saved_join_type="";

        foreach ($tokens as $token) {
            $base_expr = false;
            $upper = strtoupper(trim($token));

            if ($skip_next && $token) {
                $token_count++;
                $skip_next = false;
                continue;
            }
            if ($skip_next) {
                continue;
            }


            if (preg_match("/^\\s*\\(\\s*select/i", (string) $token)) {
                $type = 'subquery';
                $table = "DEPENDENT-SUBQUERY";
                $sub_tree = $this->parse($this->trimSubquery($token));
                $subquery = $token;
            }

            switch ($upper) {
                    case 'OUTER':
                    case 'LEFT':
                    case 'RIGHT':
                    case 'NATURAL':
                    case 'CROSS':
                    case ',':
                    case 'JOIN':
                        break;

                    default:
                        $expression .= $token == '' ? " " : $token;
                        if ($ref_type) {
                            $ref_expr .= $token == '' ? " " : $token;
                        }
                    break;
                }

            switch ($upper) {
                    case 'AS':
                        $token_count++;
                        $n=1;
                        $alias = "";
                        while ($alias == "") {
                            $alias = trim($tokens[$i+$n]);
                            ++$n;
                        }

                        continue;
                    break;

                    case 'INDEX':
                        if ($token_category == 'CREATE') {
                            $token_category = $upper;
                            continue 2;
                        }

                    break;

                    case 'USING':
                    case 'ON':
                        $ref_type = $upper;
                        $ref_expr = "";

                        // no break
                    case 'CROSS':
                    case 'USE':
                    case 'FORCE':
                    case 'IGNORE':
                    case 'INNER':
                    case 'OUTER':
                    #	$expression .= $token;
                        $token_count++;
                        continue;
                    break;



                    case 'FOR':
                        $token_count++;
                        $skip_next = true;
                        continue;
                    break;

                    case 'LEFT':
                    case 'RIGHT':
                    case 'STRAIGHT_JOIN':
                        $join_type=$saved_join_type;

                        $modifier = $upper . " ";
                        break;


                    case ',':
                        $modifier = 'CROSS';

                        // no break
                    case 'JOIN':

                        if ($first_join) {
                            $join_type = 'JOIN';
                            $saved_join_type = ($modifier ? $modifier : 'JOIN');
                        } else {
                            $new_join_type = ($modifier ? $modifier : 'JOIN');
                            $join_type = $saved_join_type;
                            $saved_join_type = $new_join_type;
                            unset($new_join_type);
                        }

                        $first_join = false;

                        if (!trim($alias)) {
                            $alias = $table;
                        }

                        if ($subquery) {
                            $sub_tree = $this->parse(trim($subquery, '()'));
                            $base_expr=$subquery;
                        }

                        if (substr(trim($table), 0, 1) == '(') {
                            $base_expr=$this->trimSubquery($table);
                            $join_type = 'JOIN';
                            $sub_tree = $this->process_from($this->split_sql($base_expr));
                            $alias="";
                        }


                        if ($join_type == "") {
                            $join_type='JOIN';
                        }
                        $expr[] = array('table'=>$table, 'alias'=>$alias,'join_type'=>$join_type,'ref_type'=> $ref_type,'ref_clause'=>$this->trimSubquery($ref_expr), 'base_expr' => $base_expr, 'sub_tree' => $sub_tree);
                        $modifier = "";
                        #$join_type=$saved_join_type;


                        $token_count = 0;
                        $table = $alias = $expression = $base_expr = $ref_type = $ref_expr = "";
                        $sub_tree=false;
                        $subquery = "";

                    break;


                    default:
                        if ($token === false || empty($token) || $token === "") {
                            continue;
                        }

                        if ($token_count == 0) {
                            if (!$table) {
                                $table = $token ;
                            }
                        } elseif ($token_count == 1) {
                            $alias = $token;
                        }
                        $token_count++;
                    break;
                }
            ++$i;
        }
        if (substr(trim($table), 0, 1) == '(') {
            $base_expr=$this->trimSubquery($table);
            $join_type = 'JOIN';
            $sub_tree = $this->process_from($this->split_sql($base_expr));
            $alias = "";
        } else {
            if (!trim($alias)) {
                $alias = $table;
            }
        }
        if ($join_type == "") {
            $saved_join_type='JOIN';
        }

        $expr[] = array('table'=>$table, 'alias'=>$alias,'join_type'=>$saved_join_type,'ref_type'=> $ref_type,'ref_clause'=> $this->trimSubquery($ref_expr), 'base_expr' => $base_expr, 'sub_tree' => $sub_tree);


        return $expr;
    }

    private function process_group(&$tokens, &$select)
    {
        $out=array();
        $expression = "";
        $direction="ASC";
        $type = "expression";
        if (!$tokens) {
            return false;
        }

        foreach ($tokens as $token) {
            switch (strtoupper($token)) {
                    case ',':
                        $expression = trim($expression);
                        if ($expression[0] != '`' || substr($expression, -1) != '`') {
                            $escaped = str_replace('`', '``', $expression);
                        } else {
                            $escaped = $expression;
                        }
                        $escaped = '`' . $escaped . '`';

                        if (is_numeric(trim($expression))) {
                            $type = 'pos';
                        } else {

                            #search to see if the expression matches an alias
                            foreach ($select as $clause) {
                                if ($clause['alias'] == $escaped) {
                                    $type = 'alias';
                                }
                            }

                            if (!$type) {
                                $type = "expression";
                            }
                        }

                        $out[]=array('type'=>$type,'base_expr'=>$expression,'direction'=>$direction);
                        $escaped = "";
                        $expression = "";
                        $direction = "ASC";
                        $type = "";
                    break;

                    case 'ASC':
                        $direction = "ASC";
                        break;
                    case 'DESC':
                        $direction = "DESC";
                        break;

                    default:
                        $expression .= $token == '' ? ' ' : $token;


                }
        }
        if ($expression) {
            $expression = trim($expression);
            if ($expression[0] != '`' || substr($expression, -1) != '`') {
                $escaped = str_replace('`', '``', $expression);
            } else {
                $escaped = $expression;
            }
            $escaped = '`' . $escaped . '`';

            if (is_numeric(trim($expression))) {
                $type = 'pos';
            } else {

                        #search to see if the expression matches an alias
                if (!$type && $select) {
                    foreach ($select as $clause) {
                        if (!is_array($clause)) {
                            continue;
                        }
                        if ($clause['alias'] == $escaped) {
                            $type = 'alias';
                        }
                    }
                } else {
                    $type="expression";
                }

                if (!$type) {
                    $type = "expression";
                }
            }

            $out[]=array('type'=>$type,'base_expr'=>$expression,'direction'=>$direction);
        }
        foreach ($out as &$term) {
            if (!empty($term['base_expr'])) {
                $term['sub_tree'] = array($this->process_select_expr($term['base_expr']));
            }
        }

        return $out;
    }

    /* Some sections are just lists of expressions, like the WHERE and HAVING clauses.  This function
               processes these sections.  Recursive.
    */
    private function process_expr_list($tokens)
    {
        $processed = [];
        $expr_type = "";
        $expr = array();
        $type = "";
        $prev_token = "";
        $skip_next = false;
        $sub_expr = "";

        $in_lists = array();
        foreach ($tokens as $key => $token) {
            if (strlen(trim($token))==0) {
                continue;
            }
            if ($skip_next) {
                $skip_next = false;
                continue;
            }

            $processed = false;
            $upper = strtoupper(trim($token));
            if (trim($token)) {
                $token=trim($token);
            }

            /* is it a subquery?*/
            if (preg_match("/^\\s*\\(\\s*SELECT/i", (string) $token)) {
                $type = 'subquery';
                #tokenize and parse the subquery.
                #we remove the enclosing parenthesis for the tokenizer
                $processed = $this->parse($this->trimSubquery($token));


            /* is it an inlist */
            } elseif ($upper[0] == '(' && substr($upper, -1) == ')') {
                if ($prev_token == 'IN') {
                    $type = "in-list";
                    $processed = $this->split_sql(substr((string) $token, 1, -1));
                    $list = array();
                    foreach ($processed as $v) {
                        if ($v == ',') {
                            continue;
                        }
                        $list[]=$v;
                    }
                    $processed = $list;
                    unset($list);
                    $prev_token = "";
                } elseif ($prev_token == 'AGAINST') {
                    $type = "match-arguments";
                    $list = $this->split_sql(substr((string) $token, 1, -1));
                    if ((is_countable($list) ? count($list) : 0) > 1) {
                        $match_mode = implode('', array_slice($list, 1));
                        $processed = array($list[0], $match_mode);
                    } else {
                        $processed = $list[0];
                    }
                    $prev_token = "";
                }

                /* it is either an operator, a colref or a constant */
            } else {
                switch ($upper) {
                    case 'AND':
                    case '&&':
                    case 'BETWEEN':
                    case 'AND':
                    case 'BINARY':
                    case '&':
                    case '~':
                    case '|':
                    case '^':
                    case 'CASE':
                    case 'WHEN':
                    case 'END':
                    case 'DIV':
                    case '/':
                    case '<=>':
                    case '=':
                    case '>=':
                    case '>':
                    case 'IS':
                    case 'NOT':
                    case 'NULL':
                    case '<<':
                    case '<=':
                    case '<':
                    case 'LIKE':
                    case '-':
                    case '%':
                    case '!=':
                    case '<>':
                    case 'REGEXP':
                    case '!':
                    case '||':
                    case 'OR':
                    case '+':
                    case '>>':
                    case 'RLIKE':
                    case 'SOUNDS':
                    case '*':
                    case '-':
                    case 'XOR':
                    case 'IN':
                            $processed = false;
                            $type = "operator";
                            break;
                    default:
                        switch ($token[0]) {
                            case "'":
                            case '"':
                                    $type = 'const';
                                    break;
                            case '`':
                                    $type = 'colref';
                                    break;

                            default:
                                if (is_numeric($token)) {
                                    $type = 'const';
                                } else {
                                    $type = 'colref';
                                }
                            break;

                        }
                        #$processed = $token;
                        $processed = false;
                    }
            }
            /* is a reserved word? */
            if (($type != 'operator' && $type != 'in-list' && $type != 'sub_expr') && in_array($upper, $this->reserved)) {
                $token = $upper;
                if (!in_array($upper, $this->functions)) {
                    $type = 'reserved';
                } else {
                    switch ($token) {
                            case 'AVG':
                            case 'SUM':
                            case 'COUNT':
                            case 'MIN':
                            case 'MAX':
                            case 'STDDEV':
                            case 'STDDEV_SAMP':
                            case 'STDDEV_POP':
                            case 'VARIANCE':
                            case 'VAR_SAMP':
                            case 'VAR_POP':
                            case 'GROUP_CONCAT':
                            case 'BIT_AND':
                            case 'BIT_OR':
                            case 'BIT_XOR':
                                $type = 'aggregate_function';
                                if (!empty($tokens[$key+1])) {
                                    $sub_expr = $tokens[$key+1];
                                }
                                #$skip_next=true;
                            break;

                            default:
                                $type = 'function';
                                if (!empty($tokens[$key+1])) {
                                    $sub_expr = $tokens[$key+1];
                                } else {
                                    $sub_expr="()";
                                }
                                #$skip_next=true;


                            break;
                        }
                }
            }

            if (!$type) {
                if ($upper[0] == '(') {
                    $local_expr = substr(trim($token), 1, -1);
                } else {
                    $local_expr = $token;
                }
                $processed = $this->process_expr_list($this->split_sql($local_expr));
                $type = 'expression';

                //					if(count($processed) == 1) {
//						$type = $processed[0]['expr_type'];
//						$base_expr  = $processed[0]['base_expr'];
//						$processed = $processed[0]['sub_tree'];
//					}
            }

            $sub_expr=trim($sub_expr);
            $sub_expr = "";

            $expr[] = array( 'expr_type' => $type, 'base_expr' => $token, 'sub_tree' => $processed);
            $prev_token = $upper;
            $expr_type = "";
            $type = "";
        }
        if ($sub_expr) {
            $processed['sub_tree'] = $this->process_expr_list($this->split_sql(substr($sub_expr, 1, -1)));
        }

        if (!is_array($processed)) {
            $processed = false;
        }

        if ($expr_type) {
            $expr[] = array( 'expr_type' => $type, 'base_expr' => $token, 'sub_tree' => $processed);
        }
        $mod = false;

        /*

        		for($i=0;$i<count($expr);++$i){
        			if($expr[$i]['expr_type'] == 'function' ||
        			   $expr[$i]['expr_type'] == 'aggregate_function') {
        				if(!empty($expr[$i+1])) {
        					$expr[$i]['sub_tree']=$expr[$i+1]['sub_tree'];
        					unset($expr[$i+1]);
        					$mod = 1;
        					++$i;  // BAD FORM TO MODIFY THE LOOP COUNTER
        				}
        			}

        		}

        */

        if ($mod) {
            $expr=array_values($expr);
        }


        return $expr;
    }

    private function process_update($tokens)
    {
    }

    private function process_delete($tokens)
    {
        $tables = array();
        $del = $tokens['DELETE'];

        foreach ($tokens['DELETE'] as $expression) {
            if ($expression != 'DELETE' && trim($expression, ' .*') != "" && $expression != ',') {
                $tables[] = trim($expression, '.* ');
            }
        }

        if (empty($tables)) {
            foreach ($tokens['FROM'] as $table) {
                $tables[] = $table['table'];
            }
        }

        $tokens['DELETE'] = array('TABLES' => $tables);

        return $tokens;
    }

    private function process_insert($tokens, $token_category = 'INSERT')
    {
        $table = "";
        $cols = "";

        $into = $tokens['INTO'];
        foreach ($into as $token) {
            if (!trim($token)) {
                continue;
            }
            if (!$table) {
                $table = $token;
            } elseif (!$cols) {
                $cols = $token;
            }
        }

        if (!$cols) {
            $cols = 'ALL';
        } else {
            $cols = explode(",", $this->trimSubquery($cols));
        }
        unset($tokens['INTO']);
        $tokens[$token_category] =  array('table'=>$table, 'cols'=>$cols);
        return $tokens;
    }


    private function load_reserved_words()
    {
        $this->functions = array(
                'abs',
                'acos',
                'adddate',
                'addtime',
                'aes_encrypt',
                'aes_decrypt',
                'against',
                'ascii',
                'asin',
                'atan',
                'avg',
                'benchmark',
                'bin',
                'bit_and',
                'bit_or',
                'bitcount',
                'bitlength',
                'cast',
                'ceiling',
                'char',
                'char_length',
                'character_length',
                'charset',
                'coalesce',
                'coercibility',
                'collation',
                'compress',
                'concat',
                'concat_ws',
                'conection_id',
                'conv',
                'convert',
                'convert_tz',
                'cos',
                'cot',
                'count',
                'crc32',
                'curdate',
                'current_user',
                'currval',
                'curtime',
                'database',
                'date_add',
                'date_diff',
                'date_format',
                'date_sub',
                'day',
                'dayname',
                'dayofmonth',
                'dayofweek',
                'dayofyear',
                'decode',
                'default',
                'degrees',
                'des_decrypt',
                'des_encrypt',
                'elt',
                'encode',
                'encrypt',
                'exp',
                'export_set',
                'extract',
                'field',
                'find_in_set',
                'floor',
                'format',
                'found_rows',
                'from_days',
                'from_unixtime',
                'get_format',
                'get_lock',
                'group_concat',
                'greatest',
                'hex',
                'hour',
                'if',
                'ifnull',
                'in',
                'inet_aton',
                'inet_ntoa',
                'insert',
                'instr',
                'interval',
                'is_free_lock',
                'is_used_lock',
                'last_day',
                'last_insert_id',
                'lcase',
                'least',
                'left',
                'length',
                'ln',
                'load_file',
                'localtime',
                'localtimestamp',
                'locate',
                'log',
                'log2',
                'log10',
                'lower',
                'lpad',
                'ltrim',
                'make_set',
                'makedate',
                'maketime',
                'master_pos_wait',
                'match',
                'max',
                'md5',
                'microsecond',
                'mid',
                'min',
                'minute',
                'mod',
                'month',
                'monthname',
                'nextval',
                'now',
                'nullif',
                'oct',
                'octet_length',
                'old_password',
                'ord',
                'password',
                'period_add',
                'period_diff',
                'pi',
                'position',
                'pow',
                'power',
                'quarter',
                'quote',
                'radians',
                'rand',
                'release_lock',
                'repeat',
                'replace',
                'reverse',
                'right',
                'round',
                'row_count',
                'rpad',
                'rtrim',
                'sec_to_time',
                'second',
                'session_user',
                'sha',
                'sha1',
                'sign',
                'soundex',
                'space',
                'sqrt',
                'std',
                'stddev',
                'stddev_pop',
                'stddev_samp',
                'strcmp',
                'str_to_date',
                'subdate',
                'substr',
                'substring',
                'substring_index',
                'subtime',
                'sum',
                'sysdate',
                'system_user',
                'tan',
                'time',
                'timediff',
                'timestamp',
                'timestampadd',
                'timestampdiff',
                'time_format',
                'time_to_sec',
                'to_days',
                'trim',
                'truncate',
                'ucase',
                'uncompress',
                'uncompressed_length',
                'unhex',
                'unix_timestamp',
                'upper',
                'user',
                'utc_date',
                'utc_time',
                'utc_timestamp',
                'uuid',
                'var_pop',
                'var_samp',
                'variance',
                'version',
                'week',
                'weekday',
                'weekofyear',
                'year',
                'yearweek');

        /* includes functions */
        $this->reserved = array(
            'abs',
            'acos',
            'adddate',
            'addtime',
            'aes_encrypt',
            'aes_decrypt',
            'against',
            'ascii',
            'asin',
            'atan',
            'avg',
            'benchmark',
            'bin',
            'bit_and',
            'bit_or',
            'bitcount',
            'bitlength',
            'cast',
            'ceiling',
            'char',
            'char_length',
            'character_length',
            'charset',
            'coalesce',
            'coercibility',
            'collation',
            'compress',
            'concat',
            'concat_ws',
            'conection_id',
            'conv',
            'convert',
            'convert_tz',
            'cos',
            'cot',
            'count',
            'crc32',
            'curdate',
            'current_user',
            'currval',
            'curtime',
            'database',
            'date_add',
            'date_diff',
            'date_format',
            'date_sub',
            'day',
            'dayname',
            'dayofmonth',
            'dayofweek',
            'dayofyear',
            'decode',
            'default',
            'degrees',
            'des_decrypt',
            'des_encrypt',
            'elt',
            'encode',
            'encrypt',
            'exp',
            'export_set',
            'extract',
            'field',
            'find_in_set',
            'floor',
            'format',
            'found_rows',
            'from_days',
            'from_unixtime',
            'get_format',
            'get_lock',
            'group_concat',
            'greatest',
            'hex',
            'hour',
            'if',
            'ifnull',
            'in',
            'inet_aton',
            'inet_ntoa',
            'insert',
            'instr',
            'interval',
            'is_free_lock',
            'is_used_lock',
            'last_day',
            'last_insert_id',
            'lcase',
            'least',
            'left',
            'length',
            'ln',
            'load_file',
            'localtime',
            'localtimestamp',
            'locate',
            'log',
            'log2',
            'log10',
            'lower',
            'lpad',
            'ltrim',
            'make_set',
            'makedate',
            'maketime',
            'master_pos_wait',
            'match',
            'max',
            'md5',
            'microsecond',
            'mid',
            'min',
            'minute',
            'mod',
            'month',
            'monthname',
            'nextval',
            'now',
            'nullif',
            'oct',
            'octet_length',
            'old_password',
            'ord',
            'password',
            'period_add',
            'period_diff',
            'pi',
            'position',
            'pow',
            'power',
            'quarter',
            'quote',
            'radians',
            'rand',
            'release_lock',
            'repeat',
            'replace',
            'reverse',
            'right',
            'round',
            'row_count',
            'rpad',
            'rtrim',
            'sec_to_time',
            'second',
            'session_user',
            'sha',
            'sha1',
            'sign',
            'soundex',
            'space',
            'sqrt',
            'std',
            'stddev',
            'stddev_pop',
            'stddev_samp',
            'strcmp',
            'str_to_date',
            'subdate',
            'substring',
            'substring_index',
            'subtime',
            'sum',
            'sysdate',
            'system_user',
            'tan',
            'time',
            'timediff',
            'timestamp',
            'timestampadd',
            'timestampdiff',
            'time_format',
            'time_to_sec',
            'to_days',
            'trim',
            'truncate',
            'ucase',
            'uncompress',
            'uncompressed_length',
            'unhex',
            'unix_timestamp',
            'upper',
            'user',
            'utc_date',
            'utc_time',
            'utc_timestamp',
            'uuid',
            'var_pop',
            'var_samp',
            'variance',
            'version',
            'week',
            'weekday',
            'weekofyear',
            'year',
            'yearweek',
            'add',
            'all',
            'alter',
            'analyze',
            'and',
            'as',
            'asc',
            'asensitive',
            'auto_increment',
            'bdb',
            'before',
            'berkeleydb',
            'between',
            'bigint',
            'binary',
            'blob',
            'both',
            'by',
            'call',
            'cascade',
            'case',
            'change',
            'char',
            'character',
            'check',
            'collate',
            'column',
            'columns',
            'condition',
            'connection',
            'constraint',
            'continue',
            'create',
            'cross',
            'current_date',
            'current_time',
            'current_timestamp',
            'cursor',
            'database',
            'databases',
            'day_hour',
            'day_microsecond',
            'day_minute',
            'day_second',
            'dec',
            'decimal',
            'declare',
            'default',
            'delayed',
            'delete',
            'desc',
            'describe',
            'deterministic',
            'distinct',
            'distinctrow',
            'div',
            'double',
            'drop',
            'else',
            'elseif',
            'enclosed',
            'escaped',
            'exists',
            'exit',
            'explain',
            'false',
            'fetch',
            'fields',
            'float',
            'for',
            'force',
            'foreign',
            'found',
            'frac_second',
            'from',
            'fulltext',
            'grant',
            'group',
            'having',
            'high_priority',
            'hour_microsecond',
            'hour_minute',
            'hour_second',
            'if',
            'ignore',
            'in',
            'index',
            'infile',
            'inner',
            'innodb',
            'inout',
            'insensitive',
            'insert',
            'int',
            'integer',
            'interval',
            'into',
            'io_thread',
            'is',
            'iterate',
            'join',
            'key',
            'keys',
            'kill',
            'leading',
            'leave',
            'left',
            'like',
            'limit',
            'lines',
            'load',
            'localtime',
            'localtimestamp',
            'lock',
            'long',
            'longblob',
            'longtext',
            'loop',
            'low_priority',
            'master_server_id',
            'match',
            'mediumblob',
            'mediumint',
            'mediumtext',
            'middleint',
            'minute_microsecond',
            'minute_second',
            'mod',
            'natural',
            'not',
            'no_write_to_binlog',
            'null',
            'numeric',
            'on',
            'optimize',
            'option',
            'optionally',
            'or',
            'order',
            'out',
            'outer',
            'outfile',
            'precision',
            'primary',
            'privileges',
            'procedure',
            'purge',
            'read',
            'real',
            'references',
            'regexp',
            'rename',
            'repeat',
            'replace',
            'require',
            'restrict',
            'return',
            'revoke',
            'right',
            'rlike',
            'second_microsecond',
            'select',
            'sensitive',
            'separator',
            'set',
            'show',
            'smallint',
            'some',
            'soname',
            'spatial',
            'specific',
            'sql',
            'sqlexception',
            'sqlstate',
            'sqlwarning',
            'sql_big_result',
            'sql_calc_found_rows',
            'sql_small_result',
            'sql_tsi_day',
            'sql_tsi_frac_second',
            'sql_tsi_hour',
            'sql_tsi_minute',
            'sql_tsi_month',
            'sql_tsi_quarter',
            'sql_tsi_second',
            'sql_tsi_week',
            'sql_tsi_year',
            'ssl',
            'starting',
            'straight_join',
            'striped',
            'table',
            'tables',
            'terminated',
            'then',
            'timestampadd',
            'timestampdiff',
            'tinyblob',
            'tinyint',
            'tinytext',
            'to',
            'trailing',
            'true',
            'undo',
            'union',
            'unique',
            'unlock',
            'unsigned',
            'update',
            'usage',
            'use',
            'user_resources',
            'using',
            'utc_date',
            'utc_time',
            'utc_timestamp',
            'values',
            'varbinary',
            'varchar',
            'varcharacter',
            'varying',
            'when',
            'where',
            'while',
            'with',
            'write',
            'xor',
            'year_month',
            'zerofill'
            );
        $reservedCount = count($this->reserved);

        for ($i=0;$i<$reservedCount;++$i) {
            $this->reserved[$i]=strtoupper($this->reserved[$i]);
            if (!empty($this->functions[$i])) {
                $this->functions[$i] = strtoupper($this->functions[$i]);
            }
        }
    }
} // END CLASS
