赞
踩
导读:欢迎来到 StarRocks 源码解析系列文章,我们将为你全方位揭晓 StarRocks 背后的技术原理和实践细节,助你逐步了解这款明星开源数据库产品。本期将主要介绍 StarRocks Parser 源码。
作者:刘航源 StarRocks Committer
Parser 的主要工作是将字符串类型的 SQL 语句文本,解析成树形结构的抽象语法树(Abstract Syntax Tree,AST),便于语义解析和查询计划的生成与优化。本文首先介绍 StarRocks 中实现语法解析的工具 Antlr4,并介绍其在 StarRocks 内部的实现,以及相关的 AST 生成逻辑。
在学习 Parser 之前,我们需要先了解一下相关的基本概念。
除了理解概念外,对工具的了解也是必不可少的。
Antlr4(Another Tool for Language Recognition)是一款基于 Java 开发的开源的语法分析器生成工具,能够根据语法规则文件生成对应的语法分析器,不仅广泛应用于 DSL 构建、语言词法语法解析等领域,在当下非常多流行的框架中也都有使用。
Antlr 可以生成不同 target 的 AST,包括 Java、C++、JS、Python、C# 等,满足不同语言的开发需求。具体来说,Antlr4 的优势主要包括如下五个方面:
在 StarRocks 中,该怎么将 Parser 和 Antlr4 结合起来并在系统内部使用呢?
SqlParser 是 StarRocks 的 SQL 解析代码的入口,我们通过分析下面的代码,就能了解整个 Parser 的主要逻辑流程。
- StarRocksLexer lexer = new StarRocksLexer(new CaseInsensitiveStream(CharStreams.fromString(sql)));
- CommonTokenStream tokenStream = new CommonTokenStream(lexer);
- StarRocksParser parser = new StarRocksParser(tokenStream);
- StarRocksParser.sqlMode = sqlMode;
- parser.removeErrorListeners();
- parser.addErrorListener(new ErrorHandler());
- StarRocksParser.SqlStatementsContext sqlStatements = parser.sqlStatements();
- StatementBase statement = (StatementBase) new AstBuilder()
- .visitSingleStatement(sqlStatements.singleStatement(0));
首先,我们需要初始化 StarRocksLexer,即词法解析器。在这里,StarRocksLexer 是根据上文介绍的 StarRocksLex.g4 词法文件,使用 Antlr4 自动生成的代码类。
然后,代码将词法解析器 StarRocksLexer 作为参数,传入语法解析器中。语法解析器类StarRocksParser,同样是根据上文介绍的 StarRocks.g4 语法文件自动生成的代码类。
到这里,我们就完成了语法解析类的构建。之后再调用 parser.addErrorListener(new ErrorHandler()),将 Antlr4 的默认错误处理规则,替换为自定义的错误处理逻辑即可。
需要注意的是,调用 parser.sqlStatements() 返回值 StarRocksParser.SqlStatementsContext,这是一套 antlr 自定义的抽象语法树,根据语法文件生成。之后我们可以使用 AstBuilder,将 antlr 的语法树转换为 StarRocks 的抽象语法树(ASTBuilder 将在下一小节介绍)。
至此,整个 StarRocks Parser 的主流程结束,生成的 statement 将作为语义解析 Analyzer 的入参。值得注意的是,在例子中的第4行,代码对 StarRocksParser 赋值了一个 SQL_MODE。在不同的 SQL_MODE 下,相同的 sql 语句可能代表不同的逻辑。
再补充一句,当前涉及语法方面的 SQL_MODE,StarRocks 仅支持 PIPES_AS_CONCAT。如果设置了PIPES_AS_CONCAT,“||”将被解析为字符串 concat 函数,否则"||"代表逻辑或。而 Parser 在解析之前,会根据 session variable 中设置的 sql_mode 来判断将要使用何种方式来解析。
在主流程中,StarRocksLexer 和 StarRocksParser 分别基于 StarRocksLex.g4 和 StarRocks.g4 文件,由 Antlr4 自动翻译并生成代码。而 AstBuilder 则需要手动编写的代码模块,这也是 Parser 的逻辑重点。
下面将介绍 AstBuilder 的整体逻辑,我们以 showTables 和 ShowDatabases 语句为例,语法文件如下:
- singleStatement
- : SHOW FULL? TABLES ((FROM | IN) db=qualifiedName)?
- ((LIKE pattern=string) | (WHERE expression))? #showTables
- | SHOW DATABASES ((LIKE pattern=string) | (WHERE expression))? #showDatabases
antlr 的语法文件采用 BNF 范式,用'|'表示分支选项,'?'表达0次或一次,其他符号可类比正则表达式。比如SHOW FULL?
表示在 showTables 中,SHOW 关键字是必须的,而 FULL 则是一个可选择项。
Antlr4 会根据语法文件生成一份 Visitor 模式的代码,这样就可以做到动作代码与文法产生式解耦,利于文法产生式的重用。而自定义的 AstBuilder 文件则继承了 StarRocksBaseVisitor,用于将 antlr 内部的 AST 翻译成 StarRocks 自定义的 AST。
我们依旧以 show table 和 show database 语法为例,代码如下:
- public class AstBuilder extends StarRocksBaseVisitor<ParseNode> {
- @Override
- public ParseNode visitSingleStatement(StarRocksParser.SingleStatementContext context) {
- return visit(context.statement());
- }
-
- // -------------------------------- Statement ------------------------------
-
- @Override
- public ParseNode visitShowDatabases(StarRocksParser.ShowDatabasesContext context) {
- if (context.pattern != null) {
- StringLiteral stringLiteral = (StringLiteral) visit(context.pattern);
- return new ShowDbStmt(stringLiteral.getValue());
- } else if (context.expression() != null) {
- return new ShowDbStmt(null, (Expr) visit(context.expression()));
- } else {
- return new ShowDbStmt(null, null);
- }
- }
-
- @Override
- public ParseNode visitShowTables(StarRocksParser.ShowTablesContext context) {
- boolean isVerbose = context.FULL() != null;
- String database = null;
- if (context.db != null) {
- database = context.db.getText();
- }
-
- if (context.pattern != null) {
- StringLiteral stringLiteral = (StringLiteral) visit(context.pattern);
- return new ShowTableStmt(database, isVerbose, stringLiteral.getValue());
- } else if (context.expression() != null) {
- return new ShowTableStmt(database, isVerbose, null, (Expr) visit(context.expression()));
- } else {
- return new ShowTableStmt(database, isVerbose, null);
- }
- }
可以看到,Antlr 采用递归下降文法,所有的解析流程都是从上到下的,从树的根节点进行解析,一直解析到树的叶子节点。singleStatement 为所有 sql statement 的根,singleStatement 可能根据 statement 的类型不同,访问不同的 visitXXX,本例即 visitShowDatabases 或 visitShowTables。
而 Antlr 则依据我们语法文件中的第3、4行后面的#注释,生成了不同的 visit 函数。#后面的注释,在Antlr 中表示为前面的一长串语法文件“起个名字”。而在相应的 visit 函数中,开发者可以根据自身的需要,将 statement 独有的代码封装在一个函数中。
这样做,就起到了语法文件与语法解析代码充分解耦合的作用。而 visit 的返回值返回一个 AST 的基类,在StarRocks 中称为 ParseNode。至此,我们就生成了 StarRocks 内部需要的抽象语法树,Parser 模块的所有功能结束。
今天我们对 Parser 源码进行了重点分析,包括 ANTLR4、SqlParser 和 ASTBuilder。与此同时,我们还通过一个例子,介绍了如何将一条文本类型的 SQL 语句,一步步解析成 StarRocks 内部使用的 AST。
可以看到,Parser 能判断出用户的 SQL 中是否存在明显的语法错误,如 SQL 语句select * from;
会在Parser 阶段报错。但如果 SQL 语句select * from foo;
没有语法错误,StarRocks 中也没有 foo 这张表,那么 StarRocks 该如何做到错误处理呢?这就需要依赖下一节的 Analyzer 模块去判断了。
本期 StarRocks 源码解析到这就结束了,好学的你肯定学会了一些新东西,又产生了一些新困惑,不妨留言评论或者加入我们的社区一起交流(StarRocks 小助手微信号)。下一篇 StarRocks 源码解析,我们将为你带来 Analyzer 源码解析。
Antlr4官方指南
Antlr4官方示例:Grammars-v4
https://pragprog.com/titles/tpa
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。