要测试一个 Parser 和其他数据库的兼容性,SQL 生成工具是个不错的选择。核心思路很简单:解析 YACC 语法文件里的产生式,自动生成 SQL 语句,然后扔给目标数据库去执行,看结果是否匹配。这样一来,兼容性好不好,一跑便知。

01 工具使用
语法文件预处理
预处理的目的,是把语法文件里无关紧要的部分剔除干净,只留下各个语句的产生式。具体操作上,可以用 bison -v sql.y 命令获取语法规则(不带 Action),然后再从生成的文件中删掉终结符列表、非终结符列表、状态转换表这些冗余信息。下面是一个示例:
生成的 sql.output 文件内容中,我们只保留“语法”这一节。注意,保留的这一节还需要把序号去掉。
为了方便,整个预处理流程已经封装在 preprocess.sh 脚本里,处理后的文件可以直接喂给工具。最终生成的 .output 文件就是预处理后的语法文件。
SQL 语句生成
拿到干净的语法文件后,就可以用它来生成 SQL 了。工具支持以下参数:
- -b:指定语法文件,必选。语法文件就是上面
preprocess.sh处理后的那个。 - -n:指定待生成的产生式名称,必选。
- -R:随机生成模式,可选,默认是枚举模式。
- -o:指定生成 SQL 语句的保存文件,可选,默认是
report.csv。 - -N:限制生成 SQL 条数,可选,默认不限制。
02 工具实现
整个工具由两个 package 组成:yacc_parser 和 sql_generator,分别负责 Token 解析和 SQL 生成。
产生式的表示方法
type SeqInfo struct {
Items []string
}
type Production struct {
Head string // 产生式头部
Alter []SeqInfo // 产生式 body
}
Token 解析
Tokenize 函数将读取的语法文件中的字符 Token 化,每次调用返回一个 Token。这个函数只处理了简单的分隔符和引号,并没有实现标准词法分析器的正则匹配,够用就行。
Parse 函数会调用 Tokenize,每拿到一个 Token 后,根据当前状态和 Token 类型,将一连串 Token 组装成 Production 结构。
SQL 生成
SQL 生成有两种模式:枚举和随机。
1、枚举
枚举模式的实现思路是:用一个链表来保存待 resolve 的 Token。每次从链表头取一个 Token,并自增该 Token 出现的次数;然后根据每个子表达式中 Token 在记录中间出现次数是否大于指定次数,筛选出可以继续推导的子表达式。
同时,用两个数组分别记录当前所取的子表达式下标(choice)和当前最大子表达式下标(max),这样下次自增 choice 就能取下一条表达式。
筛选之后,选择 choice 位置的产生式右部子表达式,将其全部 Token 插入链表头部。接着判断头部是否为 literal 或 keyword:如果是,就取出来放入 SQL 数组;如果不是,就继续循环处理链表。
当处理到当前产生式的末尾时(判断条件是 choice > max),就会尝试“进位”——把记录的当前所取位置数组的最后一位自增。举个例子:max 数组是 [1, 2, 1, 3],choice 数组是 [0, 0, 0, 3],进位后 choice 变成 [0, 0, 1, 0],表示最后一个位置已经遍历完,现在将倒数第二位自增,最后一位置零,然后继续下一轮排列组合。
生成过程通过递归实现。比如下面这条产生式,处理逻辑如图所示:
show_tables_stmt: SHOW TABLES FROM name '.' name with_comment
| SHOW TABLES FROM name with_comment
| SHOW TABLES with_comment
with_comment: WITH COMMENT
| %empty
name: IDENT
|
根据记录的 choice 值,选择产生式的第 choice 条子表达式,反复推导直到生成一条完整的 SQL。然后对 choice 数组进位,继续下一轮选择。
2、随机
随机生成模式与枚举模式大同小异,区别在于它不会顺序遍历产生式 body 列表中的每个 Token,而是随机挑一个 Token 来组成 SQL,其余逻辑基本一致。
