rust nom 实现一个简单的sql解析器

祝福

过年期间,新型冠状病毒肺炎闹的动静不小,不过相信国家可以妥善处理的。
祝大家平安健康!中国加油!武汉加油!

前言

上篇文章中,简单的描述了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]中的代码进行测试即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值