前言

其实,咱们始终都很想,基于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兼容成绩吧。