TIDB Salparse源碼解析 -1


TIDB Salparse源碼解析

網上搜尋了很多關於TIDB Salparse資料,但是關於源碼解析的幾乎沒有找到,所以想自己寫點資料記錄一下。

實例解析

隨着版本的迭代,官網給出的【例子】已經不能用了,下面是對官網的例子做出的修改

package main

import (
	"fmt"
	"github.com/pingcap/parser"
	"github.com/pingcap/parser/ast"
	_ "github.com/pingcap/tidb/types/parser_driver"
)

type visitor struct{}

func (v *visitor) Enter(in ast.Node) (out ast.Node, skipChildren bool) {
	fmt.Printf("%T\n", in)
	return in, false
}

func (v *visitor) Leave(in ast.Node) (out ast.Node, ok bool) {
	return in, true
}

func main() {
	sql := "SELECT /*+ TIDB_SMJ(employees) */ emp_no, first_name, last_name " +
		"FROM employees USE INDEX (last_name) " +
		"where last_name='Aamodt' and gender='F' and birth_date > '1960-01-01'"

	p := parser.New()
	stmt, warns, err := p.Parse(sql, "", "")
	if err != nil {
		fmt.Println(warns, "\n")
		fmt.Printf("parse error:\n%v\n%s", err, sql)
		return
	}
	fmt.Println("the length of stmt is", len(stmt))
	for _, stmNode := range stmt {
		v := visitor{}
		stmNode.Accept(&v)
	}
}

下面來分析這段代碼

  1. 在mian函數里首先定義了一個sql變量,注意這個sql里面寫了一段注釋,后面我們會發現TIDB的sqlparser會識別sql里面的注釋。

  2. 調用parser的new方法,new方法里面很簡單,先去判斷一下有沒有導入驅動,如果沒有驅動會引發panic。正常會返回一個Parser的指針結構體。

  3. 調用上面的生成的Parser的指針結構體對象的Parse方法,我們看一下Parse方法的源碼

    // Parse parses a query string to raw ast.StmtNode.
    // If charset or collation is "", default charset and collation will be used.
    func (parser *Parser) Parse(sql, charset, collation string) (stmt []ast.StmtNode, warns []error, err error) {
    	if charset == "" {
    		charset = mysql.DefaultCharset // utf8mb4
    	}
    	if collation == "" {
    		collation = mysql.DefaultCollationName // utf8mb4_bin
    	}
    
        
    	parser.charset = charset
    	parser.collation = collation
    	parser.src = sql
    	parser.result = parser.result[:0]
    
        
    	var l yyLexer
    	parser.lexer.reset(sql)
    	l = &parser.lexer
    	yyParse(l, parser)
    
        
    	warns, errs := l.Errors()
    	if len(warns) > 0 {
    		warns = append([]error(nil), warns...)
    	} else {
    		warns = nil
    	}
    	if len(errs) != 0 {
    		return nil, warns, errors.Trace(errs[0])
    	}
        
        
    	for _, stmt := range parser.result {
    		ast.SetFlag(stmt)
    	}
    	return parser.result, warns, nil
    }
    

    方法內部我們以空白行為分割分為5段,

    1-2段先是指定字符集和排序規則,默認分別是utf8mb4、utf8mb4_bin,然后對Parser指針結構體的屬性進行一些初始化

    第3段是【goyacc】根據所提供的yacc文件【parser.y】生成的代碼【parser.go】所提供的接口來對輸入的字符串進行解析,最后生成解析樹。我們只用知道它是怎么一回事,最終是干了啥就可以了。

    第4段就是判斷一個解析有沒有錯誤

    第5段為解析的結果依次設置一些標簽,最后返回解析結果,這里我們先不去看如何設置標簽的,因為越往里面看越深,會帶起更多的未知。先埋一個坑。

  4. 判斷有沒有解析失敗,一般非法的sql語句會引發解析失敗。然后獲取解析結果切片的長度,運行顯示的結果是1,因為我們傳入的sql是一條完整的語句。可以傳入多條sql語句,中間以分號隔開,這樣返回的解析結果的切片的長度等於sql語句的個數,有興趣的可以去嘗試一下,這里就不做演示了。

  5. 對返回的結果進行一個for range 遍歷,在for循環內部,初始化一個visitor結構體對象,然后從for循環中取出的對象的Accept方法,將visitor結構體對象的地址傳入。至於Accept方法里面具體干了什么,先看一下for range取到的值是什么,從Parse方法的返回值可以看到,正常運行的話會返回值類型為ast.StmtNode的切片。我們使用開發工具查看StmtNode的源碼

    // StmtNode represents statement node.
    // Name of implementations should have 'Stmt' suffix.
    type StmtNode interface {
    	Node
    	statement()
    }
    

    發現StmtNode是一個interface,所以我們不知道具體實現了該接口類型的Accept方法具體干了什么。先埋一個坑,后面具體分體。

節點

從上面的結論中我們知道了經過Parse返回的結果為一個StmtNode的結構體,而StmtNode嵌套了一個Node結構體,查看Node的源碼

// Node is the basic element of the AST.
// Interfaces embed Node should have 'Node' name suffix.
type Node interface {
	// Restore returns the sql text from ast tree
	Restore(ctx *format.RestoreCtx) error
	// Accept accepts Visitor to visit itself.
	// The returned node should replace original node.
	// ok returns false to stop visiting.
	//
	// Implementation of this method should first call visitor.Enter,
	// assign the returned node to its method receiver, if skipChildren returns true,
	// children should be skipped. Otherwise, call its children in particular order that
	// later elements depends on former elements. Finally, return visitor.Leave.
	Accept(v Visitor) (node Node, ok bool)
	// Text returns the original text of the element.
	Text() string
	// SetText sets original text to the Node.
	SetText(text string)
}

可以看到首行注釋:Node是語法抽象樹的最基本的元素。AST是abstract syntax tree的縮寫

Node里面有一個Accept方法,在上面的介紹里我們看到了StmtNode調用了Accept方法,而StmtNode嵌套了Node接口,所以只要是StmtNode接口類型就可以直接調用Accept方法。

實際上在當前模塊【ast】下,由Node接口衍生出許多接口,我大致整理了一下, 他們之間的嵌套關系為:

avatar

回到原來的實例程序,在實例的最后的for循環修改為

for _, stmNode := range stmt {
		v := visitor{}
		fmt.Printf("%T\n", stmNode)
		stmNode.Accept(&v)
	}

運行結果在控制台打印出*ast.SelectStmt, 【*ast.SelectStmt】

ast.SelectStmt結構體

// SelectStmt represents the select query node.
// See https://dev.mysql.com/doc/refman/5.7/en/select.html
type SelectStmt struct {
	dmlNode
	resultSetNode

	// SelectStmtOpts wraps around select hints and switches.
	*SelectStmtOpts
	// Distinct represents whether the select has distinct option.
	Distinct bool
	// From is the from clause of the query.
	From *TableRefsClause
	// Where is the where clause in select statement.
	Where ExprNode
	// Fields is the select expression list.
	Fields *FieldList
	// GroupBy is the group by expression list.
	GroupBy *GroupByClause
	// Having is the having condition.
	Having *HavingClause
	// WindowSpecs is the window specification list.
	WindowSpecs []WindowSpec
	// OrderBy is the ordering expression list.
	OrderBy *OrderByClause
	// Limit is the limit clause.
	Limit *Limit
	// LockTp is the lock type
	LockTp SelectLockType
	// TableHints represents the table level Optimizer Hint for join type
	TableHints []*TableOptimizerHint
	// IsAfterUnionDistinct indicates whether it's a stmt after "union distinct".
	IsAfterUnionDistinct bool
	// IsInBraces indicates whether it's a stmt in brace.
	IsInBraces bool
	// QueryBlockOffset indicates the order of this SelectStmt if counted from left to right in the sql text.
	QueryBlockOffset int
	// SelectIntoOpt is the select-into option.
	SelectIntoOpt *SelectIntoOption
}

這個結構體看上去很復雜,但我們看到了一些熟悉的字眼(mysql的關鍵字)

  • dmlNode: 內部使用的一個實現了DMLNode接口的結構體
  • resultSetNode:
  • *SelectStmtOpts:
  • Distinct: 當sql語句中有distinct去重項時為true
  • From: 存儲查詢對象的相關參數信息
  • Where: ExprNode接口的實現,存儲一些查詢時的條件的相關信息參數
  • Fields:儲存查詢的字段的相關的信息參數
  • GroupBy:儲存group by查詢時的相關信息
  • Having: 儲存having查詢時的相關信息
  • WindowSpecs
  • OrderBy:儲存group by查詢時的相關信息
  • Limit: 儲存limit查詢時的相關信息
  • LockTp: SelectLockType實現了fmt.Stringer的接口,當調用fmt.Println打印它的時候會返回對應的SelectLockType的類型的字符串, 因為是查類型,目前有for updatein share modefor update nowaitunsupported select lock typenone
  • TableHints: 表示聯接類型的表級優化器提示
  • IsAfterUnionDistinct:
  • IsInBraces:
  • QueryBlockOffset:
  • SelectIntoOpt:

Accept方法

// Accept implements Node Accept interface.
func (n *SelectStmt) Accept(v Visitor) (Node, bool) {
    // 調用Vistor的Enter方法,返回一個節點類型和一個bool值
    // 具體的業務可以在Enter方法里面實現
	newNode, skipChildren := v.Enter(n)
    
    // 如果業務選擇跳過,那么會直接將調用當前Vistor的Leave方法
	if skipChildren {
		return v.Leave(newNode)
	}
	
    // 進行非安全類型斷言,因為當前節點類型一定是`SelectStmt`,所以這種斷言它不會出錯,同時進行類型轉換(主要目的)
	n = newNode.(*SelectStmt)
    
    
    // 下面這些if判斷看上去很長,但其實都是在做一件事情
    // 判斷當前的子節點是否存在,如果不為空,那么存在該子節點就是一個Node接口的實現,調用該屬性的Accept方法,然后將返回的節點的interface進行斷言,同時將抽象的interface轉換為具體的struct
  
    
    // 最后調用Leave方法返回
    
	if n.TableHints != nil && len(n.TableHints) != 0 {
		newHints := make([]*TableOptimizerHint, len(n.TableHints))
		for i, hint := range n.TableHints {
			node, ok := hint.Accept(v)
			if !ok {
				return n, false
			}
			newHints[i] = node.(*TableOptimizerHint)
		}
		n.TableHints = newHints
	}

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

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

	if n.Where != nil {
		node, ok := n.Where.Accept(v)
		if !ok {
			return n, false
		}
		n.Where = node.(ExprNode)
	}

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

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

	for i, spec := range n.WindowSpecs {
		node, ok := spec.Accept(v)
		if !ok {
			return n, false
		}
		n.WindowSpecs[i] = *node.(*WindowSpec)
	}

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

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

	return v.Leave(n)
}

** 看注釋 **

其實基本上所有的Node及其衍生的接口類型的Accept方法都是做這一件事情,保證所有的節點都可以來將Visitor接受,讓Visitor對象可以遍歷所有的節點,這樣就可以在Visitor里面實現對Ast的完整處理

開始的那個例子,只有一個sql語句,根據之前的結論,它只會返回長度為1的切片,所以在for循環中Enter方法只會調用一次,Enter方法中的fmt.Printf也只會調用一次,但卻打印出一串內容,就是這個道理。

【ats.SetFlag】

上面遺留了一坑,SetFlag具體做了什么?

func SetFlag(n Node) {
	var setter flagSetter
	n.Accept(&setter)
}

SetFlag函數里面調用了NodeAccept方法。Accept上面分析過了,在里面會去調用傳進來的Visitor接口類型的EnterLeave方法

【ast.flagSetter】

type flagSetter struct {
}

func (f *flagSetter) Enter(in Node) (Node, bool) {
	return in, false
}

func (f *flagSetter) Leave(in Node) (Node, bool) {
	if x, ok := in.(ParamMarkerExpr); ok {
		x.SetFlag(FlagHasParamMarker)
	}
	switch x := in.(type) {
	case *AggregateFuncExpr:
		f.aggregateFunc(x)
	case *WindowFuncExpr:
		f.windowFunc(x)
	case *BetweenExpr:
		x.SetFlag(x.Expr.GetFlag() | x.Left.GetFlag() | x.Right.GetFlag())
	case *BinaryOperationExpr:
		x.SetFlag(x.L.GetFlag() | x.R.GetFlag())
	case *CaseExpr:
		f.caseExpr(x)
	case *ColumnNameExpr:
		x.SetFlag(FlagHasReference)
	case *CompareSubqueryExpr:
		x.SetFlag(x.L.GetFlag() | x.R.GetFlag())
	case *DefaultExpr:
		x.SetFlag(FlagHasDefault)
	case *ExistsSubqueryExpr:
		x.SetFlag(x.Sel.GetFlag())
	case *FuncCallExpr:
		f.funcCall(x)
	case *FuncCastExpr:
		x.SetFlag(FlagHasFunc | x.Expr.GetFlag())
	case *IsNullExpr:
		x.SetFlag(x.Expr.GetFlag())
	case *IsTruthExpr:
		x.SetFlag(x.Expr.GetFlag())
	case *ParenthesesExpr:
		x.SetFlag(x.Expr.GetFlag())
	case *PatternInExpr:
		f.patternIn(x)
	case *PatternLikeExpr:
		f.patternLike(x)
	case *PatternRegexpExpr:
		f.patternRegexp(x)
	case *PositionExpr:
		x.SetFlag(FlagHasReference)
	case *RowExpr:
		f.row(x)
	case *SubqueryExpr:
		x.SetFlag(FlagHasSubquery)
	case *UnaryOperationExpr:
		x.SetFlag(x.V.GetFlag())
	case *ValuesExpr:
		x.SetFlag(FlagHasReference)
	case *VariableExpr:
		if x.Value == nil {
			x.SetFlag(FlagHasVariable)
		} else {
			x.SetFlag(FlagHasVariable | x.Value.GetFlag())
		}
	}

	return in, true
}

Enter方法什么都沒做,關鍵是Leave,Leave里面使用了一個很長的switch case根據傳進來的節點的不同類型來做出不同的處理。

示例解析就了解到這兒,雖然還是有很多未解決的問題,但大致了解了下TIDB sqlparse為我們提供的api的基本使用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM