python - 使用 sqlparse 解析 CASE WHEN 语句
问题描述
我有以下 SQL 查询,并希望使用sqlparse
import sqlparse
query = """
select SUM(case when(A.dt_unix<=86400
and B.flag="V") then 1
end) as TEST_COLUMN_1,
SUM(case when(A.Amt - B.Amt > 0
and B.Cat1 = "A"
and (B.Cat2 = "M"
or B.Cat3 = "C"
or B.Cat4 = "B")
and B.Cat5 is NULL) then 1
end) as TEST_COLUMN_2
from test_table A
left join test_table_2 as B on A.ID=B.ID
where A.DT >B.DT
group by A.ID
"""
query_tokens = sqlparse.parse(query)[0].tokens
print(query_tokens)
将给出 SQL 语句中包含的所有标记:
[<Newline ' ' at 0x7FAA62BD9F48>, <DML 'select' at 0x7FAA62BE7288>, <Whitespace ' ' at 0x7FAA62BE72E8>, <IdentifierList 'SUM(ca...' at 0x7FAA62BF7CF0>, <Newline ' ' at 0x7FAA62BF6288>, <Keyword 'from' at 0x7FAA62BF62E8>, <Whitespace ' ' at 0x7FAA62BF6348>, <Identifier 'test_t...' at 0x7FAA62BF7570>, <Newline ' ' at 0x7FAA62BF64C8>, <Keyword 'left j...' at 0x7FAA62BF6528>, <Whitespace ' ' at 0x7FAA62BF6588>, <Identifier 'test_t...' at 0x7FAA62BF7660>, <Whitespace ' ' at 0x7FAA62BF67C8>, <Keyword 'on' at 0x7FAA62BF6828>, <Whitespace ' ' at 0x7FAA62BF6888>, <Comparison 'A.ID=B...' at 0x7FAA62BF7B10>, <Newline ' ' at 0x7FAA62BF6B88>, <Where 'where ...' at 0x7FAA62BF28B8>, <Keyword 'group' at 0x7FAA62BD9E88>, <Whitespace ' ' at 0x7FAA62BD93A8>, <Keyword 'by' at 0x7FAA62BD9EE8>, <Whitespace ' ' at 0x7FAA62C1CEE8>, <Identifier 'A.ID' at 0x7FAA62BF2F48>, <Newline ' ' at 0x7FAA62BF6C48>]
如何解析这些标记,以便以CASE WHEN
一种可以提取所有条件并保持它们的优先级的方式处理语句,如使用括号定义的那样。我无法在文档中找到任何相关示例。
对此有什么想法吗?
解决方案
该项目确实有点记录不足。我查看了示例并稍微扫描了源代码。不幸的是,该文档没有包含对这个任务有用的Token
和TokenList
类的所有方法。
例如,一个重要但被忽略的方法是TokenList.get_sublists()
method,它可以让您比其他方法更容易地遍历嵌套的标记列表;该TokenList.flatten()
方法仅在树中产生未分组CASE
的标记,而是分组标记,因此纯粹通过文档您可能会发现很难对已解析的标记树做一些有用的事情。
我在代码库中注意到的另一个方便的方法是TokenList._pprint_tree()
方法,它将当前令牌树转储到标准输出。这在尝试编写分析树的代码时非常有用。
总而言之,我的总体印象sqlparse
是,它与其说是一个解析库,不如说是一个重新格式化 SQL 的工具。它包括一个很好的解析器,但不包括一般使用它生成的树所必需的工具。
库中真正缺少的是基础节点访问者类,例如由Pythonast
模块提供的类,或树节点遍历器,再次像ast
模块提供的那样。幸运的是,要么很容易建立自己:
from collections import deque
from sqlparse.sql import TokenList
class SQLTokenVisitor:
def visit(self, token):
"""Visit a token."""
method = 'visit_' + type(token).__name__
visitor = getattr(self, method, self.generic_visit)
return visitor(token)
def generic_visit(self, token):
"""Called if no explicit visitor function exists for a node."""
if not isinstance(token, TokenList):
return
for tok in token:
self.visit(tok)
def walk_tokens(token):
queue = deque([token])
while queue:
token = queue.popleft()
if isinstance(token, TokenList):
queue.extend(token)
yield token
现在您可以使用其中任何一个来访问Case
节点:
statement, = sqlparse.parse(query)
class CaseVisitor(SQLTokenVisitor):
"""Build a list of SQL Case nodes
The .cases list is a list of (condition, value) tuples per CASE statement
"""
def __init__(self):
self.cases = []
def visit_Case(self, token):
branches = []
for when, then_ in token.get_cases():
branches
self.cases.append(token.get_cases())
visitor = CaseVisitor()
visitor.visit(statement)
cases = visitor.cases
或者
statement, = sqlparse.parse(query)
cases = []
for token in walk_tokens(statement):
if isinstance(token, sqlparse.sql.Case):
cases.append(token.get_cases())
walk_tokens()
在此示例中,和模式之间的差异NodeVisitor
可以忽略不计,但我们只是为每个CASE
语句提取分离的标记,而不处理WHEN ... THEN ...
标记。在该NodeVisitor
模式中,您将在当前访问者实例上设置更多属性以“切换齿轮”并以更多方法捕获有关这些子树标记的更多信息,这可能比生成器上的嵌套循环visit_....
更容易遵循。for
另一方面,对于walk_tokens()
生成器,如果您创建一个单独的变量来引用生成器,您可以将迭代交给辅助函数:
all_tokens = walk_tokens(stamement)
for token in walk_tokens(statement):
if isinstance(token, sqlparse.sql.Case):
branches = extract_branches(all_tokens)
whereextract_branches
将进一步迭代,直到 case 语句结束。
推荐阅读
- java - 如何用读取 txt 文件填充 ArrayList
- verilog - Verilog错误“连续赋值输出必须是网络”
- javascript - 引导标签输入 maxTag 在函数内不起作用
- javascript - 如何让我的复选框以 true 开头?
- python-3.x - 为什么我们使用 key=get 进行排序,key 方法是如何工作的
- java - 如何将在 EditText 控件中输入的值与 android studio 中微调器容器中的项目绑定在一起?
- version-control - 是否有类似 GitGraph 的 perforce 功能?
- javascript - 如何使用 Javascript 旋转这张卡片?
- visual-studio-code - 在 VSCode 中打开终端时的默认目录
- php - phpmyadmin: mysqli_real_connect(): (HY000/2002): 没有这样的文件或目录 - Ubuntu 20.04, phpmyadmin 4:4.9.5+dfsg1-2