扩展 Calcite 中的 SQL 解析语法

news2024/9/28 11:19:34
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/1130298.html

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

相关文章

PCL 视图变换(OpenGL)

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 在OpenGL中存在这样一个过程,即模拟人类看东西的过程,通过一种视图变换方式将物体置入观察空间内,以此让我们可以看到这个物体。这个过程有点类似于将一个照相机移到了模型前方的某个位置,然后再设置一下照相机…

【广州华锐互动】VR消防员模拟灭火:身临其境的火场救援

随着科技的不断发展&#xff0c;虚拟现实&#xff08;VR&#xff09;技术已经逐渐渗透到各个领域&#xff0c;为我们带来了前所未有的沉浸式体验。在这其中&#xff0c;VR模拟消防员灭火体验无疑是一种极具创新性和实用性的应用。通过这项技术&#xff0c;人们可以亲身体验到消…

RabbitMQ原理(五):消费者的可靠性

文章目录 3.消费者的可靠性3.1.消费者确认机制3.2.失败重试机制3.3.失败处理策略3.4.业务幂等性3.4.1.唯一消息ID3.4.2.业务判断 3.5.兜底方案 3.消费者的可靠性 当RabbitMQ向消费者投递消息以后&#xff0c;需要知道消费者的处理状态如何。因为消息投递给消费者并不代表就一定…

STM32 invalid UTF-8 in comment 警告解决办法

这里写自定义目录标题 STM32 invalid UTF-8 in comment 警告解决办法问题描述解决办法 STM32 invalid UTF-8 in comment 警告解决办法 问题描述 …/…/libraries/CMSIS/CM3/DeviceSupport/ST/STM32F10x\stm32f10x.h(18): warning: invalid UTF-8 in comment [-Winvalid-utf8]…

正点原子嵌入式linux驱动开发——Linux PWM驱动

PWM是很常用到功能&#xff0c;可以通过PWM来控制电机速度&#xff0c;也可以使用PWM来控制LCD的背光亮度。本章就来学习一下如何在Linux下进行PWM驱动开发。 PWM驱动解析 不在介绍PWM是什么了&#xff0c;直接进入使用。 给LCD的背光引脚输入一个PWM信号&#xff0c;这样就…

Node编写更新用户信息接口

目录 前言 定义路由和处理函数 验证表单数据 实现更新用户基本信息的功能 前言 继前面几篇文章&#xff0c;本文介绍如何编写更新用户信息接口 定义路由和处理函数 路由 // 更新用户信息接口 router.post(/userinfo, userinfo_handler.updateUserinfo) 处理函数 // 导…

Netty实战-实现自己的通讯框架

通信框架功能设计 功能描述 通信框架承载了业务内部各模块之间的消息交互和服务调用&#xff0c;它的主要功能如下&#xff1a; 基于 Netty 的 NIO 通信框架&#xff0c;提供高性能的异步通信能力&#xff1b;提供消息的编解码框架&#xff0c;可以实现 POJO 的序列化和反序…

Kmeans算法实现目标客户聚类分析

文章目录 一、Kmeans简介二、数据集描述三、实现方法一、Kmeans简介 Kmeans是聚类算法中较为简单的一种,简单但实用,有如下优势和缺点: 优势 算法简单,便于使用(算法仅需要考虑一个分类数量K即可) 适合常规数据集(最好是线性可分的数据集) 适合 不适合 缺点 K值难以确…

【COMP329 LEC4 Locomotion and Kinematics】

Only for the Test 1 which include 4.2 4.3 4.4 Locomotion and Kinematics 运动和运动学 (4.2) Part 2: Wheeled Motion 1. Wheeled Robots a. 省略控制双腿需要的计算复杂度 b. 只限于easy terrain &#xff08;地形&#xff09; c. 不平坦uneven 不规则irregular 的地形需要…

STM32-程序占用内存大小计算

STM32中程序占用内存容量 Keil MDK下Code, RO-data,RW-data,ZI-data这几个段: Code存储程序代码。 RO-data存储const常量和指令。 RW-data存储初始化值不为0的全局变量。 ZI-data存储未初始化的全局变量或初始化值为0的全局变量。 占用的FlashCode RO Data RW Data; 运行消…

Go语言用Resty库编写的音频爬虫代码

以下是一个使用Resty库的Go语言下载器程序&#xff0c;用于从facebook下载音频。此程序使用了duoip/get_proxy的代码。 package mainimport ("fmt""github.com/john-nguyen09/resty""io/ioutil""net/http" )func main() {// 设置爬虫i…

互联网金融 个人身份识别技术要求

文章目录 术语缩略语个人身份识别技术框架框架与各组部分的作用个人身份识别实现的主要功能 个人身份识别凭据技术要求概述记忆凭据类静态口令生成要求使用要求设备要求及安全要求 预设问题回答生成要求使用要求 OPT令牌生成要求使用要求安全要求 数字证书无硬介质证书生成要求…

kvm webvirtcloud 如何添加直通物理机的 USB 启动U盘

第一步&#xff1a;查看USB设备ID 在物理机上输入 lsusb 命令 rootubuntu:/media/usb1# lsusb Bus 002 Device 002: ID 0781:5581 SanDisk Corp. Ultra Bus 002 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hub Bus 001 Device 004: ID 0424:2514 Microchip Technolo…

【力扣】x (-x) 与 x (x - 1)

最近刷了很多位运算的题&#xff0c;从一开始的死记硬背x & (-x) 与 x & (x - 1)的含义&#xff0c;到现在彻底弄懂&#xff0c;花了很多时间理解。 前提知识&#xff1a; 内存中的计算都是按照补码进行计算的。正数的原反补相同&#xff0c;负数的补码等于原码取反加…

FOC系列(一)----DRV8301芯片的学习

一、 写在前面 从今年四五月份一直就想玩个无刷直流电机&#xff08;BLDC&#xff09;&#xff0c;但是碍于一直没时间。其实很早就做出来了测试板的控制板&#xff0c;可以当做开发板使用&#xff0c;考虑到成本问题&#xff0c;最后选用STM32F103CBT6芯片&#xff0c;下面是很…

边缘计算发生了什么?

边缘计算(Edge computing)成为一种革命性工具&#xff0c;可以满足日益增长的实时数据处理需求。通过在网络边缘&#xff08;更靠近数据生成位置&#xff09;进行数据处理&#xff0c;边缘计算可显着减少延迟和带宽使用。 这是我们多年来一直被告知的故事&#xff0c;但随着生…

听GPT 讲Rust源代码--library/std(2)

File: rust/library/std/src/sys_common/wtf8.rs 在Rust源代码中&#xff0c;rust/library/std/src/sys_common/wtf8.rs这个文件的作用是实现了UTF-8编码和宽字符编码之间的转换&#xff0c;以及提供了一些处理和操作UTF-8编码的工具函数。 下面对这几个结构体进行一一介绍&…

封装一个vue3 Toast组件,支持组件和api调用

先来看一段代码 components/toast/index.vue <template><div v-if"isShow" class"toast">{{msg}}</div> </template><script setup> import { ref, watch } from vue const props defineProps({show: {type: Boolean,def…

“/usr/bin/env: ‘python’: No such file or directory“:Linux中python口令无效,python3有效

文章目录 1. 问题的发现2. /usr/bin 目录里跟python有关的链接2.1 使用ll查看文件的链接2.2 分析python口令不能使用的原因 3 参考文章《linux 升级默认python 环境为python3》4 修改命令为python 1. 问题的发现 我在安装scons时&#xff0c;发现python口令不能直接用&#xf…

Unity之ShaderGraph如何实现水波纹效果

前言 今天我们来实现一个水波纹的效果 如下图所示: 主要节点 Normalize :返回输入 In 的标准化值。输出矢量与输入 In 具有相同的方向,但长度为 1。 Length:返回输入 In 的长度。这也称为大小 (magnitude)。矢量的长度是使用毕达哥拉斯定理 (Pythagorean Theorum) 计算…