Mysql源码学习——词法分析MYSQLlex

摘要:
词法分析MYSQLlex客户端向服务器发送过来SQL语句后,服务器首先要进行词法分析,而后进行语法分析,语义分析,构造执行树,生成执行计划。词法分析是第一阶段,虽然在理解Mysql实现上意义不是很大,但作为基础还是学习下比较好。然而Mysql并没有使用lex来实现词法分析,但是语法分析却用了yacc,而yacc需要词法分析函数yylex,故在sql_yacc.cc文件最前面我们可以看到如下的宏定义:/*Substitutethevariableandfunctionnames.*/#defineyyparseMYSQLparse#defineyylexMYSQLlex这里的MYSQLlex也就是本文的重点,即MYSQL自己的词法分析程序。

词法分析MYSQLlex

客户端向服务器发送过来SQL语句后,服务器首先要进行词法分析,而后进行语法分析,语义分析,构造执行树,生成执行计划。词法分析是第一阶段,虽然在理解Mysql实现上意义不是很大,但作为基础还是学习下比较好。

词法分析即将输入的语句进行分词(token),解析出每个token的意义。分词的本质便是正则表达式的匹配过程,比较流行的分词工具应该是lex,通过简单的规则制定,来实现分词。Lex一般和yacc结合使用。关于lexyacc的基础知识请参考YaccLex快速入门-IBM。如果想深入学习的话,可以看下《LEXYACC》。

然而Mysql并没有使用lex来实现词法分析,但是语法分析却用了yacc,而yacc需要词法分析函数yylex,故在sql_yacc.cc文件最前面我们可以看到如下的宏定义:

/*Substitute the variable and function names.  */#defineyyparse         MYSQLparse#defineyylex           MYSQLlex

这里的MYSQLlex也就是本文的重点,即MYSQL自己的词法分析程序。源码版本5.1.48。源码太长,贴不上来,算啦..在sql_lex.cc里面。

我们第一次进入词法分析,state默认值为MY_LEX_START,就是开始状态了,其实state的宏的意义可以从名称上猜个差不多,再比如MY_LEX_IDEN便是标识符。对START状态的处理伪代码如下:

caseMY_LEX_START:
{
Skip空格
获取第一个有效字符c
state
=state_map[c];
Break;
}

我困惑了,这尼玛肿么出来个state_map?找到了在函数开始出有个赋值的地方:

uchar *state_map=cs->state_map;

cs?!不会是反恐精英吧!!快速监视下csmy_charset_latin1,哥了然了,原来cslatin字符集,character set的缩写吧。那么为神马state_map可以直接决定状态?找到其赋值的地方,在init_state_maps函数中,代码如下所示:

/*Fill state_map with states to get a faster parser */for(i=0; i <256; i++)
{
if(my_isalpha(cs,i))
state_map[i]
=(uchar) MY_LEX_IDENT;
elseif(my_isdigit(cs,i))
state_map[i]
=(uchar) MY_LEX_NUMBER_IDENT;
#ifdefined(USE_MB) && defined(USE_MB_IDENT)elseif(my_mbcharlen(cs, i)>1)
state_map[i]
=(uchar) MY_LEX_IDENT;
#endifelseif(my_isspace(cs,i))
state_map[i]
=(uchar) MY_LEX_SKIP;
else
state_map[i]
=(uchar) MY_LEX_CHAR;
}
state_map[(uchar)
'_']=state_map[(uchar)'$']=(uchar) MY_LEX_IDENT;
state_map[(uchar)
'\'']=(uchar) MY_LEX_STRING;
state_map[(uchar)
'.']=(uchar) MY_LEX_REAL_OR_POINT;
state_map[(uchar)
'>']=state_map[(uchar)'=']=state_map[(uchar)'!']=(uchar) MY_LEX_CMP_OP;
state_map[(uchar)
'<']=(uchar) MY_LEX_LONG_CMP_OP;
state_map[(uchar)
'&']=state_map[(uchar)'|']=(uchar) MY_LEX_BOOL;
state_map[(uchar)
'#']=(uchar) MY_LEX_COMMENT;
state_map[(uchar)
';']=(uchar) MY_LEX_SEMICOLON;
state_map[(uchar)
':']=(uchar) MY_LEX_SET_VAR;
state_map[
0]=(uchar) MY_LEX_EOL;
state_map[(uchar)
'\\']=(uchar) MY_LEX_ESCAPE;
state_map[(uchar)
'/']=(uchar) MY_LEX_LONG_COMMENT;
state_map[(uchar)
'*']=(uchar) MY_LEX_END_LONG_COMMENT;
state_map[(uchar)
'@']=(uchar) MY_LEX_USER_END;
state_map[(uchar)
'`']=(uchar) MY_LEX_USER_VARIABLE_DELIMITER;
state_map[(uchar)
'"']=(uchar) MY_LEX_STRING_OR_DELIMITER;
先来看这个for循环,256应该是256个字符了,每个字符的处理应该如下规则:如果是字母,则state =MY_LEX_IDENT;如果是数字,则state = MY_LEX_NUMBER_IDENT,如果是空格,则state = MY_LEX_SKIP,剩下的全为MY_LEX_CHAR 

for循环之后,又对一些特殊字符进行了处理,由于我们的语句“select @@version_comment limit 1中有个特殊字符@,这个字符的state进行了特殊处理,为MY_LEX_USER_END

对于my_isalpha等这几个函数是如何进行判断一个字符属于什么范畴的呢?跟进去看下,发现是宏定义:

#definemy_isalpha(s, c)  (((s)->ctype+1)[(uchar) (c)] & (_MY_U | _MY_L))

Wtf,肿么又来了个ctypec作为ctype的下标,_MY_U | _MY_L如下所示,

#define_MY_U   01    /* Upper case */#define_MY_L   02    /* Lower case */

ctype里面到底存放了什么?在ctype-latin1.c源文件里面,我们找到了my_charset_latin1字符集的初始值:

CHARSET_INFO my_charset_latin1=
{
8,0,0, /*number */
MY_CS_COMPILED
|MY_CS_PRIMARY, /*state */"latin1", /*cs name */"latin1_swedish_ci", /*name */"", /*comment */
NULL,
/*tailoring */
ctype_latin1,
to_lower_latin1,
to_upper_latin1,
sort_order_latin1,
NULL,
/*contractions */
NULL,
/*sort_order_big*/
cs_to_uni,
/*tab_to_uni */
NULL,
/*tab_from_uni */
my_unicase_default,
/*caseinfo */
NULL,
/*state_map */
NULL,
/*ident_map */1, /*strxfrm_multiply */1, /*caseup_multiply */1, /*casedn_multiply */1, /*mbminlen */1, /*mbmaxlen */0, /*min_sort_char */255, /*max_sort_char */'', /*pad char */0, /*escape_with_backslash_is_dangerous */&my_charset_handler,
&my_collation_8bit_simple_ci_handler
};

可以看出ctype = ctype_latin1;而ctype_latin1值为:

staticuchar ctype_latin1[] ={
0,
32, 32, 32, 32, 32, 32, 32, 32, 32, 40, 40, 40, 40, 40, 32, 32,
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
72, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
132,132,132,132,132,132,132,132,132,132, 16, 16, 16, 16, 16, 16,
16,129,129,129,129,129,129, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 16, 16, 16, 16, 16,
16,130,130,130,130,130,130, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 16, 16, 16, 16, 32,
16, 0, 16, 2, 16, 16, 16, 16, 16, 16, 1, 16, 1, 0, 1, 0,
0, 16, 16, 16, 16, 16, 16, 16, 16, 16, 2, 16, 2, 0, 2, 1,
72, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 16, 1, 1, 1, 1, 1, 1, 1, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 16, 2, 2, 2, 2, 2, 2, 2, 2
};

看到这里哥再一次了然了,这些值都是经过预计算的,第一个0是无效的,这也是为什么my_isalpha(s, c)定义里面ctype要先+1的原因。通过_MY_U_MY_L的定义,可以知道,这些值肯定是按照相应的ASCII码的具体意义进行置位的。比如字符'A',其ASCII码为65,其实大写字母,故必然具有_MY_U,即第0位必然为1,找到ctype里面第66个(略过第一个无意义的0)元素,为129 = 10000001,显然第0位为1(右边起),说明为大写字母。写代码的人确实比较牛X,如此运用位,哥估计这辈子也想不到了,小小佩服下。State的问题点到为止了。

继续进行词法分析,第一个字母为s,其state = MY_LEX_IDENTIDENTIFIER:标识符的意思),break出来,继续循环,case进入MY_LEX_IDENT分支:

Case MY_LEX_IDENT:
{
由s开始读,直到空格为止
If(读入的单词为关键字)
{
nextstate
=MY_LEX_START;
Return tokval;
//关键字的唯一标识}
Else
{
returnIDENT_QUOTED 或者 IDENT;表示为一般标识符
}
}

这里SELECT肯定为关键字,至于为什么呢?下节的语法分析会讲。

解析完SELECT后,需要解析@@version_comment,第一个字符为@,进入START分支,state = MY_LEX_USER_END

进入MY_LEX_USER_END分支,如下:

caseMY_LEX_USER_END:        //end '@' of user@hostnameswitch(state_map[lip->yyPeek()]) {
caseMY_LEX_STRING:
caseMY_LEX_USER_VARIABLE_DELIMITER:
caseMY_LEX_STRING_OR_DELIMITER:
break;
caseMY_LEX_USER_END:
lip
->next_state=MY_LEX_SYSTEM_VAR;
break;
default:
lip
->next_state=MY_LEX_HOSTNAME;
break;

哥会心的笑了,两个@符号就是系统变量吧~~,下面进入MY_LEX_SYSTEM_VAR分支

caseMY_LEX_SYSTEM_VAR:
yylval
->lex_str.str=(char*) lip->get_ptr();
yylval
->lex_str.length=1;
lip
->yySkip(); //Skip '@'lip->next_state=(state_map[lip->yyPeek()] ==
MY_LEX_USER_VARIABLE_DELIMITER
?
MY_LEX_OPERATOR_OR_IDENT :
MY_LEX_IDENT_OR_KEYWORD);
return((int) '@');

作的操作是略过@next_state设置为MY_LEX_IDENT_OR_KEYWORD,再之后便是解析MY_LEX_IDENT_OR_KEYWORD了,也就是version_comment了,此解析应该和SELECT解析路径一致,但不是KEYWORD。剩下的留给有心的读者了(想起了歌手经常说的一句话:大家一起来,哈哈)。

Mysql的词法解析的状态还是比较多的,如果细究还是需要点时间的,但这不是Mysql的重点,我就浅尝辄止了。下节会针对上面的SQL语句讲解下语法分析。

PS: 一直想好好学习下Mysql,总是被这样或那样的事耽误,当然都是自己的原因,希望这次能走的远点.....

PS again:本文只代表本人的学习感悟,如有异议,欢迎指正。

免责声明:文章转载自《Mysql源码学习——词法分析MYSQLlex》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇使用 Consul + Docker + Registrator + Consul-template 搭建可伸缩服务发现平台43 编译原理及cmake使用手册学习下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Mysql源码学习——打造专属语法

语法分析——YACC 接触过SQL语句的人都会看过这家或者那家的SQL手册,其语法标准应该是从SQL92开始吧,在看SQL92标准的时候,你会发现里面定义的都是一些巴科斯范式(BNF),就是一种语法定义的标准。不管是牛X哄哄的ORACLE,还是不幸被其收购的Mysql,都会遵循里面的标准语法,当然一些扩展的语法除外,比如今天我们就会扩展一个简单的语法^...

Yacc 与 Lex 快速入门

源地址: http://www.ibm.com/developerworks/cn/linux/sdk/lex/index.html Lex 代表 Lexical Analyzar。Yacc 代表 Yet Another Compiler Compiler。 让我们从 Lex 开始吧。 Lex Lex 是一种生成扫描器的工具。扫描器是一种识别文本中的词汇模...