Calcite 解析层详解

news2024/10/6 2:24:15
1、概述

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

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

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

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

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

2)语法解析的执行架构

其中主要分为3个阶段:

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

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

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

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

3、Calcite中的解析体系

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

1)抽象语法树的概念

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

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

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

在这里插入图片描述

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

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

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

2)SqlNode体系
1.概述

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

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

在这里插入图片描述

2.SqlLiteral

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

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

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

在这里插入图片描述

3.SqlIdentifier

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

4.SqlCall

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

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

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树。

在这里插入图片描述

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():
{}
{
    (
        <SELECT> {System.out.println("if else select");}
        |
        <UPDATE>  {System.out.println("if else update");}
        |
        <DELETE>  {System.out.println("if else delete");}
        |
        {System.out.println("other");}
    )
}

// while 0~n
void while1Expr():
{}
{
    (<SELECT>)*
}

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

// 入口
void SelectExpr(String sql) :
{
    int res; // 声明变量-结果变量
}
{
    <SELECT> // 以SELECT开始
    res = Expression() // 处理计算表达式会获得结果
    {
        System.out.println(sql + "=" + res); // 输出结果
    }
}
// 计算表达式
int Expression() :
{
		// 声明变量
    int res = 0;
    int v;
}
{
    res = Number() // 获得第一个数
    (
        <ADD> // 加号
        v = Number() // 获得第二个数
        {res += v;} // 计算结果
|
        <SUB> // 减号
        v = Number()
        {res -= v;}
    )*
    {return res;} // 返回结果
}

int Number() :
{
    Token t;
}
{
    t = <NUMBER> 
    {
        return Integer.parseInt(t.image); // 将字符串转换成整数
    }
}
d)关键字定义

每一个关键字都由一个TOKEN构成,SKIP用于指定在解析时需要跳过的字符,每个TOKEN用尖括号标识,多个TOKEN之间用竖线分隔。

尖括号里用冒号分隔,冒号前面是变量名,冒号后面是定义该变量的正则表达式。

本节示例需要定义数字NUMBER,为了简单,示例中并未处理不能以0开始的数字,其余符号都只有一个单词,具体的定义方法下所示。

JavaCC 中关键字的定义方法:

SKIP:{ // 跳过制表符
    " "
    | "\t"
    | "\n"
    | "\r"
    | "\r\n"
}
TOKEN :
{
    < SELECT: "SELECT" >
|   < NUMBER: (["0"-"9"])+ >
|   < ADD: "+" >
|   < SUB: "-" >
}
e) JavaCC 编译生成解析代码

现在代码编写完成,需要借助 JavaCC 编译才能生成解析代码。

对于 Java 可以使用Maven插件,不用单独下载 JavaCC。

可以用 Maven 来加载相关的依赖,具体的坐标写法如下。

JavaCC代码生成的插件坐标:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>javacc-maven-plugin</artifactId>
    <version>2.6</version>
    <executions>
        <execution>
            <phase>generate-sources</phase>
            <id>javacc</id>
            <goals>
							<goal>javacc</goal>
            </goals>
            <configuration>
                <sourceDirectory>${basedir}/src/main/javacc</sourceDirectory>
                <includes>
                    <include>**/*.jj</include>
                </includes>
                <outputDirectory>${basedir}/generated-sources/</outputDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>

Java CC编译命令:

mvn org.codehaus.mojo:javacc-maven-plugin:2.6:javacc

运行命令后会在 target/generated-sources/javacc 中生成包和代码,使用时,只需要调用解析器主类即可。

在这里插入图片描述

f)解析器主类调用
final SimpleSelectParser parser = new SimpleSelectParser("select 1+1+1");

// select 1+1+1=3
parser.parse(); 
5、Calcite中 JavaCC 的使用方法

Calcite 默认采用 JavaCC 来生成词法分析器和语法分析器。

1)使用 JavaCC 解析器

Calcite中,JavaCC 的依赖已经被封装到 calcite-core 模块当中,如果使用 Maven 作为依赖管理工具,只需要添加对应的calcite-core模块坐标即可。

<dependency>
    <groupId>org.apache.calcite</groupId>
    <artifactId>calcite-core</artifactId>
    <version>1.26.0</version>
</dependency>

在代码中,可以直接使用 Calcite 的 SqlParser 接口调用对应的语法解析流程,对相关的 SQL 语句进行解析。

解析流程:

// SQL语句
String sql = "select * from t_user where id = 1";

// 解析配置
SqlParser.Config mysqlConfig = SqlParser.config().withLex(Lex.MYSQL);

// 创建解析器
SqlParser parser = SqlParser.create(sql, mysqlConfig);

// 解析SQL语句
SqlNode sqlNode = parser.parseQuery();

System.out.println(sqlNode.toString());
2)自定义语法

有时需要扩展一些新的语法操作,以数仓的操作——Load作为例子,介绍如何自定义语法。

Load操作时将数据从一种数据源导入另一种数据源中,Load操作采用的语法模板如下。

LOAD sourceType:obj TO targetType:obj 
(fromCol toCol (,fromCol toCol)*) 
[SEPARATOR '\t']

其中,sourceType 和 targetType 表示数据源类型,obj表示这些数据源的数据对象,(fromCol toCol)表示字段名映射,文件里面的第一行是表头,分隔符默认是制表符。

Load语句示例:

LOAD hdfs:'/data/user.txt' TO mysql:'db.t_user' (name name,age age) SEPARATOR ',';

在真正实现时,有两种选择。

一种是直接修改Calcite的源码,在其本身的模板文件(Parser.jj)内部添加对应的语法逻辑,然后重新编译。

但是这种方式的弊端非常明显,即对Calcite本身的源码侵入性太强。

另一种利用模板引擎来扩展语法文件,模板引擎可将扩展的语法提取到模板文件外面,以达到程序解耦的目的。

在实现层面,Calcite用到了FreeMarker,它是一个模板引擎,按照FreeMarker定义的模板语法,可以通过其提供的 Java API 设置值来替换模板中的占位符。

如下展示了 Calcite 通过模板引擎添加语法逻辑相关的文件结构,其源码将 Parser.jj 这个语法文件定义为模板,将 includes 目录下的.ftl文件作为扩展文件,最后统一通过config.fmpp来配置。

在这里插入图片描述

具体添加语法的操作可以分为3个步骤

编写新的 JavaCC 语法文件;

修改config.fmpp文件,配置自定义语法;

编译模板文件和语法文件。

1.编写新的 JavaCC 语法文件

不需要修改Parser.jj文件,只需要修改includes目录下的.ftl文件,对于前文提出的Load操作,只需要在parserImpls.ftl文件里增加Load对应的语法。

在编写语法文件之前,先要从代码的角度,用面向对象的思想将最终结果定下来,也就是最后希望得到的一个SqlNode节点。

抽象Load语句内容并封装后,得到SqlLoad,继承SqlCall,表示一个操作,Load操作里的数据源和目标源是同样的结构,所以封装SqlLoadSource,而字段映射可以用一个列表来封装,SqlColMapping仅仅包含一堆列映射,SqlNodeList代表节点列表。

扩展SqlLoad的代码实现:

// 扩展SqlLoad的代码实现
public class SqlLoad extends SqlCall {
    // 来源信息
    private SqlLoadSource source;
    // 终点信息
    private SqlLoadSource target;
    // 列映射关系
    private SqlNodeList colMapping;
    // 分隔符
    private String separator;

    // 构造方法
    public SqlLoad(SqlParserPos pos) {
        super(pos);
    }
		
		// 扩展的构造方法
    public SqlLoad(SqlParserPos pos, 
                   SqlLoadSource source, 
                   SqlLoadSource target, 
                   SqlNodeList colMapping,
                   String separator) {
        super(pos);
        this.source = source;
        this.target = target;
        this.colMapping = colMapping;
        this.separator = separator;
    }
}

由于Load操作涉及两个数据源,因此也需要对数据源进行定义。

Load语句中数据源的定义类:

/**
 * 定义Load语句中的数据源信息
 */
@Data
@AllArgsConstructor
public class SqlLoadSource {
    private SqlIdentifier type;
    private String obj;
}

Load语句中出现的字段映射关系也需要定义。

对Load语句中的字段映射关系进行定义:

// 对Load语句中的字段映射关系进行定义
public class SqlColMapping extends SqlCall {
    // 操作类型
    protected static final SqlOperator OPERATOR =
            new SqlSpecialOperator("SqlColMapping", SqlKind.OTHER);
    private SqlIdentifier fromCol;
    private SqlIdentifier toCol;
    
    public SqlColMapping(SqlParserPos pos) {
        super(pos);
		}
		
    // 构造方法
    public SqlColMapping(SqlParserPos pos, 
                         SqlIdentifier fromCol, 
                         SqlIdentifier toCol) {
        super(pos);
        this.fromCol = fromCol;
        this.toCol = toCol;
    }
}

为了输出SQL语句,还需要重写unparse方法。

unparse方法定义:

/**
 * 定义unparse方法
 */
@Override
public void unparse(SqlWriter writer, int leftPrec, int rightPrec) {
    writer.keyword("LOAD");
		source.getType().unparse(writer, leftPrec, rightPrec);
		
    writer.keyword(":");
    writer.print("'" + source.getObj() + "' ");
    writer.keyword("TO");
    target.getType().unparse(writer, leftPrec, rightPrec);
    
    writer.keyword(":");
    writer.print("'" + target.getObj() + "' ");
    
    final SqlWriter.Frame frame = writer.startList("(", ")");
    for (SqlNode n : colMapping.getList()) {
        writer.newlineAndIndent();
        writer.sep(",", false);
        n.unparse(writer, leftPrec, rightPrec);
    }
    
    writer.endList(frame);
    writer.keyword("SEPARATOR");
    writer.print("'" + separator + "'");
}

当需要的 SqlNode 节点类定义好后,就可以开始编写语法文件了,Load语法没有多余分支结构,只有列映射用到了循环,可能有多个列。

parserImpls.ftl文件中添加语法逻辑的代码示例:

// 节点定义,返回我们定义的节点
SqlNode SqlLoad() :
{
    SqlParserPos pos; // 解析定位
    SqlIdentifier sourceType; // 源类型用一个标识符节点表示
    String sourceObj; // 源路径表示为一个字符串,比如“/path/xxx”
    SqlIdentifier targetType;
    String targetObj;
    SqlParserPos mapPos;
    SqlNodeList colMapping;
    SqlColMapping colMap;
    String separator = "\t";
}
{
// LOAD语法没有多余分支结构,“一条线下去”,获取相应位置的内容并保存到变量中
<LOAD>
    {
        pos = getPos();
    }
    
sourceType = CompoundIdentifier()

<COLON> // 冒号和圆括号在Calcite原生的解析文件里已经定义,我们也能使用
    sourceObj = StringLiteralValue()
<TO>
    targetType = CompoundIdentifier()
<COLON>
    targetObj = StringLiteralValue()
    {
        mapPos = getPos();
    }
<LPAREN>
    {
        colMapping = new SqlNodeList(mapPos);
        colMapping.add(readOneColMapping());
    }
    (
<COMMA>
        {
            colMapping.add(readOneColMapping());
        }
    )*
    
<RPAREN>
[<SEPARATOR> separator=StringLiteralValue()]

// 最后构造SqlLoad对象并返回
    {
        return new SqlLoad(pos, new SqlLoadSource(sourceType, sourceObj),
               new SqlLoadSource(targetType, targetObj), colMapping, separator);
    }
}

// 提取出字符串节点的内容函数
JAVACODE String StringLiteralValue() {
    SqlNode sqlNode = StringLiteral();
    return ((NlsString) SqlLiteral.value(sqlNode)).getValue();
}

SqlNode readOneColMapping():
{
    SqlIdentifier fromCol;
    SqlIdentifier toCol;
    SqlParserPos pos;
}
{
    { pos = getPos();}
    fromCol = SimpleIdentifier()
		toCol = SimpleIdentifier()
    {
        return new SqlColMapping(pos, fromCol, toCol);
    }
}
2.修改config.fmpp文件,配置自定义语法

需要将 Calcite 源码中的 config.fmpp 文件复制到项目的 src/main/codegen 目录下,然后修改里面的内容,来声明扩展的部分。

config.fmpp文件的定义示例:

data: {
    parser: {
        # 生成的解析器包路径
        package: "cn.com.ptpress.cdm.parser.extend",
        # 解析器名称
        class: "CdmSqlParserImpl",
				# 引入的依赖类
        imports: [
            "cn.com.ptpress.cdm.parser.load.SqlLoad",
            "cn.com.ptpress.cdm.parser.load.SqlLoadSource"
            "cn.com.ptpress.cdm.parser.load.SqlColMapping"
        ]
        # 新的关键字
        keywords: [
            "LOAD",
            "SEPARATOR"
        ]
        # 新增的语法解析方法
        statementParserMethods: [
            "SqlLoad()"
        ]
        # 包含的扩展语法文件
        implementationFiles: [
            "parserImpls.ftl"
        ]
    }
}
# 扩展文件的目录
freemarkerLinks: {
    includes: includes/
}
3.编译模板文件和语法文件

在这个过程当中,需要将模板Parser.jj文件编译成真正的Parser.jj文件,然后根据Parser.jj文件生成语法解析代码。

利用Maven插件来完成这个任务,具体操作可以分为2个阶段:初始化和编译。

初始化阶段通过resources插件将codegen目录加入编译资源,然后通过dependency插件把calcite-core包里的Parser.jj文件提取到构建目录中。

编译所需插件的配置方式:

<plugin>
    <artifactId>maven-resources-plugin</artifactId>
    <executions>
		<execution>
            <phase>initialize</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
        </execution>
    </executions>
    
    <configuration>
        <outputDirectory>${basedir}/target/codegen</outputDirectory>
        <resources>
            <resource>
                <directory>src/main/codegen</directory>
                <filtering>false</filtering>
            </resource>
        </resources>
    </configuration>
</plugin>

<plugin>
    <!--从calcite-core.jar提取解析器语法模板,并放入FreeMarker模板所在的目录-->
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
		<version>2.8</version>
    <executions>
        <execution>
            <id>unpack-parser-template</id>
            <phase>initialize</phase>
            <goals>
                <goal>unpack</goal>
            </goals>
            <configuration>
                <artifactItems>
                    <artifactItem>
                        <groupId>org.apache.calcite</groupId>
                        <artifactId>calcite-core</artifactId>
                        <version>1.26.0</version>
                        <type>jar</type>
                        <overWrite>true</overWrite>
                        <outputDirectory>${project.build.directory}/</outputDirectory>
                        <includes>**/Parser.jj</includes>
                    </artifactItem>
                </artifactItems>
            </configuration>
        </execution>
			</executions>
</plugin>

这2个插件可以通过“mvn initialize”命令进行测试。

运行成功后可以看到target目录下有了codegen目录,并且多了本没有编写的Parser.jj文件。

在这里插入图片描述

然后就是编译阶段,利用FreeMarker模板提供的插件,根据config.fmpp编译Parser.jj模板,声明config.fmpp文件路径模板和输出目录,在Maven的generate-resources阶段运行该插件。

FreeMarker在pom.xml文件中的配置方式:

<plugin>
    <configuration>
        <cfgFile>${project.build.directory}/codegen/config.fmpp</cfgFile>
        <outputDirectory>target/generated-sources</outputDirectory>
        <templateDirectory>
            ${project.build.directory}/codegen/templates
        </templateDirectory>
    </configuration>
    <groupId>com.googlecode.fmpp-maven-plugin</groupId>
    <artifactId>fmpp-maven-plugin</artifactId>
    <version>1.0</version>
    <dependencies>
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.28</version>
        </dependency>
    </dependencies>
    <executions>
				<execution>
            <id>generate-fmpp-sources</id>
            <phase>generate-sources</phase>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
</plugin>

运行“mvn generate-resources”命令就可以生成真正的Parser.jj文件。

在这里插入图片描述

最后一步就是编译语法文件,使用JavaCC插件即可完成。

JavaCC插件配置方式:

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>javacc-maven-plugin</artifactId>
    <version>2.6</version>
    <executions>
        <execution>
            <phase>generate-sources</phase>
            <id>javacc</id>
            <goals>
                <goal>javacc</goal>
            </goals>
            <configuration>
                <sourceDirectory>
                    ${basedir}/target/generated-sources/
                </sourceDirectory>
                <includes>
                    <include>**/Parser.jj</include>
								</includes>
                <lookAhead>2</lookAhead>
                <isStatic>false</isStatic>
                <outputDirectory>${basedir}/src/main/java</outputDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>

注意这里的I/O目录,直接将生成的代码放在了项目里。

看起来上面每个阶段用了好几个命令,其实只需要一个Maven命令即可完成所有步骤,即“mvn generate-resources”,该命令包含以上2个操作,4个插件都会被执行。

完成编译后,就可以测试新语法,在测试代码里配置生成的解析器类,然后写一条简单的Load语句。

4.测试Load语句的示例代码
String sql = "LOAD hdfs:'/data/user.txt' TO mysql:'db.t_user' (c1 c2,c3 c4) SEPARATOR ','";

// 解析配置
SqlParser.Config mysqlConfig = SqlParser.config()
        // 使用解析器类
        .withParserFactory(CdmSqlParserImpl.FACTORY)
        .withLex(Lex.MYSQL);

SqlParser parser = SqlParser.create(sql, mysqlConfig);

SqlNode sqlNode = parser.parseQuery();

System.out.println(sqlNode.toString());

输出的结果正是重写的unparse方法所输出的。

通过unparse方法输出的结果:

LOAD 'hdfs': '/data/user.txt' TO 'mysql': 'db.t_user' 
('c1' 'c2', 'c3' 'c4') 
SEPARATOR ','

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1136741.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

建筑木模板厂家直销 915*1830*15mm酚醛面板规格

建筑木模板在建筑施工中扮演着重要的角色&#xff0c;它是支撑混凝土浇筑和保证建筑结构稳定性的关键材料。作为一家专业的建筑木模板厂家直销商&#xff0c;我们引以为傲地推出了915*1830*15mm酚醛面板规格的产品。 我们的建筑木模板采用高质量的酚醛树脂胶粘剂和优质桉木木材…

低代码平台是什么意思?低代码平台如何设计与实现?

低代码这个词&#xff0c;也许许多人都相当陌生。低代码的正式提出可以追溯到2014年&#xff0c;当时全球最具影响力的独立研究咨询公司Forrester&#xff0c;正式界定了低代码的概念。低代码指可通过最少的手工编程就能快速交付应用程序&#xff0c;并能快速设置和部署用于参与…

centos启动tomcat 并指定jdk 版本

在tomcat的catalina.sh文件手动设置JAVA_HOME变量即可 例如&#xff1a; 前提是文件存在 保存配置重新启动tomcat

短说通用版V4.1.0测试版发布|新增全新马甲模块等新功能

大家好&#xff0c; 我是给你们带来惊喜的运营小番茄。 本期更新为短说通用版 4.1.0测试版。 本次V4.1.0版本新增功能有&#xff1a; ①学院免费课程支持发布评价 ②商城子系统商品支持使用积分抵扣 ③新增管理后台查看和导出评论功能 ④支持设置积分类型展示排序功能 …

记录一次时序数据库的实战测试

0x1.前言 ​ 本文章仅用于信息安全防御技术分享&#xff0c;因用于其他用途而产生不良后果&#xff0c;作者不承担任何法律责任&#xff0c;请严格遵循中华人民共和国相关法律法规&#xff0c;禁止做一切违法犯罪行为。文中涉及漏洞均以提交至教育漏洞平台。 0x2.背景 ​ 在某…

99%的时间里使用的14个git命令

学习14个Git命令&#xff0c;因为你将会在99%的时间里使用它们 必须了解的命令整理 1&#xff0c;git init 初始化一个新的Git仓库。 这将在当前目录中创建一个名为".git"的子目录&#xff0c;Git会将所有仓库的元数据存储在其中。 2&#xff0c;git clone 克隆…

【unity3D】Rect Transform组件

&#x1f497; 未来的游戏开发程序媛&#xff0c;现在的努力学习菜鸡 &#x1f4a6;本专栏是我关于游戏开发的学习笔记 &#x1f236;本篇是unity的Rect Transform组件 Rect Transform组件 基础知识详细介绍补充 基础知识 Rect Transform是Unity中的一个UI组件&#xff0c;用于…

商淘云:如何选择开源B2B2C多用户商城系统

选择开源B2B2C多用户商城系统是一个关键的决策&#xff0c;因为它将直接影响到您的电子商务平台的性能、功能和用户体验。以下是一些指导原则&#xff0c;可帮助您做出明智的选择。 首先&#xff0c;考虑系统的稳定性和安全性 选择一个经过广泛测试和验证的开源系统&#xff0…

node读取文件和获取路径

01.fs模块-读写文件 模块&#xff1a;类似插件&#xff0c;封装了方法和属性供我们使用 fs 模块&#xff1a;封装了与本机文件系统进行交互的&#xff0c;方法和属性 fs 模块使用语法如下&#xff1a;【fs模块为node自带&#xff0c;不需要手动安装】 加载 fs 模块&#xff0…

nginx解决vue项目开发跨域问题

1、为了模拟跨域的开发情况&#xff0c;本地可以起一个后台服务 const http require(http);const PORT 3200;// 创建一个 http 服务 const server http.createServer((request, response) > {response.end("hello world"); });// 启动服务, 监听端口 server.l…

Centos安装mongodb

mongodb官网 下载MongoDB cd /optwget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-4.4.2.tgztar -xf mongodb-linux-x86_64-rhel70-4.4.2.tgz -C /usr/local/cd /usr/local/mv mongodb-linux-x86_64-rhel70-4.4.2 mongodb部署Mongodb 2.1. 创建目录 cd mo…

木马文件检测系统 毕业设计 JAVA+Vue+SpringBoot+MySQL

项目编号&#xff1a;S041&#xff0c;源码已在 Bilibili 中上架&#xff0c;需要的朋友请自行下载。 https://gf.bilibili.com/item/detail/1104375029为了帮助小白入门 Java&#xff0c;博主录制了本项目配套的《项目手把手启动教程》&#xff0c;希望能给同学们带来帮助。 …

Python 自带小型数据库详解

DBM DBM&#xff08;DataBase Manager&#xff09;是一种文件系统&#xff0c;专门用于键值对的存储&#xff0c;最初是在 Unix 平台实现&#xff0c;现在其它平台也可以用。对于 KV 模型&#xff0c;DBM 提供了一个轻量级、高效的存储解决方案。 总的来说&#xff0c;DBM 具有…

借助软文,让品牌形象深入人心

品牌形象有多重要&#xff1f;独特的品牌形象能够提升市场竞争力&#xff0c;建立消费者的品牌忠诚度&#xff0c;就像蜜雪冰城的品牌形象就是实惠好喝&#xff0c;而软文作为一种推广手段&#xff0c;就能帮助企业塑造品牌形象&#xff0c;引起读者共鸣&#xff0c;今天媒介盒…

【反射】Java反射机制 -- 常用构造器与方法

个人简介&#xff1a;Java领域新星创作者&#xff1b;阿里云技术博主、星级博主、专家博主&#xff1b;正在Java学习的路上摸爬滚打&#xff0c;记录学习的过程~ 个人主页&#xff1a;.29.的博客 学习社区&#xff1a;进去逛一逛~ Java反射 Java反射1. 获取class对象的三种方式…

Linux - 进程的优先级 和 如何使用优先级调度进程

理解linux 当中如何做到 把一个PCB 放到多个 数据结构当中 在Linux 当中&#xff0c;一个进程的 PCB 不会仅仅值存在一个 数据结构当中&#xff0c;他既可以在 某一个队列当中&#xff0c;又可以在 一个 多叉树当中。 队列比如 cpu 的 运行队列&#xff0c;键盘的阻塞队列等等…

性能测试用例和测试结果

性能测试用例和测试结果 一 核心业务功能的TPS测试1.1 登录接口测试用例1.2 进入首页接口测试用例1.3 添加购物车接口测试用例1.4 结算和下订单接口测试用例1.5 系统资源使用率1.6 单接口测试中一个测试的各个成员接口要单独做性能统计 二 业务流程&#xff08;多接口组合&…

word行内插入mathtype 公式后行距变大解决办法

现象 word行内插入mathtype 公式后行距变大 解决方法 选中要进行操作的那些行&#xff0c;依次单击菜单命令“格式→段落”&#xff0c;打开“段落”对话框&#xff1b;单击“缩进和间距”选项卡&#xff0c;将间距的“段前”和“段后”都调整为“0行”&#xff1b;将“如果…

中国密码算法与NIST标准对比

1. 引言 NIST定义AES为标准的对称密钥加密算法。但NIST被指出可能在加密算法中添加NSA后门。为此&#xff0c;在中国&#xff0c;ShāngM (SM) 系列密码算法&#xff0c;作为TLS 1.3集成和无线认证的备选方案&#xff1a; SM2&#xff1a;定义了认证&#xff08;签名&#xf…

薅!语雀致歉送6个月会员;万字教程讲透AI视频生成;提示词14个黄金设计法则;吴恩达AI职业规划指南 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f525; 讯飞发布星火认知大模型 V3.0&#xff0c;并推出多款大模型产品 10月24日&#xff0c;科大讯飞正式发布「星火认知大模型V3.0」&#…