【SQL】SQL Linter ~ SQLFluff / Custom rule ~

■ はじめに

https://dk521123.hatenablog.com/entry/2024/02/28/225002
https://dk521123.hatenablog.com/entry/2024/03/04/180308

の続き。

業務において、SQLファイルを独自のルールでチェックをする必要がありそうなので
SQLFluff の Custom rule (カスタムルール) について、とりあげる。

目次

【1】前提知識
 1)Pythonのパッケージ作成および配布
【2】SQLFluffプラグイン作成上での注意点
 1)プラグイン名は「sqlfluff-」で始めること
 2)プラグインルールクラス名は「Rule_PluginName_L000」
【3】サンプル
 例1:Hello world
【4】実行例
【5】実装を拡張するために
 1)ルールを追加したい

【1】前提知識

1)Pythonのパッケージ作成および配布

* 以下の関連記事を参照のこと

パッケージ配布 ~ pyproject.tomlによる作成方法 ~
https://dk521123.hatenablog.com/entry/2024/03/28/000000
パッケージ配布 ~ setup.pyによる作成方法 ~
https://dk521123.hatenablog.com/entry/2024/03/19/000000

【2】SQLFluffプラグイン作成上での注意点

* Custom rule 

1)プラグイン名は「sqlfluff-」で始めること

https://docs.sqlfluff.com/en/stable/developingplugins.html#few-things-to-note-about-plugins

より抜粋
~~~~~~~~~~
We recommend that the name of a plugin should start with “sqlfluff-”
 to be clear on the purpose of your plugin.
~~~~~~~~~~

2)プラグインルールクラス名は「Rule_PluginName_L000」

* 「PluginName」が使われるので、プラグイン名はあまり長くない方がいいかも、、、

https://docs.sqlfluff.com/en/stable/developingplugins.html#few-things-to-note-about-plugins

より抜粋
~~~~~~~~~~
A plugin Rule class name should have the structure: “Rule_PluginName_L000”. 
# プラグインルールクラス名は、「Rule_PluginName_L000」が良いだろう

The ‘L’ can be any letter and is meant to categorize rules;
#「L」はどんな文字でも可能で、独自ルールということを意図している

 you could use the letter ‘S’ to denote rules
 that enforce security checks for example.
# ひょっとしたら「S」は、例えば、
# セキュリティチェックで強制するってルールとして使えるかも、、、
~~~~~~~~~~
cf. meant to (mean to) = ~する意向・意図がある
cf. denote = 示す

【3】サンプル

例1:Hello world

* やっとできた。。。
 => 詳しくは、以下のGithub Repository参照

https://github.com/dk521123/sqlfluff-plugin-demo

フォルダ構成(重要なファイルのみ)

+ sqlfluff-plugin-demo
    + src/sqlfluff_plugin_demo
         + __init__.py
         + plugin_config.cfg
         + rule_custom_demo_l001.py
    + MANIFEST.in
    + setup.py

setup.py

"""Setup file for an example rules plugin."""

from setuptools import find_packages, setup

PLUGIN_LOGICAL_NAME = "demo"
PLUGIN_ROOT_MODULE = "sqlfluff_plugin_demo"

setup(
  name=f"sqlfluff-plugin-{PLUGIN_LOGICAL_NAME}",
  url="https://dk521123.hatenablog.com/entry/2024/03/31/232907",
  license="MIT",
  description="This is just a sample",
  version="0.0.1",
  include_package_data=True,
  package_dir={"": "src"},
  packages=find_packages(where="src"),
  install_requires="sqlfluff>=0.4.0",
  entry_points={
    "sqlfluff": [
      f"sqlfluff_{PLUGIN_LOGICAL_NAME} = {PLUGIN_ROOT_MODULE}"
    ]
  },
)

MANIFEST.in

include src/sqlfluff_plugin_demo/plugin_config.cfg

src/sqlfluff_plugin_demo/init.py

"""An example of a custom rule implemented through the plugin system.
This uses the rules API supported from 0.4.0 onwards.
"""

from typing import List, Type

from sqlfluff.core.config import ConfigLoader
from sqlfluff.core.plugin import hookimpl
from sqlfluff.core.rules import BaseRule

@hookimpl
def get_rules() -> List[Type[BaseRule]]:
  """Get plugin rules.

  NOTE: It is much better that we only import the rule on demand.
  The root module of the plugin (i.e. this file which contains
  all of the hook implementations) should have fully loaded before
  we try and import the rules. This is partly for performance
  reasons - but more because the definition of a BaseRule requires
  that all of the get_configs_info() methods have both been
  defined _and have run_ before so all the validation information
  is available for the validation steps in the meta class.
  """
  # i.e. we DO recommend importing here:
  from sqlfluff_plugin_demo.rule_custom_demo_l001 import Rule_Demo_L001
  return [Rule_Demo_L001]


@hookimpl
def load_default_config() -> dict:
  """Loads the default configuration for the plugin."""
  return ConfigLoader.get_global().load_config_resource(
    package="sqlfluff_plugin_demo",
    file_name="plugin_config.cfg",
  )


@hookimpl
def get_configs_info() -> dict:
  """Get rule config validations and descriptions."""
  return {
    "forbidden_columns": {"definition": "A list of column to forbid"},
  }

src/sqlfluff_plugin_demo/rule_custom_demo_l001.py

"""An example of a custom rule implemented through the plugin system.

This uses the rules API supported from 0.4.0 onwards.
"""
from sqlfluff.core.rules import (
  BaseRule,
  LintResult,
  RuleContext,
)
from sqlfluff.core.rules.crawlers import SegmentSeekerCrawler

# These two decorators allow plugins
# to be displayed in the sqlfluff docs
# https://docs.sqlfluff.com/en/3.0.3/developingrules.html#sqlfluff.core.rules.base.BaseRule  
class Rule_Demo_L001(BaseRule):
  """ORDER BY on these columns is forbidden!

  **Anti-pattern**

  Using ``ORDER BY`` one some forbidden columns.

  .. code-block:: sql

      SELECT *
      FROM foo
      ORDER BY
          bar,
          baz

  **Best practice**

  Do not order by these columns.

  .. code-block:: sql

      SELECT *
      FROM foo
      ORDER BY bar
  """

  groups = ("all",)
  config_keywords = ["forbidden_columns"]
  crawl_behaviour = SegmentSeekerCrawler({"orderby_clause"})
  is_fix_compatible = True

  def __init__(self, *args, **kwargs):
    """Overwrite __init__ to set config."""
    super().__init__(*args, **kwargs)
    self.forbidden_columns = [
      col.strip() for col in self.forbidden_columns.split(",")
    ]

  def _eval(self, context: RuleContext):
    """We should not ORDER BY forbidden_columns."""
    for seg in context.segment.segments:
      col_name = seg.raw.lower()
      if col_name in self.forbidden_columns:
        # https://docs.sqlfluff.com/en/3.0.3/developingrules.html#sqlfluff.core.rules.base.LintResult  
        return LintResult(
          anchor=seg,
          description=f"Column `{col_name}` not allowed in ORDER BY.",
        )

src/sqlfluff_plugin_demo/plugin_config.cfg

[sqlfluff:rules:Demo_L001]
forbidden_columns = bar, baaz

test.sql

-- NG
SELECT person_name FROM cte ORDER BY bar, baz;

-- NG
SELECT person_name FROM cte ORDER BY bar;

-- OK
SELECT person_name FROM cte ORDER BY person_id, person_name;

実行結果

$ sqlfluff lint --dialect snowflake test.sql
== [test.sql] FAIL
L:   2 | P:  38 | Demo_L001 | Column `bar` not allowed in ORDER BY.
L:   5 | P:  38 | Demo_L001 | Column `bar` not allowed in ORDER BY.
All Finished � �!

【4】実行例

# Install SQLFluff
pip install sqlfluff

# Install your SQLFluff plugin by pip install git+<github_url>

# 確認
pip list | grep sqlfluff

# sqlfluff lint --dialect <your-target-db><target_sql>
sqlfluff lint --dialect snowflake test.sql

1)アンインストールする場合

# pip unstall --yes <PluginName (e.g. sqlfluff-plugin-custom-rule-demo)>
pip unstall --yes sqlfluff-plugin-custom-rule-demo

【5】実装を拡張するために

* ベースは上記「サンプル」にするとして、
 これを拡張するための技術的メモを残しておく

1)ルールを追加したい

* 以下が必要。

[1] src/sqlfluff_plugin_demo 配下にルールクラスを追加
[2] __init__.py にルールクラスを追記

[1] src/sqlfluff_plugin_demo 配下にルールクラスを追加

* src/sqlfluff_plugin_demo 配下に
 BaseRuleを継承したルールクラスを追加
 => 例えば、src/sqlfluff_plugin_demo/rule_custom_demo_l002.py

[2] init.py にルールクラスを追記

@hookimpl
def get_rules() -> List[Type[BaseRule]]:
  from sqlfluff_plugin_demo.rule_custom_demo_l001 import Rule_Demo_L001
  # return [Rule_Demo_L001]
  from sqlfluff_plugin_demo.rule_custom_demo_l002 import Rule_Demo_L002 # ADD
  return [Rule_Demo_L001, Rule_Demo_L002] # ADD

参考文献

公式サイト
https://docs.sqlfluff.com/en/stable/developingplugins.html
https://github.com/sqlfluff/sqlfluff/tree/main/plugins/sqlfluff-plugin-example
Others
SQLFluffのカスタムルールの開発. バックエンドエンジニアの山下です。… | by yamashita | sprocket-inc | Medium
https://github.com/hiroto3432/sqlfluff-plugin-custom

上記を試してに実行してみると、以下の警告がでる。
参考にする場合は、注意した方がいいかも。
====
Rule 'Rule_Custom_L001' has been imported
 before all plugins have been fully loaded. 
For best performance, plugins should import any rule definitions
 within their `get_rules()` method.
Please update your plugin to remove this warning.
See: https://docs.sqlfluff.com/en/stable/developingplugins.html

Rule_Custom_L001 uses the @document_configuration decorator
 which is deprecated in SQLFluff 2.0.0.
Remove the decorator to resolve this warning.

Rule_Custom_L001 uses the @document_fix_compatible decorator
 which is deprecated in SQLFluff 2.0.0.
Remove the decorator to resolve this warning.

Rule_Custom_L001 uses the @document_groups decorator
 which is deprecated in SQLFluff 2.0.0.
Remove the decorator to resolve this warning.

関連記事

パッケージ配布 ~ pyproject.tomlによる作成方法 ~
https://dk521123.hatenablog.com/entry/2024/03/28/000000
パッケージ配布 ~ setup.pyによる作成方法 ~
https://dk521123.hatenablog.com/entry/2024/03/19/000000
Docker ~ 基本編 / Dockerfile ~
https://dk521123.hatenablog.com/entry/2020/04/14/000000