Skip to content

Ruby on Rails PR Digest - 2026年 5月

このページは rails/rails リポジトリにマージされたPull Requestを自動的に収集し、AIで要約したものです。

#57271 Gracefully handle nil during multi-parameter assignment

マージ日: 2026/5/5 | 作成者: @seanpdoyle

  1. 概要 (1-2文で)
    マルチパラメータ属性(例: Date カラム)の代入時に、これまで "" のときだけ正常に「値なし」として扱われていた挙動を、nil に対しても例外なく同様に扱えるようにした修正です。nil を渡した際に発生していた NoMethodError: undefined method 'empty?' for nil を解消します。

  1. 変更内容の詳細

何が問題だったか

Rails のマルチパラメータ属性("last_read(1i)", "last_read(2i)", "last_read(3i)" のような形式のキー)に対して、値として空文字列 "" を渡すと「値なし」として扱われていました:

ruby
topic = Topic.where.not(last_read: nil).first
topic.attributes = {
  "last_read(1i)" => "",
  "last_read(2i)" => "",
  "last_read(3i)" => "",
}
topic.last_read # => nil

一方で、同じ状況で ""nil に置き換えると、内部実装で value.empty? のようなチェックをしていたため、NilClassempty? が実装されておらず NoMethodError が発生していました:

ruby
topic = Topic.where.not(last_read: nil).first
topic.attributes = {
  "last_read(1i)" => nil,
  "last_read(2i)" => nil,
  "last_read(3i)" => nil,
}
# => NoMethodError: undefined method `empty?' for nil:NilClass

何をどう直したか

activerecord/lib/active_record/attribute_assignment.rb のマルチパラメータ処理部分で、

  • これまで: value.empty? で「空」を判定していた
  • これから: value.blank? で判定する

という変更が行われました(実質 1 行の差し替え)。

blank? は Rails 拡張であり、

  • String#blank? は内部で empty? などを使って空文字・空白文字列を判定
  • NilClass#blank? も定義されていて true を返す

ため、"" だけでなく nil に対しても同じように「値なし」という扱いができます。

この修正後は次のように動作します:

ruby
topic = Topic.where.not(last_read: nil).first
topic.attributes = {
  "last_read(1i)" => nil,
  "last_read(2i)" => nil,
  "last_read(3i)" => nil,
}
topic.last_read # => nil (例外は発生しない)

テストとドキュメント

  • activerecord/test/cases/multiparameter_attributes_test.rb に、nil を渡したケースが正しく nil に設定され、例外が出ないことを確認するテストが追加されています。
  • activerecord/CHANGELOG.md に、この挙動変更(バグフィックス)が追記されています。

  1. 影響範囲・注意点
  • 対象となる機能

    • Active Record のマルチパラメータ属性("column(1i)", "column(2i)", "column(3i)" など)を使った代入ロジック全般が対象です。典型的にはフォームからの Date / Time / DateTime 入力のパラメータ処理で利用されます。
  • アプリケーションへの実質的な影響

    • これまで「nil を渡すと例外が出る」ために、ワークアラウンドとして nil"" に変換していた箇所があれば、その必要がなくなります。
    • "" を「値なし」として扱っていた挙動は維持されつつ、nil も同様に「値なし」として安全に扱われるようになります。
    • blank? を使うことで " "(空白のみの文字列)なども「空」とみなされる点は、従来の empty? との違いになり得ますが、フォーム入力的には通常期待される挙動です(※既存コードでも多くの場合 blank? を利用しているので、違和感は少ないはずです)。
  • 後方互換性の観点

    • これまで nil によって意図的に例外を発生させていた、というような特殊な利用をしていない限り、挙動は「エラーにならなくなる」という改善として受け取れます。
    • もしマルチパラメータ用の値に、" "(スペースのみ)を意図的に「非空」として扱いたいような非常にニッチなケースがあれば、今回の blank? への変更で「空」と見なされるようになる可能性があります。

  1. 参考情報 (あれば)

#50889 Introduce ActiveSupport::TestCase.around

マージ日: 2026/5/5 | 作成者: @seanpdoyle

  1. 概要 (1-2文で)
    Rails の ActiveSupport::TestCasearound コールバック(フック)が追加され、各テストの setupteardown を1つの「囲い込み」処理として扱えるようになりました。これにより、RSpec などで馴染みのある「around フック」を Rails 標準のテストでもシンプルに使えるようになります。

  1. 変更内容の詳細

2-1. ActiveSupport::TestCase.around が追加

ActiveSupport::TestCasearound コールバックが導入され、以下の順序で実行される処理全体を 1 つのフックで囲めます。

  • setup
  • 実際のテストメソッド (test "..." do ... end)
  • teardown

PR の説明にある通り、ブロックには テストクラスのインスタンス (test_case) と実行ブロック (&block) が渡されます。

ruby
class MyTest < ActiveSupport::TestCase
  around do |test_case, &block|
    # 例: あるクライアントをスタブした状態でテストを実行
    Client.with(stubbed: true, &block)
  end

  def setup
    # 通常の setup
  end

  test "something" do
    # この中身が Client.with(stubbed: true) の中で実行される
  end

  def teardown
    # 通常の teardown
  end
end

上記の around ブロックは、内部的には

ruby
around do |test_case, &block|
  # setup → test → teardown を表す「ひとかたまりの block」が渡される
  Client.with(stubbed: true) do
    block.call
  end
end

というイメージで動作します。

2-2. 実装位置

実装は activesupport/lib/active_support/testing/setup_and_teardown.rb に追加されています。
ActiveSupport::TestCase が持つ既存の setup / teardown に続く、テスト用コールバック機構の一部として around が組み込まれました。

Rails には既に Controller / Job / Mailer などで利用されている Active Support のコールバックシステムがあるため、それと同じ仕組み・記法で around を利用できます。

2-3. テストとファイル構成の変更

  • 新規テスト: activesupport/test/testing/around_callback_test.rb
    around コールバックの挙動(順序・ネスト・複数定義など)がテストされています。
  • 既存の teardown 周りのテストファイル名を整理するため、after_teardown_test.rbcallbacks_test.rb にリネームされています(PR 説明にあるが、この diff ではリネームというより around 用テスト追加がメイン)。

2-4. 典型的な利用例

  1. 一時的なコンテキストや接続の差し替え:
ruby
class ApiClientTest < ActiveSupport::TestCase
  around do |test_case, &block|
    ApiClient.with_base_url("https://stubbed.example.com") do
      block.call
    end
  end
end
  1. グローバル設定の一時変更:
ruby
class FeatureFlagTest < ActiveSupport::TestCase
  around do |test_case, &block|
    old_value = Feature.enabled?(:new_ui)

    Feature.enable!(:new_ui)
    block.call
  ensure
    Feature.set!(:new_ui, old_value)
  end
end
  1. トランザクション・外部リソースのラップ(※DB は Rails が標準でトランザクションフィクスチャを持つので、別リソース向けなどに向いている):
ruby
class ExternalServiceTest < ActiveSupport::TestCase
  around do |test_case, &block|
    ExternalService.transaction do
      block.call
      raise ActiveRecord::Rollback
    end
  end
end

  1. 影響範囲・注意点
  • 対象: ActiveSupport::TestCase を継承する Rails 標準のテスト(モデルテスト、コントローラテスト、システムテストなど)。
    ただし、Minitest::Test を直接継承しているテストには影響しません。
  • 既存の setup / teardown との関係:
    • around は「setup + test本体 + teardown の外側」を囲むイメージです。
    • したがって、around 内で block.call を実行しないと、setup / テスト本体 / teardown が一切実行されません。
  • 複数の around の組み合わせ:
    • 複数の around を定義した場合、コールバック順序(LIFO / FIFO)やネストのされ方は Active Support の一般的なコールバックと同様です。
    • 内外の順番を意識しないと、想定と違う順序で環境のセットアップ・クリーンアップが行われる可能性があります。
  • minitest-hooks との競合:
    既に minitest-hooks を使って around を導入している場合、ActiveSupport::TestCase 側の around と競合する可能性があります。
    Rails 標準の around に移行する場合は、二重適用・メソッド名衝突などに注意してください。
  • 実行コスト・複雑さ:
    around はテスト全体をラップできるため便利な一方で、ロジックが重いものを入れるとテスト全体のパフォーマンス悪化や理解コスト上昇につながります。
    簡潔なスコープ制御・設定/復元処理に留めるのが望ましいです。

  1. 参考情報 (あれば)

#50320 Tag Builders: render keywords as dasherized HTML attributes

マージ日: 2026/5/5 | 作成者: @seanpdoyle

  1. 概要 (1-2文で)
    Rails の tag ヘルパー等で、属性にネストした Hash を渡したときに、キーを「ダッシュ区切り(dasherize)した HTML 属性名」として展開して出力できるようになりました。これにより、Stimulus/Turbo の data-* だけでなく、Alpine.js の x-* や htmx の hx-* 形式の属性も、Ruby 側で自然なハッシュ記法のまま生成しやすくなっています。

  1. 変更内容の詳細(サンプルコードを含めて)

2-1. ネストした Hash を HTML 属性に展開する挙動の追加

これまでの Rails では、data: については data: { controller: "..." }data-controller="..." のような展開がありましたが、hx:x: など任意のプレフィックスに対して、かつ「任意にネストした Hash」を属性に落とし込むような標準サポートはありませんでした。

この PR により、tag ビルダー (例: tag.div, tag.button) で、次のようなコードが書けます:

ruby
tag.button "POST to /clicked",
  hx: { post: "/clicked", swap: :outerHTML, data: { json: true } }

出力は以下のようになります:

html
<button
  hx-post="/clicked"
  hx-swap="outerHTML"
  hx-data="{&quot;json&quot;:true}"
>
  POST to /clicked
</button>

ポイント:

  • トップレベルの hx: キーが「プレフィックス」として扱われる
  • その中の Hash のキーは dasherize されて、hx-post, hx-swap, hx-data という属性名になる
    • posthx-post
    • swaphx-swap
    • datahx-data
  • 値がさらにネストした Hash (data: { json: true }) の場合、適切に JSON へシリアライズされ、属性値としてエスケープされて格納される
    hx-data="{&quot;json&quot;:true}"

Stimulus/Turbo でおなじみの:

ruby
tag.div data: { controller: "users", action: "click->users#save" }
# => <div data-controller="users" data-action="click->users#save"></div>

というスタイルが、hx:x: にも自然に拡張できるイメージです。

2-2. 実装箇所

  • actionview/lib/action_view/helpers/tag_helper.rb
    • Tag ビルダーで、属性ハッシュを処理するロジックに、任意のネスト Hash を「ダッシュ区切り属性名」に展開する処理が追加。
    • 具体的には、hx: { foo_bar: ... } のような場合に hx-foo-bar="..." という属性に変換するような仕組み。

2-3. テストの追加

以下にテストが追加されています。

  • actionview/test/template/tag_helper_test.rb
    • tag ヘルパー単体で、ネスト Hash → 属性展開の挙動を確認するテストが追加。
  • actionview/test/template/form_helper_test.rb
  • actionview/test/template/form_helper/form_with_test.rb
    • form_tagform_with 経由でも同様の属性構築が期待通り動くことを確認するテストが追加。

2-4. CHANGELOG の更新

  • actionview/CHANGELOG.md
    • Action View に「タグビルダーがネストした Hash から dasherized な HTML 属性を生成できるようになった」という新機能として記載。

  1. 影響範囲・注意点
  • 対象範囲

    • ActionView::Helpers::TagHelper を利用しているすべてのコード
      • tag.div, tag.button, form_with, form_for, form_tag 内で生成される各種タグの属性処理に影響します。
  • 後方互換性

    • 既存の data: { ... } の挙動はそのまま維持されつつ、より一般的なプレフィックス (hx:, x: 等) にも同様の変換ロジックが適用される形です。
    • 通常の単層 Hash (class: "btn", hx_post: "/..." など) は従来通り動作します。
      ネストさせたときにのみ、新しい展開ロジックが効きます。
  • 注意点・設計上のポイント

    • 「トップレベルのキー」(例: hx:) と「その値の Hash のキー」(例: post, swap, data) を組み合わせて属性名を構築する前提になっています。
    • ネストがさらに深くなる場合 (hx: { data: { json: { foo: :bar } } }) については、基本的に JSON シリアライズされるため、「プレフィックス + さらにダッシュでつなぐ」形ではなく「1 属性の JSON 値」として表現されます。この点は data: { foo: { bar: :baz } } と同じ考え方に沿うと考えられます。
    • Hash のキーは dasherize されるため、foo_barfoo-bar になります。JS 側の属性名と揃えるために、キー名・キャメルケース/スネークケースの扱いには注意してください。

  1. 参考情報 (あれば)
ruby
tag.div x: { data: { foo: :bar }, on: { click: "doSomething()" } }
# => <div x-data="{&quot;foo&quot;:&quot;bar&quot;}" x-on-click="doSomething()"></div>

このように、サーバーサイドで Ruby の Hash を自然に書きつつ、各種フロントエンドフレームワーク用の属性を柔軟に生成できるようになった、というのが本 PR の主旨です。


#57240 Move generated kamal gem into development group

マージ日: 2026/5/5 | 作成者: @jaykava

  1. 概要 (1-2文で)
    Rails のアプリケーション生成時に自動で追加される kamal gem を、トップレベルではなく group :development 内に移動する変更です。これにより、Kamal が本番環境の Bundle からデフォルトで外れ、開発時専用のデプロイツールとして扱われます。

  1. 変更内容の詳細

背景・動機

  • 現状の rails new では、生成される Gemfile に以下のような行がトップレベルに入ります:
    ruby
    gem "kamal", require: false
  • その結果、「デプロイ用ツールである Kamal」が、本番環境用の bundle install --without development test などをしていても、トップレベル依存として本番バンドルに含まれてしまう問題がありました。
  • この PR は「Kamal は開発時のデプロイツールであり、本番コードの実行に不要」という前提に沿って、Gemfile 上の位置づけを見直しています。

Gemfile テンプレートの変更

変更対象: railties/lib/rails/generators/rails/app/templates/Gemfile.tt

  • これまで:
    ruby
    # 例: トップレベルに kamal
    gem "kamal", require: false
  • 変更後: group :development 内に移動
    ruby
    group :development do
      gem "kamal", require: false
    end

実際には他の development 向け gem(web-console, listen など)と同じグループに Kamal が含まれる形になります。
これにより、RAILS_ENV=production bundle install などで development グループを除外した際、Kamal はインストール対象から外れます。

ジェネレータの挙動とテスト

変更対象テストファイル:

  • railties/test/generators/app_generator_test.rb
  • railties/test/generators/api_app_generator_test.rb

テストで担保しているポイント:

  1. 通常のアプリ生成 (rails new my_app)
    • 生成された Gemfile において、Kamal が group :development 内に含まれていることを確認。
  2. API モードのアプリ生成 (rails new my_app --api)
    • API アプリでも同様に Kamal が development グループに入っていることを確認。
    • 既存の API 用 Gemfile 形状(特に assets 周りを含む Kamal の設定)との整合性をテスト。
  3. --skip-dev-gems オプション
    • rails new my_app --skip-dev-gems では、development グループ用の gem が Gemfile に含まれないため、Kamal も Gemfile に現れないことを確認。
  4. --skip-kamal オプション
    • 既存オプションの挙動維持: rails new my_app --skip-kamal を指定した場合は、Kamal 自体が Gemfile に出現しないことを確認。

開発者向けに想定される生成結果イメージ:

ruby
# Gemfile (抜粋)

# 通常の gem 群
gem "rails", "~> 7.2.0"
gem "pg"
# ...

group :development do
  gem "kamal", require: false
  # その他 development 用 gem
end

  1. 影響範囲・注意点

影響範囲

  • 新しく rails new で生成するアプリケーションのみが対象です。
  • 既存アプリの Gemfile は自動で書き換えられませんが、同じ問題(Kamal がトップレベルにある)が気になる場合、自分で group :development に移動しても良いです。
  • 通常/ API モードいずれのアプリ生成でも、Kamal は「開発グループの gem」として扱われます。

注意点

  1. 本番環境で Kamal を実行したい場合
    • サーバ上でデプロイ作業を行う際に bundle exec kamal ... を使う運用を想定している場合、production 環境で bundle install --without development のようにしていると Kamal がインストールされません。
    • その場合:
      • デプロイ用の環境(ローカルCIサーバなど)だけは development グループも含めて bundle install する
      • もしくは「アプリ実行環境」と「Kamal 実行環境」を分ける(ローカル or CI で Kamal からリモートにデプロイする) などの運用設計が必要です。
  2. --skip-dev-gems / --skip-kamal の組み合わせ
    • --skip-dev-gems を付けると Kamal を含む全ての development グループ gem が除外されます。
    • --skip-kamal は Kamal 単体の除外用であり、development グループの他の gem には影響しません。
    • これらの挙動は引き続き既存通りであることがテストで担保されています。

  1. 参考情報 (あれば)
  • 対応 issue: #57205 — 「Kamal が本番 bundle に含まれてしまう」問題への対応 PR。
  • 実行されたテストコマンド:
    • 一般アプリのジェネレータテスト:
      bash
      bundle exec ruby -Irailties/test \
        railties/test/generators/app_generator_test.rb \
        -n "/kamal|skip_dev_gems/"
    • API アプリのジェネレータテスト:
      bash
      bundle exec ruby -Irailties/test \
        railties/test/generators/api_app_generator_test.rb \
        -n "/test_api_modified_files|test_kamal_deploy_yml_excludes_asset_path_for_api_apps/"
  • 変更ファイル:
    • Gemfile テンプレート: railties/lib/rails/generators/rails/app/templates/Gemfile.tt
    • ジェネレータテスト:
      • railties/test/generators/app_generator_test.rb
      • railties/test/generators/api_app_generator_test.rb

#57301 Document _came_from_user? and _for_database attribute method suffixes [ci skip]

マージ日: 2026/5/5 | 作成者: @joaomarcos96

  1. 概要 (1-2文で)
    Rails の ActiveRecord::AttributeMethods::BeforeTypeCast が内部的に提供している *_for_database*_came_from_user? という属性メソッドサフィックスについて、公式ドキュメントに追記した PR です。挙動の変更は一切なく、ドキュメントとコメントの整備のみです。

  1. 変更内容の詳細(あればサンプルコードも含めて)

対象となるモジュール

ActiveRecord::AttributeMethods::BeforeTypeCast では、以下のように3つのサフィックス付き属性メソッドを定義しています:

ruby
attribute_method_suffix "_before_type_cast", "_for_database", parameters: false
attribute_method_suffix "_came_from_user?", parameters: false

従来は _before_type_cast だけが rdoc に説明されており、_for_database / _came_from_user? は、コードを読まない限りほぼ知られない状態でした。

追加されたドキュメントの要点

この PR では、モジュールレベルの rdoc に下記 2 つのサフィックスの説明と、Book モデル+enum を用いた簡単な例が追加されています。

*_for_database

  • 対象: book.status_for_database のように、属性名 + _for_database というメソッド
  • 役割:
    「その属性が実際に DB に保存される際の値」(=type キャスト・enum 変換などを適用した後の値)を取得するためのディスパッチ先
  • 元々存在していた API:
    • read_attribute_for_database(:status)
    • attributes_for_database からも利用できる機能だが、*_for_database というメソッド形式はドキュメント化されていなかった、という位置づけです。

イメージ例(enum を利用した Book モデル):

ruby
class Book < ApplicationRecord
  enum status: { draft: 0, published: 1 }
end

book = Book.new(status: :published)

book.status               # => "published" (enum のキー)
book.status_before_type_cast  # => "published" (元入力)
book.status_for_database      # => 1 (DB に保存される実際の整数値)

*_came_from_user?

  • 対象: book.status_came_from_user? のように、属性名 + _came_from_user? というメソッド
  • 役割:
    「その属性の現在の値が、ユーザー入力(属性代入)由来かどうか」を判定するためのディスパッチ先
  • これも内部的には以前から存在していましたが、コメントや rdoc に説明がなかったため、今回説明を追加しています。

イメージ例(フォーム入力などを想定):

ruby
book = Book.new

book.status_came_from_user?   # => false (まだ何も代入されていない)

book.status = 'published'

book.status_came_from_user?   # => true (ユーザー入力によってセットされた値)

※ 実際の判定ロジックは ActiveRecord の内部実装に依存しますが、概念としては「DB 読み込みやデフォルト値ではなく、アプリ側からの代入でセットされた値か」を知るためのフラグです。

コメントの追加

内部メソッドに、他の _before_type_cast と同様のコメントが追加されました。

  • attribute_for_database に:
ruby
# Dispatch target for *_for_database attribute methods.
  • attribute_came_from_user? に:
ruby
# Dispatch target for *_came_from_user? attribute methods.

これにより、*_for_database / *_came_from_user? がどのメソッドにディスパッチされるのか、コードリーディング時にも分かりやすくなっています。


  1. 影響範囲・注意点
  • 挙動変更なし
    既存の動作・API に一切変更はありません。
    *_for_database / *_came_from_user? メソッドは以前から利用可能であり、今回の PR はそれを公式に文書化しただけです。

  • ドキュメント依存のユーザーにとっての影響

    • これまで「知られざる内部 API」っぽかった status_for_database / status_came_from_user? などが、公式にサポートされている振る舞いとして分かりやすくなります。
    • enum・独自 type・暗号化 attribute などを扱うときに、
      • 「DB に実際に保存される値」を直接確認したい
      • 「これはユーザー入力起点の値かどうか」を判別したい
        といった場面で、これらのメソッドを安心して利用しやすくなります。
  • 将来の互換性の観点

    • ドキュメントに明示されたことで、「半ば内部実装」に見えていたこれらの方法が、よりパブリックな契約に近づいたと解釈できます。
    • gem やアプリケーションがこれらに依存しても、将来的に破壊的変更になりにくくなる、という意味でプラスです。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57301
  • 関連する API:
    • ActiveRecord::AttributeMethods::BeforeTypeCast
    • Model#attribute_before_type_cast
    • Model#read_attribute_for_database(name)
    • Model#attributes_for_database

#57129 Use Hash#each_key instead of Hash#keys.each

マージ日: 2026/5/5 | 作成者: @ashwin47

  1. 概要 (1-2文で)
    Rails内部で、Hash#keys.each を使っていた箇所の一部を Hash#each_key に置き換え、余計な配列生成を避けることでパフォーマンスとメモリ効率をわずかに改善するリファクタリングです。ハッシュを反復中に変更しない箇所のみを安全に置き換えており、挙動の変更はほぼありません。

  1. 変更内容の詳細

背景・意図

  • Hash#keys.each は以下のようなコードです:

    ruby
    hash.keys.each do |key|
      # ...
    end

    これは

    1. hash.keys で「全キーを配列にコピー」
    2. その配列に対して each でループ

    という2段階処理になり、中間配列の生成コスト(メモリ・GC)が発生します。

  • 一方、Hash#each_key は:

    ruby
    hash.each_key do |key|
      # ...
    end

    のように、ハッシュ内部構造を直接たどってキーを反復するため、中間配列を生成せずに済みます。

  • PR #17099 でも同種のクリーンアップが行われており、その続きとして今回のPRが入っています。

実際の変更点

PR本文では以下2ファイルと書かれていますが、今回の diff では3ファイルが変更対象として挙がっています(実際のコード差分は1行ずつの軽微な修正):

  • activemodel/lib/active_model/schematized_json.rb
  • activerecord/test/cases/connection_adapters/connection_handler_test.rb
  • activesupport/test/env_configuration_test.rb

いずれも本質的には同じパターンの置き換えです:

変更前(イメージ):

ruby
hash.keys.each do |key|
  # ハッシュはこのループ中に変更されない
end

変更後:

ruby
hash.each_key do |key|
  # ハッシュはこのループ中に変更されない
end

重要なのは、「ループ中に delete などでハッシュを破壊的に変更しない」箇所だけが置き換え対象になっている点です。

置き換え対象に していない 箇所

PR説明中では、以下の3箇所はそのまま keys.each が残されていると明記されています:

  • hash/keys.rb
  • strong_parameters.rb
  • routing/mapper.rb

これらではループ内で delete などによりハッシュを変更しているため、先に keys でスナップショット(配列コピー)を取ってから反復する必要があるケースです。このような場合に each_key に変えると、繰り返し中に対象ハッシュが変化してバグや予期せぬ動作を招く可能性があるため、意図的に残されています。


  1. 影響範囲・注意点
  • 影響範囲は内部実装のパフォーマンスに限られ、外部APIの仕様変更や互換性破壊はありません。

  • 変更対象となった箇所ではループ中にハッシュを変更しておらず、挙動は keys.each と論理的に等価です。

  • メモリ使用量がごくわずかに減り、GC負荷もわずかに軽くなる可能性がありますが、多くのアプリケーションでは体感できない程度の微小な改善です。

  • 自分のアプリ・ライブラリ側でも、以下のようなループは each_key / each_value / each_pair に置き換え可能か検討できます:

    ruby
    # 変更前
    hash.keys.each do |key|
      # ループ中に hash を変更しない
    end
    
    # 推奨パターン
    hash.each_key do |key|
      # ...
    end

    ただし、ループ中に hash.delete(key)hash[some_key] = ... 等で構造を変える場合は、従来どおり hash.keys.each でスナップショットを取る方が安全です。


  1. 参考情報 (あれば)

#55826 Add conflicting record ID to validates_uniqueness_of error details

マージ日: 2026/5/5 | 作成者: @bvicenzo

  1. 概要 (1-2文で)
    validates_uniqueness_of / validates :field, uniqueness: true の検証に失敗した場合、errors.details に「衝突している既存レコードの主キーID(existing_id)」が含まれるようになりました。これにより、どのレコードと一意制制約がぶつかったかをアプリ側で特定しやすくなります。

  1. 変更内容の詳細

2-1. 何が変わったか

ActiveRecord の ActiveRecord::Validations::UniquenessValidator の重複チェック実装が以下のように変更されています。

  • 以前:
    検証対象と同じ値を持つレコードが存在するかを .exists? で確認していた
    → 存在するかどうか (true/false) しか分からない

  • 変更後:
    .pick(klass.primary_key) を使って、衝突している既存レコードの主キー値を取得する
    → 存在する場合は、その ID を errors.details:existing_id として格納

具体的には、バリデーションが失敗したときのエラー詳細が次のように変わります。

変更前

ruby
class Topic < ApplicationRecord
  validates :title, uniqueness: true
end

topic = Topic.new(title: "Existing Topic")
topic.valid? # => false

topic.errors.details[:title]
# => [{ error: :taken, value: "Existing Topic" }]

変更後

ruby
topic.errors.details[:title]
# => [{ error: :taken, value: "Existing Topic", existing_id: 1 }]

existing_id には、衝突しているレコードの primary_key(通常は id)が入ります。
スコープ付き一意性 (validates :title, uniqueness: { scope: :user_id } など) に対しても同様に適用されます。

2-2. 実装上のポイント

  • DB問い合わせ部分で .exists?.pick(klass.primary_key) に変更。
    • どちらも LIMIT 1 で 1 行だけ取得するクエリになるため、パフォーマンス特性はほぼ同等。
    • .pick は該当行があれば主キーIDを返し、なければ nil を返す。
  • 衝突レコードが見つかった場合のみ、errors.details の要素ハッシュに existing_id: <主キー値> を追加。
  • I18n 関連・ユニークネス関連のテストがこの新しい振る舞いに合わせて更新されています。
  • activerecord/CHANGELOG.md に仕様変更として追記済み(公式にサポートされる振る舞い)。

2-3. 利用イメージ

JSON API で 409/422 エラーに「どのレコードと衝突したか」を載せたいケースなどで、そのまま使いやすくなります。

ruby
# 例: API レスポンス整形
record.valid?
if record.errors.added?(:email, :taken)
  existing_id = record.errors.details[:email].first[:existing_id]

  render json: {
    error: "email_taken",
    message: "Email is already taken by another user",
    conflict_user_id: existing_id,
    conflict_user_url: user_url(existing_id)
  }, status: :unprocessable_entity
end

  1. 影響範囲・注意点
  • errors.details のスキーマが変わる
    一意性バリデーションエラーの details 要素に :existing_id キーが増えます。
    既存コードで errors.details[:attr].first をそのままシリアライズしている場合、追加フィールドがクライアントに見えるようになります。

    • もし既存クライアントが「キーを厳密にチェックしている」場合は、API 仕様として許容するか、サーバ側でフィルタリングが必要な可能性があります。
  • セキュリティ / 情報漏えい面の検討

    • existing_id によって、既存レコードの ID 情報がクライアントに渡せるようになります。
    • 自分以外のリソースIDをユーザーに見せたくないケース(マルチテナントでID自体を秘匿したい、など)の場合は、レスポンス整形で existing_id を露出しないようにする必要があります。
    • 逆に「ID をキーにしたリンクを返したい」など、積極的に ID を利用したい API では便利になります。
  • パフォーマンス
    ベンチマーク(SQLite, Ruby 3.4.6, AR 8.1.0.beta1)では:

    • .exists? : 約 9.6k i/s
    • .pick(:id): 約 9.7k i/s
      と誤差範囲内の差で、実質的なパフォーマンス劣化はほぼ無しとみなせます。
  • 主キーがカスタムな場合

    • klass.primary_key を使用しているため、id 以外を主キーにしているモデルでも、その主キー値が existing_id として格納されます。
    • existing_primary_key ではなく existing_id というキー名である点は注意(意味的には「existing recordの主キー」)。

  1. 参考情報 (あれば)

#57247 Use ** in .dockerignore template for recursive directory exclusions

マージ日: 2026/5/5 | 作成者: @bogdan

  1. 概要 (1-2文で)
    Rails が rails new --docker などで生成する .dockerignore テンプレートを修正し、/log/*/tmp/* といった「1階層だけマッチするパターン」を /log/** のような「再帰的にすべてのサブディレクトリを含めてマッチするパターン」に変更した PR です。これにより、ログ・tmp・storage などの配下にある深い階層のファイルも Docker build context から確実に除外されるようになります。

  1. 変更内容の詳細

具体的な変更

.dockerignore テンプレート (railties/lib/rails/generators/rails/app/templates/dockerignore.tt) が以下のように変更されています。

パターンの変更

単一階層マッチ * → 再帰マッチ ** に変更:

diff
- /log/*
- /tmp/*
- /storage/*
- /app/assets/builds/*
+ /log/**
+ /tmp/**
+ /storage/**
+ /app/assets/builds/**

Docker の .dockerignore では:

  • dir/*dir 直下のファイル/ディレクトリのみマッチ
  • dir/**dir 配下のあらゆる階層のファイル/ディレクトリを再帰的にマッチ

となるため、** にすることで、深い階層のファイルも確実に build context から除外されます。

冗長なエントリの削除

もともと .dockerignore には、/tmp 配下を個別に除外する行がありましたが、/tmp/** を導入したことで冗長になったため削除されています。

diff
- /tmp/pids/**
- /tmp/storage/**

/tmp/**/tmp/pids/tmp/storage の中身もすべてカバーするためです。

.keep の例外指定を整理

tmp 以下に置かれた .keep ファイルを context に含めるための否定パターン(除外ルールの例外)をまとめ直しています:

dockerignore
/tmp/**
!/tmp/.keep
!/tmp/pids/.keep
!/tmp/storage/.keep
  • /tmp/** ですべてを除外
  • その後 ! プレフィックスで .keep ファイルだけを再度含める(.dockerignore の挙動: 後勝ち)

という形で、tmp 配下のディレクトリ構造を保ちつつ、中身の実データは送らないようにしています。


  1. 影響範囲・注意点

どこに影響するか

  • Rails が生成する .dockerignore が変わるため、今後 rails new で Docker 対応のアプリを作成したときのデフォルト挙動が変わります。
  • 既存プロジェクトには自動で適用されないので、恩恵を受けたい場合は自分で .dockerignore を更新する必要があります。

具体的に改善される点

旧パターンのままだと除外されていなかった代表例:

  • tmp/cache/assets/sprockets/v4.0.0/ab/abcdef1234...
    → Sprockets のアセットキャッシュが深くネストされており、/tmp/* では除外しきれなかったが、/tmp/** により除外される。
  • tmp/pids/server.pid
    → サーバ PID ファイルも tmp/pids/ 以下のため、旧パターンでは build context に含まれうる。
  • storage/ab/cd/abcdefghijklmn
    → Active Storage の Disk サービスはキーをプレフィックスで2階層に分けて保存するため、ユーザのアップロードファイルが Docker イメージの build context に含まれてしまう可能性があった。

これらがすべて .dockerignore により除外されるようになります。

結果として:

  • Docker build context が小さくなり、ビルド時間・アップロード時間(リモートビルドの場合)が短縮される。
  • 誤ってユーザーのアップロードファイルやキャッシュ・PID などを Docker イメージに含めてしまうリスクが下がる。

注意点

  • .dockerignore の挙動は .gitignore と完全に同じではありません。特に ** の再帰マッチは Docker 固有の挙動です。
    カスタマイズしている場合は、Docker ドキュメントを前提にパターンを確認してください。
  • 既存プロジェクトで手動マージする際:
    • すでに /tmp/** などを独自に書いている場合は重複しないように統合する。
    • /tmp 配下であえて含めたいファイル(例: 独自に何か生成しているもの)がある場合は、!/tmp/your_file のように例外指定が必要です。

  1. 参考情報 (あれば)

#57208 Add ActiveJob::Attributes to persist data between steps

マージ日: 2026/5/5 | 作成者: @bdewater-thatch

  1. 概要 (1-2文で)
    Rails 8.1で導入された Active Job Continuation(マルチステップジョブ)に対して、ステップ間で一時的なデータを安全に保持できる ActiveJob::Attributes が追加されました。Active Model Attributes をベースに、ジョブのシリアライズ/デシリアライズを意識せずに型付き属性を使って値を永続化できます。

  1. 変更内容の詳細

ActiveJob::Attributes の追加

  • 新モジュール ActiveJob::Attributes が追加されました。
  • 中身は基本的に ActiveModel::Attributes の API をそのまま使えるようにしたものです。
    • attribute DSL による宣言
    • 型付き属性 (:string, :integer, :datetime, 独自 type など)
    • デフォルト値、lazy default(Proc)、cast、serialize など Active Model Attributes の機能を利用可能

イメージとしては「Active Job 版 Active Model Attributes」で、ジョブ引数とは別に「ジョブの状態を持つための属性レイヤー」を提供するものです。

Continuable へのデフォルト組み込み

  • ActiveJob::Continuableinclude ActiveJob::Attributes が追加されています。
  • つまり、Continuation を使ったマルチステップジョブでは、特別な設定なしに attribute が使えます。
rb
class EnrollmentJob < ApplicationJob
  include ActiveJob::Continuable

  # ステップ間で保持したいデータ
  attribute :payment_token, :string
  attribute :user_metadata, :json

  step :tokenize_payment
  step :charge_payment

  def tokenize_payment(step)
    self.payment_token = PaymentGateway.tokenize(enrollment.user.payment_instrument)
    # この時点で payment_token はジョブのペイロードに埋め込まれる
    step.advance!
  end

  def charge_payment(step)
    # 再キューイング後でも payment_token が復元されている
    PaymentGateway.charge(token: payment_token)
    step.advance!
  end
end

上記のように、step の間で payment_token をインスタンス変数や外部テーブルに自前保存することなく、安全に保持できます。

既存案(step の戻り値を自動保存)を採用しなかった理由

PR本文で触れられているが、以下のような API は採用されていません:

rb
payment_token = step(:tokenize_payment_instrument, store: true) do
  PaymentGateway.tokenize(enrollment.user.payment_instrument)
end

この案を見送った理由:

  • attribute ベースの方がシンプルかつ明示的である
  • Continuable がカーソルを step.cursor / set! / advance! で明示的に扱っている現状の API と整合的
    (フレームワークが「勝手に戻り値を保存する」より、「属性として明示的に状態を持つ」方針に揃えている)

Active Model 側の微調整

  • activemodel/lib/active_model/attributes.rb にも 1 行変更があり、ActiveJob::Attributes からの利用に必要な小さな調整が入っています(主に include まわりの都合付けで、既存利用への影響は極小と考えられます)。

  1. 影響範囲・注意点
  • 対象範囲

    • 新機能なので既存ジョブはそのまま動作します。
    • ActiveJob::Continuable を使うジョブでは、デフォルトで ActiveJob::Attributes が使えるようになります。
    • Continuable を使っていない通常の Active Job でも、include ActiveJob::Attributes すれば利用可能です。
  • 使いどころ

    • マルチステップ処理の「中間状態」を一時的に保持したいが、DB のカラム追加や専用テーブル作成まではしたくないケース。
    • 例:
      • 外部 API のトークン、セッション情報
      • ステップ 1 で計算した一時的な集計値
      • 後続ステップでのみ必要な、生存期間の短いデータ
  • ジョブ引数との違い

    • ジョブ引数は「ジョブ作成時に渡すもの」であり、通常は immutable な前提で扱われます。
    • ActiveJob::Attributes は「ジョブのライフサイクルの中で変化する“状態”」を持つための仕組みです。
    • ジョブを再キューイング・再開しても、これらの属性はシリアライズ/デシリアライズされて維持されます。
  • シリアライズサイズへの配慮

    • 属性はジョブのペイロードに載るため、扱うデータはできるだけコンパクトなものに限定するべきです。
    • 大きなオブジェクト・バイナリなどを保存すると、キューシステム(Sidekiq, Resque など)のメッセージサイズ制限に引っかかる可能性があります。
    • 「必要な識別子だけ属性に保存し、実データは再度取得する」ような設計が推奨されます。
  • 型・変換の注意

    • Active Model Attributes の型システムに依存するため、
      • カスタム type を使うなら、Active Model 側で type を定義してから利用する
      • JSON や Array、Hash などもサポートされるが、シリアライズ表現(JSON / String 化)に注意する
        といった点は既存の Active Model Attributes と同様です。

  1. 参考情報 (あれば)

この PR により、Active Job のマルチステップ処理で「一時的だが、再開時にも必要な状態」を持つための公式なパターンが用意され、従来の serialize/deserialize のオーバーライドや独自ストレージ実装が不要になることが期待されます。


#56450 Fix ActionMailer::Base.mail dispatch in 8.1+

マージ日: 2026/5/5 | 作成者: @afurm

  1. 概要 (1-2文で)
    Rails 8.1 で ActionMailer::Base.mail をクラスメソッドとして直接呼び出した際に ActionNotFound が発生するリグレッションを修正し、8.0 と同じ挙動(クラスレベル配送)を復元する PR です。mail が ActionMailer の「アクション」として常に認識されるようにし、その挙動を担保する回帰テストが追加されています。

  1. 変更内容の詳細 (サンプルコード含む)

問題の背景

Rails 8.1 以降で、以下のようなコードが動かなくなっていました:

ruby
ActionMailer::Base.mail(
  to: "user@example.com",
  from: "no-reply@example.com",
  subject: "Hello",
  body: "Hi"
).deliver_now

従来(〜Rails 8.0)はこれが許可されていましたが、8.1 では mail が「有効なメールアクション(action_methods)」として扱われなくなり、Action Mailer の内部ディスパッチで ActionNotFound が発生するようになっていました。

修正内容

1) ActionMailer::Base の action_methods に常に mail を含める

actionmailer/lib/action_mailer/base.rb に変更が入り、ベースクラス (ActionMailer::Base) を直接使う場合、mail が常にアクションとしてみなされるようになりました。

イメージとしては、Action Mailer が action_methods を決定するロジックに「ActionMailer::Base に関しては mail を特別扱いして常に含める」という処理が入った形です。

その結果、ActionMailer::Base.mail 呼び出し時も内部的には通常の Action Mailer のアクションディスパッチフローに乗り、ActionNotFound が起きなくなります。

2) 回帰テストの追加

actionmailer/test/base_test.rb に、以下のようなテストが追加されています:

  • ActionMailer::Base.mail を直接呼び出して deliver_now / deliver_later できること
  • その呼び出しが Action Mailer のアクションディスパッチを通って成功すること(=ActionNotFound にならないこと)

テストは PR 説明にある通り、以下で実行確認されています。

bash
bundle exec ruby -Iactionmailer/test actionmailer/test/base_test.rb -n "/ActionMailer::Base.mail/"

利用イメージ

この PR 適用後は、Rails 8.1+ でも以下のような「簡易ワンショットメール送信」が再び安全に使えます。

ruby
# メイラークラスを定義しなくても、ベースクラスだけで送信可能
ActionMailer::Base.mail(
  to: "user@example.com",
  from: "no-reply@example.com",
  subject: "Quick mail",
  body: "This is a quick one-off email."
).deliver_now

  1. 影響範囲・注意点
  • 影響範囲

    • ActionMailer::Base.mail(...) をクラスメソッドとして直接呼んでいる既存コードに対して、Rails 8.1 で発生していた ActionNotFound が解消され、Rails 8.0 と同じ挙動になります。
    • 独自のメイラークラス(UserMailer < ApplicationMailer など)で、通常のアクション(def welcome_email ... end)を使っているケースには基本的に影響はありません。
  • 互換性

    • 明確に「8.0 の挙動を復元する」ことが目的のため、既存の 8.0 向けコードとの後方互換性を回復する変更です。
    • ActionMailer::Base を特殊な形で継承し、action_methods をカスタマイズしているような高度なメタプログラミングをしていない限り、副作用の可能性は低いと考えられます。
  • 注意点

    • PR 時点では CHANGELOG は更新されていないため、この挙動変更は一見分かりづらい可能性があります。Rails を 8.1 系に上げた際に ActionMailer::Base.mail 周辺が壊れた場合、この修正が入っているバージョンかを確認するとよいです。
    • ActionMailer::Base を直接使ったメール送信はあくまで「簡易用途」向けであり、大量のメール送信や複雑なロジックには従来通り独自のメイラークラスの定義を推奨します。

  1. 参考情報 (あれば)

#57288 Define as_json on ActiveStorage::Attached::One and Attached::Many

マージ日: 2026/5/5 | 作成者: @onyxblade

  1. 概要 (1-2文で)
    has_one_attached / has_many_attached の名前が実カラム(あるいは ignored_columns に入ったカラム)と衝突しているときに、record.to_jsonSystemStackError(無限再帰)が発生するバグを修正するPRです。ActiveStorage::Attached::One / Many に明示的な as_json を定義し、レコードの JSON シリアライズ時に安全に変換されるようにしました。

  1. 変更内容の詳細

問題の背景

以下のようなモデルがあるとします:

ruby
class Product < ApplicationRecord
  self.ignored_columns = [:photo]
  has_one_attached :photo
end

Product.select('*').first.to_json
# => SystemStackError: stack level too deep

ポイント:

  • DB 上には photo カラムが存在する。
  • Rails 側では self.ignored_columns = [:photo] で無視しつつ、同名で has_one_attached :photo を定義。
  • Product.select('*') により、無視されるはずの photo カラムも結果セットには含まれる。
  • その状態で record.to_json を呼ぶと、ActiveModel の JSON シリアライズ処理の途中で、Attached::One / Attached::Many のインスタンスが Object#as_json にフォールバック。
  • Object#as_jsoninstance_values(インスタンス変数をすべてハッシュ化)を返すため、その中に含まれる @record からまた as_json が呼ばれ…というサイクルが発生し、スタックオーバーフローになる。

スタックトレース上では、Attached::One / ManyObject#as_json を通ってしまうことが原因で、record へのバックリファレンスがシリアライズ対象になり無限再帰が発生していました。

対応内容

ActiveStorage::Attached::OneActiveStorage::Attached::Manyas_json を定義し、Object#as_json に落ちないようにする、というローカルな修正です。

Attached::One#as_json

  • 振る舞い:
    • 添付済みなら、そのアタッチメント(ActiveStorage::Attachment レコード)の as_json を返す。
    • 未添付なら nil を返す。

擬似コードイメージ:

ruby
# activestorage/lib/active_storage/attached/one.rb

def as_json(*)
  if attached?
    attachment.as_json
  else
    nil
  end
end

結果として、Product.first.to_json の中で photo が JSON 化されるときは、以下のような JSON に展開されます(概念的な例):

json
{
  "id": 1,
  "name": "Sample product",
  "photo": {
    "id": 10,
    "blob_id": 99,
    "record_id": 1,
    "record_type": "Product",
    "name": "photo",
    "created_at": "2026-05-05T12:34:56Z"
  }
}

Attached::Many#as_json

  • 振る舞い:
    • アタッチされている全ての ActiveStorage::Attachmentas_json を配列で返す。

擬似コードイメージ:

ruby
# activestorage/lib/active_storage/attached/many.rb

def as_json(*)
  attachments.map(&:as_json)
end

JSON は以下のような形になります(概念的な例):

json
{
  "id": 1,
  "name": "Sample product",
  "photos": [
    {
      "id": 10,
      "blob_id": 99,
      "record_id": 1,
      "record_type": "Product",
      "name": "photos",
      "created_at": "2026-05-05T12:34:56Z"
    },
    {
      "id": 11,
      "blob_id": 100,
      "record_id": 1,
      "record_type": "Product",
      "name": "photos",
      "created_at": "2026-05-05T12:35:56Z"
    }
  ]
}

なぜこれで安全なのか

  • ActiveStorage::Attachment は以下のようなスカラーカラムのみを持つシンプルなモデルです:
    • id, blob_id, name, record_id, record_type, created_at
  • これらのカラムには ActiveStorage::Attached::One / Many のような「レコード自身へのバックリファレンス」は含まれていないため、Attachment#as_json を呼び出しても再帰ループは発生しません。
  • そのため Attached::* インスタンスの JSON 化は、Attachment レコードの配列(または単一オブジェクト)に収束し、安全にシリアライズが完了します。

代替案として検討されたもの

PR説明で触れられている代替案:

  • ActiveRecord::Persistence#instantiate_instance_of の中で、ignored_columns に入っているカラムを row hash から取り除く。
    • これにより、select('*') を書いていても、ignored_columns に入っているカラムは attribute_names に現れないようにできる。
  • しかし、この方法は:
    • ユーザーが明示的に SQL で取得したカラムが「サイレントにドロップされる」挙動変更であり、影響範囲が広い。
    • ActiveStorage 以外にも「レコードをバックリファレンスする任意の Object サブクラス」には効かない。

そのため、より局所的で副作用の少ない「Attached::* プロキシに as_json を生やす」方針が選ばれています。

テスト・ドキュメント

  • activestorage/test/models/attached/one_test.rb
  • activestorage/test/models/attached/many_test.rb

にテストが追加され、今回の as_json の挙動が保証されています。

  • activestorage/CHANGELOG.md にも変更が記載され、挙動変更として明示されています。

  1. 影響範囲・注意点
  • 対象:

    • Active Storage を利用しているアプリケーションのうち、
      • has_one_attached / has_many_attached の名前が実カラム名と衝突している、
      • または ignored_columns と組み合わせてカラムを無視しつつ、同名で attachment を定義している、
      • さらに select('*') や特定の select で無視したはずのカラムを結果に含めてしまう、 といったケースで record.to_json を呼んでいるプロジェクト。
  • この PR により、これらのケースで SystemStackError が発生しなくなります。

  • JSON 形式の変化:

    • これまで to_json を呼んでもエラーで落ちていたようなケースでは、今回初めて JSON が正常に返ることになります。
    • has_one_attached のフィールドは「単一の Attachment オブジェクト or nil」、has_many_attached は「Attachment オブジェクトの配列」として JSON に含まれるようになります。
    • もし to_json / as_json をオーバーライドして独自形式を返したい場合は、アプリ側で as_json を追加実装する必要があります。
  • Rails の他部分への影響:

    • 修正箇所は Active Storage の Attached::One / Many に限定されており、ActiveRecord の汎用的なインスタンス生成や ignored_columns の扱いには手を触れていません。
    • そのため、ActiveRecord 全体の挙動変更のリスクは小さいです。

  1. 参考情報 (あれば)
  • 対応する issue: #57287
    has_one_attached / has_many_attached が属性名をシャドウするときの SystemStackError」を扱うものです。
  • 対象クラス:
    • ActiveStorage::Attached::One
    • ActiveStorage::Attached::Many
    • ActiveStorage::Attachment
  • 類似の問題を避けるための指針:
    • カラム名と ActiveStorage の attachment 名を可能な限り衝突させない。
    • どうしても同名にする必要がある場合でも、今回の修正により少なくとも to_json 時の無限再帰は解消されます。

#57162 Skip CreateUsers migration when User model already exists

マージ日: 2026/5/5 | 作成者: @johntopley

  1. 概要 (1-2文で)
    Rails の rails generate authentication で、すでに User モデルが存在する場合に CreateUsers マイグレーションを自動生成しないようにし、「table already exists」エラーを避ける修正です。既存アプリに後から認証機能を追加しやすくなる変更です。

  1. 変更内容の詳細

2-1. 挙動の変更点

これまで:

  • rails generate authentication User などを実行すると、
    • 常に db/migrate/xxxxxx_create_users.rb のような CreateUsers マイグレーションを生成
    • 既に users テーブルがある(=既存の User モデル+マイグレーションがある)状態で db:migrate を実行すると、
      • 「table already exists」エラーで失敗

今回の変更後:

  • ジェネレータ実行前に app/models/user.rb が存在するかをチェック
  • 存在する場合は CreateUsers マイグレーションの生成をスキップ
  • そのため、既存の users テーブルがあるアプリに後から authentication generator を適用しても、テーブル作成マイグレーションが重複せずに動く

2-2. コードレベルでの変更点 (概要)

railties/lib/rails/generators/rails/authentication/authentication_generator.rb にて:

  • User モデルファイルの存在チェックロジックを追加
    • 例: File.exist?(Rails.root.join("app/models/user.rb")) のような条件を導入し、真の場合は CreateUsers マイグレーションのテンプレート生成を行わないようにしている(正確なメソッド名・条件はPR本文からの推測だが、実装方針はこれ)
  • 既存のマイグレーション生成処理の前後に条件分岐を挿入し、「Userモデルがないときだけ create_users マイグレーションを生成」するように変更

railties/test/generators/authentication_generator_test.rb では:

  • 新しい挙動に対するテストが追加
    • app/models/user.rb がある状態でジェネレータを実行したときに CreateUsers マイグレーションが生成されないことを検証
    • User モデルがない状態では従来どおりマイグレーションが生成されることも担保

railties/CHANGELOG.md:

  • 上記の仕様変更が CHANGELOG に追記され、振る舞いの変更として明文化されています。

  1. 影響範囲・注意点
  • 影響対象:
    • Rails 付属の authentication generator を利用しているプロジェクト
    • 既存アプリに後から rails generate authentication User を適用するケースで特に恩恵がある
  • 注意点:
    • 今回の判定は「app/models/user.rb が存在するかどうか」で行われているため、
      • テーブル名は users だがモデルファイルが別名/別ディレクトリにある、
      • あるいは User モデルがあってもファイル名が慣例どおりでない
        といった特殊な構成をしている場合は、期待どおりに判定されない可能性があります。
    • 逆に、app/models/user.rb だけ存在していて users テーブルがまだない場合は、マイグレーションが生成されずテーブルが作成されないので、そのような構成をとる場合は手動でマイグレーションを用意する必要があります。
  • バックワード互換性:
    • 一般的な Rails アプリ(User モデル=app/models/user.rb、テーブル=users)では、既存の挙動が「壊れる」ことは基本的になく、むしろ二重マイグレーションのエラーが防止される方向の変更です。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57162
  • 閉じられた Issue(動機となったバグ報告): #57149
    • 「既に User モデルと users テーブルが存在するアプリで authentication generator を使うと、db:migrate で ‘table already exists’ が発生する」という報告に対応した変更です。

#57298 Fix eager_load for CPK models with explicit select

マージ日: 2026/5/5 | 作成者: @fatkodima

  1. 概要 (1-2文で)
    複合主キー(CPK: Composite Primary Key)を持つモデルに対して、select を明示したクエリに eager_load を使うと正しく動かない不具合が修正されました。eager_load 時の主キー列の扱いを見直し、CPK モデルでも関連を正しくプリロードできるようになっています。

  1. 変更内容の詳細

※ パッチ自体は非常に小さく、join_dependency.rb の条件分岐を 1 行変更し、それに対応するテストを eager_test.rb に追加したものです。実際のコード断片はリポジトリを直接参照する必要がありますが、変更の意図と動きは次のように整理できます。

背景

  • ActiveRecord の eager_load は、関連を 1 クエリで取得するために LEFT OUTER JOIN を使います。
  • その際、関連付けの整合性をとるために、主キー列(id など)を必ず SELECT 句に含めるロジックがあります。
  • しかし、複合主キーを持つモデル(内部的には primary_key が複数列)で、かつ select を明示的に指定している場合、
    • 必要な複合主キーのカラムが SELECT に含まれない/含め方が不適切
    • → eager_load の JOIN 結果を元にレコードを組み立てる処理で不整合・エラーが生じる
      という問題 (#57296) がありました。

join_dependency.rb の修正

ActiveRecord::Associations::JoinDependency には、eager load 用クエリに含めるカラムを決定するロジックがあります。
今回の修正は:

  • 「CPK モデル + explicit select」というケースで
    • そのモデルの すべての主キーカラムを必ず SELECT に含める
  • という条件付けが正しく働くよう、条件式を微調整したものです。

イメージとしては、次のような動きをするようになったと考えてください(擬似コード):

ruby
# 擬似コードレベルのイメージ
if using_eager_load?
  # 以前は単一主キー前提 or 条件が甘く、CPK + select 時に不足
  columns_to_select += Array(klass.primary_key)  # CPK の場合は ["key1", "key2"] など
end

これにより、たとえば以下のようなケースが正しく動くようになります。

ruby
# モデル Post が複合主キー (year, code) を持っているとする
Post.select(:title)            # 明示的 select
    .eager_load(:comments)     # 関連を eager_load
    .to_a

以前は yearcode が SELECT 句に含まれず、eager_load が期待通りのレコード構築を行えないことがありましたが、修正後は内部的に主キー列も自動で SELECT に追加されます。

テストの追加

activerecord/test/cases/associations/eager_test.rb に 11 行のテストが追加されています。内容の趣旨は次の通りです。

  • 複合主キーを持つモデルを用意
  • select で一部カラムのみを指定しつつ eager_load を使う
  • 結果がエラーにならず、関連が正しく読み込まれることを検証

このテストが、回 regress しないことを保証する役割を持ちます。


  1. 影響範囲・注意点

影響範囲

  • 対象:
    • ActiveRecord で複合主キーを扱っているアプリケーション
    • かつ、そのモデルに対して select を明示しつつ eager_load を使っているコード
  • 具体的な影響:
    • 以前は
      • エラーが起きる
      • もしくは、関連が正しくロードされない/結果が不完全になる
    • といった挙動が出ていた箇所が、正常に動作するようになります。
    • 内部的には SELECT されるカラムに主キー列(複合主キーの全列)が追加されます。

注意点

  • 明示的に select を使っていて、「本当にそのカラムしか SELECT したくない」という前提でコードを書いていた場合でも、
    • eager_load を使う限り、内部的には主キー列が追加で SELECT されます。
    • これは単一主キーのときも同様の設計思想であり、CPK でもそれに揃えた形です。
  • そのため、
    • 「SELECT 対象カラムを厳密に制御してパフォーマンスを最適化したい」
    • 「SELECT されるカラム数を完全にコントロールしたい」 といったユースケースでは、eager_load ではなく includes + references / preload を使う設計を検討したほうがよい場合があります。

  1. 参考情報 (あれば)
  • 関連 Issue: #57296
  • 変更ファイル:
    • activerecord/lib/active_record/associations/join_dependency.rb
    • activerecord/test/cases/associations/eager_test.rb
  • キーワード:
    • Composite Primary Key (CPK)
    • eager_load
    • 明示的 select
    • ActiveRecord::Associations::JoinDependency

#57255 Update AGENTS.md to remove PR reference

マージ日: 2026/5/5 | 作成者: @nateberkopec

  1. 概要 (1-2文で)
    このPRは、AGENTS.md に含まれていた特定のプルリクエスト(PR)への言及を削除する、ドキュメントのみの微修正です。Rails本体の機能や挙動には一切変更がありません。

  1. 変更内容の詳細
  • 対象ファイル: AGENTS.md
  • 差分: 1行削除(追加行なし)

AGENTS.md は、Rails 内で利用される「エージェント」に関するドキュメント(例: User-Agent や各種クライアント識別、もしくは内部的な“agent”コンポーネントの解説)をまとめたファイルと考えられます。
今回削除されたのは、その中にあった「特定の PR 番号(あるいは PR への直接リンク)を参照する一文」です。

説明文にある:

Maybe I'm missing something, but this seems way too specific to be intentional.

というコメントから、この PR 参照は意図せず紛れ込んだか、あるいは履歴上のごく一時的な情報をドキュメントに固定してしまっていたため、「ドキュメントとしては不自然なほど具体的すぎる」ので削除した、という判断です。

擬似的なイメージとしては、以下のような行が削除されたと考えられます(実際の文面は異なる可能性があります):

diff
- See PR #12345 for more details about this behavior.

あるいは、もっと具体的なケース:

diff
- This behavior was introduced in PR #56789 and should only affect FooBar agents.

Rails のガイドライン的にも、ユーザ向けドキュメントに「開発時の個別 PR の履歴」を直接書き込むことはあまり望まれず、安定した概念・API・設定方法に焦点を当てるべきなので、その整理の一環と見てよいです。


  1. 影響範囲・注意点
  • 機能影響: なし
    • ソースコード・設定・API などへの変更は一切なく、あくまでドキュメント行削除のみです。
  • 互換性への影響: なし
    • アプリケーションコードを修正する必要も、Rails のバージョンアップにおける互換性問題も発生しません。
  • 開発者視点の注意点:
    • AGENTS.md を直接参照していた場合、その中にあった「特定の PR に飛んで詳細を見る」という導線がなくなります。
    • ただし、その情報は主に内部開発者・コントリビュータ向けの履歴情報であり、通常のアプリケーション開発者にとってはほぼ影響ありません。
    • 今後、ドキュメントに履歴的な情報を書く場合は「PR番号を直接書く」よりも、「その機能が何をするのか」「いつから利用可能か(バージョン)」といった抽象度の高い情報にとどめるのが望ましい、という方向性が改めて示されたとも言えます。

  1. 参考情報 (あれば)

この変更は「クリーンアップ系のドキュメント整理」と捉えてよく、Rails を利用する側としては特別な対応は不要です。


#57280 Fix ActionCable Overview JS example: localStorage.get → getItem

マージ日: 2026/5/5 | 作成者: @Edilbek

  1. 概要 (1–2文で)
    Action Cableガイド内の JavaScript サンプルコードで、localStorage の誤った呼び出し方 (get) を正しい getItem に修正したドキュメント変更です。ブラウザでコピペして動く形になるように、動的 WebSocket URL の例を実用的なコードに直しています。

  2. 変更内容の詳細

対象ファイル: guides/source/action_cable_overview.md の「Connect Consumer」セクション内 getWebSocketURL の例。

もともとのガイドでは (概念的には) こんなコードになっていました:

js
function getWebSocketURL() {
  const token = localStorage.get('auth-token') // ← これが誤り
  return `wss://example.com/cable?token=${token}`
}

この PR で以下のように修正されています:

js
function getWebSocketURL() {
  const token = localStorage.getItem('auth-token') // ← 正しい Web Storage API
  return `wss://example.com/cable?token=${token}`
}

技術的背景:

  • ブラウザの Web Storage API (Storage インターフェイス) は getItem(key: string): string | null を提供します。
  • localStorage.get というメソッドは存在しないため、サンプルをそのまま実行すると
    TypeError: localStorage.get is not a function が発生します。
  • これを getItem に直すことで、Action Cable の接続 URL を localStorage のトークンで動的に組み立てる例が、実際にブラウザで動くコードになります。
  1. 影響範囲・注意点
  • 影響範囲:

    • Rails 本体のランタイム動作には一切影響なし (ガイド文書のみの変更)。
    • Action Cable の「概要」ガイドを読みながら実装する開発者が、ブラウザ上でエラーに遭遇しなくなる。
  • 注意点:

    • すでにドキュメントを参考にして localStorage.get(...) を書いてしまっているコードがある場合は、手元の実装を getItem(...) に修正する必要があります。
    • getItem はキーが存在しない場合 null を返すため、本番コードでは null チェックや未ログイン時のハンドリングを追加することが推奨されます (例: トークンがなければ接続しない、ログイン画面へ誘導するなど)。
  1. 参考情報 (あれば)

#56209 Update Configurable deprecaton warning

マージ日: 2026/5/5 | 作成者: @skipkayhil

  1. 概要 (1-2文で)
    Rails の ActiveSupport::Configurable を非推奨にする際に表示される警告文が、「class_attribute を使え」と単一の代替手段だけを示していたのをやめ、複数の代替案を提案する、より中立的な文言に変更した PR です。機能的な変更はなく、あくまで deprecation warning のメッセージ内容のみが更新されています。

  1. 変更内容の詳細

変更ファイルは 1 行のみで、ActiveSupport::Configurable 使用時に表示される非推奨警告文の文言が修正されています。

背景:

  • 以前の PR (ref: https://github.com/rails/rails/pull/53970) で ActiveSupport::Configurable が非推奨になり、その際の警告文で「class_attribute を使うように」と強く推奨していました。
  • その結果、実際には単純な attr_accessor や別の仕組みで十分なケースでも、ライブラリ作者やアプリ開発者が「とりあえず class_attribute」を選びがちになる、という誤誘導が発生していた、という問題意識があります。

今回の PR の意図:

  • Configurableclass_attribute に置き換えるべき」という “一択” のガイダンスをやめる。
  • 用途によっては attr_accessor 等のよりシンプルな代替の方が適切であることを反映し、複数の選択肢を警告文に列挙する。
  • これにより、「自分の使い方なら何を使うべきか」を、開発者自身が検討しやすくする。

実際のコード変更のイメージ(擬似例):

ruby
# 変更前(イメージ)
ActiveSupport::Deprecation.warn(
  "ActiveSupport::Configurable is deprecated. " \
  "Use `class_attribute` instead."
)

# 変更後(イメージ)
ActiveSupport::Deprecation.warn(
  "ActiveSupport::Configurable is deprecated. " \
  "Use an alternative such as `attr_accessor`, `mattr_accessor`, " \
  "`class_attribute`, or another configuration pattern as appropriate."
)

※上記は説明用の擬似コードです。実際の文言は英語・1 行差分のみですが、趣旨としては「class_attribute 一択 → 複数案提示」に変わっています。


  1. 影響範囲・注意点
  • ランタイム挙動:

    • ActiveSupport::Configurable の機能自体や挙動にはいっさい変更ありません。
    • 影響があるのは 警告メッセージの内容のみ です。
  • 移行ガイドとしての影響:

    • これまで警告に従って機械的に class_attribute へ置き換えていた場合、それが必ずしもベストプラクティスではないことを再検討すべきケースがあります。
    • 特に以下のような場合は、よりシンプルな手段が適している可能性があります:
      • 単に「設定値を保持するだけ」のクラスで、クラスインスタンス変数 + attr_accessor で事足りる。
      • クラス階層間での設定の継承やスレッドローカルな設定を必要としていない。
    • 一方で、以下のような要件がある場合は class_attributemattr_accessor などを含めた検討が必要です:
      • サブクラスごとに別々の設定値を持たせたい。
      • 設定値をクラスレベルで参照・変更したいが、一定の継承挙動も欲しい。
  • gem・ライブラリ作者への注意:

    • 既に「Configurable 非推奨 → class_attribute に一律置換」という対応をしている gem は、その選択が妥当かを再確認してもよいです。
    • 今後 Configurable の非推奨警告を見た開発者には、「いくつか選択肢があるので、自分のユースケースに合うものを選ぶ」ことが推奨されます。

  1. 参考情報

この PR 自体にはコード上の API 変更や破壊的変更はなく、「移行を促すメッセージの質の改善」が目的です。


#57285 Deactivate ViewReloader hooks when reloaders are cleared

マージ日: 2026/5/4 | 作成者: @ariens

  1. 概要 (1-2文で)
    ViewReloader が登録する「ビューの監視用フック」が app.reloaders.clear 実行後も残り続けて無駄なファイルスキャンを行っていた問題を修正し、リローダー削除時にフックも確実に解除されるようにした PR です。これにより、特に Spring のフォークワーカーなどで、不要な Dir[] / File.mtime 呼び出しが発生しなくなります。

  1. 変更内容の詳細

背景

  • ViewReloader は、レイルタイ初期化時に PathRegistry.file_system_resolver_hooksrebuild_watcher フックを登録します(#57269 で導入)。
  • app.reloaders には ViewReloader などのリローダーオブジェクトが格納されていて、例えば Spring のテストワーカーなどのフォーク後プロセスでは app.reloaders.clear が呼ばれます。
  • しかしこれまでは:
    • app.reloaders の配列から ViewReloader 自体は削除される
    • 登録済みの file_system_resolver_hooks 内のフックは生き残る
  • 結果として、prepend_view_path などが呼ばれるたびに、もはや必要のない Dir.[] + File.mtime を全ビューディレクトリに対して実行してしまう、という無駄な処理が行われていました。

この PR は「リローダーが Rails から取り除かれるときは、その副作用で登録していたフックも無効化する」という責務を明示化しています。


ViewReloader に #deactivate を追加

actionview/lib/action_view/cache_expiry.rb にある ViewReloader に新しく deactivate メソッドが追加されました。

イメージとしては以下のような処理です(疑似コード):

ruby
class ActionView::CacheExpiry::ViewReloader
  def initialize(...)
    # railtie 側で hook 登録するようになった (#57269)
  end

  def deactivate
    # PathRegistry.file_system_resolver_hooks から
    # 自分が登録した hook を取り除く処理
  end
end

ポイント:

  • ViewReloader が登録した file_system_resolver_hooks 内のエントリを削除するロジックをカプセル化。
  • これにより、「リローダーのライフサイクル」と「フックのライフサイクル」が同期される。

app.reloaders の Array を ReloadersCollection に差し替え

railties/lib/rails/application.rb で、これまで単なる Array だった app.reloaders の実体が、ReloadersCollection クラスに差し替えられました。

新クラスは railties/lib/rails/application/reloaders_collection.rb に定義され、主な役割は:

  • Array 互換のインターフェイスを提供しつつ、
  • clear / delete などで要素が削除されるときに、その要素に #deactivate があれば呼び出すこと。

ざっくりしたイメージ:

ruby
class Rails::Application::ReloadersCollection
  include Enumerable

  def initialize
    @reloaders = []
  end

  def <<(reloader)
    @reloaders << reloader
  end

  def clear
    @reloaders.each { |r| r.deactivate if r.respond_to?(:deactivate) }
    @reloaders.clear
  end

  def delete(reloader)
    if @reloaders.delete(reloader)
      reloader.deactivate if reloader.respond_to?(:deactivate)
    end
  end

  # 他にも each / [] / size など Array っぽい API を実装
end

これにより:

  • 既存コードは基本的に app.reloaders を Array として扱い続けられる(<<, each, clear など)。
  • しかし内部的には、cleardelete のタイミングで自動的に deactivate が呼ばれ、ViewReloader のフックが確実に外される。
  • respond_to?(:deactivate) チェックをしているため、ViewReloader 以外のリローダー(deactivate を定義していない)には影響しない。

Railtie 側の変更

actionview/lib/action_view/railtie.rb では、#57269 の変更を前提にしたわずかな修正が入っていますが、今回の PR の中心は以下の2点です:

  • フック登録の実装の位置は前 PR (#57269) で railtie に移っているため、今回の変更は「登録したフックをどう解除するか」にフォーカス。
  • ViewReloader がアプリケーションのリローダー一覧に登録されている前提のもと、その削除時に deactivate が呼ばれるようになった。

テスト追加

以下のテストが追加されています。

  • actionview/test/template/cache_expiry_test.rb

    • ViewReloaderdeactivate が実際に file_system_resolver_hooks からフックを取り除くかを確認するテスト。
    • prepend_view_path 時にフックが呼ばれなくなることを検証していると推測されます。
  • railties/test/application/reloaders_collection_test.rb

    • ReloadersCollection の挙動テスト:
      • clear が各リローダーの deactivate を呼ぶか。
      • delete が対象リローダーの deactivate を呼ぶか。
      • Enumerable 的な動作を保っているか。

  1. 影響範囲・注意点

主に影響を受けるケース

  • Spring など、Rails アプリケーションをフォークしてワーカーを立ち上げる環境で、
    • フォーク後に app.reloaders.clear を呼んでいる場合。
  • これまではフォーク後のワーカーで:
    • ViewReloader 自体は無効化されたつもりでも、
    • PathRegistry.file_system_resolver_hooks のフックだけは生き残り、
    • prepend_view_path(たとえばテストでパスを切り替えるなど)のたびに余計なファイルシステムスキャンが走っていた。
  • 今回の変更により:
    • その無駄な Dir[] / File.mtime 呼び出しがなくなり、フォークワーカー上でのオーバーヘッドが減る。
    • フォーク後のプロセスでファイル監視が不要な典型ケース(テストワーカーなど)で特に有効。

注意点・後方互換性

  • app.reloaders は依然として Array ライクなオブジェクトとして振る舞うため、普通に <<, each, clear, delete をしている分にはコード変更は不要。
  • ただし、リフレクションや is_a?(Array) に依存したコードを書いている場合は注意が必要です。
    • そのようなコードがあれば ReloadersCollection を許容する形に直す必要があります。
  • 独自リローダーを app.reloaders に追加していて「削除時に何かクリーンアップしたい」場合:
    • 任意で #deactivate を定義しておけば、app.reloaders.clear / delete 時に自動で呼ばれるようになります。
    • 逆に、そうしたクリーンアップを意図していなければ、特に何もしなくてよい(respond_to?(:deactivate) で守られている)。

パフォーマンス・動作上の効果

  • フォークワーカー上で、不要な view ディレクトリの走査がなくなるため、テスト起動・実行のパフォーマンス向上が見込めます。
  • ビューのリロード自体の挙動(開発環境などでの通常のオートリロード)は、今回の PR によって変化しないはずです。
    • 影響が出るのは「リローダーを明示的に clear / delete したとき」のみ。

  1. 参考情報 (あれば)
  • この PR が前提にしている変更:
    • #57269: ViewReloader のフック登録を initialize から railtie へ移動し、watcher の生成を遅延させることでブート時の無駄な再構築を防いだ PR。
  • Spring 側の関連 PR:
    • https://github.com/rails/spring/pull/754
      • Spring.after_environment_load によるアプリケーションレベルのプリロード制御。
      • 本 PR は Rails 内部のクリーンアップ(app.reloaders.clear が自動的にフックも無効化する)を担当し、Spring 側のフックとは役割分担されています。
  • 変更ログ:
    • actionview/CHANGELOG.md, railties/CHANGELOG.md に今回の修正内容の要約が追記されています。

#57289 Raise ArgumentError when MemCacheStore is created with Dalli::Client

マージ日: 2026/5/4 | 作成者: @p8

  1. 概要 (1-2文で)
    Rails の ActiveSupport::Cache::MemCacheStore に、Dalli::Client インスタンスを渡して初期化しようとした場合に ArgumentError を明示的に発生させる変更が入りました。これにより、すでに非推奨・削除されていた Dalli::Client ベースの初期化方法が、エラーメッセージどおりに完全に禁止されます。

  1. 変更内容の詳細

対象:
activesupport/lib/active_support/cache/mem_cache_store.rb の初期化ロジックが 1 行だけ変更されています。

背景:

  • 以前のコミット 78b74e9 で「Dalli::ClientMemCacheStore に渡す」方法は非推奨となっていました。
  • さらに c33e2d2e49d でそのサポートが削除されています。
  • しかし、コード上・エラーメッセージ上は「空配列・アドレス文字列・アドレス配列のみ許可」となっているにもかかわらず、Dalli::Client を渡したときに明示的な ArgumentError を投げていませんでした。

今回のPRでは、この不整合を解消し、Dalli::Client が渡された場合は必ず ArgumentError を投げるようにしています。

イメージとしては以下のようなチェックが入っていると考えてください(実際のコードは 1 行変更のみですが、概念的にはこのような条件分岐です):

ruby
# 例: MemCacheStore のコンストラクタ内のイメージ
def initialize(addresses = nil, **options)
  # 許可されるのは:
  # - nil または空配列
  # - "localhost:11211" のような address 文字列
  # - ["host1:11211", "host2:11211"] のようなアドレス配列
  #
  # Dalli::Client は明示的にエラーにする
  if addresses.is_a?(Dalli::Client)
    raise ArgumentError,
      "MemCacheStore: addresses must be empty array, address string, or array of addresses (Dalli::Client is no longer supported)"
  end

  # ... 残りの初期化処理 ...
end

PR 説明にある通り、エラーメッセージの仕様

only "empty array, address, or array of addresses" are allowed

と実際の挙動を揃えるのが主目的です。


  1. 影響範囲・注意点

影響があるケース:

  • アプリケーションやライブラリで、以下のように Dalli::Client オブジェクトを直接渡して MemCacheStore を作っている場合:
ruby
# 以前(非推奨だが技術的には動いていたパターン)
client = Dalli::Client.new("localhost:11211")
Rails.application.config.cache_store = :mem_cache_store, client

この PR 適用後は、上記のようなコードは ArgumentError で落ちます。

正しい移行方法(従来から推奨されていた書き方):

  • MemCacheStore には アドレス文字列またはアドレス配列 を渡してください。
ruby
# 推奨パターン
Rails.application.config.cache_store = :mem_cache_store, "localhost:11211"

# 複数サーバー
Rails.application.config.cache_store = :mem_cache_store,
  ["cache1.example.com:11211", "cache2.example.com:11211"]
  • Dalli を直接使いたい場合は、MemCacheStore 経由ではなく、自前で Dalli をラップする、あるいは Dalli 専用の設定パス(gem・ミドルウェアなど)を利用する必要があります。

注意点:

  • この変更は後方互換性を壊す可能性がありますが、
    • そもそも過去のコミットで非推奨になっていた
    • 対応するエラーメッセージも既に存在していた
      という経緯から、「非推奨状態からの明示的なエラー化」という整理にあたります。
  • テストと CHANGELOG が更新されているため、この挙動は Rails の正式な仕様変更として扱われます

  1. 参考情報 (あれば)
  • このPR: Raise ArgumentError when MemCacheStore is created with Dalli::Client (#57289)
  • 関連コミット(PR本文で言及されているもの)
    • 78b74e9: Dalli::Client を渡す方法の非推奨化
    • c33e2d2e49d: Dalli::Client サポートの削除
  • MemCacheStore のサポートする引数は、公式ドキュメントおよびエラーメッセージ上、
    • 空配列
    • 単一のアドレス文字列
    • アドレス文字列の配列
      に限定される仕様になっています。

#57291 [ci skip] Fix typos in test descriptions

マージ日: 2026/5/2 | 作成者: @jackbowlin

  1. 概要 (1-2文で)
    このPRは、テストコード内の説明文字列(テスト名)のスペルミスを修正したものです。テストの挙動やアプリケーションコードには一切変更はなく、影響は極めて限定的です。

  1. 変更内容の詳細

対象は以下2つのテストファイルです。

  • activesupport/test/env_configuration_test.rb
  • activestorage/test/models/named_variant_test.rb

修正されたのは「テストの説明文字列(テスト名)」だけで、テスト内容(アサーションや実行ロジック)は変更されていません。

ActiveSupport::EnvConfigurationTest

誤字:

  • reqiure → 正: require

例(イメージ):

ruby
# 変更前(イメージ)
test "env configuration reqiure something" do
  # ...
end

# 変更後(イメージ)
test "env configuration require something" do
  # ...
end

実際には "reqiure" という文字列が "require" に修正されています。

ActiveStorage::NamedVariantTest

誤字:

  • explicity → 正: explicitly

例(イメージ):

ruby
# 変更前(イメージ)
test "named variants are applied explicity" do
  # ...
end

# 変更後(イメージ)
test "named variants are applied explicitly" do
  # ...
end

こちらも "explicity""explicitly" に修正されたのみです。

テスト実行について

PRの説明どおり、「テストの名前(説明)」しか変えていないため、テストは実行されていません。Ruby/Railsのテストランナーは通常、test "..." do の文字列を人間向けの説明として扱うだけで、テストの実行ロジックには影響しません。


  1. 影響範囲・注意点
  • テスト・本番動作への影響

    • テストの実行結果や挙動、本番コードの動作には影響ありません。
    • スナップショットツールや外部のテストレポートに「テスト名で紐づけている」場合、名称の変更として認識される可能性があります(例: テストレポートビューアでのテストケース名が少し変わる程度)。
  • テスト選択・フィルタリングへの影響

    • Railsのruby -Itest test/path/to/file_test.rb -n "..."のように、テスト説明文字列を指定してフィルタリングしている場合、旧スペルで指定しているスクリプトはマッチしなくなる可能性があります。
    • そのような用途で使っている場合は、新しいテスト名(正しいスペル)に更新する必要があります。
  • リファクタリング観点

    • 本PRは技術的負債の解消というよりは、テストのメタデータの整備に近い変更です。
    • このような誤字修正は、ドキュメント生成・テストレポート・検索性(grepなど)におけるノイズ削減に貢献します。

  1. 参考情報 (あれば)

いずれも文字列の修正のみで、テストケースの構造やアサーションロジックの変更はありません。


#57269 Defer ViewReloader build when no view paths are registered

マージ日: 2026/5/1 | 作成者: @Korri

  1. 概要 (1-2文で)
    View 用のファイル更新監視(ViewReloader / FileUpdateChecker)の生成タイミングをさらに遅延させ、「まだ view path が1つも登録されていない状態」で updated? が呼ばれた場合でも不要な watcher を作らないようにした PRです。これにより、特に Spring 利用時のテスト実行など、開発環境での起動〜初回リクエスト前の無駄なディレクトリ走査が大幅に削減されます。

  1. 変更内容の詳細

背景と問題点

前回の変更 (#51308) で、「最初の updated? チェックまでは View watcher を作らない(lazy build)」という仕組みが導入されていました。しかし、以下のケースで問題が残っていました。

  • dirs_to_watch(監視対象ディレクトリの配列)が まだ空の状態updated? が呼ばれる
  • その時点で #build_watcher が呼ばれ、中身が空の FileUpdateChecker でもとりあえず作成して @watcher にセットしてしまう
  • 以降は @watcher が non-nil なので #rebuild_watcherreturn unless @watcher ガードが常に通り、
    後から各エンジンが prepend_view_path するたびに「全ディレクトリ集合に対する Dir[*globs] + File.mtime のフルスキャン」が走る

Spring を使ったテスト環境ではこれが顕在化します。

  • Spring の preloader は Rails.application.reloaders を直接見て reload! が必要か判断します(Reloader.check! は通さない)
  • 多くのエンジンの prepend_view_pathActiveSupport.on_load(:action_mailer) / :action_controller_base のフック内で行われるため、preload フェーズではまだ呼ばれていない
  • そのため「view path が1つも登録されていない状態」で updated? が呼ばれ、空 watcher が固定されてしまう
  • 結果として、その後の各 prepend_view_path ごとにフルスキャンが走り、フォークごとに高コストなディスク I/O が発生

PR の説明中にある挙動比較は以下の通りです。

修正前

  • 早期ブート時の updated? 呼び出し
    • → watcher が空のまま作られる(軽いが「作ってしまう」)
  • その後の prepend_view_path ×10
    • 10回ともフル再構築(ディレクトリ再走査)
  • 最初のリクエスト時の updated?
    • → すでにウォッチャーがあるので再構築なし

修正後

  • 早期ブート時の updated? 呼び出し
    • view path が空なので watcher を作らない
  • その後の prepend_view_path ×10
    • @watcher がまだ nil のため 0回再構築
  • 最初のリクエスト時の updated?
    • → ここで初めて watcher を build(1回だけディレクトリ走査

実際のコード上の変更点(概念的な説明)

※実際の diff からの要約であり、擬似コードを含みます。

1) ActionView::CacheExpiry 内の watcher ビルドロジック

actionview/lib/action_view/cache_expiry.rb の主な変更点は:

  • build_watcher / rebuild_watcher 周りの条件分岐が見直され、
  • 「監視対象ディレクトリが空のときは watcher を作らない」 というガードが追加された

イメージとしては以下のような流れになります(擬似コード):

ruby
def build_watcher
  return if dirs_to_watch.empty?  # 追加されたガード: 何も監視対象がなければ何もしない

  @watcher = ActiveSupport::FileUpdateChecker.new(dirs_to_watch, &callback)
end

def rebuild_watcher
  return unless @watcher         # 既存のガード: watcher がないなら何もしない
  return if dirs_to_watch.empty? # 追加(もしくは強化)された条件: 空なら再構築もしない

  @watcher = @watcher.rebuild(dirs_to_watch)
end

ポイント:

  • 初回 updated?
    • @watcher が nil かつ dirs_to_watch が空 → build_watcher しても何も作らない → @watcher は nil のまま
  • view path が登録されたあと
    • 初めて dirs_to_watch が非空の状態で updated? が走ったタイミングで watcher を生成
  • その後のパス追加
    • すでに @watcher がある状態で dirs_to_watch が変化したときにだけ rebuild_watcher が動く

これにより「空の watcher が早期に固定される」パターンが潰され、
prepend_view_path のたびにフルスキャンが走るのを防いでいます。

2) Railtie 側の微調整

actionview/lib/action_view/railtie.rb では 1 行だけの追加ですが、
ViewReloader の登録や初期化タイミングに関する配慮が行われています。

  • ねらいとしては、「reloader の初期化そのものは行うが、実際の FileUpdateChecker の生成は view path が揃うまで遅延させる」という設計に沿うようになっています。

3) テストの追加

actionview/test/template/cache_expiry_test.rb に約 94 行のテストが追加されています。
主に以下を確認する内容と考えられます。

  • view path 未登録時に updated? を呼んでも watcher が作られない こと
  • その後に prepend_view_path を複数回呼んでも、不必要に watcher を再構築しないこと
  • 最初に「非空の view path セット」で updated? を呼んだときにのみ watcher が1回だけ作られ、その後の挙動が正しいこと

これにより、今回の遅延ロジックが将来的に regress しないように自動テストでカバーしています。

4) CHANGELOG

actionview/CHANGELOG.md に今回の修正の概要が追記され、Action View の挙動変更として明示されました。


  1. 影響範囲・注意点

影響範囲

  • 対象:
    • ActionView の ViewReloader / CacheExpiry 周り
    • 主に 開発環境・Spring 利用時 に影響
  • 影響しないケース:
    • production 等で cache_classes = true の場合
      → ViewReloader 自体がスキップされるため、挙動は従来通り
    • 「最初の reloader チェック(updated?)よりも先に、全ての view path が登録される」ような小規模アプリ
      → もともと問題が顕在化しないケースなので挙動上の差はほぼない

性能面のインパクト

  • Shopify の Rails モノレポでの計測では、Spring を使ったテストループ(小さめのワークロード)で
    • 5.16 秒 → 3.66 秒(約 29% の短縮)
  • 実際には、prepend_view_path を多用している大規模アプリケーションほど恩恵が大きくなります。

互換性・注意点

  • 監視対象ディレクトリが「存在しない or 空」である場合には watcher が作られない挙動になりますが、
    • 「view path が 1 つも登録されていないのに view キャッシュの更新検知をしようとする」というケースはそもそも意味がないため、実質的には妥当な制約です。
  • 独自に ActionView::CacheExpiry を拡張しているような特殊なコードベースがある場合は、
    • @watcher が nil であり続けるパターンが増えることを前提にしているかどうかを確認した方が安全です。
  • view path の追加タイミングに依存して「最初の updated? 時に必ず watcher がある」と仮定しているコードがあると、
    その前提が崩れる可能性はありますが、通常の Rails アプリではまず問題にならない想定です。

  1. 参考情報 (あれば)
  • 本 PR:
    • Defer ViewReloader build when no view paths are registered (#57269)
  • 直前の関連 PR:
    • Do not build View watcher until the first updated? check (#51308)
  • コード参照:
    • actionview/lib/action_view/cache_expiry.rb
      • View の更新検知周り(build_watcher, rebuild_watcher, updated? など)
    • actionview/lib/action_view/railtie.rb
      • ViewReloader の登録・初期化
    • actionview/test/template/cache_expiry_test.rb
      • 遅延ビルドと再構築ロジックのテスト
  • Spring 側の関連コード(参考):
    • spring/lib/spring/application.rb:197
      • Rails.application.reloaders を直接見て reload が必要か判定するロジック

#57283 Avoid assert_nil for Mime::NullType request format

マージ日: 2026/5/1 | 作成者: @mugitti9

  1. 概要 (1-2文で)
    Railsのテストコードで、未知のフォーマット要求時の request.formatassert_nil で検証していた箇所を、Mime::NullType を前提とした検証に変更し、最新の Minitest の挙動変更に追従したテスト専用の修正です。実行時の挙動や公開APIには影響せず、テストの互換性・堅牢性を高めるための変更です。

  1. 変更内容の詳細

背景となる問題

  • Rails では、未知のフォーマットがリクエストされた場合、request.formatnil ではなく Mime::NullType のインスタンスを返します。
  • もともとテストでは、このケースを assert_nil request.format のように「nil であること」で検証していました。
  • しかし Minitest 側の変更(assert_nilobj.nil? ではなく nil == obj で比較するようになった)により、
    • 以前: obj.nil? を呼ぶ → Mime::NullType#nil?true を返せばテストが通る
    • 現在: nil == obj を評価 → どんなに obj.nil?true でも、obj が実際に nil でないため失敗
      という不整合が発生していました。

このPRでの主な修正ポイント

1) request.format のテストを Mime::NullType 前提に変更

actionpack/test/dispatch/request_test.rb のテストが、以下のような形に修正されています(擬似コード):

ruby
format = request.format

# 以前:
# assert_nil format

# 以後: 「NullType であること」と「nil? が true であること」を明示的に検証
assert_instance_of Mime::NullType, format
assert_predicate format, :nil?
# 既存の html?/xml?/json? などの否定チェックも維持
refute_predicate format, :html?
refute_predicate format, :xml?
refute_predicate format, :json?

ポイント:

  • 単に「nil と等しいか」ではなく、「Mime::NullType というクラスのオブジェクトであり、その #nil?true を返す」という Rails 独自の仕様を明示的にテスト。
  • すでに存在していた html?, xml?, json? などのフォーマット判定メソッドのテストは維持され、Unknown フォーマット時にはどれも false であることを引き続き確認しています。

2) actiontext 側のテストも同様に修正

actiontext/test/unit/model_test.rb にも同種の assert_nil を使ったテストがあり、同じ問題が起きる可能性があるため、こちらも同じ方針で Mime::NullType + nil? の検証に書き換えられています。

3) Gemfile.lock の微調整

Gemfile.lock に数行の変更 (+3/-2) が入っており、Minitest を含むテスト関連依存のバージョンが更新されています(今回の assert_nil の仕様変更を取り込んだバージョンに追従するためのものと考えられます)。
本体コードではなくテスト環境に関連する変更です。


  1. 影響範囲・注意点
  • 実行時の Rails コード (Mime::Type, Mime::NullType, ActionDispatch::Request など) の挙動は変わりません。あくまでテストコードのみの変更です。

  • ただし、以下のようなプロジェクトには実務的な示唆があります:

    1. Minitest を新しいバージョンに上げた Rails アプリ

      • 自前のテストで assert_nil some_request.format のように「nil? に依存したテスト」を書いている場合、今回と同じ理由で壊れる可能性があります。
      • 未知フォーマットで Mime::NullType を返す箇所に対しては、
        • assert_instance_of Mime::NullType, obj
        • assert_predicate obj, :nil?
          など、Rails の実際の戻り値仕様に沿ったテストに修正するのが望ましいです。
    2. nil? をオーバーライドしているクラスに対する assert_nil

      • Minitest の仕様変更により、「nil? をオーバーライドした擬似 Nil オブジェクト」を assert_nil でテストする手法は通用しなくなります。
      • そのようなオブジェクトについては、このPRと同様に
        • 型 (assert_instance_of / assert_kind_of)
        • ふるまい (assert_predicate obj, :nil?) で個別に検証するスタイルに切り替える必要があります。
  • 本PRはテストの期待値を Minitest 実装依存から切り離し、「Rails 側が本来保証したい仕様(NullType + nil?)」を直接テストする形に整理しているため、長期的にはテストの安定性が向上します。


  1. 参考情報 (あれば)
  • 関連する Minitest の変更:
    • assert_nil(obj) が内部で obj.nil? を呼ぶ実装から、nil == obj で比較する実装に変更されたコミット:
      • minitest/minitest@c471347
  • Rails の Mime::NullType 仕様:
    • 未知の MIME タイプに対するフォーマットオブジェクトであり、nil?true にすることで「nil に準ずる」扱いを可能にしているが、オブジェクトとしては nil とは異なる、という設計になっています。