本文向您介绍一种利用mysql解析器和bison的调试选项进行sql语法解析跟踪的方法。
数据库开发过程中我们常会遇到修改sql语法的需求。我们知道,mysql的sql解析器是基于yacc文法,采用EBNF格式进行规则描述(sql/sql_yacc.yy
),并借助bison工具生成(sql_yacc.h, sql_yacc.cc
), 所以修改sql语法,不可避免地要和这些yacc文法打交道,对sql_yacc.yy进行改造升级。
yacc文法是对语法解析的高度概括,它为我们修改解析器提供了一种优雅的方式,但与此同时当我们遇到语句解析问题,通常比较难直接从抽象的语法规则中找到原因。幸运的是,结合mysql和bison提供的调试工具,我们有机会将整个语法解析的过程形象化,通过解析日志,yacc规则和自动状态机的对应,能够比较快地完成问题的定位。
- mysql解析器调试开关
sql/sql_yacc.yy文件下,可以看到如下一段代码:
#ifndef NDEBUGvoid turn_parser_debug_on(){ /* MYSQLdebug is in sql/sql_yacc.cc, in bison generated code. Turning this option on is **VERY** verbose, and should be used when investigating a syntax error problem only. The syntax to run with bison traces is as follows : - Starting a server manually : mysqld --debug="d,parser_debug" ... - Running a test : mysql-test-run.pl --mysqld="--debug=d,parser_debug" ... The result will be in the process stderr (var/log/master.err) */ extern int yydebug; yydebug= 1;}#endif
它告诉我们,debug版本下,在mysqld启动时添加 -debug="d, parser_debug
选项,数据库服务器会为我们输出sql解析的具体信息(bison traces)。
这里我们使用一条简单的sql语句SELECT 1+2*3 FROM DUAL
作为例子,看它的日志输出信息(注:’#‘号后为后添加的说明,非原始信息),开头部分如下:
#注:SQL语句会首先被词法解析器(LEXER)处理,输出'SELECT_SYM NUM + NUM * NUM FROM DUAL_SYM'这样的序列,作为语法解析器的输出Starting parse #语句解析开始Entering state 0Reading a token: Next token is token SELECT_SYM (: ) # 读入SELECTShifting token SELECT_SYM (: ) # 移进SELECTEntering state 42 # 栈用于记录当前推导情况Reading a token: Next token is token NUM (: ) # 读入NUM(第一个数字'1'的词法解析标记)Reducing stack by rule 1377 (line 10001): # 在读入之前,做一次栈规约(使用的规则在sql_yacc.yy的10001行)-> $$ = nterm select_options (: ) Stack now 0 42Entering state 1013 # 栈规约后,进入新的状态...
输出信息里state 42, 1013
等信息,yacc语法自动状态机里的状态编号,为了查看它,我们需要使用到bison工具手动生成自动状态机文件。
- 自动状态机文件
使用bison的-v
选项,得到语法的自动状态机文件,生成方式示例如下:
cd ${SOURCE_DIR}/sql #SOURCE_DIR 为mysql源码目录位置/usr/bin/bison --name-prefix=MYSQL --yacc --warnings=all,no-yacc,no-empty-rule,no-precedence,no-deprecated --defines=${BUILD_DIR}/sql/sql_yacc.h -v sql_yacc.yy #BUILD_DIR为用户自定的编译目录位置
执行成功后,将在${SOURCE_DIR}/sql
下生成一个名为y.output的文件,该文件描述了bison根据语法规则计算得出的状态机描述文件,在文件里我们会看到:
- 带编号的语法规则描述。如前文提及的rule 1377,在文件中的内容为:
1377 select_options: %empty
它表示可以将一个空的产生式规约为select_option
- 所有自动机状态。前文提及的state 42,在文件中显示为:
State 42 1366 query_specification: SELECT_SYM . select_options select_item_list into_clause opt_from_clause opt_where_clause opt_group_clause opt_having_clause opt_window_clause... ALL shift, and go to state 1004... select_options go to state 1013 select_option_list go to state 1014 select_option go to state 1015 query_spec_option go to state 1016
- 带shift/reduce,reduce/reduce冲突的状态统计:
State 27 conflicts: 2 shift/reduceState 42 conflicts: 2 shift/reduceState 220 conflicts: 2 shift/reduce...
本文测试使用的是mysql-8.0.25, 它现存的shift/reduce冲突总共为66个,mysql不鼓励因为语法修改而使状态机产生任何新的冲突,因此在开发过程中需要多加注意:
/* 1. We do not accept any reduce/reduce conflicts 2. We should not introduce new shift/reduce conflicts any more.*/%expect 66
有了mysql提供的栈信息,结合bison -v 生成的状态机文件,我们就可以将语法解析过程中的某个具体节点的推导路径给打印出来,如我们可以将解析器在处理完SELECT_SYM NUM +
后,准备读入NUM前的推导过程(栈状态为:0 42 1013
)整理如下(注:"." 位置左边,可以看做当前状态已经移进或者规约的内容):
状态编号 | 状态内容 |
---|---|
0 | 0 $accept: . start_entry $end |
42 | 1366 query_specification: SELECT_SYM . select_options select_item_list into_clause opt_from_clause opt_where_clause opt_group_clause opt_having_clause opt_window_clause 1367 | SELECT_SYM . select_options select_item_list opt_from_clause opt_where_clause opt_group_clause opt_having_clause opt_window_clause |
1013 | 1366 query_specification: SELECT_SYM select_options . select_item_list into_clause opt_from_clause opt_where_clause opt_group_clause opt_having_clause opt_window_clause 1367 | SELECT_SYM select_options . select_item_list opt_from_clause opt_where_clause opt_group_clause opt_having_clause opt_window_clause |
这样,我们就能够比较清晰的知道,在sql解析的每个阶段,解析器的具体状态,因此当出现语法修改错误时,就能够很容易地定位到自己规则哪一部分出现异常,进而更快速地解决问题。
Enjoy GreatSQL 🙂