add_foreign_key の if_not_exists が逆変換で正しく扱われるように変更
Rails のマイグレーションで外部キー追加時に if_not_exists: true を指定した場合、ロールバック時に正しい逆操作が行われるように add_foreign_key の逆変換ロジックが修正されました。これにより、マイグレーションの冪等性が保たれ、開発フローが安定します。
背景
add_foreign_key に if_not_exists: true を付与したマイグレーションを逆変換すると、従来は remove_foreign_key(..., if_not_exists: true) が生成されていました。remove_foreign_key は :if_not_exists オプションを認識しないため、ロールバック時に外部キーが削除されず、逆変換が冪等でなくなっていました。PR の説明ではこの振る舞いが「idempotent inverse ではない」ことが指摘され、実装上の不整合が問題となっていました。したがって、逆変換時に適切なオプションに置き換える必要がありました。
技術的な変更
activerecord/lib/active_record/migration/command_recorder.rb の invert_add_foreign_key メソッドに、if_not_exists を if_exists に変換するロジックが追加されました。具体的には、引数の最後がハッシュであれば :validate を除去した上で、if_not_exists が存在すれば if_exists に置換しています。変更前後のコードは以下の通りです。
変更前:
def invert_add_foreign_key(args)
args.last.delete(:validate) if args.last.is_a?(Hash)
super
end
変更後:
def invert_add_foreign_key(args)
if (options = args.last).is_a?(Hash)
options.delete(:validate)
options[:if_exists] = options.delete(:if_not_exists) if options.key?(:if_not_exists)
end
super
end
さらに、テストスイートに新しいケースが追加され、if_not_exists が if_exists に変換されることが検証されています。テストは activerecord/test/cases/migration/command_recorder_test.rb に以下のように実装され、期待する逆変換シンボル列が得られることを明示しています。
def test_invert_add_foreign_key_if_not_exists
enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, if_not_exists: true]
assert_equal [:remove_foreign_key, [:dogs, :people, if_exists: true], nil], enable
end
これらの変更により、add_foreign_key(..., if_not_exists: true) の逆操作は remove_foreign_key(..., if_exists: true) へ正しく変換され、ロールバック時に外部キーが安全に削除されます。
設計判断
今回の実装は、既存の invert_add_foreign_key に最小限の分岐を追加する形で行われ、後方互換性を保持しつつ 同様のロジックが既に invert_add_check_constraint に実装されている点を踏襲しています。新たに別メソッドや設定キーを導入せず、オプション変換だけで解決したため、コードベースへの影響範囲が限定的です。これにより、他のマイグレーションコマンドとの一貫性が保たれ、将来的な拡張でも同様のパターンを再利用しやすくなります。
まとめ
add_foreign_key の if_not_exists オプションが逆変換時に if_exists に置換されるようコマンドレコーダーが修正されたことで、マイグレーションのロールバックが正しく冪等になります。テスト追加により挙動が保証され、既存設計との整合性も維持された点が本変更の主な意義です。