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
有两种方式:
-
通过数据源的方式,也就是我们知道执行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()); }
-
手动添加catalog信息,不需要连库就能验证
javaprivate 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();
}
}