RAKSUL TechBlog

RAKSULグループのエンジニアが技術トピックを発信するブログです

チーム開発を加速するAI駆動型dbtワークフローの実践

はじめまして。バックエンドエンジニアの渡邉です。普段は、データエンジニアリングチームの一員として、dbtを使った開発ワークフローの自動化や効率化を支援しています。 この度、私たちのチームが実践しているAIと人間が協働する新しい開発スタイルについて、筆を執らせていただくことになりました。 本記事が、皆さんのチームの開発体験を向上させる一助となれば幸いです。

はじめに:なぜAI駆動型ワークフローが必要か

データエンジニアリングの現場では、dbtを使ったデータモデリングが主流になってきました。しかし、チーム開発において以下のような課題を感じることはないでしょうか。

  • 新しいモデルを作るたびに、命名規則やコーディングスタイルにばらつきが生じる
  • テストを書くのが後回しになり、設計の検証や改善の機会を失う
  • レビューに時間がかかり、開発のボトルネックになる
  • ジュニアメンバーの立ち上がりに時間がかかる

これらの課題を解決するため、私たちはCursorのProject Rules機能を活用することにしました。

CursorのProject Rulesとは?

CursorのProject Rulesは、AIエージェントの振る舞いを制御するための、再利用可能でスコープが限定された指示セットです。具体的には、プロジェクトのルートや特定のディレクトリに.cursor/rulesというフォルダを作成し、その中にルールを記述したファイル(.mdc形式)を配置します。

これにより、以下のようなことが可能になります。

  • ドメイン知識の共有: プロジェクト固有の知識(命名規則、アーキテクチャ方針など)をルールとしてAIに教え込む。
  • ワークフローの自動化: 定型的な作業(例:Boilerplateの生成、テストケースの追加)をAIに任せる。
  • 品質の標準化: コーディングスタイルや設計原則を強制し、チーム全体の成果物の品質を一定に保つ。

私たちはこの機能を活用し、AIと人間が協働する新しいdbt開発ワークフローを構築しました。本記事では、その実践内容と効果について紹介します。

AI駆動型ワークフローの全体像

私たちが構築したワークフローは、人間主導の設計・レビューと、2つのルールによる自動化を組み合わせた4つのフェーズから構成されています。

graph LR
    A["Phase 1<br/>設計<br/>(人間主導)"] --> B["Phase 2<br/>Staging自動生成"]
    B --> C["Phase 3<br/>TDDで設計・実装"]
    C --> D["Phase 4<br/>チームレビュー<br/>(人間主導)"]

    style A fill:#e1f5f3
    style B fill:#fff3cd
    style C fill:#fff3cd
    style D fill:#cfe2ff

設計・レビューは人間が主導し、実装フェーズでAIと人間が協働することで、良い設計と開発速度の両立を目指しています。

私たちの技術スタック

このワークフローは、以下のツールを組み合わせて実現しています:

  • データ統合: Fivetran(外部データソースからの自動取り込み)
  • データウェアハウス: Snowflake / BigQuery
  • データ変換: dbt(データモデリングとテスト)
  • 開発環境: Cursor(AIペアプログラミング)
  • MCPサーバー: DataPilot(VSCode拡張のPower User for dbtに付属)
  • 品質管理: SQLFluff(コードスタイルチェック)、dbt_project_evaluator(プロジェクト構造の自動評価)

Phase 1: 設計:今後のAI活用への期待

最初のフェーズである設計は、現在も人間が主導で行っています。ビジネス要求からテクニカルな仕様への変換は、経験豊富なエンジニアの暗黙知に依存しており、属人性が高い状況です。

現状では、「売上データの日次集計モデルを作りたい」という要求に対して、人間のエンジニアが以下のような質問で要件を明確化しています:

  • 集計の粒度は?(日次×商品?日次×店舗?)
  • 売上の定義は?(受注ベース?出荷ベース?)
  • 必要なディメンションは?(地域、カテゴリ、顧客属性など)

このような要件明確化のプロセスも、今後はルールとしてAIに実行させることで、設計品質の標準化を図っていく予定です。

Phase 2: Stagingモデルの自動生成で単純作業から解放

ソーステーブルからstagingモデルを作成する作業は、定型的ながら意外と時間がかかります。私たちのプロジェクトでは、Fivetranを使って様々な外部データソースからデータを取り込んでいますが、ここで一つの課題に直面していました。

特に課題となっていたのが、FivetranのGoogle Sheets Connectorの挙動です。私たちのチームでは、ビジネス部門が管理するGoogleスプレッドシートも重要なデータソースとして活用しています。これらのシートでは、管理のしやすさから「注文ID」や「顧客名」といった日本語でカラム名が付けられていることが少なくありません。

しかし、Fivetranがこれらの日本語カラムを持つテーブルを同期する際に、日本語の漢字が意図せず中国語のローマ字(ピンイン)のように変換されてしまうケースがありました。例えば、「注文ID」というカラムがDING_DAN_IDのような、元の意味を推測しにくい文字列になってしまうのです。

この結果、以下のような作業が機械的でありながら、ミスが起きやすく、開発者の負担となっていました。

  • 列名の解読と標準化(中国語ローマ字→英語のスネークケース変換、略語の展開)
  • データ型の適切な設定
  • 不要なメタデータカラムの除外(Fivetranが付与する同期情報など)
  • ドキュメントの記述

dbt-stgルールを使うことで、これらの作業が自動化されます。

実際の動作例

例えば、以下のようなソーステーブルがあったとします:

  • ソーステーブル: ORDER_TRANSACTIONS
  • カラム:
    • DING_DAN_ID (VARCHAR) -- 订单ID
    • KE_HU_MING (VARCHAR) -- 客户名
    • JIN_E (NUMBER) -- 金额
    • RI_QI (VARCHAR) -- 日期
    • _FIVETRAN_SYNCED (TIMESTAMP)
    • _FILE (VARCHAR)
    • _LINE (NUMBER)

AIに対して「SOURCE_ECOMMERCE.MAINのORDER_TRANSACTIONSテーブルに対してstgモデルを作成してください」と指示すると、以下のようなファイルが自動生成されます:

models/staging/ecommerce/source.yml

version: 2

sources:
  - name: ecommerce_source
    database: SOURCE_ECOMMERCE
    schema: MAIN
    tables:
      - name: order_transactions
        columns:
          - name: DING_DAN_ID
            description: '订单ID - 注文ID'
          - name: KE_HU_MING
            description: '客户名 - 顧客名'
          - name: JIN_E
            description: '金额 - 金額'
          - name: RI_QI
            description: '日期 - 日付'

models/staging/ecommerce/stg_ecommerce__order_transactions.sql

with
    source as (
        select
            DING_DAN_ID,
            KE_HU_MING,
            JIN_E,
            RI_QI
        from {{ source('ecommerce_source', 'order_transactions') }}
    ),
    cleaned as (
        select
            ding_dan_id as order_id,           -- 订单ID → order_id
            ke_hu_ming as customer_name,       -- 客户名 → customer_name
            jin_e::number as amount,           -- 金额 → amount
            ri_qi::date as order_date          -- 日期 → order_date
        from source
    )
select * from cleaned

models/staging/ecommerce/stg_ecommerce__order_transactions.yml

version: 2

models:
  - name: stg_ecommerce__order_transactions
    description: 'Eコマースの注文トランザクションデータ'
    columns:
      - name: order_id
        description: '注文ID'
        data_type: VARCHAR
        data_tests:
          - unique
          - not_null
      - name: customer_name
        description: '顧客名'
        data_type: VARCHAR
      - name: amount
        description: '注文金額'
        data_type: NUMBER
        data_tests:
          - not_null
      - name: order_date
        description: '注文日'
        data_type: DATE
        data_tests:
          - not_null

このように、中国語ローマ字の列名を適切な英語名に変換し、Fivetranのメタデータを除外し、適切なデータ型とテストまで設定してくれます。

Phase 3: テスト駆動開発(TDD)で設計を進化させる

stagingモデルの次は、ビジネスロジックを含むmartモデルの実装です。ここで重要なのが、テスト駆動開発(TDD)のアプローチです。

TDDは単に「テストを先に書く」だけの手法ではありません。Kent Beck氏の定義によれば、TDDは小さなステップで設計を進化させていくプロセスです。テストは設計の副産物として得られ、将来の変更に対する安全網となります。

dbtルールは、必ず以下のサイクルを守るように設計されています:

  1. Red(失敗するテストを書く) - まだ存在しない振る舞いを定義
  2. Green(テストをパスさせる) - 最小限の実装で動作させる
  3. Refactor(設計を改善する) - テストを通したまま内部設計を洗練

TDDサイクルの実例

ここでは、私たちの実際の業務を単純化した「日次の売上サマリーモデルを作成する」という架空の要求を例に、AIがTDDサイクルに沿って実装を進める様子を解説します。

Step 1: Red - 失敗するテストを書く

unit_tests:
  - name: test_daily_sales_summary_aggregation
    model: mrt_daily_sales_summary
    description: '日次×店舗別に売上が正しく集計されることをテスト'
    given:
      - input: ref('stg_ecommerce__order_transactions')
        rows:
          - { order_date: '2024-01-01', store_id: 'STORE_A', amount: 1000 }
          - { order_date: '2024-01-01', store_id: 'STORE_A', amount: 500 }
          - { order_date: '2024-01-01', store_id: 'STORE_B', amount: 2000 }
          - { order_date: '2024-01-02', store_id: 'STORE_A', amount: 1500 }
    expect:
      rows:
        - { sales_date: '2024-01-01', store_id: 'STORE_A', total_sales: 1500, order_count: 2 }
        - { sales_date: '2024-01-01', store_id: 'STORE_B', total_sales: 2000, order_count: 1 }
        - { sales_date: '2024-01-02', store_id: 'STORE_A', total_sales: 1500, order_count: 1 }

この時点でdbt buildを実行すると、モデルがまだ存在しないため、テストは失敗します。

Step 2: Green - テストをパスさせる

次に、テストをパスさせるための最小限のコードを実装します:

select
    order_date as sales_date,
    store_id,
    sum(amount) as total_sales,
    count(*) as order_count
from {{ ref('stg_ecommerce__order_transactions') }}
group by 1, 2

Step 3: Refactor - コードを改善する

テストがパスしたら、コードの品質を向上させます:

with
    daily_aggregation as (
        select
            order_date as sales_date,
            store_id,
            sum(amount) as total_sales,
            count(distinct order_id) as order_count,
            avg(amount) as average_order_value
        from {{ ref('stg_ecommerce__order_transactions') }}
        where order_date is not null
            and store_id is not null
        group by 1, 2
    )
select
    sales_date,
    store_id,
    total_sales,
    order_count,
    average_order_value
from daily_aggregation

このTDDサイクルにより、以下のような効果が得られます:

  • 設計の進化: 必要最小限の実装から始めて、徐々に洗練された設計へと進化させる
  • 仕様の明確化: テストを書く過程で、曖昧な要求が具体的な振る舞いとして定義される
  • 変更への安心感: 自動テストがセーフティネットとなり、リファクタリングを恐れずに実施できる

TDDは「品質保証」の手法というよりも、「設計手法」として捉えることが重要です。品質の向上は、良い設計の結果として自然に得られる副次的な効果なのです。

Phase 4: チームレビュー:AIによる設計と実装の接続へ

TDDサイクルによって技術的な品質が担保されたモデルも、最終的にはチームによるレビューが必要です。このフェーズの理想は、Phase 1で定義された設計と、Phase 3で実装されたコードが完全に一致していることを、効率的かつ客観的に検証することです。

現状、私たちのレビュープロセスは、dbt_project_evaluatorによる機械的なチェックと、人間による本質的なチェックの二段階で構成されています。dbt_project_evaluatorは、命名規則やテスト・ドキュメントのカバレッジなど、定義可能なベストプラクティスを網羅的にチェックしてくれる強力なツールです。

しかし、このアプローチにも課題は残されています。 「実装されたビジネスロジックが、本当に設計の意図を反映しているか?」という最も重要な検証が、レビュアーの経験やコンテキスト理解度に依存し、属人化してしまっているのです。

この課題を解決するため、私たちはPhase 1の成果物(設計情報)をインプットとしてAIに与え、ビジネスロジックの妥当性まで踏み込んでレビューを行う、新しいルールの導入を計画しています。これにより、設計と実装の間の見えない乖離をAIが検出し、人間はより戦略的な判断に集中できるようになります。

人間が最終的に注力すべきレビュー観点

AIレビューが導入された後、人間のレビュアーは以下の様な、より高度で戦略的な観点に集中することになります。

  1. ビジネスインパクトの評価
    • このモデル変更がビジネス全体に与える影響は何か
    • 隠れた前提条件や、ビジネス上のリスクはないか
  2. チーム横断でのコンテキスト
    • 他チームのプロジェクトとの依存関係や整合性
    • プロジェクト全体のロードマップとの整合性
  3. 未来への投資
    • 長期的なアーキテクチャの方向性との一致
    • 技術的負債の許容範囲と返済計画
    • パフォーマンスとコストの最適なバランス

従来のレビューで時間を取られていた機械的なチェックやロジックの逐次的な確認はAIとツールに任せ、人間は「最終承認者」として、より大局的な視点からの判断に責任を持つことになります。

導入による変化:体感的な効果

このワークフローを導入してから、チームの開発体験は大きく変わりました。具体的な変化を紹介します。

開発体験の向上

自動生成による効果

  • 「またstagingモデルを作るのか...」という憂鬱な気持ちがなくなった
  • 命名規則で悩む時間がゼロになった
  • チーム全体でコードの一貫性が保たれるようになった

TDD実装による効果

  • 小さなステップで設計を進化させることで、複雑な問題も扱いやすくなった
  • リファクタリングを恐れなくなり、より良い設計を追求できるようになった
  • テストを書く過程で仕様の曖昧さを早期に発見し、明確化できるようになった
  • 「動くコード」から「良い設計のコード」へと段階的に改善する習慣が身についた

AIレビューによる効果

  • 基本的なミスによる手戻りがほぼなくなった
  • レビュー前の自己チェックの負担が大幅に減った
  • ベストプラクティスを自然に学べるようになった

人によるレビューの質的向上

  • 「ここのビジネスロジック、こういうケースはどう扱う?」といった本質的な議論が増えた
  • レビューが学びの場になった
  • チーム全体のスキルレベルが向上した

チーム全体への影響

特に印象的だったのは、ジュニアメンバーの成長速度です。AIのサポートにより、経験が浅いメンバーでも最初から一定品質のコードを書けるようになりました。また、AIレビューのフィードバックを通じて、ベストプラクティスを実践的に学べるようになっています。

シニアメンバーにとっても、機械的なレビューから解放され、アーキテクチャ設計やビジネス要件の深掘りなど、より価値の高い活動に時間を使えるようになりました。

ルールの紹介(簡易版)

以下に、実際に使用しているルールを一部簡略化して紹介します。

dbt-stg.mdc

あなたは開発の権限を委任されたエージェント。許可したコマンドは確認なしで実行して、指示されたタスクを自律的に完遂してください。指示された範囲のタスクを自律的に完遂することに全力を挙げてください。指示内容の再確認、実行できるコマンドは依頼者に確認することは禁止。以下の指示に従って、効率的かつ正確にタスクを遂行してください。

## 概要

ソーステーブルからdbt staging modelを完全自動で生成します。`@etl/dbt.mdc`のTDD手法と併用してください。

## 使用方法

~~~入力テンプレート
{データベース名}.{スキーマ名}の以下テーブルに対してstgモデルを作成してください。
{テーブル名パターン or テーブル名を列挙}
~~~

例:

~~~入力例1
SOURCE_NA4D.MAIN_novasellの以下テーブルに対してstgモデルを作成してください。
dris_プレフィックスを持つテーブルj
~~~

~~~入力例2
SOURCE_NA4D.MAIN_novasellの以下テーブルに対してstgモデルを作成してください。
- dris_base_and_sales_company_mappings
- dris_conversions
- dris_internal_heim_cv_mappings
~~~

---

## 自動実行フロー

### Phase 1: 環境確認と情報収集

1. **プロジェクト情報取得**
   - `mcp_datapilot_get_projects`で現在のdbtプロジェクトを特定

2. **対象テーブル特定**
   - `SHOW TABLES IN {指定されたスキーマ}`で対象テーブルを抽出
   - 各テーブルの列情報を`DESCRIBE TABLE`で取得

3. **既存ファイル確認**
   - `models/staging/{データソース名}/`ディレクトリの存在確認
   - 既存のsource.ymlファイルの確認

### Phase 2: 列マッピング自動生成

1. **中国語ローマ字→英語変換の例**

   以下は単なる例なので、その他のパターンにも対応してください。**列名が中国語ローマ字でない場合、このルールは無視してください。**

   - FAN_SHE → sales_company (販社)
   - DAN_DANG → responsible/assigned (担当)
   - JU_DIAN → base/location (拠点)
   - FEN_LEI → category/classification (分類)
   - KATEGORI → category (カテゴリ)
   - RI/RI_QI → date (日/日期)
   - SHI_JIAN → time/datetime (時間)
   - MING → name (名)
   - HAO/ID → id/number (号/ID)
   - SHU → count/number (数)
   - JIN_E → amount (金額)
   - YU_BEI → reserved/spare (予備)
   - XIANG_MU → item/project (項目)
   - QI_HUA → plan/campaign (企画)
   - ZHONG_BIE → type/kind (種別)
   - DOU_DAO_FU_XIAN → prefecture (都道府県)

2. **型変換検出**
   - 日時パターン: `_RI`, `_DATE`, `_TIME`, `_TIMESTAMP` → DATE/TIMESTAMP型へ
   - 数値パターン: `_ID`, `_COUNT`, `_AMOUNT`, `_SHU` → NUMBER型へ
   - その他: VARCHAR型(デフォルト)

3. **不要列の自動除外**

   以下はFivetranメタデータ列です。不要なので除外してください。

   - `_FILE`
   - `_LINE`
   - `_SHEET_NAME`
   - `_MODIFIED`
   - `_FIVETRAN_SYNCED`
   - `_DELETED`

### Phase 3: ファイル自動生成

#### 3.1 source.yml生成

~~~yaml
version: 2

sources:
  - name: {データソース名}_source
    database: SOURCE_NA4D
    schema: {スキーマ名}
    tables:
      - name: {テーブル名}
        columns:
          - name: {列名}
            description: '{自動推測された説明}'
~~~

#### 3.2 SQLモデル生成(CTE構造)

~~~sql
with
    source as (
        select
            {必要な列のみを明示的に選択}
        from {{ source('{source_name}', '{table_name}') }}
    )
    , cleaned as (
        select
            {中国語列} as {英語列名},
            {日時列}::date as {列名},
            {数値列}::number as {列名}
        from source
    , final as (
        select
            {最終的に必要な列のリスト}
        from {直前のCTE}
    )
select * from cleaned
~~~

#### 3.3 YMLファイル生成

- モデル定義とdescription
- 列定義とdata_type
- unit_testsの雛形
- data_testsの自動設定

### Phase 4: 品質保証

1. **命名規則の適用**
   - モデル名: `stg_{データソース名}__{テーブル名}`
   - 列名: snake_case英語名

2. **ディレクトリ構造**

   ~~~dir-structure
   models/staging/{データソース名}/
   ├── source.yml
   ├── stg_{データソース名}__{テーブル名}.sql
   └── stg_{データソース名}__{テーブル名}.yml
   ~~~

---

## 注意事項

- **@etl/dbt.mdcとの併用**: TDDサイクルの実行は`@etl/dbt.mdc`の手順に従ってください
- **中国語ローマ字→英語変換に自信がない場合**: なるべく質問してください

dbt.mdc

あなたは開発の権限を委任されたエージェント。許可したコマンドは確認なしで実行して、指示されたタスクを自律的に完遂してください。指示された範囲のタスクを自律的に完遂することに全力を挙げてください。指示内容の再確認、実行できるコマンドは依頼者に確認することは禁止。以下の指示に従って、効率的かつ正確にタスクを遂行してください。

## 目的

追加・変更する **dbt モデル**に対し、テスト駆動開発(TDD)を実践することで、設計を段階的に進化させながら実装を進める。具体的には、まず失敗するテストを書いて振る舞いを定義し、そのテストをパスする最小限のコードを実装し、その後コードをリファクタリングして設計を改善するサイクルを繰り返す。このプロセスを通じて、良い設計と自動テストという2つの成果物を同時に得る。

---

## 事前準備

1. **モデルを追加する場合のみ、モデル名を提案してユーザーに確認する**
   * モデル変更の場合、「2. **dbtのドキュメントを確認する**」以降を実行する。
   * モデルを追加する場合、適切なモデル名を提案する。
   * **ユーザーの応答を待つ。**
   * ユーザーがユーザーが提案されたモデル名を承認したら、その名前を使用して以降の作業を実施する。
   * ユーザーが承認しなかった場合は、再度モデル名を提案するか、ユーザーの指示を待つ。
2. **dbtのドキュメントを確認する**
   * 作業前に、必ずDocsの @dbt を確認する。
   * 特に、 Best practice guides および Unit Testing のセクションについて読み込む。
   * 他に、ユーザの指示に関連する項目について読み込む。
   * **このステップの完了を明示的に報告すること。**
     * 具体例
       ```markdown
       DBTドキュメントの確認が完了しました。特に以下の項目について理解を深めました。
       - dbtのベストプラクティス
       - dbtのユニットテストの記述方法
       - {{ その他、理解した主要項目を列挙 }}
       ```

---

## テスト駆動開発(TDD)サイクル

以下の「レッド」「グリーン」「リファクタリング」のステップを、実装すべき各機能や変更点に対して繰り返すこと。

### 1. レッド:失敗するテストを書く

* **a. 要求の理解**: ユーザーからの指示に基づき、これから実装または変更するモデルの振る舞いやロジックを明確に理解する。
    * 理解できない場合、**ユニットテスト記述前に**、質問する。
* **b. ユニットテストの記述**:
    * 理解した要求を検証するための **`unit_test`** を、対象モデルに対応する `*.yml` ファイルのルート(`models`と同階層)に記述する。
    * このテストは、現時点では**必ず失敗する**ように書くこと(つまり、まだ実装されていないロジックや、変更前のロジックではパスしないテストを意図的に作る)。
    * テストケースは、正常系だけでなく、代表的な異常系や境界値も考慮すること。
    * 既存モデルの変更の場合、既存の`unit_tests`が新しい要求に適合しなくなる場合は、まずそのテストを修正・更新する(これが失敗するテストとなる)。不足しているテストがあれば追加する。
* **c. `dbt build` の実行と失敗確認**:
    * `datapilot の MCP ツール` を使用して `dbt build --select <対象モデル名>` を実行する。
    * 記述した `unit_test` が意図通りに **FAIL** することを必ず確認する。もしPASSしてしまう場合は、テストケースが不十分か、既に実装済みである可能性があるので、テストを見直す。

### 2. グリーン:テストをパスさせる

* **a. 最小限のコード実装**:
    * ステップ1で失敗した `unit_test` を **PASSさせるためだけの最小限のコード**を、対象のdbtモデル(`.sql`ファイル)に記述または修正する。
    * ここでは、完璧なコードを目指すのではなく、テストをグリーンにすることだけを目的とする。
* **b. `dbt build` の実行と成功確認**:
    * `datapilot の MCP ツール` を使用して `dbt build --select <対象モデル名>` を実行する。
    * 該当の `unit_test` が **PASS** することを必ず確認する。
    * もし他のテストが意図せずFAILした場合は、影響範囲を特定し修正する。すべてのユニットテストがPASSするまでこのステップを繰り返す。

### 3. リファクタリング:設計を改善する

* **a. モデルコードのリファクタリング**:
    * ステップ2で書いたモデルコード(`.sql`)を見直し、設計の観点から改善する。
    * 可読性、保守性、パフォーマンスを考慮しながら、より良い設計へと進化させる。
    * ロジックの重複排除、変数名やCTEの命名規則遵守、複雑なロジックの簡略化などを行う。
    * **SQL のインラインコメント**: 冗長で無意味なインラインコメントは追加しない。**既存のインラインコメントは、削除しないこと。**
* **b. データテスト(`data_tests`)の追加・更新**:
    * モデルの出力データの品質を担保するため、`*.yml` ファイルのモデル定義またはカラム定義に **`data_tests`** を追加・更新する。
    * 追加する制約は、以下の情報を総合的に判断して設定する。
        * **参照テーブルのスキーマ定義(`.yml`ファイル)の確認**:
            * 当該モデルが参照している他のテーブル(ソース、ステージング、中間モデルなど)の `.yml` ファイルを確認する。
            * それらのファイルに定義されている `data_tests`(特に `unique`, `not_null_multiple_columns`, `relationships` など)を把握する。
            * 新しく作成または変更するモデルのデータテストを記述する際に、これらの既存の制約との整合性を考慮し、必要に応じて同様の制約を継承または適用する。
        * **実データの確認**: `datapilot MCP ツール(execute_sql)` で **参照テーブルをクエリ** し、実データ(データそのものや、DESCRIBE TABLE等で得られるスキーマ情報を含む)を確認したうえで、制約の妥当性を判断する。
    * `tests:` キーの使用は禁止。必ず **`data_tests:`** を用いる。
    * **NOT NULL 制約**: 独自マクロである **`not_null_multiple_columns`** を用いて記述する。標準の `not_null` テストは使用しない。
    * **ユニーク制約**:
        * **単一カラム**のユニーク制約には、標準の **`unique`** テストを用いる。
        * **複数カラムの組み合わせ(複合キー)**によるユニーク制約には **`dbt_utils.unique_combination_of_columns`** を用いる。
    * その他、**参照整合性制約** (`relationships`)などを中心に追加する。
* **c. データタイプ(`data_type`)の追加・更新**
    * 上記 **b. データテスト(`data_tests`)の追加・更新** のために確認した以下の情報を元に、各列に `data_type` を設定する。
        * 参照テーブルの `.yml` ファイルに記述された既存の `data_type`。
        * `datapilot MCP ツール(execute_sql)` で参照テーブルを直接クエリして得られた実際のカラムの型情報(例: `DESCRIBE TABLE` コマンドの結果など)。
    * 型の表現は、参照している他のテーブルの `.yml` ファイルや、プロジェクト内の既存モデルの記述に合わせ、一貫性を保つこと。もし参照先に `data_type` の記述がない場合は、クエリで得られた実際の型情報に基づいて適切なものを選択する。
* **d. ユニットテストのリファクタリング**:
    * 既存の `unit_tests` が冗長であったり、改善の余地がある場合は修正する。
    * 必要に応じて、カバレッジ向上のために `unit_tests` を追加する。
* **e. `dbt build` の再実行と全テストPASS確認**:
    * `datapilot の MCP ツール` を使用して `dbt build --select <対象モデル名>` を実行し、全ての `unit_tests` および `data_tests` が **PASS** することを確認する。
* **f. `sqlfluff lint` の実行と修正**:
    * 以下のコマンドを実行し、SQLのスタイルガイドに従っているか確認する。
        ~~~bash
        sqlfluff lint <変更したモデルの.sqlファイルパス>
        ~~~
    * すべてのエラーおよび警告が解消されるまで、修正と再実行を繰り返す。

---

## 実装完了の判断基準

* ユーザーから指示された全ての機能追加・変更が完了している。
* 全ての機能・変更点に対して上記のTDDサイクル(レッド・グリーン・リファクタリング)が実施されている。
* 最終的な `dbt build` で、対象モデルに関連する全てのテスト(`unit_tests``data_tests`)が **PASS** する。
* 最終的な `sqlfluff lint` で、エラーが **0件** である。

---

## 禁止事項

* **TDDサイクルを無視した実装**: 必ず「失敗するテストを先に書く」ことから始めること。
* **SQL のインラインコメントの不適切な使用**:
    * 冗長で無意味なインラインコメントを新規に追加しない。
    * **既存のインラインコメントは、削除しないこと。**
* `tests:` キーの使用(必ず `data_tests:` を使うこと)

---

## ymlに記述するテストの具体例

~~~yml
version: 2

models:
  - name: stg_searchconsole__search_term_performances
    description: 'Google Search Console の日別・検索語句別パフォーマンス集計'
    data_tests:
      - dbt_utils.unique_combination_of_columns:
          combination_of_columns:
            - date
            - search_term
      - not_null_multiple_columns:
          columns:
            - date
            - search_term
            - impressions
            - clicks
            - data_source_id
    columns:
      - name: date
        description: '日付'
        data_type: DATE
      - name: search_term
        description: '検索語句'
        data_type: VARCHAR
      - name: impressions
        description: '表示回数'
        data_type: NUMBER
        data_tests:
          - dbt_utils.expression_is_true:
              expression: '>= 0'
      - name: clicks
        description: 'クリック数'
        data_type: NUMBER
        data_tests:
          - dbt_utils.expression_is_true:
              expression: '>= 0'
      - name: data_source_id
        description: 'データソースID'
        data_type: VARCHAR

unit_tests:
  - name: test_stg_searchconsole__search_term_performances_aggregation
    model: stg_searchconsole__search_term_performances
    description: '日別・検索語句別に集計され、data_source_idが正しく設定されることをテスト'
    given:
      - input: source('novasell_source', 'google_search_console')
        rows:
          - { DATE: '2023-01-01', QUERY: 'term1', CLICKS: 10, IMPRESSIONS: 100 }
          - { DATE: '2023-01-01', QUERY: 'term1', CLICKS: 5, IMPRESSIONS: 50 }
          - { DATE: '2023-01-01', QUERY: 'term2', CLICKS: 20, IMPRESSIONS: 200 }
          - { DATE: '2023-01-02', QUERY: 'term1', CLICKS: 1, IMPRESSIONS: 10 }
    expect:
      rows:
        - {
            date: '2023-01-01',
            search_term: 'term1',
            clicks: 15,
            impressions: 150,
            data_source_id: 'novasell_gsc_search_term',
          }
        - {
            date: '2023-01-01',
            search_term: 'term2',
            clicks: 20,
            impressions: 200,
            data_source_id: 'novasell_gsc_search_term',
          }
        - {
            date: '2023-01-02',
            search_term: 'term1',
            clicks: 1,
            impressions: 10,
            data_source_id: 'novasell_gsc_search_term',
          }
~~~

今後の展望

現在は2つのルールを使っていますが、今後は以下のような拡張を考えています:

  • dbt-design: 設計フェーズでの要件明確化支援
  • dbt-review: 設計情報を基にビジネスロジックの妥当性を検証するレビュー支援
  • dbt-test: より高度なテストケースの自動生成
  • dbt-docs: ドキュメントの自動生成と更新
  • dbt-perf: パフォーマンスチューニングの提案

また、他のdbt以外のツールを使った開発(Dagsterなど)への展開も視野に入れています。

謝辞

本記事を執筆するにあたり、多くの方からインスピレーションをいただきました。この場をお借りして感謝申し上げます。

AIエンジニアのkinopeeさんには、AIとの協働に関する数々の先進的な取り組みから、本記事の着想を得る上で多大な影響を受けました。kinopeeさんの存在がなければ、この記事が形になることはありませんでした。

また、dbt.mdcルールの中核をなすTDDサイクルは、和田卓人(@t_wada)さんによる「【翻訳】テスト駆動開発の定義」から大きな着想を得ています。この記事のおかげでTDDの本質を学ぶことができました。素晴らしい記事を翻訳し、公開してくださったことに心から感謝いたします。

この記事が、皆さんの開発の一助となれば幸いです。