一 词法,语法解析简介 SQL作为现代计算机行业的数据处理事实标准,一直倍受数据分析师和软件开发者所青睐,从最早应用SQL的单机DBMS(如MySQL、Oracle),到NoSQL提供SQL兼容层(如HBase的Phoenix),到最近火热的NewSQL(如Spanner、TiDB),还有几乎所有主流的计算框架(如Spark、Flink)都兼容了SQL。
SQL规范也一直在稳定发展,近年来通过语法拓展满足了离线批处理、流式处理以及时序数据处理几乎所有的需求,并且因为用户基数极大,兼容SQL让用户可以没有学习成本地把业务迁移到新的软件系统中。SQL本质上是一种数据处理的描述语言,或者说是一系列符合SQL标准的字符串,任何系统都可以解析SQL来生成对用平台的任务,因此SQL parser就是这些系统中最重要的组件之一,而且通过拓展SQL parser还可以实现自定义的SQL语义。那么怎样才可以实现自己的SQL parser呢,正是本文准备介绍的。
二 实现sql parser原理 SQL是一种描述语言的规范,有SQL86、SQL89、SQL92、SQL1999、SQL2003、SQL2006、SQL2008、SQL2011等标准,而MySQL、Oracle甚至Spark也有自己支持的SQL规范,所谓SQL兼容其实也不是和SQL社区定义的标准100%一样,和Python定义语法规范但还有CPython、Jython、Cython等多种虚拟机实现类似。而SQL描述语言相比其他编程语言更简单一些,一般分为DDL和DML,大部分计算系统只是支持DML中的查询语法,也就是SELECT FROM语句,因为对于计算系统来说不需要管理Schema的修改以及数据的增删改,如果是兼容SQL的存储系统就需要兼容几乎所有DDL和DML语句。
那么要解析用户编写的SQL字符串一般有以下几种方法:
简单字符串处理,使用字符串查找或者正则表达式来提取SQL中的字段,对于简单的SQL可以这样实现,但SQL规范还有复杂的开闭括号以及嵌套查询,复杂SQL几乎不可能通过字符串匹配来实现。
使用已有的开源SQL解释器,不同开源系统解析SQL后的应用是不一样的,一般会转化成自己实现的抽象语法树,这些数据结构包含自己定义的树节点以及表达式节点,要兼容使用这些数据结构是非常困难的,这也是为什么开源项目中支持SQL的很多但基本不可以复用同一套实现的原因。
从头实现SQL解释器,对SQL做词法解析、语法解析的项目已经有很多了,通过遍历抽象语法树来解析SQL实现对应的业务逻辑 ,从头实现一个SQL解释器并没有想象中困难,而且还可以选择增加或减少支持的SQL语法范围。
本文会基于这里会基于封装性更好并且更加易用的Antlr4来介绍,(Presto和Spark都是基于Antlr框架来实现SQL parser的,也就是他们没有重复造SQL词法解析器、SQL语法解析器的轮子)
三 使用Antlr4基于spark环境解析sparkSql Antlr4是一个Java实现的开源项目,用户需要编写g4后缀的语法文件,Antlr4可以自动生成词法解析器和语法解析器,提供给开发者的接口是已经解析好的抽象语法树以及易于访问的Listener和Visitor基类。什么意思呢,就是如果你要实现一个SQL parser,只要提供一个SQL语法规范的g4描述文件,这个文件可以从Presto或Spark项目中获得 ,那么Antlr就会生成编译过程中的抽象语法树,用户也只需要写一个Java类来选择感兴趣的节点接口,g4文件格式需要符合Antlr要求但因为是标准SQL我们不用自己重新写可以复用Presto或Spark的
spark的g4文件地址:
1 https://github.com/apache/spark/blob/v2.3.1/sql/catalyst/src/main/antlr4/org/apache/spark/sql/catalyst/parser/SqlBase.g4
基于现有的语法文件和开源库,用户只要只要传入SQL字符串,就可以马上得到SQL的抽象语法树了 。这里推荐使用IntelliJ IDEA,安装antlr插件后,输入SQL资源就可以可视化这棵抽象语法树,方面后续遍历抽象语法树实现自己的业务逻辑。
有了抽象语法树,以及Antlr自动生成的Visitor基类,用户只需要自己的Visitor类即可,这部分需要参考Antlr的使用文档,实际上也并不复杂。例如我们需要解析SQL获取用户查询的表明,从下面的抽象语法树结构看到需要遍历的节点是fromClause、relationPrimary、tableIdentifier、identifier和strictIdentifier,当然如果用户写的是“SELECT c1, c2 FROM t1 as t2”,那么还可以在tableAlias节点获取重命名的字段。
要获取这个表名并打印出来,实现的Visitor类代码也很直观,只需要重写我们关注的几个节点的visit函数即可 ,如果要支持获取其他字段只需多visit几个节点即可,示例代码如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 import org.antlr.v4.runtime.tree.ParseTree;import org.apache.spark.sql.catalyst.parser.SqlBaseBaseVisitor;import org.apache.spark.sql.catalyst.parser.SqlBaseParser;public class SimpleSqlVisitor extends SqlBaseBaseVisitor { String tableName="" ; public String getTableName () { return tableName; } public void setTableName (String tableName) { this .tableName = tableName; } @Override public Object visitDropTable (SqlBaseParser.DropTableContext ctx) { List<ParseTree> children = ctx.children; String droptableName="" ; for (int i=0 ;i<children.size();i++){ ParseTree parseTree = children.get(i); String text = parseTree.getText(); if (text.equalsIgnoreCase("exists" )){ droptableName=children.get(i+1 ).getText(); } } System.out.println(droptableName); this .tableName=droptableName; return super .visitDropTable(ctx); } @Override public Object visitInsertOverwriteTable (SqlBaseParser.InsertOverwriteTableContext ctx) { List<ParseTree> children = ctx.children; String tableName="" ; for (int i=0 ;i<children.size();i++){ ParseTree parseTree = children.get(i); String text = parseTree.getText(); if (text.equalsIgnoreCase("table" )){ tableName=children.get(i+1 ).getText(); } } this .tableName=tableName; System.out.println(tableName); return super .visitInsertOverwriteTable(ctx); } @Override public Object visitCreateHiveTable (SqlBaseParser.CreateHiveTableContext ctx) { List<ParseTree> children = ctx.children; String tableName="" ; for (int i=0 ;i<children.size();i++){ ParseTree parseTree = children.get(i); String text = parseTree.getText(); if (text.contains("CREATETABLE" )){ tableName=text.split("TABLE" )[1 ]; } } System.out.println(tableName); this .tableName=tableName; return super .visitCreateHiveTable(ctx); } @Override public Object visitInsertIntoTable (SqlBaseParser.InsertIntoTableContext ctx) { List<ParseTree> children = ctx.children; String tableName="" ; for (int i=0 ;i<children.size();i++){ ParseTree parseTree = children.get(i); String text = parseTree.getText(); if (text.equalsIgnoreCase("table" )){ tableName=children.get(i+1 ).getText(); } } this .tableName=tableName; System.out.println(tableName); return super .visitInsertIntoTable(ctx); } @Override public String visitFromClause (SqlBaseParser.FromClauseContext ctx) { String tableName = visitRelation(ctx.relation(0 )); System.out.println("SQL table name: " + tableName); return tableName; } @Override public String visitRelation (SqlBaseParser.RelationContext ctx) { if (ctx.relationPrimary() instanceof TableNameContext) { return visitTableName((TableNameContext)ctx.relationPrimary()); } return "" ; } @Override public String visitTableName (SqlBaseParser.TableNameContext ctx) { return visitTableIdentifier(ctx.tableIdentifier()); } @Override public String visitTableIdentifier (SqlBaseParser.TableIdentifierContext ctx) { return ctx.getChild(0 ).getText(); } }
然后我们需要输入一个SQL字符串进行测试,实现简单的Java main函数,初始化我们定制的visitor实例,然后以此调用Antlr提供的lexer和parser,示例如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class SqlExample { public static void main (String[] argv) { String sqlText = "SELECT C1, C2 FROM T1" ; String sqlF = sqlText.toUpperCase(); SimpleSqlVisitor visitor = new SimpleSqlVisitor(); SqlBaseLexer lexer = new SqlBaseLexer(CharStreams.fromString(sqlText)); CommonTokenStream tokenStream = new CommonTokenStream(lexer); SqlBaseParser parser = new SqlBaseParser(tokenStream); parser.singleStatement().accept(visitor); } }
如果是不需要提取SQL脚本内容,只是想实现一个SQL语法校验功能,那么直接运行visitor检查是否抛lexer或者parser异常即可,或者可以实现一个error listener,直接截取语法异常相关的信息。
1 2 3 4 5 6 7 8 9 10 11 public class SimpleErrorListener extends BaseErrorListener { @Override public void syntaxError (Recognizer<?, ?> recognizer, Object offendingSymbol, int line, int charPositionInLine, String msg, RecognitionException e) { System.out.println("Get SQL syntax error" ); } }
最后需要在parser处注册使用,如果传入语法错误的SQL语句,通过回调找到我们自定义的error listener错误处理逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class SqlExample { public static void main (String[] argv) { String sqlText = "SELECT2 C1, C2 FROM T1" ; SimpleSqlVisitor visitor = new SimpleSqlVisitor(); SqlBaseLexer lexer = new SqlBaseLexer(CharStreams.fromString(sqlText)); CommonTokenStream tokenStream = new CommonTokenStream(lexer); SqlBaseParser parser = new SqlBaseParser(tokenStream); SimpleErrorListener errorListener = new SimpleErrorListener(); parser.addErrorListener(errorListener); parser.singleStatement().accept(visitor); } }
在spark的项目中不需要提供g4描述文件;
ps: 当在spark的项目中,对于sql只想解析sql,看有无词法语法错误,可以这样写:
1 2 val plan =sparkSession.sessionState.sqlParser.parsePlan(sql)logger.info(plan)
四 Antlr4 的使用