sqlparseを使って簡単にSQLをパースする

大量のSQLのリストやログから特定テーブルへのUPDATEを行うクエリだけを抽出したいといったケースが、しばしばあります。1つのアプリケーションからアクセスされるDBであればコードを追っていくことでどうにかなるのですが、異なる言語やORMで構成された複数のアプリケーションで共用していたり、エンジニアやアナリストがアドホックにクエリを実行していたりすると、なかなか厄介です。

  • 予約語を大文字にしたり小文字にしたり、改行やインデントなど、フォーマットがまちまち
  • サブクエリを含むと "どのテーブルに対する操作か" が、単純な文字列マッチ (字句解析) などではわからない

PythonでSQLを簡単にパースするライブラリとして sqlparse を紹介します。

$ python --version
Python 3.6.9

$ pip list | grep sqlparse
sqlparse                      0.3.1

sqlparseのsplitメソッドでステートメントの分割、formatメソッドで整形などの簡単な処理ができます。

import sqlparse

# ステートメントの分割
statements = sqlparse.split("""
select id, name from customers where deleted = 0;
select created_at where id = 10
""")

print(statements)
# ['select id, name from customers where deleted = 0;', 'select created_at where id = 10']
print(type(statements[0])  # <class 'str'>

# 整形
statement = sqlparse.format(statements[0], reindent=True, keyword_case="upper")

print(statement)
# SELECT id,
#        name
# FROM customers
# WHERE deleted = 0;

さらにparseでトークン解析もできます。parseではStatementインスタンスが返されます。このインスタンスのtokensプロパティがTokenのリストとなってます。Tokenは再帰的な木構造になってます。

  • Token.ttypeにDML or DLL、Where、Keywordなどの種類が入ってます
  • Tokenにもtokensプロパティを持ちます
    • 以下の例ではIdentifierListのが4つのトークン ("id", ",", " ", "name") を持っています
    • parentも持っており、上にたどることもできます
parsed_statements = sqlparse.parse("""
select id, name from customers where deleted = 0;
select created_at where id = 10
""")
pprint(parsed_statements)
# (<Statement ' selec...' at 0x7F39CE745390>,
#  <Statement ' selec...' at 0x7F39CE669ED0>)

parsed_statement = parsed_statements[0]
pprint(parsed_statement.tokens)
# [<Newline ' ' at 0x7F39CE6FAC48>,
#  <DML 'select' at 0x7F39CE6D13A8>,
#  <Whitespace ' ' at 0x7F39CE6D1048>,
#  <IdentifierList 'id, na...' at 0x7F39CE6D8660>,
#  <Whitespace ' ' at 0x7F39CE6D1288>,
#  <Keyword 'from' at 0x7F39CE6D12E8>,
#  <Whitespace ' ' at 0x7F39CE6D1348>,
#  <Identifier 'custom...' at 0x7F39CE6D85E8>,
#  <Whitespace ' ' at 0x7F39CE7398E8>,
#  <Where 'where ...' at 0x7F39CE6D8570>]

print(type(parsed_statement.tokens[0]))
# <class 'sqlparse.sql.Token'>

pprint(parsed_statement.tokens[3].tokens)
# [<Identifier 'id' at 0x7F39CE69BD68>,
#  <Punctuation ',' at 0x7F39CE69DA08>,
#  <Whitespace ' ' at 0x7F39CE699BE8>,
#  <Identifier 'name' at 0x7F39CE69B840>]