5. 想在代码中验证sql的正确性?

1. 简介

我们在平时的开发中可能会遇到需要验证一下sql是否正确,也就是需要check一下sql。

判断sql是否正确一般包含一下几点:

1. sql中使用的列是否存在
2. sql语法是否正确
3. sql中使用到的操作符/函数是否存在,有没有正确的使用

我们可以用以下的sql示例来探究一下使用calcite如何校验sql

sql 复制代码
select 
	u.sex,
    max(u.age)
from user u
         inner join role r on u.role_id = r.id
where r.id = 1
group by u.sex

2. Maven

xml 复制代码
<dependency>
    <groupId>org.apache.calcite</groupId>
    <artifactId>calcite-core</artifactId>
    <version>1.37.0</version>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>

3. 验证

首先 在calcite中验证sql的正确性是通过使用calcite中的SqlValidator类进行校验的,但SqlValidator是一个接口, 通常是通过SqlValidatorUtil.newValidator(...)方法进行实例化的, 如下:

java 复制代码
public static SqlValidatorWithHints newValidator(SqlOperatorTable opTab,
                                                 SqlValidatorCatalogReader catalogReader, 
                                                 RelDataTypeFactory typeFactory, 
                                                 SqlValidator.Config config) 
{
    return new SqlValidatorImpl(opTab, catalogReader, typeFactory, config);    
}
  • SqlOperatorTable:用来提供sql验证所需的操作符(SqlOperator)和函数(SqlFunction)例如:>, <, = 或max(),in()
  • SqlValidatorCatalogReader:用来提供验证所需的元数据信息 例如: schema, table, column
  • RelDataTypeFactory:处理数据类型的工厂类,用来提供类型、java类型和集合类型的创建和转化。针对不同的接口形式,calcite支持sql和java两种实现(SqlTypeFactoryImpl和JavaTypeFactoryImpl),当然这里用户可以针对不同的情况自行扩展
  • SqlValidator.Config:可以自定义一些配置,例如是否开启类型隐式转换、是否开启 SQL 重写等等

3.1 创建SqlValidator

创建SqlValidator之前需要先实例化上述的四个入参对象,好在calcite提供了对应属性的默认实现,使得我们能很方便的创建SqlValidator对象

java 复制代码
SqlValidator validator = SqlValidatorUtil.newValidator(
    SqlStdOperatorTable.instance(),
    catalogReader, // catalog信息需要自己手动创建
    new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT),
    SqlValidator.Config.DEFAULT);

这里除了SqlValidatorCatalogReader对象需要额外的自己创建,如果没有特殊的需求,我们都可以使用calcite提供的默认实现。

我们这里着重讲一下SqlValidatorCatalogReader对象如何创建

首先SqlValidatorCatalogReader使用来提供验证所需的catalog信息的,那我们就需要提供一下catalog信息(因为calcite需要做元数据的验证,比如表,字段是否存在,不提供元数据calcite谈何验证)

创建SqlValidatorCatalogReader有两种方式:

  1. 通过数据源的方式,也就是我们知道执行sql的server信息,把连接信息给calcite,让calcite自己去获取元信息并进行验证,也就是这个时候需要去连接db才能进行验证

    java 复制代码
    @SneakyThrows
    private static CalciteCatalogReader createCatalogReaderWithDataSource() {
        Connection connection = DriverManager.getConnection("jdbc:calcite:");
        CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class);
        SchemaPlus rootSchema = calciteConnection.getRootSchema();
        DataSource dataSource = JdbcSchema.dataSource(
            "jdbc:mysql://localhost:3306/test",
            "com.mysql.cj.jdbc.Driver",
            "root",
            "123456"
        );
        JdbcSchema jdbcSchema = JdbcSchema.create(rootSchema, "my_mysql", dataSource, null, null);
        rootSchema.add("my_mysql", jdbcSchema);
        calciteConnection.setSchema("my_mysql");
        CalciteServerStatement statement = connection.createStatement().unwrap(CalciteServerStatement.class);
        CalcitePrepare.Context prepareContext = statement.createPrepareContext();
        SqlTypeFactoryImpl factory = new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT);
    
        return new CalciteCatalogReader(
            prepareContext.getRootSchema(),
            prepareContext.getDefaultSchemaPath(),
            factory,
            calciteConnection.config());
    }
  2. 手动添加catalog信息,不需要连库就能验证

    java 复制代码
    private static CalciteCatalogReader createCatalogReaderWithMeta() {
        SchemaPlus rootSchema = Frameworks.createRootSchema(true);
        RelDataTypeSystem relDataTypeSystem = RelDataTypeSystem.DEFAULT;
        RelDataTypeFactory typeFactory = new SqlTypeFactoryImpl(relDataTypeSystem);
        rootSchema.add("user", new AbstractTable() {
            @Override
            public RelDataType getRowType(RelDataTypeFactory typeFactory) {
                RelDataTypeFactory.Builder builder = typeFactory.builder();
                builder.add("id", new BasicSqlType(relDataTypeSystem, SqlTypeName.INTEGER));
                builder.add("name", new BasicSqlType(relDataTypeSystem, SqlTypeName.VARCHAR));
                builder.add("age", new BasicSqlType(relDataTypeSystem, SqlTypeName.INTEGER));
                builder.add("sex", new BasicSqlType(relDataTypeSystem, SqlTypeName.VARCHAR));
                builder.add("role_id", new BasicSqlType(relDataTypeSystem, SqlTypeName.INTEGER));
                return builder.build();
            }
        });
        rootSchema.add("role", new AbstractTable() {
            @Override
            public RelDataType getRowType(RelDataTypeFactory typeFactory) {
                RelDataTypeFactory.Builder builder = typeFactory.builder();
                builder.add("id", new BasicSqlType(relDataTypeSystem, SqlTypeName.INTEGER));
                builder.add("name", new BasicSqlType(relDataTypeSystem, SqlTypeName.VARCHAR));
                return builder.build();
            }
        });
        CalciteConnectionConfig connectionConfig = CalciteConnectionConfig.DEFAULT;
    
        return new CalciteCatalogReader(
            CalciteSchema.from(rootSchema),
            CalciteSchema.from(rootSchema).path(null),
            typeFactory,
            connectionConfig);
    }

    ok,至此创建SqlValidator所需的参数都已备齐,但是当执行验证方法的时候所需的参数并不是sql字符串而是SqlValidator.validate(SqlNode topNode), 那么SqlNode又要怎么创建 ?

3.2 解析Sql

SqlNode 顾名思义就是sql节点对象,直接通过SqlParser对象创建,如下

java 复制代码
SqlParser.Config config = SqlParser.config()
                                     // 解析工厂
                                     .withParserFactory(SqlParserImpl.FACTORY)
                                     // 也可以直接设置为对应数据库的词法分析器
                                     // .withLex(Lex.MYSQL) 
                                     // 不区分大小写
                                     .withCaseSensitive(false)
                                     // 引用符号为反引号
                                     .withQuoting(Quoting.BACK_TICK)
                                     // 未加引号的标识符在解析时不做处理
                                     .withUnquotedCasing(Casing.UNCHANGED)
                                     // 加引号的标识符在解析时不做处理
                                     .withQuotedCasing(Casing.UNCHANGED)
                                     // 使用默认的语法规则
                                     .withConformance(SqlConformanceEnum.DEFAULT);
// sql解析器
final SqlParser parser = SqlParser.create(SQL, config);
// 将sql转换为calcite的SqlNode
SqlNode sqlNode = parser.parseQuery();

3.3 执行验证

通过上述的步骤 我们已经能创建SqlValidator对象并且能创建其验证时需要的SqlNode对象,其实很简单, 只要验证时不报错,即sql是正确的

java 复制代码
try{
 // 校验 sql
 validator.validate(sqlNode);
 log.info("sql is valid");
}
catch (Exception e) {
 log.error("sql is invalid", e);
}

4. 完整验证代码

4.1 通过SqlValidator进行验证

java 复制代码
package com.ldx.calcite;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.calcite.adapter.jdbc.JdbcSchema;
import org.apache.calcite.avatica.util.Casing;
import org.apache.calcite.avatica.util.Quoting;
import org.apache.calcite.config.CalciteConnectionConfig;
import org.apache.calcite.jdbc.CalciteConnection;
import org.apache.calcite.jdbc.CalcitePrepare;
import org.apache.calcite.jdbc.CalciteSchema;
import org.apache.calcite.prepare.CalciteCatalogReader;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelDataTypeSystem;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.schema.impl.AbstractTable;
import org.apache.calcite.server.CalciteServerStatement;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.parser.impl.SqlParserImpl;
import org.apache.calcite.sql.type.BasicSqlType;
import org.apache.calcite.sql.type.SqlTypeFactoryImpl;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.validate.SqlConformanceEnum;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorUtil;
import org.apache.calcite.tools.Frameworks;
import org.junit.jupiter.api.Test;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;

@Slf4j
public class SqlValidatorTest {
    private static final String SQL = "select u.sex, max(u.age) from `user` u inner join role r on u.role_id = r.id where r.id = 1 group by u.sex";

    @Test
    @SneakyThrows
    public void given_sql_and_meta_then_validate_sql() {
        SqlParser.Config config = SqlParser.config()
                                           // 解析工厂
                                           .withParserFactory(SqlParserImpl.FACTORY)
                                           // 也可以直接设置为对应数据库的词法分析器
                                           // .withLex(Lex.MYSQL)
                                           // 不区分大小写
                                           .withCaseSensitive(false)
                                           // 引用符号为反引号
                                           .withQuoting(Quoting.BACK_TICK)
                                           // 未加引号的标识符在解析时不做处理
                                           .withUnquotedCasing(Casing.UNCHANGED)
                                           // 加引号的标识符在解析时不做处理
                                           .withQuotedCasing(Casing.UNCHANGED)
                                           // 使用默认的语法规则
                                           .withConformance(SqlConformanceEnum.DEFAULT);
        // sql解析器
        final SqlParser parser = SqlParser.create(SQL, config);
        // 将SQL转换为Calcite的SqlNode
        SqlNode sqlNode = parser.parseQuery();
        // 创建 SqlValidator 来进行校验
        SqlValidator validator = SqlValidatorUtil.newValidator(
                SqlStdOperatorTable.instance(),
                // 使用直接提供元信息的方式
                createCatalogReaderWithMeta(),
                // 使用提供数据源的方式
                //createCatalogReaderWithDataSource(),
                new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT),
                SqlValidator.Config.DEFAULT);
        try{
            // 校验 sql
            validator.validate(sqlNode);
            log.info("sql is valid");
        }
        catch (Exception e) {
            log.error("sql is invalid", e);
        }
    }

    private static CalciteCatalogReader createCatalogReaderWithMeta() {
        SchemaPlus rootSchema = Frameworks.createRootSchema(true);
        RelDataTypeSystem relDataTypeSystem = RelDataTypeSystem.DEFAULT;
        RelDataTypeFactory typeFactory = new SqlTypeFactoryImpl(relDataTypeSystem);
        rootSchema.add("user", new AbstractTable() {
                    @Override
                    public RelDataType getRowType(RelDataTypeFactory typeFactory) {
                        RelDataTypeFactory.Builder builder = typeFactory.builder();
                        builder.add("id", new BasicSqlType(relDataTypeSystem, SqlTypeName.INTEGER));
                        builder.add("name", new BasicSqlType(relDataTypeSystem, SqlTypeName.VARCHAR));
                        builder.add("age", new BasicSqlType(relDataTypeSystem, SqlTypeName.INTEGER));
                        builder.add("sex", new BasicSqlType(relDataTypeSystem, SqlTypeName.VARCHAR));
                        builder.add("role_id", new BasicSqlType(relDataTypeSystem, SqlTypeName.INTEGER));
                        return builder.build();
                    }
                });
        rootSchema.add("role", new AbstractTable() {
            @Override
            public RelDataType getRowType(RelDataTypeFactory typeFactory) {
                RelDataTypeFactory.Builder builder = typeFactory.builder();
                builder.add("id", new BasicSqlType(relDataTypeSystem, SqlTypeName.INTEGER));
                builder.add("name", new BasicSqlType(relDataTypeSystem, SqlTypeName.VARCHAR));
                return builder.build();
            }
        });
        CalciteConnectionConfig connectionConfig = CalciteConnectionConfig.DEFAULT;

        return new CalciteCatalogReader(
                CalciteSchema.from(rootSchema),
                CalciteSchema.from(rootSchema).path(null),
                typeFactory,
                connectionConfig);
    }

    @SneakyThrows
    private static CalciteCatalogReader createCatalogReaderWithDataSource() {
        Connection connection = DriverManager.getConnection("jdbc:calcite:");
        CalciteConnection calciteConnection = connection.unwrap(CalciteConnection.class);
        SchemaPlus rootSchema = calciteConnection.getRootSchema();
        DataSource dataSource = JdbcSchema.dataSource(
                "jdbc:mysql://localhost:3306/test",
                "com.mysql.cj.jdbc.Driver",
                "root",
                "123456"
        );
        JdbcSchema jdbcSchema = JdbcSchema.create(rootSchema, "my_mysql", dataSource, null, null);
        rootSchema.add("my_mysql", jdbcSchema);
        calciteConnection.setSchema("my_mysql");
        CalciteServerStatement statement = connection.createStatement().unwrap(CalciteServerStatement.class);
        CalcitePrepare.Context prepareContext = statement.createPrepareContext();
        SqlTypeFactoryImpl factory = new SqlTypeFactoryImpl(RelDataTypeSystem.DEFAULT);

        return new CalciteCatalogReader(
                prepareContext.getRootSchema(),
                prepareContext.getDefaultSchemaPath(),
                factory,
                calciteConnection.config());
    }
}

4.2 使用Planner对象进行验证

其实Planner.validate方法其底层使用的还是SqlValidator对象进行验证

java 复制代码
package com.ldx.calcite;

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.calcite.avatica.util.Casing;
import org.apache.calcite.avatica.util.Quoting;
import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rel.type.RelDataTypeSystem;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.schema.impl.AbstractTable;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.parser.impl.SqlParserImpl;
import org.apache.calcite.sql.type.BasicSqlType;
import org.apache.calcite.sql.type.SqlTypeName;
import org.apache.calcite.sql.validate.SqlConformanceEnum;
import org.apache.calcite.tools.FrameworkConfig;
import org.apache.calcite.tools.Frameworks;
import org.apache.calcite.tools.Planner;
import org.apache.calcite.tools.ValidationException;
import org.junit.jupiter.api.Test;

@Slf4j
public class SqlValidatorWithPlannerTest {
    private static final String SQL = "select u.sex, max(u.age) from `user` u inner join role r on u.role_id = r.id where r.id = 1 group by u.sex";

    @Test
    @SneakyThrows
    public void given_sql_and_meta_then_validate_sql() {
        // 创建Calcite配置
        FrameworkConfig config = createFrameworkConfig();
        // 创建Planner
        Planner planner = Frameworks.getPlanner(config);
        // 解析SQL
        final SqlNode parse = planner.parse(SQL);

        try {
            // 获取SqlValidator进行校验
            planner.validate(parse);
            log.info("sql is valid");
        } catch (ValidationException e) {
            log.error("sql is invalid", e);
        }
    }

    private static FrameworkConfig createFrameworkConfig() {
        SchemaPlus rootSchema = Frameworks.createRootSchema(true);
        RelDataTypeSystem relDataTypeSystem = RelDataTypeSystem.DEFAULT;
        rootSchema.add("user", new AbstractTable() {
            @Override
            public RelDataType getRowType(RelDataTypeFactory typeFactory) {
                RelDataTypeFactory.Builder builder = typeFactory.builder();
                builder.add("id", new BasicSqlType(relDataTypeSystem, SqlTypeName.INTEGER));
                builder.add("name", new BasicSqlType(relDataTypeSystem, SqlTypeName.VARCHAR));
                builder.add("age", new BasicSqlType(relDataTypeSystem, SqlTypeName.INTEGER));
                builder.add("sex", new BasicSqlType(relDataTypeSystem, SqlTypeName.VARCHAR));
                builder.add("role_id", new BasicSqlType(relDataTypeSystem, SqlTypeName.INTEGER));
                return builder.build();
            }
        });
        rootSchema.add("role", new AbstractTable() {
            @Override
            public RelDataType getRowType(RelDataTypeFactory typeFactory) {
                RelDataTypeFactory.Builder builder = typeFactory.builder();
                builder.add("id", new BasicSqlType(relDataTypeSystem, SqlTypeName.INTEGER));
                builder.add("name", new BasicSqlType(relDataTypeSystem, SqlTypeName.VARCHAR));
                return builder.build();
            }
        });
        SqlParser.Config config = SqlParser.config()
                                           .withParserFactory(SqlParserImpl.FACTORY)
                                           .withQuoting(Quoting.BACK_TICK)
                                           .withCaseSensitive(false)
                                           .withUnquotedCasing(Casing.UNCHANGED)
                                           .withQuotedCasing(Casing.UNCHANGED)
                                           .withConformance(SqlConformanceEnum.DEFAULT);
        return Frameworks
                .newConfigBuilder()
                .defaultSchema(rootSchema)
                .parserConfig(config)
                .build();
    }
}
相关推荐
Anarkh_Lee4 天前
MySQL深度剖析-InnoDB索引与B+树
数据结构·数据库·b树·mysql·算法·db
张铁牛1 个月前
4. 使用sql查询excel内容
db·calcite·middleware
张铁牛1 个月前
3. 使用sql查询csv/json文件内容,还能关联查询?
db·calcite·middleware
张铁牛1 个月前
2. 什么?你想跨数据库关联查询?
db·calcite·middleware
张铁牛1 个月前
1. Calcite元数据创建
db·calcite
dami_king2 个月前
SQL如何添加数据?|SQL添加数据示例
数据库·sql·db
程序猿进阶3 个月前
Otter 安装流程
java·数据库·后端·mysql·数据同步·db·otter
哇~是小菜呀3 个月前
db2函数之decode
db
FserSuN4 个月前
Apache Calcite - 查询优化之自定义优化规则
apache·calcite