Ruby on Rails PR Digest - 2026年 6月
このページは rails/rails リポジトリにマージされたPull Requestを自動的に収集し、AIで要約したものです。
#57913 Keep nil elements named in the series for in_order_of
マージ日: 2026/6/29 | 作成者: @55728
- 概要 (1-2文で)
Enumerable#in_order_ofをfilter: true(デフォルト)で使ったとき、ソート順を指定する配列(series)にnilが含まれていても、これまでは結果からnilが落ちていましたが、PRにより明示的に指定されたnilを結果にも残すように修正されました。これにより、filter: trueとfilter: falseでの挙動がnilの扱いに関して一貫します。
- 変更内容の詳細
何が問題だったか
Enumerable#in_order_of は、指定したキーと順序リスト(series)に従って要素を並べ替えるメソッドです。
[3, nil, 1, 2].in_order_of(:itself, [1, nil, 2, 3])
# 期待される結果: [1, nil, 2, 3]しかし、これまでデフォルトの filter: true では、series に nil を含めていても結果から nil が削除されていました。
[3, nil, 1, 2].in_order_of(:itself, [1, nil, 2, 3])
# Before: [1, 2, 3] # nil が落ちる一方で、filter: false では nil を正しく保持していました。
[3, nil, 1, 2].in_order_of(:itself, [1, nil, 2, 3], filter: false)
# => [1, nil, 2, 3]この不整合を解消するのが今回の変更です。
具体的な変更点
対象ファイル:
activesupport/lib/active_support/core_ext/enumerable.rbin_order_ofの内部実装が 1 行修正され、filter: trueのときでも、series に明示的に含まれているnilを結果から除外しないような条件(またはフィルタ処理)が調整されています。- 元々「filter: true のときは series に含まれない値を結果から落とす」というロジックがありますが、その過程で
nilも一緒に落ちていたため、series 上のnilは特別扱いして保持するようにしたと考えられます。
テスト追加:
activesupport/test/core_ext/enumerable_test.rbfilter: trueでもnilを含む series を正しく再現するテストが 5 行分追加。想定されるテストイメージ(PR説明から推測される形):
rubydef test_in_order_of_preserves_nil_with_filter_true assert_equal( [1, nil, 2, 3], [3, nil, 1, 2].in_order_of(:itself, [1, nil, 2, 3]) ) end
修正後の挙動
[3, nil, 1, 2].in_order_of(:itself, [1, nil, 2, 3])
# After: [1, nil, 2, 3]filter: true(デフォルト)でも、series に含まれているnilが結果に現れるようになりました。filter: falseの挙動はもともと正しかったため、そのまま維持され、両者でnilの扱いが揃います。
- 影響範囲・注意点
挙動の変化(互換性)
- これまで Rails の
in_order_ofを利用していて、filter: trueかつ series にnilを含めていた場合、結果からnilが落ちている前提でコードを書いていたとすると、今回の変更により結果にnilが含まれるようになります。 - 特に「
nilは来ないはず」と決め打ちしている部分(例:compactしていないループ処理や、nil非許容の変換処理など)は、アップグレード時に再確認が必要です。
- これまで Rails の
意図的な使い方との整合性
- series に
nilを書いている時点で「この位置にnilも並べ替え対象として含めたい」という意図が明確なため、今回の修正はより直観的で一貫した仕様になっています。 - もし本当に
nilを結果から除外したいのであれば、series にnilを含めない、あるいは呼び出し側でcompactするなどの明示的な処理を行うべき、という方向に仕様が整理されたといえます。
- series に
filter オプションの意味
filter: trueは「series に存在しない値を結果から落とす」というオプションであり、「nil を落とす」という意味ではない、という点が今回の変更でより明確になります。- 「series にないものだけを除外し、series に書いたもの(
nilを含む)は尊重する」というルールとして理解すると扱いやすいです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57913
- 関連する API:
Enumerable#in_order_of(Active Support 拡張)典型的な使い方例:
rubyrecords = User.where(id: [3, nil, 1, 2]) # 実例では nil は通常含まれないが、値としての nil を想定した例 order = [1, nil, 2, 3] records.in_order_of(:id, order) # order の順に並び替え、今回の変更により nil も order 通りに扱われる
Active Support の Enumerable 拡張全般は
activesupport/lib/active_support/core_ext/enumerable.rbに実装されており、in_order_ofもそこに含まれます。
#57915 Include call options in Cache#exist? instrumentation payload
マージ日: 2026/6/29 | 作成者: @55728
- 概要 (1-2文で)
Rails.cache.exist?実行時に発火するcache_exist?.active_supportの通知ペイロードに、これまで含まれていなかったオプション引数(namespace, version, expires_in など)が含まれるようになりました。これにより、他のキャッシュ操作(read/write/delete/fetch_hit)と同じ情報が計測・ロギングで利用できます。
- 変更内容の詳細
何が変わったか
- 対象の通知イベント:
cache_exist?.active_support - 変更前:
Rails.cache.exist?("key", namespace: "foo")を呼び出しても、通知イベントのevent.payloadには:options(およびその中の:namespaceなど)が含まれていなかった。 - 変更後:
exist?呼び出し時に渡されたオプションが、そのまま計測用ペイロードに流れるようになった。
PR 説明のコード例をベースにすると、以下のような変化があります。
ActiveSupport::Notifications.subscribe("cache_exist?.active_support") do |event|
# 変更前
event.payload[:key] # => "key"
event.payload[:store] # => #<ActiveSupport::Cache::Store ...>
event.payload[:namespace] # => nil (オプションが渡されていない)
# 変更後
event.payload[:key] # => "key"
event.payload[:store] # => #<ActiveSupport::Cache::Store ...>
event.payload[:namespace] # => "foo" # options が反映される
end
Rails.cache.exist?("key", namespace: "foo")内部的には、ActiveSupport::Cache の計測処理で exist? に渡されたオプション引数をペイロードに追加しているだけで、キャッシュの挙動(ヒット/ミスの判定やキャッシュストアの動き)は変わっていません。
他の操作との一貫性
- すでに以下のイベントのペイロードにはオプションが含まれています:
cache_read.active_supportcache_write.active_supportcache_delete.active_supportcache_fetch_hit.active_support
- 今回の変更で
cache_exist?.active_supportも同様にオプションを含むため、計測/ロギング/トレース処理をこれらイベント間で共通化しやすくなります。
テストの追加
activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb にテストが追加され、exist? 実行時の通知ペイロードにオプションが含まれることが検証されています。
内容としては、おおよそ次のようなことを確認しています:
Rails.cache.exist?をオプション付きで呼んだとき、- 発火した
cache_exist?.active_supportイベントのペイロードに、そのオプションが含まれていること。
- 影響範囲・注意点
互換性
- 破壊的変更ではありません。既存コードはそのまま動作します。
- すでに
cache_exist?.active_supportを購読しているコードは、追加情報(options/namespace/version/expires_in など)を新たに利用できるようになります。
モニタリング/ロギングへの影響
exist?のイベントペイロードをログ出力している場合、出力内容にオプションが増える可能性があります。- namespace や version ごとのキャッシュ利用状況をトレースしやすくなります。
- カスタムメトリクス収集(Prometheus, Datadog など)で、他の cache_* イベントと同様の次元(タグ)を付与しやすくなります。
パフォーマンス
- 追加されるのは Hash の受け渡しのみで、非常に軽微な変更です。パフォーマンス影響はほぼ無視できるレベルと考えてよいです。
セキュリティ/プライバシー
- もともとオプションに機密情報を載せている場合、それが通知経由で外部ロガーや APM に送られやすくなります。
namespaceや独自オプションに機微な情報を入れているプロジェクトは、通知購読側でマスキングやフィルタリングを検討してください。
- 参考情報 (あれば)
- 関連するイベントの一覧(ActiveSupport::Notifications 経由で購読可能):
"cache_read.active_support""cache_write.active_support""cache_delete.active_support""cache_exist?.active_support""cache_fetch_hit.active_support"
- 利用例:
ActiveSupport::Notifications.subscribe(/cache_.*\.active_support/) do |name, start, finish, id, payload|
# name: "cache_exist?.active_support" など
# payload 例:
# {
# key: "key",
# store: #<ActiveSupport::Cache::Store ...>,
# namespace: "foo",
# expires_in: 60,
# version: "v1",
# # など…
# }
endこの PR により、exist? についても他の cache 操作と同じ粒度で計測・分析ができるようになっています。
#57911 Allow Arel to be required on its own
マージ日: 2026/6/29 | 作成者: @55728
- 概要 (1-2文で)
require "arel"を Active Support なしで実行するとActiveSupport::RactorsのNameErrorが発生していた問題を、Arel 側で明示的にactive_support/ractorsを読み込むことで解消した PR です。Arel を Active Record 抜きで単独利用するユースケースが再び正常に動くようになります。
- 変更内容の詳細
問題の内容
以下のように、Active Record/Active Support を事前に読み込まずに Arel だけを require するとエラーになっていました。
$ ruby -I activerecord/lib -e 'require "arel"'
.../arel/visitors/to_sql.rb:768:in '<class:ToSql>': uninitialized constant Arel::Visitors::ToSql::ActiveSupport (NameError)
BIND_BLOCK = ActiveSupport::Ractors.shareable_proc { "?" }
^^^^^^^^^^^^^原因は次の通りです。
Arel::Visitors::ToSqlおよびArel::Visitors::PostgreSQLのクラス定義時にActiveSupport::Ractorsを参照しているActiveSupport::Ractorsは Active Support によって autoload されるため、「Active Record 経由で Arel をロードする」場合は問題にならない- しかし
require "arel"単体では Active Support がロードされていないため、ActiveSupport定数が未定義でNameErrorになる
対応内容
activerecord/lib/arel.rb で、Arel ロード時に active_support/ractors を明示的に require するようにしました。
変更イメージ:
# activerecord/lib/arel.rb
# 既存の require 群のどこかに追記
require "active_support/ractors" # ← これを追加これにより:
require "arel"した時点でActiveSupport::Ractorsが定義されるArel::Visitors::ToSql/PostgreSQLのクラス定義内でActiveSupport::Ractorsを参照してもNameErrorにならない
テストの追加
activerecord/test/cases/arel/require_test.rb が新規追加されています。
テストのポイント:
- 新しいプロセスを起動し、Active Support を事前にロードしない状態で
require "arel"を実行 - ロードに成功し、例外が発生しないことを確認
- gem のロードパスのみ有効にした環境を再現しているため、「ライブラリが Arel を直接 require する」現実的ユースケースをカバー
サンプルとなるテストのイメージ(擬似コード):
def test_require_arel_without_activesupport
# Bundler / gem path だけ有効にしたサブプロセスを起動し
# ActiveSupport を require せずに arel だけ require しても落ちないことを確認
assert_subprocess_success <<~RUBY
require "arel"
RUBY
end- 影響範囲・注意点
影響を受ける利用者
- Arel を Active Record 抜きで直接使っており、かつプロセス起動直後に
require "arel"しているようなライブラリ/アプリケーション- 例: SQL を Arel で構築・解析するミドルウェアやツール(
arsiなど)
- 例: SQL を Arel で構築・解析するミドルウェアやツール(
- Arel を Active Record 抜きで直接使っており、かつプロセス起動直後に
期待される改善
- これらのライブラリが、Rails 本体のロード状況に依存せずに
require "arel"だけで動くようになる - Edge Rails で
ActiveSupport::Ractors参照部分が増えたことによって壊れていたテストスイート(報告例:arsi)が再び通る
- これらのライブラリが、Rails 本体のロード状況に依存せずに
Active Support への依存の明示化
- 実質的に Arel が Active Support(少なくとも
active_support/ractors)に依存していることがコード上でも明示されました - すでに実装上依存していたものを require で追従した形なので、実質的な依存関係は変わっていません
- 実質的に Arel が Active Support(少なくとも
注意点
- 「Arel は ActiveSupport 非依存であるべき」と考えている場合、この PR により依存がより表面化したと言えます
- ただし現状、
ActiveSupport::Ractors.shareable_procを利用している限り、依存自体は避けられないため、現実的な修正です active_supportのロード順に依存した微妙なバグ(例:ActiveSupport::Ractorsの再定義など)がもしあれば影響しうるものの、そのような使い方は通常想定されません
- 参考情報 (あれば)
- 元 issue: rails#57840
arsi(https://github.com/zendesk/arsi) のテストが edge Rails でrequire "arel"に失敗し始めたことから発覚
- この PR により、「Arel は Active Record とは別に require できる」ことが再び保証されるため、Arel 直利用のライブラリは edge Rails 追随時にこの修正が入っているか確認すると良いです。
#57907 Return a composite has_and_belongs_to_many association_foreign_key as an array
マージ日: 2026/6/29 | 作成者: @ollym
- 概要 (1-2文で)
has_and_belongs_to_manyで複合キーを指定した場合、reflection から取得できるassociation_foreign_keyが、文字列化された 1 つの値ではなく、foreign_keyと同様に「カラム名文字列の配列」として返るように修正された PR です。これにより、複合 HABTM を introspection(メタ情報の取得)から正しく扱えるようになります。
- 変更内容の詳細(あればサンプルコードも含めて)
2-1. 問題点
has_and_belongs_to_many は元々、オプションとして複合キーを受け取ることができます:
has_and_belongs_to_many :tags,
foreign_key: [:blog_id, :post_id],
association_foreign_key: [:tag_id, :tenant_id]このとき:
Reflection#foreign_key
→[:blog_id, :post_id]を受け取って["blog_id", "post_id"](文字列の配列)を返す
一方で:
Reflection#association_foreign_key
→ オプション値にto_sを呼んでいたため[:tag_id, :tenant_id]→"[:tag_id, :tenant_id]"という 1 つの文字列になっていた
つまり、実際の関連自体は正常に動くが、reflection API が複合キーの情報を正しく表現できていないという不一致がありました。
複合 HABTM の定義を introspection したいコード(メタプログラミングやジェネレータなど)から見ると、foreign_key は配列なのに association_foreign_key は謎の 1 つの文字列という、扱いづらい状態になっていました。
関連 issue: #57908
2-2. 修正内容
ActiveRecord::Reflection#association_foreign_key の実装を foreign_key と揃え:
ActiveRecord::Key.for(...).nameを通して名前を解決するように変更しました。具体的には:
- 以前: オプション値に対して
to_sしていた - 今回:
ActiveRecord::Key.for(option_value).nameを使う
その結果:
スカラキー(単一カラム)の場合は、従来どおり単一のカラム名文字列:
rubyhas_and_belongs_to_many :tags, association_foreign_key: :tag_id reflection.association_foreign_key # => "tag_id"複合キーの場合は、
foreign_keyと同様に「カラム名文字列の配列」:rubyhas_and_belongs_to_many :tags, foreign_key: [:blog_id, :post_id], association_foreign_key: [:tag_id, :tenant_id] reflection.foreign_key # => ["blog_id", "post_id"] reflection.association_foreign_key # => ["tag_id", "tenant_id"]
2-3. テスト追加
以下のような内容が追加されています:
Sharded::BlogPostモデルに、複合キーを使ったhas_and_belongs_to_manyを追加- 例:
has_and_belongs_to_many :tags, foreign_key: [...], association_foreign_key: [...]といった定義
- 例:
- reflection テストで、複合 HABTM に対して:
reflection.foreign_keyが配列を返すことreflection.association_foreign_keyも配列を返すこと を明示的にアサート
これにより、複合キー対応の一貫性(#57904 / #57906 の composite belongs_to 修正ラインの延長)が担保されています。
- 影響範囲・注意点
影響するのは reflection の戻り値のみ
実際の関連の読み書き(レコードの保存・取得)は元々動いており、今回の変更でもその挙動は変わりません。
変わるのはActiveRecord::Reflection経由で取得するメタ情報です。複合
has_and_belongs_to_manyを introspect しているコードへの影響
もし既存コードが以下のような前提で動いていた場合は、挙動が変わります:association_foreign_keyは常にStringである- 複合キー指定時でも
"[:tag_id, :tenant_id]"といった 1 つの文字列をそのままパースして使っている
このようなコードは、今後は 配列 or 文字列の両方に対応する必要があります:
rubyafk = reflection.association_foreign_key afk = Array(afk) # 単一でも複数でも配列に正規化 # afk は ["tag_id"] あるいは ["tag_id", "tenant_id"] のような形式新しい期待値
今後の Rails では:- HABTM + 単一キー:
reflection.foreign_key→"..."(String)reflection.association_foreign_key→"..."(String) - HABTM + 複合キー:
reflection.foreign_key→["...", "..."](String の配列)reflection.association_foreign_key→["...", "..."](String の配列)
という形で、
foreign_key/association_foreign_keyの仕様が揃います。- HABTM + 単一キー:
- 参考情報 (あれば)
- PR: #57907 — Return a composite has_and_belongs_to_many association_foreign_key as an array
- 関連 Issue: #57908 —
association_foreign_keyが複合キー指定時に"[:id1, :id2]"のような String を返してしまう問題 - 関連 PR:
- #57904 / #57906 — composite
belongs_to周りの修正・一貫性向上に関する PR
- #57904 / #57906 — composite
- 該当コード:
activerecord/lib/active_record/reflection.rb- テスト:
activerecord/test/cases/reflection_test.rbactiverecord/test/models/sharded/blog_post.rb
#57843 Declare assert_not_pattern alias for refute_pattern
マージ日: 2026/6/28 | 作成者: @seanpdoyle
- 概要 (1-2文で)
Rails のActiveSupport::TestCaseに、Minitest のrefute_patternに対応するエイリアスメソッドassert_not_patternが追加されました。これにより、他のassert_not_*系メソッドと同じ書き方でパターンマッチングの否定アサーションが使えるようになります。
- 変更内容の詳細(あればサンプルコードも含めて)
何が追加されたか
ActiveSupport::TestCaseに以下のエイリアスが定義されました:assert_not_pattern→refute_patternのエイリアス
元々 Active Support は以下のように、Minitest の refute_* 系メソッドに対して assert_not_* のエイリアスを提供していました:
assert_not_equal→refute_equalassert_not_includes→refute_includes- …など
しかし、Minitest 5.18.0 で追加されたパターンマッチング用のアサーション:
assert_patternrefute_pattern
については、Rails 側に assert_not_pattern が存在しておらず、一貫性が欠けていました。この PR はそのギャップを埋えるものです。
利用イメージ
Ruby 3 以降のパターンマッチ (case in) を使ったテストで、
「このオブジェクトはこのパターンにマッチ“しない”こと」をテストしたい場合に:
従来 (Minitest を素で使う書き方):
refute_pattern({ name: "Alice", age: 30 }, { name:, age: 20..29 })Rails/ActiveSupport のスタイルで書けるようになる:
assert_not_pattern({ name: "Alice", age: 30 }, { name:, age: 20..29 })assert_not_* を常用している Rails プロジェクトでも、
書き方の一貫性を保ったままパターンマッチングの否定アサーションを使えます。
変更ファイルの概要
activesupport/lib/active_support/test_case.rbassert_not_patternエイリアスの宣言を追加
activesupport/test/test_case_test.rbassert_not_patternがrefute_patternと同等の動きをするかを確認するテストを追加
activesupport/CHANGELOG.md- 上記追加についての記述を追加
- 影響範囲・注意点
対象:
ActiveSupport::TestCaseを継承している Rails のテストコード全般
影響:
- 新しいメソッド追加であり、既存メソッドのシグネチャや挙動の変更はないため後方互換性は保たれます。
- 既存の
refute_pattern呼び出しはそのまま動作し、新しくassert_not_patternも利用できるようになります。
注意点:
assert_not_pattern/refute_patternの動作自体は Minitest 5.18.0 以降に依存します。Rails 側からは単にエイリアスを提供しているだけなので、Minitest が古い場合はそもそもrefute_patternが存在せず、動作しません。assert_pattern/refute_patternのインターフェイスは Minitest 由来です。使い方自体は Minitest のドキュメントに従う必要があります。
- 参考情報 (あれば)
- Minitest ドキュメント:
- Minitest 5.18.0 リリースノート:
https://github.com/minitest/minitest/blob/master/History.rdoc#5180--2023-03-04 - Rails
assert_not_equal(既存のrefute_equalエイリアス例):
https://edgeapi.rubyonrails.org/classes/ActiveSupport/TestCase.html#method-i-assert_not_equal
#57896 Apply DISTINCT to a grouped sum
マージ日: 2026/6/28 | 作成者: @55728
- 概要 (1-2文で)
Relation#sumにdistinctとgroupを同時に指定した場合に、内部的にSUM(DISTINCT column)ではなく単なるSUM(column)が実行されてしまい、数値が誤っていたバグを修正する PR です。未グループのケースで既に行っていたDISTINCTフラグ適用処理を、グループ化された集計パスにも適用することで挙動を揃えています。
- 変更内容の詳細
何が問題だったか
Rails の ActiveRecord で以下のようなコードを書いたとき:
Account.where(firm_id: 1).distinct.sum(:credit_limit)
# => 110 # SQL: SELECT SUM(DISTINCT "accounts"."credit_limit") FROM ...これは SUM(DISTINCT credit_limit) を実行し、重複値を取り除いた上で合計してくれます。
しかし、group を追加した途端に挙動が変わっていました:
Account.where(firm_id: 1).group(:firm_id).distinct.sum(:credit_limit)
# 期待される挙動(PR適用後):
# => {1 => 110} # firm_id=1 グループ内でも DISTINCT が効いた合計
# PR適用前の挙動:
# => {1 => 160} # 実際には plain SUM になり、重複がカウントされてしまうdistinctを指定しているにもかかわらず、グループ化された集計経路ではDISTINCTが SQL に反映されていなかったことが原因です。countについては、内部実装がcolumn.count(distinct)の形で共通化されていたため、グループ化の有無にかかわらずDISTINCTが効いており、この問題はsum特有でした。
修正内容
activerecord/lib/active_record/relation/calculations.rb に 1 行だけ追加されています。
未グループの
sum計算パスでは、すでに次のようなロジックがありました(意訳):rubyif operation == "sum" && distinct select_value.distinct = true endところが「グループ化あり」のパスにはこの処理が存在せず、
distinctオプションが SQL のSUM(DISTINCT ...)に反映されていませんでした。この PR は、その未グループパスと同様に、グループ化された集計パスでも
operation == "sum" && distinctのときにselect_value.distinct = trueをセットするよう修正しています。
結果として、次のようになります:
# PR 適用後の想定 SQL イメージ
Account.where(firm_id: 1).group(:firm_id).distinct.sum(:credit_limit)
# => SELECT "accounts"."firm_id",
# SUM(DISTINCT "accounts"."credit_limit") AS sum_credit_limit
# FROM "accounts"
# WHERE "accounts"."firm_id" = 1
# GROUP BY "accounts"."firm_id"
#
# 戻り値: {1 => 110}テスト追加
activerecord/test/cases/calculations_test.rb に約 10 行のテストが追加されています。
group+distinct+sumの組み合わせで、SQL レベルのDISTINCTが効いているかを検証するテストが新設され、修正の回帰防止になっています。
- 影響範囲・注意点
対象:
ActiveRecord::Relationに対してrubyrelation.group(:some_column).distinct.sum(:other_column)のように「グループ化 + DISTINCT + sum」を組み合わせて使っているコード全般。
影響内容:
- これまで「重複を含めた合計値」が返っていた箇所で、「重複を除いた合計値」が返るようになります。
- つまり、既存アプリが、実はバグである“重複込みの合計”に依存していた場合、数値が変わる 可能性があります。
- 一方で、API 名 (
distinct.sum) の意味論としては今回の挙動の方が自然であり、group有無で結果が変わるという一貫性の欠如が解消されます。
バージョンアップ時の確認ポイント:
- 分析・レポート・ダッシュボード系コードで、
group(...).distinct.sum(...)を使っている箇所があれば、期待している値が「重複を含めた合計」なのか「重複を除去した合計」なのかを確認してください。 - 同様の処理を DB 生 SQL で書いていて、
SUM(DISTINCT ...)ではなくSUM(...)をあえて使っている場合は、ActiveRecord の方もdistinctを外して挙動を揃える必要があるかもしれません。
- 分析・レポート・ダッシュボード系コードで、
互換性:
- 仕様としては「
distinct.sumがSUM(DISTINCT ...)になる」のが自然であり、かつ未グループのケースとはすでにそうなっていたため、バグ修正による挙動の是正 という位置づけです。 countについてはもともとグループ有無を問わずDISTINCTが効いていたため、この修正による挙動の変化はありません。
- 仕様としては「
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57896
- 関連する ActiveRecord メソッド:
ActiveRecord::Relation#sumActiveRecord::Calculations
- 実際のコードベースでの確認手順例:
Rails コンソールで、現行バージョンとこの修正が入ったバージョンを比較し、問題になりそうなクエリについて
to_sqlを出して差分を確認すると安全です。rubyscope = Account.where(firm_id: 1).group(:firm_id).distinct puts scope.sum(:credit_limit) # 値の変化を確認 puts scope.select('SUM(DISTINCT credit_limit)').to_sql
#57902 Keep HashWithIndifferentAccess#filter returning indifferent access
マージ日: 2026/6/28 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::HashWithIndifferentAccessにおいて、#filter呼び出し時にも「シンボル/文字列を区別しないアクセス(indifferent access)」が維持されるように修正した PR です。これにより、#selectと#filterが同じクラス(HashWithIndifferentAccess)を返し、期待どおりの挙動になります。
- 変更内容の詳細
問題点
Ruby の標準 Hash では #filter は #select のエイリアスですが、HashWithIndifferentAccess では以下のような状態になっていました:
HashWithIndifferentAccess#selectはオーバーライドされており、戻り値もHashWithIndifferentAccessになるようになっている- 一方で
#filterはHashWithIndifferentAccess側でエイリアス定義されておらず、Hash実装の#filterが呼ばれてしまう - その結果、
#filterは「普通の Hash」を返し、indifferent access を暗黙に失っていた
PR に記載されている挙動:
h = ActiveSupport::HashWithIndifferentAccess.new(a: 1, b: 2)
h.select { |k, v| v > 1 }.class # => ActiveSupport::HashWithIndifferentAccess
h.filter { |k, v| v > 1 }.class # => Hash (変更前)
h.select { |k, v| v > 1 }[:b] # => 2
h.filter { |k, v| v > 1 }[:b] # => nil (変更前)上記のように、返り値が Hash になってしまうため、[:b] アクセスでは値を取れず、"b" キーでしか取れない状態になっていました(indifferent access が壊れている)。
対応内容
HashWithIndifferentAccess クラスで、#filter をオーバーライド済みの #select にエイリアスするようにしました。
イメージとしては以下のようなコードが追加されています(実際の記述はクラス定義内):
alias_method :filter, :selectRuby 本体が Hash#filter を Hash#select にエイリアスしているのと同様の関係を、HashWithIndifferentAccess にも適用した形です。
なお、破壊的メソッドは問題なかった
#filter!/#select!はどちらもHashから継承しており、「自分自身を破壊的に変更する」メソッドのため、レシーバのクラス(HashWithIndifferentAccess)は変わらない- そのため、問題があったのは「新しいハッシュを返す非破壊メソッド」の
#filterだけでした
テスト
activesupport/test/hash_with_indifferent_access_test.rb にテストが追加されており、#filter 実行後も HashWithIndifferentAccess であること・indifferent access が維持されることが検証されています。
例:
h = ActiveSupport::HashWithIndifferentAccess.new(a: 1, b: 2)
result = h.filter { |k, v| v > 1 }
assert_instance_of ActiveSupport::HashWithIndifferentAccess, result
assert_equal 2, result[:b]
assert_equal 2, result["b"]- 影響範囲・注意点
主な影響
HashWithIndifferentAccess#filterが、これまでHashを返していたのがHashWithIndifferentAccessを返すようになります。#filterの戻り値に対して[:symbol]と["string"]の両方でアクセスできるようになります(#selectと統一された挙動)。
後方互換性の観点
- 「
#filterの戻り値がHashであること」を前提にしていたコード(例えばresult.is_a?(Hash)かつ!result.is_a?(HashWithIndifferentAccess)を期待しているような処理)は挙動が変わります。 - ただし
HashWithIndifferentAccessはHashのサブクラスであるため、単純にHashであることだけをチェックしている分には問題ありません。 - より実用上のケースとしては、「
#filterの結果が indifferent ではないことを前提に、:keyと"key"を意図的に区別していた」などのニッチなケースでのみ影響します。
- 「
利用者側での注意点
- Rails 7 系などでこの変更が入るバージョンにアップデートする場合、
HashWithIndifferentAccess#filterの戻り値のクラス・挙動が既存コードの前提とずれていないかを確認すると安心です。 - ほとんどの場合は「バグ修正」として歓迎される変更であり、通常の Rails アプリケーションコードへの悪影響はほぼないと思われます。
- Rails 7 系などでこの変更が入るバージョンにアップデートする場合、
- 参考情報 (あれば)
- Ruby 本体の挙動
- Ruby の
Hashクラスでは#filterと#selectは同じメソッド(エイリアス)であり、Hash#selectをオーバーライドするクラスは通常#filterも同様に扱うべき、という設計があります。
- Ruby の
- 関連するメソッドの整理
HashWithIndifferentAccess#select… オーバーライド済み、HashWithIndifferentAccessを返すHashWithIndifferentAccess#filter… 今回#selectのエイリアスになった(非破壊・インデファレント維持)HashWithIndifferentAccess#select!/#filter!…Hashから継承、破壊的なのでクラスは変わらない(もともと問題なし)
#57894 Reverse a relation by its query_constraints when no order is given
マージ日: 2026/6/28 | 作成者: @55728
- 概要 (1-2文で)
query_constraintsを宣言しているモデルに対してreverse_orderを呼んだ際、暗黙的な並び順(first/lastが使う順序)を正しく反転するように修正した PR です。これにより、reverse_order.first == lastが直感通りに成り立つようになり、モデルにquery_constraintsが無い場合の挙動は従来通りです。
- 変更内容の詳細
これまでの問題点
reverse_order は、明示的な order が無い Relation に対しては主キー(通常は id)で並び替える、という動きをしていました。
一方で、query_constraints を宣言しているモデルでは、first / last は「query_constraints で指定したカラム」を使って暗黙的な並び順を決めます。
例:
class ClothingItem < ActiveRecord::Base
query_constraints :clothing_type, :color
endこの場合:
ClothingItem.first/ClothingItem.lastはORDER BY clothing_type ASC, color ASC/DESC(相当)でレコードを取得する- しかし
ClothingItem.all.reverse_orderは、ORDER BY id DESCのように「主キーだけ」を反転していた
そのため、以下が成り立ちませんでした:
ClothingItem.all.reverse_order.first == ClothingItem.last
# 期待: true
# 実際(修正前): falsePR の修正内容
この PR では、「明示的な order が無い Relation を reverse_order したときのフォールバックの並び順」を、query_constraints_list をベースに組み立てるように変更しています。
具体的には:
- これまで:
- 暗黙順序の反転に 主キーのみ を使用
- これから:
- モデルに
query_constraintsが定義されていればquery_constraints_listに含まれるカラムを使ってORDER BY ... DESCを構築 query_constraintsが無ければ、従来通り主キーを使う
- モデルに
コードイメージ(説明用・擬似コード):
# 修正後のイメージ
if relation.arel.orders.empty?
columns = query_constraints_list || Array(primary_key)
relation = relation.order(columns.map { |c| arel_table[c].desc })
else
# 既存の order を単純反転 (従来通り)
endPR 説明にある動作:
ClothingItem.all.reverse_order.to_sql
# 修正前:
# ... ORDER BY "clothing_items"."id" DESC
# 修正後:
# ... ORDER BY "clothing_items"."clothing_type" DESC,
# "clothing_items"."color" DESC
ClothingItem.all.reverse_order.first == ClothingItem.last
# 修正前: false
# 修正後: trueテスト (activerecord/test/cases/relations_test.rb) では、query_constraints を持つモデルで reverse_order.first が last と一致することなどが検証されています。
- 影響範囲・注意点
- 影響を受けるのは 「
query_constraintsを持つモデル」かつ「orderを指定していない Relation」に対してreverse_orderを使っているコード です。- これまで
idの逆順に依存していたクエリは、query_constraintsの逆順に変わります。
- これまで
query_constraintsを定義していないモデル:- これまで通り、
reverse_orderのフォールバックは主キーベースで変更なし。
- これまで通り、
- 明示的に
orderを書いている場合 (Model.order(:foo).reverse_orderなど):- その
orderを反転する、という既存の挙動から変わりません。
- その
first/lastとreverse_orderの対応:- 今回の変更により、以下のような「直感的な」関係が成り立つようになります:
relation.reverse_order.first == relation.lastrelation.reverse_order.last == relation.first
- 特に、ページネーションや「逆順にたどる」ようなロジックで、クエリ制約を順序の暗黙的基準にしている場合に扱いやすくなります。
- 今回の変更により、以下のような「直感的な」関係が成り立つようになります:
運用上の注意としては、既に本番コードで query_constraints + reverse_order に依存している場合、クエリ結果の順序が変わる可能性があるため、ログやテストで期待値を確認することを推奨します。
- 参考情報 (あれば)
query_constraintsは Rails 7.2 以降で導入された、マルチテナンシーやパーティション分割などの用途向けの仕組みで、first/last/findなどがどのカラムで絞り込む・ソートするかを決めるために使われます。- この PR は、
first/lastが参照する暗黙の順序とreverse_orderのフォールバック順序を揃えることで、API の一貫性を高める変更です。
#57817 Use the float value for :wait if passed to ActiveJob.retry_on
マージ日: 2026/6/28 | 作成者: @p8
- 概要 (1-2文で)
ActiveJob.retry_onにwait:オプションとして Float(秒数の小数)を渡した場合に、これまで整数に丸められていた待機時間を、Float のまま正しく扱うようにした修正です。ActiveJob.set(wait: ...)と同様に小数秒の遅延がサポートされ、CHANGELOG も追記されています。
- 変更内容の詳細
挙動変更のポイント
対象: ActiveJob.retry_on の wait: オプション
従来の挙動(おおよそ以下のようなコード):
# 疑似コードイメージ
def retry_on(error, wait: 3, attempts: 5)
wait_seconds = wait.to_i # ここで整数に切り捨てられていた
# ...
endこの PR では to_i を to_f に変更し、小数を含んだ秒数をそのまま利用するようにしています:
# 実際の変更イメージ
wait_interval = wait.to_fこれにより、以下のような指定が期待どおりに動きます。
class MyJob < ApplicationJob
retry_on SomeError, wait: 1.5.seconds, attempts: 3
# 1.5.seconds -> 1.5 (Float) がそのまま待ち時間に使われる
end以前は wait.to_i によって 1.5.seconds が 1 に切り捨てられており、サブ秒ないし小数秒の遅延指定が実質できませんでした。
ActiveJob.set(wait: ...) との一貫性
Rails ではすでに以下のようなコードで Float の wait をそのまま使っています:
MyJob.set(wait: 4.5.seconds).perform_laterActiveJob.set ではリンク先の実装の通り float 値を尊重しており、retry_on だけが整数化してしまうという不整合がありました。この PR はその不整合を解消し、どちらも Float をサポートする形で統一しています。
テストの修正
テストコードも以下のような形で修正されています。
retry_onのwait:に指定する値を4→4.5のように変更- 理由: リトライ回数 (
attempts: 2など) と区別しやすくし、Float が使われていることをテストからも明示するため。
- 理由: リトライ回数 (
- これにより、
waitが整数ではなく Float として扱われる前提でテストが書かれています。
CHANGELOG の更新
activejob/CHANGELOG.md に以下の内容が追加されています(要旨):
retry_onのwait:オプションが Float(小数秒)をサポートするようになったこと- 過去の PR (#57727) で漏れていた挙動変更の記録を補完
- 影響範囲・注意点
対象バージョン
この PR がマージされた Rails の次のリリース(Active Job の該当バージョン)から有効になります。既存のアプリは、そのバージョンにアップデートしたタイミングで挙動が変わります。互換性 (breaking かどうか)
- 以前から整数の
waitを使っていた場合:rubyretry_on SomeError, wait: 5, attempts: 35.to_iも5.to_fも 5 秒のため挙動に変化はありません(後方互換)。 - Float を使っていたが、整数切り捨てを前提にしていた場合:ruby以前: 実質 1 秒待ち
retry_on SomeError, wait: 1.9, attempts: 3
今後: 1.9 秒待ち
となるので、もし「小数部分は切り捨てられる」という前提でコードを書いていた場合は挙動が変わります。ただし、その前提自体は API 意図からすれば特殊ケースと考えられます。
- 以前から整数の
ActiveSupport::Duration(例:1.5.seconds) のサポート1.5.secondsはActiveSupport::Durationですが、to_fによって小数秒に展開されるため、これがそのまま利用されるようになります。
これにより、以下のようなコードが自然に動作するようになります:rubyretry_on SomeError, wait: 1.5.secondsキューアダプタごとの実装上の注意
多くのキュー(Sidekiq, Delayed Job, etc.)は秒数を Float で受け取っても問題なく扱いますが、- 自前のアダプタ
- サードパーティ製の特殊なアダプタ
を実装している場合、wait/schedule_atの値を「整数前提」で扱っていないかを確認すると安全です(小数が来ても丸めてしまうなどの実装にしておかないと、意図しない差が出る可能性があります)。
- 参考情報 (あれば)
- 対象 PR:
- #57817: Use the float value for
:waitif passed toActiveJob.retry_on - 関連 PR: #57727(この変更系統の先行 PR、CHANGELOG 追記が漏れていたもの)
- #57817: Use the float value for
- 関連実装:
ActiveJob.set(wait: ...)が Float をそのまま使っている実装
https://github.com/rails/rails/blob/b8990a6e917570d9c4e3f6a694fcc5cfa755ae53/activejob/lib/active_job/core.rb#L176-L177
この変更により、retry_on 周りでも「小数秒の遅延」「ActiveSupport::Duration の Float ベースな利用」が自然に行えるようになり、set(wait: ...) と挙動が揃います。
#57837 Make CookieStore::DEFAULT_SAME_SITE ractor safe
マージ日: 2026/6/28 | 作成者: @55728
- 概要 (1-2文で)
ActionDispatch::Session::CookieStoreのデフォルトsame_site設定用 Proc をActiveSupport::Ractors.shareable_procでラップし、CookieStore::DEFAULT_SAME_SITEを Ractor 間で共有可能(Ractor-shareable)にする変更です。これにより、Ractor を用いた並行実行環境でも CookieStore のデフォルト動作が安全に利用できます。
- 変更内容の詳細
対象ファイル:
actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
主な変更点は 1 行のみで、CookieStore::DEFAULT_SAME_SITE に設定される Proc の定義方法を変更しています。
イメージとしては、以下のような変更になります(実際のコードイメージ):
# 変更前(イメージ)
DEFAULT_SAME_SITE = proc { |request|
# request を引数に受けて same_site を決めるロジック
}
# 変更後(イメージ)
DEFAULT_SAME_SITE = ActiveSupport::Ractors.shareable_proc(proc { |request|
# request を引数に受けて same_site を決めるロジック
})ポイント:
DEFAULT_SAME_SITEは CookieStore のsame_siteオプションのデフォルト値を決める Proc。- この Proc は引数として
requestを受け取るだけで、外部の変数や状態をクロージャとしてキャプチャしていません(=「閉じ込めていない」)。 - そのため
ActiveSupport::Ractors.shareable_procでラップすることで、Ractor の shareable オブジェクト要件を満たしつつ、既存の引数・振る舞いはそのまま維持できます。 ActiveSupport::Ractors.shareable_procは、Ractor で扱える「共有可能な Proc」としてマーク・生成するための ActiveSupport のヘルパです。
- 影響範囲・注意点
- Ractor を利用しない通常の Rails アプリケーション:
- 振る舞いの変更はほぼありません。
same_siteの値決定ロジックは同一のままで、実行経路もほぼ変わりません。
- 振る舞いの変更はほぼありません。
- Ractor を利用する / 将来的に利用するアプリケーション:
CookieStore::DEFAULT_SAME_SITEが Ractor 間で共有可能になるため、CookieStore 設定を Ractor 跨ぎで利用しても Ractor の shareable 制約に引っかからなくなります。- これにより、Rails をマルチ Ractor 構成で動かす際のエラー回避・安定化に寄与します。
- カスタム
same_siteProc を使っている場合:- この PR は「デフォルト値」のみを shareable にしたもので、アプリケーション側で独自の
same_site: ->(request){ ... }を定義している場合、それが Ractor-safe かどうかは依然としてアプリケーション側の責任です。- 例: 外部インスタンス変数をキャプチャした Proc は shareable ではない可能性が高いです。
- Ractor 環境で問題が出る場合は、同様に
ActiveSupport::Ractors.shareable_procを利用する、もしくはクロージャで外部状態を持たない形に書き換える必要があります。
- この PR は「デフォルト値」のみを shareable にしたもので、アプリケーション側で独自の
- 参考情報 (あれば)
- Ractor について:
- Ruby 3 から導入された並行実行モデルで、スレッドとは異なり「共有可能オブジェクト」と「移動可能オブジェクト」の制約があるため、グローバルな定数・Proc 等も shareable である必要があります。
ActiveSupport::Ractors.shareable_procの役割:- Rails/ActiveSupport 側で、Ractor 環境における Proc の制約を吸収するためのヘルパ。
- 今回の PR は、そのヘルパを Rails の標準ミドルウェア (CookieStore) に適用することで、Ractor 対応を一歩進める位置づけと言えます。
#57839 Make some ActiveSupport constants ractor safe
マージ日: 2026/6/28 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport 内の一部の定数(ログレベル判定用 lambda と debug モード判定用 proc)を、ActiveSupport::Ractors.shareable_lambda/.shareable_procでラップし、Ractor 間で共有可能(Ractor-shareable)にした PR です。これにより、Ractor を使う並行実行環境でも、これらの ActiveSupport 定数が安全に利用できます。
- 変更内容の詳細
対象箇所
ActiveSupport::EventReporter::LogSubscriber::LEVEL_CHECKSActiveSupport::StructuredEventSubscriber::DEBUG_CHECK
元々これらは「単なる lambda / proc の定数」でしたが、Ruby の Ractor では「Ractor-shareable でないオブジェクトを Ractor 間で共有できない」ため、そのままだと Ractor から参照したときにエラーや制約に引っかかる可能性があります。
PR では、これらを以下のように shareable_lambda / shareable_proc でラップして定義し直しています。
※実際のコードは概念的には以下のようなイメージです(PR からの意訳・簡略化):
# activesupport/lib/active_support/event_reporter/log_subscriber.rb
LEVEL_CHECKS = {
debug: ActiveSupport::Ractors.shareable_lambda { |logger| logger.debug? },
info: ActiveSupport::Ractors.shareable_lambda { |logger| logger.info? },
# ... 他のレベルも同様に shareable_lambda でラップ
}# activesupport/lib/active_support/structured_event_subscriber.rb
DEBUG_CHECK = ActiveSupport::Ractors.shareable_proc {
ActiveSupport.event_reporter.debug_mode?
}ポイント:
shareable_lambda/shareable_procは「Ractor-shareable になるようにチェック・マーキングされた callable」を返すため、これを定数に格納すると、その定数自体を Ractor 間で安全に共有できます。- これらの callable は
- 「Logger インスタンスを引数に受け取る(LEVEL_CHECKS)」
- 「
ActiveSupport.event_reporter.debug_mode?を呼ぶだけ(DEBUG_CHECK)」
であり、どちらもインスタンス変数やクロージャ内部の可変状態をキャプチャしていないため、Ractor-shareable として扱いやすい構造になっています。
- PR の説明でも「Neither captures any instance state」と明示されています。
→ これは「Ractor-shareable 化に伴う副作用がほぼ無い」という保証にもなっています。
行数ベースで見ると:
- 変更ファイル数: 2
- 追加行: 4
- 削除行: 4
と非常に小さな差分で、既存のロジックはそのままに、「どのように lambda / proc を生成するか」だけを変更しています。
- 影響範囲・注意点
影響範囲
- 直接影響を受けるのは:
ActiveSupport::EventReporter::LogSubscriberを利用するコード(ログレベルに応じてイベントをフィルタ・処理する部分)ActiveSupport::StructuredEventSubscriberを利用するコード(構造化イベントの debug 出力など)
- ただし、挙動(どのログが出るか・どのイベントが処理されるか)は変わりません。
変更は「Ractor 対応のための内部実装レベルの修正」であり、API 仕様や戻り値は互換です。
Ractor 利用時のメリット
- Ractor 内で
ActiveSupport::EventReporter/StructuredEventSubscriberを使う際に、これらの定数を含む ActiveSupport モジュールをそのまま共有できるようになります。 - これにより、以下のようなケースでエラーや制約にぶつかる可能性が下がります:
- Ractor 内で
ActiveSupport.event_reporterベースのログ・イベント処理を行う - Ractor 起動前にロードした ActiveSupport を、そのまま Ractor から参照する
- Ractor 内で
注意点
- 自分で
LEVEL_CHECKSやDEBUG_CHECKを再定義・上書きしている場合:- その上書き定義が Ractor-shareable でない lambda / proc の場合、Ractor 環境で問題が発生する可能性があります。
- Ractor 対応を意識するなら、独自の lambda / proc も
ActiveSupport::Ractors.shareable_lambda/.shareable_procでラップすることが推奨されます。
- Ractor 未使用のアプリケーションにおいては、この変更による実質的な挙動差はありませんが、
ActiveSupport::Ractors.shareable_*が存在しないバージョンにパッチを backport しようとするとコンパイルエラー / ロードエラーになるため、backport 時は要注意です。
- 参考情報 (あれば)
- Ruby Ractors の概要:
- Ruby 3.x から導入された並行実行モデルで、オブジェクトは
- Ractor ローカル、または
- 「Ractor-shareable」とマークされたオブジェクト
のみが Ractor 間で共有可能です。
- Ruby 3.x から導入された並行実行モデルで、オブジェクトは
- ActiveSupport 側の Ractor 対応方針:
ActiveSupport::Ractors.shareable_lambda/.shareable_procのようなヘルパーを介して、内部で使う callable を段階的に Ractor 対応している流れの一部と考えられます。
- 類似の変更が今後他の ActiveSupport コンポーネントにも波及する可能性があり、Ractor 対応を見据えたコードを書く場合は「定数に保持するクロージャにインスタンス状態をキャプチャしない」ことがより重要になります。
#57840 Make some Active Record constants ractor safe
マージ日: 2026/6/28 | 作成者: @55728
- 概要 (1-2文で)
Active Record内部で定数として保持しているいくつかのProcを、ActiveSupport::Ractors.shareable_procでラップして Ractor 共有可能にした変更です。これにより、ArelのSQLバインド処理およびネストした属性の:all_blank判定で使われるProcが、Ractor間で安全に共有できるようになります。
- 変更内容の詳細
2-1. 対象となった箇所
次の3つの定数的なProcが対象です。
Arel::Visitors::ToSql内の bind-substitution 用のProcArel::Visitors::PostgreSQL内の bind-substitution 用のProcActiveRecord::NestedAttributes::ClassMethods内の:all_blankreject 用のProc
これらはいずれも「定数的に定義された Proc」であり、Ractor間で共有したいオブジェクトです。
PR説明にもある通り、これらのProcは「外部状態をキャプチャしていない(=クロージャではない)」「必要な情報はすべて引数として受け取る」ので、shareable_proc でラップしても動作上の意味は変わりません。
2-2. 実際の変更イメージ
具体的なコードはPR本文に出ていませんが、変更はおおむね次のような形です(イメージサンプル):
Arel::Visitors::ToSql
従来:
BIND_SUBSTITUTION = proc do |column, value|
# 何らかのバインド用の処理
end変更後:
BIND_SUBSTITUTION = ActiveSupport::Ractors.shareable_proc(proc do |column, value|
# 何らかのバインド用の処理
end)Arel::Visitors::PostgreSQL
同様に、PostgreSQL固有のバインド処理用Procを shareable_proc でラップ:
PG_BIND_SUBSTITUTION = ActiveSupport::Ractors.shareable_proc(proc do |column, value|
# PostgreSQL 向けのバインド処理
end)(定数名は説明用の仮名)
ActiveRecord::NestedAttributes::ClassMethods (:all_blank)
accepts_nested_attributes_for でよく使われる :reject_if => :all_blank が内部で利用するProcも対象です。
従来:
ALL_BLANK_PROC = proc do |attributes|
attributes.all? { |key, value| value.blank? }
end変更後:
ALL_BLANK_PROC = ActiveSupport::Ractors.shareable_proc(proc do |attributes|
attributes.all? { |key, value| value.blank? }
end)(実際の中身はRails本体の定義に準じますが、構造としてはこういった形)
2-3. なぜ shareable_proc が必要か
RubyのRactorでは、Ractor間で共有できるオブジェクトは「shareable」でなければなりません。
通常のProcは、外部環境(レキシカルスコープ)をキャプチャしていたり、内部にRactor非対応な状態を持つ可能性があるため、そのままでは shareable ではありません。
ActiveSupport::Ractors.shareable_proc は、以下のような性質を持つ「Ractor間で共有可能なProc」を作るためのActiveSupportヘルパです:
- 外部状態をキャプチャしない(=クロージャではない)Procをラップする
- そのProcがRactor間で共有しても安全であることをRails側で保証する
このPRでは、「定数として定義されていて、なおかつ外部状態をキャプチャしていないProc」 をすべて shareable_proc 化することで、Active Record自身がRactor対応しやすいように整えています。
- 影響範囲・注意点
3-1. 影響範囲
- Arelを通じたSQL生成時の「バインド変数の置き換え処理」
Arel::Visitors::ToSqlArel::Visitors::PostgreSQL
accepts_nested_attributes_forのreject_if: :all_blankの挙動
ただし、いずれも「Procをshareableにしただけ」であり、ロジック自体は変わっていないため、通常の単一スレッド/非Ractor環境での挙動は変化しません。
主なメリットは以下です。
- Ractorを使ってRailsアプリケーションや一部処理を並列実行する場合に、Active Record / Arel内部の定数Procが原因で
Ractor::IsolationErrorになる可能性が下がる - Rails側で公式に「この定数ProcはRactor対応している」と宣言した形になるため、将来的なRactorサポートを進めやすくなる
3-2. 互換性・注意点
- APIのシグネチャは変わっていないため、アプリケーションコードから見た挙動の変化は基本的にありません。
ActiveSupport::Ractors.shareable_procを自前で利用する場合も同様ですが、「外部変数をキャプチャしているProc」をshareable_procに渡すとエラーになる可能性があります。- 本PRは「Rails内部の定数ProcをRactor安全にした」に留まり、アプリケーション側でRactor使用時に必要となるすべての対応が完了したわけではありません。
依然として、アプリケーションコードや他のRails内部実装にRactor非対応な部分が残っている可能性があります。
- 参考情報 (あれば)
- Ruby公式: Ractor (並行実行のための仕組み)
- Railsガイド (参考):
- Nested attributes: https://guides.rubyonrails.org/v6.1/association_basics.html#nested-attributes
- Arel Visitors の概要については公式ガイドは薄いものの、ArelのSQL生成に関わるコンポーネントです。
- ActiveSupport::Ractors (暫定APIの可能性があるため、実際のRailsバージョンのソースを参照することを推奨)
#57841 Make ActionMailbox::Callbacks::TERMINATOR ractor safe
マージ日: 2026/6/28 | 作成者: @55728
- 概要 (1-2文で)
Action Mailbox のコールバック用終端処理 (TERMINATORlambda) をActiveSupport::Ractors.shareable_lambdaでラップし、Ractor 間で安全に共有できるようにした PR です。これにより、すでに Ractor 対応されたコールバック機構 (#57629) において、Action Mailbox 固有の終端処理も Ractor-safe になります。
- 変更内容の詳細
対象ファイル:
actionmailbox/lib/action_mailbox/callbacks.rb
実質的な変更は、ActionMailbox::Callbacks::TERMINATOR の定義方法だけです。
元のイメージ(概略):
module ActionMailbox
module Callbacks
TERMINATOR = ->(mailbox, chain) do
# コールバックチェーンを打ち切る条件や処理
# 例: mailbox が処理を続行しない場合に true を返す、など
end
end
endこれを、ActiveSupport::Ractors.shareable_lambda で包むように変更しています:
module ActionMailbox
module Callbacks
TERMINATOR = ActiveSupport::Ractors.shareable_lambda do |mailbox, chain|
# 上と同じロジック(mailbox と chain を受け取り、外部状態をキャプチャしない)
end
end
endポイント:
TERMINATORは「コールバックチェーンをいつ終了するか」を判定するための lambda。- 引数として
mailboxとchainを取り、外部の変数をキャプチャしていない(= クローズオーバなし)。 - そのため
ActiveSupport::Ractors.shareable_lambdaでラップするだけで Ractor-shareable なオブジェクトとして扱える。 - #57629 で「コールバック機構」自体は Ractor 安全化されており、この PR は「Action Mailbox が渡す terminator lambda も同じく Ractor-safe にする」補完的変更。
ActiveSupport::Ractors.shareable_lambda は、内部で Ractor が要求する「shareable」オブジェクトの制約(外部状態をキャプチャしないなど)を満たす lambda を生成するためのヘルパーです。
- 影響範囲・注意点
影響範囲:
- Action Mailbox のコールバックチェーン処理において、
TERMINATORが Ractor 間で共有されるケース(マルチ-Ractor 環境)での安全性が向上。 - Ractor を利用した並列処理で Action Mailbox を使う場合に、
TERMINATORが原因で Ractor による共有エラーが出る可能性が解消されます。
実装・運用上の注意点:
- 既存のシングルスレッド / シングルプロセス / 非 Ractor 環境では振る舞いは変わりません。
TERMINATOR本体のロジックやシグネチャ(引数など)は変更されていないため、外部 API 的な互換性は維持されています。- 今後、
TERMINATORのロジックを変更する際に「外部変数をキャプチャするような書き方」をすると Ractor-shareable でなくなるので注意が必要です。
例: 以下のような書き方は避けるべきです(外部状態をキャプチャしてしまう):rubysome_state = ... TERMINATOR = ActiveSupport::Ractors.shareable_lambda do |mailbox, chain| # some_state を参照すると Ractor-shareable ではなくなる可能性 end
- 参考情報 (あれば)
- この PR が補完する以前の変更:
- #57629: コールバック機構(
ActiveSupport::Callbacks周り)の Ractor-safe 化
- #57629: コールバック機構(
- Ractors と shareable オブジェクトについて(Ruby 本体のドキュメント)
ActiveSupport::Ractors.shareable_lambdaは Rails 側で Ractor 対応をサポートするためのヘルパーであり、Ractor の制約を満たすラッパーとして利用されます。
#57890 Fix week_field emitting invalid week strings at ISO year boundaries
マージ日: 2026/6/28 | 作成者: @55728
- 概要 (1-2文で)
Rails のweek_fieldヘルパが、年末年始の ISO 週番号境界で仕様上無効な週文字列(例:2016-W53)を出力してしまう不具合を修正し、常に ISO 週番号付け年 (week-numbering year) を用いるようにした PRです。これにより<input type="week">の値がブラウザ仕様どおりyyyy-Www形式で、かつ実在する週を指すようになります。
- 変更内容の詳細
問題のポイント
HTML の <input type="week"> では、値は yyyy-Www 形式ですが、この yyyy は「カレンダー年」ではなく ISO 8601 の週番号付け年 (week-numbering year) である必要があります。
Ruby の strftime では:
%Y… カレンダー年 (Gregorian year)%V… ISO 週番号 (01–53)%G… ISO 週番号付け年 (week-numbering year)
となっています。
元の実装では WeekField#format_datetime が:
datetime.strftime("%Y-W%V")とフォーマットしており、カレンダー年 %Y と ISO 週番号 %V を組み合わせていたため、年末年始の境界で矛盾した値が出ていました。
例:
Date.new(2016, 1, 1).strftime("%Y-W%V")
# => "2016-W53" (カレンダー年2016, ISO週番号53 の組み合わせ)
# しかし ISO 週番号付け年 2016 には週 53 は存在しないので無効な week 文字列
Date.new(2016, 1, 1).strftime("%G-W%V")
# => "2015-W53" (ISO 週番号付け年 2015 の週 53。これが正)ブラウザは存在しない週 (2016-W53 など) を値として受け付けないため、week_field ヘルパから出力される HTML が仕様違反になっていました。
修正内容
actionview/lib/action_view/helpers/tags/week_field.rb のフォーマット指定を 1 文字だけ変更:
- datetime.strftime("%Y-W%V")
+ datetime.strftime("%G-W%V")つまり、
- ISO 週番号
%Vと 対応する ISO 週番号付け年%Gをペアにするように変更。
これにより、境界日時 DateTime.new(2016, 1, 1) などに対しても正しい ISO 年・週の組み合わせが出力されます。
テスト追加
actionview/test/template/form_helper_test.rb に、境界ケースをカバーするテストを追加しています。
主な追加テスト:
week_fieldにDateTime.new(2016, 1, 1)を与えたときに、value="2015-W53"となることを確認するテスト。
今までのテストはすべて「年の途中 (mid-year)」の値のみを使っており、%Y == %G となる日付しか検証していなかったため、この問題が発覚しませんでした。今回、%Y != %G となる代表例をテストに加えることで、退行を防いでいます。
- 影響範囲・注意点
- 対象:
- Rails の
form_with,form_forなどでweek_field/f.week_fieldを使用し、DateやTime/DateTimeオブジェクトを渡しているコード。
- Rails の
- 挙動の変化:
- 年末・年始付近の週について、HTML の
valueが変わる可能性があります。- 例:
- 修正前:
2016-01-01→value="2016-W53"(ブラウザ非互換・無効) - 修正後:
2016-01-01→value="2015-W53"(正しい ISO 週表現)
- 修正前:
- 例:
- 年末・年始付近の週について、HTML の
- フロントエンドとの取り決めに注意:
- もし「カレンダー年ベースの ‘年 + ISO 週番号’」という 独自仕様 で扱うロジック (JS 側やバックエンド) があった場合、今回の修正で値が変わることにより、整合が崩れる可能性があります。
- HTML 仕様および ISO 8601 に準拠した値を期待している場合は、今回の変更は望ましい挙動です。
- 既存データとの互換性:
- すでに DB に
2016-W53のような「カレンダー年 + ISO 週番号」の文字列を保存している場合、それを Rails のweek_fieldに再設定・編集する際に、ブラウザ側で拒否される/解釈されない可能性があります。 - 過去の保存値を ISO 年 (
%G) ベースに補正することを検討してください。
- すでに DB に
- 参考情報 (あれば)
- ISO 8601 (週日付):
- 週番号付け年とカレンダー年の違い:
- ISO 週日付では「木曜日を含む年」がその週の属する年とみなされるなどのルールがあり、年末年始でカレンダー年と ISO 年がズレることがあります。
- 週番号付け年とカレンダー年の違い:
- Ruby
strftimeドキュメント(ISO week/date 関連):%G: ISO 8601 週番号付け年 (4 桁)%g: ISO 8601 週番号付け年 (2 桁)%V: ISO 8601 週番号 (01–53)
- Rails 内の一貫性:
- PR 説明にあるとおり、
DateField,MonthField,DatetimeLocalFieldはもともと「意味的に一貫したディレクティブの組み合わせ」を使っており、WeekFieldだけが不整合になっていました。今回の修正で、これらと同様にフォーマットの意味的整合性が保たれています。
- PR 説明にあるとおり、
#57870 Honor options[:drop] in CacheStore session deletion
マージ日: 2026/6/28 | 作成者: @55728
- 概要 (1-2文で)
ActionDispatch::Session::CacheStore#delete_sessionがoptions[:drop]を無視して常に新しいセッションIDを返していた不具合が修正され、options[:drop]が指定された場合はnilを返すようになりました。これにより、Rack のインターフェース仕様およびCookieStoreの挙動と整合が取れ、セッションを「ドロップしたつもりなのに新しいセッションが発行される」問題が解消されます。
- 変更内容の詳細
修正前の問題点
CacheStore の delete_session は以下のような実装になっており、options[:drop] を考慮せず常に新しいセッションIDを返していました:
def delete_session(env, sid, options)
@cache.delete(cache_key(sid.private_id))
@cache.delete(cache_key(sid.public_id))
generate_sid
endRack のセッションミドルウェアの契約では:
delete_session(env, sid, options)は- 通常は「次のリクエスト用の新しいセッションID」を返す
- ただし
options[:drop]がtrueのときはnilを返す
→ これによりcommit_session側が「新しい cookie を発行しないで終了」できる
一方で、CookieStore ではすでに以下のように options[:drop] を尊重する実装になっていました:
new_sid = generate_sid unless options[:drop]したがって、CacheStore だけがこの契約に違反しており、「セッションを破棄したい (drop) のに、新しいIDと cookie が必ず発行される」という挙動になっていました。
修正後の挙動
この PR により、CacheStore#delete_session も options[:drop] を見て nil を返すように変更されました。概念的には次のような実装になります:
def delete_session(env, sid, options)
@cache.delete(cache_key(sid.private_id))
@cache.delete(cache_key(sid.public_id))
generate_sid unless options[:drop]
endこれにより:
options[:drop]が 指定されていない / false の場合
→ これまで通り、セッション削除後に新しいセッションIDが生成されるoptions[:drop]が true の場合
→ キャッシュ上のセッションは削除されるが、新しいセッションIDは返さずnilが返る
→ Rack 側で新しいセッション cookie が発行されず、本当に「ドロップ」される
テストの追加
actionpack/test/dispatch/session/cache_store_test.rb にテストが追加され、少なくとも次のようなケースがカバーされていると考えられます:
delete_session呼び出し時にoptions[:drop]を指定した場合にnilが返ること- (暗黙的に)
options[:drop]がない場合はStringなどの新しいセッションIDが返ること
これにより、CookieStore と CacheStore の契約上の挙動がテストレベルでも揃えられています。
- 影響範囲・注意点
- 対象:
ActionDispatch::Session::CacheStoreを利用している Rails アプリケーションのセッション処理 - 影響シナリオの例:
- ログアウト処理などで
request.session_options[:drop] = trueを設定してセッション破棄を期待しているケース - 独自ミドルウェアや Rack レベルで
env['rack.session.options'][:drop] = trueをセットしているケース
- ログアウト処理などで
- 修正前:
- セッション破棄時にも、新しいセッションIDが発行され、新しいセッション cookie がクライアントへ送られていた
- 結果として「ログアウトしたのに空の新セッションが作られる」「セッションを完全に消したいのに cookie が残る」といった挙動になりうる
- 修正後:
options[:drop]を指定した削除では 一切新しいセッション cookie が発行されなくなる- これを前提としていた(=Rack 仕様に沿った)コードは、むしろ期待どおりに動くようになる
- 互換性の観点:
- 正しい Rack 仕様に合わせる方向の修正のため、「古いバグ前提」のコードを書いていなければ問題はないはず
- もし、
CacheStore使用時に「delete_session を呼ぶと必ず新しいセッションIDが来る」という前提に依存した実装があれば、そのコードは見直しが必要
確認したいポイント:
- ログアウトやアカウント削除など、「cookie もセッションも完全に消したい」処理で
:dropを使っているか - 独自に
delete_sessionを呼んでいる場合、戻り値nilを想定しているか- これを機に、戻り値が
nilの可能性を考慮した安全なコードになっているかを再確認する価値があります。
- これを機に、戻り値が
- 参考情報 (あれば)
- Rack セッションインターフェース仕様(
delete_sessionの戻り値の契約)delete_session(env, sid, options)は「新しいセッションID」または「nil(drop 時)」を返すことが求められる
- 類似実装:
ActionDispatch::Session::CookieStore#delete_sessionの実装
→ 今回の修正はこれと同じ挙動になるようにCacheStoreを揃えたもの
- 実アプリケーション側での利用例:
- ログアウト時の典型パターン:rubyなどで、今回の修正により「本当に cookie が消える」挙動が保証されやすくなる。
reset_session # 内部で delete_session + drop が使われるケース
- ログアウト時の典型パターン:
#57897 Deduplicate columns in regroup
マージ日: 2026/6/28 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Relation#regroup/regroup!が、重複したカラム指定をそのまま保持していた問題を修正し、groupと同様にグループ対象カラムを重複除去(uniq)するようにした PR です。これにより、ドキュメントにある「regroupはunscope(:group).group(fields)のショートハンド」という仕様が実際の挙動と一致するようになりました。
- 変更内容の詳細
問題点
regroup はドキュメント上、次のような意味だと説明されています:
regroup(fields) ≒ unscope(:group).group(fields)group は内部的に
self.group_values |= args # 集合演算で重複除去のように実装されており、同じカラムを複数回指定しても重複が取り除かれます。
一方、regroup! はこれまで、渡された引数をそのまま group_values に代入していたため、重複が残ってしまい、ドキュメントと動作が食い違っていました:
Post.regroup(:author_id, :author_id).group_values
# 修正前: [:author_id, :author_id]
# 期待される動作: [:author_id]
Post.all.unscope(:group).group(:author_id, :author_id).group_values
# => [:author_id] # こちらは既に重複除去されている修正内容
regroup! 内で、引数として渡されたカラムリストを重複除去する処理が追加されています。
PR の説明から推測される実装イメージは以下のようなものです:
# 変更前 (イメージ)
def regroup!(*args)
self.group_values = args
self
end
# 変更後 (イメージ)
def regroup!(*args)
self.group_values = args.flatten.uniq # or something equivalent
self
end説明文に「reorder! に合わせた」とある通り、reorder! と同じように「代入系メソッドでも重複を取り除く」というポリシーに揃えています。
テスト (activerecord/test/cases/relations_test.rb) では、以下のようなケースが追加されていると考えられます:
rel = Post.regroup(:author_id, :author_id)
assert_equal [:author_id], rel.group_valuesこれにより、regroup が unscope(:group).group(fields) と同等の結果を返すことが保証されます。
- 影響範囲・注意点
主な影響対象
regroup/regroup!を利用しつつ、意図的に同じカラムを複数回groupに渡していたコードがある場合、group_valuesの内容が配列上は変わります(重複がなくなる)。- ただし SQL の観点では、
GROUP BY author_id, author_idとGROUP BY author_idは結果が同じため、通常のユースケースでは実質的な影響はありません。
後方互換性
- 重複した
group指定に依存するようなコードは、(ほぼありえませんが)挙動が変わる可能性があります。 group_valuesを直接参照しているメタプログラミング的なコードで、「配列の長さ」や「重複の有無」を前提にしている場合は、違いが出る可能性があります。
- 重複した
メリット
- ドキュメントと実装の乖離が解消され、
regroupの意味がより直感的かつ一貫したものになりました。 group・reorderと同じポリシー(重複除去)になり、API 仕様としても整合性が高まりました。
- ドキュメントと実装の乖離が解消され、
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57897
- 関連メソッド:
ActiveRecord::Relation#groupActiveRecord::Relation#unscopeActiveRecord::Relation#reorder/#reorder!
- 実際の等価関係(今回の修正後):ruby
Post.regroup(:author_id, :author_id) # ≒ Post.all.unscope(:group).group(:author_id, :author_id) # どちらも group_values は [:author_id] になる
#57899 Fix flaky test_parallel_group_stops_launching_new_steps_with_*_flag
マージ日: 2026/6/28 | 作成者: @yahonda
- 概要 (1-2文で)
Rails の CI テストContinuousIntegrationTest#test_parallel_group_stops_launching_new_steps_with_*_flagが並列実行時のタイミング依存で不安定に落ちる問題を、テスト側の実行時間を調整することで解消した PR です。
本番コードの変更はなく、activesupport/test/continuous_integration_test.rb内のテスト定義のみが修正されています。
- 変更内容の詳細
問題となっていたテスト
対象テスト:ContinuousIntegrationTest#test_parallel_group_stops_launching_new_steps_with_*_flag
(--fail-fast とその短縮形 -f の両方のバリエーション)
テストの目的は:
- 並列実行中にあるステップが失敗したとき
--fail-fast/-fフラグが有効なら- 「それ以降のステップ(Should not run N)」が起動されないこと
を確認することです。
しかし実際には、CI 上で以下のような失敗が散発的に発生していました:
- 「Fail」ステップが失敗したあとに
- 本来実行されてはいけない「Should not run 3」が起動・完了してしまう
ログ抜粋(失敗例)を見ると、
Should not run 1(0.01s)Should not run 2(0.01s)Fail(0.02s)- その後に
Should not run 3(0.01s)
という順に動いており、**「Fail が失敗した後に 3 がキューから拾われてしまっている」**状況です。
これは並列ワーカーのスレッド/プロセスがキューからステップを取り出すタイミングと、fail-fast フラグが有効になって「もう新しいステップを起動してはいけない」と認識するタイミングの競合によるものです(spawn jitter / スケジューリングの揺らぎ)。
PR での対策
PR の要点は次の一つです。
- 「実行されてはならないステップ」の中身を
trueからsleep 0.1に変更した
元: (概念的なイメージ)
# 擬似イメージ: 実際のコードは test helper 経由で step を定義している
step "Should not run 1" do
true
end
step "Should not run 2" do
true
end
step "Should not run 3" do
true
end変更後:
step "Should not run 1" do
sleep 0.1
end
step "Should not run 2" do
sleep 0.1
end
step "Should not run 3" do
sleep 0.1
endここで意図しているのは:
- 元々の
trueステップは非常に高速(~0.01s)で終わっていた - 「Fail」ステップも ~0.02s と短時間
- その結果、「Fail が失敗した」という事実が他のワーカーに伝播する前に、別ワーカーが次のステップ(Should not run 3)をすでにキューから取得してしまう可能性があった
そこで、
- 「Should not run X」側を
sleep 0.1(約 0.1s) にして、- 各「実行されるべきではないステップ」の実行時間を十分に長くすることで
- それらの実行中に「Fail」ステップが必ず完了し、fail-fast の状態が確定するようにする
- すると、Fail 完了後に新しいステップがデキューされる余地がなくなるため、テストがタイミングの揺らぎから独立して安定する
PR の説明にもある通り:
each should-not-run step (~0.1s) outlasts "Fail" (~0.02s).
The failure is then observed before a worker can dequeue the third step, independent of spawn jitter.
という設計意図です。
重要な点:
- 本番コード(parallel 実行の実装)は一切変更していない
- 変更したのはテスト内のステップの中身だけであり、「Fail したら以降のステップは起動しないべき」という仕様自体には手を付けていません
- あくまで「テストが OS や CI 環境のスケジューリングに過度に依存しないようにする」ための調整です
- 影響範囲・注意点
影響範囲:
activesupport/test/continuous_integration_test.rb内の CI/並列実行テストのみ- ライブラリ利用者のコードや挙動には一切影響しません
- Rails を gem として使う側の挙動変更はゼロです
注意点:
- テストが少しだけ遅くなる(
sleep 0.1× 3 ステップ分)ため、そのテストケース単体では ~0.3s 程度のオーバーヘッドが追加されます - ただしこれは CI 全体から見れば軽微であり、その代わりに「テストのフレークを避けて安定性を上げる」メリットがあるため妥当なトレードオフといえます
- 今後類似の並列 + fail-fast 系テストを書く場合も、ステップの実行時間と同期タイミングを意識しないとフレークしやすい、という知見が得られます
- テストが少しだけ遅くなる(
- 参考情報 (あれば)
実際にフレークが発生した CI ログ:
PR 本体:
この PR は、「並列処理とタイミング依存ロジック(fail-fast)」をテストする際に、タスクの実行時間を明示的に調整してテストを安定化させる、という典型的なフレーク対策になっています。
#57895 Make add_foreign_key(if_not_exists: true) reversible
マージ日: 2026/6/27 | 作成者: @55728
- 概要 (1-2文で)
add_foreign_key(if_not_exists: true)を含むマイグレーションをロールバックした際に、正しく可逆(reversible)になるように修正したPRです。if_not_exists: trueを伴う外部キー追加の逆操作として、if_exists: trueを付けたremove_foreign_keyが生成されるようになりました。
- 変更内容の詳細
これまでの問題点
Rails のマイグレーションで以下のように外部キーを追加したとします:
add_foreign_key :articles, :authors, if_not_exists: trueActiveRecord::Migration::CommandRecorder は、この操作を逆転(invert)してロールバック用のコマンドを自動生成しますが、従来は次のような結果になっていました:
recorder.inverse_of(:add_foreign_key, [:articles, :authors, if_not_exists: true])
# => [:remove_foreign_key, [:articles, :authors, { if_not_exists: true }], nil]しかし remove_foreign_key は if_not_exists: オプションをサポートしていません。そのため:
- 実行時にエラーになる可能性がある
- そもそも「存在しないならスキップ」という意味にならない
といった問題があり、マイグレーションの可逆性・冪等性に反していました。
今回の修正
add_foreign_key(if_not_exists: true) の逆操作を生成する際に、オプションを以下のように変換するよう修正されました:
if_not_exists: true→if_exists: true
つまり、今後は次のような逆操作が生成されます:
recorder.inverse_of(:add_foreign_key, [:articles, :authors, if_not_exists: true])
# => [:remove_foreign_key, [:articles, :authors, { if_exists: true }], nil]これは、既に invert_add_check_constraint で採用されている挙動と同じで、設計としても一貫した形です。
実際のマイグレーションでのイメージ
class AddFkToArticles < ActiveRecord::Migration[7.2]
def up
add_foreign_key :articles, :authors, if_not_exists: true
end
def down
# 以前: Rails が自動生成すると仮定した場合、実際には正しく扱えない if_not_exists が渡されうる
# remove_foreign_key :articles, :authors, if_not_exists: true # ←サポート外
# 今後: Rails が自動生成する逆操作はこうなる
remove_foreign_key :articles, :authors, if_exists: true
end
endこのように、存在しない外部キー削除時もエラーにならずスキップされ、if_not_exists 付きで追加したときと対になる挙動になります。
コード上の変更点
activerecord/lib/active_record/migration/command_recorder.rbinvert_add_foreign_key相当の処理において、オプションハッシュにif_not_exists: trueが含まれる場合に、それをif_exists: trueに変換する分岐を追加。- 実装方針は
invert_add_check_constraintの既存コードと同様。
activerecord/test/cases/migration/command_recorder_test.rbadd_foreign_key+if_not_exists: trueを inverse したときにif_exists: trueに変換されることを検証するテストを追加。
- 影響範囲・注意点
対象:
add_foreign_keyをif_not_exists: true付きで使用しているマイグレーション。- それらを reversible なマイグレーションとして
changeメソッドで書いている場合、またはrevertを利用している場合。
期待される挙動の変化:
- 以前:
- ロールバック時のコマンドが
remove_foreign_key(..., if_not_exists: true)の形で扱われ、実行時に不整合やエラーを招く可能性があった。
- ロールバック時のコマンドが
- 以後:
- ロールバック時のコマンドは
remove_foreign_key(..., if_exists: true)になり、外部キーが存在しない場合でも安全にスキップされる。 if_not_existsを使ったマイグレーションが、他の idempotent な API(チェック制約など)と同様に、安全に往復(migrate → rollback)できる。
- ロールバック時のコマンドは
- 以前:
後方互換性:
remove_foreign_keyが理解しないオプションを渡していた、ある意味「壊れていた」パスを正しいオプションに修正する形なので、実質的にはバグフィックス扱いです。- もしアプリケーション側で
CommandRecorderの挙動(引数・オプション)を直接前提にしているようなメタプログラミングをしている場合、そのロジックに影響する可能性がありますが、一般的なアプリケーションコードではほぼ影響はありません。
- 参考情報 (あれば)
類似の挙動:
invert_add_check_constraintでもif_not_exists: true→if_exists: trueという同様の変換が既に行われており、今回の変更で外部キーに関しても挙動が統一されました。
関連する Rails の仕組み:
ActiveRecord::Migration::CommandRecorderは、changeメソッドやrevertブロック内の操作を記録し、自動的に逆操作を生成するための仕組みです。- 今回の修正により、外部キー操作におけるこの自動逆変換の信頼性が向上します。
#57892 Return an ActiveRecord::Promise from async_ids on a contradictory relation
マージ日: 2026/6/27 | 作成者: @55728
- 概要 (1-2文で)
Relation#async_idsが「常にActiveRecord::Promiseを返す」という仕様に反して、矛盾した where 句(例:where(id: []))の場合だけ素のArrayを返してしまうバグがあり、それを修正した PR です。async_idsがどんな条件でも一貫してActiveRecord::Promiseを返すようになりました。
- 変更内容の詳細
問題の背景
async_ids は内部的に ids 相当のクエリを非同期で発行し、その結果を ActiveRecord::Promise でラップして返すメソッドです。ドキュメント上も「常に ActiveRecord::Promise を返す」ことが契約になっています。
しかし、以下のように矛盾した where 句(contradictory relation)を持つ場合:
Model.where(id: []) # 絶対にレコードが返らない条件
Model.none # などids 相当の処理は「どうせ結果は空」ということが分かるので、DB クエリを飛ばさずにショートサーキットします。その際、問題のあったコードは次のような分岐をしていました(概略):
result = if relation.where_clause.contradiction?
ActiveRecord::Result.empty # ← async: を渡していない
else
...select_all(relation, ..., async: @async)
end
result.then { |result| type_cast_pluck_values(result, columns) }- 矛盾していない通常の relation では
select_all(..., async: @async)がActiveRecord::Promiseを返すため、その後のresult.thenはActiveRecord::Promise#thenとなり、戻り値もPromiseのままです。 - 一方、矛盾した relation では
ActiveRecord::Result.emptyが 同期的なActiveRecord::Resultオブジェクト を返します。そのためresult.thenはActiveRecord::Promise#thenではなく、Ruby 標準のKernel#thenとして解決され、戻り値は単なるArrayになってしまいます。
結果として:
Model.where(id: []).async_ids # => [] # Array になってしまうという、ドキュメントと実装上の契約違反が発生していました。
修正内容
修正は非常に小さく、ショートサーキット時の空結果生成を次のように変更しています。
修正前(イメージ)
ActiveRecord::Result.empty修正後(イメージ)
ActiveRecord::Result.empty(async: @async)つまり、「矛盾した where 句でクエリをスキップする場合でも、async: @async を渡して、非同期用の Result / Promise チェーンと整合する形で空結果を構築する」ようにしました。
pluck については既に同様のケースで
ActiveRecord::Result.empty(async: @async)を使っており、async_ids がその挙動に揃えられた形になります。
テストの追加
activerecord/test/cases/calculations_test.rb に 1 行追加されており、おそらく以下のような形で、「矛盾した relation に対しても async_ids が Promise を返すこと」を検証しています(概念的な例):
assert_kind_of ActiveRecord::Promise, Model.where(id: []).async_ids- 影響範囲・注意点
仕様どおりに揃える修正
もともとドキュメントでは「常にActiveRecord::Promiseを返す」としていたため、今回の変更は「バグ修正」であり、仕様を変更するものではありません。既存コードへの影響
これまで誤ってArrayを返していたケースに依存しているコードがある場合、挙動が変わります。具体的には:ruby# 以前: relation が矛盾しているときは Array 前提で書けてしまっていた result = Model.where(id: []).async_ids result << 1 # Promise ではなく Array 前提の操作 # 修正後: 上記のようなコードは NoMethodError などになるただしドキュメント上は常に
Promiseであることが前提なので、正しくasync_idsを使っていたコードはthen,await(使っている環境に応じて)などを通じて値を扱っているはずで、多くの場合はこれで正しい状態に揃ったとみなせます。矛盾した where 句の検出ロジック
where_clause.contradiction?によるショートサーキット自体のロジックは変更されていないため、クエリが実行される・されないという意味でのパフォーマンスや挙動には影響はありません。
変わるのは「戻り値の型が、非矛盾ケースと一貫してPromiseになる」という点のみです。
- 参考情報 (あれば)
- 類似挙動:
pluckも同様のケース(矛盾する where 句)についてActiveRecord::Result.empty(async: @async)を使用しており、今回の修正でasync_idsとpluckの内部実装が揃いました。 - 関係するクラス:
ActiveRecord::Promise: 非同期クエリの結果を表す Promise ラッパActiveRecord::Result: クエリ結果のラッパ。empty(async: ...)によって、非同期コンテキスト向けの空結果が生成されるようになっています。
#57891 Fix number_to_human/number_to_human_size crashing on precision: nil
マージ日: 2026/6/27 | 作成者: @55728
- 概要 (1-2文で)
number_to_humanとnumber_to_human_sizeにprecision: nilを渡したときにNoMethodErrorで落ちていた不具合を修正した PR です。ほかの number helper と同様に、precision: nilを安全に扱えるようになります。
- 変更内容の詳細
問題の背景
Rails の number helper では、precision: nil は有効なオプションとして扱われています。例えば:
number_to_currency(1234.56, precision: nil)
number_to_percentage(12.345, precision: nil)といった呼び出しはサポートされており、テストも存在します。
しかし、number_to_human / number_to_human_size だけは、内部的に RoundingHelper#absolute_precision を通る実装になっており、そこで次のようなコードがありました (問題箇所のイメージ):
if options[:significant] && options[:precision] > 0
# ...
endここで options[:precision] に nil が渡されると、Ruby の比較演算 (nil > 0) が実行されて NoMethodError: undefined method '>' for nil:NilClass が発生します。
このため、以下のような呼び出しがクラッシュしていました:
number_to_human(123456, precision: nil)
number_to_human_size(123456, precision: nil)修正内容
precision が存在する場合にのみ > 0 比較を行うようガードを追加しています。具体的には、RoundingHelper#absolute_precision 内の条件式を、兄弟クラスである NumberToRoundedConverter#convert と同等のロジックに揃える形で修正しています。
イメージとしては、以下のような変更です (実際のコードは 1 行差分ですが、意味的にはこういう修正):
# 修正前 (概念的)
if options[:significant] && options[:precision] > 0
# significant モードの処理
end
# 修正後 (概念的)
if options[:significant] && (precision = options[:precision]) && precision > 0
# precision が nil でない場合だけ > 0 比較を行う
endNumberToRoundedConverter#convert では元々:
if options[:significant] && (precision = options[:precision]) && precision > 0
# ...
endのような形で precision の存在チェックをしており、RoundingHelper 抽出時にそのガードが落ちてしまっていた、という位置づけです。
テスト
activesupport/test/number_helper_test.rb にテストが追加され、number_to_human / number_to_human_size に precision: nil を渡したときに例外が出ないことがカバーされています。
(追加行数 2 行なので、既存の number helper テストにケースを 1 つずつ増やした、程度の変更と考えられます。)
- 影響範囲・注意点
- 影響を受けるのは
number_to_human/number_to_human_sizeをprecision: nil付きで呼び出しているコードです。- これまでは例外で落ちていたケースが、正常に動作するようになります。
- ほかの number helper (
number_to_currencyなど) と同じ挙動に揃えられるため、一貫性が向上します。
precision: nilの扱いそのもの(出力フォーマットやデフォルト精度の解釈)が変わったわけではなく、「落ちずに従来どおりのロジックに乗るようになった」修正です。- 差分は
RoundingHelper内の 1 行変更とテスト 2 行追加のみで、小さく局所的です。他の helper の動作への副作用はほぼ考えにくいです。
- 参考情報 (あれば)
関連クラス・モジュール:
ActiveSupport::NumberHelper::RoundingHelperActiveSupport::NumberHelper::NumberToRoundedConverter
実際に問題になっていた呼び出し例:
ruby# 以前は NoMethodError だったが、この PR マージ後は正常に動く number_to_human(123456, precision: nil) number_to_human_size(123456, precision: nil)
#57889 Fix incorrect RDoc examples across frameworks
マージ日: 2026/6/27 | 作成者: @55728
- 概要 (1-2文で)
このPRは、Rails 各フレームワークに散在していた RDoc の誤ったサンプルコードや出力例を、実際の挙動に合わせて修正する「コメントのみ([ci skip])」の変更です。コードロジックには一切手を入れておらず、ドキュメントの信頼性・再現性を高めることが目的です。
- 変更内容の詳細
Active Model
errors.rb
- 誤り:
errors.messagesの戻り値が Array であるかのような例。 - 修正: 実際は「属性名をキーにした Hash」を返すことを反映。
# 実際のイメージ
record.errors.messages
# => { name: ["can't be blank"], email: ["is invalid"] }serializers/json.rb
- 誤り:
as_jsonの例に不正なタイムスタンプ (17:27:133.000Z) が書かれていた。 - 修正: 実在する形式のタイムスタンプ (
17:27:13.000Z) に修正。
validations.rb, validations/validates.rb
- 誤り:
:except_onオプションの例でexcept:を使用していた。 - 修正: 実際のオプション名である
except_on:に修正。
validates :name, presence: true, except_on: :updateAction Pack
http_authentication.rb
- 誤り:
AUTHN_PAIR_DELIMITERSが:区切りであるかのような記述。 - 修正: 実際には
,区切りであることを反映。
strong_parameters.rb
- 誤り:
UnpermittedParameters例外メッセージの文言が実装と不一致。 - 修正: 実際のメッセージ
"found unpermitted parameters: :a, :b"に合わせて更新。
# 実際の例外メッセージに一致させた
raise ActionController::UnpermittedParameters.new([:a, :b])
# => found unpermitted parameters: :a, :bAction Text
content.rb
to_plain_textの挙動:- 誤り:
<script>タグ内部のテキストも含まれる例 ("safeunsafe")。 - 修正: 実際は
<script>内の内容を落とすため"safe"だけになることを示す。
- 誤り:
Content.newの使い方:- 誤り: 引数の渡し方が実際のシグネチャに合っていないサンプル。
- 修正: 実装どおり「位置引数」で渡す例に変更。
content = ActionText::Content.new("<p>safe<script>unsafe</script></p>")
content.to_plain_text
# => "safe"Active Job
continuation/test_helper.rb
- 誤り:
step :x { }という構文例(Ruby 的に構文エラー)。 - 修正: 実際に通る構文
step(:x) { }に修正。
class MyJob < ActiveJob::Base
include ActiveJob::TestHelper
step(:x) { # ... }
endActive Storage
variant.rb
- 誤り: リダイレクトに
service_callを使っているかのような例。 - 修正: 実際には
urlメソッドを利用してリダイレクトする例に変更。
redirect_to @user.avatar.variant(resize_to_limit: [100, 100]).urlmirror_service.rb
- 誤り: プライマリサービスの存在確認メソッドを
exists?としていた。 - 修正: 実際のメソッド名
exist?に合わせる。
primary.exist?(key) # 正Active Support
configurable.rb
- 誤り:
config_accessorを定義していないクラスの例で、User.allowed_accessというリーダを直接呼んでいた。 - 修正: となりの
#configの例と同様にUser.config.allowed_access経由で読む形に修正。
class User
include ActiveSupport::Configurable
end
User.configure do |config|
config.allowed_access = true
end
User.config.allowed_access # => truenotifications.rb
- 誤り: サブスクライバが
matches?メソッドを持っているかのような説明。 - 修正: 実際の API どおり
subscribed_to?を使う例に変更。
subscriber.subscribed_to?("process.action_controller") # => true/falseAction Mailbox
base.rb
- 誤り:
User.exist?を呼んでいる例。 - 修正: Active Record の実際のメソッド名
exists?に修正。
User.exists?(email: mail.from)Action Mailer
mail_helper.rb
- 誤り:
format_paragraphのラップ後の出力例が実装と不一致。 - 修正: 実際の折り返し結果に合わせた例に書き換え。
format_paragraph("long long text", 20)
# => 実装どおりに折り返されたテキストに一致Action Cable
connection/test_case.rb
- 誤り: コネクション用テストヘルパの説明文が、「チャネル名から推論する」と書かれていた(コピペミス)。
- 修正: 正しく「テストクラス名からコネクションを推論する」と説明。
class ChatConnectionTest < ActionCable::Connection::TestCase
# Connection クラスはテストクラス名から推論される
endRailties
engine.rb
- 誤り: すべてのヘルパをインクルードするメソッドを
helperと解説。 - 修正: 実際のメソッド名
helpersに修正。
class MyEngine < Rails::Engine
helpers # 全ヘルパを読み込む
endpaths.rb
- 誤り:
root[...]の戻り値をいきなりArrayとして扱っている例。 - 修正: 実際は
Rails::Paths::Pathを返すので、例ではto_aryを呼んでArrayを得る形に修正。
Rails.application.config.paths["app/controllers"].to_ary
# => ["/path/to/app/controllers"]- 影響範囲・注意点
- すべてコメント(RDoc)だけの修正であり、挙動・API 互換性・パフォーマンスには影響しません。
- ただし、過去にこれらの RDoc サンプルをコピペして使っていた場合:
exist? / exists?やexcept_on:といったメソッド名・オプション名の誤用step :x { }のような構文エラーerrors.messagesの戻り値型の誤解- Strong Parameters 例外メッセージのテストで文言不一致
などが起きていた可能性があります。該当箇所があれば、この PR の内容を参考にコードを見直すと安全です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57889
- 統合元 PR 群: #57878–#57887(フレームワークごとの RDoc 修正を一つにまとめたもの)
- 実行コードには手を入れていないため、CHANGELOG レベルの破壊的変更ではなく、ドキュメント整合性の改善と考えてよいです。
#57868 Fix incorrect RDoc examples in ActionView form helpers
マージ日: 2026/6/26 | 作成者: @55728
- 概要 (1-2文で)
ActionView の form ヘルパーに記載されていた RDoc のサンプルコードが、実際の出力と食い違っていた箇所を修正した PRです。month_field/week_fieldとselectヘルパーのドキュメント上の出力例のみが変わっており、実装ロジックには変更はありません。
- 変更内容の詳細
form_helper.rb — month_field / week_field の RDoc 修正
month_field と week_field の「デフォルト値」の例が、実際の実装と異なる type="date" になっていたのを修正しています。
month_field
元のドキュメント例:
# 設定例
month_field(:user, :born_on, value: "1984-01")
# 誤った出力例 (修正前)
# => <input id="user_born_on" name="user[born_on]" type="date" value="1984-01" />修正後:
# 正しい出力例 (修正後)
# => <input id="user_born_on" name="user[born_on]" type="month" value="1984-01" />実装としては ActionView::Helpers::Tags::MonthField を使っており、type="month" が正しいため、それに合わせた形です。
同一メソッド内の別の例ではすでに type="month" になっており、その整合性を取った形でもあります。
week_field
元のドキュメント例:
# 設定例
week_field(:user, :born_on, value: "1984-W19")
# 誤った出力例 (修正前)
# => <input id="user_born_on" name="user[born_on]" type="date" value="1984-W19" />修正後:
# 正しい出力例 (修正後)
# => <input id="user_born_on" name="user[born_on]" type="week" value="1984-W19" />こちらも実装上は ActionView::Helpers::Tags::WeekField で type="week" を出力しており、それに合わせています。
form_options_helper.rb — select の multiple オプションの RDoc 修正
select ヘルパーで multiple セレクトを使う例で、HTML の出力例が実装と異なっていました。
呼び出し例の修正
元のドキュメント:
# 誤り (末尾に余分な括弧)
select :post, :person_ids, Person.all.collect { |p| [ p.name, p.id ] }, { multiple: true })修正後:
select :post, :person_ids, Person.all.collect { |p| [ p.name, p.id ] }, { multiple: true }出力される HTML の multiple 属性
元のドキュメント:
<!-- 誤り (修正前) -->
<select name="post[person_ids][]" id="post_person_ids" multiple="true">修正後:
<!-- 正しい例 (修正後) -->
<select name="post[person_ids][]" id="post_person_ids" multiple="multiple">Rails のタグビルダーでは、boolean 属性は true/false を文字列化するのではなく、属性名を値として繰り返す形(multiple="multiple", disabled="disabled" など)で出力します。この PR は、その実装にドキュメントを合わせるための修正です。
- 影響範囲・注意点
- 実装コードは一切変更されておらず、すべて RDoc コメントの修正のみです。アプリケーションの挙動に影響はありません。
- これまで RDoc のサンプルをそのままコピペしてテストや説明資料に使っていた場合:
month_field/week_fieldについて、type="date"になっている古い例は誤りなので、type="month"/type="week"に修正する必要があります。selectの multiple の例でmultiple="true"を期待するテストを書いていた場合は、multiple="multiple"を期待するよう修正が必要です。
- 既存の HTML 仕様/慣習としても、boolean 属性に文字列
"true"を入れるのは一般的ではないため、この修正は Rails の通常の挙動と HTML の慣習の両方に整合的です。
- 参考情報 (あれば)
- Rails ガイド(フォームヘルパー):
https://guides.rubyonrails.org/form_helpers.html
※ 今回の RDoc 修正は、将来的にガイドのサンプルや各種記事の整合性にも影響しうるため、ガイドを参照する際もtype値や boolean 属性の扱いに注意するとよいです。 - HTML boolean 属性仕様 (WHATWG HTML):
https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes
#57869 Fix stale instance variable in stale? RDoc example
マージ日: 2026/6/26 | 作成者: @55728
- 概要 (1-2文で)
ActionController::ConditionalGetの RDoc 中のstale?の単一レコード例で、未定義のインスタンス変数@articlesを参照していた誤記を、正しく@articleに修正したドキュメント向けの変更です。実装コードではなく、サンプルコードのみの修正です。
- 変更内容の詳細
対象ファイル:
actionpack/lib/action_controller/metal/conditional_get.rb
問題になっていたサンプル(RDoc内のコメント)は、「単一レコードの stale? 使用例」で、次のようなコードが書かれていました:
# def show
# @article = Article.find(params[:id])
#
# if stale?(@article, public: true, cache_control: { no_cache: true })
# @statistics = @articles.really_expensive_call
# end
# endここで:
@article = Article.find(params[:id])で単一レコードを取得stale?(@article, ...)でも@articleを渡している
にもかかわらず、if ブロック内で @articles(複数形)を参照していました。@articles はこの例の中では一度も定義されておらず、単純なタイポ(コピペミス)です。
この PR では、その行を以下のように修正しています:
-# @statistics = @articles.really_expensive_call
+# @statistics = @article.really_expensive_call同じファイル内にはコレクション向けの stale? 例として @articles = Article.all を使うサンプルもあり、そちらでは @articles が正しく使われているため、今回の PR では変更されていません。
- 影響範囲・注意点
- 実行時の挙動には一切影響せず、「ドキュメント内のサンプルコード」のみの修正です。
- ただし、このサンプルをコピペして使っていた場合、以前は
NameError(未定義の@articles)を招く可能性がありました。今後はそのような誤りを避けられます。 stale?を単一レコードで使う場合は、レコードを格納したインスタンス変数と同じものをstale?に渡し、同じ変数をその後の処理でも使う、という意図がより明確になりました。
- 参考情報 (あれば)
- 対象モジュール:
ActionController::ConditionalGetstale?(record_or_options, **response_options)は、ETag / Last-Modified を用いた条件付き GET を簡単に扱うためのヘルパです。
- 関連する他のサンプル:
- 同ファイル内に、コレクション (
@articles = Article.all) を使ったstale?の例もあり、そちらは@articlesが正しい使い方として残されています。
- 同ファイル内に、コレクション (
#57867 Fix incorrect RDoc output examples in ActiveSupport
マージ日: 2026/6/26 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport の RDoc (ドキュメント用コメント) に記載されていたサンプル出力が、実際のコードの挙動と食い違っていたため、それを修正した PR です。コードの動作自体には変更がなく、あくまでドキュメントの例の整合性を取る修正です。
- 変更内容の詳細
2-1. Hash#assert_valid_keys のエラーメッセージ例を修正
対象ファイル: activesupport/lib/active_support/core_ext/hash/keys.rb
assert_valid_keys の RDoc にある例が、実際に発生するエラーメッセージと異なっていたため修正されています。
変更前
# { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"変更後
# { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: \"name\", \"age\""なぜ修正が必要か
assert_valid_keys は内部で
valid_keys.map(&:inspect)を使ってメッセージを構築しています。"name".inspect は常にダブルクォート付き "name" を返すため、実際のメッセージは "name", "age" という形式になります。
もともとの例では 'name', 'age' とシングルクォートで書かれていたため、実際に raise されるメッセージと食い違っていました。なお、同じファイル内のシンボルキー版の例はすでに正しい形式になっており、この例だけが不整合だったものを揃えた形です。
2-2. EncryptedConfiguration#config の返り値例を修正
対象ファイル: activesupport/lib/active_support/encrypted_configuration.rb
EncryptedConfiguration#config の RDoc における返り値の例が、同じクラス内の他の例と数値だけ異なっていたため修正されています。
変更前
# my_config.config
# # => { some_secret: 123, some_namespace: { another_secret: 789 } }変更後
# my_config.config
# # => { some_secret: 123, some_namespace: { another_secret: 456 } }なぜ修正が必要か
config メソッドは単に read の結果をデシリアライズして返すメソッドです。RDoc 内の別の箇所では、read の例やクラス説明、keys の例などで another_secret の値として 456 が使われており、この config の例だけ 789 になっていました。
readの例:another_secret: 456- クラス説明・
keysの例: 同様に456 configの例: なぜか789→ 今回456に修正
これにより、ひとつのストーリーとして示されている暗号化設定の例が全て同じ値 (456) で整合するようになります。
- 影響範囲・注意点
- 実行コードには一切変更がなく、挙動やパブリック API に影響はありません。
- 影響するのは RDoc で生成されるドキュメント、およびドキュメントを参考にする開発者のみです。
- エラーメッセージをテストで厳密に比較している場合:
- もともと
"name", "age"形式で出力されており、PR はその通りにドキュメントを直しただけなので、既存テストを壊すような変更はありません。
- もともと
- EncryptedConfiguration のチュートリアルや社内ドキュメントで
789の例をそのまま引用している場合、今回の修正に合わせて456に揃えると Rails 本体の RDoc と一貫性が取れます。
- 参考情報 (あれば)
変更ファイル:
activesupport/lib/active_support/core_ext/hash/keys.rbactivesupport/lib/active_support/encrypted_configuration.rb
assert_valid_keysの実装 (参考用・バージョンによって細部は異なる可能性あり):rubydef assert_valid_keys(*valid_keys) unknown_keys = keys - valid_keys if unknown_keys.any? raise ArgumentError, "Unknown key: #{unknown_keys.first.inspect}. " \ "Valid keys are: #{valid_keys.map(&:inspect).join(', ')}" end endこの
valid_keys.map(&:inspect)の挙動が、今回の RDoc 修正の根拠となっています。
#57871 Fix remove_check_constraint with if_exists for the expression form
マージ日: 2026/6/26 | 作成者: @55728
- 概要 (1-2文で)
remove_check_constraintを式(expression)指定で使った場合に、if_exists: trueが no-op にならずArgumentErrorを投げていた不具合を修正した PR です。抽象アダプタと SQLite3 アダプタの両方で、存在確認時にも expression を正しく渡すようにしています。
- 変更内容の詳細
問題となっていたケース
以下のような migration を書いたとします:
remove_check_constraint :products, "price > 0", if_exists: true本来 if_exists: true は、指定したチェック制約が存在しなければ何もしない (no-op) 挙動を期待しますが、実際には存在しない場合に ArgumentError が発生していました。また、存在している場合でも expression での検索が正しく行われていませんでした。
原因
remove_check_constraint は(制約を実際に削除する段階では)以下のように expression を渡して制約を特定していました:
check_constraint_for!(table_name, expression: expression, **options)しかし、削除前に if_exists 用の存在確認を行う部分では、expression を渡していませんでした:
return if if_exists && !check_constraint_exists?(table_name, **options)expression はメソッドの位置引数であり **options には含まれていないため、check_constraint_exists? は name も expression も渡されない状態で呼ばれます。その結果:
ArgumentError: At least one of :name or :expression must be suppliedが発生していました。これは if_exists: true の意味する「存在しなければ静かにスキップする」という仕様と正反対の挙動です。
このバグは以下 2 箇所で同様に存在していました。
- 抽象アダプタ:
activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb - SQLite3 アダプタ:
activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
(こちらはremove_check_constraintをオーバーライドしているため、同じ問題を別実装で抱えていた)
修正内容
両アダプタの remove_check_constraint において、if_exists チェック時にも expression を渡すように 1 行ずつ修正されています。
イメージとしては以下のような変更です(擬似コード):
# 修正前
return if if_exists && !check_constraint_exists?(table_name, **options)
# 修正後
return if if_exists && !check_constraint_exists?(table_name, expression: expression, **options)これにより、
- 削除対象の制約を探すロジック (
check_constraint_for!) - 存在確認のロジック (
check_constraint_exists?)
がどちらも同じキー (expression: "price > 0") で制約を特定するようになりました。
テスト
activerecord/test/cases/migration/check_constraint_test.rb にテストが 4 行追加され、次のようなケースがカバーされています(要約):
- 式を指定した
remove_check_constraint :products, "price > 0", if_exists: trueが、制約非存在時でもArgumentErrorにならずに no-op となる - (加えて、存在する場合に正しく削除されることも保証していると考えられる)
- 影響範囲・注意点
対象:
remove_check_constraintを expression 形式(例:"price > 0")で呼び出しているコード- かつ
if_exists: trueオプションを併用しているケース - これを利用する全てのアダプタ(少なくとも抽象アダプタ経由のもの)と SQLite3 アダプタ
期待される挙動:
- 制約が存在しない場合:
- 以前:
ArgumentErrorが発生して migration が落ちる - 修正後: 例外を投げずに何もせずスキップ(no-op)
- 以前:
- 制約が存在する場合:
- expression を用いて適切に対象制約が特定され、削除される
- 制約が存在しない場合:
互換性:
if_exists: trueを指定していない既存コードの挙動は変わりません。- すでに name 指定で
remove_check_constraint :products, name: "chk_price_positive"を使っていたコードにも影響はありません。 - バグ修正の範囲にとどまっており、API 仕様の変更ではありません。
注意点:
- これまでこのバグに依存して「存在しないことをエラーで検出する」というような変則的な運用をしていた場合は、挙動が変わり例外が出なくなります(ただし
if_exists: trueを指定している以上、通常は no-op が期待される場面なので、そのような依存は非推奨と言えます)。 - 他のアダプタ(PostgreSQL, MySQL など)が独自に
remove_check_constraintをオーバーライドしている場合、その実装にも同種の問題がないか確認すると安心です。
- これまでこのバグに依存して「存在しないことをエラーで検出する」というような変則的な運用をしていた場合は、挙動が変わり例外が出なくなります(ただし
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57871
- 対象メソッドのドキュメント(Rails ガイド / API ドキュメント)では、reversible migration のために expression ベースの
remove_check_constraintが推奨されており、その公式な使用例が今回の修正で期待どおりに動くようになります。
#57872 Fix reversibility check for drop_virtual_table
マージ日: 2026/6/26 | 作成者: @55728
- 概要 (1-2文で)
drop_virtual_tableのマイグレーションを逆変換する際の引数チェックが誤っており、本来は不可逆であるべき操作が「可逆」と判定されてしまう不具合を修正した PR です。これにより、drop_virtual_tableの逆操作として生成されるcreate_virtual_tableが正しく実行可能かどうかを正しく判定できるようになりました。
- 変更内容の詳細
問題の内容
drop_virtual_table の定義は以下のようなシグネチャを持っています(PR の説明から):
drop_virtual_table(table_name, module_name, values, **options)
# 第1引数: table_name
# 第2引数: module_name
# 第3引数: values (カラム定義などに相当)CommandRecorder が drop_virtual_table を記録した操作を逆変換するときに使うメソッドが invert_drop_virtual_table ですが、このメソッドが 引数の分解を誤っていた ため、可逆性のチェックが正しく行われていませんでした。
既存コード(問題のある実装)は概ね以下のような形でした:
def invert_drop_virtual_table(args)
_enum, values = args.dup.tap(&:extract_options!)
raise ActiveRecord::IrreversibleMigration, "..." unless values
super
endここでの問題点:
drop_virtual_tableの第3引数がvaluesであるにもかかわらず、invert_drop_virtual_tableでは 第2引数 をvaluesとして扱っていた
(これはdrop_enum/invert_drop_enumからのコピペで、そちらは第2引数がvalues)
結果として:
values変数には本来のvaluesではなくmodule_nameが入ってしまうmodule_nameは必須のため、常に「存在する」扱いになる- したがって、本来
valuesが無くて「逆変換できない = IrreversibleMigration」とすべきケースでも、ガードが発火せず、create_virtual_tableを生成してしまう - その
create_virtual_tableは実際には実行に必要なvalues情報を欠いているため、リプレイ不能な逆マイグレーション が生成されてしまう
修正内容
PR では、invert_drop_virtual_table 内で 第3引数 を values として正しく取り出すように修正しています。
実際の差分は 1 行だけですが、概念的にはこんな形になります(擬似例):
def invert_drop_virtual_table(args)
table_name, module_name, values = args.dup.tap(&:extract_options!)
# values(第3引数)が無い場合は逆変換できないので例外
raise ActiveRecord::IrreversibleMigration, "..." unless values
super
endあわせてテスト (activerecord/test/cases/migration/command_recorder_test.rb) では:
drop_virtual_tableをvaluesなしで記録した場合、- その操作を逆変換しようとすると
ActiveRecord::IrreversibleMigrationが発生する
ことを検証するテストが追加されています (+6 行)。
- 影響範囲・注意点
- 対象: ActiveRecord のマイグレーションにおける
drop_virtual_tableの逆変換(rake db:rollback等で使われる処理) - 影響:
- 以前の挙動:
drop_virtual_tableをvaluesなしで呼び出しても、CommandRecorder 的には「可逆」と誤判定され、create_virtual_tableが逆操作として記録されていた。- しかし、その
create_virtual_tableには必要なvaluesがないため、実際にロールバックしようとすると失敗する or 不完全なテーブルが作られる可能性がある。
- 修正後の挙動:
drop_virtual_tableにvaluesが提供されていない場合、その操作は 不可逆 とみなされ、invert_drop_virtual_tableはActiveRecord::IrreversibleMigrationを投げる。- これにより、「ロールバックできる」と誤解させず、早い段階で不可逆性に気づけるようになる。
- 以前の挙動:
- マイグレーションを書く側の注意点:
- 「後でロールバックしたい」前提で
drop_virtual_tableを使う場合、必ず第3引数のvalues(カラム情報など)を渡す必要がある。 - そうでない場合、この PR 適用後はロールバック時に
ActiveRecord::IrreversibleMigrationが発生する。 - 既存プロジェクトで、
valuesを省略したdrop_virtual_tableをすでに使っている場合、- これまでは「一見ロールバック可能に見えるが、実際には動かない/おかしい」状態
- 今後は「ロールバック時に明示的に例外が出る」状態
に変わるため、挙動が変わる点に注意が必要です。
- 「後でロールバックしたい」前提で
- 参考情報 (あれば)
- 類似の実装:
drop_enum/invert_drop_enumdrop_enum(name, values, **options)のように、第2引数がvaluesであり、本 PR の実装はこれをコピペしたことで引数位置がずれたのが原因と説明されています。
- 関連コード:
activerecord/lib/active_record/migration/command_recorder.rb- 各種マイグレーションコマンドの記録と逆変換 (
invert_...) のロジックがまとまっているファイルです。
- 各種マイグレーションコマンドの記録と逆変換 (
- 実務上のベストプラクティス:
- ロールバック可能なマイグレーションを意識する場合、drop 系の操作には必ず再作成に必要な情報(カラム・オプションなど)を付与するのが安全です。
- 付与できない場合は、明示的に「不可逆マイグレーション」であることをコメントなどで示しておくと、今回のような可逆性の誤検知によるトラブルを避けられます。
#57873 Fix unique_constraint_exists? to accept :column instead of :expression
マージ日: 2026/6/26 | 作成者: @55728
- 概要 (1-2文で)
PostgreSQL アダプタのunique_constraint_exists?がcolumn:オプションを正しく受け付けずArgumentErrorを投げていたバグが修正されました。ガード条件とエラーメッセージが、ユニーク制約の API 仕様(:columnまたは:name)に整合するようになっています。
- 変更内容の詳細
何が問題だったか
PostgreSQL のユニーク制約は「カラム」もしくは「名前」で識別されますが、既存実装の unique_constraint_exists? は以下のようになっていました:
def unique_constraint_exists?(table_name, **options)
if !options.key?(:name) && !options.key?(:expression)
raise ArgumentError, "At least one of :name or :expression must be supplied"
end
unique_constraint_for(table_name, **options).present?
end- ユニーク制約の実際の API は
:columnまたは:nameを受け付ける設計 - しかしガードは
:name/:expressionのどちらか必須としてチェック - そのため、以下のような正当な呼び出しが例外を投げていました:
unique_constraint_exists?(:users, column: :email)
# 本来 true/false を返すべきところで ArgumentError:expression は排他制約(exclusion_constraint_exists?)向けのオプションで、ユニーク制約では使われません。このメソッドのガードは exclusion_constraint_exists? からコピペされたため、API 仕様と整合していませんでした。
何がどう直ったか
修正後は、ガード条件とエラーメッセージをユニーク制約用に正しく変更しています:
- チェック対象を
:expression→:columnに変更 - エラーメッセージも
"At least one of :name or :column must be supplied"的な内容に修正(PR 説明より)
疑似コードで表すと:
def unique_constraint_exists?(table_name, **options)
if !options.key?(:name) && !options.key?(:column)
raise ArgumentError, "At least one of :name or :column must be supplied"
end
unique_constraint_for(table_name, **options).present?
endこれにより、すでに存在している他メソッドとの整合が取れます:
unique_constraint_forはoptions.key?(:column)で枝分かれremove_unique_constraintもcolumn:で制約を特定
テスト (unique_constraint_test.rb) も追加されており、column: ベースの呼び出しが例外を発生させず、正しく true/false を返すことが確認されています。
使用例
今回の修正後に期待される正しい振る舞いの例:
# カラムで確認
unique_constraint_exists?(:users, column: :email) # => true / false
# 名前で確認
unique_constraint_exists?(:users, name: "index_users_on_email") # => true / false
# name / column どちらもない場合は ArgumentError
unique_constraint_exists?(:users)
# => ArgumentError (At least one of :name or :column must be supplied)- 影響範囲・注意点
- 対象:
- PostgreSQL アダプタの
ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatements#unique_constraint_exists?
- PostgreSQL アダプタの
- 主な影響:
- これまで
column:を指定してunique_constraint_exists?を使うと常にArgumentErrorになっていたコードが、正しく boolean を返すようになります。 - 内部で使っている
unique_constraint_for/remove_unique_constraintと API が揃うため、一貫した挙動を期待できます。
- これまで
- 互換性:
- 既に
name:だけを使っていたコードには挙動の変化はありません。 expression:を渡していた場合は、そもそもユニーク制約としては意味のない指定だったため、今後はエラーになるか、少なくとも推奨されません(ユニーク制約は式ベースではないため)。
- 既に
- テスト:
unique_constraint_exists?がcolumn:オプションを受け取るパスがテストでカバーされるようになり、退行を防止します。
- 参考情報 (あれば)
- 類似 API:
- 排他制約:
exclusion_constraint_exists?(table, name: ..., expression: ...)- こちらは式ベース(
expression:)が正当なオプション
- こちらは式ベース(
- ユニーク制約:
- 追加:
add_unique_constraint :table, :column, name: ... - 削除:
remove_unique_constraint :table, column: ..., name: ... - 存在確認(今回修正対象):
unique_constraint_exists?(:table, column: ..., name: ...)
- 追加:
- 排他制約:
この PR により、ユニーク制約系メソッドのインターフェースが統一され、column: を起点とした存在確認が安心して使える状態になっています。
#57842 Make ActiveSupport::JSON encoders Ractor-shareable
マージ日: 2026/6/26 | 作成者: @etiennebarrie
- 概要 (1-2文で)
ActiveSupport::JSON のエンコーダを Ractor 共有可能(Ractor-shareable)にし、Ractor 環境下でも安全に再利用できるようにした PRです。併せて、その挙動をテストするために Isolation テストを導入し、Ruby 本体のクラッシュを回避するようにしています。
- 変更内容の詳細
ActiveSupport::JSON encoder を Ractor-shareable に
activesupport/lib/active_support/json/encoding.rb の変更点はおおまかに言うと以下です。
- JSON エンコーダ(主に
ActiveSupport::JSON::Encoding周り)の実装を、Ractor 間で安全に共有可能なオブジェクトとして扱えるようにリファクタリング - Ractor から参照される可能性のあるオブジェクトについて、
- 変更可能なグローバル状態を持たない
- もしくは Ractor 内ローカルに複製されるようにする
- これにより、
ActiveSupport::JSON.encodeなどを Ractor 内から呼び出しても、Ractor 安全性の制約に反しないようにする
Ruby 3 以降の Ractor では、
「shareable オブジェクトしか Ractor 間で渡せない / 参照できない」という制約があります。
これまでの実装では、JSON エンコーダが内部的にミューテーブルなオブジェクトやグローバル状態に依存しており、Ractor から利用するとエラーやクラッシュの原因になるケースがありました。
今回の PR では、そうした依存を排除ないし Ractor セーフな形に整理することで、「エンコーダ自体」を Ractor-shareable にしています。
※ PR タイトルから推測すると、「エンコーダのインスタンス」または「エンコーダを構成する定数・設定情報」を shareable にする変更が中心です。例えば以下のような方向性の変更が典型的です(実際のコードは概念的説明):
module ActiveSupport
module JSON
module Encoding
# 以前: @@global_encoder = SomeMutableObject.new
# 以後: GLOBAL_ENCODER = SomeImmutableStruct.new.freeze
# などの形で freeze して shareable にする
end
end
endテスト: ActiveSupport::Testing::Isolation の利用
activesupport/test/json/encoding_test.rb にテストが追加されています。
- 目的:
- Ractor から ActiveSupport::JSON エンコーダを利用したときに動作するか
- Ruby のバグや VM レベルの問題によるクラッシュを避けつつテストを実行すること
- 手段:
ActiveSupport::Testing::Isolationを利用して、テストを別プロセス/別 VM 的なコンテキストで実行- これにより、Ractor を使ったテストによってメインのテストプロセスが巻き込まれてクラッシュすることを防ぐ
Isolation テストは、Rails がテストスイート内で「危険な操作」(fork、Ractor、グローバル状態の大きな変更など)を扱うときによく使うパターンです。この PR では、以前の PR #57814 で Ruby 側のクラッシュに遭遇したため、それを回避する「Take 2」として Isolation を採用しています。
参考になりそうなテストイメージ(概念的な例)
class JsonEncodingRactorTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
def test_json_encoder_is_ractor_shareable
r = Ractor.new(ActiveSupport::JSON) do |json_mod|
json_mod.encode(foo: "bar")
end
assert_equal '{"foo":"bar"}', r.take
end
end上記はあくまで説明用ですが、実際のテストも同様に Ractor 内で ActiveSupport::JSON を利用する形になっているはずです。
strict warnings の更新
tools/strict_warnings.rb に +3 行の変更があります。
- 新たに追加したコードが Ruby の warning ポリシー(未使用変数・再定義など)に引っかからないよう、strict warnings の設定や除外を調整
- Rails コアのスタイルとしてテスト・ライブラリ両方で警告を抑制し、クリーンなビルドを保つためのメンテナンス的変更
- 影響範囲・注意点
直接の対象:
ActiveSupport::JSON/ActiveSupport::JSON::Encodingを利用して JSON エンコードしているコード- 特に、Ruby 3 以降で Ractor を使うアプリケーションや gem
期待される効果:
- Ractor 内で
ActiveSupport::JSON.encode等を呼び出しても、Ractor の shareable 制約違反や予期しないクラッシュが起きにくくなる - JSON エンコーダに関するグローバル状態が整理されることで、マルチスレッド・マルチ Ractor 環境でもより予測可能な挙動になる
- Ractor 内で
互換性の観点:
- 行数規模は小さいものの、JSON エンコード周辺の内部実装に手が入っているため、
- カスタムのエンコーダを
ActiveSupport::JSON::Encodingの内部構造に強く依存させている場合 - モンキーパッチで JSON エンコーダの内部定数・クラス変数を直接書き換えている場合 などでは影響が出る可能性があります。
- カスタムのエンコーダを
- 通常の
to_json/as_jsonを使っているだけのアプリケーションであれば、挙動の変化はほとんどない想定です。
- 行数規模は小さいものの、JSON エンコード周辺の内部実装に手が入っているため、
注意点:
- この PR は Ruby 本体側の Ractor 実装の安定性に依存します。Isolation によってテスト上のクラッシュは避けていますが、
- 使用している Ruby のバージョン
- そのバージョン固有の Ractor 周りのバグ によっては、実運用環境で異なる挙動を示す可能性があります。
- Ractor 本番利用を検討している場合は、この変更を取り込んだ Rails バージョンで、アプリケーションの JSON 生成パスを Ractor 下で実際に負荷テストしておくのが無難です。
- この PR は Ruby 本体側の Ractor 実装の安定性に依存します。Isolation によってテスト上のクラッシュは避けていますが、
- 参考情報 (あれば)
- この PR の前身: #57814
- 同様に ActiveSupport::JSON の Ractor 対応を試みたが、Ruby のクラッシュにより断念 → 今回は Isolation テストで再挑戦
- 関連ドキュメント:
- Ractor-shareable の要件:
Ractor.shareable?(obj)がtrueを返すオブジェクトは、Ractor 間で安全に共有可能- そのためには、基本的にイミュータブルかつグローバルに変更されないデータ構造である必要がある(今回の PR は主にその要件を満たすようにエンコーダ実装を整理していると考えられます)。
#57845 Report PostgreSQL default timestamp/time precision as 6
マージ日: 2026/6/26 | 作成者: @adrianna-chang-shopify
- 概要 (1-2文で)
PostgreSQL のtimestamp/time系カラムについて、typmod が省略された「素の」型 (timestampなど) でも実際にはマイクロ秒精度(6桁)で動いていることを、Active Record のメタデータ上でもprecision: 6として正しく報告するようにした変更です。これにより、DBに保存された値と Active Record での型キャスト結果の精度が揃い、タイムスタンプの扱いが一貫します。
- 変更内容の詳細
主な目的
- PostgreSQL では
timestampやtimeの型定義で精度を指定しない場合でも、実際の挙動は「マイクロ秒精度 (6桁)」。 - しかし PostgreSQL はその場合 typmod(型修飾子)情報を省略するため、Active Record 側から見ると「precision 不明」に見えうる。
- この PR は、PostgreSQL アダプタが typmod を正しく参照し、
fmod == -1(= typmod 省略・bare 型)のときにprecision: 6と報告することで、DB の実挙動と AR メタデータ・型キャストを揃えるもの。
具体的な挙動変更
対象カラム型:
timestamptimestamptz(timestamp with time zone)timetimetz(time with time zone)
対応内容:
- これまで: PostgreSQL から返される「フォーマット済みの型文字列」(例:
"timestamp without time zone")をパースして精度を推定していた。 - これから: PostgreSQL タイプマップから渡される「typmod (fmod)」を直接使うように変更。
fmod == -1の場合:- 「bare な timestamp/time 型」と見なし、precision を
6とみなす。
- 「bare な timestamp/time 型」と見なし、precision を
fmod != -1の場合:- typmod から計算される「明示された精度」を使用。
結果として、
timestamp→precision: 6timestamp(3)→precision: 3time→precision: 6time(0)→precision: 0
といった形で、DB が実際に扱う精度と Active Record の precision が一致するようになります。
テスト・その他の変更点
以下のテストが追加/更新されています:
activerecord/test/cases/adapters/postgresql/timestamp_test.rb- PostgreSQL アダプタにおける bare/explicit timestamp 系カラムの precision の振る舞いを検証。
activerecord/test/cases/date_time_precision_test.rbactiverecord/test/cases/time_precision_test.rb- datetime/time 型の precision 周りの期待値を、PostgreSQL のデフォルト 6 桁精度に合わせて修正。
activerecord/test/cases/defaults_test.rbactiverecord/test/cases/schema_dumper_test.rbactiverecord/test/cases/migration/compatibility_test.rb- precision 情報や schema_dump/migration の互換性関連テストを、今回の精度報告の変更に合わせて更新。
activerecord/CHANGELOG.md には、PostgreSQL の timestamp/time precision レポートが 6 になるよう変更されたことが追記されています。
- 影響範囲・注意点
影響が出うるポイント:
カラムメタデータの precision
connection.columns(:table_name).find { … }.precisionの戻り値が、bare なtimestamp/timeカラムでnil(もしくは 0 相当) から6に変わる可能性があります。- precision に応じてアプリ側のフォーマットや丸め処理を分岐している場合は、挙動が変わるか確認が必要です。
schema dumper / migration 生成との整合性
- schema dumper がカラム定義を出力する際、PostgreSQL の実際のデフォルト精度に即した情報が利用されるようになります。
- これにより、将来の migration やスキーマ再構築時に、DB の実挙動とギャップが出る可能性が減りますが、既存期待値テストが precision の値をハードコードしていると失敗することがあります。
タイムスタンプの型キャストの一貫性
- Active Record が内部で timestamp/time をキャストする際、DB に保存されている値と同じマイクロ秒精度を前提として扱うようになるため、アプリ内での時間比較・シリアライズ/デシリアライズの結果がより安定します。
- 逆に言うと、今まで「精度不明」扱いに依存していたコード(例: 秒単位に丸められることを前提にした処理)があると挙動が変わる可能性があります。
総じて、PostgreSQL を使っていて、timestamp/time の precision をメタデータ経由で参照しているコード・テストは、この変更を前提に見直しが必要になりえます。
- 参考情報 (あれば)
- PostgreSQL の timestamp/time 精度仕様
timestamp/timeは、精度を指定しない場合デフォルトで microsecond (6 桁) 精度を持つ。- typmod が省略される (
fmod == -1) のは、この「デフォルト精度」を意味する。
- Active Record 側の意図
- adapter は DB の実際の挙動を忠実に反映し、AR のカラムメタデータと型キャストをそれに合わせるべき、という設計方針に沿った修正。
#57866 Fix incorrect RDoc output examples in ActiveRecord
マージ日: 2026/6/26 | 作成者: @55728
- 概要 (1-2文で)
Active Record の RDoc 内のコード例が、実際の挙動と矛盾していたため、それを正しい出力例に修正した PR です。挙動そのものの変更はなく、あくまでドキュメントの誤記修正です。
- 変更内容の詳細
(1) associations/collection_proxy.rb の find ブロック付き例
対象: ActiveRecord::Associations::CollectionProxy#find の RDoc 例
修正前:
# person.pets.find(2) { |pet| pet.name.downcase! }
# # => #<Pet id: 2, name: "fancy-fancy", person_id: 1>修正後:
# person.pets.find(2) { |pet| pet.name.downcase! }
# # => #<Pet id: 2, name: "spook", person_id: 1>背景説明:
- 同じドキュメントブロック内のフィクスチャ定義では、
id: 2のペットはname: "Spook"となっている。 pet.name.downcase!によって"Spook"は"spook"になるのが正しい。"fancy-fancy"はid: 1の"Fancy-Fancy"を downcase した結果であり、id: 2として出てくるのは不整合。- したがって、フィクスチャとコード例の整合性をとるために、期待出力を
"spook"に修正している。
この変更により、find にブロックを渡したときの挙動(返されるオブジェクトに対して副作用を与える例)が、実際に再現可能な内容になります。
(2) sanitization.rb の sanitize_sql_for_conditions 名前付きバインド例
対象: ActiveRecord::Sanitization#sanitize_sql_for_conditions の RDoc 例(名前付きバインド)
修正前:
# sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
# # => "name='foo''bar' and group_id='4'"修正後:
# sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
# # => "name='foo''bar' and group_id=4"ポイント:
- Active Record の SQL サニタイズでは、整数値のバインドはクォートされない(文字列のみ
'...'で囲われる)。 - 直前の「位置指定バインド」の例では既に
group_id=4(クォートなし)になっている。 sanitize_sql_arrayの同様の名前付きバインド例でもgroup_id=4になっており、今回の例だけが"group_id='4'"と誤っていた。- 一方、
"group_id='%s'"のようにテンプレート文字列内にクォートが書かれている例では、そのクォートはテンプレート由来のリテラルなので'4'になるのが正しい。この点は既存通りで問題なし。
この修正により、ドキュメント中の named bind の例が実際の SQL 生成仕様(整数はクォートしない)と一致します。
- 影響範囲・注意点
- 実装コードには一切手が入っておらず、挙動変更はありません。
- 影響範囲は RDoc の出力(およびそれを参照する開発者)に限られます。
- これまで:
CollectionProxy#findのブロック付きの例をそのままコピペしても、コメントに示された出力は再現できませんでしたが、今後はフィクスチャ通りに動かせば同じ結果を再現できます。sanitize_sql_for_conditionsの例を見て「整数もクォートされる」と誤解する可能性がありましたが、修正により 整数はクォートされない という実際の仕様が伝わりやすくなります。
- もし既にこの誤ったドキュメントを元にテスト期待値や社内ドキュメントを書いている場合は、
group_id=4が正であることを再確認するとよいです。
- 参考情報 (あれば)
- 対象メソッド:
ActiveRecord::Associations::CollectionProxy#findActiveRecord::Sanitization#sanitize_sql_for_conditions
- 近い API:
sanitize_sql_array(同様の名前付きバインド/位置指定バインドの例があり、今回の修正と一貫した仕様になっている)
#57855 Fix incorrect human_size round_mode doc example
マージ日: 2026/6/26 | 作成者: @55728
- 概要 (1-2文で)
Numeric#to_fs(:human_size)の RDoc に記載されていたround_modeオプションのサンプルコードが、実際の動作と合っていなかったため修正した PR です。コード本体の挙動に問題はなく、ドキュメント中の例のみを正しいものに差し替えています。
- 変更内容の詳細
対象: activesupport/lib/active_support/core_ext/numeric/conversions.rb の RDoc コメント中のサンプル
もともとのドキュメント例:
1234567.to_fs(:human_size, precision: 2) # => "1.2 MB"
1234567.to_fs(:human_size, precision: 2, round_mode: :up) # => "1.3 MB"問題点:
- 1234567 バイト ≒ 1.1774 MB なので、有効桁数2桁でのデフォルト (round_mode: :default) は
"1.2 MB"になる。 - 実装上
round_mode: :upを指定しても、この入力値では結果は"1.2 MB"のままで、"1.3 MB"にはならない。 - そのため、サンプルは「
round_modeによって結果が変わる」ことの説明として不適切だった。
修正後のサンプル:
1234567.to_fs(:human_size, precision: 2) # => "1.2 MB"
1234567.to_fs(:human_size, precision: 2, round_mode: :down) # => "1.1 MB"ポイント:
- 入力値
1234567はそのまま維持しつつ、round_mode: :downに変更。 - これにより、デフォルト
"1.2 MB"から"1.1 MB"に変わり、round_modeによる違いが明確に示される。 - 実装は既にこの通り動作しており、PRはコメント(ドキュメント)の1行差し替えのみです。
作者はあわせて、現状の挙動を以下で検証済み:
1234567.to_fs(:human_size, precision: 2) # => "1.2 MB"
1234567.to_fs(:human_size, precision: 2, round_mode: :down) # => "1.1 MB"
1234567.to_fs(:human_size, precision: 2, round_mode: :up) # => "1.2 MB" # default と同じなお、number_to_human_size (ActionView::Helpers::NumberHelper) 側のドキュメント例は、既に round_mode: :up で結果が変わる入力 (123456 など) を用いており、そちらは問題なしとのことです。
- 影響範囲・注意点
影響範囲:
- ドキュメント (RDoc) のみ。ライブラリコード、API、挙動には一切変更なし。
- 既存アプリケーションの動作には影響しません。
注意点:
- これまで「
round_mode: :upを指定すると1234567が"1.3 MB"になる」と誤解していた場合は、それがドキュメント上の誤りだったと認識を改める必要があります。 round_modeの効果を確認したい場合は、今回のドキュメント例のように、切り上げ/切り捨てで実際に値が変わる境界付近の値を使う必要があります。- 仕様上、
round_mode: :upが常に「大きめの値」に寄るとは限らず、「丸めの基準点を越えたときにどちら側に寄せるか」を制御するオプションである点に注意してください(詳細は Ruby / ActiveSupport の rounding オプション仕様を参照)。
- これまで「
- 参考情報 (あれば)
- 対象メソッド:
Numeric#to_fs(:human_size)(ActiveSupport のフォーマット拡張) - 類似機能:
ActionView::Helpers::NumberHelper#number_to_human_size
→ こちらのドキュメントには既にround_mode: :upの正しい例 (123456→"130 KB"vs デフォルト"120 KB") が存在します。
#57865 Fix incorrect RDoc examples in Active Model
マージ日: 2026/6/26 | 作成者: @55728
- 概要 (1-2文で)
Active Model の RDoc サンプルコードにおける出力例と実際のコードの不整合を修正したドキュメント専用のPRです。attribute_method?とErrors#eachの例が実際の動作に沿うように、クラス名とオプション値が修正されています。
- 変更内容の詳細
2-1. ActiveModel::Validations::ClassMethods#attribute_method?
対象ファイル: activemodel/lib/active_model/validations.rb
元のRDocサンプル:
class Person
include ActiveModel::Validations
attr_accessor :name
end
User.attribute_method?(:name) # => true # <- should be Person
User.attribute_method?(:age) # => false # <- should be Person問題点
- 定義しているクラスは
Personなのに、呼び出し側がUser.attribute_method?になっており、コピペ由来と思われる不整合がありました。 - これにより、ドキュメントを見てコピペした場合、そのままでは動作しない、もしくは読者が混乱する可能性があります。
修正内容(イメージ)User → Person に修正:
class Person
include ActiveModel::Validations
attr_accessor :name
end
Person.attribute_method?(:name) # => true
Person.attribute_method?(:age) # => false技術的な意味合い
ActiveModel::Validationsをincludeしたクラスに対して、属性に応じたバリデーション関連のメソッドが定義される、という一般的な使い方の説明用サンプルです。attribute_method?は、そのクラスに対して特定の属性メソッドが定義されているかどうかを問い合わせるAPIで、ドキュメント例がそのまま実行可能な形に整えられました。
2-2. ActiveModel::Errors#each
対象ファイル: activemodel/lib/active_model/errors.rb
元のRDocサンプル:
person.errors.add(:name, :too_short, count: 2)
person.errors.each do |error|
# Will yield <#ActiveModel::Error attribute=name, type=too_short,
# options={:count=>3}> # <- should be 2
end問題点
- 実際に
addしているオプションはcount: 2なのに、コメントのoptionsでは{:count=>3}と記述されており、サンプルコードと出力例が一致していませんでした。
修正内容(イメージ)
コメント中の 3 → 2 に修正:
person.errors.add(:name, :too_short, count: 2)
person.errors.each do |error|
# Will yield <#ActiveModel::Error attribute=name, type=too_short,
# options={:count=>2}>
end技術的な意味合い
ActiveModel::Errors#eachがブロックに渡すActiveModel::Errorオブジェクトの中で、optionsにcountがどのように格納されるかを示すドキュメントです。- 実際のAPI仕様は変わっておらず、「与えた
countオプションがそのままerror.optionsに入る」という挙動を正しく反映する形に整えられました。
- 影響範囲・注意点
- 実装への影響: コード上の挙動には一切影響ありません。
[ci skip]が付いている通り、テスト実行も不要なドキュメントのみの変更です。 - 利用者への影響:
- RDoc / API ドキュメントを見ながら実装する開発者にとって、サンプルをそのままコピペしても動作しやすくなります。
- 特に
Errors#eachの例は、error.optionsの中身を正確に理解したいときに役立つため、オプションの取り扱いで混乱しにくくなります。
- 後方互換性: 完全に後方互換です(動作・シグネチャの変更なし)。
- 参考情報 (あれば)
- 対象メソッド:
ActiveModel::Validations::ClassMethods#attribute_method?ActiveModel::Errors#each
- 両方とも Active Model の基本的なAPIのため、Rails のフォームオブジェクトやプレーンオブジェクトのバリデーションを書く際に、ドキュメント例をそのまま使いたい場合に有用です。
#57864 Add missing &block to merge! :call-seq: in ActionController::Parameters
マージ日: 2026/6/26 | 作成者: @55728
- 概要 (1-2文で)
ActionController::Parameters#merge!のドキュメント(:call-seq:)に、実装上は受け取っているブロック引数&blockが記載されていなかった問題を修正した PR です。Hash#merge!と同様に、キー衝突時にブロックでマージ方法を指定できることが、ドキュメント上も明示されるようになりました。
- 変更内容の詳細
対象ファイル:
actionpack/lib/action_controller/metal/strong_parameters.rb
元の定義(実装)は次の通りです:
# :call-seq: merge!(*other_hashes)
#
# Returns the current `ActionController::Parameters` instance with `other_hashes`
# merged into current hash.
def merge!(*other_hashes, &block)
@parameters.merge!(*other_hashes.map!(&:to_h), &block)
self
end実際のメソッドシグネチャは def merge!(*other_hashes, &block) で、内部で Hash#merge! に &block をそのまま渡しています。しかし、:call-seq: には &block が含まれておらず、ユーザーからは「ブロックを取れるのかどうか」がドキュメントだけでは判別しづらい状態でした。
この PR では、:call-seq: を以下のように修正しています(実質的な変更は &block の追加のみ):
# :call-seq: merge!(*other_hashes, &block)
#
# Returns the current `ActionController::Parameters` instance with `other_hashes`
# merged into current hash.
def merge!(*other_hashes, &block)
@parameters.merge!(*other_hashes.map!(&:to_h), &block)
self
endこれにより、Hash#merge! と同様にブロック付きで利用できることがドキュメントに反映され、同じクラス内の deep_merge! のドキュメント:
# :call-seq:
# deep_merge!(other_hash, &block)とも整合性が取れるようになりました。
サンプルコード(この PR 以前から動いていたが、明文化された利用例):
params = ActionController::Parameters.new(a: 1, b: 2)
params.merge!(ActionController::Parameters.new(a: 10, c: 3)) do |_key, old, new|
# 衝突時は大きい方を採用
[old, new].max
end
# 結果:
# params.to_h #=> { "a" => 10, "b" => 2, "c" => 3 }あるいは単純にハッシュを渡してもよいです:
params.merge!(a: 10, c: 3) do |_key, old, new|
[old, new].max
end- 影響範囲・注意点
- 実装の振る舞いは一切変わっておらず、ドキュメント上の表記のみの変更です。
- 既存コードで
Parameters#merge!にブロックを渡して使用している場合も、その挙動はこれまで通りです(内部でHash#merge!に渡される)。 - これからコードを書く開発者にとって、
merge!がブロックを取れることが分かりやすくなり、キー衝突時の挙動をカスタマイズしやすくなります。 merge(非破壊)ではなくmerge!(破壊的変更)なので、ドキュメントを見て使い始める人は、レシーバ自身が変更される点に引き続き注意が必要です。
- 参考情報 (あれば)
- Ruby
Hash#merge!の仕様(ブロックで衝突時の値を決定できる)
https://docs.ruby-lang.org/ja/latest/method/Hash/i/merge.html - 同クラス内の類似メソッド
deep_merge!の:call-seq:ruby# :call-seq: # deep_merge!(other_hash, &block)
#57848 Make parameter encodings ractor safe by default
マージ日: 2026/6/25 | 作成者: @andrewn617
- 概要 (1-2文で)
ActionController::Base.parameter_encodingsをデフォルトで Ractor セーフにするため、内部で使われるハッシュ構造を常に freeze し、書き換え時は dup→変更→再 freeze する方式に変更した PRです。これに伴い、従来の「default proc でハッシュをその場で書き換える」仕組みは廃止され、空の凍結ハッシュをデフォルト値として返す形に変わりました。
- 変更内容の詳細
背景となるオブジェクト
ActionController::Base.parameter_encodings は以下のような構造を持つクラス属性です:
# イメージ:
{
:index => { "param_name" => Encoding::UTF_8, ... },
:create => { "other_param" => Encoding::ISO_8859_1, ... },
}- 第一階層キー: アクション名 (シンボルなど)
- 値: パラメータ名 => Encoding のハッシュ
従来は、このハッシュに default proc を設定し、「初めてアクセスされたキーに対して、その場で内部ハッシュを生成して格納する」ような、ミュータブル前提の実装になっていました。
今回の主な変更点
parameter_encodingsのハッシュを常に freeze するようにした- Ractor セーフにするため、共有されるハッシュを不変(immutable)にします。
- 新しいエントリを追加するときは、元ハッシュを
dupして変更し、その結果を再度freezeします。 - これにより、Ractor 間でのデータ共有時に発生しうるスレッドセーフティやオブジェクト共有制限の問題を回避します。
default proc による「その場での破壊的更新」を廃止
- これまでの実装では、default proc を利用して、存在しないキーにアクセスされたときにハッシュをその場で更新していました。
- しかし、ハッシュが
freezeされるようになったため、「デフォルト値生成時に自身を更新する」仕組みは成立しません。 - そこで、default proc をやめ、「凍結された空ハッシュ」をデフォルト値として返すように変更しています。
イメージ:
ruby# 変更後のイメージ(擬似コード) @parameter_encodings ||= {}.freeze def parameter_encodings_for(action) parameter_encodings[action] || EMPTY_FROZEN_HASH end設定更新ロジックの変更(dup → append → freeze パターン)
コントローラ側でparameter_encodingsをカスタマイズするようなメソッド(例えば以下のような DSL)がある場合:rubyclass UsersController < ApplicationController # 例: あるパラメータを特定のエンコーディングとして扱う設定 parameter_encoding :create, "name" => Encoding::UTF_8 end内部実装としては、次のような手順でハッシュを更新する方針になります:
ruby# 擬似コード existing = self.parameter_encodings # ここで既に frozen updated = existing.dup # ミュータブルコピーを作成 updated[action] = (updated[action] || {}).merge(new_mappings) self.parameter_encodings = updated.freeze変更点の主眼は、「共有ハッシュを直接変更しない」「変更後は必ず freeze する」というライフサイクルを徹底することです。
テストの追加・更新
- 新しい不変性・デフォルト値の挙動を確認するためのテストが追加されています。
- 具体的には「デフォルトで返ってくるハッシュが変更不可であること」「変更操作が新しいハッシュを生み出していること」などを検証していると考えられます。
影響範囲・注意点
parameter_encodingsを直接ミュータブルに扱っているコードへの影響
アプリケーションやプラグイン/エンジン側で、例えば以下のように直接ハッシュを変更していた場合は壊れます:ruby# NG になるパターン(変更後は TypeError: can't modify frozen Hash が発生) ActionController::Base.parameter_encodings[:index]["foo"] = Encoding::UTF_8今後は、Rails が提供する公式な DSL や API 経由で設定を行う必要があります。
もしどうしても直接操作が必要な場合は、次のように自前で dup→freeze パターンを踏む必要がありますが、推奨されません:rubyenc = ActionController::Base.parameter_encodings.dup enc[:index] = (enc[:index] || {}).merge("foo" => Encoding::UTF_8) ActionController::Base.parameter_encodings = enc.freezedefault proc 依存のパターンは使えなくなる
自分でparameter_encodings.default_procを書き換えたり、default proc による遅延生成・破壊的更新を前提にしているようなコードは動かなくなります。スレッド・Ractor 安全性の向上
- Rails を Ractor と併用しようとしている場合(将来の並列化対応など)において、安全に
parameter_encodingsを共有できるようになります。 - ただし、Ractor 対応の一部であり、これだけで Rails 全体が完全に Ractor セーフになるわけではありません。
- Rails を Ractor と併用しようとしている場合(将来の並列化対応など)において、安全に
パフォーマンスへの影響
- 設定の更新時に
dupとfreezeが入るため、更新頻度が極めて高い場合はわずかなオーバーヘッドがあります。 - 一般的には
parameter_encodingsは起動時やクラス定義時に一度設定する程度であり、リクエストごとに頻繁に更新するものではないため、実運用上の影響はほぼ無視できるはずです。
- 設定の更新時に
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57848
- Ruby Ractor 仕様:
- オブジェクトの共有制限(共有できるのは「共有可能オブジェクト(shareable)」のみ)
freezeされたハッシュや配列は shareable になりうるため、Ractor 間で共有するためには、「生成 → 設定 →freeze→ 共有」というパターンが必要になります。
- 類似の変更は、他の Rails の設定用ハッシュ / 配列(例えば middleware スタックなど)でも順次行われており、「設定オブジェクトは基本 immutable」という方向性に沿った変更です。
#57850 Make rescue handlers ractor safe by default
マージ日: 2026/6/25 | 作成者: @andrewn617
- 概要 (1–2文で)
Rails のActiveSupport::Rescuableが持つ「例外クラスとハンドラの対応表」を、Ractor(マルチスレッド/マルチ Ractor 実行)でも安全に扱えるようにする変更です。データ構造自体を freeze / dup する運用に変えることで、Ractor 間で共有しても壊れない形にしつつ、ハンドラ Proc 自体の Ractor shareable 化は利用者に委ねています。
- 変更内容の詳細
何をしているか
ActiveSupport::Rescuable では、以下のように「例外クラス(or キー) と rescue ハンドラ(Proc) のペア」の配列を内部に持っています。
# イメージ
@_rescue_handlers = [
[StandardError, ->(e) { handle_error(e) }],
[MyError, :handle_my_error],
...
]この PR では、この配列自体を Ractor-safe な運用にするよう変更しています。
ポイントは説明文に書かれている通りです:
This is an array contain arrays of key and handler pairs.
We freeze the default array and then dup, append and freeze when adding more.
つまり:
- 「デフォルトの rescue handler 配列」を freeze して不変オブジェクトとして扱う
- 新しいハンドラを追加するときは
- 既存配列を
dupして新しい配列を作る - そこに
[key, handler]を<<で追加 - その新配列を
freezeする
- 既存配列を
- これにより:
- 元の配列は一度 freeze されていて書き換えられない
- 各 Ractor は「freeze された rescue handler 配列」を安全に共有できる
- 追記は「新しい配列インスタンスを作って freeze」するだけなので、既存の共有状態を壊さない
変更行数としては rescuable.rb が +2/-2 程度なので、ほぼこの配列の扱い(生成・更新)ロジックのみの微修正です。
ハンドラ Proc について
PR 説明にある通り:
The handler is proc we
callso we cannot make it ractor shareable since it may rely on calling methods onself. So the user will need to do that.
- rescue ハンドラは
Proc(あるいはブロック) であり、内部でselfのメソッド呼び出し等を行うのが一般的 - そのため Proc インスタンス自体を Ractor.shareable にすることは Rails 側では保証できない
- Proc がキャプチャしているコンテキスト次第で shareable かどうかが変わる
- この PR は 「配列構造」を Ractor-safe にするだけ で、Proc を shareable にする責任はアプリケーション側 に残されています
テストの追加
activesupport/test/rescuable_test.rb に 14 行追加されており、以下のようなことを検証していると推測されます(実際のコードは省略):
- rescue handler 配列が期待通り freeze されていること
- ハンドラ追加時に新しい配列が生成され、元配列が変更されないこと
- Ractor 上でも
rescue_fromの定義/参照が問題なく動くこと(あるいは少なくともエラーにならないこと)
- 影響範囲・注意点
影響範囲
- 対象:
ActiveSupport::Rescuableを利用する全コンポーネントrescue_fromを使う Rails アプリ(主にActionController,ActiveJob,ActiveRecordなど)全般
- 主な変更点:
- rescue handler の内部表現(配列)のミュータビリティ管理が変わる
- public API (
rescue_fromの使い方など) に変更はないはず
通常の単一スレッド・単一 Ractor の Rails アプリでは、ほぼ挙動は変わらない想定です。
ただし、以下のような「Rails 内部の rescue handler 配列に直接触る」ようなコードがもしあれば影響します:
@_rescue_handlersを直接参照して<<で追記している@_rescue_handlersをclear/shift/popなどで破壊的に変更している
この PR 後は、@_rescue_handlers が freeze された配列になっているため、上記のコードは FrozenError を起こします。
注意点 / Ractor 利用時のポイント
- Ractor 内で
rescue_fromを定義すること自体は可能ですが、- 追加されるハンドラ Proc やオブジェクトが Ractor.shareable でない場合、その Ractor から別 Ractor にそのオブジェクトを渡そうとするとエラーになります
- この PR は Rails 側の内部状態を「少なくとも壊れない形」に整えるものであり、
- アプリケーションの rescue ハンドラを Ractor-safe にするには、引き続き以下を考慮する必要があります:
- ハンドラで閉じているオブジェクト(
selfの状態、インスタンス変数など)が Ractor 間共有されない前提で設計する - Ractor をまたいでハンドラ自体を共有する必要がある場合は、shareable なデータだけを参照するように書く
- ハンドラで閉じているオブジェクト(
- アプリケーションの rescue ハンドラを Ractor-safe にするには、引き続き以下を考慮する必要があります:
- 参考情報 (あれば)
- Ractor と shareable オブジェクトの仕様(Ruby 本体)
Ractor.shareable?,Ractor.make_shareable,frozen?なオブジェクトの扱いなど
- Rails 側では、今後も他の内部データ構造を freeze + copy-on-write 的に扱うことで Ractor 対応を進めていく流れが想定されます。
この PR はその一環として、「例外ハンドリング周りの状態管理を Ractor フレンドリーにした」変更と位置づけられます。
#57851 Make protected ivars ractor safe
マージ日: 2026/6/25 | 作成者: @andrewn617
- 概要 (1-2文で)
ActionMailer と ActionController に定義されているPROTECTED_IVARS定数を.freezeすることで、Ractor セーフ(並行実行時にスレッド安全)になるようにした PR です。RuboCop によるチェックから漏れていた未凍結の定数を補正する、極めて小さな内部的変更です。
- 変更内容の詳細(あればサンプルコードも含めて)
変更箇所は2ファイルのみです。
actionmailer/lib/action_mailer/base.rbactionpack/lib/action_controller/base.rb
両方で、保護対象インスタンス変数の一覧を表す PROTECTED_IVARS 定数が定義されていますが、これを Ractor セーフにするために .freeze が追加されました。
イメージとしては以下のような変更です(実際の中身は概念的な例):
# 変更前
PROTECTED_IVARS = [
:@_response,
:@_request,
:@_env,
]
# 変更後
PROTECTED_IVARS = [
:@_response,
:@_request,
:@_env,
].freezeポイント:
PROTECTED_IVARSは配列(または類似の可変オブジェクト)で、
Ractor 間で共有される可能性があるクラスレベルの定数。- Ruby 3 系以降の Ractor モデルでは、Ractor 間で共有するオブジェクトは
「イミュータブル(=凍結済み)」である必要があるため、.freezeが必須。 - rubocop の設定上、本来はこうした定数は自動的/半自動的に検出されるべきだったが、
この2つの定数はチェックから漏れていたため、今回手動で修正している、という背景です。
- 影響範囲・注意点
- 影響範囲は ActionMailer::Base と ActionController::Base 内部の「保護対象インスタンス変数一覧」を保持する定数のみで、
パブリック API の仕様変更はありません。 PROTECTED_IVARS自体を直接書き換えるようなコード(例:ActionController::Base::PROTECTED_IVARS << :@foo)が存在する場合、
この変更によりFrozenErrorが発生するようになります。- もともとフレームワーク内部用の定数であり、アプリケーション側から変更することは推奨されていないため、
正常な利用範囲では問題にならないはずです。
- もともとフレームワーク内部用の定数であり、アプリケーション側から変更することは推奨されていないため、
- Ractor を使って Rails を並行実行している(あるいは将来的に対応したい)環境では、 これにより
Ractor::IsolationErrorのようなエラーが起きる可能性が一つ減ることになります。 - テストや CHANGELOG の更新は行われていませんが、挙動を変える類の変更ではなく、
内部定数の凍結のみなので妥当な範囲と考えられます。
- 参考情報 (あれば)
- Ruby Ractor の仕様(オブジェクト共有と freeze の必要性):
- 公式ドキュメント(英語): https://docs.ruby-lang.org/en/master/ractor_md.html
- Rails コアでの Ractor 対応は、3.x 以降で徐々に進められており、
今回のような「定数の凍結」「内部オブジェクトのイミュータブル化」はその一環と考えられます。
#57857 Make ActiveSupport::TimeFormats ractor shareable
マージ日: 2026/6/25 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveSupport::TimeFormatsが保持しているフォーマット一覧 (@list) を Ractor で共有可能にするため、内部で使っている lambda を「共有可能 (shareable)」な形に修正した PRです。これにより、Ruby 3 の Ractor を用いた並列実行環境でもActiveSupport::TimeWithZone#to_sなどが安全に利用しやすくなります。
- 変更内容の詳細
※PR本文とファイル名・行数からの推定を含みます。実際のコードは概ね以下のような変更になっていると考えられます。
何をしているか
ActiveSupport::TimeFormatsは、Time/TimeWithZoneなどのto_s(:format_name)で使われるフォーマット定義を@listというハッシュに持っています。- その中の一部フォーマットは、
Proc/lambdaで定義されており、Ruby 3 の Ractor では「共有可能でないオブジェクト」があると Ractor 間共有に制限がかかります。 - この PR では、その lambda を Ractor で共有可能にするための書き換え・定義方法の変更を行っています。
なぜ lambda が問題になるのか
Ruby 3 の Ractor では、Ractor 間で共有できるのは「immutable かつ shareable なオブジェクト」に限定されます。
通常の lambda/Proc は、以下のような理由で shareable でないことがあります。
- 外側のスコープをキャプチャしていて、内部に非 shareable なオブジェクトを持つ
- 定義方法によっては Ractor の shareable フラグが立たない
この PR では、ActiveSupport::TimeFormats::DATE_FORMATS のような定数ないし @list に格納される lambda を
- 外部スコープをキャプチャしない「クローズオーバーなし」の形にする
- Ractor 起動前に一度だけ初期化される定数・クラスインスタンス変数にまとめる
などの方法で shareable にしています。
変更イメージ(サンプルコード)
例としてイメージすると、次のようなスタイルから:
@list[:db] = lambda { |time| time.utc.strftime("%Y-%m-%d %H:%M:%S") }のような「外部状態を取りうる lambda」を、
DB_FORMAT = "%Y-%m-%d %H:%M:%S".freeze
@list[:db] = ->(time) { time.utc.strftime(DB_FORMAT) }のように、
- フォーマット文字列を freeze 済みの定数に切り出し
- lambda 自体も外部をキャプチャしない & Ractor 起動前にセット
といった形に変えることで、ActiveSupport::TimeFormats.@list が Ractor.shareable? をパスするようにしている、というイメージです。
(実際の変更は +4/-4 行なので、よりピンポイントにブロック → lambda、あるいは Proc.new → -> に変えて shareable 判定を通す修正になっている可能性も高いです。)
- 影響範囲・注意点
影響範囲
- 主な対象:
ActiveSupport::TimeFormatsを利用する時間/日時の文字列化処理(Time#to_s(:format),TimeWithZone#to_s(:format)など)。 - 振る舞いの変更: フォーマット結果自体は変えず、「Ractor で
ActiveSupport::TimeFormatsを共有したときにエラーにならない/より安全に使える」ようにする内部実装変更です。 - 既存フォーマット名 (
:db,:number,:time,:short等) やカスタムフォーマットの API には変更が入っていないと考えられます。
Ractor を使う場合の注意点
- この PR は「標準で定義されているフォーマット (
@listに最初から入っているもの)」を shareable にするためのものです。 - アプリケーション側で追加するカスタムフォーマットが lambda/Proc を含む場合、それらが非 shareable だと、依然として Ractor からは問題になる可能性があります。
例:
# Ractor 的に危ない例(非 shareable かもしれない)
FORMATTER = SomeMutableObject.new
Time::DATE_FORMATS[:foo] = ->(time) { FORMATTER.call(time) }- Ractor を本格的に使う場合は、アプリ側でも:
- フォーマットの実装が外部のミュータブルオブジェクトをキャプチャしない
- 文字列・シンボル・数値などの不変オブジェクト(かつ freeze 済)のみを参照する といった形にしておく必要があります。
- 参考情報 (あれば)
- Ruby Ractor ドキュメント(shareable オブジェクトの制約)
https://docs.ruby-lang.org/en/master/doc/ractor_md.html ActiveSupport::TimeWithZone/ActiveSupport::TimeFormatsの概要- Rails ガイド: Active Support Core Extensions – Date and Time
https://guides.rubyonrails.org/active_support_core_extensions.html#extensions-to-date-and-time
- Rails ガイド: Active Support Core Extensions – Date and Time
- 類似の Ractor 対応 PR(参考になりそうなもの)
- 例: ActiveSupport の各種定数・キャッシュを Ractor shareable にする対応が Rails 7.x / 8.0 以降で進んでいます。
#57858 Only document the public ActionView::Template API [ci skip]
マージ日: 2026/6/25 | 作成者: @etiennebarrie
- 概要 (1-2文で)
このPRは、ActionView::Template のうち「アプリケーションから使うことを想定したメソッドだけ」を公式ドキュメントに載せるよう整理したものです。具体的にはregister_template_handlerを ActionView::Template の公開APIとして明示的にドキュメント化し、それ以外のテンプレート関連クラス/メソッドを内部APIとして非公開扱いにしています。
- 変更内容の詳細
2-1. register_template_handler を正式な公開APIとしてドキュメント化
- これまで:
- 新しいテンプレートタイプ(例: Haml, Markaby など)を Action View に認識させるための公式な手段は
register_template_handlerでしたが、 - 実装は
ActionView::Template::Handlersモジュールにあり、かつそのモジュールには:nodoc:が付いていたため、RDoc 上では「非公開っぽく」見えていました。
- 新しいテンプレートタイプ(例: Haml, Markaby など)を Action View に認識させるための公式な手段は
- 実際の利用例:
- Haml:
ActionView::Template.register_template_handler(:haml, Haml::Plugin) - Markaby:
ActionView::Template.register_template_handler(:mab, Markaby::TemplateHandler)
- Haml:
PRの方針:
register_template_handlerを ActionView::Template のクラスメソッドとして ドキュメント化。- 実際には
TemplateがTemplate::Handlersをextendしているため、「最終的な受け皿」としてActionView::Template.register_template_handlerが正しい呼び出し先であることを、ドキュメントでも明示する。 - ドキュメントの扱いを
local_assignsと同じ形に揃える(local_assignsも Template が extend したメソッドで、Template 自体のシングルトンメソッドとして説明されている)。
Rails アプリ/gem 側での利用イメージ:
# config/initializers/custom_template_handler.rb など
module MyApp
class MyTemplateHandler
# ActionView が呼び出すインターフェース
def self.call(template, source = nil)
# template: ActionView::Template インスタンス
# source: テンプレートの文字列 (オプション)
"MyCompiler.compile(#{template.source.inspect})"
end
end
end
ActionView::Template.register_template_handler :my, MyApp::MyTemplateHandlerこのような呼び出し方が「公式にサポートされている」という位置づけになります。
2-2. ActionView::Template のその他のメソッド・クラスを「内部API」として非公開化
PR で明確化された方針:
ActionView::Templateのうち、公開APIとして残すのは次の2つだけ:ActionView::Template.register_template_handlerActionView::Template#local_assigns(インスタンス側の公開エントリーポイント)
- それ以外はすべて内部実装として扱い、RDoc に出さない。
具体的な変更:
ActionView::Template以下で「ハンドラ関連の内部実装」に関する以下を:nodoc:または:stopdoc:で隠す:- レジストリ用ヘルパーメソッド
- ビルトインのハンドラクラス:
ActionView::Template::Handlers::ERBActionView::Template::Handlers::RawActionView::Template::Handlers::HtmlActionView::Template::Handlers::Builder
- Template 自体のクラスメソッド・インスタンスメソッドの大半
actionview/lib/action_view/template/handlers_*.rbに対して:- クラス定義に
:nodoc:を追加・維持する形で、RDoc の公開API一覧から除外。
- クラス定義に
actionview/lib/action_view/template.rbに:stopdoc:を追加:- 公開エントリーポイント(
register_template_handler,local_assigns)以降の部分はドキュメント生成対象から外す。
- 公開エントリーポイント(
結果として、APIドキュメント上では ActionView::Template のページに表示されるのは原則この2つのみになります。
2-3. class << self につけていた :nodoc: を削除
- 以前は
ActionView::Template内のclass << selfに:nodoc:を付けていたが、RDoc は シングルトンクラスに付いた:nodoc:を無視する 仕様。 - そのため、以下のようなクラスメソッドが誤って「公開API」としてドキュメントに出てしまっていた:
mime_types_implementation=validate_formatsnormalized_formats
- 今回:
- この
:nodoc:は実質的に意味がなかったため削除。 - 代わりに
:stopdoc:などの適切な手段で Template の内部メソッドが RDoc に出ないように制御。
- この
- 意図としては「これらのメソッドは引き続き存在するが、アプリケーションから触ることを前提にしていない内部API」であることを明確にした、という整理です。
- 影響範囲・注意点
3-1. 実行時の挙動への影響はほぼなし
- 変更は主に ドキュメント生成と可視性(
nodoc系の指示) に関するものです。 - メソッドのシグネチャや挙動を変更しているわけではないため、既存アプリケーション・gem の実行時動作には基本的に影響しません。
3-2. 「Rails 公開APIかどうか」の判断基準がより明確になる
- APIドキュメント上から消えたメソッドやクラスは「内部実装」であり、将来の非互換変更の対象になりやすいと解釈すべきです。
- 特に以下を直接使っている場合は注意:
ActionView::Template::Handlers::ERBなどのビルトインハンドラを直接参照しているコードActionView::Templateのクラスメソッド・インスタンスメソッドで、公式ガイドやAPIドキュメントに明示されていないものを呼んでいるコード
- 今後は、テンプレートハンドラを拡張したい場合は
ActionView::Template.register_template_handler一択であることがより明確になります。
3-3. gem 作者向けの注意点
- 既に多くのテンプレートエンジン系 gem(Haml, Markaby など)は
ActionView::Template.register_template_handlerを使っているため、このPRにより 「やっていることが正式に推奨される形になった」 という位置づけです。
- もし独自に Template や Handlers の内部メソッドに依存している場合は、将来の Rails アップグレード時に壊れるリスクが高まるため、公開APIへの移行を検討した方が安全です。
- 参考情報 (あれば)
- PR 本体:
https://github.com/rails/rails/pull/57858 register_template_handlerの利用例:- 関連する公開API(
local_assigns)のドキュメントはActionView::Templateの RDoc を参照すると、今回の整理後の形が分かります。
#57784 Fix inverse matching for association composite primary keys
マージ日: 2026/6/25 | 作成者: @Korri
- 概要 (1-2文で)
Rails の関連付けで「関連自体は複合主キーを使うが、所有側モデルのidは単一キー」というケースでinverse_ofの自動設定が壊れていた問題を修正した PR です。Association#matches_foreign_key?の比較ロジックを修正し、子オブジェクトの複合外部キーと親オブジェクト側の値が正しくマッチするようにしています。
- 変更内容の詳細
問題の背景
- 過去の PR #56664 で「配列 vs 配列」の inverse マッチング(主に複合主キー関連の比較)が修正された。
- しかし次のようなケースが未対応だった:
- 関連の
primary_keyは「複合キー(配列)」を宣言している - しかし所有クラス(親モデル)は通常どおり単一の
idを持っている
- 関連の
- この場合、
Association#matches_foreign_key?が親側オブジェクトの値を比較する際に、関連が指定している複合primary_keyではなく、クラスのデフォルトprimary_keyを使ってしまい、inverse マッチングに失敗していた。
イメージとしては次のような比較が期待されるのに、実際にはずれていた:
- 子の外部キー:
[shop_id, order_id] - 親側で比較すべき値:
[shop_id, id]← 関連が「[ :shop_id, :id ]」のような複合 primary_key を宣言しているパターン
この「配列同士」の比較で、親側が関連の active_record_primary_key を見ずにクラスの primary_key を見ていたためミスマッチが起き、inverse_of を正しくセットできない、というリグレッションになっていました。
コード上の修正ポイント
Association#matches_foreign_key? の修正
activerecord/lib/active_record/associations/association.rb で、関連の逆側(owner)を比較するときに使うキーを、クラスの primary_key ではなく「関連に紐づく active_record_primary_key」から取得するよう変更しています。
擬似コードで言うと、だいたい以下のようなイメージの変更です(実際のコードとは多少異なる可能性がありますが、意図を示すための例):
# 修正前(イメージ)
def matches_foreign_key?(record)
owner_key = Array(klass.primary_key).map { |key| owner.public_send(key) }
foreign_key = Array(reflection.foreign_key).map { |key| record.public_send(key) }
owner_key == foreign_key
end
# 修正後(イメージ)
def matches_foreign_key?(record)
# 関連が宣言している primary key を使う
owner_keys = Array(reflection.active_record_primary_key)
owner_values = owner_keys.map { |key| owner.public_send(key) }
foreign_keys = Array(reflection.foreign_key)
foreign_values = foreign_keys.map { |key| record.public_send(key) }
owner_values == foreign_values
endポイントは:
- 「どのカラムを親の比較キーとするか」を
reflection.active_record_primary_keyに委ねるようにしたこと - これにより、クラス自体は単一
idでも、特定の関連だけ「[:shop_id, :id]」のような複合 primary_key を宣言でき、その形で比較が行われる
テストの追加
activerecord/test/cases/associations_test.rb に以下のようなリグレッションテストが追加されています:
test_belongs_to_a_model_with_composite_association_primary_key_sets_inverse_of- belongs_to 先のモデルが「複合 association primary key」を持つ場合に、
inverse_ofが正しく設定されることを確認するテスト - 具体的には:
- 子モデル側の外部キーは
[shop_id, order_id] - 親モデル側は
[shop_id, id] - 上記が一致すると判断されて inverse 関連がセットされることを検証
- 子モデル側の外部キーは
- belongs_to 先のモデルが「複合 association primary key」を持つ場合に、
また、activerecord/test/models/cpk/book.rb と activerecord/test/models/cpk/order.rb に複合主キー関連の振る舞いを確認するためのモデル定義が追記されています。Book や Order モデルが、CPK (Composite Primary Key) シナリオのために拡張されており、今回の失敗パターンを再現するために利用されています。
CHANGELOG
activerecord/CHANGELOG.md に今回の修正内容が追加され、複合主キー関連の inverse matching に関する不具合が修正されたことが明示されています。
- 影響範囲・注意点
影響を受けるのは、次のようなアプリケーションです:
- Active Record の関連で、association 側で複合
primary_keyを宣言している - しかし、関連先のモデルクラス自体の
primary_keyは単一カラム (id) のまま - その上で
inverse_ofによる逆関連の自動設定(キャッシュ)に依存している
この PR によって:
- 上記のような構成でも
inverse_ofが意図どおり効き、親子オブジェクト間でのキャッシュ共有が正しく行われるようになります。 - 逆に言うと、これまで「なぜか inverse が張られない/N+1 的な振る舞いをする/同じレコードが別インスタンスとして存在する」といった怪しい挙動があった場合、今回の修正で自然に解消される可能性があります。
注意点:
- 仕様変更というよりはバグ修正であり、「ドキュメントどおりに設定した複合 primary_key + inverse_of の挙動がやっと正しくなった」という位置づけです。
- もし既にこのバグを前提にワークアラウンドを入れている場合(例: 手動で inverse 設定、比較ロジックの上書きなど)、Rails 更新後に挙動が二重に効いていないかを確認してください。
- テストコードで「inverse が nil であること」を前提としていたようなケースがあれば、そこが失敗する可能性があります。
- 参考情報 (あれば)
- 元の配列 vs 配列 inverse matching 修正 PR:
https://github.com/rails/rails/pull/56664 - 当該メソッド (
Association#matches_foreign_key?) の実装位置:activerecord/lib/active_record/associations/association.rb - リグレッションテスト:
activerecord/test/cases/associations_test.rbtest_belongs_to_a_model_with_composite_association_primary_key_sets_inverse_of - CPK モデル定義:
activerecord/test/models/cpk/book.rbactiverecord/test/models/cpk/order.rb
この辺りを追うと、「関連ごとに独自の primary_key(複合含む)を宣言し、それに従って inverse マッチングを行う」という現在の設計意図が読み取れます。
#57859 Better shareable_lambda usage
マージ日: 2026/6/25 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails内部で使われているshareable_lambdaの使い方を、よりシンプルで読みやすい形にリファクタリングしたPRです。挙動や外部APIは変えずに、定数として定義されているラムダの書き方を整理しただけの変更です。
- 変更内容の詳細
※PR本文と変更統計から読み取れる範囲での解説です(実際の差分は概ね以下のようなイメージの変更です)。
2-1. 背景: shareable_lambda とは
shareable_lambda は Rails 内部で使われるヘルパーで、スレッドセーフかつ再利用可能なラムダオブジェクトを簡潔に定義するためのものです。
たとえば、コールバックやミドルウェア、各種ディスパッチ処理で「毎回新しい lambda を作るのではなく、定数として共有したい」ような場面で使われます。
従来、以下のように「わざとらしくネストしたり、回りくどい書き方」になっていた箇所があった、というのが作者の自己コメントです。
SOME_CONST = shareable_lambda do |*args|
->(env) { ... }
endあるいは、無名モジュールや Proc を経由してから shareable_lambda を呼ぶような、「何をしているか一目で分かりづらい」構造になっていた部分があったと考えられます。
今回のPRでは、それらをよりストレートな形に書き直しています。イメージとしては以下のような変化です:
Before(あくまで例)
class ActionController::Metal
SOME_HANDLER = shareable_lambda do |controller, request|
# 何か処理
end
endに対して、更に回りくどいラップがあったり、定数の中で別の lambda を返していたりしたのを、
After(イメージ)
class ActionController::Metal
SOME_HANDLER = shareable_lambda do |controller, request|
# 同じ処理だが、直接わかりやすく記述
end
endというように、「一目で引数と処理内容が分かる」形に整えています。
2-2. どのファイルが対象か
以下の6ファイルで、定数として定義されているラムダの書き方が整理されています(+14/-14 なので、ほぼ1行単位の書き換えのみ):
actionpack/lib/action_controller/metal.rb- コントローラの基底クラス周辺で使われるラムダ定数(例えば、リクエスト処理やレスポンス生成のための共有ラムダ)に対して、
shareable_lambdaの使い方を簡潔化。
- コントローラの基底クラス周辺で使われるラムダ定数(例えば、リクエスト処理やレスポンス生成のための共有ラムダ)に対して、
actionpack/lib/action_dispatch/http/parameters.rb- パラメータのパース・フィルタリングなどに使われるラムダ(パラメータ処理用の共通関数)が、より素直な
shareable_lambda定義に。
- パラメータのパース・フィルタリングなどに使われるラムダ(パラメータ処理用の共通関数)が、より素直な
actionpack/lib/action_dispatch/journey/visitors.rb- ルーティングツリー(Journey AST)を辿る visitor 用ラムダの定義が読みやすく整理。
actionpack/lib/action_dispatch/routing/mapper.rb- ルーティング定義(
resourcesなど)をマッピングする際に使う共有ラムダがリファクタリング。
- ルーティング定義(
actionpack/lib/action_dispatch/routing/route_set.rb- ルートセットが内部的に使用するディスパッチ用ラムダなどが、より直接的な
shareable_lambda使用に変更。
- ルートセットが内部的に使用するディスパッチ用ラムダなどが、より直接的な
activesupport/lib/active_support/log_subscriber.rbActiveSupport::LogSubscriberがログ出力時に使用する共有ラムダの書き方が整理。
たとえば、イベントを受け取ってログフォーマットする処理などがshareable_lambdaで共有されていますが、その宣言方法が素直な形に。
コードレベルでは、
- 不要なネスト(
-> { shareable_lambda { ... } }のようなもの) - 意味のない中間変数・中間オブジェクト
- ブロックのスコープが分かりづらい書き方
などを取り除き、「この定数はこのラムダを指している」ことが一目でわかるように整えた、という性質の変更です。
- 影響範囲・注意点
- 挙動は変わらない想定
- 変更はリファクタリング(スタイルの改善)であり、ロジック自体は変えていません。
- テスト追加はされていませんが、既存のテストがグリーンであることが前提の軽量変更です。
- 外部APIへの影響なし
- 変更対象はすべて内部定数・内部実装(ActionPack / ActiveSupport の内部で使用されるラムダ)であり、パブリックAPIのインターフェイスは変わっていません。
- パフォーマンスへの影響はほぼなし(あるとしても僅少な改善)
shareable_lambdaの用途に沿った、より素直な使い方になったことで、不要なオブジェクト生成が減っている可能性はありますが、差分規模的に体感できるほどではないと考えられます。
- Rails内部コードを読む人にとっては可読性向上
- ルーティングやログ周りなどの内部実装を追う際に、「この定数がどんなラムダなのか」が以前より分かりやすくなっています。
- gemやアプリでRailsの内部定数を直接参照・モンキーパッチしているようなかなり特殊なケースを除けば、互換性問題はほぼ起こりません。
- 参考情報 (あれば)
- PR #57859: Better shareable_lambda usage
- 関連するRails内部機構
ActiveSupport::LogSubscriberActionDispatch::Routing(Journey,RouteSet,Mapper)ActionController::Metal
shareable_lambda自体の使い方の参考としては、Railsコードベース全体での定数定義パターンを見るとよいです(例:Rails.application.routes周辺や、ログサブスクライバの実装など)。
#57853 Fix incorrect number_to_rounded doc example [ci skip]
マージ日: 2026/6/25 | 作成者: @antonzaharia
- 概要 (1-2文で)
ActiveSupport::NumberHelper#number_to_roundedの RDoc に記載されていたprecision: 0の出力例が、実装と異なり「切り捨て」のように見えていた問題を修正し、「四捨五入」の挙動に合わせた正しい例に更新した PR です。コードの挙動自体は従来どおりで、ドキュメントのみの修正です。
- 変更内容の詳細
対象:activesupport/lib/active_support/number_helper.rb 内の number_to_rounded の RDoc コメント(ドキュメント例)
修正前のドキュメント例では:
number_to_rounded(12345.6789, precision: 0) # => "12345"と記載されていましたが、実際のメソッドの挙動は「四捨五入」であり、返り値は "12346" になります。
そのため、以下のように修正されています:
number_to_rounded(12345.6789, precision: 0) # => "12346"PR 説明にある検証例:
ActiveSupport::NumberHelper.number_to_rounded(12345.6789, precision: 0) # => "12346"
ActiveSupport::NumberHelper.number_to_rounded(12345.4, precision: 0) # => "12345" # 0.4 → 切り捨てではなく四捨五入の結果
ActiveSupport::NumberHelper.number_to_rounded(12345.6, precision: 0) # => "12346"ポイント:
precision: 0の場合も挙動は「小数第1位で四捨五入」であり、整数化・切り捨てではない。- 既に他の例(
12345.679,12345.68,12345.00000)は正しく四捨五入の結果になっており、この 1 行だけが誤っていた。
- 影響範囲・注意点
- 実装コードは一切変更されておらず、挙動は既存と同じです。
- 影響があるのは ドキュメントを読んで実装方針を決める開発者の理解 だけです。
- 特に「
precision: 0は小数点以下を切り捨てて整数にしてくれる」と誤解していた場合は注意が必要です。 - 実際には「小数第1位を四捨五入して整数にする」挙動なので、
12345.6789→12346になります。
- 特に「
- 小数点以下を「常に切り捨てたい」場合は、
number_to_roundedではなく以下のような別手段を検討すべきです(例):- Ruby の標準メソッド:
number.floor/number.truncate - あるいはビジネス要件に応じた前処理をしてから
number_to_roundedに渡す
- Ruby の標準メソッド:
- 参考情報 (あれば)
- 該当メソッド:
ActiveSupport::NumberHelper#number_to_rounded- 主にビューやヘルパーで数値フォーマットを行うためのヘルパー。
- 丸めモードを詳細に制御したい場合には、
NumberHelperのオプションやBigDecimal、roundのhalf:オプションなどと組み合わせて挙動を確認するとよいです。
#57602 Make Active Record Railtie Ractor safe
マージ日: 2026/6/25 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Active Record の Railtie 内で使われている message verifier 用の Proc を「Ractor 共有可能(Ractor-shareable)」な形に変更し、Ractor 環境でも安全に動作するようにした PR です。Rails の初期化処理と Ractor の併用時に、Active Record が Ractor の制約に抵触しないようにするのが目的です。
- 変更内容の詳細(あればサンプルコードも含めて)
※PR 本体の diff は 3 行追加 / 3 行削除のみで、activerecord/lib/active_record/railtie.rb 内の「message verifier を生成する Proc」の定義が Ractor で共有可能な形に書き換えられています。
背景となる問題
Ruby の Ractor では以下のような制約があります:
- Ractor 間でオブジェクトをやり取りするには「shareable」である必要がある
- もしくはコピーして渡す(が、Rails の設定オブジェクトなどはコピーでは困る場合がある)
- Proc / lambda / block などは、環境(クロージャ)に「非 shareable なオブジェクト」が閉じ込められていると shareable にならない
Rails の Railtie は、アプリケーション起動時に設定を行う部分で、Active Record では「メッセージ検証(MessageVerifier)」関連の仕組みを Proc で定義している箇所があります。
この Proc が Config や logger などの非 shareable なオブジェクトをキャプチャしていると、Ractor からその Proc を参照した時点でエラーになる可能性があります。
この PR の具体的な変更
この PR では、その「message verifier を返す Proc」について:
- Proc の中でキャプチャしているオブジェクトを見直し
- Ractor.shareable? が true になるような形で定義し直し
という変更が行われています。
擬似コードでイメージを書くと、従来は例えば:
# 例: Ractor 非対応になりがちなパターン
config = app.config
ActiveRecord::Railtie.config.active_record.verifier_generator = proc do |name|
ActiveSupport::MessageVerifier.new(config.some_secret_for(name))
endのように config をクロージャ内でキャプチャしており、config が shareable でないために Proc 自体も非 shareable になる、という状態だったと考えられます。
これを、例えば次のように「Proc 内でグローバルな設定・クラスメソッド等を参照するだけ」にする、あるいは「Proc を定数として freeze して共有可能にする」といった形に修正していると推測されます:
# 例: Ractor 共有可能にしやすい書き方のイメージ
ActiveRecord::Railtie.config.active_record.verifier_generator =
->(name) do
ActiveSupport::MessageVerifier.new(Rails.application.credentials.dig(:active_record, name))
end
# もしくは config を事前に凍結して shareable にする など実際のコードは Rails 本体に依存しますが、「message verifier を生成する Proc で、Ractor が問題にするような閉じ方をしないよう修正した」という点が本質です。
- 影響範囲・注意点
影響範囲
- Active Record の Railtie(
ActiveRecord::Railtie)が提供する「message verifier 関連の設定」部分のみが対象 - ActiveRecord の通常の利用(モデル定義、クエリ、migration など)にはほぼ影響なし
- Ractor を利用せず、マルチスレッドのみで動いているアプリケーションは基本的に挙動変化はありません
この変更により:
- Rails アプリケーションを Ractor 内で起動する / Ractor 内から ActiveRecord の Railtie 初期化に触れるようなケースで、Ractor の shareable 制約に起因するエラーが発生しにくくなります。
- Ractor を使った並列処理環境で Active Record を利用する場合の安全性が向上します。
注意点
- PR のチェックリストでは「テストの追加・更新」「CHANGELOG 更新」は未チェックのままマージされています
- 挙動変更はごく小さく、既存テストでカバーされている/後方互換と判断されている可能性が高いですが、Ractor を積極的に使う場合は Ruby のバージョンと Rails 本体の組み合わせで挙動を確認することを推奨します。
- もしアプリ側で
ActiveRecord::Railtieの message verifier 周りの設定を独自に上書きしている場合:- 自作の Proc / lambda が非 shareable なオブジェクト(例: コネクションプールインスタンス、ミューテックス、非凍結の設定オブジェクト)をクロージャに閉じ込めていると、依然として Ractor で問題になる可能性があります。
- Ractor を使う予定がある場合は、自前設定も
Ractor.shareable?が true になるように設計する必要があります。
- 参考情報 (あれば)
- Ruby Ractor の shareable 仕様
- Rails の Railtie について
- ActiveSupport::MessageVerifier の概要
この PR 自体は極小変更ですが、「Rails コアが Ractor 対応を少しずつ進めている」流れの一環であり、将来的に Ractor + Rails を組み合わせたい場合には重要な基盤整備といえます。
#57849 Bump cgi to 0.5.2
マージ日: 2026/6/24 | 作成者: @yahonda
- 概要 (1-2文で)
Rails の CI(特に guides ジョブ)でcgiのバージョン不一致により落ちていたため、Gemfile.lock内のcgiを 0.5.2 に上げて CI を安定化させる PR です。コード挙動を変える修正ではなく、CI 環境と rubygems.org 上の最新版を揃えるためのメンテナンス的変更です。
- 変更内容の詳細
実際の変更
変更は Gemfile.lock の 1 行のみで、cgi のバージョンを更新しています。
- cgi (0.5.0)
+ cgi (0.5.2)※PR 説明中では「0.5.0 → 0.5.2」とありますが、CI のログメッセージには「You have already activated cgi 0.5.1」と出ているため、CI イメージ上の実際のバージョンは 0.5.1 で動いていた可能性があります。いずれにせよ、Gemfile.lock を 0.5.2 に揃えることで、CI 上の事前バンドル済みバージョンと rubygems.org 上の最新版を一致させる狙いです。
なぜ guides ジョブだけが落ちたのか
Rails の guides CI ジョブでは、guides/bug_report_templates/*.rb が以下のような形で実行されています。
- 各テンプレートは
Bundler.unbundled_systemで実行される - さらにテンプレート内で
bundler/inlineのgemfile(true)を使っており、プロジェクトのGemfile.lockとは独立して rubygems.org に対して最新版を解決する
そのため:
CI イメージには、すでに何らかのバージョンの
cgi(例: 0.5.1)がインストール済み・アクティベート済みbug_report_templates/action_controller.rbが最初に実行されるとき、gemfile(true)により rubygems.org の最新版cgi0.5.2 を要求すると Bundler が「もう
cgi 0.5.1がアクティベートされているのに、Gemfile では0.5.2が必要」と判断して、次のエラーで落ちる:textYou have already activated cgi 0.5.1, but your Gemfile requires cgi 0.5.2. Prepending `bundle exec` to your command may solve this. (Gem::LoadError)その後に実行される他のテンプレートは、すでにインストール済みの
cgi 0.5.2を再利用して通るため、最初のテンプレートだけがコケてジョブ全体が失敗する状態になる
この PR の狙い
Gemfile.lock 側を cgi 0.5.2 に bump することで:
- CI ベースイメージを再構築するときに 最初から
cgi 0.5.2がGEM_HOMEに入った状態 になる - その結果、
guides/bug_report_templates/action_controller.rbが最初に走る時点で、すでにcgi 0.5.2がアクティベートされているので、途中で0.5.2を新規インストールしようとしてバージョン競合を起こすことがなくなる
このように、「CI イメージ内のプリインストール版」と「inline Gemfile が要求する版」を一致させて、Gem::LoadError を回避するのがこの PR の主目的です。
- 影響範囲・注意点
- 実際の Rails のアプリケーションコードには一切変更がなく、
Gemfile.lockのcgiバージョンのみが上がるため、Rails 自体の API や挙動に直接の変更はありません。 - 影響範囲としては主に以下です:
- Rails 本体の CI(特に guides ジョブ)が安定して通るようになる
- Rails を開発している環境で
bundle installした場合、開発者のローカルにもcgi0.5.2 がインストールされる
cgi自体は Ruby 標準の default gem 系列であり、今回のような「default gem の新バージョンが rubygems.org で公開され、それを inline Gemfile が取ってくる一方で、CI イメージ内には古いバージョンがある」というパターンで 定期的に同様の問題が発生 していることが説明されています。- PR の説明にもある通り、これは以下のような既存対応の延長線上にある「1行 Gemfile.lock bump」で、既存の運用ルールに沿った標準的な対処です:
- stringio 3.1.1 への bump
- strscan 3.0.4 への bump
- psych 5.1.2 / 5.1.1.1 への bump
- timeout 0.4.1 への bump
注意点としては、根本原因は rubygems / Bundler 側の default gem の扱いにあるため、将来的には rubygems 側の修正でこの種の問題をなくす方向が想定されています(下記参照)。
- 参考情報 (あれば)
- PR 本文で言及されている、根本対策となりうる rubygems 側の Issue:
Bundler.unbundled_systemとbundler/inline(gemfile(true)) の挙動:unbundled_system: 現在の Bundler コンテキストを外して別プロセスを実行し、プロジェクトの Gemfile とは独立した解決を行わせるbundler/inlineのgemfile(true): スクリプト内から Gemfile を宣言し、その場で bundle install + 実行を行う。trueにするとスクリプト終了後に Gemfile を破棄する一時的な利用モード
- 実務的には、Rails 本体に対して:
- この PR を取り込んだ後に CI ベースイメージを再ビルドする(あるいは CI 側で
bundle installをやり直す)ことでエラー解消が見込めます。
- この PR を取り込んだ後に CI ベースイメージを再ビルドする(あるいは CI 側で
#57847 Move Action Cable configuration to top level
マージ日: 2026/6/24 | 作成者: @samuel-williams-shopify
- 概要 (1-2文で)
Action Cable の設定クラスをActionCable::Server::ConfigurationからトップレベルのActionCable::Configurationに抽出し、旧クラスは互換用エイリアスとして残すリファクタリングです。既存コードを壊さずに、今後のサーバー入れ替えフックなどの拡張をしやすくするための下準備となる変更です。
- 変更内容の詳細
主な変更点
新規:
ActionCable::Configuration- これまで
ActionCable::Server::Configurationに実装されていた設定ロジックを、そのままトップレベルのActionCable::Configurationに移動。 - Action Cable の各種設定(例:
url,logger,connection_class,channels_pathなど)が、この新クラスに集約される形になります(実装自体はほぼそのまま移動)。
- これまで
互換性用エイリアス:
ruby# actioncable/lib/action_cable/server/configuration.rb module ActionCable module Server Configuration = ActionCable::Configuration end endのように、
ActionCable::Server::ConfigurationはActionCable::Configurationへの単なる定数エイリアスに変更されました。- これにより、既存コードで
ActionCable::Server::Configurationを参照していても動作は変わりません。
- これにより、既存コードで
テストの追加:
actioncable/test/server/base_test.rbに、以下を満たすテストが追加されています:ActionCable::Server::ConfigurationがActionCable::Configurationのエイリアスになっていること。- デフォルトのサーバー設定オブジェクトが正しく生成されること。
CHANGELOG 更新:
- Action Cable の CHANGELOG に、この設定クラスのトップレベルへの移動が記載されました。
利用イメージ(概念的なもの)
これまで:
config = ActionCable::Server::Configuration.new
config.cable = { adapter: "redis" }今後推奨される書き方:
config = ActionCable::Configuration.new
config.cable = { adapter: "redis" }既存コード:
config = ActionCable::Server::Configuration.newも、内部的には新しい ActionCable::Configuration を指すため、そのまま動作します。
- 影響範囲・注意点
互換性:
ActionCable::Server::Configurationはエイリアスとして残っているため、既存アプリケーションは基本的に影響を受けません。- ただし、
ActionCable::Server::Configurationのクラス名文字列や定義場所に依存したメタプログラミングをしている場合(name,to_s,const_source_location等)、挙動がわずかに変わる可能性があります。
推奨される移行:
- 新規コードやリファクタリング時には、
ActionCable::Server::ConfigurationではなくActionCable::Configurationを直接使う のが今後の標準となります。 - gem やエンジンなどで Action Cable を拡張している場合は、サポートバージョンを見ながら、将来的に参照先をトップレベルの
ActionCable::Configurationに切り替えることを検討するとよいです。
- 新規コードやリファクタリング時には、
今後の変更の布石:
- この PR は、別 PR (#57803) で提案されている「Action Cable サーバー差し替えフック」のための事前分割です。
- 設定オブジェクトをサーバー実装 (
ActionCable::Server) から切り離すことで、異なるサーバー実装(例: 独自サーバー / 別プロセス / 別アダプタ)と組み合わせやすくなります。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57847
- 関連 PR(サーバー置き換えフック): https://github.com/rails/rails/pull/57803
- 実行された検証コマンド:bash
ruby -Iactioncable/test actioncable/test/server/base_test.rb bundle exec rubocop \ actioncable/lib/action_cable/configuration.rb \ actioncable/lib/action_cable/server/configuration.rb \ actioncable/test/server/base_test.rb
#57844 Remove the use of $, as a default value for safe_join
マージ日: 2026/6/24 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Rails のActionView::Helpers::OutputSafetyHelper#safe_joinが、暗黙にグローバル変数$,を区切り文字のデフォルトとして使う挙動をやめ、常にnil(=区切りなし)をデフォルトとするように変更されました。これにより、Ractor セーフティ向上と、Ruby 本体で非推奨な$,への依存解消が行われています。
- 変更内容の詳細
何が変わったか
safe_joinのメソッド定義のデフォルト引数が変更されました。変更前(概念的にはこうなっていた):
rubydef safe_join(array, sep = $,) # ... end変更後:
rubydef safe_join(array, sep = nil) # ... endactionviewの CHANGELOG にこの挙動変更が追記されています。safe_joinが$,を使うことを前提としたテストが削除されています(output_safety_helper_test.rbで該当テストが -18 行)。
動作イメージ
変更後の safe_join の基本的な使い方は従来どおりです:
# 明示的に区切り文字を渡すパターン(今後も推奨)
safe_join(["a", "b", "c"], ", ")
# => "a, b, c"
# デフォルト(sep を渡さない)時は区切り無し
safe_join(["a", "b", "c"])
# 変更前: $,.nil? でなければ $, に依存
# 変更後: 常に "abc"Ruby のグローバル変数 $,($OUTPUT_FIELD_SEPARATOR)を設定していた場合、以前はそれが safe_join のデフォルト区切りとして使われ得ましたが、今後は一切利用されず、常に nil(区切りなし)となります。
- 影響範囲・注意点
影響を受けるケース
アプリケーション側で
$,を設定しつつ、safe_joinを 第2引数無しで呼び出し、かつ「$,がデフォルト区切りとして使われる」ことを暗黙に期待していた場合にのみ影響します。例:
ruby$, = ", " safe_join(["foo", "bar"]) # 変更前: "foo, bar" # 変更後: "foobar"ただし PR の説明によると:
- Rails の公式ドキュメントでは「
safe_joinが$,を参照する」ことは一切説明されていない。 - Ruby では
$,の設定自体が 10 年近く非推奨になっている。 - GitHub 全体で
$, =を使っている Ruby コードは非常に少ない。
ため、この挙動に依存しているアプリは非常にレアであることが見込まれています。
- Rails の公式ドキュメントでは「
移行時の注意点・対応策
safe_joinに暗黙の区切りを期待しているかもしれないコードを確認する場合:- プロジェクト内で
$, =を検索し、セットしている場所がないか確認する。 - もし
$,を設定していたら、その周辺でsafe_join(array)のように第2引数なしで使っている箇所がないか確認する。
- プロジェクト内で
もし挙動変更の影響を受けている場合は、以下のように 常に第2引数で区切り文字を明示してください:
ruby# 悪: 以前はたまたま $, に依存していた safe_join(items) # 良: 区切り文字を常に明示する safe_join(items, ", ")Ractor を使う/検討している環境では、この変更により
safe_joinが Ractor セーフになる方向に寄与します(グローバル可変状態への依存が消える)。
- 参考情報 (あれば)
- PR 本文で言及されている背景:
$,は Ractor セーフではないため、並行実行モデルとの相性が悪い。- Rails は
safe_joinのデフォルト区切りが$,であることをドキュメント化しておらず、「ドキュメント外仕様を前提としたコード」に対しては非互換変更とみなさない判断。
- Ruby 側の状況:
$,($OUTPUT_FIELD_SEPARATOR)の設定は Ruby 本体で長期的に非推奨(deprecated)扱い。- 今後の Ruby/Rails の流れとしても、こうしたグローバル変数依存を排除していく方向性と整合的な変更です。
#57829 Refactor Arel::Table to use keyword name: argument
マージ日: 2026/6/24 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Arel::Tableのコンストラクタ引数が「位置引数+オプション」で name を渡す形から、「キーワード引数 name:」で渡す形にリファクタリングされた PR です。これに伴い、Arel::Tableを直接生成している内部コード・テストコードがすべて新しい呼び出し方に書き換えられています。
- 変更内容の詳細
背景
- 直前の PR #57642 で
Arel::Tableに「任意の追加引数」が導入されたが、その追加引数は位置引数として定義されていた。 - その結果、クラス (
klass) を指定するために「name, klass, …」のようなシグネチャになり、「クラスを指定しない場合でもnilを位置引数として渡す」ような呼び方が発生していた。 Arel::Tableは#:nodoc:で「Rails 内部の非公開 API」として扱われているため、内部仕様を破壊的に整理しやすい。
この PR では、その「オプションの引数」をキーワード引数に切り替えることで、わかりやすく安全なインターフェイスに整理しています。
Arel::Table のコンストラクタ変更
以前は大まかに次のような形でした(イメージ):
# 旧: 位置引数で name, klass を取る
Arel::Table.new("users") # klass 省略
Arel::Table.new("users", User) # klass 指定
Arel::Table.new("users", nil, type_caster) # name だけ指定したいが nil を渡す必要があるケースこの PR により、name がキーワード引数になり、「name」を必ず明示的に指定する形にリファクタリングされています:
# 新: name をキーワードで指定
Arel::Table.new(name: "users")
Arel::Table.new(name: "users", klass: User)
Arel::Table.new(name: "users", type_caster: type_caster)ポイント:
- 「
nilをダミーで渡して位置を合わせる」ようなコードが不要になる。 name:というキーワードを通じて、引数の意味が明示的になる。Arel::Table自体は nodoc(内部 API)なので、こうしたシグネチャ変更を割と大胆に行っている。
呼び出し側の修正
Arel::Table.new を呼んでいる箇所(Active Record 内部や Arel のノード生成周りのコード)がすべて新しい形に書き換えられています。
例としてありそうな変更イメージ:
# 旧
Arel::Table.new(table_name)
Arel::Table.new(table_name, klass)
# 新
Arel::Table.new(name: table_name)
Arel::Table.new(name: table_name, klass: klass)対象は以下のようなファイル群です(一部抜粋):
activerecord/lib/active_record/connection_adapters/abstract/database_statements.rbactiverecord/lib/active_record/internal_metadata.rbactiverecord/lib/active_record/relation/query_methods.rbactiverecord/lib/active_record/schema_migration.rbactiverecord/lib/active_record/table_metadata.rbactiverecord/lib/arel/nodes/cte.rbactiverecord/lib/arel/nodes/node.rbactiverecord/lib/arel/table.rb(本体)- Arel 関連のテスト群(
activerecord/test/cases/arel/...一式)
テストファイルでは、Arel::Table.new(:users) のような呼び出しが大量にあり、それらが一斉に Arel::Table.new(name: :users) のような形に置き換えられているため、+392/-392 と行数だけ見ると大きく見えますが、ロジックとしては「呼び出し方の統一・リファクタ」に留まっています。
- 影響範囲・注意点
影響範囲
- 公式には
Arel::Tableは private API(nodoc)ですが、Arel を直接使って SQL を組み立てているアプリ/ライブラリでは影響を受ける可能性があります。 - この PR により、
Arel::Table.newのシグネチャが変わっているため、Rails 内部と同じく Arel を直接 new しているコードは修正が必要になります。
想定されるブレイク
もし自前コードで下記のように書いていると壊れます:
# 壊れる可能性のあるコード例(旧スタイル)
users = Arel::Table.new(:users)
users = Arel::Table.new(:users, User)
users = Arel::Table.new(:users, nil, some_type_caster)この場合、次のように修正する必要があります:
# 新スタイル
users = Arel::Table.new(name: :users)
users = Arel::Table.new(name: :users, klass: User)
users = Arel::Table.new(name: :users, type_caster: some_type_caster) # 引数名は実装に合わせる注意点
Arel::Tableは private API なので、この変更はセマンティクス上は「非公開 API の内部整理」に分類されますが、実際には Arel を直接使っているコードが多いため、マイナーアップデートでも破壊的変更と感じられる可能性があります。- この PR 自体には新しいテスト追加はなく、既存テストの引数形式のみ更新されているため、「機能的な振る舞いの変更」は意図していないと見てよいです(インターフェイスの形のみ変更)。
- CHANGELOG は更新されていないため、この変更に気づきにくい可能性があります。Arel を直接触っている場合は、アップデート時に
Arel::Table.newの使い方を確認することを推奨します。
- 参考情報 (あれば)
この PR の動機となった前提変更:
https://github.com/rails/rails/pull/57642
(Arel::Tableにオプションの追加引数が入った PR)Arel::Tableは Rails の public API ではないものの、Arel ベースでクエリを組む高度なユースケースでは利用されることが多いため、Rails アップグレードガイドやコミュニティ情報も確認すると安全です。
#57833 Only reset predicate builder on table_name exists
マージ日: 2026/6/24 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Active Record のtable_nameが「遅延計算(lazy)」になったことで、最初の計算時にもpredicate_builderがリセットされてしまう不具合があり、それを修正する PR です。具体的には、すでにpredicate_builderが存在する場合にだけリセットするよう条件を追加し、テーブル名の初回決定ではリセットされないようにしています。
- 変更内容の詳細
何が問題だったか
- rails/rails#57642 で
table_nameの算出が「必要になるまで計算しない」方式に変更された。 - その実装の中で、「
table_nameが変わったらpredicate_builderをリセットする」という処理が行われていた。 - しかし「初めて
table_nameを計算したタイミング」でも、このリセットが実行されてしまい、- クエリ時にカスタムな型ハンドラ(テストでは Regexp を使用)を使って
table_nameを決定するようなケースで、想定外にpredicate_builderが作り直され、不整合が起きる可能性があった。
- クエリ時にカスタムな型ハンドラ(テストでは Regexp を使用)を使って
- 上記が issue rails/rails#57824 で報告された不具合。
この PR での修正内容
対象ファイルは 1 行差分のみ:
activerecord/lib/active_record/model_schema.rb
- 変更前(概念的なイメージ):
def table_name=(name)
@table_name = name
@predicate_builder = nil # 常にリセット
endあるいは、table_name の決定ロジックの中で「テーブル名変化 → @predicate_builder = nil」としていたが、
初回計算時にも走ってしまっていた。
- 変更後(概念的なイメージ):
def table_name=(name)
@table_name = name
@predicate_builder = nil if defined?(@predicate_builder) && @predicate_builder # 既にあるときだけリセット
end実際の 1 行変更は:
- 「無条件で
predicate_builderをnilにしていた」コードを - 「
predicate_builderがすでにセットされているときにだけnilを代入する」条件付きのコードに変えた
という趣旨です。
挙動イメージ
- 初回に
table_nameが計算されるケース:- 以前:
predicate_builderがあれば・なければに関わらずリセットしようとしていた。 - 今回:
@predicate_builderがまだ存在しなければ、何も起きない(リセットされない)。
- 以前:
- 一度
table_nameが決まり、その後でtable_nameを書き換えるケース:- 以前: リセットされる(正しい)。
- 今回: 依然としてリセットされる(
@predicate_builderが既にあればnilになる)。
これにより、「テーブル名が本当に変更された場合だけ predicate_builder をリセットする」という本来意図していた挙動になります。
- 影響範囲・注意点
- 対象: Active Record のクエリ構築ロジック(
predicate_builder)とtable_nameを動的に決めるようなコード。- 特に、テーブル名をカスタムな型ハンドラ(例:
Regexp)を通して解決しているケースで不具合が出ていた。
- 特に、テーブル名をカスタムな型ハンドラ(例:
- この変更による影響:
- テーブル名を「初めて」計算するときには、
predicate_builderはリセットされない。 - テーブル名を「すでにある状態から変更した」ときには、今まで通り
predicate_builderがリセットされる。
- テーブル名を「初めて」計算するときには、
- 互換性:
- 一般的なアプリケーション(
table_nameを普通に固定の文字列で指定しているだけ)の場合、挙動はほぼ変わらない。 - issue #57824 で報告されていたような「遅延決定されるテーブル名+カスタム型ハンドラ+高度なクエリ構築」をしているケースでは、今回の修正によってクエリが安定することが期待される。
- 一般的なアプリケーション(
- テスト:
- PR 説明上は「テスト追加/更新」はチェックされていない(未実施扱い)ですが、変更は 1 行の条件追加のみで、リスクは比較的小さい範囲です。
- それでも、
table_nameを runtime で動的に変えるライブラリ/gem を使っている場合は、Rails 更新後にクエリ周りのテストを一通り回しておくのが無難です。
- 参考情報 (あれば)
- 本 PR が修正する Issue:
- 関連する以前の変更(テーブル名の lazy 計算導入):
- 変更ファイル:
activerecord/lib/active_record/model_schema.rb(1 行のみ差分)
#57832 Bump json to 2.20.0 and pass allow_comments: true
マージ日: 2026/6/24 | 作成者: @yahonda
- 概要 (1-2文で)
json gem を 2.20.0 に更新し、Rails 本体が「コメント付き JSON(JSONC)」をパースしている箇所でallow_comments: trueオプションを明示的に渡すようにした PR です。これにより、json 2.20.0 で追加された「コメント検出時の非推奨警告」による nightly テストの失敗を解消しています。
- 変更内容の詳細
json gem のバージョンアップ
Gemfile.lockjsonを2.15.2→2.20.0に更新
このバージョンから、デフォルトでは JavaScript 由来のコメント (// ... や /* ... */) を JSON 内でサポートすることが非推奨となり、コメントを含む JSON をパースすると警告が出るようになりました。
将来的な json 3.0 では、この挙動が「警告」から「エラー」に格上げされる予定です。
allow_comments: true を付けた箇所
コメント入り JSON を「意図的に」読み込んでいる箇所にだけ allow_comments: true を追加しています。
1) Active Support のメッセージシリアライザ fallback
ファイル: activesupport/lib/active_support/messages/serializer_with_fallback.rb
- 不正確(=「イレギュラー」)な JSON を読むテストケース向けの
_load実装で、ActiveSupport::JSON.decodeを経由して JSON を読む箇所にallow_comments: trueを追加。
イメージとしては以下のような変更です(擬似コード):
# 変更前
ActiveSupport::JSON.decode(payload)
# 変更後
ActiveSupport::JSON.decode(payload, allow_comments: true)このテストは「コメント付き JSON を読めること」を意図的に検証しているため、コメント許可オプションを明示的に指定する方針になっています。
2) Railties: devcontainer.json 関連 (JSONC by spec)
ファイル:
railties/lib/rails/generators/rails/db/system/change/change_generator.rbrailties/test/generators/generators_test_helper.rb
devcontainer.json は実質 JSONC (コメントを含む JSON フォーマット) が仕様として許容されているファイルであり、Rails 側でもコメントを含んだ状態で生成・再読込しています。
そのため、以下のように JSON 読み込み処理に allow_comments: true を追加:
# 変更前(概略)
JSON.parse(File.read(devcontainer_path))
# 変更後(概略)
JSON.parse(File.read(devcontainer_path), allow_comments: true)テストヘルパ側でも同様に JSON.parse 呼び出しに allow_comments: true を付与し、devcontainer.json に含まれるコメントで警告が出ないようにしています。
解消したテスト失敗
json 2.20.0 適用後に nightly CI で失敗していたのは、Strict Warnings モードで「警告を例外化」しているテストです。
該当ログはいずれも「Encountered comment in JSON. This will raise an error in json 3.0 unless enabled via allow_comments: true」という警告を原因として落ちていました。
MessagesSerializerWithFallbackTest#test_:json_serializer_can_load_irregular_JSONAppGeneratorTest#test_devcontainer_skip_kamalRails::Generators::Db::System::ChangeGeneratorTest#test_change_from_db_with_devcontainer_service_to_one_without
今回の修正で、これらのテストは再び通るようになります。
- 影響範囲・注意点
Rails 本体の影響範囲は限定的
- 変更ファイルは 4 つ、行数も +5/-5 とごく小規模で、影響箇所も「コメント付き JSON を読むことを前提にしている箇所」に限られています。
- 既存のコメントなし JSON を読む処理は挙動に変化はありません。
将来の json 3.0 への備え
- コメント付き JSON を読みたい場合は、今後は
allow_comments: trueを必須で付けるべき、という upstream の方針に沿っています。 - Rails 内部でコメント付き JSON を扱う箇所は、今回の対応で将来の 3.0 でも動作継続できるようになります。
- コメント付き JSON を読みたい場合は、今後は
アプリケーション側開発者への示唆
- 自前コードで
JSON.parse/ActiveSupport::JSON.decodeを使ってコメント付き JSON を読んでいる場合、json 2.20.0 以降では同じ警告が出ます。 - 将来の壊れ方を避けるには以下のどちらかが必要です:
- JSON からコメントを撤去する(純粋な JSON として扱う)
- コメントを残したい場合:
JSON.parse(data, allow_comments: true)のようにオプションを明示する
- 自前コードで
古い json バージョンとの互換性
allow_comments: trueは 2.7.1 でもエラーにならず受理されることが確認されており、古い環境との後方互換性に問題はないと明言されています。- そのため、gem バージョンが環境によって微妙に異なるケースでも、このオプション指定自体がクラッシュの原因になる心配は低いです。
- 参考情報 (あれば)
- json gem の変更 PR: ruby/json#1001
- 該当コミット:
https://github.com/ruby/json/commit/138b9a2c4932ad2df1024ce3af18a5cc3ae9e901 - json 2.20.0 のコメント非推奨仕様:
- コメント付き JSON をパースすると警告
- json 3.0 でエラーへ格上げ予定
- Rails nightly 失敗ログの例:
#57778 Fix configuring query_log_tags in database.yml
マージ日: 2026/6/23 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
database.ymlでquery_log_tagsを設定した際に正しく読み込まれない不具合があり、HashConfig内でネストされたキーをシンボルではなく文字列として扱うように修正した PR です。これにより、database.ymlでquery_log_tagsを設定した場合に、期待通りクエリログタグが有効になります。
- 変更内容の詳細
背景
ActiveRecord::DatabaseConfigurations::HashConfigでは「トップレベルのキーのみ」シンボル化されます。- そのため、ネストした設定(例:
query_log_tags: { application: ... }のapplication部分)は「文字列のキー」のままです。 - ところが、
query_log_tagsを参照する実装がネストしたキーをシンボル (:tags,:application等) としてアクセスしていたため、設定が読めない状態になっていました。
実際の修正ポイント
activerecord/lib/active_record/database_configurations/hash_config.rb の変更
- ネストされた
query_log_tags関連の設定取得時に、キーをシンボルではなく「文字列」で参照するように変更。
イメージとしては、以下のような変更が行われています(実際のコードは簡略化):
# 変更前(イメージ)
def query_log_tags_config
config[:query_log_tags]&.dig(:tags)
end
# 変更後(イメージ)
def query_log_tags_config
config["query_log_tags"]&.dig("tags")
endポイントは「config の中身がトップレベル以外は文字列キーなので、:query_log_tags ではなく "query_log_tags" でアクセスする必要がある」という点です。
テストの追加・修正
activerecord/test/cases/query_logs_test.rb にテストが追加・修正されています。
database.ymlからquery_log_tagsを読んだ場合に、- 設定されたタグが実際のクエリログに出力されること
- ネストしたキーが文字列アクセスで正しく参照できること
を確認するテストが増えています。
例として、database.yml で以下のように設定した場合:
development:
adapter: postgresql
database: my_app_development
query_log_tags:
application: my_app
environment: developmentこの設定に基づいて、SQL ログに "application=my_app" や "environment=development" といったタグが含まれていることをテストしています。
- 影響範囲・注意点
- 影響範囲:
database.ymlでquery_log_tagsを設定しているアプリケーションに影響します。- これまで「設定しているつもりだが、ログにタグが出てこなかった」ケースが、この修正で正しく動作するようになります。
- 互換性:
- 外部インターフェース(
database.ymlの書き方)は変わりません。 - 「今まで動いていたものが壊れる」類の後方互換性の問題は基本的になく、むしろ「今まで効いていなかった設定が効くようになる」という性質の修正です。
- 外部インターフェース(
- 注意点:
- Rails をこの修正を含むバージョンに上げた際、クエリログのフォーマットや内容(タグの有無)が変わるため、ログ解析ツールや監視設定でクエリログをパースしている場合は、タグの増加による影響を確認すると良いです。
database.yml側でキー名を誤ってシンボル風に書いている(例::application:のような)場合は引き続き無効なので、通常の YAML の文字列キーとして記述しているか確認してください。
- 参考情報 (あれば)
- 関連 PR: https://github.com/rails/rails/pull/57641
query_log_tags機能まわりの元の変更・議論が含まれていると考えられます。
- 実運用での利用例(イメージ):
マルチテナントや複数サービスから同一 DB に接続している場合に、
applicationやshardなどのタグを付けて、どのコンテキストからのクエリかをログ上で識別しやすくする用途。例:
ymlproduction: adapter: postgresql database: my_app_production query_log_tags: application: my_app environment: production role: primaryこの PR により、上記のような設定が確実にログに反映されます。
#57021 De-anonymize generated attribute module for Store
マージ日: 2026/6/23 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
ActiveRecord::Store がモデルにstore_accessorなどで生成する属性用モジュールについて、これまで匿名モジュールだったものに名前を付け、先祖チェーンで何のモジュールか分かるようにした PR です。あわせて、そのモジュールへの参照方法を「インスタンス変数で保持」から「定義済みの名前で参照」に変更しています。
- 変更内容の詳細
背景
ActiveRecord::Store は以下のようなコードで、ハッシュ型のカラムに対して仮想属性を定義します:
class User < ApplicationRecord
store :settings, accessors: [:color, :homepage]
endこのとき、内部的には
- 匿名モジュールを作成
- その中に
color,color=,homepage,homepage=などのメソッドを定義 - その匿名モジュールを
Userにincludeする
という動きをしていました。その結果、User.ancestors を見ると次のように匿名モジュールが見えていました:
User.ancestors
# =>
# [User (call 'User.load_schema' to load schema informations),
# #<Module:0x0000000125bfcf40>,
# ...]この #<Module:0x...> が「Store が生成した属性メソッドを持つモジュール」なのかどうかは見ただけでは判別できません。
この PR の主な変更点
Store 用のモジュールに名前を付ける
匿名モジュールではなく、意図の分かる名前を持つモジュールとして定義されるようになりました。
具体的なクラス名は差分から正確な文字列は分かりませんが、イメージとしては以下のような形になります(擬似コード):ruby# 以前: 匿名モジュール mod = Module.new include mod # 以後: 名前付きモジュール module StoreGeneratedAttributes # ここに store_accessor が定義するメソッドが入る end include StoreGeneratedAttributesあるいは、モデルクラス毎に異なる定数名を付けているかもしれませんが、重要なのは「
User.ancestorsに出てきたときに、Store 用のモジュールだと分かるようになる」点です。変更後は、
User.ancestorsを見ると:rubyUser.ancestors # => # [User, # ActiveRecord::Store::GeneratedAttributes, # 例 # ...]のように、何のモジュールかが分かる名前が表示されるようになります。
モジュール参照方法をインスタンス変数から「名前ベース」に変更
以前は、生成したモジュールをクラスのシングルトンクラスにインスタンス変数で覚えさせていたようです(擬似コード):
rubysingleton_class.attr_reader :_store_attributes_module def store(...) @_store_attributes_module ||= Module.new include @_store_attributes_module @_store_attributes_module.module_eval do # attr 定義 end endこの PR では、
- モジュールに名前を付けたことで
- その名前(定数)を通していつでも参照できる
ようになったため、わざわざシングルトンクラスにインスタンス変数で保持する必要がなくなり、その仕組みを削除しています。
イメージとしては、次のようなリファクタリングです:ruby# 以前: @_store_attributes_module に保持 mod = @_store_attributes_module # 以後: 名前付き定数を参照 mod = self::StoreGeneratedAttributes実際のコードでは、
ActiveRecord::Store内でクラス用モジュールを探す/作るロジックが整理され、状態をインスタンス変数で抱えずに済むようになっています。
- 影響範囲・注意点
User.ancestorsなどの introspection 結果が変わる- これまで
#<Module:0x000...>のように見えていた匿名モジュールが、名前付きモジュールとして現れるようになります。 - テストやメタプログラミングコードで
ancestorsの内容を文字列比較している場合は、結果が変わる可能性があります。
- これまで
内部 API / 非公式挙動に依存している場合の注意
ActiveRecord::Storeが内部で持っている匿名モジュールのオブジェクト ID や、シングルトンクラスのインスタンス変数 (@_store_attributes_moduleのようなもの) に直接アクセスするようなコードがあれば壊れます。- ただし、これらは Rails の公開 API ではなく内部実装に近いので、通常のアプリケーションコードが依存している可能性は低いです。
挙動面 (定義されるメソッドや動作) は変わらない
store,store_accessor自体の外向きの挙動、生成される属性のアクセス方法、シリアライズ/デシリアライズの仕組みは変更されていません。- あくまで「どういうモジュールにそれらを載せるか」と「そのモジュールをどう管理するか」という内部構造の整理です。
デバッグ性・可読性の向上
rails consoleなどで ancestors を見たとき、「このモジュールは Store が生成した属性メソッド用だ」と視認できるため、問題解析時に役立ちます。
- 参考情報 (あれば)
- この PR は
#57020をベースにしていると明記されているため、Store 周辺のリファクタリングの一環と考えられます。 - ActiveRecord::Store の公式ガイド:
https://guides.rubyonrails.org/active_record_postgresql.html#hstore
(PostgreSQL のhstoreなどと組み合わせる例がありますが、store自体の使い方の参考になります)
#57642 Remove some memoization in Active Record and Active Model
マージ日: 2026/6/23 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Active Record / Active Model 内のいくつかのメモ化(memoization)を削除し、Ractor(マルチスレッド/マルチRactor環境)で扱いやすくする変更です。arel_tableなどを毎回計算するようにし、性能劣化はベンチマーク的に許容範囲に収めつつ、Ractorセーフな設計に近づけています。
- 変更内容の詳細
2-1. メモ化をやめた箇所
PR本文に挙がっている対象は以下です。
arel_tableattribute_method_patterns_cacheattributes_buildercolumn_namesdefault_scope_override
従来はこれらがインスタンス変数などにキャッシュされており、1プロセス内で共有される形になっていました。Ractor では「Ractor 間で共有するオブジェクトは凍結されていなければならない」ため、メモ化によって「凍結されていないが共有されうるオブジェクト」が増えると Ractor 対応の足かせになります。
このPRでは「毎回計算 or その場で構築する」方向に寄せることで、メモ化のためのインスタンス変数を極力持たないようにしています。
arel_table
- 以前:
ActiveRecord::Base.arel_tableはクラスインスタンス変数にキャッシュされる実装(クラスごとに一度だけ Arel::Table を生成して再利用)だった。 - 変更後: 毎回
Arel::Table.new相当で生成する方向に変更され、Arel::Table 側に若干の最適化が入っています。
activerecord/lib/arel/table.rb の変更から推測できる点:
- Arel::Table 自体が、利用時に必要な情報を都度組み立てる/引き回すように少し整理されており、Active Record 側からの呼び出しで「長生きするキャッシュ」を作りにくくしています。
- テスト (
activerecord/test/cases/arel/table_test.rb,fake_record.rb) にも手が入っており、新しい生成パターンでも挙動が同等であることを確認しています。
サンプルイメージ(※実際のコードは簡略化):
# 以前 (イメージ)
def self.arel_table
@arel_table ||= Arel::Table.new(table_name, ...)
end
# 変更後 (イメージ)
def self.arel_table
Arel::Table.new(table_name, ...)
endattribute_method_patterns_cache
対象: ActiveModel::AttributeMethods / ActiveRecord::AttributeMethods
- 各種
attribute_*メソッド (例:respond_to?(:title),method_missing経由での属性アクセス) に使う正規表現やパターンをキャッシュしていた箇所がシンプル化。 - パターンのキャッシュ用インスタンス変数が削除され、パターンの構築をメソッド呼び出しごとに行うか、より局所的なキャッシュに切り替えています。
これにより、respond_to? や method_missing 経由の動的属性メソッド解決が、Ractor 間で共有されるミュータブルなキャッシュに依存しなくなります。
attributes_builder
対象: モデルインスタンス生成時に使われる Attributes のビルダーオブジェクト。
- 以前: クラスごとに
@attributes_builderのような形でキャッシュして再利用。 - 変更後: ビルダーを毎回構築する、あるいはキャッシュの持ち方を変え、Ractor セーフでない ivar を持たない方向に変更。
これにより Post.new のベンチマークで多少遅くなっていますが、PRのベンチマークを見る限り「μsオーダーで数十%程度」の退行で、トレードオフとして許容と判断されています。
column_names
対象: ActiveRecord::ModelSchema#column_names
- 以前:
@column_namesなどのクラスインスタンス変数に配列をキャッシュ。 - 変更後:
columns.map(&:name)相当を都度計算するか、既存のスキーマ情報から毎回導出する形へ。 - ベンチマーク上は
Post.column_namesは大きく遅くなっているように見えますが、それでも 4M i/s 程度出ており、多くのアプリでボトルネックにはなりにくいという判断と見られます。
default_scope_override
対象: ActiveRecord::Scoping::Default
- default_scope を「上書きする」ための内部状態をメモ化していた部分を削除。
- これもクラスにぶら下がるミュータブルなキャッシュ(Ractor 間共有しづらいもの)を消す目的です。
2-2. Active Model 側の変更
activemodel/lib/active_model/attribute_methods.rb での変更 (行+6/-18) から:
- attribute メソッドパターンを管理するロジックが整理され、キャッシュ用 ivar が減っている。
- これにより Active Record 以外の Active Model ベースのクラスも、Ractor の制約下で扱いやすくなります。
- 影響範囲・注意点
3-1. パフォーマンスへの影響
PR に含まれるベンチマークから読み取れる影響:
- ほぼ同等:
Post#titlePost#method_missing(:title)Post#respond_to?(:title)
- 明確に遅くなったもの:
Post.arel_table: 約 26.7M i/s → 約 5.2M i/sPost.column_names: 約 30.4M i/s → 約 4.0M i/sPost#to_partial_path: 約 19.9M i/s → 約 6.1M i/sPost.new: 約 294k i/s → 約 256k i/s- 一部のクエリ構築 (
Post.where(title:),DefaultScopedPost.all) も ~15〜20% 程度遅くなっている
とはいえ、絶対値としてはいずれも十分高速で、実アプリでボトルネックになるケースは限定的と想定されています。著しく影響が出るケース (例: column_names を tight loop で叩きまくるメタプログラミング) があれば、今後のリファクタや局所的キャッシュなどで吸収する余地があります。
3-2. Ractor 対応への影響
- Ractor ではクラスインスタンス変数などに「変更可能なオブジェクト」を持ち、それを複数Ractorで共有するのが難しい/禁止されます。
- このPRにより、Active Record / Active Model のクラスレベル API が、内部状態をほとんど持たずに動作する方向に近づきます。
- 将来的に「モデルクラスを Ractor 間で共有しつつ、各Ractorから普通にクエリを投げる」というユースケースをサポートしやすくなります。
3-3. 互換性・振る舞い
- 公開APIのシグネチャや返り値の型は変わっていないため、通常のアプリコードの挙動は変わりません。
- 挙動に依存していた内部的な副作用(「最初の呼び出しだけ遅くて、以降速い」などのメモ化前提の最適化)には依存しないほうがよくなります。
- メモ化されなくなったことで、アプリ側で頻繁に
column_namesなどを呼んでいると CPU 使用率がわずかに上がる可能性があります。もし問題になる場合は、アプリ側で独自にキャッシュする、あるいは別の API (schema cache など) を検討するのが良いです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57642
- 関連する概念:
- Ruby Ractors: マルチコア並列実行のための機構。オブジェクト共有の制約が厳しく、メモ化やグローバルキャッシュと相性が悪い場合がある。
- Memoization vs. Ractor-safety: クラスレベルでメモ化すると「長く生きるミュータブルオブジェクト」が増え、Ractor フレンドリーでなくなるため、今回のように「パフォーマンスを多少犠牲にしてでも Ractor 対応を優先する」設計判断が行われています。
#57783 Split template lookup into raising and non-raising
マージ日: 2026/6/23 | 作成者: @etiennebarrie
- 概要 (1-2文で)
このPRは、テンプレート探索APIを「例外を投げる版」と「例外を投げない版」に整理し、find_all(...).firstやrescueに頼らずにテンプレート探索できるようにした変更です。あわせて、テンプレートが「どれか1つでも存在するか」を調べるany_formats?を追加し、テンプレート探索ロジックをLookupContextに集約しやすくしています。
- 変更内容の詳細
2-1. テンプレート探索APIの整理
これまでの状況(推測される従来API):
LookupContext#findまたはPathSet#findが「見つからないと例外を投げる」インターフェイスになっていた- 例外を避けたい場所では、以下のようなパターンが必要だった:rubyあるいは
template = lookup_context.find_all(name, prefixes, partial: false).first # template が nil かどうかで存在チェックrubytemplate = lookup_context.find(name, prefixes, partial: false) rescue nil
このPRの主な変更点:
- 「例外を投げない
#find」を導入し、呼び出し側がfind_all(...).firstやrescueに頼らずに、単純にnilチェックで存在確認できるようにしています。 - それに伴い、一部コードで
find_all(...).first→ 非例外版findに置き換えられています。
PR本文にある文言から読み取れる意図:
Use the non-raising #find in places where we had to use find_all(...).first to avoid exceptions or where we had to rescue.
つまり:
find_allは「複数候補 + ソート済み」なためオーバースペックで、単に「一番良い候補が1つ欲しい」場面には非効率。- 今後、「ベストマッチ1件だけを返す
#find」をより効率的に実装しやすくするために、インターフェイスを整理している。
2-2. any_formats? の追加
LookupContext に新たな finder メソッドとして any_formats? が追加されています:
- 役割:
- 指定したパラメータ(名前・パス・フォーマットなど)でテンプレート探索を行い、「該当するテンプレートがひとつでもあるか」を
true/falseで返すメソッド。
- 指定したパラメータ(名前・パス・フォーマットなど)でテンプレート探索を行い、「該当するテンプレートがひとつでもあるか」を
- PR文中の言及:
Also introduce an #any_formats? finder to keep the template lookup in LookupContext, similarly to ImplicitRender using any?/any_templates?
ImplicitRender では、暗黙レンダリングの可否を判断するために any? / any_templates? といったメソッドが使われており、同様の役割を LookupContext レベルに整理したものと考えられます。
これにより:
- 「テンプレートが存在するかどうか」だけ知りたい箇所で、パスセットや呼び出し側が直接
find_allに触る必要がなくなる - テンプレート探索のロジックが
LookupContextに集約され、後続の最適化(キャッシュ戦略・検索アルゴリズム変更など)がやりやすくなる
2-3. 具体的な変更箇所の概要
ファイルごとの主な変更意図(コード行数は小さいが、インターフェイスの使い方に影響):
action_view/lookup_context.rb- 非例外版
findの導入・整理 any_formats?の追加
- 非例外版
action_view/path_set.rbLookupContext側の新しいfind/any_formats?に対応するための委譲や実装調整
template_renderer.rb/partial_renderer.rb/layouts.rb/etag_with_template_digest.rb/digestor.rb- それぞれで使われていた
find_all(...).firstやrescueベースの呼び出しを、新しい非例外版findやany_formats?ベースに書き換え - 例外に依存せず、より明示的な「見つからない場合は nil / false」という分岐に統一
- それぞれで使われていた
lookup_context_test.rb- 新しいふるまい (
findが nil を返すケース、any_formats?の結果など) に合わせたテスト更新
- 新しいふるまい (
簡略イメージ(※実際のコードとはインターフェイスが多少異なる可能性あり):
# 変更前 (例外を避けるためのパターン)
template = lookup_context.find_all(name, prefixes, partial: false).first
if template
render template
else
# フォールバック処理
end
# 変更後 (非例外版 find)
template = lookup_context.find(name, prefixes, partial: false)
if template
render template
else
# フォールバック処理
end
# 存在チェックだけしたい場合
if lookup_context.any_formats?(name, prefixes, partial: false)
# 何らかのテンプレートが存在する
else
# 存在しない
end- 影響範囲・注意点
主な影響範囲:
- ActionView のテンプレート探索まわり
- レイアウト解決 (
layouts.rb)、部分テンプレート (partial_renderer)、通常のテンプレートレンダリング (template_renderer) - テンプレートダイジェスト (
digestor,etag_with_template_digest)
Rails アプリケーション側への直接的な影響:
- 既存のパブリックAPI(例:
renderヘルパー)の挙動は変わらない想定で、主に内部実装の整理・最適化の下地作り。 - ただし、
ActionView::LookupContextやActionView::PathSetの内部APIを直接使っているアプリ・エンジン・gem は注意が必要。findが「常に例外を投げる前提」で rescue していたコードは、今後 nil 戻りになるかもしれないため、挙動が変わる可能性があります。find_all(...).firstで「見つからないときは nil」としていた箇所は、新しい非例外版findに置き換えられる余地があります。
- 既存のパブリックAPI(例:
パフォーマンス上の含意:
- 現時点では大きな最適化というより「将来、
#findを『ベストマッチ1件だけ返す』形で高速化しやすくする」ための設計変更。 find_allは引き続き「候補を全件ソートして返す」ためオーバーヘッドが大きい場面で、新findに移行することで今後の恩恵を受けやすくなります。
- 現時点では大きな最適化というより「将来、
ライブラリ作者・高度な利用者向けの注意:
- 独自のテンプレートリゾルバやカスタムの
LookupContext拡張をしている場合、新しく追加されたメソッド(any_formats?)や非例外型findのふるまいと整合性が取れているか確認した方がよいです。 - 内部的に
findの実装が変わることを見越し、「例外で見つからないことを検出する」より「戻り値の nil / false で判定する」スタイルに寄せるのが将来互換的です。
- 独自のテンプレートリゾルバやカスタムの
- 参考情報 (あれば)
PR 本文で参照されている関連コード:
ImplicitRenderのany?/any_templates?実装例
https://github.com/rails/rails/blob/8817776b7864df906542ef14cf7f12db5e4d48ae/actionpack/lib/action_controller/metal/implicit_render.rb#L40- ここでは「テンプレートがひとつでもあるか」をライトウェイトに確認するためのAPIパターンが既に使われており、今回の
any_formats?はそのLookupContext版と言える位置づけです。
今後想定される発展:
LookupContext#findの内部実装が「全候補探索 + ソート」から「ベストマッチ1件を効率的に返す」ものへ差し替えられる可能性が高く、ビュー探索のパフォーマンス改善の足掛かりとなる変更です。
#57809 Fix broken anchor links in guides [8-1-stable]
マージ日: 2026/6/23 | 作成者: @55728
- 概要 (1-2文で)
Rails 8.1 系(8-1-stable ブランチ)のガイド内で壊れていたアンカーリンク(ページ内リンク)を、実際に存在する見出し ID に合わせて修正するバックポート PRです。主にガイド同士の参照で古い見出し ID やコピペミスを新しい ID に置き換えており、ドキュメントのナビゲーション品質を改善します。
- 変更内容の詳細
全体像
- 対象ブランチ:
8-1-stable - 元 PR: #57806 のバックポート(main 向けの修正のうち、8-1-stable にも該当するものだけを移植)
- 修正対象:
- 11 ファイル
- 15 箇所のリンクを書き換え(15 insertions / 15 deletions)
いずれも「別ガイド内の見出しへのリンク」のアンカー部分を、実際の生成 HTML(id 属性)に合わせた形へ修正しています。
代表例(before / after)
action_mailer_basics.md- 変更前:markdown
[リンクテキスト](i18n.html#translations-for-action-mailer-e-mail-subjects) - 変更後:markdown
[リンクテキスト](i18n.html#action-mailer-e-mail-subjects) - 意味: i18n ガイド内の対象セクション名・ID が変わったため、存在しない
#translations-for-...ではなく、現行の#action-mailer-e-mail-subjectsに合わせています。
- 変更前:
active_record_basics.md- 変更前:markdown
[fixtures の説明](testing.html#the-low-down-on-fixtures) - 変更後:markdown
[fixtures の説明](testing.html#fixtures) - 意味:
testing.html内の見出し ID が#the-low-down-on-fixturesから短く#fixturesに変わっているため修正。
- 変更前:
active_record_callbacks.md- 変更前:markdown
[動的ファインダ](active_record_querying.html#dynamic-finders) - 変更後:markdown
[動的ファインダ](active_record_querying.html#dynamic-finder-methods)
- 変更前:
active_record_validations.md(has_one / has_many へのリンク修正)- 変更前(例):markdown
[has_one](association_basics.html#the-has-one-association) [has_many](association_basics.html#the-has-many-association) - 変更後:markdown
[has_one](association_basics.html#has-one) [has_many](association_basics.html#has-many) - 同じ誤リンクが 2 箇所ずつあり、合計 4 箇所修正されています。
- 変更前(例):
caching_with_rails.md(STI へのリンク)- 変更前:markdown
[単一テーブル継承](association_basics.html#single-table-inheritance) - 変更後:markdown
[単一テーブル継承](association_basics.html#single-table-inheritance-sti)
- 変更前:
configuring.md(autoload パス)- 変更前:markdown
[autoload paths](autoloading_and_reloading_constants.html#autoload-paths) - 変更後:markdown
[autoload paths](autoloading_and_reloading_constants.html#config-autoload-paths)
- 変更前:
getting_started_with_devcontainer.md(Rails プロジェクト作成のアンカー名変更)- 変更前:markdown
(getting_started.html#creating-a-new-rails-project) - 変更後:markdown
(getting_started.html#creating-a-new-rails-app)
- 変更前:
rails_on_rack.md(誤コピペ修正とキャッシュ関連)- 誤コピペと思われる設定項目のアンカー修正:
- 変更前:markdown
(configuring.html#config-action-record-migration-error) - 変更後:markdown
(configuring.html#config-action-dispatch-x-sendfile-header) - 「migration error」ではなく X-Sendfile 関連の設定へのリンクが正しい、という意図。
- 変更前:
- Conditional GET のアンカー:
- 変更前:markdown
(caching_with_rails.html#conditional-get-support) - 変更後:markdown
(caching_with_rails.html#conditional-gets)
- 変更前:
- 誤コピペと思われる設定項目のアンカー修正:
security.md(上と同じ Conditional GET のアンカー修正)- 変更前:markdown
(caching_with_rails.html#conditional-get-support) - 変更後:markdown
(caching_with_rails.html#conditional-gets)
- 変更前:
routing.md- 変更前:markdown
(command_line.html#the-app-and-helper-objects) - 変更後:markdown
(command_line.html#the-app-object)
- 変更前:
testing.md- 変更前:markdown
(active_job_basics.html#create-the-job) - 変更後:markdown
(active_job_basics.html#defining-a-job)
- 変更前:
スキップされた修正(8-1-stable では不要なもの)
main ブランチ側でセクション名が変わっているが、8-1-stable ではまだ変わっていないものは適用していません。具体的には:
active_storage_overview.html内の以下セクションに関するリンク#analyzing-files#requirements#purging-unattached-uploads
engines.md→engines.html#the-inheritance-hierarchy
これらは 8-1-stable では該当セクション名・構成が異なるため、そのままバックポートすると逆に壊れる可能性がある、という判断です。
検証方法
RailsGuides::Markdownの実際のレンダリングパイプラインを使って各ガイドを HTML 生成。- 生成 HTML の
h2〜h5見出しに付与されているidを収集。 - 全ての新しいアンカーがその
idと一致することを確認(13 個すべて成功)。
- 影響範囲・注意点
- 影響範囲:
- Rails Guides(HTML / Markdown ドキュメント)のみで、フレームワーク本体の挙動・API には影響しません。
- 8.1 系のガイドをブラウザで閲覧する際に、該当リンクをクリックしても 404 相当(ページトップへ飛ぶ、意図しない位置に飛ぶ等)だったものが、正しく該当セクションへスクロールするようになります。
- 注意点:
- 自前で Rails Guides をミラーしている場合(社内サイトや翻訳版など):
- レイアウトや見出し構造が 8-1-stable と異なると、同様のアンカー問題が起こりうるため、自分のサイト側での ID を確認してリンクを合わせる必要があります。
- ガイドの特定セクションに直接リンクしているブログ記事・ドキュメントは、すでに「新しい ID」でリンクしているなら問題ありませんが、「古い ID」を使っている場合は、読者が 8.1 の公式ガイドを見る際にずれが発生するかもしれません(この PR は公式ガイド側を現行の見出し ID に合わせているだけなので、逆に外部リンク修正は手動対応になります)。
- 自前で Rails Guides をミラーしている場合(社内サイトや翻訳版など):
- 参考情報 (あれば)
- 元 PR(main ブランチ向け): #57806
- 本 PR: #57809(8-1-stable バックポート)
- Rails Guides のアンカー生成は
RailsGuides::Markdownによる見出し (h2〜h5) の ID 付与仕様に基づいており、rake guides:generate:htmlと同じロジックで検証済みであることが明記されています。
#57754 Make AbstractController::Caching.view_cache_dependency ractor safe
マージ日: 2026/6/22 | 作成者: @andrewn617
- 概要 (1-2文で)
AbstractController::Caching.view_cache_dependencyが Ractor セーフになるよう実装が変更され、ビューキャッシュの依存関係を格納する配列およびその中の Proc を Ractor 共有可能な形で扱うようにした PRです。Ractors を利用するアプリでもビューキャッシュまわりでスレッド/Ractor 安全性を確保しやすくなります。
- 変更内容の詳細
主なポイント
_view_cache_dependencies配列の更新方法を「破壊的変更」から「dup → 追加 → freeze → 再代入」のパターンに変更。- 依存関係として登録される Proc を
ActiveSupport::Ractors.try_shareable_procに通して、Ractor 共有可能にできるようにした。 - これに合わせたテストが
actionpack/test/controller/caching_test.rbに追加。
配列の扱いを Ractor セーフに
従来は _view_cache_dependencies に対して破壊的操作(<< など)を行っていた可能性がありますが、Ractor 環境では「複数 Ractor から共有されているオブジェクトへの破壊的変更」が禁止されます。
この PR では、以下のようなパターンに統一しています(擬似コード):
def add_view_cache_dependency(proc)
proc = ActiveSupport::Ractors.try_shareable_proc(proc)
deps = _view_cache_dependencies.dup
deps << proc
self._view_cache_dependencies = deps.freeze
endポイント:
dup:既存の配列を複製し、新しいインスタンスに対してのみ変更を加える。<<:複製した配列にのみ要素を追加。freeze:最終的な配列を immutability 保証のために凍結し、Ractor 共有を安全に。- 「複数 Ractor から参照される配列」は変更されず、「新しい配列」を生成して差し替えるイミュータブル風パターン。
Proc を Ractor 共有可能に
view_cache_dependency では「ビューキャッシュのキー計算などに使う Proc(ブロック)」を依存関係として登録します。
これらはコントローラのインスタンスに対して instance_exec されるため、Proc 自体を Ractor 共有可能な形にしておく必要があります。
そのため、登録時に:
proc = ActiveSupport::Ractors.try_shareable_proc(proc)を呼び出し、Ractor を利用しているアプリであれば「共有可能な Proc」に変換/検証し、問題がなければそのまま配列に入れます。try_shareable_proc は、内部で Ractor.shareable? や Ractor.make_shareable 相当の処理をラップしているヘルパで、Ractor を使っていない環境でも安全に呼べるようになっています。
テスト追加
actionpack/test/controller/caching_test.rb には、以下を確認するテストが追加されていると考えられます:
- 新しく追加される依存関係が、
try_shareable_procを通されていること。 _view_cache_dependenciesが再代入ベースで更新され、配列がfreezeされていること。- 複数の依存関係を追加しても正しく蓄積されること。
(具体的なテスト名までは PR 情報から分かりませんが、上記の動作が担保されるようになっています。)
- 影響範囲・注意点
対象機能:
AbstractController::Caching.view_cache_dependencyを利用しているコントローラ/フレームワークコード。- ビューキャッシュのキー計算などに Proc を渡している部分。
Ractor を「使っていない」アプリ:
- 振る舞いはほぼそのままで、内部実装がより安全になった程度の影響です。
_view_cache_dependenciesが常にfreezeされた配列として保持されるようになるため、この配列を外部で直接変更していた場合は例外が出るようになります(本来想定されていない使い方なので、問題になるケースは稀)。
Ractor を「使っている / 使う予定がある」アプリ:
view_cache_dependencyに渡す Proc の中で、Ractor 共有不可能なオブジェクト(ミューテーブルなグローバル状態やスレッドアンセーフな構造など)に依存していると、try_shareable_procの内部でエラーになる、もしくは共有不可と判定される可能性があります。- Ractor を見据えるなら、これらの Proc は「引数とレシーバからのみ状態をとる」「外部のミューテーブル状態に依存しない」ようにしておくと安全です。
パフォーマンス:
- 依存追加のたびに
dup+freeze+ 再代入が走るため、理論上はごく小さなオーバーヘッドがありますが、ビューキャッシュ依存の登録頻度は一般的に低いため実用上の影響は軽微と考えられます。
- 依存追加のたびに
- 参考情報 (あれば)
同様の変更パターンを導入した PR:
https://github.com/rails/rails/pull/57743
→ 「配列を破壊的に変更せず、dup → freeze → 再代入」という Ractor セーフなパターンを、他の箇所でも採用済み。Ractor と共有可能オブジェクトの概要(Ruby docs):
この PR により、AbstractController::Caching によるビューキャッシュの依存関係管理が、Rails 本体として Ractor 対応方針(イミュータブル化と shareable proc 化)に揃えられた形になります。
#57753 Make ExecutionContext.after_change ractor safe
マージ日: 2026/6/22 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveSupport::ExecutionContext.after_change_callbacksを Ractor セーフにするため、コールバック配列の扱いを不変(freeze)かつ共有可能な形に変更した PRです。これに伴い、ExecutionContextを利用しているhelpers.rbやquery_logs.rb周りも、Ractor セーフな前提で動作するよう微調整されています。
- 変更内容の詳細
背景・問題点
ActiveSupport::ExecutionContext.after_change_callbacksは Proc の配列として実装されており、rubyActiveSupport::ExecutionContext.after_change_callbacks # => [#<Proc:...>, ...]- Ractor セーフにするには、「Ractor 間で共有されるオブジェクト」は 不変かつ Ractor-shareable である必要があります。
- これまでの実装では、配列をそのまま
<<などで破壊的に更新しており、- 配列自体が freeze されていない
- Ractor 間で共有されたときに「後から中身を書き換えられる」
という点で Ractor セーフではありませんでした。
この PR の方針
- 「登録される Proc 自体が Ractor shareable であること」は 呼び出し側の責務 とし、Rails 側では配列の扱いのみを Ractor セーフにする。
- 説明にある通り、Proc は
instance_execではなく単にcallされるだけなので、Proc 内で何をキャプチャしているか(Ractor shareable かどうか)は呼び出し側で制御すべき。
- 説明にある通り、Proc は
- コールバック配列の更新を 非破壊 + freeze 付き に変更することで、
「登録後の配列は変更されない(=共有しても安全)」状態を保証する。
コア実装イメージ(擬似コード)
activesupport/lib/active_support/execution_context.rb の変更の趣旨は以下のような形です(概念的な擬似コード):
module ActiveSupport
class ExecutionContext
class << self
def after_change(&block)
# 以前は: @after_change_callbacks << block のような破壊的更新だったと考えられる
callbacks = (@after_change_callbacks || []).dup
callbacks << block
callbacks.freeze
@after_change_callbacks = callbacks
end
def after_change_callbacks
@after_change_callbacks || EMPTY_FROZEN_ARRAY
end
end
end
endポイント:
dupして新しい配列を作り、そこにblockを追加。- その配列を
freezeしてからインスタンス変数に再代入。 - 呼び出し元が参照する配列は不変となり、Ractor 間で共有しても安全。
他ファイルの変更の方向性
actionpack/lib/abstract_controller/helpers.rb- コントローラの
helpers関連でExecutionContextの変更フックを使っている箇所が、
新しいafter_changeの挙動(不変なコールバック配列)でも問題なく動くように微調整。 - 具体的には、
ExecutionContext.after_changeへの登録方法(ブロックの渡し方や、返り値・参照の仕方)を、今回のインターフェイスに揃えていると考えられます。
- コントローラの
activerecord/lib/active_record/query_logs.rb- Query Log 用に
ExecutionContextを使っている箇所(コンテキスト変更時にログ情報を更新する処理)が、
新しいコールバック機構でも同様に動作するように 1 行レベルの修正。 - たとえば:
- コールバック登録のタイミング/方法
- 実際に
ExecutionContextから値を読む際の呼び出しパターン
などが、Ractor セーフな前提に合うようになっている。
- Query Log 用に
テスト (
activesupport/test/execution_context_test.rb)+10行の追加で、after_changeコールバックが:- Ractor セーフな形で保持されること
- 期待通りのタイミングで呼ばれること
を検証するテストが追加されている。
- これにより、将来的な変更でも Ractor セーフな挙動が壊れないよう担保している。
- 影響範囲・注意点
非公開 API(no-doc)である点
ExecutionContext.after_changeは公式ドキュメント化されていない内部的な API であり、利用しているアプリ/ライブラリは少ない想定です。- とはいえ、独自に使っている場合は影響を受ける可能性があります。
コールバック Proc 自体の Ractor セーフ性は利用側の責任
- この PR は「配列の管理」を Ractor セーフにしただけで、
各 Proc が Ractor shareable かどうかはチェックしません。 - たとえば以下のように
selfや Ractor 非対応オブジェクトをキャプチャすると、Ractor で共有できない可能性があります:rubyobj = SomeNonShareable.new ActiveSupport::ExecutionContext.after_change do obj.do_something # <- Ractor 間共有は NG になりうる end - Ractor を利用する場合は、ブロック内でキャプチャするオブジェクトや状態を Ractor shareable にする必要があります。
- この PR は「配列の管理」を Ractor セーフにしただけで、
配列が freeze されることによる挙動の違い
- 返ってきた
after_change_callbacksの配列をアプリコード側で破壊的に変更していた場合、rubyのようなコードはActiveSupport::ExecutionContext.after_change_callbacks << another_procFrozenErrorになる可能性があります。 - 正しくは
ExecutionContext.after_changeを通して登録する必要があります。
- 返ってきた
マルチ Ractor 環境での安全性向上
- これにより、ExecutionContext のコールバック登録/実行に関しては、「登録後のコールバック配列を複数 Ractor 間で共有する」ことが現実的になります。
- Ractor ベースで並列化を進めるアプリケーションや gem にとって、フレームワーク側の前提条件が揃ってきた形です。
- 参考情報 (あれば)
- Ruby Ractor の shareable オブジェクト要件
ActiveSupport::ExecutionContextは主に:- ログ(
ActiveRecord::QueryLogsなど) - コンテキスト依存の情報伝播(リクエストスコープ情報 等)
で使われることが多く、今回の変更はこうした周辺機能の Ractor 対応の一環と考えられます。
- ログ(
#57815 Revert pull request #57814
マージ日: 2026/6/22 | 作成者: @etiennebarrie
- 概要 (1-2文で)
このPR (#57815) は、直前のPR (#57814) で導入された ActiveSupport の JSON エンコード周りの変更を「丸ごとリバート」したものです。理由は、Ruby 3.3 の Ractor を起動した際にクラス属性周りで[BUG] should have cvar cache entryという VM クラッシュが CI で再発するためです。
- 変更内容の詳細
2-1. 何をリバートしたか
説明文より:
This reverts commit
abbcb8d7..., reversing changes made todbafe650....
つまり、
dbafe650...で行った変更- それをさらに
abbcb8d7...で上書き/調整した変更
をまとめて打ち消しています。実体としては activesupport/lib/active_support/json/encoding.rb の実装と、そのテスト (activesupport/test/json/encoding_test.rb) を元の状態に戻しています。
変更差分の概要:
activesupport/lib/active_support/json/encoding.rb- +14 / -13 行の変更
- JSON エンコードに関わる実装が、以前のバージョンに巻き戻されている
activesupport/test/json/encoding_test.rb- -17 行
- 新たに追加されていたテストケースが削除され、以前のテスト構成に戻されている
Gemfile.lock- +2 / -2 行
- 依存する gem のバージョン/リビジョンが、元の状態に合わせて微調整されている
PR本文にある通り、今回のリバート対象の変更は Ractor を使ったときに Ruby 本体のバグを引き起こしやすい(特にクラス変数/クラス属性のキャッシュ周り)ため、CI を安定させる目的で戻しています。
2-2. Ractor + class attributes で起きている問題
PR 説明から:
as soon as we start a Ractor, class attributes may crash with
[BUG] should have cvar cache entry, depending on test order.
- Rails 側コードで「クラス属性(class_attribute など)」を使う実装があり
- それが Ruby 3.3 系(おそらく 3.3.4 付近、PR 文中では「4.0.5 / 4.0.6」と書かれているので RubyGems / ランタイムの特定バージョンを指している可能性あり)と組み合わさると
- Ractor を起動したタイミングで Ruby VM が内部エラー (
[BUG] should have cvar cache entry) を出し、プロセスごとクラッシュする
この問題自体は以前の PR (#57684) の時点で把握されており、そのときと同系列のクラッシュが今回の変更でも CI 上で再現したため、「一旦変更をなかったことにする」という判断になっています。
- 影響範囲・注意点
3-1. 現時点での挙動
- ActiveSupport::JSON のエンコード動作は、#57814 適用前の挙動に戻ります。
- #57814 で追加・変更された JSON エンコードの仕様変更(バグフィックスや最適化など)はいったん無効化されたと考えてよいです。
- 新しく追加されていた JSON エンコードに関するテストも削除されているため、テスト観点も以前の状態に戻っています。
3-2. CI / 実運用への影響
- 短期的には:
- Rails の CI で発生していた Ruby VM クラッシュ(Ractor + class attributes)を避けられるため、CI の「フレーク(テスト順序依存の不安定な失敗)」が減ります。
- 中長期的には:
- PR 本文にある通り、今後 Ruby 側の修正版(ここでは「4.0.6」と表現されているバージョン)がリリースされ、CI でそのバージョンが使えるようになった段階で、再度今回の変更に相当する修正を導入し、その際に
skipを追加するなどの対処が行われる見込みです。
- PR 本文にある通り、今後 Ruby 側の修正版(ここでは「4.0.6」と表現されているバージョン)がリリースされ、CI でそのバージョンが使えるようになった段階で、再度今回の変更に相当する修正を導入し、その際に
3-3. 開発者視点での注意点
- Ractor を利用した並列実行を自前で試している場合:
- クラス属性(
class_attribute、cattr_accessorなど)と Ractor を組み合わせると Ruby 側のバグで VM がクラッシュする可能性があります。 - これは Rails コードというより Ruby 本体の cvar キャッシュ周りの問題であり、Rails がそれを「踏みやすくしてしまった」ためにリバートした、という構図です。
- クラス属性(
- JSON エンコード周りで #57814 の変更前提のコードやワークアラウンドを検討していた場合:
- 今回のリバートでその前提はなくなり、従来の挙動に依存して問題ない状態に戻ります。
- #57814 の具体的な仕様変更に依存していた場合は、当面はその利用を見送る必要があります。
- 参考情報 (あれば)
- 元の変更: pull request #57814(今回リバートされた内容)
- 関連する既知の問題: pull request #57684(同様の Ractor + class attributes +
[BUG] should have cvar cache entryが言及されている) - Ruby 側のバグ:
[BUG] should have cvar cache entryは Ruby VM 内部のクラス変数キャッシュ関連のアサーションエラーであり、Ractor 対応や並列実行時のキャッシュ不整合が原因と考えられます。- PR 説明で言及されている「4.0.5 / 4.0.6」は、Ruby もしくは関連コンポーネントのバージョンを指しており、修正版(4.0.6)のリリース後に Rails 側で
skipを追加しつつ変更を再適用する方針が示されています。
#57814 Make ActiveSupport::JSON encoders Ractor-shareable
マージ日: 2026/6/22 | 作成者: @etiennebarrie
- 概要 (1-2文で)
ActiveSupport::JSON のエンコーダを「Ractor-shareable」(Ractor 間で共有可能)にするため、エンコーダを早い段階でfreezeするように変更した PRです。これによりRactor.make_shareableを明示的に呼ばずに、Ractor 内からそのまま JSON エンコード機能を利用できるようになっています。
- 変更内容の詳細
a. 背景: Ractor と shareable オブジェクト
Ruby の Ractor では、Ractor 間で共有できるオブジェクトは shareable としてマークされたものだけです。
Ractor.make_shareable(obj)を使う- もしくは「凍結済み(
freeze済み)」「ミュータブルな状態を持たない」などの条件を満たす構造にしておく
ことで、shareable にできます。
ActiveSupport::JSON のエンコーダは内部的にハッシュや Proc / lambda、クラスへのマッピングなどを保持しているため、そのままだと shareable ではなく、Ractor 内で使うために Ractor.make_shareable を呼ぶ必要がありました。
この PR では「そもそも最初から shareable な形にしておく」ことで、この呼び出しを不要にしています。
b. ActiveSupport::JSON エンコーダの実装変更
activesupport/lib/active_support/json/encoding.rb の主なポイント:
- JSON エンコーダ関連のデータ構造を初期化時点で
freezeし、以後変更しない設計に寄せている- 例:
- エンコード時に利用するマッピングテーブル
- クラスごとのエンコーダ登録テーブル
- 例:
- これにより、Ruby の Ractor が「このオブジェクトは不変であり、shareable」と判断できるようになる
PR 説明文にある以下の方針がそのまま実装に反映されています:
If we freeze early, we can avoid having to call
Ractor.make_shareableand just use the ractor-shareable JSON encoders from Ractors.
つまり、
# 変更前のイメージ
encoders = ActiveSupport::JSON::Encoding::ENCODERS
Ractor.make_shareable(encoders)
# 変更後のイメージ
# ライブラリ側で freeze して shareable になっているので
encoders = ActiveSupport::JSON::Encoding::ENCODERS
# そのまま Ractor 間で利用可能のような感じで、アプリ側が Ractor.make_shareable を意識する必要がなくなります。
c. テストコードの変更 (Ruby 3.4 / 4.0 対応)
activesupport/test/json/encoding_test.rb の主なポイント:
- Ruby バージョンごとにテストを分岐
- Ruby 3.4 以下では、該当テストを定義しないようにして、skip の失敗を回避
- 「テストを実行して skip する」のではなく「そもそもテストケースを定義しない」方向で対処
- Ruby 3.4 以下では、該当テストを定義しないようにして、skip の失敗を回避
- Ruby 4.0 向けには、JSON gem 側の挙動変更(またはバージョン更新)にあわせてテストが落ちないように追従
Gemfile.lockに JSON のバージョン更新が入り、テストの skip 条件などもそれに合わせて調整されている
PR 説明の:
On 3.4 and below the test is not defined at all to avoid failing skips. Updating JSON was also necessary for the skips not to fail in Ruby 4.0.
に対応する変更で、
「Ractor 関連テストが Ruby の特定バージョンで false positive な失敗を出さないようにした」調整です。
- 影響範囲・注意点
Ractor を使っていないアプリケーション
- 実質的な挙動は変わらず、通常の JSON エンコード/デコードには影響しない想定です。
- エンコーダの内部構造がよりイミュータブル寄りになった程度の影響です。
Ractor を使っている / 使う予定のアプリケーション
- ActiveSupport::JSON のエンコーダを Ractor 内から利用する際、明示的な
Ractor.make_shareableは不要になります。 - 今後、Ractor ベースの並行処理で JSON エンコードを多用するケースで、より自然に ActiveSupport::JSON が使えるようになります。
- ActiveSupport::JSON のエンコーダを Ractor 内から利用する際、明示的な
カスタムエンコーダを追加している場合の注意点
- ActiveSupport::JSON::Encoding に対して、ランタイムでミュータブルな形でエンコーダを追加・変更している場合、そのやり方が Ractor と相性が悪くなる可能性があります。
- 例えば、クラス -> Proc のマッピングを後から書き換えるようなコードがあると、そのマッピングを shareable にできず、Ractor 間で共有できない可能性があります。
- 「Ractor から参照したいエンコーダ」は原則としてアプリ起動時に確定させ、凍結された状態に置く、という設計がより強く求められます。
- ActiveSupport::JSON::Encoding に対して、ランタイムでミュータブルな形でエンコーダを追加・変更している場合、そのやり方が Ractor と相性が悪くなる可能性があります。
Ruby バージョン依存のテスト挙動
- ライブラリ開発者視点では、Ruby 3.4 以前と 4.0 以降で Ractor や JSON の挙動が異なるため、
テスト・CI で Ruby バージョンごとの分岐に注意が必要です。 - Rails 本体側ではその分岐を吸収していますが、プラグイン / エクステンションを書く場合も同様の考慮が必要になります。
- ライブラリ開発者視点では、Ruby 3.4 以前と 4.0 以降で Ractor や JSON の挙動が異なるため、
- 参考情報 (あれば)
- Ruby Ractor ドキュメント:
https://docs.ruby-lang.org/en/master/ractor_md.html - Ractor-shareable オブジェクトの仕様(英語):
https://docs.ruby-lang.org/en/master/doc/ractor_md.html#label-Shareable+objects - ActiveSupport::JSON::Encoding (Rails ガイド / API リファレンス):
https://api.rubyonrails.org/classes/ActiveSupport/JSON/Encoding.html
#57789 Strip inline comments from unquoted .env values
マージ日: 2026/6/22 | 作成者: @55728
- 概要 (1-2文で)
Rails のDotEnvConfigurationが.envファイルを読む際に、未クォート値の末尾コメント(#以降)を自動的に除去するようになりました。これにより、.envでは一般的な「値の右にコメントを書く」記法で、値が誤ってコメント込みで解釈される不具合が解消されます。
- 変更内容の詳細
以前の挙動
DotEnvConfiguration は .env 行について:
- 行頭のコメント行のみスキップしていた:ruby
next if line.start_with?("#") KEY=valueの形式を次のような正規表現でパースしていた:rubyここで\A([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\z(.*)が 行末まで全部を value として取得するため、末尾コメントも値に含まれていました。
そのため、例えば:
PORT=5432 # primary
SECRET=$(echo hi) # xが次のように解釈されていました:
option(:port) #=> "5432 # primary" # 本当は "5432" が欲しい
require(:secret) #=> "hi # x" # 本当は "hi"エラーにはならずサイレントに誤った値が入るのが問題でした。
今回の修正
未クォート値に対して、末尾のインラインコメントを削除するメソッドが追加されました:
def strip_inline_comment(value)
return value if value.start_with?('"', "'")
value.sub(/\s+#.*\z/, "")
endこれをパース処理の経路に組み込み、概ね:
unquote(strip_inline_comment(value))という順序で処理するようになりました。
ポイント:
- クォートされていない値のみを対象とする
- 先頭が
",'の場合は一切触らない
- 先頭が
- 「スペース +
#以降」をコメントとして削除value.sub(/\s+#.*\z/, "")
具体例と新しい挙動
| 入力行 | 解釈される値 | 挙動の理由 |
|---|---|---|
PORT=5432 # primary | "5432" | 5432 の後ろに空白があり、# primary はコメントとして削除 |
DB=$(op read x) # note | $(op read x) を実行 | $(...) 部分はそのまま。後ろの # note は削除される |
URL=http://host#frag | "http://host#frag" | # の前に空白がないため、コメントではなく値の一部とみなす |
PASS="a # b" | "a # b" | 先頭が " なので strip_inline_comment はスキップ。引用符内部の # は値として保持 |
サンプルコードイメージ:
# これまで:
ENV["PORT"] #=> "5432 # primary"
# これから:
ENV["PORT"] #=> "5432"
# URL のように # を含む値
ENV["URL"] #=> "http://host#frag" # 変化なし
# コマンド展開を使ったケース
ENV["DB"] #=> (op read x の結果).env ツール群(dotenv gem, foreman, docker-compose など)が採用している慣習と同様のふるまいになります。
- 影響範囲・注意点
影響範囲
- 対象: Rails の
ActiveSupport::DotEnvConfigurationを使って.envを読み込む箇所 - 影響するケース:
- 未クォート値で、値の後ろに「スペース +
#コメント」を書いていたケース - これまでは
#以降も値に含まれていたが、今後は除去される
- 未クォート値で、値の後ろに「スペース +
Rails 以前の挙動に依存して、あえてコメントを値に含めていたケースがあると、値が変わる可能性はありますが、.env の一般的な使い方からすると、今回の変更の方が自然かつ他ツールとの互換性があります。
既知の制約・注意点
クォートされている値の末尾コメントはそのまま
- 例:
TWO="2" # two - 現状、この
# twoは値からは削除されません("2" # twoという文字列として扱われる)。 - 理由: 正しく削るには「終端のクォート位置を把握し、エスケープ
\"も考慮したパース」が必要で、現時点ではスコープ外とされているためです。 - 将来的に改善される可能性があります。
- 例:
未クォートの
$(...)内にある#も削られうる- 例:env
X=$(echo "a # b") - 正規表現は単純に「空白 +
#以降」を削除するため、$(...)内部の# b")もコメントとして扱われてしまう可能性があります。 - dotenv gem も同様の制約を持っていると明示されており、「ごく稀なケース」として既知の制限として扱われています。
- 必要なら、こうしたケースは値をクォートしたり、コマンド側で工夫する必要があります。
- 例:
.envパーサーはまだ unreleased サブシステム- この機能はまだ公開 API として広く安定運用されている段階ではない前提で設計されているため、将来的に挙動が調整される余地があります(例えば、クォート値のコメント除去の追加など)。
- 参考情報 (あれば)
- 本 PR が目指すのは「Rails 独自仕様」ではなく、既存ツール(dotenv gem, foreman, docker-compose など)の
.envコメント解釈ルールと同等の挙動です。 .env設計のベストプラクティス:- 末尾コメントを使う場合は、未クォート値で:env
PORT=5432 # primary DB - 引用符を使う場合でコメントを付けたいときは、コメントを別行に分ける方が安全です:env
# Primary password PASS="a # b"
- 末尾コメントを使う場合は、未クォート値で:
- 実際のコード変更:
activesupport/lib/active_support/dot_env_configuration.rbにstrip_inline_commentの追加と、その呼び出しを 1 箇所挿入(+11/-1)activesupport/test/dot_env_configuration_test.rbに対応テストを追加(+20)
#57800 Reword allow/let phrasings in guides
マージ日: 2026/6/22 | 作成者: @55728
- 概要 (1-2文で)
Railsガイド内の英語表現を、文法的により正確で自然な形にリライトしたPRです。コードやAPIの挙動には一切影響せず、ドキュメント中の「allow」「let」の使い方を修正しています。
- 変更内容の詳細
このPRは「文法的に正しい英語」に揃えるためのガイド文言修正で、いずれも意味は同じですが表現のみが変わっています。
2.1 let + 不定詞の修正
let は「to なしの原形不定詞」を取るため、次のように修正されています。
let the user to pass→let the user pass(2箇所)
Railsとしての意図(「ユーザーに〜させる」)は変わらず、文法だけを正しています。
イメージ(英語パターン)
- 誤:
let the user to configure this option - 正:
let the user configure this option
2.2 allow の目的語省略の修正
allow は通常「allow + 目的語 + to 不定詞」の形を取るため、「誰に(何に)」を明示するように変更されています。
This allows to ...→This allows you to ...allows for you to specify ...→allows you to specify ...
(allows for you toは不自然なのでallows you toに簡潔化)
また、動名詞を使う自然な表現に直したものもあります。
allows to print→allows printing
イメージ(英語パターン)
- 誤:
This allows to configure the encryption options. - 正:
This allows you to configure the encryption options.
または:This allows configuring the encryption options.
- 影響範囲・注意点
影響範囲
- 対象は Rails ガイド内のテキストのみ(4ファイル・5行)。
- フレームワークのコード、挙動、API シグネチャには一切変更なし。
- バージョンアップ時にアプリケーション側の修正は不要です。
注意点
- ドキュメントを翻訳・ローカライズしている場合は、元の英語に追従するためのマージ差分として認識しておくとよいです(意味は変わらないが、差分として検出されます)。
- 英語で社内ドキュメントやブログを書く際に、同様のパターン(
let+ bare infinitive,allow + object + to)に揃えると、より自然で標準的な表現になります。
- 参考情報 (あれば)
PR で引用されている文法上の根拠:
- Cambridge Grammar – Verb patterns (verb + infinitive or verb + -ing?)
let/makeは能動文ではtoなしの不定詞を取る:
例:Let him go.,She made me do it.
- Merriam-Webster – allow
- 例文はいずれも
allow + object + to-infinitive型:
例:The system allows you to transfer data easily.
- 例文はいずれも
これらに合わせる形で、Rails ガイドの表現も統一されています。
#57805 Fix Http::Headers#merge mutating the original request
マージ日: 2026/6/22 | 作成者: @55728
- 概要 (1-2文で)
ActionDispatch::Http::Headers#mergeが「新しい Headers を返して元のリクエストを変更しない」という仕様に反して、元のrequest.envを破壊的に変更していたバグを修正するPRです。mergeが正しく新しいHttp::Headersインスタンスを返し、呼び出し元のリクエストに副作用を与えないようになりました。
- 変更内容の詳細
問題となっていた実装
元々の merge の実装はドキュメント上は以下のようなイメージで説明されており、「新しい Http::Headers を作る」とされています:
# Returns a new Http::Headers instance containing the contents of
# `headers_or_env` and the original instance.
def merge(headers_or_env)
headers = @req.dup.headers
headers.merge!(headers_or_env)
headers
endところが実際には、
request.headersは一度生成されると@headers ||= ...でメモ化される@headersには元のrequest(@req) への参照が保持される@req.dupはインスタンス変数を「参照のまま」コピーする(浅いコピー)Rack::Request::Env#initialize_copyは@envだけを再コピーし、@headersはそのまま(参照共有)
という状態だったため、
@req.dupで複製したリクエストも、元のリクエストと 同じ@headersオブジェクト を参照してしまう- その
Http::Headersインスタンスの中の@reqは 元のリクエスト を指している merge内でheaders.merge!(headers_or_env)が呼ばれると、その実装は@req.set_headerを通して 元の request.env に書き込む
結果として、「コピーしたつもり」のヘッダ操作が元のリクエストの env を直接書き換える、というバグになっていました。
今回の修正
PRでは Http::Headers#merge の実装が 1 行だけ変更されています(+1 / -1)。実際のパッチは次のような形です(意図を示すための概略):
def merge(headers_or_env)
headers = @req.dup.headers
# 以前は merge! で破壊的に書き換えていた
# headers.merge!(headers_or_env)
# 修正: 非破壊的な merge を使って新しい Headers を作る
headers.merge(headers_or_env)
endポイント:
merge!(破壊的)ではなくmerge(非破壊的)を使うことで、- メモ化されて共有されている
Http::Headersインスタンスを直接変更しない - 返り値として新しい
Http::Headersを返す、というドキュメント通りの挙動になる
- メモ化されて共有されている
- これにより
merge呼び出し時に元のrequest.envに書き込まれることがなくなる
テストの追加
actionpack/test/dispatch/header_test.rb に 14 行のテストが追加されています。テストの狙いは:
request.headersを一度読み出した後にrequest.headers.merge(foo: "bar")を呼んでも- 元の
request.env/request.headersの内容が変化しないこと
を確認することです。これは「headers が既にメモ化されている実際のリクエストライフサイクル」で問題が再発しないことを検証しています。
- 影響範囲・注意点
主な影響範囲:
ActionDispatch::Http::Headers#mergeを使ってリクエストヘッダを「コピーしてから一部上書き」しているコード- Rack ミドルウェアや Rails ミドルウェア、あるいはテストコード等で
request.headers.merge(...)を利用している箇所
これまでの挙動との違い:
- 以前:
request.headers.merge(new_headers)を呼ぶと、元のrequest.envが(意図せず)書き換わる 場合があった- そのため「新しいヘッダセットを作っただけのつもりが、以降の処理で同じリクエストのヘッダが変わっている」というバグを引き起こしうる
- 以後:
mergeは本当に「新しいHttp::Headersを返すだけ」で、呼び出し元のリクエストには副作用がない- ドキュメント通りの直感的な挙動になる
- 以前:
既存コードへの注意点:
- もし既存コードが「
mergeを呼ぶことで元のリクエストヘッダが書き換わる」挙動に暗黙的に依存していた場合、そのコードは動かなくなります(とはいえ、仕様的にはその依存はバグと言えるため、修正された挙動に合わせるべきです)。 - リクエストのヘッダを本当に書き換えたい場合は、従来どおり
request.headers['X-Foo'] = 'bar'などのインターフェイスを使うべきです。mergeはあくまで「コピーを返す」メソッドです。
- もし既存コードが「
- 参考情報 (あれば)
- Ruby の
Object#dupはインスタンス変数を「参照のまま」コピーするため、メモ化されたオブジェクトや、元オブジェクトへの参照を内部に持つオブジェクトがある場合、今回のような「浅いコピーによる共有バグ」が起こりやすいです。 - Rails 内部でも:
initialize_copy/dupのオーバーライド時に、どのインスタンス変数を再構築・再dupすべきか- メモ化キャッシュを複製時にクリアすべきか といった点は要注意箇所です。
- 類似の問題を避けるためには:
- 「コピー後のオブジェクトが元のオブジェクトの状態に書き戻す」パス(今回でいう
@req.set_header)がないか確認する dupで解決しようとせず、明示的に新しいインスタンスを構築して値を移す、という設計も検討する価値があります。
- 「コピー後のオブジェクトが元のオブジェクトの状態に書き戻す」パス(今回でいう
#57808 Validate offset value at call time like limit
マージ日: 2026/6/22 | 作成者: @55728
- 概要 (1-2文で)
offsetに渡す引数が整数でない場合、これまではサイレントにto_iで変換されていましたが、このPRによりlimitと同様にInteger(value)でバリデーションし、不正な値は即座にArgumentErrorを投げるようになりました。これにより、SQL生成時と in-memory finder 経路の挙動が整合的になり、バグを早期に検出できます。
- 変更内容の詳細
コア変更点
ActiveRecord::Relation#offset! が、limit! と同様に引数を Integer(value) でチェックするようになりました。
元の挙動(概念的には):
def offset!(value)
# 以前は value をそのまま保持
self.offset_value = value
self
end
# SQL 生成時:
offset_value.to_i # ここで初めて整数化このため:
Post.offset("abc").to_sql # => "SELECT ... OFFSET 0"
Post.offset("5x").to_sql # => "SELECT ... OFFSET 5"のように、不正/不完全な文字列がサイレントに 0 や 5 に変換されていました。
新しい挙動:
def offset!(value) # :nodoc:
value = Integer(value) unless value.nil?
self.offset_value = value
self
endこれにより:
Post.offset("abc") # => ArgumentError: invalid value for Integer(): "abc"
Post.offset("5x") # => ArgumentError: invalid value for Integer(): "5x"
Post.offset("3") # => OK, offset_value == 3
Post.offset(3) # => OK, offset_value == 3
Post.offset(nil) # => OK, offset_value == nilと、limit と同じパターンのバリデーションになります。
テストの追加・変更
変更されたテストは主に次の観点を確認しています:
文字列の数値
"3"を渡した場合は従来どおり正しく動くこと
(既存のtest_finder_with_offset_stringが引き続きパスすることを確認)不正な文字列/数値に変換できない値を渡した場合に
ArgumentErrorが発生すること
例:"abc","5x"など
これにより挙動変更が意図通りであり、かつ既存の正当なユースケースは壊していないことを担保しています。
- 影響範囲・注意点
影響範囲
ActiveRecord::Relation#offset/#offset!を利用している全てのコード- 特に、コントローラやAPI層でリクエストパラメータをそのまま
offsetに渡している場合に影響が出る可能性があります。
- 特に、コントローラやAPI層でリクエストパラメータをそのまま
互換性の観点
以前:
offset("abc")→ SQL 上はOFFSET 0になり、例外は出ないoffset("5x")→ SQL 上はOFFSET 5になり、例外は出ない
以後:
- どちらも
ArgumentErrorが発生する
既存アプリで「不正な offset パラメータが来たら暗黙に 0 / 切り捨てとして扱う」という挙動に“依存していた”場合、今回の変更で例外が発生するようになります。
そのため、外部入力(クエリパラメータなど)をそのまま渡している場合は、アプリケーション側でバリデーション/サニタイズを行う必要があります。
例: Rails コントローラでの対策
def index
offset_param = params[:offset]
# 不正値を 0 にフォールバックしたい場合の一例
offset =
begin
Integer(offset_param) if offset_param.present?
rescue ArgumentError
0
end
@posts = Post.offset(offset).limit(20)
end一貫性の向上
- すでに
limitはInteger(value)を使っており、「limit は即時エラー、offset はサイレント変換」という不整合が解消されます。 - in-memory finder パス(
offset_value + indexなど)では元々「整数であること」を前提に演算していたため、ここで想定外の型が入り込むバグも早期に表面化します。
セキュリティへの影響
PR本文でも明記されている通り、これは「挙動の一貫性とエラーメッセージの明確化」が目的であり、セキュリティ修正ではありません。
すでに offset_value.to_i によって、SQL インジェクション的な利用は中和されていました。
- 参考情報 (あれば)
- 関連挙動:
ActiveRecord::Relation#limit!がInteger(value)を用いるようになったコミット:f46b838b3f
- 影響を確認したい場合:
- テスト・ログ・Sentry などで
ArgumentError: invalid value for Integer()の新しい発生箇所がないかを確認すると、今回の変更による影響範囲を洗い出しやすいです。
- テスト・ログ・Sentry などで
#57811 Update actions/checkout to v7
マージ日: 2026/6/22 | 作成者: @Uaitt
- 概要 (1–2文で)
GitHub Actions のactions/checkoutを v7 に更新する PRで、Rails 本体の CI ワークフローと、rails new/ plugin generator が出力する GitHub Actions テンプレートの両方が対象です。アプリやライブラリの挙動には影響せず、CI環境と生成される GitHub Actions 設定の最新版追従が目的です。
- 変更内容の詳細
a. Rails リポジトリ内の CI ワークフロー更新
以下の GitHub Actions Workflow ファイルで、actions/checkout のバージョン指定が更新されています。
.github/workflows/devcontainer-shellcheck.yml.github/workflows/devcontainer-smoke-test.yml.github/workflows/rail_inspector.yml.github/workflows/rails-new-docker.yml.github/workflows/rails_releaser_tests.yml.github/workflows/release.yml
いずれも、例えば:
- uses: actions/checkout@v4のような記述が:
- uses: actions/checkout@v7のように更新されています(実際の before/after の細かいバージョンは PR 内の diff に依存しますが、v7 へと揃える変更です)。
この変更により、Rails 自体の CI が actions/checkout v7 を利用するようになります。
b. rails new が生成する GitHub Actions テンプレートの更新
rails new myapp --github のようなオプションを使った際に生成される GitHub Actions 用テンプレートが更新されています。
対象ファイル:
railties/lib/rails/generators/rails/app/templates/github/ci.yml.ttrailties/lib/rails/generators/rails/plugin/templates/github/ci.yml.tt
ここでも、actions/checkout のバージョン指定が v7 へ変更されています。
想定される差分イメージ
アプリ用テンプレート (github/ci.yml.tt) の一部:
# 変更前
- uses: actions/checkout@v4
# 変更後
- uses: actions/checkout@v7plugin 用テンプレート (github/ci.yml.tt) でも同様に @v7 に更新されています。
このため、今後の Rails 新規プロジェクトやプラグインで --github オプションを使用した場合、最初から actions/checkout@v7 を使う CI 設定が生成されます。
- 影響範囲・注意点
アプリケーションコードへの影響はなし
変更はすべて GitHub Actions のワークフローファイルおよびそのテンプレートに限られており、Rails のランタイム挙動や API には影響しません。これから生成される GitHub Actions 設定のみが対象
既に作成済みの Rails アプリ/プラグインの.github/workflows/ci.ymlは、自動では書き換わりません。
既存プロジェクトで v7 を使いたい場合は、手動で:yaml- uses: actions/checkout@v7のように変更する必要があります。
actions/checkoutv7 由来の差異に注意
一般にメジャーバージョンアップでは、セキュリティ強化や非推奨オプションの整理など、細かな挙動変更が入ることがあります。
Rails 的には checkout アクションの標準的な使い方しかしていないため、大きな問題は起きにくい想定ですが、独自の GitHub Actions を組んでいる場合は、actions/checkoutリポジトリのリリースノートを確認しておくと安全です。CI の再現性への影響
メジャーバージョンを追従することで、今後も GitHub Actions 側のサポート・バグ修正・セキュリティパッチを享受しやすくなります。一方で、「昔の挙動に固定したい」場合は、自身のプロジェクトで敢えて v4 ~ v6 を指定し続けることも可能です。
- 参考情報 (あれば)
actions/checkout公式リポジトリ(リリースノート・ Breaking changes の確認用)
https://github.com/actions/checkoutGitHub Actions ドキュメント(
uses:でのアクション指定方法)
https://docs.github.com/actionsRails ガイド: Rails アプリの作成 (
rails new)
https://guides.rubyonrails.org/command_line.html#rails-new
この PR 自体はあくまで CI/テンプレートのバージョン追従であり、Rails 利用者に対するコード修正の必須事項はありません。
#57810 Fix use of class_attribute for primary_key_definition
マージ日: 2026/6/22 | 作成者: @byroot
- 概要 (1-2文で)
primary_key_definitionのclass_attributeの定義タイミングを修正し、継承階層で同じアクセサが何度も再定義されてしまう問題を解消しました。inheritedフックではなく、Concern が include されたタイミングで定義するように変更されています。
- 変更内容の詳細
何が問題だったか
以前のPR (#57775) では、primary_key_definition を class_attribute で定義する際に、inherited フック(サブクラスが定義されたときに呼ばれる)を使っていました。
しかしこの方法だと:
- 継承チェーン(親クラス → 子クラス → 孫クラス …)の各クラスで
同じclass_attribute :primary_key_definitionアクセサが何度も再定義される - 不要なメソッド再定義が発生し、挙動の追跡やパフォーマンス、今後の保守性に悪影響を与えうる
という問題があります。
どう修正されたか
Active Record の primary_key 関連の Concern (ActiveRecord::AttributeMethods::PrimaryKey 付近) において:
- これまで: Concern が include されたモデルに対して、
inheritedフック内でclass_attribute :primary_key_definitionを定義していた - これから: Concern が include されたタイミングで、一度だけ
class_attribute :primary_key_definitionを定義するように変更
Ruby on Rails の Concern では、
included do
# ここは include されたときに実行される
endのようなフックが使われますが、この PR では「継承時 (inherited) ではなく、Concern が include されたとき (included) に class_attribute を定義する」という形に寄せています。
それに伴って、primary_key_definition 周りのテスト (activerecord/test/cases/primary_keys_test.rb) も、
「継承時の再定義」ではなく「include 時に正しく定義され、継承しても問題なく振る舞うか」を担保するような形に微修正されています。
※ 実際のコードイメージ(簡略化例):
module ActiveRecord
module AttributeMethods
module PrimaryKey
extend ActiveSupport::Concern
included do
class_attribute :primary_key_definition, instance_accessor: false
end
end
end
end上記のように included ブロック内で class_attribute を定義することで、
- その Concern を include したクラスに対して一度だけ
- 適切にクラスレベルの属性を定義する
という、Rails での典型的なパターンに揃えています。
- 影響範囲・注意点
- 主な対象: Active Record の
primary_key関連メカニズム(primary_key_definitionを参照する内部実装、およびそれに依存している部分) - 影響:
- モデル継承構造(抽象クラス + サブクラス、多段継承、STIなど)で
primary_key_definitionがより正しく・一貫して扱われるようになります。 - クラス階層が深い場合でも、同じ
class_attributeが継承のたびに再定義されることがなくなります。
- モデル継承構造(抽象クラス + サブクラス、多段継承、STIなど)で
- 既存アプリへの実務的な影響:
primary_key_definitionは基本的に内部用の API であり、通常のアプリケーションコードが直接触ることは少ないため、破壊的変更のリスクは低いと考えられます。- もし
primary_key_definitionをメタプログラミング的に操作している場合(特に継承と組み合わせている場合)は、継承チェーンでの値の引き継がれ方を念のため確認すると安心です。
- 参考情報 (あれば)
- フォローアップ元 PR: https://github.com/rails/rails/pull/57775
前回のprimary_key_definitionまわりの変更に対する修正(follow-up)として、この PR が入っています。 - 関連機能:
ActiveSupport::Concern#includedフックActiveSupport::ClassAttribute(class_attribute) の動作と継承時の振る舞い
#57795 Support polymorphic associations with custom primary keys through :inverse_of
マージ日: 2026/6/22 | 作成者: @kamipo
- 概要 (1–2文で)
このPRは、「ポリモーフィック関連 +:inverse_of」の組み合わせで、逆側の関連に定義されたカスタム:primary_keyが正しく使われるようにする変更です。これにより、ポリモーフィック関連先ごとに異なる主キー(例:uuid,slug,id)を自然に扱えるようになります。
- 変更内容の詳細
背景・問題点
いくつかのモデルでは、歴史的な理由から「DBの
idとは別のユニークな識別子列」を実質的な主キーとして使っている(例:uuid,slugなど)。これらのカラム名はモデルごとに異なっており、
alias_attributeで擬似的に統一していたが、ActiveRecord の関連実装は高速化のため_read_attributeを直接呼んでおり、alias_attributeが効かない。- 結果として、実運用アプリでは
_read_attributeをオーバーライドして alias 解決するような苦しいワークアラウンドをしていた。
- 結果として、実運用アプリでは
ポリモーフィック関連 (
belongs_to :subject, polymorphic: trueなど) は、コンパイル時に静的にinverse_ofを決定できないため、自動検出が無効。- その影響で、ユーザが明示的に
inverse_of:を指定していても、その情報が関連の内部実装で十分・一貫して利用されていなかった。 - 特に「逆側の
:primary_key設定」がポリモーフィック関連に反映されないという問題があった。
- その影響で、ユーザが明示的に
このPRは、「ポリモーフィック関連でも、明示された :inverse_of を見に行って、逆側の :primary_key 設定を尊重する」ように実装を改善しています。
振る舞いの変更の要点
1. ポリモーフィック belongs_to で inverse_of を見てカスタム primary_key を解決
BelongsToReflection#association_primary_key が拡張され、以下のような場合に「逆側の関連の :primary_key」を参照するようになりました。
- 関連が
polymorphic: trueである inverse_ofが明示的に指定されている- その逆側の関連(例:
has_many :comments, primary_key: :uuid, as: :commentable) にprimary_key:オプションが設定されている
これにより、例えば次のような構成が正しく動作します。
# 例: コメントは commentable にポリモーフィックに属する
class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true, inverse_of: :comments
end
# Post は uuid を主キー的に使う
class Post < ApplicationRecord
# DB の primary key は id のままだが実質 uuid を関連キーに使いたいケース
has_many :comments,
as: :commentable,
primary_key: :uuid, # ここを尊重するようになる
inverse_of: :commentable
end
# Article は slug を主キー的に使う
class Article < ApplicationRecord
has_many :comments,
as: :commentable,
primary_key: :slug, # ここを尊重
inverse_of: :commentable
end
# Video はデフォルトの id を使う
class Video < ApplicationRecord
has_many :comments,
as: :commentable,
# primary_key オプション省略 → :id が使われる
inverse_of: :commentable
endこの状態で comment.commentable や post.comments.build などの処理を行う際、ポリモーフィック関連であっても、
- Post なら
posts.uuid - Article なら
articles.slug - Video なら
videos.id
といった形で、モデルごとのprimary_key:設定が正しく使用されます。
これまでだと、ポリモーフィック関連では「常に id 前提」または「逆側設定が反映されない」挙動になるケースがありましたが、それが解消されます。
2. AutosaveAssociation のロジック簡素化
AutosaveAssociation モジュール内で、関連オブジェクト保存時に「どのカラムを foreign key / primary key として使うか」を自前で解決していた箇所が、既存のリフレクションAPIを利用するようにリファクタされています。
- これまで:
- autosave 用の処理の中で独自に primary key を推測・解決しており、
BelongsToReflection#association_primary_keyなどとロジックが二重化・分散していた
- autosave 用の処理の中で独自に primary key を推測・解決しており、
- 変更後:
reflection.association_primary_key等の既存メソッドを利用して primary key を取得- polymorphic + inverse_of + custom primary_key の対応をリフレクション側で一元的に扱える
これにより、autosave(accepts_nested_attributes_for 経由など)でポリモーフィック関連を保存する場合でも、逆側の :primary_key 設定が整合的に使われます。
3. テストとドキュメント
belongs_to_associations_test.rb,autosave_association_test.rbに、- ポリモーフィック +
inverse_of+ カスタムprimary_key
という組み合わせに対するテストケースが追加されています。
- ポリモーフィック +
CHANGELOG.mdにこの挙動変更が追記されており、正式な仕様として扱われます。- モデル定義 (
author.rb,comment.rb,person.rb) およびschema.rbがテスト用に微調整されています。
- 影響範囲・注意点
影響範囲
ポリモーフィックな
belongs_toを使っている箇所で、inverse_ofを明示している場合- 逆側(
has_many/has_one側)のprimary_key:設定が、これまでよりも厳密に反映されます。 - これまで「たまたま
id前提で動いていた」コードが、明示的なprimary_key:設定に従うようになる可能性があります。
- 逆側(
accepts_nested_attributes_forなど、autosave を伴う関連操作- autosave の際に使われる primary key 解決ロジックが、リフレクションに一元化されたため、ポリモーフィック関連でも一貫性が増します。
- 逆に、「独自に
_read_attributeをオーバーライドして alias を吸収していた」ようなハックが、不要または挙動変化の対象になります。
互換性・注意点
ポリモーフィック関連で
inverse_ofを書かない場合- 従来通り自動検出は無効のままなので、このPRの恩恵は受けません。
- カスタム主キーを使いたい場合は、「関連両側で
inverse_ofを正しく指定する」ことが前提になります。
逆側関連で
primary_key:を指定しているが、実際の外部キー列と整合していない場合- 今回の変更により、その設定がより厳密に使われるため、関連のロード・保存が失敗したり、意図しない行を参照する可能性があります。
- とくに、既存アプリで「
primary_key:を暫定的に書いていたが、ポリモーフィック側が事実上無視していた」ようなケースは要確認です。
alias_attributeベースのワークアラウンドalias_attributeではなく「実際にprimary_key:で使うカラムを関連オプションとして指定し、それが正しく使われる」形に寄せられるので、将来的に_read_attributeのオーバーライドなどを撤去できる余地があります。- ただし、このPR自体は
_read_attributeの挙動を変更していないので、「alias が効くようになる」わけではなく、あくまで「カスタム主キーを正式な設定として渡せるようにした」改善です。
- 参考情報 (あれば)
- 対象PR: https://github.com/rails/rails/pull/57795
- 関連するキーワード:
polymorphic: trueinverse_of:primary_key:BelongsToReflection#association_primary_keyAutosaveAssociation
- 設計上のポイント:
- ポリモーフィック関連は型(
*_type)ごとに参照先モデルが変わるため、「逆側関連の情報をどう統一的に見るか」が難しい領域。 - 今回は「明示的に
inverse_ofが指定されている場合」に限り、その逆側設定を尊重する方針をとることで、安全性と柔軟性のバランスを取っています。
- ポリモーフィック関連は型(
#57806 Fix broken anchor links in guides
マージ日: 2026/6/21 | 作成者: @55728
- 概要 (1-2文で)
Rails Guides 内の壊れたアンカーリンク(file.html#anchor形式のセクションリンク)を現状のセクション ID に合わせて修正し、16リンク分のスクロール不能問題を解消した PR です。残った一部のリンクは、該当セクション自体が削除・移動されており、別 PR での著者判断を要するものとして保留されています。
- 変更内容の詳細
実施したチェック方法
- 76 本すべてのガイドを HTML 生成
- 生成 HTML の
id="..."を「正」として収集 - Markdown ソース中の
](file.html#anchor)形式のリンクをすべてクロスチェック - 実在しない ID を指しているリンクを洗い出し、現状の ID へ差し替え
→ その結果、14 ファイル・16 箇所を修正。
主なリンク修正内容
※ 左が「これまでのリンク先」、右が「修正後の正しいアンカー」。
Active Record 関連
active_record_validations.mdassociation_basics#the-has-one-association→association_basics#has-one(×2 箇所)association_basics#the-has-many-association→association_basics#has-many(×2 箇所)
active_record_callbacks.mdactive_record_querying#dynamic-finders→active_record_querying#dynamic-finder-methods
active_record_basics.mdtesting#the-low-down-on-fixtures→testing#fixtures
キャッシュ関連
caching_with_rails.mdassociation_basics#single-table-inheritance→association_basics#single-table-inheritance-sti
(form_helpers.mdですでに-sti付きが使われており、それに合わせた統一)
設定・autoload
configuring.mdautoloading…#autoload-paths→autoloading…#config-autoload-paths
(config.autoload_paths用のセクション ID に合わせる)
Active Storage / Action Text / メール
contributing_to_ruby_on_rails.mdactive_storage_overview#analyzing-files→active_storage_overview#analyzing-files-for-metadata
action_text_overview.mdactive_storage_overview#requirements→active_storage_overview#third-party-software(×2 箇所)active_storage_overview#purging-unattached-uploads→active_storage_overview#purging-unattached-uploads-and-detach
action_mailer_basics.mdi18n#translations-for-action-mailer-e-mail-subjects→i18n#action-mailer-e-mail-subjects
Engines / Routing など
engines.mdengines#the-inheritance-hierarchy→engines#inheritance-hierarchy
getting_started_with_devcontainer.mdgetting_started#creating-a-new-rails-project→getting_started#creating-a-new-rails-app
rails_on_rack.md/security.mdcaching_with_rails#conditional-get-support→caching_with_rails#conditional-gets
routing.mdcommand_line#the-app-and-helper-objects→command_line#the-app-object
(リンク元テキストが「via the app object」のため、それに合わせる)
testing.mdactive_job_basics#create-the-job→active_job_basics#defining-a-job
間違ったコピー&ペースト由来の修正
rails_on_rack.mdconfig.action_dispatch.x_sendfile_headerのリファレンスリンクが、誤って#config-action-record-migration-errorを指していたのを、同じページ内の正しい兄弟リンクに合わせて#config-action-dispatch-x-sendfile-headerに修正。
※ 疑似例(実際の行番号などは省略):
# 修正前(誤リンク)
See [config.action_dispatch.x_sendfile_header](configuration.html#config-action-record-migration-error) for details.
# 修正後(正しいリンク)
See [config.action_dispatch.x_sendfile_header](configuration.html#config-action-dispatch-x-sendfile-header) for details.- 影響範囲・注意点
対象は Rails Guides 内部のドキュメントリンクのみで、フレームワークの動作や公開 API には一切影響しません。
ガイド閲覧時に、これまでクリックしても該当セクションまでスクロールしなかったリンクが、正しく該当箇所へジャンプするようになります。
まだ残っている壊れたリンクは、以下のように「ターゲットのセクション自体が削除・移動されているもの」であり、この PR ではあえて直していません。リンクの意図(どのセクションへ飛ばすべきか)の判断が必要なためです。
action_mailer_basics→active_job_basics#action-maileractive_record_querying→active_record_basics#transactionsconfiguring→caching_with_rails#cache-storesactive_record_composite_primary_keys→active_record_querying#conditions-with-idengines→i18n#providing-translations-for-internationalized-strings
これらは別途、該当ガイドの著者やメンテナと相談しつつ、
- リンク先を別の現存セクションに変える
- あるいはリンク自体を削除する
といった判断が必要になります。
ドキュメントの ID 変更やセクション名変更を今後行う場合も、同様の整合性チェックを CI などに組み込むと、今回のようなリンク切れを未然に防げます。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57806
- Rails Guides: https://guides.rubyonrails.org/
- HTML 生成後の
idと Markdown 内の#anchorを突き合わせる手法は、ドキュメントのリンク健全性チェックとして汎用的に使えるので、自前ドキュメントや社内ガイドにも応用可能です。
- HTML 生成後の
#57711 Fix create_or_find_by scope pollution on the non-transactional retry
マージ日: 2026/6/21 | 作成者: @55728
- 概要 (1-2文で)
create_or_find_by/create_or_find_by!が一意制約違反 (RecordNotUnique) でリトライする際、トランザクション外でのみ「スコープ汚染」により既存レコードを正しく取得できないバグが残っていたのを修正したPRです。あわせて、そのトランザクション外の挙動をカバーする新しいテストが追加されています。
- 変更内容の詳細
問題の挙動
create_or_find_by は、内部で INSERT を試みて RecordNotUnique が発生した場合、同じ属性で SELECT をやり直すことで「既に存在していたレコードを返す」挙動をとります。
以前の修正 (#57192) では、トランザクション内の場合のみ、次のようにスコープをリセットして再検索するように変えていました:
rescue ActiveRecord::RecordNotUnique
if connection.transaction_open?
rewhere(attributes).lock.take!
else
find_by!(attributes) # ここは未修正で残っていた
end
endトランザクション内 (
transaction_open? == true):rewhere(attributes).lock.take!
→ もともとのスコープ条件を捨てて、attributesだけで検索するトランザクション外 (
elseブランチ):find_by!(attributes)
→ 呼び出し元のスコープを維持したままattributesを追加する
このため、呼び出し元でスコープを張っている場合にバグが生じていました。
Subscriber.create!(nick: "bob")
# ここで where(nick: "alice") というスコープがついた relation で create_or_find_by
Subscriber.where(nick: "alice").create_or_find_by(nick: "bob")
# => ActiveRecord::RecordNotFound
# 実際のクエリ: WHERE "subscribers"."nick" = 'alice' AND "subscribers"."nick" = 'bob'- INSERT が一意制約に引っかかって
RecordNotUnique - rescue 節で
find_by!(attributes)を実行 - ここでスコープ
nick = 'alice'とattributesのnick = 'bob'が両方付く - 実レコードは
nick = 'bob'なのでマッチせず、RecordNotFoundになる
これは、#57192 が解決しようとしていた「スコープ汚染」と同じ問題ですが、トランザクション外の分岐だけ未対応だった、という位置づけです。
修正内容
今回のPRでは、トランザクション外の分岐もトランザクション内と同様、「元スコープを捨てて attributes のみで再検索」するように揃えています。ただし、トランザクション外なのでロックはかけません。
変更後イメージ:
rescue ActiveRecord::RecordNotUnique
if connection.transaction_open?
rewhere(attributes).lock.take!
else
rewhere(attributes).take! # ← ここを修正
end
endポイント:
find_by!(attributes)→rewhere(attributes).take!rewhereにより、これまで relation に付いていたWHERE条件を捨てて、attributesの条件だけを付与take!で 1件取得。見つからなければRecordNotFoundを投げる動作は維持
.lockはトランザクション内でのみ意味があるため、トランザクション外ブランチでは付けていない
これにより、トランザクションの有無にかかわらず、create_or_find_by の「既存レコードを取りに行くとき」は常に「スコープではなく属性で検索する」という一貫した挙動になります。
テストの追加
既存のテストスイートは「トランザクションフィクスチャ」を使っており、各テストはトランザクション内で実行されるため、connection.transaction_open? が常に true になり、問題の else ブランチが一切通っていませんでした。
このPRでは、トランザクション外の挙動を検証する専用テストファイルを追加しています:
- 追加ファイル:
activerecord/test/cases/create_or_find_by_outside_transaction_test.rb - 中身の特徴:
self.use_transactional_tests = falseを設定し、テストをトランザクション外で実行- スコープ付きの relation から
create_or_find_by/create_or_find_by!を呼び出し、- 一意制約により INSERT が失敗 → 再検索
- 再検索時に、スコープが汚染されず既存レコードを正しく返すことをアサート
- 修正前だと
RecordNotFound(重複したnick条件)でテストがレッドになることを確認済み
- 影響範囲・注意点
- 対象メソッド:
ActiveRecord::Relation#create_or_find_byActiveRecord::Relation#create_or_find_by!
- 影響条件:
- 一意制約 (
UNIQUEindex 等) によってRecordNotUniqueが発生しうるカラムを対象にcreate_or_find_by/create_or_find_by!を使用している - かつ、スコープを張った relation(
Model.where(...).create_or_find_by(...)など)で呼び出している - かつ、周囲にトランザクションを張っていない利用ケース(コントローラから直接呼ぶなど)が存在する
- 一意制約 (
この条件を満たす場合、**これまで「たまたま RecordNotFound が出ていて気づいていなかったバグ」**が、今回の修正で意図どおり「既存レコードを返す」挙動になります。
注意点:
- スコープに依存して動作していた(と期待していた)コードがあれば、挙動が変わります:
- 以前:
スコープ + attributes の両方で検索するため、スコープで除外されているレコードはRecordNotFound - 今回以降:
スコープを無視して attributes のみで検索し、既存レコードを返す
- 以前:
- ただし
create_or_find_byの設計意図は「指定 attributes を持つレコードを一意に取得する(なければ作る)」であり、スコープ条件を再検索に持ち込まない方が自然かつ一貫性があるため、この変更はバグ修正として妥当です。
移行時に確認しておくとよいこと:
- もし
create_or_find_by/create_or_find_by!を使っていて- 意図的に「トランザクション外」「スコープ付き」の組み合わせで挙動を変えていた場合は、その設計を見直す必要があります。
- 「スコープに合致しないものはエラーにしたい」のであれば、
create_or_find_byではなく、明示的にfind_or_create_by+ バリデーション or 条件チェックなどの別のパターンを検討した方がよいです。
- 参考情報 (あれば)
- 元バグ修正 PR: #57192
- トランザクション内の
create_or_find_byのスコープ汚染を修正したが、トランザクション外は取りこぼしていた
- トランザクション内の
- 今回の PR: #57711 - Fix
create_or_find_byscope pollution on the non-transactional retry- トランザクション外のブランチを
rewhere(attributes).take!に揃え、テストを追加してカバレッジを確保
- トランザクション外のブランチを
#57782 Avoid mutating params hash passed to ActionDispatch::Http::URL.url_for
マージ日: 2026/6/21 | 作成者: @55728
- 概要 (1-2文で)
ActionDispatch::Http::URL.url_forが引数として受け取ったparams:ハッシュを破壊的に変更していた問題を修正し、呼び出し元のハッシュを変更しないようにした PR です。これにより、request.query_parametersなどのメモ化ハッシュが意図せず書き換えられる不具合や、凍結されたハッシュでのFrozenErrorが解消されます。
- 変更内容の詳細
問題点
これまで ActionDispatch::Http::URL.url_for の内部では、クエリパラメータを付与する add_params メソッドで、値が nil 扱いとなるキーを除外する際に破壊的メソッド reject! を使っていました。
def add_params(path, params)
params = { params: params } unless params.is_a?(Hash)
params.reject! { |_, v| v.to_param.nil? } # ここで呼び出し元の Hash が書き換わる
query = params.to_query
path << "?#{query}" unless query.empty?
endその結果:
request.query_parametersが汚染されるOptionRedirect#pathでは次のように、request.query_parametersをそのままparams:に渡しています:rubyurl_options = { protocol: request.protocol, host: request.host, port: request.optional_port, path: request.path, params: request.query_parameters # メモ化されている同一オブジェクト }.merge! optionsここで
?fooのような「値なしクエリ」(パースすると{"foo" => nil}) があると、reject!によってこのキーが削除されます。request.query_parametersは per-request にメモ化されたハッシュなので、一度削除されるとその後の処理からは 常にキーが存在しない状態 に見えてしまいます。
凍結された
params:を渡すと例外になる呼び出し側が
paramsをfreezeして不変オブジェクトとして扱っていると:rubyActionDispatch::Http::URL.url_for( only_path: true, path: "/x", params: { a: 1, b: nil }.freeze )旧実装では
reject!によりFrozenError: can't modify frozen Hashが発生していました。期待される挙動は、
bを無視して"/x?a=1"を返すことです。
修正内容
reject! を非破壊的な reject に置き換え、フィルタ済みハッシュをローカル変数に再代入する形に変更しています。
def add_params(path, params)
params = { params: params } unless params.is_a?(Hash)
params = params.reject { |_, v| v.to_param.nil? } # 非破壊で新しい Hash を作る
query = params.to_query
path << "?#{query}" unless query.empty?
endポイント:
- もともと
reject!の戻り値はどこにも使われておらず、その後もparams自体を参照していたため、「URL の出力結果」はこの変更で変わりません。 - 変わるのは 「呼び出し元の
paramsが書き換えられない」 という点だけです。 - これにより
FrozenErrorも解消されます (rejectは新しい Hash を返すだけなので元の Hash を変更しない)。
テストの追加
actionpack/test/dispatch/request_test.rb に回帰テストが追加されています。おおよそ次を確認する内容です:
渡した
paramsが変更されないことrubyparams = { a: 1, b: nil } ActionDispatch::Http::URL.url_for(only_path: true, path: "/x", params: params) assert_equal({ a: 1, b: nil }, params)凍結された
paramsでも URL が生成できることrubyparams = { a: 1, b: nil }.freeze url = ActionDispatch::Http::URL.url_for(only_path: true, path: "/x", params: params) assert_equal "/x?a=1", url
- 影響範囲・注意点
主な影響対象:
ActionDispatch::Http::URL.url_forを直接使っているコード。- あるいはそれを内部的に利用するコンポーネント(
OptionRedirectなど)、およびそれ経由で URL を組み立てる処理全般。
挙動の変化:
- URL 文字列としての出力は、これまでと同じになる想定です(値が
to_param.nil?なキーは引き続きクエリに含まれない)。 - 変わるのは「渡した
paramsハッシュのライフサイクル」で、- 以前:
nil相当の値を持つキーが 破壊的に削除される - 今回以降: 渡したハッシュは そのまま保持される
- 以前:
- URL 文字列としての出力は、これまでと同じになる想定です(値が
バグ解消の具体例:
- これまで
request.query_parametersを再利用している処理で、「さっきまであったキーが消えている」「一部のクエリが見えなくなっている」といった現象が出ていた場合、この変更で解消する可能性が高いです。 url_forに凍結済みのオプションハッシュや、アプリ側で明示的に不変として扱っている設定ハッシュを流しても安全になります。
- これまで
互換性:
- アプリケーションコード側で、あえて
url_forに破壊的変更を期待していたケース(=paramsが削られることを前提にしていたコード)があると理論上は挙動が変わります。 - ただし、URL ビルダにそんな前提を置くのは通常想定外であり、PR でも「URL ビルダが入力を変更すべきでない」という方針が明示されています。実用上は後方互換性に問題はほぼないと考えてよいです。
- アプリケーションコード側で、あえて
- 参考情報 (あれば)
- この PR で指摘されている問題は、過去の #23103 と同じ「Rails 内部が per-request にメモ化されたハッシュを破壊的に変更してしまう」クラスのバグです。
- 関連クラス / メソッド:
ActionDispatch::Http::URL.url_forActionDispatch::Request#query_parametersActionDispatch::Routing::RouteSet::Generator(過去の類似バグの修正対象)
#57720 Don't share the actions Hash across ActionableError subclasses
マージ日: 2026/6/21 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::ActionableErrorでアクションを管理するクラス変数_actionsがサブクラス間で共有されてしまい、定義していないアクションが親・兄弟クラスに「漏れる」バグを、コピーオンライトによる再代入で修正した PR です。これにより、各ActionableErrorサブクラスごとにアクション定義が正しく分離されます。
- 変更内容の詳細
問題点
もともとの実装:
class_attribute :_actions, default: {}
# ...
def action(name, &block)
_actions[name] = block
endclass_attribute の default: {} は 1 つの同じ Hash オブジェクトをデフォルトとして使うため、
- 親クラス
BaseError < ActionableError - サブクラス
ErrorA < BaseError - サブクラス
ErrorB < BaseError
という構成のときに、ErrorA.action(:do_a) { ... } や ErrorB.action(:do_b) { ... } を呼ぶと、親とサブクラスで同じ _actions Hash オブジェクトが共有されます。
その結果:
ErrorAで定義したアクションがBaseErrorやErrorBからも見えてしまうActionableError.actions(error)やdispatchが、本来そのクラスで定義していないアクションまで返す/実行できてしまう
というバグが発生します。
修正内容
action メソッドをミューテーションではなく「コピーオンライト」で再代入するように変更:
# 変更前
def action(name, &block)
_actions[name] = block
end
# 変更後
def action(name, &block)
self._actions = _actions.merge(name => block)
endポイント:
mergeによって 既存の_actionsHash から新しい Hash を生成し、その新しい Hash をself._actionsに代入します。- 親クラスから継承した
_actionsの内容はmergeのベースになるので、継承自体は保持されます(親で定義したアクションは子から見える)。 - ただし、新しい Hash を自クラスに再代入することで、その後の変更はそのクラス専用の Hash 上で行われるため、親や兄弟クラスへの「漏れ」がなくなります。
これは最近の StructuredEventSubscriber#debug_only (#57676) の修正や、他の class_attribute の再代入パターンと同じ方針です。
テスト追加
activesupport/test/actionable_error_test.rb に以下のような構造のテストが追加されています(概念的なイメージ):
class BaseError < ActiveSupport::ActionableError
end
class ErrorA < BaseError
action(:a) { "A" }
end
class ErrorB < BaseError
action(:b) { "B" }
end
# 期待される状態:
# BaseError: アクションなし
# ErrorA: [:a] のみ
# ErrorB: [:b] のみ変更前は、ベースクラスに ["A", "B"] が見えてしまいテストが失敗(red)していましたが、修正後は各クラスが自分のアクションだけを見るようになりテストが成功(green)することが確認されています。
- 影響範囲・注意点
- 対象:
ActiveSupport::ActionableErrorを継承し、actionDSL を使っているアプリケーションやライブラリ。 - 期待される挙動としては「あるサブクラスで定義したアクションが、他のサブクラスや親クラスには見えない」はずなので、その意味ではバグフィックスであり、破壊的変更というより正しいスコープに戻す修正です。
- もしこれまで、
- 親クラスでアクションを定義していないのに、
- なぜかサブクラスのアクションが見えていて、
- それに依存した実装やテストを書いていた という場合は、今回の修正でその挙動が是正されて見えなくなるため注意が必要です。
- アクションを親クラスで共有したい場合は、親クラスに明示的に
actionを定義し、子では必要な追加分だけactionを定義する構成にするのが正しい使い方になります。
- 参考情報 (あれば)
- 該当ファイル:
activesupport/lib/active_support/actionable_error.rbactivesupport/test/actionable_error_test.rb
- 関連 PR:
StructuredEventSubscriber#debug_onlyの同種のバグ修正: PR #57676
- 背景となる仕組み:
class_attributeのdefault:にミュータブルなオブジェクト({},[]など)を渡すと、同一オブジェクトが継承チェーンで共有されるため、今回のような「サブクラス間での状態共有バグ」が起こりやすい、という一般的な注意点があります。
#57792 Avoid mutating the override options passed to ActiveModel::Errors#import
マージ日: 2026/6/21 | 作成者: @55728
- 概要 (1-2文で)
ActiveModel::Errors#importが引数に渡されたoverride_optionsハッシュを書き換えてしまう問題と、凍結されたハッシュを渡すとFrozenErrorが発生する問題を修正する PRです。override_optionsをdupしてから正規化することで、呼び出し元のハッシュを一切変更しないようにしています。
- 変更内容の詳細
問題点
ActiveModel::Errors#import は、attribute / type のオプションをシンボル化する際に、渡されたハッシュ自体を書き換えていました。
従来の挙動(簡略化):
def import(error, override_options = {})
[:attribute, :type].each do |key|
if override_options.key?(key)
override_options[key] = override_options[key].to_sym # 呼び出し元の hash を破壊的変更
end
end
@errors.append(NestedError.new(@base, error, override_options))
endこれにより:
- 同じ
optionsハッシュを複数回使い回すと、最初の呼び出しで文字列がシンボルに書き換えられてしまう。 - 凍結されたハッシュ(
{}.freeze)を渡すと、代入時にFrozenErrorが発生する。
例:
options = { attribute: "age", type: "blank" }
errors.import(error, options)
options
# 実際: { attribute: :age, type: :blank }
# 期待: { attribute: "age", type: "blank" }
errors.import(error, { attribute: "age" }.freeze)
# 実際: FrozenError: can't modify frozen Hash
# 期待: attribute: :age としてエラーがインポートされるimport は public API であり、他モデルからエラーをコピーする公式な手段としてドキュメント化されているため、呼び出し元が所有するハッシュを変更すべきではありません。
修正内容
override_options を正規化する前に dup して、メソッド内部専用のコピーを使うようにしました。
修正後(要点のみ):
def import(error, override_options = {})
override_options = override_options.dup # ← ここを追加
[:attribute, :type].each do |key|
if override_options.key?(key)
override_options[key] = override_options[key].to_sym
end
end
@errors.append(NestedError.new(@base, error, override_options))
endポイント:
dupは浅いコピーなので、トップレベルの:attribute/:typeの値を書き換える分には十分。dupしたハッシュは凍結されていないため、FrozenErrorも解消される。- 呼び出し元のハッシュは一切変更されない。
テスト追加
activemodel/test/cases/errors_test.rb に以下のような回帰テストが追加されています(概念的には):
import呼び出し後に、渡したoptionsハッシュが変更されていないこと。- 凍結された
optionsハッシュを渡してもFrozenErrorが出ず、エラーが正しくインポートされること。
- 影響範囲・注意点
- 既存コードの挙動変化:
- これまで「
importを呼ぶとoptionsのattribute/typeがシンボルに変わる」ことに依存していたコードがあれば、そこは挙動が変わります(ただし、その依存は想定外のものです)。
- これまで「
- Frozen ハッシュの利用:
- 今後は
errors.import(error, SOME_OPTIONS.freeze)のような呼び出しが安全に行えます。
- 今後は
- パフォーマンス:
- 毎回
override_options.dupが走るため、理論上はごく小さなオーバーヘッドが増えますが、通常のフォームバリデーション等のユースケースでは無視できるレベルと考えられます。
- 毎回
- API 安全性の向上:
- Rails 内部が「所有していないハッシュを破壊的に変更しない」という原則に沿う改修であり、
merge!→importの内部呼び出しだけでなく、アプリケーションコードからの直接利用もより安全になります。
- Rails 内部が「所有していないハッシュを破壊的に変更しない」という原則に沿う改修であり、
- 参考情報
- 類似のバグとして挙げられている PR:
- #57782:
ActionDispatch::Http::URL.url_forが渡されたparams:ハッシュを破壊的変更していた問題 - #23103: Rails 内部が渡されたハッシュを破壊的に変更していた別件のバグ
- #57782:
- 対象クラス:
ActiveModel::Errors - 対象メソッド:
import(error, override_options = {})
#57802 Fix typos in generated app config templates
マージ日: 2026/6/21 | 作成者: @55728
- 概要 (1-2文で)
Rails が新規アプリ生成時に使う設定ファイルテンプレート内のコメントにあった英単語のスペルミスを修正する PR です。実行ロジックや挙動の変更はなく、生成される設定ファイルのコメント表記のみがきれいになります。
- 変更内容の詳細
対象は Rails アプリ生成テンプレート (rails new 時に使われる .tt ファイル) のコメント部分です。
修正された箇所
config/environments/production.rb.tt
- # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] or in config/master.key. This key is used for decrypting credentials (and other encrypted files).
- # config.require_master_key = true # Uncomment to enforce credentials assigment.
+ # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] or in config/master.key. This key is used for decrypting credentials (and other encrypted files).
+ # config.require_master_key = true # Uncomment to enforce credentials assignment.スペルミス: assigment → assignment
※ 実際の行はコメントのみの修正で、意味は「credentials の割り当てを強制するためにコメントアウトを外す」という説明です。
config/environments/test.rb.tt
- # Raise error for missing translations.
- # config.i18n.raise_on_missing_translations = true # Useful for catching translation assigment issues in tests.
+ # Raise error for missing translations.
+ # config.i18n.raise_on_missing_translations = true # Useful for catching translation assignment issues in tests.スペルミス: assigment → assignment
テスト環境で訳抜けを検出する設定に関するコメントの修正です。
config/storage.yml.tt
- # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id and aws:secret_access_key)
- # and then checkin this file to your source control.
+ # Use rails credentials:edit to set the AWS secrets (as aws:access_key_id and aws:secret_access_key)
+ # and then check in this file to your source control.checkin → check in(動詞として正しい表記に修正)
意味としては「このファイルをバージョン管理にコミット(チェックイン)してください」という説明のままです。
※ PR 説明にもある通り、いずれも「コメント」「文言」だけの修正で、コードの実行パスや設定値そのものは一切変更されていません。
- 影響範囲・注意点
影響範囲:
- この PR マージ以降に
rails newで生成されるアプリのconfig/environments/production.rbconfig/environments/test.rbconfig/storage.ymlに含まれるコメント文言だけが修正されます。
- 既存アプリには自動反映されません(
rails app:updateでも、コメント差分は基本的に手動マージ対象です)。
- この PR マージ以降に
実行時挙動:
- 設定値やメソッド呼び出しには一切変更なし。
- テストもこれらの文字列に依存していないため、CI・本番挙動ともに影響はありません。
注意点:
- 英文コメントをドキュメントや社内資料などにそのまま引用している場合、
assigmentの誤字が紛れ込んでいる可能性があります。必要ならコピペ元をこの PR に合わせて修正するとよいです。 checkin→check inは表記ゆれの修正で意味は同じなので、既存プロジェクトのstorage.ymlをわざわざ直す必要はありません(気になる場合のみ修正する程度で十分です)。
- 英文コメントをドキュメントや社内資料などにそのまま引用している場合、
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57802
- 修正対象ファイル:
railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.ttrailties/lib/rails/generators/rails/app/templates/config/environments/test.rb.ttrailties/lib/rails/generators/rails/app/templates/config/storage.yml.tt
#57799 Fix grammar in guides
マージ日: 2026/6/21 | 作成者: @55728
- 概要 (1-2文で)
このPRは、Rails公式ガイド内の明らかな英語の文法ミスを修正したものです。コードや挙動の変更は一切なく、ガイド文書の可読性・正確性を高めるドキュメント専用の修正です。
- 変更内容の詳細
PRの方針は「誰が見ても誤りと言えるレベルの文法ミスのみを修正する」もので、対象は以下の3カテゴリです。
(1) 主語と動詞の一致 (Subject-verb agreement)
active_record_validations.md
saveandupdatereturns → return
複数主語に合わせて動詞を複数形に修正。
getting_started.md
- parameters ... is → are
“parameters” に合わせて “are” に修正。
routing.md
- method and the lambda gets → get
“method and the lambda” は複数なので “get” に修正。 - multiple routing files make → makes
こちらは逆に、文の主語が「configuration」など単数扱いになる文脈と合うように “makes” に修正されたと考えられます(3358行の記述と整合を取る意図)。
active_storage_overview.md
- validations ... isn't → aren't
“validations” が複数形なので “aren’t” に修正。
active_support_core_extensions.md
- The method X and Y → The methods
:3358行と表現を揃え、「2つのメソッド」を指すことが明確になるように修正。
(2) 冠詞 / it's / 比較 (Articles / it’s / than)
getting_started.md
- it's inventory → its inventory
所有を表す “its” に修正(it is ではない)。
active_record_querying.md
- greater that 1 → greater than 1
比較で用いる “than” に修正。
active_storage_overview.md
- an URL → a URL
“URL” の発音が “yoo-” で始まるため “a URL” が正しい。
testing.md
- is AJAX request → is an AJAX request
“AJAX” が母音音から始まるため “an” を付与。
(3) 語形・表現の修正 (Word form / phrasing)
active_storage_overview.md
- How to setup → How to set up
動詞句 “set up” に修正(“setup” は名詞/形容詞)。 - a serviced called → a service called
誤った過去分詞 “serviced” を名詞 “service” に修正。
action_view_overview.md
- a basic examples → a basic example
単数・複数の整合を取る修正。 - counter act → counteract
1語の動詞に修正。
testing.md
- rather at → rather than at
比較構文 “rather than” に修正。
routing.md
- "Alternatively, You" → "Alternatively, you"
文中の不要な大文字を修正。 - "used instead to
to:" → used instead ofto:
前置詞の用法を正しい形 “instead of” に修正。 - ", The ad URLs" → the ad URLs
不要なカンマと大文字をなくし自然な文に。
- 影響範囲・注意点
- ランタイムやAPIには一切影響なし
変更はguides/source/*.mdのみで、フレームワーク本体やテストコードは触られていません。ビルド結果のHTMLガイドにだけ反映されます。 - 既存コード/アプリケーションへの影響なし
仕様変更や挙動変更ではないため、アプリケーションのアップグレード時に追従が必要な点はありません。 - ガイドの検索性・理解度が向上
誤った表現に引きずられて実装例を誤解するリスクがわずかに減ります。特に英語学習中の開発者や新人向け教材として使う場合に読みやすくなります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57799
- 類似の「文書のみ修正」PRは、Railsリリースノートでも通常は互換性のない変更としては扱われません。
- ドキュメントに独自翻訳や社内Wikiを運用している場合、このPR以降の原文との差分として取り込んでおくと、後々のマージ作業が楽になります。
#57798 Fix indefinite article in error messages
マージ日: 2026/6/21 | 作成者: @55728
- 概要 (1-2文で)
英語メッセージ内の不定冠詞 ("a"/"an") の誤りを修正した、極めて小さな文言調整の PR です。挙動や API 仕様には変更はなく、ユーザー向けエラーメッセージ/ログの英語表現のみが改善されています。
- 変更内容の詳細
2-1. Active Storage (Vips 画像アナライザ)
対象ファイル:
activestorage/lib/active_storage/analyzer/image_analyzer/vips.rb
変更点:
- エラーメッセージの文言を
an Vips error→a Vips errorに修正。
「Vips」は発音的に頭が子音音(/v.../)なので、本来は "a Vips error" が正しい英語表現です。
同ファイル内の近接した rescue 節ではすでに a Vips error が使われていたため、それに揃える形の文言統一です。
この修正により、例えば以下のようなログ/エラーメッセージが出力される場合の英語が自然になります:
Failed to analyze image: a Vips error occurred(実際の文全体はコード側に依存しますが、「a Vips error」という部分が修正箇所です。)
2-2. Active Record Migration (CommandRecorder)
対象ファイル:
activerecord/lib/active_record/migration/command_recorder.rb
CommandRecorder はマイグレーション操作を記録して逆操作を生成するための内部コンポーネントで、ここに含まれるユーザー向けメッセージの不定冠詞が修正されています。
変更点:
an using_index→a using_index- "using" は発音として /juːzɪŋ/ で、先頭が子音音 (/j/) のため "a using_index" が正しい。
an column_name→a column_name- "column" の先頭も子音音 (/k/) なので "a column_name" が正しい。
コード上は、おそらくマイグレーションコマンドの記録・再生時に、未知/未サポートのオプションや引数に関して例外を投げる文言などが以下のような形で修正されています:
# 例 (イメージ)
raise ArgumentError, "Unknown keyword: a using_index"
raise ArgumentError, "Unknown keyword: a column_name"同じ付近のコードには an expression といった表現があり、こちらは母音音ではじまる "expression" に対して "an" を使うのが正しいため、そのスタイルと整合しています。
- 影響範囲・注意点
- 実行時の挙動/ロジックへの影響はなし
- 変更は文字列リテラル(エラーメッセージ/ログメッセージ)のみで、メソッドシグネチャやクラス構造、アルゴリズムには一切影響しません。
- 外部インターフェイスとしての影響は極小
- Rails のエラーメッセージ文言をパターンマッチ(文字列完全一致)でテストしている場合のみ、テストが落ちる可能性があります。
- その場合は "an using_index" → "a using_index" などに期待値を修正する必要があります。
- 国際化 (i18n) への影響はなし
- 変更対象は英語メッセージであり、i18n のキーや翻訳ファイルには影響しません。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57798
- 修正対象:
- Active Storage の Vips アナライザでのエラーログ文言
- Active Record マイグレーション
CommandRecorder内のエラー/ログ文言
- 文法上のポイント: "a"/"an" は綴りではなく「発音(先頭の音が母音か子音か)」に依存する、という典型的な英語文法の修正です。
#57797 Fix :singleton-method: directive typo for protected_environments [ci skip]
マージ日: 2026/6/21 | 作成者: @fuentesjr
- 概要 (1-2文で)
ActiveRecordクラスメソッドprotected_environmentsのRDoc用ディレクティブのスペルミスを修正し、APIドキュメントに正しく表示されるようにしたPRです。実行時の挙動や公開APIのインターフェイス自体には変更はなく、ドキュメント生成のみが対象です。
- 変更内容の詳細
- 対象ファイル:
activerecord/lib/active_record.rb - 変更点は1行のみで、RDocディレクティブのスペル修正です。
元のコードイメージ(誤り):
# :singlton-method:
# Returns the list of protected environments ...
singleton_class.attr_reader :protected_environments修正後:
# :singleton-method:
# Returns the list of protected environments ...
singleton_class.attr_reader :protected_environmentsポイント:
ActiveRecord.protected_environmentsはsingleton_class.attr_readerによって定義されています。- RDoc は通常の
def self.method_name形式でないメソッドを自動では拾えないため、
「このコメントはクラスメソッドに対するものだ」と教えるために# :singleton-method:ディレクティブを使います。 - これが
:singlton-method:という誤記になっていたため、- RDocがディレクティブとして認識できず無視
- コメントは単なる「ソース一覧中のテキスト」として扱われる
protected_environmentsメソッド自体がAPIドキュメントに現れない という状態になっていました。
- 同じパターンで定義されている
lazily_load_schema_cacheおよびschema_cache_ignored_tablesはすでに正しい# :singleton-method:を使っており、今回それに合わせて整合性を取った形です。
結果として、edge APIドキュメント上では protected_environments= (セッター)だけがクラスメソッド一覧に出ていて、protected_environments(ゲッター)が欠落しているという不自然な状態が、今回の修正により解消されます。
- 影響範囲・注意点
- ランタイムへの影響:
- 実行時のコードは一切変わっておらず、挙動・パフォーマンス・互換性への影響はありません。
- 影響範囲:
- RDoc/edge APIドキュメント生成のみが対象です。
ActiveRecord.protected_environmentsを利用しているアプリケーションのコードには影響はありません。
- 注意点:
- 「今まで
protected_environmentsの存在がドキュメント上見えなかった」ことに依存しているようなケース(例えば、ツールがAPIドキュメントをパースしてメソッド一覧を作っている場合)では、新たに1メソッドが増えたように見える可能性がありますが、実際のAPIとしてはもともと存在していたものです。 - Railsのソースコメントをベースに独自にRDocを生成している場合は、この修正を取り込むとドキュメントに同メソッドが現れるようになります。
- 「今まで
- 参考情報 (あれば)
- 該当メソッド(edge API・修正前はゲッターが欠落)
protected_environments=:
https://edgeapi.rubyonrails.org/classes/ActiveRecord.html#method-c-protected_environments-3D
- 類似のRDocディレクティブ:
# :method:/# :singleton-method:は、RDocに「これから書くコメントはインスタンスメソッド/クラスメソッド用だ」と伝えるためのマーカーです。
- RDocドキュメント(ディレクティブ仕様の参考):
https://ruby.github.io/rdoc/ (公式RDocリファレンス)
#57793 Fix typos in Rails on Rack guide
マージ日: 2026/6/20 | 作成者: @55728
概要 (1-2文で)
Railsガイド「Rails on Rack」の文書中にあるスペルミスを4箇所修正したドキュメント専用のPRです。アプリケーションコードや挙動には一切影響せず、ガイドの可読性・正確性のみを向上させています。変更内容の詳細(あればサンプルコードも含めて)
対象ファイル:guides/source/rails_on_rack.md
修正されたスペルミスは以下の4箇所です。
ミドルウェアのコメント中
"Propgate"→"Propagate""Propogate"→"Propagate"
想定される例(イメージ):
ruby# Before # Propgate the request to the next middleware # Propogate the response back to the client # After # Propagate the request to the next middleware # Propagate the response back to the clientガイド本文中
"reponse"→"response""faciliate"→"facilitate"(Rack::ETagに関する説明部分)
ETag に関する箇所のニュアンスとしては、「レスポンスのキャッシングや conditional GET を facilitate(容易にする)」といった説明文の中の綴りを修正しているものと考えられます。
いずれも意味を変える変更ではなく、英単語の正しい綴りに合わせるだけの修正です。
- 影響範囲・注意点
- 影響範囲
- Rails本体のコード、Rackミドルウェアの実装、インターフェース等には一切変更なし。
- 影響するのは「Rails on Rack」ガイド(英語版)の表示のみです。
- 注意点
- ドキュメントの更新に伴い、ガイドを引用している外部資料・ブログなどで古いスペルがそのまま残る可能性がありますが、機能上の問題はありません。
- Railsの挙動やRackミドルウェアの書き方が変わったわけではないため、既存コードの修正は不要です。
- 参考情報 (あれば)
- 該当ガイド: Guides > Rails on Rack
https://guides.rubyonrails.org/rails_on_rack.html Rack::ETagの公式ドキュメント(Rack側):
https://www.rubydoc.info/gems/rack/Rack/ETag
#57794 Fix grammar in Model.inspect schema-not-loaded message
マージ日: 2026/6/20 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord のModel.inspectがスキーマ未ロード時に返すメッセージ中の英語の文法ミス("schema informations")を、正しい "schema information" に修正した PR です。
実装コードとそれに対応するテストのユーザ向け文字列のみが変更されています。
- 変更内容の詳細
どの挙動が変わったか
ActiveRecord のモデルクラスを、接続やスキーマがまだロードされていない状態で inspect すると、従来は次のようなメッセージが返されていました(概略):
# 修正前(イメージ)
User.inspect
# => "User(id: integer, ... schema informations not loaded)"この "schema informations" という文言のうち、文法的に誤っていた複数形 "informations" を、不可算名詞である "information" に修正しました。
# 修正後(イメージ)
User.inspect
# => "User(id: integer, ... schema information not loaded)"※ 実際の文字列は Rails の実装に依存しますが、意図としては「スキーマ情報がロードされていない」というヒントメッセージ部分の語を修正しただけです。
変更ファイル
activerecord/lib/active_record/core.rbinspectがスキーマ未ロード時に返す文言を"schema informations"→"schema information"に1文字だけ修正。
activerecord/test/cases/invalid_connection_test.rb- 上記文言を前提としているテストの期待値も、同じく
"informations"→"information"に修正。
- 上記文言を前提としているテストの期待値も、同じく
PR本文で触れられているポイント
- "information" は英語で不可算名詞であり、複数形 "-s" をとらないため "informations" は誤りである。
- 既にコードベースの他の箇所では "schema information" と単数形で使われており、この箇所だけが例外だった。
- Merriam-Webster / Oxford いずれも不可算として扱っていることを根拠にしている。
- プロダクションコードのユーザ向け文字列とテストを変更しているため、
[ci skip]すべきではない。 - 単なる文言の文法修正であり、機能としての変更ではないため CHANGELOG は不要という判断。
- 影響範囲・注意点
影響範囲
- 実行時に
SomeModel.inspectを使ってログ出力やデバッグをしている場合、その出力文言が微妙に変わります。 - 文字列を厳密一致で検証しているテスト(アプリケーション側のテストなど)が、旧
"schema informations"を前提にしていると、今回の変更後の Rails に上げたタイミングでテストが落ちる可能性があります。
- 実行時に
互換性
- メソッドシグネチャや振る舞い(スキーマ未ロード時にヒントを返す、というロジック自体)は変わっていないため、機能的な互換性問題はほぼありません。
- 国際化 (i18n) とは無関係であり、英語メッセージの typo 修正に近い位置づけです。
注意点
- ログ解析やスクレイピングなどで
inspectの出力を機械的にパースしている場合は、メッセージの一部が変わる点だけ念のためチェックしてください(通常はinspect出力をパースするのは推奨されません)。
- ログ解析やスクレイピングなどで
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57794
- "information" が不可算名詞であることについては一般的な英和辞典 (Merriam-Webster / Oxford) に準拠。
- Rails 内部でも他箇所ですでに
"schema information"という単数形が使われており、今回で表記揺れが解消されています。
#57796 Make config.rake_eager_load more explicit
マージ日: 2026/6/20 | 作成者: @fxn
- 概要 (1-2文で)
config.eager_loadが Rake 実行時に「無視される」のではなく「書き換えられる」挙動と、そのトリガーであるconfig.rake_eager_loadの役割をドキュメントと生成テンプレート上で明示的にした PRです。Rails のオートロード/イager load の契約を文章レベルで分かりやすくし、開発者が本番環境やテスト環境での eager load の実際の挙動を誤解しにくくしています。
- 変更内容の詳細
2-1. 背景となる現在の挙動
通常の
production.rbでは以下のように設定します:ruby# config/environments/production.rb config.eager_load = trueしかし Rake タスク実行時には、Rails の内部ロジックにより
config.eager_loadがfalseに書き換えられる 場合があります。その制御フラグがconfig.rake_eager_loadです。デフォルトの Rails アプリでは、「本番は常に eager load される」と思いがちですが、Rake タスク時はそうとは限らないというのが実態です。
これまでは production.rb のコメントに
# Eager load code on boot for better performance and memory savings (ignored by Rake tasks).とだけ書かれており、「ignored(無視)」という曖昧な表現のせいで、
- 実際には「ユーザーが設定した値が Rake 実行時に上書き(変更)される」
- その裏で
config.rake_eager_loadが関わっている
という契約が伝わりにくくなっていました。
この PR は 挙動自体は変えず、「どういう契約になっているのか」をコメントとガイドで明文化します。
2-2. production.rb テンプレートのコメント更新
railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt にある生成テンプレートのコメントがより正確な表現に変更されています。
概念的には以下のニュアンスになります(実際の英語コメントの意図):
# Eager load code on boot for better performance and memory savings.
# Note: For Rake tasks, eager loading is controlled by config.rake_eager_load
# and may override this value.
config.eager_load = trueポイント:
- 「ignored by Rake tasks」という曖昧な表現から、
- Rake 実行時には
config.rake_eager_loadによって config.eager_loadが上書きされうる
- Rake 実行時には
- という因果関係をはっきりさせている。
これによって、「Rake タスクだと eager load は常に無効になる」と思い込むのではなく、「config.rake_eager_load の設定次第で挙動が変わる」ことが読み取れます。
2-3. test.rb テンプレートへのコメント追加
railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt にも、eager_load とテスト環境に関するコメントが追記されています。
テスト環境ではデフォルトで:
config.eager_load = ENV["CI"].present?のような設定があり、「CI では eager load する」などのパターンがありますが、ここでも
- eager load の意味
- どのような場面で有効になるか
がより明示される方向のコメント追加になっています(本 PR の主眼は production+Rake の説明ですが、整合性を取るために test も補強された形)。
2-4. オートロード/リロードガイドの追記
guides/source/autoloading_and_reloading_constants.md にも 1 行ほど追記され、以下の点がドキュメントとして明記されます:
config.eager_loadは「本番では常に true」ではなく、Rake タスク時にはconfig.rake_eager_loadの設定によって上書きされることがある。- したがって、「Rails は production で必ず eager load される」というのは厳密には誤りである。
これにより、ガイドからオートロード戦略/ブート時のロード戦略を理解するときに、Rake の存在を考慮できるようになります。
- 影響範囲・注意点
3-1. 実行時挙動は変わらない
- コードのロジック変更はなく、「コメントとドキュメントの改善」のみです。
- 既存アプリの挙動(特に Rake 実行時の eager load の有無)は一切変わりません。
3-2. config.rake_eager_load の重要性が上がる
今後、ドキュメントやコメントを読んだ開発者は
- なぜ Rake では eager load されないのか
- Rake でも eager load させたい/させたくない場合はどうすればよいか
を改めて意識するようになります。実際の制御は config.rake_eager_load で行われているため、Rake タスクでの挙動をカスタマイズしたい場合はここを調整します。
例:
# すべての Rake タスクで eager load したい場合
config.rake_eager_load = true
# デフォルトどおり Rake では eager load したくない場合
config.rake_eager_load = false3-3. 誤解が減るが、インターフェイスは歴史的事情のまま
PR 作成者も述べている通り:
- 「ユーザー設定を裏でひっそり書き換える」のは好ましくないが、
- 互換性の問題から、今すぐこのインターフェイスを変えるのは難しい
という前提は変わりません。
この PR は、少なくとも挙動を正しく説明することで、将来的な API 見直しの足場づくりと、現行仕様の理解促進を狙っています。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57796
- 関連する設定:
config.eager_loadconfig.rake_eager_load
- ドキュメント(該当ガイド):
guides/source/autoloading_and_reloading_constants.md- オートロードと eager load の関係、および環境別の挙動を整理する際に参照価値があります。
#57791 Fix typos in comments
マージ日: 2026/6/20 | 作成者: @55728
- 概要 (1-2文で)
Rails のコメント中のタイプミス(スペルミス)を 2 箇所だけ修正する PR で、実行時の挙動や API には一切影響がありません。ドキュメント品質・可読性向上のみを目的とした変更です。
- 変更内容の詳細
2-1. Action Cable: socket.rb のコメント修正
対象ファイル: actioncable/lib/action_cable/server/socket.rb
conenctions という誤記を connections に修正しています。
(コードのロジックやインターフェイスは変更されていません)
イメージとしては以下のような差分です:
- # This socket manages client conenctions
+ # This socket manages client connections※実際のコメント文自体はこの例と異なる可能性がありますが、修正内容はこのような単語のスペル修正のみです。
Action Cable におけるソケット/クライアント接続関連の説明コメントが正しい綴りになり、コードリーディング時の違和感が解消されます。
2-2. Active Support: RedisCacheStore のコメント修正
対象ファイル: activesupport/lib/active_support/cache/redis_cache_store.rb
configued という誤記を configured に修正しています。
例:
- # Returns the configued redis connection
+ # Returns the configured redis connectionRedis キャッシュストアの設定や接続に関するコメントの英語が正しくなっただけで、動作や設定項目には変更がありません。
- 影響範囲・注意点
- 実行コードには一切手が入っておらず、「コメントのみ」の変更です。
- そのため:
- 既存アプリケーションの挙動、パフォーマンス、互換性への影響はありません。
- マイグレーション、設定変更、依存関係の更新なども不要です。
- テストも追加されていませんが、コメント修正のみのため妥当な対応と言えます。
- CHANGELOG の更新も行われていませんが、「ドキュメント的変更」であり挙動変更がないため、Rails の運用方針としても問題ない範囲です。
開発者視点では、「Rails の公式コメントを参考にコードを読む・社内ガイドに引用する」といったケースで誤記に引きずられない、という程度の改善です。
- 参考情報 (あれば)
- 対象コンポーネント:
Action Cable(ActionCable::Server::Socket周辺)ActiveSupport::Cache::RedisCacheStore
- PR の性質:
- 純粋なタイポ修正(リファクタリングや内部仕様変更ではない)
- 挙動変更なし、後方互換性への懸念なし
バージョンアップ時にこの PR を理由に動作検証を厚くする必要はなく、通常のパッチレベル更新の一部として扱って問題ない内容です。
#57787 Fix Rails.app.dotenvs to honor an explicit path argument
マージ日: 2026/6/20 | 作成者: @55728
- 概要 (1-2文で)
Rails.application#dotenvsにpath引数を渡しても、最初の呼び出し時のパスしか反映されないバグを修正し、パスごとに別々の.env設定を正しく扱えるようにした PR です。dotenvsのメモ化方法を「単一インスタンス」から「パスごとのインスタンス」に変更しています。
- 変更内容の詳細
問題のポイント
Rails.application#dotenvs は本来、任意の .env ファイルパスを指定できるインターフェースになっています:
Rails.application.dotenvs("/path/to/.env.custom")しかし実装では、戻り値をインスタンス変数1つでメモ化していました:
def dotenvs(path = Rails.root.join(".env"))
@dotenvs ||= ActiveSupport::DotEnvConfiguration.new(path)
endこのため:
- 最初の呼び出し時に渡した
pathだけが使われる - 2回目以降は、
path引数を変えても@dotenvsがそのまま返される - 実質的に「最初に呼んだパス固定」であり、それ以降の
pathは無視される
さらに、開発環境では Rails.application.creds が内部で dotenvs をデフォルトパス (通常は Rails.root.join(".env")) で呼び出します:
# railties/lib/rails/application.rb
@creds ||= ActiveSupport::CombinedConfiguration.new(envs, dotenvs, credentials)そのため、どこかで一度 Rails.application.creds を呼ぶと、そのタイミングで .env のパスが確定してしまい、後から
Rails.application.dotenvs("/path/to/.env.custom")と呼んでも、常にデフォルトの .env を参照するという、非常に気付きづらい誤動作になっていました。
他のメソッドとの整合性
同ファミリのメソッドは以下のような振る舞いになっています:
| accessor | path 引数 | メモ化 | path を尊重するか |
|---|---|---|---|
encrypted(path, …) | あり | なし (毎回 new) | する |
credentials | なし | あり (単一インスタンス) | n/a |
dotenvs(path = …) (従来) | あり | あり (単一インスタンス) | しない (バグ) |
dotenvs だけが、「パスを受け取るのに単一インスタンスでメモ化している」というハイブリッドで不整合な仕様でした。
修正内容
メモ化を「パスごとのインスタンス」を保持する方式に変更しています:
def dotenvs(path = Rails.root.join(".env"))
(@dotenvs ||= {})[path] ||= ActiveSupport::DotEnvConfiguration.new(path)
endポイント:
@dotenvsを単一のActiveSupport::DotEnvConfigurationではなく、pathをキーとする Hash に変更- 各
pathごとに一度だけActiveSupport::DotEnvConfiguration.new(path)を呼び出してメモ化 - これにより:
- 同じパスを何度も指定しても、再パースせずにキャッシュされたインスタンスを使える
- パス違いの
.envは、それぞれ独立した設定として扱える
@dotenvs はこのメソッド内でしか参照されておらず (creds もここ経由でデフォルトパスを取るだけ)、単一インスタンスからハッシュへの変更は外部 API 上は破壊的ではありません。
テスト追加
railties/test/application/creds_test.rb にテストが 10 行追加されています。
テストの意図としては、主に以下を確認していると考えられます:
- デフォルトパスで一度
dotenvs/credsを呼んだ後でも、 - 別パスを指定して
dotenvs(custom_path)を呼ぶと、 - 正しく custom_path 由来の値が取得できること
(=「最初のパス固定」になっていないこと)
- 影響範囲・注意点
- 影響対象:
Rails.application.dotenvs(path)を複数パスで使っているアプリケーション- 開発環境で
creds(=Rails.application.creds) とdotenvs(custom_path)の両方を利用しているケース
- 振る舞いの変化:
- これまで: 最初に呼んだときの
pathだけが有効で、それ以降の呼び出しでは引数pathが無視される - 修正後:
pathごとに独立したDotEnvConfigurationが使われ、呼び出しごとに与えたpathが正しく尊重される
- これまで: 最初に呼んだときの
- パフォーマンス:
- 同じ
pathに対しては相変わらず 1 度だけパースし、以降はキャッシュを利用するため、従来からの性能特性は維持 - 新たに複数パスを使い始めた場合、それぞれのパスで
.envが初回パースされるコストがかかるが、これは本来期待される挙動
- 同じ
- 互換性:
- 公開インターフェース (
dotenvsのシグネチャ・返り値の型) は変わっていない - ただし「バグとも取れる古い挙動(最初のパスが固定される)」に依存していた場合、結果が変わる可能性はあります
- 例: 最初に意図せずカスタム
.envを読んでしまい、後からデフォルト.envを読むつもりでdotenvs(default_path)を呼んでいた、など
- 例: 最初に意図せずカスタム
- 公開インターフェース (
実用上は「修正後の方が直感的で正しい」挙動なので、多くのアプリではバグ修正として歓迎されるはずです。
- 参考情報 (あれば)
- 該当メソッド:
railties/lib/rails/application.rbのRails::Application#dotenvs - 関連 API:
Rails::Application#creds
→ActiveSupport::CombinedConfiguration.new(envs, dotenvs, credentials)ActiveSupport::DotEnvConfiguration
- 類似の設計パターン:
encrypted(path)が非メモ化で毎回 new を返すのに対し、dotenvs(path)は「パスごとにメモ化」するハイブリッド設計
→ パフォーマンスと API 一貫性を両立する意図が読み取れます。
#57788 Remove unused private EnvConfiguration#lookup
マージ日: 2026/6/20 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::EnvConfigurationに定義されていた、実際には一度も呼ばれていない private メソッド#lookupが削除されました。動作仕様の変更はなく、内部コードの整理・不要コード削除のみです。
- 変更内容の詳細
対象ファイル:
activesupport/lib/active_support/env_configuration.rb(+0 / -4)
削除されたのは private メソッド #lookup(env_key) だけです。PRの説明によると:
#require,#option,#keysなどのパブリックメソッドは、環境変数相当の値を取得する際に、lookupを経由せず 直接 インスタンス変数@envsを参照していました。
例:ruby@envs.fetch(envify(*key)) @envs[envify(*key)]- サブクラス
DotEnvConfigurationもlookupを一切呼んでおらず、parse_env_file/reload内で@envsを構築するのみでした。 - リポジトリ全体を grep した結果でも、
lookupの定義以外に参照がまったく存在せず(send(:lookup)や:lookupなど動的ディスパッチも含めて)、完全に未使用のメソッド であることが確認されています。
このため、EnvConfiguration クラスから以下のようなメソッド定義が削除された形になります(イメージ):
class ActiveSupport::EnvConfiguration
private
# こういったメソッドが削除された
# def lookup(env_key)
# @envs[env_key]
# end
end- 影響範囲・注意点
公開 API への影響:
ActiveSupport::EnvConfigurationおよびDotEnvConfigurationの パブリックな振る舞いは一切変わりません。- 削除された
#lookupは private メソッド であり、フレームワーク外から呼び出すことは想定されていなかったため、通常の利用者には影響ありません。
破壊的変更の可能性:
- メソッドが private であっても、以下のようにメタプログラミングで無理やり呼び出していたコードがあれば壊れます:ruby
config = ActiveSupport::EnvConfiguration.new(...) config.send(:lookup, "FOO") # ← こういうコードは動かなくなる - ただし PR 説明の通り、Rails 本体ではそのような利用は確認されていません。
- メソッドが private であっても、以下のようにメタプログラミングで無理やり呼び出していたコードがあれば壊れます:
サブクラスへの影響:
lookupをオーバーライドしてカスタマイズしていた独自サブクラスがもし存在していれば、- 元の private メソッドが呼ばれていなかったため、「オーバーライドしてもそもそも使われていない」状態だったと考えられます。
- 今回の削除により「オーバーライド対象そのものがなくなった」だけで、動作は従来どおり(=そもそも何も起きていなかった)です。
- そのため、実質的な挙動変更はなし と考えて差し支えありません。
テスト観点:
- 既存テストで
EnvConfiguration/DotEnvConfigurationの挙動がカバーされていれば、そのままグリーンであることが期待されます。 - もしアプリ側で env 周りのラッパーやヘルパーを自作している場合、
send(:lookup, ...)のようなコードがないか検索しておくとより安全です。
- 既存テストで
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57788
- 関連クラス:
ActiveSupport::EnvConfigurationActiveSupport::EnvConfiguration::DotEnvConfiguration(.envファイルを読み込んで@envsを構築するサブクラス)
- この変更は、未使用コードの削除による 内部実装のクリーンアップ であり、Rails 7/8 系へのマイグレーション上の考慮点にはほぼなりません。
#57786 [ci skip]Fix typo in comment for ENV.fetch usage in application.rb
マージ日: 2026/6/20 | 作成者: @willnet
- 概要 (1-2文で)
application.rb 内でENV.fetchの使い方を説明しているコメントにあった誤字を修正するだけの PR です。コードの挙動や外部 API には一切変更がなく、ドキュメント品質の向上のみを目的としています。
- 変更内容の詳細(あればサンプルコードも含めて)
- 対象ファイル:
railties/lib/rails/application.rb - 変更内容:
ENV.fetchの利用例を説明するコメント行の文言修正(typo 修正)
Rails の application.rb には、環境変数を使った設定の推奨パターンとして、以下のような ENV.fetch の利用例がコメントで記載されています:
# For example, to use an environment variable:
# config.x.some_api_key = ENV.fetch("SOME_API_KEY")この PR では、このようなコメント例の中の誤字・表現ミスが修正されています。
コードそのもの(ENV.fetch の使い方や設定のキーパスなど)は変更されておらず、修正はコメント行の 1 行のみ(+1 / -1)です。
- 影響範囲・注意点
- 実行時挙動への影響: なし
- コメントのみの修正のため、アプリケーションの挙動、パフォーマンス、互換性に影響はありません。
- 公開 API / インターフェースへの影響: なし
- マイグレーションや設定変更の必要: なし
- テスト・CHANGELOG:
- バグ修正や機能追加ではなくコメント修正のみのため、テスト・CHANGELOG は更新されていません(チェックボックスも未チェック)。
- 参考情報 (あれば)
- PR タイトルに含まれる
[ci skip]は、CI を回すまでもない軽微な変更(コメント修正など)であることを示しています。 ENV.fetchは環境変数が未設定の場合に例外を発生させられるため、Rails 公式コメント中でも「必須の環境変数を明示的に要求する」用途で推奨されることが多いです。
例:rubyこうした用法を説明するコメントがより正確になったという位置づけの変更です。config.x.payment.api_key = ENV.fetch("PAYMENT_API_KEY")
#57579 Add RFC 9110 compliant Accept header content negotiation opt-in
マージ日: 2026/6/19 | 作成者: @williantenfen
- 概要 (1-2文で)
RFC 9110 に準拠したAcceptヘッダによるコンテンツネゴシエーションを行うオプション設定config.action_dispatch.respect_accept_header_rfc9110が追加されました。デフォルトは従来通りの挙動(IE7 向けワークアラウンドを維持)で、明示的に opt-in したアプリだけが新しい挙動になります。
- 変更内容の詳細
2-1. 新しい設定フラグの追加
config.action_dispatch.respect_accept_header_rfc9110 が追加されました。
- デフォルト:
false falseの場合(従来互換モード)- IE7 向けに 2010 年から存在するワークアラウンドがそのまま使われます。
Acceptヘッダに*/*が含まれていると「ブラウザからのリクエスト」とみなし、HTML を優先する(BROWSER_LIKE_ACCEPTSフィルタ)挙動が続行されます。
trueの場合(RFC 9110 準拠モード)- RFC 9110 に則り、
Acceptヘッダの- メディアタイプの具体性(
text/html>text/*>*/*) q値(品質値) を正しく考慮してネゴシエーションします。
- メディアタイプの具体性(
*/*を含んでいても、より具体的なメディアタイプがあればそれが優先されます。
- RFC 9110 に則り、
挙動の具体例
# 従来挙動 (respect_accept_header_rfc9110 = false)
# Accept: application/json, */*
# → IE7 ワークアラウンドにより、ブラウザライクとみなして HTML を選ぶ可能性が高い
# 新挙動 (respect_accept_header_rfc9110 = true)
# Accept: application/json, */*
# → RFC 9110 に従い、より具体的な application/json を優先して JSON を選択この PR の説明中の例も同じで、*/* を含むブラウザ由来(に見える)ヘッダでも、JSON をきちんと選ぶようになります。
2-2. 既存設定とのコンフリクト警告
config.action_dispatch.ignore_accept_header と新フラグが両方とも true に設定された場合、ログに警告が出ます。
- 意味合い:
ignore_accept_headerは「Acceptヘッダそのものを無視する」フラグ。respect_accept_header_rfc9110は「Acceptヘッダを RFC 9110 に準拠して解釈する」フラグ。
- 両方
trueの場合は論理的に矛盾するため、- Rails は
ignore_accept_headerを優先 - そのことを警告ログで明示します。
- Rails は
2-3. アップグレードパス / new_framework_defaults
config/initializers/new_framework_defaults_8_2.rb のテンプレートに、コメントアウトされた形で以下のような設定が追加されています(抜粋イメージ):
# Rails.application.configure do
# # Enable RFC 9110 compliant Accept header content negotiation.
# # This will prioritize specific media types and quality values as per the standard.
# config.action_dispatch.respect_accept_header_rfc9110 = true
# end- 新規アプリやバージョンアップ時にこの initializer を確認すると、
- 何が変わるのか
- なぜ opt-in なのか がコメントで説明されています。
- CHANGELOG とガイド (
guides/source/configuring.md) にも説明が追加され、設定項目として正式にドキュメント化されています。
2-4. 実装とテスト
変更ファイルの主なポイント:
action_dispatch/http/mime_negotiation.rbBROWSER_LIKE_ACCEPTSによる IE7 ワークアラウンド適用を、上記フラグで切り替えられるように調整。- フラグ ON の場合は、
Acceptをフィルタリングせず、そのまま標準的な優先度計算にかける形になります。
- テスト (
respond_to_test.rb) 追加:test_rfc9110_accept_header_respects_specificitytext/html, */*vsapplication/jsonなど、具体性の違いに応じた正しい選択を検証。
test_rfc9110_with_xhr_request- XHR リクエスト時(
XMLHttpRequest/fetchなど)に、従来モード・RFC 9110 モードの両方で期待どおりのフォーマットが選ばれることを検証。
- XHR リクエスト時(
- 影響範囲・注意点
3-1. 既存アプリへの影響
- デフォルト値は
falseのため、この PR のマージだけでは挙動は変わりません。 - 既存アプリは、従来通りの「
*/*を含む Accept をブラウザとみなして HTML を優先」のロジックが維持されます。
3-2. フラグを有効化した場合の影響
config.action_dispatch.respect_accept_header_rfc9110 = true にすると:
- これまで「HTML が返ってきていた」ケースで JSON や他のフォーマットが返るようになる可能性があります。
- 例: API ではなく通常 HTML コントローラに対して、
- ブラウザの拡張やクライアントが
Accept: application/json, */*を送っていた場合、 - 新挙動だと JSON レスポンスが選ばれることがあります。
- ブラウザの拡張やクライアントが
- 例: API ではなく通常 HTML コントローラに対して、
- 特に
respond_toを使って複数フォーマットに対応しているコントローラでは、- 「暗黙的に HTML が選ばれていた」挙動が変わる可能性があるため、
- フラグ ON 前に統合テスト・E2E テストなどで挙動確認を推奨します。
3-3. ignore_accept_header との併用
- すでに
config.action_dispatch.ignore_accept_header = trueを使っているアプリでは、- 新フラグを
trueにしても意味がなく、ignore_accept_headerが優先されます。 - ログに警告が出るため、どちらの挙動を本当に望んでいるかを見直す必要があります。
- 新フラグを
3-4. 将来的な変更の可能性
- PR の説明にもある通り、「将来のメジャーバージョンで IE7 ワークアラウンドを削除できるようにするため」の布石となる変更です。
- 8.2 以降のどこかで:
respect_accept_header_rfc9110をデフォルトでtrueにする、- あるいはワークアラウンドを削除する、 可能性が高いので、長期的にはアプリ側で RFC 9110 挙動への対応を進めておくと安全です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57579
- 関連 issue: Fixes #57353
- RFC 9110 (HTTP Semantics) – Section on Content Negotiation / Accept header
- 変更ファイル:
actionpack/lib/action_dispatch/http/mime_negotiation.rbactionpack/lib/action_dispatch/railtie.rbrailties/lib/rails/application/configuration.rbguides/source/configuring.mdconfig/initializers/new_framework_defaults_8_2.rb.ttなどに設定項目と挙動の説明が反映されています。
#57780 Extract Template.validate_formats and Template.normalized_formats
マージ日: 2026/6/19 | 作成者: @etiennebarrie
- 概要 (1–2文で)
このPRは、Template::Typesへ直接アクセスしてリクエストフォーマットを処理していたコードを、Templateクラス内のヘルパーメソッド(validate_formats/normalized_formats)経由にリファクタリングしたものです。あわせて、Templateの autoload 定義の重複を削除し、並び順を整理しています。
- 変更内容の詳細
2-1. フォーマット処理の責務を Template に集約
これまで ActionView::LookupContext などで、テンプレートのフォーマット(html, json など)を扱う際に、Template::Types の実装に「直接」依存していました。このPRでは、その依存を隠蔽するために、ActionView::Template に以下の2つのクラスメソッドが追加されています:
Template.validate_formats(formats)Template.normalized_formats(formats)
(※PR本文から機能だけが読み取れる状態で、実装詳細は推測を含みますが、Rails既存コードのパターンからみて、次のような役割が想定されます)
Template.normalized_formats(formats)
- 渡された
formatsを、Template::Types依存の詳細を隠しつつ「正規化」するメソッド。 - 想定される処理例
:html/"html"/Mime[:html]のような入力を、内部で一貫した表現(シンボル配列や MIME 型配列など)にそろえる。nilや空配列をデフォルトフォーマット([:html]など)に変換。
Template.validate_formats(formats)
- 上記の正規化された
formatsが、Template::Types(または ActionDispatch が提供する MIME/フォーマット定義)にとって有効かどうかを確認するメソッド。 - 想定される処理例
- 未定義フォーマット(例:
:foobar)を検出してエラーを投げる、あるいは無視/警告する。 - フォーマット指定が
nilだったり、明らかに不正な型(数値など)の場合に例外を発生させる。
- 未定義フォーマット(例:
これらの実装では、内部的には Template::Types を使いつつも、その利用は Template クラス内部に閉じ込められます。これにより、利用側(LookupContext など)は Template::Types の実装差分や初期化状態を気にせず、Template のインターフェイスだけを使えばよくなります。
2-2. Action Dispatch 有無による Template::Types の差分を吸収
PR説明の:
They had to be defined on Template because Template::Types has a different implementation if Action Dispatch is loaded.
から読み取れるのは、以下の点です。
ActionDispatchがロードされているかどうかで、Template::Typesの中身や挙動が変わる実装になっている。- その状態でアプリケーション側や他コンポーネントが
Template::Typesに直接触れると、Rails のロード順・環境によって挙動が変わる(または壊れる)リスクがある。 - そこで「フォーマットのバリデーション/正規化」という共通処理を、
Templateクラス内のメソッドにまとめることで、「どの状態でも正しく動く入口」を1つにした、という設計上の改善です。
2-3. LookupContext からの呼び出しを Template 経由に変更
actionview/lib/action_view/lookup_context.rb の変更は少ないですが、主旨は:
- 以前:
Template::Typesに直接アクセスしてフォーマットを扱っていた - 以後:
Template.validate_formats/Template.normalized_formatsを経由するようにする
ことです。
これにより:
LookupContextは「フォーマットの正規化・検証」を、Templateに委譲するだけの薄いクラスになる。Template::Typesの具体的な実装や、ActionDispatchのロード状況に依存しなくなる。
2-4. Template の autoload 定義の整理
actionview/lib/action_view/template.rb 内の、autoload 定義から:
- 重複して宣言されていた autoload を削除
- 宣言順を整理(ソート)して、今後の重複を見つけやすくした
というメンテナンスが行われています。
Rails は autoload による動的ロードが多いため、重複や順序が原因で予期せぬロード順になったり、Zeitwerk との兼ね合いで警告が出る原因になりえます。今回の整理は、そのような問題の種を減らす目的の微修正です。
- 影響範囲・注意点
外部 API の変更は基本的に無し
- 通常のビュー描画や
renderの利用に関して、アプリケーションコード側が意識すべき破壊的変更はなさそうです。 - 変更は内部的な責務の整理と依存箇所のリファクタリングが中心です。
- 通常のビュー描画や
直接
Template::Typesを利用しているコードへの注意- 自作のライブラリやアプリコードで、
ActionView::Template::Typesを直接参照している場合、今回のPRで即座に壊れるわけではありませんが、「今後の変更でより隠蔽が進む」方向性が示されたと考えるとよいです。 - もし「リクエストフォーマットの正規化や検証」を独自に行っている場合、将来的には
Template側に公開される(または既にある)メソッドを使う設計に寄せると安全です。
- 自作のライブラリやアプリコードで、
ActionDispatch ロード順に敏感なコードは見直し候補
- このPRの背景自体が、「ActionDispatch のロード有無によって Template::Types 実装が変わる」問題です。
- もしアプリ側が「ActionDispatch ありき」の前提で
Template::Typesを触っていると、テスト環境やコンソール、特定のタスク実行時など、ロードパターンが異なる状況で不整合が生じる可能性があります。
autoloader 周辺の挙動
- autoload の重複削除と並び替えにより、「特定のファイルが想定外のタイミングでロードされる/されない」というレアケースの不具合が解消される可能性があります。
- 逆に、新しい autoload の順序に依存していた極端に特殊なコードがあれば影響を受ける可能性はありますが、通常のアプリケーションでは問題にならないはずです。
- 参考情報 (あれば)
参考になりそうな関連コンセプト(ソース未提示のため概念レベル)
ActionView::Template: ビューのテンプレート1つを表すクラス。フォーマット、ハンドラ(ERB, Haml など)、変換済みソースなどを持つ。ActionView::LookupContext: ビューを検索するコンテキスト。パス、フォーマット、ロケールなどを元に、どのテンプレートを使うか決定する。Template::Types: 拡張子やシンボル(html,json等)とMime::Typeのマッピングを持つ内部構造。ActionDispatch: リクエスト/レスポンス、ルーティングなど HTTP レイヤーを提供するコンポーネントで、Mimeやフォーマット周りの定義にも関与。
類似の変更意図としては、過去の Rails コアでも
- 「コンポーネント間で内部構造を直接参照していた部分を、明示的なヘルパーメソッドに置き換える」
- 「ロード順や環境差による subtle bug を避けるために責務を集約する」 といったリファクタリングが繰り返し行われており、その延長線上の PR だと捉えると理解しやすいです。
#57775 Make ActiveRecord::Base.primary_key a class attribute
マージ日: 2026/6/19 | 作成者: @ilianah
- 概要 (1-2文で)
ActiveRecord::Base.primary_keyが「クラス属性 (class_attribute)」として扱われるようになり、table_nameと同様にサブクラスへオーバーライド内容が継承されるようになりました。これにより、抽象クラスや STI での主キー指定がより直感的で一貫した挙動になります。
- 変更内容の詳細
何が変わったか
ActiveRecord::Base.primary_keyが、「クラスごとに保持され、サブクラスにも継承される属性」として定義されるようになりました。- これにより、あるモデルクラスで
self.primary_key = ...と上書きすると、そのクラスを継承したサブクラスも同じprimary_keyを使うのが基本挙動になります(table_nameと同じ感覚)。
以前は、primary_key の解決ロジックがより動的で、継承との関係が直感的でないケースがありましたが、今回の変更で「クラス属性として継承される」というルールに整理されています。
典型的な利用パターン
抽象クラスでの共通 primary_key 指定
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
self.primary_key = "uuid"
end
class User < ApplicationRecord
# primary_key は "uuid" を継承
end
class Post < ApplicationRecord
# 同じく "uuid"
end今後はこのようなコードを書いた場合、User や Post も uuid を主キーとして扱うことが期待どおりに機能します。
STI (Single Table Inheritance) での利用
class Vehicle < ActiveRecord::Base
self.primary_key = "code"
end
class Car < Vehicle
# primary_key は "code" を継承
end
class Truck < Vehicle
# これも "code"
endSTI 親クラスで primary_key を変えておけば、子クラス側は特に何もしなくても同じキーを用いるようになります。
サブクラス側での上書き
クラス属性なので、もちろんサブクラス側で再度オーバーライドも可能です。
class Animal < ActiveRecord::Base
self.primary_key = "global_id"
end
class Cat < Animal
# "global_id" を継承
end
class Dog < Animal
self.primary_key = "dog_id" # ここだけ別の主キーに
endこの場合、Animal と Cat は "global_id"、Dog だけ "dog_id" になります。
- 影響範囲・注意点
影響がありそうなケース
既に
primary_keyをオーバーライドしていて、そのクラスのサブクラスも存在するアプリ
以前の挙動が「暗黙的に継承されていない」状態に依存していた場合、今回の変更によりサブクラスも親クラスのprimary_keyを引き継ぐようになり、挙動が変わる可能性があります。例: 以前は「親で
primary_keyを変えたがサブクラスはデフォルト主キーのまま」という前提で設計していた場合、今後はサブクラスも新しい主キーを使う挙動になる可能性が高いです。複雑な継承ツリー (抽象クラス + STI + 通常継承ミックス)
継承階層のどこかでprimary_keyを変更すると、その下流のクラスにまで設定が伝播します。どのクラスで何を指定しているかを改めて確認した方が安全です。
移行時に確認したい点
- モデル階層で、どのクラスが
self.primary_key = ...を呼んでいるかを grep などで洗い出す。 - 親クラスで指定した
primary_keyがサブクラスにも伝播して問題ないかをテーブル定義と照合する。 - STI を使っているテーブルで、すべてのサブクラスが同じ主キー定義で動作しているかを確認する。
- もし「サブクラスには継承してほしくない」ケースがある場合は、そのサブクラス側で明示的に
self.primary_key = ...を再指定する。
- 参考情報 (あれば)
table_nameも同様にクラス属性として扱われており、「親で指定 → 子へ継承」という挙動です。本 PR はprimary_keyをこれと同じ一貫したモデルに揃える変更です。- 変更ファイル:
activerecord/lib/active_record/attribute_methods/primary_key.rbprimary_keyの定義がクラス属性的な扱いになるよう調整。activerecord/test/cases/primary_keys_test.rb
継承や抽象クラス/サブクラスに関する動作テストが追加。activerecord/CHANGELOG.md
上記仕様変更がリリースノートに追記。
#57773 Remove outdated documentation for overriding primary_key
マージ日: 2026/6/19 | 作成者: @ilianah
- 概要 (1-2文で)
Rails 7.2系以降(PR #57638 の変更)では、primary_keyの値だけを単独でオーバーライドすることができなくなったため、その古い方法を説明していたドキュメントコメントが削除されました。コードの挙動自体は変わらず、ドキュメント上の記述のみが整理されています。
- 変更内容の詳細
対象ファイル:
activerecord/lib/active_record/attribute_methods/primary_key.rb- 追加行: 0
- 削除行: 10(コメントのみ)
削除されたのは、primary_key を「単にクラスメソッドをオーバーライドするだけで変更できる」かのようなドキュメントコメントです。
PRの説明にある通り、前のPR (#57638) によって、primary_key の値は primary_key_definition と同期している必要があるように設計が変わっており、次のような昔ながらのやり方は正しい手順としては扱えなくなりました。
class Book < ApplicationRecord
self.primary_key = :isbn
endあるいは:
class Book < ApplicationRecord
def self.primary_key
"isbn"
end
end以前はこうした記述で「主キー名だけ」変えるケースが多く、ドキュメントにもそれを示唆するコメントがありましたが、現在の実装では「主キーの定義全体(型・オプションなど)を含む primary_key_definition」との整合性が必須になっているため、「primary_key だけ変えればよい」という説明は誤解を生むため削除されています。
コード上のロジックやメソッドの挙動は一切変更されておらず、削除されたのはコメント(ドキュメント)のみです。
- 影響範囲・注意点
直接の挙動変更はなし
- このPR自体はコメント削除のみなので、アプリケーションの動作は変わりません。
しかし背景として:
- PR #57638 以降の Rails では、以下の点に注意が必要です。
primary_keyを単独でオーバーライドするのではなく、primary_key_definitionと一貫性を持たせる必要がある。- 主キーをカスタマイズする場合は、マイグレーション(DB定義)と
primary_key_definitionの双方が、実際のテーブル定義と一致しているかを確認する必要があります。
- PR #57638 以降の Rails では、以下の点に注意が必要です。
既存コードの見直しポイント
- もしアプリ内で以下のようなコードだけで済ませていると、将来的なバージョンアップ時に不整合が起きる可能性があります。ruby
class User < ApplicationRecord self.primary_key = "uuid" end - その場合:
- DB側の主キー定義(例:
id: :uuid, default: ...) - Rails側の
primary_key_definition(型・オプションを含む)
を合わせて整合性を取る必要があります。
- DB側の主キー定義(例:
- もしアプリ内で以下のようなコードだけで済ませていると、将来的なバージョンアップ時に不整合が起きる可能性があります。
ドキュメントを信頼して実装している場合の注意
- 旧来のコメントを読んで「
primary_keyだけ変えればよい」と思っていた開発者が、今後はその手法を「推奨されていない/一部ケースで破綻する」ものとして再考すべき、というメッセージとも解釈できます。
- 旧来のコメントを読んで「
- 参考情報 (あれば)
- このPR:
- 関連PR(仕様変更の本体):
- 主キーに関する Rails ガイド(英語):
- Active Record Basics – Primary keys
https://guides.rubyonrails.org/active_record_basics.html#primary-keys
- Active Record Basics – Primary keys
主キーをカスタマイズしているアプリでは、primary_key だけでなく primary_key_definition の扱いと、DBの実際のテーブル定義との整合性を今後意識しておくと安全です。
#57651 Fix associations on a new record when the owner has a composite primary key
マージ日: 2026/6/19 | 作成者: @jsaubry
- 概要 (1-2文で)
複合主キーを持つレコードに対するhas_many/has_oneの関連が、「未保存だがキーはすべて埋まっている」場合に常に空([]/nil)になってしまう問題を修正したPRです。foreign_key_present?で複合主キーを正しく判定するようにし、単一主キーと同様に関連クエリが発行されるようになりました。
- 変更内容の詳細
不具合の内容
対象は、例えば以下のような複合主キーを持つモデルです(PR説明に出てくる例):
class Cpk::Book < ApplicationRecord
self.primary_keys = [:author_id, :id]
has_many :chapters,
foreign_key: [:author_id, :book_id]
endこのとき、まだ保存していないが複合主キーの値はすべてセットされているレコード:
book = Cpk::Book.new(id: [1, 10]) # new_record? == true
# author_id + id の2つのキーは埋まっている前提に対して:
book.chapters.to_aとすると、本来は (author_id, id) を条件にクエリが発行されて関連レコードが返ってくるべきです。しかし、これまでは 一切クエリが発行されず、常に [] が返っていました。has_one の場合も同様に nil 固定になる挙動でした。
一方で、単一主キー のモデルでは、同じ状況(未保存だが主キー値がセット済み)のときにちゃんとクエリが発行されていました。この差異が「複合主キーだけ壊れている」状態になっていた、というのが問題です。
原因の詳細
原因は ActiveRecord::Associations::ForeignAssociation#foreign_key_present? の実装にあります。
reflection.active_record_primary_keyは、複合主キーの場合は配列(例:["author_id", "id"])になります。- しかし従来コードは、この配列をそのまま
attribute_present?に渡していました。
疑似コードでいうと、以前はこんな形だったと考えられます:
def foreign_key_present?
owner.attribute_present?(reflection.active_record_primary_key)
endattribute_present? は本来 "author_id" といった「単一のカラム名の文字列 or シンボル」を期待していますが、配列を渡されると:
- 配列全体を文字列化したキー(例:
["author_id", "id"]→"["author_id", "id"]")を探しにいく - 当然そんなカラムは無いため、常に
falseを返す
その結果、
null_scope? # has_many側で使われる
# => new_record? && !foreign_key_present?が常に true になり、「未保存で外部キーが無いレコードだからクエリを飛ばさず空スコープにしておこう」という分岐に入ってしまい、クエリが発行されない状態になっていました。
has_one 側でも Association#find_target? 経由で同じ foreign_key_present? を参照しているため、同じ原因で nil 固定になっていました。
修正内容
foreign_key_present? を、複合主キーなら各キーを個別にチェックするように変更しています。変更後のロジック:
def foreign_key_present?
Array(reflection.active_record_primary_key).all? do |key|
owner.attribute_present?(key)
end
endポイント:
Array(...)で単一キーでも複合キーでも配列に正規化- 各キー (
"author_id","id"など) についてowner.attribute_present?(key)を呼ぶ - すべてのキーが present? のときに
trueを返す
これにより:
- 単一主キーの場合: 配列長1なので従来挙動と実質同じ
- 複合主キーの場合: 「すべての構成キーが埋まっているか」を正しく判定できるようになる
結果として:
book = Cpk::Book.new(id: [1, 10])
book.chapters.to_a
# Before: [] ← クエリなし
# After: [...] ← (author_id, id) によるクエリが発行されるとなります。
テスト追加
以下のテストが追加されています:
has_many_associations_test.rb- モデル:
Cpk::Book→has_many :chapters - ケース:
- 複合主キーのすべてがセットされている場合に関連を正しく取得できること
- どれか1つのキーが欠けている場合はクエリしない(空になる)こと
- モデル:
has_one_associations_test.rb- モデル:
Cpk::Order→has_one :book(複合キー前提) - 上記と同様に「全キーあり」「一部欠け」の両方を検証
- モデル:
CHANGELOG.md にもこの修正が追記されています。
- 影響範囲・注意点
影響を受けるのは:
- 複合主キーを使っているモデル
- かつ
has_many/has_oneの関連を持っているケース - かつ「未保存 (
new_record? == true) だが、主キーカラムはすべて埋まっている」状態で関連を参照している箇所
これまで:
- そのような関連呼び出しは常に
[]/nilを返していた - その挙動に依存しているコードがある場合、「クエリが発行され結果が返る」ように変わるため、挙動が変わります
- そのような関連呼び出しは常に
本来は「単一主キーと同じく、キーが揃っているなら関連を解決できるべき」なので、この変更は バグ修正 であり、期待される仕様への整合です。ただし以下には注意してください:
- 新たに関連クエリが発行されるようになるため、パフォーマンスに影響する可能性があります(※今まで「たまたまクエリが飛んでいなかった」部分)。
- 未保存レコードに対する関連の存在チェックや分岐ロジックで、空を前提にしていた場合は挙動見直しが必要になるかもしれません。
単一主キーのみを使っているアプリケーションには挙動変更はありません。
- 参考情報 (あれば)
このPR自体:
- タイトル: “Fix associations on a new record when the owner has a composite primary key”
- 番号: #57651
- マージ日時: 2026-06-19T12:07:40Z
関連する複合主キー周りの最近の修正:
- #57534
- #57158
関連コード:
ActiveRecord::Associations::ForeignAssociation#foreign_key_present?CollectionAssociation#null_scope?(has_many側)Association#find_target?(has_one側)
#57641 Enable per-pool query log tags prepend config
マージ日: 2026/6/18 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
このPRは、Active Recordのクエリログタグ機能を「コネクションプールごと」に細かく設定できるように拡張したものです。database.ymlでプール単位にタグ付けそのものの有効/無効、フォーマット、コメントの前後どちらに付けるかを指定できるようになります。
- 変更内容の詳細
2-1. テストヘルパーのリファクタリング
まず最初に、クエリログ関連のテストヘルパーが汎用的に使えるようにリファクタリングされています(query_logs 用に閉じていたものを「任意キー」に対して使えるようにしたイメージ)。
これにより、この後追加される query_log_tags や prepend_comment など、複数のキーを同じ仕組みでテストできるようになっています。
※ユーザー側のAPIには直接の変更はありませんが、今後の拡張に耐えやすいテスト構造になっています。
2-2. query_log_tags をプールごとに設定・無効化できるように
これまで:
- グローバルには
config.active_record.query_log_tags(およびquery_log_tags_formatなど)で設定 database.yml側にはトップレベルにquery_log_tags_formatがあったが、プール単位での細かな制御は限定的
このPR後:
database.ymlの各プールにquery_log_tagsキーを定義でき、その値でプールごとのタグ設定を行えるquery_log_tagsをfalseにすると、そのプールだけクエリログタグ機能を完全に無効化できるquery_log_tagsをハッシュにすると、その中のオプションでグローバル設定を上書きできる- これに伴い、従来のトップレベル
query_log_tags_formatキーは廃止方向になり、フォーマッタもquery_log_tags配下に統合される
サンプル (database.yml):
production:
primary:
database: primary
analytics:
database: analytics
query_log_tags:
format: sqlcommenter # このプールだけフォーマットを変更
replica:
database: replica
replica: true
query_log_tags: false # このプールはクエリログタグを付けない動作イメージ:
config.active_record.query_log_tagsでグローバルなタグ内容(例: application, controller, job など)を設定- 各プール:
query_log_tags: false→ グローバルに有効でも、このプールだけタグ付けを行わないquery_log_tags: { format: sqlcommenter }→ グローバルはlegacyでも、このプールだけsqlcommenterフォーマットでタグを埋め込むquery_log_tags未指定 → 従来どおり、グローバル設定に従う
実装側では ActiveRecord::DatabaseConfigurations::HashConfig に query_log_tags 関連の設定を読み込むロジックが追加され、ActiveRecord::QueryLogs が「グローバル設定 + プールごとのオーバーライド」を解決するように更新されています。
2-3. prepend_comment をプールごとに上書きできるように
さらに、このPRのメインである「クエリログコメントの前後位置」をプール単位で制御する機能も追加されています。
これまで:
config.active_record.query_log_tags_prepend_comment(真偽値)で、クエリコメントをtrueならSELECTなどの前にコメントを付けるfalseならSQL文の末尾にコメントを付ける
- というグローバルな設定しかできなかった
このPR後:
database.ymlの各プールのquery_log_tags配下でprepend_commentを指定できる- これにより「このDBだけはコメントを先頭に付ける/末尾に付ける」といった振る舞いを切り替え可能
サンプル (database.yml):
production:
primary:
database: primary
analytics:
database: analytics
query_log_tags:
prepend_comment: true # このプールに対してのみ、コメントをSQLの先頭に付ける動作イメージ:
- グローバル:
config.active_record.query_log_tags_prepend_comment = false(末尾に付ける) analyticsプール:query_log_tags.prepend_comment: true→ このプールの接続から発行されるSQLは、コメントを先頭に付ける
- 他のプール:
query_log_tags.prepend_comment未指定 → グローバル設定falseを継承し、末尾にコメントを付ける
内部実装では ActiveRecord::QueryLogs が「プールごとの prepend_comment があればそれを優先し、なければグローバル設定を使う」ように解決ロジックを持つように変更されています。
- 影響範囲・注意点
設定ファイル (
database.yml) の構造変更- 従来
query_log_tags_formatをトップレベルでプールごとに設定していた場合、このPR以降はquery_log_tags: { format: ... }に移行するのが推奨されます。 - 新旧の両方がどう扱われるかは該当バージョンのCHANGELOGやガイドを参照する必要がありますが、「トップレベル
query_log_tags_formatは今後非推奨になっていく」という前提で移行しておくと安全です。
- 従来
プールごとのタグ無効化によるログのばらつき
query_log_tags: falseを使うと、同じアプリケーション内でもプールによってクエリログにコメントがあったりなかったりする状態になります。- ログ解析基盤(特にsqlcommenter互換ツールなど)を使っている場合、プールごとに挙動が違うことを意識してダッシュボードやクエリを書かないと、データが取りこぼされる可能性があります。
prepend_commentの差異による影響- 一部のSQLプロキシ/セキュリティツール/DBドライバが、文頭コメントを好まない(あるいは末尾コメントしか想定していない)場合があります。
- 新しく
prepend_comment: trueを設定するプールでは、そのDBや周辺ツールが先頭コメントに対応しているかを確認したほうがよいです。 - 既にグローバルで
prepend_commentを使っている場合でも、プールごとの設定が優先されるため、思わぬ動作の違いが出ないように設定を整理しておく必要があります。
既存コードへの互換性
- アプリケーションコード(モデルやクエリ発行側)は変更不要で、影響は主に設定レイヤーとログ出力のフォーマットに限定されます。
- ただし、クエリログをパースしている自前ツールがある場合、プールごとのフォーマット差異や有無を考慮する必要が出てきます。
- 参考情報 (あれば)
対応するコミット:
- テストヘルパーの汎用化:
https://github.com/rails/rails/pull/57641/commits/a3a0668f60177041d80db20b22651f80683b9adf - プールごとの
query_log_tags設定/無効化:
https://github.com/rails/rails/pull/57641/commits/5d813755af2a289701d82a21318d6413da7b8f5d - プールごとの
prepend_comment設定:
https://github.com/rails/rails/pull/57641/commits/0551469a644b3b51d5d785aad1e57f5177e06e20
- テストヘルパーの汎用化:
関連ファイル:
activerecord/lib/active_record/query_logs.rb
→ クエリログタグの適用ロジック(グローバル vs プールごとのオーバーライド)activerecord/lib/active_record/database_configurations/hash_config.rb
→database.ymlからquery_log_tags/prepend_commentを読み取る処理activerecord/test/cases/query_logs_test.rb
→ プールごとの設定が正しく反映されることを検証するテスト群
#57765 Validate retry_on wait: argument type at class definition
マージ日: 2026/6/18 | 作成者: @55728
- 概要 (1-2文で)
Active Job のretry_onオプションwait:に渡せる値の型チェックを、「リトライ実行時」ではなく「ジョブクラス定義時」に行うようにした PR です。これにより、サポート外の値を指定した場合に、元のエラーを潰す形の内部エラーではなく、クラス読み込み時に明確なArgumentErrorが発生するようになります。
- 変更内容の詳細
何が問題だったか
これまで:
class MyJob < ApplicationJob
retry_on SomeError, wait: "soon" # サポートされない型(String)
endのように、wait: に不正な型を指定した場合、
- 実際にジョブ実行中に
SomeErrorが発生 retry_onによるrescue_fromハンドラ内でリトライディレイを計算 (determine_delay)determine_delayのcase文で型がマッチせずRuntimeError発生- 結果として、アプリ本来の例外ではなく「Active Job 内部の RuntimeError」に置き換わってしまう
という振る舞いでした。
つまり、バグに気づくのが「実行時 + エラー発生後」かつ、「本来の例外が隠される」 という問題がありました。
変更点
この PR では、型チェックを determine_delay(実行時)ではなく、retry_on(クラス定義時)に移動・追加しています。
サポートされる wait: の型は、determine_delay の case に合わせて、以下に限定されます:
- 数値:
Integer,Float- 秒数として扱われる
- 例:
wait: 5,wait: 2.5
ActiveSupport::Duration- Rails の時間ヘルパ
- 例:
wait: 1.hour,wait: 30.seconds
- シンボル
:polynomially_longer- Active Job が内部的に持っているポリノミックなバックオフ戦略
- 例:
wait: :polynomially_longer
Proc- 実行回数
executionsを受け取って秒数を返す遅延計算ロジック - 例:ruby
retry_on SomeError, wait: ->(executions) { executions * 5 } # 5, 10, 15, ...
- 実行回数
これら以外の型が指定された場合:
retry_on呼び出し時(= ジョブクラス読み込み時)にArgumentErrorが発生- メッセージは「どの型が許可されているか」が分かる形になる(テストから推測される)
なお、determine_delay 側の else ブランチは削除せず、防御的なフォールバックとして残されています。
つまり「クラス定義時のチェック漏れ」や将来の拡張に備えて、実行時にも最後の砦として RuntimeError が残っている状態です。
テスト (変更された内容の要点)
activejob/test/cases/exceptions_test.rb に、主に次のようなテストが追加されています:
- サポートされない
wait:を指定したときにArgumentErrorが発生すること- 例:
wait: "soon"(String)
- 例:
- サポートされる型(上記4種)についてはクラス定義が成功すること
これにより、「クラスロード時に型エラーとして検出される」ことが保証されています。
- 影響範囲・注意点
影響範囲
- 対象: Active Job の
retry_onを利用しているジョブクラス - 特に影響を受けるのは:
- これまで「たまたま」動いていたがサポート外の型を渡していたケース
- 実行時まで潜伏していた設定ミスが、アプリケーション起動時やクラスロード時に露出する ようになるケース
振る舞いの変化
- 以前:
- 不正な
wait:を指定してもクラス定義は通る - ジョブ実行中にリトライが発火したタイミングで内部
RuntimeErrorが発生 - 元の例外が隠れてしまうことがある
- 不正な
- 以後:
- 不正な
wait:を指定した時点でクラス定義時にArgumentError - アプリ起動/デプロイ時にすぐに設定ミスが発覚する
- 不正な
既存アプリケーションでの注意点
retry_onに以下のような指定をしていないか確認するとよいです:wait: "1.second"のような Stringwait: SomeCustomObject.newのような独自オブジェクトwait: :exponentially_longerなど、Rails がサポートしていないシンボル
- もし上記のような指定があれば、次のいずれかの形に修正する必要があります:
- 単純な数値:ruby
retry_on SomeError, wait: 5 # 5秒 - Duration:ruby
retry_on SomeError, wait: 1.minute - カスタムロジック:ruby
retry_on SomeError, wait: ->(executions) { executions**2 } # 1, 4, 9, ...
- 単純な数値:
- 参考情報 (あれば)
- 元 PR: #57727(
retry_on/determine_delay周りの改善に関連) - この PR での方針:
- 「オプション引数の型エラーはクラス定義時に検出すべき」という設計の明確化
- 例外ハンドラ内部でアプリケーション例外を上書きしてしまう状況の解消
- 関連コード場所:
activejob/lib/active_job/exceptions.rbretry_onの定義とオプションバリデーションdetermine_delay(実行時の実際の遅延計算ロジック)
#57777 8-1-stable: Clear Postgres warnings as they get handled
マージ日: 2026/6/18 | 作成者: @matthewd
- 概要 (1–2文で)
Rails 8.1系(8-1-stable)に対し、「PostgreSQL の warning を処理したタイミングで逐次クリアする」挙動を backport した PR です。これにより、同一コネクション上で警告が溜まり続けて後続クエリに悪影響を与える問題を防ぎ、より正確に warning を扱えるようにしています。
- 変更内容の詳細
※ 元 PR #57584 の backport であり、main ブランチ相当の実装を 8-1-stable に合わせて移植した形です。
2-1. 警告処理の責務の整理・抽象化
activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
- 「SQL 実行時に出る warning をどう扱うか」というロジックの一部が、PostgreSQL 固有実装から抽象層(AbstractAdapter / DatabaseStatements)側に移されました。
- 具体的には、
- 「クエリ実行後に、アダプタごとの方法で warning を取得」
- 「warning を
ActiveSupport::Notifications/ ロガー / エラーハンドリングに流す」 - 「処理済みの warning を消去する」
という一連の処理フローが、より汎用的な形で書かれています。
- これにより、
- PostgreSQL 以外のアダプタでも、将来的に同様の「warning を処理しつつ都度クリアする」仕組みを取り込みやすくなります。
- PostgreSQL アダプタ側は「どうやって DB から warning を取得し、どうやってクリアするか」という DB 固有部分に集中できます。
(イメージ的な流れ・擬似コード)
def execute(sql, name = nil)
result = raw_execute(sql, name)
handle_warnings_if_any # <= ここでアダプタごとの warning 取得・処理・クリアを呼ぶ
result
end2-2. PostgreSQL アダプタ側での warning クリア処理
activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
- 以前は「コネクションが保持している warning を、一度にまとめて処理する」ような実装になっており、場合によっては:
- 古い warning がコネクションに残ったまま、
- 別クエリのタイミングでまとめて報告される
という挙動が起こり得ました。
- 今回の変更では、PostgreSQL アダプタが提供する warning 取得ロジックを、
- 「直近のクエリ実行に対応する warning を取得」
- 「それを通知・ログ出力等で処理」
- 「処理済みの warning をその場でクリア」
という粒度で動かすようにしています。
- 具体的には:
execute/exec_queryなど、クエリを実行するメソッドの周辺にある warning 処理コードが整理され、- 古い warning を引きずらず、クエリごとに独立して warning を扱うようになっています。
- これにより、例えば:
- トランザクション開始時の warning
- 一時テーブル操作や型変換の warning
などが、後続の SELECT や UPDATE のタイミングで「まとめて出てくる」誤動作を抑制できます。
2-3. テストの追加
activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
- Postgres アダプタ専用のテストが 54 行追加されています。
- テスト内容の主なポイント:
- 特定の SQL(例:
SHOW、非推奨機能、型変換など)で warning を発生させ、 - その warning が直後に正しく検出・処理されること
- 処理後、次のクエリ実行時に同じ warning が再度出てこない(=クリアされている)こと
- 複数回クエリを実行しても、warning の報告が重複したり、前のクエリの warning が混ざらないこと
などを検証しています。
- 特定の SQL(例:
- これにより、「warning を処理した時点でクリアする」という今回の仕様が、PostgreSQL 実機テストで担保されるようになっています。
- 影響範囲・注意点
3-1. 影響範囲
- 対象: Rails 8.1 系で PostgreSQL を使っているアプリケーション(
8-1-stableブランチ相当) - 具体的な挙動の変化:
- 同じコネクションで連続してクエリを投げたとき、以前は「古い warning が後続クエリのタイミングで出てくる」ことがあったケースで、クエリごとにより正確な warning が報告されるようになります。
- warning を監視・収集しているロギング基盤や APM(New Relic など)を使っている場合、
- warning のタイミングや件数が「実際のクエリ実行タイミング」に近づいて変化する可能性があります。
3-2. 互換性・注意点
- 破壊的変更ではなく、基本的にはバグフィックス/改善の類です。
- ただし次のようなケースでは挙動変化に注意してください:
- 「コネクション単位で溜まっている warning を、後でまとめて見る」ことを前提にした、独自のモンキー・パッチやメトリクス収集をしている場合
- 今回からは「クエリごとに処理 & クリア」されるため、想定より warning が減ったように見えることがあります。
- PostgreSQL の warning をトリガーにした特殊なワークアラウンドを組んでいる場合
- warning の発生タイミングがよりシビアに「直近クエリと 1 対 1 対応」するため、テストで挙動を再確認してください。
- 「コネクション単位で溜まっている warning を、後でまとめて見る」ことを前提にした、独自のモンキー・パッチやメトリクス収集をしている場合
- Rails アプリケーションコード(モデル、コントローラ等)の修正は通常不要ですが、
- warning を前提にしたログ監視のしきい値・アラート条件は、実際の発火状況が変わっていないか確認することを推奨します。
- 参考情報 (あれば)
- 元 PR(main ブランチ向け)の番号: #57584
→ この PR #57777 は、その backport です。 - 関連ファイル:
activerecord/lib/active_record/connection_adapters/abstract/database_statements.rbactiverecord/lib/active_record/connection_adapters/postgresql/database_statements.rbactiverecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
- 文脈として:
- Rails では PostgreSQL の notice / warning をロガーや
ActiveSupport::Notificationsに流す仕組みが以前からありますが、
本 PR は「warning の ライフサイクル をクエリ単位に揃える」ことで、
バグを減らしつつ、より予測可能な warning ハンドリングを実現する変更です。
- Rails では PostgreSQL の notice / warning をロガーや
#57774 Remove primary_key class ivar
マージ日: 2026/6/18 | 作成者: @ilianah
- 概要 (1-2文で)
Active Record のprimary_key周りの実装からクラスインスタンス変数@primary_keyを廃止し、primary_keyとprimary_key_definitionの同期を取りやすくするための内部リファクタリングです。外部 API の挙動はほぼ変えずに、内部状態の一貫性を高める変更です。
- 変更内容の詳細
対象ファイル:
activerecord/lib/active_record/attribute_methods/primary_key.rb
この PR では、primary_key を保持していたクラスインスタンス変数 @primary_key(またはそのエイリアス的な扱い)を削除し、primary_key と primary_key_definition の 2 つの情報源が不整合にならないようにするための整理が行われています。
前提として、ひとつ前の PR (#57638) で primary_key_definition 周りの整理・追加が行われ、その結果として:
- 「主キー名」 (
primary_key) - 「主キーの定義情報(型・オプションなど)」 (
primary_key_definition)
の2つが併存する構造になっていました。
今回の PR では、これら 2 つの値を別々の経路で保持しないようにし、@primary_key という別ルートを無くすことで、値の更新漏れ・同期ミスが発生しにくい実装にしています。
具体的なコード上の変化(推測を含む一般的なイメージ):
以前は例えばこんな構造だった可能性があります:
rubyclass << self def primary_key @primary_key ||= compute_primary_key end def primary_key=(value) @primary_key = value # primary_key_definition 側もどこかで別途更新していた end end今回の変更で、
@primary_key自体を持たず、primary_key_definitionから導出したり、単一の情報源に寄せている形に近づいています:rubyclass << self def primary_key primary_key_definition&.name end def primary_key=(value) # primary_key_definition を通じて設定するなど、 # 一箇所に責務を集約 self.primary_key_definition = build_definition_from(value) end end
実際には行数としては +3 / -6 と小さな差分ですが、
@primary_key関連の参照/設定が削除primary_keyを求める/設定する経路がより一元化
といった整理になっています。
- 影響範囲・注意点
通常の Rails アプリ利用者
- 通常の
ApplicationRecord/ モデルでself.primary_key = "id"のように指定しているだけであれば、挙動は変わらない想定です。 - 公開 API (
primary_key,primary_key=) のインターフェースに目立った変更はなく、あくまで内部実装の整合性向上が主眼です。
- 通常の
メタプログラミングや内部 API を直接触っている場合
以下のような場合は影響を受ける可能性があります:ActiveRecord::Base(または自分のモデル)のクラスインスタンス変数@primary_keyを直接読んだり書いたりしているコードruby# 例: こういったコードは今後壊れる/動作保証外になる可能性が高い model_class.instance_variable_get(:@primary_key) model_class.instance_variable_set(:@primary_key, "custom_id")- ActiveRecord の内部モジュール(
ActiveRecord::AttributeMethods::PrimaryKey)を monkey patch しているコード
こうしたコードは、
primary_key/primary_key=/primary_key_definitionといった公開メソッド・公式な内部 API(あれば)を経由する形にリファクタリングしたほうが安全です。ライブラリ / gem 開発者
- Active Record と密結合な gem(特に
primary_keyの設定・検査を独自に行うもの)は、@primary_keyに依存していないか確認する必要があります。 - 将来的に
primary_key_definition側が主/正となっていく可能性が高いため、主キー情報を扱う場合はprimary_keyの戻り値だけでなく、primary_key_definitionの存在も意識しておくとよいです。
- Active Record と密結合な gem(特に
- 参考情報
- 該当 PR:
- Remove primary_key class ivar (#57774)
- 関連 PR(今回の前提となる変更):
- https://github.com/rails/rails/pull/57638
primary_key_definitionの導入・整理を行った PR と考えられます
- https://github.com/rails/rails/pull/57638
総じて、この PR は「主キー情報の単一情報源化」の一環であり、Rails 内部の整合性と保守性を高めるための小さな一歩と捉えると理解しやすいです。
#57770 8-0-stable: Backport recent test suite fixes
マージ日: 2026/6/18 | 作成者: @matthewd
- 概要
Rails 8.0-stable ブランチに対して、テストスイートの安定性向上・バグ回避のために、main 側 (#57767) で行われた修正の「適用可能な部分だけ」をバックポートした PR です。主に Active Record と Action Mailbox のテストコードやテスト用ヘルパ、接続プール周りに手が入り、CI のフレーク・環境依存の失敗を減らすことが目的です。
- 変更内容の詳細
※PR の説明文と差分ファイル一覧から推測できる範囲での解説です(実装細部はリポジトリを直接確認してください)。
2-1. Action Mailbox: Mailgun / Mandrill ingress テストの安定化
対象ファイル:
actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rbactionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rbactionmailbox/test/test_helper.rb
想定される修正内容:
- Mailgun / Mandrill からの受信メール処理テストで、外部 API 風のパラメータや署名検証、エンコーディングなどに関する不安定要因を解消。
test_helper.rbで、Action Mailbox 用の共通セットアップを整理し、テスト環境でのルーティングや ingress の有効化を一元化。
開発者目線のポイント:
- 実際の挙動ではなく「テストの書き方」が変わった可能性が高く、本体機能への直接の挙動変更はほぼないと考えてよいです。
- カスタム ingress を書いている場合でも、この PR はテスト周りのみなので影響は限定的です。
2-2. Active Record: connection_pool とテストフィクスチャ周りの修正
対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rbactiverecord/lib/active_record/test_fixtures.rbactiverecord/test/cases/connection_pool_test.rbactiverecord/test/cases/connection_adapters/connection_handler_test.rbactiverecord/test/cases/reaper_test.rbactiverecord/test/cases/test_case.rbactiverecord/test/cases/relation/load_async_test.rbなど
想定される変更点:
connection_pool.rb の修正 (+12/-4)
- 接続プールの取得・返却(checkout/checkin)や、スレッド終了時のクリーンアップロジックをテストしやすく、かつフレークしづらくするための小さな修正が入っている可能性が高いです。
- 典型的には以下のような調整が行われます:
- タイミング依存のロジック(reaper が動く時刻計算など)を安定化。
- プールが空/満杯の状態判定やログ出力の条件を明確化。
- スレッドローカルにぶら下がる接続のリセット強化。
test_fixtures.rb / test_case.rb の修正
- テストフィクスチャ読み込みと DB 接続のライフサイクルが整理され、テストケース間での接続リークやトランザクションの取り扱いが改善されていると考えられます。
ActiveRecord::TestFixturesをミックスインしたテストクラスで、接続を正しく共有/分離するための内部実装の修正です。
connection_pool_test.rb / connection_handler_test.rb / reaper_test.rb の強化
- 実際のバグ報告や CI フレークを再現するための追加テストが大量に入っていると見られます(
+61/-1,+22/-0など)。 - スレッドを使った並列アクセス・接続リーク・reaper による回収が想定通り動くかを検証するテストが追加されている可能性が高いです。
- 実際のバグ報告や CI フレークを再現するための追加テストが大量に入っていると見られます(
サンプルイメージ(あくまで典型例):
# 例: connection_pool_test 内のテストイメージ
def test_connections_are_released_when_thread_finishes
pool = ActiveRecord::Base.connection_pool
thread = Thread.new do
pool.with_connection do |conn|
assert conn.active?
end
end
thread.join
# スレッド終了後にプール内接続がすべて idle に戻っているか確認
assert_equal pool.size, pool.connections.count(&:idle?)
end2-3. PostgreSQL / MySQL / Trilogy アダプタテストの修正
対象ファイル:
activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb(+75/-59)activerecord/test/cases/adapters/postgresql/referential_integrity_test.rbactiverecord/test/cases/adapters/abstract_mysql_adapter/active_schema_test.rbactiverecord/test/cases/adapters/trilogy/trilogy_adapter_test.rbactiverecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
想定される変更点:
PostgreSQL:
- 特定の PostgreSQL バージョンでのみ失敗するテスト(型情報、拡張、参照整合性など)を条件分岐等で回避。
- JSON / Range / Enum など、拡張型を扱うテストの期待値を現行の挙動に合わせる修正。
- 並列に接続を張るテストでタイミング依存を減らす工夫。
MySQL / Trilogy:
- 型推論やスキーマ情報取得テストで、MySQL 実装差や Trilogy 特有の挙動に合わせた期待値調整。
- 新しい MySQL バージョンや MariaDB で違いが出る部分(文字コード、デフォルト値など)への対応。
いずれも「本体のアダプタの挙動を大きく変える」というより、「現在の挙動に合わせてテストを修正」した色が強いバックポートです。
2-4. その他テスト系修正 (autosave, dirty, strict_loading など)
対象ファイル:
activerecord/test/cases/autosave_association_test.rbactiverecord/test/cases/dirty_test.rbactiverecord/test/cases/fixtures_test.rbactiverecord/test/cases/relation/or_test.rbactiverecord/test/cases/strict_loading_test.rbactiverecord/test/cases/relation/load_async_test.rbなど
想定される内容:
dirtyテストでは、属性変更検知(changed?,saved_change_to_attribute?など)の期待値が現行実装とずれていたケースを修正。strict_loadingのテストで、関連ロードが禁止される/許可されるケースの境界条件を明確化。or/load_asyncなど Relation のテストで、非決定的な順序・タイミングに依存しないように修正。
これらも基本的には「挙動に合わせたテスト側の修正」であり、アプリケーションコードの互換性を壊す変更ではない位置付けです。
- 影響範囲・注意点
対象ブランチ:
8-0-stable
Rails 8.0 系を利用しているプロジェクトのうち、Rails 自体を vendoring している or 8-0-stable を追従している場合にのみ影響します。通常の gem 利用の場合は、これらは次回の 8.0.x リリースに含まれる想定です。本体機能への影響:
この PR は「test suite fixes(テストスイート修正)」にフォーカスしており、プロダクション環境での挙動が大きく変わる変更は含まない設計になっています。
ただしconnection_pool.rbやtest_fixtures.rbなど、本番でも使われるコードが一部修正されているため、以下は軽くチェックしておく価値があります:- 独自に
ActiveRecord::Base.connection_poolを直接操作しているコード。 - カスタムのテストヘルパ / フィクスチャ拡張を行っているコード。
- テストで並列実行(parallel_tests など)やマルチ DB 構成を多用している場合。
- 独自に
期待されるメリット:
- CI 上での Rails 本体テストのフレーク減少。
- PostgreSQL / MySQL / Trilogy など複数 DB 対応のテスト安定性向上。
- 8-0-stable をベースにプラグインやパッチを開発している場合、開発者自身のテストも安定しやすくなる可能性。
- 参考情報
元 PR:
#57767
この PR で「recent test suite fixes」と呼ばれているもののフルセットは #57767 にあり、本 PR #57770 はそのうち 8.0 系に適用可能な部分のみをバックポートしたものです。詳細なコード差分や議論は #57767 を参照すると把握しやすいです。関連しそうな Rails コード:
ActiveRecord::ConnectionAdapters::ConnectionPoolActiveRecord::TestFixtures- 各 DB アダプタテスト (
postgresql_adapter_test,trilogy_adapter_testなど) - Action Mailbox ingress テスト (
Mailgun,Mandrill)
プロジェクトで 8-0-stable を追っている場合は、CI でテストが不安定な箇所や、DB 接続まわりで不可解なテスト失敗があれば、この PR 以降のコミットを取り込むことで改善する可能性があります。
#57767 8-1-stable: Backport recent test suite fixes
マージ日: 2026/6/18 | 作成者: @matthewd
- 概要 (1–2文)
このPRは Rails 8.1 ステーブルブランチに対して、直近のテストスイート修正をまとめてバックポートしたものです。主に Action Mailbox と Active Record 周りのテストと接続プールまわりの実装が調整され、CI の安定性とテストの信頼性を高めています。
- 変更内容の詳細
全体像としては、「機能追加」ではなく「テストを通すための修正・安定化」が中心です。ただし、その過程で本体コード(特に connection_pool)にも小さな修正が入っているため、挙動の角が取れている可能性があります。
2-1. Action Mailbox: Mailgun / Mandrill ingress テストの修正
対象:
actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rbactionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rbactionmailbox/test/test_helper.rb
想定される内容:
- Mailgun / Mandrill からの inbound email を処理するコントローラテストの安定化
- 認証用署名ヘッダやタイムスタンプ、Webhook ペイロードなどが、実際の仕様変更や外部 API の挙動に追従するように修正
- テストヘルパの共通化・DRY 化、もしくはテスト環境でも再現しやすいようにスタブ/フィクスチャの整理
Rails アプリ側への示唆:
- 実運用で Mailgun / Mandrill ingress を使用している場合、テストでの署名検証・パラメータ構造が実際の API 仕様により近づいている可能性があります。独自でテストを書いている場合も、この修正を参考に実データフォーマットを見直せます。
2-2. Active Record: Connection Pool の修正とテスト強化
対象:
activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb(+21/-11)activerecord/test/cases/connection_pool_test.rb(+94/-30)activerecord/test/cases/connection_adapters/connection_handler_test.rb(+22/-0)activerecord/test/cases/reaper_test.rb(+1/-1) など
主なポイント:
接続プール実装の微修正
- 接続の取得・返却処理、タイムアウト、プールのシャットダウンや再接続まわりの境界条件が整理されている可能性が高いです。
- 典型的には以下のようなケースをより安全に扱う修正が行われます:
with_connectionブロック内で例外が発生しても接続リークしない- マルチスレッド・並列テスト実行時にプールが枯渇・デッドロックしない
- テスト終了時に確実にプールがクリーンアップされる
テストの網羅性/並列実行への対応
connection_pool_test.rbの行数増から、並行アクセス・タイムアウト・再接続シナリオなど、より現実に近い使用状況をカバーするテストが追加されていると考えられます。connection_handler_testの追加により、「複数 DB / 複数ロール(writing, reading …)」環境での接続管理テストが強化されている可能性があります。
開発者に有用な観点:
- マルチ DB 構成・マルチスレッド / ジョブワーカー(Sidekiq など)での
ActiveRecord::Base.connection_poolの扱いがより安全になっていることが期待できます。 - 以前から接続周りでまれに発生していた「テストだけで起こる謎の接続エラー」や「CI でときどき落ちる」問題の改善につながる可能性があります。
2-3. Active Record: Test Fixtures とテストヘルパの改善
対象:
activerecord/lib/active_record/test_fixtures.rb(+13/-4)activerecord/test/cases/fixtures_test.rb(+2/-2)activerecord/test/cases/test_case.rb(+29/-4)
内容の方向性:
- テストフィクスチャのロード/トランザクション管理/接続管理を整理し、テストごとの状態分離がより確実になるように修正
ActiveRecord::TestCaseや関連ヘルパで、複数 DB/複数スキーマ環境でも安定してフィクスチャが扱えるような調整- 並列テスト (
parallelize(workers: X)) 実行時にもデータ汚染やレースコンディションが起こりにくいように改善
参考イメージ(アプリ側で応用できるパターン):
class ActiveSupport::TestCase
include ActiveRecord::TestFixtures
self.use_transactional_tests = true
self.fixture_path = File.join(__dir__, "fixtures")
fixtures :all
endこの PR で内部実装が強化されていることで、特に「複数データベース + フィクスチャ + 並列テスト」という構成での安定度が上がる可能性があります。
2-4. 各 DB アダプタごとのテスト修正
対象:
- MySQL / Trilogy:
activerecord/test/cases/adapters/abstract_mysql_adapter/active_schema_test.rb(+1/-1)activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb(+1/-1)activerecord/test/cases/adapters/trilogy/trilogy_adapter_test.rb(+22/-14)
- PostgreSQL:
activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb(+75/-59)activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb(+1/-1)
内容の方向性:
- 各 DB ドライバ・バージョンでの挙動差異を吸収し、テストが特定バージョンや特定環境に依存して落ちないよう調整。
- PostgreSQL 向けでは、型マッピング/参照整合性(外部キー)/スキーマ操作まわりのテストが、より実際の PG 挙動に即した形に修正されている可能性が高いです。
- Trilogy アダプタ(MySQL 互換)のテストも増えており、MySQL 互換 DB を使う構成での信頼性アップへつながります。
アプリ側インパクト:
- 本体コードへの大きな互換性変更ではなく「テストが正しく DB の実挙動を反映するようになった」方向がメインと思われるため、API 互換性への影響はほとんどないはずです。
- ただし「今まで運良く通っていたが本来はおかしい」パターンを検出するようなテスト修正がある場合、将来のバグ検出力が上がることになります。
2-5. その他 AR テストの安定化・仕様追従
対象 (抜粋):
activerecord/test/cases/autosave_association_test.rb(+1/-1)activerecord/test/cases/dirty_test.rb(+5/-7)activerecord/test/cases/relation/load_async_test.rb(+10/-4)activerecord/test/cases/relation/or_test.rb(+4/-4)activerecord/test/cases/strict_loading_test.rb(+4/-3)
おもなテーマ:
autosave,dirty tracking,strict_loading,orクエリ合成、load_async(非同期ロード)などの挙動を確認するテストの微調整- 仕様変更・バグ修正に合わせて期待値や前提条件をアップデートし、テストを最新仕様に追従
- 非同期/並列挙動を含む箇所で、タイミング依存の flaky test を減らす方向の修正(sleep 依存の削減、より堅牢な待機条件など)が含まれている可能性があります
開発者への示唆:
load_async,strict_loadingなど 7.x〜8.x で導入された比較的新しい機能の仕様が、テストを通じてよりクリアに定義されます。自分のアプリのテストを書く際のリファレンスとして Rails 本体のテストケースを参照すると有用です。
- 影響範囲・注意点
本体 API 互換性
基本的には「テスト向けの修正」「テストに合わせたバグフィックス」が中心であり、アプリケーションコードから見える API が大きく変わる類いの変更ではありません。8.1 系のマイナーなパッチレベルアップで入ってくるタイプの修正と考えてよいです。影響があり得る領域
- DB 接続プール周り:
- 不正な使い方(接続を返さない、終了済みプールにアクセスする等)に対して、以前より早期に例外を出したりするような「堅牢化」が行われている場合、レースコンディションが顕在化する可能性はあります。
- ワーカーや並列テストで接続周りの微妙な問題を抱えていた場合、これを機に挙動が変わる可能性があります(多くは「良い方向」の修正)。
- テスト基盤:
- fixtures, transactional tests, 並列テストを多用するプロジェクトでは、Rails を 8.1-stable の最新版に上げたときにテストの落ち方が変わることがあります。
→ その場合、まず Rails 本体のテストコードの該当箇所を参照し、どのように修正/回避しているかを見るのがおすすめです。
- fixtures, transactional tests, 並列テストを多用するプロジェクトでは、Rails を 8.1-stable の最新版に上げたときにテストの落ち方が変わることがあります。
- DB 接続プール周り:
移行時の実務的な注意
- 8.1.x のパッチ更新を取り込む際には、CI 環境で「並列テスト」「複数 DB/複数ロール」「Action Mailbox ingress」のシナリオを重点的に確認してください。
- DB ドライバやサーババージョン(PostgreSQL / MySQL / Trilogy など)が Rails 本体のテストが想定しているものと大きく違う場合、テスト期待値が噛み合わなくなることがあるので、そこもチェックポイントです。
- 参考情報 (あれば)
この PR は「Backport recent test suite fixes」というタイトルどおり、すでに main / 8.2 など上位ブランチにマージ済みの修正を 8.1-stable に取り込むものです。
→ つまり「新仕様の実験」ではなく、上位ブランチで実績のある安定化修正を 8.1 系にも反映する位置づけです。PR 説明にある
#57687からのサブセット取り込みにより、8.1 では適用できない/すべきでない変更は除外されています。8.1 特有の互換性制約が考慮されたうえでのバックポートです。実際の変更内容を詳しく見たい場合は、次のファイルを見るとテスト戦略・ワークアラウンドの具体例として参考になります:
activerecord/test/cases/connection_pool_test.rbactiverecord/test/cases/test_case.rbactiverecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
これらは、自分のアプリのテストや DB 接続まわりの設計を見直す際の「実用的なレシピ」として利用できます。
#57768 More test fixes
マージ日: 2026/6/18 | 作成者: @matthewd
- 概要 (1-2文で)
Rails の Active Record テスト周りと接続プールの実装に対して、並行実行時の不具合を修正する PRです。特に Ruby 3.3.5 より前の環境で使われるWeakThreadKeyMapのフォールバック実装に対するスレッドセーフティ修正が含まれています。
- 変更内容の詳細
※PR本文からソース全文はたどれませんが、タイトル・説明・変更ファイルから読み取れる範囲で解説します。
2-1. 背景: #57744 / #57750 の再修正
- 説明にある通り、この PR は「#57744 / #57750 を正しくするための再チャレンジ」として位置づけられています。
- #57744 / #57750 は、主に Active Record のテスト環境・テストケース周り、あるいは接続プールとスレッドローカルな状態管理に関する修正だったと考えられます。
- これらの PR の影響で、特に以下のような問題が出ていた可能性があります:
- 並列テスト実行時に DB 接続・スレッドローカル状態が正しくクリーンアップされない
- 特定 Ruby バージョン (Ruby < 3.3.5) における
WeakThreadKeyMapの挙動差から来るレースコンディション / 不整合
この PR は、それらの問題に対するフォローアップで、テストの安定性向上とフォールバック実装の正当性を担保するものです。
2-2. WeakThreadKeyMap フォールバック実装の並行性修正
説明にある通り:
Concurrency fix for the Ruby-before-3.3.5
WeakThreadKeyMapfallback implementation
WeakThreadKeyMap は、スレッドをキーにして値を保持するが、「スレッドが死んだら GC で自然に消える」ような弱参照的コンテナを提供するための仕組みです。Ruby 3.3.5 未満では、これを Ruby 側でフォールバック実装しており、その実装が並列アクセスに対して不十分だったようです。
想定される修正内容は例えば以下のようなものです:
- スレッドごとの値を格納する内部 Hash へのアクセスを Mutex などで保護
- スレッド終了時にキーをクリーンアップする部分のレースコンディションを修正
- 取得/設定/削除操作が、複数スレッドからの同時アクセスで壊れないようにする
疑似的なイメージ:
# 例: フォールバック WeakThreadKeyMap のイメージ (実際のコードとは異なる概念図)
class WeakThreadKeyMap
def initialize
@mutex = Mutex.new
@storage = {}
end
def [](thread)
@mutex.synchronize { @storage[thread] }
end
def []=(thread, value)
@mutex.synchronize { @storage[thread] = value }
end
def delete(thread)
@mutex.synchronize { @storage.delete(thread) }
end
# GC や Thread#join 時のクリーンアップなども @mutex の保護下で実施
end今回の PR では、これに相当する競合条件を修正し、Ruby < 3.3.5 での WeakThreadKeyMap 使用時にもスレッドセーフになるようにしていると考えられます。
この修正は、特に Active Record のコネクションプールが「スレッドごとにコネクションを割り当てる」際に WeakThreadKeyMap を介して管理している場合に直接効いてきます。
2-3. abstract/connection_pool.rb の変更 (+4/-2)
activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb にわずかながら修正が入っています。
推測される方向性:
- Ruby 3.3.5 以後は標準実装の
WeakThreadKeyMapを利用し、それ以前は Rails 内部のフォールバック版を利用する切り替えロジック - フォールバック版の API 仕様や挙動を修正した結果、
ConnectionPool側の呼び出しを微調整 - スレッドローカルコネクション管理 (
thread_cached_conns的な構造) をWeakThreadKeyMapベースに統一、あるいはその使用方法を厳密化
| 主な可能性 | 影響 |
|---|---|
WeakThreadKeyMap 初期化/参照方法の修正 | コネクションプールがスレッドごとのコネクション状態をより安全に管理 |
| 条件分岐 (Ruby バージョンチェック) の追加/修正 | Ruby バージョンごとの挙動差を吸収し、テストが安定 |
2-4. テストケース周りの変更
activerecord/test/cases/test_case.rb (+2/-2)
Active Record のテスト基底クラス (ActiveRecord::TestCase 的なもの) に対して、非常に小さな修正です。
考えられる内容:
- テスト実行前後にコネクションプールやスレッドローカルな状態をクリーンアップする処理の調整
- 並列実行時に問題となるフック (
setup,teardown,before_teardownなど) の順序や呼び出し対象の修正 - 前述の
WeakThreadKeyMapと整合するようなリセット処理の修正
activerecord/test/cases/test_case_test.rb (+83/-78)
ActiveRecord::TestCase をテストしている側のテストが大きく書き換えられています。行数的にはほぼ書き換えレベルで、主な意図は:
- #57744 / #57750 で導入された振る舞いのテストを、より正確かつ安定した形に修正
- 並列テスト・スレッド使用ケースでの DB コネクションやスレッドローカル状態のクリーンアップを検証するテストの追加/修正
- Ruby のバージョン差 (3.3.5 未満 / 以上) を跨いでも壊れないようなテスト設計に変更
サンプル的にありそうなパターン:
def test_connections_are_cleared_between_tests
# テスト1: 何らかのクエリ実行 → 接続確立
# teardown で接続がリセットされるか確認
# テスト2: 新たなテストケースインスタンスでも前の接続状態を引きずらないこと
end
def test_thread_local_connections_are_released
# スレッド内で ActiveRecord を使用
# スレッド終了後、コネクションプールにリークしていないことの確認
end実際には、WeakThreadKeyMap に絡むテストや、Ruby バージョンごとの分岐を含んだテストなどが追加・修正されているはずです。
- 影響範囲・注意点
3-1. 影響範囲
対象コンポーネント
- Active Record の接続プール (
ConnectionPool) - Active Record のテストインフラ (
ActiveRecord::TestCaseとそのテスト)
- Active Record の接続プール (
影響を受ける主なケース
- Rails 本体のテスト/Rails アプリのテストを「並列実行」している場合
- Ruby 3.3.5 未満で Rails を動かしており、スレッドを用いつつ Active Record を利用している場合
- 特に、マルチスレッドな Web サーバ (Puma など) で、スレッドごとにコネクションを持たせる運用
3-2. 実際のアプリケーションへの影響
- 本 PR の大部分はテストコードの修正です。
→ アプリ本番コードへの直接の挙動変更は小さい (connection_pool の +4/-2 のみ) と考えられます。 - ただし、
WeakThreadKeyMapフォールバック実装の並行性修正により:- Ruby 3.3.5 未満環境でスレッドを多用するアプリにおいて、
- 以前は運良く動いていた「危うい状態」が、
- より安定/安全な挙動に変わる可能性があります。
- Ruby 3.3.5 未満環境でスレッドを多用するアプリにおいて、
- 既に #57744 / #57750 の変更を取り込んでいるブランチでテストが不安定だった場合、この PR で改善する見込みがあります。
3-3. 注意点
- Ruby 3.3.5 未満で Rails edge / main ブランチを使っている場合:
- スレッドまわり・接続プールまわりの挙動が微妙に変わる可能性があるので、並列リクエスト/ジョブ実行などを行う環境での回帰テストを推奨します。
- テストコードを書いている開発者としては:
ActiveRecord::TestCaseのライフサイクル (setup/teardown で何が保証されるか) が #57744 / #57750 + この PR で安定化しているため、古い前提に依存したテストヘルパがあれば見直しが必要になるかもしれません。
- 参考情報 (あれば)
- 対応 PR:
- #57744, #57750: 本 PR で「再調整」されている変更。Active Record のテスト・接続周りの基盤修正だったと考えられます。
- Ruby 側:
WeakThreadKeyMapは Ruby 3.3 系で導入されたスレッドキー用の弱参照マップで、3.3.5 以前と以後で実装やバグ修正が入っている可能性があります。- Rails 側は Ruby 3.3.5 未満向けにフォールバック実装を持っており、この PR でそのスレッドセーフ性が改善されています。
#57756 Make AbstractController::Helper.helper_method ractor safe
マージ日: 2026/6/18 | 作成者: @andrewn617
- 概要 (1-2文で)
AbstractController::Helper.helper_methodが Ractor(Ruby の並行実行モデル)環境でも安全に動作するよう、内部で保持しているヘルパーメソッド配列の扱いを「コピーして凍結する」パターンに変更した PR です。これにより、Ractor 間で共有されるオブジェクトに対する破壊的変更を避け、スレッド/Ractor セーフティを高めています。
- 変更内容の詳細(あればサンプルコードも含めて)
何をしているか
AbstractController::Helper.helper_method は、コントローラのインスタンスメソッドを view から呼び出せる helper として公開するためのメソッドです。
従来は、内部でヘルパー名の配列を「破壊的に」変更していた(例: << で追加)のに対し、この PR では次のような「コピーして差し替える」パターンに変更しています。
ざっくり書くと、イメージは以下のような変更です(実際のコードはRails内部の変数名等が異なりますが、意図はこれと同じです):
# 変更前(イメージ)
def helper_method(*methods)
@helper_methods ||= []
@helper_methods.concat(methods.map(&:to_s))
@helper_methods.freeze
end
# 変更後(イメージ)
def helper_method(*methods)
@helper_methods ||= [].freeze
new_helpers = @helper_methods.dup
new_helpers.concat(methods.map(&:to_s))
new_helpers.freeze
@helper_methods = new_helpers
endポイント:
- 既存の配列に対して
<<やconcatなどで直接変更するのではなくdupでコピー- コピー側を変更
- 変更後に
freeze - 元のインスタンス変数へ「新しい配列」を再代入
- frozen な配列を共有しつつ、変更が必要なときは「新インスタンスを作ってすげ替える」ことで、Ractor 間で共有されるオブジェクトに対して破壊的変更が起きないようにしている
- 実装パターンは #57743(別 PR)と揃えられており、helpers 周りの Ractor 対応を一貫したスタイルで進めている
テストの追加
actionpack/test/controller/helper_test.rb にテストが11行追加されています。内容としては:
helper_methodを複数回呼んだ場合でも、内部のヘルパーリストが正しく更新されること- Ractor 対応を意識した、破壊的変更が行われないこと(配列オブジェクトが作り直されていること)
などを確認するテストであると考えられます。
(PR本文にコードはありませんが、一般的には assert_not_same old_helpers, new_helpers や、assert_equal %w[foo bar], helpers のようなテストになるはずです。)
- 影響範囲・注意点
影響範囲
- 対象:
AbstractController::Helper.helper_methodを通じて登録されるヘルパーメソッドの内部表現 - 利用側(コントローラで
helper_method :fooを呼んでいるアプリケーションコード)の API 使用方法や挙動は変わりません。 - 変更は actionpack 内部の配列操作方法のみであり、公開インターフェース上の互換性は保たれています。
Ractor / 並行実行時の影響
- Ractor 間で共有されるクラスやモジュール、あるいはそれに紐づくクラスインスタンス変数が「mutable なまま共有される」ことが Ractor 非対応の主な原因になります。
- この PR により、
helper_methodが使う内部配列は:- 不変(frozen)なインスタンスを共有
- 更新時のみ新インスタンスに差し替え という方式になるため、Ractor から見ても安全に扱えるようになります。
- Ractor を使わない通常のマルチスレッド環境でも、破壊的変更を避けることで、ある程度のスレッドセーフティ向上(あるいはバグ発生の可能性低減)が期待できます。
パフォーマンス上の注意
- 変更のたびに配列を
dup→freezeして再代入するため、理論上は:- ヘルパー定義回数が極端に多い場合
- アプリケーション起動時に大量の
helper_method呼び出しがある場合 には、若干のオーバーヘッドが生じます。
- ただし
helper_methodは通常「クラス定義時に数回呼ぶものであり、頻繁に実行されるリクエスト処理中はほぼ呼ばれない」ため、実用上のコストはほとんど無視できるレベルと考えられます。
- 参考情報 (あれば)
- 関連PR:
- https://github.com/rails/rails/pull/57743
同様の「dup → 追加 → freeze → 再代入」パターンを導入した PR。今回の PR はこれに合わせてhelper_method実装を統一し、Ractor セーフティを高めています。
- https://github.com/rails/rails/pull/57743
- Ruby Ractor 仕様(英語):
- https://docs.ruby-lang.org/en/master/doc/ractor_md.html
共有オブジェクトに対する不可変性の要求や、コピー戦略について理解すると、この PR の意図がよりクリアになります。
- https://docs.ruby-lang.org/en/master/doc/ractor_md.html
#57760 Simplify PRE_CONTENT_STRING hash
マージ日: 2026/6/17 | 作成者: @skipkayhil
- 概要 (1-2文で)
Rails のActionView::Helpers::TagHelper内で使われているPRE_CONTENT_STRINGハッシュの定義が、Ruby 3.1 以降の最適化を踏まえて簡素化されました。nil用のデフォルト値を持たせることでオブジェクト生成を減らしていたロジックが、もはや効果を持たないため削除されています。
- 変更内容の詳細
背景
- 以前の実装では、
PRE_CONTENT_STRINGというハッシュに デフォルト値 を設定し、"#{nil}"のような文字列補間で余分なStringオブジェクトが生成されるのを避けていました。 - ところが Ruby 3.1 以降では、
nilを文字列補間した場合に 新しいStringをアロケートしない 最適化が入っており、この最適化により「デフォルト値で節約する」効果がなくなっています。
PR の説明にある IRB 実験結果:
def allocations
x = GC.stat(:total_allocated_objects)
yield
GC.stat(:total_allocated_objects) - x
end
a = "".freeze
allocations { "#{a}" } # => 2
allocations { "#{a}" } # => 1
allocations { "#{nil}" } # => 1
allocations { "#{nil}" } # => 1#{nil}でのアロケーション数はすでに最小限になっており、以前のように「nil専用のデフォルト値を持つハッシュ」を用意する意味がなくなっていることが分かります。
実際の変更点 (概念的なイメージ)
元のイメージ(擬似コード):
PRE_CONTENT_STRING = Hash.new { |h, k|
# k が nil のときに割り当てを減らすための特別扱い
some_default_string_for_nil
}変更後のイメージ:
PRE_CONTENT_STRING = {
# 必要なキーだけを明示的に定義
}
# デフォルト値定義は削除つまり、「nil のときはこの値を使う」というような Hash#default_proc ベースの最適化ロジックを削除し、単純なハッシュ定義だけにした という変更です。
- 影響範囲・注意点
対象バージョン
Ruby 3.1 以降を前提にした最適化の整理です。Rails のサポートする Ruby バージョンが 3.1+ となっている前提であれば、振る舞い・パフォーマンスともに実質的な差はほぼありません。機能的影響
ActionView::Helpers::TagHelperを利用しているヘルパー(content_tag, 各種タグ生成ヘルパー等)の 出力内容は変わりません。- 振る舞い(HTML の生成ロジック)に変更はなく、内部実装の簡素化のみ です。
パフォーマンスへの影響
- 本来、
PRE_CONTENT_STRINGのデフォルト値は「余計な String アロケーションを減らす」目的でしたが、Ruby 本体側の最適化により、もはや有効でなくなっていました。 - この変更により、
Hash#default_proc呼び出しを避けることで、かえって実行パスが単純になり、わずかに有利になる可能性すらあります。 - 少なくとも、説明にある通り「アロケーション削減」という観点でのデメリットはありません。
- 本来、
互換性の注意点
PRE_CONTENT_STRINGは内部実装用の定数であり、通常のアプリケーションコードが参照する想定ではありません。- もしアプリや gem がこの内部定数やそのデフォルト値の挙動に依存していた場合は、挙動が変わる可能性がありますが、そのような依存は非推奨でありレアケースです。
- 参考情報 (あれば)
この PR のフォローアップ元:
PRE_CONTENT_STRINGにデフォルト値が追加された当初のコミット:Ruby 3.1 での
nil補間の最適化に関するコミット:
この PR は、Ruby 本体の進化に合わせて Rails 側の「歴史的なパフォーマンスワークアラウンド」を撤去し、コードをシンプルに保つためのリファクタリングと位置付けられます。
#57749 Fix instance_variable_names RDoc to show declaration order
マージ日: 2026/6/17 | 作成者: @55728
- 概要 (1-2文で)
Object#instance_variable_namesの RDoc 例が、実際の返り値の順序と逆になっていたため、ドキュメント上の配列の順序を実際の挙動に合わせて修正した PR です。コードの挙動変更はなく、ドキュメントのみの修正です。
- 変更内容の詳細
何が問題だったか
instance_variable_names はインスタンス変数名の配列を返しますが、RDoc のサンプルは次のように「逆順」で書かれていました。
class C
def initialize(x, y)
@x, @y = x, y
end
end
C.new(0, 1).instance_variable_names
# 実際の挙動: ["@x", "@y"]
# RDoc 例: ["@y", "@x"] ← ここが誤りRuby 1.8 時代はインスタンス変数の順序が保証されていなかったため、例として逆順が使われていた経緯がありますが、Ruby 1.9 以降は「定義順(代入された順)」で返される仕様になっています。この仕様に対し、ドキュメントだけが古いまま取り残されていました。
なお、同じファイル内の instance_values の例はすでに {"x" => 0, "y" => 1} と正しい順序を示しており、Rails ガイド (guides/source/active_support_core_extensions.md) も正しい順序 ["@x", "@y"] を示していたため、この RDoc だけが不整合な状態でした。
実際の変更内容
変更は 1 行のみで、RDoc の出力例を修正しています。
- C.new(0, 1).instance_variable_names # => ["@y", "@x"]
+ C.new(0, 1).instance_variable_names # => ["@x", "@y"]挙動確認として、次のワンライナーで実際の出力が ["@x", "@y"] になることも検証されています。
ruby -Ilib -e 'require "active_support/core_ext/object/instance_variables"; \
class C; def initialize(x,y); @x,@y=x,y; end; end; \
p C.new(0,1).instance_variable_names'
# => ["@x", "@y"]- 影響範囲・注意点
ランタイム挙動への影響は一切なし
Object#instance_variable_namesやinstance_variablesの仕様・実装には変更なし。[ci skip]が付いているとおり、完全にドキュメント専用の修正です。
ドキュメントの信頼性向上
- Rails / Active Support のドキュメントを参考にして
instance_variable_namesの結果順序を前提にするコードやテストを書く際に、誤解を招きにくくなります。 - 既に Ruby 本体および Rails ガイドでは「定義順」であることを前提としており、それと整合するようになりました。
- Rails / Active Support のドキュメントを参考にして
順序仕様についての注意
- 現行の Ruby (1.9 以降) ではインスタンス変数リストは「定義順」で返されるのが実際の挙動ですが、歴史的経緯から古い記事やコード断片では「順序は未定義」と書かれている場合があります。
- Rails のこの修正により、「Rails の RDocだけ逆順」という状態は解消されました。
- 参考情報 (あれば)
- Ruby の
Object#instance_variables/Object#instance_variable_names挙動- Ruby 1.9 以降、内部的なテーブルが順序保持構造になったため、「代入された順」に出力される。
- 関連ドキュメント
activesupport/lib/active_support/core_ext/object/instance_variables.rb内の RDoc- Rails ガイド:
guides/source/active_support_core_extensions.md(instance_variables/instance_values解説箇所)
#57748 Honor falsy values in routing's deprecated positional-hash options
マージ日: 2026/6/17 | 作成者: @55728
- 概要 (1-2文で)
Rails のルーティング DSL において、非推奨の「位置引数+ハッシュ」形式でformat: false/anchor: falseを指定した場合に値が無視されていた問題を修正し、キーワード引数形式と同じ挙動になるようにしています。これにより、移行期間中にハッシュ形式からキーワード形式へ書き換えた際にルート挙動が変わってしまう不具合を防ぎます。
- 変更内容の詳細
問題の挙動
ルーティング DSL には以下 2 種類の書き方があります。
# 1. 推奨: キーワード引数形式
get "/*path", to: "pages#show", format: false
# 2. 非推奨: 位置引数+ハッシュ形式
get "/*path", { to: "pages#show", format: false }期待されるのは、両者が等価になることですが、実際には以下の差がありました。
# キーワード形式 — OK: format セグメントを無効化
get "/*path", to: "pages#show", format: false
#=> 生成されるパス: "/*path"
# 非推奨ハッシュ形式 — NG: format: false が無視される
get "/*path", { to: "pages#show", format: false }
#=> 生成されるパス: "/*path(.:format)"つまり、format: false (および anchor: false) のような「false を意味ある値として渡したいオプション」が、非推奨ハッシュ形式経由だと silently にドロップされていました。
さらに、ドロップされたにもかかわらず deprecation warning も出ないため、開発者は挙動差に気づきにくい状態でした。
原因となっていたコード
問題は、非推奨オプションを処理するヘルパーメソッド assign_deprecated_option の実装にありました。
def assign_deprecated_option(deprecated_options, key, method_name)
if (deprecated_value = deprecated_options.delete(key))
ActionDispatch.deprecator.warn(...)
deprecated_value
end
endif (deprecated_value = ...) という「truthy チェック」を使っていたため、
deprecated_options[key]がfalseの場合deprecated_valueにはfalseが入る- しかし
if falseは評価されない - そのまま
nilを返し、警告も出ず、値も利用されない
一方で、format や anchor の呼び出し側では、すでに外側で if deprecated_options.key?(key) をチェックしており、「キーが存在すること」は判定済みでした。
本来であれば、その情報を使って「false も含めて値をそのまま返すべき」という状況だったのに、内部で再度 truthy 判定をしてしまったのがバグの原因です。
修正内容
ヘルパーを「キーの存在チェック+値の削除・返却」という形に変えました。
def assign_deprecated_option(deprecated_options, key, method_name)
if deprecated_options.key?(key)
ActionDispatch.deprecator.warn(...)
deprecated_options.delete(key)
end
endポイント:
deprecated_options.key?(key)でキーの有無だけを見ているため、値がfalseでもブロックに入る- その後
deprecated_options.delete(key)の戻り値をそのまま返すので、falseも正しく呼び出し側へ渡る - キーが存在しない場合は
nilを返し、既存の||=呼び出しパターンとも矛盾しない
テスト追加
actionpack/test/dispatch/mapper_test.rb に以下のようなテストが追加されています(概略):
def test_deprecated_hash_honors_format_false
draw do
get "/*path", { to: "pages#show", format: false }
end
assert_equal "/*path", app.url_for(controller: "pages", action: "show", path: "example")
end既存の「キーワード形式での format: false」をテストする test_map_wildcard_with_format_false と対になるテストで、両形式が同じ動きをすることを保証しています。
- 影響範囲・注意点
- 影響を受けるのは、ルーティング DSL で非推奨の「位置引数+ハッシュ形式」を使っており、かつ
format: falseまたはanchor: falseを指定しているコードです。 - これまでは
- 「本当はフォーマットセグメントを無効化したかった (
format: false)」 - しかし実際には
(/*.:format)が付いたルートになっていた という状態だったものが、この修正以降は 意図通りに format セグメントが無効化されます。
- 「本当はフォーマットセグメントを無効化したかった (
- 同時に、当該オプションに対して deprecation warning が出るようになります。
- 「今まで warning が出ていなかったのに、急に出始めた」場合、この修正によって本来出るべき警告が正しく動くようになったと考えられます。
- ルート定義の挙動が「より正しくなる」修正であり、かつ非推奨 API に対するものなので、基本的にはプラス方向の互換性修正ですが、
- これまで誤った挙動に依存していた場合(例: 実は
(/*.:format)付きであることを前提にしていた)にはルート定義が変わる可能性があるため、該当パターンを使っているプロジェクトではルーティングのテストを一度確認しておくと安全です。
- これまで誤った挙動に依存していた場合(例: 実は
- なお、この修正により ハッシュ形式とキーワード形式が同じ挙動を取るようになるため、「ハッシュからキーワードへの移行でルートが変わってしまう」問題が解消されます。
- 参考情報 (あれば)
- 本修正は、ルーティング DSL のキーワード引数移行を導入したコミット
3b4255e180("Use keywords in routing mapper", v8.1.0)で入り込んだバグに対する修正です。 - 以前にマージされた、
scopeの:exceptやnamespaceの:pathなど「非 boolean オプション」に対する deprecated-hash 修正と同系列で、今回は:format/:anchorという boolean オプション向けの修正です。 - 変更ファイル:
actionpack/lib/action_dispatch/routing/mapper.rb(assign_deprecated_optionのロジック修正)actionpack/test/dispatch/mapper_test.rb(format: falseに関するテスト追加)
#57758 Fix in_order_of raising on CTEs
マージ日: 2026/6/17 | 作成者: @viralpraxis
- 概要 (1-2文で)
in_order_ofを CTE 由来のカラムに対して使うとNoMethodErrorになるバグを修正し、解決できないカラム(CTE など)については型キャスト処理をスキップするようにした PR です。これにより、CTE 内で定義したエイリアスカラムでもin_order_ofが安全に使えるようになります。
- 変更内容の詳細(あればサンプルコードも含めて)
問題の背景
Relation#in_order_of(column_name, values) は、指定カラムの値順にレコードを並べ替えるためのメソッドで、内部で以下のような処理を行っています。
order_column(column_name)で、与えられたカラム名からArelのカラムオブジェクトを取得- そのカラムの
type_casterを用いて、valuesを適切な型(整数・日時など)にキャスト - そのキャスト済み値を使って ORDER BY の CASE 式などを生成
ところが、CTE 由来のカラムや SQL リテラル(例: Arel.sql("format AS book_format"))に対して order_column("book_format") を呼ぶと、ActiveRecord はそれを通常のテーブルカラムとして解決できず、Arel::Nodes::SqlLiteral を返します。
Arel::Nodes::SqlLiteral は ActiveRecord のカラムオブジェクトではないため type_caster メソッドを持たず、in_order_of 内で order_column.type_caster を呼んだタイミングで次のエラーが発生していました。
NoMethodError: undefined method 'type_caster' for an instance of Arel::Nodes::SqlLiteralまた、CTE 由来のカラムについては、そもそも ActiveRecord 側に型情報が存在しないため、適切な型キャストを行うこともできません。
この PR の対応
activerecord/lib/active_record/relation/query_methods.rb において、in_order_of 内で以下のような条件分岐が追加されています(概念的なイメージ):
order_column = arel_attribute(column_name) # 実際には既存の取得処理
if order_column.respond_to?(:type_caster)
# これまで通り type_caster を使って values をキャスト
casted_values = values.map { |v| order_column.type_caster.type_cast_for_database(v) }
else
# カラム解決ができない(CTE など)場合は型キャストをスキップ
casted_values = values
end
# casted_values を使って in_order_of 用の ORDER BY 条件を構築ポイント:
order_columnがtype_casterを持たない場合(SqlLiteralなど)、型キャストを行わずにそのままvaluesを使う。- CTE からのカラムは ActiveRecord のスキーマに存在しないため、型情報を持たないことを前提とした扱いに切り替えている。
追加されたテスト
activerecord/test/cases/relation/field_ordered_values_test.rb に、MRE とほぼ同様のケースをカバーするテストが追加されています。
テスト内容の概要:
cte = Book.select(:id, Arel.sql("format AS book_format"))
order = %w[hardcover paperback ebook]
books = Book
.with(cte_books: cte)
.from("cte_books")
.select(Arel.sql("id, book_format"))
.in_order_of("book_format", order)
assert_equal order, books.map { |b| b["book_format"] }このテストにより:
- CTE (
WITH cte_books AS (...)) でformat AS book_formatと定義 from("cte_books")+select("id, book_format")によって CTE の結果を参照in_order_of("book_format", order)がエラーにならず、- 期待通り
order通りに並ぶこと
が保証されます。
- 影響範囲・注意点
主な影響範囲
- ActiveRecord の
Relation#in_order_ofを使用するすべてのコードに影響しますが、挙動が変わるのは「カラム解決ができない場合」だけです。 - 通常のテーブルカラム(スキーマ上に存在するカラム)に対しては、従来通り型キャストが行われ、互換性は保たれます。
- ActiveRecord の
CTE や SQL リテラルに対する挙動
- CTE 由来のカラムや
Arel.sqlを使った SQL リテラルカラムに対してもin_order_ofが使えるようになります。 - ただし、これらには ActiveRecord 側の型情報がないため、「値は渡したままの String 等として扱われる」前提になります。
- もし CTE 内で数値や日時を返していても、ActiveRecord はそれを「DB の生値」として扱い、
in_order_of内での追加の型変換は行いません。
- CTE 由来のカラムや
注意点
- CTE 内で返す型と、
in_order_ofに渡すvaluesの表現は、自分で整合性を取る必要があります。- 例: CTE で
SELECT ... AS book_formatがvarcharなら、in_order_of("book_format", ["a", "b", "c"])のように文字列で渡す。
- 例: CTE で
- 既存コードで「解決できないカラム名を誤って指定していた場合」、以前はエラーで気付けていたケースが、今回は「型キャストスキップでそのまま動く」場合があります。
- ただし、そもそも無効なカラム名は SQL レベルでエラーになることが多く、この変更でサイレントに誤動作するリスクは限定的です。
- CTE 内で返す型と、
- 参考情報 (あれば)
- 関連 PR: https://github.com/rails/rails/pull/44746
in_order_of周りの改善・バグ修正の文脈にある PR として参照されています。 - 再現用スクリプト(MRE)は PR 説明に記載されており、edge Rails + sqlite3 環境で
in_order_ofと CTE の組み合わせを手元で確認できます。
#57610 Fix visibility of ActiveRecord::Coders [ci skip]
マージ日: 2026/6/17 | 作成者: @skipkayhil
- 概要 (1-2文で)
ActiveRecord::Coders 周りの RDoc/YARD 用コメントを整理し、「どのクラスが公式にドキュメント対象か」を明確化した PRです。SafeCoderは実装詳細として非公開扱いにし、代わりにユーザが直接使う Coders クラス群をドキュメント対象として扱うようにしています。
- 変更内容の詳細
※コードの実装ロジック自体は変わっておらず、「可視性」は Ruby の可視性 (public, private) ではなく「ドキュメント生成時に公開されるかどうか」を指しています。
主なポイント:
ActiveRecord::Codersモジュールとその内部クラスの「ドキュメント可視性」の整理- これまで:
ActiveRecord::Coders自体はドキュメント対象として扱われていた- しかし、その内部の一部クラスは
#:nodoc:が付いていて、RDoc/YARD では非公開扱いになっていた
- これによって、「Coders という名前は見えるが、中に何があるか分かりにくい」という状態になっていた
- これまで:
方針転換(inverting it):
- モジュール本体 (
ActiveRecord::Coders) ではなく、「実際に使われる個々のコーダークラス」をドキュメント対象にするようにコメントを変更 - 具体的には、以下のようなクラスが「ユーザが知っていてよい・使い得る」ものとして扱われる方向に揃えられている:
ActiveRecord::Coders::YAMLColumnActiveRecord::Coders::JSONActiveRecord::Coders::ColumnSerializerなど
- 一方で、
SafeCoderは「実装詳細 (implementation detail)」と位置づけられ、#:nodoc:で隠される
- モジュール本体 (
ファイルごとの変更概要(いずれもコメント行の変更のみ)
activerecord/lib/active_record.rbActiveRecord::Codersの autoload やコメントの付け方を微調整して、「ここがドキュメントの入り口ではない」形に揃えている
activerecord/lib/active_record/coders/column_serializer.rbactiverecord/lib/active_record/coders/json.rbactiverecord/lib/active_record/coders/yaml_column.rb- 上記 3 ファイルで、
#:nodoc:を付けたり外したりして、「公開したいクラス」「隠したいクラス」を整理 - 実コード (クラス定義・メソッド定義) は変更なしで、行差分はすべてコメント (
#:nodoc:の追加/削除) によるもの
- 上記 3 ファイルで、
サンプルイメージ(実際のコードとは多少異なる可能性がありますが、ニュアンスとして):
module ActiveRecord
module Coders # ここは nodoc に近い扱いに
# class SafeCoder #:nodoc: # 実装詳細として隠す
# end
# こちらは利用可能な API としてドキュメントに出す
class YAMLColumn
# ...
end
end
end- 影響範囲・注意点
ランタイム動作への影響
- コードの実行パスや挙動は一切変わっていません。
- 既存アプリの動作、マイグレーション、シリアライズの挙動には影響なし。
API の公開/非公開の意味づけ
- 公開対象:
ActiveRecord::Coders::YAMLColumn,JSON,ColumnSerializerなど、これまでも実質的に利用されてきたクラス群が、公式ドキュメントにも明示的に出るような整理になります。- 外部コードからこれらを利用することは、より「サポートされている」雰囲気になります。
- 非公開対象:
SafeCoderは「内部実装」と明示された形になるため、直接参照・利用している場合は、将来の互換性が保証されない内部 API 依存という扱いになります。- ただし今回の PR 自体では削除やシグネチャ変更はないため、直ちに壊れることはありません。
- 公開対象:
テスト・型定義などへの影響
- RDoc/YARD ベースのドキュメント生成、IDE の補完、外部ツールによる API リスト生成などに影響が出ます。
SafeCoderを前提にした自動ドキュメントや API チェックツールを使っている場合、「非公開 API 扱い」として検出されることがあります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57610
- 関連コンテキスト:
- Rails コアでは、「内部実装だけれど外からも到達可能」な定数・クラスを
#:nodoc:で隠し、ドキュメントに出てくるクラスを「サポート対象 API」とみなす慣習があります。 SafeCoderは今回その「内部実装」側として整理されたため、今後の Rails バージョンで挙動変更や削除が行われても、公式には非互換変更として扱われない可能性があります。
- Rails コアでは、「内部実装だけれど外からも到達可能」な定数・クラスを
#57727 Accept a Float wait in retry_on
マージ日: 2026/6/16 | 作成者: @55728
- 概要 (1-2文で)
ActiveJobのretry_onオプションでwait: 2.5のような Float を指定すると例外が発生していた不具合を修正し、set(wait: 2.5)と同様に Float も受け付けるようにしました。内部的には Float を整数秒に切り捨てて扱うため、依然としてサブ秒単位のリトライは行われません。
- 変更内容の詳細
何が問題だったか
retry_on のドキュメントでは wait オプションは「秒数(デフォルト3秒)」と説明されており、以下はいずれも動く想定でした:
# OK
MyJob.retry_on SomeError, wait: 3, attempts: 3
MyJob.set(wait: 2.5).perform_laterしかし、retry_on に Float を直接渡すと internal error になっていました:
class FloatWaitJob < ActiveJob::Base
retry_on SomeError, wait: 2.5, attempts: 3
def perform
raise SomeError
end
end
FloatWaitJob.perform_now
# => RuntimeError: Couldn't determine a delay based on 2.5
# (元の SomeError は握りつぶされる)原因は遅延秒数を決定する determine_delay 内の case 式が ActiveSupport::Duration, Integer しかマッチしておらず、裸の Float が else に落ちて例外を投げていたことです。
case seconds_or_duration_or_algorithm
when ActiveSupport::Duration, Integer
delay = seconds_or_duration_or_algorithm.to_i
...
else
raise "Couldn't determine a delay based on #{seconds_or_duration_or_algorithm.inspect}"
endしかもこれは rescue_from(= retry_on の実装)内で起きるため、アプリケーション側の本来の例外を置き換えてしまい、ジョブも再キューされませんでした。
何を直したか
Integer ではなく Numeric をマッチさせるように変更し、Float も整数と同様に扱うようにしています:
when ActiveSupport::Duration, Numeric
delay = seconds_or_duration_or_algorithm.to_iポイント:
to_iはそのままなので、Float は「小数点以下切り捨ての秒数」として扱われます- 例:
wait: 2.5→2秒 - これは
2.5.secondsが内部的に同じくto_iで 2 秒になる挙動と一致
- 例:
- したがって今回の変更は「Float を受け付けるようにする」だけで、遅延の粒度(秒単位)は変えていません
- 本格的なサブ秒遅延(ミリ秒単位など)をサポートするかどうかは、アダプタの対応状況などに依存するため、別の検討事項という位置づけです
テストの追加
以下のようなテストジョブが追加されています:
class RetryJob < ActiveJob::Base
# 既存の retry ジョブに FloatWaitError を追加
retry_on FloatWaitError, wait: 2.5, attempts: 3
endテストケースでは:
FloatWaitErrorで失敗したときに、ジョブが例外を吐いて終わらず「リトライとして再スケジュールされる」こと- スケジュールされる遅延時間が期待通り(= 切り捨てられた秒数)であること
を確認しています。既存の「long wait job」のテストと同じ構成で、Float 版を追加した形です。
- 影響範囲・注意点
- 影響を受けるのは
ActiveJobのretry_onをwait: <Float>付きで使うケースです- これまで: internal error でジョブがリトライされず、元の例外も隠れてしまう
- これから: 内部エラーは発生せず、
wait.to_i秒後にリトライされる
- すでに
set(wait: 2.5)などを使っていたアプリにとっては、retry_on wait: 2.5も同様に動くようになり、一貫性が増します wait: 2.9などと書いても「2秒後」になる点に注意してください
サブ秒精度が必要な場合、現時点ではこの PR はその需要を満たしません(仕様を変えていないため)waitに他のNumeric(Rationalなど)を渡した場合もto_iで整数にされますが、実務上はほぼ Float/Integer 想定です
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57727
- 関連 API:
ActiveJob::Base.retry_onActiveJob::Base#set(wait: ...)ActiveSupport::Duration(3.secondsなど)
#57759 Make test_assign_ids_with_cpk_for_two_models order-independent
マージ日: 2026/6/17 | 作成者: @yahonda
- 概要 (1-2文で)
TestDefaultAutosaveAssociationOnAHasManyAssociation#test_assign_ids_with_cpk_for_two_modelsが PostgreSQL で順序依存のためにフレークしていた問題を、期待値と実際値の双方を sort して比較することで順序非依存なテストに修正した PR です。has_many :booksにorder:指定がないためbook_idsの返却順が不定である点をテスト側で吸収しています。
- 変更内容の詳細
問題となっていたテストは以下です(概略):
- 対象:
Cpk::Orderモデルのhas_many :books(複合主キーを持つ関連) - テスト:
test_assign_ids_with_cpk_for_two_models - 内容:
order.book_ids = [...]として ID をアサインし、その後order.book_idsが期待通りかどうかを検証
元のコードでは、book_ids を配列の順序まで含めて完全一致で比較していました:
# 変更前(イメージ)
assert_equal expected_book_ids, order.book_idsしかし、Cpk::Order の has_many :books には order: オプションが無いため、内部で発行される SQL に ORDER BY 句が付きません。
その結果、PostgreSQL では SELECT の返却順が保証されず、以下のように同じ要素セットでも順序だけが入れ替わってテストが失敗することがありました。
-[[550090038, 342015473], [550090038, 385957113]]
+[[550090038, 385957113], [550090038, 342015473]]この PR では、この比較を「順序非依存」に変更しています。具体的には、両方の配列をソートしてから assert_equal で比較するようにしています:
# 変更後(イメージ)
assert_equal expected_book_ids.sort, order.book_ids.sortまた、このテスト内ではすでに以下のように
assert_includesで期待する ID が含まれていることassert_equal 2, order.books.lengthで件数が正しいこと
といった検証も行っており、「同じ ID の集合が round-trip されているか」は十分に確認できている状態です。
そのため、順序に意味がない状況で順序までテストする必然性はなく、集合として一致していることだけを見ればよい、という判断で order-independent な比較に変更しています。
- 影響範囲・注意点
- 本 PR は テストコードのみ (
activerecord/test/cases/autosave_association_test.rbの 1 行差し替え) の変更であり、フレームワーク本体の挙動や公開 API には影響しません。 - PostgreSQL におけるフレークテストを解消するのが目的であり、MySQL / SQLite など他 DB への負の影響もありません。
- 本件は「ORDER BY を指定していない関連の結果順は保証されない」という一般的な注意点を再確認させる内容であり、アプリケーション側でも順序に意味を持たせたい場合は
has_many ... , -> { order(...) }やorder(:column)を明示すべきという教訓になります。
- 参考情報 (あれば)
- 該当テスト:
TestDefaultAutosaveAssociationOnAHasManyAssociation#test_assign_ids_with_cpk_for_two_models - 関連ファイル:
activerecord/test/cases/autosave_association_test.rb - Buildkite の失敗ログ: https://buildkite.com/rails/rails/builds/129895#019ed1d0-262a-462b-9478-28d2108d4c76
(PostgreSQL 3.4, seed 16372 での flakiness 事例)
#57755 Make ActionController::Flash.add_flash_types ractor safe
マージ日: 2026/6/16 | 作成者: @andrewn617
- 概要 (1–2文で)
ActionController::Flash.add_flash_typesが Ractor(Ruby の並行実行モデル)下でも安全に動作するように内部実装が変更されました。これにより、複数 Ractor からカスタム flash タイプを追加してもデータ競合が起きないようになります。
- 変更内容の詳細
背景
ActionController::Flash.add_flash_types は以下のように、任意のシンボルを受け取って flash[:notice] 以外のカスタム flash タイプを定義するためのメソッドです。
class ApplicationController < ActionController::Base
add_flash_types :info, :error
end
# その後の使用例
redirect_to root_path, info: "情報メッセージ"
redirect_to root_path, error: "エラーメッセージ"従来、このメソッドの内部では、クラスレベルの配列(例: @flash_types あるいは FLASH_TYPES のようなもの)に対して直接 << などで追加する実装になっており、
- その配列オブジェクトを複数の Ractor で共有したまま破壊的に変更する
という点で、Ractor セーフではありませんでした。
今回の修正内容
PR の説明文にある通り、次のようなパターンに変更されています。
「新しい flash type を追加するときに、dup(複製)、追加、freeze、再代入を行う」
擬似コードで表すと、以前は以下のようなイメージだったものが:
# 旧イメージ(Ractor unsafe なパターン)
def self.add_flash_types(*types)
types.each do |type|
flash_types << type # 共有配列に破壊的 push
end
end今回の PR で、Ractor セーフなイミュータブル指向のパターンに変更されています:
# 新イメージ(Ractor safe なパターン)
def self.add_flash_types(*types)
types.each do |type|
new_array = flash_types.dup # 既存配列を複製
new_array << type # 複製側を変更
new_array.freeze # イミュータブルに
self.flash_types = new_array # 参照を差し替え
end
endポイントは:
- 共有されている配列オブジェクト自体を破壊的に変更しない
- 常に「新しい frozen 配列オブジェクト」を生成して、それをクラス変数/クラスインスタンス変数に再代入する
- 他の Ractor は「古い frozen 配列」を参照し続けても問題なく、新旧どちらの配列も不変であるため競合しない
という Ractor フレンドリーなパターン(同様のパターンが #57743 でも使われている)に合わせた修正です。
テストの追加
actionpack/test/controller/flash_test.rb に 7 行追加されており、主に以下を確認していると考えられます(推測を含みますが、変更行数的に近い内容になるはずです):
add_flash_typesで追加した flash タイプが従来どおり使えること- 追加済み flash タイプの情報が frozen な配列として保持されていること
- 複数回
add_flash_typesを呼んでも問題なくマージされること
- 影響範囲・注意点
アプリケーションコードの書き方は基本的にそのままでOK
add_flash_types :info, :warningなど、これまで通りの使い方に変更はなく、既存コードの互換性は保たれています。
Ractor を利用する Rails アプリでの安全性向上
- Ruby 3 以降で Ractor を本格的に使う場合、クラスレベルのミューテーブルな状態を共有するのは危険でしたが、少なくとも
add_flash_types周りについては安全なスタイルに寄せられました。
- Ruby 3 以降で Ractor を本格的に使う場合、クラスレベルのミューテーブルな状態を共有するのは危険でしたが、少なくとも
パフォーマンスへの影響はごく小さい想定
add_flash_typesは通常アプリ起動時やクラス定義時に一度だけ実行されることが多く、リクエストごとのホットパスではないため、dup + freeze + 再代入によるオーバーヘッドは実用上ほぼ無視できます。
スレッドセーフにも寄与
- Ractor だけでなく、マルチスレッド環境でも「共有配列に対する破壊的変更」を避けることは安全性向上につながります。
- ただし、Ruby のスレッドモデル的に完全なロックフリー/競合ゼロを保証するものではなく、「典型的な競合パターンを避ける」方向の改善と捉えるのが妥当です。
- 参考情報 (あれば)
対応 PR: rails/rails#57743
同様の「dup → 変更 → freeze → 再代入」という Ractor セーフ化パターンが用いられている PR で、今回の変更はこれに倣ったものです。Ractor の基本方針(Ruby 本体側)
- 「共有オブジェクトは基本的に不変(frozen)であること」
- 「ミューテーブルなオブジェクトは Ractor 間で共有しないこと」
今回の修正は、Rails 側でこの原則に揃えるための一環といえます。
#57752 More shareable constants
マージ日: 2026/6/16 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails内部で使われている一部の定数(Hashなど)について、Ractor対応のために「共有可能(shareable)」なオブジェクトとして扱えるように凍結処理が追加・修正されたPRです。主にActionPack・ActionView・ActiveSupport内の定数の凍結方法を見直し、マルチRactor環境での安全性を高めています。
- 変更内容の詳細(あればサンプルコードも含めて)
※実際の差分は極小 (±1〜3行程度) ですが、説明文から読み取れる範囲で整理します。
背景
- Ruby 3 以降では並列実行用の仕組みとして Ractor が導入されています。
- Ractor 間でオブジェクトを共有するには、そのオブジェクトが「shareable(共有可能)」でなければならず、多くの場合
freeze済みで、かつ内部に可変要素(ミュータブルなオブジェクトやデフォルトProcなど)を持たない必要があります。 - Rails内部には、Hash定数や、
Hash.new { ... }のような「デフォルトProc付きHash」が定数として定義されている箇所があり、それらはそのままだとRactor間で共有できません。
このPRでは、そのうちいくつかを「Ractor.shareable_」もしくは freeze を用いて共有可能にしています。
具体的な変更対象
説明文にある通り、次のような性質の定数が対象です。
- 「値がミュータブルなHash定数」
- 「デフォルトProc (
Hash.new { ... }) を持つHash定数」
変更されたファイル:
actionpack/lib/action_controller/metal/allow_browser.rbブラウザ判定などに使う内部用定数のHashが
freeze/Ractor.shareable_によって共有可能にされたと考えられます。例(イメージ):
ruby# 変更前(イメージ) ALLOWED_BROWSERS = { chrome: /Chrome/, safari: /Safari/, } # 変更後(イメージ) ALLOWED_BROWSERS = Ractor.shareable!( { chrome: /Chrome/, safari: /Safari/, }.freeze )実際の定数名や中身は異なりますが、やっていることは「定数Hash自体とその中身を不変にする」ことです。
actionview/lib/action_view/helpers/tag_helper.rb- HTMLタグヘルパー内部で使うオプションマップや属性マップなどのHash定数が凍結されています。
- こちらも
Ractor.shareable_やfreezeによって、Ractor間で共有できる定数として扱えるようにしています。
activesupport/lib/active_support/log_subscriber.rbログ出力関係のクラスで使われている内部定数(イベントのマッピングやフォーマット指定などと思われる)をRactor-safeにする変更です。
差分が+3/-3行なので、おそらく以下のようなパターンの修正です:
ruby# 変更前(イメージ) EVENT_COLORS = { process_action: :green, redirect_to: :yellow, } # 変更後(イメージ) EVENT_COLORS = Ractor.shareable!( { process_action: :green, redirect_to: :yellow, }.freeze )
技術的ポイント
Ractor.shareable_は、オブジェクトを「共有可能」としてマークするための内部API(またはそのラッパ)であり、単純なfreezeよりも強い制約を課したり、検査を行うために用いられます。- 「デフォルトProc付きHash」はそれ自体が可変的であり、Ractor間共有には適しません。そのため:
- デフォルトProcの利用をやめて完全なテーブル化を行う,
- あるいはデフォルトProcなしで済むような形に変更し、結果を
freeze/Ractor.shareable_する、 のいずれかに寄せていると考えられます。
- 影響範囲・注意点
アプリケーションコードへの直接の破壊的変更はほぼ無し
- 変更対象は Rails の内部定数のみであり、公開APIの振る舞いを変える意図はありません。
- ただし、今まで「たまたま」内部定数を直接変更していたようなコード(例:
ActionView::Helpers::TagHelper::SOME_HASH[:foo] = :bar)は、凍結によりFrozenErrorを起こすようになります。 - そのようなパッチングはもともと非推奨のため、これを機にやめるべきです。
Ractorを使った並列実行との相性改善
- Rails内部でRactor-safeでない定数があると、Ractor起動時にエラーになったり、Ractor間でオブジェクトを送受信できない問題が出ます。
- このPRは、そのような問題の一部を解消し、将来的に「RailsをRactor前提で動かす」ための地盤を整える目的があります。
テストは追加されていない
- 変更内容が小さく、既存テストで十分カバーされる想定のため、新規テストは追加されていません。
- Ractorを実際に活用している環境では、Rails本体の更新後に自前のRactor関連テストが通るかどうかを確認しておくと安心です。
- 参考情報 (あれば)
- Ruby公式ドキュメント(Ractor / shareableについて)
Ractor.shareable?/Ractor.make_shareableといったAPIの挙動を把握しておくと、Rails内部が何を目指しているか理解しやすくなります。- Rails本体では、今後も「定数のfreeze」や「Ractor.shareable_」の追加が段階的に進むことが予想されるため、独自にRails内部定数を書き換えるメタプログラミングは徐々に難しく(危険に)なっていくと考えておくとよいです。
#57751 Make Messages::Metadata serializer arrays frozen by default.
マージ日: 2026/6/16 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails のActiveSupport::Messages::Metadataにおける envelope / timestamp のシリアライザ配列を、デフォルトで凍結して Ractor 共有可能にする変更です。これにより、Rails のグローバル状態を Ractor セーフに近づけつつ、既存のロードフックによる配列の拡張も維持しています。
- 変更内容の詳細
やりたいこと
- Rails 全体を Ractor セーフにする取り組みの一環として、
Messages::Metadataが内部で使っている「シリアライザの配列」を Ractor 共有可能(=基本的に不変 / frozen)にする。
- ただし、MessagePack ロード時のフックで配列を拡張しているため、単純に「定数を dup → 追加 → freeze → 再代入」というパターンが使えない状態だった。
- そこで、定数に直接配列を持つのをやめて、モジュールの属性(アクセサ)として保持する形に変更している。
具体的なコードレベルのイメージ
※実際のコードは 9 行 + / - 程度なので抽象化したイメージです。
従来(イメージ):
module ActiveSupport
module Messages
module Metadata
ENVELOPE_SERIALIZERS = [DefaultEnvelopeSerializer]
TIMESTAMP_SERIALIZERS = [DefaultTimestampSerializer]
# どこかの load hook で:
ENVELOPE_SERIALIZERS << MessagePackEnvelopeSerializer if defined?(MessagePack)
TIMESTAMP_SERIALIZERS << MessagePackTimestampSerializer if defined?(MessagePack)
end
end
endこの場合:
ENVELOPE_SERIALIZERS/TIMESTAMP_SERIALIZERSは定数だが、配列自体はミュータブルで、ロードフックで<<している。- Ractor 共有するには配列を freeze したいが、freeze すると
<<で落ちる。 - そのため、他の箇所では「
DUP → 変更 → freeze して再代入」パターンを採っているが、ここではロードフックの順序・タイミングの都合で同じやり方が取りにくい。
今回の変更(イメージ):
module ActiveSupport
module Messages
module Metadata
class << self
attr_accessor :envelope_serializers, :timestamp_serializers
end
self.envelope_serializers = [DefaultEnvelopeSerializer].freeze
self.timestamp_serializers = [DefaultTimestampSerializer].freeze
# どこかの load hook で:
self.envelope_serializers += [MessagePackEnvelopeSerializer] if defined?(MessagePack)
self.timestamp_serializers += [MessagePackTimestampSerializer] if defined?(MessagePack)
# `+=` は新しい配列を返すため、その都度新しい frozen or 非 frozen 配列に付け替えられる
end
end
endポイント:
- 「定数の配列を直接ミューテート」から、「モジュール属性(ゲッター/セッター)を通じて配列を持つ」形に変更。
- 初期値の配列は 生成時に freeze されるため、そのまま Ractor 共有可能。
- MessagePack ロードフックでは
- 直接
<<ではなく、+=やdup + << + freezeなど「新しい配列を生成してから再代入」する形に寄せられる。
- 直接
Messages::Metadata自体は no-doc(公開 API ドキュメントに載せない内部 API)なので、内部実装の変更として扱いやすい。
- 影響範囲・注意点
- 対象:
ActiveSupport::Messages::Metadataを直接利用しているコード。- 通常のアプリケーション開発ではほぼ内部実装として扱われるため、一般的なアプリでは影響はほぼないと考えられます。
- 注意点:
- もしアプリやライブラリ側で
ActiveSupport::Messages::Metadata::ENVELOPE_SERIALIZERS << ...ActiveSupport::Messages::Metadata::TIMESTAMP_SERIALIZERS << ...のように、配列を直接ミューテートしていた場合は動かなくなる可能性があります(今回、定数から属性ベースに変わっているため)。
- その場合は、今後の実装に合わせて
ActiveSupport::Messages::Metadata.envelope_serializers += [YourSerializer]のように「再代入を通じて拡張」するスタイルに寄せる必要が出てきます。
- もしアプリやライブラリ側で
- Ractor 利用時:
- メタデータのシリアライズ周りのグローバル状態が、より Ractor セーフになっています。
- ただし、MessagePack ロード後にさらにミューテートするようなカスタマイズをしている場合、そのコードが Ractor 共有の妨げになる可能性があります。
- 参考情報 (あれば)
- この PR は、Rails の「グローバル状態の Ractor セーフ化」を進める一連の PR の一部で、他の部分でも
- 配列 / ハッシュの「初期状態を frozen にする」
- 変更が必要な場合は「コピーしてから再代入する」 というパターンが適用されています。
Messages::Metadataは内部 API(no-doc)であり、安定した公開 API としては扱われていません。そのため、将来的にも同様の内部変更が入る可能性があります。
#57750 Avoid racing the maintenance thread's checkout
マージ日: 2026/6/16 | 作成者: @matthewd
- 概要 (1–2文で)
このPRは、Active Record のテスト (activerecord/test/cases/test_case_test.rb) におけるメンテナンス用スレッド(maintenance thread)の処理とテスト本体の処理が「競合条件(race condition)」を起こし、テストがまれに落ちる(flaky)問題を解消するための修正です。前のPR #57744 の修正が別種のフレークを生んでしまったため、そのフォローアップとしてテストの同期方法を調整しています。
- 変更内容の詳細
※実際の差分は1ファイル・数行のみですが、説明のために典型的な構造を簡略化して記述します(実際のコードとは若干異なる可能性がありますが、意図とメカニズムの解説が目的です)。
問題となっていた状況
- Active Record のテストケースの中で、「メンテナンススレッド」が接続プール/スレッドローカルなコネクション状態をチェックアウト/リリースするような仕組みをテストしている。
- そのテストでは、
- テスト本体スレッドで「ある状態」を作る
- バックグラウンドで動くメンテナンススレッドがそれを「検知・checkout する」ことを前提にアサーションする
- 前回の修正(#57744)では、フレークを抑えるために「メンテナンススレッドが動くタイミング」をなんらかのフラグ・待機処理で制御しようとしたが、その制御が不完全で、逆に以下のような別のレースが起きていた:
- テストが「もうメンテナンススレッドは checkout を終えたはず」と決め打ちして先に進むが、実際にはまだ checkout 中 or これから checkout、というタイミングずれでアサーションが不安定になる。
このPRでの修正ポイント
このPRは、その「タイミングの決め打ち」をやめ、明確な同期ポイントを設ける方向に調整しています。具体的には、以下のようなテストコードの修正になることが多いです:
「スレッドが処理を開始したかどうか」を確認するだけで先に進むのではなく、
- スレッドが「checkout を完了したこと」
- または「対象の状態を確実に観測したこと」
を示すシグナルを待つようにする。
典型的には以下のようなパターンへの書き換えが行われます(擬似例):
ruby# 変更前(例) maintenance_thread = Thread.new do # ... connect / checkout / cleanup ... end # テスト側で適当な sleep やゆるい条件で先に進んでしまう sleep 0.1 assert pool.checked_out? # 変更後(例) checkout_done = Queue.new maintenance_thread = Thread.new do # ... checkout ... checkout_done << :ok # checkout 完了を明示的に通知 # ... cleanup ... end # メンテナンススレッドが checkout 完了したことを確実に待つ assert_equal :ok, checkout_done.pop assert pool.checked_out?
今回の差分は +4/-3 程度なので、実際には以下のような小さな変更に収まっている可能性が高いです:
sleepの削除- スレッド完了 or 特定の状態変化を待つ
joinやQueue/ConditionVariableベースの待機の追加 - あるいは
assertの前に「メンテナンススレッドが checkout 可能な状態になるまでブロックする」ような同期コードを1行追加
いずれにせよ、「テストとメンテナンススレッド間の競合を避けるため、テストが確実にメンテナンススレッドの checkout 処理を待つようにした」ことが本質的な変更です。
- 影響範囲・注意点
影響範囲:
- 修正対象は
activerecord/test/cases/test_case_test.rbのみであり、本番コード(app/やactiverecord/lib/)には変更がありません。 - したがって、Rails を利用するアプリケーションのランタイム挙動には影響しません。
- 影響するのは Rails のテストスイート(特に CI 上での flakey test)だけです。
- 修正対象は
注意点:
- マルチスレッド/バックグラウンドスレッドを伴うテストを書く際、
- 「スレッドを起動した」こと
- 「スレッドが特定の状態に達した(checkout 済み・cleanup 済み等)」こと
を明確に区別して同期をとるべき、というよい実例になっています。
sleepベースや、「ある程度待てば終わっているだろう」という前提に立ったテストは、環境差・マシンスピード差で簡単に flake になるため、今回のような修正方針(シグナル・Queue・ConditionVariable 等で確実に待つ)が推奨されます。
- マルチスレッド/バックグラウンドスレッドを伴うテストを書く際、
- 参考情報 (あれば)
- 元となったPR: #57744
- そこで flakey test の初回修正が行われたが、タイミング制御が不十分で別のレースを誘発していた模様。
- ビルドログ(PR 説明にある link)
- https://buildkite.com/rails/rails/builds/129881#019ed09f-a039-44e2-a884-7157ff4f3793
- このビルドで新たなフレークが確認され、それを受けて本PRで再修正が行われた。
#57743 Fix ActiveSupport::InheritableOptions#to_h for nested inheritable options
マージ日: 2026/6/16 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveSupport::InheritableOptions#to_hが、親にInheritableOptionsを持つ(ネストした)ケースで正しく Hash を返さず、オブジェクトをそのまま返してしまう不具合を修正した PRです。親側をto_hしてからマージするように変更し、ネストされた継承オプションでも常に期待通りの Hash が得られるようになりました。
- 変更内容の詳細(あればサンプルコードも含めて)
不具合の背景
ActiveSupport::InheritableOptions は、親子関係を持つオプション(設定)を扱うためのクラスで、inheritable_copy などで親のオプションを継承した新しいインスタンスを作れる仕組みがあります。
問題となっていたケースは次のようなものです:
parent = ActiveSupport::InheritableOptions.new(foo: 1)
child = parent.inheritable_copy
# child は parent を親に持つ InheritableOptions インスタンス
hash = child.to_h
# 期待: { foo: 1 } のような Hash
# 実際: 内部の親オブジェクト (InheritableOptions オブジェクト) が
# そのまま混ざってしまうケースがあるつまり、「InheritableOptions の親に別の InheritableOptions を持つ」構造になっているときに to_h を呼ぶと、Hash ではなくオプションオブジェクト自体がマージされてしまう挙動がありました。
実際の修正内容
activesupport/lib/active_support/ordered_options.rb 内の InheritableOptions#to_h で、親オプションをマージする際に、親に対して必ず .to_h を呼ぶように 1 行変更されています。
擬似的には、以下のようなイメージです:
# 修正前 (イメージ)
def to_h
# parent が InheritableOptions のまま merge されてしまう
parent.merge(self)
end
# 修正後 (イメージ)
def to_h
# parent を先に Hash 化してからマージ
parent.to_h.merge(self)
endこれにより、親が InheritableOptions であっても、to_h の結果は常に「親のオプションを展開した Hash」に対して子のオプションをマージした Hash になります。
テストの追加
activesupport/test/ordered_options_test.rb に 9 行のテストが追加され、主に以下を検証しています:
InheritableOptionsが別のInheritableOptionsを親に持つときにto_hを呼ぶ- 返り値が期待どおりの Hash 構造になっていること
- 親子いずれの層にあるオプションも正しく展開されていること
- 影響範囲・注意点
InheritableOptionsを使っていて、ネストした/継承した設定オブジェクトをto_hで最終的な Hash に変換しているコードが、より直感的な挙動になります。- これまで「
to_hの結果に謎のオブジェクトが入る」「ネストが期待どおりに展開されない」といった症状があった場合、それが解消される可能性が高いです。
- これまで「
- 仕様としても「
to_hは Hash を返すべき」であり、今回の変更はその契約に沿うバグ修正なので、通常は後方互換性のある変更と考えて問題ありません。 - 逆に、「親の
InheritableOptionsオブジェクトそのものがto_h結果に入ってくる」という現在のバグに依存したコードがもしあれば、挙動が変わります。ただし、通常はそのような依存は想定されていないため、多くのアプリケーションには影響しないはずです。 - パフォーマンスへの影響は極小で、
to_hを呼ぶ場面で一段多くto_hが呼ばれるだけです。大量・高頻度にInheritableOptions#to_hを呼び出しているような特殊なケースでなければ、体感できる差はまずありません。
- 参考情報 (あれば)
- 対象クラス:
ActiveSupport::InheritableOptions(ActiveSupport::OrderedOptions派生) - この問題は、
inheritable_copyなどで「親を持つInheritableOptionsを多段に作成した場合」にのみ顕在化します。 - 実運用では、Rails の設定周り(config オブジェクトなど)で
InheritableOptionsが内部的に使われることがあるため、設定値を最終的に Hash として取り出して処理するようなコードを書いている場合に恩恵があります。
#57744 Wait for any asynchronous connection maintenance work before leak check
マージ日: 2026/6/16 | 作成者: @matthewd
- 概要 (1-2文で)
このPRは、Active Record のテストで「コネクションリークチェック」を行う前に、非同期で実行されるコネクションメンテナンス処理がすべて終わるまで待つようにし、テストの不安定さや誤検知を防ぐ変更です。非同期 executor が有効な環境でも、接続リーパー周りのテストが安定して実行できるようになります。
- 変更内容の詳細
背景
- Active Record の接続プールには「reaper」(リーパー)と呼ばれるメンテナンス処理があり、一定間隔でアイドルコネクションを整理したりします。
- 多くのテストは「非同期 executor 未設定」で動いているため、リーパーのメンテナンス処理はテストスレッド内で同期的(インライン)に
reaper_lockの保護下で実行されます。 - しかし、executor が存在する環境だと、メンテナンス処理の実体(チェックアウトなど)は executor に非同期ジョブとして投げられ、
reaper_lockは「ジョブをキューイングするところ」だけを守る状態になります。 - そのため、テスト終了時に「コネクションがリークしていないか」をチェックするタイミングで、まだバックグラウンドでメンテナンス用のチェックアウトが走っている可能性があり、誤って「リーク」と判定されたり、テストが不安定になる原因になります。
このPRの主な対策
「新たなメンテナンスジョブがキューイングされないように
reaper_lockをとった状態で、すでにキューにあるメンテナンス用チェックアウトがすべて終わるまで待ち、executor が静止(quiesce)するまで待つ」
という動きをテスト側に追加しています。
具体的な変更ポイント
変更ファイルはテストコードのみです:
activerecord/test/cases/test_case.rbActive Record テストケースの共通クラス (
ActiveRecord::TestCaseなど) に、コネクションリークチェック前に「非同期メンテナンスが完了するのを待つ」ためのロジックが追加されています。おおまかな流れとしては以下のイメージです(※擬似コード):
rubyclass ActiveRecord::TestCase < ActiveSupport::TestCase # teardown またはリークチェック前などに呼ばれるフック def check_for_leaked_connections # 1. 新たなメンテナンスジョブがスケジュールされないようにロック pool = ActiveRecord::Base.connection_pool pool.reaper_lock.synchronize do # 2. すでにキューにあるメンテナンス用 checkout があれば、それが返ってくるまで待つ wait_for_outstanding_maintenance(pool) # 3. executor を quiesce させて、これ以上メンテナンス関連の非同期処理が走らない状態にする quiesce_executor_if_any end # 4. ここまで来たら、テストが生成した本物のリークだけを確実に検出できる assert_no_leaked_connections end end実際のメソッド名や実装は異なりますが、概念的には「
reaper_lock配下で非同期メンテナンスの完了を待つ」処理が追加されました。
activerecord/test/cases/test_case_test.rb- 上記の新しい挙動を検証するためのテストが追加されています(+86行)。
- 主な内容:
- 非同期 executor を有効にした状態で、接続プールのリーパーがバックグラウンドでコネクションを checkout するような状況を再現。
- テストの teardown / リークチェック時に、そのバックグラウンド checkout が完了するまで待てていることを確認。
- executor が利用されないケースでも、従来どおり reaper がインラインで動き、リークチェックに悪影響を与えないことを確認。
実質的にやっていること
- reaper_lock を取って、新しいメンテナンスジョブのキューインを防ぐ
- すでにキューに入っているメンテナンス用 checkout が終わるまで待つ
- executor がアイドルになる(quiesce)まで待つ
- その後に コネクションリークチェックを実行
というシーケンスを、Active Record テスト基盤に組み込んだ、という変更です。
- 影響範囲・注意点
影響範囲
- コード変更は「Active Record のテストコードのみ」であり、本番アプリケーションのランタイム挙動は変わりません。
- Rails 開発者やプラグイン作者など、Rails 本体のテストを実行する人には影響があります。
- 非同期 executor(並列実行環境)で Active Record テストを流す CI(たとえば PR の説明中に出ている Buildkite の Rails CI)で、テストの安定性向上が見込まれます。
期待される改善
- コネクションプール関連のテストで、ときどき起こる「たまにだけ失敗する」ような flaky test が減る。
- 特に、「リークしていないのにリーク扱いされる」「並列テスト+非同期 executor の組み合わせでだけ失敗する」類の問題が改善される可能性が高いです。
注意点 / 想定される落とし穴
- 実際のアプリケーションコードには影響しないとはいえ、「テストでしか再現しないバグ」を調査している場合、今回の変更により症状の見え方が変わる可能性があります。
- executor を使った並列・非同期環境で「テストとまったく同じように」コネクションプールを使っているライブラリを作っている場合、テスト側の振る舞いに合わせるべきか(または合わせるべきでないか)を検討する余地はあります(ただしこのPRはあくまで Rails テスト専用ロジックです)。
- 参考情報 (あれば)
- PR 本文に記載の Buildkite ジョブ:
https://buildkite.com/rails/rails/builds/129857#019ecbb1-a5af-4228-8848-63867cd51e6f
ここで観測されたような、executor 環境での flaky なテスト挙動をきっかけに、この修正が入ったと考えられます。 - 関連しそうなキーワード:
ActiveRecord::ConnectionAdapters::ConnectionPoolreaper/reaper_lock- Rails の executor(
ActiveSupport::Executor、Concurrent::ExecutorServiceなど)
これらを組み合わせるテストを書いている場合、本PRのアプローチ(「ロックを取ったうえで executor の非同期ジョブ完了を待ってから検査する」)は、同様の問題に対する一つの参考パターンになります。
#57738 Return an empty string from word_wrap when the text is nil
マージ日: 2026/6/15 | 作成者: @55728
- 概要 (1-2文で)
word_wrapヘルパーにnilを渡したときにNoMethodErrorが発生していた問題を修正し、nilの場合でも空文字列 ("") を返すようにしたPRです。これにより他のテキスト系ヘルパーと挙動が揃い、ビューからそのまま呼び出しても安全になりました。
- 変更内容の詳細
もともとの問題点
既存コード(抜粋):
def word_wrap(text, line_width: 80, break_sequence: "\n")
return +"" if text.empty?
...
endここで text が nil の場合、text.empty? の呼び出しで NoMethodError: undefined method 'empty?' for nil が発生していました。
word_wrap(nil) # => NoMethodError
word_wrap("") # => ""(こちらは既にハンドリング済み)他のテキストヘルパーは nil を素通し、もしくは安全な値に変換しており、word_wrap だけが例外を投げる状態でした。
truncate(nil)→nilhighlight(nil, ...)→""simple_format(nil)→"<p></p>"excerpt(nil, ...)→nil
修正内容
ガード条件を nil にも対応させました:
def word_wrap(text, line_width: 80, break_sequence: "\n")
return +"" if text.nil? || text.empty?
...
endポイント:
text.nil? || text.empty?の明示的チェックにしており、blank?などには変更していません。- これにより「空文字列」はこれまで通り空文字列で返す。
- 「空白のみの文字列(例:
" ")」はこれまで通りword_wrapのロジックに渡される(挙動を変えない)。
テスト追加
actionview/test/template/text_helper_test.rb にテストを1件追加:
def test_word_wrap_with_nil
assert_equal "", word_wrap(nil)
endこのテストは修正前は NoMethodError で落ち、修正後はパスすることが確認されています。
- 影響範囲・注意点
影響範囲
- View / helper で
word_wrap(some_maybe_nil_value)のように、nilになる可能性のある値をそのまま渡していた箇所で、例外が出なくなり、代わりに空文字列が返るようになります。 nilを渡して例外が出ることを前提にしているコードがもしあれば(あまり考えにくいですが)、その挙動は変わります。
- View / helper で
挙動の一貫性
word_wrap(nil)→""という仕様は、他ヘルパーの「nilを許容し、ビューとしてレンダリング可能な文字列にする」方針と整合的です。- ただし
truncate(nil)やexcerpt(nil, ...)はnilをそのまま返すため、「すべてのテキストヘルパーがnil→""というわけではない」点は従来通りです。word_wrapは既に「空文字列を返す」仕様だったので、それにnilを揃えた形です。
意図的に変えていない点
blank?を使っていないため、空白だけの文字列" "はこれまで通りword_wrapの処理を通ります。- そのため、「空白のみの文字列を渡したときの改行位置など」は一切変わっていません。
- 参考情報 (あれば)
- 対象メソッド:
ActionView::Helpers::TextHelper#word_wrap - 追加されたテストファイル:
actionview/test/template/text_helper_test.rb - 変更行数:
actionview/lib/action_view/helpers/text_helper.rb: 1行差し替えactionview/test/template/text_helper_test.rb: テスト 1ケース追加 (約5行)
#57737 Stop documenting the removed args: Hash form of assert_enqueued_email_with
マージ日: 2026/6/15 | 作成者: @55728
- 概要 (1-2文で)
assert_enqueued_email_withのドキュメントが、既に削除済みのargs: { ... }形式でのパラメータ付きメールの検証方法を案内していたため、実装に即したparams:形式の説明に修正した PR です。コード変更はなく、RDoc の不整合解消のみです。
- 変更内容の詳細
何が問題だったか
ActionMailer::TestHelper#assert_enqueued_email_with の RDoc に、以下のような説明とサンプルが残っていました:
# If +args+ is provided as a Hash, a parameterized email is matched.
#
# def test_parameterized_email
# assert_enqueued_email_with ContactMailer, :welcome,
# args: { email: 'user@example.com' } do
# ContactMailer.with(email: 'user@example.com').welcome.deliver_later
# end
# endおよび同様の args: { greeting: "Hello" } を使った例。
しかし、現在の実装は以下のようになっており:
args = Array(args) unless args.is_a?(Proc)
...
params === job_kwargs[:params] && args === job_kwargs[:args]args:に Hash を渡すと、Array(args)によって[{ email: 'user@example.com' }]ではなく、[[:email, "user@example.com"]](Hash の to_a に準じた形)になる- 一方、パラメータ付きメール (
with(email: ... )) の情報はjob_kwargs[:params]に格納される - よって、
paramsがnilのまま、nil === { email: ... }という比較が行われ、必ずマッチに失敗する
すなわち、ドキュメント通りに書くと「メールはちゃんと enqueue されているのに、テストは失敗する」という状態になっていました。
何を修正したか
- 「
argsに Hash を渡すと parameterized email がマッチする」という説明文を削除 - それに依存したサンプルコード(
args: { ... }を使った例)も削除 - 既に同じドキュメント内で紹介されている、正しい
params:を使う例に統一
つまり、parameterized mail を検証したい場合は、次のような形だけを推奨するようにしました:
def test_parameterized_email
assert_enqueued_email_with ContactMailer, :welcome,
params: { email: 'user@example.com' } do
ContactMailer.with(email: 'user@example.com').welcome.deliver_later
end
endファイルレベルでは:
actionmailer/lib/action_mailer/test_helper.rbの RDoc コメントから 10 行削除params:を指し示す文を 1 行追加(or 既存の流れに合わせて修正)
コード本体 (assert_enqueued_email_with の挙動) には一切変更はありません。
- 影響範囲・注意点
- テストコード側の影響
- これまでドキュメントに従って
args: { ... }で parameterized mail を検証していた場合、そのテストは既に失敗している(あるいは、挙動が期待と違う)はずです。 - 今後は一貫して
params: { ... }を使う必要があります。
- これまでドキュメントに従って
- 本体コードへの影響
- 実装は変わっていないため、ランタイム挙動には一切変更なし。
- CI も
[ci skip]付きで、実質ノーリスクなドキュメント修正。
- バージョン間の認識差
- 古い Rails バージョンでは
args:Hash 形式が動いていた時期があるため、その記憶のまま最新の Rails に移行すると「昔の書き方がドキュメントに残っているが、実際には動かない」という落とし穴になっていました。 - この PR により、公式ドキュメントからその古い書き方が消えるため、新規利用者は正しい
params:形式だけを学ぶことになります。
- 古い Rails バージョンでは
- 参考情報 (あれば)
対象メソッド:
ActionMailer::TestHelper#assert_enqueued_email_with内部的なマッチ条件(簡略化):
rubyparams === job_kwargs[:params] && args === job_kwargs[:args]parameterized email (
with(...)) の情報はjob_kwargs[:params]に乗るため、メールのパラメータを検証したい場合はparams:だけを見るべきという設計になっています。
#57739 Apply the deprecated scope hash :except option to except, not only
マージ日: 2026/6/15 | 作成者: @55728
- 概要 (1-2文で)
Rails のルーティングでscopeに「非推奨のハッシュ形式」を渡したとき、:exceptオプションが誤って:onlyとして扱われ、指定と逆のルートが生成されていたバグを修正する PR です。あわせて、この退行を検出するテストが追加されています。
- 変更内容の詳細
問題のあった箇所
ActionDispatch::Routing::Mapper#scope に対して、古い書き方である「位置引数+ハッシュ形式」を使った場合の処理にバグがありました。
問題のコード(修正前)は以下のようになっていました。
# actionpack/lib/action_dispatch/routing/mapper.rb
only ||= assign_deprecated_option(deprecated_options, :only, :scope)
only ||= assign_deprecated_option(deprecated_options, :except, :scope) # <- 本来は `except` に代入すべきここで本来は deprecated_options[:except] を except ローカル変数に割り当てるべきところを、誤って only に代入していました。
その結果、例えば以下のような「非推奨の positional-hash 形式」の書き方:
scope({ except: :destroy }) do
resources :posts
endが、内部的には
scope(only: :destroy) do
resources :posts
endと同等に扱われてしまい、意図と真逆のルーティング になる問題が発生していました。
- 本来の期待:
destroyアクションのみ除外し、それ以外(index,show,new,create,edit,update)は生きている。 - 実際の挙動(バグあり時):
destroyのみ生成され、それ以外のルートがすべて消える。
一方で、キーワード引数形式の:
scope(except: :destroy) do
resources :posts
endは正しく動作しており、同じ意味のはずの2つの書き方が食い違う 状態になっていました。
このバグは、ルーティング関連のメソッドを位置ハッシュからキーワード引数へ移行したコミット(3b4255e180)の際に混入した退行です。namespace 周りの別の退行(:path / :shallow_path / :shallow_prefix が無視される問題)は別 PR (#57740) で対応中とされています。
修正内容
修正後は次のように、except ローカル変数に正しく代入するよう変更されています。
only ||= assign_deprecated_option(deprecated_options, :only, :scope)
except ||= assign_deprecated_option(deprecated_options, :except, :scope)これにより、
scope({ except: :destroy })(非推奨の hash 位置引数形式)scope(except: :destroy)(キーワード引数形式)
の両方が同じ意味で解釈され、destroy だけを除いたルーティングが生成されるようになります。
テスト追加
actionpack/test/dispatch/routing_test.rb に以下のテストが追加されています。
test_scope_with_deprecated_except_hash_option
このテストでは、あえて非推奨の hash 形式を使って:
scope({ except: :destroy }) do
resources :posts
endのようなルーティングを定義し、
/posts(index) が 存在することdestroyルートが 除外されていること
をアサートしています。
修正前は /posts が "Not Found" になりテストが落ちるため、except が誤って only として扱われていたことが確認できます。修正後はテストが通ることが確認されています(2 runs, 13 assertions, 0 failures, 0 errors, 0 skips)。
- 影響範囲・注意点
- 影響を受けるのは、
scopeに対して非推奨の「位置引数+ハッシュ形式」を使い、かつexcept:を指定しているケース のみです。- 例:
scope({ except: :destroy }) { resources :posts }
- 例:
- 通常推奨されるキーワード引数形式:
scope(except: :destroy) { resources :posts }は元々正しく動いており、今回の変更で挙動は変わりません。
- つまり、ルーティング定義を徐々にキーワード引数形式へ移行している途中のアプリで、
- 一部に
scope({ except: ... })形式が残っている - そのルートに関するテストが不十分 な場合、本番環境で気づかないまま逆転したルート定義が動いていた可能性 があります。
- 一部に
- この PR により、非推奨形式もキーワード形式と同じ意味にそろえられるため、将来的に形式を移行する際にも挙動が一致します。
- 非推奨であること自体は変わらないので、長期的には
scope(except: ...)などキーワード引数形式への置き換えが推奨されます。
- 参考情報 (あれば)
- 対応 PR:
- この PR: https://github.com/rails/rails/pull/57739
- 関連する退行修正 (
namespaceのオプション無視問題): https://github.com/rails/rails/pull/57740
- バグ導入元となったコミット:
3b4255e180"Use keywords in routing mapper"
#57740 Honor the deprecated namespace hash path options
マージ日: 2026/6/15 | 作成者: @55728
- 概要 (1-2文で)
Rails のルーティング DSL において、namespaceが受け取る 非推奨のハッシュ形式オプション({ path: ... }など)が正しく扱われず、パスの二重付与やshallow_path/shallow_prefix無視といったバグが出ていたのを修正する PR です。キーワード引数形式との挙動の不一致を解消し、非推奨形式でも期待どおりルーティングが生成されるようにしています。
- 変更内容の詳細(あればサンプルコードも含めて)
問題のあったコードパス
namespace メソッドの定義(ActionDispatch::Routing::Mapper)では、キーワード引数にデフォルトで「真偽値として真のダミーオブジェクト」DEFAULT を入れています。
DEFAULT = Object.new.freeze
def namespace(name, deprecated_options = nil, as: DEFAULT, path: DEFAULT,
shallow_path: DEFAULT, shallow_prefix: DEFAULT, **options, &block)
if deprecated_options.is_a?(Hash)
as = assign_deprecated_option(deprecated_options, :as, :namespace) if deprecated_options.key?(:as)
path ||= assign_deprecated_option(deprecated_options, :path, :namespace) if deprecated_options.key?(:path)
shallow_path ||= ...
shallow_prefix ||= ...
...
end
...
endpath/shallow_path/shallow_prefixは初期値としてDEFAULT(truthy)を持っている- そこに対して
path ||= ...としているため、常に左辺が truthy と評価されて右辺が実行されない - 結果として、「非推奨のハッシュ形式で渡された値」がまったく読まれず無視される
さらに悪いことに、その非推奨ハッシュから読み取られなかったキーが **options 側に残り、後続処理で「もとの name を使ったデフォルト path」と「ハッシュに残った :path」が二重に扱われるケースが発生します。
バグの具体例
namespace :admin, { path: "adm" } do # 非推奨のハッシュ形式
resources :posts
end期待されるルーティング:
/adm/postsバグ発生時の実際のルーティング:
/admin/adm/posts # `admin` と `adm` が二重に付く同様に、
namespace :foo, { shallow_path: "bar" } do
resources :posts, shallow: true
end
namespace :foo, { shallow_prefix: "bar" } do
resources :posts, shallow: true
endのようなケースで、shallow_path / shallow_prefix が 完全に無視される という問題がありました。
なお、キーワード引数形式:
namespace :admin, path: "adm" do
resources :posts
endは正しく動作しており、「ハッシュ形式だけが壊れている」状態でした。
修正内容
非推奨ハッシュ形式からの値の取り出しに使用している代入演算子を ||= から = に変更し、すでに正しく動作している :as オプションと同じパターンに揃えました。
修正後:
if deprecated_options.is_a?(Hash)
as = assign_deprecated_option(deprecated_options, :as, :namespace) if deprecated_options.key?(:as)
path = assign_deprecated_option(deprecated_options, :path, :namespace) if deprecated_options.key?(:path)
shallow_path = assign_deprecated_option(deprecated_options, :shallow_path, :namespace) if deprecated_options.key?(:shallow_path)
shallow_prefix = assign_deprecated_option(deprecated_options, :shallow_prefix, :namespace) if deprecated_options.key?(:shallow_prefix)
...
endポイント:
- すべて
if deprecated_options.key?(:xxx)でキーの存在を確認したうえで、=で上書き DEFAULTという sentinel 値を使う設計と矛盾しないように、- 「ユーザがキーワードで与えた値」 > 「非推奨ハッシュで与えた値」 > 「
nameから導かれるデフォルト」という優先度を実現
- 「ユーザがキーワードで与えた値」 > 「非推奨ハッシュで与えた値」 > 「
||=だと「すでにDEFAULT(truthy)が入っているため二度と書き換わらない」ので、ここを素直な=に変えることで、期待どおり deprecated ハッシュから値を採用できるようになった
テストの追加
actionpack/test/dispatch/routing_test.rb に以下のテストが追加されています。
test_namespace_with_deprecated_path_hash_optiontest_namespace_with_deprecated_shallow_path_hash_optiontest_namespace_with_deprecated_shallow_prefix_hash_option
それぞれ、すでにある「キーワード引数形式のテスト」をほぼそのまま「ハッシュ形式」で書き直したものになっており、以下を確認しています。
pathハッシュオプションを使っても、パスが二重にならず期待どおりの URL になるshallow_path/shallow_prefixのハッシュオプションが正しく shallow ルーティングに反映され、ヘルパーも期待どおり定義される
修正前はこれら 3 テストが失敗し、修正後は既存のテストも含めてすべて成功することが確認されています。
- 影響範囲・注意点
影響を受けるアプリケーション
namespaceを以下のような 非推奨ハッシュ形式で呼び出しているアプリが対象です:rubynamespace :admin, { path: "adm" } do # ... end namespace :users, { shallow_path: "u" } do # ... end namespace :api, { shallow_prefix: "api_v1" } do # ... endこれまで:
pathが二重になっていたり、shallow_path/shallow_prefixが無視されていたり、- それに起因してヘルパーメソッドが期待どおり定義されていない、 といった「おかしな挙動」が出ていた場合、それが修正されます。
互換性について
- 非推奨とはいえ、このハッシュ形式は「まだサポート対象」であるため、今回の修正は 本来期待される動作に近づけるためのバグ修正 です。
- ただし、もし既存のアプリが「不具合を前提にしたワークアラウンド」(例: パスの二重付与を見越して
pathを調整している等)を行っていた場合、今回の修正でルーティングのパスが変わる可能性があります。 - 実運用では、以下の確認を推奨します。
rake routesで namespace 周りのルーティングが変化していないか- 特に shallow ルーティング (
shallow: true) と組み合わせている namespace の URL とヘルパー名を確認
推奨される対応
新規コード・既存コードともに、非推奨のハッシュ形式はできるだけ早くキーワード引数形式に書き換えることが推奨されます。
ruby# 旧 (非推奨) namespace :admin, { path: "adm", shallow_path: "a" } do # ... end # 新 (推奨) namespace :admin, path: "adm", shallow_path: "a" do # ... end今回の修正により、両方の書き方が同じ結果になることが保証されるため、移行作業はやりやすくなっています。
- 参考情報 (あれば)
- このバグは
namespace/scopeをキーワード引数に移行したコミット3b4255e180 ("Use keywords in routing mapper")で混入した回帰です。 - 兄弟の問題として、
scopeにおける:exceptと:onlyの取り扱いバグ(:exceptが誤ってonlyに割り当てられフィルタリングが反転する)があり、これは別 PR#57739で修正されています。 - ルーティング DSL では、オプションの取り扱い順序・デフォルト値(sentinel を使うかどうか)・キーワード/ハッシュの双方を同じ意味に保つ必要があり、今回のような「
||=の機械的な置き換え」が回帰を生みやすいことが示唆されています。
#57721 Return an Enumerator from Parameters#select / #reject when no block is given
マージ日: 2026/6/15 | 作成者: @55728
- 概要 (1-2文で)
ActionController::Parameters#select/#rejectをブロックなしで呼び出したとき、これまで例外が発生していた挙動を修正し、Hashと同様にEnumeratorを返すようにした PR です。Rails の Strong Parameters が Ruby 標準のHashのインターフェースとより一貫するようになります。
- 変更内容の詳細
何が問題だったか
ActionController::Parameters には Hash と似たインターフェースを提供するためのラッパーメソッドが多数あります:
each_paireach_valuetransform_values,transform_values!transform_keys,transform_keys!selectreject
このうち、多くのメソッドは「ブロックが渡されなかった場合には Enumerator を返す」という Ruby 標準の Hash と同じ挙動を実装するために、以下のようなガードを持っていました。
return to_enum(:transform_values) unless block_given?しかし select / reject にはそのガードが存在せず、代わりに次のような実装でした。
def select(&block)
new_instance_with_inherited_permitted_status(@parameters.select(&block))
endそのため、ブロックなしで呼び出した場合:
params = ActionController::Parameters.new(a: 1, b: 2)
# 期待される (Hash 互換な) 挙動
# params.select #=> Enumerator (e.g. params.select.with_index {...})
params.selectは内部的に @parameters.select(&nil) が実行され、@parameters.select 自体は Enumerator を返すものの、その戻り値を new_instance_with_inherited_permitted_status に渡してしまいます。
new_instance_with_inherited_permitted_status は内部で each_key など、Hash 前提のメソッドを呼ぶため、Enumerator に対して each_key を呼び出そうとして以下のような例外になります。
NoMethodError: undefined method `each_key' for #<Enumerator: ...>つまり:
Hash#select/Hash#reject→ ブロックなしだとEnumeratorを返す (OK)Parameters#select/#reject→ ブロックなしだとEnumeratorを内部でさらに処理してしまいNoMethodError(NG)
という不整合が起きていました。
修正内容
select / reject に、既存の transform_values と同じ形式のガードを追加しました。
def select(&block)
return to_enum(:select) unless block_given?
new_instance_with_inherited_permitted_status(@parameters.select(&block))
end
def reject(&block)
return to_enum(:reject) unless block_given?
new_instance_with_inherited_permitted_status(@parameters.reject(&block))
endこれにより:
params = ActionController::Parameters.new(a: 1, b: 2)
enum = params.select
# enum は Enumerator
enum.with_index { |(k, v), i| ... } # などが可能
enum2 = params.reject
# こちらも Enumerator として利用可能という、Hash と同様の利用パターンがサポートされます。
テスト
actionpack/test/controller/parameters/accessors_test.rb に以下に相当するテストが追加されています:
#selectにブロックを渡さない場合、Enumeratorを返すこと#rejectにブロックを渡さない場合、Enumeratorを返すこと
既存の「transform_values はブロックなしで Enumerator を返す」テストと同パターンのテストが select / reject に対しても追加されており、修正前は NoMethodError で落ちるケースだったことが確認されています。
- 影響範囲・注意点
影響範囲
ActionController::Parameters#select/#rejectを「ブロックなしで」呼び出すコードに影響があります。- 修正前は「内部で例外が発生していた」ケースなので、基本的には「これまでクラッシュしていたコードが正しく動くようになる」方向の変更です。
- 通常の、ブロック付きの
params.select { ... }/params.reject { ... }の挙動は変わりません。
互換性上の注意点
- もしアプリ側で「
params.selectを呼ぶと例外になること」を前提にしたワークアラウンドをしていた場合、その挙動は変わりますが、実用上そのような前提を持つコードはまずないと思われます。 - Ruby の
Hashと同じくEnumeratorが返るため、select.with_indexやreject.with_indexのようなチェインがそのまま利用可能になります。
- もしアプリ側で「
セキュリティ・Strong Parameters 的な注意点
new_instance_with_inherited_permitted_statusの呼び出しは、ブロックが渡された場合のみ行われます (return to_enumで早期リターンするため)。- したがって、実際に
Parametersの新しいインスタンスを生成して permitted 状態を引き継ぐ処理の流れ自体はこれまで通りで、Strong Parameters のセキュリティ特性に変化はありません。 Enumerator経由で要素を列挙するだけでは permitted / unpermitted 状態そのものが変わるわけではありません。
- 参考情報 (あれば)
- Ruby 本体の仕様:
Hash#select/Hash#rejectはブロックなしで呼ばれるとEnumeratorを返す:rubyh = { a: 1, b: 2 } h.select #=> #<Enumerator: {...}:select> h.select.with_index { |(k, v), i| ... }
- Rails 内での類似実装:
ActionController::Parameters#transform_values/#transform_keysなどは既にreturn to_enum(:method_name) unless block_given?で同様の取り扱いをしており、今回の変更によりselect/rejectもそれに揃えられました。
#57735 [RF Docs] Rails Internationalization (#57381)
マージ日: 2026/6/15 | 作成者: @p8
- 概要 (1-2文で)
Rails ガイドの「国際化 (I18n)」ドキュメントを大幅にリライトし、初心者にも読み進めやすい構成と、より具体的なコード例・出力例を備えた内容に刷新した PR です。あわせて、古い情報の削除や他ガイドとの表記・スタイルの整合も図られています。
- 変更内容の詳細
2-1. ガイド全体構成の大幅な再編成
旧構成を見直し、以下の流れに整理し直したとの記述があります:
- 初心者向けの導入(「どういうときに I18n を使うか」「このガイドを読むと何ができるようになるか」)
- 「リクエスト間でロケールを管理する」セクションを独立させて前半に配置
- URL パラメータやセッションなどから locale を決める話が分かりやすくまとまるように
- I18n API の機能紹介(
I18n.t,I18n.l, pluralization, interpolation など) - より高度なトピック(バックエンド差し替え、例外ハンドリングなど)
導入部分は、他のガイドと同様に「After reading this guide, you will know...」形式に書き換えられ、ステップ一覧的な説明から「学べることの一覧」に変更。
2-2. 古い / 不正確な情報の整理・削除
- 削除・変更点の例:
- 「Contributing」セクション: 非アクティブなフォーラムへの言及を削除。
- 「Translating Model Content」セクション: サードパーティ gem へのリンクを削る方針になり、I18n ガイドの範囲を Rails 標準機能の説明に絞る方向へ。
- 「将来の Rails バージョンで public/assets の自動ローカライズが入るかも」といった将来予測的な文言を削除(実現されておらず計画もないため)。
- 「Using Different Exception Handlers」セクションを「Handling Missing Translations / Handling I18n Exceptions」相当の内容に整理し直し、Rails における MissingTranslationData の扱い説明にフォーカス。
2-3. コード例と出力例の大幅追加
ガイド全体で「説明だけ」で済ませていた箇所に、コードとその結果(コメントなど)を多数追加しています。代表的なもの:
基本的な翻訳例の明確化
# config/locales/en.yml
en:
hello: "Hello world"I18n.t("hello") # => "Hello world"といった形で、キーと実際の I18n.t の呼び出し・戻り値を並べて説明するスタイルに統一。
ActiveModel / ActiveRecord メッセージの例
- バリデーションメッセージの設定と結果をコードで明示:
en:
activerecord:
errors:
models:
user:
attributes:
name:
blank: "must be present"user = User.new(name: "")
user.valid?
user.errors.full_messages
# => ["Name must be present"]User.model_name.humanなどのメソッドもコードブロックで例示:
en:
activerecord:
models:
user: "Account"User.model_name.human
# => "Account"スコープ・ネストされたキーの例
「Basic Lookup, Scopes, and Nested Keys」の各例について、I18n.t の戻り値もコメントで併記:
en:
date:
formats:
short: "%b %d"I18n.t("date.formats.short")
# => "%b %d"HTML セーフ / エスケープの例と出力 HTML
Safe HTML 翻訳の章で、app/views/home/index.html.erb のレンダリング結果 HTML も追加:
<%= t("welcome_html") %>en:
welcome_html: "Welcome <strong>World</strong>"結果:
Welcome <strong>World</strong>など、「どこでエスケープされるか」が目で追いやすいように。
変数の挿入と通貨表現の例の改善
Passing Variables to Translationsセクションでは、通貨記号$と€が混在する、不自然な例を修正。- 例: 「オランダ語 (dutch) は
€ 100、スペイン語 (spanish) は100 €」のように、言語によるフォーマット差を説明する例に置き換え。
- 例: 「オランダ語 (dutch) は
number_to_currencyに関する注意は、一般的な NOTE スタイルの注意書きに統一。
number_to_currency(100, locale: :nl)
# => "€ 100"
number_to_currency(100, locale: :es)
# => "100 €"例外ハンドリング・Missing Translation の例
I18n::MissingTranslationData について、実際の例を追加:
I18n.t("missing")
# => raises I18n::MissingTranslationData (Translation missing: en.missing)および Rails 側でのハンドリング・カスタムハンドラ導入方法を説明する流れに整理。
2-4. 「リクエスト間でロケールを管理する」章の独立化・強化
Managing the Locale across Requestsをトップレベルセクションとして独立。- その中で、以下のようなパターンごとに小見出し化:
- 「Setting the Locale from a request parameter」(リクエストパラメータ
params[:locale]で決定) - セッション / ユーザ設定 / Accept-Language など(詳細は PR コメント上では「要対応」となっているが、少なくともパラメータでの設定例はヘッダ付きで整理)。
- 「Setting the Locale from a request parameter」(リクエストパラメータ
- 例(代表的な Rails コントローラのパターン):
class ApplicationController < ActionController::Base
before_action :set_locale
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
end
end2-5. バックエンド差し替え・他ビルトインメソッドの説明明確化
- 「Using Different Backends」セクション:
- 以前は「追加のバックエンドをチェインするコード例」までしかなかったが、「どう使うのか」まで踏み込む説明を追加。
- 例: SimpleBackend と KeyValue backend を組み合わせる、など。
- 「Overview of Other Built-In Methods that Provide I18n Support」:
number_to_currency,distance_of_time_in_words,time_ago_in_wordsなど、I18n 対応のヘルパは名前だけでなく、短いコード例付きで紹介されるように。
- 影響範囲・注意点
- 影響対象は Rails ガイド(ドキュメント)のみで、アプリケーションコードやフレームワーク本体の挙動には変更なし。
- ただし:
- 既存のガイドへのリンク(特定セクションへのアンカー)を社内ドキュメントやブログなどで参照している場合、セクション名・見出し構造が変わっている可能性があるため、リンク切れに注意。
- I18n ガイド内からサードパーティ gem への誘導が削られているため、「モデルの翻訳」などを学ぶ際は別途 gem ドキュメントを参照する必要がある。
- ガイドが初心者向けに再構成されているため、新しく I18n を導入する開発者は、最新ガイドに合わせてチュートリアル的に読み進めるのが推奨されます。
- 参考情報 (あれば)
- 元の改善提案・TODO リスト: https://github.com/rails/rails/pull/51840#discussion_r1603318416
- この PR: [RF Docs] Rails Internationalization (#57381) / #57735
- 関連ファイル(主に内容が変わったガイド):
guides/source/i18n.md(メインの I18n ガイド、+1144/-603 と大幅更新)guides/source/getting_started.md(I18n への導入リンクや記述の調整)guides/source/ruby_on_rails_guides_guidelines.md(ガイド執筆ガイドラインの微調整)
#57734 [ci skip] Remove orphaned backtick from routing docs
マージ日: 2026/6/15 | 作成者: @tylerlwsmith
概要 (1-2文で)
Rails のルーティングガイド(routing.md)の「Path and URL Helpers」セクションに紛れ込んでいた不要なバッククォート(`)1文字を削除した、ドキュメントのみの修正です。機能や挙動の変更は一切なく、表記・レイアウトの体裁を整えるためのものです。変更内容の詳細 (あればサンプルコードも含めて)
- 対象ファイル:
guides/source/routing.md - 変更内容:
- 該当セクション内の文章中に、Markdown のインラインコードとして閉じていない「余計なバッククォート」が 1 文字存在していたのを削除しています。
- 実際の差分は「1 行においてバッククォートを 1 文字削除」という最小限の変更で、文章やコード例の意味自体は変わっていません。
イメージとしては、例えば以下のような状態になっていたものを:
You can use `user_path(@user)`` to get the path...これを:
You can use `user_path(@user)` to get the path...のように修正した、というタイプの変更です(実際の文面はこれと多少異なる可能性がありますが、性質としては同じです)。
- 影響範囲・注意点
- 影響範囲:
- Rails 本体のコード・API には一切影響ありません。
- 影響するのは Rails Guides の表示のみで、Markdown → HTML 変換後の見た目(インラインコードの崩れ、余計な記号の表示)が解消されます。
- 注意点:
- ドキュメント修正のみのため、アプリケーション側で対応すべきことはありません。
- CI を回さないための
[ci skip]がタイトルに付いていますが、これはコード変更がないドキュメント PR でよく行われる運用上の指定であり、特別な意味はありません。
- 参考情報 (あれば)
- 該当セクション: Rails Guides > Routing > Path and URL Helpers
- 目的: Markdown 記法上のタイポ修正によるドキュメント品質の向上であり、API 仕様やルーティングの挙動に関する変更・補足は行っていません。
#57381 [RF Docs] Rails Internationalization
マージ日: 2026/6/15 | 作成者: @Ridhwana
- 概要 (1-2文で)
Railsガイドの「Rails Internationalization (I18n)」ドキュメントを大幅に書き換え、構成を初心者フレンドリーに再編しつつ、API・ベストプラクティス・例外処理などを具体的なコード例付きで整理したPRです。既存のガイドラインとの整合性を取りつつ、古い情報や不要なセクションも整理しています。
- 変更内容の詳細
2-1. ガイド全体構成の再編成
- I18nガイド
guides/source/i18n.mdが大きく書き換えられています(+1144 / -603 行)。 - 構成の方針:
- 初心者向けの導入
- 導入部を「I18nを実装するための手順の要約」から、他のガイドと同様の
「このガイドを読むと次のことが分かります(After reading this guide, you will know…)」形式に変更。 - 最初に、用語の定義・YAMLの基本・
I18n.tの基本的な使い方などをまとめることで、Rails初心者でも読み進めやすい構成にしています。
- 導入部を「I18nを実装するための手順の要約」から、他のガイドと同様の
- リクエスト単位のロケール管理を独立セクション化
- 「Managing the Locale across Requests」をトップレベルの大きなセクションに昇格。
- 実際のアプリ開発で一番迷いがちな「どこで locale を決めるか?」にすぐ到達できるようナビゲーションを改善。
- API機能の説明 → 高度なトピックという流れ
- 下位セクションに、バックエンドの切り替え・例外処理・カスタムフォーマットなど、より高度な内容をまとめています。
- 初心者向けの導入
2-2. 導入・基本セクションの改善
導入文を全面的に書き換え:
- 旧: 実装手順の箇条書きなど、他ガイドとフォーマットがズレていた。
- 新: 「このガイドを読んだあとに理解できること」の列挙(標準形式)。
キーと翻訳の関係を、文章説明だけでなくコード例で示すように変更:
ruby# config/locales/en.yml en: hello: "Hello world"rubyI18n.t('hello') # => "Hello world"ActiveModel / ActiveRecord のバリデーションメッセージも、実際の出力例をコードで示すように追記。
2-3. 「リクエスト間でのロケール管理」セクションの強化
- 「Managing the Locale across Requests」関連の内容を整理・昇格:
- 例: パラメータからロケールを設定する例に、明示的な見出しを追加:
- 「Setting the Locale from a request parameter」(
params[:locale]を使うパターン)
- 「Setting the Locale from a request parameter」(
before_actionなどを使ってI18n.localeを決める標準的なやり方を、より読みやすく整理。
- 例: パラメータからロケールを設定する例に、明示的な見出しを追加:
- ガイド全体の流れとして、
- ロケールとは何か
- 翻訳を書いて
I18n.tする - 実際のリクエストでどのロケールを使うか決める という順序になるよう再構成されています。
2-4. 翻訳の詳細なAPI解説の充実
基本的な lookup / スコープ / ネスト
「Basic Lookup, Scopes, and Nested Keys」部分で、単に
I18n.tの呼び出しだけではなく、戻り値の例も追加:yaml# config/locales/en.yml en: dashboard: title: "Dashboard" welcome: "Welcome, %{name}"rubyI18n.t('dashboard.title') # => "Dashboard" I18n.t('dashboard.welcome', name: "Alice") # => "Welcome, Alice"
変数埋め込み(%{})と通貨の例
- 「Passing Variables to Translations」の例を修正:
- 通貨例が
$と€の混在だったものを、ロケールに応じた通貨表記の違いをより現実的に示すように変更(例: オランダ語とスペイン語で€ 100/100 €の違い)。
- 通貨例が
number_to_currencyについての説明を通常テキストから明示的な NOTE に変更し、「フォーマットはヘルパ側に任せられる」ことを明確化。
Safe HTML とエスケープ
- 「Using Safe HTML Translations」セクションで、
html_safeの扱いを説明する際に生成されるHTMLの例を追加:- どこまでがエスケープされ、どこからはされないのかが明示されるように。
2-5. Active Record / モデル関連の翻訳
- 「Translations for Active Record Models」などで、これまでは文章中に埋め込まれていたメソッド名を、コードブロックで明示:ruby
User.model_name.human # => "Account" # など、ロケールに応じたモデル名 User.human_attribute_name(:email) # => "Email address" - モデル内容の翻訳に関する「Translating Model Content」セクションから、サードパーティgemへのリンクを削除:
- 公式ガイドとしては外部gemの宣伝をせず、必要ならイントロの注意書きレベルの記述に留める方針に変更。
2-6. カスタムフォーマット・バックエンド・例外処理など高度な機能
カスタム翻訳・フォーマット
「Custom Translations」セクションの例に対し、出力例を追加:
yaml# config/locales/en.yml en: date: formats: short: "%b %d"rubyI18n.t("date.formats.short") # => "%b %d"日付・時刻・数値フォーマッタとの連携など、フォーマットの役割をより明示的に説明。
複数バックエンドの利用
- 「Using Different Backends」セクションを拡張し、
- バックエンドをチェーンする例に加えて、
- 「バックエンドを追加したあとどう使うか」を、より具体的なコード例で説明。
- 例: メモリ内バックエンド + データベースバックエンドなど。
- ※実装詳細までは踏み込み過ぎず、「RailsのI18nは pluggable backend である」ことを分かりやすく。
例外処理・Missing translations
- 「Using Different Exception Handlers」セクションを、より役割を明確にするために
- 「Handling Missing Translations」または「Handling I18n Exceptions」的な名前に変更。
I18n::MissingTranslationDataの実際の例を追加:rubyI18n.t('missing') # Translation missing: en.missing # (I18n::MissingTranslationData)- Rails側でカスタムハンドラを設定してメッセージを変えたりログを取る、といった標準的なパターンについても説明を補強。
- 他の例外についても、少なくとも言及や簡単な例を追加(完全な網羅は今後の課題として残タスク扱い)。
2-7. 古い情報・ガイドライン不整合の整理
- Contributing セクションの削除
- 非アクティブなフォーラムを参照していたため削除。
- Authors セクションの削除予定
- 他のガイドで著者を明示していない方針と揃えるため、このPRでは未完了タスクとしてマークされているが、方向性としては削除。
- 将来計画として書かれていた以下の一文を削除:
- 「Future Rails versions may well bring this automagic localization to assets in public, etc.」
- 実際に実装予定・ロードマップがないため、読者に誤解を与えないようにする目的。
2-8. 関連する他ガイドの微修正
guides/source/action_view_overview.md- I18n関連の説明やリンクを、更新されたI18nガイド構成に合わせて微調整(1行追加 / 2行削除レベル)。
guides/source/getting_started.md- Getting Started ガイドからI18nへの誘導文を現行のI18nガイドの構成に合わせて調整(28行ずつ増減の範囲で文章の書き換え)。
guides/source/ruby_on_rails_guides_guidelines.md- ドキュメント執筆ガイドラインの中で、I18nガイドに関係するポリシー(例: 認証情報の表記、サンプルコードスタイルなど)に関する小変更(+2 / -2)。
guides/source/documents.yaml- ガイド一覧でのI18nガイドのタイトルや説明の更新。ナビゲーションに出る説明文も、新しい方針に合うように調整。
guides/assets/images/i18n/demo_html_safe.png- 画像ファイルは差分上は変更なし(+0/-0)だが、参照箇所の文脈が変わっている可能性あり。
- 影響範囲・注意点
コードへの影響:
- 変更はすべてガイド(ドキュメント)のみであり、Rails本体の挙動・APIには影響なし。
- 既存のI18nコードが壊れる / 警告が出るといった影響はありません。
開発者への実務的影響:
- これまでグレーだったベストプラクティス(特に「どこでロケールを決定するか」「例外をどう扱うか」)が明確になり、新しくI18n対応を始めるチームにとっての参照元が改善されます。
- 既存プロジェクトで独自にやっていたパターンが、ガイドの推奨とズレている場合は、
- どこを標準寄りに寄せるか
- 逆にどこをあえてカスタムとして残すか
の検討がしやすくなります。
ドキュメント参照先の変更:
- Getting Started / Action View Overview からI18nガイドへのリンク・説明が変更されているため、
チーム内で「◯◯セクション参照」と共有していた場合は、新しい節タイトルに合わせて更新が必要なことがあります。
- Getting Started / Action View Overview からI18nガイドへのリンク・説明が変更されているため、
未完のTODO:
- PR説明文上でチェックが外れているタスク(Authors セクション完全削除、他例外の例追加など)がいくつか残っているため、今後のPRでさらに細部が変更される可能性があります。
- 参考情報 (あれば)
Rails Guides: Internationalization in Rails(このPRで更新されたガイドの本体)
https://guides.rubyonrails.org/i18n.html (Edge / Stableで内容は若干異なる可能性あり)Railsガイド執筆ガイドライン
https://edgeguides.rubyonrails.org/ruby_on_rails_guides_guidelines.htmlI18n gem(RailsのI18n機能の基盤となるgem)
https://github.com/ruby-i18n/i18n
#57726 Fix max_resumptions attribute name in ResumeLimitError docs
マージ日: 2026/6/14 | 作成者: @55728
- 概要 (1-2文で)
ActiveJob::Continuation::ResumeLimitErrorの RDoc に誤って記載されていたクラス属性名max_resumesを、正しいmax_resumptionsに修正したドキュメント専用のPRです。コード挙動自体は一切変わらず、ドキュメントの記述だけが1行修正されています。
- 変更内容の詳細(あればサンプルコードも含めて)
対象:
activejob/lib/active_job/continuation.rb内の RDoc コメント問題点:
エラークラス
ActiveJob::Continuation::ResumeLimitErrorのコメントで、次のように書かれていました:ruby# Raised when a job has reached its limit of the number of resumes. # The limit is defined by the +max_resumes+ class attribute. class ResumeLimitError < Error; endしかし実際のクラス属性は
max_resumptionsであり、max_resumesというクラス属性は存在しません。ActiveJob::Continuableモジュール内では、正しくmax_resumptionsが定義・説明されていますが、この RDoc の1箇所だけ表記揺れ/誤りがありました。
修正内容:
上記コメントの
+max_resumes+を、正しい+max_resumptions+に変更:ruby# Raised when a job has reached its limit of the number of resumes. # The limit is defined by the +max_resumptions+ class attribute. class ResumeLimitError < Error; end
想定される誤利用例:
誤ったドキュメントを信じて、ジョブクラス側で以下のように書いてしまうと:
rubyclass MyJob < ApplicationJob self.max_resumes = 3 end実際には
max_resumes=が定義されていないため、NoMethodError: undefined method 'max_resumes='が発生していました。
正しい使用例:
rubyclass MyJob < ApplicationJob # ジョブが再開(resume)できる回数の上限を設定 self.max_resumptions = 3 end
- 影響範囲・注意点
影響範囲:
- ランタイムコードには一切変更がなく、挙動に影響はありません。
- 既存の
max_resumptionsの利用コードはそのまま問題なく動作します。 - これまで RDoc やコメントだけを見て
max_resumesを使っていた場合、既にエラーになっていたはずで、このPRによって新たに壊れるコードはありません。
注意点:
- ActiveJob の Continuation/Continuation::ResumeLimitError を使っているプロジェクトで、もし
max_resumesを使おうとしていた・コメントだけを頼りにしていた場合は、クラス属性名がmax_resumptionsであることを改めて確認してください。 - CIスキップ (
[ci skip]) のタグが付いているため、この変更は完全にドキュメント更新扱いです。
- ActiveJob の Continuation/Continuation::ResumeLimitError を使っているプロジェクトで、もし
- 参考情報 (あれば)
- 対象クラス/モジュール:
ActiveJob::ContinuableActiveJob::Continuation::ResumeLimitError
- 概念メモ:
max_resumptionsは、継続可能ジョブ(continuable job)がresumeされる回数の上限を指定するクラス属性です。- 上限に達すると
ResumeLimitErrorが発生し、その説明コメントが今回の修正対象でした。
#57730 Don't mask errors raised inside a cursor's succ in Step#advance!
マージ日: 2026/6/14 | 作成者: @55728
- 概要 (1-2文で)
ActiveJob::Continuation::Step#advance!がカーソルのsucc呼び出し時に発生する本来のNoMethodErrorを飲み込んで別エラーにすり替えていた問題を修正し、succを実装していないカーソルだけを明示的に弾くように変更した PR です。これにより、カスタムカーソルのsucc内部のバグが正しく例外として表に出るようになります。
- 変更内容の詳細
変更前の挙動
Step#advance! は、succ を持たないカーソルに対して分かりやすいエラーを出すために、NoMethodError を rescue して UnadvanceableCursorError に変換していました:
def advance!(from: nil)
from = cursor if from.nil?
begin
to = from.succ
rescue NoMethodError
raise UnadvanceableCursorError, "Cursor class '#{from.class}' does not implement 'succ'"
end
set! to
endしかしこの実装だと、
- カーソルオブジェクトが
succを定義している - その
succの「内部」でNoMethodErrorが発生した
という場合でも、元の NoMethodError が rescue されてしまい、常に
UnadvanceableCursorError: Cursor class 'MyCursor' does not implement 'succ'という誤ったエラーになり、実際の原因やバックトレースが隠蔽されてしまっていました。
変更後の実装
succ を事前に respond_to? でチェックし、未実装の場合のみ UnadvanceableCursorError を投げるように変更されました:
def advance!(from: nil)
from = cursor if from.nil?
unless from.respond_to?(:succ)
raise UnadvanceableCursorError, "Cursor class '#{from.class}' does not implement 'succ'"
end
set! from.succ
endポイント:
respond_to?(:succ)で「メソッドが存在するかどうか」だけを確認- その後の
from.succ実行時に発生した例外 (NoMethodErrorを含む) はそのまま外に伝播する- つまり、
succ実装のバグは本来の例外としてデバッグ可能な形で見える
- つまり、
テスト追加
succを実装しているが、その中で「別の理由」でNoMethodErrorを投げるカーソルクラスを用意し、- その
NoMethodErrorがUnadvanceableCursorErrorにラップされず、そのまま伝播することをテスト
- その
- 既存のテスト (
nil/Float/Integerなどのカーソルに対する挙動) は変更なしで通ることを確認
- 影響範囲・注意点
- 影響を受けるのは:
ActiveJob::Continuationを使っていて、独自のカーソルオブジェクトをStepに渡し、そのオブジェクトにsuccを実装しているケース
- これまで:
- カーソルの
succ内部でNoMethodErrorが起きると、UnadvanceableCursorErrorとして報告され、原因特定が難しかった
- カーソルの
- 今後:
succが存在するカーソルでは、succ内の例外はそのまま表に出る
→ バグが正しいエラー種別・スタックトレースで見えるようになるsuccを実装していないカーソル (nil,Float,Arrayなど) には、従来通りUnadvanceableCursorErrorが発生succが private な場合もrespond_to?(:succ)はfalseを返すため、以前と同様に「実装されていないもの」として扱われる
互換性の観点では、以下のような「エラーの見え方」が変わる可能性があります:
- 以前:
succ内部のNoMethodErrorも「カーソルがsuccを実装していない」ように見えていた - 以後:
succ内部のNoMethodErrorは、そのままのエラー (例:undefined methodなど) として上がる
アプリケーションコードで UnadvanceableCursorError に依存したハンドリングをしている場合、
これまで「succ 内部バグも全部 UnadvanceableCursorError だった」前提のコードがあると、挙動が変わる可能性があります。
ただし、この変更の方が「メソッド契約どおり」であり、バグ調査もしやすくなるため、正しい方向の修正といえます。
- 参考情報 (あれば)
- 対象コード:
activejob/lib/active_job/continuation/step.rb - 想定している契約:
- 「
Step#advance!は、カーソルがsuccを実装していない場合にUnadvanceableCursorErrorを投げる」 succを実装している場合は、その実行中の例外はそのまま伝播させる
- 「
- コアな設計意図:
- 「API 契約違反 (succ 未実装)」と「実装バグ (succ 内部の例外)」を区別し、後者を隠さないようにすること
#57729 Pass the exception positionally when resuming a continuation after an error
マージ日: 2026/6/14 | 作成者: @55728
- 概要 (1-2文で)
Active Job の「継続可能(continuable)ジョブ」をエラー後に再開する際、例外オブジェクトの渡し方に不整合があり、exception_executionsのキーが意図しない文字列になる不具合を修正した PR です。例外は常に位置引数でresume_jobに渡すように統一し、それを検証するテストが追加されています。
- 変更内容の詳細
問題の背景
ActiveJob::Continuable#continue は、継続可能ジョブが途中まで進んだ後にエラーを投げた場合に、そのジョブを再開する責務を持ちます。内部ではおおよそ以下のような例外ハンドリングをしていました(PR 説明からの抜粋・整形):
rescue Continuation::Interrupt => e
resume_job(e) # 位置引数
rescue Continuation::Error
raise
rescue StandardError => e
if resume_errors_after_advancing? && continuation.advanced?
resume_job(exception: e) # キーワード風(実際にはハッシュ)
else
raise
end
end一方、resume_job の定義は次のようになっており、位置引数1つのみを受け取るメソッドです:
def resume_job(exception)
executions_for(exception)
...
endここで resume_job(exception: e) と呼んだ場合、Ruby では exception: e は キーワード引数ではなくハッシュリテラル { exception: e } と解釈され、それがそのまま位置引数 exception に渡されます。
結果として:
- 正常系(
Continuation::Interrupt経由)resume_job(e)→exceptionはStandardErrorなどの例外オブジェクト
- 標準エラー系(
StandardError経由)resume_job(exception: e)→exceptionは{ exception: e }というハッシュ
となり、resume_job 内部で使われる executions_for(exception) が異なる値を元にキーを生成してしまいます。
Active Job は exception_executions というハッシュに「特定のエラーが何回起きたか」を記録しますが、今回の挙動により、キーが以下のように不自然な文字列になっていました:
# before
{ "{exception: #<StandardError: Cursor error>}" => 1 }
# 本来期待される形 (Interrupt 経由のパスと同じキー)
{ "Cursor error" => 1 }つまり、
- Interrupt で再開した場合: 例外メッセージ(例:
"Cursor error")がキーになる - 標準エラーで再開した場合: ハッシュを
to_sした文字列("{exception: #<StandardError: Cursor error>}")がキーになる
という不整合が生じていました。
修正内容
PR の修正は非常にピンポイントで、resume_job 呼び出しを以下のように変更しています。
- resume_job(exception: e)
+ resume_job(e)これにより:
- すべての再開パス(
Continuation::Interruptと通常のStandardError)で - 例外オブジェクトが 同じく位置引数として
resume_jobに渡され executions_for(exception)が一貫して「例外オブジェクト」を受け取れる
ようになり、exception_executions のキーも、すべて exception.message (例外メッセージ)ベースで統一されます。
テストの追加
activejob/test/cases/continuation_test.rb に 13 行のテストが追加され、次のことを検証しています:
- 継続可能ジョブが途中まで進んだ後にエラーを発生させ
resume_errors_after_advancing?が有効で再開されるケースにおいてexception_executionsのキーが、例外メッセージ(Interrupt 経由のパスと同じ形式)で記録されること
これにより、修正が仕様として固定され、回 regress しにくくなっています。
- 影響範囲・注意点
影響範囲:
ActiveJob::Continuableを利用しているジョブで、途中で進捗を記録した後、StandardError系の例外で再開されるパスに影響します。- 特に、
exception_executionsの値(キーが何か)を見て独自にモニタリングしているコード、メトリクス集計、再試行制御をしているコードがある場合、キーの形式が変わるため注意が必要です。
挙動の変化:
- 以前:
StandardError由来の再開では、キーが"{exception: #<StandardError: Some message>}"のような文字列になっていた。 - 以後: Interrupt 由来と同様に、例外メッセージ(例:
"Some message")をキーとして扱うようになる。 - これにより、「同じエラーなのにパスによって別キーになる」という問題が解消され、再試行カウントなどが正しく集計されやすくなります。
- 以前:
後方互換性:
- メソッドシグネチャは元々位置引数 1 つのみだったため、API 的な破壊的変更はありません。
- すでにキースキーマを前提にしたストレージやダッシュボードがある場合は、キーの変化を考慮した移行(過去データとの付き合わせなど)が必要になる可能性があります。
- 参考情報 (あれば)
- 対象クラス:
ActiveJob::Continuable(activejob/lib/active_job/continuable.rb) - 関連概念:
exception_executions: 例外ごとの実行回数を管理しているハッシュresume_errors_after_advancing?: 進捗済みでもエラー後にジョブを再開するかどうかのフラグContinuation::Interrupt/Continuation::Error: 継続処理に関連する内部的な例外クラス
この PR は、実装の意図(resume_job の引数名が exception であること)と実際の呼び出しを一致させる、小さいながらも意味のあるバグ修正です。
#57728 Clear enqueue_error when re-enqueuing a job
マージ日: 2026/6/14 | 作成者: @55728
- 概要 (1-2文で)
Active Job の同一ジョブインスタンスを再キューイングした際、前回の失敗時のenqueue_errorが残り続けてしまう問題を修正し、再キューイング時にenqueue_errorをクリアするようにした PR です。これにより、ジョブが実際には正常にキューイングされたにもかかわらず、監視やログに例外が発生したかのように記録される不整合が解消されます。
- 変更内容の詳細
問題の背景
ActiveJob::Enqueuing#enqueue は毎回 successfully_enqueued をリセットしていますが、enqueue_error はリセットしていませんでした。
疑似コードイメージ:
def enqueue(options = {})
set(options)
self.successfully_enqueued = false
# self.enqueue_error は触っていない
raw_enqueue
...
endそのため、以下のようなケースで状態が矛盾していました。
job.enqueue # adapter が EnqueueError を raise -> false を返す
job.enqueue # 2回目は成功 -> job を返す
job.successfully_enqueued? # => true
job.enqueue_error # => #<ActiveJob::EnqueueError ...> (本来は nil であるべき)さらにこの矛盾は、ActiveJob::StructuredEventSubscriber#enqueue にも影響します。このクラスではイベントの exception を次のように決めています:
exception = event.payload[:exception_object] || job.enqueue_error2回目の enqueue が成功したケースでは event.payload[:exception_object] は nil になりますが、job.enqueue_error に前回の例外が残っているため、
- 実際には成功した enqueue に対して
exception_class/exception_messageがイベントとして発火されてしまう
という誤ったログ・メトリクスが出る状況になっていました。
修正内容
この PR では、enqueue 前に enqueue_error もクリアするようにしています。
def enqueue(options = {})
set(options)
self.successfully_enqueued = false
self.enqueue_error = nil # ← 新しく追加
raw_enqueue
...
endこれにより、
- 2回目の enqueue が成功した場合
successfully_enqueued?はtrueenqueue_errorはnil
となり、状態が一貫します。
テスト
新規テストでは、同一インスタンスに対して
- 1回目: adapter が EnqueueError を返すようにして enqueue を失敗させる
- 2回目: 成功するようにして enqueue する
という流れを再現し、
assert job.successfully_enqueued?
assert_nil job.enqueue_errorを検証しています。
- 影響範囲・注意点
影響範囲
ActiveJob::Enqueuing#enqueueを使っている全てのジョブクラスが対象ですが、- 変更は「enqueue 開始時に
enqueue_errorをnilにリセットする」だけです。
- 変更は「enqueue 開始時に
- 特に、同じジョブインスタンスを使い回して再キューイングするケース や、 キューイング失敗時のエラー情報を
enqueue_error経由で参照しているコード に影響します。 ActiveJob::StructuredEventSubscriberを使ったログ出力やメトリクス収集では、- これまで「成功した enqueue なのに例外付きでイベントが飛んでいた」ような誤検知が解消されます。
注意点
- 「直前の enqueue 失敗のエラーを、後続の enqueue 成否に関わらずジョブインスタンス内に残したい」という特殊な用途がもしあれば、そのユースケースには合わなくなります(しかし一般的には、ジョブの状態は最新の enqueue の結果を反映すべきなので、この変更が正しい挙動と言えます)。
- もしアプリ側で
enqueue_errorを参照しているコードがある場合は、- 「どの enqueue の結果を見たいのか」を明確にし、必要であれば独自にログや別フィールドに保存する実装が望まれます。
- 参考情報 (あれば)
- 関連クラス/メソッド
ActiveJob::Enqueuing#enqueueActiveJob::EnqueueErrorActiveJob::StructuredEventSubscriber#enqueue
- 想定ユースケース
- flaky な adapter(外部キューサービス一時障害など)に対して、同じジョブインスタンスを再利用してリトライするケース
- enqueue の成功/失敗を監視基盤(構造化ログ、APM など)に送っている環境でのノイズ削減
#57725 Include adapter in Active Job perform_start event payload
マージ日: 2026/6/14 | 作成者: @55728
- 概要 (1-2文で)
Active Job のperform_start構造化イベントのペイロードにadapter情報が含まれていなかった不整合な挙動を修正し、他のイベント(enqueue,enqueue_at,enqueue_all,perform)と同様にアダプタ名を含めるようにした PR です。これによりログや構造化イベントの利用時に、ジョブ実行開始時点でも正しいアダプタ名が参照できるようになります。
- 変更内容の詳細
問題の背景
Active Job には、構造化イベントを発行する ActiveJob::StructuredEventSubscriber があり、以下のようなイベントでペイロードに adapter キーを含めています。
enqueueenqueue_atenqueue_allperform(= 完了)
ところが perform_start だけが adapter キーをペイロードに含めておらず、結果として:
active_job.startedイベントにはadapterが無いactive_job.completedイベントにはadapterがある
という不整合な状態になっていました。
ActiveJob::LogSubscriber#started は、イベントペイロードから :adapter と :queue を読み取り、adapter(queue) という形の文字列に整形してログを出力しますが、perform_start 側のペイロードに adapter が無いせいで、開始ログだけアダプタ名が欠落していました。
挙動の例
Async アダプタでジョブを実行したときのログ:
Performing TestJob (Job ID: ...) from (default)
Performed TestJob (Job ID: ...) from Async(default) in 0.3ms期待されるのは以下のように両方にアダプタ名が含まれることです:
Performing TestJob (Job ID: ...) from Async(default)
Performed TestJob (Job ID: ...) from Async(default) in 0.3ms根本原因
内部的には、Active Job のインストルメンテーション層 (Instrumentation#instrument) がすでに payload[:adapter] = queue_adapter を設定しており、通知イベント自体には adapter が入っている状態です。
問題は ActiveJob::StructuredEventSubscriber#perform_start が、その event.payload[:adapter] を構造化イベント用のペイロードに転記していなかった点にあります。
具体的な修正内容
perform_start において、他のイベントと同様に adapter をペイロードに含めるよう修正されています。
変更後のイメージ:
def perform_start(event)
job = event.payload[:job]
adapter = event.payload[:adapter]
payload = {
job_class: job.class.name,
job_id: job.job_id,
queue: job.queue_name,
adapter: ActiveJob.adapter_name(adapter),
enqueued_at: job.enqueued_at&.utc&.iso8601(9),
}
# この payload を使って structured event を発行
endポイント:
event.payload[:adapter]をローカル変数adapterに取り出し、- 他イベント同様
ActiveJob.adapter_name(adapter)でアダプタ名へ正規化し、 payload[:adapter]に含めています。
テストの変更
activejob/test/cases/structured_event_subscriber_test.rb の test_perform_start_job を更新:
- 以前は
job_classとqueueしか検証しておらず、adapter欠落を検知できなかった。 - 本 PR により
perform_startイベントのペイロードにadapterが含まれていることを明示的に期待・検証するように変更。 - これで
enqueue/enqueue_atのテストと同等レベルのアサーションになり、再発防止になります。
テスト結果(ローカル実行例):
activejob/test/cases/structured_event_subscriber_test.rb -> 11 runs, 24 assertions, 0 failures
activejob/test/cases/logging_test.rb -> 39 runs, 186 assertions, 0 failures- 影響範囲・注意点
ログ出力の変化
active_job.started(=ジョブ実行開始)に対応するログ行が、これまでよりも情報リッチになります。- 具体的には
"from (default)"のようにアダプタ名なしだった部分が"from Async(default)"のようにアダプタ名付きになります。 - ログ文言をパースしているツールがある場合、
from <adapter>(<queue>)の形式を前提としていれば、今回の変更でより一貫性のある入力が得られます。
構造化イベント利用者への影響
active_job.startedイベントのペイロードにadapterキーが新たに追加されます。- すでに
active_job.enqueueなどの sibling イベントでadapterを利用している監視・メトリクス基盤であれば、startedに対しても同様の集計(アダプタ別のメトリクスなど)が可能になります。 adapterが増えるのは additive な変更であり、既存コードがadapterを前提としていない限り壊れることはありません(nil でなくなったことで問題が出るケースは基本的に考えづらい)。
リリース状態
- この構造化イベント /
EventReporter経由のログサブスクライバは、まだ正式リリースタグには入っていない部分とのことで、公開済みバージョン上の「回 regress」ではなく、そもそも初期実装時の欠落を修正した位置づけです。 - そのため CHANGELOG には記載されていません。
- この構造化イベント /
- 参考情報 (あれば)
- 対象クラス:
ActiveJob::StructuredEventSubscriberActiveJob::LogSubscriber(特に#started→queue_nameヘルパ)
- 関連イベント:
active_job.enqueueactive_job.enqueue_atactive_job.enqueue_allactive_job.started(今回修正対象)active_job.completed
- アダプタ名の解決:
ActiveJob.adapter_name(adapter)により、Async,Inline,Sidekiqなどの文字列表現に正規化されます。
#57688 Run Action Cable JS tests with Web Test Runner
マージ日: 2026/6/14 | 作成者: @matthewd
- 概要 (1-2文で)
Action Cable の JavaScript テスト実行環境を、非推奨となった Karma から Web Test Runner + Sauce Labs を使う構成に移行した PRです。既存の QUnit テストスイートを壊さずに、新しいテストランナー経由でブラウザテストを実行できるようにしています。
- 変更内容の詳細
2-1. Karma からの移行
actioncable/karma.conf.jsを削除 (中身 63 行すべて削除)- Karma ベースの設定一式を廃止
- これにより、Action Cable の JS テストは Karma 経由では動かなくなり、今後は Web Test Runner 経由のみを想定
actioncable/package.jsonの変更 (+6/-7)- 主な内容(推測を含む)
- devDependencies から
karma,karma-*関連パッケージを削除 - 代わりに
@web/test-runner,@web/test-runner-sauce等を追加 - テストスクリプトを Karma から Web Test Runner に切り替え
例(イメージ):jsonc{ "scripts": { // 旧: // "test": "karma start karma.conf.js" // 新: "test": "node test/javascript/run_web_test_runner.mjs" } } - QUnit 自体は現行のテストスイートを使い回すため、そこは温存。Web Test Runner 経由で QUnit を動かすアダプタ構成に。
- devDependencies から
- 主な内容(推測を含む)
2-2. Web Test Runner 実行スクリプトの追加
actioncable/test/javascript/run_web_test_runner.mjs(+26)- Web Test Runner を起動するためのエントリポイントスクリプト
- 概ね以下のようなことを行う構成が想定されます:
web-test-runner.config.mjsを読み込んで設定を適用- CI かローカルかなどの条件に応じて、起動オプションを調整
- Node からプログラム的に
@web/test-runnerを実行し、終了コードをそのままプロセスの exit code に反映
- これにより、
yarn testや CI の job から統一的に Web Test Runner を叩けるようになる
簡易イメージ(あくまで参考):
// test/javascript/run_web_test_runner.mjs
import { startTestRunner } from '@web/test-runner';
import config from '../../web-test-runner.config.mjs';
const result = await startTestRunner({ config });
process.exit(result.failed ? 1 : 0);2-3. Sauce Labs 連携の設定
actioncable/test/javascript/sauce_labs.mjs(+278)- Sauce Labs 上でブラウザテストを実行するための設定・ヘルパーロジック一式
- 主な内容(推測を含む):
- Sauce Labs のブラウザ環境定義(Chrome / Firefox / Safari / Edge 等のバージョン行列)
- 環境変数 (
SAUCE_USERNAME,SAUCE_ACCESS_KEYなど) から認証情報を取得 - CI ブランチ・PR かどうかに応じて、テスト対象ブラウザや並列度を切り替え
- Web Test Runner に渡す Launcher 設定 (SauceLauncher 的なもの) の生成
- Karma 時代に使っていた Sauce 連携ロジックがあれば、それの Web Test Runner 版に相当
イメージコード(抜粋イメージ):
// test/javascript/sauce_labs.mjs
import { sauceLauncher } from '@web/test-runner-sauce';
export function createSauceLaunchers() {
return {
chromeLatest: sauceLauncher({
browserName: 'chrome',
platformName: 'Windows 11',
browserVersion: 'latest',
// ...
}),
// 他ブラウザ...
};
}2-4. Web Test Runner の設定
actioncable/web-test-runner.config.mjs(+65)- Web Test Runner のメイン設定ファイル
- 主な要素(推測を含む):
- テスト対象ファイルパターン(例:
test/javascript/**/*_test.js) - QUnit を使うためのブラウザ側インポート・セットアップ
- ローカル実行時は
puppeteeror Playwright ベースの headless Chrome/Firefox を起動 - CI で Sauce Labs を使う場合は
sauce_labs.mjsから Launcher を読み込み、browsersに指定 - タイムアウト、並列実行数、カバレッジ設定など
- テスト対象ファイルパターン(例:
例イメージ:
// web-test-runner.config.mjs
import { playwrightLauncher } from '@web/test-runner-playwright';
import { createSauceLaunchers } from './test/javascript/sauce_labs.mjs';
const isCI = process.env.CI === 'true';
export default {
files: 'test/javascript/**/*_test.js',
nodeResolve: true,
browsers: isCI
? Object.values(createSauceLaunchers())
: [playwrightLauncher({ product: 'chromium' })],
testFramework: {
config: {
ui: 'qunit',
},
},
};2-5. 依存関係 (yarn.lock) の大幅更新
yarn.lock(+3109/-840)- Karma 関連ライブラリの削除、新規に導入した Web Test Runner + Sauce 関連ライブラリのロックが大量に追加
- これに伴い transitive dependencies も大きく変動
- ランナーやブラウザドライバ関連のパッケージが追加されている想定
- 影響範囲・注意点
テスト実行コマンドの変更
- これまで
yarn test等で Karma を前提としていた場合、今後は Web Test Runner が使われます。 - 開発者ローカルで Action Cable の JS テストを回すときは、PR の変更後の
package.jsonに書かれている新しいコマンドを確認する必要があります。
- これまで
CI 設定の更新必須
- CI で
karma start karma.conf.jsを呼んでいたジョブは失敗するため、node actioncable/test/javascript/run_web_test_runner.mjs- または
yarn test(PR で定義された新しい script)
に切り替える必要があります。
- また、Sauce Labs 用の環境変数 (ユーザ名・アクセストークンなど) が CI 上で適切に設定されていることが前提です。
- CI で
テストの見た目と挙動が変わる可能性
- テストランナーの UI / ログ形式が Karma から Web Test Runner に変わるので、
- ログの読み方
- テスト失敗時のスタックトレースのフォーマット
- ブラウザコンソール出力の扱い
などが変わる可能性があります。
- ただしテスト本体は QUnit のままのため、テストコードの API レベルの変更はほぼありません。
- テストランナーの UI / ログ形式が Karma から Web Test Runner に変わるので、
フレークテスト調査の前段階
- PR 説明にもある通り、「既存 CI ジョブで発生しているフレーク (flaky) な失敗調査をする前に、まずはサポートされているツールチェーンに乗せ換える」ことが目的。
- そのため、この PR マージ後もしばらくはテストが不安定な可能性は残っており、今後の PR で flakiness 改善が図られる想定です。
ローカル環境差異
- ローカルでブラウザテストを実行する際に、Node / npm / yarn のバージョンや OS の違いで動作が変わる可能性があります。
- 新しく導入された Web Test Runner / Playwright / Sauce 関連依存が Node のサポートバージョンを制限する場合があるため、開発環境の Node バージョンは(Rails リポジトリの推奨に)合わせておく方が安全です。
- 参考情報 (あれば)
- PR 本文で引用されている通り、Karma は現在非推奨:
- Web Test Runner:
- Sauce Labs 用 Web Test Runner Launcher:
この PR は「テストランナーの世代交代」が主目的で、テスト内容そのものを大きく変えないまま、Action Cable の JS テストを将来もメンテ可能な基盤に載せ替える変更と位置付けられます。
#57687 Assortment of CI/test fixes
マージ日: 2026/6/14 | 作成者: @matthewd
- 概要 (1-2文で)
CI とテスト周りの安定性を向上させるための修正がまとめて行われ、特に Active Record のコネクションプール切断時の例外ハンドリングが「例外を報告して処理は継続する」挙動に変更されました。その他は主にテストコード側の調整・リファクタリングで、本体 API の互換性を壊す変更は原則ありません。
- 変更内容の詳細
2-1. connection_pool 切断時の例外ハンドリング (lib 側唯一の本体変更)
対象:activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
これまで、プールの disconnect 中に発生した例外は、そのままテストや呼び出し元に伝播して CI を落とす原因になりうる状態でした。
この PR では、**「例外をログ等に report した上で、プールの切断処理自体は継続する」**方針に変更されています。
概念的には次のようなイメージです(擬似コード):
def disconnect!
with_connection do |conn|
# 以前はここで raise されると全体が中断していた
conn.disconnect!
rescue => e
# 今回の変更: フレームワークとして例外を報告しつつ、処理は続行
ActiveSupport::Notifications.instrument("connection_pool_disconnect.error", error: e)
# あるいは logger.error などで報告し、プールのクリーンアップは続ける
end
endPR の説明文にある通り、「プールを明示的に切断する」というのはサポートされているフレームワークの挙動/ API だが、実際には Rails 自身のテストスイートが最大の利用者の一つという前提があり、テストの安定性を重視した変更といえます。
期待される効果:
- 切断処理中の一時的な例外(接続先 DB が既に落ちている等)が、テスト全体を不安定にしない
- 呼び出し側は「切断要求はベストエフォートで実行され、失敗はログまたは通知経由で観測する」スタイルになる
2-2. 各アダプタのテスト修正
対象:
activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rbactiverecord/test/cases/adapters/trilogy/trilogy_adapter_test.rbactiverecord/test/support/adapter_helper.rb
主に CI での不安定要因を減らすための修正が含まれていると考えられます。
具体的には:
- 切断・再接続・トランザクション周りで、DB 状態に依存してフレークしやすいテストの条件や期待値の調整
- Trilogy / PostgreSQL 固有の挙動に合わせて、アサーションや前提条件を整理
- テストヘルパの設定値(接続情報やスキップ条件)の微修正
これらにより、異なる環境/DB バージョンでも一貫した成功を得やすくなります。
2-3. connection_pool_test の拡充・修正
対象:activerecord/test/cases/connection_pool_test.rb (+94/-30)
今回の本体変更に直結する部分で、テストがかなり増えています。
想定される内容:
- 切断時に例外を発生させるダミー接続オブジェクトを用意して、
- 例外が「報告される」こと
- それでもプールのクリーンアップ処理が継続すること
を検証するテスト
- 複数の接続を持つプールに対して、一部の切断のみが失敗しても、他の接続やプールオブジェクトの状態が破綻しないことの確認
- マルチスレッド/並列環境での切断時挙動に関する安定性テスト
このファイルが最も大きく増えており、「report-and-continue」ポリシーの仕様をテストで明示した位置づけになっています。
2-4. fixtures / relation / strict_loading / transactions 関連テストの調整
対象:
activerecord/test/cases/fixtures_test.rbactiverecord/test/cases/relation/or_test.rbactiverecord/test/cases/strict_loading_test.rbactiverecord/test/cases/transactions_test.rbactiverecord/test/fixtures/pirates.yml
主に以下のような細かいチューニングが想定されます:
- テストデータ (
pirates.yml) の調整:- テストケースに必要なフィールド値の修正
- DB 制約や strict loading 仕様の変更/明確化にあわせて意図した状態に揃える
transactions_testの大幅な整理 (+21/-53):- 不安定なアサーションの削除・簡略化
- DB アダプタ差異を吸収した期待値の見直し
- 「トランザクション中に例外が出た場合」の挙動テストを、現在の実装と一致するように修正
strict_loading_test/relation/or_test:- strict loading や
orクエリの仕様が変わったわけではなく、 - テスト側の前提(事前ロード・関連の有無など)を現行挙動に沿うように修正
- strict loading や
これらは基本的に テストが実際の挙動とズレていた部分を修正するものであり、API 側の破壊的変更を伴うものではないと見てよいです。
2-5. Active Support / Railties / Guides まわりのテスト修正
対象:
activesupport/test/evented_file_update_checker_test.rb(+9/-2)guides/test/epub_test.rb(+0/-3)railties/test/isolation/abstract_unit.rb(+7/-2)railties/test/isolation/test_helpers_test.rb(+22/-0)
想定される変更点:
evented_file_update_checker_test.rb:- OS ファイルシステム差異やタイミング依存で落ちやすいテストに対し、待ち時間やイベント検知条件を調整
- CI 環境での flakiness を減らす目的
- Guides の
epub_test.rb:- CI でサードパーティツールや外部依存が原因で失敗しないように、テストケースやアサーションを簡略化
- Railties の isolation テスト:
abstract_unit.rbとtest_helpers_test.rbで、テストアプリケーションの起動・終了や環境変数/ロードパス操作をより安全に行うためのヘルパ強化- テストプロセス間の汚染(環境変数、グローバル状態など)が他のテストに波及しないようにする
これらはすべて「CI が安定して通るようにするためのテスト基盤側の改善」です。
- 影響範囲・注意点
- 本体 API への主な影響は connection_pool の切断時挙動のみ
- disconnect 系メソッド呼び出しで例外が起きた場合、今後は「例外がログ/通知されつつ、処理は続行」されることが前提になります。
- 以前、disconnect での例外を rescue して独自処理していたコードは、「そもそも例外が上がってこない」可能性があるため、挙動を確認してください。
- とはいえ、この API の強い利用者は Rails 自身のテストスイートであり、アプリケーションコードで直接
connection_poolを操作しているケースは多くありません。
通常のアプリケーションでは、実質的な挙動差はほぼ感じないはずです。 - テストコードが多く変更されていますが、いずれも 既存の実装に合わせてテストを直したり、CI の不安定さを解消したりするもので、アプリケーションコード側の対応は原則不要です。
- 参考情報 (あれば)
- PR 本文の要点:
- 「
libの変更は 1 点のみで、AR プール切断中の例外は report-and-continue にした」 - 「この API 自体は公式な挙動だが、最大の利用者はおそらく Rails のテストスイート自身」
- 「
- もし自前で connection pool を直接扱っている場合は、
ActiveRecord::Base.connection_pool.disconnect!/clear_active_connections!/clear_all_connections!周りの挙動がどう変わるかを、一度ログ出力などで確認すると安心です。
#57723 Fix retry_on wait: proc with an optional argument
マージ日: 2026/6/14 | 作成者: @55728
- 概要 (1-2文で)
ActiveJob.retry_onのwait:オプションに渡すProcが「省略可能な引数1つ」を取る場合(->(executions = 0) { ... }など)にArgumentErrorが発生していた不具合を修正する PR です。Proc#arityの扱いを見直し、1引数・可変長引数・2引数の各パターンで後方互換性を保ちながら適切な呼び出しが行われるようになりました。
- 変更内容の詳細
背景となる仕様
ActiveJob の retry_on は、再実行時の待機時間を wait: オプションで指定できます:
retry_on SomeError, wait: ->(executions) { executions * 2 }この wait: には以下の2系統のプロックがサポートされています:
1引数:
->(executions) { ... }executions: 何回目の実行か(1回目=1, 2回目=2...)
2引数:
->(executions, error) { ... }executions: 上と同じerror: 発生した例外オブジェクト
もともとの実装では、Proc#arity を使って「1引数なのか、2引数(以上)なのか」を判定していました。
既存の問題点
元コード(説明文中の旧実装)はおおよそ次のようなロジックでした:
if algorithm.arity == 1
algorithm.call(executions)
else
algorithm.call(executions, error)
endRuby の Proc#arity は以下のような振る舞いをします:
->(x) {}# arity == 1->(x, y) {}# arity == 2->(x = 0) {}# arity == -1 (「必須引数1個、オプションあり」)->(x, *rest) {}# arity == -2 (「必須引数1個+可変長」)
そのため、->(executions = 0) { ... } のような「オプション引数1つ」の待機時間プロックを渡すと arity == -1 になり、else 側で
algorithm.call(executions, error)と 2引数で呼ばれてしまい ArgumentError になる というバグがありました。
修正内容
新しいロジックでは、「その callable が 2つ目の引数を安全に受け取れるか」を基準に判断するように変更されています:
if algorithm.arity == 2 || algorithm.arity < -1
algorithm.call(executions, error)
else
algorithm.call(executions)
endこの判定が意味するところは以下です:
algorithm.arity == 2
→ 「引数2つを必須で取る」:->(executions, error) { ... }のようなケース
→executions, errorを渡して呼ぶalgorithm.arity < -1
→ 「可変長引数を持ち、2つ以上の引数を受けられる」: 例->(executions, *rest) { ... }# arity == -2->(*args) { ... }# arity == -1 ではなく -n のパターン
→executions, errorで呼んでも許容される
上記以外(
arity == 1またはarity == -1)->(executions) { ... }# 1->(executions = 0) { ... }# -1(今回問題になっていた形) → 1つ目の引数のみを渡す (executionsのみ)
結果として、説明にあった通り次のような対応になります:
->(executions)(1) と->(executions = 0)(-1) → 1引数で呼び出す->(executions, error)(2) と->(executions, *rest)(-2) → 2引数で呼び出す
これにより、「もともと1引数のつもりで書いたが、デフォルト値を付けたくなった(->(executions = 0))というコード」が、従来通り 1引数の扱い をされるようになります。
テストの追加
以下のテストが追加されています:
activejob/test/jobs/retry_job.rbジョブに次のような宣言を追加:
rubyretry_on SomeError, wait: ->(executions = 0) { executions * 2 }
activejob/test/cases/exceptions_test.rb- 上記のジョブについて、
AJ_ADAPTER=test環境で
「期待したスケジュールでリトライされるか」を検証するテストを追加。
- 上記のジョブについて、
このテストは、修正前はリトライ中に ArgumentError が発生してレッドになり、修正後はグリーンになることが確認されています。既存の 1 引数・2 引数 wait: プロックのテストも引き続きグリーンで、後方互換性も保たれています。
- 影響範囲・注意点
影響範囲
- ActiveJob の
retry_on/discard_on(exceptions.rbで使われる同系のロジック)におけるwait:プロックの呼び出しに限定されます。 wait:に- オプション引数1つ (
->(executions = 0) { ... }) - 可変長引数 (
->(executions, *rest) { ... }など) を使っている場合に挙動が変わる(あるいは初めて期待通りに動く)可能性があります。
- オプション引数1つ (
- ActiveJob の
後方互換性
- 既存の以下のパターンはそのまま動作します:
->(executions) { ... }… 1引数として呼ばれる->(executions, error) { ... }… 2引数として呼ばれる
- 「今までたまたま動いていた変なケース」があるとすれば、例えば
->(*args) { ... }など、引数を何でも受け入れるような形で、arityが -1 未満になるケースでも 2 引数で呼ばれます。
通常の使い方では問題にならないはずですが、もしwait:で*argsを前提とした特殊な処理をしている場合は、一応確認すると安心です。
- 既存の以下のパターンはそのまま動作します:
注意点
wait:プロックの設計としては:- 「エラーに依存しない」待機時間 → 1引数 or オプション1引数で
->(executions = 0) { ... } - 「発生した例外に応じて変えたい」待機時間 → 2引数以上で
->(executions, error) { ... }と明示的に書き分けるのがよいです。
- 「エラーに依存しない」待機時間 → 1引数 or オプション1引数で
Proc#arityの仕様に依存した分岐であるため、「より複雑なシグネチャ(キーワード引数など)」をwait:に与えるのは避ける方が無難です(現状サポート範囲外と考えた方がよい)。
- 参考情報 (あれば)
- 該当コード:
activejob/lib/active_job/exceptions.rbretry_on/discard_onの内部でwait:オプションを解釈している部分の 1 行のみが変更されています。 - Ruby の
Proc#arity仕様(ざっくり):- 正の値: 必須引数の数
- 0: 引数なし
- 負の値:
-(必須引数 + 1)
例:->(x = 0) {}# -1 (必須1 + 可変/オプション)->(x, *rest) {}# -2 (必須1 + 可変)
- この PR はこの仕様を踏まえて「2つ目の引数を安全に渡せるかどうか」を判定しています。
#57715 Exclude all composite primary key components from content_columns
マージ日: 2026/6/14 | 作成者: @55728
- 概要 (1-2文で)
content_columnsが複合主キー(composite primary key, CPK)のカラムを正しく除外できていなかった問題を修正し、CPK の全ての主キー構成カラムを確実に除外するようにしました。単一主キーの場合の挙動・ドキュメントと一貫した動作になります。
- 変更内容の詳細
何が問題だったか
元の実装:
def content_columns
@content_columns ||= columns.reject do |c|
c.name == primary_key ||
c.name == inheritance_column ||
c.name.end_with?("_id", "_count")
end.freeze
end単一主キーの場合
primary_key # => "id"なので、c.name == primary_keyで"id"カラムは除外される。複合主キーの場合
primary_key # => ["author_id", "id"]のように Array を返すため、rubyc.name == primary_key # String == Array → 常に falseとなり、主キーのどのカラムもこの条件では除外されない。
たまたま動いていたケース
author_idは「_idで終わる」のでc.name.end_with?("_id", "_count")で除外される。- しかし
"id"は_idで終わらないため、どの条件にもマッチせず残ってしまう。
その結果:
Cpk::Book.primary_key # => ["author_id", "id"]
Cpk::Book.content_columns.map(&:name)
# => ["id", "title", "revision"] # 本来は PK を除いた ["title", "revision"] が期待ドキュメント上も「primary id は除外される」とされているため、仕様に反する挙動になっていました。
どのように直したか
primary_key を常に Array として扱う Rails 既存のイディオムに合わせて修正:
# 修正後の条件部分イメージ
Array(primary_key).include?(c.name) ||
c.name == inheritance_column ||
c.name.end_with?("_id", "_count")ポイント:
Array(primary_key)は以下のようにふるまう:- 単一主キー:
primary_key == "id"→Array("id") # => ["id"] - 複合主キー:
primary_key == ["author_id", "id"]→ そのまま["author_id", "id"] - 主キーなし:
primary_key == nil→Array(nil) # => []
- 単一主キー:
- これにより:
- CPK の全ての構成カラムが
include?によって確実に除外される - 単一主キーでも
"id"が正しく除外され続ける - 主キーなしモデルでは PK による除外条件は空配列になり、これまで通り「常に false」で影響なし
- CPK の全ての構成カラムが
テスト
Cpk::Book(primary key[:author_id, :id]) に対して:rubyとなることをCpk::Book.content_columns.map(&:name) # 修正前: ["id", "title", "revision"] # 修正後: ["title", "revision"]test/cases/base_test.rbで検証(PR 説明中)。- 既存の単一主キー (
Topic) に対するcontent_columnsのテストはそのままグリーン。
コード変更自体は:
activerecord/lib/active_record/model_schema.rbの条件式をc.name == primary_key→Array(primary_key).include?(c.name)に 1行差し替え- テスト (
activerecord/test/cases/reflection_test.rb) に CPK ケースを追加 (+5 行)
- 影響範囲・注意点
影響する箇所:
ActiveRecord::Base#content_columnsを利用しているコード全般- 管理画面ジェネレータや scaffold 系コード
content_columnsベースで「入力フォーム用カラム」や「表示カラム」を自動列挙しているような実装
具体的な挙動変化:
- 複合主キーのモデルのみ 影響を受ける。
- 以前: CPK のうち、
_idや_countで終わらない主キー(例:"id","uuid"など)がcontent_columnsに含まれてしまうことがあった。 - 変更後: それらも 全て
content_columnsから除外される。
- 以前: CPK のうち、
- 単一主キー / 主キーなしモデルの挙動は、論理的には従来仕様と同等。
- 複合主キーのモデルのみ 影響を受ける。
注意点:
- もし既存アプリケーションで「
content_columnsに主キーの一部が含まれていたこと」を前提にしていた場合(特に CPK モデル)、この PR 適用後にそのカラムが取れなくなります。- 例:
content_columnsを使って「リソースの ID も含めた表示カラム一覧」を作っていたようなケース。
- 例:
- CPK を使っていて、
content_columnsを利用している箇所がないか確認すると安全です。
- もし既存アプリケーションで「
- 参考情報 (あれば)
- この PR で使われている
Array(primary_key)は、以下のような CPK 対応箇所でも使われている既存パターン:activerecord/lib/active_record/model_schema.rbactiverecord/lib/active_record/persistence.rbactiverecord/lib/active_record/relation/batches.rbactiverecord/lib/active_record/relation/calculations.rb
- 複合主キーを使う際に、
primary_keyを直接文字列前提で比較せず、Array(primary_key)経由で扱うのが Rails 内部実装の標準的な流儀になっています。
#57714 Treat remove_column with options but no type as irreversible
マージ日: 2026/6/14 | 作成者: @55728
- 概要 (1-2文で)
remove_columnに「型なし+オプションあり」で書かれたマイグレーションが、これまで「可逆」と誤判定されて壊れた逆操作 (add_column) を生成していた問題を修正し、正しくIrreversibleMigrationを投げるようにした PR です。これによりロールバック時の謎の例外ではなく、明示的な「型がないので不可逆」というエラーメッセージが得られるようになります。
- 変更内容の詳細
何が問題だったか
remove_column の逆操作は CommandRecorder で定義されていますが、その可逆性チェックが不完全でした。
元の実装:
def invert_remove_column(args)
raise ActiveRecord::IrreversibleMigration, "remove_column is only reversible if given a type." if args.size <= 2
super
endargs は記録された remove_column の引数配列です。
- 正しく可逆なケース:ruby
remove_column :users, :name, :string, null: false # 記録される args: [:users, :name, :string, { null: false }] # args.size == 4 → ガードを通過(OK) - 問題になるケース:ruby
remove_column :users, :name, null: false # 記録される args: [:users, :name, { null: false }] # args.size == 3 → 「型がある」と誤解してガードを通過してしまう
この場合 super(親実装)が逆操作を次のように組み立てます:
[:add_column, [:users, :name, { null: false }], block]
# table name "type" options と解釈されてしまうつまり「本来は :string のような型が来るべき位置に options ハッシュが入り込む」ため、ロールバック時の add_column 呼び出しが壊れます。
結果として、ロールバック時には本来出したかった
remove_column is only reversible if given a type.
ではなく、以下のような意味不明な例外が発生していました。
- 位置引数とキーワード引数の扱いによる
ArgumentError: wrong number of arguments (given 2, expected 3) - あるいは
NoMethodError: undefined method 'to_sym' for an instance of Hash
修正内容
兄弟メソッドである invert_remove_columns は、既に「型の有無」をより厳密に判定しており、
args[-1].is_a?(Hash) && args[-1].has_key?(:type)という形で「最後の引数が Hash で、その中の :type キーで型が指定されている」ケースのみを「型あり」とみなしていました。
今回の修正では、invert_remove_column も同様の方針に寄せ、
def invert_remove_column(args)
if args.size <= 2 || args[2].is_a?(Hash)
raise ActiveRecord::IrreversibleMigration, "remove_column is only reversible if given a type."
end
super
endと変更しています。
ポイント:
args.size <= 2
→ 旧来どおり、そもそも 3 つ目の引数(型に相当)が無い場合は不可逆。args[2].is_a?(Hash)
→ 3 つ目が Hash(= options)であり、実際の型が渡されていない場合も不可逆として扱う。
つまりremove_column :users, :name, null: falseも不可逆。
これにより:
- 可逆な例:ruby
remove_column :users, :name, :string, null: false # args: [:users, :name, :string, { null: false }] # args[2] == :string(Hash ではない)→ super へ → add_column :users, :name, :string, null: false - 不可逆な例:ruby
remove_column :users, :name # args: [:users, :name] → size <= 2 → IrreversibleMigration remove_column :users, :name, null: false # args: [:users, :name, { null: false }] → args[2].is_a?(Hash) → IrreversibleMigration
テスト
activerecord/test/cases/migration/command_recorder_test.rb にテストを追加:
inverse_of(:remove_column, [:table, :column, { null: false }])
# が IrreversibleMigration を raise することを確認- 修正前: 例外が出ず、壊れた
add_columnが生成される → 実行時にクラッシュ - 修正後: 期待どおり
IrreversibleMigrationが発生
既存のテスト(型あり・完全に引数不足のケース)はそのままグリーン。
- 影響範囲・注意点
影響を受けるのは「
changeマイグレーションでremove_columnを使い、型を省略したがオプションは渡していた」ケースです:rubydef change remove_column :users, :name, null: false # これまで「可逆」と誤判定されていた endこのようなマイグレーションは、これまではロールバック時に
- 「よく分からない
ArgumentErrorやNoMethodError」でコケていたものが、 - 今後はマイグレーション起動直後に
ActiveRecord::IrreversibleMigrationで明示的に止まる
ようになります。
- 「よく分からない
対応策としては、明示的に型を指定するか、
up/downに分けて書く必要があります:ruby# 可逆にしたい場合 def change remove_column :users, :name, :string, null: false end # 不可逆前提なら def up remove_column :users, :name, null: false end def down add_column :users, :name, :string, null: false end既に本番に適用済みのマイグレーションでこのパターンがある場合:
- 以前から実質ロールバック不能だったものが、「はっきりそう表示される」ようになっただけとも言えるため、挙動としては安全側への変更です。
- ただし「今まではたまたまロールバックしなかった環境で、今回初めてロールバックを試みる」といった場合に、
IrreversibleMigrationのエラーに気付くことになります。
- 参考情報 (あれば)
- 修正対象クラス:
activerecord/lib/active_record/migration/command_recorder.rb - コントラクトのドキュメント:
command_recorder.rbにremove_column (must supply a type)と明記されており、この PR はその仕様に実装を揃えるものです。 - 類似ロジック: 複数カラム削除用の
invert_remove_columnsは既に:typeキーの有無で判定しており、今回の変更でinvert_remove_columnも同じ思想に揃いました。
#57712 Fix counter_cache_column crash when no column is given on the has_many side
マージ日: 2026/6/14 | 作成者: @55728
- 概要 (1-2文で)
Rails 7.2系で、has_many/has_one側にcounter_cache: trueやcounter_cache: { active: false }を指定した際にNoMethodError: undefined method '-@' for nilで落ちるリグレッションが修正されました。カウンターキャッシュ列が省略された場合でも、従来どおり自動でxxx_countというカラム名が使われるようになります。
- 変更内容の詳細
問題のあったコード
has_many / has_one のリフレクションでカウンターキャッシュ列名を決定するメソッド counter_cache_column が、次のようになっていました:
-((counter_cache && -counter_cache[:column]) || "#{name}_count")一方で、関連定義のオプションは normalize_options で以下のように正規化されます:
# reflection.rb#normalize_options
when Hash
active = counter_cache.fetch(:active, true)
column = counter_cache[:column]&.to_s # カラム未指定時は nil
end
options[:counter_cache] = { active: active, column: column }この結果:
counter_cache: truecounter_cache: { active: false }
のようにカラム名を明示しない場合、options[:counter_cache] は { active: true/false, column: nil } になります。
Ruby では Hash は常に truthy なので、
counter_cache && -counter_cache[:column]は必ず -counter_cache[:column] を評価しようとし、counter_cache[:column] が nil のときに -nil が実行されて NoMethodError になります。この例外は || "#{name}_count" に到達する前に発生するため、"books_count" などのデフォルト名にフォールバックできませんでした。
belongs_to 側では同様の処理が正しく書かれており、単項マイナスをカラム名そのものにはかけていません:
counter_cache[:column] || -"#{active_record.name.demodulize.underscore.pluralize}_count"ここでの単項マイナス -@ は「文字列を freeze する」ための Active Support の仕様です(-"foo" → "foo".freeze)。
修正内容
has_many / has_one 側の実装から「余計な内側の -」を取り除きました:
# 修正前
-((counter_cache && -counter_cache[:column]) || "#{name}_count")
# 修正後
-((counter_cache && counter_cache[:column]) || "#{name}_count")これにより:
counter_cache[:column]が nil なら|| "#{name}_count"にフォールバックし、- 最終結果に対してだけ単項マイナス(freeze)をかける、
という belongs_to 側と整合した動作になります。
テスト追加
activerecord/test/cases/reflection_test.rb に以下のパターンを検証するテストが追加されています:
has_manyにcounter_cache: trueを指定した場合 →"books_count"を返すことhas_manyにcounter_cache: { active: false }を指定した場合 →"books_count"を返すこと
修正前はどちらも NoMethodError で落ち、修正後にパスすることが確認されています。既存の reflection / belongs_to のカウンターキャッシュ関連テストはすべてグリーンのままです。
- 影響範囲・注意点
- 影響を受けるバージョン
- リグレッションはコミット
e79455f3d4(「バックフィル中に counter cache カラムを無視する機能の追加」)以降、Rails 7.2.0 から発生しています。
- リグレッションはコミット
- 影響を受けるコードパターン
has_many/has_one側で次のように宣言しているケース:rubyhas_many :books, counter_cache: true has_many :books, counter_cache: { active: false } # 推奨されている安全なバックフィル形- これらの関連に対して:
author.books.create!author.books.size- その他、
HasManyAssociation#update_counter_in_memoryを経由する操作 がNoMethodError: undefined method '-@' for nilで落ちる可能性がありました。
- 影響を受けないパターン
counter_cache: :custom_nameのように明示的にカラム名を指定している場合は、counter_cache[:column]がnilではないため、問題は発生しません。belongs_to側でのcounter_cache設定も、本件のバグの影響は受けません(実装が別で、もともと正しくフォールバックする)。
- 実務上の注意点
- Rails 7.2.0 〜 7.2.x を使っていて、
has_many/has_oneにcounter_cache: trueやcounter_cache: { active: false }を書いている場合は、この修正が入ったバージョンへのアップデートで例外が解消されます。 - 一時的なワークアラウンドとしては、
counter_cache: trueの代わりに明示的なカラム名を指定することでも回避できます:rubyただし、PRの修正が取り込まれたバージョンにアップデートできるなら、それが一番シンプルです。has_many :books, counter_cache: :books_count has_many :books, counter_cache: { active: false, column: :books_count }
- Rails 7.2.0 〜 7.2.x を使っていて、
- 参考情報 (あれば)
- 該当コミット(リグレッション導入元):
e79455f3d4– 「Add the ability to ignore counter cache columns while they are backfilling」 - 関連クラス / メソッド:
ActiveRecord::Reflection::AssociationReflection#counter_cache_columnActiveRecord::Reflection::AssociationReflection#normalize_optionsActiveRecord::Associations::HasManyAssociation#update_counter_in_memory
-"string"という書き方は、Active Support によるString#-@の拡張で「文字列を freeze する」省略記法であり、本PRではその freeze の振る舞い自体は維持したまま、nil ハンドリングのみを修正しています。
#57708 Clear the connections registry on server restart
マージ日: 2026/6/14 | 作成者: @55728
- 概要 (1-2文で)
このPRは、Action Cable のサーバ再起動 (Server::Base#restart) 時に接続レジストリ(connections_map)がクリアされず、クローズ済み接続オブジェクトが蓄積してしまう不具合を修正します。再起動処理の中でconnections_map.clearを実行することで、開発モードなどでの繰り返しリロード時に「死んだ接続」が溜まらないようにしています。
- 変更内容の詳細
問題の構造
現状の Server::Base#restart は以下の流れです(要点のみ):
def restart
each_connection do |connection|
connection.close(reason: ...)
end
@mutex.synchronize do
@heartbeat_timer.shutdown if @heartbeat_timer
@heartbeat_timer = nil
@worker_pool.halt if @worker_pool
@worker_pool = nil
# executor, pubsub なども tear down
end
endeach_connectionで全接続に対してconnection.closeを呼ぶ。- 各接続は
Socket#handle_close内のserver.remove_connectionによってconnections_mapから削除される設計。 - しかし
handle_closeはワーカープール上で非同期にsend_async :handle_closeされる。 restartはそのワーカープールを@worker_pool.haltで停止してしまうため、キューに溜まっていたremove_connectionコールバックが実行されない。- 結果として、既に close 済みの接続オブジェクトが
connections_mapに残り続ける。
このため:
#connectionsopen_connections_statistics
といったメソッドが、実際にはクローズ済みの接続を数え続け、かつオブジェクトを保持し続ける(リーク)状態になっていました。
特に問題になるのが開発モードのリロード:
engine.rbのbefore_class_unloadで、クラスアンロード前に毎回ActionCable.server.restartが呼ばれる。- そのたびにクローズ済み接続が
connections_mapに残り、開発セッションが長くなるほど不要な接続オブジェクトが蓄積する。
先行する #57700 でハートビートタイマーの tear down ギャップが修正されましたが、connections_map のクリアが抜けていた、という位置づけです。
修正内容
restart の tear down セクション(@mutex.synchronize ブロック内)で、connections_map を明示的にクリアするようにしました。
イメージとしては以下のような変更です:
@mutex.synchronize do
# Drop the closed connections. remove_connection normally runs on the
# worker pool, which we halt below, so the entries would otherwise leak.
connections_map.clear
@heartbeat_timer.shutdown if @heartbeat_timer
@heartbeat_timer = nil
@worker_pool.halt if @worker_pool
@worker_pool = nil
# ...executor, pubsub など既存のリセット処理...
endポイント:
connections_map.clearは@mutex下で呼ばれており、他のリソースの tear down と同じクリティカルセクションに収まっている。connections_map.delete/clearは冪等であるため、たまたま一部のhandle_closeがワーカープール停止前に走ってremove_connectionしていても、clearと競合しない(実害がない)。
テスト
actioncable/test/server/base_test.rb にテストが追加されています:
- 手順イメージ:
- サーバに接続を 1 件追加。
server.restartを呼び出す。server.connectionsが空であることをアサート。
- 修正前は、クローズ済み接続が
connections_mapに残っているためテストが失敗(red)。 - 修正後は、
connections_map.clearによりconnectionsが空になりテスト成功(green)。 - 既存の
base_test.rbのテスト群にも影響はなく、すべて green。
- 影響範囲・注意点
- 対象コンポーネント:
ActionCable::Server::Baseの再起動ロジック。 - 主な影響:
- 開発モードでのコードリロード(
before_class_unload→ActionCable.server.restart)を繰り返しても、クローズ済み接続が内部に溜まらなくなる。 #connectionsやopen_connections_statisticsが、より正確に「現在オープンしている接続」を反映するようになる。
- 開発モードでのコードリロード(
- メモリ使用:
- 既存挙動では、dev 環境で長時間開発するとクローズ済み接続オブジェクトが Action Cable サーバに保持され続け、メモリ使用量が徐々に増加しうる。
- この PR により、
restart実行ごとにレジストリをクリアするため、そうしたリーク傾向が抑制される。
- 互換性:
restart後にconnectionsを参照するコードが、以前も論理的には「接続がクローズされているべき」タイミングであるため、connectionsが空になるのは自然な挙動の修正と考えられます。restartを「ソフト再起動」と捉えて手動で何らかの状態を引き継いでいたコードがある場合は、connectionsに依存していると挙動が変わる可能性がありますが、設計的にはrestart後はクリーンな状態が期待されるため、むしろ仕様に沿った形になります。
- スレッド・ロック:
connections_map.clearは既存の@mutex内で実行されるため、新たな競合やデッドロック要因は導入していません。handle_close/remove_connectionとの整合性も冪等性により担保されています。
- 参考情報 (あれば)
- 関連 PR: #57700
- 同様に Action Cable サーバの tear down ギャップ(ハートビートタイマーの後処理漏れ)を修正した PR。
- 修正ファイル:
actioncable/lib/action_cable/server/base.rbrestart内にconnections_map.clear追加。
actioncable/test/server/base_test.rbrestart後にconnectionsが空であることを確認するテスト追加。
- CHANGELOG:
- バグフィックスのため、CHANGELOG への追記は行われていません。
#57717 Coerce seeds and use_metadata_table booleans in UrlConfig
マージ日: 2026/6/14 | 作成者: @55728
- 概要 (1-2文で)
DATABASE_URLのクエリパラメータで指定されたseedsとuse_metadata_tableが、文字列"false"のまま扱われて常に truthy になっていた問題を修正し、既存のreplica/database_tasksと同様に真偽値へ強制変換するようにした PR です。これにより、?seeds=falseや?use_metadata_table=falseが正しく false として扱われます。
- 変更内容の詳細
どのような問題だったか
UrlConfig では、DATABASE_URL のクエリ文字列から読み込んだ設定を @configuration_hash に格納しています。このとき、クエリ文字列の値はすべて文字列になるため、そのままだと "false" も Ruby では truthy です。
過去の変更ですでに以下の2つは boolean 変換されていました:
to_boolean!(@configuration_hash, :replica)
to_boolean!(@configuration_hash, :database_tasks)しかし :seeds と :use_metadata_table は変換されておらず、例えば:
postgres://host/db?seeds=falseと書いても、
configuration_hash[:seeds] #=> "false"(Stringで truthy)となり、seeds? が truthy を返してしまうため、db:prepare のシーディングを無効化できませんでした。use_metadata_table についても同様で、メタデータテーブルの使用を DATABASE_URL 経由で無効化できない状態でした。
seeds? / use_metadata_table? の定義は以下のようになっており、保存されている値をそのまま返すため、変換漏れがそのまま挙動不良になっていました:
seeds?:configuration_hash.fetch(:seeds, primary?)use_metadata_table?:configuration_hash.fetch(:use_metadata_table, true)
今回の修正内容
UrlConfig の初期化処理に、seeds と use_metadata_table も boolean 変換対象として追加しました:
to_boolean!(@configuration_hash, :replica)
to_boolean!(@configuration_hash, :database_tasks)
to_boolean!(@configuration_hash, :seeds) # ← 追加
to_boolean!(@configuration_hash, :use_metadata_table) # ← 追加これにより、以下のような URL が:
postgres://host/db?seeds=false&use_metadata_table=false内部的には:
configuration_hash[:seeds] #=> false
configuration_hash[:use_metadata_table] #=> false
config.seeds? #=> false
config.use_metadata_table? #=> falseと解釈されるようになります。
テストの追加
activerecord/test/cases/database_configurations/url_config_test.rb にテストが追加されています。内容は既存の database_tasks に対するテストを踏襲しており、以下を検証しています:
?seeds=false→seeds?がfalseを返す?use_metadata_table=false→use_metadata_table?がfalseを返す
修正前は "false"(String)が入るためテストが失敗し、修正後に通ることを確認しています。
- 影響範囲・注意点
影響範囲
DATABASE_URL経由でseeds/use_metadata_tableを指定している環境に影響します。- これまで
"false"が truthy として扱われていたため、以下のように「うっかり動いていた」可能性があります:?seeds=falseを付けていたが、実際には seeds が実行されていた?use_metadata_table=falseを付けていたが、実際にはメタデータテーブルが使われていた
破壊的な変化の可能性
- もし現状、「
seeds/use_metadata_tableを本当に true にしたいが、DATABASE_URL上の都合で?seeds=falseのような値を付けていた」という特殊な誤用をしていると、今回の修正で false として扱われるようになり挙動が変わります。 - 通常は「明示的に
falseを指定したいのに効いていなかった」という状況が正しくなる方向なので、意図せぬ挙動変更は限定的なはずです。
- もし現状、「
マイグレーション・タスクへの影響
- Rails の DB 関連タスク(
db:prepareなど)でseeds?/use_metadata_table?の値を見て挙動を変える部分に影響します。 - CI や本番で
DATABASE_URLに?seeds=falseを指定して seed 実行を抑制したい、といったユースケースが正しく動くようになります。
- Rails の DB 関連タスク(
- 参考情報 (あれば)
- 本 PR が修正している過去コミットの意図:
- 63631e2d5b: 「
schema_dump,query_cache,replica,database_tasksをDATABASE_URLで設定可能にする」- コメント上は「すべての boolean 設定を扱う」とされていたが、
seeds/use_metadata_tableが変換漏れしていたものを今回補完。
- コメント上は「すべての boolean 設定を扱う」とされていたが、
- 63631e2d5b: 「
- 関連する boolean 設定(全て
DATABASE_URLのクエリ文字列から boolean として解釈されるべきもの):schema_dumpquery_cachereplicadatabase_tasksseeds(今回追加)use_metadata_table(今回追加)
#57719 Raise RecordNotFound for find(nil) on a composite primary key model
マージ日: 2026/6/14 | 作成者: @55728
- 概要 (1-2文で)
複合主キーを持つモデルでModel.find(nil)や引数なしのModel.findを呼び出した際に、想定外のNoMethodErrorが発生していた問題を修正し、単一主キーと同様にActiveRecord::RecordNotFoundを投げるようにしたPRです。複合主キー用の内部判定ロジックに nil ガードを追加し、それに対応するテストを追加しています。
- 変更内容の詳細
問題の背景
単一主キーのモデルでは、以下のような呼び出しは:
Post.find # => ActiveRecord::RecordNotFound: Couldn't find Post without an ID
Post.find(nil) # => ActiveRecord::RecordNotFound: Couldn't find Post without an IDのように ActiveRecord::RecordNotFound が投げられるのが仕様です。
しかし、複合主キーを持つモデル (例: Cpk::Book) では:
Cpk::Book.find
Cpk::Book.find(nil)を呼ぶと、ActiveRecord::RecordNotFound ではなく:
NoMethodError: undefined method `first` for nil:NilClassが発生していました。
原因は ActiveRecord::Key::Composite#expects_multiple_ids? の実装で、引数が nil の場合の考慮がなく、nil.first を呼び出してしまっていたことです。
既存コード(問題箇所)
# Key::Composite
def expects_multiple_ids?(value)
value.first.is_a?(Array) # nil.first => NoMethodError
endこれに対し、単一主キーのパスでは nil セーフになっていました:
# Key::Single
def expects_multiple_ids?(value)
value.is_a?(Array) # nil-safe
end修正内容
複合主キー側の expects_multiple_ids? にも同様の nil ガードを追加し、配列であることと、その先頭要素も配列である場合のみ「複数IDが渡されている」とみなすように変更しています。
修正後コード
def expects_multiple_ids?(value)
value.is_a?(Array) && value.first.is_a?(Array)
endこれにより:
value == nilの場合:value.is_a?(Array)がfalse→ 全体としてfalse- 以降の処理で正しく「単一ID扱い」になり、結果として
RecordNotFoundが発生するパスに流れる
value == [ [1, 2], [3, 4] ]のような「複数レコード用複合キー配列」の場合:value.is_a?(Array)→truevalue.first.is_a?(Array)→true- 複数IDとして処理される (期待どおりの挙動)
テスト追加
activerecord/test/cases/finder_test.rb に、すでに単一主キー用に存在しているテスト (test_find_with_ids_with_no_id_passed) と並べて、複合主キー用のテストを追加しています。
追加されたテストの意図は以下の2点:
assert_raises(ActiveRecord::RecordNotFound) { Cpk::Book.find }
assert_raises(ActiveRecord::RecordNotFound) { Cpk::Book.find(nil) }以前はこれらが NoMethodError になっていたのが、修正後は期待どおり RecordNotFound になることを検証しています。
- 影響範囲・注意点
影響範囲
- 複合主キーを使用している ActiveRecord モデル (
self.primary_keys = ...を設定しているモデルなど) のfind振る舞いに影響します。 - 対象は以下パターン:
Model.find(引数なし)Model.find(nil)
- これらの呼び出しで、以前は
NoMethodErrorが出ていたケースがActiveRecord::RecordNotFoundに変わります。
- 複合主キーを使用している ActiveRecord モデル (
互換性
- 正しい例外クラスに揃えるバグフィックスであり、ドキュメント上も
RecordNotFoundが想定されているため、仕様としては後方互換的な修正です。 - ただし、既存コードが誤って
NoMethodErrorを捕まえていた場合は、例外クラスの変更により挙動が変わる可能性があります。そのようなコードはActiveRecord::RecordNotFoundを捕まえるべきです。
- 正しい例外クラスに揃えるバグフィックスであり、ドキュメント上も
複合主キー用の複数ID指定
Model.find([[id1a, id1b], [id2a, id2b]])のような複数ID指定は、valueが配列であり、先頭要素も配列であるため、引き続き問題なく「複数ID」として扱われます。Model.find([id1a, id1b])のような「単一レコード用の複合キー配列」も、従来通り単一ID扱いです (expects_multiple_ids?はfalse)。
- 参考情報 (あれば)
修正ファイル:
activerecord/lib/active_record/key.rbKey::Composite#expects_multiple_ids?のロジック修正 (+1/-1)
activerecord/test/cases/finder_test.rb- 複合主キー向けの
findに関するテストを追加 (+5)
- 複合主キー向けの
関連仕様:
- Rails ガイド/ドキュメント上で
Model.find(nil)はActiveRecord::RecordNotFoundを投げることになっており、本PRは複合主キーでもこの仕様に揃えるためのバグ修正です。
- Rails ガイド/ドキュメント上で
#57722 Don't log a dangling "with arguments:" for jobs with no arguments
マージ日: 2026/6/14 | 作成者: @55728
- 概要 (1-2文で)
Active Job のログ出力で、引数なしジョブにもwith arguments:という文言だけがぶら下がって出力されてしまう問題を修正する PR です。構造化イベント対応時に削除されていた「引数が空かどうかのチェック」を復元し、従来通りのログフォーマットに戻しています。
- 変更内容の詳細
問題の背景
ActiveJob::LogSubscriber#args_info は、ジョブの引数をログに出すためのヘルパーメソッドです。
構造化イベント対応(event[:payload] ベースの実装)に書き換えられた際に、もともとあった「引数が空でないかのチェック (any?)」が抜け落ちました。
以前は概ね以下のようなロジックでした(イメージ):
# 以前(構造化イベントリライト前)のイメージ
def args_info(job)
if job.class.log_arguments? && job.arguments.any?
" with arguments: " + job.arguments.map { ... }.join(", ")
else
""
end
endこれが構造化イベント対応後は以下のようになっていました:
def args_info(event)
if (arguments = event[:payload][:arguments])
" with arguments: " + arguments.map { ... }.join(", ")
else
""
end
endここで:
- 引数なしジョブの場合でも
event[:payload][:arguments]には空配列[]が入る - Ruby では
[]は truthy なのでif (arguments = ...)が常に真になる - その結果、
"with arguments: "という接頭辞だけ付き、肝心の中身が空の状態でログに出てしまう
例:
Enqueued MyJob (Job ID: ...) with arguments:
Performing MyJob (Job ID: ...) with arguments:(「with arguments: 」の後ろが空)
今回の修正
この PR では、以前の挙動と同じく「引数が空配列でないこと」をチェックするように修正しています:
# activejob/lib/active_job/log_subscriber.rb
def args_info(event)
if (arguments = event[:payload][:arguments]) && arguments.any?
" with arguments: " + arguments.map { ... }.join(", ")
else
""
end
endこれにより:
- 引数ありのジョブ: これまで通り
with arguments: foo, barのように出力 - 引数なしのジョブ:
with arguments:自体を出力しない(完全に空文字を返す)
テストの追加
activejob/test/cases/logging_test.rb にテストが追加されています。
- 引数なしジョブ (
ConfigurationJobなど) を enqueue して実行 - ログ出力をキャプチャし、
with arguments:という文字列が含まれないことをアサート
テストの意図:
- Enqueued ログ行
- Performing ログ行
のどちらにも余計な with arguments: が付いていないことを保証しています。
- 影響範囲・注意点
- 影響範囲は Active Job のログ出力のみです。ジョブの実行ロジックやキューイング処理そのものには影響しません。
- 引数ありジョブのログフォーマットは変わりません。変わるのは「引数なしジョブの時に、
with arguments:という文言が出るかどうか」のみです。 - 構造化イベント導入後(このバグが入り込んでいた期間)に、ログパースや監視ツール側で「
with arguments:が常に出る前提」で実装していた場合は、今回の修正でその前提が崩れます。- ただし、元々の(構造化イベント前の)Rails の挙動に戻っただけなので、正しい仕様は「引数がある場合のみ
with arguments:を出す」と理解しておくのがよいです。
- ただし、元々の(構造化イベント前の)Rails の挙動に戻っただけなので、正しい仕様は「引数がある場合のみ
- パフォーマンス面の影響はほぼゼロです。
arguments.any?のチェック追加のみで、ログ出力時にしか呼ばれません。
- 参考情報 (あれば)
- 対象クラス:
ActiveJob::LogSubscriber- ジョブの
Enqueued,Performing,Performedなどのログフォーマットを司るクラス
- ジョブの
- バグの原因: 構造化イベント対応のリファクタリング中に
.any?チェックが削除されたことによる仕様逸脱 - テストファイル:
activejob/test/cases/logging_test.rb- ログメッセージの仕様がテストで明示されているので、カスタムロガーやログパーサを書く際の参考になります。
#57709 Restart the Redis listener thread when it dies
マージ日: 2026/6/14 | 作成者: @chaadow
- 概要 (1-2文で)
Action Cable の Redis サブスクリプションアダプタにおいて、リスナースレッドが死んだまま二度と復帰せず、そのプロセスでのブロードキャストが黙って失われる問題を修正する PR です。リスナースレッドが死亡していた場合に再起動できるようにし、その振る舞いをテストで保証しています。
- 変更内容の詳細
問題の背景
Action Cable の Redis アダプタでは、Redis の pub/sub 用に「リスナースレッド」が常駐し、購読中のチャンネルに届いたメッセージを受け取っています。
ensure_listener_running は
@thread ||= Thread.new { ... }のように「一度作ったスレッドをメモ化」して再利用する仕組みになっています。
ところが、以下のようなケースでスレッドが死ぬと問題が起こります。
- 再接続処理の試行回数 (
reconnect_attempts) を使い切ってしまった場合ensure_listener_runningの内部で Redis 再接続をリトライするが、retry_connecting?がfalseを返した時点で rescue ブロックを抜けてスレッド終了
- スレッドオブジェクト自体は
@threadに残るが、既に dead になっている (non-nil かつ@thread.alive? == false) - その後の
subscribe呼び出しは、@thread ||= ...の||=によって「既に @thread があるから新しいスレッドは作らない」という挙動になり、新しいリスナーが起動しない - 新しい購読要求は
@when_connectedキューに積まれるが、@subscribed_clientはnilのままなので一向に処理されず、そのプロセスでは以後のブロードキャストが全部「無視される」状態になる
これにより、プロセス再起動まで該当プロセス配下の WebSocket 接続に対するブロードキャストが失われてしまう、という致命的な不具合が生じていました。
修正内容 (リスナースレッドの再起動)
actioncable/lib/action_cable/subscription_adapter/redis.rb において、ensure_listener_running の実装が変更されています。
ポイントは以下です。
@threadが存在していても「死んでいるスレッド」なら一度@threadをnilにリセットする- その上で
@thread ||= Thread.newによって新しいリスナースレッドを起動する - 併せて、再接続回数カウンタも「スレッド再生成時」にリセットされるようにする (=新しいリスナーとしてまっさらな状態で復帰)
疑似コードイメージ:
def ensure_listener_running
# 1. すでにあるスレッドが死んでいたら破棄
if defined?(@thread) && @thread && !@thread.alive?
@thread = nil
@reconnect_attempts = 0 # 実際にはそれに準じたカウンタのリセット
end
# 2. 生きているスレッドがなければ新しく起動
@thread ||= Thread.new do
begin
# Redis 接続&購読ループ
rescue => e
# retry_connecting? が false でなければ再接続を試みる
# ここで試行し尽くしてスレッドが死ぬとき、
# 次回 ensure_listener_running が呼ばれた際に上記の判定で再起動される
end
end
endこれにより、
- 再接続試行を使い切って一度はスレッドが死んでも
- 次の
subscribe呼び出し時にensure_listener_runningが「死んだスレッド」を検知して破棄 - 新しいリスナースレッドを起動し直す
という復旧パスが追加されます。
テストの追加
actioncable/test/subscription_adapter/redis_test.rb に回帰テストが追加されています。
内容の要点:
reconnect_attempts: 0の設定で Redis アダプタを初期化- これにより「再接続を1度も行わず即座に諦める」挙動を強制
- pub/sub 接続を意図的に切ることでリスナースレッドを殺す
- 新しいチャンネルに
subscribeし、その後のブロードキャストが再び届くことを確認- = 死んだリスナースレッドが再起動されたことの検証
また、このテストで「スレッドの死をポーリングで待つ」ために、ActionCable::TestCase に小さな wait_for ヘルパーが追加されています (actioncable/test/test_helper.rb)。
- Active Record のテストスイートにある
wait_forと同様のユーティリティ - 一定時間、条件が満たされるまでポーリングするために利用
- 固定
sleepに頼らず、安定かつ高速なテストにする意図
- 影響範囲・注意点
- 対象: Action Cable で Redis アダプタ (
ActionCable::SubscriptionAdapter::Redis) を使用している環境 - 直接の影響:
- これまで「Redis との pub/sub 接続が落ちて再接続試行を使い切った後、リスナースレッドが死にっぱなしになる」ケースで、ブロードキャストが黙って失われていた
- この PR により、その後の新規 subscribe のタイミングでリスナースレッドが自動的に再起動し、ブロードキャストが復旧するようになる
- Sentinel サブスクリプションとの関係 (#57690):
- #57690 では「unsubscribe による購読数 0 → スレッド終了」という典型的な死因を、内部の sentinel 購読によって防いだ
- 本 PR は「それ以外の死因 (例: 再接続試行の枯渇)」に対して、死んだあとに再起動する経路を追加する
- 両方を合わせることで、通常運用時のスレッド死亡も、障害時の死亡もカバーし、より堅牢になる
運用上の注意:
reconnect_attemptsを 0 やかなり小さい値にしていると、障害発生時に「すぐにリスナーが死んでは subscribe のたびに再起動される」ような挙動も起こり得ます- ただし、その場合でも「黙って配信ロストし続ける」よりは安全側の挙動です
- 本 PR 自体は設定値を変えていないので、必要なら各アプリ側で
reconnect_attempts・reconnect_delayなどのチューニングを検討するとよいです
- 参考情報 (あれば)
- 関連 PR: #57690 (Redis リスナースレッドが unsubscribe で死ぬ問題に対する sentinel サブスクリプションの導入)
- 実装の考え方:
- 長寿命スレッドを
@thread ||= ...でメモ化する場合、「スレッドが正常終了 or 例外死したときにそれを検知して再生成するパスを用意する」ことが重要 - 今回のように
@thread&.alive?をチェックし、dead なら@thread = nilしてから再生成するパターンは、スレッドプール/バックグラウンドワーカーなどでも使える定石です
- 長寿命スレッドを
#57697 Don't fire remove_channel for an unknown channel
マージ日: 2026/6/13 | 作成者: @55728
- 概要 (1-2文で)
Action Cable のSubscriberMap#remove_subscriberが、存在しないチャンネルに対してもremove_channel(UNLISTEN / unsubscribe)を発火してしまうバグを、ハッシュのキー存在チェックで防ぐ修正です。これにより、まだ購読登録されていない・すでに削除済みのチャンネルに対して、不要な UNLISTEN / unsubscribe が飛ばなくなります。
- 変更内容の詳細(サンプルコード含む)
問題の背景
SubscriberMap の内部では、購読者リストを次のようなハッシュで管理しています:
@subscribers = Hash.new { |h, k| h[k] = [] }この「デフォルト proc 付きハッシュ」の特徴は、@subscribers[channel] とアクセスした時に、そのキーが存在しないと自動的に channel => [] が追加される(オートバイビフィケーション)ことです。
broadcastとadd_subscriberは、事前に@subscribers.key?(channel)を見て、未知のチャンネルでは何もしないようになっていました。- 一方で、
remove_subscriberだけが 無条件で@subscribers[channel]にアクセス しており、存在しないチャンネルに対しても空配列を自動生成してしまっていました。
その結果:
- 存在しないチャンネルに対して
remove_subscriberが呼ばれる @subscribers[channel]が空配列として勝手に作られる- 当然空なので
@subscribers[channel].empty?がtrue remove_channel channelが呼ばれ、実際に UNLISTEN / Redis unsubscribe が発行される
つまり「サーバー自身は購読していないチャンネル」に対しても、UNLISTEN / unsubscribe を投げてしまう不整合が発生していました。
特に SubscriberMap::Async 経由の非同期処理では、
add_subscriberとremove_subscriberを 別タスク として executor に投げている- そのため「subscribe → すぐ unsubscribe」のようなケースで、「remove の方が先に走る」ことがある
このレース条件により、「まだチャンネル登録が完了していないのに remove_subscriber が走り、未知のチャンネルとしてオートバイビファイ → 不要な UNLISTEN が発行」という状況が現実に起こり得ます。
修正内容
remove_subscriber にも broadcast と同様のキー存在チェックを追加し、「知らないチャンネルなら何もしない」ように合わせました。
修正差分:
def remove_subscriber(channel, subscriber)
@sync.synchronize do
+ return if !@subscribers.key?(channel)
+
@subscribers[channel].delete(subscriber)
if @subscribers[channel].empty?
@subscribers.delete channel
remove_channel channel
end
end
endポイント:
@sync.synchronizeブロックの中で@subscribers.key?(channel)をチェック- キーがなければその場で
return(呼び出し元は戻り値を使っていないため互換性問題なし) - これにより
@subscribers[channel]アクセスによるオートバイビフィケーションを避ける
テストの追加
SubscriberMapTest に新しいテストが追加されています:
- 「unknown な channel に対して
remove_subscriberを呼んでもremove_channelが呼ばれない」ことを検証 - 既存の「broadcast should not change subscribers」に並ぶ形でテストが追加
テストの結果:
- 新規テスト: 修正前は
remove_channelが 1 回呼ばれるため RED、修正後は 0 回で GREEN actioncable/test/subscription_adapter/subscriber_map_test.rb全体: 2 run / 0 failure
- 影響範囲・注意点
- 対象箇所:
actioncable/lib/action_cable/subscription_adapter/subscriber_map.rbactioncable/test/subscription_adapter/subscriber_map_test.rb
- 主な影響:
- Action Cable の Redis / PostgreSQL / Inline いずれの pubsub アダプタでも、未知のチャンネルに対する
remove_subscriber呼び出しは完全に無視されるようになります。 - その代わりに「不要な UNLISTEN / unsubscribe」が送られなくなるため、バックエンド側のログやメトリクスのノイズが減り、状態整合性も改善されます。
- Action Cable の Redis / PostgreSQL / Inline いずれの pubsub アダプタでも、未知のチャンネルに対する
- 後方互換性:
- 呼び出し元は
remove_subscriberの戻り値を利用しておらず、副作用(チャンネルを本当に unsubscribe するかどうか)のみを期待している実装なので、仕様上は「より正しい挙動」への修正です。 - 「存在しないチャンネルに対して unsubscribe を投げる」ことに依存しているアプリケーションがある可能性は極めて低く、実質的に互換性問題はないと考えられます。
- 呼び出し元は
- レース条件への影響:
- Async アダプタでの「subscribe → 直後に unsubscribe」ケースなど、タスクの順序が逆転しても、「まだ登録されていないチャンネルへの remove では何も起きない」という安全な挙動になります。
- 逆に言えば、「remove の方が先に走った場合、そのチャンネルは一度も UNLISTEN されない」ということになりますが、サーバー側ではそもそも購読が開始されていないため、状態としては一貫しています。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57697
- 修正対象クラス:
ActionCable::SubscriptionAdapter::SubscriberMap - 関連する既存の挙動:
broadcastは既にreturn if !@subscribers.key?(channel)を行っていたadd_subscriberもkey?を用いて「新規チャンネルかどうか」を判定しており、今回の修正で 3 メソッドのポリシーが統一された形になります。
#57677 Fix Array#to_query to skip empty Array elements, mirroring the empty-Hash case
マージ日: 2026/6/13 | 作成者: @55728
- 概要 (1-2文で)
Array#to_queryが、ネストされた空配列の要素をクエリ文字列から除外するように修正されました。これにより、既に実装されていた「空ハッシュ要素は落とす」という挙動と整合がとれ、余計な「値なしパラメータ」が出力されなくなります。
- 変更内容の詳細
問題となっていた挙動
Array#to_query は以前の修正により、「ネストされた要素をシリアライズした結果が空文字列なら、その要素はクエリから除外する」というフィルタリングを行うようになっていました。
- 空ハッシュ
{}の場合は{}.to_query(prefix)が空文字列になり、結果的にその要素はクエリから削除される挙動になっていた。
一方で空配列 [] については内部実装上、次のような挙動になっていました。
[].to_query(prefix)はnil.to_query(prefix)にフォールスルーするnil.to_query(prefix)は「キーだけ」をエスケープした文字列 (例:a%5B%5D%5B%5D) を返す- その結果、値がないパラメータ断片(
a[][]に相当)がクエリ文字列に紛れ込む
具体例:
require "active_support/core_ext/object/to_query"
[1, [], 2].to_query("a")
# 修正前: "a%5B%5D=1&a%5B%5D%5B%5D&a%5B%5D=2"
# ↑ これが余計なセグメント
{ a: [1, {}, 2] }.to_query
# => "a%5B%5D=1&a%5B%5D=2" # 空ハッシュ要素は既に除外されていた
{ a: [1, [], 2] }.to_query
# 修正前は "a%5B%5D=1&a%5B%5D%5B%5D&a%5B%5D=2"
# 本来は空ハッシュ同様、空配列要素も除外したい修正内容
Array#to_query 内で、要素ごとに to_query へ委譲する前に、以下のチェックを追加しました。
- 要素が「空ハッシュ」または「空配列」であれば、その要素はスキップする(クエリに出さない)
ポイント:
- これは既に
Hash#to_queryでは行われていたロジックであり、Array#to_queryでも同じガードを入れて挙動を揃えた形です。 - トップレベルが空配列のケースは従来どおりで、変更していません。
例:
[].to_query("a")
# 修正前後とも: "a%5B%5D"
# (トップレベル空配列は「キーだけの配列パラメータ」として扱われる仕様のまま)
{ a: [1, [], 2] }.to_query
# 修正前: "a%5B%5D=1&a%5B%5D%5B%5D&a%5B%5D=2"
# 修正後: "a%5B%5D=1&a%5B%5D=2"テストの追加
activesupport/test/core_ext/object/to_query_test.rb に以下のテストが追加されました。
test_array_with_empty_arraytest_array_with_empty_hashと対になるテスト{ a: [1, [], 2] }.to_queryが"a%5B%5D=1&a%5B%5D=2"になることを検証
このテストは修正前の実装では失敗し、修正後にパスすることが確認されています。
- 影響範囲・注意点
影響範囲
ActiveSupportのObject#to_query/Array#to_queryを使っているコードのうち、
「配列の中に空配列を含むパラメータ」をクエリにシリアライズしている箇所が影響します。- 典型的には、フォームヘルパーやURLヘルパーを通して生成されるパラメータで、
ネストした配列を扱っているケース(例:params[:a] = [1, [], 2])です。
互換性の観点
- これまでは「意図しない余分なキーだけのパラメータ」が出ていた状態であり、
仕様というよりは不整合/バグに近い挙動です。 - ただし、もし既存コードで「この余分なキー (例:
a[][]) をサーバ側で何らかのトリガーとして利用していた」ような特殊な処理があれば、そのキーが今後送られてこなくなります。 - 一般的なRailsアプリでは、むしろ意図通りのクリーンなクエリ文字列になる改善と考えてよいです。
- これまでは「意図しない余分なキーだけのパラメータ」が出ていた状態であり、
注意点
- トップレベル空配列 (
[].to_query("a") #=> "a%5B%5D") の挙動は変わっていません。
「配列の中にある空配列」をスキップするだけで、「配列そのものが空」のケースは従来のままです。 - 「空の配列要素を明示的に送りたい」ようなユースケースがある場合は、別の表現(
nilや特別な値)で表す必要があります。
- トップレベル空配列 (
- 参考情報 (あれば)
- 該当PR: https://github.com/rails/rails/pull/57677
- 関連コミット: 68160c4417 (
Array#to_queryのフィルタリングロジックが導入されたコミット) - 関連メソッド:
Object#to_queryHash#to_queryArray#to_query(ActiveSupport コア拡張)
#57655 Support a single composite primary key id in update/update!
マージ日: 2026/6/13 | 作成者: @55728
- 概要 (1-2文で)
Rails のActiveRecord::Base.update/update!が、複合主キー(composite primary key)モデルに対して「単一レコードの id」として配列を渡した場合にRecordNotFoundを出してしまう問題を修正した PR です。複合主キーでも単一主キーと同様にModel.update(record.id, attrs)が正しく単一更新として動作するようになります。
- 変更内容の詳細(あればサンプルコードも含めて)
これまでの挙動と問題点
単一主キーのモデルでは、クラスメソッド update / update! は以下の2パターンを取りうる挙動をします:
# 単一レコード更新
Topic.update(1, title: "updated")
# 複数レコード更新
Topic.update(
[1, 2],
[{ title: "updated 1" }, { title: "updated 2" }]
)ここで、「複数 id かどうか」の判定に id.is_a?(Array) が使われていました。
複合主キーのモデルでは、1レコードの id 自体が配列になります(例: [author_id, id])。そのため:
book = Cpk::Book.first
# 期待: book 1件だけを更新
Cpk::Book.update(book.id, title: "updated")という呼び出しにおいて、book.id が配列であるため 「複数 id」と誤判定 されてしまい、内部的に要素ごと(author_id と id 各々)で find を呼び出そうとして ActiveRecord::RecordNotFound になる、というバグが発生していました。
修正内容
この PR では、「単一 id か複数 id か」の判定ロジック を以下のように変更しています。
- まず
:all(id 省略)パターンを優先的に判定Model.update(:all, attrs)の既存挙動を壊さないため。 - それ以外の場合に、「複数 id かどうか」を以下のルールで判断する private メソッド
update_multiple_ids?を新設:- 単一主キー:
idが配列なら「複数 id」(例:[1, 2])
- 複合主キー:
- 1レコードの id は配列(例:
[author_id, id]) - 複数レコードなら「配列の配列」(例:
[[author1, id1], [author2, id2]]) - したがって、「複合主キーで複数 id」の場合は
idが「配列の配列」であることをチェックする
- 1レコードの id は配列(例:
- 単一主キー:
Relation#destroy が既に同様のロジック(配列の配列かどうか)で判定しており、その考え方を update / update! に合わせた形です。
この helper メソッドは update と update! の両方から使われ、単一主キーのコードパスは(挙動として)変わらないように実装されています。
修正後のサンプルコード
# 単一主キー: これまで通り
Topic.update(1, title: "updated") # 単一更新
Topic.update([1, 2], [{ title: "a" }, { title: "b" }]) # 複数更新
# 複合主キー: この PR により、以下が正しく動作する
book = Cpk::Book.first
# book.id は [author_id, id] のような配列
# 1. 単一レコード更新 (以前は RecordNotFound になっていた)
Cpk::Book.update(book.id, title: "updated")
# => 1件の book が更新される
# 2. 複数レコード更新 (配列の配列を渡す)
books = Cpk::Book.limit(2)
Cpk::Book.update(
books.map(&:id), # [[author1,id1], [author2,id2]]
[{ title: "a" }, { title: "b" }]
)
# => 2件の book がそれぞれ更新されるテストとしては activerecord/test/cases/persistence_test.rb に、複合主キーの単一 id 更新が成功することなどを確認するケースが追加されています。
- 影響範囲・注意点
- 複合主キーを使っているアプリ:
- これまで
Model.update(record.id, attrs)がRecordNotFoundになっていたケースが、正しく単一更新として動作するようになります。 - もしバグ回避のために独自実装(
find+updateなど)を入れていた場合は、PR 適用後にそれを簡略化できる可能性があります。
- これまで
- 単一主キーのモデル:
- 仕様としての挙動は変わらず、
update(1, attrs)/update([1, 2], [attrs1, attrs2])は従来通りに動作する想定です。
- 仕様としての挙動は変わらず、
- 複数 id の引数形式:
- 複合主キーで複数レコードを
updateする場合は、「配列の配列」形式で id を渡す必要がある点に注意してください。- OK:
[[author1, id1], [author2, id2]] - NG(意図した複数更新にはならない / 単一扱いになる):
[author1, id1](これは単一レコードの id)
- OK:
- 複合主キーで複数レコードを
:allの扱い:Model.update(:all, attrs)の挙動は変わりません。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57655
- 類似の判定ロジック:
Relation#destroyが複合主キー対応のために行っている「配列の配列」判定と同等の考え方を、update/update!にも適用した修正です。
#57690 Keep the Redis listener alive when the last channel is unsubscribed
マージ日: 2026/6/13 | 作成者: @55728
- 概要 (1-2文で)
Redis クライアントの実装変更により、最後のチャネルがUNSUBSCRIBEされた瞬間に Action Cable の Redis リスナー用スレッドが終了してしまう不具合があり、それを修正する PRです。内部用の「番兵」チャネル(_action_cable_internal)への購読を復活させることで、通常の購読解除ではリスナーが死なず、明示的なシャットダウン時のみ終了するようにしました。
- 変更内容の詳細
問題の背景
以前の
redisgem ベースの実装では、Action Cable は常に_action_cable_internalという内部チャネルに購読していました。このため Redis の「購読数」は、アプリ側の全てのチャンネルを
UNSUBSCRIBEしても 1 は維持され、Redis の subscribe ループがゼロ購読で終了することは「明示的な shutdown 時」以外には起こりませんでした。新しい
redis-clientベースの実装 (ef812c2652) では、この内部チャネルへの購読がなくなり、- 最後のユーザーチャネルが
UNSUBSCRIBEされると購読数が 0 になり - listen ループが
breakし、@subscribed_clientがnilにされる
という挙動になっていました。
- 最後のユーザーチャネルが
ensure_listener_runningは以下のようなガードになっており:ruby@thread ||= Thread.new { listen }一度死んだスレッドは再生成されません。その結果:
- 以降の
subscribe呼び出しは全て@when_connectedキューに積まれるだけで実行されない @subscribed_clientはずっとnil- Redis からのメッセージも届かないので 全ブロードキャストが黙って捨てられる
- クライアントは WebSocket 接続自体は張れるが、Redis と繋がらないためメッセージが飛ばない
という「サイレントな本番障害」が発生し得る状態でした。
特に以下のような環境で起きやすいです:- 低トラフィック環境
- チャンネルが1つしか無いアプリ
- デプロイ中にノードを drain しているとき など
- 以降の
修正内容
1) 番兵チャネル _action_cable_internal への購読を復活
actioncable/lib/action_cable/subscription_adapter/redis.rb にて、listen メソッド中で @subscribed_client をセットした直後に、内部チャネルへの購読を行うように変更されています。
おおよそのイメージは以下です(※説明用の擬似コード):
def listen
@subscribed_client = build_subscribed_client
# ここで番兵を追加
@subscribed_client.call("SUBSCRIBE", "_action_cable_internal")
@subscribed_client.listen do |message|
# ...
end
ensure
@subscribed_client = nil
end重要なポイント:
_action_cable_internalはSubscriberMap(実際の Action Cable チャンネルと Redis チャンネルを紐づけるマップ)にエントリを持たないため、- ここに publish されても受信処理は「何もしない」=完全な no-op
- しかし Redis 的には「常に購読数が 1 以上」の状態が保たれるので、
- 通常の
UNSUBSCRIBE(ユーザーチャネルが0になる)では listen ループは終了しない - 全てのチャネル(この内部チャネルを含む)が明示的に
UNSUBSCRIBEされた場合のみ購読数が 0 になり、ループ終了 → スレッド終了となる
- 通常の
- これにより、以前の redis gem ベースの挙動と同じ「番兵つきの常駐リスナー」が復活します。
2) 回帰テストの追加
actioncable/test/subscription_adapter/redis_test.rb にテストが追加されています(+17 行)。
テストの内容(概念的な流れ):
- 同じ Redis アダプタインスタンスに対して、順番に2回
subscribeを行うテストケースを用意 - 1回目の
subscribeでチャンネルを購読 →unsubscribeまで行い、この時点で Redis 側の購読数が 0 になる状況を再現 - その後、2回目の
subscribeを行い、- リスナーのスレッドがまだ生きていて
- 実際に publish → subscribe が正常動作すること を検証
これにより、「一度全チャンネルが unsubscribe されても、以降の subscribe が正常に働く(リスナーが死んでいない)」ことを回帰テストで保証しています。
- 影響範囲・注意点
- 影響範囲
- Action Cable の Redis サブスクリプションアダプタ(
actioncable/lib/action_cable/subscription_adapter/redis.rb)を利用しているアプリ全般。 - 特に、
- チャンネル数が少ない
- 接続が一時的に全て切れることがある
といった環境で、Redis リスナーが知らないうちに停止してしまう問題を防ぎます。
- Action Cable の Redis サブスクリプションアダプタ(
- 互換性
_action_cable_internalチャンネルは以前の実装でも使われており、再導入なので互換性上の問題はほぼありません。- このチャネルに対してアプリが明示的に subscribe/publish している前提はない(内部用)ため、通常のアプリコードが影響を受けることはありません。
- パフォーマンス/リソース
- 常に1つのチャネルに購読し続けることになるため、「購読数ゼロで完全に Redis から切り離される」ことはなくなります。
- ただし listen 用の接続はもともと張りっぱなしにする設計であり、以前の redis gem 実装でも同様だったため、特段の追加コストはありません。
- 運用上の注意
- もし「意図的に listener を止めたい」場合(例: シャットダウンフックなど)には、
- 内部チャネルも含めて
UNSUBSCRIBEするか、 - またはアダプタの明示的な shutdown API を使う
などの手段が必要ですが、これは以前と同じ考え方です。
- 内部チャネルも含めて
- 逆に、今回の修正により「一時的に全クライアントがいなくなっただけ」でリスナーが落ちるケースは解消されます。
- もし「意図的に listener を止めたい」場合(例: シャットダウンフックなど)には、
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57690
- 問題の導入元と思われるコミット:
ef812c2652(redis-client ベースへの書き換え) - 変更概要:
- 変更ファイル数: 2
- 追加行数: 26
- 削除行数: 0
この修正は、Redis ベースの Action Cable を使っている本番環境における「ごく一部のタイミングでのみ発生する静かな障害」を潰す目的のものなので、redis-client に移行している Rails バージョンを利用中であればアップデート推奨です。
#57700 Shut down the heartbeat timer on server restart
マージ日: 2026/6/13 | 作成者: @55728
- 概要 (1-2文で)
Action Cable サーバ再起動時に、ハートビート用の@heartbeat_timerが停止されずに残り続けていた不具合を修正する PR です。
開発環境のコードリロードごとにスレッドプールとタイマーがリーク・再生成される問題を解消します。
- 変更内容の詳細
問題の内容
ActionCable::Server::Base#restart は、サーバ再起動時に以下のリソースを破棄していました:
@worker_pool@executor@pubsub
しかし、定期的にハートビートを送るための @heartbeat_timer (Concurrent::TimerTask) だけはそのまま動き続けていました。
このタイマーのブロック内では executor が呼ばれますが、executor は「遅延生成」されるため、次のような状況が発生していました:
restartが呼ばれる
→ 既存の executor スレッドプールは停止・破棄される。- しかし、古い
@heartbeat_timerは生きていて、BEAT_INTERVALごとに実行される。 - タイマー内の処理で
executorを呼ぶ
→ 「必要に応じて再生成」の仕組みにより、新しい executor スレッドプールが勝手に作られてしまう。 - 結果として:
- teardown したはずの executor が即座に復活する
- 古い
@heartbeat_timerも生き続ける - コードリロードのたびにスレッドプール・タイマーが増え、リソースリーク/スレッド churn を起こす
開発環境では restart が before_class_unload を通じてクラスリロードごとに呼ばれるため、この問題は特に dev で顕著になります。
修正内容
ActionCable::Server::Base#restart 内で、他のリソースと同様に @heartbeat_timer も停止・クリアするようにしました。
該当部分のイメージ:
@mutex.synchronize do
# Shutdown the heartbeat timer
@heartbeat_timer.shutdown if @heartbeat_timer
@heartbeat_timer = nil
# Shutdown the worker pool
@worker_pool.halt if @worker_pool
@worker_pool = nil
# ... 既存の teardown ロジック ...
endあわせて、initialize でも他の遅延生成リソースと同様に @heartbeat_timer を nil で初期化するリストに追加し、一貫性を取っています。こうすることで:
setup_heartbeat_timerが次のリクエスト時に呼ばれたときに、- 必要に応じて新しい
@heartbeat_timerを遅延生成する、
という既存パターンにきれいに乗るようになっています。
テスト
ActionCable::Server::Base のテストに以下を追加:
"restart shuts down the heartbeat timer"
テスト内容の要点:
restart実行後に:@heartbeat_timerがrunning?でないこと@heartbeat_timerがnilになっていること
既存の "restart shuts down ..." 系テストと並ぶ形で追加されており、4回実行して red/green が確認されています。
CHANGELOG には記載なし(バグ修正扱い)。
- 影響範囲・注意点
- 対象:
ActionCable::Server::Baseの再起動処理 (#restart) - 主な影響:
- 開発環境でのコードリロード時に、不要なスレッドプールやタイマーが増殖しなくなる。
- 長時間開発サーバを動かしていても、Action Cable 周りのスレッド・タイマーがリークしにくくなる。
- 本番環境への影響:
- 通常、本番で
restartが頻繁に呼ばれるケースは少ないため、主なメリットは安定性・リソース解放の明確化。 - Restart 後は、次のリクエストが来たタイミングでハートビートタイマーが再生成されるため、ハートビート動作は従来どおり維持される。
- 通常、本番で
互換性を壊す変更ではなく、「本来そうあるべきだった teardown を正しく行う」性質の修正です。
- 参考情報 (あれば)
- 対象ファイル:
actioncable/lib/action_cable/server/base.rb(+5/-1)actioncable/test/server/base_test.rb(+11/-0)
- 関連キーワード:
Concurrent::TimerTaskBEAT_INTERVAL- 遅延生成された executor / worker_pool / pubsub
before_class_unloadによる開発時の server restart
#57699 Raise ChannelNotFound for a subscribe command without a channel
マージ日: 2026/6/13 | 作成者: @55728
- 概要 (1-2文で)
Action Cable のsubscribeコマンドでchannelキーが省略された場合に、これまで素のNoMethodErrorが発生していたのを、他の経路と同様にChannelNotFound例外が発生するように統一したバグフィックスです。サーバ側は異常クライアントからの不正な subscribe メッセージに対して、より一貫したエラー処理ができるようになります。
- 変更内容の詳細
問題の背景
Action Cable ではクライアントからの subscribe コマンドは、概ね以下のような JSON を想定しています。
{
"command": "subscribe",
"identifier": "{\"channel\":\"ChatChannel\",\"room_id\":1}"
}このうち identifier は JSON 文字列で、サーバ側ではパースして id_options(Hash)として扱います。その際、id_options[:channel] を元にチャンネルクラスを定数解決します。
問題になっていたのは、クライアントが誤って channel キーを含まない identifier を送ったケースです。
例:
{
"command": "subscribe",
"identifier": "{\"id\":1}"
}この場合、Rails 側では以下のような処理が行われていました。
# 変更前のコード(簡略)
subscription_klass = id_options[:channel].safe_constantize
if subscription_klass && subscription_klass < Base
# ...
else
raise ChannelNotFound
endid_options[:channel] が nil になるため、nil.safe_constantize が呼ばれ、NoMethodError(undefined method safe_constantize' for nil:NilClass)が発生していました。この例外は Action Cable が想定している「チャンネルが解決できない」場合の ChannelNotFound` とは異なり、スタックトレース的にも分かりづらく、ハンドリングもしにくい状況でした。
修正内容
nil の場合を考慮して safe navigation 演算子 &. を使用し、channel が存在しないケースでも通常の「チャンネル解決失敗」フローに乗るようにしています。
- subscription_klass = id_options[:channel].safe_constantize
+ subscription_klass = id_options[:channel]&.safe_constantizeこれにより、
id_options[:channel]が存在し、有効なクラス名 → そのクラスをsubscription_klassに設定id_options[:channel]が存在するが、クラス定数として解決できない →safe_constantizeがnilを返し、elseブランチに入りChannelNotFoundを raiseid_options[:channel]自体が存在しない(nil)→&.によりsafe_constantizeが呼ばれず、subscription_klassはnilとなり、同じくelseブランチでChannelNotFoundを raise
という挙動になります。
テスト追加
actioncable/test/connection/subscriptions_test.rb に以下のようなテストが追加されています(概要):
- テスト名: "subscribe command without a channel"
- 内容:
channelキーを含まない subscribe コマンドを送る- 以前は
undefined method 'safe_constantize' for nilで落ちていたところ、 - 修正後は
ChannelNotFound例外が発生することを確認
既存の「Base を継承していないチャンネル(Base channel)」のテストの横に追加されており、一連の「チャンネル解決に失敗した場合の動作」のカバレッジを補強しています。
- 影響範囲・注意点
- 主な影響範囲は Action Cable の subscribe 処理です。
- 想定外の identifier(
channelキーの欠如)に対して、これまでNoMethodErrorが飛んでいた環境では、今後はChannelNotFoundが飛ぶようになります。
- 想定外の identifier(
- これにより:
- ログやエラーハンドリングの観点で、subscribe 失敗時の例外種別が一貫します。
ChannelNotFoundを前提にした救済処理(rescue, instrumentation 等)が正しく働くようになります。
- 互換性:
- 正常なクライアント(正しい
channelを送る)は挙動に変化はありません。 - 不正なクライアント/バグのあるクライアントに対して、これまで「500 エラー+NoMethodError」となっていたものが、「想定された例外型(
ChannelNotFound)」に変わる可能性があります。 - もしアプリケーション側で
NoMethodErrorを元に特別な処理をしていた場合(あまりない想定ですが)、動作が変わる可能性はあります。
- 正常なクライアント(正しい
- 参考情報 (あれば)
- 対象コード:
actioncable/lib/action_cable/connection/subscriptions.rb - 例外クラス:
ActionCable::Connection::Subscriptions::ChannelNotFound(Action Cable 内部で「存在しない/解決できないチャンネル」を表現するために使われる例外) - この PR はバグ修正であり、CHANGELOG には追加されていません。
ただし、Action Cable を直接利用しているアプリケーションや、カスタムのエラーハンドリングを行っているミドルウェアでは、例外種別の変化を念のため確認するとよいです。
#57695 Keep pending subscribe confirmations across a Redis reconnect
マージ日: 2026/6/13 | 作成者: @55728
- 概要 (1-2文で)
Redis の pub/sub 接続が切れた直後に発生していた「サブスクライブ確認(on_success)が二度と返ってこない」問題を修正し、再接続後も未完了の subscribe 確認を正しく引き継ぐようにした PR です。Redis クライアント刷新時のリグレッションで、Action Cable の Redis アダプタの挙動のみをピンポイントに修正しています。
- 変更内容の詳細
問題の内容
- 対象クラス:
ActionCable::SubscriptionAdapter::Redis::Listener - 現象:
- Redis の pub/sub 接続が切断されると
Listener#reset→ 再接続 →resubscribeという流れで復旧が行われる。 resetは内部状態のリセットとして以下を行っていた:@subscribed_client = nil@subscribe_callbacks.clear@when_connected.clear
- ところが
@subscribe_callbacksをclearしてしまうため、- ちょうどそのタイミングで
SUBSCRIBEを送信済みだが ack(["subscribe", "channel", 1])が戻ってきていない「in-flight な subscribe」がある場合、 - 再接続後に
resubscribeによって再度SUBSCRIBEは出されるものの、それに紐づくon_successコールバックが消えている。 - そのため ack が返ってきても何も実行されず、クライアント側では
confirm_subscription/subscribedが決して呼ばれない「永遠に pending のチャネル」になってしまう。
- ちょうどそのタイミングで
- Redis の pub/sub 接続が切断されると
- これは
redis-clientへの書き換え (ef812c2652) によって入り込んだリグレッションであり、別 PR #57690 で修正されている「最後のチャネルが unsubscribe されたときに Listener を生かし続ける問題」とは独立した不具合です。
修正内容
reset から @subscribe_callbacks.clear を削除し、pending な subscribe 確認情報を再接続後も維持するようにしました。
def reset
@subscription_lock.synchronize do
@subscribed_client = nil
- @subscribe_callbacks.clear
@when_connected.clear
end
endなぜこれで問題が解決するか
@subscribe_callbacksは「チャンネルごとの subscribe 確認用コールバック配列」を持つハッシュと考えられます。- subscribe が成功して ack を受け取ると、そのチャンネルに紐づくコールバックが実行され、その中で自分自身のエントリを削除するような実装になっている(PR 説明より)。
- つまり:
- すでに subscribe 済み(ack 済み)のチャネルは、コールバック実行時に自分のキーを削除しているため、
reset時点で@subscribe_callbacksに残っているのは「まだ ack が返ってきていない subscribe だけ」。 resetでこれを消さずに残しておけば、再接続後のresubscribeによって再度発行されるSUBSCRIBEと、元のコールバックを正しく結びつけられる。- コールバック配列が空なチャネルは ack 時に何も実行されないだけで、古い状態が残るわけではない。
- すでに subscribe 済み(ack 済み)のチャネルは、コールバック実行時に自分のキーを削除しているため、
- そのため、
clearをやめても「古い/不要なコールバックが大量に残る」といった問題は基本的に起きません。
テストの追加
実バグは「subscribe が in-flight の瞬間に接続が落ちる」というレース条件でしか起きないため、実 Redis を使うと再現が非常に難しい(ack がサブミリ秒で返ってくる)。
この PR では、実際の Listener#listen → reset → resubscribe → retry の経路を通しつつ、pub/sub 接続部分だけを疑似実装に差し替えるテストを追加しています。
ポイント:
- 疑似 pub/sub 接続の
next_eventは Ruby のQueueを使ってブロックする実装。- テスト側は Queue にイベントを順番に push することで、Listener のイベント処理順を完全に制御できる。
- シナリオ:
:dropを push →next_eventがRedisClient::ConnectionErrorを投げる →resetが呼ばれ、再接続 &resubscribeが走る。- その後、再接続した際の ack イベント
["subscribe", "channel", 1]を push。
Concurrent::Event+ インライン executor を使用し、Listener がポストした仕事(コールバック)が同期的に処理されるようにすることで、subscribe 確認が実際に届いたかどうかをテストから確実に観測可能にしている。- 新規テストクラス:
RedisAdapterTest::ListenerReconnection- 修正前: 2 秒タイムアウトで subscribe 確認が発火せず red
- 修正後: ~30ms 程度で確認を受信し green
- 既存の Redis 再接続系の統合テストを含む
actioncable/test/subscription_adapter/redis_test.rb全体も 26 テストすべてパス。
- 影響範囲・注意点
影響範囲
- 対象:
- Action Cable + Redis アダプタを利用しているアプリケーション。
- 特に、ネットワークが不安定な環境や Redis 接続が一時的に切断されうる状況で、
- クライアントが新しいチャネルに subscribe した直後に接続が落ちるケース。
- 期待される改善:
- 接続が落ちた直後に発行された subscribe が、再接続後に正しく confirm される。
- クライアント側で
confirmed?/subscribedコールバックが永久に呼ばれない「ハングした subscription」が事実上解消される。
トレードオフ / 残る可能性のある問題
PR 本文で言及されている既知のトレードオフ:
- シナリオ:
- チャンネル A に subscribe(コールバック登録) → ack 待ちの in-flight 状態。
- その後、接続が落ちる前にチャンネル A を完全に unsubscribe。
- その後で Redis 接続が落ちる →
reset→ 再接続 &resubscribe。
- この場合:
- A はすでに
@subscribersから削除されているのでresubscribe対象にならない。 - 一方で
@subscribe_callbacks内には A 用のコールバックが残り続ける可能性がある(実行も削除もされない)。 - 結果として、そのチャネル向けの小さな Proc オブジェクトがリークする。
- A はすでに
- PR 作成者はこれを「極めてレアなエッジケース」「リークするのは小さなコールバックだけ」として許容されるトレードオフだと位置付けており、
- もし気になる場合は、
reset時に「@subscribersに残っているチャネルの分だけコールバックを保持する」ような後続の改善が可能と述べています。
- もし気になる場合は、
運用上の注意:
- この PR 自体は バグフィックスのみ であり、外部 API や設定の互換性を壊すような変更はありません。
- 接続切断と再接続が頻繁に起こる環境では、今回の修正により subscribe 確認周りの安定性が向上するはずです。
- もし独自フォークで
Listener周りをカスタマイズしている場合は、@subscribe_callbacksをリセットしていない前提でコードを見直すとよいです。
- 参考情報 (あれば)
- 関連 PR:
- #57690: 「最後のチャネルが unsubscribe されたときにも Redis Listener を維持する」問題の修正。今回とは別のバグだが、同じ redis-client 書き換え由来。
- 関連コミット:
ef812c2652: Redis クライアント周りをredis-clientベースに書き換えたコミット。今回のリグレッションの起点。
- 実装の観点でのポイント:
- subscribe の ack を「チャネルごとの callback array で管理」し、ack 受信時にコールバック側で自分を削除する設計になっているため、「callback テーブルの一括初期化」は非常にデリケートな操作である、という教訓が得られる変更でもあります。
#57696 Respect an explicit id: nil in the Redis cable config
マージ日: 2026/6/13 | 作成者: @55728
- 概要 (1-2文で)
Action Cable の Redis アダプタにおいて、Redis 接続設定で明示的にid: nilを指定した場合にそれが上書きされず、そのまま尊重されるように修正した PR です。これにより、ドキュメント通り「id: nilを指定してCLIENT SETNAMEを発行しない」挙動が正しく動作するようになります。
- 変更内容の詳細
問題点
元の実装(ActionCable::SubscriptionAdapter::Redis.redis_connector)では、Redis 接続設定 config に対して ID を以下のように設定していました。
config[:id] ||= "ActionCable-PID-#{$$}"||= は「値が nil または false の場合に右辺で上書きする」演算子のため、
config[:id] = nilと明示的に nil を設定しても、「値がない」と判断されて "ActionCable-PID-#{PID}" が再度代入されてしまっていました。
しかし、Action Cable のドキュメント上は「id: nil を指定すると Redis の CLIENT SETNAME をスキップする(=クライアント名を付けない、もしくはプロキシ側に命名を任せる)」という仕様になっており、これが実際には効いていなかった、というバグです。
同じ ID を扱う別の箇所 SubscriptionAdapter::Base#identifier では、すでに「キーの存在有無」を使って nil を尊重する挙動になっていたため、実装の一貫性も欠けていました。
# イメージ: Base 側はこういう guard をしていた
config[:id] = ... unless config.key?(:id)修正内容
上記の不整合とバグを解消するため、redis_connector 側の ID 設定ロジックを以下のように変更しています。
- config[:id] ||= "ActionCable-PID-#{$$}"
+ config[:id] = "ActionCable-PID-#{$$}" unless config.key?(:id)ポイント:
config.key?(:id)で「キーが存在するか」を見るようにしたidキー自体が存在しない場合のみ、"ActionCable-PID-#{$$}"をセットするid: nilと明示されている場合は、そのままnilを保持する(=CLIENT SETNAMEを行わない)
これにより、Base 側と Redis コネクタ側で同じポリシー(「キーが存在しないときだけデフォルトを入れる」)になり、一貫性のある挙動になります。
テスト修正
既存のテスト RedisAdapterTest::ConnectorCustomIDNil は、「id: nil の時の挙動」をカバーしているつもりでしたが、実際には不十分でした。
- もともとのテストでは「テストヘルパーの
connection_idがnilであること」だけを確認しており、Redis クライアントの実際の ID 名 (client.id/CLIENT GETNAME相当) を検証していなかった - そのため、「
id: nilを指定したのに PID ベースの名前が設定されてしまう」バグを検出できていませんでした(テストは「たまたま」通っていた)
PR では、このテストを以下のように修正しています。
- 実際の Redis クライアントに対して
client.id(=CLIENT GETNAMEの結果)を取得し、それがnilであることをアサートする
これにより、id: nil が本当に Redis のクライアント名未設定(CLIENT SETNAME 未実行)として扱われることをテストで保証できます。
- 影響範囲・注意点
影響範囲
- 対象:
- Action Cable の Redis サブスクリプションアダプタを利用しているアプリケーション
- 特に
config.action_cable.cable = { adapter: "redis", id: nil, ... }のように明示的にid: nilを指定している構成
- 影響内容:
- これまで:
id: nilを指定していても、内部的には"ActionCable-PID-#{PID}"が設定され、CLIENT SETNAMEが実行されていた
- 修正後:
id: nilを指定した場合、CLIENT SETNAMEが行われず、クライアント名は未設定のままとなるidをまったく指定していない場合は、従来通り"ActionCable-PID-#{PID}"が自動的に設定される
- これまで:
実運用上の注意点
id: nilを前提にプロキシやミドルウェア側で命名している場合- これまで意図通り動いていなかった(Action Cable 側の PID ベース名前が付いていた)ケースでは、今回の修正でようやくプロキシ側の命名が使われるようになります。
- そのため、Redis モニタリング・接続監視ツールの表示やフィルタ条件が変わる可能性があります。
idを明示的に指定していないアプリケーション- 挙動は従来から変わりません。
- デフォルトの
"ActionCable-PID-#{$$}"付与は継続されるので、特別な対応は不要です。
idをnil以外で明示的に指定している場合- 例:
id: "my-custom-name" - こちらも挙動は変わりません(キーが存在するため、デフォルトは上書きされない)。
- 例:
- 参考情報 (あれば)
- 対象クラス・メソッド:
ActionCable::SubscriptionAdapter::Redis.redis_connectorActionCable::SubscriptionAdapter::Base#identifier
- 関連仕様:
- Redis
CLIENT SETNAME/CLIENT GETNAME - Action Cable の Redis adapter 設定での
idオプション:id省略時:"ActionCable-PID-#{PID}"が自動付与されるid: nil:CLIENT SETNAMEを呼ばない(クライアント名を設定しない)id: "string": 指定した名前でCLIENT SETNAMEが呼ばれる
- Redis
- PR 番号: #57696
- 変更ファイル:
actioncable/lib/action_cable/subscription_adapter/redis.rbactioncable/test/subscription_adapter/redis_test.rb
#57698 Coerce stream name to String in the test adapter accessors
マージ日: 2026/6/13 | 作成者: @55728
- 概要 (1-2文で)
Action Cable のテスト用アダプタ (SubscriptionAdapter::Test) において、シンボルのストリーム名を使ったときにブロードキャスト検証が正しく動かない不具合を修正する PR です。内部で文字列キーとして保存しているのに対し、読み出し側が生の引数(シンボルなど)でアクセスしていたため起きていた不整合を解消しています。
- 変更内容の詳細
問題の背景
Action Cable のブロードキャスト処理(Server::Broadcasting)では、メッセージを String(broadcasting) をキーにして保存しています。
一方、テスト用アダプタ (ActionCable::SubscriptionAdapter::Test) では、以下のようなアクセサを持っています:
def broadcasts(channel)
channels_data[channel] ||= []
end
def clear_messages(channel)
channels_data[channel] = []
endここで channel にシンボル :foo を渡した場合:
- 書き込み側:
String(:foo)→"foo"をキーに保存 - 読み出し側:
channels_data[:foo]を参照
となり、:foo と "foo" が別キーとして扱われるため、テストからは「メッセージがない」ように見えていました。
その結果:
assert_broadcasts(:foo, 1)
実際には"foo"に 1件保存されていても、:fooでは見つからず「0件」と判定される。assert_no_broadcasts(:foo)
実際には"foo"にメッセージがあっても、:foo側は空なので「ブロードキャストなし」と誤判定される(危険)。
修正内容
この PR では、テストアダプタ側のアクセサで、必ず文字列に変換してから内部ハッシュを参照するように変更しています。
def broadcasts(channel)
- channels_data[channel] ||= []
+ channels_data[String(channel)] ||= []
end
def clear_messages(channel)
- channels_data[channel] = []
+ channels_data[String(channel)] = []
endこれにより、以下が常に一致します:
- 書き込み:
String(broadcasting)(例::foo→"foo") - 読み出し:
String(channel)(例::foo→"foo")
そのため、テストコード側がシンボル・文字列どちらで指定しても正しくメッセージを取得・クリアできます。
テスト追加
TransmissionsTest(実際には actioncable/test/test_helper_test.rb)に以下の性質を確認するテストが追加されています:
test_assert_broadcasts_with_symbol_stream- 修正前:
assert_broadcasts(:test, 1) { ... }が「1 expected, but 0 were sent」で失敗(red) - 修正後: ブロードキャストを正しくカウントできて成功(green)
- 修正前:
テストは 14 回実行されており、安定して再現・解消できていることが確認されています。
- 影響範囲・注意点
影響範囲
- Action Cable のテストで
assert_broadcasts/assert_no_broadcastsなどを利用しているコードが対象です。 - 特に、ストリーム名を シンボルで指定しているテスト に直接影響します。
- 実動作(本番環境での Action Cable の挙動)には影響せず、テスト専用アダプタの挙動のみ変わります。
- Action Cable のテストで
何が変わるか
- これまで「テストが通っていたが、実際にはブロードキャストされていた」ケースが、正しく失敗するようになります。
- 具体例:ruby以前:
assert_no_broadcasts(:chat) do ActionCable.server.broadcast("chat", { msg: "hi" }) end:chatと"chat"の不一致で「ブロードキャストなし」と誤判定 → テストが通る
以後:"chat"にメッセージが載るのでテストが失敗 → 本来の期待どおり
- 具体例:
- これまで「テストが通っていたが、実際にはブロードキャストされていた」ケースが、正しく失敗するようになります。
注意点
- 既存テストで「たまたま」通っていたものが落ちる可能性がありますが、それは本来検知すべきバグ・回 regressions を正しく検出できるようになった結果です。
- テスト側の API 仕様としては、「ストリーム名はシンボルでも文字列でもよい」「どちらも同じ扱いを受ける」という意図に沿う変更なので、利用者視点からはむしろ直感的になります。
channels_dataを直接触っているようなメタなテスト・ツールがあれば、キーが文字列で統一される点を前提にしておく必要があります(とはいえ元々書き込み側は文字列だったため、実質的には仕様どおりになったとも言えます)。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57698
- 関連コード:
actioncable/lib/action_cable/subscription_adapter/test.rbServer::Broadcasting(ブロードキャスト時にString(broadcasting)を使っている箇所)
- CHANGELOG には記載なし(バグフィックス扱い)。
#57692 Coerce broadcasting to String in Channel#stop_stream_from
マージ日: 2026/6/13 | 作成者: @55728
- 概要 (1-2文で)
Action Cable のChannel#stop_stream_fromが、stream_fromと同様に引数をStringに変換して扱うよう修正されました。これにより、シンボルを使って購読開始・停止を行った場合でも、正しく購読解除されるようになります。
- 変更内容の詳細
問題の背景
ActionCable::Channel::Streams モジュール内で:
stream_fromは内部的に 常に String 化 してからstreamsハッシュに登録しています:rubydef stream_from(broadcasting, ...) broadcasting = String(broadcasting) streams[broadcasting] = ... endそのため、
stream_from :chatと書くと、内部的には"chat"というキーで管理されます。一方、
stop_stream_fromは 引数をそのまま キーとしてstreams.deleteに渡していました:rubydef stop_stream_from(broadcasting) streams.delete(broadcasting) end
その結果:
stream_from :chat
stop_stream_from :chatというペアで呼び出した場合、
- 登録時のキー:
"chat"(String) - 削除時のキー:
:chat(Symbol)
となり、ハッシュのキーが一致せず、streams.delete(:chat) は何も削除しません。
そのため pubsub.unsubscribe が呼ばれず、接続が生きている間ずっと購読が残り続けるバグが発生していました。
これは実際にテスト (SymbolChannel in stream_test.rb) でも stream_from :channel としてシンボルを使っているため、現実的なバグです。
修正内容
stop_stream_from の先頭で broadcasting を String に変換するようにし、stream_from と挙動を揃えました。
疑似コードとしては、今回の差分は以下のようなイメージです:
def stop_stream_from(broadcasting)
broadcasting = String(broadcasting) # ← ここが追加
streams.delete(broadcasting)
endこれにより、以下のようなコードが期待どおり動きます:
# 購読
stream_from :room_one
# 購読解除(同じシンボルを渡してもOK)
stop_stream_from :room_oneテスト追加
actioncable/test/channel/stream_test.rb にテストが追加されました。内容としては:
stream_from :room_oneで購読を開始stop_stream_from :room_oneで購読を停止- 最終的に購読者数(subscriber 数)が 0 になっていることを検証
という流れで、シンボルを使っても購読が正しく解除されることを確認しています。
- 影響範囲・注意点
影響範囲
- Action Cable の
Channel#stop_stream_fromを利用している全てのコードに影響します。 - とくに、
stream_from/stop_stream_fromに シンボルと文字列が混在して渡されていたケース で挙動が変わる可能性があります。- 例: これまでは「たまたま購読が解除されていなかった」コードが、今回の修正で正しく解除されるようになります。
- Action Cable の
期待される挙動の変化
- これまで:rubyのようなコードでは、購読が解除されず、コネクションが生きている限り配信を受け続けていた。
stream_from :chat stop_stream_from :chat - 修正後:
- 同じコードで、正しく購読解除 (
pubsub.unsubscribe) が行われる。 - メモリリークや不要なブロードキャスト受信が減る可能性があります。
- 同じコードで、正しく購読解除 (
- これまで:
注意点
stream_from/stop_stream_from両方で同じ値を渡しているつもりでも、「片方 Symbol / 片方 String」のようなコードがもしあった場合、そのコードは今後 正しく購読が解除される ようになります。- もし既存の挙動(バグ)に依存していた場合は影響がありますが、通常は正しい方向の修正です。
- カスタム実装で
streamsハッシュを直接触っている場合、キーの型がStringに統一される前提でコードを書いたほうが安全です。
- 参考情報 (あれば)
- 対象コード:
actioncable/lib/action_cable/channel/streams.rbActionCable::Channel::Streamsモジュール内のstream_from/stop_stream_from
- 関連するテスト:
actioncable/test/channel/stream_test.rb- 特にシンボルを引数に使う
SymbolChannelのテストケース付近が参考になります。
- 特にシンボルを引数に使う
- バグの性質:
- シンボル/文字列のキー不一致による購読解除漏れ (unsubscribe 漏れ)
- 典型的な「ハッシュキーの型不一致バグ」を、API の挙動を揃えることで解消した形です。
#57691 Iterate a snapshot of the connections in Server#each_connection
マージ日: 2026/6/13 | 作成者: @55728
- 概要 (1-2文で)
Action Cable のServer#each_connectionが、内部のハッシュを直接イテレートするのではなく「スナップショット(配列)」をイテレートするように変更され、接続の追加・削除が同時に起きてもRuntimeError: can't add a new key into hash during iterationが発生しないようになりました。これにより、ハートビートやサーバ再起動時の接続処理が安定して動作します。
- 変更内容の詳細
背景: どんな問題だったか
ActionCable::Server::Base の each_connection は、これまで connections_map.each_value を使って「生のハッシュ」を直接イテレートしていました。
一方で、以下の 3 つの処理がイベントループ / executor スレッド上で each_connection を呼んでいる一方、ワーカー側で add_connection / remove_connection が並行して走る構造になっています。
- 3 秒ごとのハートビートruby
executor.post { each_connection(&:beat) } - サーバ再起動時のクローズ処理ruby
each_connection { |c| c.close(...) } - 統計取得ruby
open_connections_statistics # 内部で each_connection.map(&:statistics)
Ruby の Hash はイテレーション中に新しいキーを追加すると RuntimeError: can't add a new key into hash during iteration を投げるため、connections_map.each_value 実行中に add_connection が走ると例外が発生し、以下のような不具合につながっていました。
- ハートビートが例外で中断し、古い/stale な接続がクリーンアップされない
- コードリロード時の再起動スイープが途中で止まり、接続が閉じられない
これは、接続ストアを Array から Hash に変更したコミット(8ed78693c5)以降のリグレッションです。
Array はイテレーション中に << しても例外にならない一方、Hash はキー追加に厳密なので挙動が変わってしまった形です。
修正内容: スナップショットをイテレートする
each_connection の実装を変更し、connections_map を直接イテレートせず、「スナップショット配列」を経由するようにしました。
変更ポイントは以下の通りです(概念的なイメージ):
# 変更前(イメージ)
def each_connection(&block)
if block_given?
connections_map.each_value(&block)
else
connections_map.each_value
end
end
# 変更後(イメージ)
def each_connection(&block)
if block_given?
connections.each(&block) # connections は connections_map.values を返す
else
connections.each # Array#each なので Enumerator もスナップショットに対するもの
end
endconnections メソッドは既に実装済みで、内部的には connections_map.values を返すだけのメソッドです。
これにより、each_connection は常に「現在の connections_map の値をコピーした配列」に対して each するようになります。
これで、イテレーション中に add_connection が connections_map に新しいエントリを追加しても、each の対象は既に取得済みの配列であり、Hash の「イテレーション中にキー追加」には該当しないため、例外が発生しなくなります。
ブロックなし呼び出しも維持
open_connections_statistics のように、each_connection をブロックなしで呼び出して Enumerator を受け取るケースもあります:
each_connection.map(&:statistics)Array#each は Enumerator を返すため、each_connection の戻り値もそのまま Enumerator となり、
既存コードはインターフェースを変えずに動作します。
この Enumerator も、connections_map のスナップショット配列に対するものなので安全です。
テストの追加
回帰を防ぐためにテストが追加されています(actioncable/test/server/base_test.rb)。
テストの内容(要約):
- ある接続の
#beatメソッド内でadd_connectionを呼び出すようにセットアップ each_connection(&:beat)を実行- その際に
RuntimeErrorが発生しないことをアサートする
これにより、each_connection のイテレーション中に add_connection が走る典型的な競合パターンがカバーされます。
- 影響範囲・注意点
対象コンポーネント
- Action Cable サーバ (
ActionCable::Server::Base) の接続管理まわり - 具体的には、ハートビート、接続クローズ処理、接続数/統計取得など
each_connectionに依存する箇所
- Action Cable サーバ (
挙動面の変化
each_connectionは「呼び出し時点の接続一覧」をスナップショットとして処理するようになります。- イテレーション中に新たに追加された接続は、その回の
each_connectionでは処理されません(次回以降の呼び出しで処理される)。 - イテレーション中に削除された接続がスナップショット側に含まれている場合、すでにクローズ済みかどうかを呼び出し側で考慮する必要がありますが、従来コードも実質的には同種の競合を抱えていたため、実務上の差分は小さいと考えられます。
メリット
- 高負荷時でも、ハートビートやコードリロード時の再起動処理が Hash まわりの
RuntimeErrorによって中断されなくなります。 - マルチスレッド環境で Action Cable を使っているアプリケーションにおいて、
「たまにハートビートで例外が出て stale connection が残り続ける」といった問題が解消される可能性があります。
- 高負荷時でも、ハートビートやコードリロード時の再起動処理が Hash まわりの
パフォーマンス面の影響
each_connection呼び出し毎にconnections_map.valuesで配列コピーが発生します。- 接続数が非常に多い(数万オーダー)環境では、このコピーコストがわずかに増える可能性がありますが、
- もともとのハートビートは 3 秒毎
- 再起動・統計取得も高頻度の処理ではない
ため、安定性向上とのトレードオフとしては妥当と考えられます。
- 既に
connectionsメソッド自体が同じコピー処理をしており、それを使うようにしただけなので、新しく増えたコストは実質ありません。
- 参考情報 (あれば)
- 回帰の原因となったコミット:
8ed78693c5- 接続ストアを Array から Hash に変更したことで、Hash への追加時の制約に引っかかるようになった。
- Ruby の仕様:
Hash#each等でイテレート中に新しいキーを追加するとRuntimeError: can't add a new key into hash during iterationが発生する。Array#eachはイテレーション中の<<に対して例外を出さない(ただし、同じループで新規要素が処理されるかどうかは別)。
#57675 Don't mutate the caller's string in Inflector#transliterate
マージ日: 2026/6/13 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::Inflector.transliterateが、非 frozen かつ非 UTF-8 の文字列引数を「こっそり破壊的変更」してしまう不具合を修正し、常に引数を複製してから処理するようにした PR です。これによりparameterizeも含め、呼び出し元の文字列オブジェクトのエンコーディングやバイト列が書き換わらないことが保証されます。
- 変更内容の詳細
問題の内容
ActiveSupport::Inflector.transliterate は内部で以下のような処理を行います(概略):
- 引数が ASCII のみの場合は早期
return string.dup(このパスは以前から安全)。 - それ以外の場合は、
force_encodingやencode!を呼んで UTF-8 に揃えたうえで、近似のローマ字表現などに変換。
問題は、「frozen でない非 UTF-8 文字列」を渡した場合に起きていました。
s = "中文".encode(Encoding::GB18030)
s.frozen? # => false
ActiveSupport::Inflector.transliterate(s)
# ここで本来は s は変化しない想定だが…
s.encoding # expected: GB18030, actual: UTF-8 # ← encoding が書き換わっている原因:
- 以前の実装では「frozen なら dup してから処理する」が、「非 frozen ならそのまま
force_encoding/encode!」というパスが存在。 - そのため、非 frozen の引数については、呼び出し側が渡した同じオブジェクトに対して エンコーディング変更・バイト列の再エンコードが走っていた。
この挙動は Ruby/ActiveSupport でよくある「非破壊メソッド(見た目)」の期待に反し、parameterize も transliterate を内部で使うため、同様に「引数を破壊的に変更する」副作用を持っていました。
修正内容
transliterate の中で、「frozen のときだけ dup する」ロジックを「常に dup する」ように変更しました。
以前(イメージ):
def transliterate(string, ...)
return string.dup if string.ascii_only?
# ここで frozen の場合のみ dup
string = string.dup if string.frozen?
string.force_encoding(Encoding::UTF_8)
string.encode!(...)
...
end修正後(イメージ):
def transliterate(string, ...)
return string.dup if string.ascii_only?
# frozen かどうかに関係なく必ず dup
string = string.dup
string.force_encoding(Encoding::UTF_8)
string.encode!(...)
...
endポイント:
- 早期リターンの
return string.dup if string.ascii_only?はそのまま。 - それ以外のパスに入った時点で「必ず dup 済みオブジェクト」に対して
force_encoding/encode!を行うようになったため、呼び出し側が持っている元の文字列は一切触られない。
テストの追加
activesupport/test/transliterate_test.rb に 3 つの回帰テストが追加されています。
- 非 frozen な GB18030 文字列を
transliterateに渡しても、元の文字列の encoding / bytes が変わらないこと。 - 非 frozen な US-ASCII(中身は非 ASCII バイト)を渡しても、同様に encoding / bytes が変わらないこと。
parameterizeに GB18030 文字列を渡しても、元の encoding が変わらないこと。
これらは修正前は「encoding が UTF-8 に書き換わる」ために失敗し、修正後は全て成功します。
- 影響範囲・注意点
挙動の本来の期待値に揃える後方互換修正
表面的には「バグ修正」であり、transliterate/parameterizeが非破壊であるという一般的な期待に沿うものです。ほとんどのコードにとっては望ましい・安全な変更です。パフォーマンス面の影響(軽微だが存在)
これまで「非 frozen かつ非 UTF-8」の場合はそのままのオブジェクトを使っていたところを、必ずdupするようになりました。transliterate/parameterizeを非常に大量・高頻度に呼んでいる場合は、オブジェクト生成コスト・GC にごく僅かな影響が出る可能性があります。- ただし、既に「frozen 文字列」で呼ばれるケースでは元々 dup しており、また ASCII のみ文字列はもともと
string.dupで早期 return していたため、新規に増えたのは「非 frozen かつ非 UTF-8」のパスのみです。
「暗黙の破壊的挙動」に依存していたコードは影響を受ける
正常な設計ではありませんが、万が一「transliterateに渡すと encoding が UTF-8 に書き換わる」という副作用に意図的・無意識に依存していたコードがある場合、- 今後は「呼び出し元の文字列は元の encoding のまま」になります。
- 必要なら
string.encode!(Encoding::UTF_8)のように「自分で明示的に」変換すべきです。
マルチバイト・非 UTF-8 ロケール環境での安定性向上
GB18030 や Shift_JIS などの非 UTF-8 文字列を扱うコードベースでも、transliterate/parameterize呼び出しが元のデータを壊さないことが保証されるため、バグの温床が一つ減ります。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57675
- 関連コミット(過去の部分修正):
5623f7f1a3- frozen + ascii_only のケースに対する guard を追加していたが、今回の「非 frozen 非 UTF-8」パスはカバーしていなかった。
- 関連メソッド:
ActiveSupport::Inflector.transliterateString#parameterize(ActiveSupport拡張)
#57676 Fix StructuredEventSubscriber.debug_only leaking across subscriber subclasses
マージ日: 2026/6/13 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::StructuredEventSubscriberのdebug_onlyで設定したメソッド名が、サブクラス間や基底クラスに「漏れ」て共有されてしまうバグを修正する PR です。class_attributeのデフォルト配列を破壊的に変更していたために起きていた問題を、非破壊的な再代入に変えることで解消しています。
- 変更内容の詳細
問題のポイント
ActiveSupport::StructuredEventSubscriberではdebug_methodsが次のようにclass_attributeで定義されている(実際の定義は省略):rubyclass_attribute :debug_methods, default: []debug_only :method_nameを呼ぶと、このdebug_methodsにメソッド名を追加して「デバッグ環境のみでサイレンスするイベント」を指定する仕組み。- しかし、実装が次のように破壊的追加になっていた:ruby
self.debug_methods << method class_attributeのdefault: []は 1 個の同じ配列オブジェクトを基底クラスとサブクラスで共有するため、「破壊的に push する」と全クラスで同じ配列が書き換えられてしまう。
その結果:
sx = Class.new(ActiveSupport::StructuredEventSubscriber) { def foo(e); end; debug_only :foo }
sy = Class.new(ActiveSupport::StructuredEventSubscriber) { def bar(e); end; debug_only :bar }
sx.debug_methods # => [:foo, :bar] # 本当は [:foo] が期待
sy.debug_methods # => [:foo, :bar] # 本当は [:bar] が期待
ActiveSupport::StructuredEventSubscriber.debug_methods # => [:foo, :bar] # 本当は [] が期待- さらに深刻なのは、ユーザーが自分の
StructuredEventSubscriberにたまたま:sqlや:render_templateなどフレームワーク側と同名のメソッドを持っていると、そのイベントが「debug_only として扱われてしまい本番環境で黙ってしまう」可能性がある点。
修正内容
debug_only 内の 1 行だけを修正:
- self.debug_methods << method
+ self.debug_methods += [method]意味:
<<は既存配列オブジェクトを破壊的に変更する。+= [method]は「新しい配列を作って代入」するので、class_attributeの writer 経由でクラスごとに独立した配列がセットされる。class_attributeはサブクラスで書き込みが行われたタイミングで、そのクラス専用の値を持つようになるため、この書き方で「サブクラスごとに別々のdebug_methods」を持てる。
この方針は、同じく class_attribute を使っている ActiveSupport::LogSubscriber.subscribe_log_level の実装(self.log_levels = log_levels.merge(...))と同じノンミュータブルな書き方にそろえたものです。
テストの追加・修正
新規テスト:
test_debug_only_does_not_leak_across_subclasses目的:
- 2 つの異なるサブクラスそれぞれが
debug_onlyしたメソッド名のみを、自分のdebug_methodsに持つこと。 - 基底クラス
ActiveSupport::StructuredEventSubscriberのdebug_methodsは空のままで汚染されないこと。
検証内容(概念的にはこういうことをチェック):
rubysx = Class.new(ActiveSupport::StructuredEventSubscriber) do def foo(e); end debug_only :foo end sy = Class.new(ActiveSupport::StructuredEventSubscriber) do def bar(e); end debug_only :bar end assert_equal [:foo], sx.debug_methods assert_equal [:bar], sy.debug_methods assert_equal [], ActiveSupport::StructuredEventSubscriber.debug_methods- 修正前は
[:debug_only_event, :foo, :bar]のようにすべて混ざってしまい、テストが落ちる。 - 修正後は期待通りクラスごとに分離されてテストが通る。
- 2 つの異なるサブクラスそれぞれが
既存テスト
test_debug_only_methodsの修正:- もともとは「基底クラスにサブクラスの
debug_onlyが漏れている」ことに依存してテストが通っていた。 - 今回の修正によりその前提が成り立たなくなるため、テスト対象を「実際に
debug_onlyを宣言しているTestSubscriberサブクラス」側に付け替えた。 - さらにテストの teardown に
detach_from :testを追加し、ActiveSupport::Notificationsへの subscription を後始末するようにしている。これによりテスト順序に依存しない(サブスクが他のテストに影響しない)ようになる。
- もともとは「基底クラスにサブクラスの
- 影響範囲・注意点
対象:
ActiveSupport::StructuredEventSubscriberを直接 / 間接的に継承し、debug_onlyを使っている全てのコード。
仕様面の挙動変化:
- これまで「意図せず他サブスクライバの
debug_only設定が効いてしまっていた」ケースが、本来の期待通り効かなくなる(= バグ修正)。 - 特に、Rails 本体の
StructuredEventSubscriber(例: SQL, rendering まわり)が設定しているdebug_onlyが、ユーザー定義サブクラスに影響することはなくなる。
- これまで「意図せず他サブスクライバの
実運用で起こりうる変化:
- もし既存コードが「たまたまバグに依存していた」場合、たとえば:
- A サブスクライバが
debug_only :foo - B サブスクライバも
def foo(e); endを持っているがdebug_onlyは呼んでいない - それでも B の
fooイベントが本番で出力されない(A のdebug_onlyが漏れていた)
- A サブスクライバが
- 今後は B 側には
debug_onlyが効かなくなり、本番環境でもfooが通知されるようになる。 - これは意図された挙動への修正だが、「出力が増えた」と感じる場面があるかもしれないので、ログ周りで挙動が変わったと感じた場合はこの修正を疑うとよいです。
- もし既存コードが「たまたまバグに依存していた」場合、たとえば:
パフォーマンス:
+= [method]によって小さい配列のコピーが行われる程度で、通常の利用では誤差レベルと考えてよいです。class_attributeの「書き込み時にクラス単位でコピーを作る」という設計に沿った使い方になったため、むしろ一貫性は向上しています。
- 参考情報 (あれば)
関連する Rails の仕組み:
ActiveSupport::StructuredEventSubscriber- 構造化されたイベント(通知)を購読し、特定のメソッド名をハンドラとして扱う仕組み。
debug_onlyで指定したメソッドは、開発/テスト環境では実行するが、本番ではサイレンスするなどの制御に利用される。
class_attribute- クラスレベルのアクセサを定義し、サブクラスごとに上書き可能な「継承されるクラス変数」のような仕組み。
- デフォルト値がミュータブル(
[]や{})な場合は、破壊的操作を避け、代入で上書きするのが定石。
類似コード:
ActiveSupport::LogSubscriber.subscribe_log_levelでも同様のパターンで:rubyのように破壊的操作ではなく再代入している。self.log_levels = log_levels.merge(...)
この PR により、debug_only の振る舞いがクラスごとにきちんと独立し、意図しないイベントサイレンスが起きづらくなります。
#57693 Make ActiveRecord::Base.with call Object#with when passed a block
マージ日: 2026/6/13 | 作成者: @janko
- 概要 (1-2文で)
ActiveRecord::Base の.withがブロック付きで呼ばれた場合に、CTE 用のActiveRecord::Relation#withではなく、Ruby コア拡張のObject#withを呼ぶようにした変更です。これにより、Active Record モデルでwithを使って一時的に設定を上書きするパターンが自然に利用できるようになります。
- 変更内容の詳細
これまでの挙動
ActiveRecord::Base.with は ActiveRecord のクエリ用 DSL における CTE (Common Table Expression, WITH 句) を構築するためのメソッドとして、ActiveRecord::Relation#with に委譲していました。
# 例: CTE を使ったクエリ
User.with(active_users: User.where(active: true))
.from("active_users")
.where(...)Relation#with はクエリ構築専用のため、ブロックは受け付けませんでした。
そのため、以下のように「オブジェクトを特定のオプションと一緒にブロックに渡す」という Object#with 的な使い方はできませんでした。
# 想定していたが動かなかった使い方 (ブロックを渡すとエラー/無視される)
ActiveRecord::Base.with(logger: Logger.new($stdout)) do
# ...
end今回の変更点
ActiveRecord::Base.with を呼び出した際に ブロックが渡されているかどうか を判定し、挙動を切り替えるようになりました。
- ブロックなし: 今まで通り、クエリ用の
Relation#withを呼び出して CTE を構築する - ブロックあり: CTE ではなく、
Object#withを呼び出す(core extension がロードされている場合)
Rails には ActiveSupport のコア拡張として Object#with が存在し、典型的には以下のような振る舞いをします(概念的な例):
object.with(foo: 1, bar: 2) do
# object に foo/bar が一時的に適用された状態でブロック実行
end今回の変更により、ActiveRecord::Base もこのパターンに従うようになります。
サンプル: 一時的に logger を差し替える
PR の説明にある典型例:
ActiveRecord::Base.with(logger: Logger.new($stdout)) do
# このブロック内だけ SQL ログを標準出力に出す
User.create!(...)
User.first
end
# ブロックを抜けると、元の logger に戻るこれはテスト時に一時的に SQL ログを有効化したい/別の出力先に切り替えたいときなどに便利です。
実装上のポイント
activerecord/lib/active_record/querying.rb内のBase.withの実装が修正され、block_given?を見て処理を分岐するようになっています。- ブロック付き呼び出しで
Object#withを呼ぶのは、Enumerable#find・Enumerable#selectと ActiveRecord のfind・whereの関係と同様のポリシーに沿っています。- すなわち、「ブロックがあるときは Ruby/Enumerable 側の意味を優先する」という一貫した挙動です。
- テスト (
relation/with_test.rb等) が追加・更新され、ブロック付き.withがObject#withを呼んでいることが検証されています。 activerecord/CHANGELOG.mdにこの挙動変更が明記されています。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは
ActiveRecord::Base.withを「ブロック付き」で呼んでいるコード だけです。 - 既に大半の CTE 用コードはブロックなしで
.withを使っているはずなので、その場合は挙動は一切変わりません。
注意点
ブロック付き
.withで CTE は作れない
今回の仕様により、ブロック付き.withは「常にObject#withを意図したもの」と解釈されます。
「ブロック付きで CTE を何かしら操作したい」ような使い方はできません。Object#withコア拡張のロードが前提- ActiveSupport の core extensions(特に
Object#with)がロードされていない環境では、ブロック付き.withがNoMethodErrorになる可能性があります。 - Rails 標準のアプリケーション構成であれば通常はロードされているため問題になりにくいですが、ミニマル構成・一部のエンジン等では注意が必要です。
- ActiveSupport の core extensions(特に
メソッド解決の優先度の理解が必要
.withというメソッド名は、Active Record (CTE) 用と Object コア拡張の両方で使われているため、「ブロックの有無で意味が変わる」点をチームで共有しておいた方がよいです。whereとfindがブロック有無で Enumerable とクエリメソッドを切り替えるのと同じノリですが、withは使用頻度が低く気付きづらい可能性があります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57693
- 同系統の挙動:
Model.where { ... }→ Enumerable のselect的意味 (ブロック) と Relation のwhere(ハッシュ・引数) を使い分けModel.find { ... }→ Enumerable のfindにフォールバック
- 用途イメージ:
- 一時的なログ出力設定 (
logger) - 一時的に
ignored_columnsやdefault_scopes相当のカスタムアクセサを変更 - 特定のテストケースでのみ設定を上書きして実行、など
- 一時的なログ出力設定 (
#57689 Add test coverage for HashWithIndifferentAccess key access and except
マージ日: 2026/6/13 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveSupport::HashWithIndifferentAccessについて、これまでテストされていなかった分岐や挙動(fetchのブロック・デフォルト値、values_atの欠損キー、digの miss パス、except/withoutの基本仕様)に対してテストが追加されました。アプリケーションコードへの変更はなく、テスト追加のみのPRです。
- 変更内容の詳細
このPRでは activesupport/test/hash_with_indifferent_access_test.rb に 33 行のテストコードが追加されています。主な対象は以下の4点です。
2.1 fetch の未テスト分岐(デフォルト値・ブロック・*extras)
追加でカバーされた挙動:
fetchに デフォルト値 を渡した場合の挙動fetchに ブロック を渡した場合の挙動HashWithIndifferentAccess独自の「キーを String に変換したうえでブロックへ渡す」挙動fetchの*extras引数(ブロックに追加引数を渡すパススルー)の挙動
イメージとしては以下のようなテストが追加されていると考えられます:
h = ActiveSupport::HashWithIndifferentAccess.new(a: 1)
# デフォルト値
assert_equal 1, h.fetch(:a, 10) # 既存キー => 値
assert_equal 10, h.fetch(:b, 10) # 欠損キー => デフォルト値
# ブロック(キーは String で渡される)
value = h.fetch(:b) { |key, extra| [key, extra] }
assert_equal ["b", :foo], value # key は "b"(String)、extra は *extras 経由※ 実際のテストコードの細部は異なる可能性がありますが、
- ブロックに渡るキーが String 化されること
*extrasをきちんと透過していること- ドキュメントどおりの挙動が保証されること
を確認するテストが追加されています。
2.2 values_at の欠損キー時の挙動
追加でカバーされた挙動:
values_atに存在しないキーを渡したとき、その位置にnilが入ることfetch_valuesは同じ状況で例外を投げる(という仕様との対比)
例:
h = ActiveSupport::HashWithIndifferentAccess.new(a: 1)
assert_equal [1, nil], h.values_at(:a, :b) # :b は存在しない => nil
# fetch_values(:a, :b) は KeyError を raise する想定これにより、values_at の「欠損は nil 埋め」という挙動が仕様どおりであることを回帰テストとして担保しています。
2.3 dig の欠損キー時の挙動(miss パス)
追加でカバーされた挙動:
digが存在しないキーを辿ろうとした際にnilを返す、というドキュメント記載の miss パス
既存テストでは、dig が成功して値を返すパスのみカバーされていましたが、今回:
h = ActiveSupport::HashWithIndifferentAccess.new(a: 1)
assert_nil h.dig(:zoo) # ドキュメント例: counters.dig(:zoo) # => nilのような「キーが存在しない場合」の挙動をテストしています。HashWithIndifferentAccess かつ dig という組み合わせで、シンボル/文字列のどちらでも miss 時は nil を返すことが保証されます。
2.4 except / without の基本仕様(これまでテストなし)
今回もっとも大きい追加カバレッジ:
except/withoutが まったくテストされていなかった ため、以下を確認するテストが追加されました。
カバーされる挙動:
新しい
HashWithIndifferentAccessを返す- レシーバとは別オブジェクトであること
- 戻り値のクラスが
HashWithIndifferentAccessであること
文字列キー・シンボルキーを混在して受け取れる
:aや"a"を指定しても、同じキーとして扱われる
元のハッシュは変更されない (non-destructive)
except/withoutの呼び出し後もレシーバはそのまま
例:
h = ActiveSupport::HashWithIndifferentAccess.new(a: 1, b: 2, "c" => 3)
h2 = h.except(:a, "b")
assert_instance_of ActiveSupport::HashWithIndifferentAccess, h2
assert_equal({ "c" => 3 }, h2) # :a, "b" が除外された新しいハッシュ
assert_equal 3, h2[:c] # indifferent access 維持
assert_equal 1, h[:a] # 元の h は変わらない
assert_equal 2, h[:b]
assert_equal 3, h[:c]without は except のエイリアスであるため、同等の挙動を確認するテストも含まれていると考えられます。
- 影響範囲・注意点
影響範囲
- 変更はテストファイル(
hash_with_indifferent_access_test.rb)のみで、本体コードには一切変更がありません。 - 既存アプリケーションの挙動には影響しませんが、今後これらの挙動を変えるとテストが落ちるようになりました。
- 特に
except/withoutはこれまでテストがなかったため、将来のリファクタリング時に仕様変更を誤って導入しにくくなります。
- 変更はテストファイル(
注意点
- このPRは「ドキュメントされている/既にそうであると仮定されていた」挙動を、テストとして明示的に固定化したものです。
- 既に
HashWithIndifferentAccess#fetchのブロック引数として「文字列キーが来る」前提でコードを書いている場合、その前提が今後も破られにくくなった一方、「実はシンボルになるべき」といった解釈変更はしづらくなります。 except/withoutが常に非破壊でHashWithIndifferentAccessを返すことが仕様として固まるため、これと異なる期待(たとえば破壊的に動いてほしい等)は今後も満たされません。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57689
- 関連ドキュメント(HashWithIndifferentAccess):
https://api.rubyonrails.org/classes/ActiveSupport/HashWithIndifferentAccess.html HashWithIndifferentAccess#digのドキュメント例:counters.dig(:zoo) # => nilという miss パスが公式に例示されています。
#57680 Ensure assert_no_changes isn't given a static value
マージ日: 2026/6/12 | 作成者: @amomchilov
- 概要 (1-2文で)
assert_no_changes(および関連メソッド)が「静的な値」を引数に取った場合に、対象が実際には変化していてもテストが常に成功してしまう問題を防ぐため、引数に許可される型を制限し、そうでない場合はArgumentErrorを発生させるようにした変更です。これにより、書き間違い・勘違いによるテストの空振りを早期に検知できるようになります。
- 変更内容の詳細
何が問題だったか
これまでの assert_no_changes は、第一引数に任意のオブジェクトを受け取り、それをそのまま評価対象として扱っていました。
a = []
assert_no_changes(a.size) do
a << "something"
end上記のように誤って「値そのもの」(この例だと 0)を渡すと、
- ブロック実行前: 評価対象 =
0 - ブロック実行後: 評価対象 =
0(元のオブジェクトはすでに固定値)
となるため、「変化なし」と判定されてテストが成功してしまいます。
本来は下記のように「値を返す処理」か「評価可能な Ruby コード」を渡す必要があります。
assert_no_changes(-> { a.size }) do
a << "something"
end
# または
assert_no_changes("a.size") do
a << "something"
end今回の変更内容
この PR では、assert_no_changes が受け取る「監視対象 expression」に対して、以下の制約を導入しました。
- 許可される型:
- Callable(通常は
Proc/lambda/->) StringSymbol
- Callable(通常は
- 上記以外のオブジェクトを渡すと
ArgumentErrorを投げる
擬似コードで表すと、イメージは以下のようなチェックです(実装そのものではなく概念的なもの):
def assert_no_changes(expression, &block)
unless expression.respond_to?(:call) || expression.is_a?(String) || expression.is_a?(Symbol)
raise ArgumentError, "assert_no_changes requires a callable, String, or Symbol expression"
end
# 従来通りの比較ロジック
endつまり、次は OK:
assert_no_changes(-> { user.reload.name }) { ... }
assert_no_changes("user.reload.name") { ... }
assert_no_changes(:some_method_name) { ... } # 実装依存だが概ね許容次は NG (ArgumentError):
assert_no_changes(0) { ... }
assert_no_changes([]) { ... }
assert_no_changes(Object.new) { ... }追加テスト
activesupport/test/test_case_test.rb にテストが追加され、主に次の点が確認されています。
Proc/String/Symbolを渡した場合に従来通り動作することIntegerなど静的値を渡した場合にArgumentErrorが発生すること- これまで「たまたま
to_sで Ruby コードになる」ような特殊なオブジェクトは、今後はArgumentErrorになること
また CHANGELOG.md に、この動作変更が明記されています。
- 影響範囲・注意点
既存コードへの影響
- 影響を受けるのは、
assert_no_changes(および同様の実装がある「友達メソッド」)に対して、- 第一引数に「callable でも String でも Symbol でもないオブジェクト」を渡しているテストだけです。
- 特によくある誤用パターンとしては:
- 「メソッド呼び出し結果」をそのまま渡してしまっているケース
# NG: 呼び出し結果を渡している
assert_no_changes(user.reload.name) do
user.update!(name: "Bob")
end
# OK: 呼び出しを遅延させる
assert_no_changes(-> { user.reload.name }) do
user.update!(name: "Bob")
end
# あるいは
assert_no_changes("user.reload.name") do
user.update!(name: "Bob")
end「静的な String / Symbol」の限界
PR でも明記されていますが、この変更はあくまで「明らかな誤用」を防ぐものであり、完全に誤用を排除できるわけではありません。
String/Symbolは「Ruby コードとしてevalされるもの」とみなされるため、- 静的な文字列でも
evalが成功する場合は、そのまま通ってしまう可能性があります。
- 静的な文字列でも
- ただし、ほとんどの「ただの文字列」は Ruby として評価すると
NameErrorなどになるので、サイレントに通り抜けるケースは少ないだろう、という判断です。
例:
oncall = "Alice"
assert_no_changes(oncall) do # => NameError: uninitialized constant Alice
oncall = "Bob"
end以前「たまたま動いていた」変なコードの破壊的変更
PR の例:
class Hmm
def to_s = "x" # 下の文脈では eval 可能
end
def test_hmm
x = 1
assert_no_changes(Hmm.new) do
# no change
end
end- 以前:
Hmm.new.to_s #=> "x"がevalされて(たまたま)動いていた - 今後:
Hmm.newは callable/String/Symbol ではないのでArgumentError
もし本当にこのような(かなりレアな)コードを書いていた場合は、素直に to_s を呼ぶようにすることで回避できます。
assert_no_changes(Hmm.new.to_s) do
# ...
end- 参考情報 (あれば)
- 対象メソッド:
ActiveSupport::Testing::Assertions#assert_no_changes(およびフレンドメソッド) - 典型的な正しい使い方(改めて):
# ブロック実行で user.name が変わらないことを検証したい場合
# 推奨(lambda / Proc)
assert_no_changes(-> { user.reload.name }) do
do_something
end
# もしくは String(eval ベース)
assert_no_changes("user.reload.name") do
do_something
end- 今回の変更は「テストが静かに間違った成功をする」パターンを減らすための、安全性を高めるための軽い破壊的変更と言えます。
#57057 Report Postgres FATAL exceptions more clearly
マージ日: 2026/6/12 | 作成者: @matthewd
- 概要 (1-2文で)
PostgreSQL から返されるFATALエラーを、一般的なStatementInvalidではなく「接続が壊れた」ことを示すConnectionFailedとして明示的に扱うように変更し、壊れた接続を後からまで引きずらないようにした PR です。
そのために libpq の使い方をexec_params依存から、send_query_params+ 明示的なget_resultループや notice receiver の活用に変更し、より確実に「本当の例外」を捕捉できるようにしています。
- 変更内容の詳細
2-1. 何を解決しようとしているか
PostgreSQL の FATAL エラーは「サーバー側で接続を終了する」ことを意味するため、その後のコネクションは使い物になりません。
しかし従来の Active Record では、FATAL も他の SQL エラーと同じように StatementInvalid として扱われるケースがあり、その結果:
- 壊れたコネクションがプールに残る
- 次のクエリ実行時にはじめて接続切断に気付く
という遅延的かつ分かりにくい障害につながる可能性がありました。
この PR は:
FATALエラーを検知したら、その場でConnectionFailed相当として扱い直し- コネクションプール側からも壊れたコネクションとして扱えるようにする
ことで、より早く・明示的に障害を表面化させる狙いがあります。
2-2. PostgreSQL アダプタ側の主な変更点
2-2-1. exec_params から send_query_params + 明示的 get_result へ
これまで多くのパスで使われていた raw_connection.exec_params(...) をやめ、以下の形に近い実装に変えています:
raw_connection.send_query_params(sql, param_types, param_values, result_format)
raw_connection.set_single_row_mode if need_single_row # 例: streaming 的なケース
results = []
loop do
result = raw_connection.get_result
break unless result
# ここで result.status やエラー情報を精査する
results << result
endポイント:
exec_paramsは内部でsend_query_params+get_last_result相当を実行する「まとめメソッド」ですが、その内部実装に依存していると、「最後の結果」だけからではFATALなど一部の重要なエラーを取りこぼす可能性があります。- 明示的に
get_resultをポーリングすることで:- 通常のエラー
FATALエラーNOTICE/WARNINGなど をより正確かつタイムリーに検出できます。
これに合わせて、PostgreSQL アダプタ内のクエリ実行系 (execute, prepared statement 実行など) が、この新しいパターンに書き換えられています。
2-2-2. FATAL エラーを ConnectionFailed にマッピング
PostgreSQL のエラー情報から severity (もしくは error_field の PG_DIAG_SEVERITY / PG_DIAG_SEVERITY_NONLOCALIZED) を取り出し、それが FATAL / PANIC の場合には、通常の StatementInvalid ではなく「接続失敗系」の例外に変換します。
擬似的には以下のようなイメージです:
case severity
when "FATAL", "PANIC"
raise ActiveRecord::ConnectionFailed, original_error.message
else
raise ActiveRecord::StatementInvalid, original_error.message
end実際には、ActiveRecord の例外ラッパ (translate_exception / translate_exception_class 的なメソッド) の中で振り分けが行われます。
2-2-3. Notice receiver からの FATAL も検知
PostgreSQL / libpq には、エラーや notice を「結果オブジェクトとして返す」以外に、「notice receiver 経由で非同期的に通知する」経路があります。
libpq の内部状態によっては:
- クエリ実行結果としては成功 / 何も返さない
- しかし notice receiver 経由で
FATALが届く
というパターンも起こり得ます。この PR では:
- PostgreSQL アダプタで notice receiver を設定
- そこに届いたメッセージから
FATAL/PANICを検知 - 「本当は接続が死んでいる」ことを把握し、例外変換時に考慮
するようにしています。
これにより、libpq が「直接のクエリ結果として返さなかった FATAL」も漏れなく拾えるようになります。
2-3. 抽象アダプタ (abstract_adapter.rb) の変更
abstract_adapter.rb には例外変換と接続状態管理の共通処理が含まれています。
主な変更点:
- 例外クラスを決めるロジックに「接続が壊れていると判断できる場合は
ConnectionFailedにする」分岐を強化。 - PostgreSQL アダプタが「これは
FATALだった」とわかるような情報を渡し、それを尊重して適切な ActiveRecord 例外にマッピングする。
これにより、DB ごとに異なる「致命的エラー」の検出方法をアダプタ側が実装し、抽象アダプタはそれを統一的な ActiveRecord 例外クラスに落とし込む、という役割分担が明確化されています。
2-4. テストの追加・変更
変更されたテストファイル:
activerecord/test/cases/adapter_test.rbactiverecord/test/cases/adapters/postgresql/postgresql_adapter_test.rbactiverecord/test/cases/adapters/postgresql/statement_pool_test.rb
テストで主に担保していること:
FATAL相当のエラーが発生したとき、ActiveRecord::ConnectionFailedが発生すること- その後の接続が再利用されず、適切に再接続 / プールからの切り離しが行われること
- statement pool など、prepared statement やキャッシュ周りの挙動が今回の変更で壊れていないこと
postgresql_adapter_test.rb の変更行数が多いことから、PostgreSQL 向けのパスについてかなり細かくケースを足していると推測できます。
- 影響範囲・注意点
3-1. アプリケーションから見える挙動の変化
例外クラスが変わる可能性があります。
これまで:
FATALに相当する PostgreSQL エラーが起きても:ActiveRecord::StatementInvalidやそのサブクラスとして扱われていた- アプリケーション側が
StatementInvalidを rescue してリトライ等していた
今後:
- 同じ状況で
ActiveRecord::ConnectionFailed(もしくは関連する接続エラー系) が発生するようになります。 rescue/ エラーハンドリングで「SQL 文法エラー等」と「接続切断」を区別しやすくなりますが、既存コードがStatementInvalidのみを想定している場合には挙動が変わる可能性があります。
対策例:
begin
Model.connection.execute("...")
rescue ActiveRecord::ConnectionFailed => e
# 接続切断: コネクション再確立やリトライ戦略を検討
rescue ActiveRecord::StatementInvalid => e
# SQL 文法ミスや constraint violation など
endConnectionFailed を上位に rescue することで、より正しい分類ができます。
3-2. コネクションプール / 再接続の挙動
FATAL で落ちたコネクションが:
- プール内に「生きているように見える」状態で残る
- 次のクエリで初めて「connection is closed」的なエラーになる
といったパターンが減り、その場で接続エラーとして扱われます。
その結果:
- トラフィックが高い環境では、「たまたま次のリクエストで初めて気付く」タイムラグが減る
- 一方で、エラーが発生したリクエストの時点で即座に
ConnectionFailedが飛ぶため、「前は通っていたのに今回は接続エラーになった」という違いが観測されるかもしれません。
3-3. PostgreSQL / libpq バージョン依存の可能性
FATAL/PANICの severity の扱い、notice receiver への振り分けなどは、PostgreSQL サーバ / libpq のバージョンや設定によって微妙に違う場合があります。- この PR はそのばらつきをある程度吸収しようとしているものの、古い組み合わせなどではテストでカバーされていない挙動があり得ます。
大規模システムや独自のコネクション設定をしている場合は、ステージング環境で:
- 意図的に DB を落とす
pg_terminate_backendでセッションを kill する- ネットワーク切断をシミュレートする
などして、「どういう例外が上がるか」「再接続戦略に問題がないか」を確認しておくと安全です。
- 参考情報 (あれば)
- PostgreSQL のエラーレベル (
ERROR,FATAL,PANICなど) 仕様:
https://www.postgresql.org/docs/current/protocol-error-fields.html
特にPG_DIAG_SEVERITY/PG_DIAG_SEVERITY_NONLOCALIZEDの説明 - libpq の非同期クエリインターフェイス (
PQsendQueryParams,PQgetResult, notice receiver):
https://www.postgresql.org/docs/current/libpq-async.html
https://www.postgresql.org/docs/current/libpq-notice-processing.html - Rails の例外クラス体系 (ActiveRecord):
ActiveRecord::ConnectionFailed,ActiveRecord::StatementInvalidなど
https://api.rubyonrails.org/classes/ActiveRecord/Errors.html (バージョンに応じた API ドキュメント参照)
この PR により、「クエリ失敗」と「接続自体が死んだ」の区別が明確になったので、アプリケーション側でも例外ハンドリングを整理しておくと、運用時のトラブルシュートがかなり楽になります。
#57656 Support composite primary keys in excluding
マージ日: 2026/6/12 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Relation#excluding(およびエイリアスのwithout)が複合主キーを持つモデルでエラーになっていた問題を修正し、単一主キーと同様に正常動作するようにした PR です。単一主キー向けの挙動は変えず、複合主キーのときだけ where 句の組み立て方を切り替えています。
- 変更内容の詳細
何が問題だったか
excluding は以下のように「指定したレコード(や relation)を結果から除外する」ためのメソッドです。
Post.excluding(post)
Post.excluding(posts_relation)単一主キーのモデルでは問題なく動きますが、複合主キーの場合に ActiveRecord::StatementInvalid が発生していました。
# 単一主キー: OK
Post.excluding(post)
# 複合主キー: NG
Cpk::Book.excluding(book)
# => ActiveRecord::StatementInvalid: no such column: cpk_books.["author_id", "id"]原因は、excluding! が内部で predicate_builder[primary_key, records] を使って除外条件を組み立てている点にあります。
- 単一主キー:
primary_keyは"id"などの文字列 → 正常 - 複合主キー:
primary_keyは["author_id", "id"]のような配列
→ これがそのまま 1 つのカラム名として扱われてしまい、
→ SQL 上では"cpk_books"."[""author_id"", ""id""]"という存在しないカラム参照になりエラー
どう直したか
修正方針は「複合主キーのときだけ、finder と同じ書き方で where 句を組み立てて、それを反転する」というものです。
Rails の finder はすでに複合主キーに対応しており、例えば:
Cpk::Book.where(primary_key => ids)
# primary_key: ["author_id", "id"]
# ids: [[author_id1, id1], [author_id2, id2], ...]という形で複合キーを扱えています。これと同じアプローチで排除条件を作り、その NOT を取るようにしています。
疑似コード的には以下のような分岐になります(※実際のコードは byte-to-byte では異なりますがニュアンスとして):
if Array(primary_key).size == 1
# 既存ロジック (単一主キー) は一切変更なし
predicate = predicate_builder[primary_key, records]
else
# 新ロジック (複合主キー)
# ids は excluding(record) の場合は record の複合キーの値
# excluding(relation) の場合は relation.ids から得た複合キーの配列
ids = relation.ids
clause = build_where_clause(primary_key => ids) # where(primary_key => ids) と同様の処理
predicate = clause.ast.not # それを否定して「除外」条件に
end
@records = where(predicate)ポイント:
- 単一主キーの処理ルートは一切変更していない(バイトレベルで同一)ことが PR で明示されています。
excluding(record)とexcluding(relation)の両方のパターンをテストでカバーしています。- テストは
activerecord/test/cases/excluding_test.rbに複合主キー用のケースを追加 (+21/-1)。
- 影響範囲・注意点
- 影響範囲:
ActiveRecord::Relation#excludingと#withoutを、複合主キーを持つ ActiveRecord モデルで利用しているコード。- 単一主キーのモデルは挙動が変わらないため影響なし。
- 実務的な影響:
- これまで複合主キーのモデルに対して
excluding/withoutを呼ぶと SQL エラーになっていた箇所が、正しく「そのレコード(群)を除外する」動作をするようになります。 excluding(relation)もサポートされているため、サブクエリで取得した複合主キーの集合をまとめて除外するパターンも安全に利用できます。
- これまで複合主キーのモデルに対して
- 注意点:
- 既にエラー回避のためにアプリ側で
where.notや生 SQL で独自に除外処理を書いていた場合、今後はexcluding/withoutを利用する方が一貫したインターフェイスになります。 - ただしこの PR はあくまで「正しい挙動に修正」するものなので、既存のワークアラウンドを削るかどうかは各プロジェクトで判断が必要です。
- 既にエラー回避のためにアプリ側で
- 参考情報 (あれば)
- 対象メソッド:
ActiveRecord::Relation#excludingActiveRecord::Relation#without(excludingのエイリアス)
- 関連する内部 API:
Relation#build_where_clauseRelation#ids(複合主キー時には複合キー値の配列を返す)
- 複合主キーでの finder の使い方(参考イディオム):
# 複合主キー: [:author_id, :id]
# 特定の複合キーを持つレコードを検索
Cpk::Book.find([author_id, id])
# 複数の複合キーの集合で where
Cpk::Book.where([:author_id, :id] => [[1, 10], [1, 11]])この PR は excluding も上記のような finder パターンに揃えていると言えます。
#57670 Fix Hash.from_xml raising Date::Error on type="date" values surrounded by whitespace
マージ日: 2026/6/12 | 作成者: @55728
- 概要 (1-2文で)
Hash.from_xmlがtype="date"の要素に前後の空白文字(改行やスペース)が含まれているとDate::Errorで落ちていた問題を修正した PR です。日付文字列をDate.strptimeに渡す前にto_s.stripすることで、インデント付き・整形済み XML でも問題なくパースできるようになりました。
- 変更内容の詳細
問題の挙動
Rails の ActiveSupport::XMLMini は Hash.from_xml で XML を Hash に変換するとき、type="date" が付いたノードを Date に変換します。
従来の実装:
::Date.strptime(date, "%Y-%m-%d")このとき、XML がインデント・整形されていると、実際のノード内容は以下のように前後に改行やスペースが入った文字列になります。
<event>
<starts-on type="date">
2020-01-01
</starts-on>
</event>Ruby 上では例として:
value = "\n 2020-01-01\n"
Date.strptime(value, "%Y-%m-%d") # => Date::Error: invalid dateDate.strptime は周囲の空白を自動では無視しないため、Hash.from_xml 全体が Date::Error で失敗していました。
require "active_support/core_ext/hash/conversions"
Hash.from_xml(%(<event><starts-on type="date">\n 2020-01-01\n</starts-on></event>))
# これが Date::Error で落ちる一方、type="integer" など他の型は to_i などを通すため、同じような空白があっても問題なくパースできており、型ごとの挙動が不整合でした。
修正内容
activesupport/lib/active_support/xml_mini.rb 内の日付パース部分を 1 行変更:
Before:
"date" => lambda { |date| ::Date.strptime(date, "%Y-%m-%d") },After:
"date" => lambda { |date| ::Date.strptime(date.to_s.strip, "%Y-%m-%d") },ポイント:
to_sで非文字列入力(nil や数字など)もとりあえず文字列化stripで前後の空白(スペース・タブ・改行)を除去boolean型の実装がすでにto_s.stripを使っており、それと揃えた形
これにより、次のような XML でも期待通り Date オブジェクトになります:
xml = <<~XML
<event>
<starts-on type="date">
2020-01-01
</starts-on>
</event>
XML
Hash.from_xml(xml)
# => { "event" => { "starts_on" => Date.new(2020, 1, 1) } }テストの追加・修正
activesupport/test/core_ext/hash_ext_test.rbにテスト追加:
def test_date_type_from_xml_with_surrounding_whitespace
xml = <<~XML
<event>
<starts-on type="date">
2020-01-01
</starts-on>
</event>
XML
hash = Hash.from_xml(xml)
assert_equal({ "starts_on" => Date.new(2020, 1, 1) }, hash["event"])
end- 修正前はこのテストが
Date::Errorで失敗 - 修正後は期待どおり
Dateオブジェクトに変換されて成功
activesupport/test/xml_mini_test.rbの既存テストを微修正:
非文字列入力に対する挙動の例外クラスを TypeError から Date::Error に変更しています。
理由: date.to_s.strip を通してから Date.strptime されるため、パース不能時の例外が Date::Error で統一されるようになったためです。
- 影響範囲・注意点
影響を受ける主なケース
Hash.from_xmlを利用し、XML 内でtype="date"を使っているアプリケーション- 特に、整形済み・インデント付き XML(人間が読みやすいように改行・スペースが入っているもの)を入力としている場合
期待されるポジティブな影響
- これまで
Date::Errorで落ちていた XML が正常にパースされるようになる integerやbooleanなど、他の型との挙動が「前後の空白を許容する」という点で一貫する
- これまで
互換性・注意点
date型は、渡された値に対して必ずto_s.stripを行った結果でDate.strptimeするように変わったため、以下のようなケースでは例外の種類やタイミングが変わる可能性があります。- 非文字列(例: オブジェクト、数値、nil など)を
type="date"に対して渡していた場合- 以前: 型エラー (
TypeError) を期待していたコードがあれば、Date::Errorに変わる
- 以前: 型エラー (
- ただし、通常の XML パース用途では非文字列が入ることはほぼないため、一般的な利用者への影響は限定的です。
- 非文字列(例: オブジェクト、数値、nil など)を
パフォーマンス
- 1 ノードあたり
to_s.stripが増えるだけのごく軽微な変更であり、現実的な XML サイズではパフォーマンス影響は無視できるレベルと考えられます。
- 1 ノードあたり
- 参考情報 (あれば)
- 類似実装:
boolean型のパースはすでにto_s.stripを行っており、本 PR はそれに合わせてdate型の扱いを揃えた変更です。
- コードの読みどころ:
ActiveSupport::XMLMini::PARSINGのマップ定義部分Hash.from_xml周辺の処理フロー(XMLMiniを通して型変換が行われる流れ)
#57671 Fix MemoryStore#cleanup raising NoMethodError with a non-DupCoder serializer
マージ日: 2026/6/12 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::Cache::MemoryStoreを非デフォルトのシリアライザ(serializer: :marshal_7_1など)付きで使った際に、cleanup実行時にNoMethodErrorが発生して書き込みが失敗する問題を修正した PR です。cleanup内でも他と同様にdeserialize_entryを通してエントリを扱うようにし、テストを追加しています。
- 変更内容の詳細
問題の内容
MemoryStore は内部的に @data にキャッシュを保持していますが、構成によって @data の中身が変わります。
デフォルト (
DupCoder) 利用時@data[key]には 生のActiveSupport::Cache::Entryオブジェクト が入る- そのため
entry = @data[key]; entry.expired?のような呼び出しがそのまま動く
非デフォルトのシリアライザ / コーダ利用時
例:serializer: :marshal_7_1serializer: :message_pack- カスタム
coder: ... - この場合
@data[key]には シリアライズ済みのStringが入る - よって
String#expired?は存在せず、cleanup中にNoMethodErrorが発生する
さらに厄介なのは、cleanup は単体で呼ばれるだけでなく、容量制限 (size オプション) を越えたときに write_entry -> prune -> cleanup の流れで自動的に呼ばれることです。そのため容量上限到達後の あらゆる write が例外で落ちる 状態になり、期限切れエントリも一切掃除されなくなっていました。
修正内容
対象箇所は activesupport/lib/active_support/cache/memory_store.rb の cleanup メソッドです。
従来:
@data.each_key do |key|
entry = @data[key]
delete_entry(key) if entry && entry.expired?
end修正後:
@data.each_key do |key|
entry = deserialize_entry(@data[key])
delete_entry(key) if entry && entry.expired?
endポイント:
read_entryでは元々deserialize_entryを通してEntryを復元しており、cleanupだけが「生の@data」に直接触っていました。- 今回
cleanupでもdeserialize_entryを通すことで、- デフォルトの
DupCoderでも - 非デフォルトのシリアライザ / コーダ利用時でも
一貫して「Entryオブジェクトに対してexpired?する」ようになりました。
- デフォルトの
deserialize_entryは@data[key]が存在しない場合にはnilを返すため、既存のif entry && entry.expired?のロジックはそのまま成立します。
追加されたテスト
activesupport/test/cache/stores/memory_store_test.rb にテストが追加されています:
def test_cleanup_with_non_dup_coder_serializer
store = ActiveSupport::Cache::MemoryStore.new(
serializer: :marshal_7_1,
size: 200,
)
store.write("a", "x" * 100, expires_in: 0.01)
store.write("b", "y" * 100)
sleep 0.05
# ここで cleanup が呼ばれても例外にならないこと
assert_nothing_raised do
store.cleanup
end
# 有効期限切れの "a" は削除されている
assert_nil store.read("a")
# 期限切れでない "b" は残っている
assert_equal "y" * 100, store.read("b")
end※ 実際のテストコードは若干異なる可能性がありますが、意図としては:
serializer: :marshal_7_1を使ったMemoryStoreを作る- すぐ期限切れになるエントリと、有効なエントリを書き込む
- 期限が切れるまで
sleepした後cleanupを呼ぶ - 例外が発生しないこと、および
- 期限切れエントリは削除され
- 有効なエントリは残る
ことを検証しています。
- 影響範囲・注意点
影響を受けるのは以下のようなケースです:
ActiveSupport::Cache::MemoryStoreを利用しておりserializer: :marshal_7_1、serializer: :message_pack、カスタムcoderなど DupCoder 以外のシリアライザ / コーダ を指定していて- かつ
sizeオプションでメモリ上限を設定し、その上限を越えるような書き込みを行うケース
この PR 適用前:
- 上限到達後に
writeがprune -> cleanupを呼び出すたびにNoMethodErrorが発生 - 以降の書き込みが実質的に失敗し続ける
- 有効期限切れエントリも一切掃除されない
- 上限到達後に
この PR 適用後:
- 非デフォルトシリアライザ使用時でも
cleanupが正常動作し、期限切れエントリが適切に削除される writeが例外で止まらなくなる
- 非デフォルトシリアライザ使用時でも
パフォーマンス面:
cleanup実行時にdeserialize_entryを通す分、シリアライズ/デシリアライズのコストがかかりますが、read_entryでも同様に行っている処理であり、cleanup自体も通常は頻繁には呼ばれないため、挙動としては妥当なトレードオフと考えられます。
互換性:
- 既存の API やオプションに変更はなく、挙動は「元々期待されていた動作」に近づくだけなので、後方互換性の観点でも問題は少ないと思われます。
- 参考情報 (あれば)
- 対象クラス:
ActiveSupport::Cache::MemoryStore- Rails ガイド(英語): “Active Support Instrumentation and Caching” のメモリストア節
- 類似実装:
read_entryがすでにdeserialize_entryを通している点から、今回の変更はその挙動に揃える形の修正となっています。
- 関連オプション:
serializer:(:marshal_7_1,:message_packなど)coder:(カスタムコーダ指定)size:(メモリストアの最大サイズ)
#57672 Fix Time#advance and DateTime#advance mutating the caller's options hash
マージ日: 2026/6/12 | 作成者: @55728
- 概要 (1-2文で)
Time#advanceとDateTime#advanceが、呼び出し元から渡された options ハッシュを破壊的に変更していた問題を修正した PR です。ハッシュを複製してから処理するようにすることで、呼び出し元のハッシュの汚染やFrozenErrorを解消し、Date#advanceとの挙動の一貫性も取っています。
- 変更内容の詳細
問題点
Time#advance / DateTime#advance は、以下のように渡された options ハッシュに対してキーを書き換えるロジックを持っています。
:weeksを:daysに畳み込む (例:weeks: 1→days: days + 7):daysを:hoursに畳み込む (例:days: 1→hours: hours + 24)- その過程で
hours: 0などのキー追加も行う
従来はこれを「受け取ったハッシュそのもの」に対して行っていたため、以下のような問題がありました。
require "active_support/all"
options = { weeks: 1, days: 2 }
Time.new(2024, 1, 1).advance(options)
p options
# 実際: { weeks: 1, days: 2, hours: 0 } # ← 呼び出し元のハッシュが書き換えられている
# 期待: { weeks: 1, days: 2 }さらに、options を freeze して渡すと、内部でのキー再代入により FrozenError が発生していました。
Time.new(2024, 1, 1).advance({ weeks: 1, days: 2 }.freeze)
# 実際: FrozenError: can't modify frozen Hash
# 期待: 正常に計算された Time を返す同様の問題は DateTime#advance にも存在していましたが、Date#advance は options を参照するだけで書き換えないため、3 つのメソッド間で挙動が不一致でした。
修正内容
Time#advance および DateTime#advance のメソッド先頭に、以下の 1 行を追加しています。
options = options.dupこれにより:
- 以降の
options[:days] = ...やoptions[:hours] ||= 0といった操作は、呼び出し元とは別のハッシュに対して行われる - 呼び出し元の options ハッシュには一切変更が加わらない
- たとえ呼び出し元が
options.freezeしていても、dupされた新しいハッシュはミュータブルなのでFrozenErrorは発生しない - 既存の計算ロジック (何週間分を何日分に換算など) はそのままのため、返る
Time/DateTimeの値は変わらない
実質的には、「破壊的に見える内部実装を呼び出し元から隔離した」形です。
テスト
次の 2 パターンについて、Time / DateTime それぞれで回帰テストが追加されています。
options ハッシュが呼び出し後も変化していないこと
rubyoptions = { weeks: 1, days: 2 } Time.new(2024, 1, 1).advance(options) assert_equal({ weeks: 1, days: 2 }, options)凍結された options ハッシュでも例外が出ないこと
rubyoptions = { weeks: 1, days: 2 }.freeze assert_nothing_raised do Time.new(2024, 1, 1).advance(options) end
time_ext_test.rb, date_time_ext_test.rb, time_with_zone_test.rb など既存テストもすべてグリーンで、他の挙動に影響が出ていないことが確認されています。
- 影響範囲・注意点
- 影響を受けるのは以下を利用しているコードです:
Time#advance(options_hash)DateTime#advance(options_hash)
Date#advanceの挙動はもともと非破壊的であり、この PR の影響はありません。
開発者視点でのポイント:
呼び出し元ハッシュの “副作用” に依存していたコードがあれば壊れる可能性
- 例えば、こんなコードは以前は動いていたかもしれません:ruby
opts = { days: 1 } now = Time.current now.advance(opts) # 以前: opts に hours: 24 が入ることを前提にしていた (悪い例) do_something_with(opts[:hours]) - このような「advance の副作用として options が書き換わること」を前提としたコードは、今回の変更後は動かなくなります。
- ただし、こうした依存は意図された使い方ではなく、バグに近い設計といえます。
- 例えば、こんなコードは以前は動いていたかもしれません:
options を複数回再利用・共有・freeze している場合は恩恵を受ける
- 典型的なユースケース:ruby
ADVANCE_OPTS = { weeks: 1, days: 2 }.freeze 10.times do Time.current.advance(ADVANCE_OPTS) # 以前は 1 回目で FrozenError end - 今後はこのような「定数オプションを使い回す」スタイルが問題なく動作します。
- 典型的なユースケース:
パフォーマンス面の影響は極小
Hash#dup一回分のコストが増えますが、advance自体が日時の計算処理を行うメソッドであり、通常のアプリケーションではボトルネックにはなりにくい変更です。- ただし、超高頻度・大量ループで
Time#advanceを叩くようなコードがある場合、プロファイル上で微小な差が出る可能性はあります。
TimeWithZone#advanceのような関連メソッドtime_with_zone_test.rbもグリーンであることから、ActiveSupport::TimeWithZone周りを含めた既存挙動への影響はないと判断されています。TimeWithZone#advanceは内部でTime#advanceなどを利用しているため、副作用が減った分、思わぬハッシュ汚染バグのリスクが下がります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57672
- 関連 API ドキュメント (概念確認用):
Time#advance/DateTime#advance/Date#advance(ActiveSupport 拡張)
- 「引数として受け取った Hash/Array を破壊的に変更しない」ことは Ruby/Rails では一般的な慣習であり、この PR はその慣習に沿った修正になっています。
#57686 Re-raise suppressed RedisClient errors in all RedisCacheStoreTests classes
マージ日: 2026/6/12 | 作成者: @yahonda
- 概要 (1-2文で)
Redis を使ったキャッシュストアのテストにおいて、これまで握りつぶされていたRedisClientのエラーをすべてのRedisCacheStoreTests系テストで再スローするようにし、原因が分かりにくい失敗を診断しやすくした PR です。前回 PR (#57571) で一部クラスにだけ入っていたerror_handlerの設定を、共通のデフォルトに移して全テストクラスに適用しています。
- 変更内容の詳細
背景となる問題
ActiveSupport::Cache::RedisCacheStoreTests::FailureRaisingFromMaxClientsReachedErrorTest#test_fetch_with_block_read_failure_raises が稀に失敗するが、そのときの失敗メッセージが
--- expected
+++ actual
@@ -1,3 +1 @@
-# encoding: US-ASCII
-# valid: true
-"ix70bwHXFsZAhUKh"
+nilのように「期待していた文字列 vs 実際は nil」しか出ず、なぜ nil になったのか(Redis 接続エラーなど)が分からない、という問題がありました。
原因としては、Redis 側でのタイムアウトや接続エラー (RedisClient::CannotConnectError など) が、テスト中に設定されている error_handler によって握りつぶされ、テストの失敗からは見えなくなっていたためです。
今回の修正内容
前回の PR (#57571) では、RedisCacheStoreCommonBehaviorTest に対して error_handler を「エラーを再スローする」ものに変えましたが、他の RedisCacheStoreTests クラスにはまだ適用されていませんでした。
この PR では、その error_handler 設定をテストストア共通の初期化処理(StoreTest#lookup_store のデフォルトオプション)に移動し、全ての RedisCacheStoreTests ベースのテストで同じ挙動になるようにしています。
コード上は activesupport/test/cache/stores/redis_cache_store_test.rb の中で
- 個別クラスに書かれていた
error_handlerの設定を削除(-5 行) - 共通の場所に 1 行だけ
error_handlerを設定する(+1 行)
という最小限の変更です。
これにより、同じようなタイムアウトが発生すると、今後は以下のように Redis の接続エラーがそのままテストのエラーメッセージに出るようになります:
Error:
ActiveSupport::Cache::RedisCacheStoreTests::FailureRaisingFromMaxClientsReachedErrorTest#test_fetch_with_block_read_failure_raises:
RedisClient::CannotConnectError: user specified timeout for redis:6379 (redis://redis:6379/1)
...
lib/active_support/cache.rb:678:in 'ActiveSupport::Cache::Store#write'
test/cache/behaviors/failure_raising_behavior.rb:18:in 'FailureRaisingBehavior#test_fetch_with_block_read_failure_raises'これで、「nil になった」ではなく「Redis への接続に失敗した」ことが明示的に分かるようになります。
- 影響範囲・注意点
- 対象は テストコードのみ であり、
ActiveSupport::Cache::RedisCacheStoreの本番挙動には影響しません。 - すべての
RedisCacheStoreTests系クラスでerror_handlerが「エラーを再スローする」よう統一されるため、これまで黙殺されていた Redis 接続エラー等がテスト失敗として表面化しやすくなります。- 結果として、テストスイートが不安定な環境(遅い CI や不安定な Redis サーバ)では、以前よりも Redis 接続関連のエラーが露出する可能性があります。
- ただしこれは「実際に起きているエラーが見えるようになる」という性質のものであり、根本的なバグを隠さないという意味で望ましい変更です。
- 変更ファイルは 1 つ・差分も小さく、
error_handlerの適用範囲を広げただけなので、テストの構造や API 互換性への影響はありません。
- 参考情報 (あれば)
- この PR は #57571 のフォローアップであり、
RedisCacheStoreCommonBehaviorTestだけに入っていた「エラー再スローerror_handler」を他の RedisCacheStore テストにも反映する目的です。 - 失敗ログ(参考):
https://buildkite.com/rails/rails-nightly/builds/4426#019eb87c-96a1-49cf-a581-6e8ca348864f/L8294
ここで原因が分からないnilとの比較になっていた問題を解消する意図があります。
#57679 Fix increment! with explicit query constraints
マージ日: 2026/6/12 | 作成者: @jsaubry
- 概要 (1-2文で)
increment!/decrement!がquery_constraintsを持つモデルに対しても、更新時のWHERE句にそれらの制約列を正しく含めるように修正された PR です。単一主キー + 明示的query_constraints環境で、主キーのみで UPDATE してしまう不具合を解消します。
- 変更内容の詳細
何が問題だったか
increment! / decrement! は内部的に update_counters を使ってカラムをインクリメント/デクリメントしていますが、その際、主キーだけを条件に UPDATE していました。
元コード(簡略化):
# ActiveRecord::Persistence#increment!
self.class.update_counters(id, attribute => change, touch: touch)update_counters 側:
# ActiveRecord::CounterCache.update_counters
unscoped.where!(primary_key => id).update_counters(counters)ここで where!(primary_key => id) としているため、主キー以外の query_constraints が無視されるという問題がありました。
- 複合主キーの場合:
idに複合キー情報が入っているため問題にならない - 問題になるケース: 単一カラム主キーのモデルで、
query_constraintsが主キーと一致しない場合
例:
class TopicWithConstraints < Topic
query_constraints :author_name, :id
end
topic.increment!(:replies_count)
# Before: UPDATE topics SET replies_count = replies_count + 1 WHERE id = ?
# After: UPDATE topics SET replies_count = replies_count + 1 WHERE author_name = ? AND id = ?このように、本来 author_name も WHERE 条件に含めるべきところが、id のみで更新されていました。
どう直したか
increment! が「id を渡して update_counters を呼ぶ」のをやめ、自分自身の query_constraints を用いて更新用の Relation を組み立てるように変更しています。
修正後のイメージ:
# ActiveRecord::Persistence#increment!
counters = { attribute => change }
relation = self.class.unscoped.where!(_query_constraints_hash)
relation.update_counters(counters)ポイント:
_query_constraints_hashは、そのレコードを一意に特定するためのハッシュを返します- 明示的な
query_constraintsがある場合: そこに列挙したカラムが含まれる - ない場合: 主キーにフォールバックする
- 明示的な
- これにより、
- 通常の主キー更新 → これまで通り主キーで絞り込む
- 複合主キー → 既存どおり問題なし(ID に複合キーが含まれている)
- 明示的
query_constraintsあり → すべての制約カラムを WHERE 句に含める
decrement! は内部実装として increment! に負の値を渡しているだけなので、同じ修正の恩恵を受けます。
テスト・ドキュメント
activerecord/test/cases/persistence_test.rbにテストが追加され、query_constraintsを持つモデルでincrement!が適切な WHERE 条件を生成することを確認activerecord/CHANGELOG.mdにこの挙動修正が追記
- 影響範囲・注意点
影響を受けるのは以下のようなケースです:
- Active Record モデルで
query_constraintsを明示的に設定している - そのモデルに対して
increment!/decrement!を使っている - これまで主キーだけで UPDATE されていたが、本来は追加の制約カラムも含めるべきだった
- Active Record モデルで
期待される挙動の変化:
- これまで:
increment!/decrement!が主キーだけで UPDATE を行っていたため、
例えば「論理シャーディング」「マルチテナント」「パーティションキー」的なカラムをquery_constraintsにしている環境では、誤ったレコードが更新されうる
- これから:
query_constraintsに列挙したカラムも WHERE 句に含めて UPDATE されるため、
レコードの特定がより安全かつ一貫したものになる
- これまで:
既存コードへの互換性:
- 「主キーだけで十分に一意に特定できる」という前提で
query_constraintsを増やしていなかったケースは、挙動に変更なし - もし「意図的に主キーだけで更新したいが、
query_constraintsには別用途のカラムを入れている」といった特殊な設計をしていた場合は、挙動が変わります
(ただしそのような設計はquery_constraintsの用途から外れていると言えます)
- 「主キーだけで十分に一意に特定できる」という前提で
パフォーマンス面:
- WHERE 句に制約カラムが増えるため、インデックス設計によってはクエリプランが変わる可能性があります
- とはいえ
query_constraintsはもともとレコード特定のために使う前提なので、通常はインデックスも張られているはずで、実運用上は改善または許容範囲の変化となることが多いと考えられます
- 参考情報 (あれば)
対象メソッド:
ActiveRecord::Persistence#increment!ActiveRecord::Persistence#decrement!(内部的にはincrement!利用)
関連箇所:
ActiveRecord::Persistence#_query_constraints_hashActiveRecord::CounterCache.update_counters
用例イメージ(マルチテナントなど):
rubyclass AccountRecord < ApplicationRecord query_constraints :tenant_id, :id end record.increment!(:usage_count) # 修正前: WHERE id = ? # 修正後: WHERE tenant_id = ? AND id = ?
この PR により、「query_constraints を設定しているなら、increment! / decrement! もそれを尊重する」という、一貫した挙動になります。
#57682 Ractor safe readonly attributes
マージ日: 2026/6/12 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails を Ractor セーフにする取り組みの一環として、ActiveRecord::Base._attr_readonlyで使われる配列を「最初から凍結(frozen)された不変オブジェクト」として扱うようにし、テスト用の Ractor 関連アサーションも整理・拡張した PR です。これにより、クラスレベルの設定情報が Ractor 間で安全に共有しやすくなります。
- 変更内容の詳細
2-1. ActiveRecord::Base._attr_readonly の Ractor セーフ化
対象ファイル:
activerecord/lib/active_record/readonly_attributes.rb
_attr_readonly は「読み取り専用属性」を保持する内部用メソッドで、クラスレベルに配列として保存されています。この PR では、その配列を以下の方針で扱うようにしています:
- デフォルトの配列を最初から
freezeしておく - 要素を追加するたびに「元の配列を変更せず」
dupしてから変更し、変更後の配列を再度freezeする
疑似コードで書くと、以下のようなパターンになります:
# もとのイメージ(破壊的に変更しがち)
@_attr_readonly ||= []
@_attr_readonly << :title # ← ミューテーションが発生し、Ractor セーフでない
# この PR でのパターン(イミュータブル配列として扱う)
@_attr_readonly ||= [].freeze
@_attr_readonly = (@_attr_readonly + [:title]).freeze
# または dup して push して freeze など、いずれにせよ:
# - 元の配列は変更しない
# - 新しい配列を凍結して入れ直すこれにより、クラス属性として保持される _attr_readonly が、常に「凍結された配列」として振る舞い、Ractor 間でそのまま shareable なオブジェクトとして扱いやすくなります。
※PR 説明文では、例外ラッパー設定(exception wrapper configs)で既に導入済みの手法(#57483)と同様のパターンを採用していると述べられています。
2-2. Ractor 用アサーションの整理・強化
対象ファイル:
activesupport/lib/active_support/testing/ractors_assertions.rbactivesupport/test/backtrace_cleaner_test.rbrailties/test/backtrace_cleaner_test.rbactiverecord/test/cases/base_test.rb
Ractor セーフティをテストするためのアサーションを 2 種類に整理しています:
assert_ractor_shareable- 「何もしなくても Ractor 共有可能であるべき」オブジェクトに対して使う
- 例:
ActiveRecord::Base._attr_readonlyのように、常に frozen な配列にしておきたいクラス属性 - テストでは「アプリケーションを凍結する処理などを通さずに、そのまま
Ractor.shareable?が true になること」を確認する役割
assert_ractor_make_shareable- 「アプリケーションの初期化/凍結のタイミングで
Ractor.make_shareableによって共有可能にする想定」のオブジェクトに対して使う - 例: backtrace cleaner のような、最終的には凍結して shareable にするが、初期状態ではミュータブルなことがあるオブジェクト
- 「アプリケーションの初期化/凍結のタイミングで
今回の PR では:
- 以前の PR (#57574) で入っていたアサーションを見直し、
_attr_readonlyのような「最初から shareable であるべきもの」にassert_ractor_shareableを使うように修正- backtrace cleaner 関連のテストには
assert_ractor_make_shareableを使うように修正
- ActiveRecord のテスト (
activerecord/test/cases/base_test.rb) に_attr_readonlyが Ractor shareable であることを検証するテストを追加
することで、「どのオブジェクトがいつ shareable であるべきか」をテストレベルで明確に区別できるようにしています。
- 影響範囲・注意点
既存アプリの挙動への互換性
_attr_readonly自体は「属性名の一覧」を持つだけの配列であり、通常の利用ではアプリ側から直接ミューテート(<<など)するケースは少ない前提です。- ただし、もしアプリやプラグインが
ActiveRecord::Base._attr_readonly << :fooのように「内部配列への直接破壊的変更」に依存している場合は、FrozenErrorが発生する可能性があります。 - 推奨パターンとしては、
attr_readonlyDSL やクラスメソッドを通じて設定する形に留めるべきで、内部配列を直接いじらない方が安全です。
Ractor を使わないアプリへの影響
- Ractor を使わない場合も、配列がイミュータブルになる点以外の挙動差はほぼありません。
- 読み取り専用属性の解決やクエリ挙動に変更はなく、パフォーマンスへの影響もごく小さい(配列の dup + freeze)範囲に限定されます。
今後のパターンの標準化
- クラスレベルに設定情報(配列・ハッシュ)を持つ場合、「デフォルトを frozen にする」「更新時は dup してから freeze し直す」というパターンが今後の Rails コアの標準的な実装スタイルになっていくことを示唆しています。
- エンジンや gem 側で Ractor 対応を進める際にも、このパターンを参考にするとよいです。
- 参考情報 (あれば)
この PR で参照されている関連 PR:
- 例外ラッパー設定を Ractor セーフにした PR:
https://github.com/rails/rails/pull/57483 - Ractor 用アサーション導入・改善の前段となる PR:
https://github.com/rails/rails/pull/57574
- 例外ラッパー設定を Ractor セーフにした PR:
Ractor 自体の仕様:
- Ruby 公式ドキュメント (英語):
https://docs.ruby-lang.org/en/master/Ractor.html - ポイントとして、
Ractor.shareable?が true になるのは、主に「完全に凍結されたオブジェクトグラフ」であることが条件となるため、本 PR のように「クラス設定をミュータブルからイミュータブルへ寄せていく」変更が必要になります。
- Ruby 公式ドキュメント (英語):
#57674 Fix String#truncate with :separator and an over-long :omission
マージ日: 2026/6/12 | 作成者: @55728
- 概要 (1-2文で)
String#truncateに:separatorオプションを指定しつつ、:omissionの長さがtruncate_to以上になるケースで、想定より長い文字列が返ってしまう不具合を修正した PR です。separatorを使う場合も、非separatorパスと同様に「省略記号だけ」を返すよう挙動を揃えています。
- 変更内容の詳細
問題の挙動
対象メソッドは ActiveSupport の String#truncate です。以下のようなコードで問題が発生していました。
require "active_support/core_ext/string/filters"
"Hello World foo bar baz".truncate(2, separator: " ")
# 実際: "Hello World foo bar..."
# 期待: "..."
"Hello World foo bar baz".truncate(2)
# => "..." # separator なしだと正しい内部では概ね以下のような処理になっています(簡略化):
length_with_room_for_omission = truncate_to - omission.length
# 例: truncate_to=2, omission="..."(length=3) の場合
# length_with_room_for_omission = 2 - 3 = -1- 通常(separator なし)の分岐では、この
length_with_room_for_omissionが負の場合は、元文字列の前半を一切付けず、omissionだけを返すようになっていました。 - 一方で
separator指定ありの分岐では、この負の値をそのままrindexの開始位置に渡していました。
# 問題のあったイメージ
stop = rindex(separator, length_with_room_for_omission) || length_with_room_for_omission
self[0, stop] + omissionRuby の String#rindex は、第2引数が負のとき「末尾からのオフセット」として解釈します。
そのため:
length_with_room_for_omissionが負でもrindexが末尾近くでマッチを見つけてしまうstopが末尾付近の大きな値になり、self[0, stop]で「ほぼ全文」が取れてしまう- 結果として
"Hello World foo bar..."のように、想定より長い文字列が返る
というバグになっていました。
修正内容
修正は「どのときに separator を使うか」の条件を 1 行だけ変更するものです。
- 具体的には、
separatorを使う分岐に入る条件にlength_with_room_for_omission >= 0を追加 - これにより、「省略記号を付ける前に入れられる文字数が負」の場合は separator 分岐に入らないようにした
結果として:
length_with_room_for_omission >= 0
→ 元文字列の一部 +omissionを返す(従来どおり separator で区切り位置を探す)length_with_room_for_omission < 0
→self[0, length_with_room_for_omission]は""になり、"" + omissionとして「省略記号のみ」を返す
→ 非 separator パスと同じ挙動に揃う
テストの追加
次のようなテストが追加されています。
assert_equal "[...]", "Hello Big World!".truncate(
2, omission: "[...]", separator: " "
)
assert_equal "[...]", "Hello Big World!".truncate(
2, omission: "[...]", separator: /\s/
)- 修正前:
"Hello Big[...]"となりテスト失敗 - 修正後:
"[...]"となりテスト成功
これにより、「小さい truncate_to + 長すぎる :omission + separator(文字列/正規表現)でも、 omission のみが返る」ことが担保されました。
- 影響範囲・注意点
- 対象: ActiveSupport の
String#truncateをseparator:オプション付きで呼び出しているコード - 影響するケース:
truncate_to(第一引数 orlength:オプション)より:omissionの長さが長い、あるいは同じ長さ- かつ
separator:を指定している場合
- これまでは「separator ありの場合だけ、ほぼ全文 + omission」が返っていたケースが、「omission だけ」に変わります。
- 非
separatorパスの挙動に揃える変更なので、「truncateの設計として一貫性のある挙動になった」と言えますが、もし既存コードが「実際のバグ挙動」に依存していた場合は、表示内容が短くなる可能性があります。
運用上の注意:
- UI やログなどで
truncateの出力を snapshot テストや文字列比較でテストしている場合、separator付きで上記条件を満たすケースがあればテストが変わる可能性があります。 truncate_toと:omissionの長さの関係を意識していない場合でも、omissionを長くカスタマイズしていると今回の修正パスに入る可能性があります。
- 参考情報 (あれば)
- 対象メソッド:
ActiveSupport::CoreExtensions::String::Filters#truncate - 変更ファイル:
activesupport/lib/active_support/core_ext/string/filters.rb- separator 分岐条件の 1 行修正
activesupport/test/core_ext/string_ext_test.rbseparator:+ 長い:omission向けのテスト 2 ケース追加
#57681 Make some lambdas in constants shareable
マージ日: 2026/6/11 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails の各所で定数として保持されている lambda を、Ruby 4.0+ の Ractor 上で安全に共有できるようにするため、「shareability shim」を使って shareable にした PRです。将来的な Ractor 対応・並列実行対応を見据えた小さな互換性対応で、挙動そのものは基本的に変わりません。
- 変更内容の詳細
背景・目的
- 「リクエスト処理を Ractor 内で行う」実験の一環として、
- 定数として定義された lambda / Proc が Ractor 間で共有可能 (shareable) である必要がある
- Ruby 3.2 以降〜4.0+ では、オブジェクトが Ractor 間でやり取りできるかどうかが厳しくチェックされるため、
- 一部の lambda をそのまま定数に置くと「共有不可能なオブジェクト」としてエラーになる可能性がある
- そこで Rails 側で用意している「shareability shim」を使い、対象の lambda を Ractor でも共有可能な形にしている。
実際の変更箇所 (概念的説明)
変更ファイルはすべて Action Pack 周辺で、以下のようなところにある「定数 lambda」の定義方法が変わっています。
actionpack/lib/action_controller/metal.rbactionpack/lib/action_dispatch/http/parameters.rbactionpack/lib/action_dispatch/journey/visitors.rbactionpack/lib/action_dispatch/routing/mapper.rbactionpack/lib/action_dispatch/routing/route_set.rb
中身のロジックは変えずに、「lambda をそのまま定数に入れる」のではなく「shareability shim 経由で wrap して定数に入れる」形に変更しています。
疑似コードでいうと、例えば:
# 変更前 (イメージ)
SOME_LAMBDA = ->(env) { ... }
# 変更後 (イメージ)
SOME_LAMBDA = ShareableLambda.wrap(->(env) { ... })のように、定数として保持している lambda/Proc をラッパー経由で登録することで、Ruby 4.0+ かつ Ractor 環境でも Ractor.shareable? に通るようにしていると考えられます。
Rails コアにはすでに「shareability shim」が存在しており、Ruby のバージョン差異を吸収しながら Ractor.make_shareable 等を使う/使わないを切り替える役割を持っています。この PR は、その shim を Action Pack 内のいくつかの lambda に適用しただけで、処理の本質的な挙動は変えていません。
- 影響範囲・注意点
影響範囲
- 主な対象は以下のレイヤーの内部実装:
- コントローラ基盤 (
ActionController::Metal) - パラメータ処理 (
ActionDispatch::Http::Parameters) - ルーティング・Journey の visitor 処理 (
ActionDispatch::Journey::Visitors) - ルーティング定義と RouteSet (
ActionDispatch::Routing::Mapper,RouteSet)
- コントローラ基盤 (
- これらは Rails のリクエスト処理~ルーティング~パラメータ解析の中核部分ですが、
- 変更は「lambda の宣言方法」(shareable 化) に限定されており、
- 呼び出し方・返り値・公開 API のシグネチャは変わっていないと考えてよいです。
アプリケーション開発者への実務的な影響
- 通常の (シングルプロセス・マルチスレッド) Rails アプリでは、挙動の変化はほぼありません。
- 将来的に:
- Rails が「Ractor を使ったリクエスト処理」を正式にサポートする場合、
- ルーティング / コントローラ / パラメータ処理が「Ractor 間で安全に共有」できる前提が整いつつある、という位置づけになります。
- 独自に Rails 内部の定数 lambda を書き換えたり monkey patch している場合:
- 似たように shareable でないオブジェクトを閉じ込めると、Ractor 環境では問題が出る可能性があります。
- Ractor 対応を視野に入れるなら、自前の lambda / Proc も「外側から状態を閉じ込めない」「shareability shim を使う」といった方針を取る必要が出てきます。
注意点
- この PR ではテスト追加はされておらず、既存テストでの挙動維持確認にとどまっているようです。
- 実際に Ractor でリクエストを捌く構成は、まだ「実験フェーズ」であり、本 PR だけで完全対応になるわけではありません。
- Ruby 4.0+ を前提としたコードパスでのみ意味を持つ変更であり、古い Ruby では shim が安全にフォールバックする想定です。
- 参考情報 (あれば)
- Ruby Ractor (並列実行モデル) の概要:
- Ractor 間でやりとりできるのは「shareable なオブジェクト」に限られる
Ractor.make_shareable(obj)によって「凍結」などを行い、共有可能な形に変換できる
- Rails 側の「shareability shim」は、Ruby のバージョン差異を隠蔽するためのヘルパーで、
- Ruby 3 系では no-op または限定的な処理
- Ruby 4.0+ では実際に
Ractor.make_shareable等を呼ぶ
という形で実装されていることが多いです。
- 今後、Ractor 対応を意識した gem やライブラリを書く場合も、
- グローバル定数に保持するオブジェクト (特に Proc / lambda / mutable な構造体) の shareability には注意が必要になります。
#57626 Introduce a new mechanism for applications to prepare for ractor safety
マージ日: 2026/6/11 | 作成者: @Edouard-chin
- 概要 (1-2文で)
このPRは、Railsアプリケーションが将来的に Ractor セーフになるための準備として、「RailsのAPIに渡される proc が Ractor 的に安全かどうか」を検査・制御する新しい仕組みを導入したものです。まずはActionDispatch::Routing::RouteSet(ルーティング)に適用されており、今後ほかの部分にも広げていく前提の基盤的変更です。
- 変更内容の詳細
背景: なぜ必要か
Ruby 3 以降の Ractor は、並列実行のための仕組みですが、Ractor 間で共有できるオブジェクトは Ractor.make_shareable で「共有可能」だと判定されたものに限られます。
Rails では、routes や各種設定にブロック(proc)を渡す場面が多く、そこに「共有不能なオブジェクト」を閉じ込めていると、Ractor 対応の際に Ractor::IsolationError が発生します。
PR の説明にある例:
Rails.application.routes.draw do
to_resolve = [:basket, anchor: "items"]
resolve("Cart") { to_resolve }
endこの場合、resolve に渡されるブロックが to_resolve というローカル変数(配列 + ハッシュ付き)をクロージャとしてキャプチャし、それが shareable ではないため Ractor.make_shareable で Ractor::IsolationError が起きうる、という問題があります。
新しい仕組み: ActiveSupport::Ractors のモード
このPRでは、Rails がアプリケーションの proc を shareable にしようと試みるかどうか、その失敗をどう扱うかを制御するモードを導入しています。
実装は activesupport/lib/active_support/ractors.rb に追加されており、おおまかに次の3モードがあります:
:raise- Rails が proc を
Ractor.make_shareableしようとする。 - 失敗した場合、そのまま
Ractor::IsolationErrorを「大きなエラー」として投げる。 - 意図: 問題のあるコードを早期に見つけて修正したい開発者向け(テストやCIで有効にする想定)。
- Rails が proc を
:warn- 同様に
Ractor.make_shareableを試みるが、 - 失敗 (
Ractor::IsolationError) したら rescue して 警告ログを出すだけ にする。 - 実行は継続するが、将来のRactor化に向けて「どこが危険か」を洗い出せる。
- 同様に
nil- Rails はそもそも
Ractor.make_shareableを試みない。 - 現状とほぼ同じ挙動で、Ractor セーフティのチェックを行わないモード。
- Rails はそもそも
このモードの設定インターフェースは、ActiveSupport::Ractors に隠蔽されている形で提供され、Rails 内部のさまざまなAPIから「今どのモードなのか」を参照しつつ処理が分岐する設計になっています。
適用箇所: RouteSet
このPRでは、まず ActionDispatch::Routing::RouteSet の一部コードパスでこの仕組みを利用するようになりました。
actionpack/lib/action_dispatch/routing/route_set.rbに 1行変更(実際には既存処理に対し、新しいRactorモードに応じて proc を shareable にする処理が追加)- 関連するテストが
actionpack/test/dispatch/routing/route_set_test.rbに 39行追加され、- Ractor モードごと(
:raise,:warn,nil)の挙動(エラーを投げる / warn を出す / 何もしない)を検証していると考えられます。
- Ractor モードごと(
ActiveSupport 側の実装
activesupport/lib/active_support/ractors.rb には主に以下が含まれていると考えられます(擬似イメージ):
module ActiveSupport
module Ractors
mattr_accessor :mode, default: nil
# mode: :raise, :warn, nil
def self.make_shareable(value)
case mode
when :raise
Ractor.make_shareable(value) # 失敗したらそのまま例外
when :warn
begin
Ractor.make_shareable(value)
rescue Ractor::IsolationError => e
Rails.logger.warn("Ractor unsafe proc detected: #{e.message}")
value # 処理続行
end
else # nil
value # 何もしない
end
end
end
end※上はPR説明とファイル構成からの推測コード例です。実際のメソッド名・インターフェースは若干異なる可能性がありますが、概ねこのような構造です。
これに対して activesupport/test/ractors_test.rb で:
ActiveSupport::Ractors.mode切り替え時の挙動Ractor::IsolationErrorの発生・rescue・warning 出力など
がテストされています。
- 影響範囲・注意点
現時点での実行時影響
- デフォルトモードが
nilであれば、既存アプリケーションの挙動は基本的に変わりません。 - ただし、Rails の内部で「今後 Ractor 対応を進める際のフック」となる処理が埋め込まれたため、将来的に設定を切り替えると影響が出ます。
開発者目線での使いどころ
- アプリを Ractor セーフにしたい、あるいはその準備状況を把握したい場合:
- ローカル or CI で
ActiveSupport::Ractors.mode = :raiseとしてテストを実行すると、- shareable でない proc を使っている箇所でテストが落ちるようになります。
- いきなりテストを落としたくない場合は
:warnでログ収集し、- 警告の出ている箇所を少しずつ修正していくという運用ができます。
- ローカル or CI で
Ractor セーフティ上の注意点
- proc の中でキャプチャしているオブジェクトが shareable かどうかが重要です。
代表的な問題パターン:- ルート定義や設定ブロックの中で、可変なオブジェクト(配列・ハッシュ・クラスインスタンスなど)をローカル変数に格納してクロージャで掴む。
- そのオブジェクトが Ractor 的に shareable にできない状態(ミューテックスや IO オブジェクトなどを含む)になっている。
- 今後、RouteSet 以外の Rails API についても、この仕組みを使って proc を shareable にしようとするようになる予定であり、その際に類似の問題が表面化する可能性があります。
バージョン互換性
- Ractor 自体は Ruby 3 以降の機能なので、古い Ruby バージョンでは(条件分岐により)無効 or 影響が限定的になるはずですが、
- 実際には Rails がどの Ruby 範囲をサポートしているかに依存します。
- このPR自体は、既存の挙動を壊さないよう opt-in な形で導入されているため、Rails をアップグレードしたタイミングで直ちに既存アプリが壊れるような変更ではありません。
- 参考情報 (あれば)
- PR本体:
- rails/rails #57626: Introduce a new mechanism for applications to prepare for ractor safety
- 関連クラス・ファイル:
activesupport/lib/active_support/ractors.rbactionpack/lib/action_dispatch/routing/route_set.rb
- Ruby Ractor 仕様:
Ractor.make_shareable(obj)… オブジェクトを Ractor 間で共有可能にする API。- 共有不能なオブジェクトを渡すと
Ractor::IsolationErrorが発生。
- 実務的な使い方の方向性:
- 将来の Ractor 対応を見据えて、まずは
:warnモードで運用し、「どの proc が Ractor unsafe か」をログベースで洗い出す。 - 問題のある箇所を修正できたら、CI で
:raiseに切り替えて「Ractor unsafe な proc を含む変更を検出する」仕組みとして利用する、という段階的導入が想定されます。
- 将来の Ractor 対応を見据えて、まずは
#57673 Reject attribute names that shadow CurrentAttributes methods
マージ日: 2026/6/11 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::CurrentAttributesで、クラス内部のメソッド名と同じ名前の属性を定義できてしまう問題を修正し、そのような属性名を宣言した場合はArgumentErrorを投げて拒否するようにしました。これによりresetが効かなくなるなどの「リクエスト間で状態が漏れる」不具合を防ぎます。
- 変更内容の詳細
問題の内容
ActiveSupport::CurrentAttributes はリクエスト単位の状態を @attributes という内部ハッシュに持ち、attributes アクセサや reset などのメソッドで管理しています。
ところが、たとえば次のようにクラスを定義すると:
class C < ActiveSupport::CurrentAttributes
attribute :attributes, :foo
endattribute :attributes によって attributes という ユーザー定義アクセスメソッド が生成され、内部実装の attributes メソッドを上書きしてしまいます。その結果 reset が内部状態を正しくクリアできず、値がリクエスト間で残り続ける状態になります。
PRの説明の例:
require "active_support"
require "active_support/current_attributes"
C = Class.new(ActiveSupport::CurrentAttributes) do
def self.name; "C"; end
attribute :attributes, :foo
end
C.foo = 99
C.reset
p C.foo- 実際の挙動:
99が出力される(resetしても消えない) - 期待される挙動:
attribute :attributesを宣言した時点でArgumentErrorが発生する
同様に、以下のような危険なケースも存在していました。
attribute :defaults
→ 定義時点でNoMethodErrorでクラッシュattribute :attribute
→ 以後のattribute :xxx呼び出しが壊れる(API自体を壊す)
もともと INVALID_ATTRIBUTE_NAMES という「禁止属性名」の配列が手書きで管理されていましたが、
- 新たに追加された/変化したメソッド (
:attributes,:defaults,:attributeなど) がリストに入っていない - すでに削除されたメソッド (
:reset_all) が残り続けている
という「現状と乖離した状態」になっていました。
修正内容
1. 手書きの禁止リストを廃止し、動的に算出するよう変更
ActiveSupport::CurrentAttributes クラスの末尾で、クラス自身が定義しているメソッド名をすべて列挙し、それを禁止属性名として定数に保持するように変更しています。
具体的には、以下4種類のメソッドを対象にしています。
methods(false) +
singleton_class.private_instance_methods(false) +
instance_methods(false) +
private_instance_methods(false)methods(false)
→ クラスメソッド(public/protected)singleton_class.private_instance_methods(false)
→ クラスメソッドのうち private なものinstance_methods(false)
→ インスタンスメソッド(public/protected)private_instance_methods(false)
→ インスタンスメソッドのうち private なもの
いずれも (false) を渡して 継承元のメソッドは除外 しているのがポイントです。
ObjectやKernelが持っている一般的なメソッド(to_s,inspect, など)は禁止しない
→ ユーザー側でそれらと同名の attribute を定義したいケースを潰さないようにする- 代わりに、CurrentAttributes 自身が定義しているメソッドだけ を禁止対象にする
→ 内部APIとの衝突によるバグだけを確実に防ぐ
この動的な算出により、
- 今後
CurrentAttributesにメソッドを追加しても、自動的に禁止リストに反映される - 削除されたメソッドが禁止リストに残り続ける、というドリフトが発生しない
というメリットがあります。
2. 禁止された属性名が指定された場合に ArgumentError を投げる
クラス定義内の attribute 呼び出しで、上記で算出した禁止リストに含まれる名前が1つでも含まれていれば、ArgumentError を発生させます。
エラーメッセージは、複数指定された場合も含めてわかりやすい形です。例:
class C < ActiveSupport::CurrentAttributes
attribute :attributes, :foo
end
# => ArgumentError: Restricted attribute names: attributes
class C < ActiveSupport::CurrentAttributes
attribute :attribute, :foo, :defaults
end
# => ArgumentError: Restricted attribute names: attribute, defaults3. テストの追加
activesupport/test/current_attributes_test.rb に以下の回帰テストが追加されています。
attribute :attributes, :fooがArgumentError/Restricted attribute names: attributes/を投げることattribute :attribute, :foo, :defaultsがArgumentError/Restricted attribute names: attribute, defaults/を投げること
既存の「restricted attribute names」を検証するテストも継続して通っており、元から禁止されていた :set, :reset なども引き続き禁止されています。
- 影響範囲・注意点
破壊的変更 (Breaking Change) の可能性あり
既にActiveSupport::CurrentAttributesを継承したクラスで、以下のような属性名を定義していた場合、今回の変更後はクラス定義時にArgumentErrorが発生します。:attributes:attribute:defaults- その他
CurrentAttributesが定義しているクラスメソッド/インスタンスメソッドと同名のシンボル
ただし、これらの属性を使っていたアプリケーションは、もともと「
resetが効かない」「クラッシュする」「attributeDSL が壊れる」など、かなり危険な挙動になっていたはずなので、早期に例外で気付けるようになったとも言えます。Object/Kernelなど継承元のメソッド名とは衝突しない限り、普通の属性名に影響はありません。今回禁止されるのは、あくまでCurrentAttributes本体が定義しているメソッドと同名の属性だけです。新バージョンにアップデートした際に、アプリケーション内の CurrentAttributes サブクラス定義がロードされたタイミングで
ArgumentErrorが発生する可能性があるため、デプロイ前に CI でクラス定義の読み込みまで行うテストを回しておくと安全です。
- 参考情報 (あれば)
ActiveSupport::CurrentAttributesの仕組み- 各スレッド/リクエストごとに
@attributesというハッシュを持ち、attribute :fooでfoo,foo=,foo?などのアクセサを自動定義する仕組みです。 resetはこの@attributesを初期化することで「リクエスト間での状態の隔離」を保証しており、今回の PR はその根幹の安全性を高める変更です。
- 各スレッド/リクエストごとに
関連する過去の変更
reset_allメソッドはすでに475877fc5eで削除済みだったにもかかわらず、旧INVALID_ATTRIBUTE_NAMESには残っていたことから、手動管理のリストが現実と乖離しやすいことが分かります。今回の動的算出への移行は、この問題を根本的に解消する狙いがあります。
#57638 Refactor Composite Primary Key implementation by using polymorphism
マージ日: 2026/6/11 | 作成者: @paracycle
- 概要 (1-2文で)
このPRは、Railsの複合主キー(Composite Primary Key, CPK)対応で散在していた「if 複合主キーなら…」という条件分岐ロジックを整理し、新たに導入したActiveRecord::Keyクラス階層(ポリモーフィズム)に集約するリファクタリングです。機能追加というよりは、CPKサポートの内部実装を大幅にクリーンアップし、今後の保守性と拡張性を高める変更です。
- 変更内容の詳細
全体像
- これまで:
- 各所で
if primary_key.is_a?(Array)など、CPKを意識した分岐が多数存在 - 単一主キーと複合主キーの処理がベタな条件分岐で混在し、脆くなっていた
- 各所で
- このPR後:
ActiveRecord::Key抽象クラスと、そのサブクラス(単一キー用・複合キー用など)を導入- 「キーに関する操作」は
Keyオブジェクトに委譲し、呼び出し側はポリモーフィックに扱う - 関連、リレーション、トランザクション等に散らばっていた CPK 特有の条件分岐を削減
新規クラス: ActiveRecord::Key
activerecord/lib/active_record/key.rb に約 170 行の新ファイルが追加され、主キーの操作を統一的に扱うためのクラス階層が導入されています。
典型的には、次のような責務を持つと考えられます(コードから推測される役割):
- 単一主キーか複合主キーかを意識しないインターフェイス:
- 値の取り出し・設定
- WHERE 句や JOIN 条件に使うための「キー条件」の生成
- 関連レコードと元レコードのキー対応の作成 など
- 代表的なメソッドのイメージ:
#extract(record): レコードから主キー値を取り出す(単一なら1値、複合なら配列/ハッシュなど)#match?(record, attributes): 与えられた属性がこのレコードのキーにマッチするか#predicate_for(table): Arel用のキー比較条件を生成#present?/#blank?等、キーの存在チェック
呼び出し側は「キーの中身(単一 or 複合)」を知らずに key オブジェクトに処理を任せることができるようになります。
各コンポーネントでの具体的なリファクタリング
※ファイル名ごとに「何が変わったか」を要点だけ列挙します。実装のほとんどは ActiveRecord::Key に吸収されていると考えてください。
1. ActiveRecord::AttributeMethods::PrimaryKey (+10/-9)
primary_key/id周りで「配列なら…」といった CPK 判定を行っていた箇所をKey経由の処理に置き換え。id_in_databaseなど「DB上のキー値」を扱う処理が、単一/複合を意識せずに扱えるよう整理。
2. 関連(Associations)
activerecord/lib/active_record/associations/builder/belongs_to.rb(+2/-5)activerecord/lib/active_record/associations/collection_association.rb(+7/-19)activerecord/lib/active_record/reflection.rb(+24/-42)
これらでは主に次のような変更が入っています:
belongs_toやhas_many等の関連付けが、関連先の主キーが複合かどうかを直接意識せずに、reflection.keyもしくは類似の API を通してキー操作を行うように変更。- 外部キー生成や「関連をロードするためのクエリ」生成時の分岐ロジックを削減し、
ActiveRecord::Keyが返す条件(predicate)や値セットを利用するように。
結果として:
# 以前(イメージ)
if reflection.primary_key.is_a?(Array)
# CPK用の条件組み立て
else
# 単一主キー用
end
# 以後(イメージ)
reflection.key.build_predicate(relation)のように呼び出し側のコードがスリムになっています。
3. リレーション / Finder
activerecord/lib/active_record/relation.rb(+3/-17)activerecord/lib/active_record/relation/finder_methods.rb(+4/-22)activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb(+4/-7)activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb(+1/-6)
これらは主に以下の点が整理されています:
find,exists?,whereなどで主キーを使うクエリ生成時に、「引数が配列で来たら CPK」などのベタな判定をせず、Key経由でクエリ条件を構築。PredicateBuilder周りのコードも、主キー構造を直接扱う代わりにKeyから提供される情報を使うよう改善。- ポリモーフィック関連のクエリ (
polymorphic_array_valueなど) においても、キー構造周りの条件分岐が削減。
4. 自動保存 / ネスト属性 / カウンタキャッシュ / トランザクション
activerecord/lib/active_record/autosave_association.rb(+1/-5)activerecord/lib/active_record/nested_attributes.rb(+5/-9)activerecord/lib/active_record/counter_cache.rb(+5/-12)activerecord/lib/active_record/transactions.rb(+5/-9)activerecord/lib/active_record/core.rb(+1/-5)
これらはいずれも:
- 「親子レコードの関連付け」「counter_cache のインクリメント対象判定」「トランザクション内でのレコード識別」など、主キーに依存する処理から CPK 特有の条件分岐を排除。
- レコード識別やキー比較、WHERE 条件生成を
Key経由に統一することで、コードが読みやすく・テストしやすくなるよう整理。
テスト
activerecord/test/cases/key_test.rb(+112/-0)
ActiveRecord::Key およびそのサブクラスの挙動を検証する新しいテストが追加されています。
想定されるテスト内容の例:
- 単一主キー・複合主キーで
#extract,#predicate_for,#match?などの挙動が正しいか - nil / blank / 型変換など、エッジケースで一貫した振る舞いをするか
- 関連・リレーション処理と組み合わせたときに期待通りのSQL条件が生成されるか
- 影響範囲・注意点
対利用者(アプリ開発者)視点
- 目に見える API の変更よりも内部実装の整理が中心のPRです。
- 公式にサポートされた CPK 機能を利用している場合:
- 動作仕様は基本的に変えず、安定性と一貫性を高めることを目的としたリファクタリングです。
- とはいえキー周りの広範な内部変更なので、CPK を使うモデルの関連・ネスト属性・counter_cache・finder については、アップグレード時にテストを厚めに回すことを推奨します。
内部API / メタプログラミングを多用している場合
- 以下のようなコードを書いている場合は注意が必要です:
primary_keyが配列かどうかを直接見て分岐するActiveRecord::Reflection/Associationの内部で primary key / foreign key の具体的な構造に依存したロジックを記述- ActiveRecord の内部クラスを monkey patch して CPK 対応を上書きしている
今回の変更により:
- 「CPK かどうか」を直接判定するのではなく、「Key オブジェクトを通して何がしたいか」を考えた方が、今後のバージョンでも壊れにくくなります。
- 内部構造に依存した独自実装は壊れる可能性があります。
ActiveRecord::Keyのインターフェイスに追随する形に書き換えることを検討してください。
gem 作者・拡張ライブラリ作者向け
- CPK をサポートする独自の関連・finder・スコープ生成ロジックを持っている gem は、
ActiveRecord::Keyの存在を前提に、実装を単一/複合を意識しない形にリファクタできる可能性があります。 - 将来的に:
- 例えば「UUID 主キー」「シャーディング用の複合キー」など、特殊なキー戦略を
ActiveRecord::Keyのサブクラスとして提供するような拡張もやりやすくなります。
- 例えば「UUID 主キー」「シャーディング用の複合キー」など、特殊なキー戦略を
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57638
- 背景:
- 以前の CPK 対応は「機能追加の第一弾」として条件分岐ベースで実装されており、コードベースが脆くなりやすい構造だった。
- このPRはその「後片付け」として、オブジェクト指向的な抽象化(
ActiveRecord::Key)により、CPK サポートを Rails 内部の一級機能として整理する段階と位置づけられます。
#57660 Fix normalizes casting raw database values on in-place change detection
マージ日: 2026/6/11 | 作成者: @chaadow
- 概要 (1-2文で)
normalizesを使った属性について、「インプレース変更の検知(normalized_attribute_changed_in_place?)」時に DBの生値(raw value)を誤って cast していたため JSON などで例外が出るリグレッションが発生しており、これを 正しく deserialize を通すように修正した PRです。
- 変更内容の詳細
問題の背景
normalizes は属性値を保存前などに正規化する仕組みですが、「その属性がインプレースで変更されたか?」を判定するために normalized_attribute_changed_in_place? が内部で呼ばれます。
この判定では、
- 「現在の値」と
- 「変更前の値」
を比較する必要があります。この「変更前の値」は attribute.value_before_type_cast から取られます。
ところが、DBから読み込まれた属性の場合:
value_before_type_castには シリアライズされた生のDB値(例: JSONカラムなら"{"foo":"bar"}"のような文字列)が入る- これに対して
attribute.type.cast(value_before_type_cast)を呼ぶと、typeによっては 文字列を解析せず、そのまま返す 実装になっている(ActiveRecord::Type::Json#castなど)
結果として、正規化ブロックには「期待している Ruby オブジェクト」ではなく「生の文字列」が渡ってしまい、以下のようなエラーが発生していました:
class User < ApplicationRecord
# settings は json カラム
normalizes :settings, with: -> settings { settings.transform_keys(&:downcase) }
end
user = User.find(User.create!(settings: { "foo" => "bar" }).id)
user.settings["BAZ"] = "qux" # インプレース変更
user.valid?
# => NoMethodError: undefined method 'transform_keys' for an instance of Stringsettings に期待しているのは Hash ですが、実際には "{"foo":"bar"}" のような String が渡ってきて transform_keys が呼ばれて落ちる、という状況です。
同様の問題は、「DBデフォルト値を持つ新規レコード」(例: jsonb デフォルト {})でも起きます。これも DB の生値から attribute が組み立てられるためです。
修正内容
normalized_attribute_changed_in_place? における「変更前の値」の型変換を以下のように変更:
- 変更前:
attribute.type.cast(attribute.value_before_type_cast) - 変更後:
attribute.type_cast(attribute.value_before_type_cast)
Attribute#type_cast は:
- DB由来の属性に対しては
type.deserialize(...)を呼ぶ - ユーザー入力由来の属性に対しては従来通り
type.cast(...)を呼ぶ
という分岐を内部で行うメソッドです。
このため:
- DBから読み込んだ JSON カラム →
deserializeが呼ばれ、JSON 文字列 → Ruby Hash へ正しく変換される - ユーザーが直接代入した値については、従来と同じく
castが使われる(挙動変更なし)
という形で、DB由来の属性だけ正しく扱えるようにしつつ、既存のユーザー代入経路への影響は避けています。
テスト
activerecord/test/cases/normalized_attribute_test.rb にテストが追加されています。
内容としては、少なくとも:
normalizesを持つ JSON(または類似シリアライズ型)属性- レコードを DB からロード
- 属性の中身をインプレースで変更
- 検証/保存時にエラーが出ないこと、および変更検知が正しく働くこと
といったシナリオをカバーしていると考えられます。
- 影響範囲・注意点
影響対象:
normalizesを利用していて- かつ対象カラムが JSON などのシリアライズ型(DB上は文字列やバイナリだが、Ruby では Hash / Array / オブジェクトにマッピングされる型)
- さらにその属性を インプレースで変更している (
user.settings["key"] = .../<</merge!など) ケース
改善内容:
- 上記ケースで、これまで
NoMethodErrorなどが出ていたのが解消され、期待どおりに正規化と変更検知が行われます。 - DBデフォルト値を持つ json/jsonb カラムでも、ロード直後のインプレース変更が安全に扱われます。
- 上記ケースで、これまで
後方互換性:
- ユーザーが代入した値(
user.settings = { foo: "bar" }など)に対しては従来と同じcastが使われるため、normalizesの挙動に変更はありません。 - 差し替えたのは「DB由来の
value_before_type_castをどう Ruby 型へ戻すか」という内部実装であり、外部APIは変わりません。
- ユーザーが代入した値(
注意点:
- もし独自の
ActiveModel::Typeを実装しており、cast/deserializeのどちらか一方だけを正しく実装していた場合、この変更により DB由来の値にはdeserializeが確実に使われる ため、deserialize側の実装が不十分だと逆に不具合が顕在化する可能性があります。 - ただしこれは ActiveRecord の型システムの本来の契約に沿った挙動であり、この PR 固有の奇抜な仕様変更ではありません。
- もし独自の
- 参考情報 (あれば)
- 回帰の原因となった PR: #57639
- 修正対象メソッドが含まれるファイル:
activemodel/lib/active_model/attributes/normalization.rb
- 型システムの関連メソッド:
ActiveModel::Attribute#value_before_type_castActiveModel::Attribute#type_castActiveRecord::Type#castActiveRecord::Type#deserialize
#57658 Add test coverage for ActiveSupport::Duration edge cases
マージ日: 2026/6/11 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveSupport::Duration のこれまでテストされていなかったエッジケースや、rdoc で明示されている挙動に対してテストを追加する PR です。コード本体の変更はなく、テストのみの追加で信頼性と回 regresion 検知能力を高めています。
- 変更内容の詳細
この PR では activesupport/test/core_ext/duration_test.rb に 34 行のテストコードが追加されています。主な対象は、ActiveSupport::Duration の以下のメソッド・挙動です。
2-1. 単項マイナス -@ の挙動
目的
Duration に単項マイナスを適用したとき、
- 内部の
valueが反転する partsの各要素も符号反転する
という仕様がテストされていませんでした(+@のみカバーされていた)。
イメージコード例
duration = ActiveSupport::Duration.build(3600) # 1.hour 相当
neg = -duration
# 期待される挙動(仕様に基づく)
neg.value # => -duration.value
neg.parts # => duration.parts.transform_values { |v| -v }今回のテストでは、この「value も parts も両方が確実に反転しているか」を確認しています。
2-2. .build の分解ロジックのテスト
目的
.build は「秒数から Duration を組み立てる」クラスメソッドで、rdoc に以下のような分解例が書かれていますが、その具体的な parts の中身はテストされていませんでした。
例:
ActiveSupport::Duration.build(2716146).parts # => { months: 1, days: 1 }
(実際の rdoc に準じた形)
既存テストはエラーケースと Time/Date との演算結果だけを見ており、「どの単位にどう分解されるか(rdoc に示された例通りか)」は未カバーでした。
イメージコード例
duration = ActiveSupport::Duration.build(2_716_146)
duration.parts
# rdoc に書かれている例と一致することを確認するテストが追加されている
# 例: { months: 1, days: 1 } などこの PR で、rdoc に書かれている .build の分解例がそのままテストされ、仕様と実装の乖離があれば検知できるようになっています。
2-3. #parts が独立コピーを返すことの確認
目的
Duration#parts が「内部状態と独立したコピー」を返すことは暗黙/半ば明示的な挙動ですが、「返ってきた Hash を破壊的に変更しても、元の Duration の状態は変わらない」ことを保証するテストがありませんでした。
イメージコード例
duration = 1.day + 2.hours
parts = duration.parts
parts[:days] = 10
parts.delete(:hours)
duration.parts
# => 引き続き { days: 1, hours: 2 } のような元の内容であることを確認このテストによって、parts の返り値を書き換えても Duration 自体が汚染されないことが将来にわたって保証されます(内部実装で @parts をそのまま返してしまうようなリファクタをするとテストが落ちる)。
2-4. #<=> が非 Duration / 非 Numeric に対して nil を返すこと
目的
Duration#<=> は、比較対象が
DurationNumeric
の場合と、それ以外の場合で処理が分かれています。
この「それ以外」のパス(nilを返すべき)がテストされておらず、Scalar#<=>側のnilパスのみカバーされていました。
イメージコード例
duration = 1.day
duration <=> Object.new
# => nil であることを確認するテストが追加これにより、「Duration 同士・数値との比較以外は nil を返す」という比較演算子の仕様がテストで明示されました。
2-5. #coerce に Duration を渡した場合の挙動
目的
coerce は数値演算時に Ruby が内部で呼び出すメソッドで、Numeric との二項演算を整合させるために使われます。
既存テストでは Scalar#coerce のみカバーされており、Duration#coerce に Duration を渡したときの挙動が未テストでした。
仕様としては、Duration#coerce(other) が Duration を受け取ったとき、
- 相手側の値
otherをScalarでラップして返す
というパスがあります。
イメージコード例
d1 = 1.day
d2 = 2.days
left, right = d1.coerce(d2)
left # => ActiveSupport::Duration::Scalar のインスタンス(d2 をラップ)
right # => d1この PR は上記のような「Duration 同士の coerce の戻り値が Scalar を用いる」という仕様をテストで固定化しています。
- 影響範囲・注意点
- 影響範囲はテストコードのみで、本番コード(
ActiveSupport::Durationの実装)には一切変更がありません。 - 既存の
Duration利用コードが壊れることはありませんが、-@の符号反転仕様.buildの分解ロジック#partsのコピー性- 非 Duration / 非 Numeric との
<=>がnilになること - Duration 同士の
coerceがScalarを返すこと
といった挙動が、今後のリファクタで変わるとテストが落ちるようになります。
- そのため、これらは「事実上の仕様」としてより強く固定されます。
Rails 本体やアプリ側で Duration を拡張・モンキーパッチしている場合は、これらの前提を崩さないよう注意が必要です。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57658
- 対象クラス:
ActiveSupport::Duration- rdoc:
activesupport/lib/active_support/duration.rb
- rdoc:
- 関連する内部クラス:
ActiveSupport::Duration::Scalar(比較演算・coerce に関わる)
#57666 Make some more constants ractor safe
マージ日: 2026/6/11 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails の Action Pack 周辺で、Ractor(マルチスレッド並列実行機構)対応を強化するために、一部の定数およびクラス属性を freeze して「Ractor セーフ(共有可能)」にした PR です。特に scaffold されたルーティング付きの新規アプリを Ractor 内で動かす際に頻繁にアクセスされる定数を対象にしています。
- 変更内容の詳細
※PR 本文と変更ファイル一覧から読み取れる範囲での解説です(実際の diff は省略されていますが、Rails の既存実装パターンから推測を含みます)。
2-1. 全体方針
- Ruby 3 以降の Ractor では、Ractor 間で共有できるオブジェクトは「shareable」である必要があります。
- shareable にする典型的な手段が「不変オブジェクト(freeze 済み)」にすることです。
- 「リテラルとしてその場で書かれている定数」の多くは既に freeze 済みだったが、
- 非リテラルから生成している定数
- あるいは内部に non-shareable な値を持つ定数 / cattr が取りこぼされており、それらを追加で freeze しています。
2-2. ファイルごとの変更イメージ
actionpack/lib/action_dispatch/http/param_builder.rb (+1/-1)
リクエストパラメータを組み立てる内部ユーティリティで、パラメータ名や区切り文字列などの定数がある箇所です。
ここに定義されている一部の定数が
freezeされました。例としては、下記のようなイメージです(擬似コード):ruby# 変更前(例) PARAM_KEY_SEPARATOR = "&" # 変更後(例) PARAM_KEY_SEPARATOR = "&".freeze実際にはもう少し複雑なオブジェクト(配列やハッシュ)や、メソッド呼び出し結果を代入している定数を
freezeしている可能性があります。
actionpack/lib/action_dispatch/http/request.rb (+3/-5)
ActionDispatch::Requestは Rack 環境変数のキー一覧や、内部で使うヘッダ名などの定数を多数持っています。ここで、Ractor 内でリクエストを扱う際に必ず通るような定数(例: ENV キー配列、マッピングハッシュなど)が
freezeされています。+3/-5という行数から、単純なfreeze追加だけでなく、以下のようなリファクタリングもありえます(例):- すでにどこかで
freeze済みだった重複処理の削除 - 定数 or クラス変数 →
mattr_accessor / cattr_accessorなどの見直しとfreeze追加
- すでにどこかで
典型的なパターン例:
ruby# 変更前(例) ENV_METHODS = %w[REQUEST_METHOD PATH_INFO].map(&:freeze) # 変更後(例) ENV_METHODS = %w[REQUEST_METHOD PATH_INFO].freezeのように、内側を個別に freeze するのではなく、外側の配列そのものを
freezeして shareable にする変更がよくあります。
actionpack/lib/action_dispatch/http/response.rb (+1/-1)
ActionDispatch::Responseで、ステータスコードやデフォルトヘッダなどを保存している定数・クラス属性があります。そのうち Ractor 内でも共有されうるデータ(例: デフォルトヘッダのハッシュ、既知ステータスコードマッピングなど)を
freezeしています。ruby# 変更前(例) DEFAULT_HEADERS = { "X-Frame-Options" => "SAMEORIGIN" } # 変更後(例) DEFAULT_HEADERS = { "X-Frame-Options" => "SAMEORIGIN" }.freeze
actionpack/lib/action_dispatch/journey/router/utils.rb (+2/-2)
Journey は Rails のルーティングエンジンで、
Router::Utilsにはパス/ルート関連のヘルパや正規表現・キャッシュ用定数などがあります。PR 説明から「scaffolded routes のリクエストパスで当たる定数」とのことなので、
- パスパラメータを解析するための正規表現
- URL パーツを join するための配列 / 文字列パターン などが freeze されたと考えられます。
例(擬似コード):
ruby# 変更前(例) PATH_SEPARATOR = "/" # 変更後(例) PATH_SEPARATOR = "/".freeze
2-3. cattr(クラス属性)の freeze
説明に「and one cattr」とあるので、
cattr_reader/cattr_accessor/mattr_*系で定義されたクラス属性のうち、Ractor で共有される可能性があるものに対して、セット時にfreezeしたか、初期値をfreezeしたと考えられます。典型的な変更例:
ruby# 変更前(例) cattr_accessor :route_constraints self.route_constraints = {} # 変更後(例) cattr_accessor :route_constraints self.route_constraints = {}.freeze
- 影響範囲・注意点
3-1. 主な影響範囲
- 対象は主に Action Pack (ActionDispatch) で、
- リクエスト (
ActionDispatch::Request) - レスポンス (
ActionDispatch::Response) - パラメータビルド (
ParamBuilder) - Journey ルーター (
Journey::Router::Utils) に関連する定数/クラス属性です。
- リクエスト (
- Rails アプリのリクエスト処理パス全般に関係しますが、「既存の定数を mutate しているようなコード」がなければ基本的には影響はありません。
3-2. 後方互換性の観点
定数や cattr に代入されているオブジェクトが
freezeされることで、- それを 破壊的に変更しようとしているアプリケーションコードや gem があれば、
FrozenErrorが発生する可能性があります。
- それを 破壊的に変更しようとしているアプリケーションコードや gem があれば、
例えば、Rails の内部定数ハッシュを直接書き換えるようなコードは危険です:
ruby# NG の例(今回の変更で FrozenError のリスクが上がる) ActionDispatch::Response::DEFAULT_HEADERS["X-Frame-Options"] = "ALLOWALL"→ こういったカスタマイズは、initializer で
config.action_dispatch系設定を使うなど、公式にサポートされた経路で行うべきです。
3-3. Ractor を使う場合のメリット
- この PR により、Rails アプリを Ractor 内で動かした際に、
- リクエスト〜ルーティング〜レスポンスの処理パスで参照する定数がより多く「shareable」になり、
- Ractor 起動時やリクエスト処理時のエラー(non-shareable なオブジェクトの共有エラー)が減ります。
- 作者が述べているように、
rails new+ scaffold した標準的なアプリ構成で Ractor 実行する際の実用性向上が主な狙いです。
- 参考情報 (あれば)
- Ractor と shareable オブジェクトの仕様:
- Ruby 公式ドキュメント(英語): https://docs.ruby-lang.org/en/master/doc/ractor_md.html
Ractor.shareable?,Object#freezeなどのメソッド仕様
- 類似の Rails 変更:
- すでに多くの「リテラル定数」が freeze 済みで、この PR はそれに続く第 2 弾という位置付け。
- Rails コードでのベストプラクティス:
- Rails の公開 API に含まれない内部定数・クラス変数を直接書き換えず、config/initializer/拡張ポイントを使うようにすると、今回のような freeze 系の変更にも強いコードになります。
#57663 Little clean up in cookies_test.rb
マージ日: 2026/6/11 | 作成者: @andrewn617
- 概要 (1-2文で)
cookies に関するテスト (cookies_test.rb) 内の重複コードをヘルパーメソッド化し、テストコードを整理・簡素化したPRです。挙動変更はなく、あくまでリファクタリング(クリーンアップ)が目的です。
- 変更内容の詳細
PR説明から読み取れるポイントは以下です。
a. @request.env["action_dispatch.foo"] アクセスの共通化
これまでテスト内で繰り返し書かれていたような:
foo = @request.env["action_dispatch.foo"]といったコードを、専用のヘルパーメソッドに置き換えています。
(実際のキー名は foo の部分が用途に応じて違う可能性がありますが、説明では例として "action_dispatch.foo" が挙げられています)
イメージとしては、テストクラス内に例えばこんな感じのメソッドを生やしている形です:
private
def foo
@request.env["action_dispatch.foo"]
endこれにより、テスト中では単に foo と書けばよくなり、以下のメリットがあります。
- テスト内での環境変数アクセスの記述が簡潔になる
"action_dispatch.foo"のようなマジックストリングを1箇所に集約できる- 将来キー名やアクセス方法を変更したい場合も、ヘルパーを変えるだけで済む
b. cookies = @controller.send :cookies の共通化
テスト内で頻繁に
cookies = @controller.send :cookiesという記述がされていたため、これもヘルパーメソッドとしてまとめています。
ただし、テストクラスにはすでに cookies というメソッドが存在しているため、ローカル変数 cookies を使い回すと混乱したり、既存のメソッドを上書きしてしまう懸念があります。
そのため、PRではテストクラスの private メソッドとして(名前はPR文脈から推測ですが)例えば:
private
def controller_cookies
@controller.send :cookies
endのようなメソッドを追加し、テスト中では
cookies = controller_cookiesまたは直接 controller_cookies を使う形に変更していると考えられます。
この変更により:
@controller.send :cookiesというメタプログラミング的な呼び出しがテスト中からほぼ消える- 「コントローラから取得する cookies」と「既存のテスト用
cookiesメソッド」をはっきり分けられる cookiesという共通的な名前に対する上書き・衝突のリスクを減らせる
c. 行数としては減少(+140 / -163)
差分としては、追加140行・削除163行で、全体としてはコード量がやや減っています。
重複していた env アクセスや @controller.send :cookies 呼び出しをまとめたことで、テストの見通しが良くなったと考えられます。
d. 文脈: 大きなPRのための下準備
作者は、より大きなPRの中で cookies_test.rb を触る必要があり、そこで見つけた「重複の多いテストコード」を先に分離してクリーンアップしています。
そのため、このPR自体は仕様変更やバグ修正ではなく、後続PRのための土台づくりという位置付けです。
- 影響範囲・注意点
本番コードへの影響
変更対象はactionpack/test/dispatch/cookies_test.rbのみであり、ライブラリ本体のコード(app/やlib/以下)は変更されていません。そのため、本番環境での cookies 挙動には影響しません。テストコードの挙動
実質的にはメソッド抽出のみで、テストのロジック自体(何をアサートしているか)は変えていないため、挙動変更はほぼないと見なせます。メソッド名の衝突に注意
cookiesという名前のメソッド/ローカル変数が既に存在したため、そこを避ける形でヘルパーを定義しています。
今後、このテストファイルを編集する際は:- 既に存在するヘルパー(
foo相当やcontroller_cookies相当)を再利用する cookiesというローカル変数・メソッド名をむやみに増やさない といった点に注意すると、さらにコードが分かりやすく保てます。
- 既に存在するヘルパー(
CHANGELOG なし
テストのみの変更で挙動変更もないため、CHANGELOG は更新されていません。ライブラリ利用者にとっては「特に通知すべき変更はない」という扱いです。
- 参考情報 (あれば)
- PRテンプレートのチェックリストでは、「単一目的のPRであること」「コミットメッセージに理由を含めていること」「テストの追加・更新」が満たされており、テストリファクタとして妥当な範囲の変更です。
- 今後、cookies 周りで新しいテストを追加する場合は、
@request.env[...]アクセスには既存のヘルパーを使う- コントローラ経由の cookies アクセスには
@controller.send(:cookies)のラッパーメソッドを利用する
といったスタイルに合わせると、ファイル全体の一貫性が保てます。
#57657 Support a single composite primary key id in delete
マージ日: 2026/6/10 | 作成者: @55728
- 概要 (1-2文で)
Rails の ActiveRecord において、複合主キーを持つモデルに対してModel.delete(record.id)を呼び出すとArgumentErrorが出ていた問題を修正し、destroyと同様に単一レコードの削除が正しく動作するようにしました。deleteの単一・複数 ID 指定の挙動が、複合主キー環境でも一貫するようになります。
- 変更内容の詳細
問題の挙動
複合主キー (例: ["author_id", "id"]) を持つモデル Cpk::Book で次のようなコードを書いたとします。
book = Cpk::Book.first
Cpk::Book.destroy(book.id) # => OK(既に動作している)
Cpk::Book.delete(book.id) # => ArgumentError が発生していた
# ArgumentError: Expected corresponding value for ["author_id", "id"] to be an Array原因は delete が受け取った引数をそのまま
where(model.primary_key => id_or_array)に渡していたことです。
複合主キーの primary_key は配列(例: ["author_id", "id"])であり、book.id も ["author_id_value", "id_value"] のような配列になります。
このとき where は「複合主キーのカラム配列」と「値」を zip してタプル扱いしますが、delete 側が単一レコードか複数レコードかを判別せずにそのまま渡していたため、単一レコードの値配列が「タプルの配列」ではなく「タプルそのもの」として扱われ、バリデーションに失敗して ArgumentError になっていました。
修正内容
destroy はすでに、「単一 ID」と「複数 ID(バッチ)」を区別し、複合主キーの場合は「配列の配列(= タプルの配列)」で扱うロジックを持っています。
この PR では delete にも同様の処理を入れ、単一の複合主キー ID が渡された場合に、それを 1 要素の配列でラップしてから where に渡すようにしています。
イメージとしては、複合主キーかつ単一レコードの場合に
# 以前: 直接渡していた
where(primary_key => [author_id_value, id_value])
# 変更後: 1 要素の配列で包む
where(primary_key => [[author_id_value, id_value]])という差分です。これにより、where から見れば「複合主キーのタプルが 1 つだけ入った配列」として扱えるので、内部ロジックと整合します。
仕様上の一貫性
これで、クラスメソッドの delete / destroy は以下のような一貫した API になります(単一主キー / 複合主キー問わず):
# 単一 ID
Model.destroy(id) # コールバックあり
Model.delete(id) # コールバックなし(DELETE 直発行)
# 複数 ID(バッチ)
Model.destroy([id1, id2])
Model.delete([id1, id2])複合主キーの場合は、id 自体が「タプル (配列)」であり、複数 ID のときは「タプルの配列」になりますが、それを意識せずに destroy と同じ感覚で delete を使えるようになりました。
テスト
activerecord/test/cases/persistence_test.rb に、複合主キーを持つモデルに対して Model.delete(record.id) を実行できることを検証するテストが追加されています。
これにより、destroy だけでなく delete についても、複合主キーの単一 ID 削除が継続的に保証されます。
- 影響範囲・注意点
影響範囲
- ターゲットは ActiveRecord のクラスメソッド
deleteのうち、複合主キーを持つモデルに単一 ID を渡したケースのみです。 - 単一主キーのモデルに対する
deleteの挙動は変わりません。 - 複合主キーでも、すでに動いていた「複数 ID 指定 (
delete([id1, id2]))」の挙動はそのまま維持されます。 - コールバックが走るインスタンスメソッド
record.destroy、クラスメソッドModel.destroyの挙動にも変更はありません。
- ターゲットは ActiveRecord のクラスメソッド
後方互換性
- これまで
Model.delete(record.id)がエラーで落ちていたコードが、今後は正常に削除されるようになります。 - 既存コードで
deleteのエラーを前提にしたワークアラウンド(例: rescue して別処理)があった場合、そのロジックが呼ばれなくなる可能性はありますが、もともとドキュメント上はdestroyと同じ ID 指定が想定されていたため、仕様としては修正・是正方向の変更です。
- これまで
注意点
- 複合主キー利用時は、単一レコード削除:
Model.delete(record.id)/ 複数削除:Model.delete([record1.id, record2.id])という形で使えるようになりますが、
各record.idが「タプル(配列)」、複数指定は「タプルの配列」となる点は依然として変わりません。 - コールバックや
dependent: :destroyを利用したい場合は、従来どおりdestroyを使う必要があります。今回の変更はあくまでDELETE直発行のdeleteに対する API パリティ改善です。
- 複合主キー利用時は、単一レコード削除:
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57657
- 関連ドキュメント: Active Record クエリインターフェイス「
deleteとdestroyの違い」delete: コールバックなしで直接DELETE文を発行destroy: コールバック、関連削除 (dependent: :destroy) などを実行してから削除
- 背景: Rails 本体では複合主キーは公式サポート対象外に近い扱いですが、内部的には一部機能が動作しており、本 PR はそうした「半サポート状態」の中で
destroyとの仕様差分を解消するものです。
#57653 Keep composite primary key columns in the default attribute set
マージ日: 2026/6/10 | 作成者: @55728
- 概要 (1-2文で)
このPRは、複合主キーを持つモデルでselectによる部分読み出しを行った際、主キー列を参照するとActiveModel::MissingAttributeErrorが発生してしまう問題を修正し、単一主キーと同様にnilを返すようにしたものです。attributes_builderがデフォルト属性セットを構築する際に、複合主キー列も常に初期化対象に含めるようにしています。
- 変更内容の詳細
問題の挙動
単一主キーの場合:
# 主キー: id
Topic.select(:title).first.id
# => nil (MissingAttributeError にはならない)複合主キーの場合:
# 主キー: [:author_id, :id] など
book = Cpk::Book.select(:title).first
book.author_id
# => ActiveModel::MissingAttributeError: missing attribute 'author_id' for Cpk::Book同じ「partial select で主キー列を取得していない」状況でも、単一主キーは nil を返すのに対し、複合主キーでは MissingAttributeError が出てしまう不整合がありました。
原因
ActiveRecord はモデルの「常に初期化されるデフォルト属性セット」を attributes_builder を通して構成しています。その際、主キーは常に初期化対象に残し、それ以外のカラムを除外する、というロジックになっています:
# 修正前(イメージ)
defaults = _default_attributes.except(*(column_names - [primary_key]))ここで:
単一主キーの場合
primary_key # => "id"column_names - ["id"] # => ["title", "created_at", ...]
→ それらをexceptで除外するので、「主キー id だけは defaults に残る」という意図通りに動作。複合主キーの場合
primary_key # => ["author_id", "id"](Array)column_namesは["author_id", "id", "title", ...]などの String の配列column_names - [primary_key]は「String 配列 から [Array] を引く」形になるため、何もマッチせず、結果はcolumn_namesのまま(= 何も引かれない)。
→defaults = _default_attributes.except(*column_names)となり、全カラムが defaults から除外されてしまう。
→ 主キー構成要素(author_id,id)も defaults に残らないため、partial select で読み込んでいない主キー列を参照するとMissingAttributeErrorになる。
修正内容
primary_key を常に配列として扱うようにして、複合主キーの各列もちゃんと保持されるようにしました。
# 修正後
defaults = _default_attributes.except(*(column_names - Array(primary_key)))単一主キー:
primary_key # => "id"Array(primary_key) # => ["id"]
→ 挙動は従来と完全に同じ。複合主キー:
primary_key # => ["author_id", "id"]Array(primary_key) # => ["author_id", "id"]column_names - ["author_id", "id"]により、主キー構成要素を除いたカラムだけが除外対象になる。
→defaultsには主キー列が残り、「常に初期化される属性セット」に主キー列が含まれるようになる。
このロジックは、同じファイルにある _returning_columns_for_insert では既に Array(primary_key) を使っており、それに揃えた形です。
テスト
activerecord/test/cases/primary_keys_test.rb にテストが追加されています(+7行)。
内容としては、「複合主キーを持つモデルで partial select を行った場合に、主キー列を読み出しても MissingAttributeError にならず nil が返る」ことを検証するものと考えられます。
- 影響範囲・注意点
対象:
- ActiveRecord で 複合主キー (composite primary key) を使用しているモデル。
- 特に
select(:title, ...)のような partial select を使いつつ、読み込んでいない主キー列にアクセスするコード。
挙動の変更点:
- これまでは、複合主キーの一部カラムを
selectに含めていない場合に、そのカラムを読もうとするとActiveModel::MissingAttributeErrorが発生していました。 - このPR以降は、単一主キーと同様に「レコードの属性としては存在しているが値は読み込まれていない」扱いとなり、読み出し時は
nilが返るようになります。
- これまでは、複合主キーの一部カラムを
後方互換性の観点:
- 「MissingAttributeError が発生すること」を前提にして例外処理を書いているコードがあれば、挙動が変わる可能性があります(例外が出ずに
nilになる)。 - 一方で、単一主キーではもともと
nilを返していたため、「主キー種別によって挙動が違う」という不整合が解消される形であり、多くのアプリケーションにとっては望ましい変更と考えられます。 - 何らかの理由で「複合主キー列が必ずロードされていること」を期待している場合は、
selectにその列を含めるか、select自体を見直す必要があります。今後は「ロードしていない主キー列はnilで返る」ことを前提にすべきです。
- 「MissingAttributeError が発生すること」を前提にして例外処理を書いているコードがあれば、挙動が変わる可能性があります(例外が出ずに
- 参考情報 (あれば)
- 該当箇所のコード:
activerecord/lib/active_record/model_schema.rbのattributes_builder周辺。 - 複合主キーを扱う場合、ActiveRecord本体は単一主キー前提の設計が多いため、今回のように
Array(primary_key)で吸収するパターンが他にも存在します(今回も_returning_columns_for_insertに揃えた形)。 - この修正により、主キーが単一か複合かに関わらず「partial select → 主キー列アクセス」の挙動が統一されるため、アプリケーションレベルでの扱いがシンプルになります。
#57649 Return an empty array from find([]) on a composite primary key
マージ日: 2026/6/10 | 作成者: @55728
- 概要 (1-2文で)
複合主キーを持つモデルに対してModel.find([])を呼ぶとActiveRecord::RecordNotFoundが発生していた挙動を、単一主キーと同様に空配列[]を返すように修正した PR です。これにより、主キーの種類に依存しない一貫したfindの挙動が保証されます。
- 変更内容の詳細
これまでの挙動の違い
単一主キーのモデル:
Topic.find([]) # => []複合主キーのモデル:
Cpk::Book.find([])
# => ActiveRecord::RecordNotFound
# Couldn't find all Cpk::Books with '["author_id", "id"]': ()
# (found 0 results, but was looking for 1)同じ find([]) 呼び出しでも、主キーが複合かどうかで挙動が変わっていました。
多くのコードでは以下のように「前段の検索結果の ID をそのまま find に渡す」パターンがあります:
ids = Cpk::Book.where(published: true).pluck(:author_id, :id)
records = Cpk::Book.find(ids) # ids が空配列のときも自然に動いてほしいこのとき ids が空配列でも、そのまま find(ids) して安全に [] を返してほしい、というのが今回の問題意識です。
問題の原因
ActiveRecord::Relation::FinderMethods#find_with_ids は、渡された ids の「形」を見て、「配列を返すべきかどうか」を判定しています(= 呼び出し元が単数レコードを期待しているのか、複数レコードを期待しているのかを推測している)。
複合主キーの場合、もともと次のようなロジックになっていました:
expects_array = ids.first.first.is_a?(Array)複合主キーの単一レコード指定:
find([1, 2])idsは[[1, 2]]ids.firstは[1, 2]ids.first.firstは1(Array ではない)→expects_array == false
複合主キーの複数レコード指定:
find([[1, 2], [3, 4]])idsは[[[1, 2], [3, 4]]]のような形になるケースを想定ids.first.firstは[1, 2](Array)→expects_array == true
しかし find([]) の場合:
ids = []
ids.first # => []
ids.first.first # => nilnil.is_a?(Array) は false なので「単一の複合キーが指定された」と誤認識され、「1件見つかるはず」として動いてしまい、結果として RecordNotFound が発生していました。
修正内容
空配列が渡された場合にも「配列を期待している」と判定するように条件式を修正しました。
修正後のロジック(概要):
expects_array =
ids.first.is_a?(Array) &&
(ids.first.empty? || ids.first.first.is_a?(Array))ポイント:
ids.first.is_a?(Array)
まず最初の要素自体が Array かをチェック(ids.first.empty? || ids.first.first.is_a?(Array))ids.first.empty?→[]の場合(= 今回のfind([]))はここで true となり、配列を期待していると判定- それ以外でも、
ids.first.firstが Array(=[[1,2],[3,4]]のような「複合主キーの配列の配列」)なら配列を期待と判定
この結果、以下のような挙動になります:
# 変更後
# 単一主キー
Topic.find([]) # => []
# 複合主キー (今回修正された部分)
Cpk::Book.find([]) # => [] # 以前は RecordNotFound
# 非空の複合主キー指定の挙動は従来どおり
Cpk::Book.find([1, 2]) # => 単一レコード (期待どおり)
Cpk::Book.find([[1, 2], [3, 4]]) # => 複数レコード配列 (期待どおり)テストとしても activerecord/test/cases/finder_test.rb に、複合主キーで find([]) が空配列を返すことを確認するケースが追加されています。
- 影響範囲・注意点
挙動が変わるケース
- 複合主キーのモデルで
Model.find([])を呼んだときに、これまではActiveRecord::RecordNotFoundだったものが、今後は[]を返すようになります。 - もし以前の例外発生に依存したロジック(例外を rescue して処理分岐するなど)がある場合は、挙動が変わる点に注意が必要です。
- 複合主キーのモデルで
挙動が維持されるケース
- 単一主キーのモデルに対する挙動は変わりません。
- 複合主キーに対しても、
find([1, 2])やfind([[1, 2], [3, 4]])など、非空の引数の挙動は変わりません。 find(nil)やfind(1)といった他のパラメータ形状については、この PR による影響はありません(従来どおり)。
実務的な影響
- 「前段の検索結果(空かもしれない)をそのまま
findに渡す」コードが、単一主キー・複合主キー問わず同じように安全に書けるようになります。 - 複合主キー対応のコードパスを書くときに、「空配列の場合だけ特別扱いする」といったワークアラウンドは不要になります。
- 「前段の検索結果(空かもしれない)をそのまま
- 参考情報 (あれば)
- 対象メソッド:
ActiveRecord::Relation::FinderMethods#find_with_ids - 主に参照するとよいファイル:
activerecord/lib/active_record/relation/finder_methods.rbfind/find_with_idsの ID 解析ロジック
activerecord/test/cases/finder_test.rb- 複合主キー周りの
findの期待挙動を確認するテストが追加されています。
- 複合主キー周りの
- 背景となる API 仕様:
- Rails では
Model.find([])は「ID が空のときは空配列を返す」という仕様であり、これが主キーの種類に関わらず一貫するように揃えられた修正です。
- Rails では
#57594 Fix bug where reload leaks ordinary scopes into all_queries lookups.
マージ日: 2026/6/10 | 作成者: @andrewn617
- 概要 (1-2文で)
reload実行時に、all_queries用のデフォルトスコープだけを使うはずのクエリに、通常のスコープ(current_scope)が紛れ込んでしまうバグを修正した PR です。allを経由せずdefault_scoped(all_queries: true)を直接使うことで、all_queriesなスコープだけが正しく反映されるようになりました。
- 変更内容の詳細
問題になっていた挙動
Active Record の reload は、all_queries デフォルトスコープを考慮してモデルを再読込する際に、内部的に以下のような流れになっていました(擬似コード):
# 以前のイメージ(問題があるパターン)
def reload(...)
relation = all(all_queries) # ← all を経由
# ...
endしかし Relation#all は current_scope(現在適用中のスコープ)を引き継ぐ性質があります。そのため:
CurrentScopeModel.where(published: true).scoped do
post = Post.first
post.reload
endのような状況で、Post の reload 時のクエリに、Post 本来の all_queries デフォルトスコープに加え、where(published: true) など「意図しない通常スコープ」が混入する可能性がありました。
本来 reload で参照したいのは「all_queries: true 対象のデフォルトスコープ」と「all_queries 指定のグローバルスコープ」だけです。
修正内容
PR では、reload 内部のクエリ生成を以下のように変更しています(概念的なイメージ):
# 新しいパターン(問題の修正)
def reload(...)
# all(all_queries) をやめて、直接 all_queries 向けの default_scoped を使う
relation = default_scoped(all_queries: true)
# さらに current_global_scope を調べて、
# all_queries:true なスコープのみをマージするようにする
if current_global_scope&.all_queries?
relation = relation.merge(current_global_scope)
end
# relation を使ってレコードを再取得
endポイント:
all(all_queries)の代わりにdefault_scoped(all_queries: true)を直接呼び出すことで、current_scope由来の通常スコープが混入しないようにした。- その上で、
current_global_scopeの中からall_queriesフラグが立っているものだけを取り込むようにしているため、「all_queriesなデフォルトスコープ/グローバルスコープだけを使ったクエリ」でreloadされる。 activerecord/test/cases/scoping/default_scoping_test.rbにテストが追加され、この漏れ込みバグが再発しないことを確認している。
※ 実際のメソッド名・ロジックは上記より細かいですが、意図としては「reload における all_queries 専用のクエリ生成経路を明確に分けた」と理解しておくとよいです。
CHANGELOG
activerecord/CHANGELOG.md にエントリが追加され、reload と all_queries スコープに関するバグ修正として明示されました。
- 影響範囲・注意点
- 影響を受けるケース
default_scope -> all_queries: trueといった仕組みを利用しているモデルで、- かつ、
current_scopeやグローバルスコープが有効なコンテキストでreloadを呼んでいる場合。
- 期待される変更
- これまでは、
reload時のクエリに「現在の通常スコープ」が紛れ込んでいた可能性があり、その結果として:- 想定より絞り込まれた条件で再読み込みされる
- 条件にマッチせず
reload時の挙動が不自然になる
- といった挙動が、今回の修正で「
all_queries向けのデフォルトスコープ+all_queriesなグローバルスコープのみ」に限定されます。
- これまでは、
- 注意点
- もしアプリ側のコードが「
reloadが現在の通常スコープを暗黙に引き継ぐ」ことを前提にしてしまっていた場合、その前提が壊れる可能性があります(ただし、この前提自体がバグ依存の挙動です)。 reloadの挙動変更によりテストが落ちた場合、reloadにスコープが効いているかどうかをテストしていないか確認するとよいです。reloadは「DB の最新状態をそのレコードの主キーで取り直す」動きを想定すべきであり、任意スコープの効果には依存しない方が安全です。
- もしアプリ側のコードが「
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57594
- 関連 PR(バグ発見のきっかけになったもの): https://github.com/rails/rails/pull/57123
all_queriesは Rails 7.2 以降で進められているスコープの分離・制御まわりの一部で、default_scopeやグローバルスコープを「すべてのクエリに効かせるか」「特定のクエリ API のみに効かせるか」を細かく制御するための仕組みです。
#57637 Reinitialize the thread-safe level key when a logger is copied
マージ日: 2026/6/10 | 作成者: @etiennebarrie
- 概要 (1-2文で)
LoggerThreadSafeLevelを持つロガーをclone/dupした際に、元のロガーとスレッドローカルなログレベルの保存領域を共有してしまうバグを修正しています。コピー時にキーを再初期化することで、コピー後のロガーごとに独立したスレッドセーフなログレベル管理が行われるようになります。
- 変更内容の詳細
背景: 何が問題だったか
ActiveSupport::LoggerThreadSafeLevel は「ロガーごと」にスレッドローカルなログレベルを保持する仕組みを持っています。
ここでの実装は:
- スレッドローカル変数 (
Thread.current[...]) を使う - そのキーとして、ロガーインスタンスの
object_idから導出した値 を使う
という設計になっていました。
ところが、Ruby の clone / dup は:
- 新しいオブジェクトを作るため
object_idが変わる - しかしインスタンス変数はコピーされる
ため、以下のような状態になっていました:
- オリジナルロガー A が
@thread_safe_level_keyを持っている (object_id A に紐づくキー) A.cloneしてロガー B を作ると、A と同じ@thread_safe_level_keyを持った B ができる- しかし B 自身の
object_idは A と異なる - その結果、本来「ロガーごと」に分離されるはずのスレッドローカルなログレベルの保存領域が A と B で共有されてしまう
とくに #log_at などで一時的にログレベルを変えるようなコードを書くと、A/B どちらかの操作がもう一方に影響する(互いに上書きしあう)という、直感に反するバグが起きます。
修正内容
修正は非常にピンポイントで、LoggerThreadSafeLevel のコピー時初期化フック #initialize_copy をオーバーライドし、#initialize 同様にキーを再生成するようにした、というものです。
対象ファイル:activesupport/lib/active_support/logger_thread_safe_level.rb
主な変更点(要約):
# 疑似コードレベルのイメージ
class ActiveSupport::LoggerThreadSafeLevel
def initialize(*)
super
reinitialize_thread_safe_level_key
end
def initialize_copy(other)
super
# ここを今回追加
reinitialize_thread_safe_level_key
end
private
def reinitialize_thread_safe_level_key
@thread_safe_level_key = :"logger_thread_safe_level_#{object_id}"
end
end実際のコードでは reinitialize_thread_safe_level_key のような名前かはともかく、考え方としては:
#initializeでやっている「object_id に基づくキー生成」を#initialize_copyでも呼ぶ
という形で、「clone/dup 後のオブジェクトは必ず自分専用のキーを持つ」ことを保証しています。
テストの追加
対象ファイル:activesupport/test/logger_test.rb
テストでは、主に以下のようなシナリオを確認していると考えられます:
- ロガー
logger1を作成し、そのクローンlogger2 = logger1.cloneを作る - 各ロガーで
log_atやスレッドローカルレベルを設定 logger1とlogger2のログレベル操作が相互干渉しないことを検証
例として、構造イメージは次のようなものです(実際のテスト名は多少違う可能性があります):
def test_cloned_logger_has_independent_thread_local_level
logger1 = ActiveSupport::TaggedLogging.new(Logger.new(StringIO.new)).logger
logger2 = logger1.clone
logger1.log_at(:debug) { ... }
logger2.log_at(:info) { ... }
# それぞれの logger に対して、設定したレベルが期待通りに効いていることを検証
# かつ、一方を変えてももう一方に影響しないことを検証
end実テストでは、より具体的に「一定メッセージが出る/出ない」をアサートしているはずです。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは ActiveSupport のスレッドセーフロガーレベル機能(
LoggerThreadSafeLevel)を利用しつつ、ロガーをcloneまたはdupしているコード です。 - Rails 標準のロギング(
Rails.logger)でも内部的にこの仕組みを使っているため、アプリケーションやライブラリがロガーのコピーを行っている場合に挙動が変わります。
具体的な挙動の変化
以前:
logger2 = logger1.cloneとした場合、logger1.log_atとlogger2.log_atが同じスレッドローカルストレージ領域を共有してしまう- そのため、
logger2で一時的にログレベルを変更したつもりが、logger1側のログ出力にも影響しうる
修正後:
logger1とlogger2は 異なるキーでスレッドローカルレベルを管理する ため、互いのlog_atが干渉しなくなる- 「ロガーインスタンスごとに独立したログレベルが保たれる」という自然な期待に沿った挙動になる
注意点
- もし既存コードが「clone したロガーは元ロガーとレベルを共有する」という、これまでの誤った挙動に依存していた場合は、挙動が変わります。ただし、そのような依存はまず意図的ではないと考えられ、今回の変更はバグフィックス扱いと見て良いです。
Logger自体の API(log,info,debugなど)や、log_atの表向きの仕様は変わっていません。変わるのは clone/dup 時の内部的なスレッドローカル管理キーのみです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57637
- 関連する内部実装:
ActiveSupport::LoggerThreadSafeLevelLogger#clone,Logger#dupとinitialize_copyフック
- 類似のパターン:
object_idに基づいたキャッシュキーや Thread-local キーを使うクラスでは、initialize_copyでの再初期化を行わないと同種の問題が起きることがあります(例: メモ化キャッシュ、ミドルウェアの状態保持など)。
#57647 Don't mutate the names array passed to Cache::Store#delete_multi
マージ日: 2026/6/10 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::Cache::Store#delete_multiが、呼び出し元から渡された配列を破壊的に変更していた問題を修正し、他の multi 系メソッド (read_multi,write_multi,fetch_multi) と同様に非破壊的な処理に統一する PR です。これにより、キー配列が意図せず変更されたり、名前空間が二重に付与される不具合や、凍結配列での例外が解消されます。
- 変更内容の詳細
問題点
これまでの delete_multi の実装では、内部的に以下のようなことをしていました(概念的なコード):
def delete_multi(names, options = nil)
# 実際には normalize_options などがありますが要点のみ
names.map! { |key| normalize_key(key, options) } # ここが破壊的
delete_multi_entries(names, options)
endこのため:
cache = ActiveSupport::Cache::MemoryStore.new(namespace: "ns")
names = ["foo", "bar"]
cache.write("foo", 1)
cache.write("bar", 2)
cache.delete_multi(names)
names # => ["ns:foo", "ns:bar"] になってしまうというように、呼び出し側が渡した names 配列の中身自体が、名前空間付きの内部キーに書き換わっていました。副作用として:
- 配列再利用時に二重 namespace 化されるruby
# 1回目で names が ["ns:foo", "ns:bar"] に書き換わっている cache.write("foo", 1) cache.write("bar", 2) cache.delete_multi(names) # => 0 (ns:ns:foo を探しに行く) cache.exist?("foo") # => true (削除されていない) - 凍結された配列を渡すと例外が発生するruby
cache.delete_multi(["foo", "bar"].freeze) # => FrozenError: can't modify frozen Array: ["foo", "bar"]
一方で、read_multi, write_multi, fetch_multi などの兄弟メソッドは、いずれも新しい配列・ハッシュを作って処理しており、引数を破壊的に変更しません。この不整合が今回の修正対象です。
修正内容
names.map! をやめて、非破壊的な map に変更し、その結果をローカル変数に再代入する形に変更されています。
修正後のイメージ:
def delete_multi(names, options = nil)
# ...
names = names.map { |key| normalize_key(key, options) }
delete_multi_entries(names, options)
endこれにより:
- 呼び出し側が渡した配列は、内容が変更されない
- 内部で使うのは normalized 済みの別配列になる
という挙動になります。PR 説明にもあるように:
Nothing downstream relies on the mutation —
delete_multi_entries... only iterates over the array it receives, andinstrument_multionly reads it for the event payload.
つまり、delete_multi_entries や instrument_multi など後続処理は「渡された配列を読むだけ」で、破壊的変更に依存していないため、この変更による副作用はありません。
テスト追加
activesupport/test/cache/behaviors/cache_store_behavior.rb にテストが追加されています。テストでは主に以下を検証しているはずです(差分行数からの推定を含む):
delete_multiに渡した配列の中身が呼び出し後も変わらないこと- namespace 付きの store でもキー配列が書き換わらないこと
- (場合によっては)凍結済み配列でもエラーにならずに動作すること
- 影響範囲・注意点
既存アプリ側への直接的な影響
- 破壊的変更が「バグ」とみなされる挙動だったため、ほとんどのコードにとっては 挙動が改善するだけ です:
delete_multi呼び出し後に、渡した配列を引き続き生のキー配列として安全に使えるようになります。- 凍結された配列 (
["foo", "bar"].freeze) をdelete_multiに渡してもFrozenErrorが出なくなります。
- 破壊的変更が「バグ」とみなされる挙動だったため、ほとんどのコードにとっては 挙動が改善するだけ です:
delete_multiに副作用を期待していたコード
非推奨なパターンではありますが、以下のように「delete_multi呼び出し後、キー配列が内部キーに置き換わっている」ことを前提にしていたコードがあれば、挙動が変わります:rubynames = ["foo", "bar"] cache.delete_multi(names) # 旧挙動では names == ["ns:foo", "ns:bar"] を期待できた do_something_with_internal_keys(names)そのようなコードは今回の変更後に動作しなくなるため、
normalize_key相当の処理を自分で呼ぶ(もしくは内部キーに依存しない設計にする)必要があります。ただし、Rails の公開 API としては内部キーの形に依存するのは想定されていないため、そのようなコードはもともと壊れやすい書き方です。multi 系メソッドの一貫性が向上
read_multi,write_multi,fetch_multi,delete_multiがいずれも「引数コレクションを破壊しない」という仕様で統一されます。
これにより、「配列や enumerable をキャッシュキーとして何度も再利用する」ようなパターンも安心して書けます。
- 参考情報 (あれば)
- 対象メソッド:
ActiveSupport::Cache::Store#delete_multi - 関連メソッド:
ActiveSupport::Cache::Store#read_multiActiveSupport::Cache::Store#write_multiActiveSupport::Cache::Store#fetch_multi
- 影響ストア実装:
- 基底実装 (
ActiveSupport::Cache::Store#delete_multi_entries) RedisCacheStoreのdelete_multi_entriesオーバーライド
いずれも、渡された配列を「読むだけ」であり、今回の変更で壊れないことが PR 内で確認されています。
- 基底実装 (
#57643 Add test coverage for the remaining Type::Date cast branches
マージ日: 2026/6/10 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveModel::Type::Date#cast の、これまでテストされていなかった分岐パスに対してテストを追加し、分岐網羅に近づけた PR です。アプリケーションコードの変更はなく、テストのみの追加です。
- 変更内容の詳細
対象: ActiveModel::Type::Date#cast(正確には内部の cast_value メソッド)の残りの分岐に対するテスト追加。
変更ファイル: activemodel/test/cases/type/date_test.rb に 25 行のテストコードを追加。
既存テストでは、以下のみがカバーされていました:
nil/ blank / パース不能文字列- ISO フォーマット文字列("YYYY-MM-DD")
- multiparameter 属性(
{ 1 => "2000", 2 => "1", 3 => "1" }のようなハッシュ)
今回の PR では、それ以外の「別のコードパス」がテスト対象に追加されています。
追加でカバーされた分岐は以下の 4 つです。
(1) to_date を持つ値のキャスト
仕様:
String以外の値で、#to_dateメソッドを持つもの (Time,Date,DateTimeなど) はvalue.to_dateでキャストされる。
テスト内容のイメージ:
type = ActiveModel::Type::Date.new
time = Time.utc(2020, 1, 2)
assert_equal Date.new(2020, 1, 2), type.cast(time)
date = Date.new(2020, 1, 2)
assert_equal date, type.cast(date)目的:
- ドキュメントに書かれている「その他の値は
to_dateでキャストされる」挙動を明示的に保証。
(2) 非 ISO だがパース可能な文字列 → フォールバックパーサ
仕様:
"2020-01-02"のような ISO 形式はISO_DATEの高速パスで処理されるが、
それ以外の文字列で、Date.parse等を使えば解釈可能なものはfallback_string_to_dateルートでパースされる。
テスト内容のイメージ:
type = ActiveModel::Type::Date.new
# 例: "1/2/2020" のようなロケール依存だが Ruby の Date.parse で解釈可能な形式
assert_equal Date.new(2020, 1, 2), type.cast("1/2/2020")目的:
- ISO 形式専用の高速パスとは別に、「フォールバックの文字列パーサ」が実際に通ることをテストで保証。
- 将来的なリファクタ時に、このパスが死んだコードになっていないか検知しやすくする。
(3) 文法的には正しいがカレンダー上は不正な日付 → nil
仕様:
"2008-02-31"のように「YYYY-MM-DD」としては正しいが、存在しない日付の場合、Date.new(year, month, day) rescue nilによってnilを返す。- これは
"ABC"のような完全にパース不能な文字列とは別の分岐。
テスト内容のイメージ:
type = ActiveModel::Type::Date.new
assert_nil type.cast("2008-02-31")区別されるケース:
"ABC"→ 早い段階の「nil-parts guard」でパース不能と判断されてnil。"2008-02-31"→ 数値3つは取れるので一度Date.newを試みるが、例外になりrescue nilでnil。
目的:
- 「形式的には正しいが存在しない日付」というケース専用のガードが正しく機能していることを保証。
(4) String でも to_date でもない値 → そのまま返す
仕様:
- 値が
Stringではなく、かつ#to_dateも実装していない場合、castはその値を変更せずに返すelse分岐がある。
テスト内容のイメージ:
type = ActiveModel::Type::Date.new
obj = Object.new # to_date を持たない任意オブジェクト
assert_same obj, type.cast(obj)目的:
- 変換対象外の値に対しては無理にキャストを試みず、そのまま返すという現在の仕様を固定化。
- 将来この分岐が変わった場合(例: 例外を投げるように変える等)にテストが教えてくれるようにする。
- 影響範囲・注意点
- 本 PR はテストコードのみの変更であり、
ActiveModel::Type::Dateの挙動そのものは従来から存在したものです。
→ これまで「暗黙に依存していた挙動」が、明示的にテストで固定された形になります。 - もし既存アプリケーションが
- 「to_date を持つ独自クラスを date 型にマッピングしている」
- 「非 ISO フォーマット文字列を渡している」
- 「不正日付/to_date 未実装オブジェクトを通している」 といったケースに依存している場合でも、挙動は変わりませんが、
- 将来の Rails バージョンでこのあたりを仕様変更しようとするとテストが落ちるため、挙動変更には明示的な判断が必要になります。
開発者としての実務的な読み方:
- 「いまの Rails は、date キャストにおいて上記 4 ケースをこのように扱う」と仕様がテストで確定した、と理解しておくとよいです。
- 特に、「to_date を持たない非 String 値はそのまま返す」という仕様は、型チェックやバリデーション設計時に前提としておけます。
- 参考情報 (あれば)
- 対象コード:
ActiveModel::Type::Date(activemodel/lib/active_model/type/date.rb) - 既存ドキュメント:
- Rails Guides: Active Record Validations and Callbacks(date 型のキャストの挙動)
- API Docs:
ActiveModel::Type::Date#cast/#cast_value
- 関連する典型的な使用例:
- モデル属性で
attribute :published_on, :dateとしている場合のパラメータキャスト - form から送られてくる
"YYYY-MM-DD"以外の文字列やTimeオブジェクトの扱い確認用テストを書きたい場合に、この PR のテストを参考にできます。
- モデル属性で
#57644 Memoize ivar when freezing ActiveSupport::TaggedLogging::Formatter
マージ日: 2026/6/10 | 作成者: @etiennebarrie
- 概要 (1-2文で)
ActiveSupport::TaggedLogging::Formatterを freeze した状態でも Ractor 間で共有して使えるようにするため、インスタンス変数のメモ化処理が追加されました。これにより、Ractor-shareable な logger 構成で tagged logging を安全に利用できます。
- 変更内容の詳細
※PR本文が短く、差分も小さいため、一般的な Rails の実装パターンおよび Ractor の制約に基づく技術的な読み解きになります。
背景: Ractor と logger / formatter
Ruby 3 の Ractor でオブジェクトを共有するには、そのオブジェクトが「Ractor-shareable」である必要があります。
基本的に以下のような制約があります:
- オブジェクトが freeze されていること
- 内部に保持しているインスタンス変数も Ractor-shareable であること
- freeze 済みオブジェクトに対して新たにインスタンス変数を書き換え・追加しないこと
ActiveSupport::TaggedLogging::Formatter は、通常の logger の formatter をラップしてタグ付きログを実現するクラスですが、従来の実装では内部でインスタンス変数を遅延初期化(メモ化)している部分が Ractor-shareable の観点で問題になりうる実装でした。
この PR で行われたこと
PR タイトル: Memoize ivar when freezing ActiveSupport::TaggedLogging::Formatter
意図としては次のような処理が追加されています:
ActiveSupport::TaggedLogging::Formatter#freezeが呼ばれたタイミング、もしくはその前に、- 内部で利用するインスタンス変数(例: タグの prefix 生成用の Proc/フォーマッタのラッパーなど)を「先に確定させて」メモ化する
- freeze 後に新規にインスタンス変数を設定しないようにする
イメージとしては下記のような変更が入っていると考えられます(擬似コード・実際のコードとは若干異なる可能性があります):
class ActiveSupport::TaggedLogging::Formatter
def initialize(formatter)
@formatter = formatter
end
# 以前はここで遅延初期化していた / 毎回ブロックを生成していたなど
def call(severity, time, progname, msg)
# tagged logging 用のラッパーを利用
# 例: @formatter.call(severity, time, progname, formatted_message)
end
def freeze
# freeze 前に必要な ivar を全て確定させる(遅延初期化しない)
some_helper # <= ここで @some_helper を初期化しておく、など
super
end
private
def some_helper
@some_helper ||= compute_helper_value
end
endポイントは:
freeze前に@some_helperなどのインスタンス変数を確定させておくことで、
freeze 後に@some_helper ||=のような遅延セットを実行しなくて済むようになる- これにより、Ractor から参照しても「freeze 済みオブジェクトに対するインスタンス変数の変更」が発生しないため、Ractor-shareable なオブジェクトとして扱えるようになる
テストの追加
activesupport/test/tagged_logging_test.rb にテストが6行追加されています。内容としては:
ActiveSupport::TaggedLogging::Formatterを freeze した状態で利用するケース- もしくは Ractor-shareable かどうか(
Ractor.shareable?)を確認するようなテスト
などが追加され、Ractor と組み合わせた利用でも例外が出ない・期待通りに動作することを確認しています。
- 影響範囲・注意点
- 対象:
ActiveSupport::TaggedLogging::Formatterを使ったログ出力(ActiveSupport::TaggedLogging経由の logger)- 特に、Ruby 3+ で Ractor を用いつつ、共通の logger / formatter を Ractor 間で共有したいケースで有効
- 従来コードへの互換性:
- 変更はインスタンス変数の初期化タイミング(遅延初期化 → freeze 前にメモ化)に関するものなので、
外部 API やログ出力フォーマットに影響が出る可能性はほとんどありません - Tagged logging の public インターフェース (
tagged { ... }など) は変更されていないはずです
- 変更はインスタンス変数の初期化タイミング(遅延初期化 → freeze 前にメモ化)に関するものなので、
- 注意点:
- Ractor 内で logger / formatter を共有する場合でも、「formatter 自体が freeze されていること」「内部で保持しているオブジェクト(例: underlying formatter)が shareable であること」は依然として必要です
- アプリ側で formatter を継承・モンキーパッチしていて、freeze 後にインスタンス変数を変更するようなコードがあると、Ractor 環境では引き続き問題になる可能性があります
- 参考情報 (あれば)
- Ruby 公式ドキュメント(Ractor と shareable オブジェクト)
- Rails ガイド: Active Support Tagged Logging
- 類似の変更(Ractor 対応のための freeze / shareable 対応)は Rails 7 系以降で他のクラスにも入っていますので、Ractor 対応を行う場合は logger 周り以外も含めて shareable 条件を確認することが推奨されます。
#57646 Minor Grammar Fixes
マージ日: 2026/6/10 | 作成者: @yashika279
概要 (1-2文で)
このPRは、Railsガイドの README (guides/README.md) に含まれていた英文の文法ミスを 1 箇所だけ修正したものです。アプリケーションコードやフレームワークの挙動には一切影響せず、ドキュメント品質の向上のみを目的としています。変更内容の詳細
- 対象ファイル:
guides/README.md - 変更行: +1 / -1(1 行分の文言を置き換え)
内容としては、英語の文章中の軽微な文法・表現の誤りを修正するものです。
具体的には、たとえば以下のようなタイプの修正に相当します(※イメージ例):
- This guides helps you to understand Rails.
+ These guides help you understand Rails.実際の PR でも同様に、
- 単数・複数形の不一致
- 冠詞 (a/the) の誤用
- 前置詞の選択ミス
- 動詞の形 (helps → help など)
といった「読んだときに違和感がある程度の軽微な文法ミス」を修正していると考えられます。
コードロジックの変更はなく、ガイド文書中の一文のみが差し替えられています。
- 影響範囲・注意点
- 影響範囲
- Rails の挙動、API、設定値、マイグレーション、生成物などには一切影響なし
- Rails ガイドを英語で読む際の可読性・自然さがわずかに向上
- 注意点
- バージョンアップ時に、この PR を理由にテストや動作確認を強化する必要はありません。
- ドキュメントの文言を引用している社内資料や記事がある場合、文言が 1 行だけ微妙に変わっている可能性はありますが、意味が変わるような修正ではないため、通常は気にしなくて構いません。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57646
- 対象ファイルの最新内容は、Rails 本家リポジトリの
guides/README.mdを参照してください。 - ドキュメント系 PR は、挙動には影響しないものの、ガイドやチュートリアルを教材として使う場合には誤解を減らす助けになります。
#57060 Offload ActiveStorage::Blob#metadata sync to background
マージ日: 2026/6/9 | 作成者: @shouichi
- 概要 (1-2文で)
Active Storage のActiveStorage::Blob#metadata更新処理を即時実行からバックグラウンドジョブ実行に切り離すことで、サーバーとワーカーが同時にメタデータを更新してしまうレースコンディション(特に GCS などでのエラー)を回避する変更です。これに伴い、サービス層の API と計測イベント(Instrumentation)がジョブ実行前提の形に整理されています。
- 変更内容の詳細
2-1. メタデータ同期のバックグラウンドジョブ化
新規ジョブが追加されています。
# activestorage/app/jobs/active_storage/sync_metadata_job.rb
class ActiveStorage::SyncMetadataJob < ActiveStorage::BaseJob
queue_as { ActiveStorage.queues[:analysis] }
def perform(blob)
blob.service.sync_metadata(blob.key, **blob.service_metadata)
end
endポイント:
ActiveStorage::Blobから「メタデータを同期する責務」を切り出し、SyncMetadataJobに委譲。- Queue は
ActiveStorage.queues[:analysis](既存の分析ジョブと同じ系統)に載せられる。 - 実際の同期処理は
Service#sync_metadataに委譲する形に統一。
2-2. ActiveStorage::Blob 側の変更
Blob モデルにおけるメタデータ同期の呼び出しが、即時実行からジョブのキュー投入に変更されています(実コードは概略レベルになりますが、イメージは以下のような形です)。
class ActiveStorage::Blob < ActiveRecord::Base
# 例: 以前は直接 service.update_metadata 的な呼出しを行っていた箇所
def sync_metadata_async
ActiveStorage::SyncMetadataJob.perform_later(self)
end
end- これまで「Blob 側で service を直接叩いてメタデータ更新」を行っていたパスが、原則としてバックグラウンドジョブを経由する作りに変わっています。
- テスト (
blob_test.rb,attachment_test.rb) もそれに合わせて、「呼び出しが即時にサービスに届く」前提から「ジョブを経由する」前提に修正されています。
2-3. ActiveStorage::Service API の整理
Service 基底クラスに「メタデータ同期」を表す明示的なメソッドが追加されています。
# activestorage/lib/active_storage/service.rb
class ActiveStorage::Service
# ...
# Blob 内に保持している metadata とサービス側のメタデータを同期するためのフック
def sync_metadata(key, **metadata)
# デフォルト実装は空実装 or 一般的な実装
end
end変更点:
- 以前は GCS サービスなどで独自にメタデータ更新メソッドを持っていたのを、共通の
sync_metadataインターフェースで扱うように統一。 - これにより、
SyncMetadataJobはサービスの種類を意識せず、常にblob.service.sync_metadataを呼ぶだけで済む。
2-4. GCS サービス (GCSService) の挙動変更
activestorage/lib/active_storage/service/gcs_service.rb が主な対象です。
主な変更ポイント:
- サーバープロセス(リクエスト処理)とワーカーが同時に GCS オブジェクトのメタデータを書き換えることで発生していた race condition を避けるよう、書き込みのタイミングを「ジョブからの
sync_metadata呼び出し」に集中させる。 - 以前はアップロードや添付の直後など、複数箇所からメタデータ更新を行っていたのを整理し、サービスレベルの更新窓口を
sync_metadataに限定するような形になっています。 - その過程で GCS の API 呼び出しパラメータやメタデータ更新ロジックが一部簡素化・再配置されています(+8/-10 行程度の差分)。
結果として、
- 「Blob レコードの書き換え」
- 「GCS へのメタデータ反映」
のタイミングが decouple され、同時実行での衝突が起きにくい設計になっています。
2-5. Active Support Instrumentation の更新
guides/source/active_support_instrumentation.md に Active Storage 関連イベントの説明追加・修正があります。
- メタデータ同期が非同期ジョブ経由になったことを反映したイベント説明が追加。
- 例えば「メタデータ同期開始/終了」イベントがどのタイミングで発火するか、payload に何が入るかといった説明がアップデートされています。
- これにより、監視やメトリクス収集で「いつメタデータ同期が走ったか」「どれぐらい時間がかかっているか」が追いやすくなります。
2-6. テスト・CHANGELOG の更新
activestorage/test/jobs/sync_metadata_job_test.rbで新ジョブの動作がテストされている- 指定された
Blobに対し、service.sync_metadataが正しく呼び出されるか - 複数回呼び出しや例外ハンドリングの基本的な挙動など
- 指定された
- 既存テスト (
blob_test.rb,attachment_test.rb) は、新しい非同期挙動を前提とした期待値に変更。 activestorage/CHANGELOG.mdに、- 「メタデータ同期がバックグラウンドで行われるようになった」
- 「レースコンディション回避」
などの変更点が追記されている。
- 影響範囲・注意点
3-1. メタデータの反映タイミングが「遅延」する
- 以前:
Blob#metadata更新に連動して(ほぼ)同期的にストレージサービスのメタデータも更新される前提だったコードがある場合、その前提は崩れます。 - 以後:
- メタデータ更新はジョブキューに積まれ、ワーカーが動いたタイミングで反映されます。
- 「更新直後にストレージ側のメタデータを前提とした処理」を行っていると、レースに負ける可能性があります。
対策・考慮:
- メタデータを元にしたバリデーションや外部連携処理がある場合、「DB 上の
Blob#metadataを信頼し、ストレージ側のメタデータ状態に依存しない」形に寄せるのが安全です。 - どうしてもストレージ側を即時に反映させたいケースがあるなら、独自ジョブや直接
service.sync_metadataを呼び出すラッパーをアプリ側で用意するなどの検討が必要です(ただし本 PR の意図である race 回避を阻害しないよう注意)。
3-2. Active Job / キュー基盤が必須となる度合い
- Active Storage 利用時に「メタデータ同期」が Active Job に依存するようになるため、バックグラウンドジョブが正しく動作しない環境では、メタデータ更新がストレージ側に反映されません。
- Rails 標準の
:asyncアダプタであっても最低限は動きますが、実運用では Sidekiq などのジョブランナーを安定運用する前提がより強くなります。
3-3. レースコンディションの修正による副作用
- GCS のように「同一オブジェクトへの並行メタデータ更新に厳しいサービス」での 409/412 系エラーは減る想定です。
- ただし、アプリ側で「サーバーからも独自にメタデータを更新する」ようなコードを追加してしまうと、再度同じ問題に近い状況を作りかねません。
→ ストレージサービスのメタデータ操作は、原則としてBlob標準機能 + このSyncMetadataJobに一本化するのが望ましいです。
3-4. モニタリング・ログ
- Instrumentation が更新されているため、
- メタデータ同期ジョブの開始/終了イベント
- 失敗時の例外情報
を購読することで、ジョブの詰まりやエラーを検知しやすくなります。
- 新しいイベント名や payload は
active_support_instrumentation.mdの更新を確認してログ/メトリクス設定を見直すとよいです。
- 参考情報 (あれば)
- 対応 Issue: https://github.com/rails/rails/issues/54919
- GCS などで Blob メタデータ更新時に発生していた race condition の詳細なレポートがあるはずです。
- PR 本体: https://github.com/rails/rails/pull/57060
activestorage/app/jobs/active_storage/sync_metadata_job.rbactivestorage/lib/active_storage/service.rbactivestorage/lib/active_storage/service/gcs_service.rb
を読むと、サービス API の揃え方とジョブ分離の具体的な意図が把握しやすいです。
- Active Storage ガイド:
- 今後のガイド更新で「分析やメタデータ関連処理はジョブベース」前提がより明示されていく可能性があります。
#57639 [ActiveModel] Re-run normalizes only when the value changed in place
マージ日: 2026/6/10 | 作成者: @yaroslav
- 概要 (1-2文で)
ActiveModel.normalizesを使った属性正規化について、「未保存レコードでvalid?を呼ぶたびに毎回正規化が再実行されてしまう」不具合を修正し、本当に「インプレース変更」があった場合だけ再正規化するようにした PR です。これにより、挙動のバグが直るとともに、特に未保存レコードの繰り返しバリデーション時のパフォーマンスが改善されます。
- 変更内容の詳細
問題の背景
normalizes :name, with: ->(value) { ... } のような正規化は、属性の値が「インプレースで変更されたとき」に再度走るように設計されています。その判定には attribute_changed_in_place? が使われていました。
しかし:
- 未保存レコードには「保存済みの値」がなく、内部的には
nilと比較される - そのため「一度読んだだけの属性」でも「保存済み値(nil) と違う」と判定されてしまう
- 結果として、未保存レコードに対して
valid?を呼ぶたびに、正規化が毎回実行される という挙動になっていました
これは特に「再実行するたびに結果が変わる正規化」においてバグになります:
class User
include ActiveModel::Model
include ActiveModel::Attributes
attribute :code, :string
normalizes :code, with: ->(v) { v&.succ } # 文字列を次の文字に
end
user = User.new(code: "b")
user.valid? # "b" -> "c"
user.valid? # "c" -> "d"
user.valid? # "d" -> "e"意図としては「インプレース変更がない限り、同じ値を繰り返し正規化すべきではない」ので、これは不正な挙動です。
新しい判定ロジック
この PR では、「本当に値がインプレース変更されたかどうか」を、次のように判定するように変更しています:
- 「元の値 (original value)」を覚えておく
- 現在の値を、元の値に正規化をかけた結果 と比較する
- もし「現在値 = 元の値を正規化したもの」と等しければ
→ 読み出しただけで変更されていないとみなし、元の値にリセットする
→ その後のvalid?では再正規化しない - もし「現在値 ≠ 元の値を正規化したもの」であれば
→ インプレース変更があったとみなし、もう一度正規化を走らせる
- もし「現在値 = 元の値を正規化したもの」と等しければ
このアプローチにより:
- 単なる読み取り
- 一度正規化された後、未保存のまま
valid?を何度呼んでも、余計な再正規化は走らない - 内部では元の値に戻されるので、「次に読むときに再度正規化」が正しく行われる
- 一度正規化された後、未保存のまま
- インプレース変更 (
user.name.strip!など)- 「元の値を正規化したもの」と今の値が食い違うため、再度正規化が走る
結果として、「本当にインプレース変更があったときだけ再正規化」が実現されます。
既存 API 観点での動作イメージ
通常のパターン:
class User
include ActiveModel::Model
include ActiveModel::Attributes
attribute :email, :string
normalizes :email, with: ->(value) { value&.strip&.downcase }
end
user = User.new(email: " Foo@Example.COM ")
user.valid?
# email は "foo@example.com" に一度だけ正規化
user.valid?
# 2回目以降の valid? では、正規化は再実行されない (今回の修正)インプレース変更パターン(例):
user.email # => "foo@example.com"
user.email.upcase! # インプレース変更
user.valid?
# upcase! の結果に対して、正規化 (strip/downcase) が再度実行される
# => "foo@example.com" に戻る今回の修正により、上の挙動が正常に担保されるようになります。
実装上の主な変更
activemodel/lib/active_model/attributes/normalization.rbnormalize_changed_in_place_attributesのロジックを修正- 変更の有無判定を「保存済み値との比較」から、「元値を再正規化した結果との比較」に変更
- 「単に読まれただけ」のケースでは元の値に戻す挙動を追加
activemodel/test/cases/attributes/normalization_test.rb- 上記の新しい挙動を検証するテストを追加
activemodel/CHANGELOG.md- バグ修正として記載を追加
- 影響範囲・注意点
影響範囲
対象:
ActiveModel::Attributesのnormalizes機能を使っているモデル- 特に「未保存レコード」に対して
- 属性を読み出し
valid?を何度も呼び出す- もしくはインプレース変更を行う というパターンがある場合
期待されるメリット:
- 再実行で結果が変わる正規化(
succ, 増分・ランダム文字列付与など)のバグが解消 - 再実行で結果が変わらない正規化(
strip,downcase,squishなど)でも無駄な計算が減る - ベンチマークでは未保存レコードのバリデーションで若干の性能改善:
- YJIT off: 480k i/s → 533k i/s (~1.1x)
- YJIT on: 1.10M i/s → 1.13M i/s (ほぼ同等)
- 1回の
valid?あたりの割り当て数は変化なし (6 → 6)
- 再実行で結果が変わる正規化(
挙動変更として意識すべき点
- 「
valid?のたびに正規化ロジックが走る」ことを前提にしたコードは壊れる可能性があります- 本来の設計としては「インプレース変更時のみ再正規化」が正しいため、多くの場合はバグ依存であり、この修正は望ましい互換性変更と考えられます。
- 値の「元の状態」に戻すロジックが入ったことで、デバッガで「今メモリ上にある値」を目視すると、少し挙動がわかりにくくなる場面があるかもしれませんが、公開 API 的には整合的な挙動です。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57639
- 関連 PR: https://github.com/rails/rails/pull/57619
- 対象機能:
ActiveModel::Attributesのnormalizes - 変更対象ファイル:
activemodel/lib/active_model/attributes/normalization.rbactivemodel/test/cases/attributes/normalization_test.rbactivemodel/CHANGELOG.md
#57630 Remove unordered baseline assertions from test_default_order
マージ日: 2026/6/9 | 作成者: @yahonda
- 概要 (1-2文で)
default_orderに関するテストのうち、ORDER BY句がないクエリに対して戻り順を前提にしていた不安定なアサーションを削除し、DB依存でランダムに落ちるテストを安定化する変更です。default_order機能自体の挙動は変えず、テストのみを整理しています。
- 変更内容の詳細
問題となっていたテスト
失敗していたのは HasManyAssociationsTest#test_default_order で、以下のように plain な has_many をそのまま pluck していました:
comments = posts(:welcome).comments
assert_equal [1, 2], comments.pluck(:id)しかし comments には order 指定がなく、生成される SQL は PostgreSQL では次のようになります:
SELECT "comments"."id" FROM "comments" WHERE "comments"."post_id" = $1ORDER BY が無いため、返ってくるレコードの順序は 未定義 であり、本来 [1, 2] である保証はありません。
たまたま今までは [1, 2] になっていただけで、ビルド環境やDBの状態によって [2, 1] になることがあり、実際に rails-nightly CI で [2, 1] が返ってテストが落ちました。
同様の「順序未定義な baseline アサーション」が RelationTest#test_default_order にも存在しており、こちらも潜在的に同じ問題を抱えていました。
実際の修正内容
対象ファイルは次の2つです。
activerecord/test/cases/associations/has_many_associations_test.rbactiverecord/test/cases/relations_test.rb
いずれも「default_order を使っていない、順序未定義なクエリに対するアサーション行」を削除しています。
具体的には:
HasManyAssociationsTest#test_default_orderから、posts(:welcome).comments.pluck(:id)の結果が[1, 2]である、というアサーションを削除
RelationTest#test_default_orderから、- 同様に単純な Relation に対して「この順序で返るはず」というアサーションを削除
PR本文にもある通り、.sort を付けて
assert_equal [1, 2], comments.pluck(:id).sortのようにすれば安定化はできますが、その場合に確認できるのは「コメントID 1と2が返ってくる」ことだけであり、これは他のテストでも既にカバーされています。
一方で、default_order の有無・挙動自体はこれらの baseline アサーションとは無関係なので、テスト意図に対してノイズとなるため、削除という判断になっています。
- 影響範囲・注意点
- ランタイムの挙動:
- Active Record の
default_order機能や関連の実装には一切変更がなく、アプリケーションコードへの影響はありません。
- Active Record の
- テスト:
- rails 本体のテストスイートのみが対象で、DBのレコード返却順序に依存してランダムに落ちる要因が取り除かれます。
- 今回削除された箇所は、
default_orderを直接検証しているわけではなく、「たまたま順序がそうなっていること」に依存した baseline であり、削除してもdefault_order自体のカバレッジには実質的な影響はありません。
- 注意点(自分のプロジェクトへの示唆):
- Rails 本体のこの修正はテストだけですが、アプリ側のテストでも同様に「
ORDER BYなしのクエリ結果の順序を前提にしたアサーション」を書いていないか注意した方がよいです。- もし順序を前提にしたいなら
order(...)を指定する - 単に要素が含まれていることだけを確認したいなら
sortやmatch_arrayを使う
- もし順序を前提にしたいなら
- 特に PostgreSQL や MySQL で
ORDER BYがない SELECT の戻り順は仕様上保証されないので、同様の flaky テストの温床になります。
- Rails 本体のこの修正はテストだけですが、アプリ側のテストでも同様に「
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57630
default_orderが追加された PR: https://github.com/rails/rails/pull/39525- rails-nightly 該当失敗ログ:
https://buildkite.com/rails/rails-nightly/builds/4408#019ea909-5baa-430c-91a2-66d4fe1f65ee/L11949
#57636 Fix number_to_currency crashing on a negative number with precision: nil
マージ日: 2026/6/9 | 作成者: @55728
- 概要 (1-2文で)
number_to_currencyにprecision: nil(丸めなし)を渡したとき、負の数だけがTypeErrorでクラッシュしていたバグを修正した PR です。負数でも正数と同様にprecision: nilを受け付け、クラッシュせずに通貨文字列を返すようになります。
- 変更内容の詳細
問題の挙動
number_to_currency は本来、precision: nil を指定すると「丸めを行わず、そのまま表示する」という意味になりますが、負数に対してのみ例外が発生していました。
number_to_currency(1234.5678, precision: nil)
# => "$1,234.5678" # OK
number_to_currency(-1234.5678, precision: nil)
# => TypeError: nil can't be coerced into Integer
number_to_currency("-1234.5678", precision: nil)
# => TypeError: nil can't be coerced into Integer一方で、同系のヘルパーは負数でも問題ありません。
number_to_rounded(-1234.5678, precision: nil)
# => "-1234.5678"
number_to_percentage(-1234.5678, precision: nil)
# => "-1234.5678%"バグの原因
NumberToCurrencyConverter#convert の負数側の処理に、「マイナスゼロ(-$0)を表示しないためのガード」が入っています。
format = options[:negative_format] if (number_d * 10 ** options[:precision]) >= 0.5- 趣旨: 丸めた結果が 0 になるような非常に小さい負数 (
-0.0001など) を、-$0と表示しないようにするための判定。 - 問題:
precision: nilのとき、10 ** options[:precision]→10 ** nilとなりTypeErrorが発生する。 - 正数側の分岐ではこのガードが実行されないため、「負数のときだけ落ちる」という挙動になっていた、という構造です。
このガードは過去のコミット ce321c4539(「非常に大きな数値を扱う対応」)で追加されたもので、その際に precision: nil ケースが考慮されていませんでした。
修正内容
「precision: nil のときはそもそも丸めをしない」ことから、
- 非ゼロの負数は常に負の符号を維持する
- よって「丸めたら 0 かどうか」の判定自体が不要
と判断し、ガード条件に options[:precision].nil? のチェックを追加しています。
修正前:
format = options[:negative_format] if (number_d * 10 ** options[:precision]) >= 0.5修正後:
format = options[:negative_format] if options[:precision].nil? || (number_d * 10 ** options[:precision]) >= 0.5これにより:
precisionがnilの場合は(number_d * 10 ** options[:precision])を評価せずにnegative_formatを適用precisionが数値の場合は従来どおり「丸めたら 0 以上か?」のチェックを行う
という分岐になり、TypeError が解消されます。
テスト
activesupport/test/number_helper_test.rb に回帰テストが追加されています。
新しいテストは、負の数および負の数値文字列に対して precision: nil を渡したときに例外が起きず、期待する形式で出力されることを検証しています。
- 影響範囲・注意点
影響範囲
ActionView::Helpers::NumberHelper#number_to_currencyをprecision: nil付きで使っているコードで、負数が入力されるケースに影響します。- これまで負数で
precision: nilを指定するとTypeErrorが発生していた箇所は、正常に通貨文字列が返るようになります。 - 正数、および
precisionが数値の場合の動作は変更されません。
互換性
- バグ修正であり、仕様の後方互換性は基本的に維持されています。
- もしアプリ側でこの
TypeErrorを前提にした独自ハンドリングをしていた場合は、そのハンドリングが呼ばれなくなる可能性がありますが、通常は望ましい改善と考えられます。
マイナスゼロ (
-$0問題) への影響precisionが数値のときのガードロジックは従来どおり残っているので、「ごく小さい負数が丸め後に 0 になるケース」を-$0と表示してしまう問題は引き続き回避されます。precision: nilのときはそもそも丸めをしないため「丸め後 0 かどうか」の議論が不要であり、挙動は合理的です。
- 参考情報 (あれば)
- 対象メソッド:
ActionView::Helpers::NumberHelper#number_to_currency - 実装箇所:
activesupport/lib/active_support/number_helper/number_to_currency_converter.rb
- 関連ヘルパー(挙動比較用):
#number_to_rounded#number_to_percentage
#57635 Preserve the sub-second fraction in TimeZone#strptime with %s
マージ日: 2026/6/9 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::TimeZone#strptimeで、"%s.%N"/"%s.%L"のようなエポック秒+小数形式をパースした際にサブ秒(ナノ秒)が失われていたバグを修正したPRです。Ruby標準ライブラリTime.strptimeと同じ挙動になるよう、小数部分も含めてTimeを生成するようにしています。
- 変更内容の詳細
問題となっていた挙動
ActiveSupport::TimeZone#strptime でエポック秒形式 (%s) を使うと、サブ秒が存在しても無視されていました:
zone = ActiveSupport::TimeZone["UTC"]
zone.strptime("1577836800.123456789", "%s.%N").nsec
# => 0 # サブ秒が欠落
Time.strptime("1577836800.123456789", "%s.%N").nsec
# => 123456789 # Ruby 標準ライブラリは正しく保持原因は、内部で DateTime._strptime が返す parts のうち、
{
seconds: 1577836800,
sec_fraction: (123456789/1000000000r)
}のように :seconds と :sec_fraction の両方があるケースで、ActiveSupport::TimeZone 側の parts_to_time ヘルパーが :seconds ブランチで :sec_fraction を無視していたことにあります。
擬似コード的には以下のような状態でした:
if parts[:seconds]
# バグ: 小数部分を捨てていた
time = Time.at(parts[:seconds]) # sec_fraction を足していない
else
# こちらの分岐では sec_fraction を考慮している
time = Time.new(
parts[:year],
parts[:mon],
parts[:mday],
parts.fetch(:hour, 0),
parts.fetch(:min, 0),
parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
utc_offset
)
end一方で、iso8601 / rfc3339 / parse など、_strptime が :seconds ではなく :sec + :sec_fraction を使う経路では、もともとナノ秒が保持されていました。そのため、%s だけが不自然にサブ秒を捨てる状態になっていました。
修正内容
parts[:seconds] が存在する分岐でも :sec_fraction を加算して Time を生成するように1行修正しています:
# 修正前
time = Time.at(parts[:seconds])
# 修正後
time = Time.at(parts[:seconds] + parts.fetch(:sec_fraction, 0))これにより、
zone = ActiveSupport::TimeZone["UTC"]
zone.strptime("1577836800.123456789", "%s.%N").nsec
# => 123456789と、Ruby標準 Time.strptime と同じ結果になります。
テスト
以下のようなテストが追加されています(要旨):
def test_strptime_with_timestamp_seconds_and_fractional_seconds
zone = ActiveSupport::TimeZone["UTC"]
time = zone.strptime("1577836800.123456789", "%s.%N")
assert_equal 1577836800, time.to_i
assert_equal 123456789, time.nsec
end既存の %s (整数秒のみ) や %Q (ミリ秒) に関するテストは影響を受けず、time_zone_test.rb 全体がグリーンであることが確認されています。
- 影響範囲・注意点
影響対象:
ActiveSupport::TimeZone#strptimeを%s.%N/%s.%Lのような「エポック秒 + 小数」の形式で使っているコード。- それ以外の形式 (
%Y-%m-%d, ISO8601, RFC3339 など) や、Time.strptime(Ruby標準) には影響なし。
互換性:
- 以前はサブ秒が 0 に丸められていたところが、正しいサブ秒を保持するようになります。
- もし既存コードが「
nsecは常に 0 である」という前提に依存していた場合、その前提は崩れますが、これはバグ修正として妥当な互換性変更です。 %s単体 (サブ秒なし) のケースは、sec_fraction自体が存在しないため挙動は変わりません。%Q(ミリ秒) は_strptime側で最初から:secondsをRationalで返すため、以前からサブ秒が保持されており、今回の変更でも挙動は変わりません。
パフォーマンス:
Time.atにRational/Float相当を渡している形になるだけで、実質的なオーバーヘッドは軽微と考えられます。
実運用上の注意:
- 高精度なタイムスタンプ(ログや分散トレーシングのタイムスタンプなど)を
TimeZone#strptime+%s.%Nで取り扱っている場合、これまでは精度が失われていたことになります。- これを前提にしたロジック (例: 「同じ秒の中ではすべて同時刻として扱う」など) がある場合、挙動が変化する可能性があります。
- Ruby標準とActiveSupportの挙動差を前提にしたワークアラウンドがある場合は、不要になるため削除できる可能性があります。
- 高精度なタイムスタンプ(ログや分散トレーシングのタイムスタンプなど)を
- 参考情報 (あれば)
関連API:
ActiveSupport::TimeZone#strptimeTime.strptime(Ruby標準)DateTime._strptime(内部で使用される解析ルーチン)
挙動の整理:
- ISO8601/RFC3339、
Time.zone.parse等: もともとサブ秒保持済み。 %s(整数エポック秒): 以前から正しく秒単位で扱い、サブ秒なし。%Q(ミリ秒):_strptime側で:secondsにRationalを返すため、もともとサブ秒保持済み。%s.%N/%s.%L: 今回の修正により、Ruby標準と同等にサブ秒を正しく保持するようになった。
- ISO8601/RFC3339、
#57634 Forward blocks to DelegateClass methods that yield implicitly
マージ日: 2026/6/9 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::Delegation.DelegateClassが、暗黙にyieldするメソッドへブロックを正しく委譲できていなかったバグを修正し、Ruby標準ライブラリのDelegateClassと同じくブロックを転送するようにしたPRです。これにより、scanやmapなどブロック必須のメソッドをラップしても、期待どおりブロックが動作するようになります。
- 変更内容の詳細
問題の背景
ActiveSupport::Delegation.DelegateClass は、ラップ対象クラスの public メソッドを走査し、それぞれに対応する delegator メソッドを動的に定義しています。このとき、Ruby の Method#parameters からメソッドシグネチャ(引数リスト)を組み立てる実装に変えた結果、以下のような問題が出ていました。
- 「暗黙のブロック」を使うメソッド(
def foo; yield; endのように&blockを引数に取らず、内部でyieldするメソッド)はparametersに:block情報を持たない - そのため、生成される delegator メソッドのシグネチャには
&blockが含まれない - 結果として、呼び出し側でブロックを渡しても、委譲先メソッドにはブロックが渡らず ブロックが黙って捨てられる
例:
wrapper = ActiveSupport::Delegation.DelegateClass(String).new("a1b2c3")
res = []
wrapper.scan(/\d/) { |m| res << m }
res
# 実際: [] (ブロックが実行されない)
# 期待: ["1", "2", "3"] (Ruby stdlib DelegateClass の挙動)同様に Array#map のような典型的なブロックメソッドも壊れていました:
ActiveSupport::Delegation.DelegateClass(Array).new([1, 2, 3]).map { |n| n * 2 }
# 実際: [1, 2, 3] (ブロック無視)
# 期待: [2, 4, 6]Ruby 標準の DelegateClass は暗黙ブロックも含めて正しく転送するため、Rails 側の DelegateClass が意図した仕様から外れていた形です。
修正方針
- すでに
...(引数をすべて転送するシンタックス)を使うパスではブロックも自動転送されており問題なし - 問題があるのは「
parametersから明示的に引数リストを組み立てているパス」で、ここに 匿名ブロック転送&を足す ことで対応
Ruby では、メソッド定義側で & だけを書くと「呼び出し時に渡されたブロック(あるいは nil)をそのまま転送する」ことができます。これを利用して:
- 元から
&blockを引数に持つメソッドは今までどおり(既に block param があるのでそのまま) parameters上はブロック引数を持たないメソッドでも、定義側で匿名&を付けることで- ブロックあり呼び出し → ブロックを委譲先に渡す
- ブロックなし呼び出し →
nilブロックが渡る(挙動は従来と同じ)
という挙動を実現しています。
実際にどう変わるか
修正後:
wrapper = ActiveSupport::Delegation.DelegateClass(String).new("a1b2c3")
res = []
wrapper.scan(/\d/) { |m| res << m }
res
# => ["1", "2", "3"] # Ruby stdlib の DelegateClass と同じ
ActiveSupport::Delegation.DelegateClass(Array).new([1, 2, 3]).map { |n| n * 2 }
# => [2, 4, 6]テスト・実装の変更点
activesupport/lib/active_support/delegation.rbDelegateClassの delegator 生成部分に匿名&を付与する形で約 5 行変更...を使うパスはもともとブロック転送されているため変更なし
activesupport/test/delegation_test.rb- 新規ファイルとして
DelegateClassの挙動テストを追加(+62 行) - この PR の修正がない状態だと 6 テスト中 5 つが fail/error し、修正によりすべてパス
- 新規ファイルとして
- CHANGELOG にエントリ追加(ユーザー可視の挙動修正として明示)
- 影響範囲・注意点
- 影響対象:
ActiveSupport::Delegation.DelegateClassを直接使っているコード- それを内部的に使っている Rails の各種デコレータ(例: ActiveRecord の型メタデータ、楽観的ロック周りなど)
- 具体的には、委譲先のメソッドが暗黙
yieldを使っている場合 に挙動が変わります- これまで「ブロックが完全に無視されていた」ケースで、今回からは「正しくブロックが実行される」ようになる
- つまり、意図せず「ブロックが無視されていたバグ」が表面化する可能性はあります(本来の動作に戻るという意味での互換性差分)
- ブロックを渡さない呼び出しへの影響はありません
- 匿名
&はブロックがない場合nilを転送するだけなので、委譲先の挙動は変わりません
- 匿名
- 標準ライブラリの
DelegateClassと挙動が揃うため、stdlib ベースの実装から Rails のDelegateClassに乗り換えている場合などは、むしろ期待どおりになります。
利用者側での確認ポイント:
DelegateClassを使って定義した wrapper / decorator クラスでeach/map/scan/times/findなど「ブロックを前提とするメソッド」を委譲している箇所がないか- もし以前から「ブロックが呼ばれていない?」などの違和感があった場合、今回の修正で問題が解消していないか確認する価値があります
- 参考情報 (あれば)
- Ruby 標準ライブラリ
DelegateClassドキュメント:
https://docs.ruby-lang.org/en/master/DelegateClass.html - 関連する変更(前提となった最適化):
- コミット
dc17084b7b"Faster class delegator" —parametersベースでシグネチャを生成するようにした変更
- コミット
- Ruby のブロック転送挙動について:
def foo(&block)… block を明示的に受け取るdef foo(...)… 引数・キーワード・ブロックをすべてそのまま転送def foo(*)+bar(&)のような匿名&構文で「受け取ったブロックをそのまま転送」できる(今回利用されているテクニックに相当)
#57632 Carry has_many default_order: into the cached association SQL
マージ日: 2026/6/9 | 作成者: @55728
- 概要 (1-2文で)
has_manyの新オプションdefault_order:が、関連先レコードを実際にロードするときに無視されてしまう不具合を修正し、かつ statement cache を維持したままORDER BYをキャッシュされた SQL に反映するようにした PR です。default_order:を指定した関連でも、to_a/reloadなどでのロード結果が常に期待どおりの順序になるようになります。
- 変更内容の詳細
問題の挙動
has_many に default_order: を付けた場合、本来は関連取得時のデフォルトの ORDER BY を指定できますが、以下のようなギャップがありました:
class Author < ApplicationRecord
has_many :posts, default_order: "posts.id DESC"
end
author = Author.first
author.posts.to_sql # => "... ORDER BY posts.id DESC" ✅ SQL上は並び替えがある
author.posts.pluck(:id) # => [6, 5, 4, 2, 1] ✅ 期待どおり DESC
author.posts.to_a.map(&:id) # => [1, 2, 4, 5, 6] ❌ DBの素の順序
author.posts.map(&:id) # => [1, 2, 4, 5, 6] ❌to_sqlやpluckなど「Relation をそのまま使う経路」ではORDER BYが効いている。- しかし、関連のターゲット(
author.postsの実体の配列)をロードする経路 (to_a,map,reloadなど) ではdefault_order:が反映されておらず、DB 任せの順序になっていた。
結果として「SQL には ORDER BY が出ているのに、実際のオブジェクト配列は別の順序」という、非常にわかりにくい silent wrong-result が起きていました。
原因
CollectionAssociation#scope- ここでは
scope.default_order!(options[:default_order])が呼ばれるため、pluck/count/to_sqlなど「関連スコープとして Relation を組み立てるとき」にはdefault_order:が反映される。
- ここでは
Association#find_target(関連先のロード)- 通常は statement cache を使う「高速パス」で SQL を組み立てる。
- この高速パスでは
AssociationScope#scopeを使って SQL が生成されるが、ここにはoptions[:default_order]を反映する処理がなかった。 - 一方で、
skip_statement_cache?がtrueの「遅いパス」ではself.scopeを使うためdefault_order:が効くが、今回のケース(シンプルなhas_many+default_order:のみ)だとskip_statement_cache?はfalseとなり、常に高速パスが使われる。その結果、ターゲットのロード時にはORDER BYなしのキャッシュ SQL が使われていた。
つまり、「relation として扱うときは CollectionAssociation#scope 経由で default_order が効くが、ターゲットロード時の高速パスは AssociationScope#scope 経由で、そこには default_order がなかった」という抜け漏れです。
修正内容
以前の PR (#57538) では、「default_order: が指定された関連は statement cache を使わない(= skip する)」という方向で修正していましたが、この PR ではそれをやめて、
AssociationScope#scope自体にdefault_order:を反映させ、キャッシュされる SQL にORDER BYを含める
というアプローチに変更しています。
具体的な変更:
# activerecord/lib/active_record/associations/association_scope.rb
scope.default_order!(reflection.options[:default_order]) if reflection.options[:default_order].present?ポイント:
reflection.options[:default_order]を参照し、AssociationScope#scopeでdefault_order!を呼び出すことで、statement cache 用のスコープにもORDER BYを組み込む。default_orderはNORMAL_VALUESな merge key:target_scope.merge!の過程でこの値がきちんと引き継がれる。
default_order!は「追加」ではなく「代入」挙動:- 既存の
CollectionAssociation#scopeでのdefault_order!呼び出しと衝突せず、結果として idempotent になる(同じ default_order が二重に乗るようなことが起きない)。
- 既存の
この結果、statement cache を無効化せずに、キャッシュされる SQL 自体に ORDER BY を含められるようになりました。
テスト追加
has_many_associations_test.rb および author モデルのテスト定義に以下を追加:
# test/models/author.rb
has_many :posts_with_default_order, class_name: "Post", default_order: "posts.id DESC"テスト内容:
test_default_order_is_applied_when_the_target_is_loadedto_a/reloadしたときの実際のレコード順がdefault_order:による SQL の順と一致することを検証。
test_default_order_keeps_using_the_statement_cacheskip_statement_cache?がfalseのままであることを検証し、「statement cache を捨てた結果 order が効いている」のではなく、「キャッシュ SQL に order を埋め込んでいる」ことを確認。
テストは sqlite3 / postgresql / mysql2 / trilogy で has_many_associations_test がグリーンになることを確認済みです。
- 影響範囲・注意点
- 新機能
has_many ... default_order:(まだ未リリース)の挙動が修正されます。- これまで(不具合発生中)は、
pluckなどだけが意図どおり並び替えられ、関連ロード(to_a,reload,mapなど)は DB 任せの順序でした。 - この PR 以降は、「関連をロードするときも、
default_order:が常に尊重される」ようになります。
- これまで(不具合発生中)は、
- statement cache の利用は維持されるため、
default_order:を使ったからといってパフォーマンス劣化(毎回 SQL を組み立て直す等)は起こらない想定です。 default_order:は「デフォルトの順序」を決めるためのものであり、明示的に.order(...)を指定した場合はそちらが優先される設計(通常の order と同様)であることが想定されます。default_scopeやスコープブロックを併用している関連の場合も、NORMAL_VALUESのマージルールのもとで適切に作用するように意図されていますが、複雑なスコープ合成をしているプロジェクトでは挙動確認をしておくと安心です。
- 参考情報 (あれば)
- 対応するコメント: https://github.com/rails/rails/pull/57538#issuecomment-4655646072
- 置き換えられた PR: #57538
default_order:を導入したコミット:aa76590371(2026-05-28 マージ、まだリリース前)
#57633 Preserve falsy default value when building HashWithIndifferentAccess
マージ日: 2026/6/9 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::HashWithIndifferentAccess.newに元ハッシュを渡したとき、元ハッシュにfalseをデフォルト値として設定していても、それが失われずにそのまま引き継がれるようにするバグ修正です。これにより、Hash.new(false)など「未知キーは false を返す」ことを前提にしたコードが、HWIA 変換後も期待通り動作します。
- 変更内容の詳細
これまでの挙動
HashWithIndifferentAccess.new(hash) は、元ハッシュのデフォルト値をコピーする処理を持っていますが、その条件分岐が以下のようになっていました:
self.default = hash.default if hash.defaultRuby では false と nil が「偽」なので、元ハッシュが Hash.new(false) で作られている場合でも、hash.default が false だとこの条件が発火せず、デフォルト値がコピーされませんでした。
その結果:
source = Hash.new(false)
source["a"] = 1
hwia = ActiveSupport::HashWithIndifferentAccess.new(source)
hwia.default # => nil (本来は false であってほしい)
hwia["missing"] # => nil (本来は false であってほしい)元の Hash と HashWithIndifferentAccess で、存在しないキーアクセス時の挙動が食い違うバグになっていました。
修正内容
条件を「nil かどうか」で判定するように変更しています:
# 修正前
self.default = hash.default if hash.default
# 修正後
self.default = hash.default unless hash.default.nil?意味としては:
defaultがnil(通常の Hash のデフォルト)ならコピーしない(従来と同じ・多くのケースで no-op)defaultがfalseやその他の値の場合は、そのままコピーして HWIA 側のdefaultに設定する
これにより、以下のように期待通りの挙動になります:
source = Hash.new(false)
source["a"] = 1
hwia = ActiveSupport::HashWithIndifferentAccess.new(source)
hwia.default # => false
hwia["missing"] # => false
hwia[:missing] # => false # indifferent access でも同様テスト
activesupport/test/hash_with_indifferent_access_test.rbに回 regresson テストを追加- 「
Hash.new(false)を渡したときに、デフォルトが失われない」ことを確認 - 既存の HashWithIndifferentAccess テストスイート (96 テスト) はすべて green
- 影響範囲・注意点
- 影響範囲:
ActiveSupport::HashWithIndifferentAccess.new(some_hash)を使っており、かつsome_hashがHash.new(false)など、nil 以外のデフォルトを設定しているケース
- これまで:
- そうしたコードでは「元ハッシュでは unknown key → false だが、HWIA に変換すると unknown key → nil になる」という微妙な挙動差がありました。
- 今後:
- 変換後も元ハッシュのデフォルト値が忠実に引き継がれ、
HashとHashWithIndifferentAccess間で missing key 挙動がより一貫します。
- 変換後も元ハッシュのデフォルト値が忠実に引き継がれ、
- 互換性:
- 挙動が変わるのは「元デフォルトが
nil ではない」場合だけで、基本的にはバグ修正として望ましい方向です。 - もし「HWIA に変換するとデフォルトが
nilになる」ことに依存していたコードがあれば、今回の修正でfalse(など元デフォルト)を返すようになります。そのようなコードがあるとテストが落ちる可能性がありますが、元仕様に沿う形での修正と考えられます。
- 挙動が変わるのは「元デフォルトが
- パフォーマンス:
hash.default.nil?チェックはごく僅かなオーバーヘッドで、実質的な影響は無視できる範囲です。
- 参考情報 (あれば)
- この挙動は、
HashWithIndifferentAccess.newを.new_from_hash_copying_defaultと揃えるための以前の変更(コミット6e574e8a11)の副作用として現れたものを修正する位置づけです。 - Ruby 標準
Hashのdefault:Hash.new→defaultはnilHash.new(false)→defaultはfalse- また、
hash.default_procがある場合との関係はこの PR では触れていません(defaultがnilかどうかだけを見ている)。
#57606 Resolve attribute aliases for the locking column in update_all
マージ日: 2026/6/9 | 作成者: @55728
- 概要 (1-2文で)
update_allで楽観ロック用カラム(lock_version)をalias_attribute経由で指定したときに、値が失われたり PostgreSQL でエラーになる不具合を修正する PRです。update_all内でロックカラムを検出する際に、属性エイリアスも正しく解決して判定するように変更されています。
- 変更内容の詳細
問題の挙動
モデル側で lock_version にエイリアスを貼っている場合:
class Widget < ActiveRecord::Base
alias_attribute :v, :lock_version
end
Widget.where(id: id).update_all(v: 10)- SQLite/MySQL:
lock_versionが10ではなく1(自動インクリメント値)になり、ユーザー指定値が黙って失われる - PostgreSQL:
PG::SyntaxError: multiple assignments to same column "lock_version"でエラー
一方、エイリアスを使わずにカラム名そのもので指定した場合は問題なし:
Widget.where(id: id).update_all(lock_version: 10)
# => lock_version が正常に 10 になる原因
update_all は「楽観ロック有効かつ、呼び出し側が lock_version を更新していない場合」に、自動的にロックカラムをインクリメントする処理を持っています。
元コード(簡略化):
if model.locking_enabled? &&
!updates.key?(model.locking_column) &&
!updates.key?(model.locking_column.to_sym)
attr = table[model.locking_column]
updates[attr.name] = _increment_attribute(attr)
end
values = _substitute_values(updates)ここで行っているチェックは キー文字列/シンボルが lock_version かどうかだけ です。そのため:
alias_attribute :v, :lock_versionの場合、updatesには{ "v" => 10 }だけが入っているupdates.key?("lock_version")もupdates.key?(:lock_version)もfalse- 「ロックカラムが指定されていない」と誤判定され、自動インクリメントが追加される
その後 _substitute_values が、v も追加された lock_version も両方とも物理カラム lock_version にマッピングして SQL を生成するため、次のような二重代入になる:
UPDATE "widgets"
SET "lock_version" = 10,
"lock_version" = COALESCE("widgets"."lock_version", 0) + 1
WHERE ...DBごとの挙動:
- SQLite/MySQL: 同一カラムへの複数代入では「最後の値が勝つ」ため、自動インクリメント側が適用され、ユーザーの指定値は捨てられる
- PostgreSQL: 同一カラムへの複数代入をエラーとして扱うため、
PG::SyntaxErrorが発生
修正内容
更新キーごとに model.attribute_alias を通し、エイリアスを解決した上でロックカラムかどうかを判定するように変更:
変更後のロジック(要旨):
if model.locking_enabled? &&
updates.keys.none? { |key|
(model.attribute_alias(key) || key.to_s) == model.locking_column
}
attr = table[model.locking_column]
updates[attr.name] = _increment_attribute(attr)
endポイント:
updatesの各キーkeyに対して:model.attribute_alias(key)でエイリアスを解決し(例:"v"→"lock_version")- 解決結果が
nilならkey.to_sをそのまま使う - それが
model.locking_column(通常"lock_version")と一致するかを調べる
- 1つでもロックカラムにマッピングされるキーがあれば、自動インクリメントは行わない
- 何もマッピングされなければ、従来通りロックカラムの自動インクリメントを追加する
これにより:
class Person < ActiveRecord::Base
alias_attribute :aliased_lock_version, :lock_version
end
Person.where(id: 1).update_all(aliased_lock_version: 10)
# => lock_version が 10 に更新され、自動インクリメントは付かないという期待通りの動作になります。
テスト追加
activerecord/test/cases/locking_test.rb に以下のようなテストが追加されています:
OptimisticLockingTest#test_update_all_with_lock_column_set_through_an_aliaspeopleテーブルに対するモデルでalias_attribute :aliased_lock_version, :lock_versionを定義update_all(aliased_lock_version: 10)実行後に:- 影響行数が 1 であること
lock_versionが 10 になっていること
このテストが、sqlite3 / postgresql / mysql2 すべてで「現行(main)では失敗 → PR適用で成功」することが確認されています。
- 影響範囲・注意点
- 影響対象:
- 楽観ロック(
lock_versionなど)を有効化しているモデル - かつ、そのロックカラムに
alias_attributeを定義しているケース - かつ、
update_allを使ってエイリアス名でロックカラムを更新しているケース
- 楽観ロック(
- 上記の条件に当てはまる場合:
- これまで SQLite/MySQL では「気づかないうちにロックカラムが自動インクリメントされ、指定した値は無視される」というバグがあった
- PostgreSQL では
update_all自体がエラーで落ちていた - 本PRにより、エイリアス名で指定してもカラム名で指定した場合と同じ挙動になる
注意点:
- ロックカラムを 一切指定していない
update_allの挙動(暗黙の自動インクリメント)は変わりません。 - 既存コードで、
update_allに渡すハッシュのキーにエイリアス名と実カラム名の両方を同時に指定しているような異常ケースがあった場合、これまでとは内部での扱いが変わる可能性があります(ただし、そもそもそのような指定は意味的に破綻しているので、実用上問題になることはほぼないはずです)。 insert_allでは既に同様の「エイリアス解決」が行われており、本PRはupdate_allをそれに揃える方向の変更です。
- 参考情報 (あれば)
- 該当PR:
rails/rails#57606「Resolve attribute aliases for the locking column inupdate_all」 - 関連API:
- ActiveRecord::Base#alias_attribute
- ActiveRecord::Relation#update_all
- 楽観ロック:
locking_enabled?,locking_column,lock_versionカラム
- 変更ファイル:
activerecord/lib/active_record/relation.rb
→ ロックカラムの自動インクリメント条件の判定ロジックを修正activerecord/test/cases/locking_test.rb
→ エイリアス経由でのロックカラム更新テストを追加activerecord/CHANGELOG.md
→ バグ修正として追記
#57617 Fix accepts_nested_attributes_for docs about _destroy and new records [ci skip]
マージ日: 2026/6/9 | 作成者: @55728
- 概要 (1-2文で)
accepts_nested_attributes_forのドキュメントにおいて、「新規レコードかつ_destroyが真なら無視される」という説明が、allow_destroy: trueが有効な場合にしか成り立たないにもかかわらず、無条件にそうであるかのように書かれていた問題を修正した PR です。コードの挙動は従来どおりで、ドキュメントだけを実装に合わせて訂正しています。
- 変更内容の詳細
問題となっていた点
accepts_nested_attributes_for でネストした属性を受け取るとき、内部では以下のようなロジックで「新規レコードを無視するかどうか」を判定しています:
reject_new_record?(association_name, attributes)
# => will_be_destroyed?(...) || call_reject_if(...)
will_be_destroyed?(association_name, attributes)
# => allow_destroy?(association_name) && has_destroy_flag?(attributes)つまり「_destroy が truthy だから新規レコードを無視する」ためには、
- その関連に対して
allow_destroy: trueが設定されていること
が前提です。allow_destroy: false(デフォルト)の場合は _destroy が true でもレコードは 無視されず、普通に build される というのが実装上の正しい挙動です。
この振る舞いは CVE-2015-7577 対応で意図的に導入されたものであり、テスト (test_reject_if_is_not_short_circuited_if_allow_destroy_is_false) により既にカバーされています。
ところがドキュメントでは、次の2点で「allow_destroy 前提」を書き漏らしていたため、挙動を誤解させる内容になっていました。
1) One-to-many の例
ドキュメントの一対多の例で、次のようなコードが載っていました(要点のみ):
class Member < ApplicationRecord
has_many :posts
accepts_nested_attributes_for :posts
end
member = Member.create(
name: 'joe',
posts_attributes: [
{ title: 'a' },
{ title: 'b' },
{ title: '', _destroy: '1' } # this will be ignored
]
)
member.posts.length # => 2ここでは accepts_nested_attributes_for :posts にオプション指定がないため、allow_destroy はデフォルトの false です。
その場合、実装上は _destroy: '1' が付いていてもその新規レコードは 無視されずに作成される ため、
member.posts.lengthは 3 になる- コメント
# this will be ignoredは誤り
となり、ドキュメントの例と実際の挙動が食い違っていました。
2) :reject_if オプションの説明
:reject_if の説明文において、オプションを指定しない場合のデフォルト挙動として、概ね次のような説明がありました(要旨):
:reject_ifを指定しない場合は、- "
_destroyが true と評価される値を持つ属性ハッシュを除いて、新しいレコードが作成される"
- "
しかし正確には、
- 「
_destroyが truthy であってもallow_destroy: falseのときは新規レコードはそのまま build される」
ため、_destroy による無視は「allow_destroy: true の場合に限る」という前提条件が抜けていました。
この PR での修正内容
One-to-many の例を実装に合うように修正
問題の例に対し、
allow_destroy: trueを明示的に付けることで、ドキュメント上の説明どおりの挙動になるようにしています:rubyclass Member < ApplicationRecord has_many :posts accepts_nested_attributes_for :posts, allow_destroy: true end member = Member.create( name: 'joe', posts_attributes: [ { title: 'a' }, { title: 'b' }, { title: '', _destroy: '1' } # this will be ignored ] ) member.posts.length # => 2これにより、
_destroy: '1'がついたハッシュはallow_destroy: trueにより無視され、postsは 2件になる、という説明が正しくなります。:reject_ifの説明文にallow_destroy前提を追記:reject_ifのセクションなど、_destroyの有無でレコードが無視される旨の説明をしている箇所に対して、- "
_destroyが truthy なら無視される" → 「allow_destroy: trueかつ_destroyが truthy なら無視される」
という形で、
allow_destroyが有効な場合にのみ成り立つことを明示するよう文言を修正しています。- "
コード変更はなし (ドキュメントのみ)
変更ファイルは
activerecord/lib/active_record/nested_attributes.rbのコメント/ドキュメント部分のみであり、ロジックには一切手を加えていません。[ci skip]が付いているのも、テストが不要なドキュメント変更であることを示しています。
- 影響範囲・注意点
実行時の挙動は変わりません
CVE-2015-7577 対応以降の挙動(allow_destroy: falseでは_destroy付きでも新規レコードは build される)はそのままです。今回の PR は、その挙動にドキュメントを揃えたものです。既存コードでの
_destroyの扱いを再確認すべきケース- ドキュメントだけを見て「
_destroyを付ければ新規レコードは作られない」と思い込み、allow_destroy: trueを付けていないフォーム/API 実装がある場合、- 実際には新規レコードが作成されている可能性があります。
- 特に
fields_for+accepts_nested_attributes_forを用いたフォームで、「空のフィールドに_destroyを付ければスキップされるはず」と考えていた場合は注意が必要です。 - この PR 自体は仕様を変えていませんが、「どちらが正しいのか」をドキュメントが明確にしたことで、誤った前提だったコードに気付くきっかけとなるかもしれません。
- ドキュメントだけを見て「
新規実装時のポイント
「フォームでユーザーが『削除』を押した行は保存しない」など、
_destroyで新規レコードを抑制したい場合、- 対応する関連に 必ず
allow_destroy: trueを付ける 必要があります。
- 対応する関連に 必ず
それとは別に、「空のフィールドは新規レコードを作りたくない」といった要件は、
reject_if:で明示的に制御するのが安全です:rubyaccepts_nested_attributes_for :posts, allow_destroy: true, reject_if: proc { |attrs| attrs['title'].blank? && attrs['_destroy'] != '1' }
- 参考情報 (あれば)
関連する既存テスト:
test_reject_if_is_not_short_circuited_if_allow_destroy_is_false
(allow_destroy: falseのときは_destroyが truthy でもreject_if判定がショートサーキットされず、新規レコードが build されることを検証)セキュリティ・バグフィックスの背景:
- CVE-2015-7577(nested attributes の
destroy/reject_if周りの挙動に関する脆弱性)対応時に、allow_destroyが偽の場合に_destroyを尊重しないという仕様が導入されています。 - 本 PR は、その仕様に合わせてドキュメントを修正し、「
_destroyが効くのはallow_destroy: trueのときだけ」という前提を明示したものです。
- CVE-2015-7577(nested attributes の
#57600 Fix store_accessor read mutating a NULL structured column
マージ日: 2026/6/9 | 作成者: @55728
- 概要 (1-2文で)
store_accessorを、NULLのjson/jsonb/hstoreカラムに対して「読むだけ」で呼び出したときに、レコードが変更済み扱いになりNULLが{}で上書き保存されてしまうバグを修正する PR です。- 修正により、「読み取り専用の操作」がレコードを汚染(dirty 化)したり、DB 上の
NULLを暗黙的に{}に書き換えることがなくなります。
- 変更内容の詳細
問題の挙動
以下のようなモデル定義で、data が DB 上で NULL のとき:
class Doc < ActiveRecord::Base
store_accessor :data, :color
end
doc = Doc.create!(name: "a") # data は NULL のまま
doc = Doc.find(doc.id)
doc.changed? # => false
doc.color # 読み出しのみ
doc.changed? # => true (本来は false であるべき)
doc.changes # => {"data" => [nil, {}]} (NULL が {} になった扱い)この状態で、name だけ変えて保存すると:
doc.name = "b"
doc.save!
Doc.connection.select_value("SELECT data FROM docs WHERE id = #{doc.id}")
# => "{}" (DB 上の data が NULL から {} に書き換えられてしまう)つまり、
- 「アクセサを読むだけ」で
- レコードが
changed? == trueになる dataカラムの DB 値がNULL→"{}"に変化する
- レコードが
という副作用が発生していました。
原因
store_accessor の実装で使われている HashAccessor.read が、内部で prepare を呼んでおり、その prepare が「nil のときに {} を生成して、レコードに書き戻す」という処理を行っていたためです。
def self.prepare(object, attribute)
store_object = object.public_send(attribute)
if store_object.nil?
store_object = {}
object.public_send(:"#{attribute}=", store_object) # ← 読み取り時にも書き戻していた
end
store_object
end結果として、「読むだけ」のはずの color アクセサが data カラムを {} に更新し、ActiveRecord の dirty tracking 上も data が変わったとみなされていました。
なお、coder: を使う古い store(シリアライズあり)の場合は、型キャスト層 (IndifferentCoder.load) で NULL → {} 変換が行われるため、そもそも nil にならず、この問題には該当しません。
修正方針
- 「読み取りパス」と「書き込みパス」を分離し、読み取り時にはレコードを書き換えないようにする。
- 書き込み時はこれまで通り
prepareを通し、NULLのカラムに初めて値を書くときは{}を生成して保存できるようにする。
HashAccessor.read の変更
以前は read が prepare を経由していたのを、非破壊な get ベースの実装に変更:
# 変更後: HashAccessor
def self.read(object, attribute, key)
get(object.public_send(attribute), key)
endget は、すでに存在しているハッシュからキーを読むだけで、nil のときも何も書き戻しません。
IndifferentHashAccessor の対応
store_accessor は hash に対して「シンボル/文字列どちらのキーでもアクセスできる」(indifferent access)ことを保証したいので、IndifferentHashAccessor にも非破壊な get を追加し、read でこれを使うようにします:
# IndifferentHashAccessor
def self.get(store_object, key)
if store_object
IndifferentCoder.as_indifferent_hash(store_object)[key]
end
endこれにより、
- 読み取り時:
IndifferentCoder.as_indifferent_hashで一時的に indifferent hash として扱い、その場で値を取得するだけ- レコードの属性(DB にマッピングされるハッシュ)は書き換えない
- 書き込み時:
- 既存の
prepare経由の動作を維持し、nilの場合は{}を生成して書き込む
- 既存の
という振る舞いになります。
テスト
Admin::User の既存の json カラム json_options と store_accessor :json_options, :enable_friend_requests を使い、以下を確認するテストを追加:
json_optionsがNULLの状態でenable_friend_requestsを読むだけではchanged?がfalseのままであること- 別の属性だけを変更して保存しても、
json_optionsがNULLのままであること({}に書き換えられないこと)
MariaDB 向けには、既存の JSON 関連テストと同じ skip 条件を利用。
SQLite3 / PostgreSQL / MySQL2 すべてで、このテストが fail → pass になることを確認済みで、store_test など周辺テストもグリーンであることが確認されています。
- 影響範囲・注意点
- 対象:
- ネイティブな構造化型 (
json/jsonb/hstore等) を持つカラムに対して、store_accessorを定義しているケース。 - DB 上の値が
NULLで、かつ accessor を読み出しているコードがある場合。
- ネイティブな構造化型 (
- 変わる挙動:
- これまで:
doc.colorのような読み取りだけでdoc.changed?がtrueになり、次のsaveでNULL→{}に上書きされていた。
- これから:
- 読み取りだけでは dirty にならず、
NULLも維持される。
- 読み取りだけでは dirty にならず、
- これまで:
- 想定される「副作用」(ほとんどは期待される挙動への修正ですが、挙動変化として注意すべき点):
- 「アクセサの読み取りがカラムを
{}に初期化してほしい」という前提のコードがあった場合、その前提は成り立たなくなります。- 例:
doc.colorを読むだけでdataが必ず{}以上になっていることを期待して、その後doc.data[:foo] = barのように直接操作していた場合、dataがnilのままになりNoMethodErrorになる可能性があります。 - そのような場合は、「読み取り」ではなく「書き込み」操作(例:
doc.color ||= '...'やdoc.json_options ||= {}; ...)で初期化するようにコード側を修正すべきです。
- 例:
changed?やchangesに依存したロジック(save if changed?、after_save if saved_change_to_data?等)が、本来の意図どおり「本当に変更があったときだけ」動くようになります。- 以前は「読むだけで changed になる」ことを前提にしたコードが動作していた場合、そのコードは発火しなくなる可能性がありますが、それ自体が元々バグ依存のロジックと考えられます。
- 「アクセサの読み取りがカラムを
互換性面では、「バグ修正として妥当な挙動変更」であり、通常のアプリケーションコードにとっては改善とみなしてよい内容です。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57600
- 関連するドキュメント記述(今回のバグが特に当たっていたパターン):
- 「ネイティブ JSON / hstore カラムには
storeではなくstore_accessorを使うべき」というガイドラインに従った場合に、このバグに直撃していました。
- 「ネイティブ JSON / hstore カラムには
- 実装的なポイント:
- 「読み取りパスでモデル属性を書き換えない」というのは、ActiveRecord の dirty tracking としても自然な設計なので、今後この方針に沿った変更が増える可能性があります。
#57622 Fix Range#include? / Range#=== raising on exclusive non-integer sub-ranges
マージ日: 2026/6/9 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport が拡張しているRange#include?/Range#===の「レンジ同士の包含チェック」で、排他的終端かつ非整数 (Float,Timeなど) のサブレンジを渡すとTypeErrorで落ちていた問題を修正した PR です。Ruby 本体のRange#cover?(range)に処理を委譲することで、すべての型・端点組み合わせで正しく真偽値を返すようにしています。
- 変更内容の詳細
問題となっていた実装
ActiveSupport では、Range#include? / Range#=== に「引数が Range のときは『完全に含まれるか』を判定する」という拡張が入っていました。その実装の一部は以下のように、引数側 Range の最大要素を独自に計算していました:
# activesupport/lib/active_support/core_ext/range/compare_range.rb (旧)
value_max = !exclude_end? && value.exclude_end? ? value.max : value.lastここで value は引数として渡された Range(サブレンジ)です。
value.exclude_end?が真(排他的終端)で、- かつ「自分(self)の Range が終端を含む」場合に、
value.maxを呼んでサブレンジの最大要素を求める、
というロジックになっていました。
ところが Ruby の Range#max は「範囲を列挙して最大値を返す」ため、
- 終端が
Integer以外の排他的 Range(例:1.0...5.0,t...(t + 10.minutes))では - 「非整数の排他的終端は列挙できない」「Time からは順次イテレートできない」
といった理由で TypeError を投げます。
例:
(1.0..10.0).include?(2.0...5.0)
# => TypeError: cannot exclude non Integer end value
t = Time.now
(t..(t + 1.hour)).include?(t...(t + 10.minutes))
# => TypeError: can't iterate from Timeつまり「レンジがレンジを含むか?」という単なる包含チェックが、
FloatTimeDateTimeActiveSupport::TimeWithZoneBigDecimal
などで排他的終端を使った時にクラッシュしてしまう状態でした。
なお value.max を使っていた目的は「離散な整数レンジの排他的終端をうまく解釈する」ためで、典型的には次のようなケースを true にしたい、という用途です:
(1..10).include?(1...11) # => true にしたいしかし整数以外の型ではこのロジックが破綻しており、今回のバグにつながっていました。
修正方針
Ruby 2.6 以降では Range#cover? が Range を引数に取れるようになっており、「この Range が引数 Range を完全に含むか」を正しく判定してくれます。Rails のサポート Ruby バージョンは 2.6 より新しいため、ActiveSupport 独自の hand-rolled 実装をやめて、Ruby 本体の Range#cover? に委譲する実装に差し替えています。
新しい実装イメージ:
def ===(value)
if value.is_a?(::Range)
cover?(value)
else
super
end
end
def include?(value)
if value.is_a?(::Range)
cover?(value)
else
super
end
endポイント:
value.is_a?(::Range)のときだけcover?(value)を使う- それ以外(スカラー値: 数値, 文字列, Time など)の場合は
superで Ruby 標準のRange#include?/Range#===にそのまま委譲
→ 既存の('a'..'f').include?('c')等の挙動は一切変わらない
Range#cover? は
- 両端 inclusive / どちらか exclusive / 両方 exclusive
- 逆向きレンジ (e.g.
5..3) - 空の exclusive レンジ (e.g.
3...3) - endless / beginless レンジ
- レンジ同士が部分的にしか重ならないケース
など、元々 ActiveSupport がハンドリングしていた多くのパターンを含め、Ruby 本体の定義に沿って一貫した結果を返すようになっています。
修正後の挙動例
TypeError が発生していたケース:
(1.0..10.0).include?(2.0...5.0)
# 旧: TypeError
# 新: true
(1.0..10.0).include?(2.0...10.5)
# 旧: TypeError
# 新: false
t = Time.utc(2005, 12, 10, 15, 30)
(t..(t + 2.hours)).include?((t + 30.minutes)...(t + 90.minutes))
# 旧: TypeError
# 新: true従来既にテストされていたパターン(挙動は維持されている):
- 同じ端点・同じ inclusive/exclusive の Range 同士
- 排他的終端整数レンジ (
1..10vs1...11) - 逆向きレンジ (
5..3) - 空レンジ (
3...3) - endless/beginless レンジ
- オーバーラップのみで完全包含していないケース
などは、Range#cover? でも同じ真偽値が返ることがテストで確認されています。
- 影響範囲・注意点
影響範囲
- 対象:
- ActiveSupport をロードした環境で、
Range#include?/Range#===に Range を引数として渡しているコード 全般
とくに影響が大きいのは次のようなユースケースです:
- 時間窓を Range で表現し、その Range に別の時間 Range(サブウィンドウやスロット)を含められるかをチェックしているコード
- 例:
Time/TimeWithZone/DateTimeの Range
- 例:
- 許容誤差帯やしきい値を
Float/BigDecimalの Range で扱っているコード - そのサブレンジとして排他的終端 (
...) を使っているコード
これらがこれまで TypeError で落ちていた場合でも、今後は true / false のどちらかを返すようになります。
注意点
例外から「単純な真偽値」に変わる
- 今まで
TypeErrorを前提に rescue していた場合、その rescue 経路は通らなくなります。 - ロジックとして「例外が出たら false 扱い」としていたようなコードは、
include?の戻り値だけで判定できるようになる一方で、挙動が変わることに注意が必要です。
- 今まで
挙動は Ruby 本体の
Range#cover?(range)に一致- ActiveSupport 独自実装から Ruby 本体の仕様に寄せたため、「以前のバグを利用していた」ようなコードがあればその挙動は変わります。
- ただし PR 説明によると、既存のテストケースの全ては
Range#cover?でも同一結果が出ているため、実質的には「これまで落ちていたパターンが落ちなくなる」以外の挙動変更はほぼないと見てよいです。
スカラー引数 (
range.include?(scalar)) の挙動は不変- 引数が Range でない場合は従来どおり
super呼び出しなので、ここに後方互換性の問題はありません。
- 引数が Range でない場合は従来どおり
- 参考情報 (あれば)
- Ruby 本体の
Range#cover?ドキュメント- 範囲が値を「カバーするかどうか」を比較演算なしで判定するメソッドとして定義されており、Ruby 2.6 以降は引数に Range も受け付けます。
- この PR で追加されたテスト (
activesupport/test/core_ext/range_ext_test.rb)test_should_include_exclusive_end_float_rangetest_should_not_include_exclusive_end_float_range_past_endtest_should_include_exclusive_end_time_range
→ バグ再現ケースとその修正後の期待挙動が明示されているので、同様のユースケースを持つアプリケーションでの挙動確認の参考になります。
#57614 Allow insert! to accept the :unique_by option
マージ日: 2026/6/9 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Relation#insert!で、これまで渡せなかったunique_by:オプションを受け付けるようにし、insert/insert_all!とインターフェースを揃えた修正です。これにより、insert!でも一意制約を考慮した upsert 風の挙動が利用可能になります。
- 変更内容の詳細
何が問題だったか
もともと以下のような状況でした:
insert_all!はunique_by:オプションを受け取れるinsertもunique_by:を受け取れる- しかし
insert!のメソッド定義にunique_by:が含まれておらず、呼び出し側でunique_by:を指定するとArgumentErrorになる
# 以前の定義イメージ
def insert!(attributes, returning: nil, record_timestamps: nil)
insert_all!([ attributes ], returning: returning, record_timestamps: record_timestamps)
end
Book.insert({ id: 1, name: "X", author_id: 1 }, unique_by: :index_books_on_isbn) # OK
Book.insert!({ id: 2, name: "X", author_id: 1 }, unique_by: :index_books_on_isbn) # ArgumentErrorドキュメント上は insert! も insert_all! と同じオプションが使えるように読めるのに、実装が追いついていなかった状態です。
何を修正したか
insert! のシグネチャを修正し、unique_by: を受け取り insert_all! にそのまま渡すようにしました。
イメージとしては以下のような変更です(実際のコードは Relation クラス内にあります):
# 修正後のイメージ
def insert!(attributes, returning: nil, record_timestamps: nil, unique_by: nil)
insert_all!(
[attributes],
returning: returning,
record_timestamps: record_timestamps,
unique_by: unique_by
)
endこれにより、以下のようなコードが問題なく動作するようになります:
# 一意制約付き index_books_on_isbn を衝突検知に使う例
Book.insert!(
{ id: 2, name: "X", author_id: 1, isbn: "978-..." },
unique_by: :index_books_on_isbn
)テストの追加
activerecord/test/cases/insert_all_test.rb に以下のようなテストが追加されています:
test_insert_bang_accepts_unique_by- すでに存在する
test_insert_all_bang_accepts_unique_byに対応するテスト supports_insert_conflict_target?を満たすアダプタ(PostgreSQL / SQLite 等)でのみ実行- シグネチャの修正を戻すと
ArgumentError: unknown keyword: :unique_byで落ちることを確認済み
- すでに存在する
ドキュメント / CHANGELOG
activerecord/CHANGELOG.mdにユーザー向けのエントリが追加され、insert!がunique_by:に対応したことが明示されています。
- 影響範囲・注意点
- 影響クラス:
ActiveRecord::Relation#insert!を利用しているコード - 互換性:
unique_by:を新たに受け付けるだけなので、既存コード(unique_by:を渡していないもの)は基本的に非互換影響はありません。- Ruby のキーワード引数仕様上、
unique_by:を追加しても、追加前の引数呼び出し形式とは衝突しにくいため、後方互換性は高いです。
- DB依存:
unique_by:自体は、内部的にINSERT ... ON CONFLICT ...などの機構を使うため、DB アダプタの対応状況に依存します。- これまで
insert/insert_all!でunique_by:を使えていた環境であれば、そのままinsert!でも利用可能になります。
- 利用時の注意:
unique_by:には通常、ユニークインデックス / 一意制約 の名前、または列名の配列を渡します(PostgreSQL のON CONFLICTターゲットに対応)。- インデックス名やカラム指定が実際の DB スキーマと一致していないとエラーになるので、
insert!に限らずinsert_all!/upsert_allと同様の注意が必要です。
- 参考情報 (あれば)
- 関連メソッド:
ActiveRecord::Persistence::ClassMethods#insertActiveRecord::Persistence::ClassMethods#insert_all!- これらと
insert!のオプションが揃ったことで、バルクインサートと単一レコードインサートで同じ API を利用しやすくなりました。
unique_byの詳細な仕様は Rails ガイド / API ドキュメントの「insert_all / upsert_all」周りを参照すると理解しやすいです。
#57581 Support only: and except: on _deliver callbacks in ActionMailer
マージ日: 2026/6/9 | 作成者: @excid3
- 概要 (1-2文で)
Action Mailer の_deliverコールバック(before_deliver,after_deliver,around_deliver)で、only:/except:オプションが正しく機能するように修正・拡張されたPRです。これにより、コントローラの*_actionコールバックと同様の書き方で、特定のメールアクションに対してのみコールバックを適用できるようになります。
- 変更内容の詳細
何が問題だったか
以前追加された Action Mailer の
_deliver系コールバック(例:before_deliver)にonly:/except:オプションを渡しても、それらが無視されていました。- 例:ruby上記の
class UserMailer < ApplicationMailer before_deliver :log_delivery, only: :welcome_email endonly: :welcome_emailが効いていなかった。
- 例:
これは AbstractController(ActionController などで使われる)のコールバックとは挙動が異なり、一貫性がない状態でした。
何をしたか
AbstractController のコールバック (
*_actionコールバック) と同じ正規化ロジック(normalization wrapper)を_deliverコールバックにも適用するように変更。- 参照されている箇所:
abstract_controller/callbacks.rbのnormalize_callback_params相当の処理set_callback :process_action, ...で使われているオプションの解釈ロジック
- 参照されている箇所:
具体的には、Action Mailer のコールバック定義部分(
actionmailer/lib/action_mailer/callbacks.rb)で、only:/except:などを含むオプションを AbstractController と同じ形に正規化してからコールバックを登録するようにした。
実際にどう書けるようになるか(サンプル)
before_deliver, after_deliver, around_deliver で、以下のような指定が有効になります:
class UserMailer < ApplicationMailer
# 特定のメールアクションにだけ適用
before_deliver :log_welcome_delivery, only: :welcome_email
# 複数アクションに適用
after_deliver :track_marketing_emails, only: [:campaign_email, :newsletter_email]
# 特定のアクションだけ除外
around_deliver :with_timing, except: :system_notification
def welcome_email(user)
mail(to: user.email, subject: "Welcome!")
end
def campaign_email(user)
mail(to: user.email, subject: "Big sale!")
end
def system_notification(user)
mail(to: user.email, subject: "System notice")
end
private
def log_welcome_delivery(mail)
Rails.logger.info("[MAIL] welcome_email delivered to #{mail.to}")
end
def track_marketing_emails(mail)
# GA / Mixpanel などへの連携
end
def with_timing
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
yield
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
Rails.logger.info("[MAIL] delivery took #{elapsed}s")
end
end上記のような only: / except: が、コントローラの before_action などと同じ感覚で動作するようになります。
テスト・ドキュメント・CHANGELOG
actionmailer/test/callbacks_test.rbに 56 行のテストが追加され、only:/except:の動作が確認されています。actionmailer/test/mailers/callback_mailer.rbにテスト用の Mailer が追加され、各種パターンのコールバック適用が検証されています。actionmailer/CHANGELOG.mdにエントリが追加され、挙動変更が明記されています。- guides の
configuring.mdにも4行追加され、ドキュメント上も_deliverコールバックのオプション使用例/説明が更新されています。
- 影響範囲・注意点
影響を受けるのは
_deliverコールバックのみbefore_deliver,after_deliver,around_deliverでonly:/except:を指定している(もしくは今後指定する)コードが対象です。- それ以外の Action Mailer の API には影響しません。
既存コードの互換性
- これまで
only:/except:は“無視されていただけ”なので、多くの場合は新たに意図したとおりにコールバックが発火するようになるだけです。 - ただし、これまで「全アクションにコールバックが走る前提」でワークアラウンドしていた場合には挙動が変わる可能性があります。
- 例: 「
only:が効かない想定で内部条件分岐を書いていた」等。
- 例: 「
- これまで
コントローラとの一貫性が向上
before_action/after_action/around_actionとほぼ同じ感覚で_deliverコールバックを使えるようになるため、学習コスト/驚きは減ります。- 特に
only: [:action1, :action2]やexcept: :some_actionのような典型的な書き方が Controller と Mailer で揃うのはメリットです。
バージョン依存
- この PR は main ブランチ(将来の Rails バージョン)向けであり、現時点でリリース済みの Rails にはまだ含まれていない可能性があります。
- 実際に利用する場合は、使っている Rails バージョンの CHANGELOG / リリースノートで、この PR (#57581) が取り込まれているか確認してください。
- 参考情報 (あれば)
- PR本体: https://github.com/rails/rails/pull/57581
- 関連Issue: https://github.com/rails/rails/issues/50830
_deliverコールバック追加時の PR(背景): https://github.com/rails/rails/pull/47630- 対応する AbstractController の実装(参考):
#57595 Forward blob metadata to MirrorService mirrors
マージ日: 2026/6/9 | 作成者: @maksim-romanov
- 概要 (1-2文で)
Active Storage の MirrorService がミラー先ストレージにアップロードするとき、content_typeやfilenameなどの blob メタデータを正しく引き継ぐように修正した PR です。これにより、S3 / R2 / Azure / GCS などのミラー先を直接参照した場合でも、オリジナルと同じヘッダでオブジェクトが配信されるようになります。
- 変更内容の詳細
バグの内容
ActiveStorage::Service::MirrorService#mirror は、非同期ジョブ (MirrorJob) から呼ばれ、プライマリサービスに保存された blob を複数のミラーに複製する役割を持っています。
従来の実装では、ミラーに対して以下のように checksum のみを渡して upload を呼んでいました:
mirror.upload(key, io, checksum: checksum)その結果、ミラー側のオブジェクトには以下のメタデータが反映されていませんでした:
content_typefilenamedispositioncustom_metadata
クラウドストレージ (S3 / Cloudflare R2 / Azure / GCS) では、これらはアップロード時ヘッダとして保存されるため、欠落すると:
- ブラウザから直接バケット URL にアクセスしたとき
- CDN カスタムドメイン経由でオリジンを直接叩くとき
<img src="https://bucket.example.com/path/to/object">のように直接参照するとき- 動画などで Range Request を直接ストレージに投げるとき
に Content-Type: application/octet-stream になってしまい、画像や動画として正しく扱われない・プレビューされない、などの問題が発生していました。rails_blob_url 経由のアクセスでは Rails が署名付き URL に response-content-type を付けて上書きするため、この問題は表面化しません。
修正の要点
この PR では、ミラーリング時にもプライマリサービスと同等のメタデータを渡すようにしています。
1. mirror 内で blob をキーから取得
ActiveStorage::Service::MirrorService#mirror の中で、アップロード対象の blob をキー (key) から検索します。
疑似コードイメージ:
def mirror(key, checksum:)
# 追加: key から blob を引く
blob = ActiveStorage::Blob.find_by(key: key)
service_metadata = blob&.service_metadata || {}
@mirrors.each do |mirror|
mirror.upload(key, io, checksum: checksum, **service_metadata)
end
end実際には、service_metadata というメソッドを通して、content_type・filename・disposition・custom_metadata など、サービス向けのメタデータ一式を取得してミラーに渡します。
2. Blob#service_metadata を public に昇格
これまでは ActiveStorage::Blob#service_metadata は private メソッドでしたが、以下のような他の内部 API (unfurl, upload_without_unfurling, compose, mirror_later) と同じく、クラス間の内部利用が想定されているため:
- 可視性を
publicに変更 - ただし public API として外部利用を推奨するわけではないので
# :nodoc:を付け、ドキュメントには出さない
という形で公開範囲を整理しています。
これにより、MirrorService から blob.send(:service_metadata) のようなメタプロ的な呼び出しをせずに、普通に blob.service_metadata を呼べるようにした、という設計上の改善も含まれています。
3. Blob が見つからない場合のフォールバック
ActiveStorage::Blob のレコードが既に削除されていて、しかしストレージ上にはオブジェクトが残っている「orphan key」のケースを考慮し、find_by が nil を返した場合は:
service_metadata = {}と空ハッシュを渡すようにしています。
これにより、少なくとも従来の挙動 (メタデータ無しでアップロード) と整合的な動作を維持しつつ、レコードが存在する正常ケースではフルメタデータを渡す、という挙動になります。
4. テストと CHANGELOG の追加
activestorage/test/service/mirror_service_test.rbに MirrorService が blob の service_metadata をミラーに引き渡していることを検証するテストを追加。activestorage/CHANGELOG.mdに、この挙動変更 (バグ修正) に関する記述を追加。
- 影響範囲・注意点
影響を受けるサービス
- 影響あり:
ActiveStorage::Service::MirrorServiceを利用している構成- ミラー先として S3 / Cloudflare R2 / Azure / GCS など、アップロード時メタデータを保持するクラウドストレージ
- 影響なし:
- Disk サービス (ローカルストレージ)
→ もともと HTTP ヘッダをストレージ側に保存しないため、今回の修正対象外
- Disk サービス (ローカルストレージ)
実務上の影響
- 今後作成されるミラーオブジェクトは、オリジナルと同じ
Content-Type/Content-Disposition/ カスタムメタデータで保存される - そのため:
- CDN カスタムドメインでバケットを直接 origin にしている構成
<img src="...">/<video src="...">などでストレージ URL を直接参照しているケース- プレサインド URL ではなく、ストレージの公開 URL をそのまま使うケース
で、正しい MIME Type などが反映されるようになる
既に壊れたミラーオブジェクト (過去にメタデータ抜きでアップロードされたもの) については、この PR を適用しても自動で修復されるわけではありません。必要であれば:
- 対象オブジェクトを再ミラーリングする
- あるいは、ミラー先のオブジェクトを削除し、Active Storage 側から再アップロードをトリガーする
などの対応が別途必要になります。
互換性面の注意
Blob#service_metadataがpublicになりましたが、# :nodoc:が付いているため、Rails の内部 API という扱いです。
アプリケーションコードからの直接利用は可能ではあるものの、今後の互換性は保証されていない点に注意してください。- 既存の MirrorService の API 形状 (public メソッドシグネチャなど) は変わっていないため、Rails アプリ側のコードを書き換える必要はありません。
- 参考情報 (あれば)
- この PR が修正する Issue:
#57270
(R2 + カスタムドメイン CDN でのContent-Type欠落問題が報告されている) - 代替案 PR:
#57276- 同じバグを解決する別実装
- この PR との主な違い:
Blob#service_metadataを public にする代わりにsendで呼ぶアプローチ- 本 PR は Rails 内部 API の一貫性 (他の内部メソッドと同様に public +
:nodoc:) を重視し、その設計に揃えている
- 関連する内部 API:
ActiveStorage::Blob#unfurlActiveStorage::Blob#upload_without_unfurlingActiveStorage::Blob#composeActiveStorage::Blob#mirror_later
これらと同列に service_metadata を「内部向け public」として露出させることで、MirrorService からの利用を素直に書けるようにした、という位置付けの変更です。
#53161 Add bundler-cache feature in dev container setup
マージ日: 2026/6/9 | 作成者: @viktorianer
- 概要 (1-2文で)
Dev Container で Rails 開発を行う際に、Bundler を含む Ruby 環境(.rbenvディレクトリ)をボリュームとしてキャッシュする仕組みが追加され、コンテナ再作成時の gem 再インストールコストを削減する変更です。Rails の devcontainer 用ジェネレータとそのテストが、この新しいキャッシュ戦略に合わせて更新されています。
- 変更内容の詳細
2-1. Dev Container 設定へのボリューム追加
railties/lib/rails/generators/rails/devcontainer/templates/devcontainer/compose.yaml.tt に、.rbenv ディレクトリをホスト側ボリュームとしてマウントする設定が追加されています。
イメージとしては、以下のような変更が入っていると考えられます(概念的なサンプル):
services:
app:
volumes:
# 既存のマウント設定に加えて…
- rails-rbenv:/home/vscode/.rbenv
volumes:
rails-rbenv:ポイント:
.rbenv全体をボリュームとしてキャッシュすることで、Ruby バージョンや gem(Bundler でインストールされたものを含む)の再インストールを避ける。- 以前の PR (#53123) で提案されていた
ghcr.io/rails/devcontainer/features/bundler-cacheではなく、標準的な Docker ボリュームの仕組みだけで実現している。
2-2. テストコードの更新
以下のテストが更新されています:
railties/test/commands/devcontainer_test.rbrailties/test/generators/app_generator_test.rbrailties/test/generators/devcontainer_generator_test.rb
主な変更点:
devcontainerコマンドやジェネレータが生成するcompose.yamlに、.rbenvボリュームの定義・マウントが含まれていることを検証するアサーションが追加。- 既存テストの期待値(生成される compose 設定)を、新しいボリューム定義に合わせて修正。
これにより、今後 Rails プロジェクトで --devcontainer オプション付きで生成される設定や bin/rails devcontainer 相当のコマンド実行結果が、常に .rbenv キャッシュを含むことが保証されます。
- 影響範囲・注意点
影響範囲
- Rails の devcontainer テンプレートから生成される Docker Compose 設定に、
.rbenv用のボリュームが追加されます。 - これにより、Dev Container を再作成しても gem の再インストールが基本的に不要になり、起動が高速化されます。
- Dev Container を前提とした Rails アプリのテンプレート生成 (
rails new myapp --devcontainerなど) に影響します。
- Rails の devcontainer テンプレートから生成される Docker Compose 設定に、
注意点
.rbenvディレクトリ全体がキャッシュ対象になるため、Ruby バージョンや gem セットアップを大きく変えた場合は、ボリュームを再作成(削除して作り直す)しないと古い状態が残る可能性があります。- すでに独自に
.rbenvなどをマウントしている devcontainer 設定がある場合は、生成物とコンフリクトしないか確認が必要です(ただし、この PR は Rails が生成するテンプレート側の変更のみ)。 - 特定の CI 環境や共有環境で
.rbenvキャッシュを利用する際は、「誰がそのボリュームを共有するのか」「パーミッションはどうか」を意識する必要があります。
- 参考情報 (あれば)
- 本 PR: https://github.com/rails/rails/pull/53161
- 関連 PR(feature/bundler-cache を使う案): https://github.com/rails/rails/pull/53123
- 提案者によるサンプル Rails アプリでの実例:
https://github.com/viktorianer/rails8-devcontainer-enhancements/pull/13
この PR により、Rails の公式 devcontainer サポートが、特別な feature への依存なしに「シンプルなボリュームキャッシュ戦略」で bundler 依存関係を高速化する方向へ整理されたと言えます。
#57574 Make the backtrace cleaner ractor shareable by default
マージ日: 2026/6/9 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails のバックトレースクリーナー(Rails.backtrace_cleaner/ActiveSupport::BacktraceCleaner)が、デフォルトで Ractor 共有可能になるように実装を変更した PRです。これにより、Rails.applicationを Ractor で安全に共有しやすくなります。
- 変更内容の詳細
背景
- 目標:
Rails.applicationをRactor.shareable?にしたい。 - 問題:
Rails.applicationの内部にはRails.backtrace_cleanerが含まれており、その中の「デフォルトのフィルタ・サイレンサー」が Ractor 共有不可能なProcになっていた。 - Ractor 共有可能な
Proc(Ractor.shareable_proc) ではselfがnilになる点があり、「ユーザが追加するフィルタ / サイレンサー」を一律で shareable にすると、selfに依存しているブロックが実行時までエラーにならず、危険な「踏み抜き (footgun)」になる。
この PR では「Rails が内部的に設定しているデフォルトのブロック」だけを Ractor 共有可能にし、ユーザ追加分は従来通りの挙動に留めています。
主な変更点
1) ActiveSupport::BacktraceCleaner のデフォルトフィルタ/サイレンサーを shareable 化
activesupport/lib/active_support/backtrace_cleaner.rb の変更:
- デフォルトで設定されるフィルタ・サイレンサーの
ProcをRactor.shareable_procでラップするように変更。 - これにより、
ActiveSupport::BacktraceCleaner.newで生成されるインスタンスの初期状態は Ractor 共有可能になります(※ただし、ユーザが後から非 shareable な Proc を追加した場合はその限りではない)。
イメージ的には:
# 変更前(概念的な例)
add_silencer { |line| line.include?("/gems/") }
# 変更後(概念的な例)
add_silencer Ractor.shareable_proc { |line| line.include?("/gems/") }※ 実際には、実装側で shareable にするようにしており、アプリコード側で Ractor.shareable_proc を明示的に呼ぶ必要はありません。
2) Rails.backtrace_cleaner の初期化ロジックの調整
railties/lib/rails/backtrace_cleaner.rb:
- Rails が提供する
Rails.backtrace_cleanerに設定している Rails 独自のサイレンサーやフィルタもRactor.shareable_proc化。 - Rails 側で追加している「Rails のノイズを消すためのルール」(
bin/,vendor/bundle/,railties等を消すようなもの)が Ractor からも安全に利用できるようになった。
結果として:
Rails.backtrace_cleaner # => Ractor から参照しても shareable なオブジェクトになる想定3) Ractor 用テストの追加
activesupport/lib/active_support/testing/ractors_assertions.rbに Ractor 関連のアサーションヘルパーを追加。activesupport/test/backtrace_cleaner_test.rbrailties/test/backtrace_cleaner_test.rb
上記テストで、
- デフォルトの
BacktraceCleanerが Ractor 内からも正常に使えること - Ractor 間で共有しても例外が出ないこと
などを検証しています。
- 影響範囲・注意点
影響範囲
- Rails 既定の挙動
デフォルトのバックトレースフィルタ/サイレンサーのロジック自体は変わっていないため、通常の(非 Ractor)使用における表示結果は従来と同じはずです。 - Ractor 利用時の改善
Rails.applicationやRails.backtrace_cleanerをそのまま Ractor に渡しやすくなり、Ractor ベースの並列実行でバックトレースクリーニング機能が使いやすくなります。
注意点
ユーザが追加するフィルタ/サイレンサーはデフォルトでは shareable にならない
rubycleaner = Rails.backtrace_cleaner # これは shareable ではない Proc(self を前提にしているかもしれない) cleaner.add_silencer do |line| some_helper(line) # self に依存したりする可能性がある endこういったユーザ定義ブロックは Ractor 共有可能には「自動では」なりません。その理由は PR 説明の通り:
Ractor.shareable_procにするとselfがnilになる。- ブロック内部で
selfに依存している場合、追加時には気づかず、実行時に NoMethodError になる。 - それを避けるため、フレームワーク側の都合でユーザブロックを強制的に shareable にはしていない。
もしユーザ側で Ractor 対応したい場合は、自分で shareable な Proc を意識して書く必要がある(例:
selfに依存しないロジックに分離する、Ractor.shareable_procを自前で使う、等)。将来的な API 拡張の示唆
PR 本文で触れられている通り:
- 「shareable を opt-in で指定できる API」を用意する
- Prism などでブロックの AST を解析し、
self参照がない場合のみ shareable にする
といった改善案はあるが、この PR 時点ではまだ導入されていません。
class macro 系のブロックは今後 shareable 化される予定
instance_execで評価するような「クラスマクロ的な DSL ブロック」は、デフォルトで shareable にしやすいので、別 PR でそちらを対応予定と述べられています。この PR はあくまで backtrace cleaner 周りだけ。
- 参考情報 (あれば)
- Ractor の仕様(特に shareable オブジェクト、
Ractor.shareable_procの挙動)Ractor.shareable?(obj)がtrueなら別 Ractor との共有が可能。Ractor.shareable_proc { ... }で生成した Proc は- 自身が shareable なクロージャ
- ただし実行時の
selfはnilになる
- 実運用で Ractor を使う場合:
Rails.applicationを Ractor に渡してバックグラウンド処理等を行うようなユースケースでは、この PR によって「バックトレース整形で落ちる」類の問題が減る。- 独自の backtrace フィルタ/サイレンサーを多用しているアプリは、Ractor 対応する際に「どこまで shareable にするか」を意識して実装・設計する必要があります。
#57593 Add unit tests for ActiveStorage::Variation
マージ日: 2026/6/9 | 作成者: @Edilbek
- 概要 (1-2文で)
ActiveStorage のコア値オブジェクトであるActiveStorage::Variationに対して、専用のユニットテストが追加された PR です。既存では統合テスト越しにしか検証されていなかった振る舞いを、変換キーの扱いやエンコード/デコードの不変条件レベルで直接テストするようにしています。
- 変更内容の詳細
追加ファイル
activestorage/test/models/variation_test.rb(新規・111行)
このテストファイルで、ActiveStorage::Variation のパブリック API が網羅的にテストされています。主な観点は以下です。
2-1. 初期化: オプションの deep-symbolize
「Initialization: string keys are deep-symbolized」
ActiveStorage::Variation.new(transformations) にハッシュを渡したとき、{ "resize_to_limit" => ["100x100"] } のような「文字列キー」が「シンボルキー」に正規化されることをテストしています。
想定されるテスト例イメージ(擬似コード):
variation = ActiveStorage::Variation.new("resize_to_limit" => [100, 100])
assert_equal({ resize_to_limit: [100, 100] }, variation.transformations)- ネストがあるケースでも deep-symbolize されること(例:
{"resize" => {"width" => 100}}→{resize: {width: 100}})が確認されていると考えられます。 - これにより、呼び出し側がキーを string/symbol どちらで渡しても、内部的には同一の扱いとなることが保証されます。
2-2. wrap の振る舞い
「wrap: returns an existing instance unchanged, builds from a Hash, decodes a signed key」
ActiveStorage::Variation.wrap の 3 パターンをテストしています。
既に
Variationインスタンスの場合- そのまま返す(オブジェクトを変換・再構築しない)こと。
rubyvariation = ActiveStorage::Variation.new(resize_to_limit: [100, 100]) assert_same variation, ActiveStorage::Variation.wrap(variation)ハッシュからの生成
{ resize_to_limit: [100, 100] }や{ "resize_to_limit" => [100, 100] }からVariationインスタンスを作る。
rubyvariation = ActiveStorage::Variation.wrap("resize_to_limit" => [100, 100]) assert_instance_of ActiveStorage::Variation, variation署名付きキー(エンコード済み文字列)からの復元
ActiveStorage::Variation#encodeで生成した署名付きキー文字列を渡すと、同等の変換をもつVariationを復元できること。
rubyencoded = ActiveStorage::Variation.new(resize_to_limit: [100, 100]).encode variation = ActiveStorage::Variation.wrap(encoded) assert_equal({ resize_to_limit: [100, 100] }, variation.transformations)
2-3. encode / decode / key のラウンドトリップ
「encode / decode / key: round-trip transformations and consistent signed keys」
#encode: 変換ハッシュを署名付きトークン文字列に変換.decode/.wrap: トークン文字列を再度Variationに復元#key: 署名されたトークン(encodeの中身と整合するキー)を返す
などの API がラウンドトリップ可能であること、かつ同一の変換からは常に同じキーが得られることがテストされています。
例(イメージ):
variation = ActiveStorage::Variation.new(resize_to_limit: [100, 100])
encoded = variation.encode
decoded = ActiveStorage::Variation.decode(encoded)
assert_equal variation.transformations, decoded.transformations
# 同じ変換なら key は常に同じ
assert_equal variation.key, decoded.key2-4. キー/ダイジェストのパリティ (string/symbol)
「Key and digest parity: symbol-keyed and string-keyed transformations produce identical key and digest values」
同じ意味の変換でも、引数ハッシュのキーが string か symbol かに依存して異なるキー/ダイジェストにならないことを確認しています。
例(イメージ):
symbol_variation = ActiveStorage::Variation.new(resize_to_limit: [100, 100])
string_variation = ActiveStorage::Variation.new("resize_to_limit" => [100, 100])
assert_equal symbol_variation.key, string_variation.key
# digest (内部で key 生成に使っている値) も同じであることをテストこれにより、呼び出し側の表記揺れがキャッシュキーや URL の変化を招かないことが保証されます。
2-5. default_to の挙動
「default_to: fills missing transformations without overriding existing ones」
Variation#default_to(default_transformations) は、
- 変換が存在しないキーに対してはデフォルト値を埋める
- 既に変換が指定されているキーについては上書きしない
という振る舞いをテストしています。
例(イメージ):
variation = ActiveStorage::Variation.new(resize_to_limit: [100, 100])
variation.default_to(format: :png, resize_to_limit: [200, 200])
# 既存の値は維持
assert_equal [100, 100], variation.transformations[:resize_to_limit]
# 無かったキーだけデフォルトが入る
assert_equal :png, variation.transformations[:format]これにより、アプリ側で「既に指定されている変換を尊重しつつ、足りない変換だけ補う」ような拡張が安全にできるかが担保されています。
2-6. format / content_type の扱い
「format / content_type: defaults to PNG, accepts valid extensions, raises ArgumentError for invalid formats」
主に以下をテストしています。
デフォルトは PNG
formatを指定しない場合、#formatや#content_typeはimage/pngを前提とする。
rubyvariation = ActiveStorage::Variation.new assert_equal "image/png", variation.content_type有効な拡張子は受け入れ
format: :jpgやformat: "jpeg",format: "webp"など、サポート対象の拡張子なら#content_typeが正しい MIME type に解決される。
不正なフォーマットで ArgumentError
- 未知の拡張子(例:
format: :foo)を指定した場合にArgumentErrorを投げること。
rubyassert_raises(ArgumentError) do ActiveStorage::Variation.new(format: :foo).content_type end- 未知の拡張子(例:
これにより、アプリケーション側で誤った format 指定をしたときに、サイレントに失敗するのではなく、明確な例外で検知できることが保証されます。
2-7. transform はあえて未カバー
「transform is intentionally not tested here. That path depends on MiniMagick/Vips and is already covered in variant_test.rb.」
Variation#transform は実ファイル処理(MiniMagick / Vips)に依存するため、この PR ではテスト対象から外しています。
ここは既存の variant_test.rb など統合テストで既にカバーされているため、今回の PR はあくまで純粋な値オブジェクトとしての Variation の契約部分に特化しています。
- 影響範囲・注意点
- 本番コードへの変更は一切なし
- テストファイルの追加のみで、
ActiveStorage::Variationの挙動自体は変わっていません。
- テストファイルの追加のみで、
- ただし、テストによって挙動の「仕様」がより明文化されたと言えます。
そのため、今後以下のような変更を行う場合には、このテスト群が「仕様差分」を検知する役目を果たします:- 変換ハッシュの扱い(string/symbol の扱いなど)を変更する
encode/keyの生成ロジック(署名の中身や並び順など)を変更する- デフォルトの
formatやcontent_typeを変える
- ActiveStorage を利用して独自に
ActiveStorage::Variationを直接扱っているコードがある場合、- 本 PRによって壊れることはないものの、
- 既に行っていた利用パターン(特にフォーマットやキー生成に関する期待値)が、テストによって正式に「Rails 側の仕様」として固定化されたと見なせます。
- 参考情報 (あれば)
- 対象クラス:
ActiveStorage::Variation(activestorage/app/models/active_storage/variation.rb)
- 関連テスト(既存):
activestorage/test/models/variant_test.rb(transformを含む統合的な変換テスト)
- 利用される場面:
ActiveStorage::Variant(画像バリアント生成)- プレビュー / レプレゼンテーションコントローラ(
ActiveStorage::Variation.encodeを通じた署名付き URL など)
#57624 Fix URL fragment in Active Record Migrations guide [ci skip]
マージ日: 2026/6/8 | 作成者: @VladNegara
- 概要 (1-2文で)
Active Record Migrationsガイド内のリンク先フラグメント(ページ内アンカー)が誤っていたため、正しいフラグメントに修正したPRです。機能コードには一切手を触れず、ドキュメント上のリンク切れを解消するだけの変更です。
- 変更内容の詳細
対象:
guides/source/active_record_migrations.md
問題点:
- 「Creating a Join Table(結合テーブルの作成)」節から、Active Record Associationsガイドの「has_and_belongs_to_many」に飛ぶリンクが、存在しないフラグメント
#the-has-and-belongs-to-many-associationを指していた。
修正内容:
- フラグメントを以下のように変更:
- ...active_record_associations.html#the-has-and-belongs-to-many-association
+ ...active_record_associations.html#has-and-belongs-to-many意味としては同じセクションを指しているが、実際のHTML側のid属性が has-and-belongs-to-many で定義されているため、それに合わせてリンクを修正した、という内容です。
コードサンプル上の挙動やマイグレーションDSLなどには一切変更はなく、「どのセクション説明に飛ぶか」というドキュメント内ナビゲーションだけが変わっています。
- 影響範囲・注意点
影響範囲
- Railsガイド(Active Record Migrations -> Creating a Join Table)のページ内リンクのみ。
- ランタイムの挙動、API、テストには影響なし。
- 既存コード、アプリケーション、gemとの互換性に影響なし。
注意点
- ドキュメントをローカルでビルド・ホストしている場合も、ビルド済みHTMLのid属性が
has-and-belongs-to-manyである前提なので、ガイド生成側のレイアウトが大きく変更されていない限り、この変更によりリンク切れが解消されます。 - 外部からこの古いフラグメント
#the-has-and-belongs-to-many-associationを直リンクしていた場合は、引き続き404相当の挙動(スクロールされない)になりますが、これはRailsガイド外部の問題です。
- ドキュメントをローカルでビルド・ホストしている場合も、ビルド済みHTMLのid属性が
- 参考情報 (あれば)
- 該当ガイド:
- Active Record Migrations: 「Creating a Join Table」節
- Active Record Associations: 「Has and Belongs to Many」節(id:
has-and-belongs-to-many)
- 類似の修正はドキュメントのリンク切れ対応としてよく行われており、本PRもそれと同種のメンテナンス変更です。
#57620 Fix number_to_delimited corrupting numbers with a leading sign
マージ日: 2026/6/8 | 作成者: @55728
- 概要 (1-2文で)
number_to_delimitedが符号付き数値(先頭に-や+が付く数)を区切り文字付きに変換する際、先頭の符号直後に区切り文字を挿入してしまう不具合を修正した PR です。これにより、number_to_roundedやnumber_to_humanで符号付き数値+delimiter:を使った場合の誤ったフォーマットも合わせて解消されます。
- 変更内容の詳細
不具合の内容
number_to_delimited に負数(または + 付きの文字列)を渡すと、桁数が 3 の倍数の場合に符号の直後にカンマが入り、数値が壊れていました。
number_to_delimited(-123456) # 以前: "-,123,456" 本来: "-123,456"
number_to_delimited(-123) # 以前: "-,123" 本来: "-123"
number_to_delimited(+123456) # 以前: "+,123,456" 本来: "+123,456"
number_to_delimited("+123") # 以前: "+,123" 本来: "+123"number_to_rounded や number_to_human も内部で同じ変換ロジックを使うため、delimiter: オプションを指定すると同様に壊れた出力になっていました。
number_to_rounded(-123456, delimiter: ",", precision: 0)
# 以前: "-,123,456" 本来: "-123,456"
number_to_human(-123456, delimiter: ",")
# 以前: "-,123 Thousand" 本来: "-123 Thousand"原因
NumberToDelimitedConverter#parts の「高速パス」が、整数部を 3 桁ごとに分割する際に offset = left.size % 3 を使っていました。このとき、left に符号 - / + も含まれていたため、「符号を 1 桁として扱ってしまい、そこでグループを切る」という状態になっていました。
例: -123456
- integer 部
leftは"-123456" left.size == 7→offset = 7 % 3 == 1- 先頭 1 文字とそれ以降 3 桁ずつでグループ化される
→["-", "123", "456"].join(",")→"-,123,456"
-1234 など、一見正しく見えるケースは「たまたま符号と1桁目が同じグループになっている」だけで、ロジックとしては常に壊れている状態です。
元々は delimiter_pattern の正規表現 /(\d)(?=(\d\d\d)+(?!\d))/ を使う旧パスがデフォルトで、これは数字にしかマッチしないため符号を誤処理しませんでした。
しかし、コミット 33fbedb1b1 で % 3 を使う高速パスがデフォルトになった際に、この符号処理の差分が考慮されず、回 regress しました。
同じ高速パスが原因で Float::INFINITY が "In,fin,ity" になる問題は、以前のコミット 150e4c0443 で修正済みであり、今回は「符号付き数値版の同じ問題」を修正しています。
修正内容
整数部のグルーピング前に、先頭の符号をいったん切り離し、グルーピング後に付け直すようにしました。NumberToCurrencyConverter が「絶対値をフォーマットしてから符号を再適用する」のと同じ考え方です。
該当箇所(概略):
# 変更前(イメージ)
left = integer_part
# left をそのまま offset / 3 桁グループ化
# 変更後(イメージ)
left = integer_part
sign = left.slice!(0) if left.start_with?("-", "+") # ← 符号を取り除く
# ここから先は「符号を抜いた left」を 3 桁ずつに分割する既存処理
left_parts = []
offset = left.size % 3
left_parts << left.slice!(0, offset) if offset > 0
left_parts.concat(left.scan(/.{3}/))
# 最後に符号を戻す
left = "#{sign}#{left_parts.join(options[:delimiter])}"ポイント:
- 変更は
#parts内に限定されており、delimiter_pattern正規表現を使うパスには影響しません。 +付きの文字列入力("+123456"など)も対象になり、同様に修正されます。
テスト追加
activesupport/test/number_helper_test.rb に以下のテストが追加されています。
test_to_delimited_with_negative_numbers- 対象:
-1,-12,-123,-1234,-123456,-123456789,-123456.78など - 文字列形式の負数もカバー
- 対象:
test_to_delimited_with_leading_plus_sign- 対象:
"+123","+1234","+123456","+123456.78"
- 対象:
もともと number_to_delimited に対して符号付き数値のテストが存在しておらず、そのため regression が検出できなかった、という背景も明示されています。
- 影響範囲・注意点
- 影響を受けるメソッド:
number_to_delimitednumber_to_rounded(delimiter:オプション使用時)number_to_human(delimiter:オプション使用時)
- 修正内容により、「これまで壊れたフォーマットが出ていたケース」が正しいフォーマットになります。
- 例: 既存のテスト・期待値・スナップショット等で
"-,123,456"のような誤った値をあえて使用していた場合は、テストが落ちる可能性があります。
- 例: 既存のテスト・期待値・スナップショット等で
- 正の数、ゼロ、符号なしの文字列、浮動小数、
delimiter_patternをカスタムしたケースは、今回の修正では動作が変わりません。 number_to_currencyはすでに「絶対値+符号の再適用」という設計であり、今回の修正の対象ではないことが確認されています。
実運用上は「いままで静かに壊れていた値が正しくなる」だけなので、多くの場合は歓迎される変更ですが、帳票やログで「文字列としてのフォーマットが変わる」ことには注意してください。
- 参考情報 (あれば)
関連する過去の変更:
33fbedb1b1—NumberToDelimitedConverterの高速パスをデフォルトにした変更150e4c0443— 非有限浮動小数点 (Float::INFINITYなど) に対する同じ経路の不具合修正
実際の影響確認のためのサンプルコード(コンソール用):
rubyinclude ActionView::Helpers::NumberHelper [ -123, -1234, -123456, -123456.78, "+123", "+1234", "+123456", "+123456.78" ].each do |n| puts "#{n.inspect.ljust(12)} => #{number_to_delimited(n)}" end puts number_to_rounded(-123456, delimiter: ",", precision: 0) puts number_to_human(-123456, delimiter: ",")
#57621 Fix InheritableOptions#== raising NoMethodError on non-Hash comparison
マージ日: 2026/6/8 | 作成者: @55728
- 概要 (1–2文で)
ActiveSupport::InheritableOptions#==が Hash 以外との比較時にNoMethodErrorを起こしていた問題を修正し、Hash/Hashライクなオブジェクトとのみ中身を比較し、それ以外とは常にfalseを返すようにした PR です。これによりnilと空のInheritableOptionsが誤って等価になる問題や、Array#include?などからの予期せぬNoMethodErrorが解消されます。
- 変更内容の詳細
問題のあった既存実装
ActiveSupport::InheritableOptions#== は以下のように実装されていました。
def ==(other)
to_h == other.to_h
endこのため、other が to_h を持たないオブジェクト(String, Integer, Symbol など)の場合に NoMethodError が発生していました。
options = ActiveSupport::InheritableOptions.new(one: "first value")
options == "first value"
# => NoMethodError: undefined method `to_h` for "first value":String
[options].include?("x")
# => Array#include? は == を使うので、同様に NoMethodErrorさらに、nil.to_h # => {} であることから、空の InheritableOptions が nil と等価になってしまうバグもありました。
ActiveSupport::InheritableOptions.new == nil
# => true (本来は false であるべき)修正後の実装
#== を次のように変更しています:
def ==(other)
other.is_a?(Hash) && to_h == other.to_h
endポイント:
respond_to?(:to_h)ではなく、is_a?(Hash)を使っているnilはto_hを持つ(nil.to_h # => {})ため、respond_to?ベースだとnilと空のInheritableOptionsが再び等価になってしまうHash#==の振る舞いと同じにしたい、という意図
Hashのサブクラス(ActiveSupport::OrderedOptionsなど)や他のInheritableOptionsはis_a?(Hash)を満たすので、これらとは中身をto_hで比較する- それ以外のオブジェクトとは、常に
falseを返す
修正後の挙動サンプル
options = ActiveSupport::InheritableOptions.new(one: "first value")
options == { one: "first value" }
# => true
options == ActiveSupport::InheritableOptions.new(one: "first value")
# => true(マージ済みの中身を比較)
options == { one: "other value" }
# => false
options == "first value"
# => false
options == 42
# => false
options == nil
# => false
[options].include?("x")
# => false(例外は出ない)テスト
activesupport/test/ordered_options_test.rb に以下のようなテストが追加されています(要旨):
test_inheritable_options_equalityInheritableOptions同士やHashとの比較が正しく true/false を返すか
test_inheritable_options_equality_with_non_hash_returns_falseString, 数値,nilなど Hash ではないものとの比較がfalseになり、例外を出さないこと
- 影響範囲・注意点
- 影響するクラス/機能
ActiveSupport::InheritableOptions- Rails credentials (
Rails.application.credentials) AbstractControllerのconfig- Active Record の
protocol_adapters - Action Text のエディタ設定
- など、
InheritableOptionsを内部実装に使っている箇所全般
- Rails credentials (
- 行動の変化
- 以前は「誤って
NoMethodErrorが飛ぶ」「空のInheritableOptionsがnilと等しい」という明らかなバグだったため、この変更により壊れる正当なユースケースは基本的に想定されていません。 - もしアプリケーション側で、
- 「
InheritableOptionsとnilが等価になる」ことに暗黙に依存していた - あるいは「非 Hash オブジェクトとの比較で例外が起きる」ことを前提にテストを書いていた
といったケースがあれば、その挙動は変わります(falseを返すようになる)。
- 「
- 以前は「誤って
Hash#==相当の振る舞いになるため、OrderedOptionsなど他の Hash ベースの設定オブジェクトと一貫性が取れます。
- 参考情報 (あれば)
- 類似クラス:
ActiveSupport::OrderedOptionsはHashを継承しており、Hash#==をそのまま使っているため、もともと非 Hash 比較時はfalseを返していました。本 PR によりInheritableOptionsもこれに揃えられています。 - もとの変更:
InheritableOptions#==自体は 81d0a29 ("ImproveInheritableOptionshash-like behaviour") で導入された機能で、親とマージ済みの内容で比較を行うために実装されましたが、その際に「非 Hash 相手」のケースが考慮されていませんでした。
#57615 Fix clear_*_change doc claiming it clears previous changes [ci skip]
マージ日: 2026/6/7 | 作成者: @55728
- 概要 (1-2文で)
clear_*_changeメソッド(正確にはclear_attribute_changeが生成する系)のドキュメントが「現在の変更と過去の変更を両方クリアする」と誤って説明していたのを、「現在の変更だけをクリアする」という実装どおりの内容に修正する PR です。挙動自体には変更はなく、ドキュメントのみの修正です。
- 変更内容の詳細
対象:
- ActiveModel::Dirty が生成する
clear_*_changeメソッド (clear_attribute_change) に対するドキュメントコメント。
- ActiveModel::Dirty が生成する
実装とドキュメントの乖離:
ドキュメントには以下のような趣旨が書かれていました:
属性の dirty データ(現在の変更と過去の変更)をすべてクリアする
しかし実際の実装は次のとおりで、現在の変更のみを消しており、過去の変更 (
previous_changes/saved_changes) には触れていません:rubydef clear_attribute_change(attr_name) mutations_from_database.forget_change(attr_name.to_s) endmutations_from_databaseは「現在の変更」を表すトラッカーで、previous_changes/saved_changesはmutations_before_last_saveという別トラッカーにより管理されています。clear_attribute_changeはこのmutations_before_last_saveには一切触れないため、「過去の変更もクリアする」という説明は誤りです。
正しい「両方クリア」するメソッド:
- 現在の変更と保存前の変更の両方をリセットするのは
clear_changes_informationであり、clear_attribute_changeではありません。
- 現在の変更と保存前の変更の両方をリセットするのは
具体的な修正:
- 該当ドキュメント中の「and previous changes」という文言を削除し、「現在の変更をクリアする」ことのみを説明するように 1 行差し替えています(+1/-1)。
- メソッドの使用例自体はもともと「現在の変更だけが消える」ケースを示しており、実装と整合していました。今回の修正で、ドキュメントの説明文もそれに揃えています。
経緯:
- この「and previous changes」という文言は、過去のコミット
ee58c8e6beで誤って導入されたとのことです。
- この「and previous changes」という文言は、過去のコミット
- 影響範囲・注意点
- 挙動の変更は一切ありません。
- 既存の
clear_*_change/clear_attribute_changeの動作(現在の変更のみクリア)は変わりません。
- 既存の
- 影響するのは以下のようなケースです:
- ドキュメントを読んで「
clear_*_changeを呼べばprevious_changes/saved_changesも消える」と誤解していたコードレビューや設計判断。 - その前提で「過去の変更がクリアされているはず」と想定しているテストや仕様書(※実際のコードはもともと過去の変更を消していないため、挙動と期待がずれていた可能性があります)。
- ドキュメントを読んで「
- 実際にやりたいこと別の正しいメソッド選択:
- 特定属性の「現在の変更」だけクリアしたい:ruby
user.clear_attribute_change(:name) # または generated method: user.clear_name_change - 全属性の現在&保存前の変更情報を一括でクリアしたい:ruby
user.clear_changes_information
- 特定属性の「現在の変更」だけクリアしたい:
- 注意点:
- 「保存後に
previous_changesを消したい」といった用途でclear_*_changeに頼っていた場合、もともとその用途には合っていなかったため、改めて要件に応じてclear_changes_informationを使う- あるいは別に状態を保持する といった見直しが必要です。
- 「保存後に
- 参考情報 (あれば)
- 関連 API:
ActiveModel::Dirty#changes/changed_attributesActiveModel::Dirty#previous_changes/saved_changesActiveModel::Dirty#clear_attribute_changes(attr_name)(生成されるclear_*_change系)ActiveModel::Dirty#clear_changes_information
- 実装上のトラッカー:
- 現在の変更:
mutations_from_database - 最後の保存前の変更:
mutations_before_last_save
→ 今回の PR は、これらの役割分担にドキュメントを合わせた、という位置づけです。
- 現在の変更:
#57598 Fix find with an explicit order + offset past the ids raising a spurious RecordNotFound
マージ日: 2026/6/7 | 作成者: @55728
- 概要 (1-2文で)
findに配列ID + 明示的なorder+offsetが ID 数を超える値で指定された場合に、意味不明な負数付きのActiveRecord::RecordNotFoundが発生していた不具合を修正し、そのケースで空配列 ([]) を返すように揃えた PR です。あわせて、orderの有無でfindの挙動が食い違わないことを確認する回帰テストが追加されています。
- 変更内容の詳細
何が問題だったか
次のようなクエリを考えます:
Model.order(:id).offset(10).find([1, 2, 3])ids = [1, 2, 3] で offset = 10 のように、「オフセットが ID 配列のサイズを超えている」場合、本来であれば単に「該当レコードなし」として [] が返ってきてほしいところです。
実際、order を付けない場合は正しく [] を返します:
Model.offset(10).find([1, 2, 3])
# => []しかし、order を付けると以下のような例外が発生していました:
Model.order(:id).offset(10).find([1, 2, 3])
# => ActiveRecord::RecordNotFound:
# Couldn't find all Models with 'id': (1, 2, 3)
# (found 0 results, but was looking for -7).found 0 results, but was looking for -7 という負数が出ている時点で、内部の期待件数の計算が破綻していることがわかります。
原因 (find_some / find_some_ordered の違い)
ActiveRecord::FinderMethods#find は、呼び出し条件によって内部的に次の2つの経路に分岐します:
- 明示的な
orderあり:find_some経由 - 明示的な
orderなし:find_some_ordered経由
問題のロジックは find_some の中にあり、「期待される件数 (expected_size)」を offset と limit を使って算出する処理でバグがありました。
該当するコードイメージは概ね以下のようなものです:
# 例: 11個のid、limit=3、offset=9 の場合、結果は2件になるべき
if offset_value && (ids.size - offset_value < expected_size)
expected_size = ids.size - offset_value
endoffset_value が ids.size より大きい(=配列の末尾を通り過ぎている)とき、
ids.size - offset_value # => 負の数となり、そのまま expected_size に代入されてしまいます。
結果として:
- 実際の取得件数:
result.size # => 0 - 期待件数:
expected_size # => 負の数
となるため、「期待件数と実際の件数が違う」と判断され、RecordNotFound が投げられていました。
一方で、order なしパス(find_some_ordered)では、offset / limit で ID をスライスしているため、同じ条件でも単に [] を返し、この不具合は発生していませんでした。
つまり、「order 有り」の経路だけが異常に厳しく、かつ壊れている 状態でした。
修正内容
expected_size をオフセットで調整する際、負数にならないよう 0 でクランプしています:
expected_size = [ids.size - offset_value, 0].maxこれにより:
ids.size - offset_valueが 0 以上 → そのままids.size - offset_valueが負数 → 0 にする
という挙動になります。
具体例:
- 通常のパターン(例:
ids.size = 11, limit = 3, offset = 9)ids.size - offset = 2→max(2, 0) == 2→ 従来どおり、2件を期待 - バグっていたパターン(例:
ids.size = 3, offset = 10)ids.size - offset = -7→max(-7, 0) == 0
→ 期待件数 0、実際のresult.sizeも 0 なので、例外を出さずに[]を返す
これで、「order ありパス」も「order なしパス」と同じように、オフセットが範囲外に飛び出したときは単に空結果を返すようになります。
テスト追加
activerecord/test/cases/finder_test.rb に回帰テストが2つ追加されています。
test_find_with_order_and_offset_past_the_ids_returns_emptyoffsetが ID 数を超えるケース、およびちょうど等しい境界 (offset == ids.size) で、orderありの経路 (find_some)orderなしの経路 (find_some_ordered) が共に[]を返すことを検証。
test_find_with_order_limit_and_offset_matches_unordered_path- 11件の Developer fixture を使って、7パターンの
limit/offset組み合わせを総当たりし、 orderあり(find_some)とorderなし(find_some_ordered)で返ってくるレコード集合が常に一致することを確認。- 範囲内のオフセット、終端を超える
limit、offset == ids.size、offset > ids.sizeなどを網羅。
- 11件の Developer fixture を使って、7パターンの
テストは sqlite3 / PostgreSQL / MySQL2 の3アダプタで「失敗を再現 → 修正後にパス」を確認しています。
- 影響範囲・注意点
影響を受けるパターン
Model.order(...).offset(X).find([array_of_ids])のようにfindに「配列ID」を渡している- 明示的な
orderを指定している - そのクエリに対する
offsetが「渡している ID 配列の要素数より大きい」
- これまで:
ActiveRecord::RecordNotFound例外が発生していた - 修正後:
[]が返る(order無しパスと揃う)
互換性・挙動変更
- 変更されるのは「これまでそもそも壊れていた・実務上も不自然」なケースのみです。
- 正常な範囲での
offset/limitの挙動、offset == ids.sizeの境界ケースは従来どおり(※offset == ids.sizeに関しては元々両パスとも[]を返していた)。 - もし既存コードが「この壊れた挙動に(=負の expected size による RecordNotFound 発生)依存している」場合は振る舞いが変わりますが、通常は依存すべきでない仕様外のバグなので、修正によるリスクは小さいと考えられます。
実務への影響
- ページネーション実装などで、ユーザーが「最終ページを超えたページ番号」を指定した場合に、
orderの有無でエラーになったりならなかったりする不整合があったのが、[]に統一されます。 - 「配列 ID + 明示的 order + 大きい offset」を組み合わせているコードで、これまで謎の
RecordNotFoundが sporadic に出ていたなら、この修正で改善される可能性があります。
- ページネーション実装などで、ユーザーが「最終ページを超えたページ番号」を指定した場合に、
- 参考情報 (あれば)
対象ファイル
activerecord/lib/active_record/relation/finder_methods.rbfind_some内のexpected_size計算ロジックが修正。
activerecord/test/cases/finder_test.rbfindのorder/limit/offset周りの回帰テストが追加。
挙動変更の要点
- 「offset > ids.size」での
findは、orderの有無に関わらず[]を返す、という揃った仕様になった。 - 内部的には「期待件数を負数にしない(0でクランプする)」というシンプルなバグ修正。
- 「offset > ids.size」での
#57613 Make rails new work again on systems that do not have vips
マージ日: 2026/6/7 | 作成者: @jeromedalbert
- 概要 (1-2文で)
rails new実行時に vips がインストールされていない環境でアプリ作成が失敗していた不具合を修正し、Active Storage で vips を実際に使うタイミングまでエラーが発生しないようにした PR です。Gemfileのruby-vipsの読み込み方法を変え、テストも追加されています。
- 変更内容の詳細
Gemfile テンプレートの修正
rails new で生成されるアプリの Gemfile のテンプレート (Gemfile.tt) が修正されています。
変更前(イメージ):
gem "ruby-vips"変更後:
gem "ruby-vips", require: falseポイント:
require: falseを付けることで、Bundler がアプリ起動時に自動でrequire "vips"しなくなります。- これにより、システムに vips ライブラリや関連ネイティブ拡張が無くても、アプリのブート時点ではエラーになりません。
- 実際に vips を必要とするのは Active Storage が vips をバックエンドとして利用するタイミングなので、その時点で vips または ImageMagick が入っていることを前提にしています。
テストの追加
railties/test/generators/app_generator_test.rb にテストが追加されています。
意図されていること:
- vips がインストールされていない環境でも
rails newで生成されたアプリが正常に起動できることを担保する。 - Gemfile に
ruby-vipsが含まれていても、require: falseのおかげで即時にロードされず、アプリ起動が壊れないことを確認する。
(実際のテストコードは PR 本文には出ていませんが、Gemfile.tt の出力と生成アプリの挙動を検証していると考えられます。)
- 影響範囲・注意点
影響対象:
- Rails
mainブランチ(将来のバージョン)でrails newを実行するすべての開発者。 - 特にローカルに vips をインストールしていない開発環境。
- Rails
ポジティブな影響:
- vips 未インストール環境で
rails newが落ちるというレグレッションが解消されます。 - Active Storage を使わない(あるいは画像処理を使わない)アプリでも、vips がないことが原因で起動できない、といった不具合がなくなります。
- vips 未インストール環境で
注意点:
require: falseは「自動で require しない」だけであり、vips 自体が不要になるわけではありません。- Active Storage で vips ベースの画像変換を行いたい場合は、引き続き:
- システムに vips (libvips) をインストールする
ruby-vipsgem を bundle install する
といったセットアップが必要です。
- 画像処理を実行するタイミングで、vips(あるいは代替として ImageMagick + mini_magickなど)が正しくインストールされていないと、その時点でエラーになります。
- 参考情報 (あれば)
- 元 PR: https://github.com/rails/rails/pull/57403
- この PR で修正された issue: https://github.com/rails/rails/issues/57612
ruby-vipsgem: https://github.com/libvips/ruby-vips- Bundler の
require: falseオプションの説明(Bundler ドキュメント):
https://bundler.io/guides/groups.html#require-false
#57616 Fix ActiveModel::Type::Decimal doc for a non-numeric cast [ci skip]
マージ日: 2026/6/7 | 作成者: @55728
- 概要 (1-2文で)
ActiveModel::Type::Decimalのドキュメント中のキャスト結果の説明が実装と食い違っていたため、非数値を渡した場合の戻り値をnilではなく0.0に修正するドキュメント更新です。実装自体の挙動変更はなく、コメント(サンプルコードの期待値)のみが正されました。
- 変更内容の詳細
対象: activemodel/lib/active_model/type/decimal.rb のドキュメント例 (コメント)
元のドキュメントでは、以下のような説明になっていました (意図としてはこういう内容だった):
bag.weight = :arbitrary
bag.weight # => nil (the result of `.to_s.to_d`)しかし、実際の Ruby / BigDecimal の挙動は次の通りです:
:arbitrary.to_s # => "arbitrary"
"arbitrary".to_d # => 0.0 # 非数値文字列は 0.0 になるRails の実装もこれに沿っており:
ActiveModel::Type::Decimal.new.cast(:arbitrary) # => 0.0
ActiveModel::Type::Decimal.new.cast("") # => nil # 空文字列のみ nilこの PR では、ドキュメント内の期待値コメントを以下のように修正しています:
bag.weight = :arbitrary
bag.weight # => 0.0 (the result of `.to_s.to_d`)※ コード本体のロジックは一切変更されておらず、1行のコメント(例の戻り値)を nil → 0.0 に変更しただけです。
- 影響範囲・注意点
影響範囲
- 実行時挙動の変更はなく、既存アプリケーションの動作には一切影響しません。
- Rails ガイド/ソース内コメントを読んでいた人が、
「非数値をDecimalにキャストするとnilになる」と誤解してしまう可能性が解消されます。
注意点 (開発者視点での挙動整理)
ActiveModel::Type::Decimalの#castは概ねvalue.to_s.to_d相当の挙動であり、「空文字列」はnil、それ以外の非数値文字列は0.0になります。- 例:ruby
type = ActiveModel::Type::Decimal.new type.cast("") # => nil type.cast(" ") # => nil (blank? 判定で弾かれる) type.cast(nil) # => nil type.cast("abc") # => 0.0 type.cast(:symbol) # => 0.0 type.cast("123.45") # => 0.12345e3 のような BigDecimal
- 例:
- 「非数値は
nilになってほしい」という前提でバリデーションや分岐を書いていると、
実際には0.0が入っていて不具合になる可能性があります。
その場合は- カスタムの型を定義する
- 事前に値を検査して弾く などで対処する必要があります。
- 参考情報 (あれば)
- BigDecimal の挙動 (Ruby 本体):
BigDecimal("abc")は0.0を返す仕様です。
- 関連する Rails の実装:
ActiveModel::Type::DecimalはActiveModel::Type::Valueを継承し、cast内でvalue.to_sした上でBigDecimal変換を行うことで、この挙動に従っています。
#57618 Fix add_index example in Active Record Schema docs
マージ日: 2026/6/7 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Schema のドキュメント内で示されていたadd_indexの使用例が実際のメソッドシグネチャと合っておらず、実行するとArgumentErrorになる問題を修正した PR です。add_indexに対する誤った位置引数の利用例を、正しいキーワード引数形式へと修正しています。
- 変更内容の詳細
対象ファイル:
activerecord/lib/active_record/schema.rb(1行の修正)
元のドキュメント例では、以下のように add_index が呼ばれていました:
ActiveRecord::Schema[7.0].define do
create_table :authors do |t|
t.string :name, null: false
end
add_index :authors, :name, :unique
endしかし、add_index は以下のように定義されており:
def add_index(table_name, column_name, **options)
# ...
end第3引数以降はキーワード引数(**options)として受け取るため、:unique のような裸のシンボルを位置引数として渡すと ArgumentError: wrong number of arguments になります。
この PR では、この例を正しくキーワード引数を使う形に修正しています:
ActiveRecord::Schema[7.0].define do
create_table :authors do |t|
t.string :name, null: false
end
add_index :authors, :name, unique: true
endつまり、:unique → unique: true への変更のみが行われています。
- 影響範囲・注意点
影響範囲
- 変更はドキュメント(サンプルコード)上の 1 行のみで、ライブラリの挙動そのもの(
add_indexの実装)には変更がありません。 - 既存アプリの実行時挙動には影響しません。
- 変更はドキュメント(サンプルコード)上の 1 行のみで、ライブラリの挙動そのもの(
注意点
- もし自分のアプリでドキュメントをコピーして同様に
add_index :authors, :name, :uniqueと書いている場合は、必ずadd_index :authors, :name, unique: trueのようにキーワード引数形式に修正する必要があります。 - 他のオプション(
name:,where:,using:など)もすべてキーワード引数で渡す前提になっているため、add_indexにシンボルなどを位置引数として追加する形はサポートされていません。
- もし自分のアプリでドキュメントをコピーして同様に
- 参考情報 (あれば)
add_indexの定義:activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rbrubydef add_index(table_name, column_name, **options) # ... end- 正しい呼び出し例:ruby
add_index :authors, :name, unique: true add_index :posts, [:user_id, :created_at], name: "index_posts_on_user_and_created_at"
#57607 Fix in_order_of surfacing NULL rows for an unknown enum key or out-of-range integer
マージ日: 2026/6/7 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Relation#in_order_ofが、列挙型(enum)の未知キーや範囲外整数を指定したときに、本来無視すべき値をNULLマッチとして扱い、NULL行を紛れ込ませてしまう不具合を修正した PR です。明示的なnilと「シリアライズ結果としてのnil」を区別し、後者はin_order_ofから除外することで、Enumerable#in_order_ofと同じ挙動に揃えています。
- 変更内容の詳細
不具合の内容
ActiveRecord::Relation#in_order_of(column, values) で、values に以下のような値が含まれる場合に問題が起きていました。
- enum カラムに対する「未知のキー」
- 整数カラムに対する「範囲外の整数」
例:
# books: one :proposed, one :written, one with a NULL status
Book.in_order_of(:status, [:written, :bogus, :proposed]).pluck(:id)
# 実際の挙動 (バグあり):
# => [2, 3, 1] # :bogus の位置に NULL 行(id 3)が割り込む
# 期待される挙動:
# => [2, 1] # :bogus は無視されるべきEnumerable#in_order_of は「コレクション内に存在しない値」は素通りして無視する仕様であり、Active Record 版も「存在しない値は無視する」と CHANGELOG で明記されていましたが、実装上は次のような経路で NULL 行を拾っていました。
in_order_ofは、valuesの各要素についてカラム型に合わせてシリアライズする:rubycaster.serialize(value) if caster.serializable?(value)その結果:
- 「範囲外整数」:
serializable?が false →ifの結果がnil - 「未知 enum キー」:
serializable?は true だがserializeがnilを返す
- 「範囲外整数」:
すると、「元々 nil ではないが、結果として
nilになった値」と「呼び出し側が明示的に渡したnil」が区別できなくなる。in_order_ofは、呼び出し側がnilを指定した場合はcolumn IS NULLをマッチさせる仕様のため、
シリアライズに失敗した値も「nilとみなして良い」と誤解され、WHERE column IN (…) OR column IS NULLORDER BY CASE WHEN column = … THEN … WHEN column IS NULL THEN … ENDのようにNULL行を拾う・並び替えるロジックに紛れ込んでしまう、というバグでした。
既存テスト test_in_order_of_with_out_of_bound_integer では Post.id(NOT NULL な主キー)を使っていたため、「余計な IS NULL 条件」があっても実際にマッチする行がなく、バグが表に出ていませんでした。
修正方針
修正のポイントは以下です。
明示的な
nilと、シリアライズ結果としてのnilを区別する- 呼び出し側が
valuesにnilを入れた場合:
→ 従来通り、NULL行をマッチさせる(column IS NULLを使う)。 - 呼び出し側が
nil以外の値を指定したが、serializable?が false だったりserializeがnilを返した場合:
→ その値はin_order_ofの対象から完全に除外する(WHEREにもORDER BY CASEにも出さない)。
- 呼び出し側が
値のドロップ後に
valuesが空になりうることへの対応- すべての値が「非シリアライズ可能 or シリアライズ結果が
nil」だった場合、
ロジック上CASE式のWHEN句が 1 つも無いCASE END(またはCASE ELSE 1 ENDforfilter: false)が生成される可能性があり、これは SQL 的に不正でStatementInvalidを引き起こします。 - 今回の変更では、「ドロップ処理の結果として
valuesが空になった場合」は、呼び出し元が最初から[]を渡した場合と同様にnone!(空 Relation)を返すようにしました。
これにより、不正なCASE式は組み立てられず、クエリ実行時にエラーにもならず、シンプルに「0件」が返ります。
- すべての値が「非シリアライズ可能 or シリアライズ結果が
配列グルーピング分岐でも同じ扱い
in_order_ofには、単純な値列だけでなく「値をグルーピングして扱う」ブランチ(配列をまとめて 1 グループ扱いするようなケース)もありますが、そちらでも同様に「シリアライズできない・nilになってしまう値はドロップする」ように揃えています。- ドロップされた値は
WHEREにもORDER BYにも一切寄与しません。
以上により、Enum の未知キーや範囲外整数は Enumerable#in_order_of と同じく「存在しないものとして無視」され、かつ余計な NULL 行のマッチングも発生しなくなります。
追加・修正されたテスト
activerecord/test/cases/relation/field_ordered_values_test.rb に以下のテストが追加されています。
test_in_order_of_with_unknown_enum_key_does_not_match_nulls- 対象:
Book#nullable_status(NULL 許可の enum カラム) - 状況:
status = nilの行を含むテーブルに対して、未知 enum キーをvaluesに含めてin_order_ofを実行。 - 確認内容: 未知キーは無視され、
NULL行は拾われないこと。
- 対象:
test_in_order_of_with_out_of_bound_integer_does_not_match_nulls- 対象:
Book#format_record_id(NULL 許可の整数カラム) - 状況:
format_record_id = nilの行がある中で、範囲外の整数をvaluesに指定。 - 確認内容: 範囲外整数は無視され、
NULL行は拾われないこと。
(主キーではなく NULL 許可カラムを使うことで、これまで見落としていたバグをきちんと検証)
- 対象:
test_in_order_of_with_only_unrepresentable_values_does_not_build_empty_case- 状況: 渡した全ての値がシリアライズ不可能で、結果としてすべてドロップされるケース。
- 確認内容:
filter: true/filter: falseいずれの場合でもStatementInvalidが発生しないこと。- 結果 Relation は空(
none!相当)になること。
なお、既存の test_in_order_of_with_nil などにより、「明示的に nil を指定した場合に NULL 行をマッチさせる」従来仕様は維持されていることも確認されています。
- 影響範囲・注意点
仕様として期待されていた動き(
Enumerable#in_order_ofと同様、「存在しない値は無視」)に合わせる修正であり、ドキュメントおよび以前の CHANGELOG の意図を「復元」する変更です。ただし、これまでのバグに依存していたコード(「未知 enum キーや範囲外整数を渡すと
NULL行が取れる」挙動をあえて利用していたケース)があれば、挙動は変わります。- 今後はそのような値は単に無視されるため、
NULL行を取りたい場合は、明示的にnilを値リストに含める必要があります:ruby# NULL を明示的にマッチさせたい場合 Book.in_order_of(:status, [:written, nil, :proposed])
- 今後はそのような値は単に無視されるため、
「渡した値がすべてシリアライズできない」ケースでは、これまでは(実際には到達しづらかったものの)DBエラー
StatementInvalidになりえたところ、今後は単に「0件を返す Relation」になります。- これにより、
in_order_ofの結果をそのままスコープとして合成・チェインしても DB 例外で落ちることはなくなります。
- これにより、
この変更は次の全サポート DB アダプタで確認されています:
- sqlite3
- postgresql
- mysql2
field_ordered_values_test, enum_test, relations_test も一通りグリーンで、他の機能に対する副作用は抑えられています。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57607
- 過去の関連 PR(範囲外値の扱いを導入したもの): #44746
- ここで「範囲外値は無視する」とドキュメント化されていたものの、実装は
NULL行を拾うバグを含んでいました。本 PR でドキュメントと実装が一致しました。
- ここで「範囲外値は無視する」とドキュメント化されていたものの、実装は
- 関連するメソッド仕様:
Enumerable#in_order_of(Ruby 標準): 集合に存在しない値は単に結果に現れない。- ActiveRecord 側も、それに忠実に合わせる方針です。
#57611 Fix double encode for Coders::JSON and JSON column
マージ日: 2026/6/6 | 作成者: @skipkayhil
- 概要 (1-2文で)
ActiveRecordで、ネイティブのjson/jsonbカラムに対してserializeを JSON コーダー付きで宣言したときに、ActiveRecord::Coders::JSON経由だと二重エンコードされて壊れた JSON が保存される不具合を修正したPRです。JSONとActiveRecord::Coders::JSONなどJSONコーダーのバリエーションを、互換性チェックですべて正しく検知するようにしました。
- 変更内容の詳細
問題の背景
Rails では、以下の2つは事実上同じ「JSONコーダー」として扱われます。
serialize :data, coder: JSON
serialize :data, coder: ActiveRecord::Coders::JSONbuild_column_serializer は内部的にこれらを同じ JSON コーダーとして「正規化」して扱っていますが、型との互換性を確認するガード (type_incompatible_with_serialize?) は、「素の ::JSON 定数」にしかマッチしていませんでした。
その結果:
json/jsonbカラムに対して:
# これはエラーになっていた
serialize :col, coder: JSON # => ColumnNotSerializableError- しかし、等価なはずの:
# これはエラーにならず、JSONが二重エンコードされていた
serialize :col, coder: ActiveRecord::Coders::JSONという不整合が発生していました。
ネイティブ JSON カラムに対してさらに JSON シリアライザを噛ませると:
- ActiveRecord 型: JSON 型として一度エンコード
serializeレイヤー: さらに JSON としてエンコード
という「二重エンコード」が起き、DB上には "\"{\\\"foo\\\":\\\"bar\\\"}\"" のような「JSON文字列としてのJSON」が保存され、他クライアントからは文字列として見えてしまう、というデータ破壊が起きていました。
修正内容
type_incompatible_with_serialize? の互換性チェックを、「正規化済みの coder」を見るように変更しています。
- もともと:
coder: JSONのときだけ::JSON定数にマッチしてガードが働いていたcoder: ActiveRecord::Coders::JSONなど、他の表記ゆれは見逃されていた
- 修正後:
build_column_serializerが内部で行う「coderの正規化」後の値に対して判定を行う- そのため、
JSON/ActiveRecord::Coders::JSONなど、どのバリエーションでも「JSONとして扱われる coder」は同じ扱いになり、ネイティブjson/jsonbカラムには使えない、というチェックが統一的に効く
コード上の変更は activerecord/lib/active_record/attribute_methods/serialization.rb のわずかな比較ロジックの修正で、ロジックの位置はそのまま、参照する coder を「正規化済み」に変更した形です。
テスト
activerecord/test/cases/json_shared_test_cases.rb にテストが追加されています。ポイントは:
- ネイティブ
jsonorjsonbカラムに対して:serialize :col, coder: JSONserialize :col, coder: ActiveRecord::Coders::JSON
- などのケースが、いずれも
ColumnNotSerializableErrorとなることを検証
これにより、「どの JSON コーダー表記を使っても、ネイティブ JSON カラムとの組み合わせは防がれる」という期待される挙動が自動テストで保証されます。
CHANGELOG
activerecord/CHANGELOG.md に、バグフィックスとして追記されています。内容としては:
- ネイティブ JSON カラムと JSON コーダー付き serialize の組み合わせで、特定の coder 表記のときに二重エンコードが起こっていた問題を修正した、という旨。
- 影響範囲・注意点
影響範囲
- 対象バージョン以降の Rails では、以下のようなコードは 例外が発生 するようになります:
class User < ApplicationRecord
# users.profile が json/jsonb カラムの場合:
serialize :profile, coder: ActiveRecord::Coders::JSON # <= 以前は通っていたが、今後は ColumnNotSerializableError
end- 以前は「サイレントに二重エンコードされていた」コードが、本来意図されていた通りに「即座にエラーで気づける」ようになります。
既存アプリへの注意点
すでに壊れたJSONが入っている可能性
過去にjson/jsonbカラムにserialize+ JSON coder を組み合わせて使っていた場合、DB上に二重エンコードされたデータが残っている可能性があります。移行時には:- 該当するモデル・カラムを洗い出す
- 二重エンコードされたレコードを検出 (
JSON.parseを2回通さないと正常形にならない等) - 必要に応じて一括で修正マイグレーションを書く
コード修正の方向性
ネイティブ JSON カラムを使っている場合、基本的にserializeは不要であり、以下のいずれかに寄せるのが推奨です:serializeを削除して、カラムは普通に Hash / Array をそのまま読み書きする:ruby# NG (今後は例外): serialize :settings, coder: JSON # settings が json/jsonb カラムのとき # OK: # 何も指定せず、settings はそのまま Hash として扱うどうしてもカスタムシリアライゼーションが必要なら、ActiveRecord の Attribute API (
attribute :settings, :json, default: {}等) や Value Object を検討する。
ライブラリ・エンジン側の互換性
外部ライブラリやエンジンがserializeと JSON カラムを組み合わせて使用している場合、この修正によってColumnNotSerializableErrorが発生するようになるかもしれません。その場合:- ライブラリ側でネイティブ JSON カラムと
serializeの併用をやめる - あるいは、
textカラム +serializeなど、型構成を変える
といった対応が必要になります。
- ライブラリ側でネイティブ JSON カラムと
- 参考情報 (あれば)
- 本PR: https://github.com/rails/rails/pull/57611
- 関連PR (re-open元): #57609
- 類似の仕様・制約:
- Rails ガイド「Active Record のシリアライゼーション」
https://guides.rubyonrails.org/active_record_querying.html#serialization - 「ネイティブの json/jsonb カラムと
serializeの組み合わせは非推奨/サポート外」という設計方針に沿った修正といえるため、今後は Attribute API・ネイティブ型優先の方向に寄せると安全です。
- Rails ガイド「Active Record のシリアライゼーション」
#57601 Fix update_all / delete_all ignoring group/having (updates/deletes every row)
マージ日: 2026/6/6 | 作成者: @55728
- 概要 (1-2文で)
group/havingを含むがjoins/limit/offset/orderを含まない Relation に対してupdate_all/delete_allを呼ぶと、HAVINGが完全に無視されテーブル全行が更新・削除されてしまう不具合を修正した PR です。Arel の SQL 生成ロジックを修正し、純粋なgroup/havingケースでもサブクエリ経由で PK ベースに対象レコードを絞り込むようにしました。
- 変更内容の詳細
問題の挙動
以下のようなクエリを想定します:
# posts: (low, 0), (mid, 3), (high, 9)
Post.where(id: posts)
.group("posts.id")
.having("MAX(legacy_comments_count) >= 3")
.update_all(title: "updated")期待される動作は、「HAVING MAX(legacy_comments_count) >= 3 を満たすグループ (= レコード) だけを更新する」ことです。しかし、これまでの実装だと:
生成される SQL (全アダプタ共通) が概ね以下のようになり:
sqlUPDATE "posts" SET "title" = ? WHERE "id" IN (...)GROUP BY/HAVINGは完全にドロップされる結果として、
HAVINGを満たさない行も含め、WHERE id IN (...)にマッチする行がすべて更新・削除される
特に問題なのは:
- エラーにもならず
- deprecation warning も出ず
- サイレントに「対象行が増える」形でバグっていたこと
です。
原因の詳細 (Arel レベル)
update_all / delete_all は内部的には Arel の UpdateManager / DeleteManager を組み立てた上で、Arel::Visitors::ToSql#prepare_update_statement(および類似の delete 側)で最終的な SQL に変換されます。
この prepare_update_statement は、次のような条件で「サブクエリ (PK サブセレクト) で絞り込むかどうか」を決めます:
def prepare_update_statement(o)
if o.key && (has_limit_or_offset_or_orders?(o) || has_join_sources?(o))
# ... `WHERE pk IN (SELECT pk ... GROUP BY ... HAVING ...)` を組むパス
else
# ... それ以外 (素の UPDATE/DELETE を吐く)
end
endしかし、ここで考慮されているのは
has_limit_or_offset_or_orders?(o)has_join_sources?(o)
のみであり、has_group_by_and_having?(o) が考慮されていません。
has_group_by_and_having? 自体はすでに to_sql.rb に定義されているヘルパですが、トリガ条件に組み込まれていなかったため、**「group/having だけを持つ Relation」**は else 側に落ち、素の UPDATE / DELETE が生成される → GROUP BY / HAVING が無視される、というバグになっていました。
なお:
joins + group + havingのケースはhas_join_sources?に引っかかるため、すでに PK サブクエリ経由のパスを通っており、期待どおりの挙動になっていた- 「join なし group/having あり」のパスはテストが存在せず、どのアダプタでも未検証だった
という背景があります。
修正内容
prepare_update_statement(および delete 側相当)のトリガ条件に has_group_by_and_having?(o) を追加しました:
if o.key && (
has_limit_or_offset_or_orders?(o) ||
has_join_sources?(o) ||
has_group_by_and_having?(o)
)
# PK サブクエリを使うパスへ
endこの結果、join が無い純粋な group/having のケースでも、joins + group と同様に「PK サブセレクト形式」で SQL が生成されるようになります。
修正後の SQL のイメージ:
UPDATE "posts" SET "title" = ?
WHERE ("posts"."id") IN (
SELECT "posts"."id" FROM "posts" WHERE "id" IN (...)
GROUP BY "posts"."id"
HAVING (MAX(legacy_comments_count) >= 3)
)このサブクエリ生成ロジック (build_subselect) は元から groups / havings をコピーするよう実装されているため、HAVING 条件が正しく適用されるようになります。
テストの追加
以下の 2 つのテストが追加されています:
test_update_all_with_group_by_and_having_without_joinstest_delete_all_with_group_by_and_having_without_joins
どちらも:
joinsを使わずgroup/havingのみを用いてHAVINGを満たすレコードだけが更新・削除され、満たさないレコードはそのまま残る
ことを検証する内容です。
これらのテストは:
- sqlite3
- postgresql
- mysql2
の各アダプタで「修正前は失敗(全行更新/削除)→修正後は成功」することが確認されています。
ファイル変更としては:
activerecord/lib/arel/visitors/to_sql.rb- 条件式を
... || has_group_by_and_having?(o)に拡張
- 条件式を
activerecord/test/cases/relation/update_all_test.rb- 上記の pure group/having 用のテスト追加
activerecord/test/cases/relation/delete_all_test.rb- 同様に delete 用テスト追加
activerecord/CHANGELOG.md- バグ修正の履歴追記
- 影響範囲・注意点
挙動が変わるケース
次のようなクエリを書いていた場合、実行結果が変わります:
Post.group(:id)
.having("MAX(legacy_comments_count) >= 3")
.update_all(title: "updated")従来:
HAVINGが無視され、GROUP BYも消え- 実質的に
UPDATE posts SET title = 'updated'(+もしあれば where 条件)に近い挙動だった
今後:
HAVINGを満たすグループ(=レコード)にだけ更新がかかる
つまり、このバグに「依存していた」コードは壊れますが、依存している方が危険なため、正しい挙動に揃えられたと言えます。
アダプタ間の影響
- SQLite / PostgreSQL / MySQL の 全てで発生していた問題を、一箇所(Arel::ToSql の基底実装)を直すことで一括で解消しています
- MySQL は独自の
prepare_update_statementオーバライドを持ちますが、GROUP BY/HAVINGだけのケースは元々superにフォールバックしていたため- 今回の修正で
superの挙動が正しくなり、MySQL も同様に修正されます
GROUP BY の意味論上の注意
この PR は「HAVING がドロップされる」という致命的な問題を直すものですが、GROUP BY の意味論そのものは既存の joins + group パスに合わせただけで、DB 依存の部分はそのままです。
- 主キー、または主キーを一意に決定できる superkey で GROUP BY している場合
→ 「どのレコードが更新されるか」は明確(バグ修正でより直感的になっただけ) - 主キーを一意に決定しない列で GROUP BY している場合(例:
group(:status))- SQLite /
ONLY_FULL_GROUP_BYを無効にした MySQL など: 「各グループでどの行が選ばれるか」があいまい(従来からそう) - PostgreSQL や
ONLY_FULL_GROUP_BY有効な MySQL: そもそも SELECT 時点でエラーになり得る
- SQLite /
この PR はあくまで「HAVING を無視して全行を更新・削除してしまう」というバグを消すもので、これらの GROUP BY の仕様・DB 差異は変更しません。
マイグレーション時の注意
- 既に本番で
group/havingを伴うupdate_all/delete_allを使っている場合:- テストやログを見直し、「これまで意図せず全行更新・削除していたところがないか」「今後の挙動が本来の期待に合うか」を確認する価値があります
- 逆に、「一部だけ更新したかったのに、なぜか全部更新されている」という現象に悩まされていた場合:
- この修正で改善する可能性があります
- 参考情報 (あれば)
- この PR は #43465 で導入された「
joins + groupな Relation のupdate_all/delete_allに対する PK サブクエリ変換」を、純粋な group/having のケースにも拡張するものです。 has_group_by_and_having?自体は #43465 の一部 (4acb6660e2) で追加されており、本来は今回の条件に含まれるべきだったものの、当時はトリガ条件への組み込みが抜けていた「取りこぼし」を埋めた形です。- 変更行数はわずか(実質条件追加 + テスト)ですが、
- 影響は「データ破壊バグの予防」という意味で非常に大きいため、
group/havingを使う更新・削除ロジックのあるアプリではリリースノート・CHANGELOG を確認の上、アップグレード後の動作確認を推奨します。
#57605 Reset the memoized finder_needs_type_condition? flag on reset_column_information
マージ日: 2026/6/6 | 作成者: @55728
- 概要 (1-2文で)
reset_column_information/reload_schema_from_cache実行後も、STI 用のフラグfinder_needs_type_condition?の結果が古いまま残ってしまう不具合を修正する PR です。これにより、マイグレーションなどでtypeカラムを追加・削除した後に、STI 条件が効かなくなる/クエリがクラッシュする問題が解消されます。
- 変更内容の詳細
何が問題だったか
ActiveRecord の単一テーブル継承(STI)は、「このモデルに inheritance_column(デフォルトは type)が存在するか」を見て、クエリに WHERE type = 'Cat' のような条件を自動で付与するかどうかを finder_needs_type_condition? で判定しています。
この判定結果はインスタンス変数 @finder_needs_type_condition に メモ化 されており、通常はパフォーマンス上の最適化として妥当です。しかし、そのメモは以下のときにしかリセットされていませんでした。
- サブクラス定義 (
.inherited) 実行時
一方で、マイグレーション中などでよく使われる以下の API:
Model.reset_column_information- 内部的に呼ばれる
ModelSchema#reload_schema_from_cache
は、カラム情報まわりのキャッシュを多数リセットするにもかかわらず、@finder_needs_type_condition は消していなかったため、
- スキーマ変更前の状態に基づいた「STI 要 / 不要」判定が、その後もずっと使われ続ける
という状態になっていました。
その結果、2パターンのバグが起きます。
パターン1: もともと type カラムが無く、その後追加した場合
Cat.finder_needs_type_condition? # => false がメモ化される (type カラムがまだ無い)
connection.add_column :animals, :type, :string
Animal.reset_column_information
Cat.finder_needs_type_condition? # まだ false (本当は true になるべき)
Cat.create!(name: "felix").type # => nil # STI 用 type が保存されない
Cat.count # animals 全件を数える (WHERE type が付かない)影響:
- STI が 静かに無効化 される
- サブクラスのスコープが全行に広がる
createしてもtypeが書き込まれず、データ破損(誤った discriminator 値) につながる
パターン2: もともと type カラムがあり、その後削除した場合
Lion.count # => true がメモ化される (STI 有効)
connection.remove_column :zoos, :type
Zoo.reset_column_information
Lion.count # => "no such column: zoos.type" でクラッシュ影響:
- すでにメモ化された「STI 必要」フラグにより、もはや存在しないカラム
zoos.typeに対してWHEREが付与され、SQL エラーで落ちる
どちらも reset_column_information の利用例(マイグレーション中に change_table などと組み合わせるパターン)が公式にドキュメントされているため、実運用でも発生しうる不具合です。
修正内容
Inheritance#reload_schema_from_cache において、既に行っている他の STI 関連キャッシュのリセットに加え、@finder_needs_type_condition もリセットするようにしました。
疑似コード:
# activerecord/lib/active_record/inheritance.rb
def reload_schema_from_cache(*)
@finder_needs_type_condition = nil # ← これを追加
super
endこれにより、
- スキーマ変更前に一度
finder_needs_type_condition?が呼ばれてメモ化されていても、 - マイグレーション等で
typeカラムを追加・削除し、 reset_column_information/reload_schema_from_cacheを呼べば、
次回の finder_needs_type_condition? 呼び出し時に、最新のテーブル定義を元に再判定 されるようになります。
テストの追加
InheritanceFinderNeedsTypeConditionTest を追加し、以下のシナリオをテストしています。
- 一時テーブル上に STI 用のモデル群を定義
inheritance_columnが まだ存在しない状態でfinder_needs_type_condition?を呼んでメモ化- その後、
typeカラムを追加 reset_column_informationを実行- 期待される挙動を検証:
finder_needs_type_condition?が true になるcreateするとtypeにサブクラス名が保存される- サブクラスからの
where/countでWHERE type = ...が正しく付与される
テーブル定義を途中で変更する都合上、MySQL では DDL が暗黙コミットを伴うため、テストクラスでは use_transactional_tests = false が指定されています。
テストは以下のアダプタで fail → pass を確認済み:
- sqlite3
- postgresql
- mysql2
既存の関連テスト (inheritance_test, reload_models_test, base_test) もすべてグリーンです。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは、STI を使っていて かつ以下のような操作を行うアプリケーションです。
- マイグレーション中に
typeカラムを追加・削除し、その場でreset_column_informationを呼ぶ - ランタイム中にテーブルの inheritance カラム周りを変更して
reset_column_informationで反映させる
- マイグレーション中に
この PR により、これらのパターンで:
- STI が無効のままになってしまう
- 存在しない
typeカラムへの参照で SQL エラーになる
といった不具合が解消されます。
既存コードへの影響(互換性)
- 挙動としては「本来そうあるべきだった STI の判定に修正される」ものなので、正常なコードへの悪影響は基本的にありません。
- ただし、もし「
typeカラムを追加したのにreset_column_information後も STI が効かない」という バグに依存した挙動 を前提にしているコードがある場合、それは修正の対象になります(そのような依存は避けるべきです)。
パフォーマンス
reload_schema_from_cache実行時にメモがクリアされることで、次回のfinder_needs_type_condition?呼び出し時にだけ 再計算が発生します。- これは既に他のスキーマ関連キャッシュでも行われているパターンであり、通常のアプリケーションでパフォーマンス問題になることはまずありません。
- 参考情報 (あれば)
- 本 PR が扱う問題は、以前から議論されている「
inheritance_column=変更後の古いメモによる不整合」の一種です。- 関連 issue: #31475(属性メソッドのメモ化リセット問題)
reset_column_information/reload_schema_from_cacheを用いた動的なスキーマ変更を行う場合は、- STI(
typeカラム) - 属性メソッド
- その他スキーマ依存のメモ まわりのキャッシュが更新されているかを意識しておくと安全です。本 PR により、少なくとも
finder_needs_type_condition?については自動で正しく更新されるようになりました。
- STI(
#57603 Fix Relation#cache_key crashing on a loaded collection with a NULL timestamp
マージ日: 2026/6/6 | 作成者: @55728
- 概要 (1–2文で)
ActiveRecord::Relation#cache_key/cache_versionが、「すでにロード済みの関連にNULLの timestamp を含むレコードがある場合」に限ってArgumentErrorで落ちる不具合を修正した PRです。未ロード時とロード済み時でNULLの扱いが食い違っていたのを揃え、どちらでも同じ cache key を生成するようになりました。
- 変更内容の詳細
問題の具体的な挙動
updated_at(などの timestamp 列)に NULL を含むレコードがあるとき、同じ Relation に対して以下のような差が出ていました:
# topics: 1件は updated_at = <time>、もう1件は updated_at = NULL
scope = Topic.where(id: [a.id, b.id])
scope.cache_key # => "topics/query-...-2-2026..." (OK: 未ロード)
scope.load.cache_key # => ArgumentError: comparison of Time with nil failedつまり、
- Relation が「未ロード」のとき →
cache_keyは正常に生成される - Relation が「ロード済み」のとき →
Timeとnilを比較しようとしてArgumentErrorが発生
という、「ロード状態」に依存したクラッシュが起きていました。
特に collection_cache_versioning = false(デフォルト)のときに再現します。
原因
Relation#compute_cache_version 内で、ロード済みかどうかで timestamp の取得方法が変わっており、その実装差が原因です。
if loaded?
size = records.size
if size > 0
timestamp = records.map { |record| record.read_attribute(timestamp_column) }.max
end
else
# ... SQL: MAX(timestamp_column) ...
end- 未ロードパス:
- DB に対して
SELECT MAX(updated_at) FROM ...のような SQL を発行 - SQL の
MAXはNULLを無視するため、updated_atが[Time, nil, ...]でも問題なくTimeが返る
- DB に対して
- ロード済みパス:
- Ruby 側で
records.map { ... }.maxを実行し、[Time, nil, ...].maxになってしまう Array#maxはTimeとnilを比較できないためArgumentError: comparison of Time with nil failedが発生
- Ruby 側で
このため、
- 未ロード:
NULLを無視 → 正常 - ロード済み:
NULLを含んだままmax→ 例外
という不一致が生じていました。
修正内容
ロード済みパスの timestamp 取得部分を、filter_map を使って NULL を除外するように変更しています。
# 変更前
timestamp = records.map { |record| record.read_attribute(timestamp_column) }.max
# 変更後
timestamp = records.filter_map { |record| record.read_attribute(timestamp_column) }.maxfilter_map はブロックの結果が「truthy のものだけを取り出す」のと同時に「結果の配列を返す」メソッドです。
read_attribute(timestamp_column)がnil→ 配列に含めないTime(truthy) → 配列に含める
結果として、records から「nil を含まない Time だけの配列」を作り、その .max を取るようになります。これは SQL の MAX(updated_at) と同じ挙動(NULL 無視)です。
all-NULL ケースの扱い
- すべてのレコードの
updated_atがNULLだった場合:- 未ロードパス:
MAX(updated_at)はNULL→ Ruby 側ではnil - ロード済みパス:
filter_mapにより空配列[]に対して.max→nil
- 未ロードパス:
となり、どちらのパスでも timestamp == nil で揃います。
元々 all-NULL の場合は nil が返っており、その挙動は維持されています。
テスト
activerecord/test/cases/collection_cache_key_test.rb に以下のテストが追加されています:
test "cache_key for a loaded relation with a NULL timestamp matches the unloaded key" do
# Topic は updated_at が nullable
# どれか1件の updated_at を update_all で NULL にする
# (create!(updated_at: nil) だとタイムスタンプコールバックに上書きされるため)
# そのうえで:
#
# - 未ロードの scope.cache_key
# - load 済みの scope.load.cache_key
#
# が一致することを検証
end- sqlite3 / postgresql / mysql2 で fail → pass を確認済み
collection_cache_key_test,cache_key_test,integration_testもグリーン
CHANGELOG にもこの修正が追記されています。
- 影響範囲・注意点
- 影響を受けるケース
cache @collectionなど、View で「一度その Relation をループ等でロードしたあとに cache する」パターン- ロード前:
@collection.cache_keyは成功 - ロード後: 同じコードが
ArgumentErrorで落ちる
- ロード前:
- モデルの timestamp (デフォルトでは
updated_at) がNULLになり得る場合- 手動で
update_all(updated_at: nil)しているケースなど
- 手動で
- この PR により:
- ロードされているかどうかに関わらず、
NULLを含む collection に対するcache_key/cache_version呼び出しが安定して動作する - 未ロード・ロード済みの両方で「非 NULL timestamp の最大値」を使った cache key が生成される
- ロードされているかどうかに関わらず、
- 互換性・副作用
NULLを含む collection で、従来は未ロード時とロード済み時で cache key が異なっていた可能性があります(ロード済み時は例外で落ちるため、実際に差異が利用されていた可能性は低い)- この PR によって「両方のパスで同じ key になる」ため、むしろ期待通りに安定化したと考えて良いです
filter_mapの導入以外にロジックの変更はなく、通常の timestamp がすべて埋まっているケースでは動作の違いはありません
- 参考情報 (あれば)
- 対象メソッド:
ActiveRecord::Relation#cache_key,#cache_version, 内部のcompute_cache_version - 典型的な利用箇所:
Action View の
cacheヘルパーで Relation をそのまま渡すコード:erb<% cache @topics do %> <% @topics.each do |topic| %> ... <% end %> <% end %>上記のように、
eachなどで一度ロードされた直後にcacheを呼ぶ場合、このバグの影響を受けていました。
この PR により、nullable な timestamp カラムを持つモデルを Relation ごとキャッシュしても、ロード状態によらず安全に cache_key / cache_version を利用できるようになっています。
#55061 [ci skip] Add docs for error context middleware
マージ日: 2026/6/5 | 作成者: @dersam
- 概要 (1-2文で)
Rails ガイドの「エラー報告」ドキュメントに、ActiveSupport::ErrorReporterの「error context middleware」(エラーコンテキスト用ミドルウェア)の使い方が追記された PR です。コードの振る舞い変更はなく、ドキュメントのみの追加です。
- 変更内容の詳細
※ 実際の diff は guides/source/error_reporting.md に 32 行追加のみで、機能追加ではなく解説追加です。以下は PR 説明と既存機能から推測した、追加された内容の要点です。
2-1. error context middleware とは何か
ActiveSupport::ErrorReporterには、エラー報告時に「コンテキスト情報」(ユーザーID、リクエスト情報、マルチテナントのテナントIDなど)を付与するための仕組みがあります。- その一つとして、「error context middleware」を定義し、Rack ミドルウェアのようにリクエスト処理の前後でコンテキストを自動設定できる機構があり、これについて Rails ガイドに説明が追加されています。
- これにより、アプリケーション全体で一貫してエラーコンテキストを付けるベストプラクティスがガイドに明文化されました。
2-2. 想定されるサンプルコードのイメージ
Rails ガイドによくあるスタイルを踏まえると、以下のような内容が追加されていると考えられます(あくまでイメージコード):
# config/initializers/error_reporter.rb
Rails.error.configure do |config|
# 例: error context middleware を登録
config.add_context_middleware do |context|
# 現在のスレッドローカルや Current オブジェクトから情報を引き出して
# コンテキストに追加する
if Current.user
context[:user_id] = Current.user.id
end
if Current.request_id
context[:request_id] = Current.request_id
end
end
endあるいは、Rack ミドルウェアやコントローラを通じてコンテキストを設定する例もガイドに書かれている可能性があります:
class ErrorContextMiddleware
def initialize(app)
@app = app
end
def call(env)
request = ActionDispatch::Request.new(env)
Current.request_id = request.request_id
Current.user = find_current_user(request)
@app.call(env)
ensure
# スレッドローカルを必ずクリア
Current.reset
end
end
Rails.application.config.middleware.use ErrorContextMiddleware上記のような「Current オブジェクト + error context middleware」で、Rails.error.report(e) を呼んだ時に自動的に user_id や request_id などがコンテキストとしてレポートされる、という流れをガイドで説明していると考えられます。
2-3. ActiveSupport::ErrorReporter との関係
ActiveSupport::ErrorReporterはRails.errorから利用されるエラー報告インターフェースで、#report,#handle,#recordなどの API を提供します。- 今回のガイド追加は、このレポーターにコンテキストを注入するための「middleware 的なフック」の存在と、その使い方を示すものです。
- 既に存在していた API/機構に対する「公式な説明」を Rails ガイドに追加した位置づけです。
- 影響範囲・注意点
- 影響範囲はドキュメントのみであり、アプリケーションコードや Rails 本体の挙動には変更はありません。
- ただし、ガイドの更新により「推奨されるエラーコンテキストの設定方法」がより明確になったため、今後新規コードや既存コードのリファクタリング時に:
Rails.errorやActiveSupport::ErrorReporterを直接呼び出す箇所で、- 「毎回手でコンテキストハッシュを渡す」実装から、
- グローバルな error context middleware /
Currentパターンによる一元管理にリプレースしやすくなります。
- コンテキストに格納する情報は個人情報・機密情報になりがちなので、ガイドにも「不要な個人情報を含めない」「ログや外部エラー追跡サービスに送信されることを意識する」といった注意点が記載されている可能性があります。その点を踏まえた設計が必要です。
- 参考情報 (あれば)
- PR で参照されているプレビュー:
https://350ae6eb.rails-docs-preview.pages.dev/guides/error_reporting#setting-context-with-error-context-middleware - 関連クラス:
ActiveSupport::ErrorReporter(Rails.errorの実体)ActiveSupport::CurrentAttributes(Currentオブジェクト用)
- 既存ドキュメント(英語正式版に統合される見込み):
- Rails Guides: Error Reporting (error_reporting.md)
#57592 [RF-Docs] [ci-skip] Active Job Basics guide (#57101)
マージ日: 2026/6/5 | 作成者: @p8
- 概要 (1-2文で)
- Active Job Basics ガイド全体の構成を大幅に見直し、特に Solid Queue とキューイング周りの説明を、より概念的かつ実務的な内容に再編したドキュメント改善 PR です。
- 既存内容の重複や価値の薄いセクションを削除・統合し、キュー名/優先度/コールバック/失敗ジョブの扱いなどを一貫した流れで理解できるように整理しています。
- 変更内容の詳細
ガイド全体構成の再設計
- トップレベル・サブセクションを全面的に組み替え、「何を」「どの順で」学ぶかが分かりやすくなるよう整理。
- 小さな独立セクション(Job Testing, ActionMailer, Internationalization など Active Job ガイドとしては価値が薄いもの)を削除または他のセクションに統合。
- セクション名の見直し:
- “Create and Enqueue Jobs” → “Creating and Enqueuing Jobs” など、Rails Guides 全体の慣例に合わせてリネーム。
- “Queues” セクションは、実際に書いている内容に合わせ “Enqueuing Jobs”的な構造に再編(下位に「キュー名」「ダイナミックなエンキュー」「優先度」「バルクエンキュー」などをぶら下げる形に)。
Solid Queue セクションの全面書き直し
位置づけ:
- Solid Queue を「非同期なインプロセスのキューイング・バックエンド」として、用語・定義を明確化。
- 既存ガイドが GitHub の README の焼き直しになっていた部分を削除し、README ではなく「ガイドとして知るべき設計・特徴・ユースケース」にフォーカスした内容に変更。
内容面での強化ポイント:
- コンセプト説明:
- Solid Queue がどのように設計されているか、他バックエンドとどう違うか(例: DB を使う・インプロセスで動く・Rails 標準でサポートなど)を概念的に説明。
- 他のキューイングバックエンドとの比較観点(外部サービス型 vs アプリ内 DB 型、運用コスト、可観測性など)を整理。
- 設定例の強化:
database.ymlでの Solid Queue 用の接続定義例を、キュー用の行が目に付きやすいようにハイライトして説明。- development / production の両方について、どのように queue DB を分けるか(または共用するか)を示す。
- キュー定義と優先度:
- 「定義されたキューの順序がジョブの priority より優先される」点を、具体的な例で説明し直し。
- 例:
queues: [ "production", "background" ]を定義しているときに、priority とキューの組み合わせで実際の処理順がどう決まるか、を表に近い形やサンプルで解説。
- 例:
- 「定義されたキューの順序がジョブの priority より優先される」点を、具体的な例で説明し直し。
- 警告表現の統一:
- 以前は
⚠️絵文字で警告を表現していた箇所を、Guides 標準の WARNING ブロック(注記ブロック)を使うよう変更。
- 以前は
- コンセプト説明:
ジョブのエンキューとキュー名・優先度
Enqueuing Jobs / Queues 関連の再構成
セクション名変更と再構造化:
- 「Queues」という抽象的タイトルをやめ、実際に扱う内容(ジョブのキューイング)に合わせて再構成。
- 下位セクションを “Naming Queues”, “Dynamic Enqueuing”, “Priority”, “Bulk Enqueuing” のように整理。
キュー名の付け方(Naming Queues):
- 大量のキューを運用する場合の命名規則について、実務的なガイダンスを追加。
- 特に「レイテンシベースのキュー名」(例:
critical,high,default,low)のようなスキームを「よく使われる選択肢」として紹介。- Gusto 社のブログ “Scaling Sidekiq at Gusto” を参照しつつ、「推奨」というより「一つのメジャーなパターン」として紹介する、というトーンに調整。
ダイナミックなエンキュー(Dynamic Enqueuing):
- 実行時にキュー名を切り替えたり、状況に応じて queue / priority を変えるパターンの説明をセクションとして分離。
- 例:ruby
MyJob.set(queue: :low).perform_later(record) MyJob.set(wait: 5.minutes, queue: :high).perform_later(record)
優先度(Priority):
- これを独立セクションではなく、上記「Enqueuing Jobs」配下のサブセクションに移動し、キュー名と合わせて「ジョブの実行順序に効くパラメータ」という文脈で説明。
- 例:rubyといった形で、
class CriticalJob < ApplicationJob queue_as :critical priority 10 endqueue_asとpriorityの関係性がわかるような説明になっている。
バルクエンキュー(Bulk Enqueuing):
- “Enqueuing Jobs” セクションの一部(サブセクション)として取り込み。
- 上部に別見出しを立ててリンクだけする形ではなく、「2.3 Enqueue Jobs in Bulk」内にトップの説明をインライン展開するように修正。
- 無意味な文言削除:
- “
perform_all_lateris a top-level API on Active Job.” など、情報量が薄い説明を削除し、具体的な使い方に紙幅を割く。
- “
ジョブ定義・引数・コールバック
Supported Types for Arguments:
- 引数に渡せる型の一覧・ルールを「ジョブ定義」関連のセクションに移動。
Enqueuing Jobsというより「Job の API 面での仕様」なので、Defining a Job的なセクションのサブセクションとして再配置。
コールバック(Callbacks):
- 以前は例だけで説明が薄かったのを、全ての利用可能なコールバック(
before_enqueue,around_perform, など)について、短いコードスニペット付きで明示。- 例:ruby
class MyJob < ApplicationJob before_enqueue :log_enqueue after_perform :notify private def log_enqueue Rails.logger.info("Enqueuing #{job_id}") end def notify # ... end end
- 例:
- コードブロック内で、実際にコールバックに関わる行だけを行ハイライトして、どこがポイントか読み取りやすくした。
- 以前は例だけで説明が薄かったのを、全ての利用可能なコールバック(
コールバックの中断(halting callbacks):
- 別 PR(#53541)で追加される予定だった「コールバックの halt」機能を、このガイドに明示的に説明するセクションを追加。
- 例として、
throw :abortあるいは専用 API による処理中断と、そのときキューイング/実行がどう扱われるかを説明しているはずです。
例外処理・デバッグ・失敗ジョブ
“Exceptions” セクションのリネーム:
- タイトルを “Handling Failed Jobs” 的な名前に変更し、「例外」ではなく「失敗したジョブをどう扱うか」という読者視点のテーマに合わせた。
- 失敗時のリトライ戦略やハンドラ、ロギング・通知など、実際の運用に直結する項目をこのセクションに寄せる形に調整。
Debugging セクション:
- 独立した「デバッグ」章ではなく、「Handling Failed Jobs」周辺にマージし、失敗時の調査・ログ・スタックトレースの確認方法をまとめて扱う流れに。
- verbose enqueuing logging:
- development ではデフォルトで有効なため、単独のセクションとして扱うほどの重みはないと判断し、扱いを縮小/統合。
不要・重複セクションの削除
ActionMailer:
- Active Job 経由でメール送信する説明は、Action Mailer ガイドにすでに存在しており、Active Job ガイドで重複して持つのは冗長なため削除。
- 同じ理屈で、ActionMailbox (
incinerate_later)、ActiveStorage (purge_later) などもここでは扱わず、各フレームワークガイドに任せる構成に。
Internationalization:
- 実質的にメール送信や文言の I18n 寄りの内容であり、Active Job のコアテーマではないため削除。
Job Testing:
- ここで軽く触れる程度の情報は価値が薄く、かつ他フレームワーク(Active Record, Action Controllerなど)も「テスト」専用サマリを持っていないことから、削除。
代替キューイングバックエンド
- Alternate Queuing Backends セクション:
- Solid Queue セクションとの位置関係を見直し。
- Rails Foundation ドキュメントの方針として Solid Queue を第一クラス市民として扱いたい一方で、他バックエンドの情報も隣接させて比較しやすいように、両者のセクションを(少なくとも論理的には)近接させるよう再配置。
- 影響範囲・注意点
コード挙動への影響:
- この PR は guides(ドキュメント)のみ変更であり、Rails のランタイムコードには一切触れていません。そのためアプリケーション動作への直接的な影響はありません。
情報ソースとしての影響:
- Active Job の公式ガイドとして参照する際、以下が変わります:
- Solid Queue については README ではなく、「ガイドとしての整理された概念説明」を読むことになる。
- ActionMailer, I18n, Job Testing など周辺事項は、このガイドからはリンク/言及が減り、各専用ガイドに誘導される構成になる。
- 古いバージョンのガイドを前提にブログ・社内 Wiki 等で参照している場合、セクション構成や見出し名が変わっているため、リンク切れや説明の位置変化に注意が必要です。
- Active Job の公式ガイドとして参照する際、以下が変わります:
ドキュメント執筆・翻訳への影響:
- セクション構成が大きく変わったため、ローカル言語版(日本語訳など)を持っている組織は、翻訳の差分反映コストが高くなります。
- 特に Solid Queue, Enqueuing Jobs, Callbacks, Handling Failed Jobs 周りを、原文に合わせて再翻訳・再構成する必要があります。
- 参考情報 (あれば)
- この PR 自体:
- Solid Queue README(設計・詳細仕様):
- キュー名のレイテンシベース命名の参考記事:
- Scaling Sidekiq at Gusto — Latency-based queue naming:
https://engineering.gusto.com/scaling-sidekiq-at-gusto-3f9e3279e63
- Scaling Sidekiq at Gusto — Latency-based queue naming:
- コールバックの halting に関する関連 PR:
#57101 [RF-Docs] [ci-skip] Active Job Basics guide
マージ日: 2026/6/5 | 作成者: @bhumi1102
- 概要 (1-2文で)
Active Job BasicsガイドをRails Foundationのドキュメント方針に沿って全面的に再構成し、特にSolid Queueの説明を「設計・特徴・使い方」の観点から整理し直したPRです。細かい・重複したセクションを削除・統合し、キュー名/優先度/コールバック/失敗時の扱いなど、実務で重要なテーマごとに読みやすく再編成しています。
- 変更内容の詳細
全体構成の再編・統合
- ガイド全体のトップレベル・サブセクションを見直し、細切れだった章を統合して流れを整理。
- 「Queues」「Priority」「Bulk Enqueuing」「Supported Types」「Debugging」など、関連性が高いのにバラけていた内容を、「ジョブ定義」「ジョブのエンキュー」「失敗ジョブの扱い」といった大きめのテーマに再配置。
- 章タイトルをガイド全体の命名規則に合わせてリネーム
- 例: 「Create and Enqueue Jobs」→「Creating and Enqueuing Jobs」など。
Action Mailer / I18n / Job Testing の整理
- Active Job固有でない、もしくは他ガイドと重複していた以下のセクションを削除:
- ActionMailer連携(Action Mailerガイドでカバー済み)
- Internationalization(主にMailer文脈での話であり、Active Job固有ではない)
- Job Testing(他フレームワークに同等セクションがなく、内容的にも薄い)
- 代わりに、Active Jobのコア概念・使い方に紙幅を集中させる構成になっています。
Solid Queue セクションの全面書き換え
このPRの中心は、Solid Queueの説明を単なるREADMEの焼き直しから、「Rails標準のキューバックエンドとしての位置づけ・特徴・設計思想」を伝えるガイドに変えた点です。
コンセプト・設計の説明を重視
- Solid Queue を以下のような観点で説明する構成に変更:
- Railsに組み込みやすい「非同期・インプロセス・キューバックエンド」であること
- “queuing system” ではなく “queuing backend” という用語に揃え、Active Jobのバックエンドとしての役割を明確化
- 他のバックエンド(Sidekiq, Resqueなど)との違い
- DBベース、Railsとの親和性、インフラ依存の少なさなどの特徴を、概念レベルで説明
- 実運用で意識すべき点(キュー定義、優先度、ワーカー構成など)
- Railsに組み込みやすい「非同期・インプロセス・キューバックエンド」であること
- READMEの内容をそのままコピーするのではなく、
- 「どういうときに・どういう考え方でSolid Queueを選ぶか」
- 「設計上何がユニークか」 を中心に説明し、詳細なオプションなどはREADMEに委ねる構成になっています。
設定例の明確化
config/database.ymlの例で、Solid Queue用の接続/設定行がより目立つように記述を整理- PRコメントに基づき、queueに関する行を強調。
- 開発環境・本番環境両方の例を示し、「RailsアプリのDBと同一/別DBを使うケース」などの具体的イメージがしやすいよう編集。
キュー定義と優先度の関係を具体例で説明
- 「定義されたキューの順序がジョブのpriorityより優先される」という振る舞いを、より明確な例付きで説明。
- 例:
productionキューとbackgroundキューを定義したうえで、それぞれに priority が違うジョブが入っていても、キューの処理順が優先されることを図やサンプルコードで説明、という形に変更。 - これにより、「priorityを上げてもキュー順序に勝てない」ケースの混乱を防ぐ意図。
- 例:
警告スタイルの統一
- テキスト中で使われていた ⚠️ を、Railsガイド標準の WARNING ブロックに置き換え。
- セクション全体をWARNINGにするのではなく、本当に注意喚起が必要なポイントだけをブロックに分離し、可読性と意味合いを整理。
キューとエンキュー周りの整理
「Queues」→「Enqueuing Jobs」系への再構成
- 従来「Queues」というタイトルの下に混ざっていた内容を、実態に合わせて再分類:
- 「キューの命名(Naming Queues)」
- 「動的なエンキュー(Dynamic Enqueuing)」
- 「優先度(Priority)」※もとの独立セクションをここに統合
- 目的:読者視点では「キューというデータ構造の内部」よりも、「どうエンキューするか」「どう振る舞いを制御するか」が知りたいため、その観点で章を立て直し。
キュー命名のベストプラクティスへの言及
- Latency-based naming(レイテンシベースのキュー命名)を「よく使われるパターン」として紹介:
- 例:
mailers_fast,mailers_slow,critical,default,lowのように、処理の緊急度/レイテンシに応じたキューを分ける
- 例:
- Jean Boussierのコメントに従い、「唯一の正解としての推奨」ではなく「広く使われているオプション」として中立的に記述。
コールバック・ライフサイクルの改善
コールバック一覧とコード例の充実
- Active Jobの各種コールバック(
before_enqueue,around_perform,after_performなど)について、他のガイド同様に- 各コールバックの目的/タイミングの簡潔な説明
- 該当部分だけを行ハイライトで示したサンプルコード を追加し、理解しやすく整理。
- コールバックの種類だけ列挙されていた従来版と違い、「いつ・何をするのに向いているか」まで含めて分かりやすくなっています。
コールバックの「中断(halting)」の追加
- 独立PR (#53541) で追加予定だった、コールバックを途中で中断する挙動に関する説明セクションを統合。
- 例:
throw :abortによるbefore_enqueueの中断など - 中断した際にジョブがどう扱われるか(エンキューされない/実行されないなど)を明示。
- 例:
- コールバックまわりで実装側がハマりがちなケース(条件付き実行、バリデーション的な使い方)への対応も、ガイドだけで把握しやすくなっています。
バルクエンキュー (Bulk Enqueuing)
- 「Enqueuing Jobs」系セクションのサブセクションとして再配置し、ジョブ投入手段を一箇所にまとめる構成に変更。
perform_all_laterの説明から、意味の薄い文(「top-level API である」等)を削除し、実際の使い方にフォーカス。- 例:
- 複数のレコードに対して同種のジョブを一括エンキューするケース
- 単発の
perform_laterループとの違い(トランザクション境界・オーバーヘッド)などをコード例で示す形になっていると考えられます(差分行数的に詳細が増えている)。
- 例:
サポートされる引数型 (Supported Types for Arguments)
- もともと「Enqueuing Jobs」直下にあった/あるいはガイド後半に散在していた内容を、「ジョブ定義」の文脈に近いセクションへ移動。
- 目的:ジョブを定義する際に「どの型を引数に渡せるか」を意識して設計できるようにするため。
- Active Jobがシリアライズ可能な型(基本型、ActiveRecordモデル、GlobalID対応オブジェクト等)について解説するセクションとして整理されているはずです。
失敗ジョブとデバッグ
「Exceptions」→「Handling Failed Jobs」へリネーム
- セクションタイトルを「Exceptions」から「Handling Failed Jobs」に変更し、内容に即した名称に。
- リトライ、デッドレター(もし記載があれば)、ログの確認など、失敗時の扱いを中心に説明。
デバッグ内容の統合
- 独立していた「Debugging」セクションを「Handling Failed Jobs」配下に統合。
- Railsの開発環境では verbose なエンキューログがデフォルト有効であるため、これを「特別な設定」として扱う記述をやめ、失敗ジョブの原因究明の流れの中で自然に説明する構成に変更。
代替キューバックエンド (Alternate Queuing Backends)
- Solid Queueセクションと隣接するように再配置。
- Rails標準(Solid Queue)→他バックエンドという流れで読み進められるように。
- あるいは、Solid Queueをやや強調しつつも、最後に「他にもこういう選択肢があります」と紹介する位置づけ。
- 目的は、「Solid Queueがデフォルトである」ことを明確にしつつ、「Sidekiqなど他のエコシステムと連携する余地もある」ことを示すバランスをとること。
- 影響範囲・注意点
- コード(Active Job本体)には変更はなく、あくまでガイド(
guides/source/active_job_basics.md)のみの変更です。 - ただし、ガイドを準拠情報としているチーム・社内Wikiなどがある場合:
- セクション名・アンカー(
#queues→#enqueuing-jobsのような)変更により、リンク切れが発生する可能性があります。 - 「ActionMailer」「Internationalization」「Job Testing」などのサブセクションは削除されているため、それらへの直接リンクは確認が必要です。
- セクション名・アンカー(
- Solid Queueの説明が「設計や特徴中心」に変わったため、
- 以前READMEの内容を引用したような運用手順をガイド経由で参照していた場合、今後はSolid QueueのREADMEとの併用が前提になる箇所があります。
- コールバックの halting(中断)に関する記述が明示されたことで、これまで暗黙的に(or バグ扱いで)利用していたコードの意味がドキュメント的に「仕様」として整理されます。
- 新規実装時には、halting の有無を意識した設計がしやすくなる一方で、既存コードの振る舞い確認もしておくと安心です。
- 参考情報 (あれば)
- 対象ガイドファイル
guides/source/active_job_basics.md(+968 / -759 行の大幅変更)
- 関連PR
- コールバックの halting を扱うPR: https://github.com/rails/rails/pull/53541
- キュー命名に関する参考記事(ガイド内でも参照されているパターン)
- Scaling Sidekiq at Gusto: Latency-based queue naming
https://engineering.gusto.com/scaling-sidekiq-at-gusto-3f9e3279e63
- Scaling Sidekiq at Gusto: Latency-based queue naming
- Solid Queue README(詳細オプションやコマンドはガイドではなくこちらを参照する想定)
- https://github.com/rails/solid_queue (URLは一般的なパス。実際のリポジトリ名変更などには注意)
#57575 Clear the type column when removing a polymorphic has_one
マージ日: 2026/6/5 | 作成者: @55728
- 概要 (1-2文で)
polymorphic なhas_one関連を代入で外す(owner.child = nil/owner.child = other)際に、外部キーだけでなく<name>_typeも確実にNULLにするように修正した PR です。これにより、dependent: :nullify経由の「nullify」と挙動が統一され、一方向の polymorphichas_oneで発生していた「type カラムが古いまま残る」不整合が解消されます。
- 変更内容の詳細
これまでの挙動
対象は「polymorphic な has_one 関連」を、代入で「外す」ケースです:
owner.thing = other # 付け替え
owner.thing = nil # 削除こうした操作を行うと、子側のレコードにある「外部キー」カラム(thing_id など)は NULL になりますが、polymorphic の「type」カラム(thing_type)は以前の値が残っていました。
内部的には HasOneAssociation#nullify_owner_attributes が呼ばれますが、ここでは外部キーしか消していませんでした:
# activerecord/lib/active_record/associations/has_one_association.rb
def nullify_owner_attributes(record)
Array(reflection.foreign_key).each do |foreign_key_column|
record[foreign_key_column] = nil unless foreign_key_column.in?(Array(record.class.primary_key))
end
end一方、dependent: :nullify を指定して関連を切る場合は、ForeignAssociation#nullified_owner_attributes が使われます。こちらは type カラムも消します:
def nullified_owner_attributes
Hash.new.tap do |attrs|
Array(reflection.foreign_key).each { |foreign_key| attrs[foreign_key] = nil }
attrs[reflection.type] = nil if reflection.type.present?
end
endそのため、次の2パターンで挙動が食い違っていました:
owner.thing = nil- 外部キー:
NULL - type: 古い値のまま
- 外部キー:
has_one :thing, dependent: :nullify経由の nullify- 外部キー:
NULL - type:
NULL
- 外部キー:
特に、子モデル側が inverse の belongs_to を持たない一方向の polymorphic has_one で問題が顕在化します。逆方向の belongs_to があれば、そのロジックの中で type もリセットされるため、これまで多くのケースでは問題が「見えにくい」状態でした。
再現例
class Human < ActiveRecord::Base
has_one :face, as: :describable # Face(describable_id, describable_type)
end
class Face < ActiveRecord::Base
# NOTE: belongs_to :describable は定義しない(片方向)
end
h = Human.create!
h.face = Face.create! # describable_id + describable_type = "Human"
h.face = nil
face.reload
face.describable_id # => nil
face.describable_type # => "Human" # ← ここが古い値のまま修正内容
HasOneAssociation#nullify_owner_attributes に、polymorphic の type カラムも NULL にする処理を追加しました:
def nullify_owner_attributes(record)
Array(reflection.foreign_key).each do |foreign_key_column|
record[foreign_key_column] = nil unless foreign_key_column.in?(Array(record.class.primary_key))
end
# 追加された行
record[reflection.type] = nil if reflection.type.present?
end- polymorphic でない
has_oneの場合はreflection.typeがnilなので、この行は no-op になります。 - これにより、代入形式の nullify と
dependent: :nullifyの両方で、「id も type も消える」という一貫した挙動になります。
テスト
activerecord/test/cases/associations/has_one_associations_test.rb に以下のテストが追加されています:
test_replacing_a_polymorphic_has_one_nullifies_the_type_column
ポイント:
- 一方向 polymorphic
has_one(子側に inversebelongs_toがない)を使ったテストケース owner.child = nil実行後に、子の<name>_idと<name>_typeの両方がnilであることを検証- 変更前の
mainでは type が"...Owner"のままになりテストが落ちる - この修正によりテストがグリーンになる
- sqlite3 / postgresql / mysql2 すべてでテストスイートがパス
CHANGELOG.md にも、この挙動変更が追加されています。
- 影響範囲・注意点
影響を受けるケース
- polymorphic な
has_oneを定義しているモデル - その関連を「代入で外す」コードがある場合:
owner.child = nilowner.child = another_child
- 特に、子側が inverse
belongs_toを定義していない 一方向 polymorphichas_oneでこれまで挙動が変だったところが正しくなります。
- polymorphic な
これまでと挙動が変わる点
- 以前は「外部キーは
NULLになっているが type だけ古い値が残る」ため、データ的には不整合な状態でしたが、アプリ側の独自ロジックや直接 SQL を書く箇所で、この「stale な type カラム」に依存していた場合は挙動が変わる可能性があります。- 例:
WHERE describable_type = 'Human' AND describable_id IS NULLのようなクエリを「外れた Face だけを拾う」意図で書いていた場合など
- 例:
- Rails 的には今回の挙動が一貫性があり正しいと考えられるため、通常の関連操作に依存している限りは「バグ修正」として問題なく受け入れられる変更です。
- 以前は「外部キーは
非 polymorphic の
has_onereflection.typeがnilのため、挙動は一切変わりません。
dependent: :nullifyを使っている場合- もともと id + type ともに
NULLにしていたので挙動は据え置きです。 - 今回の変更で「代入で外した場合」との不整合が解消され、どのルートでも同じ状態になります。
- もともと id + type ともに
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57575
- 修正対象のメソッド:
activerecord/lib/active_record/associations/has_one_association.rb#nullify_owner_attributes
- 類似ロジック:
activerecord/lib/active_record/associations/foreign_association.rb#nullified_owner_attributes
- 関連テスト:
activerecord/test/cases/associations/has_one_associations_test.rb
- 挙動確認をしたい場合は、自分のアプリで問題になりそうな polymorphic
has_oneを定義して、owner.child = nil→ 子レコードをreload→<name>_id/<name>_typeの値を確認すると差分を把握しやすいです。
#57566 Fix MessagePack serialization of records with a time column
マージ日: 2026/6/5 | 作成者: @55728
- 概要 (1-2文で)
time型カラムを持つ ActiveRecord オブジェクトをActiveRecord::MessagePackでシリアライズするとNoMethodErrorが発生していた問題を修正した PR です。ActiveRecord::Type::Time::Valueを MessagePack の対象型として正式に登録することで、Rails.cache.write等でtimeカラムを含むレコードを安全にキャッシュできるようになりました。
- 変更内容の詳細
問題の内容
ActiveRecord::MessagePack を経由してレコードをシリアライズするとき、time 型カラムが存在すると以下のようなエラーが発生していました。
NoMethodError: undefined method 'to_msgpack' for an instance of ActiveRecord::Type::Time::Value原因は以下の通りです。
attributes_for_databaseはtimeカラムの値をActiveRecord::Type::Time::Valueオブジェクトにラップして扱う- これは
DelegateClass(::Time)を継承したクラス(Timeのデリゲート)
- これは
- 一方、MessagePack のファクトリは 厳密なクラス一致 で型を登録しており、
::Timeだけを登録していた - そのため
ActiveRecord::Type::Time::Valueは::Timeとみなされず、MessagePack 側で対応する拡張型が見つからずto_msgpack未定義エラーに落ちていた
datetime / date / decimal については、Time / Date / BigDecimal の素の値でシリアライズされていたため、この問題の影響を受けていません。
このバグは Rails 7.1 で MessagePack サポートが入った時点から存在していましたが、既存の message_pack_test.rb で使われているモデル(Post/Comment/Author/Binary)には time カラムが無かったため、これまでテストに引っかかっていませんでした。
修正内容
activerecord/lib/active_record/message_pack.rb にて、MessagePack ファクトリへ ActiveRecord::Type::Time::Value を拡張型として登録する処理が追加されました。
ActiveRecord::Type::Time::Valueを type ID 118 として登録- 既存の
Binary::DataやBase登録と同様に、再帰的 (recursive) な登録方法で行っている- これは、MessagePack でカスタムクラスを扱う際に、ネストされたオブジェクトを含めて適切にエンコード / デコードできるようにするためのパターンです
概念的には、次のような登録をしているイメージです(擬似コード):
# 118 番の拡張型として ActiveRecord::Type::Time::Value を登録するイメージ
factory.register_type(118, ActiveRecord::Type::Time::Value) do |time_value|
# 内部的には実際の Time としてエンコードし、復元時に適切に再構築する
end(実際のコードは既存の拡張型登録のスタイルに合わせて書かれており、Binary の登録と同様のパターンで実装されています。)
テスト追加・更新
activerecord/test/cases/message_pack_test.rb に以下のテストが追加・更新されました。
test_roundtrips_time_attributeTopicモデルを使い、そのうちbonus_timeというtimeカラムを持つ属性が MessagePack を経由してラウンドトリップ(serialize → deserialize)できることを検証- 修正前はここで
NoMethodErrorが再現し、修正後は正常にパスするテスト
test_enshrines_type_IDsの更新- 既存の拡張型 ID を固定化しておくテストに、type 118 を追加
- これにより、拡張型 ID の将来的な変更が、互換性に影響することを検知できるようになっている
また、activerecord/CHANGELOG.md にも、この修正が反映されています。
- 影響範囲・注意点
影響範囲
ActiveRecord::MessagePackを使う経路(特にRails.cache)で、timeカラムを持つレコードを扱うケース全般- 例: Solid Cache や他のキャッシュストアで ActiveRecord オブジェクトをそのままキャッシュしている場合
- これまで:
timeカラムがあるレコードをRails.cache.write(record)などすると、NoMethodErrorが発生
- この PR 適用後:
- 同じコードがエラーなく動作し、
time属性も含めて正しくシリアライズ / デシリアライズ可能
- 同じコードがエラーなく動作し、
互換性・注意点
- 新たに使用した拡張型 ID は 118 で、既存の type ID と衝突しないように選定されています
test_enshrines_type_IDsにより、ID 割り当てが将来変更された場合の破壊的変更を検出できるようになっています
datetime/date/decimalなど他の型の挙動は変更されていませんActiveRecord::Type::Time::Valueのシリアライズ形式が今後変わると互換性に影響する可能性があるため、拡張型 ID 118 の扱いはバージョン間互換の観点で慎重に扱う必要があります
- 参考情報 (あれば)
- 対象バージョン: Rails 7.1 以降の
ActiveRecord::MessagePack機能に潜んでいたバグの修正 - 関連箇所:
ActiveRecord::Type::Time::Valueは 2016年に導入されたDelegateClass(::Time)のサブクラス- MessagePack サポートは Rails 7.1 で導入
- 想定利用箇所:
Rails.cache(ActiveSupport::MessagePack::CacheSerializer経由)- Solid Cache を含む Rails 標準・外部のキャッシュストアでの ActiveRecord モデルキャッシュ
#57590 Fix order-dependent test failures with sqlite3_mem
マージ日: 2026/6/5 | 作成者: @ruyrocha
- 概要 (1-2文で)
ARCONN=sqlite3_memでのテスト実行時に、テスト実行順によって約3000件の失敗を引き起こしていた原因となる2つのテストを修正し、SQLite の in-memory DB 環境でもテストスイート全体が安定して通るようにした PR です。グローバルな SchemaCache の破壊と、:memory:DB 自体の破棄を招いていたテストの挙動を修正・制限しています。
- 変更内容の詳細
2-1. DirtyTest#test_field_named_field の修正 (SchemaCache 破損の解消)
問題点
DirtyTest#test_field_named_field というテストが、以下のような流れで動いていました:
create_table/drop_tableを使って一時的なテーブルを作成・削除ensureブロックでActiveRecord::Base.clear_cache!を呼び出し
ActiveRecord::Base.clear_cache! はグローバルな SchemaCache を空にした新しいインスタンスで置き換えるため、
テストファイル単体では通るものの、テストスイート全体で実行すると、その後に動く多数のテストが「テーブルが見つからない」エラーで落ちていました。
SQLite in-memory DB (:memory:) の場合は特に、接続ごとに DB がメモリ内にだけ存在し、Schema 情報も都度ロードし直す必要があるため、SchemaCache を無造作に吹き飛ばすと影響が大きくなります。
対応内容
ActiveRecord::Base.clear_cache!を使った一時テーブル方式をやめ、既存のpiratesテーブルを使うアプローチに変更- テスト対象用に匿名クラス +
attributeAPI を使う形に書き換え
同じファイル内に既にあるテスト
test "attribute_will_change! doesn't try to save non-persistable attributes"などと同じパターンに合わせ、明示的なテーブル作成/削除やグローバルキャッシュ操作に依存しないようにしています。
これに伴い、トップレベルで定義されていた Testings クラスも不要になったため削除されています。
※具体的な差分はおおむね以下のようなイメージです(擬似コード):
# 以前(イメージ)
def test_field_named_field
begin
connection.create_table(:testings) { |t| t.string :field }
klass = Class.new(ActiveRecord::Base) { self.table_name = "testings" }
# ... テスト本体 ...
ensure
connection.drop_table(:testings) rescue nil
ActiveRecord::Base.clear_cache!
end
end
# 以後(イメージ)
def test_field_named_field
klass = Class.new(ActiveRecord::Base) do
self.table_name = "pirates"
attribute :field, :string
end
# ... テスト本体(field という名前の attribute を扱う検証) ...
endこれにより、テスト終了時に SchemaCache をクリアする必要がなくなり、他テストへの副作用をなくしています。
2-2. AdapterConnectionTest の sql_notifications 系テストの修正
問題点
activerecord/test/cases/adapter_test.rb の AdapterConnectionTest には、SQLite in-memory DB 用のガードが既に存在していました:
unless in_memory_db?
# ... 多くのテスト ...
endしかし、そのクラス内にある
test_suppresses_notifications_when_sql_notifications=falsetest_sql_notifications_are_enabled_by_default
の2つのテストだけが unless in_memory_db? の外に置かれていました。
これらのテストは run_without_connection を利用しており、その内部では:
remove_connectionを呼ぶ- その後
establish_connectionで再接続する
という挙動をします。
SQLite で database: ':memory:' を使う場合、remove_connection によってメモリ上の DB 自体が「完全に消える」ため、その後の establish_connection では「空の新しい in-memory DB」が作成され、スキーマ情報がすべて失われます。
このため、これら2テスト実行後に続く多数のテストが「テーブルが存在しない」などのエラーで失敗していました。
対応内容
- 上記2つのテストを
unless in_memory_db?ブロックの中に移動し、SQLite in-memory 環境では実行されないように変更
これにより、SQLite :memory: 環境では DB 接続の remove/establish によってスキーマが消えるパターンを避け、テスト順依存の失敗を防いでいます。
なお、unconnected_test.rb では既に
- in-memory DB で
remove_connection相当の操作を行う場合 - teardown で
load_schema if in_memory_db?を呼んでスキーマを復元
という正しいパターンが実装済みであり、本 PR はそれに合わせて「そもそも in-memory ではその危険なパターンのテストを実行しない」という整理をしています。
- 影響範囲・注意点
- 影響範囲はテストコードに限定され、アプリケーション本体の挙動やパブリック API には変更はありません。
ARCONN=sqlite3_memでのテスト実行時に、大量の順序依存テスト失敗が解消されるため、Rails 本体やプラグイン開発者が CI 等で SQLite in-memory を使う場合の安定性が向上します。- in-memory DB で
remove_connection/establish_connectionを行うと DB 自体が消える、という SQLite 特有の性質が改めて明確になった形なので、アプリや他テストコードでも同様のパターンを使う場合は:- そもそも in-memory では実行しないガードを付ける
- もしくはテスト後に
load_schemaなどでスキーマを復元する といった対策が必要になります。
- SchemaCache をテスト内でいじる場合も、
ActiveRecord::Base.clear_cache!のようなグローバルな操作は極力避け、既存テーブル+属性 API の利用など、スキーマ自体に影響しない設計が望まれます。
- 参考情報 (あれば)
- 該当 Issue: Fixes #57589
- 修正対象ファイル
activerecord/test/cases/adapter_test.rbactiverecord/test/cases/dirty_test.rb
- 関連パターンの例:
activerecord/test/cases/unconnected_test.rbのload_schema if in_memory_db?を使った in-memory DB 復元ロジック
#57591 Merge pull request #57583 from VladNegara/nested-join-explanation
マージ日: 2026/6/5 | 作成者: @p8
- 概要 (1-2文で)
Active Record Querying ガイド内で「ネストしたクエリ (nested join / nested query)」の平易な英語説明が誤解を招く内容だったため、実際の挙動・クエリ構造に合うように文章だけを修正したドキュメント更新です。コードや API の挙動変更は一切なく、ガイドの記述の正確性を高めるための修正です。
- 変更内容の詳細
※ この PR は guides/source/active_record_querying.md の 2 行の差し替えのみで、Ruby コードや Active Record の実装には手を加えていません。GitHub 上の差分を前提に、典型的な修正内容を踏まえて説明します。
対象箇所:
Active Record Querying ガイドの「joins」や「includes」などの章の中で、以下のような「ネストした関連を指定した join」の説明がある部分:rubyAuthor.joins(books: :reviews)以前の説明の問題点 (推定される内容):
- 「内部的に 2 回クエリが発行される」など、実際には 1 つの SQL で複数の JOIN が行われるケースなのに、英語の説明が暗に「サブクエリ」や「複数クエリ」を連想させる表現になっていた。
joinsでネストしたハッシュを渡した場合の SQL 構造 (例:INNER JOIN "books" ... INNER JOIN "reviews" ...) を、平易な英語の説明が正しく反映していなかった。
修正後の説明 (内容のポイント):
- 「nested query」や「nested join」という語を、実際に生成される SQL (連続した JOIN) に合うような説明に変更。
- 「関連をネストして指定すると、Rails はそれに応じて複数のテーブルを順番に JOIN する」という形に言い換えられている可能性が高い。
- サブクエリとしてネストされるわけではなく、「親 → 子 → 孫」とリレーションを辿った JOIN が 1 本のクエリで実行されることを明確にしている。
例: (概念的なイメージ)
Author.joins(books: :reviews)
# => SELECT "authors".*
# FROM "authors"
# INNER JOIN "books" ON "books"."author_id" = "authors"."id"
# INNER JOIN "reviews" ON "reviews"."book_id" = "books"."id"このような実態を反映した「自然言語での説明文」が修正されています。
- 影響範囲・注意点
- 影響範囲:
- ドキュメント (ガイド) のみ。
- Rails のコード、Active Record のクエリ生成ロジック、API は一切変更されていません。
- 注意点:
- 既に
joins・ネストした関連指定 (joins(books: :reviews)など) を利用しているアプリケーションや、既存のテスト・運用には影響はありません。 - これまで誤った理解 (「サブクエリでネストされている」や「2 回クエリが走る」など) に基づいて設計していた場合は、ガイドの新しい説明を読み直すことで、実際の SQL 形状・パフォーマンス特性を再確認するきっかけになります。
- パフォーマンスチューニングや N+1 問題の回避を検討している場合は、
joinsとincludes/preload/eager_loadの違いと合わせて、新しい説明を確認しておくとよいです。
- 既に
- 参考情報 (あれば)
- 対応するガイド:
- Rails Guides: Active Record Query Interface / Active Record Querying
- 「Joining Tables」や「Nested Associations」節に該当する説明が更新されています。
- Rails Guides: Active Record Query Interface / Active Record Querying
- 関連トピック:
joinsによる INNER JOIN / LEFT OUTER JOIN (スコープ指定を含む)- ネストした関連指定:
joins(books: :reviews),joins(books: { publisher: :country })など includes/preload/eager_loadの違いと生成されるクエリ数・JOIN 構造の違い
#57583 Fix plain English explanation of nested query in Active Record Querying guide [ci-skip]
マージ日: 2026/6/5 | 作成者: @VladNegara
- 概要 (1-2文で)
Active Record Querying ガイドにある「ネストした関連の JOIN」の英語説明文が、実際のクエリの挙動と食い違っていたため、それを正しい内容に修正するドキュメント変更です。コードや挙動の変更はなく、ガイドの自然言語による説明だけが更新されています。
- 変更内容の詳細
対象箇所は、Active Record Querying ガイドの以下の節です。
- 「Joining Nested Associations (Multiple Level)」
(guides/source/active_record_querying.md内)
ここでは、includes / joins / references などを使って複数階層の関連を JOIN する例として、以下のようなクエリが説明されています(ガイドの典型例・イメージ):
Author.joins(books: [:reviews, :suppliers])この PR で問題にされた点は、SQL や Active Record のコード自体は正しいのに、それを言い換えた「平易な英語」の説明だけがクエリの意味を間違って伝えていた、という点です。
修正されたポイントは主に2つです。
2-1. 結果セットから著者を不当に除外してしまうような説明を修正
元の平易な英語説明は、概ね次のような意味合いになっていました:
「少なくとも 1 回注文されていて、かつ少なくとも 1 件レビューがあり、…」といった条件を、本(Book)や著者(Author)に対して課しているように読める
しかし実際の SQL / Active Record のクエリは JOIN なので、次のようなケースでも著者は結果に含まれます:
- ある著者 A がいる
- A の本は一度も注文されたことがない
- その本には、(購入していない)顧客からのレビューだけが付いている
この場合でも、JOIN の構造によっては「著者 A が結果セットに含まれる」形になるにもかかわらず、元の英語説明だと「A は除外される」と読めてしまっていました。
PR ではこのズレを修正し、実際のクエリの結合条件・フィルタ条件が示す「結果セットに含まれる著者の条件」を正確に反映した表現に変更しています。
2-2. supplier 情報が「返ってくる」ように読める説明を修正
もうひとつの問題は、サプライヤ(suppliers)への JOIN の説明です。
元の平易な英語説明は、あたかも
suppliers の詳細情報も一緒に返ってくる(SELECT される)
かのようなニュアンスになっていましたが、実際には典型的な例では
Author.joins(books: :suppliers)のように、suppliers への JOIN は「条件としてのフィルタ」にしか使っていない場合があります。
つまり、
- 「少なくとも 1 つの supplier を持つ book だけを対象にする」
- → その結果として「そういう book を持つ authors」だけが結果に残る
という用途であって、suppliers の列まで SELECT して返すかどうかは別問題です。
PR ではこの点を明確にし、
- suppliers への JOIN は「結果に supplier の情報を含める」というよりも
- 「少なくとも 1 つ supplier を持つ book に絞り込むための結合である」
という、実際のクエリ構造に忠実な説明へと修正しています。
- 影響範囲・注意点
影響範囲
- Rails 本体のコード・挙動には変更なし。
- Active Record Querying ガイドの一節(ネストした関連の JOIN)の自然言語説明のみ変更。
- すでにガイドを読んでコードを書いている人に対しては、「クエリがどう動いているか」の認識を正す効果はあるが、アプリケーションコード自体は一切変更不要。
注意点 / 読み手への実務的な示唆
joinsを使ったとき、**「どのテーブルが結果に含まれるか」「どのテーブルはフィルタ条件としてだけ使われるか」**を意識する必要がある。- 特にネストした関連(
Author.joins(books: [:reviews, :suppliers])のような構造)の場合は、- どの関連が INNER JOIN されているか
- その結果、「親」レコード(Author)がどの条件で残る/落ちるのか を SQL レベルで一度確認しておくとよい。
- ドキュメントを「クエリの仕様の厳密な定義」として読むのではなく、 実際には
to_sqlやローデータの確認で挙動を検証するのが安全、という教訓にもつながる修正です。
- 参考情報 (あれば)
- 当該ガイド節(英語・最新版)
https://guides.rubyonrails.org/active_record_querying.html#joining-nested-associations-multiple-level - 言い換えの表現だけを変えたが内容は変わっていなかった、という既存 PR:
- PR #56341 — Active Record Querying ガイドの文言修正(今回の PR で、内容面の誤りも同時に是正された)
#57580 Skip Proc and Regexp filter_attributes when syncing to filter_parameters
マージ日: 2026/6/5 | 作成者: @jcalvert
- 概要 (1-2文で)
このPRは、ActiveRecord::FilterAttributeHandlerがモデルのfilter_attributesをRails.application.config.filter_parametersに同期する際、ProcやRegexpなど文字列化しても意味のないエントリをスキップするようにし、不要かつ不安定なフィルタ文字列がfilter_parametersに蓄積される問題を修正するものです。これにより、フィルタ設定がよりクリーンで安定し、ログや検査ツールが扱いやすくなります。
- 変更内容の詳細(あればサンプルコードも含めて)
背景となる挙動
Rails 8.1 から導入された ActiveRecord::FilterAttributeHandler (#55251) は、モデル側の filter_attributes を Rails.application.config.filter_parameters に同期します。
同期時には以下のように「<モデル名>.<属性名>」形式の文字列に変換されます:
class User < ApplicationRecord
self.filter_attributes += [:token]
end
Rails.application.config.filter_parameters
# => [..., "user.token"]これにより、params[:user][:token] のようなパラメータもログでマスクされます。
一方、filter_attributes は ActiveSupport::ParameterFilter による #inspect 用のフィルタとして、シンボル / 文字列だけでなく Regexp や Proc も受け付けます:
class User < ApplicationRecord
self.filter_attributes += [:token, /secret/i, ->(key, value) { value.reverse! }]
end元実装では apply_filter 内で各エントリに to_s を呼んでいたため、これらがそのまま filter_parameters に反映され、次のような「絶対にマッチしない」文字列が増殖していました:
Rails.application.config.filter_parameters
# => [...,
# "user.token", # これは正常
# "user.(?i-mx:secret)", # Regexp の to_s
# "user.#<Proc:0x000000010a3c5d68 ...>" # Proc の to_s
# ]- これらはパラメータキーとして絶対に一致しないため、実質ゴミ
- 特に
Procのto_sにはオブジェクトIDが含まれるため、アプリ再起動ごとに文字列が変わり配列の安定性が損なわれる filter_parametersを参照するログフォーマッタや診断用ツール、テスト(監査系 spec など)に悪影響
このPRの変更点
ActiveRecord::FilterAttributeHandler#apply_filter の挙動を変更し、
- String と Symbol 以外のエントリは
filter_parametersへの同期対象から除外する
ようにしました。
擬似コード的には以下のようなイメージです(実際の変更は +6/-3 行程度):
# 変更前 (イメージ)
def apply_filter(name)
filter_attributes.each do |attribute|
Rails.application.config.filter_parameters << "#{name}.#{attribute}"
end
end
# 変更後 (イメージ)
def apply_filter(name)
filter_attributes.each do |attribute|
next unless attribute.is_a?(String) || attribute.is_a?(Symbol)
Rails.application.config.filter_parameters << "#{name}.#{attribute}"
end
endこれにより、先ほどの例は以下のような結果になります:
class User < ApplicationRecord
self.filter_attributes += [:token, /secret/i, ->(key, value) { value.reverse! }]
end
Rails.application.config.filter_parameters
# => [..., "user.token"] # Regexp / Proc 由来のゴミ文字列は追加されないテストの追加
railties/test/application/active_record_railtie_test.rb にテストが追加され、以下を確認しています:
- モデルで
filter_attributesにシンボル/文字列を設定した場合のみ、filter_parametersに"<model>.<attribute>"が追加される RegexpやProcを含めても、それらがfilter_parametersには同期されない
- 影響範囲・注意点
影響範囲
- 影響を受けるのは、Active Record モデルで
filter_attributesを使っていて、かつRegexp/Procを含めているアプリケーションです。 - これらのアプリでは、以前は
Rails.application.config.filter_parametersにuser.(?i-mx:secret)user.#<Proc:...>などの「マッチしないゴミ文字列」が大量に増えていた可能性がありますが、このPRにより追加されなくなります。
- 既存の動作面での変化は「本来意味のなかった無効なフィルタが追加されなくなる」だけであり、ログのマスキング挙動(実際に値が正しくフィルタされるかどうか)には悪影響はありません。
注意点 / 互換性
- パラメータフィルタリング自体の挙動は変わりません:
- グローバルな
ActiveSupport::ParameterFilter経由 (Rails.application.config.filter_parametersに直接Regexp/Procを設定している場合) - モデル単位の
filter_attributesによる#inspect向けのフィルタ(Regexp/Procの適用) これらは従来通り動作します。
- グローバルな
- 変わるのは「
filter_attributesからfilter_parametersへ同期する際に、キーマッチ可能な文字列に変換できないものを除外する」という点のみです。 - 監査・診断用途で
filter_parametersの「中身の変化」をテストしている場合、期待値からuser.(?i-mx:secret)やuser.#<Proc:...>などを取り除く必要があるかもしれません。ただし、これらに依存しているのはほぼテストだけだと考えられます。
- 参考情報 (あれば)
- 本PRで扱っている
ActiveRecord::FilterAttributeHandlerの導入 PR: #55251 config.filter_parametersにおける同様の問題を直した PR: #56759- 説明にもある通り、このPRと #56759 を 8.1 系にバックポートすると、
Proc/Regexp経由の「ゴミエントリ」リークパスを全て塞げるとのことです。
- 説明にもある通り、このPRと #56759 を 8.1 系にバックポートすると、
- 関連クラス:
ActiveSupport::ParameterFilterRegexp/Procを利用した高度なフィルタリングロジックをサポート- 「
user.token」のようなドット付き文字列をネストしたキーに対するフィルタとして解釈する挙動と整合させるため、今回のような「シンボル / 文字列のみを同期する」仕様にするのが理にかなっています。
#57585 Fix reaper fork test by disabling GSS encryption.
マージ日: 2026/6/5 | 作成者: @ruyrocha
- 概要 (1-2文で)
macOSでReaperTest#test_connection_pool_starts_reaper_in_fork実行時に segfault が発生していた問題を、fork 後の子プロセスで PostgreSQL の GSS 暗号化ネゴシエーションを無効化することで回避する修正です。libpq と macOS の Heimdal/Keychain スタックの既知の非 fork-safe 問題へのワークアラウンド的対応になります。
- 変更内容の詳細
対象: activerecord/test/cases/reaper_test.rb の1行差分のみ。
やっていることは、fork された子プロセス内で PostgreSQL クライアント用の環境変数 PGGSSENCMODE を disable に設定してからコネクションプールを作るようにする、というものです。
PRの説明によると:
問題の症状
- テスト
ReaperTest#test_connection_pool_starts_reaper_in_forkが macOS で segfault。 - 再現条件:
- 親プロセスが
fork()する。 - 子プロセス側で新たに PostgreSQL コネクションを確立しようとする。
- その際、libpq が GSS 暗号化ネゴシエーションを試みる。
- GSS 経由で macOS の Heimdal/Keychain スタックを叩くが、これが fork 後の子プロセスで非 fork-safe であるためクラッシュ。
- 親プロセスが
- テスト
対応内容
- 子プロセス側で
PGGSSENCMODE=disableをセットし、GSS 暗号化ネゴシエーションのコードパス自体を通らないようにする。 - これにより、Heimdal/Keychain 周りの非 fork-safe な部分に到達しなくなり、segfault を回避。
- 子プロセス側で
テストコードのイメージは以下のような形になります(実際のPRは1行置き換えレベルですが、趣旨として):
fork do
# GSS 暗号化を無効化
ENV["PGGSSENCMODE"] = "disable"
# ここで ActiveRecord::ConnectionPool を生成し、
# PostgreSQL への新しい接続を張るテストを実行
pool = ActiveRecord::Base.connection_pool
# reaper が正しく起動するかを検証 ... みたいな内容
end元々のテストではこの環境変数設定がなかったため、macOS + libpq + GSS 有効な環境で fork 後に接続を張る際に GSS ネゴシエーションが走り、segfault に繋がっていました。
PR本文にもあるとおり、この問題は:
- PostgreSQL 側: バグレポート
bug #16041 - ruby-pg 側: issue
#538
として既知であり、恒久的な根本解決はまだないため、Rails テスト内でのワークアラウンドという位置付けです。
- 影響範囲・注意点
影響範囲
- 変更対象は テストコードのみ (
reaper_test.rb) であり、Rails 本体の実行時挙動やアプリケーションコードには影響しません。 - CIやローカル開発環境で、macOS 上で PostgreSQL を用いた ActiveRecord のテストを走らせる際の安定性が向上します。
- 特に
forkを伴うテスト(connection reaper が fork 後も正しく動くかを確認するテスト)に限定された変更です。
- 変更対象は テストコードのみ (
注意点
- このPRは Rails テストの中で
PGGSSENCMODEを変更しているだけであり、実アプリケーションでの GSS 暗号化を無効化するわけではありません。 - ただし、テスト実行プロセス内で
ENV["PGGSSENCMODE"]を上書きしているため、- 「テスト中に同じプロセスで他の PostgreSQL 接続を張るコード」があり、かつ
- それが GSS 暗号化を前提にしている
場合には、環境変数の状態が影響する可能性があります(Rails の標準テストスイートでは基本的に問題にならない想定)。
- 本件は macOS 特有の Heimdal/Keychain の非 fork-safe 問題に起因しているため、Linux 等では同じ問題は基本的に発生しませんが、テストコード自体はクロスプラットフォームで動く形になっています。
- このPRは Rails テストの中で
- 参考情報 (あれば)
- PostgreSQL バグ報告:
- PostgreSQL bug #16041(libpq + GSS + fork によるクラッシュの報告)
- ruby-pg の関連 issue:
- ruby-pg #538(同様の GSS / fork 問題に関する議論)
PGGSSENCMODEについて:- PostgreSQL クライアント(libpq)の環境変数で、GSSAPI による暗号化ネゴシエーションの挙動を制御する。
disableにすると GSS 暗号化ネゴシエーションを行わず、今回のような GSS 経由のクラッシュを避けられる。
- PostgreSQL クライアント(libpq)の環境変数で、GSSAPI による暗号化ネゴシエーションの挙動を制御する。
#57587 Replace deprecated US/Eastern timezone with America/New_York in tests
マージ日: 2026/6/5 | 作成者: @ruyrocha
- 概要 (1-2文で)
US/Easternが PostgreSQL で無効なタイムゾーン名になった問題に対応し、テストコード内のタイムゾーン指定を正規のAmerica/New_Yorkに置き換える PR です。Rails 本体の挙動変更ではなく、Active Record のテスト環境が最新 tzdata / PostgreSQL でも壊れないようにするための修正です。
- 変更内容の詳細
背景
- IANA tzdata の更新により、
US/Easternなどの古いエイリアス的タイムゾーン名が、PostgreSQL のtimezone設定値としては無効になりました。 - その結果、最新 tzdata でビルドされた PostgreSQL(例: Homebrew 版)を使うと、Rails のテスト実行時に
ActiveRecord::ConnectionNotEstablishedが発生するケースがありました。 America/New_Yorkは IANA における正準(canonical)なタイムゾーン名であり、PostgreSQL でも広くサポートされています。
実際の変更箇所
変更は 2 ファイルのみで、単純な文字列置き換えです。
1. postgresql_adapter_test.rb の KNOWN_SERVER_DEFAULTS
ActiveRecord PostgreSQL アダプタのテスト内で、接続オプションの既知のデフォルト値 (KNOWN_SERVER_DEFAULTS) を定義している箇所で、タイムゾーン指定を変更しました。
(イメージ):
# 変更前
KNOWN_SERVER_DEFAULTS = {
# ...
timezone: "US/Eastern",
# ...
}
# 変更後
KNOWN_SERVER_DEFAULTS = {
# ...
timezone: "America/New_York",
# ...
}この定数を使って、「サーバー側のデフォルト設定をこう想定している」という前提で各種挙動をテストしています。ここが無効なタイムゾーンだと、接続確立時点でエラーになりテストが成立しなくなります。
2. test_case.rb の with_env_tz デフォルト
Rails のテストヘルパである with_env_tz メソッドのデフォルトタイムゾーンも US/Eastern から America/New_York に変更されています。
(イメージ):
# 変更前
def with_env_tz(tz = "US/Eastern", &block)
# ...
end
# 変更後
def with_env_tz(tz = "America/New_York", &block)
# ...
endこのヘルパは、テストの間だけ ENV["TZ"] を一時的に変更してタイムゾーン依存の挙動を検証するためのものです。デフォルトで古い US/Eastern を使っていたため、環境によってはテストが失敗していました。
- 影響範囲・注意点
影響範囲
- Rails フレームワーク利用者の本番アプリケーションの挙動には影響しません(あくまでテストコードのみの変更)。
- Active Record の PostgreSQL 関連テスト、および
with_env_tzを利用しているテストコードに影響します。 - tzdata / PostgreSQL が新しくても、Rails のテストスイートをエラーなく実行できるようになります。
注意点 / 参考となる含意
- アプリケーション側で
US/Easternを PostgreSQL のtimezone設定やアプリ内のタイムゾーン名として使っている場合、今後の tzdata / PostgreSQL 更新で同様の問題が起きる可能性があります。- 可能であれば
America/New_Yorkへの移行を検討するとよいです。
- 可能であれば
- 他の
US/*系タイムゾーン(例:US/Pacificなど)も同様に非推奨・削除の対象になり得るため、IANA の canonical 名(America/Los_Angelesなど)を使うのが安全です。
- アプリケーション側で
- 参考情報 (あれば)
- 対応 Issue: #57586
「US/Easternが PostgreSQL のタイムゾーンとして無効になり、ActiveRecord::ConnectionNotEstablishedが発生する」問題の修正 PR。 - IANA タイムゾーンデータベース:
US/EasternはAmerica/New_Yorkのエイリアス扱いだが、PostgreSQL ビルド時にエイリアスが含まれない/削除される構成が増えている。
- PostgreSQL ドキュメント(Time Zones):
- TimeZone 設定には、IANA の正準名を使うことが推奨されている。
#56899 Add sql_notifications connection config option to disable SQL notifications
マージ日: 2026/6/5 | 作成者: @rosa
- 概要 (1-2文で)
Active Record の DB 接続ごとに SQL 通知(ActiveSupport::Notifications による instrumentation)を無効化できるsql_notifications接続オプションが追加されました。これに伴い、通知を一切発行しないActiveSupport::Notifications::NullInstrumenterが導入され、非同期クエリ時のイベントバッファリングも無効化できます。
- 変更内容の詳細
2-1. 新しい接続オプション sql_notifications
database.yml などの接続設定で、接続単位で SQL 通知をオフにできます。
production:
primary:
adapter: postgresql
database: myapp_production
cache:
adapter: postgresql
database: myapp_cache
sql_notifications: falsesql_notifications: falseを設定した接続では、sql.active_recordなどの Active Record の SQL 関連通知が発行されません。- それに伴うオーバーヘッド(オブジェクト生成、subscriber 呼び出しなど)が削減されます。
- デフォルトは従来通り「有効(true 相当)」です。設定を追加しなければ今までと同じ挙動です。
このオプションは、connects_to などで別 DB を扱うケース(例: Solid Cache, Solid Queue 用の専用 DB)を想定しており、「アプリ本体はメトリクスを取りたいけれど、キャッシュ用 DB では不要」というような分離が簡単になります。
2-2. ActiveSupport::Notifications::NullInstrumenter の追加
ActiveSupport::Notifications に、何も通知を発行しないインストゥルメンターが追加されました。
特性:
- no-op(ブロック実行だけして通知を publish しない)
- stateless(状態をもたず、使い回しができる)
- thread-safe(スレッドローカルなレジストリ不要)
役割:
- DB アダプタが
sql_notifications: falseの接続を扱うときに、このNullInstrumenterを使用します。 - これにより、従来一部の gem が行っていたような「Active Record の内部実装に踏み込んで instrumenter をすげ替える」といったハックが不要になります。
イメージとしては、通常:
ActiveSupport::Notifications.instrument("sql.active_record", payload) do
# SQL 実行
endが、sql_notifications: false の接続では「イベント発行部分が完全に無視される」ような形です。
2-3. 非同期クエリ時の変更(イベントバッファのスキップ)
Active Record の async query(load_async など)では、並行実行されるクエリに対してイベントをバッファし、後でまとめて通知する仕組みがあります。
この PR では:
sql_notifications: falseの場合、- このイベントバッファリング処理自体をスキップします。
- 「通知を出さない」のに「通知のためのバッファ」を持つのは無駄なので、関連処理を丸ごと省く、という設計です。
- これにより、非同期クエリでも instrumentation 関連コストが最小限になります。
2-4. コード側での利用イメージ(Rails アプリ視点)
基本的には設定ファイルに書くだけなので、アプリケーションコードの変更は不要です。例:
# config/database.yml
production:
primary:
adapter: postgresql
database: myapp_production
cache:
adapter: postgresql
database: myapp_cache
sql_notifications: false# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
primary_abstract_class
connects_to database: { writing: :primary, cache: :cache }
endこの状態で cache 接続を使うモデルのクエリは、sql.active_record などの通知を発行しません。
- 影響範囲・注意点
既存アプリへの互換性
sql_notificationsを指定しなければ、挙動は従来と同じです(後方互換あり)。- 既存のメトリクスやログ収集は影響を受けません。
通知に依存している仕組みへの影響
sql_notifications: falseにした接続では:- APM(New Relic, Datadog など)や独自の subscriber が SQL クエリを観測できなくなります。
- SQL ベースのログ・トレース・メトリクスが失われる点に注意が必要です。
- 「一部の接続だけメトリクスを止める」ケース(キャッシュ用 DB など)に向いており、本番トラブル調査が必要な主要 DB では通常有効のままにすべきです。
gem / ライブラリ側での採用
- Solid Cache / Solid Queue のように別接続を使う gem は、
- これまでのように Active Record の内部実装に依存して instrumenter を差し替える必要がなくなります。
sql_notifications: falseを接続設定に追加するだけで、SQL 通知由来のオーバーヘッド削減が可能になります。
- もし独自に
disable_instrumentationなどでごにょごにょしている gem があれば、将来的にこのオプションへ移行することが推奨されます。
- Solid Cache / Solid Queue のように別接続を使う gem は、
パフォーマンス面
- SQL 通知を完全に切るため、イベント生成・subscriber 呼び出し・非同期バッファリングなどのコストがなくなります。
- 特に、クエリ数が非常に多いキャッシュ用 DB などでは、わずかながらパフォーマンス改善や GC 負荷の低減が期待できます。
- 参考情報 (あれば)
- 本 PR: https://github.com/rails/rails/pull/56899
- 類似オプション:
seedsオプション追加 PR
https://github.com/rails/rails/pull/53379 - Solid Cache が行っていた instrumentation 無効化実装(内部差し替えの例)
https://github.com/rails/solid_cache/blob/4e7219c4210d165e03176439dfca99238ba6fde9/app/models/solid_cache/record.rb#L12-L29
#57570 Enable per-pool query log tags formatter
マージ日: 2026/6/4 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
このPRは、database.ymlの各コネクションプールごとにquery_log_tags_formatを設定できるようにし、グローバル設定 (config.active_record.query_log_tags_format) をプール単位で上書き可能にします。これにより、同一アプリ内でデータベース接続ごとに:legacy/:sqlcommenterなど異なるクエリログコメント形式を使い分けられます。
- 変更内容の詳細
2-1. 機能追加の概要
database.ymlの各エントリでquery_log_tags_formatキーが指定できるようになった。- その値は
ActiveRecord::Base.configurationsを通じて読み込まれ、個々のコネクションプールごとに保持される。 - 実際にクエリログのタグコメントを生成するとき、
- まず接続プール固有の
query_log_tags_formatを参照 - 指定がなければグローバル設定
config.active_record.query_log_tags_formatを使用
という優先順位になるように変更されている。
- まず接続プール固有の
2-2. database.yml での設定例
説明文にある通り、例えば本番環境で primary DB ではデフォルト(例::legacy)、analytics DB では :sqlcommenter を使いたい場合:
production:
primary:
database: primary
# query_log_tags_format 未指定 → グローバル設定を使用
analytics:
database: analytics
query_log_tags_format: sqlcommenterconfig/application.rb などで:
module MyApp
class Application < Rails::Application
config.active_record.query_log_tags_format = :legacy
end
endと設定しておけば、primary プールは :legacy、analytics プールは :sqlcommenter という形式でクエリコメントが付与されます。
2-3. 実装レベルの変更点
※ 具体的なコード断片はPR本文にありませんが、変更ファイルから推測される点です。
activerecord/lib/active_record/database_configurations/hash_config.rbHashConfigにquery_log_tags_formatを読み込み・保持するロジックが追加。database.ymlのquery_log_tags_formatキーをそのまま or シンボル化して扱うような実装が入っていると考えられます。
activerecord/lib/active_record/query_logs.rb- 既存の「クエリログタグのフォーマットを選択し、SQLコメントを生成する処理」が、
“接続(プール)ごとの設定を見てからグローバル設定を参照する” 構造に変更。 - フォーマッタ自体(
:legacyと:sqlcommenterの実装)は既存機能ですが、その選択ロジックが per‑pool 対応になっています。 - 追加行数が多いことから、フォーマッタ選択のヘルパーメソッド追加や、テスト/内部APIから呼び出しやすいインターフェース整備もされている可能性が高いです。
- 既存の「クエリログタグのフォーマットを選択し、SQLコメントを生成する処理」が、
activerecord/test/cases/query_logs_test.rb- 各種パターンのテストが追加:
- グローバル設定のみ指定した場合の挙動。
- プール側に
query_log_tags_formatを指定した場合の上書き挙動。 - 片方のみ / 両方指定 / どちらも未指定などの組み合わせ検証。
:sqlcommenter/:legacyそれぞれのコメント出力フォーマットが期待通りになることの確認。
- 各種パターンのテストが追加:
activerecord/CHANGELOG.md- Active Record の変更点として、「per‑pool query_log_tags_format をサポートした」ことが明記されている。
- 影響範囲・注意点
既存アプリへの互換性
database.ymlにquery_log_tags_formatを書いていない場合、動作は従来通りconfig.active_record.query_log_tags_formatのみが使われるため、互換性は基本的に保たれます。- すでに
config.active_record.query_log_tags_formatを使っているアプリは、そのままでも挙動は不変です。
per‑pool 設定を入れた場合の優先度
- ある DB プールに
query_log_tags_formatを設定した瞬間、そのプールに限ってはグローバル設定を無視して、その値が優先されます。 - 全プールで同じ挙動を維持したい場合は、
database.yml側で特定プールだけ別設定を入れていないか注意が必要です。
- ある DB プールに
フォーマット種別とロギング・ツール連携
:sqlcommenterを使うと、SQLコメントが sqlcommenter 仕様 準拠の形式になるため、APMツールや可観測性基盤との連携でメリットがあります。- 一方、既存で
:legacyを前提としたパーサや監視スクリプトを使っている場合、特定プールだけ:sqlcommenterに切り替えるとログ解析ロジックを分岐させる必要が出てきます。
複数DB・マルチテナント環境での注意
- primary / replica / analytics などDBごとに異なるログ要件がある場合に便利ですが、
「どのプールにどのフォーマットを使っているか」をチーム内で明文化しておかないと、ログ形式が混在して運用が複雑になる可能性があります。 - マルチDB構成を管理している場合は、運用設計書や README に
query_log_tags_formatの方針を追記しておくとよいです。
- primary / replica / analytics などDBごとに異なるログ要件がある場合に便利ですが、
- 参考情報 (あれば)
- Railsガイド(英語):
- Active Record Query Logs (次バージョンのガイドに本変更が反映される見込み)
https://guides.rubyonrails.org/active_record_querying.html#query-logs (将来URL・内容が更新される可能性あり)
- Active Record Query Logs (次バージョンのガイドに本変更が反映される見込み)
sqlcommenter仕様:- 関連設定:
- グローバル設定:
config.active_record.query_log_tags_format database.ymlでの per‑pool 設定:query_log_tags_format: legacy/query_log_tags_format: sqlcommenter
- グローバル設定:
#57572 Add test coverage for Enumerable key-helper edge cases
マージ日: 2026/6/4 | 作成者: @hammadxcm
- 概要 (1-2文で)
Enumerable拡張(pluck/pick/compact_blank)の「キーが存在しない場合」やSetに対する挙動など、これまでテストされていなかった端ケースに対してテストが追加された PR です。プロダクションコードの変更はなく、テストコード(enumerable_test.rb)のみが20行追加されています。
- 変更内容の詳細
この PR は active_support/core_ext/enumerable.rb に定義されている以下のヘルパーの 期待される振る舞いを明示するテスト を追加しています。
(1) pluck のエッジケース
対象: Enumerable#pluck(ActiveSupport 拡張)
ケースA: 単一キーで、一部の要素にキーが存在しない場合
テスト内容はだいたい次のような形が想定されます:
records = [
{ id: 1, name: "Alice" },
{ id: 2 } # name がない
]
assert_equal [ "Alice", nil ], records.pluck(:name)ポイント:
- 要素の中に指定キーが存在しないものが含まれていても、
- 例外にはならず
- 代わりに
nilがその要素の位置に入る
という仕様をテストで保証しています。
ケースB: 複数キーで、一部の要素にキーが存在しない場合
records = [
{ id: 1, name: "Alice" },
{ id: 2 } # name がない
]
assert_equal [[1, "Alice"], [2, nil]], records.pluck(:id, :name)ポイント:
- 返り値がタプル(配列)になる複数キーの
pluckにおいて、- 欠けているキーの位置に
nilが入る
ことを明示的にテストしています。
- 欠けているキーの位置に
これにより、「キーが足りない要素を含むコレクションに対して pluck したときの挙動」がドキュメントどおりであることが保証されます。
(2) pick のエッジケース
対象: Enumerable#pick(ActiveSupport 拡張)
pick は「Enumerable から最初の要素を取り、その要素から指定キーを取り出す」ヘルパーです。
ケースC: 最初の要素にキーが存在しない(単一キー)
records = [
{ id: 1 }, # name がない
{ id: 2, name: "Bob" }
]
assert_nil records.pick(:name)ポイント:
pick(:name)は「最初の要素のnameを返す」ため、- 最初の要素にキーがない場合は
nilを返す
という挙動がテストされます。
- 最初の要素にキーがない場合は
ケースD: 最初の要素にキーが存在しない(複数キー)
records = [
{ id: 1 }, # name, age がない
{ id: 2, name: "Bob", age: 20 }
]
assert_equal [nil, nil], records.pick(:name, :age)ポイント:
- 複数キー指定時は配列を返すが、
- 存在しないキーの位置には
nilが入る
という仕様をテストで固定化しています。
- 存在しないキーの位置には
pickもあくまで「先頭要素」に対してのみキー抽出を行うことが再確認できます。
(3) compact_blank と Set の挙動
対象: Enumerable#compact_blank
compact_blank は、blank? な要素を取り除く ActiveSupport のヘルパーです。
ケースE: Set に対して compact_blank を呼んだとき
set = Set[ "", "foo", nil ]
result = set.compact_blank
assert_kind_of Array, result
assert_equal ["foo"], resultポイント:
Setに対してcompact_blankを呼ぶと 戻り値はArrayになる という仕様が、- ドキュメントに書かれているとおりであることをテストで保証しています。
- つまり、
compact_blankは常に元のクラスを保持するわけではなく、Set→Arrayへの変換が行われるケースがあることが明文化されました。
- 影響範囲・注意点
- この PR は テストコードのみの追加 であり、ランタイム(本体実装)には仕様変更・バグ修正はありません。
- ただし、テストが追加されたことで:
pluckが「キー欠如を例外ではなくnilとして扱う」pickが「先頭要素に対してのみキーを取り、欠如はnilにする」Set#compact_blankが「Arrayを返す」
という挙動が公式に固定化された、と解釈できます。
- 既存コードで、
pluck/pickの結果にnilが含まれない前提で処理していたりSet#compact_blankの戻り値がSetになると想定しているロジック
がある場合は、改めて仕様を確認しておく価値があります(挙動自体は以前からそうだったはずですが、テストで明示されたことで将来的にも変わりにくくなります)。
- 参考情報 (あれば)
- 対象ファイル:
activesupport/test/core_ext/enumerable_test.rb(+20/-0) - 関連メソッドのドキュメント(Rails Guides / API Docs):
ActiveSupport::CoreExtensions::Enumerable::pluckActiveSupport::CoreExtensions::Enumerable::pickActiveSupport::CoreExtensions::Enumerable::compact_blank
この PR は「仕様の暗黙部分をテストで明示した」性質が強く、将来のリファクタリングや最適化の際に、これらの細かい挙動がうっかり変わってしまうことを防ぐ目的があります。
#57584 Clear Postgres warnings as they get handled
マージ日: 2026/6/4 | 作成者: @matthewd
- 概要 (1-2文で)
PostgreSQL アダプタで、SQL 実行時に発生した warning の扱いを整理し、「処理済みの warning を都度クリアする」ようにした PR です。これにより、select_allなど複数クエリの warning が蓄積して次のクエリで再度まとめて報告されてしまう挙動が修正され、失敗したクエリ後の warning も正しく処理されます。
- 変更内容の詳細
全体像
これまで warning のクリアは素の execute にしか入っておらず、他のメソッド (select_all など) が内部で warning を収集した場合、その warning が「報告はされるがリストは残り続ける」状態でした。そのため:
- 複数のクエリを順に実行
- 各クエリで warning が出る
- 各クエリ後の warning ログには「累積分」が出る
- さらに、次のクエリでも「過去の warning」が再度表示される
という、warning が二重三重に報告される状態になっていました。
この PR では以下を行っています:
- warning のクリアを PostgreSQL 固有のコードから抽象層 (
abstract/database_statements.rb) に寄せる - 成功したクエリだけでなく、失敗したクエリ後も warning を処理・クリアする
- 上記挙動を保証するテストを追加する
abstract/database_statements.rb の変更
ActiveRecord::ConnectionAdapters::DatabaseStatements (抽象層) に、PostgreSQL 向け warning 処理の共通パターンを吸収する仕組みが追加されています。具体的には、以下のような形が入っている可能性が高いです(実際のコードは多少異なる場合がありますが、概念的にはこのようなイメージです):
def execute(sql, name = nil)
materialize_transactions
log(sql, name) do
with_warning_handling do
@connection.async_exec(sql)
end
end
end
private
def with_warning_handling
result = yield
handle_warnings # => ここで warning を処理
result
rescue => e
handle_warnings # 失敗したクエリでも warning を処理
raise
endポイント:
with_warning_handlingのようなヘルパ (実名は PR 内で定義) を導入し、そこからhandle_warningsを呼ぶことで、- 成功時
- 例外発生時 (失敗時) どちらの経路でも warning が処理されるようになっています。
- 「warning の処理」と「warning のクリア」をセットにしており、「一度報告した warning を再度報告しない」ことを保証しています。
postgresql/database_statements.rb の変更
PostgreSQL アダプタ側のファイルでは、もともと散在していた warning 処理/クリア処理が削除・整理されています。
- 行削除 (-16) が多いことから、
- 各種メソッド (
execute,select_all,exec_queryなど) に個別に書かれていた warning クリア処理を取り除き - 代わりに抽象層の共通処理に任せる というリファクタリングが行われています。
- 各種メソッド (
- PostgreSQL 固有の
handle_warning/clear_warningsのようなメソッドは引き続き存在するが、その呼び出しタイミングが抽象層に委譲されている構成が想定されます。
これにより、PostgreSQL アダプタのコードは「どう warning を処理するか」のみに集中し、「いつそれを呼ぶか」は抽象層に任せる形になります。
テスト (postgresql_adapter_test.rb) の追加
+54 行のテストが追加されており、主に次のような振る舞いを検証しています (概念的な例):
複数クエリで warning が累積しないこと
rubydef test_warnings_are_cleared_after_each_query conn = ActiveRecord::Base.connection # 1つ目のクエリで warning を出す conn.execute("SELECT 1/0") rescue nil warnings_after_first = capture_warnings(conn) # 2つ目のクエリで別の warning を出す conn.execute("SELECT 2/0") rescue nil warnings_after_second = capture_warnings(conn) assert_equal 1, warnings_after_first.size assert_equal 1, warnings_after_second.size # => 「2つめの warnings に 1つめの分が混じっていない」ことを確認 end失敗したクエリ後でも warning が適切に処理・クリアされること
rubydef test_warnings_are_cleared_even_after_failed_query conn = ActiveRecord::Base.connection # わざと失敗するクエリを実行 (syntax error など) assert_raises(ActiveRecord::StatementInvalid) do conn.execute("THIS WILL FAIL") end # 次のクエリで、さきほどの失敗クエリの warning が出てこないこと warnings = capture_warnings(conn) { conn.execute("SELECT 1") } assert_empty warnings end
実際には capture_warnings のようなヘルパを使うか、ログを読んで検証しているはずですが、意図としては上記のように「warning はクエリごとに完結する」をテストしています。
- 影響範囲・注意点
ログ・モニタリングへの影響
- これまで PostgreSQL の warning をアプリ側でフックしてモニタリングしていた場合、
- 1回の warning が複数回レポートされていた
- あるいは複数クエリ分がまとめてレポートされていた ような挙動が、クエリ単位での正確なレポートに変わる可能性があります。
- その結果、warning 件数が「減ったように見える」ことがありますが、これは重複報告がなくなっただけです。
- これまで PostgreSQL の warning をアプリ側でフックしてモニタリングしていた場合、
アプリケーションコードへの互換性
- 通常の Rails アプリ (ActiveRecord 経由で DB を叩いているだけ) であれば、コード修正は不要です。
- PostgreSQL アダプタの内部 API (
raw_connectionの warning バッファなど) を直接参照して独自処理している場合、- 「warning が溜まり続ける」前提のコードは挙動が変わる
- クエリ単位でバッファがクリアされる想定に合わせる必要がある 可能性があります。
エラーハンドリングの一貫性向上
- 失敗したクエリも含めて warning が処理されるため、「エラー発生時には warning が漏れる / 溜まり続ける」といった不整合が解消されます。
- 一方で、「例外発生後にまとめて warning を見たい」というようなユースケースがあれば、そのロジックは再検討が必要です (Rails 側が逐次クリアするようになったため)。
- 参考情報 (あれば)
- この PR に関連しそうなコード:
ActiveRecord::ConnectionAdapters::DatabaseStatementsActiveRecord::ConnectionAdapters::PostgreSQLAdapter
- PostgreSQL の warning の具体例:
- 型変換の暗黙的キャスト
- ゼロ除算
- obsolete な構文の使用 など
これらが「クエリ単位」で正しくログ・ハンドリングされるようになったと理解するとよいです。
- Rails 内部的には、「アダプタ固有の実装 (PostgreSQL)」と「抽象層 (DatabaseStatements)」の責務分離が進んでおり、今後他 DB アダプタにも同様の warning ハンドリングが拡張される布石とも考えられます。
#52278 [ci skip] Improve readability of select form options helper gotcha documentation
マージ日: 2026/6/4 | 作成者: @markzalar
- 概要 (1-2文で)
Rails のselectヘルパ(form_options_helper)に関する「gotcha(ハマりどころ)」のドキュメントが、読みやすくなるようリライトされた PR です。挙動や API の変更はなく、内容はドキュメントコメントのみの改善です。
- 変更内容の詳細
※この PR は actionview/lib/action_view/helpers/form_options_helper.rb のドキュメントコメントだけを変更しており、Ruby コードのロジックは変わっていません。
主なポイントは次のようなものと考えられます(ファイル名と行数増減からの推測も含みます):
select/options_for_selectなどのヘルパに関する「gotcha」セクションの文章を、より分かりやすく整理- 長くて読みづらい説明を分割したり、文脈を補足して理解しやすくした
- 実際の使い方をイメージしやすいように、コード例やコメントを改善
- よくある勘違いやハマりポイントの説明を明瞭化
- 例:
options_for_selectの第2引数で選択状態を指定する方法prompt/include_blankとselectedオプションの組み合わせで起きる挙動selectでnil/ 空文字がどのように扱われるか- 値とラベルの順序(
[[label, value], ...]か[ [value, label], ... ]か)に関する注意
- 「なぜそうなるのか」「どのように書けば意図した挙動になるのか」をはっきり説明するようにリライトされたと考えられます。
- 例:
- コメントのスタイル統一・英語としての読みやすさ向上
- 不自然な表現や曖昧な言い回しの修正
- 段落構成や見出しの整理により、「gotcha」がどこからどこまでなのか、何に注意すべきなのかが把握しやすくなっているはずです。
変更は約 +20 / -9 行で、完全にコメント(ドキュメント)レベルの修正にとどまっています。
- 影響範囲・注意点
- 実行時挙動の変更なし
selectやoptions_for_selectを利用しているアプリケーションのコードが変わる必要はありません。- 既存のテストや挙動に影響はありません。
- 影響するのは「Rails のソースコードを読んで
form_options_helperを理解する開発者」や、「公式ドキュメント生成の元」となるコメント部分のみです。 - ドキュメントが改善されたことで、
selectヘルパに関する過去の微妙なハマりどころを新規/中級ユーザーが理解しやすくなり、誤用やバグの予防に役立ちます。
- 参考情報 (あれば)
- 関連ファイル:
actionview/lib/action_view/helpers/form_options_helper.rbselect,options_for_select,option_groups_from_collection_for_selectなど、フォームのセレクトボックス生成関連ヘルパが定義されているファイル
- Rails ガイド(英語・参考):
- Form Helpers – Select and Option Tags
https://guides.rubyonrails.org/form_helpers.html#select-and-option-tags
この PR のコメント改善内容は、将来的に上記ガイドや API ドキュメントに反映される可能性があります。
- Form Helpers – Select and Option Tags
#57582 Action Cable: only skip eager loading for Redis and Postgres
マージ日: 2026/6/4 | 作成者: @byroot
- 概要 (1-2文で)
Action Cable が「起動時の eager load をスキップする」対象を、Redis と PostgreSQL のサブスクリプションアダプタに限定するように調整した PR です。前 PR (#57249) の挙動をより適切に絞り込んだ小さなリファインメントです。
- 変更内容の詳細
何をしたか
ActionCableの初期化処理まわりで、eager loading をスキップするアダプタを Redis / Postgres のみに限定しました。- Redis アダプタ側ファイルでの「eager loading 抑制」に関する処理が簡略化・削除され、責務が
action_cable.rb側に集約されています。
コミット差分情報から読み取れるポイント:
# actioncable/lib/action_cable.rb
+ # ここで、Redis / Postgres アダプタを使う場合のみ
+ # eager load をスキップするような条件分岐が追加されたと考えられる# actioncable/lib/action_cable/subscription_adapter/redis.rb
- # Redis アダプタ側で行っていた eager load 抑制ロジックを削除
- # (もしくはそれに準ずる初期化まわりの特別扱いを削除)
+ # 共通ロジックに寄せるため、アダプタ固有の分岐が減らされている実際のコードは省略されていますが、PR 説明文から:
Action Cable: only skip eager loading for Redis and Postgres
The other adapters are small enough that it's not really worth skipping them.
とあるため、以下のようなイメージの挙動になっています:
- 環境設定で
adapter: "redis"またはadapter: "postgres"の場合:- Rails 起動時の eager loading 対象から Action Cable まわり(あるいは特定の部分)を外している
- それ以外のアダプタ (例: async, inline, test, etc.) の場合:
- 通常どおり eager loading を行う
前の PR #57249 では、より広い範囲で eager loading をスキップしていた可能性があり、それを「重いアダプタ(Redis/Postgres)だけに限定」した、というのが今回の趣旨です。
- 影響範囲・注意点
影響範囲
- 対象:
- Action Cable を利用していて、
- サブスクリプションアダプタに Redis または Postgres を指定しているアプリケーション。
- 他のアダプタ(
async,test, あるいは独自実装の軽量アダプタなど)では、従来どおり eager loading が行われるようになります。
実質的な影響
- 起動時間・メモリ使用量:
- Redis/Postgres アダプタでは、前 PR で導入された「eager load をスキップして起動負荷を下げる」挙動が維持されます。
- それ以外のアダプタでは、無条件にスキップするのではなく通常どおり eager load されるため、「軽いアダプタを使っているのに Action Cable だけ妙に lazy load される」といった違和感が減ります。
- 予期せぬ遅延初期化の回避:
- 軽量アダプタは eager load をスキップするメリットが小さいため、起動後の最初の接続時に「初めて読み込まれる」ことによるレイテンシが抑えられます。
- 特にテスト環境や単純な非本番構成で、Action Cable の動作確認時に「最初だけ遅い」現象が減る可能性があります。
注意点
- Redis/Postgres を利用している場合:
- eager loading のスキップはそのまま有効なので、「起動直後の最初の Action Cable 利用時に少しだけ遅く感じる」可能性は残りますが、その分起動時の負荷軽減が期待できます。
- eager loading に依存した初期化(例: Railtie / Engine の
eager_load!フックに強く依存するようなカスタムコード)を書いている場合は、Action Cable 関連の挙動に違いがないか確認しておくと安全です。
- 他アダプタを使っていて「前 PR の挙動を前提にしていた」場合:
- 今回の変更で eager loading が再び有効になるため、その前提が崩れていないか注意してください(ただし、そのようなケースはかなりレアだと思われます)。
- 参考情報 (あれば)
- 該当 PR:
- 関連 PR (今回の「よりよい修正」の元になったもの):
- Action Cable ガイド:
- https://guides.rubyonrails.org/action_cable_overview.html
(アダプタ構成や Redis/Postgres の利用形態を確認する際に有用です)
- https://guides.rubyonrails.org/action_cable_overview.html
#57249 Action Cable: explicitly require sibling deps in Redis adapter
マージ日: 2026/6/4 | 作成者: @gotrevor-notarize
- 概要 (1-2文で)
Action Cable の Redis サブスクリプションアダプタが、自身と同じディレクトリ内のBaseとChannelPrefixを明示的にrequireするようにし、Zeitwerk の lazy autoload との競合で起きるスレッドセーフティ問題(NameError)を防ぐ修正です。これにより、Puma などのマルチスレッド/マルチプロセス環境で、稀に Redis アダプタが定義されないままになってしまう致命的な状態を避けられます。
- 変更内容の詳細
背景となる問題
actioncable/lib/action_cable/subscription_adapter/redis.rb の先頭付近には、以下のようなクラス定義があります:
class Redis < Base
prepend ChannelPrefixこのとき Base と ChannelPrefix は、同じ subscription_adapter/ ディレクトリ内の別ファイルに定義されたクラス/モジュールです。
一方で、Action Cable は action_cable.rb 内で次のように subscription_adapter/ ディレクトリを eager_load 対象から除外しています(loader.do_not_eager_load(...))。そのため、Base や ChannelPrefix は Zeitwerk の「lazy autoload(最初に参照されたタイミングでロードする方式)」で解決されます。
この状態で「複数スレッドがほぼ同時に Redis アダプタを初めて参照する」と、以下のようなレースコンディションが発生し得ます:
- あるスレッドが
Baseの autoload を開始 - autoload 管理の途中で別スレッドが
Redis < Baseを評価しようとする - まだ
Baseが定義されていないタイミングに当たるとNameError: uninitialized constant ActionCable::SubscriptionAdapter::Baseが発生 - さらに厄介なのは、一度この競合に負けたプロセスでは autoload ポインタが消費されてしまい、その後
Redisクラスが最後まで定義されないため、該当プロセスの寿命中ずっとNameErrorが起き続ける点
実際に、Rails 7.2.3.1 + Puma クラスタ構成の本番環境で、同一ホスト上の1プロセスがこの状態に入り、以後のブロードキャストがすべて失敗する事象が観測されています。
実際の修正内容
この PR では、Redis アダプタファイルの冒頭に、兄弟クラス/モジュールを明示的に require するコードが追加されています:
# 追加された行
require "action_cable/subscription_adapter/base"
require "action_cable/subscription_adapter/channel_prefix"redis.rb にはもともと次のように外部依存 (redis gem や ActiveSupport の拡張) を明示的に require しているスタイルがあり、その方針に揃えた形になっています:
require "redis"
require "active_support/core_ext/hash/except"
# ... (今回ここに Base/ChannelPrefix の require が追加された)これにより:
Redis < Baseが評価される前に、Baseが確実に定義されているprepend ChannelPrefixが評価される前に、ChannelPrefixが確実に定義されている
という状態が保証され、Zeitwerk の autoload 管理ウィンドウに依存しない、より堅牢な読み込み順序になります。
PR 作成者は、他にも同様のパターンで兄弟クラスに依存しているアダプタ:
subscription_adapter/test.rbsubscription_adapter/async.rbsubscription_adapter/inline.rbsubscription_adapter/postgresql.rb
にも同様の修正を広げる余地があると述べていますが、この PR では実際に不具合が再現・観測された redis.rb のみに絞っています。
また、レースコンディションが本質的に非決定的であるため、自動テストで再現性の高いテストを書くには:
$LOADED_FEATURESを操作したりModule#remove_constで定数を消したり- あるいは別プロセス/subshell を立ち上げる
といった、テストプロセスを汚しがちな手法が必要になることから、本 PR ではテスト追加は見送り、「方針があれば対応する」としています。
- 影響範囲・注意点
影響範囲
- 直接の変更対象は
actioncable/lib/action_cable/subscription_adapter/redis.rbのみ。 - Action Cable で Redis サブスクリプションアダプタを使用しているアプリケーション(特に Puma クラスタやスレッド数多めの構成)で、稀に起きる
NameError常態化問題を解消する効果があります。 - 機能追加ではなく「読み込み順序の明示化」に近い修正のため、正常系の挙動は変わりません。
- 直接の変更対象は
後方互換性
- すでに autoload によって解決されていたクラス/モジュールを
requireで明示的にロードするだけなので、後方互換性への影響はほぼありません。 - 既存アプリ側で
BaseやChannelPrefixを上書きしているような特殊なメタプログラミングをしていない限り、問題になるケースは考えにくいです。
- すでに autoload によって解決されていたクラス/モジュールを
注意点
- 同様の問題を抱えうる他のアダプタ (
test,async,inline,postgresql) にはまだパッチが当たっていないため、Redis 以外のアダプタを使っていても、理屈上は同種のレースが起こり得ます。 - 自前でカスタム subscription adapter を作っている場合も、「クラス定義時に兄弟クラス/モジュールへ依存する」箇所は明示的に
requireを入れる方が安全です。
- 同様の問題を抱えうる他のアダプタ (
- 参考情報 (あれば)
PR 本文で挙げられている関連 Issue / PR:
- rails/rails#50802 — Zeitwerk のデッドロック関連
- fxn/zeitwerk#52 — autoload のスレッドセーフティに関する議論
- fxn/zeitwerk#198 — Sidekiq + Bootsnap 環境での autoload race 問題
Action Cable 側の方針:
actioncable/lib/action_cable.rbのdo_not_eager_load行には「Adapters are required and loaded on demand」とコメントされており、「アダプタファイル自身が依存をrequireする」設計意図であることが明示されています。- 今回の修正はその設計意図に沿った形で、Redis アダプタをその方針に揃えたものと言えます。
#57576 Resolve attribute aliases in the update_attribute readonly check
マージ日: 2026/6/4 | 作成者: @55728
- 概要 (1-2文で)
update_attribute/update_attribute!が readonly 属性をチェックする際に「属性エイリアス」を解決していなかった問題を修正し、update_columnsと同じくエイリアス経由でも readonly 制約が正しく効くようにした PR です。これにより、readonly なカラムのエイリアスに対してupdate_attributeで更新しようとした場合も、期待どおり例外が発生します。
- 変更内容の詳細
問題の背景
update_attribute / update_attribute! はドキュメント上、readonly な属性を更新しようとすると ActiveRecord::ActiveRecordError を投げることになっていますが、実装では「属性エイリアス」が考慮されていませんでした。
元の実装(抜粋)は以下のようになっています。
def update_attribute(name, value)
name = name.to_s
verify_readonly_attribute(name) # <- alias が解決されていない
public_send("#{name}=", value)
save(validate: false)
endverify_readonly_attribute(name) → readonly_attribute?(name) → _attr_readonly.include?(name) という流れで readonly 判定を行いますが、_attr_readonly には「カラムの正規(canonical)名」だけが入っており、エイリアス名は入っていません。そのため、エイリアスを渡すと readonly チェックをすり抜けてしまっていました。
一方で update_columns はすでにエイリアスを解決してから readonly チェックをしており、こちらは正しい挙動になっています。
name = self.class.attribute_aliases[name] || name
verify_readonly_attribute(name)実際に起きていた不整合
config.active_record.raise_on_assign_to_attr_readonly = false(デフォルト。config.load_defaults 7.1 以降は true) の場合に顕在化します。
class Account < ActiveRecord::Base
alias_attribute :available_credit, :credit_limit
attr_readonly :credit_limit
end
a = Account.create!(credit_limit: 5)
a.update_attribute(:credit_limit, 77) # => ActiveRecord::ActiveRecordError (期待どおり)
a.update_columns(available_credit: 99) # => ActiveRecord::ActiveRecordError (こちらも正しい)
a.update_attribute(:available_credit, 88) # => true (例外が出ない)
a.credit_limit # => 88 (メモリ上では変更される)
Account.find(a.id).credit_limit # => 5 (DB には永続化されず、書き込みがサイレントに失われる)- canonical 名(
credit_limit)の場合:update_attributeが先にverify_readonly_attributeで弾くため、常に例外。 - エイリアス名(
available_credit)の場合:verify_readonly_attributeがエイリアスを認識できず通過。raise_on_assign_to_attr_readonly = falseなので、writer 側でも例外は出ない。- 結果、メモリ上の値だけ変わり、
save(validate: false)では UPDATE から落ちるため DB は変わらない。 - それにもかかわらず
update_attributeはtrueを返す。
これは「canonical 名なら必ず例外」「エイリアス名なら成功したように見えるが実は保存されない」という一貫性のない挙動でした。
修正内容
update_attribute / update_attribute! において、readonly チェックの前に属性エイリアスを解決するように変更しています。update_columns と同じやり方です。
def update_attribute(name, value)
name = name.to_s
name = self.class.attribute_aliases[name] || name # ここを追加
verify_readonly_attribute(name)
public_send("#{name}=", value)
save(validate: false)
endattribute_aliasesに該当があれば canonical 名に置き換え。- 該当がなければそのまま(既存の挙動と同じ)。
update_attribute!も同様の修正。
テスト
activerecord/test/cases/base_test.rb に以下のようなテストを追加(概略):
NonRaisingPost(raise_on_assign_to_attr_readonly = false)に対して、alias_attribute :headline, :titleattr_readonly :title
update_attribute(:title, ...)/update_attribute(:headline, ...)update_attribute!(:title, ...)/update_attribute!(:headline, ...)
のすべてが ActiveRecord::ActiveRecordError を投げることを確認。
このテストは main ブランチでは alias に対してのみ失敗(例外が出ない)し、本 PR 適用後に通ることが確認されています。
sqlite3 / postgresql / mysql2 でグリーン。
- 影響範囲・注意点
- 影響範囲:
- readonly 属性(
attr_readonly)を定義しているモデルで、 - その属性に
alias_attributeを定義し、 update_attribute/update_attribute!を使ってエイリアス名経由で更新しているコード に影響があります。
- readonly 属性(
- 挙動の変化:
- これまで「エイリアス名のみ例外が出ず、DB に保存されない」というバグがありましたが、今後はエイリアス名でも
ActiveRecord::ActiveRecordErrorが発生します。 - canonical 名(元のカラム名)に対する挙動は変わりません。
update_columnsはすでに同様の挙動で動いていたため、update_columnsから見れば挙動は変わりません。
- これまで「エイリアス名のみ例外が出ず、DB に保存されない」というバグがありましたが、今後はエイリアス名でも
- 設定との関係:
config.active_record.raise_on_assign_to_attr_readonly = true/falseにかかわらず、update_attribute/update_attribute!は「readonly 属性(canonical / alias を問わず)」に対しては例外を投げる、という一貫した挙動になります。
raise_on_assign_to_attr_readonlyは通常の属性代入(record.attr = ...)側の挙動に影響しますが、update_attributeはその前にガードするため、今回の修正で一貫性が増しています。
- 互換性上の注意:
- もし既存コードが「エイリアス経由で readonly 属性を
update_attributeし、例外が出ないこと」を前提にしていた場合、今回から例外が出るようになります。 - ただし、本来は書き込みがサイレントにドロップされておりバグに近い状態だったため、この変更はバグフィックスとして扱うのが自然です。
- もし既存コードが「エイリアス経由で readonly 属性を
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57576
- 関連する以前の議論: https://github.com/rails/rails/pull/45060
- こちらは「エイリアス自体に
attr_readonlyを指定した場合にどう扱うか」という設計論争でクローズされたもの。 - 今回は「canonical 側が readonly のときに、すでに
update_columnsでは alias も readonly とみなしている」という既存仕様との内部整合を取るだけの変更であり、新しい設計判断ではない、という位置づけです。
- こちらは「エイリアス自体に
#56375 Make ActionCable unsubscribe idempotent
マージ日: 2026/6/4 | 作成者: @matthewbjones
- 概要 (1-2文で)
ActionCable の「unsubscribe(購読解除)」を同じチャンネルに対して何度呼んでもエラーにならない“冪等(idempotent)”な挙動に変更した PR です。購読解除時だけは、存在しないサブスクリプションに対する unsubscribe をエラーではなく「何もしない成功」とみなしつつ、perform_actionは従来通り見つからなければエラーを出すように区別しています。
- 変更内容の詳細
挙動の変更ポイント
従来の問題点:
- クライアントが既に削除済みのサブスクリプションに対して
unsubscribeするとRuntimeErrorが発生。 - その例外は
execute_commandの rescue に捕まってrescue_with_handlerに流れるため、rescue_fromで Sentry などに通知している場合、実害のない race condition 由来のエラーが大量にレポートされてしまう。 - 「unsubscribe は冪等であるべき」という意味論にも反していた。
この PR の方針:
- サブスクリプション検索メソッド
findの振る舞いを変更し、「見つからない場合は例外ではなくnilを返す」ようにした。 - そのうえで、
findの呼び出し元ごとに「見つからない場合の扱い」を変える:remove(unsubscribe 相当):nilなら何もしないで return(冪等な no-op)perform_action: 依然として「存在しないサブスクリプション」はクライアントのバグとみなし、明示的に例外を発生させる
Subscriptions#find の変更
元のコード(イメージ)では、find が存在しない ID に対して RuntimeError を上げていました。この PR では:
def find(id)
subscriptions[id] # 見つからなければ nil を返す
endのように、単純に nil を返す方向に変更され、その結果として呼び出し側の責務が変わっています。
remove(unsubscribe)の変更
remove はクライアントからの unsubscribe コマンドに対応します。ここで find の戻り値をチェックし、見つからない場合は「何もせずに終わる」ようになりました。
擬似コード例:
def remove(id)
subscription = find(id)
return unless subscription # 存在しなければ no-op
# 存在すれば従来通り unsubscribe 処理
subscription.unsubscribe_from_channel
subscriptions.delete(id)
endこれにより、以下のようなケースがすべて成功扱い(例外なし)になります:
- サブスクリプションが既に解除済みなのに、同じ ID に対して再度
unsubscribeが届いた。 - サーバ再起動などでサーバ側のサブスクリプション情報が消えているが、クライアントが古い ID で
unsubscribeしてきた。
perform_action の変更
perform_action はクライアントがサブスクリプションを通じてメッセージを送る際に使われます。ここはクライアントが「存在するはずのサブスクリプションに対し」アクションを実行する前提のため、見つからなければ引き続きエラーにします。
擬似コード例:
def perform_action(data)
subscription = find(data["identifier"])
raise "Subscription not found" unless subscription
subscription.perform_action(data["data"])
endこの区別により:
- 「unsubscribe だけ」は冪等でノイズにならない
- 「存在しないサブスクリプションに対する perform_action」は依然としてアプリのバグとして報告される
テストの追加・変更
actioncable/test/connection/subscriptions_test.rb:
- すでに存在しないサブスクリプションに対して
unsubscribeしても例外が発生しないこと(no-op で終わること)を確認するテストを追加。 perform_actionが存在しないサブスクリプション ID を指定した際には例外が上がることを確認するテストも追加/更新。
actionpack/test/dispatch/content_disposition_test.rb:
- 行数増加がありますが、この PR の主目的(ActionCable の挙動変更)とは直接関係しない微調整・テスト整備と思われます(CI 通過や関連テストの整合性確保のため)。
- 影響範囲・注意点
影響範囲:
- ActionCable を使っているアプリのうち、
- Turbo Streams や独自の JS クライアントなどで「短い間隔で subscribe/unsubscribe を繰り返す」ようなケース
- 接続の再試行やタブの閉じ開けなどで race condition が起きやすいケース で、これまで
unsubscribe時に発生していた RuntimeError が発生しなくなります。
rescue_fromを使って ActionCable の例外をエラー監視サービスに飛ばしている場合、「unsubscribe 関連のノイズ」が減ることが期待できます。
注意点:
- もしアプリ側が「unsubscribe でサブスクリプションが見つからないこと自体を何らかのロジックで検知」していた場合、今回の変更でその検知はできなくなります(単なる成功扱いになる)。
- ただし、そのようなユースケースは通常想定されておらず、「unsubscribe は冪等である」という意味論に沿った変更なので、多くのアプリでは問題にならないはずです。
- 一方で、
perform_actionでの「サブスクリプション見つからず」は引き続き例外となるため、ここに依存したエラーハンドリングは今まで通り動作します。
- 参考情報 (あれば)
- 関連 Issue:
- #25381: 2016 年から報告されていた「ActionCable unsubscribe のエラー」問題
- 関連 PR:
- #30702: エラーメッセージは改善したものの、「見つからないと例外を上げる」挙動自体は維持していた PR
- この PR #56375 での位置づけ:
- 過去 PR でのメッセージ改善に加えて、unsubscribe の意味論(冪等性)を実装レベルで正しく反映し、「本当にバグであるケース(perform_action)」だけを例外として残す、という最終的な整理になっています。
#57577 Fix incorrect callback ordering note in Active Record callbacks docs
マージ日: 2026/6/4 | 作成者: @55728
- 概要 (1-2文で)
Active Record のコールバック順序に関するドキュメントの記述ミスを修正し、「メソッドで定義したコールバックだけ最後に呼ばれる」という誤った注記を削除した PR です。実際の挙動通り、すべてのコールバックが「定義された順に実行される」とだけ記載されるようになりました。
- 変更内容の詳細(あればサンプルコードも含めて)
対象: activerecord/lib/active_record/callbacks.rb のドキュメントコメントのみが変更されています。
実装コードの変更は一切なく、「キャンセル可能なコールバック (Canceling callbacks)」節の文章を修正しています。
修正前の問題となっていた記述
以前のドキュメントには、概ね次のようなニュアンスの文言がありました:
コールバックは一般に定義された順番で実行されますが、モデル上のメソッドとして定義されたコールバックだけは最後に呼び出されます。
この「メソッドとして定義されたものは最後に呼ばれる」という例外ルールが、現行の Rails では事実と異なります。
実際の挙動
PR では、以下のような検証コードを示しています:
class Widget < ActiveRecord::Base
attr_reader :log
after_initialize { @log = [] }
before_save -> { log << :proc1 }
before_save :method1
before_save -> { log << :proc2 }
before_save :method2
def method1 = log << :method1
def method2 = log << :method2
end
Widget.create!.log
# => [:proc1, :method1, :proc2, :method2]before_saveに対して- Proc 形式 (
-> { ... }) - シンボルで指定したメソッド (
:method1,:method2) を交互に登録していますが、実行順は 定義順どおり になっています。
- Proc 形式 (
もし古いドキュメントの記述が正しければ、[:proc1, :proc2, :method1, :method2]
のように「メソッドで定義したコールバックが最後に固まって実行」されるはずですが、実際にはそうなっていません。
歴史的背景
PR では、この誤記がどこから来たかも説明しています。
2005 年ごろ(pre Rails 1.0)の古い実装では、
- 「マクロで登録されたコールバック (before_save :foo のようなもの)」
- 「オーバーライド可能なインスタンスメソッド (def before_save; end)」 という “二系統” の仕組みがあり、
- まずマクロ登録されたコールバック群を実行し、
- そのあとでオーバーライドされたメソッド本体を最後に
sendで呼ぶ
という挙動でした。
これはドキュメントが言っている「メソッドが最後に呼ばれる」挙動と一致します。
Rails 2.3 の API ドキュメントまでは、この二つのスタイルが共存していました。
その後、
ActiveSupport::Callbacksによってコールバック機構が統一され、- すべてのコールバックが「登録された順に」実行されるモデルに変更。
- しかし、古い挙動を説明した文言だけが 20 年近く残り続けていました。
今回の変更
該当の文言をシンプルに次のように修正しています:
コールバックは定義された順番に実行されます。
つまり「例外(メソッドだけ最後に呼ばれる)」という一文を削除しただけです。
- 影響範囲・注意点
コードの挙動は一切変わりません。
以前から Rails の実際の実装は「定義順で実行」になっており、その仕様にドキュメントを合わせただけです。すでに「メソッドで定義したコールバックは最後に寄せられるはず」と誤解していたコードがある場合:
- その前提はもともと成立しておらず、Rails の動作としては 常に定義順だった ことになります。
- この PR をきっかけにドキュメントを読み直した際、「アプリの期待と違うのでは?」と感じた場合は、アプリ側のコールバック定義順を見直す必要があります。
コールバックの種類(シンボル/Proc/ブロック)に関係なく、登録順がそのまま実行順になる、というのが現在の正しい仕様です。
- 参考情報 (あれば)
- 当該 PR: https://github.com/rails/rails/pull/57577
- 言及されている古いコミット:
823554eafe - 古いドキュメント例(Rails 2.3.8 の ActiveRecord::Callbacks):
https://api.rubyonrails.org/v2.3.8/classes/ActiveRecord/Callbacks.html
コールバック順序を前提にしたロジックを書いている場合は、
「定義順で実行される」「メソッド定義だからといって別枠で最後に呼ばれたりしない」
という点を明確にしておくと、安全です。
#57088 Disconnect pools while cycling tests' connection handlers
マージ日: 2026/6/4 | 作成者: @matthewd
- 概要 (1-2文で)
CI で頻発していた「FATAL: sorry, too many clients already」(PostgreSQL の接続数上限超え)を避けるため、テスト実行中に接続ハンドラを切り替える際に、既存の接続プールを明示的に切断するようにした PR です。テスト用の connection handler のライフサイクル管理を強化し、使い終わった DB 接続が確実に解放されるようにしています。
- 変更内容の詳細
※実際の diff 構造からの推測を含みますが、Rails の既存テストコードと整合的な内容です。
a. test_fixtures 周りでの connection handler 切り替え時に disconnect を実行
変更ファイル: activerecord/lib/active_record/test_fixtures.rb
- Rails のテストで
use_transactional_testsや fixtures を使うとき、テストごと/スレッドごとにActiveRecord::Base.connection_handlerを入れ替える処理があります。 - これまでは「新しい connection handler をセットする」だけで、古い handler が保持していた connection pool を積極的に閉じていませんでした。
- この PR では、connection handler を「サイクル(入れ替え)」するときに、古い handler が持っている全ての connection pool を
disconnect!する処理を追加しています。
イメージ的な処理(概念的なサンプル):
def cycle_connection_handler
old_handler = ActiveRecord::Base.connection_handler
new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
ActiveRecord::Base.connection_handler = new_handler
# ★ ここが今回追加されたポイント
# 以前の handler が管理していた各プールを明示的に切断
old_handler.connection_pool_list.each do |pool|
pool.disconnect!
end
endポイント:
- 「handler を捨てるだけ」だと、PostgreSQL などのサーバ側から見るとコネクションが残り続け、CI のように大量テストを走らせる環境で「too many clients already」が発生し得る。
disconnect!を呼ぶことで、Ruby プロセス内でも DB サーバ側でもコネクションがクローズされるようになります。
b. テストケース基底クラス側での接続ハンドラ管理を強化
変更ファイル: activerecord/test/cases/test_case.rb
ActiveRecord のテストケース基底クラスで、
setup/teardown周りに connection handler の切り替え・復元・切断の処理が追加・整理されています。例えば、テストごとに新しい handler を使う場面で:
- テスト開始前に
original_handlerを保存 - テスト中は専用の handler を使用
- 終了時に:
- テスト用 handler の pool を
disconnect! original_handlerに戻す
- テスト用 handler の pool を
- テスト開始前に
という流れをきちんと保証するようなコードが追加されています。
c. connection handler の挙動を確認するテスト追加
変更ファイル:
activerecord/test/cases/connection_adapters/connection_handler_test.rbactiverecord/test/cases/test_fixtures_test.rb
主なテスト内容(要約):
connection handler を差し替えた際に、古い handler の connection pool が適切に
disconnect!されるかを確認するテスト- handler 入れ替え前後で、
connection_pool_listやactive_connections?などの状態を確認。 - 入れ替え後に「古い方の接続が残っていない」ことを assertion しています。
- handler 入れ替え前後で、
fixtures / transactional tests と接続サイクルの組み合わせのテスト
- fixtures を使うテストケースを擬似的に実行し、テスト終了後にプールが切断されているかを検証するテストが追加されています。
- 特に並列テスト実行や複数 DB (multi-DB) のケースを意識したテストが含まれている可能性が高いです。
- 影響範囲・注意点
影響範囲
- **対象は主に Rails 自身のテスト基盤(ActiveRecord のテストケース・fixtures 用ユーティリティ)**であり、アプリケーションコードのランタイム挙動には基本的に影響しません。
- ただし、以下のようなケースでは間接的に影響を受ける可能性があります:
- Rails アプリ側で Rails の内部テストヘルパ (
ActiveRecord::TestFixtures/ActiveRecord::TestCase) をそのまま流用している場合 - 独自に
ActiveRecord::Base.connection_handlerを差し替えてテストしているような高度なユースケース
- Rails アプリ側で Rails の内部テストヘルパ (
技術的ポイント・注意点
- connection handler を切り替えるときに、不要になったプールは必ず
disconnect!すべきという方針が明確になりました。 - これにより:
- テスト実行時間中に開きっぱなしになる DB 接続数が減る
- CI やコンテナ環境のように接続上限の小さい環境で、
too many clients alreadyが起きにくくなる
- 逆にいうと、アプリや独自テストで:
ActiveRecord::Base.connection_handler = ...のように handler を差し替えているにも関わらず- 古い handler を保持しっぱなし+
disconnect!もしていない といったパターンがある場合、同様の接続リークが起こりうるため、この PR の方針を参考にして明示的にdisconnect!を呼ぶのが安全です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57088
- CI エラーの例:
ActiveRecord::ConnectionNotEstablished: connection to server at "172.16.0.4", port 5432 failed: FATAL: sorry, too many clients already - 関連概念:
ActiveRecord::Base.connection_handlerActiveRecord::ConnectionAdapters::ConnectionHandler#connection_pool_listActiveRecord::ConnectionAdapters::ConnectionPool#disconnect!
この PR は、「connection handler を切り替えるときに古いプールを必ず閉じる」という運用ルールをテスト基盤に実装したものと捉えると理解しやすいです。
#57562 Add test coverage for String filter boundary inputs
マージ日: 2026/6/4 | 作成者: @hammadxcm
- 概要 (1-2文で)
String拡張のフィルタ系メソッド(remove,remove!,truncate_words)について、これまでテストされていなかった「何もしない境界ケース(no-op)」に対するテストが追加されました。アプリケーションコードには一切変更がなく、テストコードのみの追加です。
- 変更内容の詳細
対象は active_support/core_ext/string/filters.rb にある以下のメソッドの挙動確認用テストです。
String#remove の境界ケース
- ケース: 引数にパターンを一切渡さない場合
- 期待挙動:
- 文字列内容は変わらない(no-op)
- 返り値は元の文字列とは別オブジェクト(コピー)
テストのイメージ(実テストに沿った形で表現すると):
str = "abc"
result = str.remove
assert_equal "abc", result # 内容は同じ
refute_equal str.object_id, result.object_id # 別オブジェクトRails の String#remove は通常以下のように使いますが:
"hello world".remove("l") # => "heo word"今回のテストは「引数なしで呼んだとき」に何も削除されず、かつコピーが返ることを保証しています。
String#remove! の境界ケース
- ケース: 引数にパターンを一切渡さない場合
- 期待挙動:
- 自身の内容は変わらない(no-op)
- 戻り値は
self(破壊的メソッドとしての一貫した挙動)
テストイメージ:
str = "abc"
result = str.remove!
assert_same str, result # self を返す
assert_equal "abc", str # 内容は変わらない既存テストは「マッチするパターンを渡した場合」だけだったため、「引数なしで呼び出した場合の no-op 性」と「self を返すこと」が今回新たにカバーされています。
String#truncate_words の境界ケース
- ケース1:
lengthに 0 を渡した場合 - ケース2:
lengthに負の値を渡した場合
どちらも 「元の文字列をそのまま返す(no-op)」 という挙動をテストしています。
テストイメージ:
str = "one two three"
# length = 0
assert_equal str, str.truncate_words(0)
# length < 0
assert_equal str, str.truncate_words(-1)既存テストでは、
- 正の値
- かつ、単語数以上のカウント
といった通常系・上限系のみがカバーされていたため、ゼロ・負値という「境界かつ不正寄りの引数」に対する挙動が明示的に確認されました。
変更ファイル
activesupport/test/core_ext/string_ext_test.rbにテストが 25 行追加- 本体コード (
filters.rb) には変更なし
- 影響範囲・注意点
- 機能面の影響: 既存の挙動を確認するテスト追加のみのため、実行時挙動に変更はありません。
- 将来のリグレッション検知:
removeが「引数なしで呼んだときに別オブジェクトを返す」という仕様remove!が「引数なしで no-op + self を返す」という仕様truncate_wordsが「0 もしくは負のlengthで no-op」という仕様
これらが、将来の refactor などで壊れた際に CI で検知できるようになります。
- 利用側での前提にできること:
truncate_wordsに 0 や負数が渡っても例外にはならず、そのまま文字列が返る前提でロジックを書いてよい(ただし仕様依存になるので、異常値をアプリ側で弾きたいかどうかは別途設計判断)。removeを引数なしで呼んでも安全(no-op)で、かつ破壊はされない。remove!を引数なしで呼んでも安全(no-op)で、オブジェクトは同一。
- 参考情報 (あれば)
- 対象メソッド実装:
active_support/core_ext/string/filters.rb
- 類似 API:
String#delete/String#delete!(Ruby 標準)とのインターフェイス類似性・破壊/非破壊の挙動差分を確認する際に、今回のテストの期待値が参考になります。
- この PR(#57562)は、テストカバレッジ向上が目的であり、仕様追加・変更ではなく「仕様のドキュメンテーションとしてのテスト」の意味合いが強いです。
#57571 Re-raise suppressed RedisClient errors in RedisCacheStore behavior tests
マージ日: 2026/6/4 | 作成者: @yahonda
- 概要 (1-2文で)
RedisCacheStoreCommonBehaviorTestにおいて、Redis 接続のタイムアウトなどでRedisClient::ConnectionErrorが発生した場合に、failsafeによって握りつぶされていたエラーをテスト専用のerror_handlerで再スローするようにし、テスト失敗時に原因が分かりやすくなるようにした PR です。
本番コードの挙動は変えず、遅い CI 環境での「原因不明の nil 返却によるテスト失敗」を「RedisClient のタイムアウトなどが見える失敗」に置き換えるためのテスト改善です。
- 変更内容の詳細
背景
RedisCacheStoreCommonBehaviorTestでは以下のような条件でテストが実行されている:pool: falsetimeout: 0.1秒
- Ruby trunk の debug ビルドなど、非常に遅い環境では
- Redis への connect / read が 0.1 秒を超えることがある
- その結果
RedisClient::ConnectionError(具体的にはReadTimeoutErrorやCannotConnectError)が発生
- しかし
RedisCacheStore#failsafeは、こうしたRedisClientのエラーを rescue してnilを返す仕様になっているため、- たとえば
@cache.write(...)がnilを返し、テストアサーションが「truthy であること」を期待しているとExpected nil to be truthy.という、原因が分かりづらい失敗になる
- たとえば
- テスト環境では suppressed されたエラーがログにも出ないため、「0.1 秒の timeout が原因」というヒントが出ないのが問題だった。
どのような修正か
変更ファイル: activesupport/test/cache/stores/redis_cache_store_test.rb (+4行)
RedisCacheStoreCommonBehaviorTest 内で lookup_store をオーバーライドし、RedisCacheStore 生成時に error_handler オプションを付与するようにしています。この error_handler は、通常 failsafe によって握りつぶされるはずの RedisClient 関連エラーを再スローします。
イメージとしては以下のような変更です(実際のコードから概念的に再構成):
class RedisCacheStoreCommonBehaviorTest < ActiveSupport::TestCase
private
def lookup_store(*args)
super(*args, error_handler: ->(method:, returning:, exception:, **) do
# failsafe が rescue したエラーをここで再スローする
raise exception
end)
end
endポイント:
error_handlerはRedisCacheStoreに元々用意されているフックで、failsafe内部で「エラーが発生した時にどう扱うか」を差し込める仕組み。- 通常は
- 本番などでは「ログに出して nil で握りつぶす」などの使い方をする
- この PR ではテストクラスに限定して
- 「
failsafeが握りつぶそうとしているエラーを、そのまま再スローする」ハンドラを設定
- 「
- これにより、例えば
@cache.write('key', 'value')が timeout により失敗した場合、- 以前:
nilが返ってExpected nil to be truthyとなるだけ - 以後:
RedisClient::ReadTimeoutErrorとしてテストが Error で落ちる
- 以前:
変更前後の出力の違い
変更前:
textFailure: ActiveSupport::Cache::RedisCacheStoreTests::RedisCacheStoreCommonBehaviorTest#test_retains_encoding: Expected nil to be truthy.変更後:
textError: ActiveSupport::Cache::RedisCacheStoreTests::RedisCacheStoreCommonBehaviorTest#test_retains_encoding: RedisClient::ReadTimeoutError: user specified timeout for redis:6379 (redis://redis:6379/1)
これにより、どのテストで、どの Redis 接続に対して、どの種類のエラー(例: ReadTimeoutError)が起きたかが明示されます。
なぜ RedisCacheStoreCommonBehaviorTest のみにスコープしているか
FailureSafety/FailureRaisingといったテスト群は、わざと到達不能な Redis を向くようにして- 「失敗時にどう振る舞うか」をテストしている
- そこに「エラーを必ず再スローする
error_handler」を差し込んでしまうと- 本来の挙動テストの意味が失われる
- 期待している「失敗時に nil を返す」「ログに残す」などが確認できなくなる
- そのため、本 PR の変更は
RedisCacheStoreCommonBehaviorTestクラスのlookup_storeのみに限定されており、他のテストには影響しないようになっている。
- 影響範囲・注意点
- 影響範囲は テストコードのみ:
- 変更ファイルは
activesupport/test/cache/stores/redis_cache_store_test.rb1 ファイル - 行数変更も +4 行のみで、本番環境の
RedisCacheStore実装には手を加えていない
- 変更ファイルは
- 通常の(十分高速な)環境では、タイムアウトや接続エラーが発生しない限り
error_handlerは呼ばれないため、- 既存のテストが通っている状況では挙動は変わらない
- 目的は「不安定な CI や遅いビルド環境で、失敗の原因を可視化すること」であり、
- 不安定さを解消するためのリトライや timeout 値の調整ではなく、
- あくまで「原因を明示する」ための改善である点に注意
- エラーが再スローされることで、これまで
FailureとしてカウントされていたケースがErrorになる可能性はあるが、- そもそも本質的には Redis タイムアウトによるエラーなので、より正しい状態に近づいたといえる
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57571
- 影響するテストクラス:
ActiveSupport::Cache::RedisCacheStoreTests::RedisCacheStoreCommonBehaviorTest
- 関連クラス / 機能:
ActiveSupport::Cache::RedisCacheStoreRedisCacheStore#failsafeRedisClient::ConnectionError(ReadTimeoutError,CannotConnectErrorなど)error_handlerオプション(RedisCacheStoreのエラー処理フック)
#57549 Test Type::Boolean#serialize and #serialize_cast_value
マージ日: 2026/6/4 | 作成者: @hammadxcm
- 概要 (1-2文で)
このPRは、ActiveModel::Type::Booleanの#serializeおよび#serialize_cast_valueに対するテストを追加し、既存の挙動(#castと同等の振る舞い)を明示的に担保するものです。プロダクションコードの変更は一切なく、テストコードのみが追加されています。
- 変更内容の詳細
対象クラス
ActiveModel::Type::Boolean
これまでは #cast のみがテストされており、以下のメソッドは暗黙的にしかカバーされていませんでした。
#serialize#serialize_cast_value
このPRでは、これら2つのメソッドに対して、実際の仕様どおりの動作が行われていることを確認するテストが追加されています。
#serialize の仕様確認
PR説明文から、#serialize の期待される挙動は次のとおりです。
- 「偽」とみなされる値 →
falseにシリアライズ - 空文字列/
nil→nilにシリアライズ - 上記以外の値 →
trueにシリアライズ
ここでの「偽」とみなされる値は、#cast と同じルールで判定されます。代表的には次のようなものです。
type = ActiveModel::Type::Boolean.new
type.serialize(false) # => false
type.serialize("false") # => false
type.serialize(0) # => false(false 相当とみなされる値)
type.serialize(nil) # => nil
type.serialize("") # => nil
type.serialize(true) # => true
type.serialize("1") # => true
type.serialize("abc") # => true
type.serialize(42) # => true今回のテストでは、#cast と同等のマッピングを行っていることが確認されます。つまり、「DBなどに書き出す直前の値も、キャスト時と同じように true/false/nil に正規化される」という仕様をテストで固定しています。
#serialize_cast_value の仕様確認
#serialize_cast_value は、すでに #cast 済みの値を受け取る想定のメソッドです。
PR説明文によると、その挙動は「渡された値をそのまま返す(パススルー)」です。
type = ActiveModel::Type::Boolean.new
casted_true = type.cast("1") # => true
casted_false = type.cast("0") # => false
casted_nil = type.cast(nil) # => nil
type.serialize_cast_value(casted_true) # => true
type.serialize_cast_value(casted_false) # => false
type.serialize_cast_value(casted_nil) # => nil今回のテストでは、この「変換を行わずに値をそのまま返していること」を保証します。
追加されたテストファイルの内容
変更は以下の1ファイルのみです。
activemodel/test/cases/type/boolean_test.rb(+17/-0)
中身としてはおおむね次のような構成が想定されます(擬似コード):
class ActiveModel::Type::BooleanTest < ActiveModel::TestCase
def setup
@type = ActiveModel::Type::Boolean.new
end
test "serialize mirrors cast semantics" do
assert_equal false, @type.serialize("false")
assert_nil @type.serialize(nil)
assert_nil @type.serialize("")
assert_equal true, @type.serialize("something")
# ...など、false/nil/true 各パターンの確認
end
test "serialize_cast_value is pass-through" do
assert_same true, @type.serialize_cast_value(true)
assert_same false, @type.serialize_cast_value(false)
assert_nil @type.serialize_cast_value(nil)
end
end実際にはもう少し具体的な値がテストされているはずですが、意図としてはこのレベルです。
- 影響範囲・注意点
影響範囲
- コード変更はテストファイルのみであり、ランタイムの挙動は変わりません。
- 既存アプリケーションに対する互換性への影響はありません。
- CIでのテストカバレッジが向上し、
Boolean型シリアライズ周りのリグレッション検知能力が上がります。
注意点 / 読み取り方
#serializeと#castが事実上同じルールで true/false/nil を決定する前提が、テストによって固定化されました。
今後この仕様を変えようとする場合には、テスト修正が必要になります。#serialize_cast_valueが「必ずパススルーである」という仕様も固定されるため、ここで追加の正規化や変換を行う設計は、今後行いにくくなります(行う場合はテストとの矛盾を明示的に解消する必要があります)。- 逆に言えば、「DBに書き込むタイミングで
Booleanが意図せず再解釈される」ということはなく、castされた値がそのまま保存されることがテストで保証された、と捉えることもできます。
- 参考情報 (あれば)
- Rails ガイド(英語): Active Model Basics – Type Cast
ActiveModel::Type系の仕組み・キャストの流れがまとまっています。 - 実際の
ActiveModel::Type::Boolean実装:activemodel/lib/active_model/type/boolean.rb
(このPRでは変更されていませんが、#cast,#serialize,#serialize_cast_valueの実装を確認する際の参照になります)
#57565 Preserve index attributes when renaming an index without native support
マージ日: 2026/6/3 | 作成者: @55728
- 概要 (1-2文で)
rename_indexのフォールバック実装(ネイティブな index rename を持たないアダプタ用)で、partial index のWHEREや列ソート順などの属性が失われていた問題を修正し、元の index 属性を保ったままリネームされるようにした PR です。主に SQLite や古い MySQL/MariaDB で、開発・テスト時の index 挙動が意図せず変わる問題を解消します。
- 変更内容の詳細(あればサンプルコードも含めて)
何が問題だったか
ネイティブな ALTER INDEX … RENAME を持たないアダプタでは、rename_index は以下のようなフォールバック実装になっていました(概念的に):
def rename_index(table, old_name, new_name)
old_index = indexes(table).find { |i| i.name == old_name }
remove_index(table, name: old_name)
add_index(table, old_index.columns, name: new_name, unique: old_index.unique?)
endこのとき add_index に渡しているのは columns と unique だけなので、元の index が持っていた以下のような属性が すべて失われる という問題がありました。
where(partial index の WHERE 句)orders(列ごとの ASC/DESC)lengths(プレフィックス長など)opclassesusing(USING btree 等)typeinclude(covering index の INCLUDE 列)nulls_not_distinctcomment(MySQL/MariaDB の index コメント)
問題例:
add_index :posts, :views,
where: "title IS NOT NULL",
name: "idx",
order: { views: :desc }
rename_index :posts, "idx", "idx2"
# 修正前:
# - WHERE 句が消える
# - order 情報も消える
# - 結果として単なる (views) の index になる特に SQLite では rename_column 実装の一部として index の付け替えに rename_index が使われるため、開発・テスト環境の SQLite で:
- unique な partial index の「どの行が一意制約の対象か」が変わってしまう
- 本番(PostgreSQL など)とは違う挙動になる
という、かなり危険なズレが起きうる状態でした。
今回の修正内容
フォールバックパス(super による実装)で index を作り直す際に、IndexDefinition が持っている各種属性を add_index に引き継ぐようにしました。
具体的には以下の属性を、nil / 空でなければ渡します:
whereorderslengthsopclassesusingtypeincludenulls_not_distinctcomment(SQLite では無意味だが、MySQL/MariaDB のフォールバック向けに保持)
イメージ:
def rename_index(table_name, old_name, new_name)
index = indexes(table_name).find { |i| i.name == old_name }
options = {
name: new_name,
unique: index.unique
}
options[:where] = index.where if index.where
options[:order] = index.orders if index.orders.present?
options[:length] = index.lengths if index.lengths.present?
options[:opclass] = index.opclasses if index.opclasses.present?
options[:using] = index.using if index.using
options[:type] = index.type if index.type
options[:include] = index.include if index.include.present?
options[:nulls_not_distinct] = index.nulls_not_distinct if index.nulls_not_distinct
options[:comment] = index.comment if index.comment
remove_index(table_name, name: old_name)
add_index(table_name, index.columns, **options)
end(実際のコードは既存の抽象実装に最小限の変更を加える形ですが、ロジックとしては上記のように「元 index の属性をすべて合成して渡す」方針になっています。)
テスト
以下のテストが追加されています:
test_rename_index_preserves_where_clausesupports_partial_index?を満たすアダプタでのみ実行- rename 前後の
where属性を比較し、保持されていることを確認
test_rename_index_preserves_ordersupports_index_sort_order?を満たすアダプタでのみ実行- rename 前後の
orders属性({ column_name => :asc/:desc })を比較
SQLite3 では修正前に where が nil、orders が {} に変わることを確認し、修正後は preserved になることを確認済み。PostgreSQL ではもともとネイティブ rename (ALTER INDEX ... RENAME) を使っているため挙動に変更はなく、テストもグリーンのままです。
CHANGELOG にも「rename_index が index 属性を保持するようになった」旨が追記されています。
- 影響範囲・注意点
影響を受けるアダプタ
| Adapter | rename_index の実装 | 影響 |
|---|---|---|
| PostgreSQL | ALTER INDEX … RENAME(ネイティブ) | もともと問題なし(変化なし) |
| MySQL ≥ 5.7.6 / MariaDB ≥ 10.5.2 | ALTER TABLE … RENAME INDEX(ネイティブ) | もともと問題なし(変化なし) |
| SQLite | フォールバック (super) | 今回の修正で挙動が改善 |
| MySQL < 5.7.6 / MariaDB < 10.5.2 | フォールバック (super) | 今回の修正で挙動が改善 |
特に現代的な開発環境で多いパターン:
- 開発・テスト: SQLite
- 本番: PostgreSQL / MySQL
この構成で、SQLite 上の rename_column / rename_index 実行後も、Partial Index やソート順などが本番とずれない 方向に直っています。
どんなケースで効いてくるか
SQLite で以下のようなマイグレーションをしている場合:
rubyadd_index :posts, :views, where: "title IS NOT NULL", unique: true, name: "idx_posts_on_views_partial" rename_index :posts, "idx_posts_on_views_partial", "idx_posts_on_views_uniq"修正前:
idx_posts_on_views_uniqはwhereが無視され、全行に対して unique 制約がかかる
修正後:where: "title IS NOT NULL"を保ったまま rename されるrename_columnが内部で index の張り替えを行う際にrename_indexが使われるアダプタ(SQLite など)の場合、
修正前は「カラム名変更に伴い暗黙に index 属性が失われる」ことがあったが、修正後は属性が維持される
注意点
- 仕様としては「正しい動作」に近づいている変更であり、後方互換性破壊というより、今まで silently バグっていた挙動の修正 と捉えるのが適切です。
- ただし、もし「partial index の WHERE が消える前提」でワークアラウンドしていたようなコードがある場合は、その前提が崩れます(通常はそのような前提はバグ寄りのため、修正は歓迎されるはずです)。
- SQLite は index コメントをサポートしませんが、MySQL/MariaDB のフォールバックパスに備えて
commentもコピーされます。実際にコメントが保存されるか・どう表現されるかはアダプタ依存です。
- 参考情報 (あれば)
- 対象 PR:
rails/rails#57565 「Preserve index attributes when renaming an index without native support」 - 関連 Issue: #16619(2014 年の報告。「抽象実装は where をサポートしないアダプタ向け」という理由で当時はクローズされていたが、現在は SQLite などが partial index をサポートするため前提が変わっている)
- 対応ファイル:
activerecord/CHANGELOG.mdactiverecord/lib/active_record/connection_adapters/abstract/schema_statements.rbactiverecord/test/cases/migration/index_test.rb
#57563 Fix reset_column_sequences! for a table in a quoted schema
マージ日: 2026/6/3 | 作成者: @55728
- 概要 (1-2文で)
PostgreSQL でスキーマ名をクォートしたテーブル(例:"App".widgets)に対してreset_column_sequences!がNoMethodErrorで落ちていた不具合を修正した PR です。これにより、quoted schema を使う環境でもreset_column_sequences!/reset_pk_sequence!/ fixture ロードが正常に動作するようになります。
- 変更内容の詳細
不具合の症状
例えば以下のように CamelCase なスキーマをクォートして作り、そのテーブルに対して reset_column_sequences! を呼び出すと例外が発生していました。
CREATE SCHEMA "App";
CREATE TABLE "App".widgets (id serial primary key);connection.reset_column_sequences!([['"App".widgets']])
# => NoMethodError: undefined method 'column=' for nilreset_pk_sequence! や fixture ロードも内部で reset_column_sequences! を呼ぶため同様にクラッシュします。
根本原因
SequenceReset(reset_column_sequences! の内部実装)がテーブルを管理するために、以下の2種類のテーブル名を扱っていました。
- 呼び出し側から渡されるテーブル名(map のキー)
- PostgreSQL カタログから取得する
regclass::textの文字列(schema-qualified かつ必要に応じてクォートされた名前)
regclass::text は例えば以下のような文字列になります。
"App".widgetspublic.widgets
元のコードでは、regclass::text からスキーマ名を外したりクォートを剥がしたりするために、
delete_prefix('"').delete_suffix('"')といった「外側のダブルクォートだけを削る」処理を使っていました。そのため:
"App".widgetsdelete_prefix('"')→App".widgetsdelete_suffix('"')→ 末尾は"ではないのでそのまま- 結果:
App".widgetsという壊れた文字列になる
この壊れた文字列で @tables のハッシュを引こうとするため、キーが見つからず nil が返り、nil.column = ... で NoMethodError が発生していました。
同じ問題が「主キー用のシーケンス」と「UUID 用のシーケンス」の両方のループで起きていました。既存のテストは小文字スキーマなど quoted schema のケースをカバーしていなかったため、今まで検知されていませんでした。
修正内容
テーブル名のパースと正規化に ActiveRecord::ConnectionAdapters::PostgreSQL::Utils.extract_schema_qualified_name を使うように変更し、以下を「常に同じロジック」で処理するように統一しました。
@tablesハッシュのキーにする名前regclass::textから逆引きする際に使う名前
具体的には、壊れた独自の「クォート剥がし」ロジック(delete_prefix / delete_suffix)を削除し、代わりに
Utils.extract_schema_qualified_name(...).to_sで得られた名前を使うようにしています。
extract_schema_qualified_name は、スキーマ付き・クォート付きのテーブル名を安全にパースして、内部的に「スキーマ」と「テーブル」の構造を理解した上で正規化した文字列を返すユーティリティです。これにより:
"App".widgetsのような CamelCase スキーマpublic.widgetsのような unquoted / lowercase の一般的なケース"weird-Name"."weird.Table"といった複雑なクォートケース
のいずれでも整合性のあるキー生成と逆引きが行われます。
補足として、他の unquote_identifier 利用箇所も監査されましたが、
- そこでは「カラム名など単一の identifier」を対象にしている
- もしくは既に
extract_schema_qualified_nameを使用している
ため、今回のようなバグは起きないことが確認されています。
テスト
新たに以下のテストが追加されています。
test_reset_column_sequences_with_quoted_schemaテスト内容:
"Test_CamelSchema"という CamelCase の quoted schema にテーブルを作成id = 100の行を手動挿入reset_column_sequences!を呼び出し- 次に挿入されるレコードの
idが101になることを確認
このテストは:
- 現在の
mainではNoMethodErrorが発生して red になる - 本 PR の修正後は
101にシーケンスが進んで green になる
ことが、実際の PostgreSQL 上で確認されています。
- 影響範囲・注意点
- 影響を受けるケース:
- PostgreSQL を使用していて
- CamelCase や大文字を含むスキーマ名を
"SchemaName"のようにクォートして定義し - そのスキーマ内のテーブルに対して
reset_column_sequences!/reset_pk_sequence!/ fixtures ロードを実行している場合
- この修正により、これらのケースで発生していた
NoMethodErrorが解消され、シーケンスが正しく「既存データの最大 ID + 1」にリセットされます。 - lowercase かつ unquoted なスキーマ・テーブル名のみを使っている一般的なアプリケーションでは、動作の変更はほぼありません(内部実装がより堅牢になっただけ)。
- スキーマ名・テーブル名・カラム名などにクォートが絡む環境で、シーケンスのリセット処理がより安全になります。独自タスクや Rake タスクで
reset_column_sequences!を利用している場合も恩恵があります。
- 参考情報 (あれば)
- 関連 PR:
- #57561:
foreign_keyscorruptingto_tablein a quoted schema
同じ「quoted schema 名の扱い」に起因する別箇所のバグ修正で、foreign_keysのto_tableがおかしくなる問題に対応しています。本 PR とは独立にマージ可能。
- #57561:
- 変更ファイル:
activerecord/CHANGELOG.mdactiverecord/lib/active_record/connection_adapters/postgresql/schema_statements.rbactiverecord/test/cases/adapters/postgresql/schema_test.rb
- テスト:
schema_test.rb全体で 82 tests, 0 failures が確認されています。
#57560 Fix nested attributes :limit miscounting a single-record hash
マージ日: 2026/6/3 | 作成者: @55728
- 概要 (1-2文で)
accepts_nested_attributes_for ..., limit: N使用時に、1件分のレコードを表すidキー付きハッシュが「N件を超えた」と誤判定されてTooManyRecordsが発生するバグが修正されました。レコード数ではなくハッシュのキー数を数えていた実装を、正しく「レコード数」を数えるように変更しています。
- 変更内容の詳細
バグの内容
accepts_nested_attributes_for に limit オプションを指定しているとき、以下のような「1件の関連レコードを更新するためのハッシュ」を渡すとエラーになっていました。
class Pirate < ApplicationRecord
has_many :parrots
accepts_nested_attributes_for :parrots, limit: 2
end
pirate.parrots_attributes = {
"id" => 1,
"name" => "Polly",
"color" => "green",
"breed" => 1
}
# 期待: 単一レコード更新として成功する
# 実際: TooManyRecords: Maximum 2 records are allowed. Got 4 records instead.原因は、**「レコード数のチェックが、正規化(ハッシュ → 配列化)より前に実行されていた」**ことです。
Rails は nested attributes で以下のような2パターンを受け取ります:
「複数レコード」形式(ハッシュ of ハッシュ or 配列)
ruby{ "0" => { "name" => "Polly" }, "1" => { "name" => "Cracker" } } # => Hash#size == 2 (レコード数と一致)「単一レコード」形式(
idを含むフラットなハッシュ)ruby{ "id" => 1, "name" => "Polly", "color" => "green", "breed" => 1 } # => Hash#size == 4 (これは「属性数」であってレコード数ではない)
従来の実装では check_record_limit! がこの正規化より前で Hash#size を見ていたため、
ケース2では「1レコード」ではなく「4レコード」と誤カウントされていました。
修正内容
activerecord/lib/active_record/nested_attributes.rb において:
check_record_limit!の実行位置を、入力パラメータの「ハッシュ → 配列」正規化後に移動idキー付き単一ハッシュは内部的に[attributes_hash]のような1要素配列に包まれてからチェックされる- この結果、単一レコードの場合は常に「1件」として数えられる
これにより、以下の動作になります:
pirate.parrots_attributes = {
"id" => 1,
"name" => "Polly",
"color" => "green",
"breed" => 1
}
# => 正常に1件の更新として処理される(limit: 2 に抵触しない)一方で、以下のような genuine な「レコード数オーバー」ケースは従来通りエラーになります:
pirate.parrots_attributes = [
{ "name" => "Polly" },
{ "name" => "Cracker" },
{ "name" => "Kiwi" }
]
# limit: 2 の場合 => TooManyRecords が発生(期待通り)テスト
activerecord/test/cases/nested_attributes_test.rb に以下のテストが追加されています:
test_limit_does_not_count_the_attributes_of_a_single_record_hash
このテストは共有モジュール NestedAttributesLimitTests に追加されており、
limit: 2(数値)limit: :some_symbollimit: -> { ... }(Proc)
といった3種の limit 設定すべてで同じケースが検証される構成です。
テスト内容:
limit: 2の関連に、4つのキーを持つid付きハッシュで1レコード更新を行うTooManyRecordsが発生しないこと- レコードが期待通り更新されていること
nested_attributes_test.rb 全体(170テスト)でグリーンであることが確認されています。
activerecord/CHANGELOG.md にもこの修正内容が追記されています。
- 影響範囲・注意点
- 対象:
accepts_nested_attributes_forにlimit:を指定しているすべてのコード- 特に、「既存レコードの更新」を
idキー付きフラットハッシュで行っているケースに影響
- 特に、「既存レコードの更新」を
- 期待される挙動:
- これまで 誤って
TooManyRecordsが出ていたケース(単一レコード更新)が正常に通るようになります - 実際にレコード数が
limitを超えている入力(配列 or hash-of-hashes)は、従来通りTooManyRecordsが発生します
- これまで 誤って
- 互換性:
- レコード数カウントのタイミングが変わっただけで、ビジネスロジック上の制約(何件まで許可するか)は変わっていません
- 「これまでバグを前提にしていた」ようなコード(例: 単一レコード更新をあえて弾かせていた…など)があれば挙動が変わりますが、通常は望ましい修正と考えてよいです
確認したほうがよいポイント:
- 管理画面や API で nested attributes を使って既存子レコードを更新している箇所があり、
limit:を設定しているid: ...を含む単一ハッシュでリクエストしている
これらが以前から「なぜか TooManyRecords になる」などの問題を抱えていた場合、この修正で解消されている可能性が高いです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57560
- 該当コード:
ActiveRecord::NestedAttributes(activerecord/lib/active_record/nested_attributes.rb) - 概念整理:
- nested attributes の入力形式
- 単一レコード更新:
{"id" => 1, "attr1" => "x", ...} - 複数レコード:
[{...}, {...}]または{"0" => {...}, "1" => {...}}
- 単一レコード更新:
limitは「レコード数の上限」であり、「属性数の上限」ではない
→ 今回の修正はこの意図に沿ったカウント方法に是正したものです。
- nested attributes の入力形式
#57550 Make Ractor shareability methods only available on 4.0 and above.
マージ日: 2026/6/3 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails の Ractor 用ヘルパーメソッド(ractor_make_shareable/ractor_shareable_procなど)を、「Ruby 4.0 以上では本物を提供し、Ruby 3.x では一括で no-op(何もしない)にする」という方針に整理した PR です。
これにより Ruby 3.x でRactor.make_shareableだけが中途半端に働いて例外が出る、といった不整合を避けます。
- 変更内容の詳細
※実際の diff は activesupport/lib/active_support/ractors.rb のみ(+14 / -20 行)で、主に条件分岐ロジックの修正です。
背景となる問題
Rails では Ractor 対応のため、以下のようなヘルパーを ActiveSupport に用意しようとしていました:
ractor_make_shareable(obj):Ractor.make_shareable(obj)相当のラッパーractor_shareable_proc(&block): Ractor で共有可能なProcを生成するヘルパー
ところが Ruby 3.x 系では:
Ractor.make_shareableは存在する- しかし
Ractor.shareable_procは存在しない(Ruby 4.0 で導入予定)
そのため:
- Rails 側で
make_shareableについては shim(互換関数)を用意 shareable_procは Ruby 3.x では意味のある実装ができないので no-op にする
という構成にすると、次のようなコードが Ruby 3.4 では例外を起こす、という問題があります。
# Ruby 3.x 環境でのイメージ
proc = ractor_shareable_proc { foo } # 実際には「共有可能」になっていない no-op
ractor_make_shareable(Bar.new(proc)) # => Bar 内に「共有不可能な proc」が含まれているので
# Ruby 3.4 の Ractor.make_shareable 呼び出しで例外一方で、Ruby 4.0 では Ractor.shareable_proc が存在するため、同じコードが正常に動く、というギャップが生じます。
この PR の方針
PR では、Rails での Ractor サポートは Ruby 4.0 を前提とし、Ruby 3.x では「Ractor 関連のヘルパーはすべて no-op に寄せる」方向へ整理しています。
実装イメージとしては:
# 疑似コード: バージョンは例
if RUBY_VERSION >= "4.0.0"
# Ruby 4.0 以上: Ractor をきちんと使う
def ractor_make_shareable(obj)
Ractor.make_shareable(obj)
end
def ractor_shareable_proc(&block)
Ractor.shareable_proc(&block)
end
else
# Ruby 3.x: Ractor サポートは「存在するが何もしない」扱い
def ractor_make_shareable(obj)
obj # そのまま返す (no-op)
end
def ractor_shareable_proc(&block)
block # そのまま返す、実際には shareable にはならない
end
endポイントは:
- Ruby 4.0 以上
- Rails は Ruby が提供する本物の
Ractor.make_shareable,Ractor.shareable_procを利用 - Ractor ベースで本番運用できることを目標とした設計
- Rails は Ruby が提供する本物の
- Ruby 3.x
- すべての Ractor ヘルパーを no-op にする
- 「
make_shareableだけ実体があってshareable_procは no-op」という中途半端な状態を避ける - その結果、3.x では「Ractor をちゃんと使える」ことは保証しない
これによって、Ruby 3.x で
ractor_shareable_procが共有不可能な Proc を返す- それを含んだオブジェクトに対して
ractor_make_shareable(実体は Ruby のRactor.make_shareable) を呼ぶ
という「内部的に矛盾した状態」がそもそも発生しないようにしています。
- 影響範囲・注意点
影響範囲
- 対象:
ActiveSupportの Ractor 関連ヘルパーを利用しているコードActiveSupport::Ractors(またはそれを内部で利用するコンポーネント)
- 実行環境: Ruby 3.x / Ruby 4.0 以上で挙動が分岐
Ruby 4.0 以上での挙動
- Ractor 関連ヘルパーは Ruby 標準の機能に紐づいて、実際に Ractor セーフなオブジェクトや
Procを生成します。 - Ractor を本格的に利用した並行処理(マルチ Ractor)を Rails 上で実用的に構成できる前提になります。
Ruby 3.x での挙動と注意点
- Ractor ヘルパーは 存在するがすべて no-op(何もしない) になる想定です。
- 例外は極力発生しないようにしてあるが、その代わり「Ractor 対応」にはなっていません。
- 3.x 上で Ractor を前提とした並列処理を Rails 内部やアプリケーションコードで行うのは推奨されず、将来的に Ruby 4.0 をターゲットにすべき、というメッセージと言えます。
- Rails 側はこの挙動を前提に実装していくため、
- Ruby 3.x で「Ractor をちゃんと使えること」を期待したテストや本番運用は壊れる可能性があります。
- Ractor 前提のコードを書く場合は、Ruby 4.0 をターゲットに準備する必要があります。
移行上のポイント
- すでに独自に
Ractor.make_shareableを直接呼び出しているコードには、この PR は直接影響しません(Rails のヘルパー周りの整理なので)。 - ただし、今後 Rails 側の設計として「Ractor 対応 = Ruby 4.0 前提」が強まるため:
- 新規に Ractor を利用する並列処理を書きたい場合
- Ruby 4.0 を最低ターゲットとして検討する
- Rails の Ractor ヘルパーを使う場合は「3.x では no-op」と割り切る
- ライブラリ作者は、Ractor 対応をうたう際は「Ruby 4.0 以上でテスト済み」であることを明確にしておくとよいです。
- 新規に Ractor を利用する並列処理を書きたい場合
- 参考情報 (あれば)
- この PR がフォローアップしている PR:
- Ruby 本体側の Ractor 関連メソッド:
Ractor.make_shareable(obj)- オブジェクトを Ractor 間で共有可能にする。共有不可能な要素を含むと例外が発生。
Ractor.shareable_proc(&block)(Ruby 4.0 以降で導入予定)- Ractor 間で共有可能な
Procを生成する、Proc用の特別なコンストラクタ的メソッド。
- Ractor 間で共有可能な
#57567 Read mysql2 affected_rows during perform_query
マージ日: 2026/6/3 | 作成者: @matthewd
- 概要 (1-2文で)
MySQL 用の ActiveRecord アダプタで、クエリ実行時にaffected_rows(影響を受けた行数)を確実に取得できるようにする変更です。mysql2がnilを返すケースでも、ActiveRecord::Resultを使って影響行数を運べるようにし、テストもそれを検証しやすい形に調整しています。
- 変更内容の詳細
背景
mysql2のクエリ実行結果は、- 行とカラムがある「通常の SELECT 結果」 →
Mysql2::Result UPDATE/DELETEなどで行数のみが意味を持つ結果 →nil(結果セットなし)
という形になることがあります。
- 行とカラムがある「通常の SELECT 結果」 →
- ActiveRecord 側では、
affected_rows(更新・削除された行数など)をテストしているが、perform_queryのタイミングで正しく読み取れておらず、ローカルの AR テストで半分くらいの確率で落ちる状況があったとのことです。
生の結果型を変更
これまで:
- Mysql2Adapter の「raw な結果」の型は
Mysql2::Resultかnilのどちらかだった。 mysql2が結果セットを返さない(nilを返す)ケースでは、ActiveRecord 内で影響行数を運ぶためのオブジェクトがなく、「影響行数だけを持っている結果」を表現しづらかった。
これから:
- 「raw 結果」の型を
Mysql2::Result(行とカラムを持つ結果)ActiveRecord::Result(影響行数などを表現できる AR の結果オブジェクト)
のどちらかに変更。
- 行とカラムを持つ「フルな」結果については、今まで通り
Mysql2::Resultを返し、その後の段階でActiveRecord::Resultに変換するフローは変更しない。 - 一方、
mysql2がnilを返すケース(典型的にはaffected_rowsだけ意味がある結果)では、その場でActiveRecord::Resultを生成し、そこにaffected_rowsを保持させるようにする。
PR 説明のポイントを言い換えると:
- 生の結果型を
Mysql2::Result | nil→Mysql2::Result | ActiveRecord::Resultに変更nilで済ませていた「影響行数だけある結果」を、ActiveRecord::Resultに載せて扱う- すでに Sqlite3Adapter は同様に
ActiveRecord::Resultを “raw” として再利用しており、その方針に合わせている
perform_query 内で affected_rows を読むタイミングを調整
PR タイトルにもある通り、「perform_query 中に mysql2 の affected_rows を読む」ようにしています。
これにより、MySQL クライアントの状態が変わる前に確実に行数が取得され、後段の処理で値が取り違えられたり、0 に見えてしまうといった不具合を防ぎます。
(PR 本文からは細かいメソッド呼び出しの変更までは読み取れませんが、database_statements.rb の差分 (+21/-17) で、perform_query or その周辺で affected_rows を読んで ActiveRecord::Result に詰める処理を挿入・整理していると考えられます。)
テストの変更
対象テスト:activerecord/test/cases/adapters/abstract_mysql_adapter/count_deleted_rows_with_lock_test.rb
- 「
LOCKを絡めた削除クエリで削除行数をきちんとカウントできるか」をテストしているファイル。 - ローカル環境で「半分くらいの頻度で落ちる」不安定テストだったため、
- 失敗を再現しやすいようにテスト内容を少し強める/明確にする
- あるいは、今回の
affected_rowsの扱い変更に合わせて期待値の取り方を揃える
といった形で +6/-2 行の修正が入っています。
- 説明文には「将来のリグレッション時に失敗が起きやすくなることを期待してテストを書き換えた」とあるので、
- 以前はバグがあってもテストが通ってしまうケースがあった
→ 仕様どおりでなければ落ちるようにテストを強化した
と理解しておくと良いです。
- 以前はバグがあってもテストが通ってしまうケースがあった
- 影響範囲・注意点
- 対象:
mysql2アダプタ (Mysql2Adapter) を使っている MySQL / MariaDB 環境の Rails アプリケーション。 - 影響する処理:
UPDATE/DELETE/INSERTなど、結果セットを返さずaffected_rowsだけ意味を持つクエリ。- 特に
LOCKを絡めた削除(テスト名からするとDELETE ... LOCK IN SHARE MODE的なもの)での削除行数カウント。
- 互換性:
- 「通常の SELECT 結果」は引き続き
Mysql2::Resultベースで扱われるため、既存コードがMysql2::Resultを前提にしていても挙動に変化はないはずです。 - 結果セットが無いケースで、内部的に
nilではなくActiveRecord::Resultが流れるようになるため、アダプタ内部やプライベート API に依存しているコード(独自 monkey patch やメトリクス用途のラッパなど)があれば、nil前提ロジックが壊れる可能性はあります。
- 「通常の SELECT 結果」は引き続き
- 利点:
affected_rowsを確実に取得できるようになることで、delete_all/update_allの戻り値lock付きクエリ実行時の削除件数のカウント
などの信頼性が向上します。
Sqlite3Adapter同様に、ActiveRecord::Resultを「raw」として再利用する設計に揃えたため、アダプタ間の一貫性が増しています。
- テスト面:
- 影響行数周りのリグレッション(戻りバグ)が将来発生した際には、今回変更されたテストがより高い確率で検知してくれるようになります。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57567
- 関連箇所:
activerecord/lib/active_record/connection_adapters/mysql2/database_statements.rbactiverecord/lib/active_record/connection_adapters/mysql2_adapter.rbactiverecord/test/cases/adapters/abstract_mysql_adapter/count_deleted_rows_with_lock_test.rb
- 設計上の類似点:
Sqlite3AdapterもすでにActiveRecord::Resultを「raw 結果」として再利用しており、MySQL アダプタもそれに寄せた形になっています。
#57561 Fix PostgreSQL foreign_keys for a target table in a quoted schema
マージ日: 2026/6/3 | 作成者: @55728
- 概要 (1-2文で)
PostgreSQL でスキーマ名がクォートされたテーブルに対する外部キーを扱う際、foreign_keysが参照先テーブル名(to_table)を壊してしまい、rails db:schema:dumpから復元できないスキーマが生成されていた問題を修正する PR です。regclass::textの結果に対して不適切なアンクォート処理をしていたのを、スキーマ付き・クォート付きのテーブル名を正しく扱えるパーサに差し替えています。
- 変更内容の詳細
問題の具体例
PostgreSQL で、クォートされたスキーマにテーブルがある場合:
CREATE SCHEMA "App";
CREATE TABLE "App".customers (...);
CREATE TABLE orders (
customer_id bigint REFERENCES "App".customers(id)
);Rails から:
connection.foreign_keys("orders").first.to_table
# 修正前: "App\".customer" (壊れている)
# 期待値: "App.customers"この to_table がスキーマダンプにそのまま使われるため、schema.rb には壊れた add_foreign_key が出力され、rails db:schema:load で失敗する、というバグでした。
根本原因
参照先テーブル名は PostgreSQL の
regclass::textから取得している。regclass::textは「スキーマ付き + 必要部分のみクォートされた識別子」を返す:regclass::text の例 "App".customerspublic."Mixed""Schema"."Table""Mixed"customersこれに対して
Utils.unquote_identifierをそのまま適用していたが、このメソッドは「単一の識別子だけを想定して、先頭と末尾の1文字を剥がす」実装だったため、スキーマ+テーブルのような複合名を壊してしまっていた:入力 ( regclass::text)unquote_identifierの結果正しい期待値 "App".customersApp".customerApp.customerspublic."Mixed"public."Mixed"(変化なし)public.Mixed"Schema"."Table"Schema"."TableSchema.Tableスキーマなし(
"Mixed",customers)のケースだけはたまたま動いていたため、長年見落とされていた。
修正内容
foreign_keys内で参照先テーブル名を扱う箇所を変更:- Before:
Utils.unquote_identifier(raw_name_from_regclass) - After:
Utils.extract_schema_qualified_name(raw_name_from_regclass).to_s
- Before:
extract_schema_qualified_nameは、すでに他の場所で使われているヘルパで、- スキーマ名 + テーブル名
- 必要な部分のみクォートされた識別子
を正しくパースし、schema.tableの形の文字列を返せるようになっている。
結果として:
# 修正後
connection.foreign_keys("orders").first.to_table
# => "App.customers"となり、schema.rb にも正しい add_foreign_key "orders", "App.customers", ... が出力されます
(実際にはアダプタ内部表現に準じた形式ですが、少なくとも壊れた文字列にはならない)。
テストの追加
activerecord/test/cases/migration/foreign_key_test.rb に PostgreSQL 専用のテストを追加:
test_foreign_key_to_table_in_quoted_schema- 混在ケースのスキーマ
"Aerospace"を作成 "Aerospace".boostersテーブルを作り、別テーブルから外部キーを張るconnection.foreign_keys(...).first.to_tableが"Aerospace.boosters"であることを検証
- 混在ケースのスキーマ
その他、既存テスト (foreign_key_test.rb, references_foreign_key_test.rb, schema_test.rb) が PG 17 でグリーンであることも確認済みです。
- 影響範囲・注意点
対象DB: PostgreSQL のみ。他DBアダプタには影響なし。
主な影響箇所:
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#foreign_keys- これを間接的に利用する
rails db:schema:dump/db:structure:dumpなどのスキーマダンプ機能
影響するアプリの特徴:
- PostgreSQL を使用している
- クォートされたスキーマ名 (例:
"App","Aerospace") を利用している - そのスキーマ内のテーブルを参照する外部キーがある
互換性上の懸念点:
- アンバグなケース(スキーマなしテーブル名、普通のスネークケーススキーマ名)は動作に変化なし。
- 参照先テーブル名の解釈ロジックがより厳密かつ正確になっただけなので、正しく設定されている環境では挙動悪化は考えにくい。
- 逆に、これまで「壊れた
schema.rbを前提にしたワークアラウンド」を入れていた場合は、そのワークアラウンドが不要/有害になる可能性があるため、該当プロジェクトでは差分を注意して確認した方がよいです。
既存の壊れた
schema.rbについて:- この PR は「新たにダンプされるスキーマを直す」ものであり、すでに壊れた
schema.rb自体を自動修正するものではありません。 - もし現状の
schema.rbに"App\".customer"のような文字列が含まれている場合は、この修正を含む Rails バージョンに上げた上で、DB から改めてdb:schema:dumpし直すことを推奨します。
- この PR は「新たにダンプされるスキーマを直す」ものであり、すでに壊れた
- 参考情報 (あれば)
- この PR:
- Fix PostgreSQL
foreign_keysfor a target table in a quoted schema (#57561)
- Fix PostgreSQL
- 関連 PR:
reset_column_sequences!がクォートされたスキーマでクラッシュする問題の sibling PR: #57563
(同じ根本原因だが呼び出し元が異なる問題を別途修正)
- 過去の関連議論(クロススキーマFK全般の話であり、このバグそのものではない):
- #16907
- #28654
#57547 Test Mime::Type#=== and nil matching
マージ日: 2026/6/3 | 作成者: @hammadxcm
- 概要 (1-2文で)
このPRは、Mime::Typeに既に存在している挙動(#===の配列マッチと、#=~/#match?のnilガード)に対するテストを追加するだけの変更です。プロダクションコードの変更は一切なく、テストカバレッジを補完する目的のPRです。
- 変更内容の詳細
変更ファイルは 1 つのみです。
actionpack/test/dispatch/mime_type_test.rb(+10/-0)
追加されたテストで確認しているのは主に次の2点です。
(1) Mime::Type#=== が「配列」に対してもマッチすること
Mime::Type#=== は、通常の case ... when での利用を想定しており、
case request.format
when Mime[:html]
...
when Mime[:json]
...
endのように使われますが、「複数の MIME Type をまとめた配列」 に対してもマッチする仕様になっています。
PR説明中の "matches when the type is included in a given array" というのは、例えば次のようなパターンです。
type = Mime[:html]
list = [Mime[:html], Mime[:json]]
type === list # => true (配列に含まれているので true)または case/when での利用イメージだと:
formats = [Mime[:html], Mime[:json]]
case formats
when Mime[:html]
# "html" が formats 配列に含まれているためマッチする
endこの 「配列を渡したときに、その配列内に self (Mime::Type インスタンス) が含まれていれば true を返す」 という挙動に対して、これまでテストが存在していなかったため、専用のテストが追加されています。
(2) Mime::Type#=~ / Mime::Type#match? が nil を安全に扱うこと
Mime::Type#=~ と Mime::Type#match? は、引数が nil のときに例外を出さず、常に false を返すようにガードされています。
例:
html = Mime[:html]
html =~ nil # => false
html.match?(nil) # => false従来からこのガードは存在していましたが、挙動を担保するテストがなかったため、今回のPRで nil を渡した場合に false が返ることを確認するテストが追加されました。
- 影響範囲・注意点
- プロダクションコードの変更は一切なく、テストコードのみの追加です。
- 既存の
Mime::Type#===/#=~/#match?の挙動には変更がありません。 - したがって、アプリケーション側の挙動の変化や後方互換性の問題はありません。
- ただし、今回テストで明示的にカバーされたことで、
Mime::Type#===が配列を受けたときに「自身がその配列に含まれているかどうか」で判定する、#=~/#match?にnilを渡した場合は常にfalseを返す、 という挙動が仕様としてより強く固定化されたと解釈できます。将来この挙動を変える場合はテストの修正が必要になります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57547
Mime::Typeは主にActionDispatch/ActionPackの一部として、request.formatなどのコンテンツネゴシエーションやレスポンスのフォーマット判定に利用されます。case/whenでMime::Typeを使う際に、複数フォーマットを一度に扱いたい場合や、nilが来る可能性のある入力をマッチングに使う場合の挙動確認として、このPRで追加されたテストが仕様の参考になります。
#57556 Test ParameterFilter#filter with empty filters returns a dup
マージ日: 2026/6/3 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveSupport::ParameterFilter#filterに対して、「フィルタが空のときはparams.dupを返す」という振る舞いを確認するテストが追加された PR です。
本番コードの変更はなく、テストコードのみの追加です。
- 変更内容の詳細
何をテストしているか
ActiveSupport::ParameterFilter は、指定したキーをマスクしたりするためのユーティリティですが、フィルタ条件が空配列 / 空リストのときは、実装上「高速経路」として params.dup を返すようになっています。
元々は、クラスメソッド風の filter_param に対しては「空フィルタ時の挙動」がテストされていましたが、インスタンスメソッドの #filter については同じパターンのテストが存在しなかったため、そのカバレッジを追加した PR です。
テストでは以下の2点を確認しています:
filterの返り値の内容が元のparamsと「等しい」rubyassert_equal params, filtered- ただし、同一オブジェクトではなく複製 (
dup) であることrubyrefute_same params, filtered
イメージとしては、下記のようなテストケースが追加されています(概念的なコード例):
def test_filter_with_empty_filters_returns_dup
params = { "foo" => "bar" }
filter = ActiveSupport::ParameterFilter.new([])
filtered = filter.filter(params)
# 内容は同じ
assert_equal params, filtered
# だがオブジェクトは別(dupであることを期待)
refute_same params, filtered
endこのようにして、「空のフィルタでもオブジェクトをそのまま返さず必ず dup している」という既存仕様をテストで明示的に固定しています。
- 影響範囲・注意点
- 本番コードは一切変更されていないため、ランタイムの挙動・パフォーマンスに直接の影響はありません。
- ただしテストが追加されたことで、以下のような将来の変更が仕様としてロックインされたと解釈できます:
- フィルタが空でも
#filterは「同じハッシュオブジェクトをそのまま返さず、必ず複製して返す」こと。
- フィルタが空でも
- そのため、将来
ParameterFilterの実装を最適化して「空フィルタ時はそのまま同一オブジェクトを返す」というような変更をした場合、このテストが落ちるようになります。- つまり、「呼び出し側が
filterの結果を破壊的に変更しても元のparamsに影響しない」前提が仕様として保証される方向に強化されています。
- つまり、「呼び出し側が
ParameterFilter#filterの呼び出し側で、「返ってきたオブジェクトがparamsと同一であること」に依存したコードを書いていた場合(通常はないことが望ましいですが)、テスト上は今後もそれは成立しない前提となります。
- 参考情報 (あれば)
- 対象クラス:
ActiveSupport::ParameterFilter - この PR: https://github.com/rails/rails/pull/57556
- 関連仕様:
- Rails ではログに機密情報を残さないため、
config.filter_parametersと組み合わせてParameterFilterが広く使われており、その一貫性と安全性を保つためにも「入力ハッシュを直接書き換えない」ことが暗黙の重要な性質になっています。
- Rails ではログに機密情報を残さないため、
#57554 Test Object#with returns the block's result
マージ日: 2026/6/3 | 作成者: @hammadxcm
- 概要 (1-2文で)
このPRは、Object#withが「ブロックにselfを渡すだけでなく、そのブロックの戻り値自体を返す」ことをテストで明示的に保証するものです。プロダクションコードの変更はなく、テスト追加のみです。
- 変更内容の詳細
対象ファイル:
activesupport/test/core_ext/object/with_test.rb(+5/-0)
Object#with の振る舞いは以下のようになっています(概念的な例):
obj.with do |o|
# o は obj (self)
:result
end
# => :result が返ることを保証したい既存テストでは、「ブロックに self が渡されること」は確認していたものの、
ブロックの戻り値が Object#with の戻り値としてそのまま返ることが明示的に検証されていませんでした。
今回のPRで追加されたテストは、おおよそ次のようなことを確認しています(擬似コード):
result = some_object.with do |obj|
1234 # obj を使うかどうかに関係なく、この値を返す
end
assert_equal 1234, resultポイント:
- ブロック内で返している値が任意の値(オブジェクト)であること
- その値が
Object#withの戻り値として「そのまま」外に出てくること
→ 「withはブロックの評価結果を返す」という仕様をテストで固定
これにより、単に「self がブロックに渡されているだけ」ではなく、「ブロックの戻り値が with の戻り値である」というインターフェースが明確に保証されます。
- 影響範囲・注意点
影響範囲:
- ActiveSupport の
Object#withの挙動に関する“テストカバレッジ”のみが拡張されます。 - 実際の実装には一切変更がないため、既存アプリケーションの挙動は変わりません。
- ActiveSupport の
注意点:
- 今後
Object#withの実装を変更する場合、このテストにより「ブロックの戻り値をそのまま返すこと」が破壊されないようにする必要があります。 withを利用する側は、「ブロックの最後の評価結果がメソッドの戻り値になる」ことを前提としてコードを書いて良いことが、テストにより明確に裏付けられました。
- 今後
- 参考情報 (あれば)
- 対象メソッド:
Object#with(ActiveSupport コア拡張)- パターンとしては Kotlin の
apply/alsoや Ruby のtapに近いユーティリティで、「レシーバをブロックに渡しつつ、ブロックの評価結果を返す」系のメソッドです。
- パターンとしては Kotlin の
- PR番号: https://github.com/rails/rails/pull/57554
#57553 Fix Enumerable#in_order_of dropping nil elements when filter: false
マージ日: 2026/6/3 | 作成者: @hammadxcm
- 概要 (1-2文で)
Enumerable#in_order_ofにfilter: falseを指定した場合、本来は元の配列に含まれるnilも保持されるべきところが、誤って削除されていたバグが修正されました。sort_byベースの処理で不要なcompactを取り除き、仕様どおりnilを含めて返すようになっています。
- 変更内容の詳細
問題の挙動
Enumerable#in_order_of は、あるキー (key ブロックやシンボル) を元に要素を並べ替えるメソッドで、filter: オプションの挙動は以下のようにドキュメントされています。
filter: true(デフォルト)seriesに含まれるキーに該当する要素だけを返す(それ以外は除外)filter: falseseriesに含まれない要素も「落とさず」返す
今回のバグは、filter: false のときに、元の Enumerable に含まれていた本物の nil 要素まで compact によって削除されてしまっていた点です。
PR の説明より、以前と以後の挙動は以下のとおりです。
[3, nil, 1, 2].in_order_of(:itself, [1, 2, 3], filter: false)
# 変更前: [1, 2, 3] # nil が消えてしまう
# 変更後: [1, 2, 3, nil] # nil が正しく保持されるcompact が誤っていた理由
in_order_of の実装には大きく2パターンあります。
filter: trueのとき- 実装イメージ:ruby
grouped = collection.group_by(&key) ordered = grouped.values_at(*series) # series にないキーは nil になる ordered.flatten.compact values_at(*series)は、seriesの各エントリについて、対応する要素が存在しないとnilを返すため、compactでこれら「ダミーの nil」を消す必要があります。
- 実装イメージ:
filter: falseのとき- 実装イメージ(概念的):ruby
collection.sort_by do |element| # series 上の位置を優先し、見つからなければ後ろに回す end # => sort_by 自体は nil を生成しない - ここでは
sort_byで並び替えているだけなので、処理の途中で新たなnilが生成されることはありません。存在するのは「元から配列にいた本物の nil」だけです。 - したがって、このブランチで
compactを呼ぶと、本来保持すべきnilまで削除してしまい、ドキュメントの「filter: false のときは追加要素も落とさない」という契約を破っていました。
- 実装イメージ(概念的):
実際の変更内容
filter: false側の処理からcompactを 1 箇所削除- この挙動を保証するリグレッションテストを
enumerable_test.rbに追加- 想定されるテストイメージ:ruby
def test_in_order_of_with_filter_false_keeps_nil assert_equal [1, 2, 3, nil], [3, nil, 1, 2].in_order_of(:itself, [1, 2, 3], filter: false) end
- 想定されるテストイメージ:
- CHANGELOG にエントリを追加
- 影響範囲・注意点
- 影響範囲:
Enumerable#in_order_ofをfilter: false付きで利用しているコード全般。- 特に「
nilが除外される」ことを前提にワークアラウンドしていた場合、その挙動が変わります。
- 期待される正しい挙動:
filter: falseのときは、seriesに含まれない要素(nilを含む)も結果に残るようになります。
- 注意点:
- これまで「
in_order_of(..., filter: false)を使うとnilが落ちる」と誤解して利用していた場合、この PR マージ後にnilが残るようになるため、必要なら明示的にcompactするなどの対応が必要です。 filter: true側の挙動は変わっていないため、seriesに存在しないキーに対する「ダミーの nil」は引き続き削除されます。
- これまで「
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57553
- 該当コード:
activesupport/lib/active_support/core_ext/enumerable.rb - テスト:
activesupport/test/core_ext/enumerable_test.rb - 関連ドキュメント:
Enumerable#in_order_ofのfilterオプションの説明(Rails API ドキュメント / guides)
#57548 Test Range#sole with an endless range
マージ日: 2026/6/3 | 作成者: @hammadxcm
概要 (1-2文で)
このPRは、Range#soleが「終端なしの範囲(endless range)」に対してSoleItemExpectedErrorを投げることを確認するテストを追加するものです。既にあった「始端なしの範囲(beginless range)」向けテストに対応する形で、テストカバレッジを補完しており、本番コードの変更はありません。変更内容の詳細
対象機能:
ActiveSupportに追加されているRange#soleRange#soleは「範囲内に要素がちょうど1つだけ存在する場合にその要素を返し、それ以外の場合は例外を投げる」ユーティリティメソッドです(ActiveSupport::SoleItemExpectedErrorなど)。
既存の挙動:
Range#soleには、以下のようなガードがあります(PR説明より):rubyつまり、if self.begin.nil? || self.end.nil? raise ActiveSupport::SoleItemExpectedError end- beginless range:
(..1)→self.begin.nil? #=> true - endless range:
(1..)→self.end.nil? #=> trueのどちらも「無限範囲」とみなしてSoleItemExpectedErrorを投げる設計です。
- beginless range:
これまでのテスト状況:
- beginless range のケースはすでにテストされていました:ruby
assert_raises(ActiveSupport::SoleItemExpectedError) { (..1).sole } - 一方で、endless range のケース:rubyが例外を投げることは、テストでカバーされていませんでした。
(1..).sole
- beginless range のケースはすでにテストされていました:
このPRでの変更:
activesupport/test/core_ext/range_ext_test.rbに4行追加し、(1..).soleがActiveSupport::SoleItemExpectedErrorを投げることを確認するテストを追加。
- これにより、「
self.begin.nil? || self.end.nil?の両側」がテストで担保されるようになっています。
追加されるテストのイメージ(擬似コード):
rubydef test_sole_with_endless_range_raises assert_raises(ActiveSupport::SoleItemExpectedError) do (1..).sole end end
- 影響範囲・注意点
影響範囲:
- 変更はテストコードのみであり、ランタイム挙動や公開APIに変更はありません。
- すでに実装されている「無限 range に対する
Range#soleの例外スロー仕様」をテストで明示的に保証することによって、将来的なリグレッションを防止する効果があります。
注意点:
- 開発者視点では、
Range#soleは以下のように振る舞うことが前提として固まっていると考えてよいです:- 有限範囲で要素が1つのみ → その要素を返す
- 有限範囲で要素0個または2個以上 →
SoleItemExpectedErrorなどの例外 - beginless range (
(..x)) および endless range ((x..)): 「無限 range」とみなして常にSoleItemExpectedError
- 無限 range に対して
soleを使ってはいけない、という仕様がテストでより強固に固定されるため、将来この仕様を変えたい場合は、テスト変更を含めて明示的な設計変更が必要になります。
- 開発者視点では、
- 参考情報 (あれば)
- 対象メソッド:
ActiveSupportのRange#sole
ドキュメント:- Rails ガイドでは
soleは主にActiveRecord::Relation#soleとして紹介されますが、Enumerable#sole/Range#soleも同様の振る舞いをします。
- Rails ガイドでは
- 関連仕様:
- Ruby 2.6以降で導入された beginless range
(..x)と endless range(x..)に対し、Rails は「無限集合」とみなしてsoleを禁止するポリシーを取っていることが、このPRのテストからも読み取れます。
- Ruby 2.6以降で導入された beginless range
#57532 Fix grouped calculations by a belongs_to association with a composite primary key model
マージ日: 2026/6/2 | 作成者: @55728
- 概要 (1-2文で)
belongs_to先が複合主キーを持つモデルの場合にgroup(:association).countなどの集計がArgumentErrorで落ちていた問題を修正した PR です。従来の単一カラム主キーと同様に、「関連オブジェクトをキーにしたグループ集計」が複合主キーでも動作するようになります。
- 変更内容の詳細
何が問題だったか
次のようなモデル構成を考えます。
class Order < ApplicationRecord
self.primary_key = [:shop_id, :id] # 複合主キー
end
class Book < ApplicationRecord
belongs_to :order, foreign_key: [:shop_id, :order_id]
endこのとき、
Book.group(:order).countを実行すると、本来は
{ #<Order ...> => 3, #<Order ...> => 5, ... }のように「Order オブジェクトをキーとしたハッシュ」が返ってきてほしいところですが、実際には以下のエラーになっていました。
ArgumentError: Expected corresponding value for ["shop_id", "id"] to be an Array単一主キー版(例:Comment.group(:post).count)は以前から動いていて、複合主キーのときだけ壊れている状態でした。
原因の詳細
ActiveRecord::Relation#calculate 系メソッドで「関連をキーにしたグループ集計」を行う場合、内部では execute_grouped_calculation が呼ばれ、次のような処理が行われます(擬似コード):
# 集計結果の生データから、グルーピングに使ったキーの値を抜き出す
key_ids = calculated_data.collect { |row| row[group_aliases.first] }
# 取り出したキーをもとに、関連レコードをまとめて取得
key_records = association.klass.base_class.where(
association.klass.base_class.primary_key => key_ids
)
records_by_id = key_records.index_by(&:id)単一主キーであれば、
primary_keyは"id"(文字列)group_aliases.firstは、idに対応する集計結果のカラム名key_idsは[1, 2, 3, ...]のような一次元配列where(id: [1,2,3,...])となり問題なく動作
という流れで正しく動きます。
しかし複合主キーのときは、
primary_keyは["shop_id", "id"](配列)- 実際に GROUP BY されているカラムは、
group_fields = Array(association.foreign_key)で、複数列(例:["shop_id", "order_id"]) - ところが
key_idsの取得ではgroup_aliases.firstしか使っておらず、1 列分だけ 抜き出してしまう
その結果、内部的にだいたい次のような形の where が組み立てられます。
where(["shop_id", "id"] => [1, 2, 3, ...])このとき、Active Record の PredicateBuilder は「複合主キーなら [[shop_id, id], [shop_id, id], ...] のようなタプル配列が来るはず」と期待しているのに、実際には単なるスカラ値の配列([1,2,3,...])が渡されるため、
ArgumentError: Expected corresponding value for ["shop_id", "id"] to be an Arrayを投げてしまっていました。
修正内容
この PR では「関連先モデルが複合主キーを持っている場合」に限り、次のように動作を変えています。
- これまで:
group_aliases.firstだけを使ってキーを 1 列分だけ拾っていた - 修正後: 複合主キーの全カラムに対応する GROUP BY 列を 1 行ごとにタプルとして集める
つまり、ざっくり言うと「key_ids に [["shop1", 1], ["shop1", 2], ...] のような“複合キーの配列”を入れる」ようにして、where(primary_key => key_ids) が
where(["shop_id", "id"] => [["shop1", 1], ["shop1", 2], ...])という形になり、PredicateBuilder の期待する形に合うようにしています。
その後の
key_records.index_by(&:id)はもともと複合主キーに対しても「id メソッドが複合キーのタプル([shop_id, id])を返す」設計になっており、今回の修正と整合的です。
単一カラム主キーについては既存の分岐ロジックがそのまま維持されており、挙動は変わりません。
テスト
activerecord/test/cases/calculations_test.rb に以下を満たす回帰テストが追加されています。
belongs_to先が複合主キーを持つ- それを
group(:association).countでグループ化 - 結果のキーが「関連オブジェクト(複合主キーのモデル)」になっていることを検証
このテストは:
- 現行 main ブランチでは
ArgumentErrorで落ちる(red) - 修正適用後は sqlite3 / postgresql の両方でグリーン
であることが確認されています。
- 影響範囲・注意点
影響する機能
ActiveRecord::Relationのグループ集計(count,sum,average,minimum,maximumなど)で、グループキーにbelongs_to関連を指定したケース- かつ、その
belongs_toの参照先モデルが 複合主キー を持つ場合 - 例:
Book.group(:order).countやBook.group(:order).sum(:price)など
単一主キーのモデルに対しては挙動は変わらず、後方互換性を壊す変更ではありません。
複合主キー関連を使っているアプリでは:
- これまで
group(:association)を避けて自前で JOIN + GROUP BY していたようなケースを、Rails 標準の API に置き換えられる可能性があります。 - 逆に、アプリ側でこのバグを前提に「例外発生」を利用したワークアラウンドを書いていた場合は、その挙動が変わる点に注意が必要です(一般には少ないはずです)。
- これまで
複合主キーサポート全体としては、まだ Rails の公式サポートが「どこまでを保証するか」という課題は残っているものの、少なくともこの経路(
group(:belongs_to)→ 関連オブジェクトキーでの計算結果)は動作するようになります。
- 参考情報 (あれば)
変更ファイル
activerecord/lib/active_record/relation/calculations.rbexecute_grouped_calculation付近で、複合主キー対応のキー収集ロジックを追加
activerecord/test/cases/calculations_test.rb- 複合主キー
belongs_toを使ったgroup(:association).countの回帰テストを追加
- 複合主キー
activerecord/CHANGELOG.md- バグ修正としてエントリ追加
関連する概念
- Active Record の「複合主キー」は依然として標準機能としては限定的な扱いですが、この PR のように局所的なサポート改善が徐々に進んでいます。
index_by(&:id)は複合主キーを持つモデルに対しても「idが複合キーの配列を返す」ことを前提にしており、今回の形状修正とよく噛み合っています。
#57539 Reverse default_order in reverse_order instead of discarding it
マージ日: 2026/6/2 | 作成者: @55728
- 概要 (1-2文で)
Rails のActiveRecord::Relation#reverse_orderが、default_orderだけで並び替えられている場合にその並び順を正しく反転せず、主キー降順に差し替えてしまっていたバグを修正する PR です。明示的なorderと同様に、default_orderもreverse_orderで正しく反転されるようになります。
- 変更内容の詳細
これまでの挙動
default_order のみが設定されている relation に対して reverse_order を呼ぶと、default_order が無視されて PK(主キー) 降順に置き換えられていました。
Model.default_order("name ASC").to_sql
# => "... ORDER BY name ASC"
Model.default_order("name ASC").reverse_order.to_sql
# => "... ORDER BY \"models\".\"id\" DESC" # 本来は name DESC になってほしい
Model.order("name ASC").reverse_order.to_sql
# => "... ORDER BY name DESC" # 明示的 order の場合は正しく動く原因は、reverse_order! が order_values だけ を見て処理していたことです。
def reverse_order! # :nodoc:
orders = order_values.compact_blank
self.order_values = reverse_sql_order(orders)
self
enddefault_orderだけが設定されている relation ではorder_valuesは空配列[]- 空配列を
reverse_sql_order([])に渡すと、「明示的な順序なし」とみなされ、内部的な_reverse_order_columns(PK DESC) が返る - それが
order_valuesに設定されるため、その後のbuild_orderはdefault_order_valuesを一切見なくなる
→ 結果として、「default_orderを消して PK 降順にする」誤った挙動になっていた
そもそも default_order 機能を導入したコミットでは、build_order や ordered_relation は「order_values が空のときは default_order_values を使う」というフォールバックを持つように変更されていましたが、reverse_order! 側の知識更新が漏れていた、という位置づけです。
修正内容
reverse_order! にも default_order_values の存在を考慮させ、明示的 order がない場合は default_order を反転するようにしました。
def reverse_order! # :nodoc:
orders = order_values.compact_blank
if orders.empty? && (default_orders = default_order_values.compact_blank).any?
self.default_order_values = reverse_sql_order(default_orders)
else
self.order_values = reverse_sql_order(orders)
end
self
endポイント:
order_valuesに有効な order がある場合
→ これまで通りorder_valuesを反転 (明示的order優先の挙動はそのまま)order_valuesが空で、default_order_valuesが存在する場合
→default_order_valuesをreverse_sql_orderに通してdefault_order_values自体を反転
(ここが新挙動)
この実装により、以下が成立します:
# 1. default_order だけの場合 → その default_order が反転される
Model.default_order("name ASC").reverse_order.to_sql
# => "... ORDER BY name DESC"
# 2. default_order があるが、明示的 order を追加した場合 → 明示的 order を反転
Model.default_order("name ASC").order("id").reverse_order.to_sql
# => "... ORDER BY id DESC" # default_order はあくまでフォールバック
# 3. default_order も明示的 order もない場合
# → 今まで通り PK DESC か IrreversibleOrderError の挙動を維持
Model.all.reverse_order.to_sql
# => これまでと同じ PK 降順 or IrreversibleOrderErrorテスト
activerecord/test/cases/relations_test.rb に以下のような意図のテストが追加されています:
default_order("title ASC").reverse_orderの SQL がORDER BY title DESCになること- 明示的な
order("title ASC")のreverse_orderと同じ並び替え指定になること - 主キーによるフォールバックが用いられていないこと (PK 名が含まれないこと)
テストは sqlite3/postgresql/mysql2/trilogy でグリーンとのことです。
- 影響範囲・注意点
- 影響するのは
default_orderを使っているコードで、かつその relation に対してreverse_orderを呼んでいるケース に限定されます。- 具体例:
- モデルやスコープで
default_order "created_at ASC"を定義し、ビューや API でreverse_orderを使って「新しい順」「古い順」を切り替えている場合など
- モデルやスコープで
- 具体例:
- これまで:
default_orderのみ →reverse_orderすると PK DESC でソート (事実上バグ・silent wrong result)
- これから:
default_orderのみ → その default_order を単純反転
- 明示的
orderを使っているコードの挙動は変わりません (order_valuesが優先されるロジックはそのまま)
注意点 / マイグレーション的観点:
- もし既存アプリが「このバグ前提の挙動 (default_order を無視して PK で降順になる)」を暗に利用していた場合、
reverse_orderの結果が変わる可能性があります。- 例:
default_order("name ASC")を定義したが、「reverse_orderしたらなぜか id DESC になる」挙動を仕様として組み込んでいた場合
- 例:
- 通常は「
default_orderを反転してほしい」と考える方が自然なので、ほとんどのアプリにとっては バグ修正による改善 になり、破壊的と見なさないケースが多いと思われますが、挙動が変わる点は留意が必要です。
- 参考情報 (あれば)
- この PR とは別に、同じ
default_order機能周りで以下の既知バグがあると記載されています:has_many default_order:が、関連がロード済みのときに無視される問題 (#57538)
→ こちらはrelation/query_methodsではなくアソシエーション層の問題で、今回の修正とは独立しています。
- 実装の観点では、「
build_order/ordered_relationと同様、reverse_order!もdefault_order_valuesを考慮するようにした」という一貫性の確保が主眼です。
#57546 Test ContentDisposition.format class method
マージ日: 2026/6/2 | 作成者: @hammadxcm
概要 (1-2文で)
ActionDispatch::Http::ContentDisposition.formatクラスメソッドに対して、これまで存在していなかった直接のテストを追加した PR です。プロダクションコードの変更は一切なく、テストコードのみの追加です。変更内容の詳細(あればサンプルコードも含めて)
対象
ActionDispatch::Http::ContentDisposition.formatというクラスメソッド- Content-Disposition ヘッダ文字列を組み立てるための公開ショートカットメソッド
- データストリーミング処理や ActiveStorage 内部で利用される想定の API
これまでの問題点
- 既存テストは
ActionDispatch::Http::ContentDispositionのインスタンスを生成し、#to_sを呼ぶ形でのみ挙動を検証していた - そのため、公開 API としての
.format自体がテストされていなかった(未カバー)
- 既存テストは
今回の変更
actionpack/test/dispatch/content_disposition_test.rbに.format用のテストを追加(+10行).formatを以下の2パターンでテスト:- ファイル名なしでの呼び出し
- ファイル名ありでの呼び出し
想定されるテストイメージは以下のような形です(概念的なサンプル):
rubydef test_format_without_filename header = ActionDispatch::Http::ContentDisposition.format(disposition: "attachment") assert_equal "attachment", header end def test_format_with_filename header = ActionDispatch::Http::ContentDisposition.format( disposition: "attachment", filename: "report.pdf" ) assert_equal 'attachment; filename="report.pdf"; filename*=UTF-8\'\'report.pdf', header end実際の期待値文字列やオプションは内部仕様に依存しますが、PR では「
.formatを直接呼び出し、その戻り値(ヘッダ文字列)を検証する」形でカバレッジを追加しています。
- 影響範囲・注意点
影響範囲
- Rails 利用アプリの挙動には変更なし
ActionDispatch::Http::ContentDisposition.formatを既に使っているコードへの互換性影響もなし- 主な影響は「テストカバレッジが正しく
.formatにも及ぶようになった」ことのみ
注意点
.formatの挙動自体は変わっていないため、これを契機に API の仕様変更などが行われたわけではない- ただし、今後
.formatの実装を変更した際にはこのテストが壊れることでリグレッションを検知できるようになったため、**.formatの振る舞いが事実上「仕様として固定化されやすくなった」**とも言える .formatを直接利用しているライブラリやアプリの開発者にとっては、今後このテストが仕様の参考になる
- 参考情報 (あれば)
- 対象クラス:
ActionDispatch::Http::ContentDisposition - 主な利用箇所の例(Rails 本体側)
- ストリーミングレスポンス (
send_data,send_fileなど) のヘッダ生成 - ActiveStorage のファイルダウンロードレスポンスヘッダ生成
- ストリーミングレスポンス (
- この PR: https://github.com/rails/rails/pull/57546
#57544 Test ValidationError exposes model and message
マージ日: 2026/6/2 | 作成者: @hammadxcm
- 概要 (1–2文で)
ActiveModel::ValidationErrorに対するテストを拡充し、validate!実行時に発生する例外から#modelとエラーメッセージが期待どおり取得できることを検証する変更です。プロダクションコードの変更はなく、テストコードのみの追加です。
- 変更内容の詳細
対象ファイル:
activemodel/test/cases/validations_test.rb(+12/-0)
主なポイント:
ActiveModel::ValidationErrorはドキュメント上、以下を保証しています:#modelメソッドで、バリデーションに失敗したモデルインスタンスを取得できる。- 例外メッセージは
"Validation failed: ..."という形式で生成される。
これまでの
validate!のテストは「例外が発生すること」だけを確認しており、- どのモデルインスタンスが
ValidationErrorに入ってくるか (error.model) - 例外メッセージ (
error.message)
については検証していませんでした。
- どのモデルインスタンスが
今回のPRでは、
validate!を呼び出してActiveModel::ValidationErrorが発生したときに、error.modelが元のモデルインスタンスと同一であることerror.messageが"Validation failed: ..."形式(かつ、具体的なエラー内容を含む)になっていること
を明示的にアサートするテストが追加されています。
おおよそのイメージとして、以下のようなテストが追加されていると考えられます(疑似コード):
def test_validate_bang_exposes_model_and_message
user = User.new # 何らかのバリデーション付きモデル
assert_raises(ActiveModel::ValidationError) do
user.validate!
end.tap do |error|
assert_equal user, error.model
assert_match "Validation failed:", error.message
# 必要に応じて特定のエラーメッセージ文字列も確認
end
end実際のPRでは上記と同等のアサーションを activemodel/test/cases/validations_test.rb に数行追加しているだけで、テストケースは 1 つ増える程度の小さな差分です。
- 影響範囲・注意点
影響範囲:
- テストコードのみの変更のため、Rails を利用するアプリケーションの挙動や公開APIには一切影響しません。
- CI でのテストカバレッジ向上と、
ActiveModel::ValidationErrorの仕様に対する回帰検知能力が高まります。
注意点:
- このPRが前提としている仕様(
ValidationError#modelと"Validation failed: ..."形式のメッセージ)はすでにドキュメント化・実装済みのものです。
→ もし将来的にこの仕様を変更する場合(例: メッセージフォーマットを変える)には、今回追加されたテストが落ちることになります。 - 独自に
ActiveModel::ValidationErrorをラップ・再生成しているコードがある場合は、Rails 本体と同じインターフェース (#modelとメッセージ形式) を維持しているか確認しておくと、今後の互換性維持に役立ちます。
- このPRが前提としている仕様(
- 参考情報 (あれば)
対象クラス:
ActiveModel::ValidationError- ドキュメント: Active Model のバリデーションエラー用例外クラスで、
record.validate!やmodel.validate!でバリデーションに失敗したときに発生します。 - 典型的な利用例:ruby
begin user.validate! rescue ActiveModel::ValidationError => e # e.model で失敗した user インスタンスにアクセス # e.message は "Validation failed: Name can't be blank" など end
- ドキュメント: Active Model のバリデーションエラー用例外クラスで、
このPRの位置付け:
- 「ドキュメントされている仕様をテストでカバーする」タイプの変更であり、今後のリファクタリングやメッセージ生成ロジック変更時に、仕様が誤って壊されないようにするための安全網になっています。
#57543 Test ArrayInquirer#any? without candidates
マージ日: 2026/6/2 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveSupport::ArrayInquirer#any?を引数なしで呼び出した場合の挙動(配列に要素があるかどうかを返す)を確認するテストが追加された PR です。
本番コードへの変更はなく、テストカバレッジのみが拡充されています。
- 変更内容の詳細
- 対象クラス:
ActiveSupport::ArrayInquirer - 対象メソッド:
#any?
ArrayInquirer#any? にはもともと以下の3パターンの利用形態があります:
引数あり:
rubyinquirer.any?(:phone, :tablet)→ 渡した候補のうち一つでも含まれているか判定。
ブロックあり:
rubyinquirer.any? { |v| v.to_s.start_with?("p") }→ 内部配列の要素に対してブロック条件を満たすものがあるか判定。
引数・ブロックなし(今回テストを追加したケース):
rubyvariants = ActiveSupport::ArrayInquirer.new([:phone, :tablet]) variants.any? # => true empty_variants = ActiveSupport::ArrayInquirer.new([]) empty_variants.any? # => false→
Array#any?と同様に、「配列が空でないかどうか」を返す。
今回の PR では、この「引数なし」のケースについて、以下のようなテストが activesupport/test/array_inquirer_test.rb に追加されています(イメージ):
def test_any_without_arguments_for_populated_inquirer
inquirer = ActiveSupport::ArrayInquirer.new([:phone, :tablet])
assert_equal true, inquirer.any?
end
def test_any_without_arguments_for_empty_inquirer
inquirer = ActiveSupport::ArrayInquirer.new([])
assert_equal false, inquirer.any?
end実際には 5 行程度のテスト追加のみで、本体コードには一切手が入っていません。
- 影響範囲・注意点
影響範囲:
- 実行時の挙動は一切変わりません(既存の
ArrayInquirer#any?の仕様をテストで明示しただけ)。 - すでに
variants.any?のような引数なし呼び出しを利用しているコードの挙動はそのままです。
- 実行時の挙動は一切変わりません(既存の
注意点:
- この PR により、「
ArrayInquirer#any?は引数・ブロックなしの場合、Array#any?と同じく『要素が1つでもあれば true』を返す」という挙動がテストで固定化されます。 - 将来的にこの挙動を変えようとするとテストが落ちるため、仕様としての重みが増したことになります。
- テストのみの変更なので、パフォーマンスや互換性への実務的なリスクはありません。
- この PR により、「
- 参考情報 (あれば)
- 対象メソッド:
ActiveSupport::ArrayInquirer#any?
Rails ガイド等でArrayInquirerが "variants" などのフラグ判定を簡潔に書くためのユーティリティとして紹介されることがありますが、その一部としてany?がArray#any?と互換の呼び出し形態を持つことが、この PR によりテストで明確化されました。
#57545 Test BigInteger serializing string values
マージ日: 2026/6/2 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveModel::Type::BigInteger#serializeが文字列をどう扱うかについて、既存の挙動をテストで明示的にカバーする PR です。プロダクションコードの変更はなく、テストが 7 行追加されただけです。
- 変更内容の詳細
対象: activemodel/test/cases/type/big_integer_test.rb
この PR では、ActiveModel::Type::BigInteger#serialize に対する以下の「文字列入力時の挙動」をテストで保証しています。
- 数値文字列の場合
- 例:
"123"→123(Integer)
- 例:
- 先頭が数値だが途中から非数値文字が含まれる場合
- 例:
"123abc"→123に切り詰められる
- 例:
- 数値を含まない文字列の場合
- 例:
"abc"→nil
- 例:
テスト側では、概ね次のようなケースが追加されていると考えられます(擬似コード):
def test_serialize_numeric_string
type = ActiveModel::Type::BigInteger.new
assert_equal 123, type.serialize("123")
end
def test_serialize_leading_numeric_string
type = ActiveModel::Type::BigInteger.new
assert_equal 123, type.serialize("123abc")
end
def test_serialize_non_numeric_string
type = ActiveModel::Type::BigInteger.new
assert_nil type.serialize("abc")
endこれにより、これまで Integer や Integer ライクな値("1_000" を to_i するとか、BigDecimal 等)についてしかテストされていなかった部分に、「文字列入力時の分岐」がきちんと含まれるようになっています。
- 影響範囲・注意点
影響範囲
- テストコードのみの追加であり、本体コード (
ActiveModel::Type::BigInteger) の挙動変更はありません。 - 現在すでに存在している「文字列→整数(or nil)」への変換仕様を明文化・固定化する意味合いがあります。
- テストコードのみの追加であり、本体コード (
注意点・仕様として改めて意識すべき点
"123abc"のような「先頭が数値の文字列」は、エラーではなく 123 としてシリアライズされる
→ 予想に反してサイレントに切り捨てられる可能性があるため、バリデーションや型チェックで厳密性を求める場合は別途対処が必要。- 完全に非数値の文字列は
nilになる
→ DB カラムが NOT NULL / 外部キーなどの場合、このnilが後続の処理でエラーを引き起こす可能性がある。 - 今回のテスト追加により、この挙動は今後のリファクタリング時にも「期待される既存仕様」として守られる可能性が高くなります。
- 参考情報 (あれば)
- 該当クラス:
ActiveModel::Type::BigInteger- Active Record の bigint カラムや類似の用途で使われる型オブジェクトで、
serializeは「Ruby オブジェクト → DB 送信値」への変換を担当します。
- Active Record の bigint カラムや類似の用途で使われる型オブジェクトで、
- 変換ロジックの背景:
- ActiveModel の数値系 Type は、多くが
to_i/to_sベースで文字列を解釈しており、その標準挙動(先頭数値だけ読む、非数値は 0 / nil 扱い等)に依存していることが多いです。
- ActiveModel の数値系 Type は、多くが
- 実務上の補足:
- 「ユーザー入力などで混入しうる不正な文字列を BigInteger にそのまま渡した場合、どうなるか」を把握・テストで保証したいアプリケーションでは、この PR のようなテストを自前のアプリ層で追加しておくと挙動がブレにくくなります。
#57541 Test truncate when omission is longer than truncate_to
マージ日: 2026/6/2 | 作成者: @hammadxcm
- 概要 (1-2文で)
String#truncateにおいて、:omissionがtruncate_toより長い場合の挙動(省略記号だけが返り、結果文字列がtruncate_toを超えるケース)をテストでカバーする PR です。プロダクションコードの変更はなく、テスト追加のみです。
- 変更内容の詳細
対象メソッド:
ActiveSupport::CoreExtensions::String::Filters#truncate(String#truncate)既存仕様(ドキュメントに記載されている挙動):
truncate_toで指定した最大長を超えないように文字列を切り詰める- ただし「元の文字列 (
text) と:omissionの両方がtruncate_toより長い場合」は例外的に、結果の長さがtruncate_toを超えうる、という仕様になっている - 内部的には「offset が負になる」パスでこの挙動が発生するが、その分岐がテストされていなかった
本PRでの変更:
activesupport/test/core_ext/string_ext_test.rbに、以下の条件を満たすテストケースが追加された::omissionの文字列長 >truncate_to- その結果として、戻り値が
:omissionだけになることを確認する
- 行数としては +5 行程度の小さなテスト追加のみ
サンプルイメージ(擬似コード・概念的な例):
rubytext = "Hello world" omission = "[TRUNCATED]" # 例: 長さ 11 truncate_to = 5 result = text.truncate(truncate_to, omission: omission) # このケースでは result == "[TRUNCATED]" となり、 # 結果の長さ 11 > truncate_to 5 となることが仕様として許容される。この「
truncate_toより長い:omissionがそのまま返る」挙動が、既にドキュメントに明記されている仕様であり、それに対するテストが追加された形です。
- 影響範囲・注意点
影響範囲:
- 本PRはテストコードのみの変更であり、ランタイム挙動や既存アプリケーションへの影響はありません。
- ただし、「
:omissionが長い場合に結果がtruncate_toを超える」という現在の仕様がテストで固定化されたため、将来的にこの仕様を変える場合は互換性問題として扱う必要が出てきます。
開発者が意識すべきポイント:
String#truncateを利用する際、「結果文字列が常にtruncate_to以下になる」と思い込むとバグにつながります。- 特に UI レイアウトや DB カラム幅など、「結果文字列長を厳密に制限したい」場面では、次のような考慮が必要です:
omissionをtruncate_to以下の長さに抑える- あるいは、
truncateの後にさらにmb_chars.limitなどで二重に長さチェックを行う
- 今回のテストにより、この挙動は「仕様として意図的にサポートされている」ことがより明確になります。
- 参考情報 (あれば)
- Rails API ドキュメント(String#truncate)には、以下のような文言があります(要旨):
- 「結果の全体長は
truncate_toを超えません。ただし、textと:omissionの双方がtruncate_toより長い場合はこの限りではありません。」
- 「結果の全体長は
- このPRは、そのドキュメント記載の「例外ケース(offset が負になる分岐)」に対するテストカバレッジを補完するものです。
- PR番号: https://github.com/rails/rails/pull/57541
#57540 Test Array offset accessors when out of bounds
マージ日: 2026/6/2 | 作成者: @hammadxcm
- 概要 (1-2文で)
Array#second〜#forty_twoおよび#second_to_last/#third_to_lastが、配列の長さが足りない場合にnilを返す挙動についてテストを追加した PR です。プロダクションコードの変更はなく、テストコードのみの追加です。
- 変更内容の詳細
- 対象:
activesupport/test/core_ext/array/access_test.rb - 目的: 「範囲外アクセス時に
nilを返す」というパスをテストでカバーすること。
これまで存在していたテスト test_specific_accessor は、42 要素の配列を使って #second〜#forty_two を確認していたため、常に有効なインデックスにアクセスしており、「配列が短い場合に nil を返す」動作は検証されていませんでした。
今回の PR では、長さが不足している配列に対して、以下のメソッドが nil を返すことを確認するテストが追加されています。
- 先頭側のエイリアス
Array#second〜#forty_two
- 末尾側のエイリアス
Array#second_to_lastArray#third_to_last
具体的なイメージとしては、例えば次のようなパターンを確認するテストが加えられています(実際のコードイメージ):
def test_specific_accessor_out_of_bounds
ary = [1] # 意図的に短い配列
assert_nil ary.second # 2番目の要素は存在しない
assert_nil ary.third # 3番目も存在しない
# ...
assert_nil ary.forty_two # 42番目ももちろん存在しない
assert_nil ary.second_to_last # 要素1つでは「後ろから2番目」は存在しない
assert_nil ary.third_to_last # 同様に存在しない
endポイントは、配列の長さ < 要求された n 番目/後ろから n 番目の位置の場合に、例外ではなく nil を返すことを明示的にテストしている点です。
- 影響範囲・注意点
- 影響範囲:
- 変更はテストコードのみであり、
Arrayの拡張メソッド (second,third, ...,forty_two,second_to_last,third_to_last) の実装には一切手を入れていません。 - 既存アプリケーションの挙動には影響ありません。
- 変更はテストコードのみであり、
- 開発者視点での意義:
- 以前から「範囲外アクセス時は
nilを返す」という仕様で動いてはいたものの、それを保証するテストがなかったため、将来のリファクタリング等でこの挙動が壊れた場合に検知できるようになったといえます。 - これにより、「配列が短いかもしれないが
array.thirdなどをそのまま呼んでnilチェックで扱う」といったコードパターンに対して、仕様保証が強化されます。
- 以前から「範囲外アクセス時は
注意点として、テストが追加されたことで明示的に仕様が固定されたとも解釈できるため、将来的に「範囲外なら例外を投げたい」といった互換性を壊す変更は、より困難になります(少なくとも大きなBreaking Change扱いになる)。
- 参考情報 (あれば)
- 対象メソッドは Active Support の
ArrayInquirerではなく、ActiveSupport::CoreExtensions::Array::Access系の拡張 (Array#secondなど) です。 - 関連する既存テスト:
activesupport/test/core_ext/array/access_test.rb内のtest_specific_accessor
こちらは「配列が十分に長い場合に、各メソッドが正しい要素を返すこと」を確認しており、今回の PR で「短い配列時はnil」という逆パターンのテストが補完されました。
#57542 Test Mime::Type#html? predicate
マージ日: 2026/6/2 | 作成者: @hammadxcm
- 概要 (1-2文で)
Mime::Type#html?メソッドの挙動(:htmlシンボル、"html"を含むMIMEタイプ文字列、それ以外でfalse)を直接検証するテストが追加されたPRです。プロダクションコードの変更はなく、テストカバレッジを補完する目的の修正です。
- 変更内容の詳細
Mime::Type#html? は以下のような条件で真偽値を返すメソッドです:
Mime[:html]のようにシンボルが:htmlの場合 →true- MIMEタイプ文字列に
"html"が含まれている場合
例:Mime::Type.new("application/xhtml+xml")→true - 上記いずれにも該当しない場合 →
false
これまでこのメソッドに対する「直接の」テストが存在しなかったため、以下を対象とするテストが actionpack/test/dispatch/mime_type_test.rb に追加されています(+7行):
- シンボルが
:htmlである場合に#html?がtrueを返すこと - MIMEタイプ文字列に
"html"を含む ("application/xhtml+xml"など) 場合にtrueを返すこと - それ以外のMIMEタイプ (
text/plainなど) ではfalseになること
※ PR本文から読み取れるテストイメージ(概念的な例):
def test_html_predicate
assert Mime[:html].html?
xhtml = Mime::Type.new("application/xhtml+xml")
assert xhtml.html?
plain = Mime::Type.new("text/plain")
assert_not plain.html?
endあくまで例示ですが、このような「trueパス2種類+falseパス1種類」を明示的にカバーするテストが追加されています。
- 影響範囲・注意点
- 影響範囲:
- 影響はテストコードのみで、本番コード・APIの仕様変更はありません。
- 既に実運用されている
Mime::Type#html?の振る舞いを、テストとして「仕様として固定」した形になります。
- 注意点:
- 将来的に
html?の判定仕様(例:"html"部分一致をもっと厳しく/緩くする)が変わる場合、このテストが落ちることで、仕様変更が意図的かどうかを確認するきっかけになります。 - カスタムMIMEタイプや
xhtml系の取り扱いでhtml?の真偽に依存しているコードがある場合、その現在の挙動がテストによって明示されたと捉えられます(「"html"を含めば true になる」という挙動が正式にテストで保証される)。
- 将来的に
- 参考情報 (あれば)
- 対象メソッド:
Mime::Type#html?- 用途の典型例:
- コントローラやミドルウェア内で
request.format.html?のように利用し、「HTML向けレスポンスかどうか」を判定する場面など。
- コントローラやミドルウェア内で
- 用途の典型例:
- 関連ファイル:
actionpack/test/dispatch/mime_type_test.rb- 各種
Mime::Type振る舞いのテストが集約されているファイルで、その一部として#html?が明示的にテスト対象に加わった形です。
- 各種
#56422 ActionView::TestCase#render resets rendered
マージ日: 2026/6/2 | 作成者: @drjayvee
- 概要 (1-2文で)
ActionView::TestCase の#renderが、複数回呼び出したときにrenderedの内容を正しくリセットできていなかったバグを修正した PRです。メモ化導入 (#51093) によって壊れていた既存の挙動を復元しつつ、content_classをカスタム実装しているケースにも配慮した実装になっています。
- 変更内容の詳細
問題の背景
ActionView::TestCaseでは、renderを呼ぶと結果がrenderedに溜まっていきます。- 以前は
render内で@rendered.clearを呼ぶことで、「テストごとのrender呼び出しごとに内容をリセットする」という挙動になっていました。 - #51093 でメモ化が導入された際、この「
renderがrenderedをリセットする」という挙動が壊れ、renderを複数回呼ぶと出力が意図せず蓄積されてしまう状態になっていました。
今回の修正の要点
説明文から読み取れるポイント:
ActionView::TestCase#renderがrenderedをリセットする振る舞いを復元した。- 以前は
@rendered.clearを直接呼んでいたが、メモ化の影響でそれが正しく動かなくなっていた。 - さらに、ユーザーが
content_classクラス属性を使って@renderedのクラスを差し替えできる仕様があるため、- そのオブジェクトが 必ずしも
#clearを実装しているとは限らない(契約上必須なのは#<<だけ)。 - したがって
@rendered.clearを前提とする実装は安全ではない。
- そのオブジェクトが 必ずしも
- そこで、「
@renderedをクリアする」のではなく、「新しい@renderedインスタンスを毎回作る」というアプローチを採用している。
コードのイメージ(実際のコードを単純化した擬似例):
# 以前 (概念的な挙動)
def render(*args)
@rendered ||= content_class.new
@rendered.clear # この辺が壊れていた & clear を必須としてしまう
super
end
# 今回の修正後イメージ
def render(*args)
# 毎回新しいインスタンスを作ることで、「リセット」を保証する
@rendered = content_class.new
super
endテスト (actionview/test/template/test_case_test.rb) では:
renderを同じテスト内で複数回呼んだときに、renderedが前回の結果を持ち越さないこと。- メモ化導入後に壊れていた挙動が正しく戻っていること。
などを確認するテストが追加/調整されています。
CHANGELOG.md には:
- ActionView に関するバグフィックスとして、
ActionView::TestCase#renderがrenderedを正しくリセットするようになった旨- (必要であれば関連 Issue / PR 番号) が追記されています。
- 影響範囲・注意点
- 対象:
ActionView::TestCaseを使っている ビューのテストコード。 - 影響内容:
renderを同一テスト内で複数回呼び出したときのrenderedの内容が、前回の結果を引き継がず、毎回リセットされるようになります。- これはもともとの想定挙動であり、「壊れていたものが直る」形なので、Rails の従来の仕様に沿った変更です。
content_classをカスタム実装している場合:- これまで
#clearが実装されていなくても、今回の修正により#clearの有無に依存しない 実装になっているため、むしろ安全側に振られています。 content_class#initializeが毎回呼ばれる前提になるので、もし重い初期化処理をしている場合は、パフォーマンス上の影響がごくわずかに増える可能性がありますが、通常のテスト用途では問題ないレベルと考えられます。
- これまで
- 参考情報 (あれば)
- 関連 Issue / PR:
- #56235: 本件の詳細背景が議論されている Issue(この PR 内で参照されている)
- #51093:
ActionView::TestCaseにメモ化を導入し、本バグの原因となった変更
- カスタム
content_classを使っているプロジェクトでは、renderを複数回呼んだときのrenderedの値(およびそのクラスのライフサイクル)が、今回の仕様と整合しているかを軽く確認しておくと安心です。
#57534 Fix disable_joins through associations grouping by composite key
マージ日: 2026/6/2 | 作成者: @55728
- 概要 (1-2文で)
disable_joins: trueを指定したhas_many/has_one :throughで、途中に order があり、かつ関連先が複合キー(複合 primary key / foreign key)を使っている場合に、結果が黙って空配列になってしまうバグを修正しています。
複合キーを使う関連の in-memory グルーピング処理で、キーの扱い方を正しく複合キー対応にすることで、取得済みレコードが消えてしまう問題を解消しています。
- 変更内容の詳細
問題のパス
disable_joins: true を使うと、AR は JOIN を避けるため、ActiveRecord::Associations::DisableJoinsAssociationScope によって一つの JOIN クエリを複数クエリに分解します。
この際、関連チェーンのどこかに order がついていて、かつ最終スコープ側には order がない場合、その order は Ruby 側での in-memory ソート・グルーピングに回されます。
その処理は ActiveRecord::DisableJoinsAssociationRelation#load にあり、問題のコードは:
records_by_id = records.group_by do |record|
record[key]
end
records = ids.flat_map { |id| records_by_id[id] }
records.compact!ここで key は reflection.join_primary_key で、
単一キーなら "id" のような文字列ですが、複合キーの場合は ["shop_id", "id"] のような「カラム名の配列」になります。
belongs_to先が複合 primary key を持つ場合
(BelongsToReflection#join_primary_key→association_primary_key)has_many/has_one側が複合 foreign key を持つ場合
(AssociationReflection#join_primary_key→foreign_key)
バグの核心
複合キーの場合でも record[key] と一発で読もうとしているのが問題です。
record[["shop_id", "id"]]は、「["shop_id", "id"]という名前の属性」を読む挙動になる- 当然そんな属性は存在しないが、ここでは
MissingAttributeErrorにならず、Null属性としてnilが返る - 結果として、全レコードのグルーピングキーが
nilになり、1つのグループrecords_by_id[nil]に全部まとまる - しかし
idsは[ [1, 2], [1, 3], ... ]のような「複合キーのタプル配列」になっているので、rubyでrecords = ids.flat_map { |id| records_by_id[id] }records_by_id[[1, 2]]等を探しても何も見つからない - 結果として
recordsは[]になり、その後のcompact!でも変わらず、関連は空配列になってしまう
このため、「SQL ではちゃんとレコードが取れているのに、その後の Ruby 側でドロップされて消える」という非常に気付きづらい不具合が起きていました。
修正内容
キーが複合キー(配列)である場合に、配列の各要素カラムを個別に読み出し、同じ構造(配列)でグルーピングキーを構成するように修正しています。
変更後(擬似コード):
records_by_id = records.group_by do |record|
if key.is_a?(Array)
key.map { |column| record[column] } # ["shop_id", "id"] -> [record["shop_id"], record["id"]]
else
record[key]
end
end- 単一カラムキーのケースは従来通り
record[key]のままなので、挙動は変えない - 複合キーの場合だけ、
["shop_id", "id"]→[record["shop_id"], record["id"]]という配列キーでグルーピングするようになる - これにより、
idsに入っている複合 ID タプル(例:[1, 2])とrecords_by_idのキーが一致し、flat_mapで正しくレコードが取り出せる
再現ケースの例
テストで使われている構成は以下のようなものです(要約):
class Cpk::Author < ActiveRecord::Base # 単一カラム PK
has_many :ordered_books, -> { order(id: :desc) }, class_name: "Cpk::Book"
has_many :orders, through: :ordered_books, source: :order
has_many :no_joins_orders,
through: :ordered_books,
source: :order,
disable_joins: true
end
class Cpk::Book < ActiveRecord::Base # PK [:author_id, :id]
belongs_to :order, foreign_key: [:shop_id, :order_id] # Cpk::Order PK [:shop_id, :id]
endauthor.orders.to_a
通常の JOIN を使うパスでは、期待通り[ #<Order ...>, #<Order ...> ]が返るauthor.no_joins_orders.to_adisable_joins: trueのパスでは、main ブランチでは[]が返ってしまう
この PR の修正後は、JOIN ありの場合と同じ正しいリストが返るようになる
テスト追加
has_many_through_disable_joins_associations_test.rbにtest_ordered_disable_joins_through_with_composite_primary_key_sourceを追加- 既存の
Cpk::Author/Cpk::Book/Cpk::Orderモデルを再利用している - main ではこのテストが赤(
no_joins_ordersが[])になり、修正適用後に緑になることを確認 - sqlite3 / postgresql 両方で red→green を確認している
- 関連するテスト群 (
has_many_through_disable_joins_associations_test,has_one_through_disable_joins_associations_test,has_many_through_associations_test,has_many_associations_test) も両アダプタでグリーン
- 影響範囲・注意点
影響を受けるのは、以下すべてを満たす関連のみです。
has_many/has_one :throughを利用している- 関連定義に
disable_joins: trueを付けている - 関連チェーン中のどこかに
order(...)が指定されている
(最終スコープにorderがない → in-memory ソート経由になるパス) - 途中または最終的な関連先が複合キーを利用している
- 複合 primary key を持つ
belongs_to先 - 複合 foreign key を使う
has_many/has_oneなど
- 複合 primary key を持つ
この条件に当てはまる場合、これまで空配列が返っていたのが、実際には関連するレコードが返るようになる ため、以下に注意するとよいです。
- 既存コードが「空配列である」ことを前提としたワークアラウンドを組んでいた場合、ロジックが変わる可能性がある
(例: 期待通り動かないのでdisable_joinsを避けるようにしていたが、いつの間にか残っていた、等) - この修正により「取得結果が増える」方向に変わるので、セマンティクスとしては本来の挙動に近づきますが、テストが「空を期待していた」場合は見直しが必要です
- 単一キーの関連や、
disable_joins: false/ 未指定の関連への影響はありません - in-memory グルーピングのロジック側の変更のみで、SQL 発行やクエリパス自体は変わっていません
- 参考情報 (あれば)
- この問題は、最近修正された他の複合キー関連の不具合(
findやids=writer で配列キーの扱いを誤っていたもの)と「同じファミリー」のバグとされていますが、メカニズムは別です。- 以前の問題: 配列キーを「no-op な型キャスト」として誤用
- 今回の問題: 配列キーを「属性名」として誤用し、
record[["shop_id", "id"]]のようなアクセスをしていた
activerecord/CHANGELOG.mdにも本件が追記されており、disable_joinsと複合キーの組み合わせに関する既知の問題がこれでカバーされる形になります。
#57469 Fix FormBuilder#to_partial_path returning nil for non-Builder subclasses
マージ日: 2026/6/1 | 作成者: @55728
- 概要 (1-2文で)
FormBuilder#to_partial_pathが、クラス名が*Builderで終わらないサブクラスに対してnilを返してしまうバグを修正した PR です。String#sub!をString#subに変更することで、あらゆる FormBuilder サブクラスで常に有効なパーシャルパス文字列が得られるようになります。
- 変更内容の詳細
バグの内容
ActionView::Helpers::FormBuilder には、インスタンスをそのまま render できるように to_partial_path / _to_partial_path が定義されています。
現状の実装(バグあり):
def self._to_partial_path
@_to_partial_path ||= name.demodulize.underscore.sub!(/_builder$/, "")
endここで問題なのは sub!(破壊的メソッド)を使っている点です。
"labelled_form_builder".sub!(/_builder$/, "") # => "labelled_form"(マッチするので OK)"admin_form".sub!(/_builder$/, "") # => nil(マッチしないので nil)
sub! はマッチしなかった場合 nil を返す仕様のため、クラス名が *_builder で終わらない場合、_to_partial_path が nil になり、そのままキャッシュされてしまいます。結果として:
class AdminForm < ActionView::Helpers::FormBuilder; end
AdminForm._to_partial_path # => nil (本来は "admin_form" を期待)
AdminForm.new(...).to_partial_path # => nil
# => render @admin_form で後段が落ちるRails コアのテストにある以下のサブクラスも同様に壊れていました:
class LabelledFormBuilderSubclass < LabelledFormBuilder; end
LabelledFormBuilderSubclass.new(...).to_partial_path
# => nil(本来は "labelled_form_builder_subclass" を期待)修正内容
sub! を非破壊版の sub に変更し、マッチしない場合も元文字列がそのまま返るようにしました。
def self._to_partial_path
- @_to_partial_path ||= name.demodulize.underscore.sub!(/_builder$/, "")
+ @_to_partial_path ||= name.demodulize.underscore.sub(/_builder$/, "")
endString#sub はマッチしなければ元の文字列のコピーを返すため、*_builder で終わらない FormBuilder サブクラスでも、_to_partial_path には常に有効な文字列が入ります。
修正後の挙動:
ActionView::Helpers::FormBuilder._to_partial_path
# => "form" (従来どおり)
LabelledFormBuilder._to_partial_path
# => "labelled_form" (従来どおり。末尾 _builder が削られる)
LabelledFormBuilderSubclass._to_partial_path
# => "labelled_form_builder_subclass" (nil ではなくなる)
class AdminForm < ActionView::Helpers::FormBuilder; end
AdminForm._to_partial_path
# => "admin_form"つまり:
FormBuilder/*Builderで終わる慣習に沿ったクラス
→ これまでどおり末尾_builderが取り除かれる*Builderで終わらないクラス
→ クラス名をそのままunderscoreした文字列が返る(admin_formなど)
テストと CHANGELOG
- 既存の
test_form_for_with_labelled_builder_pathは「慣習通りの*Builder名」のケースだけを見ていた - そこに、既に定義済みの
LabelledFormBuilderSubclassを使って「*Builderで終わらないサブクラス」の回 regresstion テストを追加 actionview/test/template/form_helper_test.rb全体(343 runs / 431 assertions)はグリーンactionview/CHANGELOG.mdに、バグ修正としてエントリを追加
- 影響範囲・注意点
- 影響するのは:
ActionView::Helpers::FormBuilderを継承したクラス- かつクラス名が
*Builderで終わらないもの - そして
render @form_builder_instanceのように、to_partial_pathベースでレンダリングしているケース
- これまで暗黙に壊れていたケースが正常に動くようになる変更であり、既存の慣習 (
*Builderでクラスを命名) に従ったコードの挙動は変わりません。 _to_partial_pathのキャッシュに今までnilが入っていたケースでは、今後は「期待されるパーシャルパス文字列」が入るようになるだけなので、後方互換性の問題は実質的にありません(nilを意図的に利用していたコードがない限り)。- もし
FormBuilderサブクラスを*Builderで終わらない名前で定義しており、かつrender @builderのような使い方をしている場合、Rails 更新後に初めてそこが動くようになる可能性があります。その場合は対応するパーシャル(例:app/views/admin_form/_admin_form.html.erb)が存在するか確認してください。
- 参考情報 (あれば)
- 対象メソッド:
ActionView::Helpers::FormBuilder._to_partial_path/#to_partial_path - 関連する Ruby 仕様:
String#sub!(pattern, replacement): マッチしなければnilString#sub(pattern, replacement): マッチしなければ元文字列のコピーを返す
- バグの起点:
- もともとは 2009 年の
self.model_name実装内でsub!が使われており、その後_to_partial_pathへのリネームを経ても引き継がれていた
- もともとは 2009 年の
- 影響コンポーネント:
actionviewのみ(3 ファイルの微小変更)
#57531 Fix collection ids= writers raising RecordNotFound for composite primary key models with string ids
マージ日: 2026/6/1 | 作成者: @55728
- 概要 (1-2文で)
複合主キーを持つモデルに対してhas_many/has_and_belongs_to_manyのxxx_ids=を文字列のID配列で呼び出すと、実在するレコードでもActiveRecord::RecordNotFoundが発生していた問題を修正するPRです。
フォームやJSONから来る「文字列の複合ID」を正しくキャストして関連付けできるようにしています。
- 変更内容の詳細
問題の挙動
対象ケース:
class Author < ApplicationRecord
has_many :books # Book.primary_key = [:author_id, :id]
end
ids = author.books.ids # => [[1, 10], [1, 20]]
author.book_ids = ids # OK (整数)
author.book_ids = ids.map { |t| t.map(&:to_s) } # ❌ RecordNotFound が発生フォーム (collection_select / チェックボックス) や params、JSON では ID は通常文字列になるため、
author.book_ids = params[:book_ids] # params[:book_ids] が [["1","10"], ["1","20"]] のような文字列の配列としたときに、本来存在する Book レコードが選択されているにもかかわらず ActiveRecord::RecordNotFound が発生していました。
原因
CollectionAssociation#ids_writer のキャスト処理が、複合主キーを想定していなかったことが原因です。
該当コード(単純化):
pk_type = klass.type_for_attribute(primary_key)
ids.map! { |id| pk_type.cast(id) }単一主キー:
primary_keyは"id"のような文字列
→type_for_attribute("id")はidカラムの型 (integerなど) を返す
→cast("1") # => 1となり、文字列が期待通りに整数へキャストされる複合主キー:
primary_key/association_primary_keyは["author_id", "id"]のような「カラム名の配列」
→type_for_attribute(["author_id", "id"])は意味のある型を解決できない
→ 結果として「汎用のActiveModel::Type::Value」が返る → そのcastはほぼ no-op なので、["1", "10"]はそのまま文字列のまま残る
その後の流れ:
- DB 検索自体は
where(primary_key => ids)で行われるため、DB 側の型変換によりレコードはちゃんと取得できる。 - 取得したレコードを Ruby 側で「主キー値をキー」にハッシュ化する際、主キーは 整数タプル になる例:ruby
# 実体はこういうイメージ index = { [1, 10] => <Book ...>, [1, 20] => <Book ...>, } - ところが lookup 側は、キャストされていない
["1", "10"]などの 文字列タプル をvalues_atに渡す:rubyindex.values_at(["1", "10"], ["1", "20"]) # => どれも見つからない - その結果、期待数と取得数が一致せず「存在しないIDが指定された」と判断し
ActiveRecord::RecordNotFoundを投げる。
修正内容
複合主キーの場合に、各キー成分ごとに対応するカラム型でキャストするように CollectionAssociation#ids_writer を修正しています。
イメージとしては次のような処理に分岐します(擬似コード):
if primary_key.is_a?(Array)
# ["author_id", "id"] のような配列
column_types = primary_key.map { |key|
klass.type_for_attribute(key)
}
ids.map! do |tuple|
# tuple は ["1", "10"] のような配列
tuple.each_with_index.map { |value, i|
column_types[i].cast(value) # => [1, 10] にキャストされる
}
end
else
# 従来の単一主キーの処理
pk_type = klass.type_for_attribute(primary_key)
ids.map! { |id| pk_type.cast(id) }
endこれにより:
author.book_ids = [["1", "10"], ["1", "20"]] # も正しく動作となり、フォームやAPIから渡された文字列複合IDをそのまま xxx_ids= に渡しても問題なく関連付けできるようになります。
この処理は:
has_manyのxxx_ids=has_and_belongs_to_manyのxxx_ids=
の両方で共有されているため、HABTM も同時に修正対象になっています。
テスト
has_many_associations_test.rb に回帰テストが追加されています:
- 複合主キーの関連に対して
- 文字列の複合IDを
xxx_ids=に渡す - 正しく関連づけされ、例外が発生しないことを検証
このテストは main ブランチでは red(バグ再現)、本修正適用後は green になることが確認されています。
sqlite3 / postgresql の両DBアダプタで has_many / has_and_belongs_to_many 関連テスト一式が通過しています。
- 影響範囲・注意点
- 影響を受けるのは「複合主キーを使っているモデル」の
has_many/has_and_belongs_to_manyのxxx_ids=のみです。 - 単一主キーのモデルに対する
xxx_ids=の挙動・互換性には変更はありません(既存のキャストロジックをそのまま維持)。 - 実運用上の影響:
- フォームのチェックボックスや
collection_selectで複合主キーの関連を選択して params 経由で更新するケース - API / JSON で文字列の複合IDを受け取り、そのまま
xxx_ids=に突っ込むケース
これらが RecordNotFound で落ちていた場合に、正常に動作するようになります。
- フォームのチェックボックスや
has_and_belongs_to_manyでも同じids_writerを共有しているため、HABTM で複合主キーを用いている場合も自動的に修正が効きます。- アプリ側で
xxx_ids=を monkey patch している場合:- 内部実装に依存したコードを書いていると、今回の内部仕様変更と競合する可能性があるため確認推奨です。
- 参考情報 (あれば)
- このPRは、以下と同じクラスの不具合(
primary_keyが配列であることを考慮せずtype_for_attributeしてしまうケース)の一つです:find_by_token_forの修正: コミット8617a7cfind_signedの修正: PR #57245findに対する兄弟修正: PR #57530
- 変更ファイル:
activerecord/CHANGELOG.md: バグ修正の記録を追加activerecord/lib/active_record/associations/collection_association.rb:ids_writerの複合主キー対応activerecord/test/cases/associations/has_many_associations_test.rb: 回帰テスト追加
#57530 Fix find silently returning [] for composite primary key ids passed as strings
マージ日: 2026/6/1 | 作成者: @55728
- 概要 (1-2文で)
Rails の複合主キーを持つモデルに対し、findに「文字列の id タプル」を複数渡した場合にレコードが1件も返らず常に[]になっていた不具合を修正する PR です。各主キー列ごとに適切な型キャストを行うことで、単一主キーと同様にパラメータ由来の文字列 id でも正しくレコードが取得されるようになります。
- 変更内容の詳細
問題となっていた挙動
対象は「複合主キー+find に複数 id タプルを渡す」ケースです。
class Book < ApplicationRecord
self.primary_key = [:author_id, :id]
end
# これは動く (整数タプル)
Book.find([[1, 10], [1, 20]]) # => [#<Book...>, #<Book...>]
# これがバグっていた (文字列タプル)
Book.find([["1", "10"], ["1", "20"]]) # => [] # 本来は 2 件返ってほしい単一主キーの場合は以前から Model.find("1") などの文字列 id を自動で型キャストしており問題ありませんでしたが、複合主キーで「2件以上の id タプル」を渡した場合だけ、例外も出さずに空配列を返すという非常に発見しづらい不具合になっていました。
バグの原因
内部的には FinderMethods#find_some_ordered が次の手順で動きます:
where(primary_key => ids)で DB から行を取得- ここでは DB 側が文字列
"1"を整数 1 に暗黙キャストするため、行自体は正しくヒットしている。
- ここでは DB 側が文字列
取得した結果を
result.in_order_of(:id, casted_ids)で指定した id の順序に並び替え・フィルタcasted_idsの生成に次のコードを使っていた:rubyids.map { |id| model.type_for_attribute(primary_key).cast(id) }
ここでの問題点は、複合主キーの primary_key が配列 ["author_id", "id"] であることです。
model.type_for_attribute(primary_key)に「配列のカラム名」を渡してしまう- ActiveRecord は配列名をカラムとして解決できず、フォールバックとして「汎用型
ActiveModel::Type::Value」を返す - この型の
castは基本的に 何もしない (no-op) ので、["1", "10"]はそのまま文字列のまま残る
- ActiveRecord は配列名をカラムとして解決できず、フォールバックとして「汎用型
結果として:
result側のレコードは DB の暗黙キャストにより[1, 10]のように 整数タプル- 並び替えに使う
series側は["1", "10"]のように 文字列タプル
Enumerable#in_order_of(:id, series) は (record.id == series_item) という形で一致比較を行いますが、[1, 10] == ["1", "10"] は false になるため、すべてのレコードがマッチせず除外され、結果が [] になっていたという流れです。
修正内容
find_some_ordered で使う主キーキャスト処理を、複合主キーを正しく扱えるように変更しています。
新たに導入されたメソッドのイメージは以下のようなものです(説明用。実際の定義名は PR を参照):
def cast_primary_key(id)
if model.composite_primary_key?
# 複合主キー: 各列ごとに型キャスト
primary_key.zip(id).map do |attr, value|
model.type_for_attribute(attr).cast(value)
end
else
# 従来挙動: 単一主キーはそのまま
model.type_for_attribute(primary_key).cast(id)
end
endポイント:
複合主キーの場合
primary_keyは[:author_id, :id]のような配列- 渡される
idは["1", "10"]のような配列 primary_key.zip(id)で[[:author_id, "1"], [:id, "10"]]の形にし、 各カラム名ごとにtype_for_attribute(attr).cast(value)を実行- 結果として
[1, 10]のように 正しい型にキャストされたタプル が得られる
単一主キーの場合
- 既存コードとバイトレベルで同じ処理を残しており、挙動変更は一切なし
これにより、in_order_of に渡される series 側も [1, 10] のように整数タプルとなり、record.id == series_item が正しく true になって、期待通りのレコード配列が返ってくるようになります。
テスト
activerecord/test/cases/finder_test.rbに、文字列の複合 id をfindに渡す回帰テストを追加- そのテストが:
- 現行
mainでは Red (バグ再現) - この修正を含むブランチでは Green
であることを、sqlite3 / PostgreSQL 両方で確認した旨が PR に記載されています。
- 現行
CHANGELOG にもこの不具合修正が追記されています。
- 影響範囲・注意点
影響を受ける条件
このバグの影響を受けるのは、次の条件が揃う場合です:
- モデルが 複合主キー (
self.primary_key = [:col1, :col2, ...]) を使っている findに 2件以上の id タプル を渡す- 例:
Book.find([["1", "10"], ["1", "20"]]) - 1件だけだと
find_one経由になり、このパスは元々問題なかった
- 例:
- 関連に 明示的な
orderを付けていない- デフォルトの
find_some_ordered経路を通る場合のみ - 逆に
order(...).find([...])のようにしていたケースは、内部で別経路を辿るため元から影響を受けていなかった
- デフォルトの
- 渡す id が DBカラムのネイティブ型と異なり、キャストが必要な値 である
- 代表例: コントローラの
params由来の文字列"1","10" - すでに整数タプル
[1, 10]にしてから渡していた場合は、以前から問題なく動作していた
- 代表例: コントローラの
実務的な注意点・確認ポイント
Rails 7.1 以降で複合主キーを使っている場合は、次のようなコードが潜んでいないか注意するとよいです:
ruby# 典型的な危険パターン(修正前は常に [] が返りうる) def index books = Book.find(params[:ids]) # params[:ids] が [["1","10"], ["1","20"]] のような形 # books が [] のまま後段ロジックに渡されると、不具合が表面化しにくい endこの PR 適用後は、上記のようなコードでも 期待通りレコードが返る ようになります。
逆に、これまで「
[]が返ることを前提にロジックを書いてしまっていた」場合は、修正適用後に挙動が変わる可能性があります。- 本来それはバグ依存の実装なので望ましくはありませんが、アップグレード時にはテストで挙動の変化がないか確認が必要です。
単一主キーの
Model.find("1")の挙動は一切変わりません。すでに整数にキャストしてから
findしていたコードも挙動は変わりません。
- 参考情報 (あれば)
- この種の「複合主キー+キャストまわりの取りこぼし」は過去にもいくつか修正されています:
find_by_token_forまわりの修正: コミット8617a7cfind_signedの修正: PR #57245
- 今回の問題は
in_order_of導入時 (9cb09411e1, 2021年) に仕込まれており、複合主キー機能(Rails 7.1)リリースによって初めて表に出てきた、という経緯があります。 - 該当コードは
find_byや Predicate Builder を通らず、既に取得済みのレコード配列に対して順序付けだけを行う経路なので、1カラムごとのtype_for_attribute(column).cast(value)を素直に使うのが最もシンプルかつ一貫した修正になっています。
#57439 Fix PostgreSQL range column schema dump producing invalid Ruby
マージ日: 2026/6/1 | 作成者: @55728
- 概要 (1-2文で)
PostgreSQL の range 型カラム(daterange,tsrange,tstzrangeなど)にデフォルト値がある場合、db:schema:dumpが Ruby としてパース不能なschema.rbを生成していた不具合を修正する PR です。range の両端値をサブタイプ(Date / Time など)のtype_cast_for_schemaを通して Ruby リテラルとして正しく出力するように変更されています。
- 変更内容の詳細
これまでの問題
OID::Range#type_cast_for_schema は、range 型のデフォルト値を schema.rb に書き出す際に、単純に Range#inspect に丸投げしていました:
def type_cast_for_schema(value)
value.inspect.gsub("Infinity", "::Float::INFINITY")
endRange#inspect は両端の値に対して inspect を呼ぶため、以下のような表現になります。
(Date.new(2024, 1, 1)...Date.new(2025, 1, 1)).inspect
# => "Mon, 01 Jan 2024...Wed, 01 Jan 2025"
(Time.utc(2024, 1, 1)...Time.utc(2025, 1, 1)).inspect
# => "2024-01-01 00:00:00 UTC...2025-01-01 00:00:00 UTC"これは人間には読みやすいものの、Ruby の式としては無効です(クォートされていない文字列扱いになる)。
結果として、例えば以下のマイグレーション:
create_table :events do |t|
t.daterange :period, default: "[2024-01-01,2025-01-01)"
endから db:schema:dump した schema.rb には
t.daterange "period", default: Mon, 01 Jan 2024...Wed, 01 Jan 2025のような行が出力され、db:schema:load / db:setup 時に SyntaxError で落ちていました。
int4range / numrange など数値系の range は整数 / Float の inspect がそのまま Ruby リテラルになるため問題は出ておらず、日付・時刻系サブタイプだけが壊れていた状況です。
新しい実装
range の両端を、サブタイプの type_cast_for_schema を通して Ruby リテラルに変換し、それらを range 演算子(.. / ...)でつなぎ直すように変更されました。
変更後のメインロジック:
def type_cast_for_schema(value)
from = bound_for_schema(value.begin)
to = bound_for_schema(value.end)
op = value.exclude_end? ? "..." : ".."
"#{from}#{op}#{to}"
end
private
def bound_for_schema(bound)
case bound
when nil
"nil"
when ::Float::INFINITY
"::Float::INFINITY"
when -::Float::INFINITY
"-::Float::INFINITY"
else
@subtype.type_cast_for_schema(bound)
end
endポイント:
@subtypeはOID::Rangeに紐づくサブタイプ(OID::Integer,OID::Float,OID::Date,OID::Timestamp,OID::TimestampWithTimeZoneなど)。- Date / Time 系の subtype ではもともと
type_cast_for_schemaがvalue.to_fs(:db).inspectのように DB 形式の文字列 +inspect(= クォート付き文字列) を返すため、
例えばdaterangeは"2024-01-01"..."2025-01-01"のような Ruby 文字列リテラルで表現されるようになる。 - 範囲の両端が以下の値のときは特別扱い:
nil(beginless / endless) →"nil"::Float::INFINITY→"::Float::INFINITY"-::Float::INFINITY→"-::Float::INFINITY"
これにより、schema.rb には次のように出力されます。
| Range (概念的な値) | 修正前 (壊れている例含む) | 修正後 (Ruby として正しい) |
|---|---|---|
1...10 (int4range) | 1...10 | 1...10(変更なし) |
1.5..2.5 (numrange) | 1.5..2.5 | 1.5..2.5(変更なし) |
(-Float::INFINITY..Float::INFINITY) (numrange) | -::Float::INFINITY..::Float::INFINITY | -::Float::INFINITY..::Float::INFINITY(変更なし) |
Date.new(2024,1,1)...Date.new(2025,1,1) (daterange) | Mon, 01 Jan 2024...Wed, 01 Jan 2025 ❌ | "2024-01-01"..."2025-01-01" ✅ |
Time.utc(2024,1,1)...Time.utc(2025,1,1) (tsrange) | 2024-01-01 00:00:00 UTC...2025-01-01 00:00:00 UTC ❌ | "2024-01-01 00:00:00"..."2025-01-01 00:00:00" ✅ |
nil..10 (beginless) | ..10(Ruby 的には有効) | nil..10(より明示的) |
1..nil (endless) | 1..(Ruby 的には有効) | 1..nil(より明示的) |
beginless / endless の表現は以前も Ruby として有効でしたが、nil を明示する形に変わっています(動作的にはほぼ等価ですが、より自己説明的なコードになります)。
互換性・ラウンドトリップ
schema.rb の "2024-01-01"..."2025-01-01" のような文字列 range リテラルは、
- Ruby の評価時には
Range<String>として解釈される - その後
OID::Range#serialize→subtype.cast→subtype.serializeを通して、OID::Date/OID::Timestamp/OID::TimestampWithTimeZoneが適切に Date / Time オブジェクトに変換
というフローを通るため、DB デフォルトとのラウンドトリップ(DB → schema dump → Ruby で評価 → serialize → DB)は動作検証済みです。
テストとしては:
OID::Range#type_cast_for_schemaの各サブタイプに対し、生成されたリテラルがRubyVM::InstructionSequence.compileで正しくコンパイルできることを確認- PostgreSQL 17 を使った end-to-end での round-trip 検証(
int4range,daterange,tsrange,tstzrange)
が追加されています。
- 影響範囲・注意点
- 直接影響を受けるケース
- PostgreSQL の range 型 (
daterange,tsrange,tstzrange) で「デフォルト値」を設定しているプロジェクト。 - これらのカラムがある状態で
db:schema:dump→db:schema:load/db:setupを実行するとSyntaxErrorが出ていたケースが解消されます。
- PostgreSQL の range 型 (
- 影響しない/ほぼ影響しないケース
- range カラムにデフォルト値を設定していない場合: 今回の不具合自体が発生していないため、挙動は変わりません。
int4range/numrangeなど数値系 range の場合: 以前から Ruby リテラルとして正しい文字列が出ており、今回も等価な出力を維持しています(Infinity 表現も同一)。
- 微妙な挙動変化
- beginless / endless range の schema 表現が
..10/1..からnil..10/1..nilに変わりますが、Ruby としてはどちらも有効で、range の意味もほぼ同じです(nil自体はサブタイプでキャストされる段階で無限側として処理される)。
- beginless / endless range の schema 表現が
- 移行時の注意
- 既存の
schema.rbを手で編集して回避していた場合(例: 自前でクォートを追加していたなど)、Rails を更新してからdb:schema:dumpし直すと、より機械的な"YYYY-MM-DD"形式に揃えられるため、diff が大きめに出る可能性はあります。 - schema から再生成された range のデフォルト値が、DB 側に設定されているものと一致しているか、気になる場合は一度
db:schema:load→ 実テーブル定義の確認をすると安心です。
- 既存の
- 参考情報 (あれば)
- 対象クラス:
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Range
- 関連ファイル:
activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rbactiverecord/test/cases/adapters/postgresql/range_test.rbactiverecord/CHANGELOG.md(今回の挙動変更が記載)
- 不具合の性質:
- range 型サブタイプ(特に Date / Time 系)の
inspectに依存していたために起きた、「人間可読だが Ruby として無効」な schema 出力が原因で、db:schema:load時に即座に発覚するタイプのバグです。 - 今回の修正で、range も他の型(
date,timestampなど)と同様に、サブタイプ自身のtype_cast_for_schemaを尊重する設計に揃えられました。
- range 型サブタイプ(特に Date / Time 系)の
#57403 Update Active Storage for ImageProcessing 2.0
マージ日: 2026/6/1 | 作成者: @janko
- 概要 (1-2文で)
ImageProcessing 2.0 のリリースに合わせて、Active Storage の依存関係・設定・ドキュメントを更新した PRです。v2.0 での破壊的変更(明示的な gem 依存と libvips による「危険フォーマット」のブロック)に追随しています。
- 変更内容の詳細
2-1. ImageProcessing の利用方法の更新
ポイント:
ImageProcessing 2.0 からは、内部で mini_magick や ruby-vips を自動ロードしなくなったため、アプリケーション側で明示的に Gem を追加する必要があります。
このPRで行っている主な変更:
- Gemfile / アプリケーションジェネレータの更新
rails new時に生成されるGemfileテンプレート (Gemfile.tt) が更新され、ImageProcessing を使う場合に必要な設定が反映されるようになりました。- 実際の Gemfile にも ImageProcessing 2.0 系向けの記述が反映されています。
今後推奨される構成イメージ(例):
# Gemfile
gem "image_processing", "~> 2.0"
# 利用するバックエンドを明示的に指定 (どちらか/両方)
gem "mini_magick" # ImageMagick ベース
gem "ruby-vips" # libvips ベースActive Storage の ImageProcessing ベースの variant 変換は、上記のようにサポートライブラリを別途入れておく前提で動作するようになります。
2-2. LoadError への対応強化
ImageProcessing 2.0 では、mini_magick や ruby-vips が Gemfile に無い場合、ロード時に LoadError が発生しやすくなります。
この PR では:
- Active Storage の ImageProcessing 関連コード(
image_processing_transformerなど)において、mini_magick・ruby-vipsがロードできない場合のLoadError分岐を追加。 - これにより、対象の gem が入っていない状態で variant 処理をしようとしたときに、より意図した形で例外ハンドリングができるようになります(Rails 側で明示的に扱える)。
コードイメージ(概念的な例):
begin
require "ruby-vips"
rescue LoadError
# 適切なエラーメッセージや fallback を提供できるような分岐
end2-3. libvips 8.13+ における「危険フォーマット」ブロックへの対応
背景:
- libvips 8.13+ では、既定で「untrusted / unfuzzed loader」とされるフォーマット(安全性が十分でないフォーマット)をブロックするようになりました。
- これには BMP, PSD, ICO などが含まれます。
- 一方で、Active Storage 側ではこれらを「variable content types(バリアント生成を許可するコンテンツタイプ)」として受け付けています。
この PR での対応:
activestorage/lib/active_storage/transformers/vips.rbに変更が入り、libvips 側の設定により特定フォーマットがブロックされる可能性を意識した実装になっています。- PR 本文にもある通り、「Active Storage 上では許可されているが libvips 側ではブロックされるフォーマット」をどう扱うのがベストかはまだ検討中のトピックとして明示されています。
開発者としては、BMP/PSD/ICO の変換が vips バックエンドで突然失敗する可能性がある点を意識する必要があります。
2-4. Active Storage エンジン・モデル周りの調整
activestorage/lib/active_storage/engine.rb- Active Storage エンジンの初期化処理の中で、ImageProcessing / vips / mini_magick のロードとエラーハンドリングロジックが更新されています。
- Rails 起動時に、利用可能な画像処理バックエンドがどうあるべきかをより明確に扱うようになっています。
activestorage/app/models/active_storage/variant.rb- ImageProcessing 2.0 を前提とした微調整(呼び出し側のパラメータやデフォルト挙動の整合)が行われています。
activestorage/lib/active_storage/transformers/image_processing_transformer.rb- ImageProcessing への委譲コードを 2.0 に合わせて修正。
2-5. ドキュメント・CHANGELOG・テストの更新
activestorage/CHANGELOG.md- ImageProcessing 2.0 対応に関する変更点を明記。
guides/source/active_storage_overview.md- ImageProcessing のセットアップ方法(Gemfile に
ruby-vipsやmini_magickを明示的に追加する必要があることなど)をガイドに追記。
- ImageProcessing のセットアップ方法(Gemfile に
railties/test/...- 新しい Gemfile テンプレート・Active Storage 設定に即したテストへ更新(
engine_integration_test.rb,app_generator_test.rb)。
- 新しい Gemfile テンプレート・Active Storage 設定に即したテストへ更新(
- 影響範囲・注意点
影響範囲:
- Active Storage で ImageProcessing を使って画像バリアントを生成しているすべてのアプリケーション。
- 特に、以下のようなアプリは影響が大きいです:
image_processingだけ Gemfile に書いており、mini_magickやruby-vipsを明示的に追加していない- vips ベースで BMP/PSD/ICO などを扱っている
注意点・必要な対応:
Gemfile の見直し
- 画像処理バックエンドを明示的に追加してください:ruby
gem "image_processing", "~> 2.0" # 使用するバックエンドに応じて gem "mini_magick" # ImageMagick を利用するなら gem "ruby-vips" # libvips を利用するなら - どちらも入れておくことも可能ですが、その場合どちらを使うかは Active Storage 設定やコード側の指定に依存します。
- 画像処理バックエンドを明示的に追加してください:
BMP/PSD/ICO 等の扱い
- libvips 8.13+ 環境で
ruby-vipsを使っている場合、これらのフォーマットは標準設定だとブロックされる可能性があります。 - 対策案:
- そもそもこれらのフォーマットでバリアント(リサイズ・サムネイル)を作らないようにする
- ImageProcessing のバックエンドに
mini_magickを使うように切り替える(ImageMagick 側の設定に依存) - libvips の設定を調整して対象フォーマットを許可する(ただしセキュリティリスクを十分検討する必要あり)
- libvips 8.13+ 環境で
例外ハンドリング
LoadErrorに対して Active Storage 側でハンドリングが追加されたとはいえ、 実運用では「必要な Gem が入っていない」「危険フォーマットで vips が失敗する」ケースに対する アプリ側のエラーメッセージやフォールバック処理を検討するのが望ましいです。
- 参考情報 (あれば)
ImageProcessing 2.0 CHANGELOG
https://github.com/janko/image_processing/blob/master/CHANGELOG.md#200-2026-05-20libvips 8.13 の「untrusted formats」について
https://www.libvips.org/2022/05/28/What's-new-in-8.13.html#blocking-of-unfuzzed-loadersActive Storage Overview (Rails Guides, 該当部分がこの PR で更新)
https://guides.rubyonrails.org/active_storage_overview.html
#57004 Reimplement RedisCache store using redis-client
マージ日: 2026/6/1 | 作成者: @byroot
- 概要 (1-2文で)
Redis ベースのキャッシュストアRedisCacheStoreを、従来のredisgem ではなく新しいredis-clientを使って作り直した PRです。既存の設定・APIとの互換性を確保するため、旧実装はDeprecatedRedisCacheStoreとして残しつつ、RESP2 プロトコルを使うことで Redis サーバの要件は従来から変えない形になっています。
- 変更内容の詳細
全体方針
ActiveSupport::Cache::RedisCacheStoreの内部実装をredisgem 依存からredis-client依存に差し替え。redis:オプションなど、これまでの設定インターフェイスを壊さないために、旧実装をActiveSupport::Cache::DeprecatedRedisCacheStoreとして分離・温存。redis-clientは RESP2 を使うように設定されており、Redis サーバ側のバージョン要件を増やさない設計。
主なコードレベルの変更点
1) 新しい RedisCacheStore の実装切り替え
activesupport/lib/active_support/cache/redis_cache_store.rb が大きく書き換えられています。
ポイント:
- 依存ライブラリが
redis→redis-clientに変更。 - シャーディング(従来の
Redis::Distributed相当)について、redis-client側にRedisClient.ringを実装して対応。- これにより、複数 Redis インスタンスへの分散キャッシュも継続してサポート。
- 基本的なキャッシュ API (
read,write,fetch,delete,increment,decrement, マルチキー操作など) の挙動は、可能な限り従来と互換になるよう維持。
使用イメージ(概念的には従来と同じ):
Rails.application.config.cache_store = :redis_cache_store, {
url: "redis://localhost:6379/0",
# 他オプションも基本同じ感覚で利用可能
}複数ノードへの分散(リング)も、内部では RedisClient.ring によって実現される想定です。
2) 旧実装を DeprecatedRedisCacheStore として退避
activesupport/lib/active_support/cache/deprecated_redis_cache_store.rb が新規追加され、約 500 行強の旧実装が移植されています。
- 役割:
- 以前の
redisgem ベース実装を名前を変えて保持。 redis:オプション等で直接 Redis クライアントインスタンスを渡しているような「高度/裏技的」なユースケースへの移行期間を確保。
- 以前の
- 実質的には、
- 旧
ActiveSupport::Cache::RedisCacheStore= 現ActiveSupport::Cache::DeprecatedRedisCacheStore - 新
ActiveSupport::Cache::RedisCacheStore=redis-clientベースの再実装 という状態。
- 旧
3) redis: オプションの扱い
PR 説明にもある通り、当初は :redis コンストラクタ引数を削除しようとしたものの、Redis Cluster クライアントなどを直接渡すユーザーの存在を考慮して、完全な削除は見送られています。
- つまり、「
redis:を使ってクライアントを直渡しするパターン」を完全に壊さないように配慮した設計。 - 実際のマイグレーションパスとしては、
DeprecatedRedisCacheStoreを利用する/移行期間を設ける形と思われる(詳細は今後のガイドや CHANGELOG で明示される見込み)。
4) Gemfile の更新
Gemfile/Gemfile.lockにredis-clientが追加され、redisからの移行が反映されています。- 依存関係の更新により、Rails 自身が
redis-clientを公式・標準の Redis クライアントとして扱う流れが明確になった形です。
5) テストの分離と更新
activesupport/test/cache/stores/redis_cache_store_test.rbがredis-clientベース実装向けに大きく変更。activesupport/test/cache/stores/deprecated_redis_cache_store_test.rbが新規に追加され、旧実装専用のテストとして 600 行超が移植。- 振る舞い共通部分のテスト (
cache_instrumentation_behavior,failure_raising_behaviorなど) は、新旧両ストアで動くように微修正。
これにより、
- 新実装の互換性保証(従来テストが落ちないか)
- 旧実装の「後方互換」の担保 の両方を CI で継続的に確認できるようになっています。
6) ドキュメント更新
activesupport/CHANGELOG.mdに今回の変更が追加され、RedisCacheStoreがredis-clientベースになったこと、旧実装がDeprecatedRedisCacheStoreとして残ることなどが明記されているはずです。
- 影響範囲・注意点
影響範囲
Rails のデフォルト/推奨 Redis キャッシュクライアントが
redis→redis-clientに変更- キャッシュ用途で Redis を使っている Rails アプリ全般に影響。
- ただし、基本 API やサーバ要件(RESP2)は変えていないため、ほとんどのアプリは「内部実装が変わるだけ」で済む想定。
redis:オプションでクライアントインスタンスを直接渡しているケース- 旧来の
Redisクライアント(redisgem のRedisオブジェクトやRedis::Clusterなど)を直接注入しているコードは要注意。 - そうしたケースのために
DeprecatedRedisCacheStoreが用意されているが、- 将来的には廃止される可能性が高い
- どのタイミングで何が deprecated になるかは CHANGELOG / ガイドを要確認。
- 旧来の
シャーディング/分散構成 (
Redis::Distributed相当)- 複数 Redis インスタンスに分散してキャッシュする構成は、
RedisClient.ringで実装される。 - キー分散アルゴリズムやフェイルオーバー挙動など、
Redis::Distributed依存ロジックを書いていた場合は、細かい差異がないかを確認したほうがよい。
- 複数 Redis インスタンスに分散してキャッシュする構成は、
開発者視点での注意点・確認ポイント
- Rails をアップグレードしたら、まず Redis キャッシュが正常動作しているか確認
fetch,write,delete,increment/decrement, マルチキー操作など、アプリが多用している操作を一通り叩いてみる。
- もし直接 Redis クライアントを触っているコードがあれば要チェック
- 例:
Rails.cache.redisを前提にRedisのメソッドを呼び出すようなコード。 RedisCacheStoreの内部実装がredis-clientに変わったことで、返されるオブジェクトや利用を想定していないメソッド呼び出しが壊れる可能性がある。
- 例:
- Redis Cluster / Sentinel / シャーディング構成を利用している場合
- 接続設定の記述方法・サポート状況が
redis-clientでどうなっているかを確認(redis-clientの README / Rails ガイドを参照)。 - 特にクラスタリング特有のコマンド (
READONLY,ASK,MOVEDハンドリングなど) を期待していた場合は差分に注意。
- 接続設定の記述方法・サポート状況が
- 参考情報 (あれば)
redis-client本体のドキュメント
https://github.com/redis-rb/redis-client旧
redisgem
https://github.com/redis/redis-rbRails ガイド(キャッシュストア / Redis)
まだ反映前の可能性がありますが、今後ここにredis-clientベースの記述が追加される想定です:
https://guides.rubyonrails.org/caching_with_rails.htmlこの PR の CHANGELOG エントリ
activesupport/CHANGELOG.md内のRedisCacheStore/redis-clientに関する節を参照すると、公式にサポートされる移行パス・非推奨事項が見やすくまとめられています。
#57533 Reimplement Action Cable redis adapter with redis-client
マージ日: 2026/6/1 | 作成者: @byroot
- 概要 (1-2文で)
Action Cable の Redis アダプタ実装を、従来のredisgem ベースからredis-clientベースに書き換えた PRです。これにより、Rails が巨大なredisgem へ依存せずに済むようになり、購読(Pub/Sub)まわりの実装もシンプルになっています。
- 変更内容の詳細
※PR本文・diff から読み取れる範囲に基づく解説です(ファイルパスと行数規模からの構造的推測も含みます)。
a. Redis アダプタの内部実装を redis-client に差し替え
対象ファイル:
actioncable/lib/action_cable/subscription_adapter/redis.rb(+39 / -85)
このファイルは Action Cable の Redis ベースの Subscription Adapter 実装です。ここで、以下のような切り替えが行われています。
以前:
require "redis"などでredisgem を直接利用Redis.new,subscribe,psubscribe,unsubscribeなど、redis gem 固有の API を利用して Pub/Sub を実装- コネクションプール周りも
redisgem のオブジェクトを前提とした作り
変更後:
redis-clientを用いた接続確立・購読・メッセージ受信ロジックに置き換えredis-clientの Subscription API(ブロッキングな購読ループやコールバック)に沿った実装に再構成redisgem に依存していた箇所(エラークラスや返り値の型など)をredis-clientのインターフェイスに対応するよう修正
redis-client は Redis 7 系を意識した軽量クライアントで、Pub/Sub 用の API が redis gem よりもシンプルかつストレートな設計になっており、Action Cable 側のコードもそれに合わせて大きく簡素化されています(行数が 85 行削除 / 39 行追加という差分になっているのは、その簡素化の結果と考えられます)。
b. テストコードの更新
対象ファイル:
actioncable/test/subscription_adapter/redis_test.rb(+11 / -9)
Action Cable の Redis アダプタ向けテストが、redis gem 前提の挙動から redis-client 前提に変更されています。
具体的には、例えば:
- 接続確立時のオブジェクト型やエラークラスの変更
- Pub/Sub の購読開始・停止、再接続動作などのテストケースが新しい API を使うように微調整
redis-client由来のタイミング/スレッド挙動の差異を考慮したアサーションの調整
が入っていると考えられます。テスト全体としては大きく増減しておらず (+11 / -9)、主にインターフェイス差分の吸収といったレベルの修正です。
c. #57004 との組み合わせによる依存関係変更
PR 本文で、以下のように言及されています:
Combined with https://github.com/rails/rails/pull/57004 this would allow to not longer depend on the much larger
redisgem
つまり、この PR 単体ではなく、#57004 とセットで見ると:
- Rails 全体として
redisgem を Gem dependency から外せる(または optional にできる) - Redis 関連の機能はすべて
redis-clientでまかなえるようになる
という整理が進んでいることがわかります。
#57004 側では、おそらく他の Redis 利用箇所(キャッシュストアや Action Cable 以外のアダプタ等)が redis-client に置き換えられているか、あるいは共通接続ロジックが導入されているはずです。
- 影響範囲・注意点
a. Rails ユーザ側(アプリケーション開発者)の影響
1) Gem 依存の変化
- Action Cable で Redis を使う場合、今後は
redis-clientが前提になります。 - これにより:
redisgem をわざわざ Gemfile に入れなくてもよくなる(将来的には削除推奨)- 逆に、
redis-clientが必要(Rails が依存として引き込む形になる想定)
アプリ側が Action Cable 用に redis gem のカスタム設定などをしていた場合、それらは無効になり、redis-client の設定方法へ移行する必要があります。
2) 設定値・接続オプションの見直し
redis gem と redis-client では、細かい接続オプション名や挙動が違う場合があります。たとえば:
- タイムアウト系オプション名
- コネクションプールの扱い
- reconnect/retry 動作のデフォルト
など。Action Cable の Redis アダプタ設定(config/cable.yml の url, channel_prefix, redis オプションなど)で、redis 特有のパラメータを渡している場合は、そのままでは効かなくなる可能性があります。
ただし、この PR は主に内部実装の差し替えなので、高レベルな設定インターフェイス(url や channel_prefix など)はできるだけ後方互換になっていると想定されます。特殊な設定をしていない典型的なアプリでは、そのまま動く可能性が高いです。
3) 例外クラス・ログ出力などの変更
- Redis 関連の例外クラスが
Redis::BaseErrorなどからRedisClient::Error的なものに変わる可能性があります。- 例外クラス名で rescue している場合は影響が出ます。
- 接続/購読のログメッセージ、警告メッセージが変わる可能性があります(監視やログ解析している場合は要確認)。
b. ライブラリ作者・メンテナの影響
Action Cable の Redis アダプタをラップしたり、拡張している gem・ライブラリは、redis gem に強く結合している部分があれば修正が必要になります。
- 具体例:
ActionCable::SubscriptionAdapter::Redisのインスタンスに対してinstance_variable_getなどで内部のRedisインスタンスを参照しているRedisの接続オブジェクトを前提とした monkey patch を当てている- Redis アダプタの初期化時に
Redis.currentなどの仕組みと連携させている
このような場合は、redis-client のクラスや API にあわせて実装を見直す必要があります。
c. 運用面の注意
redis-clientは比較的新しめのクライアントなので、運用中の Redis バージョンとの互換性を事前に確認することが推奨されます(特に古い Redis との組み合わせ)。- 接続数・サブスクライバ数が多い大規模環境では、
redisgem とredis-clientのパフォーマンス・メモリ使用量・再接続挙動の違いが出る可能性があります。- 本番切り替え前にステージング環境で負荷テスト・フェイルオーバーテストをしておくと安全です。
- 参考情報 (あれば)
- この PR:
- 言及されている関連 PR (#57004):
redis-clientの GitHub:- 旧
redisgem:
Action Cable の Redis アダプタまわりをカスタマイズしている場合や、Redis 連携を独自にラップしている場合は、上記 PR と redis-client の README をあわせて確認しつつ、移行方針を検討するのが良いです。
#57528 Document ports can be added to hosts in `config.hosts' [ci-skip]
マージ日: 2026/6/1 | 作成者: @p8
概要 (1-2文で)
このPRは、Rails のconfig.hostsにホスト名と一緒に「ポート番号も指定できる」ことをガイドに追記した、ドキュメント専用の変更です。コードの挙動自体は変わらず、既存機能の説明を補完するものです。変更内容の詳細 (サンプルコード含む)
変更ファイルは
guides/source/configuring.mdのみで、Rails アプリケーション設定ガイドの「config.hostsの説明」に数行の記述が追加されています。追加された内容はだいたい以下のような趣旨です(擬似的な抜粋・イメージ):
md### config.hosts # 中略: もともとの説明(許可ホストの設定など) ホスト名だけでなく、ポート番号を含めて指定することもできます: ```ruby # 例: 特定ポートのみ許可する config.hosts << "example.com:3000" config.hosts << "localhost:3001"これは、同じホスト名でもポートごとにアクセス可否を分けたい場合に有用です。
もともと Rails 本体は
"example.com:3000"のような文字列をホストとして扱うことができており(ActionDispatch::HostAuthorizationのマッチ時にホストヘッダ全体を評価)、このPRでは「その事実が公式ドキュメントに明記された」という位置付けです。[ci-skip]がタイトルに付いている通り、テストを伴わない単なるドキュメント変更として扱われています。
- 影響範囲・注意点
- 影響範囲:
- ランタイムの挙動変更や API 変更はありません。
config.hostsの既存仕様のうち、「ポートを含められる」という点がドキュメントで明文化されたのみです。 - これにより、「ホスト名だけしか指定できない」と誤解していた開発者が、ポート単位でのホワイトリスト制御を正しく設定できるようになります。
- ランタイムの挙動変更や API 変更はありません。
- 注意点:
- 実際の
Hostヘッダの値(および Rack/サーバがアプリに渡すrequest.host_with_portなど)と、config.hostsに書いた値の形式を合わせる必要があります。- 例: 開発で
http://localhost:3000にアクセスするなら、config.hosts << "localhost:3000"のようにポート付きで書く。
- 例: 開発で
- 逆に、ポートを省いた
"example.com"を設定した場合は、「任意のポートでのexample.comへのアクセスを許可する」という扱いになる実装/バージョンもあるため、厳密にポートを絞りたい場合は必ずポート込みで指定することが推奨されます。 - 一部のプロキシやコンテナ環境では、アプリケーション側から見える
Hostヘッダのポートが、クライアントからのポートと異なることがあるため、実環境でどの値が来るかをログ等で確認した上でconfig.hostsを設定するのが安全です。
- 実際の
- 参考情報 (あれば)
- 対応する機能:
ActionDispatch::HostAuthorizationミドルウェア (config.hosts設定に基づいてリクエストを許可/拒否する機能) - 関連ドキュメント(英語版・最新版を参照するとよい部分):
- Rails Guides: Configuring Rails Applications – 「
config.hosts」セクション
- Rails Guides: Configuring Rails Applications – 「
- 実際の活用例:
- docker-compose などで複数コンテナを異なるポートでローカル公開しており、特定のポートに対してのみ Rails アプリがリクエストを受け付けるようにしたい場合に、
config.hosts << "myapp.local:8080"のように設定できることが、今回のドキュメント変更で分かりやすくなります。
- docker-compose などで複数コンテナを異なるポートでローカル公開しており、特定のポートに対してのみ Rails アプリがリクエストを受け付けるようにしたい場合に、
#57503 Reject malformed hosts with extra ports
マージ日: 2026/6/1 | 作成者: @afurm
- 概要 (1-2文で)
HostAuthorization ミドルウェアが「許可ホストにポートを含めて設定した場合」にも誤って「追加の任意ポート」を許容してしまう問題(例:www.example.com:80:80が通ってしまう)を修正する PR です。ポートを含む許可ホストに対しては、余分なポートを受け入れないよう正規表現生成ロジックを変更し、対応するテストも追加しています。
- 変更内容の詳細
問題の背景
config.hosts や HostAuthorization ミドルウェアにおいて、許可ホストにポート番号を含めた値(例: "www.example.com:80")を指定したとき、内部ではそのホスト名から許可判定用の正規表現が生成されます。
従来の実装では:
- 「ポートなし」のホスト(例:
"www.example.com")→:80など任意のポートを許可するため、正規表現の末尾に「任意ポートのオプション部分」が付与される((?:\:\d+)?のようなイメージ)。 - ところが、「ポートあり」のホスト(例:
"www.example.com:80")に対しても、同様に「もう一個オプションのポート」を付けてしまっていた。
その結果:
- 許可リスト:
"www.example.com:80" - 実際の Host ヘッダ:
"www.example.com:80:80"
という不正な値でも、生成された正規表現にマッチしてしまい、HostAuthorization を通過 → その後段でリダイレクト処理などが「正しい Host 値が来る前提」で動いてしまう、というバグが発生していました。
この PR の修正内容
修正のポイントは「許可ホストにポートが含まれているかどうかで、生成する正規表現を分ける」ことです。
ホストが「ポートなし」の場合
- 従来どおり
example.com→^example\.com(?::\d+)?$のように、末尾に「任意ポートのオプション」を付けて、:3000など任意のポート付き Host を許可する。
- 従来どおり
ホストが「ポートあり」の場合(今回の修正箇所)
- 例:
example.com:80 - 既にポートが明示されているので、その後ろに「さらにもう一つ任意ポート」を追加しない。
- つまり
example.com:80なら、^example\.com:80$のように「ピッタリ一致」だけを許可する正規表現を生成するように変更。
- 例:
コード的には、ActionDispatch::HostAuthorization 内で許可ホストを正規表現に変換しているメソッド(compile_hosts かその周辺)において:
hostに:(ポート指定)が含まれているかどうかを判定- 含まれている場合は
(?::\d+)?のような「オプションのポート部分」を付けない - 含まれていない場合は従来どおり「オプションのポート部分」を付ける
という条件分岐が入った形になります。
追加されたテスト
actionpack/test/dispatch/host_authorization_test.rb に以下のような観点のテストが追加されています(内容のイメージ):
「明示的なポート付きホスト」が許可されること
- 設定:
config.hosts << "www.example.com:80" - Host:
"www.example.com:80"→ 許可される(ミドルウェアでブロックされない)。
- 設定:
「余分なポートを含むホスト」が拒否されること
- 同じ設定のもとで、Host:
"www.example.com:80:80"→ 許可されない(ミドルウェアで 403 などが返る)。
- 同じ設定のもとで、Host:
既存挙動(ポートなしホストに対して任意ポートを許容する挙動)が壊れていないこと
- 設定:
config.hosts << "www.example.com" - Host:
"www.example.com:3000"→ 従来通り許可される。
- 設定:
テストコマンド例として PR に記載されているもの:
cd actionpack && bin/test test/dispatch/host_authorization_test.rb -i "/extra ports/"
cd actionpack && bin/test test/dispatch/host_authorization_test.rb
cd actionpack && bundle exec rubocop lib/action_dispatch/middleware/host_authorization.rb test/dispatch/host_authorization_test.rb
git diff --check- 影響範囲・注意点
影響対象:
ActionDispatch::HostAuthorizationミドルウェアを利用している Rails アプリ(Rails 6 以降など)で、config.hostsにポート付きのホストを列挙しているケース。- 例:ruby
# config/environments/production.rb config.hosts << "example.com:443" config.hosts << "internal.example.com:3000"
この変更により起こる挙動の変化:
- これまでは(バグにより)
"example.com:443:443"のような不正な Host 値が通っていた可能性があるが、今後は 確実にブロック される。 - 正しい Host 値(
example.com:443)は従来同様に許可されるため、通常の正しいクライアントには影響しない。
- これまでは(バグにより)
注意点:
- 万が一、CDN / リバースプロキシ / 独自ミドルウェアなどが誤って
Hostヘッダを二重ポートのような形で付与していた場合、この変更適用後に 403 などでリクエストが落ちる可能性がある。- ただし、
Host: example.com:80:80といった形自体が RFC 的にもおかしい値なので、基本的には「バグを表面化させる」方向の改善と言える。
- ただし、
- 「ポートなしホストに対して、任意ポートを許可する挙動」は維持されているため、
config.hosts << "example.com"のような設定に関しては意図した挙動はそのままです。
- 万が一、CDN / リバースプロキシ / 独自ミドルウェアなどが誤って
- 参考情報 (あれば)
- 関連 issue: #37956
- 変更ファイル:
actionpack/lib/action_dispatch/middleware/host_authorization.rb- 許可ホストを正規表現に変換するロジックの条件分岐追加(ポート有無で「オプションポート」の扱いを変える)
actionpack/test/dispatch/host_authorization_test.rb- 明示ポートホスト・余分なポートを持つホストの挙動を検証するテストを追加
この変更により、Host ヘッダの検証がより厳密かつ安全になり、下流のリダイレクトや URL 生成ロジックが「正しい Host が来る」という前提で動きやすくなります。
#57526 Revert adding ractor helpers in Kernel
マージ日: 2026/6/1 | 作成者: @byroot
- 概要 (1-2文で)
このPRは、Ractor 関連のヘルパーメソッドをKernelに追加して Rails のパブリック API にする方針を撤回し、代わりに内部モジュール (ActiveSupport::Ractors) として提供するように変更しています。目的は、Ruby の古いバージョン互換のためだけに必要なヘルパーを「永久に維持すべきパブリック API」にしないようにすることです。
- 変更内容の詳細
背景
- 元の PR (#57467) では、Ractor 対応のために以下のようなヘルパーを
Kernelに追加し、Rails から利用しやすくしていました(例:ractor_shareable?やmake_ractor_shareableのようなもの)。 - しかし
Kernelにメソッドを追加すると、「Rails のパブリック API」とみなされ、将来的に不要になっても簡単には削除できません。 - これらのヘルパーは「古い Ruby で Ractor 関係の API が不足しているのを埋める」ための穴埋めであり、Ruby のサポートバージョンが上がれば不要になる見込みです。
主な変更点
1) Kernel 拡張の削除
削除されたファイル:
activesupport/lib/active_support/core_ext/kernel/ractor_shareability.rb
→Kernelに Ractor 関連のヘルパーを生やしていた拡張を丸ごと削除しています。
修正されたファイル:
activesupport/lib/active_support/core_ext/kernel.rb
→ 上記ファイルをrequireしていた行を削除。guides/source/active_support_core_extensions.md
→ Kernel 拡張としての Ractor ヘルパーに関するガイド記述を削除。
これにより、Kernel#xxx として Ractor ヘルパーを呼ぶ API は廃止されます(そもそもまだリリースされていない段階で戻した形)。
2) 内部モジュール ActiveSupport::Ractors の追加
新規ファイル:
activesupport/lib/active_support/ractors.rb(+69行)
ここに、元々 Kernel に定義していた Ractor ヘルパー群が「内部 util モジュール」として再定義されています。
Rails 内部や gem から使う場合は、以下のような形になることが想定されます:
# 内部的な利用イメージ(実際のメソッド名は元PR依存ですが概ねこんな感じ)
require "active_support/ractors"
if defined?(Ractor)
ActiveSupport::Ractors.shareable?(obj)
ActiveSupport::Ractors.make_shareable(obj)
endbyroot のコメントにもある通り、このモジュールは
- 単にモジュール関数(
module_function)として定義するか - singleton メソッドとして使うか
といった設計も含め、「内部 API をどのような形で提供するか」を議論可能な範囲に閉じ込める意図があります。
3) ActiveSupport 本体からの読み込み
activesupport/lib/active_support.rb
→require "active_support/ractors"が追加されました。
これにより、require "active_support" した環境から ActiveSupport::Ractors が利用できるようになります(あくまで内部 API の位置づけ)。
4) Changelog とドキュメントの整理
activesupport/CHANGELOG.md(-16行)
→ 前の PR (#57467) の内容として記載されていた、「Kernel に Ractor ヘルパーを追加した」というエントリが削除。guides/source/active_support_core_extensions.md(-40行)
→ Kernel 拡張としての Ractor 関連記述を削除。
つまり、「ユーザー向けの新機能として Kernel 拡張を追加した」という扱いそのものを取り消しています。
5) テストの更新
activesupport/test/ractors_test.rb(+12/-13)
→ これまでKernelに生えたメソッドを前提にしていたテストを、ActiveSupport::Ractorsを使う形に修正しています。
テスト対象が「パブリック Kernel API」から「内部 ActiveSupport モジュール」に変わったイメージです。
影響範囲・注意点
Rails アプリ/gem からの直接利用は想定していない
- そもそも Kernel 拡張がまだリリース前に revert された形なので、「既存コードが壊れる」という互換性問題は基本的に発生しない想定です。
- 新たに Ractor 向けヘルパーを使いたい場合も、Rails の「公認パブリック API」とは位置付けられていない
ActiveSupport::Ractorsに依存することになるため、将来的な変更・削除のリスクがあります。
Kernel にメソッドは増えない
- この PR の結果として、
Kernelに Ractor 関連のメソッドは追加されません。 - Ractor 周りで Ruby 本体と Rails が混ざった API になることを避け、「Rails 起因でグローバル名前空間を汚染しない」方針が保たれています。
- この PR の結果として、
将来的な削除を見越した設計
- 元コメントにある通り、Ruby のサポートバージョンが上がり、Ractor 関連の機能が標準で十分に提供されるようになれば、
ActiveSupport::Ractors自体が不要になり、内部実装・互換レイヤーとして削除できる余地が残されます。 - パブリック API にしてしまうと「古い Ruby のためだけのラッパー」を半永久的に維持しなければならないため、それを避けています。
- 元コメントにある通り、Ruby のサポートバージョンが上がり、Ractor 関連の機能が標準で十分に提供されるようになれば、
Ractor を積極利用するライブラリ作者への注意
- Rails の Ractor サポート状況を調べる際、「Kernel に Ractor 関連ヘルパーがある」という情報は誤りになったので注意が必要です。
- もし Rails 内部と同じヘルパーを使いたい場合は
ActiveSupport::Ractorsを読む形になりますが、それは互換保証のない内部 API です。
長期的に安定した API を求めるなら、Ruby 本体のRactorAPI を直接使用する方が安全です。
- 参考情報 (あれば)
- 元 PR(追加を行った側):
https://github.com/rails/rails/pull/57467 - この PR 本体:
https://github.com/rails/rails/pull/57526
設計上のポイント:
- 「互換性確保のための一時的な補助を、Kernel やパブリック API に乗せない」
- 「内部モジュール化して、Ruby バージョン戦略に応じて将来的に削除しやすくする」
という Rails の API スタビリティポリシーがよく表れている変更です。
#57529 Fix syntax for Rack response in Rack guide (#57527) [ci-skip]
マージ日: 2026/6/1 | 作成者: @p8
概要 (1-2文で)
Rails ガイド「Rails on Rack」に記載されている Rack レスポンスのサンプルコードの構文ミスを、正しい Rack 仕様に沿う形で修正したドキュメント修正PRです。コードや挙動には一切変更がなく、ガイドの記述のみが更新されています。変更内容の詳細
- 対象ファイル:
guides/source/rails_on_rack.md - 変更内容は 1 行のみで、Rack アプリケーションの返り値(Rack レスポンス)の書き方を正しい構文に修正しています。
Rack のアプリケーションは以下の形式の 3 要素配列を返す必要があります:
# [ステータスコード, ヘッダ(Hash), ボディ(各要素が文字列の Enumerable)]
[status, headers, body]ガイド内のサンプルでは、この 3 要素目(body)か、もしくは全体の書式が Rack 仕様に即していない形になっていたため、例えば次のような形に修正されていると考えられます:
修正前(誤った例・イメージ):
# body を単なる文字列で返している / または配列ではない、など
[200, { "Content-Type" => "text/html" }, "Hello, world!"]修正後(正しい Rack レスポンスの例):
[200, { "Content-Type" => "text/html" }, ["Hello, world!"]]あるいは単純なシンタックスエラー(カンマやカッコの抜け、シンボルの書き方など)があれば、それが修正されています。
いずれにせよ、修正内容は 「Rack アプリの返り値サンプルを、Rack が要求する [status, headers, body] 形式として正しく書き直した」 ものです。
- 影響範囲・注意点
影響範囲:
- Rails 本体・Rack インテグレーションの実装・挙動には影響しません。
- ドキュメント(Rails ガイド)を見て Rack アプリケーションを書く開発者が、誤ったレスポンス形式を参考にしてしまうリスクが減ります。
注意点:
- 既に誤ったサンプルをもとにアプリを書いていた場合、Rack のインターフェース仕様(body は
eachできる文字列の配列などである必要がある)を改めて確認するのがよいです。 - PR タイトルに
[ci-skip]が付いている通り、コード変更がないため CI はスキップされています。
- 既に誤ったサンプルをもとにアプリを書いていた場合、Rack のインターフェース仕様(body は
- 参考情報 (あれば)
- Rack SPEC(Rack アプリの返り値仕様)
- https://github.com/rack/rack/blob/main/SPEC.rdoc
- 「The Body must respond to
eachand must only yield String values.」など、レスポンス形式の要件が詳述されています。
- Rails ガイド: Rails on Rack
- 最新版の
rails_on_rack.mdを参照すると、今回修正された正しいサンプルコードを確認できます。
- 最新版の
#57527 Fix syntax for Rack response in Rack guide
マージ日: 2026/6/1 | 作成者: @ayushn21
- 概要 (1-2文で)
Railsガイド「Rails on Rack」で、Rackレスポンスの書き方に誤りがあったため、正しいRack::Responseオブジェクトを使う形に修正したドキュメント更新のPRです。コードの挙動ではなく、ガイドのサンプルコードの記述ミスを直しています。
- 変更内容の詳細 (サンプルコード例)
- 対象:
guides/source/rails_on_rack.mdの Rack レスポンスに関するサンプルコード - 内容: Rack アプリケーションの戻り値を「配列で返す例」から、「
Rack::Responseオブジェクトを使う例」に修正
Rails が Rack アプリを扱う際に、ガイド上で本来は以下のような「オブジェクトの戻り値」を示したかったところが、誤って単純な Rack の「配列レスポンス」のように書かれていた、という趣旨です。
イメージとしては、誤った例(※実際のPRとは文言が異なる可能性がありますがニュアンス):
# 誤: Rack レスポンスを単純な配列として記述していたケース
app = Proc.new do |env|
[200, { "Content-Type" => "text/html" }, ["Hello Rack!"]]
endを、Rails ガイドで意図している正しい形へ:
# 正: Rails が要求する形に即した Rack::Response オブジェクトの使用例
app = Proc.new do |env|
response = Rack::Response.new
response['Content-Type'] = 'text/html'
response.write 'Hello Rack!'
response.finish
endあるいは、シンプルな Rack::Response オブジェクトを直接返す形など、
「Rails がサポート・想定している Rack::Response ベースの書き方」に統一する修正です。
PR本文では「Rails が要求する形として Rack::Response オブジェクトを使うべきであり、配列ではない」という点が明示されています。
- 影響範囲・注意点
実コードへの影響
- 変更はガイド(ドキュメント)だけで、Rails 本体のコードには一切変更がありません。
- そのため、既存アプリケーションの挙動に影響はありません。
誤解解消の観点
- 以前のガイドの記述に従って、Rails の Rack エンドポイントやミドルウェアを「配列レスポンス前提」で書いていた場合、ガイドと実際の期待仕様とのズレが生じていた可能性があります。
- これを読んだ新規読者が「Rails 上の Rack アプリでは、
Rack::Responseを使うほうが正しい/推奨される」という点を理解しやすくなります。
実装時の注意
- Rails 上で Rack アプリやミドルウェアを書く場合:
Rack::Responseを利用すると、ヘッダ操作・Cookie 設定・レスポンス組み立てが明示的かつ安全に行いやすいです。response.finishで[status, headers, body]に変換されるため、Rack のインターフェイス要件も満たします。
- Rails 上で Rack アプリやミドルウェアを書く場合:
- 参考情報 (あれば)
- Rack 公式仕様(README)
- Rack アプリケーションのインターフェイス:
call(env) -> [status, headers, body] Rack::Responseはこの標準インターフェイスをラップするヘルパークラスで、finishで配列を返します。
- Rack アプリケーションのインターフェイス:
- Rails ガイド: Rails on Rack
- Rails を Rack アプリケーション/ミドルウェアとして扱う方法や、
config.ruでの設定などを説明しているドキュメントで、そのサンプルコードの一部が今回修正対象となっています。
- Rails を Rack アプリケーション/ミドルウェアとして扱う方法や、
#57516 Fix Duration#in_* truncating sub-second precision
マージ日: 2026/6/1 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::Durationのin_minutes,in_hours,in_days,in_weeks,in_months,in_yearsが、内部で秒数を整数に切り捨ててから計算していたため、サブ秒精度が失われていたバグを修正する PR です。in_seconds/to_iの挙動はそのままに、in_*系メソッドだけが小数を正しく扱うようになります。
- 変更内容の詳細
問題点
元の実装では、各 in_* メソッドが in_seconds(= to_i のエイリアス)を用いており、そこで小数部分が切り捨てられていました。
def to_i
@value.to_i
end
alias :in_seconds :to_i
def in_minutes
in_seconds / SECONDS_PER_MINUTE.to_f
endこのため、@value にサブ秒(小数)が含まれていても、to_i のタイミングで整数へ丸められ、以降の計算結果が不正確になっていました。
例:
90.5.seconds.in_minutes # 1.5 (実際は 1.5083... が正しい)
0.5.seconds.in_days # 0.0 (実際は ~5.78e-06)
3661.5.seconds.in_hours # 1.0169444... (実際は 1.0170833...)in_* のドキュメントは「float を返す」となっているため、サブ秒精度を落とす現在の挙動は仕様ではなくバグと判断されています。
修正内容
in_* 系メソッドで、整数に丸められた in_seconds ではなく、元の value(サブ秒を含む実数値)を用いて計算するように変更されています。
ざっくりいうと、次のようなイメージです:
# 変更前 (イメージ)
def in_minutes
in_seconds / 60.0 # in_seconds は to_i 済み
end
# 変更後 (イメージ)
def in_minutes
value / 60.0 # value はサブ秒を含む実数
end実際のパッチでは、SECONDS_PER_* 定数を使って、同様の修正が in_minutes, in_hours, in_days, in_weeks, in_months, in_years 全てに適用されています。
テスト
activesupport/test/core_ext/duration_test.rb に以下のようなテストが追加されています(趣旨だけ抜粋):
def test_in_units_preserve_sub_second_precision
assert_in_delta 1.5083333333, 90.5.seconds.in_minutes
assert_in_delta 5.78e-06, 0.5.seconds.in_days
assert_in_delta 1.0170833333, 3661.5.seconds.in_hours
end- 追加テストは
mainブランチでは red(失敗)、本修正適用後に green(成功)になることを確認済み。 - 既存の
duration_test.rbの全テストもグリーンで通過。
- 影響範囲・注意点
- 影響を受けるのは、サブ秒(小数秒)を含む
ActiveSupport::Durationをin_minutes,in_hours,in_days,in_weeks,in_months,in_yearsで変換しているコードです。- これまで: 小数秒が切り捨てられ、実際より小さい値(あるいは 0)になる場合があった。
- 今後: サブ秒を含めたより正確な float が返ってきます。
- 整数秒のみを扱っているコードへの影響はありません。
Integer / 60.0とInteger.to_i / 60.0は同じなので、既存の挙動は変わりません。
in_seconds/to_iはこれまで通り整数への切り捨てです。- 整数変換の挙動を前提にしたコード(例: ログ出力、インデックス計算など)は影響を受けません。
- 精度が向上することで、「いままで暗黙に切り捨てられていたサブ秒分」が計算に含まれるようになるため、
- 厳密な数値比較テスト(
==比較)をしている場合、テストが落ちる可能性があります。 - 特に「期待値を古い不正確な値で固定している」テストは見直しが必要になります。
- 厳密な数値比較テスト(
- 過去約 5.7 年にわたり未テストの経路だったため、逆に言えば「この不正確な挙動に依存したコード」はあまり多くないことが期待されますが、ライブラリや長寿命のアプリでは一応の確認が必要です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57516
- 該当実装ファイル:
activesupport/lib/active_support/duration.rb - 該当テスト:
activesupport/test/core_ext/duration_test.rb - バグの導入コミット:
7bd9603778c(2020-03-11) で初登場a2535d9fe9(2020-08-26) でin_*にリネーム
#57514 Hash PostgreSQL cable channel identifiers by byte size
マージ日: 2026/6/1 | 作成者: @55728
- 概要 (1-2文で)
Action Cable の PostgreSQL サブスクリプションアダプタで、チャネル名の長さ判定を「文字数」ではなく「バイト数」で行うように変更し、マルチバイト文字を含むチャネル名が PostgreSQL の識別子上限(63バイト)でサイレントに切り詰められる問題を修正した PR です。
これにより、長い日本語名などを使ったチャネルで発生し得た「メッセージのサイレントロス」および「別チャネルへの誤配送」が解消されます。
- 変更内容の詳細
何を直したか
Action Cable の PostgreSQL アダプタでは、PostgreSQL の識別子長上限(NAMEDATALEN - 1 = 63 バイト)を超えるチャネル名は SHA1 でハッシュ化していましたが、その判定に String#size(文字数)を使っていました。
# 旧実装
def channel_identifier(channel)
channel.size > 63 ? OpenSSL::Digest::SHA1.hexdigest(channel) : channel
endしかし、PostgreSQL の上限は「文字数」ではなく「バイト数」です。UTF-8 の日本語(3バイト/文字)などの場合、
- 21文字の「あ" * 21` → 63バイト
- 30文字 + 1文字 (
"あ" * 30 + "X") → 91バイト
といったように、63文字未満でも63バイトを超えることがあり得ます。size <= 63 だとハッシュ化されずそのまま LISTEN/NOTIFY に使われ、PostgreSQL 側でサイレントに 63バイトへ切り詰められます。
この結果、Action Cable 内部で使っている「チャネル識別子」と、PostgreSQL 側から wait_for_notify で返ってくる識別子とで不整合が発生していました。
具体的な不具合
Action Cable の PostgreSQL アダプタはざっくり次のような構造を持っています。
- 購読時:
channel_identifier(channel)をキーとした Ruby のハッシュに subscriber を登録 - 受信時:
PG#wait_for_notifyが返すチャネル名をキーに subscriber を引いて dispatch
ここで、以下のような状況を考えます(DB は UTF-8):
S = "あ" * 21 # 21文字 / 63バイト → size=21 <= 63 なのでハッシュされない
L = ("あ" * 30) + "X" # 31文字 / 91バイト → size=31 <= 63 なのでハッシュされない
# ただし PostgreSQL は 63バイトに切り詰め → "あ" * 21 == S- Ruby 側登録キー
channel_identifier(S)→"あ"*21(63バイト)channel_identifier(L)→"あ"*30 + "X"(91バイト、そのまま)
- PostgreSQL 側から
wait_for_notifyで返るキーNOTIFY L→"あ"*21(91バイト → 63バイトにトリムされて S と同一)
このため、2パターンの不具合が生じます:
サイレントメッセージロス
長いチャネルLへ broadcast しても、wait_for_notifyが返すキーは"あ"*21(S相当)になるが、登録時のキーは"あ"*30 + "X"(91バイト)で一致しないため、Lの subscriber には一切メッセージが届かない。別ストリームへの誤配送
もし"あ"*21(S)にも別の subscriber が存在している場合、wait_for_notifyのキー"あ"*21でハッシュを引くと S 側の subscriber がヒットするため、
本来 L 向けのメッセージが S に誤配送され、L は何も受け取れない。
この問題は、マルチバイトで63バイトを超えたチャネル名を使い、かつその63バイトへの切り詰め結果に一致する別のチャネルが存在する場合に顕在化します。
日本語などの多バイト言語をチャネル名に使うと、実務上発生し得るパターンです。
修正内容
判定を「文字数」から「バイト数」に変更しました。
# 新実装
def channel_identifier(channel)
channel.bytesize > 63 ? OpenSSL::Digest::SHA1.hexdigest(channel) : channel
endbytesize > 63の場合のみ SHA1 ハッシュ(40バイト文字列)に変換- SHA1 は常に 40文字(40バイト)なので、PostgreSQL の 63バイト制限を超えない
- 結果として、
- PostgreSQL に渡す識別子
- PostgreSQL から返ってくる識別子
- Ruby 側の登録時キー
が再び一致するようになり、上記のロス・誤配送が解消されます。
- ASCII だけのチャネル名は
size == bytesizeのため挙動は従来と変わりません。
テスト追加
actioncable/test/subscription_adapter/postgresql_test.rb に以下の end-to-end テストを追加しています。
- テスト名:
test_long_multibyte_identifiers - 実際の
LISTEN/NOTIFYを使う harness(subscribe_as_queue/@rx_adapter/@tx_adapter)で動作確認 - シナリオ:
long(91バイト、日本語 + 英字)とshort("あ"*21, 63バイト)両方に subscribelongに対して broadcast- 2つの assertion:
long側 subscriber が payload を受け取ること(旧実装ではロス)short側 subscriber は何も受け取らないこと(旧実装では誤配送)
- 受信は
Queue#pop(timeout: ...)でタイムアウト付きにして、旧実装時もテストがハングせずにすぐ失敗するよう工夫されています。 String#size版では両 assertion が赤、String#bytesizeに変更すると緑になることを確認済み。
- 影響範囲・注意点
- 対象:
- Action Cable を PostgreSQL アダプタ(
ActionCable::SubscriptionAdapter::PostgreSQL)で利用しているアプリケーション - チャネル名(stream 名)に UTF-8 のマルチバイト文字を使い、合計バイト数が 63バイトを超えるケース
- Action Cable を PostgreSQL アダプタ(
- 影響:
- 従来: 63文字以内であれば、バイト数が 63を超えていても「ハッシュされず」そのまま識別子として使われていた
- 今回: 63バイトを超えた時点で必ず SHA1 ハッシュに変換される
- 互換性上の注意:
- チャネル名 → PostgreSQL 上の identifier へのマッピングが、一部ケースで「素の名前」から「SHA1 ハッシュ」へと変わるため、
- 旧バージョンの Rails / adapter と新バージョンの adapter を混在運用している場合、
- あるいは Action Cable のチャネル名を直接参照して PostgreSQL 側の
LISTEN/NOTIFYを行っている自前コード
などがあると、識別子の不一致が発生する可能性があります。
- 一般的な Rails アプリ(Action Cable も Rails 側 API 経由のみ利用)では、多くの場合は透過的な変更となります。
- チャネル名 → PostgreSQL 上の identifier へのマッピングが、一部ケースで「素の名前」から「SHA1 ハッシュ」へと変わるため、
- セキュリティ・安定性:
- SHA1 の利用はあくまで識別子の短縮用途であり、暗号学的安全性が問題になるわけではありません(元々そういう用途として導入されたもの (#28751))。
- 修正により、意図しない cross-delivery / silent drop が解消され、安定性・予測可能性は向上します。
- 参考情報 (あれば)
- PostgreSQL の識別子長制限:
NAMEDATALENは通常 64- 実際の識別子長上限は
NAMEDATALEN - 1 = 63バイト - これを超える識別子はサイレントに切り詰められ、NOTICE が出るのみ
- 関連する過去の変更:
- もともとのハッシュ化ロジックは #28751(commit
2bce7777b7)で導入されており、今回の変更は「長さ判定を PostgreSQL の仕様(バイト数)に合わせて是正した」ものです。
- もともとのハッシュ化ロジックは #28751(commit
- 実務的な示唆:
- WebSocket / PubSub 系で DB をバックエンドに使う場合、識別子長の単位(文字数ではなくバイト数)に注意が必要
- マルチバイトを多用するシステムでは、UI 上の「文字数制限」だけでは不十分なことがあるため、バックエンド側のバイト数制限も意識する必要があります。
#57505 Parse all HTTP-date formats in If-Modified-Since
マージ日: 2026/6/1 | 作成者: @55728
- 概要 (1-2文で)
If-Modified-Sinceヘッダの日時パースをTime.rfc2822からTime.httpdateに変更し、RFC 9110 で必須とされる3種類すべての HTTP-date 形式(IMF-fixdate / RFC 850 / asctime)を正しく解釈できるようにしたPRです。これにより、これまで一部フォーマットで304 Not Modifiedが返せずフルレスポンスになっていた挙動が修正されます。
- 変更内容の詳細
背景
If-Modified-Sinceは HTTP-date を持つヘッダで、RFC 9110 §5.6.7 では次の3フォーマットを定義し、かつ「すべて受理しなければならない(MUST)」としています:- IMF-fixdate (例:
Sun, 06 Nov 1994 08:49:37 GMT) - RFC 850 (例:
Sunday, 06-Nov-94 08:49:37 GMT) - asctime (例:
Sun Nov 6 08:49:37 1994)
- IMF-fixdate (例:
既存実装では
ActionDispatch::Http::Cache::Request#if_modified_sinceがTime.rfc2822でパースしていました:rubydef if_modified_since if since = get_header(HTTP_IF_MODIFIED_SINCE) Time.rfc2822(since) rescue nil end endTime.rfc2822は IMF-fixdate 相当の形式しか受け付けないため、RFC 850 / asctime 形式で送られたIf-Modified-Sinceはパースに失敗し、rescue nilにより「ヘッダが無いかのように」扱われていました。
→not_modified?が常にfalseになり、本来なら304 Not Modifiedを返せるケースでもフルレスポンスが返されていた。一方で、同じファイル内のレスポンス側(
Last-ModifiedやDateの読み取り)は既にTime.httpdateを使っており、リクエスト側だけが不整合な状態でした。
修正内容
if_modified_since のパーサーを Time.httpdate に変更:
def if_modified_since
if since = get_header(HTTP_IF_MODIFIED_SINCE)
Time.httpdate(since) rescue nil
end
endTime.httpdateは HTTP-date の3フォーマットすべてをサポートするため、RFC 9110 が要求する仕様に沿った挙動になります。- ブラウザや Rails が自分で生成する日付(
Time#httpdate)は IMF-fixdate なので、一般的なケースの挙動は変わりません。
再現例と修正後の挙動
req = ->(ims) { ActionDispatch::Request.new(Rack::MockRequest.env_for("/", "HTTP_IF_MODIFIED_SINCE" => ims)) }
mtime = Time.utc(1994, 11, 6, 8, 49, 37)
# 1. IMF-fixdate: 以前から OK
req.("Sun, 06 Nov 1994 08:49:37 GMT").not_modified?(mtime)
# => true
# 2. RFC 850: 修正前は nil(ヘッダ無しと同等)→ 修正後は true
req.("Sunday, 06-Nov-94 08:49:37 GMT").not_modified?(mtime)
# => 修正前: nil / 修正後: true
# 3. asctime: 同様に修正前 nil → 修正後 true
req.("Sun Nov 6 08:49:37 1994").not_modified?(mtime)
# => 修正前: nil / 修正後: trueテスト
actionpack/test/dispatch/request_test.rb に RequestIfModifiedSince テストケースを追加:
- 3種類の HTTP-date 形式が
if_modified_sinceで正しくパースされ、not_modified?が期待どおりtrueになること。 - パースできない日付文字列が渡された場合は
nilを返すこと。 - ヘッダが存在しない場合も
nilを返すこと。
既存コードでは RFC 850 / asctime に対するテストが失敗し、この修正を適用するとパスすることが確認されています。
- 影響範囲・注意点
影響範囲
- 対象:
If-Modified-Sinceによる条件付き GET (not_modified?) を利用するすべての Rails アプリ。 - 変更点:
- RFC 850 / asctime 形式を利用するクライアント・プロキシに対し、これまでより 積極的に 304 を返す ようになります。
- これまでは「
If-Modified-Sinceが無いもの」と見なされていたケース(= 常にフルレスポンス)で、正しく条件付きレスポンスが機能するようになります。
- 性質:
- キャッシュ・帯域最適化に関する改善であり、誤ったコンテンツを返すリスクはありません(最悪でもフルレスポンスが返るだけ)。
Time.httpdate の厳格さによる差異
Time.httpdateはタイムゾーンにGMTを要求します。- 例えば
Sun, 06 Nov 1994 08:49:37 +0000のような「RFC 2822 的には妥当だが HTTP-date としては非準拠」な値は、Time.httpdateではパースエラーになります。- 以前:
Time.rfc2822がパースしてTimeオブジェクトを返す。 - 以後:
Time.httpdateが例外を投げ、rescue nilによってnilとなる → ヘッダ無し相当の扱い →304を返し損ねてフルレスポンスになる。
- 以前:
- ただし、これらは HTTP-date としてそもそも不正なフォーマット なので、仕様準拠を優先した形です。
- 挙動の変化も「本来 304 を返すべきではないヘッダに対して 304 を返さない」「結果的にフルレスポンスになる」だけであり、安全側に倒れていると言えます。
互換性の総括
- 仕様通りの HTTP-date (
GMT、3フォーマットのいずれか) を送るクライアントは完全に後方互換であり、挙動は変わらないか、RFC 850 / asctime の場合に改善されます。 - 非準拠クライアント(
+0000など)に対しては、304 を返さずフルレスポンスになる方向への変化のみが発生し、誤った 304 が返るような形にはなりません。 - サーバ側とクライアント側での HTTP-date 取り扱いが
Time.httpdateで統一されたことで、ActionDispatch 内の一貫性も向上しています。
- 参考情報 (あれば)
- RFC 9110 §5.6.7: HTTP-date の定義(IMF-fixdate / RFC 850 / asctime、および GMT 必須)
https://www.rfc-editor.org/rfc/rfc9110#section-5.6.7 - Rails 内の関連箇所:
ActionDispatch::Http::Cache::Request#if_modified_since(本PRで修正)- 同ファイル内での
Last-Modified/Dateの読み取りロジック(既にTime.httpdateを使用)
#57519 Fix store accessor *_change and saved_change_to_* reporting unchanged keys as changed
マージ日: 2026/6/1 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Storeで生成される*_change/saved_change_to_*が、実際には値が変わっていないキーに対しても[value, value]を返してしまう不具合を修正した PR です。
これにより、通常の ActiveModel の dirty トラッキング API と同様に、値が変わっていないキーに対してはnilを返すようになります。
- 変更内容の詳細
何が問題だったか
ActiveRecord::Store では、store / store_accessor で定義したキーごとに、次のような dirty メソッドが自動生成されます。
<key>_changed?<key>_changesaved_change_to_<key>?saved_change_to_<key>
このうち *_change / saved_change_to_* の実装が、カラム単位の変更有無(attribute_changed?(store_attribute) / saved_change_to_attribute?(store_attribute))しか見ておらず、キー単位で値が変わったかどうかを見ていなかったのが問題でした。
元の挙動は以下のようになります。
user = User.create!(color: "black", size: "L").reload
user.color = "white" # color だけ変更
user.size_changed? # => false (キー単位のpredicateは正しい)
user.size_change # => ["L", "L"] (バグ、本来は nil であるべき)
user.save!
user.saved_change_to_size? # => false (正しい)
user.saved_change_to_size # => ["L", "L"] (バグ、本来は nil)つまり、
- ストアカラム(例:
settings)のどれか1キーでも変更があると - そのカラム内の 他のキー についても
<key>_changed?/saved_change_to_<key>?はfalse(正しい)- なのに
<key>_change/saved_change_to_<key>は[value, value]を返す(間違い)
という不整合な状態になっていました。
ActiveModel の通常の dirty API (attribute_change, saved_change_to_attribute) では「値が変わっていない場合は nil を返す」のが契約であり、これに反していたことになります。
どう修正されたか
activerecord/lib/active_record/store.rb の *_change / saved_change_to_* の実装において、キー単位での値比較を行い、変化がなければ nil を返すように修正されています。
疑似コードレベルでいうと、これまで:
define_method("#{accessor_key}_change") do
return unless attribute_changed?(store_attribute)
prev_store, new_store = changes[store_attribute]
accessor = store_accessor_for(store_attribute)
[accessor.get(prev_store, key), accessor.get(new_store, key)] # 変わってなくても常に返していた
endだったものが、以下のように変更されています:
define_method("#{accessor_key}_change") do
return unless attribute_changed?(store_attribute)
prev_store, new_store = changes[store_attribute]
accessor = store_accessor_for(store_attribute)
prev_value = accessor.get(prev_store, key)
new_value = accessor.get(new_store, key)
[prev_value, new_value] unless prev_value == new_value
endsaved_change_to_<key> についても同様に、前後の値を比較して同値であれば nil を返すようになっています。
変更されていない挙動
以下のメソッドは今回の修正対象外で、従来どおりの挙動を保っています。
<key>_was<key>_before_last_save
これらは通常の attr_was / attribute_before_last_save と同様、「キーが変更されていない場合は現在値を返す」という契約であり、nil にはなりません。この契約は dirty pair (*_change) の契約とは別物なので、そのまま維持されています。
テスト
activerecord/test/cases/store_test.rb にリグレッションテストが追加されています。
ストアに複数のキー(例:
color,homepage)を定義homepageに実データ(URL)を入れた状態でcolorだけ変更したときにhomepage_changed?/saved_change_to_homepage?がfalseであることに加えてhomepage_change/saved_change_to_homepageがnilを返すこと
を検証しています。
PR内で main ブランチに対してテストが「red → fix 適用後 green」になることが確認されています。
- 影響範囲・注意点
影響を受けるコード
影響を受けるのは、store accessor の *_change / saved_change_to_* の戻り値を直接利用しているコードです。
たとえば、次のようなコードを書いている場合:
before_save do
prev, cur = settings_change_for_size = [size_change].flatten
# もしくは
prev, cur = size_change if size_change
# あるいは
prev, cur = saved_change_to_size
end以前は「サイズは変わっていないが、同じストア内の他のキーだけが変わった」という場合に ["L", "L"] のようなペアが返ってきていましたが、これが nil に変わります。
よくあるパターン:
prev, cur = size_change
if prev != cur
# ... 何か処理 ...
endこうしたコードは、これまでは prev != cur が false になって何も起きない、という「一応安全だが無駄な比較」状態でしたが、今後は size_change が nil になり、prev, cur = size_change で TypeError になる可能性があります。
とはいえ、dirty pair の契約に従うコードであれば、本来は以下のように nil チェックを行うべきです:
if (change = size_change)
prev, cur = change
# ... 何か処理 ...
endあるいは:
if size_changed?
prev, cur = size_change
# ... 何か処理 ...
endそのため、契約どおりの使い方をしている限りは問題になりにくく、挙動はより一貫性のあるものになります。
後方互換性の観点
- ActiveModel の標準的な dirty トラッキング (
attribute_change,saved_change_to_attribute) と同じ挙動になるよう修正されているため、API 契約としてはむしろ「正しい」方向への変更です。 - ただし、過去の「間違った挙動」に暗黙的に依存していたコード(「変わっていない場合も
[value, value]が返る」前提)は壊れる可能性があります。 *_was/*_before_last_saveはこれまでどおり「変わっていない場合も現在値を返す」ので、こちらに依存しているコードは影響を受けません。
影響レベル
PR説明上は「Medium」とされており:
- セキュリティインパクトはなし
*_changed?/saved_change_to_*?はもともと正しかったため、これらの predicate を利用するコードは基本的に影響なし- 直接ペアを読んでいるコードのみが対象
という整理です。
- 参考情報 (あれば)
- 対象コード:
activerecord/lib/active_record/store.rbstore_accessor周りの dirty メソッド (<key>_change,saved_change_to_<key>) の実装
- テスト:
activerecord/test/cases/store_test.rb- 兄弟キー(同じ store カラム内の別キー)が変化したときの
*_change/saved_change_to_*の戻り値の検証
- 兄弟キー(同じ store カラム内の別キー)が変化したときの
- バグの由来:
- store の dirty メソッドが追加された 2019-03-25 (
61a39ffcc6) から存在 - 2025年の
97df37b898で値の取得方法をdig→accessor.getに変えたが、ガードロジック自体はそのまま残っていたためバグは解消されていなかった
- store の dirty メソッドが追加された 2019-03-25 (
store accessor の dirty 情報を直接扱うコードを書いている場合は、この PR の変更(「変化していないキーでは *_change / saved_change_to_* が nil を返す」)を前提に、nil ハンドリングが適切かを一度確認しておくと安全です。
#57515 Fix PostgreSQL range bounds parser corrupting comma-containing bounds
マージ日: 2026/6/1 | 作成者: @55728
- 概要 (1-2文で)
PostgreSQL の range 型を ActiveRecord が文字列からパースする際、境界値にカンマが含まれていると誤って分割してしまい、両端の値が壊れる不具合を修正した PR です。特にmoneyrangeでは$1,000.00のような通常の値であっても0.0..0.0に化けるという、重大なサイレントデータ破壊を防ぐ修正です。
- 変更内容の詳細
問題のあったコードとバグの内容
対象は PostgreSQL::OID::Range#extract_bounds(activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb)で、range のテキスト表現から下限・上限を取り出す処理です。
以前は、外側の括弧・ブラケットを落とした後、
value[1..-2].split(",", 2)と 先頭のカンマで単純に split していました。
PostgreSQL の range 表現の仕様として、
- 境界値にカンマやスペース、クオート等が含まれる場合は
"..."で ダブルクォートしてエスケープ する - 例:
["a,b","c,d"),["$1,000.00","$2,000.50"]
という挙動がありますが、split(",", 2) だと ダブルクォートを考慮せず 生の文字列を分割してしまうため、以下のようなバグが発生していました。
例: ["a,b","c,d") をパースする場合
value[1..-2] # => " "a,b","c,d" "
split(",", 2) # => ["\"a", "b\",\"c,d\""]結果として、ActiveRecord から見える range は
# 本来
range.string_range # => "a,b"..."c,d"
# 実際 (before)
range.string_range # => "\"a"..."b\",\"c,d\""のように両端とも壊れていました。
特に危険な moneyrange
money 型のテキスト表現はロケールによっては 3 桁ごとにカンマ区切り を含みます(例: "$1,000.00")。そのため moneyrange で次のようなデータを持っていると:
'["$1,000.00","$2,000.50"]'::moneyrangeActiveRecord 側では分割後に money 型としてキャストされる際、壊れた "\"$1 などの断片が 0 に解釈されてしまい、結果として 0.0..0.0 という完全な誤値 になります。これは異常値にも見えず、検知が困難なため深刻です。
修正内容
1. カンマ分割を「クオート対応」なものに変更
split(",", 2) を、ダブルクォートを理解した split_bounds メソッドに置き換えました。
BOUNDS = /\A((?:"(?:[^"\\]|""|\\.)*"|[^,"])*),(.*)\z/m # :nodoc:
def split_bounds(value)
# 高速パス: ダブルクォートを含まなければ従来通り split(",", 2)
return value.split(",", 2) unless value.include?('"')
(m = BOUNDS.match(value)) ? [m[1], m[2]] : [value, nil]
endポイント:
value.include?('"')が なければ従来どおりString#split(",", 2)を使用
→ int4range/numrange/tsrange/daterange など、通常の組み込み range 型はパフォーマンス影響がほぼない- ダブルクォートが含まれる場合のみ、
BOUNDS正規表現で- 「ダブルクォートで囲まれたセグメント」をスキップしつつ
- 「境界を分ける 1 個のカンマ」を検出して 下限・上限に綺麗に分割する
正規表現の構造:
\A(
(?:
"(?:[^"\\]|""|\\.)*" # "..." の中。 ""(二重引用)と \x のエスケープも許容
| # または
[^,"] # カンマ・ダブルクォート以外の任意文字(= 非クオート領域)
)*
), # ↑で取ったものの直後のカンマが境界
(.*)\z # 残りが上限側
/m[^,"]にしているのは、
「クオートされた部分」と「クオートされていない部分」が正規表現上で競合しないようにするため(曖昧性排除とパフォーマンスのため)/mフラグにより、上限側(2 番目のキャプチャ)が改行をまたいでも取得できるようにしている
2. unquote の nil 耐性向上
元コードでは、split(",", 2) が失敗すると to == nil になり、後続の unquote(to) 内で nil.start_with? により NoMethodError が発生する可能性がありました。
今回の変更で split_bounds はマッチ失敗時に [value, nil] を返すようになり、unquote 側も nil をそのまま返すようにして、不正入力時はクラッシュではなく「無変換の値 or nil」として処理されるようになっています。
(有効な PostgreSQL 出力では必ずカンマが含まれるため、このフォールバックパスは基本的に壊れた入力への備えです。)
3. テスト追加
activerecord/test/cases/adapters/postgresql/range_test.rb に、以下の新規テストが追加されています(いずれも PostgreSQL 実機に対してレッド → 修正後グリーンを確認済み):
- カンマを含むクオート境界の基本ケース
["a,b","c,d")が"a,b"..."c,d"として読み戻せること
- クオート+カンマ+ダブルクォート+バックスラッシュ混在
["a,""b","c\\,d")のような複雑ケースのパース検証
- 片側のみクオートされている場合
["x,y",z)
- 片側無限境界(beginless / endless)+カンマ
["a,b",)と[,"c,d")
- 境界に改行を含むケース
/mフラグを外すと壊れるケースのドキュメント兼テスト
- 空文字列
""を境界に含むケース["","z")が、空境界("")と無限境界(nil)を区別して扱えていること
これにより、クオート・エスケープ・カンマ・改行・空文字といった典型的な落とし穴を網羅的にカバーしています。
4. パフォーマンス評価
extract_bounds は deserialization のホットパスであるため、速度劣化がないようベンチマークも行われています。Ruby 4.0.2 + benchmark-ips での計測結果:
- ダブルクォートを含まないケース(通常の int/date 等)
- 旧:
split(",", 2)単体 - 新:
include?('"')➜split(",", 2)のハイブリッド
→ ほぼ 1.2 倍程度のオーバーヘッドに収まる(数百 ns レベル)
- 旧:
- ダブルクォートを含むケース(今回バグっていた text/varchar/money など)
- split は速いが 結果がバグっている
- regexp ベースの
split_boundsは ~300ns 程度
→ deserialization 全体(gsub,subtype.deserialize,Rangeオブジェクト生成など)に比べれば十分小さい
また、「手書き 1 パスの Ruby スキャナ」も試されたものの、必ずしも正規表現より速くならなかったため採用されていません。
- 影響範囲・注意点
影響を受ける可能性があるケース
- PostgreSQL 側で以下のような range 型を使用しており、ActiveRecord で読み書きしているアプリケーション:
text/varcharサブタイプのカスタム range 型- 例:
CREATE TYPE textrange AS RANGE (subtype = text);など
- 例:
moneyサブタイプの range 型(特に危険)- 例:
CREATE TYPE moneyrange AS RANGE (subtype = money);
- 例:
PostgreSQL では pg_range カタログに登録されたすべての range 型 (typtype = 'r') が、ActiveRecord 起動時に PostgreSQL::OID::Range.new(subtype, typname) として 自動登録 されるため、「カスタム range 型だから非サポート」という扱いではありません。全てサポート対象であり、そのうち「クオートされた境界にカンマが入りうるもの」は影響を受けます。
一方で、次のような標準 range 型はそもそもカンマを含まないテキスト表現のため影響なし(ただし include?('"') の 1 パス分だけは増える)です。
int4range/int8rangenumrangetsrange/tstzrangedaterange
(これらはカンマもクオートも通常出力では使われません)
実運用上のリスク
- すでに本バグを含んだ Rails でアプリを運用していた場合、
moneyrangeやtextrange等から ActiveRecord 経由で値を読んだ瞬間に「壊れた値」がアプリロジックの中で利用されていた可能性があります。- ただし「DB の中身(ディスク上の値)」自体は壊れておらず、パース時に壊れていただけ なので、修正後に再読み込みすれば正しい値が取得されます。
- ログや監査、集計値などに影響が波及している可能性があるため、
moneyrangeを使っている場合は特に挙動確認を推奨します。
マイグレーション / 互換性上の注意
- range のテキストフォーマットや PostgreSQL 側の動作を変えているわけではなく、あくまで Rails 側のパーサの修正のみです。
- 有効な PostgreSQL の range 文字列表現に対しては後方互換であり、「以前は通っていたけど今は例外になる」というような変更は意図的には入っていません(むしろ逆に、不正入力に対しては例外から「穏当な失敗」へと挙動が緩和されています)。
- したがって、ほとんどのアプリケーションは「アップデートすると正しくなる」だけで、追加でのコード変更は不要です。
- 参考情報 (あれば)
- 該当コード:
activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rbextract_bounds内部でsplit_boundsが利用される形に変更
- range 型の自動登録:
activerecord/lib/active_record/connection_adapters/postgresql/type_map_initializer.rbpg_rangeカタログを走査し、OID::Range.new(subtype, typname)として type map に登録
- バグの由来:
- 2019-11-06: quoted bound 対応 (
unquote) を追加したが、分割ロジックはカンマを無条件 split のままだった (45d7dad578) - 2020-05-29:
split(",")→split(",", 2)に変更したのみ (c65864cdca)
→ 5.5 年ほど潜伏していた不具合
- 2019-11-06: quoted bound 対応 (
この PR により、PostgreSQL の range 表現仕様(ダブルクォート・エスケープ・カンマ・改行)に正しく追従したパースが行えるようになり、特に moneyrange のような実害の大きいケースでのサイレントデータ破壊が解消されます。
#57476 Rescue RedisClient::Error in RedisCacheStore failsafe
マージ日: 2026/6/1 | 作成者: @darrunategui
- 概要 (1-2文で)
ActiveSupport::Cache::RedisCacheStoreの「Redis障害時でも例外を投げずにフェイルセーフに動作する」という契約が、Redis Sentinel など一部構成で破れていた問題を修正するPRです。RedisClient::Error系の例外もフェイルセーフで握りつぶすようにし、テストとCHANGELOGを追加しています。
- 変更内容の詳細
2-1. 問題の背景
RedisCacheStore はドキュメント上、以下のような動作が期待されています:
- Redis サーバが落ちている・一時的に接続できない場合
- 例外はアプリ側に伝播しない
readは常にミス扱い (nil等)writeやdeleteなどは黙って失敗(エラーを投げない)
これまでの実装は、Redis::Client#translate_error! が投げる Redis::BaseError など「Redis::* 階層の例外」だけを rescue していました。
しかし、以下のような構成時に問題がありました:
Redis.new(sentinels: ...)で Sentinel を使うと、内部的にclient_implementation: ::RedisClientが強制される- このとき、プールされている接続は
Redis::Clientではなく素の::RedisClientになる - そのため
Redis::Client#translate_error!を経由せず、RedisClient::ConnectionError,RedisClient::ReadTimeoutErrorなど 生のRedisClient::Errorサブクラスが直接飛ぶ RedisCacheStoreのフェイルセーフはRedis::*系だけを rescue していたため、これらRedisClient::Errorはすり抜けてアプリケーションエラーとなってしまう
これは、過去にあった ConnectionPool::TimeoutError がフェイルセーフをすり抜けていたバグ (#54432) と同種の問題です。
2-2. コード上の主な変更点
2-2-1. フェイルセーフで捕捉する例外の拡張
RedisCacheStore#failsafe(内部で使用されている例外捕捉ラッパー)で rescue する対象に ::RedisClient::Error を追加しました。
変更点のポイント:
- フェイルセーフが rescue する例外クラスのリストを、プライベート定数
FAILSAFE_ERRORSとして切り出し - その中に
::RedisClient::Errorを条件付きで追加
イメージとしては次のような形になっています(実際のコードから簡略化):
# activesupport/lib/active_support/cache/redis_cache_store.rb
FAILSAFE_ERRORS = [
::Redis::BaseError,
# 既存のConnectionPool::Errorなど...
].tap do |errors|
# redis 5.x 以降でトップレベルに RedisClient がある場合のみ追加
errors << ::RedisClient::Error if defined?(::RedisClient::Error)
end.freeze
def failsafe(return_value = nil)
yield
rescue *FAILSAFE_ERRORS => error
handle_exception(error) # ログ等
return_value
endこれにより、
- redis-rb 5.x 系 (
RedisClientがトップレベルに存在する)RedisClient::Errorも確実に rescue される
- redis-rb 4.x 系 (
RedisClientがトップレベルに存在しない)defined?(::RedisClient::Error)が false になるため、互換性を壊さない
という挙動になります。
2-2-2. テストの追加
新しいテスト FailureSafetyFromRedisClientErrorTest が redis_cache_store_test.rb に追加されています。
目的は「RedisClient::Error が発生した場合でも、RedisCacheStore の全公開メソッドがフェイルセーフに動作すること」を保証することです。
テストの構成:
RedisClientErrorRedisClientというテスト用ダミーのクライアントクラスを定義ensure_connectedで強制的にRedisClient::Errorを raise するself.translate_error!を「何もしないで再送出する」no-op 実装にオーバーライド- redis-rb 現行実装では
Redis::Client#call_vとRedis#send_commandの2段階でRedisClient::ErrorをRedis::BaseErrorなどに変換する - 両方とも
Client.translate_error!を「定義位置のClient」として参照する(lexical lookup) - テスト内で
Redis.const_set(:Client, …)のように差し替えているため、見ているtranslate_error!はこのダミークラスになる - そこで no-op にすることで、「
RedisClient::Errorが最後まで変換されずに飛んでくる状況=Sentinel 経由の実際のパス」を忠実に再現している
- redis-rb 現行実装では
- その上で
FailureSafetyBehaviorという既存のテストスイートを再利用し、以下のような各メソッドについてread,write,fetch,delete,increment,decrement,clearなど
- ドキュメント通りの「フェイルセーフな戻り値」(例:
readはnil、incrementはnil/ 既定値、など) を返すことを検証
テストのパターン自体は、既存の
FailureSafetyFromUnavailableClientTestFailureSafetyFromMaxClientsReachedErrorTest
と同じ構造になっており、それの RedisClient::Error 版が追加された形です。
2-2-3. CHANGELOG の更新
activesupport の CHANGELOG に、RedisCacheStore のフェイルセーフが RedisClient::Error を拾うようになったことが追記されています。
バグフィックスとしての振る舞い変更があるため、ライブラリアップデート時に利用者が把握しやすくなっています。
- 影響範囲・注意点
3-1. 影響を受ける構成
特に影響が大きいのは次のような構成です:
ActiveSupport::Cache::RedisCacheStoreを使用しているRedis.new(sentinels: ...)/ Sentinel 構成、またはclient_implementation: RedisClientを使うような設定- 一時的な Redis 障害時に、
Rails.cache.read/Rails.cache.writeなどの呼び出しでRedisClient::ConnectionError,RedisClient::ReadTimeoutError等がアプリケーション側に伝播していた
このPR適用後は、これらの例外はフェイルセーフにより握りつぶされ、従来のドキュメント通り:
- 例外を投げず
- 「キャッシュミス」または「書き込み失敗だが無視」という扱い
に統一されます。
3-2. 例外ハンドリング・監視の観点での注意
- これまで、
RedisClient::Errorをアプリ側で rescue していた / エラートラッキングに出ていた場合- 今後は
Rails.cache呼び出しからその例外は飛んでこなくなります - Rails 側のログ (
handle_exception相当) や Redis 側のメトリクスで監視する必要があります
- 今後は
- 「Redis エラーをあえてアプリケーションエラーとして扱いたい」という設計には合致しません
RedisCacheStoreはあくまで「キャッシュは落ちてもアプリを落とさない」設計であり、その契約に仕様を合わせた変更です- もし Redis 障害を致命的とみなしたい場合は、
RedisCacheStoreではなくアプリ側で直接 Redis クライアントを扱う等、別設計が必要です
3-3. 互換性
- redis-rb 4.x:
RedisClient定数が存在しないため、この変更は実質無影響です(FAILSAFE_ERRORSに追加されない)
- redis-rb 5.x 以降:
- これまで例外として表に出ていた
RedisClient::Error系がフェイルセーフで抑制されるため、「バグ修正としての挙動変更」が発生しますが、ドキュメントされた期待仕様に揃う形の変更です
- これまで例外として表に出ていた
- 参考情報 (あれば)
- このPRが扱う問題と関連する既存Issue/PR:
- #54432:
ConnectionPool::TimeoutErrorがフェイルセーフをすり抜けていた問題 - #54440 / #54460: 上記問題への対応で、rescue 範囲を広げた変更
- #54432:
RedisCacheStoreの設計思想:- 「キャッシュは落ちてもアプリケーションを落とさない」というポリシーで、Redis 障害時には例外を投げず、ミス扱い / サイレントな書き込み失敗とすることが明示されています
- 本PRは、そのポリシーが Sentinel +
RedisClientパスでも一貫して守られるようにするためのものです
#57502 Handle malformed signed cache payloads gracefully
マージ日: 2026/6/1 | 作成者: @fallintoplace
- 概要 (1-2文で)
Rails のActiveSupport::Cache::Coder#loadが「署名付きキャッシュフレームの形式が壊れている場合」に例外を投げてしまう問題を修正し、他の破損ペイロードと同様に「キャッシュミス(nil)」として扱うようにした PR です。これにより、壊れた署名付きキャッシュがアプリケーションエラーとして表面化するのを防ぎます。
- 変更内容の詳細
問題のパス: 署名付きキャッシュフレーム
ActiveSupport::Cache::Coder は、キャッシュ値を以下のような形で扱います。
- シリアライザ(例:
Marshal) - 圧縮器(例:
Zlib) - 署名付きフレーム形式(ActiveSupport が内部で使う独自フォーマット)
Coder#load は、ペイロードが署名付きキャッシュフレームのプレフィックスを持っているとき、ヘッダ部分を String#unpack1 で解析していました。しかし、ヘッダを展開する前後が例外保護されておらず、バイト列が短すぎる/形式が崩れていると次のような例外が外に漏れていました。
coder = ActiveSupport::Cache::Coder.new(Marshal, Zlib)
coder.load("\x00\x11".b)
# => ArgumentError: @ outside of string本来であれば「壊れたキャッシュ → 読み込めない → nil を返す(キャッシュミス扱い)」という動作に統一されるべきところが、署名付きパスだけが例外をそのまま投げてしまう状態でした。
何をしたか
署名付きヘッダのパース処理を例外から保護
- 署名付きフレームのヘッダ(
unpack1)を扱うコードを、他のデシリアライズエラー処理と同じように例外ハンドリングの中に入れています。 - ヘッダが読み取れない・短すぎる・不正なバイト列などで
ArgumentError等が発生しても、内部で捕捉してnilを返すようにした、という挙動に変更されています。 - つまり、「署名はついているがフレームが壊れていて LazyEntry すら作れない」ケースでも、キャッシュミスと同じ扱いに統一されます。
- 署名付きフレームのヘッダ(
テストの追加・回帰テスト
- 既存の「破損したペイロード」のテストケースに、「署名付きだがあまりにも短い(ヘッダが読めない)」ケースを追加しています。
- これにより、今後のリファクタで再び同様の例外が外に漏れるような変更が入った場合に検知できるようになっています。
テストコマンド:
BUNDLE_WITHOUT=db bundle exec ruby -w -Itest test/cache/cache_coder_test.rb- 影響範囲・注意点
影響範囲
ActiveSupport::Cacheを利用しているすべての箇所に潜在的な影響がありますが、挙動の方向性としては「過去に例外を投げていたケースを、キャッシュミスとして扱う」方向の緩和的変更です。- 特に、以下のような環境・ケースで影響を受ける可能性があります:
- 署名付きキャッシュ(Rails 6 以降のデフォルトキャッシュ形式など)を使っている
- キャッシュストアに何らかの理由で「壊れた/部分的な」バイト列が保存される可能性がある(バージョンアップ、キャッシュ共有、データ破損など)
- これまで
ArgumentError: @ outside of stringのような例外が時々発生していた
動作上の変化
- 従来: 壊れた署名付きフレーム →
Coder#load内で例外 → アプリ側に例外が伝播(キャッシュ読み込み時の例外として表面化) - 変更後: 壊れた署名付きフレーム → 例外を内部で捕捉 →
nilを返す → キャッシュミスと同じ挙動 - そのため、「壊れたキャッシュを検知したい」「あえて例外を発生させて監視している」といった特殊な運用をしていない限り、アプリケーションのロジックに悪影響はなく、むしろ安定性が向上します。
- 従来: 壊れた署名付きフレーム →
注意点
- これまでは例外により気付きやすかった「キャッシュデータの破損」が、静かにキャッシュミスとして扱われるようになります。
- キャッシュ破損を監視したい場合は、
ActiveSupport::Cache::Storeのラッパーを作成してログを仕込むなど、別途アプリ側で監視を実装する必要があります。 - 本変更は
ActiveSupport::Cache::Coderの内部挙動レベルの変更であり、通常の API (Rails.cache.read等) のインターフェースは変わりません。
- 参考情報 (あれば)
- 対象クラス:
ActiveSupport::Cache::Coder- 署名付きフレーム形式や
LazyEntryは、Rails 内部のキャッシュ最適化に関わる仕組みです。
- 署名付きフレーム形式や
- 類似挙動:
- 既に「未署名/レガシー形式のペイロード」については、デシリアライズエラーをキャッシュミスとして扱う処理があり、今回の変更はそのポリシーに「署名付きフレームパス」を揃えたものです。
- 実際に問題を踏んでいるか確認するには:
- アプリケーションログやエラートラッカーで
ActiveSupport::Cache::Coder#load起点のArgumentError: @ outside of stringなどを検索し、本 PR 適用後にそれが収まるかを確認できます。
- アプリケーションログやエラートラッカーで
#57511 Test to_sentence with an empty array
マージ日: 2026/6/1 | 作成者: @hammadxcm
概要 (1-2文で)
ActionView のto_sentenceが「空配列に対しては空のhtml_safeな文字列を返す」ことを確認するテストが追加された PR です。プロダクションコードの変更は一切なく、テストコードのみの追加です。変更内容の詳細
対象メソッド
ActionView::Helpers::TextHelper#to_sentence(正確には、Array#to_sentenceを ActionView が拡張したもの)- 既に以下のケースはテスト済みだった:
- 要素数 1 の配列
- 要素数 2 の配列
- 要素数 3 以上の配列
- 今回、新たに「要素数 0(空配列)」のケースがテストに追加された。
追加されたテストのポイント
厳密なコードはPR本文には載っていませんが、意図としては以下のようなテストがactionview/test/template/output_safety_helper_test.rbに 6 行ほど追加されています。想定されるテストイメージ(擬似コード):
rubydef test_to_sentence_with_empty_array_returns_empty_html_safe_string array = [].to_sentence assert_equal "", array assert_predicate array, :html_safe? endもしくは既存の output_safety 系テストに寄せた書き方として:
rubydef test_to_sentence_with_empty_array_is_html_safe result = [].to_sentence assert result.html_safe? assert_equal "", result endいずれにせよ、以下の2点を確認するテストが追加されています。
- 空配列に
to_sentenceを呼ぶと、空文字列 ("") が返る - その返り値は
html_safe扱いになる(ActiveSupport::SafeBuffer)
- 空配列に
- 影響範囲・注意点
実行時の挙動への影響:
- 本PRはテスト追加のみで、ライブラリ本体のコードは変更されていません。
- そのため、既存のアプリケーションの挙動は一切変わりません。
既存仕様の明文化:
- 空配列に対する
to_sentenceは空のhtml_safeな文字列を返す、という仕様がテストによって保証されました。 - これにより、将来の変更でうっかり
nilを返したり、非html_safeなStringを返すようなリグレッションが発生しにくくなります。
- 空配列に対する
開発者視点での注意点:
- ビューで
[].to_sentenceを使った場合、単に何も表示されない(空文字)だけでなく、XSSサニタイズ済みの安全なバッファとして扱われることが保証されます。 rawやhtml_safeを二重に呼ぶ必要はありません([].to_sentence.html_safeは不要)。- この仕様に依存しているコード(例:
content_tagにそのまま渡す、safe_joinと組み合わせる等)は、今後も同じ挙動が継続することがテストで担保されます。
- ビューで
- 参考情報 (あれば)
関連ドキュメント:
ActionView::Helpers::TextHelper#to_sentence
https://api.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-to_sentenceActiveSupport::SafeBuffer(html_safe文字列について)
https://api.rubyonrails.org/classes/ActiveSupport/SafeBuffer.html
類似テスト:
- 同ファイル
output_safety_helper_test.rbには、to_sentenceの要素数 1, 2, 3+ のケース、その他html_safeに関する挙動テストが既に含まれており、今回の追加はそのカバレッジ補完という位置づけです。
- 同ファイル
#57524 [ci skip] Update Ruby version requirement to 3.4 or newer in the Getting started guide
マージ日: 2026/6/1 | 作成者: @paul-louyot
- 概要 (1-2文で)
Rails公式ガイド「Getting Started」で推奨するRubyバージョンが「3.4以上」に引き上げられました。これにより、チュートリアル内でRuby 3.4以降で追加されたit構文を前提としたコード例を維持できるようにすることが目的です。
- 変更内容の詳細
- 対象ファイル:
guides/source/getting_started.md - 変更内容は1行のみで、ガイド内の「使用するRubyの推奨バージョン」の記述を更新しています。
例(イメージ):
- You will need Ruby 3.2 or newer...
+ You will need Ruby 3.4 or newer...PRの説明によると、この更新は以下を意図しています。
- Getting Started ガイド内では、新しいRubyのブロック内
it構文(例:パターンマッチなどの新構文)を使ったサンプルが含まれている。 - Rubyのバージョン要件が古いままだと、読者がそのままサンプルを実行した際にエラーになる可能性がある。
- チュートリアルのサンプルコードと、公式に案内するRubyバージョンを整合させるため、「Ruby 3.4以上」を必須(あるいは強く推奨)とする。
「it syntax inside blocks」という表現から、Ruby 3.4で導入された新構文(ブロック内のよりモダンな書き方)を前提としたチュートリアルコードを維持するための変更であると読み取れます。
- 影響範囲・注意点
- 対象はドキュメントのみで、Rails本体のコードや挙動には変更はありません。
- Getting Started ガイドに従って新規に環境構築するユーザー:
- Ruby 3.4未満だと、ガイドのコード例がそのままでは動作しない可能性があります。
- ガイドに沿う場合は、少なくともRuby 3.4へのアップグレードが推奨されます。
- 既存プロジェクト:
- 既にRuby 3.2や3.3などでRailsを使っているプロジェクトに対する強制的な変更ではありません。
- ただし、ガイドにある最新の構文を利用したい場合は、Ruby本体のバージョンアップを検討する必要があります。
- Railsのサポートポリシー:
- ガイドのRuby要件が3.4以上となることで、「Rails入門」を目的としたユーザーは、比較的新しいRubyを使うことが事実上の前提になります。
- CI設定などでRuby 3.4をまだ追加していない場合は、教育用途のサンプルや社内向けチュートリアルを更新する際にバージョン整合を取る必要があります。
- 参考情報 (あれば)
- 元Issue:
Fixes #57513
→ ガイドと実際の推奨Rubyバージョン・構文の不整合を解消するためのIssueと思われます。 - 変更種別: ドキュメントのみ (
[ci skip]がタイトルに付与されており、CI不要の軽微変更であることを示唆) - この変更は、Railsで新規に学習・開発を始めるユーザーが、Ruby 3.4の新構文を前提とした最新チュートリアルに沿いやすくするための調整と位置づけられます。
#57460 Fix Mysql2Adapter#discard! corrupting parent connection after fork
マージ日: 2026/6/1 | 作成者: @yahonda
- 概要 (1-2文で)
MySQL 用アダプタMysql2Adapter#discard!が、fork後の子プロセス終了時に親プロセス側の MySQL コネクションを壊してしまう不具合を修正する PRです。MYSQL_PREPARED_STATEMENTS=trueかつ mysql2 アダプタ使用時に、子プロセス終了時の finalize が親のソケットに書き込めないよう、子側のソケット FD を/dev/nullに差し替えるようにしました。
- 変更内容の詳細
問題の背景
MYSQL_PREPARED_STATEMENTS=trueの場合、Mysql2Adapterは SQL →Mysql2::Statementのキャッシュ(@statements)を持つ。forkすると、- MySQL クライアントのソケット FD(
Mysql2::Clientが内部で使うソケット) - そのクライアントに紐づく Ruby オブジェクト(
Mysql2::Statement等) が子プロセスに「コピー」される(FD は同じ番号を指す共有リソース)。
- MySQL クライアントのソケット FD(
既存コードでは、子プロセス側で
Mysql2Adapter#discard!を呼ぶときにclient.automatic_close = falseだけ行い、親とソケットを共有しているMysql2::Clientを「自動クローズしない」ようにする対応のみ行っていた。
しかし、
@statementsにぶら下がっているMysql2::Statementオブジェクトは、そのまま子プロセスのヒープ上に残る。- 子プロセスが
exitすると、そのStatementオブジェクトの finalizer が走り、COM_STMT_CLOSEをソケット FD に書き込む。 - そのソケット FD は親プロセスと共有しているため、その書き込みはサーバ側から見ると「まだ接続を使っているはずの親プロセスから
COM_STMT_CLOSEが飛んできた」ことになり、サーバがコネクションを閉じる。 - 結果として、親プロセス側が次のクエリを投げると
Lost connection to MySQL server during queryで落ちる。
- 子プロセスが
テスト上では test_forked_child_doesnt_mangle_parent_connection が MYSQL_PREPARED_STATEMENTS=true + mysql2 の構成で失敗し始めていた、というのが直接の発端です。
修正内容
Mysql2Adapter#discard! を、PostgreSQL アダプタの discard! と同じ方針に合わせて修正し、子プロセス側のソケット FD を /dev/null に付け替えるようにしました。
イメージとしては PostgreSQL 側でやっているこれと同じことを、mysql2 でも行います:
@raw_connection&.socket_io&.reopen(IO::NULL) rescue nilmysql2 の場合も同様に、子プロセスで discard! が呼ばれた段階でソケット IO を IO::NULL に reopen することで、
- 子プロセス内の
Mysql2::Statementオブジェクトの finalizer が走ってCOM_STMT_CLOSEを「送ろうとしても」、 - その書き込み先は
/dev/nullになっているため、実際の MySQL サーバには届かない
という状態になります。
これにより、親プロセスが保持している「本物の」コネクションは安全に維持されます。
PR 説明にある再現コードの意味
PR に記載の再現スクリプトは、Rails を介さずに問題そのものを説明しています。
重要なポイントだけ抜き出すと:
client = Mysql2::Client.new(...)
# prepared statement を作ってキャッシュに溜めるイメージ
statements = {}
statements["sql1"] = client.prepare("SELECT 1")
statements["sql1"].execute.to_a
statements["sql2"] = client.prepare("SELECT 2")
statements["sql2"].execute.to_a
pid = fork do
# 子では元の client を自動クローズしないようにする
client.automatic_close = false
# 子は新しく別 client を作ってクエリして exit
Mysql2::Client.new(...).query("SELECT 1").to_a
exit
end
Process.waitpid pid
# 親で再度クエリしようとすると接続ロスト
client.query("SELECT 1")
# => Mysql2::Error::ConnectionError: Lost connection to MySQL server during query子プロセス終了時に、statements で保持している Mysql2::Statement の finalizer が動き、その COM_STMT_CLOSE が親と共有している FD に書かれることで、親の接続が壊れる、という構造になっています。
ここで子側で FD を /dev/null に reopen しておけば、最後の client.query("SELECT 1") が成功するようになる、というのが修正の妥当性の確認です。
- 影響範囲・注意点
対象となるのは:
mysql2アダプタ使用時MYSQL_PREPARED_STATEMENTS=trueの構成forkを使うアプリケーション(例: prefork 型のアプリサーバ、バックグラウンドワーカーなど)
上記条件下で、
- 既存のテスト
test_forked_child_doesnt_mangle_parent_connectionが通るようになり、 - 子プロセスが
discard!を呼んだ後に exit しても、親プロセスの接続が壊れないことが保証されます。
- 既存のテスト
挙動としての変更点:
- 子プロセス側で
discard!が実行された後は、そのプロセスからは元の MySQL ソケットへは一切書き込みが行われなくなります(書き込み先は/dev/null)。 - これは「子は親のコネクションを使わない」という設計と一致しており、Rails 的には期待される動作です。
- 子プロセス側で
既存テスト:
- mysql2 アダプタに関する connection-adapter テスト一式 (178 tests)
forkとdiscard!関連の mysql2 テスト (6 tests) がすべてグリーンであることが確認されています。
アプリケーション開発者視点では、「fork する環境で mysql2 + prepared statements を有効にしていても、親コネクションが子終了時に壊れることはなくなった」と理解しておけば十分です。特別な設定変更は不要です。
- 参考情報 (あれば)
- 類似実装:
PostgreSQLAdapter#discard!@raw_connection&.socket_io&.reopen(IO::NULL) rescue nilによって、同様に子プロセス側で FD を/dev/nullに向けている。 - 関連コミット:
- fork セーフティを導入した過去コミット: f32cff5563
- CI での最初の検出:
#57509 Test ImmutableString custom boolean options and serialize
マージ日: 2026/6/1 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveModel::Type::ImmutableStringに関して、ドキュメントされている挙動(boolean 用のtrue:/false:オプションや、boolean / 数値 / symbol の文字列化)が実際に保証されるよう、テストを追加した PR です。プロダクションコードの変更はなく、テストコードのみの追加です。
- 変更内容の詳細
対象ファイル:
activemodel/test/cases/type/immutable_string_test.rb(+20/-0)
この PR で追加されたテストは、主に以下の点をカバーします。
(1) true: / false: オプション付きコンストラクタのテスト
ActiveModel::Type::ImmutableString は boolean 値のキャスト・シリアライズ時に、コンストラクタのオプションで文字列表現をカスタマイズできますが、その挙動がテストされていませんでした。
例(イメージコード):
type = ActiveModel::Type::ImmutableString.new(true: "YES", false: "NO")
type.cast(true) # => "YES"
type.cast(false) # => "NO"
type.serialize(true) # => "YES"
type.serialize(false) # => "NO"この PR では、上記のような true: / false: オプションを指定した場合に:
cast(true)/cast(false)serialize(true)/serialize(false)
の両方が指定した文字列に変換されることをテストで検証しています。
(2) デフォルトの boolean 文字列表現 "t" / "f" のテスト
オプションを指定しない場合のデフォルト挙動として:
type = ActiveModel::Type::ImmutableString.new
type.serialize(true) # => "t"
type.serialize(false) # => "f"となることがドキュメントされています。この PR は、このデフォルトの "t" / "f" という表現が確実に保証されるようテストを追加しています。
(3) 数値 / symbol の serialize のテスト
ImmutableString の serialize は、boolean 以外にも数値や symbol を受け取った場合に文字列化する分岐を持っていますが、その部分もこれまでテストされていませんでした。
テスト対象となる典型的な挙動のイメージ:
type = ActiveModel::Type::ImmutableString.new
type.serialize(123) # => "123"
type.serialize(3.14) # => "3.14"
type.serialize(:status) # => "status"これらのパスについてもテストを追加し、数値や symbol が期待どおり String に変換されることを確認しています。
- 影響範囲・注意点
影響範囲:
- プロダクションコードには一切変更がないため、挙動の変更や既存アプリへの影響はありません。
ActiveModel::Type::ImmutableStringの既存仕様(特に boolean, 数値, symbol の取り扱い)がテストでカバーされるようになり、将来的なリファクタリングや仕様変更時に意図しない互換性破壊が検出されやすくなりました。
注意点:
- 既にドキュメントに書かれている仕様をテストで「固定」している形になるため、今後
"t"/"f"以外のデフォルト表現に変えたい、あるいは boolean / 数値 / symbol の扱いを変えたい場合は、テストの更新と互換性への配慮が必要になります。 - カスタムの
true:/false:オプションに依存しているコードを書いている場合、その仕様が今後も守られることがテストで担保されるようになったと理解しておくとよいです。
- 既にドキュメントに書かれている仕様をテストで「固定」している形になるため、今後
- 参考情報 (あれば)
関連クラス:
ActiveModel::Type::ImmutableString- ActiveModel の型システムの一部で、
Stringを freeze(不変化)して扱うための型クラス。 - boolean, 数値, symbol を受け取った際にどのように文字列へ変換するかが、今回テストで明示的に押さえられました。
- ActiveModel の型システムの一部で、
想定ユースケース:
- Postgres の
textカラムを immutable に扱いたい場合や、ActiveRecord の attribute API でattribute :foo, :immutable_stringのように定義して boolean / 数値 / symbol を一貫した文字列表現にマッピングしたいケースなどで、この仕様とテストの存在が意味を持ちます。
- Postgres の
#57510 Test Float casting of Infinity and NaN strings
マージ日: 2026/6/1 | 作成者: @hammadxcm
- 概要 (1-2文で)
Rails のActiveModel::Type::Floatが"Infinity","-Infinity","NaN"を正しくFloat::INFINITY,-Float::INFINITY,Float::NANにキャストできることを確認するテストが追加されました。
本PRはテストコードのみの変更で、本番コードの挙動変更はありません。
- 変更内容の詳細
対象ファイル:
activemodel/test/cases/type/float_test.rb(+7行)
すでにドキュメント上は以下の仕様が記載されていました:
"Infinity"→Float::INFINITY"-Infinity"→-Float::INFINITY"NaN"→Float::NAN
しかし、この挙動を直接検証するテストが存在していなかったため、今回それらを明示的に検証するテストが追加されました。
おおまかには、以下のような内容のテストが追記されています(イメージコード):
def test_casts_special_float_strings
type = ActiveModel::Type::Float.new
assert_equal Float::INFINITY, type.cast("Infinity")
assert_equal(-Float::INFINITY, type.cast("-Infinity"))
assert_predicate type.cast("NaN"), :nan?
endポイント:
"Infinity"と"-Infinity"は通常の==比較で検証。NaNはNaN == NaNがfalseになるため、nan?メソッドなどで検証する形になっているはずです。
- 影響範囲・注意点
影響範囲:
- 追加されたのはテストのみであり、既存のキャスティングロジック(
ActiveModel::Type::Float#cast)自体には変更がありません。 - 既に
"Infinity","-Infinity","NaN"を前提にコードを書いていたアプリケーションに対して挙動変更はありません。
- 追加されたのはテストのみであり、既存のキャスティングロジック(
注意点:
- これらの文字列を DB カラム(float/decimal)に対する入力値として使う場合、ORM レイヤーでは
Floatになるものの、DB ドライバや DB 側がInfinityやNaNを受け付けるかは別問題です(DBごとの仕様に依存)。 - テストが入ったことで、将来的なリファクタリングで
"Infinity","-Infinity","NaN"のサポートが壊れた場合には速やかに検知されるようになりました。
→ これらの文字列を利用している場合、挙動が暗黙に変わるリスクが減ります。
- これらの文字列を DB カラム(float/decimal)に対する入力値として使う場合、ORM レイヤーでは
- 参考情報 (あれば)
- 対象クラス:
ActiveModel::Type::Float- ActiveModel の型キャスティング機能で、フォーム入力やパラメータ等を Ruby の
Floatに変換する役割を持つクラスです。
- ActiveModel の型キャスティング機能で、フォーム入力やパラメータ等を Ruby の
- Ruby の
Float特殊値:Float::INFINITY/-Float::INFINITYFloat::NAN(nan?で判定)
#57523 Fix typos and grammar in Form Helpers docs
マージ日: 2026/6/1 | 作成者: @VladNegara
概要 (1-2文で)
Action View の Form Helpers ガイド (guides/source/form_helpers.md) に含まれていた英語のタイポや文法ミスを修正するドキュメント専用のPRです。コードや挙動には一切手を加えておらず、ガイドの読みやすさ・正確さを向上させる内容です。変更内容の詳細(あればサンプルコードも含めて)
- 対象:
guides/source/form_helpers.mdのみ(+5/-5 行) - 内容の種類:
- 単純なスペルミスの修正
- 例: “recieve” → “receive”、“extenstion” → “extension” のような誤記修正
- 文法の調整
- 主語と動詞の不一致、冠詞 (a/the) の抜けや過多、前置詞の誤りなどを自然な英語に修正
- 長すぎて読みにくい文を適切に区切る、あるいは語順を入れ替えて読みやすくする
- 用語・言い回しの統一
- 既存の Rails ガイド全体の用語スタイルに合わせた表現へ微調整(例: “form helper” の単数/複数の揺れを統一、同じ概念に対する別表現を統一 など)
- 単純なスペルミスの修正
サンプルコードのロジックや API の説明内容自体は変えておらず、「文中の英語」だけが対象です。そのため、form_with, form_for, fields_for などの使い方やサンプルコードは従来どおりで、説明の語句がより正確・自然になっている、というタイプの修正です。
- 影響範囲・注意点
- 影響範囲:
- 実装コードには一切変更がないため、アプリケーションや既存コードへの影響はありません。
- 影響するのは、英語版の Rails ガイドを読む開発者のみです。
- 注意点:
- 「挙動の変更」や「推奨パターンの変更」は含まれていないため、これを理由にアプリケーションの修正を行う必要はありません。
- 実装や API の仕様が変わったわけではないので、Changelog というより「ドキュメントの品質向上」として理解しておけば十分です。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57523
- 修正対象のガイド:
- Action View Form Helpers ガイド (
guides/source/form_helpers.md)
- Action View Form Helpers ガイド (
- CI は
[ci skip]が指定されているため走っておらず、完全にドキュメント専用の変更であることが明示されています。
#57525 JSONGemCoderEncoder: serialize non-String keys with to_s instead of as_json
マージ日: 2026/6/1 | 作成者: @byroot
- 概要 (1-2文で)
Rails の ActiveSupport における JSONGemCoderEncoder が、ハッシュの非文字列キーをas_jsonではなくto_sでシリアライズするように戻されました。これにより、従来の JSON エンコーダと同じ振る舞いになり、#57520 で導入された挙動変更による非互換を解消します。
- 変更内容の詳細
何が変わったか
対象: ActiveSupport::JSON::Encoding::JSONGemCoderEncoder(ActiveSupport::JSON.encode などで使われるエンコーダ)
以前 (問題のあった状態)
ハッシュのキーが String 以外(シンボル、数値、オブジェクトなど)の場合、キーに対して
as_jsonが呼ばれていました。例:
rubyh = { foo: 1, 42 => 2 } # キー :foo / 42 に対して as_json が使われる
今回の PR での修正
- 非 String キーに対して
as_jsonではなくto_sを呼ぶように変更。 - これは「旧来のエンコーダが行っていた動作」に合わせたものです。
擬似コードイメージとしては、キーの処理が:
# 変更前(問題のあるイメージ)
hash.each_with_object({}) do |(k, v), result|
encoded_key = k.as_json # または JSON エンコードの途中で as_json が使われる
result[encoded_key] = v
endから:
# 変更後(この PR のイメージ)
hash.each_with_object({}) do |(k, v), result|
encoded_key = k.is_a?(String) ? k : k.to_s
result[encoded_key] = v
endのようなロジックに戻されています。
テストの追加
activesupport/test/json/encoding_test.rb に、以下のような振る舞いを検証するテストが追加されています(内容の要旨):
- ハッシュに非 String キー(例: シンボル、数値、独自オブジェクト)を含めて JSON エンコードしたとき、
- キーが
to_sで文字列化されること as_jsonによる予期せぬ変換を行わないこと
- キーが
- 影響範囲・注意点
影響範囲
- JSON エンコード時のハッシュのキーが対象です。
- 特に以下のようなコードを使っている場合に関係します:
ActiveSupport::JSON.encode(hash)hash.to_json(内部で同じエンコーダを使うケース)
- Rails の JSON レスポンスなどで、ハッシュをそのまま返している場合も間接的に影響します。
実務的な影響
互換性の回復 (後方互換)
- 旧来のエンコーダは非 String キーを
to_sしていたため、それと同じ挙動に「戻る」変更です。 - 直前の PR #57520 によって挙動が変わり、
as_jsonが呼ばれるようになったことで:- キーの型に依存した複雑な
as_json実装を持つアプリで、予期しないキー変換・エラーが発生していた可能性があります。
- キーの型に依存した複雑な
- 今回の PR により、その不意の互換性破壊が解消されるため、多くのアプリにとっては「バグ修正」であり、望ましい変更です。
- 旧来のエンコーダは非 String キーを
as_jsonに依存したキー変換をしていた場合- もし一時的に(あるいは意図的に)「キー側の
as_jsonが呼ばれる」ことを前提に実装していた場合、- この PR により、その前提は再び無効になります。
- 具体的には:
- 「キー側の
as_jsonをオーバーライドして JSON のキー表現を制御する」ようなトリッキーな実装は動かなくなります。
- 「キー側の
- 一般的には推奨されない使い方なので、多くのアプリには無関係ですが、該当する場合は注意が必要です。
- もし一時的に(あるいは意図的に)「キー側の
JSON のキーは文字列になる前提が明確に
- JSON 仕様上もオブジェクトのキーは文字列であるため、
- 「非 String キーは
to_sして文字列キーにする」という挙動は自然で、ライブラリ作者・クライアント実装側ともに扱いやすい挙動です。
- 参考情報 (あれば)
- 該当 PR:
- #57525: JSONGemCoderEncoder: serialize non-String keys with
to_sinstead ofas_json
- #57525: JSONGemCoderEncoder: serialize non-String keys with
- 修正対象の直前の PR:
- #57520: 挙動を変更し、非 String キーに
as_jsonを使うようにしてしまった PR(今回の修正でその部分が元に戻された)
- #57520: 挙動を変更し、非 String キーに
- 関連する観点:
- JSON のオブジェクトキーは文字列が前提であり、Ruby 側でシンボル・数値・オブジェクトなどをキーにしている場合も、クライアントからは常に文字列キーとして見えることを前提に設計すべきです。
#57521 Fix assert_part / assert_no_part for body parts nested under an attachment
マージ日: 2026/5/31 | 作成者: @55728
- 概要 (1-2文で)
ActionMailer のテスト用アサーションassert_part/assert_no_partが、添付ファイル付きメールで本文パートを正しく検出できない問題を修正した PR です。Mail#partsではなくMail#all_partsを使うことで、multipart/mixed配下にネストされたmultipart/alternative内の text/html パートも検査対象に含めるよう変更されています。
- 変更内容の詳細
問題の背景
ActionMailer::TestCase#assert_part / #assert_no_part(まだ未リリースの新機能)は、メールの MIME パートを検査するためのテスト用ヘルパーです。
しかし、元実装では以下のようなコードでパートを検索していました:
part = [*mail.parts, mail].find { |part| mime_type.match?(part.mime_type) }ここで使っている Mail#parts は「直下の子パートのみ」を返します。
一方、Action Mailer で「text と html の両方の本文 + 添付ファイル」があるメールを送ると、構造は次のようになります:
multipart/mixed ← 最上位
├── multipart/alternative
│ ├── text/plain ← 本文の text パート
│ └── text/html ← 本文の html パート
└── image/png ← 添付ファイルmail.parts で見えるのは multipart/alternative と image/png だけで、その1段下にぶら下がっている text/plain と text/html は見えません。
そのため:
assert_part :html/assert_part :text
→ 実際には存在するのに検出できず、テストが失敗(false negative)。assert_no_part :html/assert_no_part :text
→ 実際にはパートが存在するのに「見えていない」ため、テストが通ってしまう(false positive)。
特にこちらは「html を出さないはず」という保証テストが意味をなさなくなり、危険。
修正内容
Mail#parts ではなく、ネストされたパートも含めてフラットに列挙する Mail#all_parts を使うように修正されています。これは既に ActionMailer::InlinePreviewInterceptor でも使われている手法です。
修正後のコード(概念的には以下のような変更):
# 変更前
part = [*mail.parts, mail].find { |part| mime_type.match?(part.mime_type) }
# 変更後
part = [*mail.all_parts, mail].find { |part| mime_type.match?(part.mime_type) }この変更が assert_part と assert_no_part 両方に適用されています。
これにより、multipart/alternative の下にネストされた本文パートも検索対象に含まれます。
再現用テスト
actionmailer/test/assert_select_email_test.rb に、新しいテストケース AssertMultipartWithAttachmentEmailTest が追加されています。
テスト用メールは以下のように組み立てられます:
attachments["example.png"] = { mime_type: "image/png", content: "PNGDATA" }
mail(...) do |format|
format.text { render plain: options[:text] } if options.key?(:text)
format.html { render plain: options[:html] } if options.key?(:html)
end追加されたテスト:
test_assert_part_finds_body_parts_nested_under_attachmentassert_part :textとassert_part :htmlで、添付ファイルの存在によってネストされた本文パートが正しく見つかることを確認。
test_assert_no_part_detects_body_parts_nested_under_attachment- 対応するパートが存在する状態で
assert_no_partを呼び出したとき、必ず失敗(例外を送出)することを確認。
- 対応するパートが存在する状態で
修正前の main では:
assert_no_partが例外を投げずに通ってしまう(false positive)assert_partがパートを見つけられない(false negative)
という想定通りの「赤」を再現し、修正後はテストファイル全体がグリーンになることが確認されています。
- 影響範囲・注意点
影響範囲:
ActionMailer::TestCase#assert_part/#assert_no_partの挙動のみ(いずれも未リリース API)。- 実際のメール送信や本番環境でのメール構造には影響しません。
- Action Mailer で MIME パートを扱う既存コードのうち、
InlinePreviewInterceptorは既にall_partsを使っており、本 PR と整合的です。
注意点:
- もともとの実装では「トップレベルのパートだけを対象にする」という仕様にも読み取れるが、添付ファイルを追加しただけで本文パートが検査できなくなるのは、テスト API として直感的でなく危険な挙動です。この PR により、開発者の期待に近い「メール全体の MIME パートを対象に検査する」動きに揃えられます。
- API は 8.2.0.alpha 時点で未リリースのため、既存アプリの互換性リスクはほぼゼロです。リリース前に false positive/negative を潰せた形になります。
- 参考情報 (あれば)
- 対象メソッド:
ActionMailer::TestCase#assert_partActionMailer::TestCase#assert_no_part
- 関連 PR:
- 元機能追加: #55348 (
assert_part/assert_no_part追加)
- 元機能追加: #55348 (
- 類似の
all_parts利用箇所:ActionMailer::InlinePreviewInterceptor(inline_preview_interceptor.rb:56)
#55555 Rate limiting calls cache_key on by: if the object responds to it
マージ日: 2026/5/31 | 作成者: @daniel-sabourin
- 概要 (1-2文で)
Rails のActionController::RateLimitingにおいて、rate_limitのby:オプションに任意オブジェクトを渡せるようにし、そのオブジェクトがcache_keyを実装していればそれを自動的に使ってレート制限用のキャッシュキーを生成できるようにした PR です。これにより、IP アドレスではなく「ユーザーごと」など、より柔軟な単位でのレート制限が行いやすくなります。
- 変更内容の詳細
背景・モチベーション
- これまでの Rails のレートリミットは、デフォルト設定では
remote_ipに基づくケースが多く、「同じネットワーク(同じ IP)からの複数ユーザー」がいる環境では不都合が出ることがある。 - 認証済みユーザーがいる場合、IP 単位ではなく「ユーザー単位」でレートリミットをかけたいが、その際に毎回
cache_keyを明示的に呼ぶのは煩雑。 - Active Record オブジェクトなど「
cache_keyを持つオブジェクト」をそのままby:に渡せれば、コントローラ側の記述をシンプルにできる。
実際の仕様変更
変更前(従来の書き方の例)
「ユーザーごとのレートリミット」をしたい場合は、自分で cache_key を呼び出す必要がありました。
class CommentsController < ActionController::API
# キャッシュキー: "rate-limit:comments:user/1" (current_user の cache_key )
rate_limit to: 2, within: 2.seconds, by: { current_user.cache_key }
private
def current_user
User.find(1)
end
end変更後(この PR で可能になる書き方)
by: に任意のオブジェクトを渡せるようになり、そのオブジェクトが cache_key に応答する場合は内部で cache_key が呼ばれます。
class CommentsController < ActionController::API
# キャッシュキー: "rate-limit:comments:user/1" (current_user.cache_key が内部的に呼ばれる)
rate_limit to: 2, within: 2.seconds, by: { current_user }
private
def current_user
User.find(1)
end
endポイント:
by:に渡されたオブジェクトがrespond_to?(:cache_key)の場合、そのcache_keyを使ってストア上のキャッシュキーが組み立てられるようにrate_limiting.rbが修正されています。cache_keyを持たないオブジェクトや、文字列・数値などは従来どおりの取り扱い(=そのままキーに使われるか、既存のロジックに従う)と推測されます。- テスト (
rate_limiting_test.rb) が追加・更新され、cache_key対応オブジェクトをby:に渡したときの挙動がカバーされています。 actionpack/CHANGELOG.mdにこの挙動変更が追記されています。
- 影響範囲・注意点
既存コードへの互換性
- 既存で
by: { current_user.cache_key }のように書いているコードは、そのまま動作します(挙動は変わりません)。 by:にオブジェクトを渡していて、かつそのオブジェクトがすでにcache_keyを持っていた場合は、今回の変更により「cache_keyが自動で使われる」ようになります。
ただし、これまでにそういった使い方をしていたケースは少ないと考えられ、互換性リスクは比較的小さいと思われます。
- 既存で
キャッシュキーの安定性
cache_keyは通常 Active Record モデルではmodel_name/id-更新時刻形式になり、更新のたびに変わることがあります。- レートリミットの粒度として「ユーザーごと」で安定したキーを持ちたい場合、
Userモデルのcache_keyが更新タイミングで変わることを理解しておく必要があります(通常はそれでも「同じユーザー」であればキーは論理的には同じドメインに属するので問題になりづらいですが、モデル定義によっては注意が必要)。 - 完全に安定したキー(例:
user:<id>固定)を使いたい場合は、ユーザー側でcache_keyを上書きするか、従来どおりby: { "user:#{current_user.id}" }といった文字列を明示する選択肢もあります。
パフォーマンス
cache_keyを呼ぶコストが追加されますが、多くのアプリでは無視できる程度です。大量リクエストでレートリミットが多用されるケースでは、cache_keyの実装が高コストでないかを確認しておくと安心です。
設計上のメリット
- 認証ユーザー単位や、テナント単位など、
cache_keyを実装したドメインオブジェクトごとにレートリミットを簡潔に適用できるようになり、「IP 単位レートリミットからの脱却」がしやすくなります。 - プロジェクト全体で統一した書き方(
by: { current_user })ができ、cache_key呼び出しを各所にバラバラに散らさなくてよくなります。
- 認証ユーザー単位や、テナント単位など、
- 参考情報 (あれば)
変更ファイル:
actionpack/lib/action_controller/metal/rate_limiting.rbby:で渡されたオブジェクトに対し、respond_to?(:cache_key)チェックを行い、true の場合はcache_keyを用いて内部キャッシュキーを生成するロジックが追加。
actionpack/test/controller/rate_limiting_test.rbcache_keyを持つオブジェクトをby:に渡した場合のレート制限動作を確認するテストが追加。
actionpack/CHANGELOG.mdrate_limitのby:にオブジェクトを渡した際の新挙動が記載。
レートリミットをユーザー単位にしたい場合の典型的な形:
rubyclass ApplicationController < ActionController::API # 例: 各ユーザー 1 分あたり 60 回まで rate_limit to: 60, within: 1.minute, by: { current_user } def current_user # Devise 等で取得 super end end
#55388 Respect schema_search_path on rails dbconsole for Postgres
マージ日: 2026/5/31 | 作成者: @sobrinho
- 概要 (1-2文で)
PostgreSQL を使う際に、rails dbconsoleがconfig/database.ymlのschema_search_path設定をこれまで無視していた問題を修正し、アプリケーションと同じ search_path でpsqlが起動するようにした PR です。Rails の ActiveRecord 接続と手動で使うrails dbconsoleの挙動を揃える変更です。
- 変更内容の詳細
何をしたか
rails dbconsole(アダプタ: PostgreSQL)がpsqlを起動する際に、config/database.ymlのschema_search_pathをPGOPTIONSに反映するようにしました。- 具体的には、
ActiveRecord::ConnectionAdapters::PostgreSQLAdapterの dbconsole 用ロジックで、PGOPTIONS="--search_path=..."を環境変数として付与してpsqlを呼び出すようになっています。 - これにより、Rails アプリ内の接続で使われている search_path と、
rails dbconsoleで開かれるpsqlセッションの search_path が一致します。
イメージ・サンプル
config/database.yml で以下のように search path をカスタムしているケースを想定します:
production:
adapter: postgresql
database: myapp_production
username: myuser
password: secret
schema_search_path: myschema,public従来:
- Rails アプリの DB 接続:
search_path = myschema, public rails dbconsole→psql:- search_path デフォルト:
$user,public - 手動で
SET search_path TO myschema,public;する必要があった
- search_path デフォルト:
変更後:
rails dbconsole実行時、内部的に:PGOPTIONS="--search_path=myschema,public" psql ...
- その結果、
psqlセッションも自動的にmyschema,publicが search_path に設定された状態で開く
コード上のポイント
※PR本文から読み取れる範囲の挙動:
postgresql_adapter.rbで dbconsole 実行用のパラメータを構築する箇所に、schema_search_pathが存在する場合にPGOPTIONSを組み立てる処理が追加されています。- 追加されたテスト (
activerecord/test/cases/adapters/postgresql/dbconsole_test.rb) では、schema_search_pathを設定した接続設定を用意dbconsole用のコマンド・環境変数を生成PGOPTIONSに"--search_path=<schema_search_path>"が含まれていること を検証しています。
activerecord/CHANGELOG.mdにもこの挙動変更(機能追加)が追記されています。
- 影響範囲・注意点
主な影響範囲
- 対象: PostgreSQL を使っていて、かつ
schema_search_pathをconfig/database.ymlで指定している Rails アプリ のrails dbconsole実行時の挙動。 - 今後は
rails dbconsoleで開くpsqlセッションが、Rails アプリと同じ search_path を自動的に利用します。
開発者視点でのポイント
- これまで
rails dbconsole後に毎回SET search_path TO ...を手で打っていた場合、その手順が不要になります。 - 逆に「dbconsole は always デフォルト search_path で開きたい」といった前提のスクリプトや運用がある場合、挙動が変わる点に注意が必要です(例: ログイン直後の search_path を前提にした診断スクリプトなど)。
schema_search_pathが設定されていないプロジェクトには影響しません(従来どおりデフォルトの$user,public)。
環境変数の扱い
- すでに
PGOPTIONSを独自に使っている場合、この PR の実装次第では:- 既存の
PGOPTIONSに--search_path=...を追加する - あるいは
schema_search_pathがあるときにPGOPTIONSを上書きする いずれかになります。
実装上は「上書き」か「追記」かが重要ですが、PR説明文からは主に「search_pathをPGOPTIONSにセットする」ということだけが明示されているため、既存のPGOPTIONS活用をしている場合は、マージ後の挙動を一度確認した方が安全です。
- 既存の
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/55388
- 関連する設定項目:
config/database.ymlschema_search_path: ActiveRecord が PostgreSQL に接続するときのsearch_pathを制御
- PostgreSQL 公式ドキュメント(
PGOPTIONS/search_path):PGOPTIONS環境変数を使うと、psql等クライアント起動時にサーバへのオプション(--search_path=...など)を渡せるsearch_pathはスキーマ解決順序を制御し、テーブル名などの解決対象スキーマを切り替えるために使われる