Мульти-запросов парсер PHP для SQL-файлов


У меня есть внешний проект, который должен выполнить sql-файл, содержащий несколько запросов SQL (немного типа mysqldump, если вам нравится, но созданные пользователем, с какого-либо заявления/определение SQL).

В PHP mysqli, то() не позволяет мульти-запросы, и эта проблема возникает довольно часто, когда (например) обновление сайта-модуль/виджет на более высокую версию, и там обновление.sql файл некоторых видов. Аналогично для де-инсталляции.

Учитывая следующее (1):

//DB connection values
$sHost = "localhost";
$sName = "test";
$sUser = "";
$sPass = "";
$sPort = 3307;


//The following could be retrieved using file_get_contents, or a file streamer
$sFileContents = <<<EOT
 -- This is the first comment
SELECT * FROM dl_bookmarks WHERE iID=3;

 /* This is the second comment */
SELECT * FROM dl_bookmarks WHERE sTitle="\"Paragon\" Initiative Enterprises Software consulting and web development for businesses \\\\ \'smes\'";

 # This is the third comment
SELECT * FROM dl_bookmarks WHERE sTitle LIKE '\"xDEEP\" Diving Equipment; Scuba Gear; Single tank BC; Side Mount; Double tank BCD; Diving computer \'equipment\'';
EOT;

Идеальным решением будет выполнить sql-файл в командной строке, как в следующем коде демонстрируется (с выше примера выше):

//Variant 1: Run a local SQL file. Since we stored our SQL contents in a 
//variable (could have been retrieved before using eg. file_get_contents), 
//we need to temporarily create a file for this

$sTempFile = tempnam(sys_get_temp_dir(), 'Sql');

//Create the temp file
if(!file_put_contents($sTempFile, $sFileContents)) {
    trigger_error("Failed to create temporary file", E_USER_ERROR);
}

//Assemble the command
$sCommand = 'mysql'
        . ' --host=' . $sHost
        . ' --port=' . $sPort
        . ' --user=' . $sUser
        . ' --password=' . $sPass
        . ' --database=' . $sName
        . ' --execute="SOURCE ' . $sTempFile . '"'
                ;
$sOutput = shell_exec($sCommand);

//Cleanup: remove the temp file
if(!unlink($sTempFile)) {
    trigger_error("Failed to remove temporary file", E_USER_ERROR);
}

...но некоторые проекты на общих серверах или других серверах с ограниченным доступом, где выполнение оболочки не допускается, или команда MySQL может быть недоступна.

Поэтому, чтобы избежать импорта тяжелых внешних парсинг библиотек с тяжелой поддержкой для решения проблемы небольшого сайта-обзоры проблемой, многие проекты петли через sql-файл, и разделить на всех ; символ в новый запрос, который выполняется индивидуально. Поскольку SQL значения могут содержать эти символы тоже, этот процесс не удается, в особых случаях, а разрез делается только для символов в конце строки (но это еще не для многострочных значений SQL с этим персонаж, появляющийся в конце.)

Мое предложенное решение состоит в том, чтобы правильно проанализировать SQL-файлов/контента, и разделить запросах где запросы действительно конец, согласно стандартам SQL. Я прошу помочь с этим.

Мой код (с выше первом примере блок выше):

//Variant 2: Run a parser

//Connect to the database
$rMysqlI = new mysqli("localhost", "", "", "test", $sPort);
if ($rMysqlI->connect_errno) {
    trigger_error("Failed to connect to MySQL: (" . $rMysqlI->connect_errno . ") " . $rMysqlI->connect_error, E_USER_ERROR);
}



//START_OF_PARSER

$iCur = 0;            //Current character pointer inside the SQL content
$iInside = 0;         //The context, in which the pointer is currently located (is the pointer inside a 
                      //comment, an SQL query, or deeper into an SQL query value?)
$sBuffer = "";        //The buffer of the next individual query
$aQueries = array();  //The list of queries
while($iCur < strlen($sFileContents)) {

    switch ($iInside) {
        case 0: //Inside query-context
            //Change context: Comments beginning with --
            if(substr($sFileContents, $iCur, 2) === "--") {
                $iCur++;
                $iInside = 2;

                //Change context: Comments beginning with /*
            } elseif(substr($sFileContents, $iCur, 2) === "/*") {
                $iCur++;
                $iInside = 3;

                //Change context: Comments beginning with #
            } elseif(substr($sFileContents, $iCur, 1) === "#") {
                $iInside = 2;

                //Separator for a new query
            } elseif(substr($sFileContents, $iCur, 1) === ";") {
                $aQueries[] = trim($sBuffer); //$sBuffer;  //Add current buffer to a unique array query item
                $sBuffer = "";  //Start a new buffer

                //Change context: query values opened with '
            } elseif(substr($sFileContents, $iCur, 1) === "'") {
                $sBuffer .= substr($sFileContents, $iCur, 1);
                $iInside = 1;

                //Change context: query values opened with "
            } elseif(substr($sFileContents, $iCur, 1) === '"') {
                $sBuffer .= substr($sFileContents, $iCur, 1);
                $iInside = 4;

                //Not a special character
            } else {
                $sBuffer .= substr($sFileContents, $iCur, 1);
            }
            break;

        case 1: //Inside value-context, ending with '

            //Escaping character found within the query-value
            if(substr($sFileContents, $iCur, 1) === "\\") {
                $sBuffer .= substr($sFileContents, $iCur, 2);
                $iCur++;  //Skip next char

                //The ending character for the query-value is found
            } elseif(substr($sFileContents, $iCur, 1) === "'") {
                $sBuffer .= substr($sFileContents, $iCur, 1);
                $iInside = 0;

                //Not a special character
            } else {
                $sBuffer .= substr($sFileContents, $iCur, 1);
            }
            break;

        case 4: //Inside value-context, ending with "

            //Escaping character found within the query-value
            if(substr($sFileContents, $iCur, 1) === "\\") {
                $sBuffer .= substr($sFileContents, $iCur, 2);
                $iCur = $iCur + 1;  //Skip next char

                //The ending character for the query-value is found
            } elseif(substr($sFileContents, $iCur, 1) === '"') {
                $sBuffer .= substr($sFileContents, $iCur, 1);
                $iInside = 0;

                //Not a special character
            } else {
                $sBuffer .= substr($sFileContents, $iCur, 1);
            }
            break;

        case 2: //Inside comment-context, ending with newline

            //A two-character newline is found, signalling the end of the comment
            if(substr($sFileContents, $iCur, 2) === "\r\n") {
                $iCur++;
                $iInside = 0;

                //A single-character newline is found, signalling the end of the comment
            } elseif(substr($sFileContents, $iCur, 1) === "\n" || substr($sFileContents, $iCur, 1) === "\r") {
                $iInside = 0;
            }
            break;

        case 3: //Inside comment-context, ending with */

            //A two-character */ is found, signalling the end of the comment
            if(substr($sFileContents, $iCur, 2) === "*/") {
                $iCur++;
                $iInside = 0;
            }
            break;

        default:
            break;
    }
    $iCur++;
}

//END_OF_PARSER

//Preview our results
foreach($aQueries as $sQuery) {
    if (!$rMysqlI->query($sQuery)) {
        echo "ERROR \"{$sQuery}\": (" . $rMysqlI->errno . ") " . $rMysqlI->error . "<br />", E_USER_ERROR;
    } else {
        echo "SUCCESS \"{$sQuery}\"<br />", E_USER_ERROR;
    }
}

Для моей проблемной зоной, я в настоящее время интересует только то, что между START_OF_PARSER и END_OF_PARSER, т. е.. парсер бит, или блок кода, который преобразует содержимое SQL в массив отдельных запросов, которые могут/могут быть выполнены в индивидуальном позже (как выше по каждому элементу делает), производить такой же результат, как если Вариант 1 блок код был использован.

Проблемы, которые я имею о моем коде:

  • Запрос-стоимость инкапсуляции символы: они всегда ' или " или другой запрос-значение инкапсуляция символов?
  • Запрос-стоимость экранирование символов: я не думаю, что его хорошая идея, чтобы взять свои навыки спасаясь (из соображений безопасности..), а игнорирование проблемы безопасности для Теперь, я предполагаю, глядя на одного побега символ (обратный слеш ) достаточно, чтобы определить, какой запрос-значение символа действительно конец запроса-значение символа. Но это слеш только Escape-символа (или метод), в других диалектах SQL?
  • Игнорируя замечание: удалять комментарии, я ищу /* и */, - и строки, # и символы новой строки. Достаточно ли этого?
  • ДБ абстракция: я предположил, что MySQL, но я знаю, что проблемы я обращаюсь за PearDB, что может быть Oracle или PostgreSQL или LiteSQL или др. У этих диалектов SQL также имеют одни и те же персонажи комментарии, спасаясь, стоимость-инкапсуляция, и т. д? Это будет работать для большинства из тех ДБС ?

Выполнение этот код, кажется, работает под Windows (и Linux тоже предположительно).



685
0
задан 13 февраля 2018 в 05:02 Источник Поделиться
Комментарии
1 ответ

Почему вы пытаетесь построить парсер SQL? Это выглядит как много работы и большой потенциал неустойчивости для случая использования лучше подходят для других подходов. Вы имеете дело с большой SQL-скрипт. Выполнить его как таковой. Не разбирать его на части.

0
ответ дан 14 февраля 2018 в 03:02 Источник Поделиться