我们在之前的文章中,介绍了 Doris 官方提供的两种方言转换工具,分别是 sql convertor 和方言 plugin。StarRocks 目前同样也提供了类似的方言转换功能。本文我们就一起来看一下这个功能的实现与 Doris 相比有何不同。
一、Trino 方言验证
我们可以通过如下 SQL 来验证 Trino 的方言转换在 SR 中的效果:
set enable_profile = true;
set sql_dialect = starrocks;
select BOROUGH, approx_count_distinct(ZIP_CODE) cnt from crashdata group by BOROUGH order by cnt desc;
set sql_dialect = trino;
select BOROUGH, approx_distinct("ZIP_CODE") cnt from crashdata group by BOROUGH order by cnt desc;
针对上述查询,我们在 SR 集群执行,结果如下所示:
可以看到,执行结果完全一致,说明方言转换已经生效,并且符合预期。通过 WebUI 查看提交的两条 SQL:
页面上显示的仍然是原始的 SQL 而不是改写之后的。如果在 SR 的方言下,执行 SQL2 的话,那么会直接报错,如下所示:
提示函数找不到,这也是符合预期的,因为 approx_distinct 这个函数在 SR 中是不存在的。下面我们就结合代码来看一下 SR 是如何实现这个方言转换功能的。
二、Trino AST 简介
由于 Trino 的方言支持,主要思路是将 Trino 的相关结构转换成 SR 的结构,因此这里先简单了解下 Trino 的 AST 相关结构:
后续提到的相关结构,就可以直接参考上述图片。
三、Trino Transformer 介绍
SR 在 FE 中实现了一套 transformer 可以将 Trino 的 function 转换为 SR 的 function,从而实现了方言转换的功能。
3.1 整体流程图
整个 parse 的相关流程如下所示:
上述流程可以分为如下几个步骤:
- FE 启动的时候,Trino2SRFunctionCallTransformer 会通过静态方法将所有 Trino 到 SR 的函数映射注册到 TRANSFORMER_MAP 这个 map 中;
- FE 调用 Trino 的 SqlParser 将 sql string 转换为 Trino 的 Statement 结构,可以参考上述的 AST 结构图;
- 在 trino/AstBuilder 中,将 Trino 的 Statement 转换为 SR 的 StatementBase,transformer 用于进行函数转换;
- 根据 sql 中的 Trino 函数名,在 Map 中进行匹配,找到对应的 FunctionCallTransformer;
- 在 FunctionCallTransformer 中构造 FunctionCallRewriter,最终返回至 trino/AstBuilder,完成函数的转换;
- 继续后续的其他操作,最终生成 SR 的 StatementBase结构,完成 parse 操作。
这里我们来一一看下对应的操作。
3.2 函数映射注册
Trino 到 SR 的函数映射注册代码位于 Trino2SRFunctionCallTransformer 中,这个类在加载的时候,会完成对应的函数映射注册,如下所示:
private static void registerAllFunctionTransformer() {
registerAggregateFunctionTransformer();
registerArrayFunctionTransformer();
registerDateFunctionTransformer();
registerStringFunctionTransformer();
registerRegexpFunctionTransformer();
registerJsonFunctionTransformer();
registerURLFunctionTransformer();
registerBitwiseFunctionTransformer();
registerUnicodeFunctionTransformer();
registerMapFunctionTransformer();
registerBinaryFunctionTransformer();
// todo: support more function transform
}
以 registerAggregateFunctionTransformer 为例,这里负责对聚合函数的映射进行注册,相关代码如下所示:
private static void registerAggregateFunctionTransformer() {
// 1.approx_distinct
registerFunctionTransformer("approx_distinct", 1,
"approx_count_distinct", ImmutableList.of(Expr.class));
// 2. arbitrary
registerFunctionTransformer("arbitrary", 1,
"any_value", ImmutableList.of(Expr.class));
// 3. approx_percentile
registerFunctionTransformer("approx_percentile", 2,
"percentile_approx", ImmutableList.of(Expr.class, Expr.class));
通过这个函数的映射,Trino 的 approx_distinct 函数就会被转换成 SR 的 approx_count_distinct,对应的结构体转换如下所示:
可以看到,最终在 map 中保存了 approx_distinct 这个 Trino 函数到 SR 的映射。需要注意的是,map 的 value 是一个 list,主要是为了处理参数不同的重载函数。通过 debug 可以直接查看已经注册的函数映射:
其中,PlaceholderExpr 就是用来保存 SR 函数的输入参数,主要就是 index 和 参数的类型,后续用于进行匹配,最终会被替换成实际的函数参数,位于 FunctionCallRewriter 的 sourceArguments 中。
3.3 Transformer 匹配
在 FE 启动之后,Trino 的函数映射已经全部注册完成。当我们通过设置方言为 trino 之后,首先需要根据 Trino 的函数名去 map 中进行匹配,由于重载函数的存在,因此还需要比较对应的参数类型。相关的函数调用如下所示:
parse(SqlParser.java):56
--parseWithTrinoDialect(SqlParser.java):68/74
---toStatement(TrinoParserUtils.java):42
----accept(io/trino/sql/tree/Statement.java)
// 省略部分函数调用栈
-visitFunctionCall(trino/AstBuilder.java):713
--convert(Trino2SRFunctionCallTransformer.java):47
---convertRegisterFn(Trino2SRFunctionCallTransformer.java):62
----match(FunctionCallTransformer.java)
其中,match函数的参数为 List<Expr>,对应的就是 2.1 中 List<Expression> 转换后的结果,即将 Trino 的 Expression 转换为 SR 中的 Expr 结构。匹配的过程主要分为两步:
- 比较参数个数是否一致;
- 比较每一个 PlaceholderExpr 的类型,是否是实际参数类型的超类。
两个条件都满足的话,则证明这个 transformer 是相符的,则继续进行后续的转换。
3.4 函数转换
转换操作主要就是生成一个 FunctionCallRewriter 对象,相关的函数调用如下所示:
visitFunctionCall(trino/AstBuilder.java):713
-convert(Trino2SRFunctionCallTransformer.java):47
--convertRegisterFn(Trino2SRFunctionCallTransformer.java):69
---transform(FunctionCallTransformer.java):113
----ctor(FunctionCallRewriter.java)
我们通过 debug 分别对比下 Trino 和 SR 中的function call 的结构,如下所示:
可以看到,最终生成的 FunctionCallRewriter 中,已经包含了具体的参数,即 ZIP_CODE 这个列,对应的类型是 SlotRef,而 2.2 中的 PlaceholderExpr 只有类型信息,即 Expr(SlotRef是其一个子类,所以类型可以匹配上)。
四、总结
4.1 与 Doris 方言功能比较
由于 Doris 的 sql convertor 工具是借助 SqlGlot 实现的,因此与 Doris 本身关系不大。这里我们主要比较下 Doris 的方言 plugin 与 SR 的 transformer 优缺点:
方言功能 | 优点 | 缺点 |
---|---|---|
SR Transformer | 实现比较完善,支持各种函数转换 | 与SR代码耦合紧,并且仅支持Trino,扩展性一般 |
Doris Plugin | 通过Plugin的方法与Doris代码进行了解耦,扩展性相对较好,目前支持Trino和Spark | 实现比较简单,函数转换未提供,并且存在一些问题,可用性较差 |
4.2 思考小结
由于目前 SR 官方只支持 Trino 的方言转换,并且与源码耦合比较紧,如下所示:
// SqlParser.java
public static List<StatementBase> parse(String sql, SessionVariable sessionVariable) {
if (sessionVariable.getSqlDialect().equalsIgnoreCase("trino")) {
return parseWithTrinoDialect(sql, sessionVariable);
} else {
return parseWithStarRocksDialect(sql, sessionVariable);
}
}
如果想要完整的支持一种新的方言转换,需要实现对应的 FunctionCallTransformer 和 AstBuilder,并修改上述的 if-else,总体代码开发量比较大。此外,本文的所有内容是笔者基于 StarRocks-3.3 版本分析、总结而来,如有错误,欢迎指正。
五、参考文档
- sql_dialect;
- [Feature] Support Trino parser on StarRocks #14830;