我已经在自己的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优先级解析中介绍。
该技术假设你有两件事:
- 标记化器。这应该识别标记,例如:标识符/关键字、数字、运算符、空格/注释等
- 优先级表
您从令牌生成器中逐个读取令牌。当您发现令牌是一个运算符时(您知道这是因为这些运算符存储在优先级表中),您可以确定当前令牌的优先级是高于还是低于前一个运算符。如果当前运算符的优先级低于前一个标记的优先级,则必须将前一个运算符及其操作数写入解析树,然后从那里向后查看,以查找前一个操作符的前一个操作员是什么。如果令牌化器将令牌作为双链接列表交付,以便您可以轻松遍历令牌,则这些操作效果最佳。
如果这一切听起来很难,那么要么:
- 使用现有的SQL解析器。请参见示例http://code.google.com/p/php-sql-parser/
- 重新考虑您的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']);