Разбирать случай, КОГДА инструкции с помощью sqlparse

#python #sql-parser

#python #sql-анализатор

Вопрос:

У меня есть следующий 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. Написав ответ, я теперь вместо этого смотрю на этот проект , в котором отмечается, что sqlparse предоставляет не дерево, а список токенов. . Хотя это утверждение не совсем точное, sqlparse это не упрощает использование дерева токенов.

2. @MartijnPieters Я просмотрел проект mozzila, но он вызывает исключения рекурсии при анализе более сложных инструкций SQL.

3. Крошки, moz-sql-parser фактически вызывает исключение рекурсии, когда его просят разобрать ваш пример инструкции.

4. @MartijnPieters Точно.

5. Это уже структурированные данные, просто просмотрите словари и списки, возможно, со стеком.

Ответ №1:

Проект действительно немного недокументирован. Я просмотрел примеры и немного отсканировал исходный код. К сожалению, документация не включает все методы в 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 шаблоне вы бы установили больше атрибутов для текущего экземпляра visitor для «переключения передач» и сбора дополнительной информации об этих токенах поддерева с помощью большего количества 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)
  

where extract_branches выполнял бы дальнейшую итерацию до тех пор, пока не дошел бы до конца инструкции case.

Ответ №2:

Чтобы развить [фантастический] ответ Мартина, я думаю, что перевод строки и пробелы следует игнорировать при посещении узлов.

     ...
    ...
    def visit(self, token):
        """Visit a token."""
        if not token.is_whitespace:
            visitor = getattr(self, method, self.generic_visit)
            return visitor(token)