remove_column のオプションだけは不可逆化でエラーを明示
Rails のマイグレーションで remove_column に型情報が無いままオプションだけを渡すと、ロールバック時に暗黙の add_column が生成され、実行時エラーが発生していました。この PR はそのケースを検出し、明確な ActiveRecord::IrreversibleMigration 例外を即座に発生させることで、開発者に原因を示すように変更します。
背景
remove_column :users, :name, null: false のように型を省略してオプションだけを指定したマイグレーションは、CommandRecorder の可逆性チェックで args.size <= 2 のみが判定基準になっていたため guard を通過し、逆変換として [:add_column, [:users, :name, {null: false}], block] が生成されました。結果としてロールバック時に add_column が不正な引数で呼び出され、 ArgumentError や NoMethodError がスローされ、根本原因が分かりにくい状況となっていました。
この挙動は、command_recorder.rb に明記されている「remove_column は型が必須」という契約に反しており、同様のロジックを持つ invert_remove_columns が正しくチェックしていることから、実装の抜け漏れと判断されました。
技術的な変更
activerecord/lib/active_record/migration/command_recorder.rb の invert_remove_column メソッドに、オプションハッシュが型位置に来た場合も不可逆とみなす条件を追加しました。
def invert_remove_column(args)
if args.size <= 2 || args[2].is_a?(Hash)
raise ActiveRecord::IrreversibleMigration, "remove_column is only reversible if given a type."
end
super
end
-
args.size <= 2は従来通り、引数が2個以下(テーブル名とカラム名だけ)で不可逆と判定します。 -
args[2].is_a?(Hash)を加えることで、第三引数がハッシュ(オプション)で型が無いケースを検出し、同様に例外を投げます。 - それ以外は
superへ委譲し、既存の可逆ロジックをそのまま利用します。
テストも activerecord/test/cases/migration/command_recorder_test.rb に新たに追加し、オプションだけの場合に IrreversibleMigration が発生することを確認しています。
def test_invert_remove_column_with_options_but_no_type
assert_raises(ActiveRecord::IrreversibleMigration) do
@recorder.inverse_of :remove_column, [:table, :column, { null: false }]
end
end
これにより、正しい型が提供されている remove_column :users, :name, :string, null: false は従来通り可逆と扱われ、既存テストはそのまま緑化します。
設計判断
既存のガード条件に最小限の拡張を加える という方針が取られました。代替案としては新しいメソッドやフラグを導入することも考えられましたが、invert_remove_columns と同等のチェックロジックを共有することで、コードの一貫性と可読性を保ちつつ後方互換性を維持できると判断されました。
- 条件分岐は 1 行の
if文にまとめられ、変更点は可視性が高くレビューしやすいです。 - 例外メッセージは既存のものと統一し、ユーザーが期待するエラーメッセージをそのまま提供します。
- 既存の
super呼び出しを残すことで、将来的にCommandRecorderの基底実装が変わってもこのガードは影響を受けません。
まとめ
この PR は、remove_column に型情報が無いままオプションだけが渡されたケースを検出し、即座に IrreversibleMigration をスローすることで、ロールバック時の暗黙的なクラッシュを防止します。最小限の条件追加に留めた設計により、既存の可逆ロジックと後方互換性を損なうことなく、開発者に明確なフィードバックを提供します。