从语言识别到通用SQL解析

1、前言

数仓在建设过程当中,逐步借鉴平台开发经验,每每也会引入CI/CD理念,须要静态代码扫描功能,提早识别如删库、删表、改表等危险操做。此外,对于Hive等计算引擎,能够经过Hook动态获取血缘,可是,对于MySQL、Vertica等,没法动态获取血缘,须要在任务调度过程当中,静态解析血缘。全部的这些,都须要一套通用SQL解析工具。但大数据计算引擎较多,如Hive,Spark,Impala,Vertica等,各类计算引擎都有本身的方言,且各个计算引擎使用的开发语言也不太同样。若是要实现通用SQL解析,必须屏蔽开发语言的差别,从语法规则入手,实现通用SQL解析。下面将分别从语言识别,ANTLR使用,并以Hive SQL为例,介绍通用SQL解析实现。html

2、语言识别器

SQL语言与咱们日常语言同样,由语法规则及词汇符号组成,对SQL的解析其实就是将输入的一系列字符拆分红词法符号Token,并按语法规则生成一颗语法树的过程,而对SQL的分析则是经过语法树遍历实现。java

语言识别过程

如上图一条赋值语句sp = 100;,将输入字符流拆分红一个个不可再分的词法符号的过程为词法分析,将词法符号流转换成语法分析树的过程为语法分析。node

2.1 词法分析

词法分析就是根据词法规则将输入字符拆分红一个个不可再分的词法符号的过程。以SQL为例,词法符号包括下面类型:mysql

  1. 标识符。如letter(letter|digit)*。
  2. 常量。
  3. 关键字。如select,from等。
  4. 分界符。如--,;等。
  5. 运算符。如<,<=,>,>=,=,+,-,*等。

词法分析器每次读取一个字符,在当前字符与以前的字符所属分类不一致时,即完成一个词法符号识别。例如,读取'SELECT'时,第一个字符是'S',知足关键字和标识符的规则,第二个字符也一样知足,以此类推,直到第7个字符是空格时再也不知足,从而完成一次词法符号的识别。此时,'SELECT'便可认为是关键字,也能够认为是标识符,分析器须要根据优先级来判断。git

SELECT name, age FROM student WHERE age > 10;
复制代码

举个例子,上述SQL通过词法分析后,能够获得以下词汇符号:github

  • 关键字:SELECT,FROM,WHERE
  • 标识符:name,age,student
  • 常量:10
  • 分界符:;
  • 运算符:>

从上面描述的分析过程能够看出,词法分析其实就是根据已输入字符及词法规则不断进行状态转移的过程,直到全部字符扫描完成。词法分析实现上是先将正规表达式RE经过Thompson构造法转换成不肯定性有穷自动机NFA,再经过子集构造法NFA转换成肯定性有穷状态机DFA,接着经过DFA最小化进行等价状态合并,最终经过DFA实现字符扫描得到词法符号。正则表达式

词法分析过程

2.2 语法分析

经过词法分析,能够将输入的字符拆分红一个个词法符号,这些词法符号如何按某种规则进行组合,造成有意义的词句就是语法分析过程。语法分析的难点在于规则处理以及分支的选择,还有递归调用以及复杂的计算表达式。在实现上主要有自顶向下分析以及自底向上分析两种算法,下面会详细介绍。redis

2.2.1 上下文无关文法

上下文无关法是一种规则用来约定一个语言的语法规则,由四个部分组成:算法

  • 一个终结符号集合;
  • 一个非终结符号集合;
  • 一个产生式列表;
  • 一个开始符号。

好比说一个算数表达式文法:spring

S -> E
E -> E + E
E -> E - E
E -> (E) | num
复制代码

在'->'左部称为产生式头部(左部),在'->'右部的称为产生式体(右部)。全部的产生式头部都是一个非终结符号。非终结符号描述的是一种抽象规则,终结符号描述的是一个具体的事物。上面示例中,SE非终结符号,其余是终结符号

假设有下面文法:

S → AB
A → aA|a
B → b
复制代码

用上述文法推导字符串aab过程以下:

S → AB → aAB → aaB → aab
复制代码

2.2.2 推导和规约

  • 推导:从开始符号出发,利用文法推导给定的字符串,即用产生式的右部替换产生式的左部。如上面示例:
S → AB → aAB → aaB → aab
复制代码
  • 归约:规约是推导的逆过程,就是把字符串变成非终结符,再把非终结符变成非终结符,不断进行直到能到根节点。一样以上面字符串为例:
aab → aAb → Ab → AB → S       
复制代码

老是选择最左非终结串进行替换的推导为最左推导,老是选择最右非终结符进行替换的推导为最右推导,也叫规范推导。推导过程必定对应一颗语法树,但推导过程可能不惟一,对应的语法树也可能不惟一。

规约做为推导的逆过程,最右推导的逆过程称为最左规约,也即规范规约最左推导的逆过程称为最右规约

还以上面的文法和字符串为例,说明一下推导和规约流程: 推导规约示例

2.2.3 LL分析与LR分析

  • LL分析:从语句最左侧符号开始读取,从左向右,使用最左推导,直到达到终结符或者报错退出。第一个L含义为从左向右读取字符,第二个L含义为使用最左推导。表现上就是从文法规则到语句字符串的分析过程,即自顶向下的处理流程。
  • LR分析:从语句最左侧符号开始读取,从左向右,使用最右推导(最左规约)的分析方法。第一个L含义为从左向右读取字符,第二个R含义为使用最右推导。表现上就是从语句字符串到文法规则的分析过程,即自底向上的处理流程。

在LL分析中,一般有预测/输出匹配供语法分析器选择,其中:

  • 预测/输出:根据最左侧的非终结符以及一系列向前看词汇符号,肯定当前输入下匹配几率最高的产生式,并输出或者进行其余动做。
  • 匹配:根据输入最左侧未被匹配的符号,来匹配上一阶段所预测的产生式。

以上述aab为例,使用LL(1)分析过程以下,其中1表示每次向前读取一个字符,

Production       Input              Action  
--------------------------------------------------------- 
S                aab                Predict S -> AB
AB               aab                Predict A -> aA
aAB              aab                Match a
AB               ab                 Predict A -> a
aB               ab                 Match a
B                b                  Predict B -> b
b                b                  Match b
                                    Accept
复制代码

在LR分析中,一般有移入规约两个动做供语法分析器选择,其中:

  • 移入:将当前被指向的词汇符号放入到缓冲区(一般为栈)中。
  • 规约:经过将产生式与缓冲区中一个或多个符号进行逆向匹配,将该符号串转换为对应产生式中的非终结符号。

一样以上述aab为例,使用LR(1)分析过程以下:

Buffer           Input              Action
---------------------------------------------------------
                 aab                Shift
a                ab                 Shift
aa               b                  Reduce A -> a
aA               b                  Reduce A -> aA
A                b                  Shift
Ab                                  Reduce B -> b
AB                                  Reduce S -> AB
S                                   Accept
复制代码

从上面到处理流程差别能够看出,LLLR有下面区别:

  1. LL是自上而下的分析过程,从文法规则出发,根据产生式推导给定的符号串,用的是推导。LR是自下而上的分析过程,从给定的符号串规约到文法的符号,用的是规约。
  2. LRLL效率更高,没有左递归及二义性,如E -> E + E这种规则,应用LL时将会有递归产生。
  3. LR生成的代码与LL相比,过于晦涩难懂。JavaCC是LL(1)算法,ANTLR是LL(n, n>=1)算法。

3、经常使用SQL解析器对比

当前,对于SQL的解析工具主要有两大类:

  • 经过手工编写Parser,典型表明如SQL Parser(Druid中的一个模块),JSQLParser等。
  • 经过语法解析工具生成Parser的自定义语法类型解析器,典型表明如ANTLR,JavaCC等。

二者Parser在生成上的差别性,决定了他们在使用上的差别性:

  • 性能上:手工编写的Parser,能够作各类优化,性能要远高于工具生成的Parser。好比,SQL Parser,性能比ANTLR、JavaCC工具生成的Parser快10倍甚至100倍以上。
  • 语法支持上:二者均支持多种语法,但工具生成的Parser实现更容易,支持也更多。好比SQL Parser,对于Oracle、Hive、DB2等只支持常见的DML和DDL。
  • 可读性和可维护性:以ANTLR为例,其语法与代码解耦,可读性更好,在新增语法时,只须要简单修改一下语法文件便可实现。而SQL Parser将语法规则与代码耦合,可读性较差,不多的语法变动就须要改动大量代码。
  • 语法树遍历上:SQL Parser采用Visitor模式将抽象语法树彻底封装,外围程序没法直接访问抽象语法树,在无需彻底遍历树时,代码比较繁琐。而ANTLR支持visitor和listenor访问方式,能够控制语法树的遍历。

固然,对于自定义语法类型解析器,ANTLR和JavaCC,二者在功能上差很少,但ANLTR更丰富一点,且跨语言,而JavaCC只能在Java中使用。

显然,从语法支持上,可读性和可维护性,语法树遍历上,ANTLR是最佳选择,下面会着重介绍。

4、ANTLR

ANTLR是Another Tool for Language Recognition的简写,是一个用Java语言编写的识别器工具。它可以自动生成解析器,并将用户编写的ANTLR语法规则直接生成目标语言的解析器,它可以生成Java、Go、C等语言的解析器客户端。

ANTLR所生成的解析器客户端将输入的文本生成抽象语法树,并提供遍历树的接口,以访问文本的各个部分。ANTLR的实现与前文所讲述的词法分析与语法分析是一致的。词法分析器根据词法规则作词法单元的拆分;语法分析器对词法单元作语义分析,并对规则进行优化以及消除左递归等操做。

ANTLR的安装使用可参考官网

4.1 语法和词法规则

4.1.1 语法文件结构

在ANTLR中,语法文件以.g4结尾,若是语法规则和文法规则放在一个文件中,针对Name语法文件名为Name.g4,若是语法文件和词法文件单独放,则语法文件必须命名为NameParser.g4,词法文件必须命名为NameLexer.g4。一个基本语法文件结构以下: 语法文件规则

  • grammar:指定了语法名。纯语法文件声明使用parser grammar Name;,纯词法文件声明使用lexer grammar Name;
  • options:预留功能。
  • tokens:声明词法符号,存在乎义在于语法文件中可能未定义词法符号,但在语法文件中要使用,通常和action配合使用。
  • actionName:在语法规则以外使用动做,用于目标语言中,对于JAVA目前有header何members,若是指望只在词法分析器中使用,使用@lexer::name,若是指望只在语法分析器中使用,使用@parser::name
    • header:定义类文件头,好比嵌入java的package、import声明。
    • members:定义类文件内容,好比类成员、方法。

4.1.2 语法文件示例

为了区分语法和词法规则,首字母小写的为语法规则首字母大写的为词法规则。以josn语法文件示例,语法名为JSON,文件名为JSON.g4,具体内容以下:

// 指定语法名
grammar JSON;

// 一条语法规则
json
   : value
   ;

// 带有多个备选分支的语法规则
// 对于'true',为隐式定义的词法符号
// 备选分支中#表明标签,能够生成更加精确的监听器事件,
// 一条规则中的备选分支要么所有带上标签,要么所有不带标签
value
   : STRING       # ValueString
   | NUMBER       # ValueNumber
   | obj          # ValueObj
   | arr          # ValueArr
   | 'true'       # ValueTrue
   | 'false'      # ValueFalse
   | 'null'       # ValueNull
   ;

obj
   : '{' pair (',' pair)* '}'
   | '{' '}'
   ;

pair
   : STRING ':' value
   ;

arr
   : '[' value (',' value)* ']'
   | '[' ']'
   ;

// 一条词法规则
// 和普通的正则表达式相似,可使用通配符,|表示或,*表示出现0次或以上
// ?表示出现0次或1次,+表示出现1次或以上,~表示取反,?一样支持通配符的贪婪与非贪婪模式
STRING
   : '"' (ESC | SAFECODEPOINT)* '"'
   ;

// 使用fragment修饰的词法规则,该标识表示该词法规则不能单独应用于语法规则中,只能做为词法规则的一个词法片断
fragment ESC
   : '\\' (["\\/bfnrt] | UNICODE)
   ;

fragment UNICODE
   : 'u' HEX HEX HEX HEX
   ;

fragment HEX
   : [0-9a-fA-F]
   ;

fragment SAFECODEPOINT
   : ~ ["\\\u0000-\u001F]
   ;

NUMBER
   : '-'? INT ('.' [0-9] +)? EXP?
   ;

fragment INT
   : '0' | [1-9] [0-9]*
   ;

fragment EXP
   : [Ee] [+\-]? INT
   ;

// 隐藏通道,用于将不须要关注的如注释、空格等发送到隐藏通道中,须要使用时使用ANLTR的API获取
WS
   : [ \t\n\r] + -> skip
   ;
复制代码

如下面josn文本为例:

{
    "string":"字符串",
    "num":2,
    "obj":{
        "arr":[
            "English",
            "中文",
            123,
            -12.45,
            23.64e+3,
            true,
            "abc{db}def",
            {

            }
        ]
    }
}
复制代码

经过词法分析器,能够将输入字符转换成词法符号流:

[@0,0:0='{',<'{'>,1:0]
[@1,6:13='"string"',<STRING>,2:4]
[@2,14:14=':',<':'>,2:12]
[@3,15:19='"字符串"',<STRING>,2:13]
[@4,20:20=',',<','>,2:18]
[@5,26:30='"num"',<STRING>,3:4]
[@6,31:31=':',<':'>,3:9]
[@7,32:32='2',<NUMBER>,3:10]
[@8,33:33=',',<','>,3:11]
[@9,39:43='"obj"',<STRING>,4:4]
[@10,44:44=':',<':'>,4:9]
[@11,45:45='{',<'{'>,4:10]
[@12,55:59='"arr"',<STRING>,5:8]
[@13,60:60=':',<':'>,5:13]
[@14,61:61='[',<'['>,5:14]
[@15,75:83='"English"',<STRING>,6:12]
[@16,84:84=',',<','>,6:21]
[@17,98:101='"中文"',<STRING>,7:12]
[@18,102:102=',',<','>,7:16]
[@19,116:118='123',<NUMBER>,8:12]
[@20,119:119=',',<','>,8:15]
[@21,133:138='-12.45',<NUMBER>,9:12]
[@22,139:139=',',<','>,9:18]
[@23,153:160='23.64e+3',<NUMBER>,10:12]
[@24,161:161=',',<','>,10:20]
[@25,163:166='true',<'true'>,11:0]
[@26,167:167=',',<','>,11:4]
[@27,181:192='"abc{db}def"',<STRING>,12:12]
[@28,193:193=',',<','>,12:24]
[@29,211:211='{',<'{'>,17:12]
[@30,226:226='}',<'}'>,19:12]
[@31,236:236=']',<']'>,20:8]
[@32,242:242='}',<'}'>,21:4]
[@33,244:244='}',<'}'>,22:0]
[@34,246:245='<EOF>',<EOF>,23:0]
复制代码

经过语法分析器,能够实现将词法符号转换为语法树: json示例

4.1.3 常见词法规则

  1. 匹配优先级

词法规则在匹配时,若是输入串可以被多个词法规则匹配到,那么声明在前面的规则优先生效。

  1. 词法模式

词法模式容许将词法规则按上下文分组,词法分析器以默认模式开始,除非使用mode指令指定,不然都处于默认模式下。好比对XML的分析,标签体内,须要切割出多个属性等,标签体外,总体文本看成一个标签体。

<<rules in default mode>>
...
mode MODE1;
<<rules in MODE1>>
...
mode MODE2;
<<rules in MODE2>>
...
mode MODEN;
<<rules in MODEN>>
复制代码

以XMLLexer.g4片断为例:

// 遇到'<',进入INSIDE模式
OPEN        :   '<'                     -> pushMode(INSIDE) ;


// INSIDE模式词汇规则定义
mode INSIDE;
// 遇到'>',退出INSIDE模式
CLOSE       :   '>'                     -> popMode ;
SLASH       :   '/' ;
EQUALS      :   '=' ;
STRING      :   '"' ~[<"]* '"'
            |   '\'' ~[<']* '\''
            ;
复制代码
  1. 词法规则动做

词法分析器在匹配到一条词法规则后会生成一个词法符号对象,若是指望在匹配过程当中修改词法符号类型,能够经过词法规则动做来实现。

ENUM : 'enum' {if (!enumIsKeyword) setType(Identifier);};
复制代码
  1. 语义判断

在词法分析过程当中,经常须要动态地开启和关闭词法符号,此时能够经过语义判断来实现。

ENUM : 'enum' {java5}? ;
ID : [a-zA-Z]+
复制代码

好比在java 1.5版本以前,enum只是一个标识符,能够用来定义变量,在1.5版本以后,enum被用做关键字,若是用1.5版本以后的语法规则去编译1.5版本以前的代码,会编译失败,此时,经过语义判断能够实现词法规则的关闭,当java5值为true时,打开该词法规则,不然会关闭该词法规则。注意,因ENUMID两条都可以匹配enum这个输入串,如前面所述,必须将ENUM放在前面,让词法分析器优先匹配。

4.1.4 常见语法规则

  1. 备选分支标签

ANTLR根据语法文件生成的用于语法树分析的监听器中,每一个语法规则都会建立一个方法,但对于一条规则有多个备选分支时,使用较为不便,能够给每一个备选分支增长分支标签,这样在生成监听器时,每一个备选分支都会生成一个方法。上面的JSON语法文件中value规则为例:

// 使用备选分支生成的源码
public class JSONBaseListener implements JSONListener {
	@Override public void enterJson(JSONParser.JsonContext ctx) { }
	@Override public void exitJson(JSONParser.JsonContext ctx) { }
	
	// 使用备选分支标签时,不对规则生成方法,只对标签生成方法
	@Override public void enterValueString(JSONParser.ValueStringContext ctx) { }
	@Override public void exitValueString(JSONParser.ValueStringContext ctx) { }
	@Override public void enterValueNumber(JSONParser.ValueNumberContext ctx) { }
	@Override public void exitValueNumber(JSONParser.ValueNumberContext ctx) { }
    
    ...
	
	@Override public void visitTerminal(TerminalNode node) { }
	@Override public void visitErrorNode(ErrorNode node) { }
}

// 未使用备选分支生成的源码
public class JSONBaseListener implements JSONListener {
	@Override public void enterJson(JSONParser.JsonContext ctx) { }
	@Override public void exitJson(JSONParser.JsonContext ctx) { }
    
    ...
	
	// 未使用备选分支标签时,只对规则生成了方法
	@Override public void enterValue(JSONParser.ValueContext ctx) { }
	@Override public void exitValue(JSONParser.ValueContext ctx) { }
	
	...

	@Override public void visitTerminal(TerminalNode node) { }
	@Override public void visitErrorNode(ErrorNode node) { }
}
复制代码
  1. 语法规则动做、语义判断、匹配优先级

在语法规则中,一样支持相似词法规则动做和语义判断,匹配优先级与词法规则也相同。

  1. 结合性

在作加减乘除四则运算时,都是从左向右结合,但在作指数运算时,确是从右向左结合,此时须要用assoc来手动指定结合性。这样输入2^3^4就会被识别成2^(3^4),语法规则以下:

expr : <assoc=right> expr '^' xpr
     | INT
     ;
复制代码

4.2 错误报告

默认状况下,ANTLR将全部的错误消息送至标准错误输出,同时,ANTLR也提供了ANTLRErrorListener来改变这些消息的目标输出以及样式。该接口有一个同时用于词法分析器和语法分析器的syntaxError()方法。ANTLRErrorListener接口方法较多,ANTLR提供了BaseErrorListener类做为其基类实现,在使用时,只要重写该接口并修改ANTLR的错误监听器便可。

来看下ANTLR的源码:

public class ConsoleErrorListener extends BaseErrorListener {
  public static final ConsoleErrorListener INSTANCE = new ConsoleErrorListener();

  @Override
  public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line, 
    int charPositionInLine, String msg, RecognitionException e) {
    // 向控制台输出错误信息
    System.err.println("line " + line + ":" + charPositionInLine + " " + msg);
  }
}


public abstract class Recognizer<Symbol, ATNInterpreter extends ATNSimulator> {
  ...
  // 默认使用控制台错误监听器
  private List<ANTLRErrorListener> _listeners = new CopyOnWriteArrayList<ANTLRErrorListener>() {{
    add(ConsoleErrorListener.INSTANCE);
  }};
  ...
}
复制代码

在使用时,为了更好地展现错误消息,能够重写报错方法,以下面SyntaxErrorListener。此外,在发生词法或者语法错误时,ANTLR具备必定修复手段,保证解析能够继续执行,但在某些状况下,好比SQL语句,或者Shell脚本,语法发生错误时,后续都不该该再执行,所以,在监听到语法或者词法错误时,能够经过抛出异常来终止解析过程。

public class SyntaxErrorListener extends BaseErrorListener {

  @Override
  public void syntaxError(Recognizer<?, ?> recognizer, Object offendingSymbol, int line,
      int charPositionInLine, String msg, RecognitionException e) {
    List<String> stack = ((Parser) recognizer).getRuleInvocationStack();
    Collections.reverse(stack);
    SyntaxException exception = new SyntaxException("line " + line + ":" + charPositionInLine + " at " + offendingSymbol + ": " + msg, e);
    exception.setLine(line);
    exception.setCol(charPositionInLine);
    exception.setSymbol(String.valueOf(offendingSymbol));
    
    throw exception;
  }

}
复制代码

4.3 语法树遍历

ANTLR提供两种遍历树机制,即监听器和访问器。

4.3.1 监听器

监听器相似于XML解析器生成的SAX文档对象,SAX监听器接收相似于startDocument()和endDocument()的事件通知。一个监听器的方法实际上就是回调函数,ANTLR会深度优先遍历语法树,在进入或者离开节点时会触发回调函数。以一条简单的赋值语法为例:

grammar Stat;

stat : assign;
assign : 'sp' '=' expr ';';
expr : Expr;

Expr : [1-9][0-9]*;
WS : [ \t\n\r] + -> skip;
复制代码

ANTLR生成的监听器UML图以下:

监听器

StatListener接口提供了全部语法规则进入(enter)、离开(exit)时回调的抽象方法,StatBaseListener类则对全部接口作了默认实现,使用时只须要继承StatBaseListener类,重写关注的方法接口便可。

sp = 100;为例,ANTLR对其遍历顺序以下:

监听器深度优先遍历

API调用顺序

4.3.2 访问器

访问器一样采用深度优先遍历方式遍历语法树,与监听器不一样的是访问器采用显示调用方式访问节点,所以遍历过程能够控制。

访问器UML图以下:

访问器

StatVisitor接口提供了全部语法规则的抽象方法,若是想访问特定的语法规则,只需调用对应的接口便可。固然,ANTLR一样提供了默认实现类StatBaseVisitor,使用时只要继承该类便可。此外,从方法定义上也能够看出,每一个方法均有返回值,只是返回值类型固定,约束较大。

对于sp = 100;,访问器遍历顺序以下:

访问器遍历流程

4.3.3 数据传递机制

在语法树遍历过程当中,咱们每每须要传递数据,在事件方法中,目前有三种共享信息的方法。

  1. 使用方法返回值

访问器监听器实现原理上能够看出,监听器采用的是回调方式,所以返回值都为void,没法传递参数。访问器带有固定类型的返回值,能够用来共享数据,但因类型固定,所以使用上较为受限。

  1. 类成员在事件方法中共享数据

不管是访问器仍是监听器,都采用深度优先遍历方式访问语法树,每每会使用栈来存储中间数据。下面咱们以JSON语法树的遍历为例,介绍下类成员在事件方法中的使用,同时介绍下访问器监听器的具体使用。

有时为了存储便利,配置信息每每以JSON格式存储,在使用时须要转换成properties文件。下面使用上文提到的JSON.g4语法,将下面的JSON数据转换成标准的properties文件,考虑通用性,会去掉语法文件中语法规则的备选分支标签。

{
    "spring":{
        "datasource":{
            "driver-class-name":"com.mysql.cj.jdbc.Driver",
            "jdbc-url":"jdbc:mysql://127.0.0.1:3306/db",
            "username":"root",
            "password":"password",
            "type":"com.zaxxer.hikari.HikariDataSource",
            "hikari":{
                "pool-name":"HikariCP",
                "minimum-idle":5,
                "maximum-pool-size":50,
                "idle-timeout":600000,
                "max-lifetime":1800000
            }
        },
        "redis":{
            "database":0,
            "host":"127.0.0.1",
            "port":6379,
            "password":"123456"
        }
    }
}
复制代码

转换后的properties文件内容以下:

spring.redis.database = 0
spring.redis.password = 123456
spring.redis.host = 127.0.0.1
spring.redis.port = 6379
spring.datasource.hikari.pool-name = HikariCP
spring.datasource.password = password
spring.datasource.driver-class-name = com.mysql.cj.jdbc.Driver
spring.datasource.hikari.idle-timeout = 600000
spring.datasource.username = root
spring.datasource.hikari.maximum-pool-size = 50
spring.datasource.hikari.max-lifetime = 1800000
spring.datasource.jdbc-url = jdbc:mysql://127.0.0.1:3306/db
spring.datasource.type = com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle = 5
复制代码

监听器实现以下:

public class MyListener extends JSONBaseListener {

  /**
   * 键片断
   */
  private Stack<String> keys = new Stack<>();

  /**
   * 属性
   */
  @Getter
  private Map<String, String> prop = new HashMap<>();

  @Override
  public void enterValue(ValueContext ctx) {
    if (ctx.arr() != null || ctx.obj() != null) {
      return;
    }
    if (ctx.STRING() != null) {
      addProp(ctx.getText().substring(1, ctx.getText().length() - 1));
    } else {
      addProp(ctx.getText());
    }
  }

  @Override
  public void enterPair(PairContext ctx) {
    String text = ctx.STRING().getText();
    keys.push(text.substring(1, text.length() - 1));
  }

  @Override
  public void exitPair(PairContext ctx) {
    keys.pop();
  }

  private String getKey() {
    StringBuilder sb = new StringBuilder();
    for (String key : keys) {
      if (StringUtils.isNotBlank(key)) {
        sb.append(key.trim()).append(".");
      }
    }
    if (sb.length() == 0) {
      return "";
    }
    sb.deleteCharAt(sb.length() - 1);
    return sb.toString();
  }

  private void addProp(String value) {
    String key = getKey();
    // 存在同名配置时作简单处理,使用最后一次读取内容覆盖
    prop.put(key, value);
  }
}

public static void transformByListener(String json) {
  CharStream input = CharStreams.fromString(json);
  // 词法解析器将字符流转换为词法符号流
  JSONLexer lexer = new JSONLexer(input);
  CommonTokenStream tokens = new CommonTokenStream(lexer);
  // 语法解析器将词法符号流转换成语法树
  JSONParser parser = new JSONParser(tokens);
  ParserRuleContext context = parser.json();
  // 监听器实现语法树遍历
  ParseTreeWalker walker = new ParseTreeWalker();
  MyListener listener = new MyListener();
  walker.walk(listener, context);
  Map<String, String> prop = listener.getProp();
  for (String key : prop.keySet()) {
    System.out.println(key + " = " + prop.get(key) );
  }
}
复制代码

访问器实现以下:

public class MyVisitor extends JSONBaseVisitor<Void> {

  /**
   * 键片断
   */
  private Stack<String> keys = new Stack<>();

  /**
   * 属性
   */
  @Getter
  private Map<String, String> prop = new HashMap<>();

  @Override
  public Void visitValue(ValueContext ctx) {
    if (ctx.obj() != null) {
      visitObj(ctx.obj());
    } else if (ctx.arr() != null) {
      visitArr(ctx.arr());
    } else if (ctx.STRING() != null) {
      addProp(ctx.getText().substring(1, ctx.getText().length() - 1));
    } else {
      addProp(ctx.getText());
    }
    return null;
  }

  @Override
  public Void visitObj(ObjContext ctx) {
    List<PairContext> pairs = ctx.pair();
    if (pairs != null && pairs.size() > 0) {
      pairs.forEach(this::visitPair);
    }
    return null;
  }

  @Override
  public Void visitPair(PairContext ctx) {
    String text = ctx.STRING().getText();
    keys.push(text.substring(1, text.length() - 1));
    visitValue(ctx.value());
    keys.pop();
    return null;
  }

  private String getKey() {
    StringBuilder sb = new StringBuilder();
    for (String key : keys) {
      if (StringUtils.isNotBlank(key)) {
        sb.append(key.trim()).append(".");
      }
    }
    if (sb.length() == 0) {
      return "";
    }
    sb.deleteCharAt(sb.length() - 1);
    return sb.toString();
  }

  private void addProp(String value) {
    String key = getKey();
    // 存在同名配置时作简单处理,使用最后一次读取内容覆盖
    prop.put(key, value);
  }

}


public static void transformByVisitor(String json) {
  CharStream input = CharStreams.fromString(json);
  // 词法解析器将字符流转换为词法符号流
  JSONLexer lexer = new JSONLexer(input);
  CommonTokenStream tokens = new CommonTokenStream(lexer);
  // 语法解析器将词法符号流转换成语法树
  JSONParser parser = new JSONParser(tokens);
  ParserRuleContext context = parser.json();
  // 访问器实现语法树遍历
  MyVisitor visitor = new MyVisitor();
  visitor.visit(context);
  Map<String, String> prop = visitor.getProp();
  for (String key : prop.keySet()) {
    System.out.println(key + " = " + prop.get(key));
  }
}
复制代码
  1. 对语法分析树的节点进行标注来存储相关数据

从ANTLR生成的代码能够看出,对于每条语法规则,都会生成一个上下文类,所以能够经过该类对象共享数据。例如:

e returns [int value]
  : e '*' e
  | e '+' e
  | INT
  ;
  
public static class EContext extends ParserRuleContext {
  public int value;
  ...
}
复制代码

这种方式会将语法与特定的编程语言绑定而丧失灵活性。从思路上讲,无非就是实现了节点与值的关联,对此,ANTLR针对JAVA还提供了ParseTreeProperty辅助类,用于维护节点与值的关系,如何使用将会在后面SQL语法树遍历上具体介绍。

5、SQL解析实现

回到本文一开始提到的SQL解析,不管是哪一种方言,只要找到语法文件,根据须要对语法文件进行定制化改造,并实现语法树遍历逻辑,便可实现输入输出表解析,血缘解析等功能。

下面以Hive SQL 2.x版本为例,简单介绍一下。Hive 2.x版本的语法文件在Hive源码中采用了ANTLR 3.x版本实现,语法文件和代码文件耦合性较强,须要使用 4.x版本规则进行改造,改造好的语法文件见Hive SQL 2.x语法文件

该语法文件也存在下面问题:

  1. 不支持保留字,这在低版本语法中是支持的,如Hive 2.1.1版本。
  2. 不支持set参数,add jar命令,这虽然在原生Hive 中也是不支持的,可是在作SQL解析时,传入的每每是多条SQL,可能带有上述命令,支持后可避免过滤,也可实现更加丰富的功能,好比解析SQL时,能够告知输入输出表在文件中的行列号等。

5.1 语法文件改造

  1. 支持保留字
  • IdentifiersParser.g4文件在最下方增长保留字词法规则
// The following SQL2011 reserved keywords are used as identifiers in many q tests, they may be added back due to backward compatibility.
// We are planning to remove the following whole list after several releases.
// Thus, please do not change the following list unless you know what to do.
sql11ReservedKeywordsUsedAsIdentifier
    :
    KW_ALL | KW_ALTER | KW_ARRAY | KW_AS | KW_AUTHORIZATION | KW_BETWEEN | KW_BIGINT | KW_BINARY | KW_BOOLEAN
    | KW_BOTH | KW_BY | KW_CREATE | KW_CUBE | KW_CURRENT_DATE | KW_CURRENT_TIMESTAMP | KW_CURSOR | KW_DATE | KW_DECIMAL | KW_DELETE | KW_DESCRIBE
    | KW_DOUBLE | KW_DROP | KW_EXISTS | KW_EXTERNAL | KW_FALSE | KW_FETCH | KW_FLOAT | KW_FOR | KW_FULL | KW_GRANT
    | KW_GROUP | KW_GROUPING | KW_IMPORT | KW_IN | KW_INNER | KW_INSERT | KW_INT | KW_INTERSECT | KW_INTO | KW_IS | KW_LATERAL
    | KW_LEFT | KW_LIKE | KW_LOCAL | KW_NONE | KW_NULL | KW_OF | KW_ORDER | KW_OUT | KW_OUTER | KW_PARTITION
    | KW_PERCENT | KW_PROCEDURE | KW_RANGE | KW_READS | KW_REVOKE | KW_RIGHT
    | KW_ROLLUP | KW_ROW | KW_ROWS | KW_SET | KW_SMALLINT | KW_TABLE | KW_TIMESTAMP | KW_TO | KW_TRIGGER | KW_TRUE
    | KW_TRUNCATE | KW_UNION | KW_UPDATE | KW_USER | KW_USING | KW_VALUES | KW_WITH
// The following two keywords come from MySQL. Although they are not keywords in SQL2011, they are reserved keywords in MySQL.
    | KW_REGEXP | KW_RLIKE
    | KW_PRIMARY
    | KW_FOREIGN
    | KW_CONSTRAINT
    | KW_REFERENCES
    ;

复制代码
  • IdentifiersParser.g4文件标识符支持SQL保留字
identifier
    : Identifier
    | nonReserved
    // 新增,Hive 2.1.1版本支持保留字做为标识符,当前的2.3.8后续版本已不支持,所以须要加上
    | sql11ReservedKeywordsUsedAsIdentifier
    ;
复制代码
  1. 针对set参数、add jar改造

set参数值样式很是丰富,已有的词法规则并不知足,若是以语法规则形式支持,须要对词法规则作大量改造,咱们采用投机取巧方式,使用通道实现set及add jar语法的过滤。在HiveLexer.g4增长下面规则:

// 增长动做,指定header
@lexer::header {
import java.util.Iterator;
import java.util.LinkedList;
}


// 增长动做,用于检测set,add行为
@lexer::members {

  public static int CHANNEL_SET_PARAM = 2;

  public static int CHANNEL_USE_JAR = 3;

  private LinkedList<Token> selfTokens = new LinkedList<>();

  @Override
  public void emit(Token token) {
    this._token = token;
    if (token != null) {
      selfTokens.add(token);
    }
  }

  @Override
  public void reset() {
    super.reset();
    this.selfTokens.clear();
  }

  public boolean isStartCmd() {
    Iterator<Token> it = this.selfTokens.descendingIterator();
    while (it.hasNext()) {
      Token previous = it.next();
      if (previous.getType() == HiveLexer.WS || previous.getType() == HiveLexer.LINE_COMMENT
          || previous.getType() == HiveLexer.SHOW_HINT || previous.getType() == HiveLexer.HIDDEN_HINT
          || previous.getType() == HiveLexer.QUERY_HINT) {
        continue;
      }
      return previous.getType() == HiveLexer.SEMICOLON;
    }
    return true;
  }

}

// 增长词法规则,检测SET参数操做
SET_PARAM
    : {isStartCmd()}? KW_SET (~('='|';'))+ '=' (~(';'))+ -> channel(2)
    ;

// 增长词法规则,检测add jar操做
ADD_JAR
    : {isStartCmd()}? KW_ADD (~(';'))+ -> channel(3)
    ;
复制代码

5.2 语法树遍历实现

public class HiveTableVisitor extends HiveParserBaseVisitor<Void> {

  @Setter
  private String curDb;

  /**
   * 当前SQL解析出的实体
   */
  private ParseTreeProperty<List<Entity>> curProp = new ParseTreeProperty<>();

  // 其余部分省略
  ...

  @Override
  public Void visitStatement(StatementContext ctx) {
    if (ctx.execStatement() == null) {
      return null;
    }
    visitExecStatement(ctx.execStatement());
    addProp(ctx, curProp.get(ctx.execStatement()));
    return null;
  }

  // 切换数据库
  @Override
  public Void visitSwitchDatabaseStatement(SwitchDatabaseStatementContext ctx) {
    String db = ctx.identifier().getText();
    this.curDb = trimQuota(db);
    return null;
  }

  // 删除表操做
  @Override
  public Void visitDropTableStatement(DropTableStatementContext ctx) {
    TableNameContext fullCtx = ctx.tableName();
    Opt opt = new Opt(OptType.DROP, ctx.getStart().getLine(), ctx.getStart().getCharPositionInLine());
    addProp(ctx, buildTbl(fullCtx, opt));
    return null;
  }

  // 查询操做
  @Override
  public Void visitAtomSelectStatement(AtomSelectStatementContext ctx) {
    if (ctx.fromClause() != null) {
      visitFromClause(ctx.fromClause());
      Opt opt = new Opt(OptType.SELECT, ctx.getStart().getLine(), ctx.getStart().getCharPositionInLine());
      fillOpt(opt, curProp.get(ctx.fromClause()));
      addProp(ctx, curProp.get(ctx.fromClause()));
    } else if (ctx.selectStatement() != null) {
      visitSelectStatement(ctx.selectStatement());
      addProp(ctx, curProp.get(ctx.selectStatement()));
    }
    return null;
  }

  @Override
  public Void visitTableSource(TableSourceContext ctx) {
    TableNameContext fullCtx = ctx.tableName();
    addProp(ctx, buildTbl(fullCtx));
    return null;
  }

  private Entity buildTbl(TableNameContext fullCtx, Opt opt) {
    Entity entity = buildTbl(fullCtx);
    entity.setOpt(opt);
    return entity;
  }

  private Entity buildTbl(TableNameContext fullCtx) {
    Tbl tbl;
    if (fullCtx.DOT() != null) {
      IdentifierContext dbCtx = fullCtx.identifier().get(0);
      IdentifierContext tblCtx = fullCtx.identifier().get(1);
      tbl = new Tbl(
          Db.buildDb(
              curDb,
              trimQuota(dbCtx.getText()),
              dbCtx.getStart().getLine(),
              dbCtx.getStart().getCharPositionInLine()
          ),
          trimQuota(tblCtx.getText()),
          tblCtx.getStart().getLine(),
          tblCtx.getStart().getCharPositionInLine()
      );
    } else {
      IdentifierContext tblCtx = fullCtx.identifier().get(0);
      Integer line = tblCtx.getStart().getLine();
      Integer col = tblCtx.getStart().getCharPositionInLine();
      tbl = new Tbl(
          Db.buildDb(curDb, null, line, col),
          trimQuota(tblCtx.getText()),
          line,
          col
      );
    }
    return new Entity(Type.TBL).setTbl(tbl);
  }

  private void fillOpt(Opt opt, Entity entity) {
    if (entity == null || entity.getOpt() != null) {
      return;
    }
    entity.setOpt(opt);
  }

  private void fillOpt(Opt opt, List<Entity> entities) {
    if (entities == null || entities.size() == 0) {
      return;
    }
    for (Entity entity : entities) {
      if (entity.getOpt() != null) {
        continue;
      }
      entity.setOpt(opt);
    }
  }

  private String trimQuota(String name) {
    if (name == null || name.length() <= 2) {
      return name;
    }
    char start = name.charAt(0);
    char end = name.charAt(name.length() - 1);
    if (start == '`' && end == '`') {
      name = name.substring(1, name.length() - 1).replaceAll("``", "`");
    }
    return name;
  }
  // 其余部分省略
  ...
}
复制代码

6、参考文献

相关文章
相关标签/搜索