Apache Calcite:SQL解析
flowchart LR
subgraph calcite
A[1.SQL] -->|Parser| B[2.SQL Node]
B -->|Validator| C[3.SQL Node]
C -->|Convertor| D[4.Rel Node]
D -->|Optimizer| E[5.Rel Node]
end
E --> F[6.Physical Excute Plan]Calcite 中的SQL Parser 使用JavaCC 实现。JavaCC 在前面的博客中有过介绍, 本文就不赘述了。本文主要介绍Parser。
Parser
Calcite SQL Parser 的核心实现在 calcite-core 模块,在 src/main 下包含了 codegen 目录:
1 | # calcite/core/src/main/codegen |
- Parser.jj 文件是 SQL Parser 相关的词法和语法规则文件,并且为了实现 SQL Parser 的扩展,在文件内使用了 Freemarker 模板引擎。
- config.fmpp 和 default_config.fmpp 基于 fmpp,用于定义 Freemarker 模板的属性。
- includes目录下的
compoundIdentifier.ftl和parserImpls.ftl是扩展文件, 里面可以添加自定义的SQL语法规则。
在calcite/core/build.grade.kts中定义了SQLParser的构建方法:
1 | val fmppMain by tasks.registering(org.apache.calcite.buildtools.fmpp.FmppTask::class) { |
- 先通过Fmpp配置和模版生成JavaCC 的 jj 文件。
- 再通过 JavaCC 生成 Parser(包
org.apache.calcite.sql.parser.impl)。
SQL Parser - 门面类
由于真正的Parser 是通过JavaCC 生成的动态代码。Calcite 实际使用的是 SQL Parser的门面类:org.apache.calcite.sql.parser.SqlParser。调用 SQLParser.create 可以快速创建解析对象,然后进行 SQL 解析。
1 | public void simpleParse(){ |
除了通用的 Create 方法外,SqlParser 还支持对不同场景下的 SQL 解析。
1 | // 解析 SQL 表达式 |
SqlParser.Config - Parser 配置
SqlParser创建时可以通过create(String sql, Config config)传入一个 Config 类来定制 SqlParser 的一些行为,例如:
- quoting: 转义identifier的字符(可以是双引号,单引号,反引号等)
- caseSensitive: 是否大小写敏感
- quotedCasing: 转义identifier的格式转换(可以是不变,大写,小写)
- unquotedCasing: 未转义identifier的格式转换(可以是不变,大写,小写)
SqlAbstractParserImpl - Parser 抽象层
SqlAbstractParserImpl有以下实现:
- SqlParserImpl: 解析标准 SQL 语句,Calcite默认语法规则1
- SqlDdlParserImpl: 解析数据定义语言 (DDL) 语句,Calcite/server模块中实现。
- SqlBabelParserImpl: 支持多种 SQL 方言兼容(如 Oracle, MySQL, Hive 等),Calcite/babel模块中实现。
SqlParser.create(sql, SqlParser.Config.DEFAULT.withParserFactory(SqlBabelParserImpl.FACTORY)) 可以获取不同的SqlAbstractParserImpl。
Parser 调用的示例代码如下:
1 | String sql = "select name from users"; |
SQL 扩展
在上面介绍了SqlParserImpl是如何通过 Fmpp 和 JavaCC 生成。同时在SqlAbstractParserImpl部分介绍了SqlBabelParserImpl是对标准 SQL 的一种扩展。接下来,我们来看下Calcite/babel模块是如何对标准 sql 进行扩展的。
1 | # calcite/babel/src/main/codegen |
config.fmpp 文件定义了 Parser.jj 模板中需要使用的参数,如果未配置则默认会使用 default_config.fmpp 中的参数。config.fmpp中的数据分为两部分:
1 | data:{ |
includes文件夹中是语法规则的扩展文件,在原始的 Parser.jj 文件中通过下面的 freemarker 标签引入,最终会将文件内容替换上面的标签。
1 | <#-- Add implementations of additional parser statement calls here --> |
上面的逻辑都在calcite/babel/build.gradle.kts构建脚本中实现。会指定 core 模块 templates 目录下的 Parser.jj 作为模板,扩展的语法定义会被整合到模板中,统一输出最终的 Parser.jj 文件。
1 | val fmppMain by tasks.registering(org.apache.calcite.buildtools.fmpp.FmppTask::class) { |
SQL Node
SQL通过SqlParser的处理,最终生成了 SQL Node。Calcite 并没有通过 JavaCC 的jjtree 方式自动生成语法节点类,而是在代码中预先定义SqlNode和其子类,然后在 Parser.jj 文件中使用。大多数产生式规则返回的都是SqlNode的子类。
1 | SqlNode SqlStmtEof() : |
bnf对应的 java 代码如下:
1 | public final SqlNode SqlStmtEof() throws ParseException { |
SqlNode 是 Calcite 中负责封装语义信息的基础类,除了在解析阶段使用外,它还在校验(validate)、转换 RelNode(convert)以及生成不同方言的 SQL(toSqlString)等阶段都发挥了重要作用。
classDiagram
class SqlNode{
+SqlParserPos pos
+getKind() SqlKind
+isA(Set<SqlKind> category) boolean
+unparse(SqlWriter writer,int leftPrec,int rightPrec)
+toSqlString(SqlDialect dialect) SqlString
+validate(SqlValidator validator,SqlValidatorScope scope)
+validateExpr(SqlValidator validator,SqlValidatorScope scope)
+accept(SqlVisitor~R~ visitor) R
}- SqlParserPos: 表示语法节点在原始文本中的位置。
- getKind(): 用于快速查找某些类型的语法节点,默认类型是 Other。
- isA(): 用于快速识别某一类的语法节点。
- unparse(): 用于从 SQLNode 反向生成 sql 文本
- toSqlString():内部使用了unparse,并适配了不同的 Sql 方言
- validate(): 用于验证 Sql 语法节点。
- validateExpr(): 用于验证在表达式上下文内的 Sql 语法节点。
- accept(): 访问者模式驱动方法。
在 SqlNode 下面有几个细分场景(见org.apache.calcite.sql.util.SqlVisitor):
- SqlCall: 代表了Sql 子句操作,实际上是对SqlOperator 的调用。
- 例如查询操作是 SqlSelectOperator,对应的 SqlNode 是 SqlSelect。
- Join 操作是SqlJoinOperator,对应的 SqlNode 是 SqlJoin。
- SqlIdentifier: 代表 SQL 中的标识符,例如 SQL 语句中的表名、字段名。
- SqlLiteral: 主要用于封装 SQL 中的常量,通常也叫做字面量。
- SqlNodeList: 代表了SqlNode列表,例如常见的例如 Select 中的字段列表。
- SqlDataTypeSpec: 代表的 Sql 的数据类型定义,例如
ROW(foo NUMBER(5, 2) NOT NULL - SqlDynamicParam: 代表了 Sql 中的动态参数,一般常见于 Sql 预编译时使用。例如 JDBC PreparedStatement中的
? - SqlIntervalQualifier: 代表了 Sql 中时间间隔定义,例如
INTERVAL 1 DAY(注意INTERVAL '1' DAY会被解析为SqlLiteral)。
下面我们再深入了解下不同子类的具体作用。
SqlCall
SqlCall代表了Sql 子句操作,实际上是对SqlOperator 的调用。以SqlSelect为例:
1 | public class SqlSelect extends SqlCall { |
SqlSelect中的属性描述了Select 子句的不同部分,例如对 sql select name from users 解析可以得到如下的SqlNode树:
flowchart TD
A[SqlSelect] -->|selectList| B["nodeList(name)"]
A -->|from|C["SqlIdentifier(USERS)"]
B -->|reference|D["SqlIdentifier(name)"]此外SqlSelect还实现了SqlCall中的其他方法,其中和 Operator 相关的有 2 个:
- 获取操作符: getOperator
- 获取操作数列表: getOperandList
SqlOperator
SqlOperator 用于表示 SqlCall 这类节点的类型,SqlOperator包括函数、“=”等操作符和“case”语句等语法结构。操作符可以表示查询级表达式(如SqlSelectOperator)或行级表达式(如org.apache.calcite.sql.fun.SqlBetweenOperator)。
SqlOperator中包含了操作符名称,元数据和校验。此外这里的操作符,操作数的定义和编程语言是类似的。因为操作符也会涉及优先级定义:
1 | public abstract class SqlOperator { |
leftPrec和rightPrec 需要在创建SqlOperator时指定。
1 | protected SqlOperator( |
1 | protected SqlOperator( |
- 对于二元操作符
SqlBinaryOperator,一般都是区分左右结合的。在SqlStdOperatorTable中有非常多的SqlBinaryOperator实例。 - 对于一元操作符(
SqlPostfixOperator或SqlPrefixOperator)通常来说是不区分左右结合的,leftAssoc都等于 true。 - 所有的函数
SqlFunction优先级都是 100。
Function
大部分的函数在 sql 解析时会被解析为 SqlUnresolvedFunction(继承 Operator)。也就是说一个不存在但符合函数语法的函数也可以通过 SqlParser的解析,并被解析为SqlUnresolvedFunction。
1 | // org.apache.calcite.sql.parser.SqlAbstractParserImpl |
SqlLiteral
SqlLiteral代表了 Sql 中的常量:
classDiagram
class SqlLiteral{
+SqlTypeName typeName
+Object value
+getValueAs(Class<T> clazz) T
}
note for SqlLiteral "Root Class of Sql Literal"
class SqlAbstractDateTimeLiteral
note for SqlAbstractDateTimeLiteral "A SQL literal representing a DATE, TIME or TIMESTAMP value."
<<Abstract>> SqlAbstractDateTimeLiteral
SqlLiteral <|-- SqlAbstractDateTimeLiteral
SqlAbstractDateTimeLiteral <|-- SqlDateLiteral
class SqlTimeLiteral
SqlAbstractDateTimeLiteral <|-- SqlTimeLiteral
SqlAbstractDateTimeLiteral <|-- SqlTimeTzLiteral
SqlAbstractDateTimeLiteral <|-- SqlTimestampLiteral
SqlAbstractDateTimeLiteral <|-- SqlTimestampTzLiteral
class SqlAbstractStringLiteral
note for SqlAbstractStringLiteral "base for character and binary string literals."
<<Abstract>> SqlAbstractStringLiteral
SqlLiteral<|-- SqlAbstractStringLiteral
SqlAbstractStringLiteral <|-- SqlBinaryStringLiteral
SqlAbstractStringLiteral <|-- SqlCharStringLiteral
SqlLiteral<|-- SqlIntervalLiteral
note for SqlIntervalLiteral "A SQL literal representing a time interval."
SqlLiteral<|-- SqlNumericLiteral
note for SqlNumericLiteral "A numeric SQL literal."
SqlLiteral <|-- SqlUnknownLiteral
note for SqlUnknownLiteral "Literal whose type is not yet known"
SqlLiteral <|-- SqlUuidLiteral
note for SqlUuidLiteral "A SQL literal representing an UUID value"SqlLiteral中的typeName用于区分常量类型。包含的类型如下:
| 类型名称 | 类型含义 | 值类型 |
|---|---|---|
| SqlTypeName.NULL | 空值。 | null |
| SqlTypeName.BOOLEAN | Boolean 类型,包含:TRUE,FALSE 或者 UNKNOWN。 | Boolean 类型,null 代表 UNKNOWN。 |
| SqlTypeName.DECIMAL | 精确数值,例如:0,-.5,12345。 | BigDecimal |
| SqlTypeName.DOUBLE | 近似数值,例如:6.023E-23。 | BigDecimal |
| SqlTypeName.DATE | 日期,例如:DATE ‘1969-04’29’。 | Calendar |
| SqlTypeName.TIME | 时间,例如:TIME ‘18:37:42.567’。 | Calendar |
| SqlTypeName.TIMESTAMP | 时间戳,例如:TIMESTAMP ‘1969-04-29 18:37:42.567’。 | Calendar |
| SqlTypeName.CHAR | 字符常量,例如:‘Hello, world!’。 | NlsString |
| SqlTypeName.BINARY | 二进制常量,例如:X’ABC’, X’7F’。 | BitString |
| SqlTypeName.SYMBOL | 符号是一种特殊类型,用于简化解析。 | An Enum |
| SqlTypeName.INTERVAL_YEAR … SqlTypeName.INTERVAL_SECOND | 时间间隔,例如:INTERVAL ‘1:34’ HOUR。 | SqlIntervalLiteral.IntervalValue. |
值得注意的是,SqlTypeName.SYMBOL 并没有SqlLiteral的对应子类,而是直接使用 Java 的 Enum 类型。
1 | /** |
SQL 方言
在 Calcite 中 SqlNode 还有一个强大的功能——SQL 生成。因为 Calcite 的目标是适配各种不同的存储引擎,提供统一的查询引擎,因此 Calcite 需要通过 SqlNode 语法树,生成不同存储引擎对应的 SQL 方言。
在 SqlNode 中提供了 toSqlString 方法,允许用户传入不同的数据库方言,将 SqlNode 语法树转换为对应方言的 SQL 字符串。
1 | String sql = "select name from users"; |
接下来关注下 toSqlString方法的实现。
1 | public SqlString toSqlString( SqlDialect dialect, boolean forceParens) |
在 toSqlString 重载方法内部,会初始化 SqlWriterConfig 参数,该参数用于控制 SQL 翻译过程中的换行、是否添加标识符引号等行为。参数初始化完成后,会将参数设置作为 Lambda 函数传递到另一个重载方法中。
1 | public SqlString toSqlString(UnaryOperator<SqlWriterConfig> transform) { |
在该重载方法内部,会创建 SqlPrettyWriter 作为 SQL 生成的容器,它会记录 SQL 生成过程中的 SQL 字符片段。从方法实现中可以发现,SQL 生成的核心逻辑是 unparse 方法,调用时会传入 writer 类。
unparse
unparse将 SqlNode 通过 SqlWriter转为 SQL。每个 SqlNode 子类都实现了 unparse 方法。
1 | public abstract void unparse( |
接下来我们先分析 SqlCall 的实现。
- SqlCall 先判断是否要在节点外面添加括号。判断策略是节点 Operator 的优先级和 Node 的左右优先级比较。
1 | public void unparse( |
- 然后再通过 SqlDialect 的unparseCall 方法处理 SqlCall 节点。如果节点类型是 Row会有一个特殊处理逻辑。最终会转交给 Operator 来处理 unparse。
1 | // SqlDialect.java |
- Operator 会根据自身的语法类型来unparse。
a+b的Operator是二元操作符,所以使用 BINARY 方式处理。
1 | public void unparse( |
- 最终的处理逻辑是在
SqlUtil.unparseBinarySyntax()中。
1 | public static void unparseBinarySyntax( |
优先级的作用
先来看个例子:
1 | String sql = "select (c1+c2)*c3 from users"; |
对应的节点是:
flowchart TD
A[SqlSelect] -->|select Items|B[selectList]
B-->|first item|C["SqlBasicCall(*)"]
C-->D["SqlBasicCall(+)"]
C-->E["SqlIdentifier(C3)"]
D-->F["SqlIdentifier(C1)"]
D-->G["SqlIdentifier(C2)"]
A-->|from|H["SqlIdentifier(USERS)"]如果按照中序遍历语法树的话,first item会得到 C1+C2*C3。此时会发现生成的表达式结果不对了。
简单一点的解法是当节点是 expression 时,默认添加括号。
1 | // SqlCall.java |
但是这样对于表达式 c1+c2+c3,生成的 sql 就变成(c1+c2)+c3,会在 sql 引入很多无效的括号。
Calcite 引入了 Operator 优先级来解决问题:
+或-优先级是 40*或/优先级是 60
下面是左右优先级和 operator 的优先级对比。c1+c2的 rightPrec符合needsParentheses中的条件,所以会添加括号。
1 | left:3->60┌───┐ 61<-2:right |
SqlWriter
SqlWriter从解析树构造SQL语句,并支持方言差异兼容。
classDiagram
class SqlWriter{
+ getDialect() SqlDialect
+ literal(String s)
+ keyword(String s)
+ identifier(String name, boolean quoted)
+ newlineAndIndent()
+ fetchOffset(SqlNode fetch,SqlNode offset)
+ topN(SqlNode fetch,SqlNode offset)
+ startFunCall(String funName) Frame
+ endFunCall(Frame frame)
+ startList(FrameType frameType, String open, String close) Frame
+ endList(Frame frame)
+ ...()
}
<<Interface>> SqlWriter
class SqlWriterConfig{
+ dialect() SqlDialect
+ keywordsLowerCase() Boolean
+ quoteAllIdentifiers() Boolean
+ indentation() int
+ clauseStartsLine() Boolean
+ clauseEndsLine() Boolean
+ selectListItemsOnSeparateLines() Boolean
+ lineFolding() LineFolding
+ selectFolding() LineFolding
+ fromFolding() LineFolding
+ whereFolding() LineFolding
+ groupByFolding() LineFolding
+ havingFolding() LineFolding
+ windowFolding() LineFolding
+ matchFolding() LineFolding
+ orderByFolding() LineFolding
+ overFolding() LineFolding
+ valuesFolding() LineFolding
+ updateSetFolding() LineFolding
+ selectListExtraIndentFlag() Boolean
+ windowDeclListNewline() Boolean
+ valuesListNewline() Boolean
+ updateSetListNewline() Boolean
+ windowNewline() Boolean
+ caseClausesOnNewLines() Boolean
+ whereListItemsOnSeparateLines() Boolean
+ leadingComma() Boolean
+ subQueryStyle() SubQueryStyle
+ alwaysUseParentheses() Boolean
+ lineLength() int
+ foldLength() int
}
<<Interface>> SqlWriterConfig
note for ImmutableSqlWriterConfig "generated by org.immutables.processor"
SqlWriterConfig <|.. ImmutableSqlWriterConfig
SqlWriter *-- SqlWriterConfig
SqlPrettyWriter *-- SqlWriterConfig
SqlWriter <| .. SqlPrettyWriter
class SqlPrettyWriter{
+ SqlDialect dialect
+ StringBuilder buf
+ SqlWriterConfig config
}SqlWriterConfig 中存放了SqlWriter生成 sql 过程中的配置。
xxxFolding()方法用于确定sql某部分对于过长行的处理策略。xxxNewLine()用于判断 sql 某部分是否在新的行中。dialect()用于获取当前使用的 sql 方言- 还有些其他方法用于判断大小写,缩进长度,转义,使用括号等参数。
SqlWriter提供了几类方法:
print(),literal(),keyword()和identifier()直接向 buf 中 print 对应文本,由于identifier有可能是转义的,所以会通过dialect写入buf,以实现方言兼容。startList()和endList()处理复杂SqlNode(startFunCall()和endFunCall()内部调用startList()和endList())。fetchOffset()和topN()对特殊 SqlNode 的处理。
在 Node to Sql的转换过程中,SqlWriter将 Sql的不同部分拆分为不同类型的Frame。
- 先通过 StartList 创建 Frame。
- 然后处理 SqlNode 中的属性和子节点。
- 最后通过 endList 结束 Frame。
例如 SqlIdentifier的 unparse 过程:
1 | // SqlIdentifier.java |