Как проанализировать и избежать условия соединения sql в PHP


Я некоторое время работал над своей собственной библиотекой SQL/построителем запросов. (https://github.com/aviat4ion/Query) По большей части я вполне доволен тем, как все работает.

Единственная проблема заключается в соединениях запросов.

Скажите что-нибудь вроде

$db->join($table, 'table1.field1=table2.field2', 'inner');

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

Я также хочу иметь возможность обрабатывать функции в этом состоянии.

Мой текущий реализация довольно наивна - разделение условного на пробелы, поэтому "table1.field1=table2.field2" завершится неудачей, и "table1.field1 =table2.field2" будет работать.

Каждый драйвер базы данных имеет функцию для абстрактного экранирования идентификатора, которая работает с идентификаторами таблиц, такими как database.table.field, так что он экранируется как "database"."table"."field".

Итак, моя основная проблема заключается в следующем: как анализировать идентификаторы, чтобы избежать их в условном соединении.

Редактировать:

Мне нужно сделать это таким образом, чтобы можно было используется для MySQL, Postgres, SQLite и Firebird.

Author: timw4mail, 2012-08-01

2 answers

Если вы хотите проанализировать только выражение where, простой анализатор приоритета операторов сделает свое дело. Вам нужно применить несколько проверок к дереву синтаксического анализа, чтобы убедиться в правильности выражения, но это несложно.

Вы можете скачать отличное руководство по синтаксическому анализу в целом здесь http://dickgrune.com/Books/PTAPG_1st_Edition / ("Методы синтаксического анализа - Практическое руководство"). Анализ приоритета описан в разделе 9.2 АНАЛИЗ ПРИОРИТЕТА, страница 187.

Методика предполагает у вас есть 2 вещи:

  1. токенизатор. это должно распознавать такие маркеры, как: идентификаторы/ключевые слова, числа, операторы, пробелы/комментарии и т.д.
  2. таблица приоритетов.

Вы считываете токены из токенизатора один за другим. Когда вы обнаружите, что токен является оператором (вы знаете, что это так, потому что они хранятся в таблице приоритетов), вы определяете, имеет ли текущий токен более высокий или более низкий приоритет, чем предыдущий оператор. Если текущий оператор приоритет ниже, чем приоритет предыдущего токена, тогда вам нужно записать предыдущий оператор вместе с его операндами в дерево синтаксического анализа и оглянуться назад, чтобы найти, каким был предыдущий оператор ранее предыдущего оператора. Эти операции работают лучше всего, если токенизатор доставляет токены в виде двухсвязного списка, чтобы вы могли легко просматривать токены.

Если все это звучит слишком сложно, то либо:

  1. используйте существующий синтаксический анализатор SQL. См., например, http://code.google.com/p/php-sql-parser /
  2. пересмотрите свой API.

Что касается варианта № 2, то вместо того, чтобы позволять людям указывать выражения в виде необработанного текста, вы можете потребовать, чтобы они передавали его в виде массива или в виде легко разбираемого формата, такого как JSON или даже XML.

Например, вы могли бы сделать это так:

$db->join->inner($table, array(
    '=' => array(
        'left' => array (
            'table' => 'tab1'
        ,   'column' => 'col1' 
        )
    ,   'right' => array (
            'table' => 'tab2'
        ,   'column' => 'col2' 
        )
    )
));
 1
Author: Roland Bouman, 2012-08-01 15:38:32

Итак, вот примерно то, что я придумал:

class Query_Parser {

/**
 * Regex patterns for various syntax components
 *
 * @var array
 */
private $match_patterns = array(
    'function' => '([a-zA-Z0-9_]+\((.*?)\))',
    'identifier' => '([a-zA-Z0-9_-]+\.?)+',
    'operator' => '=|AND|&&?|~|\|\|?|\^|/|>=?|<=?|-|%|OR|\+|NOT|\!=?|<>|XOR'
);

/**
 * Regex matches
 *
 * @var array
 */
public $matches = array(
    'functions' => array(),
    'identifiers' => array(),
    'operators' => array(),
    'combined' => array(),
);

/**
 * Constructor/entry point into parser
 *
 * @param string
 */
public function __construct($sql = '')
{
    // Get sql clause components
    preg_match_all('`'.$this->match_patterns['function'].'`', $sql, $this->matches['functions'], PREG_SET_ORDER);
    preg_match_all('`'.$this->match_patterns['identifier'].'`', $sql, $this->matches['identifiers'], PREG_SET_ORDER);
    preg_match_all('`'.$this->match_patterns['operator'].'`', $sql, $this->matches['operators'], PREG_SET_ORDER);

    // Get everything at once for ordering
    $full_pattern = '`'.$this->match_patterns['function'].'+|'.$this->match_patterns['identifier'].'|('.$this->match_patterns['operator'].')+`i';
    preg_match_all($full_pattern, $sql, $this->matches['combined'], PREG_SET_ORDER);

    // Go through the matches, and get the most relevant matches
    $this->matches = array_map(array($this, 'filter_array'), $this->matches);
}

// --------------------------------------------------------------------------

/**
 * Public parser method for seting the parse string
 *
 * @param string
 */
public function parse_join($sql)
{
    $this->__construct($sql);
    return $this->matches;
}

// --------------------------------------------------------------------------

/**
 * Returns a more useful match array
 *
 * @param array
 * @return array
 */
private function filter_array($array)
{
    $new_array = array();

    foreach($array as $row)
    {
        if (is_array($row))
        {
            $new_array[] = $row[0];
        }
        else
        {
            $new_array[] = $row;
        }
    }

    return $new_array;
}

}

Затем я запускаю это в своем классе построителя запросов, цитирую идентификаторы в предложении, а затем строю его обратно вместе:

// Parse out the join condition
$parts = $parser->parse_join($condition);
$count = count($parts['identifiers']);

// Go through and quote the identifiers
for($i=0; $i <= $count; $i++)
{
    if (in_array($parts['combined'][$i], $parts['identifiers']) && ! is_numeric($parts['combined'][$i]))
    {
        $parts['combined'][$i] = $this->quote_ident($parts['combined'][$i]);
    }
}

$parsed_condition = implode(' ', $parts['combined']);
 0
Author: timw4mail, 2012-08-09 20:42:48