remove_foreign_key の if_exists オプションを逆転可能に
remove_foreign_key(..., if_exists: true) をロールバックした際、逆操作として生成される add_foreign_key(..., if_exists: true) が :if_exists を受け付けず、マイグレーションが冪等に戻らない問題を解消しました。今回の変更で逆転時に :if_not_exists が付与され、正しいロールバックが実現します。
背景
remove_foreign_key に if_exists オプションを付与すると、対象の外部キーが存在しない場合でもエラーを回避できますが、逆操作 add_foreign_key は if_exists を認識しないため、invert_remove_foreign_key が生成したコードは実行時に例外を投げていました。この不整合が原因でマイグレーションのロールバックが非冪等となり、デプロイ時の安全性が損なわれていました。
技術的な変更
activerecord/lib/active_record/migration/command_recorder.rb の invert_remove_foreign_key メソッドに、if_exists を if_not_exists に変換するロジックが追加されました。具体的にはオプションハッシュから :if_exists を削除し、同時に :if_not_exists キーを設定する一行が挿入されています。
変更前:
def invert_remove_foreign_key(args)
from_table, to_table = args
to_table ||= options.delete(:to_table)
# 省略
end
変更後:
def invert_remove_foreign_key(args)
from_table, to_table = args
to_table ||= options.delete(:to_table)
options[:if_not_exists] = options.delete(:if_exists) if options.key?(:if_exists)
# 省略
end
同様の変換ロジックは invert_add_foreign_key と invert_remove_check_constraint にも存在し、一貫したオプションマッピングが保証されています。テストコードも activerecord/test/cases/migration/command_recorder_test.rb に新しいケースが追加され、remove_foreign_key(if_exists: true) が逆転して add_foreign_key(if_not_exists: true) になることが検証されています。
設計判断
if_exists → if_not_exists の変換 という選択は、既存の invert_add_foreign_key の挙動と整合性を取るために採られました。add_foreign_key が if_not_exists をサポートしている点に合わせることで、逆操作全体で同一のオプション名を使用でき、コードベースの一貫性が向上します。また、オプション名を新たに導入せずに既存キーを再利用する方針は、後方互換性を保ちつつ最小限の変更で済むというトレードオフを示しています。
まとめ
この PR は、remove_foreign_key の if_exists オプションをロールバック時に if_not_exists へ正しく変換することで、マイグレーションの逆操作を冪等にし、デプロイの安全性を高めました。既存の設計を尊重しつつ、一貫したオプション変換ロジックを導入した点が主な意義です。