再读simpledb 之 SQL语句解析(2)
在(1)里面提到,lexer作为一个工具,完成了对SQL字符串的切割,将语句转化成一个tokens数组。
Parser完成了SQL解析的后序部分:使用一个lexer对象作为工具,切出tokens,然后解析语义,绑定相关的系统接口。
在这里先要回顾下simpledb的支持的SQL的语法,这个影响了它在解析字符串时使用的方法。
<Query> := SELECT <SelectList> FROM <TableList> [ WHERE <Predicate> ]
<SelectList> := <Field> [ , <SelectList> ]
<TableList> := TableName [ , <TableList> ]
<Predicate> := <Term> [ AND <Predicate> ]
<Term> := <Expression> = <Expression>
<Expression> := <Field> | <Constant>
<Field> := FieldName
<Constant> := String | Integer
<Modify> := <Insert> | <Delete> | <Update>
<Insert> := INSERT INTO TableName ( <FieldList> ) VALUES ( <ConstList> )
<FieldList> := <Field> [ , <FieldList> ]
<ConstList> := <Constant> [ , <Constant> ]
<Delete> := DELETE FROM TableName [ WHERE <Predicate> ]
<Update> := UPDATE TableName SET <Field> = <Expression> [ WHERE <Predicate> ]
<Create> := <CreateTable> | <CreateView> | <CreateIndex>
<CreateTable>:= CREATE TABLE TableName ( <FieldDefs> )
<FieldDefs> := <FieldDef> [ , <FieldDefs> ]
<FieldDef> := FieldName <TypeDef>
<TypeDef> := INT | VARCHAR ( Integer )
<CreateView> := CREATE VIEW ViewName AS <Query>
<CreateIndex>:= CREATE INDEX IndexName ON TableName ( <Field> )
Parser这里实现的编译技术中的“一趟编译”,即通过lex顺序扫描tokens数组,在扫描过程中完成对各个token元素的识别,按照
constant/field –> Expression –> Term –> Predicate
的层递关系,完成参数包装,输出一个查询关联的数据类,如(1)中的类图1所示。
首先,以(1)图2的例子“select sid,sname from students where sid='10001' ”,给出一个parser解析过程中构建的语法树:
图1 示例语法树
以下是Query方法的代码:
public QueryData query()
{
lex.eatKeyword("select");
List<string> fields = selectList();
lex.eatKeyword("from");
List<string> tables = tableList();
Predicate pred = new Predicate();
if (lex.matchKeyword("where"))
{
lex.eatKeyword("where");
pred = predicate();
}
return new QueryData(fields, tables, pred);
}
由代码和上面的语法树看出来,simpledb在解析SQL语句的时候,严格按照语法中支持的类型,“卡住”关键字,从中解析出字段列表fieldlist,表名列表tablelist,以及谓词列表predicates,然后将这些查询中实际用到的数据,包装成相应的对象,SQL语句的解析就初步完成。以上面的例子为例,QueryData对象包装完之后,会传递给查询处理模块,通过query下的一些方法,根据fieldlist,tablelist,和predicates来完成查询数据的读取。
parser中create(), delete(), insert(),query(), modify()方法,对应了上面的几类语法
作为SQL语句解析的入口,有如下的updateCMD方法,根据SQL语句首个token的不同,进行了分支:
public object updateCmd()
{
if (lex.matchKeyword("insert"))
return insert();
else if (lex.matchKeyword("delete"))
return delete();
else if (lex.matchKeyword("update"))
return modify();
else
return create();
}
补充一点,注意下上面的query()的代码,lex用match*()来检测下一个token是否满足匹配条件,用eat*()来讲满足条件的token处理掉:
public void eatDelim(char d);
public string eatId();
public int eatIntConstant()
public void eatKeyword(string w)
public string eatStringConstant()
数据相关的,均有返回值,返回处理后的结果;数据无关的,没有返回值,实际只是利用nextToken移动position指针。
关于tablelist、fieldlist的构建,使用了递归的方式实现:
private List<string> fieldList()
{
List<string> l = new List<string>();
l.Add(field());
if (lex.matchDelim(','))
{
lex.eatDelim(',');
l.AddRange(fieldList());
}
return l;
}
谓词 predicates也是用了递归的形式:
private Predicate predicate()
{
Predicate pred = new Predicate(term());
if (lex.matchKeyword("and"))
{
lex.eatKeyword("and");
pred.conjoinWith(predicate());
}
return pred;
}
略有不同的是,在谓词连接的时候,使用了Predicate类的conjoinwith方法,实际上,predicate对象下,维护了一个条件列表terms,这个方法就是把谓词中的各个term转存到一起。
本文和前一节只是扼要地描述了SQL语句解析的思路和过程,可以看到解析的结果就是生成了各种查询data,这些data会传递给query模块,由query模块利用这些得到的data,完成数据的查询。