Skip to content

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. 概要 (1-2文で)
    このPRは、Active Record が生成する ORDER BY なしの SELECT 結果をランダムにシャッフルできる設定オプション config.active_record.shuffle_unordered_selects を追加し、テスト環境などで「暗黙の順序依存」をあぶり出せるようにする変更です。デフォルトは無効で、本番挙動は変えずに、順序に依存した危ういコードやテストを早期に検出することが目的です。

  1. 変更内容の詳細

2-1. 新オプション: config.active_record.shuffle_unordered_selects ​

Rails の設定として以下のオプションが追加されています:

ruby
# 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 SELECT without ORDER BY has 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 の説明が追記
    • 主に以下の点が説明される想定です:
      • なぜ順序未定義の結果に依存するのが危険か
      • テスト環境でこのオプションを有効にすることで、そうした依存を検出しやすくなること
      • 本番では通常オフのままにしておくべきこと

  1. 影響範囲・注意点

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 を使うなど) といった修正を行うべきです。

3-3. シャッフルされないケース ​

  • 明示的に order が指定されているクエリ
  • DB アダプタや内部ロジック上、「明示的に順序が決まっている」とマークされるクエリ
  • また、今回の実装は「Active Record が生成する多くのクエリ」を対象としており、すべての生 SQL / 生接続クエリが網羅されているとは限りません。
    connection.execute("SELECT ...") のようなコードは、この機能の外側にいるケースがあります。

3-4. 本番環境での利用について ​

  • PR 説明・ガイドの位置づけからも、テスト環境向け機能として設計されています。
  • 本番で有効にすると:
    • 順序未定義のクエリが「毎回違う順序で返ってくる」ことになり、実装によっては大きく動作が変わる可能性がある
    • パフォーマンス的には「結果配列のシャッフル」程度なので大きな負荷ではないが、意味的な影響が大きすぎる
      よって、基本的には test / development での検出用途に限るのが妥当 です。

  1. 参考情報 (あれば)

#58604 Improve performance of Active Record model class predicate builders ​

マージ日: 2026/8/29 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    Active Record のモデルクラスにおける「predicate builder」(where などで使われるクエリ条件生成オブジェクト)の初期化方法を見直し、遅延初期化時のプロキシ処理をやめて「最初から実体をセットする」形に変えることでパフォーマンスを改善した PR です。初回リクエスト/クエリ時に発生していた余分なオーバーヘッドを減らすことが目的です。

  1. 変更内容の詳細

何をしているか ​

  • Active Record モデルクラスが内部的に持っている predicate builder 用のインスタンス変数(例えば @predicate_builder 的なもの)について、
    • 以前: 「必要になった時にだけ実際の builder を生成する」ための遅延初期化 + プロキシ的なアクセス
    • 今回: モデルクラス側で あらかじめ(eager に)実体を代入しておく
  • これにより、最初にクエリを投げたタイミングで発生する「proxy を介した初期化コスト」が不要になります。

具体的な変更ポイント(概念レベル) ​

※実際のコードは数行レベルの小さな差分ですが、意味合いとしては以下のような変更です。

  • ActiveRecord::Base(core.rb)か、あるいはそれを拡張する model_schema.rb あたりで、
    • 以前は predicate_builder 参照時に「まだ作ってなければ作る」ようなロジックが入っていた
    • 今回は「モデルクラスがセットアップされる段階で builder を生成してインスタンス変数に格納する」形に寄せる

擬似コードで表現すると、イメージはこんな感じです:

ruby
# 変更前(イメージ)
def predicate_builder
  @predicate_builder ||= Relation::PredicateBuilder.new(table)
end

# 変更後(イメージ)
# モデル定義のセットアップ時に
@predicate_builder = Relation::PredicateBuilder.new(table)

def predicate_builder
  @predicate_builder
end
  • model_schema.rb では、スキーマロード/テーブル情報確定タイミングで predicate builder をセットするような処理が追加されていると考えられます。
  • core.rb の 1 行の変更は、predicate_builder のデフォルト実装や初期化の責務の持ち方を微調整している可能性が高いです。

テスト ​

  • activerecord/test/cases/relation/predicate_builder_test.rb に 14 行追加されており、
    • 新しい eager 初期化の挙動が正しく行われるか
    • 遅延初期化由来のプロキシがなくなっても従来通りクエリが組み立てられるか
  • といった点を確認するテストが追加されています。

  1. 影響範囲・注意点

影響範囲 ​

  • 対象: Active Record モデルクラスで行うクエリビルド(where, not, find_by, exists? など predicate builder を内部で利用する操作全般)。
  • ユーザーコードから見た「API の振る舞い」自体には変更がなく、あくまで内部の初期化タイミングと実装方式の変更です。
  • そのため、通常の Rails アプリであれば、この変更により
    • 最初のリクエスト/コンソール起動直後の最初のクエリがわずかに高速化
    • 長期運用でのレイテンシのばらつき(「1 回目だけ遅い」現象)の軽減 が期待されます。

注意点・考慮すべき点 ​

  • predicate builder 自体をモンキーパッチしていたり、ActiveRecord::Relation::PredicateBuilder 周辺の初期化/メモ化ロジックに依存したメタプログラミングをしている場合:
    • builder が「いつ」「どのタイミング」でインスタンス変数にセットされるかが変わったことで、
      ごく一部の高度なハックが影響を受ける可能性はあります。
  • ただし、変更は内部実装レベル・かつ小さな差分であり、通常のアプリケーション開発では互換性上の問題はほぼ発生しないと考えられます。
  • CHANGELOG は更新されていないため、公式には「挙動変更として告知すべきほどのユーザー向け仕様変更ではなく、内部的なパフォーマンス改善」と位置付けられています。

  1. 参考情報 (あれば)
  • 対象 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. 概要 (1-2文で)
    Active Storage が依存する MIME タイプ判定ライブラリ「marcel」のバージョンを ~> 1.0 から ~> 2.0 に引き上げ、より精度の高いコンテンツタイプ検出とセキュリティ強化を Rails 本体のデフォルト挙動にしました。
    これにより、新規に解析・アップロードされる Blob の MIME タイプが一部「正準(canonical)なタイプ」に変わりますが、既存の Blob には影響しないよう配慮されています。

  1. 変更内容の詳細

依存関係の更新 ​

  • activestorage.gemspec

    • marcel の依存バージョンを
      • 変更前: ~> 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/aac
  • audio/x-flac → audio/flac
  • audio/vnd.wave → audio/x-wav
  • text/x-yaml → application/yaml
  • image/bmp;format=compressed → image/bmp

アプリ側で blob.content_type を前提に分岐している場合、この差分が顕在化します。

例: コントローラやサービスでの分岐コード

ruby
case blob.content_type
when "audio/x-aac" # ← 旧 marcel 1.x
  # ...
end

marcel 2.x 以降では、新規 Blob に対しては audio/aac が返ってくるようになるため、以下のような対応が必要になる可能性があります。

ruby
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 を指し続けてしまう」といった状況を避け、アップグレード時に確実にセキュリティ強化を取り込む設計になっています。

  1. 影響範囲・注意点

影響範囲 ​

  • 影響を受けるもの:

    • Active Storage を利用しており、コンテンツタイプ (content_type) でロジック分岐 している部分
    • 特定の MIME タイプ文字列をテストで厳密に == 比較している箇所
    • MIME タイプをキーにした DB 検索・集計など (例: where(content_type: "text/x-yaml"))
  • 影響を受けないもの:

    • すでに保存済みの Blob の content_type 値
      → 既存レコードはそのまま。検索でも旧 alias で引き続き解決される挙動が確保されています。
    • Rails 本体のテストスイート
      → Active Storage の全テスト (611 runs, 1876 assertions) で content_type 関連の期待値変更は不要だった、と明記されています。

アプリ開発者が確認すべきポイント ​

  1. MIME タイプでの条件分岐の見直し

    • 例: "text/x-yaml" や "audio/x-aac" などの alias を前提とした条件があれば、canonical タイプでも動くようにする。
    • 対応方針:
      • 短期: alias + canonical 両方を許容する (OR 条件・配列含有など)
      • 中長期: canonical 側に寄せたマイグレーションを検討
  2. テストコードの期待値

    • ファイルアップロードをテストして blob.content_type を assert_equal している箇所は、期待値を canonical に合わせるか、include? / 正規表現などで緩く判定する。
  3. Ruby バージョン

    • Rails main を追いかける以上、Ruby 3.3 系が前提となる点を再確認する。
    • Ruby 3.3 未満の環境では bundler 解決に失敗するため、CI / 本番環境を 3.3 以上に揃える必要がある。
  4. セキュリティ・アップロード周りのポリシー

    • MIME タイプ判定に reliance しているバリデーション (例: content_type: ["image/png", ...]) は、より正確な判定に変わることで拒否/許可の結果が変化する可能性があります。
    • 特に拡張子偽装の防止や、特定形式のブロックを行っている場合は、挙動が期待どおりか確認しておくと安全です。

  1. 参考情報 (あれば)
  • 本 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 が実行中であり、その結果を踏まえてマージされたと記載。

#58601 Fix compatibility with json 3.0 ​

マージ日: 2026/8/28 | 作成者: @Earlopain

  1. 概要 (1-2文で)
    Rails の ActiveSupport::JSON.decode と Active Record の JSON コーダーが、json gem 3.0 で導入された「オプションはキーワード引数として扱う」という仕様変更により落ちる問題を、両バージョン(2.x / 3.x)で動く形に修正した PR です。

  1. 変更内容の詳細

問題の背景 ​

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 に対して以下のような呼び出しをするとエラーになります:

ruby
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.rb
  • activerecord/test/cases/coders/json_test.rb
  • activerecord/test/cases/serialized_attribute_test.rb
  • activesupport/lib/active_support/json/decoding.rb

ActiveSupport::JSON.decode の修正 ​

activesupport/lib/active_support/json/decoding.rb 内で JSON.parse を呼ぶ際の引数の渡し方を調整しています。

元々(イメージ):

ruby
def decode(json, options = {})
  JSON.parse(json, options)  # 位置引数2つ
end

修正後(イメージ):

ruby
def decode(json, options = {})
  # options をキーワード引数として渡す形式に変更
  JSON.parse(json, **options)
end

もしくは、options が nil の場合に引数を 1 個にするなど、「json 2.x でも 3.0 でも動くように」引数の形を分岐する形になっています。

ここでのポイントは:

  • json 3.0 ではオプションが「キーワード引数」とみなされるようになったため、位置引数としてハッシュを渡さない。
  • かつ json 2.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.rb
  • activerecord/test/cases/serialized_attribute_test.rb

の2つで、1行ずつの微修正が入っています。

ここでは:

  • JSON デコードの呼び出し方、もしくは期待される挙動を、json 3.0 対応後の API に合わせて修正。
  • 特に「オプションをハッシュで渡す」挙動をチェックしているテストがあるため、json 3.0 でも落ちない形に揃えている。

PR 本文にもある通り、「ハッシュを渡せることをテストしているものがありつつ、encode 相当のメソッドとの対称性もある」ため、完全に def decode(...) に変えるのではなく、既存インターフェースを壊さず裏側だけを調整する方針をとっています。


  1. 影響範囲・注意点
  • 影響範囲:
    • ActiveSupport::JSON.decode を経由して JSON をパースするすべての場所。
    • Active Record の serialize などで JSON コーダーを使っている箇所(ActiveRecord::Coders::JSON)。
  • 想定される影響:
    • json gem 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 内部の互換性レイヤーに依存せず、直接 json gem を叩いているライブラリ・プラグインも同様にチェックが必要です。

  1. 参考情報 (あれば)
  • json gem 3.0rc1 の変更ログ
    https://github.com/ruby/json/blob/master/CHANGES.md#2026-08-11-300rc1
  • (PR 本文より) json 3.0 での仕様:
    • 「すべてのメソッドの options はキーワード引数として扱われ、未知のオプション(タイポなど)は ArgumentError になる」
    • これにより、ハッシュを位置引数として渡す従来の呼び方が一部互換性問題を起こす。

#58602 remove DS_Store ​

マージ日: 2026/8/28 | 作成者: @tenderlove

  1. 概要 (1-2文で)
    このPRは、activerecord ディレクトリ配下に誤ってコミットされていた macOS のメタデータファイル .DS_Store をリポジトリから削除するものです。機能追加やバグ修正は一切なく、純粋な不要ファイルの掃除です。

  2. 変更内容の詳細

  • 変更ファイル: activerecord/.DS_Store
  • 内容: .DS_Store ファイルの削除(実質的にバイナリ/メタデータファイルの除去であり、コード差分はありません)

.DS_Store は macOS Finder が各ディレクトリごとに自動生成する、表示設定などを保持する隠しファイルです。
Rails のコードベースやテストに関する変更は一切行われていません。

  1. 影響範囲・注意点
  • ランタイム動作への影響: なし
    • アプリケーションの挙動、Active Record の機能、パフォーマンスには関係ありません。
  • ビルド/テストへの影響: なし
    • 単に不要な OS 依存ファイルがリポジトリから取り除かれただけです。
  • 開発プロセス上の意味合い:
    • 他の開発者環境で .DS_Store による差分が出にくくなり、クリーンな Git 履歴を維持できます。
    • もし .gitignore に .DS_Store が未登録なら、今後の混入防止のために .gitignore で無視することが推奨されます(このPR自体では .gitignore は変更されていません)。
  1. 参考情報 (あれば)
  • .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. 概要 (1-2文で)
    このPRは、Rails が新規アプリ生成時に出力する GitHub Actions 設定 (ci.yml) に、各ジョブ共通の timeout-minutes: 15 を設定する変更です。これにより、ネットワーク障害などで処理がハングした場合でも、デフォルトの360分待たずに15分でタイムアウトし、無駄なランナー時間の消費を防ぎます。

  1. 変更内容の詳細

変更対象 ​

  • 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本文から推測される典型的なイメージ)

yaml
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分を設けるという方針です。


  1. 影響範囲・注意点

影響範囲 ​

  • 対象: このPRが入ったバージョン以降で rails new myapp --github などにより GitHub Actions 用 CI ファイルを生成した新規アプリ。
  • 既存アプリの .github/workflows/ci.yml には自動では反映されませんが、同様の設定を手動で取り込むことはできます。

実務的な注意点 ​

  1. 長時間かかるテストスイートを持つアプリ

    • テスト全体が 15分を超える可能性がある場合、そのジョブは 常にタイムアウトで失敗するようになります。

    • その場合は、生成された ci.yml 内の該当ジョブについて、例えば次のように値を引き上げる必要があります:

      yaml
      jobs:
        test:
          runs-on: ubuntu-latest
          timeout-minutes: 30
          steps:
            ...
  2. まれにハングするケースを見つけるトリガーになる

    • 15分で打ち切られた場合、原因が
      • テストが純粋に長すぎるのか
      • ネットワーク障害や外部サービス待ちで固まっているのか を切り分ける必要があります。
    • 「最近ハングしがちでタイムアウトする」という場合は、ジョブログを確認してどのステップで止まっているかを特定し、
      • リトライ (e.g. retry アクション)
      • 外部サービスへの依存削減
      • キャッシュ利用
        などを検討する余地があります。
  3. 組織ランナー・GitHub Actions の課金への影響

    • ハングしたジョブが 360分走り続けることを避けられるため、ランナー時間の節約に直結します。
    • 特に self-hosted runner や GitHub-hosted runner を大量に使う組織には有益です。

  1. 参考情報 (あれば)

#58599 Prefixed partial names ractor safe ​

マージ日: 2026/8/28 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    Action View の「オブジェクトレンダリング」と ActiveSupport のイベントレポーター周りで、Ractor 非対応だったグローバル状態を「Ractorごとの状態管理」に変更し、Ractor セーフにした PR です。マルチ Ractor 実行時にも部分テンプレートのプレフィックスキャッシュとイベント通知が安全に動くようになります。

  1. 変更内容の詳細

2-1. ActionView のプレフィックス付きパーシャル名キャッシュを Ractor セーフに ​

変更点概要 ​

従来は、ActionView::AbstractRenderer::ObjectRendering 内で、クラスレベルの定数的なキャッシュを使っていました:

ruby
# 旧: 実際のコード名は説明用
PREFIXED_PARTIAL_NAMES = {}

このような「プロセス全体で共有されるミュータブルな Hash」は、Ractor 間で共有できないため Ractor セーフではありません。

PR では、これを「Ractorごとのキャッシュ」に切り替えるために、prefixed_partial_names メソッドを新設し、そこからキャッシュにアクセスするようにしています。

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

ruby
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 は、イベントの発火/購読まわりで内部的に使われるオブジェクトです。従来は単純にクラスインスタンス変数を使っていました:

ruby
# 旧イメージ
module ActiveSupport
  class << self
    def event_reporter
      @event_reporter ||= build_reporter
    end
  end
end

このような ivar ベースのシングルトンも、Ractor 間でそのまま共有するのは安全ではありません。

PR では、以下の方針に変更されています:

  • メイン Ractor では従来通りインスタンス変数を使用
  • それ以外の Ractor では、Ractor ローカルな event_reporter を保持

疑似コードのイメージ:

ruby
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 セーフに動くこと

  1. 影響範囲・注意点
  • Ractor を使わないアプリケーション
    従来と同じ挙動となるよう配慮されているため、基本的に振る舞いの変化はありません。パフォーマンス面でも、以前の「プロセス全体でのキャッシュ」が「Ractorごとのキャッシュ」に分割されるだけなので、単一 Ractor(=通常の MRI 実行)ではほぼ同等です。

  • Ractor を使う/今後使う可能性があるアプリケーション

    • Action View のオブジェクトレンダリング(例: render @user や render @collection)を Ractor 内で行っても安全に動作し、キャッシュ汚染や例外が起こりづらくなります。
    • ActiveSupport のイベント周り(ActiveSupport::Notifications など、内部で event_reporter を利用するもの)が Ractor ごとに独立したレポーターを持つため、Ractor 間でイベントが想定外に混ざることを防げます。
    • 一方で、「Ractor 間でイベントレポーターを共有して、全ての Ractor のイベントを一元的に集計したい」といった設計は、この変更と整合しません。その場合は、各 Ractor から別プロセスやスレッドセーフな別チャンネル(Queue、ログ、DB など)にイベントを送る設計にする必要があります。
  • カスタムパッチ/Monkey Patch をしている場合の注意点

    • ActionView::AbstractRenderer::ObjectRendering::PREFIXED_PARTIAL_NAMES を直接参照・上書きしているような Monkey Patch がある場合は動かなくなります。代わりに、prefixed_partial_names メソッドを経由するように修正する必要があります。
    • ActiveSupport.event_reporter をグローバルなシングルトン前提で利用していた独自コードがある場合、Ractor 内では「Ractorごとのインスタンス」になる点に注意してください。

  1. 参考情報 (あれば)
  • Ruby の Ractor 仕様・制約

    • Ractor 間でミュータブルオブジェクトは共有できず、共有するには shareable_constant_value などでイミュータブル化するか、メッセージパッシングが必要。
    • Rails 内部の多数のシングルトン/グローバルキャッシュは、Ractor 対応時に「Ractor ローカル」や「スレッドローカル」へと段階的に移行中。
  • 本 PR は CHANGELOG 未更新のため、「外向きの新機能」としてよりは、内部の Ractor 対応改善の一環という位置付けです。Ractor を積極的に使いたい場合には、この PR を含むバージョン以降を前提に検証する価値があります。


#58593 Remove no-op Marshalling.format_version handling from tests ​

マージ日: 2026/8/27 | 作成者: @kamipo

  1. 概要 (1-2文で)
    Rails 7.1 以降で ActiveRecord::Marshalling.format_version が常に 7.1 固定になった仕様を前提に、もはや意味を持たないテスト上の format_version の保存・復元処理と、それに依存していた重複テストを削除した PR です。結果として、マージ後はテストコードがシンプルになりつつ、実質的なテストカバレッジは変わりません。

  1. 変更内容の詳細

背景: 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 にあった、次のようなテストコードが削られています(実際の差分は概念的にはこのイメージ):

ruby
# 以前(イメージ)
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 版が削除。
  • 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 行削除

と、主に不要コードの削除が中心です。


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

    • 本 PR はテストコードのみの変更であり、本体のフレームワーク挙動には影響しません。
    • ActiveRecord::Marshalling の振る舞い自体は、すでに #57382 時点の仕様(format_version 固定)を前提としており、この PR はそれにテストを追従させるものです。
  • 注意点 / 開発者視点での示唆:

    • 外部アプリケーションやライブラリ側で、「ActiveRecord::Marshalling.format_version をテストや実行時に切り替えて古いフォーマットとの互換性を担保する」という設計をしている場合、それは既に Rails 本体の現行仕様と整合しません。
      • "7.1" 以外への変更は例外になるため、バージョン切り替えによる動作確認は不可能です。
      • 古いバージョンとの互換性確認は、Rails のバージョン(およびその時点の ActiveRecord)自体を切り替えて行う必要があります。
    • テスト内で「以前のフォーマットバージョンを明示して検証する」という考え方は現行のマシュアル仕様とは合わないため、新規に似たコードを書くべきではありません。

  1. 参考情報 (あれば)
  • 対応する過去の変更:
    • PR #57382: ActiveRecord::Marshalling.format_version 周りの仕様変更(7.1 固定、および setter の制限)
  • 該当箇所のテストファイル:
    • activerecord/test/cases/base_test.rb
    • activerecord/test/cases/marshal_serialization_test.rb

この PR は、上記仕様変更を前提に「もはやテストとして意味をなさないコード」を整理したものと理解できます。


#58592 More small hash optimizations ​

マージ日: 2026/8/27 | 作成者: @byroot

  1. 概要 (1-2文で)
    Rails のルーティング処理(ActionDispatch::Routing::RouteSet)において、URL オプション用ハッシュに対する不要な処理を減らすことで、パフォーマンスをわずかに改善するための最適化 PR です。機能仕様は変えずに、内部でのハッシュ操作(コピーやマージなど)を避ける形でコードが整理されています。

  1. 変更内容の詳細

※ 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 が返ってくる」ことを前提に、実行時に発生するオブジェクト生成・ハッシュ操作の回数を減らすための変更です。


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

    • link_to, url_for, *_url, *_path など、ルーティング経由で URL を生成するすべてのコードが、内部的にこの変更の影響を受けます。
    • 特に 大量に URL を生成する処理(API レスポンス組み立て、HTML レンダリングでヘルパーを大量に呼ぶ処理など)で、ごくわずかなパフォーマンス改善が期待できます。
    • 外部向けの API やメソッドシグネチャ変更はなく、アプリケーションコードからの呼び出し方法は従来通りです。
  • 互換性 / 注意点

    • 仕様変更ではなく内部最適化であるため、通常のアプリケーションコードに対する後方互換性の問題は基本的にありません。
    • ただし、次のような「内部実装への過度な依存」をしている場合は注意してください:
      • ルーティング内部で渡されるハッシュオブジェクトの同一性(object_id)に依存したメタプログラミング
      • default_url_options / url_options などに対して、副作用を期待して破壊的変更 (options[:foo] = ...) を行っているコード
    • この PR では「不要なコピーを減らす」方向の変更なので、今までたまたまコピーされていたハッシュが、コピーされなくなる箇所がある可能性があります。
      → ルーティング内部で渡されるハッシュは、「自由に破壊的変更してよいものではない」とみなすのが安全です(以前からそうであるべきですが、今回の変更でより顕在化しやすくなります)。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    このPRは、Active Record のコネクションプール関連テストがローカル実行時にときどき失敗する(flakyになる)問題を減らすため、テストのエラー報告まわりのコンテキストを正しく隔離するよう修正したものです。ConnectionPoolTests に ActiveSupport::ExecutionContext::TestHelper を mixin することで、テスト実行ごとのコンテキスト汚染を防いでいます。

  1. 変更内容の詳細

対象ファイル:

  • activerecord/test/cases/connection_pool_test.rb(+2行 / -0行)

実質的には、ConnectionPoolTests のテストクラス(もしくはそのベースクラス)に以下のような形で ActiveSupport::ExecutionContext::TestHelper をインクルードする変更です(PR説明からの推定コード例):

ruby
class ConnectionPoolTest < ActiveRecord::TestCase
  include ActiveSupport::ExecutionContext::TestHelper

  # 既存の connection pool 関連テスト群
end

ActiveSupport::ExecutionContext::TestHelper は、ActiveSupport の ExecutionContext(ログ、エラー報告、その他のコンテキスト情報をスレッドローカル等で持つ仕組み)をテストごとにリセット/隔離するためのヘルパーモジュールです。

この PR では、コネクションプール切断テスト(disconnect に関するテスト)の実行中に、他テストや前後のテストで設定されたエラーコンテキストが混ざることで、たまに想定外の状態になりテストが失敗する、という flaky な状態を改善するのが目的です。

ローカルで main ブランチのテストを走らせたときにだけ稀に落ちる、という性質からも、グローバル/スレッドローカルな状態のリークが原因であることを踏まえた修正になっています。


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

    • 本変更は テストコードのみに対する変更 であり、本番コード(ライブラリ本体)には一切変更がありません。
    • 影響は Active Record のコネクションプール関連テスト (connection_pool_test.rb) の実行時にのみ及びます。
    • CI・ローカル共に、コネクションプールのテストの再現性が向上し、ランダムなテスト失敗が減ることが期待されます。
  • 注意点

    • ExecutionContext を利用している他のテストでも類似の flaky がある場合、本PRと同様に ActiveSupport::ExecutionContext::TestHelper をインクルードすることで改善できる可能性があります。
    • バグ修正ではあるものの、ランタイム挙動ではなくテスト安定性に関する修正なため、CHANGELOG は更新していません(チェックリスト上でも未チェック)。

  1. 参考情報 (あれば)
  • ActiveSupport::ExecutionContext / ActiveSupport::ExecutionContext::TestHelper について:
    • Rails 内部でのログ・エラー報告・計測などに紐づく「実行コンテキスト」を扱う仕組みで、スレッドローカルに近い形で管理されます。
    • TestHelper を使うことで、テストごとにこのコンテキストをクリアし、前後のテスト間で状態が漏れないようにできます。
  • flaky なテストに対する一般的な対処:
    • グローバル状態/スレッドローカル状態のリセット
    • モック/スタブの解除、タイマーやスレッドの終了確認
    • ランダムシード固定
    • 今回は「実行コンテキストのリセット」がそれにあたる対策になっています。

#58581 [8-1-stable] Silence Time.rfc3339 redefinition warning ​

マージ日: 2026/8/26 | 作成者: @yahonda

  1. 概要 (1-2文で)
    Ruby 4.1 で time 標準ライブラリに Time.rfc3339 が入ることにより、Active Support 側の同名メソッド定義と衝突して警告が出ていた問題を、警告だけを抑制する形で解消する PR です。8-1-stable では挙動を一切変えず、CI での strict warnings エラーだけをなくすことを目的としています。

  1. 変更内容の詳細

背景 ​

  • Rails(Active Support)は Rails 5.1 以降、自前で Time.rfc3339 を定義している。

  • Ruby 4.1 では time gem(標準添付)側に Time.rfc3339 が入る予定。

  • その結果、Ruby 4.1 上で active_support/core_ext/time/calculations.rb を読み込むと:

    text
    warning: 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 にも記載されている。
  • 一方、この PR は 8-1-stable に対するパッチであり、パッチリリースで挙動を変えたくないため:

    • 引き続き すべての Ruby バージョンで Active Support の Time.rfc3339 を使い続ける。
    • その代わり、Ruby 4.1 で出るメソッド再定義 warning だけを 抑制 する。

実際の変更内容 ​

対象ファイル:
activesupport/lib/active_support/core_ext/time/calculations.rb に 3 行追加。

実装としては、既存の Date#to_time, Date#xmlschema, Time#to_time などで使っているのと同じ silence_redefinition_of_method ヘルパを Time.rfc3339 にも適用します。

イメージとしては以下のような形です(実際の位置や細部は省略):

ruby
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 の挙動は変わりません。

ruby
Time.rfc3339("2026-08-26T10:00:00Z")
# => 2026-08-26 10:00:00 +0000
#    utc? => false (Active Support 独自の +00:00 固定オフセット扱い)

表にすると次のようになります:

RubyTime.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 の中では不変です。


  1. 影響範囲・注意点
  • 対象バージョン:
    • Rails 8.1 系(8-1-stable のみ)。
      Ruby 4.1 リリース時点でサポート期間中なのが 8.1 系だけになるため、そこにだけ適用する方針です。
  • 影響範囲:
    • Ruby 4.1(または同様の条件で Time.rfc3339 が既に定義されている環境)で、Active Support を読み込む際の warning が消える。
    • CI などで RAILS_STRICT_WARNINGS=1 を有効にしている場合に、Time.rfc3339 の再定義 warning によるテスト中断がなくなる。
  • 挙動の後方互換性:
    • 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 上でもすべてパスすることが確認されている。

  1. 参考情報 (あれば)

#58576 Actionpack ractorize ​

マージ日: 2026/8/26 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    Action Pack 内のいくつかのグローバル/共有オブジェクトを Ractor セーフにするための PRです。ParamBuilder のデフォルト、Request の LOCALHOST、Response のデフォルト文字コードとヘッダが、Ractor から安全に扱えるように変更されています。

  1. 変更内容の詳細

全体方針 ​

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 が使えることを検証。

イメージコード例(変更後の設計イメージ):

ruby
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 から利用できることを確認。

イメージコード例(変更後の設計イメージ):

ruby
module ActionDispatch
  class Request
    LOCALHOST = /\A(?:localhost|127\.0\.0\.1)\z/.freeze
  end
end

Ractor 間で共有される定数はミュータブルオブジェクトだと問題になるため、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 を利用できることを確認。

イメージコード例(変更後の設計イメージ):

ruby
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 セーフになっています)

  1. 影響範囲・注意点
  • Ractor を使っていない通常の Rails アプリ
    挙動の差分はほぼありません。default_charset や default_headers を通常どおりオーバーライドして使っている分には、従来と同様に動く想定です。

  • 既存コードとの互換性

    • ActionDispatch::Response.default_headers に対して「破壊的変更(merge! など)」を直接行っている場合、
      今回の変更で freeze されたオブジェクトが返る場合があるため、FrozenError になる可能性があります。
      その場合は以下のように、コピーを作ってから変更する必要があります。
      ruby
      headers = ActionDispatch::Response.default_headers.dup
      headers["X-Custom"] = "value"
    • ParamBuilder.default にセットするオブジェクトを、Ractor 間で共有されることを前提としていない(内部状態を更新する)オブジェクトにしている場合、不具合やエラーの原因になり得ます。
      Proc やイミュータブルなオブジェクトを使うことが推奨されます。
  • Ractor を使うアプリケーション

    • 本PRにより、ActionDispatch 周りで代表的に共有される設定値が Ractor セーフになり、Ractor 実行時の Ractor::UnsafeError を避けやすくなります。
    • ただし Action Pack 全体が完全に Ractor 対応完了したわけではなく、「いくつかの非対応部分を修正した」段階と明示されています。他の部分でまだ Ractor 非対応の箇所が残っている可能性があります。

  1. 参考情報 (あれば)
  • 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. 概要 (1–2文で)
    複数の Redis URL を指定して RedisCacheStore を構成した場合に、clear と stats が NoMethodError で落ちてしまうバグを修正した PRです。これらのメソッドが「リング全体を 1 クライアント」と誤って扱っていたのを、各 Redis サーバーごとに正しく処理するように変更しています。

  1. 変更内容の詳細

問題の背景 ​

RedisCacheStore は、urls: [...] のように複数の Redis URL を指定すると、内部的に「リング(分散)」構成になります。

ruby
Rails.application.config.cache_store = :redis_cache_store, {
  urls: [
    "redis://cache-1.example.com:6379/0",
    "redis://cache-2.example.com:6379/0"
  ]
}

この構成時に、以下を呼び出すと NoMethodError が発生していました:

ruby
Rails.cache.clear
Rails.cache.stats

原因は、clear / stats が「単一の Redis クライアント」を前提とした実装になっており、複数URLで使われる「リングオブジェクト」に対して存在しないメソッドを呼んでしまっていたためです。

修正内容の概要 ​

  • RedisCacheStore#clear

    • これまでは「リングを 1 クライアント」として扱い、flushdb 相当の処理を行おうとしていた。
    • 修正後は、リングに含まれる各 Redis クライアントに対して個別に clear 処理を実行するように変更。
  • RedisCacheStore#stats

    • これまでは「リング全体」に対して stats を取りに行こうとして NoMethodError が発生。
    • 修正後は、各 Redis サーバーから統計情報を取得するように変更。
      • 実装としては、各クライアントに対して info 的なコマンドを発行し、その結果を適切な形でまとめる/返すようになっていると考えられます(テストが追加されていることから、戻り値の形式も複数URL対応で整合が取れています)。

テストの追加 ​

activesupport/test/cache/stores/redis_cache_store_test.rb に 28 行のテストが追加されており、主に次を検証していると考えられます:

  • 複数 URL 構成の RedisCacheStore で clear を呼んでも例外が発生しないこと。
  • 複数 URL 構成の RedisCacheStore で stats を呼んでも例外が発生せず、期待する形式の結果が返ってくること。
  • 単一 URL 構成時の挙動は従来どおりであること(リグレッション防止)。

実装の行数差は小さく(redis_cache_store.rb +3 / -2)、既存ロジックに対して最小限の修正で対応している PR です。


  1. 影響範囲・注意点
  • 主な影響範囲
    • 複数の Redis URL を指定している環境での Rails.cache.clear と Rails.cache.stats の挙動。
    • これまで NoMethodError で落ちていたケースが、正常に動作するようになります。
  • 単一 URL 構成
    • 「1 つの URL」の場合の挙動は「これまでと同じ」と明言されているため、後方互換性への影響はほぼありません。
  • 運用・監視上の注意
    • stats の戻り値は、複数サーバー分を扱う形になるため、複数 URL 対応前提のフォーマットになっている可能性があります。
      • もしアプリケーション側で Rails.cache.stats の戻り値構造に強く依存している場合(特に独自メトリクス集約ロジックなど)、アップデート後に一度確認すると安心です。
  • パフォーマンス面
    • clear / stats 実行時に、URL の数だけ Redis に問い合わせを行うため、URL が多い構成では従来よりも僅かに処理時間が増える可能性があります。
    • ただし、これらのメソッドは頻発するものではなく、実用上大きな問題にはなりにくいと考えられます。

  1. 参考情報 (あれば)
  • 関連 Issue: #58573
    • 「複数 Redis URL 構成で clear / stats が NoMethodError になる」というバグ報告。
  • 対象ファイル:
    • 実装: 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. 概要 (1-2文で)
    Rails の schema cache(.schema_cache.dump / .dump.gz)読み込み処理で、Ruby 2.7 以降の Marshal.load(freeze: true) を利用することで、文字列の intern / freeze を C 側で一括で行い、Ruby レベルの後処理を省いて高速化した PRです。1000+ テーブル規模のコードベースで、スキーマ読み込みから約 40ms を削減できたと報告されています。

  1. 変更内容の詳細

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 に変更(エンコーディング変換を避けるための純粋なバイナリ読み込み)

概念的にはこんなイメージです(実際のコードはもう少し違いますが趣旨として):

ruby
# 変更前(イメージ)
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 の再構築だけを行うように簡素化

だいたい以下のようなイメージです:

ruby
# 変更前(イメージ)
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 だけを「再構築」する処理に特化

  1. 影響範囲・注意点

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 レベルに移行したという違いだけがあります。

  1. 参考情報 (あれば)
  • 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_from
      • SchemaCache#marshal_load
  • schema cache の仕組み全般については Rails ガイド「Active Record のスキーマキャッシュ」および rake db:schema:cache:dump の説明が参考になります。

#58577 Support primary key resetting on Ractors ​

マージ日: 2026/8/26 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    RailsのActive Recordにおいて、Ractor(並列実行用の機能)上でのプライマリキー割り当てが正しく動かない問題を修正し、非メインRactorからでも主キーリセット・取得が正しく行えるようにしたPRです。reset_primary_key の実行をメインRactorに委譲することで、Ractor間で一貫した主キー情報を扱えるようにしています。

  1. 変更内容の詳細

何が問題だったか ​

  • Active Recordのモデルは reset_primary_key を通じて、テーブル定義を見て primary_key を決定・キャッシュしています。
  • しかし、この処理が「メインRactor」を前提にしており、
    • メインRactor以外(サブRactor)で新たにモデルを使おうとしたとき
    • まだプライマリキー情報が設定・キャッシュされていない状態
      だと、主キーの割り当てが正しく行われない(または期待通りに共有されない)という問題がありました。

Ruby 3系で導入されたRactorは「オブジェクトを基本的に共有しない」モデルなので、
単純なクラス変数のキャッシュや初期化処理がRactorごとに分断されてしまう可能性があります。

何を変えたか ​

  • reset_primary_key の処理を「メインRactor側で行う」ように変更し、
    非メインRactorは、主キー情報が未設定のときにメインRactorへ「代理実行(プロキシ)」するようにしました。

PRの説明文をコードレベルに落とすと、イメージとしては次のような動きになります(擬似コード):

ruby
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 が期待通りにセットされるかを確認するようなテストになっているはずです。

  1. 影響範囲・注意点

影響範囲 ​

  • Active Record を Ruby 3 以降のRactor環境で利用している場合に影響があります。
    • 特に、Ractor.new 内で Active Record モデルにアクセスし、
      その主キーを使ったINSERT/UPDATEなどを行うコード。
  • 非Ractor(通常のマルチスレッドやシングルスレッド)環境では挙動はほぼ変わらず、
    既存コードへの影響は非常に小さいと考えられます。

注意点 ​

  • Ractor間は原則としてオブジェクトを共有しないため、
    「メインRactorに問い合わせてprimary_keyを決定してもらう」ための経路が
    Rails内部で用意されるような形になります。
  • パフォーマンス:
    • 非メインRactorで「最初に」primary_keyを決める際に、
      メインRactorとのやり取りが発生しますが、主キー決定は通常は1テーブルあたり1回程度の処理であり、
      実務上のオーバーヘッドは小さいと考えられます。
  • もしアプリ側で独自に primary_key や reset_primary_key をオーバーライドしている場合は、
    Ractor環境での動作(特に「どのRactorで決定するか」)に影響がないかを確認した方が安全です。

  1. 参考情報 (あれば)

#58559 Allow redis gem 6 in the Action Cable adapter on Rails 8.1 ​

マージ日: 2026/8/26 | 作成者: @mugitti9

  1. 概要 (1-2文で)
    Rails 8.1系のAction Cable Redisアダプタが、redis gem 6系も利用できるように依存バージョン制約を緩和したPRです。これにより、redis 6.0.0 を利用するアプリケーションでも、Action Cable の Redis pubsub アダプタを問題なくロードできるようになります。

  1. 変更内容の詳細

2-1. 依存バージョン制約の変更 ​

Action Cable の Redis サブスクリプションアダプタは、これまでロード時に以下のような制約を持っていました:

ruby
gem "redis", ">= 4", "< 6"

このPRで、上限が 6 未満から 7 未満に引き上げられています:

ruby
gem "redis", ">= 4", "< 7"

これにより、redis 6.x が有効化されている環境でも、Action Cable の Redis アダプタをロード可能になります。

具体的には、これまで redis 6.0.0 を入れた状態で Rails 8.1 を動かそうとすると、以下のようなエラーが発生していました:

text
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 未満にピンされていました:

ruby
gem "redis", "< 6"

このPRで、これも "< 7" に変更され、CI 環境でも redis 6 系が解決されるようになります:

ruby
gem "redis", "< 7"

Gemfile.lock もそれに合わせて更新されています(依存解決結果の更新のみで、ロジック変更はなし)。

2-3. アダプタ実装自体の変更はなし ​

actioncable/lib/action_cable/subscription_adapter/redis.rb では、実装コードのロジック変更はありません。
変更は「サポート対象バージョン定義のみ」で、以下のようなAction Cableから見た利用API (subscribe, _client, without_reconnect など) に影響するような修正は不要と判断されています。


  1. 影響範囲・注意点

3-1. 影響範囲 ​

  • Rails 8.1 系で Action Cable + Redis アダプタを利用しているアプリケーション
  • かつ redis gem 6.x を導入したい、あるいはすでに導入しているアプリケーション

これらのアプリは、今回の PR が取り込まれたバージョン以降であれば、特別な変更なしに redis 6.x と Action Cable Redis アダプタを併用できます。

3-2. 互換性について ​

  • redis-rb 6 は、Action Cable が使う主な API については「ほぼドロップイン互換」とされています。
  • 主な変更点は「デフォルトプロトコルが RESP3 になったこと」ですが、古い Redis サーバに対しては自動で RESP2 にフォールバックするため、通常の利用においては特別な設定変更なしでも動く前提です。
  • main ブランチではすでに Redis アダプタが redis gem ではなく redis-client ベースに再実装されていますが、8.1-stable はまだ旧アダプタを維持しているため、このバージョン制約緩和が必要になっています。

3-3. アプリケーション側での注意点 ​

  • すでに redis 6.x を利用していて、Action Cable 起動時に Gem::LoadError が出ていた場合は、Rails 8.1 の該当パッチバージョンに更新することで解消が期待できます。

  • 独自に redis バージョンを Gemfile でロックしている場合、例えば:

    ruby
    gem "redis", "~> 5.0"

    のようになっていれば、今回の PR により「6 を使わなければならない」わけではなく、従来通り 5.x のまま利用可能です。
    6.x を試したい場合は:

    ruby
    gem "redis", "~> 6.0"

    などに変更すればよいです。

  • Redis サーババージョンがかなり古い場合でも、RESP2 フォールバックによって基本的には動作が維持される想定ですが、パフォーマンスや細かい挙動差が気になる場合は、ステージング環境などで動作検証してから本番に適用するのが無難です。


  1. 参考情報 (あれば)
  • このPRでの上限引き上げは、過去のメジャーバージョンアップ対応と整合しています:
    • redis 4.x 対応: #30748
    • redis 5.x 対応: #45856 / #45873
  • main ブランチでの redis-client ベース再実装: ef812c2652
    → 将来的に 8.2 以降などへアップグレードする場合は、そちらの新アダプタ実装へ移行することになります。

#58569 Remove unreachable default for DerivedSecretKeyProvider#derive_key_from ​

マージ日: 2026/8/26 | 作成者: @aryrabelo

  1. 概要 (1-2文で)
    ActiveRecord::Encryption::DerivedSecretKeyProvider#derive_key_from のキーワード引数 using: に設定されていた到達不能なデフォルト値(key_generator)を削除し、必須キーワードに変更したクリーンアップ PRです。挙動は実質的に変わらず、「間違った呼び出し時の例外の種類」が NameError から ArgumentError に変わる程度の影響に留まります。

  1. 変更内容の詳細

問題の背景 ​

DerivedSecretKeyProvider には次のような構造があります(概念的なイメージ):

ruby
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 の定義部分です。

ruby
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 行のみの変更):

ruby
# 変更前
def derive_key_from(secret, using: key_generator)
  ...
end

# 変更後
def derive_key_from(secret, using:)
  ...
end

これにより:

  • すべての呼び出し側は using: を必ず指定しなければならない
  • これはもともと実際の呼び出し側が行っていたことと一致するため、正常系の挙動は全く変わりません
  • インターフェース(契約)としても「Generator は呼び出し側が必ず渡すもの」であることが明示されます

例外の違い ​

もし仮に、外部コードが using: を省略して直接呼んだ場合の挙動は以下のように変化します:

ruby
provider = ActiveRecord::Encryption::DerivedSecretKeyProvider.new(...)

# 想定外の直接呼び出し:
provider.send(:derive_key_from, "secret")
  • 変更前: NameError (undefined local variable or method key_generator)
  • 変更後: ArgumentError ("missing keyword: :using")

PR 説明で示されている検証結果:

text
# before                                        # after
default via send: NameError                     default via send: ArgumentError (missing keyword: :using)

となっており、正常経路 (caminho normal) および DeterministicKeyProvider 経由では前後で同じ Encryption::Key が得られることも確認済みです。


  1. 影響範囲・注意点
  • 正常な利用コードへの影響

    • Rails 本体およびテストコードでは、derive_key_from は #initialize からのみ呼ばれ、using: が明示的に渡されているため、挙動は変わりません。
    • 従って、この PR は「動作変更」ではなく「死んだコードのクリーンアップ」と位置づけられています。
  • メタプログラミング等で private メソッドを直接呼んでいる場合

    • 仮にアプリケーションやプラグイン側で send(:derive_key_from, ...) などを行い、using: を省略していた場合:
      • 以前は NameError が発生していた箇所が、
      • 今後は ArgumentError (missing keyword: :using) に変わります。
    • いずれにせよそのコードは既に「動いていない」コードであり、今回の変更で初めて壊れる、というものではありません。
  • APIとしての契約の明確化

    • using: キーワード引数が「必須」であることがシグネチャに反映されたため、静的解析ツールや型チェッカにとっても扱いやすくなります。
    • PR は itaruby(作者が開発中の Ruby 型チェッカ)によって発見された問題であり、型・静的解析視点でも自然な修正です。
  • テストについて

    • ローカル環境では bin/test を実行できない状態だったものの、CI でのテストを前提とした PR です。
    • 既存テストの挙動は変わらず、正常経路は変更前後で同一の結果となることが手動検証されています。

  1. 参考情報 (あれば)
  • この 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. 概要 (1-2文で)
    RouteSetTest 内で定義されていた、実際には一度も使われておらず、呼び出すと例外が出てしまう request ヘルパーメソッドを削除する PR です。テストコード中の「死んだコード」を整理することで、テストスイートの可読性と健全性を高めています。

  1. 変更内容の詳細

何が消されたか ​

対象ファイル:

text
actionpack/test/controller/routing_test.rb

このファイル内に定義されていた RouteSetTest#request というインスタンスメソッドが削除されています(4行削除のみ、追加行なし)。

削除されたメソッドは概ね以下のような形だったと考えられます(PR文脈からの再構成):

ruby
def request
  @request ||= ActionController::TestRequest.new
end

Ruby の標準的な「メモ化」パターンですが、ここで使っている ActionController::TestRequest.new は、実際には以下のように3つの必須引数を取るコンストラクタです:

ruby
# actionpack/lib/action_controller/test_case.rb:69 付近
def initialize(env, session, controller_class)
end

引数なしで呼ぶと ArgumentError になります。

なぜ「死んだコード」と言えるのか ​

PR では、次の2つの観点から「絶対に使われていない」と論証しています。

  1. 呼び出し側が存在しない (grep による検証)

    • RouteSetTest クラス本体で request というメソッド名が出てくるのは:
      • request_path_params 呼び出し(これは RoutingVerbs モジュール側のメソッドで controller.request を使う)
      • lambda { |request| ... } のブロック引数(ローカル変数)
    • actionpack/test 以下で
      • send(:request)
      • public_send(:request)
      • respond_to?(:request)
        などの間接呼び出しも存在しない。
    • method_missing や delegate による動的ディスパッチもこのファイルにはない。
  2. 呼ばれたら必ず落ちるシグネチャの不一致

    • ActionController::TestRequest#initialize は (env, session, controller_class) の3引数必須。
    • 対象ヘルパーはこれを引数なしで呼ぶ。
    • 従ってこのメソッドが実際に呼ばれていれば、確実に ArgumentError でテストが落ちるはず。
    • テストスイートはグリーンであるため、このメソッドは到達しない = 実際にどこからも呼ばれていない。

他の候補になりそうな呼び出し元の調査 ​

RouteSetTest には以下のモジュールが include されています。

  • RoutingTestHelpers
  • ActionDispatch::RoutingVerbs

PR では「ここから request が間接的に呼ばれていないか」を確認しています。

  • ActionDispatch::RoutingVerbs は、実際には
    • routes.call(params) を通じてルーティングを評価し
    • controller.request を参照している
      だけで、テストクラスのインスタンスメソッド request は使わない。
  • RoutingTestHelpers 側も、request メソッドを定義も呼び出しもしていない。

また、テストスイートで実際に使われている Request ファクトリは:

ruby
TestRequest.create(controller_class)

であり、問題の ActionController::TestRequest.new 直接呼び出しパターンは使われていないことが指摘されています。


  1. 影響範囲・注意点

影響範囲 ​

  • 実行時の挙動は変わらない
    対象メソッドは一度も呼ばれておらず、呼ばれたら即 ArgumentError で落ちる状態だったため、削除による動作変更はありません。
  • 影響するのは actionpack のテストコード内のみ
    actionpack/test/controller/routing_test.rb の内部実装のクリーンアップであり、ライブラリ本体のコード (app/, lib/ 以下) には一切影響しません。

注意点 / 想定される懸念 ​

  • 外部からこのヘルパーを期待していた場合?
    Rails 本体のテストクラス (RouteSetTest) のプライベートな補助メソッドであり、外部(アプリケーション側や別 gem)から利用される想定の API ではありません。そのため互換性の問題は事実上ありません。
  • 将来 RouteSetTest で request を使いたくなった場合
    そのときに ActionController::TestRequest を新しく正しいシグネチャで組み立てる必要があります。現時点では使われていないので、必要になったタイミングで正しい形で追加するのが適切です。

  1. 参考情報 (あれば)
  • 関連 PR(同様の未使用コードクリーンアップ):
    • #58217
    • #58219
    • #58223
  • ActionController::TestRequest の正しい利用パターン(テストスイート内で一般的なもの):
    ruby
    request = TestRequest.create(controller_class)
  • この未使用コードは、作者が開発中の Ruby 型チェッカー「itaruby」による解析で発見された、と PR の末尾で言及されています。

#58568 Remove unused Active Record internals ​

マージ日: 2026/8/26 | 作成者: @aryrabelo

  1. 概要 (1-2文で)
    Active Record / Arel 内部の実際には到達不能なコード(死んでいる分岐・メソッド呼び出し)を削除し、代わりに本来使われている経路にはテストを追加してカバーした PR です。挙動の変更はなく、型チェックや静的解析上のノイズを減らすリファクタリングです。

  1. 変更内容の詳細

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::ArrayHandler
    • HomogeneousIn#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=#&lt;Arel::Table @name="users">, name="name">
    Arel.fetch_attribute: "users"
    equality?: true  invert: :notin
    
    # after
    fetch_attribute yielded: #<struct Arel::Attributes::Attribute relation=#&lt;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 がありました(要旨):

ruby
def columns_hash
  @connection.columns_hash # 引数なしで呼び出している
end

一方で、対応する FakeConnection#columns_hash のシグネチャは:

ruby
def columns_hash(table_name)
  ...
end

となっており、呼び出し側から引数が渡されないため、実行されれば必ず ArgumentError となります。

PR では:

  • この FakeRecord#columns_hash メソッドを削除。
  • リポジトリ全体を grep した結果、このフェイクレコードの columns_hash が呼ばれていないことを確認済み。
  • ダイナミックディスパッチ (send, public_send, respond_to?, method_missing, delegate) でこのメソッドが到達するパスも存在しないことを確認。

つまり、こちらも完全に未使用かつ呼ばれればクラッシュするコードの削除です。


  1. 影響範囲・注意点
  • 外部 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 に新規テストが追加されており、少なくとも「現実に使っている経路」については将来の退行を防ぐ仕組みが強化されました。
  • もし独自に Arel の内部クラスを継承・モンキーパッチしている場合

    • 通常のアプリケーションでは問題になりませんが、Arel の HomogeneousIn を直接継承/再定義し、expr にフォールバックする動作を意図的に使っていたような非常に特殊なコードがあると、今回のクリーンアップによりコンパイル時(読み込み時)に気付きやすくなるかもしれません(ただし、そのようなコードはそもそも実行すれば例外になっていたはずです)。

  1. 参考情報 (あれば)
  • この PR は、未使用コードの整理シリーズの一部です:
    • #58217
    • #58219
    • #58223
  • 死んだ分岐の発見には、作者が開発中の Ruby 型チェッカー「itaruby(まだ未リリース)」が使われたと明記されています。
    → 型チェックや静的解析導入の効果として、「理論上存在するが絶対に到達しない経路」を整理できる好例です。

#58580 Get rid of QueryCacheRegistry#clear ​

マージ日: 2026/8/26 | 作成者: @byroot

  1. 概要 (1-2文で)
    ActiveRecord::QueryCacheRegistry に定義されていた未使用メソッド #clear が削除されました。機能的な挙動変更はなく、不要なデッドコードの整理のみです。

  1. 変更内容の詳細
  • 対象ファイル:
    activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb

  • 変更内容:
    QueryCacheRegistry#clear メソッドの定義が削除されました(6行削除、追加なし)。
    PR説明にもある通り、このメソッドはどこからも呼ばれていない「dead code」と判断され、クリーンアップとして削除されています。

  • 背景:

    • byroot 氏が前の PR (#58566) とあわせて Query Cache 周りを整理しており、その一環として、利用されていない API を削除しています。
    • Rails 内部でも外部でも参照されていないことが確認されたため、後方互換性をほぼ気にせずに削除可能と判断されたと考えられます。

  1. 影響範囲・注意点
  • 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 して確認すると安全です。

  1. 参考情報 (あれば)

#58554 Add config.active_record.schema_ignored_tables ​

マージ日: 2026/8/26 | 作成者: @edcarvio

  1. 概要 (1-2文で)
    Rails の Active Record に、新しい設定 config.active_record.schema_ignored_tables が追加され、これまで別々だった SchemaDumper 用 (ignore_tables) と SchemaCache 用 (schema_cache_ignored_tables) の無視テーブル設定が統合されました。これに伴い、既存の2つの設定は非推奨となり、テーブル名のマッチ方法も「物理名ベース」に統一されています。

  1. 変更内容の詳細

新しい設定項目: 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
    が導入されました。

典型的な設定例:

ruby
# 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_tables
  • config.active_record.schema_cache_ignored_tables

ただし、内部的にはどちらも schema_ignored_tables に委譲されます。

ポイント:

  • 読み書きするときにdeprecation warning を出しつつ、schema_ignored_tables に値を転送する
  • 「現在有効な設定」は常に config.active_record.schema_ignored_tables の値だけになる

移行イメージ:

ruby
# 旧: SchemaDumper だけ
ActiveRecord::SchemaDumper.ignore_tables = [/^_/]

# 旧: SchemaCache だけ
config.active_record.schema_cache_ignored_tables = [/^_/]

# 新: 両方まとめて
config.active_record.schema_ignored_tables = [/^_/]

両方を同時に設定している場合の挙動 ​

旧APIを両方とも設定しているアプリでは、次のようになります:

ruby
# 例: どちらも使っている場合
ActiveRecord::SchemaDumper.ignore_tables = [/^_/]
config.active_record.schema_cache_ignored_tables = ["foo"]
  • 内部的には両方とも schema_ignored_tables = ... を呼び出しているので、
  • 最後に代入された値だけが有効になります(この例では ["foo"] のみ)

この点は、今回の PR で明示的にドキュメント化された仕様です。


  1. 影響範囲・注意点

1) 「物理テーブル名」でマッチするように統一された(Breaking change) ​

これが実質的な Breaking change です。

従来:

  • SchemaDumper.ignore_tables
    → マッチ対象は「prefix/suffix を取り除いた論理名」(remove_prefix_and_suffix 済み)
  • SchemaCache 側 (schema_cache_ignored_tables)
    → マッチ対象は「DB上の物理名」

今回から:

  • どちらも「物理テーブル名」でマッチします

具体例:

ruby
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 を使っているアプリで、正規表現や名前マッチの前提が変わる可能性があります
  • 例えば、「先頭 _ のテーブルを除外したい」なら、物理名に対する条件に書き換える必要があります

例:

ruby
# 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 が追記され、設定ガイドも更新済み

  1. 参考情報 (あれば)
  • PR 本体:
    https://github.com/rails/rails/pull/58554
  • PR 内の挙動議論(特に物理名 vs 論理名の話):
    https://github.com/rails/rails/pull/58554#issuecomment-5409182565
  • 実運用での移行ガイドライン(まとめ):
    1. ActiveRecord::SchemaDumper.ignore_tables と
      config.active_record.schema_cache_ignored_tables は 新設定に置き換える
    2. 全て config.active_record.schema_ignored_tables に一本化する
    3. prefix / suffix を使っている場合は、「物理テーブル名」でマッチしているか確認する
      (DB の実テーブル名を \d などで確認しつつ、正規表現や文字列リストを見直す)

#58570 Fix RedisCacheStore clear and stats with redis-client ​

マージ日: 2026/8/25 | 作成者: @luizkowalski

  1. 概要 (1-2文で)
    Redis 用キャッシュストア RedisCacheStore が、redis-client を使った場合に clear と stats で NoMethodError が出る問題を修正した PRです。FLUSHDB と INFO コマンドの呼び出し方法を RedisClient#call ベースに揃え、回帰テストを追加しています。

  1. 変更内容の詳細

問題の背景 ​

RedisCacheStore は内部で Redis クライアントに対して以下の操作を行っています。

  • #clear(キャッシュ全削除)で FLUSHDB を実行
  • #stats(統計取得)で INFO を実行

従来の redis gem(Redis クラス)では flushdb や info といったメソッドが直接定義されていましたが、redis-client 側ではこれらのメソッドが用意されておらず、「call メソッドで任意コマンドを送る」スタイルになっています。

そのため、redis-client を渡した場合に:

  • redis.flushdb のようなメソッド呼び出し → NoMethodError
  • redis.info のようなメソッド呼び出し → NoMethodError

となってしまう、というバグがありました。

関連 issue: https://github.com/redis-rb/redis-client/issues/311

具体的な修正内容 ​

1) RedisCacheStore#clear の修正 ​

FLUSHDB コマンド実行を、クライアントが持つ call メソッド経由で行うように変更しています。

イメージとしては、以下のような変更です(概念的なサンプル):

ruby
# 変更前(イメージ)
@data.flushdb

# 変更後(イメージ)
@data.call("FLUSHDB")

これにより、以下の両方に対応できます。

  • 従来の Redis クラス(call を実装している)
  • redis-client のクライアント(call だけを提供する実装)

特に「unnamespaced cache」(名前空間なしで全 DB を使っている構成)で clear を呼んだときに NoMethodError が出ていた問題が解消されます。

2) RedisCacheStore#stats の修正 ​

同様に、Redis の INFO コマンドを call 経由で実行するようにしています。

概念的なサンプル:

ruby
# 変更前(イメージ)
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 ベースのクライアント実装との互換性が保たれるようになっています。


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

    • ActiveSupport::Cache::RedisCacheStore を利用し、かつバックエンドクライアントに redis-client(または同様に call だけを提供するクライアント)を使っているアプリケーションでの clear / stats の挙動。
    • 従来の redis gem (Redis クラス) を使っている場合も、call はもともと存在するため後方互換性は維持されます。
  • 実運用上の注意点

    • 「Redis クライアントが call を実装している」ことが前提条件になります。
      • 何らかのカスタムクライアントを使っていて call が無い場合は引き続き問題が出る可能性があります。
    • clear は FLUSHDB を叩くため、その接続先 Redis の「現在選択中の DB 全体」が消える点は変わりません(namespaced でない構成ほど影響が大きいので、従来通り注意が必要)。
    • stats は INFO の結果をパースして使うため、Redis 側のバージョン差異でレスポンス形式が変わるケースでは、従来通りパース処理への影響も考えられますが、この PR 自体は「呼び出し方の変更」のみです。

  1. 参考情報 (あれば)

#58552 Add Herb as an HTML-aware ERB implementation ​

マージ日: 2026/8/25 | 作成者: @marcoroth

  1. 概要 (1-2文で)
    Rails の Action View に、HTML 構造を理解する ERB 実装「Herb::Engine」を使うためのハンドラ ActionView::Template::Handlers::ERB::Herb が追加されました。これにより、HTML+ERB テンプレートを構文木ベースで解析しつつ、現行の Erubi とほぼ互換なインターフェイスで利用できる土台が整えられています。

  1. 変更内容の詳細

2-1. 追加されたコンポーネントの概要 ​

  • 新規クラス: ActionView::Template::Handlers::ERB::Herb
    • Herb::Engine を継承
    • ActionView::Template::Handlers::ERB::Erubi と API 互換になるように実装
  • 依存関係:
    • actionview.gemspec に herb gem を追加
    • Gemfile.lock に Herb のエントリ追加
  • テスト:
    • actionview/test/template/erb/herb_test.rb で Erubi 実装と同等の振る舞いを確認するテストが多数追加

2-2. Herb::Engine の特徴と Rails への組み込み方 ​

Herb は以下の2つのレイヤで解析を行います:

  1. HTML + ERB を「一つの構文木」として解析

    • HTML タグ・テキスト・ERB タグが一体として扱われる
    • テンプレート全体の HTML 構造を把握できる
  2. 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 としては以下のように差し替え可能な設計になっています:

ruby
# 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 の更新

が提案されています。


  1. 影響範囲・注意点

3-1. 影響範囲 ​

  • actionview:
    • 新しい ERB 実装候補が追加されただけで、現時点でデフォルトは Erubi のまま
    • 通常の Rails アプリは、この PR がマージされただけでは挙動は変わりません
  • 依存ライブラリ:
    • herb gem が 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 に依存したテンプレートでは挙動確認が必要です。

  1. 参考情報 (あれば)

#58563 Fix PostgreSQL exclusion constraints with multiline expressions ​

マージ日: 2026/8/25 | 作成者: @jakemcallister

  1. 概要 (1-2文で)
    PostgreSQL の排他制約(exclusion constraint)定義に改行を含む場合、Active Record がそれを正しくパースできず exclusion_constraints 呼び出しやスキーマダンプが壊れる不具合を修正した PR です。制約定義をパースする正規表現に「複数行マッチ」を有効化し、CASE 式を含む排他制約でも正しく読み取れるようにしています。

  1. 変更内容の詳細

何が問題だったか ​

  • 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 など)が複数行に分かれていても、.* / .+ の部分が改行をまたいでマッチするようになります。

(実際のコードは PR 本体になりますが、イメージとしては以下のような変更です)

ruby
# 変更前(例)
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 経由でその制約を読み出したときに、式が正しく取得できるかを確認する回帰テストです。

  • 概要イメージ:

ruby
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万件以上のテスト)でも問題なく通過しています。

  1. 影響範囲・注意点
  • 影響を受けるのは PostgreSQL を使っていて、排他制約を利用している環境 かつ、以下のような状況です:

    • 排他制約の定義に CASE などを含み、pg_get_constraintdef が 改行を含む定義文字列を返すパターン
  • そのような制約を既に使っているアプリでは、Rails をこの修正を含むバージョンに上げることで:

    • ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#exclusion_constraints が正常に動くようになる
    • db/schema.rb(または structure.sql を経由した再生成プロセス)で該当テーブルの排他制約が正しく反映される
      という実利のある修正になります。
  • マイグレーション側の書き方(1行/複数行)では回避できない問題だったため、「CASE を含む排他制約を定義しているのにスキーマダンプに出てこない/読み込みで落ちる」ようなケースは、この修正で解消されます。

  • 正規表現のマッチモードを変えただけなので、既存の単一行の排他制約定義に対しての挙動は変わらない想定です(後方互換性は高い修正です)。


  1. 参考情報 (あれば)
  • この 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. 概要 (1-2文で)
    Rails が内部で設定している I18n の英語ロケール (en.rb) のロードパス用 Proc を、Ruby の Ractor から安全に共有できる形に変更した PRです。これにより、Ractor を用いた並行実行環境でも Rails のデフォルト I18n 設定が問題なく動作するようになります。

  1. 変更内容の詳細

何をしている 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 間で共有可能なオブジェクトにキャストしています。

具体的なコードイメージ ​

実際の差分は小さいですが、イメージとしては次のような変更です(概念的な擬似コード):

ruby
# 変更前(イメージ)
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 環境での回帰を防止します。


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

    • 影響する対象は、Rails がデフォルトで提供する英語ロケール (activesupport/lib/active_support/locale/en.rb) のロード方法のみです。
    • アプリケーション側で独自に設定している I18n.load_path や Proc には直接の変更はありません。ただし、それらが Ractor セーフでない場合は、依然として Ractor 使用時に問題が起きる可能性があります。
    • Ractor を利用しない通常のアプリケーション実行では、挙動面での違いは基本的にありません(翻訳結果は変わらない前提の変更)。
  • Ractor 利用時の恩恵

    • マルチ Ractor 構成(将来的な並行 Web サーバー・ジョブワーカーなど)で Rails を動かした際に、
      • 「I18n のロードパスにある Proc が shareable でない」ことに起因するエラーや警告を避けられます。
    • Ractor 起動前に I18n を初期化しても、それを Ractor に安全に共有できるようになります。
  • 注意点 / 今後検討事項

    • この PR は Rails コアが提供する en ロケールに限定した修正です。
      • アプリケーションや gem が追加する他ロケールや Proc ベースの I18n 設定も Ractor セーフにしたい場合は、それぞれで同様のヘルパを利用するか、Ractor セーフなデータ構造のみをキャプチャするように実装する必要があります。
    • CHANGELOG は更新されておらず、「外から見える仕様変更」ではなく内部実装寄りの安定性向上として扱われています。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    ActionText::Attachment#to_markdown が添付ファイルの caption(キャプション)をエスケープせずにそのまま Markdown に出力していた問題を修正し、Markdown 用に適切にエスケープするようにした PR です。これにより、ユーザー由来のキャプションから不正なリンクなどが Markdown として機能してしまうクラスの問題を防ぎます。

  1. 変更内容の詳細

問題となっていた挙動 ​

元の実装:

ruby
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
end

attachable_markdown_representation を実装していないアプリケーション独自の attachable では、caption がそのまま返されていました。
その結果、例えば以下のようなキャプションをユーザーが入力した場合:

markdown
[click](javascript:alert(1))

Action Text から Markdown に変換した際に、この文字列がそのまま Markdown として解釈され、「javascript:」スキーム付きリンクが動作してしまう状況になっていました。

この caption は以下の経路で未エスケープのまま外に出ていました:

  • Content#to_markdown
  • render markdown: some_content
  • Attachment#to_markdown を直接呼び出し

いずれの経路でも MarkdownConversion.escape_markdown_text や MarkdownConversion.markdown_link の URI スキームチェックを通らずに出力されていました。

※なお、to_markdown 自体はまだリリースされていない API であるため、リリース済みの Rails には影響しません。

修正内容 ​

修正後は、attachable_markdown_representation を実装していない場合のフォールバックパスで caption を Markdown 向けにエスケープします。

イメージとしては以下のような変更です(実際のコードから簡略化):

ruby
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

これにより、先ほどの例:

markdown
[click](javascript:alert(1))

は Markdown 出力としては:

markdown
\[click\](javascript:alert(1))

となり、[click] 部分がただのテキスト扱いになり、Markdown のリンクとして動作しなくなります(javascript: のスキームチェックまでは行っていないが、そもそもリンク構文として成立しない)。

なぜ Attachment#to_markdown 側でエスケープするのか ​

Markdown 変換のパスは複数あります:

  • Content#to_markdown
  • render markdown: attachment
  • Attachment#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_text
    • ActionText::MarkdownConversion.markdown_link などを自前で呼び出し、エスケープやスキームチェックを行う責務がある。

また、RDoc の例で MarkdownConversion を無修飾で参照していたため、トップレベルのモデルから使うと NameError になる問題がありましたが、ActionText::MarkdownConversion のように名前空間付きで参照するように修正されています。

テスト ​

actiontext/test/unit/markdown_conversion_test.rb にテストが追加され、以下をカバーしています:

  • Content#to_markdown 経由での変換
  • render markdown: ... 経由での変換
  • Attachment#to_markdown 直接呼び出し

いずれのパスでもキャプションが Markdown 用にエスケープされることが確認されています。


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

    • ActionText::Attachment#to_markdown を利用するコード(特に、アプリケーション独自の attachable クラスで ActionText::Attachable を include しているが、attachable_markdown_representation を実装していないもの)。
    • ただし to_markdown はまだ未リリース API のため、現時点の安定版 Rails を使っているアプリには直接の影響はありません。
  • Rails 標準の attachable への影響:

    • ActiveStorage::Blob
    • RemoteImage
    • ContentAttachment
    • MissingAttachable
      などはすでに 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 出力に達する」クラスの問題の一つとして整理されています。

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/58553
  • 関連コミット(同種の問題修正): 2191ca0d(本文中で参照されている)
  • 関連クラス / モジュール:
    • ActionText::Attachment
    • ActionText::MarkdownConversion
    • ActionText::Attachable
    • ActionText::Content#to_markdown

#58049 Drive result delivery through QueryIntent ​

マージ日: 2026/8/24 | 作成者: @matthewd

  1. 概要 (1–2文で)
    ActiveRecord のクエリ実行フローにおいて、「結果が勝手に返ってくる」受動的なモデルから、「結果を明示的に受け取りに行く」能動的なモデルに変更し、その中核として QueryIntent を拡張・活用する PR です。現時点で機能的な挙動は変えずに、今後の非同期実行(query pipelining など)を実装するための土台を整えています。

  1. 変更内容の詳細

※実際のコード全文にはアクセスできないため、差分ファイル構成と Rails の既存構造からの推定を含みます。概念的な理解と API レベルの把握を目的にしています。

2-1. 全体像:Result delivery を「能動的な処理」に ​

これまでの ActiveRecord のクエリ実行は、

  1. アダプタが SQL を実行する
  2. DB クライアントが結果を返す
  3. そのまま呼び出し元に戻る

という「実行=結果取得」が一体になった、同期・直列的なモデルでした。

この 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 受け取り、それに「結果を届ける」形に

というスタイルに整理されている可能性が高いです。

イメージとしては:

ruby
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 intent
    • exec_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 ベースになっても例外パスが壊れていないかを検証

  1. 影響範囲・注意点

3-1. 公開 API への影響 ​

  • PR 説明にある通り、「機能的な違いはない」=現時点では公開 API の挙動変更はない ことが意図されています。
    • Model.where(...).to_a
    • connection.select_all, exec_query, execute
    • load_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」が追加されていくと予想されます。


  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    このPRは、Active Record の各 Type が「比較に使う Arel 式(predicate expression)」を定義できる仕組みを導入し、where / order / association などでの値変換・比較ロジックを型に委譲するものです。これにより、Active Model に AR/Arel 依存を持ち込まずに、型ごとに正しい SQL 比較表現を柔軟に定義・拡張できるようになっています。

  1. 変更内容の詳細

全体像 ​

  • 「値を 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 に任せる設計に変更。
  • 結果:
    • 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 名とは異なる可能性があります)が、概念的には以下のような拡張が可能になります。

ruby
# 独自型
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 などクエリ全般で一貫した変換・比較が行われるようになります。


  1. 影響範囲・注意点
  • 影響範囲:
    • 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 テストを確認するのが推奨されます。

  1. 参考情報 (あれば)
  • 対応 PR / Issue:
    • 元 PR: #58135 (Replaces #58135 と明記)
    • 関連 Issue: #58407, #58410
  • 関連実装ファイル:
    • activerecord/lib/active_record/type/query_predicates.rb
    • activerecord/lib/active_record/relation/predicate_builder/comparison.rb
    • activerecord/lib/active_record/relation/predicate_builder/*.rb
    • activerecord/lib/active_record/relation/query_methods.rb
    • activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
    • activerecord/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. 概要 (1-2文で)
    Action Text の Content#to_markdown で使用している内部用タグ <action-text-markdown> について、ユーザーが本文中に同名タグを書いた場合でもそれが「信頼済みの生 Markdown」として扱われないようにする修正です。未リリース機能 (to_markdown) に対する修正であり、リリース済みの Rails には影響しません。

  1. 変更内容の詳細

問題の背景 ​

  • 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」として解釈されてしまっていました。

具体的には、次のようなコードが問題になります。

ruby
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 自体がまだリリースされていないため、
セキュリティリリースとしては扱っていません。


コミット構成 ​

  1. doc: use Markdown markup in Action Text Markdown docs

    • content.rb / markdown_conversion.rb の RDoc コメントが # :markup: markdown に対して適切でなかったものを修正。
    • +text+ や <tt> といった RDoc 固有のマークアップを Markdown 形式に書き直し。
    • コード挙動には一切変更なし。
  2. Move Markdown attachment rendering into MarkdownConversion

    • これまで Content#to_markdown の中でインラインに書かれていた「添付ファイルを <action-text-markdown> で包む」処理を
      MarkdownConversion.render_attachment に移動。
    • <action-text-markdown> というタグ名を RAW_MARKDOWN_TAG_NAME として共通の定数にまとめ、
      複数箇所で文字列リテラルを直接書いていたのを統一。
    • 挙動としては変更なし(リファクタリング)。
  3. Strip user-supplied <action-text-markdown> while canonicalizing — 本体の修正

どこでフィルタするか ​

  • 修正は Content.fragment_by_canonicalizing_content に入っています。
    • このメソッドは、フレームワーク外部から入ってくるマークアップを正規化(canonicalize)するための共通の入口で、
      • ユーザーが新たに入力したリッチテキスト
      • DB から読み出した既存のリッチテキスト を含め、Content の生成時に必ず通る場所です。
  • 一方で、Action Text が添付ファイル用に <action-text-markdown> マーカーを付与するのは render_attachments であり、
    これは canonicalize: false で呼ばれるようになっています。
    • つまり、
      1. 外部から来た HTML を fragment_by_canonicalizing_content で正規化 & 「ユーザー入力の <action-text-markdown> は剥がす」
      2. その後に内部処理 (render_attachments) で「信頼済みマーカーとしての <action-text-markdown>」を付与
        という順序がフレームワーク内部で構造的に保証されるように組まれています。
    • これにより、「どこかで 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> が安全に扱われることを検証。
  • actiontext/test/unit/markdown_conversion_test.rb
    • MarkdownConversion レベルでの Markdown 変換挙動(特に今回のタグに関わる部分)をユニットテスト。
  • 正規化と添付レンダリングの順序が正しく保たれていることを「名前付きのテスト」で明示的に検証しており、
    たまたま他のテストが落ちることに依存しない作りになっています。

  1. 影響範囲・注意点

影響範囲 ​

  • リリース済み 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 1run 2run 3
before1758 i/s1702 i/s1687 i/s
after1393 i/s1372 i/s1367 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 経由で正規化・サニタイズを通してください。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58543
  • HackerOne 報告: https://hackerone.com/reports/3727743
  • 変更ファイルの主なポイント:
    • actiontext/lib/action_text/content.rb
      • fragment_by_canonicalizing_content にユーザー由来 <action-text-markdown> 剥離ロジックを追加。
    • actiontext/lib/action_text/markdown_conversion.rb
      • RAW_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. 概要 (1-2文で)
    I18n.translate / t ヘルパーの scope: 引数でも、先頭にドットが付いた相対キー(.foo)を「現在の翻訳コンテキストに対する相対パス」として解決できるようになりました。これにより、key 引数と scope: 引数の相対キー挙動が揃い、テンプレート内の翻訳記述が一貫し書きやすくなります。

  1. 変更内容の詳細

背景・モチベーション ​

Rails では既に、t(".foo.bar") のように先頭に . を付けたキーを「現在のビュー/コントローラの翻訳スコープ」に対する相対キーとして解釈しています(例: posts#index なら posts.index.foo.bar へ展開)。
しかし scope: オプションに対しては、.foo のような相対指定をしても「相対スコープ」として扱われず、単に foo として扱われていました。

この PR は、scope: に対しても相対キー展開を有効にし、「key と同じ感覚で書けるようにしたい」という意図の変更です。


具体的な挙動の変更 ​

これまでの挙動 ​

ruby
# key 引数側の相対キーは動く
t(".foo.bar") 
# => posts.index.foo.bar

# scope 側の先頭ドットは「無視」される
t("bar", scope: ".foo")
# => foo.bar   # 現在のテンプレートに対する相対展開はされない

この PR 適用後の挙動 ​

ruby
# key 側は従来通り
t(".foo.bar")
# => posts.index.foo.bar

# scope 側でも相対キーとして解決されるようになる
t("bar", scope: ".foo")
# => posts.index.foo.bar

つまり、「scope: に先頭ドットを付けたら、key の相対キーと同様に現在の翻訳コンテキストから解決する」ようになりました。

実用的な使い方の改善例 ​

配列を回しながら翻訳キーを組み立てる場面で記述が簡潔になります。

従来は、相対スコープを使いたい場合、key 側を文字列連結で組み立てる必要がありました:

ruby
["foo", "bar"].each do |val|
  t(".baz.#{val}")
end

この PR 以降は、scope: 側に相対スコープを書けるので、より見通しのよい記述になります:

ruby
["foo", "bar"].each do |val|
  t(val, scope: ".baz")
end

優先順位・互換性に関する仕様 ​

  • 歴史的に、scope: の先頭ドットは「無視される(noop)」挙動だったため、既存コードへの影響を抑えるための配慮が入っています。
  • 作者は「key と scope の両方が相対キーで書かれている場合は、key 側の解釈を優先する」というルールを入れています。
    これにより、t(".bar", scope: ".foo") のような書き方で、従来の key の相対解決ロジックを崩さないようにしています。
  • scope: に配列を渡した場合は、相対展開は行わず、従来どおりの挙動を維持します。

例:

ruby
# 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: での相対キー解決サポート」が追記されています。

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

    • コントローラ内での translate / t 呼び出し
    • ビュー・テンプレート内での t / translate 呼び出し
    • 特に「scope: オプションに先頭ドット付き文字列を渡しているコード」が挙動変更の対象になり得ます。
  • 後方互換性面の注意

    • 以前は scope: ".foo" が「実質 scope: "foo" と同じ」だったのに対し、今後は「現在のコンテキストからの相対スコープ」として扱われます。
    • もし既存コードで「scope: に先頭 . を入れているが、相対解決を期待していない」ケースがあると、翻訳キー解決先が変わる可能性があります。
    • ただし、scope: に配列を渡すケースや、key 側が相対キーで scope 側も相対キーの場合は、互換性に配慮した優先順位(key 優先)が働きます。
  • 実務的なチェックポイント

    • プロジェクト内で scope: ".something" のような記述を grep し、この挙動変更が望ましいか確認しておくと安全です。
    • 今後は、繰り返し処理などで動的にキーを組み立てる場面では、scope: で相対スコープを分離して書くスタイルが推奨しやすくなります。

  1. 参考情報 (あれば)

#58539 Fix the Active Record Store dirty tracking documentation example ​

マージ日: 2026/8/24 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveRecord::Store のドキュメントにある「dirty tracking」の例が、実際の挙動(未保存レコード時)と食い違っていたため、例を「保存済みレコード」を前提とする形に修正した PR です。コード本体の挙動には変更はなく、ドキュメントのみの修正です。

  1. 変更内容の詳細

何が問題だったか ​

ActiveRecord::Store のドキュメント中の dirty tracking のサンプルコードは、以下のように User.new(未保存レコード)に対して説明されていました:

ruby
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 は nil
    • color_change は [nil, 'green']

になります。

これはすでにテストで明示されています:

ruby
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!) に対するテストでは、ドキュメントの期待値どおり、「元の値」が存在する前提で検証されています:

ruby
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!(...) などの「保存済みレコード」を作るコードに差し替えた

といった内容です。

この結果、ドキュメント上のコメントと実際の動きが一致します:

ruby
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 で確認した結果として以下が示されています:

ruby
u = User.new(...)     # -> color_was = nil,     color_change = [nil, "green"]
u = User.create!(...) # -> color_was = "black", color_change = ["black", "green"]

つまり「ドキュメントが示したいケースは、実際には保存済みレコードを前提にした例だった」ので、その前提に合わせるためにサンプルコードだけ修正した、という形です。


  1. 影響範囲・注意点
  • コード本体・挙動の変更は一切なく、「ドキュメントのみ」の変更です。
  • 既存アプリケーションの動作は変わりません。
  • ActiveRecord::Store を利用して dirty tracking(*_changed?, *_was, *_change)を行う際の実際の挙動は次のようになります:
    • 未保存レコード (Model.new) でストア属性に初めて値をセットした場合
      • 「元の値」は存在しないため *_was は nil
      • *_change は [nil, new_value]
    • 既に保存されたレコードに対する更新の場合
      • 「保存時点の値」が *_was
      • *_change は [old_value, new_value]
  • ドキュメントを読んで「new しただけのレコードでも *_was に初期値が入る」と誤解していた場合、その理解を修正する必要があります。

  1. 参考情報 (あれば)
  • 対応するテスト:
    • activerecord/test/cases/store_test.rb
      • 新規レコード時の dirty tracking テスト
      • 保存済みレコード (create!) 時の dirty tracking テスト
  • 関連 API:
    • ActiveRecord::Store
    • Active Record の dirty tracking API:
      • attribute_changed?
      • attribute_was
      • attribute_change

#58538 Fix the documented return type of AVG calculations ​

マージ日: 2026/8/24 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveRecord::Calculations のドキュメントに書かれていた AVG 計算結果の戻り値の型が、実際の挙動と一致するように修正された PR です。コードの挙動自体は変わらず、ドキュメントのみの修正です。

  1. 変更内容の詳細

もともとの記述(誤り) ​

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 の戻り値のクラス
integerBigDecimal
decimalBigDecimal
floatFloat

テスト抜粋(既存テスト):

ruby
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 メソッドの実装には一切手を入れていません。


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

    • ランタイム挙動は一切変わりません。
    • 既存のアプリケーションコードやテストコードへの影響はありません。
    • 影響があるのは、Rails ドキュメントを参考に平均値の戻り型を設計していた場合の「認識」だけです。
  • 注意点

    • average の戻り型を Float 前提で扱っていると、「ドキュメントに書いてあったからそうだと思っていた」という思い込みと、実際のクラス (BigDecimal) の不一致が起こり得ます。

    • これまでにも実挙動は BigDecimal だったので、以下のようなコードを書いていた場合は、昔から壊れていたはずです:

      ruby
      avg = Account.average(:id)
      # ドキュメントだけを信じて Float 前提の API を叩く
      avg.nan?            # BigDecimal にはないメソッド
      avg.is_a?(Float)    # => false
    • 今後はドキュメントが正しくなったことで、

      • 整数 / decimal カラム → BigDecimal
      • float カラム → Float
        を前提にコードを書くことができます。
    • DB ごとの違いについて:

      • PR 作成者が SQLite / PostgreSQL / MySQL で確認しており、この 3 つでは同じ挙動であることが確認済みです。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    ActionController::UrlFor#url_for 内で、既存の Hash を dup してキーを追加する実装をやめ、Hash のキーワード展開(Hash splat)を使うようにしたパフォーマンス改善のPRです。Ruby 4.1 以降で Hash 再確保(reallocation)を避けられ、URL生成まわりの性能・メモリ効率が向上します。

  1. 変更内容の詳細

変更の主旨 ​

これまで url_for では、オプション Hash にキーを追加する際に:

ruby
options = options.dup
options[:only_path] = true

のように「元の Hash を dup した後に新しいキーを代入」していました。
このパターンは、元の Hash が既に「いっぱい」だった場合、コピー側も同じ容量で確保されるため、新しいキーを追加するときに再度メモリ再確保(realloc)を行う必要があるという問題があります。

そこで、次のような Hash のキーワード展開(splat)構文 に書き換えることで、Ruby が最初から「必要なサイズの Hash」を 1 回で確保できるようにしています(Ruby 4.1 以降で効果がある最適化)。

ruby
options = { **options, only_path: true }

Ruby 本体側の最適化(ref: ruby/ruby#18344)により、この { **base, new_key: 1 } パターンは、

  • base のエントリ数+1 を見越した容量の Hash を一発で確保
  • そのうえで要素をコピー&新キーを挿入

という形で実装されるため、重い再確保を避けられます。

実際に近いコードイメージ ​

url_for の内部では、おおよそ次のような変化が入っていると考えられます(実際のコードから簡略化したイメージ):

変更前(イメージ):

ruby
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

変更後(イメージ):

ruby
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 をキーワード展開で組み立て直している」ことです。


  1. 影響範囲・注意点

影響範囲 ​

  • 対象: ActionController::UrlFor#url_for を通じて呼ばれる URL 生成(url_for ヘルパ、*_url / *_path ヘルパなど、内部的にここを経由するもの)
  • 性能面:
    • Ruby 4.1 以降:
      • url_for 呼び出し時の内部 Hash 操作の再確保が減り、メモリアロケーションが削減される。
      • ruby-bench の lobsters / shipit ベンチでボトルネックと認識されていた箇所の改善。
    • それ以前の Ruby:
      • 振る舞いはほぼ同等で、パフォーマンス的にも「悪化しない(ニュートラル)」と説明されています。

互換性・挙動への影響 ​

  • ロジック上は「元のオプション 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 負荷低減やスループット改善につながることがあります。

  1. 参考情報 (あれば)
  • Ruby 本体の最適化 PR:
  • 対象コードの場所:
    • actionpack/lib/action_controller/metal/url_for.rb
      • Rails の URL 生成ロジック(url_for を含む)実装ファイル。

#58546 Fix typo in Reflection initializer ​

マージ日: 2026/8/24 | 作成者: @bquorning

  1. 概要 (1-2文で)
    Reflection 初期化処理内のタイポにより、常に不要な dup が行われていたバグを修正する PR です。
    未定義インスタンス変数 @extension を参照していた箇所を、正しく定義済みの @extensions に修正しています。

  1. 変更内容の詳細

問題の内容 ​

元のコードでは、Reflection のイニシャライザ(あるいは関連する初期化処理)の中で、以下のような構造になっていたと推測できます(簡略化した疑似コード):

ruby
# 実際のコード例(イメージ)
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, and nil.frozen? is always true, causing unnecessary dup calls.

少し補足すると、本来は「オブジェクトが frozen? でないときだけ dup する」というガードを入れたかったのに、間違って nil.frozen? #=> true を見ていたため、「常に frozen? が true」だと誤判断されていました。
その結果、unless @extension.frozen? が常に偽になり、本来必要に応じて行うはずだった dup の条件分岐が壊れてしまい、意図しない挙動や不要な dup 呼び出しが発生していました(PR 文面のニュアンスから、条件が逆だった/ロジックが崩れていた可能性も高いです)。

修正内容 ​

変更ファイルは 1 行のみ:

diff
- @extension.dup unless @extension.frozen?
+ @extensions.dup unless @extensions.frozen?

のような、単数形 → 複数形への単純なタイポ修正です(正確なコードはリポジトリ側に依存しますが、趣旨はこの通りです)。

意図されている正しいロジックは:

  • Reflection に渡された extensions コレクション(モジュールや Proc などの拡張)を保持する
  • そのコレクションが frozen? でないときだけ dup して内部に格納し、外部からの変更の影響を避ける

といった標準的な Rails の防御的コピーのパターンです。


  1. 影響範囲・注意点
  • 対象: Active Record の ActiveRecord::Reflection 周辺ロジックにおける「extensions」関連処理
    • association reflection(has_many, has_one, belongs_to など)のメタ情報を扱う部分で利用されるクラスです。
  • 実際の影響:
    • タイポにより、意図した条件付き dup が行われず、extensions の freeze 状態やオブジェクト共有の前提が崩れていた可能性があります。
    • 直前の PR (#58541) で導入された回帰バグのため、影響期間は短いと思われますが、その PR のコミットを拾っている環境では、以下のような不具合が起こり得ます:
      • association の extensions を外側から渡している場合、オブジェクトが不必要に共有・複製される/されないなどの不一致
      • freeze されたオブジェクトに対する dup が意図通り制御されない
  • 互換性:
    • タイポ修正のみであり、設計意図に沿った動作に戻す変更なので、API レベルの互換性への影響はありません。
    • #58541 適用後に、この挙動に依存したコードを書いていない限り、通常は「バグ修正による正常化」と見なせます。

  1. 参考情報 (あれば)
  • 該当 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? #=> true
    • dup はオブジェクトを浅くコピーするため、mutable なオブジェクト共有を避けるために Rails 内部でよく使われます。

#58545 Don't touch the connection in MySQL's fetch_indexes for an empty list ​

マージ日: 2026/8/24 | 作成者: @kamipo

  1. 概要 (1-2文で)
    MySQL アダプタの fetch_indexes が、空のテーブルリストに対しても不要に DB 接続を開いてしまう問題を修正し、SQLite と同様に「空配列なら即 return」するようにした PRです。これにより、indexes([]) 呼び出し時に接続を触らないため、特定のテストが不安定に落ちる問題も解消されます。

  1. 変更内容の詳細

対象ファイル:

  • activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb

実質的な変更は以下のようなイメージです(擬似コード・抜粋):

ruby
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)でしばしば発生していた不安定な失敗をこの変更で解消している。

  1. 影響範囲・注意点
  • 対象: MySQL アダプタを使う環境で、indexes([]) や、内部的に fetch_indexes に空配列を渡すような呼び出し。
  • 動作の変化:
    • これまで: 空配列でも DB 接続を開く可能性があった。
    • これから: 空配列なら接続に触らず即座に空結果を返す。
  • 互換性:
    • 空配列に対する戻り値としてもとから「空の結果」を期待しているはずなので、アプリケーションレベルの互換性は基本的に問題ない。
    • 「空配列なのに接続が開く」ことに依存しているコードがあれば理論上は影響しうるが、実用上そのような依存は想定されない。
  • 利点:
    • 不要な接続確立を避けることで、テストや本番での無駄な接続コストが減る。
    • 接続の再確立後すぐの状態を前提としたテストがより安定する。
    • SQLite 実装との一貫性が向上し、アダプタ間の挙動差異が減る。

  1. 参考情報 (あれば)

#58541 Further improve Reflextion#extensions ​

マージ日: 2026/8/22 | 作成者: @byroot

  1. 概要 (1-2文で)
    ActiveRecord::Reflection#extensions の実装を前PR(#58540)に続いてリファクタリングし、処理の簡潔化・一貫性向上・不要なコードの削除を行った変更です。挙動自体は基本的に互換を維持しつつ、内部実装を整理しています。

  1. 変更内容の詳細

※ファイル差分が小さく、reflection.rb 内のメソッド(特に Reflection#extensions)周辺のみが対象です。以下はRailsの既存実装とPRタイトル・行数差分からの技術的な推定を含みます。

背景: Reflection#extensions とは ​

ActiveRecord::Reflection は、has_many / has_one / belongs_to などの関連定義を表現するオブジェクトで、
extensions は関連定義に指定された拡張モジュール(extend オプションなど)を返すためのメソッドです。

ruby
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
# => [#&lt;Module ...>] (この extend で渡したモジュールの配列)

今回のPRで行われた主な変更点 ​

コード量としては「+7 / -12」と比較的小さいですが、以下のようなリファクタリングが行われています。

  1. extensions の組み立てロジックの単純化

    • 以前のPR(#58540)で導入したロジックの後続として、条件分岐や配列の組み立て方がさらに整理されています。
    • 例:
      • Array(...) や compact, flatten の使い方を整理し、「常に配列を返す」「nil は返さない」といった性質を明確化。
      • if / case で重複していた分岐をまとめ、1つのパスで配列を構築するような形にしている可能性が高いです。
  2. active_record/reflection.rb 内だけに閉じた変更

    • 変更ファイルは reflection.rb のみで、他のクラス・モジュールに跨る影響はありません。
    • Reflection#extensions 以外にも、これを呼び出す内部メソッドの記述が少し整理されている可能性がありますが、根本的な責務の変更はありません。
  3. 前PR(#58540)のフォローアップ

    • 前PRにより extensions がより柔軟に扱えるようになった一方で、まだ改善余地のあった箇所(冗長な処理・境界ケースの扱い)が今回のPRで修正されています。
    • たとえば以下のようなケースでの扱いが、より一貫した形になっている可能性があります:
      • extend: nil(明示的に nil を渡した場合)
      • extend に単一モジュール vs 複数モジュールの配列を渡した場合
      • scope など他のオプションと extensions の混在

概念的には、次のようなイメージの整理が行われたと考えられます:

ruby
# (イメージ)以前はパターンごとにバラバラに処理していたものを…

def extensions
  exts = []
  exts << options[:extend] if options[:extend]
  exts.concat(Array(@extensions)) if defined?(@extensions)
  exts.flatten.compact
end

# のように「最終的に常に Array&lt;Module> を返す」ことを
# シンプルなパイプラインで保証する形に統一、など。

※上はあくまで構造イメージであり、実際の実装とは異なります。


  1. 影響範囲・注意点
  • 対象バージョン

    • マージ日からすると Rails 7.2 〜 8.0 系列のどこか(あるいは main ブランチ)に入る変更です。ActiveRecord の関連メタ情報周りを使っているコードにのみ影響します。
  • アプリケーションコードへの影響

    • 公開APIである Reflection#extensions の返り値の「型」(配列であること)は維持されています。
    • ただし、以下のようなコードを書いている場合は動作確認が推奨されます:
      • reflection.extensions の中身に対して、nil を含む前提や、特定の順序に依存したコード
      • 前PR(#58540) 時点のバグや曖昧な挙動を“仕様”として利用していたメタプログラミング
  • メタプログラミング・プラグイン作者向け

    • gem / エンジンで Reflection#extensions を直接参照している場合は、テストを流して想定通りの配列が返っているか確認すると安心です。
    • 特に、extend: オプションに配列や Proc を渡すような高度な使い方をしている場合は、rails console などで reflect_on_association(...).extensions の実際の値を確認しておくとよいです。

  1. 参考情報 (あれば)
  • 対象PR:

    • #58541: 「Further improve Reflextion#extensions」
    • 前PR #58540: Reflection#extensions に対する最初の改善(今回の変更の前提)
  • 関連コード(現行の公式実装を確認するには):

    • activerecord/lib/active_record/reflection.rb
      • ActiveRecord::Reflection::AssociationReflection 付近
      • def extensions またはそれに類するメソッドの定義を確認すると、今回のリファクタリング結果が追えます。
  • 利用中アプリでの確認方法例:

ruby
# コンソールなどから
reflection = User.reflect_on_association(:posts)
reflection.extensions         # => どういう配列が返っているか確認
reflection.extensions.map(&:class)

この値をRailsアップグレード前後で比較すると、挙動の差分を把握しやすくなります。


#58540 Avoid needless Array#flatten calls ​

マージ日: 2026/8/22 | 作成者: @byroot

  1. 概要 (1-2文で)
    extending! などで使われていた「*args を受け取って args.flatten する」パターンをやめ、ほとんどの呼び出しで無駄になっている Array#flatten 呼び出しを避ける最適化が行われました。これにより、特にネストした配列が渡される場合に発生していた高コストな内部処理(identity Hash の生成)が減り、パフォーマンスがわずかに向上します。

  1. 変更内容の詳細

背景 ​

  • これまで extending! は典型的な書き方として以下のような実装になっていました(擬似コード):

    ruby
    def extending!(*modules)
      modules = modules.flatten
      ...
    end
  • このパターンは「柔軟に配列/単体/ネスト配列を受け取れる」というメリットがある一方で、

    • 単一の空配列を渡すケース(extending!([]) など)が多い
    • もしくはそもそもネストしていない配列がほとんど という状況だと、毎回 Array#flatten を呼ぶのは無駄になります。
  • 特に Array#flatten は、ネストした配列を辿る際に再帰検出のための identity Hash(オブジェクトIDをキーにした Hash)を内部的に生成するため、コストが高くなりがちです。

今回の変更のポイント ​

PR説明文から読み取れる意図と差分統計(+11 / -2, ファイル2つ)から、以下のような変更が行われたと考えられます。

  1. extending! 周りから無条件の flatten を排除

    • 以前: *args を受け取り、必ず args.flatten を呼んでいた。
    • 以後:
      • 可能な限り「すでにフラットな配列 or モジュール列」として扱う。
      • 必要な場合にのみ flatten 相当の処理を行う、あるいは呼び出し側の API を整理して、ネストした配列をそもそも渡さないようにしている。
  2. ActiveRecord::Reflection と AssociationScope での呼び出し整理
    変更ファイル:

    • activerecord/lib/active_record/associations/association_scope.rb
    • activerecord/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! 側では「ネスト配列が来ない」前提でシンプルに処理できるようにしている可能性があります。


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

    • Active Record の Reflection と AssociationScope 周辺、特に extending / extending! オプションの処理。
    • 関連スコープやカスタムモジュールを関連にミックスインする機能を利用しているコードパス。
  • 外部 API 互換性

    • Rails のパブリック API としての extending / extending! の使い方(シンボルまたはモジュールを一つまたは複数渡す)は、従来どおり動くように配慮されているはずです。
    • ただし、非推奨な使い方(ネストした配列を渡すなど)に依存していた場合には挙動の変化 or 受け付けなくなる可能性があります。
      • 例: extending!([[MyExtension]]) のような深いネストに依存している場合。
      • 一般的には想定されていない使い方なので、問題になる可能性は低いです。
  • パフォーマンス面

    • コストが高いケースは「ネストした配列 + flatten」という組み合わせなので、そうした呼び出しが頻発する場合にわずかながら改善が見込めます。
    • ほとんどのアプリでは体感できるほどの差はないものの、大規模な関連定義や激しくクエリを構築するケースでは微小な改善が積み重なります。
  • テスト観点

    • extending / extending! を利用しているモデル・関連に対し、特に以下を確認すると安心です:
      • 単一モジュール / 複数モジュールを渡した場合の動作
      • アプリ側が配列で渡している場合(extending [Foo, Bar] など)があるなら、その挙動が変わっていないか
    • Rails 側のテストで互換性は担保されているはずですが、アプリ固有のメタプログラミング的な使い方をしている場合は念のため確認を推奨します。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58540
  • 関連知識:
    • Ruby の Array#flatten はネスト検出のために内部で identity Hash を用いる実装になっており、深いネストや自己参照配列を扱う想定のために比較的重い処理です。
    • Rails コア全体で「*args 受け取り + flatten」パターンを見直し、ホットパスから余計な flatten を排除する動きが進んでいます。

#58537 Fix async ActiveRecord::StatementCache execution with out-of-range bind values ​

マージ日: 2026/8/22 | 作成者: @viralpraxis

  1. 概要 (1-2文で)
    ActiveRecord::StatementCache の非同期実行パスで、存在しない Promise.wrap を呼んでいたことによる例外を修正し、正しく Promise::Complete.new を使うようにした PR です。これにより、バインド変数が範囲外のときの非同期実行でも、例外ではなく空結果を返すようになります。

  1. 変更内容の詳細

バグの内容 ​

ActiveRecord::StatementCache の「非同期でのクエリ実行+バインド変数が範囲外」というケースで、以下のようなエラーが出ていました。

text
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

    ruby
    ActiveRecord::Promise::Complete.new([])

これにより、API が想定している正しい方法で「すでに完了している(結果が空の)Promise」を返すようになりました。

テストの追加 ​

activerecord/test/cases/statement_cache_test.rb に、非同期実行かつ範囲外バインド値の挙動を確認するテストが追加されています。

テストの意図としては:

  • ステートメントキャッシュを利用
  • バインド値が範囲外になるような条件でクエリを実行
  • async: true のような非同期パスを通す
  • 結果が「例外ではなく空の結果 (empty relation / empty array 等)」で返ること
  • かつ Promise の処理が正常に完了すること

を検証しています。


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

    • Rails 7.2 以降(ActiveRecord::Promise / 非同期クエリ / StatementCache が導入・変更されたバージョン以降)で、
    • ActiveRecord::StatementCache を使っており、
    • 非同期実行(async / load_async など)を経由し、
    • かつ「バインド変数が範囲外」になる可能性があるクエリを実行するコード
  • これまでの挙動

    • 上記条件を満たすと 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 修正」といった扱いになる可能性があります。

  1. 参考情報 (あれば)

#58519 Load routes lazily when a mounted helper is called ​

マージ日: 2026/8/22 | 作成者: @seuros

  1. 概要 (1-2文で)
    Rails のルーティングを「遅延ロード」している環境で、main_app やエンジンのマウントヘルパを最初に呼び出した場合に NoMethodError が出ていた問題を修正し、必要に応じてルートを自動ロードしてから再実行するようになりました。これにより、URL/パスヘルパと同様に、マウントヘルパもルートの遅延ロードと整合的に動作します。

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

何が問題だったか ​

Rails には、ルートを「最初に使われたタイミングで読み込む」LazyRouteSet があり、以下のような「名前付き URL ヘルパ」はすでにそれを利用していました。

ruby
# これらの呼び出しは LazyRouteSet によりルート定義を読み込む
app.users_path
app.new_user_url

しかし、以下のような「マウントヘルパ」は挙動が異なっていました。

  • main_app
  • my_engine など mount SomeEngine => "/some" によって提供されるエンジン用ヘルパ
erb
<%= main_app.root_path %>
<%= blog_engine.posts_path %>

これらは「routes.rb の mount が実行されたときに定義されるメソッド」であり、
名前に _path/_url のようなサフィックスもないため、LazyRouteSet でのフィルタ条件にも引っかからず、

  • ルートがまだ一度も描画(draw)されていない状態で
  • いきなり main_app やエンジンヘルパを呼ぶ

と、ルートが読み込まれず、そもそもヘルパメソッドが存在しないため NoMethodError が発生していました。

何をしたか ​

MountedHelpers モジュールに method_missing / respond_to_missing? を実装し、
「マウントヘルパらしきメソッドが呼ばれたら、一度ルートをロードしてから再試行する」ようにしました。

おおまかな流れは次の通りです(擬似コード):

ruby
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 にも、マウントヘルパに対する遅延ロード対応が行われた旨が追記されています。


  1. 影響範囲・注意点

影響範囲

  • 遅延ルーティング(LazyRouteSet)を利用している Rails アプリ全般に影響します。
    • 特に、初回アクセスが main_app やエンジンのマウントヘルパ経由になるケース
      (例: レイアウトや initializer 内で main_app を参照する、など)で挙動が変わります。
  • 従来は「初回に NoMethodError になっていた」パターンが
    「ルートをロードしてから正常に動作する」ようになるため、基本的には後方互換的な修正です。

注意点

  • method_missing が追加されたことで、MountedHelpers を include しているコンテキストでは、
    ある程度「柔らかい」メソッド解決が行われるようになります。
    • ただし、ルートロード後も存在しないメソッドは NoMethodError になるため、
      デバッグが極端に難しくなるような変更ではありません。
  • ルーティングのロードタイミングが変わる可能性があります。
    • もし「ルートがまだロードされていないことを前提にした特殊な処理」を自前でしている場合は、
      main_app/エンジンヘルパを呼んだ時点でルートがロードされる点に注意が必要です。
  • respond_to? の結果も、ルートロードをトリガしてから判定されるようになるため、
    反射的にメソッド存在チェックを行うようなロジックで挙動が変わる場合があります
    (もっとも、多くの場合は期待通りの修正と言えます)。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    ActiveRecord::Relation#create のドキュメント中のサンプルコードが、スコープ付き create の振る舞いと食い違っていたため、正しい例になるように修正した PR です。コードの挙動自体には変更はなく、ドキュメントのみの修正です。

  1. 変更内容の詳細

問題になっていた元のドキュメント例は以下のような流れでした(説明文を要約):

ruby
users = User.where(name: 'Oscar')
users.create # => #&lt;User id: 3, name: "Oscar", ...>

users.create(name: 'fxn')
users.create # => #&lt;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 呼び出しが混ざっていた:

ruby
users.create(name: 'fxn')
users.create # => #&lt;User id: 4, name: "fxn", ...>  # ←これが誤解を招く

この PR では、その「余計な引数なし create 呼び出し」を削り、他の例と同様に「create の戻り値をそのまま表示する形式」に揃えています。
変更ファイルは activerecord/lib/active_record/relation.rb の 1 行差し替えのみで、実装コードではなくコメント・ドキュメント行の修正です。


  1. 影響範囲・注意点
  • ランタイム挙動・API 仕様・テストには一切変更なしです。
  • 影響があるのは Relation#create の使い方をドキュメントから学ぶ開発者の理解のみです。
  • 今回の修正から改めて確認できる重要なポイント:
    • User.where(name: 'Oscar').create
      → name: 'Oscar' が自動的に使われる。
    • User.where(name: 'Oscar').create(name: 'fxn')
      → 明示した属性 (name: 'fxn') が優先される。
  • 既存コードを変更する必要はありませんが、「スコープ付き Relation に対して引数なし create を呼ぶと、スコープ条件が属性として使われる」という仕様を正しく理解しておく必要があります。

  1. 参考情報 (あれば)
  • 対象 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. 概要 (1-2文で)
    Rails の Active Model にある AttributeMethodPatternSet に Enumerable が追加され、attribute_method_patterns が再び列挙可能(each や map などが使用可能)になるように調整した PR です。以前は配列だったものをクラスに置き換えた際に失われた「列挙性」を補う変更です。

  1. 変更内容の詳細

背景 ​

  • 過去の 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 行程度の追加)。

イメージとしては以下のような形になります(概念的なコード例):

ruby
class AttributeMethodPatternSet
  include Enumerable

  def initialize(patterns = [])
    @patterns = patterns
  end

  def each(&block)
    @patterns.each(&block)
  end
end

これにより:

ruby
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 経由で期待どおり列挙できること)が確認されています。


  1. 影響範囲・注意点
  • 対象: Active Model の attribute_method_patterns を直接扱っているコード(メタプログラミングやプラグイン、gem など)。
  • 影響:
    • 以前(配列時代)にできていた「列挙処理」が、新クラス移行後にできなくなっていた場合、この PR により再び可能になります。
    • Enumerable を include したことで map, select, reject, find など標準的な Enumerable メソッドが使えるようになり、実質的には「従来の配列に近いインターフェイス」が復元されます。
  • 互換性:
    • public API ではなく private API であるため、公式には互換性保証外ですが、内部的・拡張用コードで依存している場合の「非公開だけど事実上の互換性」を戻した形です。
    • すでに attribute_method_patterns を「配列ではない前提」で書き換えていたコード(例えば to_a を毎回呼ぶなど)は、そのままでも動作しますが、冗長になっている可能性があります。

  1. 参考情報 (あれば)
  • この PR の動機となった変更:
  • 対象クラスの利用箇所の概念:
    • Active Model / Active Record が、find_by_foo, foo?, foo_before_type_cast のような動的属性メソッドを定義する際の「パターンリスト」を表すのが attribute_method_patterns / AttributeMethodPatternSet です。

#58522 Fix Callbacks only made shareable in :raise mode ​

マージ日: 2026/8/20 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    ActiveSupport::Callbacks が、Ractors.unshareable_proc_action を :warn にしている場合でもコールバックを「Ractor 共有可能」にするよう修正された PR です。あわせて、freeze の可視性バグ修正と、アプリケーションの executor が Ractor 内で動作するために必要な executor 関連コールバックの shareable 化も行われています。

  1. 変更内容の詳細

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 設定時にも動くように修正されています。

イメージとしては以下のような変更が入っていると考えられます(擬似コード):

ruby
# 変更前(イメージ)
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 として扱うようにしていることが考えられます(あくまでも概念的なサンプル):

ruby
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 行)。


  1. 影響範囲・注意点

  2. Ractor を使っている Rails / ActiveSupport コードへの影響

    • Ractors.unshareable_proc_action = :warn のときも、Callbacks が自動で shareable 化を試みるようになり、以前より「Ractor で動く」コードが増える可能性があります。
    • 一方で、これまで「実質 Ractor 上では動いていなかったが運良く落ちていなかった」ようなケースで、挙動が変わる可能性があります(より正しく shareable にされる/警告が出る等)。
  3. カスタムコールバックを大量に使っている場合

    • コールバックチェーン内部で Ractor 共有不能なオブジェクト(例: ミューテックス、IO オブジェクト、スレッドローカルに依存する Proc など)を閉じ込めていると、unshareable_proc_action の設定に応じて警告あるいは例外が表に出やすくなります。
    • 将来的に Ractor を活用したい場合は、コールバックに閉じ込めるオブジェクトを「共有可能」なもの(不変オブジェクト、freeze 済みオブジェクト)に寄せることが望ましいです。
  4. freeze 可視性変更の影響

    • ActiveSupport::Callbacks を継承・拡張していて、freeze の可視性に依存した挙動(メソッドをオーバーライドしているなど)をしている場合は、その前提が変わっていないかを確認する必要があります。
    • 一般的なアプリケーションコードでは副作用はほぼありませんが、メタプログラミングを多用しているライブラリでは影響を受けうる箇所です。
  5. Executor を Ractor 内で使っている/使う予定がある場合

    • この修正により、Rails.application.executor 系の処理が Ractor 内でより正式にサポートされる方向に進んでいます。
    • ただし、executor 内部で実際に動く処理(around_action 的なものやフック)に Ractor 非対応なロジックがある場合は、依然としてエラーや警告が出る可能性があります。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Rails の URL ヘルパー(post_path など)を Ractor 内から呼び出せるようにするため、RouteSet と URL ヘルパー生成周りの実装が Ractor 対応になるよう調整された PRです。主なポイントは「URL ヘルパーを格納する Proc を Ractor 共有可能(shareable)にするための参照方法の工夫」と「ルーティング確定後に RouteSet を freeze すること」です。

  1. 変更内容の詳細

背景となる問題 ​

従来、Ractor 内で URL ヘルパーを呼ぶと、内部的に参照している ActionDispatch::Routing::RouteSet オブジェクトが「shareable ではない」ために Ractor::IsolationError が発生していました。

Ruby 3 の Ractor では、Ractor 間で共有できるのは「shareable なオブジェクト」だけで、shareable でないオブジェクトを閉じ込めた Proc も shareable にできません。
URL ヘルパーは内部的には Proc 経由で RouteSet にアクセスするため、ここがネックになっていました。

ruby
routes = self # RouteSet オブジェクト

Ractor.make_shareable_proc { routes }
# => Ractor::IsolationError: cannot make a shareable Proc because it can refer unshareable object ...

解決方針の要点 ​

  1. URL ヘルパーの Proc 自体を shareable にする
  2. ルーティング確定後 (finalize 後) に RouteSet を凍結 (freeze) して shareable にする
  3. URL ヘルパーの Proc は「生成時点では shareable ではない RouteSet にアクセスしない」形で作りつつ、「後から freeze された RouteSet には届く」ようにする

この「一度は Ruby を“だます”が、最終的には正当になる」構成が PR の肝です。

具体的な実装のアイデア ​

1. Proc が直接 RouteSet をキャプチャしないようにする ​

以前は、URL ヘルパー生成時に以下のように RouteSet をローカル変数として閉じ込めていたイメージです(概念図):

ruby
routes = self # RouteSet

helper = proc do |*args|
  routes.url_for(...)
end

この形だと helper は shareable にできません。

PR では、Proc 内で「ローカル変数を通して RouteSet を参照する」のではなく、「モジュール(またはクラス)経由で RouteSet に辿る」ように変えています。Ractor 的には「Module は shareable」とみなされるため、その Module をキャプチャしている Proc であれば shareable にできます。

イメージとしては:

ruby
# 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 し直すなど)。

  1. 影響範囲・注意点
  • 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 しない設計であれば、リクエスト時パスにはほぼ影響しないはず。

  1. 参考情報 (あれば)
  • 本 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. 概要 (1-2文で)
    Rails の URL ヘルパー(edit_post_path など)を Ractor 内から呼び出せるようにするための下準備として、URL ヘルパー生成まわりのオブジェクトを「Ractor セーフ(共有可能)」にする変更です。まだこれ単体では Ractor 内で URL ヘルパーを完全に使えるわけではありませんが、そのための重要な一部です。

  1. 変更内容の詳細

目的・背景の整理 ​

  • 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 内で新たな書き込みが発生しないようにする
  • これにより「Proc が Route をキャプチャしているためにその Proc が unshareable になる」という問題を回避する狙いです。

3. action_dispatch/routing/route_set.rb ​

  • URL ヘルパーを実際に define_url_helper 的なメソッドで定義しているのが RouteSet で、ここでの Proc 生成方法が Ractor 対応されています。

ポイントは2つ:

  1. define_method に unshareable な Proc を渡さない

    • 以前は、ブロックで define_method していて、そのブロックが self や外部ローカル変数をキャプチャしていた可能性があります。
    • それにより、その Proc が Ractor 的に「共有不可能」になり、新しい Ractor に RouteSet を渡したり、Ractor 内でヘルパー生成を叩くと isolation error になっていました。
    • この PR では、
      • Proc が参照するオブジェクトを最小限にし、かつ shareable にする
      • もしくは define_method(name) do |*args| ... end のような形から、外部コンテキストをほぼ持たない形へと書き換える
        といった対応を行っています。
  2. RouteSet の内部依存グラフを freeze / eager load

    • URL ヘルパーメソッド定義時に、Route/Pattern 側で必要な情報をすべて読み出し、メモ化や配列構築を済ませておく
    • その後、それらのオブジェクトを freeze しておき、Ractor 開始後(あるいは worker ractor 内)では状態が変化しないようにする
    • これにより「ヘルパー呼び出し時に ivar に初回アクセス→メモ化→書き込み」というパターンを排除します。

4. テストの追加 ​

  • actionpack/test/dispatch/routing/route_set_test.rb
  • actionpack/test/journey/route_test.rb

で、主に以下のような観点のテストが追加されています:

  • Route および Pattern が freeze / shareable な状態で機能するかどうか
  • URL ヘルパーが生成されるときに isolation error(Ractor の shareable チェック)を引き起こさないこと
  • Ractor 内から呼んだときに、以前問題になっていたメモ化や外部変数アクセス系のエラーを出さないこと

実際のテストコードは概ね次のようなイメージです:

ruby
# 疑似コードイメージ
route = build_route(...)
route.freeze

assert route.ractor_shareable? # 実際には shareable? に相当する検査

ractor = Ractor.new(route) do |r|
  # URL ヘルパーや route の情報を参照しても isolation error が出ないこと
end

ractor.take

あるいは:

ruby
# RouteSet 経由で URL ヘルパーを生成しても Ractor 的に通ることを確認
set = build_route_set(...)
set.finalize! # ここで helpers を定義

assert_nothing_raised { Ractor.new(set) { |s| s.url_helpers.some_path } }

※正確なメソッド名はコードに依存しますが、おおよそこういった性格のテストが追加されています。


  1. 影響範囲・注意点

開発者への実質的な影響 ​

  • 通常のアプリ開発者にとっては、既存コードを書き換える必要は基本的になく、挙動も互換を保つように設計されています。
  • この PR の直接の効果は「URL ヘルパー生成が Ractor セーフになる」ことなので、最終的には以下ができるようになる方向です:
    • View や background job などで Ractor を使い、その中から *_path / *_url ヘルパーを呼ぶ
    • マルチスレッド / マルチRactor環境で RouteSet を共有してパフォーマンスを上げる

ただし、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 上でも追加のロック・同期なしに動けるようになります
  • 大規模ルーティングを持つアプリでは、起動時パフォーマンスへの影響を観測しておくと安心です。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Active Record のテストで、ORDER BY を指定していない SELECT の結果順序に依存してしまう問題(テストのフレーク化やバグの温床)を防ぐため、SQLite でテストを実行する際に reverse_unordered_selects PRAGMA を有効にする設定が config.example.yml に追加されました。これにより、今後のテストが「暗黙の順序」を前提としないよう強制されます。

  2. 変更内容の詳細

  • 変更ファイル: activerecord/test/config.example.yml のみ
  • 内容: SQLite 用のテスト設定に reverse_unordered_selects PRAGMA をオンにする設定項目が追加されています。

イメージとしては、以下のような設定が config.example.yml に含まれるようになったと考えられます(実際のキー名は例ですが、趣旨はこれに相当します):

yaml
sqlite:
  adapter: sqlite3
  database: db/test.sqlite3
  variables:
    reverse_unordered_selects: true

SQLite の PRAGMA reverse_unordered_selects は、ORDER BY が明示されていない SELECT の結果順序を意図的に「逆順」にすることで、「実装依存のたまたまの順序」に依存しているコードやテストをあぶり出すためのデバッグ向け機能です。

この PR では以下の文脈があります:

そのため、Rails 本体の Active Record テストスイートを実行するときに、SQLite では原則 reverse_unordered_selects をオンにするような設定例が公式に提供される形になっています。

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

    • 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 を明示すべきだった箇所の発見につながります。
  • 注意点

    • すべてのテストが SQLite ベースではないため、PostgreSQL 専用テストや SQLite アダプタをスキップしているテストには、この PRAGMA は効きません。したがって「完全な保証」ではなく、「大部分のテストで暗黙順序依存を検出しやすくする」程度の効果です。
    • 自身のアプリケーションのテストでも同様の問題をあぶり出したい場合、開発・テスト用の SQLite 接続で reverse_unordered_selects をオンにすることを検討できますが、その場合は既存テストが壊れる可能性が高いので、段階的な導入を推奨します。
  1. 参考情報 (あれば)

#58527 activerecord: make sure SQLite tests do not rely on implicit SELECT order ​

マージ日: 2026/8/20 | 作成者: @viralpraxis

  1. 概要 (1-2文で)
    このPRは、Active Record の has_many 関連のテストのうち、SQLite で暗黙的なレコードの並び順に依存して失敗していたテストを修正し、データベース実装に依存しない決定的なテストにしたものです。MySQL/Trilogy/SQLite でたまたま成り立っていたインデックス順序への依存を取り除いています。

  1. 変更内容の詳細

対象テスト:
activerecord/test/cases/associations/has_many_associations_test.rb
HasManyAssociationsTest#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 のフォローアップとして、この「インデックスに起因する偶然の順序」に依存しているテストを修正しています。

症状(失敗しているテストの例):

diff
--- expected
+++ actual
-#&lt;Topic id: 4, title: "The Fourth Topic of the day", author_name: "Carl", ...>
+#&lt;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 の呼び方や検証の仕方を、小さな範囲で調整している程度です。

  1. 影響範囲・注意点
  • 実際のライブラリコード(ActiveRecord の本体機能)には変更はなく、「テストコードのみ」の修正です。
  • そのため Rails 利用アプリケーションの挙動に直接の影響はありません。
  • ただし、次のような点に示唆があります:
    • 「ORDER BY を指定していない SELECT の結果順」は、DB 実装やインデックス、バージョン、プラン変更で簡単に変わるため、アプリケーション側でもそれに依存すべきではない。
      • 例: Model.where(...).first を「id が最小のレコード」と思い込まないこと。必要なら order(:id).first と明示する。
    • Rails の公式テストスイートもこのポリシーに揃える方向に継続的に修正されているため、独自のテストを書く際にも「暗黙順序に依存しない」ことが推奨される。

CI・開発環境への影響:

  • SQLite を使って Rails 本体テストを回している環境で、当該テストの不定期な落ち方が解消されます。
  • 他DB(MySQL / PostgreSQL / Trilogy)では主に安定性確保の意味合いで、挙動そのものの変化はありません。

  1. 参考情報 (あれば)
  • フォローアップ元のPR:
  • テストケースファイル:
    • activerecord/test/cases/associations/has_many_associations_test.rb
  • 暗黙的な SELECT 順序に関する一般的な注意:
    • SQL標準では ORDER BY を指定しない限り、結果の順序は未定義であり、特定のDBでの「たまたまの動作」は将来保証されない。

#58530 Make ActiveModel::Conversion._to_partial_path ractor safe ​

マージ日: 2026/8/20 | 作成者: @cam-woloshyn

  1. 概要 (1-2文で)
    ActiveModel::Conversion の _to_partial_path が内部で使うキャッシュの初期化処理を、Ractor に対応できるように変更した PRです。非メイン Ractor から初回アクセスした際に起きていた Ractor::IsolationError を回避しつつ、既存のキャッシュヒット時のパフォーマンスは維持しています。

  1. 変更内容の詳細

何が問題だったか ​

ActiveModel::Conversion には、モデルのパーシャルパスを返す _to_partial_path というメソッドがあります。

ruby
class User
  include ActiveModel::Conversion
end

User.new.to_partial_path # => "users/user"

このパスはクラスインスタンス変数(例: @_to_partial_path)に遅延キャッシュされます。
Ruby 3 以降の Ractor 環境では、

  • メイン Ractor 以外から
  • 初めてこのキャッシュを書き込もうとしたとき

に、クラスインスタンス変数への書き込みが Ractor 間の分離規則に反し、Ractor::IsolationError が発生するケースがありました。

解決方針 ​

PR では次のような方針で修正されています:

  1. キャッシュ済み (既にパスが計算済み) の場合

    • これまで通り、クラスインスタンス変数から直接読み出す「キャッシュヒットの早い経路」を維持。
    • 読み取りだけであれば Ractor 的に安全なため、パフォーマンスも従来通り。
  2. まだキャッシュされていない (初回計算) 場合

    • 「キャッシュを計算・書き込みする処理」を メイン Ractor に委譲 するように変更。
    • 非メイン Ractor からは「メイン Ractor にパーシャルパスを計算+キャッシュさせ、その結果を受け取る」形になるため、Ractor::IsolationError を避けられる。

コードイメージとしては、以下のような流れになっています(※実際のコードから構造を簡略化したイメージ):

ruby
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 が発生しないことを確認。
  • 正しいパス文字列が返ってくることも併せて検証。

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

    • ActiveModel::Conversion#_to_partial_path のキャッシュ初期化ロジックのみが対象。
    • 既存アプリケーションで _to_partial_path / to_partial_path を使っている場合も、振る舞い(返される文字列)は従来と同じです。
  • Ractor を使っていないアプリ

    • 変更の影響はほぼありません。
    • 既存のキャッシュヒット経路は温存されているため、パフォーマンス劣化も原則ありません。
  • Ractor を活用している / これから使いたいアプリ

    • ActiveModel を Ractor 内で扱いつつ _to_partial_path を呼び出す場合、以前は Ractor::IsolationError に遭遇していた可能性がありますが、この PR によりその問題が解消されます。
    • 「キャッシュ未初期化状態での初回アクセス」のみメイン Ractor 経由の処理が挟まるため、ごく限定的なオーバーヘッドが発生する可能性がありますが、通常は気にする必要のない範囲です。
  • 互換性

    • 公開 API のシグネチャ/返り値は変わっていません。
    • Ractor 非対応 Ruby(古いバージョン)では特別な副作用はありません。

  1. 参考情報 (あれば)
  • 対象クラス: ActiveModel::Conversion
    • 主に to_partial_path / _to_partial_path を提供し、ActionView のパーシャルレンダリングで利用される。
  • 関連キーワード:
    • Ractor, Ractor::IsolationError, クラスインスタンス変数, 遅延キャッシュ, マルチスレッド / マルチ Ractor 対応。

#58521 Add temporary name to App's Executor/Reloader ​

マージ日: 2026/8/20 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    Rails アプリケーションの Executor / Reloader クラスに、一時的な「わかりやすい名前」を付けるようにした PR です。これにより、コンソールやデバッガでこれらのクラスを確認する際に、匿名クラスではなく、元アプリケーションに紐づいた名前で表示され、デバッグしやすくなります。

  1. 変更内容の詳細

何をしているか ​

Rails アプリケーションが内部的に使っている Executor と Reloader は、これまで「匿名クラス」として生成されていましたが、それらに Ruby 3.3 で追加された Module#set_temporary_name を使って一時名を付与しています。

PR 説明にあるように、変更前後の挙動は以下の通りです。

変更前:

ruby
d(dev):001> Rails.app.executor
=> #&lt;Class:0x0000000128412318>

変更後:

ruby
d(dev):001> Rails.app.executor
=> ActiveSupport::Executor(#&lt;D::Application:0x00000001131b5008>)

これにより、Rails.app.executor を見ただけで「ActiveSupport::Executor の、D::Application 用のインスタンスである」という情報が分かります。

コードレベルの変更イメージ ​

ファイル: railties/lib/rails/application.rb

おおよそ次のような形の変更が入っています(実際のコードは多少異なる可能性がありますが、概念的にはこのイメージです)。

ruby
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(" やアプリケーション名が含まれているかをアサートしている。

  1. 影響範囲・注意点

影響範囲 ​

  • 主に「開発・デバッグ体験」に関する変更です。

    • 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 などを厳密な文字列比較に使っているような場合(あまり一般的ではないが)、表示文字列の変化がテストの失敗を誘発する可能性はあります。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    ActiveModel::Type::ImmutableString のAPIドキュメントから、true: / false: オプションに関する記述が削除されました。これらは内部実装用であり、パブリックAPIとしてはサポートしない方針が明確化されています。

  1. 変更内容の詳細

何が変わったか ​

  • 対象ファイル: 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でドキュメントから削除されています。

※実コードでの利用例を意図したサンプル(※以下は「やってはいけない例」):

ruby
# 本来は想定されていない内部オプション利用
attribute :status, :immutable_string, true: "1", false: "0"

このような使い方を「公式にサポートしている」と誤認させないために、APIドキュメントからこの類の記述が削除されています。


  1. 影響範囲・注意点
  • 実装コードへの影響:

    • ほぼドキュメントのみの変更であり、ImmutableString の挙動そのものは変わっていません。
    • 既存アプリの実行時挙動に影響は基本的にありません。
  • 公開APIの観点:

    • true: / false: オプションは「そもそも公開APIではなかった」と公式に位置づけが明示された形です。
    • もしこれら内部オプションに依存したコードを書いていた場合:
      • これまでも非公式・非推奨な利用であり、将来のバージョンで削除される可能性が高い、というシグナルと捉えるべきです。
      • 代替としては、boolean から string への変換はアプリケーションレイヤー(カスタム型、callback、value object など)で明示的に行うことが推奨されます。
  • ドキュメントとの整合性:

    • APIドキュメントを見て「ImmutableString で boolean シリアライズ形式を変えられる」と思っていた開発者は、今後はその前提を見直す必要があります。
    • RBSや型定義、YARD 等の外部ツールは、ドキュメントに依存している場合、このオプションが「ないもの」として扱われるようになります。

  1. 参考情報 (あれば)
  • 関連PR:
    • #39599: MySQL系アダプタの boolean シリアライズ("1" / "0")を保つために true: / false: オプションが導入されたPR
    • #44306: ImmutableString に true: / false: をAPIドキュメントとして記載してしまったPR
  • 今回のPRの意図:
    • 「内部実装のためだけのオプションを公開API扱いにしない」
    • 「将来的にはこれらオプションそのものを削除したい」というメンテナの意思表明

実務的には、「ImmutableString に boolean シリアライズをねじ込むための隠しオプションは使わない」「MySQL固有の表現はアダプタや専用の型クラスに閉じ込める」という設計方針がより明確になった変更と考えられます。


#58525 Deprecate ActiveRecord::Relation#uniq! ​

マージ日: 2026/8/20 | 作成者: @kamipo

  1. 概要 (1-2文で)
    ActiveRecord::Relation#uniq! が非推奨(deprecate)となり、Rails 9.0 で削除される予定になりました。Rails 7.0 以降はクエリの重複排除が自動化されているため、このメソッドは役割を終えています。

  1. 変更内容の詳細

背景 ​

  • Rails 7.0 で、「複数値をとるクエリメソッド(たとえば select, group, order など)」に対して、同じ値が重複して指定されても自動的に重複を取り除く(deduplication) 挙動がデフォルトになりました。
  • その移行パスとして、Rails 7 以前のコードベースで挙動の違いを段階的に慣らすために導入されたのが Relation#uniq! です(PR #39358)。
    • 当時は「重複排除を有効にしたいアプリ側が明示的に uniq! を呼ぶ」ためのメソッドでした。
  • しかし 7.0 以降ではすでに自動で重複排除されるため、uniq! 自体が意味を持たなくなりました。

今回の変更点 ​

主な変更は以下の通りです。

  1. ActiveRecord::Relation#uniq! に非推奨警告を追加

    • uniq! を呼び出すと、将来の削除を知らせる deprecation warning が出るようになります。
    • 内部的な処理としては、すでに Rails 7.0 以降で自動的に行われている deduplication に頼るだけか、あるいは単に何もしない「no-op」扱いで、機能的な意味はなくなっています。
    • 実質的には「呼んでも動作結果は変わらないが、ログに警告が出るメソッド」という位置付けになります。
  2. CHANGELOG の更新

    • activerecord/CHANGELOG.md に
      • ActiveRecord::Relation#uniq! が非推奨になったこと
      • Rails 9.0 で削除予定であること
        が明記されました。
  3. テストコードの更新

    • activerecord/test/cases/calculations_test.rb
    • activerecord/test/cases/relation/merging_test.rb
      が修正され、uniq! を前提としたテストや、その挙動まわりの期待値が「非推奨である」ことを前提とした内容に調整されています。
    • つまり、今後は uniq! を積極的に使うのではなく、既存利用の後方互換性と警告表示のみを担保するという方針がテストにも反映されています。

イメージ用サンプルコード ​

ruby
# 旧: 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! を呼ばなくても自動で重複が排除される設計

  1. 影響範囲・注意点
  • 影響を受けるのは ActiveRecord::Relation#uniq! を明示的に呼んでいるコードのみ です。
    • そもそも使っていなければ、機能的な挙動もログも変わりません。
  • 今すぐの挙動変化はほぼ無し
    • 既に Rails 7.0 以降では deduplication がデフォルトで有効なので、uniq! の有無によるクエリの「結果の違い」は基本的にありません。
    • 今回の PR による目に見える変化は、「呼ぶと deprecation warning が出るようになる」点です。
  • 今後の対応方針
    • Rails 9.0 で Relation#uniq! は削除される予定です。
    • 今のうちにアプリケーションコードから uniq! 呼び出しを削除してください。
      • uniq! 呼び出し行をそのまま消すだけで、通常は機能的に問題は発生しない想定です。
      • 不安な場合は、uniq! 呼び出し前後で生成される SQL や結果件数を比較して regression テストを行うと安全です。
  • uniq(非破壊版)との混同に注意
    • uniq! は relation を破壊的に変更するメソッドでしたが、uniq という API が(もし存在している環境で)どう扱われているかは別の話です。
    • 実アプリで uniq を拡張していたり、ActiveRecord 以外の層で uniq 相当のものを使っている場合は、今回の PR だけを見て混同しないよう注意してください。
      (この PR で非推奨なのは Relation#uniq! だけ です。)

  1. 参考情報 (あれば)
  • 該当 PR:
    • Deprecate ActiveRecord::Relation#uniq! (#58525)
  • 関連 Issue:
    • #56338 — Relation#uniq! の今後の扱いに関する議論をクローズ
  • 元の導入 PR:
    • #39358 — Rails 7.0 のデフォルト deduplication 導入時の移行用として uniq! 追加
  • 実務上のガイドライン:
    • Rails 7 以降のアプリで Relation#uniq! を使っている場合は、Rails 9 移行までに全削除する 方針でリファクタリングを進めるとよいです。

#58489 Track fixture connection pools as they are pinned ​

マージ日: 2026/8/20 | 作成者: @ngan

  1. 概要 (1-2文で)
    ActiveRecord のテスト用フィクスチャで「接続プールのピン止め(pinning)」を行う処理の順序と記録方法を修正し、エラー時に teardown が誤った接続解除を試みて二次的な例外を発生させないようにした PR です。
    これにより、実際の障害原因(例: SQLite の database is locked)が素直に報告され、ピン止め状態の取り扱いも正しくなります。

  1. 変更内容の詳細

背景となる問題 ​

元のコードでは、テスト開始時の setup_transactional_fixtures で「これからピン止めする予定の接続プール一覧」を先に配列に詰め、その後で順次 pin_connection! を呼んでいました:

ruby
@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つあります。

  1. ピン止めに失敗したプールも @fixture_connection_pools に残ってしまう

    • pin_connection! が例外を投げた場合でも、そのプールは配列から取り除かれません。
    • teardown では @fixture_connection_pools.map(&:unpin_connection!) を実行するため、「そもそもピンされていない接続」に対して unpin_connection! を呼ぶことになり、
      RuntimeError: There isn't a pinned connection が発生していました。
  2. unpin_connection! による “全プールのトランザクション状態チェック” が中断される

    ruby
    unless @fixture_connection_pools.map(&:unpin_connection!).all?
      @@already_loaded_fixtures.clear
    end
    • unpin_connection! が途中で例外を投げると all? まで到達しないため、「どこか別のプールでテスト中にトランザクションがコミット・ロールバックされた」場合に、キャッシュ済みフィクスチャ (@@already_loaded_fixtures) をクリアできないケースがありました。

さらに、テスト実行中に新たな接続プールが作られた場合に、それを pin する通知サブスクライバ側にも順序の問題がありました:

ruby
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 の修正内容 ​

  1. ピン止めした「後で」プールを記録するように変更

    • setup_transactional_fixtures 側:
      • 以前は「リストを先に作る → 順に pin する」だったところを、
      • 「プールを順に処理し、pin に成功したものだけを @fixture_connection_pools に記録する」ように変更しています。
    • これにより、pin_connection! が例外を投げたプールは @fixture_connection_pools に入らず、teardown がそのプールに対して unpin_connection! を呼ぶことはなくなります。
  2. 通知サブスクライバ側も “pin に成功した時点で記録” に変更

    • 新しいプールがテスト中に作られた場合も、pin_connection! 成功後ただちに @fixture_connection_pools に追加し、その後に lease_connection などを行う順序に変更。
    • lease_connection などが失敗しても、既にプールは tracking 対象になっているため、teardown で確実に unpin_connection! が呼ばれます。
  3. 新しいテストの追加

activerecord/test/cases/test_fixtures_test.rb に、ピンできないプールを実際に用意して挙動を検証するテストが追加されています。

  • 前提: pool: 0 + 短い checkout_timeout の接続プールを使い、pin_connection! が ConnectionTimeoutError を必ず投げるようにする。
  • テストが確認する点:
    • teardown 時に発生するエラーが RuntimeError: There isn't a pinned connection ではなく、「本来の障害」である ConnectionTimeoutError として報告されること。
  • このテストは 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 がテスト失敗としてそのまま見えるようになりますが、ロック競合そのものは別問題として残っています。


  1. 影響範囲・注意点
  • 対象: ActiveRecord のテストフィクスチャ (ActiveRecord::TestFixtures) の内部実装
    • 本番アプリケーションの接続プール動作ではなく、Rails 内部・アプリのテスト環境におけるトランザクションフィクスチャの振る舞いが対象です。
  • 期待される改善:
    • トランザクションフィクスチャ利用時に、エラーが発生したセットアップ/teardown のログがより正確になる
      • 二次的な There isn't a pinned connection が出なくなる
      • 「本当の原因」(ConnectionTimeoutError や SQLite の BusyException など) がダイレクトに表示される
    • 異常系発生後も、ピン止めされた接続の深さや open transaction 数が正しく戻されるため、以降のテストケースへの副作用(コネクションがプールに返らない、外側トランザクションが張り付き続ける等)が減る
  • 互換性への影響:
    • 公開 API ではなくテスト周りの内部挙動の修正であり、通常のアプリケーションコードに対する後方互換性への影響は基本的にありません。
    • ただし、「これまで teardown が投げていた『ピンされていない』系の例外に依存したテスト」がもしあれば(ほぼないと思われますが)、エラーメッセージ/エラー種別が変わります。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    table_options が複数テーブルを一括で読めるようになり、PostgreSQL/SQLite でテーブルメタデータの読み取りが大幅にバッチ化・高速化された PR です。特に SQLite はカラム・PK・FK・インデックス等の取得をほぼ定数個の SQL にまとめ、スキーマダンプのクエリ数と時間を大きく削減しています。

  1. 変更内容の詳細

2-1. table_options のインターフェース拡張 ​

SchemaStatements#table_options が単一テーブルだけでなく「テーブル名の配列」を受け取れるようになりました。

ruby
# 従来どおり単一テーブル
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)」としても利用できる特性を活かし、一度のクエリで複数テーブルの情報を取得する形に全面的に書き換えています。

代表的なクエリ例:

sql
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) を JOIN
  • m.sql(CREATE TABLE 文全文)を一緒に流すことで、
    • COLLATION
    • AUTOINCREMENT
    • GENERATED COLUMN など、pragma では拾えない情報も解析可能にしている

これにより、以下のリーダーがテーブル数に依存しないほぼ定数個のクエリで済むようになっています。

リーダー変更前のステートメント数変更後
columns5061
primary keys5061
indexes4352
foreign keys7592
check constraints2531
primary_key_has_autoincrement?4401

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 の「プレーンなテーブル」を大量に作った合成スキーマでの結果:

テーブル数ステートメント数時間
250768 → 19126.3 → 48.5 ms
10003018 → 19435.5 → 125.7 ms

PostgreSQL / SQLite では、テーブル数を増やしてもステートメント数が増えない形になっているのがポイントです。

2-4. ダンプ結果の互換性検証 ​

挙動変更による「順序の変化」などを疑われる箇所について、かなり厳密に互換性確認が行われています。

  • PostgreSQL / SQLite / MySQL 8.0.46 / MariaDB 11.8.8 のスキーマダンプは バイト単位で完全一致

  • インデックス・FK のダンプはソートされるので、リーダーの返却順が変わっていてもダンプだけでは検出できない
    → そのため、

    • columns
    • indexes
    • primary_keys
    • foreign_keys
    • check_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_names
  • table_partition_definition

PostgreSQL 側での batched query が両者の情報をまとめて返すようになったためです。


  1. 影響範囲・注意点
  • 公開インターフェースとしては事実上 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 の削除の影響を受ける可能性があります(通常のアプリケーションではほぼ無関係)。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58498
  • 関連 PR:
    • #58421, #58488, #58494(今回のフォローアップとして位置づけられている)
  • 影響ファイル(抜粋):
    • activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
    • activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
    • activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
    • activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
    • activerecord/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. 概要 (1-2文で)
    Rails のルーティングで使われるポリモーフィック URL ヘルパー用のキャッシュを frozen(不変オブジェクト)にし、Ractor 内から link_to(record) などを安全に呼べるようにした PR です。これにより、Ractor 実行時に発生していた「非共有オブジェクトへのアクセス」エラーを防ぎます。

  1. 変更内容の詳細

背景・問題点 ​

Ractor は「共有可能なオブジェクト」しか他 Ractor から参照できません。
従来の実装では、ポリモーフィック URL を生成する際のキャッシュが「凍結されていない(mutable な)定数」であったため、サブ Ractor からアクセスすると以下のようなエラーが発生していました:

text
can not access non-shareable objects in constant 

ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder::CACHE by non-main Ractor.

このエラーは例えば以下のようなケースで発生します。

ruby
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}_path
  • new_#{resource_name}_path
  • #{resource_name.to_s.pluralize}_path など

たとえば Photo モデルなら:

  • edit_photo_path
  • new_photo_path
  • photos_path

これらを毎回文字列結合で作るのはコストがあるため、Rails は内部で CACHE という定数(Hash など)に、リソース名ごとにヘルパー名を格納して再利用しています。

今回の変更 ​

PR タイトル通り、この HelperMethodBuilder::CACHE を freeze しました。

変更点は 1 行だけで、

ruby
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 を直接テストしているか、キャッシュ周辺の挙動を確認している可能性が高い)。


  1. 影響範囲・注意点
  • Ractor 対応の改善

    • サブ Ractor から link_to(record), url_for(record), polymorphic_path(record) などを呼んでも non-shareable objects エラーが出なくなります。
    • Ractor ベースの並列処理を行うアプリケーションにとっては実行時エラーが解消される重要な変更です。
  • 既存アプリへの互換性

    • CACHE はそもそも「内部キャッシュ」であり、アプリ側が直接書き換える前提の API ではないため、freeze による互換性問題は基本的にありません。
    • もしメタプログラミングで ActionDispatch::Routing::PolymorphicRoutes::HelperMethodBuilder::CACHE を直接書き換えていた場合は、今回の変更で例外が出るようになります(ただし、そのような利用は非推奨かつレアケースです)。
  • パフォーマンス

    • キャッシュ戦略自体は従来と同じで、「標準 3 アクションだけキャッシュ、その他は都度計算」のままです。
    • freeze したことによるパフォーマンス劣化は、基本的にはありません(Ractor で使えるようになる分、実効スループットは改善します)。
  • Ractor 対応の観点

    • Rails を Ractor セーフにしていく過程の一部であり、今後も同様に「定数やキャッシュを凍結して共有可能にする」変更が増えていくことが予想されます。
    • Ractor を積極的に使う予定がある場合、自分で定義しているグローバルなキャッシュやクラスインスタンス変数なども、freeze や Ractor.make_shareable を意識すると良いです。

  1. 参考情報 (あれば)

#58517 Freeze the Response default_headers at the end of the boot process ​

マージ日: 2026/8/19 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    Rails のブート完了時に ActionDispatch::Response.default_headers を freeze(凍結)するようにし、アプリケーション実行中にデフォルトレスポンスヘッダが書き換えられないようにした PR です。
    これにより Ractor から安全にデフォルトヘッダへアクセスできるようになり、並列実行時のスレッドセーフ性・予測可能性が向上します。

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

何をしているか ​

変更ファイルは 1 つだけで、actionpack/lib/action_dispatch/railtie.rb 内の 1 行が変更されています。

イメージとしては以下のような変更です(※実際のコードを概念的に表現):

ruby
# 変更前(概念)
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 間で安全に共有・参照できるようになる

という整理がされています。


  1. 影響範囲・注意点

影響範囲 ​

  • ActionDispatch::Response.default_headers をアプリケーション起動後に動的に変更していたコードは、FrozenError が発生するようになります。

例(NG になるパターン):

ruby
# config/initializers/xxx.rb ではなく、リクエスト処理中にこんなことをしているコード
ActionDispatch::Response.default_headers["X-Frame-Options"] = "DENY"

上記のように、リクエストハンドラやジョブ内などで default_headers に代入・削除を行っていると、ブート後のタイミングでは FrozenError になります。

一方で、ブートプロセス中 (initializers や config ブロック内) での設定は、after_initialize より前に行われていれば従来通り行えます。

対応方針 / ベストプラクティス ​

もし実行時にレスポンスヘッダを変えたい場合は、default_headers を書き換えるのではなく、個々のレスポンスに対してヘッダを設定してください。

例:

ruby
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 ミドルウェア中で:

ruby
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 を保つ」という設計が強制される。

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/58517
  • 関連概念:
    • Ruby Ractor / shareable objects: Ractor.shareable?, freeze によるイミュータブル化
    • Rails の after_initialize フック: アプリケーションのブート処理完了後に呼ばれるフックで、ここで freeze することで「ブート後は不変」という境界を明確化している。

#58437 Extract model schema state into SchemaContext ​

マージ日: 2026/8/19 | 作成者: @adrianna-chang-shopify

  1. 概要 (1-2文で) Rails の Active Record 各モデルが持っていた「スキーマ依存の状態(カラム情報・型・デフォルト値・ステートメントキャッシュなど)」を、新たに導入された SchemaContext オブジェクトに集約し、モデルからはそのコンテキストへ委譲する形にリファクタリングした PR です。将来的な複数スキーマ切り替えを見据えつつも、この PR 自体では「1モデル = 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 安全性を担保

イメージコード(実装イメージ・擬似コード):

ruby
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 に委譲されるようになっています。

代表的な変更:

  • 以前は:

    ruby
    class Post < ApplicationRecord
      # Post.columns などが Post クラスインスタンス変数を直接参照
    end
  • 変更後は:

    ruby
    class 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 マイグレーション中に旧スキーマ・新スキーマを切り替えたいアプリケーションは以下のようなカスタマイズを想定できます(イメージ):

ruby
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.rb
    • schema_context の生成、デフォルトコンテキストの保持、各種 schema 系メソッドの委譲実装
  • テスト:
    • activerecord/test/cases/attributes_test.rb, core_test.rb に schema_context ベース実装を前提としたテスト追加・調整

  1. 影響範囲・注意点

影響範囲 ​

  • パブリックAPIとしての振る舞いは基本的に変わらないように設計されています。
    • Model.columns, Model.attribute_types, Model.column_defaults などのメソッドシグネチャ・返り値は同じで、内部の保持場所だけが変わったイメージです。
  • ただし、以下のようなコードを書いていた場合は影響を受ける可能性があります。
    • モデルクラスの内部インスタンス変数(例: @columns, @attribute_types)を直接参照していたメタプログラミング
    • Active Record の内部実装に強く依存して拡張していた gem / ライブラリ

注意点 ​

  1. 内部構造への依存を避ける

    • 今後、さらに「スキーマコンテキストの切り替え」機構が正式に入ることも想定されているため、SchemaContext 自体も進化する可能性が高いです。
    • 内部インスタンス変数や内部メソッドに直接アクセスせず、公開されている API(columns, attribute_types など)を通して利用するのが安全です。
  2. 独自のスキーマキャッシュ拡張をしている場合

    • 既存で ActiveRecord::Base.columns などをモンキーパッチ/オーバーライドしていた場合、schema_context への委譲経路を考慮した見直しが必要になることがあります。
    • 将来的にスキーマ切り替えをしたい場合は、build_schema_context をオーバーライドしてプロキシを挟む設計が推奨パターンになります。
  3. Ractor 対応を意識した実装

    • SchemaContext が Module 継承で Ractor-shareable になったのは、マルチ Ractor を意識した設計変更です。
    • Ractor を使うアプリケーションでは、スキーマ関連の状態が Ractor-safe に共有される方向に進んでいる、と理解するとよいです。
    • ただし「キャッシュの初期化そのもの」はメイン Ractor で行うため、初期化タイミングや遅延ロードの動作には従来同様の注意が必要です。

  1. 参考情報 (あれば)
  • 元 PR: https://github.com/rails/rails/pull/57118
    • こちらはスキーマコンテキストの切り替え機能も含んでいたが、本 PR では単一コンテキストに絞った形で再設計・リファクタリングされている
  • 関連コード:
    • activerecord/lib/active_record/model_schema/schema_context.rb
    • activerecord/lib/active_record/model_schema.rb
  • 今後予想される発展:
    • マルチデータベース / ロールオーバー / スキーママイグレーション期間の「片足ずつ別スキーマ」状態を扱うための公式なスキーマコンテキスト切り替えサポート
    • Ractor 対応のさらなる強化(スキーマ情報の共有モデルの明確化)

#58485 Honour all_queries default scopes in internal queries ​

マージ日: 2026/8/19 | 作成者: @adrianna-chang-shopify

  1. 概要
    このPRは、all_queries: true が付いた default scope を「内部クエリ」にも適用するための仕組みを導入し、ユニークネスバリデーション、fixture ロード、カウンタキャッシュ更新などで、all_queries default scope が正しく尊重されるようにしたものです。通常の default scope は内部クエリには含めず、all_queries 指定のものだけを反映する専用スコープ all_queries_scope が追加されています。

  1. 変更内容の詳細

新しく導入された all_queries_scope ​

ActiveRecord::Scoping::Named に、内部的に使うためのクエリスコープ all_queries_scope(およびそれに関連する仕組み)が追加されています。役割は:

  • 適用するもの

    • default_scope all_queries: true, ... で定義された default scope
    • Model.scoping(all_queries: true) { ... } などで設定された「現在のスコープ」
  • 適用しないもの

    • 通常の default scope(all_queries: true が付いていないもの)

つまり、アプリケーションで「どんなクエリにも必ず乗せたい条件」だけを内部クエリにも反映するためのレイヤーを用意した、という位置づけです。

※ PR の説明文中で触れられている「durable scopes」(別PRで議論中)とは分離されており、この PR は「all_queries だけを扱う最小限の変更」として切り出されています。

all_queries default scope のイメージ ​

イメージとしては、以下のようなモデルを想定します:

ruby
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 など、「どんなクエリにも必ず付けたい制約」がユニークネスチェックにも反映される

サンプルイメージ:

ruby
class User < ApplicationRecord
  default_scope all_queries: true do
    where(account_id: Current.account_id)
  end

  validates :email, uniqueness: true
end

User の email ユニークネスチェックは、必ず WHERE account_id = ... を含むクエリで行われます。

fixture ロードでの利用 ​

activerecord/lib/active_record/fixtures.rb でも、fixture の挿入に使うクエリに all_queries_scope が適用されます。

  • マルチテナントアプリで、tenant_id を all_queries default 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_queries default scope が効くようになります。
  • これにより、例えば「テナントごとにカウンタを持つ」ような設計で all_queries default 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 だけは尊重する」方針へと整理されたといえます。


  1. 影響範囲・注意点

影響を受ける箇所 ​

この変更により、以下の処理で発行される SQL に all_queries default scope(および scoping(all_queries: true) で設定したスコープ)が含まれるようになります。

  • ユニークネスバリデーション (validates :foo, uniqueness: true)
  • fixture ロード (ActiveRecord::FixtureSet 経由)
  • カウンタキャッシュの更新・リセット(counter_cache: true、touch: true など関連周り)

挙動変化の具体的な例 ​

  1. ユニークネスバリデーションが、テナント単位に変わる

    これまでは「default scope を外して全テナントを対象に重複チェックしていた」ケースが、all_queries を使っている場合は「特定テナントだけ」でチェックされるようになる可能性があります。

    ruby
    class 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 = ?
  2. fixture ロード時に WHERE tenant_id = ... が付く

    テストで使う fixture データが、all_queries default scope で絞られた状態で挿入・参照されるようになります。

    • もし既存テストが「テナントに依存しない前提」で fixture を書いていた場合、テナント関連の default scope 定義との整合性を確認する必要があります。
  3. カウンタキャッシュのカウント対象が変わる

    all_queries default scope によって、カウンタキャッシュの対象レコードの範囲が狭まったり変化することがあります。

    • 例: 「論理削除されたレコードは数えない」が all_queries scope として定義されていると、カウンタキャッシュも論理削除を反映した値になります。

注意点 / マイグレーション時の確認 ​

  • すでに default_scope all_queries: true を導入済みのアプリで Rails をこの変更を含むバージョンに上げる場合:

    • ユニークネスバリデーションの挙動が変わっていないか(特にマルチテナント)
    • fixture のロード・テスト結果が変化していないか
    • カウンタキャッシュの値が想定どおりか(差分が出るようなら、一度フルリセットを検討)
  • まだ all_queries を使っていない場合:

    • この変更自体による挙動変化はほぼなく、将来 all_queries default scope を活用するための基盤が整備された、という位置づけになります。

  1. 参考情報
  • 元 PR(durable scopes 系からの切り出し):
    https://github.com/rails/rails/pull/57123

  • この PR 自体:
    https://github.com/rails/rails/pull/58485

  • 関連する主な変更ファイル:

    • 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. 概要 (1-2文で)
    Rails の flash 機能でカスタム flash type 用メソッドを定義する処理が、define_method から class_eval に変更され、Ractor(並行実行環境)内でも正しく動作するように修正されました。これにより、メイン Ractor 以外でも add_flash_types で定義したメソッドが利用可能になります。

  1. 変更内容の詳細

背景 ​

Rails では、例えば以下のようにして独自の flash type を追加できます:

ruby
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 メソッドを定義している部分が:

ruby
# 旧実装イメージ(簡略)
flash_types.each do |flash_type|
  define_method(flash_type) do
    self[flash_type]
  end
end

のような define_method ベースの実装から、class_eval を使う実装に変わっています。

イメージとしては、次のようなスタイルです(実際のコードはもう少し細かいですが、方向性として):

ruby
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 行の追加テスト)。

  1. 影響範囲・注意点
  • 一般的な Rails アプリケーションでの挙動は変わりません。
    既存の flash[:key] や add_flash_types :info で定義されるメソッドの使い方はそのまま利用できます。
  • 主な影響は「Ractor を使って Rails コードを並行実行するケース」で、これまでメイン Ractor 以外から add_flash_types で定義されたメソッドにアクセスした際に問題になる可能性があった箇所が解消されています。
  • class_eval によるメソッド定義になったため、
    • メソッド定義がソースコードとしてクラスに埋め込まれる形になり、
    • その分、スタックトレースや method_source 的なツールからはより通常メソッドに近い扱いになります。
      ただし、アプリケーションコード側から見える API や挙動は従来通りです。
  • class_eval の使用により、セキュリティ面での懸念がある場面(動的に任意のユーザ入力を埋め込む等)が考えられますが、本件では定義対象はアプリ側が指定したシンボル(flash type 名)であり、Rails コア側で管理されているため、通常の利用では追加のリスクはほぼありません。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    ActionView::ViewPaths の _prefixes を eager_load! 時に事前計算するように変更し、Ractor 内からでも安全かつ効率的に利用できるようにした PR です。これにより、Ractor 環境での ActionView 利用時のスレッド/メモリ共有まわりの問題を回避する狙いがあります。

  1. 変更内容の詳細

※ 実際の diff は 6 行追加・1 行削除のみの小規模変更です。ファイル:
actionview/lib/action_view/view_paths.rb

概ね次のようなことをしていると考えられます(擬似コードイメージ):

ruby
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 からは「すでに確定済みの値」を読むだけにする設計変更です。


  1. 影響範囲・注意点

影響範囲:

  • ActionView::ViewPaths を利用している部分(主に Rails のビュー解決ロジック)。
  • Rails.application.eager_load! などで eager_load! を呼び出す環境全般(本番環境など)。
  • Ractor を利用している/利用しようとしている Rails アプリケーション。

考えられる副作用・注意点:

  • _prefixes の計算タイミングの前倒し
    • これまでは「初回アクセスまで遅延されていた計算」が、eager_load! 時に発生するようになるため、起動時コストがごくわずかに増える可能性があります。
    • ただし _prefixes 自体は比較的軽い処理であり、実務上はほぼ影響ないレベルと考えられます。
  • Ractor を使わない通常のアプリでは挙動上の違いはほとんどありません。
    • 外部 API は変わらず、_prefixes の内容自体も従来と同じです。
  • Ractor を使う場合は、
    • 「Ractor を起動する前に eager_load! を確実に実行しておく」ことで、_prefixes を含む ViewPaths 周りの初期化をメイン Ractor に閉じ込めることができます。
    • これは「Ractor 開始後にグローバル状態を書き換えない」という Ractor セーフティの原則に沿った運用になります。

  1. 参考情報 (あれば)

#58402 Return a UTC time from Time.rfc3339 for "Z" inputs ​

マージ日: 2026/8/18 | 作成者: @yahonda

  1. 概要 (1-2文で)
    Time.rfc3339 に "Z"(UTC 指定)の文字列を渡したとき、Active Support が「固定オフセット +00:00」ではなく「UTC 時刻(utc? == true)」を返すように変更し、今後 Ruby 4.1 以降に標準で入る Time.rfc3339 の仕様に揃えた PRです。これにより時間値そのものは変わらず、主にシリアライズ時の表記が "+00:00" から "Z" に変わる可能性があります。

  1. 変更内容の詳細

Ruby 本体との整合性を取る方針 ​

  • Ruby 4.1 では time gem がデフォルトで入り、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 のポイントは以下です。

  1. Ruby が Time.rfc3339 を提供している場合

    • Active Support は 再定義しない。
    • そのまま Ruby 本体の Time.rfc3339 を利用します。
  2. Ruby が Time.rfc3339 を提供していない場合(現行の全リリース版 Ruby)

    • Active Support が Time.rfc3339 を定義する。
    • その実装を "Z" のときに「UTC 時刻」を返すように修正し、Ruby 本体の将来的な実装と揃えます。

この存在チェックは Range#overlap? の対応と同じパターンで、require "time" を先に呼び出してから Time.respond_to?(:rfc3339) のようなかたちで行われます。
これにより、require の順番によって挙動が変わる、という事態を避けています。

"Z" の扱いの具体的な変更 ​

挙動の違いは次の通りです。

ruby
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 に緩和。

  1. 影響範囲・注意点

影響する環境と挙動 ​

環境Time.rfc3339 の定義元"Z" 入力時の結果
現行の全リリース Ruby(Time.rfc3339 なし)Active SupportUTC time (utc? が true)
Ruby 4.1 以降想定(Time.rfc3339 あり)Ruby 本体UTC time (utc? が true)、再定義なし

つまり、どの環境でも最終的に "Z" からは UTC 時刻が返るという挙動に統一されます。

互換性上の注意点 ​

  1. utc? や zone に依存しているコード
    "Z" をパースした結果に対して、utc? や time.zone などをチェックしているコードがあると、挙動が変わる可能性があります。

    • 例えば、以前は utc? #=> false だったので utc? を条件分岐に用いていた場合、今回の変更で true になるため、処理が変わります。
  2. シリアライズ結果(JSON 等)の違い

    • as_json / to_json などで UTC 時刻を出力する場合、"2026-08-07T10:00:00+00:00" ではなく "2026-08-07T10:00:00Z" という表記になる可能性があります。
    • "Z" と "+00:00" のどちらも RFC 3339 としては同値ですが、文字列比較をしている外部サービス/テスト等があると差分になる可能性があります。
  3. 例外メッセージの違い

    • rescue で例外メッセージ文字列を直接マッチしているようなコード(あまり望ましくないが存在しがち)があると、Ruby 実装と Active Support 実装でメッセージが異なる点に注意が必要です。
    • 推奨は、例外クラスや、自前のラッパーを使ったハンドリングです。
  4. タイムゾーン変換との関係

    • Time.rfc3339("...Z") の結果を in_time_zone / Time.zone.rfc3339 等で扱うときの変換結果の時刻自体は変わりません。
    • 変更はあくまで「UTC フラグとシリアライズ表現」のレベルです。

  1. 参考情報
  • 対応 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. 概要 (1-2文で)
    Active Record Migrations ガイド内で、execute メソッド名の表記がコードフォントになっていなかった箇所にバッククォートが追加され、他のメソッド表記と統一されました。アプリケーションコードには一切影響せず、ドキュメント上の表記ゆれを修正するものです。

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

  • 対象ファイル: guides/source/active_record_migrations.md

  • 変更内容は 1 行のみで、テキスト中の execute メソッド名にバッククォートを付けたものです。

    例(イメージ):

    diff
  • You can also use the execute method to run arbitrary SQL.

  • You can also use the execute method to run arbitrary SQL.
    
    これにより、ガイド中でメソッド名が他の `change`, `up`, `down` などと同じくコードフォントで表示され、Markdown 的にも正しい書式になります。
  1. 影響範囲・注意点
  • 影響範囲は Rails ガイド(ドキュメント)の表示のみです。
  • Rails 本体の挙動や API 仕様、マイグレーションの書き方そのものには一切変更はありません。
  • 既存コード・既存マイグレーションを修正する必要はありません。
  • CI スキップ ([ci skip]) 指定の通り、テスト実行不要な純ドキュメント変更です。
  1. 参考情報 (あれば)
  • 対象ガイド: 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
    end
    上記のように、execute はマイグレーション内で任意の SQL を直接実行したい場合に使用するメソッドであり、その説明部分のメソッド名表記が今回の PR で整えられています。

#58507 Remove trailing whitespace from Action Controller guide ​

マージ日: 2026/8/18 | 作成者: @55728

  1. 概要 (1-2文で)
    Action Controller ガイド(guides/source/action_controller_overview.md)内の行末の余分な半角スペースを 2 箇所削除し、guides:lint:mdl の Markdown Lint (MD009 Trailing spaces) エラーで main ブランチの CI が落ちていた問題を解消する PRです。
    内容テキスト自体の変更はなく、レンダリング結果も変わらないホワイトスペースのみの修正です。

  1. 変更内容の詳細
  • 対象ファイル:

    • 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 修正のみ」であり内容変更ではありません。

  1. 影響範囲・注意点
  • 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 では走らせ続けるのが有用です。

  1. 参考情報 (あれば)
  • 関連 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

#58229 [ci skip] Fix explanation of controller callbacks with blocks ​

マージ日: 2026/8/18 | 作成者: @flavio-b

  1. 概要 (1-2文で)
    このPRは、Railsガイド中の「ブロック付きのコントローラコールバックはコントローラインスタンスのスコープで動かない」という説明が実際の挙動と逆であるため、それを修正したドキュメント更新です。実装は変更せず、現状の instance_exec による挙動(コントローラインスタンス文脈で評価される)に説明を合わせています。

  1. 変更内容の詳細
  • 対象ファイル: guides/source/action_controller_overview.md
  • 主な修正点:
    • 「ブロックを渡したアクションコールバックはコントローラのスコープで実行されない」という、古い実装(Rails 2.3 時代)に基づいた説明を削除・修正。
    • 現在の挙動どおり、「ブロックはコントローラのインスタンスコンテキストで実行される」ことが分かるようにガイドを更新。

PR本文の例:

ruby
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では、この履歴に沿って「コントローラコールバックだけがモデルコールバックと違う」という誤った印象を与える表現を修正し、モデルコールバック同様に「ブロックはインスタンスコンテキストで実行される」ことを前提とした説明に揃えています。


  1. 影響範囲・注意点
  • 影響範囲:
    • コードの挙動は一切変わらず、ドキュメントのみの修正です。
    • 既存アプリケーションの挙動には影響ありません。
  • 注意点:
    • これまで古いガイドを信じて「ブロック内の self はコントローラではないはず」と思っていた場合、その前提は誤りです。

    • 現在のRails(少なくとも 4.1 以降)では、以下のような書き方が正しく動作します:

      ruby
      before_action do
        # self はコントローラのインスタンス
        logger.info "Current path: #{request.fullpath}"
        @user = current_user
      end
    • 逆に、「|controller| 引数を必ず使わなければならない」と考えて設計している場合、その前提も不要です。self をそのまま使って問題ありません(controller 引数は互換性のため残っているだけの位置付けと捉えられます)。


  1. 参考情報 (あれば)
  • コールバック実装の変更の流れ(概要):
    • Rails 2.3: ブロックに controller を引数として渡していた設計。
    • Rails 3: コントローラにメソッドを定義して実行する方式へ。
    • Rails 4.1: instance_exec を用いて、ブロックをインスタンスコンテキストで評価する方式に変更。
  • 関連ドキュメント:
    • 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. 概要 (1-2文で)
    Sign Up and Settings ガイド (guides/source/sign_up_and_settings.md) に含まれていた英文の文法ミスを修正したドキュメント専用のPRです。アプリケーションの挙動やコードには一切変更はありません。

  2. 変更内容の詳細

  • 対象ファイル: guides/source/sign_up_and_settings.md
  • 修正内容:
    • 英文の文法的におかしい箇所を6文、読みやすく自然な英語表現に修正
    • 追加行 8 / 削除行 8 なので、文の差し替えレベルの変更に留まる
    • 主な変更パターンとして想定されるもの:
      • 動詞の時制や主語・動詞の一致の修正
      • 冠詞 (a / an / the) の抜けや誤用の修正
      • 前置詞 (in / on / at など) の不自然さの修正
      • 不自然な語順や冗長表現の簡潔化

実際のコード例やAPIの変更はなく、あくまでガイド本文の英文修正のみです。

  1. 影響範囲・注意点
  • 実行コードやテスト、設定ファイルなどには一切変更なし
    • 挙動変更なし
    • 互換性への影響なし
    • ビルド・デプロイ手順への影響なし
  • 対象はガイド文書だけなので、Rails アプリケーションを利用しているプロジェクト側で対応すべき作業はありません。
  • 英語ドキュメントを参照しているユーザーにとって、Sign Up and Settings ガイドの可読性と理解しやすさが向上します。
  • [ci skip] が付いているため、このPR自身のマージ時にはCIは実行されていません(ドキュメントのみの変更であることを示唆)。
  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Active Record のトランザクション計測(instrumentation)テストで、手動の ActiveSupport::Notifications.subscribe / unsubscribe をやめ、NotificationAssertions ヘルパーに置き換えたテスト専用のリファクタリングです。アプリケーションの実行時挙動には一切変更がありません。

  1. 変更内容の詳細

背景 ​

  • #53822 で導入された NotificationAssertions テストヘルパーを、まだ置き換えられていなかった activerecord のトランザクション関連テストに適用したフォローアップ PRです。
  • 対象は activerecord/test/cases/transaction_instrumentation_test.rb のみで、変更行数からも分かる通り、主にテストの簡素化・整理が目的です(+102 / -208 と削除が多い)。

具体的な置き換え内容 ​

これまでのテストパターン(概念的な例):

ruby
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) { ... }
    • ブロック実行中の指定イベント一覧を配列で受け取って、後から詳細を検証したいケース

イメージとしては、以下のような書き換えが行われています:

ruby
# 以前: 手動 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

または、もっと簡潔に:

ruby
assert_notifications_count "transaction.active_record", 1 do
  run_transactional_code
end

ヘルパーに置き換えなかったケース ​

ActiveSupport::Notifications.subscribe をあえて残しているテストも明示されています:

  1. start_transaction テストで、トランザクション途中の状態をアサートするもの
    • NotificationAssertions は基本的に「ブロック実行後に通知を検証する」前提のため、トランザクションの途中経過(mid-transaction)を逐次検証するようなテストでは意味が変わってしまう。
  2. after_commit / after_rollback の発火順(ordering)を検証するテスト
    • イベントの順序やタイミングを細かく見たいテストは、通知のサブスクライブをより低レベルに制御できる従来の方法を維持。
  3. 「壊れた(broken)サブスクリプション」テスト
    • サブスクライバ自体が例外を投げるケースを扱うテストで、ヘルパーに抽象化してしまうと意図が変わる/見えづらくなるため、素の subscribe / unsubscribe を維持。

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

    • 変更は activerecord/test/cases/transaction_instrumentation_test.rb のみで、本番コードには一切手を入れていません。
    • Active Record のトランザクションに関する ActiveSupport::Notifications の動作仕様は変更されていません。
    • gem を利用する側(アプリケーション/ライブラリ)に対する互換性の影響はありません。
  • 注意点

    • 今後、トランザクションの計測イベント周りのテストを追加/修正する場合、基本は NotificationAssertions を優先的に使うのが推奨される流れです。
    • ただし、イベントの「途中経過」「発火順」「サブスクライバの例外挙動」といった、時間軸や低レベルな挙動をテストしたい場合は、これまで通り ActiveSupport::Notifications.subscribe を直接使うべきケースも残る、という設計方針が明確になっています。

  1. 参考情報 (あれば)
  • 関連 PR:
    • #53822 — NotificationAssertions ヘルパー導入の元 PR
  • 該当テストファイル:
    • activerecord/test/cases/transaction_instrumentation_test.rb
      • トランザクション開始・コミット・ロールバックなどに対する ActiveSupport::Notifications のイベント発火を検証するテスト群が、より読みやすく・短くなっています。

#58484 Fix clearing PK-overlapping association keys when assigning nil ​

マージ日: 2026/8/18 | 作成者: @matthewd

  1. 概要 (1-2文で)
    belongs_to / has_one に nil を代入したときの外部キークリア処理で、「外部キーが参照元モデルの主キーと完全に重なっている場合」の挙動を修正した PR です。特に単一カラムFKやポリモーフィック belongs_to で、外部キーが正しく NULL にされない/type だけ消えるといった不整合な挙動を修正しています。

  1. 変更内容の詳細

背景となる仕様と問題 ​

  • 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 がそのまま残るという不整合も発生していた。

この PR の修正方針 ​

説明文を元にすると、今回のルールは以下のように整理できます:

  • 「PK と重なっている FK は残す」という特別ルールは、
    FK が複数カラムから構成されていて、かつ PK と重なっていない別の FK カラムをクリアできる場合にのみ適用する。
  • 逆に言うと、
    • FK のすべてのカラムが PK に含まれている(= 完全オーバーラップ)の場合は、
      • その外部キーも 通常通りクリア対象とする(nil 代入を有効にする)。
  • ポリモーフィック 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 にする(従来は何も変わらなかった)。
  • ポリモーフィック belongs_to
    • クリア対象外/対象カラムの判定時に、*_type と *_id を個別に扱っていた部分を修正し、
    • 両方とも同じルール(PK オーバーラップ判定も含む)で一貫して処理するように変更。

テストの追加内容 ​

  • belongs_to_associations_test.rb
    • 「外部キーが完全に PK とオーバーラップしている」ケースで、
      • association = nil したときに、外部キーがきちんと NULL になることを確認するテストを追加。
    • ポリモーフィック belongs_to について、
      • polymorphic_association = nil で *_type と *_id が両方とも正しくクリアされることを確認。
  • has_one_associations_test.rb
    • has_one 側でも同様に、「完全に PK と重なっている FK」や「複合FK + PK オーバーラップ」のパターンをカバーするテストを追加。

  1. 影響範囲・注意点
  • 影響するケース
    • belongs_to / has_one の関連に nil を代入したときの挙動で、
      • 関連先を識別する外部キーが、
        • 参照元モデルの PK にも含まれている、あるいは
        • PK と完全に一致している
      • という構成になっているモデル。
    • ポリモーフィック belongs_to を使っていて、
      • *_type / *_id のどちらか片方だけが残ってしまっていたようなケース。
  • 変わる挙動
    • 「FK が完全に PK と重なっている」場合でも、association = nil によって外部キーが NULL にされるようになります。
      • 以前: 何も更新されず、「nil を代入しても関連が切れない」状態になりうる。
      • 以後: 外部キーがクリアされ、関連が正しく切れる。
    • ポリモーフィック関連では、type と id が必ずセットでクリアされるようになり、片方だけ残る不整合が解消されます。
  • 注意点
    • もし「FK=PK の状態で、nil 代入してもレコード上のカラムは変えたくない」という前提でアプリケーションロジックを書いていた場合は、挙動が変わる可能性があります。
      • そうしたケースは設計上かなり特殊ですが、シャーディングや複合PKを多用するアプリでは、該当するモデルがないか確認した方が安全です。
    • 外部キーをシャードキー兼用にしている場合でも、非PK な別の FK カラムが存在しない完全オーバーラップ構成では、今後は nil 代入によってそのカラムが NULL になる点に留意が必要です。

  1. 参考情報 (あれば)
  • 関連 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. 概要 (1-2文で)
    Rails の bin/rails test:* コマンドが、対応する test/... ディレクトリが存在しない場合に LoadError で落ちていた挙動を修正し、「テスト0件」として正常終了するようにした PR です。特に、rails new 直後のアプリで test:system や test:jobs などを実行すると CI が真っ赤になる問題を解消します。

  1. 変更内容の詳細

何が問題だったか ​

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/system
  • test/jobs
  • test/channels
  • test/mailboxes
  • test/unit
  • test/functional
  • test/lib/generators

これらに対応するコマンド:

  • bin/rails test:system
  • bin/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 側で対処しています。
  • 「無かったことにして引数から落とす」のではなく、「マッチ0件の glob」にする

    • Runner.list_tests は、渡されたパターンのリストが空の場合 default_test_glob (全テスト)にフォールバックします。
    • そのため、単に「フォルダ引数を捨てる」だけにすると、bin/rails test:system(フォルダがない)で「テスト全体」が動いてしまう挙動になります。
    • それを避けるため、「パターンの要素としては残すが、結果的に何もマッチしない形」に変換しています。

動作確認されている例 ​

修正前:

bash
$ bin/rails test:system
cannot load such file -- /tmp/blog/test/system (LoadError)
$ echo $?
1

修正後:

bash
$ 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)

  1. 影響範囲・注意点

影響範囲 ​

  • 影響を受けるのは Rails::Command::TestCommand 経由の「フォルダショートカット」系コマンド です。
    • bin/rails test:system
    • bin/rails test:jobs
    • bin/rails test:channels
    • bin/rails test:mailboxes
    • bin/rails test:units
    • bin/rails test:functionals
    • bin/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 等) に限定されており、
      「任意パスを明示的に指定したときのフィードバック」は維持されている点がバランスです。
  • CI 上で test:system を回しているプロジェクトは、ディレクトリ未作成でも CI がグリーンになる ようになります。
    「システムテストを書いていないことをエラーで気づきたい」場合は、この変更によりそのシグナルが弱まりますが、一般的には「空のテストは成功」という挙動の方が自然と考えられています。

  1. 参考情報 (あれば)
  • 関連 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 にならないことを検証するテストを追加
    • railties/CHANGELOG.md
      • 挙動変更として記載追加

#58471 Add --no-banner to bin/rails console ​

マージ日: 2026/8/17 | 作成者: @Saidbek

  1. 概要 (1–2文で)
    bin/rails console に起動バナー(Railsロゴやバージョン等)を非表示にするための --no-banner オプションが追加されました。これにより、スクリプト実行やCIなどで一時的にバナーを消したいケースを、.irbrc を編集せずにCLIフラグだけで制御できます。

  1. 変更内容の詳細

新オプション --no-banner ​

以下のように実行できます:

bash
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 を設定する処理を追加。
  • railties/test/commands/console_test.rb
    • --no-banner オプションの挙動を保証するテストを追加。
  • railties/CHANGELOG.md
    • 新機能として --no-banner の追加を記載。

  1. 影響範囲・注意点
  • 既存の 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 の行は残る点に注意が必要です。完全無出力が必要な場合は、別途リダイレクトやサプレッションが必要になります。

  1. 参考情報 (あれば)
  • 対応 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. 概要 (1-2文で)
    ActiveModel のバイナリ型 ActiveModel::Type::Binary::Data に対して、as_json を中身の値(文字列)に委譲する実装が追加されました。これにより、バイナリ属性を含むオブジェクトを JSON 化した際に、意味のない ivar マッピングではなく、期待どおり文字列としてシリアライズされるようになります。

  1. 変更内容の詳細

何が問題だったか ​

  • Rails では多くのプリミティブオブジェクトと ActiveModel::Type::Value に #as_json が定義されています。
  • しかし ActiveModel::Type::Binary::Data には独自の as_json がなく、to_json / as_json 時に Object#as_json にフォールバックしていました。
  • Object#as_json は「インスタンス変数名 → 値」のハッシュに変換するため、バイナリ値としては不自然な JSON が生成されます。

例(イメージ):

ruby
record.binary_attr #=> #&lt;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 化する」ように変更されました。

イメージとしては以下のような実装です:

ruby
# activemodel/lib/active_model/type/binary.rb

class ActiveModel::Type::Binary::Data
  # 追加されたメソッド(実際のコードは多少異なる可能性あり)
  def as_json(*)
    value.as_json
  end
end

これにより、Data オブジェクト自体はユーザーから見えない「ラッパー」として扱われ、as_json では中に保持している文字列(またはそれに準ずる値)がそのまま JSON 変換に使われます。

変更後の挙動:

ruby
record.binary_attr #=> #&lt;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 シリアライズ挙動が明示されました。

  1. 影響範囲・注意点

影響範囲 ​

  • 対象: 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 の戻り値を厳密に比較している場合(特にスナップショットテスト)は更新が必要になることがあります。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で) Rails の Relation#update / Relation#update! が関連・スコープを無視してレコードを更新してしまう問題を修正し、関連経由の更新もきちんとスコープが効くようにした PR です。これに伴い、スコープ外の id を指定した場合は更新されず ActiveRecord::RecordNotFound が発生するようになり、一部で挙動が破壊的に変わります。

  1. 変更内容の詳細

何が問題だったか ​

従来の実装:

ruby
# 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
end

Relation#update / Relation#update! が、複数 id 指定の場合にクラスメソッド Model.update に丸投げしており、その中で Model.find が呼ばれていました。Model.find は関連や where などの relation スコープを一切考慮しないため、見かけ上は関連に対して更新しているコードが、実際には「関連外のレコード」も更新しうる状態になっていました。

例:

ruby
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 によって見つかったレコードが更新される

というセキュリティ的にも危険な挙動でした(クロステナント書き込みになりうる)。

テストにも問題が露呈しており、

ruby
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 側に移し、クラスメソッドは単なるデリゲーションに変更しています。

新しい構造(概念的):

ruby
# クラスメソッド側
def self.update(id = :all, attributes)
  all.update(id, attributes)      # Relation に委譲
end

def self.update!(id = :all, attributes)
  all.update!(id, attributes)     # 同様に委譲
end
ruby
# 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 や更新を行うようになりました。

つまり:

ruby
post.comments.update!(comment_id, body: "…")

は内部的に

ruby
post.comments.find(comment_id).update!(body: "…")
# => post.comments による WHERE 条件が必ず付く
#    (post_id = ... など)

という形で実行されるイメージになります。

また、

ruby
Comment.none.update!(id, body: "…")

のような場合は、none の relation で find を行うため、レコードは一切見つからず ActiveRecord::RecordNotFound が発生し、レコードは更新されません。

既存のクラスメソッド挙動との整合性 ​

ActiveRecord::Querying::QUERYING_METHODS には、すでに多くの「id ベース、あるいは複数行更新/削除系」のクラスメソッドが all に委譲する形で揃えられています。

  • delete
  • destroy
  • delete_all
  • destroy_all
  • update_all
  • touch_all
  • insert_all
  • upsert_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(...) } 案が採用されなかった理由 ​

代替案として、

ruby
scoping { model.update(id, attributes) }

のように、Relation の scoping を使ってクラスメソッドを呼ぶ修正も検討されましたが、scoping は current_scope をコールバック / バリデーションにも漏らしてしまうため却下されています。これは Delegation::ClassSpecificRelation#method_missing にある NotImplementedError が警告している通りの「スコープリーク」であり、より安全なため「実装ごと Relation に移動する」方針が採られています。


  1. 影響範囲・注意点

挙動の変化(Breaking change) ​

1. Relation 経由で「スコープ外 id」を更新しようとすると RecordNotFound ​

以前:

ruby
post = Post.first
# 他の Post に属するコメント
other_comment = Comment.where.not(post_id: post.id).first

# これでも other_comment が更新されてしまっていた
post.comments.update(other_comment.id, body: "oops")

新しい挙動:

ruby
post.comments.update(other_comment.id, body: "oops")
# => ActiveRecord::RecordNotFound が発生し、レコードは一切更新されない

none な relation でも同様です:

ruby
Comment.none.update([1, 2], [{ body: "a" }, { body: "b" }])
# 以前: id=1,2 が普通に更新されてしまう
# 現在: RecordNotFound(スコープ上レコードが見つからないため)

もし従来の「スコープを無視して class-level に対して更新する」挙動を意図的に利用していた場合は、明示的にクラスメソッドを呼び出す必要があります:

ruby
Comment.update(id, attributes)             # 全体に対して
Comment.unscoped.update(id, attributes)    # default_scope をも無視

2. Model.unscoped.update の挙動が正しく「unscoped」になる ​

以前:

ruby
Comment.unscoped.update(id, attributes)
# 内部でクラスメソッド Comment.update を呼ぶが、
# そもそも Comment.update が default_scope を適切に外していないケースがあった

今回の変更により、

ruby
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 内に収まっている」正常ケースにおいては、より安全になっただけで、機能的な後方互換性は維持されています。


  1. 参考情報 (あれば)
  • 本 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. 概要 (1-2文で)
    MySQL 用の MySQL::IndexDefinition クラスのコンストラクタが簡素化され、引数まわりの扱いが整理されています。外部からの使い方はほぼ変えずに、内部実装の読みやすさと一貫性を高めるリファクタリングです。

  1. 変更内容の詳細

※このPRは 1 ファイル・+3/-3 行のみの非常に小さな変更で、実質的にはコンストラクタの引数処理のリファクタです。実際のコード差分は公開情報が限られているため、典型的なパターンに基づく説明になります。

想定される変更の方向性としては、以下のようなものです:

  • キーワード引数の扱いの統一

    • 旧: 位置引数 + オプションハッシュ、あるいは *args/**options が混在していた
    • 新: 明示的なキーワード引数に整理、もしくは親クラス IndexDefinition と同じ形に寄せる
  • 不要な引数・分岐の削除

    • 例えば lengths / orders / using といった MySQL 独自オプションの扱いが簡略化され、
      • 親クラスに任せられる部分は親クラスに委譲
      • MySQL 固有部分だけを明示的に扱うように整理

典型的なリファクタ例(イメージ)は以下のようなものです:

ruby
# 変更前(例)
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

あるいは、その逆に:

ruby
# 変更前(例)
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タイトルの通り「コンストラクタのシグネチャ・実装を簡潔にし、余計な引数処理や委譲コードを削る」方向の修正です。


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

    • ActiveRecord::ConnectionAdapters::MySQL::IndexDefinition を直接使用しているコード
      • 通常のアプリケーションではほとんど無関係です(内部API色が強い)。
    • MySQL アダプタ周辺のスキーマ定義・マイグレーション生成ロジック
      • 典型的な add_index / remove_index の利用では挙動変更は発生しないはずです。
  • 注意点

    • もし自作のアダプタ拡張やメタプログラミングで MySQL::IndexDefinition.new(...) を直接呼び出している場合:
      • コンストラクタの引数リスト(位置引数/キーワード引数の構成)が変わっている可能性があります。
      • Rails の内部実装に依存したコードを書いている場合は、このクラスのシグネチャを一度確認してください。
    • 公開API (ActiveRecord::Migration のDSL: add_index, index, t.index 等) を普通に使っているだけなら、互換性の問題は基本的にありません。

  1. 参考情報 (あれば)
  • 関連しそうなコード:
    • activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
      • IndexDefinition の抽象実装。MySQL 用クラスはここを継承・拡張していることが多いです。
  • 類似のリファクタ:
    • 過去にも PostgreSQL 用の IndexDefinition やスキーマ定義クラスで、
      • 「コンストラクタのキーワード引数化」
      • 「親クラスに共通ロジックを寄せる」 といったPRが繰り返し行われているため、その一環と考えられます。

#58097 Join an Array accept in file_field_tag ​

マージ日: 2026/8/16 | 作成者: @55728

  1. 概要 (1-2文で)
  • file_field_tag に accept オプションとして配列を渡したときのレンダリングが、HTML仕様に沿ったカンマ区切り形式になるよう修正されました。
  • これにより、file_field_tag と file_field の挙動が揃い、ブラウザが複数 MIME type を正しく解釈できるようになります。
  1. 変更内容の詳細
  • 対象: ActionView::Helpers::FormTagHelper#file_field_tag

これまで:

ruby
file_field_tag("picture", accept: ["image/png", "image/gif"])
# => <input type="file" name="picture" accept="image/png image/gif">
#                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# 配列がスペース区切りで join されていた

修正後:

ruby
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 に本変更のエントリを追加
  1. 影響範囲・注意点
  • 影響を受けるケース:
    • file_field_tag を使い、accept に Array を渡していたコード
      ruby
      file_field_tag :file, accept: ["image/png", "image/jpeg"]
      これまで事実上、最初の MIME type しか効いていなかったブラウザが、修正後は複数 MIME type を正しく解釈するようになります。
  • 影響を受けないケース:
    • file_field_tag に単一の MIME type を String で渡している場合
      ruby
      file_field_tag :file, accept: "image/png"
    • file_field (モデルバインド版) を使っている場合
      → もともとカンマ区切りで join されていたため挙動は変わらない
  • レイアウト・仕様依存の注意:
    • もしスペース区切りの accept を前提とした、独自のパース処理や JS があれば、カンマ区切りに対応しているかを確認する必要があります(一般的なブラウザはもともとカンマ区切り前提なので通常は問題なし)。
    • HTML仕様・ブラウザの挙動により、修正後のほうが「本来の期待どおりに複数 MIME type が効く」状態になるため、UI 上で選択できるファイルの種類が増えたように見えるケースがあります。
  1. 参考情報 (あれば)
  • 関連コード:
    • ActionView::Helpers::Tags::FileField#render
      ここではすでに accept の Array をカンマ区切りにしており、今回 file_field_tag がそれに追従した形です。
  • HTML仕様:
    • HTML Living Standard: input 要素の accept 属性は「コンマ区切りの型/拡張子のリスト(“Comma-separated list of items”)」と規定されており、本変更はその仕様順守のための修正となります。

#58305 Require active_support/rails in rails/railtie ​

マージ日: 2026/8/16 | 作成者: @55728

  1. 概要 (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 で落ちる問題を修正します。

  1. 変更内容の詳細

問題点 ​

Rails 8.1 で以下のようにするとエラーになります。

sh
$ 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 からの意訳):

ruby
# 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.rb
    • rails/command.rb
      これらも以前のコミット (1db66c0d4e) で active_support/rails を require するようになっている
  • 今回の変更により、rails/railtie も他のエントリポイントと整合した挙動になる

  1. 影響範囲・注意点

影響するケース ​

  • Rails 本体より先に Railtie をロードする gem・アプリケーション:

    ruby
    # 例: gem の railtie ファイル内で
    require "rails/railtie"
    
    module MyGem
      class Railtie < ::Rails::Railtie
        # ...
      end
    end
  • 実際に報告されている対象:

    • terser/railtie
    • inline_svg/railtie
    • simple_form/railtie
    • scenic/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 依存を意識しておくとよいです。

  1. 参考情報 (あれば)
  • 該当 PR: Require active_support/rails in rails/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. 概要 (1-2文で)
    connection.columns が複数テーブルを一度に取得できるようになり、スキーマキャッシュのダンプ時に発行される SQL 文数と実行時間が大幅に削減されました。MySQL/MariaDB/PostgreSQL 向けにカタログからの一括取得ロジックが追加され、MariaDB のデフォルト値扱いも精緻化されています。

  1. 変更内容の詳細

2-1. columns が複数テーブル対応に ​

これまで:

ruby
connection.columns(:users)  # => [Column, ...]
# テーブルごとに個別クエリ

これから:

ruby
connection.columns(:users)            # 既存と同じ => [Column, ...]
connection.columns([:users, :posts])  # 新形式 => { "users" => [...], "posts" => [...] }
  • 単一テーブル指定時は従来どおり Array&lt;Column> を返却。
  • 複数テーブル指定時は "テーブル名" => Array&lt;Column> の Hash を返却。
  • 内部的に「スキーマ情報をまとめて読むためのインターフェイス (WHOLE_SCHEMA_READERS)」の一員として扱われるようになり、スキーマキャッシュダンプで一括読みが利用される。

2-2. スキーマキャッシュダンプの効率化 ​

250 テーブルでの測定:

statementstime
before255124.6 ms
after644.0 ms

「6ステートメント」に含まれるもの:

  1. テーブル一覧
  2. プライマリキー情報の一括取得
  3. カラム情報の一括取得(今回の変更の主対象)
  4. インデックス情報の一括取得
  5. schema_migrations の存在確認
  6. 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 (一括クエリ)

PostgreSQL ​

  • 既存の pg_attribute を読むクエリを拡張し、「単一テーブル」→「複数テーブルのリスト」に対応。
  • パラメータとして複数テーブルを与え、IN 句などでフィルタして一括取得する構成。

SQLite ​

  • SQLite 自体はテーブル単位の読み出ししか提供しないので、既存の「テーブル単位実装」をそのまま利用。
  • ただし、インターフェイスとしては複数テーブル引数に応答できるようにし、各テーブルを順番に呼び出して Hash にまとめる形。

2-4. MariaDB のデフォルト値扱いの改善 ​

MariaDB の information_schema.columns では、デフォルト値が「SQL リテラルとして宣言された文字列」の形で保存されています。SHOW FULL FIELDS の結果とは以下のような差があります:

text
宣言: '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.462176
MariaDB 11.8.8460
MySQL 5.7.4431110
MySQL 5.6.5131108
PostgreSQL 172173

検証には、通常のスキーマでは出ないようなケースも含まれています(式デフォルト、関数っぽい文字列、ON UPDATE CURRENT_TIMESTAMP、STORED/VIRTUAL 生成カラム、各種エスケープ・クォート・バックスラッシュ・NUL・Ctrl-Z・空文字、bit/enum/コメント/文字セット違い/明示的コレーションなど)。

2-6. スキーマ修飾名 (schema-qualified names) ​

  • public.things / other.things のように同名テーブルが複数スキーマに存在するケースでも、呼び出し側が渡した識別子をそのままキーにして結果を返します。

例:

ruby
connection.columns(["public.things", "other.things"])
# => {
#  "public.things" => [#&lt;Column name="id">, #&lt;Column name="pub_col">],
#  "other.things"  => [#&lt;Column name="id">, #&lt;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 マトリクスに入れているプロジェクトでは再確認推奨。

  1. 影響範囲・注意点
  • パブリックAPIの挙動変化:
    • connection.columns が「引数の型によって戻り値の型が変わる」メソッドになりました。
      • シンボル/文字列 1つ → Array&lt;Column>
      • 配列(複数テーブル) → Hash&lt;String, Array&lt;Column>>
    • ライブラリ/アプリ側で columns を直接呼んでいる場合、今後「配列を渡すと Hash が返る」ことを前提にしたコードを書ける一方で、既存コードが配列前提で「複数テーブル名を渡していた」場合はバグになりうるため要確認。
  • パフォーマンス面の影響:
    • スキーマキャッシュダンプや schema_cache.yml の生成など、「全テーブルのメタデータを読む処理」で劇的にクエリ数が減るため、大規模スキーマ(数百テーブル)環境ほど恩恵が大きいです。
    • MySQL 5.6/5.7 のような旧バージョンでも、一括クエリの方が速いことが確認されているため、特定バージョンでのパフォーマンス悪化は想定されていません。
  • MariaDB 特有の互換性:
    • カタログ経由によるデフォルト値の扱いが改善されていますが、基本的には「より正しい情報に近づく方向」であり、互換性破壊ではなくバグ修正寄りの変更です。
    • もし MariaDB + TEXT/BLOB デフォルト値を多用しているアプリがある場合は、スキーマダンプ結果や Column#default の値が従来と同じかを一度確認すると安心です(挙動は維持されている想定)。
  • SQLite は実質的に挙動維持:
    • API レベルで複数テーブルに対応しただけで、内部的には各テーブルを順に読むだけなので、SQLite 環境では性能差はほぼありません。

  1. 参考情報 (あれば)
  • 関連 PR:
    • #58421, #58488 — 同様に「スキーマ情報をまとめて読む」ための前段階の変更。
  • 実装箇所:
    • activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
    • activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
    • activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
    • activerecord/lib/active_record/connection_adapters/schema_cache.rb
    • activerecord/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. 概要 (1-2文で)
    Active Record Encryption ガイドの「3.7 Fixtures」節における英文の文法ミスを修正したドキュメント専用の PR です。コードや挙動には一切変更がなく、文書の可読性・正確性のみが向上しています。

  2. 変更内容の詳細
    対象ファイル: guides/source/active_record_encryption.md

「3.7 Fixtures」章の冒頭の英文が、以下のように修正されています。

diff
- 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 はそのまま英語で読まれることも多く、ガイドを参照しながら実装・学習する開発者にとって、文法的に自然な英語になるように整備した変更です。

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

    • Rails のフレームワーク本体や Active Record Encryption の挙動には一切影響しません。
    • CI を回さないため [ci skip] が付与された、ドキュメント専用の変更です。
    • Active Record Encryption の Fixtures に関する説明の英語が正しくなり、ガイドの可読性・理解しやすさがわずかに改善されます。
  • 注意点

    • アプリケーションコードや設定を変更する必要はありません。
    • ガイドのリンクや章構成などには変更がないため、既存のドキュメント参照(URL や節番号)もそのまま有効です。
  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    ActiveRecord::Relation のドキュメントで誤っていた戻り値の説明とコード例が修正されました。touch_all の戻り値と create のサンプルコードが、実際の挙動に沿うように整合性を取った変更です。

  1. 変更内容の詳細

touch_all のドキュメント修正 ​

  • 問題点
    Relation#touch_all は内部的に update_all を呼び出しており、実際には「更新された行数 (Integer)」を返します。
    しかし、これまでのドキュメント例では、touch_all が UPDATE文の SQL 文字列を返すかのような例になっていました。

  • 修正内容

    • ドキュメント内の touch_all の使用例 4つすべてについて、
      • 「戻り値として UPDATE 文の文字列」が出てくるような例
        をやめ、
      • 更新件数(整数)を返すことが分かるような例
        に修正。
    • これに合わせて update_all まわりのテスト (activerecord/test/cases/relation/update_all_test.rb) も、ドキュメントに沿う形で微修正されました(コメントや期待値の表現調整レベルと思われる)。

※具体的なコードは PR 本文からの抜粋がないため概略のみですが、たとえば:

ruby
# 以前(イメージ)
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 の戻り値(新しく作成されたレコード)をどう扱うか」という主旨が明確になりました。

  1. 影響範囲・注意点
  • ランタイム挙動への影響はなし
    変更はドキュメントとドキュメント整合用のテストのみであり、Relation#touch_all や Relation#create の実装・挙動自体は変わっていません。
  • 既存コードの修正は不要
    • すでに touch_all の戻り値を「更新件数 (Integer)」として扱っているコードは、従来どおりそのまま動作します。
    • create に関しても、ドキュメントのサンプルが直っただけなので、本番コードへの影響はありません。
  • 注意点
    • これまでドキュメントの例を参考に「touch_all は SQL 文字列を返す」と誤解していた場合、実際は整数が返る点を改めて確認してください。
    • スコープ付きの Relation#create を引数なしで呼ぶと、スコープ条件が属性として使われる(例: where(name: "foo").create → name: "foo" のレコードができる)という挙動を、改めて意識しておくとよいです。

  1. 参考情報 (あれば)
  • 対象クラス: ActiveRecord::Relation
    • メソッド: #touch_all, #update_all, #create
  • 変更ファイル:
    • activerecord/lib/active_record/relation.rb
    • activerecord/test/cases/relation/update_all_test.rb
  • 関連ドキュメント(英語・最新版):

#58488 Read the schema cache's primary keys and indexes for many tables at once ​

マージ日: 2026/8/15 | 作成者: @ngan

  1. 概要 (1-2文で)
    SchemaCache#add_all が、テーブルごとに個別に問い合わせていた「主キー」と「インデックス」の読み込みを、複数テーブルをまとめて取得する形に変更し、スキーマキャッシュのダンプ時の SQL ステートメント数を大幅に削減する変更です。キャッシュされる内容や順序は従来と完全に同一で、パフォーマンス改善のみが目的です。

  1. 変更内容の詳細

何をしているか ​

  • 対象: ActiveRecord::ConnectionAdapters::SchemaCache#add_all

  • 以前の挙動:

    • add_all(tables) の内部で、各テーブルごとに add(table) を呼び出していた
    • add(table) はそのテーブルの
      • 主キー (primary_key)
      • カラム情報 (columns)
      • インデックス (indexes) をそれぞれ個別のクエリで取得
    • 結果として、「テーブル数 × 3種類(PK / columns / indexes)」分のクエリが発行されていた
  • 今回の変更後:

    • add_all 内で、まず「テーブル一覧」を引数に
      • すべてのテーブルの主キー
      • すべてのテーブルのインデックス を“まとめて”問い合わせる
    • その結果をキャッシュに格納したあとで、各テーブルについては「カラム情報」だけを個別に読む
    • つまり、主キー・インデックスについては「1回でまとめて読む」ようになった

擬似コードイメージ:

ruby
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&lt;String>
  • これは、もともと SchemaStatements#primary_key が返していた形と一致しており、既存の add の挙動と整合するようにしている
  • この両パターン(単一 PK / 複合 PK)はテストでカバーされている

バッチ取得の前提 ​

  • 事前にマージされている PR (#58421) により、
    • スキーマリーダー側(アダプタ)は「テーブルのリスト」を渡して主キー・インデックスをバッチ取得できるようになっている
  • この PR はそれを SchemaCache 側で利用する follow-up で、「キャッシュ構築時にバッチ API を使う」ようにするもの

  1. 影響範囲・注意点

パフォーマンスへの影響 ​

  • 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 パスでも既にアルファベット順に返していたことを確認済みであり、
      • 新旧で順序が変わらないことを確認済み

呼び出し側との関係 ​

  • SchemaCache#add_all の呼び出し元は SchemaReflection#dump_to のみ
    • dump_to は「空の SchemaCache」に対して実行される
    • そのため、add_all 内で「常に」主キー・インデックスを事前フェッチしても安全(既存キャッシュの上書き等の問題はない)
  • SchemaCache#add 自体は変更されておらず、テーブル単位でのメモ化も従来通り
    • 1 テーブルを繰り返し読むケースでは、これまで通り追加コストは発生しない

エッジケース: 空のデータベース ​

  • テーブルが 0 件のケース:
    • クエリは発行されず、エラーにもならない
    • これは #58421 の test_an_empty_list_reads_nothing によって、リーダー側の挙動がテストで保証されている

  1. 参考情報 (あれば)
  • 関連 PR:
    • #58421: スキーマリーダー(アダプタ側)に「テーブルリストを渡して主キー・インデックスをまとめて取得」する能力を追加
  • 変更ファイル:
    • activerecord/lib/active_record/connection_adapters/schema_cache.rb
      • add_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. 概要 (1-2文で)
    CollectionProxy#delete_all のドキュメントが、実際の挙動(削除件数を返す)と食い違っていたのを修正する PR です。特に :nullify 戦略のサンプルが「レコード配列を返す」かのように書かれていた点を、正しい戻り値の説明に合わせました。

  1. 変更内容の詳細
  • 対象メソッド: ActiveRecord::Associations::CollectionProxy#delete_all

  • 実際の挙動:

    • delete_all は「削除されたレコード数(Integer)」を返す。
    • これは :nullify 戦略を含む全ての削除戦略で同じ。
  • 問題だった点:

    • ドキュメント中の :nullify のサンプルが以下のように、「削除されたレコードの配列を返す」かのような例になっていた:

      ruby
      person.pets.delete_all
      # => [
      #       #&lt;Pet id: 1, name: "Fancy-Fancy", person_id: 1>,
      #       #&lt;Pet id: 2, name: "Spook", person_id: 1>,
      #       #&lt;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 だけがレコード配列ではなく削除件数を返す仕様。
    • 古いサンプルがこれを誤解させる形になっていた、というのが今回の修正対象。

  1. 影響範囲・注意点
  • 実装コードは変わっておらず、挙動そのものは従来通りで、ドキュメントのみの修正です。
  • 影響範囲:
    • CollectionProxy#delete_all を利用するアプリケーションコード自体への影響はなし。
    • ただし、ドキュメントを信じて「配列が返る」と誤解していた場合は、仕様理解を修正する必要があります。
  • 注意点:
    • collection.delete / collection.destroy は「削除されたレコードの配列」を返すのに対し、
      collection.delete_all は「削除件数 (Integer)」のみを返す点を区別すること。
    • 特に :nullify 戦略使用時でも戻り値は件数であり、関連先レコードのオブジェクトは返ってこないため、削除対象を追跡したい場合は事前に to_a などで取得しておく必要があります。

  1. 参考情報 (あれば)
  • 対応 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. 概要 (1–2文で)
    Rails の Active Record と Action Pack の一部設定値を、アプリケーション初期化後に freeze(必要な Ruby では make_shareable)することで、Ractor 間で安全に参照できるようにした PR です。これにより、初期化後に特定の設定を動的に書き換えることは基本的にできなくなります。

  1. 変更内容の詳細

2-1. 対象となる設定の「凍結」 ​

Action Pack (ActionDispatch) と Active Record の以下の設定が、after_initialize のタイミングで freeze されるよう Railtie が変更されています。

  • Action Pack / ActionDispatch

    • ActionDispatch::ExceptionWrapper.rescue_responses
    • ActionDispatch::ExceptionWrapper.rescue_templates
    • ActionDispatch::ExceptionWrapper.wrapper_exceptions
    • ActionDispatch::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 側):

ruby
# 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
end

Active 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)になっていること」を検証しています。


  1. 影響範囲・注意点

3-1. 初期化後の動的変更が基本的にできなくなる ​

これまで以下のようなコードで「起動後に例外マッピングをいじる」「query_transformers を後から追加する」といったことをしていた場合、FrozenError が発生する可能性があります。

例: 以前はできていたかもしれないコード

ruby
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 側で以下のようなことをしている場合も注意が必要です。

ruby
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 の挙動が変わる」といったレースコンディションを防止できる
  • 設定オブジェクトが不変であることが前提になるため、デバッグや推論がしやすくなる

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Action View のログ購読クラス(LogSubscriber / StructuredEventSubscriber)で Rails.root の扱いを Ractor セーフにするため、Rails.root を「遅延メモ化」ではなく「初期化時に確定」する形に変更した PRです。これにより、マルチ Ractor 環境でも Action View のログ・イベント購読が安全に動作することを意図しています。

  1. 変更内容の詳細

背景 ​

  • これまで ActionView::StructuredEventSubscriber や ActionView::LogSubscriber では、Rails.root を必要になったタイミングでメモ化(@root ||= Rails.root のような形)して使っていたと考えられます。
  • Ractor 環境では、メイン Ractor で生成・変更されるオブジェクトを、後から別 Ractor で「初回代入」したり「再代入」したりするようなパターンは安全でない場合があります。
  • そこで、「アプリケーション全体で Ractor セーフ / frozen にされる想定の Rails.root を、メイン Ractor の初期化時にインスタンス変数へ確定代入しておく」ように修正しています。

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

1) ActionView::LogSubscriber の root 取り扱い ​

変更前(イメージ):

ruby
class ActionView::LogSubscriber < ActiveSupport::LogSubscriber
  # ...
  private

  def root
    @root ||= Rails.root
  end
end

変更後(イメージ):

ruby
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 を初期化時にインスタンス変数へ代入する形に変更されています。

変更前(イメージ):

ruby
def root
  @root ||= Rails.root
end

変更後(イメージ):

ruby
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 セーフであること、及び変わらず正しいログ/イベント出力になることの確認
  • 行数としては減っており、テスト構造が整理・簡潔化されている可能性も高いです。

  1. 影響範囲・注意点

影響範囲 ​

  • 影響を受けるのは主に以下です:
    • ActionView::LogSubscriber によるログ出力(テンプレートレンダリングのログなど)
    • ActionView::StructuredEventSubscriber による構造化イベントの購読・出力
  • それらが参照している Rails.root が:
    • 以前: 初回アクセス時に Rails.root を取得していた
    • 現在: subscriber インスタンス生成時に取得される
      という点が変わります。

何が変わるか / 開発者としての注意点 ​

  1. Rails.root の「確定タイミング」が早まる

    • サブスクライバが生成された時点の Rails.root が使われ続けます。
    • 通常 Rails アプリでは Rails.root は起動後に変化しないため、実務上は問題にならないケースがほとんどです。
    • 「ランタイムに Rails.root を差し替える」といった特殊なことをしている場合は挙動が変わる可能性があります(そのような使い方自体が非典型ですが)。
  2. Ractor 環境での安全性向上

    • アプリケーション側で Rails.root を frozen / Ractor セーフなオブジェクトとして扱うことが前提になっています。
    • Rails の初期化処理(initializer)はメイン Ractor 上で実行されるため、この段階で Rails.root が決定・凍結されていれば、後続の Ractor から読まれるだけ、という設計になります。
    • Ractor を使った並列実行(Action Cable の一部実装や、将来のマルチ Ractor Web サーバ実装など)で、Action View のログ/イベントまわりが安全に動くようになります。
  3. ライブラリ・拡張側の考慮

    • ActionView::LogSubscriber や StructuredEventSubscriber を継承・拡張している独自クラスや gem が、Rails.root を遅延メモ化に依存していた場合、挙動が変わり得ます。
    • ただし、そのような依存(「後で root が切り替わること」を前提にしているなど)は設計として想定されていないため、一般的なアプリ/ライブラリには実害は少ないはずです。

  1. 参考情報 (あれば)
  • 類似 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. 概要 (1-2文で)
    Controller / View で使われる default_url_options を「破壊的に変更できないように凍結(freeze)する」変更です。
    暗黙のミューテーションによる予測しづらいリダイレクト/URL生成の挙動を防ぎ、将来的な Ractor 対応も見据えた安全性向上が目的です。

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

対象となる 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 メソッドをオーバーライドすることが推奨されていますが、実装的には次のようなコードも「動いてしまう」状態でした。

ruby
class MyController < ApplicationController
  default_url_options[:lang] = "en"
end

こうした「ハッシュのミューテーション」は公式にはドキュメント化されておらず、かつ以下のような「超わかりづらい挙動」を引き起こします。

ruby
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 されるように変更されました。
  • これにより、次のようなコードは(実行時に)エラーになります。
ruby
class MyController < ApplicationController
  default_url_options[:lang] = "en" # => FrozenError (can't modify frozen Hash)
end
  • 正しいカスタマイズ方法は従来通り「メソッドオーバーライド」です。
ruby
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 ヘルパのデフォルトオプションの扱い方が「ミューテーションを前提にしない形」に整えられています。

  1. 影響範囲・注意点

影響があるケース ​

以下のように default_url_options を「ハッシュとして」直接いじっているアプリケーションは、今回の変更で壊れます。

ruby
# 悪い例(今回の 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 を起こすようになるので、メソッドオーバーライドに書き換える必要があります。

ruby
# 良い例
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 対応に向けた足場づくりの一部という位置づけです。

  1. 参考情報 (あれば)

#58482 Restore unshareable_proc_action with ensure in default scoping test ​

マージ日: 2026/8/14 | 作成者: @ngan

  1. 概要 (1-2文で)
    DefaultScopingTest#test_default_scopes_are_ractor_shareable 内でグローバル設定 ActiveSupport::Ractors.unshareable_proc_action を一時的に変更する際、例外発生時にも必ず元に戻るように、ActiveSupport::Ractors.with(内部で ensure を使う仕組み)を使うように修正したテスト専用の変更です。これにより、テスト失敗時に Ractors 周りの設定リークが起きて後続テストが巻き添えになる問題を防ぎます。

  1. 変更内容の詳細

これまでの問題点 ​

元のテストでは、Ractors 周りの挙動を確認するためにグローバル設定を一時的に変更していました:

ruby
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 を使って値を必ず元に戻すラッパーです。

概念的には以下のような形になっています(実際のコードは多少異なりますがイメージとして):

ruby
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_sql and 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.rb
  • actionpack/test/controller/caching_test.rb
  • activesupport/test/callbacks_test.rb
  • activesupport/test/ractors_test.rb
  • actionpack/test/controller/new_base/middleware_test.rb

今回の変更で、Ractors 関連のテスト全体のスタイル・安全性が揃えられました。


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

    • 変更は activerecord/test/cases/scoping/default_scoping_test.rb のみで、本番コードには一切影響しません。
    • テスト実行時の Ractors 設定の扱いがより安全になり、
      • 「一つのテスト失敗が原因で、後続の多数のテストが Ractors 設定リークにより巻き添えで失敗する」
        といった現象が起きにくくなります。
    • テストの安定性・デバッグ容易性が向上します。
  • 注意点

    • Ractors 関連に限らず、「グローバル設定をテスト内で一時的に変更する」場合は、
      • ensure での復元
      • あるいは今回のような with ヘルパー
        を使うべき、という Rails テストコードの実質的なガイドラインが強化された形です。
    • 同様のパターンが他に残っている場合は、同じように with / ensure に寄せるとよいです。

  1. 参考情報 (あれば)
  • 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.rb
    • actionpack/test/controller/caching_test.rb
    • activesupport/test/callbacks_test.rb
    • activesupport/test/ractors_test.rb
    • actionpack/test/controller/new_base/middleware_test.rb

#58421 Let the schema readers answer for many tables at once ​

マージ日: 2026/8/14 | 作成者: @ngan

  1. 概要 (1-2文で)
    このPRは、スキーマダンプ時にテーブルごとにバラバラに発行していたメタデータ系クエリ(インデックス・外部キー・制約など)を「複数テーブルまとめて」取得できるようにし、大規模スキーマでの db:schema:dump / db:migrate の時間とクエリ数を大幅に削減する変更です。出力されるスキーマファイルの内容は(バグ修正を除き)変わらず、内部の問い合わせ方法とAPIシグネチャが拡張されています。

  1. 変更内容の詳細

2-1. 複数テーブル版リーダーAPIの追加 ​

以下のメソッドが「単一テーブル」だけでなく「テーブル配列」も受け取れるようになりました。

  • indexes
  • primary_keys
  • foreign_keys
  • check_constraints
  • exclusion_constraints
  • unique_constraints

挙動:

ruby
# 既存の単一テーブル形式(今まで通り)
connection.indexes(:users)
# => [ActiveRecord::ConnectionAdapters::IndexDefinition, ...]

# 新しい複数テーブル形式
connection.indexes([:users, :posts])
# => {
#      "users" => [IndexDefinition, ...],
#      "posts" => [IndexDefinition, ...],
#    }

ポイント:

  • 引数が配列の場合: 戻り値は "テーブル名文字列" => 定義の配列 のハッシュ。
  • 引数が単一の場合: 既存と同じく、そのテーブルの配列だけを返す。
  • 「すべてのテーブル」という省略形は用意せず、「呼び出し側が明示的にテーブル一覧を渡す」という設計。
  • 各メソッドは内部的には「配列対応の実装」に一本化されており、単一テーブル版はそれを薄くラップしているだけ。

サンプル実装イメージ:

ruby
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 から複数テーブルをまとめて読むようになっています。

可用なカラムを環境ごとに切り替える実装:

ruby
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)取得のバグ修正 ​

従来:

sql
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_metadata
    • SchemaDumper.ignore_tables に含まれるもの を除外した「実際に出力するテーブル」だけをまとめて問い合わせるように。

2-6. 返り値の仕様 ​

  • どのテーブルにも必ずキーが存在します(インデックス等が1つもなくても、空配列を返す)。
    • 呼び出し側は「存在しないキー(nil)」を気にせずに繰り返し処理できる。
  • 空配列を渡した場合:
    • 「何もしない」ことが仕様です。
    • データベース問い合わせも発生しません。
    • テーブルが0件のデータベースをダンプするパスでも無駄なクエリが走らないように配慮。

2-7. 挙動変更(互換性に関わる部分) ​

  1. 空のテーブル名の扱い

    • 以前は、MySQL の foreign_keys / primary_keys だけが空文字を弾いて ArgumentError を投げていた一方で、
      • indexes(nil) は nil を返す
      • PostgreSQL / SQLite はどのリーダーでも nil を返す
        という不統一な状態でした。
    • PR後:
      • すべてのリーダー・アダプタで挙動が統一され、「空名で特別に ArgumentError を投げる」という挙動は廃止。
      • 空配列を許容して「何もしない」仕様を優先したため、present? ベースのバリデーションは導入していません。
  2. PostgreSQL の primary_keys が、存在しないテーブルで例外を投げなくなる

    従来:

    sql
    'table_name'::regclass

    を使っていたため、存在しないテーブル名に対して ActiveRecord::StatementInvalid が発生。

    PR後:

    • pg_class を直接フィルタする方式に変更。
    • 存在しないテーブルでも [] を返すようになります。

    他のメソッド(indexes, foreign_keys)は元から「存在しないテーブル → []」だったので、primary_keys もそれに揃えた形です。テストでも「例外を期待する」前提は存在していませんでした。


  1. 影響範囲・注意点

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) 等) はシグネチャも挙動も変わりません。
  • スキーマダンプの出力も、バグ修正を除いてバイトレベルで同一になるよう確認済みです。

注意すべき変更点:

  1. PostgreSQL + primary_keys で、存在しないテーブルに対する挙動が変わる

    • 以前: ActiveRecord::StatementInvalid を救って何かする、というコードがあれば、それはもう呼ばれません。
    • 今後: [] が返ります。
    • ライブラリやアプリ側で「存在しないテーブルを検出する」ロジックに依存している場合は注意。
  2. 空のテーブル名引数に関するバリデーション削除

    • 空文字(あるいは nil)を渡して例外を期待していたテストがあれば失敗する可能性があります。
    • そのような呼び出しは一般的にはバグなので、アプリ側で明示的にバリデーションするほうが望ましいです。
  3. 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) を満たす必要があります。

  1. 参考情報
  • 変更ファイル例
    • activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
    • activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
    • activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
    • activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
    • activerecord/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. 概要 (1–2文で)
  • Rails に HTTP の新メソッド QUERY(RFC 10008)が正式サポートとして追加され、routes・request オブジェクト・統合テストなど Action Pack 全体で GET 等と同様に扱えるようになりました。
  • QUERY は「安全かつ冪等な、ボディ付きの GET 相当メソッド」として扱われ、複雑・大容量な検索条件を URL クエリ文字列ではなくリクエストボディで送る用途を想定しています。

  1. 変更内容の詳細

2-1. QUERY メソッドの追加と基本挙動 ​

HTTP メソッドとしての登録 ​

  • ActionDispatch::Request に QUERY が既知メソッドとして追加
    • HTTP_METHODS に RFC10008 ベースの定義を追加
    • request.query? が新設
    • request.request_method は "QUERY" を返し、request.request_method_symbol は :query を返すようになる

例:

ruby
# コントローラ内
def index
  request.query?                # => true (QUERY のとき)
  request.request_method        # => "QUERY"
  request.request_method_symbol # => :query
end

2-2. ルーティング (config/routes.rb) ​

  • Journey の VerbMatchers::VERBS に QUERY を追加
  • ActionDispatch::Routing::Mapper に query ヘルパが追加

これにより GET 等と同じ感覚でルートを定義できます。

例:

ruby
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]
end

Rails 7 以前では via: :query を書いてもマッチせず 405 相当になっていましたが、この PR により正常にマッチするようになります。

2-3. 統合テスト・コントローラテスト ​

統合テスト (ActionDispatch::IntegrationTest) ​

  • get/post などと同様に query ヘルパが追加されました。

例:

ruby
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
end
  • as: :json などの扱いは他メソッドと同様で、Content-Type ベースにボディが params にパースされます。

コントローラテスト (ActionController::TestCase) ​

  • options と同様、専用のショートカットメソッドは追加されていませんが、process がそのまま利用できます。
ruby
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 に正しく展開されます。

例:

ruby
# QUERY /search
# Content-Type: application/json
# Body: { "filters": { "status": "active" } }

def index
  params[:filters][:status] # => "active"
end

2-6. SSL ミドルウェア / ナビゲーションヘルパ等 ​

  • actionpack/lib/action_dispatch/middleware/ssl.rb
    • HTTP->HTTPS へのリダイレクトや安全性チェックの中で QUERY を GET/HEAD などと並べて正しく扱うように更新
  • ActionView::Helpers::NavigationHelper
    • current_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? を定義)

  1. 影響範囲・注意点

3-1. 既存アプリへの影響 ​

  • 後方互換性はほぼ問題無し とされています。
    • これまで QUERY リクエストは ActionController::UnknownHttpMethod を投げて 405 相当で落ちていたため、「すでに QUERY を何か特別にハンドリングしていたコード」は基本的に存在しない想定です。
    • この PR によって初めてアプリケーションコードに到達するようになるため、「何かのプロキシや外部クライアントが勝手に 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 を追加する 等、サーバ側の設定が別途必要です。
  • nginx / Node.js / Spring など、他エコシステムではすでにサポートが進みつつあることが PR で触れられていますが、Rails 単体ではサーバの制約を越えられない点に注意してください。

  1. 参考情報

この PR により、Rails で「GET 相当だがクエリをボディで送る」API デザイン(複雑な検索条件、GraphQL/JSON-RPC の読み取り等)が公式にサポートされるようになりました。


#58469 Remove "missing" workarounds from Active Record ​

マージ日: 2026/8/14 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    Active Record の method_missing ベースの「missing 系ワークアラウンド」が削除され、6.1 時代の Marshal 互換のためだけに残っていたコードが整理されました。これにより、仮想属性や respond_to? 周りのパフォーマンスが向上しつつ、新形式での Marshal.load 互換性はテストで担保されています。

  1. 変更内容の詳細

背景 ​

  • 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)
    ruby
    ActiveRecord::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").first
    • row.title … 通常の属性メソッド
    • extra.tenderlove … SELECT で追加した「仮想カラム」(10 as tenderlove)

計測ケース:

ruby
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
      → 誤差範囲内で同等
  • tenderlove(仮想属性アクセス)
    • 2.18M i/s vs 1.30M i/s
      → 約 1.67 倍高速
  • rt title (respond_to?(:title))
    • 8.83M vs 8.83M i/s
      → 同等
  • rt to_hash (respond_to?(:to_hash))
    • 2.88M vs 2.49M i/s
      → ~1.16x 高速
  • rt permitted? (? suffix でパターン全スキャン)
    • 1.44M vs 1.36M i/s
      → ~1.06x 高速
  • rt tenderlove (respond_to?(:tenderlove))
    • 4.38M vs 3.55M i/s
      → ~1.23x 高速

まとめると:

  • 通常の属性メソッド (title) はほぼ変化なし
  • 仮想属性アクセス (tenderlove) と respond_to? 系が目に見えて高速化
  • 特に、存在しないメソッド/動的属性を respond_to? で問い合わせるケースが軽くなっている

これは、method_missing ベースのワークアラウンド除去により、respond_to?/respond_to_missing? がシンプルになった効果と考えられます。


  1. 影響範囲・注意点
  1. Rails 6.1 形式の Marshal との互換性を前提にしているコードはもはや考慮されない
  • すでに 6.1 Marshal フォーマット自体が削除されているため、この PR でさらに「6.1 互換の最後の保険」的なコードがなくなった形です。
  • 6.1 時代に serialize した Active Record オブジェクトを「そのまま」今の Rails で復元するような特殊なケースを想定しているライブラリがあれば、注意が必要です(ただしそのサポートは既に他の PR で切られている)。
  1. method_missing に頼った「隠れた動作」が消える
  • 以前は、属性メソッドがクラスに定義されていなくても、
    • Marshal 復元など特定条件下で record.some_attribute が「なんとなく」動いていた可能性があります。
  • この PR 以降は、属性メソッドが定義されていない場合は原則として NoMethodError になります。
  • 独自に method_missing をオーバーライドしているモデルや、メタプログラミングで AR の method_missing に依存しているコードがある場合は、挙動確認を推奨します。
  1. respond_to? の戻り値がより厳密になる可能性
  • ワークアラウンドがなくなったことで、
    • 「実在しないが以前は true が返っていた」ようなケースで false になる可能性があります。
  • respond_to? を使ってメソッド存在チェックを行い、その結果に依存して条件分岐しているコードがあれば、挙動が変わる可能性があります。
    • 特に仮想属性や動的属性(select "..., x as tenderlove" など)周り
  1. 性能面の影響
  • 利点:
    • 仮想属性および respond_to? の一般的な呼び出しが高速になる
    • メソッド探索の経路がシンプルになり、Ruby 側の最適化(YJIT 等)にも好影響が見込まれる
  • 懸念点:
    • 特定のワークアラウンドに依存したレアケース以外はデグレはほぼ無い想定だが、大規模アプリでは respond_to? を多用している部分の挙動を軽く検証しておくと安心

  1. 参考情報 (あれば)
  • 関連 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. 概要 (1-2文で)
    このPRは、ActiveSupport::Callbacks を Ractor セーフにするための対応です。具体的には、コールバックチェーンやユーザー定義の Proc を Ractor 共有可能にし、Ractor 環境下でも安全にコールバックが動作するようにしています。

  1. 変更内容の詳細

コアとなる変更点 ​

2-1. ユーザー定義 Proc を Ractor-shareable にする ​

  • コールバックとして渡される Proc / lambda / block について、設定に応じて Ractor-shareable(共有可能)な形にする処理が追加されています。
  • これは、Ruby の Ractor が「shareable でないオブジェクトのクロス-Ractor共有」を禁止しているためで、
    • 共有したい Proc は「分離(isolate)」して shareable にする
    • 共有不要な場合は従来通り
      という挙動を切り替えられるようにしています。
  • 「shareable 化するかどうか」のスイッチは、別PR(#57626)で導入される ActiveSupport::Ractors 側の設定を利用しています。

イメージコード(※概念的な例):

ruby
# どこかで 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 対応による副作用(特にコールバックを用いたバリデーション)が問題ないことをチェックしています。

  1. 影響範囲・注意点

3-1. 主に影響を受ける層 ​

  • ActiveSupport::Callbacks を直接利用しているコード
    • ActiveRecord、ActiveModel::Validations、その他独自の callback ベース DSL
  • Ractor を利用している/これから利用しようとしている Rails アプリケーション

3-2. 実務的な影響ポイント ​

  1. Ractor を使わないアプリ

    • 従来とほぼ互換の挙動で、基本的には「何もしなくても」今まで通り動きます。
    • set_callbacks が破壊的変更をやめて「新チェーンを作る」方式になっているため、
      極端にコールバックの内部オブジェクト同一性に依存しているようなメタプログラムがあると影響を受ける可能性はありますが、一般的な使い方では問題になりにくいです。
  2. Ractor を使う/使いたいアプリ

    • コールバックに渡す Proc / lambda / block の中で、
      • 非 shareable な状態(例: IO オブジェクト、ミューテーブルなグローバル変数、スレッドローカルなど)を直接キャプチャしている場合は注意が必要です。
    • shareable 化の処理は ActiveSupport::Ractors の設定に依存するため、
      • Ractor を使う場合はこの設定を有効にする
      • 有効にした場合、shareable にならないオブジェクトをキャプチャしているとエラーになる
        可能性があります。
  3. マルチプロセス/マルチスレッドとの違い

    • Ractor はスレッドよりも強い隔離を要求するため、「スレッドでは動いていたコールバック」が、そのままだと「Ractor では shareable でなくてエラー」になることがあります。
    • そのため、Ractor 対応を進める際は:
      • コールバックで参照するオブジェクトをできるだけイミュータブル・shareable に寄せる
      • 必要に応じて Ractor ローカルなオブジェクト生成に切り替える
        などのリファクタリングが必要になります。

3-3. パフォーマンス・メモリ ​

  • コールバックチェーンを都度新インスタンスとして構築するようになったことで、「定義変更時のオブジェクト数」は増えますが、
    • 定義変更は通常「アプリ起動時・リロード時」にほぼ限定され、
    • ランタイム中のコールバック実行速度にはほぼ影響しないはずです。
  • 一方で、Ractor 間でクラスやモジュールを共有する場合に、
    shareable な構造で統一されることにより、「コピーを量産しない」方向に寄せやすくなります。

  1. 参考情報 (あれば)

このPRにより、Rails のコールバック機構が Ractor フレンドリーな設計に刷新されつつあり、将来的な「Rails × Ractor」に向けた基盤整備の一環と言えます。


#57226 Fix method_missing performance regression for virtual SELECT alias attributes ​

マージ日: 2026/8/13 | 作成者: @hammadxcm

  1. 概要 (1–2文で)
    Rails 7.2.3 で導入された ActiveRecord::AttributeMethods#method_missing の変更により、仮想カラム(SELECT 句のエイリアスなど)のアクセスが大幅に遅くなっていた問題を、例外発生を避ける形で解消した PR です。public_instance_method 呼び出し前に public_method_defined? でチェックすることで、スレッドセーフ性を維持しつつパフォーマンスを改善しています。

  1. 変更内容の詳細

背景と問題点 ​

  • 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 で狙っていた「スレッドセーフにメソッドキャッシュを使う」という性質は壊れません。

擬似コードイメージ(※実際のコードは行数が少し違う可能性がありますが、ロジックはこれに近いです):

ruby
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)
実カラム .latitude0.004s0.010s
仮想属性 .virtual_attr0.189s0.077s
Virtual / Real 比42.1x7.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 など周辺のテストもパスしており、既存挙動を壊していないことを確認済み。

  1. 影響範囲・注意点
  • 影響範囲:
    • 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 側の実装と干渉していないか・パフォーマンス劣化を招いていないかを合わせて確認するとよいです。

  1. 参考情報 (あれば)
  • 元 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. 概要 (1-2文で)
    ActiveSupport::LogSubscriber の実装を見直し、Ractor 環境でも安全に動作するように「ログ関連のクラスインスタンス変数の初期化タイミングと場所」を変更した PR です。主に @supports_flush とデフォルト logger の設定を「遅延評価」から「メイン Ractor での事前初期化」に切り替えています。

  1. 変更内容の詳細

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 側の初期化処理」でセットするように変更。

疑似コードイメージ(実際のコードとは多少異なる可能性がありますが意図の説明用):

ruby
# 変更前(イメージ)
class ActiveSupport::LogSubscriber
  class << self
    def supports_flush?
      @supports_flush ||= logger.respond_to?(:flush)
    end

    def logger
      @logger ||= Rails.logger
    end
  end
end
ruby
# 変更後(イメージ)
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.logger
    • ActiveSupport::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 対応の有無の判定が壊れていないこと を確認する内容です。

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

    • 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 の制約上)。

  1. 参考情報 (あれば)
  • 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. 概要 (1–2文で)
    ActiveModel::AttributeMethods でメソッド名テンプレートを生成する際、:attr_name と :as が同じ場合に余計な文字列・パターンオブジェクトを生成しないようにし、Railsブート時の Pattern#method_name 関連のオブジェクト割り当てを削減する最適化です。機能的な挙動は変えずに、パフォーマンスとメモリ効率を改善する変更です。

  1. 変更内容の詳細

※実際の差分は 6 行追加 / 1 行削除のみの小さな修正で、主に条件分岐が追加されています。概要としては以下のようなロジックになっています(イメージの擬似コード):

ruby
# 変更前(イメージ)
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 を呼んでメソッド名文字列を生成していました。

ruby
# 変更後(イメージ)
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 負荷やメモリ消費が軽減されます。


  1. 影響範囲・注意点
  • 影響範囲:
    • ActiveModel::AttributeMethods を通じて生成される属性メソッド(ActiveRecord のカラムアクセサなどを含む)のメソッド定義処理。
    • 特に Rails ブート時(モデルロード時)のメソッド定義フェーズのメモリ割り当てに影響。
  • 挙動の互換性:
    • attr_name と as の値に応じて生成される最終的なメソッド名は従来と同じであるように実装されているため、公開 API の挙動変更はありません。
    • カスタム as: を使うケースでも、これまで通り Pattern#method_name を通して生成されるため、互換性が保たれます。
  • 注意点:
    • AttributeMethods 周辺で monkey patch している場合、内部実装のわずかな分岐変更に依存していると影響する可能性がありますが、通常のアプリケーションコードには影響しません。
    • 性能面では Rails を大規模に使うアプリ(カラム数・モデル数が多い)ほど恩恵が大きくなります。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58468
  • 関連領域: ActiveModel::AttributeMethods(ActiveRecord や ActiveModel の動的メソッド生成)
  • 背景知識:
    • Rails はモデル属性に対して name, name= , name? など多数のメソッドを動的に定義しており、このときメソッド名テンプレート(Pattern)を使って命名している。
    • ブート時にこれらが一括で定義されるため、この種の小さな割り当て削減が全体の起動時間・メモリ効率に効きやすい。

#58466 Restore performance for dynamic attribute read ​

マージ日: 2026/8/12 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    このPRは、前回の「パターンキャッシュ削除」によって悪化した ActiveModel の「動的属性読み取り(動的 attribute メソッド)」の性能を取り戻すための調整です。to_hash や permitted? など、Strong Parameters 周辺で多用される処理のパフォーマンス低下を緩和・解消することが目的です。

  1. 変更内容の詳細

背景 ​

前回コミット([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)

主に以下のような点が調整されていると考えられます(行数とコンテキストから推測しています):

  1. 動的メソッド解決のパスを最適化

    • ActiveModel は method_missing と respond_to_missing? を利用して user.name のような attribute アクセサを動的に解決しますが、この解決ロジックの中で、
      • シグネチャ作成
      • メソッド名のパターンマッチ(_before_type_cast, ?, = などのサフィックス)
      • attribute 定義の検索
        といった操作が毎回発生します。
    • 前回コミットで除去された「パターンキャッシュ」に依存しない形で、同等レベルの「繰り返しコスト削減」が行われています(ローカル変数の使いまわしや分岐の整理、冗長なチェックの削減など)。
  2. to_hash / permitted? 辺りのホットパスを意識した微調整

    • Strong Parameters (ActionController::Parameters) は、内部的に ActiveModel の attribute メソッドを多用します。このPRでは、そうしたホットパスで呼ばれる attribute 読み取りの実装ができるだけ軽くなるように分岐や条件チェックが整理されています。
    • method_missing から実際の attribute 値を取得する処理までのコールチェーンを短くする・あるいは一部ロジックをインライン化する方向の変更になっていると思われます。
  3. キャッシュの戻し方が「パターンキャッシュとは異なる」

    • タイトルが “Restore performance for dynamic attribute read” であり、「pattern cache を戻す」とは明示していないため、以前のパターンキャッシュそのものを復活させたのではなく、
      • キャッシュ対象を絞る
      • Ruby のメソッドキャッシュ(inline cache)や YJIT が効きやすい形に書き換える
        など、より JIT / メソッドキャッシュフレンドリーな形にコードを調整している可能性が高いです。

イメージ的なコード(※雰囲気をつかむための擬似例) ​

実際のコードとは異なりますが、やっていることはだいたい以下のような最適化イメージです。

変更前(イメージ):

ruby
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

変更後(イメージ):

ruby
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回メソッドを呼ぶたびに繰り返し行っていたコストの高い解析処理」を避けられます。


  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 が思わぬ順序で呼ばれていないか などをテストで確認することを推奨します。

  1. 参考情報 (あれば)
  • この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. 概要 (1-2文で)
    ActiveModel/ActiveRecord の「動的属性メソッド」判定に使っていた attribute_method_patterns_cache を撤廃し、代わりに複数のマイクロ最適化でパフォーマンスを補う変更です。キャッシュは性能面では有用だったものの、サイズ無制限かつ Ractor 非対応であるため、将来の並行実行モデルとの整合性を優先して削除されています。

  1. 変更内容の詳細

2-1. attribute_method_patterns_cache とは何をしていたか ​

ActiveModel/ActiveRecord では、respond_to? やメソッド呼び出し時に、

  • name_will_change!
  • name_before_type_cast
  • reset_name!

のような「パターンに基づく動的属性メソッド」を判別するため、
attribute_method_patterns という正規表現 / パターンのリストを走査していました。

attribute_method_patterns_cache は、次のような「メソッド名 → (属性名, パターン)」という解析結果をキャッシュするための Hash です。

例:

ruby
# 以前のイメージ
attribute_method_patterns_cache["something_will_change!"] =
  ["something", :attribute_will_change!]

これにより、同じメソッド名に対する respond_to? / 動的ディスパッチが繰り返されるケースでは、高速化が図られていました。

2-2. キャッシュ削除の理由 ​

PR で挙げられている問題点は 2 つです。

  1. 容量制限がない(unbounded)

    • 受け取ったメソッド名ごとにエントリが増え、上限やエビクションポリシーがないため、
      アプリケーションによってはメモリリーク的な増加を起こしうる。
  2. Ractor で動作しない

    • Ractor は「スレッドセーフよりも厳しい並行実行モデル」を要求するため、
      共有可変オブジェクト(典型的には通常の Hash)をそのまま利用できません。
    • グローバルなキャッシュ Hash は Ractor の世界観と相性が悪く、
      Ruby 本体・Rails を 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? 周りの実装が見直され、

  • 不要なオブジェクト生成を避ける
  • 文字列操作・正規表現マッチの順序・回数を減らす

など、細かい最適化が多数入っていると考えられます。


  1. 影響範囲・注意点

3-1. 性能面の影響 ​

PR 内ベンチマークによれば、動的属性まわりの respond_to? は以下のように遅くなっています(いずれも「キャッシュあり(before)」比)。

  • to_hash(必ず false, 事前フィルタで弾かれる)

    • 約 1.62 倍遅い:
      • before: 約 3,953,529 i/s
      • after: 約 2,447,226 i/s
  • permitted?(パターンにはマッチするが結果的には false)

    • 約 2.72 倍遅い:
      • before: 約 3,650,315 i/s
      • after: 約 1,340,220 i/s
  • tenderlove(動的属性で true)

    • 約 1.72 倍遅い:
      • before: 約 3,010,374 i/s
      • after: 約 1,752,522 i/s

ただし、これらは 非常に小さな単位のベンチマーク であり、現実のアプリケーションでは、

  • 同じメソッド名で大量の respond_to? を連発する
  • しかもそれが「動的属性 or 存在しないメソッド」

といったパターンが支配的でなければ、体感での差はそこまで大きくはならない可能性があります。

3-2. 影響しやすいコードパターン ​

次のようなコードは、この変更の影響を受ける余地があります。

  • 大量のモデルインスタンスに対して、ループ内で頻繁に respond_to? を呼んでいる
  • しかも、そのメソッド名が:
    • 動的属性(select("..., foo as bar") の bar など)であったり
    • *_will_change!, *_before_type_cast や類似の suffix を持つ(=パターンに引っかかる)ものであったり
    • 最終的には false になるケースが多い

例えば:

ruby
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? の速度低下」です。


  1. 参考情報 (あれば)
  • 変更ファイル:

    • 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. 概要 (1-2文で)
    Rails の ActiveSupport::TimeZone に、カナダ西部向けの新しいタイムゾーン名マッピング「Pacific Time (Canada)」「Alberta」が追加され、BC(ブリティッシュコロンビア)とアルバータ州での冬時間のズレを避けられるようになりました。既存の「Pacific Time (US & Canada)」「Mountain Time (US & Canada)」は互換性維持のためそのまま残されています。

  1. 変更内容の詳細

追加されたマッピング ​

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 への追記(概念的なイメージ):

ruby
MAPPINGS = {
  # 既存
  "Pacific Time (US & Canada)" => "America/Los_Angeles",
  "Mountain Time (US & Canada)" => "America/Denver",
  # 新規
  "Pacific Time (Canada)" => "America/Vancouver",
  "Alberta"                => "America/Edmonton",
  # ほか…
}

これにより、以下のように利用できます。

ruby
Time.find_zone("Pacific Time (Canada)") # => #&lt;ActiveSupport::TimeZone: ... "America/Vancouver">
Time.find_zone("Alberta")               # => #&lt;ActiveSupport::TimeZone: ... "America/Edmonton">

テストとドキュメント ​

  • activesupport/test/time_zone_test.rb
    新マッピングが正しく America/Vancouver / America/Edmonton に解決されるテストが追加。
  • activesupport/CHANGELOG.md
    新しいフレンドリゾーン名が追加されたことが記載され、振る舞い変更として明示されています。

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

    • 既存の "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 用に使っている場合は、上記への置き換えを検討してください。
  • IANA tzdb 依存:

    • 実際の DST / オフセットのルールは IANA tzdb に依存しており、Rails 側はあくまで「フレンドリ名称 → IANA 名」のマッピングを増やしただけです。
    • タイムゾーンデータは OS / tzinfo / ランタイムの更新状況に影響されるため、本番環境で tzdb が最新(少なくとも 2026a / 2026b / 2026c 以降)であることも重要です。

  1. 参考情報 (あれば)

これらと同様に、「国・州ごとの運用差が出てきた場合に、より具体的なフレンドリ名称を追加する」という方針に沿った変更です。


#58446 Fix where clauses with column-tuple syntax referencing other tables ​

マージ日: 2026/8/12 | 作成者: @cgunther

  1. 概要 (1-2文で)
    where 句で「カラムのタプル構文(配列構文)」を使う際に、他テーブルを other_table.column と明示した場合でも、現在のモデルのテーブル名が誤ってプレフィックスされてしまうバグを修正するPRです。これにより、JOIN 先のテーブルカラムを含むタプル条件が正しくSQLに変換されるようになります。

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

問題のあった挙動 ​

Active Record の where で、複数カラムをまとめて比較する「タプル構文」を使うときの想定は以下のようなケースです。

ruby
# 例: (category_id, author_id) = (1, 2) のような条件
Categorization.where(%i[category_id author_id] => [1, 2])

内部的には「カラム名の配列(例: [:category_id, :author_id])」を元に SQL を組み立てますが、このカラム配列は「すべて現在のモデルのテーブルに属するカラムである」と仮定されていました。

そのため、JOIN している別テーブルのカラムを table.column の形式で指定した場合に、誤ったプレフィックスが付与されていました。たとえば:

ruby
# categorizations テーブルと authors テーブルを JOIN している状況で
Categorization.joins(:author).where(
  %i[category_id authors.name] => [1, "Alice"]
)

これを解釈する際、authors.name というキーが「単なるカラム名」とみなされ、categorizations テーブル名が自動で付与されてしまい、結果として以下のような誤った SQL が生成されていました:

sql
... WHERE "categorizations"."category_id" = ?
      AND "categorizations"."authors.name" = ?

SQLite では、. を含む "authors.name" が「そのままのカラム名」と解釈され、当然そんなカラムは存在しないため、次のようなエラーが発生します。

text
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")」で指定されたキーを、そのまま「単一のカラム名文字列」として扱うのではなく、一度「ハッシュ形式」に変換してから処理するようにした。
  • これにより、テーブル名とカラム名が正しく分離され、「現在のモデルのテーブル名を無条件にプレフィックスする」ロジックが適用されなくなります。

イメージとしては、以下のような変換が先に行われるようにした、という変更です(擬似コード):

ruby
# 変更前のイメージ(簡略化)
# [:category_id, "authors.name"] が "categorizations" のカラムとして扱われてしまう

# 変更後のイメージ(簡略化)
# "authors.name" を見たときに:
#   "authors.name" -> { "authors" => "name" } のような形に変換
# その後の処理では適切なテーブル/カラムに分けて扱う

テスト (where_test.rb) では、他テーブルのドット記法カラムをタプル構文に含めた場合でも、正しい SQL が生成されることを確認するケースが 1 つ(または複数)追加されています。


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

    • Active Record の where で「カラムの配列(タプル構文)」を使い、かつキーの中に other_table.column のように「他テーブルのカラム」を直接書くケースが対象です。
    • 通常の where(author: { name: "Alice" }) のようなハッシュ構文は元から問題なかったため、影響は限定的です。
    • 既にワークアラウンドとして「文字列 SQL を直書きしていた」「Arel を直接使っていた」ようなコードを、より自然な where タプル構文に書き直せるようになります。
  • 後方互換性と注意点:

    • これまで「誤ったカラム解釈」に依存していたコードがある可能性は非常に低く、基本的にはバグ修正として安全な変更です。
    • 主な変化は「これまで例外が出ていたクエリが、意図通りに動くようになる」というもので、既存の正常系挙動に影響を与える可能性は小さいと考えられます。
    • PR 作成者も v8.1 へのバックポートを希望しており、「マイナーなバグ修正」として扱えるレベルの変更と位置付けられています。

  1. 参考情報 (あれば)
  • PR 本体: rails/rails リポジトリの PR #58446: “Fix where clauses with column-tuple syntax referencing other tables”

  • 関連コンポーネント:

    • ActiveRecord::Relation::PredicateBuilder
    • ActiveRecord::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. 概要 (1-2文で)
    Active Storage が内部で呼び出している ffmpeg / ffprobe に対し、「入力側の引数」をアプリケーション側から設定できるようにする PR です。これにより、利用可能なデコーダ・プロトコルをホワイトリスト方式で制限するなど、アップロードメディア処理部分のハードニングが可能になります。

  1. 変更内容の詳細

2-1. 新しい設定項目 ​

Active Storage に以下の 2 つの設定が追加されています。

ruby
# 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 などで:

ruby
config.active_storage.video_preview_input_arguments = "-codec_whitelist h264,aac"
config.active_storage.ffprobe_arguments = "-codec_whitelist h264,aac"

と設定したときの変化。

変更前 (プレビュー用 ffmpeg):

bash
ffmpeg -i /tmp/blob.mp4 -vf 'select=...' -frames:v 1 -f image2 -

変更後:

bash
ffmpeg -codec_whitelist h264,aac -i /tmp/blob.mp4 -vf 'select=...' -frames:v 1 -f image2 -

変更前 (ffprobe – Video/AudioAnalyzer 共通):

bash
ffprobe -print_format json -show_streams -show_format -v error /tmp/blob.mp4

変更後:

bash
ffprobe -print_format json -show_streams -show_format -v error -codec_whitelist h264,aac /tmp/blob.mp4

2-4. 実装箇所のポイント ​

  • activestorage/lib/active_storage.rb
    • ActiveStorage の設定に video_preview_input_arguments / ffprobe_arguments が追加。
  • engine.rb
    • 上記設定のデフォルト値を空文字に設定。
  • video_previewer.rb
    • ffmpeg コマンド作成時に、-i の前に video_preview_input_arguments を挿入するよう変更。
  • audio_analyzer.rb / video_analyzer.rb
    • ffprobe コマンド作成時に、ファイルパスの直前に ffprobe_arguments を差し込むよう変更。
  • テスト (*_test.rb) が追加され、設定値を変えた際にコマンドラインが期待どおり組み立てられるかが検証されています。
  • guides/source/configuring.md / guides/source/security.md
    • 新しい設定と「Media Processing of File Uploads」に関するセキュリティガイドが追加されています。

  1. 影響範囲・注意点
  • 既存アプリへの影響

    • 新設定のデフォルトが空文字列のため、既存のアプリケーションは設定を追加しない限り挙動は一切変わりません。
    • つまり、この PR は「設定を有効にしたアプリだけに影響する opt-in 機能」です。
  • 導入メリット

    • ffmpeg / ffprobe が扱えるコーデックやプロトコルをホワイトリスト方式で制限できるため、悪意あるメディアファイルを通じた攻撃面を減らすことができます。
    • 特に、外部からアップロードされる動画や音声をそのまま ffmpeg / ffprobe に食わせている環境で有効です。
  • 注意点

    • 誤ったホワイトリスト指定をすると、正当なファイルであっても解析・サムネイル生成が失敗します。
      • 例: 実際には H.265 (HEVC) を含むファイルがアップロードされるが、-codec_whitelist に h264 しか書いていない、など。
    • ffmpeg / ffprobe のバージョンやビルドオプションによって利用可能なコーデック名・プロトコル名が異なる場合もあるため、本番環境と同一条件での検証が推奨されます。
    • これは Rails 自体の脆弱性を修正する PR ではなく、「アプリケーション側のセキュリティポリシーを反映しやすくするための機能追加」です。
      ガイドにも明記されている通り、これ単体で全てのメディア関連攻撃を防げるわけではありません。

  1. 参考情報 (あれば)
  • 追加されたガイド:

    • 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. 概要 (1-2文で)
    ActiveSupport::Notifications の notifier に対して、Ractor 間共有用の新しいプロトコル to_ractor_snapshot / load_ractor_snapshot が導入されました。これにより、カスタム notifier を実装する際に内部実装(Fanout の構造)を真似る必要がなくなり、Ractor 共有対応も任意で選べるようになります。

  1. 変更内容の詳細

背景 ​

  • #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 実装側に委ねます。
  • load_ractor_snapshot(snapshot)
    • to_ractor_snapshot が返した snapshot から、実際に notifier の状態を再構築します。

使い方(概念イメージ) ​

ActiveSupport::Notifications 側は、以下のような「プロトコルに従うかどうか」を respond_to? で判定します。

ruby
notifier = ActiveSupport::Notifications.notifier

if notifier.respond_to?(:to_ractor_snapshot)
  snapshot = notifier.to_ractor_snapshot
  # snapshot は Ractor 間共有可能なオブジェクト
else
  # 非対応なら snapshot 機能は使わない(or ゴリっとコピー/共有しない)
end

新しい Ractor で復元するときは:

ruby
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 の内部状態を復元します。

これにより:

  • 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行)。

推測されるテスト観点:

  • デフォルトの Fanout notifier で、to_ractor_snapshot / load_ractor_snapshot 経由の挙動が正しく動くこと。
  • to_ractor_snapshot / load_ractor_snapshot を実装したカスタム notifier で、Ractor 用処理が問題なく機能すること。
  • これらを実装していないカスタム notifier を指定しても、クラッシュしないこと(respond_to? で安全に分岐できていること)。

  1. 影響範囲・注意点

影響を受ける可能性が高いコード ​

  1. ActiveSupport::Notifications.notifier をカスタム実装に差し替えているコード

    • 以前は(壊れたものの)Fanout の内部構造を真似している or していない前提で挙動していました。
    • これからは、以下の二通りの選択肢があります:
      • Ractor 共有を気にしない場合: 何も実装しなくてもよい(to_ractor_snapshot も load_ractor_snapshot も不要)。
      • Ractor 共有を有効にしたい場合: to_ractor_snapshot / load_ractor_snapshot を自前で実装する。
  2. Ractor を使って並列実行しており、その中で ActiveSupport::Notifications を利用しているコード

    • デフォルトの Fanout を使っている場合、今回の変更により、内部構造に依存しない安定した snapshot 取得ができるようになります。
    • カスタム notifier で Ractor 共有をしたいなら、Ractor 闘に安全なデータのみを snapshot に含めるような設計が必要です。

カスタム notifier 実装時の指針 ​

カスタム notifier に Ractor 共有対応をさせたい場合、次のような実装が一般的な形になります:

ruby
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 対応リライトが入る場合も、このプロトコルを保つことで互換性を維持しやすくなります。

  1. 参考情報 (あれば)
  • 対応する過去 PR:
    • #58060: Notifications.record_subscriptions が Fanout の内部構造に依存するようになった変更。
  • この PR で導入されたプロトコル:
    • ActiveSupport::Notifications の notifier 向け「Ractor 共有対応 API」
      • to_ractor_snapshot
      • load_ractor_snapshot
  • 関係者(レビュー/議論の文脈を追う場合):
    • 作成者: @kamipo
    • メンション: @gmcgibbon, @Edouard-chin

#58445 Compile non-strict templates Ractor-locally ​

マージ日: 2026/8/12 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    非 strict ローカルなテンプレートでも Ractor 内から安全にコンパイル・実行できるようにする変更です。併せて、既存 Rails で strict locals テンプレート利用時に例外バックトレースが正しく補正されない不具合も修正されています。

  1. 変更内容の詳細

背景 ​

  • Ractor からビューを描画する場合、テンプレートのコンパイル結果がスレッド/Ractor間で共有されると安全でない(Ractor 間で共有可能なオブジェクト制約に抵触する)ため、テンプレートのキャッシュ戦略を見直す必要があります。
  • これまでは主に「strict locals なテンプレート」を前提に Ractor 対応が進んでいましたが、この PR では「non-strict(従来どおりの locals の扱い)テンプレート」も Ractor から使えるようにしています。

UnboundTemplate の内部キャッシュ構造の変更 ​

これまで ​

ActionView::UnboundTemplate は、おおざっぱにいうと:

  • @templates という Hash で、「ローカル変数の正規化済みセット → コンパイル済みテンプレート」の対応をキャッシュ
  • 排他制御に Mutex

といった形でテンプレートを保持していました。

ruby
# イメージ(実際のコードとは異なります)
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 での変更点 ​

  1. 非 strict テンプレート + frozen な UnboundTemplate の場合:

    • @templates ではなく、Ractor ローカルな Concurrent::Map を使ってキャッシュします。
      • Ractor 毎に別の Map を持つので、Ractor 間でオブジェクトを共有しない設計になります。
    • さらにその内側で、競合を避けるためにもう一層 Concurrent::Map を利用して、従来の Mutex 相当の排他を行います。
    • これにより「同じ UnboundTemplate でも、各 Ractor ごとに独立したテンプレートキャッシュを持つ」構造になります。
  2. strict locals テンプレート について:

    • strict locals 用には @strict_locals_template というインスタンス変数にコンパイル済みテンプレートを持つように整理されており、その読み出しパスが明示化されました。
    • 従来の Hash (@templates) のデフォルト値として strict テンプレートを持っていたため、@templates.values に strict テンプレートが含まれず、後述のバックトレース補正に支障が出ていました。この構造を改めたことで不具合が解消されています。
  3. エイリアシング(locals 正規化の alias)を維持

    • 非 strict locals テンプレートで、「正規化前の異なる locals 指定」が「同一の正規化済 locals セット」にマッピングされる場合は、同じコンパイル済みテンプレートを再利用する、というエイリアシング挙動があります。
    • これは locals 正規化を「高速パスでは行わない」ための工夫でもあり、本 PR でもこの挙動を維持しています。
    • つまり、Ractor ローカルな Map を使いつつも、「locals の正規化キー → テンプレート」というキャッシュの構造・意味は変えないように設計されています。
  4. frozen? をトリガーにした挙動切り替え

    • UnboundTemplate が #freeze されたかどうかで、
      「Ractor ローカルキャッシュを使うか(frozen 時)」
      「従来どおりの単一キャッシュを使うか(非 frozen 時)」
      を切り替えるようになっています。
    • Ractor から利用する前提だと、通常はアプリケーション起動時にテンプレートが凍結されるため、Ractor 用のパスが有効になります。

バックトレース補正のバグ修正 ​

ActionDispatch::ExceptionWrapper#build_backtrace は、例外が ERB テンプレート内で発生した場合に、

  • 実際の .html.erb ファイルの行番号に対応するように
  • Ruby コードに変換されたあとの行番号を補正する

といった処理を行います。

この処理では、UnboundTemplate が保持している「コンパイル済みテンプレート群」から情報を取得しますが、

  • strict locals のテンプレートを
    「@templates Hash のデフォルト値」として持っていたため
  • @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 行程度の微修正が入っています(主にフラグ・フックの追加や呼び出し先の切り替えレベル)。

  1. 影響範囲・注意点

Ractor を使わないアプリケーションへの影響 ​

  • パフォーマンス面:
    • 非 strict locals テンプレートに対して:
      • 追加されるのは「frozen? チェック」が 1 回程度。
    • strict locals テンプレートに対して:
      • 「@strict_locals_template があるかどうかの ivar チェック」が 1 回。
      • Hash の [] アクセス + デフォルト値 から、単純な ivar 読み出しへ置き換えられているので、むしろコードパスがシンプルになっています。
    • いずれもオーバーヘッドは非常に小さく、実用上ほぼ影響はないと考えてよいレベルです。
  • 機能面:
    • バックトレース補正が改善され、strict locals テンプレートで発生したエラーのソース位置がより正確になります。
    • 既存コードの挙動を変えるような仕様変更は基本的にありません。

Ractor を使うアプリケーションへの影響 ​

  • 非 strict locals テンプレートを Ractor から使うことが「技術的に可能」になります。
  • ただし PR の説明にもある通り、非 strict locals テンプレートを Ractor から使うこと自体は推奨されていません。
    • 理由としては、locals の扱いが暗黙的であることにより、将来的な Ractor セーフティの維持が難しくなったり、予期しない共有や依存が紛れ込みやすいためです。
  • それでも、
    • 既存テンプレートをすべて strict locals 化するのが困難なアプリ
    • 段階的に strict 化していきたいアプリ にとっては、「まず Ractor 対応を進めるための移行ステップ」として有用です。

注意点 ​

  • Ractor からレンダリングするテンプレートは、
    できるだけ strict locals 化し、「Ractor セーフな依存関係」にしておくことが望ましいです。
  • 非 strict テンプレートでも動くようになりましたが、
    • グローバル状態
    • スレッドローカル / Fiber ローカル
    • 共有可能でないオブジェクト 等への依存があると、Ractor 内からの利用で思わぬ問題を引き起こす可能性があります。

  1. 参考情報 (あれば)
  • この PR の前提となる PR: #58396
    • strict locals を中心にした Ractor 対応の基盤部分がここで導入されており、本 PR はそのフォローアップとして「non-strict テンプレートへの対応」「strict locals 周りのバグ修正」を行っています。
  • 関連クラス・メソッド:
    • ActionView::UnboundTemplate
    • ActionView::Template::Handlers::ERB
    • ActionDispatch::ExceptionWrapper#build_backtrace
  • Ractor の仕様や制約を再確認したい場合は、公式ドキュメント(Ruby 3.x の Ractor セクション)および Rails ガイドの並行処理関連ドキュメントを見ると理解が進みます。

#58403 Move duplicated conditional_executor up to Rails::Command::Base ​

マージ日: 2026/8/12 | 作成者: @seuros

  1. 概要 (1-2文で)
    Rails::Command::ConsoleCommand と Rails::Command::RunnerCommand に重複していた conditional_executor の実装が、共通の親クラスである Rails::Command::Base に集約されました。これにより、同様の挙動を持つコマンド間でロジックを共有できるようになり、重複コードが削減されています。

  1. 変更内容の詳細

※ PR本文に説明がないため、Rails 既存実装の慣例と変更された行数・ファイル構成から推測を含みます。

どんなリファクタリングか ​

  • 変更前:
    • Rails::Command::ConsoleCommand に conditional_executor メソッド (もしくは同等のロジック) が定義されていた
    • Rails::Command::RunnerCommand にもほぼ同一の conditional_executor が定義されていた
  • 変更後:
    • この重複コードが削除され (+0/-8 が 2 ファイルで発生)
    • 共通のスーパークラスである Rails::Command::Base に conditional_executor が新たに追加されました (+9/-0)

イメージとしては、以下のようなリファクタリングが行われています:

ruby
# 変更前 (イメージ)
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
ruby
# 変更後 (イメージ)
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
end

conditional_executor が担っている可能性が高い役割 ​

Rails のコマンド実装で「conditional ~」という名前は、環境やオプションに応じて実行器(executor)を切り替えるような用途でよく使われます。
この PR で登場する conditional_executor も、例えば以下のような責務を持っていると考えられます(あくまで代表的なパターンの推測です):

  • 開発環境・本番環境・テスト環境などによって異なる実行戦略を選ぶ
  • Spring や Bootsnap、マルチスレッド/マルチプロセス実行の有無に応じて実行方法を変える
  • --environment や --no-color などの CLI オプションに応じて実行コンテキストを切り替える

共通の Base に集約されたことで:

  • Console と Runner どちらのコマンドも、同じ条件判定・同じ実行戦略を共有する
  • 将来ほかのコマンドでも同じ仕組みを使いたくなった場合、Base だけ修正すればよい

といったメリットがあります。


  1. 影響範囲・注意点

影響範囲 ​

  • 直接的な影響を受けるのは、主に以下のクラスです:
    • Rails::Command::ConsoleCommand
    • Rails::Command::RunnerCommand
  • 実装はリファクタリング(共通化)であり、外部 API やコマンドの挙動を変えることを目的とした変更ではないと考えられます。
    • そのため、通常の rails console や rails runner の利用者には、動作上の変化は基本的にありません。

注意点(カスタマイズしている場合) ​

以下のようなケースでは影響を受ける可能性があります:

  1. 独自に Rails コマンドクラスを定義している場合

    • Rails::Command::Base を継承した自作コマンドで、conditional_executor と同名メソッドを定義していた場合:
      • メソッド解決順序的には自クラスの定義が優先されるため、即座の壊れ方はしませんが、
      • 名前の衝突により、将来の Rails 側の挙動変更と干渉するリスクがあります。
    • 同名メソッドを避けるか、もし意図的に上書きしているなら、super 呼び出しや挙動の差分を意識して実装する必要があります。
  2. Rails::Command::ConsoleCommand / RunnerCommand を monkey patch している場合

    • 以前、これらのクラスの conditional_executor を直接上書きしていた場合、
      • メソッドの定義場所が Base に移ったことで、パッチが想定どおりに効かなくなる可能性があります。
    • Rails のバージョンアップ時に、method_source などで定義場所を再確認したほうが安全です。
  3. テストや計測で内部メソッドを直接呼び出している場合

    • テストコードが Rails::Command::ConsoleCommand#conditional_executor を send/public_send などで直接叩いている場合:
      • 定義場所の変更自体は Ruby 的には透過的ですが、将来的なリファクタリングで可視性や挙動が変わる可能性があるため、
      • 内部メソッドへの依存を最小限にするほうがメンテナンス性は高くなります。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Rails のスキーマ定義まわりに、テーブル変更用の新しいビルダー build_alter_table_definition が追加され、既存の create_alter_table 呼び出しに依存せず「テーブル変更定義オブジェクト(AlterTable)」を組み立てられるようになりました。これにより、create_table と同様のスタイルで「変更予定内容」をオブジェクトとして扱えるようになり、DDL 生成や外部マイグレーション管理との連携がしやすくなります。

  1. 変更内容の詳細

新しく追加されたメソッド: 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.rb
  • postgresql/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 行レベルで更新され、アダプタの振る舞い整合性を確認

コード例(イメージ・擬似的な使用例として):

ruby
# 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 を生成」という流れを実現するための基盤部分が今回の変更です。


  1. 影響範囲・注意点
  • Rails アプリ開発者(通常のマイグレーションを書く側)への直接的な変更はほぼありません。
    • 既存の change_table / add_column / remove_column などの DSL はそのまま動く想定です。
  • 影響がある可能性があるのは、以下のようなケースです:
    • アダプタ内部API(create_alter_table など)を send で直接叩いて独自に DDL を生成していたような高度なメタプログラミング/ツール
    • スキーマ定義ビルダー (build_create_table_definition など) を使って独自にマイグレーション管理・DDL 生成をしているツールやライブラリ

注意点:

  • build_alter_table_definition は :nodoc: のため、公式的には「内部API」扱いであり、将来的な変更の余地があります。
  • とはいえ、build_create_table_definition と対になる概念として導入されているため、スキーマ生成系の周辺を拡張・統合する場合には、今後こちらを利用した実装が「正攻法」になっていく可能性があります。
  • MySQL・PostgreSQL のアダプタ実装がリファクタリングされていますが、テストも更新されており、挙動としての互換性は維持される想定です。

  1. 参考情報 (あれば)

#57846 Fix normalizes on an enum attribute raises ArgumentError instead of normalizing before enum casting ​

マージ日: 2026/8/12 | 作成者: @joaoGabriel55

  1. 概要 (1-2文で)
    normalizes と enum を同じ属性に併用した際、正規化前の値に対して enum が先にバリデーションして ArgumentError を投げていた問題を修正した PR です。正規化を「enum の検証より先」に行うようにし、ユーザー入力を正しくサニタイズしてから enum キャスト・検証できるようになりました。

  1. 変更内容の詳細

何が問題だったか ​

次のようなモデルを考えます:

ruby
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 status

normalizes は status に代入された値を strip.downcase で "pending" にしてくれるはずですが、実際には " Pending " のまま enum 側のバリデーションに渡されてしまい、「有効な status ではない」として ArgumentError が発生していました。

背景にある実装構造は以下のようなものです。

  • enum も normalizes も、Active Model の「型(type)」をラップするデコレータとして実装されている
  • enum + normalizes を使うと、型のスタックはざっくり NormalizedValueType → EnumType → もとの型 という構造になる
  • ActiveModel::Attribute#with_value_from_user は、ユーザー入力に対して
    1. type.assert_valid_value(value) を「生の値」で呼び出す
    2. その後に cast して内部表現に変換する
      という順で処理する

NormalizedValueType は cast はオーバーライドして正規化を挟んでいましたが、assert_valid_value はオーバーライドしておらず、そのまま内側の EnumType に「生の値」を渡していました。その結果:

  • " Pending " が正規化される前に EnumType に渡る
  • EnumType は " Pending " を知らないので ArgumentError を投げる

という不整合な流れになっていました。

今回の修正内容 ​

ActiveModel::Attributes::Normalization::NormalizedValueType に assert_valid_value のオーバーライドを追加し、「正規化済みの値」に対して下層の型 (cast_type) のバリデーションを行うように変更しました。

擬似コードで表すと、PR で追加されたのは以下のようなロジックです:

ruby
def assert_valid_value(value)
  cast_type.assert_valid_value(cast(value))
end

これにより、実際の流れはこうなります:

  1. ユーザー入力: " Pending "
  2. NormalizedValueType#assert_valid_value が呼ばれる
  3. cast(value) により "pending" に正規化される
  4. 内側の EnumType.assert_valid_value("pending") が呼ばれる
  5. "pending" は有効な enum 値なのでエラーにならない

一方で、本当に不正な値は正規化後にも検出されます:

ruby
Order.new(status: "  bogus  ")
# 正規化後は "bogus"
# => ArgumentError: 'bogus' is not a valid status

cast で保存される値と、assert_valid_value で検証される値が一致するようになった、というのが設計上のポイントです。

テストとドキュメント ​

  • activerecord/test/cases/normalized_attribute_test.rb に回帰テストを追加
    • 正規化によって enum にマッチするようになるケース
    • 正規化してもなお不正な値のケース
      の両方を確認
  • activemodel/CHANGELOG.md に挙動変更として追記
    (バグ修正だが、enum + normalizes の組み合わせの実行時挙動がユーザーにとって意味のある形で変わるため)

  1. 影響範囲・注意点
  • 影響を受けるのは「normalizes が適用されている属性」だけです。normalizes を使っていない属性や、enum 単体の挙動には変更はありません。
  • 既存で enum + normalizes を併用していた場合:
    • これまで「正規化されるはずだったが、ArgumentError で落ちていた」ケースが、期待どおり正規化されてから enum として受け入れられるようになります。
    • もしアプリケーション側で、この ArgumentError を rescue したり、メッセージに依存したロジックを書いていた場合は、その挙動が変わる可能性があります(ただし、そのようなコードはもともと「正しく正規化されていない」というバグに依存していたことになります)。
  • 入力バリデーションの観点:
    • " Pending " → "pending" のような「表記ゆれ」の吸収が enum と安全に組み合わせて行えるようになります。
    • 利用者は、normalizes 内で「期待しない値を別の合法値にマップする」ようなロジックを書かない限り、異常系の検出はこれまで通り ArgumentError で行えます。

  1. 参考情報 (あれば)
  • 対応 Issue: https://github.com/rails/rails/issues/57828
  • 変更ファイル:
    • activemodel/lib/active_model/attributes/normalization.rb
    • activerecord/test/cases/normalized_attribute_test.rb
    • activemodel/CHANGELOG.md
  • 関連する Rails 機能:
    • Active Model Normalization (normalizes)
    • Active Record enum
    • ActiveModel::Attribute#with_value_from_user の型検証フロー

#58455 Show bin/console startup banner based on IRB ​

マージ日: 2026/8/12 | 作成者: @byroot

  1. 概要 (1-2文で)
    bin/console 実行時に表示される起動バナーを、Rails 独自実装ではなく IRB が持つバナー表示機構に合わせて出すように整理し、Rails 側のバナー制御ロジックを IRB ベースに再設計した PR です。これにより、IRB の設定やバージョンに応じた一貫したバナー表示が行われ、テストもそれに合わせて拡充されています。

  1. 変更内容の詳細

※元 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 まわり) に、起動バナー表示に関するロジック・フックを追加

概念的には次のようなフローに整理されています(擬似コード):

ruby
# Rails::Console::IRBConsole の中 (イメージ)
def start
  # IRB の設定・context 準備
  setup_irb

  # IRB が起動するタイミングに合わせて、Rails バナーを表示
  show_startup_banner_if_needed

  # IRB メインループ開始
  IRB.start
end

また、環境情報や Rails バージョンなどを含んだバナー文字列を専用メソッドで構築するような形になっている可能性が高いです:

ruby
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 ベースのバナー表示に合わせて調整

例として、従来:

bash
$ 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 用のバナー処理が走らないことの検証

擬似的なテスト例イメージ:

ruby
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

  1. 影響範囲・注意点
  • 対象: 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 の標準出力がテストに利用されていないか確認すると安心です。

  1. 参考情報 (あれば)

#58454 Avoid duplicating fixtures path ​

マージ日: 2026/8/12 | 作成者: @byroot

  1. 概要 (1-2文で)
    このPRは、Rails のテストヘルパー内部で「fixtures のパスを二重に登録してしまっていた」処理を修正し、不要な重複をなくすことでテスト実行を軽量化しています。動作上の機能追加や仕様変更はなく、パフォーマンスと内部実装の整理が主目的です。

  1. 変更内容の詳細(あればサンプルコードも含めて)
  • 対象ファイル: 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行の差分は公開されていませんが、典型的には下記のようなイメージの変更です)

ruby
# 修正前(イメージ)
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 的な重複排除を挟むような変更と考えられます。


  1. 影響範囲・注意点
  • 機能面の影響:

    • fixtures の検索パスが変わったり、どの fixtures が読み込まれるかが変化するわけではありません。
    • 既存のテストコードの挙動は基本的に変わりません。
  • パフォーマンス面:

    • 大規模なテストスイートや fixtures が大量にあるプロジェクトでは、テスト起動時や fixtures 読み込み時のオーバーヘッドがわずかに減る可能性があります。
    • ただし1行レベルの最適化であるため、体感できるほどの劇的な改善ではなく、「無駄を1つ消した」という整理の意味合いが強いです。
  • 互換性・移行時の注意点:

    • fixtures のパス設定を Rails の標準的な方法 (test/fixtures など) で行っている限り、特別な対応は不要です。
    • もし独自に fixture_paths を上書き・追記しているメタなコードを書いている場合でも、パスの重複に依存しているような特殊な実装をしていなければ影響はありません。
    • 「fixtures パスが複数回登録されている前提で、内部の配列の長さや順序に依存したコード」を書いていると影響が出る可能性がありますが、そのようなコードは通常想定されていません。

  1. 参考情報 (あれば)
  • 元の問題指摘・背景:

  • 関連する Rails コードの読みどころ:

    • railties/lib/rails/test_help.rb
      • Rails のテスト環境をセットアップする中心的なファイルで、ActiveSupport::TestCase の拡張や fixtures のロード設定などがまとまっています。
    • active_record/fixtures / ActiveRecord::FixtureSet
      • fixtures のロード・管理周りの実装に興味がある場合はこちらも読むと理解が深まります。

#57980 Construct And nodes as Nary instead of Binary ​

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

  1. 概要 (1-2文で)
    このPRは、Active Record のクエリ条件を組み立てる際の And ノードの構造を、「二分木」ではなく「N分木(可変長)」として構築するように変更したものです。これにより、複数条件を AND で結合したときの AST(抽象構文木)の形がフラットになり、扱いやすく・最適化しやすくなります。

  1. 変更内容の詳細

何が問題だったか ​

従来の実装では、複数の条件を AND で結合する際に reduce を使っていたため、次のような「左に深い二分木」の構造になっていました。

text
And
  And
    And
      Query
    Query
  Query

概念的には、例えば:

ruby
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分木」として構築するようにしています。

新しい構造は次のようになります:

text
And
  Query
  Query
  Query

つまり、同じく

ruby
rel.where(a: 1).where(b: 2).where(c: 3)

に相当する内部表現が、

  • AND(a = 1, b = 2, c = 3)

のような「1つの And ノードに複数の条件を子要素として持つ」形になります。

具体的なコードイメージ(擬似的) ​

※実際のコードは 7 行追加・1 行削除と小さい変更ですが、イメージとしては以下のような変化です:

従来(reduce で二分木を作るイメージ):

ruby
nodes.reduce do |memo, node|
  Arel::Nodes::And.new(memo, node) # 2つの子を取るBinaryなAnd
end

変更後(フラットな Nary And を作るイメージ):

ruby
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 を構築するようにしている、という変更と考えられます。


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

    • Active Record の内部でクエリ条件を Arel ノードに変換する部分(PredicateBuilder)の挙動が変わります。
    • 主に「複数条件を AND で結合する」ケースに影響します(where, having, joins 等、条件を構築するあらゆる箇所で利用される可能性があります)。
  • 互換性への影響

    • 通常のアプリケーションコード(Model.where(...) を書いている側)から見える SQL は論理的には同じです。
    • ただし、次のようなケースでは影響が出る可能性があります:
      • Arel ノードの構造(ツリー形状)を前提に独自解析・パターンマッチをしているコード
      • Arel::Nodes::And を「常に左再帰的な二分木」と見なしてトラバースしているメタプログラミング・ライブラリ
    • こうしたコードは、And が「1つのノードに複数の子を持つ」構造になることを前提に修正が必要になります。
  • パフォーマンス面

    • フラットな Nary 構造になることで:
      • 深いネストをたどる必要がなくなるため、条件のトラバースコストが軽減される可能性があります。
      • 最適化やノードの再結合・簡約(例: 重複条件の削除)などがやりやすくなります。
    • 逆に、Arel ノード構造のサイズや生成コストに目立った悪化要因はなく、実質的には改善寄りの変更と考えられます。
  • デバッグ・ログ出力

    • Arel ノードをインスペクションしてデバッグしている場合、And の見え方(ネストの深さなど)が変わります。
    • ツリーを手で追っていた場合、「ネストが浅くなった」ように見えるので注意してください。

  1. 参考情報 (あれば)
  • 関連しそうな箇所:

    • Arel::Nodes::And クラスの実装(Nary サポートの追加・変更が先行している可能性が高い)
    • ActiveRecord::Relation::WhereClause / PredicateBuilder あたりのコード(条件生成の呼び出し元)
  • 想定される今後の発展:

    • Nary And に統一されたことで、将来的に:
      • 条件式の正規化・簡約
      • 複雑なクエリの自動最適化(例えば ((A AND B) AND (A AND C)) のような式の再構成)
    • といった機能強化がしやすくなります。

このPRは主に内部構造の改善ですが、Arel ノード構造に依存したカスタム実装を持っている場合は、And がフラットな Nary ノードになる前提でコードを見直しておくと安全です。


#58388 Cleanup duplicate/unneeded requires ​

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

  1. 概要 (1-2文で)
    Rails内部のいくつかのファイルから、すでに他所で読み込まれているため不要となっていた require を削除し、requires の重複を整理したPRです。機能追加や挙動変更はなく、コードベースのクリーンアップ・メンテナンスが目的です。

  1. 変更内容の詳細

このPRでは、./tools/railspect requires . という内部ツールで検出された「重複または不要な require」を削除しています。

ツール出力(抜粋):

txt
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になっています。


  1. 影響範囲・注意点
  • 既存機能への影響

    • 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"
      などを追加する必要があります。
  • パフォーマンス・メモリ

    • require の削減により、わずかではありますがロード時のオーバーヘッドを減らす効果があります。ただし、数行レベルなので体感できる差はほぼありません。

  1. 参考情報 (あれば)
  • PRで利用されているツール: ./tools/railspect requires .
    • Railsリポジトリ内の require 依存関係を解析し、重複や不要なものを抽出するための内部ツールと考えられます。
  • 今回のようなPRは、Railsのコードベースを長期的に保守しやすくするための「テクニカルデット削減」の一環であり、他のコンポーネントでも同様のクリーンアップが行われている可能性があります。

#58450 Dedup Reflection#plural_name ​

マージ日: 2026/8/11 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    ActiveRecord::Reflection の plural_name を「重複しない(dedup)」形で扱うようにする、小さなリファクタリングの PR です。内部実装の 1行が置き換えられただけで、外部 API の振る舞いは基本的に変えずに重複処理を整理しています。

  1. 変更内容の詳細

※ PR 本文に説明がなく、変更も 1行のみのため、Rails の Reflection#plural_name の既存実装・命名規則から推測を含めて解説します。

  • 変更ファイル: activerecord/lib/active_record/reflection.rb
  • 変更行数: +1 / -1(実質的には同じロジックを別の場所/メソッドに委譲 or 既存の仕組みを再利用)

Reflection#plural_name は、関連名(association name)から「複数形の名前」を返すためのメソッドです。
典型的には、次のような用途で使われます:

ruby
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 コアの典型的なパターンだと、以下のような変更が行われている可能性が高いです(擬似コード):

ruby
# 変更前(例)
def plural_name
  name.to_s.pluralize
end

# 変更後(例)
def plural_name
  @plural_name ||= some_common_method_that_already_handles_pluralization
end

あるいは、すでに別の場所で定義済みの @plural_name キャッシュや、関連ごとに計算済みの名前付けロジックに乗せ替える、といった形で「新たに重複したロジックを持たない」ようにした可能性があります。

要点としては:

  • ロジックの重複(同じ処理のコピペ)をなくす
  • キャッシュや既存の名前解決ロジックを再利用する
  • 結果としてコードの保守性を上げる(将来の仕様変更時の修正箇所を減らす)

というリファクタリングです。


  1. 影響範囲・注意点
  • 外部 API としての Reflection#plural_name の戻り値は基本的に変わらない想定です(内部の重複を解消しただけ)。
  • ただし、次のような「Rails 内部実装に依存した高度なメタプログラミング」をしている場合は、注意が必要になります:
    • Reflection#plural_name の内部処理(たとえば name.to_s.pluralize をしていること)を前提に monkey patch している
    • plural_name がメモ化される / されないタイミングに依存したコードを書いている
  • 一般的なアプリケーションコード(reflect_on_association(:foo).plural_name を素直に呼んでいる程度)では、動作への影響はほぼないと考えて良いです。

  1. 参考情報 (あれば)
  • 対象 PR: rails/rails リポジトリの PR #58450: Dedup Reflection#plural_name
  • 関連するコード周辺を読む際のキーワード:
    • ActiveRecord::Reflection
    • ActiveRecord::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. 概要 (1-2文で)
    Active Record のクラスインスタンス変数(@default_attributes と @find_by_statement_cache)が Ractor セーフになるように実装を見直した PRです。これにより、Active Record を Ractor(マルチスレッド+並列実行)環境で安全に利用しやすくなっています。

  1. 変更内容の詳細

2-1. @default_attributes を Ractor セーフに ​

背景として、Active Record のベースクラスで利用している @default_attributes は、クラスインスタンス変数として保持されており、そのままだと Ractor 間で共有するにはオブジェクトが「Ractor シェア可能」になっている必要があります。

この PR では、

  • @default_attributes を「メイン Ractor 上で一度だけ構築」
  • 構築後にその属性セットを freeze して不変にする

という形に変えています。

ポイント:

  • メイン Ractor で一度だけ(かつ不変)にすることで、他の Ractor に安全に共有可能
  • Ractor 間で書き換えが発生しないようにしている

イメージとしては以下のようなパターンになっているはずです(実際のコードは多少異なる可能性がありますが、意図のイメージとして):

ruby
# 例: メイン 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 に追加されています(あくまでイメージです):

ruby
# 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 ローカルに」動くことをテスト。

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

    • Active Record のデフォルト属性 (default_attributes)
    • find_by 系メソッドのステートメントキャッシュ
    • 新たに追加される ActiveSupport::Ractors ユーティリティ
  • 互換性:

    • パブリック API のシグネチャ自体は変えていないため、通常のアプリケーションコードへの影響はほぼない想定
    • ただし、Active Record 内部のクラスインスタンス変数に直接アクセスしていたメタプログラミング/Monkey Patch があると挙動が変わる可能性があります
      • @default_attributes に直接上書きしていた場合
        → freeze により書き換えができなくなる、あるいは別のタイミングで初期化される
      • @find_by_statement_cache を直接参照していた場合
        → そもそもインスタンス変数として存在しなくなる
  • Ractor を使った並列実行の安定性向上:

    • Ractor を本格利用する環境(並列 Web サーバ, job worker など)では、Active Record のベースクラスで共有していた状態が整理されるため、データ競合・Ractor 制約違反によるエラーのリスクが減ります。
  • パフォーマンス:

    • Ractor ごとにキャッシュを持つため、従来「単一のキャッシュを全スレッド/全 Ractor で共有」していた場合とキャッシュヒット率やメモリ使用量のバランスが変わる可能性があります。
    • ただし、Ractor 間共有を無理に維持するよりも、安全性を優先する設計になっていると見てよいです。

  1. 参考情報 (あれば)
  • この 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. 概要 (1-2文で)
    Active Record の attribute 定義が、モデルの「属性メソッド自動生成」と常に同期するように修正されています。これにより、あとから追加された attribute でも「動的属性」ではなく、通常の getter/setter などのメソッドが必ず生成されるようになります。

  1. 変更内容の詳細

何が問題だったか ​

従来は、以下のようなケースで「属性メソッド生成のタイミング」と「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 が定義されたときにも、必ず属性メソッド生成の仕組みが動くようにする」ことです。

イメージとしては、以下のようなことを常に保証する変更です(※擬似コード):

ruby
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 内容からの推測ですが、挙動としては上記が意図されています。)


  1. 影響範囲・注意点

影響範囲 ​

  • 対象:
    • Active Record モデルで attribute :xxx, :type を利用しているコード、特にクラス定義後・実行時に attribute を追加するようなメタプログラミングをしている箇所。
  • 変化:
    • これまで「たまたま」動的属性として扱われていたケースが、正式な属性メソッドとして扱われるようになります。
    • 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 の独自実装など)がある場合は、テストが必要です。

  1. 参考情報 (あれば)

#58441 Render Markdown lists that collapse into paragraphs ​

マージ日: 2026/8/11 | 作成者: @55728

  1. 概要 (1-2文で)
    このPRは、Railsソース中の :markup: markdown なRDocコメントで、本来リストとして表示されるべき部分が1つの長い段落としてレンダリングされていた箇所をMarkdownとして正しく解釈されるように修正したものです。ついでに、リストではなくサンプル出力として表示すべき箇所をコードブロック(<pre>)として出るように整えています。

  1. 変更内容の詳細

背景・問題点 ​

  • 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行でつながっている形):

markdown
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では、例えば以下のように「前に空行+行頭に番号」になるよう修正しています(実際のコードもこの趣旨):

markdown
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::TestCase
  • ActionCable::Channel::TestCase
    (どちらも「テストの手順」の説明が番号付きリストになる)

2. 単純な箇条書きリスト (unordered list) ​

同様に、- item 形式のリストが前後の改行不足で単なる段落と解釈されていた箇所を修正しています。

代表例: ActionDispatch::Http::URL::DomainExtractor

元のコメントは概ね以下のような構造でした:

markdown
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では例えば以下のような形になるよう、空行・インデントを調整しています:

markdown
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のコードブロックとなるようにインデント/空行を修正しています。
例えば以下のような形になります(イメージ):

markdown
Output:

```ruby
#&lt;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 行だけ修正されている形です。


  1. 影響範囲・注意点
  • 実行時の挙動:
    • EncryptedFile#read の実際の実装・例外クラス・発生条件には一切変更がなく、アプリケーションコードの動作には影響しません。
  • 影響範囲:
    • Rails API ドキュメントや、RDoc/YARD などで生成されるドキュメントの HTML 表示のみ。
    • 暗号化ファイル機能(config/credentials.yml.enc 等)を使うアプリケーションにも、バグフィックスや破壊的変更はありません。
  • 注意点:
    • ドキュメントを参照している人にとって、例外のリストが読みやすく整理されるだけの変更です。
    • これを前提にしたテストやパーサー(HTML を自動解析するツールなど)がある場合は、HTML 構造が変わる可能性を考慮する必要がありますが、一般的な利用では問題にならないはずです。

  1. 参考情報 (あれば)
  • 対象メソッド: ActiveSupport::EncryptedFile#read
    Rails ガイド上では、主に「Credentials」や「Encrypted configuration」に関連する箇所で登場します。
  • 関連コミット:
    • 現状の不正な段落表示を生んだとされるコミット: 17831794cc
    • 今回の PR (#58439) は、その変更で崩れたドキュメントレイアウトを元に戻すものです。

#58412 Omit optional route segments given empty strings ​

マージ日: 2026/8/11 | 作成者: @carldaws

  1. 概要 (1-2文で)
    url_for などでルーティングの「任意セグメント」に空文字列 ("") が渡された場合、そのセグメントを URL から省略するように振る舞いを統一した修正です。これにより、フォーム送信などで空文字が来ても壊れた URL が生成されず、nil と同じ扱いになります。

  1. 変更内容の詳細

従来の問題 ​

ルーティング定義:

ruby
get "(/locale/:locale)/products(/:id)", to: "products#show"

このとき url_for の挙動は以下のようになっていました:

ruby
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= のようなクエリパラメータになってしまう、ということは起きない。

サンプル挙動(意図される新挙動):

ruby
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 内部で値の正規化・除去を行うようにしている。

  1. 影響範囲・注意点
  • 影響を受ける主なケース

    • 任意セグメント((), /: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 や空クエリパラメータの扱い といったケースが固定されています。

  1. 参考情報 (あれば)
  • 対応 Issue: #55845
  • 置き換えられた PR: #55846
  • 変更ファイル:
    • actionpack/lib/action_dispatch/journey/formatter.rb
    • actionpack/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. 概要 (1-2文で)
    Rails のルート再読み込み処理 (Rails::Application::RoutesReloader) が「ルートを実際には再描画しているのに loaded フラグが立たない」という退行バグを修正し、重複してルートが描画される問題を防ぐ PR です。execute 実行後に適切に「ルートがロード済み」であることを記録しつつ、失敗時は未ロードのままにするように振る舞いを整理しています。

  1. 変更内容の詳細

背景となる問題 ​

  • 本来 RoutesReloader#execute は「ルートを描画して finalize するだけでなく、その結果を loaded 状態として記録する」役割を担っていました。
  • しかし、過去のコミット(be08bdce21)以降、この「loaded 状態の記録」が抜け落ちており、以下のような挙動になっていました:
ruby
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 にする」処理が追加されています。

イメージとしては下記のような処理が行われるようになったと考えられます(実際のコードは若干異なる可能性がありますが、意味としてはこれに相当):

ruby
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行):

  1. execute 実行後に loaded が true になること
  2. ルートが二度描画されないこと(すなわち、execute 実行後は再描画が行われない)
  3. execute が失敗した場合は loaded が false のまま残り、再実行されること

すべての関連テスト群はパス:

  • railties/test/application/routes_reloader_test.rb: 5 runs, 11 assertions, 0 failures
  • railties/test/application/routing_test.rb: 34 runs, 0 failures
  • railties/test/application/loading_test.rb: 22 runs, 0 failures
  • railties/test/application/initializers/frameworks_test.rb: 28 runs, 0 failures

  1. 影響範囲・注意点

影響範囲 ​

  • 主に影響を受けるのは「ルート再読み込みの挙動に依存する」部分です。
    • Rails コンソールでの reload!
    • 開発環境でのコード変更後のリクエスト
    • Engine::LazyRouteSet による遅延ルーティングセットアップ
  • これまで:
    • 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 フラグを直接検査していた場合は挙動差が出る可能性があります(多くの場合は「正しくなった」方向の変化)。


  1. 参考情報 (あれば)
  • 関連コミット:
    • be08bdce21: execute が loaded 状態を記録しなくなった変更が含まれていたコミット
    • 115f92b4d8: loaded reader が復活したコミット(ただし setter 側の復元はされていなかった)
  • 関連クラス・モジュール:
    • Rails::Application::RoutesReloader
    • Rails::Engine::LazyRouteSet
  • チェックリスト上も:
    • バグ修正に対するテスト追加
    • 振る舞い変更に応じた CHANGELOG 更新
      が明示されているため、公式に「挙動としてこれが正」と位置づけられています。

#58433 Keep the string subscribers default when rebuilding a Fanout ​

マージ日: 2026/8/10 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    Ractor 内で ActiveSupport::Notifications::Fanout を再構築する際に、文字列キー用サブスクライバのデフォルト値設定が失われてクラッシュするバグを修正した PR です。サブスクリプションスナップショットから再構築する際に、Concurrent::Map とデフォルト値、および配列の複製を正しく行うようにしています。

  1. 変更内容の詳細

問題の背景 ​

  • マルチ 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_for concatenates 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 して元スナップショットと共有しない独立の配列にする ように変更されています。

概念的には以下のようなことをしています(実際のコードは多少異なりますが、イメージ用):

ruby
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 しても
      • エラーにならず正常に動作すること を確認するテストです。

  1. 影響範囲・注意点
  • 主な影響範囲:
    • ActiveSupport::Notifications を Ractor と併用しているコード
      (特に、メイン Ractor 以外でも ActiveSupport::Notifications による計測やロギングを行うようなケース)
  • この PR により、
    • サブ Ractor 内で「未購読イベントを初めて instrument したとき」に起きていたクラッシュが解消されます。
    • スナップショットからの再構築時に配列を dup しているため、
      • スナップショット取得元の Ractor と
      • 再構築先の Ractor
        のサブスクライバ配列が共有されなくなり、副作用の影響範囲が分離されます。
      • これは並行実行時の予期せぬ競合・不変性違反を防ぐ意味でも妥当な変更です。
  • 既存コードへの互換性:
    • Public API (ActiveSupport::Notifications のメソッド群) には変更がないため、通常の単一スレッド/単一 Ractor 環境では挙動に変化はありません。
    • もし独自に Fanout を直接触っている(内部 API に依存している)場合は、
      • スナップショットの形式や、内部で Concurrent::Map と配列 dup が使われている前提を壊さないよう注意が必要です。

  1. 参考情報 (あれば)
  • 元 PR: #58060
    • 本 PR はそのフォローアップとして、Ractor 向けのサブスクライバスナップショット処理の不備を修正したものです。
  • 関連クラス:
    • ActiveSupport::Notifications::Fanout
    • Concurrent::Map(concurrent-ruby のスレッドセーフな Map 実装)
  • 実際に Ractor で Notifications を使う場合のポイント:
    • メイン Ractor で設定したサブスクライバをサブ Ractor に複製している仕組みの一部がこの Fanout のスナップショットです。
    • 本修正により、サブ Ractor でもメイン Ractor と同等に安全に instrument を呼び出せるようになります(未購読イベントを含めて)。

#58385 Require rack/ractorize in ractorize! ​

マージ日: 2026/8/10 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    Rails の ractorize! 実行時に rack/ractorize を必ず読み込むようにし、Rack 側で提供される「Ractor 対応のために公開定数を freeze する処理」を Rails アプリの Ractor 実行時に確実に有効化する変更です。これにより、非メイン Ractor 内でのリクエスト処理に必要な Rack の定数 freeze が自動的に行われます。

  1. 変更内容の詳細

2-1. Rails::Application#ractorize! での require 追加 ​

railties/lib/rails/application.rb に、ractorize! 呼び出し時に rack/ractorize を読み込むコードが追加されています。

イメージとしては以下のような変更です(実際のコードは多少異なる可能性がありますが、意図としてはこれに近いです):

ruby
# railties/lib/rails/application.rb

module Rails
  class Application
    def ractorize!
      # 新たに追加された行
      require "rack/ractorize"

      # 既存の ractorize! ロジック(アプリケーションを Ractor 対応にするための処理)
      # ...
    end
  end
end

rack/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 の定数の状態に関するアサーション更新など)。


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

    • 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" のような「破壊的変更」を行っている場合は、別の仕組みに移行する必要があります(独自定数を自前で管理するなど)。

  1. 参考情報 (あれば)
  • この PR の背景となる前提:

    • Ruby の Ractor はオブジェクト共有に厳格な制約があるため、共有されるオブジェクト(特に定数でぶら下がる構造物)は immutability(freeze)が強く求められます。
    • Rack は Web サーバーとアプリをつなぐ基盤レイヤーのため、その公開定数が Ractor セーフであることは、Rails に限らず他フレームワークにとっても重要です。
  • 関連しそうなトピック/キーワード:

    • Rack の Ractor 対応 (rack/ractorize)
    • Rails の Ractor サポート (Rails.application.ractorize!)
    • Frozen オブジェクト / Frozen constants と後方互換性
    • 非メイン Ractor 上でのリクエスト処理・並列化戦略

#58426 Deprecate create alias of insert in connection adapters ​

マージ日: 2026/8/10 | 作成者: @kamipo

  1. 概要 (1-2文で)
    Railsのコネクションアダプタにおいて、SQLのINSERTを実行するメソッドinsertのエイリアスとして提供されていたcreateが非推奨(deprecate)になりました。今後はcreateではなくinsertを直接呼び出すことが推奨されます。

  1. 変更内容の詳細

目的 ​

  • ActiveRecordの低レベルAPIにおけるDMLメソッドを、SQLの用語 (insert, update, delete) と明確に対応させるため。
  • createという名前は、create_table, create_database などのDDLを連想させる一方で、実際にはINSERTを行うため、名前が誤解を生みやすいことが理由です。
  • createはinsertの単なるエイリアスであり、insertを直接使えば機能的に全く困らないため、エイリアスを廃止する方向に切り替えています。

実際の変更点 ​

コード差分は小さいですが、主に以下の3点です。

  1. 非推奨の告知を追加

    • activerecord/CHANGELOG.md に「insertのエイリアスであるcreateが非推奨になった」旨が追記されました。
    • 将来のメジャーバージョンで削除される可能性が高いことを示唆します。
  2. データベースステートメントAPIへのdeprecationマーク

    • activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb に、createメソッドの非推奨を示すコメントまたはdeprecate呼び出しが追加されています。

    • 実際の既存コードイメージとしては、概ね以下のような形になっていると考えられます(簡略化例):

      ruby
      module 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本体に依存しますが、趣旨は上記の通りです。

  3. テストコードの修正

    • activerecord/test/cases/adapter_test.rb
    • activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
    • activerecord/test/cases/database_statements_test.rb などで、create呼び出しをinsertに変更し、適宜テスト期待値を調整しています。
    • もともとcreateを使っていたテストは、今後推奨されるAPIであるinsertに置き換えられました。

  1. 影響範囲・注意点

影響を受ける可能性があるコード ​

  • ActiveRecordの低レベルAPIとして、アダプタ経由で直接SQLを発行しているコードのうち、
    • connection.create(...) を使っている箇所
  • 典型例:
    • 自前でSQL INSERTを流しているレイヤー
    • サードパーティgemの中で、アダプタに対してcreateを呼んでいるもの

通常のActiveRecordモデルの Model.create(...) / Model.create! とは別物であり、こちらは今回の変更の対象ではありません。影響を受けるのは、ActiveRecord::Base.connection 系の直接呼び出しだけです。

実務上の対応 ​

  1. コード検索

    • プロジェクト全体で ".create(" ではなく、アダプタの create を使っている箇所を検索します。例えば:

      ruby
      ActiveRecord::Base.connection.create( ... )
      some_connection.create( ... )
  2. insertへの置き換え

    • 以下のようなコードがあれば:

      ruby
      connection.create("INSERT INTO users (name) VALUES ('alice')")

      これを以下のように変更します:

      ruby
      connection.insert("INSERT INTO users (name) VALUES ('alice')")
    • 引数シグネチャもinsertと同じだったため、基本的にはメソッド名の置き換えのみで対応可能です。

  3. 警告の監視

    • Railsのログやテスト実行時に、create is deprecated; use insert instead などのdeprecation warningが出ていないか確認し、見つかった箇所を順次修正していくと安全です。

将来的な削除リスク ​

  • このPRは「非推奨化」であり、即時削除ではありませんが、次のメジャーバージョン(例: Rails 8, 9 等)でcreateメソッド自体が削除される可能性が高いです。
  • レガシーコードや外部gemがcreateに依存していると、そのタイミングでNoMethodErrorになるため、今のうちにinsertへ移行しておくのが無難です。

  1. 参考情報 (あれば)

#58428 Fix LengthValidator crash with proc minimum and nil value ​

マージ日: 2026/8/10 | 作成者: @ousamabenyounes

  1. 概要 (1-2文で)
    ActiveModel::Validations::LengthValidator において、:minimum や :is に Proc を指定し、かつ検証対象の値が nil のときに NoMethodError で落ちるバグを修正した PRです。Proc を使った長さバリデーションでも、常に数値が I18n に渡るようにし、例外なく動作するようになりました。

  1. 変更内容の詳細

問題の挙動 ​

次のように length バリデーションで :minimum に Proc を渡し、対象属性が nil のときに例外が発生していました。

ruby
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

原因は以下のロジックにあります(元コードのイメージ):

ruby
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
end
  • value が 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 の解決タイミングを変更しています。

diff
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) なので挙動は変わらない
  • value が nil で :minimum / :is の場合でも、errors_options[:count] には常に「数値などの完成した値」が入るため、I18n 側に Proc が漏れ出さない

テスト追加 ​

次の 2 パターンの回帰テストが追加されています(nil 値かつ Proc 指定で落ちていたケース):

  • :minimum に Proc を渡した場合
  • :is に Proc を渡した場合

どちらも、修正前は NoMethodError となり、修正後は Active Model のテストスイート全体でエラーなくパスしています。


  1. 影響範囲・注意点
  • 影響を受けるのは、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 内で副作用を起こしたくない、など
    • もっとも、そんな前提で書かれることはほぼ想定されておらず、一般的には安全な変更と考えられます。

  1. 参考情報 (あれば)
  • 対応する Issue: #40642 — LengthValidator が Proc 指定の :minimum / :is と nil 値で NoMethodError になる問題
  • 変更ファイル:
    • activemodel/lib/active_model/validations/length.rb
    • activemodel/test/cases/validations/length_validation_test.rb
    • activemodel/CHANGELOG.md(今回のバグ修正が追記)

この PR により、Proc ベースの動的な長さバリデーションが nil 値を含むケースでも安全かつ一貫して利用できるようになっています。


#58328 Allow ActiveSupport::ProxyLogger to ignore messages by pattern ​

マージ日: 2026/8/10 | 作成者: @federico-carrocera

  1. 概要 (1-2文で)
    ActiveSupport::ProxyLogger に「メッセージ内容でログを無視する」機能が追加され、正規表現や文字列パターンで特定のログだけをサイレンスできるようになりました。これにより、ログレベルを上げずに特定のノイズログのみを抑制できます。

  1. 変更内容の詳細

新機能: ignore パターンによるフィルタリング ​

これまで ProxyLogger は「severity(ログレベル)」でしかフィルタできず、ある gem のうるさいメッセージを消したい場合、ログレベルを上げて他の低レベルログもまとめて捨てる必要がありました。

この PR により、メッセージ内容に応じて無視するパターンを指定できます。

ruby
SomeLibrary.logger =
  ActiveSupport::ProxyLogger.new(Rails.logger)
                            .ignore(/Noisy/)
  • ignore に渡せるもの
    • Regexp … メッセージに対して正規表現マッチ
    • String … メッセージに対してリテラル一致(文字列そのものを含むか、等しいかなど、実装的には Regexp.union でまとめられるので「その文字列を含むか」を見る形)

PR 説明から:

  • 「正規表現はメッセージに対してマッチ」
  • 「文字列はリテラルとしてマッチ」
  • 複数パターンは Regexp.union で 1つの正規表現にまとめてコンパイルされる(登録時に実行)

ProxyLogger 側の挙動は以下の流れになります:

  1. まず従来通り、ログレベル(severity)のチェックを行う
  2. レベル条件を満たした場合にのみ、ignore パターンによるマッチ判定を行う
  3. パターンにマッチしたメッセージは、そのまま破棄され、下流の logger には渡されない

add メソッドのシグネチャ変更 ​

Logger#add の呼び出し方法も変更されています。

  • 以前: def add(...) のように可変長引数 (...) を受けて、元の logger に丸投げ
  • 変更後: Logger#add と同じ明示的なシグネチャを取るように変更

Ruby 標準 Logger の add シグネチャに合わせる形になっています:

ruby
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 は従来通りの挙動(パフォーマンス劣化なし)

  1. 影響範囲・注意点
  • ログフィルタリングがより細かく可能

    • gem や外部ライブラリの「一部のうるさいログだけ」抑制できる
    • 例: 永続的に出る deprecation warning / noisy poll ログなど
  • 既存コードへの互換性

    • ActiveSupport::ProxyLogger を通常通り使っているだけであれば、ignore パターン機能を使わない限り挙動は変わりません。
    • パターンマッチは severity チェック後に行われるため、severity で弾かれるログについて追加のオーバーヘッドは発生しません。
    • ProxyLogger の add を独自に呼び出したり、継承してシグネチャに依存した実装をしている場合は、Logger#add と同じシグネチャに合わせる必要があります。
  • パフォーマンス

    • パターンは登録時に Regexp.union で 1つの正規表現にコンパイルされるため、複数パターン指定時でも実行時のマッチングコストは最小限。
    • ignore パターンを使わない ProxyLogger インスタンスは従来通りで、追加のマッチ処理は行われません。
  • メッセージの解決タイミング

    • ブロック (&block) でメッセージを渡した場合も、パターンマッチのために評価されます。
    • ignore で弾かれた場合は最終的にはログ出力されませんが、「ブロックの評価コスト」自体はかかる点に注意が必要なケースもあります(高コストなログ組み立てを block で遅延している設計など)。

  1. 参考情報 (あれば)
  • 変更ファイル:

    • activesupport/lib/active_support/proxy_logger.rb
      • ignore パターンの追加ロジック、および add シグネチャの更新
    • activesupport/test/proxy_logger_test.rb
      • ignore 機能と後方互換性に関するテスト
    • activesupport/CHANGELOG.md
      • 新機能としてのエントリが追加
  • 利用イメージ(まとめ):

ruby
# 例: 特定の 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. 概要 (1-2文で)
    ActiveModel::Type::Binary に対して不足していたテストが追加され、type/binary?/changed_in_place?/Data#hex などを含めてファイル全体が行カバレッジ100%になりました。挙動変更はなく、純粋にテスト補強のみのPRです。

  1. 変更内容の詳細

このPRで追加されたのは activemodel/test/cases/type/binary_test.rb のテストコード 38行のみです。目的は ActiveModel::Type::Binary および内部クラス Binary::Data の未到達分岐をすべてカバーすることです。

カバーされるようになったポイント ​

  1. type と binary? のテスト

    追加テスト: test_type_and_binary_predicate

    • type メソッドが :binary を返すこと
    • binary? が true を返すこと

    を明示的に検証しています。これにより、Binary 型がフレームワーク内部の「バイナリ型」として正しく識別されることが保証されます。

    イメージ的なコード例:

    ruby
    type = ActiveModel::Type::Binary.new
    assert_equal :binary, type.type
    assert type.binary?
  2. serialize の戻り値の形のテスト

    追加テスト: test_serialize_returns_binary_data

    検証しているポイント:

    • nil をシリアライズすると nil のまま返る
    • nil 以外の値は ActiveModel::Type::Binary::Data でラップされて返る

    想定される振る舞いのイメージ:

    ruby
    type = ActiveModel::Type::Binary.new
    
    assert_nil type.serialize(nil)
    
    data = type.serialize("abc")
    assert_instance_of ActiveModel::Type::Binary::Data, data
  3. cast の Data 分岐のテスト

    追加テスト: test_cast_binary_data_returns_the_underlying_string

    これまで未到達だった分岐:

    ruby
    def cast(value)
      if value.is_a?(Data)
        value.to_s   # <- ここ
      else
        ...
      end
    end

    をテストするために、

    • 一度 serialize して Binary::Data オブジェクトにする
    • それをもう一度 cast に渡す

    というラウンドトリップで Data ブランチを通しています。

    イメージ:

    ruby
    type = ActiveModel::Type::Binary.new
    serialized = type.serialize("abc")   # => Binary::Data
    casted = type.cast(serialized)       # => "abc" (バイナリ文字列)
    assert_equal "abc".b, casted         # 例: エンコーディングも含めて確認

    これにより「Binary::Data を cast すると、中身の生のバイナリ文字列が返る」という仕様がテストで固定されます。

  4. changed_in_place? のテスト

    追加テスト: test_changed_in_place

    changed_in_place? は、値がインプレースで変更されたかどうかを判定するためのフックで、ActiveModel / ActiveRecord の dirty tracking で使われます。

    テスト内容:

    • 「デシリアライズ済みの古い値」と「新しい値」を比較し、
      • 中身が異なれば true
      • 同一なら false
    • となることを確認

    例のイメージ:

    ruby
    type = 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 属性が変更されたかどうかの判定ロジックがテストで保証されます。

  5. Binary::Data#hex と文字列変換のテスト

    追加テスト: test_data_hex_and_string_conversion

    テストしているのは以下:

    • 非UTF-8バイト列に対する Data#hex が期待どおりの16進文字列を返すこと
    • to_s / to_str が元のバイト列を正しく返すこと
    • 生の文字列と Binary::Data の比較が意図通り動作すること

    例えば:

    ruby
    value = "\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進表現で扱うケース、または「生文字列との比較・互換性」がテストで支えられます。


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

    • 実装コードには一切変更がなく、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 も問題なしとのことなので、品質面のリスクはほぼありません。

  1. 参考情報 (あれば)
  • 対象クラス: 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. 概要 (1-2文で)
    Rails 8.1 系で image_processing 2.x を使う際に ruby-vips や mini_magick が未インストールだと、アプリ起動時に例外で落ちてしまう問題を「警告ログを出して処理を続行する」本来の挙動に戻すバックポート対応です。main ブランチに既に入っていた例外メッセージのマッチ条件を 8-1-stable に反映しています。

  1. 変更内容の詳細

問題の発生条件 ​

  • Rails: 8.1.3.1(8-1-stable 系)
  • Gem 構成:
    • gem "image_processing", "~> 2.0"
    • ruby-vips なし、または mini_magick なし

この状態でアプリを起動すると、initializer 実行中 (run_initializers) に以下のような LoadError でアプリが落ちるケースがあります。

text
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:101

Rails 側ではもともと Active Storage エンジン内でこの LoadError を rescue し、**「警告ログを出して起動は継続する」**設計でしたが、次のような齟齬が起きていました。

  • Active Storage エンジンの rescue は、例外メッセージの case マッチで以下のようなパターンしか見ていなかった:
    • /libvips/
    • /image_processing/
  • 一方、image_processing 2.x では ruby-vips / mini_magick をソフト依存にし、不足時には以下のように 不足している gem 名を明示したメッセージ を出すようになった:
    text
    ImageProcessing::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 エンジン側が正しく反応できるようになります。

text
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.

おおまかな処理イメージ(擬似コード) ​

ruby
# 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
end

PR では上記のうち /ruby-vips/ と /mini_magick/ を見る分岐が追加されています(実コードはもう少し細かいですが、意図としてはこのイメージです)。

変更ファイル ​

  • activestorage/CHANGELOG.md
    • 上記挙動の修正が 8.1 系の CHANGELOG に追記されています。
    • ただし main にある「Gemfile と app generator の変更」については、8.1 既存アプリには不要なため触れていません。
  • activestorage/lib/active_storage/engine.rb
    • LoadError を rescue している箇所に /ruby-vips/ と /mini_magick/ のマッチ処理が追加。

  1. 影響範囲・注意点

影響範囲 ​

  • 対象:
    • Rails 8.1 系 (8-1-stable) で Active Storage を利用しているアプリ
    • image_processing 2.x を利用しており、ruby-vips または mini_magick を入れていない環境
  • 挙動の変化:
    • これまではアプリ起動時に LoadError で落ちていたケースが、
      • 起動は成功する
      • ログに「対応する gem が不足している」旨の警告が出る という本来意図されていた挙動に戻ります。

起こりうる実務上の違い ​

  • この PR 導入後も:
    • ruby-vips / mini_magick がない限り、そのバックエンドを使った画像変換は当然できません。
    • ただし「そのバックエンドが使えない」ことは警告レベルに留まり、アプリ全体のブートが失敗することはなくなる、という整理です。
  • 本番環境などで「今まで起動時に落ちて気づけていたものが、警告ログだけになってしまう」ことに注意してください。
    • ただしもともとの設計意図も「ソフト依存なので警告にとどめる」ため、この振る舞いが正です。

テストについて ​

  • image_processing 2.x + 依存 gem 不足 という状態を CI の Gemfile で再現しづらく(現行 Gemfile は image_processing 1.x + ruby-vips 同梱)、自動テストは追加されていません。
  • 手動での再現手順:
    1. Rails 8.1.3.1 のアプリを用意
    2. Gemfile に gem "image_processing", "~> 2.0" を追加
    3. ruby-vips(または mini_magick)を Gemfile から外す
    4. アプリを起動
    • 修正前: 起動時に LoadError で落ちる
    • 修正後: 起動し、ログに警告が出る

  1. 参考情報 (あれば)

#58427 Use bind parameters for array-form arguments in find_by_sql / count_by_sql ​

マージ日: 2026/8/9 | 作成者: @kamipo

  1. 概要 (1-2文で)
    find_by_sql / count_by_sql に配列形式の引数を渡したとき、これまで文字列に埋め込み(sanitize_sql)されていた値を、where と同様にバインドパラメータとして扱うように変更した PR です。これによりドキュメントどおり where と同等の API 挙動になり、SQL インジェクション耐性とプレースホルダ処理が統一されました。

  1. 変更内容の詳細

2-1. これまでの問題点 ​

find_by_sql / count_by_sql は次のような「配列形式」の呼び方ができるとドキュメントに記載されていました:

ruby
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 での配列形式:

ruby
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 などで、配列形式の引数の扱いが次のように変更されています(擬似コード的なイメージ):

変更前 (イメージ)

ruby
def find_by_sql(sql, binds = [])
  # sql が配列形式 ["SELECT ... WHERE id = ?", 1] の場合
  sql = sanitize_sql(sql)  # ここで文字列に埋め込んでしまう
  # ...
  connection.select_all(sql, ...)
end

変更後 (イメージ)

ruby
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 の挙動が一致すること

などを検証していると考えられます。


  1. 影響範囲・注意点

3-1. 既存アプリへの影響 ​

対象: find_by_sql / count_by_sql を配列形式で使っているコード

ruby
# 影響を受けるパターン
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 組み立て」をしていた場合:

ruby
# ※アンチパターン寄りの例
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" など)を渡し、あえて文字列展開させる」というトリッキーな使い方をしていた場合は挙動が変わります。

例:

ruby
# 以前は: "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 アプリでは誤差レベルと考えられます。

  1. 参考情報 (あれば)

この変更後は、「where で使える配列形式 (["col = ?", val]) は find_by_sql / count_by_sql でも同じように“バインドされる”」と考えて差し支えなくなります。


#58425 Stop deserializing column defaults ​

マージ日: 2026/8/9 | 作成者: @byroot

  1. 概要 (1-2文で)
    Rails 8.1.0で導入された「カラムのデフォルト値をあらかじめ型変換(deserialize)する」挙動をやめて、再び「デフォルト値は生の値のまま扱う」方向に戻したPRです。
    これにより、後から属性タイプが上書きされるケースでデフォルト値の型不整合が起きる問題(#58292)を解消しています。

  1. 変更内容の詳細

背景 ​

  • Rails 8.1.0 で、以下の Issue への対応として「カラム定義時にデフォルト値を type_cast(deserialize)しておく」変更が入った:
  • しかしこの変更によって、
    • モデルの attribute type が後から attribute, attribute :foo, :integer のように上書きされる場合や
    • PostgreSQL, SQLite3 などで DB の型とアプリ側の型定義が変わる場合 に、「すでに古い型で変換済みのデフォルト値」が新しい型に変換できずエラーや不整合が発生するようになりました(例: boolean → integer への変更など)。

このPRは、その「カラム定義時にデフォルト値を deserialize する」振る舞いを止め、従来どおり DB から取得した生のデフォルト値を保持し、必要なタイミングで現在の型に応じて変換する動作に戻しています。


実際の変更点の方向性 ​

コード差分は少ないですが、意味合いとしては次のような変更が行われています。

1. ActiveRecord::ConnectionAdapters::Column のデフォルト値の扱いを修正 ​

  • 以前の 8.1.0 変更で、Column が初期化されるタイミングで「型に応じて default を type_cast する」処理が入っていました。
  • このPRで、その事前 type_cast をやめ、カラム定義としては「DB から読み出したままの値」を保持する形に戻しています。

概念的には:

ruby
# 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 テストが追加されています。
    典型的には次のような状況をカバーしていると考えられます:

    ruby
    class 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 テスト用にスキーマ定義を少しだけ変更したものと思われます(型変更などのシナリオを再現するため)。


  1. 影響範囲・注意点

影響するケース ​

  • Rails 8.1.0 以降において、以下のようなコードを書いているアプリで影響が出ていた可能性があります:
    • DB カラム定義の型と、Rails モデル側の attribute type が異なっている(または後から上書きしている)
      • 例: DB: boolean カラム、モデル: attribute :flag, :integer
      • 例: DB: string カラム、モデル: attribute :settings, :json
    • そのカラムにデフォルト値が設定されている
    • 新規レコード作成時に「デフォルト値を起点にした型変換のエラー」や「予期せぬ値」が出ていた

この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 にして、アプリ側で初期値を与える
      といった方針を検討した方がよいです。

  1. 参考情報 (あれば)

#58350 Append TRADITIONAL instead of STRICT_ALL_TABLES to MySQL's sql_mode by default ​

マージ日: 2026/8/9 | 作成者: @kamipo

  1. 概要 (1-2文で)
    Rails が MySQL に接続する際のデフォルト sql_mode 設定を、STRICT_ALL_TABLES 単体ではなく TRADITIONAL を付与するように変更した PRです。これにより、RDS/Aurora などグローバル sql_mode が空の環境でも、ゼロ日付やゼロ除算などの不正値をサイレントに受け入れない、より厳格な動作が一貫して保証されます。

  1. 変更内容の詳細

何を変えたか ​

これまで Rails の MySQL アダプタは、接続時に sql_mode が空または特定条件のときに STRICT_ALL_TABLES を付け足していましたが、この PR で以下のように変更されています。

  • 旧: sql_mode に STRICT_ALL_TABLES を append
  • 新: sql_mode に TRADITIONAL を append

TRADITIONAL は MySQL の「モードセット」で、実質的に以下を含む厳格モードです(主なもの):

  • STRICT_ALL_TABLES
  • NO_ZERO_IN_DATE
  • NO_ZERO_DATE
  • ERROR_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 を組み立てる処理がだいたい次のようなイメージに変わります(あくまで説明用の擬似コード):

ruby
# 変更前のイメージ
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 ベースになることを記載。
  • activerecord/test/cases/adapters/abstract_mysql_adapter/connection_test.rb
    • 接続時の sql_mode に TRADITIONAL が含まれていることを検証するテストを追加。

また、PR 説明にもある通り、以前の挙動(STRICT_ALL_TABLES のみを指定)を再現したい場合は、database.yml で明示的に指定できます。

yaml
production:
  adapter: mysql2
  database: my_app_production
  username: ...
  password: ...
  variables:
    sql_mode: "STRICT_ALL_TABLES"

  1. 影響範囲・注意点

影響を受ける環境 ​

  • 主に以下のような環境で影響が出ます:
    • グローバル sql_mode が空、もしくは非常に緩い設定になっている MySQL サーバ
    • 特に Amazon RDS / Aurora MySQL のデフォルトパラメータグループをそのまま使っている環境

MySQL 5.7+ で、すでにサーバ側の sql_mode を TRADITIONAL 相当か、厳格なモードにしている場合は、この変更による挙動の差分はほぼありません。

具体的に変わる挙動の例 ​

以下のようなケースで、これまでは「通っていた」ものが、今後はエラーになる可能性があります(特にテスト/本番環境で sql_mode が異なる場合に顕在化しやすいです)。

  1. 不正な日付値の挿入
sql
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 が失敗。
  1. ゼロ除算
sql
SELECT 1 / 0;
  • 旧挙動(ERROR_FOR_DIVISION_BY_ZERO が無効)
    • NULL が返ってくるか、警告で済む。
  • 新挙動 (ERROR_FOR_DIVISION_BY_ZERO 有効)
    • エラーとして扱われる。

Rails アプリ側から見ると、これまで普通に通っていた save や update が ActiveRecord::StatementInvalid などの例外を投げるようになる可能性があります。

回避・調整方法 ​

  • 以前と同じ(緩い)挙動を維持したい場合:
    • database.yml で明示的に sql_mode を指定します。
yaml
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: ... } を統一的に指定しておくと環境差分を減らせます。

  1. 参考情報 (あれば)
  • この 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 公式ドキュメント:

#58424 Keep the controller middleware proxy in sync with its stack ​

マージ日: 2026/8/9 | 作成者: @55728

  1. 概要 (1-2文で)
    ActionController::Metal.middleware が返す「ミドルウェア用プロキシ」と実際のミドルウェアスタックの状態がズレていた問題を修正し、常に最新のスタックを正しく反映するようにしたPRです。合わせて、delete の戻り値や middlewares= の挙動など、従来のインターフェイスからズレていた細かい挙動も元の仕様どおりになるよう整えています。

  1. 変更内容の詳細

背景: 問題になっていた挙動 ​

dd67a577d9 以降、ActionController::Metal.middleware は実体のスタックではなく「プロキシオブジェクト」を返すようになりました。
そのプロキシは 生成時点のスタックをキャプチャ しており、読み取り系のメソッド呼び出しもそのキャプチャしたスタックに対して行われていました。

その結果、次のような不整合が起きていました:

ruby
middleware = PostsController.middleware
middleware.use Mw          # ここでスタックのコピーを作ってミューテートする

middleware.size            # => 0 (プロキシ生成時点のスタックを見ている)
PostsController.middleware_stack.size # => 1 (実際には増えている)

他にも、以下の2点の「以前の挙動とのズレ」がありました:

  1. delete が、ラップ対象であるスタックの戻り値ではなく「スタック自身」を返してしまう
    • 元々は「存在しないミドルウェアを削除しようとしたとき nil を返す」仕様だったが、プロキシ化後はこれが崩れていた。
  2. middlewares= がプロキシ経由でなく「スタックそのもの」に直接委譲されていた
    • そのため、凍結済みスタックに対しては FrozenError が発生してしまう状況になっていた (#58342 で是正した他メソッドと同じ問題)。

dd67a577d9 以前は middleware がスタックの実体だったため、上記3つはいずれも期待どおりに動いていました。


このPRでの修正点 ​

1) プロキシが「常に最新のスタック」を見るように変更 ​

これまでは:

  • プロキシ生成時点の @stack をキャプチャし、以降の size などの読み取りもキャプチャしたものに対して行う

これを:

  • プロキシの各メソッド呼び出し時に、常にコントローラの「現在の」スタックを取り直して参照する
    • つまり、プロキシは「スナップショット」ではなく「現在のスタックへの窓口」として振る舞うようになる

そのため、次のようなコードで状態が正しく同期されます:

ruby
middleware = PostsController.middleware
middleware.use Mw

middleware.size                   # => 1
PostsController.middleware.size   # => 1 (同じ状態を見ている)

2) プロキシ経由のメソッドが「スタックの戻り値」を正しく返すよう修正 ​

これまでは、プロキシがミューテーション用のメソッドを「スタックを返す」ような形でラップしており、スタック本来の戻り値が失われていました。
このPRでは:

  • プロキシが対象スタックにメソッドを委譲したとき、元のメソッドの戻り値をそのまま返す ように変更

これにより、たとえば delete の戻り値が元の仕様どおりに戻ります:

ruby
# プロキシ経由
removed = PostsController.middleware.delete(NonExistingMiddleware)
removed # => nil  (存在しない時は nil)

# 元のスタックでも同様の挙動
removed = PostsController.middleware_stack.delete(NonExistingMiddleware)
removed # => nil

3) middlewares= もプロキシ対象に追加 ​

以前は middlewares= がプロキシ経由ではなく、スタックに直接ディスパッチされていたため:

  • 凍結済みスタックに対して middlewares= を呼ぶと FrozenError が発生
  • #58342 で修正した他のミューテーション系メソッド (use など) と挙動が不揃い

このPRで:

  • middlewares= を他のミューテーション系メソッドと同様、プロキシを通して処理する ようにした
  • これにより、middlewares= も「コピーを作って置き換え → 再フリーズ」というパターンになり、スタックのシェア/フリーズ戦略と整合する

サンプルイメージ:

ruby
# 以前: 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):

  1. プロキシ経由でのミューテーションの結果が、同じプロキシを通した読み取りに反映されること
  2. 存在しないミドルウェアを delete した場合に nil が返ること
  3. middlewares= による再設定後も、スタックが「共有可能 (shareable)」な状態であること
    • つまり、スタックのコピー&再フリーズ戦略が壊れていないことを確認

全体として actionpack のフルテストスイート (4278 runs, 20060 assertions) はグリーンです。


  1. 影響範囲・注意点
  • 主に影響を受けるのは、ActionController::Metal.middleware を直接触っているコード および拡張 (エンジン・プラグイン・ライブラリ) です。
    • middleware が返すオブジェクトの挙動が、以前の「実体スタック」準拠に戻るイメージで、dd67a577d9 以降の一時的な挙動からは変わります。
  • 具体的な注意点:
    • delete の戻り値に対して、nil 以外の挙動を期待したワークアラウンドが入っていた場合、それは不要・または壊れる可能性があります。
    • middleware 経由の size などを使っており、以前の「同期していない状態」を前提にしたコードがある場合 (まずないと思われますが)、挙動が変わります。
    • middlewares= を使って独自にスタックを差し替えているコードは、この変更で 凍結/共有戦略と整合的に動くようになる ため、FrozenError をハンドリングしていたコードなどは削減できる可能性があります。
  • APIシグネチャ自体は変わっておらず、「元々のドキュメントどおりの挙動」に近づけるバグ修正なので、通常は後方互換性の観点で有利な変更と考えてよいです。

  1. 参考情報 (あれば)
  • 関連コミット: dd67a577d9
    • ActionController::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. 概要 (1-2文で)
    Rails の ActiveSupport::Cache::MemCacheStore に、dalli が delete_multi をサポートしている場合にそれを使って一度に複数キーを削除する機能が追加されました。これにより memcached へのラウンドトリップ回数が減り、バッチ削除時のパフォーマンスが向上します。

  1. 変更内容の詳細

2-1. delete_multi_entries の追加 ​

MemCacheStore に、複数キー削除用の内部メソッド delete_multi_entries が追加されています。
趣旨としては:

  • dalli クライアントが delete_multi を実装しているならそれを利用する
  • 実装されていない/使えない場合は従来通り 1 件ずつ delete_entry を呼ぶ既存の挙動にフォールバック(※この部分は既存ロジック側)

ポイント:

  • dalli の delete_multi は「キーごとの削除成否」を返さない
  • そのため Rails 側では「削除対象キー数」か「0」のどちらかしか返せない仕様になっている

疑似コードイメージ(実際のコード構造を簡略化):

ruby
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 を返す

という条件分岐が入っています。


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

    • 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 のログ出力を別途確認する必要があります。

  1. 参考情報 (あれば)

#58358 Fix incorrect documentation ​

マージ日: 2026/8/9 | 作成者: @55728

  1. 概要 (1-2文で)
    Active Record 周辺のドキュメント (RDoc と Guides) に、実際の挙動と矛盾する記述や誤ったサンプルコードがあったため、それらを実装に合わせて修正した PR です。コードロジックの変更はなく、ドキュメントのみの修正です。

  1. 変更内容の詳細

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 になることはない。

誤ったイメージの例(※説明用、実際のコード断片ではない):

ruby
connected_to(role: :reading, shard: :shard_one) do
  # 実際には false になるのに true と書かれていた例
  connected_to?(role: :writing, shard: :shard_one) # => true (と誤記)
end

正しい挙動は:

ruby
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 が発生する。

誤ったイメージの例:

ruby
users = User.where(active: true)
users.readonly
users.first.save  # => true (と誤記)

実際に起きること:

  • users.readonly の戻り値を無視すると、users は依然として writable な Relation。
  • しかし、RDoc の文脈上は readonly した Relation を対象に save しているように説明されていたため、読み手に誤解を与えていた。

正しい使い方の例(今回の修正方向):

ruby
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 を返す」のではなく、「例外が発生し、代入自体がロールバック(キャンセル)される」が正しい。

誤解を招く表現のイメージ:

md
If saving the assigned record fails, the assignment statement returns `false`.

正しいイメージ:

ruby
user.profile = profile_params # has_one の例
# 保存に失敗した場合:
# => ActiveRecord::RecordNotSaved が raise される
# user.profile の変更もロールバックされる (代入は成立しない)

ここ 3 箇所の説明を、API ドキュメントおよび 2011 年以降の実際の挙動に合わせて、「false を返す」ではなく「例外 (RecordNotSaved / RecordInvalid) が発生する」という記述に修正しています。


  1. 影響範囲・注意点
  • この PR は ドキュメントのみの変更 であり、挙動・API の追加・非互換変更は一切ありません。
  • ただし、これまで誤ったドキュメントを前提にコードを書いていた場合、以下の点で思い込みが修正される必要があります:
    1. connected_to?
      • role か shard のどちらか片方だけ一致していても true にはならない。両方一致して初めて true。
    2. Relation#readonly
      • インスタンスメソッドチェーンで Relation を変更するときは、users = users.readonly のように戻り値を必ず使う。
    3. 関連付け代入時のエラー処理
      • user.profile = profile などの関連代入が失敗したときに false 判定で分岐するコードは間違い。
        • 正しくは例外 (ActiveRecord::RecordNotSaved / RecordInvalid) を rescue して処理する。
        • 代入後の関連オブジェクトの状態がロールバックされることを前提にする。

  1. 参考情報 (あれば)
  • 関連クラス/メソッド:
    • ActiveRecord::ConnectionHandling#connected_to
    • ActiveRecord::ConnectionHandling#connected_to?
    • ActiveRecord::Relation#readonly
    • ActiveRecord::RecordNotSaved
    • ActiveRecord::RecordInvalid
  • 実際の挙動を確認したい場合は、connection_handlers_sharding_db_test.rb や Association 関連のテストを参照すると、今回修正された説明と同じ前提でテストが書かれていることが分かります。

#58420 Refactor MiddlewareStack::Middleware off Hash.ruby2_keywords_hash ​

マージ日: 2026/8/9 | 作成者: @kamipo

  1. 概要 (1-2文で)
    Rails のミドルウェアスタック内部実装から Hash.ruby2_keywords_hash への依存を取り除き、Ruby 3系以降のキーワード引数仕様に素直に従う形へリファクタリングした PR です。併せて、Middleware / InstrumentationProxy クラスを内部実装扱いとしてドキュメント非公開 (:nodoc:) にしつつも、デバッグ用途の情報は維持しています。

  1. 変更内容の詳細

背景: 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 を利用していました。

以前のイメージ(概念的なコード例) ​

ruby
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)
  • インスタンス生成時は「位置引数 + キーワード引数」を明示的に展開して呼び出し

概念的には次のような形です:

ruby
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
end

ActionController 側の Middleware サブクラスも同様の方針に合わせて修正されており、
ミドルウェアの生成時に位置引数 / キーワード引数が明確に分離されるようになりました。


API ドキュメンテーション上の扱い変更 (:nodoc:) ​

以下の2クラスが :nodoc: 指定され、公式 API ドキュメントから隠されています。

  • ActionDispatch::MiddlewareStack::Middleware
  • ActionDispatch::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 に依存しなくなった」ことが分かるようになっています。

  1. 影響範囲・注意点

通常のアプリケーション開発者への影響 ​

  • 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 への依存をやめることが推奨されます。

  1. 参考情報 (あれば)

#58239 Refactor CommandRecorder to store args and kwargs separately ​

マージ日: 2026/8/9 | 作成者: @kamipo

  1. 概要 (1–2文で)
    ActiveRecord::Migration::CommandRecorder が、記録するコマンドの引数を「位置引数」と「キーワード引数」に分けて保持する形にリファクタリングされました。これにより、ruby2_keywords 時代の互換レイヤを廃止し、Ruby 3 系の kwargs 仕様に素直に従う実装になっています。

  1. 変更内容の詳細

2-1. recorder.commands の構造変更 ​

これまで:

ruby
# commands の 1 要素は 3 要素タプル
[cmd, args, block]

# かつ kwargs は args の末尾の Hash に埋め込まれていた
[:create_table, ["users", { if_not_exists: true }], nil]

これから:

ruby
# 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 引数として別に渡されます。

旧コード例:

ruby
recorder.commands.each do |name, args, block|
  options = args.extract_options! # または args.last.is_a?(Hash) ? args.pop : {}
  # ...
end

新コード例:

ruby
recorder.commands.each do |name, args, kwargs, block|
  # args と kwargs が明確に分かれている
end

2-2. invert_* 系ヘルパのシグネチャ変更 ​

マイグレーションの「可逆性」を実現するための invert_* メソッド類が、kwargs を明示的に受け取る形に統一されています。

例: invert_add_column(イメージ)

変更前(概念的には)

ruby
def invert_add_column(table_name, column_name, type, *args)
  options = args.extract_options!
  # options の中に null: false, default: ... などが入る
end

変更後

ruby
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 行の変更)。

  1. 影響範囲・注意点

3-1. 最も重要な互換性ポイント ​

recorder.commands の形式に依存したコードは必ず壊れます。

具体的には、以下のようなコードがある場合は要修正です。

NG(旧形式前提):

ruby
recorder.commands.each do |name, args, block|
  options = args.last.is_a?(Hash) ? args.last : {}
  # ...
end

OK(新形式対応):

ruby
recorder.commands.each do |name, args, kwargs, block|
  # kwargs をそのまま使う
end

あるいは、どうしても古い/新しい両方に対応したい Gem 側などでは、以下のようなガードが必要になります:

ruby
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 で扱う
end

3-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 に直接触る形に変更する必要があります。

  1. 参考情報 (あれば)
  • 当該 PR: Refactor CommandRecorder to 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. 概要 (1-2文で)
    Ractor セーフモード時に ActionController::Metal.middleware の一部のミドルウェア操作メソッドが凍結済みスタックを直接変更しようとして FrozenError になる不具合を修正し、他の変更系メソッドと同様に「コピーして再凍結」されるように統一した PR です。Ractors 対応(ActiveSupport::Ractors.unshareable_proc_action 有効時)のミドルウェアスタック操作が一貫して安全に動作するようになります。

  1. 変更内容の詳細

背景 ​

ActionController::Metal.middleware が返すのは、実体のミドルウェアスタック (ActionDispatch::MiddlewareStack) に対するプロキシ (ActionController::MiddlewareStack::Proxy) です。

Ractor セーフモード (ActiveSupport::Ractors.unshareable_proc_action が :raise など) では、
このスタック内部の配列を Ractor 間で共有可能にするために「一度凍結」し、
変更要求が来たときは:

  1. 凍結されたスタックをコピーする
  2. コピー側に対して変更を行う
  3. コピーを再度「共有可能(shareable)」にする(凍結等)

という動作を行う必要があります。

そのため Proxy は「ミドルウェアスタックを破壊的に変更するメソッド」を列挙しておき、それらが呼ばれたときだけ「コピーして再凍結」のラッパを噛ませる設計になっています。

問題点 ​

しかし、その「破壊的変更メソッドの列挙」に以下 4 メソッドが漏れていました:

  • insert_before
  • insert_after
  • delete!
  • move_before (実体は move のエイリアス)

このため、これら 4 つは delegate_missing_to 経由で素の ActionDispatch::MiddlewareStack に直接委譲され、凍結されている配列をそのまま変更しようとして FrozenError が発生していました。

再現例(PR 説明より):

ruby
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 Array

use は「列挙済みの mutator(変更系メソッド)」なのでコピー&再凍結のラッパが働く一方、
insert_before は列挙から漏れていたため、凍結済み配列に対する直接操作になり失敗していました。

この PR の対応 ​

  • ActionController::MiddlewareStack::Proxy 内の「ミドルウェアスタックの変更系メソッドのリスト」に、漏れていた 4 メソッド:
    • insert_before
    • insert_after
    • delete!
    • move_before
      を追加。
  • これにより、他の mutator と同様に:
    • 呼び出しごとにスタックのコピーを作成
    • そのコピーに対して変更を適用
    • コピーを再度 Ractor 共有可能な状態(凍結)にする という処理が行われるようになります。

テスト (actionpack/test/controller/new_base/middleware_test.rb) も追加され、
Ractor セーフモードでこれらメソッドを呼んでも FrozenError にならないことを確認しています。

動作の Before/After ​

  • Before (Ractor セーフモード時):
    • use や、すでに列挙済みの mutator は OK
    • insert_before, insert_after, delete!, move_before は FrozenError 発生
  • After:
    • 上記 4 メソッドも含め、すべての mutator が
      「コピー → 変更 → 再凍結」ルールに従って安全に動作

  1. 影響範囲・注意点
  • 影響を受けるケース

    • 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 に依存したワークアラウンド等を書いていた場合は、その挙動は変わりますが、通常は期待されない使い方です。

  1. 参考情報 (あれば)
  • 対象クラス/モジュール:
    • ActionController::MiddlewareStack::Proxy
    • ActionDispatch::MiddlewareStack
  • 関連する設定:
    • ActiveSupport::Ractors.unshareable_proc_action
      • :raise 等に設定した際に、ミドルウェアスタック配列が Ractor 共有可能にするため凍結される
  • 本挙動は dd67a577d9 で導入された「Proxy 経由でスタックにメソッドを呼ぶときは、常に共有可能なコピーを返す」という設計の抜け漏れの修正です。

#58217 Remove unused Active Record internals ​

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

  1. 概要 (1-2文で)
    このPRは、Active Record 内部で一切使われていないメソッドや引数・インスタンス変数などの「死んだコード」をまとめて削除するクリーンアップです。挙動の変更や外部APIの破壊的変更は基本的になく、内部実装の整理・単純化が主目的です。

  1. 変更内容の詳細

それぞれ「どこで使われなくなったか」がコミット履歴ベースで確認されており、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 は呼ばれなくなっていました。

ruby
# 以前はこういったメソッドがあった(イメージ)
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 引数が削除
    • メソッド内部でも使われていなかったため、完全に参照がなくなる
ruby
# 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 はコード内で使われておらず、呼び出し側も渡していない
ruby
# 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? から参照されるため、トランザクションの外部的な振る舞いは変わりません。

ruby
# イメージ
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 はどこからも読み出されておらず、「書き込むだけで使われない」変数でした。
ruby
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 のインスタンス変数はクラス継承では引き継がれないので、そもそもここでのリセット自体意味が薄い
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_options
    • ThroughReflection#through_options

has_many :through などで使われるリフレクションの内部ヘルパでしたが、過去のリファクタリング(該当コミット: 500b1df43e / 65843e1acc)で最後の呼び出し元が削除済みで、現在は完全に未使用でした。

ruby
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 で直接メモをリセットしており、セッターは使っていない
ruby
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 をオーバーライドしても、そもそも一度も呼ばれない
ruby
class AbstractAdapter
  # def build_result(columns, rows)
  #   # アダプタが独自の結果オブジェクトを返すためのフック(という位置づけだった)
  # end
end

PR 説明では「今でもこのメソッドを extension point として残したいのであれば、このコミットは落としてもよい」と言及されていますが、最終的には削除されています。

ポイント:

  • 外部アダプタが build_result を実装していたとしても、実際にはすでに呼ばれていなかったため、「メソッドが消える」以外の実害はありません(挙動は元から変わっていない)。
  • ただし、AbstractAdapter を継承した独自アダプタで super 呼び出しなどをしていると NoMethodError になる可能性があります。

  1. 影響範囲・注意点

基本方針:

  • すべて「フレームワーク内部で参照されていない」ことが確認されたコードの削除です。
  • 公式に公開されたAPIではなく、主に内部クラス・メソッド・引数が対象です。
  • そのため、通常のアプリケーションコードに対する影響はほぼゼロと考えられます。

注意が必要なケース(拡張・メタプログラミングをしている場合):

  1. 内部クラスに直接依存している場合

    • 例: ActiveRecord::ConnectionAdapters::AbstractAdapter#build_result を独自アダプタでオーバーライドしていた
    • 例: ThroughReflection#source_options / through_options を呼んでリフレクション情報を解析していた
    • 例: HashLookupTypeMap.new(some_parent) のように引数付きで初期化していた
  2. private / internal メソッドを send で呼んでいた場合

    • PR 側でも send やシンボル参照まで含めて grep されているため、Rails 本体では未使用と確認されていますが、アプリやgemが同様の手法で呼び出していると壊れます。
  3. シグネチャ変更

    • try_to_queue_for_background_connection(checkout_timeout) → 引数なし
      自前のサブクラス・モンキーパッチで呼び出している場合は修正が必要です。

実質的な挙動の変更:

  • いずれの削除も「参照されていないコード」の削除に留まっており、Rails が発行するSQL、コネクションプールの動作、トランザクションの扱いなど、ユーザが観測可能な挙動は変わらないよう設計されています。
  • 既存のテストは、nullify! のテスト削除以外は、動作変更を伴う形で壊れていないことが前提です。

  1. 参考情報 (あれば)
  • このPR自体は、以下の「未使用コードクリーンアップ」PR群のフォローアップとして位置づけられています:
    • #58214
    • #58197
    • #58208
  • 変更の根拠となるコミット(どの時点から未使用になったか)が詳細に示されているため、もし「このメソッドに依存していたかも」と感じた場合は、該当するコミットを辿ることで設計変更の流れを追えます。
  • 依存している可能性をチェックしたい場合:
    • アプリケーションおよび社内gemで build_result, ThroughReflection#source_options, HashLookupTypeMap.new( などの文字列検索を行うと影響をざっと洗い出せます。

#58361 Fix TimeWithZone#to_s raising in a non-main Ractor ​

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

  1. 概要 (1-2文で)
    ActiveSupport::TimeZone が Ractor 共有可能になっていたにもかかわらず、非メイン Ractor で TimeWithZone#to_s などを呼ぶと Ractor::IsolationError が発生していた問題を修正した PRです。内部で使っているタイムゾーンオフセット用の文字列を凍結し、Ractor 間で安全に共有できるようにしています。

  1. 変更内容の詳細

問題の背景 ​

  • 以前のコミット 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.rb

    • UTC_OFFSET_WITHOUT_COLON を凍結 (freeze) するように変更。
    • これにより、この定数はイミュータブルになり、Ractor 間で安全に共有可能になります。
  • activesupport/test/time_zone_test.rb

    • 上記問題が再発しないように、非メイン Ractor で TimeWithZone#to_s などを呼び出すテストを追加。

    • 具体的には、おおよそ以下のようなテストシナリオが追加されています(イメージ):

      ruby
      Time.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 説明より) ​

修正前:

ruby
Time.zone = "Eastern Time (US & Canada)"
Ractor.new(Time.zone.local(2000, 1, 1)) { |t| t.to_s }.value
# => Ractor::IsolationError

修正後:

ruby
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"

  1. 影響範囲・注意点
  • 影響範囲
    • 非メイン Ractor 内で ActiveSupport::TimeWithZone オブジェクトの以下メソッドを呼ぶ処理:
      • #to_s
      • #rfc2822
      • #to_fs(:rfc822) (旧 to_s(:rfc822) 相当)
    • 上記が Ractor::IsolationError で落ちていたケースが正常に動作するようになります。
  • 既存コードへの影響
    • 変更は定数文字列を freeze しただけで、フォーマット結果の文字列内容やパブリック API には変更がありません。
    • マルチ Ractor を使っていないアプリケーションには挙動の変化はありません。
  • 注意点
    • この PR は 「TimeZone/TimeWithZone が Ractor 安全に近づいた」修正ですが、Rails の他部分が Ractor 完全対応とは限りません。
    • Ractor を本番で使う場合は、他の gem / ライブラリやアプリケーションコードも含めて Ractor.shareable? や Ractor::IsolationError を確認する必要があります。

  1. 参考情報 (あれば)

#58416 Use ActiveRecord::Key.for in remaining Array(reflection.*_key) sites ​

マージ日: 2026/8/8 | 作成者: @kamipo

  1. 概要 (1-2文で)
    Rails の ActiveRecord 内で、関連(reflection)のキーを配列化して扱っていた箇所を Array(...) から ActiveRecord::Key.for(...) に統一し、単一キーと複合キーを同じ Enumerable API で扱えるようにリファクタリングした PR です。機能追加ではなく、#58368 に続く内部実装の揃え・複合主キー対応を見越した整備です。

  1. 変更内容の詳細

何をしたか ​

  • 以下のファイルで、reflection 由来のキー名を配列化してループしていた箇所をすべて ActiveRecord::Key.for(...) に置き換え
    • associations/association.rb
    • associations/association_scope.rb
    • associations/foreign_association.rb
    • associations/has_one_association.rb
    • autosave_association.rb
    • relation/query_methods.rb
  • 行数としては +24 / -24 で、ほぼ 1:1 の API 置き換え (挙動を変えるというより手段の統一)

具体例イメージ ​

これまで典型的にやっていたコード:

ruby
Array(reflection.foreign_key).each do |key|
  # 単一 or 複合キーを意識せずに処理したい
end

を、以下のように変更:

ruby
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 が期待するキー表現」に正規化する責務を持つクラスメソッドになっていると考えられます。


  1. 影響範囲・注意点

影響範囲 ​

  • 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 的なもの) と組み合わせている場合
  • これらのケースでは:
    • 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 や挙動変化が出ていないか)

  1. 参考情報 (あれば)
  • 対応元 PR: #58368
    • ActiveRecord::Key 導入および、一部 call site をそちらに切り替えた最初の PR と考えられます。
  • 本 PR #58416 は、そのフォローアップとして、
    残っていた Array(reflection.*_key) 呼び出しをすべて ActiveRecord::Key.for に寄せる「仕上げ」のリファクタリングです。
  • 将来の「正式な複合キーサポート」や、キー周りの内部表現の整理に向けた準備として読むと理解しやすい変更です。

#58396 Eager load templates ​

マージ日: 2026/8/8 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    このPRは、Rails起動時にテンプレートを「先読み(eager load)」して共有可能にする仕組みを Action View に導入したものです。まずは strict locals なテンプレートのみを対象とし、テンプレートのコンパイル先コンテナの不整合を明示的に検出する安全チェックも追加されています。

  1. 変更内容の詳細

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.rb
  • actionview/lib/action_view/unbound_template.rb
  • actionview/lib/action_view/template/resolver.rb
  • actionview/lib/action_view/template/sources.rb

主なポイント:

  1. strict locals テンプレートのコンパイルキャッシュの扱いを改善

    • UnboundTemplate が保持していた @templates キャッシュを strict locals では 1つの Template オブジェクトに集約し、Ractor 間で共有しやすい形にしています。
    • これにより、Ractor 化された環境(ractorize! 有効時など)でのテンプレート再コンパイルやメモリ・CPU コストを抑えられるようになります。
  2. resolver に eager load 経路を追加

    • ActionView::Template::Resolver に、テンプレートを事前にロード・コンパイルするためのメソッドやフローが追加されています。
    • ファイルシステムベースの resolver (file_system_resolver_test.rb が増えている) を使って、起動時にディスク上のテンプレートファイルを走査し、コンパイル済みテンプレートをキャッシュしておくことができるようになります。
  3. 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 をオーバーライドして別のコンテナを持つクラスを使っている」などのケースで、問題の原因がすぐ分かるようになります。

具体例になりやすい状況:

ruby
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 (典型的なケース)

しかし、例えばあるコントローラで:

ruby
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 間で使い回せることを担保する意図があります。

  1. 影響範囲・注意点

  2. strict locals テンプレートを使っているアプリ

    • 起動時にテンプレートを eager load する設定を有効にすると、strict locals テンプレートはコンパイル済み状態でアプリ起動後すぐ利用されます。
    • メリット:
      • 初回アクセス時のコンパイルオーバーヘッドが減る。
      • Ractor 使用時のパフォーマンス・メモリ効率、および安定性が向上。
    • テンプレートの定義ミス(locals の宣言と実際に渡している値の不整合など)は、起動時に早めに検知される可能性が高まります。
  3. view_context_class をカスタマイズしているアプリ/エンジン

    • view_context_class をオーバーライドしており、その compiled_method_container が ApplicationController / ApplicationMailer などデフォルトとは異なる場合、新しい安全チェックにより、これまで黙って NoMethodError になっていた箇所が、より明示的な例外に変わります。
    • もしこのエラーが発生した場合は:
      • view_context_class を見直し、テンプレートを共有したいクラス間で同じ compiled_method_container を使うようにする。
      • あるいは、コンテナ違いを前提にテンプレートを別々にコンパイルする設計にする(多くの場合は前者が望ましい)ことを検討してください。
  4. Ractor を有効にしている環境 (ractorize!)

    • 今回の変更で、strict locals テンプレートが共有しやすい形に整理されたため、Ractor 間のテンプレート共有が現実的になります。
    • ただし non-strict テンプレートはまだ follow-up PR での対応予定であり、現時点では Ractor ローカルキャッシュでの最適化は strict locals のみが対象です。
  5. 互換性

    • 基本的には後方互換を維持しつつ、安全性とパフォーマンスを高める変更です。
    • 互換性上の変化は「これまで曖昧な NoMethodError が出ていたケースで、より明示的な例外が出るようになった」点にほぼ限られます。

  1. 参考情報 (あれば)

#58415 Remove symbol_column_to_string in favor of columns_hash + Symbol#name ​

マージ日: 2026/8/8 | 作成者: @kamipo

  1. 概要 (1-2文で)
    symbol_column_to_string というヘルパーとそのキャッシュを削除し、代わりに columns_hash(クラスレベルでメモ化されたハッシュ)と Ruby 3.0 以降の Symbol#name を直接使うようにする変更です。これにより、シンボルを使ったカラム参照で追加のキャッシュ構造が不要になり、コードが簡潔かつ同等のパフォーマンスを維持します。

  1. 変更内容の詳細

背景 ​

  • 以前のPR (#34197) で、respond_to? にシンボルを渡した際に**文字列オブジェクトを新規生成しない(allocation-free)**ようにするため、以下のような仕組みが導入されていました:

    • カラム名シンボル :title などをキーに、対応する文字列 "title" を値に持つ { sym => str } のハッシュをメモ化(symbol_column_to_string)
    • これにより respond_to?(:title) 等のたびに文字列を生成せずに済む
  • 一方で Active Record には既に columns_hash という、
    {"title" => #&lt;ActiveRecord::ConnectionAdapters::Column ...>}
    という形の凍結済み・クラスレベルでメモ化されたハッシュが存在しており、これ自体が O(1) でのカラム検索を提供します。

  • Ruby 3.0 以降では Symbol#name によって、新しい String を生成せずに内部的にインターンされた文字列を取得できます("title" に相当する文字列表現を割り当てなしで得られる)。

このPRでの主な変更点 ​

  1. symbol_column_to_string ヘルパー削除

    • activerecord/lib/active_record/model_schema.rb から symbol_column_to_string の定義とそれに関連するキャッシュスロットのコードが削除されています(-8行)。
    • これにより「{sym => str} キャッシュ」を使った独自の変換ロジックは無くなります。
  2. 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)
      end
    • columns_hash 自体はクラスレベルでメモ化されており、これを直接使うことで、以前の {sym => str} キャッシュと同等の O(1) ルックアップと allocation-free な変換を実現します。


  1. 影響範囲・注意点

パフォーマンス / メモリ ​

  • ねらい
    • 追加の {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 をサポートする独自フォークなどでは注意が必要)。

  1. 参考情報 (あれば)
  • 本PR: #58415
    タイトル: Remove symbol_column_to_string in favor of columns_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. 概要 (1-2文で)
    ActionDispatch::Request#media_type のドキュメント内サンプルが、実際の挙動と一致していなかったため、正しいリクエスト例(POST+ボディ付き)に修正したドキュメント専用のPRです。コードの挙動自体は一切変更されていません。

  1. 変更内容の詳細

何が問題だったか ​

元のドキュメントでは、次のように GET リクエストの例が書かれていました:

ruby
# 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 リクエストの例に差し替えられました:

ruby
# 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行のみで、コードロジックには一切手が入っていません。


  1. 影響範囲・注意点
  • 挙動変更は一切なく、「ドキュメントのサンプルが正しくなった」だけです。
  • これにより、以下の点が明確になります:
    • GET リクエスト(かつ Content-Type なし)では request.media_type は nil を返す。
    • media_type に値が欲しい場合は、ボディと Content-Type を伴うリクエスト(例: フォームPOST、JSON POSTなど)である必要がある。
  • 既存アプリケーションやテストコードを変更する必要はありませんが、
    • 「GET でもデフォルトで application/x-www-form-urlencoded が入るはず」と思い込んでいた場合は、その前提が誤りであることを認識する必要があります。

  1. 参考情報 (あれば)
  • 該当メソッド: ActionDispatch::Request#media_type
    • 実際には Content-Type ヘッダから MIME タイプ部分だけを取り出した値("application/json" など)を返し、ヘッダがない場合は nil を返します。
  • 関連テスト: test/dispatch/request_test.rb:1109
    • 「Content-Type のないリクエストでは request.media_type が nil である」ことを検証。
  • 挙動が変わった元コミット(言及されているもの): 86754a8f7b
    • デフォルトのリクエストエンコーダが Content-Type を付けなくなったことにより、古いドキュメント例が不正確になっていた経緯があります。

#58384 Allow config.active_storage.variant_processor to be set to a class ​

マージ日: 2026/8/7 | 作成者: @flavorjones

  1. 概要 (1-2文で)
    Active Storage の config.active_storage.variant_processor に、これまでの :vips / :mini_magick / :disabled というシンボルだけでなく、任意のクラスを直接指定できるようにした PR です。これにより、独自の画像変換パイプラインやサンドボックス化された変換処理などを、Rails の設定レベルで差し替えられるようになります。

  1. 変更内容の詳細

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 で次のようにシンボルをクラスにマッピングしていました:

ruby
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 に クラスを直接指定 できるようになりました。

ruby
# 例: カスタムのトランスフォーマークラスを使用
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.md
    • config.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 にも、挙動変更・新機能として記載あり。

  1. 影響範囲・注意点

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 = CustomTransformer
    のようにカスタムクラスを指定した場合は、config.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 経由で)、テストで異常系を検証している場合は、挙動が「実行時エラー → 起動時エラー」に変わっている点に注意が必要です。

  1. 参考情報 (あれば)
  • 対象 PR: Allow config.active_storage.variant_processor to be set to a class #58384
  • 変更された主なファイル:
    • activestorage/app/models/active_storage/variant.rb
    • activestorage/lib/active_storage/engine.rb
    • activestorage/lib/active_storage/transformers/transformer.rb
    • guides/source/configuring.md
    • railties/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. 概要 (1-2文で)
    Active Record 内で一部のインスタンス変数が Ractor(マルチスレッド並列実行用の仕組み)上で安全に参照できない問題を解消し、Ractor でも Active Record を扱えるようにするための「Ractor セーフティ」対応 PR です。特に、メモ化済みのインスタンス変数を freeze したり、必要に応じて main Ractor に処理を委譲する形に変更しています。

  1. 変更内容の詳細

背景・問題点 ​

  • Ractor 間でオブジェクトを共有するには「共有可能(shareable)」である必要があり、その代表的な条件が「freeze されていること」です。
  • Active Record 内では、パフォーマンスのためにいくつかの値をインスタンス変数にメモ化していますが、これらが非 frozen のまま保持されていると、main Ractor 以外でアクセスしたときに Ractor セーフではなくなります。
  • また、メモ化が eagerly(早期)に実行されると、そのタイミングの Ractor に依存したままになり、他 Ractor からのアクセスで問題が生じ得ます。

この PR は、そういったメモ化インスタンス変数の扱いを見直し、

  • 代入時に freeze する
  • 場合によっては main Ractor で初期化したものを共有するようにする

ことで、Ractor セーフティを確保しています。


対象となったインスタンス変数 ​

説明文ベースで、次のインスタンス変数の扱いが変更されています。

  1. @symbol_column_to_string_name_hash

    • シンボルカラム名と文字列カラム名の対応をキャッシュしているハッシュと思われます。
    • 代入時に値を freeze し、Ractor 間で共有しても問題ない immutable な状態にします。
  2. @_returning_columns_for_insert

    • INSERT 時に RETURNING 句で返すカラム情報をキャッシュしていると思われます。
    • これも代入時に freeze されるようになり、Ractor 上から参照可能になります。
  3. @query_constraints_list

    • モデルに対する制約(デフォルトスコープ・シングルテーブル継承等からくる種々の制約)をキャッシュした配列/構造体である可能性が高いです。
    • 代入時に freeze されるので、後から破壊的変更をしない前提で Ractor セーフに。
  4. @table_name

    • モデルのテーブル名のキャッシュ。
    • Ractor 間で共有される前提なので、ここも freeze されます。

これらはいずれも「一度計算したら以降は変更されないことが前提の値」であり、freeze によってその前提を Ruby レベルで明示しつつ、Ractor の shareable 要件も満たす形になっています。


Arel / PredicateBuilder 周り ​

説明文中で、次の 2 つのインスタンス変数についても対応が入ったとされています。

  • @predicate_builder(型: ActiveRecord::PredicateBuilder)
  • @arel_table(型: Arel::Table)

変更点のポイント:

  1. Arel::Table の freeze 対応

    • activerecord/lib/arel/table.rb が変更され、Arel::Table インスタンスが freeze された状態でも安全に扱えるようになっています(イミュータブル想定を強化した形)。
    • Ractor 間で @arel_table を共有するには、このオブジェクト自体が shareable である必要があり、そのために freeze 可能な設計に近づけています。
  2. PredicateBuilder の freeze 対応

    • activerecord/lib/active_record/relation/predicate_builder.rb が変更され、ActiveRecord::PredicateBuilder も freeze されても問題なく使えるように手当てされています。
    • メモ化された @predicate_builder を freeze して Ractor 間共有に耐えられるようにしたものと考えられます。
  3. メモ化の「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.rb
  • activerecord/test/cases/base_test.rb
  • activerecord/test/cases/primary_keys_test.rb
  • activerecord/test/cases/relation/predicate_builder_test.rb
  • activerecord/test/cases/arel/helper.rb

主に以下を確認するテストが追加されていると考えられます。

  • Arel::Table や ActiveRecord::PredicateBuilder が freeze された状態で正しく動作すること
  • 対象のインスタンス変数が freeze されていること(frozen? の確認)
  • Ractor 環境(もしくはそれを模した状況)で例外が発生しないこと

  1. 影響範囲・注意点

影響範囲 ​

  • Active Record の内部キャッシュ・メモ化周り(テーブル名、クエリ制約、Arel 関連オブジェクトなど)。
  • Ractor を利用している/利用しようとしている Rails アプリケーションにポジティブな影響があります(Ractor 上で Active Record を使いやすくなる)。

注意点(アプリケーション側) ​

  1. freeze 前提の設計になる

    • @table_name や @arel_table など、Active Record 内部のインスタンス変数にアプリケーション側から直接手を入れている場合(Monkey patch やメタプログラミングで上書きしているなど)、
      • freeze による FrozenError
      • Ractor shareable 制約
        に引っかかる可能性があります。
        通常の使い方(Active Record の公開 API のみ使用)であれば問題は出ない想定です。
  2. 破壊的変更を前提にしないこと

    • Arel オブジェクトや PredicateBuilder の内部構造へ直接アクセスし、破壊的変更(<<, push, merge!, []= など)を行っているようなコードは、freeze 後にエラーになります。
    • そのようなコードがある場合は、
      • 新しいオブジェクトを生成して返す
      • あるいは Ractor 非対応前提のままにする
        といった見直しが必要です。
  3. Ractor を使う場合の前提整理

    • この PR は「Active Record の特定のメモ化 ivar を Ractor セーフに近づけた」ものであり、Active Record 全体が完全に Ractor 対応済みになったことを意味するわけではありません。
    • Ractor で Rails / Active Record を本格的に使う場合は、他の部分(Logger、Cache、Config オブジェクトなど)についても shareable / thread-safe かどうかを検証する必要があります。

  1. 参考情報 (あれば)
  • Ruby Ractor 概要:
  • 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. 概要 (1–2文で)
    Rails の ERB テンプレートハンドラの各種オプションを、ActionView::Base(および config.action_view.*)経由で公式に設定できるようにした PR です。これにより、これまで非公開扱い(:nodoc:)となった ActionView::Template::Handlers::ERB クラスを直接参照して行っていた設定を、公開インターフェイスから行えるようになります。

  1. 変更内容の詳細

背景 ​

  • 過去のコミット 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 から全部できるようにする」という方針の整理です。

具体的な変更点 ​

  1. 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 に伝搬させる形になっています。

  2. ActionView::Railtie の設定連携の見直し

    • actionview/lib/action_view/railtie.rb で、config.action_view と ActionView::Base の設定同期部分が 1 行修正されています。
    • これにより、config.action_view.<erb関連の設定> に値を入れると、自動的に ActionView::Base に反映され、その結果 ERB ハンドラのオプションに正しく渡るようになります。
  3. テストの追加

    • actionview/test/actionpack/abstract/abstract_controller_test.rb に 56 行分のテストが追加。
      • ActionView::Base を通して設定した ERB オプションが実際のテンプレートレンダリングに反映されるかを検証。
    • railties/test/application/configuration_test.rb も調整され、config.action_view → ActionView::Base → ERB ハンドラ、という設定ルートが期待通り動作することを確認。
  4. ドキュメントと CHANGELOG の更新

    • guides/source/configuring.md に、ERB 関連の設定を config.action_view から行う方法が追記。
    • actionview/CHANGELOG.md に、この機能追加が明示的に記載され、外部向けに「ERB オプションを ActionView 経由で設定できるようになった」ことが周知されています。

  1. 影響範囲・注意点
  • 推奨される使用方法の変化

    • これまで 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 の挙動(トリムモード・エスケープ戦略など)を切り替えるのが容易になります。

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/58359
  • 関連コミット: 970bf380fe8 (ActionView::Template::Handlers::ERB に :nodoc: が付いた変更)
  • 設定ガイド(本 PR で更新): guides/source/configuring.md
    • config.action_view セクション内に ERB 関連オプションの説明が追加されているので、具体的なオプション名と使い方はそこを参照するのが確実です。

#58060 Ractor compatible Active Support Notifications ​

マージ日: 2026/8/7 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    ActiveSupport::Notifications を Ractor 内でも安全に利用できるようにするため、Ractor ごとに独立した Notifier を持つ仕組みに変更した PRです。既存の AS::Fanout を直接 Ractor-safe にするのではなく、「購読情報のスナップショット」をもとに Ractor 単位で Notifier を生成する設計になっています。

  1. 変更内容の詳細

背景と課題 ​

  • これまで 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 の要件)。

イメージ(擬似コード):

ruby
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 で確定されたスナップショット」
      という構成になります。

擬似コード:

ruby
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
end

3. 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 対応によって挙動が変わる部分(特にデフォルトスコープや通知周りのタイミング)がテストに影響しており、それに合わせて期待値やテスト構造を微調整していると思われます。

  1. 影響範囲・注意点

影響範囲 ​

  • ActiveSupport::Notifications を使うすべてのコード(Rails 本体やアプリケーションコード)が間接的に影響を受けますが、
    • 公開 API (instrument, subscribe, unsubscribe など) は変わらない設計です。
  • ただし、内部的な挙動は以下のように変わります:
    • 通知の配送は「Ractor ローカルな Notifier」に対して行われる
    • 新しく登録された購読は、「その後に生成される Ractor の Notifier」からしか見えない可能性がある
      (※特に「Ractor 生成 → subscribe → 既存 Ractor から instrument」の順序のようなケースでは、既存 Ractor の Notifier に購読が反映されない)

注意点 / 想定される落とし穴 ​

  1. Ractor 生成タイミングと subscribe のタイミング

    • 実装から推測される挙動として、
      • Ractor A 作成
      • メイン Ractor で Notifications.subscribe 呼び出し
      • Ractor A 内で instrument
        → Ractor A の Notifier は「Ractor A 生成時点のスナップショット」を持っており、その後の subscribe が反映されない可能性があります。
    • 最も安全なのは:
      • Ractor を生成する前に必要な subscribe をすべて済ませる
    • Ractors を多用するアプリでは、「購読の追加タイミング」と「Ractor 生成タイミング」を意識する必要があります。
  2. Ractor 内で subscribe した場合の扱い

    • PR の説明から見ると、「AS::Notifications.subscribe が呼ばれたときにスナップショットを更新」という設計なので、
      • Ractor 内から subscribe しても、その Ractor の Notifier を「取り替える」のか、「現 Notifier にだけ購読が付く」のかは実装依存です。
    • 一般的には、Ractor 内で subscribe するのは避け、メイン Ractor 側で行う方が安全です。
  3. Fanout を直接触っているコード

    • 非推奨だが、ライブラリやアプリが ActiveSupport::Notifications::Fanout を直接使っている場合、
      • 新しく追加された内部 API(スナップショット初期化関連)を前提にすることで Ractor 対応を行える可能性があります。
    • ただし Fanout は内部 API なので、将来的な互換性は保証されません。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Rails::Application::RoutesReloader から一度削除されていた loaded メソッド(属性)が、フレームワークの公開APIであることが判明したため復活させたPRです。これにより、既存アプリやライブラリが RoutesReloader#loaded に依存していても、引き続き問題なく動作できるようになります。

  1. 変更内容の詳細

※ 実際のコード断片はPR本文に含まれていませんが、変更ファイルと行数から推測できるレベルで説明します。

RoutesReloader に loaded を再追加 ​

対象ファイル:

  • railties/lib/rails/application/routes_reloader.rb (+5/-0)

行数から見ると、次のような変更が入っている可能性が高いです(イメージの擬似コードです):

ruby
module Rails
  class Application
    class RoutesReloader
      # 例: 再追加されたアクセサ
      attr_reader :loaded

      def initialize(...)
        @loaded = false
        ...
      end

      def reload!
        ...
        @loaded = true
      end

      # あるいは、以前と同じインターフェースを完全に再現している:
      # attr_accessor :loaded
    end
  end
end

PR説明文にある通り、loaded は一度別のPR(#58225)で削除されましたが、実は「Railsの公開API」として外部から利用されているメソッド/属性だったため、互換性維持のために元に戻されています。

「公開APIである」とは、主に以下を意味します。

  • Railsの公式クラス/モジュール上の publicメソッド として外部からの利用を想定している
  • マイナーアップデートやパッチレベルアップデートで後方互換性を維持すべき対象

今回のPRでは、その契約を守るために loaded が復活しています。

テストの追加・修正 ​

対象ファイル:

  • railties/test/application/routes_reloader_test.rb (+30/-4)

主に以下のようなテストが追加されていると考えられます(擬似コード):

ruby
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 がうっかり削除・変更されるリグレッションを防ぎます。


  1. 影響範囲・注意点

影響範囲 ​

  • Rails.application.routes_reloader.loaded を参照しているアプリケーション/エンジン/gem
  • RoutesReloader のリロード状態をフックして独自処理をしているツール

以前のPR(#58225)で loaded が削除されたことにより、上記のようなコードは NoMethodError になっていた可能性がありますが、このPRにより再び正常動作するようになります。

開発者視点でのポイント ​

  • RoutesReloader#loaded は、今後も後方互換性を期待できる公開API とみなしてよいです。

  • ルーティングのリロード状態をウォッチする必要がある場合、次のようなコードが公式にサポートされる形で書けます:

    ruby
    reloader = Rails.application.routes_reloader
    
    if reloader.loaded
      # ルートがロード済みのときだけ実行したい処理
    end
  • 一方で、loaded の意味はあくまで「RoutesReloaderがルートをロード済みかどうか」であり、「アプリケーションが完全にブートストラップ済みか」など別の意味ではないので、その点は誤用に注意が必要です。


  1. 参考情報 (あれば)
  • このPRで復活させられたメソッドは、以下のPRで一度削除されていました:
  • Railsの公開APIの扱い・後方互換性ポリシーは公式ガイドにも言及があります(Railsのアップグレードガイド等)。
  • RoutesReloader 自体の利用例は、主に開発モードでのルートの自動リロードなど、フレームワーク内部処理が中心であり、アプリケーションコードから直接触るケースは比較的少ないですが、エンジン開発や高度なメタプログラミング系gemでは利用されることがあります。

#58397 Extract duplicated UPDATE self-join preparation into Arel::Visitors::… ​

マージ日: 2026/8/7 | 作成者: @seuros

  1. 概要 (1-2文で)
    PostgreSQL と SQLite の Arel Visitor に重複していた UPDATE の self join 用の前処理ロジックが、共通の Arel::Visitors::ToSql#prepare_update_statement_with_self_join に抽出されました。挙動の変更はなく、内部実装のリファクタリングのみです。

  1. 変更内容の詳細

もともとの状態 ​

  • Arel::Visitors::PostgreSQL と Arel::Visitors::SQLite において、prepare_update_statement メソッド内部に
    • self join を伴う UPDATE 文を組み立てるための前処理ロジック
    • そのための条件チェック(ガード)
  • があり、その「本体ロジック部分」がほぼコピペ状態で重複していました。

概念的にはこういった構造だったと考えられます(疑似コード):

ruby
# 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

今回の変更点 ​

  1. 共通ヘルパーの追加
    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 を組み立てるための共通ロジック」が集約されています。

  2. 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 のための構築ロジック)のみを共通化。
  3. MySQL Visitor には変更なし

    • Arel::Visitors::MySQL はもともと prepare_update_statement を独自にオーバーライドしており、この PR の共通化対象には含めていません。
    • MySQL 用の UPDATE 自己結合の処理は今まで通りで、挙動に影響なし。

  1. 影響範囲・注意点
  • 実行時挙動

    • 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 ごとの差異は依然として「ガード部分」で吸収されるため、共通ロジックの中は「両者で本質的に同じ振る舞い」の部分に集中できます。
  • 互換性・移行面

    • パブリック API の追加・削除はなく、Arel の内部メソッドレベルのリファクタリングにとどまります。
    • 直接 Arel::Visitors::PostgreSQL#prepare_update_statement などを monkey patch しているような特殊なコードを書いていない限り、アプリケーション側での対応は不要です。

  1. 参考情報 (あれば)

この変更は「コード重複の排除とメンテナンス性向上」を目的とした内部リファクタリングであり、self join な UPDATE クエリのバグ修正や仕様変更ではありません。


#58400 Extract duplicated local cache multi-read resolution in Strategy::LocalCache ​

マージ日: 2026/8/7 | 作成者: @seuros

  1. 概要 (1-2文で)
    ActiveSupport::Cache::Strategy::LocalCache 内で、fetch_multi と read_multi_entries に重複して書かれていた「ローカルキャッシュからの複数キー解決処理」が共通メソッドに抽出されました。挙動の変更はなく、内部実装のリファクタリングです。

  1. 変更内容の詳細

背景 ​

Strategy::LocalCache は、スレッドローカルなどにぶら下がる「ローカルキャッシュ」を持ち、同じリクエスト中に何度も同じキーを読む際に、バックエンドへのアクセスを減らすために使われます。

fetch_multi と read_multi_entries はどちらも「複数キーをまとめて読む」メソッドですが、

  • すでにローカルキャッシュに入っている値を取り出す
  • 入っていないキーだけをバックエンドから読む
  • 読めた値をローカルキャッシュに格納する
    というロジックが両方のメソッドにほぼ同じ形で重複していた、というのが PR の対象です。

主な変更点 ​

local_cache.rb 内で、以下のような形で重複ロジックが共通化されたと考えられます(イメージの擬似コード):

ruby
# 変更前(イメージ)
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

これを次のようなヘルパーメソッドに抽出:

ruby
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 はこのヘルパーを呼び出すだけに簡略化されています:

ruby
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 なので、ほぼ「共通部分の切り出しと、それを呼び出すコードへの差し替え」のみの変更です。


  1. 影響範囲・注意点
  • 公開 API (Rails.cache.fetch_multi, read_multi など) の挙動は変わりません。
  • ローカルキャッシュ戦略 (Strategy::LocalCache) 自体のインターフェースも変わらず、内部実装のみの変更です。
  • 既存のアプリケーションコード・gem が、Strategy::LocalCache の内部実装詳細に依存していない限り、互換性の問題は発生しません。
  • 重複コードが減ったことで、将来的にローカルキャッシュの動作変更やバグ修正を行う際、fetch_multi と read_multi_entries の挙動差異が生まれにくくなり、保守性が向上します。

  1. 参考情報 (あれば)

#58401 Extract duplicated job matching logic in ActiveJob::TestHelper ​

マージ日: 2026/8/7 | 作成者: @seuros

  1. 概要 (1-2文で)
    ActiveJob::TestHelper 内で assert_enqueued_with と assert_performed_with がそれぞれ重複して持っていた「ジョブの条件マッチング」処理を、共通のプライベートメソッド find_matching_job に切り出した PR です。挙動は変えずに、重複コードを削減しテストヘルパー内部の保守性を高めています。

  1. 変更内容の詳細

何をしたか ​

  • activejob/lib/active_job/test_helper.rb 内で、以下の2つのアサーションが共通で使うロジックを抽出しました。
    • assert_enqueued_with
    • assert_performed_with
  • 抽出したメソッド名はプライベートメソッド find_matching_job で、与えられた条件オプションに一致するジョブをキュー(または実行履歴)の中から探す責務を持ちます。

変更前のイメージ ​

従来は assert_enqueued_with と assert_performed_with の中に、それぞれほぼ同じ形のロジックが書かれていました。典型的には次のような処理です(擬似コード):

ruby
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つのアサーションから呼び出すように変更しています:

ruby
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行
    → 重複部分をまとめたことで総行数は減っており、コードが簡潔になっています。

  1. 影響範囲・注意点
  • 公開APIの挙動は変わらない想定

    • 変更はあくまでプライベートメソッドの抽出・リファクタリングであり、assert_enqueued_with / assert_performed_with のインターフェースや期待される挙動は変わりません。
    • 既存のテストコード(これらのアサーションを利用している側)に影響はないはずです。
  • 内部実装に依存しているコードへの潜在的影響

    • ActiveJob::TestHelper の内部実装に(モンキーパッチ・リフレクション等で)依存している場合は、条件マッチングロジックの位置が変わることで影響する可能性があります。
    • ただし、抽出されたメソッドは private であり、通常は直接利用される想定ではありません。
  • ロジックの一元化によるバグ修正容易性

    • 将来的に「ジョブのマッチング条件」を変更・拡張する場合、find_matching_job だけを直せばよくなり、assert_enqueued_with と assert_performed_with の挙動差異が生まれにくくなります。
    • これまで両方に同じ変更を入れる必要があった箇所が1か所に集約されるため、バグ混入のリスク減にもつながります。

  1. 参考情報 (あれば)

#58395 Extract duplicated bind value coercion in QueryMethods ​

マージ日: 2026/8/6 | 作成者: @seuros

  1. 概要 (1-2文で)
    ActiveRecord::Relation::QueryMethods 内で、SQLリテラル用のバインド値変換ロジックが重複していた部分を共通メソッド化して整理したPRです。挙動・機能は変わらず、内部実装のリファクタリングのみです。

  1. 変更内容の詳細
  • 対象: activerecord/lib/active_record/relation/query_methods.rb
  • 目的: bind_value_for_sql_literal に関する「バインド値の型変換・正規化」処理が複数箇所で重複していたため、その重複コードを抽出し共通化。

具体的には、以下のようなイメージの変更が入っています(疑似コード):

ruby
# 変更前(イメージ)
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
ruby
# 変更後(イメージ)
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行」という構成になっています。

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

    • ActiveRecord::Relation::QueryMethods 内部で SQL リテラルのバインディングを行うコードパス全般が、この共通化されたメソッドを通るようになっています。
    • ただし、既存処理をそのまま切り出しただけなので、型変換ロジック自体は変わっていません。
  • 注意点:

    • 公開APIの仕様変更はなく、アプリケーションコード側で修正すべき点はありません。
    • 万一影響が出るとすれば、これまで「偶然通っていた」ようなマイナーなコードパスの挙動差ですが、PRの意図が「全く同じ変換の共通化」であるため、実質的に後方互換性は維持されています。
    • Rails内部に対して monkey patch で bind_value_for_sql_literal などを直接上書きしている場合は、内部構造が少し変わったことで想定外の影響が出る可能性はあります(そのような使い方は非推奨ですが)。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Mime::Type.unregister で MIME タイプを削除した後にも、そのフォーマット向けの Collector メソッドが残り続けていた問題を修正し、適切にメソッドを削除してエラーメッセージを分かりやすくしました。これにより、未登録の MIME タイプに対するフォーマットメソッド呼び出し時に、従来の紛らわしい InvalidMimeType ではなく、明確な NoMethodError が発生するようになります。

  1. 変更内容の詳細

背景となる問題 ​

ActionController::MimeResponds などで使われる Collector(例: respond_to ブロック内の format.html, format.json など)は、Mime::Type.on_change(以前は Mime.register_callback)経由で、登録された MIME タイプごとにメソッドを動的に定義しています。

ruby
respond_to do |format|
  format.html
  format.json
end

ここで、例えば Mime::Type.register で独自フォーマット :foo を登録すると、Collector に #foo メソッドが生えます。

問題は、Mime::Type.unregister(:foo) で MIME タイプを削除しても、Collector 側の #foo メソッドが削除されない ことでした。その結果、format.foo は以下のような流れになっていました。

  1. まだ残っている Collector#foo が呼ばれる
  2. 内部で Mime[:foo] を引くが、すでに unregister 済みなので nil が返る
  3. Collector#custom(nil) のような形で呼ばれ、最終的に Mime::Type.new(nil) が実行される
  4. その結果 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 のインタフェースが一貫していることが保証されています。


  1. 影響範囲・注意点
  • 影響を受けるのは、カスタム 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 のメソッドが再定義されるため、再登録後の挙動は従来どおりです。


  1. 参考情報 (あれば)

#58391 Define shareable compiled_method_container readers ​

マージ日: 2026/8/6 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    Rails のビューコンパイル周りで、compiled_method_container のリーダメソッド定義を「Ractor で共有可能 (shareable)」な形に修正した PR です。テンプレートのコンパイル/レンダリングが Ractor 環境で動作するように、lambda によるクラスキャプチャのタイミングと方法を見直しています。

  1. 変更内容の詳細

※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 非対応なパターン) ​

ruby
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 なパターン) ​

ruby
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? チェックがある可能性が高い)。

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

    • 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 非互換要素の一つを取り除いた」位置づけです。

  1. 参考情報 (あれば)

#58390 Make template lookup ractor-safe ​

マージ日: 2026/8/6 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    このPRは、テンプレートの lookup(LookupContext)と digest 計算(Digestor)周りを Ractor セーフにするための変更です。これによりメイン Ractor 以外からでも LookupContext の構築やテンプレート digest の計算が安全に行えるようになります。

  1. 変更内容の詳細

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 にオフロードしやすくなります。


  1. 影響範囲・注意点
  • 対象コンポーネント

    • ActionView::LookupContext
    • ActionView::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 での性能は基本的に変わりません。

  1. 参考情報 (あれば)
  • この 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.rb
      • actionview/test/template/digestor_test.rb

Ractor を使った並列レンダリングや、ビュー関連処理のオフロードを検討している場合、この PR と #58281 の内容を合わせて追っておくと設計の参考になります。


#58387 Reduce allocations during attribute definition ​

マージ日: 2026/8/6 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    ActiveModel の attribute メソッド定義時に使われる "define_method_" 文字列の生成回数を減らし、オブジェクト割り当て(アロケーション)を削減するパフォーマンス改善です。Pattern ごとに 1 回だけ文字列を作るようにし、大量の属性を持つモデルでメモリ負荷を軽減します。

  1. 変更内容の詳細

※ PR では activemodel/lib/active_model/attribute_methods.rb に対して +3 / -2 の小さな変更のみです。

背景 ​

ActiveModel の attribute methods(name, name=, name? などを動的に定義する仕組み)では、「あるパターンに基づいてメソッドを define_method する」ような処理があり、その際に "define_method_" のようなプレフィックス文字列が使われています。

もともとの実装イメージは概ね次のようなものです(※擬似コード):

ruby
patterns.each do |pattern|
  attributes.each do |attr_name|
    method_name = "define_method_" + pattern.suffix # ここで毎回 String を生成
    define_method(method_name) do
      # ...
    end
  end
end
  • patterns は ~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(概念的なイメージ) ​

ruby
class Pattern
  def method_name_for(attr_name)
    "define_method_" + attr_name.to_s
  end
end

After(概念的なイメージ) ​

ruby
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を参照する形に変更

といった最小限のリファクタリングにとどまっていると考えられます。


  1. 影響範囲・注意点

影響範囲 ​

  • 対象: ActiveModel の attribute_methods 機構
    • ActiveModel::AttributeMethods をインクルードしているクラス
    • Rails の ActiveRecord モデルはこのモジュールを経由しているため、実質的にすべてのモデルが間接的な影響を受けます。
  • タイプ: パフォーマンス(メモリアロケーション削減)のみ
    • 動作仕様(どのメソッドが定義されるか、呼び出し結果)は変わらない
    • 文字列フリーズ済みであれば GC 負荷も軽減される

想定される効果 ​

  • 特に大量のカラムを持つテーブルのモデルで、アプリ起動時や attribute_methods 定義時のメモリ使用量が削減
  • GC の負荷が若干軽減され、起動時間や初回アクセスのパフォーマンスがわずかに向上する可能性があります。

注意点 ​

  • 変更は内部実装レベルであり、公開 API のインターフェースに変更はありません。
  • "define_method_" の文字列を直接参照しているようなメタプログラミング(かなり特殊なケース)がある場合は、挙動に影響する可能性がありますが、通常のアプリケーションではそのような依存はまずないと考えられます。
  • スレッドセーフティ:
    • 定数化もしくは不変なインスタンス変数でのキャッシュなので、スレッドセーフです(書き換えなければ問題なし)。

  1. 参考情報 (あれば)
  • 対象ファイル:
    • 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. 概要 (1-2文で)
    Active Record の Relation::FromClause.empty の実装を、Ruby 3 の Ractor(並行実行環境)でも安全に共有できるように変更した PR です。WhereClause に対して行われたのと同種の「イミュータブルで Ractor セーフな空オブジェクト」の扱いを FromClause にも拡張しています。

  1. 変更内容の詳細

※ 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 に適用しています。

典型的な変更イメージは以下のようなものです(疑似コード):

ruby
# 変更前(イメージ)
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 間共有可能であることを明示

という変更になっています。


  1. 影響範囲・注意点

影響範囲 ​

  • 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 セーフ戦略が適用されることになります。

  1. 参考情報 (あれば)

#58206 Generate an omakase-compliant config/ci.rb when steps are skipped ​

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

  1. 概要 (1-2文で)
    bin/rails new 実行時に各種 --skip-* オプションを使った場合でも、新規アプリが自前の bin/rubocop / bin/ci(Style: Ruby)チェックに確実に通るよう、config/ci.rb テンプレートの空行出力ロジックが調整されました。
    とくに --skip-test 時に Layout/EmptyLinesAroundBlockBody で RuboCop が落ちていた問題を解消します。

  1. 変更内容の詳細

問題の背景 ​

以下のような手順でアプリを生成すると:

bash
bin/rails new myapp --skip-test
cd myapp
bin/rubocop
# または bin/ci の "Style: Ruby" ステップ

生成された config/ci.rb に対して RuboCop の Layout/EmptyLinesAroundBlockBody が発生していました。

エラー例:

text
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) 内で、各ステップの間に入れる区切り用の空行を、「両側にステップが存在する場合のみ出力する」ように条件付きに変更しています。

イメージとしては、これまで:

erb
group "Checks" do
  # rubocop step
  # brakeman step

  # ↑ ここに必ず空行が出る
end

group "Tests" do
  # test step
end

のように一律で空行を出していたところを、以下のようなイメージに近付けています:

erb
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" グループの間にあった空行がなくなる

それ以外の出力は従来と同じです。


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

    • Rails 7.2 以降(あるいはこの PR が入るバージョン)での新規アプリ生成 (bin/rails new)
    • 特に --skip-rubocop / --skip-bundler-audit / --skip-javascript / --skip-brakeman / --skip-test の任意の組み合わせでアプリを生成するケース
  • 期待される効果:

    • どのスキップオプションの組み合わせでも、生成直後に 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 由来の警告が出るかどうかは各アプリの設定次第です。

  1. 参考情報 (あれば)
  • 対応する issue/PR:

    • #58164: 認証ジェネレータが挿入する routes を修正し、新規アプリが自前の RuboCop を再びパスするようにした PR
      → 今回は config/ci.rb に対する同種の「新規アプリが RuboCop に素で通るようにする」調整。
  • 関連コマンド:

    • アプリ生成: 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. 概要 (1-2文で)
    MySQL 系アダプタ(mysql2 / trilogy)でそれぞれ重複実装されていた execute_batch を、共通の ActiveRecord::ConnectionAdapters::MySQL::DatabaseStatements に集約したリファクタリングです。挙動の変更はほぼなく、コードの重複排除とメンテナンス性向上が主目的です。

  1. 変更内容の詳細

やったこと ​

  • 共通の親モジュール:
    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 実装イメージを示します。

ruby
# 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 が個別に定義されていたものを、共通モジュールに移した形です。


  1. 影響範囲・注意点
  • 対象
    • ActiveRecord の MySQL 系アダプタすべて(mysql2 / trilogy)の execute_batch 呼び出しが、新しい共通実装を使うようになります。
  • 互換性
    • 「重複していた共通コードを一箇所に集約しただけ」の変更であり、インターフェース(メソッドシグネチャ)や基本挙動に変更はない想定です。
    • ただし、以前 mysql2 と trilogy で微妙に挙動を変えていた場合(例えばロギングやエラー処理など)には、今回の共通化で差分が吸収される可能性があります。その場合、両者の挙動がより揃う一方、アダプタ固有の「クセ」が消えることになります。
  • アプリケーションコードへの影響
    • execute_batch を直接使っているアプリケーションは通常そのまま動作します。
    • アダプタを monkey patch して execute_batch をオーバーライドしていた場合は、継承元のモジュール位置が変わっているため、モジュールのインクルード順やメソッド解決順序(lookup path)に依存した高度なパッチは確認が必要です。
  • ライブラリ側(gem作者など)への注意
    • mysql2 / trilogy の各アダプタクラスに直接 execute_batch を再定義していた middleware / extension がある場合、今後は MySQL::DatabaseStatements のメソッドを前提にした方が安全です。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58392
  • 関連コード:
    • activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
    • activerecord/lib/active_record/connection_adapters/mysql2/database_statements.rb
    • activerecord/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. 概要 (1-2文で)
    accepts_nested_attributes_for 経由で渡されたネスト属性の id が見つからない場合に発生する ActiveRecord::RecordNotFound のエラーメッセージ内で、ユーザー入力由来の id をそのまま埋め込まず .inspect するように変更した PR です。これにより、ANSIエスケープシーケンスなどを含む悪意ある id によるログ偽装・視認性の低下リスクを低減します。

  1. 変更内容の詳細

変更対象 ​

  • ActiveRecord::NestedAttributes#raise_nested_attributes_record_not_found!
    • ファイル: activerecord/lib/active_record/nested_attributes.rb
  • 対応するテスト追加
    • ファイル: activerecord/test/cases/nested_attributes_test.rb

具体的なコード変更イメージ ​

もともとネスト属性で関連レコードが見つからなかった際、以下のように(概念的に)record_id をそのままメッセージに埋め込んでいました:

ruby
raise RecordNotFound.new(
  "Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}",
  model, primary_key, record_id
)

これが次のように変更されています:

ruby
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 をネスト属性経由で送ったケース:

json
{
  "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 された形になることを検証しています。

  1. 影響範囲・注意点
  • 影響するのは:
    • accepts_nested_attributes_for を使って関連を更新/作成している
    • そのネストされた id で対象の関連レコードが見つからず ActiveRecord::RecordNotFound が発生する
    • その例外メッセージを ログや外部システムに出力している
  • フレームワーク的には RecordNotFound は通常 404 応答にマッピングされ、メッセージ文字列はレスポンスとしては返されないため、エンドユーザーへの直接的なXSSなどは発生しません。
  • 主な効果はログの安全性向上:
    • 攻撃者が任意のANSIシーケンスを含む id を送りつけても、ログを閲覧する人の端末側でそれが解釈されず、ログの色分け・削除・上書きなどの「視覚的な攻撃」がしづらくなる。
  • 既存アプリでの互換性:
    • RecordNotFound#message の文言がわずかに変わります(id がクォートされる・制御文字がエスケープされる)
    • もし RecordNotFound#message を文字列パースして独自処理しているようなコードがあれば(あまり無いと思われますが)、そのパースロジックに影響する可能性はあります。
  • 追加のアプリ側対応:
    • 特に対応は不要ですが、ログビューア・監視ツール等で RecordNotFound メッセージのパターンマッチをしている場合、ID=... 部分が ID="..." になることを前提にしているかどうかを確認するとよいです。

  1. 参考情報 (あれば)
  • 類似の脆弱性対応:
    • CVE: CVE-2025-55193
      ActiveRecord::FinderMethods 等で RecordNotFound メッセージ内の id に .inspect を適用した修正。今回のPRはその「ネスト属性版」の追従です。
  • 元報告:
    • HackerOne ユーザー ylchen による報告がもとになっており、フレームワーク単体では exploitable でないため情報提供扱いとなっていましたが、ログセキュリティの観点から今回修正されています。

#58209 Flatten block content inside links in Action Text markdown conversion ​

マージ日: 2026/8/6 | 作成者: @flavorjones

  1. 概要 (1-2文で)
    Action Text の Content#to_markdown が、リンク (<a>) 内にブロック要素が入っている場合に不正な CommonMark を出力していた問題を修正する PRです。リンク内のブロック要素を Markdown の制約に合わせて「1行のインラインテキスト」にフラット化してからリンク記法に包むように変更しています。

  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

    ](https://example.com)

    
    上記では
    - ``` ``` のフェンスコードブロック
    - その後の空行  
    によって Markdown パーサはブロック構造を先に解釈し、インラインリンクが分断され、仕様上不正かつ期待通りにリンクにならない出力となっていました。

変更方針 ​

  • CommonMark は「リンク内にブロック要素を含める」方法を提供していないため、「リンク構造を保ちつつ、中身を可能な範囲でインラインに潰して表現する」 という degrade(劣化表現)戦略を取っています。

  • 新しい visit_a の挙動:

    1. 子要素の Markdown 出力をすべて連結した文字列を取得
    2. その中の「連続する改行(\n が1回以上続く部分)」をスペース1つに置き換える
    3. それを "[...](href)" の中身として使う

    疑似コードイメージ:

    ruby
    def 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
    end
  • href がない、または許可されないアンカーについては、従来通りリンクとしてレンダリングされず、内部のブロック構造を保持したまま素通しします。つまり、「リンクにならない <a>」の振る舞いは変えていません。

具体例 ​

  1. <pre> ブロックを含んだリンク

    HTML:

    html
    <a href="https://example.com"><pre>puts 1</pre></a>

    新しい出力:

    markdown
    [``` puts 1 ``` ](https://example.com)
    • もともとコードブロック(フェンス)として扱われていた部分が、インラインコードスパン puts 1 に縮退され、その前後の改行はスペースに変換されます。
    • 結果として「リンクテキストがインラインコード puts 1」である正当な CommonMark となり、そのテキスト全体がリンクになります。
  2. 見出しを含んだリンク

    HTML:

    html
    <a href="https://example.com"><h1>heading</h1></a>

    新しい出力:

    markdown
    [# heading ](https://example.com)
    • <h1> → # heading という Markdown 見出しの表現自体は維持されますが、改行がスペースに変換されるため1行にフラット化されます。
    • その # heading という文字列自体が「リンクテキスト」として解釈されます(# はあくまでプレーンテキスト扱いで、ブロック見出しにはなりません)。
  3. リストや引用など、他のブロック要素を含む場合も同様に、行頭記号(-, >, 1. など)を含んだテキストがすべて 1 行に潰され、そのままリンク内テキストになります。

テスト ​

  • actiontext/test/unit/markdown_conversion_test.rb に、リンク内にブロック要素が含まれる様々なパターンを網羅するテストが多数追加されています (+122行)。
  • これにより、visit_a の新しいフラット化ロジックが期待通り動作することが検証されています。

  1. 影響範囲・注意点
  • 対象: Action Text の ActionText::Content#to_markdown を利用して HTML を Markdown に変換しているコード(特に WYSIWYG エディタの保存内容を Markdown に変換して外部に出すようなケース)。
  • 現時点では to_markdown 自体がまだ未リリース(#56858 で追加されたがリリース版には未含有)であるため、「公式リリース間での互換性破壊」は発生しません。
  • ただし、以下のような点は把握しておくとよいです:
    • もし unreleased な to_markdown を既に利用していた場合、「リンク内のブロック要素が壊れた Markdown として出力される」バグが修正されるため、結果の Markdown が大きく変わる可能性があります。
    • 「ブロックとしての構造」(段落区切り、コードブロックなど)はリンク内では維持されず、すべて 1 行のプレーンテキストまたはインラインコードとして縮退されます。
      • 例えば「リンク内に複数段落のテキストがある」といった HTML は、Markdown 変換後は「スペースで連結された1つのインラインテキストのリンク」になります。
    • href がない・許可されない <a> はブロック構造が維持されるため、その挙動に依存しているコードがあれば変更はありません。
  • セキュリティ面:
    • 本 PR は主に出力フォーマットの正当性の改善であり、リンク許可/不許可のポリシー自体は変更していません。encode_href やフィルタリングのポリシーへの影響はありません。

  1. 参考情報 (あれば)

#58270 asset_path: strip the query/fragment tail without a second scan ​

マージ日: 2026/8/6 | 作成者: @nuclearspike

  1. 概要 (1-2文で)
    asset_path がクエリ文字列・フラグメント(? / # 以降)を取り除く処理について、正規表現による2回のスキャンと不要な文字列コピーをやめて、1回のスキャン+スライスだけで処理するよう最適化したPRです。これにより image_tag や javascript_include_tag などのヘルパ全般で、オブジェクト割り当て削減と数%のパフォーマンス向上が見込まれます。

  1. 変更内容の詳細

変更前の処理 ​

asset_path 内では、ソース文字列からクエリ・フラグメント部分を取り出しつつ、元の文字列からそれを削除するために、正規表現を2回使っていました。

ruby
tail, source = source[/([?#].+)$/], source.sub(/([?#].+)$/, "")

問題点:

  • 正規表現マッチのスキャンが2回走る
  • String#sub はマッチしない場合でも新しい文字列を必ず生成する
  • 実際には "application.js" のようなクエリやフラグメントを含まないケースが大多数であり、その場合にも無駄なオブジェクト割り当てが発生していた

変更後の処理 ​

クエリ・フラグメントを「取り出す」処理はそのまま使い、削除処理を「長さに基づくスライス」に変更しています。

変更後のコードイメージ:

ruby
if (tail = source[/[?#].+$/])
  source = source[0, source.length - tail.length]
end

ポイント:

  • 正規表現マッチは1回だけ (source[/[?#].+$/])
  • クエリ・フラグメントがない場合 tail は nil なので、source は一切コピーされない
  • ある場合だけ、末尾から tail.length バイト分を削った新しい文字列を作る

改めて、挙動の例 ​

挙動自体は従来と同じです。たとえば:

ruby
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 コアとしてはこのようなケースを防御するためにコードを複雑化する価値はない、という判断になっています。


  1. 影響範囲・注意点

影響範囲 ​

  • 対象となるメソッド:
    • asset_path
    • およびそれを内部的に利用する各種ヘルパ:
      • image_tag
      • javascript_include_tag
      • stylesheet_link_tag
      • asset_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(/([?#].+)$/, "") 相当の処理)を書いていると、挙動に差が出る可能性がありますが、通常の利用では影響ありません

  1. 参考情報 (あれば)
  • 変更ファイル:
    • actionview/lib/action_view/helpers/asset_url_helper.rb (+3 / -1)
  • ベンチマークスクリプト:
    • PR 本文の <details> 内に掲載。activesupport / actionview をローカルパス指定し、benchmark-ips を利用して asset_path の性能比較を行う簡易ベンチ。
  • 実装上の意図:
    • 高頻度で呼ばれる小さなユーティリティ (asset_path) において、「ほとんど起きないケース(クエリ付きパス)のために、通常ケースで毎回余分なスキャンとコピーを行う」ことをやめ、ホットパスのコスト削減を狙ったマイクロ最適化。

#58135 Add query predicate hooks for Active Record types ​

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

  1. 概要 (1–2文で)
    Active Record の型ごとに、where 句などのクエリ述語の「左辺(カラム側)」と「右辺(値側)」をカスタマイズできるフックが追加されました。これにより、UUID など「書き込み時の表現」と「検索時の表現」が異なる型でも、型クラス側で SQL 生成を制御しやすくなります。

  1. 変更内容の詳細

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.rb
    • ActiveModel::Type::Value に query_attribute / query_value のデフォルト実装が追加
    • デフォルトは「何もしない」実装で、既存挙動を変えないようにしているはず
  • activerecord/lib/active_record/relation/predicate_builder*.rb
    • PredicateBuilder とその各種 handler(array_handler, range_handler, etc.)で、値や属性を扱う際に新しいフックを使うように変更
    • ハッシュ形式 (where(column: value)) を解釈するときに、型オブジェクトから query_attribute / query_value を呼んでから Arel ノードを組み立てるようになっている
  • activerecord/lib/active_record/relation.rb
    • PredicateBuilder の呼び出し部分に微修正(新しいフックに対応)
  • 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 のような型を定義し、そこで:

ruby
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

上記のような実装をすると、呼び出し側は:

ruby
User.where(id: "a1b2-...-uuid-string")

と書くだけで、内部的には:

sql
WHERE id = UUID_TO_BIN(?)

のような SQL が生成されることを狙っています(実際の Arel の書き方は実装詳細になりますが、方向性としてこういうことが可能)。

例2: 正規化して比較するテキスト列 ​

DBにはユーザーが入力した文字列をそのまま保存しつつ、検索時は正規化した値で比較したいケース:

sql
lower(custom_immutable_unaccent(name)) = lower(custom_immutable_unaccent(?))

といった SQL を生成したければ、テキスト用のカスタム型で:

ruby
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

としておけば、呼び出し側:

ruby
User.where(name: "Émilie")

が、期待通りの正規化比較 SQL に変換されるようになります。

2-4. なぜ cast / serialize ではダメなのか ​

cast や serialize は「Ruby <-> DB の値変換」の責務を持っており、INSERT/UPDATE の書き込みや SELECT の読み込みに共通で使われます。

しかし今回の要件は、

  • 「検索条件(WHERE)のみ」で
  • カラム名やバインド変数を SQL 関数で包みたい

というものなので、

  • 単純な「値変換」ではなく、Arel ノードレベルの「SQL 構造の変換」
  • SELECT の投影や INSERT の値には影響させたくない

といった性質のため、専用のクエリ述語用フックを設ける設計になっています。


  1. 影響範囲・注意点
  • 既存アプリへの影響
    • 既存の型クラスは query_attribute / query_value を実装していないため、デフォルト実装(何もしない)が使われ、挙動は従来通りです。
    • そのため、この PR を取り込んでも、既存コードは基本的にブレークしない想定です。
  • カスタム型を実装する場合の注意点
    • query_value では「プレースホルダ付きのバインド」を活かす必要があります。SQL文字列を自前で連結するのではなく、predicate_builder を介してバインド値を生成し、それを Arel で包む形にするべきです。
    • query_attribute / query_value はあくまで「述語用」のフックであり、SELECT 句の投影結果や INSERT/UPDATE の書き込み時には使われません。その前提でインターフェースを設計する必要があります。
  • サポートされる述語の種類
    • 今回のフックは Equality / Array / Range の述語で利用されます。その他の演算子(LIKE、>、<、IN (サブクエリ) など)でどこまで効くかは、PredicateBuilder 内部の実装に依存します。
  • 将来的な API 安定性
    • PR 説明では「internal predicate-builder hooks」と明記されているため、まずは内部 API として導入された可能性があります。
      公開 API としてドキュメント化されるかは今後の変更を確認する必要があります(特に Gem として公開する共通型の実装では、この点を意識した方がよいです)。

  1. 参考情報 (あれば)
  • 元 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. 概要 (1-2文で)
    このPRは、特定のテスト(TestDestroyAsPartOfAutosaveAssociation)がトランザクションなしで実行されることにより、DB上に不要な Cpk::Author レコードが残り、その後に実行される別テスト(WhereChainTest)を不安定化させていた問題を修正するものです。autosave のテストの teardown で漏れた Cpk::Author を削除する1行を追加し、テスト間のデータリークを防いでいます。

  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 のテストが先に実行されたかどうか」で結果が変わるフレークテストになっていた。

作者が再現に使ったコマンド:

sh
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行を追加しただけです。

イメージとしては、以下のようなコードが追加されたと考えられます(擬似コード):

ruby
def teardown
  super
  Cpk::Author.delete_all  # 実際にはスコープされているか、特定レコードのみ削除
end

実際の差分は1行のみで、目的は「このテストが作成した author レコードが、他のテストに影響しないようにする」ことです。


  1. 影響範囲・注意点
  • 影響範囲は Rails 本体の「テストコード」のみであり、アプリケーションのランタイム挙動や API 仕様には影響しません。
  • 修正により、以下が改善されます:
    • WhereChainTest#test_missing_with_composite_primary_key が、事前に autosave の CPK テストが走ったかどうかで結果が変わるフレーク状態から解消される。
    • CPK(Composite Primary Key / Composite Foreign Key)を使ったテスト同士が、漏れたレコードにより互いに干渉しにくくなる。
  • 注意点として、「トランザクションを使わないテスト」は、このようなデータリークを起こしやすいため、同様のパターンが他にもないかを継続的に確認する必要があります。

  1. 参考情報 (あれば)
  • 対象の不安定テスト:
    • TestDestroyAsPartOfAutosaveAssociation#test_autosave_has_one_cpk_association_when_composite_foreign_key_is_manually_set
    • WhereChainTest#test_missing_with_composite_primary_key
  • フレークが確認されたビルド:
  • 関連概念:
    • autosave associations(autosave: true な関連)
    • composite primary key / composite foreign key を用いた関連のテスト
    • トランザクションなしテストにおけるレコードリークとテスト順依存問題

#58281 Move the LookupContext::DetailsKey caches to Ractor-local storage ​

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

  1. 概要 (1-2文で)
    LookupContext::DetailsKey 周りのキャッシュを Ractor 間で共有しないようにし、Ractor ごとに独立したキャッシュを持つ形に変更することで、メイン Ractor 以外からのテンプレート lookup を可能にした PRです。あわせて、Ractor ローカルなストレージを簡単に扱うための ActiveSupport::Ractors.store_if_absent が追加され、concurrent-ruby のバージョン要件も上がっています。

  1. 変更内容の詳細

背景問題 ​

  • TemplateDetails::Requested オブジェクトのインターン(intern)と、テンプレートのダイジェストキャッシュに使っていた Concurrent::Map が、Ractor 間で共有可能なオブジェクトではなかった。
  • そのため、メイン Ractor 以外からテンプレート lookup を行うと IsolationError になる / うまく動かない 状態だった。
  • 特に Concurrent::Map は、Concurrent::NULL という sentinel オブジェクトを内部的に使うが、concurrent-ruby 1.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 ローカルストレージ に移動。

イメージとしては、以下のような変更が行われています(簡略イメージ・擬似コード):

ruby
# 変更前(イメージ)
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
ruby
# 変更後(イメージ)
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)を提供します。

イメージ(簡略):

ruby
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::NULL sentinel が freeze され、Ractor 間で扱えるようになったため。
    • それ以前のバージョンでは、Concurrent::NULL がミュータブルなままであり、Ractor 間で参照すると IsolationError を引き起こします。
    • 結果として、非メイン Ractor 内で Concurrent::Map を使うこと自体が不可能でした。
  • この PR では、少なくとも Ractor ローカルのキャッシュとして Concurrent::Map を使う必要があるため、Concurrent::NULL が Ractor セーフであることを前提としています。

4) テスト追加・変更 ​

対象:

  • actionview/test/template/lookup_context_test.rb
  • activesupport/test/ractors_test.rb

主に以下を確認するテストが追加・修正されています。

  1. LookupContext が Ractor 内でも動作すること

    • 別 Ractor からテンプレート lookup を行い、IsolationError などが発生しないこと。
    • Ractor ごとに details key のインターンやダイジェストキャッシュが正しく機能していること。
  2. ActiveSupport::Ractors.store_if_absent の挙動

    • 同じ Ractor 内で同じ key を使うと同じオブジェクトが返ること。
    • 別 Ractor では別オブジェクトが返る(=Ractor ローカルである)こと。
    • Ruby のバージョンや Ractor 有無に応じたフォールバック挙動が正しいこと。

  1. 影響範囲・注意点

影響範囲 ​

  1. テンプレート lookup とキャッシュ

    • Action View のテンプレート lookup に関する details key インターン・ダイジェストキャッシュの実体が、「プロセス全体で共有された Map」から「Ractor ごとのローカルストア」に変わりました。
    • そのため、
      • Ractor A で構築された details key やダイジェストキャッシュは、Ractor B からは見えません。
      • 同じテンプレートを複数の Ractor から使う場合、各 Ractor がそれぞれキャッシュを埋め直すことになります(ただし digest 値そのものは変わらない)。
  2. パフォーマンス上の性質

    • 単一 Ractor(従来どおりのマルチスレッドのみ)の環境では、実質的にはこれまでと大きく変わらない挙動になります。
    • 複数 Ractor を使っている場合、テンプレート lookup のキャッシュが Ractor ごとに分かれるため、
      • 初回アクセスのオーバーヘッドは Ractor 数に比例して増える
      • しかし Ractor 間でのロック・同期は減る(Ractor ごとの独立キャッシュのため)
        というトレードオフがあります。
  3. 外部インターフェース

    • 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 など)を通じて行うべきです。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Rails の ActiveRecord において、prepared_statements 設定や unprepared_statement ブロックを無視して常にプリペアドステートメントが使われてしまう回 regress を修正し、prepared_statements 設定が正しく尊重されるようにした PR です。PostgreSQL かつトランザクションプーリングプロキシ(例: PgBouncer transaction mode)配下で名前付き PREPARE が壊れる問題を解消します。

  1. 変更内容の詳細

問題の背景 ​

  • 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」を行い:

sql
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(もしくはその近辺)で、元々存在していた以下のような条件:

ruby
prepare: prepared_statements && preparable

に相当するロジックが、あるコミット(1277a8f09c)で落ちてしまっていたのを復元しています。

今回の修正でのポイント:

  • 「呼び出し元が preparable: true と言っているか」だけでなく
  • 「接続の prepared_statements 設定(実体は動的な prepared_statements? メソッド)」

を掛け合わせて、「本当にプリペアドステートメントを使うべきか」を判定するように戻しています。

擬似コードで表すと:

ruby
# 修正後イメージ
adapter.select_all(
  sql,
  name,
  prepare: prepared_statements? && !!preparable
)

という形に近い動きになります。

ここで prepared_statements? は:

  1. 接続オプション prepared_statements: false が指定されている場合は false を返す。
  2. unprepared_statement ブロック中では、一時的に false を返すようになっている。

そのため、以下の双方のケースで確実にプリペアドステートメントが無効になります:

  • 接続自体で恒久的に prepared_statements: false を設定したい場合
  • 一時的にプリペアドステートメントを避けたい箇所を unprepared_statement ブロックで囲む場合
ruby
# 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 していないことが検証されています。


  1. 影響範囲・注意点
  • 対象:
    • 主に PostgreSQL アダプタを使っているアプリケーション
    • 特に、PgBouncer の transaction モードなど、トランザクション単位で接続が入れ替わるプーリングプロキシ配下で Rails を動かしている環境
  • 影響:
    • これまで「prepared_statements: false を設定しているのに、実際には PREPARE/EXECUTE が出ている」という状況だった場合:
      • この PR 適用後は、本当に PREPARE が出なくなるため、DB 側のログやパフォーマンス特性が変わる可能性があります(本来そうあるべき挙動)。
    • unprepared_statement ブロックが、StatementCache 経由のクエリにもきちんと適用されるようになります。
  • 注意点:
    • もし既に「このバグに依存したワークアラウンド」(例: プロキシ側で PREPARE 名を握りつぶす、など)を実装している場合、この修正で不要になったり挙動が変わる可能性があります。
    • パフォーマンス上、プリペアドステートメントに頼っていたコードが、prepared_statements: false 設定のままになっていると、パフォーマンスが低下することはあり得ますが、それは設定の意図に沿った動作です。必要に応じて設定を見直してください。

  1. 参考情報 (あれば)
  • 関連 Issue: #58371
  • 回 regress の原因となったコミット: 1277a8f09c(prepared_statements && が削除されていた)
  • キーワード:
    • ActiveRecord select_all
    • StatementCache#execute
    • prepared_statements / prepared_statements?
    • unprepared_statement
    • PostgreSQL + transaction pooling(PgBouncer など)

#58360 Use local fixtures ​

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

  1. 概要 (1-2文で)
    このPRは、Railties の Active Storage 関連テストで「グローバルな fixtures ではなく、そのテスト自身が持つローカル fixtures を使う」ように変更したものです。これにより、Railties をフレームワークの他の部分から切り離して単体でテストしやすくなっています。

  1. 変更内容の詳細

対象ファイルは2つだけで、内容としては「fixture の参照元をローカルに切り替える」というごく小さな修正です。

変更されたファイル:

  • railties/test/application/active_storage/analyzers_integration_test.rb
  • railties/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 関連の統合テストにも適用した、という位置付けだと考えられます。

コードイメージ(擬似例)としては、例えば以下のような変更です(実際のパス名やメソッド名は簡略化しています):

ruby
# 変更前(共通fixturesへの依存)
fixtures :all  # test/fixtures 以下など、他コンポーネントと共有のfixtures

# 変更後(このテスト配下のローカルfixturesを参照)
fixtures :local_users, :local_attachments  # railties/test/... 配下など

もしくは、ファイルパスを直接指定する形のテストであれば、

ruby
# 変更前
file = file_fixture("images/avatar.png")

# 変更後(railties 側に置いた fixtures を使う)
file = local_file_fixture("images/avatar.png")

のように、「グローバルな fixture 参照をやめて Railties 側のローカル fixture に切り替えた」という変更が行われています(上はあくまでイメージです)。


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

    • 影響を受けるのは railties/test/application/active_storage/* にある統合テストのみです。
    • 実行される Rails 本体の動作や API 仕様には変更がありません。
    • 変更はテストコードのみであり、アプリケーションコードへの実動影響はありません。
  • 意図・メリット

    • Railties をフレームワークの他コンポーネント(特に Active Storage 本体側のテスト資産など)から切り離してテストできるようになります。
    • fixture がローカルに閉じることで、「他のテストスイートや別コンポーネントの fixtures の変更に巻き込まれて Railties テストが壊れる」といったクロスコンポーネント依存を減らせます。
    • 将来的に Railties を単独で切り出してテストしたり、モノレポの構造を変更したりする際の柔軟性が上がります。
  • 注意点

    • ローカル fixtures の場所や内容を変更する場合、Railties 側の Active Storage テストがそれに追随しているか確認する必要があります(ただし今回の PR はその依存を整理する方向の変更なので、むしろ依存関係は減っています)。
    • 他コンポーネントで共通 fixture を使っていた前提のテストコードを Railties 側に追加する場合は、今後は同様に「ローカル fixtures を使う」方針に揃えるのが望ましいです。

  1. 参考情報 (あれば)
  • 類似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. 概要 (1-2文で)
    Active Job の「継続 (Continuation)」機能で使うステップカーソルを、単なる JSON ではなく ActiveJob::Arguments を経由してシリアライズ/デシリアライズするように変更し、Date, Time, GlobalID などのカーソルの型が中断→再開後も維持されるようになりました。これにより、再開後にカーソルが String 化してメソッド呼び出しに失敗する問題が解消されます。

  1. 変更内容の詳細

背景: これまでの問題 ​

Active Job の Continuation (継続) では、ジョブの実行ステップを分割し、「中断 → 後で再開」を行うために「カーソル (cursor)」を継続データに保存しています。

従来実装では、このカーソルを「生の JSON」として書き出して/読み出していました。そのため:

  • Date, Time, ActiveSupport::TimeWithZone, GlobalID など「Active Job 的にはシリアライズ可能だが JSON 自体は知らない型」は、一度中断を挟むと String に変換されて戻ってくる
  • その結果、カーソルを想定した型として扱うコードが壊れる

例:

ruby
# 1回目の実行時
step.cursor        # => #&lt;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, TimeWithZone
  • GlobalID / SignedGlobalID
  • ActiveRecord などのモデル (GlobalID 経由)
  • それらを含む配列やハッシュ

といった「Active Job がサポートする型」を、安全にシリアライズ/デシリアライズしてくれます。

そのため、今回カーソルもこの経路に乗せることで、中断→再開の往復後も型が維持されます。

ruby
# 修正後
step.cursor        # => #&lt;Date: 2026-07-06>   # 中断→再開後も Date のまま
step.cursor.beginning_of_day   # => OK

2) 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
    • 継続まわりの仕様変更に合わせた微修正

  1. 影響範囲・注意点

互換性 ​

  • プリミティブ型のカーソル (Integer, String, それらの配列など):
    • Arguments.serialize/deserialize を通しても「バイトレベルで同一」となるよう配慮されており、既存の継続データとの互換性は保たれます。
    • 既存の JSON ベース継続データも、そのまま読み込めます。
  • 非プリミティブ型 (Date, Time, GlobalID など):
    • 従来は String になってしまっていたものが、本来の型で復元されるようになります。
    • 型に依存したロジック (例: cursor.beginning_of_day) がようやく仕様通りに安全になります。

実装側で意識すべき点 ​

  • カーソルとして使える値は、Active Job がサポートするシリアライズ可能な型に限定されます。
    • カスタムクラスをそのままカーソルに使いたい場合は:
      • Active Job の引数としてシリアライズ可能になるよう GlobalID 等を導入するか、
      • カーソル用に「シリアライズ可能な情報」のみに変換して保存する (id や string 等) 必要があります。
  • もし以前から「中断後は String に変わる」ことを前提にしていたコードがある場合 (かなり例外的なケースですが) は、今回の変更で逆に型が変わる可能性があります。
    • 例: step.cursor が Date から String になる前提で to_date していた、など
    • そのようなコードは削除/簡略化できるはずです。

デバッグ・運用上のメリット ​

  • 継続のカーソルが引数と同じ経路でシリアライズされるようになったため、シリアライズエラーの挙動が統一される:
    • ある型がジョブ引数としてシリアライズ不可能なら、カーソルとしても不可能
    • 問題切り分けが容易になる

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/58045
  • 関連 API:
    • ActiveJob::Arguments.serialize
    • ActiveJob::Arguments.deserialize
    • ActiveJob::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. 概要 (1-2文で)
    Relation#ids が pluck 委譲に変わったことで、新規レコードに紐づく未保存の関連に対する ids が空配列になってしまった問題を修正し、ids / pluck ともに関連の「target」(メモリ上のオブジェクト)から値を取得するようにした変更です。
    これにより、新規レコードに関連づけたオブジェクトに対しても、ids や pluck が直感的な結果を返すようになります。

  1. 変更内容の詳細

背景 ​

以前の変更(rails/rails@56c95aee)で、Relation#ids が内部的に pluck を使うようになりました。
関連に対して呼ぶと、実体は CollectionProxy#pluck が使われます。

ruby
post.tags.ids  # 実際には post.tags.pluck(primary_key)

ところが、新規レコード (Post.new) に対する関連は DB にはまだ存在せず、「null scope」(空のリレーション)を読む挙動になっていたため、ids が以下のように変化してしまいました。

ruby
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」を読むように変更しています。

その結果:

ruby
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.rb

    • pluck の実装を 1 行変更し、「null scope から pluck」ではなく「target から pluck」するように振る舞いを修正
      (差分は +1 / -1 でかなりピンポイントな修正)
  • テスト追加

    • has_many_associations_test.rb
    • has_and_belongs_to_many_associations_test.rb
      新規レコードに対する has_many / HABTM の関連で、ids / pluck が target を読むことを検証するテストが追加されています。
  • activerecord/CHANGELOG.md

    • この挙動変更が CHANGELOG に追記されています。

  1. 影響範囲・注意点

影響するケース ​

主に以下のようなコードに影響します。

ruby
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 を明示的に使うなど、クエリをはっきり分ける必要があります。

例:

ruby
# target(メモリ上の未保存オブジェクト)を含めず、DB だけを見たい場合
post.tags.scope.pluck(:name) # など、意図的に Relation を扱う

設計上の意味合い ​

  • この PR により、「関連に対する pluck / ids は、通常の to_a 同様に target を反映してくれる」という一貫したモデルになります。
  • PR 本文でも触れられている通り、代替案として「ids だけ特別扱いする」ことも可能でしたが、そうすると ids とその他の pluck ベースの API とで挙動がズレるため、この PR では 「pluck の側を target 寄りの挙動にそろえる」という選択が取られています。

  1. 参考情報 (あれば)
  • 当該 PR: https://github.com/rails/rails/pull/58372
  • 問題のきっかけとなったコミット: rails/rails@56c95aee
  • 関連クラス・メソッド:
    • ActiveRecord::Relation#ids
    • ActiveRecord::Associations::CollectionProxy#pluck
    • ActiveRecord::Associations::CollectionProxy#target / #scope / #null_scope

#58374 Ractor safe Active Record primary key ​

マージ日: 2026/8/5 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    Active Record の「主キーオブジェクト」を Ractor 内でも安全に扱えるようにするため、主キーを表すオブジェクトを freeze する変更が入りました。単一 PK・複合 PK・PK なしのすべてのケースで、計算済みの主キーが不変オブジェクトとして扱われるようになります。

  1. 変更内容の詳細

何をしているか ​

  • 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 で freeze しても問題なく動作する」ことを保証している。

(実際のコードイメージ・擬似例)

ruby
# 例: ActiveRecord::Key の生成タイミングで freeze される
key = ActiveRecord::Key.new(primary_key_values)
key.frozen? # => true

# レコード経由での利用イメージ
user = User.find(1)
user.primary_key # => #&lt;ActiveRecord::Key ...> (内部的には freeze 済み)
user.primary_key.frozen? # => true

  1. 影響範囲・注意点
  • 主キーオブジェクトを後から書き換えるようなコードは壊れる

    • たとえば、内部的な主キーオブジェクト(複合キーの配列など)に対して:
      • << で要素を追加する
      • 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 の「共有は不変オブジェクトのみ」ルールを満たせます。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Active Storage のファイル名サニタイズ処理において、NUL 文字 (\0) を「危険な文字」として明示的に除去するようにした変更です。
    これにより、ユーザーアップロード由来のファイル名に NUL バイトが含まれている場合でも、ArgumentError: string contains null byte などの例外が発生しにくくなります。

  1. 変更内容の詳細

背景 ​

  • ユーザーがアップロードするファイル名に 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 が追加されたという変更です(実際のコードイメージ):

ruby
# 変更前(イメージ)
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 文字が含まれたファイル名がサニタイズ時に適切に処理されることを検証するテストが追加/調整されています。

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

    • 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 が今後はサニタイズ処理で削除される点に注意が必要です(ただし、通常の利用形態では問題にならないはずです)。

  1. 参考情報 (あれば)
  • Ruby の ArgumentError: string contains null byte は、C 側の文字列処理で NUL 終端を前提にしているため、バイナリ中に NUL が含まれているとファイル名などとして扱えずエラーになることが多いです。
  • 多くのファイルシステム(UNIX, Windows 含む)では、NUL 文字はファイル名に使えない予約文字となっており、今回の変更はその制約に沿ったサニタイズ強化といえます。

#58363 Make Proc layouts shareable ​

マージ日: 2026/8/4 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    Rails のレイアウトとして利用される Proc オブジェクトを、Ractor 対応のために「shareable(共有可能)」として扱うようにした変更です。これにより、特に turbo-rails が提供する Proc ベースのレイアウト定義などで、Ractor 実行時の問題を早期に検出しやすくなります。

  1. 変更内容の詳細

何が変わったか ​

対象ファイル: actionview/lib/action_view/layouts.rb

レイアウトが Proc で指定されている場合に、その Proc を ActiveSupport::Ractors.try_shareable_proc を使って「共有可能な Proc」に変換(あるいはチェック)する処理が追加されました。

イメージとしては、従来:

ruby
# 擬似コードイメージ
if layout.is_a?(Proc)
  @layout = layout
end

だったところが、以下のような形に近づいています:

ruby
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 のケースを分けて検証すること

などが確認されていると考えられます。


  1. 影響範囲・注意点

影響範囲 ​

  • Proc レイアウトを利用しているアプリケーション全般
    • コントローラで layout ->(controller) { ... } のように Proc を返すレイアウトを定義している場合が対象になります。
  • 特に turbo-rails のフレーム用レイアウト
    • turbo-rails では、turbo frame 用に Proc ベースの layout 定義が使われており(PR 説明でリンクされている箇所)、Ractor 環境下でこの変更の影響を受けます。

何が変わるのか(挙動面) ​

  • Ractor を使っていない(通常のシングルスレッド/マルチスレッド)環境では、挙動は基本的に従来と変わりません。
  • Ractor を利用する場合:
    • レイアウト用の Proc が Ractor 共有不可なオブジェクトをキャプチャしていると、try_shareable_proc の段階で問題が顕在化します。
    • これにより、「実際のリクエスト処理中に謎の Ractor 関連エラーが出る」のではなく、「アプリケーション起動や Ractor セットアップ時点で原因が分かる」形に近づきます。

注意点(開発者が見るべきポイント) ​

  1. Proc レイアウトの中身が Ractor 共有可能かを意識する必要がある

    • 例: レイアウト決定ロジックで、
      • クラス定数・シンボル・凍結済み文字列などの shareable なオブジェクトを使う分には問題ないが、
      • コントローラインスタンスのミューテーブルな状態や、非 shareable なオブジェクトを閉じ込めているとエラーになる可能性があります。
    • 典型例:
      ruby
      # 悪い例になりうる(イメージ)
      SOME_HASH = {}
      layout ->(controller) { SOME_HASH[controller.request.format] }
      # => ミューテーブルな Hash をキャプチャしているため共有不可になりうる
  2. Ractor サポートを試すときに、問題を早めに見つけられる

    • この PR の意図として、「アプリオーナーが Ractor サポートを試した時に、問題を直接(Proc レイアウト定義の箇所で)見つけられるようにする」という狙いがあります。
    • つまり、Ractor 対応に向けてアプリを修正する際、Proc レイアウトが早期にチェックされるようになります。
  3. turbo-rails 側の対応 PR との関係

    • turbo-rails 側でも、「Proc レイアウトをメソッドに置き換える」PR (#786) が既に存在し、こちらも合わせて導入されると、turbo-rails のフレームレイアウトに関する Ractor 問題はさらに軽減される見込みです。
    • ただし、Rails 本体としては Proc レイアウトをサポートし続けるため、その扱いを Ractor 観点でも安全にするという位置付けです。

  1. 参考情報 (あれば)

#58362 Freeze ActionView::Template::Handlers::ERB.escape_ignore_list ​

マージ日: 2026/8/4 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    ActionView::Template::Handlers::ERB.escape_ignore_list をアプリ起動時に freeze し、Ractor 内からも安全に参照できるようにした PR です。これにより、マルチ Ractor 環境での ActionView/ERB の設定利用がしやすくなります。

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

何をしているか ​

  • ActionView::Template::Handlers::ERB.escape_ignore_list を Rails 起動時に freeze する処理を追加
  • それに対応するテストを railties 側に追加

変更ファイル:

  • actionview/lib/action_view/railtie.rb
  • railties/test/application/configuration_test.rb

具体的なコードイメージ ​

ActionView::Railtie の初期化プロセスのどこかで、

ruby
ActionView::Template::Handlers::ERB.escape_ignore_list.freeze

のような処理が行われるようになっています(実際には config.after_initialize など、Rails のブートシーケンスに沿った場所)。

テスト側では、おおよそ次のようなことを検証しています:

ruby
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 するのが妥当、という判断です。


  1. 影響範囲・注意点

影響範囲 ​

  • 影響を受けるのは、ActionView::Template::Handlers::ERB.escape_ignore_list を アプリケーション起動後に変更しようとしているコード です。
  • 典型的な使われ方は以下のように「初期化時だけ変更」するパターンであり、これは今後も問題ありません:
ruby
# config/initializers/erb_escape.rb
ActionView::Template::Handlers::ERB.escape_ignore_list << :raw_html

Rails 起動プロセス中(初期化フェーズ)の段階で freeze されるため、

  • Rails 推奨の「initializer で設定する」スタイルなら基本的に動作します。
  • 本番稼働中に動的にこの配列を書き換えるようなコードは、freeze によって FrozenError が発生するようになります。

注意点 ​

  • もし以下のようにリクエスト処理中やジョブ実行中など、遅いタイミングで書き換えをしている場合はエラーになります:
ruby
# 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 から参照したりすることが可能になり、並行実行時の安全性・利便性が上がります。

  1. 参考情報 (あれば)
  • この設定は 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. 概要 (1-2文で)
    belongs_to 関連の内部実装で、単一外部キーと複合外部キーの扱いを ActiveRecord::Key に統一し、is_a?(Array) や Array(...) などの分岐を排除してコードを簡潔・一貫化した変更です。これにより、BelongsToAssociation(および多態関連)で複合キーを扱うための基盤が整理され、保守性と読みやすさが向上しています。

  1. 変更内容の詳細

全体方針 ​

  • 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(イメージ):

ruby
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
end

After(イメージ):

ruby
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 オブジェクトで扱える前提に整理されていると考えられます。

  1. 影響範囲・注意点

影響範囲 ​

  • 対象となるのは内部実装レベルで、通常のアプリケーションコード(belongs_to :user などの宣言や、user.post のような利用方法)は API 的には変わりません。
  • 想定される主な効果:
    • belongs_to / 多態 belongs_to での内部動作がシンプルになり、今後の複合キーサポートや拡張がしやすくなる。
    • 単一 FK / 複合 FK 両対応のコードパスが増え、複合主キー/外部キーサポートを強化する布石になっている。

注意点(開発者目線) ​

  • 内部 API の変更に依存している場合は注意
    以下のようなコードを書いている場合、動作に影響が出る可能性があります。

    • AR の内部クラス(BelongsToAssociation / BelongsToPolymorphicAssociation)を monkey patch している。
    • reflection.foreign_key が配列かどうかを前提に内部の動きを拡張している。
    • belongs_to 関連の内部変数を前提にカスタマイズしている。
  • 今後、複合キー対応が進むことを前提とした内部設計になっているため、

    • 「カスタム関連」や「独自の association クラス」を実装している場合は、
    • 将来的に ActiveRecord::Key を使うほうが Rails 本体との互換性を維持しやすくなりそうです。
  • 仕様変更というよりリファクタリングに近いため、通常の利用では挙動変化は最小限に抑えられていると考えられますが、

    • 複合キーを駆使しているアプリでは、edge case(nil を含む複合 FK、部分的に欠けたキーなど)が扱われ方の変化でバグがあぶり出される可能性があります。
    • CI で関連周り(belongs_to・多態 belongs_to)のテストをしっかり回しておくと安全です。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Mime::Type のレジストリと関連インデックスを「アプリケーションの eager load 完了後に凍結(freeze)」し、Ractor で共有可能な状態にする変更です。これに伴い、eager load 後に MIME type を登録/解除することは非推奨となり、既存の Mime 定数プロキシや ActionView::Base.default_formats が新しいレジストリを正しく参照するように調整されています。

  1. 変更内容の詳細

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 完了後の登録/解除を非推奨化 ​

これまで:

ruby
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 などで定義し、アプリケーションロード時に読み込まれるようにする
ruby
# config/initializers/mime_types.rb

Mime::Type.register "application/x-custom", :custom

2-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 後の挙動・非推奨警告・レジストリ再ポイント)が正しく動くかどうかを確認するテストを追加/修正

  1. 影響範囲・注意点

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 を用いる

3-3. Ractor を使うユーザにとってのメリット ​

  • Mime::Type 関連のデータ構造が Ractor 共有可能(ractor-shareable)になり、マルチ Ractor 構成で安全に再利用可能
  • その結果:
    • Ractor 間で MIME registry をコピーする必要が減る
    • メモリ使用量削減・パフォーマンス改善が期待できる

  1. 参考情報 (あれば)

#58326 Define Tags#field_type when the class is loaded ​

マージ日: 2026/8/4 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    ActionView::Helpers::Tags 系の field_type が「最初に使われたときに初期化される」実装だったため、メイン Ractor 以外で最初のフォームフィールドを描画すると例外になる問題を修正した PRです。
    クラスロード時に field_type を確定させ、Ractor 間で共有可能なように freeze することで、Ractor 環境でも安全にフォームヘルパを利用できるようにしています。

  1. 変更内容の詳細

背景 ​

  • もともと TextField などのタグクラスは、field_type をクラスインスタンス変数に遅延メモ化していました。
  • 遅延メモ化は「最初に呼ばれたタイミングで値をセット」するので、
    • 最初の呼び出しがメイン Ractor 以外だと、Ractor 間共有できないオブジェクトの書き込みや初期化順序の問題で例外が発生する状況がありました。
  • Ractor セーフにするには、
    • クラス定義時(ロード時)に field_type を確定させる
    • その値を freeze して Ractor-shareable にする
      という方針が取られています。

TextField クラスの変更 ​

対象ファイル: actionview/lib/action_view/helpers/tags/text_field.rb

主なポイント:

  1. クラスロード時に field_type を確定させる

    クラス本体の中で field_type を呼び出しておき、クラス定義時にメモ化させるようにしました。

    イメージとしては:

    ruby
    class TextField < Base
      # クラス本体の評価中に呼ぶことで、クラスロード時にメモ化される
      field_type
    end

    (実際のコードではメモ化ロジックと一緒に整理されているはずです。)

  2. TextField.inherited の実装

    TextField を継承したサブクラス(匿名でないもの)についても、同じようにクラス定義時に field_type を確定させるため、inherited フックが追加されています。

    概念的にはこういう処理をしています:

    ruby
    class TextField < Base
      class << self
        def inherited(subclass)
          super
          # 匿名クラスでなければ、ロード時に field_type を確定させる
          unless subclass.name.nil?
            subclass.field_type
          end
        end
      end
    end

    これにより、例えば以下のような継承クラスでも安全に Ractor から利用できます:

    ruby
    class CustomTextField < ActionView::Helpers::Tags::TextField
      # 独自の振る舞いを追加
    end
  3. field_type メモ化時に freeze

    field_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 をオーバーライドしていましたが、今回の方針に合わせて、
    • メソッドオーバーライドではなく
    • クラス本体からクラスインスタンス変数を直接設定する形
      へと変更されています。

イメージ:

ruby
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 のメモ化と共有が期待通りに動くこと
      を検証しています。

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

    • ActionView::Helpers::Tags::TextField とそのサブクラス
    • DatetimeLocalField
    • Ractor を利用した並行レンダリング環境(view レンダリングを並列化しているアプリなど)
  • 互換性:

    • 公開インターフェース(field_type の戻り値そのもの)には変化はなく、主に初期化タイミングと内部実装の変更です。
    • TextField やそのサブクラスで field_type を独自にオーバーライドしている場合でも、通常はそのまま動作します。
      • ただし、field_type が「Ractor-shareable ではないオブジェクト」(ミューテブルなオブジェクトなど)を返していると、Ractor 利用時に問題が発生する可能性があります。
        → String 等を返す場合は freeze される/することを前提にしておくのが安全です。
  • メモ化タイミングの変化:

    • これまで「最初にフィールドがレンダリングされたタイミング」で確定していた field_type が、
    • 「クラス定義時(=アプリ起動時/autoload 時)」に確定されます。
    • 一般的なアプリでは副作用はありませんが、「field_type 決定時に外部状態に依存するようなロジック」を書いている場合は、挙動が変わる可能性があります(そのような書き方は推奨されません)。

  1. 参考情報 (あれば)
  • Ractor と shareable オブジェクト:
    • Ruby 3 系の Ractor では、Ractor 間で共有するオブジェクトは Ractor.shareable? が true となる必要があります。
    • イミュータブルな String("foo".freeze)などは shareable ですが、ミューテブルなオブジェクトはそうではありません。
  • Rails における Ractor 対応方針:
    • メモ化やキャッシュを行うクラスインスタンス変数・クラス変数について、
      • 「クラスロード時に初期化する」
      • 「freeze する」
        といったパターンで Ractor 対応が行われつつあります。
  • 関連しそうなコード:
    • ActionView::Helpers::Tags::Base や他のフィールドクラス(CheckBox, RadioButton など)も、今後同様の変更が行われる/すでに行われている可能性があります。

#58357 Define DateTimeSelector datetime readers with shareable lambdas ​

マージ日: 2026/8/4 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    ActionView::Helpers::DateHelper::DateTimeSelector が Ractor 内からも安全に使えるように、year/month/day/hour/min/sec のリーダーメソッド定義方法を「Ractor 共有可能な lambda」を使った形に変更した PR です。あわせて、この挙動を検証する Ractor 対応テストが追加されています。

  1. 変更内容の詳細

何が問題だったか ​

DateTimeSelector は、select_datetime などで <select> ベースの日付・時刻入力フィールドを描画するときに内部的に使われるクラスです。このクラスは、渡されたオブジェクトから year, month, day, hour, min, sec を取り出すための reader メソッドを define_method で定義していました。

もともとの実装はおおよそ次のようなイメージです:

ruby
# 疑似コード: 実際とは多少異なる可能性あり
[: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 を使って定義する形に切り替えています。イメージとしては以下のような変更です(概念的な例):

ruby
# 以前(イメージ)
[: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 を問題なく呼べる」ことを保証しています。


  1. 影響範囲・注意点
  • 対象: ActionView::Helpers::DateHelper::DateTimeSelector が内部的に使われるすべての date/time <select> ヘルパー

    • select_date
    • select_time
    • select_datetime
    • date_select
    • time_select など
  • 既存の挙動への影響:

    • メソッドの外部インターフェース (year, month 等) は変わっていないため、通常の(シングルスレッド/非 Ractor)環境での振る舞いは基本的に変わりません。
    • 主な変更は「内部のメソッド定義手段」であり、HTML 出力内容は変化しないことが前提です(テストで担保)。
  • Ractor を使っている/使う予定のアプリへの影響:

    • これまで Ractor 内で select_datetime 等を使うとエラーになっていた場合でも、この変更により正常に動作することが期待できます。
    • Ractor セーフティに関わる変更のため、Ractor を活用しているアプリでは、アップデート後に date/time フォームヘルパー周りのテストを一度通しておくと安心です。
  • 実装パターンへの示唆:

    • 「Ractor から呼び出される可能性のある DSL/ヘルパーで define_method + ブロック を使う場合、共有可能 lambda を検討する」という Rails コアの方針の一例と言えます。
    • 同様のパターンをアプリ側や gem 側で使っている場合も、Ractor 対応を意識するなら、module_eval や shareable lambda でのメソッド定義を検討する余地があります(PR 説明にも module_eval 案の言及あり)。

  1. 参考情報 (あれば)
  • 該当 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. 概要 (1-2文で)
    Active Model / Active Record の属性タイプ情報(attribute_types)を Ractor 内から安全に参照できるようにするため、属性タイプのオブジェクトグラフを凍結(deep freeze)し、暗号化関連タイプのメモ化ロジックを Ractor セーフな形に修正した PR です。これにより、Ractor 内で属性の型を取得しても分離エラーが発生しないようになります。

  1. 変更内容の詳細

背景: なぜ問題だったか ​

  • Active Model の attribute_types はハッシュをメモ化しており、そのハッシュや内部にぶら下がるオブジェクトが「Ractor シェア可能」になっていませんでした。
  • Ractor 間でオブジェクトを共有するには、Ruby の制約上:
    • Frozen であること
    • その内部に参照しているオブジェクトも含めて、全体として「shareable」であること
      が必要です。
  • そのため、Ractor 内で model.attribute_types["some_column"] のように型情報へアクセスしようとすると、Ractor の分離制約によりエラーが発生していました。

対応方針 ​

  1. attribute_types のハッシュを単に freeze するだけでは不十分

    • 中身の Type オブジェクトや、そのさらに内部でメモ化されるオブジェクトも含めて「deep freeze」する必要があります。
  2. Active Model / Active Record のほとんどの型クラスはそのまま凍結可能

    • 追加のコード変更なしに凍結できない一部のケース(特に暗号化周り)のみを修正しています。
  3. 暗号化 (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 可能になります。

主なファイルごとの変更ポイント ​

  • activemodel/lib/active_model/attribute_registration.rb

    • attribute_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.rb

  • activerecord/lib/active_record/encryption/key_provider.rb

  • activerecord/lib/active_record/encryption/scheme.rb

    • これらの暗号化関連クラスで使われているメモ化ロジックを修正。
    • 主な内容:
      • falsy を返しうるメソッドで @var ||= ... を使わないようにし、defined?(@var) を使った early return 方式に変更。
      • これにより、暗号化 Type / Scheme / Key Provider のオブジェクトグラフが一度確定して以降は変化せず、凍結可能になる。

  1. 影響範囲・注意点
  • Ractor を使っていないアプリへの影響

    • ほとんどの場合、動作の変化はありません。
    • ただし暗号化関連のメモ化ロジックが若干変わるため、
      • 「falsy が返ること」を前提に、毎回計算されることに依存していたような特殊なコードがあれば挙動差が出る可能性はあります(通常の利用では考えにくいパターンです)。
  • Ractor を使っている / 使う予定のアプリへのメリット

    • Ractor 内で Model.attribute_types や個々の属性型オブジェクトへアクセスしても Isolation エラーにならなくなります。
    • Ractor ベースの並列処理で ActiveRecord / ActiveModel を利用する場合の安全性が向上します。
  • パフォーマンスへの影響

    • 型情報・暗号化関連オブジェクトのメモ化を「一度きり確定」させて凍結するため、実行時のオーバーヘッドはむしろ安定方向に働くと考えられます。
    • 凍結や eager なメモ化に伴うごく小さなコストはありますが、通常のアプリで問題になるレベルではないはずです。
  • カスタム Type・暗号化拡張を書いている場合の注意

    • 自前で ActiveModel::Type や ActiveRecord::Encryption を拡張している場合:
      • 内部状態が実行中に変化し続けるような設計だと、将来的に Ractor との相性問題が起きる可能性があります。
      • この PR の方針(「Type オブジェクトは、生成されたら基本的に不変・凍結されうる」)に沿っているかを確認した方が安全です。

  1. 参考情報 (あれば)
  • Ruby Ractor 仕様(shareable オブジェクトの制約):
    • オブジェクトを Ractor 間で共有するには、Ractor.shareable? が true になる必要があり、そのためには frozen かつ内部構造も含めて共有安全である必要があります。
  • この 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. 概要 (1-2文で)
    belongs_to におけるカウンタキャッシュ、autosave、has_many :through の :nullify 削除処理で、複合外部キー(Array) と エイリアス付き外部キー(仮想属性/別名カラム) が正しく扱われるように修正した PR です。これにより、これらの機能が複合 FK / エイリアス FK 環境でもサイレントに無視されたり、間違った SQL を生成したりしなくなります。

  1. 変更内容の詳細

背景となる問題点 ​

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 を使うことで、
      「仮想属性名」と「実カラム名」のズレを吸収

イメージとしては、従来:

ruby
# 擬似コード
where(foreign_key => previous_value) # foreign_key が配列でも to_s されてしまう

だったものが、複合キー前提で:

ruby
# 擬似コード (イメージ)
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 つでも変更されていれば「変更あり」と判定
  • エイリアスに対応:

    • ループ内で、実際にトラッキングされている属性名に解決したうえで
      will_save_change_to_attribute? を呼ぶ (alias 解決をインラインで行う)

擬似イメージ:

ruby
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 をセット

擬似イメージ:

ruby
# 以前 (問題あり)
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 の変更を検知できるか
  • activerecord/test/cases/associations/has_many_through_associations_test.rb (+14)

    • has_many :through + dependent: :nullify で、複合 / エイリアス FK が正しく NULL になるか

  1. 影響範囲・注意点
  • 影響を受けるのは、以下を満たすプロジェクトです:

    • ActiveRecord で
      • 複合外部キー (Array で指定される FK)
      • もしくは エイリアス付き外部キー (attribute と実カラムが異なる) を使っている
    • かつ次のいずれかを利用している:
      • belongs_to ... , counter_cache: true
      • autosave (accepts_nested_attributes_for や autosave: true など)
      • has_many :through, dependent: :nullify または through 先に対して :nullify 削除
  • これまでの挙動との違い:

    • これまでは「サイレントに効いていなかった」「不正な SQL を生成していた」ため、
      実運用では
      • カウンタが更新されていない
      • 関連が自動保存されない
      • :nullify がエラーで落ちる / うまく nullify されない といった不具合が発生していた可能性があります。
    • 本 PR 取り込み後は「本来の正しい挙動」をするようになるため、
      • 今まで動かなかったロジックが動くようになる
      • 一見すると「挙動が変わった」ように見えるかもしれませんが、 仕様としては修正前がバグであり、これが正しい挙動です。
  • マイグレーション/設定面での注意:

    • もしアプリ側で reflection.foreign_key を直接文字列前提で扱っているコードがある場合、
      Rails 本体と同様に「配列を許容する」ように修正した方がよいです
      • 例: Array(reflection.foreign_key) で正規化してループ
      • エイリアスを使う場合は、arel_table[attribute] 経由で解決するなど

  1. 参考情報 (あれば)
  • PR 本体: https://github.com/rails/rails/pull/58364
  • 関連概念:
    • ActiveRecord::Reflection#foreign_key
    • ActiveRecord::Key#where_hash
    • Arel::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. 概要 (1-2文で)
    belongs_to の外部キーに alias_attribute を定義している場合に、変更検知(Dirty API)がエイリアス名ではなく実カラム名でしか動かず、一部機能が壊れていた問題を修正した PR です。belongs_to が Dirty API にアクセスする箇所で、常に「エイリアスを実カラム名に解決してから」参照するように統一されています。

  1. 変更内容の詳細

背景となる問題 ​

Rails では:

ruby
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 の説明を踏まえた、典型的な状況:

ruby
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 参照箇所で一貫して使用するようになっています。

  1. 影響範囲・注意点
  • 影響を受けるのは「belongs_to 外部キーに alias_attribute を付けている」ケースのみ
    • 通常の belongs_to には挙動の変化はほぼない(バグ修正のための内部挙動の整合性改善)
  • 既にこのバグを前提に「ワークアラウンド」を書いていた場合(例: 独自でカウンタ調整や touch をしている)、二重で動く可能性があるので、マージされたバージョンに上げたときは以下を確認するとよいです:
    • counter_cache が二重加算・二重減算されていないか
    • touch を手動で呼んでいないか
  • belongs_to_required_validates_foreign_key = false を使っている環境では、エイリアス付き外部キーでもバリデーション挙動が変わる可能性があるので、フォーム送信〜保存周りの動きを一度確認するのが安全です。

  1. 参考情報 (あれば)
  • この PR の前提となる修正: #58348
  • 関連する Rails 機能:
    • alias_attribute
    • belongs_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. 概要 (1-2文で)
    Rails公式ガイドの「Wishlists」ガイド内で使われているテーブル名の誤記を、正しいテーブル名に修正したドキュメント向けのPRです。コードではなくガイド文書のみの変更で、挙動には影響しません。

  2. 変更内容の詳細

  • 変更ファイル: guides/source/wishlists.md
  • 変更内容は1行のみで、「Wishlists」チュートリアル中に登場するテーブル名を、実際のモデル/マイグレーションで使われるべき正しいテーブル名に合わせる修正です。
  • タイトルに [ci skip] が付いていることからも分かる通り、テストやビルドを走らせる必要のないドキュメントのみの修正です。

(実際の diff は1行の +1/-1 で、例えば以下のようなニュアンスの修正が行われています):

diff
- In our migration, we created the table `wishlist`:
+ In our migration, we created the table `wishlists`:

あるいは:

diff
- The products_wishlist table stores...
+ The products_wishlists table stores...

といった形で、Railsの命名規約(モデル: 単数形 / テーブル: 複数形)に沿ったテーブル名に直されたものと考えられます。

  1. 影響範囲・注意点
  • 影響範囲はRailsガイドの読者のみで、アプリケーションコードやRails本体の挙動には一切影響しません。
  • ただし、旧バージョンのガイドを参考にしてテーブル名を誤って単数形や不適切な名前で作っていた場合、以下のような不整合が発生している可能性があります:
    • モデル Wishlist とテーブル wishlist のような単数形テーブル名の不一致
    • 結果として ActiveRecord がテーブルを見つけられず、ActiveRecord::StatementInvalid や「relation does not exist」エラーになる
  • これから「Wishlists」ガイドを参考に実装する場合は、このPR適用後のガイドを参照し、Railsの命名規約に沿ったテーブル名(複数形)で作成するのが安全です。
  1. 参考情報 (あれば)
  • Railsのモデルとテーブルの命名規約:
    • モデル: 単数形・CamelCase(例: WishlistItem)
    • テーブル: 複数形・snake_case(例: wishlist_items)
  • 関連ガイド:
    • 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. 概要 (1-2文で)
    Single Table Inheritance(STI) のドキュメント中にある type カラムの変更例が実際の挙動と食い違っていたため、「空文字列 "" からの変更」となっていた説明を「nil からの変更」に修正した PR です。ドキュメントのみの変更で、挙動・API には一切の変更はありません。

  1. 変更内容の詳細

何を修正したか ​

ActiveRecord の STI 概要ドキュメント(activerecord/lib/active_record/inheritance.rb のコメント部分)にある、「type カラムがどのようにセットされるか」の例が以下のようになっていたと考えられます(旧記述イメージ):

ruby
Firm.new.changes # => {"type"=>["", "Firm"]}

しかし、実際には ensure_proper_type による type の自動セットは「空文字列」ではなく「nil」からの変更として扱われます。つまり、正しい挙動は:

ruby
Firm.new.changes # => {"type"=>[nil, "Firm"]}

この PR は、その「旧値」を表すドキュメント上の値を "" から nil に修正しています。

Ruby バージョンと Hash#inspect 表記の話 ​

PR 説明中にある:

The pre-3.4 Hash#inspect style 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"]}

    のような => スタイルを使い続けて、周辺の例と一貫性を保っている、という意図です。


  1. 影響範囲・注意点
  • 実行コードの変更は一切なく、「コメント(ドキュメント)のみ」の修正です。

  • STI を利用しているアプリの挙動(type カラムのデフォルト値や changes の内容)は、以前から nil を前提としており、そこには変更はありません。

  • もし既存のアプリケーション側のテストやドキュメントで「type の旧値が "" になる」ことを前提にして書いていた場合は、それが誤解に基づいている可能性があります:

    • 新規レコード生成時の changes 例は、以下のように nil を期待するのが正しいです。

      ruby
      firm = Firm.new
      firm.changes # => {"type"=>[nil, "Firm"]}
  • データベースレベルで type カラムにデフォルト値 ''(空文字)を入れているような特殊なスキーマを使っていない限り、今回のドキュメント修正が実アプリの挙動と食い違うことはありません。


  1. 参考情報 (あれば)

#58348 Restore alias_attribute support in associations ​

マージ日: 2026/8/3 | 作成者: @kamipo

  1. 概要 (1-2文で)
    このPRは、belongs_to などの関連付けで長らく効かなくなっていた alias_attribute のサポートを復活させ、外部キーや主キーに定義したエイリアス経由でも関連が正しく読める/書けるように戻す変更です。過去にパフォーマンス目的で導入された _read_attribute 経由の「高速パス」を廃止し、再び read_attribute / write_attribute を使うようにしています。

  1. 変更内容の詳細

何が問題だったか ​

もともと belongs_to などの関連は、所有側(owner)レコードから外部キーを読むときに ActiveRecord::Base#[] を使っていました。Base#[] は内部的に read_attribute を呼ぶため、alias_attribute が定義されていてもそれを解決してくれます。

しかし、過去のコミット be2b98b4ae でパフォーマンス改善のために _read_attribute(内部用の高速メソッド)を使うようになりました。_read_attribute は

  • alias 解決をしない
  • primary key の判定などもスキップする

ため、外部キーや主キーに対して定義した alias_attribute が、関連経由では一切効かなくなるという副作用がありました。
その結果、以下のようなコードは期待どおりには動作していませんでした:

ruby
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 を外部キー側・主キー側に定義したケースで関連が正しく動くことを検証

おおまかなイメージ:

ruby
# 以前(問題があった状態)
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 説明にある例:

ruby
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 を定義しているケースでも同様に解決されるようになります。


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

    • Active Record の全ての主要な関連(belongs_to / has_one / has_many / has_many :through / ポリモーフィック / preloader / autosave 含む)の外部キー・主キーの読取/書込処理
    • これまで「alias_attribute が関連で効かない」と思っていた部分が、Rails のアップデートにより 突然期待どおりに(あるいは、以前とは違う挙動で)動き始める 可能性があります
  • 互換性・注意点:

    • 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 を絡めたテスト用定義が追加されており、実際のユースケースが想定されています

  1. 参考情報 (あれば)
  • 当該PR:
    • Restore alias_attribute support in associations (#58348)
  • 関連する背景Issue/PR:
    • #57795 — 本番で _read_attribute を上書きしてまで alias を効かせていた、という背景が言及されている
    • 過去の最適化:
      • #36052 — primary key アクセスの高速化(@primary_key キャッシュ)
      • commit 5575bd7b22 — attribute_alias? 呼び出し削減による alias 解決の高速化
      • commit 27a1ca2bfe — _read_attribute への delegating の inline 化

このPRにより、「関連周りでも alias_attribute は素直に効く」という、本来期待される挙動に戻ったと考えてよく、今後はワークアラウンドではなく素の API に依存できるようになります。


#58347 Deprecate write_attribute(:id, value) writing to the primary key ​

マージ日: 2026/8/3 | 作成者: @kamipo

  1. 概要 (1-2文で)
    write_attribute(:id, value) がカスタム主キー(@primary_key)を書き換える挙動を非推奨にし、今後は :id というカラムに対する通常の書き込みとして扱うようにする変更です。read_attribute(:id) の非推奨・削除 (#49019) に続く「書き込み側」の整合性対応であり、複合主キーの場合も読み取り側と同じ扱いに揃えています。

  1. 変更内容の詳細

背景 ​

  • 以前の PR (#49019) で、
    • read_attribute(:id) が「カスタム主キーの値」を返す挙動は非推奨 → のちに削除されました。
    • 替わりに read_attribute(:id) は「本当に id というカラムがあればその値」を返す、というより素直な仕様に変わっています。
  • しかし書き込み側の write_attribute(:id, value) はまだ「id を @primary_key に変換して書き込む」という旧仕様のままでした。
  • この PR はその「片側だけ残っていた」挙動を整理し、読み書きの仕様を揃えるものです。

具体的な変更点 ​

1) write_attribute(:id, value) の扱いの変更・非推奨化 ​

以前(問題となっている挙動):

ruby
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 の内部で以下のようなロジック変更が入っています(イメージ):

ruby
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) に続く変更であること

  1. 影響範囲・注意点

影響を受けるケース ​

  1. カスタム主キーを使っているモデル で、
  2. かつ write_attribute(:id, ...) を使って主キーを書き換えている コードがある場合

例:

ruby
class Account < ApplicationRecord
  self.primary_key = :account_uuid
end

account = Account.new
account.write_attribute(:id, "foo")  # ← ここが非推奨

このようなコードは:

  • すでに deprecation warning を出し始める(か、出すように予約される)
  • 将来的な Rails のメジャーバージョンアップ時に、挙動が変わり「account_uuid ではなく id カラムに書き込まれる」ようになるリスクがあります。

推奨される書き方 ​

カスタム主キーに値を書きたい場合は、明示的に主キー名を指定してください。

ruby
# 推奨:
account.write_attribute(:account_uuid, "foo")
# もしくは:
account.account_uuid = "foo"

write_attribute(:id, ...) を「主キーエイリアス」として使う前提のコードは、deprecation 期間中にすべて置き換えるべきです。

複合主キーを擬似的に使っている場合 ​

  • 自前実装や gem(composite_primary_keys など)で複合主キーを扱っているアプリで、
    • write_attribute(:id, ...) を「複合主キーの代表キー」扱いしているようなコードがあると、
    • 将来のバージョンで意味が変わる(単なる id カラム更新になる)可能性があります。
  • ここでも同様に「本当に書きたいカラム名・主キー名」を明示的に使うようリファクタリングしてください。

  1. 参考情報 (あれば)
  • 該当 PR:
    • Deprecate write_attribute(:id, value) writing to the primary key — #58347
  • 関連 PR(読み取り側の変更):
    • Deprecate and remove read_attribute(:id) returning the custom primary key value — #49019
  • 修正ファイル:
    • activerecord/lib/active_record/attribute_methods/write.rb
    • activerecord/test/cases/primary_keys_test.rb
    • activerecord/CHANGELOG.md

#58349 Fix test class name collision in Active Storage railties tests ​

マージ日: 2026/8/2 | 作成者: @flavorjones

  1. 概要 (1-2文で)
    Active Storage の railties テストで、2つのテストファイルが同じテストクラス名 ApplicationTests::ActiveStorageEngineTest を定義していたために発生していたクラス名衝突を解消する PR です。クラス名をファイルごとに固有の名前に分けることで、ローカル実行時に発生していた環境変数リーク検出テストの失敗を防いでいます。

  1. 変更内容の詳細

問題の状況 ​

以下の2ファイルが、どちらも同じテストクラス名を定義していました。

  • railties/test/application/active_storage/analyzers_integration_test.rb
  • railties/test/application/active_storage/engine_integration_test.rb

両方とも ApplicationTests::ActiveStorageEngineTest を定義していたため、1プロセス内で両ファイルを読み込んだ場合にクラス定義が「上書き」されます。その結果、setup / teardown で行っている処理が意図した通りに実行されず、環境変数 BUNDLE_GEMFILE が元に戻らないなどの「環境リーク」が発生していました。

テストの失敗メッセージ例:

text
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 側のクラス名を変更しています。

(イメージとしては以下のような変更)

ruby
# 変更前: 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::AnalyzersIntegrationTest
  • engine_integration_test.rb → ApplicationTests::ActiveStorageEngineTest (従来のまま)

と、ファイルごとに異なるテストクラス名が割り当てられるようになりました。

また、新しいクラス名 AnalyzersIntegrationTest は同ディレクトリ内の他テストと命名規則を揃えています:

  • DirectUploadsIntegrationTest
  • UploadsIntegrationTest
  • ValidatingServiceTest

ファイル名とクラス名を揃える Rails の慣習にも合致します。


  1. 影響範囲・注意点
  • 対象: railties/test/application/active_storage/* のテストのみ
    アプリケーションコードや公開 API には影響しません。
  • 影響内容:
    • ローカル環境で bin/test を使って railties の Active Storage 関連テストをまとめて実行した際の「環境リーク検出によるテスト失敗」が解消されます。
    • CI はもともと test:isolated によりファイルごとにプロセスを分けていたため、挙動は変わりません。
  • 注意点:
    • テストクラス名を直接参照しているメタテストやツール(例えば特定クラスだけ実行するようなカスタムツール)を使っている場合は、新しいクラス名 ApplicationTests::AnalyzersIntegrationTest に合わせて修正が必要になる可能性があります。
    • ただし、通常の bin/test / rake test の利用では影響はありません。

  1. 参考情報 (あれば)

#58254 Make CommandRecorder#record and #inverse_of private ​

マージ日: 2026/8/2 | 作成者: @kamipo

  1. 概要 (1-2文で)
    ActiveRecord の ActiveRecord::Migration::CommandRecorder クラスにおいて、内部用メソッドである #record と #inverse_of を public から private に変更した PR です。将来的なキーワード引数の扱い変更(Hash.ruby2_keywords_hash 廃止への対応)に伴ってメソッドシグネチャを壊さざるを得ないため、それを「公開 API 変更」扱いにしないよう、先に「内部 API」であることを明確化しています。

  1. 変更内容の詳細

背景 ​

  • 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 にした」旨のエントリを追記し、位置づけを明示。

  1. 影響範囲・注意点

影響を受ける可能性があるコード ​

  • 次のようなコードを書いている場合は、明確に壊れます(か、少なくとも将来のバージョンで壊れるリスクが高いです):

    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 をラップする、または独自にコマンド記録の仕組みを持つなど)を検討してください。

  1. 参考情報 (あれば)
  • この PR が依存している/前提としている関連 PR:
    • #58238: ruby2_keywords の呼び出し削除
    • #58239: CommandRecorder などでの引数保存構造を「位置引数とキーワード引数を分けて保持する」形式に変更する予定の PR
  • ドキュメント:

#58344 Pin redis below 6 in the Gemfile ​

マージ日: 2026/8/2 | 作成者: @yahonda

  1. 概要 (1-2文で)
    Rails の Gemfile で redis のバージョンを「6未満」に固定し、テストが redis-6.0.0 のリリースによって落ちる問題を回避する PR です。Action Cable の redis サブスクリプションアダプタが要求するバージョン制約(>= 4, < 6)と実際にロードされる gem のバージョンの不整合を解消しています。

  1. 変更内容の詳細

背景 ​

  • 2026-07-31 に redis 6.0.0 がリリースされた。

  • Rails の Action Cable の Redis アダプタは、ロード時に以下のような依存関係を宣言しています:

    ruby
    gem "redis", ">= 4", "< 6"
  • しかし、Gemfile では redis バージョンを特に 6 未満に固定しておらず、bundle install すると自動的に redis-6.0.0 が入る状況になっていました。

  • その結果、テスト実行時に以下のようなエラーが発生:

    text
    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. Make sure all dependencies are added to Gemfile.

    つまり「redis は 4 以上 6 未満が必要なのに、すでに 6.0.0 が有効化されているので条件を満たさない」という状態です。

この PR での実際の変更 ​

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

  1. Gemfile
  2. Gemfile.lock

差分としては、それぞれで redis のバージョン指定を「6 未満」にピン留めしています(+1/-1 行ずつ)。

イメージとしては、Gemfile が以下のようになる変更です(実際の PR と若干表記が異なる可能性はありますが、意味としてはこれ):

ruby
# 変更前(例)
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 の対象ブランチ(おそらく安定版ブランチ)は、まだ従来の redis gem ベースのアダプタを使っている。
  • ここで安易に上限を引き上げると、main ブランチとの差分(実装・依存関係)が広がってしまい、メンテナンスが複雑になる。
  • さらに、redis 6 への正式対応が行われておらず、動作保証もされていないため、テスト落ちの一時的な回避ではなく「未サポートバージョンを Gemfile 側で明示的に避ける」という解決策を選んでいる。

  1. 影響範囲・注意点

影響範囲 ​

  • Rails 開発環境および CI 環境で利用される redis gem が「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 はその移行までの「つなぎ」の対応です。

  1. 参考情報 (あれば)

#58265 Consolidate MySQL ALGORITHM / LOCK into MySQL::AlterTable state ​

マージ日: 2026/8/2 | 作成者: @kamipo

  1. 概要 (1-2文で)
    MySQL アダプタにおける ALTER TABLE の ALGORITHM / LOCK 指定を、各操作ごとのバラバラな扱いから MySQL::AlterTable オブジェクトの状態として一元管理するようにした PR です。これにより、単発・バルクいずれのスキーマ変更でも、正しい位置・回数で ALGORITHM / LOCK が出力され、オプションが落ちる不具合も解消されます。

  1. 変更内容の詳細

2-1. これまでの問題点 ​

PR 説明の通り、従来は以下の問題がありました。

  1. 各単発操作ごとの重複実装
    add_column, change_column, rename_column, remove_column などの 単発 MySQL 操作ごと に、以下のようなパターンで SQL 末尾に ALGORITHM / LOCK を付け足していました。

    ruby
    sql = "ALTER TABLE `table` ADD COLUMN ..."
    sql << ", #{algorithm}" if algorithm
    sql << ", #{lock}" if lock
    execute(sql)

    同じロジックが複数箇所に分散しており、保守性が低い状態でした。

  2. 複数インデックスをまとめて追加/削除する場合の SQL が不正
    MySQL::AddIndex / MySQL::DropIndex は自身が algorithm / lock を持ち、visit_AddIndex / visit_DropIndex が各フラグメントごとにそれを出力していました。
    そのため、ALTER TABLE ... の一文の中で複数インデックスを操作すると、例えば次のように 同じ ALGORITHM / LOCK が N 回繰り返される SQL になり、MySQL が拒否していました。

    sql
    ALTER TABLE `users`
      ADD INDEX `idx_1` (...), ALGORITHM = INPLACE, LOCK = NONE,
      ADD INDEX `idx_2` (...), ALGORITHM = INPLACE, LOCK = NONE;

    MySQL の文法上、ALGORITHM / LOCK は ALTER TABLE 文全体に対するヒントであり、文末に 1 回だけ書くのが正しい形です。

  3. バルクモードでの algorithm: / lock: オプションが無視されていた
    change_table ブロック内で t.column, t.change などに algorithm: / lock: オプションを渡しても、#{command}_for_alter 経由の処理でそれらが抽出されておらず、SQL に反映されない(黙って落ちる) 状況でした。

    ruby
    change_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 に以下の状態を追加:
    • algorithm
    • lock
  • それぞれの setter は、既存のフォーマッタである
    • index_algorithm
    • lock_clause を通して正しい SQL 断片に変換されるようになっています
      (例: :inplace → "ALGORITHM = INPLACE" など)。

visit_AlterTable のオーバーライド ​

MySQL::SchemaCreation#visit_AlterTable をオーバーライドして、ALTER TABLE 文の末尾に一度だけ ALGORITHM / LOCK を追加するように変更されました。

概念的には以下のような出力になります。

sql
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_column
  • remove_column
  • change_column
  • rename_column
  • add_index
  • remove_index

これらは、渡されたオプションから :algorithm / :lock を抽出して AlterTable 自身の状態に反映するように変更されました。
そのため、

  • change_table ... bulk: true の中
  • 単発で add_column, add_index などを呼ぶ場合

のいずれでも、処理フロー上は 同じ「抽出 → AlterTable にセット」のロジック が使われます。

内部的には、extract_algorithm_and_lock! というプライベートヘルパーで、オプションから該当キーを抜き取りつつ AlterTable の algorithm / lock にセットしている形です。

サンプル(概念的な Ruby 側のコードイメージ):

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) を呼ぶだけ

というシンプルなスタイルになりました。

擬似コードイメージ:

ruby
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: オプションが無視されず反映されること

などを確認するためのものと考えられます。


  1. 影響範囲・注意点

3-1. 開発者視点での挙動変化 ​

Rails の公開 API レベルでは、概ね「期待どおりに直った」変更であり、破壊的変更はほとんどありませんが、細かい挙動差は出ます。

  • change_table ... bulk: true で algorithm: / lock: オプションを指定した場合
    以前は黙って無視されていましたが、今後は 実際に ALTER TABLE に反映 されます。
    • これにより、既存アプリで「実は無視される前提」で動いていた場合、実際にオンライン DDL などが走るようになり、ロック時間や DDL の挙動が変わる可能性があります。
  • 複数のインデックス追加/削除をまとめた ALTER TABLE
    • 従来は MySQL に拒否されていたケース(ALGORITHM / LOCK が N 回出る)が、正しい 1 回のみの付与で成功するようになる ため、エラーが解消されます。

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 側ではヒントを扱わない設計に合わせる

などの追従が必要になる可能性があります。


  1. 参考情報 (あれば)
  • MySQL の ALTER TABLE 構文 (ALGORITHM / LOCK が文全体のヒントであること):
  • この PR に関連しうる Rails 側のクラス/ファイル:
    • ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter
    • ActiveRecord::ConnectionAdapters::MySQL::SchemaCreation
    • ActiveRecord::ConnectionAdapters::MySQL::SchemaDefinitions
    • ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements
  • PR 本体(#58265):
    • (公式 Rails リポジトリの PR ページを参照)

#58340 Transform keys in place in ActionController::Parameters#deep_transform_keys! ​

マージ日: 2026/8/2 | 作成者: @55728

  1. 概要 (1-2文で)
    ActionController::Parameters#deep_transform_keys! が、名前に反してインプレース(破壊的)ではなく新しいハッシュを組み立てていた問題を修正し、内部で _deep_transform_keys_in_object! を使って本当にインプレース変換するようにしたPRです。これによりAPIの一貫性とパフォーマンス(割り当て数削減)が改善されますが、返される Parameters オブジェクトの見かけ上の挙動は変わりません。

  1. 変更内容の詳細

もともとの問題点 ​

  • 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 deep5,825 allocations1,612 allocations

オブジェクト割り当て数が大幅に減っているため、GC負荷とメモリ使用量の面でメリットがあります。

動作イメージ(概念的なサンプル) ​

ruby
params = ActionController::Parameters.new(
  "user" => {
    "first_name" => "Alice",
    "last_name"  => "Smith"
  }
)

# キーをシンボル化する例
params.deep_transform_keys!(&:to_sym)

# 返り値・内容は変更前後で同じ:
# => #&lt;ActionController::Parameters {:user=>{:first_name=>"Alice", :last_name=>"Smith"}} permitted: false>

コード上の挙動(結果として得られる params)は変わりませんが、その内部で「新しいハッシュを組み立てる」のではなく「元のハッシュやネストされたハッシュのキーを書き換える」ようになった、という変更です。


  1. 影響範囲・注意点

影響範囲 ​

  • 対象: 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! によって、その共有しているオブジェクトのキーがインプレースで書き換わるようになりました。

つまり:

ruby
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 アプリケーションでの影響は軽微かつ後方互換的と考えられます。


  1. 参考情報 (あれば)
  • 対象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. 概要 (1-2文で)
    このPRは、ALTER TABLE 文の生成処理を AlterTable クラスに集約し、複数の操作を一度に行う際に不正なSQLが生成されうるバグを根本的に解消しつつ、MySQL/PostgreSQL 含めたアダプタ全体の設計を整理・リファクタリングしたものです。bulk_change_table などの経路もすべて AlterTable を経由するよう統一されています。

  1. 変更内容の詳細

背景となるバグ ​

これまで visit_AlterTable では、adds / foreign_key_adds などスロットごとに join(" ") した文字列同士をそのまま連結しており、異なるスロットにまたがって複数の操作が追加されると、間に区切りが入らず不正な SQL になる可能性がありました。

しかし、実際には各呼び出し側が「1回の AlterTable につき1操作」しか追加していなかったため、この問題は表面化していませんでした。

このPRでは、AlterTable に操作の順序付きリストを一本化し、SQL をそこからのみ組み立てるようにすることで、設計レベルで問題を解消しています。


AlterTable を「単一の真実のソース」にするリファクタ ​

1) 操作ごとの Data オブジェクト追加 ​

AlterTable がもつ操作を、以下のような Data.define で定義された小さな値オブジェクトで表現するようになりました。

例(PR説明より):

  • AddColumnDefinition
  • AddForeignKey
  • DropColumn
  • RenameColumn
  • ChangeColumnNull
  • AddIndex
  • PostgreSQL固有:
    • ValidateConstraint
    • AddExclusionConstraint
    • AddUniqueConstraint など

AlterTable インスタンスは、これらを1本の配列 operations に順序通りに保持します。

ruby
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 して最後に無分別に連結する」ことがなくなる

これにより、

sql
ALTER TABLE users
  ADD COLUMN age integer,
  ADD CONSTRAINT ...

のように、複数操作混在の SQL も正しく生成されます。

3) 接続アダプタ側の build ロジックを AlterTable に移管 ​

これまで接続アダプタ側(AbstractAdapter/MySQL/PostgreSQL)にあった「ALTER TABLE 用の断片生成メソッド」が AlterTable に移されました。

移管対象(例):

  • build_add_column_definition
  • build_change_column_definition
  • build_change_column_default_definition

構成としては:

  • 抽象的な共通部分: AlterTable(抽象クラス相当)
  • DB 固有の部分:
    • MySQL: Mysql::AlterTable(あるいは MySQL 用サブクラス)
    • PostgreSQL: PostgreSQL::AlterTable

に持たせる形になっています。

なお、: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 で:
    1. ALTER TABLE ... を execute
    2. deferred_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 を使うすべての呼び出し方が、

  1. ALTER TABLE 文を組み立て・実行
  2. deferred_operations を実行

というパターンに統一されるよう、プライベートヘルパ execute_alter_table(at) が導入されています。

これにより:

  • 「どのパスで ALTER TABLE を呼んでも後処理の実行漏れがない」
  • 共通化によりコード重複とバグリスクを削減

といったメリットがあります。


  1. 影響範囲・注意点

影響範囲 ​

  • 対象:
    • 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 にフックしていたコードは動かなくなる恐れがあります。

後方互換性 ​

  • 公開API(migration DSL)の表面上の挙動は変えない形でのリファクタリングを意図
  • ただし内部構造が大幅に変わっているため、
    • adapter の monkey patch
    • schema 関連の内部APIに依存している gem / プラグイン
      などには影響が出る可能性があります。

  1. 参考情報 (あれば)
  • 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) として建模するリファクタリング例としても参考になります。

#58341 Fix deprecation proxy doc examples that raise ArgumentError ​

マージ日: 2026/8/2 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveSupport::Deprecation のプロキシクラス (DeprecatedInstanceVariableProxy, DeprecatedConstantProxy) のドキュメント中のサンプルコードが、現在のシグネチャと合わずに ArgumentError を出していた問題を修正し、実際に動く形に更新したPRです。機能変更はなく、ドキュメント記載の使い方と実装を同期させたものです。

  1. 変更内容の詳細

対象ファイル:

  • activesupport/lib/active_support/deprecation/proxy_wrappers.rb (+3 / -3)

主な変更点:

2-1. DeprecatedInstanceVariableProxy の例の修正 ​

以前の(壊れている)例:

ruby
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: がキーワード引数必須になっていました:

ruby
DeprecatedInstanceVariableProxy.new(object, method, var_name, deprecator: ...)

このPRでは、ドキュメント内のサンプルを新しい呼び方に合わせて修正しています。概略は次のような形です(※実際のコードと同趣旨):

ruby
ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(
  self,
  :request,
  :@request,
  deprecator: ActiveSupport::Deprecation.new
)

さらに、説明文中で「deprecator が “最後の引数”」と、あたかも位置引数であるかのように書かれていた箇所も、キーワード引数を使う今の仕様に沿った文言へ修正されています。

2-2. DeprecatedConstantProxy の例の修正 ​

以前の(壊れている)例:

ruby
ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006')
# => ArgumentError: wrong number of arguments (given 2, expected 3)

DeprecatedConstantProxy も、fc2dc7c8d3 によってコンストラクタのシグネチャが変更され、deprecator が必須引数(位置引数)になっていました。現在のシグネチャは概ね:

ruby
DeprecatedConstantProxy.new(old_const, new_const, deprecator)

であるため、ドキュメントのサンプルは次のように修正されています(趣旨):

ruby
ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
  'PLANETS',
  'PLANETS_POST_2006',
  ActiveSupport::Deprecation.new
)

2-3. DeprecatedObjectProxy には変更なし ​

DeprecatedObjectProxy はもともと deprecator が位置引数のままで仕様変更はなく、既存のドキュメントも正しかったため、このPRでは触れられていません。

2-4. 動作確認(PR説明中の出力例) ​

修正後、ドキュメントに掲載されているサンプルコードをそのまま実行すると、説明されている通りの動作になります:

ruby
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

  1. 影響範囲・注意点
  • 実コードの挙動には変更がなく、影響範囲はドキュメント(サンプルコードと説明文)のみです。
  • ただし、以下のようなコードを書いている場合は注意が必要です:
    • 古いドキュメントを参照して
      DeprecatedInstanceVariableProxy.new(..., ..., ..., ActiveSupport::Deprecation.new) のように「deprecator を位置引数の最後」に渡している
    • DeprecatedConstantProxy.new(old, new) のように deprecator を渡していない
  • これらは現在の Rails ではすでに ArgumentError になっており、このPRはあくまで「ドキュメントを現実に合わせた」ものです。
    もし自分のコードで同様のエラーが出ている場合は、以下のように修正する必要があります:
    • DeprecatedInstanceVariableProxy
      ruby
      DeprecatedInstanceVariableProxy.new(obj, :method, :@ivar, deprecator: ActiveSupport::Deprecation.new)
    • DeprecatedConstantProxy
      ruby
      DeprecatedConstantProxy.new('OLD', 'NEW', ActiveSupport::Deprecation.new)

  1. 参考情報 (あれば)
  • 概要に出てくる変更コミット: fc2dc7c8d3
    • このコミットで DeprecatedInstanceVariableProxy と DeprecatedConstantProxy のコンストラクタ API が変更され、deprecator が必須となった。
  • 関連クラス:
    • ActiveSupport::Deprecation::DeprecatedObjectProxy
    • ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy
    • ActiveSupport::Deprecation::DeprecatedConstantProxy

#58336 Fix search_field raising NameError when passed autosave: true ​

マージ日: 2026/8/1 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    search_field ヘルパーに autosave: true を渡すと NameError が発生していた不具合を修正し、ドキュメントに記載されている「リクエストホストを反転した autosave 値」が実際に動作するようにしました。あわせて autosave オプション周りの挙動を網羅するテストが追加されています。

  1. 変更内容の詳細

バグの内容 ​

search_field はドキュメント上、autosave: true を渡すと以下のようにリクエストホストを反転した文字列を autosave 属性として自動付与することになっていました:

ruby
# 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 内で次のように書かれていました:

ruby
options["autosave"] = request.host.split(".").reverse.join(".")

しかし Tags::Base には request メソッドは定義されておらず、ビューコンテキストへのメソッド委譲もされていないため、search_field 呼び出し時には必ず次のエラーになっていました:

text
NameError: undefined local variable or method 'request' for
  an instance of ActionView::Helpers::Tags::SearchField

つまり、ドキュメントされていた autosave: true の機能は、これまで一度も正しく動いたことがありませんでした。

修正内容 ​

リクエストオブジェクトを、タグが内部的に保持しているテンプレートオブジェクト(ビューコンテキスト)から取得するように変更しました。

ruby
# 修正前(概略)
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 等に追加されています。挙動は以下のように整理されています:

  1. autosave: true

    • autosave 属性は 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"
  2. autosave: "my-key"(文字列指定)

    • 指定した文字列がそのまま autosave 属性に利用される
    • results 属性はデフォルト "10" が付与される
      例:
    ruby
    search_field(:user, :name, autosave: "my-key")
    # => autosave="my-key" results="10"
  3. results: を明示指定した場合

    • autosave によるデフォルト "10" は上書きされず、指定した results 値が優先される
      例:
    ruby
    search_field(:user, :name, autosave: true, results: 5)
    # => autosave="com.example.www" results="5"
  4. autosave: false

    • autosave="false" という属性がそのまま出力される(属性自体を付けないわけではない)
    • results 属性は付与されない
      これは既存ドキュメントに記載されていた出力と一致するようにされています。

テストスイート (actionview の関連テスト) はすべて成功し、RuboCop もパスしています。


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

    • 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 値がホストによって変わる点を踏まえ、キャッシュキーなどに組み込む場合は挙動を把握しておくとよいです。

  1. 参考情報 (あれば)
  • 変更ファイル:

    • actionview/CHANGELOG.md
    • actionview/lib/action_view/helpers/tags/search_field.rb
    • actionview/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. 概要 (1-2文で)
    Rails公式ガイド「Install Ruby on Rails」で、Rubyのインストール手順が「Miseによる事前コンパイル済みRubyの利用」を前提とした内容に更新されました。これにより、依存関係のインストールやソースからのコンパイル手順が不要となり、ガイド全体がシンプルかつ高速なセットアップ手順に整理されています。

  1. 変更内容の詳細
  • Miseの利用前提への更新

    • ガイド中の「Rubyのインストール方法」が、Miseが提供するprecompiled Rubyを利用する形に書き換えられました。
    • 従来は、OSごとのパッケージインストールや、ビルドに必要な依存パッケージの説明、ruby-build/rbenv 等と組み合わせたソースコンパイル手順が含まれていましたが、そうした説明が大幅に削除されています(-25行)。
    • 代わりに、Miseをインストールした上で、Mise経由でRubyをインストールする最小限の手順だけが残されている形です(+6行)。
  • 対象バージョンの更新

    • ガイド内で言及される Ruby・Rails・Ubuntu のバージョンが最新に更新 されています。
      • 具体的なバージョン番号はPR本文では明示されていませんが、「最新の安定版Ruby/ Rails」「最新LTSもしくは推奨版Ubuntu」の組み合わせに合わせた内容に変更されています。
  • ドキュメント専用の変更

    • 変更ファイルは guides/source/install_ruby_on_rails.md のみで、アプリケーションコードやテストコードへの変更はありません。
    • 追加行6、削除行25と、ほぼ「不要になった作業手順」を落として簡潔にした構成です。

※実際のサンプルコマンド(イメージ)
ガイド内の実際の文面はPRからは全部は読めませんが、内容的には以下のような形に近くなっていると考えられます:

bash
# Mise のインストール(例)
curl https://mise.run | sh

# 最新の Ruby をインストール
mise use --global ruby@<latest>

# インストール後の確認
ruby -v

従来入っていたであろう「Ubuntuでbuild-essentialやlibssl-devなどを入れる」「ソースをコンパイルする」などの説明が削られ、Miseのインストールとmise use程度に集約されています。


  1. 影響範囲・注意点
  • 新規ユーザへの影響(セットアップ体験)

    • 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が安定して使えるかを確認する必要があります。
  • テスト・挙動上の変更はなし

    • このPRはガイド文書のみの変更であり、Rails本体の挙動やAPIには一切変更がありません。
    • バージョンアップしたRuby/Rails/Ubuntuの組み合わせによる挙動の差分は、今回のPRというより、各プロジェクト(Ruby/ Rails/ OS)の変更の影響として別途考える必要があります。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    ActiveRecord::ConnectionAdapters の insert, update, delete に対して、従来の「第2引数以降で binds を渡す」呼び出し方が非推奨になりました。代わりに、SQL文字列とバインド値をまとめた Arel.sql(..., *binds) を渡す書き方に移行するための正式なデプリケーションです。

  1. 変更内容の詳細

非推奨になる呼び出し方 ​

これまで多くのアダプタ実装やアプリケーションコードでは、以下のように sql と binds を別々に渡す呼び出し方が可能でした:

ruby
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 から自動的にバインドを抽出するスタイルが推奨されます。

ruby
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.rb
    • insert, update, delete が、binds を位置引数で受け取った場合にデプリケーション警告を出すように変更。
    • 内部的には、Arel.sql 由来の Arel::Nodes::BoundSqlLiteral が渡された場合に、そのASTからバインド値を抽出して処理する形が明確化。
  • CHANGELOG.md
    • 上記仕様変更についての記載が追加。
  • adapter_test.rb, database_statements_test.rb
    • 旧来の呼び出しパターンが「動くが警告が出る」こと、および新パターンが期待どおり動作することを確認するテストが追加/更新。

  1. 影響範囲・注意点

主に影響を受けるのはどんなコードか ​

  • アダプタやライブラリで、以下のように 生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 を前提としたシグネチャに整理する。

注意すべき技術的ポイント ​

  • Arel.sql が SQL文字列に対してバインドを埋め込むのではなく、SQLとバインドを一緒にASTとして持つ 点に注意。
    • 手動で ? を quote で埋め込むのではなく、あくまで ? プレースホルダとバインド値を Arel.sql に渡す。
  • 既存の Model.where("name = ?", "foo") と同じスタイル・同じ仕組みでクエリを扱えるため、
    SQL構築まわりのコードを Arel ベースに寄せていく上で一貫性がとれる。

  1. 参考情報 (あれば)
  • 本PRで参照されている過去のPR: #29944
    • bind パラメータを Arel AST に移した際、既存の「生SQL + binds」利用者のために binds positionals を一時的に復活させた経緯がある。
  • 関連クラス・メソッド:
    • Arel.sql(string, *binds) → Arel::Nodes::BoundSqlLiteral
    • ActiveRecord::ConnectionAdapters::DatabaseStatements#to_sql_and_binds
    • Model.where("... = ?", value)
      これと同様のバインド管理の仕組みを、insert/update/delete でも統一していくための変更。

#58324 Fix String#parameterize raising TypeError when separator is nil ​

マージ日: 2026/8/1 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    String#parameterize に separator: nil を渡すと TypeError で落ちていた不具合を修正し、nil を空文字と同等に扱うようにした PR です。これにより、separator を設定ファイルやパラメータからそのまま渡しても安全に動くようになります。

  1. 変更内容の詳細

何が問題だったか ​

ActiveSupport::Inflector#parameterize(String#parameterize の実体)は、内部で次のような処理をしていました。

ruby
parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator)

unless separator.nil? || separator.empty?
  # 複数連続したセパレータを1つにまとめる
  # 先頭/末尾のセパレータを削除する
end

意図としては:

  • separator が nil または ""(空文字)の場合:
    • セパレータを挿入しない(そのままくっつける)
  • それ以外("-", "_" など)の場合:
    • 余分なセパレータをまとめたりトリムしたりする

という設計でした。

しかし Ruby の gsub! に replacement = nil を渡すと TypeError になるため、

ruby
"Donald E. Knuth".parameterize(separator: nil)
# => TypeError: no implicit conversion of nil into String

と、separator.nil? を見る前に落ちてしまっていました。
つまり「separator が nil の場合を考慮したガード」は存在するのに、実際にはそこまで到達できないという矛盾した状態でした。


どう直したか ​

separator: nil を separator: "" と同等に扱うように変更し、ガードロジックを整理しています。

変更のポイント:

  1. nil を空文字として扱う
    gsub! に渡す前に、separator が nil の場合は "" に正規化するようにしています(実装的には nil のまま gsub! に行かない形に整理)。

    結果として:

    ruby
    "Donald E. Knuth".parameterize(separator: nil) # => "donaldeknuth"
    "Donald E. Knuth".parameterize(separator: "")  # => "donaldeknuth"

    のように、両者は同じ動作になります。

  2. separator.nil? チェックを削除
    nil と空文字が同じ経路を通るようにしたため、

    ruby
    unless separator.nil? || separator.empty?

    という条件は冗長になり、nil? チェックが削除されています。
    実質的には「空文字相当ならセパレータ処理をスキップする」という、もともとの意図をそのままシンプルにした形です。

  3. テスト追加
    既存の「セパレータなし("")」ケースと同じテストパターンを、separator: nil に対しても追加しています。

    • StringToParameterizeWithNoSeparator
    • StringToParameterizePreserveCaseWithNoSeparator

    の2つのフィクスチャを再利用し、preserve_case: true / デフォルト両方をカバー。

    テスト結果(activesupport の該当テスト一式):

    • 645 runs
    • 3207 assertions
    • 0 failures / 0 errors
    • RuboCop も問題なし

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

    • 影響を受けるのは String#parameterize に separator: nil を渡すケースのみ です。
    • これまで TypeError が発生していたケースが、 "TypeErrorで落ちる" → "文字列として正しくパラメータ化される" に変わります。
    • separator: "" を使っていた既存コードの挙動は変わりません(nil がその挙動に揃えられただけ)。
  • 後方互換性

    • 実際に separator: nil を意図して使っていた場合:
      • これまでは例外が発生 → 今後は "donaldeknuth" のようにパラメータ化されるようになるので、「エラーで気づく」パターンを前提にしていた場合は挙動が変わる点に注意が必要です。
      • ただし「nil をサポートしない」方向(nil ガードを削除して常に TypeError にする)ではなく、「自然に動く」方向に寄せたため、一般的なアプリケーションにとっては改善と見なせる変更です。
    • 設定ファイルや params から separator をそのまま渡しているコードにとっては、nil の場合も安全に動作するようになるため、想定外エラーのリスクは減ります。
  • 注意点

    • バリデーションや例外発生有無で挙動をテストしていた場合(例: 「不正な separator なら TypeError」などの仕様を勝手に決めていた場合)、テストが通らなくなる可能性があります。
    • 「nil を指定したらエラーになるべき」と考えていた場合、この PR はそれと逆方向の挙動になります。もしそのポリシーを保ちたいプロジェクトでは、アプリケーション側で separator を検証してください。

  1. 参考情報 (あれば)
  • 対象メソッド:
    • 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. 概要 (1–2文で)
    NameError#missing_name の全ての分岐をカバーするテストが追加され、これまでテストされていなかったケース(receiver == Object、receiver 呼び出し時の ArgumentError、メッセージからの名前抽出)が網羅されました。機能変更はなく、既存挙動の仕様化・回帰防止が目的です。

  1. 変更内容の詳細

NameError#missing_name の分岐と今回のテスト ​

NameError#missing_name は、例外がどのレシーバー(self.receiver)に対して起きたかによって名前の取り出し方を変えています:

ruby
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 になるため、この分岐が使われます。

例(イメージ):

ruby
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 を正規表現でパースし、末尾の「定数っぽいもの」を抽出する

というフォールバック経路がテストされています。

例(イメージ):

ruby
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 は nil
  • missing_name? は false

となることをテストで保証しています。

例(イメージ):

ruby
e = NameError.new("undefined local variable or method `foo' for main:Object")
e.missing_name   # => nil
e.missing_name?  # => false

これにより、「すべての NameError が定数名を持つとは限らない」という前提をメソッド側が尊重していることが確認されています。


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

    • 変更は 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 を返し得ることを前提にする必要があります(今回のテストでこれが仕様として強化された形)。

  1. 参考情報 (あれば)
  • 該当コード: 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. 概要 (1-2文で)
    week_field ヘルパーのデフォルト値に使われる strftime フォーマットについて、実装とドキュメントの不一致を解消し、ドキュメント側を実装 ("%G-W%V") に合わせる修正です。コードの挙動は既に正しいため、このPRはドキュメントのみの更新です。

  1. 変更内容の詳細

何が問題だったか ​

  • 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にあるサンプル:

ruby
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ではドキュメントのみの同期を行っています。

  1. 影響範囲・注意点
  • 実行時の挙動は変わりません
    すでに week_field は %G-W%V で値を生成しており、このPRはドキュメントの表記を追従させるだけです。

  • 既存アプリへの直接の破壊的変更はなし
    ただし、以下の開発者は注意が必要です:

    • Rails のドキュメントを見て、week_field のデフォルトフォーマットが %Y-W%W だと思い込んでいた人
    • それに合わせて自前実装やテストデータを %Y-W%W ベースで書いていた人

    実際のヘルパーはすでに %G-W%V を出しているので、「ドキュメントを読んで真似していたコード」がある場合は、そこを %G-W%V に揃える必要があります。

  • ISO週と暦年の違いを意識する必要があるケース
    年末年始近辺の日付を <input type="week"> とやり取りする処理では、ISO週番号年を前提にロジックを書く必要があります。
    例えば「2015-W53」と返ってきた値は、カレンダー上では 2015年末〜2016年始を指す可能性があり、「yyyy をそのまま暦年とみなす」という扱いは誤りになることがあります。


  1. 参考情報 (あれば)
  • 関連PR:

    • #57890: WeekField#format_datetime の実装を %G-W%V に変更したPR
    • #54355: 実装+ドキュメントの両方を修正しようとしていた元PR(実装・テスト部分は #57890 で取り込まれ、本PRでドキュメント部分が反映)
  • 用語整理:

    • 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. 概要 (1-2文で)
    Rails 7.2 開発中に入った変更 (#58298) により、「無名クラスを祖先にもつモデル」で model_name を呼ぶと例外が出るリグレッションが発生していたのを、i18n キー生成時に無名祖先をスキップすることで修正した PR です。通常のモデルの動作や i18n 探索順序は変えずに、無名祖先を介したクラス継承でも model_name を安全に使えるようにしています。

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

問題の背景 ​

元の変更 (#58298) で以下が行われていました:

  • ActiveModel::Name を Ractor 共有可能にするために Name#freeze が導入された
  • freeze 時に i18n 用のキー (i18n_keys) を事前計算 (eager) するようになった
    • lookup_ancestors で祖先チェーンをたどり、それぞれの klass.model_name から i18n キーを構築する

ここで、以下のように匿名クラスを中間クラスとして使うと:

ruby
base   = Class.new(ActiveRecord::Base)   # 匿名の中間クラス
Widget = Class.new(base)                 # 名前付きモデル
Widget.model_name

base は name が空のクラスなので、Name.new の内部で

  • 「クラス名が空なので name を明示的に渡せ」という ArgumentError が発生
  • その結果、Widget.model_name 自体が落ちる

以前は:

  • i18n キーは遅延計算だった
  • model_name 自体は成功し、human を呼んだときに初めて祖先をたどっていた

ため、匿名祖先がいても model_name までは動いていました。

修正内容 ​

i18n キー生成時に「無名クラスの祖先をスキップする」ように変更しています。

擬似コードで表すと、おおよそ次のような変化です:

ruby
# 変更後イメージ
@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 計算経路を確実に通せる
  • 修正前は ArgumentError が発生してテストが red
  • 修正後は例外なく i18n キーが生成され green

  1. 影響範囲・注意点

影響範囲 ​

  • 影響を受けるケース

    • ActiveRecord/ActiveModel で、匿名クラスを中間クラスとして継承しているモデルで
      • model_name を経由するあらゆる機能:
        • form_for / form_with
        • url_for, polymorphic_url などルーティングヘルパ
        • to_partial_path / render @record のパーシャル解決
        • シリアライゼーション (as_json 等)
    • これらが PR #58298 以降のコードベースで ArgumentError によって壊れていたが、本 PR により復旧
  • 影響を受けないケース

    • 通常の「すべての祖先クラスに名前がついている」モデル
    • 匿名クラス自身に対して model_name を呼ぶ場合
      • 今回の PR でも引き続き ArgumentError を投げる(従来通り)

i18n の動作影響 ​

  • 匿名祖先には model_name で利用されるような翻訳キーは本来ないため、無視しても現実的なケースでは問題なし
  • 名前付きの祖先クラスの解決順序は従来通り維持される
    • 例: Admin::User < User < ApplicationRecord
    • User や ApplicationRecord 由来の i18n キーの探索順序は変わらない

バージョン・互換性 ​

  • このリグレッションは #58298 以降の未リリースコードでのみ発生
  • 公式にリリースされた Rails バージョンはそもそも影響を受けていない
  • そのため CHANGELOG の更新はなし

  1. 参考情報 (あれば)
  • 関連 PR
    • #58298: ActiveModel::Name を Ractor 共有可能にするための Name#freeze 導入と i18n キーの eager 計算
  • 実務上の示唆
    • engine やプラグイン、テスト等で、Class.new(ActiveRecord::Base) のような匿名ベースクラスを挟んでモデルを定義していても、この修正以降は form_for / url_for / render などフレームワーク全体で安全に扱えるようになります
    • 匿名クラスに対して model_name が必要なケースは引き続きサポート外であり、必要ならクラスに名前をつけるべき、という従来方針も維持されています

#58307 Avoid redundant worker pool dispatch for default Action Cable streams ​

マージ日: 2026/8/1 | 作成者: @npezza93

  1. 概要 (1-2文で)
    Action Cable の「デフォルトストリーム」におけるメッセージ配送で、同じジョブを executor と connection worker pool の2段階に重ねてディスパッチしていた無駄を取り除き、パフォーマンスを改善する変更です。ユーザー定義コールバックは従来通り worker pool で隔離しつつ、デフォルトハンドラだけ executor 上で完結させることで、レイテンシとスループットが大きく改善されています。

  1. 変更内容の詳細

背景となるボトルネック ​

Action Cable でメッセージが publish されて subscriber に届くまでのフローには、少なくとも以下のスレッド/キューが関わります:

  1. アダプタ (Redis / Solid Cable など)

    • サブスクライバーごとの callback を Action Cable の executor 上に enqueue して実行していた。
  2. ストリームハンドラ (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.rb
  • actioncable/test/channel/stream_test.rb

要点:

  1. 「デフォルトストリーム」と「ユーザー定義コールバック」を分岐して扱うようにした

    • デフォルトストリーム: executor 上で完結させる(worker pool への再ディスパッチをしない)。
    • ユーザー定義の subscription コールバック: 従来通り connection の worker pool に enqueue する。
  2. ストリームハンドラの実装変更

    • 以前は「すべてのストリームハンドラが 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 に逃がす」という分岐が入っています。

  3. テスト (stream_test.rb) の追加

    • 新たに追加されたテストでは、
      • デフォルトストリームが worker pool に重ねてディスパッチされないこと
      • ユーザー定義コールバックは引き続き worker pool を使うこと
        を確認しています。
    • これにより、回帰を防ぎつつ、本 PR の前提となる実行コンテキストの違いが担保されます。

ベンチマーク結果 ​

作者が提示しているベンチマーク結果の要約:

750 VUs / 7 msgs/sec ​

  • main (変更前):

    MetricRedisSolid Cable SQLite
    平均106ms289ms
    p95156ms437ms
  • 本 PR 適用後:

    MetricRedisSolid Cable SQLite
    平均90ms127ms
    p95123ms180ms

Redis / Solid Cable ともに平均・p95 ともに大きく改善しています。Solid Cable では特に顕著です。

750 VUs / 10 msgs/sec ​

  • main (変更前):

    MetricRedisSolid Cable SQLite
    平均failsfails
    p95failsfails
  • 本 PR 適用後:

    MetricRedisSolid Cable SQLite
    平均84ms160ms
    p95116ms269ms

変更によって、7 msgs/sec が限界だったシナリオで 10 msgs/sec まで耐えられるようになり、約 35% のスループット向上が確認されています。


  1. 影響範囲・注意点

影響範囲 ​

  • 影響を受けるのは Action Cable のチャンネルストリーム (stream_from 等) を使ったメッセージ配送経路です。
  • 特に、以下のような「普通のブロードキャストのみ」のケースで効果が大きいです:
    • 多数のクライアントが同一チャンネルに subscribe
    • サーバ側はほぼ ActionCable.server.broadcast だけをしている
    • 各クライアント側の処理は標準の stream ハンドラ(メッセージをそのまま JavaScript に届ける程度)しか行わない

互換性・挙動面 ​

  • ユーザー定義のコールバックは引き続き worker pool 上で実行されるため、アプリケーションコードの実行コンテキストは従来と変わりません。
    • したがって、「どのスレッド/プールでアプリケーションコードが走るか」という意味での互換性は維持されています。
  • 変更されるのは 内部のデフォルトストリームの実行場所(executor と worker pool のどちらで完結するか)だけです。
  • これにより以下のような性質が維持されます:
    • 任意のアプリケーションコードは worker pool 上で隔離される
      → 長時間ブロックする処理が executor を詰まらせにくい
    • デフォルトの decode + 送信だけであれば executor 上で高速に処理される
      → 高負荷時のレイテンシ・スループットが改善

注意点・留意事項 ​

  • executor 上で走る処理がやや増えることになるので、もし独自拡張で「デフォルトストリームに見えるが、実際には重い処理をしている」ようなコードがある場合は注意が必要です。
    • ただし、標準の設計としては「重い処理は明示的なユーザーコールバック内で行う」想定なので、一般的なアプリケーションでは問題になりにくいはずです。
  • CHANGELOG は(現時点で)更新されていないため、リリースノート等でこの改善点を見落とさないようにする必要があります。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Rails 7.2 付近で一度削除された「スキーマダンプから古いマイグレーションの version をフィルタする仕組み」を取り消し(revert)し、従来どおりフィルタを有効に戻した PR です。
    チーム開発時に「古いマイグレーションをリポジトリから削除する」運用を、実務的に破綻なく行う手段がまだ整理できていないため、仕様変更を保留にする目的のリバートです。

  1. 変更内容の詳細

2-1. 背景・問題意識 ​

Revert 対象の #58322 では、「db/schema.rb 生成時に、schema_migrations テーブルの全バージョン情報から、既にファイルが存在しない古いマイグレーション分を ‘フィルタして捨てる’ 挙動」を削除していました。
今回の PR はそれを元に戻します。

作者の懸念点は主に次です:

  • 古いマイグレーションを削除したい典型的なユースケース(例: 数年分の履歴を整理してレポジトリを軽くする)が、フィルタがないと実務的にかなり難しい。
  • 手順としては「古いマイグレーションファイル削除 → db/schema.rb の末尾(trailer)からバージョンも削除 → それをコミット・デプロイ」が想定されるが、
    • チームの他のメンバーのローカルDBには、依然として古い schema_migrations レコードが残っている。
    • その状態で bin/rails db:migrate をすると、また古いバージョン情報が db/schema.rb の末尾に書き戻され、「削除したはずの履歴」が復活してしまう。
  • この問題を回避しようとして「古いバージョンを schema_migrations から削除するデータマイグレーション」を用意すると、
    • そのデータマイグレーション自体も “マイグレーション” であり、結局また履歴に残る。
    • 誰かがそのマイグレーションを適用し忘れる(長期休暇等)と、その人だけ schema_migrations がクリーンにならない。
  • 「削除済みマイグレーションのバージョン情報をどこかに別途保持しておいて、スキーマダンプ時に参照する」ようなメタ的仕組みも考えられるが、それは「履歴を repo から消したい」というモチベーションと相反する。

結局、「フィルタがある状態」の方が、古いマイグレーション整理のユースケースにとって現実的な落としどころだろう、という判断でリバートされています。

2-2. コードレベルの変更 ​

変更ファイルは以下の3つです:

  • activerecord/lib/active_record/schema_dumper.rb
  • activerecord/test/cases/schema_dumper_test.rb
  • guides/source/active_record_migrations.md

schema_dumper.rb ​

ここに「マイグレーションのフィルタ」を復活させるロジックが戻っています。

おおざっぱには、ActiveRecord::SchemaDumper が db/schema.rb を生成する際に行う:

  • schema_migrations テーブル(および multi-DB 対応の場合は ar_internal_metadata など)からバージョンを取得
  • そのうち「現在レポジトリに存在するマイグレーションファイルに対応しない古いバージョン」を除外
  • フィルタ済みのバージョンのみを db/schema.rb の末尾(いわゆる trailer 部分)として出力

という挙動が再度有効になっています。

擬似コードで書くと、ざっくり次のようなイメージが復活した形です(正確なコードではありませんが挙動のイメージとして):

ruby
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
end

PR #58322 では、この filtered を使う処理を削って「フィルタせずにすべての version をダンプする」方向へ変えていましたが、本 PR で再び filtered を使う形に戻っています。

テスト (schema_dumper_test.rb) ​

  • フィルタの存在を前提としていた既存テストの期待値が、#58322 で変更されていたのを、今回また元に戻しています。
    • つまり「マイグレーションファイルが削除された version は、db/schema.rb には出てこない」ことを確認するテストが再び有効に。

ガイド (active_record_migrations.md) ​

  • 「schema.rb にマイグレーションバージョンがどう出力されるか」「古いマイグレーションを削除した時にどう振る舞うか」に関する説明が、#58322 時点の内容から、従来どおり「フィルタが存在する」前提の文章に戻されています。
  • ガイド上も、「古いマイグレーションを削除しても、schema.rb の trailer には残さない(/フィルタされる)」挙動を前提とした説明になります。

  1. 影響範囲・注意点

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 チームとしては現時点で次のような運用を想定していると読めます:

  1. 古いマイグレーション群を削除する(ファイルを物理削除)
  2. チームメンバーが今後 db:migrate / db:schema:dump するたび、
    • schema.rb の trailer からそれら古いバージョンが自動で消えた形になる
    • ローカルDB には古い version が残っていても、schema.rb には反映されない
  3. これにより、
    • レポジトリ上の schema.rb は「今もファイルが存在するマイグレーションだけ」を表す
    • schema_migrations テーブルがメンバーごとに必ずしも一致しなくても、運用上はさほど問題にならない

一方で、「schema_migrations テーブルを完全にクリーンにしたい」「バージョンを正規化したい」といった要求には、まだ標準で綺麗に答えられていない状態であり、その点を解くまでは仕様を動かさない、という判断でもあります。


  1. 参考情報 (あれば)

#58274 Allow prepared statements with query log tags ​

マージ日: 2026/7/31 | 作成者: @bschrag620

  1. 概要 (1-2文で)
    Railsでクエリログタグ(query_log_tags)を有効にした際でも、アプリ側の選択で prepared statements を無効化せずに使い続けられるオプションが追加されました。これにより、高カーディナリティなコメントを避けるという従来の安全デフォルトを維持しつつ、必要なアプリケーションは両方を併用できるようになります。

  1. 変更内容の詳細

背景 / 現状 ​

  • これまでのRails(Active Record)の挙動:

    • query_log_tags_enabled を有効にすると、Railtie側で disable_prepared_statements が自動的に true に設定される。
    • 理由: クエリコメント(ログタグ)が毎回異なる値になると(高カーディナリティ)、prepareされたステートメントのキャッシュが効かなくなり、DB側でのプランキャッシュ・効率に悪影響が出るケースがあるため。
  • 課題:

    • とはいえ、プロジェクトによっては
      • クエリログタグを使いたい
      • かつ prepared statements も有効のまま使いたい というニーズがあるが、現状は両立できなかった。

今回の変更ポイント ​

  1. query_log_tags_enabled に3つ目の値 :with_prepared_statements を追加
  • これまでの典型的な設定(config/database.yml など):
yaml
production:
  adapter: postgresql
  # ...
  query_log_tags_enabled: true   # or false
  • 新しいオプション:
yaml
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 は 無効化されない(アダプタ/他の設定のデフォルトのまま)
  1. 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 を組み立てる部分で、この新しいシンボル値を特別扱いする形です。)

  1. ドキュメント更新
  • guides/source/configuring.md が更新され、query_log_tags_enabled の説明に :with_prepared_statements という第三の選択肢が追加されています。
    • これにより、「タグを有効にしつつ prepared statements を維持したい場合の設定方法」が公式ガイド上で明示されました。
  1. テスト追加
  • railties/test/application/query_logs_test.rb に25行のテストが追加。
    • 想定される内容:
      • query_log_tags_enabled: :with_prepared_statements を設定したアプリケーション構成で
        • クエリログタグが有効になっていること
        • disable_prepared_statements が強制的に true にされていないこと を検証するテストケース。
    • これにより将来のリグレッション(この新挙動が壊れること)を防止。
  1. CHANGELOG 追加
  • activerecord/CHANGELOG.md にエントリが追加され、新しいオプションの存在と挙動がリリースノートに反映されています。

  1. 影響範囲・注意点

既存アプリへの影響 ​

  • query_log_tags_enabled を true / false のまま使っている既存アプリには挙動変更はありません。

    • true → 従来どおり、prepared statements は自動的に無効化される。
    • false → 引き続きログタグは使われず、prepared statements も以前通り。
  • 新しい値 :with_prepared_statements を明示的に設定したアプリだけが、新しい挙動の恩恵を受けます。

このオプションを使うときの注意点 ​

  1. 高カーディナリティなタグに注意
  • prepared statements を有効のままクエリログタグを付けると、

    • クエリ毎に異なるコメント(例: リクエストID・ユーザーIDなど)が付与される場合、DB側から見ると
      • SQLテキストが毎回違う → PREPARE キャッシュの恩恵が薄くなる
      • プランキャッシュが大量に増える
    • といった問題が起こり得ます。
  • そのため:

    • 付与するタグの粒度と種類を意識することが重要です。
      • ○ より安全: コントローラ名 / アクション名 / 環境名 など低カーディナリティな情報
      • △ 注意: ユーザーID / リクエストID / ジョブID / トレースID など高カーディナリティ情報
    • 高カーディナリティな値はログ(アプリログ側)で別途持つか、タグの種類を厳選するのが望ましいです。
  1. DBアダプタごとの実装依存
  • :with_prepared_statements によって Railtie が disable_prepared_statements を「設定しない」だけなので、
    • 実際に prepared statements が有効かどうかは
      • アダプタのデフォルト設定
      • その他の config.active_record.disable_… 系の設定 に依存します。
  • オラクル・MySQL・PostgreSQLなど、利用アダプタごとに prepared statements の有効化 / 無効化の方法・性能特性が異なる点に注意してください。
  1. 移行のステップ案
  • もし「ログタグ + prepared statements」を有効にしたい場合:
    1. まずステージング環境で:
      • query_log_tags_enabled: :with_prepared_statements を設定
      • 実際のSQLコメント(クエリログ)とDB側のプランキャッシュ状況・負荷を観測
    2. 高カーディナリティなタグを削る / 替える
    3. 問題なければ本番へ反映

  1. 参考情報 (あれば)
  • 対象PR:
  • 関連ドキュメント(更新あり):
    • Configuring Rails Applications ガイド (guides/source/configuring.md) の query_log_tags_enabled の項
  • 概念的な背景:
    • Railsのクエリログタグ機能そのもの: ActiveRecord::QueryLogs
    • prepared statements とコメント付きSQLがプランキャッシュに与える影響については、各DB(PostgreSQL・MySQLなど)のマニュアルやブログ記事が参考になります。

#58267 activerecord: make sure tests do not rely on implicit SELECT order ​

マージ日: 2026/7/31 | 作成者: @viralpraxis

  1. 概要 (1–2文で)
    このPRは、Active Record のテストが「暗黙の SELECT 結果順序(MVCC に起因する更新順など)」に依存していた部分を洗い出し、すべて明示的な ORDER BY を付けるように修正するものです。これにより、DB 実装や実行タイミングによってテスト結果が変わる flakiness を防ぎ、テストの決定性・再現性を高めています。

  1. 変更内容の詳細

全体方針 ​

  • 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.rb
  • activerecord/test/cases/associations/has_many_associations_test.rb
  • activerecord/test/cases/associations/has_many_through_associations_test.rb
  • activerecord/test/cases/associations/join_model_test.rb
  • activerecord/test/cases/autosave_association_test.rb

これらは eager loading (includes, preload, eager_load) や has_many, has_many :through, join model、autosave など、関連を伴う読み出しをテストするファイルです。多くのテストは以下のような構造をとります:

ruby
posts = author.posts # または Author.includes(:posts).find(...)
assert_equal [post1, post2], posts

この posts の順序が、DB 依存で変わらないことを暗黙に期待していた箇所に、明示的な order が追加されたと考えられます:

ruby
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 してアサートするテストがよくあります:

ruby
result = Book.insert_all([...])
books = Book.where(id: result.rows.flatten)
assert_equal [1, 2, 3], books.map(&:id)

こうした箇所に

ruby
books = Book.where(id: result.rows.flatten).order(:id)

のような order が追加されていると考えられます。
特に RETURNING 句が関与する PostgreSQL では、返却順と実際のテーブルスキャン順が一致しない場合もあり、ここが暗黙の依存ポイントになりがちです。

3. relation / query 関連テスト ​

  • activerecord/test/cases/relation/load_async_test.rb
  • activerecord/test/cases/relation/or_test.rb
  • activerecord/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 の基本動作のハブのようなファイルであり、さまざまなクエリパターンについて:

ruby
assert_equal [developers(:david), developers(:jamis)], Developer.where(...).to_a

といった順序依存の検証が多いので、ここに 15 箇所程度 order の追加・修正が入った可能性が高いです。

4. scoping 関連 ​

  • activerecord/test/cases/scoping/default_scoping_test.rb
  • activerecord/test/cases/scoping/named_scoping_test.rb

default_scope や named scope が順序を付与・合成した結果のテストで、「scope が暗黙的にある順序を前提にしている」ように見えるものを是正したとみられます。

例として:

ruby
default_scope { where(active: true) } # だけを期待していたテスト

で暗黙に挿入順を前提にしているケースがあれば、テスト側で

ruby
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.rb
  • activerecord/test/cases/unsafe_raw_sql_test.rb

message_pack_test は Active Record オブジェクトの MessagePack シリアライズ/デシリアライズやバルクロードをテストしているため、結果の配列順がそのままバイナリの順序にも影響します。ここで暗黙順序に依存していた部分に order が入ったと考えられます。

unsafe_raw_sql_test では、生 SQL を使ったクエリや危険な SQL 呼び出しの検査を行っています。ここでも:

ruby
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 コアのテストでは、この種のヘルパーを通して配列比較を行うことで、将来の暗黙順序依存を減らす方針がとられることが多いです。


  1. 影響範囲・注意点
  • 対象は「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)等を使う。

  1. 参考情報 (あれば)
  • 対象 PR:
  • pg_disorder 拡張:
    • https://github.com/viralpraxis/pg_disorder
    • PostgreSQL の SELECT 結果順を意図的に乱し、「暗黙の ORDER BY 依存」を見つけるための拡張。今回の PR はこれを reverse モードで使うことで、テスト上の問題箇所を検出しています。
  • 一般的な原則:
    • SQL 標準では、ORDER BY を指定しない限り SELECT の結果順は未定義であり、DB 実装や実行計画、同時実行状況に依存して変化しうる、という前提に立ってクエリとテストを書く必要があります。

#58297 Deprecate positional pk, id_value, and sequence_name for #insert ​

マージ日: 2026/7/31 | 作成者: @kamipo

  1. 概要 (1-2文で)
    ActiveRecord::ConnectionAdapters#insert の位置引数として使われていた pk, id_value, sequence_name が非推奨になり、それぞれに対応する新しい使い方(特に returning: オプション)への移行が明示されました。これにより、insert のインターフェイスが整理され、PostgreSQL の currval ベースのフォールバックなども段階的に廃止されます。

  1. 変更内容の詳細

2-1. 廃止される位置引数 ​

対象は ActiveRecord の low-level な insert メソッド(アダプタ実装)です。従来は概ね次のようなシグネチャで呼ばれていました(アダプタによって差異あり):

ruby
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: が単一カラム名を受け取った場合、戻り値も「単一値」になるように整備されました。
    • つまり、pk = "id" は論理的には returning: "id" に1対1で置き換え可能です。

移行イメージ:

ruby
# 旧 (非推奨): 第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 の戻り値を経由して読み直す必要は無く、そのまま自分で使えばよい。

移行方針:

ruby
# 旧 (非推奨): 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 を取得するフォールバックのために使われていた。
  • 状況:
    • 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.rb
    • sqlite3_adapter_test.rb
    • database_statements_test.rb(非推奨関連の挙動をカバー)

  1. 影響範囲・注意点

3-1. 影響がある可能性が高いコード ​

  • ActiveRecord の public API (Model.create など) を普通に使っているだけのアプリケーションは、ほぼ影響を受けません。
  • 影響するのは次のような「アダプタ層や低レベルAPIを直接触っている」コードです。

例:

ruby
# 典型的な 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: を併用。

3-3. 非推奨警告のノイズについて ​

  • 特に PostgreSQL + @use_insert_returning = false を明示的に使っている環境では、
    • pk → 内部で sequence_name 解決 → sequence_name 警告、と二重に近い形で警告が出ることがあります。
  • PRの説明では、そもそもその設定自体が非推奨なので、この追加ノイズは許容範囲とされています。
  • 将来的には、このパス自体が削除される可能性が高いと考えてよいです。

  1. 参考情報 (あれば)
  • 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 を前提にした実装に統一されつつあります。

#58322 Remove migrations filter from the schema dump ​

マージ日: 2026/7/31 | 作成者: @fxn

  1. 概要 (1-2文で)
    Rails の schema dump(db/schema.rb など)に含める「適用済みマイグレーションのバージョン一覧」から、「ローカルにファイルが存在するものだけに絞り込む」というフィルタリング処理が削除されました。これにより、マイグレーションファイルが手元に無くても、DB 上で適用済みであればそのバージョンは schema dump に書き出されるようになります。

  1. 変更内容の詳細

これまでの挙動(フィルタあり) ​

  • schema dumper は「schema_migrations テーブルに記録されているバージョン」のうち、
    • 実際に db/migrate(または設定されたパス)にマイグレーションファイルが存在するバージョンだけを、schema.rb の末尾(trailer)に記録していました。
  • その意図:
    1. ローカルに存在しないマイグレーションを dump に含めないことで「きれい」に保つ。
    2. 古いマイグレーションを「掃除」したい場合、ファイルを削除して db/schema.rb を再生成すれば trailer からも消える、という運用を想定。

イメージ (以前の schema.rb の末尾例):

ruby
# 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 本文の論点) ​

  1. ブランチをまたぐと dump が「自己完結しなくなる」問題

    • 例:
      • ブランチ A でマイグレーション X を作成 → テーブル T を追加 → マイグレーション X は schema_migrations に記録される。
      • ブランチ B に切り替えると、db/migrate からマイグレーション X のファイルが無くなる。
      • そこで rake db:schema:dump をすると:
        • テーブル T は schema に含まれる(DB には存在するため)。
        • しかし、trailer にマイグレーション X のバージョンは 書かれない(ファイルが無いから)。
    • 結果として、「この schema を適用した状態の DB には T があるが、その状態に至るマイグレーション履歴情報が dump に反映されていない」という一貫性のない状態になる。
    • PR 作者はこれを「db/schema.rb が自己完結していない」と見なしています。
  2. :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 の該当セクションに追記)。

  1. 影響範囲・注意点

影響する人 ​

  • schema_format = :ruby(デフォルト)で db/schema.rb を利用しているプロジェクト全般。
  • 特に:
    • 古いマイグレーションファイルを削除して「schema の trailer からも消えてほしい」と期待していた運用。
    • ブランチを頻繁に切り替えつつ db/schema.rb を commit しているチーム。

挙動の変化による具体的影響 ​

  1. 古いマイグレーションファイルを削除しても trailer からは消えない

    • 以前:
      • db/migrate/20200101000000_old_migration.rb を削除
      • bin/rails db:schema:dump
      • db/schema.rb の末尾から "20200101000000" が消える
    • 今後:
      • DB の schema_migrations に "20200101000000" が残っている限り trailer にも残る。
    • 「バージョン一覧を物理ファイルの存在と同期させる」という使い方はできなくなります。
    • バージョンを本当に消したい場合は、schema_migrations テーブルからバージョンを消さない限り dump からは消えない(※通常は直接いじるべきではないので、実務上は「残しっぱなし」にするのが安全)。
  2. ブランチ間での不整合は緩和される

    • 以前は、あるブランチで適用されたマイグレーションが別ブランチでは「trailer に現れない」という状態になり得ました。
    • 今後は、「DB に適用されている限りどのブランチから dump しても同じバージョンが trailer に出る」ことになり、schema ファイルの自己完結性が上がります。
  3. :sql フォーマットとの整合性が向上

    • :sql と :ruby で大きくポリシーがズレることがなくなり、ツール連携や挙動理解がしやすくなります。

注意 / 実務上のポイント ​

  • db/schema.rb の差分が少し増える可能性:
    • これまで「ローカルに無いマイグレーションは trailer から消えていた」プロジェクトでは、今後それらが再び出力されるため、PR 差分として「マイグレーションバージョン行が増えた」ように見えることがあります。
  • CI で db/schema.rb の内容チェックをしている場合:
    • テストセットアップ時にどの DB を元にしているか(schema:load / migrate / スナップショットなど)によって schema_migrations に残るバージョンが違うと、dump 結果の差異として現れ得るので注意が必要です。
  • 「古いマイグレーションの整理」のベストプラクティス:
    • これまで「ファイルを消すと schema のバージョンからも消える」という前提で運用していた場合は、方針を見直す必要があります。
    • そもそも Rails の公式スタンスとしても、「過去マイグレーションはむやみに削除しない」が推奨寄りです(新規参加者が db:setup できなくなるなどの問題もあるため)。

  1. 参考情報 (あれば)
  • 対象 PR:
    • rails/rails #58322: Remove migrations filter from the schema dump
  • 関連する設定:
    • config.active_record.schema_format = :ruby or :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. 概要 (1-2文で)
    ActionView のテンプレート依存関係トラッカー (ActionView::DependencyTracker) のレジストリを Ractor で共有可能(ractor-shareable)にするための変更です。Rails.application.ractorize! を呼んだアプリケーションに限り、依存関係トラッカーのレジストリを Ractor 間で安全に共有できるようになります。

  1. 変更内容の詳細

2-1. 依存関係トラッカーのレジストリを Ractor-shareable に ​

対象クラス: ActionView::DependencyTracker

もともと DependencyTracker は、テンプレートハンドラ(ERB, HAML など)ごとに「どのトラッカーを使うか」を登録するレジストリを持っています。このレジストリを Ractor で共有できるよう、以下の対応が行われています。

share_registry メソッドの導入 ​

PR 説明中では一時的に freeze_registry と書かれていましたが、最終的には share_registry というメソッドが導入されています(名前が示す通り「Ractor.shareable! できる状態にする」ことが目的)。

イメージとしては以下のような処理が行われるメソッドです(概念的なサンプル):

ruby
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 を呼び出すようになっています。

ざっくり言うと:

ruby
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 なハンドラに追加するケース等の互換性確認

  1. 影響範囲・注意点

3-1. ractorize! を使うアプリへの影響 ​

Rails.application.ractorize! を呼ぶアプリケーションでのみ、ActionView::DependencyTracker のレジストリが Ractor-shareable になります。

その場合:

  • レジストリは 基本的にイミュータブル とみなされるべきです。
  • Ractor 化後に、ActionView::DependencyTracker.register_tracker 等で mutate すると、
    • Ractor 的に危険(共有オブジェクトの書き換え)
    • deprecation 警告
      の両面で問題になり得ます。

3-2. 設定フェーズ後のトラッカー登録は非推奨 ​

次のようなコードを書いている場合は要注意です:

ruby
# 例: 初期化完了後のどこか(遅延ロード時など)での追加
ActionView::DependencyTracker.register_tracker(:erb, MyCustomTracker)

今後は、次のような方針が推奨されます:

  • アプリケーション設定フェーズ(config/initializers 内など)で登録を完了させる
  • 特に ractorize! を使う予定があるアプリでは、「起動後に動的にトラッカー追加・上書き」しないよう設計を見直す

3-3. Ractor 非利用アプリの互換性 ​

  • Rails.application.ractorize! を呼ばない限り、レジストリは従来通り可変です。
  • 非 shareable なトラッカーを、非 shareable なハンドラに後から足すというパターンも動き続けます。
  • ただし、「設定完了後の変更」は deprecation(将来的な削除・挙動変更の予告) である点に留意してください。
    将来のメジャーバージョンで、完全に禁止される可能性があります。

  1. 参考情報 (あれば)
  • この 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. 概要 (1-2文で)
    [[column, value], ...] 形式から移行中の「casted binds」(単なる値配列)について、SQLログおよび EXPLAIN 出力で [[nil, value], ...] と表示されていた問題を、位置パラメータ($1, $2, ...)を表示するようにして可読性を改善したPRです。PostgreSQL風のパラメータマーカーを導入し、どの値がクエリ中のどのプレースホルダに対応するか一目で分かるようにしています。

  1. 変更内容の詳細

背景: casted binds とログ表示の問題 ​

  • 既存: レガシーな binds 形式

    ruby
    # 旧形式 (レガシー)
    # [[column, value], [column, value], ...]

    ここでは column オブジェクトがあるため、ログや EXPLAIN 時に
    [[column_name, value], ...] のように、人間が読める形で表示できていました。

  • 新形式: casted binds

    ruby
    # 新形式 (casted binds)
    # [value1, value2, ...]  # 単なる値の配列

    この形式は #39106 で導入されたもので、将来的にこちらへ移行する前提ですが、
    column 情報がないため、これまでログでは次のように出ていました:

    sql
    SELECT * FROM topics WHERE title = ?  [[nil, "abcd"]]

    nil が先頭に出ており、「どのパラメータに対応しているか」が分かりにくい状態でした。

このPRでの主な変更点 ​

  1. 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 表記となり、「ログを見る人間」がパラメータ位置を即座に把握できるようにする狙い。
  2. Explain#exec_explain の出力

    • EXPLAIN を実行する際に付随して出るバインド値についても、同様に $1, $2 形式で表示されるように変更。
    • casted binds でも、EXPLAIN 出力のバインドリストが self-describing になります。
  3. 実装上の要点 (推測される変更内容)
    実際のコードは以下のようなイメージで実装されています(擬似コード):

    ruby
    # もともと: [[nil, value], ...] として扱われていたケースに対し、
    # インデックスを使って "$1", "$2" ... を生成
    casted_binds.each_with_index.map do |value, index|
      name = "$#{index + 1}"
      [name, value]
    end
    • これを StructuredEventSubscriber および Explain の双方で利用し、 casted binds に対する人間可読な表示を統一しています。
  4. テストの追加・更新

    • activerecord/test/cases/log_subscriber_test.rb
      • SQL ログの出力に $1 スタイルのマーカーが含まれていることを確認するテストが追加。
    • activerecord/test/cases/explain_test.rb
      • EXPLAIN 出力でも $1 マーカーが表示されることを確認するテストが追加。
    • bind_parameter_test.rb も軽微に更新され、bind 表示の期待値が新形式に合わせられています。

  1. 影響範囲・注意点
  • 影響を受ける箇所

    • Rails の ActiveRecord ログ (StructuredEventSubscriber#sql が吐く SQL ログ)。
    • Model.explain, relation.explain などで表示される EXPLAIN 出力のバインドリスト。
  • 既存コードへの互換性

    • 実際の SQL 発行方法・パラメータバインディングの挙動自体は変更されていません。
    • 変更されるのは「ログおよび EXPLAIN における表記のみ」です。
    • ただし、ログ出力フォーマットの文字列が変わるため、以下のようなケースでは影響があります:
      • ログを正規表現などで機械的にパースしているツールやスクリプト
      • 監査・モニタリング用にログフォーマットへ依存しているミドルウェア
    • これらのツールは、[[nil, "..."]] を前提にしていた場合、[["$1", "..."]] 形式に対応するよう修正が必要になります。
  • デバッグ・調査のしやすさの向上

    • SQL 中の ? とログのバインド値リストの対応関係が明示されるため、 特に複数の ? が並ぶクエリや、長いクエリの調査がかなり楽になります。
    • 使用DBアダプタに依存しない表記(常に $n)なので、PostgreSQL 以外でも 「n番目のパラメータ」という意味で一貫性が保たれます。

  1. 参考情報 (あれば)
  • 元のPR:
    • Show parameter marker for casted binds in SQL logs and EXPLAIN (#58312)
  • 関連PR:
    • Casted binds 導入の PR: #39106
      → [[column, value], ...] 形式から値だけの配列への移行文脈。
  • 関連クラス / メソッド:
    • ActiveRecord::Explain#exec_explain
    • ActiveRecord::StructuredEventSubscriber#sql
    • テスト:
      • activerecord/test/cases/explain_test.rb
      • activerecord/test/cases/log_subscriber_test.rb
      • activerecord/test/cases/bind_parameter_test.rb