• 扩展 Calcite 中的 SQL 解析语法


    Calcite中 JavaCC 的使用方法

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

    1)使用 JavaCC 解析器

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

    
        org.apache.calcite
        calcite-core
        1.26.0
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在代码中,可以直接使用 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());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    2)自定义语法

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

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

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

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

    Load语句示例:

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

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

    一种是直接修改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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

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

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

    /**
     * 定义Load语句中的数据源信息
     */
    @Data
    @AllArgsConstructor
    public class SqlLoadSource {
        private SqlIdentifier type;
        private String obj;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    为了输出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 + "'");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    当需要的 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语法没有多余分支结构,“一条线下去”,获取相应位置的内容并保存到变量中
    
        {
            pos = getPos();
        }
        
    sourceType = CompoundIdentifier()
    
     // 冒号和圆括号在Calcite原生的解析文件里已经定义,我们也能使用
        sourceObj = StringLiteralValue()
    
        targetType = CompoundIdentifier()
    
        targetObj = StringLiteralValue()
        {
            mapPos = getPos();
        }
    
        {
            colMapping = new SqlNodeList(mapPos);
            colMapping.add(readOneColMapping());
        }
        (
    
            {
                colMapping.add(readOneColMapping());
            }
        )*
        
    
    [ 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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    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/
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    3.编译模板文件和语法文件

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

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

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

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

    
        maven-resources-plugin
        
    		
                initialize
                
                    copy-resources
                
            
        
        
        
            ${basedir}/target/codegen
            
                
                    src/main/codegen
                    false
                
            
        
    
    
    
        
        org.apache.maven.plugins
        maven-dependency-plugin
    		2.8
        
            
                unpack-parser-template
                initialize
                
                    unpack
                
                
                    
                        
                            org.apache.calcite
                            calcite-core
                            1.26.0
                            jar
                            true
                            ${project.build.directory}/
                            **/Parser.jj
                        
                    
                
            
    			
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

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

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

    在这里插入图片描述

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

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

    
        
            ${project.build.directory}/codegen/config.fmpp
            target/generated-sources
            
                ${project.build.directory}/codegen/templates
            
        
        com.googlecode.fmpp-maven-plugin
        fmpp-maven-plugin
        1.0
        
            
                org.freemarker
                freemarker
                2.3.28
            
        
        
    				
                generate-fmpp-sources
                generate-sources
                
                    generate
                
            
        
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28

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

    在这里插入图片描述

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

    JavaCC插件配置方式:

    
        org.codehaus.mojo
        javacc-maven-plugin
        2.6
        
            
                generate-sources
                javacc
                
                    javacc
                
                
                    
                        ${basedir}/target/generated-sources/
                    
                    
                        **/Parser.jj
    								
                    2
                    false
                    ${basedir}/src/main/java
                
            
        
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    注意这里的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());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

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

    通过unparse方法输出的结果:

    LOAD 'hdfs': '/data/user.txt' TO 'mysql': 'db.t_user' 
    ('c1' 'c2', 'c3' 'c4') 
    SEPARATOR ','
    
    • 1
    • 2
    • 3
  • 相关阅读:
    大学生餐饮主题网页制作 美食网页设计模板 学生静态网页作业成品 dreamweaver美食甜品蛋糕HTML网站制作
    学习SDN开发工具
    彻底理解进程
    2.1、基于并行上下文注意网络的场景文本图像超分辨率
    文心一言 vs GPT-4 —— 全面横向比较
    netty源码看不懂?试着写一个吧
    Shiro学习笔记(四):Shiro中的授权
    多肽计算符计算:modlamp 包
    uniapp中使用axios打包到小程序时报 TypeError: adapter is not a function
    七个 LLM 的狼人杀之夜;马斯克的星链残骸会“砸死人”?OpenAI 安全漏洞曝光丨RTE开发者日报 Vol.66
  • 原文地址:https://blog.csdn.net/m0_50186249/article/details/134015441