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)
}
}
下面來分析這段代碼
-
在mian函數里首先定義了一個sql變量,注意這個sql里面寫了一段注釋,后面我們會發現TIDB的sqlparser會識別sql里面的注釋。
-
調用parser的new方法,new方法里面很簡單,先去判斷一下有沒有導入驅動,如果沒有驅動會引發panic。正常會返回一個Parser的指針結構體。
-
調用上面的生成的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段為解析的結果依次設置一些標簽,最后返回解析結果,這里我們先不去看如何設置標簽的,因為越往里面看越深,會帶起更多的未知。先埋一個坑。
-
判斷有沒有解析失敗,一般非法的sql語句會引發解析失敗。然后獲取解析結果切片的長度,運行顯示的結果是1,因為我們傳入的sql是一條完整的語句。可以傳入多條sql語句,中間以分號隔開,這樣返回的解析結果的切片的長度等於sql語句的個數,有興趣的可以去嘗試一下,這里就不做演示了。
-
對返回的結果進行一個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
接口衍生出許多接口,我大致整理了一下, 他們之間的嵌套關系為:
回到原來的實例程序,在實例的最后的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
去重項時為trueFrom
: 存儲查詢對象的相關參數信息Where
:ExprNode
接口的實現,存儲一些查詢時的條件的相關信息參數Fields
:儲存查詢的字段的相關的信息參數GroupBy
:儲存group by
查詢時的相關信息Having
: 儲存having
查詢時的相關信息WindowSpecs
:OrderBy
:儲存group by
查詢時的相關信息Limit
: 儲存limit
查詢時的相關信息LockTp
:SelectLockType
實現了fmt.Stringer
的接口,當調用fmt.Println
打印它的時候會返回對應的SelectLockType
的類型的字符串, 因為是查類型,目前有for update
、in share mode
、for update nowait
、unsupported select lock type
、none
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
也只會調用一次,但卻打印出一串內容,就是這個道理。
上面遺留了一坑,SetFlag
具體做了什么?
func SetFlag(n Node) {
var setter flagSetter
n.Accept(&setter)
}
在SetFlag
函數里面調用了Node
的Accept
方法。Accept
上面分析過了,在里面會去調用傳進來的Visitor
接口類型的Enter
和Leave
方法
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的基本使用