目标
系列第三篇里做了基本的AST遍历。
在深入做SQL中的表名列名提取前,还需要先解决第三篇里遗留的两个实用性问题,分号和大小写
分号问题
分号问题的表现是自动生成的HiveParser.java代码,只能解析单个的语句,对包含多个语句的sql文本会报错,甚至连单个语句结尾多一个分号都不行。例如这种
SELECT DISTINCT a1.c1 c2, a1.c3 c4, '' c5 FROM db2.tb2 a1 ;
还有这种
SELECT DISTINCT a1.c1 c2, a1.c3 c4, '' c5 FROM db2.tb2 a1 ;
SELECT c6 FROM tb3 ;
原因
引发这个问题的地方很好找,就在HiveParser.g里面,具体就在statement()方法对应的这条RULE上。
// starting rule
statement
: explainStatement EOF
| execStatement EOF
;
这段天书的符号体系和正则表达式很像,简单翻译成人间语言的意思是,HiveParser接受的输入都是statement,一个statement可以是一个explainStatement 加上EOF组成,也可以是execStatement加上EOF组成。explainStatement和execStatement都是有具体类型的单个语句
这个体验不符合使用hive的经验啊,最常用提交Hive语句的方式是通过hive client,它肯定是能处理多个语句的。要往下解决这个分号问题,有两个明显的方向可选
- 扩大使用hive源码的范围
- 修改HiveParser.g里的语法规则
但要怎么选呢?笔者在Hive源码里上下翻找了一通,再结合Hive实际的使用情形后有了推断。
- HiveParser.g 的语法规则只被藏在Hive执行引擎深处的代码调用,调用时的输入已经被裁剪约束到只包含单个语句
- 最常用提交Hive语句的方式是要通过hive client,它还需要被翻译到或者thrift,或者jdbc协议上的调用。
- Hive client能处理的脚本里,会存在一部分conf设置、运行参数替换这样,需要在执行hive sql前预处理的语句
有了以上几个推断,结论也很显然,处理血缘关系的需求并不需要完备的hive client处理能力,而且裁剪hive源码的工作量不太可控,所以要选择2 ,修改HiveParser.g里的语法规则
如果对1还有兴趣,可以从研究源码同目录下的ParseDriver.java类出发。
规则修订
现在的规则名为statement, 一个语句后面就是EOF结束,作为写过正则的笔者,很自然的想法是,如果能让statement这个rule匹配上多次,分号分隔一下,是不是可以?试验过程略去不说,成功的结果如下
// starting rule
statements
: statement (SEMICOLON statement )* SEMICOLON* EOF
;
statement
: explainStatement | execStatement
;
把原先statement里这个EOF去掉,两行并做一行
然后增加一个statments这个复数名字的rule,SEMICOLON是从HiveLexer.g里的定义找出的,表示分号的写法。在.g文件里的括号()
表示括号内的内容作为一个整体判断,*
的作用和正则表达式里类似,表示前面的这个整体出现0到无限次,合起来后的效果就是,一段文本里可以有多个语句,语句之间是单个分号分隔,但最末尾的语句后的分号可以省略
验证输出
测试sql语句如下
SELECT DISTINCT a1.c1 AS c2,
a1.c3 AS c4,
'' c5
FROM db2.tb2 a1 ;
SELECT c6 FROM tb3
修改后要重新产生java代码和编译成class
java -jar antlr-3.4-complete.jar HiveLexer.g HiveParser.g
javac -cp antlr-3.4-complete.jar HiveLexer.java HiveParser*.java ParseError.java
继续用上一篇里写的遍历AST树脚本,脚本比较长,后面还有一个问题,就不重复贴了,只把输出部分复制如下
None=0
TOK_QUERY=777
TOK_FROM=681
TOK_TABREF=864
TOK_TABNAME=863
db2=26
tb2=26
a1=26
TOK_INSERT=707
TOK_DESTINATION=660
TOK_TAB=835
TOK_TABNAME=863
db1=26
tb1=26
TOK_SELECTDI=792
TOK_SELEXPR=793
.=17
TOK_TABLE_OR_COL=860
a1=26
c1=26
c2=26
TOK_SELEXPR=793
.=17
TOK_TABLE_OR_COL=860
a1=26
c3=26
c4=26
TOK_SELEXPR=793
''=302
c5=26
;=299
TOK_QUERY=777
TOK_FROM=681
TOK_TABREF=864
TOK_TABNAME=863
tb3=26
TOK_INSERT=707
TOK_DESTINATION=660
TOK_DIR=661
TOK_TMP_FILE=873
TOK_SELECT=791
TOK_SELEXPR=793
TOK_TABLE_OR_COL=860
c6=26
<EOF>=-1
可以看到没有报错,并且输出的树内容上,有两个TOK_QUERY节点,对应到两个select语句
大小写问题
大小写问题的表现是,自动生成的HiveParser.java里,会需要输入的sql文本内容里,关键字只能是大写的,小写的关键字会被识别为标识符,然后因为不符合语法规则解析失败。
和前面的分号问题了类似,也有两个选择
- 扩大使用hive源码的范围
- 修改HiveParser.g里的语法规则
这次的选择和分号问题不一样了,首先是antlr自身的文档上,在处理输入标识符的大小写问题上就有两个完全不同的做法。
- 对输入内容预处理,把所有的内容归一化成大写(或者小写)
- 在定义关键字时,单独做需要大小写无关(case-insensitive)的处理,如果所有的关键字都需要大小写无关,则所有的规则都要重新定义
第1点好理解,第2点可能略为晦涩,以select这个关键字为栗子说明。
在HiveLexer.g里,select的关键字是这么定义的
KW_SELECT : 'SELECT';
如果要做大小写无关处理,其中一种可行的写法是这样的
KW_SELECT : ('s'|'S')('e'|'E')('l'|'L')('e'|'E')('c'|'C')('t'|'T');
这个改动可行,就是动静有点大。而且很明显的事实是,Hive自己不是这么处理的,如果直接大规模去修改HiveLexer.g ,生成的新代码处理行为如何和Hive本身不一致,就费力不讨好了。所以这里适合对输入内容预处理,把所有的内容归一化成大写
归一化
归一化也还是有两种做法
- 扩大hive源码的使用范围,java里实现归一化。
- 在python里自己写代码实现归一化
出于尽量和Hive本身处理一直,而且改动不大的目的,选择了扩大hive源码的视野范围。
当然也是因为Hive源码里,有关归一化的代码很短小好处理的原因。
前一节里提到了ParseDriver.java,在里面定义了一个内部类,ANTLRNoCaseStringStream, 这个类起的作用就是处理输入字符流,把字符归一化到大写。把这部分代码抠出来,与前面的ParseError.java类似处理, ANTLRNoCaseStringStream.java的代码如下。
package grammar.hive110;
import org.antlr.runtime.ANTLRStringStream;
import org.antlr.runtime.CharStream;
public class ANTLRNoCaseStringStream extends ANTLRStringStream {
public ANTLRNoCaseStringStream(String input) {
super(input);
}
@Override
public int LA(int i) {
int returnChar = super.LA(i);
if (returnChar == CharStream.EOF) {
return returnChar;
} else if (returnChar == 0) {
return returnChar;
}
return Character.toUpperCase((char) returnChar);
}
}
代码修订
因为增加了归一化的代码,需要再重新编译,树的生成代码也略有变化
编译时要增加一个输入文件
java -jar antlr-3.4-complete.jar HiveLexer.g HiveParser.g
javac -cp antlr-3.4-complete.jar HiveLexer.java HiveParser*.java ParseError.java ANTLRNoCaseStringStream.java
还使用之前的树生成代码,因为前面修改了语法规则,解析入口的方法名也要修改
修订后的python代码如下
import jnius_config
jnius_config.set_classpath('./','./grammar/hive110/antlr-3.4-complete.jar')
import jnius
StringStream = jnius.autoclass('grammar.hive110.ANTLRNoCaseStringStream')
Lexer = jnius.autoclass('grammar.hive110.HiveLexer')
Parser = jnius.autoclass('grammar.hive110.HiveParser')
TokenStream = jnius.autoclass('org.antlr.runtime.CommonTokenStream')
sql_string = (
"SELECT DISTINCT a1.c1 AS c2,\n"
" a1.c3 AS c4,\n"
" '' c5\n"
" FROM db2.tb2 AS a1 ;\n"
)
sqlstream = StringStream(sql_string)
inst = Lexer(sqlstream)
ts = TokenStream(inst)
parser = Parser(ts)
ret = parser.statements()
treeroot = ret.getTree()
傲慢程序员
发布了19 篇原创文章 · 获赞 0 · 访问量 815
私信
关注