原文章
http://whatua.com/2018/12/02/hive-sql-format-with-antlr/
以下在网上搜集了一些比较好的sql格式化工具,各有利弊。但对hive sql 来说目前还没有比较好的工具可以直接拿过来用。
1. ApexSQL Refactor SQL formatter
https://www.apexsql.com/sql-tools-refactor.aspx
2. SQL Pretty Printer
Instant SQL Formatter (在线免费版本 ) http://www.dpriver.com/pp/sqlformat.htm
SQL Pretty Printer (桌面应用收费版本) http://www.dpriver.com/products/sqlpp/desktop_index.php
3. druid
https://github.com/alibaba/druid 开源免费
druid(alibaba)实现了sql语法的分析(和antlr生成的parse非常像),目前已经支持大部分常用的sql语法。查看其代码发现Terence Parr(antlr的作者)说的挺对的,词法和语法分析十分的枯燥,而且容易出现错误。可维护性特别差。
druid内部实现了一个format方法并支持很多种sql语法,虽然druid的主要方向并不在此,有些大柴小用但是如果可用也是极好的。目前看druid在hive的语法实现上不完全,有些语法还未支持(比如定义个es的外表)。但是要实现具体的语法,门槛还是有的,相较antlr还是有点复杂、而且学习该技能通用型较差。实地勘探发现druid在sql格式化方法中的两个问题,当然在格式化这个问题上这个还存在另外一个更大的问题就是注释(这个它并没有关注,只是保留了两个listener):
1 ) 这个地方匹配末尾的一个 \n 如果是最后一行的注释没有换行符号 这个会报上个 intifiy error
代码中找: // single line comment结束符错误 这个
2) sqlformat features 参数没有向内部传递,配置了不生效。里面还没实现这些逻辑
public static SQLStatementParser createSQLStatementParser(String sql, String dbType, SQLParserFeature… features) {
if (JdbcUtils.ORACLE.equals(dbType) || JdbcUtils.ALI_ORACLE.equals(dbType)) {
相对问题看它的实现方式还很有参考价值的,性能毕竟是druid的核心关注。druid hivesql格式化的具体执行过程:
1)类的继承和集成关系
<–SQLExprParser <–HiveExprParser { HiveLexer(实例化) }
SQLParser { Lexer } |(实例化)
<– SQLStatementParser { SQLExprParser } <– HiveStatementParser
2)类的构建过程
SQLParser { Lexer } <– SQLStatementParser { SQLExprParser: HiveExprParser { Lexer: HiveLexer(实例化) } , Lexer: SQLExprParser的HiveLexer } <– HiveStatementParser
题外话: druid构建了在线语法解析的工具,对于权限控制、注入检测、分库分表等有非常大的实际使用价值(公有云的权限控制,公司集团内部的代理中间件等等都有很好的场景)。
hive中的sql parser 使用的是antlr ,目前用的是3.5.2。 antlr基于ll(*)实现的ll(*) parser
论文:
1. LL(*): The Foundation of the ANTLR Parser Generator https://www.antlr.org/papers/LL-star-PLDI11.pdf
2. Adaptive LL(*) Parsing: The Power of Dynamic Analysis https://www.antlr.org/papers/allstar-techreport.pdf
ll parser wiki: https://en.wikipedia.org/wiki/LL_parser
LL(*)的概念与实现 http://pfmiles.github.io/blog/concept-and-implementation-of-ll-star/
语法分析的各种工具对比:
https://en.wikipedia.org/wiki/Comparison_of_parser_generators
https://stackoverflow.com/questions/41427905/how-many-ways-are-there-to-build-a-parser
hive-master/ql/pom.xml => org.antlr
自己构建
下载antlr https://www.antlr3.org/download/antlr-3.5.2-complete.jar
1) cp hive-master/ql/src/java/org/apache/hadoop/hive/ql/parse/*.g ./
2)
whomm@bogon > ~/Desktop/hiveparse > mkdir output
whomm@bogon > ~/Desktop/hiveparse > which antlr3
antlr3: aliased to java -jar /usr/local/lib/antlr-3.5.2-complete.jar
whomm@bogon > ~/Desktop/hiveparse > ll
total 328
-rwxr-xr-x@ 1 whomm staff 11K 10 29 11:02 FromClauseParser.g
-rwxr-xr-x@ 1 whomm staff 1.9K 10 29 11:02 HintParser.g
-rwxr-xr-x@ 1 whomm staff 11K 10 29 11:02 HiveLexer.g
-rwxr-xr-x@ 1 whomm staff 91K 10 29 11:02 HiveParser.g
-rwxr-xr-x@ 1 whomm staff 23K 10 29 11:02 IdentifiersParser.g
-rwxr-xr-x@ 1 whomm staff 11K 10 29 11:02 ResourcePlanParser.g
-rwxr-xr-x@ 1 whomm staff 5.7K 10 29 11:02 SelectClauseParser.g
drwxr-xr-x 2 whomm staff 68B 11 2 10:18 output
whomm@bogon > ~/Desktop/hiveparse > antlr3 HiveParser.g -o output
hive源码中语法解析的过程
at org.apache.hadoop.hive.ql.parse.HiveParser.statement(HiveParser.java:1028)
at org.apache.hadoop.hive.ql.parse.ParseDriver.parse(ParseDriver.java:201)
at org.apache.hadoop.hive.ql.parse.ParseDriver.parse(ParseDriver.java:166)
at org.apache.hadoop.hive.ql.Driver.compile(Driver.java:418)
at org.apache.hadoop.hive.ql.Driver.compile(Driver.java:312)
at org.apache.hadoop.hive.ql.Driver.compileInternal(Driver.java:1201)
at org.apache.hadoop.hive.ql.Driver.runInternal(Driver.java:1296)
at org.apache.hadoop.hive.ql.Driver.run(Driver.java:1127)
at org.apache.hadoop.hive.ql.Driver.run(Driver.java:1115)
at org.apache.hadoop.hive.cli.CliDriver.processLocalCmd(CliDriver.java:220)
at org.apache.hadoop.hive.cli.CliDriver.processCmd(CliDriver.java:172)
at org.apache.hadoop.hive.cli.CliDriver.processLine(CliDriver.java:383)
at org.apache.hadoop.hive.cli.CliDriver.executeDriver(CliDriver.java:775)
at org.apache.hadoop.hive.cli.CliDriver.run(CliDriver.java:693)
at org.apache.hadoop.hive.cli.CliDriver.main(CliDriver.java:628)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:497)
at org.apache.hadoop.util.RunJar.run(RunJar.java:221)
at org.apache.hadoop.util.RunJar.main(RunJar.java:136)
FAILED: ParseException line 1:0 cannot recognize input near ‘com’ ‘sdfsdf’ ‘
antlr3 HiveLexer.g -o out #目前hive master分支antlr依赖的是3.5.2版本
antlr3 HiveParser.g -o out
maven中添加antlr&编译
${basedir}/src/java
${basedir}/src/test
org.antlr
antlr3-maven-plugin
antlr
${basedir}/src/main/java/whomm/hsqlformat/hive/parse
**/HiveLexer.g
**/HiveParser.g
命令:mvn org.antlr:antlr3-maven-plugin:antlr
maven 配置参考:
1. ANTLR 3.x Creating and Executing a Grammar in Eclipse https://vimeo.com/8015802
2. https://alexecollins.com/antlr4-and-maven-tutorial/
不得不说这个antlr是个好产品,确实把复杂的事情简单化了。
1. 逻辑和语法定义是分离的,语法的定义简单明了。
2. 能直接生成语法树,支持生成各种语言。
Hive-master的hive语法包含几个文件(hive-master/ql/src/java/org/apache/hadoop/hive/ql/parse):
HiveLexer.g
LINE_COMMENT : ‘–‘ (~(‘\n’|’\r’))* { $channel=HIDDEN; }
行注释默认被送入到hidden通道了,语法解析的时候会被直接忽略
特殊的字符作为变量就在这个文件中修改
FromClauseParser.g
from语句解析
IdentifiersParser.g
标识符定义 函数名称、系统函数、关键字等
里面有个 nonReserved这个很重要非保留的关键字是可以作为标识符的。
比如 select a as date from mytable 这个date不添加转义会报错的,但是该处如果添加 “ | KW_DATE ” date可直接作为标识符使用
ResourcePlanParser.g
资源计划
SelectClauseParser.g
select语句解析
HiveParser.g
import了 SelectClauseParser, FromClauseParser, IdentifiersParser, ResourcePlanParser 实现了所有的hive语法解析
statement 就是入口规则
HintParser.g
hive的hint语法解析
如果 parser里面添加带有linecomment的语法识别还是比较繁琐的,毕竟语法解析的目标是实际的语句。如果利用语法树每个节点的 line 和 charPositionInLine 和 lexer后的comment token的 line 和 charpositoninline ,的前后位置关系来确定注释所在的位置,简单看是行不通的因为:
parse 后的树里面的接近keyword的 token 是没有 line 和 charPositionInLine的。这个是符合逻辑的,AST有些是逻辑节点是不涉及具体 keyword的,或者是n个keyword生成的逻辑节点 (Stack Overflow里面也有相关讨论:https://stackoverflow.com/questions/9954882/antlr-preserve-line-number-and-position-in-tree-grammar) 。
虽然这个思路是不错的。但这个思路也会存在一些问题:
以下两个例子是查询语句中的场景。ddl语句中的create语句,逻辑节点少不会出现这种情况。
1)逻辑节点被优先遍历到
例如 union all 节点。
select a.id from a union all select b.id from b union all select c.id from c
此时会生成
TOK_QUERY
TOK_UNIONALL
TOK_UNIONALL
TOK_QUERY ( select a.id from a )
TOK_QUERY (select b.id from b)
TOK_QUERY (select c.id from c)
这样的棵树, 深度遍历树的时候遇到这个节点,如果这个节点是需要输出的。
查看当前node和comment的偏移量。如果当前node大于等于 comment 的 行号 和 所在的列 就输出comment。
union all 节点会被优先遍历到,所以会将 第一个select 里面的comment 提前输出来。
2)新增的虚拟节点
但是对于query类型的sql语句解析成语法树的时候会在 ast tree上 增加子查询的虚拟节点。
比如:
select id from ( select a.id from a union all select b.id from b union all select c.id from c) tmp
TOK_QUERY
TOK_FROM
TOK_SUBQUERY
TOK_QUERY
TOK_FROM
TOK_SUBQUERY
TOK_UNIONALL
TOK_UNIONALL
TOK_QUERY
TOK_QUERY
TOK_QUERY
_u1
TOK_INSERT
…
tmp
TOK_INSERT
…
_u1即为解析过程中新增的逻辑节点。这个新增出来的子查询也会导致后面的comment被提前输出。
为此只能思考更加精密的方法。目前制造了个简单的工具
https://github.com/whomm/hsqlformat