关于sql:Calcite-SQL-解析语法扩展元数据验证原理与实战下

1次阅读

共计 6251 个字符,预计需要花费 16 分钟才能阅读完成。

上一篇文章咱们介绍了 Calcite SQL 解析的原理以及如何扩大 SQL 语法,本篇咱们将进入 SQL 执行的下一个阶段:元数据验证。

二、Calcite 元数据验证

SQL 胜利解析为形象语法树后,接下来须要对整条 SQL 的语义和元数据进行验证,判断 SQL 是否满足下一步计算的要求。

判断一句 SQL 是否正确至多蕴含以下几点:

  • 查问的库表列是否存在。
  • SQL 的语义是否正确。
  • 验证操作符,判断应用的操作符或者函数是否存在,并被正确地应用,例如数据类型、参数个数等是否正确。

咱们能够用以下 SQL 为例来探索 Sql 验证的过程。

SELECT col1, avg(col2)
FROM t1,
    (SELECT col3 FROM t2) AS s1
WHERE col4 > col5
GROUP BY col1;

在 Calcite 中 验证 SQL 是否非法应用 SqlValidator,它的初始化形式如下:

protected SqlValidatorImpl(
      SqlOperatorTable opTab,
      SqlValidatorCatalogReader catalogReader,
      RelDataTypeFactory typeFactory,
      Config config)

SqlOperatorTable 提供 SQL 验证所须要的所有操作符,函数、’>’、'<‘ 等都属于操作符;SqlValidatorCatalogReader 提供验证须要的元数据信息,包含 Catalog、Schema 等;RelDataTypeFactory 提供验证过程中字段类型的互相转换、例如函数返回值类型和精度信息等。Config 能够自定义一些配置,例如是否开启类型强制转换、是否开启 SQL 重写等等。

2.1 验证库表列是否存在

库和表的验证比拟好了解,因为大多数的 OLAP 引擎有多个数据源和数据空间,并且反对联邦查问,所以 SQL 中库表往往都是全名称模式存在(或者应用 use 语句指定),形如 catalog.schema.table(例如 hive.tpcds.store),这样一来,咱们就能确定惟一一张表的地位。此外,在 Calcite 验证器中有两个重要的形象,作用域(Scope)和命名空间(Namespace)。

  • 作用域

Scope 用来示意一个字段或者一个表达式的作用域,简略了解就是验证时 晓得 select 的字段可能呈现在哪张表中,这样便能校验这个字段是否存在。以结尾的 SQL 语句为例,表达式的作用域如下:

col1, col2 可能呈现在 t1, s1
col3 只能呈现在 t2 中
col4, col5 可能呈现在 t1, s1

当解析验证一个表达式的时候,如果验证通过,就会将表达式包装成一个 Namespace 返回。在验证 SELECT * FROM hive.tpcds.store 表达式时,会解析和重组 hive.tpcds.store 的每级 schema,通过 CatalogReader 的 getTable() 办法获取元数据信息,判断表和字段是否存在。第一步,Calcite 会验证 RootSchema,也就是 Catalog 是否存在,而后顺次向上拼接 schema,验证 hive.tpcds 是否存在,最初验证 hive.tpcds.store。如果全副验证通过返回一个 SqlValidatorNamespace。

  • 命名空间

Namespace 通常用来示意一个表、视图或者子查问,Namespace 继承关系如下:

Calcite 会将 from 前面的表、视图或者子查问封装成 Namespace,除此以外在继承关系中也能够看到 JoinNamespace, SchemaNamespace 这样更细分的 Namespace,Calcite 会将这些关系也封装成 Namespace。

以结尾的 SQL 语句为例,会将他们封装成 5 个 Namespace 存在 map 中,别离是:

electNamespace: SELECT col1, avg(col2) FROM t1, (SELECT col3 FROM t2) AS s1 WHERE col4 > col5 GROUP BY col1
SelectNamespace: SELECT col3 FROM t2
IdentifierNamespace: t1
IdentifierNamespace: t2
JoinNamespace: t1 join s1

在最终的验证阶段咱们调用 Namespace 的 validateImpl() 办法 进行验证。整个验证过程的关键步骤时序图如下:

2.2 SQL 语义的验证

SQL 语义验证包含查问的字段名 是否存在歧义、和聚合语句是否正确 等等。

在查看查问的字段名是否存在歧义时,会将这些字段名进行补全,拼接其库名和表名,来验证这些语句 是否存在二义性,如果 col1 即在 t1 中存在也在 t2 中存在,咱们查问时就必须在语句中指定 col1 的库表,否则语句是存在歧义的。Calcite 会在 org.apache.calcite.sql.validate.SqlValidatorScope#fullyQualify 办法中进行字段名称补全。

Calcite 还会查看语法的正确性,例如与聚合函数一起查问的字段是否蕴含在 group by 中,比方 SQL 语句:select c1, avg(c2) from t1; 那么 c1 必须在 group by 中。

这些验证逻辑就隐含在每个 Scope 对象中,上述例子就会在 AggregatingSelectScope 中做查看,所以 Scope 对象是 形容了字段的作用域信息

2.3 验证操作符

Calcite 中操作符既蕴含 ”<“, “>” 这些操作符,也蕴含函数,在初始化 SqlValidator 的时候会传入 SqlOperatorTable,这个类的作用就是提供操作符列表和按名称查找操作符的能力。

Calcite 也提供了默认实现:SqlStdOperatorTable.instance(),继承 SqlOperatorTable 接口,其中两个外围办法 lookupOperatorOverloads(): 检索具备给定名称和语法的操作符列表,例如传入 ”sum” 和 ”SqlSyntax.Function”,则返回名称为 ”sum” 的函数列表。getOperatorList():检索所有函数和运算符的列表。

在 Calcite 验证函数时,就会调用 lookupOperatorOverloads() 办法获取函数的实现。

  • 扩大函数

那咱们如何新增一个 Calcite 中没有的函数呢?

首先咱们能够继承 SqlOperatorTable 接口,实现本人的 lookupOperatorOverloads() 办法,其次,在初始化 SqlValidator 的时候传入咱们本人的 SqlOperatorTable 即可,上面通过 新增一个 Presto 中的函数 ”SUBSTR” 来演示 如何在 Calcite 中新增函数。以下为继承 SqlOperatorTable,实现 lookupOperatorOverloads() 的办法:

public class DemoOperatorTable
        implements SqlOperatorTable
{
    //Calcite 默认的操作符表
    private static final SqlOperatorTable stdOperatorTable = SqlStdOperatorTable.instance();
    // 用于存储自定义的函数名和实现的映射
    private final ListMultimap<String, SqlOperator> opMap = ArrayListMultimap.create();
    // 所有的函数列表
    private final List<SqlOperator> operators = new ArrayList<>();

    @Override
    public void lookupOperatorOverloads(SqlIdentifier opName, @Nullable SqlFunctionCategory sqlFunctionCategory, SqlSyntax syntax, List<SqlOperator> operatorList, SqlNameMatcher nameMatcher)
{
        // 首先查找 Calcite 原生的函数列表
        stdOperatorTable.lookupOperatorOverloads(opName, sqlFunctionCategory, syntax, operatorList, SqlNameMatchers.withCaseSensitive(false));

        // 如果 Calcite 中没有找到,则在咱们本人的函数列表中查找
        if (operatorList.isEmpty() && syntax == SqlSyntax.FUNCTION && opName.isSimple()) {List<SqlOperator> ops = opMap.get(opName.getSimple().toUpperCase(Locale.US));
            if (ops != null) {operatorList.addAll(ops);
            }
        }
    }

    @Override
    public List<SqlOperator> getOperatorList()
{return operators;}
}

第二步向 ListMultimap<String, SqlOperator> opMap 中注册函数。新建 DemoSqlOperatorImpl 类,继承 org.apache.calcite.sql.SqlFunction,作为自定义函数的实现类。

public class DemoSqlOperatorImpl
        extends SqlFunction
{
    private final boolean isDeterministic;
    private final boolean isDynamic;
    private final SqlSyntax syntax;

    public DemoSqlOperatorImpl(String name,
                                 boolean isDeterministic,
                                 boolean isDynamic,
                                 SqlReturnTypeInference sqlReturnTypeInference,
                                 SqlSyntax syntax,
                                 SqlOperandTypeChecker operandTypeChecker)
{
        super(name,
                new SqlIdentifier(name, SqlParserPos.ZERO),
                SqlKind.OTHER_FUNCTION,
                sqlReturnTypeInference,
                null,
                operandTypeChecker,
                SqlFunctionCategory.USER_DEFINED_FUNCTION);
        this.isDeterministic = isDeterministic;
        this.isDynamic = isDynamic;
        this.syntax = syntax;
    }

    @Override
    public SqlSyntax getSyntax()
{return syntax;}

    @Override
    public boolean isDeterministic()
{return isDeterministic;}

    @Override
    public boolean isDynamicFunction()
{return isDynamic;}
}

初始化 函数名、函数入参和返回值 等根本信息,new 出 org.apache.calcite.sql.SqlFunction,增加到 ”opMap” 中。

public void registerFunction()
    {
        String functionName = "SUBSTR";
        boolean isDeterministic = true;
        boolean isDynamic = false;
        // 生成函数的入参
        //Presto SUBSTR 函数有多种实现,这里只注册了 substr(string, start) → varchar 这种实现
        List<SqlTypeFamily> argumentTypes = ImmutableList.of(SqlTypeFamily.CHARACTER, SqlTypeFamily.NUMERIC);
        // 用于函数入参匹配和查看
        SqlOperandTypeChecker checker = OperandTypes.family(argumentTypes);、// 函数返回值
        SqlReturnTypeInference returnTypeInference = ReturnTypes.CHAR;
        org.apache.calcite.sql.SqlFunction sqlFunction = new DemoSqlOperatorImpl(functionName,
                isDeterministic,
                isDynamic,
                returnTypeInference,
                SqlSyntax.FUNCTION,
                checker);
        opMap.put(functionName, sqlFunction);
        operators.add(sqlFunction);
    }

至此验证阶段的函数扩大就实现了,能够通过简略的验证:

String sql = "select substr('abcdefg', 2)";
SqlParser parser = SqlParser.create(sql, SqlParser.config());
SqlNode sqlNode = parser.parseQuery();
SqlValidator sqlValidator = SqlValidatorUtil.newValidator(demoOperatorTable, catalogReader, javaTypeFactoryImpl, SqlValidator.Config.DEFAULT);
sqlValidator.validate(sqlNode);

整个函数注册和校验流程大抵如下图:

细节局部能够跟踪 Calcite 源码:org.apache.calcite.sql.validate.SqlValidator#validate。

下面例子只是非常简单的函数验证过程的扩大,在 理论的生产环境中还会面临以下问题:

  • 自定义的函数如何执行?如果是个常量如何进行常量折叠?
  • 如果函数的入参类型 Calcite 不反对,如何在 Calcite 中自定义数据类型?

这些问题解决起来会更加简单,并且还会对 Calcite 外围的关系代数 改写阶段产生肯定影响。

总结

Calcite 是一个宏大且简单的数据管理框架,SqlParser 和 SqlValidator 只是其中一小部分,并不是“重头戏”,Calcite 在此着墨不多,留下了许多扩大的路径。上文通过两个 demo 简略介绍了 SqlParser 和 SqlValidator 的原理与扩大。这部分 绝大状况下只在 Calcite 的根底上进行扩大即可,不须要批改源码,对 Calcite 自身的影响较小。

后续咱们还会介绍更多 Calcite 原理技术与实战,钱能够带来高兴,玩技术也能够!如果你对Calcite 原理技术、数据虚拟化、湖仓平台、数据库、SQL 优化器等相干技术感兴趣的话,欢送关注“Aloudata 技术团队”公众号。

✎ 本文作者 / 淳译,Aloudata OLAP 引擎开发工程师,参加 Aloudata AIR Engine 的多个外围模块开发,目前负责 Aloudata 数据虚拟化引擎的 SQL 层、元数据和多源异构引擎集成等相干工作。

正文完
 0