フューチャー技術ブログ

uroborosql-fmtにおける2WaySQLフォーマット (後編: 結果検証編)

はじめに

こんにちは。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
/*IF cond*/
column1
/*ELSE*/
column2
/*END*/
from
table1

このSQLは column1column2 の間に , がないため、PostgreSQLのパーサでは構文エラーとなってしまいます。そこで、条件 cond の真偽で2パターンのSQLを生成し、それぞれフォーマット、マージを行っています。

この処理の詳細については、2WaySQLのフォーマット方法を紹介した記事があるので、そちらを参照してください。

ここで、2WaySQLのフォーマットの為に元のソースコードの分解・マージを行うことで、SQL文の意味が変わってしまうのではないかという懸念があります。そのため、本記事で紹介する検証ロジックを実装しました。

検証ロジック

uroboroSQL-fmtでは、フォーマット前後のSQLに対して字句解析を行い、その結果であるトークン列を比較することでSQLが壊れていないことを検証をしています。

一般的なフォーマッタはトークン列が変わるようなフォーマットをすることがありますが、uroboroSQL-fmtでは以下の2つのケース以外ではトークン列が変わらないように設計しています。

  1. 自動補完をすることで、トークンが追加・削除される可能性がある
  2. カンマの位置を行頭に変更することで、トークン列の順番が変わる可能性がある

これらの場合、単にトークン列を比較するだけではうまくいかないため、それぞれ対処しています。

1. 自動補完をすることで、トークンが追加・削除される可能性がある

この問題に対しては、元のSQLの字句解析結果と、補完・除去オプションをオフにしてフォーマットしたSQLの字句解析結果を比較することで対応しました。

自動補完とは、カラムのエイリアス補完やキャスト変換といったuroboroSQL-fmtのauto-fix機能のことです(詳細はuroboroSQL-fmtのリリース記事をご覧ください)。自動補完により、元のSQLに存在しないトークンを追加したり、元のSQLに存在するトークンを削除する場合があります。

例えば、以下のようなエイリアスを記述していないSQLに対して、エイリアスを自動付与できます。

before_format.sql
select
t1.column1
from
table1 t1
after_format.sql
select
t1.colum1 as column1
from
table1 t1

このような自動補完を行うことでフォーマット前後の字句解析結果は異なる場合があります。

そこで、検証時に実際のフォーマットは別に自動補完のオプションをオフにしたフォーマットを行い、その結果の字句解析結果を元のSQLのものと比較します。

余分なフォーマット処理を行うことにより、実行速度が遅くなるのではないかという懸念がありますが、それについては後述します。

2. カンマの位置を行頭に変更することで、トークン列の順番が変わる可能性がある

この問題に対しては、フォーマット前のSQLに対応した字句解析結果に対して、適切にトークン列を入れ替えることで対処しています。

uroboroSQL-fmt はカンマを行頭に配置するようフォーマットします。そのため、フォーマット前のファイルでカンマを行末コメントの前に置いていた場合、フォーマット前後の字句解析結果は異なった形になります。

例えば、以下のような行末カンマのSQLをフォーマットすると、行頭カンマに置き換わります。

before_format.sql
select
column1, -- column1の説明
column2 -- column2の説明
from
table1
after_format.sql
select
column1 -- column1の説明
, column2 -- column2の説明
from
table1

ここで、,-- column1の説明 が入れ替えられるため、字句解析の結果として得られるトークン列の順番が変わってしまいます。

そこで、検証ロジックでは、字句解析結果の , と行末コメントの並びを入れ替えてから比較を行います。

例えば、上述した行末カンマのSQLに対するトークン列は以下のように、トークンの順序を入れ替えます。

before_formatのトークン列
['select', 'column1', ',', '-- column1の説明', 'column2', ...]
検証で使用されるトークン列
['select', 'column1', '-- column1の説明', ',' , 'column2', ...]

以上2つの対応により、フォーマット前後の字句解析結果を比較することによる、SQLが壊れていないことの検証を行っています。

実行速度

2WaySQLをサポートし、SQLが壊れていないことの検証を行っても、uroboroSQL-fmtは十分高速に動作します。

上記の機能追加に伴い二つの懸念点がありました。

  1. 2WaySQLのフォーマットの為、分岐の数に対応したSQLファイルをフォーマットする処理が重いのではないか
  2. 検証の為、一度余分にフォーマットすることによりフォーマットが遅くなってしまうのではないか

uroboroSQL-fmtは2WaySQLのフォーマットの為に、一つのSQLファイルを分岐の選択肢の数やネストの深さに応じた数のSQLに分割して、それぞれフォーマットを行います。

例えば、次のSQLを考えてみます。

select
/*IF outerCond1*/
column1 as column1
/*IF innerCondA*/
-- pattern1
, column2 as pattern
/*ELIF innerCondB*/
, column3 as column3 -- pattern2
/*ELSE*/
, column4 as column4 -- pattern3
/*END*/
/*ELIF outerCond2*/
column5 as column5 -- pattern4
/*ELSE*/
column6 as column6 -- pattern5
/*END*/
from
table1

このSQLは外側の分岐の選択肢が3つで、outerCond1true の場合に内側の分岐の選択肢が3つあるため、uroboroSQL-fmtは 通りのSQL文をフォーマットします。さらに、検証のために補完・除去オプションをオフにしたフォーマットを行うため、合計通りのSQLを処理することになります。このように、2WaySQLの分岐の選択肢数とネストの深さによって、通常のSQLをフォーマットした時に比べて数倍の時間がかかってしまうのではないかという懸念があります。

そこで、実際のプロジェクトで使用されている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. 1.本記事で紹介したロジックの実装箇所はvalidate.rsです。