Разбор CASE WHEN операторов с sqlparse - PullRequest
0 голосов
/ 04 марта 2019

У меня есть следующий запрос 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 таким образом, чтобы я мог извлечь все условия и сохранить их приоритет, как определено использованием скобок.Мне не удалось найти какие-либо соответствующие примеры в документации.

Есть мысли по этому поводу?

1 Ответ

0 голосов
/ 04 марта 2019

Проект действительно немного недокументирован.Я посмотрел на примеры и немного отсканировал исходный код.К сожалению, в документацию не включены все методы классов Token и TokenList, которые полезны для этой задачи.

Например, важным, но опущенным методом является метод TokenList.get_sublists() , что позволяет вам легче просматривать вложенные списки токенов, чем другие методы;метод TokenList.flatten() дает только несгруппированные токены в дереве, тогда как CASE является сгруппированным токеном, поэтому, исходя из документации, вам может быть трудно сделать что-то полезное спроанализированное дерево токенов.

Другой удобный метод, который я заметил в кодовой базе, - это метод TokenList._pprint_tree() , который выводит текущее дерево токенов в стандартный вывод.Это очень полезно при попытке написать код, который анализирует дерево.

В целом мое общее впечатление о sqlparse состоит в том, что это не столько библиотека разбора, сколько инструмент для переформатирования SQL.Он включает в себя хороший синтаксический анализатор, но не включает инструменты, необходимые для общего использования дерева, которое он создает.

Чего действительно не хватает в библиотеке, так это посетителя базового узлакласс , такой как тот, который предоставляется модулем Python ast или обходчиком узлов дерева, опять же, как модуль 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)

где extract_branches будет повторяться до конца оператора case.

...