系列:用python+antlr解析hive sql获得数据血缘关系(四)

目标

系列第三篇里做了基本的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,它肯定是能处理多个语句的。要往下解决这个分号问题,有两个明显的方向可选

  1. 扩大使用hive源码的范围
  2. 修改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文本内容里,关键字只能是大写的,小写的关键字会被识别为标识符,然后因为不符合语法规则解析失败。

和前面的分号问题了类似,也有两个选择

  1. 扩大使用hive源码的范围
  2. 修改HiveParser.g里的语法规则

这次的选择和分号问题不一样了,首先是antlr自身的文档上,在处理输入标识符的大小写问题上就有两个完全不同的做法。

  1. 对输入内容预处理,把所有的内容归一化成大写(或者小写)
  2. 在定义关键字时,单独做需要大小写无关(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本身不一致,就费力不讨好了。所以这里适合对输入内容预处理,把所有的内容归一化成大写

归一化

归一化也还是有两种做法

  1. 扩大hive源码的使用范围,java里实现归一化。
  2. 在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()

系列:用python+antlr解析hive sql获得数据血缘关系(四)系列:用python+antlr解析hive sql获得数据血缘关系(四) 傲慢程序员 发布了19 篇原创文章 · 获赞 0 · 访问量 815 私信 关注
上一篇:系列:用python+antlr解析hive sql获得数据血缘关系(三)


下一篇:在Ubuntu13.10下使用Eclipse搭建Hadoop-2.2.0 开发环境