一、概述

SparkSQL 的 Parser 模块负责将 SQL 解析为 LogicalPlan,把整个 SparkSQL Parser 模块分为 构建抽象语法树遍历抽象语法树 两个部分去学习😊~~~

二、Parser 模块架构设计

2.1. 架构设计

2.1.1. ParseInterface

Catalyst 中提供了直接面向用户的 ParseInterface 接口,该接口中包含了对 SQL 语句、Expression 表达式和 TableIdentifier 数据表标识符的解析方法。AbstractSqlParser 是实现了 ParseInterface 的虚类,其中定义了返回 AstBuilder 的函数。

2.1.2. AbstractSqlParser

  1. SparkSqlParser

    SparkSQL 的 SparkSqlParser,将 SQL 解析为 LogicalPlan。

  2. CatalystSqlParser

CatalystSqlParser 用于 Catalyst 内部,而 SparkSqlParser 用于外部调用

2.1.3. SqlBaseBaseVisitor

Spark 在 AstBuilder 中重写了 SqlBaseBaseVisitor 的一些访问方法,这个类主要适用于 Catalyst 内部调用,而外部调用则由 AstBuilder 的子类 SparkSqlAstBuilder 负责。

不论是 SparkSqlParser 还是 CatalystSqlParser,都是对访问者 SqlBaseVisitor (SparkSqlParser 中是SparkSqlAstBuilder,CatalystSqlParser中是AstBuilder) 的一个封装而已,底层干活的都是它~

三、实现

3.1. 构建抽象语法树

Spark 提供了一个 .g4 文件,编译的时候会使用 Antlr 根据这个 .g4 生成对应的词法分析类和语法分析类,同时还使用了访问者模式,用以构建语法树。

3.2. 遍历语法树

3.2.1. Visitor

Spark SQL 编译器中主要采用 Visitor 方式,在遍历过程中,对每个元素都实施 $accept()$ 方法,在每个元素的 $accept()$ 方法中回调访问者的 $visit$ 方法,从而使访问者针对对象结构设计不同的具体访问者类来完成不同的操作。实现 Visitor 中的关键逻辑,直接调用 ANTLR4 生成的各个模块来访问语法分析器解析得到的语法树,最后返回结果。

  1. AstBuilder

    AstBuilder,继承了 ANTLR4 生成的默认 SqlBaseBaseVisitor,用于生成 SQL 对应的抽象语法树 AST

    • $visitSingleStatement$

      语法树中 SingleStatementContext 是根节点,但是在访问该节点时一般什么都不做,只递归访问子节点。

      1
      2
      3
      override def visitSingleStatement(ctx: SingleStatementContext): LogicalPlan = withOrigin(ctx) {
      visit(ctx.statement).asInstanceOf[LogicalPlan]
      }
      • ctx.statement

        调用 $getRuleContext$ 方法查找 SingleStatementContext 的子节点中第一个类型是 StatementContext 的节点

        1
        2
        3
        public StatementContext statement() {
        return getRuleContext(StatementContext.class,0);
        }
      • visit

        开始遍历获取的 StatementContext 节点

        1
        2
        3
        public T visit(ParseTree tree) {
        return tree.accept(this);
        }

        $ctx.statement$ 返回 StatementContext,StatementContext 并没有实现 $accept$ 方法。

        visit

        调用其父类的父类 RuleContext 的 $accept()$ 方法

        1
        2
        3
        public <T> T accept(ParseTreeVisitor<? extends T> visitor) { 
        return visitor.visitChildren(this);
        }

        接着 $RuleContext.accept()$ 调用 $AstBuilder.visitChildren()$:

        1
        2
        3
        4
        5
        6
        7
        override def visitChildren(node: RuleNode): AnyRef = {
        if (node.getChildCount == 1) {
        node.getChild(0).accept(this)
        } else {
        null
        }
        }

        返回 QueryContext,并调用其 $accept()$ 方法~

    • $visitQuery$

      在 query 语句下,$StatementContext—>RuleContext.accept()$ 调用 $AstBuilder.visitChildren()$ 返回 QueryContext,并继续调用其 $accept()$ 方法~

      Query 语法定义:

      1
      2
      3
      query
      : ctes? queryTerm queryOrganization
      ;

      visitQuery:

      1
      2
      3
      4
      override def visitQuery(ctx: QueryContext): LogicalPlan = withOrigin(ctx) {
      val query = plan(ctx.queryTerm).optionalMap(ctx.queryOrganization)(withQueryResultClauses)
      query.optionalMap(ctx.ctes)(withCTE)
      }
  2. SparkSqlAstBuilder

    SparkSqlAstBuilder 继承 AstBuilder,并在其基础上定义了一些 DDL 语句的访问操作,主要在 SparkSqlParser 中调用。

3.2.2. Listener

  1. PostProcessor

  2. UnclosedCommentProcessor

四、总结

当面临开发新的语法支持时,首先需要改动的是 ANTLR4 文件(在 SqlBase.g4 中添加文法), 重新生成词法分析器(SqlBaseLexer)、语法分析器( SqlBaseParser)和访问者类( SqlBase Visitor 接口与 SqlBaseBaseVisitor 类),然后在 AstBuilder 等类中添加相应的访问逻辑,最后添加执行逻辑。