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 Validator。Calcite 通过 SQL 校验器实现 SQL 绑定,SQL 校验器所需的 Catalog 信息,在深入理解 Apache Calcite Catalog 中已经做了详细的介绍。
SQL 绑定主要的作用是将 SQL 解析生成的 AST 和数据库的元数据进行绑定,从而生成具有语义的 AST。SQL 绑定会通过自底向上的方式遍历 AST,对抽象语法树中的节点进行绑定分析,绑定的过程中会将表、列等元数据附在语法树上,最后生成具有语义的语法树 Bounded AST。
Validator
SQL 校验器的核心类为 SqlValidator,它负责使用 Calcite 元数据信息对 AST 进行验证,最终生成具有语义信息的 AST。除了 SqlValidator 校验器类之外,Calcite 为了将 SQL 中的名称解析为对象,还在校验器内部构建了两个对象:SqlValidatorScope 和 SqlValidatorNamespace。
- SqlValidatorScope 表示名称解析的范围,代表了在查询中的某一个位置,当前可见的字段名和表名。
- SqlValidatorNamespace 则表示了校验过程中查询语句的数据来源,不同的查询位置都有不同类型的 namespace 类,例如:表名对应的 IdentifierNamespace,Select 语句对应的 SelectNamespace,以及 UNION、EXCEPT、INTERSECT 对应的 SetopNamespace。
SqlValidator
classDiagram
class SqlValidator {
<<Interface>>
+ getCatalogReader() SqlValidatorCatalogReader
}
class SqlValidatorCatalogReader{
<<Interface>>
+ getTable(List~String~ names) SqlValidatorTable
+ getNamedType(SqlIdentifier typeName) RelDataType
+ getRootSchema() CalciteSchema
+ getConfig() CalciteConnectionConfig
+ nameMatcher() SqlNameMatcher
+ createTypeFromProjection(RelDataType type,List~String~ columnNameList) RelDataType
+ getAllSchemaObjectNames(List~String~ names) List~SqlMoniker~
}
class SqlOperatorTable{
<<Interface>>
+ lookupOperatorOverloads(SqlIdentifier opName,SqlFunctionCategory category,SqlSyntax syntax,List<SqlOperator> operatorList,SqlNameMatcher nameMatcher)
+ getOperatorList() List~SqlOperator~
+ ... ...()
}
class RelDataTypeFactory{
<<Interface>>
+ getTypeSystem() RelDataTypeSystem
+ createJavaType(Class clazz) RelDataType
+ createJoinType(RelDataType... types) RelDataType
+ createStructType(StructKind kind, List~RelDataType~ typeList,List~String~ fieldNameList) RelDataType
+ createStructType(List~RelDataType~ typeList,List~String~ fieldNameList) RelDataType
+ createArrayType(RelDataType elementType,long maxCardinality) RelDataType
+ createMapType(RelDataType keyType, RelDataType valueType) RelDataType
+ ... ... ()
}
class SqlValidatorImpl{
- SqlValidatorCatalogReader catalogReader
- SqlOperatorTable opTab
- RelDataTypeFactory typeFactory
- SqlValidator.Config config
- TypeCoercion typeCoercion
}
class SqlValidator.Config{
<<Interface>>
}
SqlValidatorImpl --|> SqlValidator
SqlValidatorImpl o-- SqlValidator.Config
SqlValidatorImpl o-- RelDataTypeFactory
SqlValidatorImpl o-- SqlValidatorCatalogReader
SqlValidatorImpl o-- SqlOperatorTable
class Prepare.CatalogReader{
<<Interface>>
}
class CalciteCatalogReader{
# CalciteSchema rootSchema
# RelDataTypeFactory typeFactory
# CalciteConnectionConfig config
}
Prepare.CatalogReader --|> SqlValidatorCatalogReader
Prepare.CatalogReader --|> SqlOperatorTable
CalciteCatalogReader --|> Prepare.CatalogReader
class ChainedSqlOperatorTable{
# List~SqlOperatorTable~ tableList
}
ChainedSqlOperatorTable --|> SqlOperatorTable
class SqlStdOperatorTable
SqlStdOperatorTable --|> SqlOperatorTable
class RelDataTypeFactoryImpl{
<<Abstract>>
}
class SqlTypeFactoryImpl
class JavaTypeFactoryImpl
class JavaTypeFactory{
<<Interface>>
}
JavaTypeFactory --|> RelDataTypeFactory
RelDataTypeFactoryImpl --|> RelDataTypeFactory
SqlTypeFactoryImpl --|> RelDataTypeFactoryImpl
JavaTypeFactoryImpl --|> SqlTypeFactoryImpl
JavaTypeFactoryImpl --|> JavaTypeFactorySqlValidatorImpl 是 SqlValidator的默认实现类。SqlValidatorImpl初始化方法SqlValidatorImpl(SqlOperatorTable opTab,SqlValidatorCatalogReader catalogReader,RelDataTypeFactory typeFactory,Config config)依赖了 4 个组件:
- SqlOperatorTable: 用来定义查找SQL算子(SqlOperator)和函数的接口。
- SqlValidatorCatalogReader: 用来给SqlValidator提供目录(Catalog)信息,也就是获得表、类型和Schema信息,它的实现类为CalciteCatalogReader。
- RelDataTypeFactory: 是处理数据类型的工厂类,它负责SQL类型、Java类型和集合类型的创建和转化。针对不同的接口形式,Calcite支持SQL和Java两种实现(SqlTypeFactoryImpl和JavaTypeFactoryImpl)。
- SqlValidator.Config: SqlValidator的配置信息,比如是否允许类型隐式转换、是否展开选择的列,等等。
此外, SqlValidatorImpl在初始化时,还会创建 TypeCoercion 类型转换类,它主要用于 SQL 中可能存在的隐式类型转换[1]。
SqlValidatorScope
在通用编程语言中都有作用域的概念, 只能使用当前作用域或父作用域内的函数或变量, 例如函数是一个作用域, 函数内部只能使用函数内定义的局部变量, 或定义在全局作用域内的全局变量或函数, 但是不能使用定义在其他函数内部的局部变量. SQL语言中同样有作用域, 在Calcite中称为Scope。
Calcite内的作用域(SqlValidatorScope)指SQL每个子查询的解析范围,可以是解析树中的任何位置或者任何含有列的节点。Calcite 根据不同的 SQL 类型实现了众多 SqlValidatorScope 子类,以满足不同场景下的 SQL 校验需求。下图展示了SqlValidatorScope的常见子类:
classDiagram
class SqlValidatorScope{
<<Interface>>
}
class AggregatingScope{
<<Interface>>
}
AggregatingScope --|> SqlValidatorScope
class DelegatingScope{
<<Abstract>>
}
DelegatingScope ..|> SqlValidatorScope
class AggregatingSelectScope
AggregatingSelectScope --|> DelegatingScope
AggregatingSelectScope ..|> AggregatingScope
class CatalogScope
CatalogScope --|> DelegatingScope
class ListScope{
<<Abstract>>
}
ListScope --|> DelegatingScope
class CollectScope
CollectScope --|> ListScope
class EmptyScope
EmptyScope --|> SqlValidatorScope
class GroupByScope
GroupByScope --|> DelegatingScope
class JoinScope
JoinScope --|> ListScope
class OrderByScope
OrderByScope --|> DelegatingScope
class OverScope
OverScope --|> ListScope
class ParameterScope
ParameterScope --|> EmptyScope
class PivotScope
PivotScope --|> ListScope
class SelectScope
SelectScope --|> ListScope
class TableScope
TableScope --|> ListScope
class UnpivotScope
UnpivotScope --|> ListScope
class WithScope
class WithRecursiveScope
WithScope --|> ListScope
WithRecursiveScope --|> ListScopeSelectScope 表示查询语句的名称解析范围,该范围中可见的对象包含了 FROM 子句的对象以及从父节点继承的对象。如下展示了一个常见的查询语句,该语句中包含了关联查询、子查询以及排序。
1 | SELECT expr1 |
Calcite 会将该语句拆分为 4 个 SelectScope 分别表示不同表达式对象的可见范围。
- expr1 可以访问 t1, t2, q3 中的对象;也就是说expr1只能使用t1, t2, q3中存在的列名.
- expr2 可以访问 t3 中的对象;
- expr3 可以访问 t4 中的对象
- expr4 可以访问 t1, t2, q3, 以及在 SELECT 子句中定义的任何列别名。
SqlValidatorNamespace
SqlValidatorNamespace 代表查询中的一个命名空间(Namespace),用于管理该命名空间内的可见列和类型信息。例如:在查询SELECT emp.deptno, age FROM emp, dept时,FROM 子句形成了一个包含 emp 和 dept 两张表,以及这些表中列组成的行类型在内的命名空间。不同的 RelNode 类型有与之对应的 Namespace 对象,下图展示了 Calcite 中定义的常见 SqlValidatorNamespace 实现类。
classDiagram
class SqlValidatorNamespace{
<<Interface>>
}
class DelegatingNamespace{
<<Abstract>>
}
DelegatingNamespace --|> SqlValidatorNamespace
class AbstractNamespace{
<<Abstract>>
}
AbstractNamespace --|> SqlValidatorNamespace
class AliasNamespace
AliasNamespace --|> AbstractNamespace
class CollectNamespace
CollectNamespace --|> AbstractNamespace
class IdentifierNamespace
IdentifierNamespace --|> AbstractNamespace
class FieldNamespace
FieldNamespace --|> AbstractNamespace
class JoinNamespace
JoinNamespace --|> AbstractNamespace
class PivotNamespace
PivotNamespace --|> AbstractNamespace
class TableNamespace
TableNamespace --|> AbstractNamespace
class SelectNamespace
SelectNamespace --|> AbstractNamespace
class WithNamespace
WithNamespace --|> AbstractNamespaceSelectNamespace 表示了查询语句对应的命名空间,我们同样以如下的查询语句为例:
1 | SELECT expr1 |
Calcite 会从查询语句中提取出 4 个命名空间,分别如下所示,命名空间代表的关系,简单理解可以认为是查询过程中的数据来源。
- t1:t1 表所代表的关系;
- t2:t2 表所代表的关系;
- (SELECT expr2 FROM t3) AS q3:子查询所代表的关系;
- (SELECT expr3 FROM t4):子查询所代表的关系。
校验示例
SqlValidator的构造和使用过程见validator示例。
对于 sql select * from mysql.customer limit 10,在 Validate 前 SqlNode 结构如下。
1 | #SqlCall: type=SqlOrderBy |
Validate 后 SqlNode 结构发生了变化:
1 | #SqlCall: type=SqlOrderBy |
可以看到原先的*已经被替换为customer表中的所有字段。此外,SqlValidator 也完成了涉及表的校验,例如表是否存在,字段是否正确,字段类型是否合法等等。
Validate 流程
接下来我们深入源码了解下 Validate 的流程。
入口方法
Validate的入口是org.apache.calcite.sql.validate.SqlValidatorImpl#validate()方法
1 | public SqlNode validate(SqlNode topNode) { |
- 首先会创建用于 root 节点校验的 EmptyScope,并将 EmptyScope 作为 CatalogScope 的父类,CatalogScope 可以查看所有 Schema 中的元数据,在校验过程中能够帮助对
schema.table.column进行列解析。 - 然后会调用 validateScopedExpression 进行校验,这部分是 SQL 校验器的核心逻辑。内部依次调用了 performUnconditionalRewrites、registerQuery 和 validate 方法,我们将对这些方法内部实现细节进行深入探究。
1 | private SqlNode validateScopedExpression(SqlNode topNode, SqlValidatorScope scope) { |
performUnconditionalRewrites
performUnconditionalRewrites 主要用于 SqlNode 重写标准化,从而方便后续的逻辑计划优化
- performUnconditionalRewrites 首先会对 SqlNodeList和SqlCall类型的节点,递归处理其内部的Operand.
- 特别是如果当前节点是 SqlUnresolvedFunction类型,会尝试从 SqlOperatorTable 中查找函数,并重新设置运算符。注意,此处是一个提前尝试,没有找到对应函数的SqlUnresolvedFunction会在后面对函数校验时,进行最终的转换(包含了类型兼容等问题的处理)。
- 然后根据节点自身类型对自身作一些变换。performUnconditionalRewrites 主要会将如下的非标准 SqlNode 转换为标准的 SqlNode,具体包括:VALUES、ORDER_BY、EXPLICIT_TABLE、DELETE、UPDATE 和 MERGE。VALUES 改写由于在类似
FROM (VALUES(...)) [ AS alias ]场景中存在问题,目前已经不进行改写。- SqlOrderBy会将排序和 limit 下推到内部的查询(SqlSelect 或 SqlWith)。
- EXPLICIT_TABLE 则会将 将 Table t 子句改写为 SELECT * FROM t 语句。
- DELETE 和 UPDATE 会根据条件改写生成 SqlSelect,代表了删除和更新语句所需要删除和更新的数据。
- MERGE 改写会将 SqlMerge 改写为 SqlUpdate 或 SqlInsert,以复用 SqlUpdate 和 SqlInsert 的处理逻辑。
registerQuery
在对 SqlNode 重写后,Calcite 会调用 registerQuery 方法生成 SqlValidatorScope 和 SqlValidatorNamespace 对象。
在调用 registerQuery 方法前,Calcite 会判断当前的 SqlNode 是否为顶层节点,SqlKind 类中定义的顶层节点包含了 QUERY、DML 和 DDL 3类。
1 | public static final EnumSet<SqlKind> TOP_LEVEL = concat(QUERY, DML, DDL); |
判断 SqlNode 为顶层节点后, 会继续调用 registerQuery 方法并传入参数。
1 | /** |
- parentScope 表示当前 SqlNode 的父名称解析范围,顶层节点的 parentScope 为 CatalogScope(CatalogScope 可以查看所有 Schema 中的元数据,它的父类是 EmptyScope)
- usingScope 将会添加当前 Scope 为需要使用的子Scope。
- node 表示当前的 SqlNode
- enclosingNode 和 node 通常是相同的,enclosingNode 通常表示 FROM 子句最顶层的节点 ,可以从 enclosingNode 中获取别名等信息
- alias 表当前查询在父查询中的名称,如果 usingScope 不为空则必须指定 alias。
registerQuery 方法内部会根据 SqlKind 进行判断,对不同类型的 SQL 进行处理,接下来以SELECT 语句为例展开介绍:
1 | case SELECT: |
然后 会对Select 的各个不同的子句进行注册:
- 首先会从 WHERE 语句开始,并将 WHERE 对应的 Scope 存储到 clauseScopes
- 再调用 registerOperandSubQueries 注册 WHERE 运算符中的子查询
- 最终整个方法会将 SELECT 语句中的 Projection、From、Where、Order By、Group By、Having 等子句都进行注册,生成对应的 Scope 和 Namespace 对象。
在对应的子句的registerXXX方法中会,递归处理其内部节点。
registerQuery 执行完成后,注册的信息会存储在 SqlValidatorImpl 对象中,分别包括了 scopes、clauseScopes 和 namespaces。
- scopes 对象用于存储查询节点和它们对应的 SqlValidatorScope 之间的映射- clauseScopes 对象则用于存储子句和 SqlValidatorScope 之间的映射,用于表示当前子句可见的范围。
- namespaces 代表了数据来源,例如表数据源,子查询数据源。
TestCase中的
testQuery03()就展示了sql中scopes和namespaces之间的关系。
validate
完成 registerQuery 后,Validator中已经包含了校验所需的Scope 和 Namespace。接下来就是基于前面准备的信息,对查询语句进行校验。Validator首先会调用 SqlNode#validate方法,该方法在不同的 SqlNode 中具有不同的实现。接下来以 SqlSelect 为例展开介绍。
1 | // SqlSelect#validate 实现 |
SqlSelect#validate 方法会调用校验器的 validateQuery 方法,validateQuery 中核心的处理逻辑为 validateNamespace 和 validateAccess,分别校验 Namespace 以及对表的访问。
1 |
|
validateNamespace
validateNamespace 方法内部会调用 SqlValidatorNamespace#validate 方法,然后设置校验后的节点类型。
1 | protected void validateNamespace(final SqlValidatorNamespace namespace, |
SqlSelect对应的 Namespace 是 SelectNamespace。SelectNamespace的 validate方法继承自父类。实际上validate中会调用子类的validateImpl方法。
1 | // org.apache.calcite.sql.validate.AbstractNamespace |
SelectNamespace#validateImpl 内部则会调用 validator.validateSelect 方法,SqlValidatorImpl#validateSelect 方法内会依次调用:
validateFrom: 校验数据源validateWhereClause: 校验过滤条件validateGroupClause: 校验分组validateHavingClause: 校验Having条件validateWindowClause: 校验窗口函数validateQualifyClause: 校验窗口函数计算后过滤条件validateSelectList: 校验查询列validateOrderList: 校验排序
1 | protected void validateSelect(SqlSelect select, RelDataType targetRowType) { |
validateFrom
validateFrom 方法会根据不同的数据源类型来分发到不同的方法。
1 | protected void validateFrom( |
当 From 的节点是 SqlTableRef时,会转为对内部的SqlIdentifier(表名)进行验证。对应的 Namespace 为 IdentifierNamespace,执行 validateFrom 时内部会调用 IdentifierNamespace#validateImpl 方法,该方法内部实现逻辑如下。
1 |
|
resolveImpl 方法会根据 id 从元数据中解析出表名,并封装为 TableNamespace 对象。对象中包含了从元数据中获取的 table 对象。然后会判断是否需要将 identifier 展开为全限定名称。
validateSelectList
再来看下 Select 投影列校验逻辑,首先会遍历 SELECT 投影列.
- 如果投影列仍然是一个子查询,则会调用 handleScalarSubQuery 处理标量子查询,判断是否符合标量子查询的规范。
- 否则会调用 expandSelectItem 方法展开投影列,将 column 展开为 tbl.column, expandSelectItem 方法也可以将星号展开为全部列。展开后的投影列 newSelectList 会分别设置到 select 节点和 selectScope 中,方便后续校验继续使用。
1 | protected RelDataType validateSelectList(final SqlNodeList selectItems, SqlSelect select, RelDataType targetRowType) { |
validateAccess
validateAccess 方法用于校验当前节点对表是否具有访问权限,node 参数表示当前节点,table 表示要访问的表,requiredAccess 则表示需要访问的类型。table.getAllowedAccess() 会从 RelOptTableImpl#getAllowedAccess 获取可访问类型,会返回全部类型,包括:SELECT、UPDATE、INSERT 和 DELETE,如果不具有访问权限,则抛出异常。
1 | /** |
总结
完成校验后,返回的 SqlNode中:
- Identifier 都进行了全限定名展开,可以很快速地获取到列对象所属的表,以及表对象所属的 Schema。
- 函数对象则会查找到 Calcite 内置的函数,或者用户在元数据中定义的函数。
这些校验后的对象将在后续 SqlNode 转换 RelNode 过程中发挥重要作用。
deriveType 类型推断部分将会在独立的文章中介绍。
类型兼容部分后面会单独介绍 ↩︎