首页 > 数据库 > > 以TiDB为例介绍分布式数据库如何新增函数支持
转自
ngaut/builddatabase
本文档用于描述如何为 TiDB 新增 builtin 函数。首先介绍一些必需的背景知识,然后介绍增加 builtin 函数的流程,最后会以一个函数作为示例。
SQL 语句在 TiDB 中是如何执行的?
SQL 语句首先会经过 parser,从文本 parse 成为 AST(抽象语法树),通过 optimizer 生成执行计划,得到一个可以执行的 plan,通过执行这个 plan 即可得到结果,这期间会涉及到如何获取 table 中的数据,如何对数据进行过滤、计算、排序、聚合、滤重等操作。对于一个 builtin 函数,比较重要的是 parse 和如何求值。这里着重说这两部分。
TiDB 语法解析的代码在 parser 目录下,主要涉及 misc.go 和 parser.y 两个文件。在 TiDB 项目中运行 make parser 会通过 goyacc 将 parser.y 其转换为 parser.go 代码文件。转换后的 go 代码,可以被其他的 go 代码调用,执行 parse 操作。
将 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 中包含函数参数、函数求值的方法,用于后续的求值。
求值过程是根据输入的参数,以及运行时环境,求出函数或者表达式的值。求值的控制逻辑 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 完成函数求值。
tokenMap 中添加函数名到 token code 的映射,其中 token code 是生成的变量,在 parser/parser.y 中通过 %token 声明,由 goyacc 自动生成;functionClass),实现 getFunction 方法builtinFunc 接口,通过实现 eval 方法来完成函数逻辑builtin.funcs 中,这里函数名用到了第4步定义的常量handleFuncCallExpr 里面添加这个函数的返回结果类型make dev,确保所有的 test case 都能跑过这里以新增 UTC_TIMESTAMP 支持的 PR 为例,进行详细说明
tokenMap 中添加一个 entryutcTimestamp 指定,其值是 goyacc 自动生成的。SQL 对大小不敏感,tokenMap 里面统一用大写。tokenMap 这张表里面的文本,不要被当作 identifier,而是作为一个特别的token。接下来在 parser 规则中,需要对这个 token 进行特殊处理。utcTimestamp 的 token,在 parser 调用 lexer 的 Lex 方法时,可能会返回这个 token code,供 parser 识别。此外,我们还给他起了个别名叫 "UTC_TIMESTAMP"。有 yacc/bison 使用经验的同学可能会好奇为何 utcTimestamp 后还跟了字符串,这是 goyacc 支持的语法,名为 literal string,可以在后续规则中替代 utcTimestamp 使用,具体说明可以参见该文档。utcTimestamp 就是 tokenMap 里面的那个 utcTimestamp,当 parser.y 生成 parser.go 的时,将会生成一个名为 被赋予一个名为 utcTimestamp 的 int 常量,即 token code。ReservedKeyword 规则下。最后,添加该函数解析规则,由于其不是关键字,因此在 FunctionCallNonKeyword 下添加如下规则: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>)。FnName 记录了函数名 "utc_timestamp",我们可以通过这个函数名在后面的 funcs 找到函数具体的实现类。ast.ExprNode 类型。make parser 生成 parser.go 了,快进入 parser 目录执行 go test 测试你的成果吧!utcTimestampFunctionClass 实现了 functionClass 接口,builtinUTCTimestampSig 实现了 builtinFunc 接口,其求值过程在上文背景知识中已经介绍过了,在此不再赘述。funcs 表里,代码如下:ast.UTCTimestamp 这个函数名找到函数的具体实现,其实现是一个 functionClass,也就是我们刚才实现的。构造这个 functionClass 时,我们传入了一函数基类,其参数含义是:这个函数的函数名是 ast.UTCTimestamp,它接受0到1个参数。我们也可能需要实现一个能接受任意多个参数的函数,此时可将 baseFunctionClass 最后一个字段设为-1。select utc_timestamp 看看 :)make dev 通过所以测试,就可以向 TiDB 提 PR 了!来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/31537575/viewspace-3147233/ ,如需转载,请注明出处,否则将追究法律责任。