SQL整形
SQLフォーマッターは、クエリのインデントをブラウザ内で整え直します。正規表現でキーワードを探すのではなく、まずSQLをトークン化します。この違いこそが、きちんと動くフォーマッターと、FROMという語を含む最初の文字列リテラルで壊れるフォーマッターとを分けています。
SQL整形はどう動きますか?
SQLフォーマッターの評価は、想定外のクエリをどう扱うかで決まります。コードベース全体に安心してかけられるフォーマッターと、移行用のSQLを黙って壊すフォーマッターを分けるのは、次の4点です。
パターンマッチではなくトークン化
定番の実装は、正規表現でキーワードを見つけて改行を挿入するものです。ところがクエリに文字列リテラルとして'FROM cache'が含まれていたり、アポストロフィ入りのコメントがあったりすると破綻し、もはやパースできないものを出力します。このツールでは、まずクエリをトークン化します。文字列、識別子、コメント、数値、演算子、キーワードは別々のものとして扱われ、レイアウトに影響するのはキーワードトークンだけです。上のサンプルクエリにFROMという語を含む文字列リテラルが入っているのは、まさにこのためです。
引用符付きの識別子は書き換えない
"User"とUserは、大文字小文字を区別するデータベースでは別のオブジェクトです。引用符を「整理」として取り去ってしまうフォーマッターは、クエリが読み取るテーブルを変えてしまっています。引用の書き方も方言ごとに異なります。MySQLのバッククォート、T-SQLの角括弧、標準の二重引用符。つまり方言の選択は、出力スタイルではなくパースの正しさに関わるものです。
方言ごとの文字列ルール
文字列内の引用符の標準的なエスケープは、引用符を2つ重ねることです。MySQLはバックスラッシュも有効にします。Postgresにはドル引用符という形式があり、区切りは書き手が選べます。$func$ … $func$のような形で、関数の本体にどんな内容でも含められるようにするためのものです。これらを知らないトークナイザーは文字列を途中で閉じてしまい、その後のすべてが誤分類されます。
コメントの位置は保たれる
単独行にあるコメントは単独行に留まり、行末のコメントは行末に留まります。標準SQLではC言語と違ってブロックコメントがネストするため、/* a /* b */ c */は1つのコメントであって構文エラーではありません。最小化では--コメントをブロックコメントに変換します。1行に連結した状態で行コメントのままにすると、残りのクエリ全体がコメントアウトされてしまうからです。この小さな配慮の有無が、最小化したクエリを使えるものと壊れたものに分けます。
出力のレイアウト
句は行の先頭から始まり、結合も行の先頭に置かれ、カラムは1行に1つずつ並びます。WHEREの中の論理結合子は継続行の先頭に来ますが、トップレベルの場合だけです。(y = 2 AND z = 3)の中のANDは式の一部なので、元の位置に留まります。行頭カンマも選べます。diffでカラムのリストを並べ替えやすくなるためで、最初は奇妙に見えても、実際に筋の通った理由があります。
入力
select a,b from t where x=1 and y=2
整形後
SELECT a, b FROM t WHERE x = 1 AND y = 2
SQL整形が対応するオプションとエッジケースは?
| パラメータ | 型 | デフォルト | 動作とエッジケース |
|---|---|---|---|
| 方言 | standard / postgres / mysql / tsql | postgres | スタイルではなく解析に影響します。識別子をどの文字で囲むか、文字列が引用符をどうエスケープするか、dollar-quotingや#コメントが存在するか、といった点です。 |
| キーワードを大文字に | 真偽値 | オン | キーワードのみです。識別子・文字列・コメントは元の大文字小文字のまま維持されます。データベースによっては、識別子の大文字小文字を変えると指すオブジェクトが変わるためです。 |
| 1行に1列 | 真偽値 | オン | SELECTした列を1行に1つ並べます。diffを見たとき、SELECTの行が変わったのではなく、どの列が変わったのかが分かります。 |
| カンマを行頭に | 真偽値 | オフ | 列の後ろではなく前にカンマを置きます。賛否が分かれるスタイルですが、確かに並べ替えはしやすくなります。末尾に列を追加しても前の行に手を入れる必要がありません。 |
| 文字列リテラル | 維持 | バイト単位で同一 | 文字列内の空白も含めて一切手を加えません。文字列はデータであり、データの再フォーマットはフォーマットではありません。 |
| 引用符付きの識別子 | 維持 | — | 大文字小文字を区別するデータベースでは、"User"はUserではありません。引用符は決して追加も削除もされません。 |
| ブロックコメント | ネスト可 | — | 標準SQLではCと違ってブロックコメントを入れ子にできます。/* a /* b */ c */は1つのコメントで、最初の*/で打ち切るトークナイザーは、その後のクエリを誤って解釈します。 |
| ミニファイ | 1行 | — | 空白を詰めて1行にし、行コメントをブロックコメントに変換します。1行の中の--コメントは、その後ろすべてをコメントアウトしてしまうためです。 |
よくある質問
サンプルクエリに'FROM cache'が入っているのはなぜですか?
トークン化するフォーマッターと正規表現ベースのフォーマッターの違いを見せるためです。パターンでキーワードを探すフォーマッターは、文字列リテラルの中のFROMまでキーワードと見なし、その真ん中に改行を挿入して、もはやパースできないクエリを作り出します。先にトークン化するということは、文字列は中身が何であれ文字列のままだということです。
テーブル名やカラム名が変わることはありますか?
いいえ。大文字小文字が整えられるのはキーワードトークンだけです。引用符付きの識別子は、引用符を含めてバイト単位でそのまま通ります。これは聞こえ以上に重要です。Postgresでは"User"とUserは別のテーブルを指すので、引用符を取り去るフォーマッターは、見た目ではなくクエリの意味を変えてしまうのです。
どの方言を選べばよいですか?
実際に使っているデータベースのものです。この設定はスタイルではなく、パースに関わるからです。MySQLは識別子にバッククォートを使い、文字列内のバックスラッシュエスケープも有効にします。T-SQLは角括弧を使い、Postgresにはドル引用符があります。間違えた場合、文字列や識別子が誤った位置で終わり、それ以降のすべてが誤分類されます。
行頭カンマ——なぜわざわざ?
リストの末尾のカラムを追加・削除するとき、変更が1行で済むからです。行末カンマでは、カラムを追加するたびに前の行へカンマを足す編集が要り、diffには2行の変更が現れ、レビュアーは両方を確認しなければなりません。違和感は1日もすれば消え、その後は気にならなくなります。
クエリを最小化すると速くなりますか?
なりません。データベースは実行計画を立てる前にクエリをパースし、その時点で空白は捨てられます。最小化したクエリも整形したクエリも、生成される実行計画は同一です。最小化はクエリを1行に収めるための機能です。ログや設定ファイル、シェルコマンドに埋め込むときに使うものであって、性能のためではありません。
入力したクエリはアップロードされますか?
いいえ。トークン化も整形も、すでに開いているタブの中で動作します。ここでそれが重要なのは、クエリにはどこにも貼り付けたくない情報が含まれがちだからです。ビジネスの実態を表すテーブル名やカラム名、そして実在の顧客データであることの多いリテラル値です。