{
    "componentChunkName": "component---src-templates-blog-blog-detail-tsx",
    "path": "/blog/30mins-become-contributor-of-tidb-20190808",
    "result": {"pageContext":{"blog":{"id":"Blogs_175","title":"三十分钟成为 Contributor | 提升 TiDB Parser 对 MySQL 8.0 语法的兼容性","tags":["TiDB","社区","Contributor"],"category":{"name":"社区动态"},"summary":"本次活动聚焦于语法兼容，提升 TiDB SQL Parser 对 MySQL 8.0 的语法支持。对于新的贡献者而言，除了能将理论知识运用到实践上以外，还可以从中体验参与一个开源项目的整体流程与规范。","body":"TiDB 的一大特性就是和 MySQL 高度兼容，目标是让用户能够无需修改代码即可从 MySQL 迁移至 TiDB。要达成这个目标，需要完成两个提升兼容性的任务，分别是「语法兼容」和「功能行为兼容」。\n\n**本次活动聚焦于语法兼容，提升 TiDB SQL Parser 对 MySQL 8.0 的语法支持。对于新的贡献者而言，除了能将理论知识运用到实践上以外，还可以从中体验参与一个开源项目的整体流程与规范。**\n\n我们把 TiDB Parser 整体看作一个函数，输入是 SQL 字符串，输出是用于描述 SQL 语句的抽象语法树（AST）。在这个转换的过程中涉及到的组件有两个：一个是 lexer，负责将字符流变成 Token，并赋予它们类别标识，这个过程叫「Tokenize」；另一个是 parser，负责将 Token 转为树状结构，便于将来遍历和转换，这个过程叫「Parse」；TiDB 使用了 parser 生成工具 goyacc，它能够根据 `parser.y` 生成 `parser.go`，其中包含了名称为 `Parse` 的函数接口，供 TiDB 直接使用。更多关于 TiDB Parser 以及 Lex & Yacc 的信息请参考 [TiDB SQL Parser 的实现](https://pingcap.com/blog-cn/tidb-source-code-reading-5/)。\n\n## 参与流程\n\n参与流程分为 7 步：**领任务  -> 写 test case -> make test -> coding -> 补充 test case -> make test -> 提 PR**。\n\n### 1. 从 Issue 领取任务\n\n我们会在 [Improve parser compatibility](https://github.com/pingcap/tidb/issues/11486) 周期性发布不兼容的 Bad SQL Case 组，每组 Case 都会构成一个 Issue，Contributor 选择某个 Issue，在它的下方评论：**Let me fix it**。在我们将 Contributor 的 Github ID 添加到 Index Issue 中后，即完成任务的领取。\n\n### 2. 编写测试用例\n\n根据 Issue 描述的情况在 [`parser_test.go`](https://github.com/pingcap/parser/blob/master/parser_test.go) 中编写测试用例，除了 Issue 中提到的 Case 以外，可以适当添加更多的 Case。保证目标 SQL Case 语句能够通过 Parser 解析，并且通过 Restore 还原为预期的 SQL。\n\n### 3. 执行所有测试\n\nparser 根目录下运行 `make test`，确保第一次测试失败，并且失败的 Case 是第 2 步编写的。\n\n### 4. 编码\n\nContributor 修改文法规则。对于涉及到语义层面的规则变动，需要同步修改 AST 节点的数据结构（AST 节点定义在 `parser/ast` 中）。TiDB 通过 AST 树生成执行计划，修改 AST 节点可能会影响 TiDB 的行为，因此应尽量保持原有结构，不改变原有的属性，如果确实有修改 AST 树必要，我们会帮助 Contributor 检查 TiDB 的行为是否正常。另外，AST 节点其中的两个接口方法是 `Accept` 和 `Restore`，分别用于遍历子树和通过 AST 树还原 SQL 字符串。应确保它们的行为都符合预期。\n\n另外，还要检查新加的规则是否存在冲突问题。「冲突」可以被理解为当 parser 读到某个 token 时，有两种或以上的方式来构造语法树，从而导致歧义。goyacc 所生成的 parser 采用了 `LALR(1)` 方法进行解析，冲突有两类：一类是两条规则都能被用于归约，称为 `reduce-reduce` 冲突。另一类是既能使用一条规则归约，又能按照另一条规则移进下一个 token，称为 `shift-reduce` 冲突。可以通过指定优先级的方式消除冲突，具体可以参考 yacc 的 [%precedence 和 %prec 指示](https://www.gnu.org/software/bison/manual/html_node/Precedence.html#Precedence)。\n\n编码完成后在项目根目录下运行 `make parser`，这时会执行 goyacc 重新生成 `parser.go` 文件。\n\n### 5. 补充 test case\n\n根据实际情况尽可能提升测试覆盖率。\n\n### 6. 执行所有测试\n\nparser 根目录下运行 `make test`，确保测试通过。\n\n### 7. 提交 PR\n\t\n提交 PR 之前请先阅读 [contributing 指南](https://github.com/pingcap/tidb/blob/master/CONTRIBUTING.md)。下面是 PR 的模板，逐项填写即可。\n\n```\t\n### What problem does this PR solve?\n\n#### [ Put the subtask title here ]\n\nIssue: [ put the subtask issue link here ]\n\n#### MySQL Syntax:\n\n[ describe MySQL syntax here ]\n\n#### Bad SQL Case:\n\n[ give a SQL statement example that passes MySQL but fails TiDB parser ]\n\n[ give a SQL statement example that passes MySQL but fails TiDB parser ]\n\t\n...\n\t\n### Check List\n\t\nTests\n\n- Unit test\n```\n\n## 示例\n\n**下面以添加 「REMOVE PARTITIONING」 语法支持为例解释说明整个过程**。\n\n### 1. 从 Issue 领取任务\n\n从 [这里](https://github.com/pingcap/tidb/issues/11486) 找到 `REMOVE PARTITIONING` 子任务。[子任务 Issue](https://github.com/pingcap/parser/issues/402) 中有若干不兼容的 Case。\n\n首先，手动测试任一 Case，发现在 MySQL 下输出：\n\n```\nError 1505: Partition management on a not partitioned table is not possible\n```\n\n而在 TiDB 下输出：\n\n```\nError 1064: You have an error in your SQL syntax; check the manual that corresponds to your TiDB version for the right syntax to use line 1 column 20 near \"remove partitioning\"\n```\n\n这意味着 parser 无法识别 remove 关键字。\n\n确认了问题存在后，到 [该 Issue](https://github.com/pingcap/parser/issues/402) 下评论：**Let me fix it**，完成任务领取。\n\n### 2. 编写测试用例\n\n在 `parser_test.go` 下寻找合适位置添加测试用例，这里我们选择在 `func (s *testParserSuite) TestDDL(c *C)` 的 [末尾](https://github.com/pingcap/parser/pull/396/files#diff-688912c3f38a8a80d6bdc16c02088d69R2172) 添加：\n\n```\n// for remove partitioning\n{\"alter table t remove partitioning\", true, \"ALTER TABLE `t` REMOVE PARTITIONING\"},\n{\"alter table db.ident remove partitioning\", true, \"ALTER TABLE `db`.`ident` REMOVE PARTITIONING\"},\n{\"alter table t lock = default remove partitioning\", true, \"ALTER TABLE `t` LOCK = DEFAULT REMOVE PARTITIONING\"},\n```\n\n这里每一个 test case 分成了三部分，第一列是用于测试的 SQL 语句，第二列是「是否期望第一列的语句 parse 通过」，第三列是「从语法树 restore 后期望的 SQL 语句」。具体可以参考 [`TestDDL.RunTest()`](https://github.com/pingcap/parser/blob/53c769c5836485c83d5f339faab97ef5d853d560/parser_test.go#L308)。\n\n### 3. 执行所有测试\n\nparser 根目录下运行 `make test`，确保第一次测试失败，并且 fail 的 case 是第 2 步编写的。\n\n```\nFAIL: parser_test.go:1664: testParserSuite.TestDDL\n\nparser_test.go:2148:\n    s.RunTest(c, table)\nparser_test.go:318:\n    c.Assert(err, IsNil, comment)\n... value *errors.withStack = line 1 column 20 near \"remove partitioning\"  (\"line 1 column 20 near \\\"remove partitioning\\\" \")\n... source alter table t remove partitioning\n```\n\n错误信息和期望的一致，则可以开始进行下一步。\n\n### 4. 编码\n\n#### 4.1 修改 `parser.y` 文件\n\n首先从 [MySQL 文法](https://github.com/mysql/mysql-server/blob/8.0/sql/sql_yacc.yy) 中找到 remove partitioning 的定义：\n\n```\nalter_table_stmt: ALTER TABLE_SYM table_ident opt_alter_table_actions\n| ALTER TABLE_SYM table_ident standalone_alter_table_action\n\nopt_alter_table_actions: opt_alter_command_list\n| opt_alter_command_list alter_table_partition_options\n\nalter_table_partition_options: partition_clause\n| REMOVE_SYM PARTITIONING_SYM\n```\n\n经过分析可得该语法只能出现在 SQL 语句的最后一个部分，并且只能出现一次。\n\n在 `parser.y` 中找到 `AlterTableStmt`，其中一条规则是：\n\n```\n\"ALTER\" IgnoreOptional \"TABLE\" TableName AlterTableSpecListOpt PartitionOpt\n```\n\n其中最后一个符号 PartitionOpt 和 MySQL 中 `partition_clause` 非常相似，为了支持 remove partitioning，容易想到引入一条规则：\n\n```\nAlterTablePartitionOpt: PartitionOpt | \"REMOVE\" \"PARTITIONING\"\n```\n\n将 `PartitionOpt` 的语义动作移到 `AlterTablePartitionOpt` 中，`REMOVE PARTITIONING` 部分先返回 `nil`，并添加 parser 警告，表示目前 parser 能够解析但 TiDB 尚未支持该功能。修改后的规则如下：\n\n```\nAlterTablePartitionOpt:\n\tPartitionOpt\n\t{\n\t\tif $1 != nil {\n\t\t\t$$ = &ast.AlterTableSpec{\n\t\t\t\tTp: ast.AlterTablePartition,\n\t\t\t\tPartition: $1.(*ast.PartitionOptions),\n\t\t\t}\n\t\t} else {\n\t\t\t$$ = nil\n\t\t}\n\t}\n|\t\"REMOVE\" \"PARTITIONING\"\n\t{\n\t\t$$ = nil\n\t\tyylex.AppendError(yylex.Errorf(\"The REMOVE PARTITIONING clause is parsed but ignored by all storage engines.\"))\n\t\tparser.lastErrorAsWarn()\n\t}\n```\n\n由于 `REMOVE` 和 `PARTITIONING` 都是新添加的关键字，如果不做任何处理，lexer 扫描的时候只会将它们看作普通的标识符。于是需要在 `parser.y` 的 `%token` 字段上补充声明，其中一个目的是为该字符串产生一个 `tokenID`（一个整数），供 lexer 标识。另外 `goyacc` 也会对 `parser.y` 中所有的字符串常量进行检查，如果没有相应的 `token` 声明，会报 `Undefined symbol` 的错误。\n\n为支持这两个关键字，我们在文件开头的 `token` 字段添加声明。由于 `REMOVE` 和 `PARTITIONING` 都是非保留关键字，它们应被添加在含有 `The following tokens belong to UnReservedKeyword` [注释](https://github.com/pingcap/parser/blob/53c769c5836485c83d5f339faab97ef5d853d560/parser.y#L274) 的下方。此外，非保留字说明它们能够作为标识符 `Identifier`，因此在 `Identifier` 规则下的 [`UnRerservedKeyword`](https://github.com/pingcap/parser/blob/53c769c5836485c83d5f339faab97ef5d853d560/parser.y#L3717) 中也应加上 `REMOVE` 和 `PARTITIONING`。\n\n关于如何确定一个关键字是保留的还是非保留的，可以参考 [MySQL 文档](https://dev.mysql.com/doc/refman/8.0/en/keywords.html)。\n\n#### 4.2 增加「关键字-`tokenID`」映射\n\n前文提到，添加声明是为了让 lexer 能够识别关键字并赋予对应的 `tokenID`，对于 lexer 而言，它需要一个从关键字字符串到 `tokenID` 的映射关系。在 TiDB parser 中，这个映射关系就是 [`misc.go`](https://github.com/pingcap/parser/blob/53c769c5836485c83d5f339faab97ef5d853d560/misc.go) 中的 `tokenMap` 结构。\n\n在这个例子中，我们往 `tokenMap` 中添加 `remove` 和 `partitioning`（如果不添加，会使关键字一致性的检查测试失败）。\n\n#### 4.3 修改 AST 节点\n\n到目前为止，我们已经让 `goyacc` 生成的 parser 能够解析 remove partitioning 语法。但是，解析完成后并没有返回有效的数据结构（4.1 中我们返回了 `nil`），这意味着 parser 不能够根据语法树重新生成原 SQL 语句。\n\n所以，要修改现有的 AST 节点，使它们能够以某种形式保存 remove partitioning 信息。回顾目前规则层面的修改：\n\n```\nAlterTableStmt:\n\"ALTER\" IgnoreOptional \"TABLE\" TableName AlterTableSpecListOpt PartitionOpt\n```\n\n已改为：\n\n```\nAlterTableStmt:\n\"ALTER\" IgnoreOptional \"TABLE\" TableName AlterTableSpecListOpt AlterTablePartitionOpt\nAlterTablePartitionOpt:\n      PartitionOpt | \"REMOVE\" \"PARTITIONING\"\n```\n\n其中几个非终结符对应的数据结构如下：\n\n```\nAlterTableSpecListOpt -> []AlterTableSpec\nPartitionOpt -> PartitionOptions\n```\n\n关于修改节点，有几种方案可以选择：\n\n1. 增加一个新的节点 struct，表示 `AlterTablePartitionOpt`。其中包含 `PartitionOptions` 和一个 bool 值，表示是否为 remove partitioning。\n\n2. 将 remove partitioning 看作 `PartitionOptions`，在内部添加一个 bool 成员 `isRemovePartitioning` 以做区分。\n\n3. 将 `PartitionOpt` 和 remove partitioning 都看作 `AlterTableSpec`，为 `AlterTableSpec` 的添加一个类型，单独表示 remove partitioning。\n\n经过比较和分析，我们发现第一个方案会引入新的数据结构，有较大的概率会引起现有的 TiDB 测试不过，为此可能要修改 TiDB 方面的代码，工作量大的同时提高了 parser 的复杂度，因此作为备选方案；第二个方案没有引入新的数据结构，但是修改了现有的数据结构（加了个 bool 成员）；第三个方案即没有添加也没有修改数据结构，并且能够以较少的代码完成任务，作为首选方案。\n\n**在以上的选择中，我们遵循「尽量不修改 AST 节点结构」的原则。**\n\n按照方案三，观察 [`AlterTableSpec`](https://github.com/pingcap/parser/pull/396/files#diff-688d51c34d61e5d538b15582305c9a8dL1768)，其定义如下：\n\n```\ntype AlterTableSpec struct {\n  node\n  ...\n  Tp              AlterTableType\n  Name            string\n  ...\n}\n```\n\n其中一个成员是 `Tp`，它所属的类型包含了 `AlterTable` 的许多操作，例如 `AddColumn`，`AddConstraint`，`DropColumn` 等。我们的任务是添加一个 [类似的 Type](https://github.com/pingcap/parser/pull/396/files#diff-688d51c34d61e5d538b15582305c9a8dR1708)，让它能够表示 remove partitioning。\n\n到这里，解析完 SQL 语句生成的 AST 树已经包含 remove partitioning 的信息了。接下来要处理 Restore，让它能够从 AST 树还原出 SQL 语句。`AlterTableSpec` 的 Restore 十分简单，加一个 case 即可，这里不再赘述。\n\n#### 4.4 完善 `parser.y`\n\n第一次修改 `parser.y` 的时候我们在新加规则的语义动作中返回了 `nil`，原因是尚未确定 AST 是否需要修改，以及如何修改。而到这一步，这些都已经确定下来了，把 remove partitioning 看作 `AlterTableSpec` 类型：\n\n```\n|       \"REMOVE\" \"PARTITIONING\"\n      {\n              $$ = &ast.AlterTableSpec{\n                     Tp: ast.AlterTableRemovePartitioning,\n              }\n             yylex.AppendError(yylex.Errorf(\"The REMOVE PARTITIONING clause is parsed but ignored by all storage engines.\"))\n             parser.lastErrorAsWarn()\n      }\n```\n\n注意，这里必须抛出警告，表示虽然目前 parser 能够解析该语法，但实际上 TiDB 并未支持相应的功能。\n\n#### 5. 补充 test case\n\n这里，所有的代码修改引入的分支结构都能够被现有的测试覆盖，因此在提升覆盖率上没有需求。当然，如果想要测试更多类似的 case，可以将它们添加到前面提到的 `TestDDL` 函数中。\n\n#### 6. 执行所有测试（`make test`）\n\n```\ngofmt (simplify)\nsh test.sh\nok      github.com/pingcap/parser       4.294s  coverage: 62.1% of statements in ./...\nok      github.com/pingcap/parser/ast   2.090s  coverage: 42.3% of statements in ./...\nok      github.com/pingcap/parser/auth  1.155s  coverage: 1.3% of statements in ./... [no tests to run]\nok      github.com/pingcap/parser/charset       1.094s  coverage: 2.0% of statements in ./...\nok      github.com/pingcap/parser/format        1.114s  coverage: 2.5% of statements in ./...\n?       github.com/pingcap/parser/goyacc        [no test files]\nok      github.com/pingcap/parser/model 1.100s  coverage: 3.5% of statements in ./...\nok      github.com/pingcap/parser/mysql 1.102s  coverage: 1.3% of statements in ./... [no tests to run]\nok      github.com/pingcap/parser/opcode        1.099s  coverage: 1.4% of statements in ./...\nok      github.com/pingcap/parser/terror        1.091s  coverage: 2.3% of statements in ./...\nok      github.com/pingcap/parser/types 1.106s  coverage: 7.0% of statements in ./...\n\n```\n\n**确保所有的 test 都是 ok 的状态。**\n\n#### 7. 提交 PR\n\n按照 PR 模板逐项填写。\n\n```\n### What problem does this PR solve?\n\n#### Fix compatibility problem about keyword REMOVE PARTITIONING\n\nIssue: pingcap/parser#402\n\n#### MySQL Syntax:\n\nalter_specification:\n...\n  | REMOVE PARTITIONING\n\n### Bad SQL Case:\n\nalter table t remove partitioning;\nalter table t lock = default, remove partitioning;\nalter table t drop check c, remove partitioning;\n\n### Check List\n\nTests\n- Unit test\n\n```\n\n**需要特别指出的是，我们鼓励各位 Contributor 多使用 `make test`。当不知道从何处入手或者失去目标时，`make test` 输出的错误信息或许能够引导大家进行思考和探索**。\n\n>Tips: [完整的 PR 示例](https://github.com/pingcap/parser/pull/396)\n\n## FAQ\n\n以下是在增加 remove partitioning 语法支持时遇到的问题和解决方法。\n\n**Q1. 为什么不在 `PartitionOpt` 中直接添加规则？** \n\n**A1**：`PartitionOpt` 用于匹配含有 `partition by` 的 SQL 语句，除了 `Alter Table` 语句以外，它还被 `Create Table` 使用，而 `remove partitioning` 只存在于 `alter table` 语句中，因此不能在 `PartitionOpt` 中添加规则。\n\n**Q2. 执行 make test 时报错：** \n\n```\nparser.y:1100:1: undefined symbol \"PARTITIONING\"\nparser.y:1100:1: undefined symbol \"REMOVE\"\nmake[1]: *** [Makefile:19: parser] Error 1\n```\n\n**A2**：在 yacc 中，出现在规则中的字符串，要么是 `token`（终结符），要么是非终结符。这里引用一段 yacc 的规范：\n\n```\nNames refer to either tokens or nonterminal symbols.\nNames representing tokens must be declared; this is most simply done by writing\n%token   name1 name2 . . .\n```\n\n所以，修复方法是在 `parser.y` 的 `%token` 字段上添加 `PARTITIONING` 和 `REMOVE` 的声明。\n\n**Q3. 执行 make test 时报错：** \n\n```\n c.Assert(len(tokenMap)-len(aliases), Equals, keywordCount-len(windowFuncTokenMap))\n... obtained int = 454\n... expected int = 456\n```\n\n**A3**：这是关键字的一致性检查出了问题，解决方案是补充 `tokenMap`（它是关键字到 `token ID` 的映射，被 scanner 用来判断某个字符串是否为关键字）。\n\n**Q4. 执行 make test 时报错：** \n\n```\nFAIL: parser_test.go:1666: testParserSuite.TestDDL\nparser_test.go:2166:\n    s.RunTest(c, table)\nparser_test.go:351:\n    c.Assert(restoreSQLs, Equals, expectSQLs, comment)\n... obtained string = \"ALTER TABLE `t`\"\n... expected string = \"ALTER TABLE `t` REMOVE PARTITIONING\"\n... restore ALTER TABLE `t`; expect ALTER TABLE `t` REMOVE PARTITIONING\n```\n\n**A4**：这个错误说明 parser 已经解析通过，但不能从语法树中恢复原 SQL 语句的 remove partitioning 部分。此时应检查相应 AST 节点的 Restore 方法是否正确处理了 `REMOVE PARTITIONING`。\n\n***最后欢迎大家加入 [TiDB Contributor Club](https://pingcap.com/community-cn/)，无门槛参与开源项目，改变世界从这里开始吧！***  \n> 点击查看更多 [成为 Contributor 系列文章](https://pingcap.com/zh/blog/?tag=Contributor)","date":"2019-08-08","author":"谢腾进 赵一霖","fillInMethod":"writeDirectly","customUrl":"30mins-become-contributor-of-tidb-20190808","file":null,"relatedBlogs":[]}}},
    "staticQueryHashes": ["1327623483","1820662718","3081853212","3430003955","3649515864","4265596160","63159454"]}