From df4fc67f541614f0a17f85355bd2c093fd3eb5ac Mon Sep 17 00:00:00 2001 From: ma91n Date: Tue, 10 Dec 2024 15:32:28 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=A4=E3=83=B3=E3=82=BF=E3=83=BC=E3=83=B320?= =?UTF-8?q?24?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...03\343\202\277\351\226\213\347\231\272.md" | 269 ++++++++++++++++++ .../as-alignment-actual-expected.png | Bin 0 -> 37423 bytes source/images/20241210a/export-demo.avif | Bin 0 -> 726240 bytes source/images/20241210a/export-demo.gif | Bin 0 -> 129234 bytes source/images/20241210a/image.png | Bin 0 -> 69316 bytes source/images/20241210a/import-demo.avif | Bin 0 -> 614975 bytes source/images/20241210a/thumbnail.gif | Bin 0 -> 10038 bytes source/images/20241210a/usecase-1-share.png | Bin 0 -> 172805 bytes 8 files changed, 269 insertions(+) create mode 100644 "source/_posts/20241210a_Engineer_Camp_2024\357\274\232_Rust_\343\201\247\343\201\256SQL\343\203\225\343\202\251\343\203\274\343\203\236\343\203\203\343\202\277\351\226\213\347\231\272.md" create mode 100644 source/images/20241210a/as-alignment-actual-expected.png create mode 100644 source/images/20241210a/export-demo.avif create mode 100644 source/images/20241210a/export-demo.gif create mode 100644 source/images/20241210a/image.png create mode 100644 source/images/20241210a/import-demo.avif create mode 100644 source/images/20241210a/thumbnail.gif create mode 100644 source/images/20241210a/usecase-1-share.png diff --git "a/source/_posts/20241210a_Engineer_Camp_2024\357\274\232_Rust_\343\201\247\343\201\256SQL\343\203\225\343\202\251\343\203\274\343\203\236\343\203\203\343\202\277\351\226\213\347\231\272.md" "b/source/_posts/20241210a_Engineer_Camp_2024\357\274\232_Rust_\343\201\247\343\201\256SQL\343\203\225\343\202\251\343\203\274\343\203\236\343\203\203\343\202\277\351\226\213\347\231\272.md" new file mode 100644 index 00000000000..b0b23f4b98e --- /dev/null +++ "b/source/_posts/20241210a_Engineer_Camp_2024\357\274\232_Rust_\343\201\247\343\201\256SQL\343\203\225\343\202\251\343\203\274\343\203\236\343\203\203\343\202\277\351\226\213\347\231\272.md" @@ -0,0 +1,269 @@ +--- +title: "Engineer Camp 2024: Rust でのSQLフォーマッタ開発" +date: 2024/12/10 00:00:00 +postid: a +tag: + - SQL + - VSCode + - フォーマッター + - インターン + - インターン2024 + - Rust + - 2WaySQL +category: + - DB +thumbnail: /images/20241210a/thumbnail.gif +author: 仲泰志 +lede: "Engineer Camp 2024 に参加した仲です。今回のインターンシップではRust製SQLフォーマッタの開発を行いました。この記事では、期間中に取り組んだ内容について紹介します!]" +--- +# はじめに + +Engineer Camp 2024 に参加した仲です。今回のインターンシップではRust製SQLフォーマッタの開発を行いました。この記事では、期間中に取り組んだ内容について紹介します! + +過去の関連インターン記事はこちらです: + +- [Engineer Camp2022 RustでSQLフォーマッタ作成(前編)](/articles/20220916b/) +- [Engineer Camp2022 RustでSQLフォーマッタ作成(後編)](/articles/20220916c/) + +# インターン内容 + +フューチャーが開催している夏のインターンシップ[「Engineer Camp 2024」](https://note.com/future_event/n/nf586b6248ef9)では、社内のプロジェクトに参画し4週間にわたり開発業務を体験します。今年は全19コースの募集がありました。 + +私が参加したコースは [(2)Rust製SQLフォーマッタの開発](https://note.com/future_event/n/nf586b6248ef9#c1363421-2c55-4738-b345-676c2d4ea9be) です。Rust製のSQLフォーマッタである uroboroSQL-fmt やVSCode拡張等の周辺ツールに対し、不具合修正や機能追加を実施しました。 + +開発対象である [uroboroSQL-fmt](https://github.com/future-architect/uroborosql-fmt) は、フューチャーのSQLコーディング規約に基づいてコード整形を行うフォーマッタです。詳細は[新しいSQLフォーマッターであるuroboroSQL\-fmtをリリースしました](/articles/20231120a/)の記事をご覧ください。 + +- https://github.com/future-architect/uroborosql-fmt + +実際に取り組んだタスクは様々ありますが、ここではそのうち2つをピックアップしてご紹介します。 + +## 1. VSCode拡張への機能追加 + +uroboroSQL-fmtにはVSCode拡張機能が存在します。 + +- https://marketplace.visualstudio.com/items?itemName=Future.uroborosql-fmt + +インターンでは、このVSCode拡張へ以下の2つの機能を追加しました。 + +1. VSCode の設定を元に、フォーマッタの設定ファイル(`.uroborosqlfmtrc.json`)を出力する**export**機能 +1. フォーマッタの設定ファイルで指定したフォーマットオプションを、VSCode の設定(`settings.json`)へ反映する**import**機能 + +### 機能デモ + +それぞれの機能を紹介します。 + +#### 1. export 機能 + +コマンドパレットから export コマンドを呼び出すことで、VSCodeの設定をフォーマッタの設定ファイル `.uroborosqlfmtrc.json` に反映できます。 + +コマンドパレットでexportコマンドを実行 + +#### 2. import 機能 + +コマンドパレットから import コマンドを呼び出すことで、フォーマッタの設定ファイル `.uroborosqlfmtrc.json` の内容をVSCodeの設定に反映できます。 + +コマンドパレットでimportコマンドを実行 + +### 本機能のユースケース + +#### 1. マルチリポジトリ構成での設定共有 + +昨今では、マルチリポジトリ構成での開発は珍しくありません。マルチリポジトリ構成においてVSCodeの設定は各リポジトリに適したものを用意しますが、SQLフォーマッタの設定は関連リポジトリ全体で共有することが望ましいでしょう。そのような場合の設定共有は、フォーマッタ用の設定ファイルを各リポジトリに配置することで対応できます。 + +しかし、フォーマッタ用の設定ファイルはただのjsonファイルなのでエディタで直接編集するのは手間がかかります。 + +そこで、今回追加したexport機能により、VSCodeの設定編集UIで作成した設定からフォーマッタ用の設定ファイルを出力し、各リポジトリに配布して共有することが可能となります。 + +また、VSCodeで[`uroborosql-fmt.configurationFilePath`](https://github.com/future-architect/vscode-uroborosql-fmt?tab=readme-ov-file#settings) を設定すれば、プロジェクトのルートにない設定ファイルを参照することもできます。 + +usecase-1-share.png + +#### 2. 既存のフォーマッタ用の設定ファイルに変更を加える場合 + +VSCodeの設定編集UIは、取りうる値をドロップダウンで選択できたり、設定項目のドキュメントを読みながら編集ができます。import 機能を用いることでフォーマッタ用の設定ファイルをVSCodeの設定に取り込むことができ、設定の変更が容易になります。 + +適切な設定が用意できたら、export 機能でフォーマッタ用の設定ファイルを更新できます。 + +image.png + +## 2. 2WaySQL との格闘 + +uroboroSQL-fmt の特徴の一つとして、2WaySQLに対応していることが挙げられます。2WaySQLをサポートするには通常のSQLとは異なる考慮が必要であるため、uroboroSQL-fmtでは2WaySQL用のフォーマット処理が用意されています。インターン期間中に2WaySQL関連の不具合が見つかりましたが、その一つは原理的に解決が難しいものでした。結局インターン期間でその不具合を完全に解決するには至らず、部分的に解決する方法をとりました。 + +このセクションではその不具合について説明するとともに、どんな解決策をとったかについて記します。 + +### 2WaySQL とは + +2WaySQL とは、バインドパラメータや制御構文を利用して実行することができるSQLの拡張構文のようなものです。uroboroSQL-fmt は [uroboroSQL](https://future-architect.github.io/uroborosql-doc/)、[go-twowaysql](https://github.com/future-architect/go-twowaysql)、[Doma](https://doma.readthedocs.io/en/latest/) といった2WaySQLをサポートしています。 + +### uroboroSQL-fmt の 2WaySQL フォーマット戦略 + +2WaySQLは通常のSQLとして不正な構文になることがあるため、通常のSQLのパーサではハンドリングできないケースがあります。uroboroSQL-fmt はこの問題を解消するために、2WaySQLに対してはパーサで読み込む前の段階でテキストを分解し、フォーマット後にマージして再構築するという戦略をとっています。これにより、フォーマット処理のコアロジックやパーサジェネレータの文法定義は通常のSQLをターゲットにしたままで、フォーマッタ全体として2WaySQLに対応することを可能にしています。 + +2WaySQLのフォーマット処理についてはこれらの記事が詳しいです: + +- [uroborosql-fmtにおける2WaySQLフォーマット (前編: フォーマット方法編)](/articles/20241018a/) +- [uroborosql-fmtにおける2WaySQLフォーマット (後編: 結果検証編)](/articles/20241021a/) + +### 実際の不具合 +インターン期間中に遭遇したフォーマッタの不具合として、2WaySQLにおいて `as` キーワードの縦揃えが崩れてしまうというものがありました。 + +as-alignment-actual-expected.png + +### 不具合が起こる仕組み + +この不具合が発生した理由について、例を使って詳しく追ってみます。 + +2WaySQLのフォーマットにおけるSQLの分割およびマージは2WaySQLが持つ制御構文(`/* IF ... */`や`/* ELSE */`)に基づいて行単位で実施されます。(詳細は[前述の記事](https://future-architect.github.io/articles/20241018a/)に譲ります) + +```sql 分割前.sql +select + 'a' as a +/*IF true*/ +, 'b' as b +/*ELSE*/ +, 'ccccccccccccccc' as c +/*END*/ +; +``` + +上のようなSQL(分割前.sql)は下に示すように、`IF` 節の中身を持つSQL(first.sql)と`ELSE`節の中身を持つSQL(second.sql)に分割されます。 + +```sql first.sql +select + 'a' as a +/*IF true*/ +, 'b' as b +/*END*/ +; +``` + +```sql second.sql +select + 'a' as a +/*ELSE*/ +, 'ccccccccccccccc' as c +/*END*/ +; +``` + +first.sql と second.sql のそれぞれをフォーマットすると次のようになります。`as` キーワードに注目すると、フォーマット処理によって縦揃えが行われていることを確認できます。 + +```sql first.sql +select + 'a' as a +/*IF true*/ +, 'b' as b +/*END*/ +; +``` + +```sql second.sql +select + 'a' as a +/*ELSE*/ +, 'ccccccccccccccc' as c +/*END*/ +; +``` + +`as` の縦揃え位置は `as` キーワードの前に現れる要素(`'a'` など)の長さによって変化しますが、分割されたそれぞれのSQLは他方のSQLの情報を持ちません。すると、マージされたSQLでは次のように、`IF`節のインデントと`ELSE`節のインデントがずれる、という現象が発生してしまいます。 + +```sql merged.sql +select + 'a' as a +/*IF true*/ +, 'b' as b +/*ELSE*/ +, 'ccccccccccccccc' as c +/*END*/ +; +``` + +### 対処 + +2WaySQLの分割処理およびマージ処理はテキストの行単位で実施されます。そのため縦揃えのように複数行にまたがる情報の保持が必要な処理では、このような問題の発生を防ぐことができません。さらにフォーマッタのコアロジックはできる限り2WaySQLの知識を持たない方針で実装されているため、問題の解決には2WaySQLを扱う処理のアーキテクチャを大きく変える必要がありそうということがわかりました。 + +2WaySQLを扱うアーキテクチャを新たに検討し実装するのはそれなりに大規模な変更となることが見込まれます。そのような変更をインターン期間中で実施するのは難しいと判断したため、今回はこの問題を**部分的に**解消する修正を入れることにしました。具体的には、通常のSQLとしてパースできる2WaySQLについては分割・マージ処理を行わず、そのままSQLとしてフォーマットするようにしました。 + +たとえば今回の例に使用したSQLは2WaySQLの機能を使うものの、通常のSQLとしてもパースが可能です。 + +```sql 通常のSQLとしても文法的に正しいSQL.sql +select + 'a' as a +/*IF true*/ +, 'b' as b +/*ELSE*/ +, 'ccccccccccccccc' as c +/*END*/ +; +``` + +このようなSQLを通常のSQLとしてフォーマットする今回の修正により、下図のように縦揃えのそろったフォーマットが行われるようになりました。 + +```sql フォーマット後.sql +select + 'a' as a +/*IF true*/ +, 'b' as b +/*ELSE*/ +, 'ccccccccccccccc' as c +/*END*/ +; +``` + +もちろん、通常のSQLとしてのパースができない2WaySQLでは、依然として同様の問題が発生し得ます。 + +例えば次に示す2WaySQLは、前述したSQLに from 句と where 句を付加したものです。2WaySQLの条件分岐機能を使っている where 句に注目すると、通常のSQLとして解釈するためには where 句のセパレータ `and` または `or` が不足していることがわかります。 + +```sql 通常のSQLとしては不正な2WaySQL(フォーマット前).sql +select + 'a' as a +/*IF true*/ +, 'b' as b +/*ELSE*/ +, 'ccccccccccccccc' as c +/*END*/ +from + some_table t +where +/*IF some_condition */ + some_column = 'value' -- 通常のSQLとして解釈するには、この位置にセパレータ(and, or)が必要 +/*ELSE*/ + some_column = 'other_value' +/*END*/ +; +``` + +これをフォーマットすると次のようになり、`as` キーワードの縦揃えが再び崩れていることが確認できます。 + +```sql 通常のSQLとしては不正な2WaySQL(フォーマット後).sql +select + 'a' as a +/*IF true*/ +, 'b' as b +/*ELSE*/ +, 'ccccccccccccccc' as c +/*END*/ +from + some_table t +where +/*IF some_condition */ + some_column = 'value' -- 通常のSQLとして解釈するには、この位置にセパレータ(and, or)が必要 +/*ELSE*/ + some_column = 'other_value' +/*END*/ +; +``` + +このような2WaySQLの扱いについては現在検討中です。今後の修正にご期待ください。 + +## インターンの感想 + +Rust と静的解析をやりたいという動機で参加したインターンでしたが、仕事の進め方や見積もり方、自分の貢献の伝え方など、至るところに思わぬ学びがたくさんありました。毎日の定例ミーティングや最終発表を通して、人に何かを説明する際の情報のまとめ方・表現のやりかたを鍛えることができたように思います。 + +開発者の体験を良くする仕組みは私が興味とするところなので、今回そのような具体的なツールに携われたことは非常に大きな経験になりました。すでにあるコードベースを読み解き、影響範囲を検証しながら修正を加えていく作業がとても楽しかったです。 + +## さいごに + +インターンではRust製SQLフォーマッタの開発を行いました。関わっていただいた社員のみなさん、そして同じインターン生のみなさんにもとても感謝しています。ありがとうございました! diff --git a/source/images/20241210a/as-alignment-actual-expected.png b/source/images/20241210a/as-alignment-actual-expected.png new file mode 100644 index 0000000000000000000000000000000000000000..b5e326e4b52f6111510aefd34caeaf20cd49ea2c GIT binary patch literal 37423 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