Skip to content

Commit

Permalink
Merge pull request ngaut#24 from zyguan/master
Browse files Browse the repository at this point in the history
tidb: update builtin.md
  • Loading branch information
ngaut authored Feb 8, 2017
2 parents 1cefb18 ba19dbb commit de0d816
Showing 1 changed file with 139 additions and 118 deletions.
257 changes: 139 additions & 118 deletions tidb/builtin.md
Original file line number Diff line number Diff line change
@@ -1,138 1,159 @@
本文档用于描述如何为 TiDB 新增 builtin 函数。首先介绍一些必需的背景知识,然后介绍增加builtin 函数的流程,最后会以一个函数作为示例。
本文档用于描述如何为 TiDB 新增 builtin 函数。首先介绍一些必需的背景知识,然后介绍增加 builtin 函数的流程,最后会以一个函数作为示例。

### **背景知识**

SQL 语句在 TiDB 中是如何执行的。
SQL 语句在 TiDB 中是如何执行的?

SQL 语句首先会经过 parser,从文本 parse 成为 AST(抽象语法树),通过 optimizer 生成执行计划,得到一个可以执行的 plan,通过执行这个 plan 即可得到结果,这期间会涉及到如何获取 table 中的数据,如何对数据进行过滤、计算、排序、聚合、滤重等操作。对于一个 builtin 函数,比较重要的是 parse 和如何求值。这里着重说这两部分。

#### Parse

TiDB语法解析的代码在 parser 目录下,主要涉及 misc.go 和 parser.y 两个文件。在 TiDB 项目中运行 make parser 会通过 goyacc 将 parser.y 其转换为 parser.go 代码文件。转换后的 go 代码,可以被其他的 go 代码调用,执行 parse 操作。
TiDB 语法解析的代码在 parser 目录下,主要涉及 misc.go 和 parser.y 两个文件。在 TiDB 项目中运行 make parser 会通过 goyacc 将 parser.y 其转换为 parser.go 代码文件。转换后的 go 代码,可以被其他的 go 代码调用,执行 parse 操作。

将 sql 语句从文本 parse 成结构化的过程中,首先是通过 Scanner,将文本切分为 tokens,每个 tokens 会有 name 和 value,其中 name 在 parser 中用于匹配预定义的规则(parser.y),匹配规则时,不断的从 Scanner 中获取 token,当能完整匹配上一条规则时,会将匹配上的tokens替换为一个新的变量。同时,在每条规则匹配成功后,可以用 tokens 的 value,构造ast 中的节点或者是 subtree。对于 builtin 函数来说,一般的形式为 name(args),scanner 中要识别 function 的 name,括号,参数等元素,parser 中匹配预定义的规则,构造出一个 ast的 node,这个 node 中包含函数参数、函数求值的方法,用于后续的求值。
将 sql 语句从文本 parse 成结构化 AST 有如下过程。首先是通过 scanner 将文本切分为 tokens,每个 token 会有 name 和 value,其中 name 在 parser 中用于匹配预定义的规则(规则在 parser.y 中定义)。匹配规则时,parser 不断的从 scanner 中获取 token (通过调用 `Scanner.Lex` 方法),这一过程称为**移进**(shift);当 parser 发现能完整匹配上一条规则时,会将匹配上的 tokens 替换为一个新的变量,这一过程称为**归约**(reduce)。同时,在每条规则匹配成功后,可以用 tokens 的 value,构造ast 中的节点或者是 subtree。对于 builtin 函数来说,一般的形式为 name(args),scanner 中要识别 function 的 name、括号、参数等元素;对于匹配预定义规则输入,parser 会构造出一个 ast 的 node,这个 node 中包含函数参数、函数求值的方法,用于后续的求值。

#### 求值

求值过程是根据输入的参数,以及运行时环境,求出函数或者表达式的值。求值的控制逻辑evaluator/evaluator.go 中。对于大部分 builtin 函数,在 parse 过程中被解析为FuncCallExpr,求值时首先将 ast.FuncCallExpr 转换成 expression.ScalarFunction,这时会调用 NewFunction() 方法(expression/scalar_function.go),通过 FnName 在 builtin.Funcs 表(evaluator/builtin.go)中找到对应的函数实现,最后在对 ScalarFunction 求值时会调用求值函数
求值过程是根据输入的参数,以及运行时环境,求出函数或者表达式的值。求值的控制逻辑 expression 包中。对于大部分 builtin 函数,在 parse 过程中被解析为 `ast.FuncCallExpr`,求值时首先将 `FuncCallExpr` 转换成 `expression.ScalarFunction`,这时会调用 `NewFunction` 方法 (expression/scalar_function.go),通过 `FuncCallExpr.FnName``funcs` 表(expression/builtin.go)中找到对应的函数实现类(`functionClass`),并通过 `functionClass.getFunction` 获得函数实现(`builtinFunc`),最后在对 `ScalarFunction` 求值时会调用 `builtinFunc.eval` 完成函数求值

### **添加 builtin 函数整体流程**

1. 修改 parser/misc.go 以及 parser/parser.y
* 在 misc.go 的 tokenMap 中添加规则,将函数名解析为 token
* 在 parser.y 中增加规则,将 token 序列转换成 ast 的 node
* 在 parser_test.go 中,增加 parser 的单元测试
1. 修改 parser/misc.go ,在 `tokenMap` 中添加函数名到 token code 的映射,其中 token code 是生成的变量,在 parser/parser.y 中通过 %token 声明,由 goyacc 自动生成;
2. 修改 parser/parser.y :
* 用 %token 声明函数
* 根据函数名性质 (请查阅 [mysql 文档](https://dev.mysql.com/doc/refman/5.7/en/keywords.html)),将其添加到 UnReservedKeyword 或 ReservedKeyword 或 NotKeywordToken 规则中
* 在合适位置添加函数解析规则
3. 在 parser_test.go 中,添加对应函数的单元测试;
4. 修改 ast/functions.go ,定义相应函数名常量,供后续代码引用;
5. 在 expression 包中实现函数,注意函数实现按函数类别分了几个文件,比如时间相关的函数在 expression/builtin\_time.go,函数实现简要说明如下:
* 定义相应函数类(`functionClass`),实现 `getFunction` 方法
* 定义函数签名,其应实现 `builtinFunc` 接口,通过实现 `eval` 方法来完成函数逻辑
* 在 expression/builtin_xxx_test.go 中添加对应函数的单元测试
* 将函数名及其实现类注册到 `builtin.funcs` 中,这里函数名用到了第4步定义的常量
6. 在 typeinferer 中添加类型推导信息,请保持函数结果类型和 MySQL 的结果一致,全部类型定义参见 [MySQL Const](https://github.com/pingcap/tidb/blob/master/mysql/type.go#L17)
* 在 plan/typeinferer.go 中的 `handleFuncCallExpr` 里面添加这个函数的返回结果类型
* 在 plan/typeinferer_test.go 中添加相应测试
7. 运行 `make dev`,确保所有的 test case 都能跑过

### **示例**

2. 在 evaluator 包中的求值函数
* 在 evaluator/builtin_xx.go 中实现该函数的功能,注意这里的函数是按照类别分了几个文件,比如时间相关的函数在。函数的接口为 type BuiltinFunc func([]types.Datum, context.Context) (types.Datum, error)
* 并将其 name 和实现注册到 builtin.Funcs 中
这里以新增 [UTC\_TIMESTAMP](https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_utc-timestamp) 支持的 [PR](https://github.com/pingcap/tidb/pull/2592) 为例,进行详细说明

3. 在 typeinferer 中添加类型推导信息
* 在 plan/typeinferer.go 中的 handleFuncCallExpr() 里面添加这个函数的返回结果类型,请保持和 MySQL 的结果一致。全部类型定义参见 [MySQL Const](https://github.com/pingcap/tidb/blob/master/mysql/type.go#L17)
1. 首先看 [parser/misc.go](https://github.com/pingcap/tidb/pull/2592/files#diff-2680bef19a08b7dc1c3a74194be5d0f6)

4. 写单元测试
* 在 evaluator 目录下,为函数的实现增加单元测试
`tokenMap` 中添加一个 entry
```go
var tokenMap = map[string]int{
...
"UTC_TIMESTAMP": utcTimestamp,
...
}
```

5. 运行 make dev,确保所有的 test case 都能跑过
这里是定义了一个从文本 'UTC\_TIMESTAMP' 到 token code 的映射关系,token code 的由常量 `utcTimestamp` 指定,其值是 goyacc 自动生成的。SQL 对大小不敏感,`tokenMap` 里面统一用大写。

### **示例**
对于 `tokenMap` 这张表里面的文本,不要被当作 identifier,而是作为一个特别的token。接下来在 parser 规则中,需要对这个 token 进行特殊处理。

2. 看 [parser/parser.y](https://github.com/pingcap/tidb/pull/2592/files#diff-5dbf5cc474f4f1eed5fa9e9796760002):

```
%token <ident>
...
utcTimestamp "UTC_TIMESTAMP"
...
```

这行的意思是,声明一个叫 `utcTimestamp` 的 token,在 parser 调用 lexer 的 `Lex` 方法时,可能会返回这个 token code,供 parser 识别。此外,我们还给他起了个别名叫 "UTC\_TIMESTAMP"。有 yacc/bison 使用经验的同学可能会好奇为何 `utcTimestamp` 后还跟了字符串,这是 goyacc 支持的语法,名为 literal string,可以在后续规则中替代 `utcTimestamp` 使用,具体说明可以参见[该文档](https://godoc.org/github.com/cznic/y#hdr-LiteralString_field)。

这里的 `utcTimestamp` 就是 `tokenMap` 里面的那个 `utcTimestamp`,当 parser.y 生成 parser.go 的时,将会生成一个名为 被赋予一个名为 `utcTimestamp`int 常量,即 token code。

在查阅[文档](https://dev.mysql.com/doc/refman/5.7/en/keywords.html)后得知 UTC_TIMESTAMP 是 MySQL 的保留字,因此我们将其加到 `ReservedKeyword` 规则下。最后,添加该函数解析规则,由于其不是关键字,因此在 `FunctionCallNonKeyword` 下添加如下规则:
```
| "UTC_TIMESTAMP" FuncDatetimePrec
{
args := []ast.ExprNode{}
if $2 != nil {
args = append(args, $2.(ast.ExprNode))
}
$$ = &ast.FuncCallExpr{FnName: model.NewCIStr($1), Args: args}
}
```

这里的意思是,当 scanner 输出的 token 序列满足这种 pattern 时,我们将这些 tokens 规约为一个新的变量,叫 `FunctionCallNonKeyword` (通过给$$变量赋值,即可给 `FunctionCallNonKeyword` 赋值),也就是一个 AST 中的 node,类型为 *ast.FuncCallExpr。其成员变量 FnName 的值为 `model.NewCIStr($1)`,其中 `$1` 在生成代码中将被替换成第一个 token (也就是 `utcTimestamp`)的 value。值得一提的是,这里使用了 utcTimestamp 这个 token 的 literal string,倒不是说 token 的 value 就是 "UTC_TIMESTAMP" 这个字符串,其真实的字由 lexer 决定,保存在 `ident string` 这个 union 字段下(因为我们之前声明 `utcTimestamp` 的‘类型’为 `<ident>`)。

至此我们的parser已经能成功的将文本 "utc\_timestamp()" 转换成为一个 AST node,其成员 `FnName` 记录了函数名 "utc\_timestamp",我们可以通过这个函数名在后面的 `funcs` 找到函数具体的实现类。

最后在补充一下yacc规则的基础知识:如果想要在规则处理代码中引用这个规则中某个 token 的值,可以用 $x 这种方式,其中 x 为 token 在规则中的位置,如上面的规则中,$1 为 utcTimestamp,$2FuncDatetimePrec 。$2.(ast.ExprNode) 的意思是引用第2个位置上的 token 的值,并断言其值为 `ast.ExprNode` 类型。

3. 在 [parser/parser_test.go](https://github.com/pingcap/tidb/pull/2592/files#diff-27c45ca411f005e1b8796b12fb53e26c) 添加测试代码

以上步骤完成后,如果没有文法冲突,你应该可以通过 `make parser` 生成 parser.go 了,快进入 parser 目录执行 `go test` 测试你的成果吧!

4. 参考 [ast/functions.go](https://github.com/pingcap/tidb/pull/2592/files#diff-ade136ede78b393a9e9538c6b7008e02) 为函数名定义一个常量

5. 在 expression 完成对函数逻辑的代码实现:

在 [expression/builtin_time.go](https://github.com/pingcap/tidb/pull/2592/files#diff-d61eef12d314ca7514bc1960312ba5e4) 中实现函数相关类型,实现代码大致如下:

```go
type utcTimestampFunctionClass struct {
baseFunctionClass
}
func (c *utcTimestampFunctionClass) getFunction(args []Expression, ctx context.Context) (builtinFunc, error) {
return &builtinUTCTimestampSig{newBaseBuiltinFunc(args, ctx)}, errors.Trace(c.verifyArgs(args))
}
type builtinUTCTimestampSig struct {
baseBuiltinFunc
}
// See https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_utc-timestamp
func (b *builtinUTCTimestampSig) eval(row []types.Datum) (d types.Datum, err error) {
args, err := b.evalArgs(row)
if err != nil {
return types.Datum{}, errors.Trace(err)
}
fsp := 0
sc := b.ctx.GetSessionVars().StmtCtx
if len(args) == 1 && !args[0].IsNull() {
if fsp, err = checkFsp(sc, args[0]); err != nil {
return d, errors.Trace(err)
}
}
t, err := convertTimeToMysqlTime(time.Now().UTC(), fsp)
if err != nil {
return d, errors.Trace(err)
}
d.SetMysqlTime(t)
return d, nil
}
```

其中,`utcTimestampFunctionClass` 实现了 `functionClass` 接口,`builtinUTCTimestampSig` 实现了 `builtinFunc` 接口,其求值过程在上文背景知识中已经介绍过了,在此不再赘述。

实现了函数后,需要将其注册到 [expression/builtin.go](https://github.com/pingcap/tidb/pull/2592/files#diff-cdbd511e3e5f2bbe0a3c5c173d3938c2) 中的 `funcs` 表里,代码如下:
```go
ast.UTCTimestamp: &utcTimestampFunctionClass{baseFunctionClass{ast.UnixTimestamp, 0, 1}},
```

意思是,我们可以通过 `ast.UTCTimestamp` 这个函数名找到函数的具体实现,其实现是一个 `functionClass`,也就是我们刚才实现的。构造这个 `functionClass` 时,我们传入了一函数基类,其参数含义是:这个函数的函数名是 `ast.UTCTimestamp`,它接受01个参数。我们也可能需要实现一个能接受任意多个参数的函数,此时可将 `baseFunctionClass` 最后一个字段设为-1

测试很重要,赶快在 [expression/builtin_time_test.go](https://github.com/pingcap/tidb/pull/2592/files#diff-9efa9861dfe0962aeeda87c63deb0f0f) 添加测试吧!

6. 在 [plan/typeinferer.go](https://github.com/pingcap/tidb/pull/2592/files#diff-686e374c1af6ac094dcaed1db4f0fd44) 实现对函数结果的类型推导,代码如下:

```go
case "now", "sysdate", "current_timestamp", "utc_timestamp":
tp = types.NewFieldType(mysql.TypeDatetime)
tp.Decimal = v.getFsp(x)
```

意思是这个函数将返回一个 'DATETIME' 类型的结果。如果不确定函数返回类型,一个小窍门/笨办法是:通过 mysql workbench 连接 mysql 数据库,执行 `select utc_timestamp` 看看 :)

目前,函数类型推导和函数实现还是分开的,略有不便,关于这点 pingcap 的小伙伴正在积极改善中,敬请期待!不过当务之急,还是先在添加 [plan/typeinferer_test.go](https://github.com/pingcap/tidb/pull/2592/files#diff-6425b785337ec89d2604eb16f63caac6) 添加测试吧!

这里[新增 timdiff() 支持的 PR](https://github.com/pingcap/tidb/pull/2249) 为例,进行详细说明

1. 首先看 `parser/misc.go`

在 `tokenMap` 中添加一个 entry

```
var tokenMap = map[string]int{
"TIMEDIFF": timediff,
}
```

这里是定义了一个规则,当发现文本是 timediff 时,转换成一个token,token的名称为 timediff。SQL对大小不敏感,tokenMap里面统一用大写。

对于 `tokenMap` 这张表里面的文本,不要被当作identifier,而是作为一个特别的token。接下来在 parser 规则中,需要对这个 token 进行特殊处理,看 `parser/parser.y`:

```
%token <ident>
timediff "TIMEDIFF"
```

这行的意思是从 lexer 中拿到 timediff 这个 token 后,我们给他起个名字叫 "TIMEDIFF”,下面的规则匹配时,我们都使用这个名字。

这里 timediff 必须跟 `tokenMap` 里面 value 的 timediff 对应上,当parser.y 生成 parser.go 的时候 timediff 会被赋予一个 int 类型的 token 编号。

由于 timediff 不是 MySQL 的关键字,我们把规则放在 `FunctionCallNonKeyword` 下,
```
| "TIMEDIFF" '(' Expression ',' Expression ')'
{
$$ = &ast.FuncCallExpr{
FnName: model.NewCIStr($1),
Args: []ast.ExprNode{$3.(ast.ExprNode), $5.(ast.ExprNode)},
}
}
```
这里的意思是,当 scanner 输出的 token 序列满足这种 pattern 时,我们将这些 tokens 规约为一个新的变量,叫 `FunctionCallNonKeyword` (通过给$$变量赋值,即可给 `FunctionCallNonKeyword` 赋值),也就是一个 AST 中的 node,类型为 *ast.FuncCallExpr。其成员变量 FnName 的值为 $1 的内容,也就是规则中第一个 token 的 value。

至此我们已经成功的将文本 "timediff()” 转换成为一个 AST node,其成员 FnName 记录了函数名 ”timediff”,用于后面的求值。

如果想引用这个规则中某个 token 的值,可以用 $x 这种方式,其中 x 为 token 在规则中的位置,如上面的规则中,$1为 "TIMEDIFF”,$2为 ’(’ , $3 为 ’)’ 。$1.(string) 的意思是引用第一个位置上的 token 的值,并断言其值为 string 类型。

2. 函数注册在 `builtin.go`中的 `Funcs` 表中:
```
ast.TimeDiff: {builtinTimeDiff, 2, 2},
```

参数说明如下:

`builtinTimediff`:timediff 函数的具体实现在 `builtinTimediff` 这个函数中
2:这个函数最少的参数个数
2:这个函数最多的参数个数,语法parse过程中,会检查参数的个数是否合法

函数实现在 `builtin_time.go` 中,一些细节可以看下面的代码以及注释
```
func builtinTimeDiff(args []types.Datum, ctx context.Context) (d types.Datum, err error) {
sc := ctx.GetSessionVars().StmtCtx
t1, err := convertToGoTime(sc, args[0])
if err != nil {
return d, errors.Trace(err)
}
t2, err := convertToGoTime(sc, args[1])
if err != nil {
return d, errors.Trace(err)
}
var t types.Duration
t.Duration = t1.Sub(t2)
t.Fsp = types.MaxFsp
d.SetMysqlDuration(t)
return d, nil
}
```

3. 添加类型推导信息:
```
case "curtime", "current_time", "timediff":
tp = types.NewFieldType(mysql.TypeDuration)
tp.Decimal = v.getFsp(x)
```

4. 给函数实现添加单元测试:
```
func (s *testEvaluatorSuite) TestTimeDiff(c *C) {
// Test cases from https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_timediff
tests := []struct {
t1 string
t2 string
expectStr string
}{
{"2000:01:01 00:00:00", "2000:01:01 00:00:00.000001", "-00:00:00.000001"},
{"2008-12-31 23:59:59.000001", "2008-12-30 01:01:01.000002", "46:58:57.999999"},
}
for _, test := range tests {
t1 := types.NewStringDatum(test.t1)
t2 := types.NewStringDatum(test.t2)
result, err := builtinTimeDiff([]types.Datum{t1, t2}, s.ctx)
c.Assert(err, IsNil)
c.Assert(result.GetMysqlDuration().String(), Equals, test.expectStr)
}
}
```
7. 至此,一个 builtin 函数已经大功告成,运行 `make dev` 通过所以测试,就可以向 [TiDB](https://github.com/pingcap/tidb) 提 PR 了!

0 comments on commit de0d816

Please sign in to comment.