祝福
过年期间,新型冠状病毒肺炎闹的动静不小,不过相信国家可以妥善处理的。
祝大家平安健康!中国加油!武汉加油!
前言
在上篇文章中,简单的描述了nom一些常用的解析器、组合器。结尾说要实现一个简单的sql解析器,但是年底事情比较多,想着忙完回家,趁着过年补上,结果回到家里就天天睡觉,战果也很明显,胖了不少…
不过该欠下的坑,还是要补的。
分析
sql解析器是一个相对比较复杂的程序,我们是为了通过例子更加了解nom的使用方法,所以我们只是实现select语句的部分功能。
实现约定:
1、只实现到from table,where、group by、order by、having等不实现
2、字段只实现常规的table.column、字符串"abc"还有子查询,函数、算术表达式等不实现
3、table只实现常规的情况,子查询、关联等方式不实现
4、实现alias
5、除了子查询,不会再出现()包裹的情况,子查询必须()包裹切必须有别名
6、因为字段中出现多个的处理方式,表的多个情况就不再实现
设定输入sql语句字符串的生命周期为'a
字段
按照实现约定,字段有三种类型:常规、字符串、子查询,因此字段是一个枚举。
针对常规格式,格式为[ table.]column [[as] alias
#[derive(Debug, PartialEq, Eq)]
struct Field<'a> {
own: Option<&'a str>, // 所属,table.column中的table
name: &'a str, // 字段 table.column中的column
alias: Option<&'a str> // 别名 as alias中的alias
}
impl <'a> Field<'a> {
fn new(name: &'a str) -> Field {
Field {
own: None,
name,
alias: None
}
}
fn new_own(own: Option<&'a str>, name: &'a str) -> Self {
Field {
own,
name,
alias: None
}
}
fn new_all(own: Option<&'a str>, name: &'a str, alias: Option<&'a str>) -> Self {
Field {
own,
name,
alias
}
}
}
针对字符串格式,格式为"str" [[as] alias]
#[derive(Debug, PartialEq, Eq)]
struct StringField<'a> {
string: &'a str, // 字符串值, "str" as s中的str
alias: Option<&'a str> // 别名 as alias 中的alias
}
impl <'a> StringField<'a> {
fn new(string: &'a str) -> Self {
StringField {
string,
alias: None
}
}
}
针对子查询,格式(sql) as alias
#[derive(Debug, Eq, PartialEq)]
struct SubSelect<'a> {
parser: Box<SelectStatement<'a>>,
alias: Option<&'a str>
}
impl <'a> SubSelect<'a> {
fn new(parser: SelectStatement<'a>, alias: &'a str) -> Self {
SubSelect {
parser: Box::new(parser),
alias: Some(alias)
}
}
}
此时三种情况的对象都已经声明,可以设定字段的枚举
#[derive(Debug, PartialEq, Eq)]
enum FieldEnum<'a> {
Normal(Field<'a>),
SubSelect(SubSelect<'a>),
String(StringField<'a>)
}
表
按照实现约定,只有常规的格式 table [[as] alias],不过也按照多种情况来设定
#[derive(Debug, PartialEq, Eq)]
enum TableEnum<'a> {
Normal(Table<'a>)
// SubSelect(SubSelect<'a>)
}
#[derive(Debug, PartialEq, Eq)]
struct Table<'a> {
name: &'a str,
alias: Option<&'a str>
}
impl <'a> Table<'a> {
fn new(name: &'a str) -> Self {
Table {
name,
alias: None
}
}
fn new_all(name: &'a str, alias: Option<&'a str>) -> Self {
Table {
name,
alias
}
}
}
查询语句
只考虑字段和表,且字段和表为多个
但实现时,表不再考虑多个情况
#[derive(Debug, Eq, PartialEq)]
struct SelectStatement<'a> {
field: Vec<FieldEnum<'a>>,
table: Vec<TableEnum<'a>>
}
编码
nom是一个流式的文本解析器,从做到右挨个的进行匹配,所以需要先设定关键字,当遇到这些关键字的时候,代表进入下一个解析步骤。
全局设置关键字,使用lazy_static
Cargo.toml
[dependencies]
lazy_static = "1.4"
需要使用到宏
main.rs 或 lib.rs中
#[macro_use]
extern crate lazy_static;
关键字
lazy_static! {
static ref KEY_WORDS: HashSet<&'static str> = {
let mut s = HashSet::<&'static str>::new();
s.insert("select");
s.insert("from");
s.insert("where");
s.insert("group by");
s.insert("order by");
s.insert("as");
s
};
}
字符规则
sql语句中,一般情况下,字段、表、函数以及存储过程的名称字符规则为英文字符、数字和下划线。
设定is_sql_alphanumeric函数用来判断过来的每一个字符是否在这个规则之内
use nom::character::is_alphanumeric;
fn is_sql_alphanumeric(chr: char) -> bool {
is_alphanumeric(chr as u8) || chr == '_'
}
然后设定sql_alphanumeric函数用来将输入的字符串进行匹配,当匹配到不在字符规则之内的字符时返回,且匹配出来的内容不能是关键字
例如:
输入abc,abc
经过sql_alphanumeric处理得到输出:Ok((",abc", “abc”))
作用和tag(“abc”)一样,只是tag(“abc”)只能匹配()内部的abc,而字段、表名这种的情况比较多,所以自己写一个类似的tag来进行处理
fn sql_alphanumeric(input: &str) -> IResult<&str, &str> {
let clone = input.clone();
let splits: Vec<&str> = clone.split(' ').collect();
if splits.len() > 0 {
let first_word = splits[0];
// order by 或 group by匹配结束,返回
if (first_word == "group" || first_word == "order") && splits.len() > 1 {
let second_word = splits[1];
if second_word == "by" {
return Err(Err::Error(ParseError::from_error_kind(input, ErrorKind::IsNot)));
}
} else {
// 关键字匹配结束,返回
if KEY_WORDS.contains(first_word) {
return Err(Err::Error(ParseError::from_error_kind(input, ErrorKind::IsNot)));
}
}
}
input.split_at_position_complete(|item| !is_sql_alphanumeric(item))
}
除此之外还需要一个遇到不匹配的就抛出Err::Error错误的sql_alphanumeric1函数,用来给many或opt组合器进行使用
fn sql_alphanumeric1(input: &str) -> IResult<&str, &str> {
let clone = input.clone();
let splits: Vec<&str> = clone.split(' ').collect();
if splits.len() > 0 {
let first_word = splits[0];
if (first_word == "group" || first_word == "order") && splits.len() > 1 {
let second_word = splits[1];
if second_word == "by" {
return Err(Err::Error(ParseError::from_error_kind(input, ErrorKind::IsNot)));
}
} else {
if KEY_WORDS.contains(first_word) {
return Err(Err::Error(ParseError::from_error_kind(input, ErrorKind::IsNot)));
}
}
}
input.split_at_position1_complete(|item| !is_sql_alphanumeric(item), ErrorKind::IsNot)
}
sql_alphanumeric函数与sql_alphanumeric1函数的区别在于
input.split_at_position_complete(...) // 这个不抛错
input.split_at_position1_complete(...) // 这个抛错
alias
别名,格式:[[as] alias]
可能存在,也可能不存在,因此调用者需要使用opt()组合器
作为别名的解析函数,需要在别名不存在的时候抛出Err::Error错误,因此需要使用上面的sql_alphanumeric1函数
且as可能也不存在
/// 转换as语句
fn alias_parser(input: &str) -> IResult<&str, &str> {
let (input, _) = if input.starts_with("as ") {
// 假如input为as alias
let (input, _) = tag("as")(input)?;
// 此时input为 _alias _代表空格
let (input, _) = space1(input)?;
// 此时input为alias
(input, ())
} else {
// 假如input为alias或_alias _代表空格
let (input, _) = space0(input)?;
// 此时input为alias
(input, ())
};
// 假如input为alias 处理后input为"" alias为alias
// 假如input为, table 处理后,会抛出Err::Error错误,经过opt处理会得到None
let (input, alias) = sql_alphanumeric1(input)?;
let (input, _) = space0(input)?;
Ok((input, alias))
}
字段
字段需要进行三种,先分开进行处理,然后用一个总的调度进行匹配进行哪一种的处理。
常规格式的字段处理
格式为 [own.]column [[as] alias]
其中别名的处理函数已经实现,使用opt调用即可
假如own存在的话,那么一定会有一个. 此时需要tuple进行组合处理,tuple((sql_alphanumeric, char(’.’))),own又可以不存在,所以需要opt进行处理
fn normal_field_parser(input: &str) -> IResult<&str, Field> {
// 假如input为table.column
// 处理后得input为column,t为table
let (input, t) = opt(tuple((sql_alphanumeric, char('.'))))(input)?;
// 此步处理后input为空,field为column
let (input, field) = sql_alphanumeric(input)?;
let (input, _) = space0(input)?;
// 此步处理后alias为None
let (input, alias) = opt(alias_parser)(input)?;
let (input, _) = opt(char(','))(input)?;
// 组合获得常规字段对象
let mut field = Field::new(field);
if t.is_some() {
let t = t.unwrap();
field.own = Some(t.0);
}
if alias.is_some() {
field.alias = Some(alias.unwrap());
}
Ok((input, field))
}
字符串格式字段处理
格式为:“str” [[as] alias]
sql中的字符串的起始终止字符一般为"或’,但具体在书写时使用的是哪种不是很清楚,因此需要将此字符传入,这样的话函数的声明与返回值就和之前的模式不一样了
fn string_field_parser(c: char) -> impl Fn(&str) -> IResult<&str, StringField>{}
/// 调用时
string_field_parser('"')(input)
使用字符串无可避免的会碰到转义字符的问题,因此需要使用escaped组合器,sql中的转移字符一般为\
fn string_field_parser(c: char) -> impl Fn(&str) -> IResult<&str, StringField> {
move |input: &str| {
// 假如input为"abc\"",处理后input为abc\""
let (input, _) = char(c)(input)?;
let s = c.to_string() +"\\";
// 此步处理后,input为",string为abc"
let (input, string) = escaped(sql_alphanumeric1, '\\',
one_of(s.as_str()))(input)?;
// 此步处理后input为空,string为abc"
let (input, _) = char(c)(input)?;
let (input, _) = space1(input)?;
let (input, alias) = opt(alias_parser)(input)?;
let mut field = StringField::new(string);
if alias.is_some() {
field.alias = Some(alias.unwrap());
}
Ok((input, field))
}
}
子查询处理
格式(subselect) [[as] alias]
子查询相对来说简单一些,将subselect直接调用sql解析函数即可,在此处我们声明sql解析函数为
fn sql_parser(input: &str) -> IResult<&str, SelectStatement>{}
那么子查询的处理函数为
fn sub_parser(input: &str) -> IResult<&str, SubSelect> {
let (input, _) = char('(')(input)?;
let (input, _) = space0(input)?;
let (input, ss) = sql_parser(input)?;
let (input, _) = space0(input)?;
let (input, _) = char(')')(input)?;
let (input, _) = space0(input)?;
let (input, alias) = alias_parser(input)?;
let ss = SubSelect::new(ss, alias);
Ok((input, ss))
}
字段处理汇总
我们以开头字符进行判断,虽然草率,但我们是为了体验nom的使用方法,所以不要在意这些细节。
以"或’开头为字符串格式的
以(开头为子查询格式的
其他的就是常规的
fn field_parser(input: &str) -> IResult<&str, FieldEnum> {
let first_char = &input.chars().next();
if first_char.is_none() {
return Err(Err::Error(ParseError::from_error_kind(input, ErrorKind::NoneOf)));
}
let first_char = first_char.as_ref().unwrap().to_owned();
// 拿第一个字符进行比对,看到底是什么类型
let (input, fe) = if first_char == '\'' || first_char == '"' {
let (input, sf) = string_field_parser(first_char)(input)?;
(input, FieldEnum::String(sf))
} else if first_char == '(' {
let (input, ss) = sub_parser(input)?;
(input, FieldEnum::SubSelect(ss))
} else {
let (input, nf) = normal_field_parser(input)?;
(input, FieldEnum::Normal(nf))
};
let (input, _) = opt(char(','))(input)?;
let (input, _) = space0(input)?;
Ok((input, fe))
}
表
表的实现逻辑思路与字段的非常相似,就不再多加阐述
整个查询语句
整个的查询语句以select开头,然后进行字段的处理,字段一般会有多个,所以使用many0进行循环解析,直到碰到了from
from之后是table的解析,table也存在多个,但逻辑与字段类似,就略过,只认为拥有一个table来处理
fn sql_parser(input: &str) -> IResult<&str, SelectStatement>{
let (input, _) = tag("select")(input)?;
let (input, _) = space1(input)?;
let (input, fes) = many0(field_parser)(input)?;
let (input, _) = tag("from")(input)?;
let (input, _) = space1(input)?;
let (input, ts) = sql_alphanumeric(input)?;
let (input, _) = space0(input)?;
let (input, alias) = opt(alias_parser)(input)?;
let t = Table::new_all(ts, alias);
let te = TableEnum::Normal(t);
let ss = SelectStatement {
field: fes,
table: vec![te]
};
Ok((input, ss))
}
结尾
这样一个非常非常简单潦草的sql解析器就实现了,虽然简单潦草,但基本上覆盖了nom大部分的使用方法,配合文档基本上可以实现多数功能的开发,比如实现一个html DOM树的解析。
本文的代码地址直接看#[test]中的代码进行测试即可