头图

在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.html
type 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。

image-20211229181323248

image-20211229181343322

在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即可。

image-20211229181414078

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

image-20211229181426763

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

image-20211229181440886

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

image-20211229182141522

简单总结

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

image-20211229181524940

阅读 466

我们注重理性思维,我们关注解决问题。

4 声望
0 粉丝
0 条评论

我们注重理性思维,我们关注解决问题。

4 声望
0 粉丝
文章目录
宣传栏