Calcite校验器校验SQL语句

Calcite校验器

要想构建一个SqlValidator,需要用到如下4个关键类:SqlOperatorTable、SqlValidatorCatalogReader、RelDataTypeFactory、SqlValidator.Config

java 复制代码
  public static SqlValidatorWithHints newValidator(
      SqlOperatorTable opTab,
      SqlValidatorCatalogReader catalogReader,
      RelDataTypeFactory typeFactory,
      SqlValidator.Config config) {
    return new SqlValidatorImpl(opTab, catalogReader, typeFactory,
        config);
  }

SqlOperatorTable

SqlOperatorTable是用来定义查找SQL算子和函数 的接口。SQL算子,指的是SqlOperator及其子类。例如,select id,u.name as u_name from t_user u 语句中,便可能包含了 AS算子、DOT算子。 可以理解为校验器所支持的SQL关键字(函数也是关键字的一种)。

SqlValidatorCatalogReader

SqlValidatorCatalogReader是用来给校验器提供目录信息的,也就是表、类型和Schema这样的元数据信息。是元数据和校验器的连接桥梁。 构造它,我们需要传入一个 CalciteSchema、一个schema名称、一个数据类型处理器(RelDataTypeFactory)和 连接配置信息(CalciteConnectionConfig)

java 复制代码
public CalciteCatalogReader(CalciteSchema rootSchema,
      List<String> defaultSchema, RelDataTypeFactory typeFactory, CalciteConnectionConfig config)

实际使用时,我们的后两个参数大都是固定的,因此只需传入rootSchema 和 默认名称即可。

java 复制代码
RelDataTypeFactory typeFactory = new JavaTypeFactoryImpl();
CalciteConnectionConfig config = CalciteConnectionConfig.DEFAULT;
// 创建CatalogReader, 用于指示如何读取Schema信息
Prepare.CatalogReader catalogReader = new CalciteCatalogReader(
    rootSchema,
    // 当有多个schema 且 SQL中缺省schema时,应该使用谁
    StringUtils.isEmpty(currentDatabase)?Collections.emptyList():Collections.singletonList(currentDatabase),
    typeFactory,
    config);

RelDataTypeFactory

RelDataTypeFactory是处理数据类型的工厂类,它负责SQL类型、Java类型和集合类型 的创建和转化。Calcite支持SQL(SqlTypeFactoryImpl)和Java(JavaTypeFactoryImpl)两种实现,也可以仿照它们自行进行扩展。

SqlValidator.Config

校验器的一些自身的配置信息。如, 是否允许隐式类型转换、是否展开选择列,等等。通常使用默认项即可SqlValidator.Config.DEFAULT

实践

准备工作

在test数据库中,准备一张用户得分表,用作此次的演示。

sql 复制代码
create table  test.t_score(
    id int(11) auto_increment primary key ,
    user_id int(11) not null comment '用户ID',
    subjects varchar(255) not null comment '学科',
    score decimal(5,2) default 0.0 comment '分数'
) ENGINE = InnoDB
  DEFAULT CHARSET = utf8mb4
  COLLATE = utf8mb4_general_ci
  ROW_FORMAT = DYNAMIC
    COMMENT ='用户得分表';

根据上一篇文章 Calcite元数据定义和获取,我们定义好一个CalciteSchema。

开始

第一步,构建CatalogReader

java 复制代码
RelDataTypeFactory typeFactory = new JavaTypeFactoryImpl();
CalciteConnectionConfig config = CalciteConnectionConfig.DEFAULT;
// 创建CatalogReader, 用于指示如何读取Schema信息
Prepare.CatalogReader catalogReader = new CalciteCatalogReader(
    rootSchema,
    // 当有多个schema 且 SQL中缺省schema时,应该使用谁
    StringUtils.isEmpty(currentDatabase)?Collections.emptyList():Collections.singletonList(currentDatabase),
    typeFactory,
    config);

需要注意的是,如果你的数据库连接没有指定currentDatabase(也就是第二个参数赋值了emptyList),那么在写SQL时,就必须声明使用了哪个库的哪张表,如:

sql 复制代码
select user_id,sum(score) as s_score from test.t_score group by user_id

反之,如果你指定了currentDatabase(也就是第二个参数赋值了singletonList),则无需声明库,校验器会使用默认值。

sql 复制代码
select user_id,sum(score) as s_score from t_score group by user_id

第二步,构建SqlValidator

java 复制代码
SqlValidator.Config validatorConfig = SqlValidator.Config.DEFAULT
        .withIdentifierExpansion(true);
SqlValidator validator = SqlValidatorUtil.newValidator(
        SqlStdOperatorTable.instance(), catalogReader, typeFactory, validatorConfig);

这里,笔者遇到一个问题,就是默认的SqlStdOperatorTable,在使用Mysql语法时,不支持if(bool,exp1,exp2)方法。但实际上,if函数的定义是可以在源代码中找到的,只是需要指明sql所用语法为hive。 我的解决办法是,新建一个类,并继承SqlStdOperatorTable,将IF函数的定义直接赋值过来(不是copy代码).

java 复制代码
public class SqlCustomOperatorTable extends SqlStdOperatorTable {
    private static final Supplier<SqlCustomOperatorTable> INSTANCE =
            Suppliers.memoize(() ->
                    (SqlCustomOperatorTable) new SqlCustomOperatorTable().init());

    // 在init方法中,通过反射遍历的方式,将SqlOperator放入list
    public static final SqlFunction IF = SqlLibraryOperators.IF;

    public static SqlCustomOperatorTable instance() {
        return INSTANCE.get();
    }

如此一来,SqlValidator的构建就变成了

java 复制代码
SqlValidator validator = SqlValidatorUtil.newValidator(
        SqlCustomOperatorTable.instance(), catalogReader, typeFactory, validatorConfig);

if函数也得到了支持。

第三步,进行校验 调用SqlNode validatedSqlNode = validator.validate(sqlNode); 方法,进行校验。如果顺利通过,则返回结果为null;否则,抛出异常。 我们注意到,validate方法需要的是一个SqlNode。如果你的app是使用了Calcite进行的拼装sql,则有现成的sqlNode;如果是一条sql字符串,还需进行Sql解析这一步骤.

java 复制代码
SqlDialect sqlDialect = MysqlSqlDialect.DEFAULT;
// 从SqlDialect中还原出SqlParser.Config
SqlParser.Config config = sqlDialect.configureParser(SqlParser.config());
SqlNode sqlNode =  SqlParser.create(querySqlContext.getSql(), config).parseQuery();

至此,我们可以做一些简单的试验。比如,故意写错某一列的名字select usr_id,sum(score) as s_score from t_score group by user_id,程序会抛出异常:

java 复制代码
Exception in thread "main" org.apache.calcite.runtime.CalciteContextException: From line 1, column 8 to line 1, column 13: Column 'usr_id' not found in any table

而没有写group by 语句,也会提示我们:

java 复制代码
// sql is select user_id,sum(score) as s_score from t_score
Exception in thread "main" org.apache.calcite.runtime.CalciteContextException: From line 1, column 8 to line 1, column 14: Expression 'user_id' is not being grouped

至于更为复杂的场景,则靠大家去试验了。 以上。

相关推荐
并不会1 小时前
常见 CSS 选择器用法
前端·css·学习·html·前端开发·css选择器
衣乌安、1 小时前
【CSS】居中样式
前端·css·css3
兔老大的胡萝卜1 小时前
ppk谈JavaScript,悟透JavaScript,精通CSS高级Web,JavaScript DOM编程艺术,高性能JavaScript pdf
前端·javascript
低代码布道师1 小时前
CSS的三个重点
前端·css
耶啵奶膘2 小时前
uniapp-是否删除
linux·前端·uni-app
王哈哈^_^4 小时前
【数据集】【YOLO】【目标检测】交通事故识别数据集 8939 张,YOLO道路事故目标检测实战训练教程!
前端·人工智能·深度学习·yolo·目标检测·计算机视觉·pyqt
cs_dn_Jie4 小时前
钉钉 H5 微应用 手机端调试
前端·javascript·vue.js·vue·钉钉
开心工作室_kaic5 小时前
ssm068海鲜自助餐厅系统+vue(论文+源码)_kaic
前端·javascript·vue.js
有梦想的刺儿5 小时前
webWorker基本用法
前端·javascript·vue.js
cy玩具6 小时前
点击评论详情,跳到评论页面,携带对象参数写法:
前端