前言
CockroachDB是著名的开源NewSQL数据库,对外提供了标准的SQL接口。上一篇文章《CockroachDB的Parser模块实现》介绍了CockroachDB中Parser模块,主要通过词法解析器将SQL语句解析成Token,然后通过语法解析器生成抽象语法树。本文将介绍如何在CockroachDB中添加一个新的SQL语法类型,来实现用户自定义的功能,并添加相应的测试,从而加深对相应模块的代码及原理的理解。
添加一个新的SQL语句
添加一个新的SQL语句,首先需要在SQL parser中添加必要的语法规则。CockroachDB的parser是通过 goyacc (go语言构建的一个yacc编译器)解析语法规则文件(pkg/sql/parser/sql.y)生成的。parser生成一颗抽象语法树(AST*),其树节点的定义在代码目录 *pkg/sql/sem/tree 下。
在parser中添加一个新的SQL语句有三个关键部分:
- 添加新的关键字; 
- 添加语法解析规则; 
- 添加新的语法节点类型 
我们将尝试在CockroachDB v2.1中添加一个新的SQL语句:FROBNICATE ,这个SQL语句支持三种语法,功能如下:
- FROBNICATE CLUSTER :在服务端打印 “It’s FrobnicateModeCluster” 
- FROBNICATE SESSION:在服务端打印 “It’s FrobnicateModeSession” 
- FROBNICATE ALL:在服务端打印 “It’s FrobnicateModeALL” 
添加新的关键字
第一步需要先定义关键字。在pkg/sql/parser/sql.y 文件中搜索”%token”,可以看到声明了许多token,例如我们语法中需要用到的SESSION、CLUSTER、ALL*都已经存在了,因此我们只需要添加关键字 *FROBNICATE 即可,如下所示:
如果关键字可以出现在标识符选项中,则必须保留该关键字(在需要使用它的地方,例如在列名中,必须使用双引号引起来),因此我们还需要将该关键字添加到”unreserved keywords”列表中,避免与标识符混淆。
| 12
 3
 4
 
 | unreserved_keyword:....
 | FROBNICATE
 ...
 
 | 
至此词法分析器已经能识别我们所有的关键字了,接下来需要告诉语法解析器如何处理新的语句。
添加语法解析规则
添加语法解析规则涉及到下列三个部分:
在 sql.y 文件中搜索”tree.Statement”,可以看到定义好的类型列表,在这里添加一个新的语法类型,如下所示:
| 1
 | %type <tree.Statement> frobnicate_stmt
 | 
然后搜索 “stmt: “,为新的语句类型添加一个case:
| 12
 3
 4
 
 | stmt:...
 | frobnicate_stmt // EXTEND WITH HELP: FROBNICATE
 ...
 
 | 
最后,我们需要在stmt中为新的语句添加语法规则及相应的帮助信息,如下所示:
| 12
 3
 4
 5
 6
 7
 
 | // %Help: FROBNICATE - show the simple message// %Category: Misc
 // %Text: FROBNICATE { CLUSTER | SESSION | ALL }
 frobnicate_stmt:
 FROBNICATE CLUSTER { return unimplemented(sqllex, "frobnicate cluster") }
 | FROBNICATE SESSION { return unimplemented(sqllex, "frobnicate session") }
 | FROBNICATE ALL { return unimplemented(sqllex, "frobnicate all") }
 
 | 
至此,parser已经能够识别新的语法类型并提示相应信息了。此处我们暂时使用unimplemented来代替该语法具体的操作,后续我们会继续完善该部分内容。
我们来尝试使用新的语法,首先需要产生 sql.go 文件:
| 12
 3
 4
 5
 
 | ~/go/src/github.com/cockroachdb/cockroach$ make generate...
 Type checking sql.y
 Compiling sql.go
 ...
 
 | 
然后重新编译:
| 12
 3
 
 | ~/go/src/github.com/cockroachdb/cockroach$ make build...
 github.com/cockroachdb/cockroach
 
 | 
使用新编译的二进制文件,起一个CockroachDB单节点实例:
 | 12
 3
 4
 
 | $ rm -fr cockroach-data/ && ./cockroach start --insecure...
 status:     initialized new cluster
 ...
 
 | 
另起一个终端,运行刚才添加的新语句:

这里虽然出现了umimplemented 报错,提示该语句还未完成,不过这时已经可以解析该语法了。如果执行一个不存在的语句,提示的错误则是 syntax error:

添加新的语法节点类型
现在我们已经可以解析相关语法,接下来还需要为该语句添加适当的语义。我们需要一个AST(抽象语法树)节点来将语句的结构信息从parser传递到runtime。
上文中我们在sql.y中添加过%type <tree.Statement>,因此我们还需要实现tree.Statement接口,该接口的定义在pkg/sql/sem/tree/stmt.go中,它有4个方法需要实现:
- fmt.Stringer 
- NodeFormatter 
- StatementType() 
- StatementTag() 
首先创建一个新文件pkg/sql/sem/tree/frobnicate.go,在该文件中实现相关AST节点:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 
 | package parser
 import "bytes"
 
 type Frobnicate struct {
 Mode FrobnicateMode
 }
 
 var _ Statement = &Frobnicate{}
 type FrobnicateMode int
 const (
 FrobnicateModeAll FrobnicateMode = iota
 FrobnicateModeCluster
 FrobnicateModeSession
 )
 
 func (node *Frobnicate) StatementType() StatementType { return Ack }
 func (node *Frobnicate) StatementTag() string { return "FROBNICATE" }
 func (node *Frobnicate) Format(buf *bytes.Buffer, f FmtFlags) {
 buf.WriteString("FROBNICATE ")
 switch node.Mode {
 case FrobnicateModeAll:
 buf.WriteString("ALL")
 case FrobnicateModeCluster:
 buf.WriteString("CLUSTER")
 case FrobnicateModeSession:
 buf.WriteString("SESSION")
 default:
 panic(fmt.Errorf("Unknown FROBNICATE mode %v!", node.Mode))
 }
 }
 
 func (node *Frobnicate) String() string {
 return AsString(node)
 }
 
 | 
接下来我们需要更新parser,让它在遇到该语句时返回相应的Frobnicate节点。
上文在 sql.y 文件中添加frobnicate_stmt规则时,使用了unimplemented来暂时代替具体实现,现在我们来实现这部分内容:
| 12
 3
 4
 5
 6
 7
 
 | // %Help: FROBNICATE - twiddle the various settings// %Category: Misc
 // %Text: FROBNICATE { CLUSTER | SESSION | ALL }
 frobnicate_stmt:
 FROBNICATE CLUSTER { $$.val = &tree.Frobnicate{Mode: tree.FrobnicateModeCluster} }
 | FROBNICATE SESSION { $$.val = &tree.Frobnicate{Mode: tree.FrobnicateModeSession} }
 | FROBNICATE ALL { $$.val = &tree.Frobnicate{Mode: tree.FrobnicateModeAll} }
 
 | 
重新编译,然后通过客户端运行该语句:

这里返回了一个错误,不过这是一个来自SQL planner的错误,当SQL planner识别到新的语法节点但是不知道该如何处理时就会报这个错误。
我们需要告诉planner如何处理这个语法,相应的代码在 pkg/sql/plan.go 中,我们在newPlan函数中为其添加一个case:
| 12
 3
 
 | switch n := stmt.(type) {case *tree.Frobnicate:
 return p.Frobnicate(ctx, n)
 
 | 
然后我们在 pkg/sql/frobnicate.go 中实现对应的方法:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | package sqlimport (
 "context"
 "fmt"
 "github.com/cockroachdb/cockroach/pkg/sql/sem/tree"
 )
 
 func (p *planner) Frobnicate(ctx context.Context, stmt *tree.Frobnicate) (planNode, error) {
 return nil, fmt.Errorf("We're not quite frobnicating yet...")
 }
 
 | 
再次编译并运行:

这里已经根据相应函数内的代码返回了一个错误,接下来我们修改该函数,实现打印出对应信息的功能:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | func (p *planner) Frobnicate(ctx context.Context, stmt *tree.Frobnicate) (planNode, error) {switch stmt.Mode {
 case tree.FrobnicateModeCluster:
 fmt.Println("It's FrobnicateModeCluster")
 case tree.FrobnicateModeSession:
 fmt.Println("It's FrobnicateModeSession")
 case tree.FrobnicateModeAll:
 fmt.Println("It's FrobnicateModeAll")
 default:
 return nil, fmt.Errorf("Unhandled FROBNICATE mode %v!", stmt.Mode)
 }
 return &zeroNode{}, nil
 }
 
 | 
注意:这里直接返回zeroNode,它是一个不包含任何行和列的planNode,所以客户端将看不到任何数据返回。如果你需要返回一些内容给客户端,可以查看 pkg/sql 包下是否有合适的planNode,或者使用自定义的planNode。
重新编译并运行:
| 12
 3
 
 | ./cockroach sql --insecure -e "frobnicate cluster"./cockroach sql --insecure -e "frobnicate session"
 ./cockroach sql --insecure -e "frobnicate all"
 
 | 
这时在服务端屏幕可以观察到已经打印出对应的内容:

至此我们已经实现了一个简单的 frobnicate 语法,别忘了最后还有一个重要步骤,添加相应的测试用例。
添加测试用例
此处需要为该语法解析添加测试用例,相关测试代码位于 pkg/sql/parser/parse_test.go ,我们只需要在对应的地方加入需要测试的语法即可:
| 12
 3
 
 | {`FROBNICATE CLUSTER`},{`FROBNICATE SESSION`},
 {`FROBNICATE ALL`},
 
 | 
然后运行测试:
如果在上述过程中可以顺利添加语句并成功编译构建,则此处应当可以成功跑通相应的测试用例。
关于SQL语句的功能测试代码在 pkg/sql/logictest/ 下相应文件中,只需要在对应的地方添加新的SQL语句和期望的返回结果即可。由于此处我们仅实现了服务端打印的测试功能,便不具体在此处进行测试,感兴趣的同学可以自行查看对应的测试文件。
给SQL语句添加别名
如果我们需要经常运行Frobnicate语句,希望它可以更简洁的话,我们也可以给它添加一个别名FROB,实现过程很简单,只需要在sql.y中添加几行规则即可:
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | unreserved_keyword:...
 + | FROB
 | FROBNICATE
 ...
 frobnicate_stmt:
 FROBNICATE CLUSTER { $$.val = &tree.Frobnicate{Mode: tree.FrobnicateModeCluster} }
 | FROBNICATE SESSION { $$.val = &tree.Frobnicate{Mode: tree.FrobnicateModeSession} }
 | FROBNICATE ALL { $$.val = &tree.Frobnicate{Mode: tree.FrobnicateModeAll} }
 + | FROB CLUSTER { $$.val = &tree.Frobnicate{Mode: tree.FrobnicateModeCluster} }
 + | FROB SESSION { $$.val = &tree.Frobnicate{Mode: tree.FrobnicateModeSession} }
 + | FROB ALL { $$.val = &tree.Frobnicate{Mode: tree.FrobnicateModeAll} }
 
 | 
总结
本文通过一个添加SQL语句的案例,介绍了SQL语句在CockroachDB代码中的实际解析和处理流程。本文所使用的案例参考了https://github.com/cockroachdb/cockroach/blob/master/docs/codelabs/01-sql-statement.md 中的案例,为了便于读者理解,在实现具体功能时直接在服务端打印内容并返回zeroNode。关于planner和生成执行计划相关的原理及实现,会在后续的SQL引擎系列文章中进行详细介绍。