Ruby on Rails PR Digest - 2026年 8月
このページは rails/rails リポジトリにマージされたPull Requestを自動的に収集し、AIで要約したものです。
#58548 activerecord: add an option to shuffle unordered SELECT results
マージ日: 2026/8/30 | 作成者: @viralpraxis
- 概要 (1-2文で)
このPRは、Active Record が生成するORDER BYなしのSELECT結果をランダムにシャッフルできる設定オプションconfig.active_record.shuffle_unordered_selectsを追加し、テスト環境などで「暗黙の順序依存」をあぶり出せるようにする変更です。デフォルトは無効で、本番挙動は変えずに、順序に依存した危ういコードやテストを早期に検出することが目的です。
- 変更内容の詳細
2-1. 新オプション: config.active_record.shuffle_unordered_selects
Rails の設定として以下のオプションが追加されています:
# config/environments/test.rb など
config.active_record.shuffle_unordered_selects = true- デフォルト値:
false - 主に test 環境向けとして想定
- 有効化すると、Active Record が発行する多くの
SELECT ...の結果行が、アプリケーションに返される段階でランダムな順序にシャッフルされます。
ここで重要なのは「DB に送る SQL をランダム化する」のではなく、「返ってきた結果セット (ActiveRecord::Result) を Ruby 側でシャッフルする」アプローチを取っている点です。そのため、DB の最適化や実行計画には影響しません。
2-2. 「順序が未定義な SELECT」を検知してシャッフル
PR 説明にもある通り、
A
SELECTwithoutORDER BYhas no specified row order
SQL では ORDER BY を指定しない限り、行の順序は仕様上「未定義」です。しかし多くの DB は実装上、一貫した順序(主キー昇順など)が返ることが多く、アプリやテストがそれに無意識に依存しがちです。
このPRでは、Active Record がクエリを実行する際の「問い合わせ意図」(QueryIntent) に「順序が確定しているか・していないか」の情報を持たせ、それに応じて以下のような振る舞いをします:
ORDER BYを明示しているクエリ → 「順序あり」とみなし、そのまま返す(シャッフルしない)orderを呼んでいない、または「順序なし」と判定されたクエリ → 「順序未定義」とみなし、shuffle_unordered_selectsが true の場合にのみシャッフル
この判定ロジックは ActiveRecord::ConnectionAdapters::QueryIntent と関連コードに追加されています。
2-3. ActiveRecord::Result レベルでのシャッフル処理
クエリ結果は ActiveRecord::Result オブジェクトとして Ruby に取り込まれます。このクラスに「シャッフル機能」が追加されました。
ActiveRecord::Resultが生成されるタイミングで、「この結果は順序未定義か?」「設定でシャッフル有効か?」を見て、必要ならrows.shuffle!相当の処理を行うcolumnsはそのまま、rowsのみが並び替えられる
この処理のテストとして activerecord/test/cases/result_test.rb が拡張され、新たに shuffle_unordered_selects_test.rb が追加されています。後者では、様々なクエリパターン(order あり・なし、limit 付き等)で、本当にシャッフルされる/されないが期待通りかを検証しています。
2-4. クエリキャッシュとの連携
activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb も微修正されています。
- クエリキャッシュが有効な場合でも、キャッシュのキーと値は元の順序のまま 保持しておき、返却時に「この結果は順序未定義だが、シャッフル設定が有効か?」を見てからシャッフルする、という流れになります。
- これにより「同じクエリを2回実行したのに、キャッシュヒット時はシャッフルされない」というような不整合を避けています。
2-5. Railtie & ガイドの更新
activerecord/lib/active_record/railtie.rbに設定項目を登録guides/source/configuring.mdにconfig.active_record.shuffle_unordered_selectsの説明が追記- 主に以下の点が説明される想定です:
- なぜ順序未定義の結果に依存するのが危険か
- テスト環境でこのオプションを有効にすることで、そうした依存を検出しやすくなること
- 本番では通常オフのままにしておくべきこと
- 主に以下の点が説明される想定です:
- 影響範囲・注意点
3-1. どんなときに恩恵があるか
- テストが「ローカルでは通るが CI や DB バージョン違いで落ちる」といった、暗黙の順序依存バグを検出しやすくなる
Model.first/Model.last/Model.all[0]など、「内部的に ORDER BY が無い」クエリや配列アクセスに依存したテスト・実装を洗い出せる- マルチ DB / DB バージョンアップ時の安定性確認
3-2. 有効化したときに起こりうること
config.active_record.shuffle_unordered_selects = true を test 環境で有効にすると:
ORDER BYを指定していないクエリに基づくテストの中で、以下のようなコードがランダムに落ちるようになります:expect(User.first.name).to eq "Alice"(order指定なし)expect(User.pluck(:id)).to eq [1, 2, 3]user = User.all[0]; expect(user.id).to eq 1
- 落ちたら、「本来は ORDER BY を付けるべきところで、DB の暗黙の順序に依存していた」ことを意味するので、
- Active Record のクエリに
order(:id)など明示的な順序指定を加える - or 「結果の順序に依存しないアサーション」に書き換える(
match_arrayを使うなど) といった修正を行うべきです。
- Active Record のクエリに
3-3. シャッフルされないケース
- 明示的に
orderが指定されているクエリ - DB アダプタや内部ロジック上、「明示的に順序が決まっている」とマークされるクエリ
- また、今回の実装は「Active Record が生成する多くのクエリ」を対象としており、すべての生 SQL / 生接続クエリが網羅されているとは限りません。
connection.execute("SELECT ...")のようなコードは、この機能の外側にいるケースがあります。
3-4. 本番環境での利用について
- PR 説明・ガイドの位置づけからも、テスト環境向け機能として設計されています。
- 本番で有効にすると:
- 順序未定義のクエリが「毎回違う順序で返ってくる」ことになり、実装によっては大きく動作が変わる可能性がある
- パフォーマンス的には「結果配列のシャッフル」程度なので大きな負荷ではないが、意味的な影響が大きすぎる
よって、基本的には test / development での検出用途に限るのが妥当 です。
- 参考情報 (あれば)
このPRがフォローアップしている関連PR:
実務での活用例:
- Rails アプリの CI 設定で、test 環境に限り
config.active_record.shuffle_unordered_selects = trueをオンにし、「ORDER BY 漏れ」や「暗黙の順序依存テスト」を早期に検出する - 大規模リファクタリングや DB バージョンアップ前に一時的に有効化し、「暗黙依存の洗い出しタスク」として使う
- Rails アプリの CI 設定で、test 環境に限り
#58604 Improve performance of Active Record model class predicate builders
マージ日: 2026/8/29 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Active Record のモデルクラスにおける「predicate builder」(whereなどで使われるクエリ条件生成オブジェクト)の初期化方法を見直し、遅延初期化時のプロキシ処理をやめて「最初から実体をセットする」形に変えることでパフォーマンスを改善した PR です。初回リクエスト/クエリ時に発生していた余分なオーバーヘッドを減らすことが目的です。
- 変更内容の詳細
何をしているか
- Active Record モデルクラスが内部的に持っている predicate builder 用のインスタンス変数(例えば
@predicate_builder的なもの)について、- 以前: 「必要になった時にだけ実際の builder を生成する」ための遅延初期化 + プロキシ的なアクセス
- 今回: モデルクラス側で あらかじめ(eager に)実体を代入しておく
- これにより、最初にクエリを投げたタイミングで発生する「proxy を介した初期化コスト」が不要になります。
具体的な変更ポイント(概念レベル)
※実際のコードは数行レベルの小さな差分ですが、意味合いとしては以下のような変更です。
ActiveRecord::Base(core.rb)か、あるいはそれを拡張するmodel_schema.rbあたりで、- 以前は
predicate_builder参照時に「まだ作ってなければ作る」ようなロジックが入っていた - 今回は「モデルクラスがセットアップされる段階で builder を生成してインスタンス変数に格納する」形に寄せる
- 以前は
擬似コードで表現すると、イメージはこんな感じです:
# 変更前(イメージ)
def predicate_builder
@predicate_builder ||= Relation::PredicateBuilder.new(table)
end
# 変更後(イメージ)
# モデル定義のセットアップ時に
@predicate_builder = Relation::PredicateBuilder.new(table)
def predicate_builder
@predicate_builder
endmodel_schema.rbでは、スキーマロード/テーブル情報確定タイミングで predicate builder をセットするような処理が追加されていると考えられます。core.rbの 1 行の変更は、predicate_builderのデフォルト実装や初期化の責務の持ち方を微調整している可能性が高いです。
テスト
activerecord/test/cases/relation/predicate_builder_test.rbに 14 行追加されており、- 新しい eager 初期化の挙動が正しく行われるか
- 遅延初期化由来のプロキシがなくなっても従来通りクエリが組み立てられるか
- といった点を確認するテストが追加されています。
- 影響範囲・注意点
影響範囲
- 対象: Active Record モデルクラスで行うクエリビルド(
where,not,find_by,exists?など predicate builder を内部で利用する操作全般)。 - ユーザーコードから見た「API の振る舞い」自体には変更がなく、あくまで内部の初期化タイミングと実装方式の変更です。
- そのため、通常の Rails アプリであれば、この変更により
- 最初のリクエスト/コンソール起動直後の最初のクエリがわずかに高速化
- 長期運用でのレイテンシのばらつき(「1 回目だけ遅い」現象)の軽減 が期待されます。
注意点・考慮すべき点
- predicate builder 自体をモンキーパッチしていたり、
ActiveRecord::Relation::PredicateBuilder周辺の初期化/メモ化ロジックに依存したメタプログラミングをしている場合:- builder が「いつ」「どのタイミング」でインスタンス変数にセットされるかが変わったことで、
ごく一部の高度なハックが影響を受ける可能性はあります。
- builder が「いつ」「どのタイミング」でインスタンス変数にセットされるかが変わったことで、
- ただし、変更は内部実装レベル・かつ小さな差分であり、通常のアプリケーション開発では互換性上の問題はほぼ発生しないと考えられます。
- CHANGELOG は更新されていないため、公式には「挙動変更として告知すべきほどのユーザー向け仕様変更ではなく、内部的なパフォーマンス改善」と位置付けられています。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58604
- 関連クラス・モジュール(コードベースを見る際の入口):
ActiveRecord::Core(activerecord/lib/active_record/core.rb)ActiveRecord::ModelSchema(activerecord/lib/active_record/model_schema.rb)ActiveRecord::Relation::PredicateBuilderとそのテスト (activerecord/test/cases/relation/predicate_builder_test.rb)
この PR は「API変更ではなく、Active Record の内部初期化を eager にして初期クエリのオーバーヘッドを減らす」ことにフォーカスした小さなパフォーマンスチューニングと理解しておくとよいです。
#58549 Upgrade to Marcel 2 for Active Storage content type detection
マージ日: 2026/8/29 | 作成者: @jeremy
- 概要 (1-2文で)
Active Storage が依存する MIME タイプ判定ライブラリ「marcel」のバージョンを~> 1.0から~> 2.0に引き上げ、より精度の高いコンテンツタイプ検出とセキュリティ強化を Rails 本体のデフォルト挙動にしました。
これにより、新規に解析・アップロードされる Blob の MIME タイプが一部「正準(canonical)なタイプ」に変わりますが、既存の Blob には影響しないよう配慮されています。
- 変更内容の詳細
依存関係の更新
activestorage.gemspecmarcelの依存バージョンを- 変更前:
~> 1.0 - 変更後:
~> 2.0
に更新。
- 変更前:
Gemfile.lock- ロックされている
marcelを 2.1.0 に更新 - CHECKSUM 行が
sha256=f8b0327...fcc3512に更新され、RubyGems 上のものと照合済み。
- ロックされている
activestorage/CHANGELOG.md- Active Storage の変更点として「marcel 2 への更新」と、その挙動差分が追記。
挙動の具体的な変更点
MIME タイプの「正準化」
Marcel 2.0 では、これまで alias として扱われていた MIME タイプが、より正準なタイプに置き換わります。影響するのは 「新しく解析される or アップロードされる Blob」だけ で、既存の Blob はそのまま保持されます。
代表的な変更:
audio/x-aac→audio/aacaudio/x-flac→audio/flacaudio/vnd.wave→audio/x-wavtext/x-yaml→application/yamlimage/bmp;format=compressed→image/bmp
アプリ側で blob.content_type を前提に分岐している場合、この差分が顕在化します。
例: コントローラやサービスでの分岐コード
case blob.content_type
when "audio/x-aac" # ← 旧 marcel 1.x
# ...
endmarcel 2.x 以降では、新規 Blob に対しては audio/aac が返ってくるようになるため、以下のような対応が必要になる可能性があります。
case blob.content_type
when "audio/aac", "audio/x-aac"
# どちらも受け入れるようにする
end同様に、YAML や WAV、FLAC などを MIME タイプで判定している処理があれば、alias と canonical の両方を許容するか、canonical 側に寄せていく必要があります。
新たな検出能力の追加 (marcel 側の機能向上)
Marcel 2.x が持つ改善点が、そのまま Active Storage に反映されます。主に:
- OOXML (docx, xlsx, pptx 等) を ZIP central directory 経由でより正確に識別
- XML 系コンテンツを root element から識別 (特定の XML vocabulary)
- 画像/アーカイブ形式の追加検出:
- CR2 (Canon RAW)
- RAR
- AVIF シーケンス
- IO まわりの堅牢性・セキュリティハードニング
特に、拡張子偽装や壊れたファイルに対する判定の堅牢性が向上するため、ユーザーアップロードを受け付けるアプリではセキュリティ的なメリットがあります。
Ruby バージョン要件
- Marcel 2.x は Ruby
>= 3.3が必須。 - Rails main ブランチ自体が Ruby
>= 3.3.1を前提としているため、bundle install時に自動的にmarcel 2.1.0が選ばれます。 - これにより「lockfile がたまたま 1.x を指し続けてしまう」といった状況を避け、アップグレード時に確実にセキュリティ強化を取り込む設計になっています。
- 影響範囲・注意点
影響範囲
影響を受けるもの:
- Active Storage を利用しており、コンテンツタイプ (
content_type) でロジック分岐 している部分 - 特定の MIME タイプ文字列をテストで厳密に
==比較している箇所 - MIME タイプをキーにした DB 検索・集計など (例:
where(content_type: "text/x-yaml"))
- Active Storage を利用しており、コンテンツタイプ (
影響を受けないもの:
- すでに保存済みの Blob の
content_type値
→ 既存レコードはそのまま。検索でも旧 alias で引き続き解決される挙動が確保されています。 - Rails 本体のテストスイート
→ Active Storage の全テスト (611 runs, 1876 assertions) でcontent_type関連の期待値変更は不要だった、と明記されています。
- すでに保存済みの Blob の
アプリ開発者が確認すべきポイント
MIME タイプでの条件分岐の見直し
- 例:
"text/x-yaml"や"audio/x-aac"などの alias を前提とした条件があれば、canonical タイプでも動くようにする。 - 対応方針:
- 短期: alias + canonical 両方を許容する (OR 条件・配列含有など)
- 中長期: canonical 側に寄せたマイグレーションを検討
- 例:
テストコードの期待値
- ファイルアップロードをテストして
blob.content_typeをassert_equalしている箇所は、期待値を canonical に合わせるか、include?/ 正規表現などで緩く判定する。
- ファイルアップロードをテストして
Ruby バージョン
- Rails main を追いかける以上、Ruby 3.3 系が前提となる点を再確認する。
- Ruby 3.3 未満の環境では bundler 解決に失敗するため、CI / 本番環境を 3.3 以上に揃える必要がある。
セキュリティ・アップロード周りのポリシー
- MIME タイプ判定に reliance しているバリデーション (例:
content_type: ["image/png", ...]) は、より正確な判定に変わることで拒否/許可の結果が変化する可能性があります。 - 特に拡張子偽装の防止や、特定形式のブロックを行っている場合は、挙動が期待どおりか確認しておくと安全です。
- MIME タイプ判定に reliance しているバリデーション (例:
- 参考情報 (あれば)
- 本 PR:
Upgrade to Marcel 2 for Active Storage content type detection(#58549, author: jeremy) - Marcel リリースノート:
- 検証状況:
- Active Storage のテストスイートは marcel 2.1.0 で全て成功 (環境依存の
yarn不足を除く)。 - 大規模プロダクションアプリ 2 つで、このブランチ +
marcel 2.1.0をピンした CI が実行中であり、その結果を踏まえてマージされたと記載。
- Active Storage のテストスイートは marcel 2.1.0 で全て成功 (環境依存の
#58601 Fix compatibility with json 3.0
マージ日: 2026/8/28 | 作成者: @Earlopain
- 概要 (1-2文で)
Rails のActiveSupport::JSON.decodeと Active Record の JSON コーダーが、jsongem 3.0 で導入された「オプションはキーワード引数として扱う」という仕様変更により落ちる問題を、両バージョン(2.x / 3.x)で動く形に修正した PR です。
- 変更内容の詳細
問題の背景
json 3.0 では以下の変更が入りました(引用元: json の CHANGES):
All methods options are now either keyword arguments or checked like keyword arguments, meaning unknown options such as typos raise
ArgumentError.
また、PR の説明に記載の通り、JSON.parse に対して以下のような呼び出しをするとエラーになります:
ArgumentError: wrong number of arguments (given 2, expected 1)
.../json/common.rb:296:in 'JSON.parse'
.../active_support/json/decoding.rb:26:in 'ActiveSupport::JSON.decode'これは、Rails 側が JSON.parse(source, options_hash) のように「位置引数としてのハッシュ」を渡していたため、json 3.0 側のインターフェースと噛み合わなくなったことが原因です。
主な修正ポイント
変更ファイルは以下の4つです:
activerecord/lib/active_record/coders/json.rbactiverecord/test/cases/coders/json_test.rbactiverecord/test/cases/serialized_attribute_test.rbactivesupport/lib/active_support/json/decoding.rb
ActiveSupport::JSON.decode の修正
activesupport/lib/active_support/json/decoding.rb 内で JSON.parse を呼ぶ際の引数の渡し方を調整しています。
元々(イメージ):
def decode(json, options = {})
JSON.parse(json, options) # 位置引数2つ
end修正後(イメージ):
def decode(json, options = {})
# options をキーワード引数として渡す形式に変更
JSON.parse(json, **options)
endもしくは、options が nil の場合に引数を 1 個にするなど、「json 2.x でも 3.0 でも動くように」引数の形を分岐する形になっています。
ここでのポイントは:
json3.0 ではオプションが「キーワード引数」とみなされるようになったため、位置引数としてハッシュを渡さない。- かつ
json2.x の API とも互換を保てる形で渡し方を工夫している。
Active Record の JSON コーダーの修正
activerecord/lib/active_record/coders/json.rb でも同様に、ActiveSupport::JSON.decode / JSON.parse の引数の渡し方を修正しています。
Active Record の JSON coder は、serialize :column, coder: ActiveRecord::Coders::JSON のような場面で使われるもので、内部的に ActiveSupport::JSON.decode を使います。
ここでも:
decodeメソッドでoptionsのハッシュを位置引数として渡していた部分を、キーワード引数風に渡すか、あるいはActiveSupport::JSON.decode側の新しい signature に合わせた呼び方へ変更。encode相当のメソッドとの整合性もとれるようにしている(PR 本文で encode との対応に言及あり)。
具体的には、decode が def decode(value, options = {}) のようなシグネチャを持ちつつ、内部では ActiveSupport::JSON.decode(value, options) の代わりに、**options を展開する、もしくは options が空のときはオプション無しで呼ぶなどの対応が入っている形です。
テストの修正
activerecord/test/cases/coders/json_test.rbactiverecord/test/cases/serialized_attribute_test.rb
の2つで、1行ずつの微修正が入っています。
ここでは:
- JSON デコードの呼び出し方、もしくは期待される挙動を、
json3.0 対応後の API に合わせて修正。 - 特に「オプションをハッシュで渡す」挙動をチェックしているテストがあるため、
json3.0 でも落ちない形に揃えている。
PR 本文にもある通り、「ハッシュを渡せることをテストしているものがありつつ、encode 相当のメソッドとの対称性もある」ため、完全に def decode(...) に変えるのではなく、既存インターフェースを壊さず裏側だけを調整する方針をとっています。
- 影響範囲・注意点
- 影響範囲:
ActiveSupport::JSON.decodeを経由して JSON をパースするすべての場所。- Active Record の
serializeなどで JSON コーダーを使っている箇所(ActiveRecord::Coders::JSON)。
- 想定される影響:
jsongem 2.x 系では従来通り動作することを維持しつつ、3.0 でもArgumentError: wrong number of argumentsが出なくなる。- アプリケーションコード側のシグネチャ(
ActiveSupport::JSON.decode(json, options_hash)など)は変わらないため、基本的にアプリ修正は不要。
- 注意点:
- アプリ側で直接
JSON.parse(json, some_hash)のように「位置引数2つ」で呼んでいる場合は、Rails ではなくアプリ側を修正する必要があります(JSON.parse(json, **some_hash)やJSON.parse(json, symbolize_names: true)のようにキーワード引数形式に切り替える)。 - Rails 内部の互換性レイヤーに依存せず、直接
jsongem を叩いているライブラリ・プラグインも同様にチェックが必要です。
- アプリ側で直接
- 参考情報 (あれば)
- json gem 3.0rc1 の変更ログ
https://github.com/ruby/json/blob/master/CHANGES.md#2026-08-11-300rc1 - (PR 本文より)
json3.0 での仕様:- 「すべてのメソッドの options はキーワード引数として扱われ、未知のオプション(タイポなど)は
ArgumentErrorになる」 - これにより、ハッシュを位置引数として渡す従来の呼び方が一部互換性問題を起こす。
- 「すべてのメソッドの options はキーワード引数として扱われ、未知のオプション(タイポなど)は
#58602 remove DS_Store
マージ日: 2026/8/28 | 作成者: @tenderlove
概要 (1-2文で)
このPRは、activerecordディレクトリ配下に誤ってコミットされていた macOS のメタデータファイル.DS_Storeをリポジトリから削除するものです。機能追加やバグ修正は一切なく、純粋な不要ファイルの掃除です。変更内容の詳細
- 変更ファイル:
activerecord/.DS_Store - 内容:
.DS_Storeファイルの削除(実質的にバイナリ/メタデータファイルの除去であり、コード差分はありません)
.DS_Store は macOS Finder が各ディレクトリごとに自動生成する、表示設定などを保持する隠しファイルです。
Rails のコードベースやテストに関する変更は一切行われていません。
- 影響範囲・注意点
- ランタイム動作への影響: なし
- アプリケーションの挙動、Active Record の機能、パフォーマンスには関係ありません。
- ビルド/テストへの影響: なし
- 単に不要な OS 依存ファイルがリポジトリから取り除かれただけです。
- 開発プロセス上の意味合い:
- 他の開発者環境で
.DS_Storeによる差分が出にくくなり、クリーンな Git 履歴を維持できます。 - もし
.gitignoreに.DS_Storeが未登録なら、今後の混入防止のために.gitignoreで無視することが推奨されます(このPR自体では.gitignoreは変更されていません)。
- 他の開発者環境で
- 参考情報 (あれば)
.DS_Storeについて:- Apple公式ドキュメント: 「About the .DS_Store file」などを参照
- 一般的な対策例:
.gitignoreに以下を追加してコミット対象から除外する運用がよく行われます:gitignore.DS_Store- グローバル Git 設定で無視する場合(開発者ローカルの設定例):bash
git config --global core.excludesfile ~/.gitignore_global echo .DS_Store >> ~/.gitignore_global
#58595 Set timeout-minutes for ci.yml jobs
マージ日: 2026/8/28 | 作成者: @t27duck
- 概要 (1-2文で)
このPRは、Rails が新規アプリ生成時に出力する GitHub Actions 設定 (ci.yml) に、各ジョブ共通のtimeout-minutes: 15を設定する変更です。これにより、ネットワーク障害などで処理がハングした場合でも、デフォルトの360分待たずに15分でタイムアウトし、無駄なランナー時間の消費を防ぎます。
- 変更内容の詳細
変更対象
railties/lib/rails/generators/rails/app/templates/github/ci.yml.tt
Rails new したときに.github/workflows/ci.ymlとして生成されるテンプレート。railties/CHANGELOG.md
上記変更がドキュメントとして追記されている。
実際の変更イメージ
ci.yml.tt 内の各ジョブ定義に timeout-minutes: 15 が追加されています。
(PR本文から推測される典型的なイメージ)
name: CI
on:
push:
pull_request:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
# 以降、依存パッケージのインストールやテスト実行など
lint:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
# Lint 実行などポイント:
- 全ジョブに対して
timeout-minutes: 15を設定- 「ハングする可能性はどのジョブでもありうる」という前提で、テストだけでなく他のジョブにも設定している。
- デフォルトのジョブタイムアウト (360分) を上書きする形で、15分で確実に打ち切る。
- PR説明にもある通り、ユーザー側で値を引き上げたり、削除することは自由。
背景にある問題
- GitHub Actions 上で OS パッケージインストールなどを行うステップが、ネットワーク不調や GitHub 側の断続的な障害により「ハングしたまま」になることがある。
- デフォルトでは 360 分(6時間) 経つまでジョブが終了しないため、その間ずっとランナー時間を消費してしまう。
- 大規模プロジェクトを多数持つ組織では、無駄なランナー分数がコストとして効いてくる。
そのため、新規 Rails アプリの標準 CI 設定として、ハングを早めに検出する「安全な上限」= 15分を設けるという方針です。
- 影響範囲・注意点
影響範囲
- 対象: このPRが入ったバージョン以降で
rails new myapp --githubなどにより GitHub Actions 用 CI ファイルを生成した新規アプリ。 - 既存アプリの
.github/workflows/ci.ymlには自動では反映されませんが、同様の設定を手動で取り込むことはできます。
実務的な注意点
長時間かかるテストスイートを持つアプリ
テスト全体が 15分を超える可能性がある場合、そのジョブは 常にタイムアウトで失敗するようになります。
その場合は、生成された
ci.yml内の該当ジョブについて、例えば次のように値を引き上げる必要があります:yamljobs: test: runs-on: ubuntu-latest timeout-minutes: 30 steps: ...
まれにハングするケースを見つけるトリガーになる
- 15分で打ち切られた場合、原因が
- テストが純粋に長すぎるのか
- ネットワーク障害や外部サービス待ちで固まっているのか を切り分ける必要があります。
- 「最近ハングしがちでタイムアウトする」という場合は、ジョブログを確認してどのステップで止まっているかを特定し、
- リトライ (e.g.
retryアクション) - 外部サービスへの依存削減
- キャッシュ利用
などを検討する余地があります。
- リトライ (e.g.
- 15分で打ち切られた場合、原因が
組織ランナー・GitHub Actions の課金への影響
- ハングしたジョブが 360分走り続けることを避けられるため、ランナー時間の節約に直結します。
- 特に self-hosted runner や GitHub-hosted runner を大量に使う組織には有益です。
- 参考情報 (あれば)
PR本文で参照されているポスト:
- https://x.com/excid3/status/2089708290963886540
- https://x.com/excid3/status/2089835568188264471
これらは GitHub Actions のハングやランナー時間浪費に関する実例・議論と思われ、
今回の変更のモチベーションとなっています。
GitHub Actions 公式ドキュメント (timeout-minutes):
https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutestimeout-minutesはジョブ単位で設定され、そのジョブの 全体実行時間の上限になります。
#58599 Prefixed partial names ractor safe
マージ日: 2026/8/28 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Action View の「オブジェクトレンダリング」と ActiveSupport のイベントレポーター周りで、Ractor 非対応だったグローバル状態を「Ractorごとの状態管理」に変更し、Ractor セーフにした PR です。マルチ Ractor 実行時にも部分テンプレートのプレフィックスキャッシュとイベント通知が安全に動くようになります。
- 変更内容の詳細
2-1. ActionView のプレフィックス付きパーシャル名キャッシュを Ractor セーフに
変更点概要
従来は、ActionView::AbstractRenderer::ObjectRendering 内で、クラスレベルの定数的なキャッシュを使っていました:
# 旧: 実際のコード名は説明用
PREFIXED_PARTIAL_NAMES = {}このような「プロセス全体で共有されるミュータブルな Hash」は、Ractor 間で共有できないため Ractor セーフではありません。
PR では、これを「Ractorごとのキャッシュ」に切り替えるために、prefixed_partial_names メソッドを新設し、そこからキャッシュにアクセスするようにしています。
イメージとしては以下のような形になります(擬似コード):
module ActionView
class AbstractRenderer
module ObjectRendering
# Ractorごと/スレッドごとのキャッシュを返す
def prefixed_partial_names
# 例: Ractor.local_variable_get / Thread.current などを使い
# 「このRactor専用のHash」を取得する形になっている
@_prefixed_partial_names ||= {}
end
def render_with_prefixes(object)
cache = prefixed_partial_names
# ここで cache[klass] などに格納して再利用する
end
end
end
end実際の実装では Ractor に対応したストレージを使い、グローバル共有の定数を廃止している点がポイントです。
テストの追加
actionview/test/template/render_test.rb にテストが追加され、以下のような点が確認されています:
- オブジェクトレンダリング時にプレフィックス付きパーシャル名が正しくキャッシュされること
- 複数回レンダリングしてもキャッシュを用いて正しいテンプレートが描画されること
- Ractor(もしくはそれに準ずる並行環境)で実行した場合も壊れないこと
2-2. ActiveSupport.event_reporter を Ractor ローカルに
変更点概要
ActiveSupport.event_reporter は、イベントの発火/購読まわりで内部的に使われるオブジェクトです。従来は単純にクラスインスタンス変数を使っていました:
# 旧イメージ
module ActiveSupport
class << self
def event_reporter
@event_reporter ||= build_reporter
end
end
endこのような ivar ベースのシングルトンも、Ractor 間でそのまま共有するのは安全ではありません。
PR では、以下の方針に変更されています:
- メイン Ractor では従来通りインスタンス変数を使用
- それ以外の Ractor では、Ractor ローカルな event_reporter を保持
疑似コードのイメージ:
module ActiveSupport
class << self
def event_reporter
if Ractor.main?
@event_reporter ||= build_reporter
else
# その Ractor の中だけで使われるレポーター
Ractor.current[:event_reporter] ||= build_reporter
end
end
end
end(実際の API 名は異なりますが、おおむねこうした仕組みになっています。)
テストの追加
activesupport/test/event_reporter_test.rb にテストが追加され、以下を確認しています:
- メイン Ractor での
event_reporterが従来どおり機能すること - 別 Ractor 内で
ActiveSupport.event_reporterを呼んでも問題なく動作し、メイン Ractor の状態とは独立していること - Ractor 間でオブジェクトが誤って共有されず、Ractor セーフに動くこと
- 影響範囲・注意点
Ractor を使わないアプリケーション
従来と同じ挙動となるよう配慮されているため、基本的に振る舞いの変化はありません。パフォーマンス面でも、以前の「プロセス全体でのキャッシュ」が「Ractorごとのキャッシュ」に分割されるだけなので、単一 Ractor(=通常の MRI 実行)ではほぼ同等です。Ractor を使う/今後使う可能性があるアプリケーション
- Action View のオブジェクトレンダリング(例:
render @userやrender @collection)を Ractor 内で行っても安全に動作し、キャッシュ汚染や例外が起こりづらくなります。 - ActiveSupport のイベント周り(ActiveSupport::Notifications など、内部で event_reporter を利用するもの)が Ractor ごとに独立したレポーターを持つため、Ractor 間でイベントが想定外に混ざることを防げます。
- 一方で、「Ractor 間でイベントレポーターを共有して、全ての Ractor のイベントを一元的に集計したい」といった設計は、この変更と整合しません。その場合は、各 Ractor から別プロセスやスレッドセーフな別チャンネル(Queue、ログ、DB など)にイベントを送る設計にする必要があります。
- Action View のオブジェクトレンダリング(例:
カスタムパッチ/Monkey Patch をしている場合の注意点
ActionView::AbstractRenderer::ObjectRendering::PREFIXED_PARTIAL_NAMESを直接参照・上書きしているような Monkey Patch がある場合は動かなくなります。代わりに、prefixed_partial_namesメソッドを経由するように修正する必要があります。ActiveSupport.event_reporterをグローバルなシングルトン前提で利用していた独自コードがある場合、Ractor 内では「Ractorごとのインスタンス」になる点に注意してください。
- 参考情報 (あれば)
Ruby の Ractor 仕様・制約
- Ractor 間でミュータブルオブジェクトは共有できず、共有するには
shareable_constant_valueなどでイミュータブル化するか、メッセージパッシングが必要。 - Rails 内部の多数のシングルトン/グローバルキャッシュは、Ractor 対応時に「Ractor ローカル」や「スレッドローカル」へと段階的に移行中。
- Ractor 間でミュータブルオブジェクトは共有できず、共有するには
本 PR は CHANGELOG 未更新のため、「外向きの新機能」としてよりは、内部の Ractor 対応改善の一環という位置付けです。Ractor を積極的に使いたい場合には、この PR を含むバージョン以降を前提に検証する価値があります。
#58593 Remove no-op Marshalling.format_version handling from tests
マージ日: 2026/8/27 | 作成者: @kamipo
- 概要 (1-2文で)
Rails 7.1 以降でActiveRecord::Marshalling.format_versionが常に 7.1 固定になった仕様を前提に、もはや意味を持たないテスト上のformat_versionの保存・復元処理と、それに依存していた重複テストを削除した PR です。結果として、マージ後はテストコードがシンプルになりつつ、実質的なテストカバレッジは変わりません。
- 変更内容の詳細
背景: Marshalling.format_version の仕様変更
- #57382 以降:
ActiveRecord::Marshalling.format_versionは常に"7.1"を返すようになっている。ActiveRecord::Marshalling.format_version = ...で"7.1"以外を設定しようとすると例外を投げる仕様になった。
- このため、「現在の
format_versionを退避してからテスト中だけ書き換え、最後に戻す」というパターンは、今や事実上 no-op(意味を持たない)になっている。
削除された処理: format_version の保存・復元
activerecord/test/cases/base_test.rb と activerecord/test/cases/marshal_serialization_test.rb にあった、次のようなテストコードが削られています(実際の差分は概念的にはこのイメージ):
# 以前(イメージ)
def test_something
previous = ActiveRecord::Marshalling.format_version
ActiveRecord::Marshalling.format_version = "6.1"
# ... テスト本体 ...
ensure
ActiveRecord::Marshalling.format_version = previous
end現状の実装では:
format_versionは常に"7.1"固定"6.1"をセットしようとすると例外
となるため、このようなコードはそもそも意味をなさず、かつ「古いフォーマットを明示的に切り替える」こと自体が不可能になっているため、削除されています。
重複テストの整理
format_version による分岐がなくなったことで、「6.1 用」として残っていたテストが実質的に 7.1 用テストのサブセットまたは完全な複製になっている状態でした。
具体的には:
test_rails_6_1_rountrip- 「Rails 6.1 のフォーマットとの round trip を確認するテスト」として書かれていたが、
format_versionを変えられない今となっては、テスト内容が「Rails 7.1 の round trip テスト」の厳密なサブセットになっていた。 - よって、7.1 版テストがあれば十分と判断され、6.1 版が削除。
- 「Rails 6.1 のフォーマットとの round trip を確認するテスト」として書かれていたが、
test_marshalling_with_associations_6_1とtest_marshalling_with_associations_7_1- 中身が完全に同一になっており、バージョンだけが違うというテスト名上の区別しかなくなっていた。
- このため、一方(6.1 向け)が冗長として削除され、7.1 向け(現仕様に即したもの)のみ残す形に整理。
結果として、この PR の差分は:
activerecord/test/cases/base_test.rb: 1 行追加 / 16 行削除activerecord/test/cases/marshal_serialization_test.rb: 22 行削除
と、主に不要コードの削除が中心です。
- 影響範囲・注意点
影響範囲:
- 本 PR はテストコードのみの変更であり、本体のフレームワーク挙動には影響しません。
ActiveRecord::Marshallingの振る舞い自体は、すでに #57382 時点の仕様(format_version固定)を前提としており、この PR はそれにテストを追従させるものです。
注意点 / 開発者視点での示唆:
- 外部アプリケーションやライブラリ側で、「
ActiveRecord::Marshalling.format_versionをテストや実行時に切り替えて古いフォーマットとの互換性を担保する」という設計をしている場合、それは既に Rails 本体の現行仕様と整合しません。"7.1"以外への変更は例外になるため、バージョン切り替えによる動作確認は不可能です。- 古いバージョンとの互換性確認は、Rails のバージョン(およびその時点の ActiveRecord)自体を切り替えて行う必要があります。
- テスト内で「以前のフォーマットバージョンを明示して検証する」という考え方は現行のマシュアル仕様とは合わないため、新規に似たコードを書くべきではありません。
- 外部アプリケーションやライブラリ側で、「
- 参考情報 (あれば)
- 対応する過去の変更:
- PR #57382:
ActiveRecord::Marshalling.format_version周りの仕様変更(7.1 固定、および setter の制限)
- PR #57382:
- 該当箇所のテストファイル:
activerecord/test/cases/base_test.rbactiverecord/test/cases/marshal_serialization_test.rb
この PR は、上記仕様変更を前提に「もはやテストとして意味をなさないコード」を整理したものと理解できます。
#58592 More small hash optimizations
マージ日: 2026/8/27 | 作成者: @byroot
- 概要 (1-2文で)
Rails のルーティング処理(ActionDispatch::Routing::RouteSet)において、URL オプション用ハッシュに対する不要な処理を減らすことで、パフォーマンスをわずかに改善するための最適化 PR です。機能仕様は変えずに、内部でのハッシュ操作(コピーやマージなど)を避ける形でコードが整理されています。
- 変更内容の詳細
※ PR 本文に「Avoid some needless operations on URL option hashes.」とある通り、url_for / パスヘルパーなどで使われる内部ルーティングロジックに対してのマイクロ最適化です。該当ファイルは以下のみです。
actionpack/lib/action_dispatch/routing/route_set.rb
典型的には、以下のような処理まわりが対象です:
url_forやpolymorphic_url/_url/_pathヘルパーが内部的に呼び出すルート生成処理- ルーティング定義から
path/urlを組み立てるときに渡されるオプションハッシュ(url_options/optionsなど)
この PR で行われているのは、次のような種類の変更です(コードイメージ):
不要なハッシュの複製をやめる
ruby# 変更前のイメージ options = options.dup options[:only_path] = true # 変更後のイメージ options = options.dup unless options.frozen? # または、必要な場合のみ dup options[:only_path] = true→ 毎回
dupしていたものを、「本当に必要な場合だけ」に絞るなど。merge/reverse_mergeのような新ハッシュ生成を避けるruby# 変更前イメージ url_options = default_url_options.merge(options) # 変更後イメージ url_options = default_url_options.dup options.each do |k, v| url_options[k] = v endまたは、すでに
default_url_options/optionsのどちらか片方が空のケースを判定して、そもそもmergeを呼ばない、などの分岐を加えている可能性があります。小さなハッシュに対してだけ、より軽量な処理に切り替え
タイトルに "small hash optimizations" とある通り、特にキー数が少ない URL オプションハッシュを高速に扱うようなパターン(例:if options.size == 1 ...など)を追加している可能性があります。
いずれも「ロジックそのものは変えず」「同じ引数を渡せば同じ URL が返ってくる」ことを前提に、実行時に発生するオブジェクト生成・ハッシュ操作の回数を減らすための変更です。
- 影響範囲・注意点
影響範囲
link_to,url_for,*_url,*_pathなど、ルーティング経由で URL を生成するすべてのコードが、内部的にこの変更の影響を受けます。- 特に 大量に URL を生成する処理(API レスポンス組み立て、HTML レンダリングでヘルパーを大量に呼ぶ処理など)で、ごくわずかなパフォーマンス改善が期待できます。
- 外部向けの API やメソッドシグネチャ変更はなく、アプリケーションコードからの呼び出し方法は従来通りです。
互換性 / 注意点
- 仕様変更ではなく内部最適化であるため、通常のアプリケーションコードに対する後方互換性の問題は基本的にありません。
- ただし、次のような「内部実装への過度な依存」をしている場合は注意してください:
- ルーティング内部で渡されるハッシュオブジェクトの同一性(
object_id)に依存したメタプログラミング default_url_options/url_optionsなどに対して、副作用を期待して破壊的変更 (options[:foo] = ...) を行っているコード
- ルーティング内部で渡されるハッシュオブジェクトの同一性(
- この PR では「不要なコピーを減らす」方向の変更なので、今までたまたまコピーされていたハッシュが、コピーされなくなる箇所がある可能性があります。
→ ルーティング内部で渡されるハッシュは、「自由に破壊的変更してよいものではない」とみなすのが安全です(以前からそうであるべきですが、今回の変更でより顕在化しやすくなります)。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58592
- タイトル: More small hash optimizations
- マージ日時: 2026-08-27
- 関連箇所:
ActionDispatch::Routing::RouteSet#generate- 各種 URL ヘルパー (
*_url,*_path,url_for) の内部実装
- 類似の最適化 PR は、
byroot氏による GC/オブジェクト生成削減系の変更として、以前から複数存在しているため、パフォーマンスチューニングの一環として継続的に行われているものと考えられます。
#58590 Fix flaky connection pool disconnect tests
マージ日: 2026/8/27 | 作成者: @gmcgibbon
- 概要 (1-2文で)
このPRは、Active Record のコネクションプール関連テストがローカル実行時にときどき失敗する(flakyになる)問題を減らすため、テストのエラー報告まわりのコンテキストを正しく隔離するよう修正したものです。ConnectionPoolTestsにActiveSupport::ExecutionContext::TestHelperを mixin することで、テスト実行ごとのコンテキスト汚染を防いでいます。
- 変更内容の詳細
対象ファイル:
activerecord/test/cases/connection_pool_test.rb(+2行 / -0行)
実質的には、ConnectionPoolTests のテストクラス(もしくはそのベースクラス)に以下のような形で ActiveSupport::ExecutionContext::TestHelper をインクルードする変更です(PR説明からの推定コード例):
class ConnectionPoolTest < ActiveRecord::TestCase
include ActiveSupport::ExecutionContext::TestHelper
# 既存の connection pool 関連テスト群
endActiveSupport::ExecutionContext::TestHelper は、ActiveSupport の ExecutionContext(ログ、エラー報告、その他のコンテキスト情報をスレッドローカル等で持つ仕組み)をテストごとにリセット/隔離するためのヘルパーモジュールです。
この PR では、コネクションプール切断テスト(disconnect に関するテスト)の実行中に、他テストや前後のテストで設定されたエラーコンテキストが混ざることで、たまに想定外の状態になりテストが失敗する、という flaky な状態を改善するのが目的です。
ローカルで main ブランチのテストを走らせたときにだけ稀に落ちる、という性質からも、グローバル/スレッドローカルな状態のリークが原因であることを踏まえた修正になっています。
- 影響範囲・注意点
影響範囲
- 本変更は テストコードのみに対する変更 であり、本番コード(ライブラリ本体)には一切変更がありません。
- 影響は Active Record のコネクションプール関連テスト (
connection_pool_test.rb) の実行時にのみ及びます。 - CI・ローカル共に、コネクションプールのテストの再現性が向上し、ランダムなテスト失敗が減ることが期待されます。
注意点
ExecutionContextを利用している他のテストでも類似の flaky がある場合、本PRと同様にActiveSupport::ExecutionContext::TestHelperをインクルードすることで改善できる可能性があります。- バグ修正ではあるものの、ランタイム挙動ではなくテスト安定性に関する修正なため、CHANGELOG は更新していません(チェックリスト上でも未チェック)。
- 参考情報 (あれば)
ActiveSupport::ExecutionContext/ActiveSupport::ExecutionContext::TestHelperについて:- Rails 内部でのログ・エラー報告・計測などに紐づく「実行コンテキスト」を扱う仕組みで、スレッドローカルに近い形で管理されます。
TestHelperを使うことで、テストごとにこのコンテキストをクリアし、前後のテスト間で状態が漏れないようにできます。
- flaky なテストに対する一般的な対処:
- グローバル状態/スレッドローカル状態のリセット
- モック/スタブの解除、タイマーやスレッドの終了確認
- ランダムシード固定
- 今回は「実行コンテキストのリセット」がそれにあたる対策になっています。
#58581 [8-1-stable] Silence Time.rfc3339 redefinition warning
マージ日: 2026/8/26 | 作成者: @yahonda
- 概要 (1-2文で)
Ruby 4.1 でtime標準ライブラリにTime.rfc3339が入ることにより、Active Support 側の同名メソッド定義と衝突して警告が出ていた問題を、警告だけを抑制する形で解消する PR です。8-1-stable では挙動を一切変えず、CI での strict warnings エラーだけをなくすことを目的としています。
- 変更内容の詳細
背景
Rails(Active Support)は Rails 5.1 以降、自前で
Time.rfc3339を定義している。Ruby 4.1 では
timegem(標準添付)側にTime.rfc3339が入る予定。その結果、Ruby 4.1 上で
active_support/core_ext/time/calculations.rbを読み込むと:textwarning: method redefined; discarding old rfc3339という再定義警告が出る。
Rails の CI では
tools/strict_warnings.rbにより warning がRailsStrictWarnings::WarningErrorへ昇格し、テストが走る前に落ちてしまう。
main ブランチとの違い
main では #58402(コミット 3e46bf2f10)で対処済み:
- Ruby が
Time.rfc3339を持っていれば Ruby 標準の実装を使い、 - そうでなければ Active Support の実装を定義する。
- その際、「
Z(UTC 指定)の扱い」を Ruby 標準に合わせて 挙動変更(utc? => true)しており、CHANGELOG にも記載されている。
- Ruby が
一方、この PR は 8-1-stable に対するパッチであり、パッチリリースで挙動を変えたくないため:
- 引き続き すべての Ruby バージョンで Active Support の
Time.rfc3339を使い続ける。 - その代わり、Ruby 4.1 で出るメソッド再定義 warning だけを 抑制 する。
- 引き続き すべての Ruby バージョンで Active Support の
実際の変更内容
対象ファイル:activesupport/lib/active_support/core_ext/time/calculations.rb に 3 行追加。
実装としては、既存の Date#to_time, Date#xmlschema, Time#to_time などで使っているのと同じ silence_redefinition_of_method ヘルパを Time.rfc3339 にも適用します。
イメージとしては以下のような形です(実際の位置や細部は省略):
class << Time
# 既存の他メソッドと同様に、再定義時の warning を抑制
silence_redefinition_of_method :rfc3339
def rfc3339(str)
# Active Support 独自実装(従来通り)
end
endこれにより、Ruby 本体(time.rb)で既に定義されている Time.rfc3339 を上書きしても warning が出ないようになります。
挙動の確認
この PR の前後で、8-1-stable における Time.rfc3339 の挙動は変わりません。
Time.rfc3339("2026-08-26T10:00:00Z")
# => 2026-08-26 10:00:00 +0000
# utc? => false (Active Support 独自の +00:00 固定オフセット扱い)表にすると次のようになります:
| Ruby | Time.rfc3339 の定義元 | "Z" の解釈 | Active Support 読み込み時の warning (変更前 → 変更後) |
|---|---|---|---|
Ruby 4.0 以前(time に rfc3339 なし) | Active Support | +00:00 固定オフセット (utc? は false) | なし → なし |
Ruby 4.1 以降(time に rfc3339 あり) | Ruby の定義を Active Support が再定義 | +00:00 固定オフセット (utc? は false) | 「method redefined」→ なし |
この PR が変えるのは Ruby 4.1 での warning の有無だけです。メソッドの挙動(返り値、utc? の結果)は 8-1-stable の中では不変です。
- 影響範囲・注意点
- 対象バージョン:
- Rails 8.1 系(8-1-stable のみ)。
Ruby 4.1 リリース時点でサポート期間中なのが 8.1 系だけになるため、そこにだけ適用する方針です。
- Rails 8.1 系(8-1-stable のみ)。
- 影響範囲:
- Ruby 4.1(または同様の条件で
Time.rfc3339が既に定義されている環境)で、Active Support を読み込む際の warning が消える。 - CI などで
RAILS_STRICT_WARNINGS=1を有効にしている場合に、Time.rfc3339の再定義 warning によるテスト中断がなくなる。
- Ruby 4.1(または同様の条件で
- 挙動の後方互換性:
Time.rfc3339("...Z")の返り値やutc?の結果は 一切変更されない。- main ブランチの #58402 のように、「Ruby 標準の定義を優先して挙動を変える」ことはしていない。
- 注意点:
- 将来的に Rails を 9.0 / main 系に上げると、
Time.rfc3339("...Z")が UTC (utc? => true) になる可能性がある(#58402 による仕様)。
8.1 → 9.0 へのアップグレード時には、この差分を意識してテストする必要がある。 - この PR 自体はテストコードの変更は行っていないが、既存の
Time.rfc3339関連テストは Ruby master 上でもすべてパスすることが確認されている。
- 将来的に Rails を 9.0 / main 系に上げると、
- 参考情報 (あれば)
- この PR: #58581 – [8-1-stable] Silence
Time.rfc3339redefinition warning - 関連 PR(main の対応): #58402 – Use Ruby's
Time.rfc3339when available - Ruby 側の追加コミット:
https://github.com/ruby/ruby/commit/6c9f9f5c6118ae024b9ffad8e0cb7eda625e8b2d - Rails strict warnings(warning をエラー扱いにする仕組み):
tools/strict_warnings.rb
#58576 Actionpack ractorize
マージ日: 2026/8/26 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Action Pack 内のいくつかのグローバル/共有オブジェクトを Ractor セーフにするための PRです。ParamBuilderのデフォルト、RequestのLOCALHOST、Responseのデフォルト文字コードとヘッダが、Ractor から安全に扱えるように変更されています。
- 変更内容の詳細
全体方針
Ruby 3 の Ractor では、Ractor 間で共有されるオブジェクトは「イミュータブル or Ractor シェア可能」である必要があります。本PRでは、Action Pack が内部で保持している「デフォルト設定」的なオブジェクトをこの制約に合わせる形で修正しています。
2-1. ActionDispatch::ParamBuilder.default の Ractor 対応
変更点:
ParamBuilder.defaultの保持方法を「シングルトンクラスの attr アクセサ」に変更し、Ractor 間での共有に耐えられる形に修正。- テスト (
param_builder_test.rb) で、Ractor 内からParamBuilder.defaultが使えることを検証。
イメージコード例(変更後の設計イメージ):
module ActionDispatch
class ParamBuilder
class << self
attr_accessor :default
end
# default に Proc などを入れて使う想定
self.default = ->(params) { params }
end
endここで重要なのは:
defaultにセットされるオブジェクトが、Ractor 間で共有され得る「設定値」であること- それをクラスインスタンス変数+シングルトンクラスの accessor で明示的に扱うことで、初期化タイミングや共有の仕方を制御しやすくしていること
2-2. ActionDispatch::Request::LOCALHOST の Ractor 対応
変更点:
LOCALHOSTの正規表現オブジェクトに#freezeを呼び出して不変化。- テスト (
request_test.rb) で、この定数を Ractor から利用できることを確認。
イメージコード例(変更後の設計イメージ):
module ActionDispatch
class Request
LOCALHOST = /\A(?:localhost|127\.0\.0\.1)\z/.freeze
end
endRactor 間で共有される定数はミュータブルオブジェクトだと問題になるため、Regex を明示的に freeze して Ractor シェア可能にしています。
2-3. ActionDispatch::Response.default_charset / default_headers の Ractor 対応
変更点:
default_charset/default_headersを「クラス属性」で管理するように変更。- テスト (
response_test.rb) で、Ractor 内からResponse.default_charset/default_headersを利用できることを確認。
イメージコード例(変更後の設計イメージ):
module ActionDispatch
class Response
class << self
attr_accessor :default_charset, :default_headers
end
self.default_charset = "utf-8"
self.default_headers = {
"Content-Type" => "text/html; charset=utf-8"
}.freeze
end
endここで重要なのは:
default_headersのようなハッシュはミュータブルなので、そのままでは Ractor シェアできない- クラス属性にしつつ、実際には
freezeされたハッシュを使うことで、「デフォルト設定」として Ractor セーフに共有できるようにしている点
(実装上freezeの呼び方は細部が違う可能性はありますが、テストが Ractor 実行を通しているので、少なくとも共有されるデフォルト値は Ractor セーフになっています)
- 影響範囲・注意点
Ractor を使っていない通常の Rails アプリ
挙動の差分はほぼありません。default_charsetやdefault_headersを通常どおりオーバーライドして使っている分には、従来と同様に動く想定です。既存コードとの互換性
ActionDispatch::Response.default_headersに対して「破壊的変更(merge!など)」を直接行っている場合、
今回の変更でfreezeされたオブジェクトが返る場合があるため、FrozenErrorになる可能性があります。
その場合は以下のように、コピーを作ってから変更する必要があります。rubyheaders = ActionDispatch::Response.default_headers.dup headers["X-Custom"] = "value"ParamBuilder.defaultにセットするオブジェクトを、Ractor 間で共有されることを前提としていない(内部状態を更新する)オブジェクトにしている場合、不具合やエラーの原因になり得ます。Procやイミュータブルなオブジェクトを使うことが推奨されます。
Ractor を使うアプリケーション
- 本PRにより、
ActionDispatch周りで代表的に共有される設定値が Ractor セーフになり、Ractor 実行時のRactor::UnsafeErrorを避けやすくなります。 - ただし Action Pack 全体が完全に Ractor 対応完了したわけではなく、「いくつかの非対応部分を修正した」段階と明示されています。他の部分でまだ Ractor 非対応の箇所が残っている可能性があります。
- 本PRにより、
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58576
- Ractor の仕様と制約(Ruby公式ドキュメント)
- Ractor とライブラリの Ractor セーフ化に関する一般的なポイント
- 共有される定数やクラス変数は「イミュータブル(
freeze)」「Ractor シェア可能」なオブジェクトにする - グローバルな設定値・デフォルト値は、Ractor ごとにコピーするか、イミュータブルとして共有するかを意図的に設計する必要がある
- 共有される定数やクラス変数は「イミュータブル(
#58574 Fix RedisCacheStore#clear and #stats with multiple Redis URLs
マージ日: 2026/8/26 | 作成者: @Saidbek
- 概要 (1–2文で)
複数の Redis URL を指定してRedisCacheStoreを構成した場合に、clearとstatsがNoMethodErrorで落ちてしまうバグを修正した PRです。これらのメソッドが「リング全体を 1 クライアント」と誤って扱っていたのを、各 Redis サーバーごとに正しく処理するように変更しています。
- 変更内容の詳細
問題の背景
RedisCacheStore は、urls: [...] のように複数の Redis URL を指定すると、内部的に「リング(分散)」構成になります。
Rails.application.config.cache_store = :redis_cache_store, {
urls: [
"redis://cache-1.example.com:6379/0",
"redis://cache-2.example.com:6379/0"
]
}この構成時に、以下を呼び出すと NoMethodError が発生していました:
Rails.cache.clear
Rails.cache.stats原因は、clear / stats が「単一の Redis クライアント」を前提とした実装になっており、複数URLで使われる「リングオブジェクト」に対して存在しないメソッドを呼んでしまっていたためです。
修正内容の概要
RedisCacheStore#clear- これまでは「リングを 1 クライアント」として扱い、
flushdb相当の処理を行おうとしていた。 - 修正後は、リングに含まれる各 Redis クライアントに対して個別に
clear処理を実行するように変更。
- これまでは「リングを 1 クライアント」として扱い、
RedisCacheStore#stats- これまでは「リング全体」に対して stats を取りに行こうとして
NoMethodErrorが発生。 - 修正後は、各 Redis サーバーから統計情報を取得するように変更。
- 実装としては、各クライアントに対して info 的なコマンドを発行し、その結果を適切な形でまとめる/返すようになっていると考えられます(テストが追加されていることから、戻り値の形式も複数URL対応で整合が取れています)。
- これまでは「リング全体」に対して stats を取りに行こうとして
テストの追加
activesupport/test/cache/stores/redis_cache_store_test.rb に 28 行のテストが追加されており、主に次を検証していると考えられます:
- 複数 URL 構成の
RedisCacheStoreでclearを呼んでも例外が発生しないこと。 - 複数 URL 構成の
RedisCacheStoreでstatsを呼んでも例外が発生せず、期待する形式の結果が返ってくること。 - 単一 URL 構成時の挙動は従来どおりであること(リグレッション防止)。
実装の行数差は小さく(redis_cache_store.rb +3 / -2)、既存ロジックに対して最小限の修正で対応している PR です。
- 影響範囲・注意点
- 主な影響範囲
- 複数の Redis URL を指定している環境での
Rails.cache.clearとRails.cache.statsの挙動。 - これまで
NoMethodErrorで落ちていたケースが、正常に動作するようになります。
- 複数の Redis URL を指定している環境での
- 単一 URL 構成
- 「1 つの URL」の場合の挙動は「これまでと同じ」と明言されているため、後方互換性への影響はほぼありません。
- 運用・監視上の注意
statsの戻り値は、複数サーバー分を扱う形になるため、複数 URL 対応前提のフォーマットになっている可能性があります。- もしアプリケーション側で
Rails.cache.statsの戻り値構造に強く依存している場合(特に独自メトリクス集約ロジックなど)、アップデート後に一度確認すると安心です。
- もしアプリケーション側で
- パフォーマンス面
clear/stats実行時に、URL の数だけ Redis に問い合わせを行うため、URL が多い構成では従来よりも僅かに処理時間が増える可能性があります。- ただし、これらのメソッドは頻発するものではなく、実用上大きな問題にはなりにくいと考えられます。
- 参考情報 (あれば)
- 関連 Issue: #58573
- 「複数 Redis URL 構成で
clear/statsがNoMethodErrorになる」というバグ報告。
- 「複数 Redis URL 構成で
- 対象ファイル:
- 実装:
activesupport/lib/active_support/cache/redis_cache_store.rb - テスト:
activesupport/test/cache/stores/redis_cache_store_test.rb
- 実装:
- CHANGELOG:
- 小規模なバグフィックスのため、CHANGELOG は未更新(チェックリスト上も「Minor bug fix」扱い)。Rails をアップグレードしてもリリースノート上では目立たない変更になる可能性があります。
#58584 Optimize schema cache dump load
マージ日: 2026/8/26 | 作成者: @MrChoclate
- 概要 (1-2文で)
Rails の schema cache(.schema_cache.dump/.dump.gz)読み込み処理で、Ruby 2.7 以降のMarshal.load(freeze: true)を利用することで、文字列の intern / freeze を C 側で一括で行い、Ruby レベルの後処理を省いて高速化した PRです。1000+ テーブル規模のコードベースで、スキーマ読み込みから約 40ms を削減できたと報告されています。
- 変更内容の詳細
2-1. 背景: これまでの問題点
従来の .dump 形式の schema cache 読み込みでは:
Marshal.loadでオブジェクトグラフを復元した後にderive_columns_hash_and_deduplicate_valuesを呼び出して@columns_hashを再構築- さらにオブジェクトグラフ全体を走査して文字列を intern / freeze する(重複文字列をまとめる)
という 2 段階処理が行われていました。
Ruby 2.7 以降では Marshal.load(..., freeze: true) が使え、デシリアライズ時に C 実装で文字列を凍結・共有化できるため、 Ruby 側でもう一度オブジェクトツリーを歩き直して文字列を dedup するのは無駄になっています。
2-2. _load_from の変更点
SchemaCache._load_from が .dump / .dump.gz の読み込みで以下のように変更されています:
Marshal.loadにfreeze: trueを渡すように変更File.readからFile.binreadに変更(エンコーディング変換を避けるための純粋なバイナリ読み込み)
概念的にはこんなイメージです(実際のコードはもう少し違いますが趣旨として):
# 変更前(イメージ)
def self._load_from(path)
data = if gzipped?(path)
Zlib::GzipReader.open(path, &:read)
else
File.read(path) # テキスト読み込み
end
Marshal.load(data).tap(&:derive_columns_hash_and_deduplicate_values)
end
# 変更後(イメージ)
def self._load_from(path)
data = if gzipped?(path)
Zlib::GzipReader.open(path, &:read) # ここは既にバイナリ
else
File.binread(path) # バイナリ読み込み
end
# ここで freeze: true により、Marshal 復元時に文字列が C 側で凍結・共有化される
Marshal.load(data, freeze: true)
endポイント:
freeze: trueによって、すべての文字列がデシリアライズ時に凍結され、かつ同じ内容の文字列は共有される(メモリ削減 + GC 負荷軽減)。- これにより、後述の
derive_columns_hash_and_deduplicate_valuesが「文字列の dedup」のために存在する必要がなくなる。
2-3. marshal_load の変更点
SchemaCache#marshal_load は Marshal.load から呼ばれるフックで、Marshal 復元後の後処理を担うメソッドです。
この PR では:
derive_columns_hash_and_deduplicate_valuesの呼び出しを完全に削除- dump に含まれていない
@columns_hashの再構築だけを行うように簡素化
だいたい以下のようなイメージです:
# 変更前(イメージ)
def marshal_load(data)
@columns = data[:columns]
@columns_hash = data[:columns_hash] # or nil
# カラム情報や各種キャッシュから columns_hash を作りつつ
# すべての文字列を再度走査して dedup していた
derive_columns_hash_and_deduplicate_values
end
# 変更後(イメージ)
def marshal_load(data)
@columns = data[:columns]
# dump には入れていない派生フィールドをここで再構築
rebuild_columns_hash_only
endこの結果:
marshal_load内では、文字列の dedup / freeze はもう行わない- 代わりに、dump に含まれていない
@columns_hashだけを「再構築」する処理に特化
- 影響範囲・注意点
3-1. パフォーマンス面
- スキーマキャッシュのロード時間が短縮されます。PR 説明では「1000+ テーブル」で約 40ms 高速化。
- 大規模プロジェクトや、プロセス起動頻度の高い環境(例: CI, ジョブワーカー、serverless 的な構成)ほど恩恵が大きいです。
3-2. メモリ & 文字列の扱い
freeze: trueにより、schema cache に含まれる文字列はデシリアライズ後すぐに凍結されます。- もともと schema cache 内の情報(テーブル名・カラム名・型情報など)は読み取り専用想定であり、ここが凍結されることは設計上問題にならないはずです。
- 以前から
derive_columns_hash_and_deduplicate_valuesでも freeze / intern 相当のことをしていたため、「突然変更不能になった」というような後方互換性問題は基本的に発生しません。
3-3. Ruby バージョンとの関係
Marshal.load(..., freeze: true)は Ruby 2.7 以降で利用可能なキーワード引数です。- Rails 本体の対応 Ruby バージョン範囲と整合している前提で導入されています。
- もし独自に
SchemaCache周りを monkey patch している場合は、Marshal.load呼び出し部でfreeze:キーワードを受け付けることを前提にしている点に注意してください。
3-4. 既存の .schema_cache.dump ファイル
- dump フォーマット自体は Marshal ベースのままで、構造も互換性を維持しているため、既存の
.schema_cache.dump/.dump.gzはそのまま読み込めます。 - ただし読み込み時の挙動(文字列の intern タイミングと実装レイヤー)が Ruby レベル → C レベルに移行したという違いだけがあります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58584
- Ruby 2.7 以降の
Marshal.load(freeze: true)ドキュメント(公式リファレンス) - 間接的に関連するコード:
activerecord/lib/active_record/connection_adapters/schema_cache.rb内のSchemaCache._load_fromSchemaCache#marshal_load
- schema cache の仕組み全般については Rails ガイド「Active Record のスキーマキャッシュ」および
rake db:schema:cache:dumpの説明が参考になります。
#58577 Support primary key resetting on Ractors
マージ日: 2026/8/26 | 作成者: @gmcgibbon
- 概要 (1-2文で)
RailsのActive Recordにおいて、Ractor(並列実行用の機能)上でのプライマリキー割り当てが正しく動かない問題を修正し、非メインRactorからでも主キーリセット・取得が正しく行えるようにしたPRです。reset_primary_keyの実行をメインRactorに委譲することで、Ractor間で一貫した主キー情報を扱えるようにしています。
- 変更内容の詳細
何が問題だったか
- Active Recordのモデルは
reset_primary_keyを通じて、テーブル定義を見てprimary_keyを決定・キャッシュしています。 - しかし、この処理が「メインRactor」を前提にしており、
- メインRactor以外(サブRactor)で新たにモデルを使おうとしたとき
- まだプライマリキー情報が設定・キャッシュされていない状態
だと、主キーの割り当てが正しく行われない(または期待通りに共有されない)という問題がありました。
Ruby 3系で導入されたRactorは「オブジェクトを基本的に共有しない」モデルなので、
単純なクラス変数のキャッシュや初期化処理がRactorごとに分断されてしまう可能性があります。
何を変えたか
reset_primary_keyの処理を「メインRactor側で行う」ように変更し、
非メインRactorは、主キー情報が未設定のときにメインRactorへ「代理実行(プロキシ)」するようにしました。
PRの説明文をコードレベルに落とすと、イメージとしては次のような動きになります(擬似コード):
def reset_primary_key
if Ractor.current == Ractor.main
# これまで通り、メインRactorでprimary_keyを決定・キャッシュ
@primary_key = connection.schema_cache.primary_keys(table_name)
else
# 非メインRactorの場合は、
# メインRactorに「reset_primary_key を実行して結果を返して」もらう
Ractor.main.send([self, :reset_primary_key])
# 返ってきた結果をRactor側に反映 or 利用する
end
end※実際のパッチはもっと小さく、primary_key/reset_primary_key 周りにRactor検出とメインRactorへの委譲を追加する程度の変更です(+6/-4行)。
※正確な実装は activerecord/lib/active_record/attribute_methods/primary_key.rb を参照。
テスト
activerecord/test/cases/primary_keys_test.rbにテストを追加(+4行)し、
「非メインRactorで主キーが正しくリセット/利用できること」を検証しています。- Ractorを使う環境で、モデルの
primary_keyが期待通りにセットされるかを確認するようなテストになっているはずです。
- 影響範囲・注意点
影響範囲
- Active Record を Ruby 3 以降のRactor環境で利用している場合に影響があります。
- 特に、
Ractor.new内で Active Record モデルにアクセスし、
その主キーを使ったINSERT/UPDATEなどを行うコード。
- 特に、
- 非Ractor(通常のマルチスレッドやシングルスレッド)環境では挙動はほぼ変わらず、
既存コードへの影響は非常に小さいと考えられます。
注意点
- Ractor間は原則としてオブジェクトを共有しないため、
「メインRactorに問い合わせてprimary_keyを決定してもらう」ための経路が
Rails内部で用意されるような形になります。 - パフォーマンス:
- 非メインRactorで「最初に」primary_keyを決める際に、
メインRactorとのやり取りが発生しますが、主キー決定は通常は1テーブルあたり1回程度の処理であり、
実務上のオーバーヘッドは小さいと考えられます。
- 非メインRactorで「最初に」primary_keyを決める際に、
- もしアプリ側で独自に
primary_keyやreset_primary_keyをオーバーライドしている場合は、
Ractor環境での動作(特に「どのRactorで決定するか」)に影響がないかを確認した方が安全です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58577
- 関連ドキュメント:
- Ruby Ractor: https://docs.ruby-lang.org/en/master/Ractor.html
- Active Record プライマリキー設定:
https://guides.rubyonrails.org/active_record_basics.html#primary-keys
- このPRでは CHANGELOG は未更新で、「バグフィックスかつ振る舞いの変更は限定的」という扱いになっています。
#58559 Allow redis gem 6 in the Action Cable adapter on Rails 8.1
マージ日: 2026/8/26 | 作成者: @mugitti9
- 概要 (1-2文で)
Rails 8.1系のAction Cable Redisアダプタが、redisgem 6系も利用できるように依存バージョン制約を緩和したPRです。これにより、redis6.0.0 を利用するアプリケーションでも、Action Cable の Redis pubsub アダプタを問題なくロードできるようになります。
- 変更内容の詳細
2-1. 依存バージョン制約の変更
Action Cable の Redis サブスクリプションアダプタは、これまでロード時に以下のような制約を持っていました:
gem "redis", ">= 4", "< 6"このPRで、上限が 6 未満から 7 未満に引き上げられています:
gem "redis", ">= 4", "< 7"これにより、redis 6.x が有効化されている環境でも、Action Cable の Redis アダプタをロード可能になります。
具体的には、これまで redis 6.0.0 を入れた状態で Rails 8.1 を動かそうとすると、以下のようなエラーが発生していました:
Gem::LoadError: Error loading the 'redis' Action Cable pubsub adapter.
Missing a gem it depends on? can't activate redis (>= 4, < 6), already
activated redis-6.0.0.このPR後は、この依存関係の不整合が解消されます。
2-2. リポジトリの Gemfile のピン変更
リポジトリ内のテスト実行用 Gemfile では、#58344 で一時的に以下のように redis の上限が 6 未満にピンされていました:
gem "redis", "< 6"このPRで、これも "< 7" に変更され、CI 環境でも redis 6 系が解決されるようになります:
gem "redis", "< 7"Gemfile.lock もそれに合わせて更新されています(依存解決結果の更新のみで、ロジック変更はなし)。
2-3. アダプタ実装自体の変更はなし
actioncable/lib/action_cable/subscription_adapter/redis.rb では、実装コードのロジック変更はありません。
変更は「サポート対象バージョン定義のみ」で、以下のようなAction Cableから見た利用API (subscribe, _client, without_reconnect など) に影響するような修正は不要と判断されています。
- 影響範囲・注意点
3-1. 影響範囲
- Rails 8.1 系で Action Cable + Redis アダプタを利用しているアプリケーション
- かつ
redisgem 6.x を導入したい、あるいはすでに導入しているアプリケーション
これらのアプリは、今回の PR が取り込まれたバージョン以降であれば、特別な変更なしに redis 6.x と Action Cable Redis アダプタを併用できます。
3-2. 互換性について
redis-rb6 は、Action Cable が使う主な API については「ほぼドロップイン互換」とされています。- 主な変更点は「デフォルトプロトコルが RESP3 になったこと」ですが、古い Redis サーバに対しては自動で RESP2 にフォールバックするため、通常の利用においては特別な設定変更なしでも動く前提です。
- main ブランチではすでに Redis アダプタが
redisgem ではなくredis-clientベースに再実装されていますが、8.1-stable はまだ旧アダプタを維持しているため、このバージョン制約緩和が必要になっています。
3-3. アプリケーション側での注意点
すでに
redis6.x を利用していて、Action Cable 起動時にGem::LoadErrorが出ていた場合は、Rails 8.1 の該当パッチバージョンに更新することで解消が期待できます。独自に
redisバージョンを Gemfile でロックしている場合、例えば:rubygem "redis", "~> 5.0"のようになっていれば、今回の PR により「6 を使わなければならない」わけではなく、従来通り 5.x のまま利用可能です。
6.x を試したい場合は:rubygem "redis", "~> 6.0"などに変更すればよいです。
Redis サーババージョンがかなり古い場合でも、RESP2 フォールバックによって基本的には動作が維持される想定ですが、パフォーマンスや細かい挙動差が気になる場合は、ステージング環境などで動作検証してから本番に適用するのが無難です。
- 参考情報 (あれば)
- このPRでの上限引き上げは、過去のメジャーバージョンアップ対応と整合しています:
redis4.x 対応: #30748redis5.x 対応: #45856 / #45873
- main ブランチでの
redis-clientベース再実装:ef812c2652
→ 将来的に 8.2 以降などへアップグレードする場合は、そちらの新アダプタ実装へ移行することになります。
#58569 Remove unreachable default for DerivedSecretKeyProvider#derive_key_from
マージ日: 2026/8/26 | 作成者: @aryrabelo
- 概要 (1-2文で)
ActiveRecord::Encryption::DerivedSecretKeyProvider#derive_key_fromのキーワード引数using:に設定されていた到達不能なデフォルト値(key_generator)を削除し、必須キーワードに変更したクリーンアップ PRです。挙動は実質的に変わらず、「間違った呼び出し時の例外の種類」がNameErrorからArgumentErrorに変わる程度の影響に留まります。
- 変更内容の詳細
問題の背景
DerivedSecretKeyProvider には次のような構造があります(概念的なイメージ):
module ActiveRecord
module Encryption
class DerivedSecretKeyProvider
def initialize(key_generator:)
@key = derive_key_from(secret, using: key_generator)
end
private
def derive_key_from(secret, using: key_generator)
# ...
end
end
end
endここで問題になっているのは derive_key_from の定義部分です。
def derive_key_from(secret, using: key_generator)using:キーワード引数のデフォルトに指定されているkey_generatorは、initializeメソッドのキーワード引数名であり、- インスタンスメソッドでもインスタンス変数でもない ため、
derive_key_fromのスコープからは参照できません。
そのため、もし呼び出し側が using: を省略してデフォルトを使おうとすると、Ruby は key_generator をローカル変数やメソッドとして解決できず、NameError になります。
ところが実際のコードベースを調査すると:
derive_key_fromはprivateメソッド- 唯一の呼び出し元は
#initializeのみ - その
#initializeでは必ずusing: key_generatorを明示的に渡している - サブクラス (
DeterministicKeyProvider,EncryptedPost::MutableDerivedSecretKeyProvider) はderive_key_fromを直接呼ばず、initializeをオーバーライドするかアクセサを追加しているだけ send(:derive_key_from)やpublic_sendなどによるメタプログラミング的な直接呼び出しもリポジトリ全体で存在しない
つまり、「derive_key_from を using: 省略で呼ぶコード」は存在せず、「デフォルト値としての key_generator」には実行時に到達できません。
実際の修正
そのため、この PR では単純に:
using:キーワード引数を「デフォルト付き」から「必須キーワード」に変更しました。
イメージとしては以下のような差分です(PR では +1 / -1 行のみの変更):
# 変更前
def derive_key_from(secret, using: key_generator)
...
end
# 変更後
def derive_key_from(secret, using:)
...
endこれにより:
- すべての呼び出し側は
using:を必ず指定しなければならない - これはもともと実際の呼び出し側が行っていたことと一致するため、正常系の挙動は全く変わりません
- インターフェース(契約)としても「Generator は呼び出し側が必ず渡すもの」であることが明示されます
例外の違い
もし仮に、外部コードが using: を省略して直接呼んだ場合の挙動は以下のように変化します:
provider = ActiveRecord::Encryption::DerivedSecretKeyProvider.new(...)
# 想定外の直接呼び出し:
provider.send(:derive_key_from, "secret")- 変更前:
NameError(undefined local variable or methodkey_generator) - 変更後:
ArgumentError("missing keyword: :using")
PR 説明で示されている検証結果:
# before # after
default via send: NameError default via send: ArgumentError (missing keyword: :using)となっており、正常経路 (caminho normal) および DeterministicKeyProvider 経由では前後で同じ Encryption::Key が得られることも確認済みです。
- 影響範囲・注意点
正常な利用コードへの影響
- Rails 本体およびテストコードでは、
derive_key_fromは#initializeからのみ呼ばれ、using:が明示的に渡されているため、挙動は変わりません。 - 従って、この PR は「動作変更」ではなく「死んだコードのクリーンアップ」と位置づけられています。
- Rails 本体およびテストコードでは、
メタプログラミング等で private メソッドを直接呼んでいる場合
- 仮にアプリケーションやプラグイン側で
send(:derive_key_from, ...)などを行い、using:を省略していた場合:- 以前は
NameErrorが発生していた箇所が、 - 今後は
ArgumentError (missing keyword: :using)に変わります。
- 以前は
- いずれにせよそのコードは既に「動いていない」コードであり、今回の変更で初めて壊れる、というものではありません。
- 仮にアプリケーションやプラグイン側で
APIとしての契約の明確化
using:キーワード引数が「必須」であることがシグネチャに反映されたため、静的解析ツールや型チェッカにとっても扱いやすくなります。- PR は itaruby(作者が開発中の Ruby 型チェッカ)によって発見された問題であり、型・静的解析視点でも自然な修正です。
テストについて
- ローカル環境では
bin/testを実行できない状態だったものの、CI でのテストを前提とした PR です。 - 既存テストの挙動は変わらず、正常経路は変更前後で同一の結果となることが手動検証されています。
- ローカル環境では
- 参考情報 (あれば)
- この PR 以前の未使用コードクリーンアップ系 PR:
- #58217
- #58219
- #58223
- 変更ファイル:
activerecord/lib/active_record/encryption/derived_secret_key_provider.rb(1 行追加 / 1 行削除)
- PR で言及されているツール:
- itaruby: 著者が開発中の Ruby 型チェッカで、今回の到達不能なデフォルト引数を検出したとのこと。
#58567 Remove unused request helper from RouteSetTest
マージ日: 2026/8/26 | 作成者: @aryrabelo
- 概要 (1-2文で)
RouteSetTest内で定義されていた、実際には一度も使われておらず、呼び出すと例外が出てしまうrequestヘルパーメソッドを削除する PR です。テストコード中の「死んだコード」を整理することで、テストスイートの可読性と健全性を高めています。
- 変更内容の詳細
何が消されたか
対象ファイル:
actionpack/test/controller/routing_test.rbこのファイル内に定義されていた RouteSetTest#request というインスタンスメソッドが削除されています(4行削除のみ、追加行なし)。
削除されたメソッドは概ね以下のような形だったと考えられます(PR文脈からの再構成):
def request
@request ||= ActionController::TestRequest.new
endRuby の標準的な「メモ化」パターンですが、ここで使っている ActionController::TestRequest.new は、実際には以下のように3つの必須引数を取るコンストラクタです:
# actionpack/lib/action_controller/test_case.rb:69 付近
def initialize(env, session, controller_class)
end引数なしで呼ぶと ArgumentError になります。
なぜ「死んだコード」と言えるのか
PR では、次の2つの観点から「絶対に使われていない」と論証しています。
呼び出し側が存在しない (grep による検証)
RouteSetTestクラス本体でrequestというメソッド名が出てくるのは:request_path_params呼び出し(これはRoutingVerbsモジュール側のメソッドでcontroller.requestを使う)lambda { |request| ... }のブロック引数(ローカル変数)
actionpack/test以下でsend(:request)public_send(:request)respond_to?(:request)
などの間接呼び出しも存在しない。
method_missingやdelegateによる動的ディスパッチもこのファイルにはない。
呼ばれたら必ず落ちるシグネチャの不一致
ActionController::TestRequest#initializeは(env, session, controller_class)の3引数必須。- 対象ヘルパーはこれを引数なしで呼ぶ。
- 従ってこのメソッドが実際に呼ばれていれば、確実に
ArgumentErrorでテストが落ちるはず。 - テストスイートはグリーンであるため、このメソッドは到達しない = 実際にどこからも呼ばれていない。
他の候補になりそうな呼び出し元の調査
RouteSetTest には以下のモジュールが include されています。
RoutingTestHelpersActionDispatch::RoutingVerbs
PR では「ここから request が間接的に呼ばれていないか」を確認しています。
ActionDispatch::RoutingVerbsは、実際にはroutes.call(params)を通じてルーティングを評価しcontroller.requestを参照している
だけで、テストクラスのインスタンスメソッドrequestは使わない。
RoutingTestHelpers側も、requestメソッドを定義も呼び出しもしていない。
また、テストスイートで実際に使われている Request ファクトリは:
TestRequest.create(controller_class)であり、問題の ActionController::TestRequest.new 直接呼び出しパターンは使われていないことが指摘されています。
- 影響範囲・注意点
影響範囲
- 実行時の挙動は変わらない
対象メソッドは一度も呼ばれておらず、呼ばれたら即ArgumentErrorで落ちる状態だったため、削除による動作変更はありません。 - 影響するのは actionpack のテストコード内のみ
actionpack/test/controller/routing_test.rbの内部実装のクリーンアップであり、ライブラリ本体のコード (app/,lib/以下) には一切影響しません。
注意点 / 想定される懸念
- 外部からこのヘルパーを期待していた場合?
Rails 本体のテストクラス (RouteSetTest) のプライベートな補助メソッドであり、外部(アプリケーション側や別 gem)から利用される想定の API ではありません。そのため互換性の問題は事実上ありません。 - 将来
RouteSetTestでrequestを使いたくなった場合
そのときにActionController::TestRequestを新しく正しいシグネチャで組み立てる必要があります。現時点では使われていないので、必要になったタイミングで正しい形で追加するのが適切です。
- 参考情報 (あれば)
- 関連 PR(同様の未使用コードクリーンアップ):
- #58217
- #58219
- #58223
ActionController::TestRequestの正しい利用パターン(テストスイート内で一般的なもの):rubyrequest = TestRequest.create(controller_class)- この未使用コードは、作者が開発中の Ruby 型チェッカー「itaruby」による解析で発見された、と PR の末尾で言及されています。
#58568 Remove unused Active Record internals
マージ日: 2026/8/26 | 作成者: @aryrabelo
- 概要 (1-2文で)
Active Record / Arel 内部の実際には到達不能なコード(死んでいる分岐・メソッド呼び出し)を削除し、代わりに本来使われている経路にはテストを追加してカバーした PR です。挙動の変更はなく、型チェックや静的解析上のノイズを減らすリファクタリングです。
- 変更内容の詳細
2-1. HomogeneousIn#fetch_attribute の不要な分岐削除
対象ファイル: activerecord/lib/arel/nodes/homogeneous_in.rb
Arel::Nodes::HomogeneousIn#fetch_attribute には、ざっくり言うと「attribute がなければ expr を使う」というフォールバック分岐がありましたが、この expr はこのクラス階層では定義されておらず、実行されれば NameError になるだけの「到達不能な死んだコード」でした。
- 実際のコード形(イメージ)ruby
def fetch_attribute if attribute attribute else expr # ← HomogeneousIn では定義されておらず、呼ばれると NameError end end
PR ではこの else 側の分岐が削除され、事実上「attribute だけを前提にした実装」に整理されています。fetch_attribute 自体は残されており、Arel.fetch_attribute および Arel::Nodes::Node#fetch_attribute を通じて引き続き利用されます。
なぜ expr フォールバックは完全に死んでいたのか
HomogeneousIn < Nodeであり、exprを持つのはArel::Nodes::Unary(attr_accessor :expr)のみ。HomogeneousInはUnaryを継承しておらず、exprメソッドは存在しない。HomogeneousInの生成箇所では常に有効なattributeが渡されている。PredicateBuilder::ArrayHandlerHomogeneousIn#invert- そのクラス自身のテスト
- さらにクラス内の別メソッド
#right,#casted_values,#proc_for_bindsが、attributeに対して無条件にメソッド呼び出しを行っており、attributeがnilだった場合にはそちらが先に例外を投げる。
上記の理由から、「attribute が falsy で expr にフォールバックする」というシナリオ自体が、コード構造的にありえないと判断されています。
2-2. HomogeneousIn#fetch_attribute のテスト追加
対象ファイル: activerecord/test/cases/arel/nodes/homogeneous_in_test.rb
これまで homogeneous_in_test.rb では #to_sql のみがテストされており、#fetch_attribute はテストされていませんでした。
PR では、削除しないメインパス(正常に attribute を返す経路)のみをテストでカバーしています。
テスト内容の要点:
Arel::Table.new(name: :users)[:name]からHomogeneousInを組み立ててfetch_attributeを呼び、その返り値を検証。- PR 説明にある動作例:ruby
# before fetch_attribute yielded: #<struct Arel::Attributes::Attribute relation=#<Arel::Table @name="users">, name="name"> Arel.fetch_attribute: "users" equality?: true invert: :notin # after fetch_attribute yielded: #<struct Arel::Attributes::Attribute relation=#<Arel::Table @name="users">, name="name"> Arel.fetch_attribute: "users" equality?: true invert: :notin - つまり、PR 前後で挙動が完全に一致することを、実運用と同じルート (
Arel.fetch_attribute) を通して確認しています。
作者も明示しているように、これは「削除した分岐の回帰テスト」ではなく、「これまでテストがなかったメソッドの正常系カバレッジを追加する」目的のテストです。
2-3. Arel の “Fake Record” の columns_hash 削除
対象ファイル: activerecord/test/cases/arel/support/fake_record.rb
テスト用の「フェイクレコード」実装内に、次のような columns_hash がありました(要旨):
def columns_hash
@connection.columns_hash # 引数なしで呼び出している
end一方で、対応する FakeConnection#columns_hash のシグネチャは:
def columns_hash(table_name)
...
endとなっており、呼び出し側から引数が渡されないため、実行されれば必ず ArgumentError となります。
PR では:
- この
FakeRecord#columns_hashメソッドを削除。 - リポジトリ全体を grep した結果、このフェイクレコードの
columns_hashが呼ばれていないことを確認済み。 - ダイナミックディスパッチ (
send,public_send,respond_to?,method_missing,delegate) でこのメソッドが到達するパスも存在しないことを確認。
つまり、こちらも完全に未使用かつ呼ばれればクラッシュするコードの削除です。
- 影響範囲・注意点
外部 API への影響は実質ゼロ
- 削除対象はいずれも「到達したら例外になるだけの死んだコード」であり、正常に動作しているアプリケーションがこの分岐に依存している可能性はありません。
HomogeneousIn#fetch_attributeの公開インターフェイス(戻り値の形・意味)は変わっていません。
到達可能性の検証がかなり厳密
expr定義の有無を grep (grep -c 'def expr') で確認。columns_hashの呼び出し箇所をリポジトリ全体で確認。send(:columns_hash),public_send,respond_to?,method_missing,delegateなど、メソッド名ベースでの動的呼び出しもないことをチェック。- クラス継承関係 (
HomogeneousIn < NodeとUnaryの違い) を踏まえて、「本当に存在しないメソッドである」ことを整理。
テストについて
- PR 作成者の環境では
bin/testを実行できていませんが、CI でカバーされる前提。 HomogeneousIn#fetch_attributeに新規テストが追加されており、少なくとも「現実に使っている経路」については将来の退行を防ぐ仕組みが強化されました。
- PR 作成者の環境では
もし独自に Arel の内部クラスを継承・モンキーパッチしている場合
- 通常のアプリケーションでは問題になりませんが、Arel の
HomogeneousInを直接継承/再定義し、exprにフォールバックする動作を意図的に使っていたような非常に特殊なコードがあると、今回のクリーンアップによりコンパイル時(読み込み時)に気付きやすくなるかもしれません(ただし、そのようなコードはそもそも実行すれば例外になっていたはずです)。
- 通常のアプリケーションでは問題になりませんが、Arel の
- 参考情報 (あれば)
- この PR は、未使用コードの整理シリーズの一部です:
- #58217
- #58219
- #58223
- 死んだ分岐の発見には、作者が開発中の Ruby 型チェッカー「itaruby(まだ未リリース)」が使われたと明記されています。
→ 型チェックや静的解析導入の効果として、「理論上存在するが絶対に到達しない経路」を整理できる好例です。
#58580 Get rid of QueryCacheRegistry#clear
マージ日: 2026/8/26 | 作成者: @byroot
- 概要 (1-2文で)
ActiveRecord::QueryCacheRegistryに定義されていた未使用メソッド#clearが削除されました。機能的な挙動変更はなく、不要なデッドコードの整理のみです。
- 変更内容の詳細
対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb変更内容:
QueryCacheRegistry#clearメソッドの定義が削除されました(6行削除、追加なし)。
PR説明にもある通り、このメソッドはどこからも呼ばれていない「dead code」と判断され、クリーンアップとして削除されています。背景:
- byroot 氏が前の PR (#58566) とあわせて Query Cache 周りを整理しており、その一環として、利用されていない API を削除しています。
- Rails 内部でも外部でも参照されていないことが確認されたため、後方互換性をほぼ気にせずに削除可能と判断されたと考えられます。
- 影響範囲・注意点
Rails 本体の挙動:
- 内部から
QueryCacheRegistry#clearが呼び出されていなかったため、Active Record のクエリキャッシュ機構の挙動自体には変更はありません。 - クエリキャッシュの有効化/無効化やスコープ管理(
ActiveRecord::Base.cacheなど)には影響しません。
- 内部から
アプリケーションコード・ライブラリへの影響:
「内部クラスである
ActiveRecord::ConnectionAdapters::QueryCacheRegistryに直接アクセスし、その#clearメソッドを呼んでいた」ようなコードがある場合のみ、NoMethodError になる可能性があります。そのようなケースはかなりレアですが、以下のようなコードを書いていた場合は要注意です:
ruby# (非推奨な使い方の例) registry = ActiveRecord::ConnectionAdapters::QueryCacheRegistry.current registry.clear # ← この呼び出しは Rails 本体では削除されたもし独自にクエリキャッシュをクリアしたい用途があるなら、以下のように公開インターフェイスを使うのが望ましいです:
ruby# 特定の接続に対してキャッシュをクリア ActiveRecord::Base.connection.clear_query_cache # 一時的にキャッシュを無効化 ActiveRecord::Base.uncached do # このブロック内はキャッシュを利用しない end
バージョンアップ時の確認:
QueryCacheRegistry自体が内部実装寄りのクラスであるため、通常のアプリケーションで影響を受ける可能性は低いです。- gem / plugin / engine 開発者で、Active Record の内部 API に依存している場合は、念のため
QueryCacheRegistry#clearを使っていないか grep して確認すると安全です。
- 参考情報 (あれば)
- この PR:
- 関連 PR(Close されている元 PR):
- クエリキャッシュの公式ガイド(背景理解用):
- Rails Guides: Caching with Rails > SQL Caching
https://guides.rubyonrails.org/caching_with_rails.html#sql-caching
- Rails Guides: Caching with Rails > SQL Caching
#58554 Add config.active_record.schema_ignored_tables
マージ日: 2026/8/26 | 作成者: @edcarvio
- 概要 (1-2文で)
Rails の Active Record に、新しい設定config.active_record.schema_ignored_tablesが追加され、これまで別々だったSchemaDumper用 (ignore_tables) とSchemaCache用 (schema_cache_ignored_tables) の無視テーブル設定が統合されました。これに伴い、既存の2つの設定は非推奨となり、テーブル名のマッチ方法も「物理名ベース」に統一されています。
- 変更内容の詳細
新しい設定項目: config.active_record.schema_ignored_tables
これまで:
- スキーマダンプ (
schema.rb生成) から除外:ActiveRecord::SchemaDumper.ignore_tables - スキーマキャッシュ (
schema_cache.ymlなど) から除外:config.active_record.schema_cache_ignored_tables
今回から:
- 両方をまとめて制御する設定として
config.active_record.schema_ignored_tables
が導入されました。
典型的な設定例:
# config/application.rb など
config.active_record.schema_ignored_tables = [
/^_/, # 先頭が "_" のテーブルを無視
"legacy_users", # 単一テーブル名
->(table_name) { ... }, # Proc も引き続き利用可能(従来と同様の形)
]これにより、次の2つの振る舞いを同時に制御できます。
rake db:schema:dump等でのschema.rb出力から除外される- スキーマキャッシュに載せない
旧設定の扱い(非推奨)
次の2つは非推奨になりましたが、互換性のために引き続き利用できます:
ActiveRecord::SchemaDumper.ignore_tablesconfig.active_record.schema_cache_ignored_tables
ただし、内部的にはどちらも schema_ignored_tables に委譲されます。
ポイント:
- 読み書きするときにdeprecation warning を出しつつ、
schema_ignored_tablesに値を転送する - 「現在有効な設定」は常に
config.active_record.schema_ignored_tablesの値だけになる
移行イメージ:
# 旧: SchemaDumper だけ
ActiveRecord::SchemaDumper.ignore_tables = [/^_/]
# 旧: SchemaCache だけ
config.active_record.schema_cache_ignored_tables = [/^_/]
# 新: 両方まとめて
config.active_record.schema_ignored_tables = [/^_/]両方を同時に設定している場合の挙動
旧APIを両方とも設定しているアプリでは、次のようになります:
# 例: どちらも使っている場合
ActiveRecord::SchemaDumper.ignore_tables = [/^_/]
config.active_record.schema_cache_ignored_tables = ["foo"]- 内部的には両方とも
schema_ignored_tables = ...を呼び出しているので、 - 最後に代入された値だけが有効になります(この例では
["foo"]のみ)
この点は、今回の PR で明示的にドキュメント化された仕様です。
- 影響範囲・注意点
1) 「物理テーブル名」でマッチするように統一された(Breaking change)
これが実質的な Breaking change です。
従来:
SchemaDumper.ignore_tables
→ マッチ対象は「prefix/suffix を取り除いた論理名」(remove_prefix_and_suffix済み)SchemaCache側 (schema_cache_ignored_tables)
→ マッチ対象は「DB上の物理名」
今回から:
- どちらも「物理テーブル名」でマッチします
具体例:
config.active_record.table_name_prefix = "omg_"
config.active_record.schema_ignored_tables = [/^_/]
# 実DBテーブル名: omg__rats
# - 論理名: _rats
# - 物理名: omg__rats- 以前の
SchemaDumper:_ratsに対して/^_/をマッチ → これは一致するので schema.rb から除外
- 以前の
SchemaCache:omg__ratsに対して/^_/をマッチ → 一致しないので 除外されない
- 今回以降:
- 両方とも
omg__ratsに対して/^_/をマッチ → 一致しない → 両方とも除外されない
- 両方とも
この挙動統一の結果:
- prefix / suffix を使っているアプリで、正規表現や名前マッチの前提が変わる可能性があります
- 例えば、「先頭
_のテーブルを除外したい」なら、物理名に対する条件に書き換える必要があります
例:
# prefix 付きで物理名に "_xxx" が来るケースに対応したい場合
config.active_record.table_name_prefix = "omg_"
config.active_record.schema_ignored_tables = [
/\Aomg__/, # prefix + "_" が物理名先頭にくるパターン
]テーブルを無視する条件に prefix / suffix を考慮していなかったアプリは、
実データベース上のテーブル名をベースに見直す必要があります。
2) 内部テーブルの扱い(schema_migrations / ar_internal_metadata)
schema_migrations と ar_internal_metadata は、Rails 内部管理用の論理名をベースに扱われています。
今回:
- これらのテーブル名は
add_prefix_and_suffixで物理名に変換された上で ignore リストに入れられるようになりました - こうすることで、prefix / suffix を使っていても、これらが
schema.rbに出てこないままになるよう維持されています
つまり、開発者側で特別な対応は不要ですが、
- 「prefix を使うと内部テーブルが schema.rb に出てくるようになった」
といった回帰は起こらないようになっています。
3) Rake タスク・テスト周りの更新
- MySQL / PostgreSQL / SQLite / Trilogy 用の DB タスクで、内部的に利用している ignore 設定の参照先が新設定に追随
schema_dumper_testやschema_cache_testなどで、新設定・新挙動に合わせたテストケースが追加/更新されていますguides/source/configuring.mdにschema_ignored_tablesが追記され、設定ガイドも更新済み
- 参考情報 (あれば)
- PR 本体:
https://github.com/rails/rails/pull/58554 - PR 内の挙動議論(特に物理名 vs 論理名の話):
https://github.com/rails/rails/pull/58554#issuecomment-5409182565 - 実運用での移行ガイドライン(まとめ):
ActiveRecord::SchemaDumper.ignore_tablesとconfig.active_record.schema_cache_ignored_tablesは 新設定に置き換える- 全て
config.active_record.schema_ignored_tablesに一本化する - prefix / suffix を使っている場合は、「物理テーブル名」でマッチしているか確認する
(DB の実テーブル名を\dなどで確認しつつ、正規表現や文字列リストを見直す)
#58570 Fix RedisCacheStore clear and stats with redis-client
マージ日: 2026/8/25 | 作成者: @luizkowalski
- 概要 (1-2文で)
Redis 用キャッシュストアRedisCacheStoreが、redis-clientを使った場合にclearとstatsでNoMethodErrorが出る問題を修正した PRです。FLUSHDBとINFOコマンドの呼び出し方法をRedisClient#callベースに揃え、回帰テストを追加しています。
- 変更内容の詳細
問題の背景
RedisCacheStore は内部で Redis クライアントに対して以下の操作を行っています。
#clear(キャッシュ全削除)でFLUSHDBを実行#stats(統計取得)でINFOを実行
従来の redis gem(Redis クラス)では flushdb や info といったメソッドが直接定義されていましたが、redis-client 側ではこれらのメソッドが用意されておらず、「call メソッドで任意コマンドを送る」スタイルになっています。
そのため、redis-client を渡した場合に:
redis.flushdbのようなメソッド呼び出し →NoMethodErrorredis.infoのようなメソッド呼び出し →NoMethodError
となってしまう、というバグがありました。
関連 issue: https://github.com/redis-rb/redis-client/issues/311
具体的な修正内容
1) RedisCacheStore#clear の修正
FLUSHDB コマンド実行を、クライアントが持つ call メソッド経由で行うように変更しています。
イメージとしては、以下のような変更です(概念的なサンプル):
# 変更前(イメージ)
@data.flushdb
# 変更後(イメージ)
@data.call("FLUSHDB")これにより、以下の両方に対応できます。
- 従来の
Redisクラス(callを実装している) redis-clientのクライアント(callだけを提供する実装)
特に「unnamespaced cache」(名前空間なしで全 DB を使っている構成)で clear を呼んだときに NoMethodError が出ていた問題が解消されます。
2) RedisCacheStore#stats の修正
同様に、Redis の INFO コマンドを call 経由で実行するようにしています。
概念的なサンプル:
# 変更前(イメージ)
info = @data.info
# 変更後(イメージ)
info = @data.call("INFO")これにより、redis-client を使った構成でも stats が動作し、キャッシュストアの統計情報が取得できるようになります。
3) 回帰テストの追加
activesupport/test/cache/stores/redis_cache_store_test.rb にテストが追加されています。ポイントは:
- 「
callメソッドだけを持つ擬似クライアント」を用意 - そのクライアントを
RedisCacheStoreに渡して#clearが適切にFLUSHDBを発行するか#statsが適切にINFOを発行し、結果をパースできるか
を検証しています。
これにより、今後も redis-client のような call ベースのクライアント実装との互換性が保たれるようになっています。
- 影響範囲・注意点
影響範囲
ActiveSupport::Cache::RedisCacheStoreを利用し、かつバックエンドクライアントにredis-client(または同様にcallだけを提供するクライアント)を使っているアプリケーションでのclear/statsの挙動。- 従来の
redisgem (Redisクラス) を使っている場合も、callはもともと存在するため後方互換性は維持されます。
実運用上の注意点
- 「Redis クライアントが
callを実装している」ことが前提条件になります。- 何らかのカスタムクライアントを使っていて
callが無い場合は引き続き問題が出る可能性があります。
- 何らかのカスタムクライアントを使っていて
clearはFLUSHDBを叩くため、その接続先 Redis の「現在選択中の DB 全体」が消える点は変わりません(namespaced でない構成ほど影響が大きいので、従来通り注意が必要)。statsはINFOの結果をパースして使うため、Redis 側のバージョン差異でレスポンス形式が変わるケースでは、従来通りパース処理への影響も考えられますが、この PR 自体は「呼び出し方の変更」のみです。
- 「Redis クライアントが
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58570
- 関連 issue (
redis-client側): https://github.com/redis-rb/redis-client/issues/311 - 修正対象ファイル:
activesupport/lib/active_support/cache/redis_cache_store.rbactivesupport/test/cache/stores/redis_cache_store_test.rb
#58552 Add Herb as an HTML-aware ERB implementation
マージ日: 2026/8/25 | 作成者: @marcoroth
- 概要 (1-2文で)
Rails の Action View に、HTML 構造を理解する ERB 実装「Herb::Engine」を使うためのハンドラActionView::Template::Handlers::ERB::Herbが追加されました。これにより、HTML+ERB テンプレートを構文木ベースで解析しつつ、現行の Erubi とほぼ互換なインターフェイスで利用できる土台が整えられています。
- 変更内容の詳細
2-1. 追加されたコンポーネントの概要
- 新規クラス:
ActionView::Template::Handlers::ERB::HerbHerb::Engineを継承ActionView::Template::Handlers::ERB::Erubiと API 互換になるように実装
- 依存関係:
actionview.gemspecにherbgem を追加Gemfile.lockに Herb のエントリ追加
- テスト:
actionview/test/template/erb/herb_test.rbで Erubi 実装と同等の振る舞いを確認するテストが多数追加
2-2. Herb::Engine の特徴と Rails への組み込み方
Herb は以下の2つのレイヤで解析を行います:
HTML + ERB を「一つの構文木」として解析
- HTML タグ・テキスト・ERB タグが一体として扱われる
- テンプレート全体の HTML 構造を把握できる
ERB 内の Ruby コードを Prism でパース
<% ... %>内の Ruby 構文を正しく解析- ブロック式の判定などを正規表現ではなく構文木ベースで実施
Rails 側の新しい Herb ハンドラは、既存の Erubi ハンドラと同じプロトコルを保っています:
- 同じ
@output_bufferを使う - 文字列の連結プロトコルも同じ (例:
@output_buffer.safe_append=系) - 改行の扱い・バッチングも同じ
# frozen_string_literal: true対応も踏襲- エスケープ処理は従来通り
ActionView::OutputBufferに委譲 (コンパイル時には行わない)
唯一の主な違いとして:
- ブロック式 (
<% if ... %>,<% @items.each do |item| %>など) の判定を、
Erubi のBLOCK_EXPR正規表現ではなく Herb の構文木 (Prism) に基づいて行う点が明示されています。
2-3. 実際の利用イメージ (概念ベース)
今回の PR では「内部実装として使える状態にした」だけで、デフォルト切り替えや設定オプションは今後の PR で追加予定です。
ただし、API としては以下のように差し替え可能な設計になっています:
# Herb 実装に切り替える(テストや実験用途を想定)
ActionView::Template::Handlers::ERB.erb_implementation =
ActionView::Template::Handlers::ERB::Herbこのとき .html.erb テンプレートのコンパイルには Herb::Engine が使われますが、出力される Ruby コードは Erubi とほぼ同じ形になるよう設計されています。
今後予定されている follow-up PR では:
- 「HTML テンプレートだけ Herb を使い、それ以外 (text, js.erb 等) は Erubi のまま」というルーティング設定
- そのための Rails 設定オプション (例:
config.action_view.herb_for_html_templates = trueのようなもの) - CHANGELOG の更新
が提案されています。
- 影響範囲・注意点
3-1. 影響範囲
- actionview:
- 新しい ERB 実装候補が追加されただけで、現時点でデフォルトは Erubi のまま
- 通常の Rails アプリは、この PR がマージされただけでは挙動は変わりません
- 依存ライブラリ:
herbgem が Action View の依存に追加- gem インストール時に Herb が一緒に入るようになる
3-2. 技術的なメリット (将来的なものを含む)
Herb を使うことで、以下のようなことが技術的に可能になります:
コンパイル時に HTML+ERB 全体を検証
- Ruby と HTML が組み合わさった結果として不正な構文になるケースを検出
better-htmlが外部から行っていた「HTML-aware な制約」をコンパイラ側で直接担保可能
より正確なエラーメッセージ
- 構文木ベースなので、「どのタグ内のどの ERB が悪いのか」をより正確に特定できる余地がある
開発体験 (DX) の強化
- Herb は linter/formatter/言語サーバ (LSP) 等のツールと同じパーサ/構文木を共有
- エディタ上の補完・フォーマット・警告内容と、実際のコンパイル結果の解釈が一致しやすくなる
パフォーマンス最適化の余地
- テンプレート構造が分かるため、将来的に出力最適化や事前検証などの工夫がしやすい
3-3. 注意点
- 現時点での利用は「明示的に切り替えた場合のみ」
→ 誤って本番環境で切り替えると、互換性の差分 (intentional divergence) による影響が出る可能性があるため、ドキュメント/Changelog が整うまでは本番適用は慎重に行うべきです。 - Herb 側では Erubi 互換性テストスイートが用意されており、byte レベルで一致する箇所と、あえて変えている箇所の両方が固定化されています。
→ それでも、独自メタプログラミングや内部 API に依存したテンプレートでは挙動確認が必要です。
- 参考情報 (あれば)
Herb 公式サイト
https://herb-tools.devHerb リポジトリ (Prism ベースのパーサ/エンジン)
https://github.com/marcoroth/herbErubi 互換テストスイート
better-html (既存の HTML-aware ERB チェッカー)
https://github.com/Shopify/better-html
#58563 Fix PostgreSQL exclusion constraints with multiline expressions
マージ日: 2026/8/25 | 作成者: @jakemcallister
- 概要 (1-2文で)
PostgreSQL の排他制約(exclusion constraint)定義に改行を含む場合、Active Record がそれを正しくパースできずexclusion_constraints呼び出しやスキーマダンプが壊れる不具合を修正した PR です。制約定義をパースする正規表現に「複数行マッチ」を有効化し、CASE 式を含む排他制約でも正しく読み取れるようにしています。
- 変更内容の詳細
何が問題だったか
- PostgreSQL では、
pg_get_constraintdefで取得できる排他制約定義が、内部表現を「整形して」返すため、CASE 式などを含むと自動的に複数行(改行入り)になることがあります。 - Active Record は PostgreSQL の排他制約を読む際に、
pg_get_constraintdefの戻り値を 正規表現でパースしていましたが、その正規表現が.を使っていながら デフォルト(1行モード)のままだったため、「.が改行にマッチしない」状態でした。 - その結果、改行を含む定義では正規表現マッチが
nilになり、exclusion_constraints呼び出し時に例外が発生するdb/schema.rbなどのスキーマダンプで該当テーブルが正しく出力されない
という不具合が発生していました。
ポイントとして、マイグレーション側で1行に書いても無意味で、PostgreSQL が保存している式をデパースする際に勝手に改行付きに整形してしまうため、アプリ側で書き方を工夫しても回避できない問題です。
この PR で行った修正
対象ファイル:
activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb修正内容の本質は、排他制約定義をパースする正規表現に対して マルチライン(改行を含む)マッチを有効化したことです。
- Ruby で言うと、
/ ... /mフラグをつける、もしくはRegexp::MULTILINEを有効にする変更が 1 行入っています。 - これにより、制約定義に含まれる式(
CASE ... ENDなど)が複数行に分かれていても、.*/.+の部分が改行をまたいでマッチするようになります。
- Ruby で言うと、
(実際のコードは PR 本体になりますが、イメージとしては以下のような変更です)
# 変更前(例)
CONSTRAINT_DEFINITION = /
EXCLUDE USING (.+) WITH (.+) WHERE (.+)
/x
# 変更後(例)
CONSTRAINT_DEFINITION = /
EXCLUDE USING (.+) WITH (.+) WHERE (.+)
/mx # ← m フラグ追加で改行にもマッチ※上記は説明用の擬似コードです。実際の正規表現の中身やキャプチャ数は Rails 本体の実装に依存します。
テストの追加
対象ファイル:
activerecord/test/cases/migration/exclusion_constraint_test.rb追加されたテストは「CASE 式を含み、改行を伴う排他制約」を DB に作成し、Active Record 経由でその制約を読み出したときに、式が正しく取得できるかを確認する回帰テストです。
概要イメージ:
create_table :events do |t|
t.daterange :period
t.integer :category
end
# CASE を含む排他制約を作成(PostgreSQL 側で複数行に整形される想定)
add_exclusion_constraint :events,
"CASE WHEN category = 1 THEN period END WITH &&",
using: :gist, name: "events_category_period_excl"
constraints = exclusion_constraints("events")
expr = constraints.first.expression
# ここで expression に CASE を含む定義が入っていることを期待
assert_match(/CASE WHEN category = 1/, expr)(テストコードもイメージです。実テスト名や細部は PR に従います。)
- このテストはターゲット回帰テストとして 1 回実行され、成功しているほか、フル PostgreSQL テストスイート(1万件以上のテスト)でも問題なく通過しています。
- 影響範囲・注意点
影響を受けるのは PostgreSQL を使っていて、排他制約を利用している環境 かつ、以下のような状況です:
- 排他制約の定義に
CASEなどを含み、pg_get_constraintdefが 改行を含む定義文字列を返すパターン
- 排他制約の定義に
そのような制約を既に使っているアプリでは、Rails をこの修正を含むバージョンに上げることで:
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#exclusion_constraintsが正常に動くようになるdb/schema.rb(またはstructure.sqlを経由した再生成プロセス)で該当テーブルの排他制約が正しく反映される
という実利のある修正になります。
マイグレーション側の書き方(1行/複数行)では回避できない問題だったため、「CASE を含む排他制約を定義しているのにスキーマダンプに出てこない/読み込みで落ちる」ようなケースは、この修正で解消されます。
正規表現のマッチモードを変えただけなので、既存の単一行の排他制約定義に対しての挙動は変わらない想定です(後方互換性は高い修正です)。
- 参考情報 (あれば)
- この PR は
WHERE句パースの問題を扱う #58107 とは別件で、「排他制約の本体定義(特に CASE 式を含む部分)」に対するバグ修正です。 - 検証状況:
- ターゲット回帰テスト: 1 run, 4 assertions, 0 failures, 0 errors
- PostgreSQL 全体テストスイート: 10,690 runs, 38,546 assertions, 0 failures, 0 errors, 28 skips
- RuboCop: 変更箇所に違反なし
git diff --check: 問題なし(余分な空白等なし)
PostgreSQL の高度な制約機能(排他制約 + CASE 式)を使うアプリで、スキーマダンプとスキーマロードの一貫性を担保するうえで重要なバグフィックスと言えます。
#58562 Make en.rb translation load path contents ractor safe
マージ日: 2026/8/25 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Rails が内部で設定している I18n の英語ロケール (en.rb) のロードパス用Procを、Ruby の Ractor から安全に共有できる形に変更した PRです。これにより、Ractor を用いた並行実行環境でも Rails のデフォルト I18n 設定が問題なく動作するようになります。
- 変更内容の詳細
何をしている PRか
activesupport/lib/active_support/locale/en.rb内で、I18n ロードパスに設定しているProcを「Ractor セーフな Proc」に変換するためのヘルパを利用するように変更しています。- その動作を検証するテストが
activesupport/test/i18n_test.rbに追加されています。
Rails では、activesupport がデフォルトの英語ロケールデータを Proc 経由で I18n に提供していますが、従来の形だとその Proc が Ruby 3 の Ractor 仕様上「shareable ではない」と判定されるケースがありました。
この PR では、既存の「Ractor 用ヘルパ」を使って、その Proc 自体を Ractor 間で共有可能なオブジェクトにキャストしています。
具体的なコードイメージ
実際の差分は小さいですが、イメージとしては次のような変更です(概念的な擬似コード):
# 変更前(イメージ)
I18n.load_path << proc {
# en ロケールのハッシュを返す
}
# 変更後(イメージ)
I18n.load_path << Rails::RactorHelpers.ractor_safe_proc(proc {
# en ロケールのハッシュを返す
})実際には Rails 内部の Ractor ヘルパ(例えば Ractor.make_shareable をラップしたようなメソッド)を用いて、
Proc内部でキャプチャしているオブジェクトが Ractor shareable になるようにする- もしくは shareable 判定をパスする形に変換する
といった処理が行われています。
テスト追加
activesupport/test/i18n_test.rb には、主に次のような観点のテストが追加されていると考えられます。
ActiveSupportが提供するenロケールのProcが、Ractor から参照してもエラーにならないこと- Ractor 内で I18n を使って英語ロケールの翻訳を読み込んでも正常に動くこと
これにより、Ractor 環境での回帰を防止します。
- 影響範囲・注意点
影響範囲
- 影響する対象は、Rails がデフォルトで提供する英語ロケール (
activesupport/lib/active_support/locale/en.rb) のロード方法のみです。 - アプリケーション側で独自に設定している
I18n.load_pathやProcには直接の変更はありません。ただし、それらが Ractor セーフでない場合は、依然として Ractor 使用時に問題が起きる可能性があります。 - Ractor を利用しない通常のアプリケーション実行では、挙動面での違いは基本的にありません(翻訳結果は変わらない前提の変更)。
- 影響する対象は、Rails がデフォルトで提供する英語ロケール (
Ractor 利用時の恩恵
- マルチ Ractor 構成(将来的な並行 Web サーバー・ジョブワーカーなど)で Rails を動かした際に、
- 「I18n のロードパスにある Proc が shareable でない」ことに起因するエラーや警告を避けられます。
- Ractor 起動前に I18n を初期化しても、それを Ractor に安全に共有できるようになります。
- マルチ Ractor 構成(将来的な並行 Web サーバー・ジョブワーカーなど)で Rails を動かした際に、
注意点 / 今後検討事項
- この PR は Rails コアが提供する
enロケールに限定した修正です。- アプリケーションや gem が追加する他ロケールや
Procベースの I18n 設定も Ractor セーフにしたい場合は、それぞれで同様のヘルパを利用するか、Ractor セーフなデータ構造のみをキャプチャするように実装する必要があります。
- アプリケーションや gem が追加する他ロケールや
- CHANGELOG は更新されておらず、「外から見える仕様変更」ではなく内部実装寄りの安定性向上として扱われています。
- この PR は Rails コアが提供する
- 参考情報 (あれば)
- Ruby Ractor の shareable オブジェクト仕様:
Ractor.shareable?による判定Ractor.make_shareable(obj)によるオブジェクトの共有化
- Rails 内部の Ractor 対応の流れ:
- Active Support 各所で Ractor セーフティを高める変更が段階的に行われており、本 PR はその一環として「I18n のデフォルトロケール Proc」を対象にしたものと考えられます。
#58553 Escape attachment captions in Attachment#to_markdown
マージ日: 2026/8/25 | 作成者: @flavorjones
- 概要 (1-2文で)
ActionText::Attachment#to_markdownが添付ファイルの caption(キャプション)をエスケープせずにそのまま Markdown に出力していた問題を修正し、Markdown 用に適切にエスケープするようにした PR です。これにより、ユーザー由来のキャプションから不正なリンクなどが Markdown として機能してしまうクラスの問題を防ぎます。
- 変更内容の詳細
問題となっていた挙動
元の実装:
def to_markdown(attachment_links: false)
if respond_to?(:attachable_markdown_representation)
attachable_markdown_representation(caption, attachment_links: attachment_links)
else
caption.to_s
end
endattachable_markdown_representation を実装していないアプリケーション独自の attachable では、caption がそのまま返されていました。
その結果、例えば以下のようなキャプションをユーザーが入力した場合:
[click](javascript:alert(1))Action Text から Markdown に変換した際に、この文字列がそのまま Markdown として解釈され、「javascript:」スキーム付きリンクが動作してしまう状況になっていました。
この caption は以下の経路で未エスケープのまま外に出ていました:
Content#to_markdownrender markdown: some_contentAttachment#to_markdownを直接呼び出し
いずれの経路でも MarkdownConversion.escape_markdown_text や MarkdownConversion.markdown_link の URI スキームチェックを通らずに出力されていました。
※なお、to_markdown 自体はまだリリースされていない API であるため、リリース済みの Rails には影響しません。
修正内容
修正後は、attachable_markdown_representation を実装していない場合のフォールバックパスで caption を Markdown 向けにエスケープします。
イメージとしては以下のような変更です(実際のコードから簡略化):
def to_markdown(attachment_links: false)
if respond_to?(:attachable_markdown_representation)
attachable_markdown_representation(caption, attachment_links: attachment_links)
else
::ActionText::MarkdownConversion.escape_markdown_text(caption.to_s)
end
endこれにより、先ほどの例:
[click](javascript:alert(1))は Markdown 出力としては:
\[click\](javascript:alert(1))となり、[click] 部分がただのテキスト扱いになり、Markdown のリンクとして動作しなくなります(javascript: のスキームチェックまでは行っていないが、そもそもリンク構文として成立しない)。
なぜ Attachment#to_markdown 側でエスケープするのか
Markdown 変換のパスは複数あります:
Content#to_markdownrender markdown: attachmentAttachment#to_markdown直接呼び出し
MarkdownConversion.render_attachment だけにエスケープ処理を入れると、render markdown: attachment や Attachment#to_markdown 直接呼び出しのような経路でキャプションが未エスケープのまま抜けてしまう可能性があります。そのため、エスケープは Attachment#to_markdown で行うのが安全という判断です。
attachable_markdown_representation の契約の明文化
この PR では挙動の修正に加え、attachable_markdown_representation に関するドキュメントも整理しています。
ポイント:
attachable_markdown_representation(caption, attachment_links: ...)に渡されるcaptionは「信頼できない(ユーザー由来の)文字列」である。attachable_markdown_representationの戻り値は「すでに Markdown としてレンダリング済みの文字列」とみなされ、そのまま出力される。- したがって、このメソッドをアプリ側で実装する場合は、必要に応じて
ActionText::MarkdownConversion.escape_markdown_textActionText::MarkdownConversion.markdown_linkなどを自前で呼び出し、エスケープやスキームチェックを行う責務がある。
また、RDoc の例で MarkdownConversion を無修飾で参照していたため、トップレベルのモデルから使うと NameError になる問題がありましたが、ActionText::MarkdownConversion のように名前空間付きで参照するように修正されています。
テスト
actiontext/test/unit/markdown_conversion_test.rb にテストが追加され、以下をカバーしています:
Content#to_markdown経由での変換render markdown: ...経由での変換Attachment#to_markdown直接呼び出し
いずれのパスでもキャプションが Markdown 用にエスケープされることが確認されています。
- 影響範囲・注意点
対象:
ActionText::Attachment#to_markdownを利用するコード(特に、アプリケーション独自の attachable クラスでActionText::Attachableを include しているが、attachable_markdown_representationを実装していないもの)。- ただし
to_markdownはまだ未リリース API のため、現時点の安定版 Rails を使っているアプリには直接の影響はありません。
Rails 標準の attachable への影響:
ActiveStorage::BlobRemoteImageContentAttachmentMissingAttachable
などはすでにattachable_markdown_representationを実装済みなので、この変更の影響は受けません。
カスタム attachable 実装時の注意:
- 自分で
attachable_markdown_representationを実装している場合:- 渡される
captionはユーザー入力由来であることを前提に、必要に応じて自前でエスケープすべきです。 ActionText::MarkdownConversion.escape_markdown_text(caption)を利用して Markdown メタ文字をエスケープしたり、ActionText::MarkdownConversion.markdown_link(text, url)を利用して URL スキームチェックを行うと安全です。
- 渡される
- 実装していない場合:
- フォールバック経路で自動的に
escape_markdown_textが呼ばれるので、今後はキャプションが Markdown として解釈されにくくなります。 - もし「キャプションを Markdown として解釈させたい」という仕様がある場合は、逆に
attachable_markdown_representationを実装し、そこに Markdown パーサを通す/エスケープポリシーを決めるなどの対応が必要です。
- フォールバック経路で自動的に
- 自分で
セキュリティ観点:
- まだ unreleased 機能の段階で、ユーザー由来テキストが未エスケープで Markdown 出力に到達していた問題を解消した、という位置づけです。
- 類似の問題として、PR 中で
2191ca0dのコミットが参照されており、「コンテンツ由来テキストがエスケープ処理を経由せずに Markdown 出力に達する」クラスの問題の一つとして整理されています。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58553
- 関連コミット(同種の問題修正):
2191ca0d(本文中で参照されている) - 関連クラス / モジュール:
ActionText::AttachmentActionText::MarkdownConversionActionText::AttachableActionText::Content#to_markdown
#58049 Drive result delivery through QueryIntent
マージ日: 2026/8/24 | 作成者: @matthewd
- 概要 (1–2文で)
ActiveRecord のクエリ実行フローにおいて、「結果が勝手に返ってくる」受動的なモデルから、「結果を明示的に受け取りに行く」能動的なモデルに変更し、その中核としてQueryIntentを拡張・活用する PR です。現時点で機能的な挙動は変えずに、今後の非同期実行(query pipelining など)を実装するための土台を整えています。
- 変更内容の詳細
※実際のコード全文にはアクセスできないため、差分ファイル構成と Rails の既存構造からの推定を含みます。概念的な理解と API レベルの把握を目的にしています。
2-1. 全体像:Result delivery を「能動的な処理」に
これまでの ActiveRecord のクエリ実行は、
- アダプタが SQL を実行する
- DB クライアントが結果を返す
- そのまま呼び出し元に戻る
という「実行=結果取得」が一体になった、同期・直列的なモデルでした。
この PR では、
- 「クエリの意図 (
QueryIntent) を表すオブジェクト」 - 「結果を どう・いつ 受け取るかというロジック」
を分離し、結果の受け取りを明示的なステップとして扱えるようにしたのがポイントです。
これにより、
- 「クエリ送信」と「結果取得」を別タイミングにする
- 並列に複数クエリを飛ばし、あとから結果を回収する(pipelining)
といった非同期モデルを構築しやすくなります。
2-2. QueryIntent の強化
activerecord/lib/active_record/connection_adapters/query_intent.rb (+151/-8) の大幅な増加から、QueryIntent がこの PR の中心です。
想定される役割:
- どのようなクエリか(select / insert / exec など)
- そのクエリの結果を「どう扱うか」
- すぐに全部読む
- ストリーミングする
- 非同期に後から回収する
- 実行時のコンテキスト・メタデータ
- タイムアウト、リトライポリシー
- 計測 (instrumentation) 情報(通知用 payload) など
今回のコメント文:
This turns result delivery into more of an active behaviour, rather than a passive outcome of execution.
から、QueryIntent には
- 「結果を deliver する責任」を持つメソッド(例:
deliver,fulfill,receive_resultsなど) - 基底アダプタ / DBごとのアダプタから呼ばれる、「結果をセットする/完了させる」ためのフック
が追加されていると考えられます。
テスト (query_intent_test.rb +306) が大きく増えているため、QueryIntent の新しい API や状態遷移(requested / executing / delivered / failed 等)についてかなり厳密に仕様が定義されているはずです。
2-3. Adapter 層のインターフェース変更
abstract_adapter.rb (+113/-114)
abstract_adapter は全ての DB アダプタの基底クラスです。ここで、
- 従来:
select,exec_query,executeなどが「SQL実行+結果返却」を一括で行う - 変更後: 上記メソッドが内部で
QueryIntentを生成 or 受け取り、それに「結果を届ける」形に
というスタイルに整理されている可能性が高いです。
イメージとしては:
def exec_query(sql, name = "SQL", binds = [], prepare: false)
intent = build_query_intent(sql: sql, name: name, binds: binds, prepare: prepare)
run_query(intent)
intent.deliver_result # または類似のメソッド
endのように、「実行」と「結果 delivery」が、内部では別責務に分かれている形になっていると考えられます(外から見ると従来通り exec_query が結果を返すため、API の互換性は維持)。
database_statements.rb (+112/-24)
DatabaseStatements は select_all, exec_delete, exec_insert などの「論理的な DB 操作」をまとめたモジュールです。
- ここも
QueryIntentを前提とした呼び出しに書き換えられ、 - 「どのメソッドがどういう intent を表すか」が一層明示的になっていると考えられます。
exec_query→ read intentexec_insert→ write intent(かつ generated key を期待)exec_delete/exec_update→ 書き込み intent(影響行数を期待)
各 DB アダプタの追従
abstract_mysql_adapter.rb(+4/-6)postgresql/database_statements.rb(+2/-8)
などで「インターフェースの変更に合わせた薄い修正」が入っており、DBごとの挙動自体は変えずに、戻り値の扱いやメソッド呼び出しの形を QueryIntent ベースに揃えていると思われます。
2-4. 非同期クエリ周りのテスト拡充
activerecord/test/cases/asynchronous_queries_test.rb (+122/-0)activerecord/test/cases/relation/load_async_test.rb (+11/-11)
非同期ロード (load_async) および asynchronous_queries 関連のテストが増えていることから、
- 既存の「なんちゃって非同期」(実際にはバックグラウンドスレッド+同期API)を
- 今後
QueryIntentを基盤にした本格的な非同期モデルに差し替える準備
が進められていると考えられます。
この PR 自体は「挙動は変えていない」と明言されていますが、
- 非同期実行中に例外が発生した場合の扱い
- タイムアウトやキャンセル時の挙動
- 同一接続で複数 intent を扱うケース
などがテストに追加されており、今後の分割 PR で「本当に async / pipelining にする」際の安全網になっているはずです。
2-5. RetryBudget の導入
activerecord/lib/active_record/connection_adapters/retry_budget.rb (+51/-0)connection_adapters/retry_budget_test.rb (+42/-0)
RetryBudget は、クエリや接続に対する「リトライ回数・頻度の制御」を行うためのクラスと思われます。
典型的な機能:
- 短時間に何度もリトライしすぎないようにする(スロットル)
- 「一定割合まではリトライを許可、それを超えたら即エラー」などのポリシー
- 接続プールやクエリ実行ループから利用される
connection_pool_test.rb (+30) も増えているので、
- 接続プール単位 or 接続単位で
RetryBudgetを持ち、 - 一時的な接続エラー・トランジェントな DB 障害に対し、「過剰リトライを防ぎつつ、ある程度は自動回復を試みる」
基盤が追加されたと考えられます。
これもまた、非同期化や pipelining と相性が良い仕組みで、パイプライン内で発生したエラーをどうリトライするか、どこまで再送を許可するか、といったポリシーに流用できます。
2-6. テストおよび周辺: instrumentation, statement_invalid など
instrumentation_test.rb(+13)QueryIntentベースになっても、ActiveSupport::Notifications 周りのイベント(sql.active_recordなど)が従来通り or それ以上に発火することを確認- intent の情報(intent type、async / sync など)が payload に入るようになっている可能性もあります
statement_invalid_test.rb(+24/-10)- SQL エラー時に
ActiveRecord::StatementInvalid等が正しく発生し、 - 非同期・intent ベースになっても例外パスが壊れていないかを検証
- SQL エラー時に
- 影響範囲・注意点
3-1. 公開 API への影響
- PR 説明にある通り、「機能的な違いはない」=現時点では公開 API の挙動変更はない ことが意図されています。
Model.where(...).to_aconnection.select_all,exec_query,executeload_asyncなども
いずれも呼び出し側からは従来通り動作する想定です。
3-2. とはいえ注意したいポイント(内部 API 依存時)
以下のような「内部挙動に依存したコード」がある場合は注意が必要です:
ActiveRecord::ConnectionAdapters::AbstractAdapterの protected / private メソッドを直接呼んでいるexec_queryなどを monkey-patch して、返ってくる「生のアダプタ結果オブジェクト」のクラスやフィールドに依存している- 特定 DB アダプタ(
PostgreSQLAdapterやAbstractMysqlAdapter)の内部で、- 「どのタイミングで結果が fetch されるか」
- 「どのスレッドで実行されるか」 に踏み込んだ拡張をしている
この PR によって、内部では「クエリの実行」と「結果の受け取り」が別物として扱われるようになっているため、
そういったコードは将来の非同期対応 PR で壊れるリスクが高まります。
現時点では:
- 挙動に違いが出ないよう慎重に実装されているはず
- しかし、Rails のマイナー or メジャーバージョンアップの際に「deprecated なしで内部挙動が変わる」典型的ポイント
なので、「アダプタ内部に依存したヘビーなメタプログラミング」をしているライブラリ作者は、QueryIntent の API を追いかけておく価値があります。
3-3. 非同期・パイプライン化への準備という観点での影響
- 将来的に、1 つの DB コネクションで
- 複数のクエリを飛ばし、
- 結果を非同期に or 任意の順序で受け取る といった「pipelining」がサポートされる布石です。
- これにより、ネットワーク往復のオーバーヘッド削減や、I/O 待ちの隠蔽(DB が処理している間に別の処理を進める)が可能になります。
- 一方で、
- アプリケーション側で「DB 接続は同期的なもの」という前提に立ったワークアラウンド
- スレッドや Fiber と絡めた独自実装の async クエリ などは、公式側の非同期モデルと衝突する可能性があります。
今はまだ「準備段階」ですが、次以降の PR でQueryIntent に「本物の async 用 API」が追加されていくと予想されます。
- 参考情報 (あれば)
- PR タイトル: Drive result delivery through QueryIntent (#58049, matthewd, 2026-08-24 merged)
- キーワード:
QueryIntent(クエリの意図と結果 delivery を担うオブジェクト)- 非同期実行 / query pipelining
RetryBudget(リトライ回数・頻度の制御)- ActiveRecord connection adapters / DatabaseStatements の内部 API 再構成
- 関心がある場合に追うと良いもの:
- 今後の「asynchronous queries」「pipelining」「connection adapter / query execution refactor」関連 PR
activerecord/test/cases/query_intent_test.rbの増加分(QueryIntentの状態遷移・契約仕様)RetryBudgetの仕様(例: どのように「予算」を消費・補充するか)
この PR 自体は安全な内部リファクタリング色が強いですが、「ActiveRecord のクエリ実行モデルが徐々に非同期フレンドリーな設計に移行し始めている」というシグナルとして捉えると良いです。
#58479 Query predicate expressions for Active Record types, take 2
マージ日: 2026/8/24 | 作成者: @matthewd
- 概要 (1-2文で)
このPRは、Active Record の各 Type が「比較に使う Arel 式(predicate expression)」を定義できる仕組みを導入し、where / order / association などでの値変換・比較ロジックを型に委譲するものです。これにより、Active Model に AR/Arel 依存を持ち込まずに、型ごとに正しい SQL 比較表現を柔軟に定義・拡張できるようになっています。
- 変更内容の詳細
全体像
- 「値を DB に保存するときの cast」だけでなく、「値を比較(where, order, join 条件など)に使うときの変換・式生成」を各 Type に担わせる仕組みが追加されています。
- PredicateBuilder の中にベタ書きされていた Range などの個別ハンドリングを減らし、型側が「この値を比較に使うときはこういう Arel 式で」と教える設計に寄せています。
- #58135 の再投稿版で、ActiveModel 側の変更を削り、ActiveRecord だけに閉じる形に整理されています。
追加された主なクラス・モジュール
ActiveRecord::Type::QueryPredicates
- 新規ファイル:
activerecord/lib/active_record/type/query_predicates.rb - Active Record の Type に「クエリ用の述語(predicate)表現」を提供するモジュール。
- 代表的な責務:
- 「等価比較」「範囲比較」「配列 IN 句」などで使うための Arel 式を返す。
- 内部的には
PredicateBuilder::Comparisonを使って、型がarel_predicate_for(value)的なインターフェイスを通じて比較式を構築できるようにするイメージ。
(実際のメソッド名は PR 本文からは完全には分かりませんが、ファイル構成・行数から「型が比較のためのクエリ述語を出力できる API」が入っていると見られます)
ActiveRecord::Relation::PredicateBuilder::Comparison
- 新規ファイル:
activerecord/lib/active_record/relation/predicate_builder/comparison.rb - 個々の比較(カラムと値の比較)を表すオブジェクト。
- 役割:
- 対象カラム(= Attribute / Arel::Attribute)と値、型を持ち、
- 型に応じて適切な比較式(= Arel ノード)を生成する。
- これにより、PredicateBuilder 本体は「Comparison を生成して型に任せる」構造になり、個別の case 分岐が減っています。
PredicateBuilder 周りの整理
activerecord/lib/active_record/relation/predicate_builder.rb
- 変更: +24/-40 と大きく減少。
- 主な変更点:
- Range, Array, BasicObject などの個別ハンドラーを呼び分けるロジックを整理し、多くを
Comparison経由+Type 依存に寄せた。 - ベースとなる Range 特有の handling(例:
BETWEEN, open-ended range など)を中核クラスから取り除き、型側や専用 handler に任せる設計に変更。
- Range, Array, BasicObject などの個別ハンドラーを呼び分けるロジックを整理し、多くを
- 結果:
- PredicateBuilder 自体の責務が「値の種類ごとに handler/Type に委譲する」ことに近づき、型を拡張することで比較ロジックを差し替えやすくなっています。
個別 handler の調整
array_handler.rb,range_handler.rb,basic_object_handler.rb,relation_handler.rbなどが微修正。- これらは引き続き「特定の値種別 (Array, Range, Relation 等)」を扱うが、型の predicate 機能を利用する方向に寄せられていると考えられます。
- 例: Range の終端開放 / 閉区間の扱い、null を含む配列の扱いなどを、Type 側のロジックと連携して行う。
Type / Attributes 周り
activemodel/lib/active_model/type/value.rb
- -41 行: ActiveModel::Type 側に入りかけていた(または前 PR で追加していた)predicate 関連のロジックが削除。
- 目的:
- 「ActiveModel は DB/SQL を知らないレイヤー」の方針を維持するために、AR/Arel 依存の機能は ActiveRecord::Type 側にのみ置く方針に戻した。
activerecord/lib/active_record/type.rb
- +1 行のみですが、
Type::QueryPredicatesを include する or 関連づけるためのフックが追加されている可能性が高いです。 - 各 Type(例えば
ActiveRecord::Type::DateTime,Enum,Serializedなど)が QueryPredicates を継承/利用し、where/order などの式をカスタマイズできるようになる。
activerecord/lib/active_record/attributes.rb
- +4 行: 属性の型情報を使って predicate expression を引き出すためのコードが追加されている模様。
- 例えば:
where(foo: value)で foo の型を調べ、その型の query_predicate に処理を委譲。
order / association / その他の文脈への適用
PR 説明で「applies transformations in more contexts (ordering, associations, for example)」とある通り、比較式への変換が以下にも拡張されています。
activerecord/lib/active_record/relation/query_methods.rb
- +23/-2 と大きく増加。
- where だけでなく、order や group などで使う値にも Type ベースの変換・述語生成が適用される。
- 例:
- 時刻型のカラムに対する
order(created_at: :asc)が、タイムゾーン変換後の適切な式で評価される。 - Enum, Serialized などの「DB 上の表現と Ruby 表現が異なる型」でも、order / group 時に一貫した変換が効く。
- 時刻型のカラムに対する
activerecord/lib/active_record/associations/association_scope.rb
- +12/-6。
- association の join 条件や scope 条件の生成でも、Type の predicate 表現を通すよう調整。
- 結果:
- has_many / has_one / belongs_to などの関連先に対して where を書いた際も、正しい型変換・比較ロジックが適用される。
activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
- +6 行。
- タイムゾーン変換付き属性 (e.g. datetime with time zone) について、クエリ時の比較でも TimeZone 変換を正しく扱うためのフックが追加されていると考えられます。
- 単なる
castだけでなく、「比較するときにどの時間帯で解釈すべきか」を型が制御できるようにする文脈。
activerecord/lib/active_record/enum.rb
- +2 行。
- enum 型のクエリ predicate サポート: where / order / association で enum シンボルを渡したときに、DB 上の整数・文字列値に適切に変換して比較するロジックが型経由に再整理されている可能性が高いです。
activerecord/lib/active_record/locking/optimistic.rb
- +6 行。
- 楽観的ロック用の
lock_versionの比較などにも、Type ベースの predicate 表現が使われるようになった様子。 - カスタム型の lock_version を持つ場合にも正しく比較されるようにするため。
activerecord/lib/active_record/reflection.rb
- +4/-1。
- association reflection が、関連属性の型情報を使ってクエリ predicate を組み立てられるようなフックの追加と思われます。
変更の背景: Issue 対応
- 説明文にもあるように、この PR は以下を解決するものとされています:
- #58407: 型変換・比較ロジックがある文脈で効いていなかった問題(詳細は Issue 本文に依存)。
- #58410: Range やその他比較の扱いが過剰 / 不正確だった問題を、より「型が責任を持つ」形で正しく解決。
- また、前 PR (#58135) では ActiveModel 側に predicate ロジックを入れてしまい、レイヤ分離的に問題があったため、その設計を改めた「take 2」として出し直されています。
サンプルイメージ(疑似コード)
あくまで PR から推測される構造の例です(実際の API 名とは異なる可能性があります)が、概念的には以下のような拡張が可能になります。
# 独自型
class Types::JsonbContains < ActiveRecord::Type::Value
include ActiveRecord::Type::QueryPredicates
def cast(value)
# Ruby の Hash にキャスト
end
# クエリ用述語をカスタマイズ
def arel_predicate(attribute, value)
# JSONB の演算子を使う
Arel::Nodes::InfixOperation.new(
"@>",
attribute,
Arel::Nodes.build_quoted(value.to_json)
)
end
end
# モデルに適用
class Article < ApplicationRecord
attribute :metadata, Types::JsonbContains.new
end
# これが
Article.where(metadata: { published: true })
# 型の arel_predicate を通じて
# WHERE "articles"."metadata" @> '{"published": true}'
# のような式に展開される、というイメージまた、time zone 付き datetime 型や enum 型でも、where / order / association などクエリ全般で一貫した変換・比較が行われるようになります。
- 影響範囲・注意点
- 影響範囲:
- ActiveRecord のクエリ生成全般(where, not, order, group, having, association scope, locking など)。
- 特に Array / Range / Relation を where に渡したときの挙動、および enum・time zone 変換を伴う型でのクエリ。
- 互換性:
- 基本的には内部実装の分離・整理を目的にしており、従来の使用法はそのまま動く想定。
- ただし、PredicateBuilder が依存していた非公開 API や、内部実装に依存するモンキーパッチをしていたアプリ/ライブラリは影響を受ける可能性があります。
- カスタム Type を実装している場合:
- これまで
type.cast(value)だけを意識していたものが、「クエリ比較の際にどのような式になるか」も型の責務としてカスタマイズ可能になります。 - 将来的に
ActiveRecord::Type::QueryPredicatesを明示的に include して、where/order での比較方法をオーバーライドするパターンが一般的になる可能性があります。
- これまで
- デバッグ観点:
- where/order などで生成される SQL が、以前と微妙に変わるケースがあります(特に Enum, TimeZone, Range、Relation ベースのサブクエリ IN 句など)。
- 本番導入前に、クエリロギングと一緒に regression テストを確認するのが推奨されます。
- 参考情報 (あれば)
- 対応 PR / Issue:
- 元 PR: #58135 (Replaces #58135 と明記)
- 関連 Issue: #58407, #58410
- 関連実装ファイル:
activerecord/lib/active_record/type/query_predicates.rbactiverecord/lib/active_record/relation/predicate_builder/comparison.rbactiverecord/lib/active_record/relation/predicate_builder/*.rbactiverecord/lib/active_record/relation/query_methods.rbactiverecord/lib/active_record/attribute_methods/time_zone_conversion.rbactiverecord/lib/active_record/enum.rb
この PR により、「型がクエリ比較の実装を知っている」設計が一段と強化されるため、今後の複雑な型サポート(JSONB, Range, IP, カスタム enum など)や、DB ごとの違いを吸収する上での基盤として重要な変更と言えます。
#58543 Strip user-supplied <action-text-markdown> while canonicalizing
マージ日: 2026/8/24 | 作成者: @flavorjones
- 概要 (1-2文で)
Action Text のContent#to_markdownで使用している内部用タグ<action-text-markdown>について、ユーザーが本文中に同名タグを書いた場合でもそれが「信頼済みの生 Markdown」として扱われないようにする修正です。未リリース機能 (to_markdown) に対する修正であり、リリース済みの Rails には影響しません。
- 変更内容の詳細
問題の背景
ActionText::Content#to_markdownは、添付ファイル(attachments)を Markdown に変換する際、
その Markdown を<action-text-markdown>要素でラップします。MarkdownConversion側では、この要素でラップされた部分を「エスケープせずにそのまま Markdown ソースとして出力する」ことで、
添付ファイル用に生成した Markdown が壊れないようにしていました。- しかし、この
<action-text-markdown>は「Action Text が内部的に付ける印」としてしか想定していないにも関わらず、
ユーザーが本文中に自分で<action-text-markdown> ... </action-text-markdown>と書いても区別がつかず、
その中身が「信頼済みの生 Markdown」として解釈されてしまっていました。
具体的には、次のようなコードが問題になります。
ActionText::Content.new(
"<action-text-markdown>[click](javascript:alert(1))</action-text-markdown>"
).to_markdown
# 修正前 => "[click](javascript:alert(1))"
# 修正後 => "\\[click\\](javascript:alert(1))"- 修正前:
<action-text-markdown>が「内部マーカー」だと誤認され、その中身がそのまま Markdown として出力される。- その結果、
javascript:スキーム付きリンクが URI スキーム検査 (MarkdownConversion#markdown_link) を素通りし得る。
- 修正後:
- ユーザー入力の
<action-text-markdown>は「ただの HTML タグ」として扱われ、Markdown に変換する際にはエスケープされる。 - 結果として、
[click]が\[でエスケープされ、Markdown のリンク構文として解釈されない。
- ユーザー入力の
報告は HackerOne (https://hackerone.com/reports/3727743) ですが、to_markdown 自体がまだリリースされていないため、
セキュリティリリースとしては扱っていません。
コミット構成
doc: use Markdown markup in Action Text Markdown docscontent.rb/markdown_conversion.rbの RDoc コメントが# :markup: markdownに対して適切でなかったものを修正。+text+や<tt>といった RDoc 固有のマークアップを Markdown 形式に書き直し。- コード挙動には一切変更なし。
Move Markdown attachment rendering into MarkdownConversion- これまで
Content#to_markdownの中でインラインに書かれていた「添付ファイルを<action-text-markdown>で包む」処理をMarkdownConversion.render_attachmentに移動。 <action-text-markdown>というタグ名をRAW_MARKDOWN_TAG_NAMEとして共通の定数にまとめ、
複数箇所で文字列リテラルを直接書いていたのを統一。- 挙動としては変更なし(リファクタリング)。
- これまで
Strip user-supplied <action-text-markdown> while canonicalizing— 本体の修正
どこでフィルタするか
- 修正は
Content.fragment_by_canonicalizing_contentに入っています。- このメソッドは、フレームワーク外部から入ってくるマークアップを正規化(canonicalize)するための共通の入口で、
- ユーザーが新たに入力したリッチテキスト
- DB から読み出した既存のリッチテキスト を含め、
Contentの生成時に必ず通る場所です。
- このメソッドは、フレームワーク外部から入ってくるマークアップを正規化(canonicalize)するための共通の入口で、
- 一方で、Action Text が添付ファイル用に
<action-text-markdown>マーカーを付与するのはrender_attachmentsであり、
これはcanonicalize: falseで呼ばれるようになっています。- つまり、
- 外部から来た HTML を
fragment_by_canonicalizing_contentで正規化 & 「ユーザー入力の<action-text-markdown>は剥がす」 - その後に内部処理 (
render_attachments) で「信頼済みマーカーとしての<action-text-markdown>」を付与
という順序がフレームワーク内部で構造的に保証されるように組まれています。
- 外部から来た HTML を
- これにより、「どこかで
canonicalize: true/falseを付け忘れたために再発する」という種のバグを避けています。
- つまり、
具体的な挙動変更
- 正規化処理の中で、
- ユーザー入力の
<action-text-markdown>要素を見つけたら剥がす(unwrap する)。 - 中身のテキスト/ノードは残るが、「内部マーカー」としては扱われない。
- ユーザー入力の
- 結果として、その中のコンテンツは通常の HTML として扱われ、
to_markdown時には通常のエスケープ/サニタイズが行われます。
他の関連クラスについて
Fragment#to_markdownとMarkdownConversion#node_to_markdownは、- 正規化も行わない
- 添付ファイルのレンダリングも行わない
- 「信頼できないコンテンツには使うな」とドキュメントに明示されており、安全なパス (
Content#to_markdown) を参照するようになっています。
- この PR ではこの方針を維持しつつ、「安全なパス」の内部でユーザー入力マーカーを確実に除去するようにしました。
テスト
actiontext/test/integration/markdown_sanitization_test.rb- 実際にリッチテキストを通した end-to-end なシナリオで、
ユーザー由来の<action-text-markdown>が安全に扱われることを検証。
- 実際にリッチテキストを通した end-to-end なシナリオで、
actiontext/test/unit/markdown_conversion_test.rbMarkdownConversionレベルでの Markdown 変換挙動(特に今回のタグに関わる部分)をユニットテスト。
- 正規化と添付レンダリングの順序が正しく保たれていることを「名前付きのテスト」で明示的に検証しており、
たまたま他のテストが落ちることに依存しない作りになっています。
- 影響範囲・注意点
影響範囲
- リリース済み Rails への直接影響はなし
- 問題の対象である
ActionText::Content#to_markdownは、まだ公式リリースに含まれていません。
- 問題の対象である
- 将来
to_markdownがリリースされたときの前提:<action-text-markdown>は Action Text 内部専用の予約タグ となります。- ユーザーが同名タグを本文に書いても、その中は「生 Markdown」としては扱われず、通常どおりエスケープ/サニタイズされます。
パフォーマンスへの影響
Content.fragment_by_canonicalizing_contentに、
「ユーザー由来<action-text-markdown>を剥がす」ための 3つ目のパス が追加されました。- ベンチマーク(20 段落程度の本文・マーカーなし・1000回 x 15セットの best-of):
| run 1 | run 2 | run 3 | |
|---|---|---|---|
| before | 1758 i/s | 1702 i/s | 1687 i/s |
| after | 1393 i/s | 1372 i/s | 1367 i/s |
| 差分 | -20.8% | -19.4% | -19.0% |
- 相対的には約 20% 程度遅くなっていますが、絶対時間としては 1ms 未満 に収まっています。
Fragment#updateで「ヒットがない場合は dup しない」などの最適化で数ポイント取り戻せるものの、API がやや複雑になるため今回は見送り。
開発者が注意すべきポイント
<action-text-markdown>はドキュメント上も Action Text 専用の予約タグ として扱われるようになります。- カスタム HTML やカスタムトランスフォーマでこのタグ名を流用しないことが推奨されます。
- ユーザーからの生 HTML を直接
Fragment#to_markdownやMarkdownConversion#node_to_markdownに渡すのは
今後も「安全ではない」パターンのままです。- ユーザー入力を Markdown に変換したい場合は、必ず
ActionText::Content.new(user_html).to_markdownのようにContent経由で正規化・サニタイズを通してください。
- ユーザー入力を Markdown に変換したい場合は、必ず
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58543
- HackerOne 報告: https://hackerone.com/reports/3727743
- 変更ファイルの主なポイント:
actiontext/lib/action_text/content.rbfragment_by_canonicalizing_contentにユーザー由来<action-text-markdown>剥離ロジックを追加。
actiontext/lib/action_text/markdown_conversion.rbRAW_MARKDOWN_TAG_NAMEを導入。- 添付ファイルの Markdown 生成を
render_attachmentに集約。
actiontext/CHANGELOG.md<action-text-markdown>が Action Text 自身のために予約されている旨を追記。
#58504 Allow translate's scope: argument to resolve relative i18n keys with leading period
マージ日: 2026/8/24 | 作成者: @bensheldon
- 概要 (1-2文で)
I18n.translate/tヘルパーのscope:引数でも、先頭にドットが付いた相対キー(.foo)を「現在の翻訳コンテキストに対する相対パス」として解決できるようになりました。これにより、key引数とscope:引数の相対キー挙動が揃い、テンプレート内の翻訳記述が一貫し書きやすくなります。
- 変更内容の詳細
背景・モチベーション
Rails では既に、t(".foo.bar") のように先頭に . を付けたキーを「現在のビュー/コントローラの翻訳スコープ」に対する相対キーとして解釈しています(例: posts#index なら posts.index.foo.bar へ展開)。
しかし scope: オプションに対しては、.foo のような相対指定をしても「相対スコープ」として扱われず、単に foo として扱われていました。
この PR は、scope: に対しても相対キー展開を有効にし、「key と同じ感覚で書けるようにしたい」という意図の変更です。
具体的な挙動の変更
これまでの挙動
# key 引数側の相対キーは動く
t(".foo.bar")
# => posts.index.foo.bar
# scope 側の先頭ドットは「無視」される
t("bar", scope: ".foo")
# => foo.bar # 現在のテンプレートに対する相対展開はされないこの PR 適用後の挙動
# key 側は従来通り
t(".foo.bar")
# => posts.index.foo.bar
# scope 側でも相対キーとして解決されるようになる
t("bar", scope: ".foo")
# => posts.index.foo.barつまり、「scope: に先頭ドットを付けたら、key の相対キーと同様に現在の翻訳コンテキストから解決する」ようになりました。
実用的な使い方の改善例
配列を回しながら翻訳キーを組み立てる場面で記述が簡潔になります。
従来は、相対スコープを使いたい場合、key 側を文字列連結で組み立てる必要がありました:
["foo", "bar"].each do |val|
t(".baz.#{val}")
endこの PR 以降は、scope: 側に相対スコープを書けるので、より見通しのよい記述になります:
["foo", "bar"].each do |val|
t(val, scope: ".baz")
end優先順位・互換性に関する仕様
- 歴史的に、
scope:の先頭ドットは「無視される(noop)」挙動だったため、既存コードへの影響を抑えるための配慮が入っています。 - 作者は「
keyとscopeの両方が相対キーで書かれている場合は、key側の解釈を優先する」というルールを入れています。
これにより、t(".bar", scope: ".foo")のような書き方で、従来のkeyの相対解決ロジックを崩さないようにしています。 scope:に配列を渡した場合は、相対展開は行わず、従来どおりの挙動を維持します。
例:
# scope: に配列を渡す場合は相対解決しない(今まで通り)
t("bar", scope: [".foo", "baz"])
# => ".foo.baz.bar" をそのまま解釈する既存ロジックを維持(相対展開なし)(正確な解決結果はコンテキストに依存しますが、「先頭ドットによる相対コンテキスト解決」は働かない、というのがポイントです。)
実装箇所(概要)
abstract_controller/translation.rb
コントローラ側での翻訳ヘルパーが使うtranslateロジックに、scope:の先頭ドットを見て相対展開する処理を追加。action_view/helpers/translation_helper.rb
ビューのt/translateヘルパーに同様のロジックを追加。テンプレートのパスに応じて相対スコープを解決。- テスト (
translation_test.rb,translation_helper_test.rbおよび対応する ERB フィクスチャ)scope:に相対キーを指定したときに、期待どおりに「現在のテンプレート/アクションのスコープ」に解決されることを検証。keyとscope両方が相対指定されたときにkey側優先になることを検証。scope:に配列を渡したときに相対展開されないことを検証。
actionpack/CHANGELOG.md,actionview/CHANGELOG.md
仕様変更として「scope:での相対キー解決サポート」が追記されています。
- 影響範囲・注意点
主な影響範囲
- コントローラ内での
translate/t呼び出し - ビュー・テンプレート内での
t/translate呼び出し - 特に「
scope:オプションに先頭ドット付き文字列を渡しているコード」が挙動変更の対象になり得ます。
- コントローラ内での
後方互換性面の注意
- 以前は
scope: ".foo"が「実質scope: "foo"と同じ」だったのに対し、今後は「現在のコンテキストからの相対スコープ」として扱われます。 - もし既存コードで「
scope:に先頭.を入れているが、相対解決を期待していない」ケースがあると、翻訳キー解決先が変わる可能性があります。 - ただし、
scope:に配列を渡すケースや、key側が相対キーでscope側も相対キーの場合は、互換性に配慮した優先順位(key優先)が働きます。
- 以前は
実務的なチェックポイント
- プロジェクト内で
scope: ".something"のような記述を grep し、この挙動変更が望ましいか確認しておくと安全です。 - 今後は、繰り返し処理などで動的にキーを組み立てる場面では、
scope:で相対スコープを分離して書くスタイルが推奨しやすくなります。
- プロジェクト内で
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58504
- 関連ファイル:
actionpack/lib/abstract_controller/translation.rbactionview/lib/action_view/helpers/translation_helper.rbactionpack/test/abstract/translation_test.rbactionview/test/template/translation_helper_test.rb
- Rails ガイド(参考・一般的な I18n 仕様):
https://guides.rubyonrails.org/i18n.html
#58539 Fix the Active Record Store dirty tracking documentation example
マージ日: 2026/8/24 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Store のドキュメントにある「dirty tracking」の例が、実際の挙動(未保存レコード時)と食い違っていたため、例を「保存済みレコード」を前提とする形に修正した PR です。コード本体の挙動には変更はなく、ドキュメントのみの修正です。
- 変更内容の詳細
何が問題だったか
ActiveRecord::Store のドキュメント中の dirty tracking のサンプルコードは、以下のように User.new(未保存レコード)に対して説明されていました:
u = User.new(color: 'black', homepage: '37signals.com', parent_name: 'Mary', partner_name: 'Lily')
# Dirty tracking
u.color = 'green'
u.color_changed? # => true
u.color_was # => 'black'
u.color_change # => ['black', 'green']しかし、実際の Active Record の挙動では、
- レコードが未保存 (
User.new) の場合、そのストア属性には「以前の値」が存在しないためcolor_wasはnilcolor_changeは[nil, 'green']
になります。
これはすでにテストで明示されています:
test "new record and no accessors changes" do
user = Admin::User.new
...
user.color = "red"
assert_predicate user, :color_changed?
assert_nil user.color_was
assert_equal "red", user.color_change[1]
end一方、保存済みレコード (Admin::User.create!) に対するテストでは、ドキュメントの期待値どおり、「元の値」が存在する前提で検証されています:
test "updating the store populates the accessor changed array correctly" do
@john.color = "red"
assert_equal "black", @john.color_was
assert_equal "black", @john.color_change[0]
assert_equal "red", @john.color_change[1]
end具体的な修正内容
変更ファイル: activerecord/lib/active_record/store.rb
変更行数: 1行追加 / 1行削除(例示コードの一部のみ)
PR 説明から読み取れる修正は:
User.new(...)を使っていたサンプルを、User.create!(...)などの「保存済みレコード」を作るコードに差し替えた
といった内容です。
この結果、ドキュメント上のコメントと実際の動きが一致します:
u = User.create!(color: 'black', homepage: '37signals.com', parent_name: 'Mary', partner_name: 'Lily')
u.color = 'green'
u.color_changed? # => true
u.color_was # => 'black'
u.color_change # => ['black', 'green']PR 内でも、main で確認した結果として以下が示されています:
u = User.new(...) # -> color_was = nil, color_change = [nil, "green"]
u = User.create!(...) # -> color_was = "black", color_change = ["black", "green"]つまり「ドキュメントが示したいケースは、実際には保存済みレコードを前提にした例だった」ので、その前提に合わせるためにサンプルコードだけ修正した、という形です。
- 影響範囲・注意点
- コード本体・挙動の変更は一切なく、「ドキュメントのみ」の変更です。
- 既存アプリケーションの動作は変わりません。
ActiveRecord::Storeを利用して dirty tracking(*_changed?,*_was,*_change)を行う際の実際の挙動は次のようになります:- 未保存レコード (
Model.new) でストア属性に初めて値をセットした場合- 「元の値」は存在しないため
*_wasはnil *_changeは[nil, new_value]
- 「元の値」は存在しないため
- 既に保存されたレコードに対する更新の場合
- 「保存時点の値」が
*_was *_changeは[old_value, new_value]
- 「保存時点の値」が
- 未保存レコード (
- ドキュメントを読んで「new しただけのレコードでも
*_wasに初期値が入る」と誤解していた場合、その理解を修正する必要があります。
- 参考情報 (あれば)
- 対応するテスト:
activerecord/test/cases/store_test.rb- 新規レコード時の dirty tracking テスト
- 保存済みレコード (
create!) 時の dirty tracking テスト
- 関連 API:
ActiveRecord::Store- Active Record の dirty tracking API:
attribute_changed?attribute_wasattribute_change
#58538 Fix the documented return type of AVG calculations
マージ日: 2026/8/24 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Calculationsのドキュメントに書かれていたAVG計算結果の戻り値の型が、実際の挙動と一致するように修正された PR です。コードの挙動自体は変わらず、ドキュメントのみの修正です。
- 変更内容の詳細
もともとの記述(誤り)
ActiveRecord::Calculations のコメント(ドキュメント)では、単一集計結果の型について次のように説明されていました:
The single value is type cast to Integer for COUNT, Float for AVG, and the given column's type for everything else.
ここで「AVG は Float にキャストされる」と書かれていましたが、実際の average メソッドの戻り値はそうなっていません。
実際の挙動
テストコードおよび実測結果(SQLite, PostgreSQL, MySQL)から、average の戻り値は次のようになっています:
| カラム型 | average の戻り値のクラス |
|---|---|
| integer | BigDecimal |
| decimal | BigDecimal |
| float | Float |
テスト抜粋(既存テスト):
def test_should_return_decimal_average_of_integer_field
value = Account.average(:id)
assert_instance_of BigDecimal, value
end
def test_should_return_decimal_average_if_db_returns_such
NumericData.create!([{ bank_balance: 37.50 }, { bank_balance: 37.45 }])
assert_instance_of BigDecimal, NumericData.average(:bank_balance)
end
def test_should_return_float_average_if_db_returns_such
NumericData.create!(temperature: 37.5)
assert_instance_of Float, NumericData.average(:temperature)
endつまり:
- integer カラムの平均 →
BigDecimal - decimal カラムの平均 →
BigDecimal - float カラムの平均 → カラム型に合わせて
Float
となっており、
「AVG は常に Float」という記述は誤りでした。
今回の修正内容
変更ファイルは 1 つだけです:
activerecord/lib/active_record/relation/calculations.rb
ここに書かれているドキュメントコメントを、実挙動に合わせて修正しています。
具体的には「AVG は Float」という説明をやめ、整数/小数カラムでは BigDecimal を返すという形で、より正しい表現にしています(PR本文の説明を踏まえた文言調整)。
コード(ロジック)の変更はなく、average メソッドの実装には一切手を入れていません。
- 影響範囲・注意点
影響範囲
- ランタイム挙動は一切変わりません。
- 既存のアプリケーションコードやテストコードへの影響はありません。
- 影響があるのは、Rails ドキュメントを参考に平均値の戻り型を設計していた場合の「認識」だけです。
注意点
averageの戻り型を Float 前提で扱っていると、「ドキュメントに書いてあったからそうだと思っていた」という思い込みと、実際のクラス (BigDecimal) の不一致が起こり得ます。これまでにも実挙動は
BigDecimalだったので、以下のようなコードを書いていた場合は、昔から壊れていたはずです:rubyavg = Account.average(:id) # ドキュメントだけを信じて Float 前提の API を叩く avg.nan? # BigDecimal にはないメソッド avg.is_a?(Float) # => false今後はドキュメントが正しくなったことで、
- 整数 / decimal カラム →
BigDecimal - float カラム →
Float
を前提にコードを書くことができます。
- 整数 / decimal カラム →
DB ごとの違いについて:
- PR 作成者が SQLite / PostgreSQL / MySQL で確認しており、この 3 つでは同じ挙動であることが確認済みです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58538
- 関連するメソッド:
ActiveRecord::Calculations#average - 型扱いのベストプラクティス:
- 金額などの精度が重要な値は
decimalカラムを使い、BigDecimalとして扱う前提で実装する - 実数計測値(温度など)で誤差を許容する場合は
floatカラムを使い、Floatとして扱う前提で実装する
- 金額などの精度が重要な値は
#58547 url_for: use Hash splat to append keys
マージ日: 2026/8/24 | 作成者: @byroot
- 概要 (1-2文で)
ActionController::UrlFor#url_for内で、既存の Hash をdupしてキーを追加する実装をやめ、Hash のキーワード展開(Hash splat)を使うようにしたパフォーマンス改善のPRです。Ruby 4.1 以降で Hash 再確保(reallocation)を避けられ、URL生成まわりの性能・メモリ効率が向上します。
- 変更内容の詳細
変更の主旨
これまで url_for では、オプション Hash にキーを追加する際に:
options = options.dup
options[:only_path] = trueのように「元の Hash を dup した後に新しいキーを代入」していました。
このパターンは、元の Hash が既に「いっぱい」だった場合、コピー側も同じ容量で確保されるため、新しいキーを追加するときに再度メモリ再確保(realloc)を行う必要があるという問題があります。
そこで、次のような Hash のキーワード展開(splat)構文 に書き換えることで、Ruby が最初から「必要なサイズの Hash」を 1 回で確保できるようにしています(Ruby 4.1 以降で効果がある最適化)。
options = { **options, only_path: true }Ruby 本体側の最適化(ref: ruby/ruby#18344)により、この { **base, new_key: 1 } パターンは、
baseのエントリ数+1 を見越した容量の Hash を一発で確保- そのうえで要素をコピー&新キーを挿入
という形で実装されるため、重い再確保を避けられます。
実際に近いコードイメージ
url_for の内部では、おおよそ次のような変化が入っていると考えられます(実際のコードから簡略化したイメージ):
変更前(イメージ):
def url_for(options = nil)
# ... 略 ...
options = options.dup
options[:script_name] = request.script_name if some_condition
options[:only_path] = true if another_condition
# ...
end変更後(イメージ):
def url_for(options = nil)
# ... 略 ...
options =
if some_condition
{ **options, script_name: request.script_name }
else
options
end
options =
if another_condition
{ **options, only_path: true }
else
options
end
# ...
end実際には 6 行追加・8 行削除なので、dup+[]= の箇所が複数まとめて { **options, key1: ..., key2: ... } のように変わっている可能性もあります。
ポイントは「既存 Hash をその場で破壊的に変更するのではなく、新しい Hash をキーワード展開で組み立て直している」ことです。
- 影響範囲・注意点
影響範囲
- 対象:
ActionController::UrlFor#url_forを通じて呼ばれる URL 生成(url_forヘルパ、*_url/*_pathヘルパなど、内部的にここを経由するもの) - 性能面:
- Ruby 4.1 以降:
url_for呼び出し時の内部 Hash 操作の再確保が減り、メモリアロケーションが削減される。- ruby-bench の lobsters / shipit ベンチでボトルネックと認識されていた箇所の改善。
- それ以前の Ruby:
- 振る舞いはほぼ同等で、パフォーマンス的にも「悪化しない(ニュートラル)」と説明されています。
- Ruby 4.1 以降:
互換性・挙動への影響
- ロジック上は「元のオプション Hash + 追加キー」で構成される Hash を使う点は従来と同じで、ユーザーコードから見える挙動は変わらない想定です。
optionsは新しい Hash に置き換えられるため、url_for内でoptionsがミューテーションされても、呼び出し元が持っている Hash オブジェクト自体は変更されない(もともとdupしていたため、外部の Hash を汚さないという性質も維持されます)。- Ruby のキーワード展開
{ **h }は、hが:nilやfalseの場合: エラーになるHash互換で respond_to?(:to_hash) のオブジェクトの場合:to_hashによる変換が必要 という制約があります。 ただし、url_forの既存コードも元々 Hash 前提で動いているため、型的な前提は従来とほぼ同じです。
注意点 (Rails アプリ開発者視点)
- 自分で
url_for相当を monkey-patch している場合、同様の最適化(dup+[]=→{ **base, key: value })を検討すると Ruby 4.1 以降で恩恵を受けられる可能性があります。 - 大量の URL 生成(API サーバ、クローラ、リンク生成の多いアプリ)では、この種の微小な Hash 最適化が積み重なって GC 負荷低減やスループット改善につながることがあります。
- 参考情報 (あれば)
- Ruby 本体の最適化 PR:
- https://github.com/ruby/ruby/pull/18344
- Hash のキーワード展開(
{ **base, key: value })におけるメモリアロケーション最適化の詳細が議論されています。
- Hash のキーワード展開(
- https://github.com/ruby/ruby/pull/18344
- 対象コードの場所:
actionpack/lib/action_controller/metal/url_for.rb- Rails の URL 生成ロジック(
url_forを含む)実装ファイル。
- Rails の URL 生成ロジック(
#58546 Fix typo in Reflection initializer
マージ日: 2026/8/24 | 作成者: @bquorning
- 概要 (1-2文で)
Reflection 初期化処理内のタイポにより、常に不要なdupが行われていたバグを修正する PR です。
未定義インスタンス変数@extensionを参照していた箇所を、正しく定義済みの@extensionsに修正しています。
- 変更内容の詳細
問題の内容
元のコードでは、Reflection のイニシャライザ(あるいは関連する初期化処理)の中で、以下のような構造になっていたと推測できます(簡略化した疑似コード):
# 実際のコード例(イメージ)
def initialize(...)
@extensions = extensions
# タイポがあった部分
@extensions = @extension.dup unless @extension.frozen?
end- 本来使うべきインスタンス変数は
@extensions(複数形) - しかし実際には
@extension(単数形)と書かれていた @extensionは定義されていないためnil- Ruby では未初期化のインスタンス変数参照は
nilを返すので、@extensionはnilとして扱われる - その結果、
@extension.frozen?はnil.frozen?となり、これは常にtrue
PR 説明にある通り:
@extension(singular) is undefined, andnil.frozen?is always true, causing unnecessarydupcalls.
少し補足すると、本来は「オブジェクトが frozen? でないときだけ dup する」というガードを入れたかったのに、間違って nil.frozen? #=> true を見ていたため、「常に frozen? が true」だと誤判断されていました。
その結果、unless @extension.frozen? が常に偽になり、本来必要に応じて行うはずだった dup の条件分岐が壊れてしまい、意図しない挙動や不要な dup 呼び出しが発生していました(PR 文面のニュアンスから、条件が逆だった/ロジックが崩れていた可能性も高いです)。
修正内容
変更ファイルは 1 行のみ:
- @extension.dup unless @extension.frozen?
+ @extensions.dup unless @extensions.frozen?のような、単数形 → 複数形への単純なタイポ修正です(正確なコードはリポジトリ側に依存しますが、趣旨はこの通りです)。
意図されている正しいロジックは:
- Reflection に渡された
extensionsコレクション(モジュールや Proc などの拡張)を保持する - そのコレクションが
frozen?でないときだけdupして内部に格納し、外部からの変更の影響を避ける
といった標準的な Rails の防御的コピーのパターンです。
- 影響範囲・注意点
- 対象: Active Record の
ActiveRecord::Reflection周辺ロジックにおける「extensions」関連処理- association reflection(
has_many,has_one,belongs_toなど)のメタ情報を扱う部分で利用されるクラスです。
- association reflection(
- 実際の影響:
- タイポにより、意図した条件付き
dupが行われず、extensionsの freeze 状態やオブジェクト共有の前提が崩れていた可能性があります。 - 直前の PR (#58541) で導入された回帰バグのため、影響期間は短いと思われますが、その PR のコミットを拾っている環境では、以下のような不具合が起こり得ます:
- association の extensions を外側から渡している場合、オブジェクトが不必要に共有・複製される/されないなどの不一致
- freeze されたオブジェクトに対する
dupが意図通り制御されない
- タイポにより、意図した条件付き
- 互換性:
- タイポ修正のみであり、設計意図に沿った動作に戻す変更なので、API レベルの互換性への影響はありません。
- #58541 適用後に、この挙動に依存したコードを書いていない限り、通常は「バグ修正による正常化」と見なせます。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58546
- 直前の原因 PR: https://github.com/rails/rails/pull/58541
- 関連クラス:
ActiveRecord::Reflection(activerecord/lib/active_record/reflection.rb) - Ruby の
frozen?とdupの基本挙動:nil.frozen? #=> truedupはオブジェクトを浅くコピーするため、mutable なオブジェクト共有を避けるために Rails 内部でよく使われます。
#58545 Don't touch the connection in MySQL's fetch_indexes for an empty list
マージ日: 2026/8/24 | 作成者: @kamipo
- 概要 (1-2文で)
MySQL アダプタのfetch_indexesが、空のテーブルリストに対しても不要に DB 接続を開いてしまう問題を修正し、SQLite と同様に「空配列なら即 return」するようにした PRです。これにより、indexes([])呼び出し時に接続を触らないため、特定のテストが不安定に落ちる問題も解消されます。
- 変更内容の詳細
対象ファイル:
activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
実質的な変更は以下のようなイメージです(擬似コード・抜粋):
def fetch_indexes(tables, **kwargs)
+ return {} if tables.empty? # 追加された早期 return
optional_columns = []
if supports_expression_index?
# database_version を参照 → ここで接続を触る
optional_columns << "EXPRESSION"
end
# 以降、テーブルごとの index 情報を取得する処理 ...
endポイント:
これまで:
fetch_indexesはtablesが空かどうかに関係なく、最初にoptional_columnsを組み立てていた。- その過程で
supports_expression_index?を呼び出し、内部でdatabase_versionを参照するため、まだ使っていない接続をオープンしてしまう。 indexes([])のように「空配列を渡して何も読まない」ケースでも DB 接続を触る挙動になっていた。
変更後:
tables.empty?であれば直ちにreturn(戻り値はアダプタの実装に応じて空の Hash/配列など;MySQL の既存インタフェースに合わせる)。- これにより
supports_expression_index?が呼ばれず、接続も開かない。 - SQLite アダプタのリーダ(同等の
fetch_indexes相当実装)が既にやっている挙動に合わせた、という位置づけ。
PR 説明にある問題の再現状況:
- テスト
SchemaStatementsTest#test_an_empty_list_reads_nothingは「空リストを渡したときに接続を読みに行かないこと」を確認するテスト。 - 直前のテストで「接続を再確立する」ような処理が走ると、その直後に
indexes([])が接続を開く挙動と噛み合ってテストが落ちるケースがあった。 - CI(Buildkite)でしばしば発生していた不安定な失敗をこの変更で解消している。
- 影響範囲・注意点
- 対象: MySQL アダプタを使う環境で、
indexes([])や、内部的にfetch_indexesに空配列を渡すような呼び出し。 - 動作の変化:
- これまで: 空配列でも DB 接続を開く可能性があった。
- これから: 空配列なら接続に触らず即座に空結果を返す。
- 互換性:
- 空配列に対する戻り値としてもとから「空の結果」を期待しているはずなので、アプリケーションレベルの互換性は基本的に問題ない。
- 「空配列なのに接続が開く」ことに依存しているコードがあれば理論上は影響しうるが、実用上そのような依存は想定されない。
- 利点:
- 不要な接続確立を避けることで、テストや本番での無駄な接続コストが減る。
- 接続の再確立後すぐの状態を前提としたテストがより安定する。
- SQLite 実装との一貫性が向上し、アダプタ間の挙動差異が減る。
- 参考情報 (あれば)
関連テスト:
SchemaStatementsTest#test_an_empty_list_reads_nothing
空リストに対してスキーマ関連メソッドが「何も読みに行かない(副作用を持たない)」ことを確認するテスト。
CI の失敗ログ (Buildkite):
#58541 Further improve Reflextion#extensions
マージ日: 2026/8/22 | 作成者: @byroot
- 概要 (1-2文で)
ActiveRecord::Reflection#extensionsの実装を前PR(#58540)に続いてリファクタリングし、処理の簡潔化・一貫性向上・不要なコードの削除を行った変更です。挙動自体は基本的に互換を維持しつつ、内部実装を整理しています。
- 変更内容の詳細
※ファイル差分が小さく、reflection.rb 内のメソッド(特に Reflection#extensions)周辺のみが対象です。以下はRailsの既存実装とPRタイトル・行数差分からの技術的な推定を含みます。
背景: Reflection#extensions とは
ActiveRecord::Reflection は、has_many / has_one / belongs_to などの関連定義を表現するオブジェクトで、extensions は関連定義に指定された拡張モジュール(extend オプションなど)を返すためのメソッドです。
class User < ApplicationRecord
has_many :posts, extend: Module.new {
def recent
where("created_at >= ?", 1.week.ago)
end
}
end
User.reflect_on_association(:posts).extensions
# => [#<Module ...>] (この extend で渡したモジュールの配列)今回のPRで行われた主な変更点
コード量としては「+7 / -12」と比較的小さいですが、以下のようなリファクタリングが行われています。
extensionsの組み立てロジックの単純化- 以前のPR(#58540)で導入したロジックの後続として、条件分岐や配列の組み立て方がさらに整理されています。
- 例:
Array(...)やcompact,flattenの使い方を整理し、「常に配列を返す」「nilは返さない」といった性質を明確化。if/caseで重複していた分岐をまとめ、1つのパスで配列を構築するような形にしている可能性が高いです。
active_record/reflection.rb内だけに閉じた変更- 変更ファイルは
reflection.rbのみで、他のクラス・モジュールに跨る影響はありません。 Reflection#extensions以外にも、これを呼び出す内部メソッドの記述が少し整理されている可能性がありますが、根本的な責務の変更はありません。
- 変更ファイルは
前PR(#58540)のフォローアップ
- 前PRにより
extensionsがより柔軟に扱えるようになった一方で、まだ改善余地のあった箇所(冗長な処理・境界ケースの扱い)が今回のPRで修正されています。 - たとえば以下のようなケースでの扱いが、より一貫した形になっている可能性があります:
extend: nil(明示的にnilを渡した場合)extendに単一モジュール vs 複数モジュールの配列を渡した場合scopeなど他のオプションとextensionsの混在
- 前PRにより
概念的には、次のようなイメージの整理が行われたと考えられます:
# (イメージ)以前はパターンごとにバラバラに処理していたものを…
def extensions
exts = []
exts << options[:extend] if options[:extend]
exts.concat(Array(@extensions)) if defined?(@extensions)
exts.flatten.compact
end
# のように「最終的に常に Array<Module> を返す」ことを
# シンプルなパイプラインで保証する形に統一、など。※上はあくまで構造イメージであり、実際の実装とは異なります。
- 影響範囲・注意点
対象バージョン
- マージ日からすると Rails 7.2 〜 8.0 系列のどこか(あるいは main ブランチ)に入る変更です。
ActiveRecordの関連メタ情報周りを使っているコードにのみ影響します。
- マージ日からすると Rails 7.2 〜 8.0 系列のどこか(あるいは main ブランチ)に入る変更です。
アプリケーションコードへの影響
- 公開APIである
Reflection#extensionsの返り値の「型」(配列であること)は維持されています。 - ただし、以下のようなコードを書いている場合は動作確認が推奨されます:
reflection.extensionsの中身に対して、nilを含む前提や、特定の順序に依存したコード- 前PR(#58540) 時点のバグや曖昧な挙動を“仕様”として利用していたメタプログラミング
- 公開APIである
メタプログラミング・プラグイン作者向け
- gem / エンジンで
Reflection#extensionsを直接参照している場合は、テストを流して想定通りの配列が返っているか確認すると安心です。 - 特に、
extend:オプションに配列や Proc を渡すような高度な使い方をしている場合は、rails consoleなどでreflect_on_association(...).extensionsの実際の値を確認しておくとよいです。
- gem / エンジンで
- 参考情報 (あれば)
対象PR:
- #58541: 「Further improve Reflextion#extensions」
- 前PR #58540:
Reflection#extensionsに対する最初の改善(今回の変更の前提)
関連コード(現行の公式実装を確認するには):
activerecord/lib/active_record/reflection.rbActiveRecord::Reflection::AssociationReflection付近def extensionsまたはそれに類するメソッドの定義を確認すると、今回のリファクタリング結果が追えます。
利用中アプリでの確認方法例:
# コンソールなどから
reflection = User.reflect_on_association(:posts)
reflection.extensions # => どういう配列が返っているか確認
reflection.extensions.map(&:class)この値をRailsアップグレード前後で比較すると、挙動の差分を把握しやすくなります。
#58540 Avoid needless Array#flatten calls
マージ日: 2026/8/22 | 作成者: @byroot
- 概要 (1-2文で)
extending!などで使われていた「*argsを受け取ってargs.flattenする」パターンをやめ、ほとんどの呼び出しで無駄になっているArray#flatten呼び出しを避ける最適化が行われました。これにより、特にネストした配列が渡される場合に発生していた高コストな内部処理(identity Hash の生成)が減り、パフォーマンスがわずかに向上します。
- 変更内容の詳細
背景
これまで
extending!は典型的な書き方として以下のような実装になっていました(擬似コード):rubydef extending!(*modules) modules = modules.flatten ... endこのパターンは「柔軟に配列/単体/ネスト配列を受け取れる」というメリットがある一方で、
- 単一の空配列を渡すケース(
extending!([])など)が多い - もしくはそもそもネストしていない配列がほとんど という状況だと、毎回
Array#flattenを呼ぶのは無駄になります。
- 単一の空配列を渡すケース(
特に
Array#flattenは、ネストした配列を辿る際に再帰検出のための identity Hash(オブジェクトIDをキーにした Hash)を内部的に生成するため、コストが高くなりがちです。
今回の変更のポイント
PR説明文から読み取れる意図と差分統計(+11 / -2, ファイル2つ)から、以下のような変更が行われたと考えられます。
extending!周りから無条件のflattenを排除- 以前:
*argsを受け取り、必ずargs.flattenを呼んでいた。 - 以後:
- 可能な限り「すでにフラットな配列 or モジュール列」として扱う。
- 必要な場合にのみ flatten 相当の処理を行う、あるいは呼び出し側の API を整理して、ネストした配列をそもそも渡さないようにしている。
- 以前:
ActiveRecord::ReflectionとAssociationScopeでの呼び出し整理
変更ファイル:activerecord/lib/active_record/associations/association_scope.rbactiverecord/lib/active_record/reflection.rb
これらの中で
extending/extending!オプションや関連する internal API に対して、- 引数の扱いを見直し、最初からフラットな配列を渡すようにしているか
- あるいは 単一 or 複数のモジュールを明示的に扱う形に修正している可能性が高いです。
具体的なイメージ(あくまで典型的な書き換え例):
ruby# 以前(例) def extending!(*modules) modules = modules.flatten @extensions.concat(modules) end # 以後(イメージ) def extending!(*modules) modules = modules.first if modules.size == 1 && modules.first.is_a?(Array) @extensions.concat(modules) endもしくは、呼び出し側で:
ruby# 以前(例) relation.extending!([SomeModule]) # 以後(例) relation.extending!(SomeModule)のようにし、
extending!側では「ネスト配列が来ない」前提でシンプルに処理できるようにしている可能性があります。
- 影響範囲・注意点
対象領域
- Active Record の
ReflectionとAssociationScope周辺、特にextending/extending!オプションの処理。 - 関連スコープやカスタムモジュールを関連にミックスインする機能を利用しているコードパス。
- Active Record の
外部 API 互換性
- Rails のパブリック API としての
extending/extending!の使い方(シンボルまたはモジュールを一つまたは複数渡す)は、従来どおり動くように配慮されているはずです。 - ただし、非推奨な使い方(ネストした配列を渡すなど)に依存していた場合には挙動の変化 or 受け付けなくなる可能性があります。
- 例:
extending!([[MyExtension]])のような深いネストに依存している場合。 - 一般的には想定されていない使い方なので、問題になる可能性は低いです。
- 例:
- Rails のパブリック API としての
パフォーマンス面
- コストが高いケースは「ネストした配列 + flatten」という組み合わせなので、そうした呼び出しが頻発する場合にわずかながら改善が見込めます。
- ほとんどのアプリでは体感できるほどの差はないものの、大規模な関連定義や激しくクエリを構築するケースでは微小な改善が積み重なります。
テスト観点
extending/extending!を利用しているモデル・関連に対し、特に以下を確認すると安心です:- 単一モジュール / 複数モジュールを渡した場合の動作
- アプリ側が配列で渡している場合(
extending [Foo, Bar]など)があるなら、その挙動が変わっていないか
- Rails 側のテストで互換性は担保されているはずですが、アプリ固有のメタプログラミング的な使い方をしている場合は念のため確認を推奨します。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58540
- 関連知識:
- Ruby の
Array#flattenはネスト検出のために内部で identity Hash を用いる実装になっており、深いネストや自己参照配列を扱う想定のために比較的重い処理です。 - Rails コア全体で「
*args受け取り +flatten」パターンを見直し、ホットパスから余計なflattenを排除する動きが進んでいます。
- Ruby の
#58537 Fix async ActiveRecord::StatementCache execution with out-of-range bind values
マージ日: 2026/8/22 | 作成者: @viralpraxis
- 概要 (1-2文で)
ActiveRecord::StatementCacheの非同期実行パスで、存在しないPromise.wrapを呼んでいたことによる例外を修正し、正しくPromise::Complete.newを使うようにした PR です。これにより、バインド変数が範囲外のときの非同期実行でも、例外ではなく空結果を返すようになります。
- 変更内容の詳細
バグの内容
ActiveRecord::StatementCache の「非同期でのクエリ実行+バインド変数が範囲外」というケースで、以下のようなエラーが出ていました。
NoMethodError: undefined method 'wrap' for class ActiveRecord::Promise
lib/active_record/statement_cache.rb:159:in 'ActiveRecord::StatementCache#execute'原因は、ActiveRecord::Promise に存在しないクラスメソッド wrap を呼び出していたことです。
実際に用意されているのは ActiveRecord::Promise::Complete.new(value) であり、同期的に値がすでにわかっている「完了済みの Promise」を表現するために使われます。
この問題は、過去の PR (#52807) の変更の一部として入り込んだものとされています。
実際の修正内容
StatementCache 内で、範囲外バインド値の場合に非同期パスで返す Promise の生成方法を修正しています。
差分は 1 行のみですが、意味としては:
before (イメージ)
ruby# 存在しないメソッド ActiveRecord::Promise.wrap([])after
rubyActiveRecord::Promise::Complete.new([])
これにより、API が想定している正しい方法で「すでに完了している(結果が空の)Promise」を返すようになりました。
テストの追加
activerecord/test/cases/statement_cache_test.rb に、非同期実行かつ範囲外バインド値の挙動を確認するテストが追加されています。
テストの意図としては:
- ステートメントキャッシュを利用
- バインド値が範囲外になるような条件でクエリを実行
async: trueのような非同期パスを通す- 結果が「例外ではなく空の結果 (empty relation / empty array 等)」で返ること
- かつ Promise の処理が正常に完了すること
を検証しています。
- 影響範囲・注意点
影響対象
- Rails 7.2 以降(
ActiveRecord::Promise/ 非同期クエリ /StatementCacheが導入・変更されたバージョン以降)で、 ActiveRecord::StatementCacheを使っており、- 非同期実行(
async/load_asyncなど)を経由し、 - かつ「バインド変数が範囲外」になる可能性があるクエリを実行するコード
- Rails 7.2 以降(
これまでの挙動
- 上記条件を満たすと
NoMethodError: undefined method 'wrap' for class ActiveRecord::Promiseが発生し、アプリ側から見ると非同期クエリの実行時に謎のNoMethodErrorが飛ぶ状態でした。
- 上記条件を満たすと
修正後の挙動
- 同期実行時と同様に、範囲外バインド値の場合は「空の結果」を表す
Promise::Completeが返り、呼び出し側はawaitやresultの取得で例外なく処理できます。 - 既存の(範囲内の)非同期クエリの挙動には影響しません。
- 同期実行時と同様に、範囲外バインド値の場合は「空の結果」を表す
マイグレーションや API 変更は不要
- 外部 API (public API) のシグネチャ変更はなく、バグ修正のみです。
- アプリケーションコードの変更は基本的には不要で、Rails のバージョンを上げるだけで恩恵を受けられます。
CHANGELOG 未更新
- PR 時点では CHANGELOG は更新されていませんが、挙動としてはバグ修正に該当します。
- リリースノートでは「非同期 StatementCache + out-of-range bind での NoMethodError 修正」といった扱いになる可能性があります。
- 参考情報 (あれば)
- 問題を導入したとされる PR:
- 対象 PR 本体:
- 関連クラス:
ActiveRecord::StatementCacheActiveRecord::PromiseActiveRecord::Promise::Complete
#58519 Load routes lazily when a mounted helper is called
マージ日: 2026/8/22 | 作成者: @seuros
- 概要 (1-2文で)
Rails のルーティングを「遅延ロード」している環境で、main_appやエンジンのマウントヘルパを最初に呼び出した場合にNoMethodErrorが出ていた問題を修正し、必要に応じてルートを自動ロードしてから再実行するようになりました。これにより、URL/パスヘルパと同様に、マウントヘルパもルートの遅延ロードと整合的に動作します。
- 変更内容の詳細(サンプルコード含む)
何が問題だったか
Rails には、ルートを「最初に使われたタイミングで読み込む」LazyRouteSet があり、以下のような「名前付き URL ヘルパ」はすでにそれを利用していました。
# これらの呼び出しは LazyRouteSet によりルート定義を読み込む
app.users_path
app.new_user_urlしかし、以下のような「マウントヘルパ」は挙動が異なっていました。
main_appmy_engineなどmount SomeEngine => "/some"によって提供されるエンジン用ヘルパ
<%= main_app.root_path %>
<%= blog_engine.posts_path %>これらは「routes.rb の mount が実行されたときに定義されるメソッド」であり、
名前に _path/_url のようなサフィックスもないため、LazyRouteSet でのフィルタ条件にも引っかからず、
- ルートがまだ一度も描画(draw)されていない状態で
- いきなり
main_appやエンジンヘルパを呼ぶ
と、ルートが読み込まれず、そもそもヘルパメソッドが存在しないため NoMethodError が発生していました。
何をしたか
MountedHelpers モジュールに method_missing / respond_to_missing? を実装し、
「マウントヘルパらしきメソッドが呼ばれたら、一度ルートをロードしてから再試行する」ようにしました。
おおまかな流れは次の通りです(擬似コード):
module MountedHelpers
def method_missing(name, *args, &block)
# 1. 一度ルートをロードさせる
load_routes_via_lazy_route_set
# 2. ルートロード後に、そのメソッドが定義されていれば再実行
if respond_to?(name)
return public_send(name, *args, &block)
end
# 3. それでも存在しなければ従来どおり NoMethodError
super
end
def respond_to_missing?(name, include_private = false)
load_routes_via_lazy_route_set
# ルートロード後に respond_to? が true になれば true を返す
super || some_mounted_helper_defined?(name)
end
end実際にはこのロジックは railties/lib/rails/engine/lazy_route_set.rb に実装されており、MountedHelpers が LazyRouteSet 経由で適切にルートロードをトリガするようになっています。
テストで確認されていること
railties/test/engine/lazy_route_set_test.rb にテストが追加されており、主に次のようなケースがカバーされています:
- ルートがまだ一度も描画されていない状態で
main_appを呼んだときに、
ルートがロードされてからヘルパが利用できること - エンジンを
mountした場合のエンジン用マウントヘルパ (blog_engineなど) でも同様に動くこと - 結局存在しないメソッドの場合は、従来どおり
NoMethodErrorになること respond_to?/respond_to_missing?を用いたメタプログラミングでも、
ルートロード後の正しい結果が返ってくること
railties/CHANGELOG.md にも、マウントヘルパに対する遅延ロード対応が行われた旨が追記されています。
- 影響範囲・注意点
影響範囲
- 遅延ルーティング(LazyRouteSet)を利用している Rails アプリ全般に影響します。
- 特に、初回アクセスが
main_appやエンジンのマウントヘルパ経由になるケース
(例: レイアウトや initializer 内でmain_appを参照する、など)で挙動が変わります。
- 特に、初回アクセスが
- 従来は「初回に
NoMethodErrorになっていた」パターンが
「ルートをロードしてから正常に動作する」ようになるため、基本的には後方互換的な修正です。
注意点
method_missingが追加されたことで、MountedHelpersを include しているコンテキストでは、
ある程度「柔らかい」メソッド解決が行われるようになります。- ただし、ルートロード後も存在しないメソッドは
NoMethodErrorになるため、
デバッグが極端に難しくなるような変更ではありません。
- ただし、ルートロード後も存在しないメソッドは
- ルーティングのロードタイミングが変わる可能性があります。
- もし「ルートがまだロードされていないことを前提にした特殊な処理」を自前でしている場合は、
main_app/エンジンヘルパを呼んだ時点でルートがロードされる点に注意が必要です。
- もし「ルートがまだロードされていないことを前提にした特殊な処理」を自前でしている場合は、
respond_to?の結果も、ルートロードをトリガしてから判定されるようになるため、
反射的にメソッド存在チェックを行うようなロジックで挙動が変わる場合があります
(もっとも、多くの場合は期待通りの修正と言えます)。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58519
- 関連概念:
LazyRouteSet(Rails のルート遅延ロード機構)- マウントヘルパ:
main_app,YourEngineName::Engineをmountしたときに生成されるヘルパ
- この変更により、「URL ヘルパは遅延ロードに対応しているが、マウントヘルパは対応していない」という
一貫性のない状態が解消され、どちらも同じように「最初の利用時にルートをロードする」挙動になります。
#58535 Fix the Relation#create documentation example
マージ日: 2026/8/22 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Relation#createのドキュメント中のサンプルコードが、スコープ付きcreateの振る舞いと食い違っていたため、正しい例になるように修正した PR です。コードの挙動自体には変更はなく、ドキュメントのみの修正です。
- 変更内容の詳細
問題になっていた元のドキュメント例は以下のような流れでした(説明文を要約):
users = User.where(name: 'Oscar')
users.create # => #<User id: 3, name: "Oscar", ...>
users.create(name: 'fxn')
users.create # => #<User id: 4, name: "fxn", ...>ここでの問題点:
usersはwhere(name: 'Oscar')でスコープされたRelation。- 引数なしの
users.createは、スコープされた属性 (name: 'Oscar') を使ってレコードを作成する。 - したがって 2 回目の
users.create(引数なし)は"fxn"ではなく"Oscar"という名前のユーザーを作るはず。 - 直前の例(1 個目の
users.create)で「スコープの属性が使われる」ことはすでに説明されており、テスト (relations_test.rb:1357-1359) でもその挙動が保証されている。
つまり、2 個目のコード例は次のように「結果表示のためだけに書いたはずの行」に、誤って引数なし create 呼び出しが混ざっていた:
users.create(name: 'fxn')
users.create # => #<User id: 4, name: "fxn", ...> # ←これが誤解を招くこの PR では、その「余計な引数なし create 呼び出し」を削り、他の例と同様に「create の戻り値をそのまま表示する形式」に揃えています。
変更ファイルは activerecord/lib/active_record/relation.rb の 1 行差し替えのみで、実装コードではなくコメント・ドキュメント行の修正です。
- 影響範囲・注意点
- ランタイム挙動・API 仕様・テストには一切変更なしです。
- 影響があるのは
Relation#createの使い方をドキュメントから学ぶ開発者の理解のみです。 - 今回の修正から改めて確認できる重要なポイント:
User.where(name: 'Oscar').create
→name: 'Oscar'が自動的に使われる。User.where(name: 'Oscar').create(name: 'fxn')
→ 明示した属性 (name: 'fxn') が優先される。
- 既存コードを変更する必要はありませんが、「スコープ付き Relation に対して引数なし
createを呼ぶと、スコープ条件が属性として使われる」という仕様を正しく理解しておく必要があります。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58535
- 関連する履歴: ブロック導入時のコミット
5edbeb0a87で例を編集した際に、不要なcreate呼び出しが持ち越されたことが今回の問題の原因と説明されています。 - 動作仕様を確認できるテスト:
activerecord/test/cases/relation_test.rbのrelations_test.rb:1357-1359(スコープされた属性がcreateで使われることを検証)。
#58534 Add enumerable to AttributeMethodPatternSet
マージ日: 2026/8/21 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Rails の Active Model にあるAttributeMethodPatternSetにEnumerableが追加され、attribute_method_patternsが再び列挙可能(each や map などが使用可能)になるように調整した PR です。以前は配列だったものをクラスに置き換えた際に失われた「列挙性」を補う変更です。
- 変更内容の詳細
背景
- 過去の PR (#58462) で
attribute_method_patternsの実装が「配列」からAttributeMethodPatternSetという独自クラスに変更された。 - 元々は配列だったため
each,map,selectなどEnumerableによる操作が可能だったが、新クラスにはeachメソッドはあるもののEnumerableを include しておらず、mapなど一般的な列挙メソッドが使えない状態になっていた。 attribute_method_patterns自体は private API だが、過去の実装では列挙できていたため、意図せず振る舞いが変わった形になっていた。
今回の変更
activemodel/lib/active_model/attribute_methods.rb において、AttributeMethodPatternSet に Enumerable を include した変更が入っています(実質 2 行程度の追加)。
イメージとしては以下のような形になります(概念的なコード例):
class AttributeMethodPatternSet
include Enumerable
def initialize(patterns = [])
@patterns = patterns
end
def each(&block)
@patterns.each(&block)
end
endこれにより:
model_class.attribute_method_patterns.each { |pattern| ... }
model_class.attribute_method_patterns.map { |pattern| pattern.prefix }
model_class.attribute_method_patterns.any? { |pattern| ... }といった、従来「配列としてできていた」使い方が引き続き可能となります。
テスト
activemodel/test/cases/attribute_methods_test.rb に 7 行のテストが追加され、attribute_method_patterns が Enumerable として振る舞うこと(少なくとも each 経由で期待どおり列挙できること)が確認されています。
- 影響範囲・注意点
- 対象: Active Model の
attribute_method_patternsを直接扱っているコード(メタプログラミングやプラグイン、gem など)。 - 影響:
- 以前(配列時代)にできていた「列挙処理」が、新クラス移行後にできなくなっていた場合、この PR により再び可能になります。
Enumerableを include したことでmap,select,reject,findなど標準的な Enumerable メソッドが使えるようになり、実質的には「従来の配列に近いインターフェイス」が復元されます。
- 互換性:
- public API ではなく private API であるため、公式には互換性保証外ですが、内部的・拡張用コードで依存している場合の「非公開だけど事実上の互換性」を戻した形です。
- すでに
attribute_method_patternsを「配列ではない前提」で書き換えていたコード(例えばto_aを毎回呼ぶなど)は、そのままでも動作しますが、冗長になっている可能性があります。
- 参考情報 (あれば)
- この PR の動機となった変更:
- https://github.com/rails/rails/pull/58462
attribute_method_patternsの実装が配列からAttributeMethodPatternSetに変更された PR
- https://github.com/rails/rails/pull/58462
- 対象クラスの利用箇所の概念:
- Active Model / Active Record が、
find_by_foo,foo?,foo_before_type_castのような動的属性メソッドを定義する際の「パターンリスト」を表すのがattribute_method_patterns/AttributeMethodPatternSetです。
- Active Model / Active Record が、
#58522 Fix Callbacks only made shareable in :raise mode
マージ日: 2026/8/20 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
ActiveSupport::Callbacksが、Ractors.unshareable_proc_actionを:warnにしている場合でもコールバックを「Ractor 共有可能」にするよう修正された PR です。あわせて、freezeの可視性バグ修正と、アプリケーションの executor が Ractor 内で動作するために必要な executor 関連コールバックの shareable 化も行われています。
- 変更内容の詳細
2-1. Callbacks と Ractors.unshareable_proc_action の挙動修正
問題点
これまで Ractors.unshareable_proc_action が :raise のときだけ、ActiveSupport::Callbacks 側で「Ractor で共有可能(shareable)」になるような処理を行っていました。unshareable_proc_action = :warn の場合は例外を投げないため、そのまま共有不能な Proc が残り、Ractor 内でコールバックを使おうとすると「共有できない Proc が混ざっている」という問題が潜在的に起こりうる状態でした。
対応内容(概念的なイメージ)
PR では、Callbacks が自分自身を shareable にしようとするロジックが、:raise のときだけでなく、少なくとも :warn 設定時にも動くように修正されています。
イメージとしては以下のような変更が入っていると考えられます(擬似コード):
# 変更前(イメージ)
if Ractors.unshareable_proc_action == :raise
make_callbacks_shareable!
end
# 変更後(イメージ)
if [:raise, :warn].include?(Ractors.unshareable_proc_action)
make_callbacks_shareable!
endこれにより、
:raise… 共有不能な Proc があれば例外を出す:warn… 共有不能な Proc は警告に留めるが、Callbacks 側はできる限り shareable な形に加工する
という挙動になり、Callbacks が Ractor 環境でより一貫して動作するようになります。
2-2. ActiveSupport::Callbacks#freeze の可視性修正
問題点ActiveSupport::Callbacks が freeze メソッドを「protected メソッド」にしてしまっていたため、Ruby 標準のオブジェクトフリーズの挙動から外れた状態になっていました。通常、freeze は public であり、外部から自由に呼べることが前提です。
対応内容
この PR で freeze の可視性が適切に修正され、再び public(もしくは Ruby 通常の期待通り)として利用できるようになっています。
これは主に:
- Callbacks を自前でフリーズして再利用する
- Ractor 間でオブジェクトを共有するために freeze したい
といったパターンで、意図せず NoMethodError になるような不具合を防ぎます。
2-3. Executor 関連のブロックベースコールバックを shareable に
問題点
Rails アプリケーションの executor(Rails.application.executor など)を Ractor 内で利用する場合、その内部で使われるブロックベースのコールバックが Ractor 的に「共有可能」になっていないと、Ractor からアクセスできません。
PR 説明にある通り:
two block based executor callbacks needed to be made explicitly shareable for the application's executor to work inside a Ractor
という状態だったため、これらのコールバックが Ractor shareable に明示的に変換されるように修正されています。
具体的な影響箇所(イメージ)activesupport/lib/active_support/railtie.rb の変更により、アプリケーションの起動時に設定される executor 用コールバックを、例えば以下のように shareable として扱うようにしていることが考えられます(あくまでも概念的なサンプル):
config.to_prepare do
# 何らかの executor callback を
callback = -> { ... }
# Ractor で共有できるように
Ractor.make_shareable(callback)
executor.register_callback(callback)
end実際には Ractor.make_shareable 呼び出しやそれに準じたユーティリティが挟まる形で、executor 関連のコールバックが Ractor-safe な形に整えられています。
2-4. テスト (callbacks_test.rb) の更新
activesupport/test/callbacks_test.rb で:
Ractors.unshareable_proc_actionの設定が:warn/:raiseそれぞれで、Callbacks が shareable 化されることfreezeの可視性や挙動が期待通りであること- executor 関連コールバックが Ractor 上で動作すること
といったケースをカバーするテストが追加・修正されています(+26/-24 行)。
影響範囲・注意点
Ractor を使っている Rails / ActiveSupport コードへの影響
Ractors.unshareable_proc_action = :warnのときも、Callbacks が自動で shareable 化を試みるようになり、以前より「Ractor で動く」コードが増える可能性があります。- 一方で、これまで「実質 Ractor 上では動いていなかったが運良く落ちていなかった」ようなケースで、挙動が変わる可能性があります(より正しく shareable にされる/警告が出る等)。
カスタムコールバックを大量に使っている場合
- コールバックチェーン内部で Ractor 共有不能なオブジェクト(例: ミューテックス、IO オブジェクト、スレッドローカルに依存する Proc など)を閉じ込めていると、
unshareable_proc_actionの設定に応じて警告あるいは例外が表に出やすくなります。 - 将来的に Ractor を活用したい場合は、コールバックに閉じ込めるオブジェクトを「共有可能」なもの(不変オブジェクト、freeze 済みオブジェクト)に寄せることが望ましいです。
- コールバックチェーン内部で Ractor 共有不能なオブジェクト(例: ミューテックス、IO オブジェクト、スレッドローカルに依存する Proc など)を閉じ込めていると、
freeze可視性変更の影響ActiveSupport::Callbacksを継承・拡張していて、freezeの可視性に依存した挙動(メソッドをオーバーライドしているなど)をしている場合は、その前提が変わっていないかを確認する必要があります。- 一般的なアプリケーションコードでは副作用はほぼありませんが、メタプログラミングを多用しているライブラリでは影響を受けうる箇所です。
Executor を Ractor 内で使っている/使う予定がある場合
- この修正により、
Rails.application.executor系の処理が Ractor 内でより正式にサポートされる方向に進んでいます。 - ただし、executor 内部で実際に動く処理(
around_action的なものやフック)に Ractor 非対応なロジックがある場合は、依然としてエラーや警告が出る可能性があります。
- この修正により、
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58522
- Ractor と shareable オブジェクトの概要:
- Ruby 公式ドキュメント「Ractor」
Ractor.make_shareable,Ractors.unshareable_proc_actionの挙動説明
- Rails の executor の概要:
- Rails ガイド: “Running Code Before, During, and After Requests” / “Execution context” など
ActiveSupport::Callbacksの基本:- Rails ガイド: “Active Support Core Extensions” / “Callbacks”
- Rails API ドキュメント:
ActiveSupport::Callbacks
#58531 Allow to call url helpers inside a ractor
マージ日: 2026/8/20 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Rails の URL ヘルパー(post_pathなど)を Ractor 内から呼び出せるようにするため、RouteSetと URL ヘルパー生成周りの実装が Ractor 対応になるよう調整された PRです。主なポイントは「URL ヘルパーを格納する Proc を Ractor 共有可能(shareable)にするための参照方法の工夫」と「ルーティング確定後にRouteSetを freeze すること」です。
- 変更内容の詳細
背景となる問題
従来、Ractor 内で URL ヘルパーを呼ぶと、内部的に参照している ActionDispatch::Routing::RouteSet オブジェクトが「shareable ではない」ために Ractor::IsolationError が発生していました。
Ruby 3 の Ractor では、Ractor 間で共有できるのは「shareable なオブジェクト」だけで、shareable でないオブジェクトを閉じ込めた Proc も shareable にできません。
URL ヘルパーは内部的には Proc 経由で RouteSet にアクセスするため、ここがネックになっていました。
routes = self # RouteSet オブジェクト
Ractor.make_shareable_proc { routes }
# => Ractor::IsolationError: cannot make a shareable Proc because it can refer unshareable object ...解決方針の要点
- URL ヘルパーの Proc 自体を shareable にする
- ルーティング確定後 (
finalize後) にRouteSetを凍結 (freeze) して shareable にする - URL ヘルパーの Proc は「生成時点では shareable ではない RouteSet にアクセスしない」形で作りつつ、「後から freeze された RouteSet には届く」ようにする
この「一度は Ruby を“だます”が、最終的には正当になる」構成が PR の肝です。
具体的な実装のアイデア
1. Proc が直接 RouteSet をキャプチャしないようにする
以前は、URL ヘルパー生成時に以下のように RouteSet をローカル変数として閉じ込めていたイメージです(概念図):
routes = self # RouteSet
helper = proc do |*args|
routes.url_for(...)
endこの形だと helper は shareable にできません。
PR では、Proc 内で「ローカル変数を通して RouteSet を参照する」のではなく、「モジュール(またはクラス)経由で RouteSet に辿る」ように変えています。Ractor 的には「Module は shareable」とみなされるため、その Module をキャプチャしている Proc であれば shareable にできます。
イメージとしては:
# self は URL ヘルパーを定義しているモジュール/クラス
mod = self # Module や Class は Ractor 的に shareable
helper = Ractor.make_shareable_proc do |*args|
# ここで初めて RouteSet に辿り着く
routes = mod._routes
routes.url_helpers.some_path(*args)
end- Proc 生成時点:
modは shareable なのでhelperは shareable と判定される_routes(= RouteSet) はこの時点では評価されず、参照もされない
- ルーティング確定後:
- RouteSet が freeze され shareable になる(後述)
- Ractor 内から
helperを呼び出すと、mod._routesを通じて shareable な RouteSet にアクセスできる
この「Module → RouteSet の間接参照」によって、Proc 自身を Ractor に渡せるようになります。
2. RouteSet を finalize タイミングで freeze する
ルーティング定義 (draw など) が完了すると finalize が呼ばれ、それ以降は Route を追加しない、というのが Rails の設計です。
この PR では、このタイミングで RouteSet を freeze します。
ポイント:
- ルート定義が変わらないことが前提になるため、freeze しても挙動上の問題は基本的にない
freezeされたRouteSetは Ractor 的には shareable とみなせる- これにより、先ほどの Module 経由アクセスであっても、最終的にたどり着く
RouteSetが shareable になる
コードとしては ActionDispatch::Routing::RouteSet と routes.rb、formatter.rb などで freeze を前提とした扱いに微調整が入っていると考えられます。
3. 関連ファイルの変更概要
actionpack/lib/action_dispatch/journey/formatter.rb- URL 生成(パス組み立て)で利用される formatter が、freeze 済みなオブジェクトでも問題なく動くような調整が入っている可能性が高い(キャッシュや内部状態の扱いなど)。
actionpack/lib/action_dispatch/journey/routes.rb- ルートオブジェクト (
Journey::Routes) 自体の freeze タイミングや、内部データ構造の shareable 化に向けた調整。
- ルートオブジェクト (
actionpack/lib/action_dispatch/routing/route_set.rb- URL ヘルパーメソッドを定義するロジックで「Module 経由の参照」に変更。
finalize内かその周辺でRouteSetをfreezeする処理が追加。
actionpack/test/dispatch/routing/route_set_test.rb- Ractor 内から URL ヘルパーを呼び出せることを確認するテストが追加。
railties/lib/rails/application/routes_reloader.rb- ルート再読み込み時の finalize や freeze の扱いに関する微調整(再読み込み時に新しい RouteSet を作る/freeze し直すなど)。
- 影響範囲・注意点
Ractor を使うアプリに対するポジティブな影響
- マルチ Ractor 構成で、ERB や view / background job などから URL ヘルパーを安全に呼べるようになる。
- Ractor 対応の Web サーバ/ワーカーで URL 生成を伴う処理を並列化しやすくなる。
RouteSet が freeze されることによる影響
- ルーティング確定後に
RouteSetを直接変更するようなコード(メタプログラミングでルートを後付けする等)は動かなくなるか、例外が出る可能性がある。 - 通常の使い方(
config/routes.rbだけでルート定義)をしていれば問題にならない想定。
- ルーティング確定後に
URL ヘルパー生成の仕組みに依存したハックとの相性
- 内部実装の変更(特に「どうやって
_routesに辿るか」)に依存した monkey patch をしている場合は壊れる可能性がある。 - たとえば
ActionDispatch::Routing::RouteSetに対して、URL ヘルパー定義メソッドを置き換えている/Proc を直接触っているような高度な拡張は要確認。
- 内部実装の変更(特に「どうやって
パフォーマンス・メモリ
- freeze によってオブジェクトの内部キャッシュが作れなくなるケースがある場合、どこかで eager に構築しておく、もしくは freeze 前に準備するような変更が入っているはずで、その影響は軽微に抑えられていると考えられる。
- ルート定義時にしか mutate しない設計であれば、リクエスト時パスにはほぼ影響しないはず。
- 参考情報 (あれば)
- 本 PR は、URL ヘルパーの Ractor 対応の「2 個目かつ最後のピース」と説明されているため、以前に「URL 生成ロジックや Journey 側の Ractor 対応」を行った関連 PR が存在するはずです。Ractor 対応状況をトレースしたい場合は、
Ractor,shareable,RouteSetあたりで Rails 本体の PR / Issue を探すとセットで理解しやすくなります。
#58529 Make url helpers ractor safe
マージ日: 2026/8/20 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Rails の URL ヘルパー(edit_post_pathなど)を Ractor 内から呼び出せるようにするための下準備として、URL ヘルパー生成まわりのオブジェクトを「Ractor セーフ(共有可能)」にする変更です。まだこれ単体では Ractor 内で URL ヘルパーを完全に使えるわけではありませんが、そのための重要な一部です。
- 変更内容の詳細
目的・背景の整理
Ractor 内で
edit_post_pathのような URL ヘルパーを呼ぶと、以下のような Ractor 制約違反が発生していました:define_methodに渡している Proc が「共有不可能 (unshareable)」- その Proc が閉じているオブジェクト(
Routeなど)が Ractor 的に共有不可能 - URL ヘルパー実行時にインスタンス変数メモ化などが走り、Ractor 内で状態が変化する
この PR は「URL ヘルパーメソッドを生成する」段階にフォーカスしており、
- 生成に使う Proc を共有可能にする
- Proc が参照するオブジェクト(
RouteやPattern)を事前に freeze / eager load して共有可能にする
という方針で、Ractor 内からでも安全に使えるようにしています。
主な変更ポイント(ファイル別)
1. action_dispatch/journey/path/pattern.rb
ActionDispatch::Journey::Path::Patternに対して、Ractor で問題になりうる遅延計算・メモ化を避けるため、事前計算・freeze を進める形の変更が入っています。- URL 生成時に Pattern オブジェクトを経由してパスを組み立てるため、Pattern が Ractor-sharable であることが重要です。
- 典型的には:
- 内部でキャッシュしていた構造(例: 解析済みトークン列・名前付きパラメータなど)を、Route 定義時点かヘルパー生成時点で eager load する
- それらを freeze して以後変更されない前提にする
- これにより、Pattern オブジェクトを Ractor 間で共有しても、Ractor 内で新たなメモ化や外部状態へのアクセスが起きないようにしています。
2. action_dispatch/journey/route.rb
ActionDispatch::Journey::Routeも同様に、Ractor で共有可能なグラフの一部として扱えるように調整されています。- やっていることの方向性は Pattern と同じで、
- Route が持つ
defaults,constraints,parts,pathなど、URL ヘルパー生成時に参照されるプロパティを事前に確定・freeze する - Route の一部で遅延メモ化していたものがあれば eager に構築し、Ractor 内で新たな書き込みが発生しないようにする
- Route が持つ
- これにより「Proc が Route をキャプチャしているためにその Proc が unshareable になる」という問題を回避する狙いです。
3. action_dispatch/routing/route_set.rb
- URL ヘルパーを実際に
define_url_helper的なメソッドで定義しているのがRouteSetで、ここでの Proc 生成方法が Ractor 対応されています。
ポイントは2つ:
define_methodに unshareable な Proc を渡さない- 以前は、ブロックで
define_methodしていて、そのブロックがselfや外部ローカル変数をキャプチャしていた可能性があります。 - それにより、その Proc が Ractor 的に「共有不可能」になり、新しい Ractor に RouteSet を渡したり、Ractor 内でヘルパー生成を叩くと isolation error になっていました。
- この PR では、
- Proc が参照するオブジェクトを最小限にし、かつ shareable にする
- もしくは
define_method(name) do |*args| ... endのような形から、外部コンテキストをほぼ持たない形へと書き換える
といった対応を行っています。
- 以前は、ブロックで
RouteSet の内部依存グラフを freeze / eager load
- URL ヘルパーメソッド定義時に、Route/Pattern 側で必要な情報をすべて読み出し、メモ化や配列構築を済ませておく
- その後、それらのオブジェクトを freeze しておき、Ractor 開始後(あるいは worker ractor 内)では状態が変化しないようにする
- これにより「ヘルパー呼び出し時に ivar に初回アクセス→メモ化→書き込み」というパターンを排除します。
4. テストの追加
actionpack/test/dispatch/routing/route_set_test.rbactionpack/test/journey/route_test.rb
で、主に以下のような観点のテストが追加されています:
- Route および Pattern が freeze / shareable な状態で機能するかどうか
- URL ヘルパーが生成されるときに isolation error(Ractor の shareable チェック)を引き起こさないこと
- Ractor 内から呼んだときに、以前問題になっていたメモ化や外部変数アクセス系のエラーを出さないこと
実際のテストコードは概ね次のようなイメージです:
# 疑似コードイメージ
route = build_route(...)
route.freeze
assert route.ractor_shareable? # 実際には shareable? に相当する検査
ractor = Ractor.new(route) do |r|
# URL ヘルパーや route の情報を参照しても isolation error が出ないこと
end
ractor.takeあるいは:
# RouteSet 経由で URL ヘルパーを生成しても Ractor 的に通ることを確認
set = build_route_set(...)
set.finalize! # ここで helpers を定義
assert_nothing_raised { Ractor.new(set) { |s| s.url_helpers.some_path } }※正確なメソッド名はコードに依存しますが、おおよそこういった性格のテストが追加されています。
- 影響範囲・注意点
開発者への実質的な影響
- 通常のアプリ開発者にとっては、既存コードを書き換える必要は基本的になく、挙動も互換を保つように設計されています。
- この PR の直接の効果は「URL ヘルパー生成が Ractor セーフになる」ことなので、最終的には以下ができるようになる方向です:
- View や background job などで Ractor を使い、その中から
*_path/*_urlヘルパーを呼ぶ - マルチスレッド / マルチRactor環境で RouteSet を共有してパフォーマンスを上げる
- View や background job などで Ractor を使い、その中から
ただし、PR 本文にもある通り:
By itself, it's still not possible to call a url helper, due to other isolation issues on the RouteSet object itself.
と書かれているので、
- このコミット単体ではまだ「Ractor 内で URL ヘルパーを完全に安全に使える」状態にはなっていません。
- RouteSet 周辺の他の isolation 問題(例: internal cache, default_url_options の扱いなど)を解消する別 PR が必要です。
ライブラリ作者・メタプログラミング勢への注意
- Routing / Journey 周辺のクラス (
Route,Patternなど) に手を入れているため、- これらのクラスの内部状態を書き換えているようなメタプログラミング
- freeze されていない前提で差し替え・ monkey patch をしているコード
があると、将来的に Ractor 対応の流れの中で壊れる可能性があります。
- Ractor 対応の基本方針は「Route グラフを構築したら原則 immutable」という方向なので、
- 独自に Route オブジェクトを操作するコードは、「Route 定義フェーズで完結させる」「実行時に書き換えない」という設計に寄せた方が良いです。
パフォーマンス面
- メモ化を Ractor 内から事前計算(eager load)に振り替えるため、
- 初期化時(ルーティングロード時)のコストは少し増える可能性があります
- 一方で、実行時の URL 生成はより安定し、Ractor 上でも追加のロック・同期なしに動けるようになります
- 大規模ルーティングを持つアプリでは、起動時パフォーマンスへの影響を観測しておくと安心です。
- 参考情報 (あれば)
- PR 本文にもあるように、これは「Ractor 対応のパズルの一片」であり、今後も以下のような変更が継続して入る可能性が高いです:
- RouteSet 全体の shareable 化
- controller / view の Ractor セーフ化
- キャッシュ・メモ化処理の freeze / eager load への移行
- 類似の変更はすでに他の部分でも行われており、
- 「グラフを freeze して Ractor 間で共有する」
- 「worker ractor で初回メモ化を起こさないようにする」
という設計パターンが Rails コアの Ractor 対応で共通して使われています。
Ractor を前提とした Rails アプリや gem を検討している場合は、
RouteやPatternを書き換えずに拡張する- 初期化フェーズと実行フェーズを明確に分ける
といった設計を採ると、今回のような変更との親和性が高くなります。
#58528 activerecord: prevent tests from relying on unspecified SELECT order
マージ日: 2026/8/20 | 作成者: @viralpraxis
概要 (1-2文で)
Active Record のテストで、ORDER BYを指定していないSELECTの結果順序に依存してしまう問題(テストのフレーク化やバグの温床)を防ぐため、SQLite でテストを実行する際にreverse_unordered_selectsPRAGMA を有効にする設定がconfig.example.ymlに追加されました。これにより、今後のテストが「暗黙の順序」を前提としないよう強制されます。変更内容の詳細
- 変更ファイル:
activerecord/test/config.example.ymlのみ - 内容: SQLite 用のテスト設定に
reverse_unordered_selectsPRAGMA をオンにする設定項目が追加されています。
イメージとしては、以下のような設定が config.example.yml に含まれるようになったと考えられます(実際のキー名は例ですが、趣旨はこれに相当します):
sqlite:
adapter: sqlite3
database: db/test.sqlite3
variables:
reverse_unordered_selects: trueSQLite の PRAGMA reverse_unordered_selects は、ORDER BY が明示されていない SELECT の結果順序を意図的に「逆順」にすることで、「実装依存のたまたまの順序」に依存しているコードやテストをあぶり出すためのデバッグ向け機能です。
この PR では以下の文脈があります:
- 過去に以下の PR で、
ORDER BY未指定のレコード順序に依存していたことが原因のテストのフレークやバグが修正された: - Byroot 氏のコメント(https://github.com/rails/rails/pull/58267#issuecomment-5113975701)を受けて、「そもそも今後こうした依存が入り込まないよう、テスト環境側で防御した方がよい」という判断になった。
そのため、Rails 本体の Active Record テストスイートを実行するときに、SQLite では原則 reverse_unordered_selects をオンにするような設定例が公式に提供される形になっています。
- 影響範囲・注意点
影響対象
- Rails コアの Active Record テストスイート(特に SQLite を使って実行している部分)
- Rails 本体にコントリビュートする開発者(特に SQLite でテストを走らせる人)
実質的な影響
- この PR 自体は 設定サンプル (
config.example.yml) への追加のみで、本番コードや本番デフォルト設定には手を入れていません。 - ただし、この
exampleを元に実際のconfig.ymlを作っている、あるいは同様の設定を自分の環境に適用した場合、ORDER BY を書いていないテストは予期しない順序で結果が返るため、次のようなテストは壊れやすくなります:Model.all.first/Model.first/Model.lastなどを、順序指定なしで前提にしているテストpluckやmapの結果順に意味を持たせて検証しているが、orderを指定していないテスト
- しかしこれは意図した「防御策」であり、テストが壊れたら、それは本来
ORDER BYを明示すべきだった箇所の発見につながります。
- この PR 自体は 設定サンプル (
注意点
- すべてのテストが SQLite ベースではないため、PostgreSQL 専用テストや SQLite アダプタをスキップしているテストには、この PRAGMA は効きません。したがって「完全な保証」ではなく、「大部分のテストで暗黙順序依存を検出しやすくする」程度の効果です。
- 自身のアプリケーションのテストでも同様の問題をあぶり出したい場合、開発・テスト用の SQLite 接続で
reverse_unordered_selectsをオンにすることを検討できますが、その場合は既存テストが壊れる可能性が高いので、段階的な導入を推奨します。
- 参考情報 (あれば)
SQLite 公式ドキュメント(PRAGMA reverse_unordered_selects)
https://sqlite.org/pragma.html#pragma_reverse_unordered_selects関連 PR:
一般的な指針:
- RDBMS の仕様上、
ORDER BYを指定しない限りSELECTの結果順序は保証されないため、アプリケーション・ライブラリ双方で「順序を期待するときは必ずorderを書く」ことが推奨されます。今回の PR は、その原則を Rails コアのテストレベルで強制するための一歩です。
- RDBMS の仕様上、
#58527 activerecord: make sure SQLite tests do not rely on implicit SELECT order
マージ日: 2026/8/20 | 作成者: @viralpraxis
- 概要 (1-2文で)
このPRは、Active Record の has_many 関連のテストのうち、SQLite で暗黙的なレコードの並び順に依存して失敗していたテストを修正し、データベース実装に依存しない決定的なテストにしたものです。MySQL/Trilogy/SQLite でたまたま成り立っていたインデックス順序への依存を取り除いています。
- 変更内容の詳細
対象テスト:activerecord/test/cases/associations/has_many_associations_test.rbHasManyAssociationsTest#test_calling_first_nth_or_last_on_existing_record_with_build_should_load_association
背景:
- 以前は MySQL, Trilogy, SQLite では
(author_name, title)インデックスに沿ったアクセスパスを取ることで、Topicレコードが「たまたま」特定の順序で返ってきていました。 - PostgreSQL はその順序を保証していなかったため、そもそも挙動が異なっていた。
- https://github.com/rails/rails/pull/58267 のフォローアップとして、この「インデックスに起因する偶然の順序」に依存しているテストを修正しています。
症状(失敗しているテストの例):
--- expected
+++ actual
-#<Topic id: 4, title: "The Fourth Topic of the day", author_name: "Carl", ...>
+#<Topic id: 3, title: "The Third Topic of the day", author_name: "Carl">つまり、first / nth / last などを通じて関連をロードしたときに、テストが「id=4 の Topic が返るはず」と決め打ちしていたが、SQLite ではクエリ実行時の実際の順序が変わり「id=3 が返る」ようになり、テストが失敗していました。
このPRで行っていること(推測を含む技術的意図):
- テストデータの取得順が「インデックスの実装依存」にならないよう、明示的な順序または比較方法に変更。
- 典型的には次のような方向性の変更が行われます:
assert_equal topic_1, association.firstのような「どのレコードが first になるかをレコードIDや生成順に依存して期待している」アサーションを、- 明示的な
orderを指定した関連経由で行う - もしくは「どれが返るかではなく、関連がロードされているか」を確認する形に変える
- 明示的な
- 典型的には次のような方向性の変更が行われます:
- これにより、SQLite でも MySQL / PostgreSQL / Trilogy でも同じテスト結果になるよう、テストを決定的(deterministic)にしています。
行数差分が +7/-7 なので:
- 既存アサーションの期待オブジェクトや期待順序の書き換え
- 必要であれば
order(:id)などの明示的な ORDER BY の追加 first/nth/lastの呼び方や検証の仕方を、小さな範囲で調整している程度です。
- 影響範囲・注意点
- 実際のライブラリコード(ActiveRecord の本体機能)には変更はなく、「テストコードのみ」の修正です。
- そのため Rails 利用アプリケーションの挙動に直接の影響はありません。
- ただし、次のような点に示唆があります:
- 「ORDER BY を指定していない SELECT の結果順」は、DB 実装やインデックス、バージョン、プラン変更で簡単に変わるため、アプリケーション側でもそれに依存すべきではない。
- 例:
Model.where(...).firstを「id が最小のレコード」と思い込まないこと。必要ならorder(:id).firstと明示する。
- 例:
- Rails の公式テストスイートもこのポリシーに揃える方向に継続的に修正されているため、独自のテストを書く際にも「暗黙順序に依存しない」ことが推奨される。
- 「ORDER BY を指定していない SELECT の結果順」は、DB 実装やインデックス、バージョン、プラン変更で簡単に変わるため、アプリケーション側でもそれに依存すべきではない。
CI・開発環境への影響:
- SQLite を使って Rails 本体テストを回している環境で、当該テストの不定期な落ち方が解消されます。
- 他DB(MySQL / PostgreSQL / Trilogy)では主に安定性確保の意味合いで、挙動そのものの変化はありません。
- 参考情報 (あれば)
- フォローアップ元のPR:
- テストケースファイル:
activerecord/test/cases/associations/has_many_associations_test.rb
- 暗黙的な SELECT 順序に関する一般的な注意:
- SQL標準では
ORDER BYを指定しない限り、結果の順序は未定義であり、特定のDBでの「たまたまの動作」は将来保証されない。
- SQL標準では
#58530 Make ActiveModel::Conversion._to_partial_path ractor safe
マージ日: 2026/8/20 | 作成者: @cam-woloshyn
- 概要 (1-2文で)
ActiveModel::Conversion の_to_partial_pathが内部で使うキャッシュの初期化処理を、Ractor に対応できるように変更した PRです。非メイン Ractor から初回アクセスした際に起きていたRactor::IsolationErrorを回避しつつ、既存のキャッシュヒット時のパフォーマンスは維持しています。
- 変更内容の詳細
何が問題だったか
ActiveModel::Conversion には、モデルのパーシャルパスを返す _to_partial_path というメソッドがあります。
class User
include ActiveModel::Conversion
end
User.new.to_partial_path # => "users/user"このパスはクラスインスタンス変数(例: @_to_partial_path)に遅延キャッシュされます。
Ruby 3 以降の Ractor 環境では、
- メイン Ractor 以外から
- 初めてこのキャッシュを書き込もうとしたとき
に、クラスインスタンス変数への書き込みが Ractor 間の分離規則に反し、Ractor::IsolationError が発生するケースがありました。
解決方針
PR では次のような方針で修正されています:
キャッシュ済み (既にパスが計算済み) の場合
- これまで通り、クラスインスタンス変数から直接読み出す「キャッシュヒットの早い経路」を維持。
- 読み取りだけであれば Ractor 的に安全なため、パフォーマンスも従来通り。
まだキャッシュされていない (初回計算) 場合
- 「キャッシュを計算・書き込みする処理」を メイン Ractor に委譲 するように変更。
- 非メイン Ractor からは「メイン Ractor にパーシャルパスを計算+キャッシュさせ、その結果を受け取る」形になるため、
Ractor::IsolationErrorを避けられる。
コードイメージとしては、以下のような流れになっています(※実際のコードから構造を簡略化したイメージ):
def _to_partial_path
if defined?(@_to_partial_path) && @_to_partial_path
# 既にメイン Ractor などで初期化されたキャッシュをそのまま利用
@_to_partial_path
else
# キャッシュ未初期化時はメイン Ractor 経由で初期化
ActiveSupport::Ractor.main_ractor_compute(self) do
# ここでパーシャルパスを計算し、クラスインスタンス変数にセット
@_to_partial_path ||= compute_partial_path
end
end
end※ActiveSupport::Ractor.main_ractor_compute 的な実装名はあくまで説明用の仮名です。
実際のパッチでも「メイン Ractor に計算を委譲し、結果を返す」ための仕組みが導入されています。
テストの追加
activemodel/test/cases/conversion_test.rb に以下のようなテストが追加されています(内容の要約):
- Ractor が利用可能な Ruby 環境でのみ実行。
- 非メイン Ractor 内で、
_to_partial_pathを初めて呼び出す。 - その呼び出しで
Ractor::IsolationErrorが発生しないことを確認。 - 正しいパス文字列が返ってくることも併せて検証。
- 影響範囲・注意点
対象箇所
ActiveModel::Conversion#_to_partial_pathのキャッシュ初期化ロジックのみが対象。- 既存アプリケーションで
_to_partial_path/to_partial_pathを使っている場合も、振る舞い(返される文字列)は従来と同じです。
Ractor を使っていないアプリ
- 変更の影響はほぼありません。
- 既存のキャッシュヒット経路は温存されているため、パフォーマンス劣化も原則ありません。
Ractor を活用している / これから使いたいアプリ
- ActiveModel を Ractor 内で扱いつつ
_to_partial_pathを呼び出す場合、以前はRactor::IsolationErrorに遭遇していた可能性がありますが、この PR によりその問題が解消されます。 - 「キャッシュ未初期化状態での初回アクセス」のみメイン Ractor 経由の処理が挟まるため、ごく限定的なオーバーヘッドが発生する可能性がありますが、通常は気にする必要のない範囲です。
- ActiveModel を Ractor 内で扱いつつ
互換性
- 公開 API のシグネチャ/返り値は変わっていません。
- Ractor 非対応 Ruby(古いバージョン)では特別な副作用はありません。
- 参考情報 (あれば)
- 対象クラス:
ActiveModel::Conversion- 主に
to_partial_path/_to_partial_pathを提供し、ActionViewのパーシャルレンダリングで利用される。
- 主に
- 関連キーワード:
- Ractor,
Ractor::IsolationError, クラスインスタンス変数, 遅延キャッシュ, マルチスレッド / マルチ Ractor 対応。
- Ractor,
#58521 Add temporary name to App's Executor/Reloader
マージ日: 2026/8/20 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
Rails アプリケーションのExecutor/Reloaderクラスに、一時的な「わかりやすい名前」を付けるようにした PR です。これにより、コンソールやデバッガでこれらのクラスを確認する際に、匿名クラスではなく、元アプリケーションに紐づいた名前で表示され、デバッグしやすくなります。
- 変更内容の詳細
何をしているか
Rails アプリケーションが内部的に使っている Executor と Reloader は、これまで「匿名クラス」として生成されていましたが、それらに Ruby 3.3 で追加された Module#set_temporary_name を使って一時名を付与しています。
PR 説明にあるように、変更前後の挙動は以下の通りです。
変更前:
d(dev):001> Rails.app.executor
=> #<Class:0x0000000128412318>変更後:
d(dev):001> Rails.app.executor
=> ActiveSupport::Executor(#<D::Application:0x00000001131b5008>)これにより、Rails.app.executor を見ただけで「ActiveSupport::Executor の、D::Application 用のインスタンスである」という情報が分かります。
コードレベルの変更イメージ
ファイル: railties/lib/rails/application.rb
おおよそ次のような形の変更が入っています(実際のコードは多少異なる可能性がありますが、概念的にはこのイメージです)。
def executor
@executor ||= begin
exec = Class.new(ActiveSupport::Executor)
exec.set_temporary_name("ActiveSupport::Executor(#{self})")
exec
end
end
def reloader
@reloader ||= begin
rel = Class.new(ActiveSupport::Reloader)
rel.set_temporary_name("ActiveSupport::Reloader(#{self})")
rel
end
endポイント:
Class.new(...)で生成していた匿名クラスに対し、set_temporary_nameを呼び出して、人間が読める一時名を付与している。- 名前には、元アプリケーション (
self=MyApp::Applicationなど) を含めているので、どのアプリ用かも判別可能。
テスト追加
ファイル: railties/test/application/configuration_test.rb
ここでは、追加された挙動を検証するテストが 7 行程度追加されています。
テスト内容のイメージ:
Rails.application.executor(およびreloader)を取得。to_sやinspect, またはname相当の情報から、一時名が設定されているかを確認。- おそらく
"ActiveSupport::Executor("やアプリケーション名が含まれているかをアサートしている。
- 影響範囲・注意点
影響範囲
主に「開発・デバッグ体験」に関する変更です。
Rails.app.executorやRails.app.reloaderをrails consoleなどで確認したときに、より意味のある名前が表示されるようになります。- スタックトレースやデバッガでクラス名を確認する際にも、匿名クラスではなく識別しやすい名前として現れる可能性があります。
アプリケーションの実行ロジックやパフォーマンスには、基本的に影響しません。
set_temporary_nameはメタデータ的な名称設定であり、クラスの振る舞いを変えるものではありません。
注意点
Module#set_temporary_nameは Ruby 3.3 で追加されたメソッドですが、Rails がサポートする最低 Ruby バージョンが 3.3.1 なので、「すべてのサポート対象環境で必ず使える」という前提で実装されています。- したがって、通常の Rails のサポート範囲で利用している限り、互換性問題は起きません。
set_temporary_nameはその名の通り「一時的な名前」であり、通常の定数によるクラス名 (MyApp::Executorのような) とは別物です。- つまり、
MyApp::Executorとしてconst_getできるようなものではなく、主にデバッグ表示用の名前です。
- つまり、
- アプリケーションコード側で、
executor/reloaderのclass.name/to_sなどを厳密な文字列比較に使っているような場合(あまり一般的ではないが)、表示文字列の変化がテストの失敗を誘発する可能性はあります。
- 参考情報 (あれば)
- Ruby 3.3 リリースノート:
Module#set_temporary_nameの追加に関する説明
(英語)https://docs.ruby-lang.org/en/master/Module.html#method-i-set_temporary_name - Rails の Executor / Reloader の役割:
ActiveSupport::Executor: スレッド境界でのコンテキスト管理(CurrentAttributes など)を担当。ActiveSupport::Reloader: コードリロードとその前後処理(ミドルウェアのto_run/to_completeコールバックなど)を管理。
この PR は、こうした内部クラスの「中身を追いやすくする」ための純粋に開発者向け DX 改善と捉えてよいです。
#58526 Remove true: / false: options from ImmutableString API doc
マージ日: 2026/8/20 | 作成者: @kamipo
- 概要 (1-2文で)
ActiveModel::Type::ImmutableStringのAPIドキュメントから、true:/false:オプションに関する記述が削除されました。これらは内部実装用であり、パブリックAPIとしてはサポートしない方針が明確化されています。
- 変更内容の詳細
何が変わったか
- 対象ファイル:
activemodel/lib/active_model/type/immutable_string.rb - 行数ベースではコメント(APIドキュメント)の削除が主で、実装ロジックはほぼ/まったく変わっていません。
ImmutableStringの型オプションとしてtrue:/false:を使えるかのように読み取れるドキュメント記述が削除されています。
もともとこれらのオプションは、MySQL系アダプタ(mysql2 / trilogy)に特有の「boolean のシリアライズ非対称性」を扱うための内部用フックでした。
- MySQL系アダプタの boolean シリアライズ:
true→"1"false→"0"
- 通常の ActiveRecord boolean/string 変換:
- 文字列で表現する場合に
"t"/"f"を使うケースがある
- 文字列で表現する場合に
これを、ImmutableMysqlString のような専用クラスを増やさずに、ImmutableString の内部オプションで吸収していたため、true: / false: オプションが導入されていました(#39599)。
しかし本来これはアダプタ実装側のための内部APIであり、アプリ開発者が直接使うことを想定していないものです。
その後 #44306 でドキュメントに載ってしまったため、「公開APIであるかのような誤解を与える」状態になっており、今回のPRでドキュメントから削除されています。
※実コードでの利用例を意図したサンプル(※以下は「やってはいけない例」):
# 本来は想定されていない内部オプション利用
attribute :status, :immutable_string, true: "1", false: "0"このような使い方を「公式にサポートしている」と誤認させないために、APIドキュメントからこの類の記述が削除されています。
- 影響範囲・注意点
実装コードへの影響:
- ほぼドキュメントのみの変更であり、
ImmutableStringの挙動そのものは変わっていません。 - 既存アプリの実行時挙動に影響は基本的にありません。
- ほぼドキュメントのみの変更であり、
公開APIの観点:
true:/false:オプションは「そもそも公開APIではなかった」と公式に位置づけが明示された形です。- もしこれら内部オプションに依存したコードを書いていた場合:
- これまでも非公式・非推奨な利用であり、将来のバージョンで削除される可能性が高い、というシグナルと捉えるべきです。
- 代替としては、boolean から string への変換はアプリケーションレイヤー(カスタム型、callback、value object など)で明示的に行うことが推奨されます。
ドキュメントとの整合性:
- APIドキュメントを見て「
ImmutableStringで boolean シリアライズ形式を変えられる」と思っていた開発者は、今後はその前提を見直す必要があります。 - RBSや型定義、YARD 等の外部ツールは、ドキュメントに依存している場合、このオプションが「ないもの」として扱われるようになります。
- APIドキュメントを見て「
- 参考情報 (あれば)
- 関連PR:
- #39599: MySQL系アダプタの boolean シリアライズ(
"1"/"0")を保つためにtrue:/false:オプションが導入されたPR - #44306:
ImmutableStringにtrue:/false:をAPIドキュメントとして記載してしまったPR
- #39599: MySQL系アダプタの boolean シリアライズ(
- 今回のPRの意図:
- 「内部実装のためだけのオプションを公開API扱いにしない」
- 「将来的にはこれらオプションそのものを削除したい」というメンテナの意思表明
実務的には、「ImmutableString に boolean シリアライズをねじ込むための隠しオプションは使わない」「MySQL固有の表現はアダプタや専用の型クラスに閉じ込める」という設計方針がより明確になった変更と考えられます。
#58525 Deprecate ActiveRecord::Relation#uniq!
マージ日: 2026/8/20 | 作成者: @kamipo
- 概要 (1-2文で)
ActiveRecord::Relation#uniq!が非推奨(deprecate)となり、Rails 9.0 で削除される予定になりました。Rails 7.0 以降はクエリの重複排除が自動化されているため、このメソッドは役割を終えています。
- 変更内容の詳細
背景
- Rails 7.0 で、「複数値をとるクエリメソッド(たとえば
select,group,orderなど)」に対して、同じ値が重複して指定されても自動的に重複を取り除く(deduplication) 挙動がデフォルトになりました。 - その移行パスとして、Rails 7 以前のコードベースで挙動の違いを段階的に慣らすために導入されたのが
Relation#uniq!です(PR #39358)。- 当時は「重複排除を有効にしたいアプリ側が明示的に
uniq!を呼ぶ」ためのメソッドでした。
- 当時は「重複排除を有効にしたいアプリ側が明示的に
- しかし 7.0 以降ではすでに自動で重複排除されるため、
uniq!自体が意味を持たなくなりました。
今回の変更点
主な変更は以下の通りです。
ActiveRecord::Relation#uniq!に非推奨警告を追加uniq!を呼び出すと、将来の削除を知らせる deprecation warning が出るようになります。- 内部的な処理としては、すでに Rails 7.0 以降で自動的に行われている deduplication に頼るだけか、あるいは単に何もしない「no-op」扱いで、機能的な意味はなくなっています。
- 実質的には「呼んでも動作結果は変わらないが、ログに警告が出るメソッド」という位置付けになります。
CHANGELOG の更新
activerecord/CHANGELOG.mdにActiveRecord::Relation#uniq!が非推奨になったこと- Rails 9.0 で削除予定であること
が明記されました。
テストコードの更新
activerecord/test/cases/calculations_test.rbactiverecord/test/cases/relation/merging_test.rb
が修正され、uniq!を前提としたテストや、その挙動まわりの期待値が「非推奨である」ことを前提とした内容に調整されています。- つまり、今後は
uniq!を積極的に使うのではなく、既存利用の後方互換性と警告表示のみを担保するという方針がテストにも反映されています。
イメージ用サンプルコード
# 旧: Rails 6 系で deduplication を試験的に有効化するときに書いていたようなコード
users = User.where(active: true)
users.uniq! # => Rails 9 で削除予定、今は非推奨。呼ぶと deprecation warning
# 新: Rails 7 以降
# 同じ where を複数回チェーンしても Rails が自動的に重複条件をまとめる。
users = User.where(active: true).where(active: true)
# uniq! を呼ばなくても自動で重複が排除される設計- 影響範囲・注意点
- 影響を受けるのは
ActiveRecord::Relation#uniq!を明示的に呼んでいるコードのみ です。- そもそも使っていなければ、機能的な挙動もログも変わりません。
- 今すぐの挙動変化はほぼ無し
- 既に Rails 7.0 以降では deduplication がデフォルトで有効なので、
uniq!の有無によるクエリの「結果の違い」は基本的にありません。 - 今回の PR による目に見える変化は、「呼ぶと deprecation warning が出るようになる」点です。
- 既に Rails 7.0 以降では deduplication がデフォルトで有効なので、
- 今後の対応方針
- Rails 9.0 で
Relation#uniq!は削除される予定です。 - 今のうちにアプリケーションコードから
uniq!呼び出しを削除してください。uniq!呼び出し行をそのまま消すだけで、通常は機能的に問題は発生しない想定です。- 不安な場合は、
uniq!呼び出し前後で生成される SQL や結果件数を比較して regression テストを行うと安全です。
- Rails 9.0 で
uniq(非破壊版)との混同に注意uniq!は relation を破壊的に変更するメソッドでしたが、uniqという API が(もし存在している環境で)どう扱われているかは別の話です。- 実アプリで
uniqを拡張していたり、ActiveRecord 以外の層でuniq相当のものを使っている場合は、今回の PR だけを見て混同しないよう注意してください。
(この PR で非推奨なのはRelation#uniq!だけ です。)
- 参考情報 (あれば)
- 該当 PR:
- Deprecate
ActiveRecord::Relation#uniq!(#58525)
- Deprecate
- 関連 Issue:
- #56338 —
Relation#uniq!の今後の扱いに関する議論をクローズ
- #56338 —
- 元の導入 PR:
- #39358 — Rails 7.0 のデフォルト deduplication 導入時の移行用として
uniq!追加
- #39358 — Rails 7.0 のデフォルト deduplication 導入時の移行用として
- 実務上のガイドライン:
- Rails 7 以降のアプリで
Relation#uniq!を使っている場合は、Rails 9 移行までに全削除する 方針でリファクタリングを進めるとよいです。
- Rails 7 以降のアプリで
#58489 Track fixture connection pools as they are pinned
マージ日: 2026/8/20 | 作成者: @ngan
- 概要 (1-2文で)
ActiveRecord のテスト用フィクスチャで「接続プールのピン止め(pinning)」を行う処理の順序と記録方法を修正し、エラー時に teardown が誤った接続解除を試みて二次的な例外を発生させないようにした PR です。
これにより、実際の障害原因(例: SQLite のdatabase is locked)が素直に報告され、ピン止め状態の取り扱いも正しくなります。
- 変更内容の詳細
背景となる問題
元のコードでは、テスト開始時の setup_transactional_fixtures で「これからピン止めする予定の接続プール一覧」を先に配列に詰め、その後で順次 pin_connection! を呼んでいました:
@fixture_connection_pools = connection_handler.connection_pool_list(:writing)
@fixture_connection_pools.select! { |pool| transactional_tests_for_pool?(pool) }
@fixture_connection_pools.each do |pool|
pool.pin_connection!(lock_threads) # ここで例外が起きても…
pool.lease_connection
end問題点は2つあります。
ピン止めに失敗したプールも
@fixture_connection_poolsに残ってしまうpin_connection!が例外を投げた場合でも、そのプールは配列から取り除かれません。- teardown では
@fixture_connection_pools.map(&:unpin_connection!)を実行するため、「そもそもピンされていない接続」に対してunpin_connection!を呼ぶことになり、RuntimeError: There isn't a pinned connectionが発生していました。
unpin_connection!による “全プールのトランザクション状態チェック” が中断されるrubyunless @fixture_connection_pools.map(&:unpin_connection!).all? @@already_loaded_fixtures.clear endunpin_connection!が途中で例外を投げるとall?まで到達しないため、「どこか別のプールでテスト中にトランザクションがコミット・ロールバックされた」場合に、キャッシュ済みフィクスチャ (@@already_loaded_fixtures) をクリアできないケースがありました。
さらに、テスト実行中に新たな接続プールが作られた場合に、それを pin する通知サブスクライバ側にも順序の問題がありました:
pool.pin_connection!(lock_threads)
pool.lease_connection # ここで例外が出ると、プールは pinned だが未記録
@fixture_connection_pools << poolここでは「ピン止め成功 → lease 取得 → 配列に追加」という順序になっており、lease_connection で例外が起きると「ピン止め済みなのに @fixture_connection_pools に入っていない」プールが生まれます。
そうなるとそのプールは teardown で unpin_connection! されず、永遠に pinned な接続がプール内にぶら下がり続ける状態になります。
pinned なまま残るとどうなるか(PR の説明より)
@pinned_connections_depthがゼロに戻らず、unpin_connection!内の@pinned_connection = nil if @pinned_connections_depth.zero?が実行されない- 結果:
- 接続がプールに戻らない(チェックインされない)
- 外側のトランザクションがプロセス終了まで開きっぱなし
lock_threadも解放されない
PR 中の例:
test 1 pinned, untracked pinned=true depth=1 open_transactions=1
test 2 setup pinned=true depth=2 open_transactions=2
test 2 teardown pinned=true depth=1 open_transactions=1
test 3 setup pinned=true depth=2 open_transactions=2
test 3 teardown pinned=true depth=1 open_transactions=1この PR の修正内容
ピン止めした「後で」プールを記録するように変更
setup_transactional_fixtures側:- 以前は「リストを先に作る → 順に pin する」だったところを、
- 「プールを順に処理し、pin に成功したものだけを
@fixture_connection_poolsに記録する」ように変更しています。
- これにより、
pin_connection!が例外を投げたプールは@fixture_connection_poolsに入らず、teardown がそのプールに対してunpin_connection!を呼ぶことはなくなります。
通知サブスクライバ側も “pin に成功した時点で記録” に変更
- 新しいプールがテスト中に作られた場合も、
pin_connection!成功後ただちに@fixture_connection_poolsに追加し、その後にlease_connectionなどを行う順序に変更。 lease_connectionなどが失敗しても、既にプールは tracking 対象になっているため、teardown で確実にunpin_connection!が呼ばれます。
- 新しいプールがテスト中に作られた場合も、
新しいテストの追加
activerecord/test/cases/test_fixtures_test.rb に、ピンできないプールを実際に用意して挙動を検証するテストが追加されています。
- 前提:
pool: 0+ 短いcheckout_timeoutの接続プールを使い、pin_connection!がConnectionTimeoutErrorを必ず投げるようにする。 - テストが確認する点:
- teardown 時に発生するエラーが
RuntimeError: There isn't a pinned connectionではなく、「本来の障害」であるConnectionTimeoutErrorとして報告されること。
- teardown 時に発生するエラーが
- このテストは sqlite3, trilogy, postgresql の各アダプタで、修正前は失敗し、修正後は通ることが確認されています。
何が「直らない」か (PR が明示している点)
この PR は、以下のような SQLite 固有の BusyException 自体は解決していません:
SQLite3Adapter#configure_connectionで新規接続ごとにDEFAULT_PRAGMASを適用する際、journal_mode: :walへの変更が行われる- DB がまだ WAL でない状態から WAL へ変えるには「排他ロック」が必要
- 他の接続が read lock であっても何らかのロックを保持していると
BusyException(database is locked) になる ReloadModelsTestはActiveSupport::Testing::Isolationにより fork されており、そこでタイミングが悪いとロック競合が起きる可能性がある
今回の変更で、少なくとも「上にかぶさっていたピン/アンピンのエラー」が消えるため、本来の SQLite3::BusyException: database is locked がテスト失敗としてそのまま見えるようになりますが、ロック競合そのものは別問題として残っています。
- 影響範囲・注意点
- 対象: ActiveRecord のテストフィクスチャ (
ActiveRecord::TestFixtures) の内部実装- 本番アプリケーションの接続プール動作ではなく、Rails 内部・アプリのテスト環境におけるトランザクションフィクスチャの振る舞いが対象です。
- 期待される改善:
- トランザクションフィクスチャ利用時に、エラーが発生したセットアップ/teardown のログがより正確になる
- 二次的な
There isn't a pinned connectionが出なくなる - 「本当の原因」(ConnectionTimeoutError や SQLite の BusyException など) がダイレクトに表示される
- 二次的な
- 異常系発生後も、ピン止めされた接続の深さや open transaction 数が正しく戻されるため、以降のテストケースへの副作用(コネクションがプールに返らない、外側トランザクションが張り付き続ける等)が減る
- トランザクションフィクスチャ利用時に、エラーが発生したセットアップ/teardown のログがより正確になる
- 互換性への影響:
- 公開 API ではなくテスト周りの内部挙動の修正であり、通常のアプリケーションコードに対する後方互換性への影響は基本的にありません。
- ただし、「これまで teardown が投げていた『ピンされていない』系の例外に依存したテスト」がもしあれば(ほぼないと思われますが)、エラーメッセージ/エラー種別が変わります。
- 参考情報 (あれば)
- PR 本文で触れられている関連 PR:
#58488(この PR の発端となった CI failure の PR) - 対象ファイル:
activerecord/lib/active_record/test_fixtures.rb- 接続プールのピン/アンピン管理ロジックの順序変更
activerecord/test/cases/test_fixtures_test.rb- ピンできないプールを使った異常系テストの追加
- テスト状況(PR 時点):
- sqlite3: 9,711 tests
- trilogy: 9,861 tests
- postgresql: 10,593 tests
いずれもフルスイートが通過済み。
#58498 Read the table options of many tables at once, and batch the SQLite readers
マージ日: 2026/8/20 | 作成者: @ngan
- 概要 (1-2文で)
table_optionsが複数テーブルを一括で読めるようになり、PostgreSQL/SQLite でテーブルメタデータの読み取りが大幅にバッチ化・高速化された PR です。特に SQLite はカラム・PK・FK・インデックス等の取得をほぼ定数個の SQL にまとめ、スキーマダンプのクエリ数と時間を大きく削減しています。
- 変更内容の詳細
2-1. table_options のインターフェース拡張
SchemaStatements#table_options が単一テーブルだけでなく「テーブル名の配列」を受け取れるようになりました。
# 従来どおり単一テーブル
connection.table_options(:measurements)
# => { options: "PARTITION BY LIST (city_id)" }
# 新しい複数テーブル対応
connection.table_options([:measurements, :posts])
# => {
# "measurements" => { options: "PARTITION BY LIST (city_id)" },
# "posts" => {}
# }戻り値は「テーブル名文字列をキーとするハッシュ」に統一されており、他の whole schema readers (columns, primary_keys など) と同じ形式になりました。
この結果、PostgreSQL では以下のような情報をテーブルごとではなくスキーマ全体でまとめて取るように変更されています。
- テーブルコメント
- 継承元テーブル(
INHERITS) - パーティションキー
今まではテーブルごとに3クエリだったものが、全テーブル分を1クエリで取得するような実装に置き換わっています。
2-2. SQLite: リーダーの本格的なバッチ化
SQLite はすでに「複数テーブル名を受け取る」インターフェース自体は持っていましたが、内部的にはテーブルごとに pragma を発行していました。この PR では、SQLite の pragma が「テーブル値関数(table valued function)」としても利用できる特性を活かし、一度のクエリで複数テーブルの情報を取得する形に全面的に書き換えています。
代表的なクエリ例:
WITH master AS (
SELECT name, type, sql FROM sqlite_master
UNION ALL
SELECT name, type, sql FROM sqlite_temp_master
)
SELECT
m.name AS table_name,
CASE WHEN m.type = 'table' THEN m.sql END AS create_table_sql,
t.*
FROM master m
JOIN pragma_table_xinfo(m.name) t
WHERE m.type IN ('table', 'view') AND m.name IN (...)
ORDER BY m.name, t.cidポイント:
sqlite_master/sqlite_temp_masterをWITH masterでまとめて、そこにpragma_table_xinfo(m.name)を JOINm.sql(CREATE TABLE 文全文)を一緒に流すことで、- COLLATION
- AUTOINCREMENT
- GENERATED COLUMN など、pragma では拾えない情報も解析可能にしている
これにより、以下のリーダーがテーブル数に依存しないほぼ定数個のクエリで済むようになっています。
| リーダー | 変更前のステートメント数 | 変更後 |
|---|---|---|
| columns | 506 | 1 |
| primary keys | 506 | 1 |
| indexes | 435 | 2 |
| foreign keys | 759 | 2 |
| check constraints | 253 | 1 |
primary_key_has_autoincrement? | 440 | 1 |
2-3. スキーマダンプ性能の改善
Active Record のテストスキーマを対象に、PR 前後での計測結果が示されています。
PostgreSQL 17
- テーブル数: 274
- ステートメント数: 836 → 24
- 時間(best of 5): 107.8 → 36.2 ms
SQLite 3.47
- テーブル数: 255
- ステートメント数: 2907 → 12
- 時間: 97.8 → 25.8 ms
MySQL 8.0.46
- テーブル数: 258
- ステートメント数: 266 → 266(変化なし)
- 時間: 65.0 → 60.6 ms(誤差程度)
また、PostgreSQL の「プレーンなテーブル」を大量に作った合成スキーマでの結果:
| テーブル数 | ステートメント数 | 時間 |
|---|---|---|
| 250 | 768 → 19 | 126.3 → 48.5 ms |
| 1000 | 3018 → 19 | 435.5 → 125.7 ms |
PostgreSQL / SQLite では、テーブル数を増やしてもステートメント数が増えない形になっているのがポイントです。
2-4. ダンプ結果の互換性検証
挙動変更による「順序の変化」などを疑われる箇所について、かなり厳密に互換性確認が行われています。
PostgreSQL / SQLite / MySQL 8.0.46 / MariaDB 11.8.8 のスキーマダンプは バイト単位で完全一致
インデックス・FK のダンプはソートされるので、リーダーの返却順が変わっていてもダンプだけでは検出できない
→ そのため、columnsindexesprimary_keysforeign_keyscheck_constraints
について、前実装との結果を
Columnのインスタンス変数まで含めて比較(48,096 行)し、完全一致を確認
2-5. MySQL / MariaDB はテーブル単位読みのまま
MySQL / MariaDB に関しては、SHOW CREATE TABLE がテーブルオプション取得の唯一の信頼できるソースであり、information_schema では代替できないため、テーブル単位での読み取りを維持しています。
理由:
TABLESPACE句の出力有無が「現状」ではなく「テーブルがいつどこで作られ/再構築されたか」に依存しているinnodb_file_per_tableが off の場合でも、OPTIMIZE TABLEの実行有無でSHOW CREATE TABLE出力にTABLESPACE 'innodb_system'が付いたり付かなかったりするinformation_schemaにあるフラグ類からTABLESPACE句の有無を推測しようとすると、多数のテーブルに誤ったTABLESPACE句を付けてしまうケースが確認されている
このため、「精度を犠牲にしたバッチ化」は行わず、あくまで正確な SHOW CREATE TABLE ベースの per-table 読みを継続しています。
2-6. テストとカバレッジ
table_options は WHOLE_SCHEMA_READERS に追加され、以下の性質がテストされています。
- 指定したテーブルだけを読む
- 単体読みと結果が一致する
- テーブル数に比例してクエリ数が増えない
- 空配列を渡すと何も読まない
- スキーマ修飾名を渡した場合、キーは指定した名前に従う
新規テスト(Mutation Testing で発見された穴):
- PostgreSQL:
- 同じベアテーブル名が複数スキーマに存在する場合、
search_pathに従って解決されること - ビューはベーステーブルではないため、コメント付きでも table_options は空になること
- 同じベアテーブル名が複数スキーマに存在する場合、
- SQLite:
- AUTOINCREMENT の付いた主キーは「デフォルトとして」ダンプされ、単なる INTEGER PK はそうならないこと
SQLite3::SchemaDumper#primary_key_has_autoincrement?にテストを新規追加
(これまで全くカバレッジがなく、偽に固定しても 440 行違うダンプが出るのにテストは通ってしまっていた)
意図的に残している mutation(仕様上やむを得ず):
- 二重にパーティションされたテーブルの
INHERITSとパーティションキーの扱いは、既存挙動を維持(厳密固定はしていない) ORDER BY i.seqno/ORDER BY i.inhseqnoを削る mutation は、DB がもともと順序を保証しているため順序が反転しない限り検出不可- PostgreSQL アダプタの
supports_native_partitioning?は true 固定のため、false 側の分岐はテスト不能
実行確認:
- postgresql, trilogy, sqlite3, MariaDB 11.8.8 でフルスイート green
2-7. 削除された内部 API
以下のリーダーは内部からの呼び出しがなくなったため削除されています(:nodoc: で元々非公開扱い)。
inherited_table_namestable_partition_definition
PostgreSQL 側での batched query が両者の情報をまとめて返すようになったためです。
- 影響範囲・注意点
- 公開インターフェースとしては事実上
SchemaStatements#table_optionsの挙動が拡張されただけで、既存コードはそのまま動きます。- 単一テーブル引数の挙動は後方互換です。
- 「メタデータを読むクエリ」の発行パターンは PostgreSQL / SQLite で大きく変わるため、
- DB 側でクエリログを取っている場合は、スキーマダンプ時などのログの形が大きく変わります。
- とはいえ、返されるメタデータの内容は検証済みであり、アプリケーションレベルでの互換性問題は原則発生しません。
- ActiveRecord のスキーマダンプは、DB ごとにバイト単位で前バージョンと一致することが確認されているため、マイグレーションファイルの再生成や
schema.rb/structure.sqlの差分が出ることは基本的にありません。 - MySQL / MariaDB 用アプリケーションでは、
table_optionsの複数テーブル対応は利用できますが、内部クエリ回数の最適化は限定的です。(SHOW CREATE TABLE依存のため) - Rails の内部・プライベート API (
:nodoc:) を直接呼び出していた場合は、inherited_table_names/table_partition_definitionの削除の影響を受ける可能性があります(通常のアプリケーションではほぼ無関係)。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58498
- 関連 PR:
- #58421, #58488, #58494(今回のフォローアップとして位置づけられている)
- 影響ファイル(抜粋):
activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rbactiverecord/lib/active_record/connection_adapters/postgresql/schema_statements.rbactiverecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rbactiverecord/lib/active_record/connection_adapters/sqlite3_adapter.rbactiverecord/lib/active_record/schema_dumper.rb- 各種アダプタテスト (
postgresql/schema_test,sqlite3_adapter_test,schema_statements_testなど)
実務的には、「大規模スキーマでの db:schema:dump / db:structure:dump がかなり速くなる」「PostgreSQL / SQLite でのメタデータ参照がテーブル数にスケールしなくなる」というのが、開発者の体感に直結するポイントです。
#58520 Freeze the route polymorphic cache
マージ日: 2026/8/20 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Rails のルーティングで使われるポリモーフィック URL ヘルパー用のキャッシュを frozen(不変オブジェクト)にし、Ractor 内からlink_to(record)などを安全に呼べるようにした PR です。これにより、Ractor 実行時に発生していた「非共有オブジェクトへのアクセス」エラーを防ぎます。
- 変更内容の詳細
背景・問題点
Ractor は「共有可能なオブジェクト」しか他 Ractor から参照できません。
従来の実装では、ポリモーフィック URL を生成する際のキャッシュが「凍結されていない(mutable な)定数」であったため、サブ Ractor からアクセスすると以下のようなエラーが発生していました:
can not access non-shareable objects in constant
ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder::CACHE by non-main Ractor.このエラーは例えば以下のようなケースで発生します。
Ractor.new do
# ここで link_to(record) が呼ばれ、
# 内部で polymorphic_url が実行されると Ractor エラーになる
ApplicationController.helpers.link_to(record)
endポリモーフィックルートのキャッシュとは
url_for(record) や polymorphic_path(record) では、以下の 3 つのヘルパーメソッド名を組み立てて呼び出しています。
edit_#{resource_name}_pathnew_#{resource_name}_path#{resource_name.to_s.pluralize}_pathなど
たとえば Photo モデルなら:
edit_photo_pathnew_photo_pathphotos_path
これらを毎回文字列結合で作るのはコストがあるため、Rails は内部で CACHE という定数(Hash など)に、リソース名ごとにヘルパー名を格納して再利用しています。
今回の変更
PR タイトル通り、この HelperMethodBuilder::CACHE を freeze しました。
変更点は 1 行だけで、
CACHE = {}.freezeのような形(実際のコードはリンク先参照)で、モジュール読み込み時にキャッシュオブジェクトを凍結して「共有可能オブジェクト」にしています。
説明文にある通り:
This cache may get hit on other other action names that aren't standard, but the resulted value aren't stored, so freezing the cache as soon as the module is loaded is safe.
つまり、
- 標準的な
edit,new, コレクションパス用の 3 種類は「キャッシュされる」が、 - それ以外のカスタムアクション (
publish_photo_pathなど) のときは、結果は計算されるがキャッシュには保存しない
という設計であるため、キャッシュ自体を「読み取り専用」にしても動作は変わらない、という保証を根拠に freeze しています。
テスト追加
actionpack/test/dispatch/routing/custom_url_helpers_test.rb にテストが 15 行追加されています。
ここでは、freeze によってカスタム URL ヘルパーの振る舞いが壊れていないことや、キャッシュの挙動が従来通りであることを検証していると考えられます(Ractor を直接テストしているか、キャッシュ周辺の挙動を確認している可能性が高い)。
- 影響範囲・注意点
Ractor 対応の改善
- サブ Ractor から
link_to(record),url_for(record),polymorphic_path(record)などを呼んでもnon-shareable objectsエラーが出なくなります。 - Ractor ベースの並列処理を行うアプリケーションにとっては実行時エラーが解消される重要な変更です。
- サブ Ractor から
既存アプリへの互換性
CACHEはそもそも「内部キャッシュ」であり、アプリ側が直接書き換える前提の API ではないため、freeze による互換性問題は基本的にありません。- もしメタプログラミングで
ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder::CACHEを直接書き換えていた場合は、今回の変更で例外が出るようになります(ただし、そのような利用は非推奨かつレアケースです)。
パフォーマンス
- キャッシュ戦略自体は従来と同じで、「標準 3 アクションだけキャッシュ、その他は都度計算」のままです。
- freeze したことによるパフォーマンス劣化は、基本的にはありません(Ractor で使えるようになる分、実効スループットは改善します)。
Ractor 対応の観点
- Rails を Ractor セーフにしていく過程の一部であり、今後も同様に「定数やキャッシュを凍結して共有可能にする」変更が増えていくことが予想されます。
- Ractor を積極的に使う予定がある場合、自分で定義しているグローバルなキャッシュやクラスインスタンス変数なども、
freezeやRactor.make_shareableを意識すると良いです。
- 参考情報 (あれば)
Ractor と共有可能オブジェクト
- Ruby 3 以降の Ractor モデルでは、以下のようなものは「共有可能」として扱われます:
- frozen な基本オブジェクト(文字列・配列・ハッシュなど)
- immutable な構造(
freeze済み、かつ中身も共有可能)
- 今回の PR はこのルールに沿って、ポリモーフィックルート用のキャッシュを「共有可能」にした、という位置づけです。
- Ruby 3 以降の Ractor モデルでは、以下のようなものは「共有可能」として扱われます:
#58517 Freeze the Response default_headers at the end of the boot process
マージ日: 2026/8/19 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Rails のブート完了時にActionDispatch::Response.default_headersを freeze(凍結)するようにし、アプリケーション実行中にデフォルトレスポンスヘッダが書き換えられないようにした PR です。
これにより Ractor から安全にデフォルトヘッダへアクセスできるようになり、並列実行時のスレッドセーフ性・予測可能性が向上します。
- 変更内容の詳細(あればサンプルコードも含めて)
何をしているか
変更ファイルは 1 つだけで、actionpack/lib/action_dispatch/railtie.rb 内の 1 行が変更されています。
イメージとしては以下のような変更です(※実際のコードを概念的に表現):
# 変更前(概念)
config.after_initialize do
# Response.default_headers の設定のみ
ActionDispatch::Response.default_headers.merge!(some_headers)
end
# 変更後(概念)
config.after_initialize do
ActionDispatch::Response.default_headers.merge!(some_headers)
# ブート(after_initialize)完了タイミングでデフォルトヘッダを凍結
ActionDispatch::Response.default_headers.freeze
endポイント:
after_initialize(= Rails アプリのブート完了タイミング) でResponse.default_headersに対してfreezeを呼んでいる。- これにより、アプリケーション起動後は
default_headersのハッシュ自体を変更(追加・更新・削除)できなくなる。
Ractor 対応の意図
Ruby の Ractor は「オブジェクト共有の制約」が厳しく、基本的にイミュータブルなオブジェクトや Ractor.shareable? なオブジェクトのみが安全に共有できます。
Response.default_headers は「全リクエストで共有される設定値」なので、本来は起動後に書き換えられるべきではなく、Ractor からも安全に参照したい値です。この PR によって:
default_headersが freeze されたイミュータブルなオブジェクトになる- その結果、Ractor 間で安全に共有・参照できるようになる
という整理がされています。
- 影響範囲・注意点
影響範囲
ActionDispatch::Response.default_headersをアプリケーション起動後に動的に変更していたコードは、FrozenErrorが発生するようになります。
例(NG になるパターン):
# config/initializers/xxx.rb ではなく、リクエスト処理中にこんなことをしているコード
ActionDispatch::Response.default_headers["X-Frame-Options"] = "DENY"上記のように、リクエストハンドラやジョブ内などで default_headers に代入・削除を行っていると、ブート後のタイミングでは FrozenError になります。
一方で、ブートプロセス中 (initializers や config ブロック内) での設定は、after_initialize より前に行われていれば従来通り行えます。
対応方針 / ベストプラクティス
もし実行時にレスポンスヘッダを変えたい場合は、default_headers を書き換えるのではなく、個々のレスポンスに対してヘッダを設定してください。
例:
class ApplicationController < ActionController::Base
before_action :set_custom_headers
private
def set_custom_headers
response.headers["X-Request-Id"] = request.request_id
end
endもしくは Rack ミドルウェア中で:
class CustomHeaderMiddleware
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
headers["X-Custom"] = "value"
[status, headers, body]
end
end注意点のまとめ
- 「アプリケーション起動後に default_headers を書き換える」という設計は今後は NG と考えるべき。
- 起動前(initializer, application.rb, environment 設定ファイルなど)で default_headers を完全に決めておき、起動後は不変とみなす。
- Ractor を使う/使わないに関わらず、「グローバルなデフォルトは起動時に確定し、その後は immutability を保つ」という設計が強制される。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58517
- 関連概念:
- Ruby Ractor / shareable objects:
Ractor.shareable?,freezeによるイミュータブル化 - Rails の
after_initializeフック: アプリケーションのブート処理完了後に呼ばれるフックで、ここで freeze することで「ブート後は不変」という境界を明確化している。
- Ruby Ractor / shareable objects:
#58437 Extract model schema state into SchemaContext
マージ日: 2026/8/19 | 作成者: @adrianna-chang-shopify
- 概要 (1-2文で) Rails の Active Record 各モデルが持っていた「スキーマ依存の状態(カラム情報・型・デフォルト値・ステートメントキャッシュなど)」を、新たに導入された
SchemaContextオブジェクトに集約し、モデルからはそのコンテキストへ委譲する形にリファクタリングした PR です。将来的な複数スキーマ切り替えを見据えつつも、この PR 自体では「1モデル = 1デフォルトコンテキスト」のみを扱います。
- 変更内容の詳細
全体像
- 各 Active Record モデルごとに 1つの
SchemaContextインスタンスを持つようになった - これまでモデルクラス自身が保持・管理していた以下のような情報を
SchemaContextに移動- カラム情報 (
columns) - 属性型 (
attribute_types/ type map) - カラムデフォルト値
- ステートメントキャッシュ
- カラム情報 (
- モデルは「スキーマに依存する状態」については
schema_contextに委譲するだけのシンプルな役割になる
SchemaContext の役割
新ファイル: activerecord/lib/active_record/model_schema/schema_context.rb(+116行)
役割のポイント:
- スキーマ依存状態を一箇所に集約
- 例:
@columns,@columns_hash,@attribute_types,@column_defaults,@statement_cacheなど
- 例:
- モデルからの委譲先
ActiveRecord::Base(および各モデル)はこれらを直接持たず、schema_contextに問い合わせる
Moduleを継承class SchemaContext < Moduleのような形でModuleを継承したオブジェクトとして定義- 理由:
Moduleは Ractor で共有可能(Ractor-shareable)なため、SchemaContextも Ractor 間で共有可能にできる
- ただし、可変なキャッシュの初期化は引き続きメイン Ractor で実行し、Ractor 安全性を担保
イメージコード(実装イメージ・擬似コード):
module ActiveRecord
module ModelSchema
class SchemaContext < Module
def initialize(model)
@model = model
@columns = nil
@attribute_types = nil
# ... その他スキーマ依存状態
end
def columns
@columns ||= load_columns_from_connection
end
def attribute_types
@attribute_types ||= compute_attribute_types_from_columns
end
# ステートメントキャッシュなどもここで管理
end
end
endモデル側の変更点
ActiveRecord::Base / ModelSchema / Attributes 等で、スキーマや属性に関するメソッドが schema_context に委譲されるようになっています。
代表的な変更:
以前は:
rubyclass Post < ApplicationRecord # Post.columns などが Post クラスインスタンス変数を直接参照 end変更後は:
rubyclass Post < ApplicationRecord # デフォルトのスキーマコンテキスト def self.schema_context @schema_context ||= build_schema_context end def self.build_schema_context ModelSchema::SchemaContext.new(self) end # columns などは schema_context への委譲 def self.columns schema_context.columns end end
つまり、モデルクラスのスキーマ情報が「schema_context という1段かませたオブジェクト」にぶら下がる構造になりました。
スキーマコンテキスト「切り替え」について
- 以前の類似PR(#57118)ではスキーマコンテキストを切り替える機構も含んでいましたが、この PR ではコンテキストの切り替え機能は入れていません
- 代わりに、「アプリケーション側で必要であれば
build_schema_contextをオーバーライドして、プロキシを噛ませることで自前で切り替えを実現してほしい」という設計になっています
たとえば、DB マイグレーション中に旧スキーマ・新スキーマを切り替えたいアプリケーションは以下のようなカスタマイズを想定できます(イメージ):
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
def self.build_schema_context
SwitchingSchemaContextProxy.new(super)
end
end
class SwitchingSchemaContextProxy
def initialize(default_context)
@default_context = default_context
@alternate_context = ...
end
def columns
current_context.columns
end
private
def current_context
# 環境変数や Thread local などに応じて切り替え
use_alternate? ? @alternate_context : @default_context
end
endこのように、「フックポイントとしての build_schema_context は提供するが、切り替えロジックの実装までは Rails 本体では持たない」というスタンスです。
ActiveModel / ActiveRecord 周辺ファイルの差分
activemodel/lib/active_model/attribute_registration.rb- 属性登録周りで、モデルクラス上に直接状態を置くのではなく、
SchemaContextに依存するパスへ寄せる調整
- 属性登録周りで、モデルクラス上に直接状態を置くのではなく、
activerecord/lib/active_record/attributes.rb- 属性型の取得などが
schema_context経由になるようリファクタリング(メソッド呼び出し先の変更が中心)
- 属性型の取得などが
activerecord/lib/active_record/core.rb,model_schema.rbschema_contextの生成、デフォルトコンテキストの保持、各種 schema 系メソッドの委譲実装
- テスト:
activerecord/test/cases/attributes_test.rb,core_test.rbにschema_contextベース実装を前提としたテスト追加・調整
- 影響範囲・注意点
影響範囲
- パブリックAPIとしての振る舞いは基本的に変わらないように設計されています。
Model.columns,Model.attribute_types,Model.column_defaultsなどのメソッドシグネチャ・返り値は同じで、内部の保持場所だけが変わったイメージです。
- ただし、以下のようなコードを書いていた場合は影響を受ける可能性があります。
- モデルクラスの内部インスタンス変数(例:
@columns,@attribute_types)を直接参照していたメタプログラミング - Active Record の内部実装に強く依存して拡張していた gem / ライブラリ
- モデルクラスの内部インスタンス変数(例:
注意点
内部構造への依存を避ける
- 今後、さらに「スキーマコンテキストの切り替え」機構が正式に入ることも想定されているため、
SchemaContext自体も進化する可能性が高いです。 - 内部インスタンス変数や内部メソッドに直接アクセスせず、公開されている API(
columns,attribute_typesなど)を通して利用するのが安全です。
- 今後、さらに「スキーマコンテキストの切り替え」機構が正式に入ることも想定されているため、
独自のスキーマキャッシュ拡張をしている場合
- 既存で
ActiveRecord::Base.columnsなどをモンキーパッチ/オーバーライドしていた場合、schema_contextへの委譲経路を考慮した見直しが必要になることがあります。 - 将来的にスキーマ切り替えをしたい場合は、
build_schema_contextをオーバーライドしてプロキシを挟む設計が推奨パターンになります。
- 既存で
Ractor 対応を意識した実装
SchemaContextがModule継承で Ractor-shareable になったのは、マルチ Ractor を意識した設計変更です。- Ractor を使うアプリケーションでは、スキーマ関連の状態が Ractor-safe に共有される方向に進んでいる、と理解するとよいです。
- ただし「キャッシュの初期化そのもの」はメイン Ractor で行うため、初期化タイミングや遅延ロードの動作には従来同様の注意が必要です。
- 参考情報 (あれば)
- 元 PR: https://github.com/rails/rails/pull/57118
- こちらはスキーマコンテキストの切り替え機能も含んでいたが、本 PR では単一コンテキストに絞った形で再設計・リファクタリングされている
- 関連コード:
activerecord/lib/active_record/model_schema/schema_context.rbactiverecord/lib/active_record/model_schema.rb
- 今後予想される発展:
- マルチデータベース / ロールオーバー / スキーママイグレーション期間の「片足ずつ別スキーマ」状態を扱うための公式なスキーマコンテキスト切り替えサポート
- Ractor 対応のさらなる強化(スキーマ情報の共有モデルの明確化)
#58485 Honour all_queries default scopes in internal queries
マージ日: 2026/8/19 | 作成者: @adrianna-chang-shopify
- 概要
このPRは、all_queries: trueが付いた default scope を「内部クエリ」にも適用するための仕組みを導入し、ユニークネスバリデーション、fixture ロード、カウンタキャッシュ更新などで、all_queriesdefault scope が正しく尊重されるようにしたものです。通常の default scope は内部クエリには含めず、all_queries指定のものだけを反映する専用スコープall_queries_scopeが追加されています。
- 変更内容の詳細
新しく導入された all_queries_scope
ActiveRecord::Scoping::Named に、内部的に使うためのクエリスコープ all_queries_scope(およびそれに関連する仕組み)が追加されています。役割は:
適用するもの
default_scope all_queries: true, ...で定義された default scopeModel.scoping(all_queries: true) { ... }などで設定された「現在のスコープ」
適用しないもの
- 通常の default scope(
all_queries: trueが付いていないもの)
- 通常の default scope(
つまり、アプリケーションで「どんなクエリにも必ず乗せたい条件」だけを内部クエリにも反映するためのレイヤーを用意した、という位置づけです。
※ PR の説明文中で触れられている「durable scopes」(別PRで議論中)とは分離されており、この PR は「all_queries だけを扱う最小限の変更」として切り出されています。
all_queries default scope のイメージ
イメージとしては、以下のようなモデルを想定します:
class Post < ApplicationRecord
# 通常の default scope(内部クエリには入らない)
default_scope { where(deleted_at: nil) }
# all_queries: true 付き default scope(内部クエリにも入る)
default_scope all_queries: true do
where(tenant_id: Current.tenant_id)
end
endこのとき、ユニークネスバリデーションなどが内部的に実行するクエリにも tenant_id = Current.tenant_id は含まれますが、deleted_at IS NULL は含まれない、という挙動になります。
ユニークネスバリデーションでの利用
activerecord/lib/active_record/validations/uniqueness.rb が all_queries_scope を使うように変更されています。
これにより、ユニークネスチェック用の内部クエリが以下のように変わります:
変更前:
- 「通常の default scope を(多くの場合)外して」検査
- マルチテナントなどのスコープが効かず、テナントをまたいだ重複検査になりうる
変更後:
all_queries: trueな default scope と、scoping(all_queries: true)による現在スコープだけが適用される- 典型的にはテナント ID など、「どんなクエリにも必ず付けたい制約」がユニークネスチェックにも反映される
サンプルイメージ:
class User < ApplicationRecord
default_scope all_queries: true do
where(account_id: Current.account_id)
end
validates :email, uniqueness: true
endUser の email ユニークネスチェックは、必ず WHERE account_id = ... を含むクエリで行われます。
fixture ロードでの利用
activerecord/lib/active_record/fixtures.rb でも、fixture の挿入に使うクエリに all_queries_scope が適用されます。
- マルチテナントアプリで、
tenant_idをall_queriesdefault scope として持たせている場合、fixture ロード時にもそのスコープが効くようになります。 - ただし通常の default scope は適用されないため、テストが fixture ロードで壊れにくくするバランスを取りつつ、テナントなど「必須の制約」だけは守れるようにした設計です。
カウンタキャッシュでの利用
activerecord/lib/active_record/counter_cache.rb や activerecord/lib/active_record/associations/belongs_to_association.rb で、カウンタキャッシュの更新・リセットに使う内部クエリが all_queries_scope をベースに組み立てられるようになりました。
Topicのposts_countのようなカウンタキャッシュを持つ場合、関連先のスコープ計算にall_queriesdefault scope が効くようになります。- これにより、例えば「テナントごとにカウンタを持つ」ような設計で
all_queriesdefault scope でテナントを絞っている場合、内部的なCOUNTクエリも正しくその条件に従います。
テスト追加ファイル (scoped_counter_cache_topic.rb / scoped_counter_cache_child.rb など) は、まさにこの挙動を検証するためのモデル定義になっています。
内部クエリの構築方法の整理
activerecord/lib/active_record/persistence.rb の内部クエリ構築ロジックも調整され、以前は unscope 等で default scope を明示的に外すだけだった箇所が、all_queries_scope を前提とした組み立て方に変わっています。
これにより、「内部クエリはすべて default scope 無しで生クエリに近い」から「内部クエリは all_queries だけは尊重する」方針へと整理されたといえます。
- 影響範囲・注意点
影響を受ける箇所
この変更により、以下の処理で発行される SQL に all_queries default scope(および scoping(all_queries: true) で設定したスコープ)が含まれるようになります。
- ユニークネスバリデーション (
validates :foo, uniqueness: true) - fixture ロード (
ActiveRecord::FixtureSet経由) - カウンタキャッシュの更新・リセット(
counter_cache: true、touch: trueなど関連周り)
挙動変化の具体的な例
ユニークネスバリデーションが、テナント単位に変わる
これまでは「default scope を外して全テナントを対象に重複チェックしていた」ケースが、
all_queriesを使っている場合は「特定テナントだけ」でチェックされるようになる可能性があります。rubyclass AccountScopedUser < ApplicationRecord default_scope all_queries: true { where(account_id: Current.account_id) } validates :email, uniqueness: true end- 以前:
SELECT * FROM users WHERE email = ?(account_id 条件なし) - 以後:
SELECT * FROM users WHERE account_id = ? AND email = ?
- 以前:
fixture ロード時に
WHERE tenant_id = ...が付くテストで使う fixture データが、
all_queriesdefault scope で絞られた状態で挿入・参照されるようになります。- もし既存テストが「テナントに依存しない前提」で fixture を書いていた場合、テナント関連の default scope 定義との整合性を確認する必要があります。
カウンタキャッシュのカウント対象が変わる
all_queriesdefault scope によって、カウンタキャッシュの対象レコードの範囲が狭まったり変化することがあります。- 例: 「論理削除されたレコードは数えない」が
all_queriesscope として定義されていると、カウンタキャッシュも論理削除を反映した値になります。
- 例: 「論理削除されたレコードは数えない」が
注意点 / マイグレーション時の確認
すでに
default_scope all_queries: trueを導入済みのアプリで Rails をこの変更を含むバージョンに上げる場合:- ユニークネスバリデーションの挙動が変わっていないか(特にマルチテナント)
- fixture のロード・テスト結果が変化していないか
- カウンタキャッシュの値が想定どおりか(差分が出るようなら、一度フルリセットを検討)
まだ
all_queriesを使っていない場合:- この変更自体による挙動変化はほぼなく、将来
all_queriesdefault scope を活用するための基盤が整備された、という位置づけになります。
- この変更自体による挙動変化はほぼなく、将来
- 参考情報
元 PR(durable scopes 系からの切り出し):
https://github.com/rails/rails/pull/57123関連する主な変更ファイル:
activerecord/lib/active_record/scoping/named.rb
→all_queries_scopeの実装activerecord/lib/active_record/validations/uniqueness.rb
→ ユニークネスバリデーションでの利用activerecord/lib/active_record/fixtures.rb
→ fixture ロード時での利用activerecord/lib/active_record/counter_cache.rb/belongs_to_association.rb
→ カウンタキャッシュ等での利用
#58510 Use class_eval for flash types
マージ日: 2026/8/18 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
Rails のflash機能でカスタム flash type 用メソッドを定義する処理が、define_methodからclass_evalに変更され、Ractor(並行実行環境)内でも正しく動作するように修正されました。これにより、メイン Ractor 以外でもadd_flash_typesで定義したメソッドが利用可能になります。
- 変更内容の詳細
背景
Rails では、例えば以下のようにして独自の flash type を追加できます:
class ApplicationController < ActionController::Base
add_flash_types :notice, :alert, :info
end
# すると controller/view 側で
redirect_to root_path, info: "info message"このとき内部的には、:info などの flash type ごとにメソッドを動的定義しています。
従来はこのメソッド定義に define_method ブロックが使われていましたが、「define_method のブロックはメイン Ractor 以外では使えない」という制約に引っかかるケースがありました。
具体的な修正
ActionDispatch::Flash::FlashHash(or 関連クラス)で flash type メソッドを定義している部分が:
# 旧実装イメージ(簡略)
flash_types.each do |flash_type|
define_method(flash_type) do
self[flash_type]
end
endのような define_method ベースの実装から、class_eval を使う実装に変わっています。
イメージとしては、次のようなスタイルです(実際のコードはもう少し細かいですが、方向性として):
flash_types.each do |flash_type|
class_eval <<~RUBY, __FILE__, __LINE__ + 1
def #{flash_type}
self[:#{flash_type}]
end
def #{flash_type}=(value)
self[:#{flash_type}] = value
end
RUBY
endポイント:
define_method+ ブロック →class_eval+ 文字列(あるいは&blockなし版)の形に変更- これにより、Ractor ごとの Proc / ブロック共有などの制約を回避し、メソッドはクラスレベルで静的に定義される
actionpack/test/controller/flash_test.rbにテストが追加され、- カスタム flash type が期待通りに動作すること
- Ractor 環境での動作(少なくとも回帰しないこと)
などがカバーされている形です(+8 行の追加テスト)。
- 影響範囲・注意点
- 一般的な Rails アプリケーションでの挙動は変わりません。
既存のflash[:key]やadd_flash_types :infoで定義されるメソッドの使い方はそのまま利用できます。 - 主な影響は「Ractor を使って Rails コードを並行実行するケース」で、これまでメイン Ractor 以外から
add_flash_typesで定義されたメソッドにアクセスした際に問題になる可能性があった箇所が解消されています。 class_evalによるメソッド定義になったため、- メソッド定義がソースコードとしてクラスに埋め込まれる形になり、
- その分、スタックトレースや
method_source的なツールからはより通常メソッドに近い扱いになります。
ただし、アプリケーションコード側から見える API や挙動は従来通りです。
class_evalの使用により、セキュリティ面での懸念がある場面(動的に任意のユーザ入力を埋め込む等)が考えられますが、本件では定義対象はアプリ側が指定したシンボル(flash type 名)であり、Rails コア側で管理されているため、通常の利用では追加のリスクはほぼありません。
- 参考情報 (あれば)
- Ruby の Ractor 制約:
define_methodに渡すブロック(Proc)は別 Ractor から共有できず、メイン Ractor 外での動的メソッド定義に制約があります。 - Rails
add_flash_typesのドキュメント:
https://api.rubyonrails.org/classes/ActionController/Flash.html - 同様の Ractor 対応で
class_evalへの切り替えが行われている他の箇所(例: Active Support の一部)と同じ方向性の変更です。
#58509 Compute ViewPaths#_prefixes on eager_load!
マージ日: 2026/8/18 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
ActionView::ViewPathsの_prefixesをeager_load!時に事前計算するように変更し、Ractor 内からでも安全かつ効率的に利用できるようにした PR です。これにより、Ractor 環境での ActionView 利用時のスレッド/メモリ共有まわりの問題を回避する狙いがあります。
- 変更内容の詳細
※ 実際の diff は 6 行追加・1 行削除のみの小規模変更です。ファイル:actionview/lib/action_view/view_paths.rb
概ね次のようなことをしていると考えられます(擬似コードイメージ):
module ActionView
class ViewPaths
def eager_load!
super # 既存の eager_load! の処理
# ここで _prefixes を事前計算しておく
_prefixes
end
def _prefixes
# ここは既存メソッド
@prefixes ||= compute_prefixes
end
end
endポイント:
- これまで
_prefixesは最初にビュー解決が走ったタイミングで「遅延計算 (lazy)」されていた。 - 今回の変更で、
eager_load!を呼んだタイミングで_prefixesを一度評価してキャッシュするようになった。 - コメントにある “To enable using it within a Ractor” から、Ractor 内部で
_prefixesを使う際に、- Ractor開始後にメモ化が走る
- 共有オブジェクトの書き換えが発生する
などの問題(Ractor の不変性やコピーコスト、Ractor セーフティ)を避ける意図があると考えられます。
つまり「Ractor を起動する前に eager_load! を呼んでおけば、_prefixes に関するメモ化はすべてメイン Ractor 側で終わる」ようにし、Ractor からは「すでに確定済みの値」を読むだけにする設計変更です。
- 影響範囲・注意点
影響範囲:
ActionView::ViewPathsを利用している部分(主に Rails のビュー解決ロジック)。Rails.application.eager_load!などでeager_load!を呼び出す環境全般(本番環境など)。- Ractor を利用している/利用しようとしている Rails アプリケーション。
考えられる副作用・注意点:
_prefixesの計算タイミングの前倒し- これまでは「初回アクセスまで遅延されていた計算」が、
eager_load!時に発生するようになるため、起動時コストがごくわずかに増える可能性があります。 - ただし
_prefixes自体は比較的軽い処理であり、実務上はほぼ影響ないレベルと考えられます。
- これまでは「初回アクセスまで遅延されていた計算」が、
- Ractor を使わない通常のアプリでは挙動上の違いはほとんどありません。
- 外部 API は変わらず、
_prefixesの内容自体も従来と同じです。
- 外部 API は変わらず、
- Ractor を使う場合は、
- 「Ractor を起動する前に
eager_load!を確実に実行しておく」ことで、_prefixesを含む ViewPaths 周りの初期化をメイン Ractor に閉じ込めることができます。 - これは「Ractor 開始後にグローバル状態を書き換えない」という Ractor セーフティの原則に沿った運用になります。
- 「Ractor を起動する前に
- 参考情報 (あれば)
- Rails ガイド:
- 「クラスの自動読み込みと再読み込み」(eager_load の背景): https://guides.rubyonrails.org/autoloading_and_reloading_constants.html
- Ractor の仕様 (Ruby 本体ドキュメント):
- ActionView のパス解決の仕組みイメージ:
_prefixesはコントローラ名・名前空間などから「どのディレクトリ階層をビュー探索対象とするか」を決めるための内部配列で、テンプレート解決性能に影響するキャッシュです。
#58402 Return a UTC time from Time.rfc3339 for "Z" inputs
マージ日: 2026/8/18 | 作成者: @yahonda
- 概要 (1-2文で)
Time.rfc3339に"Z"(UTC 指定)の文字列を渡したとき、Active Support が「固定オフセット +00:00」ではなく「UTC 時刻(utc? == true)」を返すように変更し、今後 Ruby 4.1 以降に標準で入るTime.rfc3339の仕様に揃えた PRです。これにより時間値そのものは変わらず、主にシリアライズ時の表記が"+00:00"から"Z"に変わる可能性があります。
- 変更内容の詳細
Ruby 本体との整合性を取る方針
- Ruby 4.1 では
timegem がデフォルトで入り、Time.rfc3339が提供されます。 - Ruby 本体の
Time.rfc3339は"Z"(UTC デザインator)を「UTC 時刻」として扱います(utc? #=> true)。 - 一方、Rails/Active Support は Rails 5.1 以降、
Time.rfc3339("...Z")に対して「+00:00 の固定オフセット」のTimeを返していました(utc? #=> false)。- 値としては同じ時刻ですが、「UTC フラグが立っているか」「表示/シリアライズ時の表記」が異なる実装でした。
- 今回の PR では、Ruby 本体の挙動に合わせて Active Support 側も
"Z"に対して UTC 時刻を返すように変更し、将来 Ruby 4.1 以降でTime.rfc3339が標準提供されたときに挙動が一致するようにしています。
Time.rfc3339 の定義戦略
PR のポイントは以下です。
Ruby が
Time.rfc3339を提供している場合- Active Support は 再定義しない。
- そのまま Ruby 本体の
Time.rfc3339を利用します。
Ruby が
Time.rfc3339を提供していない場合(現行の全リリース版 Ruby)- Active Support が
Time.rfc3339を定義する。 - その実装を
"Z"のときに「UTC 時刻」を返すように修正し、Ruby 本体の将来的な実装と揃えます。
- Active Support が
この存在チェックは Range#overlap? の対応と同じパターンで、require "time" を先に呼び出してから Time.respond_to?(:rfc3339) のようなかたちで行われます。
これにより、require の順番によって挙動が変わる、という事態を避けています。
"Z" の扱いの具体的な変更
挙動の違いは次の通りです。
Time.rfc3339("2026-08-07T10:00:00Z")
# 変更前: 2026-08-07 10:00:00 +0000 (utc? => false)
# 変更後: 2026-08-07 10:00:00 UTC (utc? => true)
# 以前の表現を得たい場合
Time.rfc3339("2026-08-07T10:00:00Z").getlocal("+00:00")
# => 2026-08-07 10:00:00 +0000 (utc? => false)- 時刻の「値」としては同じ(Unix time 等は変わらない)が、
- 変更前: 「ローカルタイムゾーンが +00:00 の固定オフセット時間」
- 変更後: 「UTC とマークされた時間」
- というふうに、
Timeオブジェクトに紐づくタイムゾーン情報が変わる点が本質的な変更です。
例外メッセージの扱い
Ruby 本体の Time.rfc3339 と Active Support 実装では、パース失敗時の例外メッセージが異なります。
- Ruby:
"invalid rfc3339 format" - Active Support:
"invalid date"
将来的に Ruby 実装をそのまま利用することを考慮し、テストでは例外クラスを固定しつつ、メッセージは assert_match に変更して文言の違いを許容しています。
CHANGELOG・テスト修正
activesupport/CHANGELOG.mdに"Z"の挙動変更が記載され、フレームワークデフォルトで切り替えるのではなく、直接挙動を変える Breaking change として扱っています。activesupport/test/core_ext/time_ext_test.rbは:"Z"の時にutc?がtrueであることを確認するテストに調整。- 例外メッセージのアサーションを
assert_matchに緩和。
- 影響範囲・注意点
影響する環境と挙動
| 環境 | Time.rfc3339 の定義元 | "Z" 入力時の結果 |
|---|---|---|
現行の全リリース Ruby(Time.rfc3339 なし) | Active Support | UTC time (utc? が true) |
Ruby 4.1 以降想定(Time.rfc3339 あり) | Ruby 本体 | UTC time (utc? が true)、再定義なし |
つまり、どの環境でも最終的に "Z" からは UTC 時刻が返るという挙動に統一されます。
互換性上の注意点
utc?やzoneに依存しているコード"Z"をパースした結果に対して、utc?やtime.zoneなどをチェックしているコードがあると、挙動が変わる可能性があります。- 例えば、以前は
utc? #=> falseだったのでutc?を条件分岐に用いていた場合、今回の変更でtrueになるため、処理が変わります。
- 例えば、以前は
シリアライズ結果(JSON 等)の違い
as_json/to_jsonなどで UTC 時刻を出力する場合、"2026-08-07T10:00:00+00:00"ではなく"2026-08-07T10:00:00Z"という表記になる可能性があります。"Z"と"+00:00"のどちらも RFC 3339 としては同値ですが、文字列比較をしている外部サービス/テスト等があると差分になる可能性があります。
例外メッセージの違い
rescueで例外メッセージ文字列を直接マッチしているようなコード(あまり望ましくないが存在しがち)があると、Ruby 実装と Active Support 実装でメッセージが異なる点に注意が必要です。- 推奨は、例外クラスや、自前のラッパーを使ったハンドリングです。
タイムゾーン変換との関係
Time.rfc3339("...Z")の結果をin_time_zone/Time.zone.rfc3339等で扱うときの変換結果の時刻自体は変わりません。- 変更はあくまで「UTC フラグとシリアライズ表現」のレベルです。
- 参考情報
- 対応 PR: https://github.com/rails/rails/pull/58402
- 以前の議論・関連 PR:
- #58172(今回の PR により supersede された)
- Ruby time gem 側 issue / PR: ruby/time#72, #73, #74, #75, #77, #79, #80 など
- Ruby の
Time.iso8601/Time.xmlschemaも"Z"を UTC 時刻として返しており、本 PR によってTime.rfc3339も同様の一貫した挙動になります。
#57625 Add backticks to execute method name in Active Record Migrations guide [ci skip]
マージ日: 2026/8/18 | 作成者: @VladNegara
概要 (1-2文で)
Active Record Migrations ガイド内で、executeメソッド名の表記がコードフォントになっていなかった箇所にバッククォートが追加され、他のメソッド表記と統一されました。アプリケーションコードには一切影響せず、ドキュメント上の表記ゆれを修正するものです。変更内容の詳細(あればサンプルコードも含めて)
対象ファイル:
guides/source/active_record_migrations.md変更内容は 1 行のみで、テキスト中の
executeメソッド名にバッククォートを付けたものです。例(イメージ):
diffYou can also use the execute method to run arbitrary SQL.
- You can also use the
executemethod to run arbitrary SQL.これにより、ガイド中でメソッド名が他の `change`, `up`, `down` などと同じくコードフォントで表示され、Markdown 的にも正しい書式になります。
- 影響範囲・注意点
- 影響範囲は Rails ガイド(ドキュメント)の表示のみです。
- Rails 本体の挙動や API 仕様、マイグレーションの書き方そのものには一切変更はありません。
- 既存コード・既存マイグレーションを修正する必要はありません。
- CI スキップ (
[ci skip]) 指定の通り、テスト実行不要な純ドキュメント変更です。
- 参考情報 (あれば)
- 対象ガイド: Active Record Migrations ガイド
- 一般的な利用例(参考・既存仕様のおさらい):ruby上記のように、
class AddIndexToUsersEmail < ActiveRecord::Migration[7.2] def up execute <<~SQL CREATE INDEX index_users_on_email ON users (email); SQL end def down execute <<~SQL DROP INDEX index_users_on_email; SQL end endexecuteはマイグレーション内で任意の SQL を直接実行したい場合に使用するメソッドであり、その説明部分のメソッド名表記が今回の PR で整えられています。
#58507 Remove trailing whitespace from Action Controller guide
マージ日: 2026/8/18 | 作成者: @55728
- 概要 (1-2文で)
Action Controller ガイド(guides/source/action_controller_overview.md)内の行末の余分な半角スペースを 2 箇所削除し、guides:lint:mdlの Markdown Lint (MD009 Trailing spaces) エラーでmainブランチの CI が落ちていた問題を解消する PRです。
内容テキスト自体の変更はなく、レンダリング結果も変わらないホワイトスペースのみの修正です。
- 変更内容の詳細
対象ファイル:
guides/source/action_controller_overview.md
変更内容:
- 行末に 1 つだけ含まれていた余分な半角スペースを 2 行分削除。
具体的には、#58229 で追加された 2 行に、Markdown Lint(
mdl)がMD009 Trailing spacesとして検出する行末スペースが残っていました。bash$ cd guides && mdl $(ls source/*.md | grep -v '_release_notes.md') source/action_controller_overview.md:1141: MD009 Trailing spaces source/action_controller_overview.md:1150: MD009 Trailing spaces技術的なポイント:
MD009 Trailing spacesは、Markdown の各行末に不要なスペースがあるとエラーにするルールです。- Markdown では「2 個以上の行末スペース」は改行(hard line break)として意味を持ちますが、 この PR で削除したのは「1 個だけ」のスペースのため、意味的にはノイズであり、レンダリング結果には影響しません。
- そのため、この PR は「lint 修正のみ」であり内容変更ではありません。
- 影響範囲・注意点
CI / ビルドへの影響:
rake guides:lint:mdlがすべてのガイドファイル(guides/source/*.md、release notes 除外)に対して実行されますが、 本 PR 適用後は違反が 0 件となり、buildkite/rails上の guides lint ステップが通るようになります。- これにより、
mainが常に赤くなる状態が解消され、他の PR の CI も正常に状態確認できるようになります。
動作 / 文書内容への影響:
- Web 上に公開されるガイドの表示内容は変わりません。
- Rail アプリケーションの動作や API 仕様など、コード・ランタイムには一切影響しません。
注意点:
- [ci skip] タイトル付きの PR (#58229) で lint failure が見落とされた、という背景があります。
- 「CI を全部スキップしたいわけではないが、不要なジョブだけスキップしたい」ような場合は、 ジョブごとの条件やワークフローレベルでの制御を検討する価値があります。
- 少なくとも guides のようなスタイル系チェックは、人間が見落としやすいので CI では走らせ続けるのが有用です。
- [ci skip] タイトル付きの PR (#58229) で lint failure が見落とされた、という背景があります。
- 参考情報 (あれば)
- 関連 PR:
- #58229: 今回の trailing space を含む行を導入した PR(マージハッシュ: 65cfb20082)
- Lint ルール:
MD009 - Trailing spaces: Markdown Lint のルールで、行末スペースを禁止(ただし、2 個以上は意図的な hard line break の可能性を考慮)
- CI / コマンド:
- Guides Lint 実行例:bash
cd guides mdl $(ls source/*.md | grep -v '_release_notes.md') - Rails の guides lint タスク:bash
bundle exec rake guides:lint:mdl
- Guides Lint 実行例:
#58229 [ci skip] Fix explanation of controller callbacks with blocks
マージ日: 2026/8/18 | 作成者: @flavio-b
- 概要 (1-2文で)
このPRは、Railsガイド中の「ブロック付きのコントローラコールバックはコントローラインスタンスのスコープで動かない」という説明が実際の挙動と逆であるため、それを修正したドキュメント更新です。実装は変更せず、現状のinstance_execによる挙動(コントローラインスタンス文脈で評価される)に説明を合わせています。
- 変更内容の詳細
- 対象ファイル:
guides/source/action_controller_overview.md - 主な修正点:
- 「ブロックを渡したアクションコールバックはコントローラのスコープで実行されない」という、古い実装(Rails 2.3 時代)に基づいた説明を削除・修正。
- 現在の挙動どおり、「ブロックはコントローラのインスタンスコンテキストで実行される」ことが分かるようにガイドを更新。
PR本文の例:
before_action do |controller|
puts self.equal?(controller) # => true
endこのコードから分かる通り、ブロック内の self は controller 引数と同一であり、コールバックブロックはコントローラのインスタンスコンテキスト(= self がそのコントローラ)で実行されています。
背景として説明されている変遷(実装はPRには含まれないが理解に有用):
- Rails 2.3: ブロックコールバックは
controller引数に依存した形で扱われていた。 - Rails 3: コントローラクラス上にメソッドを定義する方式に変更。
- Rails 4.1:
instance_execによる実行に変更(現在も継続)。
このinstance_execによって、ブロックのselfはコントローラのインスタンスになる。
今回のPRでは、この履歴に沿って「コントローラコールバックだけがモデルコールバックと違う」という誤った印象を与える表現を修正し、モデルコールバック同様に「ブロックはインスタンスコンテキストで実行される」ことを前提とした説明に揃えています。
- 影響範囲・注意点
- 影響範囲:
- コードの挙動は一切変わらず、ドキュメントのみの修正です。
- 既存アプリケーションの挙動には影響ありません。
- 注意点:
これまで古いガイドを信じて「ブロック内の
selfはコントローラではないはず」と思っていた場合、その前提は誤りです。現在のRails(少なくとも 4.1 以降)では、以下のような書き方が正しく動作します:
rubybefore_action do # self はコントローラのインスタンス logger.info "Current path: #{request.fullpath}" @user = current_user end逆に、「
|controller|引数を必ず使わなければならない」と考えて設計している場合、その前提も不要です。selfをそのまま使って問題ありません(controller引数は互換性のため残っているだけの位置付けと捉えられます)。
- 参考情報 (あれば)
- コールバック実装の変更の流れ(概要):
- Rails 2.3: ブロックに
controllerを引数として渡していた設計。 - Rails 3: コントローラにメソッドを定義して実行する方式へ。
- Rails 4.1:
instance_execを用いて、ブロックをインスタンスコンテキストで評価する方式に変更。
- Rails 2.3: ブロックに
- 関連ドキュメント:
- Rails Guides: Action Controller Overview(このPRで修正された節)
- Active Record callbacks のドキュメント(モデルコールバックがインスタンスコンテキストで動くことの対比として有用)
#58447 Fix grammar errors in Sign Up and Settings guide [ci skip]
マージ日: 2026/8/18 | 作成者: @eglitobias
概要 (1-2文で)
Sign Up and Settings ガイド (guides/source/sign_up_and_settings.md) に含まれていた英文の文法ミスを修正したドキュメント専用のPRです。アプリケーションの挙動やコードには一切変更はありません。変更内容の詳細
- 対象ファイル:
guides/source/sign_up_and_settings.md - 修正内容:
- 英文の文法的におかしい箇所を6文、読みやすく自然な英語表現に修正
- 追加行 8 / 削除行 8 なので、文の差し替えレベルの変更に留まる
- 主な変更パターンとして想定されるもの:
- 動詞の時制や主語・動詞の一致の修正
- 冠詞 (a / an / the) の抜けや誤用の修正
- 前置詞 (in / on / at など) の不自然さの修正
- 不自然な語順や冗長表現の簡潔化
実際のコード例やAPIの変更はなく、あくまでガイド本文の英文修正のみです。
- 影響範囲・注意点
- 実行コードやテスト、設定ファイルなどには一切変更なし
- 挙動変更なし
- 互換性への影響なし
- ビルド・デプロイ手順への影響なし
- 対象はガイド文書だけなので、Rails アプリケーションを利用しているプロジェクト側で対応すべき作業はありません。
- 英語ドキュメントを参照しているユーザーにとって、Sign Up and Settings ガイドの可読性と理解しやすさが向上します。
[ci skip]が付いているため、このPR自身のマージ時にはCIは実行されていません(ドキュメントのみの変更であることを示唆)。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58447
- 対象ガイド:
guides/source/sign_up_and_settings.md- Sign Up and Settings 機能の利用手順・設定手順を説明する公式ガイドで、ユーザー登録まわりの設定を理解する際に参照される文書です。
#58502 Simplify transaction instrumentation tests with NotificationAssertions helpers
マージ日: 2026/8/18 | 作成者: @Saidbek
- 概要 (1-2文で)
Active Record のトランザクション計測(instrumentation)テストで、手動のActiveSupport::Notifications.subscribe/unsubscribeをやめ、NotificationAssertionsヘルパーに置き換えたテスト専用のリファクタリングです。アプリケーションの実行時挙動には一切変更がありません。
- 変更内容の詳細
背景
- #53822 で導入された
NotificationAssertionsテストヘルパーを、まだ置き換えられていなかったactiverecordのトランザクション関連テストに適用したフォローアップ PRです。 - 対象は
activerecord/test/cases/transaction_instrumentation_test.rbのみで、変更行数からも分かる通り、主にテストの簡素化・整理が目的です(+102 / -208 と削除が多い)。
具体的な置き換え内容
これまでのテストパターン(概念的な例):
events = []
subscriber = ActiveSupport::Notifications.subscribe("transaction.active_record") do |*args|
events << ActiveSupport::Notifications::Event.new(*args)
end
do_something_in_transaction
ActiveSupport::Notifications.unsubscribe(subscriber)
assert_equal 1, events.size
assert_equal "transaction.active_record", events.first.nameこういったコードを NotificationAssertions ヘルパーに置き換えています。使用されている主なヘルパー:
assert_notification(name, payload: {}, &block)- 指定のイベントが1回発火し、その内容を検証したいケース
assert_notifications_count(name, count, &block)- 特定イベントが何回発火したかのみ検証したいケース
assert_no_notifications(name, &block)- 特定イベントが発火しないことを確認したいケース
capture_notifications(name, &block) { ... }- ブロック実行中の指定イベント一覧を配列で受け取って、後から詳細を検証したいケース
イメージとしては、以下のような書き換えが行われています:
# 以前: 手動 subscribe / unsubscribe
events = []
subscriber = ActiveSupport::Notifications.subscribe("transaction.active_record") do |*args|
events << ActiveSupport::Notifications::Event.new(*args)
end
run_transactional_code
ActiveSupport::Notifications.unsubscribe(subscriber)
assert_equal 1, events.size
# 以後: ヘルパー利用
events = capture_notifications("transaction.active_record") do
run_transactional_code
end
assert_equal 1, events.sizeまたは、もっと簡潔に:
assert_notifications_count "transaction.active_record", 1 do
run_transactional_code
endヘルパーに置き換えなかったケース
ActiveSupport::Notifications.subscribe をあえて残しているテストも明示されています:
start_transactionテストで、トランザクション途中の状態をアサートするものNotificationAssertionsは基本的に「ブロック実行後に通知を検証する」前提のため、トランザクションの途中経過(mid-transaction)を逐次検証するようなテストでは意味が変わってしまう。
after_commit/after_rollbackの発火順(ordering)を検証するテスト- イベントの順序やタイミングを細かく見たいテストは、通知のサブスクライブをより低レベルに制御できる従来の方法を維持。
- 「壊れた(broken)サブスクリプション」テスト
- サブスクライバ自体が例外を投げるケースを扱うテストで、ヘルパーに抽象化してしまうと意図が変わる/見えづらくなるため、素の subscribe / unsubscribe を維持。
- 影響範囲・注意点
影響範囲
- 変更は
activerecord/test/cases/transaction_instrumentation_test.rbのみで、本番コードには一切手を入れていません。 - Active Record のトランザクションに関する
ActiveSupport::Notificationsの動作仕様は変更されていません。 - gem を利用する側(アプリケーション/ライブラリ)に対する互換性の影響はありません。
- 変更は
注意点
- 今後、トランザクションの計測イベント周りのテストを追加/修正する場合、基本は
NotificationAssertionsを優先的に使うのが推奨される流れです。 - ただし、イベントの「途中経過」「発火順」「サブスクライバの例外挙動」といった、時間軸や低レベルな挙動をテストしたい場合は、これまで通り
ActiveSupport::Notifications.subscribeを直接使うべきケースも残る、という設計方針が明確になっています。
- 今後、トランザクションの計測イベント周りのテストを追加/修正する場合、基本は
- 参考情報 (あれば)
- 関連 PR:
- #53822 —
NotificationAssertionsヘルパー導入の元 PR
- #53822 —
- 該当テストファイル:
activerecord/test/cases/transaction_instrumentation_test.rb- トランザクション開始・コミット・ロールバックなどに対する ActiveSupport::Notifications のイベント発火を検証するテスト群が、より読みやすく・短くなっています。
#58484 Fix clearing PK-overlapping association keys when assigning nil
マージ日: 2026/8/18 | 作成者: @matthewd
- 概要 (1-2文で)
belongs_to/has_oneにnilを代入したときの外部キークリア処理で、「外部キーが参照元モデルの主キーと完全に重なっている場合」の挙動を修正した PR です。特に単一カラムFKやポリモーフィックbelongs_toで、外部キーが正しくNULLにされない/type だけ消えるといった不整合な挙動を修正しています。
- 変更内容の詳細
背景となる仕様と問題
- Rails では、単一系 (
belongs_to/has_one) にnilを代入すると、対応する外部キー列をNULLにするのが基本挙動。 - ただし、外部キー列が参照元モデルの 主キー (PK) にも含まれている場合、
- それは sharding 用の共通キーなどであるケースが多いので、
- 「PK に含まれている FK 列はクリアしない」という特別扱い (#55332) が入っている。
- このロジックの問題点:
- FK のすべてのカラムが PK と重なっている(=「完全に PK とオーバーラップしている」)場合でも、一切 FK をクリアしない ため、「
association = nilしても実質何も起きない」状態になっていた。 - #58368 でロジックが統一された影響で、単一カラム FK が PK の一部/全部になっている場合など、以前より広いケースにバグが波及した。
- ポリモーフィック
belongs_toでは…_typeだけクリアされ、対応する…_idがそのまま残るという不整合も発生していた。
- FK のすべてのカラムが PK と重なっている(=「完全に PK とオーバーラップしている」)場合でも、一切 FK をクリアしない ため、「
この PR の修正方針
説明文を元にすると、今回のルールは以下のように整理できます:
- 「PK と重なっている FK は残す」という特別ルールは、
FK が複数カラムから構成されていて、かつ PK と重なっていない別の FK カラムをクリアできる場合にのみ適用する。 - 逆に言うと、
- FK のすべてのカラムが PK に含まれている(= 完全オーバーラップ)の場合は、
- その外部キーも 通常通りクリア対象とする(
nil代入を有効にする)。
- その外部キーも 通常通りクリア対象とする(
- FK のすべてのカラムが PK に含まれている(= 完全オーバーラップ)の場合は、
- ポリモーフィック
belongs_toに対しても、xxx_typeとxxx_idを「ペア」として扱い、- 一方だけがクリアされることがないよう、常にセットで処理するように修正。
コードレベルの変更ポイント(推測含む)
※PR本文とファイル差分の概要から、意図と粒度を絞って整理しています。
belongs_to_association.rb/has_one_association.rb- 関連に
nilを代入したときの外部キークリア処理で、- 外部キー列のうち「参照元の PK に含まれる列」を特別扱いするロジックに、
- 「少なくとも1つは非PKな FK カラムが存在するか?」という条件分岐を追加。
- これにより、
- FK が
[shard_id, user_id]で、shard_idが PK 由来、user_idは非PK →nil代入時にはuser_idのみNULLにし、shard_idは残す(従来意図どおり)。 - FK が
[account_id]だけで、account_idが PK でもある →nil代入時にはaccount_idもNULLにする(従来は何も変わらなかった)。
- FK が
- 関連に
- ポリモーフィック
belongs_to- クリア対象外/対象カラムの判定時に、
*_typeと*_idを個別に扱っていた部分を修正し、 - 両方とも同じルール(PK オーバーラップ判定も含む)で一貫して処理するように変更。
- クリア対象外/対象カラムの判定時に、
テストの追加内容
belongs_to_associations_test.rb- 「外部キーが完全に PK とオーバーラップしている」ケースで、
association = nilしたときに、外部キーがきちんとNULLになることを確認するテストを追加。
- ポリモーフィック
belongs_toについて、polymorphic_association = nilで*_typeと*_idが両方とも正しくクリアされることを確認。
- 「外部キーが完全に PK とオーバーラップしている」ケースで、
has_one_associations_test.rbhas_one側でも同様に、「完全に PK と重なっている FK」や「複合FK + PK オーバーラップ」のパターンをカバーするテストを追加。
- 影響範囲・注意点
- 影響するケース
belongs_to/has_oneの関連にnilを代入したときの挙動で、- 関連先を識別する外部キーが、
- 参照元モデルの PK にも含まれている、あるいは
- PK と完全に一致している
- という構成になっているモデル。
- 関連先を識別する外部キーが、
- ポリモーフィック
belongs_toを使っていて、*_type/*_idのどちらか片方だけが残ってしまっていたようなケース。
- 変わる挙動
- 「FK が完全に PK と重なっている」場合でも、
association = nilによって外部キーがNULLにされるようになります。- 以前: 何も更新されず、「nil を代入しても関連が切れない」状態になりうる。
- 以後: 外部キーがクリアされ、関連が正しく切れる。
- ポリモーフィック関連では、
typeとidが必ずセットでクリアされるようになり、片方だけ残る不整合が解消されます。
- 「FK が完全に PK と重なっている」場合でも、
- 注意点
- もし「FK=PK の状態で、
nil代入してもレコード上のカラムは変えたくない」という前提でアプリケーションロジックを書いていた場合は、挙動が変わる可能性があります。- そうしたケースは設計上かなり特殊ですが、シャーディングや複合PKを多用するアプリでは、該当するモデルがないか確認した方が安全です。
- 外部キーをシャードキー兼用にしている場合でも、非PK な別の FK カラムが存在しない完全オーバーラップ構成では、今後は
nil代入によってそのカラムがNULLになる点に留意が必要です。
- もし「FK=PK の状態で、
- 参考情報 (あれば)
- 関連 PR:
- #55332: 「PK とオーバーラップする FK を安易にクリアしない」ための元の変更。
- #58368:
belongs_to/has_oneの外部キークリアロジックを統一した変更。本PRはそのフォローアップ。
- 実運用での確認ポイント:
belongs_to/has_oneの定義で、primary_key/foreign_keyをカスタム指定しているモデル。- 複合主キー的な構造(gem 等を含む)や、シャーディング用のカラムを PK に含めているモデル。
- ポリモーフィック関連で、「たまに type が nil で id だけ残る/その逆」のようなデータ不整合が発生していた場合、この修正で改善する可能性があります。
#58464 Don't raise LoadError when a test:* folder is missing
マージ日: 2026/8/17 | 作成者: @arzezak
- 概要 (1-2文で)
Rails のbin/rails test:*コマンドが、対応するtest/...ディレクトリが存在しない場合にLoadErrorで落ちていた挙動を修正し、「テスト0件」として正常終了するようにした PR です。特に、rails new直後のアプリでtest:systemやtest:jobsなどを実行すると CI が真っ赤になる問題を解消します。
- 変更内容の詳細
何が問題だったか
bin/rails test:system のような「フォルダ単位でのテスト実行コマンド」は、内部的に Rails::TestUnit::Runner に「パス」を渡しています。
Runner.extract_filters(paths)の挙動Dir.exist?(path)が true の場合:pathをディレクトリとみなし、path/**/*_test.rbという glob パターンに変換してテストファイル一覧を取る
Dir.exist?(path)が false の場合:pathを「テストファイルそのもの」と見なしてrequire(path)しようとする- 存在しないパスの場合は
LoadErrorが発生
rails new 直後のアプリには存在しないディレクトリが多く、例えば:
test/systemtest/jobstest/channelstest/mailboxestest/unittest/functionaltest/lib/generators
これらに対応するコマンド:
bin/rails test:systembin/rails test:jobs- など
を叩くと、上記のとおり Dir.exist? が false となり、LoadError でコマンド全体が失敗していました。
特に、rails new が生成する .github/workflows/ci.yml で bin/rails db:test:prepare test:system が走るため、「何もいじっていない新規アプリの最初の CI が必ず落ちる」という実害がありました。
どう直したか
修正は Runner ではなく、コマンド層 (Rails::Command::TestCommand) に閉じ込めて 行われています。
方針
test:*ショートカットコマンド(test:systemなど)が内部で渡す「フォルダパス」について、ディレクトリが存在しない場合も、エラーにせず「空の glob」として扱う ようにする。- つまり、
- フォルダが存在する場合: 従来どおり (そのディレクトリ配下の
*_test.rbを走らせる) - フォルダが存在しない場合: 「そのフォルダにはテストが1件もない」とみなし、0件で成功終了させる
- フォルダが存在する場合: 従来どおり (そのディレクトリ配下の
実装レベルでは:
Rails::Command::TestCommand内で、test:system等が Runner に渡す前の「フォルダ引数」をラップし、- 存在しないフォルダであれば、「存在しないフォルダを
requireさせる」のではなく、「マッチしない glob パターン」として扱う形に変換しています。
- 存在しないフォルダであれば、「存在しないフォルダを
ポイントは:
Runner 自体には手を入れていない
- Runner はユーザーが手で指定した任意パスも受け付けており、
test/modlesのような typo に対してLoadError(パス名付き)を返すことで、ユーザーが問題に気づける最後の手がかりになっています。 - ここで「存在しないディレクトリを黙って無視」してしまうと、ユーザーが間違ったパスを指定しても気づけない問題が出るため、「ショートカットコマンドから渡されたとわかるケースだけを特別扱い」 するために TestCommand 側で対処しています。
- Runner はユーザーが手で指定した任意パスも受け付けており、
「無かったことにして引数から落とす」のではなく、「マッチ0件の glob」にする
Runner.list_testsは、渡されたパターンのリストが空の場合default_test_glob(全テスト)にフォールバックします。- そのため、単に「フォルダ引数を捨てる」だけにすると、
bin/rails test:system(フォルダがない)で「テスト全体」が動いてしまう挙動になります。 - それを避けるため、「パターンの要素としては残すが、結果的に何もマッチしない形」に変換しています。
動作確認されている例
修正前:
$ bin/rails test:system
cannot load such file -- /tmp/blog/test/system (LoadError)
$ echo $?
1修正後:
$ bin/rails test:system
0 runs, 0 assertions, 0 failures, 0 errors, 0 skips
$ echo $?
0同様に、以下のようなコマンドも LoadError ではなく「テスト0件」で成功終了するようになります:
bin/rails test:channels(test/channels)bin/rails test:jobs(test/jobs)bin/rails test:mailboxes(test/mailboxes)bin/rails test:units(test/unit; ここからtest/modelsとtest/helpersを実行)bin/rails test:functionals(test/functional;test/controllersとtest/mailersを実行)bin/rails test:generators(test/lib/generators)
- 影響範囲・注意点
影響範囲
- 影響を受けるのは
Rails::Command::TestCommand経由の「フォルダショートカット」系コマンド です。bin/rails test:systembin/rails test:jobsbin/rails test:channelsbin/rails test:mailboxesbin/rails test:unitsbin/rails test:functionalsbin/rails test:generators
- これらが、該当する
test/...ディレクトリが存在しない場合でもLoadErrorで落ちずに、「0 runs」で exit code 0 を返すようになります。 bin/ciのように内部でこれらのコマンドを使っているスクリプト・CI も、同様に安定動作するようになります。
既存挙動を壊さない工夫
- Runner 自体(
Rails::TestUnit::Runner)の挙動は変えていないため、- ユーザーが手で
bin/rails test path/to/some_file.rbや、タイポを含むパスを指定した場合のLoadError挙動は従来通りです。 rails/plugin/test.rbやtools/test.rbのような Runner を直接使う呼び出し元にも影響しません。
- ユーザーが手で
注意点
- 「存在しないフォルダ」を指定しても成功終了するようになるため、「そのフォルダにテストを書いたつもりなのに、ディレクトリ作成を忘れていた」というケースでは気づきにくくなる可能性があります。
- ただし今回の変更対象は TestCommand 由来のショートカット (
test:system等) に限定されており、
「任意パスを明示的に指定したときのフィードバック」は維持されている点がバランスです。
- ただし今回の変更対象は TestCommand 由来のショートカット (
- CI 上で
test:systemを回しているプロジェクトは、ディレクトリ未作成でも CI がグリーンになる ようになります。
「システムテストを書いていないことをエラーで気づきたい」場合は、この変更によりそのシグナルが弱まりますが、一般的には「空のテストは成功」という挙動の方が自然と考えられています。
- 参考情報 (あれば)
- 関連 PR / Issue:
- #56853: 元の報告(同様の問題、fix なしで close)
- #57893: 同じ症状に関する Issue
- #56856, #57924: CI テンプレート側で
system-testジョブを削除 / コメントアウトするアプローチ
→ 今回の PR は「コマンド自体も直す」もので、CI テンプレートの変更と併用可能(補完関係)。
- 変更ファイル:
railties/lib/rails/commands/test/test_command.rb- フォルダショートカットコマンドの引数処理を修正
railties/test/application/test_runner_test.rb- 上記 7 コマンドについて、ディレクトリ未作成時に
LoadErrorにならないことを検証するテストを追加
- 上記 7 コマンドについて、ディレクトリ未作成時に
railties/CHANGELOG.md- 挙動変更として記載追加
#58471 Add --no-banner to bin/rails console
マージ日: 2026/8/17 | 作成者: @Saidbek
- 概要 (1–2文で)
bin/rails consoleに起動バナー(Railsロゴやバージョン等)を非表示にするための--no-bannerオプションが追加されました。これにより、スクリプト実行やCIなどで一時的にバナーを消したいケースを、.irbrcを編集せずにCLIフラグだけで制御できます。
- 変更内容の詳細
新オプション --no-banner
以下のように実行できます:
bin/rails console --no-bannerこのオプションを付けると:
- IRB 使用時のバナー (IRBが表示するバナー)
- Rails独自のコンソール起動バナー (ロゴ、バージョン、TIP、パスなど)
を非表示にします。
実装上のポイント:
IRB.conf[:SHOW_BANNER] = falseを IRBセットアップ後 に設定- これにより:
- Rails側のデフォルト設定
- ユーザーの
.irbrc内でのIRB.conf[:SHOW_BANNER]設定
のいずれよりも「--no-bannerフラグ」が優先されます(そのセッションに限り強制オフ)。
- これにより:
- Rails側の「非IRBバナー」も
--no-bannerが指定されている場合はスキップされるよう修正。
なお、「Loading … environment」のような環境ロードメッセージは、IRB.conf[:SHOW_BANNER] を無効にした場合と同様に、引き続き表示されます。完全に無出力になるわけではありません。
コード面での変更箇所 (概要)
railties/lib/rails/commands/console/console_command.rb- コンソールコマンドに
--no-bannerオプションを追加。 - オプションが指定された場合は、Railsのデフォルトバナーを表示しないよう制御。
- コンソールコマンドに
railties/lib/rails/commands/console/irb_console.rb- IRB初期化後に
IRB.conf[:SHOW_BANNER] = falseを設定する処理を追加。
- IRB初期化後に
railties/test/commands/console_test.rb--no-bannerオプションの挙動を保証するテストを追加。
railties/CHANGELOG.md- 新機能として
--no-bannerの追加を記載。
- 新機能として
- 影響範囲・注意点
- 既存の
bin/rails consoleの挙動はデフォルトでは変わりません。- 何もオプションを付けなければ、従来どおりバナーが表示されます。
- 一時的にバナーを消したい場合:
- これまでは
.irbrcにIRB.conf[:SHOW_BANNER] = falseを書いて恒久的に設定するしかありませんでした。 - 今後は
bin/rails console --no-bannerでその実行のみ非表示にできます。
- これまでは
.irbrcでIRB.conf[:SHOW_BANNER] = trueにしていても、--no-bannerを付けた場合はそのセッションでは強制的にオフになります(CLIオプションが優先される)。- CI や自動テストスクリプト等で標準出力をクリーンに保ちたい場合に有効です。
- ログパーサやスナップショットテストで、バナー出力がノイズになっている場合の対処として使えます。
Loading ... environmentの行は残る点に注意が必要です。完全無出力が必要な場合は、別途リダイレクトやサプレッションが必要になります。
- 参考情報 (あれば)
- 対応 Issue: #58470
- 主な設定との関係:
.irbrc内:IRB.conf[:SHOW_BANNER] = false→ 常にバナー非表示 (従来どおり有効)- CLI:
bin/rails console --no-banner→ そのセッションのみバナー非表示(.irbrcよりも優先)
#58474 delegate ActiveModel::Type::Binary::Data#as_json to its value
マージ日: 2026/8/17 | 作成者: @HoneyryderChuck
- 概要 (1-2文で)
ActiveModel のバイナリ型ActiveModel::Type::Binary::Dataに対して、as_jsonを中身の値(文字列)に委譲する実装が追加されました。これにより、バイナリ属性を含むオブジェクトを JSON 化した際に、意味のない ivar マッピングではなく、期待どおり文字列としてシリアライズされるようになります。
- 変更内容の詳細
何が問題だったか
- Rails では多くのプリミティブオブジェクトと
ActiveModel::Type::Valueに#as_jsonが定義されています。 - しかし
ActiveModel::Type::Binary::Dataには独自のas_jsonがなく、to_json/as_json時にObject#as_jsonにフォールバックしていました。 Object#as_jsonは「インスタンス変数名 → 値」のハッシュに変換するため、バイナリ値としては不自然な JSON が生成されます。
例(イメージ):
record.binary_attr #=> #<ActiveModel::Type::Binary::Data:0x... @value="abc", ...>
record.as_json
# 以前はおおよそこんな形:
{
"binary_attr" => {
"@value" => "abc",
# 他の ivar があればそれも展開される
}
}これは「バイナリ値をラップするオブジェクトの内部構造」が JSON に出てしまい、「値そのもの」として使いたい場面では壊れた形式になります。
特に、store_model のように DB に JSON 的な複合構造を保存・復元するライブラリと組み合わせた場合に問題になります。
今回の変更点
ActiveModel::Type::Binary::Data に as_json を実装し、「自分自身ではなく中身の値(value)を JSON 化する」ように変更されました。
イメージとしては以下のような実装です:
# activemodel/lib/active_model/type/binary.rb
class ActiveModel::Type::Binary::Data
# 追加されたメソッド(実際のコードは多少異なる可能性あり)
def as_json(*)
value.as_json
end
endこれにより、Data オブジェクト自体はユーザーから見えない「ラッパー」として扱われ、as_json では中に保持している文字列(またはそれに準ずる値)がそのまま JSON 変換に使われます。
変更後の挙動:
record.binary_attr #=> #<ActiveModel::Type::Binary::Data @value="abc">
record.as_json
# 変更後:
{
"binary_attr" => "abc"
}テストとドキュメント
activemodel/test/cases/type/binary_test.rbに、as_jsonの挙動を確認するテストが追加。- 例:
Data.new("abc").as_jsonが"abc"を返すことなど。
- 例:
activemodel/CHANGELOG.mdにこの振る舞い変更が追記され、バイナリ型の JSON シリアライズ挙動が明示されました。
- 影響範囲・注意点
影響範囲
- 対象:
ActiveModel::Type::Binary::Dataを通して扱われるバイナリ属性のas_json/to_jsonの結果。 - Active Record で
binary型(あるいは類似の型)を使っている場合、その JSON 表現が変わる可能性があります。
互換性の観点
- これまで
Dataオブジェクトのインスタンス変数構造に依存して JSON を解釈していたコードがあれば、そのコードは壊れます。- 例:
json["binary_attr"]["@value"]のように内部構造を前提にしている場合。
- 例:
- 一方で、「バイナリ値をそのまま文字列として JSON にしたい」という一般的なニーズに沿っており、ほとんどのアプリケーションにとっては「バグ修正」として扱える変更です。
移行のためのチェックポイント
- API レスポンスや Webhook、外部連携で
binaryカラムを JSON 出力しているエンドポイントがあれば、返却 JSON の形式が変わっていないか確認してください。 - 独自に
ActiveModel::Type::Binary::Dataを拡張し、as_jsonをオーバーライドしていた場合は、今回の振る舞いと衝突しないか確認が必要です。 - テストで
as_jsonの戻り値を厳密に比較している場合(特にスナップショットテスト)は更新が必要になることがあります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58474
- 関連しうるユースケース:
store_modelなど、DB の JSON/ハッシュカラムに複雑なオブジェクトを格納し、それを Rails の型変換 (ActiveModel::Type::*) 経由でロードしてから JSON シリアライズするパターン。
- バイナリ型全般の挙動は
activemodel/lib/active_model/type/binary.rbの実装が参考になります。
#58320 Keep the relation scope in Relation#update and #update!
マージ日: 2026/8/17 | 作成者: @pierry01
- 概要 (1-2文で) Rails の
Relation#update/Relation#update!が関連・スコープを無視してレコードを更新してしまう問題を修正し、関連経由の更新もきちんとスコープが効くようにした PR です。これに伴い、スコープ外の id を指定した場合は更新されずActiveRecord::RecordNotFoundが発生するようになり、一部で挙動が破壊的に変わります。
- 変更内容の詳細
何が問題だったか
従来の実装:
# activerecord/lib/active_record/relation.rb(概念的)
def update(id = :all, attributes)
if id == :all
each { |record| record.update(attributes) }
else
model.update(id, attributes) # クラスメソッドを呼ぶ = スコープから脱出
end
endRelation#update / Relation#update! が、複数 id 指定の場合にクラスメソッド Model.update に丸投げしており、その中で Model.find が呼ばれていました。Model.find は関連や where などの relation スコープを一切考慮しないため、見かけ上は関連に対して更新しているコードが、実際には「関連外のレコード」も更新しうる状態になっていました。
例:
post.comments.update!(comment_id, body: "…")
# 実際の SQL:
# UPDATE "comments" SET "body" = '…' WHERE "comments"."id" = ?
# ^ post_id などの関連条件が付かないpost.comments という関連(あるいはテナント境界を表す関連: account.users, buyer.suppliers など)経由で呼び出しているにもかかわらず、
- id がその関連外(別の post のコメントなど)であっても
- クラスレベルの
findによって見つかったレコードが更新される
というセキュリティ的にも危険な挙動でした(クロステナント書き込みになりうる)。
テストにも問題が露呈しており、
topics = TopicWithCallbacks.none
topics.update([topic1.id, topic2.id], [{ title: "adequaterecord" }, { title: "adequaterecord" }])
# none(空スコープ)にもかかわらず topic1, topic2 が更新されるという「none な relation 経由の update が普通にレコードを更新してしまう」状態でした。
どう直したか
実装の移動: クラスメソッド → Relation に本体を移す
ActiveRecord::Persistence::ClassMethods#update / #update! の本体を ActiveRecord::Relation 側に移し、クラスメソッドは単なるデリゲーションに変更しています。
新しい構造(概念的):
# クラスメソッド側
def self.update(id = :all, attributes)
all.update(id, attributes) # Relation に委譲
end
def self.update!(id = :all, attributes)
all.update!(id, attributes) # 同様に委譲
end# Relation 側の実装(元々 ClassMethods にあったロジックをそのまま移動)
def update(id = :all, attributes)
# ... ここに元々 ClassMethods#update にあったロジックがそのまま移っている
end
def update!(id = :all, attributes)
# ... 同様
endポイント:
- これまでクラス側にあった
ArgumentErrorのチェック(引数形式が不正な場合など)- 複数 id 更新ロジック (
update_multiple_ids?) findを使った id 解決 などの実装は、文言含めてそのまま Relation に移動しています(新しいraiseやメッセージ変更は無し)。
ClassMethods#update/#update!はall(= Relation)に委譲するだけになりました。
Relation でスコープを尊重するようになった
本体が Relation に移ったことで、Relation#update / Relation#update! が self という relation を前提に find や更新を行うようになりました。
つまり:
post.comments.update!(comment_id, body: "…")は内部的に
post.comments.find(comment_id).update!(body: "…")
# => post.comments による WHERE 条件が必ず付く
# (post_id = ... など)という形で実行されるイメージになります。
また、
Comment.none.update!(id, body: "…")のような場合は、none の relation で find を行うため、レコードは一切見つからず ActiveRecord::RecordNotFound が発生し、レコードは更新されません。
既存のクラスメソッド挙動との整合性
ActiveRecord::Querying::QUERYING_METHODS には、すでに多くの「id ベース、あるいは複数行更新/削除系」のクラスメソッドが all に委譲する形で揃えられています。
deletedestroydelete_alldestroy_allupdate_alltouch_allinsert_allupsert_all- …など
今回の PR で update / update! もこのパターンに揃い、Model.delete(id) == Model.all.delete(id) と同じように、Model.update(id, ...) == Model.all.update(id, ...) という構造になります。
Model.update の呼び出しに関しては、「元々 find も all.find であり」「:all 指定時も all.each を使っていた」ため、クラスレベルの API 表面上の挙動は(スコープを考えない限り)ほぼ同じです。
scoping { model.update(...) } 案が採用されなかった理由
代替案として、
scoping { model.update(id, attributes) }のように、Relation の scoping を使ってクラスメソッドを呼ぶ修正も検討されましたが、scoping は current_scope をコールバック / バリデーションにも漏らしてしまうため却下されています。これは Delegation::ClassSpecificRelation#method_missing にある NotImplementedError が警告している通りの「スコープリーク」であり、より安全なため「実装ごと Relation に移動する」方針が採られています。
- 影響範囲・注意点
挙動の変化(Breaking change)
1. Relation 経由で「スコープ外 id」を更新しようとすると RecordNotFound
以前:
post = Post.first
# 他の Post に属するコメント
other_comment = Comment.where.not(post_id: post.id).first
# これでも other_comment が更新されてしまっていた
post.comments.update(other_comment.id, body: "oops")新しい挙動:
post.comments.update(other_comment.id, body: "oops")
# => ActiveRecord::RecordNotFound が発生し、レコードは一切更新されないnone な relation でも同様です:
Comment.none.update([1, 2], [{ body: "a" }, { body: "b" }])
# 以前: id=1,2 が普通に更新されてしまう
# 現在: RecordNotFound(スコープ上レコードが見つからないため)もし従来の「スコープを無視して class-level に対して更新する」挙動を意図的に利用していた場合は、明示的にクラスメソッドを呼び出す必要があります:
Comment.update(id, attributes) # 全体に対して
Comment.unscoped.update(id, attributes) # default_scope をも無視2. Model.unscoped.update の挙動が正しく「unscoped」になる
以前:
Comment.unscoped.update(id, attributes)
# 内部でクラスメソッド Comment.update を呼ぶが、
# そもそも Comment.update が default_scope を適切に外していないケースがあった今回の変更により、
Comment.unscoped.update(id, attributes)
# => unscoped な Relation(default_scope 無し)の上で update が実行されるようになり、default_scope をちゃんと外した状態で更新が行われます。これは、これまでの「default_scope が update 時にも暗黙に効いてしまう」側から見たバグ修正とも言えます。
どんなコードが影響を受けうるか
- 関連経由 (
user.posts,account.usersなど) でupdate(id, ...)/update!(id, ...)を呼び出し、- その id が関連外のレコードを指している可能性がある
- かつ、それが今まで「たまたま動いていた」コード
Model.none.update(ids, ...)を「全件から id 検索して更新」として誤用していたコードModel.unscoped.update(...)を使っており、default_scope の効き方に依存していたコード
このようなコードは、今後 RecordNotFound による例外や default_scope の無効化によって挙動が変わります。
なお、「id が relation 内に収まっている」正常ケースにおいては、より安全になっただけで、機能的な後方互換性は維持されています。
- 参考情報 (あれば)
- 本 PR は、2023 年に提案されていた類似修正 PR #49332 の後継であり、そちらで指摘されていた
- バリデーションロジックを重複させないこと
- 新しい例外やメッセージ変更を導入しないこと といった懸念を、実装の移動(重複排除)という形で解決しています。
- テスト:
activerecord/test/cases/relation/update_all_test.rbに Relation レベルの挙動テストを追加・強化activerecord/test/cases/scoping/default_scoping_test.rbにunscoped.updateの挙動を固定するテストを追加- 既存の
persistence_test.rbのクラスレベルテストは、「クラスメソッド → Relation への委譲」を通じて引き続き coverage を維持
- CHANGELOG:
activerecord/CHANGELOG.mdに、上記 Breaking change(relation.update(ids, ...)がスコープ外 id でRecordNotFoundになること、Model.unscoped.updateが default_scope を外すこと)が明示されています。
#58497 Simplify MySQL::IndexDefinition constructor
マージ日: 2026/8/17 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
MySQL 用のMySQL::IndexDefinitionクラスのコンストラクタが簡素化され、引数まわりの扱いが整理されています。外部からの使い方はほぼ変えずに、内部実装の読みやすさと一貫性を高めるリファクタリングです。
- 変更内容の詳細
※このPRは 1 ファイル・+3/-3 行のみの非常に小さな変更で、実質的にはコンストラクタの引数処理のリファクタです。実際のコード差分は公開情報が限られているため、典型的なパターンに基づく説明になります。
想定される変更の方向性としては、以下のようなものです:
キーワード引数の扱いの統一
- 旧: 位置引数 + オプションハッシュ、あるいは
*args/**optionsが混在していた - 新: 明示的なキーワード引数に整理、もしくは親クラス
IndexDefinitionと同じ形に寄せる
- 旧: 位置引数 + オプションハッシュ、あるいは
不要な引数・分岐の削除
- 例えば
lengths/orders/usingといった MySQL 独自オプションの扱いが簡略化され、- 親クラスに任せられる部分は親クラスに委譲
- MySQL 固有部分だけを明示的に扱うように整理
- 例えば
典型的なリファクタ例(イメージ)は以下のようなものです:
# 変更前(例)
def initialize(table, name, unique, columns, lengths = nil, orders = nil, opclasses = nil, where = nil, type: nil, using: nil, comment: nil)
super(table, name, unique, columns, lengths, orders, opclasses, where, type: type, using: using, comment: comment)
end
# 変更後(例)
def initialize(**options)
super(**options)
endあるいは、その逆に:
# 変更前(例)
def initialize(**options)
super(**options)
end
# 変更後(例)
def initialize(table, name, unique, columns, lengths: nil, orders: nil, type: nil, using: nil, comment: nil, **)
super
endいずれにせよ、PRタイトルの通り「コンストラクタのシグネチャ・実装を簡潔にし、余計な引数処理や委譲コードを削る」方向の修正です。
- 影響範囲・注意点
影響範囲
ActiveRecord::ConnectionAdapters::MySQL::IndexDefinitionを直接使用しているコード- 通常のアプリケーションではほとんど無関係です(内部API色が強い)。
- MySQL アダプタ周辺のスキーマ定義・マイグレーション生成ロジック
- 典型的な
add_index/remove_indexの利用では挙動変更は発生しないはずです。
- 典型的な
注意点
- もし自作のアダプタ拡張やメタプログラミングで
MySQL::IndexDefinition.new(...)を直接呼び出している場合:- コンストラクタの引数リスト(位置引数/キーワード引数の構成)が変わっている可能性があります。
- Rails の内部実装に依存したコードを書いている場合は、このクラスのシグネチャを一度確認してください。
- 公開API (
ActiveRecord::MigrationのDSL:add_index,index,t.index等) を普通に使っているだけなら、互換性の問題は基本的にありません。
- もし自作のアダプタ拡張やメタプログラミングで
- 参考情報 (あれば)
- 関連しそうなコード:
activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rbIndexDefinitionの抽象実装。MySQL 用クラスはここを継承・拡張していることが多いです。
- 類似のリファクタ:
- 過去にも PostgreSQL 用の
IndexDefinitionやスキーマ定義クラスで、- 「コンストラクタのキーワード引数化」
- 「親クラスに共通ロジックを寄せる」 といったPRが繰り返し行われているため、その一環と考えられます。
- 過去にも PostgreSQL 用の
#58097 Join an Array accept in file_field_tag
マージ日: 2026/8/16 | 作成者: @55728
- 概要 (1-2文で)
file_field_tagにacceptオプションとして配列を渡したときのレンダリングが、HTML仕様に沿ったカンマ区切り形式になるよう修正されました。- これにより、
file_field_tagとfile_fieldの挙動が揃い、ブラウザが複数 MIME type を正しく解釈できるようになります。
- 変更内容の詳細
- 対象:
ActionView::Helpers::FormTagHelper#file_field_tag
これまで:
file_field_tag("picture", accept: ["image/png", "image/gif"])
# => <input type="file" name="picture" accept="image/png image/gif">
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# 配列がスペース区切りで join されていた修正後:
file_field_tag("picture", accept: ["image/png", "image/gif"])
# => <input type="file" name="picture" accept="image/png,image/gif">
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# 配列がカンマ区切りで join されるHTML仕様上、accept 属性は「カンマ区切りのトークンのリスト」と定義されていますが、以前の実装はスペース区切りで結合していたため、2つ目以降の値をブラウザが無視するケースがありました。
今回の修正では:
acceptに Array が渡された場合、内部でaccept.join(",")相当の処理を行い、カンマ区切りで出力するように変更acceptが String の場合はそのまま利用(既存挙動維持)- すでにオブジェクトフォーム側 (
file_field) ではカンマ区切りで join されていたため、それと同じロジックに揃えた
テスト・ドキュメント関連:
actionview/test/template/form_tag_helper_test.rbに、配列acceptが期待通りカンマ区切りでレンダリングされることを検証するテストを追加actionview/CHANGELOG.mdに本変更のエントリを追加
- 影響範囲・注意点
- 影響を受けるケース:
file_field_tagを使い、acceptに Array を渡していたコードrubyこれまで事実上、最初の MIME type しか効いていなかったブラウザが、修正後は複数 MIME type を正しく解釈するようになります。file_field_tag :file, accept: ["image/png", "image/jpeg"]
- 影響を受けないケース:
file_field_tagに単一の MIME type を String で渡している場合rubyfile_field_tag :file, accept: "image/png"file_field(モデルバインド版) を使っている場合
→ もともとカンマ区切りで join されていたため挙動は変わらない
- レイアウト・仕様依存の注意:
- もしスペース区切りの
acceptを前提とした、独自のパース処理や JS があれば、カンマ区切りに対応しているかを確認する必要があります(一般的なブラウザはもともとカンマ区切り前提なので通常は問題なし)。 - HTML仕様・ブラウザの挙動により、修正後のほうが「本来の期待どおりに複数 MIME type が効く」状態になるため、UI 上で選択できるファイルの種類が増えたように見えるケースがあります。
- もしスペース区切りの
- 参考情報 (あれば)
- 関連コード:
ActionView::Helpers::Tags::FileField#render
ここではすでにacceptの Array をカンマ区切りにしており、今回file_field_tagがそれに追従した形です。
- HTML仕様:
- HTML Living Standard:
input要素のaccept属性は「コンマ区切りの型/拡張子のリスト(“Comma-separated list of items”)」と規定されており、本変更はその仕様順守のための修正となります。
- HTML Living Standard:
#58305 Require active_support/rails in rails/railtie
マージ日: 2026/8/16 | 作成者: @55728
- 概要 (1-2文で)
Rails 8.1 でrequire "rails/railtie"を単体で実行するとdelegate_missing_toが未定義でNoMethodErrorになる不具合があり、それを解消するためにrails/railtie内でactive_support/railsを require するようにした PR です。
Rails 本体より先に Railtie を読み込む一部の gem (terser,inline_svg,simple_form,scenicなど) が 8.1 で落ちる問題を修正します。
- 変更内容の詳細
問題点
Rails 8.1 で以下のようにするとエラーになります。
$ ruby -e 'require "rails/railtie"'
.../rails/initializable.rb:41:in '<class:Collection>': undefined method 'delegate_missing_to' for class Rails::Initializable::Collection (NoMethodError)
delegate_missing_to :@collection
^^^^^^^^^^^^^^^^^^^Rails::Initializable::Collection では delegate_missing_to を使っていますが、このメソッドを提供している Active Support の拡張がまだ読み込まれていないためです。
Rails 8.0.5 では同じコードが動作していたため、8.1 で導入された変更(コミット 9c43c5bda5 付近)が原因のリグレッションとされています。
delegate_missing_to は Active Support が提供するメソッドで、active_support/rails(Rails 用のプレリュード)を require すると利用可能になります。
具体的な修正
railties/lib/rails/railtie.rb に 1 行追加されたのみです。
イメージとしては以下のような変更です(PR からの意訳):
# railties/lib/rails/railtie.rb
require "active_support/rails" # ← この1行が追加された
module Rails
class Railtie
# ...
end
endこれにより、rails/railtie を require した時点で delegate_missing_to などの Active Support 拡張が読み込まれ、Rails::Initializable::Collection などが安全に定義できるようになります。
なぜ active_support/rails なのか
delegate_missing_toなどを含む Rails 向け Active Support の「標準セット」を読み込む入口がactive_support/rails- すでに他のエントリポイントでは同じ対応がされている:
rails.rbrails/command.rb
これらも以前のコミット (1db66c0d4e) でactive_support/railsを require するようになっている
- 今回の変更により、
rails/railtieも他のエントリポイントと整合した挙動になる
- 影響範囲・注意点
影響するケース
Rails 本体より先に Railtie をロードする gem・アプリケーション:
ruby# 例: gem の railtie ファイル内で require "rails/railtie" module MyGem class Railtie < ::Rails::Railtie # ... end end実際に報告されている対象:
terser/railtieinline_svg/railtiesimple_form/railtiescenic/railtie
これらは Rails 8.0.5 では正常動作 → Rails 8.1.3 で NoMethodError → 本修正により再度ロード可能、という挙動になります。
影響しない / 変化がほぼないケース
- 通常の Rails アプリで
config/application.rbなどからrailsを require して起動する一般的なパターンでは、既にrails.rb経由でactive_support/railsが読み込まれているため挙動は従来と変わりません。 - PR 説明によると、
./tools/railspect requires .(Rails の require パターンを網羅的にテストする内部ツール)を実行した結果は、この変更の有無で同一とのことで、副作用が極めて小さいことが確認されています。
開発者が意識しておく点
- gem 側でのワークアラウンドとして
require "active_support/rails"を自前で書く必要はなくなります(Rails 8.1 以降、この修正が入ったバージョンを前提にできる場合)。 - もし 8.1 のマイナーバージョン間で「Railtie の読み込みだけで落ちる」現象に遭遇している場合、この PR を含むバージョンに上げることで解消が期待できます。
- 独自 Railtie を持つ gem で、Rails 本体より先にロードされる可能性がある場合は、この種の「Active Support 機能がまだ読み込まれていない」問題を避けるため、今後も Rails のロード順・require 依存を意識しておくとよいです。
- 参考情報 (あれば)
- 該当 PR: Require
active_support/railsinrails/railtie(#58305) - 関連する過去の変更:
rails.rbとrails/command.rbがactive_support/railsを require するようになったコミット:1db66c0d4e- リグレッションの原因になったとされるコミット:
9c43c5bda5
- 使用メソッド:
delegate_missing_toはActiveSupport::Concern/Module#delegate_missing_toとして提供される Active Support の拡張メソッドで、オブジェクトに存在しないメソッド呼び出しを指定のオブジェクトに委譲するために使われます。
#58494 Read the columns of many tables in one query
マージ日: 2026/8/16 | 作成者: @ngan
- 概要 (1-2文で)
connection.columnsが複数テーブルを一度に取得できるようになり、スキーマキャッシュのダンプ時に発行される SQL 文数と実行時間が大幅に削減されました。MySQL/MariaDB/PostgreSQL 向けにカタログからの一括取得ロジックが追加され、MariaDB のデフォルト値扱いも精緻化されています。
- 変更内容の詳細
2-1. columns が複数テーブル対応に
これまで:
connection.columns(:users) # => [Column, ...]
# テーブルごとに個別クエリこれから:
connection.columns(:users) # 既存と同じ => [Column, ...]
connection.columns([:users, :posts]) # 新形式 => { "users" => [...], "posts" => [...] }- 単一テーブル指定時は従来どおり
Array<Column>を返却。 - 複数テーブル指定時は
"テーブル名" => Array<Column>の Hash を返却。 - 内部的に「スキーマ情報をまとめて読むためのインターフェイス (
WHOLE_SCHEMA_READERS)」の一員として扱われるようになり、スキーマキャッシュダンプで一括読みが利用される。
2-2. スキーマキャッシュダンプの効率化
250 テーブルでの測定:
| statements | time | |
|---|---|---|
| before | 255 | 124.6 ms |
| after | 6 | 44.0 ms |
「6ステートメント」に含まれるもの:
- テーブル一覧
- プライマリキー情報の一括取得
- カラム情報の一括取得(今回の変更の主対象)
- インデックス情報の一括取得
schema_migrationsの存在確認schema_migrationsからバージョン一覧取得
以前は add_all 内でテーブルリストを二重に取得しており(data_source_exists? 経由)、その重複が解消され 1ステートメント減少。@data_sources を既に取得済みのテーブルリストで初期化することで、再問い合わせを避けています。
2-3. アダプタごとの実装
MySQL / MariaDB
- これまで:
SHOW FULL FIELDS FROM <table>をテーブルごとに実行。 - これから:
information_schema.columnsをテーブルリストで絞り込んで一括取得。
ポイント:
information_schema.columnsがSHOW FULL FIELDSと同等の情報を、ほぼ同じカラム名で持っていることを利用。- MySQL 5.6/5.7 のように data dictionary 登場前のバージョンでも、テーブルごとに
SHOW FULL FIELDSを打つより、一発でinformation_schemaを読む方が速いことを計測で確認。- 例: 31 テーブルで
- 5.6: 15.1 ms/テーブル vs 1.8 ms (一括クエリ)
- 5.7: 19.1 ms/テーブル vs 2.4 ms (一括クエリ)
- 例: 31 テーブルで
PostgreSQL
- 既存の
pg_attributeを読むクエリを拡張し、「単一テーブル」→「複数テーブルのリスト」に対応。 - パラメータとして複数テーブルを与え、IN 句などでフィルタして一括取得する構成。
SQLite
- SQLite 自体はテーブル単位の読み出ししか提供しないので、既存の「テーブル単位実装」をそのまま利用。
- ただし、インターフェイスとしては複数テーブル引数に応答できるようにし、各テーブルを順番に呼び出して Hash にまとめる形。
2-4. MariaDB のデフォルト値扱いの改善
MariaDB の information_schema.columns では、デフォルト値が「SQL リテラルとして宣言された文字列」の形で保存されています。SHOW FULL FIELDS の結果とは以下のような差があります:
宣言: 'a\nb' -> catalog: バックスラッシュ + n / SHOW: 実際の改行
宣言: 'a\tb' -> catalog: タブ文字 / SHOW: タブ文字
宣言: 'a\\b' -> catalog: バックスラッシュ2つ / SHOW: バックスラッシュ1つ
宣言: 'a\%b' -> catalog: バックスラッシュ保持 / SHOW: バックスラッシュ保持対応内容:
- カタログの値は クォートを外した上で Rails 内部表現に合わせて解釈されるようにしたため、
DEFAULT NULL→"NULL"という文字列ではなく、NULL として扱える。- クォートされたデフォルトは「値」であって「式」ではない、という扱いを
update_fields_for_mariadb相当のロジックなしに判断可能になった。
- これにより、
SHOW CREATE TABLEを追加で読む必要が減り、MariaDB でより正確かつ効率的な情報が得られる。
なお、TEXT/BLOB のデフォルトについては、既存挙動(new_column_from_field 側で unquote するために、ここではクォート保持)が微妙なケースを含むものの、互換性維持のため現状維持とし、コードコメントで理由が説明されています。
2-5. 生成される Column オブジェクトの同一性検証
Column#== は PostgreSQL 特有の属性 (serial, identity, generated) を比較対象から外しているため、インスタンス変数ベースで厳密比較を実施し、以下の環境で「単一テーブル読み」と「一括読み」で完全に同一の Column が生成されることを確認しています。
| DB | テーブル数 | カラム数 |
|---|---|---|
| MySQL 8.0.46 | 21 | 76 |
| MariaDB 11.8.8 | 4 | 60 |
| MySQL 5.7.44 | 31 | 110 |
| MySQL 5.6.51 | 31 | 108 |
| PostgreSQL 17 | 21 | 73 |
検証には、通常のスキーマでは出ないようなケースも含まれています(式デフォルト、関数っぽい文字列、ON UPDATE CURRENT_TIMESTAMP、STORED/VIRTUAL 生成カラム、各種エスケープ・クォート・バックスラッシュ・NUL・Ctrl-Z・空文字、bit/enum/コメント/文字セット違い/明示的コレーションなど)。
2-6. スキーマ修飾名 (schema-qualified names)
public.things/other.thingsのように同名テーブルが複数スキーマに存在するケースでも、呼び出し側が渡した識別子をそのままキーにして結果を返します。
例:
connection.columns(["public.things", "other.things"])
# => {
# "public.things" => [#<Column name="id">, #<Column name="pub_col">],
# "other.things" => [#<Column name="id">, #<Column name="other_col">]
# }- カタログから該当情報が一切返ってこないテーブルについては:
- 「見えない/存在しない」と判断し、従来と同じ「単一テーブル読み」フォールバックで扱うことで、後方互換性を維持。
2-7. テスト
columnsがWHOLE_SCHEMA_READERSに追加され、共通テストで以下をカバー:- 渡したテーブルセットだけを読むこと
- 1テーブル読みと結果が一致すること
- テーブル数に対してスケールしない(クエリ数が増えない)こと
- 空配列を渡した場合は何も読まないこと
- スキーマ修飾名をキーとして正しく扱うこと
- カラム順序が
columnsの仕様上重要なため、比較時にソートしない新規テストを追加。 - MariaDB の挙動は既存テストでかなりカバーされており、
DEFAULT NULL対応・式判定・エスケープ処理・ダブルクォート処理などをわざと壊すと複数のテストが落ちることを確認済み。 - フルテストスイート OK:
- sqlite3, mysql2, trilogy, postgresql, MariaDB 11.8
- MariaDB は元々存在するトランザクション関連 4テストが引き続き失敗するのみで、本変更に起因する新たな失敗はなし。
- MySQL 5.6/5.7 および MariaDB はローカルコンテナで確認されており、本番 CI マトリクスに入れているプロジェクトでは再確認推奨。
- 影響範囲・注意点
- パブリックAPIの挙動変化:
connection.columnsが「引数の型によって戻り値の型が変わる」メソッドになりました。- シンボル/文字列 1つ →
Array<Column> - 配列(複数テーブル) →
Hash<String, Array<Column>>
- シンボル/文字列 1つ →
- ライブラリ/アプリ側で
columnsを直接呼んでいる場合、今後「配列を渡すと Hash が返る」ことを前提にしたコードを書ける一方で、既存コードが配列前提で「複数テーブル名を渡していた」場合はバグになりうるため要確認。
- パフォーマンス面の影響:
- スキーマキャッシュダンプや
schema_cache.ymlの生成など、「全テーブルのメタデータを読む処理」で劇的にクエリ数が減るため、大規模スキーマ(数百テーブル)環境ほど恩恵が大きいです。 - MySQL 5.6/5.7 のような旧バージョンでも、一括クエリの方が速いことが確認されているため、特定バージョンでのパフォーマンス悪化は想定されていません。
- スキーマキャッシュダンプや
- MariaDB 特有の互換性:
- カタログ経由によるデフォルト値の扱いが改善されていますが、基本的には「より正しい情報に近づく方向」であり、互換性破壊ではなくバグ修正寄りの変更です。
- もし MariaDB + TEXT/BLOB デフォルト値を多用しているアプリがある場合は、スキーマダンプ結果や
Column#defaultの値が従来と同じかを一度確認すると安心です(挙動は維持されている想定)。
- SQLite は実質的に挙動維持:
- API レベルで複数テーブルに対応しただけで、内部的には各テーブルを順に読むだけなので、SQLite 環境では性能差はほぼありません。
- 参考情報 (あれば)
- 関連 PR:
- #58421, #58488 — 同様に「スキーマ情報をまとめて読む」ための前段階の変更。
- 実装箇所:
activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rbactiverecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rbactiverecord/lib/active_record/connection_adapters/postgresql_adapter.rbactiverecord/lib/active_record/connection_adapters/schema_cache.rbactiverecord/lib/active_record/schema_dumper.rb- テスト:
activerecord/test/cases/connection_adapters/schema_statements_test.rb
- CHANGELOG:
activerecord/CHANGELOG.mdに本変更が記載されており、Rails のリリースノートにも掲載される見込みです。
#58495 Fix typo Rails Guide: Active Record Encryption 3.7 Fixtures [ci skip]
マージ日: 2026/8/16 | 作成者: @AmaraFinbarrs
概要 (1-2文で)
Active Record Encryption ガイドの「3.7 Fixtures」節における英文の文法ミスを修正したドキュメント専用の PR です。コードや挙動には一切変更がなく、文書の可読性・正確性のみが向上しています。変更内容の詳細
対象ファイル:guides/source/active_record_encryption.md
「3.7 Fixtures」章の冒頭の英文が、以下のように修正されています。
- To allow your tests can use plain text values...
+ To allow your tests to use plain text values...元の文は文法的に不自然("allow A can do B" は誤用)だったため、正しい構文である "allow A to do B" に修正されています。
Rails Guides はそのまま英語で読まれることも多く、ガイドを参照しながら実装・学習する開発者にとって、文法的に自然な英語になるように整備した変更です。
- 影響範囲・注意点
影響範囲
- Rails のフレームワーク本体や Active Record Encryption の挙動には一切影響しません。
- CI を回さないため
[ci skip]が付与された、ドキュメント専用の変更です。 - Active Record Encryption の Fixtures に関する説明の英語が正しくなり、ガイドの可読性・理解しやすさがわずかに改善されます。
注意点
- アプリケーションコードや設定を変更する必要はありません。
- ガイドのリンクや章構成などには変更がないため、既存のドキュメント参照(URL や節番号)もそのまま有効です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58495
- 対象ガイド: Active Record Encryption ガイド「3.7 Fixtures」節
- 一般的な英文法としても "allow your tests to use ..." が正しい表現であり、英語ガイドの品質向上の一環と言えます。
#58491 Fix Relation documentation return values
マージ日: 2026/8/16 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Relationのドキュメントで誤っていた戻り値の説明とコード例が修正されました。touch_allの戻り値とcreateのサンプルコードが、実際の挙動に沿うように整合性を取った変更です。
- 変更内容の詳細
touch_all のドキュメント修正
問題点
Relation#touch_allは内部的にupdate_allを呼び出しており、実際には「更新された行数 (Integer)」を返します。
しかし、これまでのドキュメント例では、touch_allが UPDATE文の SQL 文字列を返すかのような例になっていました。修正内容
- ドキュメント内の
touch_allの使用例 4つすべてについて、- 「戻り値として UPDATE 文の文字列」が出てくるような例
をやめ、 - 更新件数(整数)を返すことが分かるような例
に修正。
- 「戻り値として UPDATE 文の文字列」が出てくるような例
- これに合わせて
update_allまわりのテスト (activerecord/test/cases/relation/update_all_test.rb) も、ドキュメントに沿う形で微修正されました(コメントや期待値の表現調整レベルと思われる)。
- ドキュメント内の
※具体的なコードは PR 本文からの抜粋がないため概略のみですが、たとえば:
# 以前(イメージ)
User.where(active: true).touch_all
# => "UPDATE \"users\" SET \"updated_at\" = '...'" # のような誤った例
# 修正後(イメージ)
User.where(active: true).touch_all
# => 3 # 更新されたレコード件数というように、「SQL文字列が返る」風の例が「行数が返る」説明に改められています。
create のドキュメント修正
問題点
Relation#createのドキュメントで、users.create(name: 'fxn')の結果オブジェクト(戻り値)を使った例として書いたつもりのコードが、
実際には 引数なしのusers.create呼び出し に結果が「くっついて」見える形になっており、挙動を誤解させる状態になっていました。誤解されうる点
スコープがusers = User.where(name: 'scoped')などとなっている場合、users.createを 引数なしで呼ぶと、where で指定した条件がそのまま属性として使われます。
つまり、その例のまま書くと:- 本来伝えたかった「
users.create(name: 'fxn')の戻り値を使う」説明ではなく、 - 「スコープされた name を使ってレコードを新規作成する
users.create」の呼び出しになってしまう。
- 本来伝えたかった「
修正内容
users.create(name: 'fxn')という呼び出しの戻り値を変数に代入するか、- あるいはその戻り値を正しく参照する形
になるよう、サンプルコードが修正されています。 - 結果として、
- 「scoped name で意図せずレコードを作ってしまうコード」 にはならず、
- 「
createの戻り値(新しく作成されたレコード)をどう扱うか」という主旨が明確になりました。
- 影響範囲・注意点
- ランタイム挙動への影響はなし
変更はドキュメントとドキュメント整合用のテストのみであり、Relation#touch_allやRelation#createの実装・挙動自体は変わっていません。 - 既存コードの修正は不要
- すでに
touch_allの戻り値を「更新件数 (Integer)」として扱っているコードは、従来どおりそのまま動作します。 createに関しても、ドキュメントのサンプルが直っただけなので、本番コードへの影響はありません。
- すでに
- 注意点
- これまでドキュメントの例を参考に「
touch_allは SQL 文字列を返す」と誤解していた場合、実際は整数が返る点を改めて確認してください。 - スコープ付きの
Relation#createを引数なしで呼ぶと、スコープ条件が属性として使われる(例:where(name: "foo").create→ name: "foo" のレコードができる)という挙動を、改めて意識しておくとよいです。
- これまでドキュメントの例を参考に「
- 参考情報 (あれば)
- 対象クラス:
ActiveRecord::Relation- メソッド:
#touch_all,#update_all,#create
- メソッド:
- 変更ファイル:
activerecord/lib/active_record/relation.rbactiverecord/test/cases/relation/update_all_test.rb
- 関連ドキュメント(英語・最新版):
- Rails API Doc: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html
(今後この PR の内容が反映される想定)
- Rails API Doc: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html
#58488 Read the schema cache's primary keys and indexes for many tables at once
マージ日: 2026/8/15 | 作成者: @ngan
- 概要 (1-2文で)
SchemaCache#add_allが、テーブルごとに個別に問い合わせていた「主キー」と「インデックス」の読み込みを、複数テーブルをまとめて取得する形に変更し、スキーマキャッシュのダンプ時の SQL ステートメント数を大幅に削減する変更です。キャッシュされる内容や順序は従来と完全に同一で、パフォーマンス改善のみが目的です。
- 変更内容の詳細
何をしているか
対象:
ActiveRecord::ConnectionAdapters::SchemaCache#add_all以前の挙動:
add_all(tables)の内部で、各テーブルごとにadd(table)を呼び出していたadd(table)はそのテーブルの- 主キー (
primary_key) - カラム情報 (
columns) - インデックス (
indexes) をそれぞれ個別のクエリで取得
- 主キー (
- 結果として、「テーブル数 × 3種類(PK / columns / indexes)」分のクエリが発行されていた
今回の変更後:
add_all内で、まず「テーブル一覧」を引数に- すべてのテーブルの主キー
- すべてのテーブルのインデックス を“まとめて”問い合わせる
- その結果をキャッシュに格納したあとで、各テーブルについては「カラム情報」だけを個別に読む
- つまり、主キー・インデックスについては「1回でまとめて読む」ようになった
擬似コードイメージ:
def add_all(tables)
# 変更後: 主キーとインデックスをまとめ読み
primary_keys_by_table = connection.primary_keys_for_tables(tables) # イメージ
indexes_by_table = connection.indexes_for_tables(tables) # イメージ
primary_keys_by_table.each do |table, pk|
# 単一 PK は String、複合 PK は Array として保存(従来と同じ形)
@primary_keys[table] = pk.is_a?(Array) && pk.size == 1 ? pk.first : pk
end
indexes_by_table.each do |table, idxs|
@indexes[table] = idxs
end
# カラムは従来通りテーブルごとに取得
tables.each do |table|
add_columns_for(table) # 従来の columns 読み出し処理
end
end※実際のコードはアダプタ依存のメソッド呼び出しを行っていますが、概念的にはこのような流れです。
主キーの型の扱い
add_allが主キー情報を保存する際のフォーマット:- 単一カラム主キー:
String - 複合主キー:
Array<String>
- 単一カラム主キー:
- これは、もともと
SchemaStatements#primary_keyが返していた形と一致しており、既存のaddの挙動と整合するようにしている - この両パターン(単一 PK / 複合 PK)はテストでカバーされている
バッチ取得の前提
- 事前にマージされている PR (#58421) により、
- スキーマリーダー側(アダプタ)は「テーブルのリスト」を渡して主キー・インデックスをバッチ取得できるようになっている
- この PR はそれを SchemaCache 側で利用する follow-up で、「キャッシュ構築時にバッチ API を使う」ようにするもの
- 影響範囲・注意点
パフォーマンスへの影響
- MySQL 8.0 / テーブル204個の環境でのステートメント数:
- 変更前: 615
- 変更後: 209
- 615 ≒ 204 * 3(PK / columns / indexes)に対応
- 209 ≒ 204(columns) + 2(PK と indexes のカタログクエリ各1)
- つまり:
- カラム読み出しはテーブルごとのまま(変更なし)
- 主キー・インデックスは 1 回のカタログクエリで一括取得
- カラム情報が依然としてクエリの大半を占めており、これをバッチ化する話は別途進行中とのこと
アダプタごとの挙動差
- MySQL / PostgreSQL / Trilogy など、「複数テーブル分を一度に読めるカタログ API」を持つアダプタ:
- 実際にクエリ発行数が減る(上記 MySQL の例のような効果)
- SQLite など「複数テーブル分のカタログ API を持たない」アダプタ:
- 内部でテーブルごとにループして同じだけクエリを発行するため、実質的なステートメント数の削減はない
- ただし、SchemaCache 側の呼び出しは「1回のまとめ呼び出し」になっているので、インターフェースは統一されている
キャッシュ内容への影響(互換性)
- 重要なポイント: 生成されるスキーマキャッシュの中身は、以前とビットレベルで同一
- 検証内容:
- 単一テーブル読み出し vs リストで読み出しでキャッシュを作り、
- 以下のインスタンス変数をすべて比較:
@columns@columns_hash@primary_keys@data_sources@indexes@version
- さらに
ColumnやIndexDefinitionは==を実装していないため、「各属性まで」比較 - sqlite3, trilogy, mysql2, postgresql で全て一致することを確認済み
- インデックス順序:
- 注意されがちな点なので明示:
schema.rbと違い、キャッシュは「返ってきた順序の array」をそのままシリアライズする- しかし
SchemaCache#indexes自体はconnection.indexes(table_name)の結果をそのまま使っており、- #58421 時点ですでに「単一テーブルもリストも同じカタログクエリ経由」になっている
- 従って今回の変更で順序が変わることはない
- 具体例:
- MySQL で
zzz, aaa, mmm, bbbの順に index を定義しても、 - per-table パスでも既にアルファベット順に返していたことを確認済みであり、
- 新旧で順序が変わらないことを確認済み
- MySQL で
- 注意されがちな点なので明示:
呼び出し側との関係
SchemaCache#add_allの呼び出し元はSchemaReflection#dump_toのみdump_toは「空の SchemaCache」に対して実行される- そのため、
add_all内で「常に」主キー・インデックスを事前フェッチしても安全(既存キャッシュの上書き等の問題はない)
SchemaCache#add自体は変更されておらず、テーブル単位でのメモ化も従来通り- 1 テーブルを繰り返し読むケースでは、これまで通り追加コストは発生しない
エッジケース: 空のデータベース
- テーブルが 0 件のケース:
- クエリは発行されず、エラーにもならない
- これは #58421 の
test_an_empty_list_reads_nothingによって、リーダー側の挙動がテストで保証されている
- 参考情報 (あれば)
- 関連 PR:
- #58421: スキーマリーダー(アダプタ側)に「テーブルリストを渡して主キー・インデックスをまとめて取得」する能力を追加
- 変更ファイル:
activerecord/lib/active_record/connection_adapters/schema_cache.rbadd_allの実装変更(バッチ取得の導入)
activerecord/test/cases/connection_adapters/schema_cache_test.rb- 新しいバッチ読み出しの形(単一 PK / 複合 PK)を含むテスト追加
- テスト状況:
- sqlite3: 9,713 tests
- trilogy: 9,863 tests
- postgresql: 10,592 tests
- いずれもフルスイートで失敗なし
#58487 Fix the CollectionProxy#delete_all documentation return value
マージ日: 2026/8/15 | 作成者: @55728
- 概要 (1-2文で)
CollectionProxy#delete_allのドキュメントが、実際の挙動(削除件数を返す)と食い違っていたのを修正する PR です。特に:nullify戦略のサンプルが「レコード配列を返す」かのように書かれていた点を、正しい戻り値の説明に合わせました。
- 変更内容の詳細
対象メソッド:
ActiveRecord::Associations::CollectionProxy#delete_all実際の挙動:
delete_allは「削除されたレコード数(Integer)」を返す。- これは
:nullify戦略を含む全ての削除戦略で同じ。
問題だった点:
ドキュメント中の
:nullifyのサンプルが以下のように、「削除されたレコードの配列を返す」かのような例になっていた:rubyperson.pets.delete_all # => [ # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, # #<Pet id: 2, name: "Spook", person_id: 1>, # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # ]一方、テストコードでは明確に「件数を返す」ことを前提にしている:
ruby# activerecord/test/cases/associations/has_many_associations_test.rb assert_equal count, firm.dependent_clients_of_firm.delete_all(:nullify)
この PR の修正内容:
CollectionProxy#delete_allのドキュメント上の説明文を修正し、戻り値が「削除されたレコード数 (Integer)」であることを明示。以前のコミットで
:destroyと:delete_allの例から「配列を返す」という表現は削除されていたが、delete_all用の正しい戻り値の説明が抜けていたため、それを補完。これにより、近接する
#delete/#destroyのドキュメントとの整合性がとれる:ruby# #delete # ... then it will follow the default strategy. Returns an array with the # deleted records. # #delete_all (今回修正後) # ... then it will follow the default strategy. Returns the number of deleted # records.
補足:
CollectionProxy上の 4 つの削除系メソッドのうち、delete_allだけがレコード配列ではなく削除件数を返す仕様。- 古いサンプルがこれを誤解させる形になっていた、というのが今回の修正対象。
- 影響範囲・注意点
- 実装コードは変わっておらず、挙動そのものは従来通りで、ドキュメントのみの修正です。
- 影響範囲:
CollectionProxy#delete_allを利用するアプリケーションコード自体への影響はなし。- ただし、ドキュメントを信じて「配列が返る」と誤解していた場合は、仕様理解を修正する必要があります。
- 注意点:
collection.delete/collection.destroyは「削除されたレコードの配列」を返すのに対し、collection.delete_allは「削除件数 (Integer)」のみを返す点を区別すること。- 特に
:nullify戦略使用時でも戻り値は件数であり、関連先レコードのオブジェクトは返ってこないため、削除対象を追跡したい場合は事前にto_aなどで取得しておく必要があります。
- 参考情報 (あれば)
- 対応 PR: https://github.com/rails/rails/pull/58487
- 関連する過去コミット:
:destroy/:delete_allの古い配列返却例を削除したコミット:9534006c47,02d3a25361
delete_allの戻り値に関する過去の混乱に触れている issue: #14546
#57852 Freeze configuration
マージ日: 2026/8/15 | 作成者: @gmcgibbon
- 概要 (1–2文で)
Rails の Active Record と Action Pack の一部設定値を、アプリケーション初期化後に freeze(必要な Ruby では make_shareable)することで、Ractor 間で安全に参照できるようにした PR です。これにより、初期化後に特定の設定を動的に書き換えることは基本的にできなくなります。
- 変更内容の詳細
2-1. 対象となる設定の「凍結」
Action Pack (ActionDispatch) と Active Record の以下の設定が、after_initialize のタイミングで freeze されるよう Railtie が変更されています。
Action Pack / ActionDispatch
ActionDispatch::ExceptionWrapper.rescue_responsesActionDispatch::ExceptionWrapper.rescue_templatesActionDispatch::ExceptionWrapper.wrapper_exceptionsActionDispatch::ExceptionWrapper.silent_exceptions
Active Record
ActiveRecord.query_transformers
これらはいずれも Hash や Array などミュータブルなオブジェクトであり、従来はアプリケーション起動後でも上書き・変更できていましたが、本 PR によって 初期化完了後は凍結されるようになっています。
実装的には、Railtie の config.after_initialize フック内で、Ruby 4.0 以降の環境では make_shareable、それ以前では freeze に相当する処理を呼び出している形です(PR 説明より)。
※実際のコード上は make_shareable を Ruby 4.0+ で呼ぶようなガードが入る想定。
擬似コードイメージ(Action Pack 側):
# actionpack/lib/action_dispatch/railtie.rb
config.after_initialize do |app|
wrapper = ActionDispatch::ExceptionWrapper
[wrapper.rescue_responses,
wrapper.rescue_templates,
wrapper.wrapper_exceptions,
wrapper.silent_exceptions].each do |config_value|
# Ruby 4.0+ なら make_shareable、それ以前は freeze など
config_value.make_shareable if config_value.respond_to?(:make_shareable)
end
endActive Record 側も同様に、ActiveRecord.query_transformers を after_initialize で共有可能オブジェクトにしています。
2-2. Ractor 対応のための措置
Motivation に明記されているように、この変更の主目的は **「非メイン Ractor からこれらの設定を参照できるようにする」**ことです。
Ruby の Ractor では、Ractor 間で共有できるのは「shareable なオブジェクト」に限られるため、
- アプリケーション起動中に設定を組み立てる
- 起動完了後は不変として扱う
- その代わり Ractor 間で安全に読み取りできる
というライフサイクルに設計を切り替えています。
2-3. テストの追加
railties/test/application/configuration_test.rb にテストが追加され、
「アプリケーション初期化後に対象の設定が凍結(または shareable)になっていること」を検証しています。
- 影響範囲・注意点
3-1. 初期化後の動的変更が基本的にできなくなる
これまで以下のようなコードで「起動後に例外マッピングをいじる」「query_transformers を後から追加する」といったことをしていた場合、FrozenError が発生する可能性があります。
例: 以前はできていたかもしれないコード
Rails.application.config.to_prepare do
# これまでは起動後でも変更できたケースがある
ActionDispatch::ExceptionWrapper.rescue_responses[MyCustomError] = :not_found
end本 PR により、after_initialize 完了時点で rescue_responses が凍結されるため、to_prepare の実行タイミングや、各種 initializer の順序によっては、
- ハッシュ自体が freeze された後に
.[]=しようとしてFrozenError - Ractor 間共有のために
make_shareable済みで変更禁止
となることがあります。
同様に、Active Record 側で以下のようなことをしている場合も注意が必要です。
ActiveRecord.query_transformers << MyCustomTransformer
# => 初期化後に実行すると FrozenError になる可能性対策:
- これらの設定変更は、
config/application.rbやconfig/initializers/*.rbなど、
Rails 初期化完了前 (after_initializeより前) に完了するように移動する必要があります。 - 特に
to_prepareやエンジン/マウント時の late initializer などで変更している場合は確認・修正が必要です。
3-2. ライブラリ・エンジン作者への影響
Rails エンジンやミドルウェア、外部 gem が、これらの設定を「アプリ起動後」に書き換えている場合、それらのライブラリが動かなくなる/警告やエラーが出る可能性があります。
ExceptionWrapper.rescue_responsesに独自エラーをマッピングしているミドルウェアquery_transformersを使ったクエリ書き換え系 gem
などは、initializer の実行タイミングを確認・調整する必要があります。
3-3. Ractor を使うアプリでのメリット
一方で、Ractor を積極的に使うアプリケーションや、今後並列実行を活用したいアプリにとってはメリットがあります。
- 例外ハンドリング設定や query_transformers を、Ractor 間で安全に共有できる
- 「ある Ractor が設定を書き換えてしまい、別の Ractor の挙動が変わる」といったレースコンディションを防止できる
- 設定オブジェクトが不変であることが前提になるため、デバッグや推論がしやすくなる
- 参考情報 (あれば)
- Ruby における Ractor と shareable オブジェクトの仕様:
- Ruby 公式ドキュメント「Ractor」(英語)
- 類似の変更は、今後他の Rails コンポーネントの設定にも広がる可能性があります。
この PR ではActiveRecord.query_transformers以外の AR 設定はまだ対象外ですが、
説明文に「we may need to add more later」とあるため、
今後も「設定は起動時に確定し、その後は不変」という方向性が強まると考えておくと安全です。
#58476 Make Action View subscribers ractor safe
マージ日: 2026/8/14 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Action View のログ購読クラス(LogSubscriber/StructuredEventSubscriber)でRails.rootの扱いを Ractor セーフにするため、Rails.rootを「遅延メモ化」ではなく「初期化時に確定」する形に変更した PRです。これにより、マルチ Ractor 環境でも Action View のログ・イベント購読が安全に動作することを意図しています。
- 変更内容の詳細
背景
- これまで
ActionView::StructuredEventSubscriberやActionView::LogSubscriberでは、Rails.rootを必要になったタイミングでメモ化(@root ||= Rails.rootのような形)して使っていたと考えられます。 - Ractor 環境では、メイン Ractor で生成・変更されるオブジェクトを、後から別 Ractor で「初回代入」したり「再代入」したりするようなパターンは安全でない場合があります。
- そこで、「アプリケーション全体で Ractor セーフ / frozen にされる想定の
Rails.rootを、メイン Ractor の初期化時にインスタンス変数へ確定代入しておく」ように修正しています。
主なコード上の変更点(概念的な説明)
1) ActionView::LogSubscriber の root 取り扱い
変更前(イメージ):
class ActionView::LogSubscriber < ActiveSupport::LogSubscriber
# ...
private
def root
@root ||= Rails.root
end
end変更後(イメージ):
class ActionView::LogSubscriber < ActiveSupport::LogSubscriber
def initialize(*)
super
@root = Rails.root # 初期化時にセット
end
private
attr_reader :root
endポイント:
Rails.rootを「メソッド呼び出し時に毎回/初回だけ取得する」のではなく、「インスタンス生成時に一度だけ確定」する形へ。- これにより、
@rootインスタンス変数は Ractor 間で共有される読み取り専用の frozen オブジェクトを参照することが前提になり、Ractor セーフになります。
2) ActionView::StructuredEventSubscriber の root 取り扱い
構造は LogSubscriber と同様で、StructuredEventSubscriber も Rails.root を初期化時にインスタンス変数へ代入する形に変更されています。
変更前(イメージ):
def root
@root ||= Rails.root
end変更後(イメージ):
def initialize(*)
super
@root = Rails.root
end
private
attr_reader :rootこちらも同じく、遅延メモ化を廃止し、Ractor セーフな初期化時代入に統一しています。
3) actionview/lib/action_view/railtie.rb の変更
Railtie で Action View 関連の subscriber をセットアップする際の初期化処理に手が入っており、
- サブスクライバインスタンス生成のタイミング
- あるいは
Rails.rootへの依存が、Ractor セーフになるような順序
が保証されるようになっています。
ここでは、Rails.application の初期化フェーズ(当然メイン Ractor 上)で subscriber のインスタンスを作り、その時点で @root へ代入されるような流れになっていると考えられます。
4) テストの大幅な更新
log_subscriber_test.rbとstructured_event_subscriber_test.rbのテストが大きく書き換えられています(+112/-150、+164/-205)。- 主な目的は:
- 遅延メモ化ではなく「初期化時セット」に合わせたテスト仕様の変更
- ルートの扱いが Ractor セーフであること、及び変わらず正しいログ/イベント出力になることの確認
- 行数としては減っており、テスト構造が整理・簡潔化されている可能性も高いです。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは主に以下です:
ActionView::LogSubscriberによるログ出力(テンプレートレンダリングのログなど)ActionView::StructuredEventSubscriberによる構造化イベントの購読・出力
- それらが参照している
Rails.rootが:- 以前: 初回アクセス時に
Rails.rootを取得していた - 現在: subscriber インスタンス生成時に取得される
という点が変わります。
- 以前: 初回アクセス時に
何が変わるか / 開発者としての注意点
Rails.rootの「確定タイミング」が早まる- サブスクライバが生成された時点の
Rails.rootが使われ続けます。 - 通常 Rails アプリでは
Rails.rootは起動後に変化しないため、実務上は問題にならないケースがほとんどです。 - 「ランタイムに
Rails.rootを差し替える」といった特殊なことをしている場合は挙動が変わる可能性があります(そのような使い方自体が非典型ですが)。
- サブスクライバが生成された時点の
Ractor 環境での安全性向上
- アプリケーション側で
Rails.rootを frozen / Ractor セーフなオブジェクトとして扱うことが前提になっています。 - Rails の初期化処理(initializer)はメイン Ractor 上で実行されるため、この段階で
Rails.rootが決定・凍結されていれば、後続の Ractor から読まれるだけ、という設計になります。 - Ractor を使った並列実行(Action Cable の一部実装や、将来のマルチ Ractor Web サーバ実装など)で、Action View のログ/イベントまわりが安全に動くようになります。
- アプリケーション側で
ライブラリ・拡張側の考慮
ActionView::LogSubscriberやStructuredEventSubscriberを継承・拡張している独自クラスや gem が、Rails.rootを遅延メモ化に依存していた場合、挙動が変わり得ます。- ただし、そのような依存(「後で root が切り替わること」を前提にしているなど)は設計として想定されていないため、一般的なアプリ/ライブラリには実害は少ないはずです。
- 参考情報 (あれば)
- 類似 PR:
- 説明文でも触れられているとおり、
https://github.com/rails/rails/pull/58467と同様の方針で、他コンポーネントでもRails.rootの扱いを Ractor セーフにする作業の一環です。
- 説明文でも触れられているとおり、
- Ractor セーフティに関する前提:
- Ractor 間で共有するオブジェクトは frozen または特殊なスレッドセーフオブジェクトである必要がある。
- Rails はアプリケーションの「グローバルな設定値」(
Rails.rootや一部設定オブジェクト)を frozen にして共有しやすくする方向で調整が進んでいます。
- この PR ではテスト追加フラグは OFF(
[ ] Tests are added or updated)になっていますが、実際にはテストファイルが大きく修正されているため、「新規テストケース追加」ではなく「既存テストの更新・リファクタリング」として扱われていると見られます。
#58483 Freeze the Controller default_url_options
マージ日: 2026/8/14 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Controller / View で使われるdefault_url_optionsを「破壊的に変更できないように凍結(freeze)する」変更です。
暗黙のミューテーションによる予測しづらいリダイレクト/URL生成の挙動を防ぎ、将来的な Ractor 対応も見据えた安全性向上が目的です。
- 変更内容の詳細(あればサンプルコードも含めて)
対象となる default_url_options
- この PR が扱うのは Controller / View 側 の
default_url_options(ActionController::Base経由で使われるもの)です。 RouteSet(Rails.application.routes.default_url_optionsなど)にある別のdefault_url_optionsは対象外です。
何が問題だったか
Rails ガイドでは、URL のデフォルトパラメータを変えたい場合は default_url_options メソッドをオーバーライドすることが推奨されていますが、実装的には次のようなコードも「動いてしまう」状態でした。
class MyController < ApplicationController
default_url_options[:lang] = "en"
endこうした「ハッシュのミューテーション」は公式にはドキュメント化されておらず、かつ以下のような「超わかりづらい挙動」を引き起こします。
class ApplicationController
def hello_world
redirect_to(root_path)
end
end
class MyController < ApplicationController
default_url_options[:lang] = "en"
end- アプリ起動直後などで
MyControllerがまだ autoload されていない状態で/hello_worldにアクセスすると、root_pathは/になります。 - その後、
MyControllerに初回アクセスなどが発生し autoload されると、上記のdefault_url_options[:lang] = "en"が実行され、共通のdefault_url_optionsが書き換えられる。 - 再度
/hello_worldにアクセスすると、今度はroot_pathが/?lang=enになります。
つまり、クラスの autoload タイミングによって同じアクションから生成される URL が変わるという、とてもデバッグしづらい問題が生じます。
この PR で行われたこと
- Controller / View 側で利用される
default_url_optionsが デフォルトでfreezeされるように変更されました。 - これにより、次のようなコードは(実行時に)エラーになります。
class MyController < ApplicationController
default_url_options[:lang] = "en" # => FrozenError (can't modify frozen Hash)
end- 正しいカスタマイズ方法は従来通り「メソッドオーバーライド」です。
class MyController < ApplicationController
private
def default_url_options
super.merge(lang: "en")
end
endテストの変更内容から読み取れるポイント:
default_url_optionsが freeze されることを前提に、テスト内のdefault_url_options操作が- 直接ミューテーションから
mergeなどの非破壊的操作に 書き換えられている箇所があります。
- Action Mailer / Action Pack / Action View の各テストで、URL ヘルパのデフォルトオプションの扱い方が「ミューテーションを前提にしない形」に整えられています。
- 影響範囲・注意点
影響があるケース
以下のように default_url_options を「ハッシュとして」直接いじっているアプリケーションは、今回の変更で壊れます。
# 悪い例(今回の PR で壊れる)
class ApplicationController < ActionController::Base
default_url_options[:locale] = I18n.default_locale
end
# 悪い例(他の箇所でミューテーションするパターン)
Rails.application.config.to_prepare do
ApplicationController.default_url_options[:locale] = "ja"
endこのようなコードは FrozenError を起こすようになるので、メソッドオーバーライドに書き換える必要があります。
# 良い例
class ApplicationController < ActionController::Base
private
def default_url_options
super.merge(locale: I18n.default_locale)
end
endポイント:
- 今までたまたま動いていただけで、そもそもドキュメント非推奨のやり方だったため、破壊的変更とはいえ「実際にこのパターンを使っているアプリは少ないはず」という判断です。
- ただし、レガシーコードやメンテされていないコードベースでは潜んでいる可能性があります。
Ractor 安全性との関係
- Ractor(並列実行機構)では、オブジェクト共有時にミューテーションが厳しく制限されます。
default_url_optionsを不変(frozen)にすることで、マルチ Ractor 環境でも安全に共有できるようにする狙いがあります。- 今回の PR は、Ractor 対応に向けた足場づくりの一部という位置づけです。
- 参考情報 (あれば)
- Rails ガイド(Action Controller Overview –
default_url_options)
https://github.com/rails/rails/blob/d72a482ea45898909f6be38933cb3db479bbfa4a/guides/source/action_controller_overview.md#L319-L332
→default_url_optionsをオーバーライドして使う公式なやり方が載っています。 - この PR 番号: #58483 「Freeze the Controller
default_url_options」
→ 変更の目的は- 予測しづらい挙動(autoload タイミング依存の URL 変化)の排除
- Ractor 安全性の向上 の2点。
#58482 Restore unshareable_proc_action with ensure in default scoping test
マージ日: 2026/8/14 | 作成者: @ngan
- 概要 (1-2文で)
DefaultScopingTest#test_default_scopes_are_ractor_shareable内でグローバル設定ActiveSupport::Ractors.unshareable_proc_actionを一時的に変更する際、例外発生時にも必ず元に戻るように、ActiveSupport::Ractors.with(内部でensureを使う仕組み)を使うように修正したテスト専用の変更です。これにより、テスト失敗時に Ractors 周りの設定リークが起きて後続テストが巻き添えになる問題を防ぎます。
- 変更内容の詳細
これまでの問題点
元のテストでは、Ractors 周りの挙動を確認するためにグローバル設定を一時的に変更していました:
old = ActiveSupport::Ractors.unshareable_proc_action
ActiveSupport::Ractors.unshareable_proc_action = :raise
model = Class.new(ActiveRecord::Base) do
# ...
default_scope -> { ractor_safe }
# ...
end
ActiveSupport::Ractors.unshareable_proc_action = oldここで問題なのは:
Class.newブロック内のdefault_scope呼び出しは、まさにunshareable_proc_action = :raiseをテストするコードパス- したがって、ここで例外が発生する可能性がある
- 例外が発生すると、最後の
ActiveSupport::Ractors.unshareable_proc_action = oldが実行されず、:raiseが残り続ける - その結果、他のテストでも
unshareable_proc_action = :raiseが効き続けてしまい、本来関係ないテストが失敗する(原因追跡が困難になる)
PR 説明でも「一つの失敗が無関係な失敗を量産して、本当の原因を見えにくくする」と言及されています。
今回の修正内容
このグローバル設定の一時変更を、安全に行うために ActiveSupport::Ractors.with を利用するように変更しました。with は内部で ensure を使って値を必ず元に戻すラッパーです。
概念的には以下のような形になっています(実際のコードは多少異なりますがイメージとして):
ActiveSupport::Ractors.with(unshareable_proc_action: :raise) do
model = Class.new(ActiveRecord::Base) do
# ...
default_scope -> { ractor_safe }
# ...
end
endポイント:
ActiveSupport::Ractors.withブロックの開始時にunshareable_proc_actionを:raiseにセット- ブロック終了時に、例外が発生しても
ensureで必ず元の値に戻す - これにより「例外発生後に設定がリークする」ことを防止
さらに、この with で囲む範囲をモデル定義部分にだけ限定しています。PR 説明の通り:
Wrapping only the model definition keeps the original scoping intact:
capture_sqland both assertions still run with the setting the suite came in with, not with:raise.
つまり:
:raiseを有効にするのはClass.new(ActiveRecord::Base)~default_scopeの評価までに限定- その後に実行される
capture_sqlやassert群は、テストスイートが元々持っていたunshareable_proc_actionの設定のまま動く
この設計により、
- Ractor shareability のテストに必要な箇所だけ、意図的に危険な設定 (
:raise) にして - 検証が終わったらすぐ元の環境に戻す
というメリハリが付けられています。
一貫性の向上
ActiveSupport::Ractors.with や ensure を用いるパターンはすでに以下のテストで使われており、このテストだけが浮いている状態でした:
actionpack/test/dispatch/routing/route_set_test.rbactionpack/test/controller/caching_test.rbactivesupport/test/callbacks_test.rbactivesupport/test/ractors_test.rbactionpack/test/controller/new_base/middleware_test.rb
今回の変更で、Ractors 関連のテスト全体のスタイル・安全性が揃えられました。
- 影響範囲・注意点
影響範囲
- 変更は
activerecord/test/cases/scoping/default_scoping_test.rbのみで、本番コードには一切影響しません。 - テスト実行時の Ractors 設定の扱いがより安全になり、
- 「一つのテスト失敗が原因で、後続の多数のテストが Ractors 設定リークにより巻き添えで失敗する」
といった現象が起きにくくなります。
- 「一つのテスト失敗が原因で、後続の多数のテストが Ractors 設定リークにより巻き添えで失敗する」
- テストの安定性・デバッグ容易性が向上します。
- 変更は
注意点
- Ractors 関連に限らず、「グローバル設定をテスト内で一時的に変更する」場合は、
ensureでの復元- あるいは今回のような
withヘルパー
を使うべき、という Rails テストコードの実質的なガイドラインが強化された形です。
- 同様のパターンが他に残っている場合は、同じように
with/ensureに寄せるとよいです。
- Ractors 関連に限らず、「グローバル設定をテスト内で一時的に変更する」場合は、
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58482
- 当該テスト:
activerecord/test/cases/scoping/default_scoping_test.rb - 関連する既存の
ActiveSupport::Ractors.with利用箇所:actionpack/test/dispatch/routing/route_set_test.rbactionpack/test/controller/caching_test.rbactivesupport/test/callbacks_test.rbactivesupport/test/ractors_test.rbactionpack/test/controller/new_base/middleware_test.rb
#58421 Let the schema readers answer for many tables at once
マージ日: 2026/8/14 | 作成者: @ngan
- 概要 (1-2文で)
このPRは、スキーマダンプ時にテーブルごとにバラバラに発行していたメタデータ系クエリ(インデックス・外部キー・制約など)を「複数テーブルまとめて」取得できるようにし、大規模スキーマでのdb:schema:dump/db:migrateの時間とクエリ数を大幅に削減する変更です。出力されるスキーマファイルの内容は(バグ修正を除き)変わらず、内部の問い合わせ方法とAPIシグネチャが拡張されています。
- 変更内容の詳細
2-1. 複数テーブル版リーダーAPIの追加
以下のメソッドが「単一テーブル」だけでなく「テーブル配列」も受け取れるようになりました。
indexesprimary_keysforeign_keyscheck_constraintsexclusion_constraintsunique_constraints
挙動:
# 既存の単一テーブル形式(今まで通り)
connection.indexes(:users)
# => [ActiveRecord::ConnectionAdapters::IndexDefinition, ...]
# 新しい複数テーブル形式
connection.indexes([:users, :posts])
# => {
# "users" => [IndexDefinition, ...],
# "posts" => [IndexDefinition, ...],
# }ポイント:
- 引数が配列の場合: 戻り値は
"テーブル名文字列" => 定義の配列のハッシュ。 - 引数が単一の場合: 既存と同じく、そのテーブルの配列だけを返す。
- 「すべてのテーブル」という省略形は用意せず、「呼び出し側が明示的にテーブル一覧を渡す」という設計。
- 各メソッドは内部的には「配列対応の実装」に一本化されており、単一テーブル版はそれを薄くラップしているだけ。
サンプル実装イメージ:
def check_constraints(table_name)
result = fetch_check_constraints(Array(table_name).map(&:to_s))
table_name.is_a?(Array) ? result : result[table_name.to_s]
endこれにより、従来の「単一テーブル用と複数テーブル用で別実装」という二重実装がなくなり、メンテしやすくなっています。
2-2. スキーマごとのバッチ読み取り
- 一度の読み取りは1つのスキーマに限定されます。
["users", "posts"]のようにスキーマ非指定なら1回の問い合わせ。["a.posts", "b.posts"]のようにスキーマ付きならスキーマごとに分割され、2回の問い合わせ。
- SQLite はカタログから一括取得する仕組みがないため、外から見るとバッチAPIだが、内部ではテーブルごとにループする実装。
2-3. MySQL: information_schema を用いたバッチ取得とバグ修正
インデックスの取得方法の整理
これまでは環境によって SHOW KEYS FROM ... にフォールバックすることがあり、テーブルごとにクエリが発生していました。PR後は、できるだけ information_schema.statistics から複数テーブルをまとめて読むようになっています。
可用なカラムを環境ごとに切り替える実装:
optional_columns = ""
optional_columns << ", expression AS 'Expression'" if supports_expression_index?
if supports_disabling_indexes?
optional_columns << if mariadb?
", IF(ignored = 'NO', 'YES', 'NO') AS 'Enabled'"
else
", is_visible AS 'Enabled'"
end
endこれにより:
- MariaDB(
IGNOREDはあるがEXPRESSION/IS_VISIBLEはない) - MySQL 8.0.13 より前
といった環境でも、SHOW KEYS に戻らずにバッチ読み取りが可能になります。
検証結果(MariaDB 11.8):
- PR前後でスキーマダンプのバイト列が完全に一致。
- インデックス読み取りクエリ数が「テーブル数分 → 1回」に削減。
ALTER INDEX ... IGNOREDされたインデックスも「無効」として正しく解釈。SHOW KEYS FROMが「テーブルがないときに例外を投げる」ために置かれていたrescue StatementInvalidが不要になり、削除。
テーブルの照合順序(collation)取得のバグ修正
従来:
SHOW TABLE STATUS LIKE '<name>'で1行目を取っていたため、LIKE のワイルドカード (_, %) を含むテーブル名だと別テーブルにマッチして誤った collation を拾うバグがありました。
例:
ab_c(utf8mb4_0900_ai_ci)ab c(utf8mb4_general_ci)
SHOW TABLE STATUS LIKE 'ab_c' は LIKE マッチとして ab c を拾いうるため、ab_c に utf8mb4_general_ci が誤って適用される。
PR後:
information_schema.tablesから読むように変更し、パターンマッチを廃止。- この「潜在バグ」が修正されています。
2-4. PostgreSQL: 制約まわりをまとめて取得
- これまでは、排他制約・ユニーク制約を「インデックス名フィルタ用」「ダンプ用」でテーブルごとに二度問い合わせる構成だったものが、一度のカタログクエリでまとめて取得するよう整理。
- 実装はテーブル配列ベースの共通関数に一本化され、単一テーブル呼び出しもそこを通ります。
2-5. スキーマダンパーの変更
SchemaDumper/ MySQL・PostgreSQL 各アダプタのschema_dumperが、これらの新しい「複数テーブル」APIを利用するように変更。- ダンプ対象テーブルの一覧を事前に集約し、
schema_migrations,ar_internal_metadataSchemaDumper.ignore_tablesに含まれるもの を除外した「実際に出力するテーブル」だけをまとめて問い合わせるように。
2-6. 返り値の仕様
- どのテーブルにも必ずキーが存在します(インデックス等が1つもなくても、空配列を返す)。
- 呼び出し側は「存在しないキー(
nil)」を気にせずに繰り返し処理できる。
- 呼び出し側は「存在しないキー(
- 空配列を渡した場合:
- 「何もしない」ことが仕様です。
- データベース問い合わせも発生しません。
- テーブルが0件のデータベースをダンプするパスでも無駄なクエリが走らないように配慮。
2-7. 挙動変更(互換性に関わる部分)
空のテーブル名の扱い
- 以前は、MySQL の
foreign_keys/primary_keysだけが空文字を弾いてArgumentErrorを投げていた一方で、indexes(nil)はnilを返す- PostgreSQL / SQLite はどのリーダーでも
nilを返す
という不統一な状態でした。
- PR後:
- すべてのリーダー・アダプタで挙動が統一され、「空名で特別に
ArgumentErrorを投げる」という挙動は廃止。 - 空配列を許容して「何もしない」仕様を優先したため、
present?ベースのバリデーションは導入していません。
- すべてのリーダー・アダプタで挙動が統一され、「空名で特別に
- 以前は、MySQL の
PostgreSQL の
primary_keysが、存在しないテーブルで例外を投げなくなる従来:
sql'table_name'::regclassを使っていたため、存在しないテーブル名に対して
ActiveRecord::StatementInvalidが発生。PR後:
pg_classを直接フィルタする方式に変更。- 存在しないテーブルでも
[]を返すようになります。
他のメソッド(
indexes,foreign_keys)は元から「存在しないテーブル →[]」だったので、primary_keysもそれに揃えた形です。テストでも「例外を期待する」前提は存在していませんでした。
- 影響範囲・注意点
3-1. パフォーマンス面の影響
数値的な効果(著者環境の例):
- MySQL(約2,300テーブル)
- ステートメント数: 16,203 → 4,695(約 -71%)
- 壁時計時間: 16.1s → 5.0s
- PostgreSQL(約1,500テーブル)
- ステートメント数: 18,272 → 6,122(約 -66%)
- 壁時計時間: 3.7s → 1.4s
IN (...) によるテーブル名リストの長さも検証済み:
- MySQL(2,300テーブル)
- クエリサイズ: +69 KB
- クエリ時間: 58 ms → 69 ms(+11 ms)
→ 実用上問題ない程度。
- PostgreSQL(同条件)
- クエリサイズ: +23 KB
- クエリ時間: +6 ms 程度。
- 極端なケースとして 200,000 個のテーブル名を含む 5.9 MB クエリでも 0.22 秒程度で完了。
3-2. 既存コードへの互換性
基本的には:
- 既存の単一テーブル呼び出し (
connection.indexes(:users)等) はシグネチャも挙動も変わりません。 - スキーマダンプの出力も、バグ修正を除いてバイトレベルで同一になるよう確認済みです。
注意すべき変更点:
PostgreSQL +
primary_keysで、存在しないテーブルに対する挙動が変わる- 以前:
ActiveRecord::StatementInvalidを救って何かする、というコードがあれば、それはもう呼ばれません。 - 今後:
[]が返ります。 - ライブラリやアプリ側で「存在しないテーブルを検出する」ロジックに依存している場合は注意。
- 以前:
空のテーブル名引数に関するバリデーション削除
- 空文字(あるいは
nil)を渡して例外を期待していたテストがあれば失敗する可能性があります。 - そのような呼び出しは一般的にはバグなので、アプリ側で明示的にバリデーションするほうが望ましいです。
- 空文字(あるいは
MySQL の collation 取得バグ修正
_/%を含むテーブル名を使っていて、かつ以前の「誤った collation」に暗黙依存していた場合、今回正しい collation に変わることで挙動が変化しうる(とはいえ、依存しているケースはほぼ無いと考えられます)。
3-3. 自前アダプタ・プラグインを書いている場合
SchemaStatements を拡張している自前アダプタやプラグインで、以下を行っている場合は確認が必要です。
indexes/foreign_keys/primary_keysなどをオーバーライドしている- あるいは、それらに依存した独自のスキーマダンパーを実装している
対処方針:
- 公式アダプタと同様に、
- 「配列ベースで実テーブル群を読むメソッド(例:
fetch_indexes(table_names))」 - 「それをラップする単一/複数テーブル両対応メソッド(例:
def indexes(table_name) ... end)」 の2層構成にそろえるとよいです。
- 「配列ベースで実テーブル群を読むメソッド(例:
- 新しい API 仕様として
- 配列を受け取ったときは
"テーブル名" => 配列のハッシュを返す - 配列が空のときは DB に問い合わせない
- 存在しないテーブルでも
[]を返す(特に PostgreSQL のprimary_keys) を満たす必要があります。
- 配列を受け取ったときは
- 参考情報
- 変更ファイル例
activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rbactiverecord/lib/active_record/connection_adapters/mysql/schema_statements.rbactiverecord/lib/active_record/connection_adapters/postgresql/schema_statements.rbactiverecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rbactiverecord/lib/active_record/schema_dumper.rb
- テスト:
ActiveRecord::ConnectionAdapters::SchemaStatementsTest- 配列版 API の挙動(対象テーブルだけ読む、単一呼び出しとの一致、スケーリング特性、空配列時の無問い合わせ、スキーマ付き名前のキー)を網羅。
- ミューテーションテストで、スキーマグルーピングや主キー順序、修飾名の扱いなどが十分検証されています。
- 関連するアイデア:
- インデックス取得と同様の「カラム読み取りのバッチ化」も別PRで検討されており、このPRはその基盤の一部となる設計です。
#57973 Add support for the HTTP QUERY method
マージ日: 2026/8/14 | 作成者: @magnogouveia
- 概要 (1–2文で)
- Rails に HTTP の新メソッド QUERY(RFC 10008)が正式サポートとして追加され、
routes・requestオブジェクト・統合テストなど Action Pack 全体で GET 等と同様に扱えるようになりました。 - QUERY は「安全かつ冪等な、ボディ付きの GET 相当メソッド」として扱われ、複雑・大容量な検索条件を URL クエリ文字列ではなくリクエストボディで送る用途を想定しています。
- 変更内容の詳細
2-1. QUERY メソッドの追加と基本挙動
HTTP メソッドとしての登録
ActionDispatch::Requestに QUERY が既知メソッドとして追加HTTP_METHODSに RFC10008 ベースの定義を追加request.query?が新設request.request_methodは"QUERY"を返し、request.request_method_symbolは:queryを返すようになる
例:
# コントローラ内
def index
request.query? # => true (QUERY のとき)
request.request_method # => "QUERY"
request.request_method_symbol # => :query
end2-2. ルーティング (config/routes.rb)
- Journey の
VerbMatchers::VERBSにQUERYを追加 ActionDispatch::Routing::Mapperにqueryヘルパが追加
これにより GET 等と同じ感覚でルートを定義できます。
例:
Rails.application.routes.draw do
# 専用ヘルパ
query "search", to: "search#index"
# match 経由
match "filter", to: "search#filter", via: :query
# 複数メソッドに混在させることも可能
match "items", to: "items#index", via: [:get, :query]
endRails 7 以前では via: :query を書いてもマッチせず 405 相当になっていましたが、この PR により正常にマッチするようになります。
2-3. 統合テスト・コントローラテスト
統合テスト (ActionDispatch::IntegrationTest)
get/postなどと同様にqueryヘルパが追加されました。
例:
class SearchFlowsTest < ActionDispatch::IntegrationTest
test "complex filtering with QUERY" do
query "/search",
params: { filters: { status: "active", tags: ["paid", "priority"] } },
as: :json
assert_response :success
end
endas: :jsonなどの扱いは他メソッドと同様で、Content-Type ベースにボディがparamsにパースされます。
コントローラテスト (ActionController::TestCase)
optionsと同様、専用のショートカットメソッドは追加されていませんが、processがそのまま利用できます。
process(:index, method: "QUERY", params: { filters: { status: "active" } })2-4. Forgery Protection (CSRF 保護) の扱い
ActionController::RequestForgeryProtection 側の挙動変更:
- QUERY は GET/HEAD と同様「CSRF トークン検証の対象外」 として扱われます
- 理由:
- RFC 上 QUERY は safe & idempotent (状態を変更しない前提)
- HTML フォームからは QUERY を送信できない
- QUERY は CORS の safelisted method ではないため、クロスオリジンで送るには必ず preflight が必要(
OPTIONS事前確認) - 結果として CSRF 攻撃面は GET よりもむしろ狭い
コード的には GET/HEAD の扱いに QUERY を加えた形で、独立したテストも追加されています。方針を変えたくなった場合、この部分のみを戻して「QUERY にも CSRF トークンを必須」にすることも容易です。
2-5. パラメータパース
- 変更点: なし
- Rails のボディパースはもともと HTTP メソッドではなく Content-Type に基づいて動作しているため、JSON や
application/x-www-form-urlencoded等のボディを持つ QUERY リクエストも、従来コードのままでparamsに正しく展開されます。
例:
# QUERY /search
# Content-Type: application/json
# Body: { "filters": { "status": "active" } }
def index
params[:filters][:status] # => "active"
end2-6. SSL ミドルウェア / ナビゲーションヘルパ等
actionpack/lib/action_dispatch/middleware/ssl.rb- HTTP->HTTPS へのリダイレクトや安全性チェックの中で QUERY を GET/HEAD などと並べて正しく扱うように更新
ActionView::Helpers::NavigationHelpercurrent_page?等の判定で QUERY を含むような対応(GET に準じた safe method としての扱い)
詳細実装は小さいですが、「GET 相当の安全メソッド」として Rails 内の周辺機能にも自然に統合されています。
2-7. 変更されない/対象外と明示された事項
この PR では あえて扱っていない(今後の検討事項) も明示されています:
Accept-Queryレスポンスヘッダ- QUERY レスポンスに対する「コンテンツベースのキャッシュキー」(RFC 10008 §2.7)
resources/resourceルーティングとの自動統合(resources :itemsで生成されるデフォルト REST ルートに QUERY を足すかどうか、など)- メソッドオーバーライド機構 (
_methodパラメータ) での QUERY サポート - Rack 本体への
Rack::Request#query?追加(現在は Rails 側でのみquery?を定義)
- 影響範囲・注意点
3-1. 既存アプリへの影響
- 後方互換性はほぼ問題無し とされています。
- これまで QUERY リクエストは
ActionController::UnknownHttpMethodを投げて 405 相当で落ちていたため、「すでに QUERY を何か特別にハンドリングしていたコード」は基本的に存在しない想定です。 - この PR によって初めてアプリケーションコードに到達するようになるため、「何かのプロキシや外部クライアントが勝手に QUERY を投げていた」ようなケースがあると挙動が変わる可能性はありますが、一般には稀と考えられます。
- これまで QUERY リクエストは
3-2. Ruby / Rack / アプリケーション側コードへの影響
- ルーティングで
via: :queryを指定しても正常に動作するようになるため、新しく QUERY を使ったエンドポイントを安全に追加できます。 request.query?を利用した分岐が可能になったため、GET と QUERY を明確に分離した処理を記述できます。paramsの扱いは他メソッドと同様なため、アプリケーションコード側の JSON パースや Strong Parameters の記述はほぼそのまま流用できます。
3-3. CSRF / セキュリティ上の考慮点
- デフォルトでは QUERY は CSRF 保護の対象外 です。
- RFC10008 の設計上は「状態を変更しない安全メソッド」という前提ですが、実際のアプリケーション側で QUERY を「副作用のある操作」に使い始めると CSRF 保護の穴になりえます。
- 実装者視点での推奨:
- QUERY は 検索・フィルタリング・クエリ実行など、リソースの状態を変えない読み取り系 API に限定して使う
- もし QUERY を副作用のある操作に使う場合は
- CSRF を独自に検証する
- あるいは Rails の Forgery Protection 設定をカスタマイズして QUERY も検証対象に含めることを検討する
3-4. アプリケーションサーバ (Puma 等) との関係
- この PR は Rails 側のサポートのみ で、アプリケーションサーバの挙動は変えません。
- Puma はデフォルトでは 8 つの標準メソッドのみを許可しており、QUERY は
501 Not Implementedを返します。 - QUERY を使いたい場合、Puma の
supported_http_methodsオプションにQUERYを追加する 等、サーバ側の設定が別途必要です。
- Puma はデフォルトでは 8 つの標準メソッドのみを許可しており、QUERY は
- nginx / Node.js / Spring など、他エコシステムではすでにサポートが進みつつあることが PR で触れられていますが、Rails 単体ではサーバの制約を越えられない点に注意してください。
- 参考情報
- RFC 10008: HTTP QUERY Method
https://www.rfc-editor.org/rfc/rfc10008.html - IANA HTTP Method Registry
https://www.iana.org/assignments/http-methods/http-methods.xhtml - 提案スレッド (rails-core フォーラム)
Proposal: Support for the HTTP QUERY method (RFC 10008)
https://discuss.rubyonrails.org/t/proposal-support-for-the-http-query-method-rfc-10008/91255 - 関連実装・動向:
- Node.js: Core が v21.7.2 以降で QUERY をパース
- nginx: メインラインで QUERY をサポート
- Spring Framework: HTTP QUERY サポートの PR (spring-projects/spring-framework#34993)
この PR により、Rails で「GET 相当だがクエリをボディで送る」API デザイン(複雑な検索条件、GraphQL/JSON-RPC の読み取り等)が公式にサポートされるようになりました。
#58469 Remove "missing" workarounds from Active Record
マージ日: 2026/8/14 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
Active Record のmethod_missingベースの「missing 系ワークアラウンド」が削除され、6.1 時代の Marshal 互換のためだけに残っていたコードが整理されました。これにより、仮想属性やrespond_to?周りのパフォーマンスが向上しつつ、新形式でのMarshal.load互換性はテストで担保されています。
- 変更内容の詳細
背景
- Rails 6.1 では、Active Record オブジェクトの Marshal 形式が今とは異なっており、
- 属性メソッドが定義されていない状態でも Record がロードされうる
- そのため、
method_missing/respond_to_missing?を使った「属性がなくても動かすためのワークアラウンド」が必要だった
- 最新の Rails では 6.1 形式の Marshal サポート自体が削除 されており (#50594, #57382, #58462, #58466)、それに依存したワークアラウンドも不要になった、という整理 PR です。
実際の変更点
1) ActiveRecord::AttributeMethods からの削除
activerecord/lib/active_record/attribute_methods.rb から 36 行削除されています。内容的には以下のようなロジックが消えたと考えられます:
- 「属性メソッドが定義されていない」状態でも、
record.some_attributeをmethod_missingで拾って値を返すrecord.respond_to?(:some_attribute)をrespond_to_missing?で true にする
- Marshal から復元された「古い 6.1 形式の Record」に対して、属性メソッド未定義でもなんとか動かすための処理
これが削除されることで、挙動としてはより「素直な Ruby オブジェクト」に近くなります:
- 定義されていないメソッドは普通に
NoMethodError respond_to?も実際に存在するメソッド・動的属性のみを返す(ワークアラウンド分の“なんちゃって true”が消える)
2) テストの整理と置き換え
activerecord/test/cases/attribute_methods_test.rb:
method_missingベースのワークアラウンド挙動に依存していたテストが削除(56 行減、3 行追加)- 代わりに、実際のユースケース(Marshal 復元)をカバーする「統合的なテスト」側にシフト
activerecord/test/cases/marshal_serialization_test.rb:
- Marshal まわりのテストが 2 行追加
- 目的は「属性メソッドが定義されていない状態で
Marshal.loadしても、今の(6.1 非互換な)フォーマットにおいては正しく動く」ことを確認すること- PR 説明文からすると、元々 method_missing ワークアラウンドで隠れていたバグを、今の形式で再現・検証するテストとして書き直されている
3) パフォーマンス計測と結果
PR に含まれるベンチマークスクリプトの要点:
- 環境:
ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23] - 対象:
Postモデル (sqlite3 in-memory)rubyActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Base.lease_connection.create_table(:posts) { |t| t.string :title } class Post < ActiveRecord::Base; end row = Post.create!(title: "x") extra = Post.select("id, title, 10 as tenderlove").firstrow.title… 通常の属性メソッドextra.tenderlove… SELECT で追加した「仮想カラム」(10 as tenderlove)
計測ケース:
CASES = {
"title" => -> { row.title }, # 通常の属性メソッド
"tenderlove" => -> { extra.tenderlove }, # 仮想属性(select で追加)
"rt title" => -> { row.respond_to?(:title) }, # respond_to? (属性メソッド)
"rt to_hash" => -> { row.respond_to?(:to_hash) }, # respond_to? (プリフィルタで reject される)
"rt permitted?" => -> { row.respond_to?(:permitted?) },# '?' suffix パターン全スキャン
"rt tenderlove" => -> { extra.respond_to?(:tenderlove) }, # respond_to? (仮想属性)
}before(削除前)と after(削除後)での比較:
title- 20246365.6 i/s vs 20186471.5 i/s
→ 誤差範囲内で同等
- 20246365.6 i/s vs 20186471.5 i/s
tenderlove(仮想属性アクセス)- 2.18M i/s vs 1.30M i/s
→ 約 1.67 倍高速
- 2.18M i/s vs 1.30M i/s
rt title(respond_to?(:title))- 8.83M vs 8.83M i/s
→ 同等
- 8.83M vs 8.83M i/s
rt to_hash(respond_to?(:to_hash))- 2.88M vs 2.49M i/s
→ ~1.16x 高速
- 2.88M vs 2.49M i/s
rt permitted?(?suffix でパターン全スキャン)- 1.44M vs 1.36M i/s
→ ~1.06x 高速
- 1.44M vs 1.36M i/s
rt tenderlove(respond_to?(:tenderlove))- 4.38M vs 3.55M i/s
→ ~1.23x 高速
- 4.38M vs 3.55M i/s
まとめると:
- 通常の属性メソッド (
title) はほぼ変化なし - 仮想属性アクセス (
tenderlove) とrespond_to?系が目に見えて高速化 - 特に、存在しないメソッド/動的属性を
respond_to?で問い合わせるケースが軽くなっている
これは、method_missing ベースのワークアラウンド除去により、respond_to?/respond_to_missing? がシンプルになった効果と考えられます。
- 影響範囲・注意点
- Rails 6.1 形式の Marshal との互換性を前提にしているコードはもはや考慮されない
- すでに 6.1 Marshal フォーマット自体が削除されているため、この PR でさらに「6.1 互換の最後の保険」的なコードがなくなった形です。
- 6.1 時代に serialize した Active Record オブジェクトを「そのまま」今の Rails で復元するような特殊なケースを想定しているライブラリがあれば、注意が必要です(ただしそのサポートは既に他の PR で切られている)。
method_missingに頼った「隠れた動作」が消える
- 以前は、属性メソッドがクラスに定義されていなくても、
- Marshal 復元など特定条件下で
record.some_attributeが「なんとなく」動いていた可能性があります。
- Marshal 復元など特定条件下で
- この PR 以降は、属性メソッドが定義されていない場合は原則として
NoMethodErrorになります。 - 独自に
method_missingをオーバーライドしているモデルや、メタプログラミングで AR のmethod_missingに依存しているコードがある場合は、挙動確認を推奨します。
respond_to?の戻り値がより厳密になる可能性
- ワークアラウンドがなくなったことで、
- 「実在しないが以前は true が返っていた」ようなケースで false になる可能性があります。
respond_to?を使ってメソッド存在チェックを行い、その結果に依存して条件分岐しているコードがあれば、挙動が変わる可能性があります。- 特に仮想属性や動的属性(
select "..., x as tenderlove"など)周り
- 特に仮想属性や動的属性(
- 性能面の影響
- 利点:
- 仮想属性および
respond_to?の一般的な呼び出しが高速になる - メソッド探索の経路がシンプルになり、Ruby 側の最適化(YJIT 等)にも好影響が見込まれる
- 仮想属性および
- 懸念点:
- 特定のワークアラウンドに依存したレアケース以外はデグレはほぼ無い想定だが、大規模アプリでは
respond_to?を多用している部分の挙動を軽く検証しておくと安心
- 特定のワークアラウンドに依存したレアケース以外はデグレはほぼ無い想定だが、大規模アプリでは
- 参考情報 (あれば)
- 関連 Issue/PR:
- #50594: 6.1 Marshal 形式に関する互換性整理 (推測)
- #57382, #58462, #58466: 6.1 フォーマット削除や周辺互換コード削除の前段階
- この PR は、**「古い互換コードを削除して core をスリム化&高速化する流れの仕上げ」**にあたるものです。
- 自分のアプリが影響を受けるかどうかを確認する簡易チェック:
- アプリやライブラリで以下のようなコードを使っているかどうか:
- Rails 6.1 で Marshal.dump した AR オブジェクトを、最新 Rails でそのままロードしている
- AR モデルで
method_missing/respond_to_missing?をオーバーライドし、Active Record 側の実装に強く依存している respond_to?にinclude_private: true/falseを絡めて、存在しないはずのメソッドに対して true を期待している特殊なパターン
- アプリやライブラリで以下のようなコードを使っているかどうか:
これらに当てはまらなければ、多くの場合は「パフォーマンス改善付きの安全なクリーンアップ」と考えて問題ありません。
#57629 Make callbacks ractor-safe
マージ日: 2026/8/13 | 作成者: @gmcgibbon
- 概要 (1-2文で)
このPRは、ActiveSupport::Callbacksを Ractor セーフにするための対応です。具体的には、コールバックチェーンやユーザー定義の Proc を Ractor 共有可能にし、Ractor 環境下でも安全にコールバックが動作するようにしています。
- 変更内容の詳細
コアとなる変更点
2-1. ユーザー定義 Proc を Ractor-shareable にする
- コールバックとして渡される Proc / lambda / block について、設定に応じて Ractor-shareable(共有可能)な形にする処理が追加されています。
- これは、Ruby の Ractor が「shareable でないオブジェクトのクロス-Ractor共有」を禁止しているためで、
- 共有したい Proc は「分離(isolate)」して shareable にする
- 共有不要な場合は従来通り
という挙動を切り替えられるようにしています。
- 「shareable 化するかどうか」のスイッチは、別PR(#57626)で導入される
ActiveSupport::Ractors側の設定を利用しています。
イメージコード(※概念的な例):
# どこかで Ractor 対応を有効化
ActiveSupport::Ractors.share_callbacks = true
class User
include ActiveSupport::Callbacks
define_callbacks :save
set_callback :save, :before, -> { puts "before save" }
end
# 上記コールバック Proc は、Ractor 間で共有可能な形に変換される2-2. __callbacks / CallbackChain を shareable に
- モジュールやクラスに紐づくコールバック定義 (
__callbacks) と、それを表現するCallbackChainオブジェクト自体を Ractor-shareable にできるよう変更しています。 - Ractor 間で同じクラスを使う際に、コールバック定義が Ractor の境界を越えて安全に共有できることが目的です。
- これにより、「Ractor A で定義したコールバック」を「Ractor B でも安全に使う」ことが可能になります。
2-3. set_callbacks が既存チェーンを破壊的変更しないように
- これまで
set_callbacksは既存のコールバックチェーンを 破壊的に変更(mutate) していましたが、 - Ractor では「shareable なオブジェクトは不変であるべき」という考え方になるため、
新しいチェーンインスタンスを構築して差し替える方式に変更されています。 - これにより:
- 一度 Ractor 共有されたコールバックチェーンは、その後の変更で壊されない
- 変更が必要な場合は「新インスタンス」として扱われる
という immutability に近い設計になります。
概念図:
変更前:
CallbackChain(オブジェクトA) を in-place で変更変更後:
CallbackChain(オブジェクトA) はそのまま 新しい CallbackChain(オブジェクトB) を構築して差し替え
2-4. ActiveSupport::Ractors の拡張
activesupport/lib/active_support/ractors.rbに約 27 行の追加があり、- コールバックを shareable にする/しないを制御する設定
- shareable 化のためのヘルパ
などが追加されています(詳細は #57626 側のスイッチを利用)。
2-5. テストの追加・更新
activesupport/test/callbacks_test.rbに大きくテストが追加(+108行)。- コールバックが Ractor-shareable になること
- Ractor 内外でコールバックチェーンを共有して動かせること
set_callbacksが新しいチェーンを返すこと などの振る舞いを確認。
railties/test/application/ractors_test.rbでも Ractor 環境におけるフレームワークの初期化やコールバック関連の挙動を確認するテストが調整されています。- バリデーション(
activemodel)側のテストにも 1 行追加があり、Ractor 対応による副作用(特にコールバックを用いたバリデーション)が問題ないことをチェックしています。
- 影響範囲・注意点
3-1. 主に影響を受ける層
ActiveSupport::Callbacksを直接利用しているコードActiveRecord、ActiveModel::Validations、その他独自の callback ベース DSL
- Ractor を利用している/これから利用しようとしている Rails アプリケーション
3-2. 実務的な影響ポイント
Ractor を使わないアプリ
- 従来とほぼ互換の挙動で、基本的には「何もしなくても」今まで通り動きます。
set_callbacksが破壊的変更をやめて「新チェーンを作る」方式になっているため、
極端にコールバックの内部オブジェクト同一性に依存しているようなメタプログラムがあると影響を受ける可能性はありますが、一般的な使い方では問題になりにくいです。
Ractor を使う/使いたいアプリ
- コールバックに渡す Proc / lambda / block の中で、
- 非 shareable な状態(例: IO オブジェクト、ミューテーブルなグローバル変数、スレッドローカルなど)を直接キャプチャしている場合は注意が必要です。
- shareable 化の処理は
ActiveSupport::Ractorsの設定に依存するため、- Ractor を使う場合はこの設定を有効にする
- 有効にした場合、shareable にならないオブジェクトをキャプチャしているとエラーになる
可能性があります。
- コールバックに渡す Proc / lambda / block の中で、
マルチプロセス/マルチスレッドとの違い
- Ractor はスレッドよりも強い隔離を要求するため、「スレッドでは動いていたコールバック」が、そのままだと「Ractor では shareable でなくてエラー」になることがあります。
- そのため、Ractor 対応を進める際は:
- コールバックで参照するオブジェクトをできるだけイミュータブル・shareable に寄せる
- 必要に応じて Ractor ローカルなオブジェクト生成に切り替える
などのリファクタリングが必要になります。
3-3. パフォーマンス・メモリ
- コールバックチェーンを都度新インスタンスとして構築するようになったことで、「定義変更時のオブジェクト数」は増えますが、
- 定義変更は通常「アプリ起動時・リロード時」にほぼ限定され、
- ランタイム中のコールバック実行速度にはほぼ影響しないはずです。
- 一方で、Ractor 間でクラスやモジュールを共有する場合に、
shareable な構造で統一されることにより、「コピーを量産しない」方向に寄せやすくなります。
- 参考情報 (あれば)
- 当該 PR:
- スイッチ元 PR(Ractor shareable 挙動スイッチの導入):
- Ruby の Ractor / shareable の概念:
- https://docs.ruby-lang.org/ja/latest/doc/ractor_md.html (Ractor 概要)
Ractor.shareable?,Ractor.make_shareableなどの API ドキュメント
このPRにより、Rails のコールバック機構が Ractor フレンドリーな設計に刷新されつつあり、将来的な「Rails × Ractor」に向けた基盤整備の一環と言えます。
#57226 Fix method_missing performance regression for virtual SELECT alias attributes
マージ日: 2026/8/13 | 作成者: @hammadxcm
- 概要 (1–2文で)
Rails 7.2.3 で導入されたActiveRecord::AttributeMethods#method_missingの変更により、仮想カラム(SELECT 句のエイリアスなど)のアクセスが大幅に遅くなっていた問題を、例外発生を避ける形で解消した PR です。public_instance_method呼び出し前にpublic_method_defined?でチェックすることで、スレッドセーフ性を維持しつつパフォーマンスを改善しています。
- 変更内容の詳細
背景と問題点
- Rails 7.2.3 の PR #53890 で、
ActiveRecord::AttributeMethods#method_missing内の実装が変更され、rubyを呼び出すようになりました。self.class.public_instance_method(name) - モデルに定義された「仮想属性」(DB の実カラムではなく、
SELECT ... AS alias_nameのようにクエリで付けたエイリアスや一部の動的メソッド)は、public_instance_method上では本物のインスタンスメソッドとして存在しないケースがあります。 - そのため、仮想属性アクセス時に
public_instance_methodがNameErrorを投げ、それを rescue するパスを毎回通るようになっていました。 - 例外生成 + バックトレース取得は高コストで、1 回あたり ~9μs 程度かかり、その結果仮想属性のアクセスが約 3 倍遅くなるリグレッションが発生していました。
修正方針
- 例外に頼らず、「事前にそのメソッドがクラスに定義されているかどうか」を軽量に判定することで、
NameErrorを発生させないようにしました。 - 具体的には、
public_instance_methodを呼ぶ前にpublic_method_defined?(name)でチェックします。public_method_defined?はメソッドテーブルを単に lookup するだけの、アロケーションゼロの高速な boolean チェックです。define_attribute_methodsがメソッドテーブルに attribute メソッドを定義した直後のテーブルを参照するため、#53890 で狙っていた「スレッドセーフにメソッドキャッシュを使う」という性質は壊れません。
擬似コードイメージ(※実際のコードは行数が少し違う可能性がありますが、ロジックはこれに近いです):
def method_missing(name, *args, &block)
if self.class.public_method_defined?(name)
# ここでのみ public_instance_method を安全に呼ぶ
umethod = self.class.public_instance_method(name)
umethod.bind_call(self, *args, &block)
else
super
end
endポイント:
- 以前の 7.2.3 の状態:
public_instance_method(name)を無条件に呼ぶ → 仮想属性の場合はNameError→ rescue →superなどにフォールバック
- この PR 後:
public_method_defined?(name)がfalseのときはpublic_instance_method自体を呼ばないため、NameErrorの発生経路が完全に消える- 仮想属性アクセス時には「public メソッドなし → 例外なしで別のハンドリングに移行」となる
ベンチマーク結果
50,000 回の属性アクセス (SQLite, 仮想 SELECT エイリアス) での比較:
| 種類 | 7.2.3 (Before) | この PR 適用後 (After) |
|---|---|---|
実カラム .latitude | 0.004s | 0.010s |
仮想属性 .virtual_attr | 0.189s | 0.077s |
| Virtual / Real 比 | 42.1x | 7.9x |
- 仮想属性は 7.2.3 より大きく高速化(例外コストが削除された分)しています。
- 一方で、実カラムのアクセスは 0.004s → 0.010s と、ベンチマーク上はオーバーヘッドが多少増えています。
- これは
public_method_defined?という 1 ステップが増えた(もしくは #53890 全体の影響)ことによるものと考えられますが、仮想属性の極端な遅さに比べると許容範囲という判断です。
- これは
テストとドキュメント
activerecord/CHANGELOG.mdにこのリグレッション修正についてのエントリを追加。- 仮想 SELECT エイリアス属性アクセス専用のテストを追加。
attribute_methods関連の既存テストスイート (137 runs, 624 assertions, 0 failures) がすべてパス。- Finder, Base, Calculations など周辺のテストもパスしており、既存挙動を壊していないことを確認済み。
- 影響範囲・注意点
- 影響範囲:
ActiveRecord::AttributeMethodsを通じてアクセスされる属性全般(実カラム + 仮想属性)のメソッドディスパッチ経路が影響を受けます。- 特に以下のようなケースで恩恵が大きいです:
SELECT some_expression AS virtual_attrで取得した値をrecord.virtual_attrとして頻繁に読む- Arel やカスタムクエリで大量の SELECT エイリアスを使うレポート機能や集計処理
- この PR による注意点:
- 例外駆動の分岐(
NameErrorをトリガーにするロジック)に依存したコードを書いている場合、挙動が変わる可能性がありますが、method_missingの内部実装に依存したコードはそもそも非推奨であり、実務上ほぼ影響はないはずです。 - 実カラムアクセスのオーバーヘッド増加は軽微ですが、高頻度アクセスのホットパスを極端に最適化している場合は、Rails のマイナーバージョンアップ後にパフォーマンス計測をしておくと安心です。
- 例外駆動の分岐(
運用上の示唆:
- 仮想 SELECT エイリアスを多用するアプリケーションでは、Rails 7.2.3 で顕著だった遅さが改善されるため、この修正を含むバージョンへのアップデートを検討する価値があります。
- 独自に
method_missingをオーバーライドしているモデルや Concern がある場合、Rails 側の実装と干渉していないか・パフォーマンス劣化を招いていないかを合わせて確認するとよいです。
- 参考情報 (あれば)
- 元 issue: #57183 — 仮想 SELECT エイリアス属性へのアクセス性能が 7.2.3 で大きく悪化した報告。
- 関連 PR: #53890 —
method_missingにpublic_instance_methodを導入し、スレッドセーフなメソッドキャッシュを確保した PR(今回のリグレッションの起点)。 - この PR: #57226 —
public_method_defined?による事前チェックで、例外ドリブンな分岐を排除し、パフォーマンスとスレッドセーフ性を両立させる修正。
#58467 Make ActiveSupport::LogSubscriber ractor safe
マージ日: 2026/8/13 | 作成者: @gmcgibbon
- 概要 (1-2文で)
ActiveSupport::LogSubscriber の実装を見直し、Ractor 環境でも安全に動作するように「ログ関連のクラスインスタンス変数の初期化タイミングと場所」を変更した PR です。主に@supports_flushとデフォルト logger の設定を「遅延評価」から「メイン Ractor での事前初期化」に切り替えています。
- 変更内容の詳細
2-1. 背景: なぜ変更が必要か
- これまで ActiveSupport::LogSubscriber は、クラスインスタンス変数(例:
@supports_flush)やデフォルト logger の設定を「必要になったときに初めて計算する(遅延評価)」スタイルで行っていました。 - Ractor 環境では、クラスインスタンス変数の遅延初期化(特に複数 Ractor から同時アクセスされ得るもの)はスレッドセーフ・Ractor セーフでない動作を誘発する可能性があります。
- この PR では、それらの値を「メイン Ractor で一度だけ決定し、その後は他の Ractor からは読み取り専用で参照する」という方針に変えています。
2-2. ActiveSupport::LogSubscriber の変更
activesupport/lib/active_support/log_subscriber.rb での主な変更点:
@supports_flushの算出ロジックを、クラスメソッド内での遅延評価から「初期化時に一度だけ計算する」スタイルへ変更。- デフォルトの Rails logger を LogSubscriber 側で lazily 決めるのではなく、「Railtie 側の初期化処理」でセットするように変更。
疑似コードイメージ(実際のコードとは多少異なる可能性がありますが意図の説明用):
# 変更前(イメージ)
class ActiveSupport::LogSubscriber
class << self
def supports_flush?
@supports_flush ||= logger.respond_to?(:flush)
end
def logger
@logger ||= Rails.logger
end
end
end# 変更後(イメージ)
class ActiveSupport::LogSubscriber
class << self
attr_reader :supports_flush
# supports_flush はどこかの初期化タイミング(Railtie など)で
# 一度だけ代入される想定
end
end- 遅延評価 (
||=) でクラスインスタンス変数を書き換えるパターンをやめ、main Ractor で一度だけ決定して以降 Ractor 間で共有する読み取り専用な値として扱う方向に寄せています。
2-3. Railtie 側での初期化
activesupport/lib/active_support/railtie.rb の変更:
- Rails 起動時(main Ractor)に
ActiveSupport::LogSubscriberの logger と@supports_flushをセットする初期化コードを追加。 - 典型的には、以下のような流れになります:
ActiveSupport::LogSubscriber.logger = Rails.loggerActiveSupport::LogSubscriber.instance_variable_set(:@supports_flush, Rails.logger.respond_to?(:flush))
- これにより、application boot 時に logger と flush サポートの有無が確定するため、以降に生成される各 Ractor(例: request handling Ractor)は、その確定済みの値を安全に参照できます。
2-4. テストの追加
activesupport/test/log_subscriber_test.rb の変更:
- Ractor 環境で LogSubscriber が正しく動作することを確認するテストが追加されています。
- 典型的には:
- main Ractor で初期化した logger /
supports_flushが別の Ractor から読めること - Ractor 内での logging が例外なく完了し、flush 対応の有無の判定が壊れていないこと を確認する内容です。
- main Ractor で初期化した logger /
- 影響範囲・注意点
対象範囲
- ActiveSupport::LogSubscriber を使ったログ出力全般(ActiveRecord/ActionController/ActionView などのログサブスクライバを含む)。
- 特に Ractor ベースの並列リクエスト処理や、Ractor を利用するバックグラウンド処理でのログ出力。
動作の意味的な変更
supports_flushおよび logger の決定タイミングが「初めて使ったとき」から「Rails 起動時(main Ractor)」に早まりました。- そのため、
- アプリケーション起動後に logger を差し替えるようなパターン
- 起動後に logger の API(flush メソッドの有無)が変わるような特殊ケース では、
supports_flushが期待通り更新されない可能性があります。
- ただし、通常は Rails 起動時に logger を確定する運用が一般的であり、多くのアプリには影響はほとんどありません。
Ractor を使わない場合
- ログ出力の動作としてはほぼ従来どおりで、パフォーマンスや API に目立った変更はありません。
- 遅延評価が eager 化されただけなので、logger/flush サポート判定が起動時に一度だけ実行されるようになる程度です。
Ractor を使う場合
- request Ractor(マルチ Ractor でリクエストを処理するなど)からでも、
ActiveSupport::LogSubscriberが安全に利用できる前提が強化されます。 - main Ractor 以外から logger や
supports_flushの再代入を行うようなコードは依然として避ける必要があります(Ractor の制約上)。
- request Ractor(マルチ Ractor でリクエストを処理するなど)からでも、
- 参考情報 (あれば)
Ractor とクラスインスタンス変数の関係:
- Ractor ではオブジェクトの共有と書き込みに制限があるため、「複数 Ractor から同じクラスインスタンス変数を遅延初期化する」パターンが問題を起こしやすい。
- この PR は、その典型的な落とし穴を避けるため、初期化は main Ractor で一度だけ・以後は読み取り専用という形へ設計を調整したものです。
実際に Ractor を利用している・検証したい場合:
- Rails を Ractor で動かす PoC などで、この PR 後のバージョンを使うと、ログ周りの Ractor 非互換エラーが減少(または解消)することが期待されます。
#58468 Reduce Pattern#method_name allocations during boot
マージ日: 2026/8/13 | 作成者: @skipkayhil
- 概要 (1–2文で)
ActiveModel::AttributeMethodsでメソッド名テンプレートを生成する際、:attr_nameと:asが同じ場合に余計な文字列・パターンオブジェクトを生成しないようにし、Railsブート時のPattern#method_name関連のオブジェクト割り当てを削減する最適化です。機能的な挙動は変えずに、パフォーマンスとメモリ効率を改善する変更です。
- 変更内容の詳細
※実際の差分は 6 行追加 / 1 行削除のみの小さな修正で、主に条件分岐が追加されています。概要としては以下のようなロジックになっています(イメージの擬似コード):
# 変更前(イメージ)
def define_attribute_method(attr_name, as: attr_name)
pattern = AttributeMethodPattern.new(attr_name: attr_name, as: as)
method_name = pattern.method_name
define_method(method_name) { ... }
endここでは attr_name と as が同じであっても、毎回 AttributeMethodPattern(実際には Pattern オブジェクト)を作り、#method_name を呼んでメソッド名文字列を生成していました。
# 変更後(イメージ)
def define_attribute_method(attr_name, as: attr_name)
if as == attr_name
# デフォルトケース:attr_name と as が同じ
# すでに分かっているメソッド名を直接使う or 共有のテンプレートを使う
method_name = precomputed_or_simple_name_for(attr_name)
else
# 別名をつける場合だけ Pattern を作って method_name を組み立てる
pattern = AttributeMethodPattern.new(attr_name: attr_name, as: as)
method_name = pattern.method_name
end
define_method(method_name) { ... }
endポイント:
- 「ほとんどのケースでは
:attr_nameと:asは同じ」 という前提に基づいて最適化。 - 両者が同じ場合は、これまで毎回行っていた
Patternオブジェクト生成Pattern#method_name呼び出し(文字列生成)
をスキップする。
:asを使ってカスタム名を付ける“少数ケース”でのみ、従来通りテンプレートを組み立てる。
この結果、Rails 起動時に大量に定義される attribute メソッド(foo, foo?, foo= など)について、割り当てオブジェクトの数が減り、GC 負荷やメモリ消費が軽減されます。
- 影響範囲・注意点
- 影響範囲:
ActiveModel::AttributeMethodsを通じて生成される属性メソッド(ActiveRecord のカラムアクセサなどを含む)のメソッド定義処理。- 特に Rails ブート時(モデルロード時)のメソッド定義フェーズのメモリ割り当てに影響。
- 挙動の互換性:
attr_nameとasの値に応じて生成される最終的なメソッド名は従来と同じであるように実装されているため、公開 API の挙動変更はありません。- カスタム
as:を使うケースでも、これまで通りPattern#method_nameを通して生成されるため、互換性が保たれます。
- 注意点:
AttributeMethods周辺で monkey patch している場合、内部実装のわずかな分岐変更に依存していると影響する可能性がありますが、通常のアプリケーションコードには影響しません。- 性能面では Rails を大規模に使うアプリ(カラム数・モデル数が多い)ほど恩恵が大きくなります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58468
- 関連領域:
ActiveModel::AttributeMethods(ActiveRecord や ActiveModel の動的メソッド生成) - 背景知識:
- Rails はモデル属性に対して
name,name=,name?など多数のメソッドを動的に定義しており、このときメソッド名テンプレート(Pattern)を使って命名している。 - ブート時にこれらが一括で定義されるため、この種の小さな割り当て削減が全体の起動時間・メモリ効率に効きやすい。
- Rails はモデル属性に対して
#58466 Restore performance for dynamic attribute read
マージ日: 2026/8/12 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
このPRは、前回の「パターンキャッシュ削除」によって悪化した ActiveModel の「動的属性読み取り(動的 attribute メソッド)」の性能を取り戻すための調整です。to_hashやpermitted?など、Strong Parameters 周辺で多用される処理のパフォーマンス低下を緩和・解消することが目的です。
- 変更内容の詳細
背景
前回コミット([1a59302e642b…][1])で「パターンキャッシュ」が削除され、その副作用として ActiveModel の動的 attribute メソッド解決にかかるコストが増え、to_hash / permitted? などの実行速度が 1.7〜2.7 倍遅くなったというベンチマーク結果が示されています。
to_hash (dd980d49)→to_hash (after): 約 1.7x 遅くなったpermitted? (dd980d49)→permitted? (after): 約 2.7x 遅くなった
このPRは、そのパフォーマンス劣化を解消するために ActiveModel::AttributeMethods 内の実装を見直しています。
どのあたりが変わったか(概念的な説明)
変更ファイルは 1 つのみです:
activemodel/lib/active_model/attribute_methods.rb(+17/-13)
主に以下のような点が調整されていると考えられます(行数とコンテキストから推測しています):
動的メソッド解決のパスを最適化
- ActiveModel は
method_missingとrespond_to_missing?を利用してuser.nameのような attribute アクセサを動的に解決しますが、この解決ロジックの中で、- シグネチャ作成
- メソッド名のパターンマッチ(
_before_type_cast,?,=などのサフィックス) - attribute 定義の検索
といった操作が毎回発生します。
- 前回コミットで除去された「パターンキャッシュ」に依存しない形で、同等レベルの「繰り返しコスト削減」が行われています(ローカル変数の使いまわしや分岐の整理、冗長なチェックの削減など)。
- ActiveModel は
to_hash/permitted?辺りのホットパスを意識した微調整- Strong Parameters (
ActionController::Parameters) は、内部的に ActiveModel の attribute メソッドを多用します。このPRでは、そうしたホットパスで呼ばれる attribute 読み取りの実装ができるだけ軽くなるように分岐や条件チェックが整理されています。 method_missingから実際の attribute 値を取得する処理までのコールチェーンを短くする・あるいは一部ロジックをインライン化する方向の変更になっていると思われます。
- Strong Parameters (
キャッシュの戻し方が「パターンキャッシュとは異なる」
- タイトルが “Restore performance for dynamic attribute read” であり、「pattern cache を戻す」とは明示していないため、以前のパターンキャッシュそのものを復活させたのではなく、
- キャッシュ対象を絞る
- Ruby のメソッドキャッシュ(inline cache)や YJIT が効きやすい形に書き換える
など、より JIT / メソッドキャッシュフレンドリーな形にコードを調整している可能性が高いです。
- タイトルが “Restore performance for dynamic attribute read” であり、「pattern cache を戻す」とは明示していないため、以前のパターンキャッシュそのものを復活させたのではなく、
イメージ的なコード(※雰囲気をつかむための擬似例)
実際のコードとは異なりますが、やっていることはだいたい以下のような最適化イメージです。
変更前(イメージ):
def method_missing(name, *args, &block)
if attribute_method?(name)
# 毎回 name を解析して、getter / setter / predicate を判定
type, attr_name = match_attribute_method?(name)
# ...
else
super
end
end変更後(イメージ):
def method_missing(name, *args, &block)
# 早い段階で簡易チェックして無駄な work を減らす
unless name.is_a?(Symbol)
return super
end
if mapped = fast_attribute_lookup(name)
# 解析済み結果(種別, 元 attribute 名など)をそのまま利用
type, attr_name = mapped
# ...
else
super
end
endこうした変更により、「1回メソッドを呼ぶたびに繰り返し行っていたコストの高い解析処理」を避けられます。
- 影響範囲・注意点
影響範囲
- 主に影響を受けるのは、ActiveModel の attribute メソッドを動的に大量呼び出しするコードです。
ActionController::Parameters#to_hash,#permitted?など Strong Parameters 周り- ActiveModel ベースのフォームオブジェクトや値オブジェクトで属性を多数扱うケース
- 大量のレコードに対して attribute アクセスを繰り返す処理
上記のようなコードは、このPRを含む Rails バージョンにアップデートすることで、**直前のコミットよりもパフォーマンスが改善(以前の水準に近づく/戻る)**ことが期待できます。
注意点
- 公開 API のインターフェイスは変わっておらず、通常のアプリケーションコードから見える挙動に変更はほぼないと考えられます。
- 変更は
ActiveModel::AttributeMethodsの内部実装レベルであり、以下のような場合は影響を受ける可能性があります:AttributeMethodsを独自にincludeして拡張し、method_missing・respond_to_missing?を積極的にオーバーライドしているクラスAttributeMethodsの内部メソッド(private/protected)に依存したメタプログラミングを行っているコード
- そのような高度なメタプログラミングをしている場合は、Rails 更新後に:
- 動的 attribute アクセスが期待どおりに動いているか
respond_to?の結果が変わっていないかmethod_missingが思わぬ順序で呼ばれていないか などをテストで確認することを推奨します。
- 参考情報 (あれば)
このPRがフォローアップしているコミット(パターンキャッシュ削除):
https://github.com/rails/rails/commit/1a59302e642b98e78e88464edf0f3b7741359a92ベンチマークのポイント:
to_hash・permitted?の i/s が前のコミットに対して大きく落ちていたため、それを問題として認識し、今回の PR で改善を試みています。tenderloveベンチ(おそらく別種のケース)はほぼ性能が変わっておらず、今回の劣化・改善対象が特定の動的属性読み取りパスだとわかります。
対応バージョン感:
- ベンチマークは Ruby 4.0.3 + YJIT + PRISM で実行されており、Rails 側も将来の Ruby 4 系との組み合わせでの性能を意識していることがうかがえます。
- Ruby 3.x 系でも恩恵はあるはずですが、特に JIT / PRISM 環境ではより顕著な違いが出る可能性があります。
このPRは、外側の API を変えずに内部のホットパスだけを最適化しているため、「挙動は変えずに Strong Parameters や ActiveModel 属性アクセスの速度を戻したい/改善したい」という観点で、比較的安全かつ恩恵の大きい変更といえます。
#58462 Remove attribute_method_patterns_cache
マージ日: 2026/8/12 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
ActiveModel/ActiveRecord の「動的属性メソッド」判定に使っていたattribute_method_patterns_cacheを撤廃し、代わりに複数のマイクロ最適化でパフォーマンスを補う変更です。キャッシュは性能面では有用だったものの、サイズ無制限かつ Ractor 非対応であるため、将来の並行実行モデルとの整合性を優先して削除されています。
- 変更内容の詳細
2-1. attribute_method_patterns_cache とは何をしていたか
ActiveModel/ActiveRecord では、respond_to? やメソッド呼び出し時に、
name_will_change!name_before_type_castreset_name!
のような「パターンに基づく動的属性メソッド」を判別するため、attribute_method_patterns という正規表現 / パターンのリストを走査していました。
attribute_method_patterns_cache は、次のような「メソッド名 → (属性名, パターン)」という解析結果をキャッシュするための Hash です。
例:
# 以前のイメージ
attribute_method_patterns_cache["something_will_change!"] =
["something", :attribute_will_change!]これにより、同じメソッド名に対する respond_to? / 動的ディスパッチが繰り返されるケースでは、高速化が図られていました。
2-2. キャッシュ削除の理由
PR で挙げられている問題点は 2 つです。
容量制限がない(unbounded)
- 受け取ったメソッド名ごとにエントリが増え、上限やエビクションポリシーがないため、
アプリケーションによってはメモリリーク的な増加を起こしうる。
- 受け取ったメソッド名ごとにエントリが増え、上限やエビクションポリシーがないため、
Ractor で動作しない
- Ractor は「スレッドセーフよりも厳しい並行実行モデル」を要求するため、
共有可変オブジェクト(典型的には通常の Hash)をそのまま利用できません。 - グローバルなキャッシュ Hash は Ractor の世界観と相性が悪く、
Ruby 本体・Rails を Ractor 対応していく上で障害になる。
- Ractor は「スレッドセーフよりも厳しい並行実行モデル」を要求するため、
これらを踏まえ、キャッシュ自体をやめ、Ractor フレンドリーな実装+マイクロ最適化へ舵を切っています。
2-3. 代わりに入ったマイクロ最適化
PR の説明文から読み取れる主な方針は次のとおりです。
対象
パフォーマンス影響が懸念されるのは「モデルが実際には持たないメソッド名」に対するrespond_to?だけです。- 具体的には:
- そもそも ActiveRecord/ActiveModel で定義されていない任意メソッド
- 動的属性(SELECT 句で追加した非カラムの値など)
- パターンにはマッチするが最終的に存在しないメソッド など
- 具体的には:
静的に定義済みのメソッドには影響しない
明示的に定義されたインスタンスメソッド・アクセサなどに対するrespond_to?の挙動・性能はこれまで通りです。プレフィルタなどでの早期リターン強化
PR に添付のベンチマークを見ると、to_hash→ 事前フィルタで弾かれるケースpermitted?→ パターンの走査までは行くが、結果的には false になるケースtenderlove→ 動的属性として true になるケース
をそれぞれ測定しています。
これはつまり、
- 「絶対に属性メソッドになりえない名前」は、なるべく早く判定を終える
- 「サフィックスだけはマッチする」ような名前に対しても、無駄な処理を少なくする
といった形で、キャッシュ無しでもある程度の速度を確保するためのロジック改善が入っている、ということです。
実際のコード変更(activemodel/lib/active_model/attribute_methods.rb)では、respond_to? や match_attribute_method? 周りの実装が見直され、
- 不要なオブジェクト生成を避ける
- 文字列操作・正規表現マッチの順序・回数を減らす
など、細かい最適化が多数入っていると考えられます。
- 影響範囲・注意点
3-1. 性能面の影響
PR 内ベンチマークによれば、動的属性まわりの respond_to? は以下のように遅くなっています(いずれも「キャッシュあり(before)」比)。
to_hash(必ず false, 事前フィルタで弾かれる)- 約 1.62 倍遅い:
- before: 約 3,953,529 i/s
- after: 約 2,447,226 i/s
- 約 1.62 倍遅い:
permitted?(パターンにはマッチするが結果的には false)- 約 2.72 倍遅い:
- before: 約 3,650,315 i/s
- after: 約 1,340,220 i/s
- 約 2.72 倍遅い:
tenderlove(動的属性で true)- 約 1.72 倍遅い:
- before: 約 3,010,374 i/s
- after: 約 1,752,522 i/s
- 約 1.72 倍遅い:
ただし、これらは 非常に小さな単位のベンチマーク であり、現実のアプリケーションでは、
- 同じメソッド名で大量の
respond_to?を連発する - しかもそれが「動的属性 or 存在しないメソッド」
といったパターンが支配的でなければ、体感での差はそこまで大きくはならない可能性があります。
3-2. 影響しやすいコードパターン
次のようなコードは、この変更の影響を受ける余地があります。
- 大量のモデルインスタンスに対して、ループ内で頻繁に
respond_to?を呼んでいる - しかも、そのメソッド名が:
- 動的属性(
select("..., foo as bar")のbarなど)であったり *_will_change!,*_before_type_castや類似の suffix を持つ(=パターンに引っかかる)ものであったり- 最終的には false になるケースが多い
- 動的属性(
例えば:
records.each do |record|
dynamic_name = some_runtime_string
if record.respond_to?(:"#{dynamic_name}_will_change!")
# ...
end
endこのようなコードは、以前はキャッシュの恩恵を受けていましたが、今後は毎回文字列解析・パターン走査が発生します。
3-3. 開発者としての対処・意識ポイント
- 本当に必要な
respond_to?かを見直す- メソッド存在チェックを多用している部分は、ポリモーフィズムやインターフェースの明示(モジュールなど)で再設計できないか検討する価値があります。
- パフォーマンスクリティカルなループでの
respond_to?多用は避ける- どうしても必要な場合は、事前に「扱うメソッド名の集合」を限定しておく・別のフラグで表現するなどして、動的な名前解決コストを抑える工夫が有効です。
- Ractor 対応を優先した変更であることを理解する
- 将来的に Ruby/Rails で Ractor ベースの並列処理を行いたい場合、この種のグローバルキャッシュ削除は避けられない種類の変更であり、そのための布石と考えられます。
動作仕様自体(どの名前が動的属性として解釈されるか)は基本的に変わっていない想定であり、API レベルの非互換はほぼありません。
意識すべきポイントは、おもに「一部シナリオでの respond_to? の速度低下」です。
- 参考情報 (あれば)
変更ファイル:
activemodel/lib/active_model/attribute_methods.rb- 動的属性メソッド判定ロジック・
respond_to?拡張部分の実装見直し(+61/-19)
- 動的属性メソッド判定ロジック・
activerecord/lib/active_record/attribute_methods.rb- ActiveRecord 向けの薄いラッパー側でのわずかな調整(+0/-1)
関連しうる概念・ドキュメント:
- Rails ガイド: Active Record クエリインターフェイス(SELECT で追加したカラム → 動的属性)
- Rails API:
ActiveModel::AttributeMethods - Ruby 3+ の Ractor ドキュメント(共有可変オブジェクトの制限など)
この PR は「動的属性メソッド解決の仕組み」は維持しつつ、「キャッシュを捨てて Ractor 互換な実装 + マイクロ最適化へ切り替えた」と理解しておくと把握しやすいです。
#58321 Add Pacific Time (Canada) and Alberta Time zone mappings
マージ日: 2026/8/12 | 作成者: @Saidbek
- 概要 (1-2文で)
Rails のActiveSupport::TimeZoneに、カナダ西部向けの新しいタイムゾーン名マッピング「Pacific Time (Canada)」「Alberta」が追加され、BC(ブリティッシュコロンビア)とアルバータ州での冬時間のズレを避けられるようになりました。既存の「Pacific Time (US & Canada)」「Mountain Time (US & Canada)」は互換性維持のためそのまま残されています。
- 変更内容の詳細
追加されたマッピング
ActiveSupport::TimeZone のフレンドリ名と IANA タイムゾーンの対応に、以下が追加されています。
"Pacific Time (Canada)"→"America/Vancouver""Alberta"→"America/Edmonton"
既存:
"Pacific Time (US & Canada)"→"America/Los_Angeles"(変更なし)"Mountain Time (US & Canada)"→"America/Denver"(変更なし)
背景として、BC/Alberta は冬時間の運用が米国の Pacific / Mountain とずれるようになっており、従来の Pacific Time (US & Canada) / Mountain Time (US & Canada) を「カナダ西部の代用」として使い続けると、冬期に 1 時間ズレるケースが出るためです。
実際のコード変更イメージ
activesupport/lib/active_support/values/time_zone.rb への追記(概念的なイメージ):
MAPPINGS = {
# 既存
"Pacific Time (US & Canada)" => "America/Los_Angeles",
"Mountain Time (US & Canada)" => "America/Denver",
# 新規
"Pacific Time (Canada)" => "America/Vancouver",
"Alberta" => "America/Edmonton",
# ほか…
}これにより、以下のように利用できます。
Time.find_zone("Pacific Time (Canada)") # => #<ActiveSupport::TimeZone: ... "America/Vancouver">
Time.find_zone("Alberta") # => #<ActiveSupport::TimeZone: ... "America/Edmonton">テストとドキュメント
activesupport/test/time_zone_test.rb
新マッピングが正しくAmerica/Vancouver/America/Edmontonに解決されるテストが追加。activesupport/CHANGELOG.md
新しいフレンドリゾーン名が追加されたことが記載され、振る舞い変更として明示されています。
- 影響範囲・注意点
互換性:
- 既存の
"Pacific Time (US & Canada)"/"Mountain Time (US & Canada)"はそのままなので、既存アプリは即座に壊れません。 - ただし、それらを「BC/Alberta の別名」として使っている場合、冬時間に 1 時間のズレが発生しうる点は変わりません。
- 既存の
推奨対応:
- BC を主対象とするアプリでは、今後は:
Time.find_zone("Pacific Time (Canada)")- あるいは直接
Time.find_zone("America/Vancouver")
- アルバータを主対象とするアプリでは:
Time.find_zone("Alberta")- あるいは
Time.find_zone("America/Edmonton")を使うのが望ましいです。
- 既存コードで
"Pacific Time (US & Canada)"や"Mountain Time (US & Canada)"を BC/Alberta 用に使っている場合は、上記への置き換えを検討してください。
- BC を主対象とするアプリでは、今後は:
IANA tzdb 依存:
- 実際の DST / オフセットのルールは IANA tzdb に依存しており、Rails 側はあくまで「フレンドリ名称 → IANA 名」のマッピングを増やしただけです。
- タイムゾーンデータは OS / tzinfo / ランタイムの更新状況に影響されるため、本番環境で tzdb が最新(少なくとも 2026a / 2026b / 2026c 以降)であることも重要です。
- 参考情報 (あれば)
- IANA tzdb リリース:
- 既存の類似マッピング(Rails 内のパターン例):
"Arizona"→America/Phoenix"Saskatchewan"→America/Regina"Atlantic Time (Canada)"→America/Halifax
これらと同様に、「国・州ごとの運用差が出てきた場合に、より具体的なフレンドリ名称を追加する」という方針に沿った変更です。
#58446 Fix where clauses with column-tuple syntax referencing other tables
マージ日: 2026/8/12 | 作成者: @cgunther
- 概要 (1-2文で)
where句で「カラムのタプル構文(配列構文)」を使う際に、他テーブルをother_table.columnと明示した場合でも、現在のモデルのテーブル名が誤ってプレフィックスされてしまうバグを修正するPRです。これにより、JOIN 先のテーブルカラムを含むタプル条件が正しくSQLに変換されるようになります。
- 変更内容の詳細(あればサンプルコードも含めて)
問題のあった挙動
Active Record の where で、複数カラムをまとめて比較する「タプル構文」を使うときの想定は以下のようなケースです。
# 例: (category_id, author_id) = (1, 2) のような条件
Categorization.where(%i[category_id author_id] => [1, 2])内部的には「カラム名の配列(例: [:category_id, :author_id])」を元に SQL を組み立てますが、このカラム配列は「すべて現在のモデルのテーブルに属するカラムである」と仮定されていました。
そのため、JOIN している別テーブルのカラムを table.column の形式で指定した場合に、誤ったプレフィックスが付与されていました。たとえば:
# categorizations テーブルと authors テーブルを JOIN している状況で
Categorization.joins(:author).where(
%i[category_id authors.name] => [1, "Alice"]
)これを解釈する際、authors.name というキーが「単なるカラム名」とみなされ、categorizations テーブル名が自動で付与されてしまい、結果として以下のような誤った SQL が生成されていました:
... WHERE "categorizations"."category_id" = ?
AND "categorizations"."authors.name" = ?SQLite では、. を含む "authors.name" が「そのままのカラム名」と解釈され、当然そんなカラムは存在しないため、次のようなエラーが発生します。
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: categorizations.authors.name:
SELECT "categorizations".*
FROM "categorizations"
INNER JOIN "authors" ON "authors"."id" = "categorizations"."author_id"
WHERE "categorizations"."category_id" = ?
AND "categorizations"."authors.name" = ?今回の修正内容
対象ファイル:
activerecord/lib/active_record/relation/predicate_builder.rb(+2/-1)activerecord/test/cases/relation/where_test.rb(+12/-0)
実質的な変更は PredicateBuilder 内でのキーの扱いを 1 ステップ追加したものです。
ポイント:
- 「ドット記法(
"table.column"や"authors.name")」で指定されたキーを、そのまま「単一のカラム名文字列」として扱うのではなく、一度「ハッシュ形式」に変換してから処理するようにした。 - これにより、テーブル名とカラム名が正しく分離され、「現在のモデルのテーブル名を無条件にプレフィックスする」ロジックが適用されなくなります。
イメージとしては、以下のような変換が先に行われるようにした、という変更です(擬似コード):
# 変更前のイメージ(簡略化)
# [:category_id, "authors.name"] が "categorizations" のカラムとして扱われてしまう
# 変更後のイメージ(簡略化)
# "authors.name" を見たときに:
# "authors.name" -> { "authors" => "name" } のような形に変換
# その後の処理では適切なテーブル/カラムに分けて扱うテスト (where_test.rb) では、他テーブルのドット記法カラムをタプル構文に含めた場合でも、正しい SQL が生成されることを確認するケースが 1 つ(または複数)追加されています。
- 影響範囲・注意点
影響範囲:
- Active Record の
whereで「カラムの配列(タプル構文)」を使い、かつキーの中にother_table.columnのように「他テーブルのカラム」を直接書くケースが対象です。 - 通常の
where(author: { name: "Alice" })のようなハッシュ構文は元から問題なかったため、影響は限定的です。 - 既にワークアラウンドとして「文字列 SQL を直書きしていた」「Arel を直接使っていた」ようなコードを、より自然な
whereタプル構文に書き直せるようになります。
- Active Record の
後方互換性と注意点:
- これまで「誤ったカラム解釈」に依存していたコードがある可能性は非常に低く、基本的にはバグ修正として安全な変更です。
- 主な変化は「これまで例外が出ていたクエリが、意図通りに動くようになる」というもので、既存の正常系挙動に影響を与える可能性は小さいと考えられます。
- PR 作成者も v8.1 へのバックポートを希望しており、「マイナーなバグ修正」として扱えるレベルの変更と位置付けられています。
- 参考情報 (あれば)
PR 本体:
rails/railsリポジトリの PR #58446: “Fix where clauses with column-tuple syntax referencing other tables”関連コンポーネント:
ActiveRecord::Relation::PredicateBuilderActiveRecord::QueryMethods#where
実務での利用例(期待通り動くようになるパターン):
ruby# authors テーブルに name、categorizations に category_id, author_id がある想定 Categorization .joins(:author) .where( %w[categorizations.category_id authors.name] => [1, "Alice"] )のように、タプルの中に JOIN 先テーブルのカラム (
authors.name) を混在させても正しく SQL が生成されるようになります。
#58461 Allow ffmpeg and ffprobe input arguments to be configured
マージ日: 2026/8/12 | 作成者: @flavorjones
- 概要 (1-2文で)
Active Storage が内部で呼び出している ffmpeg / ffprobe に対し、「入力側の引数」をアプリケーション側から設定できるようにする PR です。これにより、利用可能なデコーダ・プロトコルをホワイトリスト方式で制限するなど、アップロードメディア処理部分のハードニングが可能になります。
- 変更内容の詳細
2-1. 新しい設定項目
Active Storage に以下の 2 つの設定が追加されています。
# ffmpeg に対して -i より前に渡される引数
config.active_storage.video_preview_input_arguments
# ffprobe に対して、パスの直前に渡される引数
config.active_storage.ffprobe_arguments- デフォルト値はいずれも空文字列
""であり、既存アプリがこの設定を変更しなければ、実行コマンドラインは従来と完全に同じ挙動です。 ffprobe_argumentsは音声・動画両方の Analyzer で共通に使われるため、この名前になっています。
2-2. 何が解決されるか
ffmpeg / ffprobe のハードニングによく使われる以下のようなフラグは、入力指定 (-i やファイルパス) より前に置く必要があります。
-codec_whitelist-protocol_whitelist- その他「どの demuxer / decoder / protocol を許可するか」を制限する系のフラグ
これまでは:
config.active_storage.video_preview_argumentsは-iの「後ろ」に挿入されるため、-codec_whitelistなどを意図した位置に指定できなかった。- ffprobe については、そもそも引数をカスタマイズする公式な手段がなかった。
そのため、「入力パスより前にフラグを差し込みたい」場合は:
ActiveStorage::Previewer::VideoPreviewerやActiveStorage::Analyzer::VideoAnalyzer/AudioAnalyzer
といったクラスのプライベートメソッドを丸ごとオーバーライドし、ffmpeg / ffprobe の引数列を自前で再定義するしかありませんでした。
これだと、Rails 本体側で解析処理の引数がマイナー変更されるたびにアプリ側のオーバーライドと差分が生じ、メンテナンス性とセキュリティの両面でリスクがあります。
今回の PR によって:
- ffmpeg の入力引数 (
-iの前 ) - ffprobe の入力引数 ( ファイルパスの前 )
だけをアプリ側で差し込める公式な拡張ポイントができたため、オーバーライドなしでハードニングが可能になります。
2-3. コマンドラインの変化例
例: H.264 + AAC のみ許可する場合
config/application.rb などで:
config.active_storage.video_preview_input_arguments = "-codec_whitelist h264,aac"
config.active_storage.ffprobe_arguments = "-codec_whitelist h264,aac"と設定したときの変化。
変更前 (プレビュー用 ffmpeg):
ffmpeg -i /tmp/blob.mp4 -vf 'select=...' -frames:v 1 -f image2 -変更後:
ffmpeg -codec_whitelist h264,aac -i /tmp/blob.mp4 -vf 'select=...' -frames:v 1 -f image2 -変更前 (ffprobe – Video/AudioAnalyzer 共通):
ffprobe -print_format json -show_streams -show_format -v error /tmp/blob.mp4変更後:
ffprobe -print_format json -show_streams -show_format -v error -codec_whitelist h264,aac /tmp/blob.mp42-4. 実装箇所のポイント
activestorage/lib/active_storage.rbActiveStorageの設定にvideo_preview_input_arguments/ffprobe_argumentsが追加。
engine.rb- 上記設定のデフォルト値を空文字に設定。
video_previewer.rb- ffmpeg コマンド作成時に、
-iの前にvideo_preview_input_argumentsを挿入するよう変更。
- ffmpeg コマンド作成時に、
audio_analyzer.rb/video_analyzer.rb- ffprobe コマンド作成時に、ファイルパスの直前に
ffprobe_argumentsを差し込むよう変更。
- ffprobe コマンド作成時に、ファイルパスの直前に
- テスト (
*_test.rb) が追加され、設定値を変えた際にコマンドラインが期待どおり組み立てられるかが検証されています。 guides/source/configuring.md/guides/source/security.md- 新しい設定と「Media Processing of File Uploads」に関するセキュリティガイドが追加されています。
- 影響範囲・注意点
既存アプリへの影響
- 新設定のデフォルトが空文字列のため、既存のアプリケーションは設定を追加しない限り挙動は一切変わりません。
- つまり、この PR は「設定を有効にしたアプリだけに影響する opt-in 機能」です。
導入メリット
- ffmpeg / ffprobe が扱えるコーデックやプロトコルをホワイトリスト方式で制限できるため、悪意あるメディアファイルを通じた攻撃面を減らすことができます。
- 特に、外部からアップロードされる動画や音声をそのまま ffmpeg / ffprobe に食わせている環境で有効です。
注意点
- 誤ったホワイトリスト指定をすると、正当なファイルであっても解析・サムネイル生成が失敗します。
- 例: 実際には H.265 (HEVC) を含むファイルがアップロードされるが、
-codec_whitelistに h264 しか書いていない、など。
- 例: 実際には H.265 (HEVC) を含むファイルがアップロードされるが、
- ffmpeg / ffprobe のバージョンやビルドオプションによって利用可能なコーデック名・プロトコル名が異なる場合もあるため、本番環境と同一条件での検証が推奨されます。
- これは Rails 自体の脆弱性を修正する PR ではなく、「アプリケーション側のセキュリティポリシーを反映しやすくするための機能追加」です。
ガイドにも明記されている通り、これ単体で全てのメディア関連攻撃を防げるわけではありません。
- 誤ったホワイトリスト指定をすると、正当なファイルであっても解析・サムネイル生成が失敗します。
- 参考情報 (あれば)
追加されたガイド:
guides/source/security.mdの「Media Processing of File Uploads」セクション
→ ffmpeg / ffprobe 利用時に考慮すべき攻撃面と、本 PR で追加された設定項目の使い方の概説。guides/source/configuring.md
→config.active_storage.video_preview_input_arguments/config.active_storage.ffprobe_argumentsの設定方法。
ffmpeg / ffprobe の関連ドキュメント(ホワイトリスト系オプション):
-codec_whitelist-protocol_whitelist
などは、公式ドキュメントやffmpeg -h/ffprobe -hで詳細を確認できます。
#58460 Introduce to_ractor_snapshot / load_ractor_snapshot protocol for notifiers
マージ日: 2026/8/12 | 作成者: @kamipo
- 概要 (1-2文で)
ActiveSupport::Notificationsの notifier に対して、Ractor 間共有用の新しいプロトコルto_ractor_snapshot/load_ractor_snapshotが導入されました。これにより、カスタム notifier を実装する際に内部実装(Fanout の構造)を真似る必要がなくなり、Ractor 共有対応も任意で選べるようになります。
- 変更内容の詳細
背景
- #58060 以降、
ActiveSupport::Notifications.record_subscriptionsはFanoutの内部構造(string_subscribers/other_subscribers)に直接アクセスする実装になっていました。 - その結果、
ActiveSupport::Notifications.notifier = custom_notifierのようにカスタム notifier を差し替える場合でも、Fanoutと同じようなインスタンス変数・構造を持っていないとrecord_subscriptionsで落ちる(NoMethodError / NameError 的な事故が起きる)状態でした。 - この「Fanout の内部構造への依存」をやめ、明示的なプロトコル(インターフェース)に置き換えたのが本 PR です。
新しいプロトコル: to_ractor_snapshot / load_ractor_snapshot
役割
to_ractor_snapshot- Notifier(例:
Fanoutインスタンス)の「Ractor 間共有可能なスナップショット」を生成します。 - Ractor 間で安全にコピーできる形に変換する責務を、各 notifier 実装側に委ねます。
- Notifier(例:
load_ractor_snapshot(snapshot)to_ractor_snapshotが返した snapshot から、実際に notifier の状態を再構築します。
使い方(概念イメージ)
ActiveSupport::Notifications 側は、以下のような「プロトコルに従うかどうか」を respond_to? で判定します。
notifier = ActiveSupport::Notifications.notifier
if notifier.respond_to?(:to_ractor_snapshot)
snapshot = notifier.to_ractor_snapshot
# snapshot は Ractor 間共有可能なオブジェクト
else
# 非対応なら snapshot 機能は使わない(or ゴリっとコピー/共有しない)
end新しい Ractor で復元するときは:
if notifier.respond_to?(:load_ractor_snapshot)
notifier.load_ractor_snapshot(snapshot)
end※ 実際のコードは Rails の内部 API なので多少異なりますが、プロトコルの概念としてはこのような流れです。
Fanout 側の対応
activesupport/lib/active_support/notifications/fanout.rb に、上記プロトコルの実装が追加されています。
Fanout#to_ractor_snapshot- 内部状態(
string_subscribers/other_subscribersなど)から、Ractor 間で共有可能なスナップショットを生成。 - これまで
record_subscriptionsが直接参照していた内部フィールドを、このメソッド経由で取得するようにした、という位置づけです。
- 内部状態(
Fanout#load_ractor_snapshot(snapshot)- 受け取った snapshot を元に、
Fanoutの内部状態を復元します。
- 受け取った snapshot を元に、
これにより:
Notifications.record_subscriptionsはFanoutの内部変数に直接アクセスしない。- 将来
Fanoutの内部実装が変わっても、to_ractor_snapshot/load_ractor_snapshotの契約を守れば外部コードは壊れない。
Notifications.record_subscriptions の変更
- これまで:
Notifications.record_subscriptionsはFanout#string_subscribers/Fanout#other_subscribersに直接アクセスして、Ractor 用の snapshot を作っていました。 - これから:
- まず、
notifier.respond_to?(:to_ractor_snapshot)をチェック。 - 実装されていれば
notifier.to_ractor_snapshotから snapshot を取得。 - なければ snapshot 取得はスキップ/別ルートでの処理(Ractor 共有なし)にフォールバック。
- まず、
この変更で、カスタム notifier に Fanout 互換の内部構造を要求しなくなったのが大きなポイントです。
テストの追加
activesupport/test/notifications_test.rb にテストが多数追加されています(+47行)。
推測されるテスト観点:
- デフォルトの
Fanoutnotifier で、to_ractor_snapshot/load_ractor_snapshot経由の挙動が正しく動くこと。 to_ractor_snapshot/load_ractor_snapshotを実装したカスタム notifier で、Ractor 用処理が問題なく機能すること。- これらを実装していないカスタム notifier を指定しても、クラッシュしないこと(
respond_to?で安全に分岐できていること)。
- 影響範囲・注意点
影響を受ける可能性が高いコード
ActiveSupport::Notifications.notifierをカスタム実装に差し替えているコード- 以前は(壊れたものの)Fanout の内部構造を真似している or していない前提で挙動していました。
- これからは、以下の二通りの選択肢があります:
- Ractor 共有を気にしない場合: 何も実装しなくてもよい(
to_ractor_snapshotもload_ractor_snapshotも不要)。 - Ractor 共有を有効にしたい場合:
to_ractor_snapshot/load_ractor_snapshotを自前で実装する。
- Ractor 共有を気にしない場合: 何も実装しなくてもよい(
Ractor を使って並列実行しており、その中で
ActiveSupport::Notificationsを利用しているコード- デフォルトの
Fanoutを使っている場合、今回の変更により、内部構造に依存しない安定した snapshot 取得ができるようになります。 - カスタム notifier で Ractor 共有をしたいなら、Ractor 闘に安全なデータのみを snapshot に含めるような設計が必要です。
- デフォルトの
カスタム notifier 実装時の指針
カスタム notifier に Ractor 共有対応をさせたい場合、次のような実装が一般的な形になります:
class MyNotifier
# Ractor 共有用の snapshot を生成
def to_ractor_snapshot
# Ractor で共有可能なオブジェクトのみを返す
{
subscribers: @subscribers.map(&:to_h), # 例: イミュータブルなデータに変換
}
end
# snapshot から状態を復元
def load_ractor_snapshot(snapshot)
@subscribers = snapshot[:subscribers].map { |h| Subscriber.from_h(h) }
end
end注意点:
- snapshot に含めるオブジェクトは、Ractor で共有できるものに限定する必要があります(Frozen なオブジェクトや純粋なデータ構造など)。
- 共有したくない(or 共有できない)状態は snapshot に入れない、あるいは復元ロジックで再構築する必要があります。
将来の変更予定との関係
PR 説明にもある通り:
- 本来は
Fanout自体を「共有可能」な実装にし、snapshot 自体を不要にするのが理想(つまり、Ractor 間でFanoutをそのまま共有できる)。 - しかし現在の
FanoutはConcurrent::Mapベースの状態を持っており、そのままでは COW(Copy-On-Write)に適した設計になっていない。 - そのため、今回は snapshot アプローチを維持しつつ、「内部構造に直接アクセスせず、プロトコル越しに扱う」形にとどめている。
将来的にFanoutの COW 対応リライトが入る場合も、このプロトコルを保つことで互換性を維持しやすくなります。
- 参考情報 (あれば)
- 対応する過去 PR:
- #58060:
Notifications.record_subscriptionsがFanoutの内部構造に依存するようになった変更。
- #58060:
- この PR で導入されたプロトコル:
ActiveSupport::Notificationsの notifier 向け「Ractor 共有対応 API」to_ractor_snapshotload_ractor_snapshot
- 関係者(レビュー/議論の文脈を追う場合):
- 作成者: @kamipo
- メンション: @gmcgibbon, @Edouard-chin
#58445 Compile non-strict templates Ractor-locally
マージ日: 2026/8/12 | 作成者: @etiennebarrie
- 概要 (1-2文で)
非 strict ローカルなテンプレートでも Ractor 内から安全にコンパイル・実行できるようにする変更です。併せて、既存 Rails で strict locals テンプレート利用時に例外バックトレースが正しく補正されない不具合も修正されています。
- 変更内容の詳細
背景
- Ractor からビューを描画する場合、テンプレートのコンパイル結果がスレッド/Ractor間で共有されると安全でない(Ractor 間で共有可能なオブジェクト制約に抵触する)ため、テンプレートのキャッシュ戦略を見直す必要があります。
- これまでは主に「strict locals なテンプレート」を前提に Ractor 対応が進んでいましたが、この PR では「non-strict(従来どおりの locals の扱い)テンプレート」も Ractor から使えるようにしています。
UnboundTemplate の内部キャッシュ構造の変更
これまで
ActionView::UnboundTemplate は、おおざっぱにいうと:
@templatesという Hash で、「ローカル変数の正規化済みセット → コンパイル済みテンプレート」の対応をキャッシュ- 排他制御に Mutex
といった形でテンプレートを保持していました。
# イメージ(実際のコードとは異なります)
class UnboundTemplate
def initialize(...)
@templates = {} # { normalized_locals_key => compiled_template }
@lock = Mutex.new
end
def bind_locals(locals)
normalized_key = normalize_locals(locals)
@lock.synchronize do
@templates[normalized_key] ||= compile_template_with(locals)
end
end
endこの PR での変更点
非 strict テンプレート + frozen な UnboundTemplate の場合:
@templatesではなく、Ractor ローカルなConcurrent::Mapを使ってキャッシュします。- Ractor 毎に別の Map を持つので、Ractor 間でオブジェクトを共有しない設計になります。
- さらにその内側で、競合を避けるためにもう一層
Concurrent::Mapを利用して、従来の Mutex 相当の排他を行います。 - これにより「同じ
UnboundTemplateでも、各 Ractor ごとに独立したテンプレートキャッシュを持つ」構造になります。
strict locals テンプレート について:
- strict locals 用には
@strict_locals_templateというインスタンス変数にコンパイル済みテンプレートを持つように整理されており、その読み出しパスが明示化されました。 - 従来の Hash (
@templates) のデフォルト値として strict テンプレートを持っていたため、@templates.valuesに strict テンプレートが含まれず、後述のバックトレース補正に支障が出ていました。この構造を改めたことで不具合が解消されています。
- strict locals 用には
エイリアシング(locals 正規化の alias)を維持
- 非 strict locals テンプレートで、「正規化前の異なる locals 指定」が「同一の正規化済 locals セット」にマッピングされる場合は、同じコンパイル済みテンプレートを再利用する、というエイリアシング挙動があります。
- これは locals 正規化を「高速パスでは行わない」ための工夫でもあり、本 PR でもこの挙動を維持しています。
- つまり、Ractor ローカルな Map を使いつつも、「locals の正規化キー → テンプレート」というキャッシュの構造・意味は変えないように設計されています。
frozen?をトリガーにした挙動切り替えUnboundTemplateが#freezeされたかどうかで、
「Ractor ローカルキャッシュを使うか(frozen 時)」
「従来どおりの単一キャッシュを使うか(非 frozen 時)」
を切り替えるようになっています。- Ractor から利用する前提だと、通常はアプリケーション起動時にテンプレートが凍結されるため、Ractor 用のパスが有効になります。
バックトレース補正のバグ修正
ActionDispatch::ExceptionWrapper#build_backtrace は、例外が ERB テンプレート内で発生した場合に、
- 実際の
.html.erbファイルの行番号に対応するように - Ruby コードに変換されたあとの行番号を補正する
といった処理を行います。
この処理では、UnboundTemplate が保持している「コンパイル済みテンプレート群」から情報を取得しますが、
- strict locals のテンプレートを
「@templatesHash のデフォルト値」として持っていたため @templates.valuesに strict テンプレートが含まれず- 結果として、strict locals テンプレートに対するバックトレース補正が正しく行われない
という問題がありました。
この PR の「3つ目の commit」で、strict locals テンプレートがきちんと列挙されるようにデータ構造が修正され、build_backtrace による補正が期待どおり働くようになっています。
テスト・周辺変更
actionview/test/template/file_system_resolver_test.rb- ファイルシステム経由でテンプレートを解決する際に、Ractor まわりの挙動・キャッシュ挙動を検証するテストが多数追加されています(+80/-11)。
railties/test/application/ractors_test.rb- Rails アプリケーションから Ractor を使ってビューをレンダリングするケースをカバーするテストが追加・拡張されています。
actionview/lib/action_view/base.rb,actionview/lib/action_view/template/handlers/erb.rb,railties/lib/rails/application.rb- Ractor 対応・strict locals 対応に合わせて、それぞれ 1 行程度の微修正が入っています(主にフラグ・フックの追加や呼び出し先の切り替えレベル)。
- 影響範囲・注意点
Ractor を使わないアプリケーションへの影響
- パフォーマンス面:
- 非 strict locals テンプレートに対して:
- 追加されるのは「
frozen?チェック」が 1 回程度。
- 追加されるのは「
- strict locals テンプレートに対して:
- 「
@strict_locals_templateがあるかどうかの ivar チェック」が 1 回。 - Hash の
[]アクセス + デフォルト値 から、単純な ivar 読み出しへ置き換えられているので、むしろコードパスがシンプルになっています。
- 「
- いずれもオーバーヘッドは非常に小さく、実用上ほぼ影響はないと考えてよいレベルです。
- 非 strict locals テンプレートに対して:
- 機能面:
- バックトレース補正が改善され、strict locals テンプレートで発生したエラーのソース位置がより正確になります。
- 既存コードの挙動を変えるような仕様変更は基本的にありません。
Ractor を使うアプリケーションへの影響
- 非 strict locals テンプレートを Ractor から使うことが「技術的に可能」になります。
- ただし PR の説明にもある通り、非 strict locals テンプレートを Ractor から使うこと自体は推奨されていません。
- 理由としては、locals の扱いが暗黙的であることにより、将来的な Ractor セーフティの維持が難しくなったり、予期しない共有や依存が紛れ込みやすいためです。
- それでも、
- 既存テンプレートをすべて strict locals 化するのが困難なアプリ
- 段階的に strict 化していきたいアプリ にとっては、「まず Ractor 対応を進めるための移行ステップ」として有用です。
注意点
- Ractor からレンダリングするテンプレートは、
できるだけ strict locals 化し、「Ractor セーフな依存関係」にしておくことが望ましいです。 - 非 strict テンプレートでも動くようになりましたが、
- グローバル状態
- スレッドローカル / Fiber ローカル
- 共有可能でないオブジェクト 等への依存があると、Ractor 内からの利用で思わぬ問題を引き起こす可能性があります。
- 参考情報 (あれば)
- この PR の前提となる PR: #58396
- strict locals を中心にした Ractor 対応の基盤部分がここで導入されており、本 PR はそのフォローアップとして「non-strict テンプレートへの対応」「strict locals 周りのバグ修正」を行っています。
- 関連クラス・メソッド:
ActionView::UnboundTemplateActionView::Template::Handlers::ERBActionDispatch::ExceptionWrapper#build_backtrace
- Ractor の仕様や制約を再確認したい場合は、公式ドキュメント(Ruby 3.x の Ractor セクション)および Rails ガイドの並行処理関連ドキュメントを見ると理解が進みます。
#58403 Move duplicated conditional_executor up to Rails::Command::Base
マージ日: 2026/8/12 | 作成者: @seuros
- 概要 (1-2文で)
Rails::Command::ConsoleCommandとRails::Command::RunnerCommandに重複していたconditional_executorの実装が、共通の親クラスであるRails::Command::Baseに集約されました。これにより、同様の挙動を持つコマンド間でロジックを共有できるようになり、重複コードが削減されています。
- 変更内容の詳細
※ PR本文に説明がないため、Rails 既存実装の慣例と変更された行数・ファイル構成から推測を含みます。
どんなリファクタリングか
- 変更前:
Rails::Command::ConsoleCommandにconditional_executorメソッド (もしくは同等のロジック) が定義されていたRails::Command::RunnerCommandにもほぼ同一のconditional_executorが定義されていた
- 変更後:
- この重複コードが削除され (
+0/-8が 2 ファイルで発生) - 共通のスーパークラスである
Rails::Command::Baseにconditional_executorが新たに追加されました (+9/-0)
- この重複コードが削除され (
イメージとしては、以下のようなリファクタリングが行われています:
# 変更前 (イメージ)
module Rails
module Command
class ConsoleCommand < Base
private
def conditional_executor
# ここに console, runner でほぼ同じロジックがあった
end
end
end
end
module Rails
module Command
class RunnerCommand < Base
private
def conditional_executor
# ConsoleCommand とほぼ同じ
end
end
end
end# 変更後 (イメージ)
module Rails
module Command
class Base
private
def conditional_executor
# ConsoleCommand / RunnerCommand 共通のロジック
end
end
end
end
module Rails
module Command
class ConsoleCommand < Base
# conditional_executor は Base から継承
end
end
end
module Rails
module Command
class RunnerCommand < Base
# 同上
end
end
endconditional_executor が担っている可能性が高い役割
Rails のコマンド実装で「conditional ~」という名前は、環境やオプションに応じて実行器(executor)を切り替えるような用途でよく使われます。
この PR で登場する conditional_executor も、例えば以下のような責務を持っていると考えられます(あくまで代表的なパターンの推測です):
- 開発環境・本番環境・テスト環境などによって異なる実行戦略を選ぶ
- Spring や Bootsnap、マルチスレッド/マルチプロセス実行の有無に応じて実行方法を変える
--environmentや--no-colorなどの CLI オプションに応じて実行コンテキストを切り替える
共通の Base に集約されたことで:
- Console と Runner どちらのコマンドも、同じ条件判定・同じ実行戦略を共有する
- 将来ほかのコマンドでも同じ仕組みを使いたくなった場合、
Baseだけ修正すればよい
といったメリットがあります。
- 影響範囲・注意点
影響範囲
- 直接的な影響を受けるのは、主に以下のクラスです:
Rails::Command::ConsoleCommandRails::Command::RunnerCommand
- 実装はリファクタリング(共通化)であり、外部 API やコマンドの挙動を変えることを目的とした変更ではないと考えられます。
- そのため、通常の
rails consoleやrails runnerの利用者には、動作上の変化は基本的にありません。
- そのため、通常の
注意点(カスタマイズしている場合)
以下のようなケースでは影響を受ける可能性があります:
独自に Rails コマンドクラスを定義している場合
Rails::Command::Baseを継承した自作コマンドで、conditional_executorと同名メソッドを定義していた場合:- メソッド解決順序的には自クラスの定義が優先されるため、即座の壊れ方はしませんが、
- 名前の衝突により、将来の Rails 側の挙動変更と干渉するリスクがあります。
- 同名メソッドを避けるか、もし意図的に上書きしているなら、
super呼び出しや挙動の差分を意識して実装する必要があります。
Rails::Command::ConsoleCommand/RunnerCommandを monkey patch している場合- 以前、これらのクラスの
conditional_executorを直接上書きしていた場合、- メソッドの定義場所が
Baseに移ったことで、パッチが想定どおりに効かなくなる可能性があります。
- メソッドの定義場所が
- Rails のバージョンアップ時に、
method_sourceなどで定義場所を再確認したほうが安全です。
- 以前、これらのクラスの
テストや計測で内部メソッドを直接呼び出している場合
- テストコードが
Rails::Command::ConsoleCommand#conditional_executorをsend/public_sendなどで直接叩いている場合:- 定義場所の変更自体は Ruby 的には透過的ですが、将来的なリファクタリングで可視性や挙動が変わる可能性があるため、
- 内部メソッドへの依存を最小限にするほうがメンテナンス性は高くなります。
- テストコードが
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58403
- この PR は重複コードの整理・共通化が主目的であり、機能追加や仕様変更ではなく、内部実装のクリーンアップに分類される変更です。
- 同系統のリファクタリングが続く場合、
Rails::Command::Baseが「Rails CLI コマンドの共通基盤」としてより強く位置づけられていく可能性があり、自作コマンドやエクステンションを実装する際にここを起点に読むと理解しやすくなります。
#58436 Introduce builder method for alter table definitions
マージ日: 2026/8/12 | 作成者: @adrianna-chang-shopify
- 概要 (1-2文で)
Rails のスキーマ定義まわりに、テーブル変更用の新しいビルダーbuild_alter_table_definitionが追加され、既存のcreate_alter_table呼び出しに依存せず「テーブル変更定義オブジェクト(AlterTable)」を組み立てられるようになりました。これにより、create_tableと同様のスタイルで「変更予定内容」をオブジェクトとして扱えるようになり、DDL 生成や外部マイグレーション管理との連携がしやすくなります。
- 変更内容の詳細
新しく追加されたメソッド: build_alter_table_definition
ActiveRecord::ConnectionAdapters::SchemaStatements に、テーブル変更定義を構築するためのビルダーメソッドが追加されています。
ポイント:
build_alter_table_definition(table_name)がAlterTableオブジェクトを返す- 返却される
AlterTableは「そのテーブルに対して行いたい変更」を表現するオブジェクト - これまで内部的に使われていた
create_alter_table(table_name)を直接呼ばずに、ビルダー経由で変更定義を構築できるようにするのが目的 build_create_table_definitionと同様の設計だが、build_alter_table_definitionは:nodoc:扱い(内部API的な位置付け)
イメージとしては、build_create_table_definition でやっていることの「alter table 版」です。既存のコードでは、ALTER TABLE 用の DDL を用意するときに、アダプタ内部メソッド connection.send(:create_alter_table, table_name) のように send でプライベートメソッドを叩く必要がありましたが、その必要がなくなります。
既存アダプタの追従
以下のファイルで ALTER TABLE 周りの実装が build_alter_table_definition に追従する形で変更されています。
abstract_mysql_adapter.rbpostgresql/schema_statements.rb
主な変更点:
- それぞれのアダプタで、テーブル変更定義を組み立てる際に、新ビルダーを利用するような形に整理(行数としては +3/-3 や +6/-6 程度の小変更)
- ロジックの大枠は変えず、「AlterTable オブジェクトをビルダーで組み立て、そこから DDL を生成する」という一貫したパターンにリファクタリング
テストの追加・更新
activerecord/test/cases/migration/schema_definitions_test.rbにテストが追加(+18/-2)build_alter_table_definitionが正しくAlterTableオブジェクトを生成し、期待通りの変更情報を保持しているかを検証
abstract_mysql_adapter/connection_test.rbも 1 行レベルで更新され、アダプタの振る舞い整合性を確認
コード例(イメージ・擬似的な使用例として):
# connection: ActiveRecord::Base.connection
alter = connection.build_alter_table_definition("users")
alter.add_column("age", :integer)
alter.add_index(["email"], unique: true)
# 生成された AlterTable オブジェクトを元に DDL を組み立てて
# 外部の「マイグレーション管理サービス」に送る、といった利用が想定されている
ddl = some_custom_ddl_builder.to_sql(alter)
send_to_central_migrations_manager(ddl)実際の外部APIはこの PR には含まれていませんが、「AlterTable をビルダーで構築 → その情報から DDL を生成」という流れを実現するための基盤部分が今回の変更です。
- 影響範囲・注意点
- Rails アプリ開発者(通常のマイグレーションを書く側)への直接的な変更はほぼありません。
- 既存の
change_table/add_column/remove_columnなどの DSL はそのまま動く想定です。
- 既存の
- 影響がある可能性があるのは、以下のようなケースです:
- アダプタ内部API(
create_alter_tableなど)をsendで直接叩いて独自に DDL を生成していたような高度なメタプログラミング/ツール - スキーマ定義ビルダー (
build_create_table_definitionなど) を使って独自にマイグレーション管理・DDL 生成をしているツールやライブラリ
- アダプタ内部API(
注意点:
build_alter_table_definitionは:nodoc:のため、公式的には「内部API」扱いであり、将来的な変更の余地があります。- とはいえ、
build_create_table_definitionと対になる概念として導入されているため、スキーマ生成系の周辺を拡張・統合する場合には、今後こちらを利用した実装が「正攻法」になっていく可能性があります。 - MySQL・PostgreSQL のアダプタ実装がリファクタリングされていますが、テストも更新されており、挙動としての互換性は維持される想定です。
- 参考情報 (あれば)
- 元 PR: https://github.com/rails/rails/pull/58436
- 背景となる PR(前提変更): https://github.com/rails/rails/pull/58264
- こちらでスキーマ定義ビルダーまわりの整理や、中央集約的なマイグレーション管理のための基盤整備が進められており、本 PR はその follow-up です。
#57846 Fix normalizes on an enum attribute raises ArgumentError instead of normalizing before enum casting
マージ日: 2026/8/12 | 作成者: @joaoGabriel55
- 概要 (1-2文で)
normalizesとenumを同じ属性に併用した際、正規化前の値に対してenumが先にバリデーションしてArgumentErrorを投げていた問題を修正した PR です。正規化を「enum の検証より先」に行うようにし、ユーザー入力を正しくサニタイズしてから enum キャスト・検証できるようになりました。
- 変更内容の詳細
何が問題だったか
次のようなモデルを考えます:
class Order < ApplicationRecord
enum :status, { pending: "pending", confirmed: "confirmed" }
normalizes :status, with: ->(v) { v.strip.downcase }
end
Order.new(status: " Pending ")
# 以前の挙動:
# => ArgumentError: ' Pending ' is not a valid statusnormalizes は status に代入された値を strip.downcase で "pending" にしてくれるはずですが、実際には " Pending " のまま enum 側のバリデーションに渡されてしまい、「有効な status ではない」として ArgumentError が発生していました。
背景にある実装構造は以下のようなものです。
enumもnormalizesも、Active Model の「型(type)」をラップするデコレータとして実装されているenum+normalizesを使うと、型のスタックはざっくりNormalizedValueType → EnumType → もとの型という構造になるActiveModel::Attribute#with_value_from_userは、ユーザー入力に対してtype.assert_valid_value(value)を「生の値」で呼び出す- その後に
castして内部表現に変換する
という順で処理する
NormalizedValueType は cast はオーバーライドして正規化を挟んでいましたが、assert_valid_value はオーバーライドしておらず、そのまま内側の EnumType に「生の値」を渡していました。その結果:
" Pending "が正規化される前にEnumTypeに渡るEnumTypeは" Pending "を知らないのでArgumentErrorを投げる
という不整合な流れになっていました。
今回の修正内容
ActiveModel::Attributes::Normalization::NormalizedValueType に assert_valid_value のオーバーライドを追加し、「正規化済みの値」に対して下層の型 (cast_type) のバリデーションを行うように変更しました。
擬似コードで表すと、PR で追加されたのは以下のようなロジックです:
def assert_valid_value(value)
cast_type.assert_valid_value(cast(value))
endこれにより、実際の流れはこうなります:
- ユーザー入力:
" Pending " NormalizedValueType#assert_valid_valueが呼ばれるcast(value)により"pending"に正規化される- 内側の
EnumType.assert_valid_value("pending")が呼ばれる "pending"は有効な enum 値なのでエラーにならない
一方で、本当に不正な値は正規化後にも検出されます:
Order.new(status: " bogus ")
# 正規化後は "bogus"
# => ArgumentError: 'bogus' is not a valid statuscast で保存される値と、assert_valid_value で検証される値が一致するようになった、というのが設計上のポイントです。
テストとドキュメント
activerecord/test/cases/normalized_attribute_test.rbに回帰テストを追加- 正規化によって enum にマッチするようになるケース
- 正規化してもなお不正な値のケース
の両方を確認
activemodel/CHANGELOG.mdに挙動変更として追記
(バグ修正だが、enum + normalizes の組み合わせの実行時挙動がユーザーにとって意味のある形で変わるため)
- 影響範囲・注意点
- 影響を受けるのは「normalizes が適用されている属性」だけです。
normalizesを使っていない属性や、enum 単体の挙動には変更はありません。 - 既存で
enum + normalizesを併用していた場合:- これまで「正規化されるはずだったが、
ArgumentErrorで落ちていた」ケースが、期待どおり正規化されてから enum として受け入れられるようになります。 - もしアプリケーション側で、この
ArgumentErrorを rescue したり、メッセージに依存したロジックを書いていた場合は、その挙動が変わる可能性があります(ただし、そのようなコードはもともと「正しく正規化されていない」というバグに依存していたことになります)。
- これまで「正規化されるはずだったが、
- 入力バリデーションの観点:
" Pending "→"pending"のような「表記ゆれ」の吸収が enum と安全に組み合わせて行えるようになります。- 利用者は、
normalizes内で「期待しない値を別の合法値にマップする」ようなロジックを書かない限り、異常系の検出はこれまで通りArgumentErrorで行えます。
- 参考情報 (あれば)
- 対応 Issue: https://github.com/rails/rails/issues/57828
- 変更ファイル:
activemodel/lib/active_model/attributes/normalization.rbactiverecord/test/cases/normalized_attribute_test.rbactivemodel/CHANGELOG.md
- 関連する Rails 機能:
- Active Model Normalization (
normalizes) - Active Record
enum ActiveModel::Attribute#with_value_from_userの型検証フロー
- Active Model Normalization (
#58455 Show bin/console startup banner based on IRB
マージ日: 2026/8/12 | 作成者: @byroot
- 概要 (1-2文で)
bin/console実行時に表示される起動バナーを、Rails 独自実装ではなく IRB が持つバナー表示機構に合わせて出すように整理し、Rails 側のバナー制御ロジックを IRB ベースに再設計した PR です。これにより、IRB の設定やバージョンに応じた一貫したバナー表示が行われ、テストもそれに合わせて拡充されています。
- 変更内容の詳細
※元 PR (#58393) の再オープン版であり、方針はそちらを引き継いでいます。
2-1. bin/console 起動バナーの扱いを IRB ベースに変更
railties/lib/rails/commands/console/console_command.rb とrailties/lib/rails/commands/console/irb_console.rb の変更が中心です。
主なポイント:
- これまで Rails が独自にバナー(「Loading development environment (Rails x.x.x)」のような行)を表示する処理を持っていましたが、それを IRB の起動時処理に合わせて出すように整理
- IRB 側のオプションや設定(例:
IRB.conf[:USE_COLOR]やバナーの有効/無効)を尊重して表示タイミングや出し方を決定 - Rails のコンソール起動処理 (
Rails::Console/Rails::Console::IRBConsoleまわり) に、起動バナー表示に関するロジック・フックを追加
概念的には次のようなフローに整理されています(擬似コード):
# Rails::Console::IRBConsole の中 (イメージ)
def start
# IRB の設定・context 準備
setup_irb
# IRB が起動するタイミングに合わせて、Rails バナーを表示
show_startup_banner_if_needed
# IRB メインループ開始
IRB.start
endまた、環境情報や Rails バージョンなどを含んだバナー文字列を専用メソッドで構築するような形になっている可能性が高いです:
def startup_banner
"Loading #{Rails.env} environment (Rails #{Rails.version})"
endこれを IRB のバナー表示ロジックと整合が取れる位置で出力することで、
- IRB が管理する標準のバナーと二重表示しない
- IRB の設定でバナーを抑制したときに Rails 側も無駄に出さない
といった挙動に寄せています。
2-2. コンソールコマンドの振る舞い整理
rails/commands/console/console_command.rb 側では:
bin/rails console実行時に、どのコンソールアダプタ(IRB / Pry など)を使うか判断する処理の中で、バナー表示の判定も行うように整理- 一部オプション(
--sandboxや--environmentなど)との組み合わせ時のメッセージ出し分けを、IRB ベースのバナー表示に合わせて調整
例として、従来:
$ bin/rails console
Loading development environment (Rails 7.2.0)
irb(main):001:0>のように Rails 側が先にバナーを出し、その後 IRB prompt という流れだったのに対し、この PR 後は IRB の起動処理にフックすることで、IRB のライフサイクルに沿った形に近づきます(見た目は大きく変わらないが、内部的には IRB 主導)。
2-3. テストの追加・強化
railties/test/commands/console_test.rb に以下のようなテストが追加・修正されています:
bin/rails console実行時に、バナーが期待どおり 1 回だけ表示されることの検証RAILS_ENVや--environment、--sandbox指定時に、環境名などがバナーに正しく反映されることの検証- IRB を使っていないケース(例えば Pry を使う設定)では、IRB 用のバナー処理が走らないことの検証
擬似的なテスト例イメージ:
test "console prints Rails banner once" do
output = run_console_command
assert_includes output, "Loading test environment (Rails"
assert_equal 1, output.scan(/Loading .* environment \(Rails/).size
end- 影響範囲・注意点
- 対象:
bin/rails console/bin/consoleを利用するすべての Rails アプリ - 実行時の見た目
- 通常は、これまでとほぼ同じ文言のバナーが表示されますが、IRB の挙動・設定によっては表示タイミングや微細なフォーマットが変わる可能性があります。
- IRB のバナーや標準出力にフックしている gem(例: 独自に IRB 起動時メッセージを挿入するツール)との組み合わせで、順序が変わったり、表示の有無が変化する可能性があります。
- カスタムコンソール/ラッパーとの相性
Rails::ConsoleやIRBConsoleを継承・モンキーパッチして独自バナーや独自起動処理を組み込んでいる場合、今回の変更で呼び出しタイミングが変わることで、想定外の二重表示・未表示が起きるリスクがあります。- そのようなコードを書いている場合は、バナー表示の条件やフックポイント(
setup_irb,start, など IRBConsole 内部の呼び出し順)を一度確認することを推奨します。
- IRB バージョン依存の可能性
- バナー制御を IRB 側の API/挙動に寄せているため、Ruby/IRB のバージョン差異で細かい挙動が変わる可能性があります。CI などで複数 Ruby バージョンを回している場合は、
rails consoleの標準出力がテストに利用されていないか確認すると安心です。
- バナー制御を IRB 側の API/挙動に寄せているため、Ruby/IRB のバージョン差異で細かい挙動が変わる可能性があります。CI などで複数 Ruby バージョンを回している場合は、
- 参考情報 (あれば)
- 対象 PR:
- 関連ファイル:
railties/lib/rails/commands/console/console_command.rbrailties/lib/rails/commands/console/irb_console.rbrailties/test/commands/console_test.rbrailties/CHANGELOG.md(「console の起動バナーが IRB に基づくようになった」旨のエントリが追記)
#58454 Avoid duplicating fixtures path
マージ日: 2026/8/12 | 作成者: @byroot
- 概要 (1-2文で)
このPRは、Rails のテストヘルパー内部で「fixtures のパスを二重に登録してしまっていた」処理を修正し、不要な重複をなくすことでテスト実行を軽量化しています。動作上の機能追加や仕様変更はなく、パフォーマンスと内部実装の整理が主目的です。
- 変更内容の詳細(あればサンプルコードも含めて)
- 対象ファイル:
railties/lib/rails/test_help.rb - 変更行数: 1行追加 / 1行削除 (実質は1行の修正)
rails/test_help は、Rails のテスト環境(Minitest 等)をセットアップするための共通ヘルパーで、ActiveSupport::TestCase や ActiveRecord::FixtureSet の設定などを行っています。
今回のPRでは、その中で「fixtures のパスを設定する処理」で、同じパスを重複して登録してしまっていた箇所を修正しています。
重複登録があると、例えば以下のような問題が起き得ます:
- 同じディレクトリを2回スキャンするため、テストの起動時・実行時がわずかに遅くなる
- 内部的なループや配列操作が増えることで、無駄なオーバーヘッドが発生する
- 場合によっては、fixtures 読み込み処理の二重実行による副作用の温床になりかねない
PR 説明文では:
Just makes things slower for not reason.
とある通り、動作上のメリットが一切ない重複処理だったため、その不要な重複を削除する形になっています。
元になった議論/修正対象は以下のコメントです:
https://github.com/rails/rails/pull/57352#issuecomment-5260608367
(具体的な1行の差分は公開されていませんが、典型的には下記のようなイメージの変更です)
# 修正前(イメージ)
ActiveSupport::TestCase.fixture_paths << Rails.root.join("test/fixtures")
ActiveSupport::TestCase.fixture_paths << Rails.root.join("test/fixtures") # ← 重複
# 修正後(イメージ)
ActiveSupport::TestCase.fixture_paths << Rails.root.join("test/fixtures")あるいは、すでに fixture_paths に含まれるパスを再度追加していた処理を1回のみにする、もしくは uniq 的な重複排除を挟むような変更と考えられます。
- 影響範囲・注意点
機能面の影響:
- fixtures の検索パスが変わったり、どの fixtures が読み込まれるかが変化するわけではありません。
- 既存のテストコードの挙動は基本的に変わりません。
パフォーマンス面:
- 大規模なテストスイートや fixtures が大量にあるプロジェクトでは、テスト起動時や fixtures 読み込み時のオーバーヘッドがわずかに減る可能性があります。
- ただし1行レベルの最適化であるため、体感できるほどの劇的な改善ではなく、「無駄を1つ消した」という整理の意味合いが強いです。
互換性・移行時の注意点:
- fixtures のパス設定を Rails の標準的な方法 (
test/fixturesなど) で行っている限り、特別な対応は不要です。 - もし独自に
fixture_pathsを上書き・追記しているメタなコードを書いている場合でも、パスの重複に依存しているような特殊な実装をしていなければ影響はありません。 - 「fixtures パスが複数回登録されている前提で、内部の配列の長さや順序に依存したコード」を書いていると影響が出る可能性がありますが、そのようなコードは通常想定されていません。
- fixtures のパス設定を Rails の標準的な方法 (
- 参考情報 (あれば)
元の問題指摘・背景:
- https://github.com/rails/rails/pull/57352#issuecomment-5260608367
- 以前の PR (#57352) の中で、「fixtures パスの重複登録」に関する指摘があり、それを解消するフォローアップとして今回の PR (#58454) が出ています。
- https://github.com/rails/rails/pull/57352#issuecomment-5260608367
関連する Rails コードの読みどころ:
railties/lib/rails/test_help.rb- Rails のテスト環境をセットアップする中心的なファイルで、
ActiveSupport::TestCaseの拡張や fixtures のロード設定などがまとまっています。
- Rails のテスト環境をセットアップする中心的なファイルで、
active_record/fixtures/ActiveRecord::FixtureSet- fixtures のロード・管理周りの実装に興味がある場合はこちらも読むと理解が深まります。
#57980 Construct And nodes as Nary instead of Binary
マージ日: 2026/8/12 | 作成者: @skipkayhil
- 概要 (1-2文で)
このPRは、Active Record のクエリ条件を組み立てる際のAndノードの構造を、「二分木」ではなく「N分木(可変長)」として構築するように変更したものです。これにより、複数条件を AND で結合したときの AST(抽象構文木)の形がフラットになり、扱いやすく・最適化しやすくなります。
- 変更内容の詳細
何が問題だったか
従来の実装では、複数の条件を AND で結合する際に reduce を使っていたため、次のような「左に深い二分木」の構造になっていました。
And
And
And
Query
Query
Query概念的には、例えば:
rel.where(a: 1).where(b: 2).where(c: 3)のようなクエリ条件を内部表現に落とし込むとき、
- ((((a = 1) AND (b = 2)) AND (c = 3)) …)
のような連鎖する2項 AND のツリーになっていたイメージです。
このPRでどう変わったか
このPRでは、activerecord/lib/active_record/relation/predicate_builder.rb のロジックを変更し、And ノードに複数の子ノードを直接ぶら下げる「N分木」として構築するようにしています。
新しい構造は次のようになります:
And
Query
Query
Queryつまり、同じく
rel.where(a: 1).where(b: 2).where(c: 3)に相当する内部表現が、
- AND(a = 1, b = 2, c = 3)
のような「1つの And ノードに複数の条件を子要素として持つ」形になります。
具体的なコードイメージ(擬似的)
※実際のコードは 7 行追加・1 行削除と小さい変更ですが、イメージとしては以下のような変化です:
従来(reduce で二分木を作るイメージ):
nodes.reduce do |memo, node|
Arel::Nodes::And.new(memo, node) # 2つの子を取るBinaryなAnd
end変更後(フラットな Nary And を作るイメージ):
if nodes.length == 1
nodes.first
else
Arel::Nodes::And.new(nodes) # 配列を受け取ってNaryなAnd
end実際には Arel 側の Arel::Nodes::And が Nary をサポートしており、それに合わせて Active Record 側の Predicate Builder が、reduce ベースの2項構築をやめて最初から配列で And を構築するようにしている、という変更と考えられます。
- 影響範囲・注意点
対象範囲
- Active Record の内部でクエリ条件を Arel ノードに変換する部分(
PredicateBuilder)の挙動が変わります。 - 主に「複数条件を AND で結合する」ケースに影響します(
where,having,joins等、条件を構築するあらゆる箇所で利用される可能性があります)。
- Active Record の内部でクエリ条件を Arel ノードに変換する部分(
互換性への影響
- 通常のアプリケーションコード(
Model.where(...)を書いている側)から見える SQL は論理的には同じです。 - ただし、次のようなケースでは影響が出る可能性があります:
- Arel ノードの構造(ツリー形状)を前提に独自解析・パターンマッチをしているコード
Arel::Nodes::Andを「常に左再帰的な二分木」と見なしてトラバースしているメタプログラミング・ライブラリ
- こうしたコードは、
Andが「1つのノードに複数の子を持つ」構造になることを前提に修正が必要になります。
- 通常のアプリケーションコード(
パフォーマンス面
- フラットな Nary 構造になることで:
- 深いネストをたどる必要がなくなるため、条件のトラバースコストが軽減される可能性があります。
- 最適化やノードの再結合・簡約(例: 重複条件の削除)などがやりやすくなります。
- 逆に、Arel ノード構造のサイズや生成コストに目立った悪化要因はなく、実質的には改善寄りの変更と考えられます。
- フラットな Nary 構造になることで:
デバッグ・ログ出力
- Arel ノードをインスペクションしてデバッグしている場合、
Andの見え方(ネストの深さなど)が変わります。 - ツリーを手で追っていた場合、「ネストが浅くなった」ように見えるので注意してください。
- Arel ノードをインスペクションしてデバッグしている場合、
- 参考情報 (あれば)
関連しそうな箇所:
Arel::Nodes::Andクラスの実装(Nary サポートの追加・変更が先行している可能性が高い)ActiveRecord::Relation::WhereClause/PredicateBuilderあたりのコード(条件生成の呼び出し元)
想定される今後の発展:
- Nary
Andに統一されたことで、将来的に:- 条件式の正規化・簡約
- 複雑なクエリの自動最適化(例えば
((A AND B) AND (A AND C))のような式の再構成)
- といった機能強化がしやすくなります。
- Nary
このPRは主に内部構造の改善ですが、Arel ノード構造に依存したカスタム実装を持っている場合は、And がフラットな Nary ノードになる前提でコードを見直しておくと安全です。
#58388 Cleanup duplicate/unneeded requires
マージ日: 2026/8/11 | 作成者: @skipkayhil
- 概要 (1-2文で)
Rails内部のいくつかのファイルから、すでに他所で読み込まれているため不要となっていたrequireを削除し、requiresの重複を整理したPRです。機能追加や挙動変更はなく、コードベースのクリーンアップ・メンテナンスが目的です。
- 変更内容の詳細
このPRでは、./tools/railspect requires . という内部ツールで検出された「重複または不要な require」を削除しています。
ツール出力(抜粋):
actionpack/lib/action_controller/metal/renderers.rb
- active_support/deprecation (active_support/rails)
actionpack/lib/action_controller/metal.rb
- active_support/core_ext/module/delegation (active_support/rails)
actionpack/lib/action_dispatch/http/mime_type.rb
- active_support/deprecation (active_support/rails)
actionview/lib/action_view/helpers/navigation_helper.rb
- active_support/concern (active_support/rails)これに基づいて、以下の4ファイルから合計5行の require が削除されています。
actionpack/lib/action_controller/metal.rb- 削除:
require "active_support/core_ext/module/delegation"
- 削除:
actionpack/lib/action_controller/metal/renderers.rb- 削除:
require "active_support/deprecation"など合計2行(= diff -2)
- 削除:
actionpack/lib/action_dispatch/http/mime_type.rb- 削除:
require "active_support/deprecation"
- 削除:
actionview/lib/action_view/helpers/navigation_helper.rb- 削除:
require "active_support/concern"
- 削除:
これらはいずれも、active_support/rails など他のエントリポイントから既に読み込まれており、個々のファイルで明示的に require する必要がないと判断されたものです。
結果として、追加行は0行で、削除行のみ(-5)という純粋な削除PRになっています。
- 影響範囲・注意点
既存機能への影響
- Rails本体としては、依存関係の読み込み順やロードパスの前提が変わっていない限り、挙動は変わりません。
- 削除された
requireは、すでに別の場所から必ずロードされることが保証されている(とメンテナが判断している)ため、通常のアプリケーション開発者にとっては影響はありません。
Railsを特殊な形で利用している場合の注意点
- もし自前のスクリプト等で Rails の特定ファイルだけを単体で
requireしており、かつactive_support系を事前に読み込んでいない場合:- 例:
require "action_controller/metal/renderers"だけを直接ロードするような特殊な使い方
- 例:
- このような「Railsの標準的なロード順に依存していない」ユースケースだと、
ActiveSupport::DeprecationやActiveSupport::Concern、Module#delegate等が未定義になりNameErrorになる可能性があります。 - その場合は、呼び出し側で明示的に:rubyなどを追加する必要があります。
require "active_support/deprecation" require "active_support/concern" require "active_support/core_ext/module/delegation"
- もし自前のスクリプト等で Rails の特定ファイルだけを単体で
パフォーマンス・メモリ
- require の削減により、わずかではありますがロード時のオーバーヘッドを減らす効果があります。ただし、数行レベルなので体感できる差はほぼありません。
- 参考情報 (あれば)
- PRで利用されているツール:
./tools/railspect requires .- Railsリポジトリ内の
require依存関係を解析し、重複や不要なものを抽出するための内部ツールと考えられます。
- Railsリポジトリ内の
- 今回のようなPRは、Railsのコードベースを長期的に保守しやすくするための「テクニカルデット削減」の一環であり、他のコンポーネントでも同様のクリーンアップが行われている可能性があります。
#58450 Dedup Reflection#plural_name
マージ日: 2026/8/11 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
ActiveRecord::Reflectionのplural_nameを「重複しない(dedup)」形で扱うようにする、小さなリファクタリングの PR です。内部実装の 1行が置き換えられただけで、外部 API の振る舞いは基本的に変えずに重複処理を整理しています。
- 変更内容の詳細
※ PR 本文に説明がなく、変更も 1行のみのため、Rails の Reflection#plural_name の既存実装・命名規則から推測を含めて解説します。
- 変更ファイル:
activerecord/lib/active_record/reflection.rb - 変更行数: +1 / -1(実質的には同じロジックを別の場所/メソッドに委譲 or 既存の仕組みを再利用)
Reflection#plural_name は、関連名(association name)から「複数形の名前」を返すためのメソッドです。
典型的には、次のような用途で使われます:
class Author < ApplicationRecord
has_many :books
end
reflection = Author.reflect_on_association(:books)
reflection.plural_name # => "books"この PR のタイトル「Dedup Reflection#plural_name」から読み取れる意図は:
plural_nameのロジックが別のメソッド(例えばnameやactive_record.name.tableizeなど)と重複していたため、- その重複を解消し、既存の共通メソッドに委譲する形へ変更した、というものです。
Rails コアの典型的なパターンだと、以下のような変更が行われている可能性が高いです(擬似コード):
# 変更前(例)
def plural_name
name.to_s.pluralize
end
# 変更後(例)
def plural_name
@plural_name ||= some_common_method_that_already_handles_pluralization
endあるいは、すでに別の場所で定義済みの @plural_name キャッシュや、関連ごとに計算済みの名前付けロジックに乗せ替える、といった形で「新たに重複したロジックを持たない」ようにした可能性があります。
要点としては:
- ロジックの重複(同じ処理のコピペ)をなくす
- キャッシュや既存の名前解決ロジックを再利用する
- 結果としてコードの保守性を上げる(将来の仕様変更時の修正箇所を減らす)
というリファクタリングです。
- 影響範囲・注意点
- 外部 API としての
Reflection#plural_nameの戻り値は基本的に変わらない想定です(内部の重複を解消しただけ)。 - ただし、次のような「Rails 内部実装に依存した高度なメタプログラミング」をしている場合は、注意が必要になります:
Reflection#plural_nameの内部処理(たとえばname.to_s.pluralizeをしていること)を前提に monkey patch しているplural_nameがメモ化される / されないタイミングに依存したコードを書いている
- 一般的なアプリケーションコード(
reflect_on_association(:foo).plural_nameを素直に呼んでいる程度)では、動作への影響はほぼないと考えて良いです。
- 参考情報 (あれば)
- 対象 PR:
rails/railsリポジトリの PR #58450: Dedup Reflection#plural_name - 関連するコード周辺を読む際のキーワード:
ActiveRecord::ReflectionActiveRecord::Reflection::AssociationReflection#name,#plural_name,#class_name,#active_recordなどの関連メタ情報取得メソッド
- Rails の命名規則/複数形化のロジックは
ActiveSupport::Inflectorによって提供されており、pluralize,tableize,underscoreなどを経由して使われます。この PR も、そうした既存インフレクタの利用経路を整理した一環である可能性が高いです。
#58411 Active Record ractor safe ivars 2
マージ日: 2026/8/11 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Active Record のクラスインスタンス変数(@default_attributesと@find_by_statement_cache)が Ractor セーフになるように実装を見直した PRです。これにより、Active Record を Ractor(マルチスレッド+並列実行)環境で安全に利用しやすくなっています。
- 変更内容の詳細
2-1. @default_attributes を Ractor セーフに
背景として、Active Record のベースクラスで利用している @default_attributes は、クラスインスタンス変数として保持されており、そのままだと Ractor 間で共有するにはオブジェクトが「Ractor シェア可能」になっている必要があります。
この PR では、
@default_attributesを「メイン Ractor 上で一度だけ構築」- 構築後にその属性セットを
freezeして不変にする
という形に変えています。
ポイント:
- メイン Ractor で一度だけ(かつ不変)にすることで、他の Ractor に安全に共有可能
- Ractor 間で書き換えが発生しないようにしている
イメージとしては以下のようなパターンになっているはずです(実際のコードは多少異なる可能性がありますが、意図のイメージとして):
# 例: メイン Ractor でのみ初期化し、freeze したものを共有
def default_attributes
@default_attributes ||= begin
attrs = compute_default_attributes
attrs.freeze
end
endここを「メイン Ractor で初期化する」という制約を守るように実装が調整されています。
2-2. @find_by_statement_cache をクラスインスタンス変数から Ractor ストレージへ移行
find_by 系のクエリで使われる @find_by_statement_cache も、これまでクラスインスタンス変数としてキャッシュ用の構造を持っていましたが、これが Ractor セーフではありませんでした。
この PR では、
@find_by_statement_cache自体を「インスタンス変数としては持たない」ように変更- 代わりに
Ractorごとに分離されたストレージを使って、ヒット・ミス用のキャッシュマップを保持する
ように変えています。
おそらく以下のようなユーティリティが ActiveSupport::Ractors に追加されています(あくまでイメージです):
# activesupport/lib/active_support/ractors.rb に追加
module ActiveSupport
module Ractors
def self.local(key)
# Ractor ごとのストレージを返す API 的なもの
end
end
end
# Active Record 側での利用イメージ
def find_by_statement_cache
ActiveSupport::Ractors.local(:find_by_statement_cache) do
# このブロックで Ractor ローカルなキャッシュを初期化する
{ hits: Concurrent::Map.new, misses: Concurrent::Map.new }
end
endポイント:
- それぞれの Ractor が独自のキャッシュを持つため、Ractor 間でのデータ競合がなくなる
- キャッシュ構造自体は
Concurrent::Mapなどのスレッドセーフなコンテナを使いつつも、「Ractor ごとに分離」されることで Ractor の制約を満たす
2-3. ActiveSupport に Ractor 用ユーティリティを追加
activesupport/lib/active_support/ractors.rb に 16 行のコードが追加され、ActiveSupport::Ractors モジュールが拡張されています。
想定される機能:
- キーに基づいて「Ractor ローカルなストレージ」を扱うための API
- 必要に応じて初期化ブロックを受け取り、その Ractor の中でのみ初期化を行う仕組み
これにより、Active Record 以外のコードからも、将来的に Ractor ローカルな状態管理をしやすくなっている可能性があります。
2-4. テストの追加
activerecord/test/cases/attributes_test.rbに 21 行追加@default_attributesが Ractor セーフに動作するか、Ractor から利用したときの挙動が検証されていると考えられます。
activerecord/test/cases/core_test.rbに 26 行追加find_by系呼び出しが Ractor 内からも正しく動作し、キャッシュが Ractor セーフであることをテスト。
activesupport/test/ractors_test.rbに 16 行追加ActiveSupport::Ractorsの API が期待通り「Ractor ローカルに」動くことをテスト。
- 影響範囲・注意点
対象:
- Active Record のデフォルト属性 (
default_attributes) find_by系メソッドのステートメントキャッシュ- 新たに追加される
ActiveSupport::Ractorsユーティリティ
- Active Record のデフォルト属性 (
互換性:
- パブリック API のシグネチャ自体は変えていないため、通常のアプリケーションコードへの影響はほぼない想定
- ただし、Active Record 内部のクラスインスタンス変数に直接アクセスしていたメタプログラミング/Monkey Patch があると挙動が変わる可能性があります
@default_attributesに直接上書きしていた場合
→ freeze により書き換えができなくなる、あるいは別のタイミングで初期化される@find_by_statement_cacheを直接参照していた場合
→ そもそもインスタンス変数として存在しなくなる
Ractor を使った並列実行の安定性向上:
- Ractor を本格利用する環境(並列 Web サーバ, job worker など)では、Active Record のベースクラスで共有していた状態が整理されるため、データ競合・Ractor 制約違反によるエラーのリスクが減ります。
パフォーマンス:
- Ractor ごとにキャッシュを持つため、従来「単一のキャッシュを全スレッド/全 Ractor で共有」していた場合とキャッシュヒット率やメモリ使用量のバランスが変わる可能性があります。
- ただし、Ractor 間共有を無理に維持するよりも、安全性を優先する設計になっていると見てよいです。
- 参考情報 (あれば)
この PR は前の PR #58399 のフォローアップとして、残っていた Ractor 非対応の ivar を解消するものです:
Ruby の Ractor とシェア可能オブジェクトについての背景:
- Ruby 3 以降の Ractor では、Ractor 間で共有できるのは「シェア可能 (shareable)」なオブジェクトのみであり、可変なオブジェクトは基本的に共有不可です。
freeze済みのオブジェクトはシェア可能になりうるため、本 PR のように「メイン Ractor で初期化して freeze」→「他 Ractor から共有して読むだけ」という設計が重要になります。
Ractor 対応を意識した Active Record 拡張やライブラリを書く場合:
- 「クラスインスタンス変数に可変な状態を持たない」
- 「必要なら Ractor ローカルなストレージを使う」
- 「共有したいものはメイン Ractor で初期化して
freezeする」
といった方針が、この PR から読み取れるベストプラクティスです。
#58449 Ensure Active Record models define all attributes
マージ日: 2026/8/11 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
Active Record のattribute定義が、モデルの「属性メソッド自動生成」と常に同期するように修正されています。これにより、あとから追加されたattributeでも「動的属性」ではなく、通常の getter/setter などのメソッドが必ず生成されるようになります。
- 変更内容の詳細
何が問題だったか
従来は、以下のようなケースで「属性メソッド生成のタイミング」と「attribute 定義のタイミング」がずれることがありました。
- モデルクラスのロード中に一度 attribute メソッドが生成されたあとで、
- 追加で
attribute :foo, :stringなどを定義した場合、
この新しく追加された foo 属性については、foo / foo= / foo? などの Ruby メソッドが 定義されず、内部的には「動的属性」として扱われる挙動が残っていました。
「動的属性」は Rails 7 以降で原則非推奨になっている仕組みで、method_missing を使って record.unknown_column みたいな呼び出しに対応していた古いパターンです。今回の PR は、この動的属性にフォールバックしてしまう残りのエッジケースをふさぐものです。
何をしているか
activerecord/lib/active_record/attributes.rb に 1 行だけ追加されており、その目的は「新しい attribute が定義されたときにも、必ず属性メソッド生成の仕組みが動くようにする」ことです。
イメージとしては、以下のようなことを常に保証する変更です(※擬似コード):
class User < ApplicationRecord
# ここで一度 attribute メソッドが生成されたとしても…
end
# 後から attribute を追加
User.attribute :nickname, :string
# 以前:
# nickname / nickname= / nickname? が定義されないことがあり、
# method_missing ベースの「動的属性」にフォールバックするケースがあった
# 今回の修正後:
# 新しく追加された nickname についても、
# user.nickname
# user.nickname = "foo"
# user.nickname?
# などのメソッドがきちんと定義されるテスト (activerecord/test/cases/attributes_test.rb で +31 行) では、以下のような観点がカバーされていると考えられます:
- すでに attribute メソッド生成済みのモデルに対して
attributeを追加しても、新しい属性がメソッドとして使えるか - 読み取り・書き込み・predicate メソッド(
foo?)などが正しく定義されているか - 追加された属性が動的属性にフォールバックしないこと
(実際のテスト名は PR 内容からの推測ですが、挙動としては上記が意図されています。)
- 影響範囲・注意点
影響範囲
- 対象:
- Active Record モデルで
attribute :xxx, :typeを利用しているコード、特にクラス定義後・実行時に attribute を追加するようなメタプログラミングをしている箇所。
- Active Record モデルで
- 変化:
- これまで「たまたま」動的属性として扱われていたケースが、正式な属性メソッドとして扱われるようになります。
respond_to?(:foo)やmethods.include?(:foo)などで、以前はfalseだったものがtrueになる可能性があります。method_missingのフックやモンキーパッチに依存していた場合、その挙動が変わる可能性があります。
注意点
- 動的属性に依存したロジックは、今後ますます動かなくなる方向なので、この変更は「推奨パス(明示的な attribute 定義)」に寄せる改善です。
- 属性が「定義済みかどうか」を
defined?(model.some_attr)やrespond_to?などで判定しているコードがある場合、結果が変わることがあります。そういったコードは、has_attribute?,attribute_names,columns_hashなど、より正式な API で判定するよう見直した方が安全です。 - ランタイムに
attributeを追加するメタプログラミングをしている場合:- 今回の変更により、その属性に対するメソッドが自動的に定義されることを前提にして構いません。
- 逆に「メソッド未定義であること」を前提にしていたような特殊な処理(
method_missingの独自実装など)がある場合は、テストが必要です。
- 参考情報 (あれば)
- Rails ガイド: Active Record の属性 API
https://guides.rubyonrails.org/active_record_basics.html#attributes - 動的属性 (Dynamic Attributes) 廃止の流れについての背景は、Rails 7 以降の CHANGELOG や関連 PR(
method_missingベースの属性アクセス削除)を参照すると理解しやすいです。
#58441 Render Markdown lists that collapse into paragraphs
マージ日: 2026/8/11 | 作成者: @55728
- 概要 (1-2文で)
このPRは、Railsソース中の:markup: markdownなRDocコメントで、本来リストとして表示されるべき部分が1つの長い段落としてレンダリングされていた箇所をMarkdownとして正しく解釈されるように修正したものです。ついでに、リストではなくサンプル出力として表示すべき箇所をコードブロック(<pre>)として出るように整えています。
- 変更内容の詳細
背景・問題点
- RailsのRDocコメントの一部は
:markup: markdownで書かれていますが、- 箇条書きリスト(
- itemや1. item)の前後の改行・インデントが不足していた - そのため
rdocでHTML生成すると<ul>/<ol>ではなく単なる1つの段落としてレンダリングされていた
- 箇条書きリスト(
- 同様に、「サンプル出力」であるべき部分がMarkdown的には通常テキストとして扱われ、見た目が崩れていました。
このPRでは */lib 以下の :markup: markdown コメントを総なめし、問題のある箇所をすべて修正しています(#58439 のフォローアップ)。
主な修正箇所と内容
Markdownレンダリングの観点でいうと、ほぼすべて「空行の追加」または「インデント・マーカー調整」によるリスト/コードブロック化です。
1. ActionController::TestCase (ordered list)
元の状態(実際には1行でつながっている形):
Functional tests are written as follows: 1. First, one uses the get, post, patch,
put, delete, or head method to simulate an HTTP request. 2. Then, one asserts
whether the current state is as expected. ...Markdownのルール的には「文の途中に 1. や 2. がある」扱いになり、単なる段落になります。
このPRでは、例えば以下のように「前に空行+行頭に番号」になるよう修正しています(実際のコードもこの趣旨):
Functional tests are written as follows:
1. First, one uses the `get`, `post`, `patch`, `put`, `delete`, or `head`
method to simulate an HTTP request.
2. Then, one asserts whether the current state is as expected. “State” can
be anything: the controller’s HTTP response, the database contents, etc.これにより <ol><li>…</li>…</ol> としてRDocに出力されます。
該当:
ActionController::TestCaseActionCable::Channel::TestCase
(どちらも「テストの手順」の説明が番号付きリストになる)
2. 単純な箇条書きリスト (unordered list)
同様に、- item 形式のリストが前後の改行不足で単なる段落と解釈されていた箇所を修正しています。
代表例: ActionDispatch::Http::URL::DomainExtractor
元のコメントは概ね以下のような構造でした:
The module assumes a standard domain structure where domains consist of:
- Subdomains (optional, can be multiple levels)
- Domain name
- Top-level domain (TLD, can be multiple levels like .co.uk)
For example, in “api.staging.example.co.uk”:
- Subdomains: ["api", "staging"]
- Domain: "example.co.uk" (with tld_length=2)
- TLD: "co.uk"ここに空行がなかったり改行位置が悪いと、rdoc + Markdownとしては途中で折りたたまれ、1つのパラグラフに見えてしまいます。
このPRでは例えば以下のような形になるよう、空行・インデントを調整しています:
The module assumes a standard domain structure where domains consist of:
- Subdomains (optional, can be multiple levels)
- Domain name
- Top-level domain (TLD, can be multiple levels like `.co.uk`)
For example, in “api.staging.example.co.uk”:
- Subdomains: ["api", "staging"]
- Domain: "example.co.uk" (with `tld_length = 2`)
- TLD: "co.uk"同様の対応を行っている箇所:
ActionController::Live— ストリーミング/ライブコントローラの使い方の箇条書きActionController::Redirecting—redirect_toの振る舞いなどの説明ActiveSupport::DotEnvConfiguration— envファイル読み込みやキーの扱いの説明ActiveSupport::Cache::RedisCacheStore— オプションや挙動のまとめActiveSupport::Cache::MemCacheStore— キャッシュストア設定・注意点のリスト
各ファイルとも、実コードは1行前に空行を入れる・文末のコロンのあとに空行を入れる等、Markdownが期待通りに解釈できるよう最小限のフォーマット調整が行われています。
3. ActiveSupport::Callbacks::ClassMethods#skip_callback (sample output → code block)
ここだけ少し性質が異なります。
- 問題箇所は「リスト」ではなく「サンプル出力」
- コメント内の例コードの「実行結果」に相当する部分が、Markdownとしては普通のテキスト扱いになり、読みづらい状態だった
- 同じコメント中に「2つ目の
Output:ブロック」は既にコードブロック(<pre>)として表示されていた
このPRで、最初の Output: 部分も2つ目と同様にMarkdownのコードブロックとなるようにインデント/空行を修正しています。
例えば以下のような形になります(イメージ):
Output:
```ruby
#<SomeCallback ... >
実際にはRDoc+Markdownの規則に従ったインデントで書かれており、`rdoc` でのHTML生成時に `<pre>...</pre>` として出力されることが確認されています。
---
3. 影響範囲・注意点
- 影響範囲はあくまで **ドキュメント出力(RDoc / APIドキュメントサイト)** のみであり、Rubyコードの挙動・API・型・シグネチャには一切変更はありません。
- 変更されたファイルはすべて `lib` 以下のコメント部分で、合計変更行数も少なく(+16 / -4)、バイナリ互換性・動作互換性への影響はありません。
- `rdoc` を使ってAPIドキュメントを生成している場合:
- 上記のクラス/モジュールのページで、これまで1つの長い段落に見えていた説明文が、リスト/コードブロックとしてより読みやすく表示されるようになります。
- PR説明にある通り、各変更は `rdoc` でHTMLを生成し、該当箇所に `<ol>` / `<ul>` / `<pre>` が出力されていることを確認済みです。
運用・CI観点での注意点は特にありませんが、社内やプロジェクト内でRailsのRDocコメントをMarkdownとして記述している場合、
- 「リストの前後には空行を入れる」
- 「コードやサンプル出力はインデント or フェンスで明示的にコードブロック化する」
といったスタイルガイドを採用すると、同様の問題を避けやすくなります。
---
4. 参考情報 (あれば)
- 対象PR: #58441 “Render Markdown lists that collapse into paragraphs”
- フォローアップ元: #58439
- 影響を受ける主なAPIドキュメントページ:
- `ActionController::TestCase`
- `ActionCable::Channel::TestCase`
- `ActionController::Live`
- `ActionController::Redirecting`
- `ActionDispatch::Http::URL::DomainExtractor`
- `ActiveSupport::DotEnvConfiguration`
- `ActiveSupport::Cache::RedisCacheStore`
- `ActiveSupport::Cache::MemCacheStore`
- `ActiveSupport::Callbacks::ClassMethods#skip_callback`
---
## [#58439](https://github.com/rails/rails/pull/58439) Render the `EncryptedFile#read` exception list as a list {#pr-58439}
**マージ日**: 2026/8/11 | **作成者**: [@55728](https://github.com/55728)
1. 概要 (1-2文で)
`ActiveSupport::EncryptedFile#read` がどんな例外を投げるかのドキュメント表現が、1つの段落ではなく HTML の箇条書き(リスト)として正しくレンダリングされるように修正された PR です。コードの挙動変更はなく、ドキュメント(RDoc/YARD 出力)の見た目のみが改善されています。
---
2. 変更内容の詳細
- 対象: `activesupport/lib/active_support/encrypted_file.rb`
- 変更規模: 1行追加、削除なし
`ActiveSupport::EncryptedFile#read` のドキュメントコメントで、「Raises:」以下の例外リストが、あるコミット(`17831794cc`)以降、RDoc などで 1つの `<p>` 要素の中に「- MissingKeyError ... - MissingContentError ... - ActiveSupport::MessageEncryptor::InvalidMessage ...」という形で詰め込まれて表示されるようになっていました。
この PR によって、HTML 生成時に以下のような従来の期待される構造に戻ります。
```html
<p>Raises:</p>
<ul>
<li>
<p>MissingKeyError if the key is missing and <code>raise_if_missing_key</code> is true.</p>
</li>
<li>
<p>MissingContentError if the encrypted file does not exist or otherwise if the key is missing.</p>
</li>
<li>
<p>ActiveSupport::MessageEncryptor::InvalidMessage if the content cannot be decrypted or verified.</p>
</li>
</ul>つまり、以下3つの例外が、ドキュメント上で箇条書きとして明確に読めるようになる変更です(例外の種類や発生条件自体は変更されていません):
MissingKeyError- 条件: キーが存在せず、
raise_if_missing_keyがtrueの場合
- 条件: キーが存在せず、
MissingContentError- 条件: 暗号化ファイルが存在しない、またはキーがないために内容を取得できない場合
ActiveSupport::MessageEncryptor::InvalidMessage- 条件: 内容を復号/検証できない場合
実際の Ruby コードとしては、コメントのフォーマット(おそらく :nodoc: ではなく、例外の列挙部分に箇条書きを示す RDoc 記法の追加など)が 1 行だけ修正されている形です。
- 影響範囲・注意点
- 実行時の挙動:
EncryptedFile#readの実際の実装・例外クラス・発生条件には一切変更がなく、アプリケーションコードの動作には影響しません。
- 影響範囲:
- Rails API ドキュメントや、RDoc/YARD などで生成されるドキュメントの HTML 表示のみ。
- 暗号化ファイル機能(
config/credentials.yml.enc等)を使うアプリケーションにも、バグフィックスや破壊的変更はありません。
- 注意点:
- ドキュメントを参照している人にとって、例外のリストが読みやすく整理されるだけの変更です。
- これを前提にしたテストやパーサー(HTML を自動解析するツールなど)がある場合は、HTML 構造が変わる可能性を考慮する必要がありますが、一般的な利用では問題にならないはずです。
- 参考情報 (あれば)
- 対象メソッド:
ActiveSupport::EncryptedFile#read
Rails ガイド上では、主に「Credentials」や「Encrypted configuration」に関連する箇所で登場します。 - 関連コミット:
- 現状の不正な段落表示を生んだとされるコミット:
17831794cc - 今回の PR (#58439) は、その変更で崩れたドキュメントレイアウトを元に戻すものです。
- 現状の不正な段落表示を生んだとされるコミット:
#58412 Omit optional route segments given empty strings
マージ日: 2026/8/11 | 作成者: @carldaws
- 概要 (1-2文で)
url_forなどでルーティングの「任意セグメント」に空文字列 ("") が渡された場合、そのセグメントを URL から省略するように振る舞いを統一した修正です。これにより、フォーム送信などで空文字が来ても壊れた URL が生成されず、nilと同じ扱いになります。
- 変更内容の詳細
従来の問題
ルーティング定義:
get "(/locale/:locale)/products(/:id)", to: "products#show"このとき url_for の挙動は以下のようになっていました:
url_for(controller: "products", action: "show", locale: nil, id: 123)
# => "/products/123" # OK: optional な :locale が省略される
url_for(controller: "products", action: "show", locale: "", id: 123)
# => "/locale//products/123" # NG: "/locale/" + (空) + "/products/123"locale: nilの場合は期待通りlocale部分が省略されるlocale: ""の場合は空文字がパスに埋め込まれ、/locale//products/123という不正な URL になる
(定義したルート自身がこの URL にマッチできない)
フォーム送信時など、未入力のフィールドは nil ではなく "" になりやすいため、実運用で問題になっていました。
さらに、末尾にある任意セグメント(トレーリング)の場合は「たまたま」空文字が省略されるケースがあり、
- 中間の任意セグメント:
""がパスに残る - 末尾の任意セグメント:
""が省略されることがある(実装の副作用)
という不一致な状態になっていました。
今回の修正内容
- 対象:
Journey::Formatter#generate - やっていること:
- パス生成時に「任意セグメント用のパラメータ値」が空文字
""の場合、それをnilと同様に扱い、パスからそのセグメントを取り除く。 - この除去処理は、クエリパラメータの分離後、パス用パラメータに対して行う。
- そのため、「任意セグメントに使われるはずだった値」がクエリ文字列側に回って
?locale=のようなクエリパラメータになってしまう、ということは起きない。
- そのため、「任意セグメントに使われるはずだった値」がクエリ文字列側に回って
- パス生成時に「任意セグメント用のパラメータ値」が空文字
サンプル挙動(意図される新挙動):
get "(/locale/:locale)/products(/:id)", to: "products#show"
# これまでもこうだった
url_for(controller: "products", action: "show", locale: nil, id: 123)
# => "/products/123"
# 今回の修正でこうなる("" も nil と同じ扱い)
url_for(controller: "products", action: "show", locale: "", id: 123)
# => "/products/123"変更しない挙動(テストで明示的に固定)
- 必須セグメントに対して空文字
""が渡された場合:- 従来通りの動作を維持(エラーやマッチしない URL になるなど、既存仕様に依存)
false値の扱い:- 今回の変更では影響なし。従来通りの挙動を維持
- 空のクエリパラメータ:
?locale=のようなクエリ部に現れる空文字は、そのまま保持(省略しない)
また、この PR は #55846 の差し替えであり、#55846 で問題だった以下の点を解消しています。
- 以前の案(#55846)では、複数の候補ルートを試す段階で「共有されている options ハッシュ」を直接書き換えてしまい、
ある候補ルートでの正規化結果が別の候補ルートにも漏れ出す危険があった。 - 今回はそのような副作用を起こさない形で、
Journey::Formatter#generate内部で値の正規化・除去を行うようにしている。
- 影響範囲・注意点
影響を受ける主なケース
任意セグメント(
(),/:param?相当)を含むルーティングで、url_forや*_url/*_pathヘルパーに 空文字""が渡されていたケース。特に、フォームから送信された params をそのまま渡しているようなコード:
ruby# 例: locale が form の select で未選択の場合に "" になる redirect_to products_path(locale: params[:locale], id: 123)以前は
/locale//products/123のような壊れた URL を生成し得たが、今後は/products/123になる。
後方互換性の観点
- 中間の任意セグメントに空文字をあえて残したい、というニッチな利用は理論上は影響を受けますが、 そのような URL はルート自体がマッチできないことが多く、既に半ば「バグ寄り」の挙動だったため、 互換性破壊のインパクトはごく小さいと考えられます。
""を「存在する値」として扱いたい場合は、- 任意セグメントではなく必須セグメントにする
- あるいはクエリパラメータとして扱う(例:
?locale=) など、ルーティング設計側で明示的に対応する必要があります。
テスト
actionpack/test/dispatch/routing_test.rbに 65 行分のテストが追加されており、- 任意セグメント +
""→ 省略される - 必須セグメント +
""→ 従来通り falseや空クエリパラメータの扱い といったケースが固定されています。
- 任意セグメント +
- 参考情報 (あれば)
- 対応 Issue: #55845
- 置き換えられた PR: #55846
- 変更ファイル:
actionpack/lib/action_dispatch/journey/formatter.rbactionpack/test/dispatch/routing_test.rb
この挙動に依存しそうなコードとしては、
- フォームの空値をそのまま URL に埋め込むような redirect / link ヘルパー周り
- ローカライズ用プレフィックス(例:
/(:locale)/...)を optional で書いているアプリ
が対象になり得るため、そのような箇所の URL 生成結果を一度確認しておくと安心です。
#58423 Mark the routes as loaded when the routes reloader runs standalone
マージ日: 2026/8/11 | 作成者: @55728
- 概要 (1-2文で)
Rails のルート再読み込み処理 (Rails::Application::RoutesReloader) が「ルートを実際には再描画しているのに loaded フラグが立たない」という退行バグを修正し、重複してルートが描画される問題を防ぐ PR です。execute実行後に適切に「ルートがロード済み」であることを記録しつつ、失敗時は未ロードのままにするように振る舞いを整理しています。
- 変更内容の詳細
背景となる問題
- 本来
RoutesReloader#executeは「ルートを描画して finalize するだけでなく、その結果を loaded 状態として記録する」役割を担っていました。 - しかし、過去のコミット(
be08bdce21)以降、この「loaded 状態の記録」が抜け落ちており、以下のような挙動になっていました:
reloader.execute
reloader.loaded # => false になってしまうアプリケーションの finisher がインストールする reloader hook は
reloader.executeを直接呼び出します。一方で
Engine::LazyRouteSetは、URL ヘルパが見つからなかったときに「まだルートがロードされていない」と判断すると、ルートをもう一度全部描画し、after_routes_loadedフックも再度実行します。そのため、以下のようなタイミングで「ルートが二重に描画&after_routes_loaded が二重実行」される可能性がありました:
rails consoleでのreload!- 開発環境で、アプリケーションファイル変更後に入ってきた次の HTTP リクエスト
loadedアクセサ自体は115f92b4d8で reader としては復帰していたものの、そのフラグをセットする処理がこのパス(executeが単独で呼ばれる経路)には残っていませんでした。
この PR の修正内容
1. RoutesReloader#execute がルートを「ロード済み」とマークするように修正
Rails::Application::RoutesReloader#execute 内で、ルートの描画が成功して完了したあとに「loaded フラグを true にする」処理が追加されています。
イメージとしては下記のような処理が行われるようになったと考えられます(実際のコードは若干異なる可能性がありますが、意味としてはこれに相当):
def execute
# ルートを描画する処理
draw_routes
# 正常に終わったら loaded フラグを立てる
@loaded = true
endポイント:
executeが単独で呼ばれた場合でも、「この reloader インスタンスに紐づくルートはすでにロードされている」と判定されるようになる。- これにより、後続の処理(
Engine::LazyRouteSetや他のフック)が、二回目の描画やフックの再実行を行わなくなる。
2. execute_unless_loaded との整合性
execute_unless_loadedは自前で「ロード前 → フック実行 → ロード後」の状態管理を行っており、最後に自分でloadedを true にしています。- 今回の修正では、このロジックは変えず、「
execute_unless_loadedの中からexecuteが呼ばれている場合には、状態管理はexecute_unless_loaded側で引き続き行う」ように配慮されています。 - つまり:
execute単体で呼ぶパス → この PR でloadedを true にするように変更execute_unless_loaded経由のパス → もともとのexecute_unless_loadedによる state 管理を維持(executeの変更で壊さない)
3. 失敗時の挙動
- もし
execute実行中に例外などでルートの描画が失敗した場合は、loadedフラグは変更されず「未ロード」のままにしておく、という仕様が明示されています。 - これにより:
- 後からの再実行(再トライ)が正しく行われる
- 中途半端に失敗した状態を「ロード済み」と誤認識することを防ぐ
4. テスト追加
以下の挙動を保証するテストが railties/test/application/routes_reloader_test.rb に追加されています(+26行):
execute実行後にloadedが true になること- ルートが二度描画されないこと(すなわち、
execute実行後は再描画が行われない) executeが失敗した場合はloadedが false のまま残り、再実行されること
すべての関連テスト群はパス:
railties/test/application/routes_reloader_test.rb: 5 runs, 11 assertions, 0 failuresrailties/test/application/routing_test.rb: 34 runs, 0 failuresrailties/test/application/loading_test.rb: 22 runs, 0 failuresrailties/test/application/initializers/frameworks_test.rb: 28 runs, 0 failures
- 影響範囲・注意点
影響範囲
- 主に影響を受けるのは「ルート再読み込みの挙動に依存する」部分です。
- Rails コンソールでの
reload! - 開発環境でのコード変更後のリクエスト
Engine::LazyRouteSetによる遅延ルーティングセットアップ
- Rails コンソールでの
- これまで:
executeを直接呼んだ後もreloader.loaded # => falseだったため、後続処理が「まだルートがロードされていない」と見なして再描画してしまうケースがあった
- 今後:
executeが正しくloadedを true にするため、同じリクエスト内や直後に無駄な二重描画やafter_routes_loadedの二重実行が起きにくくなります。- 結果として:
- パフォーマンス面での軽微な改善(特に開発環境)
- after_routes_loaded フックの副作用を持つコードが、二重に実行されることによる不整合の防止
注意点
ルートの再描画に副作用を持たせていた場合
たとえば、after_routes_loaded内でグローバルな状態を書き換えたり、メトリクスを積み上げたりしていた場合、これまで「たまたま二重に呼ばれていた」ことに依存していたロジックがあれば挙動が変わります。- ただし、それは本来バグ寄りの挙動への依存であり、この PR によってより一貫した挙動になります。
ルート描画失敗時の扱い
executeが途中で失敗した場合、従来以上に「再試行される」ことが明確になります。
「失敗したらそのまま落ちてほしい」ようなユースケースでは、例外処理やロギング側での対応が必要になる可能性があります(とはいえ標準的な Rails アプリではこの挙動がより自然です)。API 的な互換性
変更はRoutesReloaderの内部状態管理の修正であり、外部インターフェースのメソッドシグネチャは変わっていません。
ただし、reloader.loadedの返り値が、execute呼び出し後に従来と変わるため、テスト等でloadedフラグを直接検査していた場合は挙動差が出る可能性があります(多くの場合は「正しくなった」方向の変化)。
- 参考情報 (あれば)
- 関連コミット:
be08bdce21:executeが loaded 状態を記録しなくなった変更が含まれていたコミット115f92b4d8:loadedreader が復活したコミット(ただし setter 側の復元はされていなかった)
- 関連クラス・モジュール:
Rails::Application::RoutesReloaderRails::Engine::LazyRouteSet
- チェックリスト上も:
- バグ修正に対するテスト追加
- 振る舞い変更に応じた CHANGELOG 更新
が明示されているため、公式に「挙動としてこれが正」と位置づけられています。
#58433 Keep the string subscribers default when rebuilding a Fanout
マージ日: 2026/8/10 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Ractor 内でActiveSupport::Notifications::Fanoutを再構築する際に、文字列キー用サブスクライバのデフォルト値設定が失われてクラッシュするバグを修正した PR です。サブスクリプションスナップショットから再構築する際に、Concurrent::Mapとデフォルト値、および配列の複製を正しく行うようにしています。
- 変更内容の詳細
問題の背景
- マルチ Ractor 環境では、メイン Ractor 以外の Ractor は「サブスクリプションのスナップショット」から
Fanout(通知用のオブジェクト)を再構築します。 - 元々
Fanoutでは、文字列ベースのイベント名のサブスクライバを保持するためにConcurrent::Mapを使い、rubyのような「存在しないキーにアクセスされたら自動的に空配列を作る」デフォルトを持つマップを使っていました。@listeners_for = Concurrent::Map.new { |h, k| h[k] = [] } - しかしスナップショットを代入するときに、
Concurrent::Mapではなく単なる frozen な Hash が入ってしまい、- 未購読のイベント名に対して
all_listeners_forがnilを返す - その
nilを配列として扱って連結しようとしてクラッシュ
という問題が Ractor 内部で発生していました。
- 未購読のイベント名に対して
PR 説明にある通り:
assigning the snapshot replaced the string subscribers defaulting map with a plain frozen Hash:
all_listeners_forconcatenates the list for the instrumented name with the other subscribers, so any name with no string subscriber returned nil and crashed the first instrumentation of an unsubscribed event inside a Ractor.
修正内容
activesupport/lib/active_support/notifications/fanout.rb
- スナップショットから
Fanoutの状態を復元する処理で、- 文字列キー用のサブスクライバ構造を 再度
Concurrent::Mapとして構築し直す - かつ、その値(配列)を
dupして元スナップショットと共有しない独立の配列にする ように変更されています。
- 文字列キー用のサブスクライバ構造を 再度
概念的には以下のようなことをしています(実際のコードは多少異なりますが、イメージ用):
def rebuild_from_snapshot(snapshot)
# snapshot[:string_listeners] は frozen Hash のような形で保持されていた
@string_listeners = Concurrent::Map.new { |h, k| h[k] = [] }
snapshot[:string_listeners].each do |name, listeners|
# 配列を dup して共有を避ける
@string_listeners[name] = listeners.dup
end
# 他のデータも snapshot から復元
end- これにより
all_listeners_for(name)が、- 未登録の
nameに対しても[](空配列)を返せる - 連結処理 (
list_for_string + list_for_other) が常に配列同士になる ため、クラッシュせずに動作します。
- 未登録の
activesupport/test/notifications_test.rb
- 上記のバグを再現し、修正を保証するテストが追加されています。
- 非メイン Ractor(サブ Ractor)で
- サブスクリプションスナップショットから
Fanoutを再構築し - 「まだ誰も購読していないイベント名」を
instrumentしても - エラーにならず正常に動作すること を確認するテストです。
- サブスクリプションスナップショットから
- 非メイン Ractor(サブ Ractor)で
- 影響範囲・注意点
- 主な影響範囲:
ActiveSupport::Notificationsを Ractor と併用しているコード
(特に、メイン Ractor 以外でもActiveSupport::Notificationsによる計測やロギングを行うようなケース)
- この PR により、
- サブ Ractor 内で「未購読イベントを初めて
instrumentしたとき」に起きていたクラッシュが解消されます。 - スナップショットからの再構築時に配列を
dupしているため、- スナップショット取得元の Ractor と
- 再構築先の Ractor
のサブスクライバ配列が共有されなくなり、副作用の影響範囲が分離されます。 - これは並行実行時の予期せぬ競合・不変性違反を防ぐ意味でも妥当な変更です。
- サブ Ractor 内で「未購読イベントを初めて
- 既存コードへの互換性:
- Public API (
ActiveSupport::Notificationsのメソッド群) には変更がないため、通常の単一スレッド/単一 Ractor 環境では挙動に変化はありません。 - もし独自に
Fanoutを直接触っている(内部 API に依存している)場合は、- スナップショットの形式や、内部で
Concurrent::Mapと配列dupが使われている前提を壊さないよう注意が必要です。
- スナップショットの形式や、内部で
- Public API (
- 参考情報 (あれば)
- 元 PR: #58060
- 本 PR はそのフォローアップとして、Ractor 向けのサブスクライバスナップショット処理の不備を修正したものです。
- 関連クラス:
ActiveSupport::Notifications::FanoutConcurrent::Map(concurrent-rubyのスレッドセーフな Map 実装)
- 実際に Ractor で Notifications を使う場合のポイント:
- メイン Ractor で設定したサブスクライバをサブ Ractor に複製している仕組みの一部がこの
Fanoutのスナップショットです。 - 本修正により、サブ Ractor でもメイン Ractor と同等に安全に
instrumentを呼び出せるようになります(未購読イベントを含めて)。
- メイン Ractor で設定したサブスクライバをサブ Ractor に複製している仕組みの一部がこの
#58385 Require rack/ractorize in ractorize!
マージ日: 2026/8/10 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
Rails のractorize!実行時にrack/ractorizeを必ず読み込むようにし、Rack 側で提供される「Ractor 対応のために公開定数を freeze する処理」を Rails アプリの Ractor 実行時に確実に有効化する変更です。これにより、非メイン Ractor 内でのリクエスト処理に必要な Rack の定数 freeze が自動的に行われます。
- 変更内容の詳細
2-1. Rails::Application#ractorize! での require 追加
railties/lib/rails/application.rb に、ractorize! 呼び出し時に rack/ractorize を読み込むコードが追加されています。
イメージとしては以下のような変更です(実際のコードは多少異なる可能性がありますが、意図としてはこれに近いです):
# railties/lib/rails/application.rb
module Rails
class Application
def ractorize!
# 新たに追加された行
require "rack/ractorize"
# 既存の ractorize! ロジック(アプリケーションを Ractor 対応にするための処理)
# ...
end
end
endrack/ractorize は、Rack 本体のコードベースではデフォルトでは frozen にできない(後方互換性の問題がある)「公開定数」を、Ractor 実行時には強制的に freeze してくれる補助的なエントリポイントです。
この PR では、Rails 側の「Ractor 対応モード」スイッチ (ractorize!) をオンにした際に、その Rack の Ractor 対応コードも自動で有効にするようにしています。
2-2. テストの微修正
railties/test/application/ractors_test.rb が 1 行追加・1 行削除というごく小さな変更を受けています。
内容としては、ractorize! 呼び出しに伴い rack/ractorize が正しく読み込まれる/動作することを前提としたテストの更新、あるいは require 周りの期待値調整が行われていると考えられます(たとえば、Ractor テストでの初期化順序や、Rack の定数の状態に関するアサーション更新など)。
- 影響範囲・注意点
対象:
Rails.application.ractorize!を利用して、Ractor 上でリクエスト処理を行う構成を試している/導入しているアプリケーション。
動作上のポイント:
ractorize!実行時にrack/ractorizeが必ずrequireされます。- これにより、Rack の「公開定数」(ヘッダー名や HTTP メソッド、その他ミドルウェア/インターフェースが参照する定数など)が freeze されることになります。
- これらの定数をアプリやミドルウェア側で「書き換え・再代入・破壊的変更」している場合は、
ractorize!実行後にFrozenErrorなどの例外が発生する可能性があります。
後方互換性について:
- Rack 本体では後方互換性の理由から、通常モードではまだ定数を freeze していませんが、Ractor 内でのスレッドセーフな動作を保証するためには freeze が必須になります。
- この PR は「Rails 側が Ractor モードに入るときだけ Rack の Ractor 用パッチを当てる」という形をとっており、通常の(Ractor を使わない)Rails アプリには基本的に影響しません。
- ただし、将来的には Rack 本体側でこれらの定数が常に frozen になる可能性が高く、その将来仕様を先取りして、Ractor 利用時にはすでに freeze された状態で動くことになります。
→ Ractor を使うか否かに関わらず、「Rack の公開定数を書き換える」というパターンは避けるのが安全です。
運用上の注意:
- Ractor ベースの並列化を試す前に、アプリ・ライブラリ・ミドルウェアなどが Rack の定数を変更していないかを確認する必要があります。
- 特に古いミドルウェアや独自実装で、
Rack::METHODS << "CUSTOM"のような「破壊的変更」を行っている場合は、別の仕組みに移行する必要があります(独自定数を自前で管理するなど)。
- 参考情報 (あれば)
この PR の背景となる前提:
- Ruby の Ractor はオブジェクト共有に厳格な制約があるため、共有されるオブジェクト(特に定数でぶら下がる構造物)は immutability(freeze)が強く求められます。
- Rack は Web サーバーとアプリをつなぐ基盤レイヤーのため、その公開定数が Ractor セーフであることは、Rails に限らず他フレームワークにとっても重要です。
関連しそうなトピック/キーワード:
- Rack の Ractor 対応 (
rack/ractorize) - Rails の Ractor サポート (
Rails.application.ractorize!) - Frozen オブジェクト / Frozen constants と後方互換性
- 非メイン Ractor 上でのリクエスト処理・並列化戦略
- Rack の Ractor 対応 (
#58426 Deprecate create alias of insert in connection adapters
マージ日: 2026/8/10 | 作成者: @kamipo
- 概要 (1-2文で)
Railsのコネクションアダプタにおいて、SQLのINSERTを実行するメソッドinsertのエイリアスとして提供されていたcreateが非推奨(deprecate)になりました。今後はcreateではなくinsertを直接呼び出すことが推奨されます。
- 変更内容の詳細
目的
- ActiveRecordの低レベルAPIにおけるDMLメソッドを、SQLの用語 (
insert,update,delete) と明確に対応させるため。 createという名前は、create_table,create_databaseなどのDDLを連想させる一方で、実際にはINSERTを行うため、名前が誤解を生みやすいことが理由です。createはinsertの単なるエイリアスであり、insertを直接使えば機能的に全く困らないため、エイリアスを廃止する方向に切り替えています。
実際の変更点
コード差分は小さいですが、主に以下の3点です。
非推奨の告知を追加
activerecord/CHANGELOG.mdに「insertのエイリアスであるcreateが非推奨になった」旨が追記されました。- 将来のメジャーバージョンで削除される可能性が高いことを示唆します。
データベースステートメントAPIへのdeprecationマーク
activerecord/lib/active_record/connection_adapters/abstract/database_statements.rbに、createメソッドの非推奨を示すコメントまたはdeprecate呼び出しが追加されています。実際の既存コードイメージとしては、概ね以下のような形になっていると考えられます(簡略化例):
rubymodule ActiveRecord module ConnectionAdapters module DatabaseStatements # 旧: def create(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) # insert(sql, name, pk, id_value, sequence_name, binds) # end # 今後は create は非推奨で、内部的には insert を呼ぶだけ def create(*args) ActiveSupport::Deprecation.warn(<<~MSG) `create` is deprecated and will be removed in a future version. \ Use `insert` instead. MSG insert(*args) end end end end※正確なメッセージ文言や実装はPR本体に依存しますが、趣旨は上記の通りです。
テストコードの修正
activerecord/test/cases/adapter_test.rbactiverecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rbactiverecord/test/cases/database_statements_test.rbなどで、create呼び出しをinsertに変更し、適宜テスト期待値を調整しています。- もともと
createを使っていたテストは、今後推奨されるAPIであるinsertに置き換えられました。
- 影響範囲・注意点
影響を受ける可能性があるコード
- ActiveRecordの低レベルAPIとして、アダプタ経由で直接SQLを発行しているコードのうち、
connection.create(...)を使っている箇所
- 典型例:
- 自前でSQL INSERTを流しているレイヤー
- サードパーティgemの中で、アダプタに対して
createを呼んでいるもの
通常のActiveRecordモデルの Model.create(...) / Model.create! とは別物であり、こちらは今回の変更の対象ではありません。影響を受けるのは、ActiveRecord::Base.connection 系の直接呼び出しだけです。
実務上の対応
コード検索
プロジェクト全体で
".create("ではなく、アダプタのcreateを使っている箇所を検索します。例えば:rubyActiveRecord::Base.connection.create( ... ) some_connection.create( ... )
insertへの置き換え以下のようなコードがあれば:
rubyconnection.create("INSERT INTO users (name) VALUES ('alice')")これを以下のように変更します:
rubyconnection.insert("INSERT INTO users (name) VALUES ('alice')")引数シグネチャも
insertと同じだったため、基本的にはメソッド名の置き換えのみで対応可能です。
警告の監視
- Railsのログやテスト実行時に、
create is deprecated; use insert insteadなどのdeprecation warningが出ていないか確認し、見つかった箇所を順次修正していくと安全です。
- Railsのログやテスト実行時に、
将来的な削除リスク
- このPRは「非推奨化」であり、即時削除ではありませんが、次のメジャーバージョン(例: Rails 8, 9 等)で
createメソッド自体が削除される可能性が高いです。 - レガシーコードや外部gemが
createに依存していると、そのタイミングでNoMethodErrorになるため、今のうちにinsertへ移行しておくのが無難です。
- 参考情報 (あれば)
関連するAPIドキュメント(現行バージョンの例):
ActiveRecord::ConnectionAdapters::DatabaseStatements#insert
https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-insert
用語整理:
- DML (Data Manipulation Language):
INSERT,UPDATE,DELETEなど、テーブル内のデータ操作 - DDL (Data Definition Language):
CREATE TABLE,ALTER TABLE,DROP TABLEなど、テーブルやスキーマの定義変更
このPRは、「DMLメソッドをDML由来の名前に揃え、DDLを連想させるcreateという紛らわしい名前を撤廃する」という文脈です。
- DML (Data Manipulation Language):
#58428 Fix LengthValidator crash with proc minimum and nil value
マージ日: 2026/8/10 | 作成者: @ousamabenyounes
- 概要 (1-2文で)
ActiveModel::Validations::LengthValidatorにおいて、:minimumや:isに Proc を指定し、かつ検証対象の値がnilのときにNoMethodErrorで落ちるバグを修正した PRです。Proc を使った長さバリデーションでも、常に数値が I18n に渡るようにし、例外なく動作するようになりました。
- 変更内容の詳細
問題の挙動
次のように length バリデーションで :minimum に Proc を渡し、対象属性が nil のときに例外が発生していました。
class Foo
include ActiveModel::Validations
validates :bar, length: { minimum: ->(record) { record.one + 2 } }
def one = 1
def bar = nil
end
Foo.new.errors.messages
# => NoMethodError: undefined method 'one' for an instance of Hash原因は以下のロジックにあります(元コードのイメージ):
CHECKS.each do |key, validity_check|
next unless check_value = options[key]
if !value.nil? || skip_nil_check?(key)
check_value = resolve_value(record, check_value)
next if value_length.public_send(validity_check, check_value)
end
errors_options[:count] = check_value
endvalueがnilで、keyが:minimum/:isの場合skip_nil_check?(key)はfalse(trueになるのは:maximumのみ)- そのため
if !value.nil? || skip_nil_check?(key)の中に入らない - 結果として
resolve_valueが呼ばれず、check_valueには「Proc のまま」が残る - その
check_valueがerrors_options[:count]にそのまま入る - I18n メッセージの補間時に
countが Proc のまま渡り、ハッシュを引数に Proc が呼び出されてNoMethodErrorになる(recordではなくオプションハッシュが渡される)
修正内容
上記の問題を回避するために、check_value の解決タイミングを変更しています。
CHECKS.each do |key, validity_check|
next unless check_value = options[key]
+ check_value = resolve_value(record, check_value)
+
if !value.nil? || skip_nil_check?(key)
- check_value = resolve_value(record, check_value)
next if value_length.public_send(validity_check, check_value)
end
errors_options[:count] = check_value
endポイント:
next unless check_value = options[key]の直後にresolve_value(record, check_value)を呼ぶように変更- これにより:
- Proc が渡されていた場合は、
recordを引数に 必ず一度だけ 実行され、整数などの具体値に変換される - すでに整数等の具体値であれば
resolve_valueは冪等 (idempotent) なので挙動は変わらない
- Proc が渡されていた場合は、
valueがnilで:minimum/:isの場合でも、errors_options[:count]には常に「数値などの完成した値」が入るため、I18n 側に Proc が漏れ出さない
テスト追加
次の 2 パターンの回帰テストが追加されています(nil 値かつ Proc 指定で落ちていたケース):
:minimumに Proc を渡した場合:isに Proc を渡した場合
どちらも、修正前は NoMethodError となり、修正後は Active Model のテストスイート全体でエラーなくパスしています。
- 影響範囲・注意点
- 影響を受けるのは、
validates :attr, length: { minimum: ->(record) { ... } }やis: ->(record){ ... }を使っていて、かつ値がnilになり得るケース です。- これまで: その状況で
errorsを参照するとNoMethodErrorで落ちる - 今後: 例外なく、通常どおりバリデーションエラーが構築される
- これまで: その状況で
:maximumについては元々skip_nil_check?の対象であり、valueがnilでもチェックロジックに入る設計だったため、今回の修正で挙動は変わりません。- ただし実装上は
maximumに対しても「早い段階でresolve_valueする」ようになっており、Proc があれば必ず Record を引数に解決されます(以前と同じく 1 回だけ)。
- ただし実装上は
:minimum/:maximum/:isに 整数リテラルや Symbol を渡しているコードの挙動は基本的に変化しません。- 整数:
resolve_valueはそのまま返すだけ - Symbol: 既存の処理フローで正しくメソッド呼び出しなどに解決されており、今回の変更では壊さないように配慮されています
- 整数:
- パフォーマンス面では、
CHECKSループ内でresolve_valueする位置が変わっただけで、呼び出し回数は「チェックごとに 1 回」のままです。
運用上の注意:
- もし既存コードで「
:minimum/:isに渡した Proc が、nilの場合は絶対に呼ばれないという前提」に依存した実装をしていた場合、その前提は崩れます。- 例:
valueがnilのときには Proc 内で副作用を起こしたくない、など - もっとも、そんな前提で書かれることはほぼ想定されておらず、一般的には安全な変更と考えられます。
- 例:
- 参考情報 (あれば)
- 対応する Issue: #40642 —
LengthValidatorが Proc 指定の:minimum/:isとnil値でNoMethodErrorになる問題 - 変更ファイル:
activemodel/lib/active_model/validations/length.rbactivemodel/test/cases/validations/length_validation_test.rbactivemodel/CHANGELOG.md(今回のバグ修正が追記)
この PR により、Proc ベースの動的な長さバリデーションが nil 値を含むケースでも安全かつ一貫して利用できるようになっています。
#58328 Allow ActiveSupport::ProxyLogger to ignore messages by pattern
マージ日: 2026/8/10 | 作成者: @federico-carrocera
- 概要 (1-2文で)
ActiveSupport::ProxyLogger に「メッセージ内容でログを無視する」機能が追加され、正規表現や文字列パターンで特定のログだけをサイレンスできるようになりました。これにより、ログレベルを上げずに特定のノイズログのみを抑制できます。
- 変更内容の詳細
新機能: ignore パターンによるフィルタリング
これまで ProxyLogger は「severity(ログレベル)」でしかフィルタできず、ある gem のうるさいメッセージを消したい場合、ログレベルを上げて他の低レベルログもまとめて捨てる必要がありました。
この PR により、メッセージ内容に応じて無視するパターンを指定できます。
SomeLibrary.logger =
ActiveSupport::ProxyLogger.new(Rails.logger)
.ignore(/Noisy/)ignoreに渡せるものRegexp… メッセージに対して正規表現マッチString… メッセージに対してリテラル一致(文字列そのものを含むか、等しいかなど、実装的にはRegexp.unionでまとめられるので「その文字列を含むか」を見る形)
PR 説明から:
- 「正規表現はメッセージに対してマッチ」
- 「文字列はリテラルとしてマッチ」
- 複数パターンは
Regexp.unionで 1つの正規表現にまとめてコンパイルされる(登録時に実行)
ProxyLogger 側の挙動は以下の流れになります:
- まず従来通り、ログレベル(severity)のチェックを行う
- レベル条件を満たした場合にのみ、ignore パターンによるマッチ判定を行う
- パターンにマッチしたメッセージは、そのまま破棄され、下流の logger には渡されない
add メソッドのシグネチャ変更
Logger#add の呼び出し方法も変更されています。
- 以前:
def add(...)のように可変長引数 (...) を受けて、元の logger に丸投げ - 変更後:
Logger#addと同じ明示的なシグネチャを取るように変更
Ruby 標準 Logger の add シグネチャに合わせる形になっています:
def add(severity, message = nil, progname = nil, &block)
# ...
end理由:
- メッセージ内容を ignore パターンにマッチさせる必要があるため
messageとblockのどちらでメッセージが供給されるかを解決(評価)してからマッチングする必要がある
→ 「メッセージを解決してからでないとパターンマッチできない」ので forward ではなく明示的に受ける形に変更
テスト (activesupport/test/proxy_logger_test.rb) では、以下のようなケースがカバーされていると考えられます(PRの説明から推測される代表的パターン):
- severity は通過するが message が ignore パターンにマッチ → ログが出力されない
- 複数の ignore パターンを設定した場合に、union された正規表現でマッチする
- ignore パターンを設定していない ProxyLogger は従来通りの挙動(パフォーマンス劣化なし)
- 影響範囲・注意点
ログフィルタリングがより細かく可能
- gem や外部ライブラリの「一部のうるさいログだけ」抑制できる
- 例: 永続的に出る deprecation warning / noisy poll ログなど
既存コードへの互換性
ActiveSupport::ProxyLoggerを通常通り使っているだけであれば、ignore パターン機能を使わない限り挙動は変わりません。- パターンマッチは severity チェック後に行われるため、severity で弾かれるログについて追加のオーバーヘッドは発生しません。
- ProxyLogger の
addを独自に呼び出したり、継承してシグネチャに依存した実装をしている場合は、Logger#addと同じシグネチャに合わせる必要があります。
パフォーマンス
- パターンは登録時に
Regexp.unionで 1つの正規表現にコンパイルされるため、複数パターン指定時でも実行時のマッチングコストは最小限。 - ignore パターンを使わない ProxyLogger インスタンスは従来通りで、追加のマッチ処理は行われません。
- パターンは登録時に
メッセージの解決タイミング
- ブロック (
&block) でメッセージを渡した場合も、パターンマッチのために評価されます。 - ignore で弾かれた場合は最終的にはログ出力されませんが、「ブロックの評価コスト」自体はかかる点に注意が必要なケースもあります(高コストなログ組み立てを block で遅延している設計など)。
- ブロック (
- 参考情報 (あれば)
変更ファイル:
activesupport/lib/active_support/proxy_logger.rb- ignore パターンの追加ロジック、および
addシグネチャの更新
- ignore パターンの追加ロジック、および
activesupport/test/proxy_logger_test.rb- ignore 機能と後方互換性に関するテスト
activesupport/CHANGELOG.md- 新機能としてのエントリが追加
利用イメージ(まとめ):
# 例: 特定の noisy メッセージだけを抑制したい
noisy_logger = ActiveSupport::ProxyLogger.new(Rails.logger)
.ignore(/deprecated .*foo/)
.ignore("Polling every 1s")
SomeLibrary.logger = noisy_loggerこのように、ログレベルを変更せずに、必要なメッセージは残しつつ特定のノイズだけ除外できるようになります。
#58335 Add test coverage for ActiveModel::Type::Binary
マージ日: 2026/8/10 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveModel::Type::Binaryに対して不足していたテストが追加され、type/binary?/changed_in_place?/Data#hexなどを含めてファイル全体が行カバレッジ100%になりました。挙動変更はなく、純粋にテスト補強のみのPRです。
- 変更内容の詳細
このPRで追加されたのは activemodel/test/cases/type/binary_test.rb のテストコード 38行のみです。目的は ActiveModel::Type::Binary および内部クラス Binary::Data の未到達分岐をすべてカバーすることです。
カバーされるようになったポイント
typeとbinary?のテスト追加テスト:
test_type_and_binary_predicatetypeメソッドが:binaryを返すことbinary?がtrueを返すこと
を明示的に検証しています。これにより、Binary 型がフレームワーク内部の「バイナリ型」として正しく識別されることが保証されます。
イメージ的なコード例:
rubytype = ActiveModel::Type::Binary.new assert_equal :binary, type.type assert type.binary?serializeの戻り値の形のテスト追加テスト:
test_serialize_returns_binary_data検証しているポイント:
nilをシリアライズするとnilのまま返るnil以外の値はActiveModel::Type::Binary::Dataでラップされて返る
想定される振る舞いのイメージ:
rubytype = ActiveModel::Type::Binary.new assert_nil type.serialize(nil) data = type.serialize("abc") assert_instance_of ActiveModel::Type::Binary::Data, datacastのData分岐のテスト追加テスト:
test_cast_binary_data_returns_the_underlying_stringこれまで未到達だった分岐:
rubydef cast(value) if value.is_a?(Data) value.to_s # <- ここ else ... end endをテストするために、
- 一度
serializeしてBinary::Dataオブジェクトにする - それをもう一度
castに渡す
というラウンドトリップで
Dataブランチを通しています。イメージ:
rubytype = ActiveModel::Type::Binary.new serialized = type.serialize("abc") # => Binary::Data casted = type.cast(serialized) # => "abc" (バイナリ文字列) assert_equal "abc".b, casted # 例: エンコーディングも含めて確認これにより「
Binary::Dataをcastすると、中身の生のバイナリ文字列が返る」という仕様がテストで固定されます。- 一度
changed_in_place?のテスト追加テスト:
test_changed_in_placechanged_in_place?は、値がインプレースで変更されたかどうかを判定するためのフックで、ActiveModel / ActiveRecord の dirty tracking で使われます。テスト内容:
- 「デシリアライズ済みの古い値」と「新しい値」を比較し、
- 中身が異なれば
true - 同一なら
false
- 中身が異なれば
- となることを確認
例のイメージ:
rubytype = ActiveModel::Type::Binary.new old = type.deserialize(type.serialize("foo")) # -> "foo" (バイナリ) new_diff = "bar".b new_same = "foo".b assert type.changed_in_place?(old, new_diff) refute type.changed_in_place?(old, new_same)これにより Binary 属性が変更されたかどうかの判定ロジックがテストで保証されます。
- 「デシリアライズ済みの古い値」と「新しい値」を比較し、
Binary::Data#hexと文字列変換のテスト追加テスト:
test_data_hex_and_string_conversionテストしているのは以下:
- 非UTF-8バイト列に対する
Data#hexが期待どおりの16進文字列を返すこと to_s/to_strが元のバイト列を正しく返すこと- 生の文字列と
Binary::Dataの比較が意図通り動作すること
例えば:
rubyvalue = "\xFF\x00\x01".b data = ActiveModel::Type::Binary::Data.new(value) assert_equal "ff0001", data.hex assert_equal value, data.to_s assert_equal value, data # == がオーバーロードされている場合の比較などこれにより、
Binary::Dataをログ出力やデバッグ時に16進表現で扱うケース、または「生文字列との比較・互換性」がテストで支えられます。- 非UTF-8バイト列に対する
- 影響範囲・注意点
影響範囲
- 実装コードには一切変更がなく、
activemodel/test/cases/type/binary_test.rbの追加のみです。 - そのため、既存アプリケーションの挙動に影響はありません。
active_model/type/binary.rbの行カバレッジが 25/31 → 31/31 に向上し、Binary 型まわりの挙動がテストでより強固に保証されるようになります。
- 実装コードには一切変更がなく、
注意点
- 挙動を変更していないため、Rails のアップデートによって Binary 型の仕様が変わることはありません。
- 逆に言えば、今回テストで固定された挙動 (
Data#hexの形式、changed_in_place?の比較ロジックなど) を将来的に変えたい場合は、これらのテストを更新する必要があります。 - CI 上では
activemodel/test/cases/全体で1199 runs, 5375 assertions, 0 failures, 0 errorsとなっており、RuboCop も問題なしとのことなので、品質面のリスクはほぼありません。
- 参考情報 (あれば)
- 対象クラス:
ActiveModel::Type::Binary,ActiveModel::Type::Binary::Data - 主な関心メソッド:
#type/#binary?#serialize#cast#changed_in_place?Binary::Data#hex,Binary::Data#to_s,Binary::Data#to_str
- PR番号: https://github.com/rails/rails/pull/58335
#58414 [8-1-stable] Warn when image_processing 2.x is missing ruby-vips or mini_magick
マージ日: 2026/8/10 | 作成者: @lazerg
- 概要 (1-2文で)
Rails 8.1 系でimage_processing2.x を使う際にruby-vipsやmini_magickが未インストールだと、アプリ起動時に例外で落ちてしまう問題を「警告ログを出して処理を続行する」本来の挙動に戻すバックポート対応です。mainブランチに既に入っていた例外メッセージのマッチ条件を8-1-stableに反映しています。
- 変更内容の詳細
問題の発生条件
- Rails: 8.1.3.1(
8-1-stable系) - Gem 構成:
gem "image_processing", "~> 2.0"ruby-vipsなし、またはmini_magickなし
この状態でアプリを起動すると、initializer 実行中 (run_initializers) に以下のような LoadError でアプリが落ちるケースがあります。
LoadError: ImageProcessing::Vips requires the ruby-vips gem. Please add `gem "ruby-vips", "~> 2.0"` to your Gemfile. (LoadError)
image_processing-2.0.2/lib/image_processing/vips.rb:5
activestorage-8.1.3.1/lib/active_storage/transformers/vips.rb:10
activestorage-8.1.3.1/lib/active_storage/engine.rb:101Rails 側ではもともと Active Storage エンジン内でこの LoadError を rescue し、**「警告ログを出して起動は継続する」**設計でしたが、次のような齟齬が起きていました。
- Active Storage エンジンの
rescueは、例外メッセージのcaseマッチで以下のようなパターンしか見ていなかった:/libvips//image_processing/
- 一方、
image_processing2.x ではruby-vips/mini_magickをソフト依存にし、不足時には以下のように 不足している gem 名を明示したメッセージ を出すようになった:textImageProcessing::Vips requires the ruby-vips gem. ... ImageProcessing::MiniMagick requires the mini_magick gem. ... - このため、
LoadErrorのメッセージにはruby-vips/mini_magickが含まれるが、Active Storage 側はそれをパターンに含めておらずマッチに失敗 →LoadErrorが再スローされ、起動失敗となっていた。
さらに、8.1.3.1 で transformers/vips.rb に require "image_processing/vips" を eager に追加したことで、この問題が起動時に必ず表面化するようになりました。
この PR の対応内容
main ブランチの #57403 (commit: 244549b36b) で既に追加されていた以下のパターンマッチを、8-1-stable にバックポートしています。
- 例外メッセージに
/ruby-vips/を含む場合の分岐 - 例外メッセージに
/mini_magick/を含む場合の分岐
これにより、image_processing 2.x が出す以下のメッセージに対しても、Active Storage エンジン側が正しく反応できるようになります。
ImageProcessing::Vips requires the ruby-vips gem. Please add `gem "ruby-vips", "~> 2.0"` to your Gemfile.
ImageProcessing::MiniMagick requires the mini_magick gem. Please add `gem "mini_magick", "~> 5.0"` to your Gemfile.おおまかな処理イメージ(擬似コード)
# activestorage/lib/active_storage/engine.rb 内のイメージ
begin
# vips / mini_magick のトランスフォーマをロード
require "image_processing/vips"
# ...
rescue LoadError => error
case error.message
when /libvips/, /image_processing/, /ruby-vips/, /mini_magick/
# 対応するバックエンド用の gem がないだけなので、致命的エラーにはせず警告を出す
ActiveSupport::Deprecation.warn(<<~MSG)
... (不足している gem のインストールを促すメッセージ) ...
MSG
else
# それ以外の LoadError は従来通りエラーとして再スロー
raise
end
endPR では上記のうち /ruby-vips/ と /mini_magick/ を見る分岐が追加されています(実コードはもう少し細かいですが、意図としてはこのイメージです)。
変更ファイル
activestorage/CHANGELOG.md- 上記挙動の修正が 8.1 系の CHANGELOG に追記されています。
- ただし
mainにある「Gemfile と app generator の変更」については、8.1 既存アプリには不要なため触れていません。
activestorage/lib/active_storage/engine.rbLoadErrorを rescue している箇所に/ruby-vips/と/mini_magick/のマッチ処理が追加。
- 影響範囲・注意点
影響範囲
- 対象:
- Rails 8.1 系 (
8-1-stable) で Active Storage を利用しているアプリ image_processing2.x を利用しており、ruby-vipsまたはmini_magickを入れていない環境
- Rails 8.1 系 (
- 挙動の変化:
- これまではアプリ起動時に
LoadErrorで落ちていたケースが、- 起動は成功する
- ログに「対応する gem が不足している」旨の警告が出る という本来意図されていた挙動に戻ります。
- これまではアプリ起動時に
起こりうる実務上の違い
- この PR 導入後も:
ruby-vips/mini_magickがない限り、そのバックエンドを使った画像変換は当然できません。- ただし「そのバックエンドが使えない」ことは警告レベルに留まり、アプリ全体のブートが失敗することはなくなる、という整理です。
- 本番環境などで「今まで起動時に落ちて気づけていたものが、警告ログだけになってしまう」ことに注意してください。
- ただしもともとの設計意図も「ソフト依存なので警告にとどめる」ため、この振る舞いが正です。
テストについて
image_processing2.x + 依存 gem 不足 という状態を CI の Gemfile で再現しづらく(現行 Gemfile はimage_processing1.x +ruby-vips同梱)、自動テストは追加されていません。- 手動での再現手順:
- Rails 8.1.3.1 のアプリを用意
- Gemfile に
gem "image_processing", "~> 2.0"を追加 ruby-vips(またはmini_magick)を Gemfile から外す- アプリを起動
- 修正前: 起動時に
LoadErrorで落ちる - 修正後: 起動し、ログに警告が出る
- 参考情報 (あれば)
- 該当 PR:
- 修正対象 Issue:
- 元となった
mainブランチ側の PR: image_processing2.x の依存関係変更(ソフト依存化):ruby-vipsおよびmini_magickは必須ではなくなり、利用時にだけ必要 → 不足時はLoadErrorを投げて不足 gem 名をメッセージで案内する仕様。
#58427 Use bind parameters for array-form arguments in find_by_sql / count_by_sql
マージ日: 2026/8/9 | 作成者: @kamipo
- 概要 (1-2文で)
find_by_sql/count_by_sqlに配列形式の引数を渡したとき、これまで文字列に埋め込み(sanitize_sql)されていた値を、whereと同様にバインドパラメータとして扱うように変更した PR です。これによりドキュメントどおりwhereと同等の API 挙動になり、SQL インジェクション耐性とプレースホルダ処理が統一されました。
- 変更内容の詳細
2-1. これまでの問題点
find_by_sql / count_by_sql は次のような「配列形式」の呼び方ができるとドキュメントに記載されていました:
Post.find_by_sql(["SELECT * FROM posts WHERE id = ?", 1])
Post.count_by_sql(["SELECT COUNT(*) FROM posts WHERE id = ?", 1])ドキュメント上は where と同じように「? にバインドされる」ことが約束されていましたが、実装では以下のような挙動になっていました。
["... ? ...", value1, value2, ...]という配列をsanitize_sqlに渡し、
→ その時点で Ruby 側で SQL 文字列に埋め込んでしまう(eager interpolation)- その結果、バインドパラメータではなく“既に展開された SQL 文字列”として扱われる
where での配列形式:
Post.where(["id = ?", 1])は内部的に Arel のバインドパラメータ($1 / ? など DB アダプタ依存)として処理されますが、find_by_sql / count_by_sql では同じ書き方でも実際は文字列連結に近い扱いになっており、API ドキュメントの「where と同等」という説明と挙動が食い違っていました。
2-2. 今回の対応内容
この PR では、配列形式の find_by_sql / count_by_sql 引数を、where と同じルートでバインドパラメータとして扱うように変更しています。
主な変更点は以下のとおりです。
2-2-1. Sanitization#bound_sql_literal_for の追加
activerecord/lib/active_record/sanitization.rb に以下のようなメソッドが追加されています(名前からの要約):
Sanitization#bound_sql_literal_for(...)build_where_clauseなどと共通で使用する「プレースホルダのディスパッチ処理」をここに集約- 値を「即座に文字列として埋め込む」のではなく、
Arel::Nodes::BoundSqlLiteralを介して「バインド可能な SQL リテラル」として扱う
これにより、where で使っている「値を Arel のノードとして束縛するロジック」と、find_by_sql / count_by_sql での配列形式処理が共通化されました。
2-2-2. find_by_sql / count_by_sql の配列引数ルートの変更
activerecord/lib/active_record/querying.rb などで、配列形式の引数の扱いが次のように変更されています(擬似コード的なイメージ):
変更前 (イメージ)
def find_by_sql(sql, binds = [])
# sql が配列形式 ["SELECT ... WHERE id = ?", 1] の場合
sql = sanitize_sql(sql) # ここで文字列に埋め込んでしまう
# ...
connection.select_all(sql, ...)
end変更後 (イメージ)
def find_by_sql(sql, binds = [])
if sql.is_a?(Array)
# 1. 配列を BoundSqlLiteral 付きの Arel ノードとして扱う
bound_sql = bound_sql_literal_for(sql)
# 2. バインド値と SQL を connection に渡す形式に変換
sql, binds = connection.to_sql_and_binds(bound_sql)
end
connection.select_all(sql, "SQL", binds)
end実際にはアダプタや Arel 層を挟みますが、要するに:
["SELECT ... WHERE id = ?", 1]という配列をArel::Nodes::BoundSqlLiteralに包んだ形で扱い- DB アダプタレベルで正しいバインドクエリ (
SELECT ... WHERE id = $1) + バインド配列 ([1]) へ変換する
という流れに変えています。
2-2-3. build_where_clause とのプレースホルダ処理の統合
activerecord/lib/active_record/relation/query_methods.rb では、build_where_clause 周辺にあった「プレースホルダの分岐ロジック」が大幅に削られています (-36 / +1 行ほど)。
これは、もともと build_where_clause 側に散在していた「値をどうバインドするか」の判断を Sanitization#bound_sql_literal_for へ移し、find_by_sql / count_by_sql からもそれを利用できるようにしたためです。
結果として:
whereでの配列利用find_by_sql/count_by_sqlでの配列利用
が、同じ bound_sql_literal_for を経由するようになり、実装・挙動の一貫性が増しています。
2-2-4. テストの追加
activerecord/test/cases/finder_test.rb にテストが 37 行追加されています。
内容としては(推測レベルを含みますが):
find_by_sql(["SELECT ... WHERE id = ?", 1])/count_by_sql(["SELECT ... WHERE id = ?", 1])が- 正しい結果を返すこと
- 実際にバインドクエリとして扱われていること(ログ・バインド数など)
whereとfind_by_sql/count_by_sqlの挙動が一致すること
などを検証していると考えられます。
- 影響範囲・注意点
3-1. 既存アプリへの影響
対象: find_by_sql / count_by_sql を配列形式で使っているコード
# 影響を受けるパターン
Post.find_by_sql(["SELECT * FROM posts WHERE title = ?", user_input])
Post.count_by_sql(["SELECT COUNT(*) FROM posts WHERE category = ?", params[:category]])これらは、これまでは Ruby 側で文字列に埋め込まれていたが、今後は DB のプレースホルダにバインドされる ようになります。
多くのケースでは挙動は論理的に同じで、より安全になるだけですが、次のようなケースでは影響が出る可能性があります。
3-1-1. DB アダプタ固有のプレースホルダ挙動に依存していた場合
- 以前:
"id = 1"という 完全な SQL 文字列が生成されていた
- 今後:
"id = $1"+[1]のように プレースホルダ + バインド となる
DB レベルでの実行計画キャッシュやログのフォーマット、DB 側で SQL を文字列としてパースしているような特殊ツール等で違いが出る可能性はあります(ただし普通のアプリでは問題にならないことがほとんどです)。
3-1-2. 値側に「SQL として意味のある断片」を意図的に渡していた場合
たとえば以下のような「擬似的な SQL 組み立て」をしていた場合:
# ※アンチパターン寄りの例
condition = "published_at < NOW() - INTERVAL '1 day'"
Post.find_by_sql(["SELECT * FROM posts WHERE #{condition} AND id = ?", 1])ここで condition 側は依然として自前の文字列連結ですが、["... #{condition} AND id = ?", 1] の「1」の部分だけがバインドに変わります。
多くの場合問題にはなりませんが、「配列の 2 要素目以降に SQL 断片("id = 1" など)を渡し、あえて文字列展開させる」というトリッキーな使い方をしていた場合は挙動が変わります。
例:
# 以前は: "id = 1" がそのまま埋め込まれていた
Post.find_by_sql(["SELECT * FROM posts WHERE ?", "id = 1"])- 旧挙動(sanitize_sql):
"SELECT * FROM posts WHERE id = 1"に変換されて実行
- 新挙動(バインド):
"SELECT * FROM posts WHERE $1"+["id = 1"]として実行され、
→ 構文エラー となる可能性が高い($1は値であって SQL 式ではないため)
このような使い方は本来ドキュメントにも沿っておらずアンチパターンですが、もし行っていた場合は修正が必要です。
3-2. セキュリティ観点
- 配列形式引数を使う場合、値が文字列連結ではなくバインドされるため、SQL インジェクションリスクが減少・もしくは顕在化しにくくなる 方向の変更です。
- 既に
whereで慣れている安全な書き方をfind_by_sql/count_by_sqlでも素直に使えるようになった、と捉えてよいです。
3-3. パフォーマンスへの影響
- バインドパラメータを使う方が、DB アダプタによってはクエリキャッシュやプランキャッシュが効きやすくなる可能性があります。
- 一方で「SQL を毎回文字列として作る」よりも、バインド付きクエリを経由する実装になったため、内部的に若干のオーバーヘッドは増えるかもしれませんが、通常の Rails アプリでは誤差レベルと考えられます。
- 参考情報 (あれば)
関連する Rails API ドキュメント(参考):
ActiveRecord::Base.find_by_sql
https://api.rubyonrails.org/classes/ActiveRecord/Base.html#method-c-find_by_sqlActiveRecord::Base.count_by_sql
https://api.rubyonrails.org/classes/ActiveRecord/Base.html#method-c-count_by_sqlActiveRecord::QueryMethods#where
https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-where
この変更後は、「where で使える配列形式 (["col = ?", val]) は find_by_sql / count_by_sql でも同じように“バインドされる”」と考えて差し支えなくなります。
#58425 Stop deserializing column defaults
マージ日: 2026/8/9 | 作成者: @byroot
- 概要 (1-2文で)
Rails 8.1.0で導入された「カラムのデフォルト値をあらかじめ型変換(deserialize)する」挙動をやめて、再び「デフォルト値は生の値のまま扱う」方向に戻したPRです。
これにより、後から属性タイプが上書きされるケースでデフォルト値の型不整合が起きる問題(#58292)を解消しています。
- 変更内容の詳細
背景
- Rails 8.1.0 で、以下の Issue への対応として「カラム定義時にデフォルト値を type_cast(deserialize)しておく」変更が入った:
- しかしこの変更によって、
- モデルの attribute type が後から
attribute,attribute :foo, :integerのように上書きされる場合や - PostgreSQL, SQLite3 などで DB の型とアプリ側の型定義が変わる場合 に、「すでに古い型で変換済みのデフォルト値」が新しい型に変換できずエラーや不整合が発生するようになりました(例: boolean → integer への変更など)。
- モデルの attribute type が後から
このPRは、その「カラム定義時にデフォルト値を deserialize する」振る舞いを止め、従来どおり DB から取得した生のデフォルト値を保持し、必要なタイミングで現在の型に応じて変換する動作に戻しています。
実際の変更点の方向性
コード差分は少ないですが、意味合いとしては次のような変更が行われています。
1. ActiveRecord::ConnectionAdapters::Column のデフォルト値の扱いを修正
- 以前の 8.1.0 変更で、
Columnが初期化されるタイミングで「型に応じてdefaultを type_cast する」処理が入っていました。 - このPRで、その事前 type_cast をやめ、カラム定義としては「DB から読み出したままの値」を保持する形に戻しています。
概念的には:
# 8.1.0 問題のある状態(イメージ)
def initialize(name, default, sql_type, ...)
@sql_type = sql_type
@default = cast_type.deserialize(default) # ここで早期に deserialize していた
end
# このPR後の状態(イメージ)
def initialize(name, default, sql_type, ...)
@sql_type = sql_type
@default = default # 生のまま保持
end実際の実装はもう少し細かいですが、ニュアンスとしては「Column オブジェクトでは serialize/deserialize しない」に戻しています。
2. SQLite3 アダプタでのデフォルト値取得の調整
activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rbで、SQLite3 のメタデータからデフォルト値を読み出す部分が微調整されています。- ここでも「Rails 側でデフォルトを過度に解釈・変換しない」ように寄せています。
- 例えば
DEFAULT 0やDEFAULT '0'などを DB がどう返すかに応じて、そのまま Column に渡し、実際の値の型処理は Attribute API 側に任せる形です。
3. テストの更新・追加
いくつかのテストファイルが更新されています:
attributes_test.rbに 16 行追加
→ 属性タイプの上書きとデフォルト値の挙動に関する回 regress テストが追加されています。
典型的には次のような状況をカバーしていると考えられます:rubyclass User < ActiveRecord::Base # DB 上は boolean カラムで default false # 後からアプリ側で整数として扱いたいケース attribute :flag, :integer end # new の際に「boolean として deserialized 済みの false」を # 再度 integer にキャストしようとして失敗、のような問題が起きないことを確認defaults_test.rbなどの「デフォルト値」のテスト群で、
Rails が DB のデフォルトをどう扱うかについて期待値を修正。PostgreSQL
money_test.rbや Migration 関連テスト (change_schema_test.rb,columns_test.rb,migration_test.rb) でも、
型付きカラム + デフォルト値の振る舞いが、新実装に合わせて微調整されています。test/schema/schema.rbの 1 行追加は、回 regress テスト用にスキーマ定義を少しだけ変更したものと思われます(型変更などのシナリオを再現するため)。
- 影響範囲・注意点
影響するケース
- Rails 8.1.0 以降において、以下のようなコードを書いているアプリで影響が出ていた可能性があります:
- DB カラム定義の型と、Rails モデル側の attribute type が異なっている(または後から上書きしている)
- 例: DB:
booleanカラム、モデル:attribute :flag, :integer - 例: DB:
stringカラム、モデル:attribute :settings, :json
- 例: DB:
- そのカラムにデフォルト値が設定されている
- 新規レコード作成時に「デフォルト値を起点にした型変換のエラー」や「予期せぬ値」が出ていた
- DB カラム定義の型と、Rails モデル側の attribute type が異なっている(または後から上書きしている)
このPRにより、そうした問題は解消される可能性が高いです。
互換性の観点
- 8.1.0 の「事前 deserialization」挙動に依存したコードを書いていた場合は、再び「従来の動き(DB 生値ベース)」に戻るため、挙動が変わります。
- ただし、8.1.0 で新しい挙動を前提にしたコードを書くのは難しく、かつ今回のPR自体がバグ修正扱いであることから、ほとんどのアプリにとっては「不具合が直る」「元の感覚に戻る」変更になります。
注意点・設計上の示唆
- デフォルト値の扱いは「DB が持つ型」と「Rails の Attribute 型」が一致している前提で設計するのが安全です。
- もしアプリ側で attribute type を上書きする場合は、「デフォルト値もその新しい型にとって自然な値になるか」を意識する必要があります。
- 特に Mutable な型(JSON, Array, Hstore など)では、デフォルト値の共有・破壊的変更まわりでバグを生みやすいため、
- モデル側で
attribute :settings, :json, default: -> { {} }のように Proc で都度生成する - DB のデフォルトは
NULLにして、アプリ側で初期値を与える
といった方針を検討した方がよいです。
- モデル側で
- 参考情報 (あれば)
- このPRで修正・クローズしている Issue/PR:
- 元となった変更:
- デフォルト値と型変換の挙動に関するドキュメント:
- Rails Guides: Active Record Migrations(デフォルト値、DB 依存の挙動など)
- Rails API:
ActiveRecord::Attributes,ActiveRecord::ConnectionAdapters::Column
#58350 Append TRADITIONAL instead of STRICT_ALL_TABLES to MySQL's sql_mode by default
マージ日: 2026/8/9 | 作成者: @kamipo
- 概要 (1-2文で)
Rails が MySQL に接続する際のデフォルトsql_mode設定を、STRICT_ALL_TABLES単体ではなくTRADITIONALを付与するように変更した PRです。これにより、RDS/Aurora などグローバルsql_modeが空の環境でも、ゼロ日付やゼロ除算などの不正値をサイレントに受け入れない、より厳格な動作が一貫して保証されます。
- 変更内容の詳細
何を変えたか
これまで Rails の MySQL アダプタは、接続時に sql_mode が空または特定条件のときに STRICT_ALL_TABLES を付け足していましたが、この PR で以下のように変更されています。
- 旧:
sql_modeにSTRICT_ALL_TABLESを append - 新:
sql_modeにTRADITIONALを append
TRADITIONAL は MySQL の「モードセット」で、実質的に以下を含む厳格モードです(主なもの):
STRICT_ALL_TABLESNO_ZERO_IN_DATENO_ZERO_DATEERROR_FOR_DIVISION_BY_ZERO- など、MySQL が「伝統的」な厳格動作として束ねているモード群
MySQL 5.7+ では、これら (NO_ZERO_IN_DATE, NO_ZERO_DATE, ERROR_FOR_DIVISION_BY_ZERO) がデフォルト sql_mode に含まれるようになりましたが、
Amazon RDS / Aurora MySQL の「デフォルトパラメータグループ」などではグローバル sql_mode が空のまま運用されていることがあり、その場合に Rails が STRICT_ALL_TABLES だけ追加しても、上記 3 つは有効化されないというギャップがありました。
この PR はそのギャップを埋めるためのものです。
実際のコード上の変更点(イメージ)
abstract_mysql_adapter.rb で、接続時に sql_mode を組み立てる処理がだいたい次のようなイメージに変わります(あくまで説明用の擬似コード):
# 変更前のイメージ
sql_mode = select_value("SELECT @@SESSION.sql_mode")
if sql_mode.blank?
sql_mode = "STRICT_ALL_TABLES"
else
sql_mode = "#{sql_mode},STRICT_ALL_TABLES" unless sql_mode.include?("STRICT_ALL_TABLES")
end
execute("SET SESSION sql_mode='#{sql_mode}'")
# 変更後のイメージ
sql_mode = select_value("SELECT @@SESSION.sql_mode")
if sql_mode.blank?
sql_mode = "TRADITIONAL"
else
sql_mode = "#{sql_mode},TRADITIONAL" unless sql_mode.include?("TRADITIONAL")
end
execute("SET SESSION sql_mode='#{sql_mode}'")正確な実装は PR 内の2行変更ですが、要点としては「何を足すか」が STRICT_ALL_TABLES から TRADITIONAL に変わっただけです。
テスト・ドキュメント
activerecord/CHANGELOG.md- MySQL アダプタのデフォルト
sql_mode振る舞いがTRADITIONALベースになることを記載。
- MySQL アダプタのデフォルト
activerecord/test/cases/adapters/abstract_mysql_adapter/connection_test.rb- 接続時の
sql_modeにTRADITIONALが含まれていることを検証するテストを追加。
- 接続時の
また、PR 説明にもある通り、以前の挙動(STRICT_ALL_TABLES のみを指定)を再現したい場合は、database.yml で明示的に指定できます。
production:
adapter: mysql2
database: my_app_production
username: ...
password: ...
variables:
sql_mode: "STRICT_ALL_TABLES"- 影響範囲・注意点
影響を受ける環境
- 主に以下のような環境で影響が出ます:
- グローバル
sql_modeが空、もしくは非常に緩い設定になっている MySQL サーバ - 特に Amazon RDS / Aurora MySQL のデフォルトパラメータグループをそのまま使っている環境
- グローバル
MySQL 5.7+ で、すでにサーバ側の sql_mode を TRADITIONAL 相当か、厳格なモードにしている場合は、この変更による挙動の差分はほぼありません。
具体的に変わる挙動の例
以下のようなケースで、これまでは「通っていた」ものが、今後はエラーになる可能性があります(特にテスト/本番環境で sql_mode が異なる場合に顕在化しやすいです)。
- 不正な日付値の挿入
INSERT INTO users (name, birthday) VALUES ('Alice', '0000-00-00');
INSERT INTO users (name, birthday) VALUES ('Bob', '2024-00-01');- 旧挙動 (
STRICT_ALL_TABLESのみ, かつNO_ZERO_DATEなど無効)- サイレントに
0000-00-00などが保存される、もしくは警告にとどまる。
- サイレントに
- 新挙動 (
TRADITIONAL)NO_ZERO_DATE/NO_ZERO_IN_DATEによりエラーとして扱われ、INSERT が失敗。
- ゼロ除算
SELECT 1 / 0;- 旧挙動(
ERROR_FOR_DIVISION_BY_ZEROが無効)NULLが返ってくるか、警告で済む。
- 新挙動 (
ERROR_FOR_DIVISION_BY_ZERO有効)- エラーとして扱われる。
Rails アプリ側から見ると、これまで普通に通っていた save や update が ActiveRecord::StatementInvalid などの例外を投げるようになる可能性があります。
回避・調整方法
- 以前と同じ(緩い)挙動を維持したい場合:
database.ymlで明示的にsql_modeを指定します。
production:
adapter: mysql2
variables:
sql_mode: "STRICT_ALL_TABLES" # 旧Rails相当
# あるいは、まったく独自のモードを指定してもよい
# sql_mode: ""- ただし、Rails コアチームの方針としては、
strict: falseオプションは #57077 で非推奨になっており、- 「不正な値をサイレントに受け入れない」厳格な動作が推奨されています。
そのため、よほどの互換性理由がない限りはTRADITIONALのまま運用し、アプリケーション側で不正データを生成しないように修正するのが推奨されます。
互換性上の注意点
- 既存のデータベースに
0000-00-00などの不正値が既に入っている場合、- それを更新/再保存する処理で問題が出る可能性があります。
- データクリーンアップ(不正日付の修正、正しい NULL / デフォルト値への置き換えなど)を検討してください。
- 本番とローカルで
sql_modeが異なる場合、- ローカルでは動くが本番でエラー、というずれが起こりがちです。
- Rails 側から
variables: { sql_mode: ... }を統一的に指定しておくと環境差分を減らせます。
- 参考情報 (あれば)
- この PR:
Append TRADITIONAL instead of STRICT_ALL_TABLES to MySQL's sql_mode by default(#58350)
- 関連 PR / Issue:
Deprecate strict: false option for MySQL(#57077)- MySQL アダプタの
strict: falseオプション非推奨化。
「不正な値をサイレントに許さない」方向へのポリシー変更の一環。
- MySQL アダプタの
- MySQL 公式ドキュメント:
sql_modeシステム変数TRADITIONALモードの説明(STRICT_ALL_TABLESを含むモードセットであることなど)
#58424 Keep the controller middleware proxy in sync with its stack
マージ日: 2026/8/9 | 作成者: @55728
- 概要 (1-2文で)
ActionController::Metal.middlewareが返す「ミドルウェア用プロキシ」と実際のミドルウェアスタックの状態がズレていた問題を修正し、常に最新のスタックを正しく反映するようにしたPRです。合わせて、deleteの戻り値やmiddlewares=の挙動など、従来のインターフェイスからズレていた細かい挙動も元の仕様どおりになるよう整えています。
- 変更内容の詳細
背景: 問題になっていた挙動
dd67a577d9 以降、ActionController::Metal.middleware は実体のスタックではなく「プロキシオブジェクト」を返すようになりました。
そのプロキシは 生成時点のスタックをキャプチャ しており、読み取り系のメソッド呼び出しもそのキャプチャしたスタックに対して行われていました。
その結果、次のような不整合が起きていました:
middleware = PostsController.middleware
middleware.use Mw # ここでスタックのコピーを作ってミューテートする
middleware.size # => 0 (プロキシ生成時点のスタックを見ている)
PostsController.middleware_stack.size # => 1 (実際には増えている)他にも、以下の2点の「以前の挙動とのズレ」がありました:
deleteが、ラップ対象であるスタックの戻り値ではなく「スタック自身」を返してしまう- 元々は「存在しないミドルウェアを削除しようとしたとき
nilを返す」仕様だったが、プロキシ化後はこれが崩れていた。
- 元々は「存在しないミドルウェアを削除しようとしたとき
middlewares=がプロキシ経由でなく「スタックそのもの」に直接委譲されていた- そのため、凍結済みスタックに対しては
FrozenErrorが発生してしまう状況になっていた (#58342 で是正した他メソッドと同じ問題)。
- そのため、凍結済みスタックに対しては
dd67a577d9 以前は middleware がスタックの実体だったため、上記3つはいずれも期待どおりに動いていました。
このPRでの修正点
1) プロキシが「常に最新のスタック」を見るように変更
これまでは:
- プロキシ生成時点の
@stackをキャプチャし、以降のsizeなどの読み取りもキャプチャしたものに対して行う
これを:
- プロキシの各メソッド呼び出し時に、常にコントローラの「現在の」スタックを取り直して参照する
- つまり、プロキシは「スナップショット」ではなく「現在のスタックへの窓口」として振る舞うようになる
そのため、次のようなコードで状態が正しく同期されます:
middleware = PostsController.middleware
middleware.use Mw
middleware.size # => 1
PostsController.middleware.size # => 1 (同じ状態を見ている)2) プロキシ経由のメソッドが「スタックの戻り値」を正しく返すよう修正
これまでは、プロキシがミューテーション用のメソッドを「スタックを返す」ような形でラップしており、スタック本来の戻り値が失われていました。
このPRでは:
- プロキシが対象スタックにメソッドを委譲したとき、元のメソッドの戻り値をそのまま返す ように変更
これにより、たとえば delete の戻り値が元の仕様どおりに戻ります:
# プロキシ経由
removed = PostsController.middleware.delete(NonExistingMiddleware)
removed # => nil (存在しない時は nil)
# 元のスタックでも同様の挙動
removed = PostsController.middleware_stack.delete(NonExistingMiddleware)
removed # => nil3) middlewares= もプロキシ対象に追加
以前は middlewares= がプロキシ経由ではなく、スタックに直接ディスパッチされていたため:
- 凍結済みスタックに対して
middlewares=を呼ぶとFrozenErrorが発生 - #58342 で修正した他のミューテーション系メソッド (
useなど) と挙動が不揃い
このPRで:
middlewares=を他のミューテーション系メソッドと同様、プロキシを通して処理する ようにした- これにより、
middlewares=も「コピーを作って置き換え → 再フリーズ」というパターンになり、スタックのシェア/フリーズ戦略と整合する
サンプルイメージ:
# 以前: FrozenError が起こりうる
PostsController.middleware_stack.freeze
PostsController.middlewares = [Mw1, Mw2] # FrozenError
# 今回の修正後 (プロキシがコピー・再フリーズする前提の設計)
PostsController.middleware.freeze # 内部スタックが凍結されていても
PostsController.middlewares = [Mw1, Mw2] # コピーを新たに作って差し替え可能※ 上記コードは設計意図のイメージであり、実際のAPI名/利用形態はプロジェクトのコードに依存します。
4) テストの追加
次の3ケースに対してテストが追加されています (actionpack/test/controller/new_base/middleware_test.rb):
- プロキシ経由でのミューテーションの結果が、同じプロキシを通した読み取りに反映されること
- 存在しないミドルウェアを
deleteした場合にnilが返ること middlewares=による再設定後も、スタックが「共有可能 (shareable)」な状態であること- つまり、スタックのコピー&再フリーズ戦略が壊れていないことを確認
全体として actionpack のフルテストスイート (4278 runs, 20060 assertions) はグリーンです。
- 影響範囲・注意点
- 主に影響を受けるのは、
ActionController::Metal.middlewareを直接触っているコード および拡張 (エンジン・プラグイン・ライブラリ) です。middlewareが返すオブジェクトの挙動が、以前の「実体スタック」準拠に戻るイメージで、dd67a577d9以降の一時的な挙動からは変わります。
- 具体的な注意点:
deleteの戻り値に対して、nil以外の挙動を期待したワークアラウンドが入っていた場合、それは不要・または壊れる可能性があります。middleware経由のsizeなどを使っており、以前の「同期していない状態」を前提にしたコードがある場合 (まずないと思われますが)、挙動が変わります。middlewares=を使って独自にスタックを差し替えているコードは、この変更で 凍結/共有戦略と整合的に動くようになる ため、FrozenErrorをハンドリングしていたコードなどは削減できる可能性があります。
- APIシグネチャ自体は変わっておらず、「元々のドキュメントどおりの挙動」に近づけるバグ修正なので、通常は後方互換性の観点で有利な変更と考えてよいです。
- 参考情報 (あれば)
- 関連コミット:
dd67a577d9ActionController::Metal.middlewareをスタック実体からプロキシオブジェクトに変更したコミット。
- 関連PR: #58342
- 「凍結されたミドルウェアスタックをミューテートしようとすると
FrozenErrorになる」問題を、コピー&再フリーズ戦略で回避するための修正。
- 「凍結されたミドルウェアスタックをミューテートしようとすると
- 本PR: #58424
- 上記の設計を踏まえ、「プロキシとスタックの状態の同期」「戻り値の整合性」「
middlewares=のプロキシ化」という3点を仕上げるフォローアップ。
- 上記の設計を踏まえ、「プロキシとスタックの状態の同期」「戻り値の整合性」「
#58071 Add delete_multi method to batch deletes for memcached store if dalli's delete_multi is defined
マージ日: 2026/8/9 | 作成者: @ilianah
- 概要 (1-2文で)
Rails のActiveSupport::Cache::MemCacheStoreに、dalli がdelete_multiをサポートしている場合にそれを使って一度に複数キーを削除する機能が追加されました。これにより memcached へのラウンドトリップ回数が減り、バッチ削除時のパフォーマンスが向上します。
- 変更内容の詳細
2-1. delete_multi_entries の追加
MemCacheStore に、複数キー削除用の内部メソッド delete_multi_entries が追加されています。
趣旨としては:
- dalli クライアントが
delete_multiを実装しているならそれを利用する - 実装されていない/使えない場合は従来通り 1 件ずつ
delete_entryを呼ぶ既存の挙動にフォールバック(※この部分は既存ロジック側)
ポイント:
- dalli の
delete_multiは「キーごとの削除成否」を返さない - そのため Rails 側では「削除対象キー数」か「0」のどちらかしか返せない仕様になっている
疑似コードイメージ(実際のコード構造を簡略化):
def delete_multi_entries(entries, options)
# entries: [[key, version], [key2, version2], ...] のような形式を想定
keys = entries.map { |entry| entry.key }
if @data.respond_to?(:delete_multi)
result = instrument(:delete_multi, keys) do
@data.delete_multi(keys, expires_in: options[:expires_in])
end
# dalli の delete_multi が RingError を内部で rescue して
# 空配列を返すケースがあるため、成功判定に注意が必要
if result.blank?
0
else
keys.size
end
else
# 古い dalli 等で delete_multi が無い場合は、既存の逐次削除ロジックにフォールバック
super
end
end※上記は説明用の擬似コードです。実際には entries の構造や instrument などは Rails 内部 API に合わせて実装されています。
2-2. 戻り値仕様の変更 (mem_cache_store のみの違い)
今まで:
delete_multiは内部的に複数回delete_entryを呼んでおり、各キーごとの削除成否がわかるため、「実際に削除できたキー数」を正確に返せていました。
今回:
- dalli の
delete_multiは per-key 結果を返さないため、「成功なら要求した件数」「失敗(もしくは失敗とみなす条件)なら 0」という粗い情報しか返せません。 MemCacheStoreのテスト (test_delete_multi) はこの仕様差にあわせて上書きされています。
これにより、mem_cache_store における delete_multi の戻り値は「実際の削除件数」ではなく「期待された削除件数 or 0」になります。
2-3. エラーハンドリング (Dalli::RingError 対応)
dalli 側の実装では、バッチ削除中にサーバ障害等があっても Dalli::RingError がライブラリ内で rescue され、例外を投げずに空配列を返す 挙動になっています:
Dalli::RingErrorが起きる- dalli がそれを rescue
delete_multiは[](空配列)を返す- Rails 側の
rescue_error_withには例外が届かない
Rails のキャッシュには「失敗に対して安全である(failure safety)」というテストがあり、サーバが落ちている場合には「削除件数 0 を返す」必要があります。
そのため delete_multi_entries 側で:
result.blank?(空配列やnil)なら「失敗」とみなし 0 を返す- それ以外なら「成功」とみなし
keys.sizeを返す
という条件分岐が入っています。
- 影響範囲・注意点
対象ストア:
ActiveSupport::Cache::MemCacheStoreを使用している場合にのみ影響します。- 他のキャッシュストア(MemoryStore, RedisCacheStore など)には影響しません。
dalli のバージョン依存:
delete_multiを実装している dalli バージョンではバッチ削除が 1 回のラウンドトリップで行われ、パフォーマンス向上が見込めます。delete_multiが無い古い dalli では従来どおり 1 件ずつ削除する挙動のままです。
戻り値の違いに注意:
MemCacheStore#delete_multi(およびそれを間接的に呼び出す API)の戻り値を「実際に削除されたキー数」として厳密に扱っていた場合、今回の変更で意味が変わります。- 以前: 「実削除件数」
- 今回: 「成功とみなされたら
要求件数、失敗とみなされたら0」
- ロジック上、「一部成功/一部失敗」を検知する用途には利用できなくなります。
障害時の挙動:
- memcached クラスタ障害などで dalli が
RingErrorを内部で rescue した場合でも、アプリ側には例外が飛んでこず、「0 が返ってきて終わる」挙動になります。 - 「キャッシュ削除失敗をロギング・監視したい」場合は、Rails のログ(
instrument周り)や dalli のログ出力を別途確認する必要があります。
- memcached クラスタ障害などで dalli が
- 参考情報 (あれば)
- この PR:
- 類似 PR(
fetch_multiの高速化と思われる): - dalli に
delete_multiが追加された PR: - dalli の
delete_multi実装(Dalli::RingErrorを rescue して空配列を返す部分): - Rails の failure safety テスト(キャッシュ障害時の期待挙動):
#58358 Fix incorrect documentation
マージ日: 2026/8/9 | 作成者: @55728
- 概要 (1-2文で)
Active Record 周辺のドキュメント (RDoc と Guides) に、実際の挙動と矛盾する記述や誤ったサンプルコードがあったため、それらを実装に合わせて修正した PR です。コードロジックの変更はなく、ドキュメントのみの修正です。
- 変更内容の詳細
2-1. connected_to? の RDoc 例の修正
対象: activerecord/lib/active_record/connection_handling.rb
- 問題点:
connected_to?の使用例が、実際にはありえない戻り値を示していた。 - 実際の仕様:
connected_to?(role: ..., shard: ...)は、「現在の接続コンテキストの role と shard が、指定した引数 両方 と一致したときにのみtrue」を返す。- 従って、
connected_to(role: :reading, shard: :shard_one)ブロック内では、role: :writing を指定してconnected_to?がtrueになることはない。
誤ったイメージの例(※説明用、実際のコード断片ではない):
connected_to(role: :reading, shard: :shard_one) do
# 実際には false になるのに true と書かれていた例
connected_to?(role: :writing, shard: :shard_one) # => true (と誤記)
end正しい挙動は:
connected_to(role: :reading, shard: :shard_one) do
connected_to?(role: :reading, shard: :shard_one) # => true
connected_to?(role: :writing, shard: :shard_one) # => false
endテスト (connection_handlers_sharding_db_test.rb) でも assert_not で後者が false であることが担保されており、ドキュメント側をそれと整合する内容に修正しています。
2-2. Relation#readonly の RDoc 例の修正
対象: activerecord/lib/active_record/relation/query_methods.rb
- 問題点:
#readonlyのサンプルコードが、readonlyが 新しい Relation を返すという性質を無視していて、挙動が実際と食い違う例になっていた。 - 実際の仕様:
users.readonlyは「元の Relation (users) を変更する」のではなく「読み取り専用な新しい Relation を返す」。- 元の例では、
readonlyの戻り値を変数に再代入しておらず、users.first.saveがtrueを返すかのように書かれていたが、実際にはActiveRecord::ReadOnlyRecordが発生する。
誤ったイメージの例:
users = User.where(active: true)
users.readonly
users.first.save # => true (と誤記)実際に起きること:
users.readonlyの戻り値を無視すると、usersは依然として writable な Relation。- しかし、RDoc の文脈上は
readonlyした Relation を対象にsaveしているように説明されていたため、読み手に誤解を与えていた。
正しい使い方の例(今回の修正方向):
users = User.where(active: true)
users = users.readonly # ここで Relation を差し替える
users.first.save
# => ActiveRecord::ReadOnlyRecord が raise される#create_with の RDoc 例がすでに「戻り値を変数に再代入する」形になっていたため、それと同じスタイルに合わせて修正されています。
2-3. Associations Guide の「失敗した関連付けの保存」に関する説明修正
対象: guides/source/association_basics.md
- 対象箇所:
has_one,has_many,has_and_belongs_to_manyの 3 つのセクション。 - 問題点:
- ガイドでは、「関連を代入して保存が失敗した場合、代入式が false を返す」と説明されていた。
- しかし Ruby の代入式は常に右辺値を返し、false を返すことはない。
- 実際の挙動は、保存に失敗した場合:
has_one,has_many:ActiveRecord::RecordNotSavedが raise される。has_and_belongs_to_many:ActiveRecord::RecordInvalidが raise される。
- つまり「false を返す」のではなく、「例外が発生し、代入自体がロールバック(キャンセル)される」が正しい。
誤解を招く表現のイメージ:
If saving the assigned record fails, the assignment statement returns `false`.正しいイメージ:
user.profile = profile_params # has_one の例
# 保存に失敗した場合:
# => ActiveRecord::RecordNotSaved が raise される
# user.profile の変更もロールバックされる (代入は成立しない)ここ 3 箇所の説明を、API ドキュメントおよび 2011 年以降の実際の挙動に合わせて、「false を返す」ではなく「例外 (RecordNotSaved / RecordInvalid) が発生する」という記述に修正しています。
- 影響範囲・注意点
- この PR は ドキュメントのみの変更 であり、挙動・API の追加・非互換変更は一切ありません。
- ただし、これまで誤ったドキュメントを前提にコードを書いていた場合、以下の点で思い込みが修正される必要があります:
connected_to?- role か shard のどちらか片方だけ一致していても
trueにはならない。両方一致して初めてtrue。
- role か shard のどちらか片方だけ一致していても
Relation#readonly- インスタンスメソッドチェーンで Relation を変更するときは、
users = users.readonlyのように戻り値を必ず使う。
- インスタンスメソッドチェーンで Relation を変更するときは、
- 関連付け代入時のエラー処理
user.profile = profileなどの関連代入が失敗したときにfalse判定で分岐するコードは間違い。- 正しくは例外 (
ActiveRecord::RecordNotSaved/RecordInvalid) を rescue して処理する。 - 代入後の関連オブジェクトの状態がロールバックされることを前提にする。
- 正しくは例外 (
- 参考情報 (あれば)
- 関連クラス/メソッド:
ActiveRecord::ConnectionHandling#connected_toActiveRecord::ConnectionHandling#connected_to?ActiveRecord::Relation#readonlyActiveRecord::RecordNotSavedActiveRecord::RecordInvalid
- 実際の挙動を確認したい場合は、
connection_handlers_sharding_db_test.rbや Association 関連のテストを参照すると、今回修正された説明と同じ前提でテストが書かれていることが分かります。
#58420 Refactor MiddlewareStack::Middleware off Hash.ruby2_keywords_hash
マージ日: 2026/8/9 | 作成者: @kamipo
- 概要 (1-2文で)
Rails のミドルウェアスタック内部実装からHash.ruby2_keywords_hashへの依存を取り除き、Ruby 3系以降のキーワード引数仕様に素直に従う形へリファクタリングした PR です。併せて、Middleware/InstrumentationProxyクラスを内部実装扱いとしてドキュメント非公開 (:nodoc:) にしつつも、デバッグ用途の情報は維持しています。
- 変更内容の詳細
背景: Hash.ruby2_keywords_hash の非推奨化
Ruby 本体側で Hash.ruby2_keywords_hash が非推奨予定になっており(Ruby 2系→3系のキーワード引数互換のための一時的 API)、
「Ruby 3.0 以上のみをサポートするライブラリは依存すべきでない」とされています。
この PR は、その方針に沿って Rails のミドルウェア関連コードからこの API を除去するフォローアップです(元 PR: #58239)。
コアの変更点: Middleware の引数保持方法
従来は、ミドルウェアのキーワード引数を「1つの配列に押し込む」ために Hash.ruby2_keywords_hash を利用していました。
以前のイメージ(概念的なコード例)
class MiddlewareStack::Middleware
def initialize(klass, *args, &block)
# args の中に ruby2_keywords_hash 済み kwargs を押し込んでいた
@klass = klass
@args = args # 最後に ruby2_keywords_hash(kwargs) が入っている場合がある
@block = block
end
def build(app)
# ruby2_keywords による展開前提の呼び出し
@klass.new(app, *@args, &@block)
end
endこの方式は「位置引数とキーワード引数が明確に分かれていない」「ruby2_keywords/ruby2_keywords_hash 依存」という問題がありました。
変更後: 位置引数とキーワード引数を分離
PR 説明によると、Middleware(および ActionController 側のサブクラス)は以下のような構造に変更されています。
- 位置引数:
@args - キーワード引数:
@kwargs(Hash) - インスタンス生成時は「位置引数 + キーワード引数」を明示的に展開して呼び出し
概念的には次のような形です:
class MiddlewareStack::Middleware # 実際には :nodoc:
attr_reader :args, :kwargs
def initialize(klass, args:, kwargs:, &block)
@klass = klass
@args = args # Array
@kwargs = kwargs # Hash
@block = block
end
def build(app)
# Ruby 3スタイルで明示的に **kwargs を展開して呼び出す
@klass.new(app, *args, **kwargs, &@block)
end
endActionController 側の Middleware サブクラスも同様の方針に合わせて修正されており、
ミドルウェアの生成時に位置引数 / キーワード引数が明確に分離されるようになりました。
API ドキュメンテーション上の扱い変更 (:nodoc:)
以下の2クラスが :nodoc: 指定され、公式 API ドキュメントから隠されています。
ActionDispatch::MiddlewareStack::MiddlewareActionDispatch::MiddlewareStack::InstrumentationProxy
理由:
- 通常のユーザは
use/insertなどのインターフェイスを通じてミドルウェアスタックを操作するため、
これらのクラスを直接インスタンス化する想定はない。 - ミドルウェア計測(instrumentation)についても、
process_middleware.action_dispatch通知の購読が公式な利用方法であり、InstrumentationProxyの内部構造は公開 API ではない。
一方で、Rails.application.middleware の反復処理時に Middleware#args / #kwargs を閲覧できる挙動は維持されており、
開発者がスタックの状態をデバッグする用途は引き続き可能です。
テストと CHANGELOG
actionpack/test/dispatch/middleware_stack_test.rbとrailties/test/commands/middleware_test.rbが、新しい引数の扱い方(args/kwargsの分離)に合わせて更新されています。actionpack/CHANGELOG.mdに、この変更に関するエントリが追加され、
将来のアップグレード時に「Middlewareが内部実装であり、ruby2_keywords 系 API に依存しなくなった」ことが分かるようになっています。
- 影響範囲・注意点
通常のアプリケーション開発者への影響
config.middleware.use/insert_before/insert_afterなどでミドルウェアを登録している一般的な用途では挙動は変わりません。- 既存のミドルウェアクラスのコンストラクタが
def initialize(app, *args, **kwargs)のように Ruby 3 スタイルで書かれている場合、この変更はむしろ自然にマッチします。 Rails.application.middlewareをループして中身を確認する際、middleware.args/middleware.kwargsが利用可能であり、
以前より「どれがキーワード引数か」が明確になります。
ライブラリ・メタプログラミング的なコードへの影響
注意が必要なのは、Rails の内部クラスを前提にした高度なメタプログラミングやツールの作者です。
ActionDispatch::MiddlewareStack::MiddlewareやInstrumentationProxyを直接 new していた場合:
これらは内部実装であり、:nodoc:によって今後破壊的変更もありうるクラスとして明示されました。
すでに直接利用しているコードがあれば、use/insert/ instrumentation API 経由に書き換えることを検討してください。Middleware#argsに「最後の要素が ruby2_keywords_hash 済み Hash である」ことを前提としたコードがある場合:
今後はargsが純粋な位置引数のみ、kwargsがキーワード引数を持つ、という前提に切り替える必要があります。- Ruby バージョン依存コードの整理:
ライブラリ側も「Ruby 3 専用」と位置づけるのであれば、同様にruby2_keywords/ruby2_keywords_hashへの依存をやめることが推奨されます。
- 参考情報 (あれば)
- Ruby 本体の issue:
Hash.ruby2_keywords_hash非推奨に関する議論: https://bugs.ruby-lang.org/issues/22205
- 関連 PR:
- #58239: 本 PR のフォローアップ元となる、ミドルウェア関連の引数扱いリファクタリング
- Rails ガイド:
- Active Support Instrumentation ガイド(
process_middleware.action_dispatchのサブスクライブ方法など)
https://guides.rubyonrails.org/active_support_instrumentation.html - ミドルウェアの追加/削除など:
https://guides.rubyonrails.org/rails_on_rack.html
- Active Support Instrumentation ガイド(
#58239 Refactor CommandRecorder to store args and kwargs separately
マージ日: 2026/8/9 | 作成者: @kamipo
- 概要 (1–2文で)
ActiveRecord::Migration::CommandRecorderが、記録するコマンドの引数を「位置引数」と「キーワード引数」に分けて保持する形にリファクタリングされました。これにより、ruby2_keywords時代の互換レイヤを廃止し、Ruby 3 系の kwargs 仕様に素直に従う実装になっています。
- 変更内容の詳細
2-1. recorder.commands の構造変更
これまで:
# commands の 1 要素は 3 要素タプル
[cmd, args, block]
# かつ kwargs は args の末尾の Hash に埋め込まれていた
[:create_table, ["users", { if_not_exists: true }], nil]これから:
# commands の 1 要素は 4 要素タプル
[cmd, args, kwargs, block]
# 位置引数とキーワード引数が分離される
[:create_table, ["users"], { if_not_exists: true }, nil]cmd: シンボル(例::create_table,:add_column)args: 通常の配列(位置引数)kwargs: ハッシュ(キーワード引数)block: ブロック (あれば Proc, なければ nil)
影響ポイント
- 以前は
recorder.commandsを読むコードは「最後の要素が Hash なら options とみなす」といったパターンで kwargs を扱っていましたが、今後はkwargs引数として別に渡されます。
旧コード例:
recorder.commands.each do |name, args, block|
options = args.extract_options! # または args.last.is_a?(Hash) ? args.pop : {}
# ...
end新コード例:
recorder.commands.each do |name, args, kwargs, block|
# args と kwargs が明確に分かれている
end2-2. invert_* 系ヘルパのシグネチャ変更
マイグレーションの「可逆性」を実現するための invert_* メソッド類が、kwargs を明示的に受け取る形に統一されています。
例: invert_add_column(イメージ)
変更前(概念的には)
def invert_add_column(table_name, column_name, type, *args)
options = args.extract_options!
# options の中に null: false, default: ... などが入る
end変更後
def invert_add_column(table_name, column_name, type, *args, **kwargs)
# kwargs に null:, default:, if_not_exists: などがそのまま入る
end実装内からは、.extract_options! や args.last.is_a?(Hash) に依存する処理が取り除かれ、kwargs をそのまま使う形になっています。
同様の変更が、各種 invert ヘルパ (invert_create_table, invert_add_index など) に波及しています。
2-3. ruby2_keywords 時代の互換コードの削除
背景:
- Ruby 2.7〜3.0 移行期には、メソッドチェーンの途中で kwargs を失わないように
ruby2_keywordsや「引数を 1 本の配列にパックして末尾の Hash を kwargs 扱いする」といったパターンが必要でした。 - Rails 本体でも、この互換パターンに合わせて CommandRecorder の内部表現を「
[cmd, args, block]+ args 末尾 Hash = kwargs」という形にしていました。
今回の変更点:
- Rails が Ruby 3.3.1+ を前提とするようになったため、
ruby2_keywords互換レイヤが不要になりました。 - それに伴い、kwargs 専用の引数 (
**kwargs) として情報を保持/伝播させる実装にリファクタリングされています。 .extract_options!や「options = args.last if args.last.is_a?(Hash)」に代表される「末尾 Hash を options とみなす」イディオムが、CommandRecorder 周辺からはほぼ消えています。
2-4. テスト・互換レイヤ (compatibility.rb) の更新
activerecord/test/cases/migration/command_recorder_test.rbが、新しい 4 要素タプル形式に合わせて全面的に書き換えられています(+86/-86)。invertible_migration_test.rbも、kwargs 分離後の API に合わせて修正。activerecord/lib/active_record/migration/compatibility.rbでは、古いバージョンとの互換マイグレーション実装の中で引数処理をしていた部分が、args/kwargs分離を前提にしたコードへ手直しされています。schema_statements.rbにも kwargs に関する細かい修正が入っています(合計 6 行の変更)。
- 影響範囲・注意点
3-1. 最も重要な互換性ポイント
recorder.commands の形式に依存したコードは必ず壊れます。
具体的には、以下のようなコードがある場合は要修正です。
NG(旧形式前提):
recorder.commands.each do |name, args, block|
options = args.last.is_a?(Hash) ? args.last : {}
# ...
endOK(新形式対応):
recorder.commands.each do |name, args, kwargs, block|
# kwargs をそのまま使う
endあるいは、どうしても古い/新しい両方に対応したい Gem 側などでは、以下のようなガードが必要になります:
recorder.commands.each do |tuple|
case tuple.length
when 3
name, args, block = tuple
kwargs = args.last.is_a?(Hash) ? args.pop : {}
when 4
name, args, kwargs, block = tuple
else
raise "Unknown CommandRecorder tuple size: #{tuple.length}"
end
# 以降は name, args, kwargs, block で扱う
end3-2. invert_* メソッドを独自にオーバーライドしている場合
アプリや Gem で ActiveRecord::Migration::CommandRecorder を継承し、独自に invert_* 系メソッドを定義している場合:
- 旧来のシグネチャ(
*argsのみ)にしていると kwargs が落ちる/正しく受け取れない可能性があります。 - 定義を
def invert_xxx(*args, **kwargs)、または引数を明示した上で**kwargsを受け取る形に修正しておくと安全です。
Ruby 3 では、kwargs を位置引数で受ける書き方はそもそも非推奨・エラーになるパターンがあるため、この変更は Ruby 本体の方向性にも沿っています。
3-3. Rails 内部 API を叩いているメタプログラミング的コード
- マイグレーション周りを「メタプログラミング的にラップしているコード」(DSL 追加や記録内容の書き換えなど)で、
CommandRecorderの内部表現に強く依存しているものは動作確認が必要です。 - 特に「args の末尾 Hash を書き換える」「args に options を push する」といった操作は、
kwargsに直接触る形に変更する必要があります。
- 参考情報 (あれば)
- 当該 PR: Refactor
CommandRecorderto store args and kwargs separately (#58239) - Ruby 側の関連提案:
ruby2_keywordsを将来的に非推奨にする提案
https://bugs.ruby-lang.org/issues/22205 - Rails が Ruby 3 系を前提にし始めた文脈や、kwargs 仕様の整理は、Rails/ActiveRecord の CHANGELOG (activerecord/CHANGELOG.md) にも追記されています。
この PR 以降、CommandRecorder に直接触れるコードは「[name, args, kwargs, block]」の 4 要素タプルを前提に整理するのが推奨です。
#58342 Refreeze the controller middleware stack for every mutating method
マージ日: 2026/8/8 | 作成者: @55728
- 概要 (1-2文で)
Ractor セーフモード時にActionController::Metal.middlewareの一部のミドルウェア操作メソッドが凍結済みスタックを直接変更しようとしてFrozenErrorになる不具合を修正し、他の変更系メソッドと同様に「コピーして再凍結」されるように統一した PR です。Ractors 対応(ActiveSupport::Ractors.unshareable_proc_action有効時)のミドルウェアスタック操作が一貫して安全に動作するようになります。
- 変更内容の詳細
背景
ActionController::Metal.middleware が返すのは、実体のミドルウェアスタック (ActionDispatch::MiddlewareStack) に対するプロキシ (ActionController::MiddlewareStack::Proxy) です。
Ractor セーフモード (ActiveSupport::Ractors.unshareable_proc_action が :raise など) では、
このスタック内部の配列を Ractor 間で共有可能にするために「一度凍結」し、
変更要求が来たときは:
- 凍結されたスタックをコピーする
- コピー側に対して変更を行う
- コピーを再度「共有可能(shareable)」にする(凍結等)
という動作を行う必要があります。
そのため Proxy は「ミドルウェアスタックを破壊的に変更するメソッド」を列挙しておき、それらが呼ばれたときだけ「コピーして再凍結」のラッパを噛ませる設計になっています。
問題点
しかし、その「破壊的変更メソッドの列挙」に以下 4 メソッドが漏れていました:
insert_beforeinsert_afterdelete!move_before(実体はmoveのエイリアス)
このため、これら 4 つは delegate_missing_to 経由で素の ActionDispatch::MiddlewareStack に直接委譲され、凍結されている配列をそのまま変更しようとして FrozenError が発生していました。
再現例(PR 説明より):
ActiveSupport::Ractors.unshareable_proc_action = :raise
class PostsController < ActionController::Metal; end
PostsController.middleware.use(Mw) # OK: スタックがコピー&再凍結される
PostsController.middleware.insert_before(Mw, Other) # => FrozenError: can't modify frozen Arrayuse は「列挙済みの mutator(変更系メソッド)」なのでコピー&再凍結のラッパが働く一方、insert_before は列挙から漏れていたため、凍結済み配列に対する直接操作になり失敗していました。
この PR の対応
ActionController::MiddlewareStack::Proxy内の「ミドルウェアスタックの変更系メソッドのリスト」に、漏れていた 4 メソッド:insert_beforeinsert_afterdelete!move_before
を追加。
- これにより、他の mutator と同様に:
- 呼び出しごとにスタックのコピーを作成
- そのコピーに対して変更を適用
- コピーを再度 Ractor 共有可能な状態(凍結)にする という処理が行われるようになります。
テスト (actionpack/test/controller/new_base/middleware_test.rb) も追加され、
Ractor セーフモードでこれらメソッドを呼んでも FrozenError にならないことを確認しています。
動作の Before/After
- Before (Ractor セーフモード時):
useや、すでに列挙済みの mutator は OKinsert_before,insert_after,delete!,move_beforeはFrozenError発生
- After:
- 上記 4 メソッドも含め、すべての mutator が
「コピー → 変更 → 再凍結」ルールに従って安全に動作
- 上記 4 メソッドも含め、すべての mutator が
- 影響範囲・注意点
影響を受けるケース
ActionController::Metal(およびそれを継承するコントローラ)でcontroller.middleware.insert_before(...)controller.middleware.insert_after(...)controller.middleware.delete!(...)controller.middleware.move_before(...)
を使っており、
- かつ
ActiveSupport::Ractors.unshareable_proc_actionを有効(:raiseなど)にして Ractor セーフモードで実行している場合。
非 Ractor セーフモードでは挙動は従来と変わらない想定で、主に Ractor 対応時の不具合修正です。
「凍結済みスタックがいきなり FrozenError を投げる」という現象が解消され、Ractor 対応コードでも他のミドルウェア操作メソッドと一貫した挙動になります。
挙動変更は「以前に落ちていたコードが正常動作する」方向なので、後方互換性の観点でもほぼ安全な修正と考えられます。
- もし
FrozenErrorに依存したワークアラウンド等を書いていた場合は、その挙動は変わりますが、通常は期待されない使い方です。
- もし
- 参考情報 (あれば)
- 対象クラス/モジュール:
ActionController::MiddlewareStack::ProxyActionDispatch::MiddlewareStack
- 関連する設定:
ActiveSupport::Ractors.unshareable_proc_action:raise等に設定した際に、ミドルウェアスタック配列が Ractor 共有可能にするため凍結される
- 本挙動は
dd67a577d9で導入された「Proxy 経由でスタックにメソッドを呼ぶときは、常に共有可能なコピーを返す」という設計の抜け漏れの修正です。
#58217 Remove unused Active Record internals
マージ日: 2026/8/8 | 作成者: @55728
- 概要 (1-2文で)
このPRは、Active Record 内部で一切使われていないメソッドや引数・インスタンス変数などの「死んだコード」をまとめて削除するクリーンアップです。挙動の変更や外部APIの破壊的変更は基本的になく、内部実装の整理・単純化が主目的です。
- 変更内容の詳細
それぞれ「どこで使われなくなったか」がコミット履歴ベースで確認されており、grep(send・シンボル参照も含む)で参照ゼロであることが検証されています。
2-1. FromClause#merge の削除
- 対象ファイル:
activerecord/lib/active_record/relation/from_clause.rb - 削除されたメソッド:
FromClause#merge
Relation::Merger が汎用的な「clause-merge ループ」をやめ、where/having の明示的なマージと from 句の「置き換えルール」に変わった結果、FromClause#merge は呼ばれなくなっていました。
# 以前はこういったメソッドがあった(イメージ)
class FromClause
def merge(other)
# from 句同士をマージするロジック
end
end現在は from は「マージ」ではなく「上書き」される設計になっているため、このメソッドは不要となり削除されています。
ポイント:
内部的なリレーションマージの仕組みがシンプルになった影響で、FromClause#merge にフックしているアダプタや拡張コードがなければ影響はありません。
2-2. try_to_queue_for_background_connection の checkout_timeout 引数の削除
- 対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb - 対象メソッド:
try_to_queue_for_background_connection - 変更内容:
- メソッドシグネチャの
checkout_timeout引数が削除 - メソッド内部でも使われていなかったため、完全に参照がなくなる
- メソッドシグネチャの
# Before
def try_to_queue_for_background_connection(checkout_timeout)
# checkout_timeout は無視されており、待ち時間は無制限
end
# After
def try_to_queue_for_background_connection
# 挙動自体は変わらず、無制限で待つ
endコメントにも明記されている通り、「待ち時間は意図的に無制限」で、タイムアウト値は設計的に使わない方針です。そのため引数だけが「ダミー」のように残っていました。
ポイント:
内部メソッドですが、もしアプリやgemが ConnectionPool を継承して上書き・呼び出している場合、シグネチャ変更に注意が必要です(通常は触らない箇所)。
2-3. HashLookupTypeMap#initialize の parent 引数削除
- 対象ファイル:
activerecord/lib/active_record/type/hash_lookup_type_map.rb - 変更点:
initialize(parent = nil)→initialize(引数なし)に変更- 引数
parentはコード内で使われておらず、呼び出し側も渡していない
# Before
class HashLookupTypeMap
def initialize(parent = nil)
@mapping = {}
end
end
# After
class HashLookupTypeMap
def initialize
@mapping = {}
end
end過去には TypeMap と結びついていて親のマップを引き継ぐような設計があったものの、それがすでに解除されているため、parent は完全に不要になっていました。
ポイント:HashLookupTypeMap.new(something) のように独自に使っているコードがあるとエラーになりますが、フレームワーク内部ではそのような呼び出しは行っていないことが確認されています。
2-4. TransactionState の未使用メソッド削除
- 対象クラス:
TransactionState - 削除されたメソッド:
#fully_committed?#fully_rolledback?#fully_completed?#nullify!
- 関連ファイル:
activerecord/lib/active_record/connection_adapters/abstract_adapter.rbなど
これらのメソッドは、過去のリファクタリングで最後の呼び出し元が段階的に削除され、最終的に:
- 呼び出し元ゼロ
nullify!については「テスト自身」だけが唯一の呼び出し元
という状態になっていました。そのためメソッド本体と、nullify! をテストするユニットテストも合わせて削除されています。
TransactionState 自体が持っている状態値 :fully_committed / :fully_rolledback は、引き続き committed? / rolledback? から参照されるため、トランザクションの外部的な振る舞いは変わりません。
# イメージ
state = TransactionState.new
state.committed? # 既存の public-ish な問い合わせ
state.rolledback? # こちらも継続利用される
# fully_*? 系は完全削除ポイント:
「トランザクションが 完全に コミット/ロールバック済みか」を見る細かい問い合わせメソッドが消えた形ですが、フレームワークコードからはすでに使われておらず、公的なAPIでもなかったため、互換性影響はほぼありません。
2-5. SingularAssociation#reset の書き込み専用インスタンス変数削除
- 対象ファイル:
activerecord/lib/active_record/associations/singular_association.rb - 削除内容:
reset内で@future_targetに代入していた行を削除
@future_targetはどこからも読み出されておらず、「書き込むだけで使われない」変数でした。
def reset
@target = nil
@loaded = false
# @future_target = nil ← これが削除
endポイント:
内部状態の管理がすでに別の方式に移っており、残骸だけが残っていたパターンです。挙動は変わりません。
2-6. @_query_constraints_list への「死んだ代入」の削除
- 対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb付近?
※説明文から推測すると、クラスのinheritedフック内で@_query_constraints_listをリセットするコード行 - 状況:
- 以前はメモ用インスタンス変数名が
@_query_constraints_list - その後
@query_constraints_listにリネーム済み - しかし
inheritedフック内だけ古い名前でリセットしていた - Ruby のインスタンス変数はクラス継承では引き継がれないので、そもそもここでのリセット自体意味が薄い
- 以前はメモ用インスタンス変数名が
class SomeBase
class << self
def inherited(subclass)
super
# @_query_constraints_list = nil ← これが削除される
end
end
endポイント:
- 「存在しないインスタンス変数に nil を代入しているだけ」の状態
- サブクラスからは見えないメモであり、消しても挙動は一切変わらない
2-7. ThroughReflection#source_options / #through_options の削除
- 対象ファイル:
activerecord/lib/active_record/reflection.rb - 削除されたメソッド:
ThroughReflection#source_optionsThroughReflection#through_options
has_many :through などで使われるリフレクションの内部ヘルパでしたが、過去のリファクタリング(該当コミット: 500b1df43e / 65843e1acc)で最後の呼び出し元が削除済みで、現在は完全に未使用でした。
class ThroughReflection < AssociationReflection
# def source_options; ...; end # 削除
# def through_options; ...; end # 削除
endポイント:
独自にリフレクションを操作しているメタプログラミング系のライブラリが、このメソッドを直接呼んでいた場合は NoMethodError になります。ただし正式にドキュメント化されたAPIではなく、フレームワーク内部実装寄りのメソッドです。
2-8. PoolConfig#server_version writer の削除
- 対象ファイル:
activerecord/lib/active_record/connection_adapters/pool_config.rb - 削除内容:
server_version=のセッターが削除- テストコードはすでに
instance_variable_setで直接メモをリセットしており、セッターは使っていない
class PoolConfig
# attr_writer :server_version # これに相当するものが削除
endポイント:
- 「サーバーバージョンのメモ」を外部から書き換えるためのフックが内部的にすら使われておらず、完全に死んでいた
- DB アダプタや拡張コードが
pool_config.server_version = ...のように書いていると壊れる可能性がありますが、通常はconnection側でバージョンを扱うため、かなりレアケースと考えられます。
2-9. AbstractAdapter#build_result の削除
- 対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract_adapter.rb - 削除されたメソッド:
#build_result - 背景:
- 以前は「アダプタがカスタム result オブジェクトを構築するためのフック」としてコメント付きで用意されていた
- しかし
fd24e5bfc9の変更でアダプタがcast_resultパスに寄せられ、このフックを呼び出すフレームワークコードはなくなった - 現在
build_resultをオーバーライドしても、そもそも一度も呼ばれない
class AbstractAdapter
# def build_result(columns, rows)
# # アダプタが独自の結果オブジェクトを返すためのフック(という位置づけだった)
# end
endPR 説明では「今でもこのメソッドを extension point として残したいのであれば、このコミットは落としてもよい」と言及されていますが、最終的には削除されています。
ポイント:
- 外部アダプタが
build_resultを実装していたとしても、実際にはすでに呼ばれていなかったため、「メソッドが消える」以外の実害はありません(挙動は元から変わっていない)。 - ただし、
AbstractAdapterを継承した独自アダプタでsuper呼び出しなどをしていると NoMethodError になる可能性があります。
- 影響範囲・注意点
基本方針:
- すべて「フレームワーク内部で参照されていない」ことが確認されたコードの削除です。
- 公式に公開されたAPIではなく、主に内部クラス・メソッド・引数が対象です。
- そのため、通常のアプリケーションコードに対する影響はほぼゼロと考えられます。
注意が必要なケース(拡張・メタプログラミングをしている場合):
内部クラスに直接依存している場合
- 例:
ActiveRecord::ConnectionAdapters::AbstractAdapter#build_resultを独自アダプタでオーバーライドしていた - 例:
ThroughReflection#source_options/through_optionsを呼んでリフレクション情報を解析していた - 例:
HashLookupTypeMap.new(some_parent)のように引数付きで初期化していた
- 例:
private / internal メソッドを
sendで呼んでいた場合- PR 側でも
sendやシンボル参照まで含めて grep されているため、Rails 本体では未使用と確認されていますが、アプリやgemが同様の手法で呼び出していると壊れます。
- PR 側でも
シグネチャ変更
try_to_queue_for_background_connection(checkout_timeout)→ 引数なし
自前のサブクラス・モンキーパッチで呼び出している場合は修正が必要です。
実質的な挙動の変更:
- いずれの削除も「参照されていないコード」の削除に留まっており、Rails が発行するSQL、コネクションプールの動作、トランザクションの扱いなど、ユーザが観測可能な挙動は変わらないよう設計されています。
- 既存のテストは、
nullify!のテスト削除以外は、動作変更を伴う形で壊れていないことが前提です。
- 参考情報 (あれば)
- このPR自体は、以下の「未使用コードクリーンアップ」PR群のフォローアップとして位置づけられています:
- #58214
- #58197
- #58208
- 変更の根拠となるコミット(どの時点から未使用になったか)が詳細に示されているため、もし「このメソッドに依存していたかも」と感じた場合は、該当するコミットを辿ることで設計変更の流れを追えます。
- 依存している可能性をチェックしたい場合:
- アプリケーションおよび社内gemで
build_result,ThroughReflection#source_options,HashLookupTypeMap.new(などの文字列検索を行うと影響をざっと洗い出せます。
- アプリケーションおよび社内gemで
#58361 Fix TimeWithZone#to_s raising in a non-main Ractor
マージ日: 2026/8/8 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::TimeZoneが Ractor 共有可能になっていたにもかかわらず、非メイン Ractor でTimeWithZone#to_sなどを呼ぶとRactor::IsolationErrorが発生していた問題を修正した PRです。内部で使っているタイムゾーンオフセット用の文字列を凍結し、Ractor 間で安全に共有できるようにしています。
- 変更内容の詳細
問題の背景
- 以前のコミット
26e78a20dbにより、ActiveSupport::TimeZoneインスタンス自体はRactor.shareable?になるように対応済みでした。 - しかし
TimeWithZone#to_s,#rfc2822,to_fs(:rfc822)を非メイン Ractor 内で呼ぶとRactor::IsolationErrorが発生していました。 - 理由は、フォーマット時に使う定数
UTC_OFFSET_WITHOUT_COLONが「凍結されていないString」だったためです。frozen_string_literal: trueではカバーされないケースで、String#trのようなメソッド呼び出しで文字列オブジェクトが「書き換え可能」と見なされ、Ractor 的に共有不可能になっていました。
修正内容
activesupport/lib/active_support/values/time_zone.rbUTC_OFFSET_WITHOUT_COLONを凍結 (freeze) するように変更。- これにより、この定数はイミュータブルになり、Ractor 間で安全に共有可能になります。
activesupport/test/time_zone_test.rb上記問題が再発しないように、非メイン Ractor で
TimeWithZone#to_sなどを呼び出すテストを追加。具体的には、おおよそ以下のようなテストシナリオが追加されています(イメージ):
rubyTime.zone = "Eastern Time (US & Canada)" t = Time.zone.local(2000, 1, 1) r = Ractor.new(t) do |time| [time.to_s, time.rfc2822, time.to_fs(:rfc822)] end result = r.take # 以前はここで Ractor::IsolationError が発生していた
動作例(PR 説明より)
修正前:
Time.zone = "Eastern Time (US & Canada)"
Ractor.new(Time.zone.local(2000, 1, 1)) { |t| t.to_s }.value
# => Ractor::IsolationError修正後:
Time.zone = "Eastern Time (US & Canada)"
Ractor.new(Time.zone.local(2000, 1, 1)) { |t| t.to_s }.value
# => "2000-01-01 00:00:00 -0500"- 影響範囲・注意点
- 影響範囲
- 非メイン Ractor 内で
ActiveSupport::TimeWithZoneオブジェクトの以下メソッドを呼ぶ処理:#to_s#rfc2822#to_fs(:rfc822)(旧to_s(:rfc822)相当)
- 上記が
Ractor::IsolationErrorで落ちていたケースが正常に動作するようになります。
- 非メイン Ractor 内で
- 既存コードへの影響
- 変更は定数文字列を
freezeしただけで、フォーマット結果の文字列内容やパブリック API には変更がありません。 - マルチ Ractor を使っていないアプリケーションには挙動の変化はありません。
- 変更は定数文字列を
- 注意点
- この PR は 「TimeZone/TimeWithZone が Ractor 安全に近づいた」修正ですが、Rails の他部分が Ractor 完全対応とは限りません。
- Ractor を本番で使う場合は、他の gem / ライブラリやアプリケーションコードも含めて
Ractor.shareable?やRactor::IsolationErrorを確認する必要があります。
- 参考情報 (あれば)
- 当該 PR: https://github.com/rails/rails/pull/58361
- 関連コミット:
26e78a20db(ActiveSupport::TimeZoneを Ractor-shareable にした変更) - Ruby Ractor ドキュメント:
- https://docs.ruby-lang.org/en/master/doc/ractor_md.html
- 特に「Ractor::IsolationError」「shareable objects」の節が今回の問題の背景に関連します。
#58416 Use ActiveRecord::Key.for in remaining Array(reflection.*_key) sites
マージ日: 2026/8/8 | 作成者: @kamipo
- 概要 (1-2文で)
Rails の ActiveRecord 内で、関連(reflection)のキーを配列化して扱っていた箇所をArray(...)からActiveRecord::Key.for(...)に統一し、単一キーと複合キーを同じ Enumerable API で扱えるようにリファクタリングした PR です。機能追加ではなく、#58368 に続く内部実装の揃え・複合主キー対応を見越した整備です。
- 変更内容の詳細
何をしたか
- 以下のファイルで、reflection 由来のキー名を配列化してループしていた箇所をすべて
ActiveRecord::Key.for(...)に置き換えassociations/association.rbassociations/association_scope.rbassociations/foreign_association.rbassociations/has_one_association.rbautosave_association.rbrelation/query_methods.rb
- 行数としては +24 / -24 で、ほぼ 1:1 の API 置き換え (挙動を変えるというより手段の統一)
具体例イメージ
これまで典型的にやっていたコード:
Array(reflection.foreign_key).each do |key|
# 単一 or 複合キーを意識せずに処理したい
endを、以下のように変更:
ActiveRecord::Key.for(reflection.foreign_key).each do |key|
# 同上
end同様に、primary_key, association_primary_key, join_primary_key など、reflection 周りでキーを列挙していた部分が同じ方針で ActiveRecord::Key.for に統一されています。
ActiveRecord::Key.for の意図 (推測を含む整理)
- 目的:
- 「単一キー」も「複合キー」も “Enumerable なキー集合” として同じ API で扱うためのヘルパ
- 期待される振る舞いのイメージ:
- 引数が単一キー文字列/シンボルなら、1 要素のコレクションとして扱えるようにする
- 引数が配列や複数キーを表すオブジェクトなら、そのまま複数要素として列挙
- 将来的に複合主キーや複合外部キーをちゃんとサポートする際の統一インターフェースになる
従来は Array(...) で「とりあえず配列にする」程度だったのに対して、ActiveRecord::Key.for は「ActiveRecord が期待するキー表現」に正規化する責務を持つクラスメソッドになっていると考えられます。
- 影響範囲・注意点
影響範囲
- ActiveRecord 内部の以下の処理で「単一 / 複合キーの扱い」が
ActiveRecord::Key.forベースに統一されます:- 各種 association の関連づけ・ロード処理
- association scope の生成
belongs_to/has_oneなどの外部キー・主キーの突き合わせ- autosave (関連オブジェクトの自動保存) の関連付け・キー処理
Relation#where,#eager_loadなどで reflection を利用して join / 条件を組み立てる部分の一部
外部から見た挙動への影響
- 公開 API としての挙動は基本的に変わらない想定です:
- 通常の単一主キー/外部キーを使うアプリでは、動作の変化はほぼ感じないはず
- ただし、以下に関わるようなアプリ/ライブラリは注意:
- ActiveRecord の内部 API (
reflection.foreign_keyなど) を直接触っている gem - association や autosave の内部動作に monkey patch を当てているコード
- 独自の複合キー拡張 (e.g.
composite_primary_keys的なもの) と組み合わせている場合
- ActiveRecord の内部 API (
- これらのケースでは:
Array(reflection.some_key)を前提にしたコードがある場合、
今後はActiveRecord::Key.for(reflection.some_key)を使うほうが
Rails 本体と挙動を合わせやすくなります。
バージョンアップ時のテスト観点
- Rails アップグレード時に追加で見ておくと良いポイント:
- 複合キー or カスタムな primary_key / foreign_key を指定している association が正しく
- find / build / create できるか
- eager_load / includes / joins で問題なくロードされるか
- autosave (
accepts_nested_attributes_forなど) で関連が正しく保存されるか
- gem の monkey patch と競合していないか (NoMethodError や挙動変化が出ていないか)
- 複合キー or カスタムな primary_key / foreign_key を指定している association が正しく
- 参考情報 (あれば)
- 対応元 PR: #58368
ActiveRecord::Key導入および、一部 call site をそちらに切り替えた最初の PR と考えられます。
- 本 PR #58416 は、そのフォローアップとして、
残っていたArray(reflection.*_key)呼び出しをすべてActiveRecord::Key.forに寄せる「仕上げ」のリファクタリングです。 - 将来の「正式な複合キーサポート」や、キー周りの内部表現の整理に向けた準備として読むと理解しやすい変更です。
#58396 Eager load templates
マージ日: 2026/8/8 | 作成者: @etiennebarrie
- 概要 (1-2文で)
このPRは、Rails起動時にテンプレートを「先読み(eager load)」して共有可能にする仕組みを Action View に導入したものです。まずは strict locals なテンプレートのみを対象とし、テンプレートのコンパイル先コンテナの不整合を明示的に検出する安全チェックも追加されています。
- 変更内容の詳細
2-1. 起動時にテンプレートを eager load できるように
- Action View のテンプレートを「起動時にロードしてコンパイルしておく」ための経路が追加されています。
railties/lib/rails/application.rbに変更が入っているため、config.eager_load = trueなどのタイミングで、通常のクラス/モジュールの eager load と同様にテンプレートもロード・コンパイルされるフックが増えた形です。- 対象は strict locals テンプレートのみ。
- strict locals とは、テンプレート側で受け取るローカル変数とその型(あるいは存在)を明確に定義し、呼び出し時に宣言と違う locals を渡すとエラーになるようなモードです。
- このモードではテンプレートのコンパイル結果を 1つの
Templateオブジェクトにまとめやすく、Ractor 間で共有可能な形にしやすい、という前提があります。
ポイント:
- これまでは
ActionView::UnboundTemplate.@templatesにテンプレートごとのキャッシュがぶら下がる形だったのを、strict locals の場合は単一のTemplateオブジェクトに折りたたんで扱えるようにした(=共有しやすくした)という意図です。 - 将来的には non-strict なテンプレートについても、Ractor ローカルキャッシュとしてコンパイルする follow-up が予定されています(PR説明中の Shopify/rails ブランチへのリンク参照)。
2-2. UnboundTemplate / Template 周りのキャッシュ構造変更
変更ファイル:
actionview/lib/action_view/template.rbactionview/lib/action_view/unbound_template.rbactionview/lib/action_view/template/resolver.rbactionview/lib/action_view/template/sources.rb
主なポイント:
strict locals テンプレートのコンパイルキャッシュの扱いを改善
UnboundTemplateが保持していた@templatesキャッシュを strict locals では 1つのTemplateオブジェクトに集約し、Ractor 間で共有しやすい形にしています。- これにより、Ractor 化された環境(
ractorize!有効時など)でのテンプレート再コンパイルやメモリ・CPU コストを抑えられるようになります。
resolver に eager load 経路を追加
ActionView::Template::Resolverに、テンプレートを事前にロード・コンパイルするためのメソッドやフローが追加されています。- ファイルシステムベースの resolver (
file_system_resolver_test.rbが増えている) を使って、起動時にディスク上のテンプレートファイルを走査し、コンパイル済みテンプレートをキャッシュしておくことができるようになります。
Template / sources 周りの補助的な API 追加
ActionView::TemplateやActionView::Template::Sourcesに、eager load と Ractor 共有前提の使い方をサポートする小さなメソッド・フラグが追加されています(例えばソースの frozen 管理や、コンパイル済みメソッドの保持の仕方など)。
2-3. コンパイル先コンテナの安全チェック追加
PR説明にある通り、テンプレートコンパイル時の「メソッドコンテナ」の不整合を検出する安全チェックが新しく入りました。
背景:
- Action View テンプレートは、最終的には Ruby のメソッドとして「どこかのクラス/モジュール」に定義されます。
- そのコンテナは通常、
view_context_class.compiled_method_containerです。 - テンプレートは「最初にレンダリングされたときの view のコンテナ」に対してコンパイルされます。
- 例: 最初に
ApplicationControllerの view context からレンダリング → そのview_context_class.compiled_method_containerにメソッドが定義される。
- 例: 最初に
問題だった点:
- 後から、別のコンテナを持つ view context で同じテンプレートをレンダリングしようとすると、そこにはコンパイル済みメソッドが存在しないため
NoMethodErrorが発生していました。 - しかし従来はこの
NoMethodErrorはかなり分かりづらく、「なぜメソッドが無いのか」が明示されていませんでした。
今回の変更:
- テンプレートがコンパイルされたコンテナと、現在レンダリングに使おうとしている view context のコンテナが異なる場合、
- 明示的なエラーを投げるようになりました(
NoMethodErrorではなく、「コンテナが違う」という趣旨の例外)。
- 明示的なエラーを投げるようになりました(
- これにより、「
view_context_classをオーバーライドして別のコンテナを持つクラスを使っている」などのケースで、問題の原因がすぐ分かるようになります。
具体例になりやすい状況:
class ApplicationController < ActionController::Base
end
class ApplicationMailer < ActionMailer::Base
end
# 通常は両者の view_context_class.compiled_method_container は equal? になる
ApplicationController.view_context_class.compiled_method_container.equal?(
ApplicationMailer.view_context_class.compiled_method_container
) #=> true (典型的なケース)しかし、例えばあるコントローラで:
class CustomViewContext < ActionView::Base
# compiled_method_container が異なるような実装
end
class Admin::DashboardController < ApplicationController
def view_context_class
CustomViewContext
end
endのように、異なるコンテナを持つクラスを返していると、
テンプレートが最初にコンパイルされたコンテナと異なるコンテナで再利用される可能性があり、その場合に今回の安全チェックが働きます。
2-4. Ractor 対応まわりのテスト追加
railties/test/application/ractors_test.rbにテストが追加され、Ractor 化 (ractorize!) されたアプリケーションでテンプレートの eager load / 共有が期待通りに動作するか確認しています。- strict locals テンプレートが Ractor セーフにコンパイルされ、Ractor 間で使い回せることを担保する意図があります。
影響範囲・注意点
strict locals テンプレートを使っているアプリ
- 起動時にテンプレートを eager load する設定を有効にすると、strict locals テンプレートはコンパイル済み状態でアプリ起動後すぐ利用されます。
- メリット:
- 初回アクセス時のコンパイルオーバーヘッドが減る。
- Ractor 使用時のパフォーマンス・メモリ効率、および安定性が向上。
- テンプレートの定義ミス(locals の宣言と実際に渡している値の不整合など)は、起動時に早めに検知される可能性が高まります。
view_context_classをカスタマイズしているアプリ/エンジンview_context_classをオーバーライドしており、そのcompiled_method_containerがApplicationController/ApplicationMailerなどデフォルトとは異なる場合、新しい安全チェックにより、これまで黙ってNoMethodErrorになっていた箇所が、より明示的な例外に変わります。- もしこのエラーが発生した場合は:
view_context_classを見直し、テンプレートを共有したいクラス間で同じcompiled_method_containerを使うようにする。- あるいは、コンテナ違いを前提にテンプレートを別々にコンパイルする設計にする(多くの場合は前者が望ましい)ことを検討してください。
Ractor を有効にしている環境 (
ractorize!)- 今回の変更で、strict locals テンプレートが共有しやすい形に整理されたため、Ractor 間のテンプレート共有が現実的になります。
- ただし non-strict テンプレートはまだ follow-up PR での対応予定であり、現時点では Ractor ローカルキャッシュでの最適化は strict locals のみが対象です。
互換性
- 基本的には後方互換を維持しつつ、安全性とパフォーマンスを高める変更です。
- 互換性上の変化は「これまで曖昧な
NoMethodErrorが出ていたケースで、より明示的な例外が出るようになった」点にほぼ限られます。
- 参考情報 (あれば)
- PR 本体:
https://github.com/rails/rails/pull/58396 - non-strict テンプレート対応の follow-up(Shopify フォーク):
https://github.com/Shopify/rails/tree/actionview-frozen-non-strict-templates - Ractor と Rails に関するテスト類(参考):
railties/test/application/ractors_test.rbに今回の変更と関連するテストが追加されています。
#58415 Remove symbol_column_to_string in favor of columns_hash + Symbol#name
マージ日: 2026/8/8 | 作成者: @kamipo
- 概要 (1-2文で)
symbol_column_to_stringというヘルパーとそのキャッシュを削除し、代わりにcolumns_hash(クラスレベルでメモ化されたハッシュ)と Ruby 3.0 以降のSymbol#nameを直接使うようにする変更です。これにより、シンボルを使ったカラム参照で追加のキャッシュ構造が不要になり、コードが簡潔かつ同等のパフォーマンスを維持します。
- 変更内容の詳細
背景
以前のPR (#34197) で、
respond_to?にシンボルを渡した際に**文字列オブジェクトを新規生成しない(allocation-free)**ようにするため、以下のような仕組みが導入されていました:- カラム名シンボル
:titleなどをキーに、対応する文字列"title"を値に持つ{ sym => str }のハッシュをメモ化(symbol_column_to_string) - これにより
respond_to?(:title)等のたびに文字列を生成せずに済む
- カラム名シンボル
一方で Active Record には既に
columns_hashという、{"title" => #<ActiveRecord::ConnectionAdapters::Column ...>}
という形の凍結済み・クラスレベルでメモ化されたハッシュが存在しており、これ自体が O(1) でのカラム検索を提供します。Ruby 3.0 以降では
Symbol#nameによって、新しい String を生成せずに内部的にインターンされた文字列を取得できます("title"に相当する文字列表現を割り当てなしで得られる)。
このPRでの主な変更点
symbol_column_to_stringヘルパー削除activerecord/lib/active_record/model_schema.rbからsymbol_column_to_stringの定義とそれに関連するキャッシュスロットのコードが削除されています(-8行)。- これにより「
{sym => str}キャッシュ」を使った独自の変換ロジックは無くなります。
attributes/respond_to?系の内部処理をSymbol#name+columns_hashに統一activerecord/lib/active_record/attribute_methods.rbの該当箇所で、シンボル引数を処理する際にsymbol_column_to_stringを呼び出さず、直接sym.nameを使うような形に書き換えられています(+2/-3)。典型的には以下のようなイメージに近い形になります(概念的なサンプル・実際のコードとは多少異なる可能性があります):
ruby# 変更前(イメージ) def has_attribute?(attr_name) attr_name = symbol_column_to_string(attr_name) if attr_name.is_a?(Symbol) columns_hash.key?(attr_name) end # 変更後(イメージ) def has_attribute?(attr_name) attr_name = attr_name.name if attr_name.is_a?(Symbol) # Ruby 3.0+: allocation-free columns_hash.key?(attr_name) endcolumns_hash自体はクラスレベルでメモ化されており、これを直接使うことで、以前の{sym => str}キャッシュと同等の O(1) ルックアップと allocation-free な変換を実現します。
- 影響範囲・注意点
パフォーマンス / メモリ
- ねらい
- 追加の
{Symbol => String}キャッシュを廃止することで、クラスごとのキャッシュスロットが一つ減り、内部実装がシンプルになります。 Symbol#nameは Ruby 3.0 以降で allocation-free なため、以前の最適化(symbol_column_to_string)と同等の性能が期待できます。
- 追加の
respond_to?やhas_attribute?等で、シンボルを渡した場合のカラム名解決は従来どおり高速で、余分な String オブジェクトも生成しません。- 非常に大量のカラムや大量のモデルを扱うアプリケーションであっても、実質的には性能的な劣化はなく、場合によってはメモリ断片の削減が期待できます。
互換性
symbol_column_to_stringは内部実装向けのプライベートなヘルパーであり、通常のアプリケーションコードが直接呼び出すものではありません。- そのため、通常のアプリケーションでは後方互換性の問題はほぼありません。
- もし gem / プラグイン等が
symbol_column_to_stringを直接参照していた場合は、Symbol#nameとcolumns_hashを使った実装に書き換える必要があります。- 例:
symbol_column_to_string(:title)→:title.nameもしくはcolumns_hash.key?(:title.name)など。
- 例:
Ruby バージョン
- この変更は Ruby 3.0 以降の
Symbol#nameを前提にしています。 - Rails 本体として Ruby 3.0 以上をサポート前提として進めているバージョンであることが前提となる変更です(古い Ruby をサポートする独自フォークなどでは注意が必要)。
- 参考情報 (あれば)
- 本PR: #58415
タイトル: Removesymbol_column_to_stringin favor ofcolumns_hash+Symbol#name - 関連PR: #34197
respond_to?のシンボル入力を allocation-free にするためにsymbol_column_to_stringが導入された元の変更。
- 関連API:
ActiveRecord::Base.columns_hash{String => ActiveRecord::ConnectionAdapters::Column}形式の、凍結済みメモ化ハッシュ
- Ruby 3.0+:
Symbol#name- シンボルの文字列表現を返すメソッドで、
to_sと違い新しい文字列を割り当てない実装になっている(allocation-free)。
- シンボルの文字列表現を返すメソッドで、
#58404 Fix the Request#media_type documentation example
マージ日: 2026/8/7 | 作成者: @55728
- 概要 (1-2文で)
ActionDispatch::Request#media_typeのドキュメント内サンプルが、実際の挙動と一致していなかったため、正しいリクエスト例(POST+ボディ付き)に修正したドキュメント専用のPRです。コードの挙動自体は一切変更されていません。
- 変更内容の詳細
何が問題だったか
元のドキュメントでは、次のように GET リクエストの例が書かれていました:
# get "/articles"
request.media_type # => "application/x-www-form-urlencoded" # actually nilしかし、通常の GET リクエストはボディを持たず、Content-Type ヘッダも設定されないため、実際には request.media_type は nil になります。
テスト (test/dispatch/request_test.rb:1109) でも「Content-Type がないリクエストでは media_type は nil である」と明示的に検証されており、ドキュメントだけが古い挙動を前提にしたまま残っていました。
元々は、統合テストのヘルパが常に Content-Type: application/x-www-form-urlencoded を付けており、その時代にはサンプルどおりの挙動でしたが、コミット 86754a8f7b によって「デフォルトのリクエストエンコーダが Content-Type を付けない」ように変わったため、ドキュメントと実挙動が乖離していました。
どう修正されたか
本文付きの POST リクエストの例に差し替えられました:
# post "/articles", params: { title: "Rails" }
request.media_type # => "application/x-www-form-urlencoded" # verifiedこれにより:
- フォームエンコードされたパラメータ (
params) を持つPOSTリクエストでは Content-Type: application/x-www-form-urlencodedが付与されrequest.media_typeが"application/x-www-form-urlencoded"を返す
という、実際の挙動と一致したサンプルになっています。
変更は actionpack/lib/action_dispatch/http/request.rb のコメント中の1行のみで、コードロジックには一切手が入っていません。
- 影響範囲・注意点
- 挙動変更は一切なく、「ドキュメントのサンプルが正しくなった」だけです。
- これにより、以下の点が明確になります:
GETリクエスト(かつContent-Typeなし)ではrequest.media_typeはnilを返す。media_typeに値が欲しい場合は、ボディとContent-Typeを伴うリクエスト(例: フォームPOST、JSON POSTなど)である必要がある。
- 既存アプリケーションやテストコードを変更する必要はありませんが、
- 「GET でもデフォルトで
application/x-www-form-urlencodedが入るはず」と思い込んでいた場合は、その前提が誤りであることを認識する必要があります。
- 「GET でもデフォルトで
- 参考情報 (あれば)
- 該当メソッド:
ActionDispatch::Request#media_type- 実際には
Content-Typeヘッダから MIME タイプ部分だけを取り出した値("application/json"など)を返し、ヘッダがない場合はnilを返します。
- 実際には
- 関連テスト:
test/dispatch/request_test.rb:1109- 「Content-Type のないリクエストでは
request.media_typeがnilである」ことを検証。
- 「Content-Type のないリクエストでは
- 挙動が変わった元コミット(言及されているもの):
86754a8f7b- デフォルトのリクエストエンコーダが Content-Type を付けなくなったことにより、古いドキュメント例が不正確になっていた経緯があります。
#58384 Allow config.active_storage.variant_processor to be set to a class
マージ日: 2026/8/7 | 作成者: @flavorjones
- 概要 (1-2文で)
Active Storage のconfig.active_storage.variant_processorに、これまでの:vips/:mini_magick/:disabledというシンボルだけでなく、任意のクラスを直接指定できるようにした PR です。これにより、独自の画像変換パイプラインやサンドボックス化された変換処理などを、Rails の設定レベルで差し替えられるようになります。
- 変更内容の詳細
2-1. これまでの問題点
Active Storage では、添付ファイルに対して以下の3フェーズがあります。
- analysis(メタ情報抽出):
config.active_storage.analyzersでクラス配列として差し替え可能 - preview generation(プレビュー生成):
config.active_storage.previewersでクラス配列として差し替え可能 - variant processing(画像のリサイズ・トリミングなどの変換):
ここだけconfig.active_storage.variant_processorが:vips/:mini_magick/:disabledの3つに固定されており、クラスを直接渡せなかった
内部的には、ActiveStorage::Engine で次のようにシンボルをクラスにマッピングしていました:
ActiveStorage.variant_transformer =
case ActiveStorage.variant_processor
when :disabled
ActiveStorage::Transformers::NullTransformer
when :vips
ActiveStorage::Transformers::Vips
when :mini_magick
ActiveStorage::Transformers::ImageMagick
endただし ActiveStorage.variant_transformer はドキュメント化されていない mattr_accessor であり、アプリケーションから正式には利用できないため、「variant 処理だけ差し替えたい」というニーズに応えにくい状態でした。
2-2. 新しい挙動:クラスを直接指定可能に
config.active_storage.variant_processor に クラスを直接指定 できるようになりました。
# 例: カスタムのトランスフォーマークラスを使用
config.active_storage.variant_processor = CustomTransformer- これまで通り、以下のシンボルも有効です:
:vips:mini_magick:disabled
- 上記シンボルが指定された場合は、従来通り
ActiveStorage::Transformers::Vipsなどのビルトインのクラスにマッピングされます。 - クラスが指定された場合は、そのクラスがそのまま variant 処理に利用されます。
実装上の契約(インターフェイス)
指定するクラスは、ActiveStorage::Transformers::Transformer が定めるインターフェイスを実装している必要があります。
- ただし、継承は必須ではない:
ActiveStorage::Transformers::Transformerを継承しなくてもよい- 必要なメソッドを実装していれば動作する(ダックタイピング)
- 型チェックなどは行わず、「呼び出せるかどうか」に依存しているため、間違ったインターフェイスで実装すると実行時エラーになります。
transformer.rb にも多少手が入っており、インターフェイスの役割がより明確化されています(実装契約を示す抽象層としての位置付け)。
2-3. 設定値が不正な場合のエラーが早期化
config.active_storage.variant_processor に不正な値を渡した場合の挙動が変わりました。
- 以前: 不正な値を設定しても起動時には気づかず、実際に variant を生成したタイミングで
NoMethodErrorが発生しがちだった - 今回の変更後: アプリケーション起動時に
ArgumentErrorを送出 するようになり、早い段階で誤設定に気づけるようになりました
これにより、本番で初めて画像変換を通したときに落ちる、のような事故を防ぎやすくなります。
2-4. ドキュメント・テストの更新
guides/source/configuring.mdconfig.active_storage.variant_processorの説明に「クラスを渡せる」ことと、その前提条件(必要なインターフェイス)が追記されています。
railties/test/application/active_storage/custom_processors_integration_test.rb- カスタムのプロセッサークラスを指定した場合の統合テストが追加され、実際に画像変換フローを通して検証されています。
railties/test/application/active_storage/engine_integration_test.rb- 不正な設定値など、起動時のバリデーション挙動を確認するテストが追加。
activestorage/CHANGELOG.mdにも、挙動変更・新機能として記載あり。
- 影響範囲・注意点
3-1. 互換性・マイグレーション
- 既に
:vips/:mini_magick/:disabledを使っているアプリは、そのまま動作します。 ActiveStorage.variant_transformerを直接いじっていたような非公式な使い方から、config.active_storage.variant_processor = CustomTransformerに移行するのが推奨されます。
3-2. カスタムクラス利用時の重要な注意点
「variant_processor にカスタムクラスを指定するときは、analyzers もカスタムにする必要がある(現時点では)」
- ビルトインの image analyzer は、
variant_processorが:vipsまたは:mini_magickのときだけ blob を受け付ける設計になっています。 - そのため、rubyのようにカスタムクラスを指定した場合は、
config.active_storage.variant_processor = CustomTransformerconfig.active_storage.analyzersにも対応するカスタム analyzer を追加する必要があります。 - analyzer 側でどのようなメタ情報を想定するか(transformer が必要とする情報の収集等)を設計しておく必要があります。
3-3. セキュリティ面の注意(変換パラメータのバリデーション)
現在、変換パラメータのホワイトリストは以下のようにばらついています。
ActiveStorage::Transformers::ImageMagick:- 変換名・引数の allowlist を内部に持ち、ある程度のバリデーションを行っている
ActiveStorage::Transformers::Vips:- この allowlist を適用していない
この PR 自体は、「variant_processor をクラスにできる」という抽象化だけを追加しており、ホワイトリストの共通化・移動などは行っていません。
PR 作成者は、将来的には analyzer/transformer/previewer 各クラスにバリデーションを持たせるべき とコメントしており、セキュリティポリシーをどうするかは各 transformer 実装側の責任になります。
カスタム transformer を実装する場合:
- 外部から渡される transformation オプション(例:
resize_to_limit: [100, 100]など)のバリデーションを必ず自前で実装するべきです。 - ImageMagick の wrapper を自作する場合は、
ActiveStorage::Transformers::ImageMagickの allowlist 実装を参考に、コマンドインジェクション等を防ぐためのロジックを取り込むとよいです。
3-4. エラー検知タイミングの変更による副作用
- 今回、認識されない
variant_processorの値に対しては 起動時にArgumentErrorを投げるように変更されています。 - 設定値を動的に変えていたり(例えば ENV 経由で)、テストで異常系を検証している場合は、挙動が「実行時エラー → 起動時エラー」に変わっている点に注意が必要です。
- 参考情報 (あれば)
- 対象 PR: Allow
config.active_storage.variant_processorto be set to a class #58384 - 変更された主なファイル:
activestorage/app/models/active_storage/variant.rbactivestorage/lib/active_storage/engine.rbactivestorage/lib/active_storage/transformers/transformer.rbguides/source/configuring.mdrailties/test/application/active_storage/custom_processors_integration_test.rb
- 実装時のポイント:
- 「抽象レイヤー(インターフェイス)を整えた」だけであり、新たな transformer 実装自体はこの PR には含まれていません。
- 実際のユースケースとしては、サンドボックス化・リモート変換サービス連携・GPU ベースの変換クラスなどを
CustomTransformerとして切り出し、variant_processorで差し替える、という使い方が想定されています。
#58399 Active Record ivar ractor safety
マージ日: 2026/8/7 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Active Record 内で一部のインスタンス変数が Ractor(マルチスレッド並列実行用の仕組み)上で安全に参照できない問題を解消し、Ractor でも Active Record を扱えるようにするための「Ractor セーフティ」対応 PR です。特に、メモ化済みのインスタンス変数を freeze したり、必要に応じて main Ractor に処理を委譲する形に変更しています。
- 変更内容の詳細
背景・問題点
- Ractor 間でオブジェクトを共有するには「共有可能(shareable)」である必要があり、その代表的な条件が「freeze されていること」です。
- Active Record 内では、パフォーマンスのためにいくつかの値をインスタンス変数にメモ化していますが、これらが非 frozen のまま保持されていると、main Ractor 以外でアクセスしたときに Ractor セーフではなくなります。
- また、メモ化が eagerly(早期)に実行されると、そのタイミングの Ractor に依存したままになり、他 Ractor からのアクセスで問題が生じ得ます。
この PR は、そういったメモ化インスタンス変数の扱いを見直し、
- 代入時に freeze する
- 場合によっては main Ractor で初期化したものを共有するようにする
ことで、Ractor セーフティを確保しています。
対象となったインスタンス変数
説明文ベースで、次のインスタンス変数の扱いが変更されています。
@symbol_column_to_string_name_hash- シンボルカラム名と文字列カラム名の対応をキャッシュしているハッシュと思われます。
- 代入時に値を freeze し、Ractor 間で共有しても問題ない immutable な状態にします。
@_returning_columns_for_insert- INSERT 時に RETURNING 句で返すカラム情報をキャッシュしていると思われます。
- これも代入時に freeze されるようになり、Ractor 上から参照可能になります。
@query_constraints_list- モデルに対する制約(デフォルトスコープ・シングルテーブル継承等からくる種々の制約)をキャッシュした配列/構造体である可能性が高いです。
- 代入時に freeze されるので、後から破壊的変更をしない前提で Ractor セーフに。
@table_name- モデルのテーブル名のキャッシュ。
- Ractor 間で共有される前提なので、ここも freeze されます。
これらはいずれも「一度計算したら以降は変更されないことが前提の値」であり、freeze によってその前提を Ruby レベルで明示しつつ、Ractor の shareable 要件も満たす形になっています。
Arel / PredicateBuilder 周り
説明文中で、次の 2 つのインスタンス変数についても対応が入ったとされています。
@predicate_builder(型:ActiveRecord::PredicateBuilder)@arel_table(型:Arel::Table)
変更点のポイント:
Arel::Table の freeze 対応
activerecord/lib/arel/table.rbが変更され、Arel::Tableインスタンスが freeze された状態でも安全に扱えるようになっています(イミュータブル想定を強化した形)。- Ractor 間で
@arel_tableを共有するには、このオブジェクト自体が shareable である必要があり、そのために freeze 可能な設計に近づけています。
PredicateBuilder の freeze 対応
activerecord/lib/active_record/relation/predicate_builder.rbが変更され、ActiveRecord::PredicateBuilderも freeze されても問題なく使えるように手当てされています。- メモ化された
@predicate_builderを freeze して Ractor 間共有に耐えられるようにしたものと考えられます。
メモ化の「lazy 化」と main Ractor へのディスパッチ
- 説明には「メモ化パターンも lazy にする必要があるため、そのケースでは main Ractor への dispatch を追加した」とあります。
- すなわち、
- main Ractor で初回計算 & freeze → shareable なオブジェクトとして確定
- 他 Ractor からは、その既に shareable なオブジェクトを参照するだけ
- というフローにすることで、
- 非 main Ractor 上で新たに non-shareable なオブジェクトが生成・代入される
- という事態を防いでいます。
- 実装的には、インスタンス変数がまだ未設定で、かつ現在の Ractor が main ではない場合に、main Ractor に「その ivar の初期化を依頼して結果を返してもらう」ようなディスパッチ処理が入っていると推測されます。
テストの追加・変更
次のテストファイルに変更が入っています。
activerecord/test/cases/arel/table_test.rbactiverecord/test/cases/base_test.rbactiverecord/test/cases/primary_keys_test.rbactiverecord/test/cases/relation/predicate_builder_test.rbactiverecord/test/cases/arel/helper.rb
主に以下を確認するテストが追加されていると考えられます。
Arel::TableやActiveRecord::PredicateBuilderが freeze された状態で正しく動作すること- 対象のインスタンス変数が freeze されていること(
frozen?の確認) - Ractor 環境(もしくはそれを模した状況)で例外が発生しないこと
- 影響範囲・注意点
影響範囲
- Active Record の内部キャッシュ・メモ化周り(テーブル名、クエリ制約、Arel 関連オブジェクトなど)。
- Ractor を利用している/利用しようとしている Rails アプリケーションにポジティブな影響があります(Ractor 上で Active Record を使いやすくなる)。
注意点(アプリケーション側)
freeze 前提の設計になる
@table_nameや@arel_tableなど、Active Record 内部のインスタンス変数にアプリケーション側から直接手を入れている場合(Monkey patch やメタプログラミングで上書きしているなど)、- freeze による
FrozenError - Ractor shareable 制約
に引っかかる可能性があります。
通常の使い方(Active Record の公開 API のみ使用)であれば問題は出ない想定です。
- freeze による
破壊的変更を前提にしないこと
- Arel オブジェクトや PredicateBuilder の内部構造へ直接アクセスし、破壊的変更(
<<,push,merge!,[]=など)を行っているようなコードは、freeze 後にエラーになります。 - そのようなコードがある場合は、
- 新しいオブジェクトを生成して返す
- あるいは Ractor 非対応前提のままにする
といった見直しが必要です。
- Arel オブジェクトや PredicateBuilder の内部構造へ直接アクセスし、破壊的変更(
Ractor を使う場合の前提整理
- この PR は「Active Record の特定のメモ化 ivar を Ractor セーフに近づけた」ものであり、Active Record 全体が完全に Ractor 対応済みになったことを意味するわけではありません。
- Ractor で Rails / Active Record を本格的に使う場合は、他の部分(Logger、Cache、Config オブジェクトなど)についても shareable / thread-safe かどうかを検証する必要があります。
- 参考情報 (あれば)
- Ruby Ractor 概要:
- Ractor 関連の Ruby doc: https://docs.ruby-lang.org/en/master/doc/ractor_md.html
- Ractor 間で共有できるオブジェクトの条件として「freeze されていること」が重要です。
- Arel / PredicateBuilder の役割:
Arel::Table: テーブルを表すオブジェクトで、カラムや SQL AST を組み立てる基盤。ActiveRecord::PredicateBuilder: where 句などの条件をArelノードへ変換するためのビルダ。
これらが freeze 可能になったことで、「クエリビルド用の構造をイミュータブルにして共有する」という設計に近づいています。
この PR 自体は主に内部実装の改善ですが、将来的な Rails の Ractor 対応やマルチコア並列実行の基盤整備として重要な変更です。
#58359 Allow to configure ERB options through ActionView::Base
マージ日: 2026/8/7 | 作成者: @Edouard-chin
- 概要 (1–2文で)
Rails の ERB テンプレートハンドラの各種オプションを、ActionView::Base(およびconfig.action_view.*)経由で公式に設定できるようにした PR です。これにより、これまで非公開扱い(:nodoc:)となったActionView::Template::Handlers::ERBクラスを直接参照して行っていた設定を、公開インターフェイスから行えるようになります。
- 変更内容の詳細
背景
- 過去のコミット
970bf380fe8でActionView::Template::Handlers::ERBに:nodoc:が付与され、ドキュメント上「非公開 API」と明示された。 - しかし一部アプリケーションでは、このクラスを直接参照して ERB の各種オプション(例: trim mode 以外の設定)を行っていた。
erb_trim_modeは既にconfig.action_view.erb_trim_modeなどから設定できたが、それ以外の ERB ハンドラのオプションは公式な経路がなかった。
この PR は「ERB ハンドラの設定は ActionView::Base/config.action_view から全部できるようにする」という方針の整理です。
具体的な変更点
ActionView::Baseに ERB オプション用の設定を追加ActionView::Baseに、ERB ハンドラが利用する全オプションを委譲するための設定項目が追加・整理されています。
すでに存在していたerb_trim_modeに加え、同様のスタイルで他のオプションも定義されました。典型的な利用イメージ(Rails アプリ側):
ruby# config/application.rb または各環境の設定ファイルで module MyApp class Application < Rails::Application # すでに可能だった設定 config.action_view.erb_trim_mode = '-' # 今回の PR でサポート拡張されたイメージ # (実際のオプション名は ERB ハンドラ内のものに対応) config.action_view.erb_variant = :html config.action_view.erb_escaper = MyCustomEscaper # など、ERB ハンドラ側にあるオプションを一通り ActionView 経由で設定できる end end実際には、
ActionView::Baseのクラス属性 (class_attribute) もしくは設定アクセサが増え、それをActionView::Template::Handlers::ERBに伝搬させる形になっています。ActionView::Railtieの設定連携の見直しactionview/lib/action_view/railtie.rbで、config.action_viewとActionView::Baseの設定同期部分が 1 行修正されています。- これにより、
config.action_view.<erb関連の設定>に値を入れると、自動的にActionView::Baseに反映され、その結果 ERB ハンドラのオプションに正しく渡るようになります。
テストの追加
actionview/test/actionpack/abstract/abstract_controller_test.rbに 56 行分のテストが追加。ActionView::Baseを通して設定した ERB オプションが実際のテンプレートレンダリングに反映されるかを検証。
railties/test/application/configuration_test.rbも調整され、config.action_view→ActionView::Base→ ERB ハンドラ、という設定ルートが期待通り動作することを確認。
ドキュメントと CHANGELOG の更新
guides/source/configuring.mdに、ERB 関連の設定をconfig.action_viewから行う方法が追記。actionview/CHANGELOG.mdに、この機能追加が明示的に記載され、外部向けに「ERB オプションを ActionView 経由で設定できるようになった」ことが周知されています。
- 影響範囲・注意点
推奨される使用方法の変化
- これまで
ActionView::Template::Handlers::ERBクラスを直接参照していたアプリは、今後はActionView::Base/config.action_viewを経由して設定するのが推奨ルートになります。 :nodoc:が付いているクラスは、今後の変更で互換性が壊れる可能性が高いため、依存は避けるべきです。
- これまで
既存アプリへの互換性
- 既に使われていた
config.action_view.erb_trim_modeはそのまま動作します。 - 新しく追加された設定項目は、デフォルトでは従来の挙動と同じになるように実装されているのが通常で、この PR だけで既存アプリの挙動が変わる可能性は低いです。
- ただし、アプリ側で
ActionView::Template::Handlers::ERBを直接いじっていた場合は、今後の Rails アップデートに備えて設定方法を移行しておくと安全です。
- 既に使われていた
設定箇所の一元化
- ERB 関連の設定が
ActionView::Baseおよびconfig.action_viewに集約されることで、ビュー設定の見通しが良くなり、環境ごとに ERB の挙動(トリムモード・エスケープ戦略など)を切り替えるのが容易になります。
- ERB 関連の設定が
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58359
- 関連コミット:
970bf380fe8(ActionView::Template::Handlers::ERBに:nodoc:が付いた変更) - 設定ガイド(本 PR で更新):
guides/source/configuring.mdconfig.action_viewセクション内に ERB 関連オプションの説明が追加されているので、具体的なオプション名と使い方はそこを参照するのが確実です。
#58060 Ractor compatible Active Support Notifications
マージ日: 2026/8/7 | 作成者: @Edouard-chin
- 概要 (1-2文で)
ActiveSupport::Notifications を Ractor 内でも安全に利用できるようにするため、Ractor ごとに独立した Notifier を持つ仕組みに変更した PRです。既存のAS::Fanoutを直接 Ractor-safe にするのではなく、「購読情報のスナップショット」をもとに Ractor 単位で Notifier を生成する設計になっています。
- 変更内容の詳細
背景と課題
- これまで
ActiveSupport::Notificationsは Ractor 内から安全に使えませんでした。 - 内部で使っている
ActiveSupport::Notifications::Fanoutが- 購読者リストをキャッシュする
- 通知名(例:
"sql.activerecord","foo.bar")に応じて動的にキャッシュが増える
という性質を持ち、これが Ractor 間で共有されるとスレッドセーフ / Ractor セーフにするのがほぼ不可能、というのが問題です。
方針
- 「1つのグローバル Fanout を Ractor-safe にする」のではなく、
- Ractor ごとに独立した Notifier を持つ
- その Notifier は「メイン Ractor で管理されている購読情報のスナップショット」から初期化される
- これにより、「Ractor 間で共有される可変オブジェクト」を避けつつ、Notifications の API はそのまま使えるようにしています。
実装のポイント
※ PR 本文から分かる設計を中心に整理します。
1. 購読情報のスナップショット
ActiveSupport::Notifications.subscribeが呼ばれるたびに、- グローバルな「購読一覧」を更新
- その一覧の「凍結されたスナップショット」を作る
- 各 Ractor は、このスナップショットをもとに自前の Notifier を初期化します。
- スナップショットは
freeze済みなので、Ractor 間で共有しても安全です(Ruby の Ractor の要件)。
イメージ(擬似コード):
module ActiveSupport
module Notifications
@subscriptions = []
@subscriptions_snapshot = [].freeze
def self.subscribe(pattern = nil, &block)
# 実際にはもっと複雑だが、概念イメージ
subscription = Subscription.new(pattern, block)
@subscriptions << subscription
# 新しいスナップショットを作る
@subscriptions_snapshot = @subscriptions.map(&:dup).freeze
subscription
end
def self.subscriptions_snapshot
@subscriptions_snapshot
end
end
end(実際のコードはもう少し抽象化されていますが、概念としては「subscribe 時にスナップショットを更新する」形です)
2. Ractor ごとの Notifier 生成
- Ractor から
ActiveSupport::Notifications.instrument等を呼ぶとき、- その Ractor 専用の Notifier がなければ生成
- 生成時に
Notifications.subscriptions_snapshotを読み込み、AS::Fanoutに反映
- こうして、
- 各 Ractor は「自分専用の Fanout / Notifier」
- その中身(購読者リスト)は「メイン Ractor で確定されたスナップショット」
という構成になります。
擬似コード:
module ActiveSupport
module Notifications
RACTOR_LOCAL_KEY = :__as_notifications_notifier__
def self.notifier_for_current_ractor
Ractor.current[RACTOR_LOCAL_KEY] ||= begin
notifier = Notifier.new
notifier.configure_from_snapshot(subscriptions_snapshot)
notifier
end
end
def self.instrument(name, payload = {}, &block)
notifier_for_current_ractor.instrument(name, payload, &block)
end
end
end3. AS::Fanout への小変更
activsupport/lib/active_support/notifications/fanout.rbに 5 行だけ変更が入っており、- Ractor 向けの利用を許容するため、
- 「外部からまとめて購読情報を設定できる / スナップショットから初期化できる」ような API または内部フックが追加されていると考えられます。
- Fanout 自体を Ractor-safe にしたわけではなく、「Ractor ごとの孤立したインスタンスなら安全に使える」前提で使うための変更です。
4. テスト追加
activesupport/test/notifications_test.rb(+55行)- Ractor 内から Notifications を利用できること
- 購読が正しく反映されること
- 複数 Ractor が同時に使っても問題ないこと
などをカバーするテストが追加されています。
railties/test/application/initializers/frameworks_test.rb(+49行)- Rails アプリケーション初期化時に Ractor 対応が正しく行われるか
ActiveSupport::Ractors周りの設定と併せて、Notifications が壊れていないか
などを検証していると考えられます。
- 既存テスト (
activerecord/test/...) も一部修正されているため、Ractor 対応によって挙動が変わる部分(特にデフォルトスコープや通知周りのタイミング)がテストに影響しており、それに合わせて期待値やテスト構造を微調整していると思われます。
- 影響範囲・注意点
影響範囲
ActiveSupport::Notificationsを使うすべてのコード(Rails 本体やアプリケーションコード)が間接的に影響を受けますが、- 公開 API (
instrument,subscribe,unsubscribeなど) は変わらない設計です。
- 公開 API (
- ただし、内部的な挙動は以下のように変わります:
- 通知の配送は「Ractor ローカルな Notifier」に対して行われる
- 新しく登録された購読は、「その後に生成される Ractor の Notifier」からしか見えない可能性がある
(※特に「Ractor 生成 → subscribe → 既存 Ractor から instrument」の順序のようなケースでは、既存 Ractor の Notifier に購読が反映されない)
注意点 / 想定される落とし穴
Ractor 生成タイミングと subscribe のタイミング
- 実装から推測される挙動として、
- Ractor A 作成
- メイン Ractor で
Notifications.subscribe呼び出し - Ractor A 内で
instrument
→ Ractor A の Notifier は「Ractor A 生成時点のスナップショット」を持っており、その後の subscribe が反映されない可能性があります。
- 最も安全なのは:
- Ractor を生成する前に必要な subscribe をすべて済ませる
- Ractors を多用するアプリでは、「購読の追加タイミング」と「Ractor 生成タイミング」を意識する必要があります。
- 実装から推測される挙動として、
Ractor 内で subscribe した場合の扱い
- PR の説明から見ると、「
AS::Notifications.subscribeが呼ばれたときにスナップショットを更新」という設計なので、- Ractor 内から subscribe しても、その Ractor の Notifier を「取り替える」のか、「現 Notifier にだけ購読が付く」のかは実装依存です。
- 一般的には、Ractor 内で subscribe するのは避け、メイン Ractor 側で行う方が安全です。
- PR の説明から見ると、「
Fanout を直接触っているコード
- 非推奨だが、ライブラリやアプリが
ActiveSupport::Notifications::Fanoutを直接使っている場合、- 新しく追加された内部 API(スナップショット初期化関連)を前提にすることで Ractor 対応を行える可能性があります。
- ただし Fanout は内部 API なので、将来的な互換性は保証されません。
- 非推奨だが、ライブラリやアプリが
- 参考情報 (あれば)
- Ruby Ractor の制約:
- 共有できるのは「イミュータブル(freeze 済み)」なオブジェクトか、特別に Ractor 間共有を許可されているオブジェクトのみ。
- 今回の「購読情報スナップショットを freeze して Ractor 間で共有する」という設計は、この制約に沿った典型的なパターンです。
- 実運用上の指針:
- 通知の購読 (
subscribe) は、Rails 初期化時(config/initializers/*.rb)など「Ractor 生成前」にまとめて行う。 - Ractors を使う処理の中では、
instrumentなどの発火側に専念し、購読側の構造を動的に変えないようにする。
- 通知の購読 (
#58405 Add back loaded to RoutesReloader
マージ日: 2026/8/7 | 作成者: @rafaelfranca
- 概要 (1-2文で)
Rails::Application::RoutesReloaderから一度削除されていたloadedメソッド(属性)が、フレームワークの公開APIであることが判明したため復活させたPRです。これにより、既存アプリやライブラリがRoutesReloader#loadedに依存していても、引き続き問題なく動作できるようになります。
- 変更内容の詳細
※ 実際のコード断片はPR本文に含まれていませんが、変更ファイルと行数から推測できるレベルで説明します。
RoutesReloader に loaded を再追加
対象ファイル:
railties/lib/rails/application/routes_reloader.rb(+5/-0)
行数から見ると、次のような変更が入っている可能性が高いです(イメージの擬似コードです):
module Rails
class Application
class RoutesReloader
# 例: 再追加されたアクセサ
attr_reader :loaded
def initialize(...)
@loaded = false
...
end
def reload!
...
@loaded = true
end
# あるいは、以前と同じインターフェースを完全に再現している:
# attr_accessor :loaded
end
end
endPR説明文にある通り、loaded は一度別のPR(#58225)で削除されましたが、実は「Railsの公開API」として外部から利用されているメソッド/属性だったため、互換性維持のために元に戻されています。
「公開APIである」とは、主に以下を意味します。
- Railsの公式クラス/モジュール上の publicメソッド として外部からの利用を想定している
- マイナーアップデートやパッチレベルアップデートで後方互換性を維持すべき対象
今回のPRでは、その契約を守るために loaded が復活しています。
テストの追加・修正
対象ファイル:
railties/test/application/routes_reloader_test.rb(+30/-4)
主に以下のようなテストが追加されていると考えられます(擬似コード):
class RoutesReloaderTest < ActiveSupport::TestCase
test "routes reloader exposes loaded flag" do
reloader = Rails.application.routes_reloader
# 初期状態
assert_equal false, reloader.loaded
# reload 後に true になるなど、想定されたAPI挙動を確認
reloader.reload!
assert_equal true, reloader.loaded
end
endポイントは:
loadedが存在すること(NoMethodErrorにならないこと)- その値が、ルーティングがロード済みかどうかの状態を表していること
といった、公開APIとしての振る舞いを明確にテストしている点です。
これにより、将来の変更で loaded がうっかり削除・変更されるリグレッションを防ぎます。
- 影響範囲・注意点
影響範囲
Rails.application.routes_reloader.loadedを参照しているアプリケーション/エンジン/gemRoutesReloaderのリロード状態をフックして独自処理をしているツール
以前のPR(#58225)で loaded が削除されたことにより、上記のようなコードは NoMethodError になっていた可能性がありますが、このPRにより再び正常動作するようになります。
開発者視点でのポイント
RoutesReloader#loadedは、今後も後方互換性を期待できる公開API とみなしてよいです。ルーティングのリロード状態をウォッチする必要がある場合、次のようなコードが公式にサポートされる形で書けます:
rubyreloader = Rails.application.routes_reloader if reloader.loaded # ルートがロード済みのときだけ実行したい処理 end一方で、
loadedの意味はあくまで「RoutesReloaderがルートをロード済みかどうか」であり、「アプリケーションが完全にブートストラップ済みか」など別の意味ではないので、その点は誤用に注意が必要です。
- 参考情報 (あれば)
- このPRで復活させられたメソッドは、以下のPRで一度削除されていました:
- Railsの公開APIの扱い・後方互換性ポリシーは公式ガイドにも言及があります(Railsのアップグレードガイド等)。
- RoutesReloader 自体の利用例は、主に開発モードでのルートの自動リロードなど、フレームワーク内部処理が中心であり、アプリケーションコードから直接触るケースは比較的少ないですが、エンジン開発や高度なメタプログラミング系gemでは利用されることがあります。
#58397 Extract duplicated UPDATE self-join preparation into Arel::Visitors::…
マージ日: 2026/8/7 | 作成者: @seuros
- 概要 (1-2文で)
PostgreSQL と SQLite の Arel Visitor に重複していた UPDATE の self join 用の前処理ロジックが、共通のArel::Visitors::ToSql#prepare_update_statement_with_self_joinに抽出されました。挙動の変更はなく、内部実装のリファクタリングのみです。
- 変更内容の詳細
もともとの状態
Arel::Visitors::PostgreSQLとArel::Visitors::SQLiteにおいて、prepare_update_statementメソッド内部に- self join を伴う UPDATE 文を組み立てるための前処理ロジック
- そのための条件チェック(ガード)
- があり、その「本体ロジック部分」がほぼコピペ状態で重複していました。
概念的にはこういった構造だったと考えられます(疑似コード):
# postgresql.rb
def prepare_update_statement(o)
if uses_self_join?(o)
# ↓ ここが PostgreSQL / SQLite で重複していた本体処理
do_some_self_join_preparation(o)
else
super
end
end
# sqlite.rb
def prepare_update_statement(o)
if uses_self_join?(o)
# ↓ PostgreSQL と同じ実装
do_some_self_join_preparation(o)
else
super
end
end今回の変更点
共通ヘルパーの追加
activerecord/lib/arel/visitors/to_sql.rbに以下のメソッドが追加されています(名称は PR 説明より):ruby# Arel::Visitors::ToSql def prepare_update_statement_with_self_join(o) # これまで PostgreSQL / SQLite 両方に重複して書かれていた # self join UPDATE 用の前処理ロジックがここに移植された endここに、PostgreSQL と SQLite の
prepare_update_statement内に書かれていた「self join の UPDATE を組み立てるための共通ロジック」が集約されています。PostgreSQL / SQLite Visitor から共通メソッドを呼び出すように変更
postgresql.rb/sqlite.rbでは、それぞれが持っていたprepare_update_statementの中で、 自前で本体ロジックを書くのではなく、上記の共通ヘルパーを呼び出すようになりました。構造としては以下のようなイメージになります:
ruby# postgresql.rb def prepare_update_statement(o) if uses_self_join?(o) # 各 Visitor 固有のガード判定は維持 prepare_update_statement_with_self_join(o) # 共通ヘルパー呼び出し else super end end # sqlite.rb def prepare_update_statement(o) if uses_self_join?(o) # SQLite 版のガード prepare_update_statement_with_self_join(o) # 共通ヘルパー呼び出し else super end end- 各 Visitor ごとの「self join を扱うかどうか」の条件(ガード)は、そのまま Visitor ごとに保持。
- 実際の SQL 準備処理(self join UPDATE のための構築ロジック)のみを共通化。
MySQL Visitor には変更なし
Arel::Visitors::MySQLはもともとprepare_update_statementを独自にオーバーライドしており、この PR の共通化対象には含めていません。- MySQL 用の UPDATE 自己結合の処理は今まで通りで、挙動に影響なし。
- 影響範囲・注意点
実行時挙動
- PR 説明にある通り、「No behavior change」と明示されており、外部 API / SQL 出力 / クエリ結果に変更はありません。
- 影響するのは PostgreSQL / SQLite における「self join を伴う UPDATE 文」を Arel が生成するときの内部処理のみです。
対象 DB
- PostgreSQL / SQLite のみが今回の共通化対象。
- MySQL は今回の変更の影響を受けません。
メンテナンス面
- 将来的に self join UPDATE の実装を修正する必要が出た場合、
ToSql#prepare_update_statement_with_self_joinのみを変更すれば PostgreSQL / SQLite 両方に反映されます。 - DB ごとの差異は依然として「ガード部分」で吸収されるため、共通ロジックの中は「両者で本質的に同じ振る舞い」の部分に集中できます。
- 将来的に self join UPDATE の実装を修正する必要が出た場合、
互換性・移行面
- パブリック API の追加・削除はなく、Arel の内部メソッドレベルのリファクタリングにとどまります。
- 直接
Arel::Visitors::PostgreSQL#prepare_update_statementなどを monkey patch しているような特殊なコードを書いていない限り、アプリケーション側での対応は不要です。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58397
- 関連箇所:
activerecord/lib/arel/visitors/to_sql.rbactiverecord/lib/arel/visitors/postgresql.rbactiverecord/lib/arel/visitors/sqlite.rb
この変更は「コード重複の排除とメンテナンス性向上」を目的とした内部リファクタリングであり、self join な UPDATE クエリのバグ修正や仕様変更ではありません。
#58400 Extract duplicated local cache multi-read resolution in Strategy::LocalCache
マージ日: 2026/8/7 | 作成者: @seuros
- 概要 (1-2文で)
ActiveSupport::Cache::Strategy::LocalCache内で、fetch_multiとread_multi_entriesに重複して書かれていた「ローカルキャッシュからの複数キー解決処理」が共通メソッドに抽出されました。挙動の変更はなく、内部実装のリファクタリングです。
- 変更内容の詳細
背景
Strategy::LocalCache は、スレッドローカルなどにぶら下がる「ローカルキャッシュ」を持ち、同じリクエスト中に何度も同じキーを読む際に、バックエンドへのアクセスを減らすために使われます。
fetch_multi と read_multi_entries はどちらも「複数キーをまとめて読む」メソッドですが、
- すでにローカルキャッシュに入っている値を取り出す
- 入っていないキーだけをバックエンドから読む
- 読めた値をローカルキャッシュに格納する
というロジックが両方のメソッドにほぼ同じ形で重複していた、というのが PR の対象です。
主な変更点
local_cache.rb 内で、以下のような形で重複ロジックが共通化されたと考えられます(イメージの擬似コード):
# 変更前(イメージ)
def fetch_multi(*names)
# ローカルキャッシュを見て既にある値を集める
local = {}
misses = []
names.each do |name|
if local_cache && local_cache.key?(name)
local[name] = local_cache[name]
else
misses << name
end
end
# misses だけバックエンドから取得して local_cache に詰める
# ...
end
def read_multi_entries(names, **options)
# 上とほぼ同じローカルキャッシュ解決処理
local = {}
misses = []
names.each do |name|
if local_cache && local_cache.key?(name)
local[name] = local_cache[name]
else
misses << name
end
end
# misses のみバックエンドへ
# ...
endこれを次のようなヘルパーメソッドに抽出:
def resolve_local_cache_multi(names)
local = {}
misses = []
names.each do |name|
if local_cache && local_cache.key?(name)
local[name] = local_cache[name]
else
misses << name
end
end
[local, misses]
endそして、fetch_multi / read_multi_entries はこのヘルパーを呼び出すだけに簡略化されています:
def fetch_multi(*names, **options)
local, misses = resolve_local_cache_multi(names)
# misses だけをバックエンドから fetch し、
# 結果を local_cache に格納しつつ local + backend_result を返す
end
def read_multi_entries(names, **options)
local, misses = resolve_local_cache_multi(names)
# misses のみバックエンド read_multi し、
# local + backend_result を返す
end行数としては、+12 / -25 なので、ほぼ「共通部分の切り出しと、それを呼び出すコードへの差し替え」のみの変更です。
- 影響範囲・注意点
- 公開 API (
Rails.cache.fetch_multi,read_multiなど) の挙動は変わりません。 - ローカルキャッシュ戦略 (
Strategy::LocalCache) 自体のインターフェースも変わらず、内部実装のみの変更です。 - 既存のアプリケーションコード・gem が、
Strategy::LocalCacheの内部実装詳細に依存していない限り、互換性の問題は発生しません。 - 重複コードが減ったことで、将来的にローカルキャッシュの動作変更やバグ修正を行う際、
fetch_multiとread_multi_entriesの挙動差異が生まれにくくなり、保守性が向上します。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58400
- 関連ドキュメント:
- ActiveSupport::Cache::Store#fetch_multi: https://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html#method-i-fetch_multi
- ActiveSupport::Cache::Strategy::LocalCache: https://api.rubyonrails.org/classes/ActiveSupport/Cache/Strategy/LocalCache.html
#58401 Extract duplicated job matching logic in ActiveJob::TestHelper
マージ日: 2026/8/7 | 作成者: @seuros
- 概要 (1-2文で)
ActiveJob::TestHelper 内でassert_enqueued_withとassert_performed_withがそれぞれ重複して持っていた「ジョブの条件マッチング」処理を、共通のプライベートメソッドfind_matching_jobに切り出した PR です。挙動は変えずに、重複コードを削減しテストヘルパー内部の保守性を高めています。
- 変更内容の詳細
何をしたか
activejob/lib/active_job/test_helper.rb内で、以下の2つのアサーションが共通で使うロジックを抽出しました。assert_enqueued_withassert_performed_with
- 抽出したメソッド名はプライベートメソッド
find_matching_jobで、与えられた条件オプションに一致するジョブをキュー(または実行履歴)の中から探す責務を持ちます。
変更前のイメージ
従来は assert_enqueued_with と assert_performed_with の中に、それぞれほぼ同じ形のロジックが書かれていました。典型的には次のような処理です(擬似コード):
def assert_enqueued_with(job: nil, args: nil, at: nil, queue: nil, &block)
# ...
matching_jobs = enqueued_jobs.select do |job|
# class, queue, args, scheduled時刻などをチェックする条件式
end
assert matching_jobs.any?, "Expected job to be enqueued with ..."
end
def assert_performed_with(job: nil, args: nil, at: nil, queue: nil, &block)
# ...
matching_jobs = performed_jobs.select do |job|
# 上とほぼ同じ条件式
end
assert matching_jobs.any?, "Expected job to be performed with ..."
endこれらの「条件式」部分が、それぞれのメソッド内にほぼ重複して存在していました。
変更後のイメージ
共通化された find_matching_job を導入し、2つのアサーションから呼び出すように変更しています:
def assert_enqueued_with(job: nil, args: nil, at: nil, queue: nil, &block)
# ...
matching_job = find_matching_job(enqueued_jobs, job: job, args: args, at: at, queue: queue)
assert matching_job, "Expected job to be enqueued with ..."
end
def assert_performed_with(job: nil, args: nil, at: nil, queue: nil, &block)
# ...
matching_job = find_matching_job(performed_jobs, job: job, args: args, at: at, queue: queue)
assert matching_job, "Expected job to be performed with ..."
end
private
def find_matching_job(jobs, job:, args:, at:, queue:)
jobs.find do |enqueued_job|
# jobクラス、キュー名、引数、実行予定時刻などの一致判定ロジック
end
end実際のコードでは引数名や判定条件は ActiveJob::TestHelper の仕様に沿ったものになっていますが、構造としては上記のように「集合(enqueued_jobs/performed_jobs)を受け取って条件に合うものを返す」形に抽象化されています。
行数の増減
- ファイルは1つのみ変更 (
activejob/lib/active_job/test_helper.rb) - 追加行: 25行
- 削除行: 34行
→ 重複部分をまとめたことで総行数は減っており、コードが簡潔になっています。
- 影響範囲・注意点
公開APIの挙動は変わらない想定
- 変更はあくまでプライベートメソッドの抽出・リファクタリングであり、
assert_enqueued_with/assert_performed_withのインターフェースや期待される挙動は変わりません。 - 既存のテストコード(これらのアサーションを利用している側)に影響はないはずです。
- 変更はあくまでプライベートメソッドの抽出・リファクタリングであり、
内部実装に依存しているコードへの潜在的影響
ActiveJob::TestHelperの内部実装に(モンキーパッチ・リフレクション等で)依存している場合は、条件マッチングロジックの位置が変わることで影響する可能性があります。- ただし、抽出されたメソッドは
privateであり、通常は直接利用される想定ではありません。
ロジックの一元化によるバグ修正容易性
- 将来的に「ジョブのマッチング条件」を変更・拡張する場合、
find_matching_jobだけを直せばよくなり、assert_enqueued_withとassert_performed_withの挙動差異が生まれにくくなります。 - これまで両方に同じ変更を入れる必要があった箇所が1か所に集約されるため、バグ混入のリスク減にもつながります。
- 将来的に「ジョブのマッチング条件」を変更・拡張する場合、
- 参考情報 (あれば)
- 対象コード:
activejob/lib/active_job/test_helper.rb
- 関連APIドキュメント:
- Active Job Testing —
assert_enqueued_with,assert_performed_with
https://guides.rubyonrails.org/active_job_basics.html#testing-jobs
- Active Job Testing —
- PR:
#58395 Extract duplicated bind value coercion in QueryMethods
マージ日: 2026/8/6 | 作成者: @seuros
- 概要 (1-2文で)
ActiveRecord::Relation::QueryMethods内で、SQLリテラル用のバインド値変換ロジックが重複していた部分を共通メソッド化して整理したPRです。挙動・機能は変わらず、内部実装のリファクタリングのみです。
- 変更内容の詳細
- 対象:
activerecord/lib/active_record/relation/query_methods.rb - 目的:
bind_value_for_sql_literalに関する「バインド値の型変換・正規化」処理が複数箇所で重複していたため、その重複コードを抽出し共通化。
具体的には、以下のようなイメージの変更が入っています(疑似コード):
# 変更前(イメージ)
def bind_value_for_sql_literal(attribute, value)
if value.is_a?(ActiveRecord::Relation)
# Relation 用の変換
elsif value.is_a?(Array)
# Array 用の変換
else
# それ以外の変換
end
end
def some_other_method(...)
# ↑とほぼ同じ変換ロジックを別メソッドでも書いている
end# 変更後(イメージ)
def coerce_bind_value_for_sql_literal(value)
if value.is_a?(ActiveRecord::Relation)
# Relation 用の変換
elsif value.is_a?(Array)
# Array 用の変換
else
# それ以外の変換
end
end
def bind_value_for_sql_literal(attribute, value)
coerce_bind_value_for_sql_literal(value)
end
def some_other_method(...)
coerce_bind_value_for_sql_literal(value)
endポイント:
- 「SQLリテラルに埋め込む前に、Relation / Array / その他のオブジェクトをどう扱うか」という変換処理が1つのヘルパーメソッドに集約された形です。
- コメントにもある通り「No functional change」と明示されており、外部から見える仕様は一切変えていません。
- 行数としては、重複コードが削られ、新しい共通メソッドの分だけ追加されているため、「削除 22行 / 追加 13行」という構成になっています。
- 影響範囲・注意点
影響範囲:
ActiveRecord::Relation::QueryMethods内部で SQL リテラルのバインディングを行うコードパス全般が、この共通化されたメソッドを通るようになっています。- ただし、既存処理をそのまま切り出しただけなので、型変換ロジック自体は変わっていません。
注意点:
- 公開APIの仕様変更はなく、アプリケーションコード側で修正すべき点はありません。
- 万一影響が出るとすれば、これまで「偶然通っていた」ようなマイナーなコードパスの挙動差ですが、PRの意図が「全く同じ変換の共通化」であるため、実質的に後方互換性は維持されています。
- Rails内部に対して monkey patch で
bind_value_for_sql_literalなどを直接上書きしている場合は、内部構造が少し変わったことで想定外の影響が出る可能性はあります(そのような使い方は非推奨ですが)。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58395
- 対象バージョン: マージ日時から見て、Rails 7.x もしくはそれ以降の開発ブランチ (main) への取り込みと考えられます。リリースノートでは「リファクタリング」として扱われる類の変更です。
#58382 Remove collector methods of unregistered Mime types
マージ日: 2026/8/6 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Mime::Type.unregisterで MIME タイプを削除した後にも、そのフォーマット向けの Collector メソッドが残り続けていた問題を修正し、適切にメソッドを削除してエラーメッセージを分かりやすくしました。これにより、未登録の MIME タイプに対するフォーマットメソッド呼び出し時に、従来の紛らわしいInvalidMimeTypeではなく、明確なNoMethodErrorが発生するようになります。
- 変更内容の詳細
背景となる問題
ActionController::MimeResponds などで使われる Collector(例: respond_to ブロック内の format.html, format.json など)は、Mime::Type.on_change(以前は Mime.register_callback)経由で、登録された MIME タイプごとにメソッドを動的に定義しています。
respond_to do |format|
format.html
format.json
endここで、例えば Mime::Type.register で独自フォーマット :foo を登録すると、Collector に #foo メソッドが生えます。
問題は、Mime::Type.unregister(:foo) で MIME タイプを削除しても、Collector 側の #foo メソッドが削除されない ことでした。その結果、format.foo は以下のような流れになっていました。
- まだ残っている
Collector#fooが呼ばれる - 内部で
Mime[:foo]を引くが、すでに unregister 済みなのでnilが返る Collector#custom(nil)のような形で呼ばれ、最終的にMime::Type.new(nil)が実行される- その結果
Mime::Type::InvalidMimeType (nil is not a valid MIME type)が発生
→ 実際の問題は「フォーマットが未登録」なのに、「nil は MIME type として無効」という紛らわしい例外になっていた。
今回の修正内容
この PR では、MIME タイプの削除(unregister)に追従して Collector 側のメソッドも除去するように変更されています。
ポイント:
Mime::Type.on_changeのハンドラで、- MIME タイプが「登録」されたとき: これまで通り Collector にメソッドを定義
- MIME タイプが「削除」されたとき: 対応する Collector のメソッドを
remove_methodで削除
- Collector にメソッドが無くなることで、そのフォーマット呼び出しは
Collector#method_missingにフォールバックします。 method_missingは「メソッドが定義されていない」という形でNoMethodErrorを発生させるため、- 「このフォーマットを使うには、先に MIME タイプとして登録する必要がある」という趣旨の、文脈に沿ったエラーになる。
PR description の冒頭には「Redefine the method to emit a deprecation warning」とありますが、最終的な実装は「メソッド削除」であり、実際には deprecation warning を挟まずにメソッドを消して NoMethodError に委ねる形になっています。
(説明文の後半にも「Removing the method routes the call through Collector#method_missing instead, which raises a NoMethodError...」とあり、こちらが実装に対応しています。)
テストの追加
actionpack/test/abstract/collector_test.rb で以下のようなケースがカバーされています(要約):
- MIME タイプを登録すると Collector にメソッドが生えること
- MIME タイプを unregister したあと、そのメソッドが呼ばれると
NoMethodErrorになること - MIME タイプを再登録すると、再びメソッドが使用可能になること
これにより、登録 → 削除 → 再登録のライフサイクルで Collector のインタフェースが一貫していることが保証されています。
- 影響範囲・注意点
影響を受けるのは、カスタム MIME タイプを登録・削除しているアプリケーションです。
例: initializer などでMime::Type.register "application/vnd.myapp+json", :myformatを行い、何らかのタイミングでMime::Type.unregister(:myformat)をしている場合。以前:
Mime::Type.unregister(:myformat)後にrespond_to { |f| f.myformat }を呼ぶとMime::Type::InvalidMimeType (nil is not a valid MIME type)が発生していた
変更後:
- 同じコードは
NoMethodError(「myformat というメソッドはない」)になる - 実質的には「すでに例外は投げていた」状態から、「より妥当で分かりやすい例外メッセージ」に変わっただけなので、互換性の観点での大きな破壊変更ではないと位置づけられています(PR 本文にも "No deprecation cycle: the stale method already raised, only the error message improves." と明記)。
- 同じコードは
アプリケーション側で、
InvalidMimeTypeを前提にした rescue やメッセージ判定をしている場合は、NoMethodErrorに変わる点に注意が必要です。- ただし、そのようなケースはレアと考えられ、一般的には「何が悪いのか分からない InvalidMimeType」から「使おうとしたフォーマットが登録されていない」という明確なバグシグナルに変わるため、デバッグ性は向上します。
MIME タイプを再登録した場合でも、以前と同様に Collector のメソッドが再定義されるため、再登録後の挙動は従来どおりです。
- 参考情報 (あれば)
- 当該 PR:
- 議論元になった PR とコメント:
- 関連クラス/機能:
ActionController::MimeResponds/AbstractController::CollectorMime::Type.register,Mime::Type.unregister,Mime::Type.on_change
#58391 Define shareable compiled_method_container readers
マージ日: 2026/8/6 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Rails のビューコンパイル周りで、compiled_method_containerのリーダメソッド定義を「Ractor で共有可能 (shareable)」な形に修正した PR です。テンプレートのコンパイル/レンダリングが Ractor 環境で動作するように、lambda によるクラスキャプチャのタイミングと方法を見直しています。
- 変更内容の詳細
※PR 本文から読み取れる主目的は「Ractor 互換のための shareable 化」です。該当差分は少ないですが、典型的なパターンは以下のような変更です。
背景
- Rails の
ActionView::Baseでは、テンプレートをコンパイルしたメソッドを格納するコンテナ (compiled_method_container) をクラスごとに持っています。 - そのコンテナにアクセスする reader メソッドを lambda で定義しており、その lambda が「どのオブジェクトをキャプチャしているか」によって、その Proc/lambda が Ractor 的に shareable かどうかが決まります。
- 以前のコードは、定義時のコンテキスト (たとえばベースクラス自身や非 shareable なオブジェクト) をキャプチャしてしまうため、Ractor 間で共有できない Proc になっていました。
修正のポイント
PR の説明文から読み取れる要点:
The lambda captures only the subclass, created after the assignment completes, so it can be made shareable.
- lambda がキャプチャするのは「サブクラス」だけになるように実装を変更しています。
- かつ、そのサブクラスは「代入が完了した後に生成される」ため、その時点で shareable な状態であり、lambda 自体も shareable にできる、という設計です。
おおよそのイメージコード:
変更前のイメージ (Ractor 非対応なパターン)
class ActionView::Base
class << self
def compiled_method_container
@compiled_method_container ||= begin
container = Class.new(self) # self や他の非 shareable なものを捕まえがち
# self や instance などを暗黙・明示的にキャプチャしてしまう
@compiled_method_container_reader ||= -> { container }
container
end
end
end
endこのようなコードでは、lambda が self や非 shareable なインスタンス変数をキャプチャしてしまい、Ractor で共有できないオブジェクトになります。
変更後のイメージ (shareable なパターン)
class ActionView::Base
class << self
def compiled_method_container
@compiled_method_container ||= begin
subclass = Class.new(self)
# 代入が完了した後に生成された subclass だけをキャプチャ
@compiled_method_container_reader = -> { subclass }
subclass
end
end
# 他の場所からは reader を通してコンテナにアクセスする
def compiled_method_container_reader
@compiled_method_container_reader
end
end
endポイント:
- lambda が「closure として抱える情報」を サブクラス (=コンテナ) だけに限定。
- かつ、そのサブクラスは「もうインスタンス変数にセットし終わった後に作成」されているため、Ractor 的に許可される shareable な形にできる。
- これにより、その lambda 自体を Ractor 間で安全に共有し、コンパイル済みテンプレートのコンテナ参照として利用できます。
テストの変更
actionview/test/template/lookup_context_test.rb にテストが追加 (+11 行) されています。主な確認内容は:
lookup_context/ActionView::Base周りで、compiled_method_containerの reader が期待通りのクラスを返すか。- その reader が shareable な形で定義されていること (Ractor を利用したテストや shareable? チェックがある可能性が高い)。
- 影響範囲・注意点
対象範囲
ActionView::Baseのテンプレートコンパイル・メソッド格納ロジック (compiled_method_container) 周辺。- テンプレートのレンダリング自体の外部 API (例:
render) には変更がない想定ですが、その内部で使われるコンパイル済みメソッドコンテナの取得方法が変わっています。
Ractor 利用時の影響
- Ractor 内からビューをレンダリングする場合や、Ractor を用いてテンプレートの事前コンパイル・キャッシュを行うようなユースケースで、以前はエラー/警告が出ていた部分が解消される可能性があります。
- shareable でない Proc を別 Ractor に渡そうとすると
Ractor::IsolationErrorになるため、その問題回避を目的とした変更です。
パフォーマンス・互換性
- 変更の性質上、コンテナクラスの生成タイミング・reader の定義方法が若干変わるのみで、パフォーマンスへの大きな悪影響は考えにくいです。
- 公開 API のシグネチャを変えているわけではないため、通常のアプリケーションコードで
ActionView::Baseを直接いじっていない限り、互換性問題はほぼありません。 - 一方で、独自に
compiled_method_containerやそのクラス構造を前提にメタプログラミングしている場合は、振る舞い・クラス階層が変わっていないか確認した方が安全です。
Ractor 非対応コードとの組み合わせ
- アプリケーション側が Ractor 非対応のオブジェクト (グローバル状態を持つシングルトン、スレッドローカルなど) をビュー周りで多用していると、今回の PR だけでは Ractor 対応は完結しません。
- この PR はあくまで「ActionView のテンプレートコンパイル部分での Ractor 非互換要素の一つを取り除いた」位置づけです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58391
- Ractor と shareable オブジェクトの概要:
- Ruby 公式ドキュメント (Ractor): https://docs.ruby-lang.org/en/master/Ractor.html
Object#shareable?/Ractor.make_shareableの挙動
- 関連しそうな過去 PR (推測):
- ActionView / ActiveSupport 周辺で「Ractor 対応」「shareable lambda/Proc」に言及している PR 群 (Rails 7.1 以降で散発的に進められている)
#58390 Make template lookup ractor-safe
マージ日: 2026/8/6 | 作成者: @etiennebarrie
- 概要 (1-2文で)
このPRは、テンプレートの lookup(LookupContext)と digest 計算(Digestor)周りを Ractor セーフにするための変更です。これによりメイン Ractor 以外からでもLookupContextの構築やテンプレート digest の計算が安全に行えるようになります。
- 変更内容の詳細
2-1. LookupContext の details デフォルト Proc を Ractor-shareable に
ActionView::LookupContext は、formats, variants, handlers などの「details」を内部で管理しており、そのデフォルト値は Proc で定義されています。
今回の PR では、この「details のデフォルト Proc」が Ractor 間で共有可能(Ractor-shareable)になるように修正されています。
背景:
- Ractor では「Ractor-shareable でないオブジェクト」を他 Ractor へ渡すことができません。
- 以前は
LookupContext内部のデフォルト Proc が shareable ではなく、メイン Ractor 以外でLookupContext.newを呼び出すと問題になる可能性がありました。
この PR により:
- 非メイン Ractor 内で
ActionView::LookupContext.newを直接生成しても安全に動作 - その際、details のデフォルト値がそのまま利用可能
- details のキーも Ractor 内で
intern(シンボル化)できるように保障
テスト (actionview/test/template/lookup_context_test.rb) では、Ractor 内で LookupContext を生成し、details が適切に扱えることが検証されています。
2-2. Digestor の Mutex を Ractor ローカルに
ActionView::Digestor はテンプレートの digest(キャッシュキー相当)を計算するクラスで、内部でキャッシュを持ち、その保護のために Mutex を使っています。
前提となる #58281 で:
- digest キャッシュ自体が Ractor ごとのローカルストレージに移されている(Ractor ローカルキャッシュ)
- それに対し、Mutex がまだ「グローバルな」状態だと、Ractor 間で Mutex を共有することになり、Ractor セーフではない
この PR では:
Digestorが利用する Mutex を「Ractor ローカルストレージ」に移動- つまり、「各 Ractor ごとに独立した Mutex + digest キャッシュを持つ」構成に統一
効果:
- 各 Ractor は自分専用のキャッシュとロックを持つため、Ractor 間で Mutex を共有せずに済む
- 同時に、Ractor ごとの digest キャッシュの整合性が保たれ、デッドロックや shareable 制約の問題を避けられる
テスト (actionview/test/template/digestor_test.rb) では、Ractor 内で digest 計算を行い、エラーなく正しい digest が計算できることが確認されています。
2-3. 非メイン Ractor で可能になったこと
PR 説明にもある通り、次が可能になります:
- 非メイン Ractor で
ActionView::LookupContextを構築 - details のキーを
intern(例:"html".to_symのような処理)して利用 - 非メイン Ractor でテンプレートの digest を計算(
Digestor)
並列レンダリングや、Ractor を利用したプリコンパイル・事前ウォームアップなどで、ビュー関連処理をメイン Ractor とは別の Ractor にオフロードしやすくなります。
- 影響範囲・注意点
対象コンポーネント
ActionView::LookupContextActionView::Digestor- それらを利用するテンプレート lookup / digest 計算の仕組み全般
影響のあるケース
- Ractor を利用しているアプリケーション / ライブラリ
- 特に「Ractor 内で ActionView の機能を触る」ような高度な並列化をしている場合
- テンプレート digest を独自に扱う gem(
Digestorに直接依存している場合)
互換性
- 既存のシングルスレッド / マルチスレッド(Ractor 不使用)アプリには基本的に挙動変更はありません。
- digest キャッシュ・Mutex のスコープが「Ractor ローカル」に統一されたため、Ractor を使う場合の安全性が向上しています。
- Ractor 間で digest キャッシュが共有されなくなった設計(#58281 との組み合わせ)が前提のため、「Ractor を跨いで digest キャッシュを共有する」ような前提のカスタマイズをしている場合は設計の見直しが必要です(通常の利用では問題になりません)。
パフォーマンス面
- 各 Ractor ごとに digest キャッシュを持つため、「Ractor を増やすほどキャッシュが分散する」ことになりますが、その代わりロック競合や shareable 制約が減り、Ractor 想定として自然な設計になっています。
- 単一 Ractor での性能は基本的に変わりません。
- 参考情報 (あれば)
この PR の前提となる変更
- #58281: digest キャッシュを Ractor ローカルにする変更(本 PR はそのフォローアップ)
関連コードの主な場所
actionview/lib/action_view/lookup_context.rb- details のデフォルト値や初期化周り
actionview/lib/action_view/digestor.rb- digest 計算ロジック、キャッシュ、Mutex 管理
- テスト
actionview/test/template/lookup_context_test.rbactionview/test/template/digestor_test.rb
Ractor を使った並列レンダリングや、ビュー関連処理のオフロードを検討している場合、この PR と #58281 の内容を合わせて追っておくと設計の参考になります。
#58387 Reduce allocations during attribute definition
マージ日: 2026/8/6 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
ActiveModel の attribute メソッド定義時に使われる"define_method_"文字列の生成回数を減らし、オブジェクト割り当て(アロケーション)を削減するパフォーマンス改善です。Pattern ごとに 1 回だけ文字列を作るようにし、大量の属性を持つモデルでメモリ負荷を軽減します。
- 変更内容の詳細
※ PR では activemodel/lib/active_model/attribute_methods.rb に対して +3 / -2 の小さな変更のみです。
背景
ActiveModel の attribute methods(name, name=, name? などを動的に定義する仕組み)では、「あるパターンに基づいてメソッドを define_method する」ような処理があり、その際に "define_method_" のようなプレフィックス文字列が使われています。
もともとの実装イメージは概ね次のようなものです(※擬似コード):
patterns.each do |pattern|
attributes.each do |attr_name|
method_name = "define_method_" + pattern.suffix # ここで毎回 String を生成
define_method(method_name) do
# ...
end
end
endpatternsは ~16 個程度attributesはモデルにより非常に多くなりうる(大規模なスキーマなどでは数百〜数千)- その結果、
"define_method_"という同じ文字列が、Pattern ごと・属性ごとに何度もアロケートされていた
今回の変更内容
PR の説明にある通り:
Currently this "define_method_" string is allocated per Pattern (~16) per attribute (~N, really big) defined.
This commit moves the string into Pattern so that it is only allocated once per pattern (~16).
つまり、"define_method_" 文字列を「パターン側」に持たせてキャッシュするように変更しています。イメージとしては次のような変更です:
Before(概念的なイメージ)
class Pattern
def method_name_for(attr_name)
"define_method_" + attr_name.to_s
end
endAfter(概念的なイメージ)
class Pattern
DEFINE_METHOD_PREFIX = "define_method_".freeze
def method_name_for(attr_name)
DEFINE_METHOD_PREFIX + attr_name.to_s
end
endまたは、Pattern インスタンスごとにメンバ変数で持つような形かもしれませんが、いずれにせよ:
- 以前: Pattern × Attribute の組み合わせごとに
"define_method_"が新規生成 - 以降: Pattern ごとに 1 つの
"define_method_"だけが生成され再利用
という構造になっています。
変更行数が +3/-2 と非常に少ないため、
"define_method_"のリテラルをクラス定数 or Pattern インスタンスの ivar に移動- 呼び出し側をその定数/ivarを参照する形に変更
といった最小限のリファクタリングにとどまっていると考えられます。
- 影響範囲・注意点
影響範囲
- 対象: ActiveModel の
attribute_methods機構ActiveModel::AttributeMethodsをインクルードしているクラス- Rails の ActiveRecord モデルはこのモジュールを経由しているため、実質的にすべてのモデルが間接的な影響を受けます。
- タイプ: パフォーマンス(メモリアロケーション削減)のみ
- 動作仕様(どのメソッドが定義されるか、呼び出し結果)は変わらない
- 文字列フリーズ済みであれば GC 負荷も軽減される
想定される効果
- 特に大量のカラムを持つテーブルのモデルで、アプリ起動時や
attribute_methods定義時のメモリ使用量が削減 - GC の負荷が若干軽減され、起動時間や初回アクセスのパフォーマンスがわずかに向上する可能性があります。
注意点
- 変更は内部実装レベルであり、公開 API のインターフェースに変更はありません。
"define_method_"の文字列を直接参照しているようなメタプログラミング(かなり特殊なケース)がある場合は、挙動に影響する可能性がありますが、通常のアプリケーションではそのような依存はまずないと考えられます。- スレッドセーフティ:
- 定数化もしくは不変なインスタンス変数でのキャッシュなので、スレッドセーフです(書き換えなければ問題なし)。
- 参考情報 (あれば)
- 対象ファイル:
activemodel/lib/active_model/attribute_methods.rb
- 関連概念:
ActiveModel::AttributeMethods- メソッド定義時の
define_methodと文字列アロケーション
- 類似の最適化:
- Rails 内では、同様に頻出の文字列・シンボル・正規表現などを定数化してアロケーションを抑える最適化が他の箇所でも行われています。
#58386 Make Relation::FromClause.empty Ractor safe
マージ日: 2026/8/6 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
Active Record のRelation::FromClause.emptyの実装を、Ruby 3 の Ractor(並行実行環境)でも安全に共有できるように変更した PR です。WhereClauseに対して行われたのと同種の「イミュータブルで Ractor セーフな空オブジェクト」の扱いをFromClauseにも拡張しています。
- 変更内容の詳細
※ PR 本文・差分情報が最小限のため、Rails の既存実装パターンと WhereClause の同様の変更内容から推測を交えて説明します。
背景: Relation::FromClause と empty メソッド
ActiveRecord::Relation::FromClause は、クエリの FROM 句を表現する内部クラスです。Relation::FromClause.empty は、空の FROM 句(デフォルト状態)を表すオブジェクトを返すクラスメソッドで、内部的に多くの Relation 操作がこの「空 from 句」を起点に組み立てられます。
Ractor セーフ化のポイント
Ruby 3 以降の Ractor モデルでは、Ractor 間で共有されるオブジェクトは「不変(freeze 済み)」かつ「Ractor セーフ」である必要があります。Rails では、内部で使い回す「空の句(empty な Clause オブジェクト)」をクラス変数や定数でキャッシュし、複数スレッド/Ractor間で共有しているため、それらを Ractor セーフにする必要があります。
以前のコミットで WhereClause.empty が Ractor セーフになるように変更されており、この PR はそれと同じパターンを FromClause.empty に適用しています。
典型的な変更イメージは以下のようなものです(疑似コード):
# 変更前(イメージ)
def self.empty
@empty ||= new(nil, []) # ← freeze されていない / Ractor セーフ保証なし
end
# 変更後(イメージ)
EMPTY = new(nil, []).freeze
EMPTY.ractor_make_shareable if EMPTY.respond_to?(:ractor_make_shareable)
def self.empty
EMPTY
endもしくは、WhereClause と同様に Class.new 時点で shareable_constant_value アノテーションを利用している可能性もあります。
どちらにせよ、意図としては:
emptyが同一インスタンスを返すキャッシュであることを維持- そのインスタンスを
freeze等により不変化 - Ruby 3.1+ の
ractor_make_shareableや#shareable_constant_valueを利用し、Ractor 間共有可能であることを明示
という変更になっています。
- 影響範囲・注意点
影響範囲
- Active Record 内部のクエリ構築 (
Relation) において、FROM句が未指定のときなどに使われるRelation::FromClause.emptyが Ractor 環境でも安全に共有されます。 - 通常の(Ractor を使わない)Rails アプリでは挙動はほぼ変わらず、パフォーマンスやメモリ使用に関しても、従来からあった「空オブジェクトのキャッシュ」をより安全にしただけ、という位置づけです。
- 「空の FromClause オブジェクトを破壊的に変更している」ような非公開 API の誤用をしていない限り、アプリケーションコードへの影響は基本的にありません。
注意点
Relation::FromClause.emptyが返すオブジェクトは、これまで以上に「書き換えてはいけない前提」のイミュータブルオブジェクトになります。
もし内部 API を無理に触っていて、emptyが返したオブジェクトに対して破壊的操作をしている場合、FrozenErrorなどが発生する可能性があります。- Ractor を利用して Active Record を並列利用するケースでは、
FromClause周りでRactor::UnsafeErrorが出る可能性が下がり、安定性が向上します。 - この PR は
WhereClauseの Ractor セーフ対応と整合を取る内容なので、FROMとWHEREの両方で同じ Ractor セーフ戦略が適用されることになります。
- 参考情報 (あれば)
- 同種の変更:
WhereClauseの Ractor セーフ化
https://github.com/rails/rails/commit/d8c7523c016a59e425a8c930d792fd937bbe8ccf - Ruby Ractor の概要:
- Ractor で共有可能なオブジェクトの条件(freeze / shareable)が重要
ractor_make_shareable/shareable_constant_valueなどがよく使われるパターンです。
#58206 Generate an omakase-compliant config/ci.rb when steps are skipped
マージ日: 2026/8/6 | 作成者: @55728
- 概要 (1-2文で)
bin/rails new実行時に各種--skip-*オプションを使った場合でも、新規アプリが自前のbin/rubocop/bin/ci(Style: Ruby)チェックに確実に通るよう、config/ci.rbテンプレートの空行出力ロジックが調整されました。
とくに--skip-test時にLayout/EmptyLinesAroundBlockBodyで RuboCop が落ちていた問題を解消します。
- 変更内容の詳細
問題の背景
以下のような手順でアプリを生成すると:
bin/rails new myapp --skip-test
cd myapp
bin/rubocop
# または bin/ci の "Style: Ruby" ステップ生成された config/ci.rb に対して RuboCop の Layout/EmptyLinesAroundBlockBody が発生していました。
エラー例:
config/ci.rb:12:1: C: [Correctable] Layout/EmptyLinesAroundBlockBody:
Extra empty line detected at block body end.原因は、「Checks グループ」と「Tests グループ」の間を区切るための空行が、--skip-test により Tests グループ自体が生成されないにもかかわらず、末尾に一行だけ残ってしまうことでした。
修正内容
変更ファイル:
railties/lib/rails/generators/rails/app/templates/config/ci.rb.tt(+1/-2)
テンプレート (.tt) 内で、各ステップの間に入れる区切り用の空行を、「両側にステップが存在する場合のみ出力する」ように条件付きに変更しています。
イメージとしては、これまで:
group "Checks" do
# rubocop step
# brakeman step
# ↑ ここに必ず空行が出る
end
group "Tests" do
# test step
endのように一律で空行を出していたところを、以下のようなイメージに近付けています:
group "Checks" do
# rubocop step
# brakeman step
<% if any_test_steps? %>
<% end %>
end
<% if any_test_steps? %>
group "Tests" do
# test step
end
<% end %>実際のコードは Rails のジェネレータ用 ERB で、
- RuboCop / bundler-audit / JavaScript / Brakeman / Test の各ステップの有無
- それらの間に空行を入れるかどうか
を条件分岐させる形に変更されています。
デフォルト生成との違い
オプションを指定せず bin/rails new した場合の違いは 1 点だけです:
- Brakeman ステップと
"Tests"グループの間にあった空行がなくなる
それ以外の出力は従来と同じです。
- 影響範囲・注意点
影響対象:
- Rails 7.2 以降(あるいはこの PR が入るバージョン)での新規アプリ生成 (
bin/rails new) - 特に
--skip-rubocop/--skip-bundler-audit/--skip-javascript/--skip-brakeman/--skip-testの任意の組み合わせでアプリを生成するケース
- Rails 7.2 以降(あるいはこの PR が入るバージョン)での新規アプリ生成 (
期待される効果:
- どのスキップオプションの組み合わせでも、生成直後に
bin/rubocopおよびbin/ciが Layout/EmptyLinesAroundBlockBody を含め RuboCop によるスタイルチェックをパスする - いわゆる「omakase(Rails 推奨)」設定に準拠した
config/ci.rbが常に生成される
- どのスキップオプションの組み合わせでも、生成直後に
既存アプリへの影響:
- 既存アプリの
config/ci.rbは自動で書き換わらないため、直接の破壊的変更はありません。 - 同様の RuboCop 警告が出ている既存アプリでは、
config/ci.rbの余分な末尾空行を削除することで対処可能です。
- 既存アプリの
注意点:
- この PR は空行出力の条件のみをいじっており、CI の中身(実行ステップそのもの)には変更がありません。
- RuboCop の設定 (
.rubocop.yml) 自体は触っていないため、他の Layout/Style 由来の警告が出るかどうかは各アプリの設定次第です。
- 参考情報 (あれば)
対応する issue/PR:
- #58164: 認証ジェネレータが挿入する routes を修正し、新規アプリが自前の RuboCop を再びパスするようにした PR
→ 今回はconfig/ci.rbに対する同種の「新規アプリが RuboCop に素で通るようにする」調整。
- #58164: 認証ジェネレータが挿入する routes を修正し、新規アプリが自前の RuboCop を再びパスするようにした PR
関連コマンド:
- アプリ生成:
bin/rails new myapp [--skip-* ...] - CI 実行:
bin/ci - RuboCop 実行:
bin/rubocop(Rails テンプレートではbin/ci内からも呼び出される)
- アプリ生成:
#58392 Move duplicated execute_batch up to MySQL::DatabaseStatements
マージ日: 2026/8/6 | 作成者: @seuros
- 概要 (1-2文で)
MySQL 系アダプタ(mysql2 / trilogy)でそれぞれ重複実装されていたexecute_batchを、共通のActiveRecord::ConnectionAdapters::MySQL::DatabaseStatementsに集約したリファクタリングです。挙動の変更はほぼなく、コードの重複排除とメンテナンス性向上が主目的です。
- 変更内容の詳細
やったこと
- 共通の親モジュール:
activerecord/lib/active_record/connection_adapters/mysql/database_statements.rbにexecute_batch実装を追加(+18行)。 - 個別アダプタから削除:
mysql2/database_statements.rbからexecute_batchを削除(-18行)trilogy/database_statements.rbからexecute_batchを削除(-18行)
結果として、MySQL 系アダプタはすべて同じ execute_batch 実装を継承して使う構成になっています。
想定されるコードイメージ(概念的なサンプル)
※実際のコードはPRを参照してください。ここでは典型的な Rails の execute_batch 実装イメージを示します。
# activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
module ActiveRecord
module ConnectionAdapters
module MySQL
module DatabaseStatements
def execute_batch(statements, name = nil)
statements.each do |sql|
execute(sql, name)
end
end
end
end
end
end以前は mysql2 と trilogy の各アダプタ側に、上記とほぼ同じ内容の execute_batch が個別に定義されていたものを、共通モジュールに移した形です。
- 影響範囲・注意点
- 対象
- ActiveRecord の MySQL 系アダプタすべて(
mysql2/trilogy)のexecute_batch呼び出しが、新しい共通実装を使うようになります。
- ActiveRecord の MySQL 系アダプタすべて(
- 互換性
- 「重複していた共通コードを一箇所に集約しただけ」の変更であり、インターフェース(メソッドシグネチャ)や基本挙動に変更はない想定です。
- ただし、以前
mysql2とtrilogyで微妙に挙動を変えていた場合(例えばロギングやエラー処理など)には、今回の共通化で差分が吸収される可能性があります。その場合、両者の挙動がより揃う一方、アダプタ固有の「クセ」が消えることになります。
- アプリケーションコードへの影響
execute_batchを直接使っているアプリケーションは通常そのまま動作します。- アダプタを monkey patch して
execute_batchをオーバーライドしていた場合は、継承元のモジュール位置が変わっているため、モジュールのインクルード順やメソッド解決順序(lookup path)に依存した高度なパッチは確認が必要です。
- ライブラリ側(gem作者など)への注意
- mysql2 / trilogy の各アダプタクラスに直接
execute_batchを再定義していた middleware / extension がある場合、今後は MySQL::DatabaseStatements のメソッドを前提にした方が安全です。
- mysql2 / trilogy の各アダプタクラスに直接
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58392
- 関連コード:
activerecord/lib/active_record/connection_adapters/mysql/database_statements.rbactiverecord/lib/active_record/connection_adapters/mysql2/database_statements.rbactiverecord/lib/active_record/connection_adapters/trilogy/database_statements.rb
- 文脈: Rails では DB アダプタ間で共通化できるメソッドを上位モジュールに集約しており、今回もその一環として MySQL 系の重複コードを整理したものと考えられます。
#58187 Inspect nested attribute id in RecordNotFound message
マージ日: 2026/8/6 | 作成者: @flavorjones
- 概要 (1-2文で)
accepts_nested_attributes_for経由で渡されたネスト属性のidが見つからない場合に発生するActiveRecord::RecordNotFoundのエラーメッセージ内で、ユーザー入力由来のidをそのまま埋め込まず.inspectするように変更した PR です。これにより、ANSIエスケープシーケンスなどを含む悪意あるidによるログ偽装・視認性の低下リスクを低減します。
- 変更内容の詳細
変更対象
ActiveRecord::NestedAttributes#raise_nested_attributes_record_not_found!- ファイル:
activerecord/lib/active_record/nested_attributes.rb
- ファイル:
- 対応するテスト追加
- ファイル:
activerecord/test/cases/nested_attributes_test.rb
- ファイル:
具体的なコード変更イメージ
もともとネスト属性で関連レコードが見つからなかった際、以下のように(概念的に)record_id をそのままメッセージに埋め込んでいました:
raise RecordNotFound.new(
"Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}",
model, primary_key, record_id
)これが次のように変更されています:
raise RecordNotFound.new(
"Couldn't find #{model} with ID=#{record_id.inspect} for #{self.class.name} with ID=#{id}",
model, primary_key, record_id
)ポイント:
record_id.inspectを使うことで:- 文字列IDの場合:
"foo"→"\"foo\""のようにクォートされ、制御文字はエスケープされてログ上では「ただの文字列リテラル」として表示される - 整数IDの場合:
123→"123"と見た目上ほぼ変わらない(inspectは数値をそのまま文字列化するだけ)
- 文字列IDの場合:
挙動の具体例
攻撃的な(または単におかしな)id をネスト属性経由で送ったケース:
{
"post": {
"comments_attributes": [
{
"id": "\u001b[31mHACK\u001b[0m" // = "\e[31mHACK\e[0m"
}
]
}
}従来:
ログに出力される
RecordNotFoundのメッセージ例:Couldn't find Comment with ID=\e[31mHACK\e[0m for Post with ID=1端末(ターミナル)でログを閲覧すると、
\e[31mや\e[0mが解釈され、文字色が変わったり、出力の一部が見えづらくなったりしてログ偽装・隠蔽に悪用し得る。
変更後:
メッセージ例:
Couldn't find Comment with ID="\e[31mHACK\e[0m" for Post with ID=1ログビューワ / 端末から見ると、ANSIコードはただのエスケープされた文字列として扱われ、色付けやカーソル移動などは発生しない。
何を「inspect」しているか
.inspectを適用するのは ネストされた属性のid(record_id) のみ- メッセージ内の「親」レコード側の
id(#{id})は変更なし- これは通常アプリ内部で管理されている主キーであり、外部から任意制御されないことを前提としているため
- 既存の
find系メソッドに対する同種修正 (CVE-2025-55193 対応) と整合
テスト
nested_attributes_test.rbにテストが追加されており、ネスト属性のidに制御文字を含む文字列を渡したときに、RecordNotFoundのメッセージ内でinspectされた形になることを検証しています。
- 影響範囲・注意点
- 影響するのは:
accepts_nested_attributes_forを使って関連を更新/作成している- そのネストされた
idで対象の関連レコードが見つからずActiveRecord::RecordNotFoundが発生する - その例外メッセージを ログや外部システムに出力している
- フレームワーク的には
RecordNotFoundは通常 404 応答にマッピングされ、メッセージ文字列はレスポンスとしては返されないため、エンドユーザーへの直接的なXSSなどは発生しません。 - 主な効果はログの安全性向上:
- 攻撃者が任意のANSIシーケンスを含む
idを送りつけても、ログを閲覧する人の端末側でそれが解釈されず、ログの色分け・削除・上書きなどの「視覚的な攻撃」がしづらくなる。
- 攻撃者が任意のANSIシーケンスを含む
- 既存アプリでの互換性:
RecordNotFound#messageの文言がわずかに変わります(idがクォートされる・制御文字がエスケープされる)- もし
RecordNotFound#messageを文字列パースして独自処理しているようなコードがあれば(あまり無いと思われますが)、そのパースロジックに影響する可能性はあります。
- 追加のアプリ側対応:
- 特に対応は不要ですが、ログビューア・監視ツール等で
RecordNotFoundメッセージのパターンマッチをしている場合、ID=...部分がID="..."になることを前提にしているかどうかを確認するとよいです。
- 特に対応は不要ですが、ログビューア・監視ツール等で
- 参考情報 (あれば)
- 類似の脆弱性対応:
- CVE: CVE-2025-55193
ActiveRecord::FinderMethods等でRecordNotFoundメッセージ内のidに.inspectを適用した修正。今回のPRはその「ネスト属性版」の追従です。
- CVE: CVE-2025-55193
- 元報告:
- HackerOne ユーザー
ylchenによる報告がもとになっており、フレームワーク単体では exploitable でないため情報提供扱いとなっていましたが、ログセキュリティの観点から今回修正されています。
- HackerOne ユーザー
#58209 Flatten block content inside links in Action Text markdown conversion
マージ日: 2026/8/6 | 作成者: @flavorjones
- 概要 (1-2文で)
Action Text のContent#to_markdownが、リンク (<a>) 内にブロック要素が入っている場合に不正な CommonMark を出力していた問題を修正する PRです。リンク内のブロック要素を Markdown の制約に合わせて「1行のインラインテキスト」にフラット化してからリンク記法に包むように変更しています。
- 変更内容の詳細
背景となる問題
HTML 上は
<a>は「transparent content model」を持つため、<a><p>...</p></a>や<a><pre>...</pre></a>といった「ブロック要素を内包するリンク」は合法です。一方で CommonMark 仕様では、リンクテキストは「0個以上のインライン要素の並び」と定義されており、リンクの中に段落やコードブロックなどのブロック要素を入れる手段がありません。
ActionText::Content#to_markdownの中ではvisit_aがリンク変換を担っており、子要素の出力がインラインであることを前提に以下のように生成していました:ruby"[#{inner}](#{encode_href(href)})"しかし
visit_pre,visit_p,visit_blockquote, 見出し (visit_h1〜), リスト (visit_ul,visit_ol) などは、コードフェンスや段落間の空行など「ブロックレベル」の出力を行うため、以下のような不正な Markdown が生成されていました。例: HTML
html<a href="https://example.com"><pre>puts 1</pre></a>旧挙動の出力:
markdown[``` puts 1上記では - ``` ``` のフェンスコードブロック - その後の空行 によって Markdown パーサはブロック構造を先に解釈し、インラインリンクが分断され、仕様上不正かつ期待通りにリンクにならない出力となっていました。
変更方針
CommonMark は「リンク内にブロック要素を含める」方法を提供していないため、「リンク構造を保ちつつ、中身を可能な範囲でインラインに潰して表現する」 という degrade(劣化表現)戦略を取っています。
新しい
visit_aの挙動:- 子要素の Markdown 出力をすべて連結した文字列を取得
- その中の「連続する改行(
\nが1回以上続く部分)」をスペース1つに置き換える - それを
"[...](href)"の中身として使う
疑似コードイメージ:
rubydef visit_a(node) inner = visit_children(node) flattened = inner.gsub(/\n+/, " ") if href_present_and_allowed?(node) "[#{flattened}](#{encode_href(href)})" else inner # href がない・許可されない場合はそのまま end endhrefがない、または許可されないアンカーについては、従来通りリンクとしてレンダリングされず、内部のブロック構造を保持したまま素通しします。つまり、「リンクにならない<a>」の振る舞いは変えていません。
具体例
<pre>ブロックを含んだリンクHTML:
html<a href="https://example.com"><pre>puts 1</pre></a>新しい出力:
markdown[``` puts 1 ``` ](https://example.com)- もともとコードブロック(フェンス)として扱われていた部分が、インラインコードスパン
puts 1に縮退され、その前後の改行はスペースに変換されます。 - 結果として「リンクテキストがインラインコード
puts 1」である正当な CommonMark となり、そのテキスト全体がリンクになります。
- もともとコードブロック(フェンス)として扱われていた部分が、インラインコードスパン
見出しを含んだリンク
HTML:
html<a href="https://example.com"><h1>heading</h1></a>新しい出力:
markdown[# heading ](https://example.com)<h1>→# headingという Markdown 見出しの表現自体は維持されますが、改行がスペースに変換されるため1行にフラット化されます。- その
# headingという文字列自体が「リンクテキスト」として解釈されます(#はあくまでプレーンテキスト扱いで、ブロック見出しにはなりません)。
リストや引用など、他のブロック要素を含む場合も同様に、行頭記号(
-,>,1.など)を含んだテキストがすべて 1 行に潰され、そのままリンク内テキストになります。
テスト
actiontext/test/unit/markdown_conversion_test.rbに、リンク内にブロック要素が含まれる様々なパターンを網羅するテストが多数追加されています (+122行)。- これにより、
visit_aの新しいフラット化ロジックが期待通り動作することが検証されています。
- 影響範囲・注意点
- 対象: Action Text の
ActionText::Content#to_markdownを利用して HTML を Markdown に変換しているコード(特に WYSIWYG エディタの保存内容を Markdown に変換して外部に出すようなケース)。 - 現時点では
to_markdown自体がまだ未リリース(#56858 で追加されたがリリース版には未含有)であるため、「公式リリース間での互換性破壊」は発生しません。 - ただし、以下のような点は把握しておくとよいです:
- もし unreleased な
to_markdownを既に利用していた場合、「リンク内のブロック要素が壊れた Markdown として出力される」バグが修正されるため、結果の Markdown が大きく変わる可能性があります。 - 「ブロックとしての構造」(段落区切り、コードブロックなど)はリンク内では維持されず、すべて 1 行のプレーンテキストまたはインラインコードとして縮退されます。
- 例えば「リンク内に複数段落のテキストがある」といった HTML は、Markdown 変換後は「スペースで連結された1つのインラインテキストのリンク」になります。
hrefがない・許可されない<a>はブロック構造が維持されるため、その挙動に依存しているコードがあれば変更はありません。
- もし unreleased な
- セキュリティ面:
- 本 PR は主に出力フォーマットの正当性の改善であり、リンク許可/不許可のポリシー自体は変更していません。
encode_hrefやフィルタリングのポリシーへの影響はありません。
- 本 PR は主に出力フォーマットの正当性の改善であり、リンク許可/不許可のポリシー自体は変更していません。
- 参考情報 (あれば)
- CommonMark 仕様: Links
https://spec.commonmark.org/0.31.2/#links to_markdown追加 PR (#56858):
https://github.com/rails/rails/pull/56858- 本 PR (#58209):
Flatten block content inside links in Action Text markdown conversion
https://github.com/rails/rails/pull/58209
#58270 asset_path: strip the query/fragment tail without a second scan
マージ日: 2026/8/6 | 作成者: @nuclearspike
- 概要 (1-2文で)
asset_pathがクエリ文字列・フラグメント(?/#以降)を取り除く処理について、正規表現による2回のスキャンと不要な文字列コピーをやめて、1回のスキャン+スライスだけで処理するよう最適化したPRです。これによりimage_tagやjavascript_include_tagなどのヘルパ全般で、オブジェクト割り当て削減と数%のパフォーマンス向上が見込まれます。
- 変更内容の詳細
変更前の処理
asset_path 内では、ソース文字列からクエリ・フラグメント部分を取り出しつつ、元の文字列からそれを削除するために、正規表現を2回使っていました。
tail, source = source[/([?#].+)$/], source.sub(/([?#].+)$/, "")問題点:
- 正規表現マッチのスキャンが2回走る
String#subはマッチしない場合でも新しい文字列を必ず生成する- 実際には
"application.js"のようなクエリやフラグメントを含まないケースが大多数であり、その場合にも無駄なオブジェクト割り当てが発生していた
変更後の処理
クエリ・フラグメントを「取り出す」処理はそのまま使い、削除処理を「長さに基づくスライス」に変更しています。
変更後のコードイメージ:
if (tail = source[/[?#].+$/])
source = source[0, source.length - tail.length]
endポイント:
- 正規表現マッチは1回だけ (
source[/[?#].+$/]) - クエリ・フラグメントがない場合
tailはnilなので、sourceは一切コピーされない - ある場合だけ、末尾から
tail.lengthバイト分を削った新しい文字列を作る
改めて、挙動の例
挙動自体は従来と同じです。たとえば:
asset_path("app.js?v=3", skip_pipeline: true) # => "/app.js?v=3"
asset_path("app.css#print", skip_pipeline: true) # => "/app.css#print"
asset_path("xmlhr.js?123", skip_pipeline: true)
asset_path("xmlhr.js#hash", skip_pipeline: true)
asset_path("xmlhr.js?123#hash", skip_pipeline: true)これらは既存テスト (asset_tag_helper_test.rb 内の javascript_path テスト) でカバーされており、スライスのオフバイワンがあると即座に壊れるようになっています。
仕様上の細かい差異の可能性
新しい実装では「マッチしたレンジ」ではなく「末尾からのバイト数」で削るため、ソースに tail の後ろに改行があるようなケースでは挙動が理論上変わります:
- 旧:
"app.js?\n"→"app.js\n"になる - 新:
"app.js?\n"→"app.js?"になる(末尾の"\n"が残る)
ただし、資産パス(asset_path に渡すパス文字列)に改行が入ることは想定されておらず、Rails コアとしてはこのようなケースを防御するためにコードを複雑化する価値はない、という判断になっています。
- 影響範囲・注意点
影響範囲
- 対象となるメソッド:
asset_path- およびそれを内部的に利用する各種ヘルパ:
image_tagjavascript_include_tagstylesheet_link_tagasset_url- そのほか
asset_pathを経由するヘルパ全般
これらすべてで、クエリ・フラグメント付きパス/なしパスの扱いは従来と同じ挙動を維持しつつ、以下のような改善があります。
パフォーマンス・メモリ面の影響
ベンチマーク結果(Ruby 3.3.11, arm64-darwin, benchmark-ips):
asset_path("application.js")- 511.5k i/s → 552.7k i/s(約 +8.0%)
asset_path("images/logo-with-a-longer-name.png")- 448.0k i/s → 484.4k i/s(約 +8.1%)
オブジェクト割り当て数(1,000回呼び出しあたりの平均):
- プレーンなパス/ネストしたパスともに:
- 1呼び出しあたりの割り当て数が 8 → 7 に削減
マシンのノイズにより速度向上率は +2.5%〜+13% 程度のばらつきがありますが、「1コールあたりの割り当てオブジェクト数が1つ減る」ことは安定して観測されています。テンプレート内で大量の image_tag / javascript_include_tag が呼ばれるアプリケーションでは、全体として GC 負荷の軽減に寄与します。
注意点
- 仕様上の挙動は(改行を含むような異常なソースを除き)従来と同じ
- テストは既存の
asset_tag_helper_testによる回帰検知に依存しており、明示的にテストケースを増やしてはいませんが、クエリ付き・フラグメント付き・併用パターンはカバー済み - 特殊な monkey patch を
asset_pathまわりにしている場合は、パッチから逆算しているコード(たとえば内部実装を前提にしたsource.sub(/([?#].+)$/, "")相当の処理)を書いていると、挙動に差が出る可能性がありますが、通常の利用では影響ありません
- 参考情報 (あれば)
- 変更ファイル:
actionview/lib/action_view/helpers/asset_url_helper.rb(+3 / -1)
- ベンチマークスクリプト:
- PR 本文の
<details>内に掲載。activesupport/actionviewをローカルパス指定し、benchmark-ipsを利用してasset_pathの性能比較を行う簡易ベンチ。
- PR 本文の
- 実装上の意図:
- 高頻度で呼ばれる小さなユーティリティ (
asset_path) において、「ほとんど起きないケース(クエリ付きパス)のために、通常ケースで毎回余分なスキャンとコピーを行う」ことをやめ、ホットパスのコスト削減を狙ったマイクロ最適化。
- 高頻度で呼ばれる小さなユーティリティ (
#58135 Add query predicate hooks for Active Record types
マージ日: 2026/8/5 | 作成者: @jsaubry
- 概要 (1–2文で)
Active Record の型ごとに、where句などのクエリ述語の「左辺(カラム側)」と「右辺(値側)」をカスタマイズできるフックが追加されました。これにより、UUID など「書き込み時の表現」と「検索時の表現」が異なる型でも、型クラス側で SQL 生成を制御しやすくなります。
- 変更内容の詳細
2-1. 追加されたフックの概要
Active Model / Active Record の型 (ActiveModel::Type::Value を継承したクラス) に、クエリ用のフックが追加されています。
新しく追加された主なメソッド:
query_attribute(attribute)- 述語の左辺(属性名側)の変換用フック
- 例:
lower(custom_immutable_unaccent(name))のような式に置き換えるなど
query_value(attribute, value, predicate_builder:)- 述語の右辺(バインド値側)の変換用フック
- Prepared statement を維持しながら、
UUID_TO_BIN(?)のような関数呼び出しでラップしたり、正規化した値を別列で比較したい場合などに利用
これらは内部的に PredicateBuilder から呼び出され、以下のケースで利用されます。
- 等価比較 (
where(id: 1)など) - 配列 (
where(id: [1, 2, 3])) - 範囲 (
where(created_at: start_time..end_time))
2-2. 実装上の変更点の概要
主な変更ファイルと役割:
activemodel/lib/active_model/type/value.rbActiveModel::Type::Valueにquery_attribute/query_valueのデフォルト実装が追加- デフォルトは「何もしない」実装で、既存挙動を変えないようにしているはず
activerecord/lib/active_record/relation/predicate_builder*.rbPredicateBuilderとその各種 handler(array_handler,range_handler, etc.)で、値や属性を扱う際に新しいフックを使うように変更- ハッシュ形式 (
where(column: value)) を解釈するときに、型オブジェクトからquery_attribute/query_valueを呼んでから Arel ノードを組み立てるようになっている
activerecord/lib/active_record/relation.rbPredicateBuilderの呼び出し部分に微修正(新しいフックに対応)
activerecord/test/cases/relation/predicate_builder_test.rb- 新機能をカバーするテストが多数追加(+102行)され、フックが意図した通りに呼ばれること、bind が壊れないことなどを確認
2-3. 想定されているユースケース例
例1: MySQL の BINARY(16) UUID 列
アプリ側では "a1b2-..." のような UUID 文字列を使いたいが、DB では BINARY(16) に保存している場合。
MyUuidType < ActiveModel::Type::String のような型を定義し、そこで:
class MyUuidType < ActiveModel::Type::String
# 左辺の変換: id -> id (そのまま) でもよいが、
# UUID_TO_BIN(id) のようにする場合はここで Arel を組み立てる。
def query_attribute(attribute)
# 例: Arel::Nodes::NamedFunction.new("UUID_TO_BIN", [attribute])
attribute
end
# 右辺の変換: 'a1b2...' -> UUID_TO_BIN(?) など
def query_value(attribute, value, predicate_builder:)
# value はアプリ側の UUID 表現(文字列)なので、ここで
# 「バインドされた ? を UUID_TO_BIN() で包む」Arel を返すイメージ
# 実装イメージ (擬似コード):
arel_value = predicate_builder.build_bind_attribute(attribute.name, value)
Arel::Nodes::NamedFunction.new("UUID_TO_BIN", [arel_value])
end
end上記のような実装をすると、呼び出し側は:
User.where(id: "a1b2-...-uuid-string")と書くだけで、内部的には:
WHERE id = UUID_TO_BIN(?)のような SQL が生成されることを狙っています(実際の Arel の書き方は実装詳細になりますが、方向性としてこういうことが可能)。
例2: 正規化して比較するテキスト列
DBにはユーザーが入力した文字列をそのまま保存しつつ、検索時は正規化した値で比較したいケース:
lower(custom_immutable_unaccent(name)) = lower(custom_immutable_unaccent(?))といった SQL を生成したければ、テキスト用のカスタム型で:
class NormalizedText < ActiveModel::Type::String
def query_attribute(attribute)
# lower(custom_immutable_unaccent(name))
func = Arel::Nodes::NamedFunction
func.new("lower", [
func.new("custom_immutable_unaccent", [attribute])
])
end
def query_value(attribute, value, predicate_builder:)
arel_value = predicate_builder.build_bind_attribute(attribute.name, value)
func = Arel::Nodes::NamedFunction
func.new("lower", [
func.new("custom_immutable_unaccent", [arel_value])
])
end
endとしておけば、呼び出し側:
User.where(name: "Émilie")が、期待通りの正規化比較 SQL に変換されるようになります。
2-4. なぜ cast / serialize ではダメなのか
cast や serialize は「Ruby <-> DB の値変換」の責務を持っており、INSERT/UPDATE の書き込みや SELECT の読み込みに共通で使われます。
しかし今回の要件は、
- 「検索条件(WHERE)のみ」で
- カラム名やバインド変数を SQL 関数で包みたい
というものなので、
- 単純な「値変換」ではなく、Arel ノードレベルの「SQL 構造の変換」
- SELECT の投影や INSERT の値には影響させたくない
といった性質のため、専用のクエリ述語用フックを設ける設計になっています。
- 影響範囲・注意点
- 既存アプリへの影響
- 既存の型クラスは
query_attribute/query_valueを実装していないため、デフォルト実装(何もしない)が使われ、挙動は従来通りです。 - そのため、この PR を取り込んでも、既存コードは基本的にブレークしない想定です。
- 既存の型クラスは
- カスタム型を実装する場合の注意点
query_valueでは「プレースホルダ付きのバインド」を活かす必要があります。SQL文字列を自前で連結するのではなく、predicate_builderを介してバインド値を生成し、それを Arel で包む形にするべきです。query_attribute/query_valueはあくまで「述語用」のフックであり、SELECT 句の投影結果や INSERT/UPDATE の書き込み時には使われません。その前提でインターフェースを設計する必要があります。
- サポートされる述語の種類
- 今回のフックは Equality / Array / Range の述語で利用されます。その他の演算子(LIKE、>、<、IN (サブクエリ) など)でどこまで効くかは、
PredicateBuilder内部の実装に依存します。
- 今回のフックは Equality / Array / Range の述語で利用されます。その他の演算子(LIKE、>、<、IN (サブクエリ) など)でどこまで効くかは、
- 将来的な API 安定性
- PR 説明では「internal predicate-builder hooks」と明記されているため、まずは内部 API として導入された可能性があります。
公開 API としてドキュメント化されるかは今後の変更を確認する必要があります(特に Gem として公開する共通型の実装では、この点を意識した方がよいです)。
- PR 説明では「internal predicate-builder hooks」と明記されているため、まずは内部 API として導入された可能性があります。
- 参考情報 (あれば)
- 元 PR: https://github.com/rails/rails/pull/57395 (kirs 氏による最初の試み)
- 今回の PR: https://github.com/rails/rails/pull/58135
- 実際の
PredicateBuilder内でのquery_attribute/query_value呼び出し箇所や、テストケースを読むと、どのような Arel ノードを返せばよいかがより具体的に把握できます。
- 実際の
- 関連ファイル
activemodel/lib/active_model/type/value.rb: 型フックの定義元activerecord/lib/active_record/relation/predicate_builder*.rb: クエリビルド箇所activerecord/test/cases/relation/predicate_builder_test.rb: 実際にフックが使われるパターンのテスト
この PR により、「型ごとにクエリ時の SQL 生成を細かく制御したい」というニーズに対して、Arel ベースでの拡張ポイントが公式に用意された形になります。
#58383 Delete leaked Cpk::Author in the autosave teardown
マージ日: 2026/8/5 | 作成者: @etiennebarrie
- 概要 (1-2文で)
このPRは、特定のテスト(TestDestroyAsPartOfAutosaveAssociation)がトランザクションなしで実行されることにより、DB上に不要なCpk::Authorレコードが残り、その後に実行される別テスト(WhereChainTest)を不安定化させていた問題を修正するものです。autosave のテストの teardown で漏れたCpk::Authorを削除する1行を追加し、テスト間のデータリークを防いでいます。
- 変更内容の詳細
問題の背景
問題のテスト1:
TestDestroyAsPartOfAutosaveAssociation#test_autosave_has_one_cpk_association_when_composite_foreign_key_is_manually_set- このテストは「トランザクション付きテスト」を使わずに実行されている(=各テスト後に自動でロールバックされない)。
- そのため、このテストで作られた
Cpk::Authorレコードがテスト終了後もDBに残ってしまう。
問題のテスト2:
WhereChainTest#test_missing_with_composite_primary_key- このテストでは
Cpk::Book(id: [1, 2])を作成する。 - ここで
author_idが、先にリークして残っていたCpk::AuthorのIDと一致してしまうケースがある。 - その状態で
where.missing(:author)を実行すると、本来は「author が存在しない book」を返すはずが、リークした author によって関連が埋まってしまい、結果が空になりアサーションが失敗する。 - つまり「autosave のテストが先に実行されたかどうか」で結果が変わるフレークテストになっていた。
- このテストでは
作者が再現に使ったコマンド:
ARCONN=sqlite3 bundle exec ruby -Ilib:test \
-e 'load "test/cases/autosave_association_test.rb"; load "test/cases/relation/where_chain_test.rb"' \
-- --seed=35972実際の変更内容
変更ファイル:
activerecord/test/cases/autosave_association_test.rb(+1 / -0)
内容としては、autosave 関連のテストの teardown(またはそれに準じる後処理)に「テストで作成した Cpk::Author を削除する」1行を追加しただけです。
イメージとしては、以下のようなコードが追加されたと考えられます(擬似コード):
def teardown
super
Cpk::Author.delete_all # 実際にはスコープされているか、特定レコードのみ削除
end実際の差分は1行のみで、目的は「このテストが作成した author レコードが、他のテストに影響しないようにする」ことです。
- 影響範囲・注意点
- 影響範囲は Rails 本体の「テストコード」のみであり、アプリケーションのランタイム挙動や API 仕様には影響しません。
- 修正により、以下が改善されます:
WhereChainTest#test_missing_with_composite_primary_keyが、事前に autosave の CPK テストが走ったかどうかで結果が変わるフレーク状態から解消される。- CPK(Composite Primary Key / Composite Foreign Key)を使ったテスト同士が、漏れたレコードにより互いに干渉しにくくなる。
- 注意点として、「トランザクションを使わないテスト」は、このようなデータリークを起こしやすいため、同様のパターンが他にもないかを継続的に確認する必要があります。
- 参考情報 (あれば)
- 対象の不安定テスト:
TestDestroyAsPartOfAutosaveAssociation#test_autosave_has_one_cpk_association_when_composite_foreign_key_is_manually_setWhereChainTest#test_missing_with_composite_primary_key
- フレークが確認されたビルド:
- 関連概念:
- autosave associations(
autosave: trueな関連) - composite primary key / composite foreign key を用いた関連のテスト
- トランザクションなしテストにおけるレコードリークとテスト順依存問題
- autosave associations(
#58281 Move the LookupContext::DetailsKey caches to Ractor-local storage
マージ日: 2026/8/5 | 作成者: @etiennebarrie
- 概要 (1-2文で)
LookupContext::DetailsKey 周りのキャッシュを Ractor 間で共有しないようにし、Ractor ごとに独立したキャッシュを持つ形に変更することで、メイン Ractor 以外からのテンプレート lookup を可能にした PRです。あわせて、Ractor ローカルなストレージを簡単に扱うためのActiveSupport::Ractors.store_if_absentが追加され、concurrent-rubyのバージョン要件も上がっています。
- 変更内容の詳細
背景問題
TemplateDetails::Requestedオブジェクトのインターン(intern)と、テンプレートのダイジェストキャッシュに使っていたConcurrent::Mapが、Ractor 間で共有可能なオブジェクトではなかった。- そのため、メイン Ractor 以外からテンプレート lookup を行うと IsolationError になる / うまく動かない 状態だった。
- 特に
Concurrent::Mapは、Concurrent::NULLという sentinel オブジェクトを内部的に使うが、concurrent-ruby1.3.8 より前はこれが freeze されておらず、Ractor 間で扱うと IsolationError が発生する。
この PR では、
- そもそも テンプレート lookup 関連のキャッシュを Ractor 間で共有しない
- Ractor ごとに独自のキャッシュを持たせる
ことで問題を解消しています。
1) LookupContext::DetailsKey キャッシュを Ractor ローカルに変更
対象: actionview/lib/action_view/lookup_context.rb
- もともと、テンプレート lookup の際に用いる
TemplateDetails::Requested(テンプレートのフォーマット・ロケール・ハンドラなどをまとめた key オブジェクト)と、そのダイジェストキャッシュをグローバルなConcurrent::Map的な構造で管理していた。 - この PR では、それらのキャッシュを Ractor ローカルストレージ に移動。
イメージとしては、以下のような変更が行われています(簡略イメージ・擬似コード):
# 変更前(イメージ)
DETAILS_KEY_CACHE = Concurrent::Map.new
DIGEST_CACHE = Concurrent::Map.new
def details_key(details)
DETAILS_KEY_CACHE.compute_if_absent(details) do
TemplateDetails::Requested.new(details)
end
end# 変更後(イメージ)
def details_key(details)
# Ractor ごとに「details_key_cache」という Map を持つ
cache = ActiveSupport::Ractors.store_if_absent(:details_key_cache) do
Concurrent::Map.new
end
cache.compute_if_absent(details) do
TemplateDetails::Requested.new(details)
end
end- 実際のコードでは、
TemplateDetails::Requestedの intern 処理やダイジェストキャッシュの保持にもActiveSupport::Ractors.store_if_absentが使われるようになっています。 - ポイントは「Ractor をまたいで共有される定数の Map をやめて、Ractor ごとに別の Map を持つ」という設計に転換していることです。
2) ActiveSupport::Ractors.store_if_absent の追加
対象: activesupport/lib/active_support/ractors.rb
新たに ActiveSupport::Ractors.store_if_absent が導入されています。
目的:
- 「キーに対応する Ractor ローカルオブジェクトを取得し、なければブロックで生成して保存する」という処理を一箇所で提供するユーティリティです。
- Ruby の Ractor サポートがない or 不完全なバージョン向けには、
Concurrent::Mapを使ったフォールバック(shim)を提供します。
イメージ(簡略):
module ActiveSupport
module Ractors
# Ractor ローカルなストレージに、キー key で値を保存・取得する
def self.store_if_absent(key)
store = ractor_local_store # Ractor ごとに分かれるストアを取得
store.fetch(key) do
store[key] = yield
end
end
# Ruby が Ractor をサポートしていない場合などは、
# 単純にプロセス全体で共有される Concurrent::Map を使う
# (=既存の挙動とほぼ同じ)
end
end- 実際の実装では、Ruby のバージョンや Ractor の有無を判定して動的に挙動を変えているはずです。
- Ractor 対応環境では、Ractor ごとに独立したストア(例えば
Ractor.currentに紐づく Hash など)を内部的に持つような実装になっていると考えられます。 - 非 Ractor 環境では、従来どおり
Concurrent::Mapベースで「プロセス内共有キャッシュ」として動作します。
3) concurrent-ruby のバージョンアップ
対象: Gemfile.lock
concurrent-rubyの必要バージョンが1.3.8に引き上げられています。- 理由: 1.3.8 で
Concurrent::NULLsentinel が freeze され、Ractor 間で扱えるようになったため。- それ以前のバージョンでは、
Concurrent::NULLがミュータブルなままであり、Ractor 間で参照するとIsolationErrorを引き起こします。 - 結果として、非メイン Ractor 内で
Concurrent::Mapを使うこと自体が不可能でした。
- それ以前のバージョンでは、
- この PR では、少なくとも Ractor ローカルのキャッシュとして
Concurrent::Mapを使う必要があるため、Concurrent::NULLが Ractor セーフであることを前提としています。
4) テスト追加・変更
対象:
actionview/test/template/lookup_context_test.rbactivesupport/test/ractors_test.rb
主に以下を確認するテストが追加・修正されています。
LookupContext が Ractor 内でも動作すること
- 別 Ractor からテンプレート lookup を行い、IsolationError などが発生しないこと。
- Ractor ごとに details key のインターンやダイジェストキャッシュが正しく機能していること。
ActiveSupport::Ractors.store_if_absentの挙動- 同じ Ractor 内で同じ key を使うと同じオブジェクトが返ること。
- 別 Ractor では別オブジェクトが返る(=Ractor ローカルである)こと。
- Ruby のバージョンや Ractor 有無に応じたフォールバック挙動が正しいこと。
- 影響範囲・注意点
影響範囲
テンプレート lookup とキャッシュ
- Action View のテンプレート lookup に関する details key インターン・ダイジェストキャッシュの実体が、「プロセス全体で共有された Map」から「Ractor ごとのローカルストア」に変わりました。
- そのため、
- Ractor A で構築された details key やダイジェストキャッシュは、Ractor B からは見えません。
- 同じテンプレートを複数の Ractor から使う場合、各 Ractor がそれぞれキャッシュを埋め直すことになります(ただし digest 値そのものは変わらない)。
パフォーマンス上の性質
- 単一 Ractor(従来どおりのマルチスレッドのみ)の環境では、実質的にはこれまでと大きく変わらない挙動になります。
- 複数 Ractor を使っている場合、テンプレート lookup のキャッシュが Ractor ごとに分かれるため、
- 初回アクセスのオーバーヘッドは Ractor 数に比例して増える
- しかし Ractor 間でのロック・同期は減る(Ractor ごとの独立キャッシュのため)
というトレードオフがあります。
外部インターフェース
- Digest 値そのものや、外部のフラグメントキャッシュキーは変更されていません。
- したがって、
cacheヘルパーで生成されるキーや、ActionView::Digestorが算出するダイジェストの値は、以前と互換性があります。 - 「キャッシュがどこに保持されるか(Ractor ローカルかどうか)」が変わっただけで、「何をキーにどういう値を計算するか」は変わっていません。
注意点
- Ractor を前提としたコードを書く場合
- この PR により、「テンプレート lookup はメイン Ractor 以外からも行ってよい」前提に近づきました。
- ただし、Ractor 内で自前に
Concurrent::Mapを使う場合は、concurrent-ruby >= 1.3.8が必須である点に注意する必要があります。
- グローバルなキャッシュへの依存を避ける
- もしアプリやエンジン側で、Action View の内部キャッシュに依存したハック(内部の
Concurrent::Mapインスタンスを直接参照するなど)をしていた場合、その前提は崩れます。 - キャッシュに依存する際は、基本的に公開 API(
ActionView::LookupContextやActionView::Digestorなど)を通じて行うべきです。
- もしアプリやエンジン側で、Action View の内部キャッシュに依存したハック(内部の
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58281
- Ractor と共有オブジェクト:
- Ruby 3 以降、Ractor 間で共有できるのは「immutable (frozen) で、かつ shareable としてマークされたオブジェクト」に制限される。
Concurrent::Mapや内部 sentinel オブジェクトがこの条件を満たさないと IsolationError が発生する。
- concurrent-ruby 1.3.8 の変更点(
Concurrent::NULLの freeze)により、Concurrent::Map自体が Ractor セーフに近づいたが、本 PR ではさらに「Ractor ローカルキャッシュ」という設計で Isolation 問題を避けている。
#58373 Honour prepared_statements on rails main
マージ日: 2026/8/5 | 作成者: @camallen
- 概要 (1-2文で)
Rails の ActiveRecord において、prepared_statements設定やunprepared_statementブロックを無視して常にプリペアドステートメントが使われてしまう回 regress を修正し、prepared_statements設定が正しく尊重されるようにした PR です。PostgreSQL かつトランザクションプーリングプロキシ(例: PgBouncer transaction mode)配下で名前付き PREPARE が壊れる問題を解消します。
- 変更内容の詳細
問題の背景
select_allが呼び出し元から渡されたpreparable:オプションを、そのままアダプタにprepare:として渡していた。StatementCache#executeは常にpreparable: trueを渡す実装になっている。to_sql_and_bindsは、クエリがStringのときはpreparable:を変更せずに通す。
この結果:
Model.findやModel.find_byなど、StatementCache 経由のクエリは常に「プリペアドにできる(preparable: true)」として扱われる。- さらに、その値が
select_allからアダプタに「そのまま」伝搬してしまうため、「Rails の設定としてprepared_statementsが無効化されているかどうか」に関係なく、PostgreSQL アダプタが常にプリペアドステートメント(PREPARE)を発行してしまっていた。
具体的には、PostgreSQL がクエリごとに「名前付き PREPARE」を行い:
PREPARE "a_unique_name" AS SELECT ...;
EXECUTE "a_unique_name"(...)という動きをし続けるため、トランザクションプーリングプロキシ配下(バックエンド接続がリクエストごとに入れ替わるような構成)では:
- PREPARE を発行した接続と
- EXECUTE を実行する接続
が異なってしまい、「その名前の PREPARE は存在しない」となって壊れる、という問題が生じます。
実際の修正内容
activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb の select_all(もしくはその近辺)で、元々存在していた以下のような条件:
prepare: prepared_statements && preparableに相当するロジックが、あるコミット(1277a8f09c)で落ちてしまっていたのを復元しています。
今回の修正でのポイント:
- 「呼び出し元が
preparable: trueと言っているか」だけでなく - 「接続の
prepared_statements設定(実体は動的なprepared_statements?メソッド)」
を掛け合わせて、「本当にプリペアドステートメントを使うべきか」を判定するように戻しています。
擬似コードで表すと:
# 修正後イメージ
adapter.select_all(
sql,
name,
prepare: prepared_statements? && !!preparable
)という形に近い動きになります。
ここで prepared_statements? は:
- 接続オプション
prepared_statements: falseが指定されている場合はfalseを返す。 unprepared_statementブロック中では、一時的にfalseを返すようになっている。
そのため、以下の双方のケースで確実にプリペアドステートメントが無効になります:
- 接続自体で恒久的に
prepared_statements: falseを設定したい場合 - 一時的にプリペアドステートメントを避けたい箇所を
unprepared_statementブロックで囲む場合
# 1. 接続オプションで完全にオフ
ActiveRecord::Base.establish_connection(
adapter: 'postgresql',
prepared_statements: false,
# ...
)
# 2. 処理の一部だけオフ
User.connection.unprepared_statement do
User.find(1) # ここでは PREPARE されない
endテスト追加
activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rbが追加され、以下のようなケースがテストされています(内容の要旨):prepared_statements: falseな接続でfindなどを実行しても、プリペアドステートメントが利用されないこと。unprepared_statementブロック内ではプリペアドステートメントを使わないこと。- 上記が PostgreSQL アダプタで正しく動作すること。
これにより、「preparable: が true でも、prepared_statements? が false なら実際には PREPARE しない」動作が回 regres していないことが検証されています。
- 影響範囲・注意点
- 対象:
- 主に PostgreSQL アダプタを使っているアプリケーション
- 特に、PgBouncer の transaction モードなど、トランザクション単位で接続が入れ替わるプーリングプロキシ配下で Rails を動かしている環境
- 影響:
- これまで「
prepared_statements: falseを設定しているのに、実際には PREPARE/EXECUTE が出ている」という状況だった場合:- この PR 適用後は、本当に PREPARE が出なくなるため、DB 側のログやパフォーマンス特性が変わる可能性があります(本来そうあるべき挙動)。
unprepared_statementブロックが、StatementCache 経由のクエリにもきちんと適用されるようになります。
- これまで「
- 注意点:
- もし既に「このバグに依存したワークアラウンド」(例: プロキシ側で PREPARE 名を握りつぶす、など)を実装している場合、この修正で不要になったり挙動が変わる可能性があります。
- パフォーマンス上、プリペアドステートメントに頼っていたコードが、
prepared_statements: false設定のままになっていると、パフォーマンスが低下することはあり得ますが、それは設定の意図に沿った動作です。必要に応じて設定を見直してください。
- 参考情報 (あれば)
- 関連 Issue: #58371
- 回 regress の原因となったコミット:
1277a8f09c(prepared_statements &&が削除されていた) - キーワード:
- ActiveRecord
select_all StatementCache#executeprepared_statements/prepared_statements?unprepared_statement- PostgreSQL + transaction pooling(PgBouncer など)
- ActiveRecord
#58360 Use local fixtures
マージ日: 2026/8/5 | 作成者: @voxik
- 概要 (1-2文で)
このPRは、Railties の Active Storage 関連テストで「グローバルな fixtures ではなく、そのテスト自身が持つローカル fixtures を使う」ように変更したものです。これにより、Railties をフレームワークの他の部分から切り離して単体でテストしやすくなっています。
- 変更内容の詳細
対象ファイルは2つだけで、内容としては「fixture の参照元をローカルに切り替える」というごく小さな修正です。
変更されたファイル:
railties/test/application/active_storage/analyzers_integration_test.rbrailties/test/application/active_storage/direct_uploads_integration_test.rb
どちらも Active Storage の統合テストで、これまで「他の場所にある共通 fixture(グローバル fixtures)」を前提にしていた部分を、「Railties 側に用意された fixtures(ローカル fixtures)」を参照するようにしています。
PR本文で触れられている「This is similar to #54864.」という記述から、#54864 で行われたのと同種のリファクタリング(テストの fixture 参照先を局所化して、コンポーネント単位で完結するようにする)を、Active Storage 関連の統合テストにも適用した、という位置付けだと考えられます。
コードイメージ(擬似例)としては、例えば以下のような変更です(実際のパス名やメソッド名は簡略化しています):
# 変更前(共通fixturesへの依存)
fixtures :all # test/fixtures 以下など、他コンポーネントと共有のfixtures
# 変更後(このテスト配下のローカルfixturesを参照)
fixtures :local_users, :local_attachments # railties/test/... 配下などもしくは、ファイルパスを直接指定する形のテストであれば、
# 変更前
file = file_fixture("images/avatar.png")
# 変更後(railties 側に置いた fixtures を使う)
file = local_file_fixture("images/avatar.png")のように、「グローバルな fixture 参照をやめて Railties 側のローカル fixture に切り替えた」という変更が行われています(上はあくまでイメージです)。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは
railties/test/application/active_storage/*にある統合テストのみです。 - 実行される Rails 本体の動作や API 仕様には変更がありません。
- 変更はテストコードのみであり、アプリケーションコードへの実動影響はありません。
- 影響を受けるのは
意図・メリット
- Railties をフレームワークの他コンポーネント(特に Active Storage 本体側のテスト資産など)から切り離してテストできるようになります。
- fixture がローカルに閉じることで、「他のテストスイートや別コンポーネントの fixtures の変更に巻き込まれて Railties テストが壊れる」といったクロスコンポーネント依存を減らせます。
- 将来的に Railties を単独で切り出してテストしたり、モノレポの構造を変更したりする際の柔軟性が上がります。
注意点
- ローカル fixtures の場所や内容を変更する場合、Railties 側の Active Storage テストがそれに追随しているか確認する必要があります(ただし今回の PR はその依存を整理する方向の変更なので、むしろ依存関係は減っています)。
- 他コンポーネントで共通 fixture を使っていた前提のテストコードを Railties 側に追加する場合は、今後は同様に「ローカル fixtures を使う」方針に揃えるのが望ましいです。
- 参考情報 (あれば)
- 類似PR: #54864(本PRと同種の「ローカル fixtures を使う」ためのリファクタリング)
- Motivation: 「This helps with testing Railties in isolation from other parts of the framework」
→ Railties をフレームワークの他部分から独立してテストしやすくするためのテストインフラ改善 PR であり、バグ修正や新機能追加ではなく、テストの構造改善が主目的です。
#58045 Serialize continuation step cursors through Active Job arguments
マージ日: 2026/8/5 | 作成者: @55728
- 概要 (1-2文で)
Active Job の「継続 (Continuation)」機能で使うステップカーソルを、単なる JSON ではなくActiveJob::Argumentsを経由してシリアライズ/デシリアライズするように変更し、Date,Time,GlobalIDなどのカーソルの型が中断→再開後も維持されるようになりました。これにより、再開後にカーソルがString化してメソッド呼び出しに失敗する問題が解消されます。
- 変更内容の詳細
背景: これまでの問題
Active Job の Continuation (継続) では、ジョブの実行ステップを分割し、「中断 → 後で再開」を行うために「カーソル (cursor)」を継続データに保存しています。
従来実装では、このカーソルを「生の JSON」として書き出して/読み出していました。そのため:
Date,Time,ActiveSupport::TimeWithZone,GlobalIDなど「Active Job 的にはシリアライズ可能だが JSON 自体は知らない型」は、一度中断を挟むとStringに変換されて戻ってくる- その結果、カーソルを想定した型として扱うコードが壊れる
例:
# 1回目の実行時
step.cursor # => #<Date: 2026-07-06>
# 中断 → 再開後
step.cursor # => "2026-07-06"
step.cursor.beginning_of_day # => NoMethodError (String にそんなメソッドはない)ドキュメント上は「カーソルは Active Job でシリアライズ可能な任意のオブジェクトを使える」とされているため、実装とドキュメントが乖離していました。
今回の修正内容
1) カーソルのシリアライズ経路を ActiveJob::Arguments に変更
主な変更点は「カーソルの読み書きに使う API」です。
これまで:
- 継続データへの保存: カーソルをそのまま JSON にエンコード
- 継続データからの復元: JSON をパースした結果をそのままカーソルに使う
これを:
- 保存時: カーソルを
ActiveJob::Arguments.serializeに通してから保存 - 復元時:
ActiveJob::Arguments.deserializeを通してからカーソルにセット
というフローに変更しています。
ActiveJob::Arguments は、ジョブ引数に対してすでに行われている「型付きシリアライズ」の仕組みで、例えば:
Date,Time,TimeWithZoneGlobalID/SignedGlobalIDActiveRecordなどのモデル (GlobalID 経由)- それらを含む配列やハッシュ
といった「Active Job がサポートする型」を、安全にシリアライズ/デシリアライズしてくれます。
そのため、今回カーソルもこの経路に乗せることで、中断→再開の往復後も型が維持されます。
# 修正後
step.cursor # => #<Date: 2026-07-06> # 中断→再開後も Date のまま
step.cursor.beginning_of_day # => OK2) ActiveJob::Attributes との整合性
ActiveJob::Attributes はすでに値の永続化に Arguments.serialize / deserialize を使っており、今回の変更はカーソルの取り扱いをこれと揃えるものです。
- 「ジョブ属性」と「継続カーソル」の両方が、同じシリアライザ (
ActiveJob::Arguments) によって扱われる - ドキュメントの「カーソルは Active Job でシリアライズ可能なオブジェクトを受け付ける」という記述とも一致
3) テストの修正
既存テストの一つが、「シリアライズ不可能なオブジェクト」をカーソルに入れた継続データを直接構築していました。
しかし、新しい実装では:
- カーソルは「Active Job 引数としてシリアライズ可能なもの」に限定される
- 「シリアライズ不可能なオブジェクト」を使うのは実際の利用パターンと異なる
ため、このテストは nil をカーソルとして使う ように変更されています。nil は Active Job 的に正しいシリアライズ対象であり、実際の契約に沿ったケースです。
変更された主なファイル
activejob/lib/active_job/continuable.rb- 継続ステップ/カーソルの保存・復元で
ActiveJob::Argumentsを使用するロジックを追加
- 継続ステップ/カーソルの保存・復元で
activejob/lib/active_job/continuation.rb- 継続データのシリアライズ/デシリアライズ経路を調整
activejob/test/cases/continuation_test.rb- 新しいカーソルシリアライズ仕様に沿ったテストを追加・修正
activejob/CHANGELOG.md- 上記仕様変更についてのエントリを追加
activejob/test/cases/structured_event_subscriber_test.rb- 継続まわりの仕様変更に合わせた微修正
- 影響範囲・注意点
互換性
- プリミティブ型のカーソル (
Integer,String, それらの配列など):Arguments.serialize/deserializeを通しても「バイトレベルで同一」となるよう配慮されており、既存の継続データとの互換性は保たれます。- 既存の JSON ベース継続データも、そのまま読み込めます。
- 非プリミティブ型 (
Date,Time,GlobalIDなど):- 従来は
Stringになってしまっていたものが、本来の型で復元されるようになります。 - 型に依存したロジック (例:
cursor.beginning_of_day) がようやく仕様通りに安全になります。
- 従来は
実装側で意識すべき点
- カーソルとして使える値は、Active Job がサポートするシリアライズ可能な型に限定されます。
- カスタムクラスをそのままカーソルに使いたい場合は:
- Active Job の引数としてシリアライズ可能になるよう
GlobalID等を導入するか、 - カーソル用に「シリアライズ可能な情報」のみに変換して保存する (id や string 等) 必要があります。
- Active Job の引数としてシリアライズ可能になるよう
- カスタムクラスをそのままカーソルに使いたい場合は:
- もし以前から「中断後は String に変わる」ことを前提にしていたコードがある場合 (かなり例外的なケースですが) は、今回の変更で逆に型が変わる可能性があります。
- 例:
step.cursorがDateからStringになる前提でto_dateしていた、など - そのようなコードは削除/簡略化できるはずです。
- 例:
デバッグ・運用上のメリット
- 継続のカーソルが引数と同じ経路でシリアライズされるようになったため、シリアライズエラーの挙動が統一される:
- ある型がジョブ引数としてシリアライズ不可能なら、カーソルとしても不可能
- 問題切り分けが容易になる
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58045
- 関連 API:
ActiveJob::Arguments.serializeActiveJob::Arguments.deserializeActiveJob::Continuation/ActiveJob::Continuable
- コンセプト的には、Active Job の「属性 (
ActiveJob::Attributes)」と「継続カーソル」の扱いが統一された変更であり、今後は「Active Job でシリアライズ可能 = カーソルとしても安全に使える」と考えて実装できます。
#58372 Pluck from the target when the owner is a new record
マージ日: 2026/8/5 | 作成者: @djmb
- 概要 (1-2文で)
Relation#idsがpluck委譲に変わったことで、新規レコードに紐づく未保存の関連に対するidsが空配列になってしまった問題を修正し、ids/pluckともに関連の「target」(メモリ上のオブジェクト)から値を取得するようにした変更です。
これにより、新規レコードに関連づけたオブジェクトに対しても、idsやpluckが直感的な結果を返すようになります。
- 変更内容の詳細
背景
以前の変更(rails/rails@56c95aee)で、Relation#ids が内部的に pluck を使うようになりました。
関連に対して呼ぶと、実体は CollectionProxy#pluck が使われます。
post.tags.ids # 実際には post.tags.pluck(primary_key)ところが、新規レコード (Post.new) に対する関連は DB にはまだ存在せず、「null scope」(空のリレーション)を読む挙動になっていたため、ids が以下のように変化してしまいました。
post = Post.new
post.tags = [tag]
# 56c95aee 以前
post.tags.ids # => [1]
post.tags.pluck(:name) # => []
# 56c95aee 以後
post.tags.ids # => [] # ← ここが変わってしまった
post.tags.pluck(:name) # => []つまり、ids は「target(メモリ上に入っている関連オブジェクトの配列)」を見ていたのに、pluck に委譲したことで「null scope(DB クエリ)」を見るようになり、ids の挙動が破壊的に変わってしまっていました。
この PR での修正内容
この PR では、新規レコードの関連に対しては CollectionProxy#pluck が「null scope」ではなく「target」を読むように変更しています。
その結果:
post = Post.new
post.tags = [tag] # tag.id == 1, tag.name == "ruby"
post.tags.ids # => [1]
post.tags.pluck(:name) # => ["ruby"]つまり
idsも- 任意のカラムを指定する
pluck(:name)なども
新規レコードに紐づいた未保存の関連オブジェクトを正しく反映した配列を返すようになります。
実装面の変更
activerecord/lib/active_record/associations/collection_proxy.rbpluckの実装を 1 行変更し、「null scope から pluck」ではなく「target から pluck」するように振る舞いを修正
(差分は +1 / -1 でかなりピンポイントな修正)
テスト追加
has_many_associations_test.rbhas_and_belongs_to_many_associations_test.rb
新規レコードに対するhas_many/HABTMの関連で、ids/pluckが target を読むことを検証するテストが追加されています。
activerecord/CHANGELOG.md- この挙動変更が CHANGELOG に追記されています。
- 影響範囲・注意点
影響するケース
主に以下のようなコードに影響します。
post = Post.new
post.tags.build(name: "ruby")
post.tags.ids
post.tags.pluck(:name)これまでは(56c95aee 以降)両方とも [] になる可能性があったものが、この PR 以降は ids / pluck ともに target を基準とした値を返します。
- 56c95aee 前:
ids→ target を読んで[1]pluck→ null scope で[]
- 56c95aee〜この PR まで:
ids→pluck委譲により[]pluck→[]
- この PR 適用後:
ids→ target を読んで[1]pluck(:name)→ target を読んで["ruby"]
互換性上の注意
- 56c95aee 以降の「
idsが空配列を返す」挙動に依存したコードは、この PR で再び壊れます。
ただし PR の説明にもある通り、idsを元の(より直感的な)挙動に戻しつつ、pluckも合わせることで一貫性を取っています。 - 「新規レコードの関連に対する
pluckは常に DB ベースで評価されてほしい」と期待していた場合、挙動が変わるので注意が必要です。
特に、「未保存の関連を無視したい」場合は、自前でwhereを明示的に使うなど、クエリをはっきり分ける必要があります。
例:
# target(メモリ上の未保存オブジェクト)を含めず、DB だけを見たい場合
post.tags.scope.pluck(:name) # など、意図的に Relation を扱う設計上の意味合い
- この PR により、「関連に対する
pluck/idsは、通常のto_a同様に target を反映してくれる」という一貫したモデルになります。 - PR 本文でも触れられている通り、代替案として「
idsだけ特別扱いする」ことも可能でしたが、そうするとidsとその他のpluckベースの API とで挙動がズレるため、この PR では 「pluckの側を target 寄りの挙動にそろえる」という選択が取られています。
- 参考情報 (あれば)
- 当該 PR: https://github.com/rails/rails/pull/58372
- 問題のきっかけとなったコミット: rails/rails@56c95aee
- 関連クラス・メソッド:
ActiveRecord::Relation#idsActiveRecord::Associations::CollectionProxy#pluckActiveRecord::Associations::CollectionProxy#target/#scope/#null_scope
#58374 Ractor safe Active Record primary key
マージ日: 2026/8/5 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Active Record の「主キーオブジェクト」を Ractor 内でも安全に扱えるようにするため、主キーを表すオブジェクトを freeze する変更が入りました。単一 PK・複合 PK・PK なしのすべてのケースで、計算済みの主キーが不変オブジェクトとして扱われるようになります。
- 変更内容の詳細
何をしているか
ActiveRecord::Key(主キーを表現するオブジェクト)生成時に、そのインスタンスをfreezeするように変更。- 単一カラム PK・複合 PK・PK なし(
None)の3種類のキーオブジェクトすべてが対象。 - 主キーは一度計算された後に変更されることがなく(不変)、
freezeすることで Ractor 間で安全に共有できるようにしている。
PR の説明から読み取れるポイント:
- 「Ractor 内からレコードの primary key に安全にアクセスしたい」という要件に対する対応。
- Ractor では「共有オブジェクトは不変(freeze 済み)であること」が必要なため、主キーオブジェクトもそのルールに合わせた。
テスト追加内容
activerecord/test/cases/primary_keys_test.rbに 18 行追加。- 想定されるテスト内容:
- 単一 PK の場合に主キーオブジェクトが
frozen?であること。 - 複合 PK の場合にキーオブジェクトおよび内部構造(配列等)が
frozen?であること。 - PK なしモデル(
None)でもキーオブジェクトがfrozen?であること。
- 単一 PK の場合に主キーオブジェクトが
- これにより、「全タイプの PK で freeze しても問題なく動作する」ことを保証している。
(実際のコードイメージ・擬似例)
# 例: ActiveRecord::Key の生成タイミングで freeze される
key = ActiveRecord::Key.new(primary_key_values)
key.frozen? # => true
# レコード経由での利用イメージ
user = User.find(1)
user.primary_key # => #<ActiveRecord::Key ...> (内部的には freeze 済み)
user.primary_key.frozen? # => true- 影響範囲・注意点
主キーオブジェクトを後から書き換えるようなコードは壊れる
- たとえば、内部的な主キーオブジェクト(複合キーの配列など)に対して:
<<で要素を追加するmap!,replaceなどの破壊的メソッドを呼ぶ
- といったコードがあると、freeze により
FrozenErrorが発生します。 - そもそも主キーは論理的に不変であるべきものなので、こうしたコードがあれば元々設計的に危ういものです。この PR をきっかけに見直すべき箇所といえます。
- たとえば、内部的な主キーオブジェクト(複合キーの配列など)に対して:
通常の Active Record 利用に対する影響はほぼゼロ
- 典型的な利用(
record.id,record.to_param,find,where(id: ...)等)は主キーオブジェクト自体を直接書き換えないため、振る舞いに変化はありません。 - 内部表現のオブジェクトが freeze されたことによる副作用が出るのは、主キーオブジェクトを直接保持してミューテーションしているような特殊なコードに限られます。
- 典型的な利用(
Ractor を使う並行実行環境でのメリット
ActiveRecord::Base#primary_key/ 主キー関連の内部表現を Ractor 間で共有しても安全になります。- Ractor 内で
record.primary_keyに触れるようなユースケースでも、Ractor の「共有は不変オブジェクトのみ」ルールを満たせます。
- 参考情報 (あれば)
Ractor の制約(Ruby 本体):
- 共有可能オブジェクト: freeze 済みオブジェクト、Ractor ローカルに生成されたオブジェクトなど
- 参考: Ruby 3 の Ractor 仕様(公式ドキュメントや Matz の発表など)
Active Record の主キー関連コード:
activerecord/lib/active_record/key.rb(今回の変更対象)activerecord/test/cases/primary_keys_test.rb(freeze 保証のテストが追加された場所)
この PR は、「主キーオブジェクトは不変である」という元々の前提をコードレベルで明示しつつ、Ractor 対応も同時に進めるための小さく安全な変更といえます。
#58375 Add NUL (\0) to the list of unsafe characters for storage
マージ日: 2026/8/4 | 作成者: @rosa
- 概要 (1-2文で)
Active Storage のファイル名サニタイズ処理において、NUL 文字 (\0) を「危険な文字」として明示的に除去するようにした変更です。
これにより、ユーザーアップロード由来のファイル名に NUL バイトが含まれている場合でも、ArgumentError: string contains null byteなどの例外が発生しにくくなります。
- 変更内容の詳細
背景
- ユーザーがアップロードするファイル名に NUL 文字 (
\0) が紛れ込むケースがある。 - そのファイル名を
File.joinなど、Ruby 標準ライブラリのファイル操作メソッドに渡すと、Ruby 側でArgumentError: string contains null byteが発生することがある。 - Active Storage では
ActiveStorage::Filename#sanitizedでサニタイズを行っているが、これまで NUL 文字は文字列の先頭/末尾にある分だけString#stripによって偶然取り除かれており、「途中」に含まれる NUL 文字は残っていた。
今回の変更
ActiveStorage::Filename#sanitizedが参照する「unsafe characters(不正・危険な文字)」の一覧に NUL 文字 (\0) を追加。- これにより、ファイル名中の NUL 文字は位置にかかわらず削除される。
イメージとしては、もともとこんな感じの正規表現(例)で unsafe 文字を除去していたところに、\0 が追加されたという変更です(実際のコードイメージ):
# 変更前(イメージ)
UNSAFE = /[\/\\:*?"<>|]+/
def sanitized
name.gsub(UNSAFE, "_").strip
end
# 変更後(イメージ: NUL を追加)
UNSAFE = /[\/\\:*?"<>|\0]+/
def sanitized
name.gsub(UNSAFE, "_").strip
end※上記は概念的な例であり、実際の定数名・正規表現とは多少異なる可能性がありますが、やっていることは「unsafe 文字の集合に \0 を追加する」一点です。
テスト
activestorage/test/models/filename_test.rbが 1 行修正されており、NUL 文字が含まれたファイル名がサニタイズ時に適切に処理されることを検証するテストが追加/調整されています。
- 影響範囲・注意点
影響範囲:
- Active Storage を使っており、かつユーザー由来のファイル名に NUL 文字が含まれる可能性があるアプリケーション。
- 特に、
ActiveStorage::Blob#filenameを元にFile.join,File.open,Pathnameなど OS ファイルパス関連 API を使うコード。
振る舞いの変化:
- 以前: ファイル名の先頭/末尾の NUL は
stripによって消えるが、途中にある NUL は残る → Ruby のファイル API を呼んだタイミングでArgumentErrorが起きることがある。 - 以後: ファイル名中の NUL はすべてサニタイズ対象となり削除される → そのファイル名をそのままファイル API に渡しても、少なくとも NUL バイト起因の
ArgumentErrorは発生しにくくなる。
- 以前: ファイル名の先頭/末尾の NUL は
後方互換性 / 破壊的変更の懸念:
- 一般的なファイルシステムでは NUL 文字はそもそもファイル名として無効なので、「真に有効なファイル名」を破壊する変更ではありません。
- もしアプリケーション内部で「NUL 文字を含むファイル名文字列」をあえて保持していた場合は、その NUL が今後はサニタイズ処理で削除される点に注意が必要です(ただし、通常の利用形態では問題にならないはずです)。
- 参考情報 (あれば)
- Ruby の
ArgumentError: string contains null byteは、C 側の文字列処理で NUL 終端を前提にしているため、バイナリ中に NUL が含まれているとファイル名などとして扱えずエラーになることが多いです。 - 多くのファイルシステム(UNIX, Windows 含む)では、NUL 文字はファイル名に使えない予約文字となっており、今回の変更はその制約に沿ったサニタイズ強化といえます。
#58363 Make Proc layouts shareable
マージ日: 2026/8/4 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Rails のレイアウトとして利用されるProcオブジェクトを、Ractor 対応のために「shareable(共有可能)」として扱うようにした変更です。これにより、特に turbo-rails が提供する Proc ベースのレイアウト定義などで、Ractor 実行時の問題を早期に検出しやすくなります。
- 変更内容の詳細
何が変わったか
対象ファイル: actionview/lib/action_view/layouts.rb
レイアウトが Proc で指定されている場合に、その Proc を ActiveSupport::Ractors.try_shareable_proc を使って「共有可能な Proc」に変換(あるいはチェック)する処理が追加されました。
イメージとしては、従来:
# 擬似コードイメージ
if layout.is_a?(Proc)
@layout = layout
endだったところが、以下のような形に近づいています:
if layout.is_a?(Proc)
@layout = ActiveSupport::Ractors.try_shareable_proc(layout)
end※正確なメソッド名や呼び出し位置は PR 本体に依存しますが、やっていることは「レイアウト用 Proc に対して try_shareable_proc を適用する」です。
ActiveSupport::Ractors.try_shareable_proc は、
- 渡された
Procを Ractor 間で共有可能な形にできるならそのように扱い、 - 共有不可能(=中で Ractor 非共有オブジェクトをキャプチャしている等)の場合は例外・エラーを発生させる/共有不可であることを示す
といった性質のヘルパです。
テストの追加
対象ファイル: actionview/test/actionpack/abstract/layouts_ractor_test.rb
Ractor 環境でレイアウトが正しく動作するか、あるいは問題を適切に露呈するかを確認するテストが 46 行分追加されています。
具体的には:
- Proc レイアウトを使うコントローラを Ractor 内から呼び出した場合に、レイアウト Proc が
try_shareable_procを通して扱われること - 共有可能な Proc と共有不可能な Proc のケースを分けて検証すること
などが確認されていると考えられます。
- 影響範囲・注意点
影響範囲
- Proc レイアウトを利用しているアプリケーション全般
- コントローラで
layout ->(controller) { ... }のように Proc を返すレイアウトを定義している場合が対象になります。
- コントローラで
- 特に turbo-rails のフレーム用レイアウト
turbo-railsでは、turbo frame 用に Proc ベースの layout 定義が使われており(PR 説明でリンクされている箇所)、Ractor 環境下でこの変更の影響を受けます。
何が変わるのか(挙動面)
- Ractor を使っていない(通常のシングルスレッド/マルチスレッド)環境では、挙動は基本的に従来と変わりません。
- Ractor を利用する場合:
- レイアウト用の Proc が Ractor 共有不可なオブジェクトをキャプチャしていると、
try_shareable_procの段階で問題が顕在化します。 - これにより、「実際のリクエスト処理中に謎の Ractor 関連エラーが出る」のではなく、「アプリケーション起動や Ractor セットアップ時点で原因が分かる」形に近づきます。
- レイアウト用の Proc が Ractor 共有不可なオブジェクトをキャプチャしていると、
注意点(開発者が見るべきポイント)
Proc レイアウトの中身が Ractor 共有可能かを意識する必要がある
- 例: レイアウト決定ロジックで、
- クラス定数・シンボル・凍結済み文字列などの shareable なオブジェクトを使う分には問題ないが、
- コントローラインスタンスのミューテーブルな状態や、非 shareable なオブジェクトを閉じ込めているとエラーになる可能性があります。
- 典型例:ruby
# 悪い例になりうる(イメージ) SOME_HASH = {} layout ->(controller) { SOME_HASH[controller.request.format] } # => ミューテーブルな Hash をキャプチャしているため共有不可になりうる
- 例: レイアウト決定ロジックで、
Ractor サポートを試すときに、問題を早めに見つけられる
- この PR の意図として、「アプリオーナーが Ractor サポートを試した時に、問題を直接(Proc レイアウト定義の箇所で)見つけられるようにする」という狙いがあります。
- つまり、Ractor 対応に向けてアプリを修正する際、Proc レイアウトが早期にチェックされるようになります。
turbo-rails 側の対応 PR との関係
- turbo-rails 側でも、「Proc レイアウトをメソッドに置き換える」PR (#786) が既に存在し、こちらも合わせて導入されると、turbo-rails のフレームレイアウトに関する Ractor 問題はさらに軽減される見込みです。
- ただし、Rails 本体としては Proc レイアウトをサポートし続けるため、その扱いを Ractor 観点でも安全にするという位置付けです。
- 参考情報 (あれば)
- 該当 PR:
- 言及されている turbo-rails の Proc レイアウト実装:
- turbo-rails 側の関連 PR(Proc をメソッド化する案):
- Ractor / shareable オブジェクトの概念(Ruby 本体リファレンス):
- https://docs.ruby-lang.org/ja/latest/doc/spec=2fractor.html (日本語版が追いついていない場合は英語版 Ractor ドキュメント参照)
#58362 Freeze ActionView::Template::Handlers::ERB.escape_ignore_list
マージ日: 2026/8/4 | 作成者: @Edouard-chin
- 概要 (1-2文で)
ActionView::Template::Handlers::ERB.escape_ignore_listをアプリ起動時に freeze し、Ractor 内からも安全に参照できるようにした PR です。これにより、マルチ Ractor 環境での ActionView/ERB の設定利用がしやすくなります。
- 変更内容の詳細(あればサンプルコードも含めて)
何をしているか
ActionView::Template::Handlers::ERB.escape_ignore_listを Rails 起動時に freeze する処理を追加- それに対応するテストを
railties側に追加
変更ファイル:
actionview/lib/action_view/railtie.rbrailties/test/application/configuration_test.rb
具体的なコードイメージ
ActionView::Railtie の初期化プロセスのどこかで、
ActionView::Template::Handlers::ERB.escape_ignore_list.freezeのような処理が行われるようになっています(実際には config.after_initialize など、Rails のブートシーケンスに沿った場所)。
テスト側では、おおよそ次のようなことを検証しています:
app "some_app" do
# アプリ起動
end
assert_predicate ActionView::Template::Handlers::ERB.escape_ignore_list, :frozen?背景・モチベーション
- Ractor では「共有できるオブジェクト」は原則イミュータブル(freeze 済みなど)である必要があります。
escape_ignore_listは ERB のエスケープ処理において「エスケープしない対象(メソッド名など)」を定義するための Public API の設定配列。- Ractor 内でこの配列にアクセスしたいが、現状 freeze されていないために共有オブジェクトとして扱えない、という制約があった。
- 実運用上、この配列をランタイム中に書き換えるケースはほぼなく、多くは「ブート時に一度だけ設定」されるスタイルで使われていることが GitHub 検索で確認されている。
このため、「起動後は不変である」という前提を明示するためにも freeze するのが妥当、という判断です。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは、
ActionView::Template::Handlers::ERB.escape_ignore_listを アプリケーション起動後に変更しようとしているコード です。 - 典型的な使われ方は以下のように「初期化時だけ変更」するパターンであり、これは今後も問題ありません:
# config/initializers/erb_escape.rb
ActionView::Template::Handlers::ERB.escape_ignore_list << :raw_htmlRails 起動プロセス中(初期化フェーズ)の段階で freeze されるため、
- Rails 推奨の「initializer で設定する」スタイルなら基本的に動作します。
- 本番稼働中に動的にこの配列を書き換えるようなコードは、freeze によって
FrozenErrorが発生するようになります。
注意点
- もし以下のようにリクエスト処理中やジョブ実行中など、遅いタイミングで書き換えをしている場合はエラーになります:
# NG(今後は FrozenError)
def add_escape_ignore_at_runtime
ActionView::Template::Handlers::ERB.escape_ignore_list << :some_method
endその場合は:
- 起動時(initializer / Railtie / Engine の
config.before_initializeなど)に設定を移動する - あるいは
escape_ignore_listを直接いじらず、自前のヘルパー / wrapper を設計する
といった対応が必要です。
- Ractor を利用している / する予定のアプリにとっては:
- この配列を Ractor に渡したり、Ractor から参照したりすることが可能になり、並行実行時の安全性・利便性が上がります。
- 参考情報 (あれば)
- この設定は Public API として扱われており、ドキュメントも #58359 で再導入予定とされています。
- Ractor での共有オブジェクト要件(イミュータブルであること等)は Ruby 本体の仕様に依存します。
参考: Ruby の Ractor ドキュメント (doc/ractor.mdなど)
#58368 Use ActiveRecord::Key to unify single / composite FK handling in BelongsToAssociation
マージ日: 2026/8/4 | 作成者: @kamipo
- 概要 (1-2文で)
belongs_to関連の内部実装で、単一外部キーと複合外部キーの扱いをActiveRecord::Keyに統一し、is_a?(Array)やArray(...)などの分岐を排除してコードを簡潔・一貫化した変更です。これにより、BelongsToAssociation(および多態関連)で複合キーを扱うための基盤が整理され、保守性と読みやすさが向上しています。
- 変更内容の詳細
全体方針
reflection.foreign_key(単一キー or 複合キー)を都度判定せず、- PR #58077 などで導入されたと思われる
ActiveRecord::Keyを使って- 列挙 (
Enumerable) - 値の取得 (
value_of(record_or_hash)) - 複合キー判定 (
composite?)
- 列挙 (
- を一元的に扱うようにした変更です。
これにより、「foreign_key が配列かどうかを毎回チェックする」スタイルから、「Key オブジェクトに聞く」スタイルへ移行しています。
1) BelongsToAssociation の変更
対象: activerecord/lib/active_record/associations/belongs_to_association.rb
主なポイント:
reflection.foreign_keyをそのまま使う代わりに、初期化時にActiveRecord::Key.for(reflection.foreign_key)を計算して@foreign_keyにキャッシュ。- 以降、外部キーに関する操作は 全て
@foreign_key経由で行います。
典型的な差分イメージ:
Before(イメージ):
def replace_keys(record)
if reflection.foreign_key.is_a?(Array)
reflection.foreign_key.each do |key|
owner[key] = record ? record.send(key) : nil
end
else
owner[reflection.foreign_key] = record ? record.send(reflection.association_primary_key) : nil
end
endAfter(イメージ):
def foreign_key
@foreign_key ||= ActiveRecord::Key.for(reflection.foreign_key)
end
def replace_keys(record)
foreign_key.each do |key|
owner[key] = record ? foreign_key.value_of(record)[key] : nil
end
end※ 実際のコードはもう少し複雑ですが、構造としては「Array 判定をやめて Key オブジェクトの API だけで処理する」という変更です。
ActiveRecord::Key の想定 API:
Key.for(value)- 引数がシンボル/文字列/配列などでも、常に
ActiveRecord::Keyインスタンスを返すファクトリ。
- 引数がシンボル/文字列/配列などでも、常に
each/include?などのEnumerableインターフェースcomposite?- 外部キーが複数カラム(配列)かどうか。
value_of(record_or_hash)- 渡されたレコードや Hash から、そのキーに対応する値(複合キーなら複数)をまとめて取得。
BelongsToAssociation 内の具体的な影響:
- 外部キーの読み書き、比較、nil チェックなどで、
is_a?(Array)/Array(reflection.foreign_key)のような場当たり的分岐が削減。foreign_key_columns.eachのようなイテレーションが@foreign_key.eachに統一。- 「単一キーか複合キーか」を意識したロジックが
composite?にカプセル化される。
結果として、belongs_to 関連が 単一キー/複合キーの両方を同じコードパスで扱えるように近づいている と考えられます。
2) BelongsToPolymorphicAssociation の変更
対象: activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
多態 belongs_to に対しても同様の整理が入っています。
- 多態関連では
foreign_keyに加えてforeign_typeも扱いますが、そのうち 外部キー部分についてActiveRecord::Keyベースの扱いに変更。 - ここでも
Array(...)などの分岐が削られ、@foreign_key的なActiveRecord::Keyオブジェクトを使って一貫した処理を行うようになっています。
多態かつ複合キーというややこしいパターンを視野に入れた、インターフェースの統一と言えます。
3) BelongsTo ビルダーの変更
対象: activerecord/lib/active_record/associations/builder/belongs_to.rb
belongs_to :foo を宣言した際に内部で使われるビルダーのコードも調整されています。
- ここでも
reflection.foreign_keyを直接配列変換したりArray(...)で包む処理が減り、 - 代わりに
ActiveRecord::Key経由の API を前提とした実装へ。
たとえば、関連付けで:
- デフォルト値の設定
inverse_of判定touchやoptionalオプションでの外部キー利用 などの箇所が、単一キー・複合キーどちらでも Key オブジェクトで扱える前提に整理されていると考えられます。
- 影響範囲・注意点
影響範囲
- 対象となるのは内部実装レベルで、通常のアプリケーションコード(
belongs_to :userなどの宣言や、user.postのような利用方法)は API 的には変わりません。 - 想定される主な効果:
belongs_to/ 多態belongs_toでの内部動作がシンプルになり、今後の複合キーサポートや拡張がしやすくなる。- 単一 FK / 複合 FK 両対応のコードパスが増え、複合主キー/外部キーサポートを強化する布石になっている。
注意点(開発者目線)
内部 API の変更に依存している場合は注意
以下のようなコードを書いている場合、動作に影響が出る可能性があります。- AR の内部クラス(
BelongsToAssociation/BelongsToPolymorphicAssociation)を monkey patch している。 reflection.foreign_keyが配列かどうかを前提に内部の動きを拡張している。belongs_to関連の内部変数を前提にカスタマイズしている。
- AR の内部クラス(
今後、複合キー対応が進むことを前提とした内部設計になっているため、
- 「カスタム関連」や「独自の association クラス」を実装している場合は、
- 将来的に
ActiveRecord::Keyを使うほうが Rails 本体との互換性を維持しやすくなりそうです。
仕様変更というよりリファクタリングに近いため、通常の利用では挙動変化は最小限に抑えられていると考えられますが、
- 複合キーを駆使しているアプリでは、edge case(nil を含む複合 FK、部分的に欠けたキーなど)が扱われ方の変化でバグがあぶり出される可能性があります。
- CI で関連周り(
belongs_to・多態belongs_to)のテストをしっかり回しておくと安全です。
- 参考情報 (あれば)
PR 本体:
https://github.com/rails/rails/pull/58368
(ActiveRecord::Keyの導入背景や同時期の PR を追うと、複合キー周りの設計方針がより明確に把握できます)関連しそうなトピック:
ActiveRecord::Key導入 PR(番号はこの PR の description では明示されていませんが、近い番号の PR を辿ると見つかる可能性が高いです)- 複合主キー・複合外部キー対応に関する議論・PR 群
reflection.foreign_keyの扱いと、多態関連でのforeign_typeの扱い方の違い
この PR 自体は大きな外向き API 変更ではなく、内部実装の整理と今後の複合キー対応強化のための基盤整備と捉えるのが適切です。
#57994 Make Mime types ractor-shareable
マージ日: 2026/8/4 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Mime::Type のレジストリと関連インデックスを「アプリケーションの eager load 完了後に凍結(freeze)」し、Ractor で共有可能な状態にする変更です。これに伴い、eager load 後に MIME type を登録/解除することは非推奨となり、既存の Mime 定数プロキシや ActionView::Base.default_formats が新しいレジストリを正しく参照するように調整されています。
- 変更内容の詳細
2-1. 目的: Mime レジストリを Ractor 共有可能にする
Ruby の Ractor を安全に使うには、共有するオブジェクトがイミュータブル(凍結済み)である必要があります。
この PR では:
- Mime::Type のレジストリとそのインデックス(例:
Mime::LOOKUP,Mime::SET,Mime::EXTENSION_LOOKUPなど)を eager loading のタイミングで freeze - 各 Mime::Type インスタンス自体も、生成時点で freeze された状態 & インスタンス変数も freeze 済みに
- これにより、Mime 関連のデータ構造を Ractor 間で安全に共有可能に
2-2. eager load 完了後の登録/解除を非推奨化
これまで:
Mime::Type.register "application/x-custom", :custom
Mime::Type.unregister :htmlのように、アプリケーション起動後でも自由に MIME type の登録/解除を行えました。
この PR 以降(特に config.eager_load = true な本番環境など)では:
- eager loading 完了後に
register/unregisterを呼ぶと 非推奨警告 (deprecation) が出る - 将来的には、そのタイミングでの変更自体がサポートされなくなる予定
- 最終的にレジストリ更新メソッド
update_registriesは削除される想定
開発者に求められる対応方針:
- MIME type の登録/解除は アプリケーションブート時(eager loading 前) に集約する
- 例:
config/initializers/mime_types.rbなどで定義し、アプリケーションロード時に読み込まれるようにする
- 例:
# config/initializers/mime_types.rb
Mime::Type.register "application/x-custom", :custom2-3. update_registries と DeprecatedObjectProxy の調整
Mime::Type.update_registries は、内部のレジストリオブジェクトを新しい frozen なものに入れ替える処理をしています。
このとき:
- 既存の Mime 関連定数に対して、互換性のために用意されている DeprecatedObjectProxy ベースの定数プロキシが「古いレジストリ」を参照し続けると不整合が起きる
- そこで:
DeprecatedObjectProxyにtarget=セッターを追加update_registries実行後に、これらのプロキシが新しいレジストリオブジェクトを参照するように「差し替え」
これにより:
- 古い定数経由でも常に最新の MIME レジストリ状態が見える
- かつ、内部的には freeze された安全な構造になっている
2-4. ActionView::Base.default_formats の扱い
ActionView では、ActionView::Base.default_formats が Mime.symbols の配列を共有している実装があります(過去 PR #38141 で導入)。
この PR では:
Mime::Type.on_changeという仕組みを使い、レジストリ(およびMime.symbols)の入れ替え時にActionView::Base.default_formatsの参照も更新- ただし、アプリ側で
ActionView::Base.default_formats = ...のように 独自に置き換えられている場合 は、そのまま
これにより:
- デフォルトのまま使っている場合は Ractor 共有可能な mime symbols を自動的に参照
- 独自設定しているアプリはそのまま動き続けるが、その配列が Ractor 共有可能かどうかはアプリ側の実装次第
2-5. その他の変更点(ファイル別の要点)
actionmailer/lib/action_mailer.rb/actionpack/lib/action_dispatch.rb/actionpack/lib/action_dispatch/railtie.rb- フレームワーク初期化フローのどこかのタイミングで MIME レジストリを確定・freeze するよう連携
actionpack/lib/abstract_controller/collector.rb/actionview/lib/action_view/template/types.rb- Mime::Type の変更に追随(インターフェースや定数の参照方法を現行仕様に合わせて調整)
actionpack/test/dispatch/mime_type_test.rb/actionview/test/template/types_test.rb- 新仕様(freeze 後の挙動・非推奨警告・レジストリ再ポイント)が正しく動くかどうかを確認するテストを追加/修正
- 影響範囲・注意点
3-1. 実務的な影響が出やすいケース
- 本番環境などで
config.eager_load = trueかつ:- リクエスト処理中、コンソール実行中、ジョブ実行中など「アプリケーション起動後」に
Mime::Type.register/unregisterを呼んでいる - 特定の initializer を
to_prepareなどで再実行するたびに MIME type を登録し直している
- リクエスト処理中、コンソール実行中、ジョブ実行中など「アプリケーション起動後」に
これらは今後:
- まずは deprecation warning が出る
- いずれ 完全にサポートされなくなる可能性が高い
対策:
- MIME type の登録/解除は、アプリケーションブート時に一度だけ 行われるようにする
- 冪等な initializer にまとめる(複数回実行されないようにする、もしくは register 前に存在チェックを入れる)
3-2. ライブラリ・gem 作者への注意
- Rails アプリに読み込まれた後(eager loading 完了後)に MIME type 登録を行う gem は、将来的に挙動が変わる
- 対応方針:
- Railtie / Engine の
initializerを使い、アプリケーションの eager load 前に登録を済ませる - 必要に応じて
config.to_prepareではなくconfig.before_eager_load/ 通常 initializer を用いる
- Railtie / Engine の
3-3. Ractor を使うユーザにとってのメリット
- Mime::Type 関連のデータ構造が Ractor 共有可能(ractor-shareable)になり、マルチ Ractor 構成で安全に再利用可能
- その結果:
- Ractor 間で MIME registry をコピーする必要が減る
- メモリ使用量削減・パフォーマンス改善が期待できる
- 参考情報 (あれば)
- 当該 PR:
- 既存の
ActionView::Base.default_formatsとMime.symbolsの共有に関する過去 PR: - Ractor と shareable オブジェクトについて(Ruby 公式ドキュメント):
#58326 Define Tags#field_type when the class is loaded
マージ日: 2026/8/4 | 作成者: @etiennebarrie
- 概要 (1-2文で)
ActionView::Helpers::Tags系のfield_typeが「最初に使われたときに初期化される」実装だったため、メイン Ractor 以外で最初のフォームフィールドを描画すると例外になる問題を修正した PRです。
クラスロード時にfield_typeを確定させ、Ractor 間で共有可能なようにfreezeすることで、Ractor 環境でも安全にフォームヘルパを利用できるようにしています。
- 変更内容の詳細
背景
- もともと
TextFieldなどのタグクラスは、field_typeをクラスインスタンス変数に遅延メモ化していました。 - 遅延メモ化は「最初に呼ばれたタイミングで値をセット」するので、
- 最初の呼び出しがメイン Ractor 以外だと、Ractor 間共有できないオブジェクトの書き込みや初期化順序の問題で例外が発生する状況がありました。
- Ractor セーフにするには、
- クラス定義時(ロード時)に
field_typeを確定させる - その値を
freezeして Ractor-shareable にする
という方針が取られています。
- クラス定義時(ロード時)に
TextField クラスの変更
対象ファイル: actionview/lib/action_view/helpers/tags/text_field.rb
主なポイント:
クラスロード時に
field_typeを確定させるクラス本体の中で
field_typeを呼び出しておき、クラス定義時にメモ化させるようにしました。イメージとしては:
rubyclass TextField < Base # クラス本体の評価中に呼ぶことで、クラスロード時にメモ化される field_type end(実際のコードではメモ化ロジックと一緒に整理されているはずです。)
TextField.inheritedの実装TextFieldを継承したサブクラス(匿名でないもの)についても、同じようにクラス定義時にfield_typeを確定させるため、inheritedフックが追加されています。概念的にはこういう処理をしています:
rubyclass TextField < Base class << self def inherited(subclass) super # 匿名クラスでなければ、ロード時に field_type を確定させる unless subclass.name.nil? subclass.field_type end end end endこれにより、例えば以下のような継承クラスでも安全に Ractor から利用できます:
rubyclass CustomTextField < ActionView::Helpers::Tags::TextField # 独自の振る舞いを追加 endfield_typeメモ化時にfreezefield_typeをクラスインスタンス変数にセットする際にfreezeし、Ractor 間共有の制約に適合させています:ruby@field_type ||= super_field_type.freezeこれにより、
@field_typeが変更不能であり、Ractor-shareable とみなされるようになります。
DatetimeLocalField の変更
対象ファイル: actionview/lib/action_view/helpers/tags/datetime_local_field.rb
- 以前は
DatetimeLocalFieldがfield_typeをオーバーライドしていましたが、今回の方針に合わせて、- メソッドオーバーライドではなく
- クラス本体からクラスインスタンス変数を直接設定する形
へと変更されています。
イメージ:
class DatetimeLocalField < TextField
# 以前:
# def field_type
# "datetime-local"
# end
# 今回:
@field_type = "datetime-local".freeze
endこうすることで、TextField 側の「クラスロード時に field_type を確定させる」仕組みと整合しつつ、Ractor セーフな実装になります。
Ractor テストの追加
対象ファイル: actionview/test/template/tags_ractor_test.rb
- Ractor 環境でタグヘルパが正しく動くことを確認するテストが追加されています。
- 具体的には:
- メイン Ractor ではない Ractor 内から
TextField/DatetimeLocalFieldなどのタグクラスを利用してフォームフィールドをレンダリングしても例外にならないこと field_typeのメモ化と共有が期待通りに動くこと
を検証しています。
- メイン Ractor ではない Ractor 内から
- 影響範囲・注意点
主な影響対象:
ActionView::Helpers::Tags::TextFieldとそのサブクラスDatetimeLocalField- Ractor を利用した並行レンダリング環境(view レンダリングを並列化しているアプリなど)
互換性:
- 公開インターフェース(
field_typeの戻り値そのもの)には変化はなく、主に初期化タイミングと内部実装の変更です。 TextFieldやそのサブクラスでfield_typeを独自にオーバーライドしている場合でも、通常はそのまま動作します。- ただし、
field_typeが「Ractor-shareable ではないオブジェクト」(ミューテブルなオブジェクトなど)を返していると、Ractor 利用時に問題が発生する可能性があります。
→String等を返す場合はfreezeされる/することを前提にしておくのが安全です。
- ただし、
- 公開インターフェース(
メモ化タイミングの変化:
- これまで「最初にフィールドがレンダリングされたタイミング」で確定していた
field_typeが、 - 「クラス定義時(=アプリ起動時/autoload 時)」に確定されます。
- 一般的なアプリでは副作用はありませんが、「
field_type決定時に外部状態に依存するようなロジック」を書いている場合は、挙動が変わる可能性があります(そのような書き方は推奨されません)。
- これまで「最初にフィールドがレンダリングされたタイミング」で確定していた
- 参考情報 (あれば)
- Ractor と shareable オブジェクト:
- Ruby 3 系の Ractor では、Ractor 間で共有するオブジェクトは
Ractor.shareable?がtrueとなる必要があります。 - イミュータブルな
String("foo".freeze)などは shareable ですが、ミューテブルなオブジェクトはそうではありません。
- Ruby 3 系の Ractor では、Ractor 間で共有するオブジェクトは
- Rails における Ractor 対応方針:
- メモ化やキャッシュを行うクラスインスタンス変数・クラス変数について、
- 「クラスロード時に初期化する」
- 「
freezeする」
といったパターンで Ractor 対応が行われつつあります。
- メモ化やキャッシュを行うクラスインスタンス変数・クラス変数について、
- 関連しそうなコード:
ActionView::Helpers::Tags::Baseや他のフィールドクラス(CheckBox,RadioButtonなど)も、今後同様の変更が行われる/すでに行われている可能性があります。
#58357 Define DateTimeSelector datetime readers with shareable lambdas
マージ日: 2026/8/4 | 作成者: @etiennebarrie
- 概要 (1-2文で)
ActionView::Helpers::DateHelper::DateTimeSelectorが Ractor 内からも安全に使えるように、year/month/day/hour/min/secのリーダーメソッド定義方法を「Ractor 共有可能な lambda」を使った形に変更した PR です。あわせて、この挙動を検証する Ractor 対応テストが追加されています。
- 変更内容の詳細
何が問題だったか
DateTimeSelector は、select_datetime などで <select> ベースの日付・時刻入力フィールドを描画するときに内部的に使われるクラスです。このクラスは、渡されたオブジェクトから year, month, day, hour, min, sec を取り出すための reader メソッドを define_method で定義していました。
もともとの実装はおおよそ次のようなイメージです:
# 疑似コード: 実際とは多少異なる可能性あり
[:sec, :min, :hour, :day, :month, :year].each do |method|
define_method(method) do
@datetime.public_send(method)
end
endここで define_method に渡しているブロック(do ... end の部分)は、Ractor 的には「Ractor 間で共有できない Proc」となりがちです。そのため、
- Ractor 内から
DateTimeSelectorのyearなどのメソッドを呼ぼうとすると - そのメソッド本体が「共有不可能な Proc をキャプチャしている」扱いになり
Ractor セーフではない状況が発生します。
PR の説明にもあるとおり、根本的な目的は「Ractor から sec min hour day month year を呼べるようにする」ことです。
どう修正したか
方針は「Ractor 共有可能な lambda を使ってメソッドを定義する」に変える、というものです。
Rails では「Ractor 共有可能な lambda」を作る場合、おおよそ以下のようなパターンを使います:
- ブロック内で外部のミュータブルなオブジェクトをキャプチャしない
- 構造的に Ractor.shareable? が true になるような Proc/lambda を使う
今回の PR では、これまで define_method に直接ブロックを渡していた部分を、あらかじめ定義済みの 共有可能 lambda を使って定義する形に切り替えています。イメージとしては以下のような変更です(概念的な例):
# 以前(イメージ)
[:sec, :min, :hour, :day, :month, :year].each do |method|
define_method(method) { @datetime.public_send(method) }
end
# 以後(イメージ: 実際とは細部が異なることがあります)
DATETIME_READER = ->(obj, name) { obj.public_send(name) }
DATETIME_READER.shareable! if DATETIME_READER.respond_to?(:shareable!)
[:sec, :min, :hour, :day, :month, :year].each do |method|
define_method(method, &-> { DATETIME_READER.call(@datetime, method) })
end実際のコードでは、lambda の定義方法や define_method への渡し方など、Ractor 共有可能性を満たすように細かく調整されていますが、ポイントは:
- メソッド本体を構成する Proc を「共有可能なもの」にする
- それを
define_methodに渡してメソッドを定義する
という構造になっている点です。
テストの追加
actionview/test/template/date_helper_ractor_test.rb が新規追加され、Ractor 上で DateTimeSelector を使って日付/時刻 <select> を生成できるかを検証しています。
テストのイメージ:
- Ractor を起動
- その中で
renderあるいはヘルパーメソッド(select_datetime等)を実行 - 例外が発生しないこと、期待した HTML が生成されることを確認
といった形で、「Ractor 内から DateHelper 経由で DateTimeSelector を問題なく呼べる」ことを保証しています。
- 影響範囲・注意点
対象:
ActionView::Helpers::DateHelper::DateTimeSelectorが内部的に使われるすべての date/time<select>ヘルパーselect_dateselect_timeselect_datetimedate_selecttime_selectなど
既存の挙動への影響:
- メソッドの外部インターフェース (
year,month等) は変わっていないため、通常の(シングルスレッド/非 Ractor)環境での振る舞いは基本的に変わりません。 - 主な変更は「内部のメソッド定義手段」であり、HTML 出力内容は変化しないことが前提です(テストで担保)。
- メソッドの外部インターフェース (
Ractor を使っている/使う予定のアプリへの影響:
- これまで Ractor 内で
select_datetime等を使うとエラーになっていた場合でも、この変更により正常に動作することが期待できます。 - Ractor セーフティに関わる変更のため、Ractor を活用しているアプリでは、アップデート後に date/time フォームヘルパー周りのテストを一度通しておくと安心です。
- これまで Ractor 内で
実装パターンへの示唆:
- 「Ractor から呼び出される可能性のある DSL/ヘルパーで
define_method + ブロックを使う場合、共有可能 lambda を検討する」という Rails コアの方針の一例と言えます。 - 同様のパターンをアプリ側や gem 側で使っている場合も、Ractor 対応を意識するなら、
module_evalや shareable lambda でのメソッド定義を検討する余地があります(PR 説明にもmodule_eval案の言及あり)。
- 「Ractor から呼び出される可能性のある DSL/ヘルパーで
- 参考情報 (あれば)
該当 PR:
関連トピック:
- Ruby Ractor と Proc/lambda の shareability
- Rails の Ractor 対応方針(特に ActionView / ActiveSupport のスレッド・Ractor セーフティ対応)
#58352 Allow to access the type of an attribute inside a ractor
マージ日: 2026/8/4 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Active Model / Active Record の属性タイプ情報(attribute_types)を Ractor 内から安全に参照できるようにするため、属性タイプのオブジェクトグラフを凍結(deep freeze)し、暗号化関連タイプのメモ化ロジックを Ractor セーフな形に修正した PR です。これにより、Ractor 内で属性の型を取得しても分離エラーが発生しないようになります。
- 変更内容の詳細
背景: なぜ問題だったか
- Active Model の
attribute_typesはハッシュをメモ化しており、そのハッシュや内部にぶら下がるオブジェクトが「Ractor シェア可能」になっていませんでした。 - Ractor 間でオブジェクトを共有するには、Ruby の制約上:
- Frozen であること
- その内部に参照しているオブジェクトも含めて、全体として「shareable」であること
が必要です。
- そのため、Ractor 内で
model.attribute_types["some_column"]のように型情報へアクセスしようとすると、Ractor の分離制約によりエラーが発生していました。
対応方針
attribute_typesのハッシュを単にfreezeするだけでは不十分- 中身の Type オブジェクトや、そのさらに内部でメモ化されるオブジェクトも含めて「deep freeze」する必要があります。
Active Model / Active Record のほとんどの型クラスはそのまま凍結可能
- 追加のコード変更なしに凍結できない一部のケース(特に暗号化周り)のみを修正しています。
暗号化 (
ActiveRecord::Encryption) 周りのメモ化修正- 暗号化関連のタイプ・スキーム・キー提供クラスは、lazy メモ化の過程で
nilやfalseなどの「falsy」値をキャッシュする実装になっていた箇所がありました。
- Ruby でよくある「
@memoized ||= expensive_computation」パターンは、expensive_computationがfalse/nilの場合に毎回実行され続けるため、- 「メモ化が完了した安定したオブジェクトグラフ」という状態になりにくく、
- Ractor で共有するために完全に凍結しにくい要因になっていました。
- この PR では、そういった箇所を「早期 return 型のメモ化」に書き換えています。イメージとしては以下のような変更です:
ruby# 変更前(例) def encryption_key @encryption_key ||= compute_key # falsy を返しうる end # 変更後(例) def encryption_key return @encryption_key if defined?(@encryption_key) @encryption_key = compute_key endこれにより、
compute_keyがfalseやnilを返したとしても、一度計算した結果がきちんとインスタンス変数に保持され、再評価が発生しません。
その結果、オブジェクトグラフが安定し、deep freeze 可能になります。- 暗号化関連のタイプ・スキーム・キー提供クラスは、lazy メモ化の過程で
主なファイルごとの変更ポイント
activemodel/lib/active_model/attribute_registration.rbattribute_typesのハッシュおよびその内部の Type オブジェクトを凍結するロジックを追加。- 結果として、
attribute_typesが Ractor 間で共有可能なオブジェクトになります。
activemodel/test/cases/attribute_registration_test.rb- 上記の凍結・Ractor セーフ化が想定通り動作することを検証するテストを追加。
- 具体的には
attribute_typesが frozen であること- 深い部分の Type オブジェクトも shareable であること などを確認していると考えられます(テストコードから Ractor での利用パターンもカバーしているはずです)。
activerecord/lib/active_record/encryption/encrypted_attribute_type.rbactiverecord/lib/active_record/encryption/key_provider.rbactiverecord/lib/active_record/encryption/scheme.rb- これらの暗号化関連クラスで使われているメモ化ロジックを修正。
- 主な内容:
- falsy を返しうるメソッドで
@var ||= ...を使わないようにし、defined?(@var)を使った early return 方式に変更。 - これにより、暗号化 Type / Scheme / Key Provider のオブジェクトグラフが一度確定して以降は変化せず、凍結可能になる。
- falsy を返しうるメソッドで
- 影響範囲・注意点
Ractor を使っていないアプリへの影響
- ほとんどの場合、動作の変化はありません。
- ただし暗号化関連のメモ化ロジックが若干変わるため、
- 「falsy が返ること」を前提に、毎回計算されることに依存していたような特殊なコードがあれば挙動差が出る可能性はあります(通常の利用では考えにくいパターンです)。
Ractor を使っている / 使う予定のアプリへのメリット
- Ractor 内で
Model.attribute_typesや個々の属性型オブジェクトへアクセスしても Isolation エラーにならなくなります。 - Ractor ベースの並列処理で ActiveRecord / ActiveModel を利用する場合の安全性が向上します。
- Ractor 内で
パフォーマンスへの影響
- 型情報・暗号化関連オブジェクトのメモ化を「一度きり確定」させて凍結するため、実行時のオーバーヘッドはむしろ安定方向に働くと考えられます。
- 凍結や eager なメモ化に伴うごく小さなコストはありますが、通常のアプリで問題になるレベルではないはずです。
カスタム Type・暗号化拡張を書いている場合の注意
- 自前で
ActiveModel::TypeやActiveRecord::Encryptionを拡張している場合:- 内部状態が実行中に変化し続けるような設計だと、将来的に Ractor との相性問題が起きる可能性があります。
- この PR の方針(「Type オブジェクトは、生成されたら基本的に不変・凍結されうる」)に沿っているかを確認した方が安全です。
- 自前で
- 参考情報 (あれば)
- Ruby Ractor 仕様(shareable オブジェクトの制約):
- オブジェクトを Ractor 間で共有するには、
Ractor.shareable?がtrueになる必要があり、そのためには frozen かつ内部構造も含めて共有安全である必要があります。
- オブジェクトを Ractor 間で共有するには、
- この PR の意図:
- Active Model / Active Record の「型システム」を Ractor 時代に対応させるための基盤整備的な変更であり、API 仕様追加というよりは、「既存 API を Ractor 互換にする」ための内部実装改善です。
#58364 Honor composite / aliased FKs in belongs_to counter cache, autosave, and has_many :through nullify
マージ日: 2026/8/4 | 作成者: @kamipo
- 概要 (1-2文で)
belongs_toにおけるカウンタキャッシュ、autosave、has_many :throughの:nullify削除処理で、複合外部キー(Array) と エイリアス付き外部キー(仮想属性/別名カラム) が正しく扱われるように修正した PR です。これにより、これらの機能が複合 FK / エイリアス FK 環境でもサイレントに無視されたり、間違った SQL を生成したりしなくなります。
- 変更内容の詳細
背景となる問題点
reflection.foreign_key は、以下のようなケースで問題を起こしていました:
複合 FK:
[:foo_id, :bar_id]のような配列になる
→ そのままto_sされて"[:foo_id, :bar_id]"のような文字列になり、- WHERE 条件
- UPDATE の
SET句
などで不正な SQL を生成していた
エイリアス FK: 仮想属性 ⇔ 実カラム間でトラッキングに使うキーがズレる
→ 属性変更検知 (will_save_change_to_attribute?) が効かず、
autosave が動かない / 関連更新がスキップされる 等
この PR では、それぞれの箇所で 複合 FK / エイリアス FK を前提にした処理に書き換えています。
2-1. BelongsToAssociation のカウンタキャッシュ
対象: activerecord/lib/active_record/associations/belongs_to_association.rb
問題
belongs_to :author, counter_cache: true のような関連で、
対象側に複合 / エイリアス FK がある場合、内部のカウンタキャッシュ更新処理が:
reflection.foreign_keyをそのまま 1 カラム前提で使用- 複合 FK の場合、
["col1", "col2"]のような文字列として扱われ、条件の組み立てに失敗 - 実質的に no-op (カウンタが更新されない) になっていた
修正内容
複合 FK に対応するために:
- 各 FK ごとにループして処理するように変更
- WHERE 句の構築に
ActiveRecord::Key#where_hashを使用し、col1=.. AND col2=..のような複数カラム条件を正しく生成
エイリアス FK に対応するために:
- 初期化時に解決した
@foreign_keyを使うことで、
「仮想属性名」と「実カラム名」のズレを吸収
- 初期化時に解決した
イメージとしては、従来:
# 擬似コード
where(foreign_key => previous_value) # foreign_key が配列でも to_s されてしまうだったものが、複合キー前提で:
# 擬似コード (イメージ)
keys = Array(reflection.foreign_key) # => [:foo_id, :bar_id]
where_hash = ActiveRecord::Key.new(owner, keys).where_hash(previous_values)
# => { foo_id: 1, bar_id: 2 }
scope.where(where_hash) # 正しい複合 WHERE が組まれるのように動くようになった、という形です。
2-2. AutosaveAssociation の _record_changed?
対象: activerecord/lib/active_record/autosave_association.rb
問題
accepts_nested_attributes_for や関連オブジェクトの autosave 判定で使われる_record_changed? 内で:
reflection.foreign_keyを 1 つの属性名である前提でrecord.will_save_change_to_attribute?(foreign_key)を呼んでいた- 複合 FK の場合:
"[:foo_id, :bar_id]"という存在しない属性名を見てしまい、will_save_change_to_attribute?が常にfalseを返す → 変更なしと判定される - エイリアス FK の場合: 「仮想属性名」と「実カラム名」が食い違い、
本当は変更されているのに検知されない
→ 結果として、 関連レコードに対する autosave が行われない / 実行されない ケースが発生。
修正内容
複合 FK に対応:
- FK を配列として扱い、各キーごとに
will_save_change_to_attribute?を呼ぶ - どれか 1 つでも変更されていれば「変更あり」と判定
- FK を配列として扱い、各キーごとに
エイリアスに対応:
- ループ内で、実際にトラッキングされている属性名に解決したうえで
will_save_change_to_attribute?を呼ぶ (alias 解決をインラインで行う)
- ループ内で、実際にトラッキングされている属性名に解決したうえで
擬似イメージ:
def _record_changed?(record, reflection)
Array(reflection.foreign_key).any? do |fk|
real_attr = resolve_alias(record, fk) # 仮想属性 → 実フィールド名
record.will_save_change_to_attribute?(real_attr)
end
endこれにより、複合キー / エイリアスを利用した関連でも、
「FK が変わったなら関連を保存する」という autosave の期待動作が守られます。
2-3. HasManyThroughAssociation の :nullify 削除処理
対象: activerecord/lib/active_record/associations/has_many_through_association.rb
問題
has_many :through で関連を削除する際に :dependent => :nullify を指定しているとき、
中間テーブルの外部キーを NULL にするための UPDATE を組み立てる部分で:
reflection.foreign_keyをそのまま 1 カラムとしてSET句に使用- 複合 FK の場合:
UPDATE ... SET "[\"col1\", \"col2\"]" = NULLのような不正な SQL が生成 - エイリアス FK の場合:
"aliased"のように仮想属性名で SET しようとしてしまい、実カラムとずれる
修正内容
複合 FK の展開:
- 複合 FK を「カラム => NULL」の Hash に展開して渡すように変更
- 例:
[:foo_id, :bar_id]→{ foo_id: nil, bar_id: nil } - ActiveRecord/ Arel がこれを使って
SET "foo_id" = NULL, "bar_id" = NULLを生成できるようにする
エイリアス解決:
Arel::Table#[]を介してキーを解決することで、
各スカラーキーごとにエイリアスを正しく解決し、適切な実カラムに NULL をセット
擬似イメージ:
# 以前 (問題あり)
relation.update_all(reflection.foreign_key => nil)
# 以後 (修正後のイメージ)
attrs = Array(reflection.foreign_key).index_with { nil } # { foo_id: nil, bar_id: nil }
relation.update_all(attrs) # Arel::Table#[] が alias を解決これにより、has_many :through, dependent: :nullify が
複合キー・エイリアスキーを使う中間テーブルでも正しく動作します。
2-4. テストの追加
以下のテストが追加され、挙動が担保されています:
activerecord/test/cases/counter_cache_test.rb(+34)- belongs_to のカウンタキャッシュが複合 FK / エイリアス FK で正しく更新されるか
activerecord/test/cases/autosave_association_test.rb(+16)- autosave の
_record_changed?が複合 / エイリアス FK の変更を検知できるか
- autosave の
activerecord/test/cases/associations/has_many_through_associations_test.rb(+14)- has_many :through + dependent: :nullify で、複合 / エイリアス FK が正しく NULL になるか
- 影響範囲・注意点
影響を受けるのは、以下を満たすプロジェクトです:
- ActiveRecord で
- 複合外部キー (Array で指定される FK)
- もしくは エイリアス付き外部キー (attribute と実カラムが異なる) を使っている
- かつ次のいずれかを利用している:
belongs_to ... , counter_cache: true- autosave (
accepts_nested_attributes_forやautosave: trueなど) has_many :through, dependent: :nullifyまたはthrough先に対して:nullify削除
- ActiveRecord で
これまでの挙動との違い:
- これまでは「サイレントに効いていなかった」「不正な SQL を生成していた」ため、
実運用では- カウンタが更新されていない
- 関連が自動保存されない
:nullifyがエラーで落ちる / うまく nullify されない といった不具合が発生していた可能性があります。
- 本 PR 取り込み後は「本来の正しい挙動」をするようになるため、
- 今まで動かなかったロジックが動くようになる
- 一見すると「挙動が変わった」ように見えるかもしれませんが、 仕様としては修正前がバグであり、これが正しい挙動です。
- これまでは「サイレントに効いていなかった」「不正な SQL を生成していた」ため、
マイグレーション/設定面での注意:
- もしアプリ側で
reflection.foreign_keyを直接文字列前提で扱っているコードがある場合、
Rails 本体と同様に「配列を許容する」ように修正した方がよいです- 例:
Array(reflection.foreign_key)で正規化してループ - エイリアスを使う場合は、
arel_table[attribute]経由で解決するなど
- 例:
- もしアプリ側で
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/58364
- 関連概念:
ActiveRecord::Reflection#foreign_keyActiveRecord::Key#where_hashArel::Table#[]- autosave と
will_save_change_to_attribute?の関係
- 類似の問題に対する過去の修正(PR)でも、
「reflection.foreign_keyが配列になり得る」「エイリアス解決が必要」というパターンは共通しているため、
自前でメタプログラミング的に reflection を使うコードでは同様の対処が推奨されます。
#58354 Resolve attribute aliases in belongs_to change tracking
マージ日: 2026/8/3 | 作成者: @55728
- 概要 (1-2文で)
belongs_toの外部キーにalias_attributeを定義している場合に、変更検知(Dirty API)がエイリアス名ではなく実カラム名でしか動かず、一部機能が壊れていた問題を修正した PR です。belongs_toが Dirty API にアクセスする箇所で、常に「エイリアスを実カラム名に解決してから」参照するように統一されています。
- 変更内容の詳細
背景となる問題
Rails では:
class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
alias_attribute :topic_id, :post_id
endのように、belongs_to の外部キー(ここでは post_id)に対して alias_attribute :topic_id, :post_id を定義すると:
comment.topic_id/comment.topic_id = ...は正常に動く- しかし Dirty API(
saved_change_to_attribute?,attribute_before_last_save,will_save_change_to_attribute?など)は内部的に「実カラム名(post_id)」で状態を持っている
そのため:
belongs_toの実装が Dirty API を「エイリアス名(topic_id)」で問い合わせると、「変更なし」と判定される- 結果として:
counter_cacheが壊れる(親のカウンタが増減しない or destroy 時に負の方向にズレる)touch: trueで古い親レコードが touch されないbelongs_to_required_validates_foreign_key = falseの設定下で、外部キーの必須性を Dirty ベースで判断する処理が誤動作し、ダングリング FK が検知されない
というバグが発生していました。
この PR の方針
「読み書き時と同様に、Dirty API を使うときもエイリアスを解決して実カラム名で扱う」ようにし、belongs_to 周りの全てのロジックが「見かけの属性名」ではなく「実際のカラム名」に統一してアクセスするように変更されています。
要点:
belongs_toが Dirty API(*_changed?,*_before_last_save,*_previously_changed?など)を呼ぶ全箇所で、- 渡す属性名を「association が紐付いている外部キーの実カラム名」に変換
- ポリモーフィック関連用の
BelongsToPolymorphicAssociationでも同様の修正 touch_later(非同期 touch 実装)でbelongs_to親を touch するときにも同じルールで Dirty API を参照- テストを追加して、「エイリアス経由で外部キーを更新しても、
counter_cache,touch: true, 必須バリデーションなどが期待通り動く」ことを保証
イメージしやすいサンプルコード
PR の説明を踏まえた、典型的な状況:
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
# 実カラムは post_id
belongs_to :post, counter_cache: true, touch: true
# アプリ内の都合で別名を付けている
alias_attribute :topic_id, :post_id
end
comment = Comment.create!(topic_id: post1.id) # alias 経由で作成
# 別の親を指すように alias 経由で変更
comment.topic_id = post2.id
comment.save!修正前:
post1.comments_countがデクリメントされない or destroy などをきっかけにマイナス方向へズレるpost1.updated_atが更新されない (touch: trueが古い親に効かない)- required な
belongs_to周りのバリデーションも、エイリアス経由だと誤判定されるケースがある
修正後:
counter_cacheがpost1からpost2へ正しく移動- 両方の親(特に旧親)に対して
touchが適切に動作 belongs_to_required_validates_foreign_key = falseの特殊モードでも、Dirty を使った FK の整合性チェックがエイリアスでも正しく機能
実装的には、belongs_to の内部で:
- 「この関連が使う外部キー(
foreign_key)」→「実際に Dirty API に渡すキー名」 - という変換ロジック(エイリアス解決)を導入し、それを Dirty 参照箇所で一貫して使用するようになっています。
- 影響範囲・注意点
- 影響を受けるのは「
belongs_to外部キーにalias_attributeを付けている」ケースのみ- 通常の
belongs_toには挙動の変化はほぼない(バグ修正のための内部挙動の整合性改善)
- 通常の
- 既にこのバグを前提に「ワークアラウンド」を書いていた場合(例: 独自でカウンタ調整や touch をしている)、二重で動く可能性があるので、マージされたバージョンに上げたときは以下を確認するとよいです:
counter_cacheが二重加算・二重減算されていないかtouchを手動で呼んでいないか
belongs_to_required_validates_foreign_key = falseを使っている環境では、エイリアス付き外部キーでもバリデーション挙動が変わる可能性があるので、フォーム送信〜保存周りの動きを一度確認するのが安全です。
- 参考情報 (あれば)
- この PR の前提となる修正: #58348
- 関連する Rails 機能:
alias_attributebelongs_to/counter_cache/touch: true- Dirty API (
saved_change_to_attribute?,attribute_before_last_saveなど) belongs_to_required_validates_foreign_key設定
- エイリアス付き外部キーを使っているプロジェクトでは、この PR が含まれる Rails バージョンにアップグレードした際に、
belongs_to関連の挙動テスト(特にカウンタ・touch・バリデーション)を追加しておくと安心です。
#58356 [ci skip] Fix the table name in the Wishlists guide
マージ日: 2026/8/3 | 作成者: @duffuniverse
概要 (1-2文で)
Rails公式ガイドの「Wishlists」ガイド内で使われているテーブル名の誤記を、正しいテーブル名に修正したドキュメント向けのPRです。コードではなくガイド文書のみの変更で、挙動には影響しません。変更内容の詳細
- 変更ファイル:
guides/source/wishlists.md - 変更内容は1行のみで、「Wishlists」チュートリアル中に登場するテーブル名を、実際のモデル/マイグレーションで使われるべき正しいテーブル名に合わせる修正です。
- タイトルに
[ci skip]が付いていることからも分かる通り、テストやビルドを走らせる必要のないドキュメントのみの修正です。
(実際の diff は1行の +1/-1 で、例えば以下のようなニュアンスの修正が行われています):
- In our migration, we created the table `wishlist`:
+ In our migration, we created the table `wishlists`:あるいは:
- The products_wishlist table stores...
+ The products_wishlists table stores...といった形で、Railsの命名規約(モデル: 単数形 / テーブル: 複数形)に沿ったテーブル名に直されたものと考えられます。
- 影響範囲・注意点
- 影響範囲はRailsガイドの読者のみで、アプリケーションコードやRails本体の挙動には一切影響しません。
- ただし、旧バージョンのガイドを参考にしてテーブル名を誤って単数形や不適切な名前で作っていた場合、以下のような不整合が発生している可能性があります:
- モデル
Wishlistとテーブルwishlistのような単数形テーブル名の不一致 - 結果として ActiveRecord がテーブルを見つけられず、
ActiveRecord::StatementInvalidや「relation does not exist」エラーになる
- モデル
- これから「Wishlists」ガイドを参考に実装する場合は、このPR適用後のガイドを参照し、Railsの命名規約に沿ったテーブル名(複数形)で作成するのが安全です。
- 参考情報 (あれば)
- Railsのモデルとテーブルの命名規約:
- モデル: 単数形・CamelCase(例:
WishlistItem) - テーブル: 複数形・snake_case(例:
wishlist_items)
- モデル: 単数形・CamelCase(例:
- 関連ガイド:
- Rails Guides – Active Record Basics / Naming Conventions
- Rails Guides – Wishlists(本PRで修正されたガイド)
#58353 Fix Single Table Inheritance doc example for the type column change
マージ日: 2026/8/3 | 作成者: @55728
- 概要 (1-2文で)
Single Table Inheritance(STI) のドキュメント中にあるtypeカラムの変更例が実際の挙動と食い違っていたため、「空文字列""からの変更」となっていた説明を「nilからの変更」に修正した PR です。ドキュメントのみの変更で、挙動・API には一切の変更はありません。
- 変更内容の詳細
何を修正したか
ActiveRecord の STI 概要ドキュメント(activerecord/lib/active_record/inheritance.rb のコメント部分)にある、「type カラムがどのようにセットされるか」の例が以下のようになっていたと考えられます(旧記述イメージ):
Firm.new.changes # => {"type"=>["", "Firm"]}しかし、実際には ensure_proper_type による type の自動セットは「空文字列」ではなく「nil」からの変更として扱われます。つまり、正しい挙動は:
Firm.new.changes # => {"type"=>[nil, "Firm"]}この PR は、その「旧値」を表すドキュメント上の値を "" から nil に修正しています。
Ruby バージョンと Hash#inspect 表記の話
PR 説明中にある:
The pre-3.4
Hash#inspectstyle is kept to match the surrounding examples and the gem's minimum Ruby (≥ 3.3.1).
という一文は、Rails のドキュメント中でハッシュの出力例を Ruby 3.4 以前の Hash#inspect のスタイルに合わせている、という意味です。
現在の Rails の最低サポートは Ruby 3.3.1 以上
そのため、ハッシュの表示スタイルは Ruby 3.4 以降で入るかもしれない新しいフォーマットではなく、従来の:
ruby{"type"=>[nil, "Firm"]}のような
=>スタイルを使い続けて、周辺の例と一貫性を保っている、という意図です。
- 影響範囲・注意点
実行コードの変更は一切なく、「コメント(ドキュメント)のみ」の修正です。
STI を利用しているアプリの挙動(
typeカラムのデフォルト値やchangesの内容)は、以前からnilを前提としており、そこには変更はありません。もし既存のアプリケーション側のテストやドキュメントで「
typeの旧値が""になる」ことを前提にして書いていた場合は、それが誤解に基づいている可能性があります:新規レコード生成時の
changes例は、以下のようにnilを期待するのが正しいです。rubyfirm = Firm.new firm.changes # => {"type"=>[nil, "Firm"]}
データベースレベルで
typeカラムにデフォルト値''(空文字)を入れているような特殊なスキーマを使っていない限り、今回のドキュメント修正が実アプリの挙動と食い違うことはありません。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58353
- 関連箇所:
ActiveRecord::Inheritanceまわりの STI ドキュメントコメント - STI の概要(公式ガイド)
changesの仕様(ActiveModel::Dirty)
#58348 Restore alias_attribute support in associations
マージ日: 2026/8/3 | 作成者: @kamipo
- 概要 (1-2文で)
このPRは、belongs_toなどの関連付けで長らく効かなくなっていたalias_attributeのサポートを復活させ、外部キーや主キーに定義したエイリアス経由でも関連が正しく読める/書けるように戻す変更です。過去にパフォーマンス目的で導入された_read_attribute経由の「高速パス」を廃止し、再びread_attribute/write_attributeを使うようにしています。
- 変更内容の詳細
何が問題だったか
もともと belongs_to などの関連は、所有側(owner)レコードから外部キーを読むときに ActiveRecord::Base#[] を使っていました。Base#[] は内部的に read_attribute を呼ぶため、alias_attribute が定義されていてもそれを解決してくれます。
しかし、過去のコミット be2b98b4ae でパフォーマンス改善のために _read_attribute(内部用の高速メソッド)を使うようになりました。_read_attribute は
- alias 解決をしない
- primary key の判定などもスキップする
ため、外部キーや主キーに対して定義した alias_attribute が、関連経由では一切効かなくなるという副作用がありました。
その結果、以下のようなコードは期待どおりには動作していませんでした:
class Post < ActiveRecord::Base
alias_attribute :writer_id, :author_id
belongs_to :author, foreign_key: :writer_id
end
post.author # 本来は writer_id(alias) 経由で author_id を読むはずだが、無視されていた一部の本番アプリではこの問題を避けるために、_read_attribute 自体をオーバーライドするという危険なワークアラウンドが行われていた、という背景があります(#57795 参照)。
なぜ今「高速パス」をやめてもよいのか
read_attribute / write_attribute 自体がここ数バージョンで最適化され、高速化されています。具体的には PR 説明に挙がっているとおり:
- #36052:
@primary_keyを ivar キャッシュして主キーアクセスを約 +20% 高速化 - 5575bd7b22: 重複していた
attribute_alias?の呼び出しを削除し、alias 解決を約 +40% 高速化 - 27a1ca2bfe:
_read_attributeへの委譲を inline して約 +15% 高速化
これらの結果、_read_attribute を直接使って alias / primary-key チェックを飛ばすメリットがほとんどなくなったため、正しさ(alias を尊重する)を優先して元に戻す、という判断になっています。
実際の変更点(概念的な内容)
コード上の変更は主に次のようなパターンです:
- 各種 association 実装で
_read_attribute/_write_attributeを使っていた箇所をread_attribute/write_attribute(あるいはself[attribute]的なパス)に戻す belongs_to/has_one/has_many/through/ preload 周りなど、外部キー・主キーを読む/書く箇所を一貫して alias-aware な API に戻す- これに合わせてテストを追加し、
alias_attributeを外部キー側・主キー側に定義したケースで関連が正しく動くことを検証
おおまかなイメージ:
# 以前(問題があった状態)
def owner_foreign_key
owner._read_attribute(reflection.foreign_key)
end
# 今回のPR後
def owner_foreign_key
owner.read_attribute(reflection.foreign_key)
# または owner[reflection.foreign_key]
end同様の差し替えが、association.rb・belongs_to_association.rb・has_many_*・preloader など関連処理一式に広く入っています。
動作例
PR 説明にある例:
class Post < ActiveRecord::Base
alias_attribute :writer_id, :author_id # alias 定義
belongs_to :author, foreign_key: :writer_id
end
post.author # Post#writer_id (=> author_id の alias) 経由で外部キーを読む
author.posts.create!(...) # 生成時に writer_id(alias) を経由して author_id に書き込むこのように、関連宣言で foreign_key に alias 側の名前を指定しても、正しく「元の属性」に解決されて関連が機能するようになります。
また、ターゲット側の primary key に alias_attribute を定義しているケースでも同様に解決されるようになります。
- 影響範囲・注意点
影響範囲:
- Active Record の全ての主要な関連(
belongs_to/has_one/has_many/has_many :through/ ポリモーフィック / preloader / autosave 含む)の外部キー・主キーの読取/書込処理 - これまで「
alias_attributeが関連で効かない」と思っていた部分が、Rails のアップデートにより 突然期待どおりに(あるいは、以前とは違う挙動で)動き始める 可能性があります
- Active Record の全ての主要な関連(
互換性・注意点:
alias_attributeを外部キーや主キーに対して定義しており、その上で関連定義に別名(alias)を使っていたアプリは、これまで「無視されていた alias」が有効になり、関連の挙動が変わる場合があります- 過去のワークアラウンドとして
_read_attribute/_write_attributeを独自にオーバーライドしていたアプリでは、今回の変更と二重に alias 解決をしてしまう・パフォーマンスが悪化する・想定外の副作用が出るなどのリスクがあります- Rails をアップグレードする際に、それらの monkey patch / 上書きは可能な限り外すことが推奨されます
- パフォーマンスに関して:
read_attribute/write_attributeは最適化されているため、多くのアプリでは体感できる劣化はほとんどないはずですが、大量関連アクセスを行うホットパスを持つアプリでは、アップグレード後に一度プロファイルを取ると安心です
テスト追加内容から読み取れること:
belongs_toとhas_manyについて、alias された外部キー/主キーを使った関連の read / write が明示的にテストされています- 作者モデル(
test/models/author.rb)にも alias を絡めたテスト用定義が追加されており、実際のユースケースが想定されています
- 参考情報 (あれば)
- 当該PR:
- Restore
alias_attributesupport in associations (#58348)
- Restore
- 関連する背景Issue/PR:
- #57795 — 本番で
_read_attributeを上書きしてまで alias を効かせていた、という背景が言及されている - 過去の最適化:
- #36052 — primary key アクセスの高速化(
@primary_keyキャッシュ) - commit 5575bd7b22 —
attribute_alias?呼び出し削減による alias 解決の高速化 - commit 27a1ca2bfe —
_read_attributeへの delegating の inline 化
- #36052 — primary key アクセスの高速化(
- #57795 — 本番で
このPRにより、「関連周りでも alias_attribute は素直に効く」という、本来期待される挙動に戻ったと考えてよく、今後はワークアラウンドではなく素の API に依存できるようになります。
#58347 Deprecate write_attribute(:id, value) writing to the primary key
マージ日: 2026/8/3 | 作成者: @kamipo
- 概要 (1-2文で)
write_attribute(:id, value)がカスタム主キー(@primary_key)を書き換える挙動を非推奨にし、今後は:idというカラムに対する通常の書き込みとして扱うようにする変更です。read_attribute(:id)の非推奨・削除 (#49019) に続く「書き込み側」の整合性対応であり、複合主キーの場合も読み取り側と同じ扱いに揃えています。
- 変更内容の詳細
背景
- 以前の PR (#49019) で、
read_attribute(:id)が「カスタム主キーの値」を返す挙動は非推奨 → のちに削除されました。- 替わりに
read_attribute(:id)は「本当に id というカラムがあればその値」を返す、というより素直な仕様に変わっています。
- しかし書き込み側の
write_attribute(:id, value)はまだ「idを@primary_keyに変換して書き込む」という旧仕様のままでした。 - この PR はその「片側だけ残っていた」挙動を整理し、読み書きの仕様を揃えるものです。
具体的な変更点
1) write_attribute(:id, value) の扱いの変更・非推奨化
以前(問題となっている挙動):
class User < ApplicationRecord
self.primary_key = :uuid
end
user = User.new
user.write_attribute(:id, "abc123")
# これが内部的に:
# write_attribute(@primary_key, "abc123") # => write_attribute(:uuid, "abc123")
# のように変換され、主キー(:uuid)へ書き込まれていたこの PR 以降の方針:
write_attribute(:id, value)が「カスタム主キー(@primary_key)」を指すのは非推奨。- deprecation warning を出したうえで、将来的には「
idカラムへの単純な書き込み」としてのみ扱う方向に揃える。
コード上では、write_attribute の内部で以下のようなロジック変更が入っています(イメージ):
def write_attribute(attr_name, value)
name = attr_name.to_s
if name == "id" && primary_key != "id"
# ここで deprecation warning:
ActiveSupport::Deprecation.warn(
"write_attribute(:id, ...) によるカスタム主キー(#{primary_key})への書き込みは非推奨です。 " \
"代わりに write_attribute(:#{primary_key}, ...) あるいは record.#{primary_key} = ... を使用してください。"
)
# 将来はここで primary_key へのマッピングをやめる方針
# 現時点では既存アプリが壊れないよう、まだ primary_key に書き込むが、
# 次のメジャーで挙動を変える、という位置づけの deprecation
end
# 通常の属性書き込み処理へ
# ...
end※実際のメッセージ文言や制御フローは PR 内の実コードに従いますが、意図としては上記のような deprecation です。
2) 複合主キー(composite primary keys)に対する扱い
- Rails 本体には複合主キーのファーストクラスサポートはありませんが、内部的には一部コードやテストで「配列の primary key」などを扱う場面があります。
- 読み取り側ではすでに:
read_attribute(:id)は「idカラム」にアクセスするだけ- 複合主キーの「代表キー」として特別扱いしない
- この PR で書き込み側もそれに揃えられます:
write_attribute(:id, value)も「複合主キーの何か」を指すのではなく、単にidカラムに書くだけとする方向に統一。- テスト(
primary_keys_test.rb)で複合主キーのケースが追加され、「read と write の一貫した挙動」が確認されています。
3) CHANGELOG の追記
activerecord/CHANGELOG.md に以下のような趣旨のエントリが追加されています:
write_attribute(:id, value)がカスタム主キーに書き込む挙動を非推奨にしたことread_attribute(:id)の変更 (#49019) に続く変更であること
- 影響範囲・注意点
影響を受けるケース
- カスタム主キーを使っているモデル で、
- かつ
write_attribute(:id, ...)を使って主キーを書き換えている コードがある場合
例:
class Account < ApplicationRecord
self.primary_key = :account_uuid
end
account = Account.new
account.write_attribute(:id, "foo") # ← ここが非推奨このようなコードは:
- すでに deprecation warning を出し始める(か、出すように予約される)
- 将来的な Rails のメジャーバージョンアップ時に、挙動が変わり「
account_uuidではなくidカラムに書き込まれる」ようになるリスクがあります。
推奨される書き方
カスタム主キーに値を書きたい場合は、明示的に主キー名を指定してください。
# 推奨:
account.write_attribute(:account_uuid, "foo")
# もしくは:
account.account_uuid = "foo"write_attribute(:id, ...) を「主キーエイリアス」として使う前提のコードは、deprecation 期間中にすべて置き換えるべきです。
複合主キーを擬似的に使っている場合
- 自前実装や gem(composite_primary_keys など)で複合主キーを扱っているアプリで、
write_attribute(:id, ...)を「複合主キーの代表キー」扱いしているようなコードがあると、- 将来のバージョンで意味が変わる(単なる
idカラム更新になる)可能性があります。
- ここでも同様に「本当に書きたいカラム名・主キー名」を明示的に使うようリファクタリングしてください。
- 参考情報 (あれば)
- 該当 PR:
- Deprecate
write_attribute(:id, value)writing to the primary key — #58347
- Deprecate
- 関連 PR(読み取り側の変更):
- Deprecate and remove
read_attribute(:id)returning the custom primary key value — #49019
- Deprecate and remove
- 修正ファイル:
activerecord/lib/active_record/attribute_methods/write.rbactiverecord/test/cases/primary_keys_test.rbactiverecord/CHANGELOG.md
#58349 Fix test class name collision in Active Storage railties tests
マージ日: 2026/8/2 | 作成者: @flavorjones
- 概要 (1-2文で)
Active Storage の railties テストで、2つのテストファイルが同じテストクラス名ApplicationTests::ActiveStorageEngineTestを定義していたために発生していたクラス名衝突を解消する PR です。クラス名をファイルごとに固有の名前に分けることで、ローカル実行時に発生していた環境変数リーク検出テストの失敗を防いでいます。
- 変更内容の詳細
問題の状況
以下の2ファイルが、どちらも同じテストクラス名を定義していました。
railties/test/application/active_storage/analyzers_integration_test.rbrailties/test/application/active_storage/engine_integration_test.rb
両方とも ApplicationTests::ActiveStorageEngineTest を定義していたため、1プロセス内で両ファイルを読み込んだ場合にクラス定義が「上書き」されます。その結果、setup / teardown で行っている処理が意図した通りに実行されず、環境変数 BUNDLE_GEMFILE が元に戻らないなどの「環境リーク」が発生していました。
テストの失敗メッセージ例:
Environment leak detected:
- Changed variables:
- BUNDLE_GEMFILE from "<rails>/Gemfile" to "<rails>/tmp/d20260802-463299-f70uer/app/Gemfile"CI では rake test のデフォルトタスクが test:isolated であり、各テストファイルが別プロセスで実行されるため、このクラス名衝突は表面化していませんでした。
一方、ローカルでは bin/test railties/test/application/active_storage のようにディレクトリ指定で実行すると、複数ファイルが同一プロセス内に読み込まれ、クラス名衝突による問題が発生します。
実際の修正内容
修正は1行のみで、analyzers_integration_test.rb 側のクラス名を変更しています。
(イメージとしては以下のような変更)
# 変更前: analyzers_integration_test.rb
class ApplicationTests::ActiveStorageEngineTest < ActiveSupport::TestCase
# ...
end
# 変更後: analyzers_integration_test.rb
class ApplicationTests::AnalyzersIntegrationTest < ActiveSupport::TestCase
# ...
endこれにより:
analyzers_integration_test.rb→ApplicationTests::AnalyzersIntegrationTestengine_integration_test.rb→ApplicationTests::ActiveStorageEngineTest(従来のまま)
と、ファイルごとに異なるテストクラス名が割り当てられるようになりました。
また、新しいクラス名 AnalyzersIntegrationTest は同ディレクトリ内の他テストと命名規則を揃えています:
DirectUploadsIntegrationTestUploadsIntegrationTestValidatingServiceTest
ファイル名とクラス名を揃える Rails の慣習にも合致します。
- 影響範囲・注意点
- 対象:
railties/test/application/active_storage/*のテストのみ
アプリケーションコードや公開 API には影響しません。 - 影響内容:
- ローカル環境で
bin/testを使ってrailtiesの Active Storage 関連テストをまとめて実行した際の「環境リーク検出によるテスト失敗」が解消されます。 - CI はもともと
test:isolatedによりファイルごとにプロセスを分けていたため、挙動は変わりません。
- ローカル環境で
- 注意点:
- テストクラス名を直接参照しているメタテストやツール(例えば特定クラスだけ実行するようなカスタムツール)を使っている場合は、新しいクラス名
ApplicationTests::AnalyzersIntegrationTestに合わせて修正が必要になる可能性があります。 - ただし、通常の
bin/test/rake testの利用では影響はありません。
- テストクラス名を直接参照しているメタテストやツール(例えば特定クラスだけ実行するようなカスタムツール)を使っている場合は、新しいクラス名
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58349
- 当該2ファイルが同時に同じクラス名で追加されたコミット:
https://github.com/rails/rails/commit/367445d037b8f0d0baea684d14c0343ee29e8da1 railtiesのテスト実行方法:- CI / デフォルト:
rake test:isolated(ファイルごとに別プロセス) - ローカルでの一括実行例:
bin/test railties/test/application/active_storage(1プロセスで複数ファイルをロードするため、今回のようなクラス名衝突が顕在化しうる)
- CI / デフォルト:
#58254 Make CommandRecorder#record and #inverse_of private
マージ日: 2026/8/2 | 作成者: @kamipo
- 概要 (1-2文で)
ActiveRecord のActiveRecord::Migration::CommandRecorderクラスにおいて、内部用メソッドである#recordと#inverse_ofを public から private に変更した PR です。将来的なキーワード引数の扱い変更(Hash.ruby2_keywords_hash廃止への対応)に伴ってメソッドシグネチャを壊さざるを得ないため、それを「公開 API 変更」扱いにしないよう、先に「内部 API」であることを明確化しています。
- 変更内容の詳細
背景
- Ruby 3 系でのキーワード引数仕様変更に伴い、Rails では
ruby2_keywordsの利用を段階的に削除しています (#58238 など)。 - ただし、「後で呼び出すために引数を保存しておく」系の処理(この
CommandRecorderや ActiveJob など)は、まだHash.ruby2_keywords_hashに依存しており、- 位置引数 + 最後尾に「フラグ付き Hash」として kwargs が折り畳まれて保存される という Ruby 2 系の互換的な振る舞いに頼っていました。
Hash.ruby2_keywords_hash自体も今後非推奨になる予定のため、- 「位置引数」と「キーワード引数」を別々に保持する構造 に移行する必要があります。この構造変更によって、
CommandRecorder#record/#inverse_ofのシグネチャが変わる(あるいは実質的に互換でなくなる)ことが確定しています。
- 「位置引数」と「キーワード引数」を別々に保持する構造 に移行する必要があります。この構造変更によって、
なぜ record / inverse_of を private にするのか
CommandRecorderは、マイグレーションの実行コマンドを「記録するためのラッパ」であり、本来ユーザーが直接recordやinverse_ofを叩くことを想定していない内部コンポーネントです。- 想定される利用:
create_table,add_columnなど通常のマイグレーション DSL を呼ぶと、その裏側でCommandRecorderがそれらを「記録」し、ロールバック等で逆順に呼び出す。
- 想定される利用:
- ところが現状の API ドキュメント上では
#record/#inverse_ofが public メソッドとして掲載されており、「公式に公開されている API」のように見えてしまっていました。 - 今後の PR (#58239) でシグネチャが変わるのを、
- 「public API の破壊的変更」ではなく、
- 「内部 API の実装変更」 として扱うために、先に public → private にしておこう、というのがこの PR の主旨です。
実際のコード変更(概要)
※PR 本文から読み取れる範囲での要点のみ
command_recorder.rb:CommandRecorder#recordとCommandRecorder#inverse_ofをprivateセクションの中に移動。- それに伴い、これらのメソッドの呼び出し元は同じクラス/内部からのみになる(外部からの直接呼び出しは意図しない使い方と明示)。
- 併せて、キーワード引数の扱いについての実装が、今後の構造変更(位置引数とキーワード引数を分離して保存する)を前提にした形に寄せられている可能性があります。
- テスト (
command_recorder_test.rb):- 行数が大きく増えていることから、
CommandRecorderの「公開されるべき振る舞い」(マイグレーション DSL を通した呼び出し)をより網羅的にテストするように拡充。record/inverse_ofを直接呼ぶテストがあった場合は削除/書き換えしていると考えられます。
- 行数が大きく増えていることから、
CHANGELOG.md:- 「
CommandRecorder#recordと#inverse_ofを private にした」旨のエントリを追記し、位置づけを明示。
- 「
- 影響範囲・注意点
影響を受ける可能性があるコード
次のようなコードを書いている場合は、明確に壊れます(か、少なくとも将来のバージョンで壊れるリスクが高いです):
ruby# 例1: CommandRecorder を直接 new して使っている recorder = ActiveRecord::Migration::CommandRecorder.new(connection) recorder.record(:create_table, [:users, { id: :uuid }]) # 例2: 独自の逆操作ロジックで inverse_of を直接利用 command, args = recorder.inverse_of(:add_column, [:users, :name, :string])この PR により、
record/inverse_ofは private になるため、上記のような呼び出しは Ruby 側の可視性エラー(NoMethodError)となります。
実務的な注意点
- マイグレーション DSL(
create_tableなど)を通常どおり使っている限り、この変更による影響はほぼありません。 - 独自のマイグレーションクラスやツールで、
CommandRecorderを直接扱っている場合は、- そのコードは「内部 API に依存している」状態であり、
- かつ今後の引数の扱い変更(#58239 以降)でもさらに壊れる可能性が高い ため、設計を見直すべきです。
- ライブラリ作者で、
CommandRecorderを直接使っている場合は特に要注意で、代替方法(例えばマイグレーション DSL をラップする、または独自にコマンド記録の仕組みを持つなど)を検討してください。
- 参考情報 (あれば)
- この PR が依存している/前提としている関連 PR:
- #58238:
ruby2_keywordsの呼び出し削除 - #58239:
CommandRecorderなどでの引数保存構造を「位置引数とキーワード引数を分けて保持する」形式に変更する予定の PR
- #58238:
- ドキュメント:
- 公式 API ドキュメント上ではこれまで
CommandRecorder#record/#inverse_ofが public として掲載されていましたが、この PR により「本来は内部 API」という位置づけが明確になります。
https://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html
- 公式 API ドキュメント上ではこれまで
#58344 Pin redis below 6 in the Gemfile
マージ日: 2026/8/2 | 作成者: @yahonda
- 概要 (1-2文で)
Rails の Gemfile でredisのバージョンを「6未満」に固定し、テストがredis-6.0.0のリリースによって落ちる問題を回避する PR です。Action Cable の redis サブスクリプションアダプタが要求するバージョン制約(>= 4, < 6)と実際にロードされる gem のバージョンの不整合を解消しています。
- 変更内容の詳細
背景
2026-07-31 に
redis 6.0.0がリリースされた。Rails の Action Cable の Redis アダプタは、ロード時に以下のような依存関係を宣言しています:
rubygem "redis", ">= 4", "< 6"しかし、Gemfile では
redisバージョンを特に 6 未満に固定しておらず、bundle installすると自動的にredis-6.0.0が入る状況になっていました。その結果、テスト実行時に以下のようなエラーが発生:
textGem::LoadError: Error loading the 'redis' Action Cable pubsub adapter. Missing a gem it depends on? can't activate redis (>= 4, < 6), already activated redis-6.0.0. Make sure all dependencies are added to Gemfile.つまり「
redisは 4 以上 6 未満が必要なのに、すでに 6.0.0 が有効化されているので条件を満たさない」という状態です。
この PR での実際の変更
変更ファイルは 2 つだけです。
GemfileGemfile.lock
差分としては、それぞれで redis のバージョン指定を「6 未満」にピン留めしています(+1/-1 行ずつ)。
イメージとしては、Gemfile が以下のようになる変更です(実際の PR と若干表記が異なる可能性はありますが、意味としてはこれ):
# 変更前(例)
gem "redis"
# 変更後(例)
gem "redis", "< 6"それに伴って Gemfile.lock の redis の行も、6.0.0 ではなく 5.x 系にロックされるよう変更されています。
なぜ上限を上げずに「6 未満」にしたのか
説明文にある通り、redis アダプタの上限を直接 >= 4, < 7 のように変えるのではなく、Gemfile 側で 6 未満に固定する方針が取られています。その理由は:
- Rails の main ブランチでは、Action Cable の Redis アダプタ実装がすでに
redis-clientベースに移行済み(コミットef812c2652)。 - この PR の対象ブランチ(おそらく安定版ブランチ)は、まだ従来の
redisgem ベースのアダプタを使っている。 - ここで安易に上限を引き上げると、main ブランチとの差分(実装・依存関係)が広がってしまい、メンテナンスが複雑になる。
- さらに、
redis 6への正式対応が行われておらず、動作保証もされていないため、テスト落ちの一時的な回避ではなく「未サポートバージョンを Gemfile 側で明示的に避ける」という解決策を選んでいる。
- 影響範囲・注意点
影響範囲
- Rails 開発環境および CI 環境で利用される
redisgem が「5.x 系(< 6)」に固定されます。 - それにより、Action Cable の Redis pubsub アダプタをロードするテスト・実行環境での
Gem::LoadErrorは解消されます。 - コード本体(Action Cable や他のコンポーネント)の実装には手を入れていないため、機能面の挙動は従来の
redis 5.xを使っていたときと変わりません。
注意点
redis 6.0.0以降の新機能や挙動変更には、このブランチではまだ対応していません。
アプリ側の Gemfile で Rails と一緒にredisを 6 以上に上げようとすると、同様のバージョン不整合に遭遇する可能性があります。- Rails をこのブランチ系で利用しているアプリケーションは、アプリ側の Gemfile でも
redis,redis-rails,redis-namespaceなどのバージョン制約に注意する必要があります。Rails 自体が< 6を要求している期間は、それに合わせる必要があります。 - main ブランチでは
redis-clientベースに移行しているため、将来的に Rails のバージョンを上げる際には、- 依存 gem として
redisではなくredis-clientが入る - 接続設定やオプションが若干変わる
可能性があります。この PR はその移行までの「つなぎ」の対応です。
- 依存 gem として
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58344
- CI の失敗ログ(Action Cable の Redis アダプタ読み込み失敗):
https://buildkite.com/rails/rails/builds/131861#019fbb44-6779-4ff8-af5f-102d919ad65d/L1764 >= 4, < 6制約を導入した過去 PR: https://github.com/rails/rails/pull/45856- main ブランチでの
redis-clientベース移行のコミット:ef812c2652
#58265 Consolidate MySQL ALGORITHM / LOCK into MySQL::AlterTable state
マージ日: 2026/8/2 | 作成者: @kamipo
- 概要 (1-2文で)
MySQL アダプタにおけるALTER TABLEのALGORITHM/LOCK指定を、各操作ごとのバラバラな扱いからMySQL::AlterTableオブジェクトの状態として一元管理するようにした PR です。これにより、単発・バルクいずれのスキーマ変更でも、正しい位置・回数でALGORITHM/LOCKが出力され、オプションが落ちる不具合も解消されます。
- 変更内容の詳細
2-1. これまでの問題点
PR 説明の通り、従来は以下の問題がありました。
各単発操作ごとの重複実装
add_column,change_column,rename_column,remove_columnなどの 単発 MySQL 操作ごと に、以下のようなパターンで SQL 末尾にALGORITHM/LOCKを付け足していました。rubysql = "ALTER TABLE `table` ADD COLUMN ..." sql << ", #{algorithm}" if algorithm sql << ", #{lock}" if lock execute(sql)同じロジックが複数箇所に分散しており、保守性が低い状態でした。
複数インデックスをまとめて追加/削除する場合の SQL が不正
MySQL::AddIndex/MySQL::DropIndexは自身がalgorithm/lockを持ち、visit_AddIndex/visit_DropIndexが各フラグメントごとにそれを出力していました。
そのため、ALTER TABLE ...の一文の中で複数インデックスを操作すると、例えば次のように 同じALGORITHM/LOCKが N 回繰り返される SQL になり、MySQL が拒否していました。sqlALTER TABLE `users` ADD INDEX `idx_1` (...), ALGORITHM = INPLACE, LOCK = NONE, ADD INDEX `idx_2` (...), ALGORITHM = INPLACE, LOCK = NONE;MySQL の文法上、
ALGORITHM/LOCKはALTER TABLE文全体に対するヒントであり、文末に 1 回だけ書くのが正しい形です。バルクモードでの
algorithm:/lock:オプションが無視されていたchange_tableブロック内でt.column,t.changeなどにalgorithm:/lock:オプションを渡しても、#{command}_for_alter経由の処理でそれらが抽出されておらず、SQL に反映されない(黙って落ちる) 状況でした。rubychange_table :users, bulk: true do |t| t.change :name, :string, algorithm: :inplace, lock: :none end # 従来は ALTER TABLE 文に ALGORITHM/LOCK が付かなかった
2-2. 新しい設計方針: AlterTable に集約
ALGORITHM / LOCK は ALTER TABLE 文全体に対するヒントなので、その状態を MySQL::AlterTable 自体に持たせるよう変更されました。
追加された状態・メソッド
MySQL::AlterTableに以下の状態を追加:algorithmlock
- それぞれの setter は、既存のフォーマッタである
index_algorithmlock_clauseを通して正しい SQL 断片に変換されるようになっています
(例::inplace→"ALGORITHM = INPLACE"など)。
visit_AlterTable のオーバーライド
MySQL::SchemaCreation#visit_AlterTable をオーバーライドして、ALTER TABLE 文の末尾に一度だけ ALGORITHM / LOCK を追加するように変更されました。
概念的には以下のような出力になります。
ALTER TABLE `users`
ADD COLUMN `age` int,
ADD INDEX `index_users_on_age` (`age`)
ALGORITHM = INPLACE
LOCK = NONE;これにより、単発・バルクに関わらず、ALGORITHM / LOCK は常に正しい位置に 1 回だけ出力されます。
2-3. AlterTable が操作オプションを吸い上げるように
MySQL::AlterTable が以下のメソッドをオーバーライドしています。
add_columnremove_columnchange_columnrename_columnadd_indexremove_index
これらは、渡されたオプションから :algorithm / :lock を抽出して AlterTable 自身の状態に反映するように変更されました。
そのため、
change_table ... bulk: trueの中- 単発で
add_column,add_indexなどを呼ぶ場合
のいずれでも、処理フロー上は 同じ「抽出 → AlterTable にセット」のロジック が使われます。
内部的には、extract_algorithm_and_lock! というプライベートヘルパーで、オプションから該当キーを抜き取りつつ AlterTable の algorithm / lock にセットしている形です。
サンプル(概念的な Ruby 側のコードイメージ):
# 単発
add_column :users, :age, :integer, algorithm: :inplace, lock: :none
# バルク
change_table :users, bulk: true do |t|
t.column :age, :integer, algorithm: :inplace, lock: :none
endどちらも最終的には同じように MySQL::AlterTable の algorithm / lock に値が入り、visit_AlterTable によって 1 回だけ末尾に出力されます。
2-4. AddIndex / DropIndex からの状態削除
MySQL::AddIndex / MySQL::DropIndex クラスは、もはや自身で algorithm / lock を持つ必要がなくなったため、該当フィールドが削除されました。
あわせて、visit_AddIndex / visit_DropIndex も単純化され、「インデックスを追加/削除するフラグメントのみ」を出力 する形になっています。ALGORITHM / LOCK の責務は完全に AlterTable 側へ移った、という整理です。
2-5. 単発 API 実装のシンプル化
add_column, change_column, rename_column, remove_column などの MySQL 対応コードは、
- 以前: 自前で SQL 文字列を組み立てて、末尾に
, #{algorithm}/, #{lock}を付け足す - 現在:
MySQL::AlterTableに操作を積んでexecute_alter_table(at)を呼ぶだけ
というシンプルなスタイルになりました。
擬似コードイメージ:
def add_column(table_name, column_name, type, **options)
at = AlterTable.new(table_name)
at.add_column(column_name, type, **options)
execute_alter_table(at)
endここで options[:algorithm] / options[:lock] は AlterTable#add_column 内で吸い上げられ、AlterTable 全体の状態として使われます。
2-6. テストの追加
activerecord/test/cases/adapters/abstract_mysql_adapter/active_schema_test.rb にテストが追加されています。
内容としては、
ALTER TABLEに対してALGORITHM/LOCKが一度だけ正しく付くこと- バルク・単発双方で
algorithm:/lock:オプションが無視されず反映されること
などを確認するためのものと考えられます。
- 影響範囲・注意点
3-1. 開発者視点での挙動変化
Rails の公開 API レベルでは、概ね「期待どおりに直った」変更であり、破壊的変更はほとんどありませんが、細かい挙動差は出ます。
change_table ... bulk: trueでalgorithm:/lock:オプションを指定した場合
以前は黙って無視されていましたが、今後は 実際にALTER TABLEに反映 されます。- これにより、既存アプリで「実は無視される前提」で動いていた場合、実際にオンライン DDL などが走るようになり、ロック時間や DDL の挙動が変わる可能性があります。
- 複数のインデックス追加/削除をまとめた
ALTER TABLE- 従来は MySQL に拒否されていたケース(
ALGORITHM/LOCKが N 回出る)が、正しい 1 回のみの付与で成功するようになる ため、エラーが解消されます。
- 従来は MySQL に拒否されていたケース(
3-2. 拡張 / Monkey Patch している場合の注意
以下のようなカスタマイズをしているプロジェクトは注意が必要です。
MySQL::AddIndex/MySQL::DropIndexに対してalgorithm/lockを直接操作していたvisit_AddIndex/visit_DropIndexをオーバーライドし、そこからALGORITHM/LOCKを出力していた
MySQL::SchemaCreation#visit_AlterTableを独自にオーバーライドしていた
この PR により役割分担が変わったため、
AlterTableのalgorithm/lockを見るように書き換えるvisit_AddIndex/visit_DropIndex側ではヒントを扱わない設計に合わせる
などの追従が必要になる可能性があります。
- 参考情報 (あれば)
- MySQL の
ALTER TABLE構文 (ALGORITHM/LOCKが文全体のヒントであること):- MySQL 8.0 Reference Manual – ALTER TABLE Syntax
https://dev.mysql.com/doc/refman/8.0/en/alter-table.html
- MySQL 8.0 Reference Manual – ALTER TABLE Syntax
- この PR に関連しうる Rails 側のクラス/ファイル:
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapterActiveRecord::ConnectionAdapters::MySQL::SchemaCreationActiveRecord::ConnectionAdapters::MySQL::SchemaDefinitionsActiveRecord::ConnectionAdapters::MySQL::SchemaStatements
- PR 本体(#58265):
- (公式 Rails リポジトリの PR ページを参照)
#58340 Transform keys in place in ActionController::Parameters#deep_transform_keys!
マージ日: 2026/8/2 | 作成者: @55728
- 概要 (1-2文で)
ActionController::Parameters#deep_transform_keys!が、名前に反してインプレース(破壊的)ではなく新しいハッシュを組み立てていた問題を修正し、内部で_deep_transform_keys_in_object!を使って本当にインプレース変換するようにしたPRです。これによりAPIの一貫性とパフォーマンス(割り当て数削減)が改善されますが、返されるParametersオブジェクトの見かけ上の挙動は変わりません。
- 変更内容の詳細
もともとの問題点
ActionController::Parameters#deep_transform_keys!は「!が付いている=破壊的メソッド」のはずですが、内部では「パラメータ全体を再構築」する実装になっていました。- 以前のコミット(
32587c3bdd)で_deep_transform_keys_in_object!という「本当にインプレースで変換する」ヘルパーメソッドが追加されていたものの、deep_transform_keys!からは呼ばれておらず、事実上デッドコードになっていた状態です。 - 一方で、
deep_transform_values!は_deep_transform_values_in_object!をちゃんと呼んでおり、キー版と値版で実装が非対称でした。
今回の修正内容
ActionController::Parameters#deep_transform_keys!が_deep_transform_keys_in_object!を経由するように接続されました(たった1行の差し替えですが意味は大きい変更です)。- これにより、
deep_transform_keys!もdeep_transform_values!と同様に、ネストされた構造を「その場で」変換するようになります。
PR説明中のパフォーマンス比較:
| params の形状 | 変更前 | 変更後 |
|---|---|---|
| 200 entries, 3 levels deep | 5,825 allocations | 1,612 allocations |
オブジェクト割り当て数が大幅に減っているため、GC負荷とメモリ使用量の面でメリットがあります。
動作イメージ(概念的なサンプル)
params = ActionController::Parameters.new(
"user" => {
"first_name" => "Alice",
"last_name" => "Smith"
}
)
# キーをシンボル化する例
params.deep_transform_keys!(&:to_sym)
# 返り値・内容は変更前後で同じ:
# => #<ActionController::Parameters {:user=>{:first_name=>"Alice", :last_name=>"Smith"}} permitted: false>コード上の挙動(結果として得られる params)は変わりませんが、その内部で「新しいハッシュを組み立てる」のではなく「元のハッシュやネストされたハッシュのキーを書き換える」ようになった、という変更です。
- 影響範囲・注意点
影響範囲
- 対象:
ActionController::Parameters#deep_transform_keys!を利用しているすべてのコード。 - 表向きの挙動(返される値や、処理後の
paramsの構造)は変わらないと明示されています。
重要な注意点
As with
deep_transform_values!, nested hashes that the caller still shares with the receiver are now transformed in place.
ここがポイントです。
- 以前は「内部的に新しいネストハッシュを組み立てていた」ため、
- 呼び出し側が元々持っていたネストされた
Hashオブジェクトと、Parameters内部のネストハッシュが“たまたま同一オブジェクト”であるケースは少なかった(もしくはなかった)。
- 呼び出し側が元々持っていたネストされた
- 今回の変更により、「呼び出し側が
Parameters内部のネストハッシュとオブジェクトを共有している場合」、deep_transform_keys!によって、その共有しているオブジェクトのキーがインプレースで書き換わるようになりました。
つまり:
hash = { "user" => { "first_name" => "Alice" } }
params = ActionController::Parameters.new(hash["user"]) # or hash["user"] を再利用するようなパターン
# もしここで params 内部と hash["user"] が同じオブジェクトだった場合
params.deep_transform_keys!(&:to_sym)
# 変更後は hash["user"] のキーも :first_name に変わりうる
p hash
# 以前: { "user" => { "first_name" => "Alice" } }
# 変更後: { "user" => { :first_name => "Alice" } } となる可能性Rails の通常の params 生成経路では、アプリケーションコードが同じネストハッシュオブジェクトを共有するケースはあまり多くありませんが、
- 生の
Hashを部分的にParametersに包む - あるいは
params.to_hの結果を再利用して、同じオブジェクトを紐づけるような変換をしている
といった「高度な使い方」をしている場合には、共有オブジェクトのインプレース変更を意識する必要があります。
とはいえ、PR本文でも「The returned parameters are unchanged.」と明記されており、一般的な Rails アプリケーションでの影響は軽微かつ後方互換的と考えられます。
- 参考情報 (あれば)
- 対象PR: https://github.com/rails/rails/pull/58340
- 関連コミット(ヘルパーメソッド追加元):
32587c3bdd - 類似API:
ActionController::Parameters#deep_transform_values!- 今回の修正後は、キー版・値版の両方が対称的にインプレース変換を行う実装になりました。
#58264 Consolidate ALTER TABLE building into AlterTable
マージ日: 2026/8/2 | 作成者: @kamipo
- 概要 (1-2文で)
このPRは、ALTER TABLE文の生成処理をAlterTableクラスに集約し、複数の操作を一度に行う際に不正なSQLが生成されうるバグを根本的に解消しつつ、MySQL/PostgreSQL 含めたアダプタ全体の設計を整理・リファクタリングしたものです。bulk_change_tableなどの経路もすべてAlterTableを経由するよう統一されています。
- 変更内容の詳細
背景となるバグ
これまで visit_AlterTable では、adds / foreign_key_adds などスロットごとに join(" ") した文字列同士をそのまま連結しており、異なるスロットにまたがって複数の操作が追加されると、間に区切りが入らず不正な SQL になる可能性がありました。
しかし、実際には各呼び出し側が「1回の AlterTable につき1操作」しか追加していなかったため、この問題は表面化していませんでした。
このPRでは、AlterTable に操作の順序付きリストを一本化し、SQL をそこからのみ組み立てるようにすることで、設計レベルで問題を解消しています。
AlterTable を「単一の真実のソース」にするリファクタ
1) 操作ごとの Data オブジェクト追加
AlterTable がもつ操作を、以下のような Data.define で定義された小さな値オブジェクトで表現するようになりました。
例(PR説明より):
AddColumnDefinitionAddForeignKeyDropColumnRenameColumnChangeColumnNullAddIndex- PostgreSQL固有:
ValidateConstraintAddExclusionConstraintAddUniqueConstraintなど
AlterTable インスタンスは、これらを1本の配列 operations に順序通りに保持します。
alter_table = AlterTable.new(:users)
alter_table.operations << AddColumnDefinition.new(name: :age, type: :integer)
alter_table.operations << AddIndex.new(columns: [:age])
# ...のようなイメージこれにより、
- 操作の種類に関わらず、「テーブルに対して行う順序付き操作列」として一括管理
- グループ(adds / foreign_key_adds / ...)に散らばった状態からの解放
- 複雑な ALTER TABLE 文も順序保証のある1本のリストから生成
が可能になります。
2) visit_AlterTable の書き換え
visit_AlterTable は以下のような方針で書き換えられました。
o.operationsを順番に走査- 各操作オブジェクトに対して、既存の visitor パターン (
accept(op)) で SQL 断片を生成 - それらを
", "で結合し、一つのALTER TABLE ...文にまとめる
ポイント:
- どの種類の操作 (
AddColumnDefinition,AddForeignKey, …) であっても同列に扱われ、順番が保たれる - 以前のように「スロットごとに join して最後に無分別に連結する」ことがなくなる
これにより、
ALTER TABLE users
ADD COLUMN age integer,
ADD CONSTRAINT ...のように、複数操作混在の SQL も正しく生成されます。
3) 接続アダプタ側の build ロジックを AlterTable に移管
これまで接続アダプタ側(AbstractAdapter/MySQL/PostgreSQL)にあった「ALTER TABLE 用の断片生成メソッド」が AlterTable に移されました。
移管対象(例):
build_add_column_definitionbuild_change_column_definitionbuild_change_column_default_definition
構成としては:
- 抽象的な共通部分:
AlterTable(抽象クラス相当) - DB 固有の部分:
- MySQL:
Mysql::AlterTable(あるいは MySQL 用サブクラス) - PostgreSQL:
PostgreSQL::AlterTable
- MySQL:
に持たせる形になっています。
なお、:if_not_exists による column_exists? ガードは従来どおり「呼び出し側」で残されています。これは
- 「SQLをどう組み立てるか」ではなく
- 「そもそも ALTER TABLE すべきかどうか」という前提条件チェックであり
- スキーマクエリを発行するため、ビルドロジックには含めない
という設計判断です。
4) deferred_operations の導入
PostgreSQL などでは、単一の ALTER TABLE 内に完全に収まりきらない操作があります。例:
change_column_comment(コメント変更)- デフォルト付き
change_column_nullのときに必要なUPDATE文(既存データの穴埋め)- その後改めての
ALTER TABLEでの制約変更
これらは、ALTER TABLE 実行後に別クエリとして発行する必要があります。
そこで AlterTable#deferred_operations が導入されました。
execute_alter_tableで:ALTER TABLE ...をexecutedeferred_operationsに溜めておいた処理を順次実行
というフローになります。これにより:
- 「ALTER TABLE 内にまとまる操作」と
- 「後処理として別クエリが必要な操作」
を明確に分離でき、かつ呼び出し側は統一インターフェースで扱えます。
5) COMBINABLE_COMMANDS と bulk_change_table の書き換え
bulk_change_table は、1つのテーブルに対する複数操作を「できる限り少ない ALTER TABLE でまとめて実行する」ための仕組みですが、これまではアダプタ側にたくさんの #{command}_for_alter メソッドが存在していました。
このPRでは:
AlterTableサブクラスごとにCOMBINABLE_COMMANDSを定義- 例:
add_column,add_index,change_column, ...
- 例:
bulk_change_tableはcommandを直接AlterTableのメソッドとしてpublic_sendする形に変更alter_table.add_column(...)のように呼び出す
これにより:
*_for_alter系メソッド群が不要になり削除- 複数コマンドの「同一 ALTER TABLE へのマージ可否」は
COMBINABLE_COMMANDSによりアダプタ単位で制御 - コマンド追加時のエントリポイントがシンプルに
という整理がなされています。
6) execute_alter_table(at) ヘルパの導入
AlterTable を使うすべての呼び出し方が、
ALTER TABLE文を組み立て・実行deferred_operationsを実行
というパターンに統一されるよう、プライベートヘルパ execute_alter_table(at) が導入されています。
これにより:
- 「どのパスで
ALTER TABLEを呼んでも後処理の実行漏れがない」 - 共通化によりコード重複とバグリスクを削減
といったメリットがあります。
- 影響範囲・注意点
影響範囲
- 対象:
ALTER TABLEを発行するすべてのパスchange_table/bulk_change_table/ 各種 migration DSL (add_column,change_column_null,add_foreign_key等)- MySQL / PostgreSQL / 抽象アダプタの schema 関連コード
- 変更ファイル:
schema_creation.rb,schema_definitions.rb,schema_statements.rb- MySQL / PostgreSQL 各アダプタの schema_* 系
- 関連テスト (
schema_definitions_test.rbなど)
動作上の注意点
複数操作の順序が重要なケース
これまで暗黙に保障されていた順序と、今回のoperationsベースの順序が一致しているかを意識しておく必要があります。PRとしては「実際の呼び出し順をそのまま尊重する」設計ですが、手作業で既存コードを移した部分で順序の解釈が変わっていないか要確認事項です(ただしPR自体は後方互換を維持する意図)。DBごとの COMBINABLE_COMMANDS
MySQL・PostgreSQL それぞれで「まとめられるコマンド」がCOMBINABLE_COMMANDSで制御されるため、新しいコマンドを追加する場合や既存コマンドの挙動を変える場合には、このリストのメンテナンスが必要になります。「意図せず別 ALTER に分割される/まとめられすぎる」ことがないように注意が必要です。deferred_operations を伴う変更
PostgreSQL のchange_column_null+ デフォルトや、:comment周りはdeferred_operationsの利用に依存するようになるため、もしアプリケーション側でALTER TABLEの発行をフック/ログ解析している場合は、「後続で別クエリが来る」ことを前提に調整が必要な可能性があります。拡張・カスタマイズへの影響
アダプタを拡張して独自の ALTER 系メソッドを追加している場合、今後は:- 新しい operation クラス(
Data.define)を用意 AlterTableサブクラスにビルドロジックとCOMBINABLE_COMMANDSへの追加 を行うのが推奨パスになります。既存の*_for_alterにフックしていたコードは動かなくなる恐れがあります。
- 新しい operation クラス(
後方互換性
- 公開API(migration DSL)の表面上の挙動は変えない形でのリファクタリングを意図
- ただし内部構造が大幅に変わっているため、
- adapter の monkey patch
- schema 関連の内部APIに依存している gem / プラグイン
などには影響が出る可能性があります。
- 参考情報 (あれば)
- PR本体: https://github.com/rails/rails/pull/58264
- 関連しそうなコード:
ActiveRecord::ConnectionAdapters::SchemaStatements#change_table,#bulk_change_table- 各アダプタの
schema_creation.rb,schema_definitions.rb
- 設計的な観点:
- visitor パターン (
accept/visit_Xxx) による SQL ビルドの一元化 - 「1テーブルへの一連の操作」を明示的なオブジェクト (
AlterTable+ operation objects) として建模するリファクタリング例としても参考になります。
- visitor パターン (
#58341 Fix deprecation proxy doc examples that raise ArgumentError
マージ日: 2026/8/2 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::Deprecationのプロキシクラス (DeprecatedInstanceVariableProxy,DeprecatedConstantProxy) のドキュメント中のサンプルコードが、現在のシグネチャと合わずにArgumentErrorを出していた問題を修正し、実際に動く形に更新したPRです。機能変更はなく、ドキュメント記載の使い方と実装を同期させたものです。
- 変更内容の詳細
対象ファイル:
activesupport/lib/active_support/deprecation/proxy_wrappers.rb(+3 / -3)
主な変更点:
2-1. DeprecatedInstanceVariableProxy の例の修正
以前の(壊れている)例:
ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(
self, :request, :@request, ActiveSupport::Deprecation.new
)
# => ArgumentError: wrong number of arguments (given 4, expected 2..3; required keyword: deprecator)fc2dc7c8d3 によってコンストラクタが以下のようなシグネチャに変わっており、deprecator: がキーワード引数必須になっていました:
DeprecatedInstanceVariableProxy.new(object, method, var_name, deprecator: ...)このPRでは、ドキュメント内のサンプルを新しい呼び方に合わせて修正しています。概略は次のような形です(※実際のコードと同趣旨):
ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(
self,
:request,
:@request,
deprecator: ActiveSupport::Deprecation.new
)さらに、説明文中で「deprecator が “最後の引数”」と、あたかも位置引数であるかのように書かれていた箇所も、キーワード引数を使う今の仕様に沿った文言へ修正されています。
2-2. DeprecatedConstantProxy の例の修正
以前の(壊れている)例:
ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006')
# => ArgumentError: wrong number of arguments (given 2, expected 3)DeprecatedConstantProxy も、fc2dc7c8d3 によってコンストラクタのシグネチャが変更され、deprecator が必須引数(位置引数)になっていました。現在のシグネチャは概ね:
DeprecatedConstantProxy.new(old_const, new_const, deprecator)であるため、ドキュメントのサンプルは次のように修正されています(趣旨):
ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
'PLANETS',
'PLANETS_POST_2006',
ActiveSupport::Deprecation.new
)2-3. DeprecatedObjectProxy には変更なし
DeprecatedObjectProxy はもともと deprecator が位置引数のままで仕様変更はなく、既存のドキュメントも正しかったため、このPRでは触れられていません。
2-4. 動作確認(PR説明中の出力例)
修正後、ドキュメントに掲載されているサンプルコードをそのまま実行すると、説明されている通りの動作になります:
example.old_request.to_s
# DEPRECATION WARNING: @request is deprecated! Call request.to_s instead of @request.to_s
# => "special_request"
example.request.to_s # => "special_request"
PLANETS.class # => Array- 影響範囲・注意点
- 実コードの挙動には変更がなく、影響範囲はドキュメント(サンプルコードと説明文)のみです。
- ただし、以下のようなコードを書いている場合は注意が必要です:
- 古いドキュメントを参照して
DeprecatedInstanceVariableProxy.new(..., ..., ..., ActiveSupport::Deprecation.new)のように「deprecator を位置引数の最後」に渡している DeprecatedConstantProxy.new(old, new)のように deprecator を渡していない
- 古いドキュメントを参照して
- これらは現在の Rails ではすでに
ArgumentErrorになっており、このPRはあくまで「ドキュメントを現実に合わせた」ものです。
もし自分のコードで同様のエラーが出ている場合は、以下のように修正する必要があります:DeprecatedInstanceVariableProxyrubyDeprecatedInstanceVariableProxy.new(obj, :method, :@ivar, deprecator: ActiveSupport::Deprecation.new)DeprecatedConstantProxyrubyDeprecatedConstantProxy.new('OLD', 'NEW', ActiveSupport::Deprecation.new)
- 参考情報 (あれば)
- 概要に出てくる変更コミット:
fc2dc7c8d3- このコミットで
DeprecatedInstanceVariableProxyとDeprecatedConstantProxyのコンストラクタ API が変更され、deprecatorが必須となった。
- このコミットで
- 関連クラス:
ActiveSupport::Deprecation::DeprecatedObjectProxyActiveSupport::Deprecation::DeprecatedInstanceVariableProxyActiveSupport::Deprecation::DeprecatedConstantProxy
#58336 Fix search_field raising NameError when passed autosave: true
マージ日: 2026/8/1 | 作成者: @hammadxcm
- 概要 (1-2文で)
search_fieldヘルパーにautosave: trueを渡すとNameErrorが発生していた不具合を修正し、ドキュメントに記載されている「リクエストホストを反転した autosave 値」が実際に動作するようにしました。あわせてautosaveオプション周りの挙動を網羅するテストが追加されています。
- 変更内容の詳細
バグの内容
search_field はドキュメント上、autosave: true を渡すと以下のようにリクエストホストを反転した文字列を autosave 属性として自動付与することになっていました:
# request.host => "www.example.com"
search_field(:user, :name, autosave: true)
# => <input autosave="com.example.www" id="user_name" name="user[name]" results="10" type="search" />実装(修正前)は ActionView::Helpers::Tags::SearchField#render 内で次のように書かれていました:
options["autosave"] = request.host.split(".").reverse.join(".")しかし Tags::Base には request メソッドは定義されておらず、ビューコンテキストへのメソッド委譲もされていないため、search_field 呼び出し時には必ず次のエラーになっていました:
NameError: undefined local variable or method 'request' for
an instance of ActionView::Helpers::Tags::SearchFieldつまり、ドキュメントされていた autosave: true の機能は、これまで一度も正しく動いたことがありませんでした。
修正内容
リクエストオブジェクトを、タグが内部的に保持しているテンプレートオブジェクト(ビューコンテキスト)から取得するように変更しました。
# 修正前(概略)
options["autosave"] = request.host.split(".").reverse.join(".")
# 修正後
options["autosave"] = @template_object.request.host.split(".").reverse.join(".")@template_object は ActionView タグが持っているビューコンテキストであり、ここから request を参照するのが正しいアクセス方法です。
テスト追加と仕様の明確化
autosave 周りの分岐をすべてカバーするテストが form_helper_test.rb 等に追加されています。挙動は以下のように整理されています:
autosave: trueautosave属性はrequest.hostを"."で split → reverse → join した文字列になるresults属性はデフォルトで"10"が付与される
例:
ruby# request.host = "www.example.com" search_field(:user, :name, autosave: true) # => autosave="com.example.www" results="10"autosave: "my-key"(文字列指定)- 指定した文字列がそのまま
autosave属性に利用される results属性はデフォルト"10"が付与される
例:
rubysearch_field(:user, :name, autosave: "my-key") # => autosave="my-key" results="10"- 指定した文字列がそのまま
results:を明示指定した場合autosaveによるデフォルト"10"は上書きされず、指定したresults値が優先される
例:
rubysearch_field(:user, :name, autosave: true, results: 5) # => autosave="com.example.www" results="5"autosave: falseautosave="false"という属性がそのまま出力される(属性自体を付けないわけではない)results属性は付与されない
これは既存ドキュメントに記載されていた出力と一致するようにされています。
テストスイート (actionview の関連テスト) はすべて成功し、RuboCop もパスしています。
- 影響範囲・注意点
影響範囲
ActionView::Helpers::FormTagHelper/FormHelper/FormOptionsHelper/form_withなどを通じてsearch_fieldを利用しているコードで、autosaveオプションを使っている(またはこれから使う)場合。- これまで
autosave: trueを指定していた場合は、実際にはNameErrorによって動いていなかったため、そのコードパスが通っていたアプリでは今回の修正で「初めて」期待どおり動作することになります。
後方互換性の観点
autosave: trueがこれまでそもそも動作していなかったため、「正常に動いていた挙動が変わる」という種類の破壊的変更ではありません。- ただし、例外が発生することを前提にしていた非常に特殊なケース(例: rescue して代替処理など)は、例外が出なくなることで挙動が変わり得ます。
ホスト名依存の注意点
autosave: trueの値は@template_object.request.hostを元に計算されます。requestが利用できないコンテキスト(極めて特殊なレンダリングコンテキストなど)では、理論上ここで別の例外(NoMethodErrorなど)が発生する可能性がありますが、通常のコントローラ + ビューのレンダリングでは問題ありません。- マルチホスト / サブドメイン利用アプリでは、
autosave値がホストによって変わる点を踏まえ、キャッシュキーなどに組み込む場合は挙動を把握しておくとよいです。
- 参考情報 (あれば)
変更ファイル:
actionview/CHANGELOG.mdactionview/lib/action_view/helpers/tags/search_field.rbactionview/test/template/form_helper_test.rb(autosaveブランチのテスト追加)
この修正により、
search_fieldのautosaveに関するドキュメントと実際の挙動が一致し、今後autosave関連のリグレッションが起きにくいようにテストも補強されています。
#58339 Use precompiled Rubies from Mise in Install Rails Guide
マージ日: 2026/8/1 | 作成者: @excid3
- 概要 (1-2文で)
Rails公式ガイド「Install Ruby on Rails」で、Rubyのインストール手順が「Miseによる事前コンパイル済みRubyの利用」を前提とした内容に更新されました。これにより、依存関係のインストールやソースからのコンパイル手順が不要となり、ガイド全体がシンプルかつ高速なセットアップ手順に整理されています。
- 変更内容の詳細
Miseの利用前提への更新
- ガイド中の「Rubyのインストール方法」が、Miseが提供するprecompiled Rubyを利用する形に書き換えられました。
- 従来は、OSごとのパッケージインストールや、ビルドに必要な依存パッケージの説明、
ruby-build/rbenv 等と組み合わせたソースコンパイル手順が含まれていましたが、そうした説明が大幅に削除されています(-25行)。 - 代わりに、Miseをインストールした上で、Mise経由でRubyをインストールする最小限の手順だけが残されている形です(+6行)。
対象バージョンの更新
- ガイド内で言及される Ruby・Rails・Ubuntu のバージョンが最新に更新 されています。
- 具体的なバージョン番号はPR本文では明示されていませんが、「最新の安定版Ruby/ Rails」「最新LTSもしくは推奨版Ubuntu」の組み合わせに合わせた内容に変更されています。
- ガイド内で言及される Ruby・Rails・Ubuntu のバージョンが最新に更新 されています。
ドキュメント専用の変更
- 変更ファイルは
guides/source/install_ruby_on_rails.mdのみで、アプリケーションコードやテストコードへの変更はありません。 - 追加行6、削除行25と、ほぼ「不要になった作業手順」を落として簡潔にした構成です。
- 変更ファイルは
※実際のサンプルコマンド(イメージ)
ガイド内の実際の文面はPRからは全部は読めませんが、内容的には以下のような形に近くなっていると考えられます:
# Mise のインストール(例)
curl https://mise.run | sh
# 最新の Ruby をインストール
mise use --global ruby@<latest>
# インストール後の確認
ruby -v従来入っていたであろう「Ubuntuでbuild-essentialやlibssl-devなどを入れる」「ソースをコンパイルする」などの説明が削られ、Miseのインストールとmise use程度に集約されています。
- 影響範囲・注意点
新規ユーザへの影響(セットアップ体験)
- Rails公式ガイドに従うだけで、コンパイル不要・依存関係の事前準備も不要なRubyインストールが可能になり、導入がかなり簡単かつ高速になります。
- Windows / macOS / Linuxといった複数OSで、Miseさえ動けばほぼ同じ手順でRubyを入れられるようになり、クロスプラットフォームな案内としてもわかりやすくなります。
既存環境のユーザへの影響
- 既に rbenv / rvm / asdf などを使っている開発者にとっては、コード上の互換性やRails自体の挙動には影響はありません。
- ただし、公式のインストールガイドを読むと「Mise推奨」のように見えるため、チームの文化や既存ドキュメントが他のバージョンマネージャ前提の場合は、メンバーの環境構築手順を見直す必要が出るかもしれません。
Miseのprecompiled Rubyへの依存
- ガイドが「Miseのprecompiledバイナリ」を前提としたことで、
- 対応していないOS/アーキテクチャ(例: 非公式なディストリや特殊なCPUアーキテクチャ)
- あるいはまだprecompileされていないRubyバージョン
では、ガイド通りの手順ではインストールできない可能性があります。
- 学習用途や一般的な開発マシン(Ubuntu, macOS, WSL2など)であればほぼ問題ないと思われますが、CI環境やコンテナベースの本番にそのまま流用する場合は、自分たちの環境でMiseのprecompiled Rubyが安定して使えるかを確認する必要があります。
- ガイドが「Miseのprecompiledバイナリ」を前提としたことで、
テスト・挙動上の変更はなし
- このPRはガイド文書のみの変更であり、Rails本体の挙動やAPIには一切変更がありません。
- バージョンアップしたRuby/Rails/Ubuntuの組み合わせによる挙動の差分は、今回のPRというより、各プロジェクト(Ruby/ Rails/ OS)の変更の影響として別途考える必要があります。
- 参考情報 (あれば)
- Mise リリースノート(precompiled Ruby対応の詳細)
- precompiled Ruby バイナリ一覧(対応バージョン・プラットフォームの確認用)
- Rails Guides(Install Ruby on Rails)
- 該当のガイド本文は、
guides/source/install_ruby_on_rails.mdに対応する公開版を参照してください(本PRマージ後の内容が反映されます)。
- 該当のガイド本文は、
#58323 Deprecate passing binds to insert, update, and delete
マージ日: 2026/8/1 | 作成者: @kamipo
- 概要 (1-2文で)
ActiveRecord::ConnectionAdaptersのinsert,update,deleteに対して、従来の「第2引数以降でbindsを渡す」呼び出し方が非推奨になりました。代わりに、SQL文字列とバインド値をまとめたArel.sql(..., *binds)を渡す書き方に移行するための正式なデプリケーションです。
- 変更内容の詳細
非推奨になる呼び出し方
これまで多くのアダプタ実装やアプリケーションコードでは、以下のように sql と binds を別々に渡す呼び出し方が可能でした:
connection.insert("INSERT INTO topics (title) VALUES (?)", nil, nil, [["title", "hello"]])
connection.update("UPDATE topics SET title = ? WHERE id = 1", nil, [["title", "hi"]])
connection.delete("DELETE FROM topics WHERE id = ?", nil, [["id", 1]])PRでは、この「binds を位置引数 (positional) で渡すインターフェイス」が 非推奨 (deprecate) されます。insert, update, delete すべてが対象です。
推奨される新しい呼び出し方
今後は、SQL文字列とそのバインド値を Arel.sql にまとめて渡し、to_sql_and_binds が AST から自動的にバインドを抽出するスタイルが推奨されます。
connection.insert(
Arel.sql("INSERT INTO topics (title) VALUES (?)", "hello")
)
connection.update(
Arel.sql("UPDATE topics SET title = ? WHERE id = 1", "hi")
)
connection.delete(
Arel.sql("DELETE FROM topics WHERE id = ?", 1)
)Arel.sql("... ?", *binds) は Arel::Nodes::BoundSqlLiteral を返し、Model.where("... = ?", value) と同じ仕組み(SQL + バインドをASTに載せる)で扱えるようになります。
コード上の変更点の概要
activerecord/lib/active_record/connection_adapters/abstract/database_statements.rbinsert,update,deleteが、bindsを位置引数で受け取った場合にデプリケーション警告を出すように変更。- 内部的には、
Arel.sql由来のArel::Nodes::BoundSqlLiteralが渡された場合に、そのASTからバインド値を抽出して処理する形が明確化。
CHANGELOG.md- 上記仕様変更についての記載が追加。
adapter_test.rb,database_statements_test.rb- 旧来の呼び出しパターンが「動くが警告が出る」こと、および新パターンが期待どおり動作することを確認するテストが追加/更新。
- 影響範囲・注意点
主に影響を受けるのはどんなコードか
- アダプタやライブラリで、以下のように 生SQL + binds を直接
insert/update/deleteに渡しているコード:- 例:ruby
connection.update(sql, "Some Query", nil, binds)
- 例:
- アプリケーション側で
connection.raw_connectionではなく、ActiveRecord::Base.connection.insert/update/deleteを直接使い、
SQL とバインドを別々に渡しているケース。
実際の影響
- 現時点(このPRが入った時点)では:
- 機能自体はまだ動作するが、
bindsを位置引数で渡すと デプリケーションの警告が出る。 - 将来のメジャーバージョン(または指定バージョン)でこの呼び出し方が削除される可能性が高い。
- 機能自体はまだ動作するが、
Arel.sqlベースに書き換えないと、Rails の将来バージョンにアップグレードした際にArgumentErrorやNoMethodErrorになるリスクがある。
マイグレーションの方針
- 旧スタイル:ruby
connection.insert("INSERT ... VALUES (?)", "Insert Topic", nil, [[nil, "hello"]]) - 推奨スタイル:ruby
connection.insert( Arel.sql("INSERT ... VALUES (?)", "hello") ) - カスタムアダプタ・ライブラリでは、以下のような対応を検討するとよいです:
insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])のようなシグネチャを使っている場合、- 外部公開APIであれば、
binds引数の利用を非推奨にしてArel.sql経由に切り替えを促す。 - 内部用であれば、
Arel::Nodes::BoundSqlLiteralを前提としたシグネチャに整理する。
- 外部公開APIであれば、
注意すべき技術的ポイント
Arel.sqlが SQL文字列に対してバインドを埋め込むのではなく、SQLとバインドを一緒にASTとして持つ 点に注意。- 手動で
?をquoteで埋め込むのではなく、あくまで?プレースホルダとバインド値をArel.sqlに渡す。
- 手動で
- 既存の
Model.where("name = ?", "foo")と同じスタイル・同じ仕組みでクエリを扱えるため、
SQL構築まわりのコードを Arel ベースに寄せていく上で一貫性がとれる。
- 参考情報 (あれば)
- 本PRで参照されている過去のPR: #29944
- bind パラメータを Arel AST に移した際、既存の「生SQL + binds」利用者のために
bindspositionals を一時的に復活させた経緯がある。
- bind パラメータを Arel AST に移した際、既存の「生SQL + binds」利用者のために
- 関連クラス・メソッド:
Arel.sql(string, *binds)→Arel::Nodes::BoundSqlLiteralActiveRecord::ConnectionAdapters::DatabaseStatements#to_sql_and_bindsModel.where("... = ?", value)
これと同様のバインド管理の仕組みを、insert/update/deleteでも統一していくための変更。
#58324 Fix String#parameterize raising TypeError when separator is nil
マージ日: 2026/8/1 | 作成者: @hammadxcm
- 概要 (1-2文で)
String#parameterizeにseparator: nilを渡すとTypeErrorで落ちていた不具合を修正し、nilを空文字と同等に扱うようにした PR です。これにより、separatorを設定ファイルやパラメータからそのまま渡しても安全に動くようになります。
- 変更内容の詳細
何が問題だったか
ActiveSupport::Inflector#parameterize(String#parameterize の実体)は、内部で次のような処理をしていました。
parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator)
unless separator.nil? || separator.empty?
# 複数連続したセパレータを1つにまとめる
# 先頭/末尾のセパレータを削除する
end意図としては:
separatorがnilまたは""(空文字)の場合:- セパレータを挿入しない(そのままくっつける)
- それ以外(
"-","_"など)の場合:- 余分なセパレータをまとめたりトリムしたりする
という設計でした。
しかし Ruby の gsub! に replacement = nil を渡すと TypeError になるため、
"Donald E. Knuth".parameterize(separator: nil)
# => TypeError: no implicit conversion of nil into Stringと、separator.nil? を見る前に落ちてしまっていました。
つまり「separator が nil の場合を考慮したガード」は存在するのに、実際にはそこまで到達できないという矛盾した状態でした。
どう直したか
separator: nil を separator: "" と同等に扱うように変更し、ガードロジックを整理しています。
変更のポイント:
nilを空文字として扱うgsub!に渡す前に、separatorがnilの場合は""に正規化するようにしています(実装的にはnilのままgsub!に行かない形に整理)。結果として:
ruby"Donald E. Knuth".parameterize(separator: nil) # => "donaldeknuth" "Donald E. Knuth".parameterize(separator: "") # => "donaldeknuth"のように、両者は同じ動作になります。
separator.nil?チェックを削除nilと空文字が同じ経路を通るようにしたため、rubyunless separator.nil? || separator.empty?という条件は冗長になり、
nil?チェックが削除されています。
実質的には「空文字相当ならセパレータ処理をスキップする」という、もともとの意図をそのままシンプルにした形です。テスト追加
既存の「セパレータなし("")」ケースと同じテストパターンを、separator: nilに対しても追加しています。StringToParameterizeWithNoSeparatorStringToParameterizePreserveCaseWithNoSeparator
の2つのフィクスチャを再利用し、
preserve_case: true/ デフォルト両方をカバー。テスト結果(activesupport の該当テスト一式):
- 645 runs
- 3207 assertions
- 0 failures / 0 errors
- RuboCop も問題なし
- 影響範囲・注意点
影響範囲
- 影響を受けるのは
String#parameterizeにseparator: nilを渡すケースのみ です。 - これまで
TypeErrorが発生していたケースが、"TypeErrorで落ちる" → "文字列として正しくパラメータ化される"に変わります。 separator: ""を使っていた既存コードの挙動は変わりません(nilがその挙動に揃えられただけ)。
- 影響を受けるのは
後方互換性
- 実際に
separator: nilを意図して使っていた場合:- これまでは例外が発生 → 今後は
"donaldeknuth"のようにパラメータ化されるようになるので、「エラーで気づく」パターンを前提にしていた場合は挙動が変わる点に注意が必要です。 - ただし「
nilをサポートしない」方向(nilガードを削除して常に TypeError にする)ではなく、「自然に動く」方向に寄せたため、一般的なアプリケーションにとっては改善と見なせる変更です。
- これまでは例外が発生 → 今後は
- 設定ファイルや params から
separatorをそのまま渡しているコードにとっては、nilの場合も安全に動作するようになるため、想定外エラーのリスクは減ります。
- 実際に
注意点
- バリデーションや例外発生有無で挙動をテストしていた場合(例: 「不正な separator なら TypeError」などの仕様を勝手に決めていた場合)、テストが通らなくなる可能性があります。
- 「
nilを指定したらエラーになるべき」と考えていた場合、この PR はそれと逆方向の挙動になります。もしそのポリシーを保ちたいプロジェクトでは、アプリケーション側でseparatorを検証してください。
- 参考情報 (あれば)
- 対象メソッド:
String#parameterize(ActiveSupport::Inflector#parameterize)- 実装ファイル:
activesupport/lib/active_support/inflector/transliterate.rb
- テストファイル:
activesupport/test/core_ext/string_ext_test.rb- (関連として)
inflector_test.rb,transliterate_test.rbも通過済み
- PR方針の判断:
- 「
nilを非サポートとしてエラーにする」のではなく、「""と同じ扱いにして実用上の驚きを減らす」方向が選択されています。
- 「
#58325 Add test coverage for NameError#missing_name branches
マージ日: 2026/8/1 | 作成者: @hammadxcm
- 概要 (1–2文で)
NameError#missing_nameの全ての分岐をカバーするテストが追加され、これまでテストされていなかったケース(receiver == Object、receiver呼び出し時のArgumentError、メッセージからの名前抽出)が網羅されました。機能変更はなく、既存挙動の仕様化・回帰防止が目的です。
- 変更内容の詳細
NameError#missing_name の分岐と今回のテスト
NameError#missing_name は、例外がどのレシーバー(self.receiver)に対して起きたかによって名前の取り出し方を変えています:
receiver = begin
self.receiver
rescue ArgumentError
nil
end
if receiver == Object
name.to_s
elsif receiver
"#{real_mod_name(receiver)}::#{self.name}"
else
if match = message.match(/((::)?([A-Z]\w*)(::[A-Z]\w*)*)$/)
match[1]
end
endこのうち、従来テストでカバーされていたのは:
elsif receiverブランチ(モジュール/クラス配下の定数が見つからないケース)- 「定数エラーではない場合の早期リターン」
のみでした。今回の PR では、以下 3 ケースを新たにテストしています。
1) receiver == Object のケース (トップレベル定数)
テスト名: test_missing_top_level_constant_is_not_namespaced
対象ケース: トップレベル定数参照で NameError が発生した場合。
Ruby では、トップレベルの定数参照のレシーバーは Object になるため、この分岐が使われます。
例(イメージ):
begin
HelloWorld # 未定義
rescue NameError => e
e.missing_name # => "HelloWorld" を期待
endこの挙動は NameError#missing_name のドキュメントに載っている例ですが、これまでテストされていませんでした。今回のテストで、トップレベルの未定義定数に対して missing_name が名前空間を付けず、そのまま "HelloWorld" を返すことが保証されます。
2) receiver 呼び出しが ArgumentError を投げるケース
テスト名: test_missing_name_falls_back_to_the_message_without_a_receiver
対象ケース: NameError が「レシーバー情報なし」で生成されている場合。
その場合 NameError#receiver を呼ぶと ArgumentError が発生することがあり、実装側ではそれを rescue して receiver = nil としています。
ここでは:
self.receiver呼び出しでArgumentErrorが起きる- 結果的に
receiverがnilになる - その場合、例外メッセージ
messageを正規表現でパースし、末尾の「定数っぽいもの」を抽出する
というフォールバック経路がテストされています。
例(イメージ):
e = NameError.new("uninitialized constant Foo::Bar")
e.missing_name # => "Foo::Bar" を message から抽出このように、「レシーバー情報を持たない NameError でもメッセージから定数名を復元する」挙動がテストで固定されました。
3) メッセージが定数に関するものではないケース
テスト名: test_missing_name_ignores_a_message_that_is_not_about_a_constant
対象ケース: NameError だが、メッセージが「未定義定数」に関するものではない(もしくは定数名の形式にマッチしない)場合。
この場合、正規表現マッチが失敗するため:
missing_nameはnilmissing_name?はfalse
となることをテストで保証しています。
例(イメージ):
e = NameError.new("undefined local variable or method `foo' for main:Object")
e.missing_name # => nil
e.missing_name? # => falseこれにより、「すべての NameError が定数名を持つとは限らない」という前提をメソッド側が尊重していることが確認されています。
- 影響範囲・注意点
影響範囲
- 変更は
activesupport/test/core_ext/name_error_test.rbのテスト追加のみで、本体コード (active_support/core_ext/name_error.rb) には一切変更なし。 - ただし、テストによって
NameError#missing_name/missing_name?の現状仕様(特にメッセージフォールバックとnil戻り値の扱い)が明確に固定されるため、将来の挙動変更には注意が必要になります。
- 変更は
利用者視点の注意点
- トップレベルの未定義定数では
missing_nameは"Foo"のように非修飾名を返し、ネストした定数 (Foo::Bar) の場合は"Foo::Bar"と名前空間付きで返される、という挙動が前提になっています。 NameErrorが常に定数についてとは限らないため、missing_nameを使うコードではnilを返し得ることを前提にする必要があります(今回のテストでこれが仕様として強化された形)。
- トップレベルの未定義定数では
- 参考情報 (あれば)
- 該当コード:
active_support/core_ext/name_error.rb - この PR によるカバレッジ:
active_support/core_ext/name_error.rb: 17/21 行 → 21/21 行(100%)activesupport/test/core_ext/: 1474 テスト / 6124 アサーション / 0 失敗, 0 エラー
- RuboCop もクリーンでスタイル上の問題なし。
#58329 Fix documented strftime format for week_field default value
マージ日: 2026/8/1 | 作成者: @55728
- 概要 (1-2文で)
week_fieldヘルパーのデフォルト値に使われるstrftimeフォーマットについて、実装とドキュメントの不一致を解消し、ドキュメント側を実装 ("%G-W%V") に合わせる修正です。コードの挙動は既に正しいため、このPRはドキュメントのみの更新です。
- 変更内容の詳細
何が問題だったか
WeekField#format_datetime(week_fieldのデフォルト値生成)が内部的に使っているフォーマットはstrftime("%G-W%V")。- しかし
week_fieldの API ドキュメントには、古いまま"%Y-W%W"と記載されていた。 - そのため「
week_fieldのデフォルトフォーマットは%Y-W%W」と勘違いする可能性があった。
なぜ %G-W%V が正しいのか
<input type="week"> の値は「ISO 8601 週日付」を表す文字列 (yyyy-Www) で、ISO週番号年に従います。
Ruby の strftime では:
%G: ISO 8601 週番号年 (ISO week-numbering year)%V: ISO 8601 週番号 (01–53)%Y: 通常の西暦年 (Gregorian year)%W: 月曜始まりの週番号 (00–53, ただしISOではない)
したがって、HTML仕様に合致させるなら %G-W%V の組み合わせを使うのが正しく、実装側もすでにこれになっています。
PRにあるサンプル:
date = Date.new(2015, 12, 28)
date.strftime("%G-W%V") # => "2015-W53" # 実際のヘルパーの出力
date.strftime("%Y-W%W") # => "2015-W52" # ドキュメント上の誤った表記2015-12-28 は ISO 週日付では「2015-W53」であり、%Y-W%W を使うと「2015-W52」とズレることが分かります。
このように、年末年始付近では ISO 週番号と暦年がずれるため、%Y/%W 組み合わせは <input type="week"> 用として不適切です。
このPRが行ったこと
actionview/lib/action_view/helpers/form_helper.rb内のドキュメント中の"%Y-W%W"という1行を"%G-W%V"に修正。- 実装・テストはすでに別PR (#57890) で修正済みのため、このPRではドキュメントのみの同期を行っています。
- 影響範囲・注意点
実行時の挙動は変わりません
すでにweek_fieldは%G-W%Vで値を生成しており、このPRはドキュメントの表記を追従させるだけです。既存アプリへの直接の破壊的変更はなし
ただし、以下の開発者は注意が必要です:- Rails のドキュメントを見て、
week_fieldのデフォルトフォーマットが%Y-W%Wだと思い込んでいた人 - それに合わせて自前実装やテストデータを
%Y-W%Wベースで書いていた人
実際のヘルパーはすでに
%G-W%Vを出しているので、「ドキュメントを読んで真似していたコード」がある場合は、そこを%G-W%Vに揃える必要があります。- Rails のドキュメントを見て、
ISO週と暦年の違いを意識する必要があるケース
年末年始近辺の日付を<input type="week">とやり取りする処理では、ISO週番号年を前提にロジックを書く必要があります。
例えば「2015-W53」と返ってきた値は、カレンダー上では 2015年末〜2016年始を指す可能性があり、「yyyyをそのまま暦年とみなす」という扱いは誤りになることがあります。
- 参考情報 (あれば)
関連PR:
- #57890:
WeekField#format_datetimeの実装を%G-W%Vに変更したPR - #54355: 実装+ドキュメントの両方を修正しようとしていた元PR(実装・テスト部分は #57890 で取り込まれ、本PRでドキュメント部分が反映)
- #57890:
用語整理:
- ISO 8601 week-numbering year: 週単位で年を数える体系。年の最初の木曜を含む週を「第1週」とするため、1月1日が「前年の第52/53週」になったり、その逆になったりする。
<input type="week">仕様: ISO 8601 週間日付 (YYYY-Www) を前提としており、Rails側で%G-W%Vを使うのはこの仕様に忠実な実装。
#58331 Fix model_name crashing when a model has an anonymous ancestor
マージ日: 2026/8/1 | 作成者: @55728
- 概要 (1-2文で)
Rails 7.2 開発中に入った変更 (#58298) により、「無名クラスを祖先にもつモデル」でmodel_nameを呼ぶと例外が出るリグレッションが発生していたのを、i18n キー生成時に無名祖先をスキップすることで修正した PR です。通常のモデルの動作や i18n 探索順序は変えずに、無名祖先を介したクラス継承でもmodel_nameを安全に使えるようにしています。
- 変更内容の詳細(あればサンプルコードも含めて)
問題の背景
元の変更 (#58298) で以下が行われていました:
ActiveModel::Nameを Ractor 共有可能にするためにName#freezeが導入されたfreeze時に i18n 用のキー (i18n_keys) を事前計算 (eager) するようになったlookup_ancestorsで祖先チェーンをたどり、それぞれのklass.model_nameから i18n キーを構築する
ここで、以下のように匿名クラスを中間クラスとして使うと:
base = Class.new(ActiveRecord::Base) # 匿名の中間クラス
Widget = Class.new(base) # 名前付きモデル
Widget.model_namebase は name が空のクラスなので、Name.new の内部で
- 「クラス名が空なので name を明示的に渡せ」という
ArgumentErrorが発生 - その結果、
Widget.model_name自体が落ちる
以前は:
- i18n キーは遅延計算だった
model_name自体は成功し、humanを呼んだときに初めて祖先をたどっていた
ため、匿名祖先がいても model_name までは動いていました。
修正内容
i18n キー生成時に「無名クラスの祖先をスキップする」ように変更しています。
擬似コードで表すと、おおよそ次のような変化です:
# 変更後イメージ
@klass.lookup_ancestors.filter_map do |klass|
# クラス名が空(blank)ならスキップ
klass.model_name.i18n_key unless klass.name.blank?
endポイント:
lookup_ancestorsで取得した各祖先クラスに対してklass.name.blank?なもの(匿名クラス)は i18n キー計算対象から外す
- そもそも匿名クラスには意味のある
model_name/i18n キーがないので、スキップしても翻訳解決順序的に問題はない - これにより
Widgetのような名前付きモデルは:- 祖先チェーンに匿名クラスが混ざっていても
model_nameが例外を投げなくなる - i18n 探索順序は「名前のある祖先クラス」については従来通り
- 祖先チェーンに匿名クラスが混ざっていても
テスト
activemodel/test/cases/naming_test.rb に回帰テストが追加されています。
Name#freeze→i18n_keysを直接たたく形でテスト- Ruby が Ractor をフルサポートしていなくても、
freezeによる eager 計算経路を確実に通せる
- Ruby が Ractor をフルサポートしていなくても、
- 修正前は
ArgumentErrorが発生してテストが red - 修正後は例外なく i18n キーが生成され green
- 影響範囲・注意点
影響範囲
影響を受けるケース
- ActiveRecord/ActiveModel で、匿名クラスを中間クラスとして継承しているモデルで
model_nameを経由するあらゆる機能:form_for/form_withurl_for,polymorphic_urlなどルーティングヘルパto_partial_path/render @recordのパーシャル解決- シリアライゼーション (
as_json等)
- これらが PR #58298 以降のコードベースで
ArgumentErrorによって壊れていたが、本 PR により復旧
- ActiveRecord/ActiveModel で、匿名クラスを中間クラスとして継承しているモデルで
影響を受けないケース
- 通常の「すべての祖先クラスに名前がついている」モデル
- 匿名クラス自身に対して
model_nameを呼ぶ場合- 今回の PR でも引き続き
ArgumentErrorを投げる(従来通り)
- 今回の PR でも引き続き
i18n の動作影響
- 匿名祖先には
model_nameで利用されるような翻訳キーは本来ないため、無視しても現実的なケースでは問題なし - 名前付きの祖先クラスの解決順序は従来通り維持される
- 例:
Admin::User < User < ApplicationRecord UserやApplicationRecord由来の i18n キーの探索順序は変わらない
- 例:
バージョン・互換性
- このリグレッションは #58298 以降の未リリースコードでのみ発生
- 公式にリリースされた Rails バージョンはそもそも影響を受けていない
- そのため CHANGELOG の更新はなし
- 参考情報 (あれば)
- 関連 PR
- #58298:
ActiveModel::Nameを Ractor 共有可能にするためのName#freeze導入と i18n キーの eager 計算
- #58298:
- 実務上の示唆
- engine やプラグイン、テスト等で、
Class.new(ActiveRecord::Base)のような匿名ベースクラスを挟んでモデルを定義していても、この修正以降はform_for/url_for/renderなどフレームワーク全体で安全に扱えるようになります - 匿名クラスに対して
model_nameが必要なケースは引き続きサポート外であり、必要ならクラスに名前をつけるべき、という従来方針も維持されています
- engine やプラグイン、テスト等で、
#58307 Avoid redundant worker pool dispatch for default Action Cable streams
マージ日: 2026/8/1 | 作成者: @npezza93
- 概要 (1-2文で)
Action Cable の「デフォルトストリーム」におけるメッセージ配送で、同じジョブを executor と connection worker pool の2段階に重ねてディスパッチしていた無駄を取り除き、パフォーマンスを改善する変更です。ユーザー定義コールバックは従来通り worker pool で隔離しつつ、デフォルトハンドラだけ executor 上で完結させることで、レイテンシとスループットが大きく改善されています。
- 変更内容の詳細
背景となるボトルネック
Action Cable でメッセージが publish されて subscriber に届くまでのフローには、少なくとも以下のスレッド/キューが関わります:
アダプタ (Redis / Solid Cable など)
- サブスクライバーごとの callback を Action Cable の executor 上に enqueue して実行していた。
ストリームハンドラ (
ActionCable::Channel::Streams)- サブスクライバー callback 内で、さらに connection の worker pool にジョブを enqueue し、最終的な処理(ペイロードの decode やユーザーコードの実行)を行っていた。
問題は、デフォルトストリーム(単にメッセージを decode してクライアントに流すだけのケース)でも、必ず executor → worker pool という二重ディスパッチをしていた点です。
ユーザー定義のコールバック(任意のアプリケーションコードが走るもの)は worker pool で隔離する意味がありますが、単にデフォルトの decode + 送信だけなら、すでに executor 上で動いている処理をさらに worker pool に投げ直す必要はありません。
この二重ディスパッチが 750クライアント × 10メッセージ/秒 × 300秒 というような高負荷時に大きなオーバーヘッドとなり、全アダプタで遅延と処理落ちが起きていました。
具体的な変更点
変更ファイルは以下の2つです。
actioncable/lib/action_cable/channel/streams.rbactioncable/test/channel/stream_test.rb
要点:
「デフォルトストリーム」と「ユーザー定義コールバック」を分岐して扱うようにした
- デフォルトストリーム: executor 上で完結させる(worker pool への再ディスパッチをしない)。
- ユーザー定義の subscription コールバック: 従来通り connection の worker pool に enqueue する。
ストリームハンドラの実装変更
- 以前は「すべてのストリームハンドラが connection の worker pool に投げられる」形だったところを、
- 「ハンドラがデフォルト(内部実装のハンドラ)か、ユーザー定義か」を判別して処理分岐するようにしています。
- ざっくりいうと、以下のイメージに近い変更です(実際のコードから簡略化したイメージ):
ruby# もともとのイメージ def stream_from(broadcasting, callback = nil) # adapter が executor 上でこのブロックを呼ぶ subscribe_to(broadcasting) do |message| # ここでさらに connection.worker_pool に投げていた connection.worker_pool.post do (callback || default_stream_callback).call(decode(message)) end end end # 変更後のイメージ def stream_from(broadcasting, callback = nil) # adapter が executor 上でこのブロックを呼ぶ subscribe_to(broadcasting) do |message| data = decode(message) if callback # ユーザー定義コールバック connection.worker_pool.post do callback.call(data) # 任意アプリケーションコードは worker pool 上で end else # デフォルトハンドラ default_stream_callback.call(data) # executor 上でそのまま実行 end end end実際の実装ではもう少し抽象化されていますが、要は「内部デフォルトハンドラは executor 上で完結、ユーザーコードは worker pool に逃がす」という分岐が入っています。
- 以前は「すべてのストリームハンドラが connection の worker pool に投げられる」形だったところを、
テスト (
stream_test.rb) の追加- 新たに追加されたテストでは、
- デフォルトストリームが worker pool に重ねてディスパッチされないこと
- ユーザー定義コールバックは引き続き worker pool を使うこと
を確認しています。
- これにより、回帰を防ぎつつ、本 PR の前提となる実行コンテキストの違いが担保されます。
- 新たに追加されたテストでは、
ベンチマーク結果
作者が提示しているベンチマーク結果の要約:
750 VUs / 7 msgs/sec
main (変更前):
Metric Redis Solid Cable SQLite 平均 106ms 289ms p95 156ms 437ms 本 PR 適用後:
Metric Redis Solid Cable SQLite 平均 90ms 127ms p95 123ms 180ms
Redis / Solid Cable ともに平均・p95 ともに大きく改善しています。Solid Cable では特に顕著です。
750 VUs / 10 msgs/sec
main (変更前):
Metric Redis Solid Cable SQLite 平均 fails fails p95 fails fails 本 PR 適用後:
Metric Redis Solid Cable SQLite 平均 84ms 160ms p95 116ms 269ms
変更によって、7 msgs/sec が限界だったシナリオで 10 msgs/sec まで耐えられるようになり、約 35% のスループット向上が確認されています。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは Action Cable のチャンネルストリーム (
stream_from等) を使ったメッセージ配送経路です。 - 特に、以下のような「普通のブロードキャストのみ」のケースで効果が大きいです:
- 多数のクライアントが同一チャンネルに subscribe
- サーバ側はほぼ
ActionCable.server.broadcastだけをしている - 各クライアント側の処理は標準の stream ハンドラ(メッセージをそのまま JavaScript に届ける程度)しか行わない
互換性・挙動面
- ユーザー定義のコールバックは引き続き worker pool 上で実行されるため、アプリケーションコードの実行コンテキストは従来と変わりません。
- したがって、「どのスレッド/プールでアプリケーションコードが走るか」という意味での互換性は維持されています。
- 変更されるのは 内部のデフォルトストリームの実行場所(executor と worker pool のどちらで完結するか)だけです。
- これにより以下のような性質が維持されます:
- 任意のアプリケーションコードは worker pool 上で隔離される
→ 長時間ブロックする処理が executor を詰まらせにくい - デフォルトの decode + 送信だけであれば executor 上で高速に処理される
→ 高負荷時のレイテンシ・スループットが改善
- 任意のアプリケーションコードは worker pool 上で隔離される
注意点・留意事項
- executor 上で走る処理がやや増えることになるので、もし独自拡張で「デフォルトストリームに見えるが、実際には重い処理をしている」ようなコードがある場合は注意が必要です。
- ただし、標準の設計としては「重い処理は明示的なユーザーコールバック内で行う」想定なので、一般的なアプリケーションでは問題になりにくいはずです。
- CHANGELOG は(現時点で)更新されていないため、リリースノート等でこの改善点を見落とさないようにする必要があります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58307
- 関連コンセプト:
- Action Cable executor: コネクション・チャンネルレベルの処理を実行するためのスレッドプール/実行コンテキスト
- Connection worker pool: ユーザーコード(チャンネルのメソッド内のアプリケーションロジックなど)を実行するためのワーカープール
- 実務的には、大量接続・高頻度ブロードキャストを行う Action Cable アプリ(チャット、ライブ更新ダッシュボード、マルチプレイヤーゲームなど)では、この変更を含む Rails バージョンに上げることで、アプリ側のコード変更なしにレイテンシ改善が見込めます。
#58332 Revert "Remove migrations filter from the schema dump"
マージ日: 2026/7/31 | 作成者: @fxn
- 概要 (1-2文で)
Rails 7.2 付近で一度削除された「スキーマダンプから古いマイグレーションの version をフィルタする仕組み」を取り消し(revert)し、従来どおりフィルタを有効に戻した PR です。
チーム開発時に「古いマイグレーションをリポジトリから削除する」運用を、実務的に破綻なく行う手段がまだ整理できていないため、仕様変更を保留にする目的のリバートです。
- 変更内容の詳細
2-1. 背景・問題意識
Revert 対象の #58322 では、「db/schema.rb 生成時に、schema_migrations テーブルの全バージョン情報から、既にファイルが存在しない古いマイグレーション分を ‘フィルタして捨てる’ 挙動」を削除していました。
今回の PR はそれを元に戻します。
作者の懸念点は主に次です:
- 古いマイグレーションを削除したい典型的なユースケース(例: 数年分の履歴を整理してレポジトリを軽くする)が、フィルタがないと実務的にかなり難しい。
- 手順としては「古いマイグレーションファイル削除 →
db/schema.rbの末尾(trailer)からバージョンも削除 → それをコミット・デプロイ」が想定されるが、- チームの他のメンバーのローカルDBには、依然として古い
schema_migrationsレコードが残っている。 - その状態で
bin/rails db:migrateをすると、また古いバージョン情報がdb/schema.rbの末尾に書き戻され、「削除したはずの履歴」が復活してしまう。
- チームの他のメンバーのローカルDBには、依然として古い
- この問題を回避しようとして「古いバージョンを
schema_migrationsから削除するデータマイグレーション」を用意すると、- そのデータマイグレーション自体も “マイグレーション” であり、結局また履歴に残る。
- 誰かがそのマイグレーションを適用し忘れる(長期休暇等)と、その人だけ
schema_migrationsがクリーンにならない。
- 「削除済みマイグレーションのバージョン情報をどこかに別途保持しておいて、スキーマダンプ時に参照する」ようなメタ的仕組みも考えられるが、それは「履歴を repo から消したい」というモチベーションと相反する。
結局、「フィルタがある状態」の方が、古いマイグレーション整理のユースケースにとって現実的な落としどころだろう、という判断でリバートされています。
2-2. コードレベルの変更
変更ファイルは以下の3つです:
activerecord/lib/active_record/schema_dumper.rbactiverecord/test/cases/schema_dumper_test.rbguides/source/active_record_migrations.md
schema_dumper.rb
ここに「マイグレーションのフィルタ」を復活させるロジックが戻っています。
おおざっぱには、ActiveRecord::SchemaDumper が db/schema.rb を生成する際に行う:
schema_migrationsテーブル(および multi-DB 対応の場合はar_internal_metadataなど)からバージョンを取得- そのうち「現在レポジトリに存在するマイグレーションファイルに対応しない古いバージョン」を除外
- フィルタ済みのバージョンのみを
db/schema.rbの末尾(いわゆる trailer 部分)として出力
という挙動が再度有効になっています。
擬似コードで書くと、ざっくり次のようなイメージが復活した形です(正確なコードではありませんが挙動のイメージとして):
def dump_migration_versions(stream)
migrated = ActiveRecord::Base.connection.migration_context.get_all_versions
existing = ActiveRecord::Base.connection.migration_context.migrations.map(&:version)
# repo に存在するマイグレーションの version だけ残す
filtered = migrated & existing
stream.puts " # These are migrations that have been run"
filtered.sort.each do |version|
stream.puts " add_migration_version(#{version})"
end
endPR #58322 では、この filtered を使う処理を削って「フィルタせずにすべての version をダンプする」方向へ変えていましたが、本 PR で再び filtered を使う形に戻っています。
テスト (schema_dumper_test.rb)
- フィルタの存在を前提としていた既存テストの期待値が、#58322 で変更されていたのを、今回また元に戻しています。
- つまり「マイグレーションファイルが削除された version は、
db/schema.rbには出てこない」ことを確認するテストが再び有効に。
- つまり「マイグレーションファイルが削除された version は、
ガイド (active_record_migrations.md)
- 「
schema.rbにマイグレーションバージョンがどう出力されるか」「古いマイグレーションを削除した時にどう振る舞うか」に関する説明が、#58322 時点の内容から、従来どおり「フィルタが存在する」前提の文章に戻されています。 - ガイド上も、「古いマイグレーションを削除しても、
schema.rbの trailer には残さない(/フィルタされる)」挙動を前提とした説明になります。
- 影響範囲・注意点
3-1. 実務への影響
- アプリケーションで
schema_format = :ruby(デフォルト)を使っている場合:- 「古いマイグレーションファイルを削除すると、スキーマダンプ時に
schema_migrationsの対応する version も自動で落ちる(= trailer に残らない)」従来動作に戻ります。 - そのため、古いマイグレーションの「リポジトリからのクリーンな削除」が、相対的にやりやすくなる状態です。
- 「古いマイグレーションファイルを削除すると、スキーマダンプ時に
schema_format = :sqlを使っている場合:- このフィルタは
db/structure.sqlには適用されないため、:rubyと:sqlで挙動が揃わない状態のままになります。 - 作者もこの点は認識しており、「どちらのフォーマットが実務的なトレードオフとして適切か」はまだ結論が出ていないという含みがあります。
- このフィルタは
3-2. ブランチ切り替え時の注意
- #58322 が解決しようとしていたユースケースのひとつに「ブランチを切り替えたときに
db/schema.rbが ‘汚れる’」という問題がありますが、- 作者の認識では、「いずれにせよ
db/schema.rbはブランチ切り替えで汚れがちであり、不要な変更は捨てる(git checkout -- db/schema.rb等)運用になるので、フィルタ有無による差はそこまで決定的ではない」という整理。 - 文書(ガイド)も、なぜ version が揃わないことがあるのかを説明しているので、実務では「スキーマファイルは頻繁にリセットするもの」と割り切る前提で使うことになります。
- 作者の認識では、「いずれにせよ
3-3. マイグレーション整理フロー
今回のリバートにより、Rails チームとしては現時点で次のような運用を想定していると読めます:
- 古いマイグレーション群を削除する(ファイルを物理削除)
- チームメンバーが今後
db:migrate/db:schema:dumpするたび、schema.rbの trailer からそれら古いバージョンが自動で消えた形になる- ローカルDB には古い version が残っていても、
schema.rbには反映されない
- これにより、
- レポジトリ上の
schema.rbは「今もファイルが存在するマイグレーションだけ」を表す schema_migrationsテーブルがメンバーごとに必ずしも一致しなくても、運用上はさほど問題にならない
- レポジトリ上の
一方で、「schema_migrations テーブルを完全にクリーンにしたい」「バージョンを正規化したい」といった要求には、まだ標準で綺麗に答えられていない状態であり、その点を解くまでは仕様を動かさない、という判断でもあります。
- 参考情報 (あれば)
- Reverted PR:
- https://github.com/rails/rails/pull/58322
- 「schema dump からマイグレーションフィルタを削除する」試み。今回の PR で取り消し。
- https://github.com/rails/rails/pull/58322
- 本 PR:
- ガイド(該当セクションの最新内容はこの PR 適用後を参照):
guides/source/active_record_migrations.md(公式ガイドの「Active Record Migrations」)
#58274 Allow prepared statements with query log tags
マージ日: 2026/7/31 | 作成者: @bschrag620
- 概要 (1-2文で)
Railsでクエリログタグ(query_log_tags)を有効にした際でも、アプリ側の選択で prepared statements を無効化せずに使い続けられるオプションが追加されました。これにより、高カーディナリティなコメントを避けるという従来の安全デフォルトを維持しつつ、必要なアプリケーションは両方を併用できるようになります。
- 変更内容の詳細
背景 / 現状
これまでのRails(Active Record)の挙動:
query_log_tags_enabledを有効にすると、Railtie側でdisable_prepared_statementsが自動的にtrueに設定される。- 理由: クエリコメント(ログタグ)が毎回異なる値になると(高カーディナリティ)、prepareされたステートメントのキャッシュが効かなくなり、DB側でのプランキャッシュ・効率に悪影響が出るケースがあるため。
課題:
- とはいえ、プロジェクトによっては
- クエリログタグを使いたい
- かつ prepared statements も有効のまま使いたい というニーズがあるが、現状は両立できなかった。
- とはいえ、プロジェクトによっては
今回の変更ポイント
query_log_tags_enabledに3つ目の値:with_prepared_statementsを追加
- これまでの典型的な設定(
config/database.ymlなど):
production:
adapter: postgresql
# ...
query_log_tags_enabled: true # or false- 新しいオプション:
production:
adapter: postgresql
# ...
query_log_tags_enabled: :with_prepared_statements- 意味:
true- クエリログタグを有効化
- Railtie が
disable_prepared_statements = trueを自動設定(prepared statements を無効)
false- クエリログタグは無効
- prepared statements の挙動は各アダプタのデフォルト
:with_prepared_statements(新)- クエリログタグを有効化
- Railtie は
disable_prepared_statementsを触らない- つまり、prepared statements は 無効化されない(アダプタ/他の設定のデフォルトのまま)
- ActiveRecord Railtie のロジック変更
activerecord/lib/active_record/railtie.rbの中で行っている初期化処理が1行差し替えられ、query_log_tags_enabledが真値(true)のときにdisable_prepared_statementsを必ずtrueにしていたコードが、:with_prepared_statementsの場合はdisable_prepared_statementsを設定しないように分岐されました。
(PRのdiffは最小限ですが、Rails起動時に ActiveRecord::Base の config を組み立てる部分で、この新しいシンボル値を特別扱いする形です。)
- ドキュメント更新
guides/source/configuring.mdが更新され、query_log_tags_enabledの説明に:with_prepared_statementsという第三の選択肢が追加されています。- これにより、「タグを有効にしつつ prepared statements を維持したい場合の設定方法」が公式ガイド上で明示されました。
- テスト追加
railties/test/application/query_logs_test.rbに25行のテストが追加。- 想定される内容:
query_log_tags_enabled: :with_prepared_statementsを設定したアプリケーション構成で- クエリログタグが有効になっていること
disable_prepared_statementsが強制的にtrueにされていないこと を検証するテストケース。
- これにより将来のリグレッション(この新挙動が壊れること)を防止。
- 想定される内容:
- CHANGELOG 追加
activerecord/CHANGELOG.mdにエントリが追加され、新しいオプションの存在と挙動がリリースノートに反映されています。
- 影響範囲・注意点
既存アプリへの影響
query_log_tags_enabledをtrue/falseのまま使っている既存アプリには挙動変更はありません。true→ 従来どおり、prepared statements は自動的に無効化される。false→ 引き続きログタグは使われず、prepared statements も以前通り。
新しい値
:with_prepared_statementsを明示的に設定したアプリだけが、新しい挙動の恩恵を受けます。
このオプションを使うときの注意点
- 高カーディナリティなタグに注意
prepared statements を有効のままクエリログタグを付けると、
- クエリ毎に異なるコメント(例: リクエストID・ユーザーIDなど)が付与される場合、DB側から見ると
- SQLテキストが毎回違う →
PREPAREキャッシュの恩恵が薄くなる - プランキャッシュが大量に増える
- SQLテキストが毎回違う →
- といった問題が起こり得ます。
- クエリ毎に異なるコメント(例: リクエストID・ユーザーIDなど)が付与される場合、DB側から見ると
そのため:
- 付与するタグの粒度と種類を意識することが重要です。
- ○ より安全: コントローラ名 / アクション名 / 環境名 など低カーディナリティな情報
- △ 注意: ユーザーID / リクエストID / ジョブID / トレースID など高カーディナリティ情報
- 高カーディナリティな値はログ(アプリログ側)で別途持つか、タグの種類を厳選するのが望ましいです。
- 付与するタグの粒度と種類を意識することが重要です。
- DBアダプタごとの実装依存
:with_prepared_statementsによって Railtie がdisable_prepared_statementsを「設定しない」だけなので、- 実際に prepared statements が有効かどうかは
- アダプタのデフォルト設定
- その他の
config.active_record.disable_…系の設定 に依存します。
- 実際に prepared statements が有効かどうかは
- オラクル・MySQL・PostgreSQLなど、利用アダプタごとに prepared statements の有効化 / 無効化の方法・性能特性が異なる点に注意してください。
- 移行のステップ案
- もし「ログタグ + prepared statements」を有効にしたい場合:
- まずステージング環境で:
query_log_tags_enabled: :with_prepared_statementsを設定- 実際のSQLコメント(クエリログ)とDB側のプランキャッシュ状況・負荷を観測
- 高カーディナリティなタグを削る / 替える
- 問題なければ本番へ反映
- まずステージング環境で:
- 参考情報 (あれば)
- 対象PR:
- Rails GitHub: https://github.com/rails/rails/pull/58274
- 関連ドキュメント(更新あり):
Configuring Rails Applicationsガイド (guides/source/configuring.md) のquery_log_tags_enabledの項
- 概念的な背景:
- Railsのクエリログタグ機能そのもの:
ActiveRecord::QueryLogs - prepared statements とコメント付きSQLがプランキャッシュに与える影響については、各DB(PostgreSQL・MySQLなど)のマニュアルやブログ記事が参考になります。
- Railsのクエリログタグ機能そのもの:
#58267 activerecord: make sure tests do not rely on implicit SELECT order
マージ日: 2026/7/31 | 作成者: @viralpraxis
- 概要 (1–2文で)
このPRは、Active Record のテストが「暗黙の SELECT 結果順序(MVCC に起因する更新順など)」に依存していた部分を洗い出し、すべて明示的なORDER BYを付けるように修正するものです。これにより、DB 実装や実行タイミングによってテスト結果が変わる flakiness を防ぎ、テストの決定性・再現性を高めています。
- 変更内容の詳細
全体方針
- PostgreSQL の MVCC 特性(更新のタイミングによって行の見える順序が変わる)に依存していたテストを修正。
- 「ORDER BY を付けていない SELECT の結果順が、たまたま期待どおりになる」前提を排除。
- 具体的には:
- テスト内のクエリに
order(...)を追加 - 期待結果の配列順が、暗黙の順序ではなく、明示的なソート条件に対応するように調整
- テスト共通のヘルパーにも、順序の非決定性をあぶり出すための仕掛けが追加されている可能性が高い(
test_case.rbに +13 行)。
- テスト内のクエリに
PR 説明にある通り、この問題は PostgreSQL 拡張 pg_disorder の「reverse モード」で発見されています。pg_disorder は SELECT の返却順序を意図的に乱したり反転させることで、「暗黙のORDER BY依存」を検出するツールです。
ファイルごとの大まかな変更ポイント
※ソース全文は無いため、Rails のテスト慣習・ファイル名から推測を交えていますが、方向性は技術的に妥当なものです。
1. associations 関連テスト
activerecord/test/cases/associations/eager_test.rbactiverecord/test/cases/associations/has_many_associations_test.rbactiverecord/test/cases/associations/has_many_through_associations_test.rbactiverecord/test/cases/associations/join_model_test.rbactiverecord/test/cases/autosave_association_test.rb
これらは eager loading (includes, preload, eager_load) や has_many, has_many :through, join model、autosave など、関連を伴う読み出しをテストするファイルです。多くのテストは以下のような構造をとります:
posts = author.posts # または Author.includes(:posts).find(...)
assert_equal [post1, post2], postsこの posts の順序が、DB 依存で変わらないことを暗黙に期待していた箇所に、明示的な order が追加されたと考えられます:
posts = author.posts.order(:id)
# あるいは includes した relation 側に order を付与
authors = Author.includes(:posts).order(:id)これにより、関連ロード後の配列の順序が、テーブルの挿入順や MVCC の更新順に依存せず、一貫して id 等でソートされるようになります。
2. insert_all 関連
activerecord/test/cases/insert_all_test.rb
insert_all や upsert_all では、一括挿入後にレコードを SELECT してアサートするテストがよくあります:
result = Book.insert_all([...])
books = Book.where(id: result.rows.flatten)
assert_equal [1, 2, 3], books.map(&:id)こうした箇所に
books = Book.where(id: result.rows.flatten).order(:id)のような order が追加されていると考えられます。
特に RETURNING 句が関与する PostgreSQL では、返却順と実際のテーブルスキャン順が一致しない場合もあり、ここが暗黙の依存ポイントになりがちです。
3. relation / query 関連テスト
activerecord/test/cases/relation/load_async_test.rbactiverecord/test/cases/relation/or_test.rbactiverecord/test/cases/relations_test.rb(+15 / -15 と変更が大きい)
Relation の動作を検証するテスト群では、where, or, load_async, limit, distinct などの組み合わせで得られた結果配列の順序をアサートします。
ここで暗黙順序に依存していたケースに対し:
Model.where(...).order(:id)やorder("some_column")- もしくはテスト対象として意図した順序(例えば
name ASC, created_at DESCなど)
を明示的に付けたと考えられます。
特に relations_test.rb は Relation の基本動作のハブのようなファイルであり、さまざまなクエリパターンについて:
assert_equal [developers(:david), developers(:jamis)], Developer.where(...).to_aといった順序依存の検証が多いので、ここに 15 箇所程度 order の追加・修正が入った可能性が高いです。
4. scoping 関連
activerecord/test/cases/scoping/default_scoping_test.rbactiverecord/test/cases/scoping/named_scoping_test.rb
default_scope や named scope が順序を付与・合成した結果のテストで、「scope が暗黙的にある順序を前提にしている」ように見えるものを是正したとみられます。
例として:
default_scope { where(active: true) } # だけを期待していたテストで暗黙に挿入順を前提にしているケースがあれば、テスト側で
expected = users.where(active: true).order(:id)
assert_equal expected, User.all.to_aのように明示的に順序を固定しています。
5. message pack / unsafe_raw_sql 関連
activerecord/test/cases/message_pack_test.rbactiverecord/test/cases/unsafe_raw_sql_test.rb
message_pack_test は Active Record オブジェクトの MessagePack シリアライズ/デシリアライズやバルクロードをテストしているため、結果の配列順がそのままバイナリの順序にも影響します。ここで暗黙順序に依存していた部分に order が入ったと考えられます。
unsafe_raw_sql_test では、生 SQL を使ったクエリや危険な SQL 呼び出しの検査を行っています。ここでも:
records = Developer.find_by_sql("SELECT * FROM developers WHERE ...")
assert_equal [developers(:david), developers(:jamis)], recordsのような記述に対し、SQL 自体に ORDER BY が足される、または find_by_sql の結果に対して Ruby レベルで sort_by(&:id) 相当を適用する、といった修正が行われている可能性があります。
6. test_case.rb の変更 (+13 行)
activerecord/test/cases/test_case.rb
テスト共通のベースクラスです。+13 行という比較的大きな追加から、次のようなヘルパーが追加された可能性があります:
- 「順序に意味のない」比較のためのヘルパー:
- 例:
assert_equal_unordered expected, actualのようなメソッド(sort_by(&:id)して比較など)
- 例:
- または、テスト中に Relation から
to_aする際に意図しない暗黙順序依存を検出・補助するユーティリティ。
Rails コアのテストでは、この種のヘルパーを通して配列比較を行うことで、将来の暗黙順序依存を減らす方針がとられることが多いです。
- 影響範囲・注意点
- 対象は「Rails コアの Active Record テストコード」のみであり、アプリケーションの本番コードの挙動は変わりません。
- ただし、テストが
SELECTの暗黙順序に依存していると不安定になる、という問題意識はそのままアプリ側にも当てはまります:where,joins,includesなどを使ったクエリの結果順を前提にロジックやテストを書いている場合、DB のバージョンや負荷状況によって結果順が変わる可能性があります。- 本番・CI でたまにテストが落ちる「たまたま動いているだけ」の状態が潜んでいるかもしれません。
- Rails コアがこのように修正したということは、将来的な Rails の内部実装変更(例えば、並列クエリ、最適化、DB のバージョンアップ)に備えて、「
ORDER BYなしの順序は信頼してはいけない」というメッセージとも受け取れます。
アプリ開発者の実務的な注意点:
- 「順序が大事なクエリ」…必ず
orderを明示する:- 例:
User.where(active: true)ではなくUser.where(active: true).order(:id)またはorder(:created_at)など。
- 例:
- テストで配列比較をする際:
- 順序そのものを仕様としてテストしたい → クエリ側で
orderを明示。 - 順序はどうでもよく、要素集合だけ検証したい →
sort_byしてから比較する /match_array(RSpec)等を使う。
- 順序そのものを仕様としてテストしたい → クエリ側で
- 参考情報 (あれば)
- 対象 PR:
pg_disorder拡張:- https://github.com/viralpraxis/pg_disorder
- PostgreSQL の SELECT 結果順を意図的に乱し、「暗黙の ORDER BY 依存」を見つけるための拡張。今回の PR はこれを reverse モードで使うことで、テスト上の問題箇所を検出しています。
- 一般的な原則:
- SQL 標準では、
ORDER BYを指定しない限り SELECT の結果順は未定義であり、DB 実装や実行計画、同時実行状況に依存して変化しうる、という前提に立ってクエリとテストを書く必要があります。
- SQL 標準では、
#58297 Deprecate positional pk, id_value, and sequence_name for #insert
マージ日: 2026/7/31 | 作成者: @kamipo
- 概要 (1-2文で)
ActiveRecord::ConnectionAdapters#insertの位置引数として使われていたpk,id_value,sequence_nameが非推奨になり、それぞれに対応する新しい使い方(特にreturning:オプション)への移行が明示されました。これにより、insertのインターフェイスが整理され、PostgreSQL のcurrvalベースのフォールバックなども段階的に廃止されます。
- 変更内容の詳細
2-1. 廃止される位置引数
対象は ActiveRecord の low-level な insert メソッド(アダプタ実装)です。従来は概ね次のようなシグネチャで呼ばれていました(アダプタによって差異あり):
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
...
endこのうち、以下3つの位置引数が「非推奨」として警告されます。
(1) pk
役割:
- RETURNING するカラムを指定し、そのカラムの値を
insertの戻り値として返すために使われていた。 - 例:
pk = "id"のとき、insertは挿入したレコードのidを返す。
- RETURNING するカラムを指定し、そのカラムの値を
新しい対応:
returning:オプションで同じことができます。returning:が単一カラム名を受け取った場合、戻り値も「単一値」になるように整備されました。- つまり、
pk = "id"は論理的にはreturning: "id"に1対1で置き換え可能です。
移行イメージ:
# 旧 (非推奨): 第3引数 pk で RETURNING カラムを指定
connection.insert(sql, "SQL", "id")
# 新: returning: オプションを使用
connection.insert(sql, "SQL", returning: "id")
# => 挿入された行の id を返す(戻り値の形は旧仕様と同じ単一値)(2) id_value
役割:
- DB が last inserted id を計算できない場合(RETURNING も currval も使えない等)に、呼び出し側が明示的に「この値が主キーだ」と教えるためのもの。
- その場合、
insertの戻り値としてid_valueがそのまま返ってきていた。
PRでの指摘:
id_valueを渡せるということは、呼び出し側はすでにその値を知っている。- であれば、
insertの戻り値を経由して読み直す必要は無く、そのまま自分で使えばよい。
移行方針:
# 旧 (非推奨): id_value を渡して、それを戻り値として受け取る想定
id = some_known_id
returned = connection.insert(sql, "SQL", nil, id)
# returned == id
# 新: insert の戻り値に依存しない
id = some_known_id
connection.insert(sql, "SQL") # 戻り値は無視する/使わない
# 以降は自前で持っている id を使う(3) sequence_name
- 役割:
- PostgreSQL で
use_insert_returning?がfalseの場合に、currval(sequence_name)経由で last insert id を取得するフォールバックのために使われていた。
- PostgreSQL で
- 状況:
use_insert_returning?のcurrvalフォールバック自体が非推奨になっている。- したがって、それを支える引数
sequence_nameも非推奨。
ポイント:
- PostgreSQL のアダプタでは、
@use_insert_returning = falseという、すでに非推奨の設定パスが存在します。 - このパスでは内部的に
sequence_nameをpkから解決し、super経由で再度sequence_name警告が出る可能性があります。 - PRではこれを「すでに非推奨な設定パスで発生する追加ノイズ」として容認しています。
2-2. 非推奨警告の出し方
pk,id_value,sequence_nameはそれぞれ独立にチェックされ、個別に警告が出るようになっています。- これにより、どの引数に対して、どんな移行パスを取るべきかが明示されます。
pk→returning:を使うid_value→insertの戻り値に頼らず、その場で持っている値を使うsequence_name→use_insert_returningに依存しないパスへ移動(一般には RETURNING ベース)
2-3. テストと CHANGELOG
activerecord/CHANGELOG.mdに上記非推奨が追記。- 以下のテストが修正・追加され、非推奨仕様に依存しないように変更:
adapter_prevent_writes_test.rbsqlite3_adapter_test.rbdatabase_statements_test.rb(非推奨関連の挙動をカバー)
- 影響範囲・注意点
3-1. 影響がある可能性が高いコード
- ActiveRecord の public API (
Model.createなど) を普通に使っているだけのアプリケーションは、ほぼ影響を受けません。 - 影響するのは次のような「アダプタ層や低レベルAPIを直接触っている」コードです。
例:
# 典型的な low-level な insert 呼び出し
connection.insert(sql, "CustomInsert", "id", 123, "my_seq")- こうした呼び出しは非推奨警告の対象になります。
3-2. 移行パスのまとめ
pkを使っていた場合:insert(sql, name, "id", ...)→insert(sql, name, returning: "id", ...)へ変更。
id_valueを使っていた場合:insertの戻り値としてid_valueを再取得する設計をやめ、元々持っている値を直接使う。- どうしても戻り値が欲しい場合は、
returning:で DB に生成させる/返させる形の設計に変更する。
sequence_nameを使っていた場合:- PostgreSQL で
currvalに依存する設計を捨て、RETURNINGベース(use_insert_returning?を true にする一般的な経路)に移行する。 - 必要に応じて
returning:を併用。
- PostgreSQL で
3-3. 非推奨警告のノイズについて
- 特に PostgreSQL +
@use_insert_returning = falseを明示的に使っている環境では、pk→ 内部でsequence_name解決 →sequence_name警告、と二重に近い形で警告が出ることがあります。
- PRの説明では、そもそもその設定自体が非推奨なので、この追加ノイズは許容範囲とされています。
- 将来的には、このパス自体が削除される可能性が高いと考えてよいです。
- 参考情報 (あれば)
- PR本体:
https://github.com/rails/rails/pull/58297 - 関連しうるドキュメント/コード:
ActiveRecord::ConnectionAdapters::DatabaseStatements#insert- 各アダプタ (
PostgreSQLAdapter,SQLite3Adapterなど) のinsert実装
- 傾向として:
- Rails は
insertの low-level API から「位置引数での特殊パラメータ指定」を減らし、オプションハッシュ(returning:など)ベースに寄せていく方向です。 - PostgreSQL の
currvalフォールバックも順次廃止され、INSERT ... RETURNINGを前提にした実装に統一されつつあります。
- Rails は
#58322 Remove migrations filter from the schema dump
マージ日: 2026/7/31 | 作成者: @fxn
- 概要 (1-2文で)
Rails の schema dump(db/schema.rbなど)に含める「適用済みマイグレーションのバージョン一覧」から、「ローカルにファイルが存在するものだけに絞り込む」というフィルタリング処理が削除されました。これにより、マイグレーションファイルが手元に無くても、DB 上で適用済みであればそのバージョンは schema dump に書き出されるようになります。
- 変更内容の詳細
これまでの挙動(フィルタあり)
- schema dumper は「
schema_migrationsテーブルに記録されているバージョン」のうち、- 実際に
db/migrate(または設定されたパス)にマイグレーションファイルが存在するバージョンだけを、schema.rbの末尾(trailer)に記録していました。
- 実際に
- その意図:
- ローカルに存在しないマイグレーションを dump に含めないことで「きれい」に保つ。
- 古いマイグレーションを「掃除」したい場合、ファイルを削除して
db/schema.rbを再生成すれば trailer からも消える、という運用を想定。
イメージ (以前の schema.rb の末尾例):
# ActiveRecord::Schema[7.2].define(version: 20260731000000) do
# ...
# end
# 以下のようなコメント行(例)
# Active Record::SchemaMigration.create!(version: "20240101000000")
# Active Record::SchemaMigration.create!(version: "20240202000000")
# ※ ただし、対応するマイグレーションファイルが存在しないバージョンはここに含めない今回の変更後の挙動(フィルタ削除)
schema_migrationsテーブルに登録されている すべてのバージョンが schema dump に出力されます。- もはや「ローカルにマイグレーションファイルが残っているかどうか」は考慮しません。
- 実装上は
schema_dumper.rbから、ファイル存在チェックに基づく絞り込みロジックが削除されています(約 10 行削除、3 行追加程度の軽微なコード変更)。
なぜフィルタをやめたのか(PR 本文の論点)
ブランチをまたぐと dump が「自己完結しなくなる」問題
- 例:
- ブランチ A でマイグレーション X を作成 → テーブル T を追加 → マイグレーション X は
schema_migrationsに記録される。 - ブランチ B に切り替えると、
db/migrateからマイグレーション X のファイルが無くなる。 - そこで
rake db:schema:dumpをすると:- テーブル T は schema に含まれる(DB には存在するため)。
- しかし、trailer にマイグレーション X のバージョンは 書かれない(ファイルが無いから)。
- ブランチ A でマイグレーション X を作成 → テーブル T を追加 → マイグレーション X は
- 結果として、「この schema を適用した状態の DB には T があるが、その状態に至るマイグレーション履歴情報が dump に反映されていない」という一貫性のない状態になる。
- PR 作者はこれを「
db/schema.rbが自己完結していない」と見なしています。
- 例:
:sqlフォーマットとの挙動差schema_format = :sql(structure.sqlなど)の場合は、DB 側の状態がそのまま出力され、ローカルのマイグレーションファイル有無は関係ありません。:rubyの schema dump(schema.rb)だけが「ローカルファイルの有無でバージョンを削る」というのは一貫性が無い、という判断です。
この 2 点から、「フィルタリングはやめて、DB 上の schema_migrations をそのまま出すほうが妥当」という結論になっています。
テスト・ドキュメントの更新
activerecord/test/cases/schema_dumper_test.rb- ローカルのマイグレーションファイル有無に依存していたテストを修正し、今後は「
schema_migrationsにあるものは dump される」前提に変更。
- ローカルのマイグレーションファイル有無に依存していたテストを修正し、今後は「
guides/source/active_record_migrations.md- マイグレーションと schema dump の関係に関する説明を、この新しい挙動に沿うように微修正。
activerecord/CHANGELOG.md- 上記仕様変更が記録されています(7.2 or main の該当セクションに追記)。
- 影響範囲・注意点
影響する人
schema_format = :ruby(デフォルト)でdb/schema.rbを利用しているプロジェクト全般。- 特に:
- 古いマイグレーションファイルを削除して「schema の trailer からも消えてほしい」と期待していた運用。
- ブランチを頻繁に切り替えつつ
db/schema.rbを commit しているチーム。
挙動の変化による具体的影響
古いマイグレーションファイルを削除しても trailer からは消えない
- 以前:
db/migrate/20200101000000_old_migration.rbを削除bin/rails db:schema:dumpdb/schema.rbの末尾から"20200101000000"が消える
- 今後:
- DB の
schema_migrationsに"20200101000000"が残っている限り trailer にも残る。
- DB の
- 「バージョン一覧を物理ファイルの存在と同期させる」という使い方はできなくなります。
- バージョンを本当に消したい場合は、
schema_migrationsテーブルからバージョンを消さない限り dump からは消えない(※通常は直接いじるべきではないので、実務上は「残しっぱなし」にするのが安全)。
- 以前:
ブランチ間での不整合は緩和される
- 以前は、あるブランチで適用されたマイグレーションが別ブランチでは「trailer に現れない」という状態になり得ました。
- 今後は、「DB に適用されている限りどのブランチから dump しても同じバージョンが trailer に出る」ことになり、schema ファイルの自己完結性が上がります。
:sqlフォーマットとの整合性が向上:sqlと:rubyで大きくポリシーがズレることがなくなり、ツール連携や挙動理解がしやすくなります。
注意 / 実務上のポイント
db/schema.rbの差分が少し増える可能性:- これまで「ローカルに無いマイグレーションは trailer から消えていた」プロジェクトでは、今後それらが再び出力されるため、PR 差分として「マイグレーションバージョン行が増えた」ように見えることがあります。
- CI で
db/schema.rbの内容チェックをしている場合:- テストセットアップ時にどの DB を元にしているか(
schema:load/migrate/ スナップショットなど)によってschema_migrationsに残るバージョンが違うと、dump 結果の差異として現れ得るので注意が必要です。
- テストセットアップ時にどの DB を元にしているか(
- 「古いマイグレーションの整理」のベストプラクティス:
- これまで「ファイルを消すと schema のバージョンからも消える」という前提で運用していた場合は、方針を見直す必要があります。
- そもそも Rails の公式スタンスとしても、「過去マイグレーションはむやみに削除しない」が推奨寄りです(新規参加者が
db:setupできなくなるなどの問題もあるため)。
- 参考情報 (あれば)
- 対象 PR:
- rails/rails #58322: Remove migrations filter from the schema dump
- 関連する設定:
config.active_record.schema_format = :rubyor:sql
- ドキュメント:
guides/source/active_record_migrations.md(今回の PR で内容が更新)
- 実装箇所:
activerecord/lib/active_record/schema_dumper.rb
→schema_migrationsの扱いに関するフィルタロジックが削除されている部分が中心。
#57948 Make the Action View dependency tracker registry Ractor-shareable
マージ日: 2026/7/31 | 作成者: @etiennebarrie
- 概要 (1-2文で)
ActionView のテンプレート依存関係トラッカー (ActionView::DependencyTracker) のレジストリを Ractor で共有可能(ractor-shareable)にするための変更です。Rails.application.ractorize!を呼んだアプリケーションに限り、依存関係トラッカーのレジストリを Ractor 間で安全に共有できるようになります。
- 変更内容の詳細
2-1. 依存関係トラッカーのレジストリを Ractor-shareable に
対象クラス: ActionView::DependencyTracker
もともと DependencyTracker は、テンプレートハンドラ(ERB, HAML など)ごとに「どのトラッカーを使うか」を登録するレジストリを持っています。このレジストリを Ractor で共有できるよう、以下の対応が行われています。
share_registry メソッドの導入
PR 説明中では一時的に freeze_registry と書かれていましたが、最終的には share_registry というメソッドが導入されています(名前が示す通り「Ractor.shareable! できる状態にする」ことが目的)。
イメージとしては以下のような処理が行われるメソッドです(概念的なサンプル):
module ActionView
class DependencyTracker
class << self
def share_registry
# 例: ハッシュや配列等のレジストリを凍結したり、
# Ractor.shareable!(registry) 可能な状態に変換したりする
@dependency_trackers.freeze
@cache_keys.freeze
# 必要に応じて個々の要素も freeze / shareable 化
end
end
end
end実際の実装では、内部に保持しているレジストリ構造を Ractor-shareable な状態(イミュータブル or Ractor.shareable! 済み)に整える処理が入っています。
2-2. Rails.application.ractorize! からレジストリを共有可能にする
railties/lib/rails/application.rb に 1 行だけ変更が入っています。
ここで Rails.application.ractorize! が ActionView 側の share_registry を呼び出すようになっています。
ざっくり言うと:
class Rails::Application
def ractorize!
# ほかのコンポーネントの Ractor 対応処理…
ActionView::DependencyTracker.share_registry
end
endという流れです。
つまり:
- デフォルト: レジストリは従来どおり可変(Mutable)のまま。Ractor-shareable にされない。
Rails.application.ractorize!呼び出し時: ActionView の依存関係トラッカーレジストリが Ractor-shareable にされる。
既存アプリへの互換性確保のため、「Rails を Ractor 対応モードにする」と明示したアプリだけがこの制約を受けるように設計されています。
2-3. 設定完了後の DependencyTracker 変更を非推奨に
PR 説明より:
I also deprecated mutating the dependency tracker after configuration.
これに伴い、アプリケーションの設定が完了した後に DependencyTracker を変更することが非推奨 (deprecated) になりました。
具体的には、
- レジストリに新しいトラッカーを追加する
- 既存のトラッカー登録を変更する
といった操作を、Rails の設定フェーズ完了後に行うと非推奨警告が出るような挙動になっています。
ただし:
it's still possible to add an unshareable tracker for an unshareable handler, for compatibility reasons.
とあるように、互換性のために以下は依然として許容されています:
- Ractor-shareable でないハンドラ用に、Ractor-shareable でないトラッカーを後から追加する
これは「Ractor を使わない従来のアプリ」が壊れないようにするための妥協点です。
一方で、ractorize! を使って Ractor 化を有効にしたアプリでは、そのレジストリが共有対象になるため、後からミューテーション(変更)することは基本的に NG と考えたほうが安全です。
2-4. CHANGELOG とテストの追加
actionview/CHANGELOG.mdに、この変更(Ractor-shareable な DependencyTracker レジストリ & 変更の deprecate)が追記されています。actionview/test/template/dependency_tracker_test.rbに、Ractor-shareable 化・deprecation 挙動・互換性周りをカバーするテストが大量に追加されています(+117 行)。- レジストリを shareable にしたとき、Ractor.shareable? が真になるか
- 設定完了後にミューテーションを行ったとき、deprecation が出るか
- 非 shareable なトラッカーを非 shareable なハンドラに追加するケース等の互換性確認
- 影響範囲・注意点
3-1. ractorize! を使うアプリへの影響
Rails.application.ractorize! を呼ぶアプリケーションでのみ、ActionView::DependencyTracker のレジストリが Ractor-shareable になります。
その場合:
- レジストリは 基本的にイミュータブル とみなされるべきです。
- Ractor 化後に、
ActionView::DependencyTracker.register_tracker等で mutate すると、- Ractor 的に危険(共有オブジェクトの書き換え)
- deprecation 警告
の両面で問題になり得ます。
3-2. 設定フェーズ後のトラッカー登録は非推奨
次のようなコードを書いている場合は要注意です:
# 例: 初期化完了後のどこか(遅延ロード時など)での追加
ActionView::DependencyTracker.register_tracker(:erb, MyCustomTracker)今後は、次のような方針が推奨されます:
- アプリケーション設定フェーズ(config/initializers 内など)で登録を完了させる
- 特に
ractorize!を使う予定があるアプリでは、「起動後に動的にトラッカー追加・上書き」しないよう設計を見直す
3-3. Ractor 非利用アプリの互換性
Rails.application.ractorize!を呼ばない限り、レジストリは従来通り可変です。- 非 shareable なトラッカーを、非 shareable なハンドラに後から足すというパターンも動き続けます。
- ただし、「設定完了後の変更」は deprecation(将来的な削除・挙動変更の予告) である点に留意してください。
将来のメジャーバージョンで、完全に禁止される可能性があります。
- 参考情報 (あれば)
- この PR:
- 関連 PR (
Rails.application.ractorize!の導入): - Ractor(Ruby 並行実行モデル)の概要:
Ractorはオブジェクト共有に厳しい制限があり、Ractor.shareable!(obj)で共有可能状態にしたオブジェクトや、完全にイミュータブルなオブジェクトのみを複数 Ractor 間で共有できる。そのため、この PR のようにフレームワーク内部のレジストリを「共有可能状態」に整える必要がある。
#58312 Show parameter marker for casted binds in SQL logs and EXPLAIN
マージ日: 2026/7/31 | 作成者: @kamipo
- 概要 (1-2文で)
[[column, value], ...]形式から移行中の「casted binds」(単なる値配列)について、SQLログおよびEXPLAIN出力で[[nil, value], ...]と表示されていた問題を、位置パラメータ($1,$2, ...)を表示するようにして可読性を改善したPRです。PostgreSQL風のパラメータマーカーを導入し、どの値がクエリ中のどのプレースホルダに対応するか一目で分かるようにしています。
- 変更内容の詳細
背景: casted binds とログ表示の問題
既存: レガシーな binds 形式
ruby# 旧形式 (レガシー) # [[column, value], [column, value], ...]ここでは
columnオブジェクトがあるため、ログや EXPLAIN 時に[[column_name, value], ...]のように、人間が読める形で表示できていました。新形式: casted binds
ruby# 新形式 (casted binds) # [value1, value2, ...] # 単なる値の配列この形式は #39106 で導入されたもので、将来的にこちらへ移行する前提ですが、
column情報がないため、これまでログでは次のように出ていました:sqlSELECT * FROM topics WHERE title = ? [[nil, "abcd"]]nilが先頭に出ており、「どのパラメータに対応しているか」が分かりにくい状態でした。
このPRでの主な変更点
StructuredEventSubscriber#sql のログ表示
casted binds(カラム情報なし)の場合、
[[nil, value], ...]ではなく
パラメータ位置をキーにして表示するように変更されています。例:
sql# 変更前 (イメージ): SELECT * FROM topics WHERE title = ? [[nil, "abcd"]] # 変更後: SELECT * FROM topics WHERE title = $1 [["$1", "abcd"]]ポイント:
$1,$2, ... のような PostgreSQL 風のパラメータマーカー を採用。- 実際の SQL が
?プレースホルダを使うアダプタ(例: MySQL、SQLite)であっても、 ログ上では$1表記となり、「ログを見る人間」がパラメータ位置を即座に把握できるようにする狙い。
Explain#exec_explain の出力
EXPLAINを実行する際に付随して出るバインド値についても、同様に$1,$2形式で表示されるように変更。- casted binds でも、EXPLAIN 出力のバインドリストが self-describing になります。
実装上の要点 (推測される変更内容)
実際のコードは以下のようなイメージで実装されています(擬似コード):ruby# もともと: [[nil, value], ...] として扱われていたケースに対し、 # インデックスを使って "$1", "$2" ... を生成 casted_binds.each_with_index.map do |value, index| name = "$#{index + 1}" [name, value] end- これを StructuredEventSubscriber および Explain の双方で利用し、 casted binds に対する人間可読な表示を統一しています。
テストの追加・更新
activerecord/test/cases/log_subscriber_test.rb- SQL ログの出力に
$1スタイルのマーカーが含まれていることを確認するテストが追加。
- SQL ログの出力に
activerecord/test/cases/explain_test.rb- EXPLAIN 出力でも
$1マーカーが表示されることを確認するテストが追加。
- EXPLAIN 出力でも
bind_parameter_test.rbも軽微に更新され、bind 表示の期待値が新形式に合わせられています。
- 影響範囲・注意点
影響を受ける箇所
- Rails の ActiveRecord ログ (
StructuredEventSubscriber#sqlが吐く SQL ログ)。 Model.explain,relation.explainなどで表示される EXPLAIN 出力のバインドリスト。
- Rails の ActiveRecord ログ (
既存コードへの互換性
- 実際の SQL 発行方法・パラメータバインディングの挙動自体は変更されていません。
- 変更されるのは「ログおよび EXPLAIN における表記のみ」です。
- ただし、ログ出力フォーマットの文字列が変わるため、以下のようなケースでは影響があります:
- ログを正規表現などで機械的にパースしているツールやスクリプト
- 監査・モニタリング用にログフォーマットへ依存しているミドルウェア
- これらのツールは、
[[nil, "..."]]を前提にしていた場合、[["$1", "..."]]形式に対応するよう修正が必要になります。
デバッグ・調査のしやすさの向上
- SQL 中の
?とログのバインド値リストの対応関係が明示されるため、 特に複数の?が並ぶクエリや、長いクエリの調査がかなり楽になります。 - 使用DBアダプタに依存しない表記(常に
$n)なので、PostgreSQL 以外でも 「n番目のパラメータ」という意味で一貫性が保たれます。
- SQL 中の
- 参考情報 (あれば)
- 元のPR:
- Show parameter marker for casted binds in SQL logs and EXPLAIN (#58312)
- 関連PR:
- Casted binds 導入の PR: #39106
→[[column, value], ...]形式から値だけの配列への移行文脈。
- Casted binds 導入の PR: #39106
- 関連クラス / メソッド:
ActiveRecord::Explain#exec_explainActiveRecord::StructuredEventSubscriber#sql- テスト:
activerecord/test/cases/explain_test.rbactiverecord/test/cases/log_subscriber_test.rbactiverecord/test/cases/bind_parameter_test.rb