写点什么

在 TiDB 中实现一个关键字——Parser 篇

  • 2022 年 7 月 11 日
  • 本文字数:3269 字

    阅读完需:约 11 分钟

作者: CuteRay 原文来源:https://tidb.net/blog/ea67c1b4


【是否原创】是


【首发渠道】知乎


【首发渠道链接】 在 TiDB 中实现一个关键字——Parser 篇 - 知乎 (zhihu.com)


【正文】

在 TiDB 中实现一个关键字——Parser 篇

前言

其实,我们一直都很想,基于 TiDB 做一些很 cool,很 hacker 的事情。比如我们团队小伙伴发了一篇关于 TiDB for Pg 兼容 Gitlab 的一篇文章,具体文章可以参考链接:


TiDB4PG 之兼容 Gitlab - 知乎 (zhihu.com)


这篇文章我就来简单聊聊实现兼容到 Gitlab 的艰苦过程。


我们采用了一个相对较笨的方式,将 Gitlab 的源码通过编译启动的方式,连接到最开始的 TiDB for PG,这样肯定是报错,不行的,毕竟很多东西没有兼容。为了在短期内看得见效果,采取抓包的方式,将 Gitlab 连接到 TiDB For Pg 的所执行的 SQL 语句找出来,进行粗略的分类整理,去看看有哪些 SQL 语句,去定制化的兼容开发。在兼容实现这些 SQL 语句,其中难点之一,就有 DML 语句中的 Returning 关键字。

原理

在 TiDB-Server 里面,执行一个 SQL 语句的流程,大致可以分为解析、编译、优化、执行、返回结果这几个阶段。而实现一个关键字,同样的需要在这几个阶段做一些文章。而对于 Returning 关键字而言,我们可以从 DML 语句中相对简单的 DELETE 语句入手,所以接下来的改造过程,最终结果就是实现了 DELETE RETURNING 句式。

改造实现过程

Parser

从 SQL 在 TiDB 中的流转过程,迈入后续代码的第一步,就是将客户端传来的 SQL 语句解析为一个能够被后续代码认识的结构,也就是 AST 树。而这一过程,主要就是在 Parser 这个模块儿中实现。


在 TiDB v5.0 以前,Parser 这个包是有一个专门的代码仓库的,通过 go mod 的方式导入到 TiDB,而在 5.0 之后,TiDB 将 Parser 包挪到 TiDB 源码当中。TiDB for PG 的源码也是基于 TiDB v4.0.14 改造的,这次我想尝试一下,在 TiDB 最新的源码中实现 RETURNING 关键字,一个是为了 hackathon 的比赛作准备,另一个也是为了之后 TiDB for PG 向着 TiDB 新版本靠拢试试水。


Paser 模块主要靠 Lexer & Yacc 这两个模块共同构成。在解析的过程中,Lexer 组件会将 SQL 文本转换为一个又一个 token 传给 parser,而 parser 中最为重要的 parser.go 文件,则是 goyacc 工具根据 parser.y 文件生成的,根据文件中语法的定义,来决定 lexer 中传过来的 token 能够与什么语法规则进行匹配,最终输出 AST 结构树,也就是 parser/ast 中定义的各类 stmt,而我们要实现的就是 dml.go 中的 DeleteStmt。


// DeleteStmt is a statement to delete rows from table.// See https://dev.mysql.com/doc/refman/5.7/en/delete.htmltype DeleteStmt struct {  dmlNode
// TableRefs is used in both single table and multiple table delete statement. TableRefs *TableRefsClause // Tables is only used in multiple table delete statement. Tables *DeleteTableList Where ExprNode Order *OrderByClause Limit *Limit Priority mysql.PriorityEnum IgnoreErr bool Quick bool IsMultiTable bool BeforeFrom bool // TableHints represents the table level Optimizer Hint for join type. TableHints []*TableOptimizerHint With *WithClause // 我们今天的主题,Returning 关键字 Returning *ReturningClause}
type ReturningClause struct { node Fields *FieldList}
func (n *ReturningClause) Restore(ctx *format.RestoreCtx) error { ctx.WriteKeyWord("Returning ") for i, item := range n.Fields.Fields { if i != 0 { ctx.WritePlain(",") } if err := item.Restore(ctx); err != nil { return errors.Annotatef(err, "An error occurred while restore ReturningClause.Fields[%d]", i) } } return nil}
func (n *ReturningClause) Accept(v Visitor) (Node, bool) { newNode, skipChildren := v.Enter(n) if skipChildren { return v.Leave(newNode) } n = newNode.(*ReturningClause)
if n.Fields != nil { node, ok := n.Fields.Accept(v) if !ok { return n, false } n.Fields = node.(*FieldList) }
return v.Leave(n)}
复制代码


原谅篇幅有限,不能把所有代码贴出来。这里值得提一嘴的就是 Accept() 方法。在 ast 包中,几乎所有的 stmt 结构都实现了 ast.Node 接口,这个接口中的 Accept() 方法,主要作用就是处理 AST,通过 Visitor 模式遍历所有的节点,并且对 AST 结构做一个转换。而为了能正常将 RETURNING 关键字转换成 DeleteStmt,我们还需要在 parser 中去将 RETURNING 关键字注册为 token。




在 parser.y 中 definitions 区域定义好 RETURNING 相关句式的 token,比如 RETURNING 关键字,还有 ReturningClause、ReturningOption 句式。


关于 parser 的一些基础知识可以参考文章:


TiDB Parser 模块的简单解读与改造方法 - 知乎 (zhihu.com)


TiDB 源码阅读系列文章(五)TiDB SQL Parser 的实现 | PingCAP


在做完这些之后,我们就能够在 parser.y 的 rule 部分中,找到 DELETE 句式,加入 returning 句式了,也就是 ReturningOptional,接着在其中写上简单的逻辑。


/******************************************************************* * *  Delete Statement * *******************************************************************/DeleteWithoutUsingStmt:  "DELETE" TableOptimizerHintsOpt PriorityOpt QuickOptional IgnoreOptional "FROM" TableName PartitionNameListOpt TableAsNameOpt IndexHintListOpt WhereClauseOptional OrderByOptional LimitClause ReturningOptional  {    ... 此处省略 ...    if $14 != nil {      x.Returning = $14.(*ast.ReturningClause)    }
$$ = x }| "DELETE" TableOptimizerHintsOpt PriorityOpt QuickOptional IgnoreOptional TableAliasRefList "FROM" TableRefs WhereClauseOptional ReturningOptional { ... 此处省略 ... if $10 != nil { x.Returning = $10.(*ast.ReturningClause) } $$ = x }
DeleteWithUsingStmt: "DELETE" TableOptimizerHintsOpt PriorityOpt QuickOptional IgnoreOptional "FROM" TableAliasRefList "USING" TableRefs WhereClauseOptional ReturningOptional { ... 此处省略 ... if $11 != nil { x.Returning = $11.(*ast.ReturningClause) } $$ = x }
ReturningClause: "RETURNING" SelectStmtFieldList { $$ = &ast.ReturningClause{Fields: $2.(*ast.FieldList)} }
ReturningOptional: { $$ = nil }| ReturningClause { $$ = $1 }
复制代码


接着就能利用 parser/bin/goyacc 工具,根据最新的 paser.y 生成最终的 parser.go,进入 parser 包中,运行 make all 即可。



需要注意的是,对于关键字,在生成最新的 parser.go 之后,我们还需要在 parser/misc.go 中定义,这是由于 lexer 采用了字典树技术进行 token 识别,而其实现代码就是在其中,不然 lexer 会不认识这所谓的关键字。



改完之后的验证其实很简单,在 parser 包中找到 parser_test.go 的测试文件,写一个 delete returning 的句式,运行一遍测试,过了,那就是 OK 了。



还可以启动 tidb 源码,用 mysql 客户端连上去,执行一个 delete returning 的句式,能够成功返回,那么说明,这个关键字同样是兼容成功的。


简单总结

到了这一步,初步关键字兼容已经实现了,注意,现在还只是初步兼容,而要使其生效,则需要进入到接下来的 Plan 制定以及执行器 Exexutor 执行的部分了。这一部分在 TiDB v5.0 的改造还在研究的过程中,毕竟相对于 TiDB v4.0.14 的计划制定、优化有些许变动,还没来得及去研究,我会在后续文章中详细阐述。最后给大家看看 TiDB for PG 的 returning 兼容成果吧。



发布于: 刚刚阅读数: 3
用户头像

TiDB 社区官网:https://tidb.net/ 2021.12.15 加入

TiDB 社区干货传送门是由 TiDB 社区中布道师组委会自发组织的 TiDB 社区优质内容对外宣布的栏目,旨在加深 TiDBer 之间的交流和学习。一起构建有爱、互助、共创共建的 TiDB 社区 https://tidb.net/

评论

发布
暂无评论
在TiDB中实现一个关键字——Parser篇_TiDB 底层架构_TiDB 社区干货传送门_InfoQ写作社区