Open12

【dbt】macros

YuichiYuichi

dbtのmacrosフォルダの構成

を考えるメモ

基本的なフォルダ構成

dbt_project/
│── macros/
│   ├── staging/
│   │   ├── get_source.sql
│   │   ├── clean_column_name.sql
│   │   └── generate_schema_name.sql
│   ├── transformations/
│   │   ├── calculate_metrics.sql
│   │   ├── format_date.sql
│   │   └── pivot_data.sql
│   ├── utilities/
│   │   ├── logging.sql
│   │   ├── string_helpers.sql
│   │   └── type_casting.sql
│   ├── constraints/
│   │   ├── check_null_values.sql
│   │   ├── enforce_unique_keys.sql
│   │   └── validate_referential_integrity.sql
│   ├── tests/
│   │   ├── assert_non_null.sql
│   │   ├── assert_unique.sql
│   │   └── assert_positive.sql
│   ├── jinja_helpers.sql
│   ├── dbt_project_utils.sql
│   └── README.md

フォルダの役割

  • staging/
    • ソーステーブルのクリーンアップやスキーマ名の自動生成など、データの準備に関連するマクロを格納
  • transformations/
    • 集計やデータ変換(ピボット、フォーマット変更など)を行うマクロを格納
  • utilities/
    • 文字列操作や型変換などの汎用的なヘルパーマクロを格納
  • constraints/
    • データ品質チェックのためのマクロを格納(ユニークキーの検証、NULLチェックなど)
  • tests/
    • カスタムテスト(assert_non_null など)を定義するマクロを格納
  • jinja_helpers.sql
    • Jinjaの補助的な関数を定義(例えば、リスト操作や条件分岐)
  • dbt_project_utils.sql
    • プロジェクト全体で利用するユーティリティマクロを格納
  • README.md
    • マクロの使い方や管理ルールを記載
YuichiYuichi
import requests

def get_github_tree(repo, path, indent=0, file=None):
    """ 指定した GitHub リポジトリ内のフォルダ以下のツリー構造を取得し、ファイルに出力 """
    url = f"https://api.github.com/repos/{repo}/contents/{path}"
    response = requests.get(url)

    if response.status_code == 200:
        items = response.json()
        for item in items:
            line = " " * indent + "|-- " + item["name"]  # ツリー形式の文字列
            print(line)
            file.write(line + "\n")  # ファイルに書き込み

            if item["type"] == "dir":  # フォルダなら再帰的に取得
                get_github_tree(repo, item["path"], indent + 2, file)
    else:
        print(f"Failed to fetch {path}: {response.status_code}")

# リポジトリとディレクトリの指定
repo = "calogica/dbt-expectations"
path = "macros"

# ツリー構造を取得してファイルに保存
with open("tree.txt", "w", encoding="utf-8") as f:
    f.write(f"{repo}/{path}/\n")
    get_github_tree(repo, path, file=f)

print("ツリー構造を tree.txt に保存しました。")
YuichiYuichi

https://github.com/calogica/dbt-expectations/tree/main/macros

calogica/dbt-expectations/macros/
|-- .gitkeep
|-- math
  |-- log_natural.sql
  |-- median.sql
  |-- percentile_cont.sql
  |-- rand.sql
|-- regex
  |-- regexp_instr.sql
|-- schema_tests
  |-- _generalized
    |-- _ignore_row_if_expression.sql
    |-- _truth_expression.sql
    |-- equal_expression.sql
    |-- expression_between.sql
    |-- expression_is_true.sql
  |-- aggregate_functions
    |-- expect_column_distinct_count_to_be_greater_than.sql
    |-- expect_column_distinct_count_to_be_less_than.sql
    |-- expect_column_distinct_count_to_equal.sql
    |-- expect_column_distinct_count_to_equal_other_table.sql
    |-- expect_column_distinct_values_to_be_in_set.sql
    |-- expect_column_distinct_values_to_contain_set.sql
    |-- expect_column_distinct_values_to_equal_set.sql
    |-- expect_column_max_to_be_between.sql
    |-- expect_column_mean_to_be_between.sql
    |-- expect_column_median_to_be_between.sql
    |-- expect_column_min_to_be_between.sql
    |-- expect_column_most_common_value_to_be_in_set.sql
    |-- expect_column_proportion_of_unique_values_to_be_between.sql
    |-- expect_column_quantile_values_to_be_between.sql
    |-- expect_column_stdev_to_be_between.sql
    |-- expect_column_sum_to_be_between.sql
    |-- expect_column_unique_value_count_to_be_between.sql
  |-- column_values_basic
    |-- expect_column_values_to_be_between.sql
    |-- expect_column_values_to_be_decreasing.sql
    |-- expect_column_values_to_be_in_set.sql
    |-- expect_column_values_to_be_in_type_list.sql
    |-- expect_column_values_to_be_increasing.sql
    |-- expect_column_values_to_be_null.sql
    |-- expect_column_values_to_be_of_type.sql
    |-- expect_column_values_to_be_unique.sql
    |-- expect_column_values_to_have_consistent_casing.sql
    |-- expect_column_values_to_not_be_in_set.sql
    |-- expect_column_values_to_not_be_null.sql
  |-- distributional
    |-- expect_column_values_to_be_within_n_moving_stdevs.sql
    |-- expect_column_values_to_be_within_n_stdevs.sql
    |-- expect_row_values_to_have_data_for_every_n_datepart.sql
  |-- multi-column
    |-- expect_column_pair_values_A_to_be_greater_than_B.sql
    |-- expect_column_pair_values_to_be_equal.sql
    |-- expect_column_pair_values_to_be_in_set.sql
    |-- expect_compound_columns_to_be_unique.sql
    |-- expect_multicolumn_sum_to_equal.sql
    |-- expect_select_column_values_to_be_unique_within_record.sql
  |-- string_matching
    |-- _get_like_pattern_expression.sql
    |-- expect_column_value_lengths_to_be_between.sql
    |-- expect_column_value_lengths_to_equal.sql
    |-- expect_column_values_to_match_like_pattern.sql
    |-- expect_column_values_to_match_like_pattern_list.sql
    |-- expect_column_values_to_match_regex.sql
    |-- expect_column_values_to_match_regex_list.sql
    |-- expect_column_values_to_not_match_like_pattern.sql
    |-- expect_column_values_to_not_match_like_pattern_list.sql
    |-- expect_column_values_to_not_match_regex.sql
    |-- expect_column_values_to_not_match_regex_list.sql
  |-- table_shape
    |-- _get_column_list.sql
    |-- _list_intersect.sql
    |-- expect_column_to_exist.sql
    |-- expect_grouped_row_values_to_have_recent_data.sql
    |-- expect_row_values_to_have_recent_data.sql
    |-- expect_table_aggregation_to_equal_other_table.sql
    |-- expect_table_column_count_to_be_between.sql
    |-- expect_table_column_count_to_equal.sql
    |-- expect_table_column_count_to_equal_other_table.sql
    |-- expect_table_columns_to_contain_set.sql
    |-- expect_table_columns_to_match_ordered_list.sql
    |-- expect_table_columns_to_match_set.sql
    |-- expect_table_columns_to_not_contain_set.sql
    |-- expect_table_row_count_to_be_between.sql
    |-- expect_table_row_count_to_equal.sql
    |-- expect_table_row_count_to_equal_other_table.sql
    |-- expect_table_row_count_to_equal_other_table_times_factor.sql
|-- utils
  |-- datatypes.sql
  |-- groupby.sql
  |-- md5.sql

このフォルダ構成の特徴

  1. math/
    • 数学的な計算(median.sql, percentile_cont.sql など)
    • log_natural.sqlrand.sql など、統計解析向けのマクロも含まれる
  2. regex/
    • regexp_instr.sql など、文字列パターンマッチングのユーティリティがある
  3. schema_tests/
    • _generalized/
      • 内部で再利用される一般的なロジック( _truth_expression.sql など)
    • aggregate_functions/
      • 集計値の期待値テスト( expect_column_sum_to_be_between.sql など)
    • column_values_basic/
      • カラムごとの基本的な検証( expect_column_values_to_be_between.sql など)
    • distributional/
      • データの分布に関するテスト( expect_column_values_to_be_within_n_stdevs.sql など)
    • multi-column/
      • 複数カラム間の関係をチェック( expect_column_pair_values_to_be_equal.sql など)
    • string_matching/
      • 正規表現やLIKE演算子を使ったテスト( expect_column_values_to_match_regex.sql など)
    • table_shape/
      • テーブル全体の構造をチェック( expect_table_column_count_to_be_between.sql など)
  4. utils/
    • md5.sqldatatypes.sql など、基本的なデータ型変換やハッシュ関数を定義
    • groupby.sql など、汎用的な SQL 操作を補助するマクロを格納

この構成のメリット

汎用性が高い

  • math/regex/ で、データ処理に幅広く使えるマクロを管理
  • utils/ で、共通的な便利マクロを提供

データ品質チェック (schema_tests/) を細かく管理

  • aggregate_functions/table_shape/ など、テストの種類ごとにフォルダを分けている
  • _generalized/ で共通ロジックをまとめ、再利用性を高めている

スケールしやすい

  • 必要に応じて schema_tests/ のサブカテゴリを増やせる
  • math/utils/ など、個別の計算・変換処理をどんどん追加できる
YuichiYuichi

https://github.com/brooklyn-data/dbt_artifacts/tree/main/macros

brooklyn-data/dbt_artifacts/macros/
|-- _macros.yml
|-- database_specific_helpers
  |-- column_identifier.sql
  |-- generate_surrogate_key.sql
  |-- get_relation.sql
  |-- parse_json.sql
  |-- type_helpers.sql
|-- migration
  |-- migrate_from_v0_to_v1.sql
|-- upload_individual_datasets
  |-- upload_exposures.sql
  |-- upload_invocations.sql
  |-- upload_model_executions.sql
  |-- upload_models.sql
  |-- upload_seed_executions.sql
  |-- upload_seeds.sql
  |-- upload_snapshot_executions.sql
  |-- upload_snapshots.sql
  |-- upload_sources.sql
  |-- upload_test_executions.sql
  |-- upload_tests.sql
|-- upload_results
  |-- get_column_name_lists.sql
  |-- get_dataset_content.sql
  |-- get_table_content_values.sql
  |-- insert_into_metadata_table.sql
  |-- upload_results.sql

このフォルダ構成の特徴

  1. database_specific_helpers/
    • column_identifier.sql, generate_surrogate_key.sql など、データベースごとの互換性や識別キー生成を補助
    • get_relation.sqlparse_json.sql など、リレーション管理や JSON データの処理
  2. migration/
    • migrate_from_v0_to_v1.sql という特定バージョンの移行スクリプトを管理
  3. upload_individual_datasets/
    • upload_exposures.sql, upload_models.sql, upload_tests.sql など、dbt の各種アーティファクト(exposures, models, tests など)のアップロード処理
    • upload_seed_executions.sqlupload_snapshots.sql など、実行履歴の管理 も含まれる
  4. upload_results/
    • get_column_name_lists.sql, get_dataset_content.sql など、テーブルやデータセットの情報を取得
    • insert_into_metadata_table.sql でメタデータを一括登録
    • upload_results.sql で dbt の実行結果をまとめてアップロード

この構成のメリット

dbt のアーティファクト管理に特化

  • upload_individual_datasets/upload_results/dbt のメタデータ(モデル、スナップショット、テストなど)を効率的に整理
  • database_specific_helpers/ でデータベースごとの処理を統一

メタデータの移行 (migration/) も考慮

  • migrate_from_v0_to_v1.sql で、スキーマのバージョン変更や互換性の維持をサポート

データベース依存を考慮

  • database_specific_helpers/ 内で type_helpers.sql などを定義し、データ型や識別子の処理を標準化
  • parse_json.sql でデータベースに依存しない JSON 処理を提供
YuichiYuichi

https://github.com/dbt-labs/dbt-codegen/tree/main/macros

dbt-labs/dbt-codegen/macros/
|-- create_base_models.sql
|-- generate_base_model.sql
|-- generate_model_import_ctes.sql
|-- generate_model_yaml.sql
|-- generate_source.sql
|-- helpers
  |-- helpers.sql
|-- vendored
  |-- dbt_core
    |-- format_column.sql

フォルダ構成の特徴

database_specific_helpers/

  • column_identifier.sql, generate_surrogate_key.sql: データベースごとの互換性や識別キー生成を補助するためのSQL。異なるデータベースに対応するロジックを統一。
  • get_relation.sql, parse_json.sql: リレーション管理やJSONデータの処理をサポート。データベースに依存しない形で共通の処理を提供。

migration/

  • migrate_from_v0_to_v1.sql: 特定のバージョン間での移行を管理するスクリプト。スキーマのバージョン変更や互換性の維持を支援。

upload_individual_datasets/

  • upload_exposures.sql, upload_models.sql, upload_tests.sql: dbtの各種アーティファクト(exposures, models, testsなど)のアップロード処理を担当。
  • upload_seed_executions.sql, upload_snapshots.sql: 実行履歴の管理を含み、データベース内の状態を追跡。

upload_results/

  • get_column_name_lists.sql, get_dataset_content.sql: テーブルやデータセットの情報を取得するためのSQL。dbtのメタデータを整理し、参照する。
  • insert_into_metadata_table.sql: メタデータを一括登録するためのSQL。テーブルに対する情報を効率的にまとめてアップロード。
  • upload_results.sql: dbtの実行結果をまとめてアップロードするためのメインSQL。

この構成のメリット

dbtのアーティファクト管理に特化

  • upload_individual_datasets/upload_results/ フォルダを使って、dbtのメタデータ(モデル、スナップショット、テストなど)を効率的に整理できる。
  • データベース内のdbtのオブジェクト(テーブル、ビュー、スナップショット)の一貫した管理を行える。

メタデータの移行 (migration/) も考慮

  • migrate_from_v0_to_v1.sql で、スキーマのバージョン変更や互換性を保ちながら、移行をサポート。プロジェクトが進化する際にスムーズに変更を加えられる。

データベース依存を考慮

  • database_specific_helpers/ 内で、データ型や識別子の処理を標準化するため、異なるデータベース環境においても共通の処理が提供される。
  • parse_json.sql のような汎用的な処理を提供することで、データベースの種類に依存しない形でJSONデータを扱える。
YuichiYuichi

https://github.com/dbt-labs/dbt-external-tables/tree/main/macros

dbt-labs/dbt-external-tables/macros/
|-- common
  |-- create_external_schema.sql
  |-- create_external_table.sql
  |-- get_external_build_plan.sql
  |-- helpers
    |-- dropif.sql
    |-- transaction.sql
  |-- refresh_external_table.sql
  |-- stage_external_sources.sql
  |-- update_external_table_columns.sql
|-- plugins
  |-- bigquery
    |-- create_external_schema.sql
    |-- create_external_table.sql
    |-- get_external_build_plan.sql
    |-- update_external_table_columns.sql
  |-- fabric
    |-- create_external_schema.sql
    |-- create_external_table.sql
    |-- get_external_build_plan.sql
    |-- helpers
      |-- dropif.sql
  |-- redshift
    |-- create_external_table.sql
    |-- get_external_build_plan.sql
    |-- helpers
      |-- add_partitions.sql
      |-- dropif.sql
      |-- is_ext_tbl.sql
      |-- paths.sql
      |-- render_macro.sql
      |-- transaction.sql
    |-- refresh_external_table.sql
  |-- snowflake
    |-- create_external_schema.sql
    |-- create_external_table.sql
    |-- get_external_build_plan.sql
    |-- helpers
      |-- is_csv.sql
    |-- refresh_external_table.sql
    |-- snowpipe
      |-- create_empty_table.sql
      |-- create_snowpipe.sql
      |-- get_copy_sql.sql
      |-- refresh_snowpipe.sql
  |-- spark
    |-- create_external_table.sql
    |-- get_external_build_plan.sql
    |-- helpers
      |-- dropif.sql
      |-- recover_partitions.sql
    |-- refresh_external_table.sql

フォルダ構成の特徴

common/ (共通処理)

  • create_external_schema.sql: 外部スキーマを作成するためのSQL。

  • create_external_table.sql: 外部テーブルを作成するためのSQL。

  • get_external_build_plan.sql: 外部テーブルのビルドプランを取得するSQL。

  • helpers/

    :

    • dropif.sql: 外部テーブルやスキーマが存在する場合に削除するヘルパーマクロ。
    • transaction.sql: トランザクション関連のヘルパーマクロ。外部テーブルの操作をトランザクション内で安全に実行するために使用。
  • refresh_external_table.sql: 外部テーブルのリフレッシュ処理。

  • stage_external_sources.sql: 外部ソースデータをステージングするためのSQL。

  • update_external_table_columns.sql: 外部テーブルのカラム更新処理。

plugins/ (データベース固有のプラグイン)

データベースごとに異なる処理を管理しています。

  • bigquery/

    • BigQuery用の外部テーブルに関連するSQLマクロ。
    • create_external_schema.sql: BigQuery用の外部スキーマ作成。
    • create_external_table.sql: BigQuery用の外部テーブル作成。
    • get_external_build_plan.sql: BigQuery用のビルドプラン取得。
    • update_external_table_columns.sql: BigQuery用のカラム更新。
  • fabric/

    • Fabric用の外部テーブル関連SQL。

    • create_external_schema.sql, create_external_table.sql: Fabric用の外部スキーマ・テーブル作成。

    • get_external_build_plan.sql: Fabric用のビルドプラン取得。

    • helpers/

      :

      • dropif.sql: Fabric用の削除処理。
  • redshift/

    • Redshift用の外部テーブル関連SQL。

    • create_external_table.sql: Redshift用の外部テーブル作成。

    • get_external_build_plan.sql: Redshift用のビルドプラン取得。

    • helpers/

      :

      • add_partitions.sql: パーティションの追加を補助するSQL。
      • dropif.sql: Redshift用の削除処理。
      • is_ext_tbl.sql: 外部テーブルかどうかを確認するSQL。
      • paths.sql: パスに関連する処理を補助。
      • render_macro.sql: Redshift用のマクロをレンダリング。
      • transaction.sql: トランザクション関連の処理。
    • refresh_external_table.sql: 外部テーブルのリフレッシュ。

  • snowflake/

    • Snowflake用の外部テーブル関連SQL。

    • create_external_schema.sql, create_external_table.sql: Snowflake用の外部スキーマ・テーブル作成。

    • get_external_build_plan.sql: Snowflake用のビルドプラン取得。

    • helpers/

      :

      • is_csv.sql: CSVデータの識別に使用。
    • refresh_external_table.sql: 外部テーブルのリフレッシュ。

    • snowpipe/

      :

      • create_empty_table.sql: 空のテーブルを作成。
      • create_snowpipe.sql: Snowpipeを作成するSQL。
      • get_copy_sql.sql: Snowflakeにおけるコピー操作のSQL。
      • refresh_snowpipe.sql: Snowpipeのリフレッシュ。
  • spark/

    • Spark用の外部テーブル関連SQL。

    • create_external_table.sql: Spark用の外部テーブル作成。

    • get_external_build_plan.sql: Spark用のビルドプラン取得。

    • helpers/

      :

      • dropif.sql: Spark用の削除処理。
      • recover_partitions.sql: パーティションの回復処理。
    • refresh_external_table.sql: 外部テーブルのリフレッシュ。


この構成のメリット

データベース間での外部テーブル操作の統一

  • common/ フォルダ内のマクロは、複数のデータベースに共通する操作を提供し、外部テーブルの作成や更新、リフレッシュの処理を統一的に行うことができます。

データベース固有の拡張

  • 各データベースの plugins/ フォルダに特化した処理が定義されており、BigQuery、Redshift、Snowflake、Fabric、Sparkなど、異なるデータベースに合わせた最適な操作が提供されます。

外部テーブルの効率的な管理

  • 外部テーブルを作成する、更新する、リフレッシュする、または削除するための便利なマクロが用意されており、外部テーブルの管理が効率的に行えます。

スキーマやテーブル作成の標準化

  • 各データベースで共通のスキーマやテーブル作成のマクロが使えるため、異なるデータベース間での設定変更や作業が統一された方法で実行できます。
YuichiYuichi

https://dev.to/alexmercedcoder/a-guide-to-dbt-macros-purpose-benefits-and-usage-1n1p

dbt マクロのベストプラクティス

1. マクロはシンプルに保つ
各マクロは単一の明確なタスクを実行するように設計する。
一つのマクロに過度なロジックを詰め込むのではなく、小さく分割し、再利用しやすい形にする。

2. 分かりやすい命名規則を使用する
マクロの名前は、その役割が直感的に分かるようにする。
これにより、モデル内で使用した際に可読性が向上し、メンテナンスが容易になる。

3. 例外ケースを考慮する
マクロ内で NULL 値や予期しない入力を考慮し、どのようなデータにも対応できるようにする。
信頼性の高い処理を実装することで、エラーの発生を防ぐ。

4. テストでマクロを活用する
マクロを dbt のテスト内でも使用することで、再利用可能なテストロジックを構築し、
プロジェクト全体のデータ検証の一貫性を保つ。

5. マクロをドキュメント化する
マクロの目的、パラメータ、使用方法についてコメントやドキュメントを追加する。
特に複数のチームメンバーが関与するプロジェクトでは、ドキュメントが理解の助けとなる。

YuichiYuichi

https://gitlab.com/gitlab-data/analytics/-/tree/master/transform/snowflake-dbt/macros/bamboohr

macros.mdとmacros.ymlを生成するスクリプト
参考
https://github.com/Health-Union/dbt-xdb/blob/83652fcdf2dd7268acd51beaaa461e44ddcd09fc/test_xdb/scripts/macrodocs.py
https://docs.getdbt.com/docs/build/jinja-macros
https://docs.getdbt.com/reference/macro-properties

version: 2

macros:
  - name: <macro name>
    description: <markdown_string>
    docs:
      show: true | false
    meta: {<dictionary>}
    arguments:
      - name: <arg name>
        type: <string>
        description: <markdown_string>
      - ... # declare properties of additional arguments

  - name: ... # declare properties of additional macros

https://gitlab.com/gitlab-data/analytics/-/blob/master/transform/snowflake-dbt/macros/warehouse/macros.yml

version: 2

macros:
  - name: resume_warehouse
    description: '{{ doc("alter_warehouse") }}'
  - name: suspend_warehouse
    description: '{{ doc("alter_warehouse") }}'
  - name: backup_to_gcs
    description: '{{ doc("backup_to_gcs") }}'
  - name: get_backup_table_command
    description: '{{ doc("get_backup_table_command") }}'
  - name: grant_usage_to_schemas
    description: '{{ doc("grant_usage_to_schemas") }}'
  - name: gdpr_delete
    description: '{{ doc("gdpr_delete") }}'
    arguments:
      - name: email_sha
        type: string
        description: SHA256 of the email to be removed/redacted
      - name: run_queries
        type: boolean
        description: Flag used to run queries or just print them. Default is False which will only print queries to stdout.
  - name: gdpr_delete_gitlab_dotcom
    description: '{{ doc("gdpr_delete_gitlab_dotcom") }}'
    arguments:
      - name: email_sha
        type: string
        description: SHA256 of the email to be removed/redacted
      - name: run_queries
        type: boolean
        description: Flag used to run queries or just print them. Default is False which will only print queries to stdout.
  - name: gdpr_bulk_delete
    description: '{{ doc("gdpr_bulk_delete") }}'
YuichiYuichi
import os
import re
import yaml
import json


def main():
    """
    メイン関数。指定されたディレクトリのファイルを処理し、macros.md と macros.yml を更新する。
    """
    file_path = "~"
    file_list = get_files(file_path)  # 対象のフォルダを指定

    json_formatted_str = json.dumps(file_list, indent=4)
    # print(json_formatted_str)

    update_macros_md(file_list)
    update_macros_yml(file_list)


def parse_macros(file_path):
    """
    指定されたSQLファイルからマクロ定義を解析し、マクロ情報をリストとして返す。

    Args:
        file_path (str): 解析するSQLファイルのパス

    Returns:
        list: マクロ情報を含む辞書のリスト
    """
    # ファイル名を取得(拡張子なし)
    file_name = os.path.splitext(os.path.basename(file_path))[0]

    # マクロ情報を格納するリスト
    macros = []

    # ファイルを開いて内容を読み取る
    with open(file_path, "r", encoding="utf-8") as f:
        content = f.read()

    # マクロ定義を正規表現で抽出
    macro_pattern = r"{%-?\s*macro\s+(\w+)\((.*?)\)\s*-?%}"
    matches = re.findall(macro_pattern, content)

    for macro_name, args in matches:
        # 引数を分割してリスト化
        arguments = [{"name": convert_assignment_to_default(arg.strip())} for arg in args.split(",") if arg.strip()]

        # 各マクロ情報を辞書形式で追加
        macros.append({"name": macro_name, "description": f'{{{{ doc("{file_name}") }}}}', "arguments": arguments})

    return macros


def convert_assignment_to_default(text):
    """
    引数が `key = value` の形式の場合、`(key default: value)` の形式に変換する。

    Args:
        text (str): 変換する文字列

    Returns:
        str: 変換後の文字列
    """
    match = re.match(r"(\w+)\s*=\s*(.+)", text)
    if match:
        key, value = match.groups()
        return f"{key} (default: {value})"
    return text  # `=` がなければそのまま返す


def update_yaml_file(file_path, new_data):
    """
    YAMLファイルを更新または作成する。

    Args:
        file_path (str): 更新または作成するYAMLファイルのパス
        new_data (dict): 新しいデータ
    """
    # 既存データの読み込み
    if os.path.exists(file_path):
        with open(file_path, "r", encoding="utf-8") as f:
            existing_data = yaml.safe_load(f) or {}  # `None` の場合は `{}` を設定
    else:
        existing_data = {}

    # `macros` が存在しない場合、空リストを設定
    existing_macros = existing_data.get("macros", [])

    # 既存のマクロ名を取得(重複チェック用)
    existing_macro_names = {macro["name"] for macro in existing_macros}

    # 新しいデータをマージ(重複を避ける)
    for new_macro in new_data["macros"]:
        if new_macro["name"] not in existing_macro_names:
            existing_macros.append(new_macro)

    # 更新したデータを保存
    updated_data = {"version": 2, "macros": existing_macros}

    # YAMLをフォーマットして書き込み
    yaml_text = yaml.dump(updated_data, default_flow_style=False, sort_keys=False, allow_unicode=True)

    # 各要素の間に改行を入れる
    yaml_text = yaml_text.replace("\nmacros:", "\n\nmacros:")  # `macros` の前に改行を入れる

    # ファイルに書き込む場合
    with open(file_path, "w", encoding="utf-8") as f:
        f.write(yaml_text)


def generate_yaml_from_sql(sql_files):
    """
    指定されたSQLファイルリストからYAML構造を生成する。

    Args:
        sql_files (list): 解析するSQLファイルのパスのリスト

    Returns:
        dict: 生成されたYAML構造
    """
    all_macros = []

    for sql_file in sql_files:
        macros = parse_macros(sql_file)
        all_macros.extend(macros)  # 解析結果を統合

    yaml_structure = {"version": 2, "macros": all_macros}

    return yaml_structure


def get_files(directory):
    """
    指定されたディレクトリ内のSQLファイルを収集する。

    Args:
        directory (str): 検索するディレクトリのパス

    Returns:
        dict: ディレクトリパスをキー、SQLファイル名のリストを値とする辞書
    """
    entries = {}
    for root, _, files in os.walk(directory):
        sql_files = []
        for file in files:
            if file.endswith(".sql"):
                sql_files.append(file)
        entries[root] = sql_files

    return entries


def update_macros_md(data):
    """
    指定されたデータに基づいて macros.md ファイルを更新または作成する。

    Args:
        data (dict): ディレクトリパスをキー、ファイル名のリストを値とする辞書
    """
    for directory, files in data.items():
        md_entries = {}
        for file in files:
            file_name = os.path.splitext(file)[0]
            md_entries[file_name] = f"{{% docs {file_name} %}}\n\n{{% enddocs %}}\n"

        # macros.md ファイルのパス
        md_file = os.path.join(directory, "macros.md")

        # macros.md が存在するか確認
        if not os.path.exists(md_file):
            # 存在しない場合は新規作成して書き込む
            with open(md_file, "w") as f:
                f.write("\n".join(md_entries.values()))  # リストを改行で結合して書き込み
        else:
            # macros.md が存在する場合、file_name がすでに含まれているか確認
            with open(md_file, "r") as f:
                existing_content = f.read()

            # file_name が含まれていない場合のみ書き込む
            new_entries = [
                entry for file_name, entry in md_entries.items() if f"{{% docs {file_name} %}}" not in existing_content
            ]

            if new_entries:  # 新しいエントリがある場合のみ追記
                with open(md_file, "a") as f:
                    f.write("\n".join(new_entries))


def update_macros_yml(data):
    """
    指定されたデータに基づいて macros.yml ファイルを更新または作成する。

    Args:
        data (dict): ディレクトリパスをキー、ファイル名のリストを値とする辞書
    """
    for directory, files in data.items():

        yml_file = os.path.join(directory, "macros.yml")
        sql_files = [os.path.join(directory, file) for file in files]

        if not os.path.exists(yml_file):
            result = generate_yaml_from_sql(sql_files)
            update_yaml_file(yml_file, result)


if __name__ == "__main__":
    main()
{% docs ~ %}

{% enddocs %}
version: 2

macros:
- name: ~
  description: '{{ doc("~") }}'
  arguments:
  - name: ~

各フォルダにmdおくとdocs-paths: ["docs"]の設定を衝突する
docs-paths: ["docs"]を指定していないとCustom project-level overviewsをoverviews.md以外のmdで保存して認識しなくなるのでmacros.mdの生成を断念

YuichiYuichi

ymlにコメントとしてmacroを書く時、{% raw %}{% endraw %}で挟まないとCompilation Errorになって困った

YuichiYuichi

社内システムとか、リリース前データを除外する処理をマクロで共通化する例

{% macro systemname_release_filter(date_column) %}
    date({{ date_column }}) between "2025-03-04" and current_date("Asia/Tokyo") - 1
{% endmacro %}

{# 
where {{ systemname_release_filter("created_at_jst") }}  -- リリース後に絞る 
#}
YuichiYuichi

array

{% macro array_functions__array_except(a1, a2) %}
    (
        select array_agg(distinct i order by i)
        from unnest(coalesce({{ a1 }}, [])) as i
        left join unnest(coalesce({{ a2 }}, [])) as j on i = j
        where j is null
    )
{% endmacro %}

{# 
# サンプル
select {{ array_functions__array_except("[1,2,3,4,5]", "[3,4,5,6,7]") }} as result
--> [1, 2]
#}

https://qiita.com/etsuhisa/items/758270da2a36f7fae2c0