`add_index(if_not_exists: true)` のリバーシブル化
この PR は、add_index と remove_index の逆変換時に :if_not_exists と :if_exists を相互に変換できるようにし、マイグレーションのロールバックを正しく idempotent にします。
背景
既存の逆変換ロジックは :if_not_exists と :if_exists を考慮していません。remove_index ..., if_exists: true を逆変換すると add_index に :if_exists が残り、インデックスが既に存在する場合にロールバックが例外を投げました。また、add_index ..., if_not_exists: true の逆変換は remove_index に同名オプションを渡すだけで、remove_index が :if_exists のみを認識するため同様の例外が発生していました。
この問題は invert_add_foreign_key/invert_remove_foreign_key の実装と整合性が取れていませんでした。PR #57972 で外部キーの逆変換が :if_exists ⇔ :if_not_exists に統一されたことを受け、インデックスでも同様の対応が求められました。
このギャップを埋めることで、マイグレーションのロールバック時にインデックスの有無に依存しない安全な処理が実現します。
技術的な変更
activerecord/lib/active_record/migration/command_recorder.rb にオプション変換ロジックが追加されました。invert_remove_index では :if_exists が存在すれば :if_not_exists に置き換える処理が挿入されています。
- options.delete(:if_exists)
+ options[:if_not_exists] = options.delete(:if_exists) if options.key?(:if_exists)
invert_add_index でも対称的な変換が行われます。if_not_exists が指定されていれば :if_exists に置き換えてから super を呼び出すことで、既存の逆変換ロジックを再利用しています。
+ def invert_add_index(args)
+ if (options = args.last).is_a?(Hash)
+ options[:if_exists] = options.delete(:if_not_exists) if options.key?(:if_not_exists)
+ end
+ super
+ end
テストが追加され、期待される逆変換が保証されています。command_recorder_test.rb に test_invert_add_index_if_not_exists と test_invert_remove_index_if_exists が新たに導入され、add_index と remove_index の双方でオプションが正しく変換されることを検証しています。
+ def test_invert_add_index_if_not_exists
+ remove = @recorder.inverse_of :add_index, [:table, :one, if_not_exists: true]
+ assert_equal [:remove_index, [:table, :one, if_exists: true], nil], remove
+ end
+
+ def test_invert_remove_index_if_exists
+ add = @recorder.inverse_of :remove_index, [:table, { column: [:one, :two], if_exists: true }]
+ assert_equal [:add_index, [:table, [:one, :two], if_not_exists: true]], add
+ end
これらの変更はオプションの変換のみであり、既存の add_index / remove_index の挙動や他のオプションに影響を与えることはありません。
設計判断
オプション変換を逆変換ロジックに組み込むことで、既存 API をそのまま活用できました。新たなフラグやメソッドを導入せず、command_recorder.rb の既存フローに最小限のコードを追加する方針が取られました。
他のマイグレーションインバータと一貫性を保つ設計が選択されました。invert_add_foreign_key や invert_remove_foreign_key で行われた :if_exists ⇔ :if_not_exists の変換と同様に、インデックスでも同様の変換を実装することで、開発者が期待できる振る舞いを統一しています。
このアプローチは、既存のマイグレーションコードを壊さずに idempotent なロールバックを実現し、Rails のマイグレーションシステム全体の信頼性を高める設計判断と言えます。
まとめ
add_index(if_not_exists: true) と remove_index(if_exists: true) が相互に正しく変換されるようになり、マイグレーションのロールバック時にインデックスの有無で例外が発生しなくなります。変更はオプションの変換ロジックとテスト追加に留まり、既存の API 互換性を維持しつつ、他のインバータと同様の設計方針を取り入れた点が重要です。