はじめに
こんにちは。2024年4月入社の齋藤です。
当社が開発したSQLフォーマッタであるuroboroSQL-fmt において、フォーマット前のSQLを壊していないかを検証するロジックについて紹介します。
概要
uroboroSQL-fmtは当社が公開しているPostgreSQLのコーディング規約に従ってSQL文をフォーマットするツールです。当ツールは2WaySQLのフォーマットもサポートしていますが、そのためにSQLソースの分解・再構築を行っています。これにより元のSQLにあったトークンをなくしてしまったり、同じトークンを重複させてしまうのではないかという懸念がありました。そこで、フォーマット前後の字句解析の結果を比較することで、SQLの意味が変わっていないことを検証しています。
2WaySQLフォーマットロジック概要
uroboroSQL-fmtではPostgreSQL向けのパーサ (tree-sitter-sql) で構文解析を行い、その結果を利用してフォーマットをしています。そのため、SQL文として不正な2WaySQLのソースをフォーマットできないという問題がありました。
当フォーマッタでは2WaySQLの条件分岐をサポートするために、元のSQLを条件分岐に対応したSQLに分解します。そして、各SQLをフォーマットしてからマージして、一つのSQLファイルに復元します。
例えば、以下のSQLを考えます。
select |
このSQLは column1
と column2
の間に ,
がないため、PostgreSQLのパーサでは構文エラーとなってしまいます。そこで、条件 cond
の真偽で2パターンのSQLを生成し、それぞれフォーマット、マージを行っています。
この処理の詳細については、2WaySQLのフォーマット方法を紹介した記事があるので、そちらを参照してください。
ここで、2WaySQLのフォーマットの為に元のソースコードの分解・マージを行うことで、SQL文の意味が変わってしまうのではないかという懸念があります。そのため、本記事で紹介する検証ロジックを実装しました。
検証ロジック
uroboroSQL-fmtでは、フォーマット前後のSQLに対して字句解析を行い、その結果であるトークン列を比較することでSQLが壊れていないことを検証をしています。
一般的なフォーマッタはトークン列が変わるようなフォーマットをすることがありますが、uroboroSQL-fmtでは以下の2つのケース以外ではトークン列が変わらないように設計しています。
- 自動補完をすることで、トークンが追加・削除される可能性がある
- カンマの位置を行頭に変更することで、トークン列の順番が変わる可能性がある
これらの場合、単にトークン列を比較するだけではうまくいかないため、それぞれ対処しています。
1. 自動補完をすることで、トークンが追加・削除される可能性がある
この問題に対しては、元のSQLの字句解析結果と、補完・除去オプションをオフにしてフォーマットしたSQLの字句解析結果を比較することで対応しました。
自動補完とは、カラムのエイリアス補完やキャスト変換といったuroboroSQL-fmtのauto-fix機能のことです(詳細はuroboroSQL-fmtのリリース記事をご覧ください)。自動補完により、元のSQLに存在しないトークンを追加したり、元のSQLに存在するトークンを削除する場合があります。
例えば、以下のようなエイリアスを記述していないSQLに対して、エイリアスを自動付与できます。
select |
select |
このような自動補完を行うことでフォーマット前後の字句解析結果は異なる場合があります。
そこで、検証時に実際のフォーマットは別に自動補完のオプションをオフにしたフォーマットを行い、その結果の字句解析結果を元のSQLのものと比較します。
余分なフォーマット処理を行うことにより、実行速度が遅くなるのではないかという懸念がありますが、それについては後述します。
2. カンマの位置を行頭に変更することで、トークン列の順番が変わる可能性がある
この問題に対しては、フォーマット前のSQLに対応した字句解析結果に対して、適切にトークン列を入れ替えることで対処しています。
uroboroSQL-fmt はカンマを行頭に配置するようフォーマットします。そのため、フォーマット前のファイルでカンマを行末コメントの前に置いていた場合、フォーマット前後の字句解析結果は異なった形になります。
例えば、以下のような行末カンマのSQLをフォーマットすると、行頭カンマに置き換わります。
select |
select |
ここで、,
と -- column1の説明
が入れ替えられるため、字句解析の結果として得られるトークン列の順番が変わってしまいます。
そこで、検証ロジックでは、字句解析結果の ,
と行末コメントの並びを入れ替えてから比較を行います。
例えば、上述した行末カンマのSQLに対するトークン列は以下のように、トークンの順序を入れ替えます。
['select', 'column1', ',', '-- column1の説明', 'column2', ...] |
['select', 'column1', '-- column1の説明', ',' , 'column2', ...] |
以上2つの対応により、フォーマット前後の字句解析結果を比較することによる、SQLが壊れていないことの検証を行っています。
実行速度
2WaySQLをサポートし、SQLが壊れていないことの検証を行っても、uroboroSQL-fmtは十分高速に動作します。
上記の機能追加に伴い二つの懸念点がありました。
- 2WaySQLのフォーマットの為、分岐の数に対応したSQLファイルをフォーマットする処理が重いのではないか
- 検証の為、一度余分にフォーマットすることによりフォーマットが遅くなってしまうのではないか
uroboroSQL-fmtは2WaySQLのフォーマットの為に、一つのSQLファイルを分岐の選択肢の数やネストの深さに応じた数のSQLに分割して、それぞれフォーマットを行います。
例えば、次のSQLを考えてみます。
select |
このSQLは外側の分岐の選択肢が3つで、outerCond1
が true
の場合に内側の分岐の選択肢が3つあるため、uroboroSQL-fmtは
そこで、実際のプロジェクトで使用されているSQLを対象に実行速度を計測してみました。対象ファイルには行数、選択肢数、ネストの深さが大きい4ファイルをピックアップしています。比較対象には現在Futureで使われているuroboroSQL-formatter (現行版)を使用します。現行版に対して、uorboroSQL-fmtを新版と表記します。
(※フォーマット時間の測定にはPowerShellのMeasure-Commandを使用しています)
各ファイル1度ずつしか計測しておらず、ファイルの内容によってフォーマット時間は変わるためあくまで参考値ですが、分岐の選択肢数やネストの深さにかかわらず、新版は現行版の10倍~400倍(!!)程度の速度でフォーマットできています。
No | 特徴 | 行数 | 選択肢の最大数 | ネストの深さ | 現行版(ms) | 新版(ms) |
---|---|---|---|---|---|---|
1 | 分岐なし行数多い | 3985 | 0 | 0 | 62384.3937 | 148.0973 |
2 | 分岐あり行数多い | 1668 | 2 | 1 | 7914.6458 | 164.0513 |
3 | 選択肢が多い | 274 | 9 | 1 | 648.2352 | 69.1618 |
4 | ネストが深い | 394 | 2 | 5 | 1011.0682 | 61.5854 |
まとめ
本記事では、uroboroSQL-fmtで行っている、SQLの意味を変えてしまっていないかの検証ロジックについて説明しました。もし、検証漏れ等が見つかりましたら、IssueやPRいただけると幸いです。1
- 1.本記事で紹介したロジックの実装箇所はvalidate.rsです。 ↩