探究Presto SQL引擎(1)-巧用Antlr

一岗照、背景

自2014年大數(shù)據(jù)首次寫入政府工作報(bào)告幌墓,大數(shù)據(jù)已經(jīng)發(fā)展7年悲伶。大數(shù)據(jù)的類型也從交易數(shù)據(jù)延伸到交互數(shù)據(jù)與傳感數(shù)據(jù)郊尝。數(shù)據(jù)規(guī)模也到達(dá)了PB級別秕铛。

大數(shù)據(jù)的規(guī)模大到對數(shù)據(jù)的獲取约郁、存儲、管理但两、分析超出了傳統(tǒng)數(shù)據(jù)庫軟件工具能力范圍鬓梅。在這個(gè)背景下,各種大數(shù)據(jù)相關(guān)工具相繼出現(xiàn)谨湘,用于應(yīng)對各種業(yè)務(wù)場景需求绽快。從Hadoop生態(tài)的Hive, Spark, Presto, Kylin, Druid到非Hadoop生態(tài)的ClickHouse, Elasticsearch,不一而足...

這些大數(shù)據(jù)處理工具特性不同紧阔,應(yīng)用場景不同坊罢,但是對外提供的接口或者說操作語言都是相似的,即各個(gè)組件都是支持SQL語言擅耽。只是基于不同的應(yīng)用場景和特性活孩,實(shí)現(xiàn)了各自的SQL方言。這就要求相關(guān)開源項(xiàng)目自行實(shí)現(xiàn)SQL解析乖仇。在這個(gè)背景下憾儒,誕生于1989年的語法解析器生成器ANTLR迎來了黃金時(shí)代。

二乃沙、簡介

ANTLR是開源的語法解析器生成器起趾,距今已有30多年的歷史。是一個(gè)經(jīng)歷了時(shí)間考驗(yàn)的開源項(xiàng)目警儒。一個(gè)程序從源代碼到機(jī)器可執(zhí)行训裆,基本需要3個(gè)階段:編寫、編譯、執(zhí)行缭保。

在編譯階段汛闸,需要進(jìn)行詞法和語法的分析。ANTLR聚焦的問題就是把源碼進(jìn)行詞法和句法分析艺骂,產(chǎn)生一個(gè)樹狀的分析器诸老。ANTLR幾乎支持對所有主流編程語言的解析。從antlr/grammars-v4可以看到钳恕,ANTLR支持Java,C, Python, SQL等數(shù)十種編程語言别伏。通常我們沒有擴(kuò)展編程語言的需求,所以大部分情況下這些語言編譯支持更多是供學(xué)習(xí)研究使用忧额,或者用在各種開發(fā)工具(NetBeans厘肮、Intellij)中用于校驗(yàn)語法正確性、和格式化代碼睦番。

對于SQL語言类茂,ANTLR的應(yīng)用廣度和深度會更大,這是由于Hive, Presto, SparkSQL等由于需要對SQL的執(zhí)行進(jìn)行定制化開發(fā)托嚣,比如實(shí)現(xiàn)分布式查詢引擎巩检、實(shí)現(xiàn)各種大數(shù)據(jù)場景下獨(dú)有的特性等。

三示启、基于ANTLR4實(shí)現(xiàn)四則運(yùn)算

當(dāng)前我們主要使用的是ANTLR4兢哭。在《The Definitive ANTLR4 Reference》一書中,介紹了基于ANTLR4的各種有趣的應(yīng)用場景夫嗓。比如:實(shí)現(xiàn)一個(gè)支持四則運(yùn)算的計(jì)算器迟螺;實(shí)現(xiàn)JSON等格式化文本的解析和提取舍咖;

將JSON轉(zhuǎn)換成XML矩父;從Java源碼中提取接口等。本節(jié)以實(shí)現(xiàn)四則運(yùn)算計(jì)算器為例排霉,介紹Antlr4的簡單應(yīng)用浙垫,為后面實(shí)現(xiàn)基于ANTLR4解析SQL鋪平道路。實(shí)際上郑诺,支持?jǐn)?shù)字運(yùn)算也是各個(gè)編程語言必須具備的基本能力夹姥。

3.1 自行編碼實(shí)現(xiàn)

在沒有ANTLR4時(shí),我們想實(shí)現(xiàn)四則運(yùn)算該怎么處理呢辙诞?有一種思路是基于棧實(shí)現(xiàn)辙售。例如,在不考慮異常處理的情況下飞涂,自行實(shí)現(xiàn)簡單的四則運(yùn)算代碼如下:

package org.example.calc;
 
import java.util.*;
 
public class CalcByHand {
    // 定義操作符并區(qū)分優(yōu)先級旦部,*/ 優(yōu)先級較高
    public static Set<String> opSet1 = new HashSet<>();
    public static Set<String> opSet2 = new HashSet<>();
    static{
        opSet1.add("+");
        opSet1.add("-");
        opSet2.add("*");
        opSet2.add("/");
    }
    public static void main(String[] args) {
        String exp="1+3*4";
        //將表達(dá)式拆分成token
        String[] tokens = exp.split("((?<=[\\+|\\-|\\*|\\/])|(?=[\\+|\\-|\\*|\\/]))");
 
        Stack<String> opStack = new Stack<>();
        Stack<String> numStack = new Stack<>();
        int proi=1;
        // 基于類型放到不同的棧中
        for(String token: tokens){
            token = token.trim();
 
            if(opSet1.contains(token)){
                opStack.push(token);
                proi=1;
            }else if(opSet2.contains(token)){
                proi=2;
                opStack.push(token);
            }else{
                numStack.push(token);
                // 如果操作數(shù)前面的運(yùn)算符是高優(yōu)先級運(yùn)算符祈搜,計(jì)算后結(jié)果入棧
                if(proi==2){
                    calcExp(opStack,numStack);
                }
            }
        }
 
        while (!opStack.isEmpty()){
            calcExp(opStack,numStack);
        }
        String finalVal = numStack.pop();
        System.out.println(finalVal);
    }
     
    private static void calcExp(Stack<String> opStack, Stack<String> numStack) {
        double right=Double.valueOf(numStack.pop());
        double left = Double.valueOf(numStack.pop());
        String op = opStack.pop();
        String val;
        switch (op){
            case "+":
                 val =String.valueOf(left+right);
                break;
            case "-":
                 val =String.valueOf(left-right);
                break;
            case "*":
                val =String.valueOf(left*right);
                break;
            case "/":
                val =String.valueOf(left/right);
                break;
            default:
                throw new UnsupportedOperationException("unsupported");
        }
        numStack.push(val);
    }
}

代碼量不大,用到了數(shù)據(jù)結(jié)構(gòu)-棧的特性士八,需要自行控制運(yùn)算符優(yōu)先級容燕,特性上沒有支持括號表達(dá)式,也沒有支持表達(dá)式賦值婚度。接下來看看使用ANTLR4實(shí)現(xiàn)蘸秘。

3.2 基于ANTLR4實(shí)現(xiàn)

使用ANTLR4編程的基本流程是固定的,通常分為如下三步:

  • 基于需求按照ANTLR4的規(guī)則編寫自定義語法的語義規(guī)則, 保存成以g4為后綴的文件蝗茁。

  • 使用ANTLR4工具處理g4文件醋虏,生成詞法分析器、句法分析器代碼哮翘、詞典文件颈嚼。

  • 編寫代碼繼承Visitor類或?qū)崿F(xiàn)Listener接口,開發(fā)自己的業(yè)務(wù)邏輯代碼饭寺。

基于上面的流程阻课,我們借助現(xiàn)有案例剖析一下細(xì)節(jié)。

第一步:基于ANTLR4的規(guī)則定義語法文件艰匙,文件名以g4為后綴柑肴。例如實(shí)現(xiàn)計(jì)算器的語法規(guī)則文件命名為LabeledExpr.g4。其內(nèi)容如下:

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

(注:此文件案例來源于《The Definitive ANTLR4 Reference》)

簡單解讀一下LabeledExpr.g4文件旬薯。ANTLR4規(guī)則是基于正則表達(dá)式定義定義。規(guī)則的理解是自頂向下的适秩,每個(gè)分號結(jié)束的語句表示一個(gè)規(guī)則 绊序。例如第一行:grammar LabeledExpr; 表示我們的語法名稱是LabeledExpr, 這個(gè)名字需要跟文件名需要保持一致。Java編碼也有相似的規(guī)則:類名跟類文件一致秽荞。

規(guī)則prog 表示prog是一個(gè)或多個(gè)stat骤公。

規(guī)則stat 適配三種子規(guī)則:空行、表達(dá)式expr扬跋、賦值表達(dá)式 ID’=’expr阶捆。

表達(dá)式expr適配五種子規(guī)則:乘除法、加減法钦听、整型洒试、ID、括號表達(dá)式朴上。很顯然垒棋,這是一個(gè)遞歸的定義。

最后定義的是組成復(fù)合規(guī)則的基礎(chǔ)元素痪宰,比如:規(guī)則ID: [a-zA-Z]+表示ID限于大小寫英文字符串叼架;INT: [0-9]+; 表示INT這個(gè)規(guī)則是0-9之間的一個(gè)或多個(gè)數(shù)字畔裕,當(dāng)然這個(gè)定義其實(shí)并不嚴(yán)格。再嚴(yán)格一點(diǎn)乖订,應(yīng)該限制其長度扮饶。

在理解正則表達(dá)式的基礎(chǔ)上,ANTLR4的g4語法規(guī)則還是比較好理解的乍构。

定義ANTLR4規(guī)則需要注意一種情況甜无,即可能出現(xiàn)一個(gè)字符串同時(shí)支持多種規(guī)則,如以下的兩個(gè)規(guī)則:

ID: [a-zA-Z]+;

FROM: ‘from’;

很明顯蜡吧,字符串” from”同時(shí)滿足上述兩個(gè)規(guī)則毫蚓,ANTLR4處理的方式是按照定義的順序決定。這里ID定義在FROM前面昔善,所以字符串from會優(yōu)先匹配到ID這個(gè)規(guī)則上元潘。

其實(shí)在定義好與法規(guī)中,編寫完成g4文件后君仆,ANTLR4已經(jīng)為我們完成了50%的工作:幫我們實(shí)現(xiàn)了整個(gè)架構(gòu)及接口了翩概,剩下的開發(fā)工作就是基于接口或抽象類進(jìn)行具體的實(shí)現(xiàn)。實(shí)現(xiàn)上有兩種方式來處理生成的語法樹返咱,其一Visitor模式钥庇,另一種方式是Listener(監(jiān)聽器模式)。

3.2.1 使用Visitor模式

第二步:使用ANTLR4工具解析g4文件咖摹,生成代碼评姨。即ANTLR工具解析g4文件,為我們自動(dòng)生成基礎(chǔ)代碼萤晴。流程圖示如下:

image

命令行如下:

antlr4 -package org.example.calc -no-listener -visitor .\LabeledExpr.g4

命令執(zhí)行完成后吐句,生成的文件如下:

$ tree .
.
├── LabeledExpr.g4
├── LabeledExpr.tokens
├── LabeledExprBaseVisitor.java
├── LabeledExprLexer.java
├── LabeledExprLexer.tokens
├── LabeledExprParser.java
└── LabeledExprVisitor.java

首先開發(fā)入口類Calc.java。Calc類是整個(gè)程序的入口店读,調(diào)用ANTLR4的lexer和parser類核心代碼如下:

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);

接下來定義類繼承LabeledExprBaseVisitor類嗦枢,覆寫的方法如下:

image

從圖中可以看出,生成的代碼和規(guī)則定義是對應(yīng)起來的屯断。例如visitAddSub對應(yīng)AddSub規(guī)則文虏,visitId對應(yīng)id規(guī)則。以此類推…實(shí)現(xiàn)加減法的代碼如下:

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

相當(dāng)直觀殖演。代碼編寫完成后氧秘,就是運(yùn)行Calc。運(yùn)行Calc的main函數(shù)趴久,在交互命令行輸入相應(yīng)的運(yùn)算表達(dá)式敏储,換行Ctrl+D即可看到運(yùn)算結(jié)果。例如1+3*4=13朋鞍。

3.2.2 使用Listener模式

類似的已添,我們也可以使用Listener模式實(shí)現(xiàn)四則運(yùn)算妥箕。命令行如下:

antlr4 -package org.example.calc -listener .\LabeledExpr.g4

該命令的執(zhí)行同樣會為我們生產(chǎn)框架代碼。在框架代碼的基礎(chǔ)上更舞,我們開發(fā)入口類和接口實(shí)現(xiàn)類即可畦幢。首先開發(fā)入口類Calc.java。Calc類是整個(gè)程序的入口缆蝉,調(diào)用ANTLR4的lexer和parser類代碼如下:

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
 
ParseTreeWalker walker = new ParseTreeWalker();
walker.walk(new EvalListener(), tree);

可以看出生成ParseTree的調(diào)用邏輯一模一樣宇葱。實(shí)現(xiàn)Listener的代碼略微復(fù)雜一些,也需要用到棧這種數(shù)據(jù)結(jié)構(gòu)刊头,但是只需要一個(gè)操作數(shù)棧就可以了黍瞧,也無需自行控制優(yōu)先級。以AddSub為例:

@Override
public void exitAddSub(LabeledExprParser.AddSubContext ctx) {
    Double left = numStack.pop();
    Double right= numStack.pop();
    Double result;
    if (ctx.op.getType() == LabeledExprParser.ADD) {
        result = left + right;
    } else {
        result = left - right;
    }
    numStack.push(result);
}

直接從棧中取出操作數(shù)原杂,進(jìn)行運(yùn)算即可印颤。

3.2.3 小結(jié)

關(guān)于Listener模式和Visitor模式的區(qū)別,《The Definitive ANTLR 4 Reference》一書中有清晰的解釋:

Listener模式:

image

Visitor模式:

image
  • Listener模式通過walker對象自行遍歷穿肄,不用考慮其語法樹上下級關(guān)系年局。Vistor需要自行控制訪問的子節(jié)點(diǎn),如果遺漏了某個(gè)子節(jié)點(diǎn)咸产,那么整個(gè)子節(jié)點(diǎn)都訪問不到了矢否。

  • Listener模式的方法沒有返回值,Vistor模式可以設(shè)定任意返回值脑溢。

  • Listener模式的訪問棧清晰明確僵朗,Vistor模式是方法調(diào)用棧,如果實(shí)現(xiàn)出錯(cuò)有可能導(dǎo)致StackOverFlow屑彻。

通過這個(gè)簡單的例子验庙,我們驅(qū)動(dòng)Antlr4實(shí)現(xiàn)了一個(gè)簡單的計(jì)算器。學(xué)習(xí)了ANTLR4的應(yīng)用流程酱酬。了解了g4語法文件的定義方式、Visitor模式和Listener模式云矫。通過ANTLR4膳沽,我們生成了ParseTree,并基于Visitor模式和Listener模式訪問了這個(gè)ParseTree,實(shí)現(xiàn)了四則運(yùn)算让禀。

綜合上述的例子可以發(fā)現(xiàn)挑社,如果沒有ANTLR4,我們自行編寫算法也能實(shí)現(xiàn)同樣的功能巡揍。但是使用ANTLR不用關(guān)心表達(dá)式串的解析流程痛阻,只關(guān)注具體的業(yè)務(wù)實(shí)現(xiàn)即可,非常省心和省事腮敌。

更重要的是阱当,ANTLR4相比自行實(shí)現(xiàn)提供了更具想象空間的抽象邏輯俏扩,上升到了方法論的高度,因?yàn)樗呀?jīng)不局限于解決某個(gè)問題弊添,而是解決一類問題录淡。可以說ANTLR相比于自行硬編碼解決問題的思路有如數(shù)學(xué)領(lǐng)域普通的面積公式和微積分的差距油坝。

四嫉戚、參考Presto源碼開發(fā)SQL解析器

前面介紹了使用ANTLR4實(shí)現(xiàn)四則運(yùn)算,其目的在于理解ANTLR4的應(yīng)用方式澈圈。接下來圖窮匕首見彬檀,展示出我們的真正目的:研究ANTLR4在Presto中如何實(shí)現(xiàn)SQL語句的解析。

支持完整的SQL語法是一個(gè)龐大的工程瞬女。在presto中有完整的SqlBase.g4文件窍帝,定義了presto支持的所有SQL語法,涵蓋了DDL語法和DML語法拆魏。該文件體系較為龐大盯桦,并不適合學(xué)習(xí)探究某個(gè)具體的細(xì)節(jié)點(diǎn)。

為了探究SQL解析的過程渤刃,理解SQL執(zhí)行背后的邏輯拥峦,在簡單地閱讀相關(guān)資料文檔的基礎(chǔ)上,我選擇自己動(dòng)手編碼實(shí)驗(yàn)卖子。為此略号,定義一個(gè)小目標(biāo):實(shí)現(xiàn)一個(gè)SQL解析器。用該解析器實(shí)現(xiàn)select field from table語法洋闽,從本地的csv數(shù)據(jù)源中查詢指定的字段玄柠。

4.1 裁剪SelectBase.g4文件

基于同實(shí)現(xiàn)四則運(yùn)算器同樣的流程,首先定義SelectBase.g4文件诫舅。由于有了Presto源碼作為參照系羽利,我們的SelectBase.g4并不需要自己開發(fā),只需要基于Presto的g4文件裁剪即可刊懈。裁剪后的內(nèi)容如下:

grammar SqlBase;
 
tokens {
    DELIMITER
}
 
singleStatement
    : statement EOF
    ;
 
statement
    : query                                                            #statementDefault
    ;
 
query
    :  queryNoWith
    ;
 
queryNoWith:
      queryTerm
    ;
 
queryTerm
    : queryPrimary                                                             #queryTermDefault
    ;
 
queryPrimary
    : querySpecification                   #queryPrimaryDefault
    ;
 
querySpecification
    : SELECT  selectItem (',' selectItem)*
      (FROM relation (',' relation)*)?
    ;
 
selectItem
    : expression  #selectSingle
    ;
 
relation
    :  sampledRelation                             #relationDefault
    ;
 
expression
    : booleanExpression
    ;
 
booleanExpression
    : valueExpression             #predicated
    ;
 
valueExpression
    : primaryExpression                                                                 #valueExpressionDefault
    ;
 
primaryExpression
    : identifier                                                                          #columnReference
    ;
 
sampledRelation
    : aliasedRelation
    ;
 
aliasedRelation
    : relationPrimary
    ;
 
relationPrimary
    : qualifiedName                                                   #tableName
    ;
 
qualifiedName
    : identifier ('.' identifier)*
    ;
 
identifier
    : IDENTIFIER             #unquotedIdentifier
    ;
 
SELECT: 'SELECT';
FROM: 'FROM';
 
fragment DIGIT
    : [0-9]
    ;
 
fragment LETTER
    : [A-Z]
    ;
 
IDENTIFIER
    : (LETTER | '_') (LETTER | DIGIT | '_' | '@' | ':')*
    ;
 
WS
    : [ \r\n\t]+ -> channel(HIDDEN)
    ;
 
// Catch-all for anything we can't recognize.
// We use this to be able to ignore and recover all the text
// when splitting statements with DelimiterLexer
UNRECOGNIZED
    : .
    ;

相比presto源碼中700多行的規(guī)則这弧,我們裁剪到了其1/10的大小。該文件的核心規(guī)則為: SELECT selectItem (',' selectItem)* (FROM relation (',' relation)*)

通過理解g4文件虚汛,也可以更清楚地理解我們查詢語句的構(gòu)成匾浪。例如通常我們最常見的查詢數(shù)據(jù)源是數(shù)據(jù)表。但是在SQL語法中卷哩,我們查詢數(shù)據(jù)表被抽象成了relation蛋辈。

這個(gè)relation有可能來自于具體的數(shù)據(jù)表,或者是子查詢将谊,或者是JOIN冷溶,或者是數(shù)據(jù)的抽樣渐白,或者是表達(dá)式的unnest。在大數(shù)據(jù)領(lǐng)域挂洛,這樣的擴(kuò)展會極大方便數(shù)據(jù)的處理礼预。

例如,使用unnest語法解析復(fù)雜類型的數(shù)據(jù)虏劲,SQL如下:

image

盡管SQL較為復(fù)雜托酸,但是通過理解g4文件,也能清晰理解其結(jié)構(gòu)劃分柒巫±ぃ回到SelectBase.g4文件,同樣我們使用Antlr4命令處理g4文件堡掏,生成代碼:

antlr4 -package org.example.antlr -no-listener -visitor .\SqlBase.g4

這樣就生成了基礎(chǔ)的框架代碼应结。接下來就是自行處理業(yè)務(wù)邏輯的工作了。

4.2 遍歷語法樹封裝SQL結(jié)構(gòu)信息

接下來基于SQL語法定義語法樹的節(jié)點(diǎn)類型泉唁,如下圖所示鹅龄。

image

通過這個(gè)類圖,可以清晰明了看清楚SQL語法中的各個(gè)基本元素亭畜。

然后基于visitor模式實(shí)現(xiàn)自己的解析類AstBuilder (這里為了簡化問題扮休,依然從presto源碼中進(jìn)行裁剪)。以處理querySpecification規(guī)則代碼為例:

@Override
public Node visitQuerySpecification(SqlBaseParser.QuerySpecificationContext context)
{
    Optional<Relation> from = Optional.empty();
    List<SelectItem> selectItems = visit(context.selectItem(), SelectItem.class);
 
    List<Relation> relations = visit(context.relation(), Relation.class);
    if (!relations.isEmpty()) {
        // synthesize implicit join nodes
        Iterator<Relation> iterator = relations.iterator();
        Relation relation = iterator.next();
 
        from = Optional.of(relation);
    }
 
    return new QuerySpecification(
            getLocation(context),
            new Select(getLocation(context.SELECT()), false, selectItems),
            from);
}

通過代碼拴鸵,我們已經(jīng)解析出了查詢的數(shù)據(jù)源和具體的字段玷坠,封裝到了QuerySpecification對象中。

4.3 應(yīng)用Statement對象實(shí)現(xiàn)數(shù)據(jù)查詢

通過前面實(shí)現(xiàn)四則運(yùn)算器的例子劲藐,我們知道ANTLR把用戶輸入的語句解析成ParseTree八堡。業(yè)務(wù)開發(fā)人員自行實(shí)現(xiàn)相關(guān)接口解析ParseTree。Presto通過對輸入sql語句的解析聘芜,生成ParseTree, 對ParseTree進(jìn)行遍歷兄渺,最終生成了Statement對象。核心代碼如下:

SqlParser sqlParser = new SqlParser();
Statement statement = sqlParser.createStatement(sql);

有了Statement對象我們?nèi)绾问褂媚靥郑拷Y(jié)合前面的類圖挂谍,我們可以發(fā)現(xiàn):

  • Query類型的Statement有QueryBody屬性。

  • QuerySpecification類型的QueryBody有select屬性和from屬性服鹅。

通過這個(gè)結(jié)構(gòu)凳兵,我們可以清晰地獲取到實(shí)現(xiàn)select查詢的必備元素:

  • 從from屬性中獲取待查詢的目標(biāo)表Table百新。這里約定表名和csv文件名一致企软。

  • 從select屬性中獲取待查詢的目標(biāo)字段SelectItem。這里約定csv首行為title行饭望。

整個(gè)業(yè)務(wù)流程就清晰了仗哨,在解析sql語句生成statement對象后形庭,按如下的步驟:

  • s1: 獲取查詢的數(shù)據(jù)表以及字段。

  • s2: 通過數(shù)據(jù)表名稱定為到數(shù)據(jù)文件厌漂,并讀取數(shù)據(jù)文件數(shù)據(jù)萨醒。

  • s3: 格式化輸出字段名稱到命令行。

  • s4: 格式化輸出字段內(nèi)容到命令行苇倡。

為了簡化邏輯富纸,代碼只處理主線,不做異常處理旨椒。

/**
 * 獲取待查詢的表名和字段名稱
 */
QuerySpecification specification = (QuerySpecification) query.getQueryBody();
Table table= (Table) specification.getFrom().get();
List<SelectItem> selectItems = specification.getSelect().getSelectItems();
List<String> fieldNames = Lists.newArrayList();
for(SelectItem item:selectItems){
    SingleColumn column = (SingleColumn) item;
    fieldNames.add(((Identifier)column.getExpression()).getValue());
}
 
/**
 * 基于表名確定查詢的數(shù)據(jù)源文件
 */
String fileLoc = String.format("./data/%s.csv",table.getName());
 
/**
 * 從csv文件中讀取指定的字段
 */
Reader in = new FileReader(fileLoc);
Iterable<CSVRecord> records = CSVFormat.RFC4180.withFirstRecordAsHeader().parse(in);
List<Row> rowList = Lists.newArrayList();
for(CSVRecord record:records){
    Row row = new Row();
    for(String field:fieldNames){
        row.addColumn(record.get(field));
    }
    rowList.add(row);
}
 
/**
 * 格式化輸出到控制臺
 */
int width=30;
String format = fieldNames.stream().map(s-> "%-"+width+"s").collect(Collectors.joining("|"));
System.out.println( "|"+String.format(format, fieldNames.toArray())+"|");
 
int flagCnt = width*fieldNames.size()+fieldNames.size();
String rowDelimiter = String.join("", Collections.nCopies(flagCnt, "-"));
System.out.println(rowDelimiter);
for(Row row:rowList){
    System.out.println( "|"+String.format(format, row.getColumnList().toArray())+"|");
}

代碼僅供演示功能晓褪,暫不考慮異常邏輯,比如查詢字段不存在综慎、csv文件定義字段名稱不符合要求等問題涣仿。

4.4 實(shí)現(xiàn)效果展示

在我們項(xiàng)目data目錄,存儲如下的csv文件:

image

cities.csv文件樣例數(shù)據(jù)如下:

"LatD","LatM","LatS","NS","LonD","LonM","LonS","EW","City","State"
   41,    5,   59, "N",     80,   39,    0, "W", "Youngstown", OH
   42,   52,   48, "N",     97,   23,   23, "W", "Yankton", SD
   46,   35,   59, "N",    120,   30,   36, "W", "Yakima", WA
   42,   16,   12, "N",     71,   48,    0, "W", "Worcester", MA

運(yùn)行代碼查詢數(shù)據(jù)示惊。使用SQL語句指定字段從csv文件中查詢好港。最終實(shí)現(xiàn)類似SQL查詢的效果如下:

SQL樣例1:select City, City from cities

image

SQL樣例2:select name, age from employee

image

本節(jié)講述了如何基于Presto源碼,裁剪g4規(guī)則文件米罚,然后基于Antlr4實(shí)現(xiàn)用sql語句從csv文件查詢數(shù)據(jù)钧汹。依托于對Presto源碼的裁剪進(jìn)行編碼實(shí)驗(yàn),對于研究SQL引擎實(shí)現(xiàn)阔拳,理解Presto源碼能起到一定的作用崭孤。

五、總結(jié)

本文基于四則運(yùn)算器和使用SQL查詢csv數(shù)據(jù)兩個(gè)案例闡述了ANTLR4在項(xiàng)目開發(fā)中的應(yīng)用思路和過程糊肠,相關(guān)的代碼可以在github上看到辨宠。理解ANTLR4的用法能夠幫助理解SQL的定義規(guī)則及執(zhí)行過程,輔助業(yè)務(wù)開發(fā)中編寫出高效的SQL語句货裹。同時(shí)對于理解編譯原理嗤形,定義自己的DSL,抽象業(yè)務(wù)邏輯也大有裨益弧圆。紙上得來終覺淺赋兵,絕知此事要躬行。通過本文描述的方式研究源碼實(shí)現(xiàn)搔预,也不失為一種樂趣霹期。

參考資料

1、《The Definitive ANTLR4 Reference》

2拯田、Presto官方文檔

3历造、《ANTLR 4簡明教程》

4、Calc類源碼

5、EvalVisitor類源碼

6吭产、Presto源碼

作者:vivo互聯(lián)網(wǎng)開發(fā)團(tuán)隊(duì)-Shuai Guangying

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末侣监,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子臣淤,更是在濱河造成了極大的恐慌橄霉,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件邑蒋,死亡現(xiàn)場離奇詭異姓蜂,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)医吊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門覆糟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人遮咖,你說我怎么就攤上這事滩字。” “怎么了御吞?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵麦箍,是天一觀的道長。 經(jīng)常有香客問我陶珠,道長挟裂,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任揍诽,我火速辦了婚禮诀蓉,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘暑脆。我一直安慰自己渠啤,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布添吗。 她就那樣靜靜地躺著沥曹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪碟联。 梳的紋絲不亂的頭發(fā)上妓美,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天,我揣著相機(jī)與錄音鲤孵,去河邊找鬼壶栋。 笑死,一個(gè)胖子當(dāng)著我的面吹牛普监,可吹牛的內(nèi)容都是我干的贵试。 我是一名探鬼主播丧没,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼锡移!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起漆际,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤淆珊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后奸汇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體施符,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年擂找,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了戳吝。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,133評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡贯涎,死狀恐怖听哭,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情塘雳,我是刑警寧澤陆盘,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站败明,受9級特大地震影響隘马,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜妻顶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一酸员、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧讳嘱,春花似錦幔嗦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至叛氨,卻和暖如春呼渣,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背寞埠。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工屁置, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人仁连。 一個(gè)月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓蓝角,卻偏偏與公主長得像阱穗,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子使鹅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容