使用antlr快速解析SQL
Posted 水中加点糖
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用antlr快速解析SQL相关的知识,希望对你有一定的参考价值。
出于好奇和研究需要,需要将传入的sql进行解析,获取出一些关键字段。
通过查阅一些资料后,知道了有以下几种方式可以进行SQL的解析。
- druid sql解析
- JavaCC,在Calcite中有使用到
- antlr
通过简单对比和了解后,打算用antlr来进行SQL的解析
antlr
其官网地址为:https://www.antlr.org/
介绍如下
ANTLR (ANother Tool for Language Recognition) is a powerful parser generator for reading, processing, executing, or translating structured text or binary files. It’s widely used to build languages, tools, and frameworks. From a grammar, ANTLR generates a parser that can build and walk parse trees.
总之就是:一个强大的语法解析工具。
其次antlr在很多开源的项目中都有用到它,比如sharding-jdbc、hive、presto等。
解析SQL
知道了它可以解析SQL之后,接下来就来定一个小目标,解析一段SQL快速体验一下。
解析之前,先定一个打算要解析的目标。
目标:
获取出一条SQL中的所有表名
生成解析的代码
antlr在解析前,需要定义一个语法规则文件,其中包含了对应的词法信息和对应的语法规则。
由于每个数据库的sql可能存在某些语法上的差异,所对应的语法规则可能不一样。这里以mysql的语法规则为例,那么则需要提供一个mysql数据库对应的语法规则文件,此文件可以自己编写,不过更实际一点还是直接使用antlr官方的文件即可。
antlr4官方提供的常用语法文件地址为: https://github.com/antlr/grammars-v4
在其中找到mysql数据库对应的语法文件,https://github.com/antlr/grammars-v4/tree/master/sql/mysql/Positive-Technologies
其中MySqlLexer.g4打开一看,可以看出里面主要是mysql数据库中的关键字信息,MySqlParser.g4为语法文件。
有了语法规则文件后,就可以直接生成代码了。
其生成代码的方法主要有两种:
- 下载antlr的jar包,用命令方式生成代码,其步鄹可以直接看官方的首页中的快速开始可以看到;https://github.com/antlr/antlr4/blob/master/doc/getting-started.md
OS X
$ cd /usr/local/lib
$ sudo curl -O https://www.antlr.org/download/antlr-4.9.2-complete.jar
$ export CLASSPATH=".:/usr/local/lib/antlr-4.9.2-complete.jar:$CLASSPATH"
$ alias antlr4='java -jar /usr/local/lib/antlr-4.9.2-complete.jar'
$ alias grun='java org.antlr.v4.gui.TestRig'
- 使用插件进行生成,操作步鄹在官网中也有说明。详见:https://www.antlr.org/tools.html
按照操作步骤,插件安装到idea中,再配置好生成的配置
之后点击生成,antlr插件则会快速生成对应语法规则文件对应的解析代码了。
就像我这里的一样,随便找了一个测试项目,生成了对应的解析代码
解析sql
通过上面的步骤,antlr生成出了很多代码,其中有两个类就是可以用来遍历用的。
一个是Listener类,一个是Visitor类。其遍历的规则是设计模式中的监听器设计模式和访问者设计模式。
遍历前记得要在对应的项目中导入antlr所需的jar包
<dependency>
<groupId>org.antlr</groupId>
<artifactId>antlr4</artifactId>
<version>4.9.3</version>
</dependency>
之后便可以编写对应的解析代码。
为了让分析SQL更加方便,可以先输出SQL中的解析树,
import com.github.puhaiyang.k8sops.antlr.MySqlLexer;
import com.github.puhaiyang.k8sops.antlr.MySqlParser;
import com.github.puhaiyang.k8sops.antlr.MysqlQueryFieldListener;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.ParserRuleContext;
import org.antlr.v4.runtime.tree.ParseTree;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import org.antlr.v4.runtime.tree.Trees;
import java.util.Arrays;
import java.util.List;
public class MysqlParseTest
public static void main(String[] args) throws Exception
String sql = "SELECT t1.column1,t1.column2,t1.column3,t2.xy from tableC t1 left join tableA t2 on t1.id=t2.oid where t1.column1 = 1 and t2.yy=6";
//词法分析器
MySqlLexer mySqlLexer = new MySqlLexer(CharStreams.fromString(sql.toUpperCase()));
//词法符号的缓冲区,用于存储词法分析器生成的词法符号
CommonTokenStream commonTokenStream = new CommonTokenStream(mySqlLexer);
//新建一个语法分析器,处理词法符号缓冲区内容
MySqlParser mySqlParser = new MySqlParser(commonTokenStream);
//获取出selectStatement
ParseTree tree = mySqlParser.selectStatement();
System.out.println(printSyntaxTree(mySqlParser,tree));
public static String printSyntaxTree(Parser parser, ParseTree root)
StringBuilder buf = new StringBuilder();
recursive(root, buf, 0, Arrays.asList(parser.getRuleNames()));
return buf.toString();
private static void recursive(ParseTree aRoot, StringBuilder buf, int offset, List<String> ruleNames)
for (int i = 0; i < offset; i++)
buf.append(" ");
buf.append(Trees.getNodeText(aRoot, ruleNames)).append("\\n");
if (aRoot instanceof ParserRuleContext)
ParserRuleContext prc = (ParserRuleContext) aRoot;
if (prc.children != null)
for (ParseTree child : prc.children)
recursive(child, buf, offset + 1, ruleNames);
通过调用上面的printSyntaxTree方法,可以让SQL的语法树更加结构化的方式输出,写代码时也更便于分析。
上面的代码运行后,其输出结果为:
sqlStatements
sqlStatement
dmlStatement
selectStatement
querySpecification
SELECT
selectElements
selectElement
fullColumnName
uid
simpleId
T1
dottedId
.COLUMN1
,
selectElement
fullColumnName
uid
simpleId
T1
dottedId
.COLUMN2
,
selectElement
fullColumnName
uid
simpleId
T1
dottedId
.COLUMN3
,
selectElement
fullColumnName
uid
simpleId
T2
dottedId
.XY
fromClause
FROM
tableSources
tableSource
tableSourceItem
tableName
fullId
uid
simpleId
TABLEC
uid
simpleId
T1
joinPart
LEFT
JOIN
tableSourceItem
tableName
fullId
uid
simpleId
TABLEA
uid
simpleId
T2
ON
expression
predicate
predicate
expressionAtom
fullColumnName
uid
simpleId
T1
dottedId
.ID
comparisonOperator
=
predicate
expressionAtom
fullColumnName
uid
simpleId
T2
dottedId
.OID
WHERE
expression
expression
predicate
predicate
expressionAtom
fullColumnName
uid
simpleId
T1
dottedId
.COLUMN1
comparisonOperator
=
predicate
expressionAtom
constant
decimalLiteral
1
logicalOperator
AND
expression
predicate
predicate
expressionAtom
fullColumnName
uid
simpleId
T2
dottedId
.YY
comparisonOperator
=
predicate
expressionAtom
constant
decimalLiteral
6
FROM,T2.XY,T1.COLUMN3,T1.COLUMN2,T1.COLUMN1
之后选择一种合适的遍历方式编写解析代码即可,如这里需要获取SQL语句中的表名,则可以新建一个Listener,让其继承生成的ParserBaseListener即可。
然后再重写ParserBaseListener中需要进行记录的方法。
比如要获取一个SQL语句中的表名,可以这样写一下:
public class GetTableNamesListener extends MySqlParserBaseListener
private final Set<String> tableNameSet = new HashSet<String>();
@Override
public void enterTableSources(MySqlParser.TableSourcesContext ctx)
List<MySqlParser.TableSourceContext> tableSourceContexts = ctx.getRuleContexts(MySqlParser.TableSourceContext.class);
for (MySqlParser.TableSourceContext tableSource : tableSourceContexts)
//通过tableSourceItems获取表名
getTableNameByTableSourceItems(tableSource.getRuleContexts(MySqlParser.TableSourceItemContext.class));
//获取join部分
List<MySqlParser.OuterJoinContext> joinContexts = tableSource.getRuleContexts(MySqlParser.OuterJoinContext.class);
for (MySqlParser.OuterJoinContext joinContext : joinContexts)
List<MySqlParser.TableSourceItemContext> tableSourceItemContexts = joinContext.getRuleContexts(MySqlParser.TableSourceItemContext.class);
getTableNameByTableSourceItems(tableSourceItemContexts);
private void getTableNameByTableSourceItems(List<MySqlParser.TableSourceItemContext> tableSourceItems)
for (MySqlParser.TableSourceItemContext tableSourceItem : tableSourceItems)
List<MySqlParser.TableNameContext> tableNameContexts = tableSourceItem.getRuleContexts(MySqlParser.TableNameContext.class);
for (MySqlParser.TableNameContext tableNameContext : tableNameContexts)
tableNameSet.add(tableNameContext.getText());
public Set<String> getTableNameSet()
return tableNameSet;
然后调用:
public static void main(String[] args)
String sql = "SELECT t1.column1,t1.column2,t1.column3,t2.xy from tableC t1 left join tableA t2 on t1.id=t2.oid where t1.column1 = 1 and t2.yy=6";
System.out.println(sql);
MySqlLexer lexer = new MySqlLexer(CharStreams.fromString(sql.toUpperCase()));
MySqlParser parser = new MySqlParser(new CommonTokenStream(lexer));
//定义GetTableNamesListener
GetTableNamesListener listener = new GetTableNamesListener();
ParseTreeWalker.DEFAULT.walk(listener, parser.sqlStatements());
Set<String> tableNameSet = listener.getTableNameSet();
for (String tableName : tableNameSet)
System.out.println(tableName);
其输出结果如下:
SELECT t1.column1,t1.column2,t1.column3,t2.xy from tableC t1 left join tableA t2 on t1.id=t2.oid where t1.column1 = 1 and t2.yy=6
TABLEA
TABLEC
以上是关于使用antlr快速解析SQL的主要内容,如果未能解决你的问题,请参考以下文章
Hive 源码解读 Driver 将 HQL 语句转换为 AST
Hive 源码解读 Driver 将 HQL 语句转换为 AST