最近咱們對 TiDB 代碼作了些改進,大幅度簡化了添加內建函數的流程,這篇教程將爲你們分享如何爲 TiDB 新增 builtin 函數。首先介紹一些必需的背景知識,而後介紹增長 builtin 函數的流程,最後會以一個函數做爲示例。javascript
SQL 語句發送到 TiDB 後首先會通過 parser,從文本 parse 成爲 AST(抽象語法樹),經過 Query Optimizer 生成執行計劃,獲得一個能夠執行的 plan,經過執行這個 plan 便可獲得結果,這期間會涉及到如何獲取 table 中的數據,如何對數據進行過濾、計算、排序、聚合、濾重以及如何對錶達式進行求值。
對於一個 builtin 函數,比較重要的是進行語法解析以及如何求值。其中語法解析部分須要瞭解如何寫 yacc 以及如何修改 TiDB 的詞法解析器,較爲繁瑣,咱們已經將這部分工做提早作好,大多數 builtin 函數的語法解析工做已經作完。
對 builtin 函數的求值須要在 TiDB 的表達式求值框架下完成,每一個 builtin 函數被認爲是一個表達式,用一個 ScalarFunction 來表示,每一個 builtin 函數經過其函數名以及參數,獲取對應的函數類型以及函數簽名,而後經過函數簽名進行求值。
整體而言,上述流程對於不熟悉 TiDB 的朋友而言比較複雜,咱們對這部分作了些工做,將一些流程性、較爲繁瑣的工做作了統一處理,目前已經將大多數未實現的 buitlin 函數的語法解析以及尋找函數簽名的工做完成,可是函數實現部分留空。換句話說,只要找到留空的函數實現,將其補充完整,便可做爲一個 PR。java
找到未實現的函數
在 TiDB 源碼中的 expression 目錄下搜索 errFunctionNotExists
,便可找到全部未實現的函數,從中選擇一個感興趣的函數,好比 SHA2 函數:mysql
func (b *builtinSHA2Sig) eval(row []types.Datum) (d types.Datum, err error) {
return d, errFunctionNotExists.GenByArgs("SHA2")
}複製代碼
實現函數簽名
接下來要作的事情就是實現 eval 方法,函數的功能請參考 MySQL 文檔,具體的實現方法能夠參考目前已經實現函數。git
在 typeinferer 中添加類型推導信息
在 plan/typeinferer.go 中的 handleFuncCallExpr() 裏面添加這個函數的返回結果類型,請保持和 MySQL 的結果一致。所有類型定義參見 MySQL Const。github
* 注意大多數函數除了須要填寫返回值類型以外,還須要獲取返回值的長度。複製代碼
寫單元測試
在 expression 目錄下,爲函數的實現增長單元測試,同時也要在 plan/typeinferer_test.go 文件中添加 typeinferer 的單元測試sql
運行 make dev,確保全部的 test case 都能跑過express
這裏以新增 SHA1() 函數的 PR 爲例,進行詳細說明
首先看 expression/builtin_encryption.go
:
將 SHA1() 的求值方法補充完整微信
func (b *builtinSHA1Sig) eval(row []types.Datum) (d types.Datum, err error) {
// 首先對參數進行求值,這塊通常不用修改
args, err := b.evalArgs(row)
if err != nil {
return types.Datum{}, errors.Trace(err)
}
// 每一個參數的意義請參考 MySQL 文檔
// SHA/SHA1 function only accept 1 parameter
arg := args[0]
if arg.IsNull() {
return d, nil
}
// 這裏對參數值作了一個類型轉換,函數的實現請參考 util/types/datum.go
bin, err := arg.ToBytes()
if err != nil {
return d, errors.Trace(err)
}
hasher := sha1.New()
hasher.Write(bin)
data := fmt.Sprintf("%x", hasher.Sum(nil))
// 設置返回值
d.SetString(data)
return d, nil
}複製代碼
接下來給函數實現添加單元測試,參見 expression/builtin_encryption_test.go
:框架
var shaCases = []struct {
origin interface{}
crypt string
}{
{"test", "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3"},
{"c4pt0r", "034923dcabf099fc4c8917c0ab91ffcd4c2578a6"},
{"pingcap", "73bf9ef43a44f42e2ea2894d62f0917af149a006"},
{"foobar", "8843d7f92416211de9ebb963ff4ce28125932878"},
{1024, "128351137a9c47206c4507dcf2e6fbeeca3a9079"},
{123.45, "22f8b438ad7e89300b51d88684f3f0b9fa1d7a32"},
}
func (s *testEvaluatorSuite) TestShaEncrypt(c *C) {
defer testleak.AfterTest(c)() // 監測 goroutine 泄漏的工具,能夠直接照搬
fc := funcs[ast.SHA]
for _, test := range shaCases {
in := types.NewDatum(test.origin)
f, _ := fc.getFunction(datumsToConstants([]types.Datum{in}), s.ctx)
crypt, err := f.eval(nil)
c.Assert(err, IsNil)
res, err := crypt.ToString()
c.Assert(err, IsNil)
c.Assert(res, Equals, test.crypt)
}
// test NULL input for sha
var argNull types.Datum
f, _ := fc.getFunction(datumsToConstants([]types.Datum{argNull}), s.ctx)
crypt, err := f.eval(nil)
c.Assert(err, IsNil)
c.Assert(crypt.IsNull(), IsTrue)
}
* 注意,除了正常 case 以外,最好能添加一些異常的case,如輸入值爲 nil,或者是多種類型的參數複製代碼
最後還須要添加類型推導信息以及 test case,參見 plan/typeinferer.go
,plan/typeinferer_test.go
:函數
case ast.SHA, ast.SHA1:
tp = types.NewFieldType(mysql.TypeVarString)
chs = v.defaultCharset
tp.Flen = 40複製代碼
{`sha1(123)`, mysql.TypeVarString, "utf8"},
{`sha(123)`, mysql.TypeVarString, "utf8"},複製代碼
編輯按:添加 TiDB Robot 微信,加入 TiDB Contributor Club,無門檻參與開源項目,改變世界從這裏開始吧(萌萌噠)。