フューチャー技術ブログ

uroborosql-fmtにおける2WaySQLフォーマット (前編: フォーマット方法編)

はじめに

こんにちは、2024年4月入社の川渕皓太です。

先日、当社では新しいSQLフォーマッタであるuroborosql-fmtをリリースしました。このツールは当社が公開しているPostgreSQL向けのSQLコーディング規約に基づいてSQLをフォーマットするものです。

uroborosql-fmtの基本的な情報は以下の記事を参照してください。

uroborosql-fmtはuroborosqlgo-twowaysqldomaといった2WaySQLに対応しています。

2WaySQLとはそのまま実行することもでき、アプリケーションで読み込んでバインドパラメータの指定などをして実行することも出来ます。このように2つの実行方法があることから2WaySQLと呼ばれます

分岐とバインドパラメータを含む2WaySQLの例 (uroborosql)
select
*
from
employee emp
/*BEGIN*/
where
emp.first_name = /*first_name*/'Bob'
/*IF SF.isNotEmpty(first_name)*/
and emp.first_name = /*first_name*/'Bob'
/*END*/
/*IF SF.isNotEmpty(last_name)*/
and emp.last_name = /*last_name*/'Smith'
/*END*/
/*END*/
;

本記事ではuroborosql-fmtにおいて2WaySQLのフォーマットに対応した方法を説明していきます。

uroborosql-fmtのフォーマット方法概要

process_flow.png
  1. tree-sitter-sqlで入力SQLをパースしてCSTを取得
  2. 取得したCSTを解析して独自の木構造の構造体に変換
  3. 変換した構造体から整形したSQLを生成して出力

課題点

2WaySQLでは以下のように、特別なSQLコメントを使用してバインドパラメータや条件の指定ができます。

2WaySQLの例 (uroborosql)
select
*
from
employee emp
where
/*IF SF.isNotEmpty(birth_date_from) and SF.isNotEmpty(birth_date_to)*/
emp.birth_date between /*birth_date_from*/'1990-01-01' and /*birth_date_to*/'1999-12-31'
/*ELSE*/
emp.birth_date < /*birth_date_to*/'1999-12-31'
/*END*/
;

このようなSQLは2WaySQLとしては正しいですが、通常のSQL構文としては不正(where句の内部のandが不足)であり、通常のSQLパーサではパースすることができません。そのため、先述した方法ではフォーマットが行えません。

解決方法として主に以下の二つが考えられます。

  1. 入力SQLのIF文を解析して複数のSQLを生成し、それらのSQLをフォーマット後にマージする
  2. 2WaySQLに対応したパーサを作成する

2WaySQLのIF文はどこに記述しても不正とはならず、文法ファイルが複雑になると考えたため、今回は方法1の「入力SQLのIF文を解析して複数のSQLを生成し、それらのSQLをフォーマット後にマージする」 案を採用しました。

2WaySQLをフォーマットする方法概要

2way_sql.png
  1. 入力SQLのIF文等の分岐を解析して生成されうるSQLを分岐網羅的に生成
  2. 生成したすべてのSQLをフォーマット
  3. フォーマット後のすべてのSQLをマージして出力

2WaySQLのフォーマット方法詳細

具体例として以下のようなSQLをフォーマットする場合を考えます。

入力2WaySQL
select
/*IF hoge*/
aaa
/*IF huga*/
,bbb
/*ELIF foo*/
,ccc
/*END*/
/*ELSE*/
ddd
/*END*/
from table1

1. 生成されうるSQLを分岐網羅的に生成

SQLを1行目から順にたどっていき、IF分岐を解析して以下のような木構造を作成します。

木構造を作成する際、ソースコード上で出現が早いほど左の子になるよう作成します。

この木構造の葉は生成されうるSQLを示しています。葉は分岐網羅を満たすように生成します。

2WaySQL_for_blog.png

2. 生成されたSQLのフォーマット

次に生成された木構造の葉をすべてフォーマットします。

単純なSQLのフォーマット方法はこちらの記事で詳細に説明しているため割愛します。

2WaySQL_for_blog-フォーマット後.drawio_(1).png

3. フォーマット後のSQLをマージ

木の深いところから順にマージしていき、最終的に木構造の根が全体の最終フォーマット結果になるようにします。

2WaySQL_for_blog-マージ1.drawio.png

マージは以下の処理を行うことで実現します。

2つのSQLの各行を比較
[一致する場合]
1. 現在の行を描画
2. continue

[一致しない場合]
1. 左の子から順に/*END*/の1行前まで描画
2. /*END*/を描画
3. continue

具体的に以下のマージを説明していきます。

2WaySQL_for_blog-マージ2.drawio.png
  1. 3行目まで一致するのでそのまま描画

    select
    /*IF hoge*/
    aaa
  2. 4行目は一致しない(左: /*IF huga*/、右: /*ELIF foo*/)ので、まず左の子から6行目の/*END*/の1行前まで描画

    select
    /*IF hoge*/
    aaa
    /*IF huga*/
    , bbb
  3. 右の子の6行目の/*END*/の1行前まで描画

    select
    /*IF hoge*/
    aaa
    /*IF huga*/
    , bbb
    /*ELIF foo*/
    , ccc
  4. /*END*/を描画

    select
    /*IF hoge*/
    aaa
    /*IF huga*/
    , bbb
    /*ELIF foo*/
    , ccc
    /*END*/
  5. 行の比較を再開。これ以降はすべて一致するのでそのまま描画

    select
    /*IF hoge*/
    aaa
    /*IF huga*/
    , bbb
    /*ELIF foo*/
    , ccc
    /*END*/
    /*END*/
    from
    table1

これで①のマージ処理が完了しました。

同様のマージ処理を②においても実行することで、最終的に以下のようなフォーマット結果が得られます。

最終的なフォーマット結果
select
/*IF hoge*/
aaa
/*IF huga*/
, bbb
/*ELIF foo*/
, ccc
/*END*/
/*ELSE*/
ddd
/*END*/
from
table1

さいごに

当社が作成したSQLフォーマッタであるuroborosql-fmtにおける2WaySQLのフォーマット方法を説明しました。

uroborosql-fmtは元のSQLを壊していないか検証するロジックを組み込んでいますが、万が一元のSQLが変更されている等の不具合があればissueに起票してくださると幸いです。

元のSQLを壊していないか検証するロジックについては以下の記事で紹介していますので是非ご覧ください。

関連記事