十分鐘成為 TiDB Contributor 系列 | 添加內建函數

最近我們對 TiDB 代碼做了些改進,大幅度簡化了添加內建函數的流程,這篇教程為大家分享如何為 TiDB 新增 builtin 函數。首先介紹一些必需的背景知識,然後介紹增加 builtin 函數的流程,最後會以一個函數作為示例。(作者:申礫)

背景知識

SQL 語句發送到 TiDB 後首先會經過 parser,從文本 parse 成為 AST(抽象語法樹),通過 Query Optimizer 生成執行計劃,得到一個可以執行的 plan,通過執行這個 plan 即可得到結果,這期間會涉及到如何獲取 table 中的數據,如何對數據進行過濾、計算、排序、聚合、濾重以及如何對表達式進行求值。

對於一個 builtin 函數,比較重要的是進行語法解析以及如何求值。其中語法解析部分需要了解如何寫 yacc 以及如何修改 TiDB 的詞法解析器,較為繁瑣,我們已經將這部分工作提前做好,大多數 builtin 函數的語法解析工作已經做完。

對 builtin 函數的求值需要在 TiDB 的表達式求值框架下完成,每個 builtin 函數被認為是一個表達式,用一個 ScalarFunction 來表示,每個 builtin 函數通過其函數名以及參數,獲取對應的函數類型以及函數簽名,然後通過函數簽名進行求值。

總體而言,上述流程對於不熟悉 TiDB 的朋友而言比較複雜,我們對這部分做了些工作,將一些流程性、較為繁瑣的工作做了統一處理,目前已經將大多數未實現的 buitlin 函數的語法解析以及尋找函數簽名的工作完成,但是函數實現部分留空。換句話說,只要找到留空的函數實現,將其補充完整,即可作為一個 PR。

添加 builtin 函數整體流程

  • 找到未實現的函數

    在 TiDB 源碼中的 expression 目錄下搜索 errFunctionNotExists,即可找到所有未實現的函數,從中選擇一個感興趣的函數,比如 SHA2 函數:

func (b *builtinSHA2Sig) eval(row []types.Datum) (d types.Datum, err error) {n return d, errFunctionNotExists.GenByArgs("SHA2")n}n

  • 實現函數簽名接下來要做的事情就是實現 eval 方法,函數的功能請參考 MySQL 文檔,具體的實現方法可以參考目前已經實現函數。

  • 在 typeinferer 中添加類型推導信息

    在 plan/typeinferer.go 中的 handleFuncCallExpr() 裡面添加這個函數的返回結果類型,請保持和 MySQL 的結果一致。全部類型定義參見 MySQL Const。

* 注意大多數函數除了需要填寫返回值類型之外,還需要獲取返回值的長度。n

  • 寫單元測試在 expression 目錄下,為函數的實現增加單元測試,同時也要在 plan/typeinferer_test.go 文件中添加 typeinferer 的單元測試

  • 運行 make dev,確保所有的 test case 都能跑過

示例

這裡以新增 SHA1() 函數的 PR 為例,進行詳細說明

首先看 expression/builtin_encryption.go:

將 SHA1() 的求值方法補充完整

func (b *builtinSHA1Sig) eval(row []types.Datum) (d types.Datum, err error) {n // 首先對參數進行求值,這塊一般不用修改n args, err := b.evalArgs(row)n if err != nil {n return types.Datum{}, errors.Trace(err)n }n // 每個參數的意義請參考 MySQL 文檔n // SHA/SHA1 function only accept 1 parametern arg := args[0]n if arg.IsNull() {n return d, niln }n // 這裡對參數值做了一個類型轉換,函數的實現請參考 util/types/datum.gon bin, err := arg.ToBytes()n if err != nil {n return d, errors.Trace(err)n }n hasher := sha1.New()n hasher.Write(bin)n data := fmt.Sprintf("%x", hasher.Sum(nil))n // 設置返回值n d.SetString(data)n return d, niln}n

接下來給函數實現添加單元測試,參見 expression/builtin_encryption_test.go:

var shaCases = []struct {n origin interface{}n crypt stringn }{n {"test", "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"},n {"c4pt0r", "034923dcabf099fc4c8917c0ab91ffcd4c2578a6"},n {"pingcap", "73bf9ef43a44f42e2ea2894d62f0917af149a006"},n {"foobar", "8843d7f92416211de9ebb963ff4ce28125932878"},n {1024, "128351137a9c47206c4507dcf2e6fbeeca3a9079"},n {123.45, "22f8b438ad7e89300b51d88684f3f0b9fa1d7a32"},n }n n func (s *testEvaluatorSuite) TestShaEncrypt(c *C) {n defer testleak.AfterTest(c)() // 監測 goroutine 泄漏的工具,可以直接照搬n fc := funcs[ast.SHA]n for _, test := range shaCases {n in := types.NewDatum(test.origin)n f, _ := fc.getFunction(datumsToConstants([]types.Datum{in}), s.ctx)n crypt, err := f.eval(nil)n c.Assert(err, IsNil)n res, err := crypt.ToString()n c.Assert(err, IsNil)n c.Assert(res, Equals, test.crypt)n }n // test NULL input for shan var argNull types.Datumn f, _ := fc.getFunction(datumsToConstants([]types.Datum{argNull}), s.ctx)n crypt, err := f.eval(nil)n c.Assert(err, IsNil)n c.Assert(crypt.IsNull(), IsTrue)n}n* 注意,除了正常 case 之外,最好能添加一些異常的case,如輸入值為 nil,或者是多種類型的參數n

最後還需要添加類型推導信息以及 test case,參見 plan/typeinferer.go,plan/typeinferer_test.go:

case ast.SHA, ast.SHA1:n tp = types.NewFieldType(mysql.TypeVarString)n chs = v.defaultCharsetn tp.Flen = 40n

{`sha1(123)`, mysql.TypeVarString, "utf8"},n {`sha(123)`, mysql.TypeVarString, "utf8"},n

編輯按:添加 TiDB Robot 微信(ID:tidbai),加入 TiDB Contributor Club,無門檻參與開源項目,改變世界從這裡開始吧(萌萌噠)。

聽說現在成為 TiDB Contributor 就送限量版馬克杯呦~


推薦閱讀:

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

TAG:分布式数据库 | TiDB |