本番のユーザーテーブルに何気なく NOT NULL カラムを一本足しただけで、朝までアプリが起動しなくなったことがあります。個人開発を始めて数年、自分ひとりで運用しているサービスでした。テストDBでは一瞬で終わったマイグレーションが、数十万行のテーブルではテーブル全体をロックし、その裏で走っていた書き込みがすべて詰まっていたのです。
原因は単純でした。既存行に既定値のない NOT NULL を足せば、DBは全行を書き換えようとします。頭では知っていたはずのことを、深夜の眠い頭で見落としていました。あの朝の、手が冷たくなる感覚は今でも覚えています。
# Database Migration Policy## Rules- Always create reversible migrations (include down migration)- Never drop columns directly — rename to _deprecated first- Add NOT NULL columns with DEFAULT value to avoid breaking existing rows- Create indexes concurrently for tables with > 10,000 rows- Migration file names must follow: YYYYMMDD_HHMMSS_description.sql## Naming Conventions- Tables: snake_case, plural (e.g., user_profiles)- Columns: snake_case (e.g., created_at)- Indexes: idx_{table}_{columns} (e.g., idx_users_email)- Foreign keys: fk_{table}_{ref_table} (e.g., fk_orders_users)## Safety Checks- Verify no data loss before applying- Check for long-running locks on production tables- Estimate migration duration for large tables
この AGENTS.md を配置しておくと、Antigravity Agent はマイグレーション生成時に自動的にこのポリシーを参照し、ルールに沿ったコードを出力します。冒頭で触れた NOT NULL の事故は、この Add NOT NULL columns with DEFAULT value の一行があれば防げていました。ポリシーは、過去の自分の失敗を未来のエージェントに申し送るための手紙のようなものだと考えています。
エージェントに二段階設計を任せるときは、AGENTS.md に Renames and NOT NULL must be split into Expand/Contract phases across separate deploys と明記しておきます。これを書いておくだけで、エージェントは「一回の PR に全部詰め込む」誘惑に負けなくなります。私はこのルールを入れて以来、改名系の事故を一度も起こしていません。
## Migration Review Checklist- [ ] Reversible: DOWN migration exists and is tested- [ ] No data loss: SELECT COUNT before/after matches- [ ] Performance: No full table locks on tables > 100K rows- [ ] Indexes: New columns used in WHERE/JOIN have indexes- [ ] Defaults: NOT NULL columns have sensible defaults- [ ] Naming: Follows project naming conventions
-- エージェントによる改善提案-- WARNING: avatar_url を WHERE 句で使用する予定がある場合、-- インデックスの追加を推奨しますCREATE INDEX CONCURRENTLY idx_users_avatar_url ON "users" ("avatar_url") WHERE "avatar_url" IS NOT NULL;-- CONCURRENTLY を使用することでテーブルロックを回避します
-- rollback/20260328_153000_remove_avatar_url.sql-- Safety check: カラムにデータが存在するか確認DO $$DECLARE row_count INTEGER;BEGIN SELECT COUNT(*) INTO row_count FROM "users" WHERE "avatar_url" IS NOT NULL; IF row_count > 0 THEN RAISE NOTICE '⚠ % rows have avatar_url data. Backing up...', row_count; -- バックアップテーブルに退避 CREATE TABLE IF NOT EXISTS "_backup_users_avatar_url" AS SELECT id, avatar_url FROM "users" WHERE "avatar_url" IS NOT NULL; END IF;END $$;-- ロールバック実行ALTER TABLE "users" DROP COLUMN IF EXISTS "avatar_url";-- 期待される結果: avatar_url カラムが削除される-- データが存在した場合は _backup_users_avatar_url に退避済み
# .github/workflows/migration-check.ymlname: Migration Safety Checkon: pull_request: paths: - "prisma/migrations/**" - "drizzle/**" - "src/db/schema.*"jobs: migration-check: runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: test POSTGRES_DB: test_db ports: - 5432:5432 steps: - uses: actions/checkout@v4 - name: Apply migrations to test DB env: DATABASE_URL: postgresql://postgres:test@localhost:5432/test_db run: | npx prisma migrate deploy echo "✅ Migrations applied successfully" - name: Verify rollback scripts exist run: | # 各マイグレーションに対応するロールバックがあるか確認 for dir in prisma/migrations/*/; do migration_name=$(basename "$dir") if [ ! -f "rollback/${migration_name}_rollback.sql" ]; then echo "❌ Missing rollback for: $migration_name" exit 1 fi done echo "✅ All rollback scripts present"
この CI 設定により、マイグレーションファイルが変更されたプルリクエストでは自動的にテスト DB への適用とロールバックスクリプトの存在確認が実行されます。エージェントが生成した内容が CI で検証されることで、二重の安全ネットが構築されます。前節の Shadow DB 検証スクリプトをこのジョブに一段追加すれば、ドリフト検出まで含めた三重のネットになります。
もし一つだけ今日から始めるなら、AGENTS.md に「変更タイプ別の危険度と安全な手順」の表を貼ることをおすすめします。エージェントの速さは、安全の型と組み合わさって初めて資産になります。順番としては、まずポリシーを明文化し、次に Expand/Contract を既定の作法にし、その上で Shadow DB 検証を CI に一段差し込む。ここまで来ると、深夜のマイグレーションも落ち着いて回せるようになります。