博客 / 詳情

返回

在TiDB中實現一個關鍵字——Parser篇

前言

其實,我們一直都很想,基於TiDB做一些很cool,很hacker的事情。比如我們團隊小夥伴發了一篇關於TiDB for Pg兼容Gitlab的一篇文章,具體文章可以參考鏈接:

TiDB4PG之兼容Gitlab - 知乎 (zhihu.com)

這篇文章我就來簡單聊聊實現兼容到Gitlab的艱苦過程。

我們採用了一個相對較笨的方式,將Gitlab的源碼通過編譯啓動的方式,連接到最開始的TiDB for PG,這樣肯定是報錯,不行的,畢竟很多東西沒有兼容。為了能快速實現兼容,我們決定採取抓包的方式,將Gitlab連接到TiDB For Pg的所執行的SQL語句找出來,進行粗略的分類整理,去看看有哪些SQL語句,去定製化的兼容開發。在兼容實現這些SQL語句時,難點之一,就有DML語句中的Returning關鍵字。

原理

在TiDB-Server裏面,執行一個SQL語句的流程,大致可以分為解析、編譯、優化、執行、返回結果這幾個階段。而實現一個關鍵字,同樣的需要在這幾個階段做一些文章。而對於Returning關鍵字而言,我們可以從DML語句中相對簡單的DELETE語句入手,所以接下來的改造過程,最終結果就是實現了DELETE RETURNING 句式。

改造實現過程

Parser

從SQL在TiDB中的流轉過程,邁入後續代碼的第一步,就是將客户端傳來的SQL語句解析為一個能夠被後續代碼認識的結構,也就是AST樹。而這一過程,主要就是在Parser這個模塊兒中實現。

在TiDB v5.0以前,Parser這個包是有一個專門的代碼倉庫的,通過go mod的方式導入到TiDB,而在5.0之後,TiDB將Parser包挪到TiDB源碼當中。TiDB for PG 的源碼也是基於TiDB v4.0.14改造的,這次我想嘗試一下,在TiDB 最新的源碼中實現RETURNING關鍵字,一個是為了hackathon的比賽作準備,另一個也是為了之後TiDB for PG向着TiDB新版本靠攏試試水。

Paser模塊主要靠Lexer & Yacc這兩個模塊共同構成。在解析的過程中,Lexer組件會將SQL文本轉換為一個又一個token傳給parser,而parser中最為重要的parser.go文件,則是goyacc工具根據parser.y文件生成的,根據文件中語法的定義,來決定lexer中傳過來的token能夠與什麼語法規則進行匹配,最終輸出AST結構樹,也就是parser/ast 中定義的各類stmt,而我們要實現的就是dml.go中的DeleteStmt。

// DeleteStmt is a statement to delete rows from table.
// See https://dev.mysql.com/doc/refman/5.7/en/delete.html
type DeleteStmt struct {
    dmlNode

    // TableRefs is used in both single table and multiple table delete statement.
    TableRefs *TableRefsClause
    // Tables is only used in multiple table delete statement.
    Tables       *DeleteTableList
    Where        ExprNode
    Order        *OrderByClause
    Limit        *Limit
    Priority     mysql.PriorityEnum
    IgnoreErr    bool
    Quick        bool
    IsMultiTable bool
    BeforeFrom   bool
    // TableHints represents the table level Optimizer Hint for join type.
    TableHints []*TableOptimizerHint
    With       *WithClause
    // 我們今天的主題,Returning 關鍵字
    Returning  *ReturningClause
}

type ReturningClause struct {
    node
    Fields *FieldList
}

func (n *ReturningClause) Restore(ctx *format.RestoreCtx) error {
    ctx.WriteKeyWord("Returning ")
    for i, item := range n.Fields.Fields {
        if i != 0 {
            ctx.WritePlain(",")
        }
        if err := item.Restore(ctx); err != nil {
            return errors.Annotatef(err, "An error occurred while restore ReturningClause.Fields[%d]", i)
        }
    }
    return nil
}

func (n *ReturningClause) Accept(v Visitor) (Node, bool) {
    newNode, skipChildren := v.Enter(n)
    if skipChildren {
        return v.Leave(newNode)
    }
    n = newNode.(*ReturningClause)

    if n.Fields != nil {
        node, ok := n.Fields.Accept(v)
        if !ok {
            return n, false
        }
        n.Fields = node.(*FieldList)
    }

    return v.Leave(n)
}

原諒篇幅有限,不能把所有代碼貼出來。這裏值得提一嘴的就是Accept()方法。在ast包中,幾乎所有的stmt結構都實現了ast.Node接口,這個接口中的Accept()方法,主要作用就是處理AST,通過Visitor模式遍歷所有的節點,並且對AST結構做一個轉換。而為了能正常將RETURNING關鍵字轉換成DeleteStmt,我們還需要在parser中去將RETURNING 關鍵字註冊為token。

image-20211229181323248

image-20211229181343322

在parser.y中definitions區域定義好RETURNING相關句式的token,比如RETURNING關鍵字,還有ReturningClause、ReturningOption句式。

關於parser的一些基礎知識可以參考文章:

TiDB Parser模塊的簡單解讀與改造方法 - 知乎 (zhihu.com)

TiDB 源碼閲讀系列文章(五)TiDB SQL Parser 的實現 | PingCAP

在做完這些之後,我們就能夠在parser.y的rule部分中,找到DELETE 句式,加入returning句式了,也就是ReturningOptional,接着在其中寫上簡單的邏輯。

/*******************************************************************
 *
 *  Delete Statement
 *
 *******************************************************************/
DeleteWithoutUsingStmt:
    "DELETE" TableOptimizerHintsOpt PriorityOpt QuickOptional IgnoreOptional "FROM" TableName PartitionNameListOpt TableAsNameOpt IndexHintListOpt WhereClauseOptional OrderByOptional LimitClause ReturningOptional
    {
        ... 此處省略 ...
        if $14 != nil {
            x.Returning = $14.(*ast.ReturningClause)
        }

        $$ = x
    }
|    "DELETE" TableOptimizerHintsOpt PriorityOpt QuickOptional IgnoreOptional TableAliasRefList "FROM" TableRefs WhereClauseOptional ReturningOptional
    {
        ... 此處省略 ...
        if $10 != nil {
            x.Returning = $10.(*ast.ReturningClause)
        }
        $$ = x
    }

DeleteWithUsingStmt:
    "DELETE" TableOptimizerHintsOpt PriorityOpt QuickOptional IgnoreOptional "FROM" TableAliasRefList "USING" TableRefs WhereClauseOptional ReturningOptional
    {
        ... 此處省略 ...
        if $11 != nil {
            x.Returning = $11.(*ast.ReturningClause)
        }
        $$ = x
    }

ReturningClause:
    "RETURNING" SelectStmtFieldList
    {
        $$ = &ast.ReturningClause{Fields: $2.(*ast.FieldList)}
    }

ReturningOptional:
    {
        $$ = nil
    }
|    ReturningClause
    {
        $$ = $1
    }

接着就能利用parser/bin/goyacc 工具,根據最新的paser.y生成最終的parser.go,進入parser包中,運行make all即可。

image-20211229181414078

需要注意的是,對於關鍵字,在生成最新的parser.go之後,我們還需要在parser/misc.go中定義,這是由於lexer採用了字典樹技術進行token識別,而其實現代碼就是在其中,不然lexer會不認識這所謂的關鍵字。

image-20211229181426763

改完之後的驗證其實很簡單,在parser包中找到parser_test.go的測試文件,寫一個delete returning的句式,運行一遍測試,過了,那就是OK了。

image-20211229181440886

還可以啓動tidb源碼,用mysql客户端連上去,執行一個delete returning的句式,能夠成功返回,那麼説明,這個關鍵字同樣是兼容成功的。

image-20211229182141522

簡單總結

到了這一步,初步關鍵字兼容已經實現了,注意,現在還只是初步兼容,而要使其生效,則需要進入到接下來的Plan制定以及執行器Exexutor執行的部分了。這一部分在TiDB v5.0的改造還在研究的過程中,畢竟相對於TiDB v4.0.14的計劃制定、優化有些許變動,還沒來得及去研究,我會在後續文章中詳細闡述。最後給大家看看TiDB for PG的returning兼容成果吧。

image-20211229181524940

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.