calcite问题之calcite生成sql字符串时中文字符乱码问题
问题场景:
某个需求里,需要使用calcite, 将优化后的calcite逻辑计划树转成sql语句。
static String toSql(RelNode root, DataSourceType dataSourceType) {
SqlDialect dialect;
// 根据对应数据库类型决定dialect
dialect = datasourceToSqlDialect(dataSourceType);
// 逻辑计划转sql语法树的转换器
final RelToSqlConverter converter = new RelToSqlConverter(dialect);
// 把relNode转成sqlNode(sql语法树)
final SqlNode sqlNode = converter.visitChild(0, root).asStatement();
// 把sqlNode转成sql语句
return sqlNode.toSqlString(dialect).getSql();
}
结果发现作为where条件的字符串“研究生” ,变成了很奇怪的东西:
SELECT * FROM XUELI WHERE DEGREE = u&'\7814\7a76\751f'
表面上看起来是把 研究生三个字,转成了utf-16编码,并且加了各种转义符。
可能对于一些数据库来说, 支持这种识别方式。 但我项目的需求中暂时默认各库之间统一编码。 并且这个sql语句需要作为前台展示的, 如果搞出个这种编码串肯定不行
问题定位
先通过设置断点, 查看relNode中, “研究生”这个字符串是什么个数据结构。
发现是一个RexLiteral, 里面有个value值,存放了NlsString。
看下这个NlsString是什么:
stringValue里存储的就是原文字符串,是正常的中文串,未经过修改的。
而charsetName是编码格式,这里被标记为UTF-8。
那么可以看出,此时原串未被处理, 那就可能是relNode转sqlNode期间发生变化。
于是跟读converter.visitChild源码, 看下这个NlsString是否在转换过程中被修改。
发现这里使用到了这个nlsString,并且发现该过程直接使用原value生成SqlLiteral, 以至于丢失了编码信息
上面可以看到charSet是个null。
但是原字符串仍然还在,只是编码信息没了。那什么时候真正把这个原字符串弄没了呢?
于是查看sqlNode转sql语句的过程,定位到位置在SqlDialect的quoteStringLiteral:
因为charsetName为null,为了安全起见,他把字符都转成unicode形式编码,并补充前缀。
至此,中文字符串的变换过程基本明了。
解决方式
如果对编码要求不严格,统一为UTF-8, 且需要展示到前台。
即默认生成的sql中,可以直接用原sql编码,不用特地在编码间切换。
那么只要保证在sqlNode转sql的时候, 使用原字符串即可,不要加一堆编码前缀。
只要修改SqlDialect这个转换器接口, 继承实现他的queteStringLiteral
public class TestMysqlSqlDialect extends MysqlSqlDialect {
// 下发sql时,去除编码前缀,直接拼接原字符。 见用例testUtf8Sql
@Override
public void quoteStringLiteral(StringBuilder buf, String charsetName, String val) {
buf.append(this.literalQuoteString);
buf.append(val.replace(this.literalEndQuoteString, this.literalEscapedQuote));
buf.append(this.literalEndQuoteString);
}
}
然后构造RelToSqlConverter时,使用自己定义的这个新的SqlDialect即可。
final RelToSqlConverter converter = new RelToSqlConverter(dialect);
结果测试正确:
- 点赞
- 收藏
- 关注作者
评论(0)