👏

SQLFluffのカスタムルールの開発

2024/07/23に公開

バックエンドエンジニアの山下です。
Sprocketではソフトウェアの品質向上のため、Lintツールを利用しています。Lintツールとはソースコードを静的解析し、コードの構文やスタイル、潜在的な問題を検出するツールです。JavaScript・TypeScript向けのESLint,Go言語向けのgolangci-lint,IaC化ツールTerraform向けのTFLintなどをプロダクトに導入しています。
今回はSQL向けのLintツールであるSQLFluffのカスタムルールを開発しましたので、そちらを紹介します。

SQLFluffの紹介

SQLFluffはSQL向けのLintツールで、SQLファイルの問題点の指摘や自動修正ができるPython製のツールです。MySQLやPostgreSQLといったRDBMSはもちろん、BigQueryやApache Sparkといった、大規模データを扱うソフトウェアにも対応しています。
弊社ではCIにSQLFluffを組み込み、チェックを通すことで品質を担保しています。

カスタムルールの開発に至った経緯

弊社で機能追加の開発を行なっていた際に、テーブルのカラム追加が必要となり以下のようなSQLを作成しました。

ALTER TABLE sample_table ADD COLUMN sample_column TEXT NOT NULL; 

sample_tableテーブルにsample_columnカラムを追加するというシンプルなSQLに見えますが、これをマイグレートした際に失敗しました。
こちらの失敗の原因ですが、追加したsample_columnカラムにはNOT NULL制約がついているにも関わらず、デフォルト値を設定していないためです。
ADD COLUMNでデフォルト値を設定しない場合はNULLが入るため、NOT NULL制約違反となってしまいました。

先ほどマイグレートに失敗したと書きましたが、これは元からデータが入っていたステージング環境へデプロイした際に起きた問題です。
開発を進めるローカル環境ではデータが入っていなかった(レコードが0件だった)ため、マイグレートは正常に完了しました。
マイグレートを行う前段階でこの問題に気付きづらいのが課題だと感じました。

そこで既に導入しているSQLFluffのチェックで検知できないか、と考えました。
はじめに既存のルールを確認したのですが、それらしい項目はありませんでした。
そもそもSQLFluffはコードのフォーマットに関するルールが多く、ロジックに言及しているものは少ないようです。
またissueにも同様の要望はなかったため、カスタムルールを実装することとしました。

カスタムルールの開発

プラグインの開発

SQLFluffではPluginとしてカスタムルールが追加できると公式で紹介されています。

今回はこちらの手順と、公式が用意しているプラグインのサンプルを参考に開発を進めました。

SetupToolsを利用してパッケージ化し、プラグインとして利用できる仕組みとなっています。

SQLの解析

ルールを作成するためにはSQLがどのような構文で成り立っているかを解析する必要があります。
SQLFluffではSQLの構文を解析しており、カスタムルールの開発にも利用できます。
ただし、解析結果に関する詳細なドキュメントが見当たらず、実際にSQLファイルを解析して出てきた結果から解釈しました。

今回解析しようとしているALTER文の解析結果は以下のようになります。
解析結果がRuleContextという型で渡され、内部は複数・階層構造のSegmentとして構成されているようです。
解析結果
※今回のルールの開発に不要な情報は一部割愛しています。また、独自解釈をしておりますので正確でない可能性があります。

実装方針

今回は以下の条件を全て満たしている際にエラーとすることにしました。

  • ALTER TABLE文であること
  • ADD (COLUMN)のSQLであること
  • NOT NULL制約をつけていること
  • DEFAULT値を設定していないこと、またはDEFAULT値がNULLになっていること

このうち一番上のALTER TABLEであることはAlterTableActionSegmentが存在するかどうかで判断します。
それ以外の3つはKeywordSegmentにより判定することとしました。
はじめはColumnConstraintSegmentを活用した方がきれいにコーディングできるかと考えました。
しかしNOT NULL制約とDEFAULT値の順番を入れ替えると解析結果が異なるといった安定しない挙動をとったため、単純にKeywordSegmentを活用することとしました。

実装

今回作成したコードはこちらになります。

has_notnull_constraint関数ではnotとnullの単語の連続の有無により、NOT NULL制約を持っているかを判定しています。
has_default_value_other_than_null関数ではdefaultの有無と次の単語がnullでないことを確認しています。
これにより、NULL以外のデフォルト値を持っているかを判定しています。
_eval関数内でLintResultを返すことで、エラーを返すことができます。

実行結果

以下のようなテストファイルを用意して実行してみました。

  • テストファイル
-- has not-null constraint and default value is not set (fail)
alter table tbl1
add column col1 TEXT not null;

-- has not-null constraint and default value is set (pass)
alter table tbl2
add column col1 TEXT not null default '';

-- not-null constraint and default value specification are reversed (pass)
alter table tbl3
add column col1 TEXT default ''
not null;

-- has not-null constraint and default value is null (fail)
alter table tbl4
add column col1 TEXT not null default null;

-- adding multiple Columns (fail:col1,col3)
alter table tbl5
add column col1 TEXT not null,
add column col2 TEXT default '',
add column col3 TEXT not null;

-- create table (pass)
create table tbl6 (
    col1 TEXT not null,
    col2 TEXT default '' not null
);
  • 実行コマンド
pip install sqlfluff
pip install git+https://github.com/hiroto3432/sqlfluff-plugin-custom.git
sqlfluff lint {file_path}
  • 実行結果
== [sql/test.sql] FAIL                                
L:   3 | P:   1 | Custom_L001 | Set a non-null default value for columns with a NOT NULL
                       | constraint.
L:  16 | P:   1 | Custom_L001 | Set a non-null default value for columns with a NOT NULL
                       | constraint.
L:  20 | P:   1 | Custom_L001 | Set a non-null default value for columns with a NOT NULL
                       | constraint.
L:  22 | P:   1 | Custom_L001 | Set a non-null default value for columns with a NOT NULL
                       | constraint.
All Finished 📜 🎉!

想定通りの結果になっていることが確認できます。

まとめ、感想

SQLFluffのカスタムルールを開発できました。
苦戦した点はSQLFluffでどのように解析されるか把握する点です。
動的型付け言語のPython製ということで型の把握が簡単でなかった点も、より難しくなったと感じた一因かと考えられます。

また、今回初めてPythonのプロジェクトのプラグインを開発したのですが、Setuptoolsでパッケージ化することにより簡単に外部利用できることを知りました。
パッケージの配布も容易で、良い仕組みだと感じました。

Sprocketの勉強会について

今回の記事は社内LT(ライトニングトーク)会で発表した内容を記事にまとめたものとなります。
Sprocketのエンジニアチームでは、月に1度程度社内LT会を行なっています。
チーム全体へのナレッジの共有やエンジニアのスキル向上を目的としており、業務時間中に実施しています。
発表内容としては、フロントエンド・バックエンド・インフラ・チームマネジメントなど多岐にわたります。

また社内LT会の他にも、輪読会や外部カンファレンスへの参加などスキルアップを目的とした活動を積極的に行なっております。

Sprocketで働きませんか?

弊社ではカジュアル面談を実施しております。
ご興味を持たれましたら、こちらからご応募お待ちしております。

参考文献

Sprocketテックブログ

Discussion