Skip to content

Ruby on Rails PR Digest - 2026年 8月

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

#58097 Join an Array accept in file_field_tag

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

  1. 概要 (1-2文で)
  • file_field_tagaccept オプションとして配列を渡したときのレンダリングが、HTML仕様に沿ったカンマ区切り形式になるよう修正されました。
  • これにより、file_field_tagfile_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.rbrails/command.rbactive_support/rails を require するようになったコミット: 1db66c0d4e
    • リグレッションの原因になったとされるコミット: 9c43c5bda5
  • 使用メソッド:
    • delegate_missing_toActiveSupport::ConcernModule#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.columnsSHOW 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. テスト

  • columnsWHOLE_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_allUPDATE文の 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_allRelation#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
    • さらに ColumnIndexDefinition== を実装していないため、「各属性まで」比較
    • 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.rbconfig/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::StructuredEventSubscriberActionView::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 と同様で、StructuredEventSubscriberRails.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.rbstructured_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::LogSubscriberStructuredEventSubscriber を継承・拡張している独自クラスや 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_optionsActionController::Base 経由で使われるもの)です。
  • RouteSetRails.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_sqlassert 群は、テストスイートが元々持っていた unshareable_proc_action の設定のまま動く

この設計により、

  • Ractor shareability のテストに必要な箇所だけ、意図的に危険な設定 (:raise) にして
  • 検証が終わったらすぐ元の環境に戻す
    というメリハリが付けられています。

一貫性の向上

ActiveSupport::Ractors.withensure を用いるパターンはすでに以下のテストで使われており、このテストだけが浮いている状態でした:

  • 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_cutf8mb4_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)が正式サポートとして追加され、routesrequest オブジェクト・統合テストなど 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::VERBSQUERY を追加
  • ActionDispatch::Routing::Mapperquery ヘルパが追加

これにより 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_attributemethod_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 を直接利用しているコード
    • ActiveRecordActiveModel::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_methodNameError を投げ、それを 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_missingpublic_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_nameas が同じであっても、毎回 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_nameas の値に応じて生成される最終的なメソッド名は従来と同じであるように実装されているため、公開 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_hashpermitted? など、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_missingrespond_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_missingrespond_to_missing? を積極的にオーバーライドしているクラス
    • AttributeMethods の内部メソッド(private/protected)に依存したメタプログラミングを行っているコード
  • そのような高度なメタプログラミングをしている場合は、Rails 更新後に:
    • 動的 attribute アクセスが期待どおりに動いているか
    • respond_to? の結果が変わっていないか
    • method_missing が思わぬ順序で呼ばれていないか などをテストで確認することを推奨します。

  1. 参考情報 (あれば)
  • このPRがフォローアップしているコミット(パターンキャッシュ削除):
    https://github.com/rails/rails/commit/1a59302e642b98e78e88464edf0f3b7741359a92

  • ベンチマークのポイント:

    • to_hashpermitted? の 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_subscriptionsFanout の内部構造(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_subscriptionsFanout の内部変数に直接アクセスしない。
  • 将来 Fanout の内部実装が変わっても、to_ractor_snapshot / load_ractor_snapshot の契約を守れば外部コードは壊れない。

Notifications.record_subscriptions の変更

  • これまで:
    Notifications.record_subscriptionsFanout#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_snapshotload_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 をそのまま共有できる)。
  • しかし現在の FanoutConcurrent::Map ベースの状態を持っており、そのままでは COW(Copy-On-Write)に適した設計になっていない。
  • そのため、今回は snapshot アプローチを維持しつつ、「内部構造に直接アクセスせず、プロトコル越しに扱う」形にとどめている。
    将来的に Fanout の COW 対応リライトが入る場合も、このプロトコルを保つことで互換性を維持しやすくなります。

  1. 参考情報 (あれば)
  • 対応する過去 PR:
    • #58060: Notifications.record_subscriptionsFanout の内部構造に依存するようになった変更。
  • この 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::ConsoleCommandRails::Command::RunnerCommand に重複していた conditional_executor の実装が、共通の親クラスである Rails::Command::Base に集約されました。これにより、同様の挙動を持つコマンド間でロジックを共有できるようになり、重複コードが削減されています。

  1. 変更内容の詳細

※ PR本文に説明がないため、Rails 既存実装の慣例と変更された行数・ファイル構成から推測を含みます。

どんなリファクタリングか

  • 変更前:
    • Rails::Command::ConsoleCommandconditional_executor メソッド (もしくは同等のロジック) が定義されていた
    • Rails::Command::RunnerCommand にもほぼ同一の conditional_executor が定義されていた
  • 変更後:
    • この重複コードが削除され (+0/-8 が 2 ファイルで発生)
    • 共通のスーパークラスである Rails::Command::Baseconditional_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 consolerails 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_executorsend/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文で)
    normalizesenum を同じ属性に併用した際、正規化前の値に対して 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

normalizesstatus に代入された値を strip.downcase"pending" にしてくれるはずですが、実際には " Pending " のまま enum 側のバリデーションに渡されてしまい、「有効な status ではない」として ArgumentError が発生していました。

背景にある実装構造は以下のようなものです。

  • enumnormalizes も、Active Model の「型(type)」をラップするデコレータとして実装されている
  • enum + normalizes を使うと、型のスタックはざっくり NormalizedValueType → EnumType → もとの型 という構造になる
  • ActiveModel::Attribute#with_value_from_user は、ユーザー入力に対して
    1. type.assert_valid_value(value) を「生の値」で呼び出す
    2. その後に cast して内部表現に変換する
      という順で処理する

NormalizedValueTypecast はオーバーライドして正規化を挟んでいましたが、assert_valid_value はオーバーライドしておらず、そのまま内側の EnumType に「生の値」を渡していました。その結果:

  • " Pending " が正規化される前に EnumType に渡る
  • EnumType" Pending " を知らないので ArgumentError を投げる

という不整合な流れになっていました。

今回の修正内容

ActiveModel::Attributes::Normalization::NormalizedValueTypeassert_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::ConsoleIRBConsole を継承・モンキーパッチして独自バナーや独自起動処理を組み込んでいる場合、今回の変更で呼び出しタイミングが変わることで、想定外の二重表示・未表示が起きるリスクがあります。
    • そのようなコードを書いている場合は、バナー表示の条件やフックポイント(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::TestCaseActiveRecord::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::DeprecationActiveSupport::ConcernModule#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::Reflectionplural_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 のロジックが別のメソッド(例えば nameactive_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 で書かれていますが、
    • 箇条書きリスト(- item1. 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::Redirectingredirect_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_keytrue の場合
  • 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_fornil を返す
    • その 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::NotificationsRactor と併用しているコード
      (特に、メイン 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::Mapconcurrent-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を行うため、名前が誤解を生みやすいことが理由です。
  • createinsertの単なるエイリアスであり、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
  • valuenil で、key:minimum / :is の場合
    • skip_nil_check?(key)falsetrue になるのは :maximum のみ)
    • そのため if !value.nil? || skip_nil_check?(key) の中に入らない
    • 結果として resolve_value が呼ばれず、check_value には「Proc のまま」が残る
    • その check_valueerrors_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) なので挙動は変わらない
  • valuenil: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? の対象であり、valuenil でもチェックロジックに入る設計だったため、今回の修正で挙動は変わりません。
    • ただし実装上は maximum に対しても「早い段階で resolve_value する」ようになっており、Proc があれば必ず Record を引数に解決されます(以前と同じく 1 回だけ)。
  • :minimum / :maximum / :is整数リテラルや Symbol を渡しているコードの挙動は基本的に変化しません
    • 整数: resolve_value はそのまま返すだけ
    • Symbol: 既存の処理フローで正しくメソッド呼び出しなどに解決されており、今回の変更では壊さないように配慮されています
  • パフォーマンス面では、CHECKS ループ内で resolve_value する位置が変わっただけで、呼び出し回数は「チェックごとに 1 回」のままです。

運用上の注意:

  • もし既存コードで「:minimum / :is に渡した Proc が、nil の場合は絶対に呼ばれないという前提」に依存した実装をしていた場合、その前提は崩れます。
    • 例: valuenil のときには Proc 内で副作用を起こしたくない、など
    • もっとも、そんな前提で書かれることはほぼ想定されておらず、一般的には安全な変更と考えられます。

  1. 参考情報 (あれば)
  • 対応する Issue: #40642 — LengthValidator が Proc 指定の :minimum / :isnil 値で 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 パターンにマッチさせる必要があるため
  • messageblock のどちらでメッセージが供給されるかを解決(評価)してからマッチングする必要がある
    → 「メッセージを解決してからでないとパターンマッチできない」ので 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. typebinary? のテスト

    追加テスト: 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. castData 分岐のテスト

    追加テスト: 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::Datacast すると、中身の生のバイナリ文字列が返る」という仕様がテストで固定されます。

  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-vipsmini_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.rbrequire "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])
    • 正しい結果を返すこと
    • 実際にバインドクエリとして扱われていること(ログ・バインド数など)
  • wherefind_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 0DEFAULT '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_modeSTRICT_ALL_TABLES を append
  • 新: sql_modeTRADITIONAL を 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_modeTRADITIONAL が含まれていることを検証するテストを追加。

また、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_modeTRADITIONAL 相当か、厳格なモードにしている場合は、この変更による挙動の差分はほぼありません。

具体的に変わる挙動の例

以下のようなケースで、これまでは「通っていた」ものが、今後はエラーになる可能性があります(特にテスト/本番環境で 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 アプリ側から見ると、これまで普通に通っていた saveupdateActiveRecord::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.savetrue を返すかのように書かれていたが、実際には 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::MiddlewareInstrumentationProxy を直接 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_beforeFrozenError 発生
  • 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_connectioncheckout_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#initializeparent 引数削除

  • 対象ファイル: 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::TemplateActionView::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_containerApplicationController / 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#namecolumns_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_typenil になります。
テスト (test/dispatch/request_test.rb:1109) でも「Content-Type がないリクエストでは media_typenil である」と明示的に検証されており、ドキュメントだけが古い挙動を前提にしたまま残っていました。

元々は、統合テストのヘルパが常に 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_typenil を返す。
    • 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_typenil である」ことを検証。
  • 挙動が変わった元コミット(言及されているもの): 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::TableActiveRecord::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. 変更内容の詳細

背景

  • 過去のコミット 970bf380fe8ActionView::Template::Handlers::ERB:nodoc: が付与され、ドキュメント上「非公開 API」と明示された。
  • しかし一部アプリケーションでは、このクラスを直接参照して ERB の各種オプション(例: trim mode 以外の設定)を行っていた。
  • erb_trim_mode は既に config.action_view.erb_trim_mode などから設定できたが、それ以外の ERB ハンドラのオプションは公式な経路がなかった。

この PR は「ERB ハンドラの設定は ActionView::Baseconfig.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_viewActionView::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_viewActionView::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::PostgreSQLArel::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_multiread_multi_entries に重複して書かれていた「ローカルキャッシュからの複数キー解決処理」が共通メソッドに抽出されました。挙動の変更はなく、内部実装のリファクタリングです。

  1. 変更内容の詳細

背景

Strategy::LocalCache は、スレッドローカルなどにぶら下がる「ローカルキャッシュ」を持ち、同じリクエスト中に何度も同じキーを読む際に、バックエンドへのアクセスを減らすために使われます。

fetch_multiread_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_multiread_multi_entries の挙動差異が生まれにくくなり、保守性が向上します。

  1. 参考情報 (あれば)

#58401 Extract duplicated job matching logic in ActiveJob::TestHelper

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

  1. 概要 (1-2文で)
    ActiveJob::TestHelper 内で assert_enqueued_withassert_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_withassert_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_withassert_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::FromClauseempty メソッド

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 セーフ対応と整合を取る内容なので、FROMWHERE の両方で同じ 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.rbexecute_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

以前は mysql2trilogy の各アダプタ側に、上記とほぼ同じ内容の execute_batch が個別に定義されていたものを、共通モジュールに移した形です。


  1. 影響範囲・注意点
  • 対象
    • ActiveRecord の MySQL 系アダプタすべて(mysql2 / trilogy)の execute_batch 呼び出しが、新しい共通実装を使うようになります。
  • 互換性
    • 「重複していた共通コードを一箇所に集約しただけ」の変更であり、インターフェース(メソッドシグネチャ)や基本挙動に変更はない想定です。
    • ただし、以前 mysql2trilogy で微妙に挙動を変えていた場合(例えばロギングやエラー処理など)には、今回の共通化で差分が吸収される可能性があります。その場合、両者の挙動がより揃う一方、アダプタ固有の「クセ」が消えることになります。
  • アプリケーションコードへの影響
    • 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_tagjavascript_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[/[?#].+$/])
  • クエリ・フラグメントがない場合 tailnil なので、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::Valuequery_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 ではダメなのか

castserialize は「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::LookupContextActionView::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.findModel.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.rbselect_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 の原因となったコミット: 1277a8f09cprepared_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.cursorDate から 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#idspluck 委譲に変わったことで、新規レコードに紐づく未保存の関連に対する ids が空配列になってしまった問題を修正し、ids / pluck ともに関連の「target」(メモリ上のオブジェクト)から値を取得するようにした変更です。
    これにより、新規レコードに関連づけたオブジェクトに対しても、idspluck が直感的な結果を返すようになります。

  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 まで:
    • idspluck 委譲により []
    • 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 で指定されている場合に、その ProcActiveSupport::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 判定
  • touchoptional オプションでの外部キー利用 などの箇所が、単一キー・複合キーどちらでも 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 ベースの定数プロキシが「古いレジストリ」を参照し続けると不整合が起きる
  • そこで:
    • DeprecatedObjectProxytarget= セッターを追加
    • update_registries 実行後に、これらのプロキシが新しいレジストリオブジェクトを参照するように「差し替え」

これにより:

  • 古い定数経由でも常に最新の MIME レジストリ状態が見える
  • かつ、内部的には freeze された安全な構造になっている

2-4. ActionView::Base.default_formats の扱い

ActionView では、ActionView::Base.default_formatsMime.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

  • 以前は DatetimeLocalFieldfield_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 内から DateTimeSelectoryear などのメソッドを呼ぼうとすると
  • そのメソッド本体が「共有不可能な 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 メモ化の過程で
      • nilfalse などの「falsy」値をキャッシュする実装になっていた箇所がありました。
    • Ruby でよくある「@memoized ||= expensive_computation」パターンは、expensive_computationfalse / 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_keyfalsenil を返したとしても、一度計算した結果がきちんとインスタンス変数に保持され、再評価が発生しません。
    その結果、オブジェクトグラフが安定し、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::TypeActiveRecord::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 におけるカウンタキャッシュ、autosavehas_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_forautosave: 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_cachepost1 から 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.rbbelongs_to_association.rbhas_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_tohas_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.rbApplicationTests::AnalyzersIntegrationTest
  • engine_integration_test.rbApplicationTests::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 は、マイグレーションの実行コマンドを「記録するためのラッパ」であり、本来ユーザーが直接 recordinverse_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#recordCommandRecorder#inverse_ofprivate セクションの中に移動。
    • それに伴い、これらのメソッドの呼び出し元は同じクラス/内部からのみになる(外部からの直接呼び出しは意図しない使い方と明示)。
    • 併せて、キーワード引数の扱いについての実装が、今後の構造変更(位置引数とキーワード引数を分離して保存する)を前提にした形に寄せられている可能性があります。
  • テスト (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.lockredis の行も、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 TABLEALGORITHM / 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 / LOCKALTER 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 / LOCKALTER 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::AlterTablealgorithm / 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: truealgorithm: / 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 により役割分担が変わったため、

  • AlterTablealgorithm / 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_tablecommand を直接 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
    • このコミットで DeprecatedInstanceVariableProxyDeprecatedConstantProxy のコンストラクタ 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_objectActionView タグが持っているビューコンテキストであり、ここから 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.rbautosave ブランチのテスト追加)
  • この修正により、search_fieldautosave に関するドキュメントと実際の挙動が一致し、今後 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-essentiallibssl-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::ConnectionAdaptersinsert, update, delete に対して、従来の「第2引数以降で binds を渡す」呼び出し方が非推奨になりました。代わりに、SQL文字列とバインド値をまとめた Arel.sql(..., *binds) を渡す書き方に移行するための正式なデプリケーションです。

  1. 変更内容の詳細

非推奨になる呼び出し方

これまで多くのアダプタ実装やアプリケーションコードでは、以下のように sqlbinds を別々に渡す呼び出し方が可能でした:

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 の将来バージョンにアップグレードした際に ArgumentErrorNoMethodError になるリスクがある。

マイグレーションの方針

  • 旧スタイル:
    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.sqlSQL文字列に対してバインドを埋め込むのではなく、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#parameterizeseparator: nil を渡すと TypeError で落ちていた不具合を修正し、nil を空文字と同等に扱うようにした PR です。これにより、separator を設定ファイルやパラメータからそのまま渡しても安全に動くようになります。

  1. 変更内容の詳細

何が問題だったか

ActiveSupport::Inflector#parameterizeString#parameterize の実体)は、内部で次のような処理をしていました。

ruby
parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator)

unless separator.nil? || separator.empty?
  # 複数連続したセパレータを1つにまとめる
  # 先頭/末尾のセパレータを削除する
end

意図としては:

  • separatornil または ""(空文字)の場合:
    • セパレータを挿入しない(そのままくっつける)
  • それ以外("-", "_" など)の場合:
    • 余分なセパレータをまとめたりトリムしたりする

という設計でした。

しかし Ruby の gsub!replacement = nil を渡すと TypeError になるため、

ruby
"Donald E. Knuth".parameterize(separator: nil)
# => TypeError: no implicit conversion of nil into String

と、separator.nil? を見る前に落ちてしまっていました。
つまり「separatornil の場合を考慮したガード」は存在するのに、実際にはそこまで到達できないという矛盾した状態でした。


どう直したか

separator: nilseparator: "" と同等に扱うように変更し、ガードロジックを整理しています。

変更のポイント:

  1. nil を空文字として扱う
    gsub! に渡す前に、separatornil の場合は "" に正規化するようにしています(実装的には 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#parameterizeseparator: nil を渡すケースのみ です。
    • これまで TypeError が発生していたケースが、 "TypeErrorで落ちる" → "文字列として正しくパラメータ化される" に変わります。
    • separator: "" を使っていた既存コードの挙動は変わりません(nil がその挙動に揃えられただけ)。
  • 後方互換性

    • 実際に separator: nil を意図して使っていた場合:
      • これまでは例外が発生 → 今後は "donaldeknuth" のようにパラメータ化されるようになるので、「エラーで気づく」パターンを前提にしていた場合は挙動が変わる点に注意が必要です。
      • ただし「nil をサポートしない」方向(nil ガードを削除して常に TypeError にする)ではなく、「自然に動く」方向に寄せたため、一般的なアプリケーションにとっては改善と見なせる変更です。
    • 設定ファイルや params から separator をそのまま渡しているコードにとっては、nil の場合も安全に動作するようになるため、想定外エラーのリスクは減ります。
  • 注意点

    • バリデーションや例外発生有無で挙動をテストしていた場合(例: 「不正な separator なら TypeError」などの仕様を勝手に決めていた場合)、テストが通らなくなる可能性があります。
    • nil を指定したらエラーになるべき」と考えていた場合、この PR はそれと逆方向の挙動になります。もしそのポリシーを保ちたいプロジェクトでは、アプリケーション側で separator を検証してください。

  1. 参考情報 (あれば)
  • 対象メソッド:
    • String#parameterizeActiveSupport::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 == Objectreceiver 呼び出し時の 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 が起きる
  • 結果的に receivernil になる
  • その場合、例外メッセージ 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_namenil
  • 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_datetimeweek_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

basename が空のクラスなので、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#freezei18n_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
    • UserApplicationRecord 由来の 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::SchemaDumperdb/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_enabledtrue / 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_allupsert_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_namepk から解決し、super 経由で再度 sequence_name 警告が出る可能性があります。
  • PRではこれを「すでに非推奨な設定パスで発生する追加ノイズ」として容認しています。

2-2. 非推奨警告の出し方

  • pk, id_value, sequence_name はそれぞれ独立にチェックされ、個別に警告が出るようになっています。
  • これにより、どの引数に対して、どんな移行パスを取るべきかが明示されます。
    • pkreturning: を使う
    • id_valueinsert の戻り値に頼らず、その場で持っている値を使う
    • sequence_nameuse_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 = :sqlstructure.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