如何在PHP中解析和转义sql联接条件


How to parse and escape sql join condition in PHP

我已经在自己的SQL库/查询生成器上工作了一段时间。(https://github.com/aviat4ion/Query)在大多数情况下,我对事情的运作方式很满意。

一个问题是查询联接。

说一些类似的话

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

我很困惑如何解析第二个参数,它需要正确地转义表标识符。

我希望能够在这种情况下处理函数。

我目前的实现相当天真——将条件拆分为空格,因此"table1.field1=table2.field2"将失败,"table1.field 1=table2-field2"也会起作用。

每个数据库驱动程序都有一个函数来抽象标识符转义,该函数适用于像database.table.field这样的表标识符,以便将其转义为"database"."table"."field"

所以我的基本问题是:如何解析出标识符,以便在联接条件中转义它们。

编辑:

我需要以一种可以用于MySQL、Postgres、SQLite和Firebird的方式来做这件事。

如果您只想解析where表达式,一个简单的运算符优先级解析器就可以了。您必须对解析树进行一些检查,以确保表达式有效,但这并不难。

你可以在这里下载一个优秀的解析指南http://dickgrune.com/Books/PTAPG_1st_Edition/("解析技术-实用指南")。优先级解析在第187页9.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' 
        )
    )
));

所以下面是我的大致想法:

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;
}
}

然后,我在我的Query Builder类中运行它,在子句中引用标识符,然后将其串在一起:

// 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']);