`add_index(if_not_exists: true)` のリバーシブル化

rails/rails

この PR は、add_indexremove_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_keyinvert_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.rbtest_invert_add_index_if_not_existstest_invert_remove_index_if_exists が新たに導入され、add_indexremove_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_keyinvert_remove_foreign_key で行われた :if_exists:if_not_exists の変換と同様に、インデックスでも同様の変換を実装することで、開発者が期待できる振る舞いを統一しています。

このアプローチは、既存のマイグレーションコードを壊さずに idempotent なロールバックを実現し、Rails のマイグレーションシステム全体の信頼性を高める設計判断と言えます。

まとめ

add_index(if_not_exists: true)remove_index(if_exists: true) が相互に正しく変換されるようになり、マイグレーションのロールバック時にインデックスの有無で例外が発生しなくなります。変更はオプションの変換ロジックとテスト追加に留まり、既存の API 互換性を維持しつつ、他のインバータと同様の設計方針を取り入れた点が重要です。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
d3455fab

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文、背景、技術的変更、設計判断(任意)、まとめの全体構成が揃っており、3部構成が明確に示されています。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックは `ruby:ファイルパス` の形式で正しく記述され、PRリンクは `[PR #57987](URL)` の形式で正しくリンク化されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

対象はRails開発者であり、専門用語や内部実装に焦点を当てており、初心者向けの過度な説明はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションは総論・各論・結論の流れを保ち、段落はトピックセンテンスで始まり1段落1トピック、適切な長さで空行で区切られています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事中のコードブロックは提供されたDiffと完全に一致しており、削除・追加箇所とも整合しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

使用されている技術用語はPRと一致し、誤用や不適切な表現はありません。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

変更理由・影響・実装内容がPRの説明と一致し、技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

全ての主張はPRタイトル・Description・Diffで裏付けられており、推測や捏造はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号 #57987、ファイルパス、コード内容など数値・固有名詞は正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルはPRタイトル「Make `add_index(if_not_exists: true)` reversible」の意味と一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事内にPRに記載されていない外部知識(バージョンサポート等)は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の記載はなく、PR情報と矛盾する表現もありません。