导读:欢迎来到 StarRocks 源码解析系列文章,我们将为你全方位揭晓 StarRocks 背后的技术原理和实践细节,助你逐步了解这款明星开源数据库产品。本期将主要介绍 StarRocks Parser 源码。
作者:刘航源 StarRocks Committer
概述
Parser 的主要工作是将字符串类型的 SQL 语句文本,解析成树形结构的抽象语法树(Abstract Syntax Tree,AST),便于语义解析和查询计划的生成与优化。本文首先介绍 StarRocks 中实现语法解析的工具 Antlr4,并介绍其在 StarRocks 内部的实现,以及相关的 AST 生成逻辑。
基本概念
在学习 Parser 之前,我们需要先了解一下相关的基本概念。
- 抽象语法树 (Abstract Syntax Tree,AST) :是 SQL 代码的一种抽象表示,以树的形状来表示语言的语法结构。抽象语法树一般用来进行代码语法的检查、代码风格的检查、代码的格式化、代码的高亮、代码的错误提示,以及代码的自动补全等。
- 词法分析器 (Lexer) :词法分析是指在计算机科学中,将字符序列转换为单词(Token)的过程。执行词法分析的程序便称为词法分析器,一般是供语法解析器(Parser)调用的。
- 语法解析器 (Parser):通常作为编译器或解释器出现。它的作用是进行语法检查,并构建由输入单词组成的数据结构(即抽象语法树)。语法解析器通常使用词法分析器(Lexer)从输入字符流中分离出一个个的单词,并将单词流作为其输入。在实际开发中,语法解析器可以手工编写,也可以使用工具自动生成。
ANTLR4
除了理解概念外,对工具的了解也是必不可少的。
Antlr4(Another Tool for Language Recognition)是一款基于 Java 开发的开源的语法分析器生成工具,能够根据语法规则文件生成对应的语法分析器,不仅广泛应用于 DSL 构建、语言词法语法解析等领域,在当下非常多流行的框架中也都有使用。
Antlr 可以生成不同 target 的 AST,包括 Java、C++、JS、Python、C# 等,满足不同语言的开发需求。具体来说,Antlr4 的优势主要包括如下五个方面:
- 应用范围广:Antlr4 是目前主流的语法解析框架,目前市面上大多数计算引擎均将其作为解析工具(比如 Spark、Presto、Hive)。
- 学习成本低:大量的学习资料和文档,甚至有持续维护的 SQL 语法文件,这为我们后续的开发提供非常大的便利。
- 代码清晰易读:语法文件和代码逻辑分离,自动生成 Visitor 模式的代码。而且代码清晰易懂,不容易出错。
- 周边产品完善:Anltr 可以在 ideaIDE 中直接根据语法文件和 sql 语句生成树状的图形,不仅方便调试,也能让我们清晰地看到自己的 SQL 是否出错,语法文件是否正确,从而提升开发效率。
- 开放语法文件:可以开放 Antlr4 语法文件给第三方用户使用。因为没有代码逻辑,第三方可以方便地基于语法构建周边系统。
StarRocks 的 antlr 词法定义文件和语法定义文件,分别定义在 StarRocksLex.g4 和 StarRocks.g4 两个文件中。有了对 Antlr4 工具的认识,接下来我们就来看看怎么在 StarRocks 中基于 Antlr4 实现语法解析,并生成 AST。
SqlParser
在 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 来判断将要使用何种方式来解析。
ASTBuilder
在主流程中,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 源码解析。
References
Antlr4官方指南
Antlr4官方示例:Grammars-v4
https://pragprog.com/titles/tpa