Spark-sql[1]-antrl4的入門以及在spark中的實現(xiàn)

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

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

ANTLR導(dǎo)讀

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

ANTLR概念

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

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

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

語言是由一些列有意義的語句組成,語句由詞組組成,詞組是由更小的子詞組和詞匯符號組成。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.

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

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

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

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

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

第二個階段就是語法分析輸入的詞法符號被消費以識別語句結(jié)構(gòu)

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

image.png

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

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

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

為了編寫一個語言類的程序,我們必須對每個輸入的詞組或者子詞組 執(zhí)行一些適當?shù)牟僮鳌?/p>

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

這種方式的優(yōu)點是,我們能夠重新回到JAVA的領(lǐng)域。不需要再學(xué)習(xí)復(fù)雜的ANTRL語法。

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

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

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

(The other listener calls aren’t shown. )

image.png

監(jiān)聽器方式的優(yōu)點在于,回調(diào)是自動進行的。我們不需要編寫對語法分析樹的遍歷代碼,也不需要讓我們的監(jiān)聽器顯式地訪問子節(jié)點

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

代碼demo

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

VNTLR內(nèi)部為訪問者模式提供的支持代碼會在根節(jié)點處調(diào)用visitStat方法,接下來,visitStat方法的實現(xiàn)將會調(diào)用visit方法,并將所用的子節(jié)點作為參數(shù)傳遞給它,從而繼續(xù)遍歷的過程

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

    /** 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 關(guān)鍵字必須與 .g4 文件同名, 如果一個語法文件太大可以拆分成多個文件,相互依賴就是依賴 import + 關(guān)鍵字 文件名 語句

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

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

      image.png

      運行命令

     antlr4 ArrayInit.g4
    

    可以生成一批 .java文件:

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

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

      image.png

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

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

      image.png

      我們需要做的翻譯過程包括:
      1.將 { => "。
      2.將 } => "。
      3.將每個整數(shù)表示為十六進制數(shù)并且加前綴 \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);
        }
    }
    

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

    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
        }
    }
    

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

    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]+ ;         // 匹配整數(shù)
    NEWLINE:'\r'? '\n' ;     // 新的一行
    WS  :   [ \t]+ -> skip ; // 忽略空白字符
    
    
    • 使用訪問模式來實現(xiàn)
      為了更好的使用訪問者模式我們對上面的語法文件做些許修改:
    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是為每條規(guī)則來生成方法如果希望每個備選分支都有相應(yīng)的方法來訪問,就可以像我這樣在右側(cè)加上#標簽。

      image.png

      2.怎么來實現(xiàn)屬于我們自己的訪問器類

    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

    結(jié)構(gòu)很簡單,是一個復(fù)制的語句,ID符號內(nèi)的文本是被賦值的變量,expr所代表的值是要賦值的數(shù)。我們對 expr的分析樹進行進行分析,我們發(fā)現(xiàn)expr的所有的分支都相應(yīng)的方法可以訪問visitInt、visitIdvisitMulDivvisitAddSub、visitParens,假如進入的分支是#int

    image.png

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

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

    稍微復(fù)雜一點的可能是 標簽 #MulDiv、#AddSub因為,需要根 據(jù)操作符op* 來進一步判斷進行什么操作。
    3.現(xiàn)在我們已經(jīng)擁有了我們自己的訪問器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代碼中增加額外的方法,我們可以直接對生產(chǎn)的java文件進行操作,或者在語法文件中使用 @parser::members {高級語法進行添加。。。
如果大家仍然有興趣,可以參考 json官網(wǎng)、Java語言規(guī)范:基于Java SE 8中對 json、java規(guī)范使用ANTLR來完成我們自己的解析器。

Spark中的sql解析過程

image.png

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

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

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

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

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

通過上面antlr的學(xué)習(xí),我們已經(jīng)了解到,開發(fā)這樣一個解釋器,我們需要的因素:

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

image.png

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

Spark的語法文件

spark-sql的語法文件SqlBase.g4放置在子工程catalyst中,大概有1000多行代碼,spark的語法文件是從facebook的presto中改進過來的。大家可以參考presto的語法規(guī)范
presto-parser/src/main/antlr4/com/facebook/presto/sql/parser/SqlBase.g4
databricks也為我們整理所有的spark-sql的說明
/latest/spark-sql/index.html
通過上面的 antlr語法的學(xué)習(xí),我們讀取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
文法規(guī)則statement是sql解析的核心規(guī)則:

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 之外的任何單字符
?: 匹配前面的子表達式零次或一次,或指明一個非貪婪限定符
|: 指明兩項之間的一個選擇
{: 標記限定符表達式的開始
( ): 標記一個子表達式的開始和結(jié)束位置
這里我們可以使用 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轉(zhuǎn)換成LogicalPlan的流程

我們?nèi)匀皇褂?/p>

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

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

image.png

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


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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,502評論 19 139
  • Spark SQL, DataFrames and Datasets Guide Overview SQL Dat...
    Joyyx閱讀 8,467評論 0 16
  • 一. Java基礎(chǔ)部分.................................................
    wy_sure閱讀 3,988評論 0 11
  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語法,類相關(guān)的語法,內(nèi)部類的語法,繼承相關(guān)的語法,異常的語法,線程的語...
    子非魚_t_閱讀 34,623評論 18 399
  • postman:進行rest請求方式測試nginx,supervisor,uwsgi/gunicorn 1.系統(tǒng)依...
    amyhy閱讀 1,708評論 0 0

友情鏈接更多精彩內(nèi)容