Skip to content

Ruby on Rails PR Digest - 2026年 7月

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

#57825 Introduce ractorize

マージ日: 2026/7/6 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    Rails アプリケーションを Ractor から共有可能にするための土台として、Rails::Application#ractorize! が導入され、基本的な Rails アプリを Ractor.make_shareable できるようにする PR です。あわせて、Ractor 非対応だったいくつかの内部実装(継承オプション、KeyGenerator、Mutex など)が Ractor セーフになるよう調整されています。

  1. 変更内容の詳細

新機能: Rails::Application#ractorize!

Rails::Application に以下のメソッドが追加されています。

ruby
class MyApp::Application < Rails::Application
  config.eager_load = true
  config.cache_store = :null_store

  # Ractor から共有可能にする
  config.after_initialize do
    Rails.application.ractorize!
  end
end

ractorize! が行うこと(PR 説明+差分から推測される内容):

  • アプリケーションを Ractor 共有可能な状態に「準備」する
    • Ractor セーフでないオブジェクト(Mutex など)を排除・解体
    • Engine / Initializer 周りの内部状態を Ractor 共有可能な形に再編
  • 最後に Ractor.make_shareable(app) を呼び出し、Rails.application 自体を Ractor から共有可能なオブジェクトにする

これにより、例えば「メイン Ractor でアプリをブート → サブ Ractor にアプリを共有 → 各 Ractor でリクエスト処理」みたいな構成の土台が整えられます。

制約事項

現時点(この PR 時点)で ractorize! を使うためには、次の条件が必要です:

  • アプリケーションコードとルーティングは eager load 必須
    • config.eager_load = true
    • 遅延ロードだと、Ractor 間でクラス定義のタイミングや状態が不整合になるため
  • Action Cable は非対応
    • 内部的にスレッド/コネクション管理など Ractor セーフでない構造が多数あるため
  • キャッシュストアは :null_store 必須
    • 他のキャッシュストア(メモリストアなど)が Ractor 共有できないデータ構造を使っているため

将来的にはこれらの制約を減らす予定とのことですが、この PR はあくまで最初の足場です。


継承オプション (Rails::Configuration / Rails::Engine / Rails::Initializable)

Rails::Configuration や Engine の継承オプション、initializer 系の設定は、これまで「通常の Ruby オブジェクト(しばしばミュータブル)」を前提にしていました。

この PR では:

  • 「継承オプション」の保持方法が Ractor セーフになるよう調整
    • Hashdefault_proc がミューテーションを行わないようにしたり
    • 初期化後に freeze 可能な形に揃えたり
  • Rails::Engine ごとにアプリケーションを eager に構築し、Ractor 非共有なオブジェクト(特に Mutex)を「初期化時に使って、共有前に破棄」するようなフローに変更
  • Rails::Initializable 周りで使われる Proc などを Ractor 共有可能な形に修正

これにより、Rails.application の内部構成(設定や初期化処理の一覧など)を Ractor.make_shareable に掛けられるようになります。


KeyGenerator を Ractor セーフに

ActiveSupport::KeyGenerator と、その利用箇所である ActiveSupport::Messages::RotationCoordinator に変更が入っています。

ポイント:

  • これまで KeyGenerator は内部で「Concurrent Map(あるいは似た構造)」を使っており、これは Ractor 共有不可能でした。
  • この PR では:
    • 「キー生成用のキャッシュ(Map)」を Ractor ローカル にする設計に変更
    • つまり「Ractor ごとに KeyGenerator のキャッシュを持つ」ようにして、Rails.application 自体は共有しつつも、共有不可能な Map は各 Ractor 側で持つ

影響として:

  • 同じ Rails.application を複数 Ractor から使っても、KeyGenerator 自体は共有されるが、その内部キャッシュは各 Ractor で独立して持たれる。
  • そのため、初回アクセス時のキー計算コストは Ractor ごとに一度ずつ発生するが、Ractor セーフネスとトレードオフにした pragmatic な対応になっている。

テスト (activesupport/test/key_generator_test.rb) も追加されており、Ractor 使用時の振る舞いを検証しています。


ActionDispatch Journey / Transition Table の Ractor 対応

actionpack/lib/action_dispatch/journey/gtg/transition_table.rb に追記:

  • ルーティングエンジン(Journey)の状態遷移テーブル構築処理で、Ractor 共有可能でない要素(例: デフォルト Proc 付き Hash)が含まれていた部分を修正
  • 遷移テーブル構造が Ractor.make_shareable できるようにするための小変更(ミュータブルな default_proc の削除・freeze など)が行われていると考えられます。

これにより、Rails.application.routes の内部表現も Ractor 共有しやすくなります。


Ractor 関連テストの追加

  • activesupport/lib/active_support/testing/ractors_assertions.rb
    • Ractor 関連のアサーションヘルパを定義
    • 例: 「このオブジェクトは Ractor から共有できるか」などをテストで確認する仕組み
  • railties/test/application/ractors_test.rb
    • 実際に Rails アプリをブートして ractorize! を呼び、別 Ractor から利用できるかなどを検証する統合テスト
    • eager load / null_store などの前提条件を満たしたアプリで、Ractor.make_shareable が成功することを確認していると推測できます。

  1. 影響範囲・注意点

影響範囲

  • Rails 内部:
    • アプリケーション設定 (Rails::Configuration)
    • Engine の構造 (Rails::Engine)
    • Initializer の登録・実行方法
    • KeyGenerator / メッセージ暗号化系
    • ルーティングエンジン(Journey)の内部構造
  • 外部 API レベル:
    • 新メソッド Rails::Application#ractorize!
    • Ractor ベースで Rails を動かしたい人向けの初期サポート

既存アプリへの影響

  • この PR 自体は、既存アプリが従来どおり(Ractor を使わずに)動く場合には、基本的に挙動変更を引き起こさないように配慮されています。
  • ただし以下の点は意識しておくと良いです:
    • internal な Hash の default_proc など、一部挙動が微妙に変わっている可能性はある(通常アプリではまず問題にならないレベルを目指しているはず)
    • KeyGenerator の実装変更に伴い、Ractor を明示的に使う場合は「Ractor ごとに KeyGenerator のキャッシュが別になる」ことを理解しておく必要がある

Ractor を使いたい場合の注意点

  • 必須条件:
    • config.eager_load = true
    • config.cache_store = :null_store
    • Action Cable を使わない
  • 実運用で使うには、まだ制約が大きく、かつスレッド・プロセス・Ractor の組み合わせの設計が難しいため、現時点では「実験的」な段階と見なすのが妥当です。
  • アプリケーションコード側も、自前で保持するグローバル状態やシングルトン、クラス変数などが Ractor セーフになっていないと、ractorize! 後の Ractor.make_shareable に失敗する可能性があります。

  1. 参考情報 (あれば)
  • この PR で触れられている関連 PR:
  • Ractor 自体の仕様:
  • Ractor 対応の設計上の方針:
    • 共有可能なオブジェクト (Ractor.shareable?) のみを共有し、Ractor ローカルで持つべきもの(キャッシュや Mutex など)は Ractor ごとに分離する、という設計が Rails 内部にも適用され始めているのがこの PR のポイントです。

#58018 Translate if_exists/if_not_exists guards when inverting remove_column and add_reference/remove_reference

マージ日: 2026/7/6 | 作成者: @55728

  1. 概要 (1-2文で)
    remove_column および add_reference / remove_reference(とそのエイリアス add_belongs_to / remove_belongs_to)について、マイグレーションの巻き戻し時に if_exists / if_not_exists オプションが正しく反転して適用されるように修正した PR です。これにより、これらのコマンドを使った冪等なマイグレーションの change / revert が、期待通りに動作するようになります。

  1. 変更内容の詳細

背景: command_recorder と idempotency guard

Rails の ActiveRecord::Migration::CommandRecorder は、change マイグレーションや revert ブロック実行時に発行されたコマンドを記録し、その逆操作(inverse)を自動生成します。

既に以下のコマンドでは、if_existsif_not_exists が「逆向きのガード」として正しく変換されていました:

  • add_column
  • add_index
  • add_foreign_key / remove_foreign_key
  • add_check_constraint / remove_check_constraint

しかし、以下のコマンドファミリーではこの変換が抜けていました:

  • remove_column
  • add_reference / remove_reference
  • add_belongs_to / remove_belongs_toadd_reference / remove_reference のエイリアス)

今回の PR は、この「抜けていた2ファミリー」に対してもガードの変換を適用するものです。


remove_column の変更

変更前の挙動

ruby
inverse_of :remove_column, [:users, :nickname, :string, if_exists: true]
# => [:add_column, [:users, :nickname, :string, { if_exists: true }], nil]
  • remove_column に渡した if_exists: true が、そのまま逆操作の add_column にも渡されていました。
  • しかし、add_columnif_exists オプションを認識しないため、ロールバック時にはガードが完全に無視され、「意図に反する形で」動いてしまう可能性がありました。
    • 元々の意図: 「カラムが存在する場合だけ remove する」→ ロールバックでは「カラムが存在しない場合だけ add する」としたい
    • 実際の挙動: ロールバック側でガードが消えるため、常に add_column を実行してしまう

変更後の挙動

ruby
inverse_of :remove_column, [:users, :nickname, :string, if_exists: true]
# => [:add_column, [:users, :nickname, :string, { if_not_exists: true }], nil]
  • remove_column(if_exists: true) の逆操作は add_column(if_not_exists: true) となり、
    • 削除側: 「あれば消す」
    • 逆操作: 「なければ作る」
  • というペアになり、冪等な前方・後方マイグレーションが成立するようになりました。

add_reference / remove_reference(およびエイリアス)の変更

変更前の挙動

ruby
inverse_of :add_reference, [:t, :u, if_not_exists: true]
# => [:remove_reference, [:t, :u, { if_not_exists: true }]]  # ガードが変換されず無視される

inverse_of :remove_reference, [:t, :u, if_exists: true]
# => [:add_reference, [:t, :u, { if_exists: true }]]          # 同上
  • コマンド名だけが addremove で入れ替わり、オプションハッシュはそのまま渡されていました。
  • その結果:
    • add_reference 側は if_not_exists をサポートしていても
    • remove_reference 側は if_not_exists / if_exists をサポートしていなかったり、逆に if_exists だけを見ていたり
  • という非対称な API のせいで、片方のコマンドで指定したオプションが逆操作側で「無視」されていました。

変更後の挙動

  • add_reference(if_not_exists: true) の逆操作は remove_reference(if_exists: true)
  • remove_reference(if_exists: true) の逆操作は add_reference(if_not_exists: true)
  • となるように、if_exists / if_not_exists を互いに反転して渡すように修正されました。
ruby
# 例: change マイグレーション
def change
  add_reference :comments, :post, if_not_exists: true
end

# ロールバック時に内部的に解釈されるイメージ
# => remove_reference :comments, :post, if_exists: true

add_belongs_to / remove_belongs_toadd_reference / remove_reference のエイリアスなので、この修正の恩恵をそのまま受けます。


  1. 影響範囲・注意点
  • 対象:
    • change マイグレーション(または revert ブロック)で
      • remove_column(..., if_exists: true)
      • add_reference(..., if_not_exists: true)
      • remove_reference(..., if_exists: true)
      • およびその *_belongs_to エイリアス を使っているケース。
  • 影響:
    • これまで:
      • 逆操作側でガードが「消える / 無視される」ことがあった。
      • そのため、本番環境などで「途中の状態からロールバック」するようなケースで、不要なエラーや意図しない再作成などが起こりうる。
    • これから:
      • if_exists / if_not_exists が逆操作側にも正しく反映され、前方・後方どちらの方向にも冪等性が担保されやすくなる。
  • 互換性について:
    • 挙動変更ではありますが、「ガードが無視されていた」状態から「意図通りガードされる」状態への修正なので、ほとんどの場合はバグ修正として望ましい変更です。
    • もし「逆操作側でガードが付かないこと」を前提にした非常に特殊なワークアラウンドをしている場合は、今回の修正で挙動が変わる可能性があります(そのような依存は避けるべきです)。

  1. 参考情報 (あれば)
  • 変更ファイル:
    • activerecord/lib/active_record/migration/command_recorder.rb
      • inverse_of のロジックに、remove_column および add_reference / remove_reference 系に対する if_existsif_not_exists の変換処理を追加。
    • activerecord/test/cases/migration/command_recorder_test.rb
      • 上記の変換が正しく行われることを保証するテストを追加。
  • 関連する既存仕様:
    • すでに add_column, add_index, add_foreign_key/remove_foreign_key, add_check_constraint/remove_check_constraint では、同様の guard 変換が行われており、本 PR はその不整合を解消するものです。

#58024 Derive the expected queue from the delivery job in assert_enqueued_email_with

マージ日: 2026/7/6 | 作成者: @55728

  1. 概要 (1-2文で)
    assert_enqueued_email_with が、メール送信時のキュー名を「メイラー側の設定」ではなく「delivery_job 側の設定」から導出するように修正されました。これにより、カスタム delivery_jobqueue_as を指定している場合でも、正しくテストが通るようになります。

  1. 変更内容の詳細

これまでの挙動(問題点)

ActionMailer::TestHelper#assert_enqueued_email_with は、期待されるキュー名を mailer_class.deliver_later_queue_name から導出していました。

しかし、次のようにカスタム delivery_job を使い、そのジョブに queue_as を設定しているケースでは問題が発生していました。

ruby
class RegistrationDeliveryJob < ActionMailer::MailDeliveryJob
  queue_as :notifications
end

class RegistrationMailer < ApplicationMailer
  self.delivery_job = RegistrationDeliveryJob
end

RegistrationMailer.welcome.deliver_later

assert_enqueued_email_with RegistrationMailer, :welcome
# => No enqueued job found with {job: RegistrationDeliveryJob, ..., queue: "mailers"}
#    Potential matches: {..., queue: "notifications", ...}
  • 実際には queue: "notifications" にジョブが積まれている
  • しかしアサーションは queue: "mailers" を期待してしまうため、失敗していました

これは deliver_later のドキュメント上「カスタム delivery_job を使う場合は、その job がキュー名を支配する」とある契約と食い違っていました。

今回の修正内容

変更ファイル:

  • actionmailer/lib/action_mailer/test_helper.rb (+1 / -1)
  • actionmailer/test/test_helper_test.rb (+18 / 0)

テストヘルパの実装を微調整し、「期待されるキュー名の決定ロジック」を以下のように変更しています。

  • 以前: mailer_class.deliver_later_queue_name からキュー名を導出
  • 今回: mailer_class.delivery_job 側からキュー名を導出
    delivery_jobqueue_as で指定したキューがある場合はそれを使う

結果として:

ruby
class RegistrationDeliveryJob < ActionMailer::MailDeliveryJob
  queue_as :notifications
end

class RegistrationMailer < ApplicationMailer
  self.delivery_job = RegistrationDeliveryJob
end

RegistrationMailer.welcome.deliver_later

assert_enqueued_email_with RegistrationMailer, :welcome
# 期待キュー名が "notifications" になり、正常にパスする

テスト (test_helper_test.rb) には、この振る舞いを保証するためのケースが追加されています(カスタム delivery_job + queue_as を使った場合のアサーション成功を検証)。

なお、デフォルトの MailDeliveryJob を使うケースや queue_name_prefix を利用しているケースでも、最終的に解決されるキュー名は従来と変わらないように設計されています。


  1. 影響範囲・注意点
  • テストでの影響

    • カスタム delivery_job を使っていて queue_as を指定している場合、これまで不当に落ちていた assert_enqueued_email_with が通るようになります。
    • 逆に言えば、「実運用ではカスタム delivery_job のキューに積まれている」のに、「テストで mailer 側のキュー名を前提にしていた」ような間違ったテストは、今回の修正で正しい挙動に修正されます。
  • 既存コードへの互換性

    • デフォルトの MailDeliveryJob を使っている場合、これまでとキュー名の解決結果は同じになるように配慮されているため、通常は影響はありません。
    • queue_name_prefix を利用している場合も、最終的なキュー名は従来と同じになるように実装されています。
  • テストコード側で明示的に queue を指定している場合の確認

    • assert_enqueued_email_withqueue: オプションで明示的にキューを指定している場合、その指定がカスタム delivery_job の設定と矛盾していないかは確認しておくと安全です。

  1. 参考情報 (あれば)
  • ActionMailer::TestHelper#assert_enqueued_email_with:
    • メイラーとメソッドを指定して、そのメールが正しくジョブとしてエンキューされているかを検証するためのテストヘルパ。
  • ActionMailer::Base#delivery_job:
    • メール送信に使用する Active Job クラスを指定するための設定。
    • deliver_later のドキュメント上、「カスタム delivery_job を設定した場合、その job がキュー名 (queue_as) を決定する」と規定されている。
  • 関連 PR: #58009(今回の修正はそのフォローアップとして位置付けられています)。

#58017 Preserve subsecond precision in the DateTime.now travel stub under with_usec

マージ日: 2026/7/6 | 作成者: @55728

  1. 概要 (1-2文で)
    travel_to(time, with_usec: true) 使用時に、DateTime.now でもサブ秒(マイクロ秒)精度が正しく保持されるように修正したPRです。これにより、同じ凍結時刻を参照する Time.nowDateTime.now の挙動が一致します。

  1. 変更内容の詳細

何が問題だったか

ActiveSupport::Testing::TimeHelpers#travel_to には with_usec: オプションがあります。

  • with_usec: true
    凍結する時刻のサブ秒(usec)まで含めて時間を固定する意図。
  • with_usec: false(デフォルト)
    秒単位で丸めて時間を固定する(usec は 0 になる)。

これまでは:

ruby
travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999), with_usec: true do
  Time.now.usec      # => 999999  # サブ秒が保持される
  DateTime.now.usec  # => 0       # サブ秒が失われる(バグ)
end

と、Time.nowwith_usec: true に従ってサブ秒を保持していた一方、DateTime.now のスタブは秒単位に切り捨ててしまっていました。

今回の修正内容

activesupport/lib/active_support/testing/time_helpers.rbDateTime.now のスタブ生成部分が 1行変更され、with_usec: true のときにサブ秒を保持するようになりました。

PRの説明に対応する挙動は次の通りです:

ruby
travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999), with_usec: true do
  Time.now.usec      # => 999999
  DateTime.now.usec  # => 999999   # Time.now と一致するように修正
end

また、テスト (activesupport/test/time_travel_test.rb) に DateTime.nowusec を検証するケースが 13行追加されています。
これにより、with_usec: trueDateTime.now がサブ秒を保持すること、with_usec: false では保持しないこと(usec == 0)が回 regresion テストとして担保されます。

with_usec: false の挙動は変わらない

デフォルトの with_usec: false の場合は、これまで通り DateTime.now.usec0 のままです:

ruby
travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do # with_usec: false がデフォルト
  Time.now.usec      # => 0
  DateTime.now.usec  # => 0
end

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

    • テストコードで ActiveSupport::Testing::TimeHelpers#travel_toDateTime.now を併用しており、かつ with_usec: true を指定しているケース。
    • そういったコードでは、これまで「秒単位に丸められていた」DateTime.now が「マイクロ秒まで含めて凍結される」ように変わります。
  • 挙動変化により起こりうること:

    • 以前の動作に依存して「DateTime.now.usec は常に 0 である」と決め打ちしていたテストがあると、with_usec: true の文脈では失敗する可能性があります。
    • ただし、with_usec: true を明示している場合は「サブ秒を保持したい」という意図が通常のため、この変更は意図により沿ったものといえます。
  • 注意点:

    • 秒単位で固定したいテストは、with_usec: を明示的に指定するか、with_usec: false を使うことを前提にするとよいです。
    • ライブラリやアプリ側で、DateTime.now ベースのロジックと Time.now ベースのロジックを混在させていた場合、これにより両者の挙動が揃うため、むしろ一貫性が向上します。

  1. 参考情報 (あれば)
  • 該当コード:
    • ActiveSupport::Testing::TimeHelpers#travel_totime_helpers.rb
  • 挙動の意図:
    • with_usec: true … テスト中の「凍結時刻」をサブ秒精度まで含めて再現したい場面向け。
    • with_usec: false … 秒単位で十分なテスト向け(以前からのデフォルト挙動)。

#58021 Honor an explicit :selected option in time_zone_select

マージ日: 2026/7/6 | 作成者: @55728

  1. 概要 (1-2文で)
    time_zone_select ヘルパーに selected: オプションを明示的に渡した場合、その値がきちんと選択状態として反映されるように修正したPRです。selected: を指定しない既存コードの挙動は変わりません。

  1. 変更内容の詳細

これまでの問題

time_zone_select は第4引数のオプションとして selected: を受け取れますが、実際には無視されており、常に「オブジェクトの属性値」もしくは「default: オプション」で選択タイムゾーンが決まっていました。

ruby
time_zone_select("firm", "time_zone", nil, selected: "B")
# 本来は "B" を選択させたいが、
# 実際には firm.time_zone(または default:)の値が選択されていた

フォームオブジェクト側の値を一時的に上書きしたいケース(例: 管理画面で一括編集、確認画面で一時的に別の値を見せたい等)で、selected: が効かないというバグ的挙動になっていました。

修正後の挙動

このPRでは time_zone_select の内部で、「どの値を選択状態にするか」の決定ロジックにおいて、:selected が明示的に渡されていればそれを最優先で使うように修正しています。

  • 優先順位(概念的には):
    1. options[:selected] があればそれを選択値とする
    2. なければ、従来通り「オブジェクトの属性値」または options[:default] を使う

使用イメージ:

ruby
# 明示的に "B" を選択したい
time_zone_select("firm", "time_zone", nil, selected: "B")
# => HTML上も "B" に対応する <option> が selected になる

# これまで通りオブジェクトの値を使いたい場合
time_zone_select("firm", "time_zone")
# => firm.time_zone が selected

# オブジェクトに値がなければ default: を使う(従来通り)
time_zone_select("firm", "time_zone", nil, default: "UTC")

コード上の変更点としては、ActionView::Helpers::Tags::TimeZoneSelect 内の選択値決定ロジックに 3行程度の追加があり、テスト (form_options_helper_test.rb) で「selected: を渡したときにそれが優先される」ことを検証するケースが追加されています。


  1. 影響範囲・注意点
  • 影響を受けるヘルパー

    • ActionView::Helpers::FormOptionsHelper#time_zone_select
    • form_for / form_with 等から呼ばれる同名ヘルパーも含む
  • 既存コードへの影響

    • selected: を渡していないコードは挙動が変わりません(オブジェクト値 + default: の従来ロジックそのまま)。
    • これまで「selected: を指定しても無視される」と思ってワークアラウンドをしていた場合は挙動が変わる可能性があります。
      例えば:
      ruby
      # selected: が無視される前提で、わざと「無関係な値」を渡していたような特殊なハック
      time_zone_select("user", "time_zone", nil, selected: "dummy")
      こうしたコードがあると、今回から "dummy"(に対応するタイムゾーン)が本当に選択されてしまいます。
  • 注意点 / ベストプラクティス

    • 「モデルの値をそのまま使いたい」場合は selected: を指定しない方が分かりやすく安全です。
    • 「モデルの値よりも、フォーム側で明示的に指定した選択状態を優先したい」ケース(例: 確認画面での上書き、独自の初期値制御)では selected: を積極的に使えるようになります。
    • default: はあくまで「オブジェクトの値がない時のフォールバック」として機能し、selected: を指定すれば default: よりも selected: が優先されます。

  1. 参考情報 (あれば)
  • 対象コード: actionview/lib/action_view/helpers/tags/time_zone_select.rb
  • テスト: actionview/test/template/form_options_helper_test.rb
  • 関連ヘルパー: select, time_select など他の select 系ヘルパー も似た API を持つため、今後 selected: の扱いがそれらと整合的になるかを見る際の参考になります。

#58012 Fix broken links in guides [ci skip]

マージ日: 2026/7/6 | 作成者: @fuentesjr

  1. 概要 (1-2文で)
    このPRは、Rails Guides 内および外部サイトへの「リンク切れ」を3箇所修正するドキュメント専用の変更です。動作コードには一切手を入れておらず、ガイドの参照先を正しいURL/アンカーに差し替えています。

  1. 変更内容の詳細

変更はいずれも1行ずつのリンク先修正です。

2-1. Active Storage ガイド: ImageMagick のリンク修正

対象: guides/source/active_storage_overview.md の Requirements セクション

変更前:

markdown
https://imagemagick.org/index.php

この URL は 404 になるため、サイトルートに変更。

変更後:

markdown
https://imagemagick.org/

Rails 自体の機能には影響しませんが、Active Storage をセットアップする際に参照する ImageMagick の公式サイトへのリンクが正常に開けるようになります。


2-2. Action Mailer ガイド: Active Job へのアンカー修正

対象: guides/source/action_mailer_basics.md の「コントローラの編集」付近

Action Mailer から Active Job を使ってメールを非同期送信する (deliver_later) 説明へのリンクが壊れていました。

変更前:

markdown
active_job_basics.html#action-mailer

Active Job ガイド側に #action-mailer というアンカーは存在せず、クリックしてもページトップ(もしくは何も起きない)状態でした。

変更後:

markdown
active_job_basics.html#example-sending-email

#example-sending-email セクションは、Active Job 経由でのメール送信(deliver_later など)を具体的に解説している箇所であり、文脈的にも正しいターゲットです。
Action Mailer ガイドから「メールをジョブ経由で送るサンプル」に直接ジャンプできるようになります。


2-3. Configuring ガイド: キャッシュストアへのアンカー修正

対象: guides/source/configuring.mdconfig.cache_store の説明部分

config.cache_store の説明中で、キャッシュストアの詳細として Caching ガイド中の特定セクションへリンクしていますが、そのアンカー名が存在しませんでした。

変更前:

markdown
caching_with_rails.html#cache-stores

Caching ガイドには #cache-stores セクションがなく、リンクはページトップ相当になっていました。

変更後:

markdown
caching_with_rails.html#other-cache-stores

#other-cache-stores セクションは、config.cache_store の説明で列挙されているストアと一致する内容を扱っています:

  • :memory_store
  • :file_store
  • :mem_cache_store
  • :null_store
  • :redis_cache_store

これにより、config.cache_store の説明から、そのまま各キャッシュストアの詳細説明へジャンプできるようになります。


  1. 影響範囲・注意点
  • 影響範囲は Rails Guides (ドキュメント) のみで、アプリケーションコードやフレームワークの挙動には一切影響しません。
  • 既存の URL あるいはアンカーから別の場所へ「リダイレクト」されるようになったのではなく、もともと機能していなかったリンクを、意図した場所に正しく張り直した変更です。
  • ドキュメント生成 (guides のビルド) が通るかどうかはほぼノーリスクですが、
    • プレーンな URL (https://imagemagick.org/)
    • 同一リポジトリ内の他のガイドへの相対リンク (active_job_basics.html#example-sending-email, caching_with_rails.html#other-cache-stores)
      の3つなので、ビルドエラー等の可能性も実質ありません。
  • リンクターゲットとなるガイド側のアンカー名 (#example-sending-email, #other-cache-stores) が今後変更されると再びリンク切れになるため、ドキュメント側でアンカーを変更する際は相互参照の確認が必要です。

  1. 参考情報 (あれば)

運用面では、こうした broken link は CI で自動検出しづらいことが多いため、今後リンクチェッカー(外部URL & 内部アンカー)の導入を検討すると、同種の不具合を早期に防ぎやすくなります。


#58016 Forward keyword arguments through deprecation proxies

マージ日: 2026/7/6 | 作成者: @55728

  1. 概要 (1-2文で)
    DeprecatedObjectProxy / DeprecatedInstanceVariableProxy 経由でキーワード引数必須メソッドを呼ぶと ArgumentError になっていた問題を修正し、正しくキーワード引数として転送されるようにした PR です。Ruby 3 以降の「キーワード引数の分離」による不具合を、ruby2_keywords を使って最小限の変更で解消しています。

  1. 変更内容の詳細

何が問題だったか

Ruby 3 以降では、以下のような「最後の Hash をキーワード引数として解釈する」挙動が変わり、*args で受けるとキーワードにならず「位置引数の Hash」として扱われます。

ruby
def target(a:)
  a
end

def caller(*args, &block)
  target(*args, &block)
end

caller({ a: 1 })
# Ruby 3 では ArgumentError: wrong number of arguments ...

ActiveSupport::Deprecation の以下のプロキシクラスが、まさにこのパターンでした。

  • ActiveSupport::Deprecation::DeprecatedObjectProxy
  • ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy

これらはベースクラス DeprecationProxy#method_missing を継承しており、その定義は

ruby
def method_missing(method, *args, &block)
  warn(method, caller)
  @object.__send__(method, *args, &block)
end

のような形でした。このため、プロキシ対象のメソッドが「必須キーワード引数」を持っていると、以下のようなエラーになっていました。

ruby
class Sample
  def foo(a:)
    a
  end
end

sample = Sample.new
proxy = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(sample, "sample", ActiveSupport::Deprecation.new)

proxy.foo(a: 1)
# Before:
# ArgumentError: wrong number of arguments (given 1, expected 0; required keyword: a)

Hash { a: 1 } が位置引数として転送されてしまい、キーワードとして解釈されなかったためです。

どう直したか

DeprecationProxy#method_missingruby2_keywords を付けました。

ruby
class DeprecationProxy
  ruby2_keywords def method_missing(method, *args, &block)
    warn(method, caller)
    @object.__send__(method, *args, &block)
  end
end

ruby2_keywords を付与すると、

  • 呼び出し元での「キーワードっぽい Hash」としての情報が *args の中にフラグ付きで保持される
  • 呼び出し先(ここでは @object.__send__)に転送したとき、再び「キーワード引数」として解釈される

という Ruby 2 互換の挙動が有効になります。

これにより、先ほどの例は次のように期待どおり動作します。

ruby
proxy.foo(a: 1)
# After:
# => 1
# かつ deprecation warning は従来どおり出る

なぜ *args, **kwargs にしなかったのか

DeprecationProxy#method_missingwarnargs を渡し、特に DeprecatedInstanceVariableProxy#warn では args.inspect をそのままメッセージに埋め込んでいます。

ruby
def warn(method, callstack)
  ActiveSupport::Deprecation.warn(
    "some message... Args: #{args.inspect}",
    callstack
  )
end

ここでメソッドシグネチャを

ruby
def method_missing(method, *args, **kwargs, &block)
  ...
end

のように分割してしまうと、args からキーワードが抜け落ち、警告メッセージ上では

  • 以前: Args: [1, {:a=>2}]
  • 変更後: Args: [1](キーワードは別の kwargs にいる)

となり、互換性が崩れます。

ruby2_keywords を使えば、

  • 呼び出し側・メッセージ上では「旧来どおり args に Hash が入っている」状態を維持
  • 転送先メソッドでは Ruby 3 スタイルのキーワードとして正しく解釈

という両方を満たせるため、この一行だけで済ませています。

他との一貫性

同じファイルにある DeprecatedConstantProxy#method_missing は、すでに

ruby
def method_missing(...)
  ...
  target.__send__(...)
end

という「...(dot-dot-dot)」構文でキーワードを正しく転送する形に直されていました。この PR は、その sibling 実装と挙動を揃える修正でもあります。

テスト追加

activesupport/test/deprecation_test.rb にテストが追加され、以下のようなケースがカバーされています(要約):

  • DeprecatedObjectProxy 経由で、必須キーワード引数を持つメソッドを呼べること
  • DeprecatedInstanceVariableProxy 経由でも同様に動作すること
  • その際に deprecation warning が出ること
  • 引数の表示(args.inspect)が従来どおりであること

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

    • ActiveSupport::Deprecation::DeprecatedObjectProxy
    • ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy
    • 特に、キーワード引数を持つメソッドをこれらのプロキシ越しに呼んでいるコードは挙動が変わります(以前は ArgumentError で落ちていたが、正常に呼べるようになる)。
  • 後方互換性

    • これまで例外が出ていた箇所が正しく動作するようになるため、基本的にはバグフィックスです。
    • warn に渡される args の内容・inspect 文字列は維持されるので、ログ監視・テストでメッセージを厳密比較している場合でも影響はほぼありません。
    • ruby2_keywords 自体は Ruby 3 系では「非推奨だがまだ使える互換機能」として残っていますが、Rails 内部実装に閉じており、利用者側のコードに直ちに影響はありません。
  • 注意点

    • 将来 Ruby 本体から ruby2_keywords が削除される可能性はあります。その場合、この周辺実装を *args, **kwargs などに書き換えつつ、警告メッセージの互換性をどう担保するか、という課題は残ります。
    • プロキシ経由でのメソッド呼び出しに頼ったテストが、「例外が出ること」を前提にしていた場合は、テストが通らなくなる可能性があります。

  1. 参考情報 (あれば)

#58015 Strip surrounding whitespace before parsing a duration in Hash.from_xml

マージ日: 2026/7/6 | 作成者: @55728

  1. 概要 (1-2文で)
    Hash.from_xmltype="duration" の要素をパースする際に、前後の空白文字(改行・インデント)を除去してから ActiveSupport::Duration に変換するように修正された PR です。これにより、「人間が整形した XML」に含まれる改行やインデントのせいで ParsingError が起きていた問題が解消されます。

  1. 変更内容の詳細

何が問題だったか

以下のように、整形された XML で type="duration" のノードに改行やインデントが含まれると:

ruby
Hash.from_xml(<<~XML)
  <event>
    <length type="duration">
      PT1H
    </length>
  </event>
XML

実際のテキストノードの値は " PT1H\n " のように前後に空白を含む形になり、
ActiveSupport::Duration::ISO8601Parser がこれを正しく解釈できず、ParsingError を送出していました。
同じような書き方でも、type="date", type="datetime", type="integer", type="boolean" などは内部で strip されている(または許容度が高い)ため問題なくパースできていましたが、duration だけが厳格だった状態です。

今回の修正内容

activesupport/lib/active_support/xml_mini.rb で、type="duration" の値をパースする前に strip をかけるように 1 行差し替えが行われました。

イメージとしては:

ruby
when "duration"
  # 変更前(概念的に):
  ActiveSupport::Duration.parse(value)

  # 変更後(概念的に):
  ActiveSupport::Duration.parse(value.to_s.strip)
end

※実際のコードは若干異なる可能性がありますが、意味としては「文字列に変換して前後の空白を削る」の 1 行の修正です。

テスト

activesupport/test/xml_mini_test.rb に 1 行のテストケースが追加されています。
内容は、インデント付きの type="duration" ノードを含む XML を Hash.from_xml に渡したときに、ActiveSupport::Duration(たとえば 1.hour)として正しくパースされることを検証するものです。

これにより、今回の回避策が将来のリグレッションで壊れないようにカバーされています。


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

    • ActiveSupport::XmlMini 経由で Hash.from_xml を用い、type="duration" のノードを扱う箇所すべてに影響があります。
    • インデントや改行を含む「整形された XML」や、人手で書かれた XML を扱うアプリケーションで特に恩恵があります。
    • これまで type="duration" だけ他の型と比べて扱いがシビアだった点が解消され、datedatetime などと同様にプレーンテキストに余分な空白があっても安全にパースされるようになります。
  • 後方互換性 / 互換性リスク

    • 前後の空白を strip するだけなので、通常の利用ではほぼリスクはありません。
    • ISO8601 の duration 文字列の意味的に、先頭・末尾の空白に意味を持たせるケースは現実的には想定されず、互換性問題はほぼ発生しないと考えられます。
    • もし「わざと先頭/末尾に空白を入れてパースエラーを起こさせる」といった挙動に依存しているコードがあれば、その挙動は変化します(エラーにならずパースに成功するようになる)が、そのような依存は一般的にはありえないはずです。
  • 運用・テスト上の注意

    • 既存の XML をそのまま Hash.from_xml に渡して duration 周りでエラーになっていた場合、今回の変更を含む Rails バージョンに上げることで自然に解消されます。
    • XML の仕様やフォーマッタの変更は不要です。既存の pretty-print やインデント付き XML をそのまま利用できます。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58015
    タイトル: Strip surrounding whitespace before parsing a duration in Hash.from_xml
    作者: @55728
  • 関連クラス:
    • ActiveSupport::XmlMini
    • ActiveSupport::Duration::ISO8601Parser
  • 類似仕様:
    • type="date", type="datetime", type="integer", type="boolean" など他の型では、既にプレーンテキストの前後空白に起因する問題は発生していませんでしたが、duration もその挙動に揃えられた形になります。

#58014 Remove stale payload-based filter example in EventReporter#subscribe rdoc

マージ日: 2026/7/6 | 作成者: @55728

  1. 概要 (1-2文で)
    EventReporter#subscribe の RDoc に載っていた「payload によるフィルタ」サンプルコードが、実際の挙動と矛盾しており誤解を招くため削除され、フィルタから参照できるのはイベント名のみであることを明示するドキュメント修正です。コードの挙動には一切変更はなく、ドキュメントのみの更新です。

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

変更の背景

Rails.event.subscribe の RDoc には、フィルタの例として次の2パターンが記載されていました(問題のある方だけ抜粋):

ruby
Rails.event.subscribe(subscriber) { |event| event[:payload].is_a?(UserEvent) }

しかし、実際にフィルタブロックに渡される event は「イベント名だけを持つハッシュ」であり、event[:payload] は常に nil になります。そのため、このサンプルをコピペして使うと、条件が常に false になり、イベントを一件も受け取れないという不具合を招きます(ただしコード上は例外も出ないため気づきにくい)。

モジュールレベルの「Filtered Subscriptions」の説明ではすでに:

Note that only the +:name+ key is available to filters, not the entire payload.

と明記されており、#subscribe の例だけがこれと矛盾していました。

実際の変更内容

変更ファイル: activesupport/lib/active_support/event_reporter.rb

行レベルでは以下が行われています(概要ベース):

  • #subscribe の RDoc から、event[:payload] を使ったフィルタ例を削除
  • 代わりに、「フィルタから使えるのは :name キーのみであり、payload 全体は参照できない」という注意書きを、クラス(モジュール)レベルの説明と同様に #subscribe のドキュメントにも追記

具体的な正しい利用イメージは、たとえば次のような形になります:

ruby
# OK: イベント名でフィルタする例
Rails.event.subscribe(->(event) { Rails.logger.info(event) }) do |event|
  event[:name] == "user.created"
end

event で使えるのは event[:name] だけで、payload はここには来ない、という点を強調する形に修正されています。


  1. 影響範囲・注意点
  • 影響範囲は「ドキュメントのみ」であり、ActiveSupport::EventReporter / Rails.event.subscribe の挙動そのものは以前から変わっていません。
  • すでに event[:payload] をフィルタ内で参照していたコードがある場合、それはこの PR の前から期待通りには動いておらず、「常に false になってイベントが一件も流れない」状態だった可能性が高いです。
    • もしそのようなコードが見つかった場合:
      • フィルタ部分では「イベント名のみ」を使ってフィルタする
      • payload ベースの条件分岐は「サブスクライバー側(購読ブロックの中)」で行う
        といった構成に変更する必要があります。
  • テストや実装を壊す類の変更ではないため、Rails を更新したことで挙動が変わることはありませんが、「今までたまたま気づかずに誤ったサンプルを真似していた」ケースを洗い出す良いトリガーにはなります。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58014
  • 関連ドキュメント: ActiveSupport::EventReporter の「Filtered Subscriptions」セクション
    • フィルタに渡されるハッシュは {:name => "event_name"} のように :name のみを保持し、payload は含まれないという仕様が明文化されています。

#58005 Make add_column(if_not_exists: true) reversible

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

  1. 概要 (1-2文で)
    add_column(..., if_not_exists: true) を含むマイグレーションをロールバックした際に、逆操作として不正なオプションを付けた remove_column が生成されていた問題を修正し、正しく if_exists: true が付くようにした PR です。これにより、該当マイグレーションのロールバックが安全かつ他のコマンドと一貫した挙動になります。

  1. 変更内容の詳細

何が問題だったか

以下のようなマイグレーションを書いた場合:

ruby
add_column :users, :age, :integer, if_not_exists: true

rails db:rollback などで逆マイグレーションを生成・実行すると、これまでは CommandRecorder によって次のように解釈されていました:

ruby
# BEFORE (誤った逆操作)
remove_column :users, :age, :integer, if_not_exists: true

しかし remove_columnif_not_exists オプションを受け付けません。そのため:

  • 逆マイグレーションとして不正なメソッド呼び出しが生成される
  • 将来的にはエラーになったり、意図しない挙動を起こす可能性がある

という問題がありました。

今回の修正内容

ActiveRecord::Migration::CommandRecorder において、add_column の逆操作 (invert_add_column) を組み立てる際のオプション変換ロジックが追加されました。

挙動は次のように変わります:

ruby
# 正方向
add_column :users, :age, :integer, if_not_exists: true

# 逆方向 (今回の PR 以降)
remove_column :users, :age, :integer, if_exists: true

PR の説明にもある通り:

ruby
# inverse of add_column(:table, :column, :type, if_not_exists: true)
# before: remove_column(:table, :column, :type, if_not_exists: true)
# after:  remove_column(:table, :column, :type, if_exists: true)

つまり、if_not_existsif_exists に変換してから remove_column に渡すようになりました。

他のコマンドとの一貫性

add_index, add_foreign_key, add_check_constraint ではすでに同様の変換が実装されており:

  • add_*(..., if_not_exists: true)
    の逆操作として
  • remove_* ( ..., if_exists: true)

が生成されています。
今回の PR で add_column もその設計方針に揃えられました。

テスト

activerecord/test/cases/migration/command_recorder_test.rb にテストが追加され、add_column(..., if_not_exists: true) を記録して逆変換した際に if_exists: true が付いた remove_column が生成されることが検証されています。


  1. 影響範囲・注意点
  • 対象:
    • add_column :table, :column, :type, if_not_exists: true を使用しているマイグレーション
    • それらを rails db:rollbackrevert で巻き戻すケース
  • 影響:
    • 以前は「見かけ上」remove_column(..., if_not_exists: true) が生成されていましたが、これはそもそも正当な API ではありませんでした。
    • 今後は正しく remove_column(..., if_exists: true) になるため、「すでにカラムが削除されている場合」でもエラーにならない、より安全なロールバックになります。
  • 後方互換性:
    • 公開されていない・公式にサポートされていない挙動(remove_columnif_not_exists を渡す)を修正する変更であり、通常のアプリケーションコードには実害はほぼありません。
    • もしテストやアプリケーション側で CommandRecorder の生の出力を文字列ベースで厳密に検証している場合は、if_not_existsif_exists への変更によりテストが変わる可能性があります。
  • マイグレーションの書き方としての注意:
    • 今後も、冪等にしたいマイグレーションでは:
      • 追加系: add_column ..., if_not_exists: true
      • 削除系: remove_column ..., if_exists: true を明示的に書くのが推奨されますが、今回の修正により、逆操作生成の際にもこのペアが自動で維持されるようになります。

  1. 参考情報 (あれば)
  • 当該 PR: rails/rails #58005 “Make add_column(if_not_exists: true) reversible”
  • 関連する他の reversible な API 例:
    • add_index(..., if_not_exists: true)remove_index(..., if_exists: true)
    • add_foreign_key(..., if_not_exists: true)remove_foreign_key(..., if_exists: true)
    • add_check_constraint(..., if_not_exists: true)remove_check_constraint(..., if_exists: true)

これらと同様のパターンが add_column にも適用された、という位置づけの変更です。


#58004 Return no rows without a query for none.ids

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

  1. 概要 (1-2文で)
    Relation#none に対して ids / async_ids を呼び出したとき、これまでは実際に SELECT ... WHERE (1=0) のクエリが発行されていたのを、pluck と同様にクエリを投げずに常に空配列(またはそれを返す Promise)を返すように修正した PR です。none が「DB に触れない null object」として一貫した動作をするようになります。

  1. 変更内容の詳細

背景

ActiveRecord::Relation#none は「必ず結果が 0 件で、かつ DB にクエリを投げない」ことを意図した null relation です。

  • 既に pluck など一部メソッドは @none フラグを見て、DB に触れずに結果を返していました。
  • 一方で idsnone を特別扱いしておらず、内部の条件が "1=0" という生の SQL だったため、矛盾判定(例: where(id: []))に引っかからず、実クエリが発行されていました。

その結果、以前は:

ruby
Post.none.ids        # => 実際には SELECT "posts"."id" FROM "posts" WHERE (1=0)
Post.none.pluck(:id) # => [] (クエリ無し)

という不整合な挙動になっていました。

何をしたか

calculations.rb 内の ids / async_ids に、pluck と同様の @none チェックを追加しています。

擬似コードレベルでは以下のようなガードが入っています(実装からの意訳):

ruby
def ids
  if @none
    []
  else
    # 従来どおりの ids のロジック(pluck(:id) 相当)
  end
end

def async_ids
  if @none
    # すでに完了した Promise(Future)を返し、その値は [] になる
  else
    # 従来どおり非同期で ids を取得
  end
end

結果として:

ruby
Post.none.ids        # => []  (クエリ発行なし)
Post.none.pluck(:id) # => []  (クエリ発行なし)
Post.none.async_ids  # => 完了済み Promise, value は [] (クエリ発行なし)

となり、none の定義(「DB に触れず常に空」)とテストの期待どおりの挙動になります。

テスト

activerecord/test/cases/null_relation_test.rb にテストが追加され、null_relation に対する ids の挙動(クエリを投げずに空配列を返すこと)がカバーされています。既存の pluck, delete_all, update_all, exists?, count などのテストと並ぶ形で「null relation は DB に触れない」という性質の一貫性を確認しています。


  1. 影響範囲・注意点
  • 対象メソッド

    • ActiveRecord::Relation#ids
    • ActiveRecord::Relation#async_ids
  • 挙動の変化

    • Model.none.ids が、これまでの「SELECT ... WHERE (1=0) を実行して空配列を返す」から、「クエリを打たずに即座に空配列を返す」に変わります。
    • async_ids も同様に、「クエリを打たない完了済み Promise(値は [])」を返します。
  • 後方互換性

    • 戻り値([])は変わらないため、普通にアプリ側で none.ids を使っている限りは機能的な互換性は保たれています
    • 影響し得るのは、以下のようなケースです:
      • none.ids でクエリが走ること」を前提にしてメトリクス・ログ収集・モンキーパッチなどをしていた場合(ほぼ無いはずですが)。
      • 「クエリが走るかどうか」を検査するテストを書いていた場合(assert_sql などで WHERE (1=0) を期待しているようなテストは失敗する可能性があります)。
  • パフォーマンス

    • none を通るパスについては、不必要なクエリが完全に削除されるため、ごくわずかですがパフォーマンス改善になります。
    • 特に大量に none が生成される複雑なスコープチェーンで ids を呼んでいるようなケースでは、微妙なオーバーヘッド削減になります。
  • 一貫性

    • none が関与する以下のメソッドの挙動が揃います:
      • pluck, ids, async_ids, delete_all, update_all, exists?, count, …
    • none からは絶対にクエリを出さない」という前提でコードや最適化・テストが書けるようになります。

  1. 参考情報 (あれば)
  • 修正対象: activerecord/lib/active_record/relation/calculations.rb
  • テスト: activerecord/test/cases/null_relation_test.rb
  • 関連知識:
    • Relation#none は「常に空」の relation を返すための標準 API で、default_scope, 条件付きスコープ, 認可ロジックなどで「このユーザーには 1 件も見せない」ことを表現するときに頻用されます。
    • ids は内部的に pluck(primary_key) に近い実装になっており、今回の修正で none に対する挙動が pluck と明示的に揃えられました。

#58007 Instrument the normalized key in MemoryStore#increment/#decrement

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

  1. 概要 (1-2文で)
    ActiveSupport::Cache::MemoryStoreincrement / decrement が発行する計測イベント(cache_increment / cache_decrement)について、これまで「生のキー」を送っていたものを、他のストア実装と同様に「正規化済み(名前空間付き)のキー」を送るように揃えた変更です。これにより、メモリストア利用時の計測イベントのキー情報が一貫したものになります。

  1. 変更内容の詳細

何が問題だったか

MemoryStore#increment / #decrement が ActiveSupport のインストゥルメンテーション(ActiveSupport::Notifications)で発行するイベント:

  • cache_increment.active_support
  • cache_decrement.active_support

のペイロード :key に、キャッシュストア内部で実際に使われている「正規化後キー」ではなく、呼び出し側が渡した「生のキー」をそのまま入れていました。

ruby
cache = ActiveSupport::Cache::MemoryStore.new(namespace: "myapp")

cache.increment("counter")
# 以前:
#   payload[:key] #=> "counter"           (namespace が反映されていない)
#
# 期待される & 今回の修正後:
#   payload[:key] #=> "myapp:counter"

他のストア (FileStore, MemCacheStore, RedisCacheStore) や同じ MemoryStoreread / write イベントでは、すでに「正規化済みキー」を使っていたため、インクリメント/デクリメントだけ挙動がずれていました。

具体的な修正内容

変更ファイル:

  • activesupport/lib/active_support/cache/memory_store.rb
  • activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb

ポイント:

  1. MemoryStore#increment / #decrement のインストゥルメンテーション時に、payload[:key] として 正規化後のキー を渡すように修正

    • 正規化処理は normalize_key / normalize_key 相当の内部ヘルパを通す形(既存の read/write と同じパス)に変更されたと考えられます。
    • 実質的には namespace などが反映された「実際にストア内で利用しているキー」をイベントのキーとして報告します。
  2. テスト (cache_instrumentation_behavior) の修正

    • インクリメント/デクリメントの計測イベントにおける payload[:key] が名前空間付きであることを確認するアサーションが追加・修正。
    • これにより、将来的に再度「生のキー」を返すような退行があった場合に検出できるようになっています。

サンプル(概念的なイメージ):

ruby
cache = ActiveSupport::Cache::MemoryStore.new(namespace: "myapp")

ActiveSupport::Notifications.subscribe("cache_increment.active_support") do |*args|
  event = ActiveSupport::Notifications::Event.new(*args)
  puts event.payload[:key]
end

cache.increment("counter")
# 修正前: "counter"
# 修正後: "myapp:counter"

  1. 影響範囲・注意点
  • 影響対象
    • ActiveSupport::Cache::MemoryStore を使っていて
    • cache_increment.active_support / cache_decrement.active_support イベントを購読し
    • payload[:key] を直接参照しているコード

に影響があります。

  • 挙動の変化

    • namespace オプションを使っていない場合:
      正規化後キーも元キーと同じになるため、実質的な変化はほぼありません。
    • namespace を使っている場合:
      これまで payload[:key]"counter" だったものが、"myapp:counter" のように名前空間付きになります。
    • それ以外にも、normalize_key で行っている正規化(#to_param や encoding 調整など)があれば、その結果が反映されます。
  • 後方互換性の観点

    • 監視基盤やメトリクス集計で、「このイベントの key は必ず非 namespaced な生キー」という前提でロジックを書いていた場合、集計キーが変わる可能性があります。
    • 他ストア(File/MemCache/Redis)と挙動を合わせた変更のため、「複数ストアで共通の購読コード」を書いているプロジェクトでは、むしろ今回の変更で挙動が一貫し、扱いやすくなります。
  • 対応の指針

    • もし「アプリケーションレベルの論理名のキー」と「実際のストアキー(namespace 付き)」を区別してメトリクスを取りたい場合は、今後は以下のような設計を検討するとよいです:
      • イベントから受け取った payload[:key] を「実キー」とみなし、
      • 論理キーは別途 payload に入す(独自インストゥルメンテーション)か、キー生成側でロギングする。

  1. 参考情報 (あれば)
  • 該当 PR: Instrument the normalized key in MemoryStore#increment/#decrement (#58007)
  • 関連する他ストアの挙動:
    • ActiveSupport::Cache::FileStore
    • ActiveSupport::Cache::MemCacheStore
    • ActiveSupport::Cache::RedisCacheStore
      これらはいずれもインストゥルメンテーションで「正規化済みキー」を報告しており、今回の変更で MemoryStore もそれに揃った形です。
  • 関連テスト: activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb
    ここを読むと、ActiveSupport が「計測イベントにどういう情報を載せることを期待しているか」のリファレンスとしても利用できます。

#58008 Clear a stale enqueue_error when perform_all_later re-enqueues a job

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

  1. 概要 (1-2文で)
    perform_all_laterenqueue_all 未実装アダプタ向けに「1件ずつ enqueue」するフォールバック経路を使う際、過去の失敗時にセットされた enqueue_error がクリアされず、状態が矛盾する問題を修正したPRです。これにより、再enqueueに成功したジョブは successfully_enqueued? == true かつ enqueue_error == nil という一貫した状態になります。

  1. 変更内容の詳細

背景

  • Active Job には複数ジョブをまとめてキューに積む perform_all_later があり、アダプタが enqueue_all を実装していない場合は、内部的に各ジョブに対して個別に enqueue を呼ぶフォールバックが行われます。
  • 単一ジョブの enqueue 経路では、enqueue を試行する前に enqueue_error をクリアする仕様になっており、「前回の enqueue 失敗情報を引きずらない」ようになっています。
  • しかし、perform_all_later のフォールバック経路では successfully_enqueued フラグはリセットしていた一方で、enqueue_error をクリアしていませんでした。

問題となっていた挙動

  • あるジョブが何らかの理由で enqueue に失敗し、enqueue_error がセットされた状態になったとします。
  • そのジョブを後で perform_all_later のフォールバック経路(=enqueue_all 非対応アダプタ)経由で再enqueueし、今度は成功した場合でも、古い enqueue_error が残ってしまっていました。
  • この結果、同じジョブインスタンスに対して:
ruby
job.successfully_enqueued? # => true
job.enqueue_error          # => #&lt;SomeError ...> (古いエラーが残っている)

という矛盾した状態が起きていました。

このPRでの修正内容

  • フォールバック経路(各ジョブを個別に enqueue し直すロジック)の中で、再enqueueを試みる前に enqueue_error もクリアするように変更。
  • 単一ジョブの enqueue 経路と同様に、「enqueue を試行する際には過去のエラー情報をリセットする」挙動に揃えています。

擬似的には以下のようなイメージです(実際のコードは簡略化):

ruby
jobs.each do |job|
  job.successfully_enqueued = false
  job.enqueue_error = nil      # ← この行が追加されたイメージ
  queue_adapter.enqueue(job)
end

テストの追加

activejob/test/cases/queuing_test.rb にテストが追加され、以下のようなケースが検証されています:

  • 以前の enqueue 失敗で enqueue_error がセットされているジョブがある。
  • perform_all_later 実行時、アダプタが enqueue_all を実装していないため 1件ずつ enqueue される。
  • enqueue が成功したジョブは:
    • successfully_enqueued?true
    • enqueue_errornil であることを確認。

  1. 影響範囲・注意点
  • 対象となるのは「enqueue_all を実装していない Active Job アダプタ」を使用し、かつ「同じジョブインスタンスを再利用して perform_all_later で再enqueue する」ようなケースです。
  • これまで「ジョブは正常に enqueue されているのに enqueue_error が残っている」状態に依存したコード(例えば、enqueue_error の有無だけで前回の失敗を判定していたりする処理)があれば、挙動が変わります。ただし、本来は不整合な状態だったため、通常は修正として望ましい挙動です。
  • enqueue_all を実装しているアダプタにはこの変更は直接影響しません(そちらは元々 enqueue_all の実装側の責務になります)。

  1. 参考情報 (あれば)
  • 目的は「単一ジョブ enqueue とバッチ enqueue(perform_all_later のフォールバック)の状態管理を対称に保つ」ことです。
  • Active Job でカスタムアダプタや enqueue_all の実装を行う場合も、同様に「成功したジョブには enqueue_error を残さない」という一貫性を持たせるのが望ましい設計になります。

#58009 Account for queue_name_prefix in assert_enqueued_email_with

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

  1. 概要 (1-2文で)
    ActiveJob::Base.queue_name_prefix を設定している場合でも、assert_enqueued_email_with が正しくジョブのキュー名を判定できるように修正した PR です。これにより、メール自体は正しく deliver_later されているのにテストだけが失敗する問題が解消されます。

  1. 変更内容の詳細

問題の背景

config.active_job.queue_name_prefix を設定すると、Active Job ではキュー名が以下のように変更されます:

ruby
config.active_job.queue_name_prefix = "prefix"

# 通常 "mailers" になるところが "prefix_mailers" になる
TestMailer.test.deliver_later

しかし、これまでの ActionMailer::TestHelper#assert_enqueued_email_with は、
「メール系ジョブは mailers キューに入るはず」という前提で期待キュー名を決めており、queue_name_prefix などの設定を考慮していませんでした。

そのため、以下のようなコードはジョブ自体は正しく prefix_mailers に enqueue されているにもかかわらず、テストが「mailers に入っていない」と誤判定して失敗していました。

ruby
config.active_job.queue_name_prefix = "prefix"

assert_enqueued_email_with(TestMailer, :test) do
  TestMailer.test.deliver_later
end
# before: 失敗 (期待 "mailers" / 実際 "prefix_mailers")
# after:  成功

具体的な修正点

変更ファイルは 2 つだけです。

  1. actionmailer/lib/action_mailer/test_helper.rb

    • assert_enqueued_email_with 内で「期待するキュー名」を決定する処理を、実際の配送ジョブ(ActionMailer::MailDeliveryJob 等)が enqueue 時にキュー名を決定するロジックと同じ方法で導出するように修正。
    • つまり、queue_name_prefixqueue_name_suffix が設定されていれば、それを含んだ完全なキュー名(例: "prefix_mailers", "prefix_mailers_suffix")を期待値として使うようになりました。
    • コード上は 1 行の差し替えで、直接 "mailers" を見るのではなく、ActiveJob が使うキュー名解決ロジックを経由する形になったと考えられます。
  2. actionmailer/test/test_helper_test.rb

    • この挙動を保証するテストが追加されています (+15 行)。
    • 内容としては、config.active_job.queue_name_prefix を設定した状態で TestMailerdeliver_later を呼び、assert_enqueued_email_with がパスすることを検証するテストです。
    • これにより、今後 queue_name_prefix の仕様変更や assert_enqueued_email_with の改修があっても、プレフィックス付きキュー名への対応が壊れないようにしています。

  1. 影響範囲・注意点
  • 影響を受けるのは「ActionMailer::TestHelper#assert_enqueued_email_with を使っており、かつ config.active_job.queue_name_prefix(または suffix)を使っているプロジェクト」です。

    • これまで、そのようなプロジェクトでは「メールは正常にキューに積まれているのにテストが落ちる」という症状が出ていたはずです。
    • この PR 適用後は、そのようなテストは通るようになります。
  • メール配送自体の挙動 (deliver_later 時の queue や実行タイミング) には変更がなく、「テスト用ヘルパーの期待値ロジックのみ」が修正対象です。そのため、本番環境の挙動変更リスクはほぼありません。

  • もし既にワークアラウンドとして

    • 自前で queue: "prefix_mailers" を指定して検査していたり、
    • assert_enqueued_email_with を使わずに低レベルな assert_enqueued_jobs などで検査していた といったケースがある場合、この修正によりワークアラウンドが不要になる可能性があります。
  • queue_name_suffix も「prefix と同様の仕組みで解決」されるため、この PR によって suffix を用いた構成でも期待どおりに動作するようになります(prefix/suffix を総合した「実際のキュー名」にテストが揃う)。


  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/58009
  • 関連する Rails ガイド:
    • Active Job Basics: Queue configuration
      • config.active_job.queue_adapter
      • config.active_job.queue_name_prefix
      • config.active_job.queue_name_delimiter
      • config.active_job.queue_name_suffix
  • 関連 API:
    • ActionMailer::TestHelper#assert_enqueued_email_with
    • ActiveJob::Base.queue_name_prefix / queue_name_suffix
    • ActionMailer::MailDeliveryJob(メール配送ジョブの実装)

#58010 Support endless and beginless ranges in number_field/range_field

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

  1. 概要 (1-2文で)
    number_field / range_field ヘルパで、in: / within: オプションに無限範囲 (18.. など) や片側無限範囲 (..10 など) を渡したときに RangeError になっていた問題を修正し、適切に min / max 属性へ変換できるようにした PR です。Ruby の end/beginless Range を正式にサポートしつつ、従来の閉区間・排他的区間の挙動は維持しています。

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

変更の主眼

  • 対象ヘルパ:
    • ActionView::Helpers::FormOptionsHelper#number_field
    • ActionView::Helpers::FormOptionsHelper#range_field
  • オプション:
    • in: / within: に渡される Range の扱いを修正

以前は、以下のような範囲を渡すと RangeError が発生していました。

ruby
number_field("order", "quantity", in: 18..) # endless range
number_field("order", "quantity", in: ..10) # beginless range

この PR により、以下のように HTML 属性へマッピングされるようになります。

Endless range (18..)

ruby
number_field("order", "quantity", in: 18..)
# 変更前: RangeError
# 変更後: <input type="number" name="order[quantity]" min="18" />
#         (max 属性は付与されない)

Beginless range (..10)

ruby
number_field("order", "quantity", in: ..10)
# 変更前: RangeError
# 変更後: <input type="number" name="order[quantity]" max="10" />
#         (min 属性は付与されない)

既存の bounded range の挙動はそのまま

  • 通常の閉区間(両端あり、..):

    ruby
    number_field("order", "quantity", in: 1..10)
    # <input ... type="number" min="1" max="10" />
  • 排他的範囲 (...) も従来通り:

    ruby
    number_field("order", "quantity", in: 1...10)
    # => max は「上限 - 1」としてレンダリングされる
    # <input ... type="number" min="1" max="9" />

PR 説明によると、この既存の「exclusive range は max を 1 小さくする」ロジックもそのまま維持されています。

実装変更の概要

  • actionview/lib/action_view/helpers/tags/number_field.rbRange から min/max を決定する部分を、endless / beginless の Range を許容するように 1 行修正。
  • actionview/test/template/form_helper_test.rb にテストケースを追加 (+10 行):
    • endless range を渡したときに min のみ設定されること
    • beginless range を渡したときに max のみ設定されること
    • 例外が発生しないこと

テスト追加により、今後のリグレッションも検知できるようになっています。


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

    • number_field, range_fieldin: / within: オプション付きで利用しているフォームビューが対象。
    • 特に Ruby 2.6+ などで導入された end/beginless range 構文を既に利用しているコードで、以前は例外が発生していたケースが正常に動作するようになります。
    • フロントエンド的には、min / max 属性の有無だけが変わるため、HTML5 バリデーションロジックや、JavaScript 側で min/max の有無に依存している場合に影響し得ます。
  • 仕様面の注意点

    • in: / within: に Range を渡した場合の挙動は以下に整理されます:
      • 両端あり (a..b) → min=a, max=b
      • 排他的 (a...b) → min=a, max=b-1
      • endless (a..) → min=a, max なし
      • beginless (..b) → max=b, min なし
    • Ruby の「通常の Range オブジェクトを受け付ける」という Rails のドキュメント仕様と整合しており、RangeError を投げてしまうこれまでの挙動がよりどころのない状態だったため、それを是正する変更といえます。
    • これにより、サーバーサイドバリデーション (validates :field, inclusion: a..) とフォーム側の表示 (number_field ..., in: a..) を、end/beginless range を含めて一貫して書きやすくなります。
  • 後方互換性

    • これまで RangeError をトリガーする前提で rescue するようなコードを書いていた場合、その挙動は変わる可能性がありますが、通常のアプリケーションではそういう利用はまず想定されないため、実質的には後方互換性を損なわない変更と考えられます。
    • bounded range (特に 1...10 のような排他的 range) の既存挙動には一切変更がない点がポイントです。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58010
  • 関連する Rails ドキュメント:
    • Action View Form Helpers – number_field, range_field:in / :within オプション
  • Ruby の endless / beginless range 仕様:
    • a.. / ..b は Ruby 2.6 以降で導入された Range の構文であり、通常の Range オブジェクト ((18..) === 20 #=> true) として扱われます。

#58006 Return the current value for an unchanged store accessor's _was

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

  1. 概要 (1-2文で)
    store アクセッサに対する <key>_was の挙動が、変更されていない場合にも nil を返してしまう不整合を修正し、ActiveModel::Dirty の契約どおり「未変更なら現在値を返す」ように揃えた PR です。これにより、store カラムを使う場合も通常の属性と同様の Dirty API の挙動が得られます。

  1. 変更内容の詳細

挙動の変更点

修正前(未変更時):

ruby
# 例: jsonb カラム `settings` に対して store_accessor で color を定義
# record.settings = { "color" => "black" }
record.color         # => "black"
record.color_was     # => nil  # ← 本来期待される挙動と異なる

修正後(未変更時):

ruby
record.color         # => "black"
record.color_was     # => "black"  # ← 通常の attribute と同じ挙動

値を変更した後の挙動は従来どおりで、過去の値を返します:

ruby
record.color         # => "black"
record.color = "white"

record.color_was     # => "black"      # 変更前の値
record.color_change  # => ["black", "white"]  # こちらは既に修正済みだった挙動と整合
record.color_changed? # => true

どのように修正されたか(概念レベル)

  • activerecord/lib/active_record/store.rb の store アクセッサ定義部分で、<key>_was が未変更時に nil を返さないように 1 行分のロジックが修正されています。
  • ActiveModel::Dirty#attribute_was と同様、「その属性が変更されていなければ、_was は現在値を返す」ように挙動を合わせています。
  • これまでに _change<key>_change)については未変更時の挙動が既に修正済みだったため、今回の変更で _was も含めて Dirty 系メソッド間で一貫した挙動になります。

テスト

activerecord/test/cases/store_test.rb に 9 行追加されています。

想定されるテスト内容(要約):

  • store アクセッサを持つモデルを用意し、レコードを保存した直後(未変更状態)に <key>_was を呼び出すと現在値を返すことを確認。
  • 値を更新した後は、変更前の値を返すことを確認。
  • _change や Dirty 判定との整合性を担保。

  1. 影響範囲・注意点

影響範囲

対象となるのは:

  • store / store_accessor を使っているモデル
  • かつ、<key>_was を明示的に参照しているコード

に限定されます。

これまで:

  • 「store アクセッサの <key>_was は、未変更時には nil を返す」と仮定したコード(ワークアラウンド)を書いていた場合、その前提が崩れます。

これから:

  • 通常の ActiveRecord 属性と同様に、
    • 未変更時: <key>_was は現在値
    • 変更後: <key>_was は変更前の値 となります。

移行・互換性上の注意点

  • もし次のようなコードを書いていた場合は挙動が変わります:

    ruby
    # 例: nil かどうかで「未設定かどうか」を判定しているケース
    if record.color_was.nil?
      # 以前は「未変更か、元々 nil」かを区別できないが、
      # 実際には「未変更 = nil」「変更済み = 過去値(non-nil)」のように使っていた可能性がある
    end

    このような用途には Dirty API を直接使う方が正確です:

    ruby
    if record.color_changed?
      # 変更された場合の処理
      before, after = record.color_change
    end
  • 「未変更かどうか」を知りたいなら _was ではなく

    • <key>_changed?
    • <key>_change
    • saved_change_to_<key>? / saved_change_to_<key>(永続化後の変更検知) を利用するのが筋となります。
  • nil 自体を「未設定」の意味で使っている store キーの場合、

    • 未変更時に <key>_wasnil を返す挙動は変わりません(値が nil だから)。
    • ただし、以前は「未変更だが nil」と「変更済みだが元値が nil」を _was.nil? では区別できなかった点は変わらず、Dirty API(_changed? 等)で区別する必要があります。

  1. 参考情報 (あれば)
  • 関連 API:
    • ActiveModel::Dirty#attribute_was
    • ActiveModel::Dirty#attribute_change
    • ActiveModel::Dirty#attribute_changed?
  • store / store_accessor の概要:
    • store :settings, accessors: %i[color size]
      • record.color / record.color=settings["color"] を透過的に扱う
  • この PR で _change_was の挙動が揃ったため、store アクセッサも「普通のカラム属性」と同じ感覚で Dirty 情報を扱えるようになっています。

#58011 Honor an explicit :selected option in weekday_select

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

  1. 概要 (1-2文で)
    weekday_select ヘルパに selected: オプションを明示的に渡した場合でも、その値が正しく優先されるように挙動が修正されました。これにより、他の select 系ヘルパ (select, collection_select, grouped_collection_select, date_select) と一貫した動きになります。

  1. 変更内容の詳細

これまでの挙動

weekday_select は、フォームオブジェクト(例: @digest)側に既に曜日の値が入っていると、selected: オプションを無視してオブジェクトの値を選択状態にしていました。

ruby
# モデル側の値
@digest.send_day #=> "Monday"

# ビュー側
f.weekday_select(:send_day, selected: "Friday")

上記のように書いても、実際に選択されるのは "Monday" でした。

修正後の挙動

同じコードを実行した場合、selected: "Friday" が優先され、 "Friday" が選択状態になります。

ruby
# 修正後
f.weekday_select(:send_day, selected: "Friday")
# => セレクトボックス上では "Friday" が選択される

実装面の変更ポイント

  • actionview/lib/action_view/helpers/tags/weekday_select.rb のロジックが 1 行修正され、
    「オブジェクトの属性値」よりも「オプションで明示された :selected」を優先するようになりました。
  • actionview/test/template/form_options_helper_test.rb にテストが追加され、
    • オブジェクトに曜日がセットされている
    • かつ selected: オプションが指定されている
      というケースで、明示的 selected が優先されることが保証されています。

他の select 系ヘルパと同様に、「明示的に指定された selected があればそれを使う」という Rails の一貫したポリシーに合わせた変更です。


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

    • weekday_select を使用しており、かつモデルにデフォルトの曜日が格納されているフォームで selected: オプションも指定している場合、表示される初期選択が変わる可能性があります。
    • これまで「オブジェクトの値が優先される」前提で selected: を“ダミー”的に書いていたコードは、明示指定の方が効くようになるため挙動が変わります。
  • 互換性の観点

    • 他の select 系ヘルパと同じルールになるため、API 全体としては一貫性が向上します。
    • ただし、weekday_select だけを個別仕様として利用していた場合は挙動変更となるため、フォームの初期選択状態の変化に注意してください。
  • テスト

    • 対応するテストが追加されているため、selected: の優先挙動は今後も守られることが期待できます。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58011
  • 類似の挙動を持つヘルパ:
    • ActionView::Helpers::FormOptionsHelper#select
    • ActionView::Helpers::FormOptionsHelper#collection_select
    • ActionView::Helpers::FormOptionsHelper#grouped_collection_select
    • ActionView::Helpers::DateHelper#date_select

これらと同じく、「オブジェクトの値 < 明示的な selected オプション」という優先順位になった、と理解しておくと良いです。


#58003 Fix belongs_to change tracking for composite foreign keys

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

  1. 概要 (1-2文で)
    belongs_to に複合外部キー(Composite Foreign Key)を使っている場合に、association_changed? / association_previously_changed? が常に false を返してしまうバグを修正したPRです。単一カラムの外部キーには影響せず、複合キーのときだけ正しく変更検知できるようにしています。

  1. 変更内容の詳細

問題点

belongs_to で複合外部キーを使うと、例えば以下のような関連づけになります:

ruby
class Cpk::Order < ApplicationRecord
  self.primary_keys = :shop_id, :id
end

class Cpk::Book < ApplicationRecord
  belongs_to :order,
    class_name: "Cpk::Order",
    foreign_key: [:shop_id, :order_id]
end

このとき Rails 内部では reflection.foreign_key が:

ruby
["shop_id", "order_id"]

という配列になっています。

ところが、BelongsToAssociation#target_changed? および #target_previously_changed? は、この reflection.foreign_key をそのまま「1つの名前」とみなして、スカラ用の dirty メソッドに渡していました:

ruby
attribute_changed?(reflection.foreign_key)            # 実際のイメージ
attribute_previously_changed?(reflection.foreign_key) # 実際のイメージ

スカラの dirty メソッド (attribute_changed?) は引数に対して to_s するので、配列は:

ruby
["shop_id", "order_id"].to_s
# => '["shop_id", "order_id"]'

となってしまい、「そんな属性名は存在しない」ため、常に false になっていました。その結果、belongs_to :order に対して自動生成される:

ruby
order_changed?
order_previously_changed?

などの公開APIが、複合キーのときだけ常に「変化なし」と報告してしまう状態でした。

単一カラムの外部キー ("shop_id" など) では to_s しても "shop_id" のままなので、この問題が表に出ず、これまで見逃されていました。

修正内容

BelongsToAssociation#target_changed?#target_previously_changed? を、「外部キーが配列のときは各カラムを個別にチェックする」ように変更しています。

擬似コードにすると、以下のようなイメージです:

ruby
def target_changed?
  Array(reflection.foreign_key).any? { |fk| owner.attribute_changed?(fk) }
end

def target_previously_changed?
  Array(reflection.foreign_key).any? { |fk| owner.attribute_previously_changed?(fk) }
end

ポイント:

  • Array(reflection.foreign_key) とすることで、
    • 複合キー: ["shop_id", "order_id"]["shop_id", "order_id"]
    • 単一キー: "shop_id"["shop_id"]
      のように正規化されます。
  • .any? で「どれか1つでも変わっていれば関連変更あり」と判断します。
  • これは同じファイル内で既に複合キー対応している他の処理 (replace_keys, foreign_key_present?) と同じパターンで、実装を揃えた形になっています。

ポリモーフィック belongs_to への影響

BelongsToPolymorphicAssociationsuper で上記ロジックを呼んだ上で、foreign_type もOR条件でチェックする実装になっています。

  • 複合キーの belongs_to でポリモーフィックなケースは、super の修正をそのまま継承。
  • ポリモーフィック外部キー自体はスカラなので、今回の修正で挙動が変わることはありません。

テスト

activerecord/test/cases/associations/belongs_to_associations_test.rb に回帰テストが追加されています。

テストのシナリオ(概念的な流れ):

  1. 複合主キーを持つ Cpk::Order を2つ作成(同じ shop_idorder_id だけ違う)。
  2. Cpk::Book を、最初の orderbelongs_to :order で紐づけて保存。
  3. その後、book.order = 別の order に付け替えて、shop_id は同じだが order_id を変更。
  4. ここで:
    • book.order_changed?true になることを確認。
  5. book.save 後に:
    • book.order_previously_changed?true になることを確認。

このテストは main ブランチでは両方 false になって失敗し、この修正を適用するとパスします。


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

    • 対象: belongs_to に複合外部キーを指定している関連。
    • 修正されるAPI:
      • association_changed?(例: order_changed?
      • association_previously_changed?(例: order_previously_changed?
    • 単一カラム外部キーの belongs_to の挙動は変わりません。
    • ポリモーフィック belongs_to の挙動も、実質的には従来通りです。
  • 意図的に変更していない箇所

    • saved_change_to_target? は今回は変更していません。
      • これはカウンタキャッシュや touch コールバック専用の内部APIで、ここだけ先に直すと:
        • after_update で新しい親のカウンタだけインクリメントされる
        • 一方で decrement_counters_before_last_save は古い親側のデクリメントをスキップしたまま
          という不整合が起き、カウンタキャッシュが膨らむ危険があります。
      • 複合キーに対するカウンタキャッシュ周りの扱いは #57608 で別途対応中であり、本PRでは「公開APIの association_changed? / association_previously_changed? のバグ修正」にスコープを限定しています。
  • 互換性面での注意

    • これまで「複合外部キーの belongs_to は常に *_changed?false になる」ことを前提にワークアラウンドを書いていたコードがあれば、今回の修正で挙動が変わる可能性があります。
      • 例: if !order_changed? を「常にここに入る」前提ロジックとして悪用(?)していた場合など。
    • 本来の意図した動作に修正されるものであり、一般的にはバグフィックスとして望ましい変更です。

  1. 参考情報 (あれば)
  • 該当PR: https://github.com/rails/rails/pull/58003
  • 関連する既知課題(複合キーとカウンタキャッシュまわり): https://github.com/rails/rails/pull/57608
  • 実際に影響を確認したい場合:
    • 複合外部キーの belongs_to を持つモデルで xxx_changed? / xxx_previously_changed? を呼び、外部キーの一部だけ変えたときに true になるかをテストすると、挙動差を確認できます。

#57991 Stop HTML-escaping JSON written by Coders::JSON

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveRecord::Coders::JSON が JSON を DB に書き込む際、意図されていた「HTML エスケープ無し (escape: false)」設定が実際には効いておらず、HTML エスケープされてしまっていた不具合を修正する PR です。これにより、JSON 型カラムや ActiveRecord::Type::Json との挙動が統一されます。

  1. 変更内容の詳細

問題点

ActiveRecord::Coders::JSON にはもともと、次のような方針がありました。

  • JSON を DB カラムに書き込むとき、HTML エスケープ(<\u003c など)をスキップするために、デフォルトオプションとして escape: false をマージする。

しかし実装上のバグにより:

  • エンコーダは「マージ済みの @options」ではなく、「呼び出し時に渡された生の options 引数」で初期化されていた。
  • その結果、escape: false が実際のエンコード処理に反映されておらず、HTML エスケープが有効のままだった。

具体例:

ruby
coder = ActiveRecord::Coders::JSON.new
coder.dump({ "k" => "<>&" })
# 修正前の実際の挙動:
# => "{\"k\":\"\\u003c\\u003e\\u0026\"}"

# 期待される挙動(ネイティブ JSON カラムと同じ):
# => "{\"k\":\"<>&\"}"

PR の説明で明示されている通り、修正前は:

  • {"k":"\u003c\u003e\u0026"}

が返されており、これは:

  • ネイティブ JSON カラム
  • ActiveRecord::Type::Json

によるシリアライズ結果と食い違っていました。

修正内容

ActiveRecord::Coders::JSON の内部で:

  • マージ後のオプション(escape: false を含む)を保持している @options
  • 元の引数として渡された options

のうち、エンコーダの初期化に @options を使うように 1 行だけ修正されています。

擬似コードで表すと、だいたい以下の違いです(実際のコードは若干異なりますが、意味としてはこういう修正):

ruby
# 修正前 (イメージ)
def initialize(options = {})
  @options = DEFAULT_OPTIONS.merge(options) # escape: false をマージしている
  @encoder = SomeJSONEncoder.new(options)   # ← 生 options を使っていた
end

# 修正後 (イメージ)
def initialize(options = {})
  @options = DEFAULT_OPTIONS.merge(options)
  @encoder = SomeJSONEncoder.new(@options)  # ← マージ後オプションを使う
end

これにより、ActiveRecord::Coders::JSON を使った場合も、デフォルトで escape: false がきちんと有効になります。

テストの追加

activerecord/test/cases/coders/json_test.rb に 5 行のテストが追加され、以下を確認しています:

  • Coders::JSON.new.dump({ "k" => "<>&" }) の結果が {"k":"<>&"} となること
  • つまり、HTML エスケープされないこと

  1. 影響範囲・注意点

影響範囲

  • 対象: ActiveRecord::Coders::JSON を使って、DB の文字列カラムなどに JSON を保存しているケース。
    • 例: serialize :settings, ActiveRecord::Coders::JSON のような実装
  • 変更内容: デフォルトで HTML エスケープされていた JSON 出力が、今後はエスケープされなくなります。

互換性の観点

  • ネイティブ JSON カラム(json / jsonb)や ActiveRecord::Type::Json の挙動と揃うため、「Rails 内での一貫性」という意味では正しい方向です。
  • 既存データ:
    • 既に DB に保存済みの "k":"\u003c\u003e\u0026" などの値はそのまま残り、新規保存分のみ "k":"<>&" 風になります。
    • 読み込み時はどちらも JSON として問題なく扱えるため、デシリアライズ時の挙動に違いはほぼありません。

注意点

  • HTML への直接出力時のセキュリティ:

    • この変更は「DB に保存する JSON の文字列の中身」がエスケープされなくなる、という変更です。
    • Rails の view レイヤ (<%= %>raw, html_safe など) での XSS 対策とは別物です。
    • JSON をそのまま HTML に埋め込んでいるようなコード(例えば <script>var data = <%= @model.settings.to_json %>;</script> のようなもの)は、元々も適切な HTML エスケープと j ヘルパーなどによる JS エスケープを自前で考慮すべき領域です。この PR によってそこが安全になったり危険になったりするわけではありませんが、「DB に保存される値が変わる」ことは理解しておく必要があります。
  • テストの更新:

    • 自前で Coders::JSON のシリアライズ結果文字列を検証しているテストがある場合、"\u003c" を期待していた部分は "<" になるよう修正が必要です。

  1. 参考情報 (あれば)
  • 関連するコンポーネント:

    • ActiveRecord::Coders::JSON
    • ActiveRecord::Type::Json
    • DB のネイティブ JSON 型(PostgreSQL の json / jsonb など)
  • この PR により、以下の 3 者の JSON シリアライズ結果が揃うことが期待されます:

    1. ネイティブ JSON カラム (json, jsonb)
    2. ActiveRecord::Type::Json
    3. ActiveRecord::Coders::JSON(本 PR の対象)

HTML エスケープに依存していたコードがないかだけ確認しつつ、基本的には「バグ修正 + 挙動の統一」として受け入れてよい変更です。


#57990 Match HTTP auth scheme case-insensitively for Token and Bearer

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    HTTP の Authorization ヘッダにおける Token / Bearer 認証スキームの判定が、大文字小文字を区別しない(case-insensitive)形に修正されました。これにより、bearer abc123TOKEN xyz といった非標準の大文字小文字でも正しくトークンが認識されるようになります。

  1. 変更内容の詳細

2-1. 変更の背景

  • RFC 9110 §11.1 によると、Authorization ヘッダ内の auth-schemeBasic, Bearer, Token などのスキーム名)は 大文字小文字を区別しない と規定されています。
  • 現実のクライアントやプロキシは Bearer だけでなく bearer, BEARER などさまざまな表記で送ってくることがあります。
  • しかし Rails では:
    • コントローラ側の Token 認証 (ActionController::HttpAuthentication::Token)
    • ActionDispatch::Request#bearer_token
      がいずれも 大文字小文字を区別する 実装になっており、
      • bearer abc123 → 無効として扱われ 401 を返す
      • request.bearer_tokenBearer 以外の表記では nil を返す
        といった問題がありました。
  • 一方で、同じファイルにある Basic 認証はすでに case-insensitive で比較しており、振る舞いが不統一でした。

本 PR はこの不整合を解消し、RFC 準拠かつ Basic と同様に、Token / Bearer も case-insensitive に扱うようにしています。


2-2. コントローラ側 (Token 認証) の変更

対象:

  • actionpack/lib/action_controller/metal/http_authentication.rb

ActionController::HttpAuthentication::Token 内の TOKEN_REGEX が修正されています。
元から存在する TOKEN_REGEXAuthorization ヘッダをパースして Token スキームかどうかを判定するための正規表現ですが、スキーム部分を 大文字小文字を区別せずにマッチ するように変わりました。

イメージとしては、以下のような変更です(実際のコードからの擬似例):

ruby
# Before(概念的なイメージ)
TOKEN_REGEX = /^Token\s+(.+)$/  # "Token" と完全一致でマッチ

# After(概念的なイメージ)
TOKEN_REGEX = /^Token\s+(.+)$/i # "Token" を大文字小文字を区別せずにマッチ

これにより、以下のようなヘッダがすべて同様にトークンとして認識されます:

http
Authorization: Token abc123
Authorization: token abc123
Authorization: TOKEN abc123
Authorization: ToKeN abc123

コントローラ側で authenticate_or_request_with_http_token などを使っている場合、これら全てが有効なトークンとして扱われます。


2-3. リクエスト側 (Request#bearer_token) の変更

対象:

  • actionpack/lib/action_dispatch/http/request.rb

ActionDispatch::Request#bearer_token は、Authorization ヘッダから Bearer トークンを取り出すヘルパーメソッドです。

元は Bearer というスキーム名を 大文字小文字を区別して マッチさせていたため、以下のようなケースは全て nil を返していました:

http
Authorization: bearer abc123
Authorization: BEARER abc123
Authorization: Bearer abc123
# → Bearer 以外はトークンが取れない

PR ではここも case-insensitive に変更され、Basic と同様の挙動になります。
擬似的なイメージ:

ruby
# Before(概念的なイメージ)
authorization&.match(/^Bearer\s+(.+)$/)

# After(概念的なイメージ)
authorization&.match(/^Bearer\s+(.+)$/i)

これにより、以下のようなコードがすべて期待通りに動作します:

ruby
# 例: Rack ミドルウェアやコントローラ内
token = request.bearer_token
# Authorization: bearer abc123
# Authorization: BEARER abc123
# Authorization: Bearer abc123
# いずれも token == "abc123" になる

2-4. テストの追加

対象:

  • actionpack/test/controller/http_token_authentication_test.rb (+9)
  • actionpack/test/dispatch/request_test.rb (+5)

上記の動作が保証されるよう、以下の観点のテストが追加されています:

  • Token 認証について、TOKEN, token, ToKeN などさまざまな表記の Authorization ヘッダを送っても正しく認証されること。
  • Request#bearer_token について、bearer, BEARER など任意の大文字小文字の組み合わせでもトークンを抽出できること。

これにより、今後のリファクタリング等で再び case-sensitive に戻ってしまうリグレッションを防止します。


  1. 影響範囲・注意点

3-1. 影響範囲

  • HTTP トークン認証 (ActionController::HttpAuthentication::Token) を利用しているアプリケーション
  • ActionDispatch::Request#bearer_token を直接、またはライブラリを通じて利用しているアプリケーション
  • 上記を利用しているライブラリ・ミドルウェア(OAuth2 実装、API 認証機構など)

基本的には後方互換な改善 であり、従来通り Token xxx / Bearer xxx もそのまま動作します。そのうえで、今まで 401 になっていた bearer xxx / TOKEN xxx などが通るようになります。

3-2. 注意点・想定外の影響になり得るケース

  • もしアプリケーション側で「Bearer という表記だけを許したい」「token は拒否したい」といった、あえて非 RFC 準拠な制約 を設けていた場合、その前提は崩れます。
    ただし、auth-scheme を case-sensitive に扱うこと自体が RFC 9110 に反するため、そのような前提がある場合は設計の見直しが推奨されます。
  • 同一クライアント・同一トークンで、これまでたまたま Bearer になっていたケースでは何も変わらず、混在環境(クライアントの一部が bearer を送るなど)でのみ挙動が変わる可能性があります。

  1. 参考情報 (あれば)
  • RFC 9110 §11.1: Authentication Scheme
    • auth-scheme は case-insensitive と定義されている。
  • 関連する Rails コード:
    • ActionController::HttpAuthentication::Token
      actionpack/lib/action_controller/metal/http_authentication.rb
    • ActionDispatch::Request#bearer_token
      actionpack/lib/action_dispatch/http/request.rb
  • 類似の既存挙動:
    • Basic 認証はすでにスキーム名を case-insensitive に扱っており、本 PR によって Token / Bearer もそれに揃えられた形になります。

#57993 Fix incorrect number_to_percentage default precision in docs [ci skip]

マージ日: 2026/7/4 | 作成者: @anxkhn

  1. 概要 (1-2文で)
    number_to_percentage ヘルパーのドキュメントに書かれていた :precision オプションのデフォルト値が誤っており、実際の挙動(デフォルト3桁)に合わせて「2」から「3」に修正した PR です。コードや挙動の変更はなく、ドキュメントのみの修正です。

  1. 変更内容の詳細

何が問題だったか

number_to_percentage の RDoc には、:precision オプションについて次のように書かれていました。

ruby
# [+:precision+]
#   The level of precision, or +nil+ to preserve +number+'s precision.
#   Defaults to 2.
#
#   number_to_percentage(12.3456789, precision: 4) # => "12.3457%"

しかし実際には、number_to_percentage(100) のデフォルト出力は "100.000%" であり、デフォルト精度は「3」です。

  • ベースのフォーマット設定: DEFAULTS[:format][:precision] = 3
  • :currency だけが precision を 2 にオーバーライド
  • :percentage:delimiter:format しかオーバーライドしていないため、precision=3 を継承

そのため、「Defaults to 2.」という説明が実際の挙動および直前のサンプル (number_to_percentage(100) # => "100.000%") やテストと矛盾していました。

具体的な修正内容

activesupport/lib/active_support/number_helper.rb の1行だけが変更されています。

diff
 # [+:precision+]
 #   The level of precision, or +nil+ to preserve +number+'s precision.
-#   Defaults to 2.
+#   Defaults to 3.
 #
 #   number_to_percentage(12.3456789, precision: 4) # => "12.3457%"

number_to_currency 側の「Defaults to 2.」は正しいためそのままです。


  1. 影響範囲・注意点
  • 実行時の挙動は一切変わりません
    もともと number_to_percentage のデフォルト precision は 3 であり、この PR はそれを説明するドキュメントを正しくしただけです。

  • 影響があるのは「ドキュメントを読んで実装していた人の認識」だけです。

    • これまで「precision のデフォルトは 2 だ」と思い込んでいた場合、実際の出力とのギャップに気づいていなかった可能性があります。
    • ただしコード側は以前から 3 桁出力で動いており、PR による挙動変更はありません。
  • もしアプリ側で「パーセンテージは必ず小数2桁にしたい」という要件がある場合は、明示的に precision を指定する必要があります。

    ruby
    number_to_percentage(12.3456)               # => "12.346%" (デフォルト precision: 3)
    number_to_percentage(12.3456, precision: 2) # => "12.35%"
  • 他のヘルパーとの一貫性:

    • number_to_rounded, number_to_human_size, number_to_human など、同じくベースの precision を継承するヘルパーはすでに「Defaults to 3.」と記載されており、number_to_percentage だけがドキュメント上ズレていました。本 PR により説明の一貫性が取れています。

  1. 参考情報 (あれば)

デフォルト precision が 3 であることを示すソース:

  • activesupport/lib/active_support/number_helper/number_converter.rb

    • DEFAULTS[:format][:precision] = 3
    • DEFAULTS[:currency][:format][:precision] = 2
    • DEFAULTS[:percentage]:delimiter:format だけ上書きし、precision は継承
  • activesupport/lib/active_support/locale/en.yml

    • number.format.precision: 3
    • number.currency.format.precision: 2
    • number.percentage.format は precision 未指定 → 3 を継承
  • テストコード

    • activesupport/test/number_helper_test.rb
    • activesupport/test/core_ext/numeric_ext_test.rb
      いずれも number_to_percentage(100) == "100.000%" を前提にしており、precision=3 を前提にしていることが分かります。

#57986 Fix Guides Index flyout menu overflowing its panel

マージ日: 2026/7/4 | 作成者: @iuhoay

  1. 概要 (1-2文で)
    Rails Guides のデスクトップ向け「Guides Index」フライアウトメニューで、3カラムレイアウトが横幅を超えて右にはみ出していた問題を、CSS のレイアウト指定を見直すことで解消した PR です。widthmargin の組み合わせで 100% を超えていたものを、column-gapcalc() を用いてちょうど 100% に収まるようにしています。

  1. 変更内容の詳細

対象: guides/assets/stylesrc/_main.scss

問題点:

  • .guides-section-container 内の .guides-section が以下のように指定されていました:
    • width: 33%;
    • margin: 0 2em 0.5em 0;
  • 3カラム × 33% = 99% に加え、各カラム右側に 2em マージンがあり、その分が合計で 100% を超えてしまい、右端のカラムがコンテナの外にはみ出していました。
  • はみ出した結果、右端カラム内の長いリンクテキスト(例: "Installing Rails Core Development Dependencies")がパネル外へ溢れて見えてしまう状態になっていました。

修正内容:

diff
.guides-section-container {
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
+ column-gap: 2em;
  max-height: 60em;
  width: 100%;

  .guides-section {
    flex: auto;
-   margin: 0 2em 0.5em 0;
+   margin: 0 0 0.5em 0;
    text-align: left;
-   width: 33%;
+   width: calc((100% - 4em) / 3);
  }
}

ポイント:

  • カラム間のスペースの扱いを、各要素の margin-right ではなく、コンテナ側の column-gap に移行:
    • 以前: .guides-sectionmargin-right: 2em; 相当の指定
    • 以後: .guides-section-containercolumn-gap: 2em;
  • .guides-section の幅を固定の 33% から、カラム間ギャップを考慮した calc((100% - 4em) / 3) へ変更:
    • 3カラムとその間に 2つのギャップ(2em × 2 = 4em)がある前提で、
      • コンテナの 100% 幅からギャップ合計 4em を引いた残りの幅を 3 等分。
    • これにより「カラムの幅 × 3 + ギャップ × 2 = 100%」となり、パネル幅を超えなくなります。
  • .guides-sectionmargin は下方向 (margin-bottom: 0.5em) のみ残し、左右の余白は 0 に変更。

結果として、3カラムとその間の余白がコンテナ幅にぴったり収まり、右端カラムや長いリンクテキストがパネル外へ溢れなくなります。


  1. 影響範囲・注意点
  • 影響範囲:
    • Rails Guides サイトのデスクトップビューにおける「Guides Index」フライアウトメニューのレイアウトに限定されます。
    • それ以外のページレイアウトや、Rails 本体の機能・挙動には影響しません(スタイルシートのみの変更)。
  • 技術的な注意点:
    • column-gap は比較的新しめの CSS プロパティですが、デスクトップ主要ブラウザでは概ねサポートされています。Rails Guides のターゲットブラウザを考えると問題になる可能性は低いと考えられます。
    • calc((100% - 4em) / 3) によって、コンテナ幅とフォントサイズ(em に依存)が変わるとカラム幅も自動的に再計算されるため、レイアウトはある程度柔軟に対応できますが、「3カラム+ギャップ 2つ」であることが前提になっています。将来的にカラム数を変える場合は、この式も合わせてメンテナンスする必要があります。
    • flex-direction: column; flex-wrap: wrap; という少し特殊な使い方をしているため、DOM 構造の変更やコンテナ高さの変更(max-height: 60em;)とあわせて動作を確認するのが望ましいです。

  1. 参考情報 (あれば)

#57997 Use Range#cover? for this_week?/this_month?/this_year?

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    Date/Time関連のヘルパー this_week? / this_month? / this_year? が、内部判定に Range#include? ではなく Range#cover? を使うように変更され、性能が大きく改善されました。振る舞いは変えずに、呼び出しごとの無駄な日付走査を避けるリファクタリングです。

  1. 変更内容の詳細

何をしているか

this_week?, this_month?, this_year? は、引数の日時オブジェクトが「今週 / 今月 / 今年」に属するかどうかを判定するメソッドです。
もともとはだいたい次のようなイメージで実装されていました:

ruby
def this_week?
  all_week.include?(self)
end

def this_month?
  all_month.include?(self)
end

def this_year?
  all_year.include?(self)
end

ここで all_week, all_month, all_yearDate の Range を返します。
Range#include?#succ で一つずつ値を進めながら探索する実装になるため、Date の場合:

  • this_week? → 約 7 回 succ(7日分)
  • this_month? → 最大 31 回 succ
  • this_year? → 約 365 回 succ

といった線形探索が毎回発生していました。

今回の変更では、これらを Range#cover? に差し替えています:

ruby
def this_week?
  all_week.cover?(self)
end

def this_month?
  all_month.cover?(self)
end

def this_year?
  all_year.cover?(self)
end

Range#cover? は「範囲の開始と終了の比較」だけで判定するため、内部で要素を1つずつたどることがなく、10〜100倍程度高速になることが PR 説明で言及されています。

背景

  • 先行 PR #57963 で this_quarter? はすでに cover? に変えられており、今回の PR はそのフォローアップとして、兄弟関数 (this_week? / this_month? / this_year?) を同じ実装方針に揃えたものです。
  • テストの変更はなく、既存の境界条件テストもそのまま通ることが確認されています。

  1. 影響範囲・注意点
  • 挙動面

    • Range#include?Range#cover? は、Date/Time のように比較が定義されたオブジェクトに対して、典型的な連続範囲を扱う場合は同じ結果を返します。
    • 既存テストが境界値(開始日・終了日を含むかどうか等)をカバーしており、そのままパスしているため、APIの論理的挙動は変わらないと見なして問題ありません。
  • パフォーマンス面

    • this_year? のように長い期間の Range を頻繁に判定しているコードでは、顕著な性能改善が見込めます。
    • 特に大量レコードに対して this_*? を呼ぶ場面(バリデーション、集計、バッチ処理など)で恩恵が大きくなります。
  • 後方互換性

    • 公開 API のメソッドシグネチャや返り値の型に変更はなく、内部実装の最適化のみです。
    • Range#cover? / Range#include? の差異に依存した特殊なケース(例えば、離散的でない独自オブジェクトを Range に入れている等)をこのメソッド群が利用しているわけではないので、互換性上のリスクは非常に低いと考えられます。

  1. 参考情報 (あれば)

#57963 Add this_quarter? to Date/Time

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    Date, Time, DateTime, ActiveSupport::TimeWithZone に対して「今期(今四半期)かどうか」を判定する this_quarter? メソッドが追加されました。これにより、week/month/year に揃う形で quarter にも this_*? プレディケートが用意され、日付処理の一貫性が向上しています。

  1. 変更内容の詳細

追加された機能

対象クラス:

  • Date
  • Time
  • DateTime
  • ActiveSupport::TimeWithZone

に、以下のプレディケートメソッドが追加されました:

ruby
date_or_time.this_quarter? # => true / false

判定ロジックの概要
this_month?this_year? と同様に、「現在の四半期」を基準にして判定します。

  • 基準となる“今”は Date.current(= Time.zone を考慮した現在日付)
  • その Date.current が属する四半期を「現在の四半期」とみなし、
  • 受け取った日付/時刻オブジェクトがその四半期の範囲内にあるかどうかを判定

概念的には以下と同等の判定です:

ruby
def this_quarter?
  Date.current.all_quarter.cover?(to_date)
end

※実装は activesupport/lib/active_support/core_ext/date_and_time/calculations.rb に追加されており、既存の this_month? / this_year? と同じスタイルで this_quarter? が定義されています。

使用例

ruby
# 前提: Date.current が 2000-02-15 (Q1: 1〜3月) の場合

Date.new(2000, 3, 31).this_quarter?   # => true  (同じ四半期 Q1)
Date.new(2000, 4, 1).this_quarter?    # => false (次の四半期 Q2)

Time.utc(2000, 1, 1).this_quarter?    # => true  (Q1 に含まれる)
DateTime.civil(2000, 4, 1).this_quarter? # => false (Q2)

既存機能との関係

Rails には既に以下のような四半期関連のメソッドがありました:

  • beginning_of_quarter
  • end_of_quarter
  • all_quarter
  • next_quarter
  • prev_quarter

一方で、週・月・年にはある this_*? 系が四半期にだけ存在していませんでした:

weekmonthquarteryear
beginning_of_*
end_of_*
all_*
next_*
prev_*
this_*?

今回の PR で、この this_quarter? により埋まり、API の一貫性が取れるようになりました。

テストとドキュメント

変更ファイル:

  • activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
    • this_quarter? 実装追加 (+5行)
  • activesupport/test/core_ext/date_time_ext_test.rb
    • test_this_quarter 追加 (+9行)
    • this_month? / this_year? と同じテストパターンで、四半期境界をカバー
  • activesupport/CHANGELOG.md
    • 新機能として this_quarter? 追加を明記 (+12行)

テストでは、四半期の開始日・終了日・直前直後の日付などを用い、境界条件を確認しています。


  1. 影響範囲・注意点

影響範囲

  • ActiveSupport の Date/Time 拡張を利用しているアプリケーション全般で、新たに this_quarter? が利用可能になります。
  • 既存メソッドの挙動変更や破壊的変更は含まれていません(追加のみ)。

実務上の利点

これまでよく見られたコード:

ruby
Date.current.all_quarter.cover?(record.created_at.to_date)
# または
Date.current.all_quarter.include?(record.created_at.to_date)

を、より簡潔に:

ruby
record.created_at.this_quarter?

と書けるようになります。
四半期ベースの:

  • 請求期間・課金期間の判定
  • OKR・目標管理の「今期」判定
  • 財務・レポート系でのフィルタリング

などが読みやすく実装できます。

Time.zone / タイムゾーンへの注意

this_quarter?Date.current を基準に判定するため、config.time_zoneTime.zone の設定に依存します。this_month? / this_year? と同じ挙動です。

例:

  • アプリの Time.zone"Tokyo" の場合: 「今期」は東京時間での日付を基準とする
  • DB が UTC でも、TimeWithZone で評価される場合は Time.zone を考慮

タイムゾーンを切り替えるテストや、マルチタイムゾーン対応アプリでは、Time.use_zone を用いた検証を行うと安全です。


  1. 参考情報 (あれば)

#57987 Make add_index(if_not_exists: true) reversible

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    add_index / remove_index のマイグレーションを逆変換(rollback 時の自動生成)する際に、if_not_exists / if_exists オプションが正しく対応付けられるようになりました。これにより、既存/非存在なインデックスに対して安全にロールバックでき、これまで発生していた不要な例外が解消されます。

  1. 変更内容の詳細

何が問題だったか

Rails の Migration には「コマンドインバータ(ActiveRecord::Migration::CommandRecorder)」があり、change メソッド内で書いた操作から rollback 用の逆操作を自動的に生成します。

しかし、これまで以下のような問題がありました:

  • remove_index(..., if_exists: true) を反転すると add_index が生成されるが、
    その際に if_exists: true失われる(:if_exists は add 側には存在しないオプションのため、単に削除されるだけだった)。

    • 結果: rollback 時に、すでにインデックスが存在するケースで例外が発生する。
  • add_index(..., if_not_exists: true) を反転すると remove_index が生成されるが、
    if_not_exists: true がそのまま remove_index に渡されてしまい、remove_indexif_not_exists理解しない ため、事実上オプションが無視される。

    • 結果: rollback 時に、インデックスが存在しないケースで例外が発生する。

つまり、「forward 方向では存在チェック付きで安全に動くように書いたマイグレーション」が、rollback 時にはその安全性が失われていた、という状態です。

今回の修正内容

CommandRecorderinvert_add_index / invert_remove_index において、if_not_exists / if_exists相互に変換するようになりました。

概念的には以下のような変換が行われます:

ruby
# 変更前(概念図)
invert_remove_index(columns, if_exists: true)
# => [:add_index, [columns], {}]  # :if_exists が落ちる

invert_add_index(columns, if_not_exists: true)
# => [:remove_index, [columns], { if_not_exists: true }]  # remove_index では無視される


# 変更後(概念図)
invert_remove_index(columns, if_exists: true)
# => [:add_index, [columns], { if_not_exists: true }]

invert_add_index(columns, if_not_exists: true)
# => [:remove_index, [columns], { if_exists: true }]

これにより:

  • 「存在していたら削除」を rollback すると「存在していなかったら作成しない(存在しなくてもエラーにしない)」という安全な対になる関係になる
  • 「なければ作らない」を rollback すると「すでになくてもエラーにしない削除」になる

サンプルコードイメージ

ruby
class AddIndexToUsersEmail < ActiveRecord::Migration[7.2]
  def change
    # インデックスがなければ作成
    add_index :users, :email, if_not_exists: true
  end
end

この change を rollback すると、従来は概ねこう解釈されていました:

ruby
remove_index :users, :email
# if_not_exists は無視されるので、インデックスがなければエラー

今回の修正後は、内部的には以下のような remove_index が生成されます:

ruby
remove_index :users, :email, if_exists: true
# インデックスがなくてもエラーにならない

逆に:

ruby
class RemoveIndexFromUsersEmail < ActiveRecord::Migration[7.2]
  def change
    remove_index :users, :email, if_exists: true
  end
end

を rollback すると、今までは(概念的に):

ruby
add_index :users, :email
# すでにインデックスがあるとエラー

となっていましたが、これからは:

ruby
add_index :users, :email, if_not_exists: true
# すでにインデックスがあってもエラーにならない

となります。

実装面の変更

  • activerecord/lib/active_record/migration/command_recorder.rb
    • add_index / remove_index の invert ロジックに、オプションハッシュの変換処理を追加
    • :if_not_exists:if_exists:if_exists:if_not_exists のマッピングを行うようにした
  • activerecord/test/cases/migration/command_recorder_test.rb
    • 上記の変換が正しく行われているかどうかを検証するテストを追加

この挙動は前にマージされた invert_add_foreign_key / invert_remove_foreign_key(PR #57972)や check_constraint 周りの invert 処理と揃えた形です。


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

    • change メソッド内で add_index ... if_not_exists: true または remove_index ... if_exists: true を利用しているマイグレーションの rollback 挙動が、より安全かつ期待に沿ったものになります。
    • 既存のマイグレーションファイルに対しても、Rails 本体を上げれば新しい CommandRecorder のロジックで rollback が行われます。
  • 期待されるメリット

    • すでに適用済みのマイグレーションを本番・ステージングで rollback する際に、「インデックスがもうある / もうない」状況でも例外が出づらくなります。
    • 並列デプロイや手動メンテなどでインデックス状態がずれがちな環境でも、マイグレーションの安全性が増します。
  • 注意点

    • 動作が「壊れる」方向ではなく、「これまで例外が出ていたケースで出なくなる」変更なので、互換性上の問題はほぼありません。
    • ただし、あえて rollback 時にインデックスの存在 / 非存在を厳密に検出したい、という特殊なケースでは挙動が変わる(例外が起きにくくなる)点は認識しておくとよいです。
    • Schema の整合性チェック等で、インデックスの存在を前提にした独自ツールを組んでいる場合は、「if_not_exists / if_exists 付きのマイグレーションでも rollback が失敗しにくくなった」ことを踏まえて運用を見直す余地があります。

  1. 参考情報 (あれば)
  • この PR:
    • Make add_index(if_not_exists: true) reversible (#57987)
  • 関連 PR:
    • foreign key inverter を同様に修正した PR: #57972
  • 該当クラス/メソッド:
    • ActiveRecord::Migration::CommandRecorder#invert_add_index
    • ActiveRecord::Migration::CommandRecorder#invert_remove_index

#57992 Require fileutils in Cache::FileStore

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveSupport::Cache::FileStore 内で FileUtils を利用しているにもかかわらず "fileutils" を require していなかった問題を修正し、スタンドアロンな ActiveSupport 利用時に発生していた NameError を解消する PR です。Rails フルスタックでは暗黙に読み込まれていた依存を、明示的に宣言するようになりました。

  1. 変更内容の詳細

変更は 1 行のみで、ActiveSupport::Cache::FileStore のファイル先頭付近に require "fileutils" が追加されました。

イメージとしては以下のような変更です(擬似コード):

ruby
# activesupport/lib/active_support/cache/file_store.rb

# 旧:
# module ActiveSupport
#   module Cache
#     class FileStore < Store
#       ...
#       FileUtils.makedirs(...)
#       FileUtils.rm_r(...)
#       ...

# 新: fileutils を明示的に読み込む
require "fileutils"

module ActiveSupport
  module Cache
    class FileStore < Store
      # 内部で FileUtils.makedirs, FileUtils.rm_r を使用
    end
  end
end

背景として、このクラスはキャッシュディレクトリの作成や削除に FileUtils.makedirsFileUtils.rm_r を使っていますが、これまでは同じプロセス内のどこか別の場所で "fileutils" が require されている前提になっていました。Rails 全体をロードしている場合は、他のコンポーネントが fileutils を読み込むため問題は表面化していませんでしたが、ActiveSupport 単体で ActiveSupport::Cache::FileStore を使うと次のようなエラーが起きていました。

ruby
require "active_support"
require "active_support/cache"

store = ActiveSupport::Cache::FileStore.new("/tmp/cache")
store.write("foo", "bar")
# => NameError: uninitialized constant ActiveSupport::Cache::FileStore::FileUtils

この PR により、file_store.rb 自身が require "fileutils" するため、上記のような NameError は発生しなくなります。


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

    • ActiveSupport::Cache::FileStore を利用する全てのコードに対し、挙動は「例外が出なくなる」以外は実質的な変更はありません。
    • 既に Rails フルスタックを利用しているアプリケーションでは、もともとどこかで fileutils がロードされていたため、挙動上の違いはほぼありません。
    • ActiveSupport を単体で利用しているツール・スクリプト・ライブラリにおいて、FileStore を使った際の NameError (uninitialized constant FileUtils) が解消されます。
  • 注意点

    • require "fileutils" は Ruby 標準ライブラリの読み込みであり、互換性上の問題を引き起こす可能性は低いです。
    • もしアプリ側で FileUtils 定数を独自にモンキーパッチしていた場合でも、この PR は単に標準ライブラリを require するだけなので、通常は影響ありません(既にロード済みなら再 require も安全)。
    • 「他のファイルに依存して暗黙に定数をロードする」状態が解消され、今後は file_store.rb 単体で完結した依存関係になります。

  1. 参考情報 (あれば)
  • 対象クラスのドキュメント(キャッシュストアの概要など)
    • ActiveSupport::Cache::FileStore (現行 Rails ガイド / RDoc)
  • 類似のパターンとして、標準ライブラリに依存しているクラス・モジュールは、そのファイル自身で require するのが推奨されます。今回の修正は、そのベストプラクティスに沿ったものといえます。

#57984 Quote the index name in MySQL enable_index/disable_index

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    MySQL / MariaDB 用アダプタの enable_index / disable_index が、インデックス名をクォートせずに SQL を生成していた問題を修正し、予約語や特殊文字を含むインデックス名でも正常に動作するようにしました。rename_index など他のインデックス操作と一貫して、インデックス名に quote_column_name を適用するようになっています。

  1. 変更内容の詳細

何が問題だったか

enable_index / disable_index は、内部的に MySQL / MariaDB の以下のような SQL を発行します:

sql
ALTER TABLE `testings` ALTER INDEX order VISIBLE
ALTER TABLE `testings` ALTER INDEX order INVISIBLE

ここで order のように、

  • SQL の予約語
  • バッククォートすべき特殊文字を含む名前

をインデックス名に使っていると、インデックス名がクォートされていないため 構文エラー になります。

どのように修正されたか

abstract_mysql_adapter.rbenable_index / disable_index 実装で、インデックス名を quote_column_name で包むように変更されました。

概念的には以下のような変更です:

ruby
# 変更前 (イメージ)
def enable_index(table_name, index_name)
  execute("ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{index_name} VISIBLE")
end

# 変更後 (イメージ)
def enable_index(table_name, index_name)
  execute("ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{quote_column_name(index_name)} VISIBLE")
end

def disable_index(table_name, index_name)
  execute("ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{quote_column_name(index_name)} INVISIBLE")
end

その結果、実際に発行される SQL は:

sql
-- Before
ALTER TABLE `testings` ALTER INDEX order VISIBLE

-- After
ALTER TABLE `testings` ALTER INDEX `order` VISIBLE

となり、インデックス名がバッククォートで囲まれます。

quote_column_name は、MySQL アダプタではカラム名・インデックス名など識別子を `name` のようにクォートするためのメソッドで、既に rename_index など他のインデックス操作では使われていました。今回の修正で、インデックス関連操作のクォート戦略が統一されました。

テストの追加

activerecord/test/cases/migration/index_test.rb にテストが追加され、

  • 予約語や特殊文字を含むインデックス名に対して
  • enable_index / disable_index がエラーなく動作し、正しい SQL が生成される

ことが検証されています。

(具体的なテストコードは PR 情報にはありませんが、order のようなケースをカバーしていると考えられます。)


  1. 影響範囲・注意点
  • 対象:
    • Active Record を使った MySQL / MariaDB アプリケーション
    • enable_index / disable_index を直接またはマイグレーション経由で利用している場合
  • 影響:
    • これまで「インデックス名が予約語等だと enable_index / disable_index で落ちる」ケースが解消されます。
    • 既存の正常なケースについては、単にインデックス名がクォートされるだけなので、後方互換性への影響はほぼありません(MySQL / MariaDB は識別子をクォートしても同じオブジェクトを指すため)。
  • 注意点:
    • もしアプリ側で生 SQL を組み立てて ALTER TABLE ... ALTER INDEX ... を発行している場合は、この修正は関係ありません。その場合も同様に、インデックス名はバッククォートでクォートするのが安全です。
    • マイグレーション上で index: { name: "order" } のような名前を指定しても、今回の修正により enable_index / disable_index 時のエラーは防げるようになりますが、アプリケーションコードや他の SQL からの参照でも予約語を使う場合は同様の注意が必要です。

  1. 参考情報 (あれば)
  • MySQL マニュアル:
    • 「Schema Object Names」(識別子とクォート)
    • 「Reserved Words」(予約語一覧)
  • Rails ソース周辺:
    • ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#quote_column_name
    • rename_index 実装 (同アダプタ内、インデックス名クォートの既存の扱いの参考になる)

#57988 Raise on ignored if_exists option in change_table's remove_timestamps

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    change_table ブロック内で t.remove_timestampsif_exists: / if_not_exists: オプションを渡した場合、これまでは黙って無視されていましたが、他のメソッドと同様に ArgumentError を投げて呼び出し側に誤用を知らせるように変更されました。これにより、サポートされないオプションの利用を早期に検知できるようになります。

  1. 変更内容の詳細

何が変わったか

  • 対象メソッド: ActiveRecord::ConnectionAdapters::Table#remove_timestamps
  • 変更点:
    • remove_timestamps が内部で raise_on_if_exist_options を呼ぶようになりました。
    • これにより、if_exists: / if_not_exists: オプションが渡された場合は例外を発生させます。

従来の挙動:

ruby
change_table :users do |t|
  t.remove_timestamps if_exists: true  # ← オプションは黙って無視される
end
# 例外は発生しないが、if_exists: は一切効いていない

今回の変更後:

ruby
change_table :users do |t|
  t.remove_timestamps if_exists: true
end
# => ArgumentError: options :if_exists, :if_not_exists are not supported ...

これは、既に以下のようなメソッドで行われている挙動と揃えたものです。

  • t.timestamps(if_exists: true)ArgumentError
  • t.remove(:column, if_exists: true)ArgumentError
  • t.remove_index(:column, if_exists: true)ArgumentError

つまり、change_table ブロック内の「if_exists / if_not_exists をサポートしないヘルパー」は、全て一貫して例外を投げるようになりました。

テスト追加

activerecord/test/cases/migration/change_table_test.rb にテストが追加され、例えば以下のようなケースがカバーされていると考えられます。

  • t.remove_timestamps(if_exists: true)ArgumentError を投げること
  • t.remove_timestamps(if_not_exists: true)ArgumentError を投げること

(実際のテストコードは数行で、この挙動を保証する内容になっています。)


  1. 影響範囲・注意点

影響を受けるケース

  • 既存のマイグレーションで、change_table ブロック内に以下のようなコードがある場合:
ruby
change_table :users do |t|
  t.remove_timestamps if_exists: true
  # または
  t.remove_timestamps if_not_exists: true
end

これらは これまでは正常終了していたが、オプションは効いていなかった のに対し、今後は ArgumentError によってマイグレーションが失敗します。

なぜ例外にするのか

  • サポートされていないオプションを黙って無視すると、開発者が「存在しない場合はスキップされるはず」と誤解したままになり、スキーマ不整合や本番環境でのバグの原因になります。
  • 他の change_table ヘルパー (timestamps, remove, remove_index など) はすでに同じポリシーで挙動しており、remove_timestamps だけ例外だったため、API 一貫性の観点からも修正されています。

対応方法

  • もし上記のようなコードが既に存在する場合は:

    1. if_exists: / if_not_exists: を削除する (最も単純):

      ruby
      change_table :users do |t|
        t.remove_timestamps
      end
    2. 「存在チェック込みで安全に消したい」という意図であれば、自前で条件分岐する:

      ruby
      def up
        if column_exists?(:users, :created_at) && column_exists?(:users, :updated_at)
          change_table :users do |t|
            t.remove_timestamps
          end
        end
      end
      
      def down
        change_table :users do |t|
          t.timestamps
        end
      end
  • なお、change_table ではなく remove_timestamps を直接呼び出す remove_timestamps(:users) のような DSL を使っている場合、この PR は change_table ブロック内の t.remove_timestamps に対する変更なので、その部分には直接は影響しません (この PR の差分範囲に依存しますが、説明文からは change_table ブロック内のヘルパーが対象と読み取れます)。


  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57988
  • 関連する API:
    • change_table ブロック内ヘルパー:
      • t.timestamps
      • t.remove
      • t.remove_index
      • t.remove_timestamps
    • オプション検証メソッド: raise_on_if_exist_options (ActiveRecord 内部ユーティリティ)

この変更は「サイレントに失敗するマイグレーション」を減らすための一貫性改善であり、バグ修正に近い性質の変更と考えられます。


#57989 Fix color_field emitting invalid color values for strings containing a hex substring

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    color_field ヘルパーの色値バリデーションが不正確だったため、<input type="color"> にブラウザが解釈できない値がそのまま出力されていた問題を修正しています。正規表現を「文字列全体が #rrggbb 形式かどうか」を見るように変更し、それ以外は #000000 にフォールバックする挙動を統一しました。

  1. 変更内容の詳細

何が問題だったか

従来の color_field 内の validate_color_string は、値の中に「6桁の16進数」がどこかに含まれていれば OK と判定していました。

そのため、例えば以下のような値が「有効」とみなされていました。

  • "Not a color #123456 at all"
  • "#12345678"(8桁、実際には CSS の #rrggbbaa のような形式)
  • その他、文字列の一部に #abcdef のようなパターンが含まれるもの

これらはそのまま <input type="color">value 属性に出力されるため、ブラウザ側では無効なカラー値として扱われ、期待どおり動作しない(色が選択されていない状態になるなど)問題がありました。

具体的な修正内容

validate_color_string の正規表現を、

  • 「6桁の 16 進数の連続部分をどこかから拾ってくる」
    から
  • 「文字列全体が #rrggbb 形式と完全一致しているかを確認する」

ように変更しています。

Ruby 的には、例えばこんなイメージです(実際のコード断片のイメージ):

ruby
def validate_color_string(value)
  # 変更前(イメージ)
  # value[/#[0-9a-fA-F]{6}/] || "#000000"

  # 変更後(イメージ)
  value =~ /\A#[0-9a-fA-F]{6}\z/ ? value : "#000000"
end

/\A...\z/ でアンカーしているため、以下のような挙動になります:

  • "Not a color #123456 at all" → 不一致 → "#000000" にフォールバック
  • "#12345678" → 不一致 → "#000000" にフォールバック
  • "#1234TR" → 不一致 → "#000000"(従来からのフォールバック挙動を踏襲)
  • "#123456" → 一致 → そのまま "#123456" を利用

テストの追加

actionview/test/template/form_helper_test.rb に 6 行ほどテストが追加され、上記のようなケースで:

  • 部分一致する文字列や
  • 8桁のカラーや
  • その他の malformed な値

が渡されたときに、value="#000000" が出力されることが確認されています。


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

    • ActionView::Helpers::FormTagHelper#color_field_tag
    • ActionView::Helpers::FormHelper#color_field
      など、Rails 標準の color_field / color_field_tag を通じて <input type="color"> を生成している部分すべてに影響します。
  • 挙動の変化

    • これまでは「文字列の中に有効な #rrggbb パターンが一部でも含まれていれば、それをそのまま value に出力する」挙動でした。
    • 今後は「文字列全体が #rrggbb 形式である場合だけ受け入れ、それ以外は必ず #000000 にフォールバック」になります。
    • ブラウザ上の見た目として、今まで一見「謎の初期値」になっていたケースが、明示的に黒 (#000000) で表示されるようになります。
  • 後方互換性の観点

    • もしアプリケーション側で、意図的に "Some text #ff0000" のような値を color_field に渡し、ブラウザが無効値として扱うことに依存していた場合、その挙動は変わります(常に #000000 になります)。
    • 一般的には、HTML の仕様に沿ったより厳格なバリデーションとなるため、バグ修正としての互換性の破壊は最小です。
    • #rrggbbaa(8桁)のような色表現を将来的に扱いたい場合は、color_field ではなくカスタムの input / JS を利用する必要があります(HTML <input type="color"> 自体が現状 #rrggbb しか受けないため)。
  • 注意点

    • サーバー側から color_field に渡す値は、# + 16進6桁の文字列(/\A#[0-9a-fA-F]{6}\z/)に正規化してから渡すようにしておくと、今回の変更に依存せず安全です。
    • 不正な値をそのまま投げても自動で #000000 になるため、UI 的に「入力値がおかしい」ことがユーザーに分からない可能性があります。必要に応じてモデルバリデーションやクライアント側バリデーションで補完してください。

  1. 参考情報 (あれば)
  • HTML 仕様上、<input type="color">value#rrggbb 形式(# + 16 進 6 桁)以外は無効値として扱われます。
  • Rails 側でのフォールバックポリシー:
    • 有効な #rrggbb 形式 → そのまま出力
    • それ以外 → #000000(黒)に統一
      という仕様が、この修正でより一貫した形になりました。

#57985 Update PostgreSQL database.yml sample in Command Line guide [ci skip]

マージ日: 2026/7/4 | 作成者: @fuentesjr

  1. 概要 (1-2文で)
    Command Line ガイド内の PostgreSQL 用 database.yml サンプルを、現行の rails new ... --database=postgresql が生成する内容に合わせて更新した PRです。合わせて、非推奨になりつつある pool キーの例を、新しい max_connections キーの例に置き換えています。

  1. 変更内容の詳細

対象ファイル: guides/source/command_line.md の「Configure a Different Database」セクション内の PostgreSQL サンプル config/database.yml

主な変更は2点です。

(1) pg_config のパスの更新

Homebrew でインストールした pg gem に対して pg_config の場所を指定するコメントを、Intel Mac 前提のパスから Apple Silicon 前提のパスに変更しています。

diff
-#   gem install pg -- --with-pg-config=/usr/local/bin/pg_config
+#   gem install pg -- --with-pg-config=/opt/homebrew/bin/pg_config

Rails のジェネレータテンプレート(railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt)側が、すでに /opt/homebrew に切り替わっていたため、ガイドのサンプルもそれに追随させた形です。

(2) pool から max_connections への置き換え

database.yml 内の接続数関連のキー例を、旧来の pool から max_connections に変更しています。

diff
-  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %>
+  max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

ポイント:

  • キー名が poolmax_connections に変更
    • pool は、今後 max_connections / min_connections に置き換わっていく方向で、ドキュメント上も新しいキーを教えるように修正されています。
  • デフォルト値も 35 に変更
    • これはジェネレータテンプレートに合わせたもので、実際に rails new app --database=postgresql した結果と一致させています。

この変更により、ガイドに載っている YAML サンプルと、最新の Rails が自動生成する config/database.yml の内容が揃いました。


  1. 影響範囲・注意点
  • 実装コードへの影響はなく、ドキュメントのみの変更です。
  • ただし、これからガイドを読んで PostgreSQL 設定を行う開発者は、以下の点に注意が必要です:
    • 新規:
      • 新規アプリを Rails ジェネレータで作る場合、pool: ではなく max_connections: を使うのが推奨される形になります。
      • コネクションプールサイズを調整したい場合は max_connections / min_connections を前提に考えるとよいです。
    • 既存アプリ:
      • 既存アプリで pool: を使っていても、この PR 自体が挙動を変えるわけではありません。
      • ただし、今後のガイドや設定例は max_connections ベースになるため、タイミングを見て移行を検討すると、将来の非推奨・削除対応がしやすくなります。
  • Apple Silicon Mac(/opt/homebrew)以外の環境では、pg_config のパスは当然異なり得ます:
    • Intel Mac(Homebrew): /usr/local/bin/pg_config
    • Linux (例): /usr/bin/pg_config など
      → コメントはあくまで例示なので、自分の環境に合わせてパスは調整する必要があります。

  1. 参考情報 (あれば)

#57921 Expand Action Mailer delivery_method configuration docs [ci skip]

マージ日: 2026/7/4 | 作成者: @davidstosik

  1. 概要 (1-2文で)
    Action Mailer の config.action_mailer.delivery_method のドキュメントを拡充し、循環参照的で分かりにくかった記述を、実際に使えるレベルの説明に整理した PR です。あわせて smtp_settings / sendmail_settings / file_settings との依存関係が分かるように、記述の順番も入れ替えています。

  1. 変更内容の詳細

※コード変更ではなく、guides/source/configuring.md(Rails Guides の設定ガイド)内の Action Mailer セクションの文章修正です。
実際の差分は概ね以下のような内容を含むと考えてよいです(擬似的な例を含みます):

2.1 delivery_method の説明を具体化

以前のドキュメントでは、delivery_method の説明が他の箇所を参照するだけで、実際に何をどう設定できるのかが分かりにくく、参照が行ったり来たりする「循環」状態でした。
この PR では、以下のような情報が一か所にまとまるように整理されています:

  • delivery_method に指定できる主なシンボル例:
    • :smtp
    • :sendmail
    • :file
    • :test
    • (Action Mailer アダプタを追加していれば)独自の delivery method クラス
  • それぞれが「どのような形でメールを送る/保存する」のかという概要
    • :smtp → SMTP サーバ経由で送信
    • :sendmailsendmail コマンド経由で送信
    • :file → 実際には送信せず、メール内容をファイルに書き出す
    • :testActionMailer::Base.deliveries 配列に溜めるだけ(テスト用)

たとえば、設定例として以下のようなコードがドキュメント内で示されているはずです:

ruby
# config/environments/production.rb
Rails.application.configure do
  config.action_mailer.delivery_method = :smtp

  config.action_mailer.smtp_settings = {
    address: "smtp.example.com",
    port: 587,
    user_name: ENV["SMTP_USERNAME"],
    password:  ENV["SMTP_PASSWORD"],
    authentication: :login,
    enable_starttls_auto: true
  }
end

2.2 設定項目の順序を依存関係に沿って並べ替え

元のガイドでは、smtp_settings / sendmail_settings / file_settings が先に出てきて、その後に delivery_method が説明されていた、あるいは相互に行き来する形だったため、読み始めの人には依存関係が分かりにくくなっていました。

この PR では:

  1. まず config.action_mailer.delivery_method で「どの配信方法を使うか」を説明
  2. その後に、選択した配信方法ごとの設定 (smtp_settings, sendmail_settings, file_settings など) を説明

という順番に変えています。
これにより、「delivery_method:smtp を選んだ場合に、次に smtp_settings をこう書く」という流れが自然に理解できるようになります。

2.3 delivery_method と *_settings の関係を明記

config.action_mailer.smtp_settings などが「どの delivery method を選んだときに有効になるか」が明示されるように説明が補足されています。

例:

  • config.action_mailer.smtp_settingsdelivery_method = :smtp のときに意味を持つ
  • config.action_mailer.sendmail_settingsdelivery_method = :sendmail のときに使用される
  • config.action_mailer.file_settingsdelivery_method = :file 向け

この関係が文章としてはっきり書かれたことで、「設定を書いたのに効かない」という混乱を防げるようになっています。


  1. 影響範囲・注意点
  • 影響範囲はドキュメント(Rails Guides)のみであり、Action Mailer の動作や API には変更はありません。
  • アプリケーション側のコードを書き換える必要はありませんが、今後 delivery_method を設定する際に、
    • どの method を選ぶべきか
    • どの *_settings が効くか といった点が、今回のガイド更新を読むことで理解しやすくなります。
  • 既存プロジェクトで「メールが飛ばない」「設定が効いていない」などのトラブルがある場合、まずこの更新後のガイドを確認すると原因把握に役立ちます。

  1. 参考情報 (あれば)
  • 対応している Issue: #57876
    delivery_method のドキュメントが循環参照的で分かりにくいという報告への対応。
  • 具体的な差分は Rails 本体リポジトリの guides/source/configuring.md の変更箇所で確認できます。
  • Action Mailer の基本的な設定全体は Rails Guides の「Configuring Rails Applications」内の Action Mailer セクションにまとまっています。

#57972 Make remove_foreign_key(if_exists: true) reversible

マージ日: 2026/7/3 | 作成者: @55728

  1. 概要 (1-2文で)
    Rails のマイグレーションで remove_foreign_key if_exists: true を実行した際、その逆操作(ロールバック)として生成される add_foreign_key が誤って if_exists: true を含んでしまう問題を修正した PR です。これにより、ロールバック時に if_exists が正しく if_not_exists に変換され、操作がより安全かつ意図通りに動作するようになります。

  1. 変更内容の詳細

何が問題だったか

ActiveRecord::Migration::CommandRecorder は、マイグレーションのコマンドからロールバック用の逆コマンドを自動生成します。
従来は次のような挙動になっていました:

ruby
# forward
remove_foreign_key :orders, :users, if_exists: true

# generated inverse (before this PR)
add_foreign_key :orders, :users, if_exists: true

しかし add_foreign_keyif_exists: オプションをサポートしていません。
そのため:

  • 生成されたロールバックコードが不正なオプションを含む
  • ロールバックの「冪等性」(同じロールバックを何度実行しても問題にならないこと)が崩れる

という問題がありました。

修正内容

この PR では、invert_remove_foreign_key の処理を修正し、if_exists: true をロールバック側では if_not_exists: true に変換するようにしました。

要するに:

ruby
# forward
remove_foreign_key :orders, :users, if_exists: true

# generated inverse (after this PR)
add_foreign_key :orders, :users, if_not_exists: true

となります。

これにより:

  • すでに外部キーが存在しない場合でも remove_foreign_key が安全に実行できる (if_exists: true)
  • ロールバック時に外部キーが既に存在していても add_foreign_key 側で if_not_exists: true により二重作成を避けられる

という、前後両方向での冪等性が担保されます。

実装箇所

  • activerecord/lib/active_record/migration/command_recorder.rb
    • invert_remove_foreign_key(もしくはそれに相当するロジック)に 1 行追加し、オプション変換ロジックに if_existsif_not_exists のマッピングを追加
  • activerecord/test/cases/migration/command_recorder_test.rb
    • 上記変換が正しく行われることを確認するテストを 5 行追加

実装自体はごく小さい変更ですが、挙動の一貫性と安全性を高める修正です。


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

    • remove_foreign_key ... if_exists: true をマイグレーションで使用しているプロジェクト
    • 特に rails db:rollback / rails db:migrate:down などで自動生成される逆マイグレーションを信頼しているケース
  • 期待される挙動:

    • 既存マイグレーションの「前方」実行 (db:migrate) の挙動は変わりません。
    • 新しい Rails バージョンでロールバックすると、逆操作が add_foreign_key ... if_not_exists: true として扱われ、外部キーがすでに存在している環境でもエラーになりにくくなります。
  • 注意点:

    • add_foreign_keyif_not_exists: をサポートしていることが前提です。古い Rails から途中バージョンを飛ばしてアップグレードする場合は、if_not_exists 対応が入っているか(おおむね 7.1 以降)を確認してください。
    • もし、自前で CommandRecorder を拡張している場合は、remove_foreign_key / add_foreign_key まわりのオプション変換ロジックと競合しないか軽く確認すると安全です。

  1. 参考情報 (あれば)
  • 類似の変換ロジック:
    • invert_add_foreign_keyif_not_existsif_exists に変換しているコミット: 8d5c5bd2f5
    • invert_remove_check_constraint でも同様に if_existsif_not_exists の変換を行っており、それと整合する変更です。
  • 該当 PR:

#57970 Make ActiveSupport::ProxyLogger#silence suppress logs

マージ日: 2026/7/3 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveSupport::ProxyLogger#silence が期待どおりログを抑制するように修正された PR です。silence ブロック内で、指定した一時的なログレベル未満のメッセージが内部ロガーへ転送されないようになりました。

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

背景となる問題

ActiveSupport::ProxyLoggerLoggerSilence を include しているものの、これまで silence を呼んでも実際には何も抑制されていませんでした。

ruby
proxy = ActiveSupport::ProxyLogger.new(ActiveSupport::Logger.new($stdout))

proxy.silence do
  proxy.info("noise")
end

# 期待される動作(ActiveSupport::Logger と同じ):
#   何も出力されない
# 実際の動作(修正前):
#   "noise" が出力されていた

Rails 本体では、マイグレーション処理などの内部で logger.silence を使って冗長なログを抑制しており、
「標準 Logger インターフェースをほぼサポートする」ことをドキュメントで謳っている ProxyLoggersilence を無視してしまうのは契約違反になる、というのが問題意識です。

この PR の主な修正点

  1. ProxyLogger#silence が、ブロック内でのログレベルを一時的に引き上げて実際に抑制するようになった。

    • LoggerSilence の期待する挙動に合わせ、ブロック内では一時的な severity(例: Logger::ERROR)より低いログを出さない。
    • これにより ActiveSupport::Logger と同じ挙動になります。
  2. 抑制は「その ProxyLogger インスタンスのみに限定」されるようになった。

    • 同じ「内部ロガー(ActiveSupport::Logger など)」を共有している複数の ProxyLogger がある場合、 片方で silence しても、もう片方には影響しないように実装されています。
    • これは ActiveSupport::Logger#silence の挙動と整合的です(ロガー単位ではなく、プロキシごとにスコープされる)。
  3. テストの追加

    • activesupport/test/proxy_logger_test.rb に、次のような観点のテストが追加されています:
      • silence ブロック内で、閾値より低い severity のログが出力されないこと。
      • 同じ underlying logger を共有する別の ProxyLogger が、片方の silence の影響を受けないこと。

※ 実際のコードは 4 行追加・1 行削除と小さい変更ですが、要点は「ProxyLogger 経由でログ出力される前に、自身のサイレンス状態を考慮してフィルタする」ようにしたことです。


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

    • ActiveSupport::ProxyLogger を使い、かつ proxy_logger.silence { ... } しているコードに影響します。
    • これまで「silence を呼んでも実際には抑制されていなかった」挙動が修正されるため、ログ出力量が減る 可能性があります。
    • ただし、ProxyLogger 自体がまだリリース前のクラスであり、公開バージョンではこの挙動に依存しているアプリは基本的に存在しない想定です。
  • 注意点

    • 同じ underlying logger を共有する他の ProxyLogger に対しては、silence は効きません。
      例えば以下のケースで proxy1 を silence しても、proxy2 は通常どおりログを出します:

      ruby
      base_logger = ActiveSupport::Logger.new($stdout)
      
      proxy1 = ActiveSupport::ProxyLogger.new(base_logger)
      proxy2 = ActiveSupport::ProxyLogger.new(base_logger)
      
      proxy1.silence do
        proxy1.info("hidden")  # → 抑制される
        proxy2.info("visible") # → 出力される
      end

      複数のプロキシ間で一括してサイレンスしたい場合は、基底ロガー側で制御するか、別途ラッピングが必要です。

    • ログレベルに依存する処理(例: debug ログだけをフックするミドルウェアなど)を ProxyLogger 経由で行っている場合、silence ブロック内ではその処理も呼ばれなくなる可能性があります。


  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/57970
  • 関連クラス:
    • ActiveSupport::ProxyLogger
    • ActiveSupport::Logger
    • ActiveSupport::LoggerSilence / LoggerSilence
  • logger.silence は Rails において、マイグレーションや内部処理の一部で冗長なログを抑制する用途で使用されており、本 PR により ProxyLogger でも同じ期待値で利用できるようになります。

#57888 Make autosave association callbacks Ractor-safe

マージ日: 2026/7/3 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    Active Record の autosave な関連(autosave: true な association)で生成されるコールバックメソッドを、Ractor(並行実行環境)でも安全に動作するように実装を変更した PR です。define_method + ブロックで作っていたメソッドを class_eval + シンボル呼び出しに変えることで、非共有 Proc をキャプチャしないようにしています。

  1. 変更内容の詳細

背景・問題点

  • autosave な関連(例: has_many :comments, autosave: true)では、内部的に例えば以下のようなメソッドが自動生成されます:
    • autosave_associated_records_for_comments
  • これらは Active Record 内部の define_non_cyclic_method というヘルパで定義されており、以前はだいたい次のような形で実装されていました(イメージ):
ruby
define_non_cyclic_method(:comments) do |owner|
  owner.autosave_associated_records_for_comments
end
  • 実際には define_method とブロックを使ってメソッドが定義されており、そのブロックは Ruby の Proc オブジェクトとしてクラスにぶら下がります。
  • Ractor(Ruby 3+ の並行実行モデル)では、「共有不可能なオブジェクト(unshareable)」を他の Ractor にそのまま渡せません。ブロックから生成される Proc は通常共有不可能オブジェクトのため、それをキャプチャしたメソッドを非メイン Ractor から呼び出そうとすると制約に引っかかります。
  • その結果、非メイン Ractor 内で autosave 関連のコールバックが呼び出されるケースで問題が起きうるため、Ractor-safe な形に書き換えた、というのが PR のモチベーションです。

具体的な変更点

PR 説明によると:

define_non_cyclic_method implementation to use class_eval and symbols instead of define_method and blocks.

つまり:

  • 変更前: define_method + ブロック(Proc をキャプチャ)
  • 変更後: class_eval + メソッド名(シンボル)経由のディスパッチ

のような形になります。

中でやっていることのイメージを、かなり単純化したサンプルで表現すると:

変更前(イメージ)

ruby
def define_non_cyclic_method(name, &callback)
  define_method("autosave_associated_records_for_#{name}") do
    callback.call(self)
  end
end

ここで &callbackProc であり、define_method のブロックとしてクラスにぶら下がります。この Proc が Ractor 的には共有できないオブジェクトになりうる。

変更後(イメージ)

ruby
def define_non_cyclic_method(name, method_name)
  class_eval <<~RUBY, __FILE__, __LINE__ + 1
    def autosave_associated_records_for_#{name}
      #{method_name} # あるいは send(:#{method_name}) 等
    end
  RUBY
end
  • 文字列を class_eval してメソッドを定義するため、メソッド本体は単に既存のメソッド名を呼び出すだけになります。
  • ここでは共有不可能な Proc をクラスにキャプチャしないため、Ractor 間でも問題になりにくくなります。

実際のコードはもう少し複雑ですが、「ブロックをキャプチャして定義する」から「ソースコード文字列で定義し、実際の処理は別メソッド(シンボル)に委譲する」という方向に変わった、と理解しておくと良いです。

パフォーマンス

付属のベンチマークでは、autosave_associated_records_for_comments を大量に呼び出したときの性能を比較しています:

  • before: 約 4,491,233 i/s
  • after: 約 4,411,090 i/s

差は誤差範囲内("same-ish")とされており、パフォーマンス退行はほぼ無いとみなされています。


  1. 影響範囲・注意点

影響範囲

  • Active Record の autosave 関連コールバックの生成メカニズム(define_non_cyclic_method)が変更されています。
  • 対象となるのは主に:
    • has_many :foo, autosave: true
    • has_one :bar, autosave: true
    • その他 autosave オプション付きの関連に付随する内部的なコールバックメソッド
  • 表向きの API(autosave: true の指定方法や、関連オブジェクトの保存時挙動)は変わりません。

注意点 / 開発者視点でのチェックポイント

  • Ractor を使っていないアプリに対する互換性
    • 既存挙動を壊すような変更は意図されておらず、ベンチマーク上も挙動自体は変わらない前提です。
    • ただし、メソッド定義の仕方が define_method から class_eval に変わるため、メタプログラミングや method_source 系のツールでクラス定義を解析している場合に、挙動が微妙に変わる可能性はあります(メソッドの source_location など)。
  • Ractor を利用している / 利用予定のアプリ
    • この変更により、autosave な関連を持つモデルをサブ Ractor から扱ったときの安全性が向上します。
    • ただし Ractor そのものは他にも多数の制約があるため、「これで Active Record が完全に Ractor-safe になった」という保証ではなく、「autosave association callbacks に関する一つの問題を解消した」に留まると考えるべきです。
  • テストについて
    • チェックリストでは「Tests are added or updated」が未チェックになっています。
      • つまり、この PR 単体では新規テストやテスト更新が入っていない可能性があります。
      • 既存テストで十分にカバーされている前提か、あるいは別途 Ractor 関連のテストが追加されるフェーズがあるかもしれません。
    • Ractor 環境下の挙動についてはまだテストカバレッジが薄い可能性があるため、Ractor を本格利用する場合は手元での実験・検証を推奨します。

  1. 参考情報 (あれば)
  • Ractor と shareable / unshareable オブジェクトの制約については、Ruby 本体のドキュメントが参考になります:
  • Rails の autosave 機能自体の仕様:
    • autosave: true の関連は、親オブジェクトの save 時に関連先も自動的に save / destroy される機能で、バリデーションやトランザクションと組み合わせて複雑なオブジェクトグラフの整合性を保つ用途でよく利用されます。
  • 本 PR の趣旨:
    • 「挙動を変えずに実装を Ractor フレンドリーにする」リファクタリングの一種であり、今後の Ruby 並行実行サポートに向けた地ならしと言えます。

#57927 Make Active Record scope methods Ractor-shareable

マージ日: 2026/7/3 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    Active Record の scope が Ractor(並列実行機構)上で利用できるように、スコープ定義で使われる Proc を「Ractor-shareable」な形に変更した PR です。これにより、Ractor 内から AR モデルのスコープを安全に呼び出せるようになります。

  1. 変更内容の詳細

何をしたか

  • ActiveRecord::Scoping::Named 内の scope 定義処理を変更し、スコープ定義に使う Proc を Ractor で共有可能なオブジェクト(Ractor-shareable)として扱うようにした。
  • それに合わせてテスト (named_scoping_test.rb) を追加し、Ractor 上でもスコープが動作することを確認している。

Ruby 3 以降の Ractor では、スレッド安全性を保つために「共有できるオブジェクト」と「共有できないオブジェクト」が区別されており、通常の Proc やクロージャはしばしば「共有不可」とみなされます。この PR は、Active Record の scope が内部的に利用する Proc を、Ractor をまたいで共有できる形に組み立て直しています。

想定されるコードレベルの振る舞い

普段のスコープ定義:

rb
class Post < ApplicationRecord
  scope :published, -> { where(published: true) }
end

この PR 以前:

  • 上記の -> { ... } が作る Proc は「Ractor-shareable でない」ケースがあり、
    Ractor 内から Post.published を呼ぶとエラーになったり、利用に制限があった。

この PR 後:

  • scope :published, -> { ... } で内部的に生成される Proc が、Ractor で共有可能な形になるように処理される。
  • たとえば、以下のようなコードが動くことをテストしているはずです(イメージ):
rb
r = Ractor.new do
  # Ractor 内から Active Record のスコープを呼び出す
  Post.published.to_a
end

r.take

テストファイル (named_scoping_test.rb) では、

  • スコープが Ractor 内で問題なく呼び出せること
  • スコープチェーンや既存の機能との互換性
    といった点を確認するケースが追加されています。

  1. 影響範囲・注意点

影響範囲

  • 対象: ActiveRecord::Base.scope で定義された全てのスコープ
  • 利用場面:
    • Ractor を使った並列処理の中で Active Record のスコープを呼ぶ場合
    • Ractor をまたいでモデルクラス自体を共有するような構成

既存コードのインターフェース(scope :name, -> { ... } の書き方や返り値など)は変わりません。
スコープに関係するのは主に「内部で使われる Proc の性質」なので、通常のアプリケーションで Ractor を使っていなければ、挙動は従来とほぼ同じです。

注意点

  • Ractor 非対応なオブジェクトをキャプチャするスコープ
    スコープ定義内で、Ractor 非共有オブジェクト(ミューテーブルなオブジェクト、スレッドローカルな状態、IO など)をキャプチャしている場合、それが Ractor-shareable な Proc として扱えるかどうかは Ruby 本体/Active Record 側の実装に依存します。
    原則として:

    • スコープには「Ractor 間で共有できる情報」だけを使う
    • 外部の状態(特にミューテーブルなオブジェクト)を閉じ込めない
      という形にしておくと安全です。
  • CHANGELOG 未更新
    CHANGELOG はまだ更新されていませんが、挙動としては「新機能(Ractor 対応強化)」に近い変更です。将来的にドキュメントや変更履歴に追記される可能性があります。

  • Ruby / Rails のバージョン依存
    この PR は Ractor を前提としているため、Ruby 3 以降が前提です。Rails 側がサポートする Ruby バージョンとの組み合わせに依存するので、利用する際は公式のサポートマトリクスを確認してください。


  1. 参考情報 (あれば)

#57961 Add ActiveSupport::Ractors.try_make_shareable

マージ日: 2026/7/3 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    ActiveSupport::Ractors.try_make_shareable という新しいメソッドが追加され、任意のオブジェクトを「Ractor 共有可能(shareable)」にしようと試みる共通ユーティリティが ActiveSupport に入りました。unshareable_proc_action の設定に応じて、共有不可な Proc などにぶつかったときの挙動(例外・警告・無視)を切り替えられます。

  1. 変更内容の詳細

追加された API

activesupport/lib/active_support/ractors.rb に以下のようなメソッドが追加されています(概念的なイメージ):

ruby
module ActiveSupport
  module Ractors
    class << self
      # すでに存在するもの (イメージ)
      # def try_shareable_proc(proc, unshareable_proc_action: ...)
      #   ...
      # end

      # 今回追加されたもの
      def try_make_shareable(object, unshareable_proc_action: Rails.application.config.active_support.unshareable_proc_action)
        case unshareable_proc_action
        when :raise
          ::Ractor.make_shareable(object) # Ractor::IsolationError がそのまま飛ぶ
        when :warn
          begin
            ::Ractor.make_shareable(object)
          rescue Ractor::IsolationError => error
            ActiveSupport::Deprecation.warn(<<~MSG)
              Ractor isolation error while trying to make object shareable: #{error.message}
            MSG
            object # エラーは握りつぶして元のオブジェクトを返す/処理を続行
          end
        when nil
          begin
            ::Ractor.make_shareable(object)
          rescue Ractor::IsolationError
            object # 完全に無視(ログ・警告も出さない)
          end
        else
          # 想定外の設定値が来た場合のフォールバック(多くは :raise と等価)
          ::Ractor.make_shareable(object)
        end
      end
    end
  end
end

※上記は PR 説明とテストから読み取れる挙動イメージです。実際のコードとはメソッド名や細部が多少異なる可能性があります。

Rails にはすでに「Proc 専用」の try_shareable_proc が存在しており、この PR は「任意オブジェクト版」の try_make_shareable を追加したものです。#57852 など他の PR/機能から、この共通メソッドを呼び出す前提になっています。

unshareable_proc_action による挙動の違い

unshareable_proc_action は ActiveSupport の既存設定で、Proc が Ractor 的に共有できない場合どう扱うかを決めるフラグです。この PR では**「オブジェクト全般を Ractor 共有化しようとしたときに発生する isolation error」に対しても、その設定を流用する**ようになっています。

  • :raise(デフォルト想定)

    • Ractor.make_shareable(object) 実行時に Ractor::IsolationError が起これば、そのまま例外として呼び出し元に飛ぶ。
    • Ractor 安全性が最も厳密。
  • :warn

    • Ractor::IsolationError 発生時に、ActiveSupport の警告(Deprecation.warn など)としてログを出し、処理自体は継続。
    • 実際にはオブジェクトは共有可能にならない(/なれなかった)可能性があるが、アプリは止めずに挙動を観測できる。
  • nil

    • Ractor::IsolationError を完全に握りつぶし、何も出さない。
    • Ractor 共有化に失敗しても無視して続行したい場合のため。

テスト

activesupport/test/ractors_test.rb に 50 行超のテストが追加され、主に以下をカバーしていると考えられます:

  • try_make_shareable:raise, :warn, nil の各設定値で期待通りの挙動をすること
  • 共有可能なオブジェクト(例: freeze 済みの単純な Hash/Array など)が問題なく共有可能になること
  • 共有不可能なオブジェクト(Proc を含むオブジェクト)が、設定に応じて例外・警告・無視として扱われること

  1. 影響範囲・注意点
  • Ractor を利用している / 利用予定の Rails アプリ・ライブラリ

    • Ractor.make_shareable を直接呼ぶ代わりに、ActiveSupport::Ractors.try_make_shareable を使うことで、環境設定に応じてエラー処理を一元化できます。
    • Ractor 対応のコードを書くとき、例外をどう扱うかを都度実装する必要が減ります。
  • unshareable_proc_action 設定の意味合いが広がる

    • これまでは「Proc を共有可能にするときの挙動」を制御する設定でしたが、この PR によって「任意オブジェクトを Ractor 共有可能にしようとするときの挙動」にも影響します。
    • 既に unshareable_proc_action をカスタマイズしているアプリでは、オブジェクト共有化まわりの挙動にもそのポリシーが適用される点を意識する必要があります。
  • 例外を握りつぶす設定に注意

    • :warnnil を使うと、「本来は Ractor 共有されている前提でコードを書いたが、実際には共有化されていない」という状態になりうるため、マルチ Ractor 実行時のデータ競合や設計ミスに気付きにくくなります。
    • 本番運用ではまず :raise で問題の有無を洗い出し、問題箇所を修正した上で必要に応じて :warn/nil を検討するのが安全です。

  1. 参考情報 (あれば)

#57976 Make ActiveSupport::EventReport::LogSubscriber ractor safe

マージ日: 2026/7/3 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    ActiveSupport::EventReport::LogSubscriberevent_log_level API が Ractor セーフになるように、内部で使っている log_levels の扱いを「コピー&追記&freeze」に変更した PRです。共有ハッシュをそのまま書き換えないことで、Ractor 間でのスレッドセーフ/Ractor セーフな利用ができるようになっています。

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

※実際の差分は2行だけですが、説明のために典型的な変更前後イメージを示します(正確なメソッド名や変数名は若干異なる可能性があります)。

変更のポイント

  • log_levels(イベント名 => ログレベル のマッピング)を破壊的に更新しないようにした。
  • 新しいイベントログレベルを追加するときに
    1. 既存のハッシュを dup
    2. その dup したハッシュに新しいキー/値を追加
    3. そのハッシュを freeze
    4. それを元の参照に再代入
      というフローに変更。

このパターンは「イミュータブルなハッシュをバージョンアップしていく」やり方で、Ractor から見ても「freeze 済みのオブジェクト」を共有するだけになるため安全です。

イメージコード

変更前(イメージ):

ruby
def self.event_log_level(event_name, level)
  log_levels[event_name.to_sym] = level
end

変更後(イメージ):

ruby
def self.event_log_level(event_name, level)
  new_log_levels = log_levels.dup
  new_log_levels[event_name.to_sym] = level
  @log_levels = new_log_levels.freeze
end

ここでのポイント:

  • すでに @log_levels 自体が freeze されている想定で、その frozen なハッシュを直接書き換えなくなった
  • 毎回 dup してから freeze することで、「書き換え可能なオブジェクト」が Ractor 間で共有されなくなる。

テストの追加

activesupport/test/event_reporter/log_subscriber_test.rb に6行追加されており、主に以下の点を検証していると考えられます:

  • event_log_level を複数回呼んでも例外が出ないこと
  • log_levels が常に freeze されていること
  • (Ruby が Ractor をサポートする環境であれば)Ractor 内から event_log_level を使ってもエラーにならないこと、など

  1. 影響範囲・注意点
  • 対象: ActiveSupport::EventReport::LogSubscriber を直接または経由して利用しているコード。
    • 主に「イベントレポートのログレベルを動的にカスタマイズしている」ようなコードが影響範囲です。
  • 挙動面:
    • 公開APIとしては event_log_level の使い方・結果は変わらない想定です(同じイベントに対するログレベルが期待通り設定される)。
    • 内部実装のみがスレッド/Ractor セーフな書き方に変わっています。
  • パフォーマンス:
    • event_log_level 呼び出し毎に log_levels ハッシュの dupfreeze が入るため、わずかにオーバーヘッドは増えます。
    • ただし、この API は「起動時設定や初期化フェーズで数回呼ぶ」程度のことが多く、通常のリクエストパス上で高頻度に呼ぶことは想定されていないため、実務上は問題にならないケースがほとんどです。
  • マルチスレッド/Ractor:
    • 複数スレッドや Ractor から event_log_level を呼び出しても、共有の frozen ハッシュを直接書き換えないため、競合や Ractor 不正アクセスエラーを避けられます。
    • 既に Ractor を使っているアプリケーションでは、この変更により「イベントログレベル設定」に起因するエラーが起きにくくなります。

  1. 参考情報 (あれば)
  • Ractor セーフにする際の典型パターン:
    • 共有データ構造は freeze して不変にする。
    • 更新が必要な場合は「dup → 変更 → freeze → 参照を差し替え」のパターンを取る。
  • Rails / ActiveSupport における類似の対応:
    • キャッシュ設定やログ設定など、グローバルなコンフィグを扱う箇所では、同様の「イミュータブルなハッシュ/配列を差し替える」手法が徐々に導入されています。
  • 本 PR は CHANGELOG 更新は行われておらず、「外部API仕様は変えずに内部を Ractor フレンドリーにした小規模バグ修正/改善」という位置づけです。

#57978 Make Normalization.normalizes ractor safe

マージ日: 2026/7/3 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    ActiveModel::Normalization.normalizes が Ractor(マルチスレッド並列実行)環境でも安全に利用できるよう、正規化属性を追加する内部実装を「複製してから追記して凍結する」形に変更した PR です。挙動自体は変えずに、内部データ構造の扱い方だけをスレッド/Ractor安全寄りにしています。

  1. 変更内容の詳細

何をやったか

ActiveModel::Attributes::Normalization モジュール内で、normalizes 呼び出し時に使う内部配列(もしくは類似のコレクション)の扱いを以下のように変更しています。

  • これまで: 既存の配列に対してそのまま << などで追記(破壊的変更)
  • 変更後:
    1. 既存の配列を dup で複製
    2. 複製した配列に追記 (append)
    3. その配列を freeze して不変オブジェクトとして保持

これにより、ある Ractor で参照中の配列が、別の Ractor から破壊的に変更されることを防いでいます。

PR本文で書かれていた「Dup, append and freeze.」がまさにこのパターンです。

ざっくりしたイメージコード

実際のコードとは多少違う可能性がありますが、意図としては以下のような変化です:

ruby
# 変更前のイメージ
def normalizes(name, **options, &block)
  @normalizations ||= []
  @normalizations << build_normalization(name, options, &block)
end

# 変更後のイメージ
def normalizes(name, **options, &block)
  @normalizations ||= [].freeze
  new_normalizations = @normalizations.dup
  new_normalizations << build_normalization(name, options, &block)
  @normalizations = new_normalizations.freeze
end

ポイント:

  • 共有されるインスタンス変数(例: @normalizations)自体は置き換え(再代入)で更新し、
    「すでに公開済みのオブジェクト」は一切破壊的に書き換えない。
  • 新しく作る配列は最終的に freeze されるため、Ractor 間で共有しても書き換え不可能

テストの追加

  • activemodel/test/cases/attributes/normalization_test.rb に 7 行追加されています。
  • 目的は主に:
    • normalizes 呼び出し後も内部状態が適切であること
    • 複数回 normalizes を呼んだときに前の定義が壊れないこと
    • 凍結配列を前提にした挙動でも問題ないこと
      などを確認するものと考えられます(Ractor そのものを直接テストしているかはコード次第ですが、「破壊的変更しない実装」になっていることを保証するテストが入っているはずです)。

  1. 影響範囲・注意点

影響範囲

  • 対象: ActiveModel::Attributes::Normalization を利用しているコード、具体的には:
ruby
class User
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Attributes::Normalization

  attribute :email, :string

  normalizes :email, with: ->(value) { value.to_s.strip.downcase }
end

のように normalizes を使っている箇所。

  • 表面的な挙動(値がどう正規化されるか)は変わりません。
  • 主に内部の属性正規化情報テーブルの更新手順のみが、破壊的更新から「イミュータブルな再割り当て」方式に切り替わっています。

Ractor 安全性について

  • この PR によって:
    • 「normalized 属性を追加する処理」自体が Ractor セーフに近づいた
      (既存オブジェクトの破壊的変更を避けるため)
  • ただし PR 中でも明記されている通り:

Note, attribute registration that this relies on is still not ractor safe, its slightly more involved. Someone will do it next week probably.

  • つまり:
    • 「属性(attribute)自体の登録」に関わるコードは、まだ Ractor セーフではない。
    • normalizes 周辺だけが先行して Ractor セーフなスタイルにリファクタされている状態。
  • 結論として:
    • この PR だけで ActiveModel 属性周り全体が完全に Ractor セーフになるわけではない
    • とはいえ、将来的な完全 Ractor 対応に向けた一部ステップであり、
      少なくとも normalizes による内部配列の共有・書き換えに起因する Ractor 競合バグは出にくくなります。

パフォーマンス・互換性

  • dup + freeze により:
    • normalizes 呼び出しごとに配列のコピーが発生するため、属性数・normalizes 呼び出し回数が非常に多い場合には、わずかなオーバーヘッド増加があり得ます。
    • ただし normalizes 自体は通常アプリケーション起動時にクラス定義の中で呼ぶことが多く、リクエストごとに大量に呼ばれるものではないため、実務上は問題になりにくいと考えられます。
  • API 互換性:
    • 公開 API (normalizes の引数や動作) は変わっていないため、既存コードは基本的にそのまま動作します。

  1. 参考情報 (あれば)
  • Ruby Ractor とイミュータブルデータ構造:

    • Ractor 間で共有できるのは、基本的に「凍結されたオブジェクト」や Ractor セーフな型だけであり、可変オブジェクトを共有し破壊的変更するとエラーや未定義動作の原因になります。
    • この PR の dupappendfreeze パターンは、Ractor 対応で頻出する「内部状態をイミュータブルにする」ための典型的リファクタです。
  • 今後の想定:

    • PR 本文にある通り、属性登録周りの Ractor 対応が別 PR で行われる予定で、
      それと組み合わさることで ActiveModel 属性システム全体がより Ractor フレンドリーになっていきます。

#57979 Use latest ruby for GHA integration tests

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

  1. 概要 (1-2文で)
    このPRは、GitHub Actions 上で動作する Rails の各種インテグレーションテスト(devcontainer、rails new docker、rail_inspector)で使用する Ruby のバージョンを「最新の Ruby」に切り替える変更です。テストフロー自体は変えず、実行環境としての Ruby の指定のみを更新しています。

  2. 変更内容の詳細(あればサンプルコードも含めて)
    対象ファイルはいずれも GitHub Actions のワークフローファイルです。

  • .github/workflows/devcontainer-smoke-test.yml
  • .github/workflows/rail_inspector.yml
  • .github/workflows/rails-new-docker.yml

各ワークフロー内で、Ruby のバージョンを指定している箇所が 1 行ずつ書き換えられています。具体的には、従来は固定バージョン(例: 3.3, 3.2 など)を指定していたものを、「最新の安定版 Ruby」を指す指定に変更した形が想定されます。

典型的には、以下のような変更になっている可能性が高いです(参考イメージ):

yaml
# 変更前(例)
- uses: ruby/setup-ruby@v1
  with:
    ruby-version: '3.3'

# 変更後(例)
- uses: ruby/setup-ruby@v1
  with:
    ruby-version: 'ruby'  # または '3.4' など、その時点の最新

もしくは、matrix で定義している ruby: [ '3.3' ] を最新バージョンに上げる、という形の可能性もあります。
いずれにせよ、各ワークフローの「使う Ruby のバージョンを 1 行だけ差し替えている」以外の変更は行われていません。

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

    • GitHub Actions 上で実行される以下のワークフローの Ruby 実行環境:
      • devcontainer-smoke-test
      • rail_inspector
      • rails-new-docker
    • これらが「常に最新の Ruby で Rails をテストする」状態になるため、Rails 本体が最新 Ruby に対して継続的に検証されるようになります。
  • メリット

    • Ruby の新バージョンがリリースされた際に、自動的にそのバージョンで検証が走るようになり、Ruby 側の変更による非互換や警告の早期検知が可能になります。
    • Rails の「最新 Ruby サポート状況」が CI 上で常にカバーされるため、メンテナンス性の向上に寄与します。
  • 注意点 / リスク

    • Ruby の新バージョンで非互換やバグが出た場合、Ruby リリース直後に CI が急に壊れる可能性があります(=Rails 側が即座には対応できていないケース)。
    • もし「特定の Ruby バージョンでの動作保証」を厳密にテストしたい場合は、別のジョブやマトリクスで LTS 的なバージョン(例: 3.1, 3.2 など)を並行して運用する必要があります。
    • 新しい Ruby でのみ発生する警告(deprecation など)が増え、ログがノイジーになる可能性があります。
  1. 参考情報 (あれば)

#57974 Require useragent when loading ActionController::AllowBrowser

マージ日: 2026/7/3 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    ActionController::AllowBrowser を読み込むタイミングで useragentrequire するように変更し、これまで「最初のリクエスト時」に行われていた useragent の読み込みを事前に行えるようにした PR です。主に eager load 環境や Ractor 対応時の初回読み込みタイミングの改善が目的です。

  1. 変更内容の詳細

※PR本文からはファイル単位の情報しかありませんが、内容的には以下のような極小変更です(+2/-2 行)。

対象ファイル:

  • actionpack/lib/action_controller/metal/allow_browser.rb

やっていることは要約すると:

  • これまで:
    • AllowBrowser が使われるタイミング、あるいは最初のリクエスト処理のどこかで useragentrequire していた(=eager load していても、実際の最初のリクエスト時に初回 require が走る)。
  • これから:
    • ActionController::AllowBrowser クラス/モジュールがロードされる段階で require "useragent" を実行するように変更。

イメージとしては、以下のような差分になっている可能性が高いです(擬似コード):

ruby
# 変更前 (例)
module ActionController
  class AllowBrowser
    # どこかのメソッド内や遅延ロード処理の中で:
    # require "useragent"
  end
end

# 変更後 (例)
require "useragent"  # ← ファイルロード時に実行

module ActionController
  class AllowBrowser
    # ここでは useragent はすでにロード済み
  end
end

これにより、AllowBrowser を使う環境では、そのファイルがロードされた時点で useragent もロードされます。


  1. 影響範囲・注意点
  • 影響範囲
    • ActionController::AllowBrowser を使用しているアプリ・ライブラリ。
    • 特に eager load を有効にしている本番環境や、Ractor を利用するような並行実行環境での挙動に影響します。
  • 具体的な挙動の変化
    • 以前: 最初のリクエスト処理中に useragentrequire が発生し、そのタイミングでロードコストやスレッド/Ractor との競合リスクがあった。
    • 以後: Rails が ActionController::AllowBrowser をロードした時点で useragent が確定的にロードされるため、最初のリクエスト時のオーバーヘッドが減り、Ractor 的にも安全になりやすい。
  • パフォーマンス・メモリ
    • AllowBrowser を利用している環境では、「どうせいつかは useragent を読む」ので、初回リクエスト前に読むか後に読むかの違いであり、全体的なメモリ使用増はほぼ変わりません。
    • ただし、AllowBrowser を定義だけ読み込んで実際には一切使わないような特殊な構成では、以前よりも早いタイミングで useragent がロードされる可能性があります。
  • 互換性
    • useragent 自体の API 変更はなく、あくまで require のタイミング変更のため、一般的なアプリでは後方互換性上の問題はほぼありません。
    • useragent gem を明示的に Gemfile に入れていない状況で(たまたま別の依存関係から入っていたなど)、ロード順に依存していたような非常にレアなケースでは、LoadError の発生タイミングが変わる可能性があります。

  1. 参考情報 (あれば)
  • Ractor との関連:
    • Ractor 環境では、スレッドセーフでない初回 require や定数定義が、並行実行と絡んで問題を起こし得るため、「最初のリクエスト中に require される」ようなパターンは避けたい。
    • 本PRにより、AllowBrowser のロードタイミングで useragent が確定的に読み込まれるため、Ractor 開始前に eager load を済ませる設計と相性が良くなります。
  • 実運用での確認ポイント:
    • AllowBrowser を利用しているアプリでは、本番起動時(Rails boot 時)に useragent が読み込まれることを前提に、必要ならば Gemfile に gem "useragent" が明示的に含まれているかを確認しておくと安全です。

#57977 Fix typographical errors from CHANGELOG files[ci skip]

マージ日: 2026/7/3 | 作成者: @nisusam

  1. 概要 (1-2文で)
    このPRは、Rails の CHANGELOG ファイル内に含まれていた英単語のタイポ(綴り誤り)を修正するものです。コードや挙動は一切変更せず、ドキュメントの品質向上のみを目的としています。

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

  • 対象ファイル

    • actionpack/CHANGELOG.md
    • actionview/CHANGELOG.md
    • railties/CHANGELOG.md
  • 変更内容

    • 各 CHANGELOG に記載されている文章中の typographical error(スペルミスや細かな表記揺れ)を、それぞれ正しい綴りに修正しています。
    • 追加行数 3 / 削除行数 3 から分かる通り、修正は行単位で 1 箇所ずつの軽微な置き換えであり、文面の意味や履歴内容自体は変えていません。
    • 例としては以下のような変更が想定されます(イメージであり、実際の差分そのものではありません):
      diff
      - * Fixes a behvaior where ...
      + * Fixes a behavior where ...
      
      - * Depreacted option ...
      + * Deprecated option ...

    Ruby コード・設定ファイル・テストコードなど、実行時に読み込まれる部分への変更は行われていません。

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

    • 影響するのはドキュメント(CHANGELOG)のみであり、Rails アプリケーションの実行時挙動、ビルド、テスト、API は一切影響を受けません。
    • 既存のバージョン互換性・マイグレーション・設定などにも影響なし。
  • 注意点

    • CI スキップ指定([ci skip])がタイトルに入っており、テストが走っていない点はログ上の事実としてありますが、そもそも実行可能コードに触れていないため問題にはなりません。
    • CHANGELOG の参照時に、以前のタイポに依存したリンクや検索(例: タイポした単語で全文検索していた)を行っていた場合は、今後は正しい綴りで検索する必要があります。
  1. 参考情報 (あれば)
  • PR タイトルから分かる通り、この変更は「CHANGELOG ファイル中の誤字修正」に限定されており、Rails の機能追加やバグ修正とは無関係です。
  • Rails では、コードと直接関係ないドキュメント変更についても PR ベースで管理し、CI をスキップして軽量にマージする運用が行われています。

#57959 Fix polymorphic belongs_to primary key for sharded targets

マージ日: 2026/7/3 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
  • シャーディング対応で「複合主キーを持つ has_many」と「単一外部キーを持つ polymorphic belongs_to」を組み合わせたときに、AutosaveAssociation 内で主キーと外部キーの対応付けがずれて ActiveModel::MissingAttributeError が起きる回 regress を修正した PRです。
  • BelongsToReflection#association_primary_key が、belongs_to 側が単一外部キーしか持たない場合でも、シャーディング用の複合主キー(query_constraints)をそのまま返してしまっていた挙動を是正しています。

  1. 変更内容の詳細

問題の背景

  • シャーディング対象モデルは、典型的に複合主キーのような「問い合わせ上の主キー」を持ちます:
    • 例: [:shard_id, :id]query_constraints として扱う
  • 一方で、対応する belongs_to 側は単一の外部キーしか持たないケースがあります:
    • 例: belongs_to :shipment, foreign_key: :shipment_id, polymorphic: true のような形
  • Rails 7.2 系での refactor により、AutosaveAssociation 内で使われていた compute_primary_key が削除され、代わりに BelongsToReflection#association_primary_key の戻り値がそのまま使われるようになりました(該当コミット)。
  • AutosaveAssociation は「関連先の primary_key」と「自分の foreign_key」を Array#zip して対応付ける実装になっているため、
    • primary_key 側が [:shard_id, :id](長さ2)
    • foreign_key 側が [:shipment_id](長さ1) のように長さが合わないと、
    • shard_id の対応 foreign_key が存在せず nil
    • それを読み出そうとしたときに ActiveModel::MissingAttributeError が発生してしまう、という不具合が生じていました。

原因になっていた association_primary_key の挙動

BelongsToReflection#association_primary_key は、関連の解決のために使う「関連先の主キー的なカラム」を求めるメソッドです。今回問題になったのは2つの分岐です:

  1. inverse_of がある場合の分岐:
    • inverse.options[:primary_key] をそのまま採用していた
    • 通常の「単一カラムの custom primary key(例: :uuid)」なら正しいが、
    • シャーディング対応で has_many 側に primary_key: [:shard_id, :id] などの「複合 primary_key もどき」が指定されている場合、belongs_to 側は単一外部キーでも composite をそのまま採用してしまう
  2. 上記以外のフォールスルー分岐:
    • ターゲットモデルが has_query_constraints? の場合、query_constraints(複合主キー的な配列)を展開して返す
    • その際、belongs_to が「明示的に単一 foreign_key を指定している」ことを考慮していなかった

その結果、belongs_to 側が単一 foreign_key にもかかわらず、関連先の「複合 primary key」配列が返ってきてしまい、AutosaveAssociation 側の zip で長さが合わずに例外を起こしていました。

修正内容のポイント

今回のコミットは、過去に compute_primary_key で行っていた「2つのガード」を association_primary_key に復元する形で問題を解消しています。

  1. inverse_of ブランチのガード復活

    • 目的: inverse.options[:primary_key] が「複合キー由来」のときに採用しないようにする
    • 実装の考え方:
      • has_many 側が「複合 foreign_key」を宣言しているとき、その情報は :query_constraints にも反映される
      • したがって、inverse.options:query_constraints が存在する場合、そこからは「複合キー前提」であることが分かる
    • 修正:
      • association_primary_key 内で inverse.options[:query_constraints] の有無を確認し、
        • ある場合: composite な inverse primary_key(≒query_constraints 由来)として扱うため、inverse.options[:primary_key] をそのまま採用しない
        • ない場合: 本当に単一の custom primary_key(例: uuid)を指定していると判断し、inverse.options[:primary_key] を採用する
  2. フォールスルーブランチのガード復活

    • 目的: belongs_to 側が明示的に単一 foreign_key を指定しているときには、関連先も単一 primary_key に解決するようにする
    • 修正:
      • has_query_constraints? なターゲットであっても、
        • belongs_to で「単一の foreign_key が明示されている」場合は、query_constraints を複合キーとしてそのまま返さず、
        • 単一の primary_key(primary_key ないし id)に解決するようなロジックを追加
      • これにより、zip の対象が
        • primary_key 側: [:id](または同等)
        • foreign_key 側: [:shipment_id] となり、要素数が一致して安定して動作する

テストの追加・変更

  • activerecord/test/cases/associations/belongs_to_associations_test.rb
    • polymorphic belongs_to + シャーディング対象モデル + inverse_of が絡むケースをカバーするテストを追加
  • activerecord/test/cases/autosave_association_test.rb
    • AutosaveAssociation 経由で保存されるシナリオ(accepts_nested_attributes_for 等)の中で、例外が発生しないことを確認する回帰テストを追加
  • activerecord/test/models/adjustment.rb, shipment.rb, schema.rb
    • 上記テスト用のモデル・スキーマ定義を拡張
    • シャーディング風の query_constraints/複合 primary_key もどき + polymorphic belongs_to の組み合わせを表現するためのフィクスチャモデルを追加

  1. 影響範囲・注意点
  • 影響を受ける可能性が高いケース:
    • シャーディングやマルチテナント構成で
      • モデルに query_constraints(例: [:shard_id, :id] 等)を設定している
      • そのモデルに対して polymorphic な belongs_to を張っている
      • inverse 側に has_many + primary_key: [:shard_id, :id] のような宣言をしている
      • かつ AutosaveAssociation 経由(accepts_nested_attributes_for など)で保存している
  • そのようなアプリでは、Rails のバージョン更新後に以下のような症状が出ていた場合、この PR によって解消される可能性があります:
    • ActiveModel::MissingAttributeErrorautosave のタイミングで突然出る
    • メッセージの原因が「関連先モデルの shard_id など、複合キーの一部が nil とみなされている」ように見える
  • 互換性の観点:
    • 通常の(非シャーディング・非複合キー)belongs_to / has_many には影響しない設計になっています。
    • 単一カラムの custom primary_key(例: primary_key: :uuid)を使っている場合も、inverse.options[:query_constraints] が無い限りはこれまでどおり inverse.options[:primary_key] が採用されるため、挙動は維持されます。
  • 注意点:
    • アプリ側で「belongs_to は単一外部キーだが、関連先は複合 query_constraints を前提にしている」という、かなりトリッキーなカスタマイズをしていた場合、今回の「単一 foreign_key 優先の解決ロジック」により挙動が変わる可能性があります。
    • 特に、独自に query_constraints をオーバーライドしているような高度なシャーディング実装を行っている場合は、この PR を含む Rails へのアップデート時に関連の保存・ロードの挙動をテストで確認することを推奨します。

  1. 参考情報 (あれば)
  • 回帰のきっかけとなったコミット:
  • 関連クラス・メソッド:
    • ActiveRecord::Reflection::BelongsToReflection#association_primary_key
    • ActiveRecord::AutosaveAssociation
    • ActiveRecord::Reflection#has_query_constraints?query_constraints
  • 実際のコードを確認したい場合は、activerecord/lib/active_record/reflection.rbassociation_primary_key 実装と、autosave_association_test.rb に追加されたテストケースを読むと、
    「inverse に複合 primary_key がある / belongs_to に単一 foreign_key がある」という具体的な差分が把握しやすくなります。

#57942 Return all parameters from deconstruct_keys when no keys are requested

マージ日: 2026/7/3 | 作成者: @55728

  1. 概要 (1-2文で)
    ActionController::Parameters に対するパターンマッチで、{ key:, **rest } のように 特定キーを指定せず rest バインディングだけを使った場合にマッチしなかった問題を修正し、Hash#deconstruct_keys と同じ挙動(キー指定なしなら全パラメータを返す)になるようにした変更です。

  1. 変更内容の詳細

これまでの問題点

Ruby 3 以降のパターンマッチ(case ... in)は、ハッシュライクなオブジェクトに対して deconstruct_keys を呼び出してパターンにマッチさせます。

ActionController::Parameters はこのインターフェイスを実装していますが、
パターン側で特定のキーをひとつも要求していない場合in { **rest } や、キーは指定するけど rest のみを実質使うようなケース)に、deconstruct_keys空ハッシュ {} を返してしまう挙動になっており、結果としてマッチ失敗になっていました。

PR で説明されている再現コード:

ruby
params = ActionController::Parameters.new(name: "Bob", age: 22).permit!

case params
in { name:, **rest }
  # Before: ここに来ない(マッチしない)
  # After:  name => "Bob", rest => { age: 22 }
end

原因は、deconstruct_keys(nil)(=「特定のキーは要求していない」という意味)に対して、ActionController::Parameters が空ハッシュを返していたためです。

Ruby の Hash#deconstruct_keys は、引数が nil のとき「全キー・全値」を返す仕様なので、ActionController::Parameters だけ挙動が異なり、パターンマッチが期待通り動かない不整合がありました。

今回の修正内容

actionpack/lib/action_controller/metal/strong_parameters.rbdeconstruct_keys 実装が修正されました:

  • 引数 keysnil の場合に
    全パラメータを返すように変更
  • これにより Hash#deconstruct_keys と同じ挙動になります。

テスト (actionpack/test/controller/parameters/equality_test.rb) では、おそらく次のような性質が確認されています(実際のコードは要約):

  • ActionController::Parameters.new(name: "Bob", age: 22).permit! に対し、
    • case params; in { name:, **rest }; ...; end がマッチすること
    • name"Bob" になること
    • rest{ age: 22 } 相当になること

動作イメージ(サンプル)

変更後の振る舞い:

ruby
params = ActionController::Parameters.new(
  name: "Bob",
  age: 22,
  admin: true,
).permit!

case params
in { name:, **rest }
  # deconstruct_keys(nil) が全パラメータを返すのでマッチする
  # name => "Bob"
  # rest => { "age" => 22, "admin" => true } あるいは Parameters 相当
end

case params
in { **rest }
  # これもマッチする(rest にすべてが入る)
end

  1. 影響範囲・注意点
  • 影響を受けるコード
    • ActionController::Parameters を直接 case ... in でマッチさせているコード
    • 特に { key:, **rest }{ **rest } のような rest バインディングを使うパターン を利用している場合に挙動が変わります。
  • 以前との違い
    • 以前は「マッチしない」ケースが、「マッチして全パラメータが渡される」ようになります。
    • つまり、以前は else 側に流れていた分岐が、in { ... } 側に入るようになる可能性があります。
  • 互換性
    • Ruby 標準の Hash と同じ deconstruct_keys 挙動になるため、設計上はむしろ一貫性が向上した変更です。
    • ただし、「ActionController::Parameters ではこのパターンはマッチしないはず」という前提で書かれていた奇妙なワークアラウンドがあれば、その前提は崩れます。
  • Strong Parameters / permit の扱い
    • 例では permit! 済みのオブジェクトになっており、通常は 許可されたパラメータの集合deconstruct_keys の対象です。
    • 未許可のパラメータを含む場合、もともとの ActionController::Parameters の振る舞い(フィルタリングの有無など)に依存するため、permit / require の位置づけは引き続き意識する必要があります。

  1. 参考情報 (あれば)
  • Ruby 本体の仕様: Hash#deconstruct_keys
    • 引数が nil のとき「ハッシュ自身を返す」という仕様で、今回の変更はこれに追従しています。
  • Rails側 PR: Return all parameters from deconstruct_keys when no keys are requested (Rails PR #57942)
    • ActionController::Parameters を Ruby のパターンマッチ機構とより自然に連携させるための小さな互換性改善です。

#57965 8 1 stable cp 1dbad939f8

マージ日: 2026/7/3 | 作成者: @p8

  1. 概要 (1-2文で)
    このPRは、Rails 8.1 ステーブルブランチに対して「product_reviews」という新しいガイドを大きく追加し、既存の「wishlists」ガイドをわずかに調整する、ドキュメント中心の変更です。コード本体には手が入っておらず、開発者向けの学習・サンプルアプリケーション的なガイドが充実した形になります。

  1. 変更内容の詳細

2-1. guides/source/product_reviews.md の新規追加 (+1350行)

1350行規模の大きなガイドが新規に追加されています。内容からすると、以下のような「Rails 8.1 を用いた Product Reviews(商品レビュー)機能」のチュートリアル/サンプルアプリ的ガイドである可能性が高いです(Wishlists ガイドと同系統のシリーズであることが多いです)。

想定される構成・内容:

  • アプリケーション概要

    • ECサイトや商品カタログに対して「レビュー」や「評価(rating)」を投稿できる機能を題材にしたガイド
    • Rails 8.1 の標準的なスタック(Zeitwerk, importmap / jsbundling, turbo, stimulus など)を前提とした作り
  • モデル設計例

    • Product / User / Review などの関連付け

    • 典型的には下記のような関連を解説していると考えられます:

      ruby
      class Product < ApplicationRecord
        has_many :reviews, dependent: :destroy
      end
      
      class Review < ApplicationRecord
        belongs_to :product
        belongs_to :user
      
        validates :rating, presence: true, inclusion: { in: 1..5 }
        validates :body, presence: true
      end
      
      class User < ApplicationRecord
        has_many :reviews, dependent: :nullify
      end
  • マイグレーションとスキーマ設計

    • reviews テーブルのカラム例:
      • product_id / user_id (外部キー)
      • rating (整数)
      • body (テキスト)
      • created_at, updated_at
    • インデックスや外部キー制約の付け方
  • コントローラとルーティング

    • resources :products do resources :reviews, only: %i[create update destroy] end のようなネストされたリソースルーティング

    • ReviewsController での create, update, destroy アクション実装例

      ruby
      class ReviewsController < ApplicationController
        before_action :set_product
      
        def create
          @review = @product.reviews.build(review_params.merge(user: current_user))
      
          if @review.save
            redirect_to @product, notice: "Review was successfully created."
          else
            render "products/show", status: :unprocessable_entity
          end
        end
      
        private
      
        def set_product
          @product = Product.find(params[:product_id])
        end
      
        def review_params
          params.require(:review).permit(:rating, :body)
        end
      end
  • ビュー・UI 実装の解説

    • 商品詳細ページにレビュー一覧と投稿フォームを組み込む例
    • form_with model: [@product, @product.reviews.build] などネスト資源のフォーム作成
    • rating をセレクトボックスやラジオボタンで扱う例
  • バリデーション・エラーハンドリング

    • バリデーションエラー時に同じページにレンダリングしなおすパターン
    • Turbo 対応時のエラーレスポンスやステータスコードの選び方 (422 Unprocessable Entity)
  • 認可・認証関連の話題(ある程度触れられる可能性)

    • ログイン済みユーザーのみレビュー可能
    • 自分のレビューしか編集・削除できない、といったポリシー例
  • テストのサンプル

    • Minitest / RSpec いずれか、または Rails 標準のテスティングガイドに沿った形でのモデル・コントローラテスト例
    • ルーティング・関連付け・バリデーションなどのテスト

要点として、このガイドは:

  • Rails 8.1 で「ある程度現実的なドメイン(product reviews)」を使って
  • モデル・ビュー・コントローラ・ルーティング・バリデーション・テスト
  • そして場合によっては Turbo/Stimulus まで

を一通りつなげて見せる「実践的なチュートリアル」のような位置づけと思われます。

2-2. guides/source/wishlists.md の変更 (+1 / -8)

wishlists.md に対してはごく小さい修正のみです。

典型的にありえる内容は:

  • Product Reviews ガイドとの整合性を取るためのリンク追加・章番号の調整
  • 「次に読むガイド」として product_reviews へのリンクを追加
  • 古いバージョン固有の記述や typo の修正・削除

行数的には 8 行削除 / 1 行追加なので、文章の差し替え・段落の統合・見出しレベルの変更など、小さな編集にとどまっています。機能仕様や API 解説のような本質的変更ではないと考えて問題ありません。


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

    • 変更はすべて guides/source 配下であり、Rails 本体のコードは一切変更されていません。
    • そのため、アプリケーションの挙動・API・互換性への影響はありません。
  • 対象バージョン:

    • タイトルから、「8 1 stable」ブランチ向けの cherry-pick と思われます。
    • Rails 8.1 系の公式ガイドに Product Reviews チュートリアルが追加された、という位置づけです。
  • ドキュメント利用者への影響:

    • Rails 初学者や、サンプルアプリを通じて 8.1 の構成に慣れたい開発者に有用です。
    • Wishlists ガイドを既に読んでいる場合、その続き・姉妹編のような形で Product Reviews ガイドを読むことになる可能性があります。
    • 実案件で reviews 機能を実装する際のモデル・コントローラ・ビュー構成の「リファレンス例」としても利用できます。

注意点としては以下程度です:

  • ガイドはあくまでサンプルであり、そのまま本番環境に流用する場合は認可・バリデーション・攻撃耐性(スパムや大量投稿など)を自分の要件に合わせて補強する必要があります。
  • 8.1 以外のバージョン(特に 7 系以前)では、ガイド中で前提とされている API やフロントエンド構成が異なる場合があります。

  1. 参考情報 (あれば)

#57964 Fix casing in product reviews guide

マージ日: 2026/7/3 | 作成者: @p8

  1. 概要 (1-2文で)
    このPRは、Rails ガイド「product_reviews.md」内の英単語の大文字・小文字表記(casing)を修正し、用語やコードの表記ゆれを整えるドキュメント修正です。機能や挙動の変更はなく、アプリケーションコードやテストには影響しません。

  1. 変更内容の詳細
  • 対象ファイルは guides/source/product_reviews.md のみで、行単位で「+21 / -21」となっており、文章やコード例の一部表記が置き換えられています。
  • タイトルの通り "casing" の修正が中心で、主に以下のような変更が想定されます(実際の差分から一般的に起こるパターンを整理):
    • 英語の表記ゆれの統一
      • 例: "Product reviews" → "Product Reviews"(ガイドタイトルや見出しとしての表記統一)
      • 例: "api" → "API"、"json" → "JSON" などの一般的な頭字語の大文字化
      • 例: "Postgresql" → "PostgreSQL" など、固有名詞の正しい大文字小文字への修正
    • Rails 用語の表記統一
      • 例: "Active record" → "Active Record"
      • 例: "Action cable" → "Action Cable"
    • コード例・識別子の区別を明確にするための修正
      • 文中のクラス名・モジュール名を、実際の Ruby/Rails の定義と揃えた可能性があります
        • 例: productreviewProductReview
        • 例: productreviewscontrollerProductReviewsController
      • 文章とコードの混在箇所で、コードは小文字スネークケース、クラス名はパスカルケースといった区別を明確化

※差分から見ると行数は同じであるため、段落削除や追加ではなく、同じ行の中での文字置換(大文字小文字の変更)が中心です。


  1. 影響範囲・注意点
  • ランタイム挙動への影響:
    • ガイド文書のみの変更であり、Rails 本体のコード・API・挙動・テストには一切影響しません。
  • 既存アプリケーションへの影響:
    • 既存の Rails アプリやガイドに従って実装しているコードが壊れることはありません。
  • 読者への影響 / 注意点:
    • ガイドを参照している場合、用語やクラス名の表記が Rails 本体の実際の命名規則(クラス: CamelCase、ファイル・テーブル: snake_case など)とより一致するように改善されています。
    • もし過去に古いガイドの表記に合わせてクラス名などを誤って命名していた場合は、今後このガイドを参照するときには、改めて Rails の一般的な命名規則(例: ProductReview / product_reviews / product_reviews_controller.rb)に合わせることを推奨します。

  1. 参考情報 (あれば)
  • この PR には CHANGELOG への追記はなく、Rails 的には「ドキュメントの軽微な修正」と位置づけられています。
  • Rails ガイドと実際の API/クラス名を揃えることは、チュートリアルどおりに書いたのにエラーになる、といった学習者の混乱を防ぐ効果があります。
  • Rails の命名規則の詳細は以下を参照するとよいです(英語):

#57244 [RF-DOCS][ci-skip] Add Product Reviews Tutorial

マージ日: 2026/7/3 | 作成者: @excid3

  1. 概要 (1-2文で)
    このPRは、Rails公式ガイドの「ECサイトチュートリアル」の続きとして「商品レビュー(Product Reviews)」機能を実装する新しいチュートリアルを追加するドキュメント変更です。ユーザーによる星評価付きレビュー、画像アップロード、レビューに基づく商品の平均評価更新、フィルタリング・集計表示、管理画面でのレビュー編集などを一通りカバーしています。

  1. 変更内容の詳細

※コードベース本体ではなく「ガイド(Documentation)」のみの変更です。新規に product_reviews.md が追加され、既存の wishlists.md がわずかに修正されています。

2-1. 新チュートリアル: guides/source/product_reviews.md

このチュートリアルは、既存の「ECストア」チュートリアルに続けて「商品レビュー機能」を実装する流れを詳細に説明しています。主な要素は以下です。

モデル構成と関連付け

典型的には次のようなモデル構造を前提として解説されます:

  • Product
    • 多数の Review を持つ
    • 平均評価(例: ratingaverage_rating カラム)を持ち、レビュー投稿・更新のたびに再計算
  • Review
    • belongs_to :product
    • belongs_to :user (レビュアーを紐づけるケースが多い)
    • 属性例:
      • rating (1〜5の整数)
      • body (本文)
      • 画像添付用の has_many_attached :images など (Active Storage を使用した例が多い)

サンプルイメージ:

ruby
class Product < ApplicationRecord
  has_many :reviews, dependent: :destroy

  def update_rating!
    update!(
      rating: reviews.average(:rating).to_f.round(1) # 小数1桁など
    )
  end
end

class Review < ApplicationRecord
  belongs_to :product
  belongs_to :user

  has_many_attached :images

  validates :rating, presence: true, inclusion: { in: 1..5 }

  after_commit :update_product_rating

  private

  def update_product_rating
    product.update_rating!
  end
end

コントローラとルーティング

レビューは、商品にネストしたリソースとして扱う構成が説明されます。

ruby
# config/routes.rb
resources :products do
  resources :reviews, only: [:create, :edit, :update, :destroy]
end

ReviewsController では、以下のような操作をカバーします。

  • レビュー作成 (create)
  • レビュー編集・更新 (edit / update)
  • レビュー削除 (destroy)(場合によっては管理者のみ)
ruby
class ReviewsController < ApplicationController
  before_action :set_product
  before_action :set_review, only: [:edit, :update, :destroy]
  before_action :authenticate_user!

  def create
    @review = @product.reviews.new(review_params.merge(user: current_user))

    if @review.save
      redirect_to @product, notice: "レビューを投稿しました。"
    else
      render "products/show", status: :unprocessable_entity
    end
  end

  # ...

  private

  def set_product
    @product = Product.find(params[:product_id])
  end

  def set_review
    @review = @product.reviews.find(params[:id])
  end

  def review_params
    params.require(:review).permit(:rating, :body, images: [])
  end
end

星評価入力(UI)とCSS

説明の中で特徴的なのが「星評価をラジオボタンで実装し、CSSで直感的な星アイコンにする」という点です。これにより、以下の利点があります。

  • アクセシビリティを確保(スクリーンリーダーやキーボード操作に対応)
  • フォーム送信時には通常の rating パラメータとして送られる
  • CSSのみで視覚的にはリッチな星評価UIを実現

フォーム例:

erb
<%= form_with model: [@product, @review] do |f| %>
  <div class="rating-input">
    <% 5.downto(1) do |i| %>
      <%= f.radio_button :rating, i, id: "review_rating_#{i}" %>
      <%= f.label :"rating_#{i}" do %>

      <% end %>
    <% end %>
  </div>

  <%= f.text_area :body %>

  <%= f.file_field :images, multiple: true %>

  <%= f.submit "レビューを投稿" %>
<% end %>

CSSで label を星形にスタイリングし、input:checked ~ label などのセレクタを使って、選択状態に応じて星の色が変わるようにする、というパターンがチュートリアルで解説されています。

商品の平均評価の自動更新

レビュー作成・更新・削除のたびに、Product 側の評価を自動で再集計する仕組みを解説しています。

  • コールバック (after_commit など)で product.update_rating! を呼び出す
  • SQL の AVG(rating) を利用して平均値を計算
  • Product モデルの rating カラムに保存しておくことで、一覧画面などで N+1 クエリを避ける

計算例:

ruby
def update_rating!
  update!(
    rating: reviews.average(:rating) || 0
  )
end

レビューの絞り込みと集計表示

商品詳細ページなどで、レビューを以下のように表示・集計する例が解説されています。

  • 指定した星数のみを表示するフィルタ (例: 「星5のみ」「星4のみ」)
    • params[:rating] をもとに @product.reviews.where(rating: params[:rating]) などで絞り込み
  • 星ごとのレビュー数・割合をバーグラフで表示
    • 例: 5〜1まで各星の件数をカウントし、全体に対する割合を計算
ruby
# Product モデル例
def rating_breakdown
  total = reviews.count.to_f
  return {} if total.zero?

  (1..5).each_with_object({}) do |star, hash|
    count = reviews.where(rating: star).count
    hash[star] = {
      count: count,
      percent: ((count / total) * 100).round
    }
  end
end

ビュー側例 (擬似コード):

erb
<% @product.rating_breakdown.sort.reverse_each do |star, data| %>
  <div class="rating-row">
    <span><%= star %>★</span>
    <div class="bar">
      <div class="bar-fill" style="width: <%= data[:percent] %>%"></div>
    </div>
    <span><%= data[:count] %>件 (<%= data[:percent] %>%)</span>
  </div>
<% end %>

画像アップロード(任意)のサポート

  • Active Storage を使った画像添付例が含まれています
  • 1レビューに複数画像を添付できるように has_many_attached :images を使用
  • ビューで image_tag を使ってプレビュー表示する例が示されます
ruby
class Review < ApplicationRecord
  has_many_attached :images
end
erb
<% @review.images.each do |image| %>
  <%= image_tag image.variant(resize_to_limit: [400, 400]) %>
<% end %>

管理画面でのレビュー管理

  • 管理者がレビューを編集・削除できるようにするための UI / コントローラが説明されています
  • 主な用途: 誤字修正、不適切表現の修正、スパム的レビューの削除など
  • 実装例:
    • namespace :admin 配下に Admin::ReviewsController を作成
    • または既存の管理UI(例: Admin::ProductsController)にネストして操作する

ルート例:

ruby
namespace :admin do
  resources :products do
    resources :reviews, only: [:index, :edit, :update, :destroy]
  end
end

権限管理の考慮(認証・認可)が軽く触れられている可能性があります(before_action :require_admin! など)。


2-2. guides/source/wishlists.md の微修正

wishlists.md は既存のECストアチュートリアルの別パートで、ここに対して:

  • 他パート(今回追加された product_reviews.md など)へのリンクの修正・追加
  • あるいは構成の一貫性を保つための軽微な文章・見出しの変更

が行われています(+1/-8 行なので、ごく小さな調整に留まっています)。


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

    • Rails本体のコード・APIには影響せず、公式ガイド(ドキュメント)のみの変更です。
    • ただし「Rails Way」でのECサイトにおけるレビュー機能の実装パターンとして、今後このチュートリアルが事実上の「推奨例」になる可能性があります。
    • 特に以下の設計方針が、Railsコミュニティにおけるデファクトな例として参照されやすくなります:
      • 星評価をラジオボタン + CSS で実装するアプローチ
      • モデルのコールバック(orサービスオブジェクト)で平均評価を自動更新するアプローチ
      • Active Storage を利用したレビュー画像添付
  • 注意点 / 実装上のポイント

    • パフォーマンス:
      • 大量レビューがある商品の平均値再計算は、reviews.average(:rating) を毎回呼び出すと高コストになる可能性があります。
        • チュートリアルではシンプルな実装を重視しているため、実運用ではカウンタキャッシュ的な集計(reviews_count, rating_sum など)で最適化を検討するとよいです。
    • 整合性:
      • レビュー削除時にも平均値の更新が走るように、after_commit などのコールバックの種類やタイミングには注意が必要です。
    • セキュリティ:
      • 画像アップロードでは、ファイルサイズ・拡張子・Content-Typeの制限や、サムネイル生成時のImageMagick/ libvips 周りの安全性確保など、実運用では追加対策が必要になります。
    • 権限/認可:
      • 一般ユーザーが自分のレビューだけを編集・削除できるようにするのか、管理者のみ許可するのか、チュートリアルは1つの例に過ぎないため、自プロジェクトではポリシーを明確にする必要があります。

  1. 参考情報 (あれば)
  • Rails Guides (今後このチュートリアルが掲載されるであろう場所):
    https://guides.rubyonrails.org/
    Product Reviews もしくは E-Commerce 関連のチュートリアルセクションに追加される想定)

  • Active Storage ガイド(画像アップロードの詳細):
    https://edgeguides.rubyonrails.org/active_storage_overview.html

  • Rails公式チュートリアル内の他のEC機能 (カート、ウィッシュリストなど) と合わせて読むと、フル機能のECサイト構築フローとして参考になります。


#57826 Deprecate Mime::SET, Mime::LOOKUP and Mime::EXTENSION_LOOKUP

マージ日: 2026/7/2 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    Rails の MIME タイプ周りの内部レジストリを直接公開していた Mime::SET, Mime::LOOKUP, Mime::EXTENSION_LOOKUP が非推奨化され、代わりに既存の公開 API(Mime.symbols, Mime[...], Mime::Type.lookup, Mime::Type.lookup_by_extension など)と新設の Mime.extensions を使うように誘導する変更です。
    これにより、ミュータブルな内部状態への直接アクセスを抑制しつつ、これまで内部定数でしかできなかった操作も正式なパブリック API でカバーされるようになりました。

  1. 変更内容の詳細

非推奨になった定数

以下の 3 つの定数が非推奨になり、ActiveSupport::Deprecation::DeprecatedObjectProxy でラップされるようになりました。

  • Mime::SET
    • MIME タイプのレジストリ(シンボルなど)を保持していたセット。
  • Mime::LOOKUP
    • 文字列表現(例: "text/html")から MIME タイプを引くためのハッシュ。
  • Mime::EXTENSION_LOOKUP
    • 拡張子(例: "html")から MIME タイプを引くためのハッシュ。

これらはまだ参照可能ですが、使用すると deprecation warning が出るようになっています。

推奨される置き換え API

PR 説明にある通り、用途ごとに次のように書き換えが推奨されます。

1) Mime::SET

用途: 登録済みの MIME タイプを列挙・取得したい場合

置き換え:

  • 登録済みの MIME シンボル一覧が欲しいとき
    ruby
    # 旧
    Mime::SET # 非推奨
    
    # 新
    Mime.symbols
  • MIME タイプをキー(シンボルや文字列)から取得したいとき
    ruby
    # 旧
    type = Mime::SET[:html] # など
    
    # 新(推奨)
    type = Mime[:html]
    # または
    type = Mime::Type.lookup("text/html")

2) Mime::LOOKUP

用途: "text/html" のような MIME 文字列から Mime::Type を取得

置き換え:

ruby
# 旧
Mime::LOOKUP["text/html"] # 非推奨

# 新
Mime::Type.lookup("text/html")

3) Mime::EXTENSION_LOOKUP

用途: ファイル拡張子 "html" から MIME タイプを取得したり、全拡張子を列挙したり

置き換え:

  • 拡張子から MIME タイプを取得

    ruby
    # 旧
    Mime::EXTENSION_LOOKUP["html"] # 非推奨
    
    # 新
    Mime::Type.lookup_by_extension("html")
    # または
    Mime[:html] # 既存のショートカット
  • 登録済み拡張子を列挙したい(従来は Mime::EXTENSION_LOOKUP でしかできなかった)
    今回、新しく Mime.extensions が追加されています。

    ruby
    Mime.extensions
    # => [:html, :text, :json, ...] (全ての拡張子・シノニムを含む)

これにより、「レジストリ(Hash/Set)自体」に触るのではなく、メソッド経由で取得・列挙するスタイルに統一されます。

コア実装の変更点(概要)

actionpack/lib/action_dispatch/http/mime_type.rb の変更が中心で、以下のような対応がされています。

  • Mime::SET, Mime::LOOKUP, Mime::EXTENSION_LOOKUP を内部実体に対する DeprecatedObjectProxy に差し替え
    • 実行時には従来通り動作するが、アクセスごとに deprecation warning を発生させる。
  • Mime.extensions の追加
    • Mime.symbols 同様の公開メソッドとして、拡張子の一覧取得をサポート。
  • 既存内部コードで、これら定数を直接使っていた箇所を公開 API ベースの呼び出しにリファクタリング
    • action_mailer.rb, action_dispatch.rb, abstract_controller/collector.rb など。

テストの更新

  • actionpack/test/dispatch/mime_type_test.rb などで:
    • 新メソッド Mime.extensions の挙動テストが追加。
    • 旧定数使用時に deprecation が発生することの確認テストなどが追加または修正。
  • actionview/test/template/lookup_context_test.rb など内部で MIME レジストリを使うテストも、新 API に合わせて微修正。

  1. 影響範囲・注意点

影響を受けるコード

以下に該当するアプリケーション/ライブラリは deprecation warning の対象になります。

  • Mime::SET / Mime::LOOKUP / Mime::EXTENSION_LOOKUP を直接参照しているコード
    • 例:
      ruby
      Mime::SET.each do |type|
        # ...
      end
      
      if Mime::EXTENSION_LOOKUP["csv"]
        # ...
      end

これらは Rails の内部実装詳細(ミュータブルなレジストリそのもの)に依存しているため、今後のメジャーバージョンで削除される可能性が高いです。

マイグレーションの指針

  • 列挙用途
    • MIME シンボル一覧: Mime.symbols
    • 拡張子一覧: Mime.extensions
  • ルックアップ用途
    • MIME 文字列 "text/html"Mime::Type.lookup("text/html")
    • 拡張子 "html"Mime::Type.lookup_by_extension("html") または Mime[:html]
  • レジストリを直接変更している場合(かなりレアケース)
    • 例えば Mime::EXTENSION_LOOKUP["pdf"] = ... のように書いている場合は、元々非推奨なパターンであり、公式な登録 API(Mime::Type.register など)または提供されている拡張メソッド経由に置き換えるべきです。

注意点

  • 今はまだ動きますが、DeprecatedObjectProxy 経由の使用は将来の削除を前提とした一時措置です。
    deprecation メッセージが CI などで検出される場合は、早めの対応が必要です。
  • 特に gem やエンジンなどのライブラリは、利用者のアプリケーションで deprecation noise を出さないよう、できるだけ早期に新 API へ移行しておくとよいです。

  1. 参考情報 (あれば)
  • 置き換え API 一覧:
    • Mime::SET
      • Mime.symbols, Mime[...], Mime::Type.lookup
    • Mime::LOOKUP
      • Mime::Type.lookup
    • Mime::EXTENSION_LOOKUP
      • Mime.extensions, Mime::Type.lookup_by_extension, Mime[...]
  • 関連ファイル:
    • actionpack/lib/action_dispatch/http/mime_type.rb(MIME 周りの中核実装)
    • actionpack/CHANGELOG.md(非推奨化の公式なアナウンスが追記済み)

#57917 Attribute methods ractor safe

マージ日: 2026/7/2 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    Rails の ActiveModel::AttributeMethods 周りで使われる aliases_by_attribute_name / attribute_aliases / attribute_method_patterns を、Ractor 間で安全に共有できるよう「デフォルトで freeze される値オブジェクト&コレクション」に変更した PRです。これにより、Ractor 環境でも属性メソッド関連の情報を安心して参照できるようになり、並行実行時のデータ競合や Ractor 制限によるエラーを防ぎます。

  1. 変更内容の詳細

2-1. 目的となる3つのメソッドの Ractor-safe 化

Ractor 間で共有するには、「凍結されていて、かつ Ractor-shareable なオブジェクト」である必要があります。この PR では以下をその条件に合わせるための変更が行われています。

  • AttributeMethods.aliases_by_attribute_name
  • AttributeMethods.attribute_aliases
  • AttributeMethods.attribute_method_patterns

方針としては:

  • 値オブジェクトおよび配列・ハッシュを「生成時に freeze」する
  • 「必要になった時に書き換える(遅延・破壊的変更)」スタイルを避ける
  • 継承時に子クラスへ「凍結済みのコピー/設定」を行っておく

という構造になっています。

2-2. AttributeMethodPattern の値オブジェクトをデフォルトで frozen に

AttributeMethodPattern は、属性メソッド (before_type_cast, ?, = など) を表現するための内部的な値オブジェクトです。

この PR での変更点:

  • AttributeMethodPattern のインスタンスを生成時に freeze するように変更
  • これにより、attribute_method_patterns が保持するパターンの配列全体が Ractor 間で共有可能になる

イメージとしては:

ruby
pattern = AttributeMethodPattern.new(prefix: "", suffix: "=")
pattern.frozen? # => true

のように、パターン自体に後から変更を加えない前提をコードで保証するようにしています。

2-3. aliases_by_attribute_name を「継承時に」初期化するよう変更

従来は、aliases_by_attribute_name は「最初にアクセスされたときに default_proc を使って遅延生成・更新」していた箇所がありました。これは「ハッシュにアクセスした瞬間に中身を書き換える」ため、Ractor から見ると共有が難しくなります。

この PR では:

  • aliases_by_attribute_nameクラス継承時に確定させる 形に変更
    → サブクラス用のハッシュを、親クラスからコピー・設定するタイミングを明示的に
  • default_proc 内でハッシュを書き換えるのをやめ、「必要なキーが無い場合は、凍結された空配列などを返すだけ」に変更

イメージ:

ruby
# 旧: キー未定義時に default_proc 内でハッシュを更新していた
hash.default_proc = ->(h, k) { h[k] = [] }

# 新: ハッシュ自体は固定し、見つからないキーのときは
#     単に空の frozen 配列を返すだけにする
hash.default_proc = ->(_h, _k) { EMPTY_FROZEN_ARRAY }

こうすることで、aliases_by_attribute_name 自体は「それ以上変更されないハッシュ」となり、Ractor 間で共有しても安全になります。

2-4. 「欠けているキー」に対しては凍結された配列を返す

aliases_by_attribute_name[attribute_name] が存在しない場合:

  • 以前は default_proc 内で hash[attribute_name] = [] のように書き換えつつ空配列を返していた
  • 現在は、ハッシュを変更せず、単に「中身の無い・凍結された配列」を返す

これにより:

  • 「読み取りのみ」のパターンになる
  • 返される配列も frozen なので、Ractor 間で共有しても破壊的変更ができない設計になる

2-5. attribute_aliases / attribute_method_patterns の扱い

attribute_aliasesattribute_method_patterns も、本質的には「定義時に決まって、その後あまり変えたくない情報」です。

この PR により:

  • それぞれの戻り値となるハッシュや配列がデフォルトで freeze されるよう整理
  • AttributeMethodPattern 自体も frozen なので)attribute_method_patterns 全体が Ractor-safe になる

Rails アプリ側から見ると、これらに対して「返ってきた配列/ハッシュを破壊的に変更する」コードは元々非推奨に近い使い方ですが、この変更によって 実際に破壊的操作ができなくなる(=例外になる) ため、Ractor 対応と同時に API の利用スタイルもより明示的になります。


  1. 影響範囲・注意点

3-1. 影響を受ける主なコード

  • ActiveModel::AttributeMethods を継承して独自の attribute メタプログラミングをしているクラス
  • attribute_method_patterns, attribute_aliases, aliases_by_attribute_name を直接触っているメタプログラミング系コードや gem

特に注意が必要なのは次のようなコードです。

NG になりうるパターン例

ruby
# attribute_aliases に破壊的変更を加える
MyModel.attribute_aliases[:foo] = :bar

# or
MyModel.attribute_method_patterns << some_pattern

これらは返り値が frozen になることで FrozenError になる可能性があります。

3-2. 対処法の方針例

返ってきたオブジェクトを「そのまま破壊的に変更する」代わりに:

  • dup / to_h / to_a してから新しいオブジェクトを作成し、
  • 専用の設定メソッドや API を通じて反映する

といった形が推奨されます。

例:

ruby
# 旧: 直接破壊的に変更
MyModel.attribute_aliases[:foo] = :bar

# 新: 専用 API か、初期定義時にまとめて設定する
MyModel.attribute_alias(:foo, :bar)

attribute_method_patterns も基本的には「クラス定義時にパターンを追加する」ための public API 経由で触るべきで、内部の配列を直接いじるスタイルは避けるとよいです。

3-3. Ractor を使わない場合でも影響はあるか

  • Ractor を使わないアプリでも、「内部構造が freeze されるようになった」という意味で影響があります
  • ただし、通常の利用(public API 経由で attribute alias やメソッドパターンを定義する)であれば実害はなく、テストも追加されているため後方互換性は基本的に保たれる想定です

  1. 参考情報 (あれば)
  • PR 本体: https://github.com/rails/rails/pull/57917
  • Ractor の shareable オブジェクト要件(Ruby 3+)
    • freeze されていること
    • 参照グラフ全体が shareable であること
  • Rails 内の関連コード
    • ActiveModel::AttributeMethods(属性メソッド定義のベース)
    • ActiveRecord::Base はこれを継承しているため、全 AR モデルに波及する仕組み

この PR により、 attribute 周りの内部メタデータ構造が「読み取り専用」設計に近づき、Ractor による並行実行が現実的に扱いやすくなっています。


#57757 Make InheritableOptions ractor safe

マージ日: 2026/7/2 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    Rails の InheritableOptionsActiveSupport::OrderedOptions 系)の実装を見直し、freeze 済みで Ractor 共有可能(Ractor-safe)にできるようにした PRです。親ハッシュへのフォールバックを行うデフォルト Proc を凍結時に除去し、Ractor の shareable 制約に抵触しない仕組みに変更しています。

  1. 変更内容の詳細

背景: なぜ Ractor-safe ではなかったか

InheritableOptions は、親オプションを継承しつつ子側で上書きできる仕組みで、内部的には「キーが見つからない時に親ハッシュを見る」ためのデフォルト Proc を持っています。

イメージとしては以下のような構造です:

ruby
class InheritableOptions < ActiveSupport::OrderedOptions
  def initialize(parent = nil)
    @parent = parent
    default_proc = ->(hash, key) { @parent[key] if @parent && @parent.key?(key) }
    super(&default_proc)
  end
end
  • この default_proc はインスタンス変数 @parent をキャプチャするため、
    • Proc 自体が Ractor 的に「shareable」になりにくい
    • かつ、その Proc を持つオプションオブジェクトも shareable にできない
  • Ractor.make_shareable は深くオブジェクトグラフを辿るので、「freeze したら勝手に @parent まで shareable 化する」のは副作用が大きく、freeze の責務としても不適切

そのため、「freeze 済み InheritableOptions を Ractor に渡す」ということが安全にできない状態でした。

方針: freeze 時に親参照を解消する

この PR では、

「凍結前は親を参照するが、凍結するときに親からのフォールバックを解消し、単純なハッシュとして完結させる」

という方針を取っています。

具体的には:

  1. InheritableOptions#freeze が呼ばれた時に
    • 親ハッシュの内容を「reverse merge」の形で自分自身に取り込む
      • =「自分に値がなければ親の値をコピーする」
  2. そのうえで default_proc を捨てる(親を参照しないようにする)
  3. 結果として、凍結後は「見た目は同じ値を返すが、親にアクセスしない純粋な hash-like オブジェクト」になる

擬似コードで表現すると次のようなイメージです(実装は ordered_options.rb 内):

ruby
def freeze
  unless frozen?
    if @parent
      # 親の値を自分自身に reverse_merge しておく
      @parent.each do |k, v|
        self[k] = v unless key?(k)
      end
    end

    # 親を参照する default_proc を除去
    self.default_proc = nil

    super
  end
end

※実際のコードでは OrderedOptions / InheritableOptions の構造に合わせた実装になっています。

overridden? の実装変更

InheritableOptions には概ね次のようなメソッドが存在します:

ruby
options.overridden?(:foo) # 親から継承したのではなく、子で明示的に設定されているか?

従来は「子にキーが存在するかどうか」で判定できていましたが、この PR 後は凍結時に親のキーも子へコピーされるため、そのままだと「すべて overridden 扱い」になってしまいます。

そこで、この PR では:

  • 「どのキーが子で明示的に設定されたか」を内部で追跡するように変更
    • 例: @overridden_keys のようなセットに、値を代入したタイミングでキーを記録
  • overridden?(key) は「@overridden_keys にそのキーが含まれているか」で判定するように変更

これにより、

  • 凍結前後で overridden? の意味・結果を変えずに維持
  • かつ凍結後は「親キーが子へコピーされても、それだけでは overridden と判定されない」

という動作にできます。

テスト追加

activesupport/test/ordered_options_test.rb にテストが追加されており、主に以下を検証しています:

  • Ractor-safe 化された InheritableOptions がこれまでどおりの値解決を行うこと
  • freeze 前後で overridden? の結果が変わらないこと
  • 親オプション・子オプションの関係が、凍結後も期待通りに維持されること(挙動レベルで)

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

    • ActiveSupport::InheritableOptions を直接/間接的に利用しているコード全般
      • 特にエンジンや Railtie の設定オプションなど、「親設定を継承して子で上書きする」パターン
    • overridden? を利用しているコード(Gem/アプリのメタプログラミング的な部分など)
  • 互換性

    • 通常ユースケースでは、凍結前後で返却される値は従来と一致するよう配慮されているため、挙動的な非互換は最小限
    • overridden? が「内部実装として、単に key? か否かに依存している」と仮定していたような非常に内部寄りのコードがあれば、影響を受ける可能性はありますが、そのようなコードは稀と考えられます。
  • Ractor を使う場合の注意

    • この PR により、「freeze 済み InheritableOptions を Ractor に渡す」パターンが実現しやすくなります。
    • ただし、親オプション自体や、親にぶら下がる値オブジェクトが Ractor-safe かどうかは別問題であり、アプリ側で引き続き注意が必要です。
    • freeze はあくまで「親からのフォールバックを解消し Proc を除去する」だけであり、Ractor.make_shareable を内包してはいません。そのため、凍結=完全 shareable という保証ではない点には留意してください。

  1. 参考情報 (あれば)
  • PR 本文の論点:
    • Ractor の shareable 要件と default_proc(Proc のクロージャ)が持つ参照の問題
    • freeze で勝手に Ractor.make_shareable を呼ぶことの是非(副作用の大きさと API としての責務不一致)
  • 関連するクラス/概念:
    • ActiveSupport::OrderedOptions
    • ActiveSupport::InheritableOptions
    • Ractor / Ractor.make_shareable
  • 実際に Rails アプリを Ractor-safe にする場合は、他のコンポーネント(例えば logger、キャッシュストア、クラス変数に保持している設定値など)も含めて「Proc やミューテーブルなオブジェクトがどこに残るか」を総合的に見ていく必要があります。

#57953 Mark a connection unverified when a non-StandardError interrupts a query

マージ日: 2026/7/2 | 作成者: @edaroit

  1. 概要 (1-2文で)
  • Active Record の DB コネクションプールで、StandardError 以外の例外(Timeout や fiber scheduler の Async::Stop / Async::Cancel など)でクエリが中断された場合にも、そのコネクションを「未検証状態」にマークするようにし、後続リクエストに壊れたソケットが再利用されないようにする修正です。
  • 中断されたクエリ自体の挙動(例外がそのまま飛ぶ)は変えず、「プールに戻されたコネクションの信頼性」だけを正しく扱うことで、EOFError などが後続の無関係なリクエストで発生するバグを防ぎます。

  1. 変更内容の詳細

背景となる問題

ConnectionAdapters::AbstractAdapter#with_raw_connection は内部で DB に対して「生の」クエリ実行を行い、ここで例外が起きたときにコネクション状態を調整しています。

従来の挙動:

ruby
def with_raw_connection(...)
  # 省略
rescue => e # == rescue StandardError
  downgrade_connection_after_error(e)
  raise
end
  • rescue => e は Ruby の仕様上 StandardError だけを捕捉します。
  • Timeout や、多くの async/fiber ランタイム (Async::Stop, Async::Cancel) は Exception の直下 or 途中のサブクラスであり、StandardError の外側です。
  • そのため、クエリ実行中にそうした例外で中断されると:
    • 例外は with_raw_connectionrescue をすり抜けて外側へ伝播
    • しかし、そのタイミングでコネクションオブジェクトは「正常・検証済み」としてプールに戻されてしまう
    • 次にプールからこのコネクションを checkout したリクエストが、破綻したソケットを使おうとして EOFErrorActiveRecord::ConnectionFailed を受ける
      → 一つのリクエストの中断が、後続の無関係なリクエストを壊す

特に Falcon など fiber scheduler ベースのサーバでは、キャンセルを fiber 内に Async::Stop として直接投げ込むため、このパスに頻繁に乗りやすくなります。

追加された処理

このギャップを塞ぐため、with_raw_connectionrescue Exception を追加し、「StandardError 外の例外でもコネクションを“ダウングレード”」するようにしました。

追加コード(概略):

ruby
def with_raw_connection(...)
  # 通常の処理

rescue => e # StandardError 系
  downgrade_connection_after_error(e)
  raise

rescue Exception
  @last_activity = nil
  @verified = false
  raise
end

ここで重要なのは:

  • @verified = false
  • @last_activity = nil

の2つの書き込みを行うことです。

なぜ @verified = false だけでは足りないのか

checkout 時の再検証ロジックはおおよそ次のようになっています:

ruby
elsif !@needs_reconnect && (last_activity = seconds_since_last_activity) && last_activity < verify_timeout
  # 直近で使われているなら、検証をスキップしてよし、とみなす

この分岐は「@needs_reconnect が false で、かつ最近使われた(last_activity が小さい)」場合に、検証(verify!)をスキップします。

  • @verified = false だけ変えても、
    • 「最近使われた」扱いだとこのショートカットに乗ってしまい、
    • 実際には壊れているソケットを再検証せずに再利用してしまう可能性があります。
  • そこで @last_activity = nil を一緒にクリアすることで、
    • 「最近使われた」ショートカットに入らないようにし、
    • 次の利用時に必ず検証経路に入り、そこで死んだソケットなら再接続されるようにしている。

なぜ @needs_reconnect は触らないのか

downgrade_connection_after_error は、エラーを「再試行可能な接続エラー」と判定できる場合にだけ @needs_reconnect = true を設定します。

  • 今回対象の Exception 系の割り込みは、
    • DB ドライバのエラー分類の範囲外であり、
    • 「接続エラーである」と決め打ちできる根拠がない。
  • ただし @verified / @last_activity をクリアすれば、次の利用時に verify が走り、そこで本当に再接続が必要なら @needs_reconnect が立つ or 実際に reconnect が行われます。

そのため、このパスでは「“とりあえずこのコネクションは信用しない” という最低限のフラグリセット」に留めています。

rescue Exception への懸念と既存の前例

rescue Exception は一般にアンチパターンとされますが、このファイルでは既に類似の用途に使われており、今回の変更もそれに倣っています。

  • attempt_configure_connection では Timeout::ExitExceptionException 派生)を補足するために rescue Exception を使用済み。
  • 今回も「アプリケーションコードの例外を飲み込む」のではなく、
    • 捕捉したら状態だけ直して raise し直す、という「透明な」挙動です。
    • 例外の型やスタックトレースは変えません。

テストの追加

activerecord/test/cases/adapter_test.rb に以下のようなテストが追加されています(概要):

  • あるアダプタに対して:
    1. まず普通のクエリを投げてコネクションを「使用済み・verified な状態」にする。
    2. 次に with_raw_connection の中で StandardError 外の例外を発生させる。
    3. その後のコネクション状態として:
      • @verified が false になっていること
      • @last_activity が nil になっていること をアサート。

このテストは、修正がないと失敗し(コネクションが verified のまま)、修正後は通ることが確認されています。sqlite3 と trilogy で検証済みです。


  1. 影響範囲・注意点
  • 対象:
    • Active Record 全般の DB 接続処理。
    • 特に fiber scheduler(Falcon, async-http 等)や、Timeout をクエリ実行中に使う環境で恩恵が大きいです。
  • 期待される改善:
    • StandardError の例外でクエリが中断された後に、別のリクエストで突然 EOFErrorActiveRecord::ConnectionFailed が起きる、といった「後続リクエストへの汚染」が起きにくくなります。
    • プール内のコネクションの「整合性」が高まり、障害の局所化がしやすくなります。
  • 互換性・副作用:
    • 中断されたリクエスト側の例外の種類・伝播のされ方は変わりません(あくまでコネクション状態のフラグを書き換えるだけ)。
    • StandardError を故意にクエリ中断に使っている場合、次回そのコネクション利用時に verify が必ず走るようになるため、ごく軽微ですが再接続や追加の round-trip が増える可能性があります。
    • rescue Exception を追加していますが、即座に例外を再送出しており swallowing はしていません。挙動面の変化は「コネクションが unverified になるかどうか」だけです。

  1. 参考情報 (あれば)

#57957 Bump Github Action cache version to 6

マージ日: 2026/7/1 | 作成者: @akhilgkrishnan

  1. 概要 (1-2文で)
    Rails が生成する GitHub Actions ワークフロー(CI 用)の「キャッシュバージョン」を 6 に更新した PRです。アプリケーション/プラグイン用 generator が出力する .github/workflows/ci.yml のテンプレートのみが対象です。

  1. 変更内容の詳細

対象ファイル:

  • railties/lib/rails/generators/rails/app/templates/github/ci.yml.tt
  • railties/lib/rails/generators/rails/plugin/templates/github/ci.yml.tt

これらは rails newrails plugin new 実行時に .github/workflows/ci.yml として生成されるテンプレートです。
今回の変更は、GitHub Actions のキャッシュキーに使っている「内部バージョン番号」を 5 → 6 に bump しただけの非常に小さな変更です。

イメージとしては、テンプレート中に以下のような行があるとします(実際の PR でもこれに近い形です):

yaml
- uses: actions/cache@v4
  with:
    path: vendor/bundle
    key: ${{ runner.os }}-gems-v6-${{ hashFiles('**/Gemfile.lock') }}
    restore-keys: |
      ${{ runner.os }}-gems-v6-

この v6 の部分が以前は v5 だったものを更新しています。

この「キャッシュバージョン」は、キャッシュの破棄や再生成のタイミングをコントロールするための任意のラベルで、

  • キャッシュスキーマを変えた
  • 中に入れるパス構成を変えた
  • bundle の扱いを変えた
    といったときに bump する、という運用がよくされています。

PR 説明にある followup: https://github.com/rails/rails/pull/56772 で行われた CI テンプレート関連変更(例: Bundler 周りや Ruby セットアップ周りの見直し)に続く整備として、キャッシュのバージョンだけを更新したものと考えられます。


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

    • これから rails new / rails plugin new で作成されるプロジェクト・プラグインのうち、--skip-github せず GitHub CI を生成するケースにのみ影響します。
    • 既存プロジェクトのワークフローは自動では書き換わらないため、この PR だけでは既存リポジトリには影響しません。
  • 効果

    • 新しく生成された .github/workflows/ci.yml は「バージョン 6」のキャッシュを使うので、過去に「バージョン 5」などのキーで作られていたキャッシュとは衝突せず、新しい条件でクリーンなキャッシュを作り直せるようになります。
    • Bundle パスや Ruby セットアップの変更に不整合がある古いキャッシュを誤って使い続けるリスクを減らします。
  • 注意点 / 既存プロジェクトで追従したい場合

    • Rails 7.2 以降の CI テンプレートに合わせたい場合は、既存の .github/workflows/ci.yml で、同様にキャッシュのキー中のバージョン(v5 など)を v6 に手動で変更することが推奨されます。
    • 変更後の最初のワークフロー実行では、新しいキーでキャッシュが作り直されるため、1 回目だけは依存関係のインストールにやや時間がかかる可能性があります。

  1. 参考情報 (あれば)
  • 対応する前 PR(テンプレートや CI 設定の調整などが含まれる可能性あり)
  • Rails の GitHub CI テンプレートは railties の generator で管理されており、ci.yml.ttrails new 時に展開されます。
    • app: railties/lib/rails/generators/rails/app/templates/github/ci.yml.tt
    • plugin: railties/lib/rails/generators/rails/plugin/templates/github/ci.yml.tt

#57939 Apply source mappings to dynamic permissions policy sources

マージ日: 2026/7/1 | 作成者: @55728

  1. 概要 (1-2文で)
    Permissions-Policy で「動的(callable)なソース指定」にキーワード(:self など)を返した場合も、静的指定と同様にマッピング('self' などへの変換)が適用されるようになりました。これにより、これまで動的ソースだけ不正なポリシー文字列(self のような裸の単語)が出力されていた問題が修正されています。

  1. 変更内容の詳細

何が問題だったか

Permissions-Policy(旧 Feature-Policy)の設定で、ブロック(→ Proc)などを使って「動的に」ソースを返すケースがあります。

ruby
policy.geolocation -> { :self }

このとき、:self のような「キーワードシンボル」を返しても、

  • 静的指定: policy.geolocation :self
    geolocation 'self'

  • 動的指定: policy.geolocation -> { :self }
    → 以前は geolocation self(クォートなし)になっていた

つまり、動的な場合だけ :self などのキーワードに対するマッピング処理が走らず、「素の self」が出力されてしまい、Permissions-Policy の文法上は不正なヘッダになっていました。

今回の修正内容

action_dispatch/http/permissions_policy.rb 内で、動的ソースの評価結果にも「ソースマッピング(:self'self' など)」を適用するように変更されています。

  • 静的ソースと同じ経路でマッピングされるようになり、CSP(Content Security Policy)の実装と挙動が揃えられました。
  • テスト (actionpack/test/dispatch/permissions_policy_test.rb) が追加され、動的ソースに対しても 'self' のように適切にクォートされた値が出力されることを検証しています。

挙動の具体例

修正前:

ruby
policy.geolocation :self
# => geolocation 'self'

policy.geolocation -> { :self }
# => geolocation self   # クォートなしで不正

修正後:

ruby
policy.geolocation :self
# => geolocation 'self'

policy.geolocation -> { :self }
# => geolocation 'self' # 静的と同じマッピングが適用される

他のキーワード(例: :none など)についても同様に、静的指定時と同じルールでマッピング・整形されます。


  1. 影響範囲・注意点
  • 影響を受けるのは、Permissions-Policy で ブロック / Proc などの動的ソース指定を使い、かつ :self などのキーワードシンボルを返している場合 です。
  • これまではそのようなコードが不正なヘッダ(self のようにクォートされていない単語を含む)を生成していた可能性があり、ブラウザによってはヘッダ全体が無視されていた可能性があります。
  • この修正によりヘッダが正しく解釈されるようになり、以前は実質無効化されていた Permissions-Policy が今後は有効に適用されるようになることがあります。その結果、既存アプリで以下のような挙動変化が起こり得ます:
    • geolocation, camera, microphone などの利用が、想定通りに制限され始める
    • 開発・テスト環境では問題なかったが、本番でのみ Permissions-Policy を緩く解釈していたブラウザが、修正後は厳格に解釈するように見える場合がある

確認ポイント:

  • config.action_dispatch.permissions_policy などで、Proc/ラムダを使ってソースを返している箇所を確認する:
    • -> { :self } / -> { :none } / -> { :src } などキーワードを返していないか
    • 返り値が従来から想定していた文字列表現('self', 'none', 'src' など)になることを前提に書かれていないか
  • ブラウザの DevTools で実際に送信されている Permissions-Policy / Permissions-Policy-Report-Only ヘッダを確認し、値が 'self' など適切にクォートされていること、およびポリシーが想定通りに働いていることをチェックする。

  1. 参考情報 (あれば)
  • この修正は、Content Security Policy における source mapping の挙動と整合を取る意図が明示されています。CSP での :self'self' のような扱いに慣れている場合、そのまま同じ感覚で Permissions-Policy の動的ソースも書けるようになります。
  • 実際のコード差分は小さく(+16/-1)、PermissionsPolicy クラス内のソース変換ロジックに対する一点修正と、それを保証するテスト追加が中心です。

#57938 Allow a trailing slash URL with a blank path to take query params

マージ日: 2026/7/1 | 作成者: @55728

  1. 概要 (1-2文で)
    url_fortrailing_slash: true かつ path: ""(空文字)を指定した場合に、クエリパラメータやアンカーを付与すると FrozenError が発生していた不具合を修正した PR です。
    これにより、"/" をベースに "/?a=1" のように正常にパラメータやアンカーを付与できるようになりました。

  1. 変更内容の詳細

問題の挙動

従来、以下のようなコードを実行すると:

ruby
url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 })
  • ベースのパスは "path: "" + trailing_slash: true" により "/" になる
  • その後、この "/" 文字列に対してクエリパラメータ "?a=1" を「破壊的に」連結しようとする
  • しかし、内部的に使っている文字列が freeze されたものだったため、破壊的変更ができず FrozenError: can't modify frozen String: "/" が発生

という状態になっていました。

修正内容

変更ファイル:

  • actionpack/lib/action_dispatch/http/url.rb (+1/-1)
  • actionpack/test/dispatch/request_test.rb (+6)

url_for 内部の URL 生成ロジック(ActionDispatch::Http::URL 内)で:

  • trailing_slash: true かつ path が空のケースで使われるパス文字列に対して
    クエリパラメータやアンカーを付与する際に凍結された文字列を直接破壊的変更しないように修正

実装上は、凍結された文字列をそのまま << などで変更するのではなく、
必要に応じて複製(dup)した上でクエリやアンカーを連結する形に変えたと考えられます(+1/-1 なので単純な修正)。

サンプルコード

修正前

ruby
url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 })
# => FrozenError: can't modify frozen String: "/"

修正後

ruby
url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 })
# => "/?a=1"

url_for(only_path: true, trailing_slash: true, path: "", anchor: "top")
# => "/#top"

url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 }, anchor: "top")
# => "/?a=1#top"

テスト (actionpack/test/dispatch/request_test.rb) では、
上記のようなケースが追加されているはずで、FrozenError が出ないことと、
期待する文字列が得られることを確認しています。


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

    • url_for(およびそれをラップしている *_url / *_path ヘルパなど)で
      • trailing_slash: true
      • path を空文字列("")または実質的にブランクと解釈されるケース
      • クエリパラメータ (params) またはアンカー (anchor) を付与
        という組み合わせを利用しているコードが対象になります。
    • これまではこの組み合わせが事実上「使用不能」だった(例外が出ていた)ため、
      既存アプリケーションがこの挙動に依存している可能性は極めて低く、
      変更は基本的に後方互換的で安全と考えられます。
  • 注意点

    • 今回の修正により、"/" をベースとするパスでもクエリ・アンカーを問題なく付与できるようになるので、
      「ルートパスに対してパラメータを付けたい」ケースで、path: "" + trailing_slash: true
      安心して使えるようになります。
    • 内部実装で文字列の dup などが行われている場合、微小なオブジェクト生成コストは増えますが、
      実用上は無視できるレベルと考えてよいです。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57938
  • 該当コード周辺: ActionDispatch::Http::URLactionpack/lib/action_dispatch/http/url.rb
  • 関連するヘルパー: url_for, ルーティングヘルパー(root_path, root_url など)

#57951 Improve usage of Ractor.shareable_lambda

マージ日: 2026/7/1 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    Action Mailbox 内で Ractor.shareable_lambda の使い方を、&-> を噛ませる形からブロックを直接渡す形に書き換えた、スタイル/可読性改善のための小さなリファクタリングです。挙動変更や機能追加はなく、既存の動作を保ったまま Ruby らしい書き方に揃えています。

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

PRのモチベーションとして、以下のような「悪い例」が既にコードベースに入り込んでしまっていたので、今後は推奨しない書き方に揃えたい、という説明がなされています。

ruby
# bad
shareable_lambda(&->(foo) { foo })

# good
shareable_lambda { |foo| foo }

Ractor.shareable_lambda は、Ractor(並列実行)間で共有可能なラムダを生成するためのメソッドで、actionmailbox のコールバック定義部分で使用されています。

この PR では actionmailbox/lib/action_mailbox/callbacks.rb 内の Ractor.shareable_lambda 呼び出しが、次のように変更されたと考えられます(実際のコードイメージ):

ruby
# 変更前(想定)
ActionMailbox::Callbacks.some_callback = Ractor.shareable_lambda(
  &->(inbound_email) { process_inbound_email(inbound_email) }
)

# 変更後(想定)
ActionMailbox::Callbacks.some_callback = Ractor.shareable_lambda do |inbound_email|
  process_inbound_email(inbound_email)
end

技術的な意味としては:

  • &->(foo) { foo } は「ラムダを作ってから & でブロックに変換して渡す」二段構え
  • shareable_lambda { |foo| foo } は「最初からブロックとして渡す」シンプルな形

であり、どちらも結果として shareable_lambda にブロックが渡され、そこから shareable な Proc(ラムダ)が生成される、という点では挙動は同じです。

この PR は、無駄な Proc 生成+ブロック変換をやめて、素直にブロックを渡すスタイルに統一する、というリファクタリングになります。


  1. 影響範囲・注意点
  • 機能面の影響

    • コールバックの挙動や Action Mailbox の外部 API は変わりません。
    • ラムダの引数・戻り値の扱いも変わらないため、利用側のコード修正は不要です。
  • パフォーマンス

    • &-> を経由しないため、わずかにオブジェクト生成が減る可能性はありますが、体感できるほどの差はほぼ期待できません。
    • 目的は最適化というより、スタイルと分かりやすさの改善です。
  • Ractor / スレッド安全性

    • Ractor.shareable_lambda の使い方自体は変わらないため、Ractor 間共有の安全性に関する挙動も変わりません。
  • 注意点

    • 今後同様のコードを書く際は、shareable_lambda(&-> { ... }) ではなく、ブロックを直接渡す形で統一すると、コードベースと一貫しやすくなります。
      • 例: Ractor.shareable_lambda { |mail| handle(mail) }

  1. 参考情報 (あれば)
  • Ractor.shareable_lambda について
    • CRuby の Ractor API の一つで、「Ractor 間で共有しても安全なラムダ(Proc)」を生成するためのメソッドです。
    • ブロック内で閉じ込めるオブジェクトは「共有可能 (shareable)」である必要があります(イミュータブルなオブジェクトや、.freeze 済みのオブジェクトなど)。
  • コーディングスタイル的なポイント
    • Ruby では通常のメソッド呼び出しに対し、&-> {} で Proc を渡すよりもブロックをそのまま渡す方が標準的です。
    • この PR は Rails コードベースのスタイルをそれに合わせる意図があります。

#57952 Make parameter parsers assignment ractor safe

マージ日: 2026/7/1 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    Rails のパラメータパーサー (parameter_parsers) を設定する処理が Ractor セーフになるように、代入時に渡されたハッシュを freeze するよう変更した PRです。あわせて、デフォルトパーサーの設定にパブリック API を経由せず、直接インスタンス変数に代入するようにして、不要なハッシュの複製を避けています。

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

2.1 parameters_parsers 代入時の Hashfreeze

ActionDispatch::Http::ParametersActionDispatch::Request を通して利用される)で、parameters_parsers= のようなメソッドを使ってパラメータパーサーを設定する際、渡されたハッシュをそのまま内部で変形していたため、Ractor セーフではありませんでした。

この PR では、「パラメータパーサーを代入するタイミングで、そのハッシュを freeze してからインスタンス変数に設定する」ようにしています。

イメージとしては、以下のような変更です(実際のコードは数行レベルですが、概念的なサンプル):

ruby
# 変更前(イメージ)
def parameter_parsers=(parsers)
  @parameter_parsers = parsers
  # @parameter_parsers を内部で mutate する可能性がある
end

# 変更後(イメージ)
def parameter_parsers=(parsers)
  @parameter_parsers = parsers.freeze
end

これにより、

ruby
ActionDispatch::Request.parameter_parsers = {
  Mime[:json].symbol => ->(raw) { ... }
}

のように設定すると、そのハッシュは freeze され、Rails 内部で意図せず書き換えられず、かつ Ractor から参照しても安全になります。

2.2 デフォルトパーサー設定でパブリック API を経由しない

以前のコミット(f2be45aaa8...)で、デフォルトのパラメータパーサー自体は Ractor セーフにしていたものの、そのデフォルト値を設定する際に、一度パブリックな setter を通していたため、同じハッシュの内容を二重に持つ(複製される)ような構造になっていました。

この PR では、「デフォルトパーサーを設定するときは、パブリック API (parameter_parsers= 等) を通さず、直接インスタンス変数(たとえば @parameter_parsers)に代入する」ように変更しています。
デフォルト値については Rails 側で形(キーの形式など)を保証できるため、わざわざ public API を通して整形/複製する必要がない、という判断です。

概念的には以下のイメージです:

ruby
# 変更前(イメージ)
DEFAULT_PARAMETER_PARSERS = { ... }.freeze

def initialize
  self.parameter_parsers = DEFAULT_PARAMETER_PARSERS
end

# 変更後(イメージ)
DEFAULT_PARAMETER_PARSERS = { ... }.freeze

def initialize
  @parameter_parsers = DEFAULT_PARAMETER_PARSERS
end

この変更により、

  • デフォルトハッシュの無駄な複製を避けられる
  • Ractor セーフ性を担保しやすくなる(共有オブジェクトの構造が単純になる)

という効果があります。

2.3 Proc の扱いについての考え方

PR の説明にもある通り、parameter_parsers の値として入るのは Proc-> で定義されるラムダなど)であり、これらは call されるものです。

  • Proc オブジェクトを Rails 側が勝手に Ractor.make_shareable すると、ユーザが定義したクロージャに対する制約や予期せぬエラーを招く可能性がある
  • そのため、Proc の「shareable 化」までは Rails は行わず、「ハッシュそのものを freeze する」ことで、少なくともハッシュの構造変形による非 Ractor セーフ化を防ぐ、という方針です。

Ractor 越しに parameter_parsers を共有したい場合は、ユーザ側が適切に shareable な Proc を用意する/Ractor 内で定義する必要があります。


  1. 影響範囲・注意点

  2. parameter_parsers= に渡したハッシュが freeze されるようになる

    • これまでは setter に渡したハッシュを後から変更しても動いていたコードが、今後は FrozenError になる可能性があります。

    • 例えば、以下のようなコードは動かなくなります:

      ruby
      parsers = { Mime[:json].symbol => ->(raw) { ... } }
      ActionDispatch::Request.parameter_parsers = parsers
      
      # 後から追加しようとする
      parsers[Mime[:xml].symbol] = ->(raw) { ... }  # => FrozenError
    • 対策としては、設定用のハッシュを使い回して外部から mutate しない、または、都度新しいハッシュを作って代入するようにする必要があります。

  3. Ractor を利用するアプリでの安全性向上

    • Ractor を使うマルチスレッド/マルチ Ractor 環境で parameter_parsers を共有する際に、ハッシュがミューテートされないことが保証されるため、競合状態や「shared object ではないものを Ractor 間で共有した」という類のエラーを避けやすくなります。
    • ただし前述の通り、「Proc 自体を shareable にする責任はユーザ側」にあるため、Ractor 間で共有したい場合はそこに留意が必要です。
  4. パブリック API を通さずにデフォルトを設定することによる外部 API への影響はほぼなし

    • デフォルトのパラメータパーサーは内部的な構造が少しシンプルになっただけで、挙動(JSON 等のパース結果)は変わりません。
    • ActionDispatch::Request.parameter_parsers の getter/setter といったパブリック API 自体のインターフェースは変わっていないため、通常の利用コードには影響しません。
  5. テスト・CHANGELOG について

    • この PR 単体ではテストや CHANGELOG の更新は行われていません(チェックリスト上も未チェック)。
    • 変更がかなり小さい(2行追加・2行削除)こと、および既存のテストでカバーされる範囲と判断されている可能性があります。

  1. 参考情報 (あれば)

Ractor を積極的に使う Rails アプリで、ActionDispatch::Request.parameter_parsers をカスタマイズしている場合は、設定用ハッシュが凍結されることと、Ractor 越しに共有する Proc の shareable 性を意識しておくと安全です。


#57955 Ractor safe counter cache configuration

マージ日: 2026/7/1 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    ActiveRecord のカウンタキャッシュ設定情報(_counter_cache_columnscounter_cached_association_names)をデフォルトで freeze し、Ractor(Ruby の並行オブジェクト実行モデル)でも安全に扱えるようにした PR です。これにより、Ractor 間で共有されるカウンタキャッシュ設定が不変オブジェクトとなり、スレッド/Ractor セーフティが向上します。

  1. 変更内容の詳細

何をしているか

  • ActiveRecord::CounterCache が内部的に保持している以下の情報を「初期化時に frozen にする」よう変更しています。
    • ActiveRecord::CounterCaches._counter_cache_columns
    • ActiveRecord::CounterCaches.counter_cached_association_names

これらは主に、belongs_to :foo, counter_cache: true のような関連付けに対して、「どのカラムがカウンタキャッシュとして使われているか」「どの関連がカウンタキャッシュを持つか」というメタデータを保持するための構造です。

コードレベルでのイメージ

実際の差分は小さいですが、イメージとしては次のような変更が入っています(擬似コード):

ruby
# 変更前(イメージ)
module ActiveRecord
  module CounterCaches
    def _counter_cache_columns
      @counter_cache_columns ||= {}
    end

    def counter_cached_association_names
      @counter_cached_association_names ||= Set.new
    end
  end
end

# 変更後(イメージ)
module ActiveRecord
  module CounterCaches
    def _counter_cache_columns
      @counter_cache_columns ||= {}.freeze
    end

    def counter_cached_association_names
      @counter_cached_association_names ||= Set.new.freeze
    end
  end
end

あるいは、belongs_to のビルダー側で設定を行うときに、

ruby
_counter_cache_columns[model_name] = something

のような「後から破壊的変更」をせず、
「新しい frozen ハッシュ/セットを作って差し替える」形になっている可能性があります。

いずれにせよポイントは:

  • 内部の設定オブジェクトを ミュータブルなまま共有しない
  • Ractor 間で共有できるように freeze 済みオブジェクト として扱う

という設計変更です。

テストの追加

activerecord/test/cases/counter_cache_test.rb で、主に次のようなことを検証するテストが追加されていると考えられます:

  • ActiveRecord::CounterCaches._counter_cache_columns が frozen であること
  • ActiveRecord::CounterCaches.counter_cached_association_names が frozen であること
  • カウンタキャッシュ機能自体が、freeze された設定オブジェクトのもとでも従来どおり動作すること

  1. 影響範囲・注意点

影響範囲

  • 影響を受けるのは、「ActiveRecord のカウンタキャッシュ設定情報を内部 API として直接触っているコード」です。
  • 通常の Rails アプリケーションで、以下のような典型的な使い方だけをしている場合は影響はほぼありません:
    ruby
    class Comment < ApplicationRecord
      belongs_to :post, counter_cache: true
    end
    このレベルであれば挙動は変わらず、カウンタキャッシュ機能はそのまま動作します。

注意点

  1. 内部構造への破壊的変更は不可になる

    もしアプリや gem で、内部 API に直接触れて次のような操作をしている場合は、FrozenError が発生する可能性があります:

    ruby
    # 悪い例(今後は FrozenError の可能性)
    ActiveRecord::CounterCaches._counter_cache_columns[:posts][:comments_count] = :some_value

    今後は、こうした「内部の Hash/Set を直接書き換える」前提のコードは動かなくなります。

  2. Ractor を使う場合の安全性向上

    • frozen 化により、これらの設定オブジェクトは Ractor 間で安全に共有できるようになります。
    • Ractor を利用した並列実行をしている/今後検討している Rails アプリやライブラリでは、カウンタキャッシュ周りでのデータ競合・オブジェクト共有エラーのリスクが軽減されます。
  3. カスタムパッチ・メタプログラミングに注意

    • カウンタキャッシュ周りに独自の Monkey Patch やメタプログラミングを入れている場合、
      • 「既存の Hash/Set を mutate するコード」
      • 「freeze されていないことを前提にした処理」 がないかを確認する必要があります。

  1. 参考情報 (あれば)
  • Ruby Ractor の仕様:
    • Ractor 間で共有されるオブジェクトは、基本的に immutable(freeze 済み)である必要があるため、ライブラリ側では「設定・メタデータオブジェクトを初期化時に freeze しておく」パターンが一般的になりつつあります。
  • この PR は CHANGELOG 更新が行われていないため、Rails 側では「挙動変更というより内部実装の安全性向上」として扱われていると考えられますが、内部 API を触っているコードがある場合はアップデート時に注意が必要です。

#57922 Freeze the inflection rules after the app boots

マージ日: 2026/7/1 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    このPRは、Railsアプリケーションのブート完了後に ActiveSupport::Inflector の「変換ルール(inflection rules)」を変更できないようにし、すべての言語ごとの inflections を freeze する変更です。これにより、起動後の不定な挙動を防ぎつつ、将来的な Ractor 対応も見据えたスレッドセーフ性の向上を図っています。

  1. 変更内容の詳細

2-1. 何が変わったか

  • 言語ごとに保持されている inflection インスタンス(単数・複数形変換、例外ルールなどを持つオブジェクト)を、アプリケーションブート完了時に freeze するようになりました。
  • freeze 後は、ActiveSupport::Inflector.inflections 経由でルールを追加・変更しようとすると、FrozenError が発生します。
  • Rails のブートシーケンス(ActiveSupport::Railtie)に、inflections を freeze するフックが追加されています。
  • これに対応するテストが railties/test/application/active_support_railtie_test.rb に追加されています。

2-2. 想定される処理の流れ

典型的な Rails アプリでは、config/initializers/inflections.rb などで起動時に inflection ルールを定義します。

ruby
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.irregular 'person', 'people'
  inflect.acronym 'API'
end

この PR 適用後も、ブート前(初期化時)のこのコードは有効です。

アプリケーションがブートし終わると:

ruby
ActiveSupport::Inflector.inflections(:en).frozen? #=> true

となり、以降は inflections を変更しようとするとエラーになります:

ruby
ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.irregular 'mouse', 'mice'
end
# => FrozenError (can't modify frozen ActiveSupport::Inflector::Inflections)

2-3. コードレベルのポイント (推測を含まない範囲で)

変更ファイルから読み取れる論点(PRの説明とdiffの規模から確実に言える範囲):

  • activesupport/lib/active_support/inflector/inflections.rb

    • Inflections インスタンスに対し、後から書き換えられないようにするための freeze ロジックを追加。
    • 「言語ごとに1つ存在する inflection インスタンス」をまとめて凍結する仕組みが導入された可能性が高い(説明文より)。
  • activesupport/lib/active_support/railtie.rb

    • Rails アプリケーションブート完了時(on_load または初期化プロセスの終盤)に、全 inflection インスタンスを freeze するフックが追加。
  • railties/test/application/active_support_railtie_test.rb

    • ブート後に inflections を変更できないことを確認するテストケースが追加。
    • おそらく「アプリ起動後に inflections を変更しようとして FrozenError が起きる」ことや、「ブート時に定義したルールは正しく適用される」ことを検証。

  1. 影響範囲・注意点

3-1. 影響を受けるケース

以下のようなコードを書いていると、この変更で動かなくなる/エラーが出る可能性があります。

  1. アプリケーションブート後に inflections を変更している場合

    例:

    • リクエストごとに inflection を変える
    • マルチテナントごとに inflection を動的に追加する
    • コンソール起動後に initializer 相当の処理を実行して inflection を追加している
    ruby
    # 例: Rails 起動後 / リクエスト中に
    ActiveSupport::Inflector.inflections(:en) do |inflect|
      inflect.acronym 'JSONAPI'
    end
    # => FrozenError 発生

    これらは、アプリケーションブート前(initializer)に移動する必要があります。

  2. gem やエンジンが遅いタイミングで inflections をいじる場合

    • エンジンやライブラリが to_prepare やミドルウェア・リクエスト処理の中で ActiveSupport::Inflector.inflections を変更している場合、これも FrozenError の対象となりえます。
    • gem 開発者は、inflection 変更を Rails の初期化プロセス中に移動し、ブート後に変更しない設計にする必要があります。

3-2. なぜこの仕様変更が妥当か

  • 不定なコードパスの排除
    起動後に inflection が変わると、例えば以下がリクエスト順序やタイミングによって変化する可能性がありました。

    • モデル名 → テーブル名 のマッピング
    • ルーティングのパス名
    • 他、inflector を用いる各種内部処理

    これは「あるタイミング以降でだけ挙動が変わる」ため、バグの再現や原因特定が非常に難しくなります。本PRはこれを防ぎます。

  • Ractor セーフティの向上
    Ruby 3 の Ractor を考えると、「起動後に共有オブジェクトを変更可能であること」は競合やメモリ可視性の問題を生みます。
    inflections を freeze してイミュータブルにすることで、「起動前に構築 → 起動後は読み取り専用」という Ractor 向きのモデルになります。

3-3. マイグレーション指針

  • アプリ開発者:

    • inflection の定義や変更は config/initializers/inflections.rb などのブート前処理に集約する。
    • 動的に inflection を変える設計は避け、必要なら自前ロジック(独自の変換テーブルやサービスクラス)を用意する。
  • gem / エンジン開発者:

    • Railtie / Engine の initializer ブロック内で inflections を設定し、to_prepare や実行時に inflections を再定義しない。
    • どうしても追加設定のタイミングがブート後になるなら、inflector を使わない別アプローチを検討する。

  1. 参考情報 (あれば)
  • 関連概念:

    • ActiveSupport::Inflector と inflections:
      ActiveSupport::Inflector.inflections(:en) で取得できるオブジェクトに対し、plural, singular, irregular, uncountable, acronym などでルールを定義する仕組み。
    • Ractor セーフティ:
      共有ミュータブル状態をなくし、起動前に構築したイミュータブルデータを並行に読む構造にすることで、マルチ Ractor 実行時の安全性を高める。
  • 実運用のベストプラクティス:

    • 以前からも「inflection の定義は initializer で行い、実行時に変更しない」ことが暗黙の前提/推奨だったため、その暗黙のルールを仕様として厳密に enforce した形の変更と捉えると理解しやすいです。

#57823 Initialize ActionView::LookupContext.view_context_class early

マージ日: 2026/7/1 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    ActionView::LookupContext.view_context_class をできるだけ早いタイミングで初期化することで、enable_reloading = false な環境ではミューテックスに触らずに済むようにし、Ractor からも追加セットアップなしで安全にビューコンテキストクラスを利用できるようにした変更です。

  1. 変更内容の詳細

※PR本文とファイル差分の要約ベースでの解説です(実際のコードは概念的なイメージ)。

2-1. LookupContext での view_context_class 初期化タイミングの変更

actionview/lib/action_view/lookup_context.rb において:

  • これまで:

    • view_context_class は必要になったタイミングで決定 / キャッシュされ、その際にミューテックスを用いた同期が行われていた。
    • そのため、スレッド/Ractor の利用状況によっては、view_context_class の決定時にロック処理が発生していた。
  • 変更後:

    • LookupContext の初期化時、もしくは「リロードを必要としない (enable_reloading = false)」ことが分かっている場合に、あらかじめ view_context_class を決定しておくようなロジックに変更。
    • enable_reloading = false の場合は、ビューコンテキストクラスが後から変化しない前提のため、一度決定してしまえば再度ミューテックスを取る必要がない。

その結果、enable_reloading = false の環境では:

  • view_context_class の取得がロックフリー(またはほぼロックフリー)になる。
  • Ractor からも「既に決まっている不変のクラス定義」として view_context_class をそのまま利用できる。

2-2. ActionView::Rendering 側の参照調整

actionview/lib/action_view/rendering.rb にも 1 行の変更があり、これは:

  • view_context_class の取得方法やタイミングを、LookupContext の新しい初期化タイミングに合わせて調整したものと考えられます。
  • 実質的には「ビューコンテキストを生成する際に、すでに初期化済みの view_context_class を使う」の一貫性を保つ変更です。

2-3. テスト追加

actionview/test/template/lookup_context_test.rb にテストが 16 行追加されています。

テストの狙いとしては:

  • enable_reloading = false のときに view_context_class が早期にセットされ、以降ミューテックスを介さずに参照できること。 -(間接的に)Ractor からも同じクラスを利用できる、もしくは並行実行時に問題が起きないこと。

といった挙動を担保していると考えられます。


  1. 影響範囲・注意点

3-1. パフォーマンスと並行性

  • パフォーマンス改善:
    • enable_reloading = false(本番環境が多い)で、view_context_class 取得時のミューテックス獲得が避けられるため、ビューのレンダリングにおけるロックコストが削減されます。
  • Ractor 対応の強化:
    • Ractor 内から ActionView を使う場合でも、追加のセットアップなしで view_context_class を共有しやすくなっています。
    • Ractor では「オブジェクトがイミュータブル or Ractor シェア可能」である必要がありますが、enable_reloading = false なら「クラス定義が途中で差し替わらない」ため、Ractor から扱いやすいモデルになります。

3-2. enable_reloading = true (開発環境など)の挙動

  • 開発環境では通常 enable_reloading = true のため:
    • 従来どおり、クラスの再定義やテンプレートの変更に対応する必要があり、必要な箇所ではミューテックスを使った同期が残ります。
    • この PR の主な恩恵(ロック回避・Ractor での利用容易化)は主に enable_reloading = false 側で現れます。

3-3. アプリ側コードへの影響

  • アプリコードで ActionView::LookupContext.view_context_class を直接触っていない限り、既存アプリケーションの動作は基本的に変わらない想定です。
  • もし以下のようなことをしている場合は注意が必要です:
    • view_context_class をモンキーパッチで動的にすげ替えている
    • 初回アクセス時に何か副作用を期待している
  • 今回は「初回決定タイミングがより早くなった」という性質の変更なので、view_context_class 決定時の副作用に依存しているコードがあると、タイミングずれの可能性があります(通常はそのような依存は推奨されません)。

  1. 参考情報 (あれば)

この変更は「本番運用でのロック削減」と「Rails + Ractor の相性改善」の両方に関連するため、Rails を Ruby 3 系で Ractor 活用したり、高負荷環境でのビューレンダリング性能に関心がある場合に重要なアップデートになります。


#57936 Deduplicate columns in default_order

マージ日: 2026/6/30 | 作成者: @55728

  1. 概要 (1-2文で)
    default_order に同じカラムを複数回渡した場合、ORDER BY 句に重複して出力されないよう重複排除される変更です。これにより、すでに同様の挙動をしていた orderregroup と一貫した動きになります。

  1. 変更内容の詳細

挙動の変更点

これまで:

ruby
User.default_order(:name, :name)
# ORDER BY "users"."name" ASC, "users"."name" ASC

この PR 適用後:

ruby
User.default_order(:name, :name)
# ORDER BY "users"."name" ASC

default_order に渡した引数リストから、同じカラム指定を重複して使わないように、内部的に「重複カラムの除去(デデュプリケーション)」を行うようにしています。

実装上のポイント

  • 変更ファイル: activerecord/lib/active_record/relation/query_methods.rb
    • default_order で使用するカラムリストに対して、orderregroup 同様に重複カラムをまとめる処理を追加(1行)。
  • テスト: activerecord/test/cases/relations_test.rb
    • default_order(:name, :name) のようなケースで、生成される SQL の ORDER BY 句に同じカラムが 1 回だけ現れることを確認するテストが追加(5行)。

実際には、既存の order / regroup の実装と同じような重複排除ロジック(例: uniq 的な処理)を default_order のカラム処理にも適用した形と考えられます。


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

    • default_order を使っており、同じカラムを意図的/偶発的に複数回指定しているコードで、生成される SQL が変わります。

    • 例えば、scope やモデルレベルで default_order を定義しているコード:

      ruby
      class User < ApplicationRecord
        default_scope { default_order(:name, :name, :created_at) }
      end

      これまでは name が複数回出ていたのが 1 回にまとまります。

  • パフォーマンス・動作への影響

    • ORDER BY 句の重複列が論理的な結果に違いを生むことは基本的にありません(同じ列・同じ方向での並び替えは冗長でしかないため)。
    • 従って、アプリケーションの機能的挙動が変わる可能性はほぼなく、SQL 文字列の見た目が変わる程度の影響にとどまります。
    • 一部、生成される SQL 文字列そのものを文字列比較でテストしている場合は、テストが落ちる可能性があります。その場合は、新しい形式(重複のない ORDER BY)に合わせて期待値を更新する必要があります。
  • 一貫性の向上

    • 既に orderregroup は同種の重複排除をしているため、default_order だけが異なる挙動をしていた状態が解消されます。
    • これにより、開発者は「ActiveRecord の並び替え関連メソッドは重複カラムを自動的にまとめる」という前提でコードを書けるようになります。

  1. 参考情報 (あれば)

もし既存コードで default_order を多用している場合は、テストスイートを走らせて SQL 文字列比較テストの影響有無を確認する程度で十分です。


#57856 Load ActionDispatch::Request::Session and Utils at boot time

マージ日: 2026/6/30 | 作成者: @andrewn617

  1. 概要 (1–2文で)
  • ActionDispatch::Request::Session とその内部で使われる Utils を「初回リクエスト時のオートロード」から「アプリ起動時のロード」に変更するためのリファクタリングです。
  • これにより ractor 利用時など並行環境でのオートロード問題や循環 require を避けつつ、SessionActionDispatch::Http::Session に移動して責務の位置付けを整理しています。

  1. 変更内容の詳細

背景

  • 以前の変更(rails/rails@8ce6b0c)で、ActionDispatch::Request::Session は循環 require を避けるために autoload されるようになっていました。
  • しかし autoload は「初回アクセス時に読み込む」ため、Ractor などスレッド/並行実行環境では「実行中にクラス定義が変わる」状態を招きやすく、問題になります。
  • この PR では、セッション関連クラスとユーティリティを「HTTP レイヤのクラス」として再配置し、ActionDispatch::Request 読み込み時点で一緒に require されるように変更しています。

主なリファクタリングポイント

1) ActionDispatch::Request::SessionActionDispatch::Http::Session への移動

  • 旧構成(イメージ):
ruby
# action_dispatch/http/request.rb
module ActionDispatch
  class Request
    class Session
      # セッション関連のクラス定義
    end
  end
end
  • 新構成(イメージ):
ruby
# action_dispatch/http/session.rb
module ActionDispatch
  module Http
    class Session
      # セッション関連のクラス定義
    end
  end
end
  • Request::Session はドキュメント化されていない内部 API という扱いであり、クラスの場所を変えても外部 API 互換性の問題は小さい、という判断になっています。
  • ActionDispatch::RequestActionDispatch::Http::Session に依存する側として整理され、循環参照を避ける構造になっています。

2) Utils を HTTP 層に移動し、起動時に require

  • UtilsRequest::Session と同様に HTTP レイヤで使われる内部ユーティリティとして扱い、action_dispatch/http/utils.rb に整理。
  • ActionDispatch::Http に関連するファイル (request.rb, parameters.rb, param_builder.rb など) から、Utils を明示的に require する形に変更し、起動時にロードされます。
ruby
# actionpack/lib/action_dispatch.rb
require "action_dispatch/http/session"
require "action_dispatch/http/utils"
# など Http 関連の require が追加・整理

3) autoload / require の整理

  • action_dispatch.rb, action_dispatch/http/request.rb, action_controller/metal.rb, action_dispatch/railtie.rb などで、SessionUtils の読み込み経路が調整されています。
    • これまで「最初のリクエスト時に autoload」されていたものが、
    • ActionDispatch/Http 読み込みタイミングで require されるように変更。
  • テスト (request_test.rb, session_test.rb 他) も、新しいクラス名・モジュール構成に合わせて参照先を更新しています。

4) 仕様追加・挙動変更はなし

  • 差分は主に「クラス/モジュールの配置 と ロードタイミング」であり、セッション機能自体の振る舞い(API やストアの挙動)は変えていません。
  • テストの追加・変更も挙動変更ではなく、クラスパス変更に追随するための修正が中心です。

  1. 影響範囲・注意点

影響範囲

  1. 内部 API (ActionDispatch::Request::Session) に依存しているコード

    • ドキュメント外 API ですが、もしアプリやライブラリが ActionDispatch::Request::Session を直接参照している場合、今回の変更により壊れる可能性があります。
    • 新しい正しい参照先は ActionDispatch::Http::Session になります。
  2. 初期化時のロードコスト

    • これまで「初回アクセス時にオートロード」されていた Session/Utils が「ブート時にロード」されるため、起動時間がわずかに増える可能性があります。
    • ただし、ActionDispatch 周りの他のクラスもすでにブート時にロードされており、相対的なインパクトは小さいと考えられます。
  3. Ractor / 並行実行環境での安定性向上

    • Rails プロセス起動後、アプリケーションコード実行中に Session クラスが新たに定義されることがなくなるため、Ractor やマルチスレッド環境でのオートロード競合・定義の途中読み取りなどのリスクが低減します。
    • ractor 対応や高並行処理を視野に入れている場合はプラスの変更です。

注意点・確認ポイント

  • 自前で autoload している箇所や const_get("ActionDispatch::Request::Session") のようなメタプログラミングをしている場合、クラスの場所変更により動作が変わるので確認が必要です。
  • ActionDispatch::Http::Session を直接使うつもりがない通常の Rails アプリ(コントローラレベルでしかセッションを扱わないケース)では、ほぼ影響はありません。
  • 将来的に Ractor を有効にする、あるいは Zeitwerk の strict モード・autoload 制限を強めたい場合には、この変更がその下準備の一つになっています。

  1. 参考情報 (あれば)
  • この PR の直接の動機: Ractor 対応作業中に「ActionDispatch::Request::Session が初回リクエストまでロードされない」=並行環境で問題になり得る autoload パターンであることが発覚したため。
  • 関連コミット: rails/rails@8ce6b0c
    • ここで ActionDispatch::Request::Session が autoload 化され、循環 require 回避の暫定措置が導入されていた。
  • 設計上の意味:
    • 「HTTP 関連の低レベルクラス (ActionDispatch::Http::Session, ActionDispatch::Http::Utils)」と
    • 「それを利用する高レベルな ActionDispatch::Request
    • という依存の向きが明確化され、Rails のロード順や並行実行時の安全性の面で一貫した構造になっています。

#57684 Add a Ractor-shareable ActiveSupport::TaggedLogging.shareable_logger

マージ日: 2026/6/30 | 作成者: @etiennebarrie

  1. 概要 (1–2文で)
    Ractor から安全に共有できるログ出力用 API ActiveSupport::TaggedLogging.shareable_logger が追加され、複数 Ractor / スレッドから同じ logger を使える仕組みが導入されました。内部的にはバックグラウンドスレッドに書き込み処理を委譲する ActiveSupport::ShareableLogger を tagged logging でラップする構成です。

  1. 変更内容の詳細

新しく追加された概念とクラス

2-1. ActiveSupport::TaggedLogging.shareable_logger

従来の:

ruby
logger = ActiveSupport::TaggedLogging.logger(STDOUT)

に対応する形で、Ractor 共有可能な logger を返すクラスメソッドが追加されています:

ruby
logger = ActiveSupport::TaggedLogging.shareable_logger(STDOUT)
# => ActiveSupport::TaggedLogging でラップされた ShareableLogger

特徴:

  • 返される logger は ActiveSupport::ShareableLogger::Logger のサブクラス)を内包
  • Ractor 間で共有可能になるよう設計されており、複数 Ractor から同じ logger に書ける
  • TaggedLogging によるタグ付きログ (tagged("request-id") { ... }) が通常通り使える

2-2. ActiveSupport::ShareableLogger の構成

コード的には以下のような構図になっています(ファイルは activesupport/lib/active_support/ractors/logger/*.rb):

  • Writer クラス

    • バックグラウンドで、実際の logdev(IO / ファイルなど)に書き込む専用スレッド
    • 他の Ractor/スレッドから送られてきたメッセージをキューから取り出して順次書き込む
    • 書き込み中に例外が起きた場合は:
      • 元の logger と同様に stderr にフォールバックして出力
      • 呼び出し側には例外を伝播させない(ログ出力失敗がアプリの例外にならない)
  • DeviceProxy クラス

    • Logger::LogDevice を直接触らず、その前段で「書き込み要求をキューに積む」役割
    • write, close, reopen などの操作を、Writer スレッドにメッセージとして渡す
    • これにより、アプリ側スレッド/Ractorは実ログデバイスに直接触らない

概念図:

text
[Ractor A] --\
[Ractor B] --- logger.info(...) -> DeviceProxy -> [queue] -> Writer thread -> logdev
[Ractor C] --/

2-3. エラーハンドリング

説明文にある通り、ログ書き込みエラー時の挙動は既存 Logger と同等になるように設計されています。

  • 書き込み中に IO エラーなどが発生
    • Writer 側でエラーを検知
    • stderr への書き出しにフォールバック
    • 呼び出し元 (logger.info したコード) には例外を返さない

ActiveSupport::ErrorReporter を使う案も検討されたが、それ自体が logger にフォールバックする設計のため、循環参照的になって断念、という経緯が説明されています。

2-4. Ractor 対応のためのインフラ

activesupport/lib/active_support/ractors.rb に Ractor 関連のサポートコードが追加され、それを使って:

  • Ractor 間で安全にやり取りできるオブジェクト/メッセージの制約
  • Writer スレッドとの連携

などを行う仕組みが提供されています。

2-5. テスト

  • activesupport/test/ractor_logger_test.rb(約 260 行)が追加
    • 複数 Ractor から同一 logger に書き込んでも正しくシリアライズされるか
    • flush の挙動
    • エラー時の挙動(stderr へのフォールバックなど)
  • railties/test/application/configuration_test.rb
    • Rails アプリケーション設定との統合(config 経由での利用など)が壊れていないことの確認

  1. 影響範囲・注意点

3-1. 利用シナリオ(想定)

  • Ractor ベースの並列化を行う Rails / ActiveSupport アプリケーションで、単一の logger を全 Ractor から安全に使いたいケース
  • 通常のスレッドベースの Rails アプリでも「非同期ログ書き込み」を使ってみたいケース
    • PR 説明では「通常の threaded Rails アプリでも使えるが、パフォーマンス向上があるかは不明」と明記

3-2. キューの無制限成長リスク

  • 書き込み用キューに上限がないため、ログが大量に出るエンドポイントで「Producer(アプリ側)が早く、Writer が追いつかない」状態になると、キューが肥大化しメモリを圧迫しうる
  • 緩和策:
    • 「リクエストの終わりで logger を flush する」という契約を前提としている
      • 例: Rack ミドルウェアや around_action で logger.flush 相当を呼ぶ設計を想定
  • 作者は別ブランチで「一定しきい値を超えたら同期書き込みに切り替える」backpressure 付き実装を用意しているが:
    • ractor_safe gem への依存が必要
    • そのため Rails 本体には入れず、オプション扱いに留めている

3-3. 互換性と今後の改善余地

  • 現段階では「ドキュメント未掲載の内部的/実験的 API」という位置付け
    • 公開メソッドだが、将来的な API 変更の可能性がある
  • 将来的な効果として期待されているもの:
    • Logger::LogDevice 内の mutex を無効化できる可能性(ログデバイスへの書き込みを Writer スレッドに集約するため)
    • これにより、マルチスレッド環境での logger 周りのロック競合を減らせるかもしれない

3-4. 導入時の実務的な注意点

  • Ractor を本格利用していない通常の Rails アプリで導入する場合:
    • 「本番での安定性・メモリ使用量」を慎重に観測しながらロールアウトするのが良さそう
    • ログ量が多いバッチ処理・エンドポイントでは特に、flush のタイミングとキューのサイズに注意
  • Ractor を使う場合:
    • logger を Ractor に渡す前に、Ractor-shareable であることを確認する設計にする
    • Ractor 内で logger の再構築を繰り返さず、極力共有して利用するほうがパフォーマンス上有利

  1. 参考情報 (あれば)
  • この PR の説明内で参照されている follow-up ブランチ(backpressure 付き実装案)
  • 関連するクラス/ファイル
    • ActiveSupport::TaggedLogging
      • shareable_logger が追加され、Ractor 対応 logger を生成
    • activesupport/lib/active_support/ractors/logger/writer.rb
      • バックグラウンド Writer スレッド本体
    • activesupport/lib/active_support/ractors/logger/device_proxy.rb
      • Ractor から Writer への非同期ブリッジ
    • activesupport/test/ractor_logger_test.rb
      • 具体的な利用・挙動を把握するのに有用なテストケース群

#57937 Load database config when a connection is absent from the shared section

マージ日: 2026/6/30 | 作成者: @55728

  1. 概要 (1-2文で)
    database.ymlshared セクションに存在しない接続名が環境ごとに定義されている場合、これまでは NoMethodError で落ちていたのを、「その接続は shared マージ対象外として素通りさせる」ように修正した PR です。これにより、3階層構造の database.yml をより柔軟に書けるようになり、安全にロードできるようになりました。

  1. 変更内容の詳細

何が問題だったか

3階層の database.yml を以下のように書いたケースを考えます:

yaml
shared:
  one:
    migrations_path: "db/one"

development:
  one:
    adapter: sqlite3
  two:            # `shared`にtwoが無い
    adapter: sqlite3

Rails は Rails.application.config.database_configuration を構築する際に、

  • shared セクションに同名のサブセクションがあれば、それを各環境の同名接続にマージする
  • 例: shared.onedevelopment.one にマージする

という処理を行っています。

しかし実装上、shared 側に 存在しない 接続名に対しても無条件に merge を呼んでおり、shared[:two]nil なのに merge しようとして、

ruby
NoMethodError: undefined method `merge` for nil

が発生していました。

このため、shared では一部の接続だけに共通設定を定義し、他の接続は shared 未定義のまま個別設定で使う、というごく自然な使い方ができませんでした。

どう直したか

railties/lib/rails/application/configuration.rb の shared マージ処理の中で、

  • shared 側に該当のキーがあるときだけ merge する
  • shared 側にキーが無い場合は、その接続設定は何もせず「そのまま残す」

ように条件分岐を追加しています(実質 +1/-1 の小さな修正)。

疑似コードで表すと:

ruby
# 変更前(イメージ)
merged = env_config.merge(shared_config[name]) # shared_config[name] が nil でも merge する

# 変更後(イメージ)
if shared_config[name]
  merged = env_config.merge(shared_config[name])
else
  merged = env_config # sharedに無ければそのまま
end

これにより、次のような挙動になります:

ruby
Rails.application.config.database_configuration
# Before: NoMethodError: undefined method 'merge' for nil
# After:  正常にロードされ、"two" は shared のマージを受けずにそのまま残る

テストの追加

railties/test/application/configuration_test.rb にテストが追加されており、

  • shared に存在する接続 (one) には共通設定がマージされる
  • shared に存在しない接続 (two) は例外を出さず、そのままの設定で残る

ことを確認しています。行数的に見ると、上記の YAML 構造に近い形のテストケースが1つ追加された形です。


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

    • config/database.yml で 3階層 (shared + 各環境 + 接続名) 構造を使っているアプリケーションに影響します。
    • 特に、shared が「一部の接続だけ」を定義しているプロジェクトで、これまで NoMethodError に遭遇していたケースは、この修正で解消されます。
    • shared にすべての接続名を定義していたプロジェクトでは挙動は変わりません。
  • 互換性 / 注意点

    • shared 側に無い接続名は、今後も shared の設定を一切マージされず「完全に独立した接続」として扱われます。
      • もし「本当は shared にも書くつもりだったが、書き忘れていた」ような設定ミスは、この変更では検出されず、静かに素通りします。
      • そのようなミスを検出したい場合は RuboCop などで独自ルールを追加するか、CI で database.yml の検証を自前で行う必要があります。
    • Rails 本体としては「shared に存在しない接続はマージ対象にしない」という仕様を明確化したと言えます。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57937
  • 関連しそうなキーワード:
    • 3-tier database.yml (shared / development / 接続名)
    • マルチ DB 構成 (primary, replica, readonly など)
    • Rails.application.config.database_configuration の解決ロジック

#57946 Allow the Evented LogSubscriber to be shareable

マージ日: 2026/6/30 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    Evented な LogSubscriber(および関連する StructuredEventSubscriber)の実装から「Proc を含む定数ハッシュ」を排除し、Ractor でも共有可能(shareable)になるようにリファクタリングした PR です。Ractor.shareable_proc を使わずに、設計側で Proc 依存を解消することで Ractor セーフティを高めています。

  1. 変更内容の詳細

※実際のコードを要約した概念的な説明です。

背景となる問題

  • もともと ActiveSupport::EventReporter::LogSubscriber(とそれに関連する ActiveSupport::StructuredEventSubscriber)には、
    • 「イベント名 → 処理内容」を表す 定数ハッシュ が存在
    • そのハッシュの値として Proc オブジェクト が格納されていた
  • 定数自体は freeze されていても、中に非 shareable なオブジェクト(Proc)があるため、Ractor 間で安全に共有できない状態だった。
  • Ractor.shareable_proc でラップすれば shareable にできるが、
    • そのたびにラップするのが煩雑
    • コードの可読性も下がる
      という理由で、設計レベルで Proc を定数ハッシュから排除する方向にリファクタリングしている。

主なリファクタリング内容

変更ファイルは次の2つです。

  • activesupport/lib/active_support/event_reporter/log_subscriber.rb (+10/-6)
  • activesupport/lib/active_support/structured_event_subscriber.rb (+2/-4)

おおまかな方向性は以下の通りです。

  1. 定数ハッシュから Proc を排除
    以前は、例えばこういった形だったものを(※イメージです):

    ruby
    EVENT_PROCESSORS = {
      "sql.active_record" => ->(event) { process_sql_event(event) },
      "render_template.action_view" => ->(event) { process_render_event(event) },
    }.freeze

    これを Proc を値に持たない形 に変更し、

    • シンボルや識別子だけを格納する
    • 実際の処理はメソッドディスパッチなどで行う
      という設計に変えています。例えば次のような形に近づけている可能性が高いです:
    ruby
    EVENT_PROCESSORS = {
      "sql.active_record" => :process_sql_event,
      "render_template.action_view" => :process_render_event,
    }.freeze
    
    def call(event)
      if (handler = EVENT_PROCESSORS[event.name])
        send(handler, event)
      end
    end

    こうすることで、EVENT_PROCESSORS

    • 文字列/シンボルのみで構成される
    • 完全に shareable なオブジェクト になり、Ractor 間で安全に共有できます。
  2. Evented LogSubscriber のコードパスを Ractor 安全に

    • Evented なログ購読ロジック(通知を受け取り、ログ出力や構造化イベントに変換する部分)が、
      • Ractor 内から利用される
      • かつグローバルに共有される可能性
        を考慮して、「共有しても安全なオブジェクト(Ractor.shareable)」だけを定数として持つようになっています。
    • StructuredEventSubscriber 側でも、同じ定数ハッシュや共通ロジックを参照している部分があるため、そこも Proc 非依存に合わせて微調整されています(+2/-4 程度の小修正)。
  3. Ractor.shareable_proc を使わない構成に

    • Ractor.shareable_proc は、ブロックや Proc を Ractor 間共有可能にするための仕組みですが、
      • ラップが散在しがち
      • 「どこまでが shareable でどこからが非 shareable か」が読み取りにくくなる
    • この PR では、それを使う代わりに
      • 「そもそも定数の中に Proc を入れない」
      • 「Proc 的な振る舞いはインスタンスメソッドやクラスメソッドのディスパッチに寄せる」 方針で整理しています。

  1. 影響範囲・注意点
  • 主な影響範囲
    • ActiveSupport::EventReporter::LogSubscriber
    • ActiveSupport::StructuredEventSubscriber
  • これらを直接継承・拡張しているコードが、
    • 定数ハッシュに Proc を追加・上書きしている
    • 内部の実装に強く依存している 場合は、挙動の差異や互換性に注意が必要です。
  • 公開 API としては、「イベント名に応じてログ/構造化イベントが処理される」という仕様自体は変わらない想定です。
  • この PR 自体では テストは追加・更新されていない ため、
    • Ractor を使う環境での Evented ログ出力
    • StructuredEventSubscriber 経由のイベント処理
      などを実際のアプリケーション・本番に近い環境で確認するのが無難です。
  • 今後、Ractor を前提とした並行実行(例: 複数 Ractor から ActiveSupport のイベントを購読・ログ出力する)を行う際の 前提条件 として、この変更が効いてきます。

  1. 参考情報 (あれば)
  • Ruby の Ractor と shareable オブジェクトの仕様:
  • Rails 内での関連コード:
    • ActiveSupport::LogSubscriber
    • ActiveSupport::Notifications これらと Evented LogSubscriber / StructuredEventSubscriber は密接に連携しているため、イベント処理まわりを拡張する場合はソースを一緒に読むと理解しやすいです。

#57816 Set browser binary when preloading system test driver

マージ日: 2026/6/30 | 作成者: @titusfortner

  1. 概要 (1-2文で)
    Rails の system test で Selenium ドライバをプリロードする際に、Chrome/Firefox のブラウザ実行ファイルの場所(options.binary)が設定されず「cannot find Chrome binary」で落ちる問題を修正する PR です。
    Selenium 側の仕様変更(DriverFinderoptions をミューテートしなくなった)に追従し、Rails 側で明示的にブラウザバイナリを設定するようにしています。

  1. 変更内容の詳細

背景

  • Rails の system test で Selenium ドライバを「プリロード」する際、action_dispatch/system_testing/browser.rbBrowser#preload が Selenium の Chrome::Service.driver_path / Firefox::Service.driver_path をグローバルに設定していました。
  • 以前は Selenium の DriverFinder
    • ドライバのパス解決(driver_path
    • ブラウザ実行ファイルパスの設定(options.binary) の両方を行っており、options.binary を書き換える副作用に依存していました。
  • Selenium 4.45 で DriverFinderoptions をミューテートしなくなり、その副作用が消滅。
    • Rails 側では Service.driver_path だけが設定される
    • その結果、各 worker がドライバを作るとき Selenium Manager がスキップされ、「ドライバパスだけ判明し、ブラウザパスは未設定」の状態になる
    • options.binary が空のままになり、「cannot find Chrome binary」で失敗する

この PR の対応

ポイントは「ドライバパスとブラウザパスを同じ DriverFinder から取得し、options.binary を Rails 側で明示的に設定する」ことです。

actionpack/lib/action_dispatch/system_testing/browser.rb の変更:

  • これまで:
    • Browser#preload 内で DriverFinder を呼び出して Service.driver_path を設定
    • DriverFinder が内部で勝手に options.binary も設定することに依存
  • これから:
    • 同じ DriverFinder 呼び出しで、ドライバパスとブラウザパスを両方取得
    • 取得したブラウザパスを options.binary = <resolved browser path> として明示的に代入

疑似コードイメージは以下のような形になります(実際の API 名は Selenium 実装に依存しますが、やっていることのイメージとして):

ruby
def preload
  # options は system test 用の Selenium::WebDriver::Chrome::Options など
  options = build_options

  # DriverFinder からドライバとブラウザの両方を解決
  driver_finder = Selenium::WebDriver::DriverFinder.new(browser: :chrome, options: options)
  driver_path   = driver_finder.driver_path
  browser_path  = driver_finder.browser_path # ここからブラウザパスも取得(新しく明示的に利用)

  # ドライバパスをグローバル Service に設定(従来どおり)
  Selenium::WebDriver::Chrome::Service.driver_path = driver_path

  # ブラウザバイナリを明示的に設定(新しく追加された部分)
  options.binary = browser_path

  # …以降、プリロード完了
end

※上記は PR の説明から再構成したイメージコードです。実際のメソッド名や引数は Selenium の実装に従います。

actionpack/test/dispatch/system_testing/driver_test.rb の変更:

  • テスト側で、「プリロード時に options.binary が正しく設定されること」を検証するテストが追加・更新されています。
  • 具体的には:
    • これまで「ドライバパスが解決されているか」を見ていた箇所に加えて
    • 「ブラウザバイナリパス(options.binary)も期待どおりに設定されているか」をチェックするようになっています。
  • 行数ベースでは +15/-11 程度の比較的小さな追加・修正で、既存ケースの調整+新アサーションの追加と考えられます。

  1. 影響範囲・注意点
  • 影響対象:
    • Rails の system test を Selenium(Chrome / Firefox)で動かしていて
    • config.system_testing で「ドライバのプリロード」を有効にしているプロジェクト
    • かつ Selenium 4.45 以降(DriverFinder 仕様変更後)を使っている環境
  • この PR により:
    • 「cannot find Chrome binary」などのエラーで system test が失敗していたケースが解消される見込みです。
    • ドライバとブラウザのパス解決が同じ DriverFinder によって一貫して行われるため、Selenium の >= 4.20.0 サポート範囲内で後方互換性が保たれています。
  • 注意点:
    • 独自に options.binary を上書きしている(例: 特定の Chrome/Firefox バイナリを明示指定している)場合は、Rails 側のプリロード処理との競合がないか確認するとよいです。
      • ただし、この PR は「DriverFinder が解決したブラウザパスを使う」だけなので、ほとんどのケースでは既存の期待どおりに動作します。
    • CI などで「Selenium Manager 無効化」「独自のドライバ/ブラウザ配置」をしている場合にも、DriverFinder ベースの解決に統一されるため、パス解決ロジックの挙動を一度確認しておくと安心です。

  1. 参考情報 (あれば)
  • 元の Selenium 側 Issue:
    https://github.com/SeleniumHQ/selenium/issues/17698
    DriverFinderoptions をミューテートしなくなった変更と、それにより options.binary が設定されなくなる問題の報告。
  • この PR は既存の Selenium サポート範囲(selenium-webdriver >= 4.20.0)と互換性を保つように実装されています。

#57918 Make default scopes ractor safe

マージ日: 2026/6/30 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    ActiveRecord::Base.default_scope の実装を見直し、デフォルトスコープを Ractor セーフ(スレッド/Ractor間で共有しても安全)にするために、default_scope の内部状態を freeze して不変オブジェクトとして扱うようにした PR です。加えて、この挙動を保証するテストが追加されています。

  1. 変更内容の詳細

何をしたか

  • ActiveRecord::Base.default_scope が内部的に保持している

    • 「default scope の値オブジェクト」
    • default_scopes 配列」 を、基本的に「凍結(freeze)」して不変オブジェクトとして扱うように変更。
  • default scope のブロック(proc)は instance_exec で評価されるため、ユーザーにとっても Ractor 共有可能な(Ractor.shareable? な)オブジェクトにしやすいと判断し、そこを前提に安全に共有できるようにしている。

  • default_scopes に新しい要素を追加する際は、

    1. 既存配列を dup
    2. 新しい scope を append(push)
    3. その結果の配列を freeze

    という手順で、「配列自体もイミュータブル」となるようにしている。

疑似コードイメージ(実際のコード構造を簡略化したイメージです):

ruby
# 変更前(イメージ)
def default_scope(scope = nil, &block)
  self.default_scopes ||= []
  new_scope = DefaultScope.new(scope || block)
  self.default_scopes << new_scope # 既存配列を破壊的に変更
end

# 変更後(イメージ)
def default_scope(scope = nil, &block)
  new_scope = DefaultScope.new(scope || block).freeze  # 値オブジェクトをfreeze

  # default_scopes 配列は、常に freeze された新しい配列オブジェクトに差し替える
  scopes = (self.default_scopes || []).dup
  scopes << new_scope
  self.default_scopes = scopes.freeze
end

テストの追加

activerecord/test/cases/scoping/default_scoping_test.rb にテストが追加され、主に以下のようなことが確認されていると考えられます(PR説明と統計から推測):

  • default_scope 呼び出し後に、内部で保持される default scope の値オブジェクトが frozen? == true であること。
  • default_scopes 配列自身も frozen? == true であること。
  • 複数回 default_scope を呼び出しても、配列の freeze が維持されつつ、新しい要素が正しく追加されること(dup + append + freeze の挙動が正しいこと)。

  1. 影響範囲・注意点

影響範囲

  • 対象は ActiveRecord::Base.default_scope の内部実装 およびそれに依存するクラスです。
  • 特に以下のようなケースに関わります:
    • default_scope を多用するモデル
    • Ractor を使った並列実行(Ruby 3 以降)を意識しているアプリ / ライブラリ
    • default_scope の内部状態(default_scopes 配列など)に直接アクセス・変更しているメタプログラミング的なコード

注意点

  • 破壊的変更の可能性:
    • default_scopes 配列が常に freeze されるようになるため、

      • Model.default_scopes << something
      • Model.default_scopes.push(...)
      • Model.default_scopes[0] = ...

      といった 配列を直接書き換えるコードは FrozenError で落ちる 可能性があります。

    • これらは本来 Rails が想定していない内部構造への依存なので「非推奨な使い方」ではありますが、メタプログラミングで触っている場合は注意が必要です。

  • default scope の「値オブジェクト」も freeze されるため、
    • もしライブラリ側で独自に default_scope の値オブジェクトを取り出して mutate(プロパティを書き換え)しているような場合も同様に FrozenError のリスクがあります。
  • 通常の使い方(default_scope { where(active: true) } など)をしているアプリケーションにとっては、挙動としてはほぼ影響なく、Ractor セーフになる分だけ安全性が向上します。

  1. 参考情報 (あれば)
  • Ruby の Ractor と共有可能オブジェクト:
    • Ractor.shareable?(obj)true になる条件として、「オブジェクトが再帰的にすべて frozen である」ことが重要です。今回の変更は default_scope 関連オブジェクトをその条件に近づけるものです。
  • Rails 内の類似の freeze 戦略:
    • 最近の Rails では、設定オブジェクトや Arel ノード、relation 関連オブジェクトで freeze を積極的に使うことで、Ractor セーフ / スレッドセーフな実装を進めており、その一環と見ることができます。

#57930 Only document the public ActionView::DependencyTracker API [ci skip]

マージ日: 2026/6/30 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    ActionView::DependencyTracker のうち、実際に外部から利用されている「公開API」のみを公式ドキュメントに載せるように整理した PR です。register_trackerERBTracker を正式にドキュメント対象とし、それ以外の内部実装は引き続き非公開(:nodoc:)とすることで、サポート対象のエントリポイントを明確にしています。

  1. 変更内容の詳細

2-1. 何が「公開API」として文書化されたか

この PR では以下が「公式にサポートされる public API」として RDoc/YARD などの生成ドキュメントに現れるようになります。

  • ActionView::DependencyTracker
  • ActionView::DependencyTracker.register_tracker
  • ActionView::DependencyTracker::ERBTracker

一方で、次は依然として内部用実装と位置付けられ、:nodoc: のままです。

  • find_dependencies
  • remove_tracker
  • 各種 tracker 用の正規表現(依存関係解析ロジックの詳細)
  • RubyTracker クラス

(実際のコード変更は主にこれらに対するコメント・:nodoc: の付け外しであり、仕様や挙動を変えるロジック変更ではありません。)

2-2. register_tracker の役割

register_tracker は「新しいテンプレート言語の依存関係を ActionView に教える」ための公式な拡張ポイントです。

概ね以下のような API だと考えられます(※PR 文脈と既存の coffee-rails / haml-rails からの推測であり、実際のシグネチャは Rails 本体を参照してください):

ruby
# 例: foo という独自テンプレート拡張子向けに tracker を登録する
ActionView::DependencyTracker.register_tracker(
  :foo, # テンプレートタイプ / 拡張子
  MyApp::ViewDependency::FooTracker # 依存関係解析ロジック
)

こうすることで、render の際に *.foo テンプレートから参照される partial / template などの依存関係を Rails が把握し、キャッシュの失効や再読み込みに反映できるようになります。

PR 説明でも触れられているように、すでに以下の gem がこの API を利用しています:

これらはすでに実運用されている事実上の public API でしたが、クラスごと :nodoc: になっていたため「非公開扱い」になっていました。今回それを公式に「ここが拡張ポイントです」と宣言する形です。

2-3. ERBTracker の役割

ActionView::DependencyTracker::ERBTracker

  • 「Ruby と同じような render 呼び出しを行うテンプレート言語」
    • 例: render "partial", render partial: "users/user", locals: { user: @user }
  • そのテンプレートから、どの partial / template が使われているかを解析して依存関係を抽出

するための再利用可能な tracker クラスです。

PR 説明によると、ERBTracker は「gem が自前のテンプレート言語を実装していても、その render 呼び出しが Ruby に似ているなら、そのまま使える共通の tracker」として想定されています。

例: 独自テンプレート *.foo が実質的に ERB と同じ記法で render を呼んでいるなら、

ruby
ActionView::DependencyTracker.register_tracker(:foo, ActionView::DependencyTracker::ERBTracker)

のように再利用すれば、独自に依存関係解析ロジックを書かなくてもよくなります。

今回の PR によって ERBTracker 自体の挙動は変わっていませんが、「ここを使ってよい・ここは拡張してよい」という意味での公式ドキュメントが追加され、サポート範囲が明文化されています。

2-4. 内部APIの非公開化の継続

find_dependenciesremove_trackerRubyTracker・内部の正規表現などは引き続き :nodoc: とされ、API ドキュメント生成の対象外になっています。これは

  • これらを「いつでも変更可能な内部実装」と位置付ける
  • gem 作者に対して「ここを直接触ると将来壊れる可能性が高い」とメッセージする

という意味があります。


  1. 影響範囲・注意点
  • 実行時の挙動への影響は基本的にありません
    変更はほぼドキュメントレベルであり、依存関係追跡のロジックやインターフェースのシグネチャ変更は含まれていません。

  • すでに register_tracker / ERBTracker を使っている gem / アプリは、そのまま利用継続可能です
    これまで「実質 public だがドキュメント上は private」という微妙な状態だったものが、正式に public API として認められた形です。将来的な後方互換性も、他の public API と同様のポリシーで扱われる期待が持てます。

  • 逆に、find_dependenciesRubyTracker 等の内部 API に依存している場合は要注意
    これらは今回も明確に非公開扱いとされました。

    • もし独自 gem 等で直接呼んでいる場合は、register_tracker と独自 Tracker クラス、もしくは ERBTracker の再利用に移行すべきです。
    • 将来的な Rails のマイナーアップデートで破壊的変更を受ける可能性があります。
  • ドキュメント生成時の見え方が変わります
    Rails の API ドキュメントサイト(api.rubyonrails.org 等)で、ActionView::DependencyTrackerregister_tracker / ERBTracker が見えるようになり、拡張方法がわかりやすくなります。


  1. 参考情報 (あれば)
ruby
# 独自テンプレートエンジン Foo の Tracker
class FooTracker
  # ActionView が呼び出す想定のインターフェース
  def self.call(name, template)
    # template.source を解析して render 呼び出しを拾い、
    # 依存しているテンプレートパスの配列を返す等
    # ここは Rails 本体の実装/Doc を参照して合わせる
  end
end

# 初期化時に tracker 登録
ActionView::DependencyTracker.register_tracker(:foo, FooTracker)
# もしくは render 構文が Ruby/ERB とほぼ同じなら:
ActionView::DependencyTracker.register_tracker(:foo, ActionView::DependencyTracker::ERBTracker)

独自エンジンを作る側は、今後はこのあたりが「公式なやり方」としてドキュメントで参照できるようになります。


#57932 Minor Active Support Cache refactorings

マージ日: 2026/6/30 | 作成者: @byroot

  1. 概要 (1-2文で)
    ActiveSupport::Cache 周りの内部実装を少し整理したリファクタリング PR です。instrument/ログ出力処理と、ローカルキャッシュ (LocalCache) の increment 挙動が単純化され、不要なヘルパーやメソッドが削除されています。

  1. 変更内容の詳細

2-1. ActiveSupport::Cache#instrument の整理

やったこと

  • 内部ヘルパー _instrument を削除し、instrument の処理を直接書く形に変更。
  • ログ出力において logger.level を手動で確認するのではなく、Logger#debug のブロック形式を利用するように変更。

目的・効果

  • _instrumentinstrument からしか呼ばれておらず、処理もほぼ同じだったため、メソッド分割による見通し改善よりも「追いにくさ」の方が勝っていたと判断して削除されています。
  • logger.debug { ... } 形式にすることで、デバッグログが無効なときにはブロックが評価されず、メッセージ構築コストを自動的に避けられます。
    これにより、手動で if logger.debug? のようなレベルチェックを書く必要がなくなります。

イメージコード(概念的な Before / After)

Before(イメージ):

ruby
def instrument(operation, key, options = nil)
  payload = { key: key }
  _instrument(operation, payload, options) do
    # 実処理
  end
end

def _instrument(operation, payload, options)
  if instrument_notifications?
    ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) do
      result = yield
      logger.debug("Cache #{operation}: #{payload[:key]}") if logger && logger.debug?
      result
    end
  else
    yield
  end
end

After(イメージ):

ruby
def instrument(operation, key, options = nil)
  payload = { key: key }

  if instrument_notifications?
    ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) do
      result = yield
      logger&.debug { "Cache #{operation}: #{payload[:key]}" }
      result
    end
  else
    result = yield
    logger&.debug { "Cache #{operation}: #{payload[:key]}" }
    result
  end
end

※ 上記は概念を伝えるための擬似コードであり、実際のコードとは細部が異なります。


2-2. Local Cache (ActiveSupport::Cache::Strategy::LocalCache) の increment の簡略化

変更点

  • ローカルキャッシュ戦略における increment 実装がシンプルになった。
  • bypass_local_cache メソッドが削除された。

以前の挙動(概念)

LocalCache 戦略は、プロセス内で使う「スレッドローカルのキャッシュ」のようなもので、元のストア(例: Redis, Memcached, MemoryStore)をラップしています。

以前は、increment を行う際に、

  • 「ローカルキャッシュをバイパスして元のストアに直接 increment を送る」
    ような処理をしていたため、bypass_local_cache といったヘルパーが存在していました。

今回の簡略化後(概念)

  • increment を行うときに、わざわざローカルキャッシュをバイパスせず、よりストレートに本来のストア実装にフォワードする形に整理。
  • その結果、bypass_local_cache 自体が不要になり削除されています。

テスト側 (local_cache_behavior.rb) の変更から読み取れること:

  • increment 実行時のローカルキャッシュとの整合性や、複数回呼び出し時のカウンタ挙動などについてテストが拡充されているため、挙動は維持・明確化された上で内部構造だけがシンプルになった、という位置づけです。

2-3. その他の小さな変更

  • memory_store.rb の微修正 (+1/-1):
    ログ周り、あるいは increment 関連で統一的な呼び出しに合わせた小さなコード調整と思われます。
  • Redis ベースのキャッシュストアテスト
    • deprecated_redis_cache_store_test.rb
    • redis_cache_store_test.rb
      の両方で 1 行ずつ変更されています。主に increment の扱い、もしくはローカルキャッシュを経由した場合の振る舞い確認のための微修正です。

  1. 影響範囲・注意点
  • 外部 API の互換性

    • 公開 API (ActiveSupport::Cache::Store#increment, #fetch, etc.) の振る舞いは変えずに、内部実装を整理したリファクタリングです。
    • 直接 _instrumentbypass_local_cache に依存していなければ、通常のアプリケーションコードに影響はありません。
  • モンキーパッチ / 内部依存している場合

    • ActiveSupport::Cache に対して独自拡張・モンキーパッチをしている場合、以下を確認してください:
      • _instrument にフックしていたコードがあれば破綻します。
      • ActiveSupport::Cache::Strategy::LocalCache::LocalStore などの内部クラス/bypass_local_cache に依存していた場合も同様です。
    • 特にキャッシュミドルウェアや独自キャッシュストアを書く際に、内部メソッドに依存していると影響を受ける可能性があります。
  • ロギング周り

    • これまで logger.debug? 判定を前提にしていたようなモンキーパッチと組み合わせている場合、logger.debug { ... } 形式への移行によって挙動が微妙に変わる(ログメッセージ生成タイミングなど)可能性はありますが、通常は問題にならない範囲です。
  • テスト

    • Local Cache 挙動を検証するテストが増えているため、increment の意味的な挙動は今後もこの PR の形が前提になります。
    • redis_cache_store 関連テストも微修正されており、Redis + LocalCache 併用環境でも、想定どおりに increment が動くことが確認されています。

  1. 参考情報 (あれば)
  • Rails ガイド(キャッシュ)
    https://guides.rubyonrails.org/caching_with_rails.html
    ActiveSupport::Cache 全般の利用方法。今回の変更は主に内部実装であり、このガイドに書かれた使い方はそのまま有効です。

  • ActiveSupport::Notifications とログの連携
    多くのキャッシュ操作は cache_read.active_support などの通知イベントとして出るため、APM や独自メトリクス収集をしている場合は instrument 実装の読みやすさ向上がメンテナンス性に寄与します。


#57931 Implement ActiveSupport::ProxyLogger

マージ日: 2026/6/30 | 作成者: @byroot

  1. 概要 (1-2文で)
    RailsのActiveSupportに ActiveSupport::ProxyLogger が追加され、既存のロガーにログを委譲しつつ「別のログレベルで」フィルタリングできるラッパーロガーが使えるようになりました。これにより、アプリ全体と同じ出力先を使いつつ、特定ライブラリだけログを抑制・制御することが容易になります。

  1. 変更内容の詳細

2-1. ProxyLoggerとは何か

ActiveSupport::ProxyLogger は「あるロガー(親ロガー)」へログ出力を“丸ごと委譲”しつつ、自身が持つ独立したログレベルでログを間引くクラスです。

ruby
SomeLibrary.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error)

この例では:

  • 実際の出力先・フォーマットなどは Rails.logger をそのまま利用
  • しかし SomeLibrary から出るログは「error 以上」に制限される
    infodebug は、ProxyLogger 側で捨てられる

アプリ本体のログレベル(例: :info)と、ライブラリのログレベル(例: :error)を分けられるのが主な目的です。

2-2. 対応しているインターフェース

PR 説明によると、標準ライブラリ Logger のインターフェースのほとんどをサポートしています。典型的には以下のようなメソッドが委譲・対応されていると考えてよいです(実装ファイルは active_support/proxy_logger.rb):

  • レベル指定メソッド:
    • debug, info, warn, error, fatal, unknown
  • 汎用ログメソッド:
    • add(severity, message = nil, progname = nil, &block)
  • ログレベル関連:
    • level, level=, debug?, info?, warn?, error?, fatal?
  • その他 Logger 互換・ActiveSupport::Logger 互換のメソッドがほぼそのまま使える想定

挙動のイメージ:

ruby
# Rails.logger は通常通りアプリ全体のロガー
Rails.logger.level = :info

# noisy ライブラリはログが多いので、ライブラリ側だけ error 以上に絞る
NoisyLibrary.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error)

NoisyLibrary.logger.info("This will NOT be logged")  # ProxyLogger のレベル :error により破棄
NoisyLibrary.logger.error("Something bad")           # Rails.logger へ error として出力

親ロガー (Rails.logger) のログレベルや出力先には影響を与えず、ProxyLogger 側だけでフィルタリングしているのがポイントです。

2-3. 実装位置・その他の変更

  • 新クラス:
    • activesupport/lib/active_support/proxy_logger.rb に実装(176行追加)
  • 読み込み:
    • active_support.rbrequire が追加され、ActiveSupport をロードすれば利用可能
  • 既存 Logger 周りの微調整:
    • active_support/logger.rb が 1 行だけ削除されており、内部の読み込み順や依存関係の整理と思われます
  • テスト:
    • activesupport/test/proxy_logger_test.rb が追加され、主な挙動(ログレベル判定・委譲)がテストされています
  • ドキュメント:
    • activesupport/CHANGELOG.md にエントリ追加

  1. 影響範囲・注意点

3-1. 影響範囲

  • 新機能であり、既存コードは基本的に非互換変更の影響を受けません
  • 既存の Rails.logger / ActiveSupport::Logger の挙動も変わりません。
  • 外部ライブラリや自前コードで「ロガーを差し替えるポイント」があれば、そこで ProxyLogger を差し込むことができます。

3-2. 利用時の注意点・設計上の考慮

  1. ログレベルの二重管理

    • 親ロガーと ProxyLogger の両方にログレベルが存在します。
    • 実際に出力されるかどうかは「ProxyLogger のレベル判定を通過し」「かつ親ロガー側で弾かれない」場合のみです。
    • 例: 親が :error、ProxyLogger が :info だと、info ログは ProxyLogger 側で通っても親で捨てられます。
  2. 出力先・フォーマット変更は親ロガー側で行う

    • ProxyLogger は基本的に委譲しかしないため、
      • ログの出力先(ファイル・標準出力・外部サービスなど)
      • フォーマッタ、タグ付け、ログサブスクライバ
        などはすべて「親ロガー側」で設定・制御します。
  3. ライブラリのロガー差し替えが可能か確認

    • SomeLibrary.logger = ... のようにロガーを外から渡せる前提の仕組みを持つライブラリでのみ、この機能が直接活きます。
    • Rails の各種コンポーネント(ActiveRecord, ActionController など)や、Sidekiq など多くの gem がこのパターンを取っています。
  4. パフォーマンス観点

    • ProxyLogger で枝刈りすることにより、特に noisy なライブラリが debuginfo を大量に発行する場合に、出力やフォーマット処理の負荷を下げられます。
    • ただしログメッセージ構築自体(文字列連結など)が呼び出し元で行われている場合、そのコストは残るため、可能ならブロック形式のログ (logger.debug { expensive_computation }) を使うのが望ましいです。

  1. 参考情報 (あれば)
  • 追加されたクラス:
    activesupport/lib/active_support/proxy_logger.rb
    → 実装詳細・対応しているメソッド一覧を確認可能
  • 変更履歴:
    activesupport/CHANGELOG.md の ActiveSupport セクション
  • 利用イメージまとめ:
    ruby
    # アプリ本体
    Rails.logger.level = :info
    
    # ライブラリAはログがうるさいので error 以上だけ
    LibraryA.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error)
    
    # ライブラリBは debug も見たい
    LibraryB.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :debug)

このPRにより、「同じ出力先を共有しつつ、コンポーネントごとにログレベルを変える」というパターンが、Rails 標準の API だけでシンプルに書けるようになりました。


#57874 Make the Action View template handler registry Ractor-shareable

マージ日: 2026/6/30 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    Action View のテンプレートハンドラのレジストリを Ractor で共有可能(Ractor-shareable)にするため、内部実装がクラス変数ベースからモジュールインスタンス変数+不変(frozen)な構造に再設計された PR です。これにより、テンプレート検索・描画パスでのハンドラ参照が Ractor セーフかつスレッド安全になり、読み取り時に書き込みが発生しないようになっています。

  1. 変更内容の詳細

2-1. レジストリの格納場所の変更

従来:

  • ActionView::Template::HandlersTemplateextend し、
  • ハンドラのレジストリ(extension → handler)は クラス変数 に保持されていました。
  • そのため、Ractor 的には共有しづらく、「読み取り時にレジストリを書き換える」ようなパターンも残っていました(遅延メモ化など)。

変更後:

  • レジストリは Template::Handlers モジュールの モジュールインスタンス変数 に保持されます。
  • Handlers モジュール自体が Ractor 間で共有され、非メイン Ractor からも「共有可能な値」として安全に読めるように設計されています。
  • Template 側の API (Template.handler_for_extension, Template::Handlers.extensions など) は、モジュール上の実装に委譲する薄いラッパーになりました。
    • Template は依然として Handlers を extend していますが、実体はモジュール側に寄せた形です。

2-2. ハンドラオブジェクトを Ractor-shareable に

Ractor 間で共有するには、保持されるオブジェクト(ハンドラ)自体も shareable である必要があります。この PR では以下のように処理しています。

  1. 組み込みハンドラ

    • 例: ERB ハンドラ、Builder ハンドラなど
    • これらのインスタンスを freeze して不変オブジェクトに。
    • 不変にすることで、Ractor 間で安全に共有可能となります。
  2. :ruby ハンドラ

    • :ruby テンプレート用のハンドラは ActiveSupport::Ractors.shareable_lambda を使って生成。
    • shareable_lambda は、Ractor 間で共有できる Proc/lambda を作るための ActiveSupport ヘルパです。
  3. アプリケーション側で登録される Proc ハンドラ

    • アプリケーションコードから ActionView::Template.register_template_handler などで登録される Proc/lambda については、
      • ActiveSupport::Ractors.try_shareable_proc を経由して登録します。
    • try_shareable_proc は、渡された Proc を shareable にできる場合はそうし、そうでなければ(たとえばクロージャで非共有な外部オブジェクトを閉じている場合など)適切に扱うためのヘルパです。
    • これにより、「アプリケーションが登録したハンドラが Ractor 的に問題を起こす」ケースを減らしつつ、できるだけ共有可能にする方針になっています。

2-3. レジストリ更新方式の変更(コピーオンライト & イager 計算)

以前の問題点:

  • Template::Handlers.extensions などが「遅延メモ化(memoization)」しており、読み取りのタイミングで内部状態を書き換える可能性がありました。
  • Ractor・並行性の観点からは、読み取りが決して書き込みを行わない構造が望ましいです。

変更後:

  • レジストリ(extension → handler の Hash)は 登録時に新しい frozen Hash を作るコピーオンライト方式に変更されました。

    • 例イメージ(実装イメージ・擬似コード):

      ruby
      def register_handler(ext, handler)
        @handlers = @handlers.merge(ext.to_s => handler).freeze
        @extensions = @handlers.keys.freeze
      end
  • Template::Handlers.extensions の内容(拡張子の一覧)も、

    • 登録時に毎回再計算して freeze
    • 読み取りは計算済みの frozen オブジェクトを返すだけ
  • これにより、「読み取り時には一切書き込みが発生しない」設計となり、Ractor/並行実行での安全性と予測可能性が向上しています。

2-4. 初期登録の場所変更 (self.extended フックを排除)

従来:

  • TemplateHandlers を extend する際の self.extended フック(モジュールの extend 時に呼ばれるフック)内で、組み込みハンドラの初期登録を行っていました。

変更後:

  • ハンドラのレジストリと登録ロジックはすべて Handlers モジュール自身に集約されました。
  • 組み込みハンドラは モジュール読み込み時に直接登録されます。
  • self.extended フックは不要となり削除されています。
    • テスト用・公開 API としての ActionView::Template.* インスタンスメソッドは、単に Handlers に委譲するだけです。

2-5. Builder ハンドラの require メモ化方式変更

ActionView::Template::Handlers::Builder では、

  • 以前は「require "builder" をしたかどうか」を インスタンス変数でメモ化していました。
  • しかし、この PR により Builder ハンドラも freeze されるため、インスタンス変数への書き込み(=メモ化)が不可能になります。

そこで、

  • require "builder" が既に実行されたかどうか」を defined? ガードで判定する方式に変えています。

    • 例イメージ(概念的なもの):

      ruby
      def call(...)
        require "builder" unless defined?(::Builder)
        ...
      end
  • これにより、ハンドラインスタンス自体は完全に不変のまま、require の多重実行を避けることができます。

2-6. テストとツールの更新

  • actionview/test/template/handlers_test.rb に、Ractor-shareable な動作や新たな登録ロジックを検証するテストが追加。
  • actionview/test/template/render_test.rb は、挙動変更に伴う小さな修正のみ。
  • tools/strict_warnings.rb でも、今回の実装変更で発生する警告に対応するための微修正が入っています。

  1. 影響範囲・注意点

3-1. Ractor/並行実行まわり

  • テンプレートハンドラのレジストリとハンドラオブジェクトが Ractor-shareable になったことで、
    • 将来の Action View の Ractor 対応
    • マルチ Ractor でのレンダリング・テンプレート解決
      への布石となります。
  • 「読み取り時に書き込みが発生しない」ようになったため、従来まれに起こり得た競合や予期しない挙動がさらに減ります。

3-2. カスタムテンプレートハンドラを定義しているアプリケーションへの影響

アプリケーション側で Proc/lambda ベースのカスタムテンプレートハンドラを登録している場合:

  • 登録処理は ActiveSupport::Ractors.try_shareable_proc を通るようになっています。
  • 一般的なハンドラ(トップレベル定義のメソッドや、外部の Ractor 非共有な状態を閉じていないラムダ)であれば、そのまま問題なく動作するはずです。
  • ただし、以下のようなケースでは Ractor-shareable にならない可能性があります:
    • クラスインスタンス変数やスレッドローカルなど、「Ractor 非共有なオブジェクト」を閉じた Proc
    • ミューテーブルな状態に依存するハンドラ
  • その場合でも動作自体は維持される想定ですが、将来の完全な Ractor 対応の観点からは、
    • ハンドラをできるだけ不変/再入可能に設計する、
    • Ractor 非共有な状態を閉じないようにする
      ことが推奨されます。

3-3. Builder ハンドラの挙動

  • Builder ハンドラは require "builder" のメモ化の仕方が変わりましたが、
    • Ruby の require 自体が多重呼び出しに耐性があること、
    • 実際には defined? によるガードで一度だけ読まれることから、
  • アプリケーション側から見た挙動に変化は基本的にありません。

  1. 参考情報 (あれば)
  • この PR で導入・利用されている Ractor 関連 API:
    • ActiveSupport::Ractors.shareable_lambda
    • ActiveSupport::Ractors.try_shareable_proc
  • 関連しそうなドキュメント/コード:
    • actionview/lib/action_view/template/handlers.rb
    • actionview/lib/action_view/template/handlers/builder.rb
    • actionview/test/template/handlers_test.rb
      → 新しいレジストリ実装の実例として読むと理解しやすいです。

#57916 Treat a Symbol stream name as a literal stream in channel test assertions

マージ日: 2026/6/30 | 作成者: @55728

  1. 概要 (1-2文で)
    Action Cable のチャンネル用テストヘルパ (ActionCable::Channel::TestCase) において、シンボルで指定したストリーム名を本番動作と同じく「文字列に対応するリテラルなストリーム名」として扱うように修正した PR です。これにより、:my_room"my_room" を使ったストリーム/アサーションの挙動が一貫し、誤検知・見逃しが起きないようになります。

  1. 変更内容の詳細

背景: 本番環境での挙動

本番では以下のようなコードは:

ruby
stream_from :my_room

内部的には "my_room" というリテラルなストリーム名に対して購読します。
つまり:

ruby
stream_from :my_room
stream_from "my_room"

は同じストリームを指します。

また ActionCable.server.broadcast も、シンボル・文字列を同等に扱うストリーム名としてサポートしています。

問題となっていたテストアサーションの挙動

ActionCable::Channel::TestCase の以下のアサーションだけが、この仕様とズレていました:

  • assert_broadcasts(:my_room)
  • assert_broadcast_on(:my_room)
  • assert_has_stream(:my_room)
  • assert_has_no_stream(:my_room)

1. assert_broadcasts / assert_broadcast_on の問題

シンボルで書いたときだけ、チャンネルスコープ付きの名前 に変換されていました。

ruby
assert_broadcasts(:my_room, 1) do
  ActionCable.server.broadcast("my_room", { text: "hi" })
end
# エラーメッセージ:
# 1 broadcasts to my_channel:my_room expected, but 0 were sent.
  • 実際に送っているのは "my_room" へのブロードキャスト
  • しかしアサーション側は my_channel:my_room という別のストリームを見てしまう
    → その結果、「0件しか送られていない」と誤判定。

文字列の場合は元々正しく動いており:

ruby
assert_broadcasts("my_room", 1) do
  ActionCable.server.broadcast("my_room", { text: "hi" })
end
# これは成功していた

2. assert_has_stream / assert_has_no_stream の問題

類似の問題がストリーム存在チェック系にもありました。

ruby
# 実際には stream_from :my_room が有効
assert_has_stream(:my_room)
# => "Stream my_room has not been started" で失敗

逆に:

ruby
# 実際には stream_from :my_room が有効
assert_has_no_stream(:my_room)
# => 何も検出できず成功してしまう(false green)

これは #57698 で指摘された assert_no_broadcasts と同じく、「テストが緑なのに本当は何も検証できていない」という危険な失敗パターンです。

具体的な修正内容

actioncable/lib/action_cable/channel/test_case.rb にて、以下のようなポリシーに統一:

シンボルで指定されたストリーム名は、テストアサーション内でも 等価な文字列として扱う

つまり:

  • :my_room"my_room" に変換し、チャンネルスコープを付けず にそのまま使う
  • これにより本番の stream_from / broadcast と同じストリーム名解釈となる

結果として、次のように書いたとき:

ruby
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from :my_room
  end
end

テストで:

ruby
assert_has_stream :my_room    # OK
assert_has_stream "my_room"   # OK

assert_broadcasts :my_room, 1 do
  ActionCable.server.broadcast(:my_room, text: "hi")
end

assert_broadcasts "my_room", 1 do
  ActionCable.server.broadcast("my_room", text: "hi")
end

といった書き方がいずれも期待どおり動きます。

テストの追加

actioncable/test/channel/test_case_test.rb に45行分のテストが追加され、主に以下をカバーしています:

  • シンボル指定の assert_broadcasts / assert_broadcast_on が、文字列指定と同じストリームを検証すること
  • シンボル指定の assert_has_stream / assert_has_no_stream も、実際のストリーム開始状況と一致して判定されること
  • 既存の文字列パスが壊れていないこと

  1. 影響範囲・注意点
  • 対象: Action Cable のチャンネルテスト (ActionCable::Channel::TestCase) を書いているコードベース

  • 本番挙動への影響: なし(本番とテストのズレを埋める変更)

  • 破壊的変更の可能性:

    • これまで「たまたま」緑になっていたテストが、本来の挙動に合わせてレッドになる 可能性があります。
    • 特に、以下のようなコードを書いていたプロジェクトは要注意です:
      ruby
      # 実際には my_channel:my_room にしかブロードキャストしていない
      stream_from "my_channel:my_room"
      
      # しかしテストで :my_room を想定していた
      assert_broadcasts :my_room, 1 do
        ActionCable.server.broadcast("my_channel:my_room", payload)
      end
      以前は「たまたま」 :my_roommy_channel:my_room にスコープされて通っていたものが、
      今後は "my_room" を指すようになるため失敗します。
    • その場合は、明示的に望むストリーム名を文字列で指定する か、
      テストと実装のストリーム名を揃える(:my_room / "my_room" いずれかに統一)必要があります。
  • ベストプラクティス:

    • 本番コードでシンボルを使っているなら、テストも同じシンボル/文字列に揃える
    • 「チャンネル名を含んだフルネーム」をあえて使いたいなら、必ず文字列で明示する:
      ruby
      stream_from "chat_channel:my_room"
      assert_broadcasts "chat_channel:my_room", 1 do
        # ...
      end

  1. 参考情報 (あれば)
  • PR本体: #57916 “Treat a Symbol stream name as a literal stream in channel test assertions”
  • 関連PR:
    • #57698: テストアダプタの assert_no_broadcasts におけるシンボル/文字列非対称問題の修正
    • #57703, #57704: 同じファイル内の周辺レイヤーでの、シンボル vs 文字列非対称を個別に修正した PR(本PRで統合)

#57912 Reference ActionCable::Configuration in the Action Cable guide

マージ日: 2026/6/30 | 作成者: @55728

  1. 概要 (1-2文で)
    Action Cable ガイド内の「Configuration」セクションで参照していたクラス名を、古い ActionCable::Server::Configuration から現在の正規名 ActionCable::Configuration に修正したドキュメント更新です。バックワードコンパチ目的のエイリアスは残っていますが、ガイド上は新しい正規クラス名を指すようになりました。

  1. 変更内容の詳細

対象ファイル:

  • guides/source/action_cable_overview.md

変更内容は 1 行のみで、Action Cable の設定項目一覧へ誘導する箇所のクラス名を修正しています。

従来 (概念的なイメージ):

md
See `ActionCable::Server::Configuration` for the full list of configuration options.

修正後:

md
See `ActionCable::Configuration` for the full list of configuration options.

つまり、ドキュメント中で「全設定オプションの一覧は ActionCable::Server::Configuration を見てください」と案内していたのを、「ActionCable::Configuration を見てください」という案内に更新しただけの変更です。

Rails 本体側では、ActionCable::Configuration がトップレベルの正規クラスであり、ActionCable::Server::Configuration は互換性維持用のエイリアスとして残っている状態になっている前提です。この PR は、その実装にドキュメントを追従させたものです。


  1. 影響範囲・注意点
  • 影響対象:
    • 公式ガイドを見て設定方法を確認する開発者
      ガイドに書かれているクラス名が、実際のコードベースが採用している正規の名前 (ActionCable::Configuration) と一致するようになります。
  • 実コードへの影響:
    • コードの挙動や API 仕様そのものは変更されていません。
    • ActionCable::Server::Configuration は引き続きエイリアスとして存在するため、これを参照している既存コードが壊れることはありません。
  • 注意点:
    • 新規実装やリファクタリング時には、今後は ActionCable::Configuration を前提にコードやドキュメントを書くのが推奨されます。
    • 既存の記事・ブログ・社内ドキュメントなどで ActionCable::Server::Configuration を紹介している場合、Rails の現行バージョンに合わせて表記だけ ActionCable::Configuration に更新するのが望ましいです。

  1. 参考情報 (あれば)
  • Action Cable ガイド (Configuration セクション):
    guides/source/action_cable_overview.md の設定セクション
  • 関連する実装 (推定箇所):
    • ActionCable::Configuration クラス定義
    • ActionCable::Server::Configuration = ActionCable::Configuration のようなエイリアス定義 (互換性維持用)

#57928 Eliminate a useless frame in AS::Notifications.instrument

マージ日: 2026/6/30 | 作成者: @byroot

  1. 概要 (1-2文で)
    ActiveSupport::Notifications.instrument 内の「無駄なスタックフレーム」を取り除き、スタックトレースやフレームグラフが少し読みやすくなるようにした変更です。性能への影響はほぼありませんが、デバッグやプロファイリング時の視認性が向上します。

  1. 変更内容の詳細

対象ファイル:

  • activesupport/lib/active_support/notifications.rb (+2/-2)

ActiveSupport::Notifications.instrument の実装から、実行上ほとんど意味を持たない「余計なメソッド呼び出し」や「余計なブロックラップ」を削り、スタックフレームを 1 段減らしています。

典型的には、以下のような構造になっていたものを:

ruby
def instrument(name, payload = {})
  if notifier.listening?(name)
    _instrument(name, payload) do
      yield
    end
  else
    yield
  end
end

def _instrument(name, payload)
  # 実際の計測・通知処理
end

これを、余計な中継レイヤをなくすような形に寄せています:

ruby
def instrument(name, payload = {})
  if notifier.listening?(name)
    # ここで直接計測処理を行う or 余計なラップを減らす
    notifier.start(name, payload)
    begin
      yield
    ensure
      notifier.finish(name, payload)
    end
  else
    yield
  end
end

実際の差分は +2/-2 行と非常に小さいもので、主な意図は「メソッドのネストを 1 段浅くする」「ブロック経由の無駄なコールを消す」ことで、結果的に:

  • 例外発生時のバックトレースに出てくるフレームが 1 つ減る
  • stackprof / rbspy / flamegraph などのプロファイリングツールでのコールスタック表示が少し簡潔になる

という効果があります。


  1. 影響範囲・注意点
  • 公開 API (ActiveSupport::Notifications.instrument) の挙動やシグネチャには変更なし
  • 通知されるイベント名・payload・タイミングも同一で、機能的な互換性は保たれています
  • 変更はごく小さく、かつ「中継用のフレーム削除」にとどまるため、通常のアプリケーションコードが影響を受ける可能性はほぼありません
  • backtrace を前提にした特殊な解析ツールや、スタックフレーム数に強く依存しているようなテストを組んでいる場合のみ、フレーム 1 つ分の違いが出る可能性があります

  1. 参考情報 (あれば)

#57919 Make LogSubscriber.log_levels ractor safe

マージ日: 2026/6/30 | 作成者: @andrewn617

  1. 概要 (1-2文で)
  • ActiveSupport::LogSubscribersubscribe_log_level が Ractor(マルチスレッド/マルチ Ractor)環境でも安全に動作するように、内部で持っている log_levels ハッシュをフリーズするようにした PR です。
  • これにより、Ractor 間で LogSubscriber を共有してもログレベル設定が原因でエラーやデータ競合が起きにくくなります。

  1. 変更内容の詳細

変更の主旨

元々、LogSubscriber には以下2点がありました:

  • ログレベル判定用の定数 LEVEL_CHECKS はすでに freeze 済み
  • しかし、各サブスクライバごとに保持している log_levels ハッシュはミューテーブルであり、Ractor セーフではない

この PR では、subscribe_log_level 呼び出し時に、この log_levels をフリーズするようにしています。すなわち、

  • ハッシュを更新してから freeze する
  • あるいは更新結果として新しい frozen ハッシュを持つようにする

という形を取ることで、Ractor が要求する「オブジェクトが不変であること」の条件を満たします。

具体的なイメージ

元のコード (イメージ):

ruby
def subscribe_log_level(*log_levels)
  # self.log_levels は Hash
  self.log_levels.merge!(derived_log_levels_from_args(log_levels))
end

Ractor では、Ractor 間で共有されるオブジェクトは基本的に frozen? である必要があるため、このままだと:

  • ある Ractor で log_levels を更新
  • 別の Ractor と共有しているときに Ractor セーフではなくなる

といった問題が起こりえます。

この PR では、以下のような形の変更がなされていると考えられます(正確なコードは PR 本文では省略されていますが、内容からの推測です):

ruby
def subscribe_log_level(*log_levels)
  self.log_levels = self.log_levels.merge(derived_log_levels_from_args(log_levels)).freeze
end

ポイント:

  • merge! のような破壊的更新ではなく、新しい Hash を merge で生成
  • 生成した Hash を freeze
  • log_levels には常に frozen Hash を持たせるようにする

テスト (activesupport/test/log_subscriber_test.rb) も追加されており:

  • subscribe_log_level 実行後に log_levelsfrozen? であること
  • 複数回呼び出しても正しいマージ結果と frozen 状態が保たれていること

といったことが検証されていると考えられます。


  1. 影響範囲・注意点
  • 影響を受けるのは ActiveSupport::LogSubscribersubscribe_log_level を利用しているコードのみです。

  • log_levels が常に frozen Hash になるため、公開 API から log_levels を直接破壊的に書き換えていた場合はエラーになります

    例(これまでなんとなく動いていたが、今後は壊れる可能性があるコード):

    ruby
    subscriber = MyLogSubscriber.new
    subscriber.subscribe_log_level(:info)
    
    # こういった破壊的更新は frozen になった Hash に対しては例外を投げる
    subscriber.log_levels[:debug] = true     # => FrozenError
    subscriber.log_levels.merge!(...)        # => FrozenError

    望ましい書き方としては、subscribe_log_level の API を通して設定するか、どうしても差し替えたい場合は新しい Hash を作って setter に渡す形です:

    ruby
    subscriber.log_levels = subscriber.log_levels.merge(debug: true).freeze
    # ただし、このような使い方自体も将来の仕様変更に脆いので、可能なら避ける
  • LogSubscriber を Ractor 間で共有するケース(あるいは Rails ロガー周りを Ractor 内で使うケース)では、今回の変更により Ractor セーフティが向上します。

  • CHANGELOG の更新は行われていないため、「挙動変更」というよりは「安全性向上のための内部的変更」という扱いになっています。


  1. 参考情報 (あれば)
  • Ruby の Ractor 仕様上、Ractor 間で共有されるオブジェクトは原則として immutable(freeze)である必要があります。
  • 以前から LEVEL_CHECKS 定数は freeze 済みでしたが、今回の PR によって log_levels も同様に不変化され、LogSubscriber 全体として Ractor フレンドリーな設計に近づいています。
  • マルチ Ractor 対応を見据えた Rails の内部改善の一環と位置付けられます。

#57920 Eagerly assign ExecutionWrapper's active key

マージ日: 2026/6/30 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    ExecutionWrapper のサブクラスにおける active_key の代入タイミングを「継承時」に早めることで、最初のリクエストが非メイン Ractor で処理された場合に発生する Ractor の隔離エラーを防ぐ修正です。Ractor 対応を強化するための内部的な初期化順序の見直しになります。

  1. 変更内容の詳細

背景

Ruby の Ractor では、「メイン Ractor 以外からクラスインスタンス変数を初期設定する」といった操作に制約があり、適切に扱わないと「ractor isolation error(Ractor の隔離違反)」が発生します。
ExecutionWrapper は Rails の実行コンテキスト(ミドルウェア的な周辺処理)を扱うためのクラスで、そのサブクラスごとに active_key というクラスインスタンス変数を持ちます。

従来は、この active_key が「必要になったタイミング(=リクエスト処理時など)」で初期化されていたため、最初のリクエストがメイン Ractor ではない Ractor で処理されるケースで Ractor の制約に引っかかる可能性がありました。

今回の変更

  • active_key の代入を 「サブクラス定義時 (継承フック inherited 内)」に移動 しました。
  • これにより、ExecutionWrapper を継承したクラスがロードされた瞬間、まだメイン Ractor 上にいる段階で active_key が確実にセットされます。

実装イメージ(簡略化した擬似コード):

ruby
class ActiveSupport::ExecutionWrapper
  class << self
    # もともとどこか別のタイミングで行っていた active_key 代入を
    # 継承時フックに移したイメージ
    def inherited(subclass)
      super
      subclass.instance_variable_set(:@active_key, :"active_#{subclass.name}")
    end
  end
end

※上記はあくまで概念的なサンプルです。実際のキー名や代入方法はコードに依存しますが、ポイントは「inherited フック内でサブクラスのクラスインスタンス変数をセットする」ことです。


  1. 影響範囲・注意点
  • 主な影響範囲
    • ActiveSupport::ExecutionWrapper を継承しているクラス(およびそれを利用するコード全般)
    • マルチ Ractor 環境で Rails を動かしているアプリケーション
  • 期待される効果
    • 最初のリクエストがメイン Ractor 以外で処理された場合でも、active_key に関する Ractor 隔離エラーを防げる。
    • ExecutionWrapper の初期化がより決定的(deterministic)になり、Ractor 利用時の不定動作の可能性が下がる。
  • 互換性・既存コードへの影響
    • active_key の値や外部 API が変わるわけではなく、「いつセットされるか」だけが変わるため、通常のアプリケーションコードへの影響はほぼありません。
    • ExecutionWrapper のサブクラス化や @active_key に対してメタプログラミングを行っているような高度なコードがある場合は、初期化タイミングの違いに注意してください(ただしそのようなケースは稀と思われます)。
  • パフォーマンス
    • 継承時にクラスインスタンス変数を一つセットするだけなので、事実上無視できるレベルのオーバーヘッドです。

  1. 参考情報 (あれば)
  • Ruby Ractor の制約として、「Ractor 間で共有されるオブジェクトや、Ractor 境界を跨いだミューテーション」に厳しい制約があり、クラスインスタンス変数の扱いにも注意が必要になります。
  • inherited フックは Ruby における標準的なメタプログラミングフックで、class Child < Parent; end のようにサブクラスが定義されたときに Parent.inherited(Child) が呼ばれます。
    今回はこの仕組みを利用して、「サブクラスロード時に Ractor セーフな形で内部状態 (active_key) を初期化する」ようにしています。