■ はじめに
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