摘要:用于向中寫入名稱庫名,表名,列名等。它是否被引號包裹及轉義規則受,,,控制。它將被直接寫入不受影響。所謂語義相同,指的是由還原出的文本再被解析為后,兩個是相等的。參考寫單元測試參考示例在相關文件下添加單元測試。
作者:趙一霖背景知識
SQL 語句發送到 TiDB 后首先會經過 parser,從文本 parse 成為 AST(抽象語法樹),AST 節點與 SQL 文本結構是一一對應的,我們通過遍歷整個 AST 樹就可以拼接出一個與 AST 語義相同的 SQL 文本。
對 parser 不熟悉的小伙伴們可以看 TiDB 源碼閱讀系列文章(五)TiDB SQL Parser 的實現。
為了控制 SQL 文本的輸出格式,并且為方便未來新功能的加入(例如在 SQL 文本中用 “*” 替代密碼),我們引入了 RestoreFlags 并封裝了 RestoreCtx 結構(相關源碼):
// `RestoreFlags` 中的互斥組: // [RestoreStringSingleQuotes, RestoreStringDoubleQuotes] // [RestoreKeyWordUppercase, RestoreKeyWordLowercase] // [RestoreNameUppercase, RestoreNameLowercase] // [RestoreNameDoubleQuotes, RestoreNameBackQuotes] // 靠前的 flag 擁有更高的優先級。 const ( RestoreStringSingleQuotes RestoreFlags = 1 << iota ... ) // RestoreCtx is `Restore` context to hold flags and writer. type RestoreCtx struct { Flags RestoreFlags In io.Writer } // WriteKeyWord 用于向 `ctx` 中寫入關鍵字(例如:SELECT)。 // 它的大小寫受 `RestoreKeyWordUppercase`,`RestoreKeyWordLowercase` 控制 func (ctx *RestoreCtx) WriteKeyWord(keyWord string) { ... } // WriteString 用于向 `ctx` 中寫入字符串。 // 它是否被引號包裹及轉義規則受 `RestoreStringSingleQuotes`,`RestoreStringDoubleQuotes`,`RestoreStringEscapeBackslash` 控制。 func (ctx *RestoreCtx) WriteString(str string) { ... } // WriteName 用于向 `ctx` 中寫入名稱(庫名,表名,列名等)。 // 它是否被引號包裹及轉義規則受 `RestoreNameUppercase`,`RestoreNameLowercase`,`RestoreNameDoubleQuotes`,`RestoreNameBackQuotes` 控制。 func (ctx *RestoreCtx) WriteName(name string) { ... } // WriteName 用于向 `ctx` 中寫入普通文本。 // 它將被直接寫入不受 flag 影響。 func (ctx *RestoreCtx) WritePlain(plainText string) { ... } // WriteName 用于向 `ctx` 中寫入普通文本。 // 它將被直接寫入不受 flag 影響。 func (ctx *RestoreCtx) WritePlainf(format string, a ...interface{}) { ... }
我們在 ast.Node 接口中添加了一個 Restore(ctx *RestoreCtx) error 函數,這個函數將當前節點對應的 SQL 文本追加至參數 ctx 中,如果節點無效則返回 error。
type Node interface { // Restore AST to SQL text and append them to `ctx`. // return error when the AST is invalid. Restore(ctx *RestoreCtx) error ... }
以 SQL 語句 SELECT column0 FROM table0 UNION SELECT column1 FROM table1 WHERE a = 1 為例,如下圖所示,我們通過遍歷整個 AST 樹,遞歸調用每個節點的 Restore() 方法,即可拼接成一個完整的 SQL 文本。
值得注意的是,SQL 文本與 AST 是一個多對一的關系,我們不可能從 AST 結構中還原出與原 SQL 完全一致的文本,
因此我們只要保證還原出的 SQL 文本與原 SQL 語義相同 即可。所謂語義相同,指的是由 AST 還原出的 SQL 文本再被解析為 AST 后,兩個 AST 是相等的。
我們已經完成了接口設計和測試框架,具體的Restore() 函數留空。因此只需要選擇一個留空的 Restore() 函數實現,并添加相應的測試數據,就可以提交一個 PR 了!
實現 Restore() 函數的整體流程請先閱讀 Proposal、Issue
在 Issue 中找到未實現的函數
在 Issue-pingcap/tidb#8532 中找到一個沒有被其他貢獻者認領的任務,例如 ast/expressions.go: BetweenExpr。
在 pingcap/parser 中找到任務對應文件 ast/expressions.go。
在文件中找到 BetweenExpr 結構的 Restore 函數:
// Restore implements Node interface. func (n *BetweenExpr) Restore(ctx *RestoreCtx) error { return errors.New("Not implemented") }
實現 Restore() 函數
根據 Node 節點結構和 SQL 語法實現函數功能。
參考 MySQL 5.7 SQL Statement Syntax
寫單元測試
參考示例在相關文件下添加單元測試。
運行 make test,確保所有的 test case 都能跑過。
提交 PR
PR 標題統一為:parser: implement Restore for XXX
請在 PR 中關聯 Issue: pingcap/tidb#8532
這里以實現 BetweenExpr 的 Restore 函數 PR 為例,進行詳細說明:
首先看 ast/expressions.go:
我們要實現一個 ast.Node 結構的 Restore 函數,首先清楚該結構代表什么短語,例如 BetweenExpr 代表 expr [NOT] BETWEEN expr AND expr (參見:MySQL 語法 - 比較函數和運算符)。
觀察 BetweenExpr 結構:
// BetweenExpr is for "between and" or "not between and" expression. type BetweenExpr struct { exprNode // 被檢查的表達式 Expr ExprNode // AND 左側的表達式 Left ExprNode // AND 右側的表達式 Right ExprNode // 是否有 NOT 關鍵字 Not bool }
3. 實現 `BetweenExpr` 的 `Restore` 函數: ``` // Restore implements Node interface. func (n *BetweenExpr) Restore(ctx *RestoreCtx) error { // 調用 Expr 的 Restore,向 ctx 寫入 Expr if err := n.Expr.Restore(ctx); err != nil { return errors.Annotate(err, "An error occurred while restore BetweenExpr.Expr") } // 判斷是否有 NOT,并寫入相應關鍵字 if n.Not { ctx.WriteKeyWord(" NOT BETWEEN ") } else { ctx.WriteKeyWord(" BETWEEN ") } // 調用 Left 的 Restore if err := n.Left.Restore(ctx); err != nil { return errors.Annotate(err, "An error occurred while restore BetweenExpr.Left") } // 寫入 AND 關鍵字 ctx.WriteKeyWord(" AND ") // 調用 Right 的 Restore if err := n.Right.Restore(ctx); err != nil { return errors.Annotate(err, "An error occurred while restore BetweenExpr.Right ") } return nil } ```
接下來給函數實現添加單元測試, ast/expressions_test.go:
// 添加測試函數 func (tc *testExpressionsSuite) TestBetweenExprRestore(c *C) { // 測試用例 testCases := []NodeRestoreTestCase{ {"b between 1 and 2", "`b` BETWEEN 1 AND 2"}, {"b not between 1 and 2", "`b` NOT BETWEEN 1 AND 2"}, {"b between a and b", "`b` BETWEEN `a` AND `b`"}, {"b between "" and "b"", "`b` BETWEEN "" AND "b""}, {"b between "2018-11-01" and "2018-11-02"", "`b` BETWEEN "2018-11-01" AND "2018-11-02""}, } // 為了不依賴父節點實現,通過 extractNodeFunc 抽取待測節點 extractNodeFunc := func(node Node) Node { return node.(*SelectStmt).Fields.Fields[0].Expr } // Run Test RunNodeRestoreTest(c, testCases, "select %s", extractNodeFunc) }
至此 BetweenExpr 的 Restore 函數實現完成,可以提交 PR 了。為了更好的理解測試邏輯,下面我們看 RunNodeRestoreTest:
// 下面是測試邏輯,已經實現好了,不需要 contributor 實現 func RunNodeRestoreTest(c *C, nodeTestCases []NodeRestoreTestCase, template string, extractNodeFunc func(node Node) Node) { parser := parser.New() for _, testCase := range nodeTestCases { // 通過 template 將測試用例拼接為完整的 SQL sourceSQL := fmt.Sprintf(template, testCase.sourceSQL) expectSQL := fmt.Sprintf(template, testCase.expectSQL) stmt, err := parser.ParseOneStmt(sourceSQL, "", "") comment := Commentf("source %#v", testCase) c.Assert(err, IsNil, comment) var sb strings.Builder // 抽取指定節點并調用其 Restore 函數 err = extractNodeFunc(stmt).Restore(NewRestoreCtx(DefaultRestoreFlags, &sb)) c.Assert(err, IsNil, comment) // 通過 template 將 restore 結果拼接為完整的 SQL restoreSql := fmt.Sprintf(template, sb.String()) comment = Commentf("source %#v; restore %v", testCase, restoreSql) // 測試 restore 結果與預期一致 c.Assert(restoreSql, Equals, expectSQL, comment) stmt2, err := parser.ParseOneStmt(restoreSql, "", "") c.Assert(err, IsNil, comment) CleanNodeText(stmt) CleanNodeText(stmt2) // 測試解析的 stmt 與原 stmt 一致 c.Assert(stmt2, DeepEquals, stmt, comment) } }
**不過對于 ast.StmtNode(例如:ast.SelectStmt)測試方法有些不一樣,
由于這類節點可以還原為一個完整的 SQL,因此直接在 parser_test.go 中測試。**
下面以實現 UseStmt 的 Restore 函數 PR 為例,對測試進行說明:
Restore 函數實現過程略。
給函數實現添加單元測試,參見 parser_test.go:
在這個示例中,只添加了幾行測試數據就完成了測試:
// 添加 testCase 結構的測試數據 {"use `select`", true, "USE `select`"}, {"use `sel``ect`", true, "USE `sel``ect`"}, {"use select", false, "USE `select`"},
我們看 testCase 結構聲明:
type testCase struct { // 原 SQL src string // 是否能被正確 parse ok bool // 預期的 restore SQL restore string }
測試代碼會判斷原 SQL parse 出 AST 后再還原的 SQL 是否與預期的 restore SQL 相等,具體的測試邏輯在 parser_test.go 中 RunTest()、RunRestoreTest() 函數,邏輯與前例類似,此處不再贅述。
加入 TiDB Contributor Club,無門檻參與開源項目,改變世界從這里開始吧(萌萌噠)。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/17858.html
閱讀 1447·2021-11-16 11:44
閱讀 3286·2021-09-29 09:43
閱讀 620·2019-08-30 10:52
閱讀 938·2019-08-29 11:01
閱讀 3259·2019-08-26 11:47
閱讀 2886·2019-08-23 12:18
閱讀 1359·2019-08-22 17:04
閱讀 2047·2019-08-21 17:04