Как получить список SQL параметров запроса для PHP OCI? - PullRequest
1 голос
/ 14 января 2020

Мое приложение выполняет пользовательские операторы SQL, которые содержат параметры запроса. Чтобы определить имена параметров, которые должны быть переданы oci_bind_by_name, я использую простой шаблон reg-ex, такой как /:\w+/, но это не работает для строковых литералов и комментариев, содержащихся в операторе SQL.

BEGIN
   /* some unused :param here */
   SELECT 'some other :param there' FROM foo;
END;

Обработка обнаружения строкового литерала и комментариев с помощью большего количества шаблонов reg-ex кажется плохой идеей, если подумать о еще более неприятных примерах, таких как:

BEGIN
   SELECT '/* some comment :literals --' FROM foo;
   -- some more comment :literals */
END;

Есть ли какой-нибудь способ получить требуемые имена параметров запроса для привязки с помощью OCI8 функции? Какие еще возможности существуют, не прибегая к ручному анализу SQL в коде пользователя?

Ответы [ 2 ]

1 голос
/ 21 января 2020

Мой код ниже не является отличным способом решения этой проблемы. Прежде чем использовать этот код, продолжайте искать более официальное решение.

Похоже, что у OCI есть функция для динамического извлечения имен связывания через функцию OCIStmtGetBindInfo . Однако, похоже, что эта функция недоступна в default PHP functions . Возможно, есть другие, более продвинутые способы подключения PHP к Oracle, которые обеспечивают необходимую функцию, но я не знаю достаточно об OCI или PHP, чтобы найти их.


Если вы готовы к не очень хорошему решению, вы можете использовать мою программу с открытым исходным кодом plsql_lexer , чтобы найти имена переменных связывания. Лексер разбивает операторы SQL на маленькие токены и обрабатывает сложные синтаксические проблемы, такие как комментарии и строки. Результаты должны быть намного более точными, чем использование нескольких регулярных выражений.

Недостатком является то, что программа не является полным анализатором, и вам приходится иметь дело с примитивными токенами. В этом случае относительно легко найти 99,9999% переменных связывания с помощью одного оператора SQL. После установки программы поместите ваш SQL в середину следующего оператора SELECT:

--Find bind variables.
--(Words or numerics that were immediately preceded (excluding whitespace) by a colon.)
select to_char(value) bind_variable_name
from
(
    --Get previous token.
    select type, value, first_char_position,
        lag(to_char(type)) over (order by first_char_position) previous_type
    from
    (
        --Convert to tokens, ignore whitespace.
        select type, value, first_char_position
        from table(plsql_lexer.lex(
            q'[
                --Here's the actual SQL statement you care about.
                --/*:fake_bind1*/
                select 1 a
                from dual
                where 1 = : real_bind_1 and :real_bind_2 = ':fake_bind_2'
            ]'))
        where type not in ('whitespace')
        order by first_char_position
    )
)
where type in ('numeric', 'word')
    and previous_type = ':'
order by first_char_position;


BIND_VARIABLE_NAME
------------------
real_bind_1
real_bind_2

Могут быть некоторые странные случаи, которые этот код не обрабатывает. Например, переменная связывания может быть идентификатором в кавычках, вам может потребоваться обработать двойные кавычки. И приведенный выше код не обрабатывает показатели . С другой стороны, я буквально никогда не видел ни одной из этих функций, так что это может не иметь значения для вас. Тщательно проверьте.

0 голосов
/ 23 января 2020

Наконец, я написал небольшой конечный автомат для анализа параметров связывания оператора SQL и поместил его во вспомогательный класс, чтобы он не конфликтовал с другими глобальными переменными:

class SqlBindNames {
   private static function isLineBreak($ch) {
      return (($ch === "\r") || ($ch === "\n"));
   }

   private static function isIdentChar($ch) {
      return (($ch >= 'a') && ($ch <= 'z')) ||
             (($ch >= 'A') && ($ch <= 'Z')) ||
             (($ch >= '0') && ($ch <= '9')) ||
             ($ch === '_');
   }

   private const QUOTE_SINGLE_CHR    = '\'';
   private const QUOTE_DOUBLE_CHR    = '"';
   private const COMMENT_LINE_STR    = "--";
   private const COMMENT_BEGIN_STR   = "/*";
   private const COMMENT_END_STR     = "*/";
   private const BIND_START_CHR      = ':';

   private const MODE_NORMAL         = 0;
   private const MODE_QUOTE_SINGLE   = 1;
   private const MODE_QUOTE_DOUBLE   = 2;
   private const MODE_COMMENT_LINE   = 3;
   private const MODE_COMMENT_MULTI  = 4;
   private const MODE_BIND_VARNAME   = 5;

   public static function getSqlBindNames(string $sql, bool $unique = true) {
      $mode = self::MODE_NORMAL;
      $names = array();
      $namesIndex = array();
      $len = strlen($sql);
      $i = 0;

      while ($i < $len) {
         $curr = $sql[$i];
         if ($i < $len - 1) {
            $next = $sql[$i + 1];
         } else {
            $next = "\0";
         }
         $nextMode = $mode;

         if ($mode === self::MODE_NORMAL) {
            if ($curr === self::QUOTE_SINGLE_CHR) {
               $nextMode = self::MODE_QUOTE_SINGLE;
            } else if ($curr === self::QUOTE_DOUBLE_CHR) {
               $nextMode = self::MODE_QUOTE_DOUBLE;
            } else if (($curr === self::COMMENT_LINE_STR[0]) && ($next === self::COMMENT_LINE_STR[1])) {
               $i += 1;
               $nextMode = self::MODE_COMMENT_LINE;
            } else if (($curr === self::COMMENT_BEGIN_STR[0]) && ($next === self::COMMENT_BEGIN_STR[1])) {
               $i += 1;
               $nextMode = self::MODE_COMMENT_MULTI;
            } else if (($curr === self::BIND_START_CHR) && self::isIdentChar($next)) {
               $bindName = "";
               $nextMode = self::MODE_BIND_VARNAME;
            }
         } else if (($mode === self::MODE_QUOTE_SINGLE) && ($curr === self::QUOTE_SINGLE_CHR)) {
            $nextMode = self::MODE_NORMAL;
         } else if (($mode === self::MODE_QUOTE_DOUBLE) && ($curr === self::QUOTE_DOUBLE_CHR)) {
            $nextMode = self::MODE_NORMAL;
         } else if (($mode === self::MODE_COMMENT_LINE) && self::isLineBreak($curr)) {
            $nextMode = self::MODE_NORMAL;
         } else if (($mode === self::MODE_COMMENT_MULTI) && ($curr === self::COMMENT_END_STR[0]) && ($next === self::COMMENT_END_STR[1])) {
            $i += 1;
            $nextMode = self::MODE_NORMAL;
         } else if ($mode === self::MODE_BIND_VARNAME) {
            if (self::isIdentChar($curr)) {
               $bindName = $bindName . $curr;
            }
            if (!self::isIdentChar($next)) {
               /* found new bind param */
               if (!$unique || !in_array(strtolower($bindName), $namesIndex)) {
                  array_push($namesIndex, strtolower($bindName));
                  array_push($names, $bindName);
               }
               $nextMode = self::MODE_NORMAL;
            }
         }

         $i += 1;
         $mode = $nextMode;
      }

      return $names;
   }
}

Кажется, что улучшения приветствуются!

...