yasp(Yet Another SQL Parser) 是一个SQL解析工具,此前我用TiDB Parser 用得很爽,但是用TiDB Parser写测试工具存在一个固有问题是测试工具嫖了被测对象的代码是不合理的。本着严谨科学(和主要是喜欢造轮子)的态度,开了个SQL Parser(下简称Parser)的坑。

起初我打算用lalrpop一把梭,词法语法一起做了,但是这玩意的词法分析有个小坑是正则匹配不能冲突,以SELECT语句的解析为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Comma<T>: Vec<T> = {
<mut v:(<T> ",")*> <e:T?> => match e {
None=> v,
Some(e) => {
v.push(e);
v
}
}
};

Name: CIStr = r"[0-9a-zA-Z_]+" => <>.into();

pub Fields = Comma<Field>;

pub Field: Field = {
"*" => Field::new_all(),
Name => Field::new_column(<>),
<table: Name>"."<column: Name> => Field::new_column(column).with_table(table),
};

ResultTable = Name;

pub Expr: Expr = {
"select" <fields: Fields> "from" <result_table: ResultTable> => Expr::Select(SelectNode{
fields,
result_table,
})
};
完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
use crate::ast::{
dml::*,
expr::*,
model::*
};

grammar;

Comma<T>: Vec<T> = {
<mut v:(<T> ",")*> <e:T?> => match e {
None=> v,
Some(e) => {
v.push(e);
v
}
}
};

Semicolon<T>: Vec<T> = {
<mut v:(<T> ";")*> <e:T?> => match e {
None=> v,
Some(e) => {
v.push(e);
v
}
}
};

pub Exprs = Semicolon<Expr>;

pub Expr: Expr = {
"select" <fields: Fields> "from" <result_table: ResultTable> => Expr::Select(SelectNode{
fields,
result_table,
})
};

Name: CIStr = r"[0-9a-zA-Z_]+" => <>.into();

pub Fields = Comma<Field>;

pub Field: Field = {
"*" => Field::new_all(),
Name => Field::new_column(<>),
<table: Name>"."<column: Name> => Field::new_column(column).with_table(table),
};

ResultTable = Name;

这是一个简单SELECT语句的语法分析,从上往下看。

  • Comma模板用于处理被逗号分隔的不定长项
  • Name是一个匹配自定义字段名称的符号
  • Field分析了*columntable.column这三种情况
  • Fields分析SELECT的多个目标字段
  • Expr是一个简单的SELECT语法构成

这段代码的问题在于,Expr内部的selectfrom是固定关键词匹配,而SQL是一个关键词兼容大小写的语言,所以我们需要将其改为:

1
2
3
4
5
6
pub Expr: Expr = {
r"(?i)select" <fields: Fields> r"(?i)from" <result_table: ResultTable> => Expr::Select(SelectNode{
fields,
result_table,
})
};

通过正则来匹配大小写的形式,看起来不错,但是编译却报错了。

1
2
3
4
5
6
7
8
9
10
11
12
~/workspace/rust/yasp(refactor*) » cargo test
Compiling yasp v0.1.0 (/home/you06/workspace/rust/yasp)
error: failed to run custom build command for `yasp v0.1.0 (/home/you06/workspace/rust/yasp)`

Caused by:
process didn't exit successfully: `/home/you06/workspace/rust/yasp/target/debug/build/yasp-01bf64e1e5e0f353/build-script-build` (exit code: 1)
--- stdout
processing file `/home/you06/workspace/rust/yasp/src/grammar.lalrpop`
/home/you06/workspace/rust/yasp/src/grammar.lalrpop:46:15: 46:30 error: ambiguity detected between the terminal `r#"[0-9a-zA-Z_]+"#` and the terminal `r#"(?i)from"#`

--- stderr
Name: CIStr = r"[0-9a-zA-Z_]+" => <>.into();

报错的原因是selectfrom既能够满足我们所期望的Expr的语法,也能够满足Field的语法,所以lalrpop不知道它属于那一个符号。在SQL语言里,是不能够将例如selectfrom这种关键词作为表名和字段名来使用的。对于固定的字符串,lalrpop会将它置于比正则匹配更高的优先级,所以纯小写的SQL能够被解析,但如果有多个正则表达式,他们之前将无法区分优先级(也无法做优先级标注),这造成了直接解析的失败。