mcpField
D
django-safe-migration

django-safe-migration

github.com/vintasoftwareデータ・データベース166785 回インストール14 週間前に更新
リポジトリ

django-safe-migration について

django-safe-migrationは、Django開発者向けにPostgreSQLのマイグレーションをレビュー、作成、書き換えるスキルです。生成SQL、ロック動作、デプロイ方式、実行時ガードを確認し、ローリングデプロイのリスクと安全な変更方法を示します。

主な機能

  1. 01マイグレーションの生成SQLを確認
  2. 02PostgreSQLのロックと停止リスクを特定
  3. 03プロジェクト設定に基づき実行時ガードを評価
  4. 04安全なマイグレーションを作成・書き換え
  5. 05分割とデプロイ順序を計画

活用シーン

  1. 01ローリングデプロイ向けか確認
  2. 02NOT NULL列や外部キーを追加
  3. 03インデックス作成時の書き込み阻害を軽減
  4. 04停止を招く可能性のあるマイグレーションを修正

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/vintasoftware/django-ai-plugins --skill django-safe-migration

インストール後、Claude Code で/django-safe-migrationを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

references/
examples.md
operation-guide.md
postgres-locks.md

詳細

Django Migration — ダウンタイムゼロ

プロジェクト設定

以下の値について、アクティブなホストのプロジェクト指示ファイル(例:AGENTS.md または CLAUDE.md)を読みます。設定されていない場合は、表示されているデフォルト値を使用します。

キーデフォルト備考
Django バージョンunknowndb_default が利用可能かどうかに影響します。対応しているのは Django 5.0 以降のみです
デプロイ戦略rolling deployローリングデプロイが最も制約の厳しい方式です。ブルーグリーンデプロイやメンテナンス時間帯のデプロイでは、より多くの操作が可能です
ランタイムガードnoneガード(例:django-pg-zero-downtime-migrations)が設定されている場合、そのガードが禁止する操作はエラー、対象外の操作は警告です
マイグレーションコマンドpython manage.py sqlmigrate例:make sqlmigrate または docker compose run web python manage.py sqlmigrate
ドキュメント/wiki の URLnone設定されている場合、レビュー結果で問題を指摘する際に #<anchor> リンクを追加します

このスキルをプロジェクト向けに設定するには、アクティブなホストのプロジェクト指示ファイルに次のようなセクションを追加します。

## django-safe-migration
- Django version: 5.1
- Deploy strategy: rolling deploy
- Runtime guard: none
- Migration command: docker compose run web python manage.py sqlmigrate
- Docs URL: https://github.com/your-org/repo/wiki/migrations

使用する場面

  • 「このマイグレーションをレビューしてください」
  • 「このマイグレーションは安全ですか?」
  • 「次のマイグレーションを書いてください……」
  • 「このマイグレーションを安全な形に書き直してください」
  • 「NOT NULL 列の追加/列の削除/インデックスの追加/列名の変更/FK の追加はどうすればよいですか?」

ダウンタイムゼロのマイグレーションが重要な理由

ローリングデプロイでは、まず新しいデータベーススキーマを適用し、その後、アプリケーションのインスタンスを1つずつ再起動します。デプロイ中は常に、新旧のコードが同じデータベースに対して同時に実行されています。

以前のバージョンのアプリがトラフィックを処理している間でも、安全に実行できるマイグレーションが必要です。大きなテーブルに対して ACCESS EXCLUSIVE ロックを取得するマイグレーションは、読み取りと書き込みをすべてブロックします。負荷の高いテーブルでは、数秒の停止でもダウンタイムになります。

問題はロック自体だけではなく、待機キューにもあります。50ms で完了する ALTER TABLE でも、長時間実行中のトランザクションがあるとその後ろに並び、後続のクエリもすべてマイグレーションの後ろに並びます。負荷の高いテーブルでは、これが連鎖してコネクションプールの枯渇につながります。


PostgreSQL のロックの仕組み

ロック取得する操作ブロックするもの
ACCESS EXCLUSIVEほとんどの ALTER TABLE、DROP INDEX、DROP CONSTRAINT(FK)すべての読み取りと書き込み
SHARE ROW EXCLUSIVEADD FOREIGN KEY(子テーブルと参照先テーブルの両方で同時に取得)書き込みのみ(両方のテーブル)
SHARECREATE INDEX書き込みのみ
SHARE UPDATE EXCLUSIVECREATE INDEX CONCURRENTLY、VALIDATE CONSTRAINT実質的に何もブロックしないため、トラフィック中でも安全です

ロックの競合マトリクス全体(テーブルレベルのロック × ビジネスロジックの操作 × 行レベルのロック)と FIFO 待機キューの説明については、references/postgres-locks.md を読み込みます。次の場合に読み込みます。

  • 特定の操作がなぜ安全でないのかを説明する場合
  • 開発者から、あるロックの種類が何をブロックまたは競合させるか尋ねられた場合
  • 2つの同時操作が相互に影響するかを判断する場合

lock_timeout

ACCESS EXCLUSIVE を必要とする操作の前には、必ず SET LOCAL lock_timeout を指定します。これにより、ロックを無期限に待つ代わりに、マイグレーションは明確なエラーとともにすぐに失敗します。待機の連鎖によるコネクションプールの枯渇も防げます。

通常のトランザクション型マイグレーションの場合:

migrations.RunSQL("SET LOCAL lock_timeout = '2s'"),
migrations.AlterField(...),  # or any ACCESS EXCLUSIVE operation

SET LOCAL はタイムアウトを現在のトランザクション内に限定するため、他のセッションには影響せず、マイグレーションの完了後も設定は残りません。

デフォルト:2s。テーブルで正当に長時間実行されるトランザクションがあるとわかっている場合は長くし、より厳格な環境では短くします。

atomic = False のマイグレーションでは、SET LOCAL と DDL を同じ RunSQL 操作にまとめます。詳しくは「構造上のルール」を参照してください。


主なパターン

NOT VALID + VALIDATE(FK および CHECK 制約向け)

長時間のロックを避けて制約を追加する、PostgreSQL の 2 段階の手法です。

  1. ADD CONSTRAINT … NOT VALID — 制約を作成し、新しい書き込みにはただちに適用しますが、既存行のスキャンは行いません。テーブルスキャンを伴わない短時間のロックを取得します。FK 制約では両方のテーブルに SHARE ROW EXCLUSIVE、CHECK 制約では ACCESS EXCLUSIVE を取得します。
  2. VALIDATE CONSTRAINT — 既存行をスキャンし、制約を満たしていることを確認します。SHARE UPDATE EXCLUSIVE を取得し(FK 制約では参照先テーブルに ROW SHARE も取得します)、読み取りや書き込みはブロックしません。

危険なのは制約の作成そのものではなく、テーブル全体のスキャンです。処理を分割することで、書き込みをブロックするロック時間を数ミリ秒に抑え、長時間のスキャンをブロックのない手順に移せます。

PostgreSQL のドキュメント:NOT VALID · VALIDATE CONSTRAINT


実行時ガード

プロジェクトによっては、マイグレーション実行時に安全でない操作に対してエラーを発生させる、カスタムのデータベースバックエンドやリンターを設定しています(例:zero_downtime_migrations、django-pg-zero-downtime-migrations、django-migration-linter)。

マイグレーションをレビューする際は、次のように扱います。

  • プロジェクトの実行時ガードがブロックする操作 → エラーに分類します(マイグレーションは実行されません)
  • ガードの対象外の操作 → 警告に分類します(マイグレーションは実行されますが、ダウンタイムが発生する可能性があります)

実行時ガードが設定されていない場合は、安全でない操作をすべて、プロダクションへのデプロイ前に安全な方法へ書き換える必要があるエラーとして扱います。

このプロジェクトのガードが対象とする操作については、上記の Project Configuration ブロック(または AGENTS.md)を確認してください。


モード 1:レビュー

目標

PostgreSQL でローリングデプロイを行う際に、安全でない、またはリスクのある操作をすべて特定します。

手順
  1. マイグレーションファイルを最後まで読みます。
  2. <migration_command> <app_label> <migration_name> を実行し、Django が実際に実行する SQL を取得します。必ず実行してください。生成された SQL が実際の処理内容です。 アクティブなホストのプロジェクト指示ファイルにある Migration command の値を使用します。デフォルトは python manage.py sqlmigrate です。このキーが設定されておらず、プロジェクトのルートに Makefile または docker-compose.yml がある場合は、ユーザーに「このプロジェクトでは sqlmigrate をどのように実行していますか?」と尋ね、その回答を指示ファイルに保存するよう提案してください。ORM の操作クラスだけでは不十分です。たとえば、FK フィールドに null=True を追加する AlterField は、FK 制約の削除と再追加も行います。その結果、DROP CONSTRAINT(子テーブルと参照先テーブルの両方で ACCESS EXCLUSIVE を取得)に続いて、NOT VALID なしの ADD CONSTRAINT FOREIGN KEY(両方のテーブルで全行スキャンを伴う SHARE ROW EXCLUSIVE を取得)が出力されます。これらはどちらもマイグレーションファイルだけからはわかりません。
  3. references/operation-guide.md を読み込みます。検出された操作が安全でない理由を説明する際は、references/postgres-locks.md も読み込みます。
  4. 生成された SQL 文をそれぞれ、以下の検出チェックリストに照らして確認します。
  5. ACCESS EXCLUSIVE を必要とする操作がある場合(エラーまたは警告として検出された場合)、レポートを出力する前にユーザーに尋ねてください。

    "このマイグレーションには ACCESS EXCLUSIVE 操作が含まれています。キューへの滞留やロックの連鎖を避け、すぐに失敗させるために lock_timeout を追加する必要があります。デフォルトは 2s です。確認するか、カスタム値を指定してください。" マイグレーションにすでに SET LOCAL lock_timeout が含まれている場合は、設定済みの値を伝え、それが適切か確認してください。 修正手順には、確認済みの値を使用します。

  6. 次の形式で、構造化されたレポートを出力します。

マイグレーションレビュー:

エラー — ダウンタイムを引き起こす、またはランタイムガードによってブロックされます
  • [ERROR] <app>.<Model>.<field> に対する <OperationClass>:<処理内容と危険な理由> 修正:<安全な代替手段を1文で説明> この書き換えで変わること:安全なバージョンでも ACCESS EXCLUSIVE が必要かどうかを明確にし、必要な場合は実際に改善される点(ロック時間(テーブル全体のスキャン → 数ミリ秒)、失敗時の動作(気づかないままキューが連鎖的に詰まる → すぐにタイムアウトエラーになる)、またはその両方)を説明してください。書き換えによってロックが完全になくなると読者に思わせないでください。 <設定されている場合は wiki/ドキュメントへのリンク>
警告 — 長時間のロックやデプロイ時の問題を引き起こす可能性があります
  • [WARNING] <OperationClass>:<問題> 修正:<安全な代替手段>
構造上の問題
  • [ERROR|WARNING] <問題>(例:atomic=False がない、reverse のない RunPython)
安全
  • [OK] <すべてのチェックに合格した操作>
  1. エラーまたは警告がある場合は、書き換えを提案してください(モード3)。
  2. すべてのチェックに合格した場合は、マイグレーションを安全にデプロイできることを確認してください。

モード2:作成

目標

説明された変更に対して、正しく、ゼロダウンタイムのマイグレーションを生成します。

手順
  1. references/operation-guide.md と references/examples.md を読み込みます。
  2. 操作が曖昧な場合は確認します。
    • 対象のモデルとフィールドは何ですか?
    • 新しいカラムを追加しますか、それとも既存のカラムを変更しますか?
    • 外部キーの場合:参照先のテーブルはどれですか?新しいカラムですか、それとも既存のカラムですか?
    • 型を変更する場合:変更前と変更後の型は何ですか?
    • Django のバージョンはいくつですか(db_default の利用可否に影響します)?
  3. 操作に ACCESS EXCLUSIVE が必要な場合は、ユーザーに確認します。

    "このマイグレーションでは ACCESS EXCLUSIVE を使用します。ロックを取得できない場合にすぐ失敗するよう、lock_timeout を設定します。デフォルトは 2s です。確認するか、カスタム値を指定してください。"

  4. 必要なマイグレーションファイルの数を判断します。多くのパターンでは、別々の PR に分けた2つのファイルが必要です。
  5. references/examples.md のパターンを使って、マイグレーションファイルを生成します。各 ACCESS EXCLUSIVE ステートメントの前に SET LOCAL lock_timeout = '<confirmed_value>' を追加してください。通常の atomic = True マイグレーションでは、その前に RunSQL を置けます。atomic = False のマイグレーションでは、DDL の実行時にも SET LOCAL が有効な状態となるよう、タイムアウト設定と DDL を同じ RunSQL 操作にまとめてください。
  6. 2つのファイルが必要な場合は、必ずデプロイ手順を出力してください。
## Deployment Order
- Migration 1: ships in the same PR as the model/code change
- Migration 2: ships in a follow-up PR after the deploy is confirmed stable

モード3:書き換え

目標

既存の危険なマイグレーションを、1つ以上の安全なマイグレーションに変換します。

手順
  1. マイグレーションファイルを読みます。
  2. 検出チェックリストを使って、すべての安全でない操作を特定します。
  3. references/operation-guide.md と references/examples.md を読み込みます。
  4. ACCESS EXCLUSIVE が必要な操作がある場合は、ユーザーに確認します。

    "このマイグレーションには ACCESS EXCLUSIVE 操作が含まれています。ロックを取得できない場合にすぐ失敗するよう、書き換えに lock_timeout を追加します。デフォルトは 2s です。確認するか、カスタム値を指定してください。"

  5. 安全でない操作ごとに、適切な安全パターンを適用します。
  6. 書き換えたマイグレーションでは、各 ACCESS EXCLUSIVE 文の前に SET LOCAL lock_timeout = '<confirmed_value>' を追加します。通常の atomic = True のマイグレーションでは、前に RunSQL を置けます。atomic = False のマイグレーションでは、DDL の実行中も SET LOCAL が有効になるよう、タイムアウト設定と DDL を同じ RunSQL 操作にまとめます。
  7. 書き換えにファイルの分割が必要な場合は、両方のファイルを生成し、デプロイ手順も記載します。
  8. 次の項目を保持します:マイグレーション番号の接頭辞、dependencies、変更不要な安全な操作。
  9. 書き換え後、構造上のルール(以下を参照)に従っていることを確認します。
  10. マイグレーションコードの後に、必ず 「この書き換えによる変更点」 ブロックを出力し、次の内容を説明します。
    • ACCESS EXCLUSIVE が引き続き必要かどうか(多くの場合は必要です。明示してください)。
    • 実際に改善される点:ロック時間(全テーブルスキャンからメタデータのみの操作にかかる数ミリ秒へ)、失敗時の挙動(暗黙の待ち行列から lock_timeout による速やかなタイムアウトエラーへ)、またはその両方。
    • 導入されるデータ整合性上の猶予期間(例:Migration 2 が実行されるまで、NOT VALID によって既存の行は検証されません)と、テーブルの以前の状態を踏まえて問題があるかどうか。

検出チェックリスト

エラー — 実行時ガードの有無にかかわらず安全ではない操作
Django の操作検出する内容安全でない理由
AddFieldnull=False で、db_default(Django 5.0 以降)がない古いコードはこの列を省略して挿入するため、代わりに使用できるデータベースレベルのデフォルト値がありません
RemoveFieldSeparateDatabaseAndState 内にない古いコードが削除された列を名前で参照し、ただちにエラーになります
DeleteModelSeparateDatabaseAndState 内にない古いコードが削除されたテーブルを参照し、ただちにエラーになります
AddIndexAddIndexConcurrently を使用していないCREATE INDEX は SHARE ロックを取得し、作成中の書き込みをブロックします
RemoveIndexRemoveIndexConcurrently を使用していないDROP INDEX は ACCESS EXCLUSIVE を取得し、読み取りと書き込みをブロックします
AddConstraint(FK)NOT VALID + VALIDATE パターンを使用していない子テーブルと参照先テーブルの両方で SHARE ROW EXCLUSIVE のもと、全テーブルスキャンが行われます。スキャン中は書き込み(読み取りではありません)がブロックされます
AddConstraint(CHECK)NOT VALID + VALIDATE パターンを使用していないACCESS EXCLUSIVE のもと、全テーブルスキャンが行われます
AlterFieldCHECK の手順を経ずに、既存列の null=True を解除しているACCESS EXCLUSIVE のもと、全テーブルスキャンが行われます
AlterField(FK フィールド)sqlmigrate の出力に DROP CONSTRAINT に続いて ADD CONSTRAINT FOREIGN KEY が含まれているDjango は FK を削除して再追加します。DROP CONSTRAINT は子テーブルと参照先テーブルの両方で ACCESS EXCLUSIVE を取得します。また、NOT VALID を使わない再追加では、両方のテーブルで全テーブルスキャンを伴う SHARE ROW EXCLUSIVE が取得されます。SeparateDatabaseAndState と RunSQL を使って NOT VALID を追加し、lock_timeout も含めてください。
AddIndexConcurrently または RemoveIndexConcurrentlyMigration クラスで atomic が False ではないCONCURRENTLY はトランザクション内で実行できないため、エラーになります
RunPython関数がモデルを直接インポートしている(from app.models import X)現在のモデルクラスを使用し、過去のマイグレーションのスナップショットを使用しないため、古いマイグレーションが動作しなくなります
プロジェクトの実行時ガードで検出される場合の追加エラー

プロジェクトで実行時ガードが設定されている場合(「プロジェクトの設定」を参照)、これらもエラーとして報告してください(マイグレーション時に例外が発生します)。

Django の操作検出する内容
RenameFieldあらゆる名前の変更
RenameModelあらゆる名前の変更
AlterField列の型の変更
AddConstraintExclusionConstraint

実行時ガードが設定されていない場合も、これらをエラーとして報告してください。安全な書き換えが必要です。

警告
Django の操作検出する内容問題
AddConstraint(UNIQUE)インデックスを先に作成するパターンを使用していないSHARE ロック下でインラインインデックスを作成するため、作成中は書き込みがブロックされます
AlterField既存の列に null=True を追加する(DROP NOT NULL)ACCESS EXCLUSIVE を取得します。カタログの更新は高速ですが、長時間実行中のトランザクションがあるとその後ろに待機します。後続のクエリはすべてマイグレーションの後ろに待機するため、接続プールを枯渇させる可能性があります
RunPythonreverse_code がない元に戻せません
RunSQLreverse_sql がない元に戻せません

構造上のルール(常に確認)

  • いずれかの操作で CONCURRENTLY または VALIDATE CONSTRAINT を使用する場合は、Migration クラスで atomic = False を指定します。VALIDATE CONSTRAINT については、特に次の点に注意してください。(1)SHARE UPDATE EXCLUSIVE は同じロックとの競合を引き起こします。references/postgres-locks.md の競合マトリックスを参照してください。(2)atomic = True にすると、全スキャンの実行中、ラッパートランザクションが開いたままになり、それ以前のすべてのステートメントのロックを保持して、デッドロックのリスクが高まります。(3)atomic = False にすると、VALIDATE は独自のトランザクションで実行され、完了時にロックがすぐに解放されます。
  • RemoveField/DeleteModel には SeparateDatabaseAndState による分割を使用し、必ず別々の 2 つのファイルにします。
  • RunPython では、最低限 reverse_code=migrations.RunPython.noop を必ず指定します。
  • RunSQL では、必ず reverse_sql を指定します。本当に元に戻せない場合は、migrations.RunSQL.noop を使用し、その理由を記載します。
  • RunPython でモデルにアクセスする場合は、必ず apps.get_model("app", "ModelName") を使用し、直接インポートしないでください。
  • ACCESS EXCLUSIVE を取得する操作の前には、必ず SET LOCAL lock_timeout を設定します。デフォルト値は 2s です。作成または書き換えを行う前に、必ずユーザーに確認してください。タイムアウトを現在のトランザクション内に限定するため、SET ではなく SET LOCAL を使用します。
    • atomic = False の例外:SET LOCAL はトランザクションの終了時にのみリセットされます。atomic = False のマイグレーションでは、各操作はラッパートランザクションの外で実行されるため、単独の migrations.RunSQL("SET LOCAL lock_timeout = '2s'") は次の操作の実行前にリセットされ、効果がありません。マイグレーションで atomic = False を指定する場合は、タイムアウト設定と DDL を必ず 1 つの RunSQL 呼び出しにまとめてください。
      migrations.RunSQL("""
          SET LOCAL lock_timeout = '2s';
          ALTER TABLE app_model ADD COLUMN ...;
      """)
      

コメント