【Calcite源码学习】SqlNode方言转换

2022-05-20 08:54:28 浏览数 (1)

文章目录

    • SqlNode介绍
    • 方言转换使用
    • 方言转换代码解析
      • SqlNode.toSqlString方法
      • SUBSTRING转SUBSTR
      • APPROX_COUNT_DISTINCT转APPROX_DISTINCT
      • ROLLUP重写
    • 小结

我们知道,Calcite一般会有四个阶段:parse、validate、optimize和execute。其中,在parse和validate阶段,会生成一个parse tree,树中的节点都是SqlNode的类型。在optimize节点,Calcite会将parse tree转换为RelNode,同时进行一些优化,这属于logical plan。最终在execute阶段,将logical plan转换为物理执行计划来执行。Calcite目前提供了一些方言转换的功能,可以将SqlNode和RelNode转成指定计算引擎的SQL方言,例如Mysql、Presto等,相关的方言转换类如下所示:

本我们主要看一下,Calcite针对SqlNode的方言转换是如何实现。

SqlNode介绍

先简单看一下SqlNode是什么。在使用Calcite的parser进行解析之后,SQL就会被转换成一颗parse tree,树中每一个节点都对应一个SqlNode。对于非叶子结点,基本都是一个SqlCall,继承SqlNode。而我们常见的各种SQL类型,都是继承了SqlCall,例如select查询,对应的是SqlSelect;create、drop等ddl,对应的是SqlDdl等。这里我们看下一个SqlSelect的组成:

可以看到一个SqlSelect的parse tree主要包含了select list、from、where等部分,每个部分又由其各自的成员组成。我们在进行方言转换的时候,就是要对这些SqlNode进行处理。

方言转换使用

常见的使用方式如下:

代码语言:javascript复制
SqlParser parser = SqlParser.create(sql, SqlParser.Config.DEFAULT);
SqlNode sqlNode = parser.parseStmt();
sqlNode.toSqlString(PrestoSqlDialect.DEFAULT)

主要分为三个步骤:

  1. 根据sql和SqlParser.Config构造一个SqlParser,这里的Config可以配置一些引用标识符、大小写保留等参数;
  2. 调用parseStmt方法,就可以得到一个parse tree,这里的sqlNode是树的root节点,一般就是SqlSelect。
  3. 调用toSqlString方法,就可以传入指定的SqlDialect类,实现特定的方言转换。这里我们就传入了PrestoSqlDialect,将SQL转成presto的SQL输出。

方言转换代码解析

下面我们就来看一下,Calcite是如何实现这种方言转换的功能。

SqlNode.toSqlString方法

主要的处理逻辑位于toSqlString方法,相关的代码调用如下所示:

代码语言:javascript复制
toSqlString(SqlNode.java):187
-toSqlString(SqlNode.java):178
--toSqlString(SqlNode.java):156
---unparse(SqlSelect.java):261
----unparseCall(PrestoSqlDialect.java):123

这里调用了toSqlString的三个重载方法,我们依次来看一下:

代码语言:javascript复制
//SqlNode.java
public SqlString toSqlString(@Nullable SqlDialect dialect) {
  return toSqlString(dialect, false);
}

public SqlString toSqlString(@Nullable SqlDialect dialect, boolean forceParens) {
  return toSqlString(c ->
      c.withDialect(Util.first(dialect, AnsiSqlDialect.DEFAULT))
       .withAlwaysUseParentheses(forceParens)
       .withSelectListItemsOnSeparateLines(false)
       .withUpdateSetListNewline(false)
       .withIndentation(0));
}

第一个toSqlString比较简单,就是传入了一个SqlDialect作为参数。第二个重载方法构造了一个labmda表达式作为参数,然后调用了第三个重载方法:

代码语言:javascript复制
//SqlNode.java
public SqlString toSqlString(UnaryOperator<SqlWriterConfig> transform) {
  final SqlWriterConfig config = transform.apply(SqlPrettyWriter.config());
  SqlPrettyWriter writer = new SqlPrettyWriter(config);
  unparse(writer, 0, 0);
  return writer.toSqlString();
}

//SqlPrettyWriter.java
private static final SqlWriterConfig CONFIG =
    SqlWriterConfig.of().withDialect(CalciteSqlDialect.DEFAULT);

public static SqlWriterConfig config() {
  return CONFIG;
}

第三个重载方法的参数是一个包含SqlWriterConfig的UnaryOperator,UnaryOperator继承了lambda的Function接口。因此,这三个重载函数总结下来的处理逻辑就是:

  1. 传入一个指定的SqlDialect;
  2. 在第三个重载方法中,将SqlPrettyWriter.config()作为lambda表达式中的c,去执行各个with操作,这里就包括了设置SqlDialect;
  3. 返回得到一个包含指定dialect的SqlWriterConfig,对应的实现类为ImmutableSqlWriterConfig;
  4. 使用这个config构造了一个SqlPrettyWriter,然后调用对应SqlNode的unparse方法,例如常见的select查询,对应的就是SqlSelect结构体。

可以看到,在最后一个重载方法中,调用了unparse方法。这个方法是SqlNode的一个抽象方法,需要各自的子类去实现。例如,对于常见的SqlSelect,其unparse方法的主要逻辑就是调用指定SqlDialect的unparseCall方法,如下所示:

代码语言:javascript复制
//SqlSelect.java
  @Override public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
    if (!writer.inQuery()) {
      //省略无关代码
    } else {
      writer.getDialect().unparseCall(writer, this, leftPrec, rightPrec);
    }
  }

这里将this作为参数,调用了SqlWriter的SqlDialect的unparseCall方法,SqlWriter就是用来保存转换之后的sql方言容器,而leftPrec和rightPrec代表了运算符的优先级,这里暂时不用关注。我们在上面传入的是PrestoSqlDialect,因此最终是在这个方言的unparseCall方法中,实现了方言转换:

代码语言:javascript复制
//PrestoSqlDialect.java
@Override public void unparseCall(SqlWriter writer, SqlCall call,
      int leftPrec, int rightPrec) {
    if (call.getOperator() == SqlStdOperatorTable.SUBSTRING) {
      RelToSqlConverterUtil.specialOperatorByName("SUBSTR")
          .unparse(writer, call, 0, 0);

这里的SqlCall参数,对应的就是parse tree中的某些节点,例如SqlSelect、SqlBasicCall等,也就是上面unparse方法传入的this参数。在处理完当前这个节点之后(例如SqlSelect),unparseCall方法就会调用这个节点的operator的unparse方法,如下所示:

代码语言:javascript复制
//SqlDialect.java
  public void unparseCall(SqlWriter writer, SqlCall call, int leftPrec,
      int rightPrec) {
    SqlOperator operator = call.getOperator();
    // 省略无关代码
    default:
      operator.unparse(writer, call, leftPrec, rightPrec);
    }
  }

例如,SqlSelect对应的operator就是SqlSelectOperator,而这个类的unparse方法会依次处理select每个部分,例如select list、where、from等,调用它们的unparse,所以最终,整个parse tree中的所有节点都会被遍历到。 下面,我们就结合几个具体的功能来看下一下这个方言转换主要是如何实现的。

SUBSTRING转SUBSTR

在PrestoSqlDialect中,Calcite实现了从SUBSTRING到SUBSTR的方言转换,相应的代码调用如下所示:

代码语言:javascript复制
toSqlString(SqlNode.java):187
-toSqlString(SqlNode.java):178
--toSqlString(SqlNode.java):156
---unparse(SqlSelect.java)261
----unparseCall(PrestoSqlDialect.java)123

最终,在PrestoSqlDialect的unparseCall方法中,判断是否为SUBSTRING函数,如果是的话,则替换为SUBSTR,如下所示:

代码语言:javascript复制
@Override public void unparseCall(SqlWriter writer, SqlCall call,
    int leftPrec, int rightPrec) {
  if (call.getOperator() == SqlStdOperatorTable.SUBSTRING) {
    RelToSqlConverterUtil.specialOperatorByName("SUBSTR").unparse(writer, call, 0, 0);

这个SUBSTRING对应的operator在parse之后,就已经是一个SqlSubstringFunction变量了,不需要再进行validate了,如下所示:

也就是说,当通过parse获取到SqlNode之后,就可以直接通过如下实现presto的方言转换:

代码语言:javascript复制
sqlNode.toSqlString(PrestoSqlDialect.DEFAULT);

APPROX_COUNT_DISTINCT转APPROX_DISTINCT

除此之外,Calcite还提供了从APPROX_COUNT_DISTINCT到APPROX_DISTINCT的转换。函数调用栈与上面一样,最终在PrestoSqlDialect的unparseCall方法中,判断是否为APPROX_COUNT_DISTINCT函数,如果是的话,则替换为APPROX_DISTINCT,如下所示:

代码语言:javascript复制
//PrestoSqlDialect.unparseCall()
} else if (call.getOperator() == SqlStdOperatorTable.APPROX_COUNT_DISTINCT) {
  RelToSqlConverterUtil.specialOperatorByName("APPROX_DISTINCT").unparse(writer, call, 0, 0);

但是,与SUBSTRING不同的是,在parse之后,对应的operator是一个SqlUnresolvedFunction变量,而不是SqlCountAggFunction,如下所示:

也就是说,parse之后得到的SqlNode,没办法通过toSqlString方法实现对APPROX_COUNT_DISTINCT的方言转换。我们需要再对SqlNode进行validate,如下所示:

代码语言:javascript复制
SqlNode sqlNode = getSqlNode(sql);
SqlValidator validator = SqlValidatorUtil.newValidator(xxx);
validator.validate(sqlNode);
sqlNode.toSqlString(PrestoSqlDialect.DEFAULT);

此时,我们就可以完成对APPROX_COUNT_DISTINCT的方言转换,调试信息如下所示:

ROLLUP重写

还有一种方言转换,就是针对Mysql的ROLLUP用法,可以将Calcite的“GROUP BY ROLLUP(xxx)”这种语法,转换为Mysql的“GROUP BY xxx WITH ROLLUP”,这种转换也是在parse之后就可以进行了,不需要经过validate操作。相应的代码调用如下所示:

代码语言:javascript复制
toSqlString(SqlNode.java):156
-unparse(SqlSelect.java):261
--unparseCall(MysqlSqlDialect.java):227
---unparseCall(SqlDialect.java):460
---unparse(SqlSelectOperator.java):209

group by子句是属于SqlSelect的一部分,所以最终是要在SqlSelectOperator的unparse方法中,对group by进行转换,接下来的代码调用如下所示:

代码语言:javascript复制
list(SqlPrettyWriter.java):1080
-list(SqlPrettyWriter.java):1283
--list2(SqlPrettyWriter.java):1303
---unparse(SqlCall.java):126
----unparseCall(MysqlSqlDialect.java):227
-----unparseCall(SqlDialect.java):460
------unparse(SqlRollupOperator.java):40

可以看到,最终通过MysqlSqlDialect的supportsGroupByWithRollup方法来判断是否支持“with rollup”这种语法。相关的方法如下所示:

代码语言:javascript复制
//SqlRollupOperator.java
@Override public void unparse(SqlWriter writer, SqlCall call, int leftPrec,
    int rightPrec) {
  switch (kind) {
  case ROLLUP:
    if (!writer.getDialect().supportsAggregateFunction(kind)
      unparseKeyword(writer, call, "WITH ROLLUP");
      return;
    }
    break;

其中,groupBy的结构信息如下所示:

可以看到,ROLLUP对应的operator是一个SqlRollupOperator。

小结

通过上面的代码剖析和几个方言转换的具体实现,我们可以看到:这里的方言转换,其实就是通过SqlNode的unparse方法,将自身转换为对应的sql string,然后append到SqlWriter中;而SqlDialect的unparseCall方法,则提供了一些额外的处理逻辑,可以将SqlNode转换为一些其他的方言格式,例如函数名变更、类型名称变更等,然后同样append到SqlWriter中。整个过程,按照从上往下,从左往右的顺序来遍历整个parse tree,当遍历完成之后,也就已经将转换好的sql string存储到了SqlWriter中。主要有以下几个特点:

  1. 不会影响原先的SqlNode的内容;
  2. 在进行方言匹配时,一般是比较SqlOperator(例如SqlStdOperatorTable.SUBSTRING)或者SqlKind(例如或者SqlKind.ROLLUP);
  3. 在进行方言转换的匹配时,有些匹配是需要经过validate才能进行的,例如SqlStdOperatorTable.APPROX_COUNT_DISTINCT,而有一些是直接就可以在parse之后进行。

0 人点赞