hive原理与源码分析-语法分析器和语义分析器(二)

玩个游戏:
执行:find . -name ‘*.java’ | xargs grep –color ‘main(‘ | awk ‘{print $1}’ | uniq | grep -v test

找到cli的执行main方法:
https://insight.io/github.com/apache/hive/blob/master/cli/src/java/org/apache/hadoop/hive/cli/CliDriver.java?line=685

1
2
3
4
public static void main(String[] args) throws Exception {
int ret = new CliDriver().run(args);
System.exit(ret);
}

main方法调用了CliDriver实体的runmethod:
在run methond中最后返回的是executeDriver方法

1
2
3
4
public  int run(String[] args) throws Exception {
。。。。。。。。。略 。。。
return executeDriver(ss, conf, oproc);
。。。。。。。。。略

继续跟进executeDriver():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
  private int executeDriver(CliSessionState ss, HiveConf conf, OptionsProcessor oproc)
throws Exception {
。。。。。。。。略
while ((line = reader.readLine(curPrompt + "> ")) != null) {
if (!prefix.equals("")) {
prefix += '\n';
}
if (line.trim().startsWith("--")) {
continue;
}
if (line.trim().endsWith(";") && !line.trim().endsWith("\\;")) {
line = prefix + line;
ret = cli.processLine(line, true);
prefix = "";
curDB = getFormattedDb(conf, ss);
curPrompt = prompt + curDB;
dbSpaces = dbSpaces.length() == curDB.length() ? dbSpaces : spacesForString(curDB);
} else {
prefix = prefix + line;
curPrompt = prompt2 + dbSpaces;
continue;
}
}
。。。。。。。。。。略

executeDriver方法将一条sql用“;”拆分成多条语句,每条语句执行 ret = cli.processLine(line, true);

1
2
3
4
5
6
7
8
9
10
11
/**
* Processes a line of semicolon separated commands
* @param line The commands to process
* @param allowInterrupting When true the function will handle SIG_INT (Ctrl+C) by interrupting the processing and
* returning -1
* @return 0 if ok
*/
public int processLine(String line, boolean allowInterrupting) {
。。。。。。。。。略
ret = processCmd(command);
。。。。。。。。。略

然后进入processCmd方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public int processCmd(String cmd) {
。。。。。。。。。略。。。。。。。
if (cmd_trimmed.toLowerCase().equals("quit") || cmd_trimmed.toLowerCase().equals("exit")) {
。。。。。。。。。略。。。。。。。
} else if (tokens[0].equalsIgnoreCase("source")) {
。。。。。。。。。略。。。。。。。
} else if (cmd_trimmed.startsWith("!")) {
。。。。。。。。。略。。。。。。。
} else { // local mode
try {
CommandProcessor proc = CommandProcessorFactory.get(tokens, (HiveConf) conf);
ret = processLocalCmd(cmd, proc, ss);
} catch (SQLException e) {
console.printError("Failed processing command " + tokens[0] + " " + e.getLocalizedMessage(),
org.apache.hadoop.util.StringUtils.stringifyException(e));
ret = 1;
}
}
ss.resetThreadName();
return ret;

首先processCmd判断是不是退出命令,然后是source和“!”开始的特殊命令(非SQL)的处理,最后是sql的处理逻辑,
CommandProcessor proc = CommandProcessorFactory.get(tokens, (HiveConf) conf);这句生成了一个CommandProcessor ,那么CommandProcessor 是个什么鬼呢?进入get方法看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static CommandProcessor get(String[] cmd, HiveConf conf)
throws SQLException {
CommandProcessor result = getForHiveCommand(cmd, conf);
if (result != null) {
return result;
}
if (isBlank(cmd[0])) {
return null;
} else {
if (conf == null) {
return new Driver();//此处返回的是一个Driver,即Driver是CommandProcessor 的下属类型。
}
Driver drv = mapDrivers.get(conf);
if (drv == null) {
drv = new Driver();
mapDrivers.put(conf, drv);
} else {
drv.resetQueryState();
}
drv.init();
return drv;
}
}

所以

1
2
CommandProcessor proc = CommandProcessorFactory.get(tokens, (HiveConf) conf);
ret = processLocalCmd(cmd, proc, ss);

这里的proc是一个Driver,进入processLocalCmd:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int processLocalCmd(String cmd, CommandProcessor proc, CliSessionState ss) {
int tryCount = 0;
boolean needRetry;
int ret = 0;

do {
try {
needRetry = false;
if (proc != null) {
if (proc instanceof Driver) {//一定为true
Driver qp = (Driver) proc;
PrintStream out = ss.out;
long start = System.currentTimeMillis();
if (ss.getIsVerbose()) {
out.println(cmd);
}

qp.setTryCount(tryCount);
ret = qp.run(cmd).getResponseCode();//此处调用的是Driver的run
if (ret != 0) {
qp.close();
return ret;
}
。。。。。。。。。。略

进入run方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public CommandProcessorResponse run(String command)
throws CommandNeedRetryException {
return run(command, false);
}

public CommandProcessorResponse run(String command, boolean alreadyCompiled)
throws CommandNeedRetryException {
CommandProcessorResponse cpr = runInternal(command, alreadyCompiled);

if(cpr.getResponseCode() == 0) {
return cpr;
}
SessionState ss = SessionState.get();
if(ss == null) {
return cpr;
}
MetaDataFormatter mdf = MetaDataFormatUtils.getFormatter(ss.getConf());
if(!(mdf instanceof JsonMetaDataFormatter)) {
return cpr;
}

CommandProcessorResponse cpr = runInternal(command, alreadyCompiled);执行sql的编译和返回结果:

1
2
3
4
5
6
 private CommandProcessorResponse runInternal(String command, boolean alreadyCompiled)
throws CommandNeedRetryException {
。。。。。略
// compile internal will automatically reset the perf logger
ret = compileInternal(command, true);
。。。。。。。略

compileInternal方法:

1
2
3
4
5
6
7
8
private int compileInternal(String command, boolean deferClose) {
。。。。。。略。。。。

try {
ret = compile(command, true, deferClose);
} finally {
compileLock.unlock();
}

compile(command, true, deferClose);
就是hive的入口了。
Driver的run方法最终会执行compile()操作,Compiler作语法解析和语义分析。
回顾一下解析步骤:
第一部分:语法分析
语法解析Parser
这里写图片描述

tree = ParseUtils.parse(command, ctx);【源码】ParseUtils封装了ParseDriver 对sql的解析工作,ParseUtils的parse方法:

1
2
3
4
5
6
7
8
9
/** Parses the Hive query. */
public static ASTNode parse(
String command, Context ctx, boolean setTokenRewriteStream) throws ParseException {
ParseDriver pd = new ParseDriver();
ASTNode tree = pd.parse(command, ctx, setTokenRewriteStream);
tree = findRootNonNullToken(tree);
handleSetColRefs(tree);
return tree;
}

ParseDriver 对command进行词法分析和语法解析(统称为语法分析),返回一个抽象语法树AST,进入parseDriver的parse方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public ASTNode parse(String command, Context ctx, boolean setTokenRewriteStream)
throws ParseException {
if (LOG.isDebugEnabled()) {
LOG.debug("Parsing command: " + command);
}

HiveLexerX lexer = new HiveLexerX(new ANTLRNoCaseStringStream(command));//词法分析
TokenRewriteStream tokens = new TokenRewriteStream(lexer);//根据词法分析的结果得到tokens的,此时不只是单纯的字符串,而是具有特殊意义的字符串的封装,其本身是一个流。
if (ctx != null) {
if ( setTokenRewriteStream) {
ctx.setTokenRewriteStream(tokens);
}
lexer.setHiveConf(ctx.getConf());
}
HiveParser parser = new HiveParser(tokens);
if (ctx != null) {
parser.setHiveConf(ctx.getConf());
}
parser.setTreeAdaptor(adaptor);
HiveParser.statement_return r = null;
try {
r = parser.statement();
} catch (RecognitionException e) {
e.printStackTrace();
throw new ParseException(parser.errors);
}

if (lexer.getErrors().size() == 0 && parser.errors.size() == 0) {
LOG.debug("Parse Completed");
} else if (lexer.getErrors().size() != 0) {
throw new ParseException(lexer.getErrors());
} else {
throw new ParseException(parser.errors);
}

ASTNode tree = (ASTNode) r.getTree();//生成AST返回
tree.setUnknownTokenBoundaries();
return tree;
}

Antlr对Hive SQL解析的代码如上述代码逻辑,HiveLexerX,HiveParser分别是Antlr对语法文件HiveLexer.g编译后自动生成的词法解析和语法解析类,在这两个类中进行复杂的解析。
这是解析的第一步,生辰了一个ATS。
看一下之后的词法分析,
词法分析器Lexer - HiveLexerX
输入:一堆字符,这里是HiveSQL
输出:一串Toker,这里是TokenRewriteStream
也称词法分析器 **Lexical Analyzer(LA)**或者Scanner
建议翻阅《编译原理》
上文提到HiveLexer.g,即文法分析依靠一个文件HiveLexer.g
文件定义了一些hive的关键字,form、where,数字的定义格式【0–9】,分隔符,比较符之类的etc。每一个关键字都会变成一个token。
例如:规定hive中以数字或者下划线开头:

1
2
3
4
CharSetName
:
'_' (Letter | Digit | '_' | '-' | '.' | ':' )+
;

如果你对这个规则不满意可以修改它。
语法解析 HiveParser:
如何获得ASTNode
HiveParser.statement().getTree()
https://www.codatlas.com/github.com/apache/hive/master/ql/src/java/org/apache/hadoop/hive/ql/parse/ParseDriver.java?line=197
HiveParser是Antlr根据HiveParser.g生成的文件
进入HiveParser .java看到第一行:

1
// $ANTLR 3.5.2 org/apache/hadoop/hive/ql/parse/HiveParser.g 2017-05-03 10:08:46

此Java文件在2017-05-03被生成的。但是HiveParser.g我们进去看一下:
用select字句举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
selectStatement
:
a=atomSelectStatement
set=setOpSelectStatement[$atomSelectStatement.tree]?
o=orderByClause?
c=clusterByClause?
d=distributeByClause?
sort=sortByClause?
l=limitClause?
{
if(set == null){
$a.tree.getFirstChildWithType(TOK_INSERT).addChild($o.tree);
$a.tree.getFirstChildWithType(TOK_INSERT).addChild($c.tree);
$a.tree.getFirstChildWithType(TOK_INSERT).addChild($d.tree);
$a.tree.getFirstChildWithType(TOK_INSERT).addChild($sort.tree);
$a.tree.getFirstChildWithType(TOK_INSERT).addChild($l.tree);
}
}
-> {set == null}?
{$a.tree}
-> {o==null && c==null && d==null && sort==null && l==null}?
{$set.tree}
-> ^(TOK_QUERY
^(TOK_FROM
^(TOK_SUBQUERY
{$set.tree}
{adaptor.create(Identifier, generateUnionAlias())}
)
)
^(TOK_INSERT
^(TOK_DESTINATION ^(TOK_DIR TOK_TMP_FILE))
^(TOK_SELECT ^(TOK_SELEXPR TOK_SETCOLREF))
$o? $c? $d? $sort? $l?
)
)
;

用图形表示:
这里写图片描述
TMP_FIEL是输出路径,hive是基于mr的上层框架,mr必须要有一个数据文件,mr任务完毕之后结果会存放在TMP_FIEL此路径下边,然后cli回去读取这个结果文件,展示数据结果。而另一个框架瞅准了hive的这个弱点,没有临时文件,impala边执行边输出结果。

增加一种语法这时候,你知道了……
如果我们想为Hive增加一种新的语法……
第一步……
就是修改HiveParser.g
如果要引入关键字,还需要修改HiveLexer.g

第二部分:语义解析初步 - SemanticAnalyzer
这里写图片描述
SQL执行顺序
一个SQL大致分为以下7部分,按顺序执行
(5)SELECT (6)DISTINCT