亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

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

dingda / 3674人閱讀

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

作者:趙一霖
背景知識(shí)

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

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

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

// `RestoreFlags` 中的互斥組:
// [RestoreStringSingleQuotes, RestoreStringDoubleQuotes]
// [RestoreKeyWordUppercase, RestoreKeyWordLowercase]
// [RestoreNameUppercase, RestoreNameLowercase]
// [RestoreNameDoubleQuotes, RestoreNameBackQuotes]
// 靠前的 flag 擁有更高的優(yōu)先級(jí)。
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` 中寫入字符串。
// 它是否被引號(hào)包裹及轉(zhuǎn)義規(guī)則受 `RestoreStringSingleQuotes`,`RestoreStringDoubleQuotes`,`RestoreStringEscapeBackslash` 控制。
func (ctx *RestoreCtx) WriteString(str string) {
    ...
}

// WriteName 用于向 `ctx` 中寫入名稱(庫名,表名,列名等)。
// 它是否被引號(hào)包裹及轉(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{}) {
    ...
}

我們?cè)?ast.Node 接口中添加了一個(gè) Restore(ctx *RestoreCtx) error 函數(shù),這個(gè)函數(shù)將當(dāng)前節(jié)點(diǎn)對(duì)應(yīng)的 SQL 文本追加至參數(shù) ctx 中,如果節(jié)點(diǎn)無效則返回 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 為例,如下圖所示,我們通過遍歷整個(gè) AST 樹,遞歸調(diào)用每個(gè)節(jié)點(diǎn)的 Restore() 方法,即可拼接成一個(gè)完整的 SQL 文本。

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

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

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

請(qǐng)先閱讀 Proposal、Issue

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

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

在 pingcap/parser 中找到任務(wù)對(duì)應(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")
}

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

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

參考 MySQL 5.7 SQL Statement Syntax

寫單元測(cè)試

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

運(yùn)行 make test,確保所有的 test case 都能跑過。

提交 PR

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

示例

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

首先看 ast/expressions.go

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

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

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

3. 實(shí)現(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ù)實(shí)現(xiàn)添加單元測(cè)試, ast/expressions_test.go

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

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

// 下面是測(cè)試邏輯,已經(jīng)實(shí)現(xiàn)好了,不需要 contributor 實(shí)現(xiàn)
func RunNodeRestoreTest(c *C, nodeTestCases []NodeRestoreTestCase, template string, extractNodeFunc func(node Node) Node) {
    parser := parser.New()
    for _, testCase := range nodeTestCases {
        // 通過 template 將測(cè)試用例拼接為完整的 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ǎn)并調(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)
        // 測(cè)試 restore 結(jié)果與預(yù)期一致
        c.Assert(restoreSql, Equals, expectSQL, comment)
        stmt2, err := parser.ParseOneStmt(restoreSql, "", "")
        c.Assert(err, IsNil, comment)
        CleanNodeText(stmt)
        CleanNodeText(stmt2)
        // 測(cè)試解析的 stmt 與原 stmt 一致
        c.Assert(stmt2, DeepEquals, stmt, comment)
    }
}

**不過對(duì)于 ast.StmtNode(例如:ast.SelectStmt)測(cè)試方法有些不一樣,
由于這類節(jié)點(diǎn)可以還原為一個(gè)完整的 SQL,因此直接在 parser_test.go 中測(cè)試。**

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

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

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

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

// 添加 testCase 結(jié)構(gòu)的測(cè)試數(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
}

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


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

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

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/17858.html

相關(guān)文章

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<