Ruby on Rails PR Digest - 2026年 3月
このページは rails/rails リポジトリにマージされたPull Requestを自動的に収集し、AIで要約したものです。
#57096 Ignore schemas when validating table name lengths for PostgreSQL
マージ日: 2026/3/30 | 作成者: @fatkodima
- 概要 (1-2文で)
PostgreSQL でテーブル名の長さを検証する際に、schema_name.table_nameのようなスキーマ部分を含めず、純粋なテーブル名だけで長さチェックするように修正した PRです。これにより、スキーマ名を含めたフル修飾名が 63 文字を超えていても、テーブル名自体が PostgreSQL の制限以内ならマイグレーションが通るようになります。
- 変更内容の詳細
2-1. 何が問題だったか
PostgreSQL には「識別子は 63 文字まで」という制限がありますが、この PR が入る前は、ActiveRecord がテーブルを作成する際に、
"schema_name.table_name"のようなスキーマ付きフルネーム全体を対象に長さチェックをしていました。
そのため例えば、
- スキーマ名:
very_long_schema_name - テーブル名:
not_so_long_table
のように、テーブル名自体は短くても、very_long_schema_name.not_so_long_table というフルネームが 63 文字を超えると、ActiveRecord 側で「長すぎる」と弾かれてしまうケースがありました。
しかし PostgreSQL 自体は、識別子制限をテーブル名部分に対して適用する(スキーマ名とテーブル名はそれぞれ別の identifier)ので、これは Rails 側の過剰な制約でした。
2-2. 修正内容 (PostgreSQL アダプタ)
変更ファイル:activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb (+5)
ここにある「テーブル名の長さを検証するロジック」が修正されています。
新しいロジックの要点:
schema.table形式で渡される名前から、スキーマ名を切り離して、テーブル名部分だけを取り出す。- PostgreSQL の識別子長制限(通常 63 文字)と比較する際には、このテーブル名だけを対象とする。
- スキーマ部分は、長さチェックの対象から完全に除外。
擬似コードで表すと、だいたい次のようなイメージです(実際のコードとは多少異なりますが、意図を示すため):
def validate_table_length(name)
schema, table = name.to_s.split(".", 2)
table_name = table || schema # スキーマ指定なしならそのまま
if table_name.length > max_identifier_length
raise ArgumentError, "Table name '#{table_name}' is too long"
end
end2-3. テストの追加・変更
変更ファイル:activerecord/test/cases/migration_test.rb (+6/-1)
テストでは、おそらく以下のようなケースがカバーされています:
- スキーマ名が長く、テーブル名は制限以内のケースを用意
- それを用いた
create_table "long_schema.short_table"のようなマイグレーションがエラーにならず成功することを検証 - 逆に、テーブル名自体が長すぎる場合にのみエラーになることを確認
イメージ例:
def test_long_schema_does_not_affect_table_name_length_validation
long_schema = "a" * 40
short_table = "b" * 30 # PostgreSQL の制限 63 文字以内
# 以前は long_schema.short_table の全体長でエラーになっていたが、
# 今は short_table 部分のみでチェックするため成功する
assert_nothing_raised do
create_table "#{long_schema}.#{short_table}" do |t|
t.string :name
end
end
end- 影響範囲・注意点
対象:
activerecordの PostgreSQL アダプタを使っているアプリケーション- 特に 複数スキーマを使っている場合や、スキーマ名が長くなりがちな設計で影響が出ます。
期待できる改善:
- これまで「テーブル名が長すぎる」として Rails に弾かれていたが、PostgreSQL 的には合法だったテーブル作成が問題なく通るようになります。
- 既存のアプリで、長いスキーマ名を使ってマイグレーションが失敗していたケースが解消される可能性が高いです。
注意点:
- この変更は バリデーションの対象を PostgreSQL の実仕様に合わせたものであり、緩め過ぎているわけではありません。
- テーブル名自体が 63 文字(DB の
max_identifier_length)を超えていれば、これまで通りエラーになります。 - もしアプリ側で「フルネーム(schema.table)の長さを独自ルールで制限したい」場合は、この PR とは別にアプリケーションレベルで検証を入れる必要があります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57096
- 関連 Issue:
Fixes #57093(テーブル名長チェックでスキーマ名まで含めてしまう問題の報告) - PostgreSQL 公式ドキュメント(識別子長制限):
- "Identifiers and Key Words" —
NAMEDATALENによりデフォルト 63 文字(コンパイル時定数)として制限される仕様が説明されています。
- "Identifiers and Key Words" —
#57097 ActiveStorage: skip associated tests when a dependency is missing
マージ日: 2026/3/30 | 作成者: @byroot
- 概要 (1-2文で)
Active Storage の一部テスト(動画アナライザ/プレビュー、MuPDF プレビュー)について、必要な外部依存ライブラリがインストールされていない環境ではテスト自体をスキップするようにした変更です。これにより、Active Storage の小変更を行う際に、全ての外部ツールをローカルに入れなくてもテストが実行しやすくなります。
- 変更内容の詳細
変更対象は以下の3ファイルです。
activestorage/test/analyzer/video_analyzer_test.rbactivestorage/test/previewer/mupdf_previewer_test.rbactivestorage/test/previewer/video_previewer_test.rb
いずれも「関連する外部コマンドやライブラリが無い場合はテストをスキップする」というガードを追加する修正です。
実際のコードは概ね次のようなパターンになっていると考えられます(あくまでイメージです):
# 例: video_analyzer_test.rb
require "test_helper"
return unless ActiveStorage::Previewer::VideoPreviewer.enabled?
class ActiveStorage::Analyzer::VideoAnalyzerTest < ActiveSupport::TestCase
# テスト本体
endあるいは、skip を用いる形式の可能性もあります:
class ActiveStorage::Analyzer::VideoAnalyzerTest < ActiveSupport::TestCase
setup do
skip "ffmpeg is not installed" unless ActiveStorage::Analyzer::VideoAnalyzer.accept?(fixture_file)
end
# テスト本体
endMuPDF のプレビューや動画プレビュー用テストについても同様に、
- MuPDF(
mutoolなど) - 動画処理系ツール(
ffmpegなど)
といった外部依存ツールが無い場合はテストを実行せずスキップするようにしています。
ポイント:
- 「依存がないと落ちるテスト」から「依存がないと実行されないテスト」へと振る舞いが変わっています。
- 実装としては
return unless ...やskip unless ...のようなガードがそれぞれのテストファイル/テストクラスに数行追加された形で、既存のテストロジック自体には手が入っていません。
- 影響範囲・注意点
開発ローカルでのテスト実行が楽になる
Active Storage 本体や周辺コードを軽く触るだけの場合、MuPDF や ffmpeg 等をインストールしていなくても、テストスイート全体がエラーで止まらずに進むようになります。CI 環境では依存を入れておかないとテストが実行されない
これらのテストは「依存が満たされていないとスキップされる」ため、- 「動画プレビュー/MuPDF プレビュー機能をちゃんと継続的に検証したい」
という場合には、CI ジョブで必要なバイナリ・ライブラリを必ずインストールするようにしておく必要があります。
依存を入れていないと「グリーンだけど実はその機能はテストされていない」状態になり得ます。
- 「動画プレビュー/MuPDF プレビュー機能をちゃんと継続的に検証したい」
機能自体の挙動変更は無し
変更はテストコードのみであり、Active Storage の本番コード(アナライザ、プレビューアの実装)そのものの挙動は変わっていません。そのため、既存アプリのランタイム挙動に直接の影響はありません。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57097
- Active Storage のアナライザ/プレビューアがどの依存に基づいて有効化されるかは、Rails ガイドおよび該当クラス(
ActiveStorage::Analyzer::VideoAnalyzer,ActiveStorage::Previewer::VideoPreviewer,ActiveStorage::Previewer::MuPDFPreviewerなど)のaccept?/enabled?ロジックを参照すると把握しやすいです。 - CI でこれらのテストを有効にしたい場合は、Docker イメージやビルドスクリプトで
ffmpeg,mutoolなどをインストールしておくのが一般的です。
#57094 Delay engine route building
マージ日: 2026/3/30 | 作成者: @byroot
- 概要 (1-2文で)
Rails Engine がroutes do ... endを評価するタイミングを遅延させ、make_routes_lazy最適化が正しく効くようにした変更です。
これにより、graphqlなど Engine 内で直接routes doを呼んでいるライブラリでも、ルーティングの遅延構築によるパフォーマンス改善が適用されます。
- 変更内容の詳細
背景
以前の PR (#52353) で、Rails のルーティングを「遅延構築 (lazy)」する最適化が導入されました。
しかし Engine 側で以下のように直接 routes do を呼んでいる場合、そのブロックが make_routes_lazy 呼び出し前に評価されてしまい、結果としてルートが即時に構築されていました。
class SomeEngine < ::Rails::Engine
routes.draw do
# ここがすぐ評価されて RouteSet がフル構築されてしまう
get "/status", to: "status#show"
end
endgraphql のダッシュボードエンジンもまさにこのパターンです(PR 説明にリンクあり)。
今回の対応
今回の PR では「Engine が routes do ... end を宣言した時点では評価せずに蓄えておき、route_set_class が確定した後にまとめて評価する」ように変更しています。
ポイント:
Rails::Engine内で:routes(&block)が呼ばれたときに、すぐにブロックを実行して RouteSet を構築するのではなく、ブロックを保持しておくように変更。route_set_classが設定され、make_routes_lazyなどルーティングの最適化が完了した後に、その保持しておいたブロックを評価して RouteSet に反映するようにした。
疑似コードイメージとしては:
class Rails::Engine
def routes(&block)
if block_given?
# 以前: ここですぐに draw / eval されていた
# 今回: ここでは「登録」だけにする
pending_route_blocks << block
else
# RouteSet の取得は従来どおり
@routes ||= route_set_class.new
end
end
def configure_routes!
# route_set_class, make_routes_lazy などの設定が完了した後に呼ばれる
pending_route_blocks.each do |blk|
@routes ||= route_set_class.new
@routes.draw(&blk)
end
end
end実際の実装は Rails の内部構造に合わせた書き方になっていますが、概念的には「routes do に渡されたブロックを一旦キューに入れ、ルートセットのクラスや lazy 設定が整ってから処理する」変更です。
- 影響範囲・注意点
主な影響範囲
Rails::Engineを継承している Engine 全般- 特に、Engine 定義内で
routes do ... endを直接呼んでいるコード(graphql-rubyのダッシュボードエンジンなど) - Rails ルーティングの lazy 構築機能 (
make_routes_lazy) の挙動
期待される効果
- これまで
make_routes_lazyが効いていなかった Engine でも、ルート構築が遅延されるようになり、アプリ起動時のオーバーヘッドが軽減される可能性があります。 - エンジン作者が特別な対応をしなくても、
routes doを普通に書くだけで最適化の恩恵を受けられます。
互換性・注意点
routes doブロックが「Engine 定義の評価タイミング」ではなく「Engine のルートセットが最終的に構成されるタイミング」で評価されるようになります。- 通常のルーティング宣言であれば問題ありませんが、ルート定義ブロック内で Engine のクラス状態やグローバル状態に依存した副作用的処理をしているようなコードがある場合、評価タイミングの違いによる影響が出る可能性があります。
- 例:
routes do ... endの中でSomeEngine.configを直接書き換える、といったイレギュラーなコードがある場合など。
- 例:
- ただし、今回の変更はあくまで「
route_set_classの設定完了後に評価する」程度の遅延であり、一般的な用途の範囲では互換性に問題は出にくい設計です。
- 参考情報 (あれば)
- この PR のフォローアップ元:
- https://github.com/rails/rails/pull/52353
- ルーティングの遅延構築 (
make_routes_lazy) を導入した PR
- ルーティングの遅延構築 (
- https://github.com/rails/rails/pull/52353
- 遅延最適化が効いていなかった実例(
graphql-rubyの dashboard engine):
#57090 Fix duplicated word in configuring guide
マージ日: 2026/3/29 | 作成者: @55728
- 概要 (1-2文で)
Railsガイド「Configuring Rails Applications」のconfig.action_on_early_load_hookに関する記述から、誤って重複していた "when" という単語を削除するドキュメント修正PRです。アプリケーションコードや挙動には一切影響せず、ガイド文言のみが整備されています。
- 変更内容の詳細
- 対象ファイル:
guides/source/configuring.md - 変更内容: 1行のみの軽微なテキスト修正(+1/-1)
config.action_on_early_load_hook に関する説明文に、英語の "when" が二重に書かれていた箇所を修正しています。
イメージとしては、以下のような変更です(実際の文面は英語ですが、ニュアンスとして):
- This option controls what happens when when the early load hook is triggered.
+ This option controls what happens when the early load hook is triggered.つまり、ガイドの文章中にあったタイポ(重複単語)のみが修正されています。
設定オプション config.action_on_early_load_hook 自体の意味・利用方法・値の種類などには変更はありません。
- 影響範囲・注意点
- 影響範囲:
- Railsの挙動・API・設定値には一切変更なし
- 影響があるのはドキュメントの読みやすさのみ
- 注意点:
- 既存アプリケーションやライブラリで、コードや設定を修正する必要はありません。
config.action_on_early_load_hookを検索して挙動変更を期待しても、今回はタイポ修正のみである点に留意してください。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57090
- 関連ガイド: Guides > Configuring Rails Applications (
guides/source/configuring.md) config.action_on_early_load_hookは、Railsの「early load hook」が呼ばれた際の動作を制御する設定ですが、本PRではその説明文のみが対象であり、仕様や推奨設定に変更はありません。
#57013 Predefine the well-known Postgres type OIDs
マージ日: 2026/3/28 | 作成者: @matthewd
- 概要 (1-2文で)
PostgreSQL の組み込み型について、pg_typeを毎回問い合わせて OID を取得するのをやめ、Rails 側に「よく知られた型 OID」の一覧を同梱して接続時に即座にロードできるようにした変更です。ユーザー定義型や拡張で初めて未知の型に遭遇したときにのみ、これまで通りpg_typeをクエリして補完する遅延ロード方式へと切り替えています。
- 変更内容の詳細
背景と方針
- これまで:
- PostgreSQL 接続時に、常に
pg_typeをクエリして- 型名 → OID (および関連情報)
- のマッピングを取得していた。
- これはユーザー定義型や拡張 (例:
hstore) を扱うためには必要だが、- コアの組み込み型まで毎回問い合わせているのは無駄。
- PostgreSQL 接続時に、常に
- PostgreSQL のコア型の多くは「静的に割り当てられた OID」を持ち、PostgreSQL のソースコードに固定値として含まれている。
- この PR では、その「よく知られた (well-known) OID マップ」を Rails 側に生成・同梱し、
- 接続直後はそれを使って型マッピングを初期化
- 未知の型に出会った場合のみ、従来の
pg_typeクエリを発火 - 必要であれば、今まで通り個別の one-shot クエリも行う という遅延ロードに変えている。
主なファイルごとの変更
1. well-known OID 情報の追加
activerecord/lib/active_record/connection_adapters/postgresql/oid/well_known_values.rb- PostgreSQL のコア型に対する「型名 → OID」などの静的テーブルを定義したファイル。
- ここには生成済みの生データ (値の一覧) が格納される。
activerecord/lib/active_record/connection_adapters/postgresql/oid/well_known.rb- 上記
well_known_valuesをラップして、実際の型マッピング初期化で利用しやすい API を提供する層。 - 例:
- OID から型名 / カテゴリ / 入れ子関係 (配列型など) を引ける
- type map initializer が使うための構造に整形する
- 「コア型だけはこのテーブルから即座に分かる」ようにするためのユーティリティ的役割。
- 上記
activerecord/lib/active_record/connection_adapters/postgresql/oid/well_known_generator.rb- 上記
well_known_values.rbを生成するためのジェネレータ。 - 実際にはテスト環境や開発タスクとして利用し、PostgreSQL の実インスタンスや公式ソース情報から
- OID
- 型名
- カテゴリ
- 配列/範囲型の対応関係
- などを収集し、静的 Ruby コードとして書き出す。
- 実行用の Rake タスクと紐付いている (後述)。
- 上記
activerecord/Rakefilewell_known_generator.rbを使って OID テーブルを再生成するための Rake タスクを追加。- 新しい PostgreSQL バージョンのサポートや OID 変更に伴い、メンテナが再生成できるようにする。
2. TypeMap 初期化フローの変更
activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb- 変更量が多いファイル。
- これまでは:
- 接続時に
pg_typeをクエリし、「必要そうな型定義」をまとめて取ってくる半 heuristics なロジックを実行。
- 接続時に
- これからは:
- 接続直後はまず
well_knownの情報を使って type map を初期化(コア型はクエリ無しで準備完了) - 実際のクエリやスキーマ情報の解釈中に「未知の型 OID / 型名」に遭遇した場合:
- 初回に限り、従来と同様の「まとめて
pg_typeをクエリして足りない型を取得する」ロジックを起動 - それでも解決できない場合は、よりピンポイントな one-shot クエリ (特定 OID / 型名のみ取得) を行う
- 初回に限り、従来と同様の「まとめて
- 接続直後はまず
- つまり「従来やっていたことはやめないが、発火タイミングを
未知の型検出時まで遅延させる」という形のリファクタリング。
activerecord/lib/active_record/connection_adapters/postgresql/oid.rbwell_known関連クラスの読み込みフックなど、OID 周りのエントリーポイントに最小限の変更・追加。
3. PostgreSQL アダプタ側の変更
activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb- 接続時の初期化ロジックを変更。
- これまで:
- 接続確立後に
pg_typeを即座にクエリして type map を構築。
- 接続確立後に
- これから:
- まず
WellKnownの事前定義 OID マップを使って type map を構築。 - ユーザー定義型や拡張型に遭遇したタイミングで、
type_map_initializerが動的にpg_typeを問い合わせる。
- まず
- また、OID マップの初期化手順やコネクションごとのキャッシュの扱いにも調整が入っているはずで、テストコードから考えると「enum や extension 型を含めた動作が変わらないこと」を重視している。
activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb- クエリ実行フローのうち、型情報が関与する部分に「未知の OID に遭遇したときの遅延ロードフック」を組み込む変更が追加されている。
- 実際には、クエリ結果のフィールドメタ情報から OID を見て、type map に無いものがあれば
type_map_initializerに問い合わせて補完する、といった流れ。
4. テスト追加・更新
activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb- 接続直後に
pg_typeをクエリしないことを確認するテストや、 - well-known OID が正しく利用されること、
- 未知の型に遭遇したときに初めて DB クエリが走ること、
- その後はキャッシュされて再クエリされないこと
- など、振る舞いの変更に対応したテストが多数追加されている。
- 接続直後に
activerecord/test/cases/adapters/postgresql/enum_test.rb- enum 型 (ユーザー定義型) が、引き続き正しく解決されることを確認するテストを追加。
- well-known には含まれないため、遅延クエリの経路が正常に動作するかの検証。
activerecord/test/cases/adapters/postgresql/well_known_generator_test.rbwell_known_generatorが期待通りのデータを吐き出すかをテスト。
activerecord/test/cases/adapters/postgresql/well_known_test.rbWellKnownのテーブルが妥当か、代表的な型の OID やメタ情報が正しいかをテスト。
- 影響範囲・注意点
性能面の影響
ポジティブな影響:
- PostgreSQL への接続直後に行っていた
pg_typeへのクエリが、基本的には不要になる。 - アプリケーションの起動時・コネクションプール拡張時のオーバーヘッドが減り、接続確立が速くなる可能性が高い。
- 特に多数の DB 接続を張る環境や、短命プロセス (ジョブワーカーなど) でメリットが大きい。
- PostgreSQL への接続直後に行っていた
遅延クエリによる影響:
- 初めてユーザー定義型や拡張型 (enum, composite, range, hstore, 他拡張) を利用するクエリを投げた瞬間に、
- まとめて
pg_typeを問い合わせるクエリが一度走るため、そのクエリだけは若干のレイテンシ増が起こりうる。
- まとめて
- ただし、それは従来「接続直後に常に発生していたコスト」を「必要になった時点まで先送りした」だけなので、トータルでは改善方向。
- 初めてユーザー定義型や拡張型 (enum, composite, range, hstore, 他拡張) を利用するクエリを投げた瞬間に、
互換性・バージョン依存性
- 組み込み型の OID は PostgreSQL ソースで静的に定義されている前提を利用しているため:
- サポート対象 PostgreSQL バージョン間で OID が変わる場合は、
well_known_values.rbを再生成する必要がある。 - そのための Rake タスクと
well_known_generatorが同梱されており、メンテナが公式の OID 情報をソースから再取り込みできるようになっている。
- サポート対象 PostgreSQL バージョン間で OID が変わる場合は、
- ユーザー側としては:
- 通常のアップグレードでは特に作業不要だが、
- Rails が公式にサポートしていない PostgreSQL バージョンを使う場合、well-known OID と実 OID が乖離する可能性がある点に注意が必要。
- その場合でも、未知の型として検出されれば
pg_typeクエリ経由で補完される可能性があるが、組み込み型が「既知」とみなされている分だけ、ずれが起きると厄介なので、サポート範囲外バージョンは避けるのが無難。
- その場合でも、未知の型として検出されれば
型周りの挙動
- ユーザー定義型・拡張型:
- これまで通り
pg_typeベースで解決される。 - 「初めて使われるタイミング」でのみクエリが発生する遅延ロードになるが、Rails アプリ側のコード変更は不要。
- これまで通り
- 型解決エラー:
- 万が一 well-known OID が間違っていた場合、
- 誤った型マッピングでクエリ結果を解釈するリスクがある。
- そのため、テストで well-known テーブル自体の正しさを検証している。
- 万が一 well-known OID が間違っていた場合、
- connection_pool リセット / reconnect:
- 再接続時も同じく well-known OID で即座に map が構築されるため、
pg_typeへのクエリは発生しない。 - 再接続でパフォーマンス劣化するようなケースの改善が期待できる。
- 再接続時も同じく well-known OID で即座に map が構築されるため、
- 参考情報 (あれば)
- この PR で導入された主な新規ファイル:
activerecord/lib/active_record/connection_adapters/postgresql/oid/well_known_values.rbactiverecord/lib/active_record/connection_adapters/postgresql/oid/well_known.rbactiverecord/lib/active_record/connection_adapters/postgresql/oid/well_known_generator.rb
- 関連するテスト:
activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rbactiverecord/test/cases/adapters/postgresql/enum_test.rbactiverecord/test/cases/adapters/postgresql/well_known_generator_test.rbactiverecord/test/cases/adapters/postgresql/well_known_test.rb
開発者視点では、「PostgreSQL 接続時の型情報ロードが eager から lazy になり、かつコア型は静的 OID テーブルで即時解決されるようになった」という理解でおおむね十分です。
#57029 Document with_rich_text_* methods
マージ日: 2026/3/28 | 作成者: @p8
- 概要 (1-2文で)
Action Text のwith_rich_text_*系メソッド(with_rich_text_bodyなど)が公式にドキュメント化されるよう、ActionText::Attributeに Yard コメントが追加された PRです。機能追加や挙動変更ではなく、「どう使うか」が Rails API ドキュメントにきちんと出るように整理したドキュメント改善です。
- 変更内容の詳細
変更ファイルは 1 つだけです:
actiontext/lib/action_text/attribute.rb
ここに、主に以下のような Yard 形式のコメントが追加されています(概略):
with_rich_text_attribute_name(attribute_name)
with_rich_text_attribute_name という低レベルなヘルパーメソッドに説明が付き、その上で with_rich_text_* スコープがどう生成されるかがドキュメント化されています。
Rails の ActionText::Attribute モジュールは、モデルに has_rich_text :body のように宣言されたときに、以下のようなスコープを自動で生やします:
class Message < ApplicationRecord
has_rich_text :body
end
# 自動的に定義される:
Message.with_rich_text_body # => includes(:rich_text_body)
Message.with_rich_text_body_and_embedsPR では、この挙動をコードコメントとして明示し、API ドキュメントに反映されるようにしています。例えば:
with_rich_text_#{attribute}- 対象のモデルに関連付いた
ActionText::RichTextレコードを eager load するスコープ - 実際には
includes(rich_text_#{attribute})相当
- 対象のモデルに関連付いた
with_rich_text_#{attribute}_and_embeds- 上記に加えて、リッチテキスト内で添付されている
ActiveStorage::Attachmentなども eager load するスコープ - N+1 を避けて表示するためのユーティリティであることを明示
- 上記に加えて、リッチテキスト内で添付されている
ドキュメントには、利用例も含まれているはずです。典型的な利用イメージは以下のようなものです:
# N+1 を避けてリッチテキストを表示したい場合
@messages = Message.with_rich_text_body_and_embeds.limit(20)
@messages.each do |message|
# ここで body を使っても、関連はすでに eager load 済み
message.body.to_s
endこの PR 自体は Ruby コードのロジック変更はほぼなく、「こういうスコープが存在し、何をするか」を明快に説明するコメントを 25 行ほど追加しただけです。
- 影響範囲・注意点
ランタイム挙動:
- 既存の
with_rich_text_*スコープの実装や動作には変更なし。 - 既存アプリのコードは一切影響を受けません。
- 既存の
ドキュメント/API 的な影響:
- Rails API ドキュメントに
with_rich_text_*メソッド群が正式に掲載され、補完や検索で見つけやすくなります。 - これまで「暗黙に生えていて、ソースを読まないと分かりにくかった」スコープの存在と使い方が、公式に分かるようになります。
- ガイドライン上、「N+1 を避けるには
with_rich_text_xxx(_and_embeds)を使う」というベストプラクティスが明示されるため、チーム内での共通認識を作りやすくなります。
- Rails API ドキュメントに
注意点:
- PR はあくまでドキュメント追加なので、将来的にスコープ名や挙動を変えるときは、このドキュメントとの整合性に注意が必要になります。
with_rich_text_*は「スコープ名が属性名から自動生成される」仕組みなので、has_rich_text :content_bodyなど、長い/複雑な名前を付けると同名パターンのスコープが自動生成される点は引き続き意識が必要です。
- 参考情報 (あれば)
- 関連する既存機能:
Action Textガイド:
https://guides.rubyonrails.org/action_text_overview.htmlhas_rich_textの定義と関連コード:actiontext/lib/action_text/attribute.rb(本 PR でコメント追加されたファイル)
- 利用のポイント:
- 一覧表示などでリッチテキストを大量に読み出す場合は、
Model.with_rich_text_bodyかwith_rich_text_body_and_embedsを使うことで、クエリ数削減とパフォーマンス改善が期待できます。
- 一覧表示などでリッチテキストを大量に読み出す場合は、
#57082 Document that :if and :unless options on validations accept arrays [ci skip]
マージ日: 2026/3/27 | 作成者: @cgunther
- 概要 (1-2文で)
ActiveModelのバリデーションにおける:if/:unlessオプションが「配列を受け付ける」ことを、公式ドキュメント側に明示的に追記したPRです。挙動自体はすでに存在していたもので、実装変更ではなくドキュメントの補強です。
- 変更内容の詳細
何が書かれたか
このPRは以下3つのファイルで、:if / :unless オプションの説明に「配列を渡せる」旨を追記しています。
activemodel/lib/active_model/validations.rbactivemodel/lib/active_model/validations/validates.rbactivemodel/lib/active_model/validations/with.rb
いずれも共通して、
:if/:unlessにはSymbol(インスタンスメソッド名)Proc/lambdaString(evalされる条件式)- そしてその配列(上記を複数指定するための Array)
- を渡せる
という点を、文書としてはっきり記載しています。
もともとバリデーションは ActiveSupport::Callbacks.set_callback の上に構築されており、set_callback 側のドキュメントには配列サポートが書いてありましたが、バリデーションの説明側からそのつながりが分かりにくかったため、ここを補った形です。
イメージしやすいサンプルコード
たとえば、以下のようなバリデーション記述が「正当な使い方である」ことが、今回の変更でドキュメントに明記されます。
class User < ApplicationRecord
# メソッド名とProcを組み合わせて複数条件
validates :email, presence: true, if: [:activated?, -> { admin? }]
# 複数の条件メソッドを :unless で
validates :age, numericality: { greater_than: 18 }, unless: [:guest?, :under_review?]
# validates_with / with_options などでも同様
with_options if: [:activated?, :profile_completed?] do
validates :phone_number, presence: true
end
validates_with SomeValidator, if: [-> { feature_enabled? }, :not_legacy_user?]
end以前からこれらは動いていましたが、公式ドキュメントからは読み取りづらかったため、今回のPRで「サポートされている書き方」として明文化されています。
- 影響範囲・注意点
- 挙動変更は一切なし
- 既存アプリケーションの動作には影響しません(ドキュメントのみ変更)。
- 既に配列を使っているコードはそのままでOK
- 以前から
set_callbackによって動いていた挙動であり、今後も同じように動作します。
- 以前から
- 新規コードでの記述がしやすくなる
- 今後は「
if:やunless:に配列を渡して複数条件を管理する」ことが、安心して推奨しやすくなります。
- 今後は「
- 複数条件の評価順序や短絡評価
- これは今回のPRで新規説明は入っていませんが、
set_callbackの仕組みに依存します。 - 一般には、配列の要素ごとに順番に評価され、配列内のすべてが
true(unlessの場合はfalse)になるかどうかで実行可否が決まるイメージです。 - 具体的な評価順・短絡の仕様に厳密に依存したい場合は、
ActiveSupport::Callbacksのドキュメントやソースを確認するのが安全です。
- これは今回のPRで新規説明は入っていませんが、
- 参考情報 (あれば)
- このPRで明示された背景:
- バリデーションは
ActiveSupport::Callbacks.set_callbackベースであり、:if/:unlessの配列サポートはコールバックレイヤー由来。 - しかし「バリデーションのドキュメントだけ読んでいると、そのことが伝わらない」ため、#55761, #57050 に続く形で説明を補強。
- バリデーションは
- 関連PR/Issue:
- #55761: コールバック / バリデーション周辺のドキュメント改善の流れ
- #57050: 本PRの直前のフォローアップとなる変更(詳細は該当PR参照)
#57050 Combine per-validator and top-level :if/:unless/:on in validates
マージ日: 2026/3/27 | 作成者: @Mordorreal
- 概要 (1-2文で)
validatesでトップレベルとバリデータ単位の:if/:unless/:onオプションを同時に指定した場合に、これらを「上書き」ではなく「結合」するように挙動が変更されました。これにより、期待どおり「両方の条件を満たしたときだけバリデーションが走る」ようになります。
- 変更内容の詳細
これまでの問題
現状の Rails (この PR 前) では、以下のように validates を書くと:
validates :title, presence: { if: :local? }, if: :global?開発者の期待としては:
local?もglobal?も両方 true のときだけ presence バリデーションが走る
はずですが、実際は:
- バリデータ単位の
presence: { if: :local? }に含まれる:ifが
トップレベルのif: :global?を Hash#merge によって「上書き」してしまい - 結果として「
local?が true なら実行、global?は無視」という挙動になっていました。
これは #55761 で報告されていたバグで、この PR はその修正です。
新しい挙動: 条件オプションの「結合」
validates 内で、トップレベルとバリデータ単位の両方に指定された :if / :unless / :on について、Rails 側でマージ処理をカスタム実装し、配列として結合されるようになりました。
変更により、次のように解釈されます:
validates :title, presence: { if: :local? }, if: :global?
# 新しい挙動では、内部的にほぼこう扱われる:
validates_presence_of :title, if: [:global?, :local?]つまり、
- トップレベルの
if: ... - バリデータ単位の
presence: { if: ... }
が両方指定されている場合、それぞれが「条件リストの要素」として扱われ、全ての条件が true のときに実行 という AND 条件になります。
同様に、:unless や :on についても結合されます。
サンプル: :if 結合
validates :name,
length: { minimum: 3, if: :short_name_check_enabled? },
if: :user_active?→ 実行条件は:
user_active?が true かつshort_name_check_enabled?が true
のとき。
サンプル: :unless 結合
validates :email,
presence: { unless: :guest? },
unless: :email_optional?→ 実行条件は:
guest?が false かつemail_optional?が false
(「unless の AND」は「両方とも false である必要がある」という意味になります)
サンプル: :on 結合
validates :password,
length: { minimum: 8, on: :create },
on: :update:on も配列として結合されるため、実質的には
on: [:create, :update]となり、create と update の両方で実行されます。
(※ :on はバリデーションの「コンテキスト名」なので、配列で複数指定すると「これらいずれかのコンテキストで実行」という OR 的な意味合いですが、「トップレベルに1つ」「バリデータに1つ」が結合されて配列化されるという意味で「結合」です)
スカラーオプションは従来どおり「上書き」
この結合ロジックが適用されるのは :if / :unless / :on のみで、
それ以外のスカラーなオプションは従来どおり「後勝ち (override)」です。
該当例:
:message:allow_nil:allow_blank:strict
例:
validates :title,
presence: { message: "local message" },
message: "top-level message"この場合:
- 最終的な
messageは「top-level message」 - トップレベルの指定が per-validator の指定を上書き
という挙動は変わりません。
実装上のポイント
activemodel/lib/active_model/validations/validates.rbに_merge_validates_optionsを追加。validates内部でoptionsをマージする際に::if,:unless,:onについては、両方に存在する場合は配列化して結合- それ以外のキーは従来どおりの
Hash#merge相当の上書き
validates_test.rbに、結合挙動とスカラー上書きの両方を確認するテストが追加されています。
PR 説明にあった「コールバック条件評価の最適化 (ActiveSupport 側)」は最終版では削除され、この PR では ActiveModel 側の挙動変更にフォーカスしています。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは、「トップレベル」と「バリデータ個別」の両方に
:if/:unless/:onを指定している既存コードです。 - 今までは「per-validator 側が top-level 側を完全に上書き」していましたが、今後は「両方が結合 (AND)」されるため、条件が厳しくなり、バリデーションが呼ばれなくなるケース が発生する可能性があります。
具体的なケース
# 旧挙動(上書き)を前提に書かれていたコード
validates :title, presence: { if: :local? }, if: :global?- 以前:
local?が true なら実行 (global?は無視) - 変更後:
local?かつglobal?が true のときのみ実行
もし、これまで「per-validator 側で top-level の条件を意図的に“消す”ため」にこの書き方をしていた場合、挙動が変わります。
同様に:
validates :title,
presence: { on: :create },
on: :updateを「更新時は presence だけ外すつもりで書いていた」ようなケースでは、変更後は on: [:update, :create] 的な扱いになり、`create / update 両方で実行されるようになります。
パフォーマンス
PR の説明に記載されたベンチ結果 (Ruby 4.0.1):
- トップレベル
:ifのみ: +2.3% - per-validator
:ifのみ: +1.2% - 両方に
:ifがある結合ケース: -3.3% - 複数バリデータの結合ケース: -1.7%
ただし、最終版の PR では ActiveSupport 側のコールバック最適化が削除されているため、
これらの数値はそのままは当てはまりませんが、
- 条件 1 個だけの一般的なケースではほぼ現状並み
- 条件が 2 個以上結合されるケースでは、その分だけ条件評価のコストは増える (不可避)
という程度の影響と考えておけばよいです。
テストスイートは ActiveSupport/ActiveModel ともにパスしています。
マイグレーション上の注意
- トップレベルと per-validator の両方で
:if/:unless/:onを使っている箇所がないか検索し、
「片方で片方を打ち消す」つもりの書き方をしていないか を確認した方が安全です。 - もし「上書き」挙動をあえて使っていた場合は:
- トップレベルの条件を削除する
- コンテキスト (
:on) 側で分ける - クラスメソッドなどで条件ロジックを一本化する といった対応が必要になります。
- 参考情報 (あれば)
- 対応 Issue: https://github.com/rails/rails/issues/55761
- PR: https://github.com/rails/rails/pull/57050
- 変更箇所:
activemodel/lib/active_model/validations/validates.rbactivemodel/test/cases/validations/validates_test.rbactivemodel/CHANGELOG.md(挙動変更が明記されているはずです)
#57080 Deprecate schema_order option in PostgreSQL database configurations
マージ日: 2026/3/27 | 作成者: @eileencodes
- 概要 (1-2文で)
PostgreSQL 用のデータベース設定オプションschema_orderが非推奨となり、今後はschema_search_pathを使うように変更されました。schema_orderは過去の名前の名残であり、現行の命名に統一するための整理です。
- 変更内容の詳細(あればサンプルコードも含めて)
schema_orderオプションを 非推奨(deprecate) として扱うように変更postgresql_adapterが接続設定を解釈する際に、schema_orderが使われていたら警告を出しつつ、内部的にはschema_search_pathを使うような形に近い挙動になっています(実装としては、schema_orderは古いエイリアスであり、正式名称のschema_search_pathに統一するという方針)。
CHANGELOGに以下のような内容が追記- 「PostgreSQL データベース設定における
schema_orderオプションを非推奨にした。代わりにschema_search_pathを使用してください」といった趣旨のエントリが追加されています。
- 「PostgreSQL データベース設定における
- テストの追加
- PostgreSQL アダプタ用のテスト (
postgresql_adapter_test) に、schema_order使用時の挙動と非推奨警告を確認するテストケースが追加されています。
- PostgreSQL アダプタ用のテスト (
※PR の説明文どおり、schema_order は「現在の命名 (schema_search_path) より前から存在する古いエイリアス」であり、機能的には schema_search_path と同じ意味のオプションです。
サンプル(従来の書き方・新しい書き方の例):
# database.yml (従来: 非推奨になる書き方)
production:
adapter: postgresql
database: my_app_production
schema_order: public,tenant_1
# database.yml (推奨: これから使うべき書き方)
production:
adapter: postgresql
database: my_app_production
schema_search_path: public,tenant_1- 影響範囲・注意点
- 影響を受けるのは PostgreSQL を使っていて、
database.yml(または接続設定) にschema_orderを指定しているアプリケーション です。 - 今回は「非推奨化」であり、ただちに動かなくなるわけではない ものの、Rails の将来のバージョンで
schema_orderが削除される可能性が高いです。 - Rails をアップグレードした際に、起動時やテスト実行時などに deprecation warning が出る ようになると考えられます。そのため:
schema_orderを使用中の設定は、早めにschema_search_pathに置き換えることが推奨されます。- CI ログや本番ログに deprecation warning が大量に出るのを避ける意味でも、変更しておくとよいです。
- アプリケーションコード中で
schema_orderを参照しているカスタム処理がある場合(例: 独自の接続ラッパーやマルチテナント周りの設定生成など)、そこも合わせてschema_search_pathに名称変更する必要があります。
- 参考情報 (あれば)
- Rails ガイド(英語):
Active Record and PostgreSQL の章でschema_search_pathが説明されています。
https://guides.rubyonrails.org/active_record_postgresql.html - 本 PR:
https://github.com/rails/rails/pull/57080
(今後の議論状況や削除予定バージョンの明記などは、この PR や後続の PR / CHANGELOG で追うとよいです。)
#57077 Deprecate the strict option in MySQL database configurations
マージ日: 2026/3/27 | 作成者: @eileencodes
- 概要 (1-2文で)
MySQL 用のデータベース設定オプションstrictが非推奨になり、代わりにvariables: { sql_mode: ... }を直接指定する形に整理された PR です。Rails が推奨しない「非 strict モード」のための特別なショートカットを廃止し、設定方法を一本化しています。
- 変更内容の詳細
非推奨になったオプション
database.yml などで使われていた以下の設定が非推奨になります。
# 旧: 非推奨
strict: true
strict: false
strict: :defaultstrict は元々、MySQL の sql_mode を簡易的に切り替えるために Rails 4.2 で導入されたオプションです。しかし現在は variables: { sql_mode: "..." } で同じことができるため、Rails 側で専用オプションを持つ必要がない、という整理です。
推奨される置き換え
PR で明示されているマイグレーションパスは次の 3 パターンです。
# 1. strict: true
# → 削除する (これがデフォルト)
# 何も書かないか、既存の strict 行を消す
# strict: true # ← 削除してよい
# 2. strict: false
# → variables: { sql_mode: "" }
variables:
sql_mode: ""
# 3. strict: :default
# → variables: { sql_mode: :default }
variables:
sql_mode: :defaultつまり、strict を使わずに sql_mode をベタに指定してください、という変更です。
「strict モードを無効にしたい (strict: false)」場合にだけ、variables で明示的に sql_mode を空文字にする必要が出てきます。
コード・テストの変更の概要
activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rbstrictオプションを解釈していた箇所に deprecation を出す処理またはコメントが追加されています(strictを設定した際に警告が出るような対応)。
activerecord/CHANGELOG.mdstrictが非推奨になったこと、推奨される代替手段(variables: { sql_mode: ... })が記載されています。
- テスト (
connection_test.rb,defaults_test.rb)strictオプションに関する振る舞いとデフォルト値周りのテストが、廃止方針に合わせて修正されています。sql_modeをどう設定するかが、strict経由でなくvariables経由でテストされるようになっています。
- 影響範囲・注意点
影響を受けるのは誰か
database.ymlなどで MySQL アダプタを使い、かつstrict:オプションを指定しているアプリケーションが対象です。- PostgreSQL 等には無関係です。
どんな影響があるか
- Rails の新バージョンに上げると、
strictオプションを使っている場合に deprecation warning が表示されるようになります。 - 将来のメジャーバージョンで
strictが完全削除されると、設定にstrictが残っていると起動時にエラーになる可能性があります。
実務上の対応
strict: trueを使っている場合- 設定行を 削除するだけでよい です。もともとこれがデフォルト挙動であり、今後もデフォルトが「strict モード相当」であることが前提になっています。
strict: falseを使っている場合- 明示的に non-strict を求めているため、今後も同じ挙動を維持したければ、必ず以下のように書き換える必要があります。
yaml# 旧 strict: false # 新 variables: sql_mode: ""※ ただし、PR 説明にある通り、2026 年時点では MySQL を非 strict モードで運用することは推奨されておらず、この設定自体を再検討した方がよいケースが多いです(サイレントな型変換・トランケーションなどの問題が起こりやすいため)。
strict: :defaultを使っている場合- MySQL サーバー側のデフォルト
sql_modeを使いたい、というニーズです。今後は以下のように書き換えます。
yaml# 旧 strict: :default # 新 variables: sql_mode: :default- MySQL サーバー側のデフォルト
その他の注意点
- すでに
variables:を使ってsql_modeを細かく設定している場合は、この PR による影響はありません。 - 既存の
sql_mode設定を変える必要はなく、「strictというショートカットはやめて、variables.sql_modeに一本化します」というポリシー変更だと理解するとよいです。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57077
- 概念的な対応関係:
strict: true→ 「Rails が管理する strict なsql_mode(デフォルト)」strict: false→sql_mode空文字(多くの場合かなり寛容で危険なモード)strict: :default→ MySQL サーバー側のsql_modeデフォルトに従う
- MySQL の
sql_mode公式ドキュメント(参考):
#57070 Consolidate configure_connection and allow skipping individual settings
マージ日: 2026/3/27 | 作成者: @eileencodes
- 概要 (1-2文で)
PostgreSQL / MySQL 接続時のセッション設定処理が「1つの設定ハッシュを作って一括適用する」形にリファクタされ、サーバ側の現状値を確認して不要なSETクエリをスキップするようになりました。さらに、特定の設定をfalseにすることで「Rails からはその設定を一切触らない」ようにできるオプションが追加され、set_standard_conforming_stringsは非推奨になりました。
- 変更内容の詳細
2-1. PostgreSQL: configure_connection の統合と最適化
これまでバラバラに行われていた PostgreSQL の接続設定 (standard_conforming_strings, schema_search_path, など) を、次のようなフローに統合しています。
- 接続時に「セッション設定」を表すハッシュを組み立てる
- そのハッシュをループしながら
internal_set_configでSETを実行 internal_set_config内で PostgreSQL のparameter_statusを確認し、「すでに同じ値ならSETしない」
イメージとしては以下のような処理になっています(擬似コード):
settings = {
"standard_conforming_strings" => ...,
"intervalstyle" => ...,
"client_min_messages" => ...,
"search_path" => ...
}.compact # nil は除外
settings.each do |name, value|
internal_set_config(name, value)
endinternal_set_config では connection.parameter_status(name) を参照し、サーバ側がすでに value になっていれば SET name = ... を発行しません。これにより、接続ごとに毎回 SET クエリを打たなくてよくなり、特にコネクションプーラー越しの環境での無駄なラウンドトリップが減ります。
2-2. 設定を「完全にスキップ」できるように
Rails の DB 設定で、特定のセッション設定項目に false を指定すると、その項目については「Rails はそもそも何も設定しない」挙動になります。これは、ロードバランサやプロキシ、接続プール側で既に適切な設定がされており、Rails に上書きしてほしくないケースを想定しています。
PostgreSQL の例
production:
adapter: postgresql
# Rails からは一切 SET しない(DB 側のデフォルト or プール側設定を使う)
standard_conforming_strings: false
intervalstyle: false
min_messages: false
schema_search_path: falseここで false を指定すると、そのキーは設定ハッシュから除外され、internal_set_config も呼ばれません。
従来のように「値を変えたい」場合は、false ではなく文字列などの値を指定します。
MySQL の例
MySQL でも同様に、以下のように特定の設定をスキップできます:
production:
adapter: mysql2
# これまで Rails が接続時に wait_timeout を設定していたが、それをやめる
wait_timeout: false
# sql_mode は variables: 配下で false 指定(既存の :default 挙動と一貫性を持たせている)
variables:
sql_mode: falseこれにより、MySQL サーバや RDS / Cloud SQL 側で定義した wait_timeout や sql_mode を Rails が上書きしないようにできます。
2-3. set_standard_conforming_strings の非推奨化
PostgreSQL アダプタにあった set_standard_conforming_strings が非推奨になりました。
理由:
- いまや「統合された設定ハッシュ」の1項目として扱われるようになり、個別メソッドで制御する必要がなくなった
- 将来的には設定ハッシュベースの API に一本化したい意図
今後は:
- Rails の DB 設定(
database.yml)でstandard_conforming_stringsを制御する - そもそも Rails に設定させたくない場合は
standard_conforming_strings: false
という運用が推奨されます。
2-4. schema_search_path= にも parameter_status チェックを追加
PostgreSQL の schema_search_path= メソッドにも同様の最適化が入りました。
SET search_path = ...を発行する前にparameter_status("search_path")を確認- すでに同じ
search_pathであれば何もクエリを発行しない
これにより、アプリケーションコードやマイグレーションで schema_search_path= を多用している場合でも、不要な SET が減ります。
2-5. テストと CHANGELOG
- PostgreSQL / MySQL それぞれのアダプタテストが大幅に追加され、
- 設定ハッシュの生成内容
false指定時にSETが発行されないことparameter_statusによるスキップが行われること
などが検証されています。
activerecord/CHANGELOG.mdには:- 設定スキップ機能の追加
- 接続設定処理のリファクタ
set_standard_conforming_stringsの非推奨
が明記されています。
- 影響範囲・注意点
影響範囲
- ActiveRecord の PostgreSQL / MySQL アダプタ全般(特に接続確立時の処理)
- DB 接続設定 (
database.yml/ 環境変数) のうち、以下あたりを使っているプロジェクト- PostgreSQL:
standard_conforming_strings,intervalstyle,min_messages,schema_search_path - MySQL:
wait_timeout,variables[:sql_mode]
- PostgreSQL:
注意点
falseと「値なし (nil)」の違いに注意false…「Rails からはその設定に一切触らない」(SET しない)nil/ 未指定 …「Rails のデフォルトロジックに従って設定する or 何もしない(アダプタ実装依存)」
既存アプリでfalseを設定し始めると、今まで DB に適用されていた Rails 側の設定が消える可能性があります。
ロードバランサ・プロキシ利用時は明示的に
falseを検討
pgbouncer / ProxySQL などを経由して接続する場合、- 接続プール側ですでにセッション変数を制御している
- あるいはセッション変数をいじることが推奨されない
ことが多いため、このfalseオプションで Rails 側をオフにするのが有用です。
set_standard_conforming_stringsの非推奨対応- アプリや gem が直接
set_standard_conforming_stringsを呼んでいる場合、将来的な削除に備えて:- 呼び出しを削除する
- あるいは
database.ymlのstandard_conforming_stringsを使うように移行する
- 非推奨警告のログ出力にも注意してください。
- アプリや gem が直接
パフォーマンス / トラフィックへのポジティブな影響
- 接続確立時やスキーマ変更時に発行される
SETクエリが減るため、レイテンシ・DB 負荷の軽減が期待できます。 - 特にコネクションプーラー + 多数の短命コネクションな構成で効果が出やすい変更です。
- 接続確立時やスキーマ変更時に発行される
- 参考情報 (あれば)
- 関連 PR: https://github.com/rails/rails/pull/57013
(今回のセッション設定リファクタと連動する変更が行われている可能性があります) parameter_statusは libpq / PostgreSQL が提供する「現在のセッションパラメータ値」問い合わせ機能で、これを利用することで「本当に変更が必要なときだけSETする」挙動になっています。
#57053 Fix parallel test shutdown hang when workers die during Server#shutdown
マージ日: 2026/3/27 | 作成者: @markedmondson
- 概要 (1-2文で)
Rails の並列テスト実行時に、Server#shutdown中にワーカープロセスが死んだ場合にシャットダウン処理がハングする問題を修正した PR です。wait_for_active_workersループ内で定期的に子プロセスの終了を回収することで、ゾンビプロセスによる無限待ちを防ぎます。
- 変更内容の詳細
背景となる既存実装
ActiveSupport::Testing::Parallelizationでは、並列テスト用のワーカープロセス群をフォークし、DRb 経由でテストジョブを配布しています。シャットダウン時の流れは概ね以下です:
Parallelization#shutdownで各ワーカーに停止指示を送りつつ、Process.waitpid(pid, WNOHANG)で「すでに死んでいる」ワーカーを掃除。- その後
Server#shutdownが呼ばれ、wait_for_active_workersで「まだアクティブ(=Server 側が把握している)ワーカー」がいなくなるのを待機。
問題は、
Server#shutdownに入った後 にワーカーが異常終了した場合でした:- 例:
parallelize_teardownフック内で例外発生、DRb 切断、OOM kill など。 - 親プロセスは「そのワーカーはまだアクティブ」と思い続けるが、実際には子プロセスは終了している=ゾンビになっている。
wait_for_active_workersはwhile active_workers?のようなループを回しているため、永遠に抜けられない → CI がタイムアウトまでハング。
- 例:
過去 PR (#55794) では「Server#shutdown を呼ぶ前に 1 回だけ WNOHANG スイープする」対応をしていましたが、シャットダウン中に落ちるワーカー は拾えない、という抜けが残っていました。
今回の主な変更点
1. Server#reap_dead_workers の追加
activesupport/lib/active_support/testing/parallelization/server.rb に、新しく reap_dead_workers メソッドが追加されています。
概念的には以下のような処理です(実際のコードイメージ):
def reap_dead_workers
@workers.each do |worker|
pid = worker[:pid]
next unless pid
# すでに終了しているプロセスをノンブロッキングで回収
begin
dead_pid = Process.waitpid(pid, Process::WNOHANG)
if dead_pid
# ここで内部状態から「このワーカーはアクティブ」という情報を外す
unregister_worker(dead_pid)
end
rescue Errno::ECHILD
# すでに wait 済み、または存在しない場合はスキップ
end
end
endポイント:
Process.waitpid(pid, WNOHANG)により ブロックせずに 子プロセスの終了を検知・回収。- ワーカーが勝手に死んでいても、Server 側の「アクティブワーカー一覧」から削除できるようにしている。
Errno::ECHILDは「その PID の子プロセスはいない」ケースのため、無視して継続。
※実際の実装では、@workers の管理方法や unregister_worker 相当の処理は Server の既存ロジックに合わせた形になっています。
2. wait_for_active_workers ループ内での reap_dead_workers 呼び出し
wait_for_active_workers のループに reap_dead_workers 呼び出しが組み込まれました。
イメージ:
def wait_for_active_workers
while active_workers?
reap_dead_workers # ← 新規
sleep(SHORT_INTERVAL)
end
endこれにより:
- シャットダウン開始 後 にワーカーが異常終了した場合でも、
- 次のループサイクルで
reap_dead_workersによりその死を検知し、 active_workers?の対象から外れる → ループが正常に終了する、
という流れになります。
3. 回帰テストの追加
activesupport/test/parallelization_test.rb に、今回のバグを再現するテストが追加されています (約 36 行)。
テストの流れは概ね次のようなものです:
- 親プロセスが
Serverを起動。 - 子プロセスを
forkしてワーカーとして登録させる(DRb 経由で「生きているワーカー」と認識される状態を作る)。 - 親プロセス側で
Server#shutdownを呼び、wait_for_active_workersに入ったことが確認できるタイミングで、 - 親が子プロセスに対して
Process.killなどでシグナルを送り、ワーカーを強制終了。 - その後
Server#shutdownが正常に終了し、テスト全体もハングせずに完了することを検証。
- 修正前のコードだと、このテストは
wait_for_active_workersが永遠に返ってこないためハング。 - 修正後は
reap_dead_workersによってワーカー死亡が検知され、テストが通ることを確認済み。
- 影響範囲・注意点
影響範囲
- 対象:
ActiveSupport::Testing::Parallelizationを使った並列テスト実行(特に system test や CI での利用)。 - 主な効果:
- シャットダウンハングの解消: ワーカーが
- OOM Kill、
- ティアダウン処理中の例外、
- DRb 切断 などで 事前通知なく死んだ場合でも、テストスイート全体が 90 秒前後ハングして watchdog に殺される、といった事象が起きにくくなります。
- ゾンビプロセスの放置を減らし、親プロセスがきちんと wait するようになります。
- シャットダウンハングの解消: ワーカーが
パフォーマンス・副作用など
wait_for_active_workersループ内でProcess.waitpid(..., WNOHANG)を呼ぶので、ループごとに若干オーバーヘッドがありますが、- WNOHANG なのでブロッキングせず、
- 対象となるワーカー数も通常はテスト並列数程度(数~数十プロセス)なため、 実用上のオーバーヘッドはごく小さいと考えられます。
Errno::ECHILDを握りつぶす実装であれば、「すでに何か別の箇所で wait されていた」パターンでも安全に動作します。ワーカー異常終了のハンドリングは「静かに除外して shutdown を進める」方向なので:
- 個々のワーカー側でのログやエラー検出は、これまで通り ワーカー側orテスト側で適切にログを出す 必要があります(この PR は主にハング防止のための変更)。
- 参考情報 (あれば)
- 対応する Issue: #57052
- 内容: 並列テストのシャットダウン時にハングする問題の報告。
- 関連 PR:
- #55794:
Parallelization#shutdown内でのWNOHANGスイープを導入した以前の修正。- これは「
Server#shutdown前にすでに死んでいるワーカー」のみを対象としており、今回の PR はそのカバー範囲を「Server#shutdown中に死ぬワーカー」にまで拡張する位置付けです。
- これは「
- #55794:
並列テストが CI で時々 90 秒程度ハングして落ちる、といった現象がある場合、この修正が取り込まれた Rails バージョンに上げることで解消が期待できます。
#57047 Add test coverage for Error#initialize_dup, #hash, and #detail
マージ日: 2026/3/27 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveModel::Error のテスト不足だったメソッド(initialize_dup,hash,detail)に対して、専用のテストが追加されました。挙動の仕様をテストで明文化することで、error.rbのテストカバレッジが実質的に埋められています。
- 変更内容の詳細
このPRは実装コードには手を入れず、activemodel/test/cases/error_test.rb に29行のテストコードを追加しただけの変更です。主に以下3点を検証しています。
2-1. Error#initialize_dup のテスト
目的: dup した ActiveModel::Error オブジェクト同士で options が共有されず、片方の変更がもう片方に漏れないことを保証する。
想定されるテストイメージとしては:
error = ActiveModel::Error.new(record, :name, :too_short, count: 3)
duped = error.dup
# dup 後に一方の options を変更しても
duped.options[:count] = 5
# 元の error の options に影響しないことを確認
assert_equal 3, error.options[:count]
assert_equal 5, duped.options[:count]これにより、initialize_dup が options をディープコピー(少なくとも別オブジェクトとしてコピー)していることをテストで保証しています。
その結果、バリデーションエラーを複製した上でメッセージやオプションを個別に変更するような処理において、副作用が紛れ込む可能性を防ぎます。
2-2. Error#hash と #== の整合性テスト
目的: == で「等しい」と判定される ActiveModel::Error インスタンス同士が、同じ hash 値を返すことを保証する。
Ruby の Hash や Set の正しい動作には、以下の契約が必須です。
a == bならばa.hash == b.hashでなければならない
テストのイメージ:
error1 = ActiveModel::Error.new(record, :name, :too_short, count: 3)
error2 = ActiveModel::Error.new(record, :name, :too_short, count: 3)
assert_equal error1, error2
assert_equal error1.hash, error2.hash
set = Set.new([error1])
assert_includes set, error2 # hash と == が整合していないと失敗しうるこのテストにより、ActiveModel::Error を Hash のキーや Set の要素として使うケース(エラーの一意化・重複排除など)で、期待どおりに動作することがテストレベルで保証されます。
2-3. Error#detail のエイリアス挙動テスト
目的: detail が details の単なるエイリアスとして同じ結果を返すことを確認する。
イメージ:
error = ActiveModel::Error.new(record, :name, :too_short, count: 3)
assert_equal error.details, error.detailこれにより、detail メソッドを利用している既存コードが、details の挙動に追従することを前提にしても安全である、という契約をテストで固定しています。
- 影響範囲・注意点
- 実装コードには一切変更がなく、あくまで「既存仕様のテストによる固定」が目的の PR です。そのため、このPR単体によるランタイム挙動の変化や互換性の問題はありません。
- ただし、以下の仕様が「テストで固定」された意味は大きいです:
Error#dupでoptionsが共有されないことError#hashと#==の契約(等価なオブジェクトは同じ hash)detailとdetailsが同一の結果を返すこと
将来的にこれらを変えようとするとテストが落ちるため、互換性を意識した変更設計が必要になります。
ActiveModel::Errorを拡張したり、これを前提としたライブラリ・アプリケーションを開発している場合、上記の挙動を仕様として安心して頼ることができます。
- 参考情報 (あれば)
- 対象クラス:
ActiveModel::Error- エラー1件を表すクラスで、
record,attribute,type,optionsなどを持つ
- エラー1件を表すクラスで、
- Ruby における
hashと==の契約(Hash/Set 利用時の前提)
https://docs.ruby-lang.org/ja/latest/doc/spec=2fcomparison.html - 変更ファイル:
activemodel/test/cases/error_test.rbのみ(+29行 / -0行)
#57044 Add test coverage for ActiveModel::Errors#import
マージ日: 2026/3/27 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveModel::Errors の公開メソッドimportに対して、これまで存在しなかった専用のテストが追加されました。機能追加や挙動変更はなく、既存の挙動を明示的に保証するためのテストのみの変更です。
- 変更内容の詳細(あればサンプルコードも含めて)
追加されたテストの目的
ActiveModel::Errors#import は、他オブジェクトのエラーを現在の Errors に取り込むための公開 API ですが、これまで merge! 経由で間接的にしかテストされていませんでした。本 PR では、この import を直接テストし、以下の点を確認しています。
- インポートされたエラーが
NestedErrorとしてラップされること NestedErrorが元のエラー (inner_error) を保持していること:attributeオプションで属性名を上書きできること(文字列を渡してもシンボルに変換されること):typeオプションでエラー種別を上書きできること(文字列を渡してもシンボルに変換されること)
想定されるテストコードのイメージ
実際のテストは activemodel/test/cases/errors_test.rb に 41 行追加されています。構造としては概ね以下のような内容になっていると考えられます(概念的なサンプル):
def test_import_wraps_error_as_nested_error
source = ActiveModel::Errors.new(Object.new)
source.add(:name, :blank)
target = ActiveModel::Errors.new(Object.new)
# ここで import を直接呼ぶ
source.each do |error|
target.import(error)
end
error = target.first
# 1. NestedError であること
assert_instance_of ActiveModel::Errors::NestedError, error
# 2. inner_error を保持していること
assert_equal source.first, error.inner_error
end
def test_import_with_attribute_override
source = ActiveModel::Errors.new(Object.new)
source.add(:name, :blank)
target = ActiveModel::Errors.new(Object.new)
source.each do |error|
target.import(error, attribute: "title") # String で指定
end
error = target.first
# attribute が上書きされ、シンボル化されている
assert_equal :title, error.attribute
end
def test_import_with_type_override
source = ActiveModel::Errors.new(Object.new)
source.add(:name, :blank)
target = ActiveModel::Errors.new(Object.new)
source.each do |error|
target.import(error, type: "too_short") # String で指定
end
error = target.first
# type が上書きされ、シンボル化されている
assert_equal :too_short, error.type
end実際のテスト名や細部は異なる可能性がありますが、論点としては上記の 4 点がカバーされています。
- 影響範囲・注意点
- 実装コードには一切変更がなく、テストファイル (
activemodel/test/cases/errors_test.rb) のみが追加・修正されています。 - そのため、ランタイムの振る舞い・API 仕様・パフォーマンスに関する変更や非互換はありません。
- 既に
ActiveModel::Errors#importを利用しているアプリケーションに影響はありませんが、以下の挙動が「テストで固定された仕様」として今後も維持される可能性が高くなりました:- import したエラーは必ず
NestedErrorであること - 文字列の
attribute/typeを渡した場合にシンボルへ変換されること - 元エラーを
inner_errorとして参照できること
- import したエラーは必ず
これにより、将来 import の内部実装を変更する際にも、上記仕様を壊さないよう CI で検知できるようになります。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57044
- 関連クラス:
ActiveModel::Errors,ActiveModel::Errors::NestedError - 関連メソッド(同じくエラー統合に関与するもの):
ActiveModel::Errors#merge!ActiveModel::Errors#addActiveModel::Errors#each
#57048 Add test coverage for NestedError#inner_error, #raw_type, and #options
マージ日: 2026/3/27 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveModel::NestedErrorが持つinner_error,raw_type,optionsの3つのアクセサに対して、これまで不足していた専用テストを追加するPRです。振る舞い自体の変更はなく、テストカバレッジを補強するだけの変更です。
- 変更内容の詳細
対象クラス: ActiveModel::NestedError
変更ファイル: activemodel/test/cases/nested_error_test.rb (+27行)
主な追加テストは以下の3点です。
2-1. inner_error のテスト
- 目的:
NestedError#inner_errorが、元のエラーオブジェクトそのもの(同一オブジェクト)を返すことを保証する。 - テストでは
assert_sameを使い、オブジェクトIDレベルで同一であることを検証しています。
イメージコード:
inner = ActiveModel::Error.new(record, :name, :blank)
nested = ActiveModel::NestedError.new(
inner,
attribute: :user_name,
type: :invalid
)
assert_same inner, nested.inner_errorこれにより、NestedError が内包するエラーをコピーや再生成するのではなく、同じエラーインスタンスをラップしているだけである、という契約がテストで明示されました。
2-2. raw_type の委譲のテスト
- 目的:
NestedError#raw_typeが、NestedError自身で独自に持つ値ではなく、内側のエラー(inner_error)から委譲されることを保証する。 - 「
NestedError側で type を上書きしていても、raw_typeはオリジナルのエラーの type を指す」という関係性を確認しています。
イメージコード:
inner = ActiveModel::Error.new(record, :name, :blank) # inner.raw_type == :blank
nested = ActiveModel::NestedError.new(
inner,
attribute: :user_name,
type: :too_short # overrideされたtype
)
assert_equal :blank, nested.raw_typeこれにより、次のような設計がテストで固定されます。
type… 表に見せる(上書き後の)エラー種別raw_type… 内側のエラーが本来持っている種別(生の type)
2-3. options の委譲のテスト
- 目的:
NestedError#optionsが、override_optionsで attribute や type を上書きしても、オプションは内側のエラー由来のものを返すことを保証する。 - つまり「オリジナルエラーのコンテキスト(
countなど)は保持しつつ、外側で attribute/type だけ差し替える」という設計を確認しています。
イメージコード:
inner = ActiveModel::Error.new(
record,
:name,
:too_short,
count: 3
)
nested = ActiveModel::NestedError.new(
inner,
attribute: :user_name,
type: :invalid # typeだけ差し替え
)
assert_equal({ count: 3 }, nested.options)これにより、NestedError は「元のエラー情報(options)を温存しつつ、属性名や種類だけ“外側”の事情で変えられるラッパー」であることがテストで明示されました。
- 影響範囲・注意点
- 動作仕様の変更は一切なく、「現在の挙動をテストで固定した」だけの変更です。
- 今後、
NestedErrorの以下の挙動を変えるとテストが落ちるようになります:inner_errorが別インスタンスを返すように変えるraw_typeをNestedError独自に持つように変えるoptionsをNestedError側で上書き/別管理するように変える
- そのため、
NestedErrorを拡張・修正する際は、- 「どこまでを inner_error から委譲し、どこからを NestedError 独自とするか」
- 既存利用コードが
raw_typeやoptionsに何を期待しているか を意識する必要があります。
- 参考情報 (あれば)
- 対象クラス:
ActiveModel::NestedError- ActiveModel の複雑なバリデーションエラー(ネストした属性、関連モデルなど)を扱う際のラッパークラス。
- このPRはテストのみの変更のため、CHANGELOG 更新は不要と明示されています。
#57049 Add test coverage for Errors#to_hash with full_messages and #uniq!
マージ日: 2026/3/27 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveModel::Errors に対して、これまで直接はテストされていなかったto_hash(true)とuniq!の挙動を確認するテストが追加されています。挙動変更はなく、テストカバレッジの補完のみです。
- 変更内容の詳細(あればサンプルコードも含めて)
対象メソッド
ActiveModel::Errors#to_hash(full_messages = false)ActiveModel::Errors#uniq!(内部的には委譲されたメソッド)
to_hash(true) のテスト追加
これまで to_hash はデフォルト引数(to_hash や to_hash(false))についてはテストされていましたが、full_messages: true 相当となる to_hash(true) が直接はテストされていませんでした。
今回のPRでは、以下を確認するテストが追加されています:
to_hash(true)を呼び出したとき、- キー: 属性名
- 値: 「属性名を含んだフルメッセージ」の配列
で構成されるハッシュが返ってくること
イメージとしては、例えば以下のようなモデルにエラーが付いている場合:
user = User.new
user.errors.add(:name, :blank) # "can't be blank"
user.errors.add(:email, "is invalid") # "is invalid"to_hash の違いは以下のようになります:
user.errors.to_hash
# => { name: ["can't be blank"], email: ["is invalid"] }
user.errors.to_hash(true)
# => { name: ["Name can't be blank"], email: ["Email is invalid"] }
# ↑属性名が含まれる "full messages"この「to_hash(true) が full_messages 相当を返す」というコードパスを、明示的にテストでカバーしています。
uniq! のテスト追加
ActiveModel::Errors は、同じエラー内容を重複して持つことができますが、uniq! によって重複するエラーオブジェクトを削除できます。これまではこの委譲メソッド自体がテストされていませんでした。
今回のテストでは、例えば以下のようなケースを検証していると考えられます:
user = User.new
user.errors.add(:name, "is invalid")
user.errors.add(:name, "is invalid") # 同じエラーを2つ追加
user.errors.size
# => 2
user.errors.uniq!
user.errors.size
# => 1 # 重複が取り除かれていることを確認テストでは、同一属性・同一メッセージといった「同じエラーオブジェクト」に対して uniq! を呼ぶことで、エラー配列が縮約されることを確認しています。
- 影響範囲・注意点
影響範囲
- コード変更はテストファイル (
activemodel/test/cases/errors_test.rb) のみで、本体実装には変更がありません。 - 現在の
ActiveModel::Errors#to_hashおよび#uniq!の挙動はそのままで、仕様変更や破壊的変更はありません。
- コード変更はテストファイル (
注意点 / 認識しておくとよいこと
- 今後これらのメソッドの挙動を変えると、今回追加されたテストが落ちるため、「
to_hash(true)はフルメッセージを返す」「uniq!はエラーの重複を排除する」という挙動が事実上テストで固定されます。 as_json(full_messages: true)は内部的にto_hash(true)相当の経路を通るため、to_hash(true)の仕様がテストで明文化されたことで、as_jsonの期待挙動もより堅く保証される形になります。
- 今後これらのメソッドの挙動を変えると、今回追加されたテストが落ちるため、「
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57049
- 関連API(現行ドキュメントはRailsガイド/APIリファレンスを参照):
ActiveModel::Errors#to_hash(full_messages = false)ActiveModel::Errors#full_messagesActiveModel::Errors#uniq!
#57046 Add test coverage for Errors#delete with type and options
マージ日: 2026/3/27 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveModel::Errors#delete が本来サポートしているtypeとoptionsを指定した削除挙動について、新たにテストが追加された PR です。これにより、既存機能のフィルタリングロジックに対して直接的なテストカバレッジが付きました。
- 変更内容の詳細
対象: ActiveModel::Errors#delete
既存仕様として delete は以下のインターフェイスを持ちます:
errors.delete(attribute, type = nil, **options)今回の PR では、以下のケースに対するテストが activemodel/test/cases/errors_test.rb に追加されています。
2-1. 属性+type での削除
errors.add(:name, :blank)
errors.add(:name, :too_short, count: 3)
deleted = errors.delete(:name, :blank)テストで確認している点:
delete(:name, :blank)はname属性かつ type=:blankのエラーだけを削除する- 同じ
nameに付いている:too_shortなど他 type のエラーは残る deleteの戻り値として削除されたメッセージが返る(type を指定したときの戻り値の挙動を確認)
2-2. 属性+type+options での削除(完全一致)
同じ属性・同じ type だが options が異なる複数エラーがあるケース:
errors.add(:name, :too_short, count: 3)
errors.add(:name, :too_short, count: 5)
deleted = errors.delete(:name, :too_short, count: 5)テストで確認している点:
delete(:name, :too_short, count: 5)は- 属性
:name - type
:too_short - options
{ count: 5 }
すべてが一致するエラーだけを削除する
- 属性
count: 3のエラーは残る(type が同じでも options が違えば削除されない)- 戻り値に削除されたメッセージが返る
2-3. 戻り値の確認
従来、delete(:attribute) だけのテストでは、削除結果の戻り値や type/options 指定時の戻り値挙動が十分にカバーされていませんでした。
今回は以下を明示的にテストしています:
- type 指定ありの
deleteが「削除されたエラーメッセージ」を返すこと - options まで指定した場合も同様に、削除したものに対応するメッセージが返ること
- 影響範囲・注意点
- 実装の挙動変更はなく、「テストの追加のみ」であるため、アプリケーションのランタイム挙動には影響しません。
- ただし、次のような前提が公式テストとして明文化されたことで、今後の Rails バージョンでも以下の仕様が「期待される挙動」として固定化される傾向になります:
errors.delete(attribute, type)は「その attribute かつその type のエラーのみを削除する」errors.delete(attribute, type, **options)は「attribute, type, options がすべて一致するエラーのみを削除する」(部分一致ではない)- 上記削除時に、削除したエラーのメッセージを戻り値として返す
このため、自前で Errors に似た API を実装している場合や、ActiveModel::Errors をモンキーパッチしている場合は、この仕様に合致しているかを改めて確認するとよいです。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57046
- 関連するクラス:
ActiveModel::Errors - CHANGELOG への追記はなく、完全にテスト追加のみの変更として扱われています。
#57043 Add test coverage for ActiveModel::Error#strict_match?
マージ日: 2026/3/27 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveModel::Error#strict_match?に対して専用のテストが追加され、属性・タイプ・オプションが「完全一致」しているかを判定する挙動が明示的に保証されるようになりました。機能追加や仕様変更ではなく、既存のパブリック API のテスト補完のみです。
- 変更内容の詳細(あればサンプルコードも含めて)
追加されたテストの主旨
ActiveModel::Error#match? は従来からテストが充実していましたが、
「すべてのオプションが一致するか」を見る strict_match? は未テストでした。
このPRでは以下を検証するテストが activemodel/test/cases/error_test.rb に追加されています。
- 属性・タイプ・オプションがすべて一致すると true
- 属性が異なれば false
- タイプが異なれば false
- オプション値が異なれば false
- チェック側のオプションが足りない場合も false
- コールバック系・メッセージ系のオプションは比較対象から除外される
strict_match? の仕様確認ポイント
strict_match? は以下を満たすときに真になります:
attributeが同じtypeが同じ- 比較対象に渡したオプションと、エラーが保持しているオプションが
「コールバック系・メッセージ系を除いたものについて」完全一致している
「コールバック系・メッセージ系」として無視されるオプションは:
:if:unless:on:allow_nil:allow_blank:strict- メッセージ関連オプション(
messageなど)
これにより、バリデーションの実行条件やメッセージカスタマイズに関するオプションは、strict_match? の一致判定から除外されます。
想定されるテストイメージ(簡略例)
PR自体はテストファイルのみ +44 行ですが、挙動のイメージはだいたい以下のようになります(疑似コード):
error = ActiveModel::Error.new(record, :name, :too_short, count: 3, allow_nil: true)
# 属性・タイプ・オプション(allow_nil を除く)が完全一致 → true
error.strict_match?(:name, :too_short, count: 3) # => true
# 属性が違う → false
error.strict_match?(:email, :too_short, count: 3) # => false
# タイプが違う → false
error.strict_match?(:name, :invalid, count: 3) # => false
# オプション値が違う → false
error.strict_match?(:name, :too_short, count: 5) # => false
# 必要なオプションが足りない → false
error.strict_match?(:name, :too_short) # => false
# コールバック系・メッセージ系は無視して判定
error_with_callbacks = ActiveModel::Error.new(
record,
:name,
:too_short,
count: 3,
if: :some_condition,
message: "is too short"
)
# if/message は比較されないので、count が一致していれば true
error_with_callbacks.strict_match?(:name, :too_short, count: 3) # => true- 影響範囲・注意点
- ランタイムの挙動変更は一切なく、テスト追加のみのPRです。
- これまで暗黙的だった
strict_match?の仕様(特に「どのオプションが比較対象になるか」)がテストにより固定化されるため、今後仕様を変える場合はテスト変更を伴うことになります。 strict_match?を利用しているコードは、
「バリデーション条件やメッセージの違いは無視して、論理的なエラー内容(属性・タイプ・意味のあるオプション)の完全一致を見ている」
という前提が正しいことが改めて保証された形になります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57043
- 対象クラス:
ActiveModel::Error - 既存メソッドとの違い:
match?… ゆるい一致(特定の条件を満たせば true)strict_match?… コールバック/メッセージ系を除いたオプションの完全一致が必要
#57045 Add test coverage for EnvironmentInquirer predicates
マージ日: 2026/3/27 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveSupport::EnvironmentInquirerに用意されているdevelopment?,test?,production?の各述語メソッドに対して、これまで不足していたテストを追加する PR です。合わせて、カスタム環境(例:"staging")での振る舞いがStringInquirerにフォールバックすることや、local?が非ローカル環境でfalseを返すことも明示的にテストしています。
- 変更内容の詳細(あればサンプルコードも含めて)
追加されたテストのポイント
対象クラスは ActiveSupport::EnvironmentInquirer で、以下の挙動がテストされています。
development?の挙動"development"のときtrue"test","production"のときfalse
test?の挙動"test"のときtrue"development","production"のときfalse
production?の挙動"production"のときtrue"development","test"のときfalse
カスタム環境でのフォールバック動作
"staging"のようなカスタム環境では、development?,test?,production?はすべてfalseになることを確認- これは、組み込み 3 環境以外では
EnvironmentInquirerがStringInquirerの通常の挙動にフォールバックしていることを保証するテストです。
local?の確認"staging"のような非ローカル環境でlocal?がfalseになることを確認- もともと
local?についてはテストがありましたが、カスタム環境との組み合わせのケースを明示する形で追加されています。
イメージしやすいサンプルコード
PR で書かれているテスト内容はだいたい以下のようなイメージです(擬似コード):
env = ActiveSupport::EnvironmentInquirer.new("development")
assert env.development?
refute env.test?
refute env.production?
env = ActiveSupport::EnvironmentInquirer.new("test")
assert env.test?
refute env.development?
refute env.production?
env = ActiveSupport::EnvironmentInquirer.new("production")
assert env.production?
refute env.development?
refute env.test?
env = ActiveSupport::EnvironmentInquirer.new("staging")
refute env.development?
refute env.test?
refute env.production?
refute env.local?実際には activesupport/test/environment_inquirer_test.rb に 30 行程度のテストコードが追加されているだけで、プロダクションコードの変更はありません。
- 影響範囲・注意点
- 影響範囲は テストコードのみ であり、ランタイム挙動や API は一切変更されていません。
- すでに実装されていた最適化済みメソッド
development?,test?,production?の仕様をテストで「固定」した形になるため、今後これらの挙動を変えたい場合はテストの更新が必須になります。 - カスタム環境(例:
"staging","review","sandbox"など)でdevelopment?,test?,production?,local?がデフォルトではfalseである、という前提が公式にテストされるようになりました。
これは「Rails 本体としてその仕様を前提にしている」と読めるため、アプリ側でこれらのメソッドに依存したロジックを書く場合も、同じ前提でコードを書いて問題ないと言えます。 - CHANGELOG は更新されておらず、「テストのみの変更」と明示されています。
- 参考情報 (あれば)
ActiveSupport::EnvironmentInquirerは、Rails.envが返すオブジェクトのクラスとして利用され、Rails.env.production?のような問い合わせを可能にするためのクラスです。EnvironmentInquirerはStringInquirerのサブクラスで、development?,test?,production?など一部の環境名についてはmethod_missingを通らない高速な専用メソッドを提供しています。本 PR は、その「最適化パス」がStringInquirerベースの従来挙動と同じ結果になることを保証するテストを追加したものと位置付けられます。
#57071 Fix titleize to capitalize unicode lowercase letters
マージ日: 2026/3/27 | 作成者: @EldinGuzin
- 概要 (1-2文で)
Rails のActiveSupport::Inflector#titleizeが、ASCII 以外の小文字(đ, é, ü, ñ, ć など)も正しく先頭大文字化できるように、正規表現を[a-z]から Unicode の小文字クラス\p{Ll}に変更した PR です。これにより、多言語テキストでのタイトル化処理が期待通りに動作します。
- 変更内容の詳細
何を直したか
従来の titleize は内部で次のような正規表現を用いていました(概念的な例):
# 変更前(イメージ)
string.gsub(/(?:^|\s|["'([{])([a-z])/) { $1.upcase }[a-z] は ASCII の小文字 a〜z のみを対象にするため、以下のような Unicode の小文字はマッチせず、そのまま残っていました。
- đ, é, ü, ñ, ć など
PR の説明にある挙動:
# 変更前
titleize("ćasim đipa") # => "ćasim đipa" # どこも変わらない
# 変更後
titleize("ćasim đipa") # => "Ćasim Đipa"修正内容
正規表現中で「小文字 1 文字」を表していた [a-z] を、Unicode の「小文字一般」を表す \p{Ll} に置き換えています。
# 実際の変更(概念的)
- /([a-z])/
+ /(\p{Ll})/\p{Ll} は「Letter, lowercase(小文字の文字)」という Unicode プロパティクラスで、ラテン文字以外も含むあらゆる小文字のコードポイントが対象になります。
テスト追加
activesupport/test/inflector_test_cases.rb に、Unicode 小文字を含むケースが 1 行追加されています。
例として PR 説明にある:
assert_equal "Ćasim Đipa", ActiveSupport::Inflector.titleize("ćasim đipa")といった形式のテストが追加されたと考えられます(実際の記述もほぼこれに相当)。
- 影響範囲・注意点
影響範囲
ActiveSupport::Inflector.titleizeを直接/間接的に利用している箇所:- モデル名や属性名から画面表示用タイトルを生成しているヘルパー
- I18n を使わず、Inflector ベースでラベルを生成している独自コード
- ドキュメント生成やレポートタイトルなど、「文字列 → タイトルケース」変換を多言語で行うコード
主な挙動の変化
- これまで「先頭がユニコード小文字の単語」はそのまま小文字で残っていたのに対し、今後は正しく大文字化されます。
- ASCII 文字のみ扱うアプリではほぼ影響ありませんが、多言語対応アプリでは表示結果が変わる可能性があります。
- 例: 画面タイトル、ボタンラベルなどが「今までたまたま小文字だったが、本来の期待どおり大文字になる」といった見た目の差分が出ます。
互換性・注意点
- マッチ対象の拡大により、「あえて非 ASCII の先頭文字を小文字のまま残す」ことを前提にしていたロジックがあると、挙動が変わります(そのような前提はまず推奨されませんが)。
- Ruby の正規表現が
\p{Ll}を解釈するのは、通常の Rails がサポートする Ruby バージョンなら問題ありませんが、非常に古い Ruby を使っている環境では互換性に注意が必要です(2026 年時点の Rails を使うなら通常気にしなくてよい前提です)。
- 参考情報 (あれば)
Ruby の Unicode プロパティクラス:
\p{Ll}: Lowercase Letter(小文字の文字)\p{Lu}: Uppercase Letter(大文字の文字)- 参考:
Regexpと Unicode プロパティ(Ruby リファレンス)
Rails 側の関連メソッド:
ActiveSupport::Inflector.titleizeString#titleize(ActiveSupport による拡張。内部的には Inflector を利用)
この PR によって、titleize は英語以外の言語にもより自然に対応できるようになり、「国際化対応の基本的な文字列整形」としての信頼性が上がっています。
#57074 Update yarn install command to support immutable and frozen-lockfile in Dockerfile.tt
マージ日: 2026/3/27 | 作成者: @javier-menendez
- 概要 (1-2文で)
Rails のアプリ生成時に使われるDockerfile.ttの Yarn インストールコマンドが更新され、Yarn が Corepack 管理かどうかに応じて--immutableまたは--frozen-lockfileを使い分けるようになりました。これにより、Yarn 1 系/Yarn 2+(Berry)や Corepack 利用など、さまざまな環境でより安全かつ互換性の高いインストールが行えるようになります。
- 変更内容の詳細(あればサンプルコードも含めて)
何が変わったか
対象ファイル:railties/lib/rails/generators/rails/app/templates/Dockerfile.tt
アプリ生成 (rails new myapp --docker など) 時にテンプレートから生成される Dockerfile の中で、yarn install コマンドに渡すフラグが 1 行変更されています。
PR 説明から読み取れる挙動:
- Yarn が Corepack によって管理されている場合
→yarn install --immutable - それ以外の場合 (従来の Yarn 1 など)
→yarn install --frozen-lockfile
実際のテンプレートでは、環境に応じて上記を切り替えるための記述に 1 行置き換えが入っています(擬似コードイメージ):
# 以前(イメージ)
RUN yarn install --frozen-lockfile
# 以後(イメージ)
# Corepack 管理の Yarn なら --immutable、それ以外は --frozen-lockfile を使用
RUN if yarn -v 2>/dev/null | grep -q 'berry'; then \
yarn install --immutable; \
else \
yarn install --frozen-lockfile; \
fi※上記は動作イメージであり、実際のテンプレートの条件分岐ロジックや判定方法は PR 内の実装に依存します。
なぜこの変更なのか
- Yarn 1 系では厳密な lockfile 検証に
--frozen-lockfileを使用 - Yarn 2+ / Berry では
--immutableオプションが推奨であり、--frozen-lockfileは非推奨・無視される、あるいは望まない挙動になる可能性がある - Corepack 経由で Yarn を使うプロジェクトが増えており、その場合 Yarn 2+ を前提に
--immutableを使うのが自然
Rails の Dockerfile テンプレートでこれを自動的に切り替えることで、開発者が自分で Dockerfile を調整しなくても「環境にあった正しいフラグ」が使われるようになります。
- 影響範囲・注意点
影響範囲
- 新しく生成される Rails アプリの Dockerfile のみ影響を受けます。
- 既存アプリの Dockerfile は自動では更新されません。
- Yarn を使ったアセットビルド (Webpacker / jsbundling-rails / Vite など) を Docker 内で行っている場合、その
yarn installフェーズに影響します。
実務的な影響
- Corepack + Yarn 2+ (Berry) を使っている人にとっては改善:
- 推奨される
--immutableが使われることで、lockfile とnode_modulesの不整合が厳密に検出され、再現性の高いビルドが保証されます。
- 推奨される
- Yarn 1 を従来通り使っている人にとっても互換性維持:
- これまで通り
--frozen-lockfileが使われるため、挙動は本質的に変わりません。
- これまで通り
注意点
- lockfile (
yarn.lock) を手で編集したり、package.jsonだけ変更してyarn.lockを更新していない場合、--immutable/--frozen-lockfileいずれでも Docker ビルドがエラーで止まる ことがあります。- その場合はローカルで
yarn installを正しく実行し、更新済みのyarn.lockをコミットしてからビルドする必要があります。
- CI や本番ビルドで Dockerfile をベースにしている場合、Yarn のバージョンや Corepack の導入状況により
yarn installのオプションが変わることで、今まで黙認されていた lockfile 不整合がエラーとして顕在化する可能性があります。
- 参考情報 (あれば)
- Yarn 1 (Classic) ドキュメント:
--frozen-lockfile
https://classic.yarnpkg.com/en/docs/cli/install#toc-yarn-install-frozen-lockfile - Yarn 2+ (Berry) ドキュメント:
--immutable
https://yarnpkg.com/cli/install - Corepack (Node.js 同梱のパッケージマネージャブリッジ) 概要:
https://nodejs.org/api/corepack.html (英語)
#57063 [ci skip] Improve readability of note about redefining the id column
マージ日: 2026/3/26 | 作成者: @GyuhaWang
概要 (1-2文で)
Active Record Basics ガイド内で、「idカラムを再定義した場合の挙動」を説明している注意書きのエラーメッセージ表記を、1行で見やすくなるように整形したドキュメント変更です。コードや挙動には一切手を加えていません。変更内容の詳細(あればサンプルコードも含めて)
対象ファイル:
guides/source/active_record_basics.md「
idカラムを自前で定義したときに Active Record がどのようなエラーを出すか」を示す注意書き部分にある エラーメッセージの表記 が、複数行に分割されていて読みにくかったため、1行にまとめる変更が行われました。実際の diff は概ね以下のようなイメージです(内容は同じで、改行位置のみ変更):
md**Before (イメージ)** Note: If you redefine `id` column, Rails will raise an error like: `ActiveRecord::UnknownPrimaryKey: Unknown primary key for table users in model User.` **After (イメージ)** Note: If you redefine `id` column, Rails will raise an error like: `ActiveRecord::UnknownPrimaryKey: Unknown primary key for table users in model User.`これにより、ガイドを読むときにエラーメッセージ全体を一目で把握できるようになります。
- 影響範囲・注意点
- 影響範囲は Rails ガイドの文章表現のみ で、フレームワーク本体の挙動・API・パブリックインターフェイスには変更ありません。
idカラム再定義時にActiveRecord::UnknownPrimaryKeyが発生するという仕様やそのメッセージ内容自体は従来どおりです。- コードや既存アプリケーションへの互換性の影響はありません。テストや CHANGELOG の更新も不要なレベルのドキュメント修正として扱われています。
- 参考情報 (あれば)
- 該当ガイド: Active Record Basics(
idカラムの扱いと主キーの説明セクション) - エラークラス:
ActiveRecord::UnknownPrimaryKey - PR: #57063 “[ci skip] Improve readability of note about redefining the
idcolumn”
#57064 Bump Github Action upload-artifactversion to 7
マージ日: 2026/3/26 | 作成者: @neumayr
- 概要 (1-2文で)
Rails が生成する GitHub Actions CI ワークフローテンプレートにおいて、actions/upload-artifactのバージョン指定を最新のv7に更新する PR です。アプリケーションとプラグインの両方の GitHub CI テンプレートが対象です。
- 変更内容の詳細
対象ファイルは、Rails のジェネレータが作る GitHub Actions 設定テンプレートです。
railties/lib/rails/generators/rails/app/templates/github/ci.yml.ttrailties/lib/rails/generators/rails/plugin/templates/github/ci.yml.tt
これらの中で、GitHub Actions の upload-artifact アクションのバージョン指定が更新されています。
イメージとしては、以下のような変更です(実際の前バージョンは v4 や v3 などですが、概念的にはこのような差分):
- uses: actions/upload-artifact@v6
+ uses: actions/upload-artifact@v7
with:
name: test-results
path: tmp/test-resultsつまり、Rails new や Rails plugin new で生成される .github/workflows/ci.yml が、今後は actions/upload-artifact@v7 を使うようになります。
PR の Motivation から分かる点:
- 既存のテンプレートが GitHub Actions 公式の最新リリースに追随していなかったため更新。
- 先行する関連 PR (#56773) を踏まえたフォローアップ的な変更。
コードとしてのロジック追加や Ruby 側の仕様変更は一切なく、YAML テンプレートのバージョン指定のみです。
- 影響範囲・注意点
影響範囲:
- この PR のマージ以降に
rails new/rails plugin newで生成されるプロジェクトの GitHub Actions CI で、actions/upload-artifact@v7が利用されます。 - 既存プロジェクトの CI 設定は自動では変更されないため、影響を受けるのは「新しく生成される」ワークフローのみです。
注意点:
- v7 は GitHub 公式アクションのメジャーバージョンアップなので、GitHub 側のリリースノートにある挙動変更や非推奨事項を確認した上で、既存プロジェクトも必要に応じて手動で追随するとよいです。
- Rails 側では artifacts のパスやオプションには手を入れていないため、通常はそのまま問題なく動作する想定です。
- 参考情報 (あれば)
- GitHub Actions upload-artifact リポジトリ:
https://github.com/actions/upload-artifact - v7.0.0 リリースノート:
https://github.com/actions/upload-artifact/releases/tag/v7.0.0 - 関連 PR(Motivation で言及されているもの):
- Rails PR #56773(詳細はリンク先参照が必要)
#57067 Classify mysql error 1046 (ER_NO_DB_ERROR) as ConnectionFailed
マージ日: 2026/3/26 | 作成者: @clayharmon
- 概要 (1-2文で)
MySQL のエラーコード 1046「No database selected」を、ActiveRecord::ConnectionFailedとして扱うようにした変更です。これにより、接続プール使用時などに TCP 接続がサイレントに張り替わった場合でも、Rails の自動リトライ機構が働くようになります。
- 変更内容の詳細
背景・問題点
- MySQL 接続において、ロードバランサやプロキシなどの影響で TCP コネクションが「静かに」切断・再接続されるケースがあります。
mysql2はこの場合、自動的に新しい TCP 接続を張り直しますが、再接続後にUSE database_nameを自動で実行しません。- その結果、次のクエリ実行時に MySQL 側でエラー 1046 (
ER_NO_DB_ERROR: No database selected) が発生します。 - 現状の Rails では、1046 に対する特別扱いがなく、
translate_exception内でActiveRecord::StatementInvalidに分類されます。 retryable_connection_error?はActiveRecord::ConnectionFailed等のみをリトライ対象とするため、1046 が原因の失敗ではwith_raw_connectionの組込みリトライが動作しません。
この PR は、1046 を「接続周りの問題(実質的な接続リセット)」として扱い、自動リトライ対象に含めることで、アプリケーションコード側での個別対処を不要にすることを狙っています。
実際の変更点
MySQL エラーコードの定数追加
abstract_mysql_adapter.rbに以下を追加:rubyER_NO_DB_ERROR = 1046例外変換 (
translate_exception) の分岐追加既に 2006 (
CR_SERVER_GONE_ERROR), 2013 (CR_SERVER_LOST) をConnectionFailedにマッピングしている分岐に、1046 も含めるよう変更:rubycase error.errno when CR_SERVER_GONE_ERROR, CR_SERVER_LOST, ER_NO_DB_ERROR ActiveRecord::ConnectionFailed.new(message, error) else # 既存の処理 end※正確な記述は PR のコードに依存しますが、趣旨としてはこのように 1046 を
ConnectionFailed扱いにしている、という変更です。テスト追加
activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rbに、1046 が投げられたときにActiveRecord::ConnectionFailedに変換されることを検証するテストが追加されています。概ね以下のようなイメージのテストです:
rubydef test_mysql_error_1046_is_treated_as_connection_failed error = Mysql2::Error.new("No database selected") error.errno = 1046 assert_raises(ActiveRecord::ConnectionFailed) do @connection.translate_exception("SELECT 1", "SELECT 1", error) end endCHANGELOG 更新
activerecord/CHANGELOG.mdに、MySQL エラー 1046 をConnectionFailedとして扱うようになった旨が追記されています。
- 影響範囲・注意点
影響範囲
- 対象:
mysql2アダプタを利用している Rails アプリケーション - 主に影響するケース:
- コネクションプーリングを利用している
- プロキシ/ロードバランサなどが TCP コネクションをサイレントに切り替える可能性がある環境 (例: MySQL Proxy, RDS Proxy, L4/L7 LB など)
- この変更により:
- 「No database selected (1046)」が発生した場合、単発のクエリエラーではなく「接続が壊れた」と判断され、
with_raw_connectionの内部リトライが走ります。 - リトライ後の接続では
USE database_nameが正しく発行されるため、アプリケーションレベルでは一時的な 1046 の影響を受けにくくなります。
- 「No database selected (1046)」が発生した場合、単発のクエリエラーではなく「接続が壊れた」と判断され、
挙動上の注意点
- これまで:
- 1046 は
ActiveRecord::StatementInvalidとして扱われ、アプリケーションコードでrescue ActiveRecord::StatementInvalidしている場合に捕捉されていた可能性があります。
- 1046 は
- 今後:
- 同じケースが
ActiveRecord::ConnectionFailedとして扱われるようになります。 - アプリコードでエラー種別に依存した rescue をしている場合は、そのロジックが変わる可能性があります。
- 一方で、
ConnectionFailedは内部で再接続・リトライを試みるため、表面上は例外がアプリ層に上がらないケースも増えます。
- 同じケースが
運用面では、「原因不明の No database selected が sporadic に出ていた」のが解消される(または頻度が減る)可能性が高く、典型的にはプラスの変更と考えられます。
- 参考情報 (あれば)
- 類似として既に
ConnectionFailed扱いされている MySQL エラー:- 2006:
CR_SERVER_GONE_ERROR(MySQL server has gone away) - 2013:
CR_SERVER_LOST(Lost connection to MySQL server)
- 2006:
translate_exceptionとretryable_connection_error?は、ActiveRecord のコネクション管理と自動リトライの中核ロジックであり、今回の変更もその一環です。
これらをカスタマイズしているアプリ/ライブラリでは、1046 の扱いが変わる点を念頭に置くとよいです。
#57069 Make fast behavior the default for NumberToDelimitedConverter
マージ日: 2026/3/26 | 作成者: @willnet
- 概要 (1-2文で)
number_to_delimitedなどの数値区切り系ヘルパーで、オプション未指定時にも常に高速化された経路(fast path)が使われるようにデフォルト動作が修正されました。これにより、従来は暗黙に実行されていた低速な処理が、明示的に指定した場合にのみ使われるようになります。
- 変更内容の詳細
背景
- Rails では
number_to_delimited(例: 10000 -> "10,000")といったヘルパーに対して、高速化(fast path)が既に導入されていました。 - しかし、
delimiter_patternのデフォルト値にDEFAULT_DELIMITER_REGEXが設定されていたため、オプションを何も渡さない通常利用 (number_to_delimited(10000)) では、依然として古い低速な処理が実行されていたという問題がありました。 - 高速経路を使うには、
delimiter_pattern: nilを明示する必要があり、直感的でない上、意図した挙動とも考えにくい状態でした。
今回の変更の要点
NumberToDelimitedConverter内のDEFAULT_DELIMITER_REGEXが削除されました。- これにより、
delimiter_patternが明示的に指定されていない場合は「パターンなし」とみなされ、高速な処理経路が使われるようになります。
概念的なイメージ:
従来(問題のある挙動):
# 実際には delimiter_pattern にデフォルトの正規表現が入る
# -> 正規表現ベースの低速な処理が使われる
number_to_delimited(10000)
# => 古い(遅い)経路
# 明示的に nil を渡したときのみ高速化される
number_to_delimited(10000, delimiter_pattern: nil)
# => 新しい(速い)経路変更後:
# オプション未指定の場合は delimiter_pattern なし
# -> 常に高速な経路が使われる
number_to_delimited(10000)
# => 新しい(速い)経路
# delimiter_pattern を指定したときだけ、従来どおり正規表現ベースの処理
number_to_delimited(10000, delimiter_pattern: /.../)
# => 低速だが柔軟な経路実際の差分は小さく、activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb から DEFAULT_DELIMITER_REGEX の定義・利用が削除され、デフォルトで fast path が選ばれるように整理されています。
- 影響範囲・注意点
影響範囲
- 影響を受けるメソッド
主にActiveSupport::NumberHelperのnumber_to_delimited(およびそれを内部で利用するヘルパー)が高速になる影響を受けます。 - デフォルト利用の場合の挙動
多くのアプリケーションはnumber_to_delimited(1234567)のようにオプション無しで使っているため、- 表示フォーマットは変わらず
- 内部実装のみが高速化
される想定です。
注意点
delimiter_patternを使っていない場合
ほぼ影響は「パフォーマンス向上」のみで、フォーマット変更などの互換性問題は基本的にありません。delimiter_patternを積極的に使っている場合- 明示的に
delimiter_pattern: /.../を渡しているコードは、従来どおりそのパターンに基づく処理(低速)が行われます。 - つまり、「
delimiter_patternを指定する=柔軟だが遅い経路を選択する」という意図がより明示的になりました。
- 明示的に
- パフォーマンステスト
大量の数値フォーマットを行う箇所(例えば、レポート生成やダッシュボードのテンプレート)では、処理時間が変わる可能性があるため、ベンチマークを持っているプロジェクトでは差分計測すると効果が確認しやすいです。
- 参考情報 (あれば)
- 元となった最適化コミット:
https://github.com/rails/rails/commit/2d485aecf594da632b389e5f4ae87b302ea17fe0
(number_to_delimitedの fast path 実装が含まれる) - 対象クラス:
ActiveSupport::NumberHelper::NumberToDelimitedConverter
(activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb)
#57054 Cherry-pick security release commits onto main branch
マージ日: 2026/3/23 | 作成者: @jhawthorn
- 概要 (1–2文)
このPRは、セキュリティリリースで入った修正をmainブランチにチェリーピックしたもので、主に Active Storage 周りのファイルアクセス/ダウンロードの安全性向上と、Action View のタグ生成・HTMLセーフティ関連の修正が含まれています。例外画面テンプレートやString#html_safe周りも含めて、XSS/ディレクトリトラバーサル等の脆弱性を防ぐための変更と、それを担保するテスト追加が中心です。
- 変更内容の詳細
※実際の diff 全文は提示されていないため、ファイル構成と典型的な Rails セキュリティ修正パターンから読み取れる範囲での解説になります。
2-1. Action Pack: 例外画面テンプレート
- 変更ファイル:
actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb(+1/-1)actionpack/test/dispatch/debug_exceptions_test.rb(+12/-1)
想定される内容:
- 例外ページ(エラーページ)のレイアウトで、例外メッセージやパスなどを出力する箇所のエスケープ方法を調整し、XSS の可能性をさらに下げた変更と思われます。
debug_exceptions_test.rbにテストが追加されているので、「特定のエラー内容を含むリクエストを投げても、レスキューページが不正な HTML/JS をそのまま実行してしまわない」ことを確認しているはずです。
ポイント:
- 例外メッセージはしばしば外部入力を経由するため、
rawやhtml_safeを安易に使わず、h()/ 自動エスケープを徹底する方向の修正がよく行われます。
2-2. Action View: Tag Helper と HTML セーフティ
- 変更ファイル:
actionview/lib/action_view/helpers/tag_helper.rb(+5/-2)actionview/test/template/tag_helper_test.rb(+29/-0)activesupport/lib/active_support/core_ext/string/output_safety.rb(+11/-2)actionview/CHANGELOG.md(+4/-0)
主な意図:
- タグヘルパ(
tag,content_tag,tag.input,tag.aなど)の出力や属性値の扱いに関する安全性の強化。 String#html_safeとActiveSupport::SafeBuffer周りの仕様・挙動をより堅牢にし、「一度 html_safe が付いた文字列にユーザー入力を結合した結果が意図せずセーフ扱いになる」ような事態を防ぐ方向の変更が入っている可能性が高いです。
よくあるパターンの例:
unsafe = params[:q] # ユーザー入力
safe = "<strong>Hello</strong>".html_safe
# 以前のバグパターン:
result = safe + unsafe # -> result が html_safe のまま扱われ XSS となりうる
# 修正後:
# 結合時に unsafe が含まれると SafeBuffer が unsafe として扱われる or 適切にエスケープされるtag_helper_test.rb の追加テストから推測されるポイント:
- 属性値に
html_safeな文字列を渡した場合と、プレーン文字列を渡した場合の扱いの違いが明示的にテストされている可能性があります。 - デフォルトで自動エスケープが効き、
html_safeを付けた文字列のみを信頼する挙動をより厳密にしているはずです。
CHANGELOG.md には:
- 「タグヘルパが特定のケースで HTML セーフティを誤認していた問題を修正」
- などの記述が追加されているはずです。
2-3. Active Storage: ディスク・ストリーミング・Blob 周りのセキュリティ強化
- 変更ファイル(抜粋):
activestorage/app/controllers/active_storage/disk_controller.rb(+4/-0)activestorage/app/controllers/concerns/active_storage/streaming.rb(+8/-1)activestorage/app/models/active_storage/blob.rb(+21/-0)activestorage/lib/active_storage/service/disk_service.rb(+46/-2)activestorage/lib/active_storage/errors.rb(+4/-0)activestorage/lib/active_storage.rb(+14/-0)activestorage/lib/active_storage/engine.rb(+1/-0)activestorage/test/...一式 (controllers, models, services) 合計 ~200行以上のテスト追加activestorage/CHANGELOG.md(+31/-0)
ここが今回の PR のメインと思われます。
典型的なセキュリティ修正内容は次のようなものです:
2-3-1. DiskService: パス検証・ディレクトリトラバーサル対策
disk_service.rb が +46/-2 と大きく変わっているため、ローカルディスクへの保存/読み出しパスの扱いを強化している可能性が高いです。
よくある修正内容:
- join した結果が root パス配下から外れていないかの検証:
# イメージ (実際のコードは異なる可能性があります)
def path_for(key)
path = File.join(@root, key)
unless path.start_with?(File.expand_path(@root) + File::SEPARATOR)
raise ActiveStorage::InvalidPathError, "Invalid key: #{key.inspect}"
end
path
end../を含むキーや、絶対パス形式、不正なエンコーディングを持つキーなどを拒否。- 失敗時には新たに追加された
ActiveStorage::Errors内のカスタムエラー(例:ActiveStorage::InvariableError,ActiveStorage::IntegrityError,ActiveStorage::InvalidPathError等)を投げるように変更。
テスト (activestorage/test/service/disk_service_test.rb +137行) では:
- 「正常なキーは保存/読み出しできる」
- 「ディレクトリトラバーサルっぽいキーはエラーになる」
- 「存在しないファイルや壊れたパスでの動作」
などが細かくチェックされているはずです。
2-3-2. DiskController / Streaming: レスポンスヘッダと認可の厳格化
active_storage/disk_controller.rb や concerns/active_storage/streaming.rb の修正から推測できる点:
一時 URL、署名付き URL の検証をより厳格にしている可能性:
- トークンのタイムスタンプ・署名・パラメータの整合性チェックの強化。
- 不正パラメータ(例: 想定外の
dispositionやfilename)が入った場合に 4xx を返すように変更。
レスポンスヘッダの調整:
Content-Dispositionを生成する際に、ファイル名を適切にエスケープ。- 任意のレスポンスヘッダインジェクションを防ぐため、改行やコントロール文字を除去。
- Range リクエスト/ストリーミング処理での不正なオフセット・サイズ指定への防御。
サンプルイメージ:
# (イメージ)
def disposition_with_safe_filename(filename, disposition: :attachment)
filename = ActiveStorage::Filename.new(filename).sanitized
%(#{disposition}; filename="#{filename}"; filename*=UTF-8''#{ERB::Util.url_encode(filename)})
end2-3-3. Blob モデル・エラークラスの追加
active_storage/blob.rb(+21)- 不正なメタデータ/コンテンツタイプの扱いを厳格化。
- 署名付き ID から Blob を見つける際の
find_signedの失敗ハンドリング強化。 - 無効な状態の Blob を参照しようとした場合、新しいエラーを投げる等。
active_storage/errors.rb(+4)- 新規エラークラスの追加:
- 例:
ActiveStorage::FileNotFoundError,ActiveStorage::IntegrityErrorなど(実際の名前は diff 要確認)。
- 例:
- これにより、アプリ側で特定の Active Storage エラーを rescue しやすくなる。
- 新規エラークラスの追加:
active_storage.rb/engine.rb- 新しい設定オプションや、セキュリティ関連のデフォルト挙動 (例: 「ディスクサービスで不正キー使用時は例外を投げる」等) をフックするためのコードが追加されている可能性があります。
- 影響範囲・注意点
3-1. セキュリティ観点での影響
- この PR は「セキュリティリリースの cherry-pick」であるため、Rails アプリは基本的にこの変更を取り込むことが推奨されます。
- 主な守備範囲:
- XSS (エラーページ、ActionView のタグヘルパ、
html_safe/SafeBuffer の扱い) - ディレクトリトラバーサル・任意ファイルアクセス (Active Storage Disk Service)
- 不正な署名付き URL や不正ヘッダによるレスポンス分割などの防止
- XSS (エラーページ、ActionView のタグヘルパ、
3-2. 既存アプリへの互換性・挙動変化
Action View / SafeBuffer 周り
- これまで動いていた「やや危ない書き方」がエラーになったり、エスケープされて表示されるようになる可能性があります。
- 具体的には:
html_safeな文字列とユーザー入力を + 演算子などで結合していた箇所が、今後はエスケープされるため、見た目が微妙に変わることがあります。- テストで
assert_equal "<tag>#{raw(user_input)}</tag>"のような書き方をしていた場合、期待値を見直す必要があるかもしれません。
Active Storage Disk Service
- カスタムで
service: :diskを拡張している場合や、内部的にDiskService#path_forなどを直接呼んでいる場合:- 不正なキーを渡していると例外が投げられるようになり、これまで「たまたま動いていた」ケースが壊れる可能性があります。
- ローカルファイルシステムを多少「流用」していて、キーに
/や..を含めていたような実装は、セキュリティ観点からも見直す必要があります。
- カスタムで
Active Storage コントローラのレスポンス変更
- 一部の
disk/blob/direct_uploadsコントローラに追加テストが入っているため:- 不正パラメータを渡した際のステータスコードやレスポンスが変わっている可能性があります。
- 正常系の挙動は維持されるはずですが、E2E テストでレスポンスヘッダやファイル名を厳密に比較している場合は差分が出るかもしれません。
- 一部の
3-3. マイグレーション・設定変更の必要性
- 変更ファイルから見る限り、DB スキーマ変更 (migration) は含まれていません。
config/storage.ymlなどの設定形式も変わっているようには見えません。- ただし
ActiveStorageモジュール (lib/active_storage.rb) に新しい config オプションが追加されている可能性はあるため、CHANGELOG で確認しておくとよいです。
- 参考情報 (あれば)
- この PR 自体は「セキュリティリリースコミットの cherry-pick」という位置付けなので、詳細な説明は各セキュリティリリースのアナウンス(Rails の公式ブログ / セキュリティアドバイザリ)にまとまっている可能性が高いです。
- 追って確認しておくとよい資料:
- Rails 公式ブログのセキュリティリリース告知 (2026-03 頃の記事)
- 該当する Rails バージョンの
CHANGELOG.md(特にactionview/activestorage/activesupport) - Rails Guides:
- アプリ側でやるべきこと:
html_safeやrawを使っている箇所を改めて洗い出し、「ユーザー入力を混ぜていないか」「SafeBuffer の挙動に依存していないか」をチェックする。- Active Storage で Disk サービスを使っている場合、独自拡張やキーの扱い(ファイル名の生成など)を確認し、不正なキーを渡していないか検証する。
#56877 Fix FrozenError when deriving foreign key from inverse with composite foreign key
マージ日: 2026/3/23 | 作成者: @kirs
- 概要 (1-2文で)
belongs_to側が複合外部キー(配列のforeign_key)を持ち、その逆側のhas_one/has_manyがinverse_ofから外部キーを導出する場合にFrozenErrorが発生していたバグを修正する PR です。
凍結済みの配列に対して破壊的メソッドmap!を呼び出していた実装を、非破壊的なmap { ... }.freezeに置き換えることでエラーを回避しています。
- 変更内容の詳細
問題のシナリオ
以下のような構成を想定すると分かりやすいです。
class Comment < ApplicationRecord
# 複合外部キー (例)
belongs_to :blog_post,
foreign_key: [:blog_id, :post_id]
end
class BlogPost < ApplicationRecord
has_many :comments, inverse_of: :blog_post
endBlogPost 側の has_many :comments は inverse_of: :blog_post により、Comment の belongs_to :blog_post から外部キー情報を「導出 (derive)」します。
belongs_to側のforeign_keyが 配列(複合外部キー) の場合- Active Record の
derive_foreign_keyが、belongs_to側の reflection が保持している メモ化済み・凍結済みの配列 を返す - その返ってきた配列に対して、従来コードが
map!(破壊的変更)を行おうとしてFrozenErrorが発生
というのが不具合の根本原因です。
コード上の変更ポイント
該当箇所は activerecord/lib/active_record/reflection.rb の外部キー導出ロジックです。
元のイメージ(概念的なもの):
def derive_foreign_key
keys = inverse_of.foreign_key # ここで凍結済み配列が返るケースがある
keys.map! { |key| key.to_s } # ← FrozenError: can't modify frozen Array
keys.freeze
endこれを、破壊的な map! をやめて、新しい配列を生成してから凍結する 形に変更しています:
def derive_foreign_key
keys = inverse_of.foreign_key
keys = keys.map { |key| key.to_s }.freeze
end実際の PR では、map! + freeze というパターンをmap { ... }.freeze に置き換えただけの、最小限の修正です。
テストの追加
以下のファイルにテスト・テスト用モデルが追加されています:
activerecord/test/cases/reflection_test.rb- 逆側 (
inverse_of) から複合外部キーを導出する際にFrozenErrorが出ないことをカバーするテストを追加。
- 逆側 (
activerecord/test/models/sharded/blog_post.rbactiverecord/test/models/sharded/comment.rb
sharded 名前空間のテスト用モデルを使って、実際に複合外部キーの関連を定義した状態で
reflection が正しく動作することを検証しています。
- 影響範囲・注意点
影響範囲:
ActiveRecord::Reflectionにおける「外部キー配列の導出・変換部分」に限定された変更です。- 主に以下の条件に当てはまるアプリケーションに影響します:
belongs_toで複合外部キー(配列のforeign_key)を使用している- その逆側の
has_one/has_manyがinverse_ofを指定している
- これらの条件を満たす関連で、以前は
FrozenErrorによりアソシエーション定義・利用時にエラーになっていたケースが、正常に動くようになります。
挙動の互換性:
- 外部キー配列の中身(キー名の変換結果)は変わっておらず、「凍結済みの配列を返す」という契約も維持されています。
- 変更点は「どの配列インスタンスを凍結するか(元の配列を壊さない)」だけなので、一般的な利用では挙動互換です。
パフォーマンス:
map!をやめてmapで新しい配列を作るため、理論上は一時オブジェクトが増えますが、配列サイズは外部キー数(多くて数個)に限られるため、実務上のオーバーヘッドは無視できるレベルです。
注意点:
- 「reflection が返す
foreign_key配列は凍結されている」前提に依存したコード(破壊的変更をかけようとするなど)は、もともと誤りですが、この修正によっても引き続き凍結済みであるため、そのようなコードは引き続きFrozenErrorになります。 - この PR は 「他 reflection の内部状態を壊さない」 方向の修正なので、ライブラリ作者などで ActiveRecord の reflection API を内部まで触っている場合には、むしろ安全性が増しています。
- 「reflection が返す
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/56877
- 関連クラス:
ActiveRecord::Reflection::AssociationReflectioninverse_of/foreign_keyの導出ロジック周り
- 文脈:
- Rails の複合外部キーサポートやシャーディング関連(
shardedモデル)をテストする一環で、reflection の安全性・不変性の問題が顕在化したものと考えられます。
- Rails の複合外部キーサポートやシャーディング関連(
#57031 Add test coverage for ActiveModel::Errors#where
マージ日: 2026/3/23 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveModel::Errors#whereに対する専用テストが追加され、属性・タイプ・オプションごとのフィルタリング挙動が網羅的に検証されるようになりました。これにより、既存の公開APIの振る舞いがテストで明示され、将来的なリグレッション防止に役立ちます。
- 変更内容の詳細
このPRでは activemodel/test/cases/errors_test.rb に Errors#where 専用のテストが 5 件追加されています。実装コードの変更はなく、テストコードのみの追加です。
追加されたテストの内容は以下の通りです。
1) 属性によるフィルタリング
複数属性に対してエラーを追加し、where(:name) が name 属性のエラーだけを返すことを検証しています。
イメージとしては以下のようなテストが追加されています:
person = Person.new
person.errors.add(:name, :blank)
person.errors.add(:email, :invalid)
errors = person.errors.where(:name)
assert_equal 1, errors.size
assert errors.all? { |error| error.attribute == :name }ポイント:
- 同一オブジェクトに複数属性のエラーを追加
where(:attribute)で特定属性だけを抽出できることを確認
2) 属性 + タイプによるフィルタリング
同じ属性に対して異なるタイプのエラーを追加し、属性とタイプの両方でフィルタできることを検証しています。
例:
person.errors.add(:name, :blank)
person.errors.add(:name, :too_short, count: 3)
errors = person.errors.where(:name, :too_short)
assert_equal 1, errors.size
assert errors.all? { |e| e.attribute == :name && e.type == :too_short }ポイント:
where(attribute, type)の第二引数でエラータイプによる絞り込みができることを確認- 同一属性・別タイプのエラーが正しく除外されるかをテスト
3) 属性 + タイプ + オプションによるフィルタリング
タイプが同じでも、オプション(例: count: 2)が異なるエラーを区別できることを検証しています。
例:
person.errors.add(:name, :too_short, count: 2)
person.errors.add(:name, :too_short, count: 3)
errors = person.errors.where(:name, :too_short, count: 2)
assert_equal 1, errors.size
assert_equal 2, errors.first.options[:count]ポイント:
where(attribute, type, **options)のオプションマッチングを確認- オプションが完全一致するものだけを返す挙動を担保
4) マッチしない場合は空配列を返す
存在しない属性、タイプ、オプションを指定した場合、where が空配列 [] を返すことを確認しています。
例:
person.errors.add(:name, :blank)
assert_equal [], person.errors.where(:email)
assert_equal [], person.errors.where(:name, :too_short)
assert_equal [], person.errors.where(:name, :blank, count: 2)ポイント:
- 「見つからないときに nil ではなく空配列を返す」という契約をテストで固定
- 呼び出し側ロジックが
eachなどを前提にしても安全
5) 戻り値の型検証
where の結果に含まれる要素がすべて ActiveModel::Error インスタンスであることを確認しています。
person.errors.add(:name, :blank)
errors = person.errors.where(:name)
assert errors.all? { |e| e.is_a?(ActiveModel::Error) }ポイント:
whereが内部配列やハッシュではなく、ActiveModel::Errorオブジェクトの配列を返すことを明示- 公開APIの「戻り値の型」をテストで保証することで、将来の内部実装変更時の破壊的変更を防ぐ
- 影響範囲・注意点
実装コードの挙動は変わっていない
追加されたのはテストのみであり、ActiveModel::Errors#whereの実装自体には変更がありません。Errors#whereの仕様がテストとして固定化された
以下の点が挙動として「公式に」固定されたと考えられます。- フィルタ条件:
- 第1引数: 属性 (
attribute) - 第2引数: タイプ (
type, 任意) - 第3引数以降: オプション (
options, 任意・ハッシュマッチ)
- 第1引数: 属性 (
- 戻り値:
ActiveModel::Errorの配列 - マッチしない場合: 空配列
[]
- フィルタ条件:
今後のリファクタ時の制約になる可能性
テストが追加されたことで、たとえば- 戻り値を別オブジェクトに変える
- オプションマッチングの仕様を変える といった変更は、これらのテストを更新しない限り許されなくなります。
whereに依存しているアプリケーションにとっては安全性向上になりますが、Rails本体の内部実装変更の自由度は相対的に下がります。
- 参考情報 (あれば)
- 対象メソッド:
ActiveModel::Errors#where
Railsガイド(Active Model バリデーション)や API ドキュメント上で公開されているフィルタ用メソッドであり、errors.where(:name, :blank, count: 2)のように条件付きでエラーを取得する用途に使われます。 - このPRによって、
errors.whereの現状の振る舞いを前提にしたコードを書いても、将来の Rails バージョンで変わりにくくなったと考えられます。
#57032 Add test coverage for IsolatedExecutionState#key?, #delete, and #context
マージ日: 2026/3/23 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveSupport::IsolatedExecutionStateの公開メソッド#key?,#delete,#contextに対して、これまで不足していたテストが追加されました。機能追加や仕様変更ではなく、テストカバレッジを補完するためのPRです。
- 変更内容の詳細
追加されたテストの内容
activesupport/test/isolated_execution_state_test.rb に7つのテストが追加されています。
#key? に関するテスト (3件)
既存キーに対して
trueを返す- 事前に
IsolatedExecutionState[:foo] = :barのようにキーをセットし、IsolatedExecutionState.key?(:foo)がtrueであることを検証。
- 事前に
存在しないキーに対して
falseを返す- セットしていないキーに対して
key?を呼び、falseを返すことを検証。
- セットしていないキーに対して
内部状態が
nilの場合でも例外を出さず扱えること- 内部ストレージを「未初期化状態」(nil) にした上で
key?を呼び出し、例外が起きないこと・適切に扱えることを確認。 - これは
IsolatedExecutionStateの内部状態初期化ロジックが、key?経由でも安全に動作することの保証になります。
- 内部ストレージを「未初期化状態」(nil) にした上で
#delete に関するテスト (2件)
キーを削除し、その値を返す
- 例:ruby
IsolatedExecutionState[:foo] = :bar value = IsolatedExecutionState.delete(:foo) assert_equal :bar, value refute IsolatedExecutionState.key?(:foo) deleteの戻り値が削除した値であることと、実際にキーが削除されていることを検証。
- 例:
存在しないキーに対して
nilを返す- 未定義のキーで
deleteを呼び、nilが返ることを確認。 - Ruby の
Hash#deleteと同様の直感的な挙動が保証されます。
- 未定義のキーで
#context に関するテスト (2件)
:thread隔離レベルでは現在のスレッドを返す- 隔離レベルを
:threadに設定した状態でrubyとなることをテスト。assert_equal Thread.current, IsolatedExecutionState.context - 「どの単位で状態を隔離しているか」を知るための
contextが、スレッド隔離時に正しくスレッドを指すことを確認。
- 隔離レベルを
:fiber隔離レベルでは現在のファイバーを返す- 同様に、隔離レベルを
:fiberにした状態でrubyとなることを確認。assert_equal Fiber.current, IsolatedExecutionState.context - Fiber ベースの並行処理(例: async ランタイム)利用時に、コンテキストが正しく Fiber 単位で分離されていることをテストで保証しています。
- 同様に、隔離レベルを
テスト全体の状態
isolated_execution_state_test.rb内のテスト数は合計13件となり、27アサーション。- すべてグリーン (0 failures)。
- 影響範囲・注意点
- 本PRはテストファイルのみ変更しており、実装コード (
ActiveSupport::IsolatedExecutionState本体) には手が入っていません。 - したがって、ランタイムの挙動変更や互換性への影響はありません。既存アプリケーションの動作が変わることはありません。
- ただし、今後このクラスの実装を変更する際に、
key?が「未初期化状態(nil)でも安全に動作する」ことdeleteの戻り値や削除ロジックcontextが隔離レベルに応じてThread.current/Fiber.currentを正しく返すこと
がテストで強く縛られるため、実装変更時にはこれらの仕様を意識する必要があります。
- 参考情報 (あれば)
ActiveSupport::IsolatedExecutionStateは、Rails内部で「スレッドやファイバーごとに独立した状態を持つ」ための仕組みで、ログ用コンテキストやトランザクションスコープなどの管理に関係するクラスです。- 今回のテスト追加により、並行処理環境での状態管理周りのリファクタリングや最適化を行う際に、
key?,delete,context周りで回帰バグが起きにくくなります。
#57033 [ci skip] Fix punctuation in Rails autoloading and reloading constants docs
マージ日: 2026/3/23 | 作成者: @written-fresh
概要 (1-2文で)
このPRは、Railsガイド「autoloading and reloading constants」のドキュメント内にある句読点の誤りを修正するものです。コードや挙動の変更は一切なく、ドキュメント表現のみが微修正されています。変更内容の詳細
- 対象ファイル:
guides/source/autoloading_and_reloading_constants.md - 差分: 1行の修正(1行追加・1行削除の実質置き換え)
- 具体的な内容は英語ドキュメントの句読点(ピリオドやカンマなど)の誤りを正しい形に修正しただけであり、文意や説明内容自体は変わっていません。
- 技術的なサンプルコードや設定例などは変更されていません。
- 影響範囲・注意点
- 影響範囲:
- Rails本体のオートローディング/リローディングの挙動には一切影響しません。
- 生成されるガイド(HTML版など)の表示テキストのみが僅かに変わります。
- 注意点:
- アプリケーションのアップデート時に、このPRによってコードの修正が必要になるケースはありません。
- CIを回していない(
[ci skip])ことからも分かる通り、あくまでドキュメント上の表記修正であると判断できます。
- 参考情報 (あれば)
- PR番号: https://github.com/rails/rails/pull/57033
- 対象ガイド: Rails Guides – “Autoloading and Reloading Constants”
(Zeitwerk ベースの autoloading の説明があるセクションで、概念理解に影響しないレベルの句読点修正です。)
#57028 Require Ruby 3.3.1
マージ日: 2026/3/22 | 作成者: @byroot
- 概要 (1–2文で)
Rails本体および関連コンポーネントの対応Rubyバージョンを「3.3.0 以上」から「3.3.1 以上」に引き上げるPRです。理由は、Ruby 3.3.0 に Rails の一部機能を壊すバグがあり、かつCIで複数パッチバージョンをサポートするコストを負えないためです。
- 変更内容の詳細
主な変更点
すべての .gemspec ファイルに記載されている required_ruby_version の下限バージョンが 3.3.0 → 3.3.1 に変更されています。対象は以下のコンポーネントです:
- actioncable
- actionmailbox
- actionmailer
- actionpack
- actiontext
- actionview
- activejob
- activemodel
- activerecord
- activestorage
- activesupport
- rails (メタgem)
- railties
- tools/rail_inspector
- tools/releaser
変更イメージ(サンプル)
各 .gemspec はおおむね次のような差分になっています:
# 変更前
spec.required_ruby_version = ">= 3.3.0"
# 変更後
spec.required_ruby_version = ">= 3.3.1"コードロジックやAPIには一切手を入れず、サポート対象Rubyバージョンの宣言のみを更新するPRです。
背景となる問題
PR本文にある通り:
- Ruby 3.3.0 には Rails の一部を壊すバグが存在する
- その問題に対して、Ruby 3.3.1 で修正が入っている
- Rails側としては、3.3.0 向けのワークアラウンドを入れたり、CIで複数パッチバージョン(3.3.0/3.3.1)をテストし続けるコストを負わない方針
そのため、公式にサポートする最小バージョンを 3.3.1 に上げることで、3.3.0 特有の不具合切り捨てとテストマトリクス簡素化を図っています。
- 影響範囲・注意点
影響範囲
Bundlerによるインストール・更新時に、Ruby 3.3.0 以下の環境では Rails 7.2以降(想定)やmaster/main系のgemがインストールできなくなるか、バージョン解決が失敗します。
- 例:bash
$ ruby -v ruby 3.3.0 $ bundle add rails # => Rails の該当バージョンは required_ruby_version >= 3.3.1 のため解決失敗
- 例:
すでに Rails をインストール済みのプロジェクトで、今後 Rails のバージョンを上げる際に、Ruby も 3.3.1 以上に上げる必要が生じます。
Rails アプリ開発者側の対応
- Ruby 3.3.0 を利用している場合は、Ruby 3.3.1 以上へのアップデートが必須になります。
- rbenv/rvm/asdf 等のバージョンマネージャを使っている場合は、3.3.1をインストールしてアプリの
.ruby-versionやGemfileのruby宣言を更新します。
- rbenv/rvm/asdf 等のバージョンマネージャを使っている場合は、3.3.1をインストールしてアプリの
- CI設定でも、3.3.0 で Rails のテストを回している場合は、3.3.1 以上に切り替える必要があります。
注意点
- このPRは「Ruby 3.3.0 を完全に非推奨扱いにする」ものに近く、3.3.0 上での動作保証は行わないというRailsチームのメッセージと言えます。
- 開発環境と本番環境のRubyバージョンがズレていると、デプロイ時にRailsのバージョンアップが失敗したり、不整合を生む可能性があるため、全環境で3.3.1以上に揃えることが望まれます。
- 参考情報 (あれば)
- 問題の元PR: https://github.com/rails/rails/pull/57026
上記でRuby 3.3.0 の不具合や、それに対するRails側の対応方針が議論されていると考えられます。 - Ruby 3.3.1 リリースノート(Ruby側のバグ修正内容)
- Ruby公式ニュース: https://www.ruby-lang.org/
- そこから 3.3.1 のリリースアナウンスを参照することで、Railsに影響していたバグの詳細を確認できます。
#57022 Fix insert_all and upsert_all log messages for anonymous classes
マージ日: 2026/3/21 | 作成者: @byroot
- 概要 (1-2文で)
insert_all/upsert_all実行時のログ出力で、匿名クラス(anonymous class)の場合でも人間が読めるモデル名が出るように修正したPRです。これにより、例えばAnonymousBook Bulk Insert/AnonymousBook Bulk Upsertのような分かりやすいログメッセージになります。
- 変更内容の詳細
何が問題だったか
これまで insert_all / upsert_all のログメッセージで使われるクラス名は、通常 ModelName Bulk Insert / ModelName Bulk Upsert のようになりますが、クラスが匿名クラス(Class.new(ActiveRecord::Base) など)である場合に、ログ上の名前が読みにくい・適切でない形になっていました。
Rails では、テストや一部のメタプログラミング用途で「匿名クラスの ActiveRecord モデル」を使うことがあり、その際のログが判読しづらい状態になっていた、という背景です。
今回の修正内容
変更ファイルは以下の2つです。
activerecord/lib/active_record/insert_all.rb(+1/-1)activerecord/test/cases/insert_all_test.rb(+15/-0)
実質的には、insert_all / upsert_all の内部でログメッセージを組み立てる際に使用する「モデル名の決め方」を 1 行修正し、それに対応するテストを追加しています。
ログメッセージ生成部分
元のコードは概ね、
instrument(:insert_all, ...) do
# 例: "User Bulk Insert"
ActiveRecord::LogSubscriber.log("Bulk Insert #{self.name}")
endのように self.name(クラス名)をそのまま使っていたり、もしくは model.name ベースで文字列を組み立てていました。
今回のPRでは「匿名クラスでも、人間に読める ‘モデル名’ を返すメソッド(あるいはロジック)」を使うように1行差し替えられています。
それにより、匿名クラスにも AnonymousBook のような「匿名モデル用の名前」が割り当てられ、ログが以下のようにきれいになります。
AnonymousBook Bulk Insert (0.3ms) ...
AnonymousBook Bulk Upsert (0.4ms) ...テストの追加
activerecord/test/cases/insert_all_test.rb に、15行分のテストが追加されています。
想定されるテストイメージは次のようなものです(実際のコードは多少異なる可能性がありますが、意図は同じです):
def test_insert_all_logs_with_anonymous_class_name
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
end
assert_logged(/AnonymousBook Bulk Insert/) do
klass.insert_all([{ title: "foo" }])
end
end
def test_upsert_all_logs_with_anonymous_class_name
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
end
assert_logged(/AnonymousBook Bulk Upsert/) do
klass.upsert_all([{ id: 1, title: "bar" }])
end
endポイントは、「匿名クラスの insert_all / upsert_all 実行時に、期待どおり AnonymousBook Bulk Insert/Upsert というパターンのログが出力されること」を検証している点です。
- 影響範囲・注意点
影響範囲
- ActiveRecord の
insert_all/upsert_allを利用しているアプリケーションで、ActiveRecord::Baseを継承した匿名クラス(テスト用モデルや動的に生成したモデルクラスなど)を使っている場合に、ログに出るクラス名の表記が変わります。 - 通常の(定義済みの)モデルクラス (
User,Bookなど) を使っている場合のログは、基本的にこれまでと同じ形式のままです。
- ActiveRecord の
ログパース等への影響
- ログのテキストを正規表現などでパースしている場合(特に匿名クラスを前提にしていた場合)、クラス名部分が変わることでマッチ条件に影響する可能性があります。
- ただし、匿名クラスのログを機械的にパースしているケースは多くないと思われるため、実務上の影響は軽微と考えられます。
パフォーマンス
- 変更はログメッセージに用いるクラス名解決だけで、
insert_all/upsert_all自体の性能への影響は事実上ありません。
- 変更はログメッセージに用いるクラス名解決だけで、
- 参考情報 (あれば)
- 該当PR: https://github.com/rails/rails/pull/57022
- 元PR(rebase & cleanup 元): https://github.com/rails/rails/pull/57018
insert_all/upsert_allの公式ガイド:- Rails Guides: Active Record Insert All / Upsert All (API Docs)
ActiveRecord::Persistence#insert_allActiveRecord::Persistence#upsert_all
- Rails Guides: Active Record Insert All / Upsert All (API Docs)
#57010 Fix invalid HTML in routing error template
マージ日: 2026/3/21 | 作成者: @pardeyke
- 概要 (1-2文で)
Rails のルーティングエラー用レスキューテンプレート(routing_error.html.erb)の HTML 構造が W3C/WHATWG の仕様上不正だったため、<p>タグ内からブロック要素(<h2>/<ol>)を取り除き、正しい HTML 構造になるように修正した PR です。
これにより、ブラウザやツールによっては崩れていた DOM 構造が、仕様に沿った形で安定して解釈されるようになります。
- 変更内容の詳細
何が問題だったか
HTML 仕様上、<p> 要素の中に書けるのは「phrasing content(インライン要素など)」のみであり、見出し(<h2>)やリスト(<ol>)といったブロックレベル要素を <p> の直下に書くことはできません。
元テンプレートでは、例えば以下のような構造になっていたと考えられます(イメージ):
<p>
このページは見つかりませんでした。
<h2>Routes</h2>
<ol>
<li>...</li>
</ol>
</p>このような記述は HTML 的には不正で、ブラウザは <h2> や <ol> に到達した時点で暗黙的に <p> を閉じてしまい、結果として意図しない DOM 構造になります。
どう修正されたか
routing_error.html.erb から <p> タグの内側にあった <h2> と <ol> を取り除き、適切な場所でブロックレベル要素として独立させるように変更されています。
変更の方向性としては、次のような形になります(概念的なサンプル):
修正前(不正な HTML の例):
<p>
The page you were looking for doesn't exist.
<h2>Possible routes</h2>
<ol>
<% @routes.each do |route| %>
<li><%= route %></li>
<% end %>
</ol>
</p>修正後(仕様に沿った HTML の例):
<p>
The page you were looking for doesn't exist.
</p>
<h2>Possible routes</h2>
<ol>
<% @routes.each do |route| %>
<li><%= route %></li>
<% end %>
</ol>実際の diff は +6 / -8 行と小規模で、主にタグの配置・入れ子の整理に留まります。
文言やロジック(Ruby コード)には手を入れず、ビューのマークアップ構造だけを修正しています。
- 影響範囲・注意点
影響範囲
- 対象: Rails アプリケーションで発生する
ActionController::RoutingErrorのデフォルトエラーページ(development/test 環境やconsider_all_requests_localが true の場合などに表示されるテンプレート)。 - 変更点は HTML 構造のみであり、レスポンスのステータスコードやヘッダ、エラー内容自体には影響しません。
- 対象: Rails アプリケーションで発生する
実見た目への影響
- 多くのブラウザは元々暗黙的に
<p>を閉じてレンダリングしていたため、見た目はほとんど、あるいは全く変わらない可能性が高いです。 - ただし、HTML 検証ツールやアクセシビリティ関連ツール、DOM 解析ツール(クローラ、スクレイパー、E2E テストなど)にとっては、より予測可能で正しい DOM 構造になります。
- 多くのブラウザは元々暗黙的に
カスタムテンプレートを使っている場合の注意点
actionpack/lib/action_dispatch/middleware/templates/rescues以下のテンプレートをベースにして独自の rescue テンプレートを作っている場合、同様に<p>内にブロックレベル要素を入れていないかを確認するとよいです。- 特に、独自のエラーページを HTML5 として厳格に検証したい場合や、アクセシビリティ・SEO を意識している場合、この変更方針に倣ってマークアップを整理することをおすすめします。
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/57010
- HTML 仕様(WHATWG):
- p 要素の定義: https://html.spec.whatwg.org/#the-p-element
- 「p 要素は phrasing content しか含めてはいけない」ことが明記されており、
<h1>〜<h6>,<ol>,<ul>,<div>などのブロック要素は<p>の中に置くことができません。
この PR は、Rails のエラー画面テンプレートを HTML 仕様に準拠させるための小さなクリーンアップであり、将来的なブラウザ挙動の変化や検証の厳格化に対しても安全側に倒した変更と言えます。
#57017 LogSubscriber: Avoid repeated respond_to? calls
マージ日: 2026/3/21 | 作成者: @byroot
- 概要 (1-2文で)
ActiveSupport::LogSubscriberが、イベントハンドラ呼び出し時に毎回respond_to?を実行していたのをやめ、事前に「どのメソッドが存在するか」をキャッシュすることでオーバーヘッドを削減する変更です。結果としてログ購読処理のパフォーマンスがわずかに向上し、多数の通知が発生する環境での無駄なメソッド存在チェックが減ります。
- 変更内容の詳細
※PR本文に説明が無いため、LogSubscriber の既存実装とタイトルからの推測を含みますが、Rails の一般的なパターンに基づいた技術的に妥当な解説です。
背景
ActiveSupport::LogSubscriber は ActiveSupport::Notifications のイベントを受け取り、"process_action.action_controller" → def process_action(event)
のように、イベント名に対応するインスタンスメソッドを呼び出す仕組みになっています。
典型的には以下のようなコードが存在します(簡略化):
def call(subscriber, event)
method = event.name.split('.').first
if subscriber.respond_to?(method)
subscriber.public_send(method, event)
end
endここで、イベントが発生する度に respond_to? が呼ばれるため、高頻度イベントの場合はそこそこのオーバーヘッドになります。
今回の変更点
このPRのタイトル「Avoid repeated respond_to? calls」から読み取れるのは、
「respond_to? の結果を都度計算するのではなく、メソッド存在情報をキャッシュして再利用する」ようになった、という点です。
イメージとしては次のような変更が行われています:
# 変更前(イメージ)
def call(subscriber, event)
method = event_name_to_method(event.name)
if subscriber.respond_to?(method)
subscriber.public_send(method, event)
end
end
# 変更後(イメージ)
def call(subscriber, event)
method = event_name_to_method(event.name)
if supported_method?(subscriber, method)
subscriber.public_send(method, event)
end
end
def supported_method?(subscriber, method)
@supported_methods ||= {}
@supported_methods[method] ||= subscriber.respond_to?(method)
endもしくは、クラス単位で一度だけ「ハンドラーメソッドの一覧」を構築するような形になっている可能性もあります。
いずれのパターンでも、同じイベント種別に対しての respond_to? 実行回数を削減することが目的です。
変更差分が +6/-2 行と小さいため、
call内部でrespond_to?を直接呼んでいたのを、- 「メソッド一覧を事前に計算 (またはメモ化) するヘルパー」を介して判定するようにした
程度のスコープであると考えられます。
- 影響範囲・注意点
影響範囲
- 対象:
ActiveSupport::LogSubscriberを継承した全てのログサブスクライバ- 例:
ActionController::LogSubscriber,ActiveRecord::LogSubscriberなど
- 例:
- 動作仕様:
- イベント名に対応するメソッドが存在すれば呼び出す、という仕様自体は変わらない想定です。
- 変わるのは「メソッド存在チェックのやり方」であり、外から見える挙動は基本的に同一です。
注意点
キャッシュ・メモ化を導入すると、次のようなケースに影響しうる点に注意が必要です:
動的にメソッドを追加する場合
define_method,class_eval,method_missing+respond_to_missing?などを使って、 実行時にイベントハンドラメソッドを追加・変更しているようなかなりトリッキーな実装では、 「一度計算したrespond_to?結果がキャッシュされる」ことで、
追加後のメソッドが認識されない・あるいは逆に存在しないと判断される、というリスクが理論上あります。- ただし
LogSubscriberをそういった動的メタプロで使っているケースは一般的には稀です。
method_missingベースの実装method_missingだけでイベントを処理し、respond_to_missing?をうまく実装している場合、respond_to?の結果は正しく返るはずなので、挙動は変わらないのが通常です。- しかし、キャッシュが入ることで「一度
falseと判断されたメソッドが、その後trueになっても反映されない」
といったケースが発生しうるため、もしそのような高度に動的なLogSubscriber実装がある場合は要確認です。
スレッドセーフティ
- メソッド存在情報のキャッシュに共有状態 (
@supported_methodsなど) を使う場合、 スレッドセーフに扱われる実装であるかどうかが重要です。 - Rails本体のコーディング規約や既存のメモ化パターンに従っていれば、
実害のあるレベルの競合はほぼ起こらないように設計されているはずです。
- メソッド存在情報のキャッシュに共有状態 (
通常の、クラス定義時にハンドラーメソッドを定義して以降は変化させない使い方をしている大多数のアプリケーションにおいては、
挙動が変わる心配はほぼなく、純粋なパフォーマンス改善とみなしてよい変更です。
- 参考情報 (あれば)
ActiveSupport::LogSubscriberの仕組み (Railsガイド / ソース)- 関連する概念
ActiveSupport::Notifications: https://guides.rubyonrails.org/active_support_instrumentation.htmlrespond_to?とrespond_to_missing?:
このPRは、特にログが大量に流れる本番環境や、トレース・計測を多用するシステムで、
わずかながら CPU オーバーヘッドを減らすためのマイクロオプティマイズと言えます。
#57020 Fix lazy ivars causing different model shapes
マージ日: 2026/3/21 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
ActiveRecord::Base のサブクラスで、インスタンス変数が「遅延で」定義されることによってクラスごとに異なるオブジェクトシェイプ(shape)が発生していた問題を解消し、すべてのモデルクラスで同一の shape になるようにした PR です。
これにより、Lobsters ベンチマークにおいて ActiveRecord モデルの shape が 3 種類から 1 種類に減り、Ruby VM の最適化(JIT など)に有利になります。
- 変更内容の詳細
背景: なぜ shape が増えていたか
Ruby 3 以降(特に YJIT など)では、オブジェクトが持つインスタンス変数の構造(順序・有無)によって “shape” が決まり、それが JIT の最適化などに影響します。
この PR の説明から読み取れる問題点は以下の通りです。
@pending_attribute_modificationsなどのインスタンス変数が「必要になったタイミングで初期化される」実装だった- 一部のモデルクラスでは、クラス定義中の
attribute呼び出しなどにより「早い段階で」その ivar がセットされる - 他のモデルでは、
attribute_typesが初めて呼ばれるまで ivar が作られない typed_storeを使うモデル(@_store_accessors_module,@local_stored_attributes)も同様に、特定のモデルだけ別の ivar セットを持つ
結果として、同じ ActiveRecord::Base のサブクラスでも、どのタイミングで / どの ivar が初期化されるかにより、Ruby VM 上で複数の shape が発生していた、という状況です。
PR の Before/After の表では、Lobsters アプリのベンチにおいて:
Before:
- shape 2169: 21 モデル(
Category, Comment, ... Vote)- 理由:
@pending_attribute_modificationsがattribute_types呼び出しまで作られない
- 理由:
- shape 2175: 2 モデル(
Message, ReplyingComment)- 理由: クラスボディ内の
attribute呼び出しにより@pending_attribute_modificationsが早期にセット
- 理由: クラスボディ内の
- shape 2172: 1 モデル(
User)- 理由:
@pending_attribute_modificationsに加えtyped_store由来の@_store_accessors_module,@local_stored_attributesを持つため
- 理由:
- shape 2169: 21 モデル(
After:
- shape 1831: 24 モデルすべて
- 理由: すべてのインスタンス変数が
inheritedなどのタイミングで統一的にセットされる
- 理由: すべてのインスタンス変数が
- shape 1831: 24 モデルすべて
実際の変更点(ファイル単位)
1) activemodel/lib/active_model/attribute_registration.rb
ActiveModel 側で、属性登録まわりのインスタンス変数が「必ずサブクラス生成時に揃う」ように調整されています。
おそらく以下のようなパターンの変更が入っています(擬似コード):
def inherited(subclass)
super
# 以前は必要になったときに初期化していた ivar を
# サブクラス生成時に必ず用意するようにする
subclass.instance_variable_set(:@pending_attribute_modifications, {})
endポイント:
inherited(base)の引数名は Rails 内の他のinherited実装に合わせている(PR 説明で言及されている「パラメータ名が違うのは変だけど、他と揃えた」という話)- これにより、
attribute_typesなどが呼ばれる前でも、全サブクラスに同じ ivar セットが存在する
2) activerecord/lib/active_record/store.rb
typed_store / store 機能に関わるクラスインスタンス変数:
@_store_accessors_module@local_stored_attributes
などを、これもまた継承タイミングで必ず初期化するように変更しています。
イメージとしては:
def inherited(subclass)
super
# store 用のモジュールや属性ハッシュを必ず用意する
subclass.instance_variable_set(:@_store_accessors_module, Module.new)
subclass.instance_variable_set(:@local_stored_attributes, {})
endこれにより、typed_store を使う/使わないによって shape が変わるのではなく、「どのモデルでもひとまず同じ ivar 構造を持つ」という状態になります。
(実データが空のハッシュやモジュールであっても、shape 的には「同じインスタンス変数が定義されている」ことが重要。)
- 影響範囲・注意点
パフォーマンス面
- VM レベルでモデルクラスの shape が 1 つに統一されるため、YJIT などの最適化が効きやすくなり、Rails アプリの実行性能改善が見込まれます(特にモデルクラスのメソッド呼び出しが多いケース)。
- Lobsters ベンチで具体的に shape 数が 3 → 1 になったことが計測されているため、少なくとも一例では改善が確認されています。
互換性 / バグリスク
- 挙動としては、「これまで遅延でセットされていたインスタンス変数が、クラス継承時にあらかじめセットされる」ようになっただけで、表面的な API は変わっていません。
- ただし、以下のような場合は影響の可能性があります:
- クラスインスタンス変数の「未定義(
instance_variable_defined?が false)」と「空の状態({}など)」を区別していたようなメタプログラミング defined?(@pending_attribute_modifications)のようなチェックに依存していたコード
- クラスインスタンス変数の「未定義(
- Rails の内部 API に近い部分なので、通常のアプリケーションコードがここに依存しているケースは多くないと思われますが、エッジなメタプログラミングをしている場合は注意が必要です。
バックポートについて
- PR 内で作成者が
@byrootに対して「8.1 にはバックポート可能だと思うが、それより前は難しそう」とコメントしています。 - これは内部実装の差異や、8.1 以前との互換性リスクを考慮しての判断と考えられます。
→ Rails 8.1 以降に入る変更として見ておくのが安全そうです。
- 参考情報 (あれば)
- 関連コンセプト:
- Ruby の “object shape” / “hidden class” / “map” などの用語(特に YJIT や MJIT の文脈)
- コードリーディングの際のヒント:
ActiveModel::AttributeRegistrationのinherited実装ActiveRecord::Store(store/store_accessor/typed_store)の継承処理
- ベンチマーク:
- ruby-bench の Lobsters ベンチ マーカー
(ActiveRecord モデルの利用頻度が高いため shape の違いの影響が出やすい)
- ruby-bench の Lobsters ベンチ マーカー
#57008 Remove unused requires from ActiveSupport::JSON::Decoding
マージ日: 2026/3/19 | 作成者: @Saidbek
- 概要 (1-2文で)
ActiveSupport::JSON::Decoding 内で、現在は一切使われていないrequireを2つ削除したPRです。機能追加や挙動変更は行われておらず、不要な依存を整理するリファクタリングです。
- 変更内容の詳細
対象ファイル:activesupport/lib/active_support/json/decoding.rb
削除されたのは、以下のような「require系」の2行です(実際の行はPR本文からの推測を含みますが、意味としてはこの2種類):
active_support/core_ext/module/attribute_accessorsに関する require- delegation(
ActiveSupport::Delegationやdelegate関連)に関する require
PR説明によると:
attribute_accessorsの require- もともとは
mattr_accessor :parse_json_timesを使うために必要だったが、コミットc562b54でmattr_accessorがrubyからmattr_accessor :parse_json_timesrubyのような「通常のクラス/モジュールの attr_accessor」を使う形に置き換えられたsingleton_class.attr_accessor :parse_json_times - これにより、
mattr_accessorを提供するactive_support/core_ext/module/attribute_accessorsは不要になっていた
- もともとは
delegation の require
- 2012年に「プラガブルな JSON backend(自前のバックエンドを差し替え可能にする仕組み)」が削除されたタイミングで、
delegateを使った委譲も不要になっていた - しかし削除し忘れていた require が残り続けていた
- 現在の
decoding.rb内では delegation 機能は参照されていない
- 2012年に「プラガブルな JSON backend(自前のバックエンドを差し替え可能にする仕組み)」が削除されたタイミングで、
このPRでやっていることは:
- ファイル先頭付近などに書かれていた上記2つの
require行を削除する - それ以外のコード(JSONのデコード処理のロジック、API、挙動)は一切変更しない
実質的には「使われていない依存モジュールを削除しただけ」のクリーンアップです。
- 影響範囲・注意点
Rails本体の挙動
- ActiveSupport::JSON::Decoding の公開API・内部ロジック・デコード結果には一切変更なし
- JSONのパース挙動(
parse_json_timesの意味や挙動含む)も変わらない
依存関係・ロード順
- 対象
requireは、すでにどこからも参照されていなかったため、削除しても NameError などは発生しない - もしアプリケーション側が「ActiveSupport::JSON::Decoding を require すると副作用で delegation や module attribute accessors が読み込まれる」ことに“たまたま”依存していた場合は影響がありますが、そのような依存は基本的に非推奨かつ想定外です
- 例:ruby
# こういう依存は元々好ましくない(今回のPRで動かなくなる可能性がある) require "active_support/json/decoding" # delegation を勝手に使える前提でいたコード class Foo delegate :bar, to: :baz end - delegation や
mattr_accessor系が必要であれば、各自で明示的に require すべきです:rubyrequire "active_support/core_ext/module/attribute_accessors" require "active_support/dependencies" # or appropriate delegation require
- 例:
- 対象
テスト・ドキュメント
- 機能変更・挙動変更がないため、テストの追加や CHANGELOG 更新は行われていません(PRの種別的にも妥当)
- 参考情報 (あれば)
このPRが前提としている過去の変更:
mattr_accessor :parse_json_timesをsingleton_class.attr_accessorに置き換えたコミット:c562b54- プラガブル JSON backend 削除(2012年ごろ)の変更: JSON backend 差し替え機構や delegation ベースの実装が不要になった
実務的な示唆:
- ライブラリやgemでは、「副作用として何かを require することに暗黙に依存しない」設計をすべきである、という典型例
- 使われなくなった require を定期的に掃除することで:
- 不要なロード時間の削減
- 依存関係グラフの単純化
- 将来的なリファクタリングの容易化
といったメリットがあります。
#57000 Batch SQL statements when creating tables.
マージ日: 2026/3/19 | 作成者: @andrewn617
- 概要 (1-2文で)
このPRは、create_table時に発行する複数の SQL を一括送信(バッチ)することで、特に巨大なスキーマを持つアプリケーションでdb:schema:loadなどのパフォーマンスを大幅に改善する変更です。PostgreSQL で 1000+ テーブルのケースでは、スキーマロードが約 120 秒から約 25 秒まで短縮された事例が示されています。
- 変更内容の詳細
全体方針
- これまで
create_tableの中で、カラム追加・インデックス作成・コメント付与などを個別にexecuteしていた処理を、- 一旦 Ruby 側で SQL 文字列として配列に貯める
- 最後に
execute_batchでまとめて送る
という方式に変更しています。
drop_tableやコメント付与など、これまで「その場で SQL 実行」していた部分についても、「SQL を生成して返すだけのヘルパーメソッド」を新設し、バッチ用の配列に積めるようにしています。
主な変更点
1) create_table の SQL バッチ化
ActiveRecord::ConnectionAdapters::SchemaStatements#create_table 付近が主な変更箇所です。
イメージとしては以下のような流れに変わります(擬似コード):
def create_table(table_name, **options)
sqls = []
# 1. CREATE TABLE 本体
sqls << create_table_definition_sql(table_name, **options)
# 2. カラムコメントやテーブルコメントを付ける SQL を追加
sqls.concat(comment_sqls_for_table(table_name, **options))
# 3. インデックスや制約などの追加 SQL を配列に追加
sqls.concat(index_sqls_for_table(table_name, **options))
# 4. 最後にまとめて実行
execute_batch(sqls)
end実際にはアダプタごとに実装が異なりますが、概念的には上記のように「SQL をためてから、最後に execute_batch」という構成になっています。
2) drop_table やコメント関連のヘルパー追加
- これまで
drop_tableなどで直接execute("DROP TABLE ...")のように書かれていた箇所を、- 「DROP TABLE 用の SQL を生成して返すメソッド」
- 「コメント作成用の SQL を生成して返すメソッド」 に分離し、
create_tableからはそれらを呼び出して SQL 文字列だけ取得し、バッチに積むようにしています。
例(イメージ):
def drop_table_sql(table_name, **options)
# "DROP TABLE ..." の SQL 文字列だけを返す
end
def comment_sql_for_column(table_name, column_name, comment)
# "COMMENT ON COLUMN ..." の SQL 文字列だけを返す
end3) PostgreSQL / MySQL アダプタの対応
abstract_mysql_adapter.rb- MySQL 系アダプタにも
execute_batch対応や、バッチ実行を前提としたcreate_table内部処理の整理が入っています。 - MySQL 側が複数文を一度に投げるときの挙動が異なるため、それを考慮した実装になっている可能性があります(テストも追加)。
- MySQL 系アダプタにも
postgresql/schema_statements.rb- PostgreSQL アダプタ側で、テーブル作成時の付随処理(コメント、インデックスなど)を SQL 文字列として返すヘルパーを追加し、
execute_batchに渡せるようにしています。
- PostgreSQL アダプタ側で、テーブル作成時の付随処理(コメント、インデックスなど)を SQL 文字列として返すヘルパーを追加し、
4) テストの追加・更新
activerecord/test/cases/adapters/abstract_mysql_adapter/active_schema_test.rbactiverecord/test/cases/migration/change_schema_test.rb
上記テストで、以下のような観点を検証していると考えられます。
- バッチ実行でもこれまで通りテーブルが正しく作成されるか
- コメントやインデックスなど、付随するメタデータも期待通りに適用されるか
- MySQL / PostgreSQL など主要なアダプタで、バッチ処理が問題なく動作するか
5) CHANGELOG の更新
activerecord/CHANGELOG.mdにこの動作変更(パフォーマンス改善)が記載されており、アプリケーション開発者に向けて「テーブル作成時の SQL 発行がバッチ化された」ことが明示されています。
- 影響範囲・注意点
影響する場面
- 主に以下の操作に影響があります(内部的な SQL の出し方が変わる):
rails db:schema:load- マイグレーションでの
create_table/drop_table - スキーマ定義でコメントやインデックスなど、テーブル作成に付随したオプションを使っている場合
アプリケーションコードから見える API (create_table の引数や DSL) は変わらないため、通常利用ではコード修正は不要です。
パフォーマンス面
- 大量のテーブルや複雑なスキーマを持つアプリで、特にネットワークレイテンシがある環境(DB が別ホストなど)では、
「DB とのラウンドトリップ回数削減」による高速化が期待できます。 - PR では PostgreSQL 1000+ テーブルの環境で 120s → 25s まで短縮された、という具体的な数値が報告されています。
互換性・リスク
SQL を一括送信するようになったことで、以下のような点に注意が必要になる可能性があります:
- DB ドライバや設定による制約
- サーバ側で「一度に複数ステートメントを許可していない」設定がある場合、問題になる可能性があります(特に MySQL 系)。
- ただし、Rails の公式アダプタで対応した形になっているため、通常の設定であれば動くようにテスト済みと思われます。
- エラー時の扱い
- バッチ内のどの SQL で失敗したかの特定が、ログ上わずかに分かりにくくなる可能性があります(1 回の
execute_batch内に複数文が含まれるため)。 - とはいえトランザクション内で実行されていれば、失敗時のロールバック挙動自体はこれまで通りです。
- バッチ内のどの SQL で失敗したかの特定が、ログ上わずかに分かりにくくなる可能性があります(1 回の
- DB ドライバや設定による制約
gem やプラグインなどで、アダプタの
create_table/drop_tableをモンキーパッチしている場合、- 既存実装の前提(「その場で
executeされる」など)が崩れることで、不整合が起きる可能性があります。 - アダプタの内部 API(SQL を返すヘルパーの追加など)に依存するコードがあれば、追従が必要になるかもしれません。
- 既存実装の前提(「その場で
- 参考情報 (あれば)
PR で言及されている「autoresearch pattern」:
- https://github.com/karpathy/autoresearch
- 該当の実装例(pi-autoresearch): https://github.com/davebcn87/pi-autoresearch
これらを使って「DB スキーマロードのボトルネック分析・最適化案の探索」を自動化し、その中から「create_table 時の SQL バッチ化」という現実的な最適化を抽出して手書きで実装した、という背景が説明されています。
結論として:
- 外部 API の互換性を保ったまま、ActiveRecord のスキーマ・マイグレーションの実行性能を改善する、比較的安全な内部構造のリファインと言えます。
- 大規模スキーマを持つアプリでは、Rails をこのバージョンに上げるだけでスキーマロード時間が短縮される可能性があります。
#56998 Use flat_map in ContentSecurityPolicy
マージ日: 2026/3/18 | 作成者: @p8
- 概要 (1-2文で)
Rails のContentSecurityPolicy実装において、validate内でflat_mapを使うことで配列のフラット化処理を整理し、build_directiveの戻り値をvalidateの戻り値に依存しないようにする小さなリファクタリングが行われました。挙動は変えずに、コードの明確性と保守性を高める変更です。
- 変更内容の詳細
※ 実際の差分は概ね以下のようなイメージです(擬似コードレベル):
def validate(directive, sources)
# 変更前(イメージ)
resolved_sources = sources.map { |source| build_directive(directive, source) }.flatten
# 変更後
resolved_sources = sources.flat_map { |source| build_directive(directive, source) }
# validate の戻り値に依存せずに build_directive が意味のある値を返すようにする
resolved_sources
end
def build_directive(directive, source)
# 変更前(イメージ)
# result は配列や単一値を含むことがあり、そのまま flatten されていた
result = some_resolution_logic(directive, source)
Array(result).flatten
# 変更後
resolved_sources = some_resolution_logic(directive, source)
# ここで flatten しない
resolved_sources
end実際に行われていることは次の2点です。
validateでmap + flatten→flat_mapに置き換え- 従来:ruby
resolved_sources = sources.map { ... }.flatten - 変更後:ruby
resolved_sources = sources.flat_map { ... }
Array#flat_mapはmapでブロック実行 → 結果を 1 レベル flatten までを一度に行うメソッドなので、意図が明確になり、不要なflatten呼び出しも削除できます。- 従来:
build_directiveの戻り値をvalidateに依存しない形に明示- これまで
build_directiveは内部でflattenされることを前提とした戻り値設計だったため、validate側の実装(flattenするかどうか)に依存していました。 - 今回の変更では
build_directive内で「解決済みソース (resolved_sources) を明示的に返す」ようにしており、build_directiveは「1 つのディレクティブと 1 つのソース」から「解決済みソース群」を返すvalidateは「全ソースに対してflat_mapで解決済みソースを 1 次元配列にまとめる」
という責務分離がはっきりしました。
- これまで
この結果、validate と build_directive の役割がより明確になり、「どのメソッドが flatten を担当するか」がコード上で一目で分かるようになっています。
- 影響範囲・注意点
- 外部 API としての
ContentSecurityPolicyの挙動(生成される CSP ヘッダの内容)は、意図としては変わりません。 - 影響が出る可能性があるのは、以下のような「内部実装に依存した使い方」をしている場合です。
ContentSecurityPolicyをmonkey patchしていて、build_directiveが内部で flatten している前提でコードを書いている場合build_directiveの戻り値の型やネスト構造に依存した独自拡張をしている場合
- Rails 内部の通常利用(
config.content_security_policyなどを通じた標準的な使い方)では、この変更による破壊的影響は想定されません。 - flatten の責務が
validate側に一本化されたことで、今後build_directiveの実装を変更・拡張しやすくなります(テストも書きやすくなり、バグ混入のリスクが減る)。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/56998
- 対象ファイル:
actionpack/lib/action_dispatch/http/content_security_policy.rb - 変更は 1 ファイル・+4/-2 行のみで、純粋なリファクタリング (内部実装改善) に分類できる内容です。
#57003 Fix PostgreSQL column equality for generated types
マージ日: 2026/3/18 | 作成者: @Saidbek
- 概要 (1-2文で)
PostgreSQL の generated column(生成列)の「格納型(stored/virtual)」が異なる場合に、PostgreSQL::Columnの==とhashの挙動が食い違っていたバグを修正する PR です。==とhashが同じ情報(generated属性)に基づいて判定されるようにし、Hash/Set での不整合を解消しています。
- 変更内容の詳細
何が問題だったか
ActiveRecord::ConnectionAdapters::PostgreSQL::Column には == と hash が定義されていますが、PostgreSQL 18+ で導入された generated column の 2 種類のタイプ:
- stored (
"s") - virtual (
"v")
に対して、以下のような不整合がありました。
#==virtual?という boolean(「仮想かどうか」だけ)で比較していた
→ 実質、generatedの"s"/"v"の違いを潰した形で比較#hash
内部 ivar の@generated("s"or"v")をそのままハッシュ計算に使用していた
結果として:
- カラムの他の属性がすべて同じで、
generated: "s"とgenerated: "v"だけが違う 2 つのPostgreSQL::Columnオブジェクトが:col1 == col2はtrue(virtual?ベースで比較)col1.hash != col2.hash(@generatedベース)
という状態になり、eql?/hash 契約(等価なオブジェクトは同じハッシュ値を持つべき)に違反していました。
この問題は Hash / Set をキーにしたキャッシュやコレクションで、同じと見なされるはずのカラムが重複したり、逆に取り出せなかったりといった「サイレントな不具合」を引き起こします。
同種の問題は SQLite3 用の Column でも発生しており、別 PR (#56817) ですでに修正済みで、今回は PostgreSQL 版の追従です。
具体的な修正内容
activerecord/lib/active_record/connection_adapters/postgresql/column.rb にて:
- これまで
#==内で行っていた比較:
virtual? == other.virtual?を削除し、代わりに generated 属性そのものを比較するよう変更:
generated == other.generated- そのために
generatedを参照可能にするprotected attr_reader :generatedを追加
イメージとしては以下のような変更です(擬似コード):
class PostgreSQL::Column
# 以前
def ==(other)
# ...
virtual? == other.virtual? &&
# ...
end
def hash
[ # ...
@generated,
# ...
].hash
end
# virtual? は @generated が "v" かどうかを boolean で返すメソッド
endが、次のように揃えられます:
class PostgreSQL::Column
protected attr_reader :generated
def ==(other)
# ...
generated == other.generated &&
# ...
end
def hash
[ # ...
@generated, # == で比較する generated と同じ抽象レベル
# ...
].hash
end
endつまり、== も hash も「generated という同じ次元の情報」を見るようになり、stored/virtual の違いが適切に反映されるようになりました。
テスト
activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb にテストが追加されており、少なくとも以下を検証していると考えられます(要約):
- generated type が異なる 2 つのカラムが
==で異なると判定されること - あるいは(実装次第で)
==/hashが一貫していることを検証する形で、contract の維持を確認
コード上、変更行数・ファイル数は少ないですが、挙動としては重要な修正です。
- 影響範囲・注意点
- 対象: PostgreSQL 18 以降で、generated column(stored/virtual)を利用しているアプリケーション
- 影響箇所:
ActiveRecord::ConnectionAdapters::PostgreSQL::Columnをキーとして Hash / Set を作っているような場合- Active Record 内部のスキーマキャッシュ・カラムキャッシュなど、Column オブジェクトの
==/hashに依存するロジック全般
この修正により:
- 以前: stored/virtual の違いを無視して「同じカラム」とみなされてしまうケースがあった
- 今後: stored と virtual は「異なる generated type を持つ別のカラム」として区別される
したがって、「stored/virtual の違いを無視して同一扱いしていた」ことに依存した既存挙動があれば変わる可能性がありますが、通常はむしろ正しい区別ができるようになるので望ましい変更です。
Hash/Set を使った独自のメタデータ管理・キャッシュ等をしている場合、Rails 更新後に:
- カラムキーの重複・欠落がなくなっているか
- 以前不自然だった動作(列キャッシュの取り違え等)が修正されているか
を確認するとよいです。
- 参考情報 (あれば)
- 対応する SQLite3 側の修正:
- PR #56817「Fix SQLite3 column equality for generated types」
- バグの導入箇所:
- コミット
bdc7adce(#hashを raw ivar ベースにリファクタリングした際、#==が追従していなかった)
- コミット
- 関連クラス:
ActiveRecord::ConnectionAdapters::PostgreSQL::Columnvirtual?,generated(PostgreSQL の generated column の種別を表す属性)
#57001 Fix PostgresqlDbConsoleTest reports Environment leak detected
マージ日: 2026/3/17 | 作成者: @joaoGabriel55
- 概要 (1-2文で)
PostgresqlDbConsoleTest で環境変数(ENV)のリークを検出するLeakCheckerとのライフサイクルの噛み合わせ不良によりテストが失敗していた問題を、ENV のバックアップ/復元方法を変更することで修正した PRです。テストコードのみの変更で、本体の挙動には影響しません。
- 変更内容の詳細
問題の背景
Rails のテスト基盤には
LeakCheckerモジュールがActiveSupport::TestCaseにprependされており、テスト間での環境変数の「リーク」(追加・変更されたまま次のテストに持ち越されること)を検出します。LeakCheckerはテストライフサイクルの以下のタイミングで ENV をチェックします:before_setup: 現在の ENV をスナップショットafter_teardown: スナップショットと比較して差分があれば「Environment leak detected」として失敗させる
PostgresqlDbConsoleTestでは、PostgreSQL 接続用の ENV(PGUSER,PGHOST,PGPORTなど)をテスト内で書き換えるため、もともと「テストごとに元に戻す」ための仕組みを持っていました。その仕組みは「
#runをオーバーライドして、superを ENV の保存・復元でラップする」という実装でした。
疑似コード(問題のあったイメージ):
def run(*args, &block)
preserve_pg_env do
super # ← この中で LeakChecker の before_setup / after_teardown が動く
end
end
def preserve_pg_env
backup_env
yield
ensure
restore_env # ← after_teardown の後で実行されてしまう
endこの構造だと実際の実行順序は:
LeakChecker#before_setupで ENV をスナップショット- テスト本体で
PGUSER,PGHOST,PGPORTを書き換え LeakChecker#after_teardownが「ENV が変わっている」と検出して失敗- その後で
ensureによって ENV が復元される(遅すぎる)
その結果、以下の 4 テストが LeakChecker によって落ちていました:
test_postgresql_fulltest_postgresql_with_ssltest_postgresql_include_passwordtest_postgresql_include_variables
エラーメッセージ例:
textEnvironment leak detected: - Added variables: - PGPORT - Changed variables: - PGUSER from "postgres" to "user" - PGHOST from "postgres" to "host"
対応方針
#run のオーバーライドをやめ、Minitest 標準の setup / teardown を使って ENV をバックアップ・復元するよう変更しました。
重要なポイントは、「LeakChecker の after_teardown が走る前に ENV を元に戻す」ことです。
Minitest ライフサイクル(LeakChecker を含む):
run
before_setup (LeakChecker が ENV をスナップショット)
setup ← ここで PG* の ENV をバックアップ
test body ← PG* を変更
teardown ← ここで PG* の ENV を復元
after_teardown (LeakChecker が ENV を比較)これにより、LeakChecker が after_teardown で ENV を比較する時点では PostgresqlDbConsoleTest による ENV 変更はすでに元に戻っており、リークとして検出されなくなります。
実際の変更点
- 変更ファイル:
activerecord/test/cases/adapters/postgresql/dbconsole_test.rb - 主な変更内容:
#runのオーバーライドを削除preserve_pg_envというプライベートメソッドを削除- 代わりに
setup/teardownでPG*ENV をバックアップ・復元するコードを追加
イメージ(擬似コードレベル):
class PostgresqlDbConsoleTest < ActiveRecord::TestCase
def setup
super
@original_pg_env = {
"PGUSER" => ENV["PGUSER"],
"PGHOST" => ENV["PGHOST"],
"PGPORT" => ENV["PGPORT"],
# 必要なキーをここで保存
}
end
def teardown
@original_pg_env.each do |key, value|
if value.nil?
ENV.delete(key)
else
ENV[key] = value
end
end
super
end
end※上は説明用の擬似コードであり、実際のコードと細部は異なる可能性がありますが、概ねこの方針の変更です。
- 影響範囲・注意点
- 影響範囲:
- 変更はテストコードのみであり、Rails / ActiveRecord の本番コードには影響しません。
PostgresqlDbConsoleTest内で PostgreSQL 接続用 ENV をいじるテストが、LeakChecker によって「環境変数リーク」と誤検出されなくなります。
- 注意点:
- 今後、他のテストでも ENV を変更する場合は、
#runのオーバーライドではなく、setup/teardownなど Minitest のライフサイクルに沿ってバックアップ・復元を行う必要があります。 LeakCheckerを前提とすると、「after_teardownよりも前に環境差分が元に戻っている」ことが重要です。独自フックでテスト実行をラップする場合は、この順序関係に注意する必要があります。- もし他のテストクラスでも同様に
#runをオーバーライドして ENV を弄っている場合は、同種のリーク検出問題が起きうるため、同パターンに書き換えるのが望ましいです。
- 今後、他のテストでも ENV を変更する場合は、
- 参考情報 (あれば)
- 修正対象 Issue:
Fixes: #56812 LeakCheckerの実装:tools/support/leak_checker.rbに定義tools/test_common.rbでActiveSupport::TestCaseにprepend
- Minitest ライフサイクルと Rails のテスト拡張:
ActiveSupport::TestCaseは Minitest::Test をベースにしており、before_setup/after_teardownなどを通じてこうしたフックを差し込んでいます。
#56994 Drop workaround for mail gem version < 2.8
マージ日: 2026/3/16 | 作成者: @yahonda
- 概要 (1-2文で)
Action Mailbox が依存する mail gem の下限バージョンがすでに 2.8 以上になっているため、mail < 2.8 向けに入っていたワークアラウンド(互換コード)を削除した PR です。結果として、コードがシンプルになり、サポート対象が実質的に mail 2.8 以降に揃えられました。
- 変更内容の詳細
- 対象ファイル:
actionmailbox/lib/action_mailbox/mail_ext/addresses.rb - 行数ベースでは「+5 / -16」となっており、条件分岐や旧バージョン用の処理が削られています。
元々このファイルには、mail gem の古いバージョン (< 2.8) でアドレスパース周りに問題があったため、それを吸収するためのパッチ的な拡張(ワークアラウンド)が入っていました(#46643 で追加)。
しかし #50668 で「Action Mailbox は mail gem >= 2.8 を必須」としたため、mail 2.7 以前を考慮したコードは不要になり、今回それを削除しています。
イメージとしては、次のようなコードがなくなった形です(あくまでイメージであり実際のコード断片ではありません):
# 擬似コード: 古いバージョンを考慮した条件分岐
if Gem::Version.new(Mail::VERSION) < Gem::Version.new("2.8.0")
# mail 2.8 未満用の独自パッチ・上書き
class Mail::Address
# バグを回避するためのメソッド上書き
end
else
# 2.8 以上では標準実装そのまま利用
endこのような「バージョン判定+古いバージョン向けの monkey patch / 拡張」の部分が削られ、2.8 以上前提のコードだけが残る形になっています。
- 影響範囲・注意点
影響範囲
- Action Mailbox が行っている「メールアドレスのパース・処理」に関する内部実装がシンプルになります。
- mail gem 2.8 以上の振る舞いを前提としているため、それ以前の挙動を想定した特別な処理はもう行われません。
注意点
- 実務的には、Rails / Action Mailbox を使うプロジェクトで mail gem を 2.8 未満に固定していると、依存関係の解決でそもそも弾かれる(または動かない)前提になっています。
- もしプロジェクト側が
mail (~> 2.7)のように古いバージョンを強制している場合は、Rails のバージョンアップ時に Gemfile の制約も見直す必要があります。
- もしプロジェクト側が
- mail 2.8 未満でのみ再現していたバグを避けるためのワークアラウンドが削除されるため、「mail を 2.8 以上に上げること」自体がバグ回避条件になります(= サポート外のバージョンにおける動作は考慮されない)。
- 実務的には、Rails / Action Mailbox を使うプロジェクトで mail gem を 2.8 未満に固定していると、依存関係の解決でそもそも弾かれる(または動かない)前提になっています。
Rails アプリ開発者の視点では、「サポートされていない mail gem を無理に使わない限り、挙動が変わることはほぼない」と考えて差し支えありません。
- 参考情報 (あれば)
- この PR:
Drop workaround for mail gem version < 2.8(#56994) - ワークアラウンドを最初に導入した PR: #46643
- Action Mailbox が
mail >= 2.8を要求するようになった変更: #50668
#56980 Bump bundler from 4.0.6 to 4.0.8
マージ日: 2026/3/16 | 作成者: @larouxn
- 概要 (1-2文で)
Rails リポジトリで使用している Bundler のバージョンを 4.0.6 から 4.0.8 に更新した PRです。これにより、bundle installなどで利用される Bundler が最新のパッチバージョンになります。
- 変更内容の詳細
- 変更ファイルは
Gemfile.lockのみで、Bundler のバージョン表記を 4.0.6 → 4.0.8 に更新しています。 - 実質的には、Rails の開発・動作環境で前提とされる Bundler のバージョンを最新の 4.0.8 に揃える変更です。
- PR本文で 4.0.7 / 4.0.8 のリリースノートへのリンクが貼られているため、4.0.6 → 4.0.8 の間で入った修正・改善を取り込む意図があります(機能追加というよりパッチ・バグフィックスが主な想定)。
Gemfile.lock 中の変更イメージ(概念的な例):
- BUNDLED WITH
- 4.0.6
+ BUNDLED WITH
+ 4.0.8コードロジックやライブラリ依存関係そのものには手を加えておらず、あくまで Bundler のバージョン指定のみの変更です。
- 影響範囲・注意点
影響範囲:
- Rails の開発者・CI環境などで
bundle installやbundle execを実行する際に、Bundler 4.0.8 を前提とした挙動になります。 - Bundler 本体のバグ修正や互換性改善が取り込まれるため、依存関係の解決・インストール時の挙動が一部変わる可能性がありますが、同一メジャー・マイナー内のパッチアップデートのため、後方互換性が大きく崩れるリスクは低いと考えられます。
- Rails の開発者・CI環境などで
注意点:
- ローカル環境にインストールされている Bundler バージョンが 4.0.8 より古い場合、
bundle install実行時にバージョン差異による警告が出たり、うまく動作しない可能性があります。その場合はgem install bundler -v 4.0.8などで更新する必要があります。 - CI や開発用 Docker イメージで Bundler のバージョンを固定している場合は、それらの環境設定も 4.0.8 に合わせて更新する必要があります。
- PR 作成者によるローカルテストは済んでいるものの、プロジェクト固有の環境差異で何かしら Bundler 依存の不具合が出る可能性はゼロではないため、CI の通過と実環境での確認が推奨されます。
- ローカル環境にインストールされている Bundler バージョンが 4.0.8 より古い場合、
- 参考情報 (あれば)
Bundler 4.0.7 リリースノート:
https://github.com/ruby/rubygems/releases/tag/bundler-v4.0.7Bundler 4.0.8 リリースノート:
https://github.com/ruby/rubygems/releases/tag/bundler-v4.0.8
これらのリリースノートを見ることで、4.0.6 から 4.0.8 までに入ったバグ修正・改善点を確認できます。
#56926 ActionText: support block children in editor elements alongside value
マージ日: 2026/3/16 | 作成者: @jorgemanrubia
- 概要 (1-2文で)
ActionText のエディタヘルパーで、value引数とは別にブロックで渡した内容を「エディタ要素の子要素」として同時に扱えるようになりました。Trix エディタについては、従来どおり「valueが無い場合はブロック内容を初期値として使う」という挙動も維持されています。
- 変更内容の詳細
これまでの挙動
- ActionText のエディタヘルパー(例:
form.rich_text_area相当の低レベル API)でブロックを渡すと、そのブロックは「エディタの初期値」として解釈されていました。 - そのため、
value引数とブロックは排他的 (OR) で、どちらか一方しか使えませんでした。valueを指定するとvalueが優先valueが無い場合のみ、ブロックをキャプチャしてエディタ初期コンテンツに充てる
これからの新しい挙動
- ブロックは エディタ要素の DOM 子要素としてレンダリング されるように変更されました。
valueはこれまでどおり「エディタの内容(コンテンツバインディング)」に流れ込みます。- ブロックは「エディタタグの内側に配置される追加要素」として扱われます。
- つまり、
valueとブロックを同時に併用可能 になります。value… リッチテキストそのもの- ブロック … プロンプトメニュー、ツールバー拡張、設定用の要素などをエディタ要素内にインジェクトする用途
イメージとなるサンプル(擬似コード)
<%= action_text_editor_tag value: @post.body do %>
<!-- ここはエディタ要素の「子要素」としてそのまま出力される -->
<custom-toolbar slot="toolbar"></custom-toolbar>
<prompt-menu data-target="editor.prompt"></prompt-menu>
<% end %>- 上記のように、
value: @post.bodyで本文を設定しつつ、ブロック内には拡張 UI を自由に埋め込めるイメージです。 - Lexxy のような他実装のエディタは、この「ブロック = 子要素」構文を設定・拡張用に利用しつつ、リッチテキストは
value属性から受け取ることができるようになります。
Trix エディタに対する互換対応
- Trix 向けの
TrixEditor::Tag#render_inでは、「valueが存在しない場合のみ、ブロックをキャプチャして初期値にする」という従来挙動 を保持しています。valueがnilor 未指定 → ブロック内容を初期値として hidden input に設定(従来どおり)valueが指定されている →valueがエディタ内容になり、ブロックは DOM 子要素としてレンダリング
これにより、既存の Trix ベースのコードで「ブロックに初期リッチテキストを書いている」ケースは、そのまま動作し続けます。
- 影響範囲・注意点
新規実装 / 他エディタ実装(Lexxy など)
- エディタヘルパーのブロックを「DOM 子要素挿入のためのフック」として使えるようになります。
valueは常に「リッチテキスト値」、ブロックは「DOM 構造の拡張」として扱えるため、API の責務が明確になります。- エディタ側実装では、タグの子要素として渡されるノードを設定 UI やスロットコンテンツとして解釈することが可能です。
既存 Trix ベースのアプリ
valueなし + ブロックで初期コンテンツを書いている場合:- 今回の PR では Trix のみ特別に互換レイヤーが入り、従来通り「ブロックが初期値」として扱われるため、基本的に動作は変わりません。
valueとブロックを併用しているコードは、これまでは「ブロック側は初期値としてキャプチャされ、DOM 子要素としては存在しない」挙動でしたが、今後は「valueが優先され、ブロックはエディタの子要素としてレンダリング」される形になります。- そのような使い方をしているケースは少ないと思われますが、もし独自に依存している場合は挙動の変化に注意が必要です。
テスト・内部仕様
actiontext/test/unit/editor_test.rbおよびform_helper_test.rbにテストが追加・更新され、valueとブロックの独立性- Trix における後方互換 がカバーされています。
CHANGELOG.mdにもこの挙動変更が明記されています。
- 参考情報 (あれば)
- 対応する既存 PR:
- ブロック機能導入の元 PR: #55827
- この PR:
- #56926: ActionText: support block children in editor elements alongside value
- 補足的な実装意図:
- Lexxy など、Trix 以外のエディタ実装が ActionText のエディタタグを使う際に、
value… リッチテキストの値- ブロック … 設定用 / 拡張 UI 用の DOM 子要素
という形で明確に使い分けられるようにするための設計変更です。
- Lexxy など、Trix 以外のエディタ実装が ActionText のエディタタグを使う際に、
#56990 fix(guides): bin/jobs start was not copying
マージ日: 2026/3/16 | 作成者: @j01nb0k
- 概要 (1-2文で)
Railsガイド「Active Job Basics」の中にあるbin/jobs startのコマンド例が、シェル上で想定どおりコピー・実行できない状態になっていた問題を修正したPRです。原因は、ガイドのコードブロックから「$プロンプト記号」が抜けていたことによる単純なドキュメント上のミスです。
- 変更内容の詳細
- 対象ファイル:
guides/source/active_job_basics.md - 変更内容は 1 行のみで、
bin/jobs startの記述に$を追加しています。
例(イメージ):
- bin/jobs start
+ $ bin/jobs startRailsガイドでは、シェルで実行するコマンド例は慣例として先頭に $ を付けて記述しています。
この $ がないと、ガイドの「右上のコピーアイコン(Copy code)」などでコマンドをコピーした際に、ほかの類似箇所と挙動が揃わない、あるいはレンダリング上の扱いが不統一になる場合があります。
PR説明文の "was not being copied" という表現から、ガイドをHTMLに変換した際に、この行だけコピー機能やスタイルの対象外になっていた(他の $ 付きコマンド例とは異なる扱いになっていた)ことが分かります。そのため、$ を付けて他のコマンド例と同じ形式に揃えた形です。
- 影響範囲・注意点
- 影響範囲:
- コード・ライブラリ本体には一切影響せず、Railsガイドのドキュメント表示・コピー挙動のみの変更です。
bin/jobs start自体の動作や Active Job のAPI、Railsアプリの挙動には変更はありません。
- 注意点:
- 既存アプリケーション・ジョブキュー・本番運用への影響はゼロです。
- これを機に、他のガイドにおいても
$の有無が不揃いになっていないか確認すると、ドキュメントの一貫性向上につながります。
- 参考情報 (あれば)
- 該当ガイド:
- Active Job Basics: https://guides.rubyonrails.org/active_job_basics.html
- 関連知識:
- Railsガイドでは、シェルコマンド例に
$を付けることで「ユーザーが打つ部分」と「プロンプト」を明確にしつつ、コピー時には$も含めるかどうかをコピー機能側で制御できるような前提で書かれていることが多いです。
- Railsガイドでは、シェルコマンド例に
#56987 Add tests for ActiveModel::Name#human count argument
マージ日: 2026/3/16 | 作成者: @nicolasva
- 概要 (1-2文で)
ActiveModel::Name#human に渡せるcount引数が、I18n の複数形ルールに沿って正しく扱われることを確認するテストが追加されています。機能変更ではなく、既存仕様のテスト補完が目的のPRです。
- 変更内容の詳細
- 追加ファイル:
activemodel/test/cases/translation_test.rbに17行のテストコードが追加 - テスト対象:
ActiveModel::Name#human(count: ...)
ActiveModel::Name#human は以下のように呼び出されるメソッドです:
User.model_name.human # => "User" など
User.model_name.human(count: 2) # => 複数形ルールに基づいた翻訳今回のPRでは、この count: オプションが I18n の複数形 (pluralization) に使われていることをテストで保証しています。
具体的には:
ActiveModel::Name#humanにcount: 1やcount: 2などを渡したとき、- 対応する I18n キー (例:
activemodel.models.user.one/activemodel.models.user.otherなど) が正しく選択されているか - あるいは
activerecord.models.*/activemodel.models.*の優先順位や fallback が期待どおりか
といった振る舞いを検証するテストが追加されています。
テストコード自体は、他の翻訳系テストと同様に:
- テスト用のモデルクラス(例:
Personなど)を定義 - テスト用の I18n ロケールを設定
model_name.human(count: X)を呼び出して返り値を検証
という形になっているはずです。
- 影響範囲・注意点
- ランタイムの挙動変更はなく、テストの追加のみです。
- 既に
countに対応している実装を「壊さないための安全ネット」が増えたと捉えられます。 - これにより、今後
ActiveModel::Name#human実装を変更した際に:countオプションが無視される- I18n の pluralization が崩れる
などのリグレッションが CI で検出されるようになります。
- 既存アプリで
model_name.human(count: ...)を利用している場合、その挙動が今後の変更で壊れにくくなる、という意味での間接的なメリットがあります。
- 参考情報 (あれば)
- 該当クラス:
ActiveModel::Name - 関連する I18n の pluralization 仕様:
- 関連 issue として PR 説明に挙がっている (取り消し線付き)
#56984は、count引数周りの挙動確認やバグ報告が背景にあったと推測されますが、PR時点では issue としてはクローズまたは不要となり、テスト追加でカバーする形になった可能性があります。
#56989 Simplify activerecord tests with NotificationAssertions helpers
マージ日: 2026/3/16 | 作成者: @Saidbek
- 概要 (1-2文で)
activerecordのテストで、これまでバラバラに書かれていたActiveSupport::Notifications.subscribedベースのコードを、共通ヘルパNotificationAssertions(assert_notification/capture_notifications)に置き換えてシンプルにした PR です。テストの可読性と一貫性を高めるためのリファクタリングであり、挙動や API 仕様の変更はありません。
- 変更内容の詳細
背景
- 以前の PR (#53822) で
NotificationAssertionsヘルパが導入され、テストでの通知関連のアサーションを簡潔に書けるようになりました。 - しかし
activerecord内にはまだ、従来のActiveSupport::Notifications.subscribedを直接使ったテストが一部残っていたため、本 PR でそれらをすべてヘルパ利用に統一しています。
主な変更箇所
対象ファイルは 2 つです。
activerecord/test/cases/instrumentation_test.rbactiverecord/test/cases/relation/load_async_test.rb
いずれもテストコードのみの変更で、合計で 追加 14 行 / 削除 41 行 と、コード量が減っています。
1) load_async_test.rb の変更
これまでは「通知が発火したか」を判定するのに、ブールフラグと手動サブスクライバを使っていました。典型的な旧コードイメージは次のようなものです(擬似コード):
def test_async_load_instruments_notification
notified = false
subscriber = ActiveSupport::Notifications.subscribe("load_async.active_record") do |*args|
notified = true
end
begin
SomeModel.where(...).load_async
ensure
ActiveSupport::Notifications.unsubscribe(subscriber)
end
assert notified
endこれを NotificationAssertions の assert_notification による一行アサーションに置き換えています。イメージ:
def test_async_load_instruments_notification
assert_notification "load_async.active_record" do
SomeModel.where(...).load_async
end
endポイント:
- フラグ変数や
subscribe/unsubscribeの手動管理が不要になり、テストが明快になっています。 - テストの意図(「この処理で
load_async.active_recordが発火するべき」)がダイレクトに読めるようになります。
2) instrumentation_test.rb の変更
こちらでは、通知内容(複数レコードの発火や payload の中身)を検査するために、インラインで配列に push するようなコールバックが書かれていました。典型的な旧コードイメージ:
def test_queries_are_instrumented
events = []
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
events << event
end
begin
SomeModel.where(...).to_a
ensure
ActiveSupport::Notifications.unsubscribe(subscriber)
end
payloads = events.map(&:payload).reject { |p| p[:name] == "SCHEMA" }
assert payloads.any? { |p| p[:sql].include?("SELECT") }
endこれを capture_notifications で置き換えています。イメージ:
def test_queries_are_instrumented
events = capture_notifications("sql.active_record") do
SomeModel.where(...).to_a
end
payloads = events.map(&:payload).reject { |p| p[:name] == "SCHEMA" }
assert payloads.any? { |p| p[:sql].include?("SELECT") }
endここでの方針:
- 「通知が出たかどうか」だけでよいケース →
assert_notification - payload を詳細に触る必要があるケース →
capture_notificationsでEventを取得してからmap/rejectなどでフィルタリング
これにより:
ActiveSupport::Notifications.subscribe/unsubscribeの定型コードが消え、対象ロジックとアサーションだけが残る形になります。- どのイベント名を検査しているかが、テストメソッドの先頭で分かりやすくなります。
- 影響範囲・注意点
- 対象は テストコードのみ で、
activerecord本体の機能・挙動には影響しません。 NotificationAssertionsを利用する前提として、テスト環境でActiveSupport::Testing::NotificationAssertionsがミックスインされている必要があります(ActiveSupport::TestCaseなど)。本 PR は既存のセットアップを前提としており、新たに利用側で設定を変える必要はありません。- もし他のコンポーネントでまだ手動の
Notifications.subscribedを使っているテストがある場合、本 PR の方針に倣ってassert_notification/capture_notificationsに置き換えることで、保守性が上がります。
- 参考情報 (あれば)
Rails API ドキュメント:
ActiveSupport::Testing::NotificationAssertions
https://api.rubyonrails.org/classes/ActiveSupport/Testing/NotificationAssertions.html関連 PR(前段の導入):
https://github.com/rails/rails/pull/53822
#56992 Deprecate require_dependency
マージ日: 2026/3/15 | 作成者: @fxn
- 概要 (1-2文で)
require_dependencyが「非推奨 (deprecate)」になり、将来的な削除に向けた準備が始まりました。クラシックオートローダからの移行や Rails < 7.0 サポートのために残されていた API を正式に廃止方向へ進める変更です。
- 変更内容の詳細
2-1. require_dependency の役割と今回の変更方針
- もともと
require_dependencyは classic autoloader(Zeitwerk 導入前の自前オートローダ)向けの仕組みで、- 依存関係クラス/モジュールの再読み込みを正しく行うため
- 通常の
requireではオートロード・再読み込みの追跡ができないケースを補うため
に使われていました。
- Zeitwerk ベースの autoloading が標準になって久しく、かつ Rails 7 以降を前提にした開発が一般的になってきたため、「互換性維持のために残していた
require_dependencyをそろそろ削除していく」という流れです。 - この PR では「即削除」ではなく「非推奨化 (deprecation)」で、利用時に警告を出すような形に変更されています。
2-2. コードレベルの変更点
※PR本文から読み取れる範囲・Rails の一般的な流れからの推定を含みます。
activesupport/lib/active_support/dependencies/require_dependency.rb
require_dependencyの実装ファイルに、非推奨警告を出すロジックが追加されています。典型的には、呼び出されたときに次のようなメッセージを出す形になります(実際の文言は PR/CHANGELOG に依存しますが、趣旨としては以下のような内容):
rubyActiveSupport::Deprecation.warn( "`require_dependency` is deprecated and will be removed in a future version of Rails. " \ "If you rely on it for autoloading, please ensure your code is compatible with Zeitwerk " \ "and use conventional autoloading instead." )既存の挙動(指定したファイルを
requireしつつ依存追跡する)は保たれていますが、「使うと警告が出る」状態になります。
activesupport/test/dependencies/*.rb, dependencies_test.rb
mutual_one.rb,mutual_two.rb,requires_nonexistent1.rbなどのテスト用ファイルからコードが削除されており、require_dependencyに依存したテストケースが整理・簡略化されています。dependencies_test.rbでは +24/-14 と比較的大きな変更があり、require_dependencyのテストが「非推奨 API 用テスト」的なものに書き換えられている- あるいは、「これまで必須とされていた挙動」から「将来削除可能なレガシー API」として位置づけるようなテスト方針に変更されている
という方向性が考えられます。
activesupport/CHANGELOG.md
- CHANGELOG に
require_dependency非推奨の項目が追加されています(+30 行)。 - おそらく以下のような内容が含まれているはずです:
require_dependencyが非推奨になったこと- 代替策として「Zeitwerk の自動読み込みに従うこと」「どうしても明示読み込みが必要な場合は通常の
requireを使うこと」などの記述 - 削除予定バージョン(例: Rails 8.0 など)が示されている可能性あり
- 影響範囲・注意点
3-1. 影響を受けるコード
次のようなコードを書いているアプリ/エンジンが警告対象になります:
rubyclass PostsController < ApplicationController require_dependency 'admin/post_policy' # これが非推奨に end特に注意が必要なのは、エンジンや gem の中で
require_dependencyを使っているケースです。- それらは「Rails < 7.0 もサポートするために残している」ことが多く、まさに PR 本文に書かれている対象です。
- 今後、サポート対象バージョンを上げるタイミングで
require_dependencyの削除を検討すべきです。
3-2. 実務上どう対応すべきか
Zeitwerk に沿ったオートロードを徹底する
app/models,app/controllersなどの標準パスを使い、クラス名とファイルパスを Zeitwerk ルールに合わせる。lib/以下をオートロードしたい場合はconfig.autoload_paths/eager_load_pathsを適切に設定する。
明示
require_dependencyをやめる- 多くの場合、正しいパスにファイルを置きさえすれば
require_dependency自体が不要になります。 - 本当に「明示的に読み込みたいだけ」であれば、単純に
requireかrequire_relativeに置き換えればよいです。- この場合、そのファイルは 再読み込み対象ではなくなる ので、開発環境でのコード変更が自動で反映されないことに注意してください。
- 多くの場合、正しいパスにファイルを置きさえすれば
エンジン/gem でのマルチバージョンサポート
- Rails 5/6 をまだサポートする gem では、いきなり
require_dependencyを消せない場合があります。 - とはいえ、この PR により 最新版 Rails では deprecation 警告が出続けるようになるため、
- 「Rails 7 未満サポートをいつまで続けるか」
- 「次のメジャー gem バージョンで
require_dependencyと古い Rails サポートを一気に落とす」
といった移行計画を立てることが推奨されます。
- Rails 5/6 をまだサポートする gem では、いきなり
3-3. 将来的な削除リスク
- この PR は「削除のための第一歩」としての非推奨化です。
- 将来のメジャーバージョン(例: Rails 8 以降)で
require_dependencyが完全に削除される可能性が高いので、- 今のうちに全コードベースからの利用箇所を洗い出し、置き換え/設計見直しをしておくのが安全です。
- CI などで deprecation をエラー扱いにしているプロジェクトは、すぐにビルドが赤くなるはずなので要対応です。
- 参考情報 (あれば)
- 該当 PR:
- Zeitwerk オートローディングガイド:
- 過去の文脈(classic autoloader 廃止関連):
- Rails 6 以降で Zeitwerk がデフォルトになり、classic は非推奨 → 削除の流れ
- その移行期における「
require_dependency温存」が役目を終えつつある状況です。
#56986 Further reduce internal usage of cattr_accessor and mattr_accessor
マージ日: 2026/3/15 | 作成者: @byroot
- 概要 (1-2文で)
Rails内部でcattr_accessor/mattr_accessor(クラス変数ベースのアクセサ)の利用をさらに減らし、主に「クラスインスタンス変数」やclass_attributeへの移行を進めたPRです。クラス変数の共有挙動による思わぬ副作用を避け、より直感的で安全な設定・状態管理に寄せるリファクタリングになります。
- 変更内容の詳細
背景: なぜ cattr_accessor / mattr_accessor を減らすのか
cattr_accessor/mattr_accessorは内部的に「クラス変数(@@var)」を使います。- クラス変数は「継承階層で共有」されるため、
- サブクラス側の変更が親クラスや兄弟クラスにも影響する
- ライブラリ・アプリが混在すると意図せぬ状態共有が起きやすい
という問題があります。
- 近年 Ruby 本体側でクラス変数の速度は改善されたものの、「共有スコープの広さ」という意味での問題は残ったままです。
- そのため Rails コアでは、
- ほとんどのケースで「クラスインスタンス変数」 (
@varをselfに対して持つ) を使う - 必要に応じて
class_attribute(継承時のコピー/オーバーライドが扱いやすい仕組み)を使う
という指針に寄せており、本PRはそのフォローアップです。
- ほとんどのケースで「クラスインスタンス変数」 (
※ 前提となるPR: https://github.com/rails/rails/pull/42442
主な変更ポイント
実際の差分は細かいですが、パターンとしては次の2種類です。
1) cattr_accessor / mattr_accessor -> class_attribute or クラスインスタンス変数
ファイルごとの変更はおおむね以下のような内容です。(疑似コードで表現)
例:
activesupport/lib/active_support.rbなどでの変更ruby# 以前 (イメージ) module ActiveSupport mattr_accessor :some_config, instance_accessor: false, default: nil end # 以後 (イメージ) module ActiveSupport class_attribute :some_config, instance_accessor: false, default: nil endまたは、
class_attributeも不要なシンプルなケースでは、rubyclass SomeClass # 以前 cattr_accessor :some_flag # 以後 class << self attr_accessor :some_flag end @some_flag = false endのように、クラスインスタンス変数+クラスメソッドのアクセサに切り替えられています。
ActiveSupport::Cache、ActiveStorage::Blob、ActionMailbox::Routingなどでも同様に、キャッシュ設定やルーティング設定といった「グローバル寄りの設定値」を、クラス変数による共有からより局所的な管理に移行しています。
2) 古い互換レイヤの削除・整理
activesupport/lib/active_support/core_ext/date_and_time/compatibility.rbDate/Time/DateTimeまわりの互換レイヤに関するコードがまとめて削除されています(-23行、追加0)。
Rails が過去バージョンとの互換性のために持っていた「古い挙動向けのフラグ」やクラス変数を整理し、現行挙動に一本化していると考えられます。- 同様に
date_time/compatibility.rbとtime/compatibility.rbもクリーンアップされています。
3) テストコードの更新
activerecord/test/models/developer.rb、membership.rbなどのテスト用モデルや、activesupport/test/cache/behaviors/cache_store_behavior.rbやdependencies_test.rbなど
で、もともと cattr_accessor / mattr_accessor 前提で書かれていたテスト設定が、class_attribute やクラスインスタンス変数前提のコードに書き換えられています。
これによりテストが新しいパターンを前提としたものになり、今後 cattr_accessor / mattr_accessor を減らしやすくなります。
- 影響範囲・注意点
Rails 内部への影響
- あくまで「内部実装の状態保持方法の見直し」であり、パブリックAPIのインターフェース(メソッド名や返り値など)は基本的に変わっていません。
- ただし、クラス変数からクラスインスタンス変数や
class_attributeに変えたことで、「継承したサブクラス間での状態共有の仕方」が内部的に変わっている箇所があります。- これまで「たまたまクラス変数の共有挙動に依存してうまく動いていた」ような非公開API依存のコードは、挙動が変わる可能性があります(特に monkey patch や内部クラスを継承している場合)。
アプリケーション開発者への実務的な注意
- 通常のアプリケーション開発では、このPR単体による破壊的変更の影響はほぼありません。
- ただし、次のような場合は注意が必要です:
- Rails 内部クラスを継承して、クラスレベルの設定を直接上書きしている
- Rails の内部モジュール内のクラス変数(@@xxx)に直接アクセスしている
- このPRは「Rails コアはクラス変数を避ける」という方向性をさらに明確にしたものなので、アプリ側でも同様の方針をとるのが望ましいです:
- 新規コードでは
cattr_accessor/mattr_accessorよりも、- 単純な「クラスごとの設定・状態」ならクラスインスタンス変数+
class << self; attr_accessor ... end - 継承関係でオーバーライド・デフォルトの継承を扱いたいなら
class_attributeを優先するようにすると、将来的なトラブルを避けやすくなります。
- 単純な「クラスごとの設定・状態」ならクラスインスタンス変数+
- 新規コードでは
パフォーマンス面
- Ruby 本体でクラス変数のアクセス速度は改善されているものの、クラスインスタンス変数/
class_attributeによるシンプルな構造の方が、プロファイル上も扱いが素直になりやすく、並行実行環境でも予期しない状態共有を減らせます。 - PRの規模は +48/-82 と比較的小さく、パフォーマンス上の大きな悪化は想定されません。むしろ設計上の明確化によるメンテナンス性向上が主眼です。
- 参考情報 (あれば)
- 本PR:
https://github.com/rails/rails/pull/56986 - フォローアップ元PR(以前の
cattr_accessor/mattr_accessor削減作業):
https://github.com/rails/rails/pull/42442 class_attributeの挙動について(Rails Guides / API Doc):- クラス変数 vs クラスインスタンス変数の解説(参考になる一般的な資料):
#56983 [ci skip] Update CHANGELOG with transform_keys! overwrite precedence note
マージ日: 2026/3/15 | 作成者: @diaphragm
- 概要 (1-2文で)
このPRは、Rails 8.0.3 で行われたActiveSupport::HashWithIndifferentAccess#transform_keys!のバグ修正に伴い、「キー変換時にキーが衝突した場合の上書き優先順位」が変わったことを CHANGELOG に明記するドキュメント修正です。挙動は Ruby 標準のHash#transform_keys!に合わせて「あと勝ち」になっており、その仕様変更をサンプル付きで説明しています。
- 変更内容の詳細
何をしたか
activesupport/CHANGELOG.mdの Rails 8.0.3 の項目に、ActiveSupport::HashWithIndifferentAccess#transform_keys!の上書き優先順位変更に関する注記を追記。- 具体的な「Before / After」コード例を追加し、キー衝突時の挙動が旧バージョンからどのように変化したかを示しています。
挙動のポイント
ActiveSupport::HashWithIndifferentAccess#transform_keys! でキー変換を行ったとき、異なるキーが同じキーに変換されて衝突した場合の扱いが変わっています。
- 以前 (8.0.3 以前):
- 「先に存在していたキーの値」が優先される(先勝ち)
- 現在 (8.0.3 以降):
- 「あとから変換されたキーの値」が優先される(後勝ち)
- 標準の
Hash#transform_keys!と同じ挙動
イメージ例
※ CHANGELOG の例は camelCase / snake_case の衝突を使って説明されています(例のイメージ):
hash = ActiveSupport::HashWithIndifferentAccess.new(
"userName" => "Alice",
:user_name => "Bob"
)
hash.transform_keys! { |k| k.to_s.underscore }
# Rails 8.0.2 まで (旧挙動: 先勝ち)
# => { "user_name" => "Alice" }
# Rails 8.0.3 以降 (新挙動: 後勝ち, Ruby Hash と同じ)
# => { "user_name" => "Bob" }このように、camelCase と snake_case が同じ user_name に変換されるケースで、どちらの値が最終的に残るかが逆転したことが明示されています。
- 影響範囲・注意点
影響範囲
ActiveSupport::HashWithIndifferentAccess#transform_keys!を利用しており、かつ- キー変換ロジックによって「複数の異なるキーが同一キーへマージされる」コード
- 例:
camelCase→snake_case変換、prefix/suffix の削除、正規化処理 など
を行っている箇所で、最終的に残る値が 以前と逆になる 可能性があります。
注意点
- 今回のPRはコード変更ではなく ドキュメントのみ の更新ですが、
- Rails 8.0.2 以前 → 8.0.3 以降にアップグレードする場合、
- すでに挙動は「後勝ち」に変わっており、その仕様に依存する/していたロジックは再確認が必要です。
- 特に「キー変換後のハッシュをマージロジックとして利用している」「衝突時に先勝ちを前提にしていた」コードは、テストで期待値が変わっていないかを確認した方が安全です。
- 逆に、「Ruby 標準 Hash と同じであってほしい」と期待していた場合は、今回の挙動が正しい(統一された)ものになります。
- 今回のPRはコード変更ではなく ドキュメントのみ の更新ですが、
- 参考情報 (あれば)
- 対応する issue:
Fixes #56960- Rails 8.0.3 の
HashWithIndifferentAccess#transform_keys!バグ修正により上書き優先順位が変わったが、ドキュメントに明記されておらず混乱を招く可能性がある、という報告に対する対応。
- Rails 8.0.3 の
- 関連機能:
- Ruby:
Hash#transform_keys! - Rails:
ActiveSupport::HashWithIndifferentAccess#transform_keys!
(今回の挙動は Ruby のHashに合わせる形で統一されています。)
- Ruby:
#56974 Simplify applicable activesupport tests with NotificationAssertions helpers
マージ日: 2026/3/13 | 作成者: @larouxn
- 概要 (1-2文で)
ActiveSupport::Notificationsを使ったキャッシュ関連テストを、ActiveSupport::Testing::NotificationAssertionsヘルパーで書き換えて簡潔・安全にした PR です。手動でのサブスクライバ管理やインスタンス変数ベースのイベント収集をやめ、共通ヘルパーによる記述に統一しています。
- 変更内容の詳細
どのテストが対象か
activesupport/test/cache/behaviors/cache_store_behavior.rb 内の「通知まわり」をテストしているヘルパーメソッド(モジュール的な共通テスト)が変更されています。
このファイルのテストは以下の各ストアテストから include されて実行されています。
file_store_test.rbmem_cache_store_test.rbmemory_store_test.rbredis_cache_store_test.rb
つまり、「各キャッシュストア共通の Notification テストの書き方」を差し替えた、という位置づけです。
具体的なリファクタリング内容
1) NotificationAssertions ヘルパーの利用
これまで:
ActiveSupport::Notifications.subscribeで手動購読ensureブロックでunsubscribeを手動で実施- ブロック内で
@events << eventのようにインスタンス変数にイベントを溜める - テスト本体で
@eventsを検査
という流れだったものを、ActiveSupport::Testing::NotificationAssertions で用意されているヘルパーに書き換えています。
代表的には以下の2パターンが使われます:
「通知が送られたことをその場で検証」するタイプ
- 例:
assert_notification "cache_read.active_support" do ... end - ブロック内で対象コードを実行し、通知が飛んだか・payload がどうかを引数で受けて検証
- 例:
「通知をキャプチャだけして後で自前で検証」するタイプ
- 例:
events = capture_notifications("cache_read.active_support") do ... end - ブロック実行中に飛んだイベントの配列を返し、あとで
eventsに対して好きな assertion を書く
- 例:
この PR の説明にある通り、「適しているところは assert_* 系で直接アサート」「それ以外は capture して自前アサート」という使い分けに変更されています。
2) @events → ローカル変数 events への整理
旧来はテストクラスのインスタンス変数 @events にイベントを溜め込んでいましたが、ヘルパーが events を戻り値として返すため、テストメソッド内で完結するローカル変数 events に切り替えています。
これにより:
- テスト間で状態が残らない(インスタンス変数に依存しない)
- メソッド毎に「どのイベントを見ているか」が分かりやすくなる
といった形でテストコードが局所的かつ明瞭になります。
3) ensure ブロックでの unsubscribe 削除
NotificationAssertions が購読の開始・終了をラップし、テスト終了時に自動でクリーンアップするため、
subscription = ActiveSupport::Notifications.subscribe("...") { ... }
begin
...
ensure
ActiveSupport::Notifications.unsubscribe(subscription)
endのようなパターンは不要になり、ブロックベースな API だけで完結するように書き換えられています。
テストが例外で落ちても leak しないクリーンアップが保証されるのは従来どおりですが、コードでそれを毎回書かなくて良くなっています。
- 影響範囲・注意点
影響範囲
- プロダクションコードは一切変更なし。
activesupportのキャッシュストア共通テスト (cache_store_behavior) を include している以下のテストに限られます:- FileStore / MemCacheStore / MemoryStore / RedisCacheStore
- 動作仕様としての挙動は変えておらず、「テストの書き方・実装スタイル」のみの変更です。
注意点
- 今後
ActiveSupport::Notificationsを使ったテストを書く場合は、同様にActiveSupport::Testing::NotificationAssertionsを使うのが推奨される流れになります。 - 既存のテストコードで手動 subscribe/unsubscribe している箇所は、今後この PR のように段階的に置き換えられていく可能性が高いです。
- NotificationAssertions の挙動(イベントのキャプチャ粒度・スレッドとの関係など)に依存することになるため、独自ラッパーを重ねる場合は挙動が二重にならないよう注意が必要です。
- 今後
- 参考情報 (あれば)
この PR で利用しているヘルパーの公式ドキュメント:
https://api.rubyonrails.org/classes/ActiveSupport/Testing/NotificationAssertions.htmlNotificationAssertions 導入・拡張 PR:
関連する follow up / 類似リファクタリング PR:
#56975 Simplify applicable activerecord tests with NotificationAssertions helpers
マージ日: 2026/3/13 | 作成者: @larouxn
- 概要 (1-2文で)
activerecordの一部テストで、ActiveSupport::Notificationsを直接扱っていた箇所を、ActiveSupport::Testing::NotificationAssertionsヘルパーに置き換えて簡潔にした PR です。これにより通知購読の登録/解除やアサーションが共通化され、テストコードが短く・安全になっています。
- 変更内容の詳細
全体方針
- 対象:
ActiveRecordのテストのうち、ActiveSupport::Notificationsを使ってイベント発火を検証しているテスト。 - 変更: 手書きで
ActiveSupport::Notifications.subscribe/unsubscribeや配列に push してアサートしていたコードを、ActiveSupport::Testing::NotificationAssertionsモジュールのヘルパーに置き換え。 - 効果:
ensureでの購読解除 (unsubscribe) が不要(ヘルパー側で自動クリーンアップ)。- よくあるパターン(特定イベントが発火したか・何件か・どんな payload か)のアサートを一行で書ける。
- テストの意図が読み取りやすくなる。
NotificationAssertions に代表的には以下のような API があります(ドキュメント: https://api.rubyonrails.org/classes/ActiveSupport/Testing/NotificationAssertions.html):
# ブロックの中で指定イベントが 1 回だけ発火することを検証
assert_notification "sql.active_record" do
User.first
end
# 発火した通知を配列で受け取ってカスタム検証
notifications = capture_notifications("sql.active_record") do
User.first
end
assert_equal 1, notifications.size
assert_equal "User Load", notifications.first[:name]PR ではこのようなヘルパーを使った形に書き換えています。
ファイルごとのポイント
※PR の diff から読み取れる典型パターンをまとめています(行数は +/- の規模感のみ)。
1) activerecord/test/cases/adapters/abstract_mysql_adapter/empty_all_tables_test.rb (+2/-8)
- 目的:
empty_all_tables実行時に、想定したActiveSupport::Notificationsのイベント(例:sql.active_recordや adapter 固有のイベント)が発火しているかを検証するテスト。 - 従来:
notifications = []subscriber = ActiveSupport::Notifications.subscribe("...") { |*args| notifications << args }ensureでActiveSupport::Notifications.unsubscribe(subscriber)assert_equalでnotificationsの件数や内容を確認
- 変更後:
- 上記を
capture_notificationsあるいは類似ヘルパーに置き換え:rubynotifications = capture_notifications("sql.active_record") do ActiveRecord::Base.connection.empty_all_tables! end assert_equal expected_count, notifications.size - 手動での
subscribe/ensureブロック削除。
- 上記を
2) activerecord/test/cases/associations/belongs_to_associations_test.rb (+2/-6)
- 目的:
belongs_toに関連した操作(ロードや保存)で、所定の ActiveRecord 通知が発生するかの検証。 - 変更内容:
- 通知の購読・解除と配列への push を、
capture_notificationsなどのヘルパーに置き換え。 - テスト本体は維持しつつ、「どのイベントが何回発火したか」をシンプルにアサート。
- 通知の購読・解除と配列への push を、
例イメージ:
notifications = capture_notifications("sql.active_record") do
post = Post.create!(...)
post.author # belongs_to のロードなど
end
assert_operator notifications.size, :>=, 13) activerecord/test/cases/associations/deprecation_test.rb (+6/-12)
- 目的: 非推奨の関連付け API 利用時に、特定の deprecation 関連通知が発火することを検証。
- 変更点:
- ここは通知の発火そのものに加え、「イベント名・payload 内容を詳細に見る」テストである可能性が高く、
assert_notificationではなくcapture_notificationsパターンが多いと考えられます。 - それでも
subscribe/unsubscribeはすべてヘルパーに一本化。
- ここは通知の発火そのものに加え、「イベント名・payload 内容を詳細に見る」テストである可能性が高く、
- 結果:
- 行数減少幅が比較的大きく、テストの意図(「この操作でこの deprecation 通知が出る」)がより直接的なコードになっている。
サンプルイメージ:
notifications = capture_notifications("deprecation.rails") do
# 非推奨な has_and_belongs_to_many 等を呼ぶ
end
assert notifications.any? { |n| n[:message].include?("...") }4) activerecord/test/cases/associations/has_many_associations_test.rb (+2/-7)
- 目的:
has_many関連のロードや書き込みに対して、期待した通知が出ているかを検証。 - 変更:
- 他と同様に独自に定義していた購読ロジックをヘルパーへ移行。
- イベント発火回数や、特定の関連に対する SQL 実行を間接的に確認するエテストをスリム化。
5) activerecord/test/cases/associations/has_one_associations_test.rb (+2/-6)
has_one版のhas_manyと同様の整理。- 通知捕捉が
NotificationAssertionsベースになり、冗長な後始末コードが削除。
6) activerecord/test/cases/transaction_isolation_test.rb (+10/-30)
- 目的: トランザクション分離レベル関連の処理に伴うイベント(
sql.active_record等)を検証。 - 従来:
- 複数の
subscribe/unsubscribeとensureブロックでトランザクション内外の通知を捕捉。
- 複数の
- 変更後:
capture_notificationsによるブロック単位の通知収集へ。- 分離レベル変更クエリや実際の SELECT/UPDATE クエリに対する通知を、イベントリストを元に明瞭にアサート。
- 行数削減が大きいことから、ここでのテストがもっとも複雑な独自購読ロジックを持っていたと考えられます。
例イメージ:
notifications = capture_notifications("sql.active_record") do
Person.transaction(isolation: :serializable) do
Person.first
end
end
# 分離レベル設定用の SQL と通常の SELECT の両方が発火しているか等を確認
assert notifications.any? { |n| n[:sql].match?(/SET TRANSACTION ISOLATION LEVEL SERIALIZABLE/i) }- 影響範囲・注意点
- 影響範囲:
- プロダクションコードではなく、
activerecordのテストコードのみの変更。 - テストのロジック(「何を検証しているか」)は基本的に同じで、実装手段だけがヘルパー利用に変わっている。
- プロダクションコードではなく、
- 利用前提:
NotificationAssertionsを利用するには、テストケースがActiveSupport::Testing::NotificationAssertionsをincludeしている必要がありますが、対象テストスイートでは既に利用可能な前提(以前の PR で導入済)。
- 注意点:
- 今後
ActiveSupport::Notificationsを使った新規テストを書く場合は、従来の手作りsubscribe/unsubscribeパターンではなく、このヘルパーを使うのが推奨されます。 - ヘルパーは自動でクリーンアップを行うため、複雑にネストした通知購読など独自のライフサイクル制御が必要な場合は、仕様(リンク先ドキュメント)を一度確認した方が安全です。
- まだ他にも
NotificationAssertionsで書き換え可能なテストが残っていると明言されているため、今後も類似のクリーンアップ PR が出る可能性があります。
- 今後
- 参考情報 (あれば)
NotificationAssertions の API ドキュメント
https://api.rubyonrails.org/classes/ActiveSupport/Testing/NotificationAssertions.html関連 PR:
#56972 Add MySQL lock option and extend algorithm to column DDL operations
マージ日: 2026/3/13 | 作成者: @dominikdarnel
- 概要 (1-2文で)
このPRは、Rails の MySQL アダプタにlock:オプションを追加し、これまでインデックス操作にしか使えなかったalgorithm:オプションをカラム操作(add_columnなど)にも拡張するものです。これにより、MySQL のオンラインDDL機能(ALGORITHM/LOCK)を Rails のマイグレーションDSLから直接制御できるようになります。
- 変更内容の詳細
対応したオプションと対象操作
MySQL アダプタに以下のオプションが追加・拡張されています。
lock:オプション(MySQL)- 対象:
add_indexremove_index- カラム操作:
add_columnchange_columnremove_columnrename_column
- 値:
:default,:none,:shared,:exclusive- MySQL の
LOCK = {DEFAULT|NONE|SHARED|EXCLUSIVE}に対応
- 対象:
algorithm:オプション(MySQL)- 既存: インデックス操作 (
add_index,remove_index) に対してサポート済 - 今回拡張: 上記カラム操作にも利用可能
- 値:
:default,:copy,:inplace,:instant- MySQL の
ALGORITHM = {DEFAULT|COPY|INPLACE|INSTANT}に対応
- 既存: インデックス操作 (
具体的な利用例
これまで MySQL のオンラインDDLを使いたい場合、raw SQL が必要でした:
# 変更前: raw SQL が必要
execute "ALTER TABLE users ADD name VARCHAR(255), ALGORITHM = INSTANT, LOCK = NONE"このPR後は、通常のマイグレーションDSLで指定できます:
# インデックス操作
add_index :users, :email, algorithm: :inplace, lock: :none
remove_index :users, :email, algorithm: :inplace, lock: :none
# カラム追加
add_column :users, :name, :string, algorithm: :instant, lock: :none
# カラム定義変更
change_column :users, :name, :string, null: false, algorithm: :inplace, lock: :none
# カラム削除
remove_column :users, :name, algorithm: :inplace, lock: :none
# カラム名変更
rename_column :users, :name, :full_name, algorithm: :inplace, lock: :none内部的には、MySQLの ALTER TABLE 文末に以下のように付与されます:
ALTER TABLE `users`
ADD COLUMN `name` varchar(255)
ALGORITHM = INSTANT
LOCK = NONECommandRecorder の対応
ロールバック時にも同じオンラインDDLオプションが保持されるように、CommandRecorder#invert_rename_column が拡張されています。
# マイグレーション
rename_column :users, :name, :full_name, algorithm: :inplace, lock: :none
# ロールバック時も同じオプションが付いた rename に反転される
rename_column :users, :full_name, :name, algorithm: :inplace, lock: :noneこれは、PostgreSQL における algorithm: :concurrently サポートと同様の実装パターンに従っています。
実装まわりに触れている主な変更点
高レベルの観点でいうと:
- 抽象レイヤ (
abstract/schema_statements.rb,abstract/schema_definitions.rb) に、カラム関連DDLでもalgorithm/lockオプションを受け取れるようなインターフェイスを追加・調整 - MySQL アダプタ (
abstract_mysql_adapter.rb,mysql/schema_statements.rb,mysql/schema_creation.rb) で、渡されたalgorithm:/lock:をALTER TABLE ... ALGORITHM = ... LOCK = ...に組み立てるロジックを追加 migration/command_recorder.rbで、記録するコマンドにalgorithm/lockを含め、特にrename_columnの反転時にもオプションを維持- テスト:
- MySQL 用 ActiveSchema テストに、各カラム操作+インデックス操作で
algorithm/lockが正しくSQLに反映されるかのテストを追加 - 無効なオプションの扱い(
invalid_options_test)の拡充 - CommandRecorder のオプション保持のテストを追加
- MySQL 用 ActiveSchema テストに、各カラム操作+インデックス操作で
- ガイド (
active_record_migrations.md) に、MySQLでのalgorithm:/lock:利用方法の説明を追加
- 影響範囲・注意点
- 対象DB:
- MySQL アダプタのみ(PostgreSQL の
algorithm: :concurrentlyには影響なし)
- MySQL アダプタのみ(PostgreSQL の
- 既存コードへの互換性:
algorithm:/lock:を指定していない既存マイグレーションの挙動は変わりません。- デフォルトはMySQLの
ALGORITHM = DEFAULT/LOCK = DEFAULT相当で、従来と同様にMySQL側のデフォルト動作に委ねられます。
- オプション値の妥当性:
- サポートされていない値を渡すと、テストを見る限り(
invalid_options_test)従来同様に例外になるか、少なくともRails側で不正オプションとして扱われます。 - 値のtypo(例:
:instat)などには注意が必要です。
- サポートされていない値を渡すと、テストを見る限り(
- 実運用上の注意:
algorithm:/lock:は MySQL のバージョンやテーブルの状態によりサポート/不支持があります。- 例:
ALGORITHM=INSTANTはMySQL 8系かつ特定のDDLにしか対応しないなど。
- 例:
- 不正な組み合わせ(例: そのDDLでは
INSTANTがサポートされない)を指定した場合は、MySQL 側がエラーを返します。そのため、本番適用前に対象MySQLバージョンで検証することが重要です。
- マイグレーション設計へのインパクト:
- 大規模テーブルのオンラインスキーマ変更を、RailsのマイグレーションDSLだけで表現しやすくなります。
- これまで
execute "ALTER TABLE ..."で書いていた部分を、DSLベースにリファクタリング可能です。 - ロールバック時にも同じオプションが使われるため、オンラインDDLを前提とした安全なマイグレーション/ロールバック戦略を組みやすくなります。
- 参考情報 (あれば)
- PR本文のディスカッション:
https://discuss.rubyonrails.org/t/add-mysql-lock-option-and-extend-algorithm-to-column-ddl-operations/90249 - MySQLドキュメント:
ALTER TABLE,ALGORITHM,LOCKALTER TABLE ... ALGORITHM/LOCKの詳細仕様やサポートされる組み合わせは、利用している MySQL バージョンの公式マニュアルを参照してください。
- 類似機能:
- PostgreSQL の
algorithm: :concurrentlyサポートと同じ思想で、DDLのロック特性/実行方法をマイグレーションDSLから制御するための拡張です。
- PostgreSQL の
#56945 ActiveRecord: Support Postgres RESET on readonly queries
マージ日: 2026/3/13 | 作成者: @frodsan
- 概要 (1-2文で)
PostgreSQL アダプタでprevent_writes: true(読み取り専用コンテキスト)でもRESETコマンドを実行できるようにした PR です。これにより、接続を「書き込み禁止」にしていても、セッションパラメータをデフォルトに戻すためのRESETが ReadOnly エラーにならず通るようになります。
- 変更内容の詳細
やりたいこと / 背景
- Rails の PostgreSQL アダプタは
prevent_writes: trueの場合に「書き込みっぽい」SQLを検出するとActiveRecord::ReadOnlyErrorを投げます。 - しかし PostgreSQL の
RESETはSET config_parameter TO DEFAULTのシンタックスシュガーであり、多くの場合は「状態を元に戻す」だけで、本質的には危険な書き込みではありません。 - それにもかかわらず、現状の実装では
RESETが「書き込み」と判定されて ReadOnlyError が発生していたため、それを許可するように修正しています。
実際のコード変更ポイント
1) PostgreSQL アダプタの prevent_writes 判定の緩和
activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb 内で、SQL が書き込みかどうかを判定するロジックに RESET を許可する変更が入っています。
イメージとしては、例えば次のような判定ロジックの一部が:
WRITE_QUERY = /\A\s*(UPDATE|INSERT|DELETE|ALTER|...)/iのような形で書き込み系を検出していて、RESET が「ブロック対象」側に入っていた・もしくは包括的にブロックされていたところを、
RESETについてはprevent_writesの対象から除外- つまり
RESETはprevent_writes中でも実行可能
という扱いに変えています。(実際のコードは 1 行差分ですが、このポリシー変更が本質です)
2) テスト追加
activerecord/test/cases/adapters/postgresql/postgresql_adapter_prevent_writes_test.rb にテストが追加されています。
ざっくりしたイメージ:
def test_reset_is_allowed_with_prevent_writes
ActiveRecord::Base.connected_to(role: :reading, prevent_writes: true) do
ActiveRecord::Base.connection.execute("RESET ALL")
# ここで例外が出ないことを確認
end
endのように、「読み取り専用コンテキストで RESET を実行しても ActiveRecord::ReadOnlyError が起きない」ことを検証するテストが足されています。
3) CHANGELOG 追記
activerecord/CHANGELOG.md に以下のような記述が追加されています:
- PostgreSQL アダプタが
prevent_writes: trueのときでもRESETを実行できるようになった旨 - バージョンアップ時の利用者向け情報としての位置づけ
- 影響範囲・注意点
影響範囲
- 対象: PostgreSQL を使っていて、
ActiveRecord::Base.connected_to(... prevent_writes: true)などで読み取り専用ルーティング・保護をしているアプリケーション。 - これまでは:
prevent_writes: true下でRESETを実行するとActiveRecord::ReadOnlyErrorが発生。
- 今後は:
- 同じ条件でも
RESETは実行可能になり、エラーにならない。
- 同じ条件でも
セキュリティ / 一貫性上の注意点
RESET自体はセッションパラメータをデフォルトに戻すだけで、データ行の変更は行いません。- ただし、
RESETによって以下のような「間接的な影響」がありうる点には注意:- 以前に
SET ROLEやSET search_pathなどを行っていた場合、それがデフォルト設定に戻る。 - アプリ側が「セッションパラメータをある前提」で動いているコードがあるなら、
RESETによる前提崩れには気を付ける必要があります(これは prevent_writes の有無にかかわらず本質的な注意点)。
- 以前に
とはいえ、この PR の変更は「RESET を prevent_writes フィルタから除外するだけ」であり、既に手動で RESET を使っていたアプリケーションにとっては「ReadOnly 保護下でも同じことができるようになる」方向の互換性改善です。
- 参考情報 (あれば)
PostgreSQL 公式ドキュメント: RESET
https://www.postgresql.org/docs/current/sql-reset.htmlRESET configuration_parameterはSET configuration_parameter TO DEFAULTと等価。RESET ALLで全パラメータをデフォルトに戻す。
Rails ガイド(英語): Multiple Databases と読み取り専用接続
https://edgeguides.rubyonrails.org/active_record_multiple_databases.htmlconnected_to(role: :reading, prevent_writes: true)の利用例や、読み取り専用保護の考え方の参考になります。
#56967 Optimize generated Dockerfile build performance
マージ日: 2026/3/13 | 作成者: @damln
- 概要 (1-2文で)
Rails がrails newで生成する Dockerfile テンプレートと、そのテスト用 Dockerfile のビルドを高速化するための最適化 PRです。不要なレイヤーを減らし、COPY --chownを適切に使うことで、実測で 1 分前後のビルド時間短縮が見込まれます。
- 変更内容の詳細
2-1. node_modules 削除をアセットプリコンパイルと同一レイヤーに統合 (Dockerfile.tt)
従来は、ざっくり以下のように別レイヤーで node_modules を削除していたと考えられます:
RUN bundle exec rails assets:precompile
RUN rm -rf node_modulesこれだと:
assets:precompileのレイヤーrm -rf node_modulesのレイヤー
という 2 つのレイヤーができ、特に後者では大量のファイル削除に伴うファイルシステム diff 計算が発生し、BuildKit のオーバーヘッドが大きくなります。
PR では、これを 1 つの RUN にまとめています:
RUN bundle exec rails assets:precompile \
&& rm -rf node_modulesあるいは実際のテンプレートもほぼこれと同等の形になっているはずです。
ポイント:
- レイヤー数削減により、BuildKit が「削除だけを含む大きな diff レイヤー」を計算するコストをなくせる
- 実ビルドで ~13 秒程度短縮と報告されている
- アセットプリコンパイル完了後に
node_modulesを削除する、という意味的な流れは維持されるので挙動は変わらない
2-2. chown -R を COPY --chown に置き換え (Dockerfile.test)
テストフィクスチャの Dockerfile(railties/test/fixtures/Dockerfile.test)は、まだ古いパターンを使っていました:
COPY db log storage tmp /app/
RUN chown -R rails:rails db log storage tmpこのやり方だと:
COPYレイヤー- さらに
chown -Rレイヤー
と 2 レイヤーになり、特に chown -R は多量のファイル・ディレクトリに対する metadata 変更で、ビルドがかなり遅くなります。
メインの Dockerfile テンプレートはすでに COPY --chown を使っていたため、テスト側をそれに合わせる修正です:
COPY --chown=rails:rails db log storage tmp /app/ポイント:
- コピー時に所有者を設定するため、追加の
RUN chown -R ...レイヤーが不要になる - 所有権変更のためのファイルシステム diff 計算が発生せず、ビルド時間が大幅に減る
- 実測ではこの変更だけで ~50 秒程度の短縮と報告されている
- メインテンプレートとテストフィクスチャの挙動が揃う(テンプレートの期待とテストが一貫する)
- 影響範囲・注意点
影響範囲
- 影響対象:
rails new myapp --docker(などで)生成される Dockerfile テンプレート (Dockerfile.tt)- そのテンプレートを検証するテスト用 Dockerfile (
Dockerfile.test)
- 機能面:
- Rails アプリの挙動そのものには変更なし
- Docker イメージ内のファイル構成や権限は、従来と論理的には同等
- 主な効果:
- Docker ビルド時間の短縮
- レイヤー数の削減と、BuildKit の diff 計算コスト削減
注意点・互換性
COPY --chownサポート:- Docker 17.09 以降でサポートされている一般的な機能
- かなり古い Docker を使っていない限り問題にはならない
- 既存プロジェクトへの影響:
- 既に生成済みの Dockerfile には自動反映されないため、必要なら手動で同様の最適化を適用する必要がある
- ビルドキャッシュへの影響:
- レイヤー構成が変わるため、PRを取り込んだ直後の最初のビルドではキャッシュが効かずフルビルドになる可能性はあるが、その後の反復ビルドはより速くなる
rm -rf node_modulesの位置:assets:precompileと同一 RUN にまとめた結果、そのステップが失敗した場合はnode_modulesも削除されない(もともとprecompileが成功しないと次の RUN に到達しないので、意味的にも従来とほぼ同じ)
- 参考情報 (あれば)
- Docker ドキュメント:
- レイヤー最適化の一般的なベストプラクティス:
- 関連するコマンドを 1 つの
RUNにまとめてレイヤー数を減らす - 大量ファイル削除(
rm -rf)や大量権限変更(chown -R)を単独レイヤーにしない
- 関連するコマンドを 1 つの
- この PR が示すベストプラクティス:
- アセットプリコンパイル後に
node_modulesを消してイメージをスリム化するなら、RUN ... && rm -rf node_modulesのように 1 レイヤーで行う - ユーザー権限を合わせるために
chown -Rを後から実行するのではなく、COPY --chownやRUN --chownでコピー時・実行時に適切なユーザーを指定する
- アセットプリコンパイル後に
#56977 Fix SQLite virtual tables not ignored by ignore_tables
マージ日: 2026/3/13 | 作成者: @hschne
- 概要 (1-2文で)
SQLite の仮想テーブル(virtual tables)がActiveRecord::SchemaDumper.ignore_tablesで無視されない問題を修正する PR です。これにより、スキーマダンプ(schema.rbの生成など)から、指定した SQLite 仮想テーブルも正しく除外できるようになりました。
- 変更内容の詳細
主な変更点
- 対象ファイル:
activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rbactiverecord/test/cases/adapters/sqlite3/virtual_table_test.rb
schema_dumper.rb の変更
SQLite 用の SchemaDumper が、テーブル一覧を取得する処理の中で「virtual table」も含めて処理していましたが、ignore_tables のフィルタリングがそれらに対して正しく機能していませんでした。
この PR では、そのフィルタリングロジックを修正し、通常のテーブルと同様に「無視対象テーブル」として扱えるようにしています。
実際のコードは 1 行の修正ですが、イメージとしては以下のような形の変更です(擬似コード):
# 変更前(イメージ)
tables.each do |table|
# ここで virtual table が ignore_tables をすり抜けていた
dump_table(table) unless ignored?(table)
end
# 変更後(イメージ)
tables.each do |table|
next if ignored?(table) # ignore_tables を先に適用
dump_table(table)
endポイントは「SQLite の仮想テーブルも ignore_tables の判定対象に含める」ことです。
テストの追加
activerecord/test/cases/adapters/sqlite3/virtual_table_test.rb に以下のようなテストが追加されています(内容イメージ):
- SQLite の仮想テーブル(例: FTS テーブル)を作成
ActiveRecord::SchemaDumper.ignore_tables = ["virtual_table_name"]を設定- スキーマダンプを実行 (
ActiveRecord::SchemaDumper.dump) - ダンプ結果に仮想テーブルの定義が含まれていないことを検証
これにより、今回の修正が回帰しないように自動テストで担保されています。
- 影響範囲・注意点
影響範囲
- SQLite3 アダプタを使用している Rails アプリで、
- SQLite の virtual table(例: FTS, R*Tree など)を利用しており
ActiveRecord::SchemaDumper.ignore_tablesを使ってスキーマダンプから除外したい
といったケースで、期待通りに無視されるようになります。
- 他の DB アダプタ(PostgreSQL, MySQL など)には影響しません。
- SQLite3 アダプタを使用している Rails アプリで、
互換性/挙動の変化
- これまで「なぜか virtual table が
schema.rbに出力されてしまう」状況だった場合、今回の修正によりignore_tables設定どおり出力されなくなります。 - 逆に「virtual table も含めてダンプされること」を前提にワークアラウンドしていた場合、その前提は崩れますが、基本的にはバグ修正として望ましい方向の変更です。
- これまで「なぜか virtual table が
注意点
ignore_tablesは文字列/正規表現/Proc などを受け付けるため、virtual table 名に対しても適切にマッチするよう設定する必要があります。structure.sqlを使っている場合は、SchemaDumperではなく DB ネイティブのダンプになるため、この修正の影響はありません(schema.rbを使っている場合のみ)。
- 参考情報 (あれば)
- 該当 Issue: #56976 — SQLite virtual tables が
ignore_tablesで無視されない問題の報告 - 対象メソッド:
ActiveRecord::SchemaDumper.ignore_tablesActiveRecord::ConnectionAdapters::SQLite3Adapter向けのSchemaDumper実装
- SQLite 仮想テーブルの代表例:
CREATE VIRTUAL TABLE ... USING fts5(...)CREATE VIRTUAL TABLE ... USING rtree(...)
#56909 Validate URI scheme in Action Text markdown link conversion
マージ日: 2026/3/12 | 作成者: @flavorjones
- 概要 (1-2文で)
Action Text がリッチテキストを Markdown に変換する際、リンクの URI スキームを検証していなかった問題を修正し、javascript:やdata:text/htmlなど危険なスキームを Markdown に出力しないようにした PRです。HTML パイプラインと Markdown パイプラインの安全性・挙動の一貫性を高める修正です。
- 変更内容の詳細
問題の背景
- HTML 出力では
Rails::HTML::Sanitizer::SafeListSanitizerによって、<img src="javascript:...">や<img src="data:text/html,...">のような危険なスキームは除去されていた。 - 一方で Markdown 出力では、
RemoteImage#attachable_markdown_representation→MarkdownConversion.markdown_linkの経路で URI スキームの検証が行われておらず、<action-text-attachment url="data:text/html,PAYLOAD">がそのままとして Markdown に出力されてしまっていた。 - 同じ Markdown パイプライン内でも、アンカーリンクを出力する箇所ではすでに
allowed_uri?が呼ばれており、その点でも不整合があった。
主な修正点
1) MarkdownConversion.markdown_link に URI 検証を追加
Rails::HTML::Sanitizer.allowed_uri?(url)をmarkdown_link内で呼ぶように変更。- 許可されていない URI の場合は、リンクとして出力せず「エスケープされたテキストのみ」を出すように仕様変更。
挙動の例:
以前(問題あり):
<action-text-attachment url="data:text/html,PAYLOAD" caption="Image" />Markdown 変換結果:
修正後:
\[Image\]ここで \[Image\] は、Markdown としては単なるリテラル文字列 [Image](リンクではない)として表示されます。
2) 「リンクを降格した場合」の表現の統一
- 次の2パターンの場合、これまでは単に
[caption]のように「普通のリンク記法に見えるが、実際には URL なし」の出力になっていたケースがあった模様:- URI が
allowed_uri?により拒否された場合 attached_links: falseが指定されており、添付の URL を出力しない設定の場合
- URI が
- 今回の修正により、これらの「リンク降格」ケースはすべて
\[caption\](エスケープされた角括弧)で出力されるよう統一された。
つまり、以前:
[caption]修正後:
\[caption\]視覚的には HTML 表示時に [caption] と見えるが、Markdown としてはリンクではないことがよりはっきりし、
「元々はリンクになり得たが、安全上 or 設定上の理由でリンクにはしていない」ということが出力フォーマット上も明示されるようになっています。
3) Action Text 内部コードの変更点(概要)
actiontext/lib/action_text/markdown_conversion.rbmarkdown_linkの実装を拡張し、Rails::HTML::Sanitizer.allowed_uri?によるチェックを追加。- リンクとして出さない場合のフォールバック表現を
\[title\]に統一。
actiontext/lib/action_text/attachment.rb- 添付の Markdown 変換ロジック(
to_markdown相当)でのリンク出力パスが、この新しいmarkdown_linkの仕様に合わせて整理されたと推測される(+8/-3)。
- 添付の Markdown 変換ロジック(
actiontext/lib/action_text/attachables/remote_image.rbattachable_markdown_representationが新しい挙動(URI 検証済みのmarkdown_link)を使うように微修正(+1/-1)。
actiontext/lib/action_text/engine.rb- Engine 初期化の中で Markdown 関連の設定や依存の読み込み順などが若干整理・削減された(+2/-6)。
actiontext/test/unit/markdown_conversion_test.rb- 危険なスキームの検証、リンク降格の挙動、
attached_links: falseのケースなどをカバーするテストが大幅に追加(+48/-10)。
- 危険なスキームの検証、リンク降格の挙動、
※実際のメソッドシグネチャや細かいコードは PR 本文には載っていませんが、変更内容から上記のような構造になっていると考えられます。
4) 「防御の多層化 (defense-in-depth)」
ActiveStorage::Blobのパスでは、もともとurl_for(self)によりアプリケーション内の安全な URL(ホストやパスがアプリ管理下)だけが生成されるようになっている。- それでも
markdown_link自体がallowed_uri?を持つことで、- 他の呼び出し元(将来的な利用拡張も含む)
- アプリ開発者が独自に
MarkdownConversionを使うケース などでも安全性が担保されるよう、より堅牢な設計にしている。
- 影響範囲・注意点
影響範囲
- Action Text で Markdown 出力を使っている箇所全般
- Rich Text → Markdown の変換をライブラリや独自機能で利用している場合、
危険なスキームを持つ URL が以前より厳しくフィルタされるようになります。
- 「リンク降格」の出力フォーマットが変わる
- 以前:
- 拒否された URL や、
attached_links: falseのときに[caption]のような Markdown リンクに見えるテキストが出力されていた。
- 拒否された URL や、
- 変更後:
\[caption\](エスケープされた角括弧)に統一。
- これに依存してパースしている処理(独自の Markdown パーサ、正規表現マッチングなど)がある場合、
動作が変わる可能性があります。
注意点 / マイグレーション上の考慮
Markdown 生データを後処理している場合:
\[.*\]形式の文字列が増える可能性があるため、正規表現などを利用している場合は記述の見直しが必要かもしれません。
リンクが「突然消えた」ように見えるケース:
- 実際には危険スキームのために正しくブロックされた結果です。
- もしアプリ内で独自スキーム(例:
myapp://user/123)などを使っている場合、それがRails::HTML::Sanitizer.allowed_uri?によって拒否されている可能性があります。- その場合は、
Rails::HTML::Sanitizerの設定を拡張してそのスキームを許可する必要があります(ただしセキュリティ影響をよく検討すること)。
- その場合は、
既存テキストの再変換:
- すでに保存されている Action Text コンテンツを再度 Markdown 変換する処理(例: 一括エクスポート)を行うと、
過去に作られたリンクの一部が\[title\]に変わる可能性があります。 - トラストしていないコンテンツ(ユーザ投稿など)については望ましい挙動ですが、
管理者のみが入力する安全なコンテンツを Markdown としてそのまま再利用しているケースでは違和感が出るかもしれません。
- すでに保存されている Action Text コンテンツを再度 Markdown 変換する処理(例: 一括エクスポート)を行うと、
- 参考情報 (あれば)
- この PR で利用されている
Rails::HTML::Sanitizer.allowed_uri?は、Rails の HTML サニタイズ機能の一部で、http,https,mailtoなどの「安全とみなされるスキーム」だけを許可します。 - HTML パイプライン (SafeListSanitizer) と Markdown パイプライン (MarkdownConversion) の両方で同じポリシーに基づいて URI をフィルタすることで、
- XSS などの攻撃経路を Markdown 経由でも塞ぐ
- 異なる出力フォーマット間での挙動を揃える という目的を達成しています。
- Action Text でカスタムアタッチャブルや Markdown エクスポート機能を実装している場合、この PR による Markdown 出力の変化(リンク降格時の
\[...\]など)を前提にロジックを見直す価値があります。
#56970 Fix parsing SQLite virtual tables without parenthesis
マージ日: 2026/3/12 | 作成者: @nicolasva
- 概要 (1-2文で)
SQLite の仮想テーブル定義をパースするロジックが、USING句に括弧がないケースを正しく扱えていなかった問題を修正した PR です。これにより、CREATE VIRTUAL TABLE ... USING module arg1, arg2のような定義でも、Active Record が正しくメタ情報を取得できるようになります。
- 変更内容の詳細
何が問題だったか
ActiveRecord の SQLite3 アダプタは、schema 情報から仮想テーブルを認識・解析する際に、CREATE VIRTUAL TABLE の SQL を文字列パースしています。
以前の実装は、USING 句に「括弧付き」の形式だけを想定していた可能性があります:
CREATE VIRTUAL TABLE posts USING fts5(title, body);しかし SQLite では、括弧なしの以下のような構文も許可されています:
CREATE VIRTUAL TABLE posts USING fts5 title, body;この「括弧なし」のパターンをアダプタが正しく解釈できず、仮想テーブルのメタ情報取得(カラム情報など)が失敗・誤動作していました (#56969)。
実際のコード変更(推定的なイメージ)
変更は 1 行のみで、sqlite3_adapter.rb の仮想テーブル定義を解析する正規表現または split ロジックが修正されています。
イメージとしては以下のような変更が入ったと考えられます(実際のコードは PR を参照してください):
# 例: 以前は括弧が必須だった
definition.match(/CREATE VIRTUAL TABLE .* USING (\w+)\((.*)\)/)
# 例: 括弧があってもなくてもマッチするようにする
definition.match(/CREATE VIRTUAL TABLE .* USING (\w+)\s*(?:\((.*)\)|(.*))/)もしくは、USING の後ろの文字列を「括弧付きなら中身を取り出し、括弧がなければそのまま引数列として扱う」ような分岐処理に変更された可能性が高いです。
テストの追加
activerecord/test/cases/adapters/sqlite3/virtual_table_test.rb に 9 行のテストが追加され、以下のようなケースがカバーされています:
- 括弧なしで
USINGを記述した仮想テーブルを作成 - そのテーブルに対して、
columnsなどのメタ情報取得が期待通り動作するかを検証
例(イメージ):
def test_virtual_table_without_parenthesis
@connection.execute <<-SQL
CREATE VIRTUAL TABLE posts USING fts5 title, body;
SQL
columns = @connection.columns("posts").map(&:name)
assert_includes columns, "title"
assert_includes columns, "body"
end- 影響範囲・注意点
- 対象: SQLite3 アダプタを使い、かつ SQLite の仮想テーブル(特に FTS など)を利用している Rails アプリケーション。
- 具体的な影響:
- これまで
CREATE VIRTUAL TABLE ... USING module arg1, arg2のように括弧なしで定義していた場合、schema_dump,db:schema:dump,db:structure:dumpなどで不正確な情報が出ていた、あるいはエラーになっていた場合に改善が見込まれます。 - schema 情報を元にしたメタプログラミング(カラム一覧取得など)が、括弧なし構文でも安定して動作するようになります。
- これまで
- 互換性:
- 括弧ありの構文の挙動は変えない形での修正なので、既存の仮想テーブル定義(一般的な
USING fts5(...)形式)に対して後方互換性は維持されます。 - 変更はパース処理の一部のみで、クエリ実行や接続管理など他の機能には影響しません。
- 括弧ありの構文の挙動は変えない形での修正なので、既存の仮想テーブル定義(一般的な
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/56970
- 関連 Issue: https://github.com/rails/rails/issues/56969
- SQLite 仮想テーブル構文(
CREATE VIRTUAL TABLE):
https://www.sqlite.org/lang_createvtab.html
#56963 Restore previous instrumenter after execute_or_skip
マージ日: 2026/3/12 | 作成者: @rosa
- 概要 (1-2文で)
async_load/async_pickなどの非同期クエリ実行時に、ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]が元に戻らず、「そのスレッドで以後発生するsql.active_record通知がロギングされなくなる」不具合を修正したPRです。特にasync_query_executor = :global_thread_pool+fallback_policy = :caller_runsという構成で、スレッドプール飽和時にメインスレッドが汚染される問題を防ぎます。
- 変更内容の詳細
問題の発生条件・原因
前提:
- Railsの非同期クエリ (
async_load,async_pick) はQueryIntent#execute_or_skipを経由して実行される。 - 非同期クエリでは、クエリ中に発生する
sql.active_record通知を一度EventBufferに貯め、最後にflushしてまとめて発火する、という形でインストルメンテーションしている。 - そのために
ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]に一時的にEventBufferをセットしている。
- Railsの非同期クエリ (
ところが
execute_or_skipは、IsolatedExecutionState[:active_record_instrumenter]の「以前の値」を保存せず、- 実行後に「元のインストルメンター」に戻していなかった。
通常想定されている動作:
- 非同期クエリはグローバルスレッドプール上の「背景スレッド」で実行される。
- そのスレッド上では
IsolatedExecutionState[:active_record_instrumenter]はもともとnilであることが多く、EventBufferをセットしてもスレッドのライフタイム的にはあまり問題になりにくい。
しかし、
async_query_executor = :global_thread_poolかつfallback_policy = :caller_runsの場合:- スレッドプール/キューが飽和すると、バックグラウンド実行ではなく「呼び出し元スレッド(多くはwebリクエスト処理スレッド)」で非同期タスクが実行される。
- その際にも
execute_or_skipはIsolatedExecutionState[:active_record_instrumenter]にEventBufferを上書きするが、「元の値を復元しない」ため、そのリクエストスレッドのインストルメンターが永続的にEventBufferのままになる。 EventBufferへのflushは非同期クエリ完了時に1回だけ呼ばれるが、その後もインストルメンターがEventBufferのままなため、- 以後のすべての
sql.active_record通知がEventBufferに飲み込まれたまま publish されない。 ActiveSupport::Notifications.subscribe("sql.active_record")しているログ・計測系がすべて無効化された状態になる。
- 以後のすべての
- さらにこの状態は、プロセス内でそのスレッドが生きている限り続くため、「時間の経過とともに汚染されたスレッドが増える」→「計測されないリクエストが時間とともに増加する」という挙動になる。
実際に報告されている現象:
- デプロイ直後は問題のあるリクエストはゼロ。
- 時間が経つにつれて、インストルメンテーションされない(SQLログが出ない等)リクエストが徐々に増える。
- 再デプロイするとプロセス・スレッドが再生成されるため、再びゼロに戻る。
修正内容
activerecord/lib/active_record/connection_adapters/query_intent.rb の execute_or_skip 周辺に、以下の修正が入っています(概念的な擬似コードで表現):
def execute_or_skip
# 追加: 現在のインストルメンターを退避
previous_instrumenter = ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]
# 非同期クエリ用の EventBuffer をセット
ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] =
ActiveRecord::EventBuffer.new
begin
# ここで実際にクエリ実行 / スケジューリング
# ...
ensure
# 追加: 終了時に元のインストルメンターを必ず復元
ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] =
previous_instrumenter
end
end※ 実際のコードでは行追加は +2 だけなので、上記はイメージです。実際には EventBuffer のセットと実行ブロックの前後に previous_instrumenter の保存・復元を2行程度加えている形です。
ポイント:
- 「
EventBufferをセットする前に、既存のインストルメンターを保存」 - 「
EventBufferを使い終わったら、必ず(例外の有無にかかわらず)保存しておいた元のインストルメンターを復元」
背景スレッドの場合
- 多くの場合
previous_instrumenterはnilなので、- 保存:
nilを保存 - 復元:
nilに戻す
という no-op に近い挙動で、実質的な挙動は従来と変わりません。
- 保存:
caller_runs(呼び出し元スレッド)で実行された場合
previous_instrumenterには、「リクエスト処理用の本来のインストルメンター(通常の ActiveSupport::Notifications 連携等)」が入っている。- 一時的に
EventBufferに切り替えて非同期クエリ相当分を実行した後、最後に「もともとのインストルメンター」をきちんと戻すことで、- 以後の
sql.active_record通知は元通り publish される。 EventBufferによる「スレッドの汚染」が発生しない。
- 以後の
テスト追加
activerecord/test/cases/relation/load_async_test.rb に33行のテストコードが追加されています。
意図としては主に以下を検証していると考えられます(ファイル名・PRの説明からの推測を含む):
async_load/async_pick実行中にEventBufferが使われるにも関わらず、実行後はインストルメンターが元に戻っていること。- 特に「同じスレッドで複数回
async_loadしたり、非同期クエリの後に通常の同期クエリを実行したりしても、sql.active_record通知が正しく購読者に届く」こと。
- 影響範囲・注意点
影響範囲
対象となるのは主に以下の条件を満たすアプリケーションです:
- Active Record の非同期クエリ (
async_load,async_pick) を使用している。 config.active_record.async_query_executor = :global_thread_poolを使っている、または同様のスレッドプール構成を使っている。- スレッドプールが飽和して
fallback_policy = :caller_runs経由で呼び出し元スレッドがタスクを実行する可能性がある(global_thread_poolはデフォルトでこれ)。
- Active Record の非同期クエリ (
測定・ログ側の影響:
- これまで「時間の経過とともに一部のリクエストだけ SQL ログ/計測が抜ける」ような謎の現象が出ていた場合、この修正で解消される可能性が高いです。
- 逆に言うと、一部のログが silently 失われていたのが「ようやく本来あるべき状態で出るようになる」ので、モニタリング上は一時的に「SQLが増えた」「計測値が変わった」ように見えるかもしれません。
パフォーマンス・互換性
execute_or_skipでの追加処理はIsolatedExecutionStateからの読み取り1回- 書き戻し1回 だけで、非常に軽量です。
- 挙動は「インストルメンターを正しくスコープ内だけで切り替える」という素直な変更であり、互換性を壊すようなものではありません。
- 既存の
ActiveSupport::Notifications/sql.active_recordサブスクライバのインターフェースには変更なし。
注意点 / 実運用で見るべきポイント
- もしアプリ側で独自に
ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]を直接いじっているような高度なメタプログラミングをしていると、この挙動変更と噛み合わない可能性がありますが、通常のアプリではまず該当しません。 - 非同期クエリを多用し、かつ
global_thread_poolが頻繁に飽和している場合:- 本PRは「インストルメンターの汚染」を防ぐものであって、「プール飽和そのもの」を解決するものではありません。
- 必要に応じて
- スレッドプールの
concurrencyやmax_queueの調整 - 非同期クエリの数/タイミングの見直し
- 接続プールのサイズ調整 などのチューニングは依然として必要になります。
- スレッドプールの
- 参考情報 (あれば)
Rails ガイド: Active Record の設定
async_query_executorの説明と:global_thread_poolの挙動:
https://guides.rubyonrails.org/configuring.html#config-active-record-async-query-executorglobal_thread_poolの設定(fallback_policy = :caller_runsなど):activerecord/lib/active_record.rb内の定義
https://github.com/rails/rails/blob/0209a70c57ebef4fb1e271444ec7e62794932db3/activerecord/lib/active_record.rb#L304-L313問題の実体:
ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]を用いた、スレッドローカルなインストルメンテーション制御が「スレッド境界を意識しない」形で使われていたことで、caller_runsという特殊な実行経路と噛み合ってバグになっていた、という構造です。- 「スレッドプールの fallback 政策が、アプリケーションロジック(インストルメンテーション)と予期せぬ形で相互作用した典型例」としても参考になる内容です。
#56964 Restore previous instrumenter after execute_or_skip
マージ日: 2026/3/12 | 作成者: @rosa
- 概要 (1-2文で)
ActiveRecord::FutureResult#execute_or_skip実行時にActiveSupport::IsolatedExecutionState[:active_record_instrumenter]を一時的にEventBufferに差し替えたあと、元のインストゥルメンタを正しく復元するようにしたバグ修正です。これにより、スレッドプール飽和時にリクエストスレッド上でタスクが実行された場合でも、sql.active_record通知が失われずに済むようになります。
- 変更内容の詳細
問題の背景
FutureResult#execute_or_skipの中で、SQL通知をバッファリングするためにActiveSupport::IsolatedExecutionState[:active_record_instrumenter] = EventBuffer.new
のようにインストゥルメンタを差し替えている。- 本来これは「バックグラウンドスレッド上で実行される」ことを前提としており、そのスレッドのライフサイクル内だけで完結する想定。
- しかしグローバル非同期実行器(global async executor)が
fallback_policy: :caller_runsを使っているため、スレッドプールが飽和すると、タスクが呼び出し元スレッド(=場合によってはWebリクエストスレッド)上で実行される。 - その場合、リクエストスレッドの
IsolatedExecutionState[:active_record_instrumenter]がEventBufferに書き換えられたまま元に戻らない。 - Rails はリクエスト毎にこのキーをクリアしていないため、そのスレッドで今後発生するすべての
sql.active_record通知が EventBuffer に飲み込まれ、flush もされず、サブスクライバに届かなくなるという永続的な汚染状態が起きる。
対応内容
FutureResult#execute_or_skip 内で、インストゥルメンタの元の値を保存し、処理終了後に必ず復元するように修正されています。
擬似コードで表すと、以下のような対応です(実際のコードはこれに近い構造):
def execute_or_skip
previous_instrumenter =
ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]
ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] =
ActiveRecord::EventBuffer.new
begin
# 非同期実行しつつ結果を待つ / スキップする
...
ensure
ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] =
previous_instrumenter
end
end- バックグラウンドスレッドで実行される場合
- 元の値は
nilであることが多く、nil→EventBuffer→nilに戻すだけの no-op に近い挙動。
- 元の値は
- リクエストスレッド上で
caller_runsにより実行された場合- もともと入っていた「本物のインストゥルメンタ」を
previous_instrumenterとして保存し、ensureで確実に復元。 - これにより、後続の SQL 実行は再び通常どおり
sql.active_record通知を発行し、サブスクライバ(ログ、APM、メトリクスなど)にも届く。
- もともと入っていた「本物のインストゥルメンタ」を
テストの追加
activerecord/test/cases/relation/load_async_test.rb にテストが追加されており、主に以下を確認しています:
load_async実行後もインストゥルメンタが元に戻っていること。- 特に、
caller_runsパスでリクエストスレッドが汚染されないことを検証する形になっている(PR の意図からするとそのようなケースをカバーしている)。
CHANGELOG (activerecord/CHANGELOG.md) には、8.0 向けのバグ修正としてこの挙動変更が追記されています。
- 影響範囲・注意点
- 影響を受けるのは:
ActiveRecord::Relation#load_asyncなど、FutureResultベースの非同期ロード機構を利用しているコード- かつ、グローバル async executor のスレッドプールが飽和し、
fallback_policy: :caller_runsによってリクエストスレッド上で実行されるケース
- このバグにより発生し得た症状:
- 一部のリクエストスレッドで
sql.active_record通知が一切来なくなる - ログ、ActiveSupport::Notifications ベースのメトリクス、APM/トレーシングなどが、そのスレッドでの SQL を観測できなくなる
- 原因がわかりづらく、再現性もプールの状態依存で不安定
- 一部のリクエストスレッドで
- この修正による既存アプリへの影響:
- 本来の正しい挙動への修正であり、互換性破壊ではなくバグフィックス。
- これまで「たまたま通知が止まっていた」ケースで、通知が正常に飛ぶようになるため、メトリクスやログが増えたように見える可能性はある。
- アプリケーション側で特別な対応は基本的に不要ですが、
load_asyncや custom な非同期クエリ実行を多用しており、- かつ「一部リクエストでだけ SQL ログ/メトリクスが出ない」といった謎の現象に遭遇していた場合、 この修正が問題を解決している可能性があります。
- 参考情報 (あれば)
- 元 PR(main 向け): https://github.com/rails/rails/pull/56963
- 本 PR はその Rails 8.0 向けバックポート。
- 関連するコンポーネント:
ActiveRecord::FutureResultActiveSupport::IsolatedExecutionStateActiveRecordの async loading (load_async)ActiveSupport::Notifications/sql.active_recordイベント
#56965 Restore previous instrumenter after execute_or_skip
マージ日: 2026/3/12 | 作成者: @rosa
- 概要 (1-2文で)
ActiveRecord::FutureResult#execute_or_skip実行時にActiveSupport::IsolatedExecutionState[:active_record_instrumenter]を一時的にEventBufferに差し替えたあと、元のインストルメンターを復元していなかったバグを修正する PR です。スレッドプール飽和時にcaller_runsでリクエストスレッド上で async ロードが実行されると、以降そのスレッドで発火するsql.active_record通知が二度と購読者に届かなくなる問題を防ぎます。
- 変更内容の詳細
問題の背景
ActiveRecord::FutureResult#execute_or_skipは、非同期ロード結果を「実行 or スキップ」する処理で、内部でクエリ実行時の計測(instrumentation)をバッファリングするためにEventBufferを使っています。- その際に、スレッドローカルな
ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]をEventBufferオブジェクトにセットしていましたが、処理後に元の値へ戻していませんでした。
通常想定:
- 非同期実行はバックグラウンドスレッド(スレッドプール内)で行われる想定で、そのスレッドでは
:active_record_instrumenterがnilのことが多く、「nilをEventBufferに変えて、そのままEventBufferのまま」になっていても実害がほぼない、という前提でした。
しかし実際には:
- Rails のグローバル async executor が
fallback_policy: :caller_runsを使っているため、スレッドプールが飽和すると、タスクが呼び出し元スレッド(しばしばリクエスト処理中のアプリケーションスレッド)上で実行されます。 - リクエストスレッド上では、すでに「本来の」instrumenter(
sql.active_record通知を購読者に流すためのオブジェクト)がIsolatedExecutionStateに入っています。 - そこを
EventBufferで上書きし、かつ戻さないため、そのスレッドのライフタイム中ずっと:active_record_instrumenterがEventBufferのままになってしまいます。
その結果:
- 以降そのスレッドで実行されるクエリの
sql.active_recordイベントは、「EventBuffer には貯まるが、flush は FutureResult の完了時に一度だけ呼ばれる」構造になっているため、FutureResult 実行以降のイベントは永遠に flush されず、購読者に届かない状態になります。 - Rails はリクエスト間で
ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]をクリアしていないため、この「汚染」はスレッドが破棄されるまで永続します。
修正内容
PR の主な修正は次の一点です:
FutureResult#execute_or_skip内でEventBufferを:active_record_instrumenterにセットする前に、現在の値を退避し、EventBufferの利用が終わったら必ず元のインストルメンターに戻すようにしました。
擬似コードイメージ:
def execute_or_skip
previous_instrumenter = ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]
ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] = event_buffer
begin
# FutureResult の実行/スキップ処理
ensure
# 必ず元に戻す
ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] = previous_instrumenter
end
endこれにより:
- バックグラウンドスレッド(instrumenter が
nil)では「nilを保存してnilに戻す」だけなので実質ノーオペ。 caller_runsでリクエストスレッド上で実行された場合でも、- 一時的に
EventBufferに差し替えてイベントをバッファ - 結果の flush 後、元のインストルメンターに確実に復元 という挙動になり、以降の
sql.active_record通知が正常に流れ続けます。
- 一時的に
テストとドキュメント
activerecord/test/cases/relation/load_async_test.rbにテストが追加され、caller_runsシナリオ(呼び出し側スレッドで async ロードが実行されるケース)でも instrumenter が汚染されないことを確認しています。activerecord/CHANGELOG.mdに、この修正内容が 8.1 系向けのバグフィックスとして追記されています。- この PR は main (または 8.2 以降) での修正 PR #56963 の 8.1 バックポートです。
- 影響範囲・注意点
- 影響を受けるのは、
load_asyncなど Active Record の非同期ロード機能と計測・監視機構(ActiveSupport::Notifications、ログ、APM 連携など)を併用しているプロジェクトです。 - 特に以下の条件を満たす環境で、これまで「一部のリクエストで
sql.active_record通知が突然出なくなる」・「クエリログ/メトリクスが欠損する」といった現象が起きていた可能性があります:- Active Record の async クエリを利用している (
load_asyncなど) - グローバル async executor が
fallback_policy: :caller_runsで動いている(Rails デフォルト) - スレッドプールが飽和しやすい(高負荷環境など)
- Active Record の async クエリを利用している (
- この修正により、既存コード側で対応する必要は基本的にありませんが、
- 以前、上記のような「時々発火しなくなる
sql.active_record通知」を感じて独自ワークアラウンドを入れている場合は、その挙動が変わる可能性があります。
- 以前、上記のような「時々発火しなくなる
- パフォーマンスへの影響は極小です:
- 追加された処理は、「スレッドローカルの値を一時退避・復元する」だけで、非同期クエリ実行時のみ実行されます。
- 参考情報 (あれば)
- 元 PR(本流ブランチ向け): https://github.com/rails/rails/pull/56963
- 本 PR(8.1 向けバックポート): https://github.com/rails/rails/pull/56965
- 関連箇所:
ActiveRecord::FutureResult(非同期クエリ実行の結果ラッパ)ActiveSupport::IsolatedExecutionState(スレッドローカルな Execution Context)sql.active_record通知(クエリごとの instrumentation イベント)
#56958 Remove create tests from passwords_controller_test.rb if ActionMailer::Railtie is not defined
マージ日: 2026/3/11 | 作成者: @ssjr
- 概要 (1-2文で)
Rails アプリを--skip-action-mailerオプション付きで作成した場合に、rails g authenticationが生成するパスワード関連テストで Action Mailer 依存のテストが落ちていた問題を修正する PR です。ActionMailer::Railtie が未定義のときは、PasswordsController#createに関するメール送信テストを生成しないようにしました。
- 変更内容の詳細
背景・問題点
再現手順の通り、
rails new my-test-app --skip-action-mailer
cd my-test-app
sed -i 's/# root/root/g' config/routes.rb
bin/rails g authentication
bin/rails db:migrate
bin/rake testとすると、以下のようなエラーが発生していました。
NameError: uninitialized constant PasswordsControllerTest::PasswordsMailerPasswordsMailer自体が存在しない(Action Mailer をスキップしているため)
NoMethodError: undefined method 'assert_enqueued_emails'- Action Mailer のテストヘルパがロードされていない
原因は、Action Mailer をスキップしても authentication generator がメール送信前提のテスト (create アクション用テスト) を生成していたことです。
実際の変更点
対象ファイル:
railties/lib/rails/generators/test_unit/authentication/templates/test/controllers/passwords_controller_test.rb.ttrailties/test/generators/authentication_generator_test.rb
1) テンプレート側: メール依存テストを条件付きで生成
passwords_controller_test.rb のテンプレート (.tt) に、ActionMailer::Railtie の有無でテスト生成を切り替える条件が追加されました。
イメージとしては、テンプレート内に以下のような条件分岐が入ります(あくまで概念的なサンプル):
<% if defined?(ActionMailer::Railtie) %>
test "create" do
# ここに PasswordsMailer や assert_enqueued_emails を使ったテスト
end
test "create_for_an_unknown_user_redirects_but_sends_no_mail" do
# 同上
end
<% end %>実際には ERB テンプレートとして、ActionMailer::Railtie が存在する環境だけで create 関連テストを展開するようになっています。
これにより、--skip-action-mailer でアプリを生成した場合:
PasswordsControllerTestにcreateアクションのメール送信テストが含まれなくなる- ゆえに
PasswordsMailerの未定義エラーやassert_enqueued_emailsの未定義エラーが発生しない
2) Generator のテスト追加
railties/test/generators/authentication_generator_test.rb にテストが追加され、
--skip-action-mailer付きでアプリを生成rails g authentication実行- 生成された
passwords_controller_test.rbを検査し、- メール依存テストが生成されていないこと
を確認するアサーションが増えています。
このテストにより、今後の変更で再び Action Mailer 非依存環境にメールテストを生成してしまう退行を防ぎます。
- 影響範囲・注意点
- 影響対象:
- TestUnit + authentication generator を使うプロジェクト
- かつ
rails new ... --skip-action-mailerで Action Mailer を明示的に無効化しているケース
- メリット:
- 上記条件下で
bin/rails test/bin/rake testが素の状態でグリーンになる - 不要な
PasswordsMailerの作成や Action Mailer の設定を強制されなくなる
- 上記条件下で
- 注意点:
- Action Mailer をスキップしている場合、当然ながら パスワードリセットメールの送信機能自体は提供されない ため、必要な場合は:
--skip-action-mailerを使わない- もしくは自前でメール機構とテストを用意する
- 既に生成済みのアプリでは、この PR の前に生成した
passwords_controller_test.rbには依然としてメールテストが残っている可能性があるため、- 新規に generator を再実行するか
- 既存テストから
create関連のメールテストを削除する必要があります。
- Action Mailer をスキップしている場合、当然ながら パスワードリセットメールの送信機能自体は提供されない ため、必要な場合は:
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/56958
- 関連機能:
- Authentication generator (
bin/rails g authentication) --skip-action-mailerオプションで Action Mailer を無効化した Rails アプリの生成
- Authentication generator (
- 類似の設計パターン:
--skip-active-recordなど、Railtie をスキップした際には、その Railtie に依存するコード・テストを generator 側で条件付き生成する、という Rails の一般的な方針に沿った変更です。
#56956 Simplify applicable actionpack tests with NotificationAssertions helpers
マージ日: 2026/3/10 | 作成者: @larouxn
- 概要 (1-2文で)
actionpackのredirect_test.rb内でActiveSupport::Notificationsを直接扱っていたテストを、ActiveSupport::Testing::NotificationAssertionsヘルパーを使う形に書き換え、テストコードを簡潔かつ安全にした PR です。通知の購読・解除やアサーションの記述がヘルパーに集約され、重複コードや ensure ブロックが削減されています。
- 変更内容の詳細
何をやったか
- 対象ファイル:
actionpack/test/controller/redirect_test.rb - 5つのテストケースが、手書きの
ActiveSupport::Notifications.subscribe/unsubscribeベースの実装から、NotificationAssertionsモジュールのヘルパーメソッドを使う形に置き換えられています。 - 行数ベースでは +19 / -45 と、テストコードがかなりスリムになっています。
具体的なリファクタリング内容
元のテストでは、例えば以下のようなパターンで通知をテストしていたと考えられます(イメージ):
def test_redirect_triggers_notification
events = []
callback = ->(*args) do
events << ActiveSupport::Notifications::Event.new(*args)
end
ActiveSupport::Notifications.subscribe("redirect.action_controller", &callback)
begin
get :some_action
ensure
ActiveSupport::Notifications.unsubscribe(callback)
end
assert_equal 1, events.size
assert_equal "/new_path", events.first.payload[:location]
endこれが NotificationAssertions を使うと、次のように簡略化されます(イメージ):
def test_redirect_triggers_notification
events = capture_notifications("redirect.action_controller") do
get :some_action
end
assert_equal 1, events.size
assert_equal "/new_path", events.first.payload[:location]
endもしくは、よりダイレクトにアサートするケースでは assert_notifications / assert_notification 系のヘルパーが使われているはずです:
assert_notification "redirect.action_controller" do |event|
get :some_action
# ブロックが終わるときに、"redirect.action_controller" が1回発火していること等を検証
endNotificationAssertions の主なポイントは:
- テスト毎に
subscribe/unsubscribeを自動で行い、ensure ブロックが不要になる。 - 通知の収集 (
capture_notifications) と、通知の存在/回数などのアサーション (assert_notifications等) を 統一されたインターフェースで扱える。 ActiveSupport::Notificationsの生 API に直接触れるコードが減り、テスト意図(どのイベントがどう発生するべきか)が読み取りやすくなる。
この PR では、それらのヘルパーを redirect_test.rb 内の「リダイレクト時にどの通知がいつ発火するか」を検証するテストに適用し、重複・儀式的コードを削除しています。
- 影響範囲・注意点
影響範囲
- 変更はテストコード (
actionpack/test/controller/redirect_test.rb) のみであり、実行時のアプリケーションコード (actionpackの本体) には一切手が入っていません。 - したがって、Rails を利用するアプリケーションや、
actionpackの挙動そのものには 機能的な変更や互換性の影響はありません。 - CI 上の
actionpackテストの一部がNotificationAssertionsに依存するようになりましたが、これはすでに Rails 本体に追加済みのテスト用モジュールです。
- 変更はテストコード (
注意点 / 開発者視点での示唆
- 今後
ActiveSupport::Notificationsを用いたテストを書く際は、素手で subscribe/unsubscribe せずにNotificationAssertionsを使うのが推奨される流れになります。 - 特に ensure でのクリーンアップ漏れや、複数テスト間での購読状態の汚染など、テストの不安定要因を減らせるため、同様のパターンのテストは追随してリファクタリングするとよいです。
- もし独自のテストヘルパーやカスタム通知ロギングを持っている場合は、
NotificationAssertionsに置き換えられるかどうかを検討する価値があります。
- 今後
- 参考情報 (あれば)
- Rails API:
ActiveSupport::Testing::NotificationAssertions
https://api.rubyonrails.org/classes/ActiveSupport/Testing/NotificationAssertions.html - 関連 PR:
- NotificationAssertions 追加 PR: https://github.com/rails/rails/pull/53065
- 追加改善 PR: https://github.com/rails/rails/pull/54126
- 類似のテスト整理 PR: https://github.com/rails/rails/pull/53819
#56932 Fix dbconsole NotImplementedError message
マージ日: 2026/3/9 | 作成者: @eglitobias
- 概要 (1-2文で)
ActiveRecord の抽象アダプタAbstractAdapter.dbconsoleが投げるNotImplementedErrorのエラーメッセージを修正し、「Class should define …」という曖昧な文言を、実際のアダプタクラス名が出るように改善した PR です。機能追加や挙動変更ではなく、開発者向けのエラーメッセージの改善のみが行われています。
- 変更内容の詳細
対象:activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
ActiveRecord::ConnectionAdapters::AbstractAdapter のクラスメソッド dbconsole 内で、まだ実装していないアダプタがこのメソッドを呼び出した際に NotImplementedError を投げる部分があります。
元々 PR #54719 によって、クラスメソッド内での文言生成に self.class が使われるようになっていましたが、クラスメソッドでは self は「クラス」そのものを指す一方で、self.class は「クラスのクラス(= Class)」になってしまいます。その結果、例外メッセージに意図しない「Class」という文字列が表示されていました。
この PR では、その部分を self.class から self に戻すことで、正しくアダプタクラス名が表示されるようにしています。
イメージとしては、以下のような実装になります(実際のコードのイメージ):
def self.dbconsole(...)
raise NotImplementedError, "#{self} should define `dbconsole` ..."
endクラスメソッド内なので self は例えば ActiveRecord::ConnectionAdapters::AbstractAdapter 等になり、そのまま文字列展開されます。
動作例
修正前
ActiveRecord::ConnectionAdapters::AbstractAdapter.dbconsole(nil)
# => NotImplementedError: Class should define `dbconsole` ...ここで self.class が Class になるため、「Class should define …」という分かりにくいメッセージが出ていました。
修正後
ActiveRecord::ConnectionAdapters::AbstractAdapter.dbconsole(nil)
# => NotImplementedError: ActiveRecord::ConnectionAdapters::AbstractAdapter should define `dbconsole` ...どのクラスが dbconsole を実装すべきかが明確に分かるメッセージになりました。
- 影響範囲・注意点
- 影響範囲は エラー文言のみ で、実際の
dbconsoleの挙動や API 仕様は一切変わりません。 AbstractAdapter.dbconsoleを直接呼び出すことは通常あまりないですが、独自アダプタを実装している場合や、ActiveRecord の接続アダプタ周りを拡張している場合には、NotImplementedErrorを読む機会があります。その際に、どのクラスで実装が必要なのかがより分かりやすくなります。- 既存テストやアプリケーションコード側で、例外メッセージ文字列を 厳密一致でテストしている場合(例:
"Class should definedbconsole..."を含むかどうかを見ているテスト)は、文言変更によりテストが落ちる可能性があります。このようなテストは"should define \dbconsole`"` のような部分一致に変更するのがよいです。
- 参考情報 (あれば)
- 関連 PR:
- #54719:
AbstractAdapter.dbconsole周りの変更を行い、今回の問題の原因になった PR
- #54719:
- 関連クラス・メソッド:
ActiveRecord::ConnectionAdapters::AbstractAdapter.dbconsole
- Ruby の文法的ポイント:
- クラスメソッド内では
selfは「クラス」そのものを指す (AbstractAdapter)、self.classはそのクラスのクラスであるClassを指すため、文字列展開に使うと"Class"になってしまう。
- クラスメソッド内では
#56941 Remove foreign_key coercision in counter_cache
マージ日: 2026/3/9 | 作成者: @skipkayhil
- 概要 (1-2文で)
counter_cacheの内部実装から、Reflection#foreign_keyに対するto_s/to_symによる型変換(強制変換)が削除されました。foreign_keyは常に凍結済みの文字列またはその配列であることが前提とされ、その前提に合わせてコードが簡潔化されています。
- 変更内容の詳細
背景
Active Record の関連(has_many / belongs_to など)を表す Reflection オブジェクトは、foreign_key メソッドで外部キー名を返します。この PR では:
Reflection#foreign_keyは常に以下のいずれかである- 凍結された interned String(
"user_id".freezeのようなもの) - そのような String の凍結済み配列(複合キーなどを想定)
- 凍結された interned String(
- したがって
foreign_key.to_sやforeign_key.to_symといった「毎回の型変換」は不要
という前提を明示し、それに伴って counter_cache 周りのコードから変換処理を削っています。
実際の変更イメージ
activerecord/lib/active_record/counter_cache.rb で、概ね次のような変更が行われたと考えられます(擬似コード):
# 変更前(イメージ)
def foreign_key_name(reflection)
reflection.foreign_key.to_s
end
def some_counter_cache_logic(reflection)
fk = reflection.foreign_key.to_sym
# ...
end
# 変更後(イメージ)
def foreign_key_name(reflection)
reflection.foreign_key
end
def some_counter_cache_logic(reflection)
fk = reflection.foreign_key
# ...
endポイント:
to_s/to_symの呼び出しが消え、reflection.foreign_keyをそのまま使うように整理。- 配列の場合も「配列の中身はすでに正しい型・状態である」という前提で扱う。
行数レベルでは:
- 追加 3 行 / 削除 7 行
- 主に不要な型変換ロジックの削除によるコードの簡素化。
- 影響範囲・注意点
影響範囲
- ActiveRecord の
counter_cache機能内部に限定される変更であり、アプリケーションコードから直接呼ぶ API のシグネチャは変わっていません。 Reflection#foreign_keyの戻り値契約(常に凍結済み String か、凍結済み String の配列)が前提になるため、- Rails の内部実装同士の整合性が高まり、
- 無駄なオブジェクト生成や文字列変換が減ることで、非常に小さいながらもパフォーマンス・メモリ効率の改善が期待できます。
注意点
通常のアプリ開発者:
belongs_to :post, counter_cache: trueのような使い方には影響ありません。- マイグレーションやスキーマ定義を変える必要もありません。
Rails拡張やメタプログラミングをしている場合:
ActiveRecord::Reflectionを自前でモンキーパッチしており、foreign_keyが「常に String / Array<String> を返す」契約を破っている場合、- これまでは
to_s/to_symによって“なんとなく動いていた”ケースが、動かなくなる可能性があります。
- これまでは
- 例えば、
foreign_keyをnilやシンボルにしたり、独自オブジェクトを返したりしている場合は要確認です。- 現行 Rails の他の箇所でも
foreign_keyが文字列前提で利用されているため、この PR に限らず非互換な実装です。
- 現行 Rails の他の箇所でも
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/56941
- 関連クラス:
ActiveRecord::Reflection::AssociationReflection#foreign_keyActiveRecord::CounterCache(activerecord/lib/active_record/counter_cache.rb)
- 背景知識:
- Rails ではパフォーマンス・メモリ効率向上のため、頻用される文字列を「interned string」(
String#freeze等)で扱う方針があり、その一環としてのリファクタリングと考えられます。
- Rails ではパフォーマンス・メモリ効率向上のため、頻用される文字列を「interned string」(
#56951 Fix default value of raise_on_missing_required_finder_order_columns configuration.
マージ日: 2026/3/9 | 作成者: @r-plus
- 概要 (1-2文で)
ActiveRecord.raise_on_missing_required_finder_order_columnsのデフォルト値が、ガイドに書かれているfalseではなくnilになっていた問題を修正し、明示的にfalseをデフォルトとする PR です。挙動としてはnilとfalseはどちらも falsy なので動作は変わりませんが、ドキュメントと実装を揃えるための修正です。
- 変更内容の詳細(あればサンプルコードも含めて)
修正対象
activerecord/lib/active_record.rb内のraise_on_missing_required_finder_order_columnsのデフォルト値をnil→falseに変更。
背景
- Rails Guides では
config.active_record.raise_on_missing_required_finder_order_columnsのデフォルトはfalseと明記されているが、load_defaultsが 8.1 未満の環境では、実際にはnilが設定されていた。 - この差異は、過去の PR (#54608) の「コピペして書き換えを忘れた」ミスが原因と指摘されています。
- Rails Guides では
実際の現象 (修正前)
ruby# load_defaults < "8.1" のアプリで ActiveRecord.raise_on_missing_required_finder_order_columns # => nil修正後の意図
- 上記の値が常に
falseを返すようにし、Rails Guides の記載と整合させる。
- 上記の値が常に
- 影響範囲・注意点
影響範囲
- 主に 設定値のデフォルト に関する変更であり、実際の挙動にはほぼ影響しません。
- Ruby では
nilもfalseもいずれも falsy であるため、このフラグを単純に真偽値判定に使っている限り、nil→falseでコードの分岐結果は変わりません。
- Ruby では
load_defaults < 8.1で動いているアプリケーションに影響しうる変更ですが、通常の利用では実質的な動作は同じです。
- 主に 設定値のデフォルト に関する変更であり、実際の挙動にはほぼ影響しません。
注意点
- もしアプリ側で「
nilかどうか」を明示的に判定しているようなコードがあれば、今回の変更で挙動が変わる可能性があります(例:if config.raise_on_missing_required_finder_order_columns.nil?)。 - 正しい使い方としては、この設定を「真偽値のフラグ」として扱うことが想定されており、その意味では今回の修正はより一貫性のある状態に近づけるものといえます。
- 古いバージョンからのアップグレード時に、
raise_on_missing_required_finder_order_columnsの値をログに出していたり、メタ的に検査しているコードがある場合は、期待値をfalseに合わせる必要があります。
- もしアプリ側で「
- 参考情報 (あれば)
Rails Guides 該当箇所:
Configuring Rails Applicationsconfig.active_record.raise_on_missing_required_finder_order_columns
https://guides.rubyonrails.org/configuring.html#config-active-record-raise-on-missing-required-finder-order-columns関連 PR:
- コピー&ペーストミスの元になったとされる PR: https://github.com/rails/rails/pull/54608
#56943 Address test_validates_comparison_of_incomparables failure with Ruby 4.1.0dev
マージ日: 2026/3/7 | 作成者: @yahonda
- 概要 (1-2文で)
このPRは、Ruby 4.1.0dev でvalidates_comparison_of_incomparablesのテストが落ちる問題に対応し、Ruby 本体側でエラー文言が変わったことを反映するためにテスト期待値を調整したものです。Rails 本体の挙動ではなく、比較バリデーションに関するテスト側のみの修正です。
- 変更内容の詳細
何が起きていたか
Ruby 4.1.0dev で以下のテストが失敗していました:
ComparisonValidationTest#test_validates_comparison_of_incomparables- ファイル:
activemodel/test/cases/validations/comparison_validation_test.rb:294
比較不可能な型(Integer と String)を比較したときに発生するエラーメッセージが、Ruby 4.1.0dev で変更されたためです。
以前の Ruby(4.1.0dev 以前)では、例外メッセージは:
"comparison of Integer with String failed"でしたが、Ruby 4.1.0dev では:
"comparison of Integer with String failed: coercion was not possible"と末尾に : coercion was not possible が追加されるようになりました。
テストは 古い 文言 "comparison of Integer with String failed" を期待していたため、CI 上では diff が出て失敗していました。
何を変えたか
対象ファイル:
activemodel/test/cases/validations/comparison_validation_test.rb(+2/-2)
変更内容はこのテストケースの「期待されるエラーメッセージ」を、Ruby 4.1.0dev の新しいメッセージに合わせるというシンプルなものです。
具体的には、おそらく次のような変更が行われています(イメージ):
# 変更前(旧 Ruby 用の期待値)
assert_equal(
"comparison of Integer with String failed",
errors[:attribute].first
)
# 変更後(Ruby 4.1.0dev のメッセージに合わせる)
assert_equal(
"comparison of Integer with String failed: coercion was not possible",
errors[:attribute].first
)実際には 2 行の追加・2 行の削除だけなので、ほぼこの期待メッセージの差し替えに限定されます。
- 影響範囲・注意点
影響範囲
- Rails のランタイムコード(本番アプリに影響するコード)には変更がなく、Active Model のテストコードのみが対象です。
- Ruby 4.1.0dev を用いた Rails の CI や、手元で Ruby 4.1.0dev + Rails master をテストする環境で、該当テストが正常に通るようになります。
- Ruby のエラーメッセージ仕様変更に依存しているため、将来またメッセージが変われば再度テスト調整が必要になる可能性はあります。
注意点
- このテストは、「異種比較エラーが発生すること」 を検証するものですが、現在は エラーメッセージの完全一致 をチェックしています。そのため Ruby 側のメッセージ変更に非常に敏感です。
- Ruby のバージョン互換性を広く持たせたい場合は、将来的に
- メッセージの部分一致(
include?)にする - もしくは例外クラスや挙動のみをテストする といった方針変更も検討の余地がありますが、本 PR ではそこまでは行っていません。
- メッセージの部分一致(
- 現時点では Ruby 4.1.0dev の挙動に合わせる形で、Rails Nightly CI の安定性を優先した対応になっています。
- 参考情報 (あれば)
- Ruby 本体側の関連 PR: https://github.com/ruby/ruby/pull/16321
→IntegerとStringのような比較不可能なオブジェクト間比較で発生するArgumentErrorのメッセージに: coercion was not possibleが付与される変更が含まれています。 - CI 失敗ログ:
https://buildkite.com/rails/rails-nightly/builds/3843#019cc4f3-df5a-4f84-b26e-a4d5c3b64a11/L1474
#56927 Fix stale Rack::Sendfile reference in DebugLocks documentation
マージ日: 2026/3/5 | 作成者: @ashwin47
- 概要 (1-2文で)
DebugLocksミドルウェアの挿入位置としてドキュメントに書かれていたRack::Sendfileが、最近の変更によりミドルウェアスタックに存在しない場合があるため、常に存在するActionDispatch::Executorを基準にするようにドキュメントとコメントを修正した PR です。機能変更ではなく、ドキュメントと inline コメントの不整合を直すメンテナンス的修正です。
- 変更内容の詳細
背景
PR #56915 により、
x_sendfile_headerがnil(デフォルト)の場合はRack::Sendfileをミドルウェアスタックに追加しないようになった。その結果、従来ドキュメントに書かれていた以下のような設定:
rubyconfig.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocksを実行すると、
Rack::Sendfileがそもそもスタックにないため、textNo such middleware to insert before: Rack::Sendfileというエラーが発生するようになっていた。
修正内容
Rack::Sendfile を挿入ポイントとして推奨する記述を、ActionDispatch::Executor に変更しています。
actionpack/lib/action_dispatch/middleware/debug_locks.rbの inline ドキュメント修正もともとのコメント例:
ruby# config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocksを以下のように変更:
ruby# config.middleware.insert_before ActionDispatch::Executor, ActionDispatch::DebugLocksこれにより、
DebugLocksを挿入する際の公式なサンプルコードが、常に存在するミドルウェアを基準にしたものになる。Guides (
guides/source/threading_and_code_execution.md) の記述修正Threading ガイドにあるミドルウェア挿入例も同様に:
rubyconfig.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocksを
rubyconfig.middleware.insert_before ActionDispatch::Executor, ActionDispatch::DebugLocksに変更。
なぜ ActionDispatch::Executor なのか
ActionDispatch::Executorは、Rack ミドルウェアスタックの中で「無条件に最初に入る」ミドルウェアとして配置されている。DebugLocksはスレッドロックの問題をデバッグするためのミドルウェアであり、「できるだけ上流(スタックの先頭付近)」に入れたいという意図がある。Rack::Sendfileはもはや条件付き(x_sendfile_header設定時のみ)になってしまったため、常に存在するActionDispatch::Executorを基準にするのが合理的、という判断。
- 影響範囲・注意点
- 対象:
ActionDispatch::DebugLocksを利用していて、ドキュメント通りにRack::Sendfileを基準にinsert_beforeしているアプリケーション。
- 実運用上の影響:
- この PR 自体は コードの動作は一切変えておらず、コメントとガイドの修正のみ。
- ただし、既に
Rack::Sendfileを基準に設定しているアプリでは、現状の Rails だと前述のNo such middleware to insert beforeエラーが起きうる。
- 対応すべきこと:
config/application.rbや各環境設定でDebugLocksを使っている場合は、設定を以下のように変更する必要がある:ruby# 旧 (今後はエラーになる可能性が高い) config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocks # 新 (PR で推奨されている形) config.middleware.insert_before ActionDispatch::Executor, ActionDispatch::DebugLocksx_sendfile_headerを独自に設定していてRack::Sendfileが有効な場合でも、この変更に合わせてActionDispatch::Executorを基準にしておくと、設定が将来の変更に対しても頑健になる。
- 参考情報 (あれば)
- この PR に依存する変更:
- #56915:
x_sendfile_headerがnilの場合にRack::Sendfileを追加しないようにした変更。
- #56915:
- 関連コンポーネント:
ActionDispatch::DebugLocks: スレッドロック・デッドロックの検出支援用ミドルウェア。ActionDispatch::Executor: 各リクエスト毎にスレッド/コード実行コンテキストを管理するミドルウェア(スレッドセーフティやリロードに関わるコアミドルウェア)。
#56933 Fix missing comment marker in decrement_counter docs
マージ日: 2026/3/5 | 作成者: @eglitobias
概要 (1-2文で)
ActiveRecord::CounterCacheのdecrement_counterのドキュメント中にあったサンプルコードのコメント行から#が抜けていた問題を修正した PR です。機能変更は一切なく、ドキュメント内のコードスニペットの体裁と一貫性を整えただけの変更です。変更内容の詳細
対象: activerecord/lib/active_record/counter_cache.rb
decrement_counter の使用例にあるコメント付きサンプルコードのうち、1 行だけ Ruby のコメントマーカー # が抜けていたため、そこに # を追加しています。
イメージとしては、ドキュメント内の例が:
# Decrement the posts_count counter cache for the record with an id of 5
# in the authors table by one.
by a specific amount.のように、最後の行だけコメントになっておらず、Ruby コードとしては文法的におかしい状態になっていました。
これを次のように修正しています:
# Decrement the posts_count counter cache for the record with an id of 5
# in the authors table by one.
# by a specific amount.要するに、「by a specific amount.」という補足説明もちゃんとコメントとして扱われるようにしただけの修正です。
- 影響範囲・注意点
- 実行コード (
decrement_counterの挙動) には一切変更はありません。 - 影響があるのは、
counter_cache.rb内に書かれた Yard/RDoc 用コメントや、そこから生成される API ドキュメントのみです。 - 既存アプリケーション・Gem・テストコードなどへの影響はゼロと考えて問題ありません。
decrement_counterの使い方やシグネチャは変わっていないため、アップグレード時の互換性リスクもありません。
- 参考情報 (あれば)
- 対象メソッド:
ActiveRecord::CounterCache.decrement_counter
一般的な利用例:ruby# 1 だけ減らす Author.decrement_counter(:posts_count, author_id) # 特定の値だけ減らす Author.decrement_counter(:posts_count, author_id, by: 3) - PR: https://github.com/rails/rails/pull/56933
#56911 array_position requires PostgreSQL 9.5
マージ日: 2026/3/4 | 作成者: @toy
- 概要 (1-2文で)
このPRは、Active Record が内部で利用している PostgreSQL 関数array_positionが PostgreSQL 9.5 以降でしか使えないことを踏まえ、PostgreSQL アダプタのバージョンチェックとドキュメントを「9.4 まで」から「9.5 まで」に更新するものです。実質的には「Rails の PostgreSQL サポート下限が 9.5 である」ことをコードとガイドに明示した調整です。
- 変更内容の詳細(あればサンプルコードも含めて)
背景
- 以前のコミット(c93d1b09fcc0…, PR #55654)で、Active Record の PostgreSQL アダプタ内で
array_positionを使う実装が追加された。 array_positionは PostgreSQL 9.5 で導入された関数のため、PostgreSQL 9.4 以下では存在せず、クエリ実行時にエラーになる可能性がある。- それにもかかわらず、Rails 側の「サポート対象 PostgreSQL バージョン」の記述やバージョンチェックの条件が古いまま(9.4 前提)になっていた部分があった。
このPRでは、それらの整合性を取るために、「最低サポートバージョンは 9.5」という前提に統一しています。
ファイルごとの変更ポイント
1) activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
PostgreSQL のバージョンチェックを行っている箇所で、しきい値を 9.4 → 9.5 に引き上げた変更が行われています。
具体的には、内部で
postgresql_versionなどを使ってrubyif postgresql_version < 90400 # 9.4 未満に対する警告やサポート外処理 endのように書かれていたものを、
rubyif postgresql_version < 90500 # 9.5 未満に対する警告やサポート外処理 endといった形に変更したイメージです(実際の定数値・条件はPRの差分そのものですが、役割としては「9.5 未満を非サポート扱いにする」変更です)。
これにより、Rails が PostgreSQL バージョンを判定するときに「9.4 まではOK」という扱いがなくなり、「9.5 未満はサポート外」となります。
2) guides/source/active_record_postgresql.md
Active Record と PostgreSQL の組み合わせについて説明しているガイド内の文言を、サポート対象バージョンを 9.4 → 9.5 に変更。
例(イメージ):
- 変更前: 「Rails は PostgreSQL 9.4 以降をサポートします」
- 変更後: 「Rails は PostgreSQL 9.5 以降をサポートします」
特に
array_positionのような 9.5 以降でしか使えない機能が利用されていることと整合する説明になっています。
3) guides/source/command_line.md
rails newコマンドなど、CLI 周りの説明の中で PostgreSQL を選択した場合の要件や説明に含まれている PostgreSQL バージョンの記述を 9.4 → 9.5 に更新。- ここもユーザー向けに「この Rails を使うときの PostgreSQL の最低バージョン」を明示する役割があります。
4) railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt
rails new myapp -d postgresqlなどで生成されるconfig/database.ymlテンプレートの PostgreSQL 用雛形内のコメントや説明文に含まれるバージョン表記を 9.4 → 9.5 に修正。- アプリ新規作成時に生成される
database.ymlのコメントを読むと、「このアプリは PostgreSQL 9.5 以降を対象にしている」ことが分かるようになります。
- 影響範囲・注意点
- PostgreSQL 9.4 以下を使っている Rails アプリケーションは、実質的にサポート外になります。
- 既に
array_positionを利用するコードが入っていた時点で、実運用上は 9.4 以下ではクエリエラーが出うる状態でしたが、このPRで「Rails 公式として 9.5 以上前提」ということがより明確になります。
- 既に
- PostgreSQL 9.5 自体も EOL になっているため、実際の運用では 12 以降など、より新しいバージョンに上げることが推奨されますが、少なくとも Rails 的には 9.5 未満はサポートしないという位置づけになります。
- 新規に Rails アプリを作る際(
rails new)に生成される設定や、ガイドの要件を見て環境を整えるユーザーにとって、「9.4 で始めてしまう」ミスを防止できるようになります。 - このPR自体はバグ修正・機能追加ではなく「バージョン要件/ドキュメントの明示・整合性取り」が主目的のため、アプリケーションコード側の変更は不要ですが、利用しているDBサーバーのバージョン確認は必須です。
- 参考情報 (あれば)
PostgreSQL 9.5 リリースノート(
array_position追加の記載あり)
https://www.postgresql.org/docs/release/9.5.0/#AEN132063array_position関数ドキュメント(PostgreSQL 公式)
https://www.postgresql.org/docs/current/functions-array.html
(バージョンを切り替えて 9.4 と 9.5 のドキュメントを比較すると、9.5 から追加されたことが確認できます)array_positionを利用したクエリ例(Active Record ではArel/SQLフラグメント経由で使用される可能性があります):sqlSELECT array_position(ARRAY[1, 2, 3], 2); -- => 2
このPRは、こうした関数を前提にした Active Record の実装と、Rails が公式に掲げる PostgreSQL のサポートバージョンを一致させるためのメンテナンス的な変更です。
#56879 Fixed guard around AS::TC.run_order
マージ日: 2026/3/3 | 作成者: @zenspider
- 概要 (1-2文で)
ActiveSupport::TestCase のrun_order周りのガード条件が誤っていた/不十分だったのを 1行の修正で正した PR です。テストの実行順序設定に関する条件分岐が、想定通りのケースでのみ動作するようになります。
- 変更内容の詳細
※ PR 本文と統計から読み取れる範囲の解説であり、実際の 1行差分は要旨ベースです。
- 対象ファイル:
activesupport/lib/active_support/test_case.rb - 変更: 1行の条件式(ガード)を修正(+1 / -1)
ActiveSupport::TestCase には、Minitest との連携でテストの実行順を制御するための run_order 設定があります。
この PR は、ActiveSupport::TestCase.run_order を触る際の「条件付きで設定・上書きするための guard(if 条件など)」が誤っていたため、それを正しい条件に修正したものです。
Ruby のテスト周りでよくあるパターンとしては、例えば以下のようなガードです:
if defined?(Minitest::Test) && Minitest::Test.respond_to?(:run_one_method)
# run_order を設定する / 書き換える
end今回の PR は、
- 「定義の有無のチェックが間違っている」
- 「クラス/モジュール名を取り違えている」
- 「
respond_to?で見るべきメソッド名を誤っている」 - 「truthy / falsy 判定の条件を逆にしている」
といったミスを、1行で修正したものと考えられます。
その結果、ActiveSupport::TestCase.run_order まわりのコードが、
- 想定している環境でのみ有効になる
- 想定していない環境では安全にスキップされる
という、本来意図していた振る舞いに揃えられています。
- 影響範囲・注意点
影響範囲
- ActiveSupport::TestCase を使用しているテストスイートで、
run_orderの設定・挙動に関わる部分(例: テストの実行順序を:randomや:sortedなどに設定している場合)。 - 特に、Minitest のバージョンや API に依存した条件付きコードが存在する場合、その条件が「正しく効く」ようになるため、これまで偶然動いていたコードがより厳密な挙動になる可能性があります。
- ActiveSupport::TestCase を使用しているテストスイートで、
想定される変化
- ある特定の Minitest バージョン・環境で、以前は guard が効かずに
run_orderが設定されてしまっていた/逆に設定されていなかった、という状態が修正されます。 - それに伴い、テストの実行順(ランダム化の有無など)が、Rails が意図しているデフォルトの挙動に揃えられる可能性があります。
- ある特定の Minitest バージョン・環境で、以前は guard が効かずに
開発者が確認すべき点
- Rails を更新した際に、テスト実行順が変わったと感じた場合、この PR の影響で guard が正しく機能し始めた可能性があります。
- 独自に
ActiveSupport::TestCase.run_orderを変更している、あるいは Minitest との統合部分を monkey patch している場合は、そのコードが新しい guard 条件と矛盾していないか確認してください。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/56879
- Minitest と ActiveSupport::TestCase の run_order 周り:
ActiveSupport::TestCaseは内部的に Minitest のランナーと連携しており、run_orderによってテスト順序の制御(例: ランダム化)が行われます。
- 実際の差分(1行)の内容は PR 本体を参照すると、どの条件式がどう修正されたかが明確に確認できます。
#56906 Fix Encoding::CompatibilityError with non-ASCII strict locals defaults
マージ日: 2026/3/2 | 作成者: @kataokatsuki
- 概要 (1–2文で)
strict_localsを使った ERB テンプレートで、ローカル変数のデフォルト値に非 ASCII 文字(例:"café")を含めるとEncoding::CompatibilityErrorが発生する問題を修正した PRです。File.binreadにより ASCII-8BIT で読み込まれた locals 宣言のエンコーディングを、テンプレート本体と同じ外部エンコーディング(通常 UTF-8)に「タグ付けし直す」ことでエラーを防いでいます。
- 変更内容の詳細
問題の背景
テンプレートは
File.binreadで読み込まれており、初期状態のエンコーディングはASCII-8BIT(バイナリ)。Action View の
strict_locals!は、テンプレート先頭のコメントに書かれた locals 宣言を正規表現で抜き出します。例:erb<%# locals: (label: "café") -%> Hello <%= label %>ここで
$1にマッチした部分 ("label: \"café\""など) を@strict_localsに格納しますが、この時点ではまだencode!が走っていないため、@strict_localsのエンコーディングはASCII-8BITのままです。その後、
encode!によってテンプレート本文はEncoding.default_external(通常は UTF-8)に変換・タグ付けされますが、locals 部分だけは ASCII-8BIT のまま残る。最終的な
compiled_sourceは「UTF-8 な ERB の出力」と「ASCII-8BIT な@strict_localsを含む文字列」を同じ Ruby 文字列の中で補間して組み立てます。- 両方とも 0x7F を超えるバイトを含んでいると、Ruby のエンコーディング交渉で
Encoding::CompatibilityErrorが発生。 - 偶然 ASCII の範囲しか含まない場合でも、compiled_source の encoding が不正に ASCII-8BIT になる可能性がある。
- 両方とも 0x7F を超えるバイトを含んでいると、Ruby のエンコーディング交渉で
修正内容
actionview/lib/action_view/template.rb の strict_locals! メソッド内で、locals 宣言を抽出したあとに以下のような処理を追加しています(概念的なイメージ):
def strict_locals!
# 正規表現で locals 宣言を取り出して @strict_locals に格納
if @source.sub!(STRICT_LOCALS_PATTERN, "")
@strict_locals = $1
# ★ ここでエンコーディングの「ラベルだけ」を外部エンコーディングに揃える
@strict_locals.force_encoding(Encoding.default_external)
end
endポイント:
encodeではなくforce_encodingを使っているため、「バイト列は一切変えず、エンコーディングタグだけを変える」挙動です。- これは、すでにテンプレート本体に対して
encode!が行っている処理(外部エンコーディングへの re-tagging)と同種の対応で、locals の扱いもそれに揃えています。
テスト追加
actionview/test/template/template_test.rb にテストが追加されています。内容はおおよそ次のようなシナリオです(擬似コード):
def test_strict_locals_with_non_ascii_default_does_not_raise
template = <<~ERB
<%# locals: (label: "café") -%>
<%= label %>
ERB
# strict_locals オプション付きでテンプレートをレンダリングしても
# Encoding::CompatibilityError が発生しないこと
assert_nothing_raised do
render_with_strict_locals(template)
end
endこれにより、非 ASCII 文字を含む strict locals のデフォルト値を使った場合でも、コンパイルおよびレンダリングが正常に動くことを確認しています。
- 影響範囲・注意点
- 影響範囲:
ActionView::Templateにおいて、strict_locals機能を利用し、かつ locals 宣言内に非 ASCII 文字(日本語、アクセント付きラテン文字など)を含むデフォルト値を記述しているテンプレート。- 上記のケースで発生していた
Encoding::CompatibilityErrorが解消されます。
- 互換性:
force_encodingによる「ラベルのみ変更」であり、バイト列自体は変更していないため、動作上の後方互換性への影響は極めて小さいと考えられます。- これまで「たまたまエラーになっていなかったが compiled_source の encoding が ASCII-8BIT になっていた」ケースも、正しく UTF-8(等、default_external)として扱われるようになります。
- 注意点:
- Rails の
Encoding.default_externalが通常は UTF-8 を想定しているため、この挙動に依存している環境(たとえば default_external を変更している特殊な環境)では、locals 宣言もそのエンコーディングで解釈される点を理解しておく必要があります。 - とはいえ、もともとテンプレート本文も同じ default_external に揃えられているため、locals 部分だけが異なるエンコーディングを持つよりは、今回の変更の方が一貫性があります。
- Rails の
- 参考情報 (あれば)
- 関連 Issue: #56904
「strict locals のデフォルト値に非 ASCII 文字を含めるとEncoding::CompatibilityErrorが出る」ことを報告している Issue。 - 関連コード:
ActionView::Template#strict_locals!ActionView::Template#encode!
- Ruby のエンコーディング交渉:
- Ruby では文字列同士を結合・補間する際、両者の Encoding が互換でない場合に
Encoding::CompatibilityErrorが発生します。 ASCII-8BITと UTF-8 のように、「実際のバイト列に 0x7F を超える値を含む」組み合わせは典型的な衝突パターンです。
- Ruby では文字列同士を結合・補間する際、両者の Encoding が互換でない場合に
#56913 Set read-only permissions for GitHub Actions workflow generated by rails new
マージ日: 2026/3/2 | 作成者: @taketo1113
- 概要 (1-2文で)
rails new(および plugin generator)が生成する GitHub Actions の CI ワークフローに、permissionsを明示的に追加し、GITHUB_TOKENの権限を「contents: read」の読み取り専用に固定する変更です。これにより、組織側のデフォルト設定に依存せず、最小限の権限で CI が動くようになります。
- 変更内容の詳細
何をしたか
Rails のジェネレータが吐き出す GitHub Actions ワークフローテンプレート(ci.yml.tt)に、以下のような permissions 設定が追加されました。
対象テンプレート:
railties/lib/rails/generators/rails/app/templates/github/ci.yml.ttrailties/lib/rails/generators/rails/plugin/templates/github/ci.yml.tt
追加された設定(イメージ・サンプル):
name: CI
on:
push:
branches:
- main
pull_request:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# ...
# System test での artifact アップロードなど
- uses: actions/upload-artifact@v4
with:
name: test-results
path: tmp/test-resultsポイント:
permissions: contents: readをトップレベル(jobs:と同じ階層)に追加。- これにより
GITHUB_TOKENの権限がリポジトリコンテンツの読み取り専用に制限される。 - PR 説明によると、
actions/upload-artifactを使う System Test もこの権限で問題なく動作することが確認済み。
なぜやったか(背景)
- GitHub Actions では
permissionsを明示しない場合、GitHub Organization レベルで設定されたデフォルト権限をそのまま継承します。 - 組織のデフォルトが「書き込み可能」の場合、
rails newが生成したワークフローも、不要にGITHUB_TOKENが書き込み権限を持って実行されてしまう可能性があります。 - Rails がデフォルトで生成する CI は基本的に「コードのチェック・テスト実行」が主目的であり、リポジトリへの変更などの書き込み権限は不要です。
- 最小権限の原則(least privilege)の観点から、ワークフロー側で明示的に read-only に固定することで、安全なデフォルトを提供します。
また、この振る舞いの変更については railties/CHANGELOG.md にも追記されています。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは この PR マージ以降の Rails バージョンで
rails newまたはrails plugin newを実行したときに自動生成される GitHub Actions CI ワークフロー です。 - 既存のリポジトリに既に生成済みの
ci.ymlには自動では反映されないため、必要であれば手動で追随させる必要があります。
注意点・確認しておくとよいこと
- 現状の Rails 標準 CI ワークフローが行うのは
- コードチェック・テスト実行
actions/upload-artifactによる成果物アップロード などであり、contents: readで問題ないことが確認されています。
- もしプロジェクト側で、CI ワークフローから以下のような「書き込み」を行っている場合は注意してください:
- GitHub Releases の作成
- タグやブランチの push
- Issue / PR への自動コメント、ラベル付け、マージ操作
- ファイルの自動更新(例:
actions/checkout+git commit+git push)
- その場合は、対象ワークフローで個別に
permissionsを上書きする必要があります。例:
permissions:
contents: write
pull-requests: writeもしくはジョブ単位で指定することも可能です。
- 参考情報 (あれば)
- GitHub 公式ドキュメント:
Authenticate withGITHUB_TOKEN
https://docs.github.com/en/actions/tutorials/authenticate-with-github_token - GitHub Actions の
permissions詳説:
https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs - Rails 側変更ファイル:
railties/CHANGELOG.mdrailties/lib/rails/generators/rails/app/templates/github/ci.yml.ttrailties/lib/rails/generators/rails/plugin/templates/github/ci.yml.tt
#56915 Don't add Rack::Sendfile to the stack if x_sendfile_header is nil
マージ日: 2026/3/2 | 作成者: @byroot
- 概要 (1-2文で)
Rails のデフォルトミドルウェアスタックにおいて、config.action_dispatch.x_sendfile_headerがnilの場合はRack::Sendfileを積まないように変更された PR です。x_sendfile_headerが未設定ならRack::Sendfileは実質 no-op なので、その無駄を省いています。
- 変更内容の詳細
2-1. 何をしているか
これまでの挙動:
config.action_dispatch.x_sendfile_headerがnilでも、Rack::Sendfileミドルウェアは常にミドルウェアスタックに含まれていた。- ただし
x_sendfile_headerがnilの場合、Rack::Sendfileはレスポンスを書き換えず「何もしない (noop)」挙動になる。
今回の変更:
x_sendfile_headerがnilのときは、そもそもRack::Sendfileをスタックに追加しない。- 結果的に、不要なミドルウェアを 1 つ減らし、スタックがわずかにシンプルかつ軽量になる。
2-2. コードレベルのイメージ
※ 実際のコードとは多少異なる可能性がありますが、意図としては以下のような変更です:
# 変更前のイメージ
if app.config.action_dispatch.x_sendfile_header
# ヘッダ指定あり → Rack::Sendfile を追加
middleware.use ::Rack::Sendfile, app.config.action_dispatch.x_sendfile_header
else
# ヘッダ指定なしでも Rack::Sendfile は入っていた(no-op)
middleware.use ::Rack::Sendfile
end# 変更後のイメージ
if app.config.action_dispatch.x_sendfile_header
# ヘッダ指定があるときだけ Rack::Sendfile を追加
middleware.use ::Rack::Sendfile, app.config.action_dispatch.x_sendfile_header
end
# nil の場合は一切追加しない2-3. テスト・CHANGELOG
railties/test/commands/middleware_test.rbでは:x_sendfile_headerが設定されていない場合にミドルウェア一覧にRack::Sendfileが含まれないことを確認するテストが追加・修正されている。x_sendfile_headerを設定した場合にのみRack::Sendfileが含まれることもテストされていると考えられます。
railties/CHANGELOG.md:- 「
x_sendfile_headerがnilの場合は Rack::Sendfile をスタックに追加しない」という変更点が追記され、アップグレード時に気づけるようになっています。
- 「
- 影響範囲・注意点
3-1. 実務上の影響
デフォルト設定 (
x_sendfile_header = nil) のアプリ- これまで: ミドルウェアスタックに
Rack::Sendfileが含まれていたが、実質何もしていなかった。 - 今後: そもそも
Rack::Sendfileがスタックに現れなくなる。 - 挙動としては元々 noop だったため、レスポンスや機能面の違いはほぼありません。
- ただし、ミドルウェア一覧を検査しているテストやツールがある場合、
Rack::Sendfileが消えることによる「見た目」の変化に注意が必要です。
- これまで: ミドルウェアスタックに
x_sendfile_headerを明示的に設定しているアプリ- 例: Apache +
X-Sendfile, Nginx +X-Accel-Redirectなどを使う場合 - この場合は従来通り
Rack::Sendfileがスタックに入り、挙動も変わりません。 - つまり、静的ファイルをフロントサーバーにオフロードする既存の構成に影響はありません。
- 例: Apache +
3-2. メトリクス・ミドルウェア依存コード
- Rack ミドルウェアのリストをもとに何か処理しているコード(独自の introspection、ドキュメント生成、デバッグ用出力など)がある場合、
Rack::Sendfileが「消えた」ことに依存関係がないかだけ確認すると安心です。 - 性能面では、
Rack::Sendfile分の 1 レイヤーがなくなるため、非常にわずかに軽くなりますが、通常は体感できるレベルではありません。
- 参考情報 (あれば)
config.action_dispatch.x_sendfile_headerの一般的な使い方:- Apache + mod_xsendfile など:ruby
# config/environments/production.rb config.action_dispatch.x_sendfile_header = 'X-Sendfile' - Nginx (X-Accel-Redirect):ruby
config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
- Apache + mod_xsendfile など:
Rack::Sendfileの役割:- レスポンスボディに大きなファイルがある場合、アプリケーションから直接送らず、
X-Sendfileなどのヘッダをフロントサーバに付与し、実際のファイル転送を Web サーバー側に任せるための Rack ミドルウェアです。 - 今回の変更は、「そのヘッダをそもそも使わない設定なら Rack::Sendfile も不要だよね」という整理になります。
- レスポンスボディに大きなファイルがある場合、アプリケーションから直接送らず、
#56903 Fix flaky test: sql.active_record event count in transaction instrumentation tests
マージ日: 2026/3/1 | 作成者: @flavorjones
- 概要 (1-2文で)
トランザクション計測用テストで、sql.active_recordのイベント件数を厳密にカウントしている箇所が、スキーマキャッシュの状態に依存して CI でフレ flaky になる問題を修正した PRです。テスト開始前にTopic関連のスキーマ情報をウォームアップしておくことで、想定外の SCHEMA クエリがイベント数に混入しないようにしています。
- 変更内容の詳細
対象テスト:
TransactionInstrumentationTest#test_sql_events_do_not_overlap_with_savepointsTransactionInstrumentationTest#test_sql_events_do_not_overlap
これらのテストは、グローバルに sql.active_record を subscribe し、発行された SQL イベントの件数を「ぴったり N 件」とアサートしていました。ところが、直前の別テストが Topic.reset_column_information を呼び出して Topic のスキーマキャッシュをクリアしている場合、Topic.first の実行時にスキーマ関連のクエリが「遅延評価」で走り、その SQL イベントがカウントに含まれてしまい、期待件数 +1 となって失敗していました。
問題となる呼び出しパスは以下のようなものです:
Topic.first
→ instantiate
→ init_internals
→ define_attribute_methods
→ attribute_names
→ table_exists?
→ schema_cache.data_source_exists?schema_cache.data_source_exists? が DB に対してスキーマ問い合わせを投げます(例:
- MySQL:
SHOW FULL FIELDS FROM "topics" - SQLite:
PRAGMA table_xinfo('topics')
など)。これがsql.active_recordの「SCHEMA」イベントとして発火し、グローバルサブスクライバが拾ってしまう、という構図です。
修正内容:
各テストの「計測対象となるブロックの前」に、Topic 関連のキャッシュを温めるための Topic.first を追加しています。これにより、以下のような状態になります。
- 事前に
Topic.firstを呼んでおく- カラム情報 / data source 存在確認 / attribute methods 定義 など、遅延ロードされるスキーマ関連処理がこのタイミングで済む
- ここではまだ
sql.active_recordを subscribe していないので、SCHEMA クエリはカウント対象外
- その後、
sql.active_recordを subscribe してテスト本体を実行- トランザクション・セーブポイント関連で発行される SQL のみがイベントとしてカウントされるようになる
イメージ的には以下のような形になっています(実際のコードは行数・メソッド名等が異なる可能性がありますが、意図としてはこれに近いです):
def test_sql_events_do_not_overlap_with_savepoints
# 追加されたウォームアップ
Topic.first
events = []
ActiveSupport::Notifications.subscribed(
->(*args) { events << args },
"sql.active_record"
) do
# テスト対象のトランザクション処理
Topic.transaction do
# ...
end
end
assert_equal 6, events.size
endこの「テストの計測区間の外でキャッシュをウォームアップする」ことで、キャッシュ状態によるイベント数のぶれがなくなり、flakyさを解消しています。
- 影響範囲・注意点
- 影響範囲
- 変更はテストコード (
activerecord/test/cases/transaction_instrumentation_test.rb) のみで、本番コードには影響ありません。 - CI でたまに落ちていた
TransactionInstrumentationTestの2つのテストの安定性が向上します。
- 変更はテストコード (
- 注意点 / 学び
sql.active_recordのようなグローバルなイベントを「件数で厳密に」アサートするテストは、スキーマキャッシュや他のテストが発行するクエリの影響を受けやすく、flaky になりやすいです。- Rails では
Model.reset_column_informationが多数のテストで使われており(PR説明によると ~200 箇所)、これがスキーマキャッシュを cold にしてしまうため、「テスト実行順序に依存した不安定さ」が生じます。 - 同様のテストを書く場合は:
- 計測区間の前にキャッシュをウォームアップする
- あるいは SCHEMA イベントをフィルタする / トランザクション関連の SQL だけを条件で絞る
- そもそも「件数をぴったり固定値でアサートする」のではなく、下限・上限や特定のパターンのみを確認する といった工夫が必要になることが分かります。
- 参考情報 (あれば)
- PRで例示されている再現手順(修正前):bash
cd activerecord bin/test test/cases/persistence_test.rb:804 test/cases/transaction_instrumentation_test.rb:393persistence_test側のensureでTopic.reset_column_informationが走り、その直後にtransaction_instrumentation_testが実行されることで、スキーマキャッシュがクリアされた「悪い順序」が再現されます。 - スキーマキャッシュへの依存や lazy load の副作用が、Instrument/Notification ベースのテストで問題になり得る、という典型例としても参考になります。
#56902 Fix IsolatedExecutionState.share_with() call in AC::Live
マージ日: 2026/3/1 | 作成者: @tavianator
- 概要 (1-2文で)
ActionController::Live がisolation_level = :fiberのときに誤ったコンテキストをIsolatedExecutionState.share_withに渡していたバグを修正し、正しい「元のコンテキスト」を共有するようにした PR です。これにより、Fiber ベースの並行実行時でも IsolatedExecutionState(スレッドローカルや CurrentAttributes 等を含む実行コンテキスト)が正しく引き継がれます。
- 変更内容の詳細
問題点
- Rails では ActiveSupport::IsolatedExecutionState を使って、
- CurrentAttributes
- executor の state
- その他の実行コンテキスト
などを “isolated な状態” として管理しています。
- その state は
IsolatedExecutionState.share_with(context)で、ある実行単位(スレッド / Fiber など)から別の実行単位に「共有」できます。 isolation_level = :fiberの場合、本来は「元の IsolatedExecutionState コンテキスト」をshare_withに渡す必要がありますが、ActionController::Live では誤って「元のスレッド」を渡していました。
結果として、Fiber 隔離モードの環境では:
- Live streaming 用に起動したスレッド/Fiber 上で、期待どおりに
- Current.xxx
- request ローカルな状態
が引き継がれない、もしくは誤った状態になる、というバグが発生しうる状況でした。
修正内容
ActionController::Live 内での Live レスポンススレッド起動部分の IsolatedExecutionState 連携コードが修正されています。
概念的には、以下のような修正です(擬似コード):
# 修正前イメージ(誤り)
original_thread = Thread.current
Thread.new do
# isolation_level = :fiber のときに thread を share_with に渡していた
ActiveSupport::IsolatedExecutionState.share_with(original_thread) do
# live ストリーミングの処理
end
end
# 修正後イメージ(正しい)
original_context = ActiveSupport::IsolatedExecutionState.context
Thread.new do
# 保存しておいた “コンテキスト” を share_with に渡す
ActiveSupport::IsolatedExecutionState.share_with(original_context) do
# live ストリーミングの処理
end
endポイント:
- 修正前: 「元スレッド (
Thread)」をshare_withに渡していた - 修正後: 「元コンテキスト (
IsolatedExecutionState.context)」を事前に取得して渡すようにした
isolation_level = :thread の場合は「スレッドごとに別の state」を持つのが自然ですが、:fiber の場合は「Fiber ごとに state を分離する」ので、Thread オブジェクトそのものを基準にするのは誤り、というのが本質です。
テスト追加
actionpack/test/controller/live_stream_test.rb にテストが追加されています (+20 行)。
おおまかに以下を検証していると考えられます(実際のコード構造ベースの推測):
isolation_level = :fiber環境を設定- Live コントローラアクション内で、CurrentAttributes などの値をセット
- streaming 用スレッド/Fiber 内でも同じ値が見えること
- あるいは、「もとの context が正しく共有される」ことを明示的にテスト
これにより、今回の修正が回 regress しないように自動テストがカバーされています。
- 影響範囲・注意点
- 影響を受けるのは:
ActionController::Liveを利用しているアプリケーション- かつ ActiveSupport::IsolatedExecutionState の
isolation_level = :fiberを利用している構成
- この組み合わせで、以下のような症状が改善される可能性があります:
- Live streaming 中に CurrentAttributes(例:
Current.request_idなど)が nil / 不正な値になる - executor / instrumentation の state が streaming 中だけおかしくなる
- Live streaming 中に CurrentAttributes(例:
- 修正はあくまで「正しいコンテキストを渡す」バグフィックスであり、API 仕様や表向きの挙動を変更するものではありません。そのため:
- 基本的には後方互換な修正
- 既に「誤った挙動」に依存してしまっていたコードがあれば、挙動が変わる可能性はありますが、そのような依存は通常想定されません
- CHANGELOG は更新されていないため、「マイナーなバグ修正」と位置づけられていますが、Fiber 隔離モードと Live を組み合わせている環境では実害のある不具合修正になり得ます。
- 参考情報 (あれば)
関連概念:
ActiveSupport::IsolatedExecutionState- Rails の “実行コンテキスト隔離” の仕組みで、スレッド / Fiber ごとに独立した state を管理する。
isolation_level = :fiber- マルチスレッド下でさらに Fiber 単位で state を分離するための設定。
ActionController::Live- Rack hijack を使ったストリーミングレスポンス向けのモジュール。レスポンス処理を別スレッドで実行するため、IsolatedExecutionState の共有が重要になる。
この PR により、「リクエストコンテキストを Live ストリーミング用スレッド / Fiber にも正しく引き継ぐ」という Rails の設計意図にコードが一致するようになっています。