<?php
/**
 * SQLProcessor.php
 *
 * This file implements the processor for the base SQL statements.
 *
 * Copyright (c) 2010-2012, Justin Swanhart
 * with contributions by André Rothe <arothe@phosco.info, phosco@gmx.de>
 *
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification,
 * are permitted provided that the following conditions are met:
 *
 *   * Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 *   * Redistributions in binary form must reproduce the above copyright notice,
 *     this list of conditions and the following disclaimer in the documentation
 *     and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
 * SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 */

require_once(dirname(__FILE__) . '/AbstractProcessor.php');
require_once(dirname(__FILE__) . '/SQLChunkProcessor.php');

/**
 *
 * This class processes the base SQL statements.
 *
 * @author arothe
 *
 */
class SQLProcessor extends SQLChunkProcessor {

    /*
     * This function breaks up the SQL statement into logical sections.
     * Some sections are then further handled by specialized processors.
     */
    public function process($tokens) {
        $prev_category = "";
        $token_category = "";
        $skip_next = 0;
 //       $out = false;
        $out = array();

        $tokenCount = count($tokens);
        for ($tokenNumber = 0; $tokenNumber < $tokenCount; ++$tokenNumber) {

            $token = $tokens[$tokenNumber];
            $trim = trim($token); // this removes also \n and \t!

            // if it starts with an "(", it should follow a SELECT
            if ($trim !== "" && $trim[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 > 0) {
                if ($trim === "") {
                    if ($token_category !== "") { # is this correct??
                        $out[$token_category][] = $token;
                    }
                    continue;
                }
                #to skip the token we replace it with whitespace
                $trim = "";
                $token = "";
                $skip_next--;
                if ($skip_next > 0) {
                    continue;
                }
            }

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

            /* Tokens that get their own sections. These keywords have subclauses. */
			case 'IGNORE':
            case 'TRIGGER':
            case 'SELECT':
            case 'ORDER':
            case 'DUPLICATE':
            case 'VALUES':
            case 'GROUP':
            case 'HAVING':
            case 'WHERE':
            case 'CALL':
            case 'PROCEDURE':
            case 'FUNCTION':
            case 'SERVER':
            case 'LOGFILE':
            case 'DEFINER':
            case 'RETURNS':
            case 'TABLESPACE':
            case 'DO':
            case 'FLUSH':
            case 'KILL':
            case 'RESET':
            case 'STOP':
            case 'PURGE':
            case 'EXECUTE':
            case 'PREPARE':
            case 'DEALLOCATE':
                if ($trim === 'DEALLOCATE') {
                    $skip_next = 1;
                }
                $token_category = $upper;
                break;

            case 'SET':
                if ($token_category !== 'TABLE') {
                    $token_category = $upper;
                }
                break;

            case 'LIMIT':
            case 'PLUGIN':
            # no separate section
                if ($token_category === 'SHOW') {
                    break;
                }
                $token_category = $upper;
                break;

            case 'FROM':
            # this FROM is different from FROM in other DML (not join related)
                if ($token_category === 'PREPARE') {
                    break;
                }
                # no separate section
                if ($token_category === 'SHOW') {
                    break;
                }
                $token_category = $upper;
                break;

            case 'EXPLAIN':
            case 'DESCRIBE':
            case 'SHOW':
                $token_category = $upper;
                break;

            case 'DESC':
                if ($token_category === '') {
                    // short version of DESCRIBE
                    $token_category = $upper;
                }
                // else direction of ORDER-BY
                break;

            case 'RENAME':
            // jump over TABLE keyword
                $token_category = $upper;
                $skip_next = 1;
                continue 2;

            case 'DATABASE':
            case 'SCHEMA':
                if ($prev_category === 'DROP') {
                    continue 2;
                }
                if ($prev_category === 'SHOW') {
                    continue 2;
                }
                $token_category = $upper;
                break;

            case 'EVENT':
            // issue 71
                if ($prev_category === 'DROP' || $prev_category === 'ALTER' || $prev_category === 'CREATE') {
                    $token_category = $upper;
                }
                break;

            case 'DATA':
            // prevent wrong handling of DATA as keyword
                if ($prev_category === 'LOAD') {
                    $token_category = $upper;
                }
                break;

            case 'INTO':
				// Check for a 'CREATE TRIGGER' statement with 'FOR EACH ROW INSERT INTO' or 'FOR EACH ROW DELETE' clause
  				if (isset($out['TRIGGER']) && isset($out['CREATE']))
					$out[$upper][0] = '';
//					$out[$token_category][] = $upper;
//					continue 2;
//					break;

	   // prevent wrong handling of CACHE within LOAD INDEX INTO CACHE...
                if ($prev_category === 'LOAD') {
                    $out[$prev_category][] = $upper;
                    continue 2;
                }
                $token_category = $upper;
                break;

            case 'USER':
            // prevent wrong processing as keyword
                if ($prev_category === 'CREATE' || $prev_category === 'RENAME' || $prev_category === 'DROP') {
                    $token_category = $upper;
                }
                break;

            case 'VIEW':
            // prevent wrong processing as keyword
                if ($prev_category === 'CREATE' || $prev_category === 'ALTER' || $prev_category === 'DROP') {
                    $token_category = $upper;
                }
                break;

            case 'UPDATE':
            // prevent wrong processing as keyword
                if (empty($prev_category)) {
                    $token_category = $upper;
                }
                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 'ALTER':
            case 'TRUNCATE':
            case 'OPTIMIZE':
            case 'GRANT':
            case 'REVOKE':
            case 'HANDLER':
            case 'LOAD':
            case 'ROLLBACK':
            case 'SAVEPOINT':
            case 'UNLOCK':
            case 'INSTALL':
            case 'UNINSTALL':
            case 'ANALZYE':
            case 'BACKUP':
            case 'CHECKSUM':
            case 'REPAIR':
            case 'RESTORE':
            case 'USE':
            case 'HELP':
                $token_category = $upper;
                // set the category in case these get subclauses in a future version of MySQL
                $out[$upper][0] = $upper;
                continue 2;
				break;

            case 'REPLACE':
                if ($prev_category === 'TABLE') {
                    # part of the CREATE TABLE statement
                    $out[$prev_category][] = $upper;
                    continue 2;
                }
                // set the category in case these get subclauses in a future version of MySQL
                $token_category = $upper;
                $out[$upper][0] = $upper;
                continue 2;

            case 'IGNORE':
                if ($prev_category === 'TABLE') {
                    # part of the CREATE TABLE statement
                    $out[$prev_category][] = $upper;
                    continue 2;
                }
                $out['OPTIONS'][] = $upper;
                continue 2;
                break;

            case 'CHECK':
                if ($prev_category === 'TABLE') {
                    $out[$prev_category][] = $upper;
                    continue 2;
                }
                $token_category = $upper;
                $out[$upper][0] = $upper;
                continue 2;

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

            case 'TABLE':
                if ($prev_category === 'CREATE') {
                    $out[$prev_category][] = $upper;
                    $token_category = $upper;
                }
                break;

            case 'TEMPORARY':
                if ($prev_category === 'CREATE') {
                    $out[$prev_category][] = $upper;
                    $token_category = $prev_category;
                    continue 2;
                }
                break;

            case 'IF':
                if (($prev_category === 'TABLE') && isset($out['CREATE'])) {
                    $token_category = 'CREATE';
                    $out[$token_category] = array_merge($out[$token_category], $out[$prev_category]);
                    $out[$prev_category] = array();
                    $out[$token_category][] = $upper;
                    $prev_category = $token_category;
                    continue 2;
                }
                break;

            case 'NOT':
                if (($prev_category === 'CREATE') || ($prev_category === 'DROP')){
                    $token_category = $prev_category;
                    $out[$prev_category][] = $upper;
                    continue 2;
                }
                break;

            case 'EXISTS':
                if (($prev_category === 'CREATE') || ($prev_category === 'DROP')){
                    $out[$prev_category][] = $upper;
                    $prev_category = $token_category = 'TABLE';
                    continue 2;
                }
                break;

            case 'CACHE':
                if ($prev_category === "" || $prev_category === 'RESET' || $prev_category === 'FLUSH'
                        || $prev_category === 'LOAD') {
                    $token_category = $upper;
                    continue 2;
                }
                break;

            /* This is either LOCK TABLES or SELECT ... LOCK IN SHARE MODE */
            case 'LOCK':
                if ($token_category === "") {
                    $token_category = $upper;
                    $out[$upper][0] = $upper;
                } else {
                    $trim = 'LOCK IN SHARE MODE';
                    $skip_next = 3;
                    $out['OPTIONS'][] = $trim;
                }
                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;
 //                    continue 2;
                 }
                break;

            case 'FOR':
				// Check for a 'CREATE TRIGGER' statement with 'FOR EACH ROW' clause
				if (isset($out['TRIGGER']) && isset($out['CREATE'])) {
                   $token_category = $upper;
 					continue 2;
					break;
				}

               if ($prev_category === 'SHOW') {
                    continue 2;
                }

                $skip_next = 1;
                $out['OPTIONS'][] = 'FOR UPDATE';
                continue 2;
                break;

            case 'INSERT':
				// Check for a 'CREATE TRIGGER' statement with 'FOR EACH ROW INSERT' clause
				if (isset($out['TRIGGER']) && isset($out['CREATE'])) {
					if (isset($out['FOR'])) {
						$out['FOR'][] = $upper;
						continue 2;
						break;
					}
				}
               $token_category = $upper;
                // set the category in case these get subclauses in a future version of MySQL
                $out[$upper][0] = $upper;
                continue 2;
				break;

            case 'DELETE':
				// Check for a 'CREATE TRIGGER' statement with 'FOR EACH ROW DELETE' clause
				if (isset($out['TRIGGER']) && isset($out['CREATE'])) {
					if (isset($out['FOR'])) {
						$out['FOR'][] = $upper;
						continue 2;
						break;
					}
				}
				if ($token_category === "") {
                    $token_category = $upper;
					$out[$upper] = $upper;
					continue 2;
					break;
                }
                if ($token_category === 'DUPLICATE') {
                    continue 2;
                }
                break;

            case 'START':
                $trim = "BEGIN";
                $out[$upper][0] = $upper;
                $skip_next = 1;
                break;

            /* These tokens are ignored. */
            case 'TO':
                if ($token_category === 'RENAME') {
                    break;
                }
            case 'BY':
            case 'ALL':
            case 'SHARE':
            case 'MODE':
            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 'LOW_PRIORITY':
            case 'DELAYED':
            case 'FORCE':
            case 'QUICK':
                $out['OPTIONS'][] = $upper;
                continue 2;
                break;

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

            case 'AS':
                break;

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

            default:
                break;
            }

            // remove obsolete category after union (empty category because of
            // empty token before select)
			// Make sure we carry-on as usual in a TRIGGER statement with INSERT
            if (($token_category !== "" && ($prev_category === $token_category))
				|| ($prev_category === 'TRIGGER' && ($token_category === 'INSERT'))) {
//					|| ($prev_category === '' && ($token_category === 'DELETE'))) {

				if (!isset($out[$token_category]))
					$out[$token_category] = array();
				
				if (!is_array($out[$token_category]))
					$out[$token_category] = array($out[$token_category]);
				
                $out[$token_category][] = $token;
            }

            $prev_category = $token_category;
        }

        return parent::process($out);
    }
}
?>