前言
文接上篇内容:
写此sqlparse库的目的还是寻找在python编程内可行的SQL血缘解析,JAVA去解析Hive的源码实践的话我还是打算放到后期来做,先把Python能够实现的先实现完。上篇系列讲述的基于antrl解析说是用python其实还是太牵强了,无非就是使用PyJnius调用JAVA的类方法来实现,没有多大的意义来牵扯到Python编程。主要是HiveSQL的底层就是JAVA代码,怎么改写还是绕不开JAVA的。不过上篇系列我有提到过sqlparse,其实这个库用来解析血缘的话也不是不可以,但是能够实现的功能是有限的,目前我实验还行,一些复杂超过千行的数据分析SQL没有测试过。做一些简单的血缘解析的话还是没有应该太大问题,后续我会在此基础之上开发尝试。
一、基类-Statement
此类作为基类存在有绝对的意义。parse函数解析转换的类型也是该类,众多处理方法也是根据此类来编写,那么此类必定承载着SQL分析的基础。
class Statement(TokenList): """Represents a SQL statement.""" def get_type(self): """Returns the type of a statement. The returned value is a string holding an upper-cased reprint of the first DML or DDL keyword. If the first token in this group isn't a DML or DDL keyword "UNKNOWN" is returned. Whitespaces and comments at the beginning of the statement are ignored. """ first_token = self.token_first(skip_cm=True) if first_token is None: # An "empty" statement that either has not tokens at all # or only whitespace tokens. return 'UNKNOWN' elif first_token.ttype in (T.Keyword.DML, T.Keyword.DDL): return first_token.normalized elif first_token.ttype == T.Keyword.CTE: # The WITH keyword should be followed by either an Identifier or # an IdentifierList containing the CTE definitions; the actual # DML keyword (e.g. SELECT, INSERT) will follow next. fidx = self.token_index(first_token) tidx, token = self.token_next(fidx, skip_ws=True) if isinstance(token, (Identifier, IdentifierList)): _, dml_keyword = self.token_next(tidx, skip_ws=True) if dml_keyword is not None \ and dml_keyword.ttype == T.Keyword.DML: return dml_keyword.normalized # Hmm, probably invalid syntax, so return unknown. return 'UNKNOWN'
此类只有一个方法就是返回一个获取此条SQL的DML类型,也就是SQL的功能类型:
query = 'CREATE TABLE AS Select a, col_2 as b from Table_A;select * from foo' for each in sqlparse.parse(query): print(each.get_type())
里面的判断逻辑也是根据 Keyword.DML和Keyword.DDL来判断的。根据第一次获取到的token来判断。有了get_type那么我们要实现的SQL解析的第一步已经有了,首先就可以确定这个SQL的功能与用户的读写查改权限匹配了。先不急我们还需要知道如何解析成一颗树。
二、基类-TokenList
这个类就相当的大了,也正是我们了解解析成AST抽象解析树的关键所在了。源码就不贴上去可太多了,主要找一些能够改写使用到的方法即可。
该类继承Token,而Statement就是继承的此类,也就是Statement最终继承的此两者全部方法。
query = 'CREATE TABLE AS Select a, col_2 as b from Table_A;select * from foo' stmt=sqlparse.parse(query) stmt_1=stmt[0].tokens #for each_token in stmt_1: #print(each_token) sqlparse.sql.TokenList(stmt_1)._get_repr_name() stmt[0]._get_repr_name()
1. _get_repr_name()方法
将输出自身数据结构:
def _get_repr_name(self): return type(self).__name__
2._pprint_tree()方法
这里有关树的解析在这个打印_pprint_tree函数上面:
def _pprint_tree(self, max_depth=None, depth=0, f=None, _pre=''): """Pretty-print the object tree.""" token_count = len(self.tokens) for idx, token in enumerate(self.tokens): cls = token._get_repr_name() value = token._get_repr_value() last = idx == (token_count - 1) pre = '`- ' if last else '|- ' q = '"' if value.startswith("'") and value.endswith("'") else "'" print("{_pre}{pre}{idx} {cls} {q}{value}{q}" .format(**locals()), file=f) if token.is_group and (max_depth is None or depth < max_depth): parent_pre = ' ' if last else '| ' token._pprint_tree(max_depth, depth + 1, f, _pre + parent_pre)
第一次看到这个函数我就认为使用sqlparse解析SQL血缘是可以做成功的:
从打印的函数循迹看是否能够得到血缘关系。这点是可以做到的,先遍历最底层的结构,再依次输出,此时这里我已经有了一个明确的实现思路,但是这里先不开展,我们还是先将此类看明白再下定论。先通读这个方法:
和我之前写的树递归函数差不多,这里要注意到一点就是空格会影响树的输出,所以传入sql之前还是得做去除空格的操作,最好还是化成一句没有空格和缩进的语句。当然也可以通过改写去除Whitespace这一标识符。
通过解析树的输出我们发现到IdentifierList 此类就开始往下层调了,这取决于这段代码:
if token.is_group and (max_depth is None or depth < max_depth): parent_pre = ' ' if last else '| ' token._pprint_tree(max_depth, depth + 1, f, _pre + parent_pre)
也就是说is_group为True就会开始下一层的遍历,而token的初始is_group则为False,也就是解析为TokenList的时候才为True。此Tokenlist就很明显是与IdentifierList 这个类有关了。下个小节我们再细细研究IdentifierList 基类,先让我们再看看TokenList的其他功能函数。
3.get_token_at_offset(self, offset)
该方法将返回一个位置偏移上的token。
offsert_token=stmt[0].get_token_at_offset(1) offsert_token
4.flatten(self)
和token的方法几乎差不多,但是生产的没有分类的tokens。对所有子tokens递归时调用此方法
5._token_matching(self, funcs, start=0, end=None, reverse=False)
该函数就是将token与funcs功能函数进行匹配,内部调用。
6.token_first(self, skip_ws=True, skip_cm=False)
这个是一个比较重要的方法,返回第一个子token。也就是可以返回这条sql的功能类型。
stmt[0].token_first()
其他方法很多都是主类方法的工具函数,主要是现在抓到了重点先搞清楚。Identifier这个类
三、Identifier类
这个类继承了两个父类NameAliasMixin和TokenList,前者为主要为实现get_real_name和get_alias的方法,后者也是我们摸清楚了的TokenList方法。
Identifier类主要代表标识符。标识符可能有别名或类型转换。其中有四个主要方法:
1.get_array_indices
()
返回索引token列表的迭代器:
print(stmt_1[13].get_array_indices())
2.get_ordering
()
将order或None作为大写字符串返回。
print(Identifier.get_ordering(stmt[0]))
我写的sql没有order故为None。
3.get_typecast
()
以字符串形式返回此对象的typecast
或None。
4.is_wildcard
()
如果此标识符包含通配符,则返回True。
以上就是Identifier的四大类,也就是说这个类以后如果我们想要识别什么新的功能函数可以继承此类。
先大体研究这么多,写出点相关的代码再继续深入研究,下篇文章我会将一些初步构思和代码加入其中完成简单的SQL语法解析。