作者:王东 就像是我们使用编译器编译c, cpp文件一样, 对查询语句的编译过程也分为了如下几部分: l 词法分析;
l 语法分析;
l 语义分析和检测;
l 查询重写;
l 生成查询计划;
l 挑选和优化查询计划; 这里主要是针对前三步进行说明。
当我们输入一个SQL语句时, CUBRID的查询编译器会按照类似代码编译器的方式进行工作。 1. 首先进行词法分析,词法分析的目标是将SQL语句进行扫描并分割成为一系列记号(Token),在CUBRID中,是使用Flex生成的文件进行词法分析的。Flex(fast lexical analyser generator)是一个自动的词法分析器生成器(参考附1)。在CUBRID中,有一个描述SQL词法的.l 文件,该文件描述了SQL语句中用的词汇。通过使用Flex,并输入.l 文件,将生成对应的可以用的编译的.c文件。词法分析的结果产生了各种记号(Token), 例如:关键字,标识符,字符串等等;
2. 接下来进行语法分析。语法分析是将词法分析产生的记号(Token)按照语法要求生成语法树(Syntax Tree)。CUBRID 中采用bison来进行语法分析(参考附录2)。类似于编译器中的表达式一样,不同的SQL语句,将被看作为不同表达式(Expression)。CUBRID中有一个描述语法的.y文件,该文件中包含表达式树是如何组织起来的代码。通过bison,并输入.y文件,生成对应可以用于编译的.c和.h文件。这样SQL就被组织成语法树的结构了。
3. 接下来就是对语法树进行语义检测(Semantic analysis and check)。因为语法分析中只是完成了对表达式语法层面的分析,并没有对真正这条语句的内容是否合法进行检测。 CUBRID 里面的Semantic check, 是对生成的语法树进行遍历和检查。由于树的节点不同,对每个节点的遍历算法也是不同的。语义检测的结果是对不满足输入的SQL语句进行过滤(提示报错),对语法树进行适当的扩充和删减,便与接下来的生成查询计划等工作。 接下来就进行查询重写,生成查询计划其他了操作。这里暂且不提。
下面就这三部分给出CUBRID中实现的三个例子。
例子1 CUBRID词法分析
.l 文件是由正则表达式和c语法组成的。
//定义段代码
%{
//定义include文件,宏,辅助函数,flex会跳过对这些的处理。
#include"csql_grammar.h"
#include"parse_tree.h"
#defineCSQL_MAXNAME256
staticintparser_yyinput_single_line(char* buff, intmax_size);
externintyyline;
//……
%}
// 词法规则段代码
%%
[sS][eE][lL][eE][cC][tT] { begin_token(yytext); returnSELECT; }
([a-zA-Z_#]|(\xa1[\xa2-\xee\xf3-\xfe])|([\xa2-\xfe][\xa1-\xfe])|(\x8e[\xa1-\xfe]))([a-zA-Z_#0-9]|(\xa1[\xa2-\xfe])|([\xa2-\xfe][\xa1-\xfe])|(\x8e[\xa1-\xfe]))*
{ begin_token(yytext);
if (strlen(yytext) >= 254)
yytext[254] = 0;
csql_yylval.cptr = pt_makename(yytext);
returnIdName;
}
%%
通过编辑.l文件,我们可以定义我们感兴趣的token。
生成的c代码,如下:
case 338:
YY_RULE_SETUP
#line 537 "../../src/parser/csql_lexer.l"
{ begin_token(yytext); returnSELECT; }
YY_BREAK
例子2CUBRID语法分析
.y 文件定义语法树表达方式, 其中包含了Token, rule type, rule的组织方式. 例子中,select 语句的节点中就包含很多内容。
/* Token define */
/*{{{*/
%tokenSELECT
%tokenIdName
/* define rule type (node) */
/*{{{*/
%type stmt_
%type create_stmt
%type select_stmt
stmt_
: create_stmt
{ $$ = $1; }
…
| select_stmt
{ $$ = $1; }
…
;
select_stmt
: SELECT /* $1 */
{{ /* $2 发现是select语句,就创建select节点*/ PT_NODE *node = parser_new_node (this_parser, PT_SELECT); … DBG_PRINT}} … select_list /* $5 */ {{ /* $6 创建select 选项的列表节点*/
PT_NODE *node = parser_top_select_stmt_node ();
if (node)
{
node->info.query.q.select.list = $5;
}
DBG_PRINT}}
opt_select_param_list /* $7 */
FROM /* $8 */
…
opt_where_clause /* $11 where语句对应的节点*/
…
opt_groupby_clause /* $14 group语句对应的节点*/
opt_having_clause /* $16 having语句对应的节点*/
…
{{
if (node)
{
node->info.query.into_list = $7; /* param_list */
node->info.query.q.select.where = $11;
node->info.query.q.select.group_by = $14;
node->info.query.q.select.having = $16;
…
}
$$ = node;
DBG_PRINT}}
;
}}
opt_where_clause
: /* empty */
{{
$$ = NULL;
DBG_PRINT}}
| WHEREsearch_condition
{{
parser_restore_ic ();
parser_restore_sysc ();
parser_restore_prc ();
parser_restore_cbrc ();
$$ = $2;
DBG_PRINT}}
;
看到这里大家应该明白了,这种类似递归的方式传递构造语法树的过程。最终构建的是一个以select节点为根节点的语法树。其中包含了q.select.list,where,group_by,having 等节点。
例子3 CUBRID语义检测
语法检测阶段拿到的就是一个语法树的指针。在CUBRID中这个语法树就是一个PT_NODE。该结构是包含了许多类型的节点,例如创建类型,select类型等等。PT_NODE 的类型和info 是匹配的。例如:如果Type = PT_CREATE_ENTITY, Info 则是PT_CREATE_ENITITY_INFO结构体。
其中PT_NODE类型的节点用于记录节点的类型,PT_XXX_INFO的节点是用于存储信息,里面可以包含数据,也可以再包含PT_NODE节点。
这样整个语法树就递归起来。每个节点本身是PT_NODE, 每个节点的Info 里面可以包含PT_NODE。
最终整个语法树的叶子节点必然是一个PT_NODE, 它的Info中不包含任何PT_NODE的PT_NODE节点。
而PT_NODE 在内存中的布局就类似如下:
简单的语义检测可以直接操作这个语法树,例如CUBRID中partition table by Hash, 其中partition的个数不能超过1024个。
复杂的语言检测有时候需要遍历语法树,可以采用递归的深度优先算法进行遍历。
CUBRID的遍历语法树的算法的主要逻辑是:
1 刚进入节点时:处理进入前回调函数:
2 根据PT_NODE 调用自己的节点对应的遍历函数;
通常遍历函数,就是依次遍历该节点包含的子节点,由于节点不同,每个节点的子节点个数都不一样。
根据需要遍历Next节点等等;
3 离开节点时:处理离开前回调函数
例如:检测遍历PT_NODE中提到的table都是必须数据库中存在的。可以遍历语法树,关注type=PT_SPEC的节点即可。
到此为止,相信大家已经明白CUBRID语法分析和检测的过程了。如果感兴趣的,请查看我们的开源项目CUBRID源代码(参考1).
附录:
附1:什么是Flex?
Flex是一个生成扫描器的工具,能够识别文本中的词法模式。Flex读入给定的输入文件,该文件为需要生成的扫描器的描述。此描述叫做规则,由正则表达式和 C代码对组成。当运行可执行文件的时候,它分析输入文件,为每一个正则表达式寻找匹配。当发现一个匹配时,它执行与此正则表达式相关的C代码。Flex 不是GNU工程,但是GNU为Flex 写了手册。
附2: 什么是bison?
GNU bison 是属于 GNU 项目的一个语法分析器生成器。Bison 把一个关于"向前查看从左到右最右"(LALR) 上下文无关文法的描述转化成可以分析该文法的 C 或 C++ 程序。它也可以为二义文法生成"通用的从左到右最右" (GLR)语法分析器。
Bison 基本上与 Yacc 兼容,并且在 Yacc 之上进行了改进。此软件的源代码是可自由获得的,在 GPL 下发布。
Flex 和Bison 常常结合一起使用。
参考: 1. CUBRID 官方站点: http://www.cubrid.org 2. Flex 介绍: http://en.wikipedia.org/wiki/Flex_lexical_analyser 3. Yacc 介绍: http://zh.wikipedia.org/zh-cn/Yacc 4. GUN 项目里bison主页: http://www.gnu.org/software/bison/ 6. Flex和Bison入门: http://code.google.com/p/msys-cn/wiki/ChapterFour
l 语法分析;
l 语义分析和检测;
l 查询重写;
l 生成查询计划;
l 挑选和优化查询计划; 这里主要是针对前三步进行说明。
当我们输入一个SQL语句时, CUBRID的查询编译器会按照类似代码编译器的方式进行工作。 1. 首先进行词法分析,词法分析的目标是将SQL语句进行扫描并分割成为一系列记号(Token),在CUBRID中,是使用Flex生成的文件进行词法分析的。Flex(fast lexical analyser generator)是一个自动的词法分析器生成器(参考附1)。在CUBRID中,有一个描述SQL词法的.l 文件,该文件描述了SQL语句中用的词汇。通过使用Flex,并输入.l 文件,将生成对应的可以用的编译的.c文件。词法分析的结果产生了各种记号(Token), 例如:关键字,标识符,字符串等等;
2. 接下来进行语法分析。语法分析是将词法分析产生的记号(Token)按照语法要求生成语法树(Syntax Tree)。CUBRID 中采用bison来进行语法分析(参考附录2)。类似于编译器中的表达式一样,不同的SQL语句,将被看作为不同表达式(Expression)。CUBRID中有一个描述语法的.y文件,该文件中包含表达式树是如何组织起来的代码。通过bison,并输入.y文件,生成对应可以用于编译的.c和.h文件。这样SQL就被组织成语法树的结构了。
3. 接下来就是对语法树进行语义检测(Semantic analysis and check)。因为语法分析中只是完成了对表达式语法层面的分析,并没有对真正这条语句的内容是否合法进行检测。 CUBRID 里面的Semantic check, 是对生成的语法树进行遍历和检查。由于树的节点不同,对每个节点的遍历算法也是不同的。语义检测的结果是对不满足输入的SQL语句进行过滤(提示报错),对语法树进行适当的扩充和删减,便与接下来的生成查询计划等工作。 接下来就进行查询重写,生成查询计划其他了操作。这里暂且不提。
下面就这三部分给出CUBRID中实现的三个例子。
例子1 CUBRID词法分析
.l 文件是由正则表达式和c语法组成的。
//定义段代码
%{
//定义include文件,宏,辅助函数,flex会跳过对这些的处理。
#include"csql_grammar.h"
#include"parse_tree.h"
#defineCSQL_MAXNAME256
staticintparser_yyinput_single_line(char* buff, intmax_size);
externintyyline;
//……
%}
// 词法规则段代码
%%
[sS][eE][lL][eE][cC][tT] { begin_token(yytext); returnSELECT; }
([a-zA-Z_#]|(\xa1[\xa2-\xee\xf3-\xfe])|([\xa2-\xfe][\xa1-\xfe])|(\x8e[\xa1-\xfe]))([a-zA-Z_#0-9]|(\xa1[\xa2-\xfe])|([\xa2-\xfe][\xa1-\xfe])|(\x8e[\xa1-\xfe]))*
{ begin_token(yytext);
if (strlen(yytext) >= 254)
yytext[254] = 0;
csql_yylval.cptr = pt_makename(yytext);
returnIdName;
}
%%
通过编辑.l文件,我们可以定义我们感兴趣的token。
生成的c代码,如下:
case 338:
YY_RULE_SETUP
#line 537 "../../src/parser/csql_lexer.l"
{ begin_token(yytext); returnSELECT; }
YY_BREAK
例子2CUBRID语法分析
.y 文件定义语法树表达方式, 其中包含了Token, rule type, rule的组织方式. 例子中,select 语句的节点中就包含很多内容。
/* Token define */
/*{{{*/
%tokenSELECT
%tokenIdName
/* define rule type (node) */
/*{{{*/
%type stmt_
%type create_stmt
%type select_stmt
stmt_
: create_stmt
{ $$ = $1; }
…
| select_stmt
{ $$ = $1; }
…
;
select_stmt
: SELECT /* $1 */
{{ /* $2 发现是select语句,就创建select节点*/ PT_NODE *node = parser_new_node (this_parser, PT_SELECT); … DBG_PRINT}} … select_list /* $5 */ {{ /* $6 创建select 选项的列表节点*/
PT_NODE *node = parser_top_select_stmt_node ();
if (node)
{
node->info.query.q.select.list = $5;
}
DBG_PRINT}}
opt_select_param_list /* $7 */
FROM /* $8 */
…
opt_where_clause /* $11 where语句对应的节点*/
…
opt_groupby_clause /* $14 group语句对应的节点*/
opt_having_clause /* $16 having语句对应的节点*/
…
{{
if (node)
{
node->info.query.into_list = $7; /* param_list */
node->info.query.q.select.where = $11;
node->info.query.q.select.group_by = $14;
node->info.query.q.select.having = $16;
…
}
$$ = node;
DBG_PRINT}}
;
}}
opt_where_clause
: /* empty */
{{
$$ = NULL;
DBG_PRINT}}
| WHEREsearch_condition
{{
parser_restore_ic ();
parser_restore_sysc ();
parser_restore_prc ();
parser_restore_cbrc ();
$$ = $2;
DBG_PRINT}}
;
看到这里大家应该明白了,这种类似递归的方式传递构造语法树的过程。最终构建的是一个以select节点为根节点的语法树。其中包含了q.select.list,where,group_by,having 等节点。
例子3 CUBRID语义检测
语法检测阶段拿到的就是一个语法树的指针。在CUBRID中这个语法树就是一个PT_NODE。该结构是包含了许多类型的节点,例如创建类型,select类型等等。PT_NODE 的类型和info 是匹配的。例如:如果Type = PT_CREATE_ENTITY, Info 则是PT_CREATE_ENITITY_INFO结构体。
其中PT_NODE类型的节点用于记录节点的类型,PT_XXX_INFO的节点是用于存储信息,里面可以包含数据,也可以再包含PT_NODE节点。
这样整个语法树就递归起来。每个节点本身是PT_NODE, 每个节点的Info 里面可以包含PT_NODE。
最终整个语法树的叶子节点必然是一个PT_NODE, 它的Info中不包含任何PT_NODE的PT_NODE节点。
而PT_NODE 在内存中的布局就类似如下:
简单的语义检测可以直接操作这个语法树,例如CUBRID中partition table by Hash, 其中partition的个数不能超过1024个。
复杂的语言检测有时候需要遍历语法树,可以采用递归的深度优先算法进行遍历。
CUBRID的遍历语法树的算法的主要逻辑是:
1 刚进入节点时:处理进入前回调函数:
2 根据PT_NODE 调用自己的节点对应的遍历函数;
通常遍历函数,就是依次遍历该节点包含的子节点,由于节点不同,每个节点的子节点个数都不一样。
根据需要遍历Next节点等等;
3 离开节点时:处理离开前回调函数
例如:检测遍历PT_NODE中提到的table都是必须数据库中存在的。可以遍历语法树,关注type=PT_SPEC的节点即可。
到此为止,相信大家已经明白CUBRID语法分析和检测的过程了。如果感兴趣的,请查看我们的开源项目CUBRID源代码(参考1).
附录:
附1:什么是Flex?
Flex是一个生成扫描器的工具,能够识别文本中的词法模式。Flex读入给定的输入文件,该文件为需要生成的扫描器的描述。此描述叫做规则,由正则表达式和 C代码对组成。当运行可执行文件的时候,它分析输入文件,为每一个正则表达式寻找匹配。当发现一个匹配时,它执行与此正则表达式相关的C代码。Flex 不是GNU工程,但是GNU为Flex 写了手册。
附2: 什么是bison?
GNU bison 是属于 GNU 项目的一个语法分析器生成器。Bison 把一个关于"向前查看从左到右最右"(LALR) 上下文无关文法的描述转化成可以分析该文法的 C 或 C++ 程序。它也可以为二义文法生成"通用的从左到右最右" (GLR)语法分析器。
Bison 基本上与 Yacc 兼容,并且在 Yacc 之上进行了改进。此软件的源代码是可自由获得的,在 GPL 下发布。
Flex 和Bison 常常结合一起使用。
参考: 1. CUBRID 官方站点: http://www.cubrid.org 2. Flex 介绍: http://en.wikipedia.org/wiki/Flex_lexical_analyser 3. Yacc 介绍: http://zh.wikipedia.org/zh-cn/Yacc 4. GUN 项目里bison主页: http://www.gnu.org/software/bison/ 6. Flex和Bison入门: http://code.google.com/p/msys-cn/wiki/ChapterFour