一、前言
1.1 作者介绍
王波,SphereEx MeshLab 研发工程师,目前专注于 Database Mesh,Cloud Native 的研发。Linux,llvm,yacc,ebpf user。 Gopher & Rustacean and c bug hunter。
GitHub: https://github.com/wbtlb
1.2 背景
在上篇文章《Pisa-Proxy 之 SQL 解析实践》中介绍了 Pisa-Proxy 的核心模块之一 SQL 解析器的相关内容。在 MySQL 和 PostgreSQL 中 SQL 解析是通过 Yacc 实现的,同样 Pisa- Proxy 的 SQL 解析器是由类似 Yacc 这样的工具实现的,所以本篇文章会围绕 SQL 解析器为大家介绍一些编译原理和 Lex & Yacc 的使用,同时也会为读者展示如何通过 Lex & Yacc 实现一个简单的 SQL 解析器。从而帮助大家更好地理解 Pisa-Proxy 中 SQL 解析器是如何工作的。
二、编译器初探
一个程序语言不论是我们常用的 Java,Golang 或者是 SQL 本质上都是一个记号系统,如同自然语言一样,它的完整定义应该包括语法和语义两个方面。一种语言的语法其实是对应的一组规则,用它可以形成和产生一个合适的程序。当前使用最广泛的手段是上下文无关的文法,上下文无关的文法作为程序设计语言语法的描述工具。语法只是定义什么样的符号序列是合法的,与这些符号的含义毫无关系。然而在语义中分为两类:静态语义和动态语义。静态语义是指一系列的限定规则,并确定哪些语法对于程序来说是合适的;动态语义也称作运行语义或者执行语义,明确程序具体要计算什么。
2.1 编译器工作流程
如图 2.1.1 中所示,通常编译器将源代码编译成可执行文件主要有以下几步:
- 对源文件进行扫描,将源文件的字符流拆分分一个个的词(token),此为词法分析
- 根据语法规则将这些记号构造出语法树,此为语法分析
- 对语法树的各个节点之间的关系进行检查,检查语义规则是否被违背,同时对语法树进行必要的优化,此为语义分析
- 遍历语法树的节点,将各节点转化为中间代码,并按特定的顺序拼装起来,此为中间代码生成
- 对中间代码进行优化
- 将中间代码转化为目标代码
- 对目标代码进行优化,生成最终的目标程序
对于 SQL 解析来说,就可以将上图中的步骤简化为如图 2.1.2 的形式,源码输入(SQL 语句),将 SQL 语句进行词法分析,生成 SQL 中特定的 token 记号流。然后拿到记号流后进行语法分析后生成最终的 SQL AST。
2.2 词法分析
上文中提到,无论是编译器还是 SQL 解析器有一个关键步骤就是要对源文件做词法分析,词法分析我们可以理解为对 SQL 语句本身做分词处理。那么在这个阶段,SQL 解析器要做的工作就是从左到右扫描源文件,将 SQL 语句分割成一个个的 token,这里说的 token 是指 SQL 中不能再进一步分割的一串字符。例如图 2.1.2 中的 SQL 语句,经过词法分析后,生成的 token 为:SELECT
、*
、FROM
、pisa_proxy
等等。
在 SQL 语句中能用到的 token 类别也是有限的,比如保留字 SELECT
、INSERT
、DELETE
等等。还有操作符,比如:算术操作符、比较操作符。还有标识符,比如:内置函数名等等。在此阶段每扫描一个 token 会被维护到一个数据结构中,然后在下个阶段语法分析阶段使用。
通常来说,词法分析有直接扫描,正则匹配扫描方式。
2.2.1 直接扫描法
直接扫描法逻辑非常清晰,每次扫描根据第一个字符判断属于哪种类型的 token,然后采取不同的策略扫描出一个完整的 token,然后再进行下一轮扫描。在 Pisa-Proxy 中的 SQL 解析中,词法分析就采用了这种实现方式,用 Python 展示如何实现一个简单的 SQL 词法分析器对 SQL 进行扫描,代码如下:
# -*- coding: utf-8 -*-
single_char_operators_typeA = {
";", ",", "(", ")","/", "+", "-", "*", "%", ".",
}
single_char_operators_typeB = {
"<", ">", "=", "!"
}
double_char_operators = {
">=", "<=", "==", "~="
}
reservedWords = {
"select", "insert", "update", "delete", "show",
"create", "set", "grant",