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
语法分析树或者句法树的数据结构,该数据结构记录了语法分析器识别输入语句结构的过程,以及该结构的各组成部分。
语法分析树的内部节点是 词组名,这些名字用于识别它们的子节点,并可以将子节点归类。根节点是比较抽象的一个名字,在这里是 stat(statement)
。
语法分析树的叶子节点永远是输入的词法符号。
句子,也即符号的线性组合,本质上是语法分析树在人脑中的串行化。通过操作语法分析树,识别同一种语言的不同程序就能服用同一个语法分析器。
为了编写一个语言类的程序,我们必须对每个输入的词组或者子词组 执行一些适当的操作。
进行这项工作最简单的方式就是操作语法分析器自动帮我们生成的语法分析树。
这种方式的优点是,我们能够重新回到JAVA的领域。不需要再学习复杂的ANTRL语法。
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
方法。
(The other listener calls aren’t shown.
)
监听器方式的优点在于,回调是自动进行的。我们不需要编写对语法分析树的遍历代码,也不需要让我们的监听器显式地访问子节点
-
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 + 关键字 文件名 语句语法分析器的规则以小写字母开头(
init
和value
)-
词法分析器的规则以大小字母开头(
INT
和WS
)
我们可以安装antlr官网来简单配置我们的运行环境:运行命令
antlr4 ArrayInit.g4
可以生成一批
.java
文件:-
ArrayInitLexer
: 词法解析器类识别我们语法中的文法规则和词法规则 -
ArrayInitParser
: 语法解析器类
ArrayInit.tokens
: ANTLR会给每个我们定义的词法符号指定一个数字形式的类型
-
ArrayInitListener
,ArrayInitBaseListener
:监听器类
-
ArrayInitVisitor
,ArrayInitBaseVisitor
:访问者模式类
-
使用监听器来实现把short数组初始化为字符串对象
我们需要做的翻译过程包括:
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
-
为不同的备选分支添加的了标签(
#MulDiv
/#AddSub
),如果没有标签,ANTLR是为每条规则来生成方法如果希望每个备选分支都有相应的方法来访问,就可以像我这样在右侧加上#
标签。
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
结构很简单,是一个复制的语句,ID
符号内的文本是被赋值的变量,expr
所代表的值是要赋值的数。我们对expr
的分析树进行进行分析,我们发现expr
的所有的分支都相应的方法可以访问visitInt
、visitId
、visitMulDiv
、visitAddSub
、visitParens
,假如进入的分支是#int
因为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中对 json、java规范使用ANTLR来完成我们自己的解析器。
Spark中的sql解析过程
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
)来处理格式转换。
用户输入的
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
Spark的访问者类-sql
转换成LogicalPlan
的流程
我们仍然使用
SELECT SUM(COUNT1) FROM (SELECT NAME,COUNT(*) AS COUNT1 FROM TEST GROUP BY NAME)A
作为我们的样本sql,流程如下
visitor
会按照树的结构从上到下遍历,并按照返回值组装我们的LogicalPlan
可以从获得的LogicalPlan
看出,这个sql将包含了聚合的操作,聚合函数。
到此为止,这篇文章想说的东西,基本结束。我们初步生产的LogicalPlan
将会再经历 什么处理呢,后面的文章再展开讲。