三十分钟成为 Contributor | 提升 TiDB Parser 对 MySQL 8.0 语法的兼容性

谢腾进 赵一霖 社区动态 2019-08-08

TiDB的一大特性就是和 MySQL 高度兼容,目标是让用户能够无需修改代码即可从 MySQL 迁移至 TiDB。要达成这个目标,需要完成两个提升兼容性的任务,分别是「语法兼容」和「功能行为兼容」。

本次活动聚焦于语法兼容,提升 TiDB SQL Parser 对 MySQL 8.0 的语法支持。对于新的贡献者而言,除了能将理论知识运用到实践上以外,还可以从中体验参与一个开源项目的整体流程与规范。

我们把 TiDB Parser 整体看作一个函数,输入是 SQL 字符串,输出是用于描述 SQL 语句的抽象语法树(AST)。在这个转换的过程中涉及到的组件有两个:一个是 lexer,负责将字符流变成 Token,并赋予它们类别标识,这个过程叫「Tokenize」;另一个是 parser,负责将 Token 转为树状结构,便于将来遍历和转换,这个过程叫「Parse」;TiDB 使用了 parser 生成工具 goyacc,它能够根据parser.y生成parser.go,其中包含了名称为Parse的函数接口,供 TiDB 直接使用。更多关于 TiDB Parser 以及 Lex & Yacc 的信息请参考TiDBSQL Parser 的实现

参与流程

参与流程分为 7 步:领任务 -> 写 test case -> make test -> coding -> 补充 test case -> make test -> 提 PR

1. 从 Issue 领取任务

我们会在Improve parser compatibility周期性发布不兼容的 Bad SQL Case 组,每组 Case 都会构成一个 Issue,Contributor 选择某个 Issue,在它的下方评论:Let me fix it。在我们将 Contributor 的 Github ID 添加到 Index Issue 中后,即完成任务的领取。

2. 编写测试用例

根据 Issue 描述的情况在parser_test.go中编写测试用例,除了 Issue 中提到的 Case 以外,可以适当添加更多的 Case。保证目标 SQL Case 语句能够通过 Parser 解析,并且通过 Restore 还原为预期的 SQL。

3. 执行所有测试

parser 根目录下运行make test,确保第一次测试失败,并且失败的 Case 是第 2 步编写的。

4. 编码

Contributor 修改文法规则。对于涉及到语义层面的规则变动,需要同步修改 AST 节点的数据结构(AST 节点定义在parser/ast中)。TiDB 通过 AST 树生成执行计划,修改 AST 节点可能会影响 TiDB 的行为,因此应尽量保持原有结构,不改变原有的属性,如果确实有修改 AST 树必要,我们会帮助 Contributor 检查 TiDB 的行为是否正常。另外,AST 节点其中的两个接口方法是AcceptRestore,分别用于遍历子树和通过 AST 树还原 SQL 字符串。应确保它们的行为都符合预期。

另外,还要检查新加的规则是否存在冲突问题。「冲突」可以被理解为当 parser 读到某个 token 时,有两种或以上的方式来构造语法树,从而导致歧义。goyacc 所生成的 parser 采用了LALR(1)方法进行解析,冲突有两类:一类是两条规则都能被用于归约,称为reduce-reduce冲突。另一类是既能使用一条规则归约,又能按照另一条规则移进下一个 token,称为shift-reduce冲突。可以通过指定优先级的方式消除冲突,具体可以参考 yacc 的%precedence 和 %prec 指示

编码完成后在项目根目录下运行make parser,这时会执行 goyacc 重新生成parser.go文件。

5. 补充 test case

根据实际情况尽可能提升测试覆盖率。

6. 执行所有测试

parser 根目录下运行make test,确保测试通过。

7. 提交 PR

提交 PR 之前请先阅读contributing 指南。下面是 PR 的模板,逐项填写即可。

### What problem does this PR solve? #### [ Put the subtask title here ] Issue: [ put the subtask issue link here ] #### MySQL Syntax: [ describe MySQL syntax here ] #### Bad SQL Case: [ give a SQL statement example that passes MySQL but fails TiDB parser ] [ give a SQL statement example that passes MySQL but fails TiDB parser ] ... ### Check List Tests - Unit test

示例

下面以添加 「REMOVE PARTITIONING」 语法支持为例解释说明整个过程

1. 从 Issue 领取任务

这里找到REMOVE PARTITIONING子任务。子任务 Issue中有若干不兼容的 Case。

首先,手动测试任一 Case,发现在 MySQL 下输出:

Error1505: Partition management on a not partitioned table is not possible

而在 TiDB 下输出:

Error 1064: You have an error in your SQL syntax;checkthe manual that corresponds to your TiDB version for the right syntax to use line 1 column 20 near"remove partitioning"

这意味着 parser 无法识别 remove 关键字。

确认了问题存在后,到该 Issue下评论:Let me fix it,完成任务领取。

2. 编写测试用例

parser_test.go下寻找合适位置添加测试用例,这里我们选择在func (s *testParserSuite) TestDDL(c *C)末尾添加:

// for remove partitioning {"alter table t remove partitioning",true,"ALTER TABLE `t` REMOVE PARTITIONING"}, {"alter table db.ident remove partitioning",true,"ALTER TABLE `db`.`ident` REMOVE PARTITIONING"}, {"alter table t lock = default remove partitioning",true,"ALTER TABLE `t` LOCK = DEFAULT REMOVE PARTITIONING"},

这里每一个 test case 分成了三部分,第一列是用于测试的 SQL 语句,第二列是「是否期望第一列的语句 parse 通过」,第三列是「从语法树 restore 后期望的 SQL 语句」。具体可以参考TestDDL.RunTest()

3. 执行所有测试

parser 根目录下运行make test,确保第一次测试失败,并且 fail 的 case 是第 2 步编写的。

FAIL: parser_test.go:1664: testParserSuite.TestDDLparser_test.go:2148: s.RunTest(c, table) parser_test.go:318: c.Assert(err, IsNil, comment) ... value *errors.withStack= line1column20near"remove partitioning"("line 1 column 20 near \"remove partitioning\" ") ... source altertablet删除partitioning

错误信息和期望的一致,则可以开始进行下一步。

4. 编码

4.1 修改parser.y文件

首先从MySQL 文法中找到 remove partitioning 的定义:

alter_table_stmt:ALTERTABLE_SYM table_ident opt_alter_table_actions|ALTERTABLE_SYM table_ident standalone_alter_table_action opt_alter_table_actions: opt_alter_command_list|opt_alter_command_list alter_table_partition_options alter_table_partition_options: partition_clause|REMOVE_SYM PARTITIONING_SYM

经过分析可得该语法只能出现在 SQL 语句的最后一个部分,并且只能出现一次。

parser.y中找到AlterTableStmt,其中一条规则是:

"ALTER"IgnoreOptional"TABLE"TableName AlterTableSpecListOpt PartitionOpt

其中最后一个符号 PartitionOpt 和 MySQL 中partition_clause非常相似,为了支持 remove partitioning,容易想到引入一条规则:

AlterTablePartitionOpt: PartitionOpt| "REMOVE" "PARTITIONING"

PartitionOpt的语义动作移到AlterTablePartitionOpt中,REMOVE PARTITIONING部分先返回nil,并添加 parser 警告,表示目前 parser 能够解析但 TiDB 尚未支持该功能。修改后的规则如下:

AlterTablePartitionOpt: PartitionOpt {if$1!= nil {$$= &ast.AlterTableSpec{ Tp: ast.AlterTablePartition, Partition:$1.(*ast.PartitionOptions), } }else{$$= nil } } |"REMOVE""PARTITIONING"{$$= nil yylex.AppendError(yylex.Errorf("The REMOVE PARTITIONING clause is parsed but ignored by all storage engines.")) parser.lastErrorAsWarn() }

由于REMOVEPARTITIONING都是新添加的关键字,如果不做任何处理,lexer 扫描的时候只会将它们看作普通的标识符。于是需要在parser.y%token字段上补充声明,其中一个目的是为该字符串产生一个tokenID(一个整数),供 lexer 标识。另外goyacc也会对parser.y中所有的字符串常量进行检查,如果没有相应的token声明,会报Undefined symbol的错误。

为支持这两个关键字,我们在文件开头的token字段添加声明。由于REMOVEPARTITIONING都是非保留关键字,它们应被添加在含有The following tokens belong to UnReservedKeyword注释的下方。此外,非保留字说明它们能够作为标识符Identifier,因此在Identifier规则下的UnRerservedKeyword中也应加上REMOVEPARTITIONING

关于如何确定一个关键字是保留的还是非保留的,可以参考MySQL 文档

4.2 增加「关键字-tokenID」映射

前文提到,添加声明是为了让 lexer 能够识别关键字并赋予对应的tokenID,对于 lexer 而言,它需要一个从关键字字符串到tokenID的映射关系。在 TiDB parser 中,这个映射关系就是misc.go中的tokenMap结构。

在这个例子中,我们往tokenMap中添加removepartitioning(如果不添加,会使关键字一致性的检查测试失败)。

4.3 修改 AST 节点

到目前为止,我们已经让goyacc生成的 parser 能够解析 remove partitioning 语法。但是,解析完成后并没有返回有效的数据结构(4.1 中我们返回了nil),这意味着 parser 不能够根据语法树重新生成原 SQL 语句。

所以,要修改现有的 AST 节点,使它们能够以某种形式保存 remove partitioning 信息。回顾目前规则层面的修改:

AlterTableStmt:"ALTER"IgnoreOptional"TABLE"TableName AlterTableSpecListOpt PartitionOpt

已改为:

AlterTableStmt:"ALTER"IgnoreOptional"TABLE"TableName AlterTableSpecListOpt AlterTablePartitionOptAlterTablePartitionOpt:PartitionOpt |"REMOVE""PARTITIONING"

其中几个非终结符对应的数据结构如下:

AlterTableSpecListOpt->[]AlterTableSpec PartitionOpt->PartitionOptions

关于修改节点,有几种方案可以选择:

  1. 增加一个新的节点 struct,表示AlterTablePartitionOpt。其中包含PartitionOptions和一个 bool 值,表示是否为 remove partitioning。

  2. 将 remove partitioning 看作PartitionOptions,在内部添加一个 bool 成员isRemovePartitioning以做区分。

  3. PartitionOpt和 remove partitioning 都看作AlterTableSpec,为AlterTableSpec的添加一个类型,单独表示 remove partitioning。

经过比较和分析,我们发现第一个方案会引入新的数据结构,有较大的概率会引起现有的 TiDB 测试不过,为此可能要修改 TiDB 方面的代码,工作量大的同时提高了 parser 的复杂度,因此作为备选方案;第二个方案没有引入新的数据结构,但是修改了现有的数据结构(加了个 bool 成员);第三个方案即没有添加也没有修改数据结构,并且能够以较少的代码完成任务,作为首选方案。

在以上的选择中,我们遵循「尽量不修改 AST 节点结构」的原则。

按照方案三,观察AlterTableSpec,其定义如下:

typeAlterTableSpecstruct { node ...TpAlterTableTypeNamestring ... }

其中一个成员是Tp,它所属的类型包含了AlterTable的许多操作,例如AddColumnAddConstraintDropColumn等。我们的任务是添加一个类似的 Type,让它能够表示 remove partitioning。

到这里,解析完 SQL 语句生成的 AST 树已经包含 remove partitioning 的信息了。接下来要处理 Restore,让它能够从 AST 树还原出 SQL 语句。AlterTableSpec的 Restore 十分简单,加一个 case 即可,这里不再赘述。

4.4 完善parser.y

第一次修改parser.y的时候我们在新加规则的语义动作中返回了nil,原因是尚未确定 AST 是否需要修改,以及如何修改。而到这一步,这些都已经确定下来了,把 remove partitioning 看作AlterTableSpec类型:

|"REMOVE""PARTITIONING"{$$= &ast.AlterTableSpec{ Tp: ast.AlterTableRemovePartitioning, } yylex.AppendError(yylex.Errorf("The REMOVE PARTITIONING clause is parsed but ignored by all storage engines.")) parser.lastErrorAsWarn() }

注意,这里必须抛出警告,表示虽然目前 parser 能够解析该语法,但实际上 TiDB 并未支持相应的功能。

5. 补充 test case

这里,所有的代码修改引入的分支结构都能够被现有的测试覆盖,因此在提升覆盖率上没有需求。当然,如果想要测试更多类似的 case,可以将它们添加到前面提到的TestDDL函数中。

6. 执行所有测试(make test

gofmt (simplify) sh test.sh ok github.com/pingcap/parser4.294s coverage:62.1% of statementsin./... ok github.com/pingcap/parser/ast 2.090s coverage: 42.3% of statements in ./... ok github.com/pingcap/parser/auth 1.155s coverage: 1.3% of statements in ./... [no tests to run] ok github.com/pingcap/parser/charset 1.094s coverage: 2.0% of statements in ./... ok github.com/pingcap/parser/format 1.114s coverage: 2.5% of statements in ./... ? github.com/pingcap/parser/goyacc [no test files] ok github.com/pingcap/parser/model 1.100s coverage: 3.5% of statements in ./... ok github.com/pingcap/parser/ mysql 1.102 s覆盖率:1.3%的语句。/... [no tests to run] ok github.com/pingcap/parser/opcode 1.099s coverage: 1.4% of statements in ./... ok github.com/pingcap/parser/terror 1.091s coverage: 2.3% of statements in ./... ok github.com/pingcap/parser/types 1.106s coverage: 7.0% of statements in ./...

确保所有的 test 都是 ok 的状态。

7. 提交 PR

按照 PR 模板逐项填写。

### What problem does this PR solve? #### Fix compatibility problem about keyword REMOVE PARTITIONING Issue: pingcap/parser#402#### MySQL Syntax: alter_specification: ... | REMOVE PARTITIONING ### Bad SQL Case: alter table t remove partitioning; alter table t lock = default, remove partitioning; alter table t drop check c, remove partitioning; ### Check List Tests - Unit test

需要特别指出的是,我们鼓励各位 Contributor 多使用make test。当不知道从何处入手或者失去目标时,make test输出的错误信息或许能够引导大家进行思考和探索

Tips:完整的 PR 示例

FAQ

以下是在增加 remove partitioning 语法支持时遇到的问题和解决方法。

Q1. 为什么不在PartitionOpt中直接添加规则?

A1PartitionOpt用于匹配含有partition by的 SQL 语句,除了Alter Table语句以外,它还被Create Table使用,而remove partitioning只存在于alter table语句中,因此不能在PartitionOpt中添加规则。

Q2. 执行 make test 时报错:

parser.y:1100:1: undefined symbol"PARTITIONING"parser.y:1100:1: undefined symbol"REMOVE"make[1]: ***[Makefile:19: parser] Error 1

A2:在 yacc 中,出现在规则中的字符串,要么是token(终结符),要么是非终结符。这里引用一段 yacc 的规范:

Names refer toeithertokensornonterminal symbols. Names representing tokens mustbedeclared; this is most simply done by writing%token name1 name2 . . .

所以,修复方法是在parser.y%token字段上添加PARTITIONINGREMOVE的声明。

第三季。执行做测试时报错:

c.Assert(len(tokenMap)-len(aliases), Equals, keywordCount-len(windowFuncTokenMap))...obtainedint=454...expectedint=456

A3:这是关键字的一致性检查出了问题,解决方案是补充tokenMap(它是关键字到token ID的映射,被 scanner 用来判断某个字符串是否为关键字)。

Q4. 执行 make test 时报错:

FAIL: parser_test.go:1666: testParserSuite.TestDDL parser_test.go:2166: s.RunTest(c, table) parser_test.go:351: c.Assert(restoreSQLs,Equals, expectSQLs, comment)...obtainedstring="ALTER TABLE `t`"...expectedstring="ALTER TABLE `t` REMOVE PARTITIONING"...restore ALTER TABLE`t`; expect ALTER TABLE`t`REMOVE PARTITIONING

A4:这个错误说明 parser 已经解析通过,但不能从语法树中恢复原 SQL 语句的 remove partitioning 部分。此时应检查相应 AST 节点的 Restore 方法是否正确处理了REMOVE PARTITIONING

最后欢迎大家加入TiDBContributor Club,无门槛参与开源项目,改变世界从这里开始吧!

点击查看更多成为 Contributor 系列文章

目录