关于tidb:在TiDB中实现一个关键字Parser篇

81次阅读

共计 4243 个字符,预计需要花费 11 分钟才能阅读完成。

前言

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

在 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 兼容成绩吧。

正文完
 0