基于 Antlr4 的 Sql 解析

政采云技术团队.png

元宿.png



    在实际业务中,涉及到 SQL 解析的场景非常多,常见的诸如 MySQL 之类的 SQL 解析有像阿里的 Druid 之类的工具可以解析,但目前各种技术栈推出、更新迅速,经常会遇到某些 SQL 用常规 SQL 解析工具无法解析的情况,此类场景,可自定义借助 Antlr4 实现解析。
    在本文中,对 Antlr4 不再做详细介绍,仅借助 Spark Sql 做示例,通过 Antlr4 解析 SQL 解决项目中遇到的一些问题。

一、常见业务场景

1、鉴权

解析 SQL 获取对应的表名、字段。

2、数据血缘

解析 SQL 获取输入表、输出表,输入字段、输出字段。

基于 Antlr4,在解析 SQL 获取字段相关的信息时,以下两种情况是无法解析的:

  1. SQL 中涉及到 * 的查询,诸如 select * from test_tab ,在此,单纯从语法解析角度而言无法得知*具体涉及到哪些字段。

  2. 语义歧义,如:select t1.col1 ,col2 from tab1 t1 left join tab2 t2 on t1.id=t2.id ,col2 来自于 tab2 表,由于在 tab1 表中不存在 col1 中,从最终执行来看该 SQL 不会报错,且查询结果正确,但单纯从 SQL 解析的角度而言,无法得知 col2 字段来源于 tab2 表。

    诸如问题 1、2 产生的问题,当然也可以通过查询表元数据获取到具体字段信息,但是实际中有更好的解决办法,像 Spark Sql,在执行过程中,我们可以获取它的物理计划(SparkPlan),通过获取物理计划即可获取相应的字段信息。

3、其他场景

    输入一个表达式,判断该表达式类型,是否为窗口函数或者聚合函数等。
    注:该场景是曾经做某系统,业务代码涉及 SQL 拼接,需要判断当前查询是否为聚合函数,在 SQL 拼接中,普通字段条件过滤和聚合字段条件过滤是不一样的,普通字段条件过滤用 where,而聚合字段则用 having

二、IDEA 中 Antlr4 的环境部署

1、Antlr4 插件安装

image.png

2、获取语法文件

如 Spark2.4,语法文件在 /spark/sql/catalyst/src/main/antlr4/org/apache/spark/sql/catalyst/parser 路径,文件后缀为 g4。 image (1).png

3、pom.xml 依赖引入

<antlr4.version>4.10.1</antlr4.version><dependency>
   <groupId>org.antlr</groupId>
   <artifactId>antlr4-runtime</artifactId>
   <version>${antlr4.version}</version>
</dependency><plugin>
   <groupId>org.antlr</groupId>
   <artifactId>antlr4-maven-plugin</artifactId>
   <version>${antlr4.version}</version>
       <executions>
           <execution>
               <goals>
                  <goal>antlr4</goal>
               </goals>
               <phase>none</phase>
           </execution>
       </executions>
       <configuration>
           <sourceDirectory>src/main/scala/cn/zcy/antlr4/spark/g4</sourceDirectory>
                <includes>
                    <include>SparkSqlBaseLexer.g4</include>
                    <include>SparkSqlBaseParser.g4</include>
                </includes>
                <visitor>true</visitor>
                <listener>true</listener>
        </configuration>
 </plugin>

4、生成源码文件

(1)右键配置生成文件相关信息 image (2).png image (3).png (2)生成文件 image (4).png SparkSql.tokens:antlr4 给每个词法符号指定一个数字形式的类型,它们的对应关系存在该文件中 SparkSqlListener、SparkSqlBaseListener:语法分析器会将文本转换为一个语法树,遍历语法树的时候,会触发一系列的事件回调,SparkSqlListener 接口给出了这些回调方法的定义,SparkSqlBaseListener 则是 SparkSqlListener 的默认实现类。 SparkSqlVisitor、SparkSqlBaseVisitor:Visitor 模式的工具包。 SparkSqlLexer:词法分析器的类定义;语法中的文法规则和词法规则,主要靠这个识别。 SparkSqlParser:包含语法分析器类的定义。

SparkSqlListener 和 SparkSqlVisitor 的区别: Antlr 访问语法树有 Visitor 和 Listener 两种模式,Listener 模式中,每个节点包含 enter 和 exit 两个方法,会解析语法树的各个节点,Visitor 模式则是根据语法树遍历访问。

三、代码实现

1、语法树查看

(1)从 SparkSql.g4 文件中可得知 statement 为根节点,通过测试规则,可以查看对应 SQL 的语法树。 image (5).png image (6).png

2、代码解析

以下仅对解析 SQL 中的表名做示例。

import cn.zhengcaiyun.idata.connector.parser.spark.SparkSqlLexer;
import cn.zhengcaiyun.idata.connector.parser.spark.SparkSqlParser;
import cn.zhengcaiyun.idata.connector.util.model.UpperCaseCharStream;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.atn.PredictionMode;
​
public class SparkSqlHelper {
    public static void main(String[] args) {
        String sql = "SELECT T1.COL1,T2.COL2 \n" +
                "FROM TAB1 T1 \n" +
                "LEFT JOIN TAB2 T2 ON T1.ID=T2.ID";
        System.out.println(SparkSqlHelper.parse(sql));
    }
​
    public static List<String> parse(String sql) {
        UpperCaseCharStream charStream = new UpperCaseCharStream(CharStreams.fromString(sql));
​
        //新建一个词法分析器,解析创建的流
        SparkSqlLexer lexer = new SparkSqlLexer(charStream);
        //新建一个词法符号的缓冲器,存储词法分析器生成的词法符号
        CommonTokenStream tokenStream = new CommonTokenStream(lexer);
        //新建语法解析器,处理词法符号缓冲器中的内容
        SparkSqlParser parser = new SparkSqlParser(tokenStream);
        //指定预测模式(1)
        parser.getInterpreter().setPredictionMode(PredictionMode.SLL);
        //创建解析器访问对应的方法
        SparkSqlAst visitor = new SparkSqlAst();
        return (List<String>) visitor.visit(parser.singleStatement());
    }
}
​

(1)预测模式 SLL:在进行预测时会忽略当前解析器上下文,解析器要么返回正确的解析树( LL 预测模式返回的解析树),要么报告语法错误。 LL:在实际实现中一般先用 SSL 模式,若抛 ParseCancellationException,则使用 LL 模式再重试。这种模式在输入的语法组合正确的情况下,保证解析的结果正确;若语法模糊,则无法保证准确结果。 LL_EXACT_AMBIG_DETECTION:除了提供同 LL 模式一样的正确性保证外,为解析过程中遇到的每个歧义决策确定完整而准确的歧义备选项集;这种预测模式不为语法错误的输入的预测行为提供任何保证。

import cn.zcy.antlr4.spark.parser.SparkSqlBaseVisitor;
import cn.zcy.antlr4.spark.parser.SparkSqlParser;
​
public class SparkSqlAst extends SparkSqlBaseVisitor {
    private List<String> tableList = new ArrayList<>();
    
    @Override
    public List<String> visitSingleStatement(SparkSqlParser.SingleStatementContext ctx) {
        super.visitSingleStatement(ctx);
        return tableList;
    }
​
    /**
     * 通过对语法树的观察可得知,表名的获取可以通过该方法来获取
     *
     * @param ctx
     * @return
     */
    @Override
    public List<String> visitTableIdentifier(SparkSqlParser.TableIdentifierContext ctx) {
        String db = ctx.db == null ? "" : ctx.db.getText();
        String tableName = ctx.table == null ? "" : ctx.table.getText();
​
        tableList.add("".equals(db) ? tableName : (db + "." + tableName));
        return null;
    }
}
​

执行结果: image (7).png

参考:《ANTLR4 权威指南》,特恩斯.帕尔 著,张博 译

推荐阅读

指标体系的设计和思考

redis 性能分享

基于gitlab ci_cd实现代码质量管理

sharding-jdbc 分享

用户路径分析

招贤纳士

政采云技术团队(Zero),一个富有激情、创造力和执行力的团队,Base 在风景如画的杭州。团队现有 500 多名研发小伙伴,既有来自阿里、华为、网易的“老”兵,也有来自浙大、中科大、杭电等校的新人。团队在日常业务开发之外,还分别在云原生、区块链、人工智能、低代码平台、中间件、大数据、物料体系、工程平台、性能体验、可视化等领域进行技术探索和实践,推动并落地了一系列的内部技术产品,持续探索技术的新边界。此外,团队还纷纷投身社区建设,目前已经是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等众多优秀开源社区的贡献者。如果你想改变一直被事折腾,希望开始折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的技术团队的成长过程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 zcy-tc@cai-inc.com

微信公众号

文章同步发布,政采云技术团队公众号,欢迎关注

政采云技术团队.png