Spark-sql[1]-antrl4的入門以及在spark中的實現

we are all in the gutter,but some of us are looking at the stars. --王爾德
Just For M

為了更方便分析人員使用平臺,越來越多的計算框架都實現了Sql接口,有的是類sql,有的標準的sql規范,其目的就是更好的服務于分析人員。比如 hive使用了antlr3實現了自己的HQL, Flink使用Apache Calcite,而Calcite的解析器是使用JavaCC實現的,Spark2.x以后采用了antlr4實現自己的解析器,Presto也是使用antlr4。而本文將對antlr4實現解析器做一個系統的解讀。

ANTLR導讀

如果想全面深入的學習ANTLR4,可以參考一下文章:
http://www.antlr.org/
The Definitive ANTLR 4 Reference

ANTLR概念

ANTLR能夠根據用戶定義的語法文件自動生成詞法分析器和語法分析器,并將輸入文本處理為語法分析樹。這一切都是自動進行的,
所需的僅僅是一份描述該語言的語法文件

ANTLR自動生成的編譯器高效、準備,能夠將開發者從繁雜的編譯理論中解放出來,集中精力處理自己的業務邏輯。ANTRL4引入的自動語法分析樹創建與遍歷機制,極大地提高了語言識別程序的開發效率。時至今日,仍然是Java世界中實現編譯器的不二之選,同時,它也對其他編程語言也提供了支持。

為了實現一門編程語言,我們需要構建一個程序,讀取輸入的語句,對其中的詞組和輸入符號進行正確的處理。

語言是由一些列有意義的語句組成,語句由詞組組成,詞組是由更小的子詞組和詞匯符號組成。A language is a set of valid sentences, a sentence is made up of phrases, and a phrase is made up of subphrases and vocabulary symbols.

如果一個程序能夠分析計算或者執行語句,我們就把它稱之為解釋器(interpreter)。解釋器需要識別出一門特定的語言的所有的有意義的語句,詞組和子詞組。識別一個詞組意味著我們可以將它從眾多的組成部分中辨認和區分出來。
比如我們會把 sp=100; 識別成賦值語句, 這意味著我們能夠辨識出sp是被賦值的目標,100則是要被賦予的值。我們也都知道我們在學習英語的時候,識別英語語句,需要辨認出一段對話的不同部分,例如主謂賓。在識別成功之后,程序還能執行適當的操作。
識別語言的程序被稱為語法分析器(parser)或者句法分析器(syntax analyzer), syntax 是指約束語言中的各個組成部分之間關系的規則。grammar是一系列規則的集合,每條規則表述出一種詞匯結構。ANTLR就是能夠將其轉成如同經驗豐富的開發者手工構建的一般的語法分析器(ANTLR是一個能夠生產其他程序的程序

ANTRL它本身語法又是遵循一種專門用來描述其他語言的語法。

ANTRL將語法分析的過程分解為兩個相似但獨立的任務,我們并不是一個字符一個字符地閱讀一個句子,而是將句子看作一列單詞。在識別整個句子的語法結構之前,人類的大腦首先通過潛意識將字符聚集為單詞,然后獲取每個單詞的意義。

第一個階段將字符聚集為單詞或者符號(token) 的過程稱為 詞法分析或者詞法符號化 (lexical analysis or simply tokenizing)
把輸入的文本轉換成詞法符號的程序稱為詞法分析器(lexer)

詞法分析器可以將相關的詞法符號歸類,例如 INT (integers), ID (identifiers), FLOAT (floating-point numbers)等等。如果接下來的語法分析器不關系單個符號,而是僅僅關系符號的類型時,詞法分析器就需要將 詞匯/符號歸類。 詞法符號包含至少兩個部分的信息: 詞法符號的類型該詞法符號對應的文本

第二個階段就是語法分析輸入的詞法符號被消費以識別語句結構

仍然以 sp=100; 為例。ANTRL生成的語法分析器會建造一種
稱為 a parse tree or syntax tree 語法分析樹或者句法樹的數據結構,該數據結構記錄了語法分析器識別輸入語句結構的過程,以及該結構的各組成部分。

image.png

語法分析樹的內部節點是 詞組名,這些名字用于識別它們的子節點,并可以將子節點歸類。根節點是比較抽象的一個名字,在這里是 stat(statement)

語法分析樹的葉子節點永遠是輸入的詞法符號。

句子,也即符號的線性組合,本質上是語法分析樹在人腦中的串行化。通過操作語法分析樹,識別同一種語言的不同程序就能服用同一個語法分析器。

為了編寫一個語言類的程序,我們必須對每個輸入的詞組或者子詞組 執行一些適當的操作。

進行這項工作最簡單的方式就是操作語法分析器自動幫我們生成的語法分析樹。

這種方式的優點是,我們能夠重新回到JAVA的領域。不需要再學習復雜的ANTRL語法。

image.png
ANTLR兩種遍歷分析樹的機制

默認情況下,ANTLR使用內建的遍歷器訪問生成的語法分析樹,并為每個遍歷時可能觸發的事件生成一個語法分析樹監聽器接口 (ANTLR generates a parse-tree listener interface) 。監聽器類似于XML解析器生成的SAX文檔對象。SAX監聽器接收類似startDocument和endDocument。
除了監聽器的方式,還有一種遍歷語法分析樹的方式:訪問者模式(vistor pattern)

  • Parse-Tree Listeners
    為了將遍歷樹時觸發的事件轉化為監聽器的調用,ANTLR提供ParseTreeWalker類。我們可以自行實現ParseTreeListener的接口,在其中填充自己的邏輯。ANTLR為每個語法文件生成一個ParseTreeListener的子類,在該類中,語法的每條規則都有對應的enter方法和exit方法。
image.png

(The other listener calls aren’t shown. )

image.png

監聽器方式的優點在于,回調是自動進行的。我們不需要編寫對語法分析樹的遍歷代碼,也不需要讓我們的監聽器顯式地訪問子節點

  • Parse-Tree Visitors
    有時候,我們希望控制遍歷語法分析樹的過程,通過顯式的方法調用來訪問子節點。語法中的每條規則對應接口中的一個visit方法。

代碼demo

ParseTree tree = ... ; // tree is result of parsing
MyVisitor v = new MyVisitor();
v.visit(tree);

VNTLR內部為訪問者模式提供的支持代碼會在根節點處調用visitStat方法,接下來,visitStat方法的實現將會調用visit方法,并將所用的子節點作為參數傳遞給它,從而繼續遍歷的過程

ANTLR應用實例
  • 實例一: 牛刀小試-識別包裹在花括號或者嵌套的花括號中的整數 {1,2,3} 和 {1,{2,3}}
    這里例子很簡單,我們需要寫的語法文件也比較簡單,但是我們可以通過這個簡單的語法文件來熟悉語法文件的結構,如果讀者有正則表達式經驗,書寫起來更加快捷:

    /** Grammars always start with a grammar header. This grammar   is called
     *  ArrayInit and must match the filename: ArrayInit.g4
     */
    grammar ArrayInit;
    
    /** A rule called init that matches comma-separated values   between {...}. */
    init  : '{' value (',' value)* '}' ;  // must match at least one value
    
    /** A value can be either a nested array/struct or a simple integer   (INT) */
    value : init
          | INT
          ;
    
    // parser rules start with lowercase letters, lexer rules with   uppercase
    INT :   [0-9]+ ;             // Define token INT as one or more digits
    WS  :   [ \t\r\n]+ -> skip ; // Define whitespace rule, toss it out
    
    • grammars 關鍵字必須與 .g4 文件同名, 如果一個語法文件太大可以拆分成多個文件,相互依賴就是依賴 import + 關鍵字 文件名 語句

    • 語法分析器的規則以小寫字母開頭( initvalue)

    • 詞法分析器的規則以大小字母開頭(INTWS)
      我們可以安裝antlr官網來簡單配置我們的運行環境:

      image.png

      運行命令

     antlr4 ArrayInit.g4
    

    可以生成一批 .java文件:

    image.png
    • ArrayInitLexer: 詞法解析器類識別我們語法中的文法規則和詞法規則

      image.png
    • ArrayInitParser: 語法解析器類

      image.png

    • ArrayInit.tokens: ANTLR會給每個我們定義的詞法符號指定一個數字形式的類型

    image.png
    • ArrayInitListener,ArrayInitBaseListener:監聽器類
    image.png
    • ArrayInitVisitor ,ArrayInitBaseVisitor:訪問者模式類
    image.png
    • 使用監聽器來實現把short數組初始化為字符串對象

      image.png

      我們需要做的翻譯過程包括:
      1.將 { => "。
      2.將 } => "。
      3.將每個整數表示為十六進制數并且加前綴 \u

    /** Convert short array inits like {1,2,3} to "\u0001\u0002\u0003" */
    public class ShortToUnicodeString extends ArrayInitBaseListener {
        /** Translate { to " */
        @Override
        public void enterInit(ArrayInitParser.InitContext ctx) {
            System.out.print('"');
        }
    
        /** Translate } to " */
        @Override
        public void exitInit(ArrayInitParser.InitContext ctx) {
            System.out.print('"');
        }
    
        /** Translate integers to 4-digit hexadecimal strings prefixed with   \\u */
        @Override
        public void enterValue(ArrayInitParser.ValueContext ctx) {
            // Assumes no nested array initializers
            int value = Integer.valueOf(ctx.INT().getText());
            System.out.printf("\\u%04x", value);
        }
    }
    

    監聽器編輯之后,我們下面就需要將其配置到分析樹上面

    import org.antlr.v4.runtime.*;
    import org.antlr.v4.runtime.tree.*;
    
    public class Translate {
        public static void main(String[] args) throws Exception {
            // create a CharStream that reads from standard input
            ANTLRInputStream input = new   ANTLRInputStream(System.in);
            // create a lexer that feeds off of input CharStream
            ArrayInitLexer lexer = new ArrayInitLexer(input);
            // create a buffer of tokens pulled from the lexer
            CommonTokenStream tokens = new   CommonTokenStream(lexer);
            // create a parser that feeds off the tokens buffer
            ArrayInitParser parser = new ArrayInitParser(tokens);
            ParseTree tree = parser.init(); // begin parsing at init rule
    
            // Create a generic parse tree walker that can trigger callbacks
            ParseTreeWalker walker = new ParseTreeWalker();
            // Walk the tree created during the parse, trigger callbacks
            walker.walk(new ShortToUnicodeString(), tree);
            System.out.println(); // print a \n after translation
        }
    }
    

  • 實例二:匹配算數表達式的語言-構建一個簡單的計算器,只允許基本的加減乘除、圓括號、整數以及變量出現且只允許整數出現

    193
    a = 5
    b = 6
    a+b*2
    (1+2)*3
    

    來看一下我們的語法文件:

    grammar Expr;
    
    /** The start rule; begin parsing here. */
    prog:   stat+ ;
    
    stat:   expr NEWLINE
        |   ID '=' expr NEWLINE
        |   NEWLINE
        ;
    
    expr:   expr ('*'|'/') expr
        |   expr ('+'|'-') expr
        |   INT
        |   ID
        |   '(' expr ')'
        ;
    
    ID  :   [a-zA-Z]+ ;      //匹配英語字母
    INT :   [0-9]+ ;         // 匹配整數
    NEWLINE:'\r'? '\n' ;     // 新的一行
    WS  :   [ \t]+ -> skip ; // 忽略空白字符
    
    
    • 使用訪問模式來實現
      為了更好的使用訪問者模式我們對上面的語法文件做些許修改:
    grammar LabeledExpr; // rename to distinguish from Expr.g4
    
    prog:   stat+ ;
    
    stat:   expr NEWLINE                # printExpr
        |   ID '=' expr NEWLINE         # assign
        |   NEWLINE                     # blank
        ;
    
    expr:   expr op=('*'|'/') expr      # MulDiv
        |   expr op=('+'|'-') expr      # AddSub
        |   INT                         # int
        |   ID                          # id
        |   '(' expr ')'                # parens
        ;
    
    MUL :   '*' ; // assigns token name to '*' used above in grammar
    DIV :   '/' ;
    ADD :   '+' ;
    SUB :   '-' ;
    ID  :   [a-zA-Z]+ ;      // match identifiers
    INT :   [0-9]+ ;         // match integers
    NEWLINE:'\r'? '\n' ;     // return newlines to parser (is end-statement   signal)
    WS  :   [ \t]+ -> skip ; // toss out whitespace
    
    
    1. 為不同的備選分支添加的了標簽(#MulDiv/#AddSub),如果沒有標簽,ANTLR是為每條規則來生成方法如果希望每個備選分支都有相應的方法來訪問,就可以像我這樣在右側加上#標簽。

      image.png

      2.怎么來實現屬于我們自己的訪問器類

    import java.util.HashMap;
    import java.util.Map;
    
    public class EvalVisitor extends   LabeledExprBaseVisitor<Integer> {
      /** "memory" for our calculator; variable/value pairs go here */
      Map<String, Integer> memory = new HashMap<String, Integer>  ();
    
      /** ID '=' expr NEWLINE */
      @Override
      public Integer visitAssign(LabeledExprParser.AssignContext ctx)   {
          String id = ctx.ID().getText();  // id is left-hand side of '='
          int value = visit(ctx.expr());   // compute value of expression   on right
          memory.put(id, value);           // store it in our memory
          return value;
      }
    
      /** expr NEWLINE */
      @Override
      public Integer visitPrintExpr(LabeledExprParser.PrintExprContext ctx) {
          Integer value = visit(ctx.expr()); // evaluate the expr child
          System.out.println(value);         // print the result
          return 0;                          // return dummy value
      }
    
      /** INT */
      @Override
      public Integer visitInt(LabeledExprParser.IntContext ctx) {
          return Integer.valueOf(ctx.INT().getText());
      }
    
      /** ID */
      @Override
      public Integer visitId(LabeledExprParser.IdContext ctx) {
          String id = ctx.ID().getText();
          if ( memory.containsKey(id) ) return memory.get(id);
          return 0;
      }
    
      /** expr op=('*'|'/') expr */
      @Override
      public Integer visitMulDiv(LabeledExprParser.MulDivContext   ctx) {
          int left = visit(ctx.expr(0));  // get value of left subexpression
          int right = visit(ctx.expr(1)); // get value of right subexpression
          if ( ctx.op.getType() == LabeledExprParser.MUL ) return left *   right;
          return left / right; // must be DIV
      }
    
      /** expr op=('+'|'-') expr */
      @Override
      public Integer visitAddSub(LabeledExprParser.AddSubContext ctx) {
          int left = visit(ctx.expr(0));  // get value of left subexpression
          int right = visit(ctx.expr(1)); // get value of right subexpression
          if ( ctx.op.getType() == LabeledExprParser.ADD ) return left +   right;
          return left - right; // must be SUB
      }
    
      /** '(' expr ')' */
      @Override
      public Integer visitParens(LabeledExprParser.ParensContext ctx) {
          return visit(ctx.expr()); // return child expr's value
      }
    }
    

    如果進入了 visitAssign方法說明我們進入了標簽#assign

    image.png

    結構很簡單,是一個復制的語句,ID符號內的文本是被賦值的變量,expr所代表的值是要賦值的數。我們對 expr的分析樹進行進行分析,我們發現expr的所有的分支都相應的方法可以訪問visitIntvisitIdvisitMulDivvisitAddSubvisitParens,假如進入的分支是#int

    image.png

    因為 INT代表的就是具體的值,我們把它獲取出來既可!

    return Integer.valueOf(ctx.INT().getText());
    

    稍微復雜一點的可能是 標簽 #MulDiv、#AddSub因為,需要根 據操作符op* 來進一步判斷進行什么操作。
    3.現在我們已經擁有了我們自己的訪問器EvalVisitor,接下來要做的就是將我們的訪問器作用于我們的分析樹上

    import org.antlr.v4.runtime.*;
    import org.antlr.v4.runtime.tree.ParseTree;
    
    import java.io.FileInputStream;
    import java.io.InputStream;
    
    public class Calc {
       public static void main(String[] args) throws Exception {
           String inputFile = null;
           if ( args.length>0 ) inputFile = args[0];
           InputStream is = System.in;
           if ( inputFile!=null ) is = new FileInputStream(inputFile);
           ANTLRInputStream input = new ANTLRInputStream(is);
           LabeledExprLexer lexer = new LabeledExprLexer(input);
           CommonTokenStream tokens = new   CommonTokenStream(lexer);
           LabeledExprParser parser = new LabeledExprParser(tokens);
           ParseTree tree = parser.prog(); // parse
    
           EvalVisitor eval = new EvalVisitor();
           eval.visit(tree);
       }
    }
    

至此,基本的ANTLR的介紹可以告一段落,當然也有一些高級的語法,比如,我們需要朝ANTLR自動生成的java代碼中增加額外的方法,我們可以直接對生產的java文件進行操作,或者在語法文件中使用 @parser::members {高級語法進行添加。。。
如果大家仍然有興趣,可以參考 json官網Java語言規范:基于Java SE 8中對 jsonjava規范使用ANTLR來完成我們自己的解析器。

Spark中的sql解析過程

image.png

spark-sql工程是在spark-core上擴展的包,使用戶可以使用sql或者dsl來實現自己的spark應用,上圖描述的是原生的sql是怎么在spark-sql的一步步解析之下化作我們熟悉的RDD
本文的關注點主要是第一步: spark是如何解讀sql成自己熟悉的LogicalPlan,這一步操作在 spark2.x針對在spark1.x中的邏輯,進行了重構:

spark1.x的解析分為兩個部分

  • 其一是使用 scala自帶的 scala.util.parsing.combinator.PackratParsers來定義自己的規則
  • 另一部分如果是HQL則調用hive driver的解析器來獲取分析樹,然后再翻譯這里的分析樹

spark2.x 則使用antlr4重新寫了自己的語法文件,統一了一個入口,也借助antlr4提高了解析效率

想要了解spark1.x是如何進行解析的可以參考文章:
Spark Sql源碼解讀
Spark SQL Catalyst源碼分析之SqlParser

通過上面antlr的學習,我們已經了解到,開發這樣一個解釋器,我們需要的因素:

  • 語法文件(SqlBase.g4)
  • 監視器類或者訪問者類
    在spark-sql的體系中,主要是使用訪問者類(SparkSqlAstBuilder),但是也使用了監聽器類輔助(PostProcessor)來處理格式轉換。
image.png
image.png

image.png

用戶輸入的sqlText通過sessionState.sqlParser.parsePlan(sqlText)傳遞給上圖中配置了監聽器訪問類的分析樹,輸出程序所需要的LogicalPlan

Spark的語法文件

spark-sql的語法文件SqlBase.g4放置在子工程catalyst中,大概有1000多行代碼,spark的語法文件是從facebook的presto中改進過來的。大家可以參考presto的語法規范
presto-parser/src/main/antlr4/com/facebook/presto/sql/parser/SqlBase.g4
databricks也為我們整理所有的spark-sql的說明
/latest/spark-sql/index.html
通過上面的 antlr語法的學習,我們讀取SqlBase.g4也是更加清晰的。

grammar SqlBase;

@members {
  /**
   * Verify whether current token is a valid decimal token (which contains dot).
   * Returns true if the character that follows the token is not a digit or letter or underscore.
   *
   * For example:
   * For char stream "2.3", "2." is not a valid decimal token, because it is followed by digit '3'.
   * For char stream "2.3_", "2.3" is not a valid decimal token, because it is followed by '_'.
   * For char stream "2.3W", "2.3" is not a valid decimal token, because it is followed by 'W'.
   * For char stream "12.0D 34.E2+0.12 "  12.0D is a valid decimal token because it is folllowed
   * by a space. 34.E2 is a valid decimal token because it is followed by symbol '+'
   * which is not a digit or letter or underscore.
   */
  public boolean isValidDecimal() {
    int nextChar = _input.LA(1);
    if (nextChar >= 'A' && nextChar <= 'Z' || nextChar >= '0' && nextChar <= '9' ||
      nextChar == '_') {
      return false;
    } else {
      return true;
    }
  }
}

tokens {
    DELIMITER
}

spark為生成的java代碼增加了一個驗證Decimal的邏輯,同時額外了增加了tokens DELIMITER
文法規則statement是sql解析的核心規則:

statement
    : query                                                            #statementDefault
    | USE db=identifier                                                #use
    | CREATE DATABASE (IF NOT EXISTS)? identifier
        (COMMENT comment=STRING)? locationSpec?
        (WITH DBPROPERTIES tablePropertyList)?                         #createDatabase
    | ALTER DATABASE identifier SET DBPROPERTIES tablePropertyList     #setDatabaseProperties
    | DROP DATABASE (IF EXISTS)? identifier (RESTRICT | CASCADE)?      #dropDatabase
    | createTableHeader ('(' colTypeList ')')? tableProvider
        (OPTIONS options=tablePropertyList)?
        (PARTITIONED BY partitionColumnNames=identifierList)?
        bucketSpec? locationSpec?
        (COMMENT comment=STRING)?
        (AS? query)?                                                   #createTable
    | createTableHeader ('(' columns=colTypeList ')')?
        (COMMENT comment=STRING)?
        (PARTITIONED BY '(' partitionColumns=colTypeList ')')?
        bucketSpec? skewSpec?
        rowFormat?  createFileFormat? locationSpec?
        (TBLPROPERTIES tablePropertyList)?
        (AS? query)?                                                   #createHiveTable
    | CREATE TABLE (IF NOT EXISTS)? target=tableIdentifier
        LIKE source=tableIdentifier locationSpec?                      #createTableLike
    | ANALYZE TABLE tableIdentifier partitionSpec? COMPUTE STATISTICS
        (identifier | FOR COLUMNS identifierSeq)?                      #analyze
    | ALTER TABLE tableIdentifier
        ADD COLUMNS '(' columns=colTypeList ')'                        #addTableColumns
    | ALTER (TABLE | VIEW) from=tableIdentifier
        RENAME TO to=tableIdentifier                                   #renameTable
    | ALTER (TABLE | VIEW) tableIdentifier
        SET TBLPROPERTIES tablePropertyList                            #setTableProperties
    | ALTER (TABLE | VIEW) tableIdentifier
        UNSET TBLPROPERTIES (IF EXISTS)? tablePropertyList             #unsetTableProperties
    | ALTER TABLE tableIdentifier partitionSpec?
        CHANGE COLUMN? identifier colType colPosition?                 #changeColumn
    | ALTER TABLE tableIdentifier (partitionSpec)?
        SET SERDE STRING (WITH SERDEPROPERTIES tablePropertyList)?     #setTableSerDe
    | ALTER TABLE tableIdentifier (partitionSpec)?
        SET SERDEPROPERTIES tablePropertyList                          #setTableSerDe
    | ALTER TABLE tableIdentifier ADD (IF NOT EXISTS)?
        partitionSpecLocation+                                         #addTablePartition
    | ALTER VIEW tableIdentifier ADD (IF NOT EXISTS)?
        partitionSpec+                                                 #addTablePartition
    | ALTER TABLE tableIdentifier
        from=partitionSpec RENAME TO to=partitionSpec                  #renameTablePartition
    | ALTER TABLE tableIdentifier
        DROP (IF EXISTS)? partitionSpec (',' partitionSpec)* PURGE?    #dropTablePartitions
    | ALTER VIEW tableIdentifier
        DROP (IF EXISTS)? partitionSpec (',' partitionSpec)*           #dropTablePartitions
    | ALTER TABLE tableIdentifier partitionSpec? SET locationSpec      #setTableLocation
    | ALTER TABLE tableIdentifier RECOVER PARTITIONS                   #recoverPartitions
    | DROP TABLE (IF EXISTS)? tableIdentifier PURGE?                   #dropTable
    | DROP VIEW (IF EXISTS)? tableIdentifier                           #dropTable
    | CREATE (OR REPLACE)? (GLOBAL? TEMPORARY)?
        VIEW (IF NOT EXISTS)? tableIdentifier
        identifierCommentList? (COMMENT STRING)?
        (PARTITIONED ON identifierList)?
        (TBLPROPERTIES tablePropertyList)? AS query                    #createView
    | CREATE (OR REPLACE)? GLOBAL? TEMPORARY VIEW
        tableIdentifier ('(' colTypeList ')')? tableProvider
        (OPTIONS tablePropertyList)?                                   #createTempViewUsing
    | ALTER VIEW tableIdentifier AS? query                             #alterViewQuery
    | CREATE (OR REPLACE)? TEMPORARY? FUNCTION (IF NOT EXISTS)?
        qualifiedName AS className=STRING
        (USING resource (',' resource)*)?                              #createFunction
    | DROP TEMPORARY? FUNCTION (IF EXISTS)? qualifiedName              #dropFunction
    | EXPLAIN (LOGICAL | FORMATTED | EXTENDED | CODEGEN | COST)?
        statement                                                      #explain
    | SHOW TABLES ((FROM | IN) db=identifier)?
        (LIKE? pattern=STRING)?                                        #showTables
    | SHOW TABLE EXTENDED ((FROM | IN) db=identifier)?
        LIKE pattern=STRING partitionSpec?                             #showTable
    | SHOW DATABASES (LIKE pattern=STRING)?                            #showDatabases
    | SHOW TBLPROPERTIES table=tableIdentifier
        ('(' key=tablePropertyKey ')')?                                #showTblProperties
    | SHOW COLUMNS (FROM | IN) tableIdentifier
        ((FROM | IN) db=identifier)?                                   #showColumns
    | SHOW PARTITIONS tableIdentifier partitionSpec?                   #showPartitions
    | SHOW identifier? FUNCTIONS
        (LIKE? (qualifiedName | pattern=STRING))?                      #showFunctions
    | SHOW CREATE TABLE tableIdentifier                                #showCreateTable
    | (DESC | DESCRIBE) FUNCTION EXTENDED? describeFuncName            #describeFunction
    | (DESC | DESCRIBE) DATABASE EXTENDED? identifier                  #describeDatabase
    | (DESC | DESCRIBE) TABLE? option=(EXTENDED | FORMATTED)?
        tableIdentifier partitionSpec? describeColName?                #describeTable
    | REFRESH TABLE tableIdentifier                                    #refreshTable
    | REFRESH (STRING | .*?)                                           #refreshResource
    | CACHE LAZY? TABLE tableIdentifier (AS? query)?                   #cacheTable
    | UNCACHE TABLE (IF EXISTS)? tableIdentifier                       #uncacheTable
    | CLEAR CACHE                                                      #clearCache
    | LOAD DATA LOCAL? INPATH path=STRING OVERWRITE? INTO TABLE
        tableIdentifier partitionSpec?                                 #loadData
    | TRUNCATE TABLE tableIdentifier partitionSpec?                    #truncateTable
    | MSCK REPAIR TABLE tableIdentifier                                #repairTable
    | op=(ADD | LIST) identifier .*?                                   #manageResource
    | SET ROLE .*?                                                     #failNativeCommand
    | SET .*?                                                          #setConfiguration
    | RESET                                                            #resetConfiguration
    | unsupportedHiveNativeCommands .*?                                #failNativeCommand
    ;

包含了增刪改成所有的備選分支,同時備選分支都想語義清晰的標簽,用于訪問類進行分支訪問,語法中也包含了大量正則的標識:

*: 匹配前面的子表達式零次或多次
.: 匹配除換行符 \n 之外的任何單字符
?: 匹配前面的子表達式零次或一次,或指明一個非貪婪限定符
|: 指明兩項之間的一個選擇
{: 標記限定符表達式的開始
( ): 標記一個子表達式的開始和結束位置
這里我們可以使用 sql

SELECT SUM(COUNT1) FROM (SELECT NAME,COUNT(*) AS COUNT1 FROM TEST GROUP BY NAME)A

來進行測試語法文件

?  parser git:(master) ? antlr4 SqlBase.g4
?  parser git:(master) ? ls
ArrayInit.g4             SqlBase.tokens           SqlBaseLexer.java        SqlBaseListener.java
SqlBase.g4               SqlBaseBaseListener.java SqlBaseLexer.tokens      SqlBaseParser.java
?  parser git:(master) ? javac *.java
?  parser git:(master) ? grun SqlBase singleStatement -tree  
SELECT SUM(COUNT1) FROM (SELECT NAME,COUNT(*) AS COUNT1 FROM TEST GROUP BY NAME)A eof
image.png

Spark的訪問者類-sql轉換成LogicalPlan的流程

我們仍然使用

SELECT SUM(COUNT1) FROM (SELECT NAME,COUNT(*) AS COUNT1 FROM TEST GROUP BY NAME)A

作為我們的樣本sql,流程如下

image.png

visitor會按照樹的結構從上到下遍歷,并按照返回值組裝我們的LogicalPlan
可以從獲得的LogicalPlan看出,這個sql將包含了聚合的操作,聚合函數。


到此為止,這篇文章想說的東西,基本結束。我們初步生產的LogicalPlan 將會再經歷 什么處理呢,后面的文章再展開講。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容