成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

十分鐘成為 Contributor 系列 | 支持 AST 還原為 SQL

dingda / 3627人閱讀

摘要:用于向中寫入名稱庫名,表名,列名等。它是否被引號包裹及轉(zhuǎn)義規(guī)則受,,,控制。它將被直接寫入不受影響。所謂語義相同,指的是由還原出的文本再被解析為后,兩個是相等的。參考寫單元測試參考示例在相關(guān)文件下添加單元測試。

作者:趙一霖
背景知識

SQL 語句發(fā)送到 TiDB 后首先會經(jīng)過 parser,從文本 parse 成為 AST(抽象語法樹),AST 節(jié)點與 SQL 文本結(jié)構(gòu)是一一對應(yīng)的,我們通過遍歷整個 AST 樹就可以拼接出一個與 AST 語義相同的 SQL 文本。

對 parser 不熟悉的小伙伴們可以看 TiDB 源碼閱讀系列文章(五)TiDB SQL Parser 的實現(xiàn)。

為了控制 SQL 文本的輸出格式,并且為方便未來新功能的加入(例如在 SQL 文本中用 “*” 替代密碼),我們引入了 RestoreFlags 并封裝了 RestoreCtx 結(jié)構(gòu)(相關(guān)源碼):

// `RestoreFlags` 中的互斥組:
// [RestoreStringSingleQuotes, RestoreStringDoubleQuotes]
// [RestoreKeyWordUppercase, RestoreKeyWordLowercase]
// [RestoreNameUppercase, RestoreNameLowercase]
// [RestoreNameDoubleQuotes, RestoreNameBackQuotes]
// 靠前的 flag 擁有更高的優(yōu)先級。
const (
    RestoreStringSingleQuotes RestoreFlags = 1 << iota
    
    ...
)

// RestoreCtx is `Restore` context to hold flags and writer.
type RestoreCtx struct {
    Flags RestoreFlags
    In    io.Writer
}

// WriteKeyWord 用于向 `ctx` 中寫入關(guān)鍵字(例如:SELECT)。
// 它的大小寫受 `RestoreKeyWordUppercase`,`RestoreKeyWordLowercase` 控制
func (ctx *RestoreCtx) WriteKeyWord(keyWord string) {
    ...
}

// WriteString 用于向 `ctx` 中寫入字符串。
// 它是否被引號包裹及轉(zhuǎn)義規(guī)則受 `RestoreStringSingleQuotes`,`RestoreStringDoubleQuotes`,`RestoreStringEscapeBackslash` 控制。
func (ctx *RestoreCtx) WriteString(str string) {
    ...
}

// WriteName 用于向 `ctx` 中寫入名稱(庫名,表名,列名等)。
// 它是否被引號包裹及轉(zhuǎn)義規(guī)則受 `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 函數(shù),這個函數(shù)將當(dāng)前節(jié)點對應(yīng)的 SQL 文本追加至參數(shù) ctx 中,如果節(jié)點無效則返回 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 樹,遞歸調(diào)用每個節(jié)點的 Restore() 方法,即可拼接成一個完整的 SQL 文本。

值得注意的是,SQL 文本與 AST 是一個多對一的關(guān)系,我們不可能從 AST 結(jié)構(gòu)中還原出與原 SQL 完全一致的文本,
因此我們只要保證還原出的 SQL 文本與原 SQL 語義相同 即可。所謂語義相同,指的是由 AST 還原出的 SQL 文本再被解析為 AST 后,兩個 AST 是相等的。

我們已經(jīng)完成了接口設(shè)計和測試框架,具體的Restore() 函數(shù)留空。因此只需要選擇一個留空的 Restore() 函數(shù)實現(xiàn),并添加相應(yīng)的測試數(shù)據(jù),就可以提交一個 PR 了!

實現(xiàn) Restore() 函數(shù)的整體流程

請先閱讀 Proposal、Issue

在 Issue 中找到未實現(xiàn)的函數(shù)

在 Issue-pingcap/tidb#8532 中找到一個沒有被其他貢獻者認(rèn)領(lǐng)的任務(wù),例如 ast/expressions.go: BetweenExpr。

在 pingcap/parser 中找到任務(wù)對應(yīng)文件 ast/expressions.go

在文件中找到 BetweenExpr 結(jié)構(gòu)的 Restore 函數(shù):

// Restore implements Node interface.
func (n *BetweenExpr) Restore(ctx *RestoreCtx) error {
    return errors.New("Not implemented")
}

實現(xiàn) Restore() 函數(shù)

根據(jù) Node 節(jié)點結(jié)構(gòu)和 SQL 語法實現(xiàn)函數(shù)功能。

參考 MySQL 5.7 SQL Statement Syntax

寫單元測試

參考示例在相關(guān)文件下添加單元測試。

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

提交 PR

PR 標(biāo)題統(tǒng)一為:parser: implement Restore for XXX
請在 PR 中關(guān)聯(lián) Issue: pingcap/tidb#8532

示例

這里以實現(xiàn) BetweenExpr 的 Restore 函數(shù) PR 為例,進行詳細(xì)說明:

首先看 ast/expressions.go

我們要實現(xiàn)一個 ast.Node 結(jié)構(gòu)的 Restore 函數(shù),首先清楚該結(jié)構(gòu)代表什么短語,例如 BetweenExpr 代表 expr [NOT] BETWEEN expr AND expr (參見:MySQL 語法 - 比較函數(shù)和運算符)。

觀察 BetweenExpr 結(jié)構(gòu):

// BetweenExpr is for "between and" or "not between and" expression.
type BetweenExpr struct {
    exprNode
    // 被檢查的表達式
    Expr ExprNode
    // AND 左側(cè)的表達式
    Left ExprNode
    // AND 右側(cè)的表達式
    Right ExprNode
    // 是否有 NOT 關(guān)鍵字
    Not bool
}

3. 實現(xiàn) `BetweenExpr` 的 `Restore` 函數(shù):

```
// Restore implements Node interface.
func (n *BetweenExpr) Restore(ctx *RestoreCtx) error {
    // 調(diào)用 Expr 的 Restore,向 ctx 寫入 Expr
    if err := n.Expr.Restore(ctx); err != nil {
        return errors.Annotate(err, "An error occurred while restore BetweenExpr.Expr")
    }
    // 判斷是否有 NOT,并寫入相應(yīng)關(guān)鍵字
    if n.Not {
        ctx.WriteKeyWord(" NOT BETWEEN ")
    } else {
        ctx.WriteKeyWord(" BETWEEN ")
    }
    // 調(diào)用 Left 的 Restore
    if err := n.Left.Restore(ctx); err != nil {
        return errors.Annotate(err, "An error occurred while restore BetweenExpr.Left")
    }
    // 寫入 AND 關(guān)鍵字
    ctx.WriteKeyWord(" AND ")
    // 調(diào)用 Right 的 Restore
    if err := n.Right.Restore(ctx); err != nil {
        return errors.Annotate(err, "An error occurred while restore BetweenExpr.Right ")
    }
    return nil
}
```

接下來給函數(shù)實現(xiàn)添加單元測試, ast/expressions_test.go

// 添加測試函數(shù)
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""},
    }
    // 為了不依賴父節(jié)點實現(xiàn),通過 extractNodeFunc 抽取待測節(jié)點
    extractNodeFunc := func(node Node) Node {
        return node.(*SelectStmt).Fields.Fields[0].Expr
    }
    // Run Test
    RunNodeRestoreTest(c, testCases, "select %s", extractNodeFunc)
}

至此 BetweenExprRestore 函數(shù)實現(xiàn)完成,可以提交 PR 了。為了更好的理解測試邏輯,下面我們看 RunNodeRestoreTest

// 下面是測試邏輯,已經(jīng)實現(xiàn)好了,不需要 contributor 實現(xiàn)
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
        // 抽取指定節(jié)點并調(diào)用其 Restore 函數(shù)
        err = extractNodeFunc(stmt).Restore(NewRestoreCtx(DefaultRestoreFlags, &sb))
        c.Assert(err, IsNil, comment)
        // 通過 template 將 restore 結(jié)果拼接為完整的 SQL
        restoreSql := fmt.Sprintf(template, sb.String())
        comment = Commentf("source %#v; restore %v", testCase, restoreSql)
        // 測試 restore 結(jié)果與預(yù)期一致
        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)測試方法有些不一樣,
由于這類節(jié)點可以還原為一個完整的 SQL,因此直接在 parser_test.go 中測試。**

下面以實現(xiàn) UseStmt 的 Restore 函數(shù) PR 為例,對測試進行說明:

Restore 函數(shù)實現(xiàn)過程略。

給函數(shù)實現(xiàn)添加單元測試,參見 parser_test.go

在這個示例中,只添加了幾行測試數(shù)據(jù)就完成了測試:

// 添加 testCase 結(jié)構(gòu)的測試數(shù)據(jù)
{"use `select`", true, "USE `select`"},
{"use `sel``ect`", true, "USE `sel``ect`"},
{"use select", false, "USE `select`"},

我們看 testCase 結(jié)構(gòu)聲明:

type testCase struct {
    // 原 SQL
    src     string
    // 是否能被正確 parse
    ok      bool
    // 預(yù)期的 restore SQL
    restore string
}

測試代碼會判斷原 SQL parse 出 AST 后再還原的 SQL 是否與預(yù)期的 restore SQL 相等,具體的測試邏輯在 parser_test.goRunTest()、RunRestoreTest() 函數(shù),邏輯與前例類似,此處不再贅述。


加入 TiDB Contributor Club,無門檻參與開源項目,改變世界從這里開始吧(萌萌噠)。

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/17858.html

相關(guān)文章

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<