基于 Calcite 自定义 SQL 解析器

这本应该是《我也能写数据库》系列文章中的一篇,但是最近一直在反思这个系列标题是不是有点不亲民,所以,暂时放弃这个系列标题了。

本文会介绍如何扩展Calcite的SQL解析器使之更符合你的业务需求,或是特殊的语法需求,以前的文章里我们介绍过如何撰写UDF,其实这些都是对SQL进行扩展,只是我们今天会对SQL的结构进行扩展。用一句简单的话说,就是如何定义属于你自己的SQL语法。

Calcite 使用 javacc作为语法解析器,并且使用freemarker作为模板引擎,在编译的时候,freemarker会将配置文件与模板语法文件以及附加文件整体生成最终的语法文件,并通过javacc编译,形成calcite的语法文件。其整个过程如下图所示

下面,我们将从一个简单案例入手,

select ids, name from test where id < 5

是一条正常的SQL,我们要加入关键字 jacky job ,形成一个新的sql语法

jacky job  'select ids, name from test where id < 5'

并且,使之可以正常解析。

构建maven工程

这里注意,需要将编译插件配置好,主要包括freemarker和javacc,否则会出现文件找不到,或是类找不到等奇怪问题,下面是我的pom文件片段

    
        
            org.apache.maven.plugins
            maven-compiler-plugin
            3.2
            
                1.8
                1.8
            
        
        
            org.codehaus.mojo
            javacc-maven-plugin
            
                
                    javacc
                    
                        javacc
                    
                    
                        ${project.build.directory}/generated-sources/fmpp
                        
                            **/Parser.jj
                        
                        2
                        false
                    
                
                
                    javacc-test
                    generate-test-sources
                    
                        javacc
                    
                    
                        ${project.build.directory}/generated-test-sources/fmpp
                        ${project.build.directory}/generated-test-sources/javacc
                        
                            **/Parser.jj
                        
                        2
                        false
                    
                
            
        
        
            org.apache.drill.tools
            drill-fmpp-maven-plugin
            
                
                    
                        src/main/codegen/config.fmpp
                        ${project.build.directory}/generated-sources/fmpp
                        src/main/codegen/templates
                    
                    generate-fmpp-sources
                    validate
                    
                        generate
                    
                
            
        
    

复制模板文件

从calcite源码包中,将code\src\main\codegen下所有文件复制到自己的代码路径下

写解析类

创建SqlJacky类,包路径为 org.apache.calcite.sql  因为,SqlJacky需要继承SqlNode类,而该类没有public构造函数。

package org.apache.calcite.sql;
import org.apache.calcite.sql.parser.SqlParserPos;
import org.apache.calcite.sql.util.SqlVisitor;
import org.apache.calcite.sql.validate.SqlValidator;
import org.apache.calcite.sql.validate.SqlValidatorScope;
import org.apache.calcite.util.Litmus;
public class SqlJacky extends SqlNode {
    private String jackyString;
    private SqlParserPos pos;
    public  SqlJacky(SqlParserPos pos, String jackyString){
        super(pos);
        this.pos = pos;
        this.jackyString = jackyString;
    }

    public String getJackyString(){
        System.out.println("getJackyString");
        return this.jackyString;
    }

    @Override
    public SqlNode clone(SqlParserPos sqlParserPos) {
        System.out.println("clone");
        return null;
    }

    @Override
    public void unparse(SqlWriter sqlWriter, int i, int i1) {
        sqlWriter.keyword("jacky");
        sqlWriter.keyword("job");
        sqlWriter.print("\n");
        sqlWriter.keyword("" + jackyString + "");
    }

    @Override
    public void validate(SqlValidator sqlValidator, SqlValidatorScope sqlValidatorScope) {
        System.out.println("validate");
    }

    @Override
    public  R accept(SqlVisitor sqlVisitor) {
        System.out.println("accept");
        return null;
    }

    @Override
    public boolean equalsDeep(SqlNode sqlNode, Litmus litmus) {
        System.out.println("equalsDeep");
        return false;
    }
}

在这个解析类里面,其实我们并没有做很多工作,只是在构造器里面,将变量保存起来。

需要注意的是这个方法,unparse ,这里用于解析显示用的,我们将关键字输出出来。

修改config.fmpp文件

找到

package: "org.apache.calcite.sql.parser.impl",

将下方的class,替换成一个你自己的类名,后面会用到。例如

class: "JackySqlParserImpl",

修改Parser.jj文件

首先需要在import的地方引入上面的解析类

import org.apache.calcite.sql.SqlJacky;

然后再后处理代码中加入解析逻辑

SqlNode SqlJacky() :
{
     SqlNode stringNode;
}
{
     
    stringNode = StringLiteral()
    {
        return new SqlJacky(getPos(), token.image);
    }
}

接下来 找到声明语句的方法

SqlNode SqlStmt() :

|
    stmt = SqlJacky()

加入到适当的位置。

最后在

 TOKEN :

的地方将,jacky 和 job 关键字加入

|   
|   

由于这个文件比较大,这里就不能贴完整的代码了,下面的连接中,有参考案例。

编译

执行maven的编译命令

测试

在构建测试的时候,注意将自己的解析解析类设置好,即在fmpp里设置的类名

 .setParserFactory(JackySqlParserImpl.FACTORY)

完整测试代码如下

package cn.flinkhub;
import org.apache.calcite.avatica.util.Casing;
import org.apache.calcite.avatica.util.Quoting;
import org.apache.calcite.schema.SchemaPlus;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.tools.FrameworkConfig;
import org.apache.calcite.tools.Frameworks;
import org.apache.calcite.sql.parser.impl.JackySqlParserImpl;
public class CustomParser {
    public static void main(String[] args) {
        SchemaPlus rootSchema = Frameworks.createRootSchema(true);
        final FrameworkConfig config = Frameworks.newConfigBuilder()
                .parserConfig(SqlParser.configBuilder()
                        //.setLex(Lex.ORACLE)
                        .setParserFactory(JackySqlParserImpl.FACTORY)
                        .setCaseSensitive(false)
                        .setQuoting(Quoting.BACK_TICK)
                        .setQuotedCasing(Casing.TO_UPPER)
                        .setUnquotedCasing(Casing.TO_UPPER)
                        //.setConformance(SqlConformanceEnum.ORACLE_12)
                        .build())
                .build();
//        "jacky 'select ids, name from test where id < 5'";
        String sql = "jacky job  'select ids, name from test where id < 5'";
        SqlParser parser = SqlParser.create(sql, config.getParserConfig());
        try {
            SqlNode sqlNode = parser.parseStmt();
            System.out.println(sqlNode.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

执行结果

到这里,解析的部分我们就做完了,后续我计划写一些执行计划相关的文章,让这个语法用起来。

研究calcite的时间有限,有错误的地方欢迎大家勘误。同时也希望对calcite有兴趣的小伙伴和我交流。

鸣谢:这个demo主要参考了 余启大神 的代码,受益匪浅。

参考连接:

https://blog.csdn.net/ccllcaochong1/article/details/93367343

https://github.com/yuqi1129/calcite-test

https://github.com/quxiucheng/apache-calcite-tutorial/tree/a7d63273d0c7585fc65ad250c99a67a201bcb8b5