使用 sqlparse 解析 CASE WHEN 语句

Parse CASE WHEN statements with sqlparse

我有以下 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 语句。我无法在文档中找到任何相关示例。

对此有什么想法吗?

该项目的文档确实有点不足。我查看了 examples 并略微扫描了源代码。遗憾的是,该文档并未包括 TokenTokenList class 上对这项任务有用的所有方法。

例如,一个重要但被忽略的方法是 TokenList.get_sublists() method, which lets you traverse over nested token lists more easily than other methods do; the TokenList.flatten() 方法只在树中产生 ungrouped 个标记,而 CASE 是一个分组标记,因此,仅根据文档,您可能会发现很难对已解析的令牌树做一些有用的事情。

我在代码库中注意到的另一个方便的方法是 TokenList._pprint_tree() method,它将当前令牌树转储到标准输出。这在尝试编写分析树的代码时非常有用。

总而言之,我对 sqlparse 的总体印象是它与其说是一个解析库,不如说是一个重新格式化 SQL 的工具。它包括一个很好的解析器,但不包括一般使用它生成的树所必需的工具。

库中真正缺少的是基础 节点访问者 class,例如 Python ast module, or a tree node walker, again like the ast module provides 提供的那个。幸运的是,这两种方法都很容易构建自己:

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 模式中,您将当前访问者实例的更多属性设置为 'switch gears' 并在更多 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)

其中 extract_branches 将进一步迭代,直到到达 case 语句的末尾。