Calcite 解析层详解

1、概述

用户的操作请求经过服务层的接收和封装被传递给calcite-core模块。

其中第一站就是解析层,它的作用主要是对SQL语句进行语法解析。

在这个过程中,初始的SQL字符串会被转化为Calcite内部的语法解析节点,为进一步的语法校验和优化做准备。

2、语法解析过程
1)概述

语法解析是利用词法分析器、语法分析器将输入的语句通过一些预定的规则解析为抽象语法树的过程。

2)语法解析的执行架构

其中主要分为3个阶段:

1.首先字符串处理器会将源语句中的字符串转换成字符流;

2.然后词法分析器会对字符流中的一些词法进行匹配,形成词组(Token)流;

3.最后由语法分析器将这些词组流进行语义逻辑的理解,转变为最终的抽象语法树。

在这个过程当中,还有两个维护组件,一个是负责维持词法和语法匹配逻辑的表格管理器,另一个是负责检查语法错误的异常监听器。

Calcite 解析层详解_第1张图片

3、Calcite中的解析体系

对于数据管理系统,语法解析主要针对的是将SQL语句解析成抽象语法树的过程

1)抽象语法树的概念

语法解析的最终结果是一棵抽象语法树,它以树状的形式表现出语法结构,树上的每个节点都表示源码中的一种结构。

如果给计算机输入的指令是“(1+2)*3”,那么经过语法解析以后就会生成抽象语法树,其中圆形节点表示叶子节点,一般是参数,方形节点表示非叶子节点,一般是操作符。

抽象语法树将纯文本转换为一棵树,其中每个节点对应代码中的一种结构,例如上述的表达式转换为源码中的结构的形式。

Calcite 解析层详解_第2张图片

同理,输入的一条SQL语句也会生成一棵抽象语法树,例如select id from table where id > 1。

Calcite 解析层详解_第3张图片

这棵树的每个节点仅仅是对语法的抽象,并未对应到相应的源码结构当中。

因此为了能够匹配每个节点相应的源码结构,Calcite构建了它的SqlNode体系来完成这项任务。

2)SqlNode体系
1.概述

SqlNode是负责封装语义信息的基础类,是Calcite中非常重要的概念,不只是解析阶段,也和后续的校验、优化息息相关,它是所有解析节点的父类。

在Calcite中SqlNode的实现类有40多个,每个类都代表一个节点到源码结构的映射,其大致可以分为3类,即SqlLiteral、SqlIdentifier、SqlCall。

Calcite 解析层详解_第4张图片

2.SqlLiteral

SqlLiteral类主要封装输入的常量,也被称作字面量。

它和它的子类一般用来封装具体的变量值,也可以通过调用getValue方法返回所需要的值。

为了实现其通用性,Calcite支持了很多数据类型,展示了当前版本SqlLiteral可以表示的常量类型。

Calcite 解析层详解_第5张图片

3.SqlIdentifier

SqlIdentifier代表输入的标识符,例如在一条SQL语句中表的名称、字段名称,都可以封装成一个SqlIdentifier对象。

4.SqlCall

每一个操作都可以对应一个SqlCall,如查询是SqlSelect,插入是SqlInsert。

为了更加细粒度地介绍Calcite是如何使用SqlCall的子类来封装操作的,以负责查询的SqlSelect为例,介绍SqlCall内部具体是如何封装操作的。

Calcite 解析层详解_第6张图片

a)SqlSelect中包含的属性以及常量
/**
 * 封装查询操作的SqlSelect节点
 */
public class SqlSelect extends SqlCall {

    public static final int FROM_OPERAND = 2;
    public static final int WHERE_OPERAND = 3;
    public static final int HAVING_OPERAND = 5;

    SqlNodeList keywordList;
    // 查询字段列表
    @Nullable SqlNodeList selectList;
    // 数据源信息
    @Nullable SqlNode from;
    // 过滤条件信息
    @Nullable SqlNode where;
    // 分组信息
    @Nullable SqlNodeList groupBy;
    @Nullable SqlNode having;
    SqlNodeList windowDecls;
    @Nullable SqlNodeList orderBy;
    @Nullable SqlNode offset;
    @Nullable SqlNode fetch;
    @Nullable SqlNodeList hints;
}

通过观察SqlSelect的成员变量,可以发现在SqlSelect当中封装了数据源信息(FROM子句)、过滤条件信息(WHERE子句)、分组信息(GROUP BY子句)等查询信息。

当SQL语句是一条查询语句的时候,会生成一个SqlSelect节点,在这个节点下面封装了SQL语句当中每一个关键的参数。

同理,在负责插入数据的SqlInsert中,在SqlInsert中封装了目标表信息(targetTable)、源信息(source)、字段对应信息(columnList),基本上将插入数据时需要的信息都囊括了进来。

b)SqlInsert中包含的属性以及常量
public class SqlInsert extends SqlCall {
    public static final SqlSpecialOperator OPERATOR =
        new SqlSpecialOperator("INSERT", SqlKind.INSERT);

    SqlNodeList keywords;
    SqlNode targetTable;
    SqlNode source;
    @Nullable SqlNodeList columnList;
}
c)那么SqlNode中的各个类是如何工作的呢?

如下SQL包含字段的投影(id)、数据源的制定(t)、查询过滤条件(id>1)以及分组条件(id)。

select
    id
from t
where id > 1 

经过Calcite的SqlNode规范化,最终形成SqlNode树。

Calcite 解析层详解_第7张图片

4、JavaCC
1)JavaCC简介

JavaCC 代码生成器,它的作用就是生成在语法解析过程中的词法分析器和语法分析器。

通过模板文件(例如.jj文件、.jjt文件以及.jtb文件)来生成Java程序,Calcite利用这些Java程序来完成语法解析的工作。

2)JavaCC简单示例
1.示例

解析一条 select 1+1 查询语句,把它加起来并输出结果,select 1+1输出2,select 2+3输出5。

JavaCC 主要实现都在.jj文件中。

2.JavaCC中定义语法的模板
options {
    JavaCC的配置项
}
PARSER_BEGIN(解析器类名)

package包名;
import库名;
public class解析器类名 {
    任意Java代码
}

PARSER_END(解析器类名)
a)options

options是解析配置项,格式为键值对key=value。

比如在解析时忽略大小写:IGNORE_ CASE = true。

JavaCC配置模板

options {
		// 大小写配置成不敏感的状态
    IGNORE_CASE = true;
    // STATIC代表生成的解析器类是否为静态类,默认是true,我们需要它可以多次初始化,所以设为false。
    STATIC = false;
} 
b)PARSER声明

PARSER声明是PARSER_BEGIN和PARSER_END之间的部分,这部分完全是Java代码,同时只有一个类,这个类就是解析器类。

解析器类,代表解析的入口,其输入是要解析的内容,可以用程序允许的方式输入,比如字符串参数、文件或数据流,而输出则看实现情况,比如简单的计算器直接在解析完时就计算好了,复杂的SQL语句解析会生成一棵抽象语法树。

解析器类和普通类类似,不过在生成代码时JavaCC会自动为其生成一些构造方法,可以输入字符流和字节流,所以 SimpleSelectParser可以直接调用字符流构造方法。

解析器类一般作为被调用的入口类,传入要解析的字符内容,然后调用parse方法开始解析,我们声明的SQL属性用来保存传入的SQL。

JavaCC中的代码模板:

PARSER_BEGIN(SimpleSelectParser)

package cn.com.ptpress.cdm.parser.select;

import java.io.* ;

public class SimpleSelectParser {
    private String sql;
    
    public void parse() throws ParseException {
        SelectExpr(sql);
    }
    
    public SimpleSelectParser(String expr) {
        this((Reader)(new StringReader(expr)));
        this.sql = expr;
    }
}

PARSER_END(SimpleSelectParser)
c)解析逻辑

解析逻辑部分由代码和表达式构成,可以分为2种代码:纯Java代码和解析逻辑代码。

i)纯Java代码

纯 Java 代码以 JAVACODE 关键字开始,后面就是 Java 代码里方法的声明,内容也只限于 Java 代码,这些方法的作用就是供解析代码调用,比如匹配前缀。

这些解析逻辑是可选的,其本质是公共方法抽取,当然也可能整个语法文件都没有抽出一个方法。

JavaCC中纯Java代码:

JAVACODE boolean matchPrefix(String prefix, String[] arr) {
    for (String s : arr) {
        if (s.startsWith(prefix)) {
            return true;
        }
    }
    return false;
}
ii)解析逻辑代码

解析逻辑代码,其构成多了冒号和冒号后面的花括号然后才是方法体

程序的基本构成是变量、语句、分支结构、循环结构等,这几个简单元素组合起来就可以构成很复杂的程序。

JavaCC的解析逻辑代码和纯Java代码的最大区别是:可以嵌入JavaCC的语法。

首先,这些代码在结构上看起来像方法,不过其由两个花括号构成,第一个花括号里声明变量,第二个花括号里写逻辑,同时方法名后面还有一个冒号,以此和纯粹的方法区分开。

解析逻辑代码和纯 Java 代码类似,都有分支、循环,只是看起来不像代码,其结构类似正则表达式,还可以混入Java代码。

在循环结构中,用正则表达分支逻辑时,会用到圆括号和竖线

如(a|b|c),在 JavaCC 里,a、b、c可以换成解析逻辑:关键字+代码处理。

关键字就是后面要讲的常量字符定义,代码处理就是走到某个词语后执行什么操作,这里仅仅输出一句话。

关键字和代码处理,就是最小构成,整个结构可以无限递归。

JavaCC中循环逻辑代码:

// if - else
void ifElseExpr():
{}
{
    (
        )*
}

关于查询表达式的简单示例:

// 入口
void SelectExpr(String sql) :
{
    int res; // 声明变量-结果变量
}
{