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-2文で)
file_field_tagにacceptオプションとして配列を渡したときのレンダリングが、HTML仕様に沿ったカンマ区切り形式になるよう修正されました。- これにより、
file_field_tagとfile_fieldの挙動が揃い、ブラウザが複数 MIME type を正しく解釈できるようになります。
- 変更内容の詳細
- 対象:
ActionView::Helpers::FormTagHelper#file_field_tag
これまで:
file_field_tag("picture", accept: ["image/png", "image/gif"])
# => <input type="file" name="picture" accept="image/png image/gif">
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# 配列がスペース区切りで join されていた修正後:
file_field_tag("picture", accept: ["image/png", "image/gif"])
# => <input type="file" name="picture" accept="image/png,image/gif">
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# 配列がカンマ区切りで join されるHTML仕様上、accept 属性は「カンマ区切りのトークンのリスト」と定義されていますが、以前の実装はスペース区切りで結合していたため、2つ目以降の値をブラウザが無視するケースがありました。
今回の修正では:
acceptに Array が渡された場合、内部でaccept.join(",")相当の処理を行い、カンマ区切りで出力するように変更acceptが String の場合はそのまま利用(既存挙動維持)- すでにオブジェクトフォーム側 (
file_field) ではカンマ区切りで join されていたため、それと同じロジックに揃えた
テスト・ドキュメント関連:
actionview/test/template/form_tag_helper_test.rbに、配列acceptが期待通りカンマ区切りでレンダリングされることを検証するテストを追加actionview/CHANGELOG.mdに本変更のエントリを追加
- 影響範囲・注意点
- 影響を受けるケース:
file_field_tagを使い、acceptに Array を渡していたコードrubyこれまで事実上、最初の MIME type しか効いていなかったブラウザが、修正後は複数 MIME type を正しく解釈するようになります。file_field_tag :file, accept: ["image/png", "image/jpeg"]
- 影響を受けないケース:
file_field_tagに単一の MIME type を String で渡している場合rubyfile_field_tag :file, accept: "image/png"file_field(モデルバインド版) を使っている場合
→ もともとカンマ区切りで join されていたため挙動は変わらない
- レイアウト・仕様依存の注意:
- もしスペース区切りの
acceptを前提とした、独自のパース処理や JS があれば、カンマ区切りに対応しているかを確認する必要があります(一般的なブラウザはもともとカンマ区切り前提なので通常は問題なし)。 - HTML仕様・ブラウザの挙動により、修正後のほうが「本来の期待どおりに複数 MIME type が効く」状態になるため、UI 上で選択できるファイルの種類が増えたように見えるケースがあります。
- もしスペース区切りの
- 参考情報 (あれば)
- 関連コード:
ActionView::Helpers::Tags::FileField#render
ここではすでにacceptの Array をカンマ区切りにしており、今回file_field_tagがそれに追従した形です。
- HTML仕様:
- HTML Living Standard:
input要素のaccept属性は「コンマ区切りの型/拡張子のリスト(“Comma-separated list of items”)」と規定されており、本変更はその仕様順守のための修正となります。
- HTML Living Standard:
#58305 Require active_support/rails in rails/railtie
マージ日: 2026/8/16 | 作成者: @55728
- 概要 (1-2文で)
Rails 8.1 でrequire "rails/railtie"を単体で実行するとdelegate_missing_toが未定義でNoMethodErrorになる不具合があり、それを解消するためにrails/railtie内でactive_support/railsを require するようにした PR です。
Rails 本体より先に Railtie を読み込む一部の gem (terser,inline_svg,simple_form,scenicなど) が 8.1 で落ちる問題を修正します。
- 変更内容の詳細
問題点
Rails 8.1 で以下のようにするとエラーになります。
$ ruby -e 'require "rails/railtie"'
.../rails/initializable.rb:41:in '<class:Collection>': undefined method 'delegate_missing_to' for class Rails::Initializable::Collection (NoMethodError)
delegate_missing_to :@collection
^^^^^^^^^^^^^^^^^^^Rails::Initializable::Collection では delegate_missing_to を使っていますが、このメソッドを提供している Active Support の拡張がまだ読み込まれていないためです。
Rails 8.0.5 では同じコードが動作していたため、8.1 で導入された変更(コミット 9c43c5bda5 付近)が原因のリグレッションとされています。
delegate_missing_to は Active Support が提供するメソッドで、active_support/rails(Rails 用のプレリュード)を require すると利用可能になります。
具体的な修正
railties/lib/rails/railtie.rb に 1 行追加されたのみです。
イメージとしては以下のような変更です(PR からの意訳):
# railties/lib/rails/railtie.rb
require "active_support/rails" # ← この1行が追加された
module Rails
class Railtie
# ...
end
endこれにより、rails/railtie を require した時点で delegate_missing_to などの Active Support 拡張が読み込まれ、Rails::Initializable::Collection などが安全に定義できるようになります。
なぜ active_support/rails なのか
delegate_missing_toなどを含む Rails 向け Active Support の「標準セット」を読み込む入口がactive_support/rails- すでに他のエントリポイントでは同じ対応がされている:
rails.rbrails/command.rb
これらも以前のコミット (1db66c0d4e) でactive_support/railsを require するようになっている
- 今回の変更により、
rails/railtieも他のエントリポイントと整合した挙動になる
- 影響範囲・注意点
影響するケース
Rails 本体より先に Railtie をロードする gem・アプリケーション:
ruby# 例: gem の railtie ファイル内で require "rails/railtie" module MyGem class Railtie < ::Rails::Railtie # ... end end実際に報告されている対象:
terser/railtieinline_svg/railtiesimple_form/railtiescenic/railtie
これらは Rails 8.0.5 では正常動作 → Rails 8.1.3 で NoMethodError → 本修正により再度ロード可能、という挙動になります。
影響しない / 変化がほぼないケース
- 通常の Rails アプリで
config/application.rbなどからrailsを require して起動する一般的なパターンでは、既にrails.rb経由でactive_support/railsが読み込まれているため挙動は従来と変わりません。 - PR 説明によると、
./tools/railspect requires .(Rails の require パターンを網羅的にテストする内部ツール)を実行した結果は、この変更の有無で同一とのことで、副作用が極めて小さいことが確認されています。
開発者が意識しておく点
- gem 側でのワークアラウンドとして
require "active_support/rails"を自前で書く必要はなくなります(Rails 8.1 以降、この修正が入ったバージョンを前提にできる場合)。 - もし 8.1 のマイナーバージョン間で「Railtie の読み込みだけで落ちる」現象に遭遇している場合、この PR を含むバージョンに上げることで解消が期待できます。
- 独自 Railtie を持つ gem で、Rails 本体より先にロードされる可能性がある場合は、この種の「Active Support 機能がまだ読み込まれていない」問題を避けるため、今後も Rails のロード順・require 依存を意識しておくとよいです。
- 参考情報 (あれば)
- 該当 PR: Require
active_support/railsinrails/railtie(#58305) - 関連する過去の変更:
rails.rbとrails/command.rbがactive_support/railsを require するようになったコミット:1db66c0d4e- リグレッションの原因になったとされるコミット:
9c43c5bda5
- 使用メソッド:
delegate_missing_toはActiveSupport::Concern/Module#delegate_missing_toとして提供される Active Support の拡張メソッドで、オブジェクトに存在しないメソッド呼び出しを指定のオブジェクトに委譲するために使われます。
#58494 Read the columns of many tables in one query
マージ日: 2026/8/16 | 作成者: @ngan
- 概要 (1-2文で)
connection.columnsが複数テーブルを一度に取得できるようになり、スキーマキャッシュのダンプ時に発行される SQL 文数と実行時間が大幅に削減されました。MySQL/MariaDB/PostgreSQL 向けにカタログからの一括取得ロジックが追加され、MariaDB のデフォルト値扱いも精緻化されています。
- 変更内容の詳細
2-1. columns が複数テーブル対応に
これまで:
connection.columns(:users) # => [Column, ...]
# テーブルごとに個別クエリこれから:
connection.columns(:users) # 既存と同じ => [Column, ...]
connection.columns([:users, :posts]) # 新形式 => { "users" => [...], "posts" => [...] }- 単一テーブル指定時は従来どおり
Array<Column>を返却。 - 複数テーブル指定時は
"テーブル名" => Array<Column>の Hash を返却。 - 内部的に「スキーマ情報をまとめて読むためのインターフェイス (
WHOLE_SCHEMA_READERS)」の一員として扱われるようになり、スキーマキャッシュダンプで一括読みが利用される。
2-2. スキーマキャッシュダンプの効率化
250 テーブルでの測定:
| statements | time | |
|---|---|---|
| before | 255 | 124.6 ms |
| after | 6 | 44.0 ms |
「6ステートメント」に含まれるもの:
- テーブル一覧
- プライマリキー情報の一括取得
- カラム情報の一括取得(今回の変更の主対象)
- インデックス情報の一括取得
schema_migrationsの存在確認schema_migrationsからバージョン一覧取得
以前は add_all 内でテーブルリストを二重に取得しており(data_source_exists? 経由)、その重複が解消され 1ステートメント減少。@data_sources を既に取得済みのテーブルリストで初期化することで、再問い合わせを避けています。
2-3. アダプタごとの実装
MySQL / MariaDB
- これまで:
SHOW FULL FIELDS FROM <table>をテーブルごとに実行。 - これから:
information_schema.columnsをテーブルリストで絞り込んで一括取得。
ポイント:
information_schema.columnsがSHOW FULL FIELDSと同等の情報を、ほぼ同じカラム名で持っていることを利用。- MySQL 5.6/5.7 のように data dictionary 登場前のバージョンでも、テーブルごとに
SHOW FULL FIELDSを打つより、一発でinformation_schemaを読む方が速いことを計測で確認。- 例: 31 テーブルで
- 5.6: 15.1 ms/テーブル vs 1.8 ms (一括クエリ)
- 5.7: 19.1 ms/テーブル vs 2.4 ms (一括クエリ)
- 例: 31 テーブルで
PostgreSQL
- 既存の
pg_attributeを読むクエリを拡張し、「単一テーブル」→「複数テーブルのリスト」に対応。 - パラメータとして複数テーブルを与え、IN 句などでフィルタして一括取得する構成。
SQLite
- SQLite 自体はテーブル単位の読み出ししか提供しないので、既存の「テーブル単位実装」をそのまま利用。
- ただし、インターフェイスとしては複数テーブル引数に応答できるようにし、各テーブルを順番に呼び出して Hash にまとめる形。
2-4. MariaDB のデフォルト値扱いの改善
MariaDB の information_schema.columns では、デフォルト値が「SQL リテラルとして宣言された文字列」の形で保存されています。SHOW FULL FIELDS の結果とは以下のような差があります:
宣言: 'a\nb' -> catalog: バックスラッシュ + n / SHOW: 実際の改行
宣言: 'a\tb' -> catalog: タブ文字 / SHOW: タブ文字
宣言: 'a\\b' -> catalog: バックスラッシュ2つ / SHOW: バックスラッシュ1つ
宣言: 'a\%b' -> catalog: バックスラッシュ保持 / SHOW: バックスラッシュ保持対応内容:
- カタログの値は クォートを外した上で Rails 内部表現に合わせて解釈されるようにしたため、
DEFAULT NULL→"NULL"という文字列ではなく、NULL として扱える。- クォートされたデフォルトは「値」であって「式」ではない、という扱いを
update_fields_for_mariadb相当のロジックなしに判断可能になった。
- これにより、
SHOW CREATE TABLEを追加で読む必要が減り、MariaDB でより正確かつ効率的な情報が得られる。
なお、TEXT/BLOB のデフォルトについては、既存挙動(new_column_from_field 側で unquote するために、ここではクォート保持)が微妙なケースを含むものの、互換性維持のため現状維持とし、コードコメントで理由が説明されています。
2-5. 生成される Column オブジェクトの同一性検証
Column#== は PostgreSQL 特有の属性 (serial, identity, generated) を比較対象から外しているため、インスタンス変数ベースで厳密比較を実施し、以下の環境で「単一テーブル読み」と「一括読み」で完全に同一の Column が生成されることを確認しています。
| DB | テーブル数 | カラム数 |
|---|---|---|
| MySQL 8.0.46 | 21 | 76 |
| MariaDB 11.8.8 | 4 | 60 |
| MySQL 5.7.44 | 31 | 110 |
| MySQL 5.6.51 | 31 | 108 |
| PostgreSQL 17 | 21 | 73 |
検証には、通常のスキーマでは出ないようなケースも含まれています(式デフォルト、関数っぽい文字列、ON UPDATE CURRENT_TIMESTAMP、STORED/VIRTUAL 生成カラム、各種エスケープ・クォート・バックスラッシュ・NUL・Ctrl-Z・空文字、bit/enum/コメント/文字セット違い/明示的コレーションなど)。
2-6. スキーマ修飾名 (schema-qualified names)
public.things/other.thingsのように同名テーブルが複数スキーマに存在するケースでも、呼び出し側が渡した識別子をそのままキーにして結果を返します。
例:
connection.columns(["public.things", "other.things"])
# => {
# "public.things" => [#<Column name="id">, #<Column name="pub_col">],
# "other.things" => [#<Column name="id">, #<Column name="other_col">]
# }- カタログから該当情報が一切返ってこないテーブルについては:
- 「見えない/存在しない」と判断し、従来と同じ「単一テーブル読み」フォールバックで扱うことで、後方互換性を維持。
2-7. テスト
columnsがWHOLE_SCHEMA_READERSに追加され、共通テストで以下をカバー:- 渡したテーブルセットだけを読むこと
- 1テーブル読みと結果が一致すること
- テーブル数に対してスケールしない(クエリ数が増えない)こと
- 空配列を渡した場合は何も読まないこと
- スキーマ修飾名をキーとして正しく扱うこと
- カラム順序が
columnsの仕様上重要なため、比較時にソートしない新規テストを追加。 - MariaDB の挙動は既存テストでかなりカバーされており、
DEFAULT NULL対応・式判定・エスケープ処理・ダブルクォート処理などをわざと壊すと複数のテストが落ちることを確認済み。 - フルテストスイート OK:
- sqlite3, mysql2, trilogy, postgresql, MariaDB 11.8
- MariaDB は元々存在するトランザクション関連 4テストが引き続き失敗するのみで、本変更に起因する新たな失敗はなし。
- MySQL 5.6/5.7 および MariaDB はローカルコンテナで確認されており、本番 CI マトリクスに入れているプロジェクトでは再確認推奨。
- 影響範囲・注意点
- パブリックAPIの挙動変化:
connection.columnsが「引数の型によって戻り値の型が変わる」メソッドになりました。- シンボル/文字列 1つ →
Array<Column> - 配列(複数テーブル) →
Hash<String, Array<Column>>
- シンボル/文字列 1つ →
- ライブラリ/アプリ側で
columnsを直接呼んでいる場合、今後「配列を渡すと Hash が返る」ことを前提にしたコードを書ける一方で、既存コードが配列前提で「複数テーブル名を渡していた」場合はバグになりうるため要確認。
- パフォーマンス面の影響:
- スキーマキャッシュダンプや
schema_cache.ymlの生成など、「全テーブルのメタデータを読む処理」で劇的にクエリ数が減るため、大規模スキーマ(数百テーブル)環境ほど恩恵が大きいです。 - MySQL 5.6/5.7 のような旧バージョンでも、一括クエリの方が速いことが確認されているため、特定バージョンでのパフォーマンス悪化は想定されていません。
- スキーマキャッシュダンプや
- MariaDB 特有の互換性:
- カタログ経由によるデフォルト値の扱いが改善されていますが、基本的には「より正しい情報に近づく方向」であり、互換性破壊ではなくバグ修正寄りの変更です。
- もし MariaDB + TEXT/BLOB デフォルト値を多用しているアプリがある場合は、スキーマダンプ結果や
Column#defaultの値が従来と同じかを一度確認すると安心です(挙動は維持されている想定)。
- SQLite は実質的に挙動維持:
- API レベルで複数テーブルに対応しただけで、内部的には各テーブルを順に読むだけなので、SQLite 環境では性能差はほぼありません。
- 参考情報 (あれば)
- 関連 PR:
- #58421, #58488 — 同様に「スキーマ情報をまとめて読む」ための前段階の変更。
- 実装箇所:
activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rbactiverecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rbactiverecord/lib/active_record/connection_adapters/postgresql_adapter.rbactiverecord/lib/active_record/connection_adapters/schema_cache.rbactiverecord/lib/active_record/schema_dumper.rb- テスト:
activerecord/test/cases/connection_adapters/schema_statements_test.rb
- CHANGELOG:
activerecord/CHANGELOG.mdに本変更が記載されており、Rails のリリースノートにも掲載される見込みです。
#58495 Fix typo Rails Guide: Active Record Encryption 3.7 Fixtures [ci skip]
マージ日: 2026/8/16 | 作成者: @AmaraFinbarrs
概要 (1-2文で)
Active Record Encryption ガイドの「3.7 Fixtures」節における英文の文法ミスを修正したドキュメント専用の PR です。コードや挙動には一切変更がなく、文書の可読性・正確性のみが向上しています。変更内容の詳細
対象ファイル:guides/source/active_record_encryption.md
「3.7 Fixtures」章の冒頭の英文が、以下のように修正されています。
- To allow your tests can use plain text values...
+ To allow your tests to use plain text values...元の文は文法的に不自然("allow A can do B" は誤用)だったため、正しい構文である "allow A to do B" に修正されています。
Rails Guides はそのまま英語で読まれることも多く、ガイドを参照しながら実装・学習する開発者にとって、文法的に自然な英語になるように整備した変更です。
- 影響範囲・注意点
影響範囲
- Rails のフレームワーク本体や Active Record Encryption の挙動には一切影響しません。
- CI を回さないため
[ci skip]が付与された、ドキュメント専用の変更です。 - Active Record Encryption の Fixtures に関する説明の英語が正しくなり、ガイドの可読性・理解しやすさがわずかに改善されます。
注意点
- アプリケーションコードや設定を変更する必要はありません。
- ガイドのリンクや章構成などには変更がないため、既存のドキュメント参照(URL や節番号)もそのまま有効です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58495
- 対象ガイド: Active Record Encryption ガイド「3.7 Fixtures」節
- 一般的な英文法としても "allow your tests to use ..." が正しい表現であり、英語ガイドの品質向上の一環と言えます。
#58491 Fix Relation documentation return values
マージ日: 2026/8/16 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Relationのドキュメントで誤っていた戻り値の説明とコード例が修正されました。touch_allの戻り値とcreateのサンプルコードが、実際の挙動に沿うように整合性を取った変更です。
- 変更内容の詳細
touch_all のドキュメント修正
問題点
Relation#touch_allは内部的にupdate_allを呼び出しており、実際には「更新された行数 (Integer)」を返します。
しかし、これまでのドキュメント例では、touch_allが UPDATE文の SQL 文字列を返すかのような例になっていました。修正内容
- ドキュメント内の
touch_allの使用例 4つすべてについて、- 「戻り値として UPDATE 文の文字列」が出てくるような例
をやめ、 - 更新件数(整数)を返すことが分かるような例
に修正。
- 「戻り値として UPDATE 文の文字列」が出てくるような例
- これに合わせて
update_allまわりのテスト (activerecord/test/cases/relation/update_all_test.rb) も、ドキュメントに沿う形で微修正されました(コメントや期待値の表現調整レベルと思われる)。
- ドキュメント内の
※具体的なコードは PR 本文からの抜粋がないため概略のみですが、たとえば:
# 以前(イメージ)
User.where(active: true).touch_all
# => "UPDATE \"users\" SET \"updated_at\" = '...'" # のような誤った例
# 修正後(イメージ)
User.where(active: true).touch_all
# => 3 # 更新されたレコード件数というように、「SQL文字列が返る」風の例が「行数が返る」説明に改められています。
create のドキュメント修正
問題点
Relation#createのドキュメントで、users.create(name: 'fxn')の結果オブジェクト(戻り値)を使った例として書いたつもりのコードが、
実際には 引数なしのusers.create呼び出し に結果が「くっついて」見える形になっており、挙動を誤解させる状態になっていました。誤解されうる点
スコープがusers = User.where(name: 'scoped')などとなっている場合、users.createを 引数なしで呼ぶと、where で指定した条件がそのまま属性として使われます。
つまり、その例のまま書くと:- 本来伝えたかった「
users.create(name: 'fxn')の戻り値を使う」説明ではなく、 - 「スコープされた name を使ってレコードを新規作成する
users.create」の呼び出しになってしまう。
- 本来伝えたかった「
修正内容
users.create(name: 'fxn')という呼び出しの戻り値を変数に代入するか、- あるいはその戻り値を正しく参照する形
になるよう、サンプルコードが修正されています。 - 結果として、
- 「scoped name で意図せずレコードを作ってしまうコード」 にはならず、
- 「
createの戻り値(新しく作成されたレコード)をどう扱うか」という主旨が明確になりました。
- 影響範囲・注意点
- ランタイム挙動への影響はなし
変更はドキュメントとドキュメント整合用のテストのみであり、Relation#touch_allやRelation#createの実装・挙動自体は変わっていません。 - 既存コードの修正は不要
- すでに
touch_allの戻り値を「更新件数 (Integer)」として扱っているコードは、従来どおりそのまま動作します。 createに関しても、ドキュメントのサンプルが直っただけなので、本番コードへの影響はありません。
- すでに
- 注意点
- これまでドキュメントの例を参考に「
touch_allは SQL 文字列を返す」と誤解していた場合、実際は整数が返る点を改めて確認してください。 - スコープ付きの
Relation#createを引数なしで呼ぶと、スコープ条件が属性として使われる(例:where(name: "foo").create→ name: "foo" のレコードができる)という挙動を、改めて意識しておくとよいです。
- これまでドキュメントの例を参考に「
- 参考情報 (あれば)
- 対象クラス:
ActiveRecord::Relation- メソッド:
#touch_all,#update_all,#create
- メソッド:
- 変更ファイル:
activerecord/lib/active_record/relation.rbactiverecord/test/cases/relation/update_all_test.rb
- 関連ドキュメント(英語・最新版):
- Rails API Doc: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html
(今後この PR の内容が反映される想定)
- Rails API Doc: https://api.rubyonrails.org/classes/ActiveRecord/Relation.html
#58488 Read the schema cache's primary keys and indexes for many tables at once
マージ日: 2026/8/15 | 作成者: @ngan
- 概要 (1-2文で)
SchemaCache#add_allが、テーブルごとに個別に問い合わせていた「主キー」と「インデックス」の読み込みを、複数テーブルをまとめて取得する形に変更し、スキーマキャッシュのダンプ時の SQL ステートメント数を大幅に削減する変更です。キャッシュされる内容や順序は従来と完全に同一で、パフォーマンス改善のみが目的です。
- 変更内容の詳細
何をしているか
対象:
ActiveRecord::ConnectionAdapters::SchemaCache#add_all以前の挙動:
add_all(tables)の内部で、各テーブルごとにadd(table)を呼び出していたadd(table)はそのテーブルの- 主キー (
primary_key) - カラム情報 (
columns) - インデックス (
indexes) をそれぞれ個別のクエリで取得
- 主キー (
- 結果として、「テーブル数 × 3種類(PK / columns / indexes)」分のクエリが発行されていた
今回の変更後:
add_all内で、まず「テーブル一覧」を引数に- すべてのテーブルの主キー
- すべてのテーブルのインデックス を“まとめて”問い合わせる
- その結果をキャッシュに格納したあとで、各テーブルについては「カラム情報」だけを個別に読む
- つまり、主キー・インデックスについては「1回でまとめて読む」ようになった
擬似コードイメージ:
def add_all(tables)
# 変更後: 主キーとインデックスをまとめ読み
primary_keys_by_table = connection.primary_keys_for_tables(tables) # イメージ
indexes_by_table = connection.indexes_for_tables(tables) # イメージ
primary_keys_by_table.each do |table, pk|
# 単一 PK は String、複合 PK は Array として保存(従来と同じ形)
@primary_keys[table] = pk.is_a?(Array) && pk.size == 1 ? pk.first : pk
end
indexes_by_table.each do |table, idxs|
@indexes[table] = idxs
end
# カラムは従来通りテーブルごとに取得
tables.each do |table|
add_columns_for(table) # 従来の columns 読み出し処理
end
end※実際のコードはアダプタ依存のメソッド呼び出しを行っていますが、概念的にはこのような流れです。
主キーの型の扱い
add_allが主キー情報を保存する際のフォーマット:- 単一カラム主キー:
String - 複合主キー:
Array<String>
- 単一カラム主キー:
- これは、もともと
SchemaStatements#primary_keyが返していた形と一致しており、既存のaddの挙動と整合するようにしている - この両パターン(単一 PK / 複合 PK)はテストでカバーされている
バッチ取得の前提
- 事前にマージされている PR (#58421) により、
- スキーマリーダー側(アダプタ)は「テーブルのリスト」を渡して主キー・インデックスをバッチ取得できるようになっている
- この PR はそれを SchemaCache 側で利用する follow-up で、「キャッシュ構築時にバッチ API を使う」ようにするもの
- 影響範囲・注意点
パフォーマンスへの影響
- MySQL 8.0 / テーブル204個の環境でのステートメント数:
- 変更前: 615
- 変更後: 209
- 615 ≒ 204 * 3(PK / columns / indexes)に対応
- 209 ≒ 204(columns) + 2(PK と indexes のカタログクエリ各1)
- つまり:
- カラム読み出しはテーブルごとのまま(変更なし)
- 主キー・インデックスは 1 回のカタログクエリで一括取得
- カラム情報が依然としてクエリの大半を占めており、これをバッチ化する話は別途進行中とのこと
アダプタごとの挙動差
- MySQL / PostgreSQL / Trilogy など、「複数テーブル分を一度に読めるカタログ API」を持つアダプタ:
- 実際にクエリ発行数が減る(上記 MySQL の例のような効果)
- SQLite など「複数テーブル分のカタログ API を持たない」アダプタ:
- 内部でテーブルごとにループして同じだけクエリを発行するため、実質的なステートメント数の削減はない
- ただし、SchemaCache 側の呼び出しは「1回のまとめ呼び出し」になっているので、インターフェースは統一されている
キャッシュ内容への影響(互換性)
- 重要なポイント: 生成されるスキーマキャッシュの中身は、以前とビットレベルで同一
- 検証内容:
- 単一テーブル読み出し vs リストで読み出しでキャッシュを作り、
- 以下のインスタンス変数をすべて比較:
@columns@columns_hash@primary_keys@data_sources@indexes@version
- さらに
ColumnやIndexDefinitionは==を実装していないため、「各属性まで」比較 - sqlite3, trilogy, mysql2, postgresql で全て一致することを確認済み
- インデックス順序:
- 注意されがちな点なので明示:
schema.rbと違い、キャッシュは「返ってきた順序の array」をそのままシリアライズする- しかし
SchemaCache#indexes自体はconnection.indexes(table_name)の結果をそのまま使っており、- #58421 時点ですでに「単一テーブルもリストも同じカタログクエリ経由」になっている
- 従って今回の変更で順序が変わることはない
- 具体例:
- MySQL で
zzz, aaa, mmm, bbbの順に index を定義しても、 - per-table パスでも既にアルファベット順に返していたことを確認済みであり、
- 新旧で順序が変わらないことを確認済み
- MySQL で
- 注意されがちな点なので明示:
呼び出し側との関係
SchemaCache#add_allの呼び出し元はSchemaReflection#dump_toのみdump_toは「空の SchemaCache」に対して実行される- そのため、
add_all内で「常に」主キー・インデックスを事前フェッチしても安全(既存キャッシュの上書き等の問題はない)
SchemaCache#add自体は変更されておらず、テーブル単位でのメモ化も従来通り- 1 テーブルを繰り返し読むケースでは、これまで通り追加コストは発生しない
エッジケース: 空のデータベース
- テーブルが 0 件のケース:
- クエリは発行されず、エラーにもならない
- これは #58421 の
test_an_empty_list_reads_nothingによって、リーダー側の挙動がテストで保証されている
- 参考情報 (あれば)
- 関連 PR:
- #58421: スキーマリーダー(アダプタ側)に「テーブルリストを渡して主キー・インデックスをまとめて取得」する能力を追加
- 変更ファイル:
activerecord/lib/active_record/connection_adapters/schema_cache.rbadd_allの実装変更(バッチ取得の導入)
activerecord/test/cases/connection_adapters/schema_cache_test.rb- 新しいバッチ読み出しの形(単一 PK / 複合 PK)を含むテスト追加
- テスト状況:
- sqlite3: 9,713 tests
- trilogy: 9,863 tests
- postgresql: 10,592 tests
- いずれもフルスイートで失敗なし
#58487 Fix the CollectionProxy#delete_all documentation return value
マージ日: 2026/8/15 | 作成者: @55728
- 概要 (1-2文で)
CollectionProxy#delete_allのドキュメントが、実際の挙動(削除件数を返す)と食い違っていたのを修正する PR です。特に:nullify戦略のサンプルが「レコード配列を返す」かのように書かれていた点を、正しい戻り値の説明に合わせました。
- 変更内容の詳細
対象メソッド:
ActiveRecord::Associations::CollectionProxy#delete_all実際の挙動:
delete_allは「削除されたレコード数(Integer)」を返す。- これは
:nullify戦略を含む全ての削除戦略で同じ。
問題だった点:
ドキュメント中の
:nullifyのサンプルが以下のように、「削除されたレコードの配列を返す」かのような例になっていた:rubyperson.pets.delete_all # => [ # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, # #<Pet id: 2, name: "Spook", person_id: 1>, # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # ]一方、テストコードでは明確に「件数を返す」ことを前提にしている:
ruby# activerecord/test/cases/associations/has_many_associations_test.rb assert_equal count, firm.dependent_clients_of_firm.delete_all(:nullify)
この PR の修正内容:
CollectionProxy#delete_allのドキュメント上の説明文を修正し、戻り値が「削除されたレコード数 (Integer)」であることを明示。以前のコミットで
:destroyと:delete_allの例から「配列を返す」という表現は削除されていたが、delete_all用の正しい戻り値の説明が抜けていたため、それを補完。これにより、近接する
#delete/#destroyのドキュメントとの整合性がとれる:ruby# #delete # ... then it will follow the default strategy. Returns an array with the # deleted records. # #delete_all (今回修正後) # ... then it will follow the default strategy. Returns the number of deleted # records.
補足:
CollectionProxy上の 4 つの削除系メソッドのうち、delete_allだけがレコード配列ではなく削除件数を返す仕様。- 古いサンプルがこれを誤解させる形になっていた、というのが今回の修正対象。
- 影響範囲・注意点
- 実装コードは変わっておらず、挙動そのものは従来通りで、ドキュメントのみの修正です。
- 影響範囲:
CollectionProxy#delete_allを利用するアプリケーションコード自体への影響はなし。- ただし、ドキュメントを信じて「配列が返る」と誤解していた場合は、仕様理解を修正する必要があります。
- 注意点:
collection.delete/collection.destroyは「削除されたレコードの配列」を返すのに対し、collection.delete_allは「削除件数 (Integer)」のみを返す点を区別すること。- 特に
:nullify戦略使用時でも戻り値は件数であり、関連先レコードのオブジェクトは返ってこないため、削除対象を追跡したい場合は事前にto_aなどで取得しておく必要があります。
- 参考情報 (あれば)
- 対応 PR: https://github.com/rails/rails/pull/58487
- 関連する過去コミット:
:destroy/:delete_allの古い配列返却例を削除したコミット:9534006c47,02d3a25361
delete_allの戻り値に関する過去の混乱に触れている issue: #14546
#57852 Freeze configuration
マージ日: 2026/8/15 | 作成者: @gmcgibbon
- 概要 (1–2文で)
Rails の Active Record と Action Pack の一部設定値を、アプリケーション初期化後に freeze(必要な Ruby では make_shareable)することで、Ractor 間で安全に参照できるようにした PR です。これにより、初期化後に特定の設定を動的に書き換えることは基本的にできなくなります。
- 変更内容の詳細
2-1. 対象となる設定の「凍結」
Action Pack (ActionDispatch) と Active Record の以下の設定が、after_initialize のタイミングで freeze されるよう Railtie が変更されています。
Action Pack / ActionDispatch
ActionDispatch::ExceptionWrapper.rescue_responsesActionDispatch::ExceptionWrapper.rescue_templatesActionDispatch::ExceptionWrapper.wrapper_exceptionsActionDispatch::ExceptionWrapper.silent_exceptions
Active Record
ActiveRecord.query_transformers
これらはいずれも Hash や Array などミュータブルなオブジェクトであり、従来はアプリケーション起動後でも上書き・変更できていましたが、本 PR によって 初期化完了後は凍結されるようになっています。
実装的には、Railtie の config.after_initialize フック内で、Ruby 4.0 以降の環境では make_shareable、それ以前では freeze に相当する処理を呼び出している形です(PR 説明より)。
※実際のコード上は make_shareable を Ruby 4.0+ で呼ぶようなガードが入る想定。
擬似コードイメージ(Action Pack 側):
# actionpack/lib/action_dispatch/railtie.rb
config.after_initialize do |app|
wrapper = ActionDispatch::ExceptionWrapper
[wrapper.rescue_responses,
wrapper.rescue_templates,
wrapper.wrapper_exceptions,
wrapper.silent_exceptions].each do |config_value|
# Ruby 4.0+ なら make_shareable、それ以前は freeze など
config_value.make_shareable if config_value.respond_to?(:make_shareable)
end
endActive Record 側も同様に、ActiveRecord.query_transformers を after_initialize で共有可能オブジェクトにしています。
2-2. Ractor 対応のための措置
Motivation に明記されているように、この変更の主目的は **「非メイン Ractor からこれらの設定を参照できるようにする」**ことです。
Ruby の Ractor では、Ractor 間で共有できるのは「shareable なオブジェクト」に限られるため、
- アプリケーション起動中に設定を組み立てる
- 起動完了後は不変として扱う
- その代わり Ractor 間で安全に読み取りできる
というライフサイクルに設計を切り替えています。
2-3. テストの追加
railties/test/application/configuration_test.rb にテストが追加され、
「アプリケーション初期化後に対象の設定が凍結(または shareable)になっていること」を検証しています。
- 影響範囲・注意点
3-1. 初期化後の動的変更が基本的にできなくなる
これまで以下のようなコードで「起動後に例外マッピングをいじる」「query_transformers を後から追加する」といったことをしていた場合、FrozenError が発生する可能性があります。
例: 以前はできていたかもしれないコード
Rails.application.config.to_prepare do
# これまでは起動後でも変更できたケースがある
ActionDispatch::ExceptionWrapper.rescue_responses[MyCustomError] = :not_found
end本 PR により、after_initialize 完了時点で rescue_responses が凍結されるため、to_prepare の実行タイミングや、各種 initializer の順序によっては、
- ハッシュ自体が freeze された後に
.[]=しようとしてFrozenError - Ractor 間共有のために
make_shareable済みで変更禁止
となることがあります。
同様に、Active Record 側で以下のようなことをしている場合も注意が必要です。
ActiveRecord.query_transformers << MyCustomTransformer
# => 初期化後に実行すると FrozenError になる可能性対策:
- これらの設定変更は、
config/application.rbやconfig/initializers/*.rbなど、
Rails 初期化完了前 (after_initializeより前) に完了するように移動する必要があります。 - 特に
to_prepareやエンジン/マウント時の late initializer などで変更している場合は確認・修正が必要です。
3-2. ライブラリ・エンジン作者への影響
Rails エンジンやミドルウェア、外部 gem が、これらの設定を「アプリ起動後」に書き換えている場合、それらのライブラリが動かなくなる/警告やエラーが出る可能性があります。
ExceptionWrapper.rescue_responsesに独自エラーをマッピングしているミドルウェアquery_transformersを使ったクエリ書き換え系 gem
などは、initializer の実行タイミングを確認・調整する必要があります。
3-3. Ractor を使うアプリでのメリット
一方で、Ractor を積極的に使うアプリケーションや、今後並列実行を活用したいアプリにとってはメリットがあります。
- 例外ハンドリング設定や query_transformers を、Ractor 間で安全に共有できる
- 「ある Ractor が設定を書き換えてしまい、別の Ractor の挙動が変わる」といったレースコンディションを防止できる
- 設定オブジェクトが不変であることが前提になるため、デバッグや推論がしやすくなる
- 参考情報 (あれば)
- Ruby における Ractor と shareable オブジェクトの仕様:
- Ruby 公式ドキュメント「Ractor」(英語)
- 類似の変更は、今後他の Rails コンポーネントの設定にも広がる可能性があります。
この PR ではActiveRecord.query_transformers以外の AR 設定はまだ対象外ですが、
説明文に「we may need to add more later」とあるため、
今後も「設定は起動時に確定し、その後は不変」という方向性が強まると考えておくと安全です。
#58476 Make Action View subscribers ractor safe
マージ日: 2026/8/14 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Action View のログ購読クラス(LogSubscriber/StructuredEventSubscriber)でRails.rootの扱いを Ractor セーフにするため、Rails.rootを「遅延メモ化」ではなく「初期化時に確定」する形に変更した PRです。これにより、マルチ Ractor 環境でも Action View のログ・イベント購読が安全に動作することを意図しています。
- 変更内容の詳細
背景
- これまで
ActionView::StructuredEventSubscriberやActionView::LogSubscriberでは、Rails.rootを必要になったタイミングでメモ化(@root ||= Rails.rootのような形)して使っていたと考えられます。 - Ractor 環境では、メイン Ractor で生成・変更されるオブジェクトを、後から別 Ractor で「初回代入」したり「再代入」したりするようなパターンは安全でない場合があります。
- そこで、「アプリケーション全体で Ractor セーフ / frozen にされる想定の
Rails.rootを、メイン Ractor の初期化時にインスタンス変数へ確定代入しておく」ように修正しています。
主なコード上の変更点(概念的な説明)
1) ActionView::LogSubscriber の root 取り扱い
変更前(イメージ):
class ActionView::LogSubscriber < ActiveSupport::LogSubscriber
# ...
private
def root
@root ||= Rails.root
end
end変更後(イメージ):
class ActionView::LogSubscriber < ActiveSupport::LogSubscriber
def initialize(*)
super
@root = Rails.root # 初期化時にセット
end
private
attr_reader :root
endポイント:
Rails.rootを「メソッド呼び出し時に毎回/初回だけ取得する」のではなく、「インスタンス生成時に一度だけ確定」する形へ。- これにより、
@rootインスタンス変数は Ractor 間で共有される読み取り専用の frozen オブジェクトを参照することが前提になり、Ractor セーフになります。
2) ActionView::StructuredEventSubscriber の root 取り扱い
構造は LogSubscriber と同様で、StructuredEventSubscriber も Rails.root を初期化時にインスタンス変数へ代入する形に変更されています。
変更前(イメージ):
def root
@root ||= Rails.root
end変更後(イメージ):
def initialize(*)
super
@root = Rails.root
end
private
attr_reader :rootこちらも同じく、遅延メモ化を廃止し、Ractor セーフな初期化時代入に統一しています。
3) actionview/lib/action_view/railtie.rb の変更
Railtie で Action View 関連の subscriber をセットアップする際の初期化処理に手が入っており、
- サブスクライバインスタンス生成のタイミング
- あるいは
Rails.rootへの依存が、Ractor セーフになるような順序
が保証されるようになっています。
ここでは、Rails.application の初期化フェーズ(当然メイン Ractor 上)で subscriber のインスタンスを作り、その時点で @root へ代入されるような流れになっていると考えられます。
4) テストの大幅な更新
log_subscriber_test.rbとstructured_event_subscriber_test.rbのテストが大きく書き換えられています(+112/-150、+164/-205)。- 主な目的は:
- 遅延メモ化ではなく「初期化時セット」に合わせたテスト仕様の変更
- ルートの扱いが Ractor セーフであること、及び変わらず正しいログ/イベント出力になることの確認
- 行数としては減っており、テスト構造が整理・簡潔化されている可能性も高いです。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは主に以下です:
ActionView::LogSubscriberによるログ出力(テンプレートレンダリングのログなど)ActionView::StructuredEventSubscriberによる構造化イベントの購読・出力
- それらが参照している
Rails.rootが:- 以前: 初回アクセス時に
Rails.rootを取得していた - 現在: subscriber インスタンス生成時に取得される
という点が変わります。
- 以前: 初回アクセス時に
何が変わるか / 開発者としての注意点
Rails.rootの「確定タイミング」が早まる- サブスクライバが生成された時点の
Rails.rootが使われ続けます。 - 通常 Rails アプリでは
Rails.rootは起動後に変化しないため、実務上は問題にならないケースがほとんどです。 - 「ランタイムに
Rails.rootを差し替える」といった特殊なことをしている場合は挙動が変わる可能性があります(そのような使い方自体が非典型ですが)。
- サブスクライバが生成された時点の
Ractor 環境での安全性向上
- アプリケーション側で
Rails.rootを frozen / Ractor セーフなオブジェクトとして扱うことが前提になっています。 - Rails の初期化処理(initializer)はメイン Ractor 上で実行されるため、この段階で
Rails.rootが決定・凍結されていれば、後続の Ractor から読まれるだけ、という設計になります。 - Ractor を使った並列実行(Action Cable の一部実装や、将来のマルチ Ractor Web サーバ実装など)で、Action View のログ/イベントまわりが安全に動くようになります。
- アプリケーション側で
ライブラリ・拡張側の考慮
ActionView::LogSubscriberやStructuredEventSubscriberを継承・拡張している独自クラスや gem が、Rails.rootを遅延メモ化に依存していた場合、挙動が変わり得ます。- ただし、そのような依存(「後で root が切り替わること」を前提にしているなど)は設計として想定されていないため、一般的なアプリ/ライブラリには実害は少ないはずです。
- 参考情報 (あれば)
- 類似 PR:
- 説明文でも触れられているとおり、
https://github.com/rails/rails/pull/58467と同様の方針で、他コンポーネントでもRails.rootの扱いを Ractor セーフにする作業の一環です。
- 説明文でも触れられているとおり、
- Ractor セーフティに関する前提:
- Ractor 間で共有するオブジェクトは frozen または特殊なスレッドセーフオブジェクトである必要がある。
- Rails はアプリケーションの「グローバルな設定値」(
Rails.rootや一部設定オブジェクト)を frozen にして共有しやすくする方向で調整が進んでいます。
- この PR ではテスト追加フラグは OFF(
[ ] Tests are added or updated)になっていますが、実際にはテストファイルが大きく修正されているため、「新規テストケース追加」ではなく「既存テストの更新・リファクタリング」として扱われていると見られます。
#58483 Freeze the Controller default_url_options
マージ日: 2026/8/14 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Controller / View で使われるdefault_url_optionsを「破壊的に変更できないように凍結(freeze)する」変更です。
暗黙のミューテーションによる予測しづらいリダイレクト/URL生成の挙動を防ぎ、将来的な Ractor 対応も見据えた安全性向上が目的です。
- 変更内容の詳細(あればサンプルコードも含めて)
対象となる default_url_options
- この PR が扱うのは Controller / View 側 の
default_url_options(ActionController::Base経由で使われるもの)です。 RouteSet(Rails.application.routes.default_url_optionsなど)にある別のdefault_url_optionsは対象外です。
何が問題だったか
Rails ガイドでは、URL のデフォルトパラメータを変えたい場合は default_url_options メソッドをオーバーライドすることが推奨されていますが、実装的には次のようなコードも「動いてしまう」状態でした。
class MyController < ApplicationController
default_url_options[:lang] = "en"
endこうした「ハッシュのミューテーション」は公式にはドキュメント化されておらず、かつ以下のような「超わかりづらい挙動」を引き起こします。
class ApplicationController
def hello_world
redirect_to(root_path)
end
end
class MyController < ApplicationController
default_url_options[:lang] = "en"
end- アプリ起動直後などで
MyControllerがまだ autoload されていない状態で/hello_worldにアクセスすると、root_pathは/になります。 - その後、
MyControllerに初回アクセスなどが発生し autoload されると、上記のdefault_url_options[:lang] = "en"が実行され、共通のdefault_url_optionsが書き換えられる。 - 再度
/hello_worldにアクセスすると、今度はroot_pathが/?lang=enになります。
つまり、クラスの autoload タイミングによって同じアクションから生成される URL が変わるという、とてもデバッグしづらい問題が生じます。
この PR で行われたこと
- Controller / View 側で利用される
default_url_optionsが デフォルトでfreezeされるように変更されました。 - これにより、次のようなコードは(実行時に)エラーになります。
class MyController < ApplicationController
default_url_options[:lang] = "en" # => FrozenError (can't modify frozen Hash)
end- 正しいカスタマイズ方法は従来通り「メソッドオーバーライド」です。
class MyController < ApplicationController
private
def default_url_options
super.merge(lang: "en")
end
endテストの変更内容から読み取れるポイント:
default_url_optionsが freeze されることを前提に、テスト内のdefault_url_options操作が- 直接ミューテーションから
mergeなどの非破壊的操作に 書き換えられている箇所があります。
- Action Mailer / Action Pack / Action View の各テストで、URL ヘルパのデフォルトオプションの扱い方が「ミューテーションを前提にしない形」に整えられています。
- 影響範囲・注意点
影響があるケース
以下のように default_url_options を「ハッシュとして」直接いじっているアプリケーションは、今回の変更で壊れます。
# 悪い例(今回の PR で壊れる)
class ApplicationController < ActionController::Base
default_url_options[:locale] = I18n.default_locale
end
# 悪い例(他の箇所でミューテーションするパターン)
Rails.application.config.to_prepare do
ApplicationController.default_url_options[:locale] = "ja"
endこのようなコードは FrozenError を起こすようになるので、メソッドオーバーライドに書き換える必要があります。
# 良い例
class ApplicationController < ActionController::Base
private
def default_url_options
super.merge(locale: I18n.default_locale)
end
endポイント:
- 今までたまたま動いていただけで、そもそもドキュメント非推奨のやり方だったため、破壊的変更とはいえ「実際にこのパターンを使っているアプリは少ないはず」という判断です。
- ただし、レガシーコードやメンテされていないコードベースでは潜んでいる可能性があります。
Ractor 安全性との関係
- Ractor(並列実行機構)では、オブジェクト共有時にミューテーションが厳しく制限されます。
default_url_optionsを不変(frozen)にすることで、マルチ Ractor 環境でも安全に共有できるようにする狙いがあります。- 今回の PR は、Ractor 対応に向けた足場づくりの一部という位置づけです。
- 参考情報 (あれば)
- Rails ガイド(Action Controller Overview –
default_url_options)
https://github.com/rails/rails/blob/d72a482ea45898909f6be38933cb3db479bbfa4a/guides/source/action_controller_overview.md#L319-L332
→default_url_optionsをオーバーライドして使う公式なやり方が載っています。 - この PR 番号: #58483 「Freeze the Controller
default_url_options」
→ 変更の目的は- 予測しづらい挙動(autoload タイミング依存の URL 変化)の排除
- Ractor 安全性の向上 の2点。
#58482 Restore unshareable_proc_action with ensure in default scoping test
マージ日: 2026/8/14 | 作成者: @ngan
- 概要 (1-2文で)
DefaultScopingTest#test_default_scopes_are_ractor_shareable内でグローバル設定ActiveSupport::Ractors.unshareable_proc_actionを一時的に変更する際、例外発生時にも必ず元に戻るように、ActiveSupport::Ractors.with(内部でensureを使う仕組み)を使うように修正したテスト専用の変更です。これにより、テスト失敗時に Ractors 周りの設定リークが起きて後続テストが巻き添えになる問題を防ぎます。
- 変更内容の詳細
これまでの問題点
元のテストでは、Ractors 周りの挙動を確認するためにグローバル設定を一時的に変更していました:
old = ActiveSupport::Ractors.unshareable_proc_action
ActiveSupport::Ractors.unshareable_proc_action = :raise
model = Class.new(ActiveRecord::Base) do
# ...
default_scope -> { ractor_safe }
# ...
end
ActiveSupport::Ractors.unshareable_proc_action = oldここで問題なのは:
Class.newブロック内のdefault_scope呼び出しは、まさにunshareable_proc_action = :raiseをテストするコードパス- したがって、ここで例外が発生する可能性がある
- 例外が発生すると、最後の
ActiveSupport::Ractors.unshareable_proc_action = oldが実行されず、:raiseが残り続ける - その結果、他のテストでも
unshareable_proc_action = :raiseが効き続けてしまい、本来関係ないテストが失敗する(原因追跡が困難になる)
PR 説明でも「一つの失敗が無関係な失敗を量産して、本当の原因を見えにくくする」と言及されています。
今回の修正内容
このグローバル設定の一時変更を、安全に行うために ActiveSupport::Ractors.with を利用するように変更しました。with は内部で ensure を使って値を必ず元に戻すラッパーです。
概念的には以下のような形になっています(実際のコードは多少異なりますがイメージとして):
ActiveSupport::Ractors.with(unshareable_proc_action: :raise) do
model = Class.new(ActiveRecord::Base) do
# ...
default_scope -> { ractor_safe }
# ...
end
endポイント:
ActiveSupport::Ractors.withブロックの開始時にunshareable_proc_actionを:raiseにセット- ブロック終了時に、例外が発生しても
ensureで必ず元の値に戻す - これにより「例外発生後に設定がリークする」ことを防止
さらに、この with で囲む範囲をモデル定義部分にだけ限定しています。PR 説明の通り:
Wrapping only the model definition keeps the original scoping intact:
capture_sqland both assertions still run with the setting the suite came in with, not with:raise.
つまり:
:raiseを有効にするのはClass.new(ActiveRecord::Base)~default_scopeの評価までに限定- その後に実行される
capture_sqlやassert群は、テストスイートが元々持っていたunshareable_proc_actionの設定のまま動く
この設計により、
- Ractor shareability のテストに必要な箇所だけ、意図的に危険な設定 (
:raise) にして - 検証が終わったらすぐ元の環境に戻す
というメリハリが付けられています。
一貫性の向上
ActiveSupport::Ractors.with や ensure を用いるパターンはすでに以下のテストで使われており、このテストだけが浮いている状態でした:
actionpack/test/dispatch/routing/route_set_test.rbactionpack/test/controller/caching_test.rbactivesupport/test/callbacks_test.rbactivesupport/test/ractors_test.rbactionpack/test/controller/new_base/middleware_test.rb
今回の変更で、Ractors 関連のテスト全体のスタイル・安全性が揃えられました。
- 影響範囲・注意点
影響範囲
- 変更は
activerecord/test/cases/scoping/default_scoping_test.rbのみで、本番コードには一切影響しません。 - テスト実行時の Ractors 設定の扱いがより安全になり、
- 「一つのテスト失敗が原因で、後続の多数のテストが Ractors 設定リークにより巻き添えで失敗する」
といった現象が起きにくくなります。
- 「一つのテスト失敗が原因で、後続の多数のテストが Ractors 設定リークにより巻き添えで失敗する」
- テストの安定性・デバッグ容易性が向上します。
- 変更は
注意点
- Ractors 関連に限らず、「グローバル設定をテスト内で一時的に変更する」場合は、
ensureでの復元- あるいは今回のような
withヘルパー
を使うべき、という Rails テストコードの実質的なガイドラインが強化された形です。
- 同様のパターンが他に残っている場合は、同じように
with/ensureに寄せるとよいです。
- Ractors 関連に限らず、「グローバル設定をテスト内で一時的に変更する」場合は、
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58482
- 当該テスト:
activerecord/test/cases/scoping/default_scoping_test.rb - 関連する既存の
ActiveSupport::Ractors.with利用箇所:actionpack/test/dispatch/routing/route_set_test.rbactionpack/test/controller/caching_test.rbactivesupport/test/callbacks_test.rbactivesupport/test/ractors_test.rbactionpack/test/controller/new_base/middleware_test.rb
#58421 Let the schema readers answer for many tables at once
マージ日: 2026/8/14 | 作成者: @ngan
- 概要 (1-2文で)
このPRは、スキーマダンプ時にテーブルごとにバラバラに発行していたメタデータ系クエリ(インデックス・外部キー・制約など)を「複数テーブルまとめて」取得できるようにし、大規模スキーマでのdb:schema:dump/db:migrateの時間とクエリ数を大幅に削減する変更です。出力されるスキーマファイルの内容は(バグ修正を除き)変わらず、内部の問い合わせ方法とAPIシグネチャが拡張されています。
- 変更内容の詳細
2-1. 複数テーブル版リーダーAPIの追加
以下のメソッドが「単一テーブル」だけでなく「テーブル配列」も受け取れるようになりました。
indexesprimary_keysforeign_keyscheck_constraintsexclusion_constraintsunique_constraints
挙動:
# 既存の単一テーブル形式(今まで通り)
connection.indexes(:users)
# => [ActiveRecord::ConnectionAdapters::IndexDefinition, ...]
# 新しい複数テーブル形式
connection.indexes([:users, :posts])
# => {
# "users" => [IndexDefinition, ...],
# "posts" => [IndexDefinition, ...],
# }ポイント:
- 引数が配列の場合: 戻り値は
"テーブル名文字列" => 定義の配列のハッシュ。 - 引数が単一の場合: 既存と同じく、そのテーブルの配列だけを返す。
- 「すべてのテーブル」という省略形は用意せず、「呼び出し側が明示的にテーブル一覧を渡す」という設計。
- 各メソッドは内部的には「配列対応の実装」に一本化されており、単一テーブル版はそれを薄くラップしているだけ。
サンプル実装イメージ:
def check_constraints(table_name)
result = fetch_check_constraints(Array(table_name).map(&:to_s))
table_name.is_a?(Array) ? result : result[table_name.to_s]
endこれにより、従来の「単一テーブル用と複数テーブル用で別実装」という二重実装がなくなり、メンテしやすくなっています。
2-2. スキーマごとのバッチ読み取り
- 一度の読み取りは1つのスキーマに限定されます。
["users", "posts"]のようにスキーマ非指定なら1回の問い合わせ。["a.posts", "b.posts"]のようにスキーマ付きならスキーマごとに分割され、2回の問い合わせ。
- SQLite はカタログから一括取得する仕組みがないため、外から見るとバッチAPIだが、内部ではテーブルごとにループする実装。
2-3. MySQL: information_schema を用いたバッチ取得とバグ修正
インデックスの取得方法の整理
これまでは環境によって SHOW KEYS FROM ... にフォールバックすることがあり、テーブルごとにクエリが発生していました。PR後は、できるだけ information_schema.statistics から複数テーブルをまとめて読むようになっています。
可用なカラムを環境ごとに切り替える実装:
optional_columns = ""
optional_columns << ", expression AS 'Expression'" if supports_expression_index?
if supports_disabling_indexes?
optional_columns << if mariadb?
", IF(ignored = 'NO', 'YES', 'NO') AS 'Enabled'"
else
", is_visible AS 'Enabled'"
end
endこれにより:
- MariaDB(
IGNOREDはあるがEXPRESSION/IS_VISIBLEはない) - MySQL 8.0.13 より前
といった環境でも、SHOW KEYS に戻らずにバッチ読み取りが可能になります。
検証結果(MariaDB 11.8):
- PR前後でスキーマダンプのバイト列が完全に一致。
- インデックス読み取りクエリ数が「テーブル数分 → 1回」に削減。
ALTER INDEX ... IGNOREDされたインデックスも「無効」として正しく解釈。SHOW KEYS FROMが「テーブルがないときに例外を投げる」ために置かれていたrescue StatementInvalidが不要になり、削除。
テーブルの照合順序(collation)取得のバグ修正
従来:
SHOW TABLE STATUS LIKE '<name>'で1行目を取っていたため、LIKE のワイルドカード (_, %) を含むテーブル名だと別テーブルにマッチして誤った collation を拾うバグがありました。
例:
ab_c(utf8mb4_0900_ai_ci)ab c(utf8mb4_general_ci)
SHOW TABLE STATUS LIKE 'ab_c' は LIKE マッチとして ab c を拾いうるため、ab_c に utf8mb4_general_ci が誤って適用される。
PR後:
information_schema.tablesから読むように変更し、パターンマッチを廃止。- この「潜在バグ」が修正されています。
2-4. PostgreSQL: 制約まわりをまとめて取得
- これまでは、排他制約・ユニーク制約を「インデックス名フィルタ用」「ダンプ用」でテーブルごとに二度問い合わせる構成だったものが、一度のカタログクエリでまとめて取得するよう整理。
- 実装はテーブル配列ベースの共通関数に一本化され、単一テーブル呼び出しもそこを通ります。
2-5. スキーマダンパーの変更
SchemaDumper/ MySQL・PostgreSQL 各アダプタのschema_dumperが、これらの新しい「複数テーブル」APIを利用するように変更。- ダンプ対象テーブルの一覧を事前に集約し、
schema_migrations,ar_internal_metadataSchemaDumper.ignore_tablesに含まれるもの を除外した「実際に出力するテーブル」だけをまとめて問い合わせるように。
2-6. 返り値の仕様
- どのテーブルにも必ずキーが存在します(インデックス等が1つもなくても、空配列を返す)。
- 呼び出し側は「存在しないキー(
nil)」を気にせずに繰り返し処理できる。
- 呼び出し側は「存在しないキー(
- 空配列を渡した場合:
- 「何もしない」ことが仕様です。
- データベース問い合わせも発生しません。
- テーブルが0件のデータベースをダンプするパスでも無駄なクエリが走らないように配慮。
2-7. 挙動変更(互換性に関わる部分)
空のテーブル名の扱い
- 以前は、MySQL の
foreign_keys/primary_keysだけが空文字を弾いてArgumentErrorを投げていた一方で、indexes(nil)はnilを返す- PostgreSQL / SQLite はどのリーダーでも
nilを返す
という不統一な状態でした。
- PR後:
- すべてのリーダー・アダプタで挙動が統一され、「空名で特別に
ArgumentErrorを投げる」という挙動は廃止。 - 空配列を許容して「何もしない」仕様を優先したため、
present?ベースのバリデーションは導入していません。
- すべてのリーダー・アダプタで挙動が統一され、「空名で特別に
- 以前は、MySQL の
PostgreSQL の
primary_keysが、存在しないテーブルで例外を投げなくなる従来:
sql'table_name'::regclassを使っていたため、存在しないテーブル名に対して
ActiveRecord::StatementInvalidが発生。PR後:
pg_classを直接フィルタする方式に変更。- 存在しないテーブルでも
[]を返すようになります。
他のメソッド(
indexes,foreign_keys)は元から「存在しないテーブル →[]」だったので、primary_keysもそれに揃えた形です。テストでも「例外を期待する」前提は存在していませんでした。
- 影響範囲・注意点
3-1. パフォーマンス面の影響
数値的な効果(著者環境の例):
- MySQL(約2,300テーブル)
- ステートメント数: 16,203 → 4,695(約 -71%)
- 壁時計時間: 16.1s → 5.0s
- PostgreSQL(約1,500テーブル)
- ステートメント数: 18,272 → 6,122(約 -66%)
- 壁時計時間: 3.7s → 1.4s
IN (...) によるテーブル名リストの長さも検証済み:
- MySQL(2,300テーブル)
- クエリサイズ: +69 KB
- クエリ時間: 58 ms → 69 ms(+11 ms)
→ 実用上問題ない程度。
- PostgreSQL(同条件)
- クエリサイズ: +23 KB
- クエリ時間: +6 ms 程度。
- 極端なケースとして 200,000 個のテーブル名を含む 5.9 MB クエリでも 0.22 秒程度で完了。
3-2. 既存コードへの互換性
基本的には:
- 既存の単一テーブル呼び出し (
connection.indexes(:users)等) はシグネチャも挙動も変わりません。 - スキーマダンプの出力も、バグ修正を除いてバイトレベルで同一になるよう確認済みです。
注意すべき変更点:
PostgreSQL +
primary_keysで、存在しないテーブルに対する挙動が変わる- 以前:
ActiveRecord::StatementInvalidを救って何かする、というコードがあれば、それはもう呼ばれません。 - 今後:
[]が返ります。 - ライブラリやアプリ側で「存在しないテーブルを検出する」ロジックに依存している場合は注意。
- 以前:
空のテーブル名引数に関するバリデーション削除
- 空文字(あるいは
nil)を渡して例外を期待していたテストがあれば失敗する可能性があります。 - そのような呼び出しは一般的にはバグなので、アプリ側で明示的にバリデーションするほうが望ましいです。
- 空文字(あるいは
MySQL の collation 取得バグ修正
_/%を含むテーブル名を使っていて、かつ以前の「誤った collation」に暗黙依存していた場合、今回正しい collation に変わることで挙動が変化しうる(とはいえ、依存しているケースはほぼ無いと考えられます)。
3-3. 自前アダプタ・プラグインを書いている場合
SchemaStatements を拡張している自前アダプタやプラグインで、以下を行っている場合は確認が必要です。
indexes/foreign_keys/primary_keysなどをオーバーライドしている- あるいは、それらに依存した独自のスキーマダンパーを実装している
対処方針:
- 公式アダプタと同様に、
- 「配列ベースで実テーブル群を読むメソッド(例:
fetch_indexes(table_names))」 - 「それをラップする単一/複数テーブル両対応メソッド(例:
def indexes(table_name) ... end)」 の2層構成にそろえるとよいです。
- 「配列ベースで実テーブル群を読むメソッド(例:
- 新しい API 仕様として
- 配列を受け取ったときは
"テーブル名" => 配列のハッシュを返す - 配列が空のときは DB に問い合わせない
- 存在しないテーブルでも
[]を返す(特に PostgreSQL のprimary_keys) を満たす必要があります。
- 配列を受け取ったときは
- 参考情報
- 変更ファイル例
activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rbactiverecord/lib/active_record/connection_adapters/mysql/schema_statements.rbactiverecord/lib/active_record/connection_adapters/postgresql/schema_statements.rbactiverecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rbactiverecord/lib/active_record/schema_dumper.rb
- テスト:
ActiveRecord::ConnectionAdapters::SchemaStatementsTest- 配列版 API の挙動(対象テーブルだけ読む、単一呼び出しとの一致、スケーリング特性、空配列時の無問い合わせ、スキーマ付き名前のキー)を網羅。
- ミューテーションテストで、スキーマグルーピングや主キー順序、修飾名の扱いなどが十分検証されています。
- 関連するアイデア:
- インデックス取得と同様の「カラム読み取りのバッチ化」も別PRで検討されており、このPRはその基盤の一部となる設計です。
#57973 Add support for the HTTP QUERY method
マージ日: 2026/8/14 | 作成者: @magnogouveia
- 概要 (1–2文で)
- Rails に HTTP の新メソッド QUERY(RFC 10008)が正式サポートとして追加され、
routes・requestオブジェクト・統合テストなど Action Pack 全体で GET 等と同様に扱えるようになりました。 - QUERY は「安全かつ冪等な、ボディ付きの GET 相当メソッド」として扱われ、複雑・大容量な検索条件を URL クエリ文字列ではなくリクエストボディで送る用途を想定しています。
- 変更内容の詳細
2-1. QUERY メソッドの追加と基本挙動
HTTP メソッドとしての登録
ActionDispatch::Requestに QUERY が既知メソッドとして追加HTTP_METHODSに RFC10008 ベースの定義を追加request.query?が新設request.request_methodは"QUERY"を返し、request.request_method_symbolは:queryを返すようになる
例:
# コントローラ内
def index
request.query? # => true (QUERY のとき)
request.request_method # => "QUERY"
request.request_method_symbol # => :query
end2-2. ルーティング (config/routes.rb)
- Journey の
VerbMatchers::VERBSにQUERYを追加 ActionDispatch::Routing::Mapperにqueryヘルパが追加
これにより GET 等と同じ感覚でルートを定義できます。
例:
Rails.application.routes.draw do
# 専用ヘルパ
query "search", to: "search#index"
# match 経由
match "filter", to: "search#filter", via: :query
# 複数メソッドに混在させることも可能
match "items", to: "items#index", via: [:get, :query]
endRails 7 以前では via: :query を書いてもマッチせず 405 相当になっていましたが、この PR により正常にマッチするようになります。
2-3. 統合テスト・コントローラテスト
統合テスト (ActionDispatch::IntegrationTest)
get/postなどと同様にqueryヘルパが追加されました。
例:
class SearchFlowsTest < ActionDispatch::IntegrationTest
test "complex filtering with QUERY" do
query "/search",
params: { filters: { status: "active", tags: ["paid", "priority"] } },
as: :json
assert_response :success
end
endas: :jsonなどの扱いは他メソッドと同様で、Content-Type ベースにボディがparamsにパースされます。
コントローラテスト (ActionController::TestCase)
optionsと同様、専用のショートカットメソッドは追加されていませんが、processがそのまま利用できます。
process(:index, method: "QUERY", params: { filters: { status: "active" } })2-4. Forgery Protection (CSRF 保護) の扱い
ActionController::RequestForgeryProtection 側の挙動変更:
- QUERY は GET/HEAD と同様「CSRF トークン検証の対象外」 として扱われます
- 理由:
- RFC 上 QUERY は safe & idempotent (状態を変更しない前提)
- HTML フォームからは QUERY を送信できない
- QUERY は CORS の safelisted method ではないため、クロスオリジンで送るには必ず preflight が必要(
OPTIONS事前確認) - 結果として CSRF 攻撃面は GET よりもむしろ狭い
コード的には GET/HEAD の扱いに QUERY を加えた形で、独立したテストも追加されています。方針を変えたくなった場合、この部分のみを戻して「QUERY にも CSRF トークンを必須」にすることも容易です。
2-5. パラメータパース
- 変更点: なし
- Rails のボディパースはもともと HTTP メソッドではなく Content-Type に基づいて動作しているため、JSON や
application/x-www-form-urlencoded等のボディを持つ QUERY リクエストも、従来コードのままでparamsに正しく展開されます。
例:
# QUERY /search
# Content-Type: application/json
# Body: { "filters": { "status": "active" } }
def index
params[:filters][:status] # => "active"
end2-6. SSL ミドルウェア / ナビゲーションヘルパ等
actionpack/lib/action_dispatch/middleware/ssl.rb- HTTP->HTTPS へのリダイレクトや安全性チェックの中で QUERY を GET/HEAD などと並べて正しく扱うように更新
ActionView::Helpers::NavigationHelpercurrent_page?等の判定で QUERY を含むような対応(GET に準じた safe method としての扱い)
詳細実装は小さいですが、「GET 相当の安全メソッド」として Rails 内の周辺機能にも自然に統合されています。
2-7. 変更されない/対象外と明示された事項
この PR では あえて扱っていない(今後の検討事項) も明示されています:
Accept-Queryレスポンスヘッダ- QUERY レスポンスに対する「コンテンツベースのキャッシュキー」(RFC 10008 §2.7)
resources/resourceルーティングとの自動統合(resources :itemsで生成されるデフォルト REST ルートに QUERY を足すかどうか、など)- メソッドオーバーライド機構 (
_methodパラメータ) での QUERY サポート - Rack 本体への
Rack::Request#query?追加(現在は Rails 側でのみquery?を定義)
- 影響範囲・注意点
3-1. 既存アプリへの影響
- 後方互換性はほぼ問題無し とされています。
- これまで QUERY リクエストは
ActionController::UnknownHttpMethodを投げて 405 相当で落ちていたため、「すでに QUERY を何か特別にハンドリングしていたコード」は基本的に存在しない想定です。 - この PR によって初めてアプリケーションコードに到達するようになるため、「何かのプロキシや外部クライアントが勝手に QUERY を投げていた」ようなケースがあると挙動が変わる可能性はありますが、一般には稀と考えられます。
- これまで QUERY リクエストは
3-2. Ruby / Rack / アプリケーション側コードへの影響
- ルーティングで
via: :queryを指定しても正常に動作するようになるため、新しく QUERY を使ったエンドポイントを安全に追加できます。 request.query?を利用した分岐が可能になったため、GET と QUERY を明確に分離した処理を記述できます。paramsの扱いは他メソッドと同様なため、アプリケーションコード側の JSON パースや Strong Parameters の記述はほぼそのまま流用できます。
3-3. CSRF / セキュリティ上の考慮点
- デフォルトでは QUERY は CSRF 保護の対象外 です。
- RFC10008 の設計上は「状態を変更しない安全メソッド」という前提ですが、実際のアプリケーション側で QUERY を「副作用のある操作」に使い始めると CSRF 保護の穴になりえます。
- 実装者視点での推奨:
- QUERY は 検索・フィルタリング・クエリ実行など、リソースの状態を変えない読み取り系 API に限定して使う
- もし QUERY を副作用のある操作に使う場合は
- CSRF を独自に検証する
- あるいは Rails の Forgery Protection 設定をカスタマイズして QUERY も検証対象に含めることを検討する
3-4. アプリケーションサーバ (Puma 等) との関係
- この PR は Rails 側のサポートのみ で、アプリケーションサーバの挙動は変えません。
- Puma はデフォルトでは 8 つの標準メソッドのみを許可しており、QUERY は
501 Not Implementedを返します。 - QUERY を使いたい場合、Puma の
supported_http_methodsオプションにQUERYを追加する 等、サーバ側の設定が別途必要です。
- Puma はデフォルトでは 8 つの標準メソッドのみを許可しており、QUERY は
- nginx / Node.js / Spring など、他エコシステムではすでにサポートが進みつつあることが PR で触れられていますが、Rails 単体ではサーバの制約を越えられない点に注意してください。
- 参考情報
- RFC 10008: HTTP QUERY Method
https://www.rfc-editor.org/rfc/rfc10008.html - IANA HTTP Method Registry
https://www.iana.org/assignments/http-methods/http-methods.xhtml - 提案スレッド (rails-core フォーラム)
Proposal: Support for the HTTP QUERY method (RFC 10008)
https://discuss.rubyonrails.org/t/proposal-support-for-the-http-query-method-rfc-10008/91255 - 関連実装・動向:
- Node.js: Core が v21.7.2 以降で QUERY をパース
- nginx: メインラインで QUERY をサポート
- Spring Framework: HTTP QUERY サポートの PR (spring-projects/spring-framework#34993)
この PR により、Rails で「GET 相当だがクエリをボディで送る」API デザイン(複雑な検索条件、GraphQL/JSON-RPC の読み取り等)が公式にサポートされるようになりました。
#58469 Remove "missing" workarounds from Active Record
マージ日: 2026/8/14 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
Active Record のmethod_missingベースの「missing 系ワークアラウンド」が削除され、6.1 時代の Marshal 互換のためだけに残っていたコードが整理されました。これにより、仮想属性やrespond_to?周りのパフォーマンスが向上しつつ、新形式でのMarshal.load互換性はテストで担保されています。
- 変更内容の詳細
背景
- Rails 6.1 では、Active Record オブジェクトの Marshal 形式が今とは異なっており、
- 属性メソッドが定義されていない状態でも Record がロードされうる
- そのため、
method_missing/respond_to_missing?を使った「属性がなくても動かすためのワークアラウンド」が必要だった
- 最新の Rails では 6.1 形式の Marshal サポート自体が削除 されており (#50594, #57382, #58462, #58466)、それに依存したワークアラウンドも不要になった、という整理 PR です。
実際の変更点
1) ActiveRecord::AttributeMethods からの削除
activerecord/lib/active_record/attribute_methods.rb から 36 行削除されています。内容的には以下のようなロジックが消えたと考えられます:
- 「属性メソッドが定義されていない」状態でも、
record.some_attributeをmethod_missingで拾って値を返すrecord.respond_to?(:some_attribute)をrespond_to_missing?で true にする
- Marshal から復元された「古い 6.1 形式の Record」に対して、属性メソッド未定義でもなんとか動かすための処理
これが削除されることで、挙動としてはより「素直な Ruby オブジェクト」に近くなります:
- 定義されていないメソッドは普通に
NoMethodError respond_to?も実際に存在するメソッド・動的属性のみを返す(ワークアラウンド分の“なんちゃって true”が消える)
2) テストの整理と置き換え
activerecord/test/cases/attribute_methods_test.rb:
method_missingベースのワークアラウンド挙動に依存していたテストが削除(56 行減、3 行追加)- 代わりに、実際のユースケース(Marshal 復元)をカバーする「統合的なテスト」側にシフト
activerecord/test/cases/marshal_serialization_test.rb:
- Marshal まわりのテストが 2 行追加
- 目的は「属性メソッドが定義されていない状態で
Marshal.loadしても、今の(6.1 非互換な)フォーマットにおいては正しく動く」ことを確認すること- PR 説明文からすると、元々 method_missing ワークアラウンドで隠れていたバグを、今の形式で再現・検証するテストとして書き直されている
3) パフォーマンス計測と結果
PR に含まれるベンチマークスクリプトの要点:
- 環境:
ruby 4.0.6 (2026-07-14 revision 03b6d3f889) +YJIT +PRISM [arm64-darwin23] - 対象:
Postモデル (sqlite3 in-memory)rubyActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:") ActiveRecord::Base.lease_connection.create_table(:posts) { |t| t.string :title } class Post < ActiveRecord::Base; end row = Post.create!(title: "x") extra = Post.select("id, title, 10 as tenderlove").firstrow.title… 通常の属性メソッドextra.tenderlove… SELECT で追加した「仮想カラム」(10 as tenderlove)
計測ケース:
CASES = {
"title" => -> { row.title }, # 通常の属性メソッド
"tenderlove" => -> { extra.tenderlove }, # 仮想属性(select で追加)
"rt title" => -> { row.respond_to?(:title) }, # respond_to? (属性メソッド)
"rt to_hash" => -> { row.respond_to?(:to_hash) }, # respond_to? (プリフィルタで reject される)
"rt permitted?" => -> { row.respond_to?(:permitted?) },# '?' suffix パターン全スキャン
"rt tenderlove" => -> { extra.respond_to?(:tenderlove) }, # respond_to? (仮想属性)
}before(削除前)と after(削除後)での比較:
title- 20246365.6 i/s vs 20186471.5 i/s
→ 誤差範囲内で同等
- 20246365.6 i/s vs 20186471.5 i/s
tenderlove(仮想属性アクセス)- 2.18M i/s vs 1.30M i/s
→ 約 1.67 倍高速
- 2.18M i/s vs 1.30M i/s
rt title(respond_to?(:title))- 8.83M vs 8.83M i/s
→ 同等
- 8.83M vs 8.83M i/s
rt to_hash(respond_to?(:to_hash))- 2.88M vs 2.49M i/s
→ ~1.16x 高速
- 2.88M vs 2.49M i/s
rt permitted?(?suffix でパターン全スキャン)- 1.44M vs 1.36M i/s
→ ~1.06x 高速
- 1.44M vs 1.36M i/s
rt tenderlove(respond_to?(:tenderlove))- 4.38M vs 3.55M i/s
→ ~1.23x 高速
- 4.38M vs 3.55M i/s
まとめると:
- 通常の属性メソッド (
title) はほぼ変化なし - 仮想属性アクセス (
tenderlove) とrespond_to?系が目に見えて高速化 - 特に、存在しないメソッド/動的属性を
respond_to?で問い合わせるケースが軽くなっている
これは、method_missing ベースのワークアラウンド除去により、respond_to?/respond_to_missing? がシンプルになった効果と考えられます。
- 影響範囲・注意点
- Rails 6.1 形式の Marshal との互換性を前提にしているコードはもはや考慮されない
- すでに 6.1 Marshal フォーマット自体が削除されているため、この PR でさらに「6.1 互換の最後の保険」的なコードがなくなった形です。
- 6.1 時代に serialize した Active Record オブジェクトを「そのまま」今の Rails で復元するような特殊なケースを想定しているライブラリがあれば、注意が必要です(ただしそのサポートは既に他の PR で切られている)。
method_missingに頼った「隠れた動作」が消える
- 以前は、属性メソッドがクラスに定義されていなくても、
- Marshal 復元など特定条件下で
record.some_attributeが「なんとなく」動いていた可能性があります。
- Marshal 復元など特定条件下で
- この PR 以降は、属性メソッドが定義されていない場合は原則として
NoMethodErrorになります。 - 独自に
method_missingをオーバーライドしているモデルや、メタプログラミングで AR のmethod_missingに依存しているコードがある場合は、挙動確認を推奨します。
respond_to?の戻り値がより厳密になる可能性
- ワークアラウンドがなくなったことで、
- 「実在しないが以前は true が返っていた」ようなケースで false になる可能性があります。
respond_to?を使ってメソッド存在チェックを行い、その結果に依存して条件分岐しているコードがあれば、挙動が変わる可能性があります。- 特に仮想属性や動的属性(
select "..., x as tenderlove"など)周り
- 特に仮想属性や動的属性(
- 性能面の影響
- 利点:
- 仮想属性および
respond_to?の一般的な呼び出しが高速になる - メソッド探索の経路がシンプルになり、Ruby 側の最適化(YJIT 等)にも好影響が見込まれる
- 仮想属性および
- 懸念点:
- 特定のワークアラウンドに依存したレアケース以外はデグレはほぼ無い想定だが、大規模アプリでは
respond_to?を多用している部分の挙動を軽く検証しておくと安心
- 特定のワークアラウンドに依存したレアケース以外はデグレはほぼ無い想定だが、大規模アプリでは
- 参考情報 (あれば)
- 関連 Issue/PR:
- #50594: 6.1 Marshal 形式に関する互換性整理 (推測)
- #57382, #58462, #58466: 6.1 フォーマット削除や周辺互換コード削除の前段階
- この PR は、**「古い互換コードを削除して core をスリム化&高速化する流れの仕上げ」**にあたるものです。
- 自分のアプリが影響を受けるかどうかを確認する簡易チェック:
- アプリやライブラリで以下のようなコードを使っているかどうか:
- Rails 6.1 で Marshal.dump した AR オブジェクトを、最新 Rails でそのままロードしている
- AR モデルで
method_missing/respond_to_missing?をオーバーライドし、Active Record 側の実装に強く依存している respond_to?にinclude_private: true/falseを絡めて、存在しないはずのメソッドに対して true を期待している特殊なパターン
- アプリやライブラリで以下のようなコードを使っているかどうか:
これらに当てはまらなければ、多くの場合は「パフォーマンス改善付きの安全なクリーンアップ」と考えて問題ありません。
#57629 Make callbacks ractor-safe
マージ日: 2026/8/13 | 作成者: @gmcgibbon
- 概要 (1-2文で)
このPRは、ActiveSupport::Callbacksを Ractor セーフにするための対応です。具体的には、コールバックチェーンやユーザー定義の Proc を Ractor 共有可能にし、Ractor 環境下でも安全にコールバックが動作するようにしています。
- 変更内容の詳細
コアとなる変更点
2-1. ユーザー定義 Proc を Ractor-shareable にする
- コールバックとして渡される Proc / lambda / block について、設定に応じて Ractor-shareable(共有可能)な形にする処理が追加されています。
- これは、Ruby の Ractor が「shareable でないオブジェクトのクロス-Ractor共有」を禁止しているためで、
- 共有したい Proc は「分離(isolate)」して shareable にする
- 共有不要な場合は従来通り
という挙動を切り替えられるようにしています。
- 「shareable 化するかどうか」のスイッチは、別PR(#57626)で導入される
ActiveSupport::Ractors側の設定を利用しています。
イメージコード(※概念的な例):
# どこかで Ractor 対応を有効化
ActiveSupport::Ractors.share_callbacks = true
class User
include ActiveSupport::Callbacks
define_callbacks :save
set_callback :save, :before, -> { puts "before save" }
end
# 上記コールバック Proc は、Ractor 間で共有可能な形に変換される2-2. __callbacks / CallbackChain を shareable に
- モジュールやクラスに紐づくコールバック定義 (
__callbacks) と、それを表現するCallbackChainオブジェクト自体を Ractor-shareable にできるよう変更しています。 - Ractor 間で同じクラスを使う際に、コールバック定義が Ractor の境界を越えて安全に共有できることが目的です。
- これにより、「Ractor A で定義したコールバック」を「Ractor B でも安全に使う」ことが可能になります。
2-3. set_callbacks が既存チェーンを破壊的変更しないように
- これまで
set_callbacksは既存のコールバックチェーンを 破壊的に変更(mutate) していましたが、 - Ractor では「shareable なオブジェクトは不変であるべき」という考え方になるため、
新しいチェーンインスタンスを構築して差し替える方式に変更されています。 - これにより:
- 一度 Ractor 共有されたコールバックチェーンは、その後の変更で壊されない
- 変更が必要な場合は「新インスタンス」として扱われる
という immutability に近い設計になります。
概念図:
変更前:
CallbackChain(オブジェクトA) を in-place で変更変更後:
CallbackChain(オブジェクトA) はそのまま 新しい CallbackChain(オブジェクトB) を構築して差し替え
2-4. ActiveSupport::Ractors の拡張
activesupport/lib/active_support/ractors.rbに約 27 行の追加があり、- コールバックを shareable にする/しないを制御する設定
- shareable 化のためのヘルパ
などが追加されています(詳細は #57626 側のスイッチを利用)。
2-5. テストの追加・更新
activesupport/test/callbacks_test.rbに大きくテストが追加(+108行)。- コールバックが Ractor-shareable になること
- Ractor 内外でコールバックチェーンを共有して動かせること
set_callbacksが新しいチェーンを返すこと などの振る舞いを確認。
railties/test/application/ractors_test.rbでも Ractor 環境におけるフレームワークの初期化やコールバック関連の挙動を確認するテストが調整されています。- バリデーション(
activemodel)側のテストにも 1 行追加があり、Ractor 対応による副作用(特にコールバックを用いたバリデーション)が問題ないことをチェックしています。
- 影響範囲・注意点
3-1. 主に影響を受ける層
ActiveSupport::Callbacksを直接利用しているコードActiveRecord、ActiveModel::Validations、その他独自の callback ベース DSL
- Ractor を利用している/これから利用しようとしている Rails アプリケーション
3-2. 実務的な影響ポイント
Ractor を使わないアプリ
- 従来とほぼ互換の挙動で、基本的には「何もしなくても」今まで通り動きます。
set_callbacksが破壊的変更をやめて「新チェーンを作る」方式になっているため、
極端にコールバックの内部オブジェクト同一性に依存しているようなメタプログラムがあると影響を受ける可能性はありますが、一般的な使い方では問題になりにくいです。
Ractor を使う/使いたいアプリ
- コールバックに渡す Proc / lambda / block の中で、
- 非 shareable な状態(例: IO オブジェクト、ミューテーブルなグローバル変数、スレッドローカルなど)を直接キャプチャしている場合は注意が必要です。
- shareable 化の処理は
ActiveSupport::Ractorsの設定に依存するため、- Ractor を使う場合はこの設定を有効にする
- 有効にした場合、shareable にならないオブジェクトをキャプチャしているとエラーになる
可能性があります。
- コールバックに渡す Proc / lambda / block の中で、
マルチプロセス/マルチスレッドとの違い
- Ractor はスレッドよりも強い隔離を要求するため、「スレッドでは動いていたコールバック」が、そのままだと「Ractor では shareable でなくてエラー」になることがあります。
- そのため、Ractor 対応を進める際は:
- コールバックで参照するオブジェクトをできるだけイミュータブル・shareable に寄せる
- 必要に応じて Ractor ローカルなオブジェクト生成に切り替える
などのリファクタリングが必要になります。
3-3. パフォーマンス・メモリ
- コールバックチェーンを都度新インスタンスとして構築するようになったことで、「定義変更時のオブジェクト数」は増えますが、
- 定義変更は通常「アプリ起動時・リロード時」にほぼ限定され、
- ランタイム中のコールバック実行速度にはほぼ影響しないはずです。
- 一方で、Ractor 間でクラスやモジュールを共有する場合に、
shareable な構造で統一されることにより、「コピーを量産しない」方向に寄せやすくなります。
- 参考情報 (あれば)
- 当該 PR:
- スイッチ元 PR(Ractor shareable 挙動スイッチの導入):
- Ruby の Ractor / shareable の概念:
- https://docs.ruby-lang.org/ja/latest/doc/ractor_md.html (Ractor 概要)
Ractor.shareable?,Ractor.make_shareableなどの API ドキュメント
このPRにより、Rails のコールバック機構が Ractor フレンドリーな設計に刷新されつつあり、将来的な「Rails × Ractor」に向けた基盤整備の一環と言えます。
#57226 Fix method_missing performance regression for virtual SELECT alias attributes
マージ日: 2026/8/13 | 作成者: @hammadxcm
- 概要 (1–2文で)
Rails 7.2.3 で導入されたActiveRecord::AttributeMethods#method_missingの変更により、仮想カラム(SELECT 句のエイリアスなど)のアクセスが大幅に遅くなっていた問題を、例外発生を避ける形で解消した PR です。public_instance_method呼び出し前にpublic_method_defined?でチェックすることで、スレッドセーフ性を維持しつつパフォーマンスを改善しています。
- 変更内容の詳細
背景と問題点
- Rails 7.2.3 の PR #53890 で、
ActiveRecord::AttributeMethods#method_missing内の実装が変更され、rubyを呼び出すようになりました。self.class.public_instance_method(name) - モデルに定義された「仮想属性」(DB の実カラムではなく、
SELECT ... AS alias_nameのようにクエリで付けたエイリアスや一部の動的メソッド)は、public_instance_method上では本物のインスタンスメソッドとして存在しないケースがあります。 - そのため、仮想属性アクセス時に
public_instance_methodがNameErrorを投げ、それを rescue するパスを毎回通るようになっていました。 - 例外生成 + バックトレース取得は高コストで、1 回あたり ~9μs 程度かかり、その結果仮想属性のアクセスが約 3 倍遅くなるリグレッションが発生していました。
修正方針
- 例外に頼らず、「事前にそのメソッドがクラスに定義されているかどうか」を軽量に判定することで、
NameErrorを発生させないようにしました。 - 具体的には、
public_instance_methodを呼ぶ前にpublic_method_defined?(name)でチェックします。public_method_defined?はメソッドテーブルを単に lookup するだけの、アロケーションゼロの高速な boolean チェックです。define_attribute_methodsがメソッドテーブルに attribute メソッドを定義した直後のテーブルを参照するため、#53890 で狙っていた「スレッドセーフにメソッドキャッシュを使う」という性質は壊れません。
擬似コードイメージ(※実際のコードは行数が少し違う可能性がありますが、ロジックはこれに近いです):
def method_missing(name, *args, &block)
if self.class.public_method_defined?(name)
# ここでのみ public_instance_method を安全に呼ぶ
umethod = self.class.public_instance_method(name)
umethod.bind_call(self, *args, &block)
else
super
end
endポイント:
- 以前の 7.2.3 の状態:
public_instance_method(name)を無条件に呼ぶ → 仮想属性の場合はNameError→ rescue →superなどにフォールバック
- この PR 後:
public_method_defined?(name)がfalseのときはpublic_instance_method自体を呼ばないため、NameErrorの発生経路が完全に消える- 仮想属性アクセス時には「public メソッドなし → 例外なしで別のハンドリングに移行」となる
ベンチマーク結果
50,000 回の属性アクセス (SQLite, 仮想 SELECT エイリアス) での比較:
| 種類 | 7.2.3 (Before) | この PR 適用後 (After) |
|---|---|---|
実カラム .latitude | 0.004s | 0.010s |
仮想属性 .virtual_attr | 0.189s | 0.077s |
| Virtual / Real 比 | 42.1x | 7.9x |
- 仮想属性は 7.2.3 より大きく高速化(例外コストが削除された分)しています。
- 一方で、実カラムのアクセスは 0.004s → 0.010s と、ベンチマーク上はオーバーヘッドが多少増えています。
- これは
public_method_defined?という 1 ステップが増えた(もしくは #53890 全体の影響)ことによるものと考えられますが、仮想属性の極端な遅さに比べると許容範囲という判断です。
- これは
テストとドキュメント
activerecord/CHANGELOG.mdにこのリグレッション修正についてのエントリを追加。- 仮想 SELECT エイリアス属性アクセス専用のテストを追加。
attribute_methods関連の既存テストスイート (137 runs, 624 assertions, 0 failures) がすべてパス。- Finder, Base, Calculations など周辺のテストもパスしており、既存挙動を壊していないことを確認済み。
- 影響範囲・注意点
- 影響範囲:
ActiveRecord::AttributeMethodsを通じてアクセスされる属性全般(実カラム + 仮想属性)のメソッドディスパッチ経路が影響を受けます。- 特に以下のようなケースで恩恵が大きいです:
SELECT some_expression AS virtual_attrで取得した値をrecord.virtual_attrとして頻繁に読む- Arel やカスタムクエリで大量の SELECT エイリアスを使うレポート機能や集計処理
- この PR による注意点:
- 例外駆動の分岐(
NameErrorをトリガーにするロジック)に依存したコードを書いている場合、挙動が変わる可能性がありますが、method_missingの内部実装に依存したコードはそもそも非推奨であり、実務上ほぼ影響はないはずです。 - 実カラムアクセスのオーバーヘッド増加は軽微ですが、高頻度アクセスのホットパスを極端に最適化している場合は、Rails のマイナーバージョンアップ後にパフォーマンス計測をしておくと安心です。
- 例外駆動の分岐(
運用上の示唆:
- 仮想 SELECT エイリアスを多用するアプリケーションでは、Rails 7.2.3 で顕著だった遅さが改善されるため、この修正を含むバージョンへのアップデートを検討する価値があります。
- 独自に
method_missingをオーバーライドしているモデルや Concern がある場合、Rails 側の実装と干渉していないか・パフォーマンス劣化を招いていないかを合わせて確認するとよいです。
- 参考情報 (あれば)
- 元 issue: #57183 — 仮想 SELECT エイリアス属性へのアクセス性能が 7.2.3 で大きく悪化した報告。
- 関連 PR: #53890 —
method_missingにpublic_instance_methodを導入し、スレッドセーフなメソッドキャッシュを確保した PR(今回のリグレッションの起点)。 - この PR: #57226 —
public_method_defined?による事前チェックで、例外ドリブンな分岐を排除し、パフォーマンスとスレッドセーフ性を両立させる修正。
#58467 Make ActiveSupport::LogSubscriber ractor safe
マージ日: 2026/8/13 | 作成者: @gmcgibbon
- 概要 (1-2文で)
ActiveSupport::LogSubscriber の実装を見直し、Ractor 環境でも安全に動作するように「ログ関連のクラスインスタンス変数の初期化タイミングと場所」を変更した PR です。主に@supports_flushとデフォルト logger の設定を「遅延評価」から「メイン Ractor での事前初期化」に切り替えています。
- 変更内容の詳細
2-1. 背景: なぜ変更が必要か
- これまで ActiveSupport::LogSubscriber は、クラスインスタンス変数(例:
@supports_flush)やデフォルト logger の設定を「必要になったときに初めて計算する(遅延評価)」スタイルで行っていました。 - Ractor 環境では、クラスインスタンス変数の遅延初期化(特に複数 Ractor から同時アクセスされ得るもの)はスレッドセーフ・Ractor セーフでない動作を誘発する可能性があります。
- この PR では、それらの値を「メイン Ractor で一度だけ決定し、その後は他の Ractor からは読み取り専用で参照する」という方針に変えています。
2-2. ActiveSupport::LogSubscriber の変更
activesupport/lib/active_support/log_subscriber.rb での主な変更点:
@supports_flushの算出ロジックを、クラスメソッド内での遅延評価から「初期化時に一度だけ計算する」スタイルへ変更。- デフォルトの Rails logger を LogSubscriber 側で lazily 決めるのではなく、「Railtie 側の初期化処理」でセットするように変更。
疑似コードイメージ(実際のコードとは多少異なる可能性がありますが意図の説明用):
# 変更前(イメージ)
class ActiveSupport::LogSubscriber
class << self
def supports_flush?
@supports_flush ||= logger.respond_to?(:flush)
end
def logger
@logger ||= Rails.logger
end
end
end# 変更後(イメージ)
class ActiveSupport::LogSubscriber
class << self
attr_reader :supports_flush
# supports_flush はどこかの初期化タイミング(Railtie など)で
# 一度だけ代入される想定
end
end- 遅延評価 (
||=) でクラスインスタンス変数を書き換えるパターンをやめ、main Ractor で一度だけ決定して以降 Ractor 間で共有する読み取り専用な値として扱う方向に寄せています。
2-3. Railtie 側での初期化
activesupport/lib/active_support/railtie.rb の変更:
- Rails 起動時(main Ractor)に
ActiveSupport::LogSubscriberの logger と@supports_flushをセットする初期化コードを追加。 - 典型的には、以下のような流れになります:
ActiveSupport::LogSubscriber.logger = Rails.loggerActiveSupport::LogSubscriber.instance_variable_set(:@supports_flush, Rails.logger.respond_to?(:flush))
- これにより、application boot 時に logger と flush サポートの有無が確定するため、以降に生成される各 Ractor(例: request handling Ractor)は、その確定済みの値を安全に参照できます。
2-4. テストの追加
activesupport/test/log_subscriber_test.rb の変更:
- Ractor 環境で LogSubscriber が正しく動作することを確認するテストが追加されています。
- 典型的には:
- main Ractor で初期化した logger /
supports_flushが別の Ractor から読めること - Ractor 内での logging が例外なく完了し、flush 対応の有無の判定が壊れていないこと を確認する内容です。
- main Ractor で初期化した logger /
- 影響範囲・注意点
対象範囲
- ActiveSupport::LogSubscriber を使ったログ出力全般(ActiveRecord/ActionController/ActionView などのログサブスクライバを含む)。
- 特に Ractor ベースの並列リクエスト処理や、Ractor を利用するバックグラウンド処理でのログ出力。
動作の意味的な変更
supports_flushおよび logger の決定タイミングが「初めて使ったとき」から「Rails 起動時(main Ractor)」に早まりました。- そのため、
- アプリケーション起動後に logger を差し替えるようなパターン
- 起動後に logger の API(flush メソッドの有無)が変わるような特殊ケース では、
supports_flushが期待通り更新されない可能性があります。
- ただし、通常は Rails 起動時に logger を確定する運用が一般的であり、多くのアプリには影響はほとんどありません。
Ractor を使わない場合
- ログ出力の動作としてはほぼ従来どおりで、パフォーマンスや API に目立った変更はありません。
- 遅延評価が eager 化されただけなので、logger/flush サポート判定が起動時に一度だけ実行されるようになる程度です。
Ractor を使う場合
- request Ractor(マルチ Ractor でリクエストを処理するなど)からでも、
ActiveSupport::LogSubscriberが安全に利用できる前提が強化されます。 - main Ractor 以外から logger や
supports_flushの再代入を行うようなコードは依然として避ける必要があります(Ractor の制約上)。
- request Ractor(マルチ Ractor でリクエストを処理するなど)からでも、
- 参考情報 (あれば)
Ractor とクラスインスタンス変数の関係:
- Ractor ではオブジェクトの共有と書き込みに制限があるため、「複数 Ractor から同じクラスインスタンス変数を遅延初期化する」パターンが問題を起こしやすい。
- この PR は、その典型的な落とし穴を避けるため、初期化は main Ractor で一度だけ・以後は読み取り専用という形へ設計を調整したものです。
実際に Ractor を利用している・検証したい場合:
- Rails を Ractor で動かす PoC などで、この PR 後のバージョンを使うと、ログ周りの Ractor 非互換エラーが減少(または解消)することが期待されます。
#58468 Reduce Pattern#method_name allocations during boot
マージ日: 2026/8/13 | 作成者: @skipkayhil
- 概要 (1–2文で)
ActiveModel::AttributeMethodsでメソッド名テンプレートを生成する際、:attr_nameと:asが同じ場合に余計な文字列・パターンオブジェクトを生成しないようにし、Railsブート時のPattern#method_name関連のオブジェクト割り当てを削減する最適化です。機能的な挙動は変えずに、パフォーマンスとメモリ効率を改善する変更です。
- 変更内容の詳細
※実際の差分は 6 行追加 / 1 行削除のみの小さな修正で、主に条件分岐が追加されています。概要としては以下のようなロジックになっています(イメージの擬似コード):
# 変更前(イメージ)
def define_attribute_method(attr_name, as: attr_name)
pattern = AttributeMethodPattern.new(attr_name: attr_name, as: as)
method_name = pattern.method_name
define_method(method_name) { ... }
endここでは attr_name と as が同じであっても、毎回 AttributeMethodPattern(実際には Pattern オブジェクト)を作り、#method_name を呼んでメソッド名文字列を生成していました。
# 変更後(イメージ)
def define_attribute_method(attr_name, as: attr_name)
if as == attr_name
# デフォルトケース:attr_name と as が同じ
# すでに分かっているメソッド名を直接使う or 共有のテンプレートを使う
method_name = precomputed_or_simple_name_for(attr_name)
else
# 別名をつける場合だけ Pattern を作って method_name を組み立てる
pattern = AttributeMethodPattern.new(attr_name: attr_name, as: as)
method_name = pattern.method_name
end
define_method(method_name) { ... }
endポイント:
- 「ほとんどのケースでは
:attr_nameと:asは同じ」 という前提に基づいて最適化。 - 両者が同じ場合は、これまで毎回行っていた
Patternオブジェクト生成Pattern#method_name呼び出し(文字列生成)
をスキップする。
:asを使ってカスタム名を付ける“少数ケース”でのみ、従来通りテンプレートを組み立てる。
この結果、Rails 起動時に大量に定義される attribute メソッド(foo, foo?, foo= など)について、割り当てオブジェクトの数が減り、GC 負荷やメモリ消費が軽減されます。
- 影響範囲・注意点
- 影響範囲:
ActiveModel::AttributeMethodsを通じて生成される属性メソッド(ActiveRecord のカラムアクセサなどを含む)のメソッド定義処理。- 特に Rails ブート時(モデルロード時)のメソッド定義フェーズのメモリ割り当てに影響。
- 挙動の互換性:
attr_nameとasの値に応じて生成される最終的なメソッド名は従来と同じであるように実装されているため、公開 API の挙動変更はありません。- カスタム
as:を使うケースでも、これまで通りPattern#method_nameを通して生成されるため、互換性が保たれます。
- 注意点:
AttributeMethods周辺で monkey patch している場合、内部実装のわずかな分岐変更に依存していると影響する可能性がありますが、通常のアプリケーションコードには影響しません。- 性能面では Rails を大規模に使うアプリ(カラム数・モデル数が多い)ほど恩恵が大きくなります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58468
- 関連領域:
ActiveModel::AttributeMethods(ActiveRecord や ActiveModel の動的メソッド生成) - 背景知識:
- Rails はモデル属性に対して
name,name=,name?など多数のメソッドを動的に定義しており、このときメソッド名テンプレート(Pattern)を使って命名している。 - ブート時にこれらが一括で定義されるため、この種の小さな割り当て削減が全体の起動時間・メモリ効率に効きやすい。
- Rails はモデル属性に対して
#58466 Restore performance for dynamic attribute read
マージ日: 2026/8/12 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
このPRは、前回の「パターンキャッシュ削除」によって悪化した ActiveModel の「動的属性読み取り(動的 attribute メソッド)」の性能を取り戻すための調整です。to_hashやpermitted?など、Strong Parameters 周辺で多用される処理のパフォーマンス低下を緩和・解消することが目的です。
- 変更内容の詳細
背景
前回コミット([1a59302e642b…][1])で「パターンキャッシュ」が削除され、その副作用として ActiveModel の動的 attribute メソッド解決にかかるコストが増え、to_hash / permitted? などの実行速度が 1.7〜2.7 倍遅くなったというベンチマーク結果が示されています。
to_hash (dd980d49)→to_hash (after): 約 1.7x 遅くなったpermitted? (dd980d49)→permitted? (after): 約 2.7x 遅くなった
このPRは、そのパフォーマンス劣化を解消するために ActiveModel::AttributeMethods 内の実装を見直しています。
どのあたりが変わったか(概念的な説明)
変更ファイルは 1 つのみです:
activemodel/lib/active_model/attribute_methods.rb(+17/-13)
主に以下のような点が調整されていると考えられます(行数とコンテキストから推測しています):
動的メソッド解決のパスを最適化
- ActiveModel は
method_missingとrespond_to_missing?を利用してuser.nameのような attribute アクセサを動的に解決しますが、この解決ロジックの中で、- シグネチャ作成
- メソッド名のパターンマッチ(
_before_type_cast,?,=などのサフィックス) - attribute 定義の検索
といった操作が毎回発生します。
- 前回コミットで除去された「パターンキャッシュ」に依存しない形で、同等レベルの「繰り返しコスト削減」が行われています(ローカル変数の使いまわしや分岐の整理、冗長なチェックの削減など)。
- ActiveModel は
to_hash/permitted?辺りのホットパスを意識した微調整- Strong Parameters (
ActionController::Parameters) は、内部的に ActiveModel の attribute メソッドを多用します。このPRでは、そうしたホットパスで呼ばれる attribute 読み取りの実装ができるだけ軽くなるように分岐や条件チェックが整理されています。 method_missingから実際の attribute 値を取得する処理までのコールチェーンを短くする・あるいは一部ロジックをインライン化する方向の変更になっていると思われます。
- Strong Parameters (
キャッシュの戻し方が「パターンキャッシュとは異なる」
- タイトルが “Restore performance for dynamic attribute read” であり、「pattern cache を戻す」とは明示していないため、以前のパターンキャッシュそのものを復活させたのではなく、
- キャッシュ対象を絞る
- Ruby のメソッドキャッシュ(inline cache)や YJIT が効きやすい形に書き換える
など、より JIT / メソッドキャッシュフレンドリーな形にコードを調整している可能性が高いです。
- タイトルが “Restore performance for dynamic attribute read” であり、「pattern cache を戻す」とは明示していないため、以前のパターンキャッシュそのものを復活させたのではなく、
イメージ的なコード(※雰囲気をつかむための擬似例)
実際のコードとは異なりますが、やっていることはだいたい以下のような最適化イメージです。
変更前(イメージ):
def method_missing(name, *args, &block)
if attribute_method?(name)
# 毎回 name を解析して、getter / setter / predicate を判定
type, attr_name = match_attribute_method?(name)
# ...
else
super
end
end変更後(イメージ):
def method_missing(name, *args, &block)
# 早い段階で簡易チェックして無駄な work を減らす
unless name.is_a?(Symbol)
return super
end
if mapped = fast_attribute_lookup(name)
# 解析済み結果(種別, 元 attribute 名など)をそのまま利用
type, attr_name = mapped
# ...
else
super
end
endこうした変更により、「1回メソッドを呼ぶたびに繰り返し行っていたコストの高い解析処理」を避けられます。
- 影響範囲・注意点
影響範囲
- 主に影響を受けるのは、ActiveModel の attribute メソッドを動的に大量呼び出しするコードです。
ActionController::Parameters#to_hash,#permitted?など Strong Parameters 周り- ActiveModel ベースのフォームオブジェクトや値オブジェクトで属性を多数扱うケース
- 大量のレコードに対して attribute アクセスを繰り返す処理
上記のようなコードは、このPRを含む Rails バージョンにアップデートすることで、**直前のコミットよりもパフォーマンスが改善(以前の水準に近づく/戻る)**ことが期待できます。
注意点
- 公開 API のインターフェイスは変わっておらず、通常のアプリケーションコードから見える挙動に変更はほぼないと考えられます。
- 変更は
ActiveModel::AttributeMethodsの内部実装レベルであり、以下のような場合は影響を受ける可能性があります:AttributeMethodsを独自にincludeして拡張し、method_missing・respond_to_missing?を積極的にオーバーライドしているクラスAttributeMethodsの内部メソッド(private/protected)に依存したメタプログラミングを行っているコード
- そのような高度なメタプログラミングをしている場合は、Rails 更新後に:
- 動的 attribute アクセスが期待どおりに動いているか
respond_to?の結果が変わっていないかmethod_missingが思わぬ順序で呼ばれていないか などをテストで確認することを推奨します。
- 参考情報 (あれば)
このPRがフォローアップしているコミット(パターンキャッシュ削除):
https://github.com/rails/rails/commit/1a59302e642b98e78e88464edf0f3b7741359a92ベンチマークのポイント:
to_hash・permitted?の i/s が前のコミットに対して大きく落ちていたため、それを問題として認識し、今回の PR で改善を試みています。tenderloveベンチ(おそらく別種のケース)はほぼ性能が変わっておらず、今回の劣化・改善対象が特定の動的属性読み取りパスだとわかります。
対応バージョン感:
- ベンチマークは Ruby 4.0.3 + YJIT + PRISM で実行されており、Rails 側も将来の Ruby 4 系との組み合わせでの性能を意識していることがうかがえます。
- Ruby 3.x 系でも恩恵はあるはずですが、特に JIT / PRISM 環境ではより顕著な違いが出る可能性があります。
このPRは、外側の API を変えずに内部のホットパスだけを最適化しているため、「挙動は変えずに Strong Parameters や ActiveModel 属性アクセスの速度を戻したい/改善したい」という観点で、比較的安全かつ恩恵の大きい変更といえます。
#58462 Remove attribute_method_patterns_cache
マージ日: 2026/8/12 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
ActiveModel/ActiveRecord の「動的属性メソッド」判定に使っていたattribute_method_patterns_cacheを撤廃し、代わりに複数のマイクロ最適化でパフォーマンスを補う変更です。キャッシュは性能面では有用だったものの、サイズ無制限かつ Ractor 非対応であるため、将来の並行実行モデルとの整合性を優先して削除されています。
- 変更内容の詳細
2-1. attribute_method_patterns_cache とは何をしていたか
ActiveModel/ActiveRecord では、respond_to? やメソッド呼び出し時に、
name_will_change!name_before_type_castreset_name!
のような「パターンに基づく動的属性メソッド」を判別するため、attribute_method_patterns という正規表現 / パターンのリストを走査していました。
attribute_method_patterns_cache は、次のような「メソッド名 → (属性名, パターン)」という解析結果をキャッシュするための Hash です。
例:
# 以前のイメージ
attribute_method_patterns_cache["something_will_change!"] =
["something", :attribute_will_change!]これにより、同じメソッド名に対する respond_to? / 動的ディスパッチが繰り返されるケースでは、高速化が図られていました。
2-2. キャッシュ削除の理由
PR で挙げられている問題点は 2 つです。
容量制限がない(unbounded)
- 受け取ったメソッド名ごとにエントリが増え、上限やエビクションポリシーがないため、
アプリケーションによってはメモリリーク的な増加を起こしうる。
- 受け取ったメソッド名ごとにエントリが増え、上限やエビクションポリシーがないため、
Ractor で動作しない
- Ractor は「スレッドセーフよりも厳しい並行実行モデル」を要求するため、
共有可変オブジェクト(典型的には通常の Hash)をそのまま利用できません。 - グローバルなキャッシュ Hash は Ractor の世界観と相性が悪く、
Ruby 本体・Rails を Ractor 対応していく上で障害になる。
- Ractor は「スレッドセーフよりも厳しい並行実行モデル」を要求するため、
これらを踏まえ、キャッシュ自体をやめ、Ractor フレンドリーな実装+マイクロ最適化へ舵を切っています。
2-3. 代わりに入ったマイクロ最適化
PR の説明文から読み取れる主な方針は次のとおりです。
対象
パフォーマンス影響が懸念されるのは「モデルが実際には持たないメソッド名」に対するrespond_to?だけです。- 具体的には:
- そもそも ActiveRecord/ActiveModel で定義されていない任意メソッド
- 動的属性(SELECT 句で追加した非カラムの値など)
- パターンにはマッチするが最終的に存在しないメソッド など
- 具体的には:
静的に定義済みのメソッドには影響しない
明示的に定義されたインスタンスメソッド・アクセサなどに対するrespond_to?の挙動・性能はこれまで通りです。プレフィルタなどでの早期リターン強化
PR に添付のベンチマークを見ると、to_hash→ 事前フィルタで弾かれるケースpermitted?→ パターンの走査までは行くが、結果的には false になるケースtenderlove→ 動的属性として true になるケース
をそれぞれ測定しています。
これはつまり、
- 「絶対に属性メソッドになりえない名前」は、なるべく早く判定を終える
- 「サフィックスだけはマッチする」ような名前に対しても、無駄な処理を少なくする
といった形で、キャッシュ無しでもある程度の速度を確保するためのロジック改善が入っている、ということです。
実際のコード変更(activemodel/lib/active_model/attribute_methods.rb)では、respond_to? や match_attribute_method? 周りの実装が見直され、
- 不要なオブジェクト生成を避ける
- 文字列操作・正規表現マッチの順序・回数を減らす
など、細かい最適化が多数入っていると考えられます。
- 影響範囲・注意点
3-1. 性能面の影響
PR 内ベンチマークによれば、動的属性まわりの respond_to? は以下のように遅くなっています(いずれも「キャッシュあり(before)」比)。
to_hash(必ず false, 事前フィルタで弾かれる)- 約 1.62 倍遅い:
- before: 約 3,953,529 i/s
- after: 約 2,447,226 i/s
- 約 1.62 倍遅い:
permitted?(パターンにはマッチするが結果的には false)- 約 2.72 倍遅い:
- before: 約 3,650,315 i/s
- after: 約 1,340,220 i/s
- 約 2.72 倍遅い:
tenderlove(動的属性で true)- 約 1.72 倍遅い:
- before: 約 3,010,374 i/s
- after: 約 1,752,522 i/s
- 約 1.72 倍遅い:
ただし、これらは 非常に小さな単位のベンチマーク であり、現実のアプリケーションでは、
- 同じメソッド名で大量の
respond_to?を連発する - しかもそれが「動的属性 or 存在しないメソッド」
といったパターンが支配的でなければ、体感での差はそこまで大きくはならない可能性があります。
3-2. 影響しやすいコードパターン
次のようなコードは、この変更の影響を受ける余地があります。
- 大量のモデルインスタンスに対して、ループ内で頻繁に
respond_to?を呼んでいる - しかも、そのメソッド名が:
- 動的属性(
select("..., foo as bar")のbarなど)であったり *_will_change!,*_before_type_castや類似の suffix を持つ(=パターンに引っかかる)ものであったり- 最終的には false になるケースが多い
- 動的属性(
例えば:
records.each do |record|
dynamic_name = some_runtime_string
if record.respond_to?(:"#{dynamic_name}_will_change!")
# ...
end
endこのようなコードは、以前はキャッシュの恩恵を受けていましたが、今後は毎回文字列解析・パターン走査が発生します。
3-3. 開発者としての対処・意識ポイント
- 本当に必要な
respond_to?かを見直す- メソッド存在チェックを多用している部分は、ポリモーフィズムやインターフェースの明示(モジュールなど)で再設計できないか検討する価値があります。
- パフォーマンスクリティカルなループでの
respond_to?多用は避ける- どうしても必要な場合は、事前に「扱うメソッド名の集合」を限定しておく・別のフラグで表現するなどして、動的な名前解決コストを抑える工夫が有効です。
- Ractor 対応を優先した変更であることを理解する
- 将来的に Ruby/Rails で Ractor ベースの並列処理を行いたい場合、この種のグローバルキャッシュ削除は避けられない種類の変更であり、そのための布石と考えられます。
動作仕様自体(どの名前が動的属性として解釈されるか)は基本的に変わっていない想定であり、API レベルの非互換はほぼありません。
意識すべきポイントは、おもに「一部シナリオでの respond_to? の速度低下」です。
- 参考情報 (あれば)
変更ファイル:
activemodel/lib/active_model/attribute_methods.rb- 動的属性メソッド判定ロジック・
respond_to?拡張部分の実装見直し(+61/-19)
- 動的属性メソッド判定ロジック・
activerecord/lib/active_record/attribute_methods.rb- ActiveRecord 向けの薄いラッパー側でのわずかな調整(+0/-1)
関連しうる概念・ドキュメント:
- Rails ガイド: Active Record クエリインターフェイス(SELECT で追加したカラム → 動的属性)
- Rails API:
ActiveModel::AttributeMethods - Ruby 3+ の Ractor ドキュメント(共有可変オブジェクトの制限など)
この PR は「動的属性メソッド解決の仕組み」は維持しつつ、「キャッシュを捨てて Ractor 互換な実装 + マイクロ最適化へ切り替えた」と理解しておくと把握しやすいです。
#58321 Add Pacific Time (Canada) and Alberta Time zone mappings
マージ日: 2026/8/12 | 作成者: @Saidbek
- 概要 (1-2文で)
Rails のActiveSupport::TimeZoneに、カナダ西部向けの新しいタイムゾーン名マッピング「Pacific Time (Canada)」「Alberta」が追加され、BC(ブリティッシュコロンビア)とアルバータ州での冬時間のズレを避けられるようになりました。既存の「Pacific Time (US & Canada)」「Mountain Time (US & Canada)」は互換性維持のためそのまま残されています。
- 変更内容の詳細
追加されたマッピング
ActiveSupport::TimeZone のフレンドリ名と IANA タイムゾーンの対応に、以下が追加されています。
"Pacific Time (Canada)"→"America/Vancouver""Alberta"→"America/Edmonton"
既存:
"Pacific Time (US & Canada)"→"America/Los_Angeles"(変更なし)"Mountain Time (US & Canada)"→"America/Denver"(変更なし)
背景として、BC/Alberta は冬時間の運用が米国の Pacific / Mountain とずれるようになっており、従来の Pacific Time (US & Canada) / Mountain Time (US & Canada) を「カナダ西部の代用」として使い続けると、冬期に 1 時間ズレるケースが出るためです。
実際のコード変更イメージ
activesupport/lib/active_support/values/time_zone.rb への追記(概念的なイメージ):
MAPPINGS = {
# 既存
"Pacific Time (US & Canada)" => "America/Los_Angeles",
"Mountain Time (US & Canada)" => "America/Denver",
# 新規
"Pacific Time (Canada)" => "America/Vancouver",
"Alberta" => "America/Edmonton",
# ほか…
}これにより、以下のように利用できます。
Time.find_zone("Pacific Time (Canada)") # => #<ActiveSupport::TimeZone: ... "America/Vancouver">
Time.find_zone("Alberta") # => #<ActiveSupport::TimeZone: ... "America/Edmonton">テストとドキュメント
activesupport/test/time_zone_test.rb
新マッピングが正しくAmerica/Vancouver/America/Edmontonに解決されるテストが追加。activesupport/CHANGELOG.md
新しいフレンドリゾーン名が追加されたことが記載され、振る舞い変更として明示されています。
- 影響範囲・注意点
互換性:
- 既存の
"Pacific Time (US & Canada)"/"Mountain Time (US & Canada)"はそのままなので、既存アプリは即座に壊れません。 - ただし、それらを「BC/Alberta の別名」として使っている場合、冬時間に 1 時間のズレが発生しうる点は変わりません。
- 既存の
推奨対応:
- BC を主対象とするアプリでは、今後は:
Time.find_zone("Pacific Time (Canada)")- あるいは直接
Time.find_zone("America/Vancouver")
- アルバータを主対象とするアプリでは:
Time.find_zone("Alberta")- あるいは
Time.find_zone("America/Edmonton")を使うのが望ましいです。
- 既存コードで
"Pacific Time (US & Canada)"や"Mountain Time (US & Canada)"を BC/Alberta 用に使っている場合は、上記への置き換えを検討してください。
- BC を主対象とするアプリでは、今後は:
IANA tzdb 依存:
- 実際の DST / オフセットのルールは IANA tzdb に依存しており、Rails 側はあくまで「フレンドリ名称 → IANA 名」のマッピングを増やしただけです。
- タイムゾーンデータは OS / tzinfo / ランタイムの更新状況に影響されるため、本番環境で tzdb が最新(少なくとも 2026a / 2026b / 2026c 以降)であることも重要です。
- 参考情報 (あれば)
- IANA tzdb リリース:
- 既存の類似マッピング(Rails 内のパターン例):
"Arizona"→America/Phoenix"Saskatchewan"→America/Regina"Atlantic Time (Canada)"→America/Halifax
これらと同様に、「国・州ごとの運用差が出てきた場合に、より具体的なフレンドリ名称を追加する」という方針に沿った変更です。
#58446 Fix where clauses with column-tuple syntax referencing other tables
マージ日: 2026/8/12 | 作成者: @cgunther
- 概要 (1-2文で)
where句で「カラムのタプル構文(配列構文)」を使う際に、他テーブルをother_table.columnと明示した場合でも、現在のモデルのテーブル名が誤ってプレフィックスされてしまうバグを修正するPRです。これにより、JOIN 先のテーブルカラムを含むタプル条件が正しくSQLに変換されるようになります。
- 変更内容の詳細(あればサンプルコードも含めて)
問題のあった挙動
Active Record の where で、複数カラムをまとめて比較する「タプル構文」を使うときの想定は以下のようなケースです。
# 例: (category_id, author_id) = (1, 2) のような条件
Categorization.where(%i[category_id author_id] => [1, 2])内部的には「カラム名の配列(例: [:category_id, :author_id])」を元に SQL を組み立てますが、このカラム配列は「すべて現在のモデルのテーブルに属するカラムである」と仮定されていました。
そのため、JOIN している別テーブルのカラムを table.column の形式で指定した場合に、誤ったプレフィックスが付与されていました。たとえば:
# categorizations テーブルと authors テーブルを JOIN している状況で
Categorization.joins(:author).where(
%i[category_id authors.name] => [1, "Alice"]
)これを解釈する際、authors.name というキーが「単なるカラム名」とみなされ、categorizations テーブル名が自動で付与されてしまい、結果として以下のような誤った SQL が生成されていました:
... WHERE "categorizations"."category_id" = ?
AND "categorizations"."authors.name" = ?SQLite では、. を含む "authors.name" が「そのままのカラム名」と解釈され、当然そんなカラムは存在しないため、次のようなエラーが発生します。
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: categorizations.authors.name:
SELECT "categorizations".*
FROM "categorizations"
INNER JOIN "authors" ON "authors"."id" = "categorizations"."author_id"
WHERE "categorizations"."category_id" = ?
AND "categorizations"."authors.name" = ?今回の修正内容
対象ファイル:
activerecord/lib/active_record/relation/predicate_builder.rb(+2/-1)activerecord/test/cases/relation/where_test.rb(+12/-0)
実質的な変更は PredicateBuilder 内でのキーの扱いを 1 ステップ追加したものです。
ポイント:
- 「ドット記法(
"table.column"や"authors.name")」で指定されたキーを、そのまま「単一のカラム名文字列」として扱うのではなく、一度「ハッシュ形式」に変換してから処理するようにした。 - これにより、テーブル名とカラム名が正しく分離され、「現在のモデルのテーブル名を無条件にプレフィックスする」ロジックが適用されなくなります。
イメージとしては、以下のような変換が先に行われるようにした、という変更です(擬似コード):
# 変更前のイメージ(簡略化)
# [:category_id, "authors.name"] が "categorizations" のカラムとして扱われてしまう
# 変更後のイメージ(簡略化)
# "authors.name" を見たときに:
# "authors.name" -> { "authors" => "name" } のような形に変換
# その後の処理では適切なテーブル/カラムに分けて扱うテスト (where_test.rb) では、他テーブルのドット記法カラムをタプル構文に含めた場合でも、正しい SQL が生成されることを確認するケースが 1 つ(または複数)追加されています。
- 影響範囲・注意点
影響範囲:
- Active Record の
whereで「カラムの配列(タプル構文)」を使い、かつキーの中にother_table.columnのように「他テーブルのカラム」を直接書くケースが対象です。 - 通常の
where(author: { name: "Alice" })のようなハッシュ構文は元から問題なかったため、影響は限定的です。 - 既にワークアラウンドとして「文字列 SQL を直書きしていた」「Arel を直接使っていた」ようなコードを、より自然な
whereタプル構文に書き直せるようになります。
- Active Record の
後方互換性と注意点:
- これまで「誤ったカラム解釈」に依存していたコードがある可能性は非常に低く、基本的にはバグ修正として安全な変更です。
- 主な変化は「これまで例外が出ていたクエリが、意図通りに動くようになる」というもので、既存の正常系挙動に影響を与える可能性は小さいと考えられます。
- PR 作成者も v8.1 へのバックポートを希望しており、「マイナーなバグ修正」として扱えるレベルの変更と位置付けられています。
- 参考情報 (あれば)
PR 本体:
rails/railsリポジトリの PR #58446: “Fix where clauses with column-tuple syntax referencing other tables”関連コンポーネント:
ActiveRecord::Relation::PredicateBuilderActiveRecord::QueryMethods#where
実務での利用例(期待通り動くようになるパターン):
ruby# authors テーブルに name、categorizations に category_id, author_id がある想定 Categorization .joins(:author) .where( %w[categorizations.category_id authors.name] => [1, "Alice"] )のように、タプルの中に JOIN 先テーブルのカラム (
authors.name) を混在させても正しく SQL が生成されるようになります。
#58461 Allow ffmpeg and ffprobe input arguments to be configured
マージ日: 2026/8/12 | 作成者: @flavorjones
- 概要 (1-2文で)
Active Storage が内部で呼び出している ffmpeg / ffprobe に対し、「入力側の引数」をアプリケーション側から設定できるようにする PR です。これにより、利用可能なデコーダ・プロトコルをホワイトリスト方式で制限するなど、アップロードメディア処理部分のハードニングが可能になります。
- 変更内容の詳細
2-1. 新しい設定項目
Active Storage に以下の 2 つの設定が追加されています。
# ffmpeg に対して -i より前に渡される引数
config.active_storage.video_preview_input_arguments
# ffprobe に対して、パスの直前に渡される引数
config.active_storage.ffprobe_arguments- デフォルト値はいずれも空文字列
""であり、既存アプリがこの設定を変更しなければ、実行コマンドラインは従来と完全に同じ挙動です。 ffprobe_argumentsは音声・動画両方の Analyzer で共通に使われるため、この名前になっています。
2-2. 何が解決されるか
ffmpeg / ffprobe のハードニングによく使われる以下のようなフラグは、入力指定 (-i やファイルパス) より前に置く必要があります。
-codec_whitelist-protocol_whitelist- その他「どの demuxer / decoder / protocol を許可するか」を制限する系のフラグ
これまでは:
config.active_storage.video_preview_argumentsは-iの「後ろ」に挿入されるため、-codec_whitelistなどを意図した位置に指定できなかった。- ffprobe については、そもそも引数をカスタマイズする公式な手段がなかった。
そのため、「入力パスより前にフラグを差し込みたい」場合は:
ActiveStorage::Previewer::VideoPreviewerやActiveStorage::Analyzer::VideoAnalyzer/AudioAnalyzer
といったクラスのプライベートメソッドを丸ごとオーバーライドし、ffmpeg / ffprobe の引数列を自前で再定義するしかありませんでした。
これだと、Rails 本体側で解析処理の引数がマイナー変更されるたびにアプリ側のオーバーライドと差分が生じ、メンテナンス性とセキュリティの両面でリスクがあります。
今回の PR によって:
- ffmpeg の入力引数 (
-iの前 ) - ffprobe の入力引数 ( ファイルパスの前 )
だけをアプリ側で差し込める公式な拡張ポイントができたため、オーバーライドなしでハードニングが可能になります。
2-3. コマンドラインの変化例
例: H.264 + AAC のみ許可する場合
config/application.rb などで:
config.active_storage.video_preview_input_arguments = "-codec_whitelist h264,aac"
config.active_storage.ffprobe_arguments = "-codec_whitelist h264,aac"と設定したときの変化。
変更前 (プレビュー用 ffmpeg):
ffmpeg -i /tmp/blob.mp4 -vf 'select=...' -frames:v 1 -f image2 -変更後:
ffmpeg -codec_whitelist h264,aac -i /tmp/blob.mp4 -vf 'select=...' -frames:v 1 -f image2 -変更前 (ffprobe – Video/AudioAnalyzer 共通):
ffprobe -print_format json -show_streams -show_format -v error /tmp/blob.mp4変更後:
ffprobe -print_format json -show_streams -show_format -v error -codec_whitelist h264,aac /tmp/blob.mp42-4. 実装箇所のポイント
activestorage/lib/active_storage.rbActiveStorageの設定にvideo_preview_input_arguments/ffprobe_argumentsが追加。
engine.rb- 上記設定のデフォルト値を空文字に設定。
video_previewer.rb- ffmpeg コマンド作成時に、
-iの前にvideo_preview_input_argumentsを挿入するよう変更。
- ffmpeg コマンド作成時に、
audio_analyzer.rb/video_analyzer.rb- ffprobe コマンド作成時に、ファイルパスの直前に
ffprobe_argumentsを差し込むよう変更。
- ffprobe コマンド作成時に、ファイルパスの直前に
- テスト (
*_test.rb) が追加され、設定値を変えた際にコマンドラインが期待どおり組み立てられるかが検証されています。 guides/source/configuring.md/guides/source/security.md- 新しい設定と「Media Processing of File Uploads」に関するセキュリティガイドが追加されています。
- 影響範囲・注意点
既存アプリへの影響
- 新設定のデフォルトが空文字列のため、既存のアプリケーションは設定を追加しない限り挙動は一切変わりません。
- つまり、この PR は「設定を有効にしたアプリだけに影響する opt-in 機能」です。
導入メリット
- ffmpeg / ffprobe が扱えるコーデックやプロトコルをホワイトリスト方式で制限できるため、悪意あるメディアファイルを通じた攻撃面を減らすことができます。
- 特に、外部からアップロードされる動画や音声をそのまま ffmpeg / ffprobe に食わせている環境で有効です。
注意点
- 誤ったホワイトリスト指定をすると、正当なファイルであっても解析・サムネイル生成が失敗します。
- 例: 実際には H.265 (HEVC) を含むファイルがアップロードされるが、
-codec_whitelistに h264 しか書いていない、など。
- 例: 実際には H.265 (HEVC) を含むファイルがアップロードされるが、
- ffmpeg / ffprobe のバージョンやビルドオプションによって利用可能なコーデック名・プロトコル名が異なる場合もあるため、本番環境と同一条件での検証が推奨されます。
- これは Rails 自体の脆弱性を修正する PR ではなく、「アプリケーション側のセキュリティポリシーを反映しやすくするための機能追加」です。
ガイドにも明記されている通り、これ単体で全てのメディア関連攻撃を防げるわけではありません。
- 誤ったホワイトリスト指定をすると、正当なファイルであっても解析・サムネイル生成が失敗します。
- 参考情報 (あれば)
追加されたガイド:
guides/source/security.mdの「Media Processing of File Uploads」セクション
→ ffmpeg / ffprobe 利用時に考慮すべき攻撃面と、本 PR で追加された設定項目の使い方の概説。guides/source/configuring.md
→config.active_storage.video_preview_input_arguments/config.active_storage.ffprobe_argumentsの設定方法。
ffmpeg / ffprobe の関連ドキュメント(ホワイトリスト系オプション):
-codec_whitelist-protocol_whitelist
などは、公式ドキュメントやffmpeg -h/ffprobe -hで詳細を確認できます。
#58460 Introduce to_ractor_snapshot / load_ractor_snapshot protocol for notifiers
マージ日: 2026/8/12 | 作成者: @kamipo
- 概要 (1-2文で)
ActiveSupport::Notificationsの notifier に対して、Ractor 間共有用の新しいプロトコルto_ractor_snapshot/load_ractor_snapshotが導入されました。これにより、カスタム notifier を実装する際に内部実装(Fanout の構造)を真似る必要がなくなり、Ractor 共有対応も任意で選べるようになります。
- 変更内容の詳細
背景
- #58060 以降、
ActiveSupport::Notifications.record_subscriptionsはFanoutの内部構造(string_subscribers/other_subscribers)に直接アクセスする実装になっていました。 - その結果、
ActiveSupport::Notifications.notifier = custom_notifierのようにカスタム notifier を差し替える場合でも、Fanoutと同じようなインスタンス変数・構造を持っていないとrecord_subscriptionsで落ちる(NoMethodError / NameError 的な事故が起きる)状態でした。 - この「Fanout の内部構造への依存」をやめ、明示的なプロトコル(インターフェース)に置き換えたのが本 PR です。
新しいプロトコル: to_ractor_snapshot / load_ractor_snapshot
役割
to_ractor_snapshot- Notifier(例:
Fanoutインスタンス)の「Ractor 間共有可能なスナップショット」を生成します。 - Ractor 間で安全にコピーできる形に変換する責務を、各 notifier 実装側に委ねます。
- Notifier(例:
load_ractor_snapshot(snapshot)to_ractor_snapshotが返した snapshot から、実際に notifier の状態を再構築します。
使い方(概念イメージ)
ActiveSupport::Notifications 側は、以下のような「プロトコルに従うかどうか」を respond_to? で判定します。
notifier = ActiveSupport::Notifications.notifier
if notifier.respond_to?(:to_ractor_snapshot)
snapshot = notifier.to_ractor_snapshot
# snapshot は Ractor 間共有可能なオブジェクト
else
# 非対応なら snapshot 機能は使わない(or ゴリっとコピー/共有しない)
end新しい Ractor で復元するときは:
if notifier.respond_to?(:load_ractor_snapshot)
notifier.load_ractor_snapshot(snapshot)
end※ 実際のコードは Rails の内部 API なので多少異なりますが、プロトコルの概念としてはこのような流れです。
Fanout 側の対応
activesupport/lib/active_support/notifications/fanout.rb に、上記プロトコルの実装が追加されています。
Fanout#to_ractor_snapshot- 内部状態(
string_subscribers/other_subscribersなど)から、Ractor 間で共有可能なスナップショットを生成。 - これまで
record_subscriptionsが直接参照していた内部フィールドを、このメソッド経由で取得するようにした、という位置づけです。
- 内部状態(
Fanout#load_ractor_snapshot(snapshot)- 受け取った snapshot を元に、
Fanoutの内部状態を復元します。
- 受け取った snapshot を元に、
これにより:
Notifications.record_subscriptionsはFanoutの内部変数に直接アクセスしない。- 将来
Fanoutの内部実装が変わっても、to_ractor_snapshot/load_ractor_snapshotの契約を守れば外部コードは壊れない。
Notifications.record_subscriptions の変更
- これまで:
Notifications.record_subscriptionsはFanout#string_subscribers/Fanout#other_subscribersに直接アクセスして、Ractor 用の snapshot を作っていました。 - これから:
- まず、
notifier.respond_to?(:to_ractor_snapshot)をチェック。 - 実装されていれば
notifier.to_ractor_snapshotから snapshot を取得。 - なければ snapshot 取得はスキップ/別ルートでの処理(Ractor 共有なし)にフォールバック。
- まず、
この変更で、カスタム notifier に Fanout 互換の内部構造を要求しなくなったのが大きなポイントです。
テストの追加
activesupport/test/notifications_test.rb にテストが多数追加されています(+47行)。
推測されるテスト観点:
- デフォルトの
Fanoutnotifier で、to_ractor_snapshot/load_ractor_snapshot経由の挙動が正しく動くこと。 to_ractor_snapshot/load_ractor_snapshotを実装したカスタム notifier で、Ractor 用処理が問題なく機能すること。- これらを実装していないカスタム notifier を指定しても、クラッシュしないこと(
respond_to?で安全に分岐できていること)。
- 影響範囲・注意点
影響を受ける可能性が高いコード
ActiveSupport::Notifications.notifierをカスタム実装に差し替えているコード- 以前は(壊れたものの)Fanout の内部構造を真似している or していない前提で挙動していました。
- これからは、以下の二通りの選択肢があります:
- Ractor 共有を気にしない場合: 何も実装しなくてもよい(
to_ractor_snapshotもload_ractor_snapshotも不要)。 - Ractor 共有を有効にしたい場合:
to_ractor_snapshot/load_ractor_snapshotを自前で実装する。
- Ractor 共有を気にしない場合: 何も実装しなくてもよい(
Ractor を使って並列実行しており、その中で
ActiveSupport::Notificationsを利用しているコード- デフォルトの
Fanoutを使っている場合、今回の変更により、内部構造に依存しない安定した snapshot 取得ができるようになります。 - カスタム notifier で Ractor 共有をしたいなら、Ractor 闘に安全なデータのみを snapshot に含めるような設計が必要です。
- デフォルトの
カスタム notifier 実装時の指針
カスタム notifier に Ractor 共有対応をさせたい場合、次のような実装が一般的な形になります:
class MyNotifier
# Ractor 共有用の snapshot を生成
def to_ractor_snapshot
# Ractor で共有可能なオブジェクトのみを返す
{
subscribers: @subscribers.map(&:to_h), # 例: イミュータブルなデータに変換
}
end
# snapshot から状態を復元
def load_ractor_snapshot(snapshot)
@subscribers = snapshot[:subscribers].map { |h| Subscriber.from_h(h) }
end
end注意点:
- snapshot に含めるオブジェクトは、Ractor で共有できるものに限定する必要があります(Frozen なオブジェクトや純粋なデータ構造など)。
- 共有したくない(or 共有できない)状態は snapshot に入れない、あるいは復元ロジックで再構築する必要があります。
将来の変更予定との関係
PR 説明にもある通り:
- 本来は
Fanout自体を「共有可能」な実装にし、snapshot 自体を不要にするのが理想(つまり、Ractor 間でFanoutをそのまま共有できる)。 - しかし現在の
FanoutはConcurrent::Mapベースの状態を持っており、そのままでは COW(Copy-On-Write)に適した設計になっていない。 - そのため、今回は snapshot アプローチを維持しつつ、「内部構造に直接アクセスせず、プロトコル越しに扱う」形にとどめている。
将来的にFanoutの COW 対応リライトが入る場合も、このプロトコルを保つことで互換性を維持しやすくなります。
- 参考情報 (あれば)
- 対応する過去 PR:
- #58060:
Notifications.record_subscriptionsがFanoutの内部構造に依存するようになった変更。
- #58060:
- この PR で導入されたプロトコル:
ActiveSupport::Notificationsの notifier 向け「Ractor 共有対応 API」to_ractor_snapshotload_ractor_snapshot
- 関係者(レビュー/議論の文脈を追う場合):
- 作成者: @kamipo
- メンション: @gmcgibbon, @Edouard-chin
#58445 Compile non-strict templates Ractor-locally
マージ日: 2026/8/12 | 作成者: @etiennebarrie
- 概要 (1-2文で)
非 strict ローカルなテンプレートでも Ractor 内から安全にコンパイル・実行できるようにする変更です。併せて、既存 Rails で strict locals テンプレート利用時に例外バックトレースが正しく補正されない不具合も修正されています。
- 変更内容の詳細
背景
- Ractor からビューを描画する場合、テンプレートのコンパイル結果がスレッド/Ractor間で共有されると安全でない(Ractor 間で共有可能なオブジェクト制約に抵触する)ため、テンプレートのキャッシュ戦略を見直す必要があります。
- これまでは主に「strict locals なテンプレート」を前提に Ractor 対応が進んでいましたが、この PR では「non-strict(従来どおりの locals の扱い)テンプレート」も Ractor から使えるようにしています。
UnboundTemplate の内部キャッシュ構造の変更
これまで
ActionView::UnboundTemplate は、おおざっぱにいうと:
@templatesという Hash で、「ローカル変数の正規化済みセット → コンパイル済みテンプレート」の対応をキャッシュ- 排他制御に Mutex
といった形でテンプレートを保持していました。
# イメージ(実際のコードとは異なります)
class UnboundTemplate
def initialize(...)
@templates = {} # { normalized_locals_key => compiled_template }
@lock = Mutex.new
end
def bind_locals(locals)
normalized_key = normalize_locals(locals)
@lock.synchronize do
@templates[normalized_key] ||= compile_template_with(locals)
end
end
endこの PR での変更点
非 strict テンプレート + frozen な UnboundTemplate の場合:
@templatesではなく、Ractor ローカルなConcurrent::Mapを使ってキャッシュします。- Ractor 毎に別の Map を持つので、Ractor 間でオブジェクトを共有しない設計になります。
- さらにその内側で、競合を避けるためにもう一層
Concurrent::Mapを利用して、従来の Mutex 相当の排他を行います。 - これにより「同じ
UnboundTemplateでも、各 Ractor ごとに独立したテンプレートキャッシュを持つ」構造になります。
strict locals テンプレート について:
- strict locals 用には
@strict_locals_templateというインスタンス変数にコンパイル済みテンプレートを持つように整理されており、その読み出しパスが明示化されました。 - 従来の Hash (
@templates) のデフォルト値として strict テンプレートを持っていたため、@templates.valuesに strict テンプレートが含まれず、後述のバックトレース補正に支障が出ていました。この構造を改めたことで不具合が解消されています。
- strict locals 用には
エイリアシング(locals 正規化の alias)を維持
- 非 strict locals テンプレートで、「正規化前の異なる locals 指定」が「同一の正規化済 locals セット」にマッピングされる場合は、同じコンパイル済みテンプレートを再利用する、というエイリアシング挙動があります。
- これは locals 正規化を「高速パスでは行わない」ための工夫でもあり、本 PR でもこの挙動を維持しています。
- つまり、Ractor ローカルな Map を使いつつも、「locals の正規化キー → テンプレート」というキャッシュの構造・意味は変えないように設計されています。
frozen?をトリガーにした挙動切り替えUnboundTemplateが#freezeされたかどうかで、
「Ractor ローカルキャッシュを使うか(frozen 時)」
「従来どおりの単一キャッシュを使うか(非 frozen 時)」
を切り替えるようになっています。- Ractor から利用する前提だと、通常はアプリケーション起動時にテンプレートが凍結されるため、Ractor 用のパスが有効になります。
バックトレース補正のバグ修正
ActionDispatch::ExceptionWrapper#build_backtrace は、例外が ERB テンプレート内で発生した場合に、
- 実際の
.html.erbファイルの行番号に対応するように - Ruby コードに変換されたあとの行番号を補正する
といった処理を行います。
この処理では、UnboundTemplate が保持している「コンパイル済みテンプレート群」から情報を取得しますが、
- strict locals のテンプレートを
「@templatesHash のデフォルト値」として持っていたため @templates.valuesに strict テンプレートが含まれず- 結果として、strict locals テンプレートに対するバックトレース補正が正しく行われない
という問題がありました。
この PR の「3つ目の commit」で、strict locals テンプレートがきちんと列挙されるようにデータ構造が修正され、build_backtrace による補正が期待どおり働くようになっています。
テスト・周辺変更
actionview/test/template/file_system_resolver_test.rb- ファイルシステム経由でテンプレートを解決する際に、Ractor まわりの挙動・キャッシュ挙動を検証するテストが多数追加されています(+80/-11)。
railties/test/application/ractors_test.rb- Rails アプリケーションから Ractor を使ってビューをレンダリングするケースをカバーするテストが追加・拡張されています。
actionview/lib/action_view/base.rb,actionview/lib/action_view/template/handlers/erb.rb,railties/lib/rails/application.rb- Ractor 対応・strict locals 対応に合わせて、それぞれ 1 行程度の微修正が入っています(主にフラグ・フックの追加や呼び出し先の切り替えレベル)。
- 影響範囲・注意点
Ractor を使わないアプリケーションへの影響
- パフォーマンス面:
- 非 strict locals テンプレートに対して:
- 追加されるのは「
frozen?チェック」が 1 回程度。
- 追加されるのは「
- strict locals テンプレートに対して:
- 「
@strict_locals_templateがあるかどうかの ivar チェック」が 1 回。 - Hash の
[]アクセス + デフォルト値 から、単純な ivar 読み出しへ置き換えられているので、むしろコードパスがシンプルになっています。
- 「
- いずれもオーバーヘッドは非常に小さく、実用上ほぼ影響はないと考えてよいレベルです。
- 非 strict locals テンプレートに対して:
- 機能面:
- バックトレース補正が改善され、strict locals テンプレートで発生したエラーのソース位置がより正確になります。
- 既存コードの挙動を変えるような仕様変更は基本的にありません。
Ractor を使うアプリケーションへの影響
- 非 strict locals テンプレートを Ractor から使うことが「技術的に可能」になります。
- ただし PR の説明にもある通り、非 strict locals テンプレートを Ractor から使うこと自体は推奨されていません。
- 理由としては、locals の扱いが暗黙的であることにより、将来的な Ractor セーフティの維持が難しくなったり、予期しない共有や依存が紛れ込みやすいためです。
- それでも、
- 既存テンプレートをすべて strict locals 化するのが困難なアプリ
- 段階的に strict 化していきたいアプリ にとっては、「まず Ractor 対応を進めるための移行ステップ」として有用です。
注意点
- Ractor からレンダリングするテンプレートは、
できるだけ strict locals 化し、「Ractor セーフな依存関係」にしておくことが望ましいです。 - 非 strict テンプレートでも動くようになりましたが、
- グローバル状態
- スレッドローカル / Fiber ローカル
- 共有可能でないオブジェクト 等への依存があると、Ractor 内からの利用で思わぬ問題を引き起こす可能性があります。
- 参考情報 (あれば)
- この PR の前提となる PR: #58396
- strict locals を中心にした Ractor 対応の基盤部分がここで導入されており、本 PR はそのフォローアップとして「non-strict テンプレートへの対応」「strict locals 周りのバグ修正」を行っています。
- 関連クラス・メソッド:
ActionView::UnboundTemplateActionView::Template::Handlers::ERBActionDispatch::ExceptionWrapper#build_backtrace
- Ractor の仕様や制約を再確認したい場合は、公式ドキュメント(Ruby 3.x の Ractor セクション)および Rails ガイドの並行処理関連ドキュメントを見ると理解が進みます。
#58403 Move duplicated conditional_executor up to Rails::Command::Base
マージ日: 2026/8/12 | 作成者: @seuros
- 概要 (1-2文で)
Rails::Command::ConsoleCommandとRails::Command::RunnerCommandに重複していたconditional_executorの実装が、共通の親クラスであるRails::Command::Baseに集約されました。これにより、同様の挙動を持つコマンド間でロジックを共有できるようになり、重複コードが削減されています。
- 変更内容の詳細
※ PR本文に説明がないため、Rails 既存実装の慣例と変更された行数・ファイル構成から推測を含みます。
どんなリファクタリングか
- 変更前:
Rails::Command::ConsoleCommandにconditional_executorメソッド (もしくは同等のロジック) が定義されていたRails::Command::RunnerCommandにもほぼ同一のconditional_executorが定義されていた
- 変更後:
- この重複コードが削除され (
+0/-8が 2 ファイルで発生) - 共通のスーパークラスである
Rails::Command::Baseにconditional_executorが新たに追加されました (+9/-0)
- この重複コードが削除され (
イメージとしては、以下のようなリファクタリングが行われています:
# 変更前 (イメージ)
module Rails
module Command
class ConsoleCommand < Base
private
def conditional_executor
# ここに console, runner でほぼ同じロジックがあった
end
end
end
end
module Rails
module Command
class RunnerCommand < Base
private
def conditional_executor
# ConsoleCommand とほぼ同じ
end
end
end
end# 変更後 (イメージ)
module Rails
module Command
class Base
private
def conditional_executor
# ConsoleCommand / RunnerCommand 共通のロジック
end
end
end
end
module Rails
module Command
class ConsoleCommand < Base
# conditional_executor は Base から継承
end
end
end
module Rails
module Command
class RunnerCommand < Base
# 同上
end
end
endconditional_executor が担っている可能性が高い役割
Rails のコマンド実装で「conditional ~」という名前は、環境やオプションに応じて実行器(executor)を切り替えるような用途でよく使われます。
この PR で登場する conditional_executor も、例えば以下のような責務を持っていると考えられます(あくまで代表的なパターンの推測です):
- 開発環境・本番環境・テスト環境などによって異なる実行戦略を選ぶ
- Spring や Bootsnap、マルチスレッド/マルチプロセス実行の有無に応じて実行方法を変える
--environmentや--no-colorなどの CLI オプションに応じて実行コンテキストを切り替える
共通の Base に集約されたことで:
- Console と Runner どちらのコマンドも、同じ条件判定・同じ実行戦略を共有する
- 将来ほかのコマンドでも同じ仕組みを使いたくなった場合、
Baseだけ修正すればよい
といったメリットがあります。
- 影響範囲・注意点
影響範囲
- 直接的な影響を受けるのは、主に以下のクラスです:
Rails::Command::ConsoleCommandRails::Command::RunnerCommand
- 実装はリファクタリング(共通化)であり、外部 API やコマンドの挙動を変えることを目的とした変更ではないと考えられます。
- そのため、通常の
rails consoleやrails runnerの利用者には、動作上の変化は基本的にありません。
- そのため、通常の
注意点(カスタマイズしている場合)
以下のようなケースでは影響を受ける可能性があります:
独自に Rails コマンドクラスを定義している場合
Rails::Command::Baseを継承した自作コマンドで、conditional_executorと同名メソッドを定義していた場合:- メソッド解決順序的には自クラスの定義が優先されるため、即座の壊れ方はしませんが、
- 名前の衝突により、将来の Rails 側の挙動変更と干渉するリスクがあります。
- 同名メソッドを避けるか、もし意図的に上書きしているなら、
super呼び出しや挙動の差分を意識して実装する必要があります。
Rails::Command::ConsoleCommand/RunnerCommandを monkey patch している場合- 以前、これらのクラスの
conditional_executorを直接上書きしていた場合、- メソッドの定義場所が
Baseに移ったことで、パッチが想定どおりに効かなくなる可能性があります。
- メソッドの定義場所が
- Rails のバージョンアップ時に、
method_sourceなどで定義場所を再確認したほうが安全です。
- 以前、これらのクラスの
テストや計測で内部メソッドを直接呼び出している場合
- テストコードが
Rails::Command::ConsoleCommand#conditional_executorをsend/public_sendなどで直接叩いている場合:- 定義場所の変更自体は Ruby 的には透過的ですが、将来的なリファクタリングで可視性や挙動が変わる可能性があるため、
- 内部メソッドへの依存を最小限にするほうがメンテナンス性は高くなります。
- テストコードが
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58403
- この PR は重複コードの整理・共通化が主目的であり、機能追加や仕様変更ではなく、内部実装のクリーンアップに分類される変更です。
- 同系統のリファクタリングが続く場合、
Rails::Command::Baseが「Rails CLI コマンドの共通基盤」としてより強く位置づけられていく可能性があり、自作コマンドやエクステンションを実装する際にここを起点に読むと理解しやすくなります。
#58436 Introduce builder method for alter table definitions
マージ日: 2026/8/12 | 作成者: @adrianna-chang-shopify
- 概要 (1-2文で)
Rails のスキーマ定義まわりに、テーブル変更用の新しいビルダーbuild_alter_table_definitionが追加され、既存のcreate_alter_table呼び出しに依存せず「テーブル変更定義オブジェクト(AlterTable)」を組み立てられるようになりました。これにより、create_tableと同様のスタイルで「変更予定内容」をオブジェクトとして扱えるようになり、DDL 生成や外部マイグレーション管理との連携がしやすくなります。
- 変更内容の詳細
新しく追加されたメソッド: build_alter_table_definition
ActiveRecord::ConnectionAdapters::SchemaStatements に、テーブル変更定義を構築するためのビルダーメソッドが追加されています。
ポイント:
build_alter_table_definition(table_name)がAlterTableオブジェクトを返す- 返却される
AlterTableは「そのテーブルに対して行いたい変更」を表現するオブジェクト - これまで内部的に使われていた
create_alter_table(table_name)を直接呼ばずに、ビルダー経由で変更定義を構築できるようにするのが目的 build_create_table_definitionと同様の設計だが、build_alter_table_definitionは:nodoc:扱い(内部API的な位置付け)
イメージとしては、build_create_table_definition でやっていることの「alter table 版」です。既存のコードでは、ALTER TABLE 用の DDL を用意するときに、アダプタ内部メソッド connection.send(:create_alter_table, table_name) のように send でプライベートメソッドを叩く必要がありましたが、その必要がなくなります。
既存アダプタの追従
以下のファイルで ALTER TABLE 周りの実装が build_alter_table_definition に追従する形で変更されています。
abstract_mysql_adapter.rbpostgresql/schema_statements.rb
主な変更点:
- それぞれのアダプタで、テーブル変更定義を組み立てる際に、新ビルダーを利用するような形に整理(行数としては +3/-3 や +6/-6 程度の小変更)
- ロジックの大枠は変えず、「AlterTable オブジェクトをビルダーで組み立て、そこから DDL を生成する」という一貫したパターンにリファクタリング
テストの追加・更新
activerecord/test/cases/migration/schema_definitions_test.rbにテストが追加(+18/-2)build_alter_table_definitionが正しくAlterTableオブジェクトを生成し、期待通りの変更情報を保持しているかを検証
abstract_mysql_adapter/connection_test.rbも 1 行レベルで更新され、アダプタの振る舞い整合性を確認
コード例(イメージ・擬似的な使用例として):
# connection: ActiveRecord::Base.connection
alter = connection.build_alter_table_definition("users")
alter.add_column("age", :integer)
alter.add_index(["email"], unique: true)
# 生成された AlterTable オブジェクトを元に DDL を組み立てて
# 外部の「マイグレーション管理サービス」に送る、といった利用が想定されている
ddl = some_custom_ddl_builder.to_sql(alter)
send_to_central_migrations_manager(ddl)実際の外部APIはこの PR には含まれていませんが、「AlterTable をビルダーで構築 → その情報から DDL を生成」という流れを実現するための基盤部分が今回の変更です。
- 影響範囲・注意点
- Rails アプリ開発者(通常のマイグレーションを書く側)への直接的な変更はほぼありません。
- 既存の
change_table/add_column/remove_columnなどの DSL はそのまま動く想定です。
- 既存の
- 影響がある可能性があるのは、以下のようなケースです:
- アダプタ内部API(
create_alter_tableなど)をsendで直接叩いて独自に DDL を生成していたような高度なメタプログラミング/ツール - スキーマ定義ビルダー (
build_create_table_definitionなど) を使って独自にマイグレーション管理・DDL 生成をしているツールやライブラリ
- アダプタ内部API(
注意点:
build_alter_table_definitionは:nodoc:のため、公式的には「内部API」扱いであり、将来的な変更の余地があります。- とはいえ、
build_create_table_definitionと対になる概念として導入されているため、スキーマ生成系の周辺を拡張・統合する場合には、今後こちらを利用した実装が「正攻法」になっていく可能性があります。 - MySQL・PostgreSQL のアダプタ実装がリファクタリングされていますが、テストも更新されており、挙動としての互換性は維持される想定です。
- 参考情報 (あれば)
- 元 PR: https://github.com/rails/rails/pull/58436
- 背景となる PR(前提変更): https://github.com/rails/rails/pull/58264
- こちらでスキーマ定義ビルダーまわりの整理や、中央集約的なマイグレーション管理のための基盤整備が進められており、本 PR はその follow-up です。
#57846 Fix normalizes on an enum attribute raises ArgumentError instead of normalizing before enum casting
マージ日: 2026/8/12 | 作成者: @joaoGabriel55
- 概要 (1-2文で)
normalizesとenumを同じ属性に併用した際、正規化前の値に対してenumが先にバリデーションしてArgumentErrorを投げていた問題を修正した PR です。正規化を「enum の検証より先」に行うようにし、ユーザー入力を正しくサニタイズしてから enum キャスト・検証できるようになりました。
- 変更内容の詳細
何が問題だったか
次のようなモデルを考えます:
class Order < ApplicationRecord
enum :status, { pending: "pending", confirmed: "confirmed" }
normalizes :status, with: ->(v) { v.strip.downcase }
end
Order.new(status: " Pending ")
# 以前の挙動:
# => ArgumentError: ' Pending ' is not a valid statusnormalizes は status に代入された値を strip.downcase で "pending" にしてくれるはずですが、実際には " Pending " のまま enum 側のバリデーションに渡されてしまい、「有効な status ではない」として ArgumentError が発生していました。
背景にある実装構造は以下のようなものです。
enumもnormalizesも、Active Model の「型(type)」をラップするデコレータとして実装されているenum+normalizesを使うと、型のスタックはざっくりNormalizedValueType → EnumType → もとの型という構造になるActiveModel::Attribute#with_value_from_userは、ユーザー入力に対してtype.assert_valid_value(value)を「生の値」で呼び出す- その後に
castして内部表現に変換する
という順で処理する
NormalizedValueType は cast はオーバーライドして正規化を挟んでいましたが、assert_valid_value はオーバーライドしておらず、そのまま内側の EnumType に「生の値」を渡していました。その結果:
" Pending "が正規化される前にEnumTypeに渡るEnumTypeは" Pending "を知らないのでArgumentErrorを投げる
という不整合な流れになっていました。
今回の修正内容
ActiveModel::Attributes::Normalization::NormalizedValueType に assert_valid_value のオーバーライドを追加し、「正規化済みの値」に対して下層の型 (cast_type) のバリデーションを行うように変更しました。
擬似コードで表すと、PR で追加されたのは以下のようなロジックです:
def assert_valid_value(value)
cast_type.assert_valid_value(cast(value))
endこれにより、実際の流れはこうなります:
- ユーザー入力:
" Pending " NormalizedValueType#assert_valid_valueが呼ばれるcast(value)により"pending"に正規化される- 内側の
EnumType.assert_valid_value("pending")が呼ばれる "pending"は有効な enum 値なのでエラーにならない
一方で、本当に不正な値は正規化後にも検出されます:
Order.new(status: " bogus ")
# 正規化後は "bogus"
# => ArgumentError: 'bogus' is not a valid statuscast で保存される値と、assert_valid_value で検証される値が一致するようになった、というのが設計上のポイントです。
テストとドキュメント
activerecord/test/cases/normalized_attribute_test.rbに回帰テストを追加- 正規化によって enum にマッチするようになるケース
- 正規化してもなお不正な値のケース
の両方を確認
activemodel/CHANGELOG.mdに挙動変更として追記
(バグ修正だが、enum + normalizes の組み合わせの実行時挙動がユーザーにとって意味のある形で変わるため)
- 影響範囲・注意点
- 影響を受けるのは「normalizes が適用されている属性」だけです。
normalizesを使っていない属性や、enum 単体の挙動には変更はありません。 - 既存で
enum + normalizesを併用していた場合:- これまで「正規化されるはずだったが、
ArgumentErrorで落ちていた」ケースが、期待どおり正規化されてから enum として受け入れられるようになります。 - もしアプリケーション側で、この
ArgumentErrorを rescue したり、メッセージに依存したロジックを書いていた場合は、その挙動が変わる可能性があります(ただし、そのようなコードはもともと「正しく正規化されていない」というバグに依存していたことになります)。
- これまで「正規化されるはずだったが、
- 入力バリデーションの観点:
" Pending "→"pending"のような「表記ゆれ」の吸収が enum と安全に組み合わせて行えるようになります。- 利用者は、
normalizes内で「期待しない値を別の合法値にマップする」ようなロジックを書かない限り、異常系の検出はこれまで通りArgumentErrorで行えます。
- 参考情報 (あれば)
- 対応 Issue: https://github.com/rails/rails/issues/57828
- 変更ファイル:
activemodel/lib/active_model/attributes/normalization.rbactiverecord/test/cases/normalized_attribute_test.rbactivemodel/CHANGELOG.md
- 関連する Rails 機能:
- Active Model Normalization (
normalizes) - Active Record
enum ActiveModel::Attribute#with_value_from_userの型検証フロー
- Active Model Normalization (
#58455 Show bin/console startup banner based on IRB
マージ日: 2026/8/12 | 作成者: @byroot
- 概要 (1-2文で)
bin/console実行時に表示される起動バナーを、Rails 独自実装ではなく IRB が持つバナー表示機構に合わせて出すように整理し、Rails 側のバナー制御ロジックを IRB ベースに再設計した PR です。これにより、IRB の設定やバージョンに応じた一貫したバナー表示が行われ、テストもそれに合わせて拡充されています。
- 変更内容の詳細
※元 PR (#58393) の再オープン版であり、方針はそちらを引き継いでいます。
2-1. bin/console 起動バナーの扱いを IRB ベースに変更
railties/lib/rails/commands/console/console_command.rb とrailties/lib/rails/commands/console/irb_console.rb の変更が中心です。
主なポイント:
- これまで Rails が独自にバナー(「Loading development environment (Rails x.x.x)」のような行)を表示する処理を持っていましたが、それを IRB の起動時処理に合わせて出すように整理
- IRB 側のオプションや設定(例:
IRB.conf[:USE_COLOR]やバナーの有効/無効)を尊重して表示タイミングや出し方を決定 - Rails のコンソール起動処理 (
Rails::Console/Rails::Console::IRBConsoleまわり) に、起動バナー表示に関するロジック・フックを追加
概念的には次のようなフローに整理されています(擬似コード):
# Rails::Console::IRBConsole の中 (イメージ)
def start
# IRB の設定・context 準備
setup_irb
# IRB が起動するタイミングに合わせて、Rails バナーを表示
show_startup_banner_if_needed
# IRB メインループ開始
IRB.start
endまた、環境情報や Rails バージョンなどを含んだバナー文字列を専用メソッドで構築するような形になっている可能性が高いです:
def startup_banner
"Loading #{Rails.env} environment (Rails #{Rails.version})"
endこれを IRB のバナー表示ロジックと整合が取れる位置で出力することで、
- IRB が管理する標準のバナーと二重表示しない
- IRB の設定でバナーを抑制したときに Rails 側も無駄に出さない
といった挙動に寄せています。
2-2. コンソールコマンドの振る舞い整理
rails/commands/console/console_command.rb 側では:
bin/rails console実行時に、どのコンソールアダプタ(IRB / Pry など)を使うか判断する処理の中で、バナー表示の判定も行うように整理- 一部オプション(
--sandboxや--environmentなど)との組み合わせ時のメッセージ出し分けを、IRB ベースのバナー表示に合わせて調整
例として、従来:
$ bin/rails console
Loading development environment (Rails 7.2.0)
irb(main):001:0>のように Rails 側が先にバナーを出し、その後 IRB prompt という流れだったのに対し、この PR 後は IRB の起動処理にフックすることで、IRB のライフサイクルに沿った形に近づきます(見た目は大きく変わらないが、内部的には IRB 主導)。
2-3. テストの追加・強化
railties/test/commands/console_test.rb に以下のようなテストが追加・修正されています:
bin/rails console実行時に、バナーが期待どおり 1 回だけ表示されることの検証RAILS_ENVや--environment、--sandbox指定時に、環境名などがバナーに正しく反映されることの検証- IRB を使っていないケース(例えば Pry を使う設定)では、IRB 用のバナー処理が走らないことの検証
擬似的なテスト例イメージ:
test "console prints Rails banner once" do
output = run_console_command
assert_includes output, "Loading test environment (Rails"
assert_equal 1, output.scan(/Loading .* environment \(Rails/).size
end- 影響範囲・注意点
- 対象:
bin/rails console/bin/consoleを利用するすべての Rails アプリ - 実行時の見た目
- 通常は、これまでとほぼ同じ文言のバナーが表示されますが、IRB の挙動・設定によっては表示タイミングや微細なフォーマットが変わる可能性があります。
- IRB のバナーや標準出力にフックしている gem(例: 独自に IRB 起動時メッセージを挿入するツール)との組み合わせで、順序が変わったり、表示の有無が変化する可能性があります。
- カスタムコンソール/ラッパーとの相性
Rails::ConsoleやIRBConsoleを継承・モンキーパッチして独自バナーや独自起動処理を組み込んでいる場合、今回の変更で呼び出しタイミングが変わることで、想定外の二重表示・未表示が起きるリスクがあります。- そのようなコードを書いている場合は、バナー表示の条件やフックポイント(
setup_irb,start, など IRBConsole 内部の呼び出し順)を一度確認することを推奨します。
- IRB バージョン依存の可能性
- バナー制御を IRB 側の API/挙動に寄せているため、Ruby/IRB のバージョン差異で細かい挙動が変わる可能性があります。CI などで複数 Ruby バージョンを回している場合は、
rails consoleの標準出力がテストに利用されていないか確認すると安心です。
- バナー制御を IRB 側の API/挙動に寄せているため、Ruby/IRB のバージョン差異で細かい挙動が変わる可能性があります。CI などで複数 Ruby バージョンを回している場合は、
- 参考情報 (あれば)
- 対象 PR:
- 関連ファイル:
railties/lib/rails/commands/console/console_command.rbrailties/lib/rails/commands/console/irb_console.rbrailties/test/commands/console_test.rbrailties/CHANGELOG.md(「console の起動バナーが IRB に基づくようになった」旨のエントリが追記)
#58454 Avoid duplicating fixtures path
マージ日: 2026/8/12 | 作成者: @byroot
- 概要 (1-2文で)
このPRは、Rails のテストヘルパー内部で「fixtures のパスを二重に登録してしまっていた」処理を修正し、不要な重複をなくすことでテスト実行を軽量化しています。動作上の機能追加や仕様変更はなく、パフォーマンスと内部実装の整理が主目的です。
- 変更内容の詳細(あればサンプルコードも含めて)
- 対象ファイル:
railties/lib/rails/test_help.rb - 変更行数: 1行追加 / 1行削除 (実質は1行の修正)
rails/test_help は、Rails のテスト環境(Minitest 等)をセットアップするための共通ヘルパーで、ActiveSupport::TestCase や ActiveRecord::FixtureSet の設定などを行っています。
今回のPRでは、その中で「fixtures のパスを設定する処理」で、同じパスを重複して登録してしまっていた箇所を修正しています。
重複登録があると、例えば以下のような問題が起き得ます:
- 同じディレクトリを2回スキャンするため、テストの起動時・実行時がわずかに遅くなる
- 内部的なループや配列操作が増えることで、無駄なオーバーヘッドが発生する
- 場合によっては、fixtures 読み込み処理の二重実行による副作用の温床になりかねない
PR 説明文では:
Just makes things slower for not reason.
とある通り、動作上のメリットが一切ない重複処理だったため、その不要な重複を削除する形になっています。
元になった議論/修正対象は以下のコメントです:
https://github.com/rails/rails/pull/57352#issuecomment-5260608367
(具体的な1行の差分は公開されていませんが、典型的には下記のようなイメージの変更です)
# 修正前(イメージ)
ActiveSupport::TestCase.fixture_paths << Rails.root.join("test/fixtures")
ActiveSupport::TestCase.fixture_paths << Rails.root.join("test/fixtures") # ← 重複
# 修正後(イメージ)
ActiveSupport::TestCase.fixture_paths << Rails.root.join("test/fixtures")あるいは、すでに fixture_paths に含まれるパスを再度追加していた処理を1回のみにする、もしくは uniq 的な重複排除を挟むような変更と考えられます。
- 影響範囲・注意点
機能面の影響:
- fixtures の検索パスが変わったり、どの fixtures が読み込まれるかが変化するわけではありません。
- 既存のテストコードの挙動は基本的に変わりません。
パフォーマンス面:
- 大規模なテストスイートや fixtures が大量にあるプロジェクトでは、テスト起動時や fixtures 読み込み時のオーバーヘッドがわずかに減る可能性があります。
- ただし1行レベルの最適化であるため、体感できるほどの劇的な改善ではなく、「無駄を1つ消した」という整理の意味合いが強いです。
互換性・移行時の注意点:
- fixtures のパス設定を Rails の標準的な方法 (
test/fixturesなど) で行っている限り、特別な対応は不要です。 - もし独自に
fixture_pathsを上書き・追記しているメタなコードを書いている場合でも、パスの重複に依存しているような特殊な実装をしていなければ影響はありません。 - 「fixtures パスが複数回登録されている前提で、内部の配列の長さや順序に依存したコード」を書いていると影響が出る可能性がありますが、そのようなコードは通常想定されていません。
- fixtures のパス設定を Rails の標準的な方法 (
- 参考情報 (あれば)
元の問題指摘・背景:
- https://github.com/rails/rails/pull/57352#issuecomment-5260608367
- 以前の PR (#57352) の中で、「fixtures パスの重複登録」に関する指摘があり、それを解消するフォローアップとして今回の PR (#58454) が出ています。
- https://github.com/rails/rails/pull/57352#issuecomment-5260608367
関連する Rails コードの読みどころ:
railties/lib/rails/test_help.rb- Rails のテスト環境をセットアップする中心的なファイルで、
ActiveSupport::TestCaseの拡張や fixtures のロード設定などがまとまっています。
- Rails のテスト環境をセットアップする中心的なファイルで、
active_record/fixtures/ActiveRecord::FixtureSet- fixtures のロード・管理周りの実装に興味がある場合はこちらも読むと理解が深まります。
#57980 Construct And nodes as Nary instead of Binary
マージ日: 2026/8/12 | 作成者: @skipkayhil
- 概要 (1-2文で)
このPRは、Active Record のクエリ条件を組み立てる際のAndノードの構造を、「二分木」ではなく「N分木(可変長)」として構築するように変更したものです。これにより、複数条件を AND で結合したときの AST(抽象構文木)の形がフラットになり、扱いやすく・最適化しやすくなります。
- 変更内容の詳細
何が問題だったか
従来の実装では、複数の条件を AND で結合する際に reduce を使っていたため、次のような「左に深い二分木」の構造になっていました。
And
And
And
Query
Query
Query概念的には、例えば:
rel.where(a: 1).where(b: 2).where(c: 3)のようなクエリ条件を内部表現に落とし込むとき、
- ((((a = 1) AND (b = 2)) AND (c = 3)) …)
のような連鎖する2項 AND のツリーになっていたイメージです。
このPRでどう変わったか
このPRでは、activerecord/lib/active_record/relation/predicate_builder.rb のロジックを変更し、And ノードに複数の子ノードを直接ぶら下げる「N分木」として構築するようにしています。
新しい構造は次のようになります:
And
Query
Query
Queryつまり、同じく
rel.where(a: 1).where(b: 2).where(c: 3)に相当する内部表現が、
- AND(a = 1, b = 2, c = 3)
のような「1つの And ノードに複数の条件を子要素として持つ」形になります。
具体的なコードイメージ(擬似的)
※実際のコードは 7 行追加・1 行削除と小さい変更ですが、イメージとしては以下のような変化です:
従来(reduce で二分木を作るイメージ):
nodes.reduce do |memo, node|
Arel::Nodes::And.new(memo, node) # 2つの子を取るBinaryなAnd
end変更後(フラットな Nary And を作るイメージ):
if nodes.length == 1
nodes.first
else
Arel::Nodes::And.new(nodes) # 配列を受け取ってNaryなAnd
end実際には Arel 側の Arel::Nodes::And が Nary をサポートしており、それに合わせて Active Record 側の Predicate Builder が、reduce ベースの2項構築をやめて最初から配列で And を構築するようにしている、という変更と考えられます。
- 影響範囲・注意点
対象範囲
- Active Record の内部でクエリ条件を Arel ノードに変換する部分(
PredicateBuilder)の挙動が変わります。 - 主に「複数条件を AND で結合する」ケースに影響します(
where,having,joins等、条件を構築するあらゆる箇所で利用される可能性があります)。
- Active Record の内部でクエリ条件を Arel ノードに変換する部分(
互換性への影響
- 通常のアプリケーションコード(
Model.where(...)を書いている側)から見える SQL は論理的には同じです。 - ただし、次のようなケースでは影響が出る可能性があります:
- Arel ノードの構造(ツリー形状)を前提に独自解析・パターンマッチをしているコード
Arel::Nodes::Andを「常に左再帰的な二分木」と見なしてトラバースしているメタプログラミング・ライブラリ
- こうしたコードは、
Andが「1つのノードに複数の子を持つ」構造になることを前提に修正が必要になります。
- 通常のアプリケーションコード(
パフォーマンス面
- フラットな Nary 構造になることで:
- 深いネストをたどる必要がなくなるため、条件のトラバースコストが軽減される可能性があります。
- 最適化やノードの再結合・簡約(例: 重複条件の削除)などがやりやすくなります。
- 逆に、Arel ノード構造のサイズや生成コストに目立った悪化要因はなく、実質的には改善寄りの変更と考えられます。
- フラットな Nary 構造になることで:
デバッグ・ログ出力
- Arel ノードをインスペクションしてデバッグしている場合、
Andの見え方(ネストの深さなど)が変わります。 - ツリーを手で追っていた場合、「ネストが浅くなった」ように見えるので注意してください。
- Arel ノードをインスペクションしてデバッグしている場合、
- 参考情報 (あれば)
関連しそうな箇所:
Arel::Nodes::Andクラスの実装(Nary サポートの追加・変更が先行している可能性が高い)ActiveRecord::Relation::WhereClause/PredicateBuilderあたりのコード(条件生成の呼び出し元)
想定される今後の発展:
- Nary
Andに統一されたことで、将来的に:- 条件式の正規化・簡約
- 複雑なクエリの自動最適化(例えば
((A AND B) AND (A AND C))のような式の再構成)
- といった機能強化がしやすくなります。
- Nary
このPRは主に内部構造の改善ですが、Arel ノード構造に依存したカスタム実装を持っている場合は、And がフラットな Nary ノードになる前提でコードを見直しておくと安全です。
#58388 Cleanup duplicate/unneeded requires
マージ日: 2026/8/11 | 作成者: @skipkayhil
- 概要 (1-2文で)
Rails内部のいくつかのファイルから、すでに他所で読み込まれているため不要となっていたrequireを削除し、requiresの重複を整理したPRです。機能追加や挙動変更はなく、コードベースのクリーンアップ・メンテナンスが目的です。
- 変更内容の詳細
このPRでは、./tools/railspect requires . という内部ツールで検出された「重複または不要な require」を削除しています。
ツール出力(抜粋):
actionpack/lib/action_controller/metal/renderers.rb
- active_support/deprecation (active_support/rails)
actionpack/lib/action_controller/metal.rb
- active_support/core_ext/module/delegation (active_support/rails)
actionpack/lib/action_dispatch/http/mime_type.rb
- active_support/deprecation (active_support/rails)
actionview/lib/action_view/helpers/navigation_helper.rb
- active_support/concern (active_support/rails)これに基づいて、以下の4ファイルから合計5行の require が削除されています。
actionpack/lib/action_controller/metal.rb- 削除:
require "active_support/core_ext/module/delegation"
- 削除:
actionpack/lib/action_controller/metal/renderers.rb- 削除:
require "active_support/deprecation"など合計2行(= diff -2)
- 削除:
actionpack/lib/action_dispatch/http/mime_type.rb- 削除:
require "active_support/deprecation"
- 削除:
actionview/lib/action_view/helpers/navigation_helper.rb- 削除:
require "active_support/concern"
- 削除:
これらはいずれも、active_support/rails など他のエントリポイントから既に読み込まれており、個々のファイルで明示的に require する必要がないと判断されたものです。
結果として、追加行は0行で、削除行のみ(-5)という純粋な削除PRになっています。
- 影響範囲・注意点
既存機能への影響
- Rails本体としては、依存関係の読み込み順やロードパスの前提が変わっていない限り、挙動は変わりません。
- 削除された
requireは、すでに別の場所から必ずロードされることが保証されている(とメンテナが判断している)ため、通常のアプリケーション開発者にとっては影響はありません。
Railsを特殊な形で利用している場合の注意点
- もし自前のスクリプト等で Rails の特定ファイルだけを単体で
requireしており、かつactive_support系を事前に読み込んでいない場合:- 例:
require "action_controller/metal/renderers"だけを直接ロードするような特殊な使い方
- 例:
- このような「Railsの標準的なロード順に依存していない」ユースケースだと、
ActiveSupport::DeprecationやActiveSupport::Concern、Module#delegate等が未定義になりNameErrorになる可能性があります。 - その場合は、呼び出し側で明示的に:rubyなどを追加する必要があります。
require "active_support/deprecation" require "active_support/concern" require "active_support/core_ext/module/delegation"
- もし自前のスクリプト等で Rails の特定ファイルだけを単体で
パフォーマンス・メモリ
- require の削減により、わずかではありますがロード時のオーバーヘッドを減らす効果があります。ただし、数行レベルなので体感できる差はほぼありません。
- 参考情報 (あれば)
- PRで利用されているツール:
./tools/railspect requires .- Railsリポジトリ内の
require依存関係を解析し、重複や不要なものを抽出するための内部ツールと考えられます。
- Railsリポジトリ内の
- 今回のようなPRは、Railsのコードベースを長期的に保守しやすくするための「テクニカルデット削減」の一環であり、他のコンポーネントでも同様のクリーンアップが行われている可能性があります。
#58450 Dedup Reflection#plural_name
マージ日: 2026/8/11 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
ActiveRecord::Reflectionのplural_nameを「重複しない(dedup)」形で扱うようにする、小さなリファクタリングの PR です。内部実装の 1行が置き換えられただけで、外部 API の振る舞いは基本的に変えずに重複処理を整理しています。
- 変更内容の詳細
※ PR 本文に説明がなく、変更も 1行のみのため、Rails の Reflection#plural_name の既存実装・命名規則から推測を含めて解説します。
- 変更ファイル:
activerecord/lib/active_record/reflection.rb - 変更行数: +1 / -1(実質的には同じロジックを別の場所/メソッドに委譲 or 既存の仕組みを再利用)
Reflection#plural_name は、関連名(association name)から「複数形の名前」を返すためのメソッドです。
典型的には、次のような用途で使われます:
class Author < ApplicationRecord
has_many :books
end
reflection = Author.reflect_on_association(:books)
reflection.plural_name # => "books"この PR のタイトル「Dedup Reflection#plural_name」から読み取れる意図は:
plural_nameのロジックが別のメソッド(例えばnameやactive_record.name.tableizeなど)と重複していたため、- その重複を解消し、既存の共通メソッドに委譲する形へ変更した、というものです。
Rails コアの典型的なパターンだと、以下のような変更が行われている可能性が高いです(擬似コード):
# 変更前(例)
def plural_name
name.to_s.pluralize
end
# 変更後(例)
def plural_name
@plural_name ||= some_common_method_that_already_handles_pluralization
endあるいは、すでに別の場所で定義済みの @plural_name キャッシュや、関連ごとに計算済みの名前付けロジックに乗せ替える、といった形で「新たに重複したロジックを持たない」ようにした可能性があります。
要点としては:
- ロジックの重複(同じ処理のコピペ)をなくす
- キャッシュや既存の名前解決ロジックを再利用する
- 結果としてコードの保守性を上げる(将来の仕様変更時の修正箇所を減らす)
というリファクタリングです。
- 影響範囲・注意点
- 外部 API としての
Reflection#plural_nameの戻り値は基本的に変わらない想定です(内部の重複を解消しただけ)。 - ただし、次のような「Rails 内部実装に依存した高度なメタプログラミング」をしている場合は、注意が必要になります:
Reflection#plural_nameの内部処理(たとえばname.to_s.pluralizeをしていること)を前提に monkey patch しているplural_nameがメモ化される / されないタイミングに依存したコードを書いている
- 一般的なアプリケーションコード(
reflect_on_association(:foo).plural_nameを素直に呼んでいる程度)では、動作への影響はほぼないと考えて良いです。
- 参考情報 (あれば)
- 対象 PR:
rails/railsリポジトリの PR #58450: Dedup Reflection#plural_name - 関連するコード周辺を読む際のキーワード:
ActiveRecord::ReflectionActiveRecord::Reflection::AssociationReflection#name,#plural_name,#class_name,#active_recordなどの関連メタ情報取得メソッド
- Rails の命名規則/複数形化のロジックは
ActiveSupport::Inflectorによって提供されており、pluralize,tableize,underscoreなどを経由して使われます。この PR も、そうした既存インフレクタの利用経路を整理した一環である可能性が高いです。
#58411 Active Record ractor safe ivars 2
マージ日: 2026/8/11 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Active Record のクラスインスタンス変数(@default_attributesと@find_by_statement_cache)が Ractor セーフになるように実装を見直した PRです。これにより、Active Record を Ractor(マルチスレッド+並列実行)環境で安全に利用しやすくなっています。
- 変更内容の詳細
2-1. @default_attributes を Ractor セーフに
背景として、Active Record のベースクラスで利用している @default_attributes は、クラスインスタンス変数として保持されており、そのままだと Ractor 間で共有するにはオブジェクトが「Ractor シェア可能」になっている必要があります。
この PR では、
@default_attributesを「メイン Ractor 上で一度だけ構築」- 構築後にその属性セットを
freezeして不変にする
という形に変えています。
ポイント:
- メイン Ractor で一度だけ(かつ不変)にすることで、他の Ractor に安全に共有可能
- Ractor 間で書き換えが発生しないようにしている
イメージとしては以下のようなパターンになっているはずです(実際のコードは多少異なる可能性がありますが、意図のイメージとして):
# 例: メイン Ractor でのみ初期化し、freeze したものを共有
def default_attributes
@default_attributes ||= begin
attrs = compute_default_attributes
attrs.freeze
end
endここを「メイン Ractor で初期化する」という制約を守るように実装が調整されています。
2-2. @find_by_statement_cache をクラスインスタンス変数から Ractor ストレージへ移行
find_by 系のクエリで使われる @find_by_statement_cache も、これまでクラスインスタンス変数としてキャッシュ用の構造を持っていましたが、これが Ractor セーフではありませんでした。
この PR では、
@find_by_statement_cache自体を「インスタンス変数としては持たない」ように変更- 代わりに
Ractorごとに分離されたストレージを使って、ヒット・ミス用のキャッシュマップを保持する
ように変えています。
おそらく以下のようなユーティリティが ActiveSupport::Ractors に追加されています(あくまでイメージです):
# activesupport/lib/active_support/ractors.rb に追加
module ActiveSupport
module Ractors
def self.local(key)
# Ractor ごとのストレージを返す API 的なもの
end
end
end
# Active Record 側での利用イメージ
def find_by_statement_cache
ActiveSupport::Ractors.local(:find_by_statement_cache) do
# このブロックで Ractor ローカルなキャッシュを初期化する
{ hits: Concurrent::Map.new, misses: Concurrent::Map.new }
end
endポイント:
- それぞれの Ractor が独自のキャッシュを持つため、Ractor 間でのデータ競合がなくなる
- キャッシュ構造自体は
Concurrent::Mapなどのスレッドセーフなコンテナを使いつつも、「Ractor ごとに分離」されることで Ractor の制約を満たす
2-3. ActiveSupport に Ractor 用ユーティリティを追加
activesupport/lib/active_support/ractors.rb に 16 行のコードが追加され、ActiveSupport::Ractors モジュールが拡張されています。
想定される機能:
- キーに基づいて「Ractor ローカルなストレージ」を扱うための API
- 必要に応じて初期化ブロックを受け取り、その Ractor の中でのみ初期化を行う仕組み
これにより、Active Record 以外のコードからも、将来的に Ractor ローカルな状態管理をしやすくなっている可能性があります。
2-4. テストの追加
activerecord/test/cases/attributes_test.rbに 21 行追加@default_attributesが Ractor セーフに動作するか、Ractor から利用したときの挙動が検証されていると考えられます。
activerecord/test/cases/core_test.rbに 26 行追加find_by系呼び出しが Ractor 内からも正しく動作し、キャッシュが Ractor セーフであることをテスト。
activesupport/test/ractors_test.rbに 16 行追加ActiveSupport::Ractorsの API が期待通り「Ractor ローカルに」動くことをテスト。
- 影響範囲・注意点
対象:
- Active Record のデフォルト属性 (
default_attributes) find_by系メソッドのステートメントキャッシュ- 新たに追加される
ActiveSupport::Ractorsユーティリティ
- Active Record のデフォルト属性 (
互換性:
- パブリック API のシグネチャ自体は変えていないため、通常のアプリケーションコードへの影響はほぼない想定
- ただし、Active Record 内部のクラスインスタンス変数に直接アクセスしていたメタプログラミング/Monkey Patch があると挙動が変わる可能性があります
@default_attributesに直接上書きしていた場合
→ freeze により書き換えができなくなる、あるいは別のタイミングで初期化される@find_by_statement_cacheを直接参照していた場合
→ そもそもインスタンス変数として存在しなくなる
Ractor を使った並列実行の安定性向上:
- Ractor を本格利用する環境(並列 Web サーバ, job worker など)では、Active Record のベースクラスで共有していた状態が整理されるため、データ競合・Ractor 制約違反によるエラーのリスクが減ります。
パフォーマンス:
- Ractor ごとにキャッシュを持つため、従来「単一のキャッシュを全スレッド/全 Ractor で共有」していた場合とキャッシュヒット率やメモリ使用量のバランスが変わる可能性があります。
- ただし、Ractor 間共有を無理に維持するよりも、安全性を優先する設計になっていると見てよいです。
- 参考情報 (あれば)
この PR は前の PR #58399 のフォローアップとして、残っていた Ractor 非対応の ivar を解消するものです:
Ruby の Ractor とシェア可能オブジェクトについての背景:
- Ruby 3 以降の Ractor では、Ractor 間で共有できるのは「シェア可能 (shareable)」なオブジェクトのみであり、可変なオブジェクトは基本的に共有不可です。
freeze済みのオブジェクトはシェア可能になりうるため、本 PR のように「メイン Ractor で初期化して freeze」→「他 Ractor から共有して読むだけ」という設計が重要になります。
Ractor 対応を意識した Active Record 拡張やライブラリを書く場合:
- 「クラスインスタンス変数に可変な状態を持たない」
- 「必要なら Ractor ローカルなストレージを使う」
- 「共有したいものはメイン Ractor で初期化して
freezeする」
といった方針が、この PR から読み取れるベストプラクティスです。
#58449 Ensure Active Record models define all attributes
マージ日: 2026/8/11 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
Active Record のattribute定義が、モデルの「属性メソッド自動生成」と常に同期するように修正されています。これにより、あとから追加されたattributeでも「動的属性」ではなく、通常の getter/setter などのメソッドが必ず生成されるようになります。
- 変更内容の詳細
何が問題だったか
従来は、以下のようなケースで「属性メソッド生成のタイミング」と「attribute 定義のタイミング」がずれることがありました。
- モデルクラスのロード中に一度 attribute メソッドが生成されたあとで、
- 追加で
attribute :foo, :stringなどを定義した場合、
この新しく追加された foo 属性については、foo / foo= / foo? などの Ruby メソッドが 定義されず、内部的には「動的属性」として扱われる挙動が残っていました。
「動的属性」は Rails 7 以降で原則非推奨になっている仕組みで、method_missing を使って record.unknown_column みたいな呼び出しに対応していた古いパターンです。今回の PR は、この動的属性にフォールバックしてしまう残りのエッジケースをふさぐものです。
何をしているか
activerecord/lib/active_record/attributes.rb に 1 行だけ追加されており、その目的は「新しい attribute が定義されたときにも、必ず属性メソッド生成の仕組みが動くようにする」ことです。
イメージとしては、以下のようなことを常に保証する変更です(※擬似コード):
class User < ApplicationRecord
# ここで一度 attribute メソッドが生成されたとしても…
end
# 後から attribute を追加
User.attribute :nickname, :string
# 以前:
# nickname / nickname= / nickname? が定義されないことがあり、
# method_missing ベースの「動的属性」にフォールバックするケースがあった
# 今回の修正後:
# 新しく追加された nickname についても、
# user.nickname
# user.nickname = "foo"
# user.nickname?
# などのメソッドがきちんと定義されるテスト (activerecord/test/cases/attributes_test.rb で +31 行) では、以下のような観点がカバーされていると考えられます:
- すでに attribute メソッド生成済みのモデルに対して
attributeを追加しても、新しい属性がメソッドとして使えるか - 読み取り・書き込み・predicate メソッド(
foo?)などが正しく定義されているか - 追加された属性が動的属性にフォールバックしないこと
(実際のテスト名は PR 内容からの推測ですが、挙動としては上記が意図されています。)
- 影響範囲・注意点
影響範囲
- 対象:
- Active Record モデルで
attribute :xxx, :typeを利用しているコード、特にクラス定義後・実行時に attribute を追加するようなメタプログラミングをしている箇所。
- Active Record モデルで
- 変化:
- これまで「たまたま」動的属性として扱われていたケースが、正式な属性メソッドとして扱われるようになります。
respond_to?(:foo)やmethods.include?(:foo)などで、以前はfalseだったものがtrueになる可能性があります。method_missingのフックやモンキーパッチに依存していた場合、その挙動が変わる可能性があります。
注意点
- 動的属性に依存したロジックは、今後ますます動かなくなる方向なので、この変更は「推奨パス(明示的な attribute 定義)」に寄せる改善です。
- 属性が「定義済みかどうか」を
defined?(model.some_attr)やrespond_to?などで判定しているコードがある場合、結果が変わることがあります。そういったコードは、has_attribute?,attribute_names,columns_hashなど、より正式な API で判定するよう見直した方が安全です。 - ランタイムに
attributeを追加するメタプログラミングをしている場合:- 今回の変更により、その属性に対するメソッドが自動的に定義されることを前提にして構いません。
- 逆に「メソッド未定義であること」を前提にしていたような特殊な処理(
method_missingの独自実装など)がある場合は、テストが必要です。
- 参考情報 (あれば)
- Rails ガイド: Active Record の属性 API
https://guides.rubyonrails.org/active_record_basics.html#attributes - 動的属性 (Dynamic Attributes) 廃止の流れについての背景は、Rails 7 以降の CHANGELOG や関連 PR(
method_missingベースの属性アクセス削除)を参照すると理解しやすいです。
#58441 Render Markdown lists that collapse into paragraphs
マージ日: 2026/8/11 | 作成者: @55728
- 概要 (1-2文で)
このPRは、Railsソース中の:markup: markdownなRDocコメントで、本来リストとして表示されるべき部分が1つの長い段落としてレンダリングされていた箇所をMarkdownとして正しく解釈されるように修正したものです。ついでに、リストではなくサンプル出力として表示すべき箇所をコードブロック(<pre>)として出るように整えています。
- 変更内容の詳細
背景・問題点
- RailsのRDocコメントの一部は
:markup: markdownで書かれていますが、- 箇条書きリスト(
- itemや1. item)の前後の改行・インデントが不足していた - そのため
rdocでHTML生成すると<ul>/<ol>ではなく単なる1つの段落としてレンダリングされていた
- 箇条書きリスト(
- 同様に、「サンプル出力」であるべき部分がMarkdown的には通常テキストとして扱われ、見た目が崩れていました。
このPRでは */lib 以下の :markup: markdown コメントを総なめし、問題のある箇所をすべて修正しています(#58439 のフォローアップ)。
主な修正箇所と内容
Markdownレンダリングの観点でいうと、ほぼすべて「空行の追加」または「インデント・マーカー調整」によるリスト/コードブロック化です。
1. ActionController::TestCase (ordered list)
元の状態(実際には1行でつながっている形):
Functional tests are written as follows: 1. First, one uses the get, post, patch,
put, delete, or head method to simulate an HTTP request. 2. Then, one asserts
whether the current state is as expected. ...Markdownのルール的には「文の途中に 1. や 2. がある」扱いになり、単なる段落になります。
このPRでは、例えば以下のように「前に空行+行頭に番号」になるよう修正しています(実際のコードもこの趣旨):
Functional tests are written as follows:
1. First, one uses the `get`, `post`, `patch`, `put`, `delete`, or `head`
method to simulate an HTTP request.
2. Then, one asserts whether the current state is as expected. “State” can
be anything: the controller’s HTTP response, the database contents, etc.これにより <ol><li>…</li>…</ol> としてRDocに出力されます。
該当:
ActionController::TestCaseActionCable::Channel::TestCase
(どちらも「テストの手順」の説明が番号付きリストになる)
2. 単純な箇条書きリスト (unordered list)
同様に、- item 形式のリストが前後の改行不足で単なる段落と解釈されていた箇所を修正しています。
代表例: ActionDispatch::Http::URL::DomainExtractor
元のコメントは概ね以下のような構造でした:
The module assumes a standard domain structure where domains consist of:
- Subdomains (optional, can be multiple levels)
- Domain name
- Top-level domain (TLD, can be multiple levels like .co.uk)
For example, in “api.staging.example.co.uk”:
- Subdomains: ["api", "staging"]
- Domain: "example.co.uk" (with tld_length=2)
- TLD: "co.uk"ここに空行がなかったり改行位置が悪いと、rdoc + Markdownとしては途中で折りたたまれ、1つのパラグラフに見えてしまいます。
このPRでは例えば以下のような形になるよう、空行・インデントを調整しています:
The module assumes a standard domain structure where domains consist of:
- Subdomains (optional, can be multiple levels)
- Domain name
- Top-level domain (TLD, can be multiple levels like `.co.uk`)
For example, in “api.staging.example.co.uk”:
- Subdomains: ["api", "staging"]
- Domain: "example.co.uk" (with `tld_length = 2`)
- TLD: "co.uk"同様の対応を行っている箇所:
ActionController::Live— ストリーミング/ライブコントローラの使い方の箇条書きActionController::Redirecting—redirect_toの振る舞いなどの説明ActiveSupport::DotEnvConfiguration— envファイル読み込みやキーの扱いの説明ActiveSupport::Cache::RedisCacheStore— オプションや挙動のまとめActiveSupport::Cache::MemCacheStore— キャッシュストア設定・注意点のリスト
各ファイルとも、実コードは1行前に空行を入れる・文末のコロンのあとに空行を入れる等、Markdownが期待通りに解釈できるよう最小限のフォーマット調整が行われています。
3. ActiveSupport::Callbacks::ClassMethods#skip_callback (sample output → code block)
ここだけ少し性質が異なります。
- 問題箇所は「リスト」ではなく「サンプル出力」
- コメント内の例コードの「実行結果」に相当する部分が、Markdownとしては普通のテキスト扱いになり、読みづらい状態だった
- 同じコメント中に「2つ目の
Output:ブロック」は既にコードブロック(<pre>)として表示されていた
このPRで、最初の Output: 部分も2つ目と同様にMarkdownのコードブロックとなるようにインデント/空行を修正しています。
例えば以下のような形になります(イメージ):
Output:
```ruby
#<SomeCallback ... >
実際にはRDoc+Markdownの規則に従ったインデントで書かれており、`rdoc` でのHTML生成時に `<pre>...</pre>` として出力されることが確認されています。
---
3. 影響範囲・注意点
- 影響範囲はあくまで **ドキュメント出力(RDoc / APIドキュメントサイト)** のみであり、Rubyコードの挙動・API・型・シグネチャには一切変更はありません。
- 変更されたファイルはすべて `lib` 以下のコメント部分で、合計変更行数も少なく(+16 / -4)、バイナリ互換性・動作互換性への影響はありません。
- `rdoc` を使ってAPIドキュメントを生成している場合:
- 上記のクラス/モジュールのページで、これまで1つの長い段落に見えていた説明文が、リスト/コードブロックとしてより読みやすく表示されるようになります。
- PR説明にある通り、各変更は `rdoc` でHTMLを生成し、該当箇所に `<ol>` / `<ul>` / `<pre>` が出力されていることを確認済みです。
運用・CI観点での注意点は特にありませんが、社内やプロジェクト内でRailsのRDocコメントをMarkdownとして記述している場合、
- 「リストの前後には空行を入れる」
- 「コードやサンプル出力はインデント or フェンスで明示的にコードブロック化する」
といったスタイルガイドを採用すると、同様の問題を避けやすくなります。
---
4. 参考情報 (あれば)
- 対象PR: #58441 “Render Markdown lists that collapse into paragraphs”
- フォローアップ元: #58439
- 影響を受ける主なAPIドキュメントページ:
- `ActionController::TestCase`
- `ActionCable::Channel::TestCase`
- `ActionController::Live`
- `ActionController::Redirecting`
- `ActionDispatch::Http::URL::DomainExtractor`
- `ActiveSupport::DotEnvConfiguration`
- `ActiveSupport::Cache::RedisCacheStore`
- `ActiveSupport::Cache::MemCacheStore`
- `ActiveSupport::Callbacks::ClassMethods#skip_callback`
---
## [#58439](https://github.com/rails/rails/pull/58439) Render the `EncryptedFile#read` exception list as a list {#pr-58439}
**マージ日**: 2026/8/11 | **作成者**: [@55728](https://github.com/55728)
1. 概要 (1-2文で)
`ActiveSupport::EncryptedFile#read` がどんな例外を投げるかのドキュメント表現が、1つの段落ではなく HTML の箇条書き(リスト)として正しくレンダリングされるように修正された PR です。コードの挙動変更はなく、ドキュメント(RDoc/YARD 出力)の見た目のみが改善されています。
---
2. 変更内容の詳細
- 対象: `activesupport/lib/active_support/encrypted_file.rb`
- 変更規模: 1行追加、削除なし
`ActiveSupport::EncryptedFile#read` のドキュメントコメントで、「Raises:」以下の例外リストが、あるコミット(`17831794cc`)以降、RDoc などで 1つの `<p>` 要素の中に「- MissingKeyError ... - MissingContentError ... - ActiveSupport::MessageEncryptor::InvalidMessage ...」という形で詰め込まれて表示されるようになっていました。
この PR によって、HTML 生成時に以下のような従来の期待される構造に戻ります。
```html
<p>Raises:</p>
<ul>
<li>
<p>MissingKeyError if the key is missing and <code>raise_if_missing_key</code> is true.</p>
</li>
<li>
<p>MissingContentError if the encrypted file does not exist or otherwise if the key is missing.</p>
</li>
<li>
<p>ActiveSupport::MessageEncryptor::InvalidMessage if the content cannot be decrypted or verified.</p>
</li>
</ul>つまり、以下3つの例外が、ドキュメント上で箇条書きとして明確に読めるようになる変更です(例外の種類や発生条件自体は変更されていません):
MissingKeyError- 条件: キーが存在せず、
raise_if_missing_keyがtrueの場合
- 条件: キーが存在せず、
MissingContentError- 条件: 暗号化ファイルが存在しない、またはキーがないために内容を取得できない場合
ActiveSupport::MessageEncryptor::InvalidMessage- 条件: 内容を復号/検証できない場合
実際の Ruby コードとしては、コメントのフォーマット(おそらく :nodoc: ではなく、例外の列挙部分に箇条書きを示す RDoc 記法の追加など)が 1 行だけ修正されている形です。
- 影響範囲・注意点
- 実行時の挙動:
EncryptedFile#readの実際の実装・例外クラス・発生条件には一切変更がなく、アプリケーションコードの動作には影響しません。
- 影響範囲:
- Rails API ドキュメントや、RDoc/YARD などで生成されるドキュメントの HTML 表示のみ。
- 暗号化ファイル機能(
config/credentials.yml.enc等)を使うアプリケーションにも、バグフィックスや破壊的変更はありません。
- 注意点:
- ドキュメントを参照している人にとって、例外のリストが読みやすく整理されるだけの変更です。
- これを前提にしたテストやパーサー(HTML を自動解析するツールなど)がある場合は、HTML 構造が変わる可能性を考慮する必要がありますが、一般的な利用では問題にならないはずです。
- 参考情報 (あれば)
- 対象メソッド:
ActiveSupport::EncryptedFile#read
Rails ガイド上では、主に「Credentials」や「Encrypted configuration」に関連する箇所で登場します。 - 関連コミット:
- 現状の不正な段落表示を生んだとされるコミット:
17831794cc - 今回の PR (#58439) は、その変更で崩れたドキュメントレイアウトを元に戻すものです。
- 現状の不正な段落表示を生んだとされるコミット:
#58412 Omit optional route segments given empty strings
マージ日: 2026/8/11 | 作成者: @carldaws
- 概要 (1-2文で)
url_forなどでルーティングの「任意セグメント」に空文字列 ("") が渡された場合、そのセグメントを URL から省略するように振る舞いを統一した修正です。これにより、フォーム送信などで空文字が来ても壊れた URL が生成されず、nilと同じ扱いになります。
- 変更内容の詳細
従来の問題
ルーティング定義:
get "(/locale/:locale)/products(/:id)", to: "products#show"このとき url_for の挙動は以下のようになっていました:
url_for(controller: "products", action: "show", locale: nil, id: 123)
# => "/products/123" # OK: optional な :locale が省略される
url_for(controller: "products", action: "show", locale: "", id: 123)
# => "/locale//products/123" # NG: "/locale/" + (空) + "/products/123"locale: nilの場合は期待通りlocale部分が省略されるlocale: ""の場合は空文字がパスに埋め込まれ、/locale//products/123という不正な URL になる
(定義したルート自身がこの URL にマッチできない)
フォーム送信時など、未入力のフィールドは nil ではなく "" になりやすいため、実運用で問題になっていました。
さらに、末尾にある任意セグメント(トレーリング)の場合は「たまたま」空文字が省略されるケースがあり、
- 中間の任意セグメント:
""がパスに残る - 末尾の任意セグメント:
""が省略されることがある(実装の副作用)
という不一致な状態になっていました。
今回の修正内容
- 対象:
Journey::Formatter#generate - やっていること:
- パス生成時に「任意セグメント用のパラメータ値」が空文字
""の場合、それをnilと同様に扱い、パスからそのセグメントを取り除く。 - この除去処理は、クエリパラメータの分離後、パス用パラメータに対して行う。
- そのため、「任意セグメントに使われるはずだった値」がクエリ文字列側に回って
?locale=のようなクエリパラメータになってしまう、ということは起きない。
- そのため、「任意セグメントに使われるはずだった値」がクエリ文字列側に回って
- パス生成時に「任意セグメント用のパラメータ値」が空文字
サンプル挙動(意図される新挙動):
get "(/locale/:locale)/products(/:id)", to: "products#show"
# これまでもこうだった
url_for(controller: "products", action: "show", locale: nil, id: 123)
# => "/products/123"
# 今回の修正でこうなる("" も nil と同じ扱い)
url_for(controller: "products", action: "show", locale: "", id: 123)
# => "/products/123"変更しない挙動(テストで明示的に固定)
- 必須セグメントに対して空文字
""が渡された場合:- 従来通りの動作を維持(エラーやマッチしない URL になるなど、既存仕様に依存)
false値の扱い:- 今回の変更では影響なし。従来通りの挙動を維持
- 空のクエリパラメータ:
?locale=のようなクエリ部に現れる空文字は、そのまま保持(省略しない)
また、この PR は #55846 の差し替えであり、#55846 で問題だった以下の点を解消しています。
- 以前の案(#55846)では、複数の候補ルートを試す段階で「共有されている options ハッシュ」を直接書き換えてしまい、
ある候補ルートでの正規化結果が別の候補ルートにも漏れ出す危険があった。 - 今回はそのような副作用を起こさない形で、
Journey::Formatter#generate内部で値の正規化・除去を行うようにしている。
- 影響範囲・注意点
影響を受ける主なケース
任意セグメント(
(),/:param?相当)を含むルーティングで、url_forや*_url/*_pathヘルパーに 空文字""が渡されていたケース。特に、フォームから送信された params をそのまま渡しているようなコード:
ruby# 例: locale が form の select で未選択の場合に "" になる redirect_to products_path(locale: params[:locale], id: 123)以前は
/locale//products/123のような壊れた URL を生成し得たが、今後は/products/123になる。
後方互換性の観点
- 中間の任意セグメントに空文字をあえて残したい、というニッチな利用は理論上は影響を受けますが、 そのような URL はルート自体がマッチできないことが多く、既に半ば「バグ寄り」の挙動だったため、 互換性破壊のインパクトはごく小さいと考えられます。
""を「存在する値」として扱いたい場合は、- 任意セグメントではなく必須セグメントにする
- あるいはクエリパラメータとして扱う(例:
?locale=) など、ルーティング設計側で明示的に対応する必要があります。
テスト
actionpack/test/dispatch/routing_test.rbに 65 行分のテストが追加されており、- 任意セグメント +
""→ 省略される - 必須セグメント +
""→ 従来通り falseや空クエリパラメータの扱い といったケースが固定されています。
- 任意セグメント +
- 参考情報 (あれば)
- 対応 Issue: #55845
- 置き換えられた PR: #55846
- 変更ファイル:
actionpack/lib/action_dispatch/journey/formatter.rbactionpack/test/dispatch/routing_test.rb
この挙動に依存しそうなコードとしては、
- フォームの空値をそのまま URL に埋め込むような redirect / link ヘルパー周り
- ローカライズ用プレフィックス(例:
/(:locale)/...)を optional で書いているアプリ
が対象になり得るため、そのような箇所の URL 生成結果を一度確認しておくと安心です。
#58423 Mark the routes as loaded when the routes reloader runs standalone
マージ日: 2026/8/11 | 作成者: @55728
- 概要 (1-2文で)
Rails のルート再読み込み処理 (Rails::Application::RoutesReloader) が「ルートを実際には再描画しているのに loaded フラグが立たない」という退行バグを修正し、重複してルートが描画される問題を防ぐ PR です。execute実行後に適切に「ルートがロード済み」であることを記録しつつ、失敗時は未ロードのままにするように振る舞いを整理しています。
- 変更内容の詳細
背景となる問題
- 本来
RoutesReloader#executeは「ルートを描画して finalize するだけでなく、その結果を loaded 状態として記録する」役割を担っていました。 - しかし、過去のコミット(
be08bdce21)以降、この「loaded 状態の記録」が抜け落ちており、以下のような挙動になっていました:
reloader.execute
reloader.loaded # => false になってしまうアプリケーションの finisher がインストールする reloader hook は
reloader.executeを直接呼び出します。一方で
Engine::LazyRouteSetは、URL ヘルパが見つからなかったときに「まだルートがロードされていない」と判断すると、ルートをもう一度全部描画し、after_routes_loadedフックも再度実行します。そのため、以下のようなタイミングで「ルートが二重に描画&after_routes_loaded が二重実行」される可能性がありました:
rails consoleでのreload!- 開発環境で、アプリケーションファイル変更後に入ってきた次の HTTP リクエスト
loadedアクセサ自体は115f92b4d8で reader としては復帰していたものの、そのフラグをセットする処理がこのパス(executeが単独で呼ばれる経路)には残っていませんでした。
この PR の修正内容
1. RoutesReloader#execute がルートを「ロード済み」とマークするように修正
Rails::Application::RoutesReloader#execute 内で、ルートの描画が成功して完了したあとに「loaded フラグを true にする」処理が追加されています。
イメージとしては下記のような処理が行われるようになったと考えられます(実際のコードは若干異なる可能性がありますが、意味としてはこれに相当):
def execute
# ルートを描画する処理
draw_routes
# 正常に終わったら loaded フラグを立てる
@loaded = true
endポイント:
executeが単独で呼ばれた場合でも、「この reloader インスタンスに紐づくルートはすでにロードされている」と判定されるようになる。- これにより、後続の処理(
Engine::LazyRouteSetや他のフック)が、二回目の描画やフックの再実行を行わなくなる。
2. execute_unless_loaded との整合性
execute_unless_loadedは自前で「ロード前 → フック実行 → ロード後」の状態管理を行っており、最後に自分でloadedを true にしています。- 今回の修正では、このロジックは変えず、「
execute_unless_loadedの中からexecuteが呼ばれている場合には、状態管理はexecute_unless_loaded側で引き続き行う」ように配慮されています。 - つまり:
execute単体で呼ぶパス → この PR でloadedを true にするように変更execute_unless_loaded経由のパス → もともとのexecute_unless_loadedによる state 管理を維持(executeの変更で壊さない)
3. 失敗時の挙動
- もし
execute実行中に例外などでルートの描画が失敗した場合は、loadedフラグは変更されず「未ロード」のままにしておく、という仕様が明示されています。 - これにより:
- 後からの再実行(再トライ)が正しく行われる
- 中途半端に失敗した状態を「ロード済み」と誤認識することを防ぐ
4. テスト追加
以下の挙動を保証するテストが railties/test/application/routes_reloader_test.rb に追加されています(+26行):
execute実行後にloadedが true になること- ルートが二度描画されないこと(すなわち、
execute実行後は再描画が行われない) executeが失敗した場合はloadedが false のまま残り、再実行されること
すべての関連テスト群はパス:
railties/test/application/routes_reloader_test.rb: 5 runs, 11 assertions, 0 failuresrailties/test/application/routing_test.rb: 34 runs, 0 failuresrailties/test/application/loading_test.rb: 22 runs, 0 failuresrailties/test/application/initializers/frameworks_test.rb: 28 runs, 0 failures
- 影響範囲・注意点
影響範囲
- 主に影響を受けるのは「ルート再読み込みの挙動に依存する」部分です。
- Rails コンソールでの
reload! - 開発環境でのコード変更後のリクエスト
Engine::LazyRouteSetによる遅延ルーティングセットアップ
- Rails コンソールでの
- これまで:
executeを直接呼んだ後もreloader.loaded # => falseだったため、後続処理が「まだルートがロードされていない」と見なして再描画してしまうケースがあった
- 今後:
executeが正しくloadedを true にするため、同じリクエスト内や直後に無駄な二重描画やafter_routes_loadedの二重実行が起きにくくなります。- 結果として:
- パフォーマンス面での軽微な改善(特に開発環境)
- after_routes_loaded フックの副作用を持つコードが、二重に実行されることによる不整合の防止
注意点
ルートの再描画に副作用を持たせていた場合
たとえば、after_routes_loaded内でグローバルな状態を書き換えたり、メトリクスを積み上げたりしていた場合、これまで「たまたま二重に呼ばれていた」ことに依存していたロジックがあれば挙動が変わります。- ただし、それは本来バグ寄りの挙動への依存であり、この PR によってより一貫した挙動になります。
ルート描画失敗時の扱い
executeが途中で失敗した場合、従来以上に「再試行される」ことが明確になります。
「失敗したらそのまま落ちてほしい」ようなユースケースでは、例外処理やロギング側での対応が必要になる可能性があります(とはいえ標準的な Rails アプリではこの挙動がより自然です)。API 的な互換性
変更はRoutesReloaderの内部状態管理の修正であり、外部インターフェースのメソッドシグネチャは変わっていません。
ただし、reloader.loadedの返り値が、execute呼び出し後に従来と変わるため、テスト等でloadedフラグを直接検査していた場合は挙動差が出る可能性があります(多くの場合は「正しくなった」方向の変化)。
- 参考情報 (あれば)
- 関連コミット:
be08bdce21:executeが loaded 状態を記録しなくなった変更が含まれていたコミット115f92b4d8:loadedreader が復活したコミット(ただし setter 側の復元はされていなかった)
- 関連クラス・モジュール:
Rails::Application::RoutesReloaderRails::Engine::LazyRouteSet
- チェックリスト上も:
- バグ修正に対するテスト追加
- 振る舞い変更に応じた CHANGELOG 更新
が明示されているため、公式に「挙動としてこれが正」と位置づけられています。
#58433 Keep the string subscribers default when rebuilding a Fanout
マージ日: 2026/8/10 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Ractor 内でActiveSupport::Notifications::Fanoutを再構築する際に、文字列キー用サブスクライバのデフォルト値設定が失われてクラッシュするバグを修正した PR です。サブスクリプションスナップショットから再構築する際に、Concurrent::Mapとデフォルト値、および配列の複製を正しく行うようにしています。
- 変更内容の詳細
問題の背景
- マルチ Ractor 環境では、メイン Ractor 以外の Ractor は「サブスクリプションのスナップショット」から
Fanout(通知用のオブジェクト)を再構築します。 - 元々
Fanoutでは、文字列ベースのイベント名のサブスクライバを保持するためにConcurrent::Mapを使い、rubyのような「存在しないキーにアクセスされたら自動的に空配列を作る」デフォルトを持つマップを使っていました。@listeners_for = Concurrent::Map.new { |h, k| h[k] = [] } - しかしスナップショットを代入するときに、
Concurrent::Mapではなく単なる frozen な Hash が入ってしまい、- 未購読のイベント名に対して
all_listeners_forがnilを返す - その
nilを配列として扱って連結しようとしてクラッシュ
という問題が Ractor 内部で発生していました。
- 未購読のイベント名に対して
PR 説明にある通り:
assigning the snapshot replaced the string subscribers defaulting map with a plain frozen Hash:
all_listeners_forconcatenates the list for the instrumented name with the other subscribers, so any name with no string subscriber returned nil and crashed the first instrumentation of an unsubscribed event inside a Ractor.
修正内容
activesupport/lib/active_support/notifications/fanout.rb
- スナップショットから
Fanoutの状態を復元する処理で、- 文字列キー用のサブスクライバ構造を 再度
Concurrent::Mapとして構築し直す - かつ、その値(配列)を
dupして元スナップショットと共有しない独立の配列にする ように変更されています。
- 文字列キー用のサブスクライバ構造を 再度
概念的には以下のようなことをしています(実際のコードは多少異なりますが、イメージ用):
def rebuild_from_snapshot(snapshot)
# snapshot[:string_listeners] は frozen Hash のような形で保持されていた
@string_listeners = Concurrent::Map.new { |h, k| h[k] = [] }
snapshot[:string_listeners].each do |name, listeners|
# 配列を dup して共有を避ける
@string_listeners[name] = listeners.dup
end
# 他のデータも snapshot から復元
end- これにより
all_listeners_for(name)が、- 未登録の
nameに対しても[](空配列)を返せる - 連結処理 (
list_for_string + list_for_other) が常に配列同士になる ため、クラッシュせずに動作します。
- 未登録の
activesupport/test/notifications_test.rb
- 上記のバグを再現し、修正を保証するテストが追加されています。
- 非メイン Ractor(サブ Ractor)で
- サブスクリプションスナップショットから
Fanoutを再構築し - 「まだ誰も購読していないイベント名」を
instrumentしても - エラーにならず正常に動作すること を確認するテストです。
- サブスクリプションスナップショットから
- 非メイン Ractor(サブ Ractor)で
- 影響範囲・注意点
- 主な影響範囲:
ActiveSupport::Notificationsを Ractor と併用しているコード
(特に、メイン Ractor 以外でもActiveSupport::Notificationsによる計測やロギングを行うようなケース)
- この PR により、
- サブ Ractor 内で「未購読イベントを初めて
instrumentしたとき」に起きていたクラッシュが解消されます。 - スナップショットからの再構築時に配列を
dupしているため、- スナップショット取得元の Ractor と
- 再構築先の Ractor
のサブスクライバ配列が共有されなくなり、副作用の影響範囲が分離されます。 - これは並行実行時の予期せぬ競合・不変性違反を防ぐ意味でも妥当な変更です。
- サブ Ractor 内で「未購読イベントを初めて
- 既存コードへの互換性:
- Public API (
ActiveSupport::Notificationsのメソッド群) には変更がないため、通常の単一スレッド/単一 Ractor 環境では挙動に変化はありません。 - もし独自に
Fanoutを直接触っている(内部 API に依存している)場合は、- スナップショットの形式や、内部で
Concurrent::Mapと配列dupが使われている前提を壊さないよう注意が必要です。
- スナップショットの形式や、内部で
- Public API (
- 参考情報 (あれば)
- 元 PR: #58060
- 本 PR はそのフォローアップとして、Ractor 向けのサブスクライバスナップショット処理の不備を修正したものです。
- 関連クラス:
ActiveSupport::Notifications::FanoutConcurrent::Map(concurrent-rubyのスレッドセーフな Map 実装)
- 実際に Ractor で Notifications を使う場合のポイント:
- メイン Ractor で設定したサブスクライバをサブ Ractor に複製している仕組みの一部がこの
Fanoutのスナップショットです。 - 本修正により、サブ Ractor でもメイン Ractor と同等に安全に
instrumentを呼び出せるようになります(未購読イベントを含めて)。
- メイン Ractor で設定したサブスクライバをサブ Ractor に複製している仕組みの一部がこの
#58385 Require rack/ractorize in ractorize!
マージ日: 2026/8/10 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
Rails のractorize!実行時にrack/ractorizeを必ず読み込むようにし、Rack 側で提供される「Ractor 対応のために公開定数を freeze する処理」を Rails アプリの Ractor 実行時に確実に有効化する変更です。これにより、非メイン Ractor 内でのリクエスト処理に必要な Rack の定数 freeze が自動的に行われます。
- 変更内容の詳細
2-1. Rails::Application#ractorize! での require 追加
railties/lib/rails/application.rb に、ractorize! 呼び出し時に rack/ractorize を読み込むコードが追加されています。
イメージとしては以下のような変更です(実際のコードは多少異なる可能性がありますが、意図としてはこれに近いです):
# railties/lib/rails/application.rb
module Rails
class Application
def ractorize!
# 新たに追加された行
require "rack/ractorize"
# 既存の ractorize! ロジック(アプリケーションを Ractor 対応にするための処理)
# ...
end
end
endrack/ractorize は、Rack 本体のコードベースではデフォルトでは frozen にできない(後方互換性の問題がある)「公開定数」を、Ractor 実行時には強制的に freeze してくれる補助的なエントリポイントです。
この PR では、Rails 側の「Ractor 対応モード」スイッチ (ractorize!) をオンにした際に、その Rack の Ractor 対応コードも自動で有効にするようにしています。
2-2. テストの微修正
railties/test/application/ractors_test.rb が 1 行追加・1 行削除というごく小さな変更を受けています。
内容としては、ractorize! 呼び出しに伴い rack/ractorize が正しく読み込まれる/動作することを前提としたテストの更新、あるいは require 周りの期待値調整が行われていると考えられます(たとえば、Ractor テストでの初期化順序や、Rack の定数の状態に関するアサーション更新など)。
- 影響範囲・注意点
対象:
Rails.application.ractorize!を利用して、Ractor 上でリクエスト処理を行う構成を試している/導入しているアプリケーション。
動作上のポイント:
ractorize!実行時にrack/ractorizeが必ずrequireされます。- これにより、Rack の「公開定数」(ヘッダー名や HTTP メソッド、その他ミドルウェア/インターフェースが参照する定数など)が freeze されることになります。
- これらの定数をアプリやミドルウェア側で「書き換え・再代入・破壊的変更」している場合は、
ractorize!実行後にFrozenErrorなどの例外が発生する可能性があります。
後方互換性について:
- Rack 本体では後方互換性の理由から、通常モードではまだ定数を freeze していませんが、Ractor 内でのスレッドセーフな動作を保証するためには freeze が必須になります。
- この PR は「Rails 側が Ractor モードに入るときだけ Rack の Ractor 用パッチを当てる」という形をとっており、通常の(Ractor を使わない)Rails アプリには基本的に影響しません。
- ただし、将来的には Rack 本体側でこれらの定数が常に frozen になる可能性が高く、その将来仕様を先取りして、Ractor 利用時にはすでに freeze された状態で動くことになります。
→ Ractor を使うか否かに関わらず、「Rack の公開定数を書き換える」というパターンは避けるのが安全です。
運用上の注意:
- Ractor ベースの並列化を試す前に、アプリ・ライブラリ・ミドルウェアなどが Rack の定数を変更していないかを確認する必要があります。
- 特に古いミドルウェアや独自実装で、
Rack::METHODS << "CUSTOM"のような「破壊的変更」を行っている場合は、別の仕組みに移行する必要があります(独自定数を自前で管理するなど)。
- 参考情報 (あれば)
この PR の背景となる前提:
- Ruby の Ractor はオブジェクト共有に厳格な制約があるため、共有されるオブジェクト(特に定数でぶら下がる構造物)は immutability(freeze)が強く求められます。
- Rack は Web サーバーとアプリをつなぐ基盤レイヤーのため、その公開定数が Ractor セーフであることは、Rails に限らず他フレームワークにとっても重要です。
関連しそうなトピック/キーワード:
- Rack の Ractor 対応 (
rack/ractorize) - Rails の Ractor サポート (
Rails.application.ractorize!) - Frozen オブジェクト / Frozen constants と後方互換性
- 非メイン Ractor 上でのリクエスト処理・並列化戦略
- Rack の Ractor 対応 (
#58426 Deprecate create alias of insert in connection adapters
マージ日: 2026/8/10 | 作成者: @kamipo
- 概要 (1-2文で)
Railsのコネクションアダプタにおいて、SQLのINSERTを実行するメソッドinsertのエイリアスとして提供されていたcreateが非推奨(deprecate)になりました。今後はcreateではなくinsertを直接呼び出すことが推奨されます。
- 変更内容の詳細
目的
- ActiveRecordの低レベルAPIにおけるDMLメソッドを、SQLの用語 (
insert,update,delete) と明確に対応させるため。 createという名前は、create_table,create_databaseなどのDDLを連想させる一方で、実際にはINSERTを行うため、名前が誤解を生みやすいことが理由です。createはinsertの単なるエイリアスであり、insertを直接使えば機能的に全く困らないため、エイリアスを廃止する方向に切り替えています。
実際の変更点
コード差分は小さいですが、主に以下の3点です。
非推奨の告知を追加
activerecord/CHANGELOG.mdに「insertのエイリアスであるcreateが非推奨になった」旨が追記されました。- 将来のメジャーバージョンで削除される可能性が高いことを示唆します。
データベースステートメントAPIへのdeprecationマーク
activerecord/lib/active_record/connection_adapters/abstract/database_statements.rbに、createメソッドの非推奨を示すコメントまたはdeprecate呼び出しが追加されています。実際の既存コードイメージとしては、概ね以下のような形になっていると考えられます(簡略化例):
rubymodule ActiveRecord module ConnectionAdapters module DatabaseStatements # 旧: def create(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) # insert(sql, name, pk, id_value, sequence_name, binds) # end # 今後は create は非推奨で、内部的には insert を呼ぶだけ def create(*args) ActiveSupport::Deprecation.warn(<<~MSG) `create` is deprecated and will be removed in a future version. \ Use `insert` instead. MSG insert(*args) end end end end※正確なメッセージ文言や実装はPR本体に依存しますが、趣旨は上記の通りです。
テストコードの修正
activerecord/test/cases/adapter_test.rbactiverecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rbactiverecord/test/cases/database_statements_test.rbなどで、create呼び出しをinsertに変更し、適宜テスト期待値を調整しています。- もともと
createを使っていたテストは、今後推奨されるAPIであるinsertに置き換えられました。
- 影響範囲・注意点
影響を受ける可能性があるコード
- ActiveRecordの低レベルAPIとして、アダプタ経由で直接SQLを発行しているコードのうち、
connection.create(...)を使っている箇所
- 典型例:
- 自前でSQL INSERTを流しているレイヤー
- サードパーティgemの中で、アダプタに対して
createを呼んでいるもの
通常のActiveRecordモデルの Model.create(...) / Model.create! とは別物であり、こちらは今回の変更の対象ではありません。影響を受けるのは、ActiveRecord::Base.connection 系の直接呼び出しだけです。
実務上の対応
コード検索
プロジェクト全体で
".create("ではなく、アダプタのcreateを使っている箇所を検索します。例えば:rubyActiveRecord::Base.connection.create( ... ) some_connection.create( ... )
insertへの置き換え以下のようなコードがあれば:
rubyconnection.create("INSERT INTO users (name) VALUES ('alice')")これを以下のように変更します:
rubyconnection.insert("INSERT INTO users (name) VALUES ('alice')")引数シグネチャも
insertと同じだったため、基本的にはメソッド名の置き換えのみで対応可能です。
警告の監視
- Railsのログやテスト実行時に、
create is deprecated; use insert insteadなどのdeprecation warningが出ていないか確認し、見つかった箇所を順次修正していくと安全です。
- Railsのログやテスト実行時に、
将来的な削除リスク
- このPRは「非推奨化」であり、即時削除ではありませんが、次のメジャーバージョン(例: Rails 8, 9 等)で
createメソッド自体が削除される可能性が高いです。 - レガシーコードや外部gemが
createに依存していると、そのタイミングでNoMethodErrorになるため、今のうちにinsertへ移行しておくのが無難です。
- 参考情報 (あれば)
関連するAPIドキュメント(現行バージョンの例):
ActiveRecord::ConnectionAdapters::DatabaseStatements#insert
https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/DatabaseStatements.html#method-i-insert
用語整理:
- DML (Data Manipulation Language):
INSERT,UPDATE,DELETEなど、テーブル内のデータ操作 - DDL (Data Definition Language):
CREATE TABLE,ALTER TABLE,DROP TABLEなど、テーブルやスキーマの定義変更
このPRは、「DMLメソッドをDML由来の名前に揃え、DDLを連想させるcreateという紛らわしい名前を撤廃する」という文脈です。
- DML (Data Manipulation Language):
#58428 Fix LengthValidator crash with proc minimum and nil value
マージ日: 2026/8/10 | 作成者: @ousamabenyounes
- 概要 (1-2文で)
ActiveModel::Validations::LengthValidatorにおいて、:minimumや:isに Proc を指定し、かつ検証対象の値がnilのときにNoMethodErrorで落ちるバグを修正した PRです。Proc を使った長さバリデーションでも、常に数値が I18n に渡るようにし、例外なく動作するようになりました。
- 変更内容の詳細
問題の挙動
次のように length バリデーションで :minimum に Proc を渡し、対象属性が nil のときに例外が発生していました。
class Foo
include ActiveModel::Validations
validates :bar, length: { minimum: ->(record) { record.one + 2 } }
def one = 1
def bar = nil
end
Foo.new.errors.messages
# => NoMethodError: undefined method 'one' for an instance of Hash原因は以下のロジックにあります(元コードのイメージ):
CHECKS.each do |key, validity_check|
next unless check_value = options[key]
if !value.nil? || skip_nil_check?(key)
check_value = resolve_value(record, check_value)
next if value_length.public_send(validity_check, check_value)
end
errors_options[:count] = check_value
endvalueがnilで、keyが:minimum/:isの場合skip_nil_check?(key)はfalse(trueになるのは:maximumのみ)- そのため
if !value.nil? || skip_nil_check?(key)の中に入らない - 結果として
resolve_valueが呼ばれず、check_valueには「Proc のまま」が残る - その
check_valueがerrors_options[:count]にそのまま入る - I18n メッセージの補間時に
countが Proc のまま渡り、ハッシュを引数に Proc が呼び出されてNoMethodErrorになる(recordではなくオプションハッシュが渡される)
修正内容
上記の問題を回避するために、check_value の解決タイミングを変更しています。
CHECKS.each do |key, validity_check|
next unless check_value = options[key]
+ check_value = resolve_value(record, check_value)
+
if !value.nil? || skip_nil_check?(key)
- check_value = resolve_value(record, check_value)
next if value_length.public_send(validity_check, check_value)
end
errors_options[:count] = check_value
endポイント:
next unless check_value = options[key]の直後にresolve_value(record, check_value)を呼ぶように変更- これにより:
- Proc が渡されていた場合は、
recordを引数に 必ず一度だけ 実行され、整数などの具体値に変換される - すでに整数等の具体値であれば
resolve_valueは冪等 (idempotent) なので挙動は変わらない
- Proc が渡されていた場合は、
valueがnilで:minimum/:isの場合でも、errors_options[:count]には常に「数値などの完成した値」が入るため、I18n 側に Proc が漏れ出さない
テスト追加
次の 2 パターンの回帰テストが追加されています(nil 値かつ Proc 指定で落ちていたケース):
:minimumに Proc を渡した場合:isに Proc を渡した場合
どちらも、修正前は NoMethodError となり、修正後は Active Model のテストスイート全体でエラーなくパスしています。
- 影響範囲・注意点
- 影響を受けるのは、
validates :attr, length: { minimum: ->(record) { ... } }やis: ->(record){ ... }を使っていて、かつ値がnilになり得るケース です。- これまで: その状況で
errorsを参照するとNoMethodErrorで落ちる - 今後: 例外なく、通常どおりバリデーションエラーが構築される
- これまで: その状況で
:maximumについては元々skip_nil_check?の対象であり、valueがnilでもチェックロジックに入る設計だったため、今回の修正で挙動は変わりません。- ただし実装上は
maximumに対しても「早い段階でresolve_valueする」ようになっており、Proc があれば必ず Record を引数に解決されます(以前と同じく 1 回だけ)。
- ただし実装上は
:minimum/:maximum/:isに 整数リテラルや Symbol を渡しているコードの挙動は基本的に変化しません。- 整数:
resolve_valueはそのまま返すだけ - Symbol: 既存の処理フローで正しくメソッド呼び出しなどに解決されており、今回の変更では壊さないように配慮されています
- 整数:
- パフォーマンス面では、
CHECKSループ内でresolve_valueする位置が変わっただけで、呼び出し回数は「チェックごとに 1 回」のままです。
運用上の注意:
- もし既存コードで「
:minimum/:isに渡した Proc が、nilの場合は絶対に呼ばれないという前提」に依存した実装をしていた場合、その前提は崩れます。- 例:
valueがnilのときには Proc 内で副作用を起こしたくない、など - もっとも、そんな前提で書かれることはほぼ想定されておらず、一般的には安全な変更と考えられます。
- 例:
- 参考情報 (あれば)
- 対応する Issue: #40642 —
LengthValidatorが Proc 指定の:minimum/:isとnil値でNoMethodErrorになる問題 - 変更ファイル:
activemodel/lib/active_model/validations/length.rbactivemodel/test/cases/validations/length_validation_test.rbactivemodel/CHANGELOG.md(今回のバグ修正が追記)
この PR により、Proc ベースの動的な長さバリデーションが nil 値を含むケースでも安全かつ一貫して利用できるようになっています。
#58328 Allow ActiveSupport::ProxyLogger to ignore messages by pattern
マージ日: 2026/8/10 | 作成者: @federico-carrocera
- 概要 (1-2文で)
ActiveSupport::ProxyLogger に「メッセージ内容でログを無視する」機能が追加され、正規表現や文字列パターンで特定のログだけをサイレンスできるようになりました。これにより、ログレベルを上げずに特定のノイズログのみを抑制できます。
- 変更内容の詳細
新機能: ignore パターンによるフィルタリング
これまで ProxyLogger は「severity(ログレベル)」でしかフィルタできず、ある gem のうるさいメッセージを消したい場合、ログレベルを上げて他の低レベルログもまとめて捨てる必要がありました。
この PR により、メッセージ内容に応じて無視するパターンを指定できます。
SomeLibrary.logger =
ActiveSupport::ProxyLogger.new(Rails.logger)
.ignore(/Noisy/)ignoreに渡せるものRegexp… メッセージに対して正規表現マッチString… メッセージに対してリテラル一致(文字列そのものを含むか、等しいかなど、実装的にはRegexp.unionでまとめられるので「その文字列を含むか」を見る形)
PR 説明から:
- 「正規表現はメッセージに対してマッチ」
- 「文字列はリテラルとしてマッチ」
- 複数パターンは
Regexp.unionで 1つの正規表現にまとめてコンパイルされる(登録時に実行)
ProxyLogger 側の挙動は以下の流れになります:
- まず従来通り、ログレベル(severity)のチェックを行う
- レベル条件を満たした場合にのみ、ignore パターンによるマッチ判定を行う
- パターンにマッチしたメッセージは、そのまま破棄され、下流の logger には渡されない
add メソッドのシグネチャ変更
Logger#add の呼び出し方法も変更されています。
- 以前:
def add(...)のように可変長引数 (...) を受けて、元の logger に丸投げ - 変更後:
Logger#addと同じ明示的なシグネチャを取るように変更
Ruby 標準 Logger の add シグネチャに合わせる形になっています:
def add(severity, message = nil, progname = nil, &block)
# ...
end理由:
- メッセージ内容を ignore パターンにマッチさせる必要があるため
messageとblockのどちらでメッセージが供給されるかを解決(評価)してからマッチングする必要がある
→ 「メッセージを解決してからでないとパターンマッチできない」ので forward ではなく明示的に受ける形に変更
テスト (activesupport/test/proxy_logger_test.rb) では、以下のようなケースがカバーされていると考えられます(PRの説明から推測される代表的パターン):
- severity は通過するが message が ignore パターンにマッチ → ログが出力されない
- 複数の ignore パターンを設定した場合に、union された正規表現でマッチする
- ignore パターンを設定していない ProxyLogger は従来通りの挙動(パフォーマンス劣化なし)
- 影響範囲・注意点
ログフィルタリングがより細かく可能
- gem や外部ライブラリの「一部のうるさいログだけ」抑制できる
- 例: 永続的に出る deprecation warning / noisy poll ログなど
既存コードへの互換性
ActiveSupport::ProxyLoggerを通常通り使っているだけであれば、ignore パターン機能を使わない限り挙動は変わりません。- パターンマッチは severity チェック後に行われるため、severity で弾かれるログについて追加のオーバーヘッドは発生しません。
- ProxyLogger の
addを独自に呼び出したり、継承してシグネチャに依存した実装をしている場合は、Logger#addと同じシグネチャに合わせる必要があります。
パフォーマンス
- パターンは登録時に
Regexp.unionで 1つの正規表現にコンパイルされるため、複数パターン指定時でも実行時のマッチングコストは最小限。 - ignore パターンを使わない ProxyLogger インスタンスは従来通りで、追加のマッチ処理は行われません。
- パターンは登録時に
メッセージの解決タイミング
- ブロック (
&block) でメッセージを渡した場合も、パターンマッチのために評価されます。 - ignore で弾かれた場合は最終的にはログ出力されませんが、「ブロックの評価コスト」自体はかかる点に注意が必要なケースもあります(高コストなログ組み立てを block で遅延している設計など)。
- ブロック (
- 参考情報 (あれば)
変更ファイル:
activesupport/lib/active_support/proxy_logger.rb- ignore パターンの追加ロジック、および
addシグネチャの更新
- ignore パターンの追加ロジック、および
activesupport/test/proxy_logger_test.rb- ignore 機能と後方互換性に関するテスト
activesupport/CHANGELOG.md- 新機能としてのエントリが追加
利用イメージ(まとめ):
# 例: 特定の noisy メッセージだけを抑制したい
noisy_logger = ActiveSupport::ProxyLogger.new(Rails.logger)
.ignore(/deprecated .*foo/)
.ignore("Polling every 1s")
SomeLibrary.logger = noisy_loggerこのように、ログレベルを変更せずに、必要なメッセージは残しつつ特定のノイズだけ除外できるようになります。
#58335 Add test coverage for ActiveModel::Type::Binary
マージ日: 2026/8/10 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveModel::Type::Binaryに対して不足していたテストが追加され、type/binary?/changed_in_place?/Data#hexなどを含めてファイル全体が行カバレッジ100%になりました。挙動変更はなく、純粋にテスト補強のみのPRです。
- 変更内容の詳細
このPRで追加されたのは activemodel/test/cases/type/binary_test.rb のテストコード 38行のみです。目的は ActiveModel::Type::Binary および内部クラス Binary::Data の未到達分岐をすべてカバーすることです。
カバーされるようになったポイント
typeとbinary?のテスト追加テスト:
test_type_and_binary_predicatetypeメソッドが:binaryを返すことbinary?がtrueを返すこと
を明示的に検証しています。これにより、Binary 型がフレームワーク内部の「バイナリ型」として正しく識別されることが保証されます。
イメージ的なコード例:
rubytype = ActiveModel::Type::Binary.new assert_equal :binary, type.type assert type.binary?serializeの戻り値の形のテスト追加テスト:
test_serialize_returns_binary_data検証しているポイント:
nilをシリアライズするとnilのまま返るnil以外の値はActiveModel::Type::Binary::Dataでラップされて返る
想定される振る舞いのイメージ:
rubytype = ActiveModel::Type::Binary.new assert_nil type.serialize(nil) data = type.serialize("abc") assert_instance_of ActiveModel::Type::Binary::Data, datacastのData分岐のテスト追加テスト:
test_cast_binary_data_returns_the_underlying_stringこれまで未到達だった分岐:
rubydef cast(value) if value.is_a?(Data) value.to_s # <- ここ else ... end endをテストするために、
- 一度
serializeしてBinary::Dataオブジェクトにする - それをもう一度
castに渡す
というラウンドトリップで
Dataブランチを通しています。イメージ:
rubytype = ActiveModel::Type::Binary.new serialized = type.serialize("abc") # => Binary::Data casted = type.cast(serialized) # => "abc" (バイナリ文字列) assert_equal "abc".b, casted # 例: エンコーディングも含めて確認これにより「
Binary::Dataをcastすると、中身の生のバイナリ文字列が返る」という仕様がテストで固定されます。- 一度
changed_in_place?のテスト追加テスト:
test_changed_in_placechanged_in_place?は、値がインプレースで変更されたかどうかを判定するためのフックで、ActiveModel / ActiveRecord の dirty tracking で使われます。テスト内容:
- 「デシリアライズ済みの古い値」と「新しい値」を比較し、
- 中身が異なれば
true - 同一なら
false
- 中身が異なれば
- となることを確認
例のイメージ:
rubytype = ActiveModel::Type::Binary.new old = type.deserialize(type.serialize("foo")) # -> "foo" (バイナリ) new_diff = "bar".b new_same = "foo".b assert type.changed_in_place?(old, new_diff) refute type.changed_in_place?(old, new_same)これにより Binary 属性が変更されたかどうかの判定ロジックがテストで保証されます。
- 「デシリアライズ済みの古い値」と「新しい値」を比較し、
Binary::Data#hexと文字列変換のテスト追加テスト:
test_data_hex_and_string_conversionテストしているのは以下:
- 非UTF-8バイト列に対する
Data#hexが期待どおりの16進文字列を返すこと to_s/to_strが元のバイト列を正しく返すこと- 生の文字列と
Binary::Dataの比較が意図通り動作すること
例えば:
rubyvalue = "\xFF\x00\x01".b data = ActiveModel::Type::Binary::Data.new(value) assert_equal "ff0001", data.hex assert_equal value, data.to_s assert_equal value, data # == がオーバーロードされている場合の比較などこれにより、
Binary::Dataをログ出力やデバッグ時に16進表現で扱うケース、または「生文字列との比較・互換性」がテストで支えられます。- 非UTF-8バイト列に対する
- 影響範囲・注意点
影響範囲
- 実装コードには一切変更がなく、
activemodel/test/cases/type/binary_test.rbの追加のみです。 - そのため、既存アプリケーションの挙動に影響はありません。
active_model/type/binary.rbの行カバレッジが 25/31 → 31/31 に向上し、Binary 型まわりの挙動がテストでより強固に保証されるようになります。
- 実装コードには一切変更がなく、
注意点
- 挙動を変更していないため、Rails のアップデートによって Binary 型の仕様が変わることはありません。
- 逆に言えば、今回テストで固定された挙動 (
Data#hexの形式、changed_in_place?の比較ロジックなど) を将来的に変えたい場合は、これらのテストを更新する必要があります。 - CI 上では
activemodel/test/cases/全体で1199 runs, 5375 assertions, 0 failures, 0 errorsとなっており、RuboCop も問題なしとのことなので、品質面のリスクはほぼありません。
- 参考情報 (あれば)
- 対象クラス:
ActiveModel::Type::Binary,ActiveModel::Type::Binary::Data - 主な関心メソッド:
#type/#binary?#serialize#cast#changed_in_place?Binary::Data#hex,Binary::Data#to_s,Binary::Data#to_str
- PR番号: https://github.com/rails/rails/pull/58335
#58414 [8-1-stable] Warn when image_processing 2.x is missing ruby-vips or mini_magick
マージ日: 2026/8/10 | 作成者: @lazerg
- 概要 (1-2文で)
Rails 8.1 系でimage_processing2.x を使う際にruby-vipsやmini_magickが未インストールだと、アプリ起動時に例外で落ちてしまう問題を「警告ログを出して処理を続行する」本来の挙動に戻すバックポート対応です。mainブランチに既に入っていた例外メッセージのマッチ条件を8-1-stableに反映しています。
- 変更内容の詳細
問題の発生条件
- Rails: 8.1.3.1(
8-1-stable系) - Gem 構成:
gem "image_processing", "~> 2.0"ruby-vipsなし、またはmini_magickなし
この状態でアプリを起動すると、initializer 実行中 (run_initializers) に以下のような LoadError でアプリが落ちるケースがあります。
LoadError: ImageProcessing::Vips requires the ruby-vips gem. Please add `gem "ruby-vips", "~> 2.0"` to your Gemfile. (LoadError)
image_processing-2.0.2/lib/image_processing/vips.rb:5
activestorage-8.1.3.1/lib/active_storage/transformers/vips.rb:10
activestorage-8.1.3.1/lib/active_storage/engine.rb:101Rails 側ではもともと Active Storage エンジン内でこの LoadError を rescue し、**「警告ログを出して起動は継続する」**設計でしたが、次のような齟齬が起きていました。
- Active Storage エンジンの
rescueは、例外メッセージのcaseマッチで以下のようなパターンしか見ていなかった:/libvips//image_processing/
- 一方、
image_processing2.x ではruby-vips/mini_magickをソフト依存にし、不足時には以下のように 不足している gem 名を明示したメッセージ を出すようになった:textImageProcessing::Vips requires the ruby-vips gem. ... ImageProcessing::MiniMagick requires the mini_magick gem. ... - このため、
LoadErrorのメッセージにはruby-vips/mini_magickが含まれるが、Active Storage 側はそれをパターンに含めておらずマッチに失敗 →LoadErrorが再スローされ、起動失敗となっていた。
さらに、8.1.3.1 で transformers/vips.rb に require "image_processing/vips" を eager に追加したことで、この問題が起動時に必ず表面化するようになりました。
この PR の対応内容
main ブランチの #57403 (commit: 244549b36b) で既に追加されていた以下のパターンマッチを、8-1-stable にバックポートしています。
- 例外メッセージに
/ruby-vips/を含む場合の分岐 - 例外メッセージに
/mini_magick/を含む場合の分岐
これにより、image_processing 2.x が出す以下のメッセージに対しても、Active Storage エンジン側が正しく反応できるようになります。
ImageProcessing::Vips requires the ruby-vips gem. Please add `gem "ruby-vips", "~> 2.0"` to your Gemfile.
ImageProcessing::MiniMagick requires the mini_magick gem. Please add `gem "mini_magick", "~> 5.0"` to your Gemfile.おおまかな処理イメージ(擬似コード)
# activestorage/lib/active_storage/engine.rb 内のイメージ
begin
# vips / mini_magick のトランスフォーマをロード
require "image_processing/vips"
# ...
rescue LoadError => error
case error.message
when /libvips/, /image_processing/, /ruby-vips/, /mini_magick/
# 対応するバックエンド用の gem がないだけなので、致命的エラーにはせず警告を出す
ActiveSupport::Deprecation.warn(<<~MSG)
... (不足している gem のインストールを促すメッセージ) ...
MSG
else
# それ以外の LoadError は従来通りエラーとして再スロー
raise
end
endPR では上記のうち /ruby-vips/ と /mini_magick/ を見る分岐が追加されています(実コードはもう少し細かいですが、意図としてはこのイメージです)。
変更ファイル
activestorage/CHANGELOG.md- 上記挙動の修正が 8.1 系の CHANGELOG に追記されています。
- ただし
mainにある「Gemfile と app generator の変更」については、8.1 既存アプリには不要なため触れていません。
activestorage/lib/active_storage/engine.rbLoadErrorを rescue している箇所に/ruby-vips/と/mini_magick/のマッチ処理が追加。
- 影響範囲・注意点
影響範囲
- 対象:
- Rails 8.1 系 (
8-1-stable) で Active Storage を利用しているアプリ image_processing2.x を利用しており、ruby-vipsまたはmini_magickを入れていない環境
- Rails 8.1 系 (
- 挙動の変化:
- これまではアプリ起動時に
LoadErrorで落ちていたケースが、- 起動は成功する
- ログに「対応する gem が不足している」旨の警告が出る という本来意図されていた挙動に戻ります。
- これまではアプリ起動時に
起こりうる実務上の違い
- この PR 導入後も:
ruby-vips/mini_magickがない限り、そのバックエンドを使った画像変換は当然できません。- ただし「そのバックエンドが使えない」ことは警告レベルに留まり、アプリ全体のブートが失敗することはなくなる、という整理です。
- 本番環境などで「今まで起動時に落ちて気づけていたものが、警告ログだけになってしまう」ことに注意してください。
- ただしもともとの設計意図も「ソフト依存なので警告にとどめる」ため、この振る舞いが正です。
テストについて
image_processing2.x + 依存 gem 不足 という状態を CI の Gemfile で再現しづらく(現行 Gemfile はimage_processing1.x +ruby-vips同梱)、自動テストは追加されていません。- 手動での再現手順:
- Rails 8.1.3.1 のアプリを用意
- Gemfile に
gem "image_processing", "~> 2.0"を追加 ruby-vips(またはmini_magick)を Gemfile から外す- アプリを起動
- 修正前: 起動時に
LoadErrorで落ちる - 修正後: 起動し、ログに警告が出る
- 参考情報 (あれば)
- 該当 PR:
- 修正対象 Issue:
- 元となった
mainブランチ側の PR: image_processing2.x の依存関係変更(ソフト依存化):ruby-vipsおよびmini_magickは必須ではなくなり、利用時にだけ必要 → 不足時はLoadErrorを投げて不足 gem 名をメッセージで案内する仕様。
#58427 Use bind parameters for array-form arguments in find_by_sql / count_by_sql
マージ日: 2026/8/9 | 作成者: @kamipo
- 概要 (1-2文で)
find_by_sql/count_by_sqlに配列形式の引数を渡したとき、これまで文字列に埋め込み(sanitize_sql)されていた値を、whereと同様にバインドパラメータとして扱うように変更した PR です。これによりドキュメントどおりwhereと同等の API 挙動になり、SQL インジェクション耐性とプレースホルダ処理が統一されました。
- 変更内容の詳細
2-1. これまでの問題点
find_by_sql / count_by_sql は次のような「配列形式」の呼び方ができるとドキュメントに記載されていました:
Post.find_by_sql(["SELECT * FROM posts WHERE id = ?", 1])
Post.count_by_sql(["SELECT COUNT(*) FROM posts WHERE id = ?", 1])ドキュメント上は where と同じように「? にバインドされる」ことが約束されていましたが、実装では以下のような挙動になっていました。
["... ? ...", value1, value2, ...]という配列をsanitize_sqlに渡し、
→ その時点で Ruby 側で SQL 文字列に埋め込んでしまう(eager interpolation)- その結果、バインドパラメータではなく“既に展開された SQL 文字列”として扱われる
where での配列形式:
Post.where(["id = ?", 1])は内部的に Arel のバインドパラメータ($1 / ? など DB アダプタ依存)として処理されますが、find_by_sql / count_by_sql では同じ書き方でも実際は文字列連結に近い扱いになっており、API ドキュメントの「where と同等」という説明と挙動が食い違っていました。
2-2. 今回の対応内容
この PR では、配列形式の find_by_sql / count_by_sql 引数を、where と同じルートでバインドパラメータとして扱うように変更しています。
主な変更点は以下のとおりです。
2-2-1. Sanitization#bound_sql_literal_for の追加
activerecord/lib/active_record/sanitization.rb に以下のようなメソッドが追加されています(名前からの要約):
Sanitization#bound_sql_literal_for(...)build_where_clauseなどと共通で使用する「プレースホルダのディスパッチ処理」をここに集約- 値を「即座に文字列として埋め込む」のではなく、
Arel::Nodes::BoundSqlLiteralを介して「バインド可能な SQL リテラル」として扱う
これにより、where で使っている「値を Arel のノードとして束縛するロジック」と、find_by_sql / count_by_sql での配列形式処理が共通化されました。
2-2-2. find_by_sql / count_by_sql の配列引数ルートの変更
activerecord/lib/active_record/querying.rb などで、配列形式の引数の扱いが次のように変更されています(擬似コード的なイメージ):
変更前 (イメージ)
def find_by_sql(sql, binds = [])
# sql が配列形式 ["SELECT ... WHERE id = ?", 1] の場合
sql = sanitize_sql(sql) # ここで文字列に埋め込んでしまう
# ...
connection.select_all(sql, ...)
end変更後 (イメージ)
def find_by_sql(sql, binds = [])
if sql.is_a?(Array)
# 1. 配列を BoundSqlLiteral 付きの Arel ノードとして扱う
bound_sql = bound_sql_literal_for(sql)
# 2. バインド値と SQL を connection に渡す形式に変換
sql, binds = connection.to_sql_and_binds(bound_sql)
end
connection.select_all(sql, "SQL", binds)
end実際にはアダプタや Arel 層を挟みますが、要するに:
["SELECT ... WHERE id = ?", 1]という配列をArel::Nodes::BoundSqlLiteralに包んだ形で扱い- DB アダプタレベルで正しいバインドクエリ (
SELECT ... WHERE id = $1) + バインド配列 ([1]) へ変換する
という流れに変えています。
2-2-3. build_where_clause とのプレースホルダ処理の統合
activerecord/lib/active_record/relation/query_methods.rb では、build_where_clause 周辺にあった「プレースホルダの分岐ロジック」が大幅に削られています (-36 / +1 行ほど)。
これは、もともと build_where_clause 側に散在していた「値をどうバインドするか」の判断を Sanitization#bound_sql_literal_for へ移し、find_by_sql / count_by_sql からもそれを利用できるようにしたためです。
結果として:
whereでの配列利用find_by_sql/count_by_sqlでの配列利用
が、同じ bound_sql_literal_for を経由するようになり、実装・挙動の一貫性が増しています。
2-2-4. テストの追加
activerecord/test/cases/finder_test.rb にテストが 37 行追加されています。
内容としては(推測レベルを含みますが):
find_by_sql(["SELECT ... WHERE id = ?", 1])/count_by_sql(["SELECT ... WHERE id = ?", 1])が- 正しい結果を返すこと
- 実際にバインドクエリとして扱われていること(ログ・バインド数など)
whereとfind_by_sql/count_by_sqlの挙動が一致すること
などを検証していると考えられます。
- 影響範囲・注意点
3-1. 既存アプリへの影響
対象: find_by_sql / count_by_sql を配列形式で使っているコード
# 影響を受けるパターン
Post.find_by_sql(["SELECT * FROM posts WHERE title = ?", user_input])
Post.count_by_sql(["SELECT COUNT(*) FROM posts WHERE category = ?", params[:category]])これらは、これまでは Ruby 側で文字列に埋め込まれていたが、今後は DB のプレースホルダにバインドされる ようになります。
多くのケースでは挙動は論理的に同じで、より安全になるだけですが、次のようなケースでは影響が出る可能性があります。
3-1-1. DB アダプタ固有のプレースホルダ挙動に依存していた場合
- 以前:
"id = 1"という 完全な SQL 文字列が生成されていた
- 今後:
"id = $1"+[1]のように プレースホルダ + バインド となる
DB レベルでの実行計画キャッシュやログのフォーマット、DB 側で SQL を文字列としてパースしているような特殊ツール等で違いが出る可能性はあります(ただし普通のアプリでは問題にならないことがほとんどです)。
3-1-2. 値側に「SQL として意味のある断片」を意図的に渡していた場合
たとえば以下のような「擬似的な SQL 組み立て」をしていた場合:
# ※アンチパターン寄りの例
condition = "published_at < NOW() - INTERVAL '1 day'"
Post.find_by_sql(["SELECT * FROM posts WHERE #{condition} AND id = ?", 1])ここで condition 側は依然として自前の文字列連結ですが、["... #{condition} AND id = ?", 1] の「1」の部分だけがバインドに変わります。
多くの場合問題にはなりませんが、「配列の 2 要素目以降に SQL 断片("id = 1" など)を渡し、あえて文字列展開させる」というトリッキーな使い方をしていた場合は挙動が変わります。
例:
# 以前は: "id = 1" がそのまま埋め込まれていた
Post.find_by_sql(["SELECT * FROM posts WHERE ?", "id = 1"])- 旧挙動(sanitize_sql):
"SELECT * FROM posts WHERE id = 1"に変換されて実行
- 新挙動(バインド):
"SELECT * FROM posts WHERE $1"+["id = 1"]として実行され、
→ 構文エラー となる可能性が高い($1は値であって SQL 式ではないため)
このような使い方は本来ドキュメントにも沿っておらずアンチパターンですが、もし行っていた場合は修正が必要です。
3-2. セキュリティ観点
- 配列形式引数を使う場合、値が文字列連結ではなくバインドされるため、SQL インジェクションリスクが減少・もしくは顕在化しにくくなる 方向の変更です。
- 既に
whereで慣れている安全な書き方をfind_by_sql/count_by_sqlでも素直に使えるようになった、と捉えてよいです。
3-3. パフォーマンスへの影響
- バインドパラメータを使う方が、DB アダプタによってはクエリキャッシュやプランキャッシュが効きやすくなる可能性があります。
- 一方で「SQL を毎回文字列として作る」よりも、バインド付きクエリを経由する実装になったため、内部的に若干のオーバーヘッドは増えるかもしれませんが、通常の Rails アプリでは誤差レベルと考えられます。
- 参考情報 (あれば)
関連する Rails API ドキュメント(参考):
ActiveRecord::Base.find_by_sql
https://api.rubyonrails.org/classes/ActiveRecord/Base.html#method-c-find_by_sqlActiveRecord::Base.count_by_sql
https://api.rubyonrails.org/classes/ActiveRecord/Base.html#method-c-count_by_sqlActiveRecord::QueryMethods#where
https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-where
この変更後は、「where で使える配列形式 (["col = ?", val]) は find_by_sql / count_by_sql でも同じように“バインドされる”」と考えて差し支えなくなります。
#58425 Stop deserializing column defaults
マージ日: 2026/8/9 | 作成者: @byroot
- 概要 (1-2文で)
Rails 8.1.0で導入された「カラムのデフォルト値をあらかじめ型変換(deserialize)する」挙動をやめて、再び「デフォルト値は生の値のまま扱う」方向に戻したPRです。
これにより、後から属性タイプが上書きされるケースでデフォルト値の型不整合が起きる問題(#58292)を解消しています。
- 変更内容の詳細
背景
- Rails 8.1.0 で、以下の Issue への対応として「カラム定義時にデフォルト値を type_cast(deserialize)しておく」変更が入った:
- しかしこの変更によって、
- モデルの attribute type が後から
attribute,attribute :foo, :integerのように上書きされる場合や - PostgreSQL, SQLite3 などで DB の型とアプリ側の型定義が変わる場合 に、「すでに古い型で変換済みのデフォルト値」が新しい型に変換できずエラーや不整合が発生するようになりました(例: boolean → integer への変更など)。
- モデルの attribute type が後から
このPRは、その「カラム定義時にデフォルト値を deserialize する」振る舞いを止め、従来どおり DB から取得した生のデフォルト値を保持し、必要なタイミングで現在の型に応じて変換する動作に戻しています。
実際の変更点の方向性
コード差分は少ないですが、意味合いとしては次のような変更が行われています。
1. ActiveRecord::ConnectionAdapters::Column のデフォルト値の扱いを修正
- 以前の 8.1.0 変更で、
Columnが初期化されるタイミングで「型に応じてdefaultを type_cast する」処理が入っていました。 - このPRで、その事前 type_cast をやめ、カラム定義としては「DB から読み出したままの値」を保持する形に戻しています。
概念的には:
# 8.1.0 問題のある状態(イメージ)
def initialize(name, default, sql_type, ...)
@sql_type = sql_type
@default = cast_type.deserialize(default) # ここで早期に deserialize していた
end
# このPR後の状態(イメージ)
def initialize(name, default, sql_type, ...)
@sql_type = sql_type
@default = default # 生のまま保持
end実際の実装はもう少し細かいですが、ニュアンスとしては「Column オブジェクトでは serialize/deserialize しない」に戻しています。
2. SQLite3 アダプタでのデフォルト値取得の調整
activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rbで、SQLite3 のメタデータからデフォルト値を読み出す部分が微調整されています。- ここでも「Rails 側でデフォルトを過度に解釈・変換しない」ように寄せています。
- 例えば
DEFAULT 0やDEFAULT '0'などを DB がどう返すかに応じて、そのまま Column に渡し、実際の値の型処理は Attribute API 側に任せる形です。
3. テストの更新・追加
いくつかのテストファイルが更新されています:
attributes_test.rbに 16 行追加
→ 属性タイプの上書きとデフォルト値の挙動に関する回 regress テストが追加されています。
典型的には次のような状況をカバーしていると考えられます:rubyclass User < ActiveRecord::Base # DB 上は boolean カラムで default false # 後からアプリ側で整数として扱いたいケース attribute :flag, :integer end # new の際に「boolean として deserialized 済みの false」を # 再度 integer にキャストしようとして失敗、のような問題が起きないことを確認defaults_test.rbなどの「デフォルト値」のテスト群で、
Rails が DB のデフォルトをどう扱うかについて期待値を修正。PostgreSQL
money_test.rbや Migration 関連テスト (change_schema_test.rb,columns_test.rb,migration_test.rb) でも、
型付きカラム + デフォルト値の振る舞いが、新実装に合わせて微調整されています。test/schema/schema.rbの 1 行追加は、回 regress テスト用にスキーマ定義を少しだけ変更したものと思われます(型変更などのシナリオを再現するため)。
- 影響範囲・注意点
影響するケース
- Rails 8.1.0 以降において、以下のようなコードを書いているアプリで影響が出ていた可能性があります:
- DB カラム定義の型と、Rails モデル側の attribute type が異なっている(または後から上書きしている)
- 例: DB:
booleanカラム、モデル:attribute :flag, :integer - 例: DB:
stringカラム、モデル:attribute :settings, :json
- 例: DB:
- そのカラムにデフォルト値が設定されている
- 新規レコード作成時に「デフォルト値を起点にした型変換のエラー」や「予期せぬ値」が出ていた
- DB カラム定義の型と、Rails モデル側の attribute type が異なっている(または後から上書きしている)
このPRにより、そうした問題は解消される可能性が高いです。
互換性の観点
- 8.1.0 の「事前 deserialization」挙動に依存したコードを書いていた場合は、再び「従来の動き(DB 生値ベース)」に戻るため、挙動が変わります。
- ただし、8.1.0 で新しい挙動を前提にしたコードを書くのは難しく、かつ今回のPR自体がバグ修正扱いであることから、ほとんどのアプリにとっては「不具合が直る」「元の感覚に戻る」変更になります。
注意点・設計上の示唆
- デフォルト値の扱いは「DB が持つ型」と「Rails の Attribute 型」が一致している前提で設計するのが安全です。
- もしアプリ側で attribute type を上書きする場合は、「デフォルト値もその新しい型にとって自然な値になるか」を意識する必要があります。
- 特に Mutable な型(JSON, Array, Hstore など)では、デフォルト値の共有・破壊的変更まわりでバグを生みやすいため、
- モデル側で
attribute :settings, :json, default: -> { {} }のように Proc で都度生成する - DB のデフォルトは
NULLにして、アプリ側で初期値を与える
といった方針を検討した方がよいです。
- モデル側で
- 参考情報 (あれば)
- このPRで修正・クローズしている Issue/PR:
- 元となった変更:
- デフォルト値と型変換の挙動に関するドキュメント:
- Rails Guides: Active Record Migrations(デフォルト値、DB 依存の挙動など)
- Rails API:
ActiveRecord::Attributes,ActiveRecord::ConnectionAdapters::Column
#58350 Append TRADITIONAL instead of STRICT_ALL_TABLES to MySQL's sql_mode by default
マージ日: 2026/8/9 | 作成者: @kamipo
- 概要 (1-2文で)
Rails が MySQL に接続する際のデフォルトsql_mode設定を、STRICT_ALL_TABLES単体ではなくTRADITIONALを付与するように変更した PRです。これにより、RDS/Aurora などグローバルsql_modeが空の環境でも、ゼロ日付やゼロ除算などの不正値をサイレントに受け入れない、より厳格な動作が一貫して保証されます。
- 変更内容の詳細
何を変えたか
これまで Rails の MySQL アダプタは、接続時に sql_mode が空または特定条件のときに STRICT_ALL_TABLES を付け足していましたが、この PR で以下のように変更されています。
- 旧:
sql_modeにSTRICT_ALL_TABLESを append - 新:
sql_modeにTRADITIONALを append
TRADITIONAL は MySQL の「モードセット」で、実質的に以下を含む厳格モードです(主なもの):
STRICT_ALL_TABLESNO_ZERO_IN_DATENO_ZERO_DATEERROR_FOR_DIVISION_BY_ZERO- など、MySQL が「伝統的」な厳格動作として束ねているモード群
MySQL 5.7+ では、これら (NO_ZERO_IN_DATE, NO_ZERO_DATE, ERROR_FOR_DIVISION_BY_ZERO) がデフォルト sql_mode に含まれるようになりましたが、
Amazon RDS / Aurora MySQL の「デフォルトパラメータグループ」などではグローバル sql_mode が空のまま運用されていることがあり、その場合に Rails が STRICT_ALL_TABLES だけ追加しても、上記 3 つは有効化されないというギャップがありました。
この PR はそのギャップを埋めるためのものです。
実際のコード上の変更点(イメージ)
abstract_mysql_adapter.rb で、接続時に sql_mode を組み立てる処理がだいたい次のようなイメージに変わります(あくまで説明用の擬似コード):
# 変更前のイメージ
sql_mode = select_value("SELECT @@SESSION.sql_mode")
if sql_mode.blank?
sql_mode = "STRICT_ALL_TABLES"
else
sql_mode = "#{sql_mode},STRICT_ALL_TABLES" unless sql_mode.include?("STRICT_ALL_TABLES")
end
execute("SET SESSION sql_mode='#{sql_mode}'")
# 変更後のイメージ
sql_mode = select_value("SELECT @@SESSION.sql_mode")
if sql_mode.blank?
sql_mode = "TRADITIONAL"
else
sql_mode = "#{sql_mode},TRADITIONAL" unless sql_mode.include?("TRADITIONAL")
end
execute("SET SESSION sql_mode='#{sql_mode}'")正確な実装は PR 内の2行変更ですが、要点としては「何を足すか」が STRICT_ALL_TABLES から TRADITIONAL に変わっただけです。
テスト・ドキュメント
activerecord/CHANGELOG.md- MySQL アダプタのデフォルト
sql_mode振る舞いがTRADITIONALベースになることを記載。
- MySQL アダプタのデフォルト
activerecord/test/cases/adapters/abstract_mysql_adapter/connection_test.rb- 接続時の
sql_modeにTRADITIONALが含まれていることを検証するテストを追加。
- 接続時の
また、PR 説明にもある通り、以前の挙動(STRICT_ALL_TABLES のみを指定)を再現したい場合は、database.yml で明示的に指定できます。
production:
adapter: mysql2
database: my_app_production
username: ...
password: ...
variables:
sql_mode: "STRICT_ALL_TABLES"- 影響範囲・注意点
影響を受ける環境
- 主に以下のような環境で影響が出ます:
- グローバル
sql_modeが空、もしくは非常に緩い設定になっている MySQL サーバ - 特に Amazon RDS / Aurora MySQL のデフォルトパラメータグループをそのまま使っている環境
- グローバル
MySQL 5.7+ で、すでにサーバ側の sql_mode を TRADITIONAL 相当か、厳格なモードにしている場合は、この変更による挙動の差分はほぼありません。
具体的に変わる挙動の例
以下のようなケースで、これまでは「通っていた」ものが、今後はエラーになる可能性があります(特にテスト/本番環境で sql_mode が異なる場合に顕在化しやすいです)。
- 不正な日付値の挿入
INSERT INTO users (name, birthday) VALUES ('Alice', '0000-00-00');
INSERT INTO users (name, birthday) VALUES ('Bob', '2024-00-01');- 旧挙動 (
STRICT_ALL_TABLESのみ, かつNO_ZERO_DATEなど無効)- サイレントに
0000-00-00などが保存される、もしくは警告にとどまる。
- サイレントに
- 新挙動 (
TRADITIONAL)NO_ZERO_DATE/NO_ZERO_IN_DATEによりエラーとして扱われ、INSERT が失敗。
- ゼロ除算
SELECT 1 / 0;- 旧挙動(
ERROR_FOR_DIVISION_BY_ZEROが無効)NULLが返ってくるか、警告で済む。
- 新挙動 (
ERROR_FOR_DIVISION_BY_ZERO有効)- エラーとして扱われる。
Rails アプリ側から見ると、これまで普通に通っていた save や update が ActiveRecord::StatementInvalid などの例外を投げるようになる可能性があります。
回避・調整方法
- 以前と同じ(緩い)挙動を維持したい場合:
database.ymlで明示的にsql_modeを指定します。
production:
adapter: mysql2
variables:
sql_mode: "STRICT_ALL_TABLES" # 旧Rails相当
# あるいは、まったく独自のモードを指定してもよい
# sql_mode: ""- ただし、Rails コアチームの方針としては、
strict: falseオプションは #57077 で非推奨になっており、- 「不正な値をサイレントに受け入れない」厳格な動作が推奨されています。
そのため、よほどの互換性理由がない限りはTRADITIONALのまま運用し、アプリケーション側で不正データを生成しないように修正するのが推奨されます。
互換性上の注意点
- 既存のデータベースに
0000-00-00などの不正値が既に入っている場合、- それを更新/再保存する処理で問題が出る可能性があります。
- データクリーンアップ(不正日付の修正、正しい NULL / デフォルト値への置き換えなど)を検討してください。
- 本番とローカルで
sql_modeが異なる場合、- ローカルでは動くが本番でエラー、というずれが起こりがちです。
- Rails 側から
variables: { sql_mode: ... }を統一的に指定しておくと環境差分を減らせます。
- 参考情報 (あれば)
- この PR:
Append TRADITIONAL instead of STRICT_ALL_TABLES to MySQL's sql_mode by default(#58350)
- 関連 PR / Issue:
Deprecate strict: false option for MySQL(#57077)- MySQL アダプタの
strict: falseオプション非推奨化。
「不正な値をサイレントに許さない」方向へのポリシー変更の一環。
- MySQL アダプタの
- MySQL 公式ドキュメント:
sql_modeシステム変数TRADITIONALモードの説明(STRICT_ALL_TABLESを含むモードセットであることなど)
#58424 Keep the controller middleware proxy in sync with its stack
マージ日: 2026/8/9 | 作成者: @55728
- 概要 (1-2文で)
ActionController::Metal.middlewareが返す「ミドルウェア用プロキシ」と実際のミドルウェアスタックの状態がズレていた問題を修正し、常に最新のスタックを正しく反映するようにしたPRです。合わせて、deleteの戻り値やmiddlewares=の挙動など、従来のインターフェイスからズレていた細かい挙動も元の仕様どおりになるよう整えています。
- 変更内容の詳細
背景: 問題になっていた挙動
dd67a577d9 以降、ActionController::Metal.middleware は実体のスタックではなく「プロキシオブジェクト」を返すようになりました。
そのプロキシは 生成時点のスタックをキャプチャ しており、読み取り系のメソッド呼び出しもそのキャプチャしたスタックに対して行われていました。
その結果、次のような不整合が起きていました:
middleware = PostsController.middleware
middleware.use Mw # ここでスタックのコピーを作ってミューテートする
middleware.size # => 0 (プロキシ生成時点のスタックを見ている)
PostsController.middleware_stack.size # => 1 (実際には増えている)他にも、以下の2点の「以前の挙動とのズレ」がありました:
deleteが、ラップ対象であるスタックの戻り値ではなく「スタック自身」を返してしまう- 元々は「存在しないミドルウェアを削除しようとしたとき
nilを返す」仕様だったが、プロキシ化後はこれが崩れていた。
- 元々は「存在しないミドルウェアを削除しようとしたとき
middlewares=がプロキシ経由でなく「スタックそのもの」に直接委譲されていた- そのため、凍結済みスタックに対しては
FrozenErrorが発生してしまう状況になっていた (#58342 で是正した他メソッドと同じ問題)。
- そのため、凍結済みスタックに対しては
dd67a577d9 以前は middleware がスタックの実体だったため、上記3つはいずれも期待どおりに動いていました。
このPRでの修正点
1) プロキシが「常に最新のスタック」を見るように変更
これまでは:
- プロキシ生成時点の
@stackをキャプチャし、以降のsizeなどの読み取りもキャプチャしたものに対して行う
これを:
- プロキシの各メソッド呼び出し時に、常にコントローラの「現在の」スタックを取り直して参照する
- つまり、プロキシは「スナップショット」ではなく「現在のスタックへの窓口」として振る舞うようになる
そのため、次のようなコードで状態が正しく同期されます:
middleware = PostsController.middleware
middleware.use Mw
middleware.size # => 1
PostsController.middleware.size # => 1 (同じ状態を見ている)2) プロキシ経由のメソッドが「スタックの戻り値」を正しく返すよう修正
これまでは、プロキシがミューテーション用のメソッドを「スタックを返す」ような形でラップしており、スタック本来の戻り値が失われていました。
このPRでは:
- プロキシが対象スタックにメソッドを委譲したとき、元のメソッドの戻り値をそのまま返す ように変更
これにより、たとえば delete の戻り値が元の仕様どおりに戻ります:
# プロキシ経由
removed = PostsController.middleware.delete(NonExistingMiddleware)
removed # => nil (存在しない時は nil)
# 元のスタックでも同様の挙動
removed = PostsController.middleware_stack.delete(NonExistingMiddleware)
removed # => nil3) middlewares= もプロキシ対象に追加
以前は middlewares= がプロキシ経由ではなく、スタックに直接ディスパッチされていたため:
- 凍結済みスタックに対して
middlewares=を呼ぶとFrozenErrorが発生 - #58342 で修正した他のミューテーション系メソッド (
useなど) と挙動が不揃い
このPRで:
middlewares=を他のミューテーション系メソッドと同様、プロキシを通して処理する ようにした- これにより、
middlewares=も「コピーを作って置き換え → 再フリーズ」というパターンになり、スタックのシェア/フリーズ戦略と整合する
サンプルイメージ:
# 以前: FrozenError が起こりうる
PostsController.middleware_stack.freeze
PostsController.middlewares = [Mw1, Mw2] # FrozenError
# 今回の修正後 (プロキシがコピー・再フリーズする前提の設計)
PostsController.middleware.freeze # 内部スタックが凍結されていても
PostsController.middlewares = [Mw1, Mw2] # コピーを新たに作って差し替え可能※ 上記コードは設計意図のイメージであり、実際のAPI名/利用形態はプロジェクトのコードに依存します。
4) テストの追加
次の3ケースに対してテストが追加されています (actionpack/test/controller/new_base/middleware_test.rb):
- プロキシ経由でのミューテーションの結果が、同じプロキシを通した読み取りに反映されること
- 存在しないミドルウェアを
deleteした場合にnilが返ること middlewares=による再設定後も、スタックが「共有可能 (shareable)」な状態であること- つまり、スタックのコピー&再フリーズ戦略が壊れていないことを確認
全体として actionpack のフルテストスイート (4278 runs, 20060 assertions) はグリーンです。
- 影響範囲・注意点
- 主に影響を受けるのは、
ActionController::Metal.middlewareを直接触っているコード および拡張 (エンジン・プラグイン・ライブラリ) です。middlewareが返すオブジェクトの挙動が、以前の「実体スタック」準拠に戻るイメージで、dd67a577d9以降の一時的な挙動からは変わります。
- 具体的な注意点:
deleteの戻り値に対して、nil以外の挙動を期待したワークアラウンドが入っていた場合、それは不要・または壊れる可能性があります。middleware経由のsizeなどを使っており、以前の「同期していない状態」を前提にしたコードがある場合 (まずないと思われますが)、挙動が変わります。middlewares=を使って独自にスタックを差し替えているコードは、この変更で 凍結/共有戦略と整合的に動くようになる ため、FrozenErrorをハンドリングしていたコードなどは削減できる可能性があります。
- APIシグネチャ自体は変わっておらず、「元々のドキュメントどおりの挙動」に近づけるバグ修正なので、通常は後方互換性の観点で有利な変更と考えてよいです。
- 参考情報 (あれば)
- 関連コミット:
dd67a577d9ActionController::Metal.middlewareをスタック実体からプロキシオブジェクトに変更したコミット。
- 関連PR: #58342
- 「凍結されたミドルウェアスタックをミューテートしようとすると
FrozenErrorになる」問題を、コピー&再フリーズ戦略で回避するための修正。
- 「凍結されたミドルウェアスタックをミューテートしようとすると
- 本PR: #58424
- 上記の設計を踏まえ、「プロキシとスタックの状態の同期」「戻り値の整合性」「
middlewares=のプロキシ化」という3点を仕上げるフォローアップ。
- 上記の設計を踏まえ、「プロキシとスタックの状態の同期」「戻り値の整合性」「
#58071 Add delete_multi method to batch deletes for memcached store if dalli's delete_multi is defined
マージ日: 2026/8/9 | 作成者: @ilianah
- 概要 (1-2文で)
Rails のActiveSupport::Cache::MemCacheStoreに、dalli がdelete_multiをサポートしている場合にそれを使って一度に複数キーを削除する機能が追加されました。これにより memcached へのラウンドトリップ回数が減り、バッチ削除時のパフォーマンスが向上します。
- 変更内容の詳細
2-1. delete_multi_entries の追加
MemCacheStore に、複数キー削除用の内部メソッド delete_multi_entries が追加されています。
趣旨としては:
- dalli クライアントが
delete_multiを実装しているならそれを利用する - 実装されていない/使えない場合は従来通り 1 件ずつ
delete_entryを呼ぶ既存の挙動にフォールバック(※この部分は既存ロジック側)
ポイント:
- dalli の
delete_multiは「キーごとの削除成否」を返さない - そのため Rails 側では「削除対象キー数」か「0」のどちらかしか返せない仕様になっている
疑似コードイメージ(実際のコード構造を簡略化):
def delete_multi_entries(entries, options)
# entries: [[key, version], [key2, version2], ...] のような形式を想定
keys = entries.map { |entry| entry.key }
if @data.respond_to?(:delete_multi)
result = instrument(:delete_multi, keys) do
@data.delete_multi(keys, expires_in: options[:expires_in])
end
# dalli の delete_multi が RingError を内部で rescue して
# 空配列を返すケースがあるため、成功判定に注意が必要
if result.blank?
0
else
keys.size
end
else
# 古い dalli 等で delete_multi が無い場合は、既存の逐次削除ロジックにフォールバック
super
end
end※上記は説明用の擬似コードです。実際には entries の構造や instrument などは Rails 内部 API に合わせて実装されています。
2-2. 戻り値仕様の変更 (mem_cache_store のみの違い)
今まで:
delete_multiは内部的に複数回delete_entryを呼んでおり、各キーごとの削除成否がわかるため、「実際に削除できたキー数」を正確に返せていました。
今回:
- dalli の
delete_multiは per-key 結果を返さないため、「成功なら要求した件数」「失敗(もしくは失敗とみなす条件)なら 0」という粗い情報しか返せません。 MemCacheStoreのテスト (test_delete_multi) はこの仕様差にあわせて上書きされています。
これにより、mem_cache_store における delete_multi の戻り値は「実際の削除件数」ではなく「期待された削除件数 or 0」になります。
2-3. エラーハンドリング (Dalli::RingError 対応)
dalli 側の実装では、バッチ削除中にサーバ障害等があっても Dalli::RingError がライブラリ内で rescue され、例外を投げずに空配列を返す 挙動になっています:
Dalli::RingErrorが起きる- dalli がそれを rescue
delete_multiは[](空配列)を返す- Rails 側の
rescue_error_withには例外が届かない
Rails のキャッシュには「失敗に対して安全である(failure safety)」というテストがあり、サーバが落ちている場合には「削除件数 0 を返す」必要があります。
そのため delete_multi_entries 側で:
result.blank?(空配列やnil)なら「失敗」とみなし 0 を返す- それ以外なら「成功」とみなし
keys.sizeを返す
という条件分岐が入っています。
- 影響範囲・注意点
対象ストア:
ActiveSupport::Cache::MemCacheStoreを使用している場合にのみ影響します。- 他のキャッシュストア(MemoryStore, RedisCacheStore など)には影響しません。
dalli のバージョン依存:
delete_multiを実装している dalli バージョンではバッチ削除が 1 回のラウンドトリップで行われ、パフォーマンス向上が見込めます。delete_multiが無い古い dalli では従来どおり 1 件ずつ削除する挙動のままです。
戻り値の違いに注意:
MemCacheStore#delete_multi(およびそれを間接的に呼び出す API)の戻り値を「実際に削除されたキー数」として厳密に扱っていた場合、今回の変更で意味が変わります。- 以前: 「実削除件数」
- 今回: 「成功とみなされたら
要求件数、失敗とみなされたら0」
- ロジック上、「一部成功/一部失敗」を検知する用途には利用できなくなります。
障害時の挙動:
- memcached クラスタ障害などで dalli が
RingErrorを内部で rescue した場合でも、アプリ側には例外が飛んでこず、「0 が返ってきて終わる」挙動になります。 - 「キャッシュ削除失敗をロギング・監視したい」場合は、Rails のログ(
instrument周り)や dalli のログ出力を別途確認する必要があります。
- memcached クラスタ障害などで dalli が
- 参考情報 (あれば)
- この PR:
- 類似 PR(
fetch_multiの高速化と思われる): - dalli に
delete_multiが追加された PR: - dalli の
delete_multi実装(Dalli::RingErrorを rescue して空配列を返す部分): - Rails の failure safety テスト(キャッシュ障害時の期待挙動):
#58358 Fix incorrect documentation
マージ日: 2026/8/9 | 作成者: @55728
- 概要 (1-2文で)
Active Record 周辺のドキュメント (RDoc と Guides) に、実際の挙動と矛盾する記述や誤ったサンプルコードがあったため、それらを実装に合わせて修正した PR です。コードロジックの変更はなく、ドキュメントのみの修正です。
- 変更内容の詳細
2-1. connected_to? の RDoc 例の修正
対象: activerecord/lib/active_record/connection_handling.rb
- 問題点:
connected_to?の使用例が、実際にはありえない戻り値を示していた。 - 実際の仕様:
connected_to?(role: ..., shard: ...)は、「現在の接続コンテキストの role と shard が、指定した引数 両方 と一致したときにのみtrue」を返す。- 従って、
connected_to(role: :reading, shard: :shard_one)ブロック内では、role: :writing を指定してconnected_to?がtrueになることはない。
誤ったイメージの例(※説明用、実際のコード断片ではない):
connected_to(role: :reading, shard: :shard_one) do
# 実際には false になるのに true と書かれていた例
connected_to?(role: :writing, shard: :shard_one) # => true (と誤記)
end正しい挙動は:
connected_to(role: :reading, shard: :shard_one) do
connected_to?(role: :reading, shard: :shard_one) # => true
connected_to?(role: :writing, shard: :shard_one) # => false
endテスト (connection_handlers_sharding_db_test.rb) でも assert_not で後者が false であることが担保されており、ドキュメント側をそれと整合する内容に修正しています。
2-2. Relation#readonly の RDoc 例の修正
対象: activerecord/lib/active_record/relation/query_methods.rb
- 問題点:
#readonlyのサンプルコードが、readonlyが 新しい Relation を返すという性質を無視していて、挙動が実際と食い違う例になっていた。 - 実際の仕様:
users.readonlyは「元の Relation (users) を変更する」のではなく「読み取り専用な新しい Relation を返す」。- 元の例では、
readonlyの戻り値を変数に再代入しておらず、users.first.saveがtrueを返すかのように書かれていたが、実際にはActiveRecord::ReadOnlyRecordが発生する。
誤ったイメージの例:
users = User.where(active: true)
users.readonly
users.first.save # => true (と誤記)実際に起きること:
users.readonlyの戻り値を無視すると、usersは依然として writable な Relation。- しかし、RDoc の文脈上は
readonlyした Relation を対象にsaveしているように説明されていたため、読み手に誤解を与えていた。
正しい使い方の例(今回の修正方向):
users = User.where(active: true)
users = users.readonly # ここで Relation を差し替える
users.first.save
# => ActiveRecord::ReadOnlyRecord が raise される#create_with の RDoc 例がすでに「戻り値を変数に再代入する」形になっていたため、それと同じスタイルに合わせて修正されています。
2-3. Associations Guide の「失敗した関連付けの保存」に関する説明修正
対象: guides/source/association_basics.md
- 対象箇所:
has_one,has_many,has_and_belongs_to_manyの 3 つのセクション。 - 問題点:
- ガイドでは、「関連を代入して保存が失敗した場合、代入式が false を返す」と説明されていた。
- しかし Ruby の代入式は常に右辺値を返し、false を返すことはない。
- 実際の挙動は、保存に失敗した場合:
has_one,has_many:ActiveRecord::RecordNotSavedが raise される。has_and_belongs_to_many:ActiveRecord::RecordInvalidが raise される。
- つまり「false を返す」のではなく、「例外が発生し、代入自体がロールバック(キャンセル)される」が正しい。
誤解を招く表現のイメージ:
If saving the assigned record fails, the assignment statement returns `false`.正しいイメージ:
user.profile = profile_params # has_one の例
# 保存に失敗した場合:
# => ActiveRecord::RecordNotSaved が raise される
# user.profile の変更もロールバックされる (代入は成立しない)ここ 3 箇所の説明を、API ドキュメントおよび 2011 年以降の実際の挙動に合わせて、「false を返す」ではなく「例外 (RecordNotSaved / RecordInvalid) が発生する」という記述に修正しています。
- 影響範囲・注意点
- この PR は ドキュメントのみの変更 であり、挙動・API の追加・非互換変更は一切ありません。
- ただし、これまで誤ったドキュメントを前提にコードを書いていた場合、以下の点で思い込みが修正される必要があります:
connected_to?- role か shard のどちらか片方だけ一致していても
trueにはならない。両方一致して初めてtrue。
- role か shard のどちらか片方だけ一致していても
Relation#readonly- インスタンスメソッドチェーンで Relation を変更するときは、
users = users.readonlyのように戻り値を必ず使う。
- インスタンスメソッドチェーンで Relation を変更するときは、
- 関連付け代入時のエラー処理
user.profile = profileなどの関連代入が失敗したときにfalse判定で分岐するコードは間違い。- 正しくは例外 (
ActiveRecord::RecordNotSaved/RecordInvalid) を rescue して処理する。 - 代入後の関連オブジェクトの状態がロールバックされることを前提にする。
- 正しくは例外 (
- 参考情報 (あれば)
- 関連クラス/メソッド:
ActiveRecord::ConnectionHandling#connected_toActiveRecord::ConnectionHandling#connected_to?ActiveRecord::Relation#readonlyActiveRecord::RecordNotSavedActiveRecord::RecordInvalid
- 実際の挙動を確認したい場合は、
connection_handlers_sharding_db_test.rbや Association 関連のテストを参照すると、今回修正された説明と同じ前提でテストが書かれていることが分かります。
#58420 Refactor MiddlewareStack::Middleware off Hash.ruby2_keywords_hash
マージ日: 2026/8/9 | 作成者: @kamipo
- 概要 (1-2文で)
Rails のミドルウェアスタック内部実装からHash.ruby2_keywords_hashへの依存を取り除き、Ruby 3系以降のキーワード引数仕様に素直に従う形へリファクタリングした PR です。併せて、Middleware/InstrumentationProxyクラスを内部実装扱いとしてドキュメント非公開 (:nodoc:) にしつつも、デバッグ用途の情報は維持しています。
- 変更内容の詳細
背景: Hash.ruby2_keywords_hash の非推奨化
Ruby 本体側で Hash.ruby2_keywords_hash が非推奨予定になっており(Ruby 2系→3系のキーワード引数互換のための一時的 API)、
「Ruby 3.0 以上のみをサポートするライブラリは依存すべきでない」とされています。
この PR は、その方針に沿って Rails のミドルウェア関連コードからこの API を除去するフォローアップです(元 PR: #58239)。
コアの変更点: Middleware の引数保持方法
従来は、ミドルウェアのキーワード引数を「1つの配列に押し込む」ために Hash.ruby2_keywords_hash を利用していました。
以前のイメージ(概念的なコード例)
class MiddlewareStack::Middleware
def initialize(klass, *args, &block)
# args の中に ruby2_keywords_hash 済み kwargs を押し込んでいた
@klass = klass
@args = args # 最後に ruby2_keywords_hash(kwargs) が入っている場合がある
@block = block
end
def build(app)
# ruby2_keywords による展開前提の呼び出し
@klass.new(app, *@args, &@block)
end
endこの方式は「位置引数とキーワード引数が明確に分かれていない」「ruby2_keywords/ruby2_keywords_hash 依存」という問題がありました。
変更後: 位置引数とキーワード引数を分離
PR 説明によると、Middleware(および ActionController 側のサブクラス)は以下のような構造に変更されています。
- 位置引数:
@args - キーワード引数:
@kwargs(Hash) - インスタンス生成時は「位置引数 + キーワード引数」を明示的に展開して呼び出し
概念的には次のような形です:
class MiddlewareStack::Middleware # 実際には :nodoc:
attr_reader :args, :kwargs
def initialize(klass, args:, kwargs:, &block)
@klass = klass
@args = args # Array
@kwargs = kwargs # Hash
@block = block
end
def build(app)
# Ruby 3スタイルで明示的に **kwargs を展開して呼び出す
@klass.new(app, *args, **kwargs, &@block)
end
endActionController 側の Middleware サブクラスも同様の方針に合わせて修正されており、
ミドルウェアの生成時に位置引数 / キーワード引数が明確に分離されるようになりました。
API ドキュメンテーション上の扱い変更 (:nodoc:)
以下の2クラスが :nodoc: 指定され、公式 API ドキュメントから隠されています。
ActionDispatch::MiddlewareStack::MiddlewareActionDispatch::MiddlewareStack::InstrumentationProxy
理由:
- 通常のユーザは
use/insertなどのインターフェイスを通じてミドルウェアスタックを操作するため、
これらのクラスを直接インスタンス化する想定はない。 - ミドルウェア計測(instrumentation)についても、
process_middleware.action_dispatch通知の購読が公式な利用方法であり、InstrumentationProxyの内部構造は公開 API ではない。
一方で、Rails.application.middleware の反復処理時に Middleware#args / #kwargs を閲覧できる挙動は維持されており、
開発者がスタックの状態をデバッグする用途は引き続き可能です。
テストと CHANGELOG
actionpack/test/dispatch/middleware_stack_test.rbとrailties/test/commands/middleware_test.rbが、新しい引数の扱い方(args/kwargsの分離)に合わせて更新されています。actionpack/CHANGELOG.mdに、この変更に関するエントリが追加され、
将来のアップグレード時に「Middlewareが内部実装であり、ruby2_keywords 系 API に依存しなくなった」ことが分かるようになっています。
- 影響範囲・注意点
通常のアプリケーション開発者への影響
config.middleware.use/insert_before/insert_afterなどでミドルウェアを登録している一般的な用途では挙動は変わりません。- 既存のミドルウェアクラスのコンストラクタが
def initialize(app, *args, **kwargs)のように Ruby 3 スタイルで書かれている場合、この変更はむしろ自然にマッチします。 Rails.application.middlewareをループして中身を確認する際、middleware.args/middleware.kwargsが利用可能であり、
以前より「どれがキーワード引数か」が明確になります。
ライブラリ・メタプログラミング的なコードへの影響
注意が必要なのは、Rails の内部クラスを前提にした高度なメタプログラミングやツールの作者です。
ActionDispatch::MiddlewareStack::MiddlewareやInstrumentationProxyを直接 new していた場合:
これらは内部実装であり、:nodoc:によって今後破壊的変更もありうるクラスとして明示されました。
すでに直接利用しているコードがあれば、use/insert/ instrumentation API 経由に書き換えることを検討してください。Middleware#argsに「最後の要素が ruby2_keywords_hash 済み Hash である」ことを前提としたコードがある場合:
今後はargsが純粋な位置引数のみ、kwargsがキーワード引数を持つ、という前提に切り替える必要があります。- Ruby バージョン依存コードの整理:
ライブラリ側も「Ruby 3 専用」と位置づけるのであれば、同様にruby2_keywords/ruby2_keywords_hashへの依存をやめることが推奨されます。
- 参考情報 (あれば)
- Ruby 本体の issue:
Hash.ruby2_keywords_hash非推奨に関する議論: https://bugs.ruby-lang.org/issues/22205
- 関連 PR:
- #58239: 本 PR のフォローアップ元となる、ミドルウェア関連の引数扱いリファクタリング
- Rails ガイド:
- Active Support Instrumentation ガイド(
process_middleware.action_dispatchのサブスクライブ方法など)
https://guides.rubyonrails.org/active_support_instrumentation.html - ミドルウェアの追加/削除など:
https://guides.rubyonrails.org/rails_on_rack.html
- Active Support Instrumentation ガイド(
#58239 Refactor CommandRecorder to store args and kwargs separately
マージ日: 2026/8/9 | 作成者: @kamipo
- 概要 (1–2文で)
ActiveRecord::Migration::CommandRecorderが、記録するコマンドの引数を「位置引数」と「キーワード引数」に分けて保持する形にリファクタリングされました。これにより、ruby2_keywords時代の互換レイヤを廃止し、Ruby 3 系の kwargs 仕様に素直に従う実装になっています。
- 変更内容の詳細
2-1. recorder.commands の構造変更
これまで:
# commands の 1 要素は 3 要素タプル
[cmd, args, block]
# かつ kwargs は args の末尾の Hash に埋め込まれていた
[:create_table, ["users", { if_not_exists: true }], nil]これから:
# commands の 1 要素は 4 要素タプル
[cmd, args, kwargs, block]
# 位置引数とキーワード引数が分離される
[:create_table, ["users"], { if_not_exists: true }, nil]cmd: シンボル(例::create_table,:add_column)args: 通常の配列(位置引数)kwargs: ハッシュ(キーワード引数)block: ブロック (あれば Proc, なければ nil)
影響ポイント
- 以前は
recorder.commandsを読むコードは「最後の要素が Hash なら options とみなす」といったパターンで kwargs を扱っていましたが、今後はkwargs引数として別に渡されます。
旧コード例:
recorder.commands.each do |name, args, block|
options = args.extract_options! # または args.last.is_a?(Hash) ? args.pop : {}
# ...
end新コード例:
recorder.commands.each do |name, args, kwargs, block|
# args と kwargs が明確に分かれている
end2-2. invert_* 系ヘルパのシグネチャ変更
マイグレーションの「可逆性」を実現するための invert_* メソッド類が、kwargs を明示的に受け取る形に統一されています。
例: invert_add_column(イメージ)
変更前(概念的には)
def invert_add_column(table_name, column_name, type, *args)
options = args.extract_options!
# options の中に null: false, default: ... などが入る
end変更後
def invert_add_column(table_name, column_name, type, *args, **kwargs)
# kwargs に null:, default:, if_not_exists: などがそのまま入る
end実装内からは、.extract_options! や args.last.is_a?(Hash) に依存する処理が取り除かれ、kwargs をそのまま使う形になっています。
同様の変更が、各種 invert ヘルパ (invert_create_table, invert_add_index など) に波及しています。
2-3. ruby2_keywords 時代の互換コードの削除
背景:
- Ruby 2.7〜3.0 移行期には、メソッドチェーンの途中で kwargs を失わないように
ruby2_keywordsや「引数を 1 本の配列にパックして末尾の Hash を kwargs 扱いする」といったパターンが必要でした。 - Rails 本体でも、この互換パターンに合わせて CommandRecorder の内部表現を「
[cmd, args, block]+ args 末尾 Hash = kwargs」という形にしていました。
今回の変更点:
- Rails が Ruby 3.3.1+ を前提とするようになったため、
ruby2_keywords互換レイヤが不要になりました。 - それに伴い、kwargs 専用の引数 (
**kwargs) として情報を保持/伝播させる実装にリファクタリングされています。 .extract_options!や「options = args.last if args.last.is_a?(Hash)」に代表される「末尾 Hash を options とみなす」イディオムが、CommandRecorder 周辺からはほぼ消えています。
2-4. テスト・互換レイヤ (compatibility.rb) の更新
activerecord/test/cases/migration/command_recorder_test.rbが、新しい 4 要素タプル形式に合わせて全面的に書き換えられています(+86/-86)。invertible_migration_test.rbも、kwargs 分離後の API に合わせて修正。activerecord/lib/active_record/migration/compatibility.rbでは、古いバージョンとの互換マイグレーション実装の中で引数処理をしていた部分が、args/kwargs分離を前提にしたコードへ手直しされています。schema_statements.rbにも kwargs に関する細かい修正が入っています(合計 6 行の変更)。
- 影響範囲・注意点
3-1. 最も重要な互換性ポイント
recorder.commands の形式に依存したコードは必ず壊れます。
具体的には、以下のようなコードがある場合は要修正です。
NG(旧形式前提):
recorder.commands.each do |name, args, block|
options = args.last.is_a?(Hash) ? args.last : {}
# ...
endOK(新形式対応):
recorder.commands.each do |name, args, kwargs, block|
# kwargs をそのまま使う
endあるいは、どうしても古い/新しい両方に対応したい Gem 側などでは、以下のようなガードが必要になります:
recorder.commands.each do |tuple|
case tuple.length
when 3
name, args, block = tuple
kwargs = args.last.is_a?(Hash) ? args.pop : {}
when 4
name, args, kwargs, block = tuple
else
raise "Unknown CommandRecorder tuple size: #{tuple.length}"
end
# 以降は name, args, kwargs, block で扱う
end3-2. invert_* メソッドを独自にオーバーライドしている場合
アプリや Gem で ActiveRecord::Migration::CommandRecorder を継承し、独自に invert_* 系メソッドを定義している場合:
- 旧来のシグネチャ(
*argsのみ)にしていると kwargs が落ちる/正しく受け取れない可能性があります。 - 定義を
def invert_xxx(*args, **kwargs)、または引数を明示した上で**kwargsを受け取る形に修正しておくと安全です。
Ruby 3 では、kwargs を位置引数で受ける書き方はそもそも非推奨・エラーになるパターンがあるため、この変更は Ruby 本体の方向性にも沿っています。
3-3. Rails 内部 API を叩いているメタプログラミング的コード
- マイグレーション周りを「メタプログラミング的にラップしているコード」(DSL 追加や記録内容の書き換えなど)で、
CommandRecorderの内部表現に強く依存しているものは動作確認が必要です。 - 特に「args の末尾 Hash を書き換える」「args に options を push する」といった操作は、
kwargsに直接触る形に変更する必要があります。
- 参考情報 (あれば)
- 当該 PR: Refactor
CommandRecorderto store args and kwargs separately (#58239) - Ruby 側の関連提案:
ruby2_keywordsを将来的に非推奨にする提案
https://bugs.ruby-lang.org/issues/22205 - Rails が Ruby 3 系を前提にし始めた文脈や、kwargs 仕様の整理は、Rails/ActiveRecord の CHANGELOG (activerecord/CHANGELOG.md) にも追記されています。
この PR 以降、CommandRecorder に直接触れるコードは「[name, args, kwargs, block]」の 4 要素タプルを前提に整理するのが推奨です。
#58342 Refreeze the controller middleware stack for every mutating method
マージ日: 2026/8/8 | 作成者: @55728
- 概要 (1-2文で)
Ractor セーフモード時にActionController::Metal.middlewareの一部のミドルウェア操作メソッドが凍結済みスタックを直接変更しようとしてFrozenErrorになる不具合を修正し、他の変更系メソッドと同様に「コピーして再凍結」されるように統一した PR です。Ractors 対応(ActiveSupport::Ractors.unshareable_proc_action有効時)のミドルウェアスタック操作が一貫して安全に動作するようになります。
- 変更内容の詳細
背景
ActionController::Metal.middleware が返すのは、実体のミドルウェアスタック (ActionDispatch::MiddlewareStack) に対するプロキシ (ActionController::MiddlewareStack::Proxy) です。
Ractor セーフモード (ActiveSupport::Ractors.unshareable_proc_action が :raise など) では、
このスタック内部の配列を Ractor 間で共有可能にするために「一度凍結」し、
変更要求が来たときは:
- 凍結されたスタックをコピーする
- コピー側に対して変更を行う
- コピーを再度「共有可能(shareable)」にする(凍結等)
という動作を行う必要があります。
そのため Proxy は「ミドルウェアスタックを破壊的に変更するメソッド」を列挙しておき、それらが呼ばれたときだけ「コピーして再凍結」のラッパを噛ませる設計になっています。
問題点
しかし、その「破壊的変更メソッドの列挙」に以下 4 メソッドが漏れていました:
insert_beforeinsert_afterdelete!move_before(実体はmoveのエイリアス)
このため、これら 4 つは delegate_missing_to 経由で素の ActionDispatch::MiddlewareStack に直接委譲され、凍結されている配列をそのまま変更しようとして FrozenError が発生していました。
再現例(PR 説明より):
ActiveSupport::Ractors.unshareable_proc_action = :raise
class PostsController < ActionController::Metal; end
PostsController.middleware.use(Mw) # OK: スタックがコピー&再凍結される
PostsController.middleware.insert_before(Mw, Other) # => FrozenError: can't modify frozen Arrayuse は「列挙済みの mutator(変更系メソッド)」なのでコピー&再凍結のラッパが働く一方、insert_before は列挙から漏れていたため、凍結済み配列に対する直接操作になり失敗していました。
この PR の対応
ActionController::MiddlewareStack::Proxy内の「ミドルウェアスタックの変更系メソッドのリスト」に、漏れていた 4 メソッド:insert_beforeinsert_afterdelete!move_before
を追加。
- これにより、他の mutator と同様に:
- 呼び出しごとにスタックのコピーを作成
- そのコピーに対して変更を適用
- コピーを再度 Ractor 共有可能な状態(凍結)にする という処理が行われるようになります。
テスト (actionpack/test/controller/new_base/middleware_test.rb) も追加され、
Ractor セーフモードでこれらメソッドを呼んでも FrozenError にならないことを確認しています。
動作の Before/After
- Before (Ractor セーフモード時):
useや、すでに列挙済みの mutator は OKinsert_before,insert_after,delete!,move_beforeはFrozenError発生
- After:
- 上記 4 メソッドも含め、すべての mutator が
「コピー → 変更 → 再凍結」ルールに従って安全に動作
- 上記 4 メソッドも含め、すべての mutator が
- 影響範囲・注意点
影響を受けるケース
ActionController::Metal(およびそれを継承するコントローラ)でcontroller.middleware.insert_before(...)controller.middleware.insert_after(...)controller.middleware.delete!(...)controller.middleware.move_before(...)
を使っており、
- かつ
ActiveSupport::Ractors.unshareable_proc_actionを有効(:raiseなど)にして Ractor セーフモードで実行している場合。
非 Ractor セーフモードでは挙動は従来と変わらない想定で、主に Ractor 対応時の不具合修正です。
「凍結済みスタックがいきなり FrozenError を投げる」という現象が解消され、Ractor 対応コードでも他のミドルウェア操作メソッドと一貫した挙動になります。
挙動変更は「以前に落ちていたコードが正常動作する」方向なので、後方互換性の観点でもほぼ安全な修正と考えられます。
- もし
FrozenErrorに依存したワークアラウンド等を書いていた場合は、その挙動は変わりますが、通常は期待されない使い方です。
- もし
- 参考情報 (あれば)
- 対象クラス/モジュール:
ActionController::MiddlewareStack::ProxyActionDispatch::MiddlewareStack
- 関連する設定:
ActiveSupport::Ractors.unshareable_proc_action:raise等に設定した際に、ミドルウェアスタック配列が Ractor 共有可能にするため凍結される
- 本挙動は
dd67a577d9で導入された「Proxy 経由でスタックにメソッドを呼ぶときは、常に共有可能なコピーを返す」という設計の抜け漏れの修正です。
#58217 Remove unused Active Record internals
マージ日: 2026/8/8 | 作成者: @55728
- 概要 (1-2文で)
このPRは、Active Record 内部で一切使われていないメソッドや引数・インスタンス変数などの「死んだコード」をまとめて削除するクリーンアップです。挙動の変更や外部APIの破壊的変更は基本的になく、内部実装の整理・単純化が主目的です。
- 変更内容の詳細
それぞれ「どこで使われなくなったか」がコミット履歴ベースで確認されており、grep(send・シンボル参照も含む)で参照ゼロであることが検証されています。
2-1. FromClause#merge の削除
- 対象ファイル:
activerecord/lib/active_record/relation/from_clause.rb - 削除されたメソッド:
FromClause#merge
Relation::Merger が汎用的な「clause-merge ループ」をやめ、where/having の明示的なマージと from 句の「置き換えルール」に変わった結果、FromClause#merge は呼ばれなくなっていました。
# 以前はこういったメソッドがあった(イメージ)
class FromClause
def merge(other)
# from 句同士をマージするロジック
end
end現在は from は「マージ」ではなく「上書き」される設計になっているため、このメソッドは不要となり削除されています。
ポイント:
内部的なリレーションマージの仕組みがシンプルになった影響で、FromClause#merge にフックしているアダプタや拡張コードがなければ影響はありません。
2-2. try_to_queue_for_background_connection の checkout_timeout 引数の削除
- 対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb - 対象メソッド:
try_to_queue_for_background_connection - 変更内容:
- メソッドシグネチャの
checkout_timeout引数が削除 - メソッド内部でも使われていなかったため、完全に参照がなくなる
- メソッドシグネチャの
# Before
def try_to_queue_for_background_connection(checkout_timeout)
# checkout_timeout は無視されており、待ち時間は無制限
end
# After
def try_to_queue_for_background_connection
# 挙動自体は変わらず、無制限で待つ
endコメントにも明記されている通り、「待ち時間は意図的に無制限」で、タイムアウト値は設計的に使わない方針です。そのため引数だけが「ダミー」のように残っていました。
ポイント:
内部メソッドですが、もしアプリやgemが ConnectionPool を継承して上書き・呼び出している場合、シグネチャ変更に注意が必要です(通常は触らない箇所)。
2-3. HashLookupTypeMap#initialize の parent 引数削除
- 対象ファイル:
activerecord/lib/active_record/type/hash_lookup_type_map.rb - 変更点:
initialize(parent = nil)→initialize(引数なし)に変更- 引数
parentはコード内で使われておらず、呼び出し側も渡していない
# Before
class HashLookupTypeMap
def initialize(parent = nil)
@mapping = {}
end
end
# After
class HashLookupTypeMap
def initialize
@mapping = {}
end
end過去には TypeMap と結びついていて親のマップを引き継ぐような設計があったものの、それがすでに解除されているため、parent は完全に不要になっていました。
ポイント:HashLookupTypeMap.new(something) のように独自に使っているコードがあるとエラーになりますが、フレームワーク内部ではそのような呼び出しは行っていないことが確認されています。
2-4. TransactionState の未使用メソッド削除
- 対象クラス:
TransactionState - 削除されたメソッド:
#fully_committed?#fully_rolledback?#fully_completed?#nullify!
- 関連ファイル:
activerecord/lib/active_record/connection_adapters/abstract_adapter.rbなど
これらのメソッドは、過去のリファクタリングで最後の呼び出し元が段階的に削除され、最終的に:
- 呼び出し元ゼロ
nullify!については「テスト自身」だけが唯一の呼び出し元
という状態になっていました。そのためメソッド本体と、nullify! をテストするユニットテストも合わせて削除されています。
TransactionState 自体が持っている状態値 :fully_committed / :fully_rolledback は、引き続き committed? / rolledback? から参照されるため、トランザクションの外部的な振る舞いは変わりません。
# イメージ
state = TransactionState.new
state.committed? # 既存の public-ish な問い合わせ
state.rolledback? # こちらも継続利用される
# fully_*? 系は完全削除ポイント:
「トランザクションが 完全に コミット/ロールバック済みか」を見る細かい問い合わせメソッドが消えた形ですが、フレームワークコードからはすでに使われておらず、公的なAPIでもなかったため、互換性影響はほぼありません。
2-5. SingularAssociation#reset の書き込み専用インスタンス変数削除
- 対象ファイル:
activerecord/lib/active_record/associations/singular_association.rb - 削除内容:
reset内で@future_targetに代入していた行を削除
@future_targetはどこからも読み出されておらず、「書き込むだけで使われない」変数でした。
def reset
@target = nil
@loaded = false
# @future_target = nil ← これが削除
endポイント:
内部状態の管理がすでに別の方式に移っており、残骸だけが残っていたパターンです。挙動は変わりません。
2-6. @_query_constraints_list への「死んだ代入」の削除
- 対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb付近?
※説明文から推測すると、クラスのinheritedフック内で@_query_constraints_listをリセットするコード行 - 状況:
- 以前はメモ用インスタンス変数名が
@_query_constraints_list - その後
@query_constraints_listにリネーム済み - しかし
inheritedフック内だけ古い名前でリセットしていた - Ruby のインスタンス変数はクラス継承では引き継がれないので、そもそもここでのリセット自体意味が薄い
- 以前はメモ用インスタンス変数名が
class SomeBase
class << self
def inherited(subclass)
super
# @_query_constraints_list = nil ← これが削除される
end
end
endポイント:
- 「存在しないインスタンス変数に nil を代入しているだけ」の状態
- サブクラスからは見えないメモであり、消しても挙動は一切変わらない
2-7. ThroughReflection#source_options / #through_options の削除
- 対象ファイル:
activerecord/lib/active_record/reflection.rb - 削除されたメソッド:
ThroughReflection#source_optionsThroughReflection#through_options
has_many :through などで使われるリフレクションの内部ヘルパでしたが、過去のリファクタリング(該当コミット: 500b1df43e / 65843e1acc)で最後の呼び出し元が削除済みで、現在は完全に未使用でした。
class ThroughReflection < AssociationReflection
# def source_options; ...; end # 削除
# def through_options; ...; end # 削除
endポイント:
独自にリフレクションを操作しているメタプログラミング系のライブラリが、このメソッドを直接呼んでいた場合は NoMethodError になります。ただし正式にドキュメント化されたAPIではなく、フレームワーク内部実装寄りのメソッドです。
2-8. PoolConfig#server_version writer の削除
- 対象ファイル:
activerecord/lib/active_record/connection_adapters/pool_config.rb - 削除内容:
server_version=のセッターが削除- テストコードはすでに
instance_variable_setで直接メモをリセットしており、セッターは使っていない
class PoolConfig
# attr_writer :server_version # これに相当するものが削除
endポイント:
- 「サーバーバージョンのメモ」を外部から書き換えるためのフックが内部的にすら使われておらず、完全に死んでいた
- DB アダプタや拡張コードが
pool_config.server_version = ...のように書いていると壊れる可能性がありますが、通常はconnection側でバージョンを扱うため、かなりレアケースと考えられます。
2-9. AbstractAdapter#build_result の削除
- 対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract_adapter.rb - 削除されたメソッド:
#build_result - 背景:
- 以前は「アダプタがカスタム result オブジェクトを構築するためのフック」としてコメント付きで用意されていた
- しかし
fd24e5bfc9の変更でアダプタがcast_resultパスに寄せられ、このフックを呼び出すフレームワークコードはなくなった - 現在
build_resultをオーバーライドしても、そもそも一度も呼ばれない
class AbstractAdapter
# def build_result(columns, rows)
# # アダプタが独自の結果オブジェクトを返すためのフック(という位置づけだった)
# end
endPR 説明では「今でもこのメソッドを extension point として残したいのであれば、このコミットは落としてもよい」と言及されていますが、最終的には削除されています。
ポイント:
- 外部アダプタが
build_resultを実装していたとしても、実際にはすでに呼ばれていなかったため、「メソッドが消える」以外の実害はありません(挙動は元から変わっていない)。 - ただし、
AbstractAdapterを継承した独自アダプタでsuper呼び出しなどをしていると NoMethodError になる可能性があります。
- 影響範囲・注意点
基本方針:
- すべて「フレームワーク内部で参照されていない」ことが確認されたコードの削除です。
- 公式に公開されたAPIではなく、主に内部クラス・メソッド・引数が対象です。
- そのため、通常のアプリケーションコードに対する影響はほぼゼロと考えられます。
注意が必要なケース(拡張・メタプログラミングをしている場合):
内部クラスに直接依存している場合
- 例:
ActiveRecord::ConnectionAdapters::AbstractAdapter#build_resultを独自アダプタでオーバーライドしていた - 例:
ThroughReflection#source_options/through_optionsを呼んでリフレクション情報を解析していた - 例:
HashLookupTypeMap.new(some_parent)のように引数付きで初期化していた
- 例:
private / internal メソッドを
sendで呼んでいた場合- PR 側でも
sendやシンボル参照まで含めて grep されているため、Rails 本体では未使用と確認されていますが、アプリやgemが同様の手法で呼び出していると壊れます。
- PR 側でも
シグネチャ変更
try_to_queue_for_background_connection(checkout_timeout)→ 引数なし
自前のサブクラス・モンキーパッチで呼び出している場合は修正が必要です。
実質的な挙動の変更:
- いずれの削除も「参照されていないコード」の削除に留まっており、Rails が発行するSQL、コネクションプールの動作、トランザクションの扱いなど、ユーザが観測可能な挙動は変わらないよう設計されています。
- 既存のテストは、
nullify!のテスト削除以外は、動作変更を伴う形で壊れていないことが前提です。
- 参考情報 (あれば)
- このPR自体は、以下の「未使用コードクリーンアップ」PR群のフォローアップとして位置づけられています:
- #58214
- #58197
- #58208
- 変更の根拠となるコミット(どの時点から未使用になったか)が詳細に示されているため、もし「このメソッドに依存していたかも」と感じた場合は、該当するコミットを辿ることで設計変更の流れを追えます。
- 依存している可能性をチェックしたい場合:
- アプリケーションおよび社内gemで
build_result,ThroughReflection#source_options,HashLookupTypeMap.new(などの文字列検索を行うと影響をざっと洗い出せます。
- アプリケーションおよび社内gemで
#58361 Fix TimeWithZone#to_s raising in a non-main Ractor
マージ日: 2026/8/8 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::TimeZoneが Ractor 共有可能になっていたにもかかわらず、非メイン Ractor でTimeWithZone#to_sなどを呼ぶとRactor::IsolationErrorが発生していた問題を修正した PRです。内部で使っているタイムゾーンオフセット用の文字列を凍結し、Ractor 間で安全に共有できるようにしています。
- 変更内容の詳細
問題の背景
- 以前のコミット
26e78a20dbにより、ActiveSupport::TimeZoneインスタンス自体はRactor.shareable?になるように対応済みでした。 - しかし
TimeWithZone#to_s,#rfc2822,to_fs(:rfc822)を非メイン Ractor 内で呼ぶとRactor::IsolationErrorが発生していました。 - 理由は、フォーマット時に使う定数
UTC_OFFSET_WITHOUT_COLONが「凍結されていないString」だったためです。frozen_string_literal: trueではカバーされないケースで、String#trのようなメソッド呼び出しで文字列オブジェクトが「書き換え可能」と見なされ、Ractor 的に共有不可能になっていました。
修正内容
activesupport/lib/active_support/values/time_zone.rbUTC_OFFSET_WITHOUT_COLONを凍結 (freeze) するように変更。- これにより、この定数はイミュータブルになり、Ractor 間で安全に共有可能になります。
activesupport/test/time_zone_test.rb上記問題が再発しないように、非メイン Ractor で
TimeWithZone#to_sなどを呼び出すテストを追加。具体的には、おおよそ以下のようなテストシナリオが追加されています(イメージ):
rubyTime.zone = "Eastern Time (US & Canada)" t = Time.zone.local(2000, 1, 1) r = Ractor.new(t) do |time| [time.to_s, time.rfc2822, time.to_fs(:rfc822)] end result = r.take # 以前はここで Ractor::IsolationError が発生していた
動作例(PR 説明より)
修正前:
Time.zone = "Eastern Time (US & Canada)"
Ractor.new(Time.zone.local(2000, 1, 1)) { |t| t.to_s }.value
# => Ractor::IsolationError修正後:
Time.zone = "Eastern Time (US & Canada)"
Ractor.new(Time.zone.local(2000, 1, 1)) { |t| t.to_s }.value
# => "2000-01-01 00:00:00 -0500"- 影響範囲・注意点
- 影響範囲
- 非メイン Ractor 内で
ActiveSupport::TimeWithZoneオブジェクトの以下メソッドを呼ぶ処理:#to_s#rfc2822#to_fs(:rfc822)(旧to_s(:rfc822)相当)
- 上記が
Ractor::IsolationErrorで落ちていたケースが正常に動作するようになります。
- 非メイン Ractor 内で
- 既存コードへの影響
- 変更は定数文字列を
freezeしただけで、フォーマット結果の文字列内容やパブリック API には変更がありません。 - マルチ Ractor を使っていないアプリケーションには挙動の変化はありません。
- 変更は定数文字列を
- 注意点
- この PR は 「TimeZone/TimeWithZone が Ractor 安全に近づいた」修正ですが、Rails の他部分が Ractor 完全対応とは限りません。
- Ractor を本番で使う場合は、他の gem / ライブラリやアプリケーションコードも含めて
Ractor.shareable?やRactor::IsolationErrorを確認する必要があります。
- 参考情報 (あれば)
- 当該 PR: https://github.com/rails/rails/pull/58361
- 関連コミット:
26e78a20db(ActiveSupport::TimeZoneを Ractor-shareable にした変更) - Ruby Ractor ドキュメント:
- https://docs.ruby-lang.org/en/master/doc/ractor_md.html
- 特に「Ractor::IsolationError」「shareable objects」の節が今回の問題の背景に関連します。
#58416 Use ActiveRecord::Key.for in remaining Array(reflection.*_key) sites
マージ日: 2026/8/8 | 作成者: @kamipo
- 概要 (1-2文で)
Rails の ActiveRecord 内で、関連(reflection)のキーを配列化して扱っていた箇所をArray(...)からActiveRecord::Key.for(...)に統一し、単一キーと複合キーを同じ Enumerable API で扱えるようにリファクタリングした PR です。機能追加ではなく、#58368 に続く内部実装の揃え・複合主キー対応を見越した整備です。
- 変更内容の詳細
何をしたか
- 以下のファイルで、reflection 由来のキー名を配列化してループしていた箇所をすべて
ActiveRecord::Key.for(...)に置き換えassociations/association.rbassociations/association_scope.rbassociations/foreign_association.rbassociations/has_one_association.rbautosave_association.rbrelation/query_methods.rb
- 行数としては +24 / -24 で、ほぼ 1:1 の API 置き換え (挙動を変えるというより手段の統一)
具体例イメージ
これまで典型的にやっていたコード:
Array(reflection.foreign_key).each do |key|
# 単一 or 複合キーを意識せずに処理したい
endを、以下のように変更:
ActiveRecord::Key.for(reflection.foreign_key).each do |key|
# 同上
end同様に、primary_key, association_primary_key, join_primary_key など、reflection 周りでキーを列挙していた部分が同じ方針で ActiveRecord::Key.for に統一されています。
ActiveRecord::Key.for の意図 (推測を含む整理)
- 目的:
- 「単一キー」も「複合キー」も “Enumerable なキー集合” として同じ API で扱うためのヘルパ
- 期待される振る舞いのイメージ:
- 引数が単一キー文字列/シンボルなら、1 要素のコレクションとして扱えるようにする
- 引数が配列や複数キーを表すオブジェクトなら、そのまま複数要素として列挙
- 将来的に複合主キーや複合外部キーをちゃんとサポートする際の統一インターフェースになる
従来は Array(...) で「とりあえず配列にする」程度だったのに対して、ActiveRecord::Key.for は「ActiveRecord が期待するキー表現」に正規化する責務を持つクラスメソッドになっていると考えられます。
- 影響範囲・注意点
影響範囲
- ActiveRecord 内部の以下の処理で「単一 / 複合キーの扱い」が
ActiveRecord::Key.forベースに統一されます:- 各種 association の関連づけ・ロード処理
- association scope の生成
belongs_to/has_oneなどの外部キー・主キーの突き合わせ- autosave (関連オブジェクトの自動保存) の関連付け・キー処理
Relation#where,#eager_loadなどで reflection を利用して join / 条件を組み立てる部分の一部
外部から見た挙動への影響
- 公開 API としての挙動は基本的に変わらない想定です:
- 通常の単一主キー/外部キーを使うアプリでは、動作の変化はほぼ感じないはず
- ただし、以下に関わるようなアプリ/ライブラリは注意:
- ActiveRecord の内部 API (
reflection.foreign_keyなど) を直接触っている gem - association や autosave の内部動作に monkey patch を当てているコード
- 独自の複合キー拡張 (e.g.
composite_primary_keys的なもの) と組み合わせている場合
- ActiveRecord の内部 API (
- これらのケースでは:
Array(reflection.some_key)を前提にしたコードがある場合、
今後はActiveRecord::Key.for(reflection.some_key)を使うほうが
Rails 本体と挙動を合わせやすくなります。
バージョンアップ時のテスト観点
- Rails アップグレード時に追加で見ておくと良いポイント:
- 複合キー or カスタムな primary_key / foreign_key を指定している association が正しく
- find / build / create できるか
- eager_load / includes / joins で問題なくロードされるか
- autosave (
accepts_nested_attributes_forなど) で関連が正しく保存されるか
- gem の monkey patch と競合していないか (NoMethodError や挙動変化が出ていないか)
- 複合キー or カスタムな primary_key / foreign_key を指定している association が正しく
- 参考情報 (あれば)
- 対応元 PR: #58368
ActiveRecord::Key導入および、一部 call site をそちらに切り替えた最初の PR と考えられます。
- 本 PR #58416 は、そのフォローアップとして、
残っていたArray(reflection.*_key)呼び出しをすべてActiveRecord::Key.forに寄せる「仕上げ」のリファクタリングです。 - 将来の「正式な複合キーサポート」や、キー周りの内部表現の整理に向けた準備として読むと理解しやすい変更です。
#58396 Eager load templates
マージ日: 2026/8/8 | 作成者: @etiennebarrie
- 概要 (1-2文で)
このPRは、Rails起動時にテンプレートを「先読み(eager load)」して共有可能にする仕組みを Action View に導入したものです。まずは strict locals なテンプレートのみを対象とし、テンプレートのコンパイル先コンテナの不整合を明示的に検出する安全チェックも追加されています。
- 変更内容の詳細
2-1. 起動時にテンプレートを eager load できるように
- Action View のテンプレートを「起動時にロードしてコンパイルしておく」ための経路が追加されています。
railties/lib/rails/application.rbに変更が入っているため、config.eager_load = trueなどのタイミングで、通常のクラス/モジュールの eager load と同様にテンプレートもロード・コンパイルされるフックが増えた形です。- 対象は strict locals テンプレートのみ。
- strict locals とは、テンプレート側で受け取るローカル変数とその型(あるいは存在)を明確に定義し、呼び出し時に宣言と違う locals を渡すとエラーになるようなモードです。
- このモードではテンプレートのコンパイル結果を 1つの
Templateオブジェクトにまとめやすく、Ractor 間で共有可能な形にしやすい、という前提があります。
ポイント:
- これまでは
ActionView::UnboundTemplate.@templatesにテンプレートごとのキャッシュがぶら下がる形だったのを、strict locals の場合は単一のTemplateオブジェクトに折りたたんで扱えるようにした(=共有しやすくした)という意図です。 - 将来的には non-strict なテンプレートについても、Ractor ローカルキャッシュとしてコンパイルする follow-up が予定されています(PR説明中の Shopify/rails ブランチへのリンク参照)。
2-2. UnboundTemplate / Template 周りのキャッシュ構造変更
変更ファイル:
actionview/lib/action_view/template.rbactionview/lib/action_view/unbound_template.rbactionview/lib/action_view/template/resolver.rbactionview/lib/action_view/template/sources.rb
主なポイント:
strict locals テンプレートのコンパイルキャッシュの扱いを改善
UnboundTemplateが保持していた@templatesキャッシュを strict locals では 1つのTemplateオブジェクトに集約し、Ractor 間で共有しやすい形にしています。- これにより、Ractor 化された環境(
ractorize!有効時など)でのテンプレート再コンパイルやメモリ・CPU コストを抑えられるようになります。
resolver に eager load 経路を追加
ActionView::Template::Resolverに、テンプレートを事前にロード・コンパイルするためのメソッドやフローが追加されています。- ファイルシステムベースの resolver (
file_system_resolver_test.rbが増えている) を使って、起動時にディスク上のテンプレートファイルを走査し、コンパイル済みテンプレートをキャッシュしておくことができるようになります。
Template / sources 周りの補助的な API 追加
ActionView::TemplateやActionView::Template::Sourcesに、eager load と Ractor 共有前提の使い方をサポートする小さなメソッド・フラグが追加されています(例えばソースの frozen 管理や、コンパイル済みメソッドの保持の仕方など)。
2-3. コンパイル先コンテナの安全チェック追加
PR説明にある通り、テンプレートコンパイル時の「メソッドコンテナ」の不整合を検出する安全チェックが新しく入りました。
背景:
- Action View テンプレートは、最終的には Ruby のメソッドとして「どこかのクラス/モジュール」に定義されます。
- そのコンテナは通常、
view_context_class.compiled_method_containerです。 - テンプレートは「最初にレンダリングされたときの view のコンテナ」に対してコンパイルされます。
- 例: 最初に
ApplicationControllerの view context からレンダリング → そのview_context_class.compiled_method_containerにメソッドが定義される。
- 例: 最初に
問題だった点:
- 後から、別のコンテナを持つ view context で同じテンプレートをレンダリングしようとすると、そこにはコンパイル済みメソッドが存在しないため
NoMethodErrorが発生していました。 - しかし従来はこの
NoMethodErrorはかなり分かりづらく、「なぜメソッドが無いのか」が明示されていませんでした。
今回の変更:
- テンプレートがコンパイルされたコンテナと、現在レンダリングに使おうとしている view context のコンテナが異なる場合、
- 明示的なエラーを投げるようになりました(
NoMethodErrorではなく、「コンテナが違う」という趣旨の例外)。
- 明示的なエラーを投げるようになりました(
- これにより、「
view_context_classをオーバーライドして別のコンテナを持つクラスを使っている」などのケースで、問題の原因がすぐ分かるようになります。
具体例になりやすい状況:
class ApplicationController < ActionController::Base
end
class ApplicationMailer < ActionMailer::Base
end
# 通常は両者の view_context_class.compiled_method_container は equal? になる
ApplicationController.view_context_class.compiled_method_container.equal?(
ApplicationMailer.view_context_class.compiled_method_container
) #=> true (典型的なケース)しかし、例えばあるコントローラで:
class CustomViewContext < ActionView::Base
# compiled_method_container が異なるような実装
end
class Admin::DashboardController < ApplicationController
def view_context_class
CustomViewContext
end
endのように、異なるコンテナを持つクラスを返していると、
テンプレートが最初にコンパイルされたコンテナと異なるコンテナで再利用される可能性があり、その場合に今回の安全チェックが働きます。
2-4. Ractor 対応まわりのテスト追加
railties/test/application/ractors_test.rbにテストが追加され、Ractor 化 (ractorize!) されたアプリケーションでテンプレートの eager load / 共有が期待通りに動作するか確認しています。- strict locals テンプレートが Ractor セーフにコンパイルされ、Ractor 間で使い回せることを担保する意図があります。
影響範囲・注意点
strict locals テンプレートを使っているアプリ
- 起動時にテンプレートを eager load する設定を有効にすると、strict locals テンプレートはコンパイル済み状態でアプリ起動後すぐ利用されます。
- メリット:
- 初回アクセス時のコンパイルオーバーヘッドが減る。
- Ractor 使用時のパフォーマンス・メモリ効率、および安定性が向上。
- テンプレートの定義ミス(locals の宣言と実際に渡している値の不整合など)は、起動時に早めに検知される可能性が高まります。
view_context_classをカスタマイズしているアプリ/エンジンview_context_classをオーバーライドしており、そのcompiled_method_containerがApplicationController/ApplicationMailerなどデフォルトとは異なる場合、新しい安全チェックにより、これまで黙ってNoMethodErrorになっていた箇所が、より明示的な例外に変わります。- もしこのエラーが発生した場合は:
view_context_classを見直し、テンプレートを共有したいクラス間で同じcompiled_method_containerを使うようにする。- あるいは、コンテナ違いを前提にテンプレートを別々にコンパイルする設計にする(多くの場合は前者が望ましい)ことを検討してください。
Ractor を有効にしている環境 (
ractorize!)- 今回の変更で、strict locals テンプレートが共有しやすい形に整理されたため、Ractor 間のテンプレート共有が現実的になります。
- ただし non-strict テンプレートはまだ follow-up PR での対応予定であり、現時点では Ractor ローカルキャッシュでの最適化は strict locals のみが対象です。
互換性
- 基本的には後方互換を維持しつつ、安全性とパフォーマンスを高める変更です。
- 互換性上の変化は「これまで曖昧な
NoMethodErrorが出ていたケースで、より明示的な例外が出るようになった」点にほぼ限られます。
- 参考情報 (あれば)
- PR 本体:
https://github.com/rails/rails/pull/58396 - non-strict テンプレート対応の follow-up(Shopify フォーク):
https://github.com/Shopify/rails/tree/actionview-frozen-non-strict-templates - Ractor と Rails に関するテスト類(参考):
railties/test/application/ractors_test.rbに今回の変更と関連するテストが追加されています。
#58415 Remove symbol_column_to_string in favor of columns_hash + Symbol#name
マージ日: 2026/8/8 | 作成者: @kamipo
- 概要 (1-2文で)
symbol_column_to_stringというヘルパーとそのキャッシュを削除し、代わりにcolumns_hash(クラスレベルでメモ化されたハッシュ)と Ruby 3.0 以降のSymbol#nameを直接使うようにする変更です。これにより、シンボルを使ったカラム参照で追加のキャッシュ構造が不要になり、コードが簡潔かつ同等のパフォーマンスを維持します。
- 変更内容の詳細
背景
以前のPR (#34197) で、
respond_to?にシンボルを渡した際に**文字列オブジェクトを新規生成しない(allocation-free)**ようにするため、以下のような仕組みが導入されていました:- カラム名シンボル
:titleなどをキーに、対応する文字列"title"を値に持つ{ sym => str }のハッシュをメモ化(symbol_column_to_string) - これにより
respond_to?(:title)等のたびに文字列を生成せずに済む
- カラム名シンボル
一方で Active Record には既に
columns_hashという、{"title" => #<ActiveRecord::ConnectionAdapters::Column ...>}
という形の凍結済み・クラスレベルでメモ化されたハッシュが存在しており、これ自体が O(1) でのカラム検索を提供します。Ruby 3.0 以降では
Symbol#nameによって、新しい String を生成せずに内部的にインターンされた文字列を取得できます("title"に相当する文字列表現を割り当てなしで得られる)。
このPRでの主な変更点
symbol_column_to_stringヘルパー削除activerecord/lib/active_record/model_schema.rbからsymbol_column_to_stringの定義とそれに関連するキャッシュスロットのコードが削除されています(-8行)。- これにより「
{sym => str}キャッシュ」を使った独自の変換ロジックは無くなります。
attributes/respond_to?系の内部処理をSymbol#name+columns_hashに統一activerecord/lib/active_record/attribute_methods.rbの該当箇所で、シンボル引数を処理する際にsymbol_column_to_stringを呼び出さず、直接sym.nameを使うような形に書き換えられています(+2/-3)。典型的には以下のようなイメージに近い形になります(概念的なサンプル・実際のコードとは多少異なる可能性があります):
ruby# 変更前(イメージ) def has_attribute?(attr_name) attr_name = symbol_column_to_string(attr_name) if attr_name.is_a?(Symbol) columns_hash.key?(attr_name) end # 変更後(イメージ) def has_attribute?(attr_name) attr_name = attr_name.name if attr_name.is_a?(Symbol) # Ruby 3.0+: allocation-free columns_hash.key?(attr_name) endcolumns_hash自体はクラスレベルでメモ化されており、これを直接使うことで、以前の{sym => str}キャッシュと同等の O(1) ルックアップと allocation-free な変換を実現します。
- 影響範囲・注意点
パフォーマンス / メモリ
- ねらい
- 追加の
{Symbol => String}キャッシュを廃止することで、クラスごとのキャッシュスロットが一つ減り、内部実装がシンプルになります。 Symbol#nameは Ruby 3.0 以降で allocation-free なため、以前の最適化(symbol_column_to_string)と同等の性能が期待できます。
- 追加の
respond_to?やhas_attribute?等で、シンボルを渡した場合のカラム名解決は従来どおり高速で、余分な String オブジェクトも生成しません。- 非常に大量のカラムや大量のモデルを扱うアプリケーションであっても、実質的には性能的な劣化はなく、場合によってはメモリ断片の削減が期待できます。
互換性
symbol_column_to_stringは内部実装向けのプライベートなヘルパーであり、通常のアプリケーションコードが直接呼び出すものではありません。- そのため、通常のアプリケーションでは後方互換性の問題はほぼありません。
- もし gem / プラグイン等が
symbol_column_to_stringを直接参照していた場合は、Symbol#nameとcolumns_hashを使った実装に書き換える必要があります。- 例:
symbol_column_to_string(:title)→:title.nameもしくはcolumns_hash.key?(:title.name)など。
- 例:
Ruby バージョン
- この変更は Ruby 3.0 以降の
Symbol#nameを前提にしています。 - Rails 本体として Ruby 3.0 以上をサポート前提として進めているバージョンであることが前提となる変更です(古い Ruby をサポートする独自フォークなどでは注意が必要)。
- 参考情報 (あれば)
- 本PR: #58415
タイトル: Removesymbol_column_to_stringin favor ofcolumns_hash+Symbol#name - 関連PR: #34197
respond_to?のシンボル入力を allocation-free にするためにsymbol_column_to_stringが導入された元の変更。
- 関連API:
ActiveRecord::Base.columns_hash{String => ActiveRecord::ConnectionAdapters::Column}形式の、凍結済みメモ化ハッシュ
- Ruby 3.0+:
Symbol#name- シンボルの文字列表現を返すメソッドで、
to_sと違い新しい文字列を割り当てない実装になっている(allocation-free)。
- シンボルの文字列表現を返すメソッドで、
#58404 Fix the Request#media_type documentation example
マージ日: 2026/8/7 | 作成者: @55728
- 概要 (1-2文で)
ActionDispatch::Request#media_typeのドキュメント内サンプルが、実際の挙動と一致していなかったため、正しいリクエスト例(POST+ボディ付き)に修正したドキュメント専用のPRです。コードの挙動自体は一切変更されていません。
- 変更内容の詳細
何が問題だったか
元のドキュメントでは、次のように GET リクエストの例が書かれていました:
# get "/articles"
request.media_type # => "application/x-www-form-urlencoded" # actually nilしかし、通常の GET リクエストはボディを持たず、Content-Type ヘッダも設定されないため、実際には request.media_type は nil になります。
テスト (test/dispatch/request_test.rb:1109) でも「Content-Type がないリクエストでは media_type は nil である」と明示的に検証されており、ドキュメントだけが古い挙動を前提にしたまま残っていました。
元々は、統合テストのヘルパが常に Content-Type: application/x-www-form-urlencoded を付けており、その時代にはサンプルどおりの挙動でしたが、コミット 86754a8f7b によって「デフォルトのリクエストエンコーダが Content-Type を付けない」ように変わったため、ドキュメントと実挙動が乖離していました。
どう修正されたか
本文付きの POST リクエストの例に差し替えられました:
# post "/articles", params: { title: "Rails" }
request.media_type # => "application/x-www-form-urlencoded" # verifiedこれにより:
- フォームエンコードされたパラメータ (
params) を持つPOSTリクエストでは Content-Type: application/x-www-form-urlencodedが付与されrequest.media_typeが"application/x-www-form-urlencoded"を返す
という、実際の挙動と一致したサンプルになっています。
変更は actionpack/lib/action_dispatch/http/request.rb のコメント中の1行のみで、コードロジックには一切手が入っていません。
- 影響範囲・注意点
- 挙動変更は一切なく、「ドキュメントのサンプルが正しくなった」だけです。
- これにより、以下の点が明確になります:
GETリクエスト(かつContent-Typeなし)ではrequest.media_typeはnilを返す。media_typeに値が欲しい場合は、ボディとContent-Typeを伴うリクエスト(例: フォームPOST、JSON POSTなど)である必要がある。
- 既存アプリケーションやテストコードを変更する必要はありませんが、
- 「GET でもデフォルトで
application/x-www-form-urlencodedが入るはず」と思い込んでいた場合は、その前提が誤りであることを認識する必要があります。
- 「GET でもデフォルトで
- 参考情報 (あれば)
- 該当メソッド:
ActionDispatch::Request#media_type- 実際には
Content-Typeヘッダから MIME タイプ部分だけを取り出した値("application/json"など)を返し、ヘッダがない場合はnilを返します。
- 実際には
- 関連テスト:
test/dispatch/request_test.rb:1109- 「Content-Type のないリクエストでは
request.media_typeがnilである」ことを検証。
- 「Content-Type のないリクエストでは
- 挙動が変わった元コミット(言及されているもの):
86754a8f7b- デフォルトのリクエストエンコーダが Content-Type を付けなくなったことにより、古いドキュメント例が不正確になっていた経緯があります。
#58384 Allow config.active_storage.variant_processor to be set to a class
マージ日: 2026/8/7 | 作成者: @flavorjones
- 概要 (1-2文で)
Active Storage のconfig.active_storage.variant_processorに、これまでの:vips/:mini_magick/:disabledというシンボルだけでなく、任意のクラスを直接指定できるようにした PR です。これにより、独自の画像変換パイプラインやサンドボックス化された変換処理などを、Rails の設定レベルで差し替えられるようになります。
- 変更内容の詳細
2-1. これまでの問題点
Active Storage では、添付ファイルに対して以下の3フェーズがあります。
- analysis(メタ情報抽出):
config.active_storage.analyzersでクラス配列として差し替え可能 - preview generation(プレビュー生成):
config.active_storage.previewersでクラス配列として差し替え可能 - variant processing(画像のリサイズ・トリミングなどの変換):
ここだけconfig.active_storage.variant_processorが:vips/:mini_magick/:disabledの3つに固定されており、クラスを直接渡せなかった
内部的には、ActiveStorage::Engine で次のようにシンボルをクラスにマッピングしていました:
ActiveStorage.variant_transformer =
case ActiveStorage.variant_processor
when :disabled
ActiveStorage::Transformers::NullTransformer
when :vips
ActiveStorage::Transformers::Vips
when :mini_magick
ActiveStorage::Transformers::ImageMagick
endただし ActiveStorage.variant_transformer はドキュメント化されていない mattr_accessor であり、アプリケーションから正式には利用できないため、「variant 処理だけ差し替えたい」というニーズに応えにくい状態でした。
2-2. 新しい挙動:クラスを直接指定可能に
config.active_storage.variant_processor に クラスを直接指定 できるようになりました。
# 例: カスタムのトランスフォーマークラスを使用
config.active_storage.variant_processor = CustomTransformer- これまで通り、以下のシンボルも有効です:
:vips:mini_magick:disabled
- 上記シンボルが指定された場合は、従来通り
ActiveStorage::Transformers::Vipsなどのビルトインのクラスにマッピングされます。 - クラスが指定された場合は、そのクラスがそのまま variant 処理に利用されます。
実装上の契約(インターフェイス)
指定するクラスは、ActiveStorage::Transformers::Transformer が定めるインターフェイスを実装している必要があります。
- ただし、継承は必須ではない:
ActiveStorage::Transformers::Transformerを継承しなくてもよい- 必要なメソッドを実装していれば動作する(ダックタイピング)
- 型チェックなどは行わず、「呼び出せるかどうか」に依存しているため、間違ったインターフェイスで実装すると実行時エラーになります。
transformer.rb にも多少手が入っており、インターフェイスの役割がより明確化されています(実装契約を示す抽象層としての位置付け)。
2-3. 設定値が不正な場合のエラーが早期化
config.active_storage.variant_processor に不正な値を渡した場合の挙動が変わりました。
- 以前: 不正な値を設定しても起動時には気づかず、実際に variant を生成したタイミングで
NoMethodErrorが発生しがちだった - 今回の変更後: アプリケーション起動時に
ArgumentErrorを送出 するようになり、早い段階で誤設定に気づけるようになりました
これにより、本番で初めて画像変換を通したときに落ちる、のような事故を防ぎやすくなります。
2-4. ドキュメント・テストの更新
guides/source/configuring.mdconfig.active_storage.variant_processorの説明に「クラスを渡せる」ことと、その前提条件(必要なインターフェイス)が追記されています。
railties/test/application/active_storage/custom_processors_integration_test.rb- カスタムのプロセッサークラスを指定した場合の統合テストが追加され、実際に画像変換フローを通して検証されています。
railties/test/application/active_storage/engine_integration_test.rb- 不正な設定値など、起動時のバリデーション挙動を確認するテストが追加。
activestorage/CHANGELOG.mdにも、挙動変更・新機能として記載あり。
- 影響範囲・注意点
3-1. 互換性・マイグレーション
- 既に
:vips/:mini_magick/:disabledを使っているアプリは、そのまま動作します。 ActiveStorage.variant_transformerを直接いじっていたような非公式な使い方から、config.active_storage.variant_processor = CustomTransformerに移行するのが推奨されます。
3-2. カスタムクラス利用時の重要な注意点
「variant_processor にカスタムクラスを指定するときは、analyzers もカスタムにする必要がある(現時点では)」
- ビルトインの image analyzer は、
variant_processorが:vipsまたは:mini_magickのときだけ blob を受け付ける設計になっています。 - そのため、rubyのようにカスタムクラスを指定した場合は、
config.active_storage.variant_processor = CustomTransformerconfig.active_storage.analyzersにも対応するカスタム analyzer を追加する必要があります。 - analyzer 側でどのようなメタ情報を想定するか(transformer が必要とする情報の収集等)を設計しておく必要があります。
3-3. セキュリティ面の注意(変換パラメータのバリデーション)
現在、変換パラメータのホワイトリストは以下のようにばらついています。
ActiveStorage::Transformers::ImageMagick:- 変換名・引数の allowlist を内部に持ち、ある程度のバリデーションを行っている
ActiveStorage::Transformers::Vips:- この allowlist を適用していない
この PR 自体は、「variant_processor をクラスにできる」という抽象化だけを追加しており、ホワイトリストの共通化・移動などは行っていません。
PR 作成者は、将来的には analyzer/transformer/previewer 各クラスにバリデーションを持たせるべき とコメントしており、セキュリティポリシーをどうするかは各 transformer 実装側の責任になります。
カスタム transformer を実装する場合:
- 外部から渡される transformation オプション(例:
resize_to_limit: [100, 100]など)のバリデーションを必ず自前で実装するべきです。 - ImageMagick の wrapper を自作する場合は、
ActiveStorage::Transformers::ImageMagickの allowlist 実装を参考に、コマンドインジェクション等を防ぐためのロジックを取り込むとよいです。
3-4. エラー検知タイミングの変更による副作用
- 今回、認識されない
variant_processorの値に対しては 起動時にArgumentErrorを投げるように変更されています。 - 設定値を動的に変えていたり(例えば ENV 経由で)、テストで異常系を検証している場合は、挙動が「実行時エラー → 起動時エラー」に変わっている点に注意が必要です。
- 参考情報 (あれば)
- 対象 PR: Allow
config.active_storage.variant_processorto be set to a class #58384 - 変更された主なファイル:
activestorage/app/models/active_storage/variant.rbactivestorage/lib/active_storage/engine.rbactivestorage/lib/active_storage/transformers/transformer.rbguides/source/configuring.mdrailties/test/application/active_storage/custom_processors_integration_test.rb
- 実装時のポイント:
- 「抽象レイヤー(インターフェイス)を整えた」だけであり、新たな transformer 実装自体はこの PR には含まれていません。
- 実際のユースケースとしては、サンドボックス化・リモート変換サービス連携・GPU ベースの変換クラスなどを
CustomTransformerとして切り出し、variant_processorで差し替える、という使い方が想定されています。
#58399 Active Record ivar ractor safety
マージ日: 2026/8/7 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Active Record 内で一部のインスタンス変数が Ractor(マルチスレッド並列実行用の仕組み)上で安全に参照できない問題を解消し、Ractor でも Active Record を扱えるようにするための「Ractor セーフティ」対応 PR です。特に、メモ化済みのインスタンス変数を freeze したり、必要に応じて main Ractor に処理を委譲する形に変更しています。
- 変更内容の詳細
背景・問題点
- Ractor 間でオブジェクトを共有するには「共有可能(shareable)」である必要があり、その代表的な条件が「freeze されていること」です。
- Active Record 内では、パフォーマンスのためにいくつかの値をインスタンス変数にメモ化していますが、これらが非 frozen のまま保持されていると、main Ractor 以外でアクセスしたときに Ractor セーフではなくなります。
- また、メモ化が eagerly(早期)に実行されると、そのタイミングの Ractor に依存したままになり、他 Ractor からのアクセスで問題が生じ得ます。
この PR は、そういったメモ化インスタンス変数の扱いを見直し、
- 代入時に freeze する
- 場合によっては main Ractor で初期化したものを共有するようにする
ことで、Ractor セーフティを確保しています。
対象となったインスタンス変数
説明文ベースで、次のインスタンス変数の扱いが変更されています。
@symbol_column_to_string_name_hash- シンボルカラム名と文字列カラム名の対応をキャッシュしているハッシュと思われます。
- 代入時に値を freeze し、Ractor 間で共有しても問題ない immutable な状態にします。
@_returning_columns_for_insert- INSERT 時に RETURNING 句で返すカラム情報をキャッシュしていると思われます。
- これも代入時に freeze されるようになり、Ractor 上から参照可能になります。
@query_constraints_list- モデルに対する制約(デフォルトスコープ・シングルテーブル継承等からくる種々の制約)をキャッシュした配列/構造体である可能性が高いです。
- 代入時に freeze されるので、後から破壊的変更をしない前提で Ractor セーフに。
@table_name- モデルのテーブル名のキャッシュ。
- Ractor 間で共有される前提なので、ここも freeze されます。
これらはいずれも「一度計算したら以降は変更されないことが前提の値」であり、freeze によってその前提を Ruby レベルで明示しつつ、Ractor の shareable 要件も満たす形になっています。
Arel / PredicateBuilder 周り
説明文中で、次の 2 つのインスタンス変数についても対応が入ったとされています。
@predicate_builder(型:ActiveRecord::PredicateBuilder)@arel_table(型:Arel::Table)
変更点のポイント:
Arel::Table の freeze 対応
activerecord/lib/arel/table.rbが変更され、Arel::Tableインスタンスが freeze された状態でも安全に扱えるようになっています(イミュータブル想定を強化した形)。- Ractor 間で
@arel_tableを共有するには、このオブジェクト自体が shareable である必要があり、そのために freeze 可能な設計に近づけています。
PredicateBuilder の freeze 対応
activerecord/lib/active_record/relation/predicate_builder.rbが変更され、ActiveRecord::PredicateBuilderも freeze されても問題なく使えるように手当てされています。- メモ化された
@predicate_builderを freeze して Ractor 間共有に耐えられるようにしたものと考えられます。
メモ化の「lazy 化」と main Ractor へのディスパッチ
- 説明には「メモ化パターンも lazy にする必要があるため、そのケースでは main Ractor への dispatch を追加した」とあります。
- すなわち、
- main Ractor で初回計算 & freeze → shareable なオブジェクトとして確定
- 他 Ractor からは、その既に shareable なオブジェクトを参照するだけ
- というフローにすることで、
- 非 main Ractor 上で新たに non-shareable なオブジェクトが生成・代入される
- という事態を防いでいます。
- 実装的には、インスタンス変数がまだ未設定で、かつ現在の Ractor が main ではない場合に、main Ractor に「その ivar の初期化を依頼して結果を返してもらう」ようなディスパッチ処理が入っていると推測されます。
テストの追加・変更
次のテストファイルに変更が入っています。
activerecord/test/cases/arel/table_test.rbactiverecord/test/cases/base_test.rbactiverecord/test/cases/primary_keys_test.rbactiverecord/test/cases/relation/predicate_builder_test.rbactiverecord/test/cases/arel/helper.rb
主に以下を確認するテストが追加されていると考えられます。
Arel::TableやActiveRecord::PredicateBuilderが freeze された状態で正しく動作すること- 対象のインスタンス変数が freeze されていること(
frozen?の確認) - Ractor 環境(もしくはそれを模した状況)で例外が発生しないこと
- 影響範囲・注意点
影響範囲
- Active Record の内部キャッシュ・メモ化周り(テーブル名、クエリ制約、Arel 関連オブジェクトなど)。
- Ractor を利用している/利用しようとしている Rails アプリケーションにポジティブな影響があります(Ractor 上で Active Record を使いやすくなる)。
注意点(アプリケーション側)
freeze 前提の設計になる
@table_nameや@arel_tableなど、Active Record 内部のインスタンス変数にアプリケーション側から直接手を入れている場合(Monkey patch やメタプログラミングで上書きしているなど)、- freeze による
FrozenError - Ractor shareable 制約
に引っかかる可能性があります。
通常の使い方(Active Record の公開 API のみ使用)であれば問題は出ない想定です。
- freeze による
破壊的変更を前提にしないこと
- Arel オブジェクトや PredicateBuilder の内部構造へ直接アクセスし、破壊的変更(
<<,push,merge!,[]=など)を行っているようなコードは、freeze 後にエラーになります。 - そのようなコードがある場合は、
- 新しいオブジェクトを生成して返す
- あるいは Ractor 非対応前提のままにする
といった見直しが必要です。
- Arel オブジェクトや PredicateBuilder の内部構造へ直接アクセスし、破壊的変更(
Ractor を使う場合の前提整理
- この PR は「Active Record の特定のメモ化 ivar を Ractor セーフに近づけた」ものであり、Active Record 全体が完全に Ractor 対応済みになったことを意味するわけではありません。
- Ractor で Rails / Active Record を本格的に使う場合は、他の部分(Logger、Cache、Config オブジェクトなど)についても shareable / thread-safe かどうかを検証する必要があります。
- 参考情報 (あれば)
- Ruby Ractor 概要:
- Ractor 関連の Ruby doc: https://docs.ruby-lang.org/en/master/doc/ractor_md.html
- Ractor 間で共有できるオブジェクトの条件として「freeze されていること」が重要です。
- Arel / PredicateBuilder の役割:
Arel::Table: テーブルを表すオブジェクトで、カラムや SQL AST を組み立てる基盤。ActiveRecord::PredicateBuilder: where 句などの条件をArelノードへ変換するためのビルダ。
これらが freeze 可能になったことで、「クエリビルド用の構造をイミュータブルにして共有する」という設計に近づいています。
この PR 自体は主に内部実装の改善ですが、将来的な Rails の Ractor 対応やマルチコア並列実行の基盤整備として重要な変更です。
#58359 Allow to configure ERB options through ActionView::Base
マージ日: 2026/8/7 | 作成者: @Edouard-chin
- 概要 (1–2文で)
Rails の ERB テンプレートハンドラの各種オプションを、ActionView::Base(およびconfig.action_view.*)経由で公式に設定できるようにした PR です。これにより、これまで非公開扱い(:nodoc:)となったActionView::Template::Handlers::ERBクラスを直接参照して行っていた設定を、公開インターフェイスから行えるようになります。
- 変更内容の詳細
背景
- 過去のコミット
970bf380fe8でActionView::Template::Handlers::ERBに:nodoc:が付与され、ドキュメント上「非公開 API」と明示された。 - しかし一部アプリケーションでは、このクラスを直接参照して ERB の各種オプション(例: trim mode 以外の設定)を行っていた。
erb_trim_modeは既にconfig.action_view.erb_trim_modeなどから設定できたが、それ以外の ERB ハンドラのオプションは公式な経路がなかった。
この PR は「ERB ハンドラの設定は ActionView::Base/config.action_view から全部できるようにする」という方針の整理です。
具体的な変更点
ActionView::Baseに ERB オプション用の設定を追加ActionView::Baseに、ERB ハンドラが利用する全オプションを委譲するための設定項目が追加・整理されています。
すでに存在していたerb_trim_modeに加え、同様のスタイルで他のオプションも定義されました。典型的な利用イメージ(Rails アプリ側):
ruby# config/application.rb または各環境の設定ファイルで module MyApp class Application < Rails::Application # すでに可能だった設定 config.action_view.erb_trim_mode = '-' # 今回の PR でサポート拡張されたイメージ # (実際のオプション名は ERB ハンドラ内のものに対応) config.action_view.erb_variant = :html config.action_view.erb_escaper = MyCustomEscaper # など、ERB ハンドラ側にあるオプションを一通り ActionView 経由で設定できる end end実際には、
ActionView::Baseのクラス属性 (class_attribute) もしくは設定アクセサが増え、それをActionView::Template::Handlers::ERBに伝搬させる形になっています。ActionView::Railtieの設定連携の見直しactionview/lib/action_view/railtie.rbで、config.action_viewとActionView::Baseの設定同期部分が 1 行修正されています。- これにより、
config.action_view.<erb関連の設定>に値を入れると、自動的にActionView::Baseに反映され、その結果 ERB ハンドラのオプションに正しく渡るようになります。
テストの追加
actionview/test/actionpack/abstract/abstract_controller_test.rbに 56 行分のテストが追加。ActionView::Baseを通して設定した ERB オプションが実際のテンプレートレンダリングに反映されるかを検証。
railties/test/application/configuration_test.rbも調整され、config.action_view→ActionView::Base→ ERB ハンドラ、という設定ルートが期待通り動作することを確認。
ドキュメントと CHANGELOG の更新
guides/source/configuring.mdに、ERB 関連の設定をconfig.action_viewから行う方法が追記。actionview/CHANGELOG.mdに、この機能追加が明示的に記載され、外部向けに「ERB オプションを ActionView 経由で設定できるようになった」ことが周知されています。
- 影響範囲・注意点
推奨される使用方法の変化
- これまで
ActionView::Template::Handlers::ERBクラスを直接参照していたアプリは、今後はActionView::Base/config.action_viewを経由して設定するのが推奨ルートになります。 :nodoc:が付いているクラスは、今後の変更で互換性が壊れる可能性が高いため、依存は避けるべきです。
- これまで
既存アプリへの互換性
- 既に使われていた
config.action_view.erb_trim_modeはそのまま動作します。 - 新しく追加された設定項目は、デフォルトでは従来の挙動と同じになるように実装されているのが通常で、この PR だけで既存アプリの挙動が変わる可能性は低いです。
- ただし、アプリ側で
ActionView::Template::Handlers::ERBを直接いじっていた場合は、今後の Rails アップデートに備えて設定方法を移行しておくと安全です。
- 既に使われていた
設定箇所の一元化
- ERB 関連の設定が
ActionView::Baseおよびconfig.action_viewに集約されることで、ビュー設定の見通しが良くなり、環境ごとに ERB の挙動(トリムモード・エスケープ戦略など)を切り替えるのが容易になります。
- ERB 関連の設定が
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58359
- 関連コミット:
970bf380fe8(ActionView::Template::Handlers::ERBに:nodoc:が付いた変更) - 設定ガイド(本 PR で更新):
guides/source/configuring.mdconfig.action_viewセクション内に ERB 関連オプションの説明が追加されているので、具体的なオプション名と使い方はそこを参照するのが確実です。
#58060 Ractor compatible Active Support Notifications
マージ日: 2026/8/7 | 作成者: @Edouard-chin
- 概要 (1-2文で)
ActiveSupport::Notifications を Ractor 内でも安全に利用できるようにするため、Ractor ごとに独立した Notifier を持つ仕組みに変更した PRです。既存のAS::Fanoutを直接 Ractor-safe にするのではなく、「購読情報のスナップショット」をもとに Ractor 単位で Notifier を生成する設計になっています。
- 変更内容の詳細
背景と課題
- これまで
ActiveSupport::Notificationsは Ractor 内から安全に使えませんでした。 - 内部で使っている
ActiveSupport::Notifications::Fanoutが- 購読者リストをキャッシュする
- 通知名(例:
"sql.activerecord","foo.bar")に応じて動的にキャッシュが増える
という性質を持ち、これが Ractor 間で共有されるとスレッドセーフ / Ractor セーフにするのがほぼ不可能、というのが問題です。
方針
- 「1つのグローバル Fanout を Ractor-safe にする」のではなく、
- Ractor ごとに独立した Notifier を持つ
- その Notifier は「メイン Ractor で管理されている購読情報のスナップショット」から初期化される
- これにより、「Ractor 間で共有される可変オブジェクト」を避けつつ、Notifications の API はそのまま使えるようにしています。
実装のポイント
※ PR 本文から分かる設計を中心に整理します。
1. 購読情報のスナップショット
ActiveSupport::Notifications.subscribeが呼ばれるたびに、- グローバルな「購読一覧」を更新
- その一覧の「凍結されたスナップショット」を作る
- 各 Ractor は、このスナップショットをもとに自前の Notifier を初期化します。
- スナップショットは
freeze済みなので、Ractor 間で共有しても安全です(Ruby の Ractor の要件)。
イメージ(擬似コード):
module ActiveSupport
module Notifications
@subscriptions = []
@subscriptions_snapshot = [].freeze
def self.subscribe(pattern = nil, &block)
# 実際にはもっと複雑だが、概念イメージ
subscription = Subscription.new(pattern, block)
@subscriptions << subscription
# 新しいスナップショットを作る
@subscriptions_snapshot = @subscriptions.map(&:dup).freeze
subscription
end
def self.subscriptions_snapshot
@subscriptions_snapshot
end
end
end(実際のコードはもう少し抽象化されていますが、概念としては「subscribe 時にスナップショットを更新する」形です)
2. Ractor ごとの Notifier 生成
- Ractor から
ActiveSupport::Notifications.instrument等を呼ぶとき、- その Ractor 専用の Notifier がなければ生成
- 生成時に
Notifications.subscriptions_snapshotを読み込み、AS::Fanoutに反映
- こうして、
- 各 Ractor は「自分専用の Fanout / Notifier」
- その中身(購読者リスト)は「メイン Ractor で確定されたスナップショット」
という構成になります。
擬似コード:
module ActiveSupport
module Notifications
RACTOR_LOCAL_KEY = :__as_notifications_notifier__
def self.notifier_for_current_ractor
Ractor.current[RACTOR_LOCAL_KEY] ||= begin
notifier = Notifier.new
notifier.configure_from_snapshot(subscriptions_snapshot)
notifier
end
end
def self.instrument(name, payload = {}, &block)
notifier_for_current_ractor.instrument(name, payload, &block)
end
end
end3. AS::Fanout への小変更
activsupport/lib/active_support/notifications/fanout.rbに 5 行だけ変更が入っており、- Ractor 向けの利用を許容するため、
- 「外部からまとめて購読情報を設定できる / スナップショットから初期化できる」ような API または内部フックが追加されていると考えられます。
- Fanout 自体を Ractor-safe にしたわけではなく、「Ractor ごとの孤立したインスタンスなら安全に使える」前提で使うための変更です。
4. テスト追加
activesupport/test/notifications_test.rb(+55行)- Ractor 内から Notifications を利用できること
- 購読が正しく反映されること
- 複数 Ractor が同時に使っても問題ないこと
などをカバーするテストが追加されています。
railties/test/application/initializers/frameworks_test.rb(+49行)- Rails アプリケーション初期化時に Ractor 対応が正しく行われるか
ActiveSupport::Ractors周りの設定と併せて、Notifications が壊れていないか
などを検証していると考えられます。
- 既存テスト (
activerecord/test/...) も一部修正されているため、Ractor 対応によって挙動が変わる部分(特にデフォルトスコープや通知周りのタイミング)がテストに影響しており、それに合わせて期待値やテスト構造を微調整していると思われます。
- 影響範囲・注意点
影響範囲
ActiveSupport::Notificationsを使うすべてのコード(Rails 本体やアプリケーションコード)が間接的に影響を受けますが、- 公開 API (
instrument,subscribe,unsubscribeなど) は変わらない設計です。
- 公開 API (
- ただし、内部的な挙動は以下のように変わります:
- 通知の配送は「Ractor ローカルな Notifier」に対して行われる
- 新しく登録された購読は、「その後に生成される Ractor の Notifier」からしか見えない可能性がある
(※特に「Ractor 生成 → subscribe → 既存 Ractor から instrument」の順序のようなケースでは、既存 Ractor の Notifier に購読が反映されない)
注意点 / 想定される落とし穴
Ractor 生成タイミングと subscribe のタイミング
- 実装から推測される挙動として、
- Ractor A 作成
- メイン Ractor で
Notifications.subscribe呼び出し - Ractor A 内で
instrument
→ Ractor A の Notifier は「Ractor A 生成時点のスナップショット」を持っており、その後の subscribe が反映されない可能性があります。
- 最も安全なのは:
- Ractor を生成する前に必要な subscribe をすべて済ませる
- Ractors を多用するアプリでは、「購読の追加タイミング」と「Ractor 生成タイミング」を意識する必要があります。
- 実装から推測される挙動として、
Ractor 内で subscribe した場合の扱い
- PR の説明から見ると、「
AS::Notifications.subscribeが呼ばれたときにスナップショットを更新」という設計なので、- Ractor 内から subscribe しても、その Ractor の Notifier を「取り替える」のか、「現 Notifier にだけ購読が付く」のかは実装依存です。
- 一般的には、Ractor 内で subscribe するのは避け、メイン Ractor 側で行う方が安全です。
- PR の説明から見ると、「
Fanout を直接触っているコード
- 非推奨だが、ライブラリやアプリが
ActiveSupport::Notifications::Fanoutを直接使っている場合、- 新しく追加された内部 API(スナップショット初期化関連)を前提にすることで Ractor 対応を行える可能性があります。
- ただし Fanout は内部 API なので、将来的な互換性は保証されません。
- 非推奨だが、ライブラリやアプリが
- 参考情報 (あれば)
- Ruby Ractor の制約:
- 共有できるのは「イミュータブル(freeze 済み)」なオブジェクトか、特別に Ractor 間共有を許可されているオブジェクトのみ。
- 今回の「購読情報スナップショットを freeze して Ractor 間で共有する」という設計は、この制約に沿った典型的なパターンです。
- 実運用上の指針:
- 通知の購読 (
subscribe) は、Rails 初期化時(config/initializers/*.rb)など「Ractor 生成前」にまとめて行う。 - Ractors を使う処理の中では、
instrumentなどの発火側に専念し、購読側の構造を動的に変えないようにする。
- 通知の購読 (
#58405 Add back loaded to RoutesReloader
マージ日: 2026/8/7 | 作成者: @rafaelfranca
- 概要 (1-2文で)
Rails::Application::RoutesReloaderから一度削除されていたloadedメソッド(属性)が、フレームワークの公開APIであることが判明したため復活させたPRです。これにより、既存アプリやライブラリがRoutesReloader#loadedに依存していても、引き続き問題なく動作できるようになります。
- 変更内容の詳細
※ 実際のコード断片はPR本文に含まれていませんが、変更ファイルと行数から推測できるレベルで説明します。
RoutesReloader に loaded を再追加
対象ファイル:
railties/lib/rails/application/routes_reloader.rb(+5/-0)
行数から見ると、次のような変更が入っている可能性が高いです(イメージの擬似コードです):
module Rails
class Application
class RoutesReloader
# 例: 再追加されたアクセサ
attr_reader :loaded
def initialize(...)
@loaded = false
...
end
def reload!
...
@loaded = true
end
# あるいは、以前と同じインターフェースを完全に再現している:
# attr_accessor :loaded
end
end
endPR説明文にある通り、loaded は一度別のPR(#58225)で削除されましたが、実は「Railsの公開API」として外部から利用されているメソッド/属性だったため、互換性維持のために元に戻されています。
「公開APIである」とは、主に以下を意味します。
- Railsの公式クラス/モジュール上の publicメソッド として外部からの利用を想定している
- マイナーアップデートやパッチレベルアップデートで後方互換性を維持すべき対象
今回のPRでは、その契約を守るために loaded が復活しています。
テストの追加・修正
対象ファイル:
railties/test/application/routes_reloader_test.rb(+30/-4)
主に以下のようなテストが追加されていると考えられます(擬似コード):
class RoutesReloaderTest < ActiveSupport::TestCase
test "routes reloader exposes loaded flag" do
reloader = Rails.application.routes_reloader
# 初期状態
assert_equal false, reloader.loaded
# reload 後に true になるなど、想定されたAPI挙動を確認
reloader.reload!
assert_equal true, reloader.loaded
end
endポイントは:
loadedが存在すること(NoMethodErrorにならないこと)- その値が、ルーティングがロード済みかどうかの状態を表していること
といった、公開APIとしての振る舞いを明確にテストしている点です。
これにより、将来の変更で loaded がうっかり削除・変更されるリグレッションを防ぎます。
- 影響範囲・注意点
影響範囲
Rails.application.routes_reloader.loadedを参照しているアプリケーション/エンジン/gemRoutesReloaderのリロード状態をフックして独自処理をしているツール
以前のPR(#58225)で loaded が削除されたことにより、上記のようなコードは NoMethodError になっていた可能性がありますが、このPRにより再び正常動作するようになります。
開発者視点でのポイント
RoutesReloader#loadedは、今後も後方互換性を期待できる公開API とみなしてよいです。ルーティングのリロード状態をウォッチする必要がある場合、次のようなコードが公式にサポートされる形で書けます:
rubyreloader = Rails.application.routes_reloader if reloader.loaded # ルートがロード済みのときだけ実行したい処理 end一方で、
loadedの意味はあくまで「RoutesReloaderがルートをロード済みかどうか」であり、「アプリケーションが完全にブートストラップ済みか」など別の意味ではないので、その点は誤用に注意が必要です。
- 参考情報 (あれば)
- このPRで復活させられたメソッドは、以下のPRで一度削除されていました:
- Railsの公開APIの扱い・後方互換性ポリシーは公式ガイドにも言及があります(Railsのアップグレードガイド等)。
- RoutesReloader 自体の利用例は、主に開発モードでのルートの自動リロードなど、フレームワーク内部処理が中心であり、アプリケーションコードから直接触るケースは比較的少ないですが、エンジン開発や高度なメタプログラミング系gemでは利用されることがあります。
#58397 Extract duplicated UPDATE self-join preparation into Arel::Visitors::…
マージ日: 2026/8/7 | 作成者: @seuros
- 概要 (1-2文で)
PostgreSQL と SQLite の Arel Visitor に重複していた UPDATE の self join 用の前処理ロジックが、共通のArel::Visitors::ToSql#prepare_update_statement_with_self_joinに抽出されました。挙動の変更はなく、内部実装のリファクタリングのみです。
- 変更内容の詳細
もともとの状態
Arel::Visitors::PostgreSQLとArel::Visitors::SQLiteにおいて、prepare_update_statementメソッド内部に- self join を伴う UPDATE 文を組み立てるための前処理ロジック
- そのための条件チェック(ガード)
- があり、その「本体ロジック部分」がほぼコピペ状態で重複していました。
概念的にはこういった構造だったと考えられます(疑似コード):
# postgresql.rb
def prepare_update_statement(o)
if uses_self_join?(o)
# ↓ ここが PostgreSQL / SQLite で重複していた本体処理
do_some_self_join_preparation(o)
else
super
end
end
# sqlite.rb
def prepare_update_statement(o)
if uses_self_join?(o)
# ↓ PostgreSQL と同じ実装
do_some_self_join_preparation(o)
else
super
end
end今回の変更点
共通ヘルパーの追加
activerecord/lib/arel/visitors/to_sql.rbに以下のメソッドが追加されています(名称は PR 説明より):ruby# Arel::Visitors::ToSql def prepare_update_statement_with_self_join(o) # これまで PostgreSQL / SQLite 両方に重複して書かれていた # self join UPDATE 用の前処理ロジックがここに移植された endここに、PostgreSQL と SQLite の
prepare_update_statement内に書かれていた「self join の UPDATE を組み立てるための共通ロジック」が集約されています。PostgreSQL / SQLite Visitor から共通メソッドを呼び出すように変更
postgresql.rb/sqlite.rbでは、それぞれが持っていたprepare_update_statementの中で、 自前で本体ロジックを書くのではなく、上記の共通ヘルパーを呼び出すようになりました。構造としては以下のようなイメージになります:
ruby# postgresql.rb def prepare_update_statement(o) if uses_self_join?(o) # 各 Visitor 固有のガード判定は維持 prepare_update_statement_with_self_join(o) # 共通ヘルパー呼び出し else super end end # sqlite.rb def prepare_update_statement(o) if uses_self_join?(o) # SQLite 版のガード prepare_update_statement_with_self_join(o) # 共通ヘルパー呼び出し else super end end- 各 Visitor ごとの「self join を扱うかどうか」の条件(ガード)は、そのまま Visitor ごとに保持。
- 実際の SQL 準備処理(self join UPDATE のための構築ロジック)のみを共通化。
MySQL Visitor には変更なし
Arel::Visitors::MySQLはもともとprepare_update_statementを独自にオーバーライドしており、この PR の共通化対象には含めていません。- MySQL 用の UPDATE 自己結合の処理は今まで通りで、挙動に影響なし。
- 影響範囲・注意点
実行時挙動
- PR 説明にある通り、「No behavior change」と明示されており、外部 API / SQL 出力 / クエリ結果に変更はありません。
- 影響するのは PostgreSQL / SQLite における「self join を伴う UPDATE 文」を Arel が生成するときの内部処理のみです。
対象 DB
- PostgreSQL / SQLite のみが今回の共通化対象。
- MySQL は今回の変更の影響を受けません。
メンテナンス面
- 将来的に self join UPDATE の実装を修正する必要が出た場合、
ToSql#prepare_update_statement_with_self_joinのみを変更すれば PostgreSQL / SQLite 両方に反映されます。 - DB ごとの差異は依然として「ガード部分」で吸収されるため、共通ロジックの中は「両者で本質的に同じ振る舞い」の部分に集中できます。
- 将来的に self join UPDATE の実装を修正する必要が出た場合、
互換性・移行面
- パブリック API の追加・削除はなく、Arel の内部メソッドレベルのリファクタリングにとどまります。
- 直接
Arel::Visitors::PostgreSQL#prepare_update_statementなどを monkey patch しているような特殊なコードを書いていない限り、アプリケーション側での対応は不要です。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58397
- 関連箇所:
activerecord/lib/arel/visitors/to_sql.rbactiverecord/lib/arel/visitors/postgresql.rbactiverecord/lib/arel/visitors/sqlite.rb
この変更は「コード重複の排除とメンテナンス性向上」を目的とした内部リファクタリングであり、self join な UPDATE クエリのバグ修正や仕様変更ではありません。
#58400 Extract duplicated local cache multi-read resolution in Strategy::LocalCache
マージ日: 2026/8/7 | 作成者: @seuros
- 概要 (1-2文で)
ActiveSupport::Cache::Strategy::LocalCache内で、fetch_multiとread_multi_entriesに重複して書かれていた「ローカルキャッシュからの複数キー解決処理」が共通メソッドに抽出されました。挙動の変更はなく、内部実装のリファクタリングです。
- 変更内容の詳細
背景
Strategy::LocalCache は、スレッドローカルなどにぶら下がる「ローカルキャッシュ」を持ち、同じリクエスト中に何度も同じキーを読む際に、バックエンドへのアクセスを減らすために使われます。
fetch_multi と read_multi_entries はどちらも「複数キーをまとめて読む」メソッドですが、
- すでにローカルキャッシュに入っている値を取り出す
- 入っていないキーだけをバックエンドから読む
- 読めた値をローカルキャッシュに格納する
というロジックが両方のメソッドにほぼ同じ形で重複していた、というのが PR の対象です。
主な変更点
local_cache.rb 内で、以下のような形で重複ロジックが共通化されたと考えられます(イメージの擬似コード):
# 変更前(イメージ)
def fetch_multi(*names)
# ローカルキャッシュを見て既にある値を集める
local = {}
misses = []
names.each do |name|
if local_cache && local_cache.key?(name)
local[name] = local_cache[name]
else
misses << name
end
end
# misses だけバックエンドから取得して local_cache に詰める
# ...
end
def read_multi_entries(names, **options)
# 上とほぼ同じローカルキャッシュ解決処理
local = {}
misses = []
names.each do |name|
if local_cache && local_cache.key?(name)
local[name] = local_cache[name]
else
misses << name
end
end
# misses のみバックエンドへ
# ...
endこれを次のようなヘルパーメソッドに抽出:
def resolve_local_cache_multi(names)
local = {}
misses = []
names.each do |name|
if local_cache && local_cache.key?(name)
local[name] = local_cache[name]
else
misses << name
end
end
[local, misses]
endそして、fetch_multi / read_multi_entries はこのヘルパーを呼び出すだけに簡略化されています:
def fetch_multi(*names, **options)
local, misses = resolve_local_cache_multi(names)
# misses だけをバックエンドから fetch し、
# 結果を local_cache に格納しつつ local + backend_result を返す
end
def read_multi_entries(names, **options)
local, misses = resolve_local_cache_multi(names)
# misses のみバックエンド read_multi し、
# local + backend_result を返す
end行数としては、+12 / -25 なので、ほぼ「共通部分の切り出しと、それを呼び出すコードへの差し替え」のみの変更です。
- 影響範囲・注意点
- 公開 API (
Rails.cache.fetch_multi,read_multiなど) の挙動は変わりません。 - ローカルキャッシュ戦略 (
Strategy::LocalCache) 自体のインターフェースも変わらず、内部実装のみの変更です。 - 既存のアプリケーションコード・gem が、
Strategy::LocalCacheの内部実装詳細に依存していない限り、互換性の問題は発生しません。 - 重複コードが減ったことで、将来的にローカルキャッシュの動作変更やバグ修正を行う際、
fetch_multiとread_multi_entriesの挙動差異が生まれにくくなり、保守性が向上します。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58400
- 関連ドキュメント:
- ActiveSupport::Cache::Store#fetch_multi: https://api.rubyonrails.org/classes/ActiveSupport/Cache/Store.html#method-i-fetch_multi
- ActiveSupport::Cache::Strategy::LocalCache: https://api.rubyonrails.org/classes/ActiveSupport/Cache/Strategy/LocalCache.html
#58401 Extract duplicated job matching logic in ActiveJob::TestHelper
マージ日: 2026/8/7 | 作成者: @seuros
- 概要 (1-2文で)
ActiveJob::TestHelper 内でassert_enqueued_withとassert_performed_withがそれぞれ重複して持っていた「ジョブの条件マッチング」処理を、共通のプライベートメソッドfind_matching_jobに切り出した PR です。挙動は変えずに、重複コードを削減しテストヘルパー内部の保守性を高めています。
- 変更内容の詳細
何をしたか
activejob/lib/active_job/test_helper.rb内で、以下の2つのアサーションが共通で使うロジックを抽出しました。assert_enqueued_withassert_performed_with
- 抽出したメソッド名はプライベートメソッド
find_matching_jobで、与えられた条件オプションに一致するジョブをキュー(または実行履歴)の中から探す責務を持ちます。
変更前のイメージ
従来は assert_enqueued_with と assert_performed_with の中に、それぞれほぼ同じ形のロジックが書かれていました。典型的には次のような処理です(擬似コード):
def assert_enqueued_with(job: nil, args: nil, at: nil, queue: nil, &block)
# ...
matching_jobs = enqueued_jobs.select do |job|
# class, queue, args, scheduled時刻などをチェックする条件式
end
assert matching_jobs.any?, "Expected job to be enqueued with ..."
end
def assert_performed_with(job: nil, args: nil, at: nil, queue: nil, &block)
# ...
matching_jobs = performed_jobs.select do |job|
# 上とほぼ同じ条件式
end
assert matching_jobs.any?, "Expected job to be performed with ..."
endこれらの「条件式」部分が、それぞれのメソッド内にほぼ重複して存在していました。
変更後のイメージ
共通化された find_matching_job を導入し、2つのアサーションから呼び出すように変更しています:
def assert_enqueued_with(job: nil, args: nil, at: nil, queue: nil, &block)
# ...
matching_job = find_matching_job(enqueued_jobs, job: job, args: args, at: at, queue: queue)
assert matching_job, "Expected job to be enqueued with ..."
end
def assert_performed_with(job: nil, args: nil, at: nil, queue: nil, &block)
# ...
matching_job = find_matching_job(performed_jobs, job: job, args: args, at: at, queue: queue)
assert matching_job, "Expected job to be performed with ..."
end
private
def find_matching_job(jobs, job:, args:, at:, queue:)
jobs.find do |enqueued_job|
# jobクラス、キュー名、引数、実行予定時刻などの一致判定ロジック
end
end実際のコードでは引数名や判定条件は ActiveJob::TestHelper の仕様に沿ったものになっていますが、構造としては上記のように「集合(enqueued_jobs/performed_jobs)を受け取って条件に合うものを返す」形に抽象化されています。
行数の増減
- ファイルは1つのみ変更 (
activejob/lib/active_job/test_helper.rb) - 追加行: 25行
- 削除行: 34行
→ 重複部分をまとめたことで総行数は減っており、コードが簡潔になっています。
- 影響範囲・注意点
公開APIの挙動は変わらない想定
- 変更はあくまでプライベートメソッドの抽出・リファクタリングであり、
assert_enqueued_with/assert_performed_withのインターフェースや期待される挙動は変わりません。 - 既存のテストコード(これらのアサーションを利用している側)に影響はないはずです。
- 変更はあくまでプライベートメソッドの抽出・リファクタリングであり、
内部実装に依存しているコードへの潜在的影響
ActiveJob::TestHelperの内部実装に(モンキーパッチ・リフレクション等で)依存している場合は、条件マッチングロジックの位置が変わることで影響する可能性があります。- ただし、抽出されたメソッドは
privateであり、通常は直接利用される想定ではありません。
ロジックの一元化によるバグ修正容易性
- 将来的に「ジョブのマッチング条件」を変更・拡張する場合、
find_matching_jobだけを直せばよくなり、assert_enqueued_withとassert_performed_withの挙動差異が生まれにくくなります。 - これまで両方に同じ変更を入れる必要があった箇所が1か所に集約されるため、バグ混入のリスク減にもつながります。
- 将来的に「ジョブのマッチング条件」を変更・拡張する場合、
- 参考情報 (あれば)
- 対象コード:
activejob/lib/active_job/test_helper.rb
- 関連APIドキュメント:
- Active Job Testing —
assert_enqueued_with,assert_performed_with
https://guides.rubyonrails.org/active_job_basics.html#testing-jobs
- Active Job Testing —
- PR:
#58395 Extract duplicated bind value coercion in QueryMethods
マージ日: 2026/8/6 | 作成者: @seuros
- 概要 (1-2文で)
ActiveRecord::Relation::QueryMethods内で、SQLリテラル用のバインド値変換ロジックが重複していた部分を共通メソッド化して整理したPRです。挙動・機能は変わらず、内部実装のリファクタリングのみです。
- 変更内容の詳細
- 対象:
activerecord/lib/active_record/relation/query_methods.rb - 目的:
bind_value_for_sql_literalに関する「バインド値の型変換・正規化」処理が複数箇所で重複していたため、その重複コードを抽出し共通化。
具体的には、以下のようなイメージの変更が入っています(疑似コード):
# 変更前(イメージ)
def bind_value_for_sql_literal(attribute, value)
if value.is_a?(ActiveRecord::Relation)
# Relation 用の変換
elsif value.is_a?(Array)
# Array 用の変換
else
# それ以外の変換
end
end
def some_other_method(...)
# ↑とほぼ同じ変換ロジックを別メソッドでも書いている
end# 変更後(イメージ)
def coerce_bind_value_for_sql_literal(value)
if value.is_a?(ActiveRecord::Relation)
# Relation 用の変換
elsif value.is_a?(Array)
# Array 用の変換
else
# それ以外の変換
end
end
def bind_value_for_sql_literal(attribute, value)
coerce_bind_value_for_sql_literal(value)
end
def some_other_method(...)
coerce_bind_value_for_sql_literal(value)
endポイント:
- 「SQLリテラルに埋め込む前に、Relation / Array / その他のオブジェクトをどう扱うか」という変換処理が1つのヘルパーメソッドに集約された形です。
- コメントにもある通り「No functional change」と明示されており、外部から見える仕様は一切変えていません。
- 行数としては、重複コードが削られ、新しい共通メソッドの分だけ追加されているため、「削除 22行 / 追加 13行」という構成になっています。
- 影響範囲・注意点
影響範囲:
ActiveRecord::Relation::QueryMethods内部で SQL リテラルのバインディングを行うコードパス全般が、この共通化されたメソッドを通るようになっています。- ただし、既存処理をそのまま切り出しただけなので、型変換ロジック自体は変わっていません。
注意点:
- 公開APIの仕様変更はなく、アプリケーションコード側で修正すべき点はありません。
- 万一影響が出るとすれば、これまで「偶然通っていた」ようなマイナーなコードパスの挙動差ですが、PRの意図が「全く同じ変換の共通化」であるため、実質的に後方互換性は維持されています。
- Rails内部に対して monkey patch で
bind_value_for_sql_literalなどを直接上書きしている場合は、内部構造が少し変わったことで想定外の影響が出る可能性はあります(そのような使い方は非推奨ですが)。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58395
- 対象バージョン: マージ日時から見て、Rails 7.x もしくはそれ以降の開発ブランチ (main) への取り込みと考えられます。リリースノートでは「リファクタリング」として扱われる類の変更です。
#58382 Remove collector methods of unregistered Mime types
マージ日: 2026/8/6 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Mime::Type.unregisterで MIME タイプを削除した後にも、そのフォーマット向けの Collector メソッドが残り続けていた問題を修正し、適切にメソッドを削除してエラーメッセージを分かりやすくしました。これにより、未登録の MIME タイプに対するフォーマットメソッド呼び出し時に、従来の紛らわしいInvalidMimeTypeではなく、明確なNoMethodErrorが発生するようになります。
- 変更内容の詳細
背景となる問題
ActionController::MimeResponds などで使われる Collector(例: respond_to ブロック内の format.html, format.json など)は、Mime::Type.on_change(以前は Mime.register_callback)経由で、登録された MIME タイプごとにメソッドを動的に定義しています。
respond_to do |format|
format.html
format.json
endここで、例えば Mime::Type.register で独自フォーマット :foo を登録すると、Collector に #foo メソッドが生えます。
問題は、Mime::Type.unregister(:foo) で MIME タイプを削除しても、Collector 側の #foo メソッドが削除されない ことでした。その結果、format.foo は以下のような流れになっていました。
- まだ残っている
Collector#fooが呼ばれる - 内部で
Mime[:foo]を引くが、すでに unregister 済みなのでnilが返る Collector#custom(nil)のような形で呼ばれ、最終的にMime::Type.new(nil)が実行される- その結果
Mime::Type::InvalidMimeType (nil is not a valid MIME type)が発生
→ 実際の問題は「フォーマットが未登録」なのに、「nil は MIME type として無効」という紛らわしい例外になっていた。
今回の修正内容
この PR では、MIME タイプの削除(unregister)に追従して Collector 側のメソッドも除去するように変更されています。
ポイント:
Mime::Type.on_changeのハンドラで、- MIME タイプが「登録」されたとき: これまで通り Collector にメソッドを定義
- MIME タイプが「削除」されたとき: 対応する Collector のメソッドを
remove_methodで削除
- Collector にメソッドが無くなることで、そのフォーマット呼び出しは
Collector#method_missingにフォールバックします。 method_missingは「メソッドが定義されていない」という形でNoMethodErrorを発生させるため、- 「このフォーマットを使うには、先に MIME タイプとして登録する必要がある」という趣旨の、文脈に沿ったエラーになる。
PR description の冒頭には「Redefine the method to emit a deprecation warning」とありますが、最終的な実装は「メソッド削除」であり、実際には deprecation warning を挟まずにメソッドを消して NoMethodError に委ねる形になっています。
(説明文の後半にも「Removing the method routes the call through Collector#method_missing instead, which raises a NoMethodError...」とあり、こちらが実装に対応しています。)
テストの追加
actionpack/test/abstract/collector_test.rb で以下のようなケースがカバーされています(要約):
- MIME タイプを登録すると Collector にメソッドが生えること
- MIME タイプを unregister したあと、そのメソッドが呼ばれると
NoMethodErrorになること - MIME タイプを再登録すると、再びメソッドが使用可能になること
これにより、登録 → 削除 → 再登録のライフサイクルで Collector のインタフェースが一貫していることが保証されています。
- 影響範囲・注意点
影響を受けるのは、カスタム MIME タイプを登録・削除しているアプリケーションです。
例: initializer などでMime::Type.register "application/vnd.myapp+json", :myformatを行い、何らかのタイミングでMime::Type.unregister(:myformat)をしている場合。以前:
Mime::Type.unregister(:myformat)後にrespond_to { |f| f.myformat }を呼ぶとMime::Type::InvalidMimeType (nil is not a valid MIME type)が発生していた
変更後:
- 同じコードは
NoMethodError(「myformat というメソッドはない」)になる - 実質的には「すでに例外は投げていた」状態から、「より妥当で分かりやすい例外メッセージ」に変わっただけなので、互換性の観点での大きな破壊変更ではないと位置づけられています(PR 本文にも "No deprecation cycle: the stale method already raised, only the error message improves." と明記)。
- 同じコードは
アプリケーション側で、
InvalidMimeTypeを前提にした rescue やメッセージ判定をしている場合は、NoMethodErrorに変わる点に注意が必要です。- ただし、そのようなケースはレアと考えられ、一般的には「何が悪いのか分からない InvalidMimeType」から「使おうとしたフォーマットが登録されていない」という明確なバグシグナルに変わるため、デバッグ性は向上します。
MIME タイプを再登録した場合でも、以前と同様に Collector のメソッドが再定義されるため、再登録後の挙動は従来どおりです。
- 参考情報 (あれば)
- 当該 PR:
- 議論元になった PR とコメント:
- 関連クラス/機能:
ActionController::MimeResponds/AbstractController::CollectorMime::Type.register,Mime::Type.unregister,Mime::Type.on_change
#58391 Define shareable compiled_method_container readers
マージ日: 2026/8/6 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Rails のビューコンパイル周りで、compiled_method_containerのリーダメソッド定義を「Ractor で共有可能 (shareable)」な形に修正した PR です。テンプレートのコンパイル/レンダリングが Ractor 環境で動作するように、lambda によるクラスキャプチャのタイミングと方法を見直しています。
- 変更内容の詳細
※PR 本文から読み取れる主目的は「Ractor 互換のための shareable 化」です。該当差分は少ないですが、典型的なパターンは以下のような変更です。
背景
- Rails の
ActionView::Baseでは、テンプレートをコンパイルしたメソッドを格納するコンテナ (compiled_method_container) をクラスごとに持っています。 - そのコンテナにアクセスする reader メソッドを lambda で定義しており、その lambda が「どのオブジェクトをキャプチャしているか」によって、その Proc/lambda が Ractor 的に shareable かどうかが決まります。
- 以前のコードは、定義時のコンテキスト (たとえばベースクラス自身や非 shareable なオブジェクト) をキャプチャしてしまうため、Ractor 間で共有できない Proc になっていました。
修正のポイント
PR の説明文から読み取れる要点:
The lambda captures only the subclass, created after the assignment completes, so it can be made shareable.
- lambda がキャプチャするのは「サブクラス」だけになるように実装を変更しています。
- かつ、そのサブクラスは「代入が完了した後に生成される」ため、その時点で shareable な状態であり、lambda 自体も shareable にできる、という設計です。
おおよそのイメージコード:
変更前のイメージ (Ractor 非対応なパターン)
class ActionView::Base
class << self
def compiled_method_container
@compiled_method_container ||= begin
container = Class.new(self) # self や他の非 shareable なものを捕まえがち
# self や instance などを暗黙・明示的にキャプチャしてしまう
@compiled_method_container_reader ||= -> { container }
container
end
end
end
endこのようなコードでは、lambda が self や非 shareable なインスタンス変数をキャプチャしてしまい、Ractor で共有できないオブジェクトになります。
変更後のイメージ (shareable なパターン)
class ActionView::Base
class << self
def compiled_method_container
@compiled_method_container ||= begin
subclass = Class.new(self)
# 代入が完了した後に生成された subclass だけをキャプチャ
@compiled_method_container_reader = -> { subclass }
subclass
end
end
# 他の場所からは reader を通してコンテナにアクセスする
def compiled_method_container_reader
@compiled_method_container_reader
end
end
endポイント:
- lambda が「closure として抱える情報」を サブクラス (=コンテナ) だけに限定。
- かつ、そのサブクラスは「もうインスタンス変数にセットし終わった後に作成」されているため、Ractor 的に許可される shareable な形にできる。
- これにより、その lambda 自体を Ractor 間で安全に共有し、コンパイル済みテンプレートのコンテナ参照として利用できます。
テストの変更
actionview/test/template/lookup_context_test.rb にテストが追加 (+11 行) されています。主な確認内容は:
lookup_context/ActionView::Base周りで、compiled_method_containerの reader が期待通りのクラスを返すか。- その reader が shareable な形で定義されていること (Ractor を利用したテストや shareable? チェックがある可能性が高い)。
- 影響範囲・注意点
対象範囲
ActionView::Baseのテンプレートコンパイル・メソッド格納ロジック (compiled_method_container) 周辺。- テンプレートのレンダリング自体の外部 API (例:
render) には変更がない想定ですが、その内部で使われるコンパイル済みメソッドコンテナの取得方法が変わっています。
Ractor 利用時の影響
- Ractor 内からビューをレンダリングする場合や、Ractor を用いてテンプレートの事前コンパイル・キャッシュを行うようなユースケースで、以前はエラー/警告が出ていた部分が解消される可能性があります。
- shareable でない Proc を別 Ractor に渡そうとすると
Ractor::IsolationErrorになるため、その問題回避を目的とした変更です。
パフォーマンス・互換性
- 変更の性質上、コンテナクラスの生成タイミング・reader の定義方法が若干変わるのみで、パフォーマンスへの大きな悪影響は考えにくいです。
- 公開 API のシグネチャを変えているわけではないため、通常のアプリケーションコードで
ActionView::Baseを直接いじっていない限り、互換性問題はほぼありません。 - 一方で、独自に
compiled_method_containerやそのクラス構造を前提にメタプログラミングしている場合は、振る舞い・クラス階層が変わっていないか確認した方が安全です。
Ractor 非対応コードとの組み合わせ
- アプリケーション側が Ractor 非対応のオブジェクト (グローバル状態を持つシングルトン、スレッドローカルなど) をビュー周りで多用していると、今回の PR だけでは Ractor 対応は完結しません。
- この PR はあくまで「ActionView のテンプレートコンパイル部分での Ractor 非互換要素の一つを取り除いた」位置づけです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58391
- Ractor と shareable オブジェクトの概要:
- Ruby 公式ドキュメント (Ractor): https://docs.ruby-lang.org/en/master/Ractor.html
Object#shareable?/Ractor.make_shareableの挙動
- 関連しそうな過去 PR (推測):
- ActionView / ActiveSupport 周辺で「Ractor 対応」「shareable lambda/Proc」に言及している PR 群 (Rails 7.1 以降で散発的に進められている)
#58390 Make template lookup ractor-safe
マージ日: 2026/8/6 | 作成者: @etiennebarrie
- 概要 (1-2文で)
このPRは、テンプレートの lookup(LookupContext)と digest 計算(Digestor)周りを Ractor セーフにするための変更です。これによりメイン Ractor 以外からでもLookupContextの構築やテンプレート digest の計算が安全に行えるようになります。
- 変更内容の詳細
2-1. LookupContext の details デフォルト Proc を Ractor-shareable に
ActionView::LookupContext は、formats, variants, handlers などの「details」を内部で管理しており、そのデフォルト値は Proc で定義されています。
今回の PR では、この「details のデフォルト Proc」が Ractor 間で共有可能(Ractor-shareable)になるように修正されています。
背景:
- Ractor では「Ractor-shareable でないオブジェクト」を他 Ractor へ渡すことができません。
- 以前は
LookupContext内部のデフォルト Proc が shareable ではなく、メイン Ractor 以外でLookupContext.newを呼び出すと問題になる可能性がありました。
この PR により:
- 非メイン Ractor 内で
ActionView::LookupContext.newを直接生成しても安全に動作 - その際、details のデフォルト値がそのまま利用可能
- details のキーも Ractor 内で
intern(シンボル化)できるように保障
テスト (actionview/test/template/lookup_context_test.rb) では、Ractor 内で LookupContext を生成し、details が適切に扱えることが検証されています。
2-2. Digestor の Mutex を Ractor ローカルに
ActionView::Digestor はテンプレートの digest(キャッシュキー相当)を計算するクラスで、内部でキャッシュを持ち、その保護のために Mutex を使っています。
前提となる #58281 で:
- digest キャッシュ自体が Ractor ごとのローカルストレージに移されている(Ractor ローカルキャッシュ)
- それに対し、Mutex がまだ「グローバルな」状態だと、Ractor 間で Mutex を共有することになり、Ractor セーフではない
この PR では:
Digestorが利用する Mutex を「Ractor ローカルストレージ」に移動- つまり、「各 Ractor ごとに独立した Mutex + digest キャッシュを持つ」構成に統一
効果:
- 各 Ractor は自分専用のキャッシュとロックを持つため、Ractor 間で Mutex を共有せずに済む
- 同時に、Ractor ごとの digest キャッシュの整合性が保たれ、デッドロックや shareable 制約の問題を避けられる
テスト (actionview/test/template/digestor_test.rb) では、Ractor 内で digest 計算を行い、エラーなく正しい digest が計算できることが確認されています。
2-3. 非メイン Ractor で可能になったこと
PR 説明にもある通り、次が可能になります:
- 非メイン Ractor で
ActionView::LookupContextを構築 - details のキーを
intern(例:"html".to_symのような処理)して利用 - 非メイン Ractor でテンプレートの digest を計算(
Digestor)
並列レンダリングや、Ractor を利用したプリコンパイル・事前ウォームアップなどで、ビュー関連処理をメイン Ractor とは別の Ractor にオフロードしやすくなります。
- 影響範囲・注意点
対象コンポーネント
ActionView::LookupContextActionView::Digestor- それらを利用するテンプレート lookup / digest 計算の仕組み全般
影響のあるケース
- Ractor を利用しているアプリケーション / ライブラリ
- 特に「Ractor 内で ActionView の機能を触る」ような高度な並列化をしている場合
- テンプレート digest を独自に扱う gem(
Digestorに直接依存している場合)
互換性
- 既存のシングルスレッド / マルチスレッド(Ractor 不使用)アプリには基本的に挙動変更はありません。
- digest キャッシュ・Mutex のスコープが「Ractor ローカル」に統一されたため、Ractor を使う場合の安全性が向上しています。
- Ractor 間で digest キャッシュが共有されなくなった設計(#58281 との組み合わせ)が前提のため、「Ractor を跨いで digest キャッシュを共有する」ような前提のカスタマイズをしている場合は設計の見直しが必要です(通常の利用では問題になりません)。
パフォーマンス面
- 各 Ractor ごとに digest キャッシュを持つため、「Ractor を増やすほどキャッシュが分散する」ことになりますが、その代わりロック競合や shareable 制約が減り、Ractor 想定として自然な設計になっています。
- 単一 Ractor での性能は基本的に変わりません。
- 参考情報 (あれば)
この PR の前提となる変更
- #58281: digest キャッシュを Ractor ローカルにする変更(本 PR はそのフォローアップ)
関連コードの主な場所
actionview/lib/action_view/lookup_context.rb- details のデフォルト値や初期化周り
actionview/lib/action_view/digestor.rb- digest 計算ロジック、キャッシュ、Mutex 管理
- テスト
actionview/test/template/lookup_context_test.rbactionview/test/template/digestor_test.rb
Ractor を使った並列レンダリングや、ビュー関連処理のオフロードを検討している場合、この PR と #58281 の内容を合わせて追っておくと設計の参考になります。
#58387 Reduce allocations during attribute definition
マージ日: 2026/8/6 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
ActiveModel の attribute メソッド定義時に使われる"define_method_"文字列の生成回数を減らし、オブジェクト割り当て(アロケーション)を削減するパフォーマンス改善です。Pattern ごとに 1 回だけ文字列を作るようにし、大量の属性を持つモデルでメモリ負荷を軽減します。
- 変更内容の詳細
※ PR では activemodel/lib/active_model/attribute_methods.rb に対して +3 / -2 の小さな変更のみです。
背景
ActiveModel の attribute methods(name, name=, name? などを動的に定義する仕組み)では、「あるパターンに基づいてメソッドを define_method する」ような処理があり、その際に "define_method_" のようなプレフィックス文字列が使われています。
もともとの実装イメージは概ね次のようなものです(※擬似コード):
patterns.each do |pattern|
attributes.each do |attr_name|
method_name = "define_method_" + pattern.suffix # ここで毎回 String を生成
define_method(method_name) do
# ...
end
end
endpatternsは ~16 個程度attributesはモデルにより非常に多くなりうる(大規模なスキーマなどでは数百〜数千)- その結果、
"define_method_"という同じ文字列が、Pattern ごと・属性ごとに何度もアロケートされていた
今回の変更内容
PR の説明にある通り:
Currently this "define_method_" string is allocated per Pattern (~16) per attribute (~N, really big) defined.
This commit moves the string into Pattern so that it is only allocated once per pattern (~16).
つまり、"define_method_" 文字列を「パターン側」に持たせてキャッシュするように変更しています。イメージとしては次のような変更です:
Before(概念的なイメージ)
class Pattern
def method_name_for(attr_name)
"define_method_" + attr_name.to_s
end
endAfter(概念的なイメージ)
class Pattern
DEFINE_METHOD_PREFIX = "define_method_".freeze
def method_name_for(attr_name)
DEFINE_METHOD_PREFIX + attr_name.to_s
end
endまたは、Pattern インスタンスごとにメンバ変数で持つような形かもしれませんが、いずれにせよ:
- 以前: Pattern × Attribute の組み合わせごとに
"define_method_"が新規生成 - 以降: Pattern ごとに 1 つの
"define_method_"だけが生成され再利用
という構造になっています。
変更行数が +3/-2 と非常に少ないため、
"define_method_"のリテラルをクラス定数 or Pattern インスタンスの ivar に移動- 呼び出し側をその定数/ivarを参照する形に変更
といった最小限のリファクタリングにとどまっていると考えられます。
- 影響範囲・注意点
影響範囲
- 対象: ActiveModel の
attribute_methods機構ActiveModel::AttributeMethodsをインクルードしているクラス- Rails の ActiveRecord モデルはこのモジュールを経由しているため、実質的にすべてのモデルが間接的な影響を受けます。
- タイプ: パフォーマンス(メモリアロケーション削減)のみ
- 動作仕様(どのメソッドが定義されるか、呼び出し結果)は変わらない
- 文字列フリーズ済みであれば GC 負荷も軽減される
想定される効果
- 特に大量のカラムを持つテーブルのモデルで、アプリ起動時や
attribute_methods定義時のメモリ使用量が削減 - GC の負荷が若干軽減され、起動時間や初回アクセスのパフォーマンスがわずかに向上する可能性があります。
注意点
- 変更は内部実装レベルであり、公開 API のインターフェースに変更はありません。
"define_method_"の文字列を直接参照しているようなメタプログラミング(かなり特殊なケース)がある場合は、挙動に影響する可能性がありますが、通常のアプリケーションではそのような依存はまずないと考えられます。- スレッドセーフティ:
- 定数化もしくは不変なインスタンス変数でのキャッシュなので、スレッドセーフです(書き換えなければ問題なし)。
- 参考情報 (あれば)
- 対象ファイル:
activemodel/lib/active_model/attribute_methods.rb
- 関連概念:
ActiveModel::AttributeMethods- メソッド定義時の
define_methodと文字列アロケーション
- 類似の最適化:
- Rails 内では、同様に頻出の文字列・シンボル・正規表現などを定数化してアロケーションを抑える最適化が他の箇所でも行われています。
#58386 Make Relation::FromClause.empty Ractor safe
マージ日: 2026/8/6 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
Active Record のRelation::FromClause.emptyの実装を、Ruby 3 の Ractor(並行実行環境)でも安全に共有できるように変更した PR です。WhereClauseに対して行われたのと同種の「イミュータブルで Ractor セーフな空オブジェクト」の扱いをFromClauseにも拡張しています。
- 変更内容の詳細
※ PR 本文・差分情報が最小限のため、Rails の既存実装パターンと WhereClause の同様の変更内容から推測を交えて説明します。
背景: Relation::FromClause と empty メソッド
ActiveRecord::Relation::FromClause は、クエリの FROM 句を表現する内部クラスです。Relation::FromClause.empty は、空の FROM 句(デフォルト状態)を表すオブジェクトを返すクラスメソッドで、内部的に多くの Relation 操作がこの「空 from 句」を起点に組み立てられます。
Ractor セーフ化のポイント
Ruby 3 以降の Ractor モデルでは、Ractor 間で共有されるオブジェクトは「不変(freeze 済み)」かつ「Ractor セーフ」である必要があります。Rails では、内部で使い回す「空の句(empty な Clause オブジェクト)」をクラス変数や定数でキャッシュし、複数スレッド/Ractor間で共有しているため、それらを Ractor セーフにする必要があります。
以前のコミットで WhereClause.empty が Ractor セーフになるように変更されており、この PR はそれと同じパターンを FromClause.empty に適用しています。
典型的な変更イメージは以下のようなものです(疑似コード):
# 変更前(イメージ)
def self.empty
@empty ||= new(nil, []) # ← freeze されていない / Ractor セーフ保証なし
end
# 変更後(イメージ)
EMPTY = new(nil, []).freeze
EMPTY.ractor_make_shareable if EMPTY.respond_to?(:ractor_make_shareable)
def self.empty
EMPTY
endもしくは、WhereClause と同様に Class.new 時点で shareable_constant_value アノテーションを利用している可能性もあります。
どちらにせよ、意図としては:
emptyが同一インスタンスを返すキャッシュであることを維持- そのインスタンスを
freeze等により不変化 - Ruby 3.1+ の
ractor_make_shareableや#shareable_constant_valueを利用し、Ractor 間共有可能であることを明示
という変更になっています。
- 影響範囲・注意点
影響範囲
- Active Record 内部のクエリ構築 (
Relation) において、FROM句が未指定のときなどに使われるRelation::FromClause.emptyが Ractor 環境でも安全に共有されます。 - 通常の(Ractor を使わない)Rails アプリでは挙動はほぼ変わらず、パフォーマンスやメモリ使用に関しても、従来からあった「空オブジェクトのキャッシュ」をより安全にしただけ、という位置づけです。
- 「空の FromClause オブジェクトを破壊的に変更している」ような非公開 API の誤用をしていない限り、アプリケーションコードへの影響は基本的にありません。
注意点
Relation::FromClause.emptyが返すオブジェクトは、これまで以上に「書き換えてはいけない前提」のイミュータブルオブジェクトになります。
もし内部 API を無理に触っていて、emptyが返したオブジェクトに対して破壊的操作をしている場合、FrozenErrorなどが発生する可能性があります。- Ractor を利用して Active Record を並列利用するケースでは、
FromClause周りでRactor::UnsafeErrorが出る可能性が下がり、安定性が向上します。 - この PR は
WhereClauseの Ractor セーフ対応と整合を取る内容なので、FROMとWHEREの両方で同じ Ractor セーフ戦略が適用されることになります。
- 参考情報 (あれば)
- 同種の変更:
WhereClauseの Ractor セーフ化
https://github.com/rails/rails/commit/d8c7523c016a59e425a8c930d792fd937bbe8ccf - Ruby Ractor の概要:
- Ractor で共有可能なオブジェクトの条件(freeze / shareable)が重要
ractor_make_shareable/shareable_constant_valueなどがよく使われるパターンです。
#58206 Generate an omakase-compliant config/ci.rb when steps are skipped
マージ日: 2026/8/6 | 作成者: @55728
- 概要 (1-2文で)
bin/rails new実行時に各種--skip-*オプションを使った場合でも、新規アプリが自前のbin/rubocop/bin/ci(Style: Ruby)チェックに確実に通るよう、config/ci.rbテンプレートの空行出力ロジックが調整されました。
とくに--skip-test時にLayout/EmptyLinesAroundBlockBodyで RuboCop が落ちていた問題を解消します。
- 変更内容の詳細
問題の背景
以下のような手順でアプリを生成すると:
bin/rails new myapp --skip-test
cd myapp
bin/rubocop
# または bin/ci の "Style: Ruby" ステップ生成された config/ci.rb に対して RuboCop の Layout/EmptyLinesAroundBlockBody が発生していました。
エラー例:
config/ci.rb:12:1: C: [Correctable] Layout/EmptyLinesAroundBlockBody:
Extra empty line detected at block body end.原因は、「Checks グループ」と「Tests グループ」の間を区切るための空行が、--skip-test により Tests グループ自体が生成されないにもかかわらず、末尾に一行だけ残ってしまうことでした。
修正内容
変更ファイル:
railties/lib/rails/generators/rails/app/templates/config/ci.rb.tt(+1/-2)
テンプレート (.tt) 内で、各ステップの間に入れる区切り用の空行を、「両側にステップが存在する場合のみ出力する」ように条件付きに変更しています。
イメージとしては、これまで:
group "Checks" do
# rubocop step
# brakeman step
# ↑ ここに必ず空行が出る
end
group "Tests" do
# test step
endのように一律で空行を出していたところを、以下のようなイメージに近付けています:
group "Checks" do
# rubocop step
# brakeman step
<% if any_test_steps? %>
<% end %>
end
<% if any_test_steps? %>
group "Tests" do
# test step
end
<% end %>実際のコードは Rails のジェネレータ用 ERB で、
- RuboCop / bundler-audit / JavaScript / Brakeman / Test の各ステップの有無
- それらの間に空行を入れるかどうか
を条件分岐させる形に変更されています。
デフォルト生成との違い
オプションを指定せず bin/rails new した場合の違いは 1 点だけです:
- Brakeman ステップと
"Tests"グループの間にあった空行がなくなる
それ以外の出力は従来と同じです。
- 影響範囲・注意点
影響対象:
- Rails 7.2 以降(あるいはこの PR が入るバージョン)での新規アプリ生成 (
bin/rails new) - 特に
--skip-rubocop/--skip-bundler-audit/--skip-javascript/--skip-brakeman/--skip-testの任意の組み合わせでアプリを生成するケース
- Rails 7.2 以降(あるいはこの PR が入るバージョン)での新規アプリ生成 (
期待される効果:
- どのスキップオプションの組み合わせでも、生成直後に
bin/rubocopおよびbin/ciが Layout/EmptyLinesAroundBlockBody を含め RuboCop によるスタイルチェックをパスする - いわゆる「omakase(Rails 推奨)」設定に準拠した
config/ci.rbが常に生成される
- どのスキップオプションの組み合わせでも、生成直後に
既存アプリへの影響:
- 既存アプリの
config/ci.rbは自動で書き換わらないため、直接の破壊的変更はありません。 - 同様の RuboCop 警告が出ている既存アプリでは、
config/ci.rbの余分な末尾空行を削除することで対処可能です。
- 既存アプリの
注意点:
- この PR は空行出力の条件のみをいじっており、CI の中身(実行ステップそのもの)には変更がありません。
- RuboCop の設定 (
.rubocop.yml) 自体は触っていないため、他の Layout/Style 由来の警告が出るかどうかは各アプリの設定次第です。
- 参考情報 (あれば)
対応する issue/PR:
- #58164: 認証ジェネレータが挿入する routes を修正し、新規アプリが自前の RuboCop を再びパスするようにした PR
→ 今回はconfig/ci.rbに対する同種の「新規アプリが RuboCop に素で通るようにする」調整。
- #58164: 認証ジェネレータが挿入する routes を修正し、新規アプリが自前の RuboCop を再びパスするようにした PR
関連コマンド:
- アプリ生成:
bin/rails new myapp [--skip-* ...] - CI 実行:
bin/ci - RuboCop 実行:
bin/rubocop(Rails テンプレートではbin/ci内からも呼び出される)
- アプリ生成:
#58392 Move duplicated execute_batch up to MySQL::DatabaseStatements
マージ日: 2026/8/6 | 作成者: @seuros
- 概要 (1-2文で)
MySQL 系アダプタ(mysql2 / trilogy)でそれぞれ重複実装されていたexecute_batchを、共通のActiveRecord::ConnectionAdapters::MySQL::DatabaseStatementsに集約したリファクタリングです。挙動の変更はほぼなく、コードの重複排除とメンテナンス性向上が主目的です。
- 変更内容の詳細
やったこと
- 共通の親モジュール:
activerecord/lib/active_record/connection_adapters/mysql/database_statements.rbにexecute_batch実装を追加(+18行)。 - 個別アダプタから削除:
mysql2/database_statements.rbからexecute_batchを削除(-18行)trilogy/database_statements.rbからexecute_batchを削除(-18行)
結果として、MySQL 系アダプタはすべて同じ execute_batch 実装を継承して使う構成になっています。
想定されるコードイメージ(概念的なサンプル)
※実際のコードはPRを参照してください。ここでは典型的な Rails の execute_batch 実装イメージを示します。
# activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
module ActiveRecord
module ConnectionAdapters
module MySQL
module DatabaseStatements
def execute_batch(statements, name = nil)
statements.each do |sql|
execute(sql, name)
end
end
end
end
end
end以前は mysql2 と trilogy の各アダプタ側に、上記とほぼ同じ内容の execute_batch が個別に定義されていたものを、共通モジュールに移した形です。
- 影響範囲・注意点
- 対象
- ActiveRecord の MySQL 系アダプタすべて(
mysql2/trilogy)のexecute_batch呼び出しが、新しい共通実装を使うようになります。
- ActiveRecord の MySQL 系アダプタすべて(
- 互換性
- 「重複していた共通コードを一箇所に集約しただけ」の変更であり、インターフェース(メソッドシグネチャ)や基本挙動に変更はない想定です。
- ただし、以前
mysql2とtrilogyで微妙に挙動を変えていた場合(例えばロギングやエラー処理など)には、今回の共通化で差分が吸収される可能性があります。その場合、両者の挙動がより揃う一方、アダプタ固有の「クセ」が消えることになります。
- アプリケーションコードへの影響
execute_batchを直接使っているアプリケーションは通常そのまま動作します。- アダプタを monkey patch して
execute_batchをオーバーライドしていた場合は、継承元のモジュール位置が変わっているため、モジュールのインクルード順やメソッド解決順序(lookup path)に依存した高度なパッチは確認が必要です。
- ライブラリ側(gem作者など)への注意
- mysql2 / trilogy の各アダプタクラスに直接
execute_batchを再定義していた middleware / extension がある場合、今後は MySQL::DatabaseStatements のメソッドを前提にした方が安全です。
- mysql2 / trilogy の各アダプタクラスに直接
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58392
- 関連コード:
activerecord/lib/active_record/connection_adapters/mysql/database_statements.rbactiverecord/lib/active_record/connection_adapters/mysql2/database_statements.rbactiverecord/lib/active_record/connection_adapters/trilogy/database_statements.rb
- 文脈: Rails では DB アダプタ間で共通化できるメソッドを上位モジュールに集約しており、今回もその一環として MySQL 系の重複コードを整理したものと考えられます。
#58187 Inspect nested attribute id in RecordNotFound message
マージ日: 2026/8/6 | 作成者: @flavorjones
- 概要 (1-2文で)
accepts_nested_attributes_for経由で渡されたネスト属性のidが見つからない場合に発生するActiveRecord::RecordNotFoundのエラーメッセージ内で、ユーザー入力由来のidをそのまま埋め込まず.inspectするように変更した PR です。これにより、ANSIエスケープシーケンスなどを含む悪意あるidによるログ偽装・視認性の低下リスクを低減します。
- 変更内容の詳細
変更対象
ActiveRecord::NestedAttributes#raise_nested_attributes_record_not_found!- ファイル:
activerecord/lib/active_record/nested_attributes.rb
- ファイル:
- 対応するテスト追加
- ファイル:
activerecord/test/cases/nested_attributes_test.rb
- ファイル:
具体的なコード変更イメージ
もともとネスト属性で関連レコードが見つからなかった際、以下のように(概念的に)record_id をそのままメッセージに埋め込んでいました:
raise RecordNotFound.new(
"Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}",
model, primary_key, record_id
)これが次のように変更されています:
raise RecordNotFound.new(
"Couldn't find #{model} with ID=#{record_id.inspect} for #{self.class.name} with ID=#{id}",
model, primary_key, record_id
)ポイント:
record_id.inspectを使うことで:- 文字列IDの場合:
"foo"→"\"foo\""のようにクォートされ、制御文字はエスケープされてログ上では「ただの文字列リテラル」として表示される - 整数IDの場合:
123→"123"と見た目上ほぼ変わらない(inspectは数値をそのまま文字列化するだけ)
- 文字列IDの場合:
挙動の具体例
攻撃的な(または単におかしな)id をネスト属性経由で送ったケース:
{
"post": {
"comments_attributes": [
{
"id": "\u001b[31mHACK\u001b[0m" // = "\e[31mHACK\e[0m"
}
]
}
}従来:
ログに出力される
RecordNotFoundのメッセージ例:Couldn't find Comment with ID=\e[31mHACK\e[0m for Post with ID=1端末(ターミナル)でログを閲覧すると、
\e[31mや\e[0mが解釈され、文字色が変わったり、出力の一部が見えづらくなったりしてログ偽装・隠蔽に悪用し得る。
変更後:
メッセージ例:
Couldn't find Comment with ID="\e[31mHACK\e[0m" for Post with ID=1ログビューワ / 端末から見ると、ANSIコードはただのエスケープされた文字列として扱われ、色付けやカーソル移動などは発生しない。
何を「inspect」しているか
.inspectを適用するのは ネストされた属性のid(record_id) のみ- メッセージ内の「親」レコード側の
id(#{id})は変更なし- これは通常アプリ内部で管理されている主キーであり、外部から任意制御されないことを前提としているため
- 既存の
find系メソッドに対する同種修正 (CVE-2025-55193 対応) と整合
テスト
nested_attributes_test.rbにテストが追加されており、ネスト属性のidに制御文字を含む文字列を渡したときに、RecordNotFoundのメッセージ内でinspectされた形になることを検証しています。
- 影響範囲・注意点
- 影響するのは:
accepts_nested_attributes_forを使って関連を更新/作成している- そのネストされた
idで対象の関連レコードが見つからずActiveRecord::RecordNotFoundが発生する - その例外メッセージを ログや外部システムに出力している
- フレームワーク的には
RecordNotFoundは通常 404 応答にマッピングされ、メッセージ文字列はレスポンスとしては返されないため、エンドユーザーへの直接的なXSSなどは発生しません。 - 主な効果はログの安全性向上:
- 攻撃者が任意のANSIシーケンスを含む
idを送りつけても、ログを閲覧する人の端末側でそれが解釈されず、ログの色分け・削除・上書きなどの「視覚的な攻撃」がしづらくなる。
- 攻撃者が任意のANSIシーケンスを含む
- 既存アプリでの互換性:
RecordNotFound#messageの文言がわずかに変わります(idがクォートされる・制御文字がエスケープされる)- もし
RecordNotFound#messageを文字列パースして独自処理しているようなコードがあれば(あまり無いと思われますが)、そのパースロジックに影響する可能性はあります。
- 追加のアプリ側対応:
- 特に対応は不要ですが、ログビューア・監視ツール等で
RecordNotFoundメッセージのパターンマッチをしている場合、ID=...部分がID="..."になることを前提にしているかどうかを確認するとよいです。
- 特に対応は不要ですが、ログビューア・監視ツール等で
- 参考情報 (あれば)
- 類似の脆弱性対応:
- CVE: CVE-2025-55193
ActiveRecord::FinderMethods等でRecordNotFoundメッセージ内のidに.inspectを適用した修正。今回のPRはその「ネスト属性版」の追従です。
- CVE: CVE-2025-55193
- 元報告:
- HackerOne ユーザー
ylchenによる報告がもとになっており、フレームワーク単体では exploitable でないため情報提供扱いとなっていましたが、ログセキュリティの観点から今回修正されています。
- HackerOne ユーザー
#58209 Flatten block content inside links in Action Text markdown conversion
マージ日: 2026/8/6 | 作成者: @flavorjones
- 概要 (1-2文で)
Action Text のContent#to_markdownが、リンク (<a>) 内にブロック要素が入っている場合に不正な CommonMark を出力していた問題を修正する PRです。リンク内のブロック要素を Markdown の制約に合わせて「1行のインラインテキスト」にフラット化してからリンク記法に包むように変更しています。
- 変更内容の詳細
背景となる問題
HTML 上は
<a>は「transparent content model」を持つため、<a><p>...</p></a>や<a><pre>...</pre></a>といった「ブロック要素を内包するリンク」は合法です。一方で CommonMark 仕様では、リンクテキストは「0個以上のインライン要素の並び」と定義されており、リンクの中に段落やコードブロックなどのブロック要素を入れる手段がありません。
ActionText::Content#to_markdownの中ではvisit_aがリンク変換を担っており、子要素の出力がインラインであることを前提に以下のように生成していました:ruby"[#{inner}](#{encode_href(href)})"しかし
visit_pre,visit_p,visit_blockquote, 見出し (visit_h1〜), リスト (visit_ul,visit_ol) などは、コードフェンスや段落間の空行など「ブロックレベル」の出力を行うため、以下のような不正な Markdown が生成されていました。例: HTML
html<a href="https://example.com"><pre>puts 1</pre></a>旧挙動の出力:
markdown[``` puts 1上記では - ``` ``` のフェンスコードブロック - その後の空行 によって Markdown パーサはブロック構造を先に解釈し、インラインリンクが分断され、仕様上不正かつ期待通りにリンクにならない出力となっていました。
変更方針
CommonMark は「リンク内にブロック要素を含める」方法を提供していないため、「リンク構造を保ちつつ、中身を可能な範囲でインラインに潰して表現する」 という degrade(劣化表現)戦略を取っています。
新しい
visit_aの挙動:- 子要素の Markdown 出力をすべて連結した文字列を取得
- その中の「連続する改行(
\nが1回以上続く部分)」をスペース1つに置き換える - それを
"[...](href)"の中身として使う
疑似コードイメージ:
rubydef visit_a(node) inner = visit_children(node) flattened = inner.gsub(/\n+/, " ") if href_present_and_allowed?(node) "[#{flattened}](#{encode_href(href)})" else inner # href がない・許可されない場合はそのまま end endhrefがない、または許可されないアンカーについては、従来通りリンクとしてレンダリングされず、内部のブロック構造を保持したまま素通しします。つまり、「リンクにならない<a>」の振る舞いは変えていません。
具体例
<pre>ブロックを含んだリンクHTML:
html<a href="https://example.com"><pre>puts 1</pre></a>新しい出力:
markdown[``` puts 1 ``` ](https://example.com)- もともとコードブロック(フェンス)として扱われていた部分が、インラインコードスパン
puts 1に縮退され、その前後の改行はスペースに変換されます。 - 結果として「リンクテキストがインラインコード
puts 1」である正当な CommonMark となり、そのテキスト全体がリンクになります。
- もともとコードブロック(フェンス)として扱われていた部分が、インラインコードスパン
見出しを含んだリンク
HTML:
html<a href="https://example.com"><h1>heading</h1></a>新しい出力:
markdown[# heading ](https://example.com)<h1>→# headingという Markdown 見出しの表現自体は維持されますが、改行がスペースに変換されるため1行にフラット化されます。- その
# headingという文字列自体が「リンクテキスト」として解釈されます(#はあくまでプレーンテキスト扱いで、ブロック見出しにはなりません)。
リストや引用など、他のブロック要素を含む場合も同様に、行頭記号(
-,>,1.など)を含んだテキストがすべて 1 行に潰され、そのままリンク内テキストになります。
テスト
actiontext/test/unit/markdown_conversion_test.rbに、リンク内にブロック要素が含まれる様々なパターンを網羅するテストが多数追加されています (+122行)。- これにより、
visit_aの新しいフラット化ロジックが期待通り動作することが検証されています。
- 影響範囲・注意点
- 対象: Action Text の
ActionText::Content#to_markdownを利用して HTML を Markdown に変換しているコード(特に WYSIWYG エディタの保存内容を Markdown に変換して外部に出すようなケース)。 - 現時点では
to_markdown自体がまだ未リリース(#56858 で追加されたがリリース版には未含有)であるため、「公式リリース間での互換性破壊」は発生しません。 - ただし、以下のような点は把握しておくとよいです:
- もし unreleased な
to_markdownを既に利用していた場合、「リンク内のブロック要素が壊れた Markdown として出力される」バグが修正されるため、結果の Markdown が大きく変わる可能性があります。 - 「ブロックとしての構造」(段落区切り、コードブロックなど)はリンク内では維持されず、すべて 1 行のプレーンテキストまたはインラインコードとして縮退されます。
- 例えば「リンク内に複数段落のテキストがある」といった HTML は、Markdown 変換後は「スペースで連結された1つのインラインテキストのリンク」になります。
hrefがない・許可されない<a>はブロック構造が維持されるため、その挙動に依存しているコードがあれば変更はありません。
- もし unreleased な
- セキュリティ面:
- 本 PR は主に出力フォーマットの正当性の改善であり、リンク許可/不許可のポリシー自体は変更していません。
encode_hrefやフィルタリングのポリシーへの影響はありません。
- 本 PR は主に出力フォーマットの正当性の改善であり、リンク許可/不許可のポリシー自体は変更していません。
- 参考情報 (あれば)
- CommonMark 仕様: Links
https://spec.commonmark.org/0.31.2/#links to_markdown追加 PR (#56858):
https://github.com/rails/rails/pull/56858- 本 PR (#58209):
Flatten block content inside links in Action Text markdown conversion
https://github.com/rails/rails/pull/58209
#58270 asset_path: strip the query/fragment tail without a second scan
マージ日: 2026/8/6 | 作成者: @nuclearspike
- 概要 (1-2文で)
asset_pathがクエリ文字列・フラグメント(?/#以降)を取り除く処理について、正規表現による2回のスキャンと不要な文字列コピーをやめて、1回のスキャン+スライスだけで処理するよう最適化したPRです。これによりimage_tagやjavascript_include_tagなどのヘルパ全般で、オブジェクト割り当て削減と数%のパフォーマンス向上が見込まれます。
- 変更内容の詳細
変更前の処理
asset_path 内では、ソース文字列からクエリ・フラグメント部分を取り出しつつ、元の文字列からそれを削除するために、正規表現を2回使っていました。
tail, source = source[/([?#].+)$/], source.sub(/([?#].+)$/, "")問題点:
- 正規表現マッチのスキャンが2回走る
String#subはマッチしない場合でも新しい文字列を必ず生成する- 実際には
"application.js"のようなクエリやフラグメントを含まないケースが大多数であり、その場合にも無駄なオブジェクト割り当てが発生していた
変更後の処理
クエリ・フラグメントを「取り出す」処理はそのまま使い、削除処理を「長さに基づくスライス」に変更しています。
変更後のコードイメージ:
if (tail = source[/[?#].+$/])
source = source[0, source.length - tail.length]
endポイント:
- 正規表現マッチは1回だけ (
source[/[?#].+$/]) - クエリ・フラグメントがない場合
tailはnilなので、sourceは一切コピーされない - ある場合だけ、末尾から
tail.lengthバイト分を削った新しい文字列を作る
改めて、挙動の例
挙動自体は従来と同じです。たとえば:
asset_path("app.js?v=3", skip_pipeline: true) # => "/app.js?v=3"
asset_path("app.css#print", skip_pipeline: true) # => "/app.css#print"
asset_path("xmlhr.js?123", skip_pipeline: true)
asset_path("xmlhr.js#hash", skip_pipeline: true)
asset_path("xmlhr.js?123#hash", skip_pipeline: true)これらは既存テスト (asset_tag_helper_test.rb 内の javascript_path テスト) でカバーされており、スライスのオフバイワンがあると即座に壊れるようになっています。
仕様上の細かい差異の可能性
新しい実装では「マッチしたレンジ」ではなく「末尾からのバイト数」で削るため、ソースに tail の後ろに改行があるようなケースでは挙動が理論上変わります:
- 旧:
"app.js?\n"→"app.js\n"になる - 新:
"app.js?\n"→"app.js?"になる(末尾の"\n"が残る)
ただし、資産パス(asset_path に渡すパス文字列)に改行が入ることは想定されておらず、Rails コアとしてはこのようなケースを防御するためにコードを複雑化する価値はない、という判断になっています。
- 影響範囲・注意点
影響範囲
- 対象となるメソッド:
asset_path- およびそれを内部的に利用する各種ヘルパ:
image_tagjavascript_include_tagstylesheet_link_tagasset_url- そのほか
asset_pathを経由するヘルパ全般
これらすべてで、クエリ・フラグメント付きパス/なしパスの扱いは従来と同じ挙動を維持しつつ、以下のような改善があります。
パフォーマンス・メモリ面の影響
ベンチマーク結果(Ruby 3.3.11, arm64-darwin, benchmark-ips):
asset_path("application.js")- 511.5k i/s → 552.7k i/s(約 +8.0%)
asset_path("images/logo-with-a-longer-name.png")- 448.0k i/s → 484.4k i/s(約 +8.1%)
オブジェクト割り当て数(1,000回呼び出しあたりの平均):
- プレーンなパス/ネストしたパスともに:
- 1呼び出しあたりの割り当て数が 8 → 7 に削減
マシンのノイズにより速度向上率は +2.5%〜+13% 程度のばらつきがありますが、「1コールあたりの割り当てオブジェクト数が1つ減る」ことは安定して観測されています。テンプレート内で大量の image_tag / javascript_include_tag が呼ばれるアプリケーションでは、全体として GC 負荷の軽減に寄与します。
注意点
- 仕様上の挙動は(改行を含むような異常なソースを除き)従来と同じ
- テストは既存の
asset_tag_helper_testによる回帰検知に依存しており、明示的にテストケースを増やしてはいませんが、クエリ付き・フラグメント付き・併用パターンはカバー済み - 特殊な monkey patch を
asset_pathまわりにしている場合は、パッチから逆算しているコード(たとえば内部実装を前提にしたsource.sub(/([?#].+)$/, "")相当の処理)を書いていると、挙動に差が出る可能性がありますが、通常の利用では影響ありません
- 参考情報 (あれば)
- 変更ファイル:
actionview/lib/action_view/helpers/asset_url_helper.rb(+3 / -1)
- ベンチマークスクリプト:
- PR 本文の
<details>内に掲載。activesupport/actionviewをローカルパス指定し、benchmark-ipsを利用してasset_pathの性能比較を行う簡易ベンチ。
- PR 本文の
- 実装上の意図:
- 高頻度で呼ばれる小さなユーティリティ (
asset_path) において、「ほとんど起きないケース(クエリ付きパス)のために、通常ケースで毎回余分なスキャンとコピーを行う」ことをやめ、ホットパスのコスト削減を狙ったマイクロ最適化。
- 高頻度で呼ばれる小さなユーティリティ (
#58135 Add query predicate hooks for Active Record types
マージ日: 2026/8/5 | 作成者: @jsaubry
- 概要 (1–2文で)
Active Record の型ごとに、where句などのクエリ述語の「左辺(カラム側)」と「右辺(値側)」をカスタマイズできるフックが追加されました。これにより、UUID など「書き込み時の表現」と「検索時の表現」が異なる型でも、型クラス側で SQL 生成を制御しやすくなります。
- 変更内容の詳細
2-1. 追加されたフックの概要
Active Model / Active Record の型 (ActiveModel::Type::Value を継承したクラス) に、クエリ用のフックが追加されています。
新しく追加された主なメソッド:
query_attribute(attribute)- 述語の左辺(属性名側)の変換用フック
- 例:
lower(custom_immutable_unaccent(name))のような式に置き換えるなど
query_value(attribute, value, predicate_builder:)- 述語の右辺(バインド値側)の変換用フック
- Prepared statement を維持しながら、
UUID_TO_BIN(?)のような関数呼び出しでラップしたり、正規化した値を別列で比較したい場合などに利用
これらは内部的に PredicateBuilder から呼び出され、以下のケースで利用されます。
- 等価比較 (
where(id: 1)など) - 配列 (
where(id: [1, 2, 3])) - 範囲 (
where(created_at: start_time..end_time))
2-2. 実装上の変更点の概要
主な変更ファイルと役割:
activemodel/lib/active_model/type/value.rbActiveModel::Type::Valueにquery_attribute/query_valueのデフォルト実装が追加- デフォルトは「何もしない」実装で、既存挙動を変えないようにしているはず
activerecord/lib/active_record/relation/predicate_builder*.rbPredicateBuilderとその各種 handler(array_handler,range_handler, etc.)で、値や属性を扱う際に新しいフックを使うように変更- ハッシュ形式 (
where(column: value)) を解釈するときに、型オブジェクトからquery_attribute/query_valueを呼んでから Arel ノードを組み立てるようになっている
activerecord/lib/active_record/relation.rbPredicateBuilderの呼び出し部分に微修正(新しいフックに対応)
activerecord/test/cases/relation/predicate_builder_test.rb- 新機能をカバーするテストが多数追加(+102行)され、フックが意図した通りに呼ばれること、bind が壊れないことなどを確認
2-3. 想定されているユースケース例
例1: MySQL の BINARY(16) UUID 列
アプリ側では "a1b2-..." のような UUID 文字列を使いたいが、DB では BINARY(16) に保存している場合。
MyUuidType < ActiveModel::Type::String のような型を定義し、そこで:
class MyUuidType < ActiveModel::Type::String
# 左辺の変換: id -> id (そのまま) でもよいが、
# UUID_TO_BIN(id) のようにする場合はここで Arel を組み立てる。
def query_attribute(attribute)
# 例: Arel::Nodes::NamedFunction.new("UUID_TO_BIN", [attribute])
attribute
end
# 右辺の変換: 'a1b2...' -> UUID_TO_BIN(?) など
def query_value(attribute, value, predicate_builder:)
# value はアプリ側の UUID 表現(文字列)なので、ここで
# 「バインドされた ? を UUID_TO_BIN() で包む」Arel を返すイメージ
# 実装イメージ (擬似コード):
arel_value = predicate_builder.build_bind_attribute(attribute.name, value)
Arel::Nodes::NamedFunction.new("UUID_TO_BIN", [arel_value])
end
end上記のような実装をすると、呼び出し側は:
User.where(id: "a1b2-...-uuid-string")と書くだけで、内部的には:
WHERE id = UUID_TO_BIN(?)のような SQL が生成されることを狙っています(実際の Arel の書き方は実装詳細になりますが、方向性としてこういうことが可能)。
例2: 正規化して比較するテキスト列
DBにはユーザーが入力した文字列をそのまま保存しつつ、検索時は正規化した値で比較したいケース:
lower(custom_immutable_unaccent(name)) = lower(custom_immutable_unaccent(?))といった SQL を生成したければ、テキスト用のカスタム型で:
class NormalizedText < ActiveModel::Type::String
def query_attribute(attribute)
# lower(custom_immutable_unaccent(name))
func = Arel::Nodes::NamedFunction
func.new("lower", [
func.new("custom_immutable_unaccent", [attribute])
])
end
def query_value(attribute, value, predicate_builder:)
arel_value = predicate_builder.build_bind_attribute(attribute.name, value)
func = Arel::Nodes::NamedFunction
func.new("lower", [
func.new("custom_immutable_unaccent", [arel_value])
])
end
endとしておけば、呼び出し側:
User.where(name: "Émilie")が、期待通りの正規化比較 SQL に変換されるようになります。
2-4. なぜ cast / serialize ではダメなのか
cast や serialize は「Ruby <-> DB の値変換」の責務を持っており、INSERT/UPDATE の書き込みや SELECT の読み込みに共通で使われます。
しかし今回の要件は、
- 「検索条件(WHERE)のみ」で
- カラム名やバインド変数を SQL 関数で包みたい
というものなので、
- 単純な「値変換」ではなく、Arel ノードレベルの「SQL 構造の変換」
- SELECT の投影や INSERT の値には影響させたくない
といった性質のため、専用のクエリ述語用フックを設ける設計になっています。
- 影響範囲・注意点
- 既存アプリへの影響
- 既存の型クラスは
query_attribute/query_valueを実装していないため、デフォルト実装(何もしない)が使われ、挙動は従来通りです。 - そのため、この PR を取り込んでも、既存コードは基本的にブレークしない想定です。
- 既存の型クラスは
- カスタム型を実装する場合の注意点
query_valueでは「プレースホルダ付きのバインド」を活かす必要があります。SQL文字列を自前で連結するのではなく、predicate_builderを介してバインド値を生成し、それを Arel で包む形にするべきです。query_attribute/query_valueはあくまで「述語用」のフックであり、SELECT 句の投影結果や INSERT/UPDATE の書き込み時には使われません。その前提でインターフェースを設計する必要があります。
- サポートされる述語の種類
- 今回のフックは Equality / Array / Range の述語で利用されます。その他の演算子(LIKE、>、<、IN (サブクエリ) など)でどこまで効くかは、
PredicateBuilder内部の実装に依存します。
- 今回のフックは Equality / Array / Range の述語で利用されます。その他の演算子(LIKE、>、<、IN (サブクエリ) など)でどこまで効くかは、
- 将来的な API 安定性
- PR 説明では「internal predicate-builder hooks」と明記されているため、まずは内部 API として導入された可能性があります。
公開 API としてドキュメント化されるかは今後の変更を確認する必要があります(特に Gem として公開する共通型の実装では、この点を意識した方がよいです)。
- PR 説明では「internal predicate-builder hooks」と明記されているため、まずは内部 API として導入された可能性があります。
- 参考情報 (あれば)
- 元 PR: https://github.com/rails/rails/pull/57395 (kirs 氏による最初の試み)
- 今回の PR: https://github.com/rails/rails/pull/58135
- 実際の
PredicateBuilder内でのquery_attribute/query_value呼び出し箇所や、テストケースを読むと、どのような Arel ノードを返せばよいかがより具体的に把握できます。
- 実際の
- 関連ファイル
activemodel/lib/active_model/type/value.rb: 型フックの定義元activerecord/lib/active_record/relation/predicate_builder*.rb: クエリビルド箇所activerecord/test/cases/relation/predicate_builder_test.rb: 実際にフックが使われるパターンのテスト
この PR により、「型ごとにクエリ時の SQL 生成を細かく制御したい」というニーズに対して、Arel ベースでの拡張ポイントが公式に用意された形になります。
#58383 Delete leaked Cpk::Author in the autosave teardown
マージ日: 2026/8/5 | 作成者: @etiennebarrie
- 概要 (1-2文で)
このPRは、特定のテスト(TestDestroyAsPartOfAutosaveAssociation)がトランザクションなしで実行されることにより、DB上に不要なCpk::Authorレコードが残り、その後に実行される別テスト(WhereChainTest)を不安定化させていた問題を修正するものです。autosave のテストの teardown で漏れたCpk::Authorを削除する1行を追加し、テスト間のデータリークを防いでいます。
- 変更内容の詳細
問題の背景
問題のテスト1:
TestDestroyAsPartOfAutosaveAssociation#test_autosave_has_one_cpk_association_when_composite_foreign_key_is_manually_set- このテストは「トランザクション付きテスト」を使わずに実行されている(=各テスト後に自動でロールバックされない)。
- そのため、このテストで作られた
Cpk::Authorレコードがテスト終了後もDBに残ってしまう。
問題のテスト2:
WhereChainTest#test_missing_with_composite_primary_key- このテストでは
Cpk::Book(id: [1, 2])を作成する。 - ここで
author_idが、先にリークして残っていたCpk::AuthorのIDと一致してしまうケースがある。 - その状態で
where.missing(:author)を実行すると、本来は「author が存在しない book」を返すはずが、リークした author によって関連が埋まってしまい、結果が空になりアサーションが失敗する。 - つまり「autosave のテストが先に実行されたかどうか」で結果が変わるフレークテストになっていた。
- このテストでは
作者が再現に使ったコマンド:
ARCONN=sqlite3 bundle exec ruby -Ilib:test \
-e 'load "test/cases/autosave_association_test.rb"; load "test/cases/relation/where_chain_test.rb"' \
-- --seed=35972実際の変更内容
変更ファイル:
activerecord/test/cases/autosave_association_test.rb(+1 / -0)
内容としては、autosave 関連のテストの teardown(またはそれに準じる後処理)に「テストで作成した Cpk::Author を削除する」1行を追加しただけです。
イメージとしては、以下のようなコードが追加されたと考えられます(擬似コード):
def teardown
super
Cpk::Author.delete_all # 実際にはスコープされているか、特定レコードのみ削除
end実際の差分は1行のみで、目的は「このテストが作成した author レコードが、他のテストに影響しないようにする」ことです。
- 影響範囲・注意点
- 影響範囲は Rails 本体の「テストコード」のみであり、アプリケーションのランタイム挙動や API 仕様には影響しません。
- 修正により、以下が改善されます:
WhereChainTest#test_missing_with_composite_primary_keyが、事前に autosave の CPK テストが走ったかどうかで結果が変わるフレーク状態から解消される。- CPK(Composite Primary Key / Composite Foreign Key)を使ったテスト同士が、漏れたレコードにより互いに干渉しにくくなる。
- 注意点として、「トランザクションを使わないテスト」は、このようなデータリークを起こしやすいため、同様のパターンが他にもないかを継続的に確認する必要があります。
- 参考情報 (あれば)
- 対象の不安定テスト:
TestDestroyAsPartOfAutosaveAssociation#test_autosave_has_one_cpk_association_when_composite_foreign_key_is_manually_setWhereChainTest#test_missing_with_composite_primary_key
- フレークが確認されたビルド:
- 関連概念:
- autosave associations(
autosave: trueな関連) - composite primary key / composite foreign key を用いた関連のテスト
- トランザクションなしテストにおけるレコードリークとテスト順依存問題
- autosave associations(
#58281 Move the LookupContext::DetailsKey caches to Ractor-local storage
マージ日: 2026/8/5 | 作成者: @etiennebarrie
- 概要 (1-2文で)
LookupContext::DetailsKey 周りのキャッシュを Ractor 間で共有しないようにし、Ractor ごとに独立したキャッシュを持つ形に変更することで、メイン Ractor 以外からのテンプレート lookup を可能にした PRです。あわせて、Ractor ローカルなストレージを簡単に扱うためのActiveSupport::Ractors.store_if_absentが追加され、concurrent-rubyのバージョン要件も上がっています。
- 変更内容の詳細
背景問題
TemplateDetails::Requestedオブジェクトのインターン(intern)と、テンプレートのダイジェストキャッシュに使っていたConcurrent::Mapが、Ractor 間で共有可能なオブジェクトではなかった。- そのため、メイン Ractor 以外からテンプレート lookup を行うと IsolationError になる / うまく動かない 状態だった。
- 特に
Concurrent::Mapは、Concurrent::NULLという sentinel オブジェクトを内部的に使うが、concurrent-ruby1.3.8 より前はこれが freeze されておらず、Ractor 間で扱うと IsolationError が発生する。
この PR では、
- そもそも テンプレート lookup 関連のキャッシュを Ractor 間で共有しない
- Ractor ごとに独自のキャッシュを持たせる
ことで問題を解消しています。
1) LookupContext::DetailsKey キャッシュを Ractor ローカルに変更
対象: actionview/lib/action_view/lookup_context.rb
- もともと、テンプレート lookup の際に用いる
TemplateDetails::Requested(テンプレートのフォーマット・ロケール・ハンドラなどをまとめた key オブジェクト)と、そのダイジェストキャッシュをグローバルなConcurrent::Map的な構造で管理していた。 - この PR では、それらのキャッシュを Ractor ローカルストレージ に移動。
イメージとしては、以下のような変更が行われています(簡略イメージ・擬似コード):
# 変更前(イメージ)
DETAILS_KEY_CACHE = Concurrent::Map.new
DIGEST_CACHE = Concurrent::Map.new
def details_key(details)
DETAILS_KEY_CACHE.compute_if_absent(details) do
TemplateDetails::Requested.new(details)
end
end# 変更後(イメージ)
def details_key(details)
# Ractor ごとに「details_key_cache」という Map を持つ
cache = ActiveSupport::Ractors.store_if_absent(:details_key_cache) do
Concurrent::Map.new
end
cache.compute_if_absent(details) do
TemplateDetails::Requested.new(details)
end
end- 実際のコードでは、
TemplateDetails::Requestedの intern 処理やダイジェストキャッシュの保持にもActiveSupport::Ractors.store_if_absentが使われるようになっています。 - ポイントは「Ractor をまたいで共有される定数の Map をやめて、Ractor ごとに別の Map を持つ」という設計に転換していることです。
2) ActiveSupport::Ractors.store_if_absent の追加
対象: activesupport/lib/active_support/ractors.rb
新たに ActiveSupport::Ractors.store_if_absent が導入されています。
目的:
- 「キーに対応する Ractor ローカルオブジェクトを取得し、なければブロックで生成して保存する」という処理を一箇所で提供するユーティリティです。
- Ruby の Ractor サポートがない or 不完全なバージョン向けには、
Concurrent::Mapを使ったフォールバック(shim)を提供します。
イメージ(簡略):
module ActiveSupport
module Ractors
# Ractor ローカルなストレージに、キー key で値を保存・取得する
def self.store_if_absent(key)
store = ractor_local_store # Ractor ごとに分かれるストアを取得
store.fetch(key) do
store[key] = yield
end
end
# Ruby が Ractor をサポートしていない場合などは、
# 単純にプロセス全体で共有される Concurrent::Map を使う
# (=既存の挙動とほぼ同じ)
end
end- 実際の実装では、Ruby のバージョンや Ractor の有無を判定して動的に挙動を変えているはずです。
- Ractor 対応環境では、Ractor ごとに独立したストア(例えば
Ractor.currentに紐づく Hash など)を内部的に持つような実装になっていると考えられます。 - 非 Ractor 環境では、従来どおり
Concurrent::Mapベースで「プロセス内共有キャッシュ」として動作します。
3) concurrent-ruby のバージョンアップ
対象: Gemfile.lock
concurrent-rubyの必要バージョンが1.3.8に引き上げられています。- 理由: 1.3.8 で
Concurrent::NULLsentinel が freeze され、Ractor 間で扱えるようになったため。- それ以前のバージョンでは、
Concurrent::NULLがミュータブルなままであり、Ractor 間で参照するとIsolationErrorを引き起こします。 - 結果として、非メイン Ractor 内で
Concurrent::Mapを使うこと自体が不可能でした。
- それ以前のバージョンでは、
- この PR では、少なくとも Ractor ローカルのキャッシュとして
Concurrent::Mapを使う必要があるため、Concurrent::NULLが Ractor セーフであることを前提としています。
4) テスト追加・変更
対象:
actionview/test/template/lookup_context_test.rbactivesupport/test/ractors_test.rb
主に以下を確認するテストが追加・修正されています。
LookupContext が Ractor 内でも動作すること
- 別 Ractor からテンプレート lookup を行い、IsolationError などが発生しないこと。
- Ractor ごとに details key のインターンやダイジェストキャッシュが正しく機能していること。
ActiveSupport::Ractors.store_if_absentの挙動- 同じ Ractor 内で同じ key を使うと同じオブジェクトが返ること。
- 別 Ractor では別オブジェクトが返る(=Ractor ローカルである)こと。
- Ruby のバージョンや Ractor 有無に応じたフォールバック挙動が正しいこと。
- 影響範囲・注意点
影響範囲
テンプレート lookup とキャッシュ
- Action View のテンプレート lookup に関する details key インターン・ダイジェストキャッシュの実体が、「プロセス全体で共有された Map」から「Ractor ごとのローカルストア」に変わりました。
- そのため、
- Ractor A で構築された details key やダイジェストキャッシュは、Ractor B からは見えません。
- 同じテンプレートを複数の Ractor から使う場合、各 Ractor がそれぞれキャッシュを埋め直すことになります(ただし digest 値そのものは変わらない)。
パフォーマンス上の性質
- 単一 Ractor(従来どおりのマルチスレッドのみ)の環境では、実質的にはこれまでと大きく変わらない挙動になります。
- 複数 Ractor を使っている場合、テンプレート lookup のキャッシュが Ractor ごとに分かれるため、
- 初回アクセスのオーバーヘッドは Ractor 数に比例して増える
- しかし Ractor 間でのロック・同期は減る(Ractor ごとの独立キャッシュのため)
というトレードオフがあります。
外部インターフェース
- Digest 値そのものや、外部のフラグメントキャッシュキーは変更されていません。
- したがって、
cacheヘルパーで生成されるキーや、ActionView::Digestorが算出するダイジェストの値は、以前と互換性があります。 - 「キャッシュがどこに保持されるか(Ractor ローカルかどうか)」が変わっただけで、「何をキーにどういう値を計算するか」は変わっていません。
注意点
- Ractor を前提としたコードを書く場合
- この PR により、「テンプレート lookup はメイン Ractor 以外からも行ってよい」前提に近づきました。
- ただし、Ractor 内で自前に
Concurrent::Mapを使う場合は、concurrent-ruby >= 1.3.8が必須である点に注意する必要があります。
- グローバルなキャッシュへの依存を避ける
- もしアプリやエンジン側で、Action View の内部キャッシュに依存したハック(内部の
Concurrent::Mapインスタンスを直接参照するなど)をしていた場合、その前提は崩れます。 - キャッシュに依存する際は、基本的に公開 API(
ActionView::LookupContextやActionView::Digestorなど)を通じて行うべきです。
- もしアプリやエンジン側で、Action View の内部キャッシュに依存したハック(内部の
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58281
- Ractor と共有オブジェクト:
- Ruby 3 以降、Ractor 間で共有できるのは「immutable (frozen) で、かつ shareable としてマークされたオブジェクト」に制限される。
Concurrent::Mapや内部 sentinel オブジェクトがこの条件を満たさないと IsolationError が発生する。
- concurrent-ruby 1.3.8 の変更点(
Concurrent::NULLの freeze)により、Concurrent::Map自体が Ractor セーフに近づいたが、本 PR ではさらに「Ractor ローカルキャッシュ」という設計で Isolation 問題を避けている。
#58373 Honour prepared_statements on rails main
マージ日: 2026/8/5 | 作成者: @camallen
- 概要 (1-2文で)
Rails の ActiveRecord において、prepared_statements設定やunprepared_statementブロックを無視して常にプリペアドステートメントが使われてしまう回 regress を修正し、prepared_statements設定が正しく尊重されるようにした PR です。PostgreSQL かつトランザクションプーリングプロキシ(例: PgBouncer transaction mode)配下で名前付き PREPARE が壊れる問題を解消します。
- 変更内容の詳細
問題の背景
select_allが呼び出し元から渡されたpreparable:オプションを、そのままアダプタにprepare:として渡していた。StatementCache#executeは常にpreparable: trueを渡す実装になっている。to_sql_and_bindsは、クエリがStringのときはpreparable:を変更せずに通す。
この結果:
Model.findやModel.find_byなど、StatementCache 経由のクエリは常に「プリペアドにできる(preparable: true)」として扱われる。- さらに、その値が
select_allからアダプタに「そのまま」伝搬してしまうため、「Rails の設定としてprepared_statementsが無効化されているかどうか」に関係なく、PostgreSQL アダプタが常にプリペアドステートメント(PREPARE)を発行してしまっていた。
具体的には、PostgreSQL がクエリごとに「名前付き PREPARE」を行い:
PREPARE "a_unique_name" AS SELECT ...;
EXECUTE "a_unique_name"(...)という動きをし続けるため、トランザクションプーリングプロキシ配下(バックエンド接続がリクエストごとに入れ替わるような構成)では:
- PREPARE を発行した接続と
- EXECUTE を実行する接続
が異なってしまい、「その名前の PREPARE は存在しない」となって壊れる、という問題が生じます。
実際の修正内容
activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb の select_all(もしくはその近辺)で、元々存在していた以下のような条件:
prepare: prepared_statements && preparableに相当するロジックが、あるコミット(1277a8f09c)で落ちてしまっていたのを復元しています。
今回の修正でのポイント:
- 「呼び出し元が
preparable: trueと言っているか」だけでなく - 「接続の
prepared_statements設定(実体は動的なprepared_statements?メソッド)」
を掛け合わせて、「本当にプリペアドステートメントを使うべきか」を判定するように戻しています。
擬似コードで表すと:
# 修正後イメージ
adapter.select_all(
sql,
name,
prepare: prepared_statements? && !!preparable
)という形に近い動きになります。
ここで prepared_statements? は:
- 接続オプション
prepared_statements: falseが指定されている場合はfalseを返す。 unprepared_statementブロック中では、一時的にfalseを返すようになっている。
そのため、以下の双方のケースで確実にプリペアドステートメントが無効になります:
- 接続自体で恒久的に
prepared_statements: falseを設定したい場合 - 一時的にプリペアドステートメントを避けたい箇所を
unprepared_statementブロックで囲む場合
# 1. 接続オプションで完全にオフ
ActiveRecord::Base.establish_connection(
adapter: 'postgresql',
prepared_statements: false,
# ...
)
# 2. 処理の一部だけオフ
User.connection.unprepared_statement do
User.find(1) # ここでは PREPARE されない
endテスト追加
activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rbが追加され、以下のようなケースがテストされています(内容の要旨):prepared_statements: falseな接続でfindなどを実行しても、プリペアドステートメントが利用されないこと。unprepared_statementブロック内ではプリペアドステートメントを使わないこと。- 上記が PostgreSQL アダプタで正しく動作すること。
これにより、「preparable: が true でも、prepared_statements? が false なら実際には PREPARE しない」動作が回 regres していないことが検証されています。
- 影響範囲・注意点
- 対象:
- 主に PostgreSQL アダプタを使っているアプリケーション
- 特に、PgBouncer の transaction モードなど、トランザクション単位で接続が入れ替わるプーリングプロキシ配下で Rails を動かしている環境
- 影響:
- これまで「
prepared_statements: falseを設定しているのに、実際には PREPARE/EXECUTE が出ている」という状況だった場合:- この PR 適用後は、本当に PREPARE が出なくなるため、DB 側のログやパフォーマンス特性が変わる可能性があります(本来そうあるべき挙動)。
unprepared_statementブロックが、StatementCache 経由のクエリにもきちんと適用されるようになります。
- これまで「
- 注意点:
- もし既に「このバグに依存したワークアラウンド」(例: プロキシ側で PREPARE 名を握りつぶす、など)を実装している場合、この修正で不要になったり挙動が変わる可能性があります。
- パフォーマンス上、プリペアドステートメントに頼っていたコードが、
prepared_statements: false設定のままになっていると、パフォーマンスが低下することはあり得ますが、それは設定の意図に沿った動作です。必要に応じて設定を見直してください。
- 参考情報 (あれば)
- 関連 Issue: #58371
- 回 regress の原因となったコミット:
1277a8f09c(prepared_statements &&が削除されていた) - キーワード:
- ActiveRecord
select_all StatementCache#executeprepared_statements/prepared_statements?unprepared_statement- PostgreSQL + transaction pooling(PgBouncer など)
- ActiveRecord
#58360 Use local fixtures
マージ日: 2026/8/5 | 作成者: @voxik
- 概要 (1-2文で)
このPRは、Railties の Active Storage 関連テストで「グローバルな fixtures ではなく、そのテスト自身が持つローカル fixtures を使う」ように変更したものです。これにより、Railties をフレームワークの他の部分から切り離して単体でテストしやすくなっています。
- 変更内容の詳細
対象ファイルは2つだけで、内容としては「fixture の参照元をローカルに切り替える」というごく小さな修正です。
変更されたファイル:
railties/test/application/active_storage/analyzers_integration_test.rbrailties/test/application/active_storage/direct_uploads_integration_test.rb
どちらも Active Storage の統合テストで、これまで「他の場所にある共通 fixture(グローバル fixtures)」を前提にしていた部分を、「Railties 側に用意された fixtures(ローカル fixtures)」を参照するようにしています。
PR本文で触れられている「This is similar to #54864.」という記述から、#54864 で行われたのと同種のリファクタリング(テストの fixture 参照先を局所化して、コンポーネント単位で完結するようにする)を、Active Storage 関連の統合テストにも適用した、という位置付けだと考えられます。
コードイメージ(擬似例)としては、例えば以下のような変更です(実際のパス名やメソッド名は簡略化しています):
# 変更前(共通fixturesへの依存)
fixtures :all # test/fixtures 以下など、他コンポーネントと共有のfixtures
# 変更後(このテスト配下のローカルfixturesを参照)
fixtures :local_users, :local_attachments # railties/test/... 配下などもしくは、ファイルパスを直接指定する形のテストであれば、
# 変更前
file = file_fixture("images/avatar.png")
# 変更後(railties 側に置いた fixtures を使う)
file = local_file_fixture("images/avatar.png")のように、「グローバルな fixture 参照をやめて Railties 側のローカル fixture に切り替えた」という変更が行われています(上はあくまでイメージです)。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは
railties/test/application/active_storage/*にある統合テストのみです。 - 実行される Rails 本体の動作や API 仕様には変更がありません。
- 変更はテストコードのみであり、アプリケーションコードへの実動影響はありません。
- 影響を受けるのは
意図・メリット
- Railties をフレームワークの他コンポーネント(特に Active Storage 本体側のテスト資産など)から切り離してテストできるようになります。
- fixture がローカルに閉じることで、「他のテストスイートや別コンポーネントの fixtures の変更に巻き込まれて Railties テストが壊れる」といったクロスコンポーネント依存を減らせます。
- 将来的に Railties を単独で切り出してテストしたり、モノレポの構造を変更したりする際の柔軟性が上がります。
注意点
- ローカル fixtures の場所や内容を変更する場合、Railties 側の Active Storage テストがそれに追随しているか確認する必要があります(ただし今回の PR はその依存を整理する方向の変更なので、むしろ依存関係は減っています)。
- 他コンポーネントで共通 fixture を使っていた前提のテストコードを Railties 側に追加する場合は、今後は同様に「ローカル fixtures を使う」方針に揃えるのが望ましいです。
- 参考情報 (あれば)
- 類似PR: #54864(本PRと同種の「ローカル fixtures を使う」ためのリファクタリング)
- Motivation: 「This helps with testing Railties in isolation from other parts of the framework」
→ Railties をフレームワークの他部分から独立してテストしやすくするためのテストインフラ改善 PR であり、バグ修正や新機能追加ではなく、テストの構造改善が主目的です。
#58045 Serialize continuation step cursors through Active Job arguments
マージ日: 2026/8/5 | 作成者: @55728
- 概要 (1-2文で)
Active Job の「継続 (Continuation)」機能で使うステップカーソルを、単なる JSON ではなくActiveJob::Argumentsを経由してシリアライズ/デシリアライズするように変更し、Date,Time,GlobalIDなどのカーソルの型が中断→再開後も維持されるようになりました。これにより、再開後にカーソルがString化してメソッド呼び出しに失敗する問題が解消されます。
- 変更内容の詳細
背景: これまでの問題
Active Job の Continuation (継続) では、ジョブの実行ステップを分割し、「中断 → 後で再開」を行うために「カーソル (cursor)」を継続データに保存しています。
従来実装では、このカーソルを「生の JSON」として書き出して/読み出していました。そのため:
Date,Time,ActiveSupport::TimeWithZone,GlobalIDなど「Active Job 的にはシリアライズ可能だが JSON 自体は知らない型」は、一度中断を挟むとStringに変換されて戻ってくる- その結果、カーソルを想定した型として扱うコードが壊れる
例:
# 1回目の実行時
step.cursor # => #<Date: 2026-07-06>
# 中断 → 再開後
step.cursor # => "2026-07-06"
step.cursor.beginning_of_day # => NoMethodError (String にそんなメソッドはない)ドキュメント上は「カーソルは Active Job でシリアライズ可能な任意のオブジェクトを使える」とされているため、実装とドキュメントが乖離していました。
今回の修正内容
1) カーソルのシリアライズ経路を ActiveJob::Arguments に変更
主な変更点は「カーソルの読み書きに使う API」です。
これまで:
- 継続データへの保存: カーソルをそのまま JSON にエンコード
- 継続データからの復元: JSON をパースした結果をそのままカーソルに使う
これを:
- 保存時: カーソルを
ActiveJob::Arguments.serializeに通してから保存 - 復元時:
ActiveJob::Arguments.deserializeを通してからカーソルにセット
というフローに変更しています。
ActiveJob::Arguments は、ジョブ引数に対してすでに行われている「型付きシリアライズ」の仕組みで、例えば:
Date,Time,TimeWithZoneGlobalID/SignedGlobalIDActiveRecordなどのモデル (GlobalID 経由)- それらを含む配列やハッシュ
といった「Active Job がサポートする型」を、安全にシリアライズ/デシリアライズしてくれます。
そのため、今回カーソルもこの経路に乗せることで、中断→再開の往復後も型が維持されます。
# 修正後
step.cursor # => #<Date: 2026-07-06> # 中断→再開後も Date のまま
step.cursor.beginning_of_day # => OK2) ActiveJob::Attributes との整合性
ActiveJob::Attributes はすでに値の永続化に Arguments.serialize / deserialize を使っており、今回の変更はカーソルの取り扱いをこれと揃えるものです。
- 「ジョブ属性」と「継続カーソル」の両方が、同じシリアライザ (
ActiveJob::Arguments) によって扱われる - ドキュメントの「カーソルは Active Job でシリアライズ可能なオブジェクトを受け付ける」という記述とも一致
3) テストの修正
既存テストの一つが、「シリアライズ不可能なオブジェクト」をカーソルに入れた継続データを直接構築していました。
しかし、新しい実装では:
- カーソルは「Active Job 引数としてシリアライズ可能なもの」に限定される
- 「シリアライズ不可能なオブジェクト」を使うのは実際の利用パターンと異なる
ため、このテストは nil をカーソルとして使う ように変更されています。nil は Active Job 的に正しいシリアライズ対象であり、実際の契約に沿ったケースです。
変更された主なファイル
activejob/lib/active_job/continuable.rb- 継続ステップ/カーソルの保存・復元で
ActiveJob::Argumentsを使用するロジックを追加
- 継続ステップ/カーソルの保存・復元で
activejob/lib/active_job/continuation.rb- 継続データのシリアライズ/デシリアライズ経路を調整
activejob/test/cases/continuation_test.rb- 新しいカーソルシリアライズ仕様に沿ったテストを追加・修正
activejob/CHANGELOG.md- 上記仕様変更についてのエントリを追加
activejob/test/cases/structured_event_subscriber_test.rb- 継続まわりの仕様変更に合わせた微修正
- 影響範囲・注意点
互換性
- プリミティブ型のカーソル (
Integer,String, それらの配列など):Arguments.serialize/deserializeを通しても「バイトレベルで同一」となるよう配慮されており、既存の継続データとの互換性は保たれます。- 既存の JSON ベース継続データも、そのまま読み込めます。
- 非プリミティブ型 (
Date,Time,GlobalIDなど):- 従来は
Stringになってしまっていたものが、本来の型で復元されるようになります。 - 型に依存したロジック (例:
cursor.beginning_of_day) がようやく仕様通りに安全になります。
- 従来は
実装側で意識すべき点
- カーソルとして使える値は、Active Job がサポートするシリアライズ可能な型に限定されます。
- カスタムクラスをそのままカーソルに使いたい場合は:
- Active Job の引数としてシリアライズ可能になるよう
GlobalID等を導入するか、 - カーソル用に「シリアライズ可能な情報」のみに変換して保存する (id や string 等) 必要があります。
- Active Job の引数としてシリアライズ可能になるよう
- カスタムクラスをそのままカーソルに使いたい場合は:
- もし以前から「中断後は String に変わる」ことを前提にしていたコードがある場合 (かなり例外的なケースですが) は、今回の変更で逆に型が変わる可能性があります。
- 例:
step.cursorがDateからStringになる前提でto_dateしていた、など - そのようなコードは削除/簡略化できるはずです。
- 例:
デバッグ・運用上のメリット
- 継続のカーソルが引数と同じ経路でシリアライズされるようになったため、シリアライズエラーの挙動が統一される:
- ある型がジョブ引数としてシリアライズ不可能なら、カーソルとしても不可能
- 問題切り分けが容易になる
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58045
- 関連 API:
ActiveJob::Arguments.serializeActiveJob::Arguments.deserializeActiveJob::Continuation/ActiveJob::Continuable
- コンセプト的には、Active Job の「属性 (
ActiveJob::Attributes)」と「継続カーソル」の扱いが統一された変更であり、今後は「Active Job でシリアライズ可能 = カーソルとしても安全に使える」と考えて実装できます。
#58372 Pluck from the target when the owner is a new record
マージ日: 2026/8/5 | 作成者: @djmb
- 概要 (1-2文で)
Relation#idsがpluck委譲に変わったことで、新規レコードに紐づく未保存の関連に対するidsが空配列になってしまった問題を修正し、ids/pluckともに関連の「target」(メモリ上のオブジェクト)から値を取得するようにした変更です。
これにより、新規レコードに関連づけたオブジェクトに対しても、idsやpluckが直感的な結果を返すようになります。
- 変更内容の詳細
背景
以前の変更(rails/rails@56c95aee)で、Relation#ids が内部的に pluck を使うようになりました。
関連に対して呼ぶと、実体は CollectionProxy#pluck が使われます。
post.tags.ids # 実際には post.tags.pluck(primary_key)ところが、新規レコード (Post.new) に対する関連は DB にはまだ存在せず、「null scope」(空のリレーション)を読む挙動になっていたため、ids が以下のように変化してしまいました。
post = Post.new
post.tags = [tag]
# 56c95aee 以前
post.tags.ids # => [1]
post.tags.pluck(:name) # => []
# 56c95aee 以後
post.tags.ids # => [] # ← ここが変わってしまった
post.tags.pluck(:name) # => []つまり、ids は「target(メモリ上に入っている関連オブジェクトの配列)」を見ていたのに、pluck に委譲したことで「null scope(DB クエリ)」を見るようになり、ids の挙動が破壊的に変わってしまっていました。
この PR での修正内容
この PR では、新規レコードの関連に対しては CollectionProxy#pluck が「null scope」ではなく「target」を読むように変更しています。
その結果:
post = Post.new
post.tags = [tag] # tag.id == 1, tag.name == "ruby"
post.tags.ids # => [1]
post.tags.pluck(:name) # => ["ruby"]つまり
idsも- 任意のカラムを指定する
pluck(:name)なども
新規レコードに紐づいた未保存の関連オブジェクトを正しく反映した配列を返すようになります。
実装面の変更
activerecord/lib/active_record/associations/collection_proxy.rbpluckの実装を 1 行変更し、「null scope から pluck」ではなく「target から pluck」するように振る舞いを修正
(差分は +1 / -1 でかなりピンポイントな修正)
テスト追加
has_many_associations_test.rbhas_and_belongs_to_many_associations_test.rb
新規レコードに対するhas_many/HABTMの関連で、ids/pluckが target を読むことを検証するテストが追加されています。
activerecord/CHANGELOG.md- この挙動変更が CHANGELOG に追記されています。
- 影響範囲・注意点
影響するケース
主に以下のようなコードに影響します。
post = Post.new
post.tags.build(name: "ruby")
post.tags.ids
post.tags.pluck(:name)これまでは(56c95aee 以降)両方とも [] になる可能性があったものが、この PR 以降は ids / pluck ともに target を基準とした値を返します。
- 56c95aee 前:
ids→ target を読んで[1]pluck→ null scope で[]
- 56c95aee〜この PR まで:
ids→pluck委譲により[]pluck→[]
- この PR 適用後:
ids→ target を読んで[1]pluck(:name)→ target を読んで["ruby"]
互換性上の注意
- 56c95aee 以降の「
idsが空配列を返す」挙動に依存したコードは、この PR で再び壊れます。
ただし PR の説明にもある通り、idsを元の(より直感的な)挙動に戻しつつ、pluckも合わせることで一貫性を取っています。 - 「新規レコードの関連に対する
pluckは常に DB ベースで評価されてほしい」と期待していた場合、挙動が変わるので注意が必要です。
特に、「未保存の関連を無視したい」場合は、自前でwhereを明示的に使うなど、クエリをはっきり分ける必要があります。
例:
# target(メモリ上の未保存オブジェクト)を含めず、DB だけを見たい場合
post.tags.scope.pluck(:name) # など、意図的に Relation を扱う設計上の意味合い
- この PR により、「関連に対する
pluck/idsは、通常のto_a同様に target を反映してくれる」という一貫したモデルになります。 - PR 本文でも触れられている通り、代替案として「
idsだけ特別扱いする」ことも可能でしたが、そうするとidsとその他のpluckベースの API とで挙動がズレるため、この PR では 「pluckの側を target 寄りの挙動にそろえる」という選択が取られています。
- 参考情報 (あれば)
- 当該 PR: https://github.com/rails/rails/pull/58372
- 問題のきっかけとなったコミット: rails/rails@56c95aee
- 関連クラス・メソッド:
ActiveRecord::Relation#idsActiveRecord::Associations::CollectionProxy#pluckActiveRecord::Associations::CollectionProxy#target/#scope/#null_scope
#58374 Ractor safe Active Record primary key
マージ日: 2026/8/5 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Active Record の「主キーオブジェクト」を Ractor 内でも安全に扱えるようにするため、主キーを表すオブジェクトを freeze する変更が入りました。単一 PK・複合 PK・PK なしのすべてのケースで、計算済みの主キーが不変オブジェクトとして扱われるようになります。
- 変更内容の詳細
何をしているか
ActiveRecord::Key(主キーを表現するオブジェクト)生成時に、そのインスタンスをfreezeするように変更。- 単一カラム PK・複合 PK・PK なし(
None)の3種類のキーオブジェクトすべてが対象。 - 主キーは一度計算された後に変更されることがなく(不変)、
freezeすることで Ractor 間で安全に共有できるようにしている。
PR の説明から読み取れるポイント:
- 「Ractor 内からレコードの primary key に安全にアクセスしたい」という要件に対する対応。
- Ractor では「共有オブジェクトは不変(freeze 済み)であること」が必要なため、主キーオブジェクトもそのルールに合わせた。
テスト追加内容
activerecord/test/cases/primary_keys_test.rbに 18 行追加。- 想定されるテスト内容:
- 単一 PK の場合に主キーオブジェクトが
frozen?であること。 - 複合 PK の場合にキーオブジェクトおよび内部構造(配列等)が
frozen?であること。 - PK なしモデル(
None)でもキーオブジェクトがfrozen?であること。
- 単一 PK の場合に主キーオブジェクトが
- これにより、「全タイプの PK で freeze しても問題なく動作する」ことを保証している。
(実際のコードイメージ・擬似例)
# 例: ActiveRecord::Key の生成タイミングで freeze される
key = ActiveRecord::Key.new(primary_key_values)
key.frozen? # => true
# レコード経由での利用イメージ
user = User.find(1)
user.primary_key # => #<ActiveRecord::Key ...> (内部的には freeze 済み)
user.primary_key.frozen? # => true- 影響範囲・注意点
主キーオブジェクトを後から書き換えるようなコードは壊れる
- たとえば、内部的な主キーオブジェクト(複合キーの配列など)に対して:
<<で要素を追加するmap!,replaceなどの破壊的メソッドを呼ぶ
- といったコードがあると、freeze により
FrozenErrorが発生します。 - そもそも主キーは論理的に不変であるべきものなので、こうしたコードがあれば元々設計的に危ういものです。この PR をきっかけに見直すべき箇所といえます。
- たとえば、内部的な主キーオブジェクト(複合キーの配列など)に対して:
通常の Active Record 利用に対する影響はほぼゼロ
- 典型的な利用(
record.id,record.to_param,find,where(id: ...)等)は主キーオブジェクト自体を直接書き換えないため、振る舞いに変化はありません。 - 内部表現のオブジェクトが freeze されたことによる副作用が出るのは、主キーオブジェクトを直接保持してミューテーションしているような特殊なコードに限られます。
- 典型的な利用(
Ractor を使う並行実行環境でのメリット
ActiveRecord::Base#primary_key/ 主キー関連の内部表現を Ractor 間で共有しても安全になります。- Ractor 内で
record.primary_keyに触れるようなユースケースでも、Ractor の「共有は不変オブジェクトのみ」ルールを満たせます。
- 参考情報 (あれば)
Ractor の制約(Ruby 本体):
- 共有可能オブジェクト: freeze 済みオブジェクト、Ractor ローカルに生成されたオブジェクトなど
- 参考: Ruby 3 の Ractor 仕様(公式ドキュメントや Matz の発表など)
Active Record の主キー関連コード:
activerecord/lib/active_record/key.rb(今回の変更対象)activerecord/test/cases/primary_keys_test.rb(freeze 保証のテストが追加された場所)
この PR は、「主キーオブジェクトは不変である」という元々の前提をコードレベルで明示しつつ、Ractor 対応も同時に進めるための小さく安全な変更といえます。
#58375 Add NUL (\0) to the list of unsafe characters for storage
マージ日: 2026/8/4 | 作成者: @rosa
- 概要 (1-2文で)
Active Storage のファイル名サニタイズ処理において、NUL 文字 (\0) を「危険な文字」として明示的に除去するようにした変更です。
これにより、ユーザーアップロード由来のファイル名に NUL バイトが含まれている場合でも、ArgumentError: string contains null byteなどの例外が発生しにくくなります。
- 変更内容の詳細
背景
- ユーザーがアップロードするファイル名に NUL 文字 (
\0) が紛れ込むケースがある。 - そのファイル名を
File.joinなど、Ruby 標準ライブラリのファイル操作メソッドに渡すと、Ruby 側でArgumentError: string contains null byteが発生することがある。 - Active Storage では
ActiveStorage::Filename#sanitizedでサニタイズを行っているが、これまで NUL 文字は文字列の先頭/末尾にある分だけString#stripによって偶然取り除かれており、「途中」に含まれる NUL 文字は残っていた。
今回の変更
ActiveStorage::Filename#sanitizedが参照する「unsafe characters(不正・危険な文字)」の一覧に NUL 文字 (\0) を追加。- これにより、ファイル名中の NUL 文字は位置にかかわらず削除される。
イメージとしては、もともとこんな感じの正規表現(例)で unsafe 文字を除去していたところに、\0 が追加されたという変更です(実際のコードイメージ):
# 変更前(イメージ)
UNSAFE = /[\/\\:*?"<>|]+/
def sanitized
name.gsub(UNSAFE, "_").strip
end
# 変更後(イメージ: NUL を追加)
UNSAFE = /[\/\\:*?"<>|\0]+/
def sanitized
name.gsub(UNSAFE, "_").strip
end※上記は概念的な例であり、実際の定数名・正規表現とは多少異なる可能性がありますが、やっていることは「unsafe 文字の集合に \0 を追加する」一点です。
テスト
activestorage/test/models/filename_test.rbが 1 行修正されており、NUL 文字が含まれたファイル名がサニタイズ時に適切に処理されることを検証するテストが追加/調整されています。
- 影響範囲・注意点
影響範囲:
- Active Storage を使っており、かつユーザー由来のファイル名に NUL 文字が含まれる可能性があるアプリケーション。
- 特に、
ActiveStorage::Blob#filenameを元にFile.join,File.open,Pathnameなど OS ファイルパス関連 API を使うコード。
振る舞いの変化:
- 以前: ファイル名の先頭/末尾の NUL は
stripによって消えるが、途中にある NUL は残る → Ruby のファイル API を呼んだタイミングでArgumentErrorが起きることがある。 - 以後: ファイル名中の NUL はすべてサニタイズ対象となり削除される → そのファイル名をそのままファイル API に渡しても、少なくとも NUL バイト起因の
ArgumentErrorは発生しにくくなる。
- 以前: ファイル名の先頭/末尾の NUL は
後方互換性 / 破壊的変更の懸念:
- 一般的なファイルシステムでは NUL 文字はそもそもファイル名として無効なので、「真に有効なファイル名」を破壊する変更ではありません。
- もしアプリケーション内部で「NUL 文字を含むファイル名文字列」をあえて保持していた場合は、その NUL が今後はサニタイズ処理で削除される点に注意が必要です(ただし、通常の利用形態では問題にならないはずです)。
- 参考情報 (あれば)
- Ruby の
ArgumentError: string contains null byteは、C 側の文字列処理で NUL 終端を前提にしているため、バイナリ中に NUL が含まれているとファイル名などとして扱えずエラーになることが多いです。 - 多くのファイルシステム(UNIX, Windows 含む)では、NUL 文字はファイル名に使えない予約文字となっており、今回の変更はその制約に沿ったサニタイズ強化といえます。
#58363 Make Proc layouts shareable
マージ日: 2026/8/4 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Rails のレイアウトとして利用されるProcオブジェクトを、Ractor 対応のために「shareable(共有可能)」として扱うようにした変更です。これにより、特に turbo-rails が提供する Proc ベースのレイアウト定義などで、Ractor 実行時の問題を早期に検出しやすくなります。
- 変更内容の詳細
何が変わったか
対象ファイル: actionview/lib/action_view/layouts.rb
レイアウトが Proc で指定されている場合に、その Proc を ActiveSupport::Ractors.try_shareable_proc を使って「共有可能な Proc」に変換(あるいはチェック)する処理が追加されました。
イメージとしては、従来:
# 擬似コードイメージ
if layout.is_a?(Proc)
@layout = layout
endだったところが、以下のような形に近づいています:
if layout.is_a?(Proc)
@layout = ActiveSupport::Ractors.try_shareable_proc(layout)
end※正確なメソッド名や呼び出し位置は PR 本体に依存しますが、やっていることは「レイアウト用 Proc に対して try_shareable_proc を適用する」です。
ActiveSupport::Ractors.try_shareable_proc は、
- 渡された
Procを Ractor 間で共有可能な形にできるならそのように扱い、 - 共有不可能(=中で Ractor 非共有オブジェクトをキャプチャしている等)の場合は例外・エラーを発生させる/共有不可であることを示す
といった性質のヘルパです。
テストの追加
対象ファイル: actionview/test/actionpack/abstract/layouts_ractor_test.rb
Ractor 環境でレイアウトが正しく動作するか、あるいは問題を適切に露呈するかを確認するテストが 46 行分追加されています。
具体的には:
- Proc レイアウトを使うコントローラを Ractor 内から呼び出した場合に、レイアウト Proc が
try_shareable_procを通して扱われること - 共有可能な Proc と共有不可能な Proc のケースを分けて検証すること
などが確認されていると考えられます。
- 影響範囲・注意点
影響範囲
- Proc レイアウトを利用しているアプリケーション全般
- コントローラで
layout ->(controller) { ... }のように Proc を返すレイアウトを定義している場合が対象になります。
- コントローラで
- 特に turbo-rails のフレーム用レイアウト
turbo-railsでは、turbo frame 用に Proc ベースの layout 定義が使われており(PR 説明でリンクされている箇所)、Ractor 環境下でこの変更の影響を受けます。
何が変わるのか(挙動面)
- Ractor を使っていない(通常のシングルスレッド/マルチスレッド)環境では、挙動は基本的に従来と変わりません。
- Ractor を利用する場合:
- レイアウト用の Proc が Ractor 共有不可なオブジェクトをキャプチャしていると、
try_shareable_procの段階で問題が顕在化します。 - これにより、「実際のリクエスト処理中に謎の Ractor 関連エラーが出る」のではなく、「アプリケーション起動や Ractor セットアップ時点で原因が分かる」形に近づきます。
- レイアウト用の Proc が Ractor 共有不可なオブジェクトをキャプチャしていると、
注意点(開発者が見るべきポイント)
Proc レイアウトの中身が Ractor 共有可能かを意識する必要がある
- 例: レイアウト決定ロジックで、
- クラス定数・シンボル・凍結済み文字列などの shareable なオブジェクトを使う分には問題ないが、
- コントローラインスタンスのミューテーブルな状態や、非 shareable なオブジェクトを閉じ込めているとエラーになる可能性があります。
- 典型例:ruby
# 悪い例になりうる(イメージ) SOME_HASH = {} layout ->(controller) { SOME_HASH[controller.request.format] } # => ミューテーブルな Hash をキャプチャしているため共有不可になりうる
- 例: レイアウト決定ロジックで、
Ractor サポートを試すときに、問題を早めに見つけられる
- この PR の意図として、「アプリオーナーが Ractor サポートを試した時に、問題を直接(Proc レイアウト定義の箇所で)見つけられるようにする」という狙いがあります。
- つまり、Ractor 対応に向けてアプリを修正する際、Proc レイアウトが早期にチェックされるようになります。
turbo-rails 側の対応 PR との関係
- turbo-rails 側でも、「Proc レイアウトをメソッドに置き換える」PR (#786) が既に存在し、こちらも合わせて導入されると、turbo-rails のフレームレイアウトに関する Ractor 問題はさらに軽減される見込みです。
- ただし、Rails 本体としては Proc レイアウトをサポートし続けるため、その扱いを Ractor 観点でも安全にするという位置付けです。
- 参考情報 (あれば)
- 該当 PR:
- 言及されている turbo-rails の Proc レイアウト実装:
- turbo-rails 側の関連 PR(Proc をメソッド化する案):
- Ractor / shareable オブジェクトの概念(Ruby 本体リファレンス):
- https://docs.ruby-lang.org/ja/latest/doc/spec=2fractor.html (日本語版が追いついていない場合は英語版 Ractor ドキュメント参照)
#58362 Freeze ActionView::Template::Handlers::ERB.escape_ignore_list
マージ日: 2026/8/4 | 作成者: @Edouard-chin
- 概要 (1-2文で)
ActionView::Template::Handlers::ERB.escape_ignore_listをアプリ起動時に freeze し、Ractor 内からも安全に参照できるようにした PR です。これにより、マルチ Ractor 環境での ActionView/ERB の設定利用がしやすくなります。
- 変更内容の詳細(あればサンプルコードも含めて)
何をしているか
ActionView::Template::Handlers::ERB.escape_ignore_listを Rails 起動時に freeze する処理を追加- それに対応するテストを
railties側に追加
変更ファイル:
actionview/lib/action_view/railtie.rbrailties/test/application/configuration_test.rb
具体的なコードイメージ
ActionView::Railtie の初期化プロセスのどこかで、
ActionView::Template::Handlers::ERB.escape_ignore_list.freezeのような処理が行われるようになっています(実際には config.after_initialize など、Rails のブートシーケンスに沿った場所)。
テスト側では、おおよそ次のようなことを検証しています:
app "some_app" do
# アプリ起動
end
assert_predicate ActionView::Template::Handlers::ERB.escape_ignore_list, :frozen?背景・モチベーション
- Ractor では「共有できるオブジェクト」は原則イミュータブル(freeze 済みなど)である必要があります。
escape_ignore_listは ERB のエスケープ処理において「エスケープしない対象(メソッド名など)」を定義するための Public API の設定配列。- Ractor 内でこの配列にアクセスしたいが、現状 freeze されていないために共有オブジェクトとして扱えない、という制約があった。
- 実運用上、この配列をランタイム中に書き換えるケースはほぼなく、多くは「ブート時に一度だけ設定」されるスタイルで使われていることが GitHub 検索で確認されている。
このため、「起動後は不変である」という前提を明示するためにも freeze するのが妥当、という判断です。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは、
ActionView::Template::Handlers::ERB.escape_ignore_listを アプリケーション起動後に変更しようとしているコード です。 - 典型的な使われ方は以下のように「初期化時だけ変更」するパターンであり、これは今後も問題ありません:
# config/initializers/erb_escape.rb
ActionView::Template::Handlers::ERB.escape_ignore_list << :raw_htmlRails 起動プロセス中(初期化フェーズ)の段階で freeze されるため、
- Rails 推奨の「initializer で設定する」スタイルなら基本的に動作します。
- 本番稼働中に動的にこの配列を書き換えるようなコードは、freeze によって
FrozenErrorが発生するようになります。
注意点
- もし以下のようにリクエスト処理中やジョブ実行中など、遅いタイミングで書き換えをしている場合はエラーになります:
# NG(今後は FrozenError)
def add_escape_ignore_at_runtime
ActionView::Template::Handlers::ERB.escape_ignore_list << :some_method
endその場合は:
- 起動時(initializer / Railtie / Engine の
config.before_initializeなど)に設定を移動する - あるいは
escape_ignore_listを直接いじらず、自前のヘルパー / wrapper を設計する
といった対応が必要です。
- Ractor を利用している / する予定のアプリにとっては:
- この配列を Ractor に渡したり、Ractor から参照したりすることが可能になり、並行実行時の安全性・利便性が上がります。
- 参考情報 (あれば)
- この設定は Public API として扱われており、ドキュメントも #58359 で再導入予定とされています。
- Ractor での共有オブジェクト要件(イミュータブルであること等)は Ruby 本体の仕様に依存します。
参考: Ruby の Ractor ドキュメント (doc/ractor.mdなど)
#58368 Use ActiveRecord::Key to unify single / composite FK handling in BelongsToAssociation
マージ日: 2026/8/4 | 作成者: @kamipo
- 概要 (1-2文で)
belongs_to関連の内部実装で、単一外部キーと複合外部キーの扱いをActiveRecord::Keyに統一し、is_a?(Array)やArray(...)などの分岐を排除してコードを簡潔・一貫化した変更です。これにより、BelongsToAssociation(および多態関連)で複合キーを扱うための基盤が整理され、保守性と読みやすさが向上しています。
- 変更内容の詳細
全体方針
reflection.foreign_key(単一キー or 複合キー)を都度判定せず、- PR #58077 などで導入されたと思われる
ActiveRecord::Keyを使って- 列挙 (
Enumerable) - 値の取得 (
value_of(record_or_hash)) - 複合キー判定 (
composite?)
- 列挙 (
- を一元的に扱うようにした変更です。
これにより、「foreign_key が配列かどうかを毎回チェックする」スタイルから、「Key オブジェクトに聞く」スタイルへ移行しています。
1) BelongsToAssociation の変更
対象: activerecord/lib/active_record/associations/belongs_to_association.rb
主なポイント:
reflection.foreign_keyをそのまま使う代わりに、初期化時にActiveRecord::Key.for(reflection.foreign_key)を計算して@foreign_keyにキャッシュ。- 以降、外部キーに関する操作は 全て
@foreign_key経由で行います。
典型的な差分イメージ:
Before(イメージ):
def replace_keys(record)
if reflection.foreign_key.is_a?(Array)
reflection.foreign_key.each do |key|
owner[key] = record ? record.send(key) : nil
end
else
owner[reflection.foreign_key] = record ? record.send(reflection.association_primary_key) : nil
end
endAfter(イメージ):
def foreign_key
@foreign_key ||= ActiveRecord::Key.for(reflection.foreign_key)
end
def replace_keys(record)
foreign_key.each do |key|
owner[key] = record ? foreign_key.value_of(record)[key] : nil
end
end※ 実際のコードはもう少し複雑ですが、構造としては「Array 判定をやめて Key オブジェクトの API だけで処理する」という変更です。
ActiveRecord::Key の想定 API:
Key.for(value)- 引数がシンボル/文字列/配列などでも、常に
ActiveRecord::Keyインスタンスを返すファクトリ。
- 引数がシンボル/文字列/配列などでも、常に
each/include?などのEnumerableインターフェースcomposite?- 外部キーが複数カラム(配列)かどうか。
value_of(record_or_hash)- 渡されたレコードや Hash から、そのキーに対応する値(複合キーなら複数)をまとめて取得。
BelongsToAssociation 内の具体的な影響:
- 外部キーの読み書き、比較、nil チェックなどで、
is_a?(Array)/Array(reflection.foreign_key)のような場当たり的分岐が削減。foreign_key_columns.eachのようなイテレーションが@foreign_key.eachに統一。- 「単一キーか複合キーか」を意識したロジックが
composite?にカプセル化される。
結果として、belongs_to 関連が 単一キー/複合キーの両方を同じコードパスで扱えるように近づいている と考えられます。
2) BelongsToPolymorphicAssociation の変更
対象: activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
多態 belongs_to に対しても同様の整理が入っています。
- 多態関連では
foreign_keyに加えてforeign_typeも扱いますが、そのうち 外部キー部分についてActiveRecord::Keyベースの扱いに変更。 - ここでも
Array(...)などの分岐が削られ、@foreign_key的なActiveRecord::Keyオブジェクトを使って一貫した処理を行うようになっています。
多態かつ複合キーというややこしいパターンを視野に入れた、インターフェースの統一と言えます。
3) BelongsTo ビルダーの変更
対象: activerecord/lib/active_record/associations/builder/belongs_to.rb
belongs_to :foo を宣言した際に内部で使われるビルダーのコードも調整されています。
- ここでも
reflection.foreign_keyを直接配列変換したりArray(...)で包む処理が減り、 - 代わりに
ActiveRecord::Key経由の API を前提とした実装へ。
たとえば、関連付けで:
- デフォルト値の設定
inverse_of判定touchやoptionalオプションでの外部キー利用 などの箇所が、単一キー・複合キーどちらでも Key オブジェクトで扱える前提に整理されていると考えられます。
- 影響範囲・注意点
影響範囲
- 対象となるのは内部実装レベルで、通常のアプリケーションコード(
belongs_to :userなどの宣言や、user.postのような利用方法)は API 的には変わりません。 - 想定される主な効果:
belongs_to/ 多態belongs_toでの内部動作がシンプルになり、今後の複合キーサポートや拡張がしやすくなる。- 単一 FK / 複合 FK 両対応のコードパスが増え、複合主キー/外部キーサポートを強化する布石になっている。
注意点(開発者目線)
内部 API の変更に依存している場合は注意
以下のようなコードを書いている場合、動作に影響が出る可能性があります。- AR の内部クラス(
BelongsToAssociation/BelongsToPolymorphicAssociation)を monkey patch している。 reflection.foreign_keyが配列かどうかを前提に内部の動きを拡張している。belongs_to関連の内部変数を前提にカスタマイズしている。
- AR の内部クラス(
今後、複合キー対応が進むことを前提とした内部設計になっているため、
- 「カスタム関連」や「独自の association クラス」を実装している場合は、
- 将来的に
ActiveRecord::Keyを使うほうが Rails 本体との互換性を維持しやすくなりそうです。
仕様変更というよりリファクタリングに近いため、通常の利用では挙動変化は最小限に抑えられていると考えられますが、
- 複合キーを駆使しているアプリでは、edge case(nil を含む複合 FK、部分的に欠けたキーなど)が扱われ方の変化でバグがあぶり出される可能性があります。
- CI で関連周り(
belongs_to・多態belongs_to)のテストをしっかり回しておくと安全です。
- 参考情報 (あれば)
PR 本体:
https://github.com/rails/rails/pull/58368
(ActiveRecord::Keyの導入背景や同時期の PR を追うと、複合キー周りの設計方針がより明確に把握できます)関連しそうなトピック:
ActiveRecord::Key導入 PR(番号はこの PR の description では明示されていませんが、近い番号の PR を辿ると見つかる可能性が高いです)- 複合主キー・複合外部キー対応に関する議論・PR 群
reflection.foreign_keyの扱いと、多態関連でのforeign_typeの扱い方の違い
この PR 自体は大きな外向き API 変更ではなく、内部実装の整理と今後の複合キー対応強化のための基盤整備と捉えるのが適切です。
#57994 Make Mime types ractor-shareable
マージ日: 2026/8/4 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Mime::Type のレジストリと関連インデックスを「アプリケーションの eager load 完了後に凍結(freeze)」し、Ractor で共有可能な状態にする変更です。これに伴い、eager load 後に MIME type を登録/解除することは非推奨となり、既存の Mime 定数プロキシや ActionView::Base.default_formats が新しいレジストリを正しく参照するように調整されています。
- 変更内容の詳細
2-1. 目的: Mime レジストリを Ractor 共有可能にする
Ruby の Ractor を安全に使うには、共有するオブジェクトがイミュータブル(凍結済み)である必要があります。
この PR では:
- Mime::Type のレジストリとそのインデックス(例:
Mime::LOOKUP,Mime::SET,Mime::EXTENSION_LOOKUPなど)を eager loading のタイミングで freeze - 各 Mime::Type インスタンス自体も、生成時点で freeze された状態 & インスタンス変数も freeze 済みに
- これにより、Mime 関連のデータ構造を Ractor 間で安全に共有可能に
2-2. eager load 完了後の登録/解除を非推奨化
これまで:
Mime::Type.register "application/x-custom", :custom
Mime::Type.unregister :htmlのように、アプリケーション起動後でも自由に MIME type の登録/解除を行えました。
この PR 以降(特に config.eager_load = true な本番環境など)では:
- eager loading 完了後に
register/unregisterを呼ぶと 非推奨警告 (deprecation) が出る - 将来的には、そのタイミングでの変更自体がサポートされなくなる予定
- 最終的にレジストリ更新メソッド
update_registriesは削除される想定
開発者に求められる対応方針:
- MIME type の登録/解除は アプリケーションブート時(eager loading 前) に集約する
- 例:
config/initializers/mime_types.rbなどで定義し、アプリケーションロード時に読み込まれるようにする
- 例:
# config/initializers/mime_types.rb
Mime::Type.register "application/x-custom", :custom2-3. update_registries と DeprecatedObjectProxy の調整
Mime::Type.update_registries は、内部のレジストリオブジェクトを新しい frozen なものに入れ替える処理をしています。
このとき:
- 既存の Mime 関連定数に対して、互換性のために用意されている DeprecatedObjectProxy ベースの定数プロキシが「古いレジストリ」を参照し続けると不整合が起きる
- そこで:
DeprecatedObjectProxyにtarget=セッターを追加update_registries実行後に、これらのプロキシが新しいレジストリオブジェクトを参照するように「差し替え」
これにより:
- 古い定数経由でも常に最新の MIME レジストリ状態が見える
- かつ、内部的には freeze された安全な構造になっている
2-4. ActionView::Base.default_formats の扱い
ActionView では、ActionView::Base.default_formats が Mime.symbols の配列を共有している実装があります(過去 PR #38141 で導入)。
この PR では:
Mime::Type.on_changeという仕組みを使い、レジストリ(およびMime.symbols)の入れ替え時にActionView::Base.default_formatsの参照も更新- ただし、アプリ側で
ActionView::Base.default_formats = ...のように 独自に置き換えられている場合 は、そのまま
これにより:
- デフォルトのまま使っている場合は Ractor 共有可能な mime symbols を自動的に参照
- 独自設定しているアプリはそのまま動き続けるが、その配列が Ractor 共有可能かどうかはアプリ側の実装次第
2-5. その他の変更点(ファイル別の要点)
actionmailer/lib/action_mailer.rb/actionpack/lib/action_dispatch.rb/actionpack/lib/action_dispatch/railtie.rb- フレームワーク初期化フローのどこかのタイミングで MIME レジストリを確定・freeze するよう連携
actionpack/lib/abstract_controller/collector.rb/actionview/lib/action_view/template/types.rb- Mime::Type の変更に追随(インターフェースや定数の参照方法を現行仕様に合わせて調整)
actionpack/test/dispatch/mime_type_test.rb/actionview/test/template/types_test.rb- 新仕様(freeze 後の挙動・非推奨警告・レジストリ再ポイント)が正しく動くかどうかを確認するテストを追加/修正
- 影響範囲・注意点
3-1. 実務的な影響が出やすいケース
- 本番環境などで
config.eager_load = trueかつ:- リクエスト処理中、コンソール実行中、ジョブ実行中など「アプリケーション起動後」に
Mime::Type.register/unregisterを呼んでいる - 特定の initializer を
to_prepareなどで再実行するたびに MIME type を登録し直している
- リクエスト処理中、コンソール実行中、ジョブ実行中など「アプリケーション起動後」に
これらは今後:
- まずは deprecation warning が出る
- いずれ 完全にサポートされなくなる可能性が高い
対策:
- MIME type の登録/解除は、アプリケーションブート時に一度だけ 行われるようにする
- 冪等な initializer にまとめる(複数回実行されないようにする、もしくは register 前に存在チェックを入れる)
3-2. ライブラリ・gem 作者への注意
- Rails アプリに読み込まれた後(eager loading 完了後)に MIME type 登録を行う gem は、将来的に挙動が変わる
- 対応方針:
- Railtie / Engine の
initializerを使い、アプリケーションの eager load 前に登録を済ませる - 必要に応じて
config.to_prepareではなくconfig.before_eager_load/ 通常 initializer を用いる
- Railtie / Engine の
3-3. Ractor を使うユーザにとってのメリット
- Mime::Type 関連のデータ構造が Ractor 共有可能(ractor-shareable)になり、マルチ Ractor 構成で安全に再利用可能
- その結果:
- Ractor 間で MIME registry をコピーする必要が減る
- メモリ使用量削減・パフォーマンス改善が期待できる
- 参考情報 (あれば)
- 当該 PR:
- 既存の
ActionView::Base.default_formatsとMime.symbolsの共有に関する過去 PR: - Ractor と shareable オブジェクトについて(Ruby 公式ドキュメント):
#58326 Define Tags#field_type when the class is loaded
マージ日: 2026/8/4 | 作成者: @etiennebarrie
- 概要 (1-2文で)
ActionView::Helpers::Tags系のfield_typeが「最初に使われたときに初期化される」実装だったため、メイン Ractor 以外で最初のフォームフィールドを描画すると例外になる問題を修正した PRです。
クラスロード時にfield_typeを確定させ、Ractor 間で共有可能なようにfreezeすることで、Ractor 環境でも安全にフォームヘルパを利用できるようにしています。
- 変更内容の詳細
背景
- もともと
TextFieldなどのタグクラスは、field_typeをクラスインスタンス変数に遅延メモ化していました。 - 遅延メモ化は「最初に呼ばれたタイミングで値をセット」するので、
- 最初の呼び出しがメイン Ractor 以外だと、Ractor 間共有できないオブジェクトの書き込みや初期化順序の問題で例外が発生する状況がありました。
- Ractor セーフにするには、
- クラス定義時(ロード時)に
field_typeを確定させる - その値を
freezeして Ractor-shareable にする
という方針が取られています。
- クラス定義時(ロード時)に
TextField クラスの変更
対象ファイル: actionview/lib/action_view/helpers/tags/text_field.rb
主なポイント:
クラスロード時に
field_typeを確定させるクラス本体の中で
field_typeを呼び出しておき、クラス定義時にメモ化させるようにしました。イメージとしては:
rubyclass TextField < Base # クラス本体の評価中に呼ぶことで、クラスロード時にメモ化される field_type end(実際のコードではメモ化ロジックと一緒に整理されているはずです。)
TextField.inheritedの実装TextFieldを継承したサブクラス(匿名でないもの)についても、同じようにクラス定義時にfield_typeを確定させるため、inheritedフックが追加されています。概念的にはこういう処理をしています:
rubyclass TextField < Base class << self def inherited(subclass) super # 匿名クラスでなければ、ロード時に field_type を確定させる unless subclass.name.nil? subclass.field_type end end end endこれにより、例えば以下のような継承クラスでも安全に Ractor から利用できます:
rubyclass CustomTextField < ActionView::Helpers::Tags::TextField # 独自の振る舞いを追加 endfield_typeメモ化時にfreezefield_typeをクラスインスタンス変数にセットする際にfreezeし、Ractor 間共有の制約に適合させています:ruby@field_type ||= super_field_type.freezeこれにより、
@field_typeが変更不能であり、Ractor-shareable とみなされるようになります。
DatetimeLocalField の変更
対象ファイル: actionview/lib/action_view/helpers/tags/datetime_local_field.rb
- 以前は
DatetimeLocalFieldがfield_typeをオーバーライドしていましたが、今回の方針に合わせて、- メソッドオーバーライドではなく
- クラス本体からクラスインスタンス変数を直接設定する形
へと変更されています。
イメージ:
class DatetimeLocalField < TextField
# 以前:
# def field_type
# "datetime-local"
# end
# 今回:
@field_type = "datetime-local".freeze
endこうすることで、TextField 側の「クラスロード時に field_type を確定させる」仕組みと整合しつつ、Ractor セーフな実装になります。
Ractor テストの追加
対象ファイル: actionview/test/template/tags_ractor_test.rb
- Ractor 環境でタグヘルパが正しく動くことを確認するテストが追加されています。
- 具体的には:
- メイン Ractor ではない Ractor 内から
TextField/DatetimeLocalFieldなどのタグクラスを利用してフォームフィールドをレンダリングしても例外にならないこと field_typeのメモ化と共有が期待通りに動くこと
を検証しています。
- メイン Ractor ではない Ractor 内から
- 影響範囲・注意点
主な影響対象:
ActionView::Helpers::Tags::TextFieldとそのサブクラスDatetimeLocalField- Ractor を利用した並行レンダリング環境(view レンダリングを並列化しているアプリなど)
互換性:
- 公開インターフェース(
field_typeの戻り値そのもの)には変化はなく、主に初期化タイミングと内部実装の変更です。 TextFieldやそのサブクラスでfield_typeを独自にオーバーライドしている場合でも、通常はそのまま動作します。- ただし、
field_typeが「Ractor-shareable ではないオブジェクト」(ミューテブルなオブジェクトなど)を返していると、Ractor 利用時に問題が発生する可能性があります。
→String等を返す場合はfreezeされる/することを前提にしておくのが安全です。
- ただし、
- 公開インターフェース(
メモ化タイミングの変化:
- これまで「最初にフィールドがレンダリングされたタイミング」で確定していた
field_typeが、 - 「クラス定義時(=アプリ起動時/autoload 時)」に確定されます。
- 一般的なアプリでは副作用はありませんが、「
field_type決定時に外部状態に依存するようなロジック」を書いている場合は、挙動が変わる可能性があります(そのような書き方は推奨されません)。
- これまで「最初にフィールドがレンダリングされたタイミング」で確定していた
- 参考情報 (あれば)
- Ractor と shareable オブジェクト:
- Ruby 3 系の Ractor では、Ractor 間で共有するオブジェクトは
Ractor.shareable?がtrueとなる必要があります。 - イミュータブルな
String("foo".freeze)などは shareable ですが、ミューテブルなオブジェクトはそうではありません。
- Ruby 3 系の Ractor では、Ractor 間で共有するオブジェクトは
- Rails における Ractor 対応方針:
- メモ化やキャッシュを行うクラスインスタンス変数・クラス変数について、
- 「クラスロード時に初期化する」
- 「
freezeする」
といったパターンで Ractor 対応が行われつつあります。
- メモ化やキャッシュを行うクラスインスタンス変数・クラス変数について、
- 関連しそうなコード:
ActionView::Helpers::Tags::Baseや他のフィールドクラス(CheckBox,RadioButtonなど)も、今後同様の変更が行われる/すでに行われている可能性があります。
#58357 Define DateTimeSelector datetime readers with shareable lambdas
マージ日: 2026/8/4 | 作成者: @etiennebarrie
- 概要 (1-2文で)
ActionView::Helpers::DateHelper::DateTimeSelectorが Ractor 内からも安全に使えるように、year/month/day/hour/min/secのリーダーメソッド定義方法を「Ractor 共有可能な lambda」を使った形に変更した PR です。あわせて、この挙動を検証する Ractor 対応テストが追加されています。
- 変更内容の詳細
何が問題だったか
DateTimeSelector は、select_datetime などで <select> ベースの日付・時刻入力フィールドを描画するときに内部的に使われるクラスです。このクラスは、渡されたオブジェクトから year, month, day, hour, min, sec を取り出すための reader メソッドを define_method で定義していました。
もともとの実装はおおよそ次のようなイメージです:
# 疑似コード: 実際とは多少異なる可能性あり
[:sec, :min, :hour, :day, :month, :year].each do |method|
define_method(method) do
@datetime.public_send(method)
end
endここで define_method に渡しているブロック(do ... end の部分)は、Ractor 的には「Ractor 間で共有できない Proc」となりがちです。そのため、
- Ractor 内から
DateTimeSelectorのyearなどのメソッドを呼ぼうとすると - そのメソッド本体が「共有不可能な Proc をキャプチャしている」扱いになり
Ractor セーフではない状況が発生します。
PR の説明にもあるとおり、根本的な目的は「Ractor から sec min hour day month year を呼べるようにする」ことです。
どう修正したか
方針は「Ractor 共有可能な lambda を使ってメソッドを定義する」に変える、というものです。
Rails では「Ractor 共有可能な lambda」を作る場合、おおよそ以下のようなパターンを使います:
- ブロック内で外部のミュータブルなオブジェクトをキャプチャしない
- 構造的に Ractor.shareable? が true になるような Proc/lambda を使う
今回の PR では、これまで define_method に直接ブロックを渡していた部分を、あらかじめ定義済みの 共有可能 lambda を使って定義する形に切り替えています。イメージとしては以下のような変更です(概念的な例):
# 以前(イメージ)
[:sec, :min, :hour, :day, :month, :year].each do |method|
define_method(method) { @datetime.public_send(method) }
end
# 以後(イメージ: 実際とは細部が異なることがあります)
DATETIME_READER = ->(obj, name) { obj.public_send(name) }
DATETIME_READER.shareable! if DATETIME_READER.respond_to?(:shareable!)
[:sec, :min, :hour, :day, :month, :year].each do |method|
define_method(method, &-> { DATETIME_READER.call(@datetime, method) })
end実際のコードでは、lambda の定義方法や define_method への渡し方など、Ractor 共有可能性を満たすように細かく調整されていますが、ポイントは:
- メソッド本体を構成する Proc を「共有可能なもの」にする
- それを
define_methodに渡してメソッドを定義する
という構造になっている点です。
テストの追加
actionview/test/template/date_helper_ractor_test.rb が新規追加され、Ractor 上で DateTimeSelector を使って日付/時刻 <select> を生成できるかを検証しています。
テストのイメージ:
- Ractor を起動
- その中で
renderあるいはヘルパーメソッド(select_datetime等)を実行 - 例外が発生しないこと、期待した HTML が生成されることを確認
といった形で、「Ractor 内から DateHelper 経由で DateTimeSelector を問題なく呼べる」ことを保証しています。
- 影響範囲・注意点
対象:
ActionView::Helpers::DateHelper::DateTimeSelectorが内部的に使われるすべての date/time<select>ヘルパーselect_dateselect_timeselect_datetimedate_selecttime_selectなど
既存の挙動への影響:
- メソッドの外部インターフェース (
year,month等) は変わっていないため、通常の(シングルスレッド/非 Ractor)環境での振る舞いは基本的に変わりません。 - 主な変更は「内部のメソッド定義手段」であり、HTML 出力内容は変化しないことが前提です(テストで担保)。
- メソッドの外部インターフェース (
Ractor を使っている/使う予定のアプリへの影響:
- これまで Ractor 内で
select_datetime等を使うとエラーになっていた場合でも、この変更により正常に動作することが期待できます。 - Ractor セーフティに関わる変更のため、Ractor を活用しているアプリでは、アップデート後に date/time フォームヘルパー周りのテストを一度通しておくと安心です。
- これまで Ractor 内で
実装パターンへの示唆:
- 「Ractor から呼び出される可能性のある DSL/ヘルパーで
define_method + ブロックを使う場合、共有可能 lambda を検討する」という Rails コアの方針の一例と言えます。 - 同様のパターンをアプリ側や gem 側で使っている場合も、Ractor 対応を意識するなら、
module_evalや shareable lambda でのメソッド定義を検討する余地があります(PR 説明にもmodule_eval案の言及あり)。
- 「Ractor から呼び出される可能性のある DSL/ヘルパーで
- 参考情報 (あれば)
該当 PR:
関連トピック:
- Ruby Ractor と Proc/lambda の shareability
- Rails の Ractor 対応方針(特に ActionView / ActiveSupport のスレッド・Ractor セーフティ対応)
#58352 Allow to access the type of an attribute inside a ractor
マージ日: 2026/8/4 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Active Model / Active Record の属性タイプ情報(attribute_types)を Ractor 内から安全に参照できるようにするため、属性タイプのオブジェクトグラフを凍結(deep freeze)し、暗号化関連タイプのメモ化ロジックを Ractor セーフな形に修正した PR です。これにより、Ractor 内で属性の型を取得しても分離エラーが発生しないようになります。
- 変更内容の詳細
背景: なぜ問題だったか
- Active Model の
attribute_typesはハッシュをメモ化しており、そのハッシュや内部にぶら下がるオブジェクトが「Ractor シェア可能」になっていませんでした。 - Ractor 間でオブジェクトを共有するには、Ruby の制約上:
- Frozen であること
- その内部に参照しているオブジェクトも含めて、全体として「shareable」であること
が必要です。
- そのため、Ractor 内で
model.attribute_types["some_column"]のように型情報へアクセスしようとすると、Ractor の分離制約によりエラーが発生していました。
対応方針
attribute_typesのハッシュを単にfreezeするだけでは不十分- 中身の Type オブジェクトや、そのさらに内部でメモ化されるオブジェクトも含めて「deep freeze」する必要があります。
Active Model / Active Record のほとんどの型クラスはそのまま凍結可能
- 追加のコード変更なしに凍結できない一部のケース(特に暗号化周り)のみを修正しています。
暗号化 (
ActiveRecord::Encryption) 周りのメモ化修正- 暗号化関連のタイプ・スキーム・キー提供クラスは、lazy メモ化の過程で
nilやfalseなどの「falsy」値をキャッシュする実装になっていた箇所がありました。
- Ruby でよくある「
@memoized ||= expensive_computation」パターンは、expensive_computationがfalse/nilの場合に毎回実行され続けるため、- 「メモ化が完了した安定したオブジェクトグラフ」という状態になりにくく、
- Ractor で共有するために完全に凍結しにくい要因になっていました。
- この PR では、そういった箇所を「早期 return 型のメモ化」に書き換えています。イメージとしては以下のような変更です:
ruby# 変更前(例) def encryption_key @encryption_key ||= compute_key # falsy を返しうる end # 変更後(例) def encryption_key return @encryption_key if defined?(@encryption_key) @encryption_key = compute_key endこれにより、
compute_keyがfalseやnilを返したとしても、一度計算した結果がきちんとインスタンス変数に保持され、再評価が発生しません。
その結果、オブジェクトグラフが安定し、deep freeze 可能になります。- 暗号化関連のタイプ・スキーム・キー提供クラスは、lazy メモ化の過程で
主なファイルごとの変更ポイント
activemodel/lib/active_model/attribute_registration.rbattribute_typesのハッシュおよびその内部の Type オブジェクトを凍結するロジックを追加。- 結果として、
attribute_typesが Ractor 間で共有可能なオブジェクトになります。
activemodel/test/cases/attribute_registration_test.rb- 上記の凍結・Ractor セーフ化が想定通り動作することを検証するテストを追加。
- 具体的には
attribute_typesが frozen であること- 深い部分の Type オブジェクトも shareable であること などを確認していると考えられます(テストコードから Ractor での利用パターンもカバーしているはずです)。
activerecord/lib/active_record/encryption/encrypted_attribute_type.rbactiverecord/lib/active_record/encryption/key_provider.rbactiverecord/lib/active_record/encryption/scheme.rb- これらの暗号化関連クラスで使われているメモ化ロジックを修正。
- 主な内容:
- falsy を返しうるメソッドで
@var ||= ...を使わないようにし、defined?(@var)を使った early return 方式に変更。 - これにより、暗号化 Type / Scheme / Key Provider のオブジェクトグラフが一度確定して以降は変化せず、凍結可能になる。
- falsy を返しうるメソッドで
- 影響範囲・注意点
Ractor を使っていないアプリへの影響
- ほとんどの場合、動作の変化はありません。
- ただし暗号化関連のメモ化ロジックが若干変わるため、
- 「falsy が返ること」を前提に、毎回計算されることに依存していたような特殊なコードがあれば挙動差が出る可能性はあります(通常の利用では考えにくいパターンです)。
Ractor を使っている / 使う予定のアプリへのメリット
- Ractor 内で
Model.attribute_typesや個々の属性型オブジェクトへアクセスしても Isolation エラーにならなくなります。 - Ractor ベースの並列処理で ActiveRecord / ActiveModel を利用する場合の安全性が向上します。
- Ractor 内で
パフォーマンスへの影響
- 型情報・暗号化関連オブジェクトのメモ化を「一度きり確定」させて凍結するため、実行時のオーバーヘッドはむしろ安定方向に働くと考えられます。
- 凍結や eager なメモ化に伴うごく小さなコストはありますが、通常のアプリで問題になるレベルではないはずです。
カスタム Type・暗号化拡張を書いている場合の注意
- 自前で
ActiveModel::TypeやActiveRecord::Encryptionを拡張している場合:- 内部状態が実行中に変化し続けるような設計だと、将来的に Ractor との相性問題が起きる可能性があります。
- この PR の方針(「Type オブジェクトは、生成されたら基本的に不変・凍結されうる」)に沿っているかを確認した方が安全です。
- 自前で
- 参考情報 (あれば)
- Ruby Ractor 仕様(shareable オブジェクトの制約):
- オブジェクトを Ractor 間で共有するには、
Ractor.shareable?がtrueになる必要があり、そのためには frozen かつ内部構造も含めて共有安全である必要があります。
- オブジェクトを Ractor 間で共有するには、
- この PR の意図:
- Active Model / Active Record の「型システム」を Ractor 時代に対応させるための基盤整備的な変更であり、API 仕様追加というよりは、「既存 API を Ractor 互換にする」ための内部実装改善です。
#58364 Honor composite / aliased FKs in belongs_to counter cache, autosave, and has_many :through nullify
マージ日: 2026/8/4 | 作成者: @kamipo
- 概要 (1-2文で)
belongs_toにおけるカウンタキャッシュ、autosave、has_many :throughの:nullify削除処理で、複合外部キー(Array) と エイリアス付き外部キー(仮想属性/別名カラム) が正しく扱われるように修正した PR です。これにより、これらの機能が複合 FK / エイリアス FK 環境でもサイレントに無視されたり、間違った SQL を生成したりしなくなります。
- 変更内容の詳細
背景となる問題点
reflection.foreign_key は、以下のようなケースで問題を起こしていました:
複合 FK:
[:foo_id, :bar_id]のような配列になる
→ そのままto_sされて"[:foo_id, :bar_id]"のような文字列になり、- WHERE 条件
- UPDATE の
SET句
などで不正な SQL を生成していた
エイリアス FK: 仮想属性 ⇔ 実カラム間でトラッキングに使うキーがズレる
→ 属性変更検知 (will_save_change_to_attribute?) が効かず、
autosave が動かない / 関連更新がスキップされる 等
この PR では、それぞれの箇所で 複合 FK / エイリアス FK を前提にした処理に書き換えています。
2-1. BelongsToAssociation のカウンタキャッシュ
対象: activerecord/lib/active_record/associations/belongs_to_association.rb
問題
belongs_to :author, counter_cache: true のような関連で、
対象側に複合 / エイリアス FK がある場合、内部のカウンタキャッシュ更新処理が:
reflection.foreign_keyをそのまま 1 カラム前提で使用- 複合 FK の場合、
["col1", "col2"]のような文字列として扱われ、条件の組み立てに失敗 - 実質的に no-op (カウンタが更新されない) になっていた
修正内容
複合 FK に対応するために:
- 各 FK ごとにループして処理するように変更
- WHERE 句の構築に
ActiveRecord::Key#where_hashを使用し、col1=.. AND col2=..のような複数カラム条件を正しく生成
エイリアス FK に対応するために:
- 初期化時に解決した
@foreign_keyを使うことで、
「仮想属性名」と「実カラム名」のズレを吸収
- 初期化時に解決した
イメージとしては、従来:
# 擬似コード
where(foreign_key => previous_value) # foreign_key が配列でも to_s されてしまうだったものが、複合キー前提で:
# 擬似コード (イメージ)
keys = Array(reflection.foreign_key) # => [:foo_id, :bar_id]
where_hash = ActiveRecord::Key.new(owner, keys).where_hash(previous_values)
# => { foo_id: 1, bar_id: 2 }
scope.where(where_hash) # 正しい複合 WHERE が組まれるのように動くようになった、という形です。
2-2. AutosaveAssociation の _record_changed?
対象: activerecord/lib/active_record/autosave_association.rb
問題
accepts_nested_attributes_for や関連オブジェクトの autosave 判定で使われる_record_changed? 内で:
reflection.foreign_keyを 1 つの属性名である前提でrecord.will_save_change_to_attribute?(foreign_key)を呼んでいた- 複合 FK の場合:
"[:foo_id, :bar_id]"という存在しない属性名を見てしまい、will_save_change_to_attribute?が常にfalseを返す → 変更なしと判定される - エイリアス FK の場合: 「仮想属性名」と「実カラム名」が食い違い、
本当は変更されているのに検知されない
→ 結果として、 関連レコードに対する autosave が行われない / 実行されない ケースが発生。
修正内容
複合 FK に対応:
- FK を配列として扱い、各キーごとに
will_save_change_to_attribute?を呼ぶ - どれか 1 つでも変更されていれば「変更あり」と判定
- FK を配列として扱い、各キーごとに
エイリアスに対応:
- ループ内で、実際にトラッキングされている属性名に解決したうえで
will_save_change_to_attribute?を呼ぶ (alias 解決をインラインで行う)
- ループ内で、実際にトラッキングされている属性名に解決したうえで
擬似イメージ:
def _record_changed?(record, reflection)
Array(reflection.foreign_key).any? do |fk|
real_attr = resolve_alias(record, fk) # 仮想属性 → 実フィールド名
record.will_save_change_to_attribute?(real_attr)
end
endこれにより、複合キー / エイリアスを利用した関連でも、
「FK が変わったなら関連を保存する」という autosave の期待動作が守られます。
2-3. HasManyThroughAssociation の :nullify 削除処理
対象: activerecord/lib/active_record/associations/has_many_through_association.rb
問題
has_many :through で関連を削除する際に :dependent => :nullify を指定しているとき、
中間テーブルの外部キーを NULL にするための UPDATE を組み立てる部分で:
reflection.foreign_keyをそのまま 1 カラムとしてSET句に使用- 複合 FK の場合:
UPDATE ... SET "[\"col1\", \"col2\"]" = NULLのような不正な SQL が生成 - エイリアス FK の場合:
"aliased"のように仮想属性名で SET しようとしてしまい、実カラムとずれる
修正内容
複合 FK の展開:
- 複合 FK を「カラム => NULL」の Hash に展開して渡すように変更
- 例:
[:foo_id, :bar_id]→{ foo_id: nil, bar_id: nil } - ActiveRecord/ Arel がこれを使って
SET "foo_id" = NULL, "bar_id" = NULLを生成できるようにする
エイリアス解決:
Arel::Table#[]を介してキーを解決することで、
各スカラーキーごとにエイリアスを正しく解決し、適切な実カラムに NULL をセット
擬似イメージ:
# 以前 (問題あり)
relation.update_all(reflection.foreign_key => nil)
# 以後 (修正後のイメージ)
attrs = Array(reflection.foreign_key).index_with { nil } # { foo_id: nil, bar_id: nil }
relation.update_all(attrs) # Arel::Table#[] が alias を解決これにより、has_many :through, dependent: :nullify が
複合キー・エイリアスキーを使う中間テーブルでも正しく動作します。
2-4. テストの追加
以下のテストが追加され、挙動が担保されています:
activerecord/test/cases/counter_cache_test.rb(+34)- belongs_to のカウンタキャッシュが複合 FK / エイリアス FK で正しく更新されるか
activerecord/test/cases/autosave_association_test.rb(+16)- autosave の
_record_changed?が複合 / エイリアス FK の変更を検知できるか
- autosave の
activerecord/test/cases/associations/has_many_through_associations_test.rb(+14)- has_many :through + dependent: :nullify で、複合 / エイリアス FK が正しく NULL になるか
- 影響範囲・注意点
影響を受けるのは、以下を満たすプロジェクトです:
- ActiveRecord で
- 複合外部キー (Array で指定される FK)
- もしくは エイリアス付き外部キー (attribute と実カラムが異なる) を使っている
- かつ次のいずれかを利用している:
belongs_to ... , counter_cache: true- autosave (
accepts_nested_attributes_forやautosave: trueなど) has_many :through, dependent: :nullifyまたはthrough先に対して:nullify削除
- ActiveRecord で
これまでの挙動との違い:
- これまでは「サイレントに効いていなかった」「不正な SQL を生成していた」ため、
実運用では- カウンタが更新されていない
- 関連が自動保存されない
:nullifyがエラーで落ちる / うまく nullify されない といった不具合が発生していた可能性があります。
- 本 PR 取り込み後は「本来の正しい挙動」をするようになるため、
- 今まで動かなかったロジックが動くようになる
- 一見すると「挙動が変わった」ように見えるかもしれませんが、 仕様としては修正前がバグであり、これが正しい挙動です。
- これまでは「サイレントに効いていなかった」「不正な SQL を生成していた」ため、
マイグレーション/設定面での注意:
- もしアプリ側で
reflection.foreign_keyを直接文字列前提で扱っているコードがある場合、
Rails 本体と同様に「配列を許容する」ように修正した方がよいです- 例:
Array(reflection.foreign_key)で正規化してループ - エイリアスを使う場合は、
arel_table[attribute]経由で解決するなど
- 例:
- もしアプリ側で
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/58364
- 関連概念:
ActiveRecord::Reflection#foreign_keyActiveRecord::Key#where_hashArel::Table#[]- autosave と
will_save_change_to_attribute?の関係
- 類似の問題に対する過去の修正(PR)でも、
「reflection.foreign_keyが配列になり得る」「エイリアス解決が必要」というパターンは共通しているため、
自前でメタプログラミング的に reflection を使うコードでは同様の対処が推奨されます。
#58354 Resolve attribute aliases in belongs_to change tracking
マージ日: 2026/8/3 | 作成者: @55728
- 概要 (1-2文で)
belongs_toの外部キーにalias_attributeを定義している場合に、変更検知(Dirty API)がエイリアス名ではなく実カラム名でしか動かず、一部機能が壊れていた問題を修正した PR です。belongs_toが Dirty API にアクセスする箇所で、常に「エイリアスを実カラム名に解決してから」参照するように統一されています。
- 変更内容の詳細
背景となる問題
Rails では:
class Comment < ApplicationRecord
belongs_to :post, counter_cache: true
alias_attribute :topic_id, :post_id
endのように、belongs_to の外部キー(ここでは post_id)に対して alias_attribute :topic_id, :post_id を定義すると:
comment.topic_id/comment.topic_id = ...は正常に動く- しかし Dirty API(
saved_change_to_attribute?,attribute_before_last_save,will_save_change_to_attribute?など)は内部的に「実カラム名(post_id)」で状態を持っている
そのため:
belongs_toの実装が Dirty API を「エイリアス名(topic_id)」で問い合わせると、「変更なし」と判定される- 結果として:
counter_cacheが壊れる(親のカウンタが増減しない or destroy 時に負の方向にズレる)touch: trueで古い親レコードが touch されないbelongs_to_required_validates_foreign_key = falseの設定下で、外部キーの必須性を Dirty ベースで判断する処理が誤動作し、ダングリング FK が検知されない
というバグが発生していました。
この PR の方針
「読み書き時と同様に、Dirty API を使うときもエイリアスを解決して実カラム名で扱う」ようにし、belongs_to 周りの全てのロジックが「見かけの属性名」ではなく「実際のカラム名」に統一してアクセスするように変更されています。
要点:
belongs_toが Dirty API(*_changed?,*_before_last_save,*_previously_changed?など)を呼ぶ全箇所で、- 渡す属性名を「association が紐付いている外部キーの実カラム名」に変換
- ポリモーフィック関連用の
BelongsToPolymorphicAssociationでも同様の修正 touch_later(非同期 touch 実装)でbelongs_to親を touch するときにも同じルールで Dirty API を参照- テストを追加して、「エイリアス経由で外部キーを更新しても、
counter_cache,touch: true, 必須バリデーションなどが期待通り動く」ことを保証
イメージしやすいサンプルコード
PR の説明を踏まえた、典型的な状況:
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
# 実カラムは post_id
belongs_to :post, counter_cache: true, touch: true
# アプリ内の都合で別名を付けている
alias_attribute :topic_id, :post_id
end
comment = Comment.create!(topic_id: post1.id) # alias 経由で作成
# 別の親を指すように alias 経由で変更
comment.topic_id = post2.id
comment.save!修正前:
post1.comments_countがデクリメントされない or destroy などをきっかけにマイナス方向へズレるpost1.updated_atが更新されない (touch: trueが古い親に効かない)- required な
belongs_to周りのバリデーションも、エイリアス経由だと誤判定されるケースがある
修正後:
counter_cacheがpost1からpost2へ正しく移動- 両方の親(特に旧親)に対して
touchが適切に動作 belongs_to_required_validates_foreign_key = falseの特殊モードでも、Dirty を使った FK の整合性チェックがエイリアスでも正しく機能
実装的には、belongs_to の内部で:
- 「この関連が使う外部キー(
foreign_key)」→「実際に Dirty API に渡すキー名」 - という変換ロジック(エイリアス解決)を導入し、それを Dirty 参照箇所で一貫して使用するようになっています。
- 影響範囲・注意点
- 影響を受けるのは「
belongs_to外部キーにalias_attributeを付けている」ケースのみ- 通常の
belongs_toには挙動の変化はほぼない(バグ修正のための内部挙動の整合性改善)
- 通常の
- 既にこのバグを前提に「ワークアラウンド」を書いていた場合(例: 独自でカウンタ調整や touch をしている)、二重で動く可能性があるので、マージされたバージョンに上げたときは以下を確認するとよいです:
counter_cacheが二重加算・二重減算されていないかtouchを手動で呼んでいないか
belongs_to_required_validates_foreign_key = falseを使っている環境では、エイリアス付き外部キーでもバリデーション挙動が変わる可能性があるので、フォーム送信〜保存周りの動きを一度確認するのが安全です。
- 参考情報 (あれば)
- この PR の前提となる修正: #58348
- 関連する Rails 機能:
alias_attributebelongs_to/counter_cache/touch: true- Dirty API (
saved_change_to_attribute?,attribute_before_last_saveなど) belongs_to_required_validates_foreign_key設定
- エイリアス付き外部キーを使っているプロジェクトでは、この PR が含まれる Rails バージョンにアップグレードした際に、
belongs_to関連の挙動テスト(特にカウンタ・touch・バリデーション)を追加しておくと安心です。
#58356 [ci skip] Fix the table name in the Wishlists guide
マージ日: 2026/8/3 | 作成者: @duffuniverse
概要 (1-2文で)
Rails公式ガイドの「Wishlists」ガイド内で使われているテーブル名の誤記を、正しいテーブル名に修正したドキュメント向けのPRです。コードではなくガイド文書のみの変更で、挙動には影響しません。変更内容の詳細
- 変更ファイル:
guides/source/wishlists.md - 変更内容は1行のみで、「Wishlists」チュートリアル中に登場するテーブル名を、実際のモデル/マイグレーションで使われるべき正しいテーブル名に合わせる修正です。
- タイトルに
[ci skip]が付いていることからも分かる通り、テストやビルドを走らせる必要のないドキュメントのみの修正です。
(実際の diff は1行の +1/-1 で、例えば以下のようなニュアンスの修正が行われています):
- In our migration, we created the table `wishlist`:
+ In our migration, we created the table `wishlists`:あるいは:
- The products_wishlist table stores...
+ The products_wishlists table stores...といった形で、Railsの命名規約(モデル: 単数形 / テーブル: 複数形)に沿ったテーブル名に直されたものと考えられます。
- 影響範囲・注意点
- 影響範囲はRailsガイドの読者のみで、アプリケーションコードやRails本体の挙動には一切影響しません。
- ただし、旧バージョンのガイドを参考にしてテーブル名を誤って単数形や不適切な名前で作っていた場合、以下のような不整合が発生している可能性があります:
- モデル
Wishlistとテーブルwishlistのような単数形テーブル名の不一致 - 結果として ActiveRecord がテーブルを見つけられず、
ActiveRecord::StatementInvalidや「relation does not exist」エラーになる
- モデル
- これから「Wishlists」ガイドを参考に実装する場合は、このPR適用後のガイドを参照し、Railsの命名規約に沿ったテーブル名(複数形)で作成するのが安全です。
- 参考情報 (あれば)
- Railsのモデルとテーブルの命名規約:
- モデル: 単数形・CamelCase(例:
WishlistItem) - テーブル: 複数形・snake_case(例:
wishlist_items)
- モデル: 単数形・CamelCase(例:
- 関連ガイド:
- Rails Guides – Active Record Basics / Naming Conventions
- Rails Guides – Wishlists(本PRで修正されたガイド)
#58353 Fix Single Table Inheritance doc example for the type column change
マージ日: 2026/8/3 | 作成者: @55728
- 概要 (1-2文で)
Single Table Inheritance(STI) のドキュメント中にあるtypeカラムの変更例が実際の挙動と食い違っていたため、「空文字列""からの変更」となっていた説明を「nilからの変更」に修正した PR です。ドキュメントのみの変更で、挙動・API には一切の変更はありません。
- 変更内容の詳細
何を修正したか
ActiveRecord の STI 概要ドキュメント(activerecord/lib/active_record/inheritance.rb のコメント部分)にある、「type カラムがどのようにセットされるか」の例が以下のようになっていたと考えられます(旧記述イメージ):
Firm.new.changes # => {"type"=>["", "Firm"]}しかし、実際には ensure_proper_type による type の自動セットは「空文字列」ではなく「nil」からの変更として扱われます。つまり、正しい挙動は:
Firm.new.changes # => {"type"=>[nil, "Firm"]}この PR は、その「旧値」を表すドキュメント上の値を "" から nil に修正しています。
Ruby バージョンと Hash#inspect 表記の話
PR 説明中にある:
The pre-3.4
Hash#inspectstyle is kept to match the surrounding examples and the gem's minimum Ruby (≥ 3.3.1).
という一文は、Rails のドキュメント中でハッシュの出力例を Ruby 3.4 以前の Hash#inspect のスタイルに合わせている、という意味です。
現在の Rails の最低サポートは Ruby 3.3.1 以上
そのため、ハッシュの表示スタイルは Ruby 3.4 以降で入るかもしれない新しいフォーマットではなく、従来の:
ruby{"type"=>[nil, "Firm"]}のような
=>スタイルを使い続けて、周辺の例と一貫性を保っている、という意図です。
- 影響範囲・注意点
実行コードの変更は一切なく、「コメント(ドキュメント)のみ」の修正です。
STI を利用しているアプリの挙動(
typeカラムのデフォルト値やchangesの内容)は、以前からnilを前提としており、そこには変更はありません。もし既存のアプリケーション側のテストやドキュメントで「
typeの旧値が""になる」ことを前提にして書いていた場合は、それが誤解に基づいている可能性があります:新規レコード生成時の
changes例は、以下のようにnilを期待するのが正しいです。rubyfirm = Firm.new firm.changes # => {"type"=>[nil, "Firm"]}
データベースレベルで
typeカラムにデフォルト値''(空文字)を入れているような特殊なスキーマを使っていない限り、今回のドキュメント修正が実アプリの挙動と食い違うことはありません。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58353
- 関連箇所:
ActiveRecord::Inheritanceまわりの STI ドキュメントコメント - STI の概要(公式ガイド)
changesの仕様(ActiveModel::Dirty)
#58348 Restore alias_attribute support in associations
マージ日: 2026/8/3 | 作成者: @kamipo
- 概要 (1-2文で)
このPRは、belongs_toなどの関連付けで長らく効かなくなっていたalias_attributeのサポートを復活させ、外部キーや主キーに定義したエイリアス経由でも関連が正しく読める/書けるように戻す変更です。過去にパフォーマンス目的で導入された_read_attribute経由の「高速パス」を廃止し、再びread_attribute/write_attributeを使うようにしています。
- 変更内容の詳細
何が問題だったか
もともと belongs_to などの関連は、所有側(owner)レコードから外部キーを読むときに ActiveRecord::Base#[] を使っていました。Base#[] は内部的に read_attribute を呼ぶため、alias_attribute が定義されていてもそれを解決してくれます。
しかし、過去のコミット be2b98b4ae でパフォーマンス改善のために _read_attribute(内部用の高速メソッド)を使うようになりました。_read_attribute は
- alias 解決をしない
- primary key の判定などもスキップする
ため、外部キーや主キーに対して定義した alias_attribute が、関連経由では一切効かなくなるという副作用がありました。
その結果、以下のようなコードは期待どおりには動作していませんでした:
class Post < ActiveRecord::Base
alias_attribute :writer_id, :author_id
belongs_to :author, foreign_key: :writer_id
end
post.author # 本来は writer_id(alias) 経由で author_id を読むはずだが、無視されていた一部の本番アプリではこの問題を避けるために、_read_attribute 自体をオーバーライドするという危険なワークアラウンドが行われていた、という背景があります(#57795 参照)。
なぜ今「高速パス」をやめてもよいのか
read_attribute / write_attribute 自体がここ数バージョンで最適化され、高速化されています。具体的には PR 説明に挙がっているとおり:
- #36052:
@primary_keyを ivar キャッシュして主キーアクセスを約 +20% 高速化 - 5575bd7b22: 重複していた
attribute_alias?の呼び出しを削除し、alias 解決を約 +40% 高速化 - 27a1ca2bfe:
_read_attributeへの委譲を inline して約 +15% 高速化
これらの結果、_read_attribute を直接使って alias / primary-key チェックを飛ばすメリットがほとんどなくなったため、正しさ(alias を尊重する)を優先して元に戻す、という判断になっています。
実際の変更点(概念的な内容)
コード上の変更は主に次のようなパターンです:
- 各種 association 実装で
_read_attribute/_write_attributeを使っていた箇所をread_attribute/write_attribute(あるいはself[attribute]的なパス)に戻す belongs_to/has_one/has_many/through/ preload 周りなど、外部キー・主キーを読む/書く箇所を一貫して alias-aware な API に戻す- これに合わせてテストを追加し、
alias_attributeを外部キー側・主キー側に定義したケースで関連が正しく動くことを検証
おおまかなイメージ:
# 以前(問題があった状態)
def owner_foreign_key
owner._read_attribute(reflection.foreign_key)
end
# 今回のPR後
def owner_foreign_key
owner.read_attribute(reflection.foreign_key)
# または owner[reflection.foreign_key]
end同様の差し替えが、association.rb・belongs_to_association.rb・has_many_*・preloader など関連処理一式に広く入っています。
動作例
PR 説明にある例:
class Post < ActiveRecord::Base
alias_attribute :writer_id, :author_id # alias 定義
belongs_to :author, foreign_key: :writer_id
end
post.author # Post#writer_id (=> author_id の alias) 経由で外部キーを読む
author.posts.create!(...) # 生成時に writer_id(alias) を経由して author_id に書き込むこのように、関連宣言で foreign_key に alias 側の名前を指定しても、正しく「元の属性」に解決されて関連が機能するようになります。
また、ターゲット側の primary key に alias_attribute を定義しているケースでも同様に解決されるようになります。
- 影響範囲・注意点
影響範囲:
- Active Record の全ての主要な関連(
belongs_to/has_one/has_many/has_many :through/ ポリモーフィック / preloader / autosave 含む)の外部キー・主キーの読取/書込処理 - これまで「
alias_attributeが関連で効かない」と思っていた部分が、Rails のアップデートにより 突然期待どおりに(あるいは、以前とは違う挙動で)動き始める 可能性があります
- Active Record の全ての主要な関連(
互換性・注意点:
alias_attributeを外部キーや主キーに対して定義しており、その上で関連定義に別名(alias)を使っていたアプリは、これまで「無視されていた alias」が有効になり、関連の挙動が変わる場合があります- 過去のワークアラウンドとして
_read_attribute/_write_attributeを独自にオーバーライドしていたアプリでは、今回の変更と二重に alias 解決をしてしまう・パフォーマンスが悪化する・想定外の副作用が出るなどのリスクがあります- Rails をアップグレードする際に、それらの monkey patch / 上書きは可能な限り外すことが推奨されます
- パフォーマンスに関して:
read_attribute/write_attributeは最適化されているため、多くのアプリでは体感できる劣化はほとんどないはずですが、大量関連アクセスを行うホットパスを持つアプリでは、アップグレード後に一度プロファイルを取ると安心です
テスト追加内容から読み取れること:
belongs_toとhas_manyについて、alias された外部キー/主キーを使った関連の read / write が明示的にテストされています- 作者モデル(
test/models/author.rb)にも alias を絡めたテスト用定義が追加されており、実際のユースケースが想定されています
- 参考情報 (あれば)
- 当該PR:
- Restore
alias_attributesupport in associations (#58348)
- Restore
- 関連する背景Issue/PR:
- #57795 — 本番で
_read_attributeを上書きしてまで alias を効かせていた、という背景が言及されている - 過去の最適化:
- #36052 — primary key アクセスの高速化(
@primary_keyキャッシュ) - commit 5575bd7b22 —
attribute_alias?呼び出し削減による alias 解決の高速化 - commit 27a1ca2bfe —
_read_attributeへの delegating の inline 化
- #36052 — primary key アクセスの高速化(
- #57795 — 本番で
このPRにより、「関連周りでも alias_attribute は素直に効く」という、本来期待される挙動に戻ったと考えてよく、今後はワークアラウンドではなく素の API に依存できるようになります。
#58347 Deprecate write_attribute(:id, value) writing to the primary key
マージ日: 2026/8/3 | 作成者: @kamipo
- 概要 (1-2文で)
write_attribute(:id, value)がカスタム主キー(@primary_key)を書き換える挙動を非推奨にし、今後は:idというカラムに対する通常の書き込みとして扱うようにする変更です。read_attribute(:id)の非推奨・削除 (#49019) に続く「書き込み側」の整合性対応であり、複合主キーの場合も読み取り側と同じ扱いに揃えています。
- 変更内容の詳細
背景
- 以前の PR (#49019) で、
read_attribute(:id)が「カスタム主キーの値」を返す挙動は非推奨 → のちに削除されました。- 替わりに
read_attribute(:id)は「本当に id というカラムがあればその値」を返す、というより素直な仕様に変わっています。
- しかし書き込み側の
write_attribute(:id, value)はまだ「idを@primary_keyに変換して書き込む」という旧仕様のままでした。 - この PR はその「片側だけ残っていた」挙動を整理し、読み書きの仕様を揃えるものです。
具体的な変更点
1) write_attribute(:id, value) の扱いの変更・非推奨化
以前(問題となっている挙動):
class User < ApplicationRecord
self.primary_key = :uuid
end
user = User.new
user.write_attribute(:id, "abc123")
# これが内部的に:
# write_attribute(@primary_key, "abc123") # => write_attribute(:uuid, "abc123")
# のように変換され、主キー(:uuid)へ書き込まれていたこの PR 以降の方針:
write_attribute(:id, value)が「カスタム主キー(@primary_key)」を指すのは非推奨。- deprecation warning を出したうえで、将来的には「
idカラムへの単純な書き込み」としてのみ扱う方向に揃える。
コード上では、write_attribute の内部で以下のようなロジック変更が入っています(イメージ):
def write_attribute(attr_name, value)
name = attr_name.to_s
if name == "id" && primary_key != "id"
# ここで deprecation warning:
ActiveSupport::Deprecation.warn(
"write_attribute(:id, ...) によるカスタム主キー(#{primary_key})への書き込みは非推奨です。 " \
"代わりに write_attribute(:#{primary_key}, ...) あるいは record.#{primary_key} = ... を使用してください。"
)
# 将来はここで primary_key へのマッピングをやめる方針
# 現時点では既存アプリが壊れないよう、まだ primary_key に書き込むが、
# 次のメジャーで挙動を変える、という位置づけの deprecation
end
# 通常の属性書き込み処理へ
# ...
end※実際のメッセージ文言や制御フローは PR 内の実コードに従いますが、意図としては上記のような deprecation です。
2) 複合主キー(composite primary keys)に対する扱い
- Rails 本体には複合主キーのファーストクラスサポートはありませんが、内部的には一部コードやテストで「配列の primary key」などを扱う場面があります。
- 読み取り側ではすでに:
read_attribute(:id)は「idカラム」にアクセスするだけ- 複合主キーの「代表キー」として特別扱いしない
- この PR で書き込み側もそれに揃えられます:
write_attribute(:id, value)も「複合主キーの何か」を指すのではなく、単にidカラムに書くだけとする方向に統一。- テスト(
primary_keys_test.rb)で複合主キーのケースが追加され、「read と write の一貫した挙動」が確認されています。
3) CHANGELOG の追記
activerecord/CHANGELOG.md に以下のような趣旨のエントリが追加されています:
write_attribute(:id, value)がカスタム主キーに書き込む挙動を非推奨にしたことread_attribute(:id)の変更 (#49019) に続く変更であること
- 影響範囲・注意点
影響を受けるケース
- カスタム主キーを使っているモデル で、
- かつ
write_attribute(:id, ...)を使って主キーを書き換えている コードがある場合
例:
class Account < ApplicationRecord
self.primary_key = :account_uuid
end
account = Account.new
account.write_attribute(:id, "foo") # ← ここが非推奨このようなコードは:
- すでに deprecation warning を出し始める(か、出すように予約される)
- 将来的な Rails のメジャーバージョンアップ時に、挙動が変わり「
account_uuidではなくidカラムに書き込まれる」ようになるリスクがあります。
推奨される書き方
カスタム主キーに値を書きたい場合は、明示的に主キー名を指定してください。
# 推奨:
account.write_attribute(:account_uuid, "foo")
# もしくは:
account.account_uuid = "foo"write_attribute(:id, ...) を「主キーエイリアス」として使う前提のコードは、deprecation 期間中にすべて置き換えるべきです。
複合主キーを擬似的に使っている場合
- 自前実装や gem(composite_primary_keys など)で複合主キーを扱っているアプリで、
write_attribute(:id, ...)を「複合主キーの代表キー」扱いしているようなコードがあると、- 将来のバージョンで意味が変わる(単なる
idカラム更新になる)可能性があります。
- ここでも同様に「本当に書きたいカラム名・主キー名」を明示的に使うようリファクタリングしてください。
- 参考情報 (あれば)
- 該当 PR:
- Deprecate
write_attribute(:id, value)writing to the primary key — #58347
- Deprecate
- 関連 PR(読み取り側の変更):
- Deprecate and remove
read_attribute(:id)returning the custom primary key value — #49019
- Deprecate and remove
- 修正ファイル:
activerecord/lib/active_record/attribute_methods/write.rbactiverecord/test/cases/primary_keys_test.rbactiverecord/CHANGELOG.md
#58349 Fix test class name collision in Active Storage railties tests
マージ日: 2026/8/2 | 作成者: @flavorjones
- 概要 (1-2文で)
Active Storage の railties テストで、2つのテストファイルが同じテストクラス名ApplicationTests::ActiveStorageEngineTestを定義していたために発生していたクラス名衝突を解消する PR です。クラス名をファイルごとに固有の名前に分けることで、ローカル実行時に発生していた環境変数リーク検出テストの失敗を防いでいます。
- 変更内容の詳細
問題の状況
以下の2ファイルが、どちらも同じテストクラス名を定義していました。
railties/test/application/active_storage/analyzers_integration_test.rbrailties/test/application/active_storage/engine_integration_test.rb
両方とも ApplicationTests::ActiveStorageEngineTest を定義していたため、1プロセス内で両ファイルを読み込んだ場合にクラス定義が「上書き」されます。その結果、setup / teardown で行っている処理が意図した通りに実行されず、環境変数 BUNDLE_GEMFILE が元に戻らないなどの「環境リーク」が発生していました。
テストの失敗メッセージ例:
Environment leak detected:
- Changed variables:
- BUNDLE_GEMFILE from "<rails>/Gemfile" to "<rails>/tmp/d20260802-463299-f70uer/app/Gemfile"CI では rake test のデフォルトタスクが test:isolated であり、各テストファイルが別プロセスで実行されるため、このクラス名衝突は表面化していませんでした。
一方、ローカルでは bin/test railties/test/application/active_storage のようにディレクトリ指定で実行すると、複数ファイルが同一プロセス内に読み込まれ、クラス名衝突による問題が発生します。
実際の修正内容
修正は1行のみで、analyzers_integration_test.rb 側のクラス名を変更しています。
(イメージとしては以下のような変更)
# 変更前: analyzers_integration_test.rb
class ApplicationTests::ActiveStorageEngineTest < ActiveSupport::TestCase
# ...
end
# 変更後: analyzers_integration_test.rb
class ApplicationTests::AnalyzersIntegrationTest < ActiveSupport::TestCase
# ...
endこれにより:
analyzers_integration_test.rb→ApplicationTests::AnalyzersIntegrationTestengine_integration_test.rb→ApplicationTests::ActiveStorageEngineTest(従来のまま)
と、ファイルごとに異なるテストクラス名が割り当てられるようになりました。
また、新しいクラス名 AnalyzersIntegrationTest は同ディレクトリ内の他テストと命名規則を揃えています:
DirectUploadsIntegrationTestUploadsIntegrationTestValidatingServiceTest
ファイル名とクラス名を揃える Rails の慣習にも合致します。
- 影響範囲・注意点
- 対象:
railties/test/application/active_storage/*のテストのみ
アプリケーションコードや公開 API には影響しません。 - 影響内容:
- ローカル環境で
bin/testを使ってrailtiesの Active Storage 関連テストをまとめて実行した際の「環境リーク検出によるテスト失敗」が解消されます。 - CI はもともと
test:isolatedによりファイルごとにプロセスを分けていたため、挙動は変わりません。
- ローカル環境で
- 注意点:
- テストクラス名を直接参照しているメタテストやツール(例えば特定クラスだけ実行するようなカスタムツール)を使っている場合は、新しいクラス名
ApplicationTests::AnalyzersIntegrationTestに合わせて修正が必要になる可能性があります。 - ただし、通常の
bin/test/rake testの利用では影響はありません。
- テストクラス名を直接参照しているメタテストやツール(例えば特定クラスだけ実行するようなカスタムツール)を使っている場合は、新しいクラス名
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58349
- 当該2ファイルが同時に同じクラス名で追加されたコミット:
https://github.com/rails/rails/commit/367445d037b8f0d0baea684d14c0343ee29e8da1 railtiesのテスト実行方法:- CI / デフォルト:
rake test:isolated(ファイルごとに別プロセス) - ローカルでの一括実行例:
bin/test railties/test/application/active_storage(1プロセスで複数ファイルをロードするため、今回のようなクラス名衝突が顕在化しうる)
- CI / デフォルト:
#58254 Make CommandRecorder#record and #inverse_of private
マージ日: 2026/8/2 | 作成者: @kamipo
- 概要 (1-2文で)
ActiveRecord のActiveRecord::Migration::CommandRecorderクラスにおいて、内部用メソッドである#recordと#inverse_ofを public から private に変更した PR です。将来的なキーワード引数の扱い変更(Hash.ruby2_keywords_hash廃止への対応)に伴ってメソッドシグネチャを壊さざるを得ないため、それを「公開 API 変更」扱いにしないよう、先に「内部 API」であることを明確化しています。
- 変更内容の詳細
背景
- Ruby 3 系でのキーワード引数仕様変更に伴い、Rails では
ruby2_keywordsの利用を段階的に削除しています (#58238 など)。 - ただし、「後で呼び出すために引数を保存しておく」系の処理(この
CommandRecorderや ActiveJob など)は、まだHash.ruby2_keywords_hashに依存しており、- 位置引数 + 最後尾に「フラグ付き Hash」として kwargs が折り畳まれて保存される という Ruby 2 系の互換的な振る舞いに頼っていました。
Hash.ruby2_keywords_hash自体も今後非推奨になる予定のため、- 「位置引数」と「キーワード引数」を別々に保持する構造 に移行する必要があります。この構造変更によって、
CommandRecorder#record/#inverse_ofのシグネチャが変わる(あるいは実質的に互換でなくなる)ことが確定しています。
- 「位置引数」と「キーワード引数」を別々に保持する構造 に移行する必要があります。この構造変更によって、
なぜ record / inverse_of を private にするのか
CommandRecorderは、マイグレーションの実行コマンドを「記録するためのラッパ」であり、本来ユーザーが直接recordやinverse_ofを叩くことを想定していない内部コンポーネントです。- 想定される利用:
create_table,add_columnなど通常のマイグレーション DSL を呼ぶと、その裏側でCommandRecorderがそれらを「記録」し、ロールバック等で逆順に呼び出す。
- 想定される利用:
- ところが現状の API ドキュメント上では
#record/#inverse_ofが public メソッドとして掲載されており、「公式に公開されている API」のように見えてしまっていました。 - 今後の PR (#58239) でシグネチャが変わるのを、
- 「public API の破壊的変更」ではなく、
- 「内部 API の実装変更」 として扱うために、先に public → private にしておこう、というのがこの PR の主旨です。
実際のコード変更(概要)
※PR 本文から読み取れる範囲での要点のみ
command_recorder.rb:CommandRecorder#recordとCommandRecorder#inverse_ofをprivateセクションの中に移動。- それに伴い、これらのメソッドの呼び出し元は同じクラス/内部からのみになる(外部からの直接呼び出しは意図しない使い方と明示)。
- 併せて、キーワード引数の扱いについての実装が、今後の構造変更(位置引数とキーワード引数を分離して保存する)を前提にした形に寄せられている可能性があります。
- テスト (
command_recorder_test.rb):- 行数が大きく増えていることから、
CommandRecorderの「公開されるべき振る舞い」(マイグレーション DSL を通した呼び出し)をより網羅的にテストするように拡充。record/inverse_ofを直接呼ぶテストがあった場合は削除/書き換えしていると考えられます。
- 行数が大きく増えていることから、
CHANGELOG.md:- 「
CommandRecorder#recordと#inverse_ofを private にした」旨のエントリを追記し、位置づけを明示。
- 「
- 影響範囲・注意点
影響を受ける可能性があるコード
次のようなコードを書いている場合は、明確に壊れます(か、少なくとも将来のバージョンで壊れるリスクが高いです):
ruby# 例1: CommandRecorder を直接 new して使っている recorder = ActiveRecord::Migration::CommandRecorder.new(connection) recorder.record(:create_table, [:users, { id: :uuid }]) # 例2: 独自の逆操作ロジックで inverse_of を直接利用 command, args = recorder.inverse_of(:add_column, [:users, :name, :string])この PR により、
record/inverse_ofは private になるため、上記のような呼び出しは Ruby 側の可視性エラー(NoMethodError)となります。
実務的な注意点
- マイグレーション DSL(
create_tableなど)を通常どおり使っている限り、この変更による影響はほぼありません。 - 独自のマイグレーションクラスやツールで、
CommandRecorderを直接扱っている場合は、- そのコードは「内部 API に依存している」状態であり、
- かつ今後の引数の扱い変更(#58239 以降)でもさらに壊れる可能性が高い ため、設計を見直すべきです。
- ライブラリ作者で、
CommandRecorderを直接使っている場合は特に要注意で、代替方法(例えばマイグレーション DSL をラップする、または独自にコマンド記録の仕組みを持つなど)を検討してください。
- 参考情報 (あれば)
- この PR が依存している/前提としている関連 PR:
- #58238:
ruby2_keywordsの呼び出し削除 - #58239:
CommandRecorderなどでの引数保存構造を「位置引数とキーワード引数を分けて保持する」形式に変更する予定の PR
- #58238:
- ドキュメント:
- 公式 API ドキュメント上ではこれまで
CommandRecorder#record/#inverse_ofが public として掲載されていましたが、この PR により「本来は内部 API」という位置づけが明確になります。
https://api.rubyonrails.org/classes/ActiveRecord/Migration/CommandRecorder.html
- 公式 API ドキュメント上ではこれまで
#58344 Pin redis below 6 in the Gemfile
マージ日: 2026/8/2 | 作成者: @yahonda
- 概要 (1-2文で)
Rails の Gemfile でredisのバージョンを「6未満」に固定し、テストがredis-6.0.0のリリースによって落ちる問題を回避する PR です。Action Cable の redis サブスクリプションアダプタが要求するバージョン制約(>= 4, < 6)と実際にロードされる gem のバージョンの不整合を解消しています。
- 変更内容の詳細
背景
2026-07-31 に
redis 6.0.0がリリースされた。Rails の Action Cable の Redis アダプタは、ロード時に以下のような依存関係を宣言しています:
rubygem "redis", ">= 4", "< 6"しかし、Gemfile では
redisバージョンを特に 6 未満に固定しておらず、bundle installすると自動的にredis-6.0.0が入る状況になっていました。その結果、テスト実行時に以下のようなエラーが発生:
textGem::LoadError: Error loading the 'redis' Action Cable pubsub adapter. Missing a gem it depends on? can't activate redis (>= 4, < 6), already activated redis-6.0.0. Make sure all dependencies are added to Gemfile.つまり「
redisは 4 以上 6 未満が必要なのに、すでに 6.0.0 が有効化されているので条件を満たさない」という状態です。
この PR での実際の変更
変更ファイルは 2 つだけです。
GemfileGemfile.lock
差分としては、それぞれで redis のバージョン指定を「6 未満」にピン留めしています(+1/-1 行ずつ)。
イメージとしては、Gemfile が以下のようになる変更です(実際の PR と若干表記が異なる可能性はありますが、意味としてはこれ):
# 変更前(例)
gem "redis"
# 変更後(例)
gem "redis", "< 6"それに伴って Gemfile.lock の redis の行も、6.0.0 ではなく 5.x 系にロックされるよう変更されています。
なぜ上限を上げずに「6 未満」にしたのか
説明文にある通り、redis アダプタの上限を直接 >= 4, < 7 のように変えるのではなく、Gemfile 側で 6 未満に固定する方針が取られています。その理由は:
- Rails の main ブランチでは、Action Cable の Redis アダプタ実装がすでに
redis-clientベースに移行済み(コミットef812c2652)。 - この PR の対象ブランチ(おそらく安定版ブランチ)は、まだ従来の
redisgem ベースのアダプタを使っている。 - ここで安易に上限を引き上げると、main ブランチとの差分(実装・依存関係)が広がってしまい、メンテナンスが複雑になる。
- さらに、
redis 6への正式対応が行われておらず、動作保証もされていないため、テスト落ちの一時的な回避ではなく「未サポートバージョンを Gemfile 側で明示的に避ける」という解決策を選んでいる。
- 影響範囲・注意点
影響範囲
- Rails 開発環境および CI 環境で利用される
redisgem が「5.x 系(< 6)」に固定されます。 - それにより、Action Cable の Redis pubsub アダプタをロードするテスト・実行環境での
Gem::LoadErrorは解消されます。 - コード本体(Action Cable や他のコンポーネント)の実装には手を入れていないため、機能面の挙動は従来の
redis 5.xを使っていたときと変わりません。
注意点
redis 6.0.0以降の新機能や挙動変更には、このブランチではまだ対応していません。
アプリ側の Gemfile で Rails と一緒にredisを 6 以上に上げようとすると、同様のバージョン不整合に遭遇する可能性があります。- Rails をこのブランチ系で利用しているアプリケーションは、アプリ側の Gemfile でも
redis,redis-rails,redis-namespaceなどのバージョン制約に注意する必要があります。Rails 自体が< 6を要求している期間は、それに合わせる必要があります。 - main ブランチでは
redis-clientベースに移行しているため、将来的に Rails のバージョンを上げる際には、- 依存 gem として
redisではなくredis-clientが入る - 接続設定やオプションが若干変わる
可能性があります。この PR はその移行までの「つなぎ」の対応です。
- 依存 gem として
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58344
- CI の失敗ログ(Action Cable の Redis アダプタ読み込み失敗):
https://buildkite.com/rails/rails/builds/131861#019fbb44-6779-4ff8-af5f-102d919ad65d/L1764 >= 4, < 6制約を導入した過去 PR: https://github.com/rails/rails/pull/45856- main ブランチでの
redis-clientベース移行のコミット:ef812c2652
#58265 Consolidate MySQL ALGORITHM / LOCK into MySQL::AlterTable state
マージ日: 2026/8/2 | 作成者: @kamipo
- 概要 (1-2文で)
MySQL アダプタにおけるALTER TABLEのALGORITHM/LOCK指定を、各操作ごとのバラバラな扱いからMySQL::AlterTableオブジェクトの状態として一元管理するようにした PR です。これにより、単発・バルクいずれのスキーマ変更でも、正しい位置・回数でALGORITHM/LOCKが出力され、オプションが落ちる不具合も解消されます。
- 変更内容の詳細
2-1. これまでの問題点
PR 説明の通り、従来は以下の問題がありました。
各単発操作ごとの重複実装
add_column,change_column,rename_column,remove_columnなどの 単発 MySQL 操作ごと に、以下のようなパターンで SQL 末尾にALGORITHM/LOCKを付け足していました。rubysql = "ALTER TABLE `table` ADD COLUMN ..." sql << ", #{algorithm}" if algorithm sql << ", #{lock}" if lock execute(sql)同じロジックが複数箇所に分散しており、保守性が低い状態でした。
複数インデックスをまとめて追加/削除する場合の SQL が不正
MySQL::AddIndex/MySQL::DropIndexは自身がalgorithm/lockを持ち、visit_AddIndex/visit_DropIndexが各フラグメントごとにそれを出力していました。
そのため、ALTER TABLE ...の一文の中で複数インデックスを操作すると、例えば次のように 同じALGORITHM/LOCKが N 回繰り返される SQL になり、MySQL が拒否していました。sqlALTER TABLE `users` ADD INDEX `idx_1` (...), ALGORITHM = INPLACE, LOCK = NONE, ADD INDEX `idx_2` (...), ALGORITHM = INPLACE, LOCK = NONE;MySQL の文法上、
ALGORITHM/LOCKはALTER TABLE文全体に対するヒントであり、文末に 1 回だけ書くのが正しい形です。バルクモードでの
algorithm:/lock:オプションが無視されていたchange_tableブロック内でt.column,t.changeなどにalgorithm:/lock:オプションを渡しても、#{command}_for_alter経由の処理でそれらが抽出されておらず、SQL に反映されない(黙って落ちる) 状況でした。rubychange_table :users, bulk: true do |t| t.change :name, :string, algorithm: :inplace, lock: :none end # 従来は ALTER TABLE 文に ALGORITHM/LOCK が付かなかった
2-2. 新しい設計方針: AlterTable に集約
ALGORITHM / LOCK は ALTER TABLE 文全体に対するヒントなので、その状態を MySQL::AlterTable 自体に持たせるよう変更されました。
追加された状態・メソッド
MySQL::AlterTableに以下の状態を追加:algorithmlock
- それぞれの setter は、既存のフォーマッタである
index_algorithmlock_clauseを通して正しい SQL 断片に変換されるようになっています
(例::inplace→"ALGORITHM = INPLACE"など)。
visit_AlterTable のオーバーライド
MySQL::SchemaCreation#visit_AlterTable をオーバーライドして、ALTER TABLE 文の末尾に一度だけ ALGORITHM / LOCK を追加するように変更されました。
概念的には以下のような出力になります。
ALTER TABLE `users`
ADD COLUMN `age` int,
ADD INDEX `index_users_on_age` (`age`)
ALGORITHM = INPLACE
LOCK = NONE;これにより、単発・バルクに関わらず、ALGORITHM / LOCK は常に正しい位置に 1 回だけ出力されます。
2-3. AlterTable が操作オプションを吸い上げるように
MySQL::AlterTable が以下のメソッドをオーバーライドしています。
add_columnremove_columnchange_columnrename_columnadd_indexremove_index
これらは、渡されたオプションから :algorithm / :lock を抽出して AlterTable 自身の状態に反映するように変更されました。
そのため、
change_table ... bulk: trueの中- 単発で
add_column,add_indexなどを呼ぶ場合
のいずれでも、処理フロー上は 同じ「抽出 → AlterTable にセット」のロジック が使われます。
内部的には、extract_algorithm_and_lock! というプライベートヘルパーで、オプションから該当キーを抜き取りつつ AlterTable の algorithm / lock にセットしている形です。
サンプル(概念的な Ruby 側のコードイメージ):
# 単発
add_column :users, :age, :integer, algorithm: :inplace, lock: :none
# バルク
change_table :users, bulk: true do |t|
t.column :age, :integer, algorithm: :inplace, lock: :none
endどちらも最終的には同じように MySQL::AlterTable の algorithm / lock に値が入り、visit_AlterTable によって 1 回だけ末尾に出力されます。
2-4. AddIndex / DropIndex からの状態削除
MySQL::AddIndex / MySQL::DropIndex クラスは、もはや自身で algorithm / lock を持つ必要がなくなったため、該当フィールドが削除されました。
あわせて、visit_AddIndex / visit_DropIndex も単純化され、「インデックスを追加/削除するフラグメントのみ」を出力 する形になっています。ALGORITHM / LOCK の責務は完全に AlterTable 側へ移った、という整理です。
2-5. 単発 API 実装のシンプル化
add_column, change_column, rename_column, remove_column などの MySQL 対応コードは、
- 以前: 自前で SQL 文字列を組み立てて、末尾に
, #{algorithm}/, #{lock}を付け足す - 現在:
MySQL::AlterTableに操作を積んでexecute_alter_table(at)を呼ぶだけ
というシンプルなスタイルになりました。
擬似コードイメージ:
def add_column(table_name, column_name, type, **options)
at = AlterTable.new(table_name)
at.add_column(column_name, type, **options)
execute_alter_table(at)
endここで options[:algorithm] / options[:lock] は AlterTable#add_column 内で吸い上げられ、AlterTable 全体の状態として使われます。
2-6. テストの追加
activerecord/test/cases/adapters/abstract_mysql_adapter/active_schema_test.rb にテストが追加されています。
内容としては、
ALTER TABLEに対してALGORITHM/LOCKが一度だけ正しく付くこと- バルク・単発双方で
algorithm:/lock:オプションが無視されず反映されること
などを確認するためのものと考えられます。
- 影響範囲・注意点
3-1. 開発者視点での挙動変化
Rails の公開 API レベルでは、概ね「期待どおりに直った」変更であり、破壊的変更はほとんどありませんが、細かい挙動差は出ます。
change_table ... bulk: trueでalgorithm:/lock:オプションを指定した場合
以前は黙って無視されていましたが、今後は 実際にALTER TABLEに反映 されます。- これにより、既存アプリで「実は無視される前提」で動いていた場合、実際にオンライン DDL などが走るようになり、ロック時間や DDL の挙動が変わる可能性があります。
- 複数のインデックス追加/削除をまとめた
ALTER TABLE- 従来は MySQL に拒否されていたケース(
ALGORITHM/LOCKが N 回出る)が、正しい 1 回のみの付与で成功するようになる ため、エラーが解消されます。
- 従来は MySQL に拒否されていたケース(
3-2. 拡張 / Monkey Patch している場合の注意
以下のようなカスタマイズをしているプロジェクトは注意が必要です。
MySQL::AddIndex/MySQL::DropIndexに対してalgorithm/lockを直接操作していたvisit_AddIndex/visit_DropIndexをオーバーライドし、そこからALGORITHM/LOCKを出力していた
MySQL::SchemaCreation#visit_AlterTableを独自にオーバーライドしていた
この PR により役割分担が変わったため、
AlterTableのalgorithm/lockを見るように書き換えるvisit_AddIndex/visit_DropIndex側ではヒントを扱わない設計に合わせる
などの追従が必要になる可能性があります。
- 参考情報 (あれば)
- MySQL の
ALTER TABLE構文 (ALGORITHM/LOCKが文全体のヒントであること):- MySQL 8.0 Reference Manual – ALTER TABLE Syntax
https://dev.mysql.com/doc/refman/8.0/en/alter-table.html
- MySQL 8.0 Reference Manual – ALTER TABLE Syntax
- この PR に関連しうる Rails 側のクラス/ファイル:
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapterActiveRecord::ConnectionAdapters::MySQL::SchemaCreationActiveRecord::ConnectionAdapters::MySQL::SchemaDefinitionsActiveRecord::ConnectionAdapters::MySQL::SchemaStatements
- PR 本体(#58265):
- (公式 Rails リポジトリの PR ページを参照)
#58340 Transform keys in place in ActionController::Parameters#deep_transform_keys!
マージ日: 2026/8/2 | 作成者: @55728
- 概要 (1-2文で)
ActionController::Parameters#deep_transform_keys!が、名前に反してインプレース(破壊的)ではなく新しいハッシュを組み立てていた問題を修正し、内部で_deep_transform_keys_in_object!を使って本当にインプレース変換するようにしたPRです。これによりAPIの一貫性とパフォーマンス(割り当て数削減)が改善されますが、返されるParametersオブジェクトの見かけ上の挙動は変わりません。
- 変更内容の詳細
もともとの問題点
ActionController::Parameters#deep_transform_keys!は「!が付いている=破壊的メソッド」のはずですが、内部では「パラメータ全体を再構築」する実装になっていました。- 以前のコミット(
32587c3bdd)で_deep_transform_keys_in_object!という「本当にインプレースで変換する」ヘルパーメソッドが追加されていたものの、deep_transform_keys!からは呼ばれておらず、事実上デッドコードになっていた状態です。 - 一方で、
deep_transform_values!は_deep_transform_values_in_object!をちゃんと呼んでおり、キー版と値版で実装が非対称でした。
今回の修正内容
ActionController::Parameters#deep_transform_keys!が_deep_transform_keys_in_object!を経由するように接続されました(たった1行の差し替えですが意味は大きい変更です)。- これにより、
deep_transform_keys!もdeep_transform_values!と同様に、ネストされた構造を「その場で」変換するようになります。
PR説明中のパフォーマンス比較:
| params の形状 | 変更前 | 変更後 |
|---|---|---|
| 200 entries, 3 levels deep | 5,825 allocations | 1,612 allocations |
オブジェクト割り当て数が大幅に減っているため、GC負荷とメモリ使用量の面でメリットがあります。
動作イメージ(概念的なサンプル)
params = ActionController::Parameters.new(
"user" => {
"first_name" => "Alice",
"last_name" => "Smith"
}
)
# キーをシンボル化する例
params.deep_transform_keys!(&:to_sym)
# 返り値・内容は変更前後で同じ:
# => #<ActionController::Parameters {:user=>{:first_name=>"Alice", :last_name=>"Smith"}} permitted: false>コード上の挙動(結果として得られる params)は変わりませんが、その内部で「新しいハッシュを組み立てる」のではなく「元のハッシュやネストされたハッシュのキーを書き換える」ようになった、という変更です。
- 影響範囲・注意点
影響範囲
- 対象:
ActionController::Parameters#deep_transform_keys!を利用しているすべてのコード。 - 表向きの挙動(返される値や、処理後の
paramsの構造)は変わらないと明示されています。
重要な注意点
As with
deep_transform_values!, nested hashes that the caller still shares with the receiver are now transformed in place.
ここがポイントです。
- 以前は「内部的に新しいネストハッシュを組み立てていた」ため、
- 呼び出し側が元々持っていたネストされた
Hashオブジェクトと、Parameters内部のネストハッシュが“たまたま同一オブジェクト”であるケースは少なかった(もしくはなかった)。
- 呼び出し側が元々持っていたネストされた
- 今回の変更により、「呼び出し側が
Parameters内部のネストハッシュとオブジェクトを共有している場合」、deep_transform_keys!によって、その共有しているオブジェクトのキーがインプレースで書き換わるようになりました。
つまり:
hash = { "user" => { "first_name" => "Alice" } }
params = ActionController::Parameters.new(hash["user"]) # or hash["user"] を再利用するようなパターン
# もしここで params 内部と hash["user"] が同じオブジェクトだった場合
params.deep_transform_keys!(&:to_sym)
# 変更後は hash["user"] のキーも :first_name に変わりうる
p hash
# 以前: { "user" => { "first_name" => "Alice" } }
# 変更後: { "user" => { :first_name => "Alice" } } となる可能性Rails の通常の params 生成経路では、アプリケーションコードが同じネストハッシュオブジェクトを共有するケースはあまり多くありませんが、
- 生の
Hashを部分的にParametersに包む - あるいは
params.to_hの結果を再利用して、同じオブジェクトを紐づけるような変換をしている
といった「高度な使い方」をしている場合には、共有オブジェクトのインプレース変更を意識する必要があります。
とはいえ、PR本文でも「The returned parameters are unchanged.」と明記されており、一般的な Rails アプリケーションでの影響は軽微かつ後方互換的と考えられます。
- 参考情報 (あれば)
- 対象PR: https://github.com/rails/rails/pull/58340
- 関連コミット(ヘルパーメソッド追加元):
32587c3bdd - 類似API:
ActionController::Parameters#deep_transform_values!- 今回の修正後は、キー版・値版の両方が対称的にインプレース変換を行う実装になりました。
#58264 Consolidate ALTER TABLE building into AlterTable
マージ日: 2026/8/2 | 作成者: @kamipo
- 概要 (1-2文で)
このPRは、ALTER TABLE文の生成処理をAlterTableクラスに集約し、複数の操作を一度に行う際に不正なSQLが生成されうるバグを根本的に解消しつつ、MySQL/PostgreSQL 含めたアダプタ全体の設計を整理・リファクタリングしたものです。bulk_change_tableなどの経路もすべてAlterTableを経由するよう統一されています。
- 変更内容の詳細
背景となるバグ
これまで visit_AlterTable では、adds / foreign_key_adds などスロットごとに join(" ") した文字列同士をそのまま連結しており、異なるスロットにまたがって複数の操作が追加されると、間に区切りが入らず不正な SQL になる可能性がありました。
しかし、実際には各呼び出し側が「1回の AlterTable につき1操作」しか追加していなかったため、この問題は表面化していませんでした。
このPRでは、AlterTable に操作の順序付きリストを一本化し、SQL をそこからのみ組み立てるようにすることで、設計レベルで問題を解消しています。
AlterTable を「単一の真実のソース」にするリファクタ
1) 操作ごとの Data オブジェクト追加
AlterTable がもつ操作を、以下のような Data.define で定義された小さな値オブジェクトで表現するようになりました。
例(PR説明より):
AddColumnDefinitionAddForeignKeyDropColumnRenameColumnChangeColumnNullAddIndex- PostgreSQL固有:
ValidateConstraintAddExclusionConstraintAddUniqueConstraintなど
AlterTable インスタンスは、これらを1本の配列 operations に順序通りに保持します。
alter_table = AlterTable.new(:users)
alter_table.operations << AddColumnDefinition.new(name: :age, type: :integer)
alter_table.operations << AddIndex.new(columns: [:age])
# ...のようなイメージこれにより、
- 操作の種類に関わらず、「テーブルに対して行う順序付き操作列」として一括管理
- グループ(adds / foreign_key_adds / ...)に散らばった状態からの解放
- 複雑な ALTER TABLE 文も順序保証のある1本のリストから生成
が可能になります。
2) visit_AlterTable の書き換え
visit_AlterTable は以下のような方針で書き換えられました。
o.operationsを順番に走査- 各操作オブジェクトに対して、既存の visitor パターン (
accept(op)) で SQL 断片を生成 - それらを
", "で結合し、一つのALTER TABLE ...文にまとめる
ポイント:
- どの種類の操作 (
AddColumnDefinition,AddForeignKey, …) であっても同列に扱われ、順番が保たれる - 以前のように「スロットごとに join して最後に無分別に連結する」ことがなくなる
これにより、
ALTER TABLE users
ADD COLUMN age integer,
ADD CONSTRAINT ...のように、複数操作混在の SQL も正しく生成されます。
3) 接続アダプタ側の build ロジックを AlterTable に移管
これまで接続アダプタ側(AbstractAdapter/MySQL/PostgreSQL)にあった「ALTER TABLE 用の断片生成メソッド」が AlterTable に移されました。
移管対象(例):
build_add_column_definitionbuild_change_column_definitionbuild_change_column_default_definition
構成としては:
- 抽象的な共通部分:
AlterTable(抽象クラス相当) - DB 固有の部分:
- MySQL:
Mysql::AlterTable(あるいは MySQL 用サブクラス) - PostgreSQL:
PostgreSQL::AlterTable
- MySQL:
に持たせる形になっています。
なお、:if_not_exists による column_exists? ガードは従来どおり「呼び出し側」で残されています。これは
- 「SQLをどう組み立てるか」ではなく
- 「そもそも ALTER TABLE すべきかどうか」という前提条件チェックであり
- スキーマクエリを発行するため、ビルドロジックには含めない
という設計判断です。
4) deferred_operations の導入
PostgreSQL などでは、単一の ALTER TABLE 内に完全に収まりきらない操作があります。例:
change_column_comment(コメント変更)- デフォルト付き
change_column_nullのときに必要なUPDATE文(既存データの穴埋め)- その後改めての
ALTER TABLEでの制約変更
これらは、ALTER TABLE 実行後に別クエリとして発行する必要があります。
そこで AlterTable#deferred_operations が導入されました。
execute_alter_tableで:ALTER TABLE ...をexecutedeferred_operationsに溜めておいた処理を順次実行
というフローになります。これにより:
- 「ALTER TABLE 内にまとまる操作」と
- 「後処理として別クエリが必要な操作」
を明確に分離でき、かつ呼び出し側は統一インターフェースで扱えます。
5) COMBINABLE_COMMANDS と bulk_change_table の書き換え
bulk_change_table は、1つのテーブルに対する複数操作を「できる限り少ない ALTER TABLE でまとめて実行する」ための仕組みですが、これまではアダプタ側にたくさんの #{command}_for_alter メソッドが存在していました。
このPRでは:
AlterTableサブクラスごとにCOMBINABLE_COMMANDSを定義- 例:
add_column,add_index,change_column, ...
- 例:
bulk_change_tableはcommandを直接AlterTableのメソッドとしてpublic_sendする形に変更alter_table.add_column(...)のように呼び出す
これにより:
*_for_alter系メソッド群が不要になり削除- 複数コマンドの「同一 ALTER TABLE へのマージ可否」は
COMBINABLE_COMMANDSによりアダプタ単位で制御 - コマンド追加時のエントリポイントがシンプルに
という整理がなされています。
6) execute_alter_table(at) ヘルパの導入
AlterTable を使うすべての呼び出し方が、
ALTER TABLE文を組み立て・実行deferred_operationsを実行
というパターンに統一されるよう、プライベートヘルパ execute_alter_table(at) が導入されています。
これにより:
- 「どのパスで
ALTER TABLEを呼んでも後処理の実行漏れがない」 - 共通化によりコード重複とバグリスクを削減
といったメリットがあります。
- 影響範囲・注意点
影響範囲
- 対象:
ALTER TABLEを発行するすべてのパスchange_table/bulk_change_table/ 各種 migration DSL (add_column,change_column_null,add_foreign_key等)- MySQL / PostgreSQL / 抽象アダプタの schema 関連コード
- 変更ファイル:
schema_creation.rb,schema_definitions.rb,schema_statements.rb- MySQL / PostgreSQL 各アダプタの schema_* 系
- 関連テスト (
schema_definitions_test.rbなど)
動作上の注意点
複数操作の順序が重要なケース
これまで暗黙に保障されていた順序と、今回のoperationsベースの順序が一致しているかを意識しておく必要があります。PRとしては「実際の呼び出し順をそのまま尊重する」設計ですが、手作業で既存コードを移した部分で順序の解釈が変わっていないか要確認事項です(ただしPR自体は後方互換を維持する意図)。DBごとの COMBINABLE_COMMANDS
MySQL・PostgreSQL それぞれで「まとめられるコマンド」がCOMBINABLE_COMMANDSで制御されるため、新しいコマンドを追加する場合や既存コマンドの挙動を変える場合には、このリストのメンテナンスが必要になります。「意図せず別 ALTER に分割される/まとめられすぎる」ことがないように注意が必要です。deferred_operations を伴う変更
PostgreSQL のchange_column_null+ デフォルトや、:comment周りはdeferred_operationsの利用に依存するようになるため、もしアプリケーション側でALTER TABLEの発行をフック/ログ解析している場合は、「後続で別クエリが来る」ことを前提に調整が必要な可能性があります。拡張・カスタマイズへの影響
アダプタを拡張して独自の ALTER 系メソッドを追加している場合、今後は:- 新しい operation クラス(
Data.define)を用意 AlterTableサブクラスにビルドロジックとCOMBINABLE_COMMANDSへの追加 を行うのが推奨パスになります。既存の*_for_alterにフックしていたコードは動かなくなる恐れがあります。
- 新しい operation クラス(
後方互換性
- 公開API(migration DSL)の表面上の挙動は変えない形でのリファクタリングを意図
- ただし内部構造が大幅に変わっているため、
- adapter の monkey patch
- schema 関連の内部APIに依存している gem / プラグイン
などには影響が出る可能性があります。
- 参考情報 (あれば)
- PR本体: https://github.com/rails/rails/pull/58264
- 関連しそうなコード:
ActiveRecord::ConnectionAdapters::SchemaStatements#change_table,#bulk_change_table- 各アダプタの
schema_creation.rb,schema_definitions.rb
- 設計的な観点:
- visitor パターン (
accept/visit_Xxx) による SQL ビルドの一元化 - 「1テーブルへの一連の操作」を明示的なオブジェクト (
AlterTable+ operation objects) として建模するリファクタリング例としても参考になります。
- visitor パターン (
#58341 Fix deprecation proxy doc examples that raise ArgumentError
マージ日: 2026/8/2 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::Deprecationのプロキシクラス (DeprecatedInstanceVariableProxy,DeprecatedConstantProxy) のドキュメント中のサンプルコードが、現在のシグネチャと合わずにArgumentErrorを出していた問題を修正し、実際に動く形に更新したPRです。機能変更はなく、ドキュメント記載の使い方と実装を同期させたものです。
- 変更内容の詳細
対象ファイル:
activesupport/lib/active_support/deprecation/proxy_wrappers.rb(+3 / -3)
主な変更点:
2-1. DeprecatedInstanceVariableProxy の例の修正
以前の(壊れている)例:
ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(
self, :request, :@request, ActiveSupport::Deprecation.new
)
# => ArgumentError: wrong number of arguments (given 4, expected 2..3; required keyword: deprecator)fc2dc7c8d3 によってコンストラクタが以下のようなシグネチャに変わっており、deprecator: がキーワード引数必須になっていました:
DeprecatedInstanceVariableProxy.new(object, method, var_name, deprecator: ...)このPRでは、ドキュメント内のサンプルを新しい呼び方に合わせて修正しています。概略は次のような形です(※実際のコードと同趣旨):
ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(
self,
:request,
:@request,
deprecator: ActiveSupport::Deprecation.new
)さらに、説明文中で「deprecator が “最後の引数”」と、あたかも位置引数であるかのように書かれていた箇所も、キーワード引数を使う今の仕様に沿った文言へ修正されています。
2-2. DeprecatedConstantProxy の例の修正
以前の(壊れている)例:
ActiveSupport::Deprecation::DeprecatedConstantProxy.new('PLANETS', 'PLANETS_POST_2006')
# => ArgumentError: wrong number of arguments (given 2, expected 3)DeprecatedConstantProxy も、fc2dc7c8d3 によってコンストラクタのシグネチャが変更され、deprecator が必須引数(位置引数)になっていました。現在のシグネチャは概ね:
DeprecatedConstantProxy.new(old_const, new_const, deprecator)であるため、ドキュメントのサンプルは次のように修正されています(趣旨):
ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
'PLANETS',
'PLANETS_POST_2006',
ActiveSupport::Deprecation.new
)2-3. DeprecatedObjectProxy には変更なし
DeprecatedObjectProxy はもともと deprecator が位置引数のままで仕様変更はなく、既存のドキュメントも正しかったため、このPRでは触れられていません。
2-4. 動作確認(PR説明中の出力例)
修正後、ドキュメントに掲載されているサンプルコードをそのまま実行すると、説明されている通りの動作になります:
example.old_request.to_s
# DEPRECATION WARNING: @request is deprecated! Call request.to_s instead of @request.to_s
# => "special_request"
example.request.to_s # => "special_request"
PLANETS.class # => Array- 影響範囲・注意点
- 実コードの挙動には変更がなく、影響範囲はドキュメント(サンプルコードと説明文)のみです。
- ただし、以下のようなコードを書いている場合は注意が必要です:
- 古いドキュメントを参照して
DeprecatedInstanceVariableProxy.new(..., ..., ..., ActiveSupport::Deprecation.new)のように「deprecator を位置引数の最後」に渡している DeprecatedConstantProxy.new(old, new)のように deprecator を渡していない
- 古いドキュメントを参照して
- これらは現在の Rails ではすでに
ArgumentErrorになっており、このPRはあくまで「ドキュメントを現実に合わせた」ものです。
もし自分のコードで同様のエラーが出ている場合は、以下のように修正する必要があります:DeprecatedInstanceVariableProxyrubyDeprecatedInstanceVariableProxy.new(obj, :method, :@ivar, deprecator: ActiveSupport::Deprecation.new)DeprecatedConstantProxyrubyDeprecatedConstantProxy.new('OLD', 'NEW', ActiveSupport::Deprecation.new)
- 参考情報 (あれば)
- 概要に出てくる変更コミット:
fc2dc7c8d3- このコミットで
DeprecatedInstanceVariableProxyとDeprecatedConstantProxyのコンストラクタ API が変更され、deprecatorが必須となった。
- このコミットで
- 関連クラス:
ActiveSupport::Deprecation::DeprecatedObjectProxyActiveSupport::Deprecation::DeprecatedInstanceVariableProxyActiveSupport::Deprecation::DeprecatedConstantProxy
#58336 Fix search_field raising NameError when passed autosave: true
マージ日: 2026/8/1 | 作成者: @hammadxcm
- 概要 (1-2文で)
search_fieldヘルパーにautosave: trueを渡すとNameErrorが発生していた不具合を修正し、ドキュメントに記載されている「リクエストホストを反転した autosave 値」が実際に動作するようにしました。あわせてautosaveオプション周りの挙動を網羅するテストが追加されています。
- 変更内容の詳細
バグの内容
search_field はドキュメント上、autosave: true を渡すと以下のようにリクエストホストを反転した文字列を autosave 属性として自動付与することになっていました:
# request.host => "www.example.com"
search_field(:user, :name, autosave: true)
# => <input autosave="com.example.www" id="user_name" name="user[name]" results="10" type="search" />実装(修正前)は ActionView::Helpers::Tags::SearchField#render 内で次のように書かれていました:
options["autosave"] = request.host.split(".").reverse.join(".")しかし Tags::Base には request メソッドは定義されておらず、ビューコンテキストへのメソッド委譲もされていないため、search_field 呼び出し時には必ず次のエラーになっていました:
NameError: undefined local variable or method 'request' for
an instance of ActionView::Helpers::Tags::SearchFieldつまり、ドキュメントされていた autosave: true の機能は、これまで一度も正しく動いたことがありませんでした。
修正内容
リクエストオブジェクトを、タグが内部的に保持しているテンプレートオブジェクト(ビューコンテキスト)から取得するように変更しました。
# 修正前(概略)
options["autosave"] = request.host.split(".").reverse.join(".")
# 修正後
options["autosave"] = @template_object.request.host.split(".").reverse.join(".")@template_object は ActionView タグが持っているビューコンテキストであり、ここから request を参照するのが正しいアクセス方法です。
テスト追加と仕様の明確化
autosave 周りの分岐をすべてカバーするテストが form_helper_test.rb 等に追加されています。挙動は以下のように整理されています:
autosave: trueautosave属性はrequest.hostを"."で split → reverse → join した文字列になるresults属性はデフォルトで"10"が付与される
例:
ruby# request.host = "www.example.com" search_field(:user, :name, autosave: true) # => autosave="com.example.www" results="10"autosave: "my-key"(文字列指定)- 指定した文字列がそのまま
autosave属性に利用される results属性はデフォルト"10"が付与される
例:
rubysearch_field(:user, :name, autosave: "my-key") # => autosave="my-key" results="10"- 指定した文字列がそのまま
results:を明示指定した場合autosaveによるデフォルト"10"は上書きされず、指定したresults値が優先される
例:
rubysearch_field(:user, :name, autosave: true, results: 5) # => autosave="com.example.www" results="5"autosave: falseautosave="false"という属性がそのまま出力される(属性自体を付けないわけではない)results属性は付与されない
これは既存ドキュメントに記載されていた出力と一致するようにされています。
テストスイート (actionview の関連テスト) はすべて成功し、RuboCop もパスしています。
- 影響範囲・注意点
影響範囲
ActionView::Helpers::FormTagHelper/FormHelper/FormOptionsHelper/form_withなどを通じてsearch_fieldを利用しているコードで、autosaveオプションを使っている(またはこれから使う)場合。- これまで
autosave: trueを指定していた場合は、実際にはNameErrorによって動いていなかったため、そのコードパスが通っていたアプリでは今回の修正で「初めて」期待どおり動作することになります。
後方互換性の観点
autosave: trueがこれまでそもそも動作していなかったため、「正常に動いていた挙動が変わる」という種類の破壊的変更ではありません。- ただし、例外が発生することを前提にしていた非常に特殊なケース(例: rescue して代替処理など)は、例外が出なくなることで挙動が変わり得ます。
ホスト名依存の注意点
autosave: trueの値は@template_object.request.hostを元に計算されます。requestが利用できないコンテキスト(極めて特殊なレンダリングコンテキストなど)では、理論上ここで別の例外(NoMethodErrorなど)が発生する可能性がありますが、通常のコントローラ + ビューのレンダリングでは問題ありません。- マルチホスト / サブドメイン利用アプリでは、
autosave値がホストによって変わる点を踏まえ、キャッシュキーなどに組み込む場合は挙動を把握しておくとよいです。
- 参考情報 (あれば)
変更ファイル:
actionview/CHANGELOG.mdactionview/lib/action_view/helpers/tags/search_field.rbactionview/test/template/form_helper_test.rb(autosaveブランチのテスト追加)
この修正により、
search_fieldのautosaveに関するドキュメントと実際の挙動が一致し、今後autosave関連のリグレッションが起きにくいようにテストも補強されています。
#58339 Use precompiled Rubies from Mise in Install Rails Guide
マージ日: 2026/8/1 | 作成者: @excid3
- 概要 (1-2文で)
Rails公式ガイド「Install Ruby on Rails」で、Rubyのインストール手順が「Miseによる事前コンパイル済みRubyの利用」を前提とした内容に更新されました。これにより、依存関係のインストールやソースからのコンパイル手順が不要となり、ガイド全体がシンプルかつ高速なセットアップ手順に整理されています。
- 変更内容の詳細
Miseの利用前提への更新
- ガイド中の「Rubyのインストール方法」が、Miseが提供するprecompiled Rubyを利用する形に書き換えられました。
- 従来は、OSごとのパッケージインストールや、ビルドに必要な依存パッケージの説明、
ruby-build/rbenv 等と組み合わせたソースコンパイル手順が含まれていましたが、そうした説明が大幅に削除されています(-25行)。 - 代わりに、Miseをインストールした上で、Mise経由でRubyをインストールする最小限の手順だけが残されている形です(+6行)。
対象バージョンの更新
- ガイド内で言及される Ruby・Rails・Ubuntu のバージョンが最新に更新 されています。
- 具体的なバージョン番号はPR本文では明示されていませんが、「最新の安定版Ruby/ Rails」「最新LTSもしくは推奨版Ubuntu」の組み合わせに合わせた内容に変更されています。
- ガイド内で言及される Ruby・Rails・Ubuntu のバージョンが最新に更新 されています。
ドキュメント専用の変更
- 変更ファイルは
guides/source/install_ruby_on_rails.mdのみで、アプリケーションコードやテストコードへの変更はありません。 - 追加行6、削除行25と、ほぼ「不要になった作業手順」を落として簡潔にした構成です。
- 変更ファイルは
※実際のサンプルコマンド(イメージ)
ガイド内の実際の文面はPRからは全部は読めませんが、内容的には以下のような形に近くなっていると考えられます:
# Mise のインストール(例)
curl https://mise.run | sh
# 最新の Ruby をインストール
mise use --global ruby@<latest>
# インストール後の確認
ruby -v従来入っていたであろう「Ubuntuでbuild-essentialやlibssl-devなどを入れる」「ソースをコンパイルする」などの説明が削られ、Miseのインストールとmise use程度に集約されています。
- 影響範囲・注意点
新規ユーザへの影響(セットアップ体験)
- Rails公式ガイドに従うだけで、コンパイル不要・依存関係の事前準備も不要なRubyインストールが可能になり、導入がかなり簡単かつ高速になります。
- Windows / macOS / Linuxといった複数OSで、Miseさえ動けばほぼ同じ手順でRubyを入れられるようになり、クロスプラットフォームな案内としてもわかりやすくなります。
既存環境のユーザへの影響
- 既に rbenv / rvm / asdf などを使っている開発者にとっては、コード上の互換性やRails自体の挙動には影響はありません。
- ただし、公式のインストールガイドを読むと「Mise推奨」のように見えるため、チームの文化や既存ドキュメントが他のバージョンマネージャ前提の場合は、メンバーの環境構築手順を見直す必要が出るかもしれません。
Miseのprecompiled Rubyへの依存
- ガイドが「Miseのprecompiledバイナリ」を前提としたことで、
- 対応していないOS/アーキテクチャ(例: 非公式なディストリや特殊なCPUアーキテクチャ)
- あるいはまだprecompileされていないRubyバージョン
では、ガイド通りの手順ではインストールできない可能性があります。
- 学習用途や一般的な開発マシン(Ubuntu, macOS, WSL2など)であればほぼ問題ないと思われますが、CI環境やコンテナベースの本番にそのまま流用する場合は、自分たちの環境でMiseのprecompiled Rubyが安定して使えるかを確認する必要があります。
- ガイドが「Miseのprecompiledバイナリ」を前提としたことで、
テスト・挙動上の変更はなし
- このPRはガイド文書のみの変更であり、Rails本体の挙動やAPIには一切変更がありません。
- バージョンアップしたRuby/Rails/Ubuntuの組み合わせによる挙動の差分は、今回のPRというより、各プロジェクト(Ruby/ Rails/ OS)の変更の影響として別途考える必要があります。
- 参考情報 (あれば)
- Mise リリースノート(precompiled Ruby対応の詳細)
- precompiled Ruby バイナリ一覧(対応バージョン・プラットフォームの確認用)
- Rails Guides(Install Ruby on Rails)
- 該当のガイド本文は、
guides/source/install_ruby_on_rails.mdに対応する公開版を参照してください(本PRマージ後の内容が反映されます)。
- 該当のガイド本文は、
#58323 Deprecate passing binds to insert, update, and delete
マージ日: 2026/8/1 | 作成者: @kamipo
- 概要 (1-2文で)
ActiveRecord::ConnectionAdaptersのinsert,update,deleteに対して、従来の「第2引数以降でbindsを渡す」呼び出し方が非推奨になりました。代わりに、SQL文字列とバインド値をまとめたArel.sql(..., *binds)を渡す書き方に移行するための正式なデプリケーションです。
- 変更内容の詳細
非推奨になる呼び出し方
これまで多くのアダプタ実装やアプリケーションコードでは、以下のように sql と binds を別々に渡す呼び出し方が可能でした:
connection.insert("INSERT INTO topics (title) VALUES (?)", nil, nil, [["title", "hello"]])
connection.update("UPDATE topics SET title = ? WHERE id = 1", nil, [["title", "hi"]])
connection.delete("DELETE FROM topics WHERE id = ?", nil, [["id", 1]])PRでは、この「binds を位置引数 (positional) で渡すインターフェイス」が 非推奨 (deprecate) されます。insert, update, delete すべてが対象です。
推奨される新しい呼び出し方
今後は、SQL文字列とそのバインド値を Arel.sql にまとめて渡し、to_sql_and_binds が AST から自動的にバインドを抽出するスタイルが推奨されます。
connection.insert(
Arel.sql("INSERT INTO topics (title) VALUES (?)", "hello")
)
connection.update(
Arel.sql("UPDATE topics SET title = ? WHERE id = 1", "hi")
)
connection.delete(
Arel.sql("DELETE FROM topics WHERE id = ?", 1)
)Arel.sql("... ?", *binds) は Arel::Nodes::BoundSqlLiteral を返し、Model.where("... = ?", value) と同じ仕組み(SQL + バインドをASTに載せる)で扱えるようになります。
コード上の変更点の概要
activerecord/lib/active_record/connection_adapters/abstract/database_statements.rbinsert,update,deleteが、bindsを位置引数で受け取った場合にデプリケーション警告を出すように変更。- 内部的には、
Arel.sql由来のArel::Nodes::BoundSqlLiteralが渡された場合に、そのASTからバインド値を抽出して処理する形が明確化。
CHANGELOG.md- 上記仕様変更についての記載が追加。
adapter_test.rb,database_statements_test.rb- 旧来の呼び出しパターンが「動くが警告が出る」こと、および新パターンが期待どおり動作することを確認するテストが追加/更新。
- 影響範囲・注意点
主に影響を受けるのはどんなコードか
- アダプタやライブラリで、以下のように 生SQL + binds を直接
insert/update/deleteに渡しているコード:- 例:ruby
connection.update(sql, "Some Query", nil, binds)
- 例:
- アプリケーション側で
connection.raw_connectionではなく、ActiveRecord::Base.connection.insert/update/deleteを直接使い、
SQL とバインドを別々に渡しているケース。
実際の影響
- 現時点(このPRが入った時点)では:
- 機能自体はまだ動作するが、
bindsを位置引数で渡すと デプリケーションの警告が出る。 - 将来のメジャーバージョン(または指定バージョン)でこの呼び出し方が削除される可能性が高い。
- 機能自体はまだ動作するが、
Arel.sqlベースに書き換えないと、Rails の将来バージョンにアップグレードした際にArgumentErrorやNoMethodErrorになるリスクがある。
マイグレーションの方針
- 旧スタイル:ruby
connection.insert("INSERT ... VALUES (?)", "Insert Topic", nil, [[nil, "hello"]]) - 推奨スタイル:ruby
connection.insert( Arel.sql("INSERT ... VALUES (?)", "hello") ) - カスタムアダプタ・ライブラリでは、以下のような対応を検討するとよいです:
insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])のようなシグネチャを使っている場合、- 外部公開APIであれば、
binds引数の利用を非推奨にしてArel.sql経由に切り替えを促す。 - 内部用であれば、
Arel::Nodes::BoundSqlLiteralを前提としたシグネチャに整理する。
- 外部公開APIであれば、
注意すべき技術的ポイント
Arel.sqlが SQL文字列に対してバインドを埋め込むのではなく、SQLとバインドを一緒にASTとして持つ 点に注意。- 手動で
?をquoteで埋め込むのではなく、あくまで?プレースホルダとバインド値をArel.sqlに渡す。
- 手動で
- 既存の
Model.where("name = ?", "foo")と同じスタイル・同じ仕組みでクエリを扱えるため、
SQL構築まわりのコードを Arel ベースに寄せていく上で一貫性がとれる。
- 参考情報 (あれば)
- 本PRで参照されている過去のPR: #29944
- bind パラメータを Arel AST に移した際、既存の「生SQL + binds」利用者のために
bindspositionals を一時的に復活させた経緯がある。
- bind パラメータを Arel AST に移した際、既存の「生SQL + binds」利用者のために
- 関連クラス・メソッド:
Arel.sql(string, *binds)→Arel::Nodes::BoundSqlLiteralActiveRecord::ConnectionAdapters::DatabaseStatements#to_sql_and_bindsModel.where("... = ?", value)
これと同様のバインド管理の仕組みを、insert/update/deleteでも統一していくための変更。
#58324 Fix String#parameterize raising TypeError when separator is nil
マージ日: 2026/8/1 | 作成者: @hammadxcm
- 概要 (1-2文で)
String#parameterizeにseparator: nilを渡すとTypeErrorで落ちていた不具合を修正し、nilを空文字と同等に扱うようにした PR です。これにより、separatorを設定ファイルやパラメータからそのまま渡しても安全に動くようになります。
- 変更内容の詳細
何が問題だったか
ActiveSupport::Inflector#parameterize(String#parameterize の実体)は、内部で次のような処理をしていました。
parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator)
unless separator.nil? || separator.empty?
# 複数連続したセパレータを1つにまとめる
# 先頭/末尾のセパレータを削除する
end意図としては:
separatorがnilまたは""(空文字)の場合:- セパレータを挿入しない(そのままくっつける)
- それ以外(
"-","_"など)の場合:- 余分なセパレータをまとめたりトリムしたりする
という設計でした。
しかし Ruby の gsub! に replacement = nil を渡すと TypeError になるため、
"Donald E. Knuth".parameterize(separator: nil)
# => TypeError: no implicit conversion of nil into Stringと、separator.nil? を見る前に落ちてしまっていました。
つまり「separator が nil の場合を考慮したガード」は存在するのに、実際にはそこまで到達できないという矛盾した状態でした。
どう直したか
separator: nil を separator: "" と同等に扱うように変更し、ガードロジックを整理しています。
変更のポイント:
nilを空文字として扱うgsub!に渡す前に、separatorがnilの場合は""に正規化するようにしています(実装的にはnilのままgsub!に行かない形に整理)。結果として:
ruby"Donald E. Knuth".parameterize(separator: nil) # => "donaldeknuth" "Donald E. Knuth".parameterize(separator: "") # => "donaldeknuth"のように、両者は同じ動作になります。
separator.nil?チェックを削除nilと空文字が同じ経路を通るようにしたため、rubyunless separator.nil? || separator.empty?という条件は冗長になり、
nil?チェックが削除されています。
実質的には「空文字相当ならセパレータ処理をスキップする」という、もともとの意図をそのままシンプルにした形です。テスト追加
既存の「セパレータなし("")」ケースと同じテストパターンを、separator: nilに対しても追加しています。StringToParameterizeWithNoSeparatorStringToParameterizePreserveCaseWithNoSeparator
の2つのフィクスチャを再利用し、
preserve_case: true/ デフォルト両方をカバー。テスト結果(activesupport の該当テスト一式):
- 645 runs
- 3207 assertions
- 0 failures / 0 errors
- RuboCop も問題なし
- 影響範囲・注意点
影響範囲
- 影響を受けるのは
String#parameterizeにseparator: nilを渡すケースのみ です。 - これまで
TypeErrorが発生していたケースが、"TypeErrorで落ちる" → "文字列として正しくパラメータ化される"に変わります。 separator: ""を使っていた既存コードの挙動は変わりません(nilがその挙動に揃えられただけ)。
- 影響を受けるのは
後方互換性
- 実際に
separator: nilを意図して使っていた場合:- これまでは例外が発生 → 今後は
"donaldeknuth"のようにパラメータ化されるようになるので、「エラーで気づく」パターンを前提にしていた場合は挙動が変わる点に注意が必要です。 - ただし「
nilをサポートしない」方向(nilガードを削除して常に TypeError にする)ではなく、「自然に動く」方向に寄せたため、一般的なアプリケーションにとっては改善と見なせる変更です。
- これまでは例外が発生 → 今後は
- 設定ファイルや params から
separatorをそのまま渡しているコードにとっては、nilの場合も安全に動作するようになるため、想定外エラーのリスクは減ります。
- 実際に
注意点
- バリデーションや例外発生有無で挙動をテストしていた場合(例: 「不正な separator なら TypeError」などの仕様を勝手に決めていた場合)、テストが通らなくなる可能性があります。
- 「
nilを指定したらエラーになるべき」と考えていた場合、この PR はそれと逆方向の挙動になります。もしそのポリシーを保ちたいプロジェクトでは、アプリケーション側でseparatorを検証してください。
- 参考情報 (あれば)
- 対象メソッド:
String#parameterize(ActiveSupport::Inflector#parameterize)- 実装ファイル:
activesupport/lib/active_support/inflector/transliterate.rb
- テストファイル:
activesupport/test/core_ext/string_ext_test.rb- (関連として)
inflector_test.rb,transliterate_test.rbも通過済み
- PR方針の判断:
- 「
nilを非サポートとしてエラーにする」のではなく、「""と同じ扱いにして実用上の驚きを減らす」方向が選択されています。
- 「
#58325 Add test coverage for NameError#missing_name branches
マージ日: 2026/8/1 | 作成者: @hammadxcm
- 概要 (1–2文で)
NameError#missing_nameの全ての分岐をカバーするテストが追加され、これまでテストされていなかったケース(receiver == Object、receiver呼び出し時のArgumentError、メッセージからの名前抽出)が網羅されました。機能変更はなく、既存挙動の仕様化・回帰防止が目的です。
- 変更内容の詳細
NameError#missing_name の分岐と今回のテスト
NameError#missing_name は、例外がどのレシーバー(self.receiver)に対して起きたかによって名前の取り出し方を変えています:
receiver = begin
self.receiver
rescue ArgumentError
nil
end
if receiver == Object
name.to_s
elsif receiver
"#{real_mod_name(receiver)}::#{self.name}"
else
if match = message.match(/((::)?([A-Z]\w*)(::[A-Z]\w*)*)$/)
match[1]
end
endこのうち、従来テストでカバーされていたのは:
elsif receiverブランチ(モジュール/クラス配下の定数が見つからないケース)- 「定数エラーではない場合の早期リターン」
のみでした。今回の PR では、以下 3 ケースを新たにテストしています。
1) receiver == Object のケース (トップレベル定数)
テスト名: test_missing_top_level_constant_is_not_namespaced
対象ケース: トップレベル定数参照で NameError が発生した場合。
Ruby では、トップレベルの定数参照のレシーバーは Object になるため、この分岐が使われます。
例(イメージ):
begin
HelloWorld # 未定義
rescue NameError => e
e.missing_name # => "HelloWorld" を期待
endこの挙動は NameError#missing_name のドキュメントに載っている例ですが、これまでテストされていませんでした。今回のテストで、トップレベルの未定義定数に対して missing_name が名前空間を付けず、そのまま "HelloWorld" を返すことが保証されます。
2) receiver 呼び出しが ArgumentError を投げるケース
テスト名: test_missing_name_falls_back_to_the_message_without_a_receiver
対象ケース: NameError が「レシーバー情報なし」で生成されている場合。
その場合 NameError#receiver を呼ぶと ArgumentError が発生することがあり、実装側ではそれを rescue して receiver = nil としています。
ここでは:
self.receiver呼び出しでArgumentErrorが起きる- 結果的に
receiverがnilになる - その場合、例外メッセージ
messageを正規表現でパースし、末尾の「定数っぽいもの」を抽出する
というフォールバック経路がテストされています。
例(イメージ):
e = NameError.new("uninitialized constant Foo::Bar")
e.missing_name # => "Foo::Bar" を message から抽出このように、「レシーバー情報を持たない NameError でもメッセージから定数名を復元する」挙動がテストで固定されました。
3) メッセージが定数に関するものではないケース
テスト名: test_missing_name_ignores_a_message_that_is_not_about_a_constant
対象ケース: NameError だが、メッセージが「未定義定数」に関するものではない(もしくは定数名の形式にマッチしない)場合。
この場合、正規表現マッチが失敗するため:
missing_nameはnilmissing_name?はfalse
となることをテストで保証しています。
例(イメージ):
e = NameError.new("undefined local variable or method `foo' for main:Object")
e.missing_name # => nil
e.missing_name? # => falseこれにより、「すべての NameError が定数名を持つとは限らない」という前提をメソッド側が尊重していることが確認されています。
- 影響範囲・注意点
影響範囲
- 変更は
activesupport/test/core_ext/name_error_test.rbのテスト追加のみで、本体コード (active_support/core_ext/name_error.rb) には一切変更なし。 - ただし、テストによって
NameError#missing_name/missing_name?の現状仕様(特にメッセージフォールバックとnil戻り値の扱い)が明確に固定されるため、将来の挙動変更には注意が必要になります。
- 変更は
利用者視点の注意点
- トップレベルの未定義定数では
missing_nameは"Foo"のように非修飾名を返し、ネストした定数 (Foo::Bar) の場合は"Foo::Bar"と名前空間付きで返される、という挙動が前提になっています。 NameErrorが常に定数についてとは限らないため、missing_nameを使うコードではnilを返し得ることを前提にする必要があります(今回のテストでこれが仕様として強化された形)。
- トップレベルの未定義定数では
- 参考情報 (あれば)
- 該当コード:
active_support/core_ext/name_error.rb - この PR によるカバレッジ:
active_support/core_ext/name_error.rb: 17/21 行 → 21/21 行(100%)activesupport/test/core_ext/: 1474 テスト / 6124 アサーション / 0 失敗, 0 エラー
- RuboCop もクリーンでスタイル上の問題なし。
#58329 Fix documented strftime format for week_field default value
マージ日: 2026/8/1 | 作成者: @55728
- 概要 (1-2文で)
week_fieldヘルパーのデフォルト値に使われるstrftimeフォーマットについて、実装とドキュメントの不一致を解消し、ドキュメント側を実装 ("%G-W%V") に合わせる修正です。コードの挙動は既に正しいため、このPRはドキュメントのみの更新です。
- 変更内容の詳細
何が問題だったか
WeekField#format_datetime(week_fieldのデフォルト値生成)が内部的に使っているフォーマットはstrftime("%G-W%V")。- しかし
week_fieldの API ドキュメントには、古いまま"%Y-W%W"と記載されていた。 - そのため「
week_fieldのデフォルトフォーマットは%Y-W%W」と勘違いする可能性があった。
なぜ %G-W%V が正しいのか
<input type="week"> の値は「ISO 8601 週日付」を表す文字列 (yyyy-Www) で、ISO週番号年に従います。
Ruby の strftime では:
%G: ISO 8601 週番号年 (ISO week-numbering year)%V: ISO 8601 週番号 (01–53)%Y: 通常の西暦年 (Gregorian year)%W: 月曜始まりの週番号 (00–53, ただしISOではない)
したがって、HTML仕様に合致させるなら %G-W%V の組み合わせを使うのが正しく、実装側もすでにこれになっています。
PRにあるサンプル:
date = Date.new(2015, 12, 28)
date.strftime("%G-W%V") # => "2015-W53" # 実際のヘルパーの出力
date.strftime("%Y-W%W") # => "2015-W52" # ドキュメント上の誤った表記2015-12-28 は ISO 週日付では「2015-W53」であり、%Y-W%W を使うと「2015-W52」とズレることが分かります。
このように、年末年始付近では ISO 週番号と暦年がずれるため、%Y/%W 組み合わせは <input type="week"> 用として不適切です。
このPRが行ったこと
actionview/lib/action_view/helpers/form_helper.rb内のドキュメント中の"%Y-W%W"という1行を"%G-W%V"に修正。- 実装・テストはすでに別PR (#57890) で修正済みのため、このPRではドキュメントのみの同期を行っています。
- 影響範囲・注意点
実行時の挙動は変わりません
すでにweek_fieldは%G-W%Vで値を生成しており、このPRはドキュメントの表記を追従させるだけです。既存アプリへの直接の破壊的変更はなし
ただし、以下の開発者は注意が必要です:- Rails のドキュメントを見て、
week_fieldのデフォルトフォーマットが%Y-W%Wだと思い込んでいた人 - それに合わせて自前実装やテストデータを
%Y-W%Wベースで書いていた人
実際のヘルパーはすでに
%G-W%Vを出しているので、「ドキュメントを読んで真似していたコード」がある場合は、そこを%G-W%Vに揃える必要があります。- Rails のドキュメントを見て、
ISO週と暦年の違いを意識する必要があるケース
年末年始近辺の日付を<input type="week">とやり取りする処理では、ISO週番号年を前提にロジックを書く必要があります。
例えば「2015-W53」と返ってきた値は、カレンダー上では 2015年末〜2016年始を指す可能性があり、「yyyyをそのまま暦年とみなす」という扱いは誤りになることがあります。
- 参考情報 (あれば)
関連PR:
- #57890:
WeekField#format_datetimeの実装を%G-W%Vに変更したPR - #54355: 実装+ドキュメントの両方を修正しようとしていた元PR(実装・テスト部分は #57890 で取り込まれ、本PRでドキュメント部分が反映)
- #57890:
用語整理:
- ISO 8601 week-numbering year: 週単位で年を数える体系。年の最初の木曜を含む週を「第1週」とするため、1月1日が「前年の第52/53週」になったり、その逆になったりする。
<input type="week">仕様: ISO 8601 週間日付 (YYYY-Www) を前提としており、Rails側で%G-W%Vを使うのはこの仕様に忠実な実装。
#58331 Fix model_name crashing when a model has an anonymous ancestor
マージ日: 2026/8/1 | 作成者: @55728
- 概要 (1-2文で)
Rails 7.2 開発中に入った変更 (#58298) により、「無名クラスを祖先にもつモデル」でmodel_nameを呼ぶと例外が出るリグレッションが発生していたのを、i18n キー生成時に無名祖先をスキップすることで修正した PR です。通常のモデルの動作や i18n 探索順序は変えずに、無名祖先を介したクラス継承でもmodel_nameを安全に使えるようにしています。
- 変更内容の詳細(あればサンプルコードも含めて)
問題の背景
元の変更 (#58298) で以下が行われていました:
ActiveModel::Nameを Ractor 共有可能にするためにName#freezeが導入されたfreeze時に i18n 用のキー (i18n_keys) を事前計算 (eager) するようになったlookup_ancestorsで祖先チェーンをたどり、それぞれのklass.model_nameから i18n キーを構築する
ここで、以下のように匿名クラスを中間クラスとして使うと:
base = Class.new(ActiveRecord::Base) # 匿名の中間クラス
Widget = Class.new(base) # 名前付きモデル
Widget.model_namebase は name が空のクラスなので、Name.new の内部で
- 「クラス名が空なので name を明示的に渡せ」という
ArgumentErrorが発生 - その結果、
Widget.model_name自体が落ちる
以前は:
- i18n キーは遅延計算だった
model_name自体は成功し、humanを呼んだときに初めて祖先をたどっていた
ため、匿名祖先がいても model_name までは動いていました。
修正内容
i18n キー生成時に「無名クラスの祖先をスキップする」ように変更しています。
擬似コードで表すと、おおよそ次のような変化です:
# 変更後イメージ
@klass.lookup_ancestors.filter_map do |klass|
# クラス名が空(blank)ならスキップ
klass.model_name.i18n_key unless klass.name.blank?
endポイント:
lookup_ancestorsで取得した各祖先クラスに対してklass.name.blank?なもの(匿名クラス)は i18n キー計算対象から外す
- そもそも匿名クラスには意味のある
model_name/i18n キーがないので、スキップしても翻訳解決順序的に問題はない - これにより
Widgetのような名前付きモデルは:- 祖先チェーンに匿名クラスが混ざっていても
model_nameが例外を投げなくなる - i18n 探索順序は「名前のある祖先クラス」については従来通り
- 祖先チェーンに匿名クラスが混ざっていても
テスト
activemodel/test/cases/naming_test.rb に回帰テストが追加されています。
Name#freeze→i18n_keysを直接たたく形でテスト- Ruby が Ractor をフルサポートしていなくても、
freezeによる eager 計算経路を確実に通せる
- Ruby が Ractor をフルサポートしていなくても、
- 修正前は
ArgumentErrorが発生してテストが red - 修正後は例外なく i18n キーが生成され green
- 影響範囲・注意点
影響範囲
影響を受けるケース
- ActiveRecord/ActiveModel で、匿名クラスを中間クラスとして継承しているモデルで
model_nameを経由するあらゆる機能:form_for/form_withurl_for,polymorphic_urlなどルーティングヘルパto_partial_path/render @recordのパーシャル解決- シリアライゼーション (
as_json等)
- これらが PR #58298 以降のコードベースで
ArgumentErrorによって壊れていたが、本 PR により復旧
- ActiveRecord/ActiveModel で、匿名クラスを中間クラスとして継承しているモデルで
影響を受けないケース
- 通常の「すべての祖先クラスに名前がついている」モデル
- 匿名クラス自身に対して
model_nameを呼ぶ場合- 今回の PR でも引き続き
ArgumentErrorを投げる(従来通り)
- 今回の PR でも引き続き
i18n の動作影響
- 匿名祖先には
model_nameで利用されるような翻訳キーは本来ないため、無視しても現実的なケースでは問題なし - 名前付きの祖先クラスの解決順序は従来通り維持される
- 例:
Admin::User < User < ApplicationRecord UserやApplicationRecord由来の i18n キーの探索順序は変わらない
- 例:
バージョン・互換性
- このリグレッションは #58298 以降の未リリースコードでのみ発生
- 公式にリリースされた Rails バージョンはそもそも影響を受けていない
- そのため CHANGELOG の更新はなし
- 参考情報 (あれば)
- 関連 PR
- #58298:
ActiveModel::Nameを Ractor 共有可能にするためのName#freeze導入と i18n キーの eager 計算
- #58298:
- 実務上の示唆
- engine やプラグイン、テスト等で、
Class.new(ActiveRecord::Base)のような匿名ベースクラスを挟んでモデルを定義していても、この修正以降はform_for/url_for/renderなどフレームワーク全体で安全に扱えるようになります - 匿名クラスに対して
model_nameが必要なケースは引き続きサポート外であり、必要ならクラスに名前をつけるべき、という従来方針も維持されています
- engine やプラグイン、テスト等で、
#58307 Avoid redundant worker pool dispatch for default Action Cable streams
マージ日: 2026/8/1 | 作成者: @npezza93
- 概要 (1-2文で)
Action Cable の「デフォルトストリーム」におけるメッセージ配送で、同じジョブを executor と connection worker pool の2段階に重ねてディスパッチしていた無駄を取り除き、パフォーマンスを改善する変更です。ユーザー定義コールバックは従来通り worker pool で隔離しつつ、デフォルトハンドラだけ executor 上で完結させることで、レイテンシとスループットが大きく改善されています。
- 変更内容の詳細
背景となるボトルネック
Action Cable でメッセージが publish されて subscriber に届くまでのフローには、少なくとも以下のスレッド/キューが関わります:
アダプタ (Redis / Solid Cable など)
- サブスクライバーごとの callback を Action Cable の executor 上に enqueue して実行していた。
ストリームハンドラ (
ActionCable::Channel::Streams)- サブスクライバー callback 内で、さらに connection の worker pool にジョブを enqueue し、最終的な処理(ペイロードの decode やユーザーコードの実行)を行っていた。
問題は、デフォルトストリーム(単にメッセージを decode してクライアントに流すだけのケース)でも、必ず executor → worker pool という二重ディスパッチをしていた点です。
ユーザー定義のコールバック(任意のアプリケーションコードが走るもの)は worker pool で隔離する意味がありますが、単にデフォルトの decode + 送信だけなら、すでに executor 上で動いている処理をさらに worker pool に投げ直す必要はありません。
この二重ディスパッチが 750クライアント × 10メッセージ/秒 × 300秒 というような高負荷時に大きなオーバーヘッドとなり、全アダプタで遅延と処理落ちが起きていました。
具体的な変更点
変更ファイルは以下の2つです。
actioncable/lib/action_cable/channel/streams.rbactioncable/test/channel/stream_test.rb
要点:
「デフォルトストリーム」と「ユーザー定義コールバック」を分岐して扱うようにした
- デフォルトストリーム: executor 上で完結させる(worker pool への再ディスパッチをしない)。
- ユーザー定義の subscription コールバック: 従来通り connection の worker pool に enqueue する。
ストリームハンドラの実装変更
- 以前は「すべてのストリームハンドラが connection の worker pool に投げられる」形だったところを、
- 「ハンドラがデフォルト(内部実装のハンドラ)か、ユーザー定義か」を判別して処理分岐するようにしています。
- ざっくりいうと、以下のイメージに近い変更です(実際のコードから簡略化したイメージ):
ruby# もともとのイメージ def stream_from(broadcasting, callback = nil) # adapter が executor 上でこのブロックを呼ぶ subscribe_to(broadcasting) do |message| # ここでさらに connection.worker_pool に投げていた connection.worker_pool.post do (callback || default_stream_callback).call(decode(message)) end end end # 変更後のイメージ def stream_from(broadcasting, callback = nil) # adapter が executor 上でこのブロックを呼ぶ subscribe_to(broadcasting) do |message| data = decode(message) if callback # ユーザー定義コールバック connection.worker_pool.post do callback.call(data) # 任意アプリケーションコードは worker pool 上で end else # デフォルトハンドラ default_stream_callback.call(data) # executor 上でそのまま実行 end end end実際の実装ではもう少し抽象化されていますが、要は「内部デフォルトハンドラは executor 上で完結、ユーザーコードは worker pool に逃がす」という分岐が入っています。
- 以前は「すべてのストリームハンドラが connection の worker pool に投げられる」形だったところを、
テスト (
stream_test.rb) の追加- 新たに追加されたテストでは、
- デフォルトストリームが worker pool に重ねてディスパッチされないこと
- ユーザー定義コールバックは引き続き worker pool を使うこと
を確認しています。
- これにより、回帰を防ぎつつ、本 PR の前提となる実行コンテキストの違いが担保されます。
- 新たに追加されたテストでは、
ベンチマーク結果
作者が提示しているベンチマーク結果の要約:
750 VUs / 7 msgs/sec
main (変更前):
Metric Redis Solid Cable SQLite 平均 106ms 289ms p95 156ms 437ms 本 PR 適用後:
Metric Redis Solid Cable SQLite 平均 90ms 127ms p95 123ms 180ms
Redis / Solid Cable ともに平均・p95 ともに大きく改善しています。Solid Cable では特に顕著です。
750 VUs / 10 msgs/sec
main (変更前):
Metric Redis Solid Cable SQLite 平均 fails fails p95 fails fails 本 PR 適用後:
Metric Redis Solid Cable SQLite 平均 84ms 160ms p95 116ms 269ms
変更によって、7 msgs/sec が限界だったシナリオで 10 msgs/sec まで耐えられるようになり、約 35% のスループット向上が確認されています。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは Action Cable のチャンネルストリーム (
stream_from等) を使ったメッセージ配送経路です。 - 特に、以下のような「普通のブロードキャストのみ」のケースで効果が大きいです:
- 多数のクライアントが同一チャンネルに subscribe
- サーバ側はほぼ
ActionCable.server.broadcastだけをしている - 各クライアント側の処理は標準の stream ハンドラ(メッセージをそのまま JavaScript に届ける程度)しか行わない
互換性・挙動面
- ユーザー定義のコールバックは引き続き worker pool 上で実行されるため、アプリケーションコードの実行コンテキストは従来と変わりません。
- したがって、「どのスレッド/プールでアプリケーションコードが走るか」という意味での互換性は維持されています。
- 変更されるのは 内部のデフォルトストリームの実行場所(executor と worker pool のどちらで完結するか)だけです。
- これにより以下のような性質が維持されます:
- 任意のアプリケーションコードは worker pool 上で隔離される
→ 長時間ブロックする処理が executor を詰まらせにくい - デフォルトの decode + 送信だけであれば executor 上で高速に処理される
→ 高負荷時のレイテンシ・スループットが改善
- 任意のアプリケーションコードは worker pool 上で隔離される
注意点・留意事項
- executor 上で走る処理がやや増えることになるので、もし独自拡張で「デフォルトストリームに見えるが、実際には重い処理をしている」ようなコードがある場合は注意が必要です。
- ただし、標準の設計としては「重い処理は明示的なユーザーコールバック内で行う」想定なので、一般的なアプリケーションでは問題になりにくいはずです。
- CHANGELOG は(現時点で)更新されていないため、リリースノート等でこの改善点を見落とさないようにする必要があります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58307
- 関連コンセプト:
- Action Cable executor: コネクション・チャンネルレベルの処理を実行するためのスレッドプール/実行コンテキスト
- Connection worker pool: ユーザーコード(チャンネルのメソッド内のアプリケーションロジックなど)を実行するためのワーカープール
- 実務的には、大量接続・高頻度ブロードキャストを行う Action Cable アプリ(チャット、ライブ更新ダッシュボード、マルチプレイヤーゲームなど)では、この変更を含む Rails バージョンに上げることで、アプリ側のコード変更なしにレイテンシ改善が見込めます。
#58332 Revert "Remove migrations filter from the schema dump"
マージ日: 2026/7/31 | 作成者: @fxn
- 概要 (1-2文で)
Rails 7.2 付近で一度削除された「スキーマダンプから古いマイグレーションの version をフィルタする仕組み」を取り消し(revert)し、従来どおりフィルタを有効に戻した PR です。
チーム開発時に「古いマイグレーションをリポジトリから削除する」運用を、実務的に破綻なく行う手段がまだ整理できていないため、仕様変更を保留にする目的のリバートです。
- 変更内容の詳細
2-1. 背景・問題意識
Revert 対象の #58322 では、「db/schema.rb 生成時に、schema_migrations テーブルの全バージョン情報から、既にファイルが存在しない古いマイグレーション分を ‘フィルタして捨てる’ 挙動」を削除していました。
今回の PR はそれを元に戻します。
作者の懸念点は主に次です:
- 古いマイグレーションを削除したい典型的なユースケース(例: 数年分の履歴を整理してレポジトリを軽くする)が、フィルタがないと実務的にかなり難しい。
- 手順としては「古いマイグレーションファイル削除 →
db/schema.rbの末尾(trailer)からバージョンも削除 → それをコミット・デプロイ」が想定されるが、- チームの他のメンバーのローカルDBには、依然として古い
schema_migrationsレコードが残っている。 - その状態で
bin/rails db:migrateをすると、また古いバージョン情報がdb/schema.rbの末尾に書き戻され、「削除したはずの履歴」が復活してしまう。
- チームの他のメンバーのローカルDBには、依然として古い
- この問題を回避しようとして「古いバージョンを
schema_migrationsから削除するデータマイグレーション」を用意すると、- そのデータマイグレーション自体も “マイグレーション” であり、結局また履歴に残る。
- 誰かがそのマイグレーションを適用し忘れる(長期休暇等)と、その人だけ
schema_migrationsがクリーンにならない。
- 「削除済みマイグレーションのバージョン情報をどこかに別途保持しておいて、スキーマダンプ時に参照する」ようなメタ的仕組みも考えられるが、それは「履歴を repo から消したい」というモチベーションと相反する。
結局、「フィルタがある状態」の方が、古いマイグレーション整理のユースケースにとって現実的な落としどころだろう、という判断でリバートされています。
2-2. コードレベルの変更
変更ファイルは以下の3つです:
activerecord/lib/active_record/schema_dumper.rbactiverecord/test/cases/schema_dumper_test.rbguides/source/active_record_migrations.md
schema_dumper.rb
ここに「マイグレーションのフィルタ」を復活させるロジックが戻っています。
おおざっぱには、ActiveRecord::SchemaDumper が db/schema.rb を生成する際に行う:
schema_migrationsテーブル(および multi-DB 対応の場合はar_internal_metadataなど)からバージョンを取得- そのうち「現在レポジトリに存在するマイグレーションファイルに対応しない古いバージョン」を除外
- フィルタ済みのバージョンのみを
db/schema.rbの末尾(いわゆる trailer 部分)として出力
という挙動が再度有効になっています。
擬似コードで書くと、ざっくり次のようなイメージが復活した形です(正確なコードではありませんが挙動のイメージとして):
def dump_migration_versions(stream)
migrated = ActiveRecord::Base.connection.migration_context.get_all_versions
existing = ActiveRecord::Base.connection.migration_context.migrations.map(&:version)
# repo に存在するマイグレーションの version だけ残す
filtered = migrated & existing
stream.puts " # These are migrations that have been run"
filtered.sort.each do |version|
stream.puts " add_migration_version(#{version})"
end
endPR #58322 では、この filtered を使う処理を削って「フィルタせずにすべての version をダンプする」方向へ変えていましたが、本 PR で再び filtered を使う形に戻っています。
テスト (schema_dumper_test.rb)
- フィルタの存在を前提としていた既存テストの期待値が、#58322 で変更されていたのを、今回また元に戻しています。
- つまり「マイグレーションファイルが削除された version は、
db/schema.rbには出てこない」ことを確認するテストが再び有効に。
- つまり「マイグレーションファイルが削除された version は、
ガイド (active_record_migrations.md)
- 「
schema.rbにマイグレーションバージョンがどう出力されるか」「古いマイグレーションを削除した時にどう振る舞うか」に関する説明が、#58322 時点の内容から、従来どおり「フィルタが存在する」前提の文章に戻されています。 - ガイド上も、「古いマイグレーションを削除しても、
schema.rbの trailer には残さない(/フィルタされる)」挙動を前提とした説明になります。
- 影響範囲・注意点
3-1. 実務への影響
- アプリケーションで
schema_format = :ruby(デフォルト)を使っている場合:- 「古いマイグレーションファイルを削除すると、スキーマダンプ時に
schema_migrationsの対応する version も自動で落ちる(= trailer に残らない)」従来動作に戻ります。 - そのため、古いマイグレーションの「リポジトリからのクリーンな削除」が、相対的にやりやすくなる状態です。
- 「古いマイグレーションファイルを削除すると、スキーマダンプ時に
schema_format = :sqlを使っている場合:- このフィルタは
db/structure.sqlには適用されないため、:rubyと:sqlで挙動が揃わない状態のままになります。 - 作者もこの点は認識しており、「どちらのフォーマットが実務的なトレードオフとして適切か」はまだ結論が出ていないという含みがあります。
- このフィルタは
3-2. ブランチ切り替え時の注意
- #58322 が解決しようとしていたユースケースのひとつに「ブランチを切り替えたときに
db/schema.rbが ‘汚れる’」という問題がありますが、- 作者の認識では、「いずれにせよ
db/schema.rbはブランチ切り替えで汚れがちであり、不要な変更は捨てる(git checkout -- db/schema.rb等)運用になるので、フィルタ有無による差はそこまで決定的ではない」という整理。 - 文書(ガイド)も、なぜ version が揃わないことがあるのかを説明しているので、実務では「スキーマファイルは頻繁にリセットするもの」と割り切る前提で使うことになります。
- 作者の認識では、「いずれにせよ
3-3. マイグレーション整理フロー
今回のリバートにより、Rails チームとしては現時点で次のような運用を想定していると読めます:
- 古いマイグレーション群を削除する(ファイルを物理削除)
- チームメンバーが今後
db:migrate/db:schema:dumpするたび、schema.rbの trailer からそれら古いバージョンが自動で消えた形になる- ローカルDB には古い version が残っていても、
schema.rbには反映されない
- これにより、
- レポジトリ上の
schema.rbは「今もファイルが存在するマイグレーションだけ」を表す schema_migrationsテーブルがメンバーごとに必ずしも一致しなくても、運用上はさほど問題にならない
- レポジトリ上の
一方で、「schema_migrations テーブルを完全にクリーンにしたい」「バージョンを正規化したい」といった要求には、まだ標準で綺麗に答えられていない状態であり、その点を解くまでは仕様を動かさない、という判断でもあります。
- 参考情報 (あれば)
- Reverted PR:
- https://github.com/rails/rails/pull/58322
- 「schema dump からマイグレーションフィルタを削除する」試み。今回の PR で取り消し。
- https://github.com/rails/rails/pull/58322
- 本 PR:
- ガイド(該当セクションの最新内容はこの PR 適用後を参照):
guides/source/active_record_migrations.md(公式ガイドの「Active Record Migrations」)
#58274 Allow prepared statements with query log tags
マージ日: 2026/7/31 | 作成者: @bschrag620
- 概要 (1-2文で)
Railsでクエリログタグ(query_log_tags)を有効にした際でも、アプリ側の選択で prepared statements を無効化せずに使い続けられるオプションが追加されました。これにより、高カーディナリティなコメントを避けるという従来の安全デフォルトを維持しつつ、必要なアプリケーションは両方を併用できるようになります。
- 変更内容の詳細
背景 / 現状
これまでのRails(Active Record)の挙動:
query_log_tags_enabledを有効にすると、Railtie側でdisable_prepared_statementsが自動的にtrueに設定される。- 理由: クエリコメント(ログタグ)が毎回異なる値になると(高カーディナリティ)、prepareされたステートメントのキャッシュが効かなくなり、DB側でのプランキャッシュ・効率に悪影響が出るケースがあるため。
課題:
- とはいえ、プロジェクトによっては
- クエリログタグを使いたい
- かつ prepared statements も有効のまま使いたい というニーズがあるが、現状は両立できなかった。
- とはいえ、プロジェクトによっては
今回の変更ポイント
query_log_tags_enabledに3つ目の値:with_prepared_statementsを追加
- これまでの典型的な設定(
config/database.ymlなど):
production:
adapter: postgresql
# ...
query_log_tags_enabled: true # or false- 新しいオプション:
production:
adapter: postgresql
# ...
query_log_tags_enabled: :with_prepared_statements- 意味:
true- クエリログタグを有効化
- Railtie が
disable_prepared_statements = trueを自動設定(prepared statements を無効)
false- クエリログタグは無効
- prepared statements の挙動は各アダプタのデフォルト
:with_prepared_statements(新)- クエリログタグを有効化
- Railtie は
disable_prepared_statementsを触らない- つまり、prepared statements は 無効化されない(アダプタ/他の設定のデフォルトのまま)
- ActiveRecord Railtie のロジック変更
activerecord/lib/active_record/railtie.rbの中で行っている初期化処理が1行差し替えられ、query_log_tags_enabledが真値(true)のときにdisable_prepared_statementsを必ずtrueにしていたコードが、:with_prepared_statementsの場合はdisable_prepared_statementsを設定しないように分岐されました。
(PRのdiffは最小限ですが、Rails起動時に ActiveRecord::Base の config を組み立てる部分で、この新しいシンボル値を特別扱いする形です。)
- ドキュメント更新
guides/source/configuring.mdが更新され、query_log_tags_enabledの説明に:with_prepared_statementsという第三の選択肢が追加されています。- これにより、「タグを有効にしつつ prepared statements を維持したい場合の設定方法」が公式ガイド上で明示されました。
- テスト追加
railties/test/application/query_logs_test.rbに25行のテストが追加。- 想定される内容:
query_log_tags_enabled: :with_prepared_statementsを設定したアプリケーション構成で- クエリログタグが有効になっていること
disable_prepared_statementsが強制的にtrueにされていないこと を検証するテストケース。
- これにより将来のリグレッション(この新挙動が壊れること)を防止。
- 想定される内容:
- CHANGELOG 追加
activerecord/CHANGELOG.mdにエントリが追加され、新しいオプションの存在と挙動がリリースノートに反映されています。
- 影響範囲・注意点
既存アプリへの影響
query_log_tags_enabledをtrue/falseのまま使っている既存アプリには挙動変更はありません。true→ 従来どおり、prepared statements は自動的に無効化される。false→ 引き続きログタグは使われず、prepared statements も以前通り。
新しい値
:with_prepared_statementsを明示的に設定したアプリだけが、新しい挙動の恩恵を受けます。
このオプションを使うときの注意点
- 高カーディナリティなタグに注意
prepared statements を有効のままクエリログタグを付けると、
- クエリ毎に異なるコメント(例: リクエストID・ユーザーIDなど)が付与される場合、DB側から見ると
- SQLテキストが毎回違う →
PREPAREキャッシュの恩恵が薄くなる - プランキャッシュが大量に増える
- SQLテキストが毎回違う →
- といった問題が起こり得ます。
- クエリ毎に異なるコメント(例: リクエストID・ユーザーIDなど)が付与される場合、DB側から見ると
そのため:
- 付与するタグの粒度と種類を意識することが重要です。
- ○ より安全: コントローラ名 / アクション名 / 環境名 など低カーディナリティな情報
- △ 注意: ユーザーID / リクエストID / ジョブID / トレースID など高カーディナリティ情報
- 高カーディナリティな値はログ(アプリログ側)で別途持つか、タグの種類を厳選するのが望ましいです。
- 付与するタグの粒度と種類を意識することが重要です。
- DBアダプタごとの実装依存
:with_prepared_statementsによって Railtie がdisable_prepared_statementsを「設定しない」だけなので、- 実際に prepared statements が有効かどうかは
- アダプタのデフォルト設定
- その他の
config.active_record.disable_…系の設定 に依存します。
- 実際に prepared statements が有効かどうかは
- オラクル・MySQL・PostgreSQLなど、利用アダプタごとに prepared statements の有効化 / 無効化の方法・性能特性が異なる点に注意してください。
- 移行のステップ案
- もし「ログタグ + prepared statements」を有効にしたい場合:
- まずステージング環境で:
query_log_tags_enabled: :with_prepared_statementsを設定- 実際のSQLコメント(クエリログ)とDB側のプランキャッシュ状況・負荷を観測
- 高カーディナリティなタグを削る / 替える
- 問題なければ本番へ反映
- まずステージング環境で:
- 参考情報 (あれば)
- 対象PR:
- Rails GitHub: https://github.com/rails/rails/pull/58274
- 関連ドキュメント(更新あり):
Configuring Rails Applicationsガイド (guides/source/configuring.md) のquery_log_tags_enabledの項
- 概念的な背景:
- Railsのクエリログタグ機能そのもの:
ActiveRecord::QueryLogs - prepared statements とコメント付きSQLがプランキャッシュに与える影響については、各DB(PostgreSQL・MySQLなど)のマニュアルやブログ記事が参考になります。
- Railsのクエリログタグ機能そのもの:
#58267 activerecord: make sure tests do not rely on implicit SELECT order
マージ日: 2026/7/31 | 作成者: @viralpraxis
- 概要 (1–2文で)
このPRは、Active Record のテストが「暗黙の SELECT 結果順序(MVCC に起因する更新順など)」に依存していた部分を洗い出し、すべて明示的なORDER BYを付けるように修正するものです。これにより、DB 実装や実行タイミングによってテスト結果が変わる flakiness を防ぎ、テストの決定性・再現性を高めています。
- 変更内容の詳細
全体方針
- PostgreSQL の MVCC 特性(更新のタイミングによって行の見える順序が変わる)に依存していたテストを修正。
- 「ORDER BY を付けていない SELECT の結果順が、たまたま期待どおりになる」前提を排除。
- 具体的には:
- テスト内のクエリに
order(...)を追加 - 期待結果の配列順が、暗黙の順序ではなく、明示的なソート条件に対応するように調整
- テスト共通のヘルパーにも、順序の非決定性をあぶり出すための仕掛けが追加されている可能性が高い(
test_case.rbに +13 行)。
- テスト内のクエリに
PR 説明にある通り、この問題は PostgreSQL 拡張 pg_disorder の「reverse モード」で発見されています。pg_disorder は SELECT の返却順序を意図的に乱したり反転させることで、「暗黙のORDER BY依存」を検出するツールです。
ファイルごとの大まかな変更ポイント
※ソース全文は無いため、Rails のテスト慣習・ファイル名から推測を交えていますが、方向性は技術的に妥当なものです。
1. associations 関連テスト
activerecord/test/cases/associations/eager_test.rbactiverecord/test/cases/associations/has_many_associations_test.rbactiverecord/test/cases/associations/has_many_through_associations_test.rbactiverecord/test/cases/associations/join_model_test.rbactiverecord/test/cases/autosave_association_test.rb
これらは eager loading (includes, preload, eager_load) や has_many, has_many :through, join model、autosave など、関連を伴う読み出しをテストするファイルです。多くのテストは以下のような構造をとります:
posts = author.posts # または Author.includes(:posts).find(...)
assert_equal [post1, post2], postsこの posts の順序が、DB 依存で変わらないことを暗黙に期待していた箇所に、明示的な order が追加されたと考えられます:
posts = author.posts.order(:id)
# あるいは includes した relation 側に order を付与
authors = Author.includes(:posts).order(:id)これにより、関連ロード後の配列の順序が、テーブルの挿入順や MVCC の更新順に依存せず、一貫して id 等でソートされるようになります。
2. insert_all 関連
activerecord/test/cases/insert_all_test.rb
insert_all や upsert_all では、一括挿入後にレコードを SELECT してアサートするテストがよくあります:
result = Book.insert_all([...])
books = Book.where(id: result.rows.flatten)
assert_equal [1, 2, 3], books.map(&:id)こうした箇所に
books = Book.where(id: result.rows.flatten).order(:id)のような order が追加されていると考えられます。
特に RETURNING 句が関与する PostgreSQL では、返却順と実際のテーブルスキャン順が一致しない場合もあり、ここが暗黙の依存ポイントになりがちです。
3. relation / query 関連テスト
activerecord/test/cases/relation/load_async_test.rbactiverecord/test/cases/relation/or_test.rbactiverecord/test/cases/relations_test.rb(+15 / -15 と変更が大きい)
Relation の動作を検証するテスト群では、where, or, load_async, limit, distinct などの組み合わせで得られた結果配列の順序をアサートします。
ここで暗黙順序に依存していたケースに対し:
Model.where(...).order(:id)やorder("some_column")- もしくはテスト対象として意図した順序(例えば
name ASC, created_at DESCなど)
を明示的に付けたと考えられます。
特に relations_test.rb は Relation の基本動作のハブのようなファイルであり、さまざまなクエリパターンについて:
assert_equal [developers(:david), developers(:jamis)], Developer.where(...).to_aといった順序依存の検証が多いので、ここに 15 箇所程度 order の追加・修正が入った可能性が高いです。
4. scoping 関連
activerecord/test/cases/scoping/default_scoping_test.rbactiverecord/test/cases/scoping/named_scoping_test.rb
default_scope や named scope が順序を付与・合成した結果のテストで、「scope が暗黙的にある順序を前提にしている」ように見えるものを是正したとみられます。
例として:
default_scope { where(active: true) } # だけを期待していたテストで暗黙に挿入順を前提にしているケースがあれば、テスト側で
expected = users.where(active: true).order(:id)
assert_equal expected, User.all.to_aのように明示的に順序を固定しています。
5. message pack / unsafe_raw_sql 関連
activerecord/test/cases/message_pack_test.rbactiverecord/test/cases/unsafe_raw_sql_test.rb
message_pack_test は Active Record オブジェクトの MessagePack シリアライズ/デシリアライズやバルクロードをテストしているため、結果の配列順がそのままバイナリの順序にも影響します。ここで暗黙順序に依存していた部分に order が入ったと考えられます。
unsafe_raw_sql_test では、生 SQL を使ったクエリや危険な SQL 呼び出しの検査を行っています。ここでも:
records = Developer.find_by_sql("SELECT * FROM developers WHERE ...")
assert_equal [developers(:david), developers(:jamis)], recordsのような記述に対し、SQL 自体に ORDER BY が足される、または find_by_sql の結果に対して Ruby レベルで sort_by(&:id) 相当を適用する、といった修正が行われている可能性があります。
6. test_case.rb の変更 (+13 行)
activerecord/test/cases/test_case.rb
テスト共通のベースクラスです。+13 行という比較的大きな追加から、次のようなヘルパーが追加された可能性があります:
- 「順序に意味のない」比較のためのヘルパー:
- 例:
assert_equal_unordered expected, actualのようなメソッド(sort_by(&:id)して比較など)
- 例:
- または、テスト中に Relation から
to_aする際に意図しない暗黙順序依存を検出・補助するユーティリティ。
Rails コアのテストでは、この種のヘルパーを通して配列比較を行うことで、将来の暗黙順序依存を減らす方針がとられることが多いです。
- 影響範囲・注意点
- 対象は「Rails コアの Active Record テストコード」のみであり、アプリケーションの本番コードの挙動は変わりません。
- ただし、テストが
SELECTの暗黙順序に依存していると不安定になる、という問題意識はそのままアプリ側にも当てはまります:where,joins,includesなどを使ったクエリの結果順を前提にロジックやテストを書いている場合、DB のバージョンや負荷状況によって結果順が変わる可能性があります。- 本番・CI でたまにテストが落ちる「たまたま動いているだけ」の状態が潜んでいるかもしれません。
- Rails コアがこのように修正したということは、将来的な Rails の内部実装変更(例えば、並列クエリ、最適化、DB のバージョンアップ)に備えて、「
ORDER BYなしの順序は信頼してはいけない」というメッセージとも受け取れます。
アプリ開発者の実務的な注意点:
- 「順序が大事なクエリ」…必ず
orderを明示する:- 例:
User.where(active: true)ではなくUser.where(active: true).order(:id)またはorder(:created_at)など。
- 例:
- テストで配列比較をする際:
- 順序そのものを仕様としてテストしたい → クエリ側で
orderを明示。 - 順序はどうでもよく、要素集合だけ検証したい →
sort_byしてから比較する /match_array(RSpec)等を使う。
- 順序そのものを仕様としてテストしたい → クエリ側で
- 参考情報 (あれば)
- 対象 PR:
pg_disorder拡張:- https://github.com/viralpraxis/pg_disorder
- PostgreSQL の SELECT 結果順を意図的に乱し、「暗黙の ORDER BY 依存」を見つけるための拡張。今回の PR はこれを reverse モードで使うことで、テスト上の問題箇所を検出しています。
- 一般的な原則:
- SQL 標準では、
ORDER BYを指定しない限り SELECT の結果順は未定義であり、DB 実装や実行計画、同時実行状況に依存して変化しうる、という前提に立ってクエリとテストを書く必要があります。
- SQL 標準では、
#58297 Deprecate positional pk, id_value, and sequence_name for #insert
マージ日: 2026/7/31 | 作成者: @kamipo
- 概要 (1-2文で)
ActiveRecord::ConnectionAdapters#insertの位置引数として使われていたpk,id_value,sequence_nameが非推奨になり、それぞれに対応する新しい使い方(特にreturning:オプション)への移行が明示されました。これにより、insertのインターフェイスが整理され、PostgreSQL のcurrvalベースのフォールバックなども段階的に廃止されます。
- 変更内容の詳細
2-1. 廃止される位置引数
対象は ActiveRecord の low-level な insert メソッド(アダプタ実装)です。従来は概ね次のようなシグネチャで呼ばれていました(アダプタによって差異あり):
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
...
endこのうち、以下3つの位置引数が「非推奨」として警告されます。
(1) pk
役割:
- RETURNING するカラムを指定し、そのカラムの値を
insertの戻り値として返すために使われていた。 - 例:
pk = "id"のとき、insertは挿入したレコードのidを返す。
- RETURNING するカラムを指定し、そのカラムの値を
新しい対応:
returning:オプションで同じことができます。returning:が単一カラム名を受け取った場合、戻り値も「単一値」になるように整備されました。- つまり、
pk = "id"は論理的にはreturning: "id"に1対1で置き換え可能です。
移行イメージ:
# 旧 (非推奨): 第3引数 pk で RETURNING カラムを指定
connection.insert(sql, "SQL", "id")
# 新: returning: オプションを使用
connection.insert(sql, "SQL", returning: "id")
# => 挿入された行の id を返す(戻り値の形は旧仕様と同じ単一値)(2) id_value
役割:
- DB が last inserted id を計算できない場合(RETURNING も currval も使えない等)に、呼び出し側が明示的に「この値が主キーだ」と教えるためのもの。
- その場合、
insertの戻り値としてid_valueがそのまま返ってきていた。
PRでの指摘:
id_valueを渡せるということは、呼び出し側はすでにその値を知っている。- であれば、
insertの戻り値を経由して読み直す必要は無く、そのまま自分で使えばよい。
移行方針:
# 旧 (非推奨): id_value を渡して、それを戻り値として受け取る想定
id = some_known_id
returned = connection.insert(sql, "SQL", nil, id)
# returned == id
# 新: insert の戻り値に依存しない
id = some_known_id
connection.insert(sql, "SQL") # 戻り値は無視する/使わない
# 以降は自前で持っている id を使う(3) sequence_name
- 役割:
- PostgreSQL で
use_insert_returning?がfalseの場合に、currval(sequence_name)経由で last insert id を取得するフォールバックのために使われていた。
- PostgreSQL で
- 状況:
use_insert_returning?のcurrvalフォールバック自体が非推奨になっている。- したがって、それを支える引数
sequence_nameも非推奨。
ポイント:
- PostgreSQL のアダプタでは、
@use_insert_returning = falseという、すでに非推奨の設定パスが存在します。 - このパスでは内部的に
sequence_nameをpkから解決し、super経由で再度sequence_name警告が出る可能性があります。 - PRではこれを「すでに非推奨な設定パスで発生する追加ノイズ」として容認しています。
2-2. 非推奨警告の出し方
pk,id_value,sequence_nameはそれぞれ独立にチェックされ、個別に警告が出るようになっています。- これにより、どの引数に対して、どんな移行パスを取るべきかが明示されます。
pk→returning:を使うid_value→insertの戻り値に頼らず、その場で持っている値を使うsequence_name→use_insert_returningに依存しないパスへ移動(一般には RETURNING ベース)
2-3. テストと CHANGELOG
activerecord/CHANGELOG.mdに上記非推奨が追記。- 以下のテストが修正・追加され、非推奨仕様に依存しないように変更:
adapter_prevent_writes_test.rbsqlite3_adapter_test.rbdatabase_statements_test.rb(非推奨関連の挙動をカバー)
- 影響範囲・注意点
3-1. 影響がある可能性が高いコード
- ActiveRecord の public API (
Model.createなど) を普通に使っているだけのアプリケーションは、ほぼ影響を受けません。 - 影響するのは次のような「アダプタ層や低レベルAPIを直接触っている」コードです。
例:
# 典型的な low-level な insert 呼び出し
connection.insert(sql, "CustomInsert", "id", 123, "my_seq")- こうした呼び出しは非推奨警告の対象になります。
3-2. 移行パスのまとめ
pkを使っていた場合:insert(sql, name, "id", ...)→insert(sql, name, returning: "id", ...)へ変更。
id_valueを使っていた場合:insertの戻り値としてid_valueを再取得する設計をやめ、元々持っている値を直接使う。- どうしても戻り値が欲しい場合は、
returning:で DB に生成させる/返させる形の設計に変更する。
sequence_nameを使っていた場合:- PostgreSQL で
currvalに依存する設計を捨て、RETURNINGベース(use_insert_returning?を true にする一般的な経路)に移行する。 - 必要に応じて
returning:を併用。
- PostgreSQL で
3-3. 非推奨警告のノイズについて
- 特に PostgreSQL +
@use_insert_returning = falseを明示的に使っている環境では、pk→ 内部でsequence_name解決 →sequence_name警告、と二重に近い形で警告が出ることがあります。
- PRの説明では、そもそもその設定自体が非推奨なので、この追加ノイズは許容範囲とされています。
- 将来的には、このパス自体が削除される可能性が高いと考えてよいです。
- 参考情報 (あれば)
- PR本体:
https://github.com/rails/rails/pull/58297 - 関連しうるドキュメント/コード:
ActiveRecord::ConnectionAdapters::DatabaseStatements#insert- 各アダプタ (
PostgreSQLAdapter,SQLite3Adapterなど) のinsert実装
- 傾向として:
- Rails は
insertの low-level API から「位置引数での特殊パラメータ指定」を減らし、オプションハッシュ(returning:など)ベースに寄せていく方向です。 - PostgreSQL の
currvalフォールバックも順次廃止され、INSERT ... RETURNINGを前提にした実装に統一されつつあります。
- Rails は
#58322 Remove migrations filter from the schema dump
マージ日: 2026/7/31 | 作成者: @fxn
- 概要 (1-2文で)
Rails の schema dump(db/schema.rbなど)に含める「適用済みマイグレーションのバージョン一覧」から、「ローカルにファイルが存在するものだけに絞り込む」というフィルタリング処理が削除されました。これにより、マイグレーションファイルが手元に無くても、DB 上で適用済みであればそのバージョンは schema dump に書き出されるようになります。
- 変更内容の詳細
これまでの挙動(フィルタあり)
- schema dumper は「
schema_migrationsテーブルに記録されているバージョン」のうち、- 実際に
db/migrate(または設定されたパス)にマイグレーションファイルが存在するバージョンだけを、schema.rbの末尾(trailer)に記録していました。
- 実際に
- その意図:
- ローカルに存在しないマイグレーションを dump に含めないことで「きれい」に保つ。
- 古いマイグレーションを「掃除」したい場合、ファイルを削除して
db/schema.rbを再生成すれば trailer からも消える、という運用を想定。
イメージ (以前の schema.rb の末尾例):
# ActiveRecord::Schema[7.2].define(version: 20260731000000) do
# ...
# end
# 以下のようなコメント行(例)
# Active Record::SchemaMigration.create!(version: "20240101000000")
# Active Record::SchemaMigration.create!(version: "20240202000000")
# ※ ただし、対応するマイグレーションファイルが存在しないバージョンはここに含めない今回の変更後の挙動(フィルタ削除)
schema_migrationsテーブルに登録されている すべてのバージョンが schema dump に出力されます。- もはや「ローカルにマイグレーションファイルが残っているかどうか」は考慮しません。
- 実装上は
schema_dumper.rbから、ファイル存在チェックに基づく絞り込みロジックが削除されています(約 10 行削除、3 行追加程度の軽微なコード変更)。
なぜフィルタをやめたのか(PR 本文の論点)
ブランチをまたぐと dump が「自己完結しなくなる」問題
- 例:
- ブランチ A でマイグレーション X を作成 → テーブル T を追加 → マイグレーション X は
schema_migrationsに記録される。 - ブランチ B に切り替えると、
db/migrateからマイグレーション X のファイルが無くなる。 - そこで
rake db:schema:dumpをすると:- テーブル T は schema に含まれる(DB には存在するため)。
- しかし、trailer にマイグレーション X のバージョンは 書かれない(ファイルが無いから)。
- ブランチ A でマイグレーション X を作成 → テーブル T を追加 → マイグレーション X は
- 結果として、「この schema を適用した状態の DB には T があるが、その状態に至るマイグレーション履歴情報が dump に反映されていない」という一貫性のない状態になる。
- PR 作者はこれを「
db/schema.rbが自己完結していない」と見なしています。
- 例:
:sqlフォーマットとの挙動差schema_format = :sql(structure.sqlなど)の場合は、DB 側の状態がそのまま出力され、ローカルのマイグレーションファイル有無は関係ありません。:rubyの schema dump(schema.rb)だけが「ローカルファイルの有無でバージョンを削る」というのは一貫性が無い、という判断です。
この 2 点から、「フィルタリングはやめて、DB 上の schema_migrations をそのまま出すほうが妥当」という結論になっています。
テスト・ドキュメントの更新
activerecord/test/cases/schema_dumper_test.rb- ローカルのマイグレーションファイル有無に依存していたテストを修正し、今後は「
schema_migrationsにあるものは dump される」前提に変更。
- ローカルのマイグレーションファイル有無に依存していたテストを修正し、今後は「
guides/source/active_record_migrations.md- マイグレーションと schema dump の関係に関する説明を、この新しい挙動に沿うように微修正。
activerecord/CHANGELOG.md- 上記仕様変更が記録されています(7.2 or main の該当セクションに追記)。
- 影響範囲・注意点
影響する人
schema_format = :ruby(デフォルト)でdb/schema.rbを利用しているプロジェクト全般。- 特に:
- 古いマイグレーションファイルを削除して「schema の trailer からも消えてほしい」と期待していた運用。
- ブランチを頻繁に切り替えつつ
db/schema.rbを commit しているチーム。
挙動の変化による具体的影響
古いマイグレーションファイルを削除しても trailer からは消えない
- 以前:
db/migrate/20200101000000_old_migration.rbを削除bin/rails db:schema:dumpdb/schema.rbの末尾から"20200101000000"が消える
- 今後:
- DB の
schema_migrationsに"20200101000000"が残っている限り trailer にも残る。
- DB の
- 「バージョン一覧を物理ファイルの存在と同期させる」という使い方はできなくなります。
- バージョンを本当に消したい場合は、
schema_migrationsテーブルからバージョンを消さない限り dump からは消えない(※通常は直接いじるべきではないので、実務上は「残しっぱなし」にするのが安全)。
- 以前:
ブランチ間での不整合は緩和される
- 以前は、あるブランチで適用されたマイグレーションが別ブランチでは「trailer に現れない」という状態になり得ました。
- 今後は、「DB に適用されている限りどのブランチから dump しても同じバージョンが trailer に出る」ことになり、schema ファイルの自己完結性が上がります。
:sqlフォーマットとの整合性が向上:sqlと:rubyで大きくポリシーがズレることがなくなり、ツール連携や挙動理解がしやすくなります。
注意 / 実務上のポイント
db/schema.rbの差分が少し増える可能性:- これまで「ローカルに無いマイグレーションは trailer から消えていた」プロジェクトでは、今後それらが再び出力されるため、PR 差分として「マイグレーションバージョン行が増えた」ように見えることがあります。
- CI で
db/schema.rbの内容チェックをしている場合:- テストセットアップ時にどの DB を元にしているか(
schema:load/migrate/ スナップショットなど)によってschema_migrationsに残るバージョンが違うと、dump 結果の差異として現れ得るので注意が必要です。
- テストセットアップ時にどの DB を元にしているか(
- 「古いマイグレーションの整理」のベストプラクティス:
- これまで「ファイルを消すと schema のバージョンからも消える」という前提で運用していた場合は、方針を見直す必要があります。
- そもそも Rails の公式スタンスとしても、「過去マイグレーションはむやみに削除しない」が推奨寄りです(新規参加者が
db:setupできなくなるなどの問題もあるため)。
- 参考情報 (あれば)
- 対象 PR:
- rails/rails #58322: Remove migrations filter from the schema dump
- 関連する設定:
config.active_record.schema_format = :rubyor:sql
- ドキュメント:
guides/source/active_record_migrations.md(今回の PR で内容が更新)
- 実装箇所:
activerecord/lib/active_record/schema_dumper.rb
→schema_migrationsの扱いに関するフィルタロジックが削除されている部分が中心。
#57948 Make the Action View dependency tracker registry Ractor-shareable
マージ日: 2026/7/31 | 作成者: @etiennebarrie
- 概要 (1-2文で)
ActionView のテンプレート依存関係トラッカー (ActionView::DependencyTracker) のレジストリを Ractor で共有可能(ractor-shareable)にするための変更です。Rails.application.ractorize!を呼んだアプリケーションに限り、依存関係トラッカーのレジストリを Ractor 間で安全に共有できるようになります。
- 変更内容の詳細
2-1. 依存関係トラッカーのレジストリを Ractor-shareable に
対象クラス: ActionView::DependencyTracker
もともと DependencyTracker は、テンプレートハンドラ(ERB, HAML など)ごとに「どのトラッカーを使うか」を登録するレジストリを持っています。このレジストリを Ractor で共有できるよう、以下の対応が行われています。
share_registry メソッドの導入
PR 説明中では一時的に freeze_registry と書かれていましたが、最終的には share_registry というメソッドが導入されています(名前が示す通り「Ractor.shareable! できる状態にする」ことが目的)。
イメージとしては以下のような処理が行われるメソッドです(概念的なサンプル):
module ActionView
class DependencyTracker
class << self
def share_registry
# 例: ハッシュや配列等のレジストリを凍結したり、
# Ractor.shareable!(registry) 可能な状態に変換したりする
@dependency_trackers.freeze
@cache_keys.freeze
# 必要に応じて個々の要素も freeze / shareable 化
end
end
end
end実際の実装では、内部に保持しているレジストリ構造を Ractor-shareable な状態(イミュータブル or Ractor.shareable! 済み)に整える処理が入っています。
2-2. Rails.application.ractorize! からレジストリを共有可能にする
railties/lib/rails/application.rb に 1 行だけ変更が入っています。
ここで Rails.application.ractorize! が ActionView 側の share_registry を呼び出すようになっています。
ざっくり言うと:
class Rails::Application
def ractorize!
# ほかのコンポーネントの Ractor 対応処理…
ActionView::DependencyTracker.share_registry
end
endという流れです。
つまり:
- デフォルト: レジストリは従来どおり可変(Mutable)のまま。Ractor-shareable にされない。
Rails.application.ractorize!呼び出し時: ActionView の依存関係トラッカーレジストリが Ractor-shareable にされる。
既存アプリへの互換性確保のため、「Rails を Ractor 対応モードにする」と明示したアプリだけがこの制約を受けるように設計されています。
2-3. 設定完了後の DependencyTracker 変更を非推奨に
PR 説明より:
I also deprecated mutating the dependency tracker after configuration.
これに伴い、アプリケーションの設定が完了した後に DependencyTracker を変更することが非推奨 (deprecated) になりました。
具体的には、
- レジストリに新しいトラッカーを追加する
- 既存のトラッカー登録を変更する
といった操作を、Rails の設定フェーズ完了後に行うと非推奨警告が出るような挙動になっています。
ただし:
it's still possible to add an unshareable tracker for an unshareable handler, for compatibility reasons.
とあるように、互換性のために以下は依然として許容されています:
- Ractor-shareable でないハンドラ用に、Ractor-shareable でないトラッカーを後から追加する
これは「Ractor を使わない従来のアプリ」が壊れないようにするための妥協点です。
一方で、ractorize! を使って Ractor 化を有効にしたアプリでは、そのレジストリが共有対象になるため、後からミューテーション(変更)することは基本的に NG と考えたほうが安全です。
2-4. CHANGELOG とテストの追加
actionview/CHANGELOG.mdに、この変更(Ractor-shareable な DependencyTracker レジストリ & 変更の deprecate)が追記されています。actionview/test/template/dependency_tracker_test.rbに、Ractor-shareable 化・deprecation 挙動・互換性周りをカバーするテストが大量に追加されています(+117 行)。- レジストリを shareable にしたとき、Ractor.shareable? が真になるか
- 設定完了後にミューテーションを行ったとき、deprecation が出るか
- 非 shareable なトラッカーを非 shareable なハンドラに追加するケース等の互換性確認
- 影響範囲・注意点
3-1. ractorize! を使うアプリへの影響
Rails.application.ractorize! を呼ぶアプリケーションでのみ、ActionView::DependencyTracker のレジストリが Ractor-shareable になります。
その場合:
- レジストリは 基本的にイミュータブル とみなされるべきです。
- Ractor 化後に、
ActionView::DependencyTracker.register_tracker等で mutate すると、- Ractor 的に危険(共有オブジェクトの書き換え)
- deprecation 警告
の両面で問題になり得ます。
3-2. 設定フェーズ後のトラッカー登録は非推奨
次のようなコードを書いている場合は要注意です:
# 例: 初期化完了後のどこか(遅延ロード時など)での追加
ActionView::DependencyTracker.register_tracker(:erb, MyCustomTracker)今後は、次のような方針が推奨されます:
- アプリケーション設定フェーズ(config/initializers 内など)で登録を完了させる
- 特に
ractorize!を使う予定があるアプリでは、「起動後に動的にトラッカー追加・上書き」しないよう設計を見直す
3-3. Ractor 非利用アプリの互換性
Rails.application.ractorize!を呼ばない限り、レジストリは従来通り可変です。- 非 shareable なトラッカーを、非 shareable なハンドラに後から足すというパターンも動き続けます。
- ただし、「設定完了後の変更」は deprecation(将来的な削除・挙動変更の予告) である点に留意してください。
将来のメジャーバージョンで、完全に禁止される可能性があります。
- 参考情報 (あれば)
- この PR:
- 関連 PR (
Rails.application.ractorize!の導入): - Ractor(Ruby 並行実行モデル)の概要:
Ractorはオブジェクト共有に厳しい制限があり、Ractor.shareable!(obj)で共有可能状態にしたオブジェクトや、完全にイミュータブルなオブジェクトのみを複数 Ractor 間で共有できる。そのため、この PR のようにフレームワーク内部のレジストリを「共有可能状態」に整える必要がある。
#58312 Show parameter marker for casted binds in SQL logs and EXPLAIN
マージ日: 2026/7/31 | 作成者: @kamipo
- 概要 (1-2文で)
[[column, value], ...]形式から移行中の「casted binds」(単なる値配列)について、SQLログおよびEXPLAIN出力で[[nil, value], ...]と表示されていた問題を、位置パラメータ($1,$2, ...)を表示するようにして可読性を改善したPRです。PostgreSQL風のパラメータマーカーを導入し、どの値がクエリ中のどのプレースホルダに対応するか一目で分かるようにしています。
- 変更内容の詳細
背景: casted binds とログ表示の問題
既存: レガシーな binds 形式
ruby# 旧形式 (レガシー) # [[column, value], [column, value], ...]ここでは
columnオブジェクトがあるため、ログや EXPLAIN 時に[[column_name, value], ...]のように、人間が読める形で表示できていました。新形式: casted binds
ruby# 新形式 (casted binds) # [value1, value2, ...] # 単なる値の配列この形式は #39106 で導入されたもので、将来的にこちらへ移行する前提ですが、
column情報がないため、これまでログでは次のように出ていました:sqlSELECT * FROM topics WHERE title = ? [[nil, "abcd"]]nilが先頭に出ており、「どのパラメータに対応しているか」が分かりにくい状態でした。
このPRでの主な変更点
StructuredEventSubscriber#sql のログ表示
casted binds(カラム情報なし)の場合、
[[nil, value], ...]ではなく
パラメータ位置をキーにして表示するように変更されています。例:
sql# 変更前 (イメージ): SELECT * FROM topics WHERE title = ? [[nil, "abcd"]] # 変更後: SELECT * FROM topics WHERE title = $1 [["$1", "abcd"]]ポイント:
$1,$2, ... のような PostgreSQL 風のパラメータマーカー を採用。- 実際の SQL が
?プレースホルダを使うアダプタ(例: MySQL、SQLite)であっても、 ログ上では$1表記となり、「ログを見る人間」がパラメータ位置を即座に把握できるようにする狙い。
Explain#exec_explain の出力
EXPLAINを実行する際に付随して出るバインド値についても、同様に$1,$2形式で表示されるように変更。- casted binds でも、EXPLAIN 出力のバインドリストが self-describing になります。
実装上の要点 (推測される変更内容)
実際のコードは以下のようなイメージで実装されています(擬似コード):ruby# もともと: [[nil, value], ...] として扱われていたケースに対し、 # インデックスを使って "$1", "$2" ... を生成 casted_binds.each_with_index.map do |value, index| name = "$#{index + 1}" [name, value] end- これを StructuredEventSubscriber および Explain の双方で利用し、 casted binds に対する人間可読な表示を統一しています。
テストの追加・更新
activerecord/test/cases/log_subscriber_test.rb- SQL ログの出力に
$1スタイルのマーカーが含まれていることを確認するテストが追加。
- SQL ログの出力に
activerecord/test/cases/explain_test.rb- EXPLAIN 出力でも
$1マーカーが表示されることを確認するテストが追加。
- EXPLAIN 出力でも
bind_parameter_test.rbも軽微に更新され、bind 表示の期待値が新形式に合わせられています。
- 影響範囲・注意点
影響を受ける箇所
- Rails の ActiveRecord ログ (
StructuredEventSubscriber#sqlが吐く SQL ログ)。 Model.explain,relation.explainなどで表示される EXPLAIN 出力のバインドリスト。
- Rails の ActiveRecord ログ (
既存コードへの互換性
- 実際の SQL 発行方法・パラメータバインディングの挙動自体は変更されていません。
- 変更されるのは「ログおよび EXPLAIN における表記のみ」です。
- ただし、ログ出力フォーマットの文字列が変わるため、以下のようなケースでは影響があります:
- ログを正規表現などで機械的にパースしているツールやスクリプト
- 監査・モニタリング用にログフォーマットへ依存しているミドルウェア
- これらのツールは、
[[nil, "..."]]を前提にしていた場合、[["$1", "..."]]形式に対応するよう修正が必要になります。
デバッグ・調査のしやすさの向上
- SQL 中の
?とログのバインド値リストの対応関係が明示されるため、 特に複数の?が並ぶクエリや、長いクエリの調査がかなり楽になります。 - 使用DBアダプタに依存しない表記(常に
$n)なので、PostgreSQL 以外でも 「n番目のパラメータ」という意味で一貫性が保たれます。
- SQL 中の
- 参考情報 (あれば)
- 元のPR:
- Show parameter marker for casted binds in SQL logs and EXPLAIN (#58312)
- 関連PR:
- Casted binds 導入の PR: #39106
→[[column, value], ...]形式から値だけの配列への移行文脈。
- Casted binds 導入の PR: #39106
- 関連クラス / メソッド:
ActiveRecord::Explain#exec_explainActiveRecord::StructuredEventSubscriber#sql- テスト:
activerecord/test/cases/explain_test.rbactiverecord/test/cases/log_subscriber_test.rbactiverecord/test/cases/bind_parameter_test.rb