上一篇文章咱们介绍了 Calcite SQL 解析的原理以及如何扩大 SQL 语法,本篇咱们将进入 SQL 执行的下一个阶段:元数据验证。
二、Calcite 元数据验证
SQL 胜利解析为形象语法树后,接下来须要对整条 SQL 的语义和元数据进行验证,判断 SQL 是否满足下一步计算的要求。
判断一句 SQL 是否正确至多蕴含以下几点:
- 查问的库表列是否存在。
- SQL 的语义是否正确。
- 验证操作符,判断应用的操作符或者函数是否存在,并被正确地应用,例如数据类型、参数个数等是否正确。
咱们能够用以下 SQL 为例来探索 Sql 验证的过程。
SELECT col1, avg(col2)
FROM t1,
(SELECT col3 FROM t2) AS s1
WHERE col4 > col5
GROUP BY col1;
在 Calcite 中 验证 SQL 是否非法应用 SqlValidator,它的初始化形式如下:
protected SqlValidatorImpl(
SqlOperatorTable opTab,
SqlValidatorCatalogReader catalogReader,
RelDataTypeFactory typeFactory,
Config config)
SqlOperatorTable 提供 SQL 验证所须要的所有操作符,函数、’>’、'<‘ 等都属于操作符;SqlValidatorCatalogReader 提供验证须要的元数据信息,包含 Catalog、Schema 等;RelDataTypeFactory 提供验证过程中字段类型的互相转换、例如函数返回值类型和精度信息等。Config 能够自定义一些配置,例如是否开启类型强制转换、是否开启 SQL 重写等等。
2.1 验证库表列是否存在
库和表的验证比拟好了解,因为大多数的 OLAP 引擎有多个数据源和数据空间,并且反对联邦查问,所以 SQL 中库表往往都是全名称模式存在(或者应用 use 语句指定),形如 catalog.schema.table(例如 hive.tpcds.store),这样一来,咱们就能确定惟一一张表的地位。此外,在 Calcite 验证器中有两个重要的形象,作用域(Scope)和命名空间(Namespace)。
- 作用域
Scope 用来示意一个字段或者一个表达式的作用域,简略了解就是验证时 晓得 select 的字段可能呈现在哪张表中,这样便能校验这个字段是否存在。以结尾的 SQL 语句为例,表达式的作用域如下:
col1, col2 可能呈现在 t1, s1
col3 只能呈现在 t2 中
col4, col5 可能呈现在 t1, s1
当解析验证一个表达式的时候,如果验证通过,就会将表达式包装成一个 Namespace 返回。在验证 SELECT * FROM hive.tpcds.store 表达式时,会解析和重组 hive.tpcds.store 的每级 schema,通过 CatalogReader 的 getTable() 办法获取元数据信息,判断表和字段是否存在。第一步,Calcite 会验证 RootSchema,也就是 Catalog 是否存在,而后顺次向上拼接 schema,验证 hive.tpcds 是否存在,最初验证 hive.tpcds.store。如果全副验证通过返回一个 SqlValidatorNamespace。
- 命名空间
Namespace 通常用来示意一个表、视图或者子查问,Namespace 继承关系如下:
Calcite 会将 from 前面的表、视图或者子查问封装成 Namespace,除此以外在继承关系中也能够看到 JoinNamespace, SchemaNamespace 这样更细分的 Namespace,Calcite 会将这些关系也封装成 Namespace。
以结尾的 SQL 语句为例,会将他们封装成 5 个 Namespace 存在 map 中,别离是:
electNamespace: SELECT col1, avg(col2) FROM t1, (SELECT col3 FROM t2) AS s1 WHERE col4 > col5 GROUP BY col1
SelectNamespace: SELECT col3 FROM t2
IdentifierNamespace: t1
IdentifierNamespace: t2
JoinNamespace: t1 join s1
在最终的验证阶段咱们调用 Namespace 的 validateImpl() 办法 进行验证。整个验证过程的关键步骤时序图如下:
2.2 SQL 语义的验证
SQL 语义验证包含查问的字段名 是否存在歧义、和聚合语句是否正确 等等。
在查看查问的字段名是否存在歧义时,会将这些字段名进行补全,拼接其库名和表名,来验证这些语句 是否存在二义性,如果 col1 即在 t1 中存在也在 t2 中存在,咱们查问时就必须在语句中指定 col1 的库表,否则语句是存在歧义的。Calcite 会在 org.apache.calcite.sql.validate.SqlValidatorScope#fullyQualify 办法中进行字段名称补全。
Calcite 还会查看语法的正确性,例如与聚合函数一起查问的字段是否蕴含在 group by 中,比方 SQL 语句:select c1, avg(c2) from t1; 那么 c1 必须在 group by 中。
这些验证逻辑就隐含在每个 Scope 对象中,上述例子就会在 AggregatingSelectScope 中做查看,所以 Scope 对象是 形容了字段的作用域信息。
2.3 验证操作符
Calcite 中操作符既蕴含 ”<“, “>” 这些操作符,也蕴含函数,在初始化 SqlValidator 的时候会传入 SqlOperatorTable,这个类的作用就是提供操作符列表和按名称查找操作符的能力。
Calcite 也提供了默认实现:SqlStdOperatorTable.instance(),继承 SqlOperatorTable 接口,其中两个外围办法 lookupOperatorOverloads(): 检索具备给定名称和语法的操作符列表,例如传入 ”sum” 和 ”SqlSyntax.Function”,则返回名称为 ”sum” 的函数列表。getOperatorList():检索所有函数和运算符的列表。
在 Calcite 验证函数时,就会调用 lookupOperatorOverloads() 办法获取函数的实现。
- 扩大函数
那咱们如何新增一个 Calcite 中没有的函数呢?
首先咱们能够继承 SqlOperatorTable 接口,实现本人的 lookupOperatorOverloads() 办法,其次,在初始化 SqlValidator 的时候传入咱们本人的 SqlOperatorTable 即可,上面通过 新增一个 Presto 中的函数 ”SUBSTR” 来演示 如何在 Calcite 中新增函数。以下为继承 SqlOperatorTable,实现 lookupOperatorOverloads() 的办法:
public class DemoOperatorTable
implements SqlOperatorTable
{
//Calcite 默认的操作符表
private static final SqlOperatorTable stdOperatorTable = SqlStdOperatorTable.instance();
// 用于存储自定义的函数名和实现的映射
private final ListMultimap<String, SqlOperator> opMap = ArrayListMultimap.create();
// 所有的函数列表
private final List<SqlOperator> operators = new ArrayList<>();
@Override
public void lookupOperatorOverloads(SqlIdentifier opName, @Nullable SqlFunctionCategory sqlFunctionCategory, SqlSyntax syntax, List<SqlOperator> operatorList, SqlNameMatcher nameMatcher)
{
// 首先查找 Calcite 原生的函数列表
stdOperatorTable.lookupOperatorOverloads(opName, sqlFunctionCategory, syntax, operatorList, SqlNameMatchers.withCaseSensitive(false));
// 如果 Calcite 中没有找到,则在咱们本人的函数列表中查找
if (operatorList.isEmpty() && syntax == SqlSyntax.FUNCTION && opName.isSimple()) {List<SqlOperator> ops = opMap.get(opName.getSimple().toUpperCase(Locale.US));
if (ops != null) {operatorList.addAll(ops);
}
}
}
@Override
public List<SqlOperator> getOperatorList()
{return operators;}
}
第二步向 ListMultimap<String, SqlOperator> opMap 中注册函数。新建 DemoSqlOperatorImpl 类,继承 org.apache.calcite.sql.SqlFunction,作为自定义函数的实现类。
public class DemoSqlOperatorImpl
extends SqlFunction
{
private final boolean isDeterministic;
private final boolean isDynamic;
private final SqlSyntax syntax;
public DemoSqlOperatorImpl(String name,
boolean isDeterministic,
boolean isDynamic,
SqlReturnTypeInference sqlReturnTypeInference,
SqlSyntax syntax,
SqlOperandTypeChecker operandTypeChecker)
{
super(name,
new SqlIdentifier(name, SqlParserPos.ZERO),
SqlKind.OTHER_FUNCTION,
sqlReturnTypeInference,
null,
operandTypeChecker,
SqlFunctionCategory.USER_DEFINED_FUNCTION);
this.isDeterministic = isDeterministic;
this.isDynamic = isDynamic;
this.syntax = syntax;
}
@Override
public SqlSyntax getSyntax()
{return syntax;}
@Override
public boolean isDeterministic()
{return isDeterministic;}
@Override
public boolean isDynamicFunction()
{return isDynamic;}
}
初始化 函数名、函数入参和返回值 等根本信息,new 出 org.apache.calcite.sql.SqlFunction,增加到 ”opMap” 中。
public void registerFunction()
{
String functionName = "SUBSTR";
boolean isDeterministic = true;
boolean isDynamic = false;
// 生成函数的入参
//Presto SUBSTR 函数有多种实现,这里只注册了 substr(string, start) → varchar 这种实现
List<SqlTypeFamily> argumentTypes = ImmutableList.of(SqlTypeFamily.CHARACTER, SqlTypeFamily.NUMERIC);
// 用于函数入参匹配和查看
SqlOperandTypeChecker checker = OperandTypes.family(argumentTypes);、// 函数返回值
SqlReturnTypeInference returnTypeInference = ReturnTypes.CHAR;
org.apache.calcite.sql.SqlFunction sqlFunction = new DemoSqlOperatorImpl(functionName,
isDeterministic,
isDynamic,
returnTypeInference,
SqlSyntax.FUNCTION,
checker);
opMap.put(functionName, sqlFunction);
operators.add(sqlFunction);
}
至此验证阶段的函数扩大就实现了,能够通过简略的验证:
String sql = "select substr('abcdefg', 2)";
SqlParser parser = SqlParser.create(sql, SqlParser.config());
SqlNode sqlNode = parser.parseQuery();
SqlValidator sqlValidator = SqlValidatorUtil.newValidator(demoOperatorTable, catalogReader, javaTypeFactoryImpl, SqlValidator.Config.DEFAULT);
sqlValidator.validate(sqlNode);
整个函数注册和校验流程大抵如下图:
细节局部能够跟踪 Calcite 源码:org.apache.calcite.sql.validate.SqlValidator#validate。
下面例子只是非常简单的函数验证过程的扩大,在 理论的生产环境中还会面临以下问题:
- 自定义的函数如何执行?如果是个常量如何进行常量折叠?
- 如果函数的入参类型 Calcite 不反对,如何在 Calcite 中自定义数据类型?
这些问题解决起来会更加简单,并且还会对 Calcite 外围的关系代数 改写阶段产生肯定影响。
总结
Calcite 是一个宏大且简单的数据管理框架,SqlParser 和 SqlValidator 只是其中一小部分,并不是“重头戏”,Calcite 在此着墨不多,留下了许多扩大的路径。上文通过两个 demo 简略介绍了 SqlParser 和 SqlValidator 的原理与扩大。这部分 绝大状况下只在 Calcite 的根底上进行扩大即可,不须要批改源码,对 Calcite 自身的影响较小。
后续咱们还会介绍更多 Calcite 原理技术与实战,钱能够带来高兴,玩技术也能够!如果你对Calcite 原理技术、数据虚拟化、湖仓平台、数据库、SQL 优化器等相干技术感兴趣的话,欢送关注“Aloudata 技术团队”公众号。
✎ 本文作者 / 淳译,Aloudata OLAP 引擎开发工程师,参加 Aloudata AIR Engine 的多个外围模块开发,目前负责 Aloudata 数据虚拟化引擎的 SQL 层、元数据和多源异构引擎集成等相干工作。