前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >十分钟成为 Contributor 系列 | 支持 AST 还原为 SQL

十分钟成为 Contributor 系列 | 支持 AST 还原为 SQL

原创
作者头像
PingCAP
修改2018-12-21 10:48:29
1.4K0
修改2018-12-21 10:48:29
举报
文章被收录于专栏:PingCAP的专栏PingCAP的专栏

作者:赵一霖

背景知识

SQL 语句发送到 TiDB 后首先会经过 parser,从文本 parse 成为 AST(抽象语法树),AST 节点与 SQL 文本结构是一一对应的,我们通过遍历整个 AST 树就可以拼接出一个与 AST 语义相同的 SQL 文本。

对 parser 不熟悉的小伙伴们可以看 TiDB 源码阅读系列文章(五)TiDB SQL Parser 的实现

为了控制 SQL 文本的输出格式,并且为方便未来新功能的加入(例如在 SQL 文本中用 “*” 替代密码),我们引入了 RestoreFlags 并封装了 RestoreCtx 结构(相关源码):

代码语言:txt
复制
// `RestoreFlags` 中的互斥组:
// [RestoreStringSingleQuotes, RestoreStringDoubleQuotes]
// [RestoreKeyWordUppercase, RestoreKeyWordLowercase]
// [RestoreNameUppercase, RestoreNameLowercase]
// [RestoreNameDoubleQuotes, RestoreNameBackQuotes]
// 靠前的 flag 拥有更高的优先级。
const (
	RestoreStringSingleQuotes RestoreFlags = 1 << iota
	
	...
)

// RestoreCtx is `Restore` context to hold flags and writer.
type RestoreCtx struct {
	Flags RestoreFlags
	In    io.Writer
}

// WriteKeyWord 用于向 `ctx` 中写入关键字(例如:SELECT)。
// 它的大小写受 `RestoreKeyWordUppercase`,`RestoreKeyWordLowercase` 控制
func (ctx *RestoreCtx) WriteKeyWord(keyWord string) {
	...
}

// WriteString 用于向 `ctx` 中写入字符串。
// 它是否被引号包裹及转义规则受 `RestoreStringSingleQuotes`,`RestoreStringDoubleQuotes`,`RestoreStringEscapeBackslash` 控制。
func (ctx *RestoreCtx) WriteString(str string) {
	...
}

// WriteName 用于向 `ctx` 中写入名称(库名,表名,列名等)。
// 它是否被引号包裹及转义规则受 `RestoreNameUppercase`,`RestoreNameLowercase`,`RestoreNameDoubleQuotes`,`RestoreNameBackQuotes` 控制。
func (ctx *RestoreCtx) WriteName(name string) {
	...
}

// WriteName 用于向 `ctx` 中写入普通文本。
// 它将被直接写入不受 flag 影响。
func (ctx *RestoreCtx) WritePlain(plainText string) {
	...
}

// WriteName 用于向 `ctx` 中写入普通文本。
// 它将被直接写入不受 flag 影响。
func (ctx *RestoreCtx) WritePlainf(format string, a ...interface{}) {
	...
}

我们在 ast.Node 接口中添加了一个 Restore(ctx *RestoreCtx) error 函数,这个函数将当前节点对应的 SQL 文本追加至参数 ctx 中,如果节点无效则返回 error

代码语言:txt
复制
type Node interface {
    // Restore AST to SQL text and append them to `ctx`.
    // return error when the AST is invalid.
	Restore(ctx *RestoreCtx) error
    
    ...
}

以 SQL 语句 SELECT column0 FROM table0 UNION SELECT column1 FROM table1 WHERE a = 1 为例,如下图所示,我们通过遍历整个 AST 树,递归调用每个节点的 Restore() 方法,即可拼接成一个完整的 SQL 文本。

ast-tree
ast-tree

值得注意的是,SQL 文本与 AST 是一个多对一的关系,我们不可能从 AST 结构中还原出与原 SQL 完全一致的文本,

因此我们只要保证还原出的 SQL 文本与原 SQL 语义相同 即可。所谓语义相同,指的是由 AST 还原出的 SQL 文本再被解析为 AST 后,两个 AST 是相等的。

我们已经完成了接口设计和测试框架,具体的Restore() 函数留空。因此只需要选择一个留空的 Restore() 函数实现,并添加相应的测试数据,就可以提交一个 PR 了!

实现 Restore() 函数的整体流程

  1. 请先阅读 ProposalIssue
  2. Issue 中找到未实现的函数
代码语言:txt
复制
1. 在 [Issue-pingcap/tidb#8532](https://github.com/pingcap/tidb/issues/8532) 中找到一个没有被其他贡献者认领的任务,例如 `ast/expressions.go: BetweenExpr`。
代码语言:txt
复制
2. 在 [pingcap/parser](https://github.com/pingcap/parser) 中找到任务对应文件 `ast/expressions.go`。
代码语言:txt
复制
3. 在文件中找到 `BetweenExpr` 结构的 `Restore` 函数:
代码语言:txt
复制
```
代码语言:txt
复制
// Restore implements Node interface.
代码语言:txt
复制
func (n *BetweenExpr) Restore(ctx *RestoreCtx) error {
代码语言:txt
复制
    return errors.New("Not implemented")
代码语言:txt
复制
}
代码语言:txt
复制
```
  1. 实现 Restore() 函数
代码语言:txt
复制
根据 Node 节点结构和 SQL 语法实现函数功能。
代码语言:txt
复制
 > 参考 [MySQL 5.7 SQL Statement Syntax](https://dev.mysql.com/doc/refman/5.7/en/sql-syntax.html)
  1. 写单元测试
代码语言:txt
复制
参考示例在相关文件下添加单元测试。
  1. 运行 make test,确保所有的 test case 都能跑过。
  2. 提交 PR
代码语言:txt
复制
 PR 标题统一为:`parser: implement Restore for XXX`  
代码语言:txt
复制
 请在 PR 中关联 Issue: `pingcap/tidb#8532`

示例

这里以实现 BetweenExpr 的 Restore 函数 PR 为例,进行详细说明:

  1. 首先看 ast/expressions.go
代码语言:txt
复制
1. 我们要实现一个 `ast.Node` 结构的 `Restore` 函数,首先清楚该结构代表什么短语,例如 `BetweenExpr` 代表 `expr [NOT] BETWEEN expr AND expr` (参见:[MySQL 语法 - 比较函数和运算符](https://dev.mysql.com/doc/refman/5.7/en/comparison-operators.html#operator_between))。
代码语言:txt
复制
2. 观察 `BetweenExpr` 结构:
代码语言:txt
复制
```
代码语言:txt
复制
// BetweenExpr is for "between and" or "not between and" expression.
代码语言:txt
复制
type BetweenExpr struct {
代码语言:txt
复制
    exprNode
代码语言:txt
复制
    // 被检查的表达式
代码语言:txt
复制
    Expr ExprNode
代码语言:txt
复制
    // AND 左侧的表达式
代码语言:txt
复制
    Left ExprNode
代码语言:txt
复制
    // AND 右侧的表达式
代码语言:txt
复制
    Right ExprNode
代码语言:txt
复制
    // 是否有 NOT 关键字
代码语言:txt
复制
    Not bool
代码语言:txt
复制
}
代码语言:txt
复制
```
代码语言:txt
复制
3. 实现 `BetweenExpr` 的 `Restore` 函数:
代码语言:txt
复制
```
代码语言:txt
复制
// Restore implements Node interface.
代码语言:txt
复制
func (n *BetweenExpr) Restore(ctx *RestoreCtx) error {
代码语言:txt
复制
    // 调用 Expr 的 Restore,向 ctx 写入 Expr
代码语言:txt
复制
    if err := n.Expr.Restore(ctx); err != nil {
代码语言:txt
复制
        return errors.Annotate(err, "An error occurred while restore BetweenExpr.Expr")
代码语言:txt
复制
    }
代码语言:txt
复制
    // 判断是否有 NOT,并写入相应关键字
代码语言:txt
复制
    if n.Not {
代码语言:txt
复制
        ctx.WriteKeyWord(" NOT BETWEEN ")
代码语言:txt
复制
    } else {
代码语言:txt
复制
        ctx.WriteKeyWord(" BETWEEN ")
代码语言:txt
复制
    }
代码语言:txt
复制
    // 调用 Left 的 Restore
代码语言:txt
复制
    if err := n.Left.Restore(ctx); err != nil {
代码语言:txt
复制
        return errors.Annotate(err, "An error occurred while restore BetweenExpr.Left")
代码语言:txt
复制
    }
代码语言:txt
复制
    // 写入 AND 关键字
代码语言:txt
复制
    ctx.WriteKeyWord(" AND ")
代码语言:txt
复制
    // 调用 Right 的 Restore
代码语言:txt
复制
    if err := n.Right.Restore(ctx); err != nil {
代码语言:txt
复制
        return errors.Annotate(err, "An error occurred while restore BetweenExpr.Right ")
代码语言:txt
复制
    }
代码语言:txt
复制
    return nil
代码语言:txt
复制
}
代码语言:txt
复制
```
  1. 接下来给函数实现添加单元测试, ast/expressions_test.go
代码语言:txt
复制
```
代码语言:txt
复制
// 添加测试函数
代码语言:txt
复制
func (tc *testExpressionsSuite) TestBetweenExprRestore(c *C) {
代码语言:txt
复制
    // 测试用例
代码语言:txt
复制
    testCases := []NodeRestoreTestCase{
代码语言:txt
复制
        {"b between 1 and 2", "`b` BETWEEN 1 AND 2"},
代码语言:txt
复制
        {"b not between 1 and 2", "`b` NOT BETWEEN 1 AND 2"},
代码语言:txt
复制
        {"b between a and b", "`b` BETWEEN `a` AND `b`"},
代码语言:txt
复制
        {"b between '' and 'b'", "`b` BETWEEN '' AND 'b'"},
代码语言:txt
复制
        {"b between '2018-11-01' and '2018-11-02'", "`b` BETWEEN '2018-11-01' AND '2018-11-02'"},
代码语言:txt
复制
    }
代码语言:txt
复制
    // 为了不依赖父节点实现,通过 extractNodeFunc 抽取待测节点
代码语言:txt
复制
    extractNodeFunc := func(node Node) Node {
代码语言:txt
复制
        return node.(*SelectStmt).Fields.Fields[0].Expr
代码语言:txt
复制
    }
代码语言:txt
复制
    // Run Test
代码语言:txt
复制
    RunNodeRestoreTest(c, testCases, "select %s", extractNodeFunc)
代码语言:txt
复制
}
代码语言:txt
复制
```
代码语言:txt
复制
**至此 `BetweenExpr` 的 `Restore` 函数实现完成,可以提交 PR 了。为了更好的理解测试逻辑,下面我们看 `RunNodeRestoreTest`:**
代码语言:txt
复制
```
代码语言:txt
复制
// 下面是测试逻辑,已经实现好了,不需要 contributor 实现
代码语言:txt
复制
func RunNodeRestoreTest(c *C, nodeTestCases []NodeRestoreTestCase, template string, extractNodeFunc func(node Node) Node) {
代码语言:txt
复制
    parser := parser.New()
代码语言:txt
复制
    for _, testCase := range nodeTestCases {
代码语言:txt
复制
        // 通过 template 将测试用例拼接为完整的 SQL
代码语言:txt
复制
        sourceSQL := fmt.Sprintf(template, testCase.sourceSQL)
代码语言:txt
复制
        expectSQL := fmt.Sprintf(template, testCase.expectSQL)
代码语言:txt
复制
        stmt, err := parser.ParseOneStmt(sourceSQL, "", "")
代码语言:txt
复制
        comment := Commentf("source %#v", testCase)
代码语言:txt
复制
        c.Assert(err, IsNil, comment)
代码语言:txt
复制
        var sb strings.Builder
代码语言:txt
复制
        // 抽取指定节点并调用其 Restore 函数
代码语言:txt
复制
        err = extractNodeFunc(stmt).Restore(NewRestoreCtx(DefaultRestoreFlags, &sb))
代码语言:txt
复制
        c.Assert(err, IsNil, comment)
代码语言:txt
复制
        // 通过 template 将 restore 结果拼接为完整的 SQL
代码语言:txt
复制
        restoreSql := fmt.Sprintf(template, sb.String())
代码语言:txt
复制
        comment = Commentf("source %#v; restore %v", testCase, restoreSql)
代码语言:txt
复制
        // 测试 restore 结果与预期一致
代码语言:txt
复制
        c.Assert(restoreSql, Equals, expectSQL, comment)
代码语言:txt
复制
        stmt2, err := parser.ParseOneStmt(restoreSql, "", "")
代码语言:txt
复制
        c.Assert(err, IsNil, comment)
代码语言:txt
复制
        CleanNodeText(stmt)
代码语言:txt
复制
        CleanNodeText(stmt2)
代码语言:txt
复制
        // 测试解析的 stmt 与原 stmt 一致
代码语言:txt
复制
        c.Assert(stmt2, DeepEquals, stmt, comment)
代码语言:txt
复制
    }
代码语言:txt
复制
}
代码语言:txt
复制
```

**不过对于 ast.StmtNode(例如:ast.SelectStmt)测试方法有些不一样,

由于这类节点可以还原为一个完整的 SQL,因此直接在 parser_test.go 中测试。**

下面以实现 UseStmt 的 Restore 函数 PR 为例,对测试进行说明:

  1. Restore 函数实现过程略。
  2. 给函数实现添加单元测试,参见 parser_test.go
代码语言:txt
复制
在这个示例中,只添加了几行测试数据就完成了测试:
代码语言:txt
复制
```
代码语言:txt
复制
// 添加 testCase 结构的测试数据
代码语言:txt
复制
{"use `select`", true, "USE `select`"},
代码语言:txt
复制
{"use `sel``ect`", true, "USE `sel``ect`"},
代码语言:txt
复制
{"use select", false, "USE `select`"},
代码语言:txt
复制
```
代码语言:txt
复制
我们看 `testCase` 结构声明:
代码语言:txt
复制
```
代码语言:txt
复制
type testCase struct {
代码语言:txt
复制
    // 原 SQL
代码语言:txt
复制
    src     string
代码语言:txt
复制
    // 是否能被正确 parse
代码语言:txt
复制
    ok      bool
代码语言:txt
复制
    // 预期的 restore SQL
代码语言:txt
复制
    restore string
代码语言:txt
复制
}
代码语言:txt
复制
```
代码语言:txt
复制
测试代码会判断原 SQL parse 出 AST 后再还原的 SQL 是否与预期的 restore SQL 相等,具体的测试逻辑在 `parser_test.go` 中 `RunTest()`、`RunRestoreTest()` 函数,逻辑与前例类似,此处不再赘述。

加入 TiDB Contributor Club,无门槛参与开源项目,改变世界从这里开始吧(萌萌哒)。

tidb-community
tidb-community

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景知识
  • 实现 Restore() 函数的整体流程
  • 示例
相关产品与服务
云数据库 MySQL
腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档