📘

Dataform で SQLFluff が使えるプラグインを作った

2024/09/18に公開2

モチベーション

SQLFluff を使って dbt を開発すると SQL が決められたルールに保たれて可読性や保守性が増します。
ただ SQLFluff は Dataform(SQLX)に対応してなく愚直に適応しようとするとエラーとなってしまいます。

dbt で SQLFluff が使えているのは plugin として実装されているからであり Dataform も実装すればイケるのでは?という期待を持っていました。
以下がそのプラグインである sqlfluff-templater-dbt になります。

https://pypi.org/project/sqlfluff-templater-dbt/

sqlfluff-templater-dataform の紹介

そこで作ったのが sqlfluff-templater-dataform になります。

https://pypi.org/project/sqlfluff-templater-dataform/

試しに使ってみると以下のようにルールに即していない箇所を指摘してくれます。

> cat definitions/test.sqlx
config {
    type: "table",
    schema: "schama",
    tags: [
        "test"
    ],
    description: "test sqlx file for lint by sqlfluff-templater-dataform.",
    columns: {
        user_id: "ユーザーID",
        name: "ユーザー名",
        age: "年齢",
        order_date: "注文日",
        order_count: "注文回数"
    }
}

wItH users as (
select user_id,
    name,
        age
    from ${ref('users')}
)
, user_orders AS (
    select user_id, order_id, DATE(created_at) AS order_date from ${ref('user_orders')}
)
SELECt users.user_id, name, age, order_date, count(order_id) AS order_count
from users left outer join user_orders on users.user_id = user_orders.user_id
group by user_id, name, age, order_date
> sqlfluff lint
== [definitions/test.sqlx] FAIL                                                
L:  17 | P:   1 | CP01 | Keywords must be upper case.
                       | [capitalisation.keywords]
L:  17 | P:  12 | CP01 | Keywords must be upper case.
                       | [capitalisation.keywords]
L:  18 | P:   1 | CP01 | Keywords must be upper case.
                       | [capitalisation.keywords]
L:  18 | P:   1 | LT02 | Expected indent of 4 spaces. [layout.indent]
L:  18 | P:   1 | LT09 | Select targets should be on a new line unless there is
                       | only one select target. [layout.select_targets]
...
All Finished 📜 🎉!

できることできないこと

できることとしては SQLFluff の機能を Dataform(SQLX)に適応することができます。
普段と同じく .sqlfluff を用意してそこに設定や矯正したいルールや記述します。
Google Cloud と接続することはないですが、${ref()} のコンパイルの都合上 project_iddataset_id をセットする必要があります。

[sqlfluff]
templater = dataform
dialect = bigquery
sql_file_exts = .sqlx

[sqlfluff:templater:dataform]
project_id = project_id
dataset_id = dataform

[sqlfluff:rules:capitalisation.keywords]
# 予約語の大文字小文字
capitalisation_policy = upper

できないことは Javascript の解釈です。
Dataform には js{} ブロックを用いて JavaScript を実行してその結果を SQL に埋め込むことができます。
今回 SQLFluff が Python の実装なので一旦非対応としました。
js{} が含まれる場合そのファイルはスキップされます。

実装に関して

sqlfluff-templater-dbt をデバッグしていくと DbtTemplaterprocess() が毎回呼ばれていることがわかります。
こちらを参考に DataformTemplater というクラスを作り RawTemplater を継承(dbt の場合 JinjaTemplater を継承)し process メソッドを実装することにしました。
sqlfluff-templater-dbtprocess メソッドを読むと SQL をコンパイルしてテンプレート部分({{ config() }} など)の開始位置と終了位置を把握していました。

https://github.com/sqlfluff/sqlfluff/tree/main/plugins/sqlfluff-templater-dbt/sqlfluff_templater_dbt#L421

dbt の場合、コンパイルには dbt の Python パッケージを使用し、テンプレート部分の抽出には JinjaTemplater が使われます。
一方、Dataform にはこのような Python パッケージやテンプレートのパーサーがないため、自作する必要がありました。

実装としては、${ref()} の部分は .sqlfluff で指定された project_iddataset_id を使ってテーブル名を再現し、テンプレート部分(Dataform の場合は config{} など)は正規表現で取り除く形にしています。

さいごに

念願だった SQLFluff の Dataform 適応ができるようになりました!

ただし、JavaScript を使った SQLX の lint には対応していないという課題も残っています。
私が関わっているプロジェクトでは幸い JavaScript の使用頻度が低いですが、JavaScript を多用している場合には不便かもしれません。これを改善するのが今後の目標です。(JavaScript と SQL の責務が曖昧になるため、使用を避けているという側面もあります。)

その他、使いにくいところや機能要望がありましたら GitHub Issue の方でお願いします!
ぜひコントリビュートもお待ちしています🙏

https://github.com/hiracky16/sqlfluff-templater-dataform

Discussion

ゆずたそゆずたそ

sqlfluff-terraform-dataform の紹介
そこで作ったのが sqlfluff-terraform-dataform になります。

terraformtemplater のTypoかもしれません!