Apache Calcite SQL 验证
原文链接: Apache Calcite SQL验证, 查看本文最近更新及作者最新文章请点击原文链接.
本文已收录在专栏Apche Calcite原理与实践中.
本文是 Apache Calcite 原理与实践系列的第三篇, 上一篇文章介绍了 Calcite 解析器的实现原理, 本文将介绍如何对解析器输出的 SQL 解析树进行语义分析, 如表名, 字段名, 函数名和数据类型的检查. 相对于解析器, SQL 验证部分的内容扩展需求较少, 所以本文重点介绍 Calcite 中 Schema 相关的接口(用于提供元数据), 以及 SQL 验证相关的概念, 最后以SELECT
语句为例, 介绍验证过程中的重要步骤.
Calcite 元数据接口
SQL 解析阶段只要 SQL 语句中没有语法错误便可解析成功, 而不会关注 SQL 语句的具体语义是否正确, 比如表是否存在, 字段是否存在等. SQL 验证阶段就会检查 SQL 语句的语义是否正确, 这就需要依赖于外部提供的元数据信息. 为此, Calcite 提供了一系列用于提供元数据信息的接口. 这里我们将第一篇文章中与 SQL 验证相关的部分复制过来, 以便对照讲述.
从上述代码中可以看到, 有三个与元数据信息有关的重要接口: Schema
(SimpleSchema
继承自Schema
), CalciteSchema
和CatalogReader
. 下面我们将分别介绍.
Schema
是用于描述数据库结构(如包含哪些表, 函数等)的一个接口. 在关系型数据库管理系统中通常有 Catalog, Schema, Table 这样的层级结构来管理表和数据库, 不过并非每中 RDBMS 都完全这么实现, 比如 MySQL 不支持 Catalog, 并且用 Database 来替代 Schema 的位置. Calcite 的Schema
接口可以表示 Catalog 或 Schema, 因为Schema
接口支持嵌套, 这样就可以用来表示 Schema 嵌套在 Catalog 里这种层级结构. Schema
接口的实现类如下图所示.
这里介绍几个重要的实现类:
SchemaPlus
接口是Schema
接口的扩展, 增加了一些新的方法, 比如添加表.SchemaPlus
不应该由用户创建.AbstractSchema
是一个默认实现, 我们代码中的SimpleSchema
就继承自该类.DelegatingSchema
是一个简单的代理类, 将所有的操作交给内部的一个Schema
实现.
CalciteSchema
接口是Schema
接口的包装类, 可以嵌套多个Schema
实体, 并提供了一些工具方法, 比如plus()
方法可以将内部Schema
包装为SchemaPlus
后返回. 它有两个实现类, 在我们的代码中使用的是SimpleCalciteSchema
.
CatalogReader
用于读取表的元数据信息, 在绝大多数情况下只需使用其默认实现CalciteCatalogReader
即可, 如有特殊需求, 可继承CalciteCatalogReader
进行实现. 这里重点介绍一下CalciteCatalogReader
构造函数中需要的RelDataTypeFactory
. 它是一个数据类型描述符的工厂, 定义了用于实例化和组合 SQL, Java 和集合类型的方法. 在我们代码的SimpleTable.getRowType()
函数中会用到.
可以看到 Calcite 中用于提供元数据的接口还是有点绕的, 特别是 Schema 相关的接口. 不过好在在实践中我们通常只需要实现相应的Schema
接口, 用于读取特定数据源的元数据信息即可.
Calcite SQL 验证相关概念
在介绍 SQL 验证流程之前, 我们先介绍一下 Calcite 为实现 SQL 验证引入的一些概念.
在通用编程语言中都有作用域的概念, 只能使用当前作用域或父作用域内的函数或变量, 比如 C 语言的函数是一个作用域, 函数内部只能使用函数内定义的局部变量, 或定义在全局作用域内的全局变量或函数, 但是不能使用定义在其他函数内部的局部变量. SQL 语言中同样有作用域, 在 Calcite 中称为 Scope.
我们以上述 SQL 语句来说明 Calcite 中关于 Scope 的概念. 在查询的各个位置可用的作用域如下:
expr1
只能看见t1
,t2
和q3
, 也就是说expr1
只能使用t1
,t2
,q3
中存在的列名.expr2
只能看见t3
.expr3
只能看见t4
.expr4
只能看见t1
,t2
,q3
, 加上SELECT
子句中定义的任何别名.
在 Calcite 中 Scope 由SqlValidateScope
表示, 其类继承图如下.
SQL 语句需要从一个源中获取数据, Calcite 将数据源抽象为命名空间 Namespace. Namespace 是一个抽象概念, 它既可以表示一个表, 也可以是视图或子查询. 上述 SQL 语句中有 4 个 Namespace: t1
, t2
, (SELECT expr2 FROM t3) AS q3
和(SELECT expr3 FROM t4)
. Calcite 中使用SqlValidatorNamespace
表示 Namespace, 它的类继承图如下.
Calcite SQL 验证流程
在 SQL 语句中, DDL 语句是不需要验证的, DQL 和 DML 语句都需要验证. 由于各类语句的验证流程在细节上存在差别, 这里以最常见的SELECT
语句为例, 讲述其验证过程. 整个SELECT
语句验证流程中的函数调用链如下.
在验证流程中有三个主要的函数: SqlValidatorImpl.performUnconditionalRewrites()
, SqlValidatorImpl.registerQuery()
和SqlSelect.validate()
, 下文将对他们进行详细的说明.
SQL 重写
SQL 重写用于将解析阶段得到的解析树重写为统一的格式, 方便下一步的处理. 由于SqlValidatorImpl.performUnconditionalRewrites()
函数的实现十分复杂, 这里也就不详细分析重写的过程了. 而是通过两个具体的例子来说明重写的效果.
第一个例子是带有ORDER BY
的SELECT
语句, 在解析阶段这类语句会被解析为SqlOrderBy
节点, 不过SqlOrderBy
是一个纯语法节点, 在重写阶段会转化为SqlSelect
节点. 其实我们看一下这两个类的定义就能明白这种转化是如何进行的, 核心代码如下. 对于SELECT ... ORDER BY ...
语句, 在解析阶段生成的SqlOrderBy
节点中query
变量保存了SELECT ...
部分的解析结果, 它其实是一个SqlSelect
实例, ORDER BY
后面的字段列表保存在orderList
中. 在重写之后, SqlOrderBy
中的orderList
被移动到SqlSelect
的orderBy
中.
第二个例子是DELETE
语句, 在解析阶段这类语句会被解析为SqlDelete
节点, 其关键属性如以下代码所示. 在解析阶段生成的SqlDelete
中sourceSelect
为null
, 在重写阶段会生成sourceSelect
用于指示如何从表中查询需要删除的记录. 比如DELET FROM users WHERE id > 1
对应的sourceSelect
就是SELECT * FROM users WHERE id > 1
.
通过以上例子可以看到重写阶段所作的工作就是对解析树进行一些轻微的调整, 一般情况下这一阶段也不需要任何改动, 只要大概了解其流程即可. 其实如果在解析阶段就生成标准的格式, 就不需要重写了, 只不过这样会让解析器的代码变得冗长, 这应该也是 Calcite 把一些重写工作放到验证阶段的原因.
注册 Scope 和 Namespace
在真正进行验证之前, 还需要调用SqlValidatorImpl.registerQuery()
注册 Scope 和 Namespace. 在这一步 Calcite 会先遍历一遍整个 SQL 语句, 为解析树的各个部分生成对应的 Scope 和 Namespace. 解析的结果保存在SqlValidatorImpl
的成员变量中, 以下是与解析结果相关的核心代码.
SQL 语句验证
注册完 Scope 和 Namespace 之后就可以调用SqlNode.validate()
方法进行验证了, 这里 Calcite 也使用了 Visitor 模式. 如果读者不了解什么是 Visitor 模式, 可以参考笔者之前的博文. 简单来说, Visitor 模式就是把实现逻辑集中到一个类中, 在这里就是SqlValidatorImpl
. 我们可以看下SqlSelect.validate()
的实现, 它并没有实现具体的逻辑, 而是调用了传入的SqlValidator
的validateQuery
方法, 也就是SqlValidatorImpl.validateQuery
. 这样带来的一个好处是, 如果我们需要修改验证逻辑, 只需要对SqlValidatorImpl
进行修改, 而不需要修改SqlNode
的实现类.
SqlValidatorImpl.validateQuery
是SELECT
语句验证的真正入口, 它主要对 Scope 和 Namespace 进行了验证, 整个调用逻辑比较复杂, 存在很多递归调用. 如果读者有兴趣可以根据上述调用链逐步调试, 这里不在详细分析.
总结
本文梳理了 Calcite SQL 验证相关的概念和流程, 由于这部分的代码较为复杂, 存在大量的递归调用, 所以本文也没有详细的分析所有细节. 好在 SQL 验证这部分不太需要进行修改, 多数情况下只需要理解其整体流程即可. 如果要对扩展的 SQL 语法进行验证, 可以建立一个统一的基类, 让扩展的 SQL 语法节点都继承该类, 这样在验证阶段可以对这类节点单独进行验证, 并不一定要修改SqlValidatorImpl
类, 从而简化工作.
版权声明: 本文为 InfoQ 作者【不穿格子衬衫的程序员】的原创文章。
原文链接:【http://xie.infoq.cn/article/ccfaf91cf0708e600846e70bc】。文章转载请联系作者。
评论