TiDB 增加 MySQL 內建函數

本文檔用於描述如何為 TiDB 新增 builtin 函數。首先介紹一些必需的背景知識,然後介紹增加builtin 函數的流程,最後會以一個函數作為示例。(作者:申礫)

背景知識

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 操作。

將 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 中包含函數參數、函數求值的方法,用於後續的求值。

求值:

求值過程是根據輸入的參數,以及運行時環境,求出函數或者表達式的值。求值的控制邏輯evaluator/evaluator.go 中。對於大部分 builtin 函數,在 parse 過程中被解析為FuncCallExpr,求值時首先將 ast.FuncCallExpr 轉換成 expression.ScalarFunction,這時會調用 NewFunction() 方法(expression/scalar_function.go),通過 FnName 在 builtin.Funcs 表(evaluator/builtin.go)中找到對應的函數實現,最後在對 ScalarFunction 求值時會調用求值函數。

整體流程

修改 parser/misc.go 以及 parser/parser.y

  • 在 misc.go 的 tokenMap 中添加規則,將函數名解析為 token

  • 在 parser.y 中增加規則,將 token 序列轉換成 ast 的 node

  • 在 parser_test.go 中,增加 parser 的單元測試

  • make

在 evaluator 包中的求值函數

  • 在 evaluator/builtin_xx.go

  • 中實現該函數的功能,注意這裡的函數是按照類別分了幾個文件,比如時間相關的函數在。函數的介面為 type BuiltinFunc func([]types.Datum, context.Context) (types.Datum, error)並將其 name 和實現註冊到 builtin.Funcs 中

寫單元測試

  • 在 evaluator 目錄下,為函數的實現增加單元測試

示例

這裡以新增 timdiff() 支持的 PR 為例,見 https://github.com/pingcap/ti...

首先看 parser/misc.go:在 tokenMap 中增加了一個 entry

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

這裡是定義了一個規則,當發現文本是 timediff 時,轉換成一個 token,token 的名稱為 timediff。SQL 對大小不敏感,tokenMap 裡面統一用大寫。對於 tokenMap 這張表裡面的文本,不要被當作 identifier,而是作為一個特別的 token。

再看parser/parser.y:

%token <ident>ntimediff "TIMEDIFF"n

這行的意思是從 lexer 中拿到 timediff 這個 token 後,我們給他起個名字叫 「」TIMEDIFF」,下面的規則匹配時,我們都使用這個名字。

這裡 timediff 必須跟 tokenMap 裡面 value 的 timediff 對應上,當 parser.y 生成 parser.go 的時候 timedif f會被賦予一個 int 類型的 token 編號。

由於 timediff 不是 MySQL 的關鍵字,我們把規則放在 FunctionCallNonKeyword 下,

| "TIMEDIFF" ( Expression , Expression )n {n $$ = &ast.FuncCallExpr{n FnName: model.NewCIStr($1),n Args: []ast.ExprNode{$3.(ast.ExprNode), $5.(ast.ExprNode)},n }n }n

這裡的意思是,當 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 類型。

函數註冊在builtin.go中的Funcs表中:

ast.TimeDiff: {builtinTimeDiff, 2, 2},n

builtinTimediff:該 builtin 函數的實現在 builtinTimediff 這個函數中

2:最少的參數個數

2:最多的參數個數,語法parse過程中,會檢查參數的個數是否合法

函數實現在 builtin_time.go 中,一些細節可以看下面的代碼以及注釋

func builtinTimeDiff(args []types.Datum, ctx context.Context) (d types.Datum, err error) {n sc := ctx.GetSessionVars().StmtCtxn t1, err := convertToGoTime(sc, args[0])n if err != nil {n return d, errors.Trace(err)n }n t2, err := convertToGoTime(sc, args[1])n if err != nil {n return d, errors.Trace(err)n }n var t types.Durationn t.Duration = t1.Sub(t2)n t.Fsp = types.MaxFspn d.SetMysqlDuration(t)n return d, niln}n

最後需要增加單元測試:

func (s *testEvaluatorSuite) TestTimeDiff(c *C) {n // Test cases from https://dev.mysql.com/doc/refman/5.7/en/date-and-time-functions.html#function_timediffn tests := []struct {n t1 stringn t2 stringn expectStr stringn }{n {"2000:01:01 00:00:00", "2000:01:01 00:00:00.000001", "-00:00:00.000001"},n {"2008-12-31 23:59:59.000001", "2008-12-30 01:01:01.000002", "46:58:57.999999"},n }n for _, test := range tests {n t1 := types.NewStringDatum(test.t1)n t2 := types.NewStringDatum(test.t2)n result, err := builtinTimeDiff([]types.Datum{t1, t2}, s.ctx)n c.Assert(err, IsNil)n c.Assert(result.GetMysqlDuration().String(), Equals, test.expectStr)n }n}n

推薦閱讀:

TiKV 源碼解析系列——Placement Driver
跨數據中心一致性是如何實現的?
對資料庫和分散式很感興趣,學習路線是什麼?

TAG:分布式数据库 |