Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

uroborosql-fmt #1407

Merged
merged 1 commit into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ uroborosql-fmtは元のSQLを壊していないか検証するロジックを組

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

- [uroborosql-fmtにおける2WaySQLフォーマット (後編: 結果検証編)]※近日公開予定!
- [uroborosql-fmtにおける2WaySQLフォーマット (後編: 結果検証編)](/articles/20241021a/)

# 関連記事

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
---
title: "uroborosql-fmtにおける2WaySQLフォーマット (後編: 結果検証編)"
date: 2024/10/21 00:00:00
postid: a
tag:
- uroboroSQL
- フォーマッター
category:
- Infrastructure
thumbnail: /images/20241021a/thumbnail.png
author: 齋藤俊哉
lede: "当社が開発したSQLフォーマッタである[uroboroSQL-fmt] において、フォーマット前のSQLを壊していないかを検証するロジックについて紹介します。"
mathjax: true
---

<img src="/images/20241021a/logo.png" alt="" width="630" height="229">

## はじめに

こんにちは。2024年4月入社の齋藤です。

当社が開発したSQLフォーマッタである[uroboroSQL-fmt](https://github.com/future-architect/uroborosql-fmt) において、フォーマット前のSQLを壊していないかを検証するロジックについて紹介します。

## 概要

[uroboroSQL-fmt](https://github.com/future-architect/uroborosql-fmt)は当社が公開しているPostgreSQLのコーディング規約に従ってSQL文をフォーマットするツールです。当ツールは[2WaySQL](https://future-architect.github.io/uroborosql-doc/background/)のフォーマットもサポートしていますが、そのためにSQLソースの分解・再構築を行っています。これにより元のSQLにあったトークンをなくしてしまったり、同じトークンを重複させてしまうのではないかという懸念がありました。そこで、フォーマット前後の字句解析の結果を比較することで、SQLの意味が変わっていないことを検証しています。

<!--これにより元のSQLが壊れていないかどうかを検証することが課題でした。そこで、フォーマット前後の字句解析の結果を比較することで、SQLの意味が変わっていないことを検証します。-->

## 2WaySQLフォーマットロジック概要

uroboroSQL-fmtではPostgreSQL向けのパーサ ([tree-sitter-sql](https://github.com/future-architect/tree-sitter-sql)) で構文解析を行い、その結果を利用してフォーマットをしています。そのため、SQL文として不正な2WaySQLのソースをフォーマットできないという問題がありました。

当フォーマッタでは2WaySQLの条件分岐をサポートするために、元のSQLを条件分岐に対応したSQLに分解します。そして、各SQLをフォーマットしてからマージして、一つのSQLファイルに復元します。

例えば、以下のSQLを考えます。

```sql
select
/*IF cond*/
column1
/*ELSE*/
column2
/*END*/
from
table1
```

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

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

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

## 検証ロジック

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

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

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

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

<!--
検証ロジックを実装するにあたり、フォーマット前後のSQL文を字句解析することで得られるトークン列は(大文字・小文字を区別しないとして、)変わらないだろうという仮説を立てました。uroboroSQL-fmtではその仮説の元、tree-sitter-sqlによる字句解析結果を比較することによる検証を行っています。

しかし、この仮説には以下の反例があります。

1. 自動補完を行うことで、字句解析の結果が変わる可能性がある
1. カンマの位置を行頭に変更することで、トークン列の順番が変わる場合がある
-->

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

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

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

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

```sql before_format.sql
select
t1.column1
from
table1 t1
```

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

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

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

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

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

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

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

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

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

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

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

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

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

```python before_formatのトークン列
['select', 'column1', ',', '-- column1の説明', 'column2', ...]
```

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


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

## 実行速度

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

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

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

uroboroSQL-fmtは2WaySQLのフォーマットの為に、一つのSQLファイルを分岐の選択肢の数やネストの深さに応じた数の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つで、`outerCond1` が `true` の場合に内側の分岐の選択肢が3つあるため、uroboroSQL-fmtは $3 + 1 + 1 = 5$通りのSQL文をフォーマットします。さらに、検証のために補完・除去オプションをオフにしたフォーマットを行うため、合計$10$通りのSQLを処理することになります。このように、2WaySQLの分岐の選択肢数とネストの深さによって、通常のSQLをフォーマットした時に比べて数倍の時間がかかってしまうのではないかという懸念があります。

そこで、実際のプロジェクトで使用されているSQLを対象に実行速度を計測してみました。対象ファイルには行数、選択肢数、ネストの深さが大きい4ファイルをピックアップしています。比較対象には現在Futureで使われている[uroboroSQL-formatter](https://github.com/future-architect/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|

<!--
ファイルの内容によってフォーマット時間は変わるためあくまで参考値ですが、分岐の数やネストの深さ・行数が多くても、概ね10~200ms程度に収まっています。
(※ファイルの入出力はフォーマット時間に含めていません。)

|行数|分岐数|ネストの最大の深さ|フォーマット時間 (ms)|
|--|--|--|--|
|394|33|5|27.8712|
|861|77|2|15.6737|
|4625|37|1|52.4628|
|1668|71|2|150.3517|
-->

## まとめ

本記事では、uroboroSQL-fmtで行っている、SQLの意味を変えてしまっていないかの検証ロジックについて説明しました。もし、検証漏れ等が見つかりましたら、IssueやPRいただけると幸いです。[^1]

[^1]: 本記事で紹介したロジックの実装箇所は[validate.rs](https://github.com/future-architect/uroborosql-fmt/blob/main/crates/uroborosql-fmt/src/validate.rs)です。
Empty file.
Binary file added source/images/20241021a/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added source/images/20241021a/thumbnail.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading