|
| 1 | +# SQL Parser 开发指南 |
| 2 | + |
| 3 | +`parser` 是 `sqlgpt-parser` 的基础模块,它将 SQL 语句按照预定义 SQL 语法规则解析,从文本转换成抽象语法树(`AST`)。 |
| 4 | + |
| 5 | + `sqlgpt-parser` 的 `parser` 基于 [PLY](https://github.com/dabeaz/ply) 编写。PLY 是一个用于构建词法和语法分析器的 Python 工具。它能够根据指定的模式对输入的文本进行分析,它会在程序运行之前,自动编译项目 [sql-parser](../../src/sql_parser/) 文件夹下的词法规则和语法规则文件,生成可执行代码。 |
| 6 | + |
| 7 | +## 词法解析与语法解析 |
| 8 | + |
| 9 | +  |
| 10 | + |
| 11 | +词法解析和语法解析是 `SQL` 解析的两个步骤,它们的关系如上,词法解析会读取用户的输入,根据词法规则将输入转换为 `tokens` 输出。语法解析则使用词法解析的输出的 `tokens` 作为输入,根据语法规则创建抽象语法树。为了生成满足用户需求的词法解析器和语法解析器,用户需要提供自定义的词法规则和语法规则。在 `PLY` 中,词法规则和语法规则使用两种不同的定义规则。 |
| 12 | + |
| 13 | +### 词法规则 |
| 14 | + |
| 15 | +```python |
| 16 | +import ply.lex as lex |
| 17 | + |
| 18 | +tokens = ( |
| 19 | + 'NUMBER', |
| 20 | + 'PLUS', |
| 21 | + 'MINUS', |
| 22 | + 'TIMES', |
| 23 | + 'DIVIDE', |
| 24 | + 'LPAREN', |
| 25 | + 'RPAREN', |
| 26 | +) |
| 27 | + |
| 28 | +t_PLUS = r'\+' |
| 29 | +t_MINUS = r'-' |
| 30 | +t_TIMES = r'\*' |
| 31 | +t_DIVIDE = r'/' |
| 32 | +t_LPAREN = r'\(' |
| 33 | +t_RPAREN = r'\)' |
| 34 | + |
| 35 | +def t_NUMBER(t): |
| 36 | + r'\d+' |
| 37 | + t.value = int(t.value) |
| 38 | + return t |
| 39 | + |
| 40 | +lexer = lex.lex() |
| 41 | +``` |
| 42 | + |
| 43 | +PLY 中 `token` 都用一个正则表达式规则来表示。规则都需要用 `t_` 开头 ,紧跟在 `t_` 之后单词则必须和 `tokens` 列表中某个值相对应。 |
| 44 | + |
| 45 | +对于简单的 `token` ,可以直接使用正则表达式定义: |
| 46 | + |
| 47 | +```python |
| 48 | +t_PLUS=r'\+' |
| 49 | +``` |
| 50 | + |
| 51 | +复杂的 `token` 则可以定义成一个函数,当输入字符串匹配正则表达式时,函数内的代码会被执行,在下面的函数中,输入会被转换为整数并存储在 `t.value` 中,并返回 `token` 类型为 `NUMBER` 。 |
| 52 | + |
| 53 | +```python |
| 54 | +def t_NUMBER(t): |
| 55 | + r'\d+' |
| 56 | + t.value = int(t.value) |
| 57 | + return t |
| 58 | +``` |
| 59 | + |
| 60 | +### 语法规则 |
| 61 | + |
| 62 | +#### 语法分析基础 |
| 63 | + |
| 64 | +```txt |
| 65 | +%left '+' '-' |
| 66 | +%left '*' '/' |
| 67 | +%% |
| 68 | +expr: |
| 69 | + INTEGER |
| 70 | + | expr + expr { $$ = $1 + $3; } |
| 71 | + | expr - expr { $$ = $1 - $3; } |
| 72 | + | expr * expr { $$ = $1 * $3; } |
| 73 | + | expr / expr { $$ = $1 / $3; } |
| 74 | + | '(' expr ')' { $$ = $2; } |
| 75 | +
|
| 76 | +``` |
| 77 | + |
| 78 | +第一部分定义了 `token` 类型和运算符的结合性。四种运算符都是左结合,同一行的运算符优先级相同,不同行的运算符,后定义的行具有更高的优先级。 |
| 79 | + |
| 80 | +语法规则使用了 `BNF` 定义。`BNF` 可以用来表达上下文无关(*context-free*)语言,大部分的现代编程语言都可以使用 `BNF`表示。上面的规则定义了一个产生式。产生式冒号左边的项 `expr` 被称为非终结符, `INTEGER` 和 `+`,`-`,`*`,`/` 被称为终结符,它们是由词法解析器返回的 `token`。 |
| 81 | + |
| 82 | +`PLY` 生成的语法分析器使用自底向上的归约(*shift-reduce*)方式进行语法解析,同时使用堆栈保存中间状态。以下是表达式 `1 + 2 * 3`的解析过程: |
| 83 | + |
| 84 | +```text |
| 85 | +1 . 1 + 2 * 3 |
| 86 | +2 1 . + 2 * 3 |
| 87 | +3 expr . + 2 * 3 |
| 88 | +4 expr + . 2 * 3 |
| 89 | +5 expr + 2 . * 3 |
| 90 | +6 expr + expr . * 3 |
| 91 | +7 expr + expr * . 3 |
| 92 | +8 expr + expr * 3 . |
| 93 | +9 expr + expr * expr . |
| 94 | +10 expr + expr . |
| 95 | +11 expr . |
| 96 | +``` |
| 97 | + |
| 98 | +点(`.`)表示当前的读取位置,随着 `.`从左向右移动,我们将读取的 `token `压入堆栈,当发现堆栈中的内容匹配了文法右部的语法规则,则将匹配的项从堆栈中弹出,将该文法左边的非终结符压入堆栈。这个过程持续进行,直到读取完所有的 `tokens`,并且只有启始非终结符(本例为 `expr`)保留在堆栈中。 |
| 99 | + |
| 100 | +产生式右侧的大括号中定义了该规则关联的动作,例如: |
| 101 | + |
| 102 | +```text |
| 103 | +expr: expr '+' expr { $$ = $1 + $3; } |
| 104 | +``` |
| 105 | + |
| 106 | +我们将堆栈中匹配该产生式右侧的项替换为产生式左侧的非终结符,本例中我们弹出 `expr '*' expr`,然后把 `expr`压回堆栈。 我们可以使用 `$position` 的形式访问堆栈中的项,`$1` 引用的是第一项,`$2` 引用的是第二项,以此类推。`$$` 代表的是归约操作执行后的堆栈顶。本例的动作是将三项从堆栈中弹出,两个表达式相加,结果再压回堆栈顶。 |
| 107 | + |
| 108 | +#### 使用 PLY 定义语法规则 |
| 109 | + |
| 110 | +```python |
| 111 | +import ply.yacc as yacc |
| 112 | + |
| 113 | +# Get the token map from the lexer. This is required. |
| 114 | +from calclex import tokens |
| 115 | + |
| 116 | +precedence = ( |
| 117 | + ('left', 'PLUS', 'MINUS'), |
| 118 | + ('left', 'TIMES','DIV'), |
| 119 | +) |
| 120 | + |
| 121 | +def p_expr(p): |
| 122 | + """expr : expr PLUS expr |
| 123 | + | expr MINUS expr |
| 124 | + | expr TIMES expr |
| 125 | + | expr DIV expr |
| 126 | + """ |
| 127 | + if p.slice[2].type == 'PLUS': |
| 128 | + p[0]=p[1]+p[3] |
| 129 | + elif p.slice[2].type == 'MINUS': |
| 130 | + p[0]=p[1]-p[3] |
| 131 | + elif p.slice[2].type == "TIMES": |
| 132 | + p[0]=p[1]*p[3] |
| 133 | + elif p.slice[2].type == "DIV": |
| 134 | + p[0]=p[1]/p[3] |
| 135 | + |
| 136 | +def p_expr_paren(p): |
| 137 | + """expr : LPAREN expr RPAREN""" |
| 138 | + p[0]=p[2] |
| 139 | + |
| 140 | +def p_expr_number(p): |
| 141 | + """expr : NUMBER""" |
| 142 | + p[0]=p[1] |
| 143 | + |
| 144 | +# Build the parser |
| 145 | +parser = yacc.yacc() |
| 146 | +``` |
| 147 | + |
| 148 | +`precedence` 定义了 `token` 的结合性和优先级。如上列所示,元组中的第一个元素表示 `token ` 的结合性,`left` 表示 `token` 是左结合的,同一行的 `token` 优先级相同,不同行的优先级从低向高排序,上例中 `TIMES` 和 `DIV` 的优先级比 `PLUS` 和 `MINUS` 高。 |
| 149 | + |
| 150 | +每个语法规则被定义成 Python的方法,方法的注释描述了该方法相应的上下文无关文法,语句实现了规则的语义行为。每个方法接受一个 `p` 参数,`p` 是一个包含有当前匹配语法的符号的序列,`p[i]` 与语法符号的对应关系如下: |
| 151 | + |
| 152 | +```python |
| 153 | +def p_expr_paren(p): |
| 154 | + """expr : LPAREN expr RPAREN""" |
| 155 | + # ^ ^ ^ ^ |
| 156 | + # p[0] p[1] p[2] p[3] |
| 157 | + p[0] = p[2] |
| 158 | +``` |
| 159 | + |
| 160 | +`PLY` 使用 `p[position]` 的形式访问堆栈,`p[0]` 相当于上文提到的`$$` ,`p[1]` 相当于`$1`,`p[2]` 相当于`$2`,以此类推。这里的动作则是弹出栈顶的三个元素,将 `p[2]` 值的值赋给 `p[0]`,之后压回堆栈。 |
| 161 | + |
| 162 | +## `sqlgpt-parser`的 `parser` 实现 |
| 163 | + |
| 164 | +`sqlgpt-parser` 中共有三个 SQL 语法解析器 ,分别在 [mysql_parser](../../src/sql_parser/mysql_parser)、 [oceanbase_parser](../../src/sql_parser/oceanbase_parser) 和 [odps_parser](../../src/sql_parser/odps_parser) 文件夹下,三个文件夹都包含`lexer.py`,`reserved.py`,`parser.py` 三个文件。 |
| 165 | + |
| 166 | +`lexer.py` 和`reserved.py` 文件都用于词法解析。`reserved.py` 中定义了 SQL 的关键字,关键字定义在`reserved` 和 `nonreserved` 两个变量中 ,`reserved` 里面包含了所有 `sql` 中不可用作列名、表名或者别名的关键字,`nonreserved` 则是可以用作列名、表名或者别名的关键字。 |
| 167 | + |
| 168 | +`lexer.py` 分为两部分,`tokens` 变量中定义了所有可用于 `parser` 的 `token` ,在这里会将 `reserved.py` 中的 SQL 关键词引入,将其变为 `parser` 可用的 `token`。 |
| 169 | + |
| 170 | +```python |
| 171 | +tokens = ( |
| 172 | + [ |
| 173 | + 'IDENTIFIER', |
| 174 | + 'DIGIT_IDENTIFIER', |
| 175 | + ... |
| 176 | + ] |
| 177 | + + list(reserved) |
| 178 | + + list(nonreserved) |
| 179 | +) |
| 180 | + |
| 181 | +``` |
| 182 | + |
| 183 | + 剩余部分则定义了用户输入会转换成什么样的 `token` 。 |
| 184 | + |
| 185 | +```python |
| 186 | +... |
| 187 | +t_BIT_MOVE_LEFT = r'<<' |
| 188 | +t_BIT_MOVE_RIGHT = r'>>' |
| 189 | +t_EXCLA_MARK = r'!' |
| 190 | + |
| 191 | +def t_DOUBLE(t): |
| 192 | + r"[0-9]*\.[0-9]+([eE][-+]?[0-9]+)?|[-+]?[0-9]+([eE][-+]?[0-9]+)" |
| 193 | + if 'e' in t.value or 'E' in t.value or '.' in t.value: |
| 194 | + t.type = "FRACTION" |
| 195 | + else: |
| 196 | + t.type = "NUMBER" |
| 197 | + return t |
| 198 | +... |
| 199 | +``` |
| 200 | + |
| 201 | +如上文所述,简单的 `token` 直接使用正则表达式定义,将与正则表达式匹配的值转换为 `t_` 后跟的 `token`,复杂的 `token` 则用方法定义,如`t_DOUBLE` 中,会对读入的值进一步判断,如果是小数,会将值的 `token` 设置为 `FRACTION` ,如果不是小数则设置为`NUMBER` 。 |
| 202 | + |
| 203 | +`parser.py` 文件同样分为两个部分,`precedence` 定义了 `token` 的优先级和结合性,剩余部分则定义了相应的语法规则以及对应的 `action` 。 |
| 204 | + |
| 205 | +```python |
| 206 | +precedence = ( |
| 207 | + ('right', 'ASSIGNMENTEQ'), |
| 208 | + ('left', 'PIPES', 'OR'), |
| 209 | + ('left', 'XOR'), |
| 210 | + ('left', 'AND', 'ANDAND'), |
| 211 | + ('right', 'NOT'), |
| 212 | + ... |
| 213 | + ('left', 'EXCLA_MARK'), |
| 214 | + ('left', 'LPAREN'), |
| 215 | + ('right', 'RPAREN'), |
| 216 | +) |
| 217 | + |
| 218 | +``` |
| 219 | + |
| 220 | +`right` 和 `left` 表示元组中的 `token` 是左结合还是右结合,优先级则是由低向高排列,上面的例子中 `RPAREN` 的优先级最高,`ASSIGNMENTEQ` 优先级最低。 |
| 221 | + |
| 222 | +`SQL` 的语法规则十分复杂,`parser.py` 中的大部分内容都是语法规则的定义。 `SQL` 的语法规则可以参照对应数据库的参考手册中的定义。例如 `MySQL` 数据库,可以参照它参考手册的 [SQL Statements](https://dev.mysql.com/doc/refman/8.0/en/sql-statements.html) 部分,其中 `DELETE` 语法定义如下: |
| 223 | + |
| 224 | +```text |
| 225 | +DELETE [LOW_PRIORITY] [QUICK] [IGNORE] FROM tbl_name [[AS] tbl_alias] |
| 226 | + [PARTITION (partition_name [, partition_name] ...)] |
| 227 | + [WHERE where_condition] |
| 228 | + [ORDER BY ...] |
| 229 | + [LIMIT row_count] |
| 230 | +``` |
| 231 | + |
| 232 | +我们可以在 `parser.py` 中找到 `DELETE` 的语法规则定义; |
| 233 | + |
| 234 | +```python |
| 235 | +def p_delete(p): |
| 236 | + r"""delete : DELETE FROM relations where_opt order_by_opt limit_opt |
| 237 | + | DELETE FROM relations partition where_opt order_by_opt limit_opt |
| 238 | + | DELETE table_name_list FROM relations where_opt order_by_opt limit_opt |
| 239 | + | DELETE table_name_list FROM relations partition where_opt order_by_opt limit_opt |
| 240 | + | DELETE FROM table_name_list USING relations where_opt order_by_opt limit_opt |
| 241 | + | DELETE FROM table_name_list USING relations partition where_opt order_by_opt limit_opt |
| 242 | + """ |
| 243 | + length=len(p) |
| 244 | + p_limit = p[length-1] |
| 245 | + if p_limit is not None: |
| 246 | + offset,limit = int(p_limit[0]),int(p_limit[1]) |
| 247 | + else: |
| 248 | + offset,limit=0,0 |
| 249 | + if p.slice[3].type=="relations": |
| 250 | + tables,table_refs=p[3],None |
| 251 | + elif p.slice[2].type=="table_name_list": |
| 252 | + tables,table_refs=p[4],p[2] |
| 253 | + else: |
| 254 | + tables,table_refs=p[3],p[5] |
| 255 | + p[0] = Delete(table=tables,table_refs=table_refs,where=p[length-3], order_by=p[length-2], limit=limit, offset=offset) |
| 256 | +``` |
| 257 | + |
| 258 | +这里 `p_delete` 的注释中语法规则与 `DELETE` 语法是对应的。当输入满足语法规则后,会调用方法中的函数,构建出 `AST ` 的 `DELETE` 节点。 |
| 259 | + |
| 260 | +完成语法规则的编写后就可以使用,该语法规则解析 `SQL` 语句,以 `mysql_parser` 为例。 |
| 261 | + |
| 262 | +```python |
| 263 | +from src.sql_parser.mysql_parser import parser as mysql_parser |
| 264 | +sql = "DELETE FROM t WHERE a=1" |
| 265 | +result = mysql_parser.parse(sql) |
| 266 | +``` |
| 267 | + |
| 268 | +如果你是第一次启动,这段代码会耗用很长时间,因为 PLY 需要编译语法文件,执行结果如下图所示,生成了 `SQL` 语句的执行计划树。 |
| 269 | + |
| 270 | + |
| 271 | + |
| 272 | +## 参考文献 |
| 273 | + |
| 274 | +* [TiDB 源码阅读系列文章(五)TiDB SQL Parser 的实现](https://cn.pingcap.com/blog/tidb-source-code-reading-5) |
| 275 | + |
| 276 | +* [PLY](https://github.com/dabeaz/ply) |
0 commit comments