Ruby on Rails PR Digest - 2026年 7月
このページは rails/rails リポジトリにマージされたPull Requestを自動的に収集し、AIで要約したものです。
#58311 Fix connection.explain(arel) for Arel input with AST binds
マージ日: 2026/7/30 | 作成者: @kamipo
- 概要 (1-2文で)
connection.explain(arel)に Arel(AST binds 付き)を渡した場合に、バインド変数が無視されてEXPLAINが失敗していた問題を修正した PR です。to_sqlではなくto_sql_and_bindsを使うことで、EXPLAIN 実行時にプレースホルダへ正しくバインド値が渡されるようになりました。
- 変更内容の詳細
何が問題だったか
Arel オブジェクトを connection.explain(arel) に渡すと、内部的に次のような流れで処理されていました:
- Arel を
to_sqlで SQL 文字列にコンパイル - 生成された SQL を
EXPLAIN ...して実行
しかし to_sql は「抽出したバインド値(binds)」を返さず、SQL 文字列だけを返します。そのため、PostgreSQL / SQLite3 / MySQL の explain メソッドの実装では、以下のような「プレースホルダだけあるが値のない」状態で EXPLAIN を実行していました:
EXPLAIN SELECT "cars".* FROM "cars" WHERE "cars"."name" = $1
-- しかし $1 に対応する bind が無い結果として PostgreSQL などでは:
ActiveRecord::StatementInvalid: PG::UndefinedParameter: ERROR: there is no parameter $1のようなエラーが発生していました。
何を変えたか
PostgreSQL / SQLite3 / MySQL の各アダプタの explain 実装で、Arel を扱う部分を to_sql から to_sql_and_binds に変更し、抽出したバインド値を select_all に渡すように修正しています。
概念的には、次のような変更です(実際のコードはアダプタごとに少しずつ違いますが、意図は共通):
修正前(イメージ):
def explain(arel, binds = [])
sql = to_sql(arel) # SQL だけ生成、binds は捨てられる
sql = "EXPLAIN #{sql}"
select_all(sql) # binds を渡さずに実行
end修正後(イメージ):
def explain(arel, binds = [])
sql, arel_binds = to_sql_and_binds(arel) # SQL と binds を両方取得
all_binds = binds + arel_binds # 既存の binds とマージ(実装依存)
sql = "EXPLAIN #{sql}"
select_all(sql, nil, all_binds) # binds を渡して実行
endこれにより、Arel 内で管理されている AST のバインド変数(AST binds)が EXPLAIN 実行時にも正しく利用されます。
テストの追加
activerecord/test/cases/explain_test.rb にテストが 6 行追加されています。
内容としては、「AST binds を持つ Arel を explain したときにエラーにならず、正しく EXPLAIN が実行できること」を検証するテストが追加されていると考えられます(行数からすると 1 ケース程度の小さな追加)。
- 影響範囲・注意点
影響対象:
- Arel オブジェクトを直接
connection.explain(arel)に渡しているコード - 特に、
where句などにバインド変数(プレースホルダ)を含んだ Arel を EXPLAIN したいケース
- Arel オブジェクトを直接
対象 DB:
- PostgreSQL, SQLite3, MySQL の ActiveRecord アダプタ(
database_statements.rbへの変更)
- PostgreSQL, SQLite3, MySQL の ActiveRecord アダプタ(
影響を受けないもの:
- 生の SQL 文字列を
connection.explain("SELECT ...")で渡しているコード- これは「現在通り
exec_explain経由のパス」であり、バインド処理がもともと正しく行われているため、挙動は変わりません。
- これは「現在通り
- 生の SQL 文字列を
実務上の意味:
- 「Arel ベースでクエリを組み立て、
explainで実行計画を見る」といったユースケースで、バインド付き条件を含めてもエラーにならず、実際のクエリと同じ形で EXPLAIN を確認できるようになりました。 - これまでは、PostgreSQL 等で「parameter $1 が無い」というエラーに悩まされていたケースが解消されます。
- 「Arel ベースでクエリを組み立て、
- 参考情報 (あれば)
- 関連 API:
ActiveRecord::Relation#explainActiveRecord::ConnectionAdapters::DatabaseStatements#explainActiveRecord::ConnectionAdapters::DatabaseStatements#to_sql_and_binds
- この PR の趣旨は、Rails 内部の「Arel → SQL + binds」変換を
EXPLAIN実行にも一貫して適用し、Arel ベースのクエリデバッグ・チューニングをしやすくすることです。
#58310 Deprecate passing binds to to_sql
マージ日: 2026/7/30 | 作成者: @kamipo
- 概要 (1-2文で)
Rails 5.2 以降、SQL バインド値は Arel AST 側で管理されており、to_sqlにbinds引数を渡しても実質使われていませんでした。この PR ではto_sql(binds)の利用を非推奨 (deprecate) にし、将来削除するための警告を追加しています。
- 変更内容の詳細
背景
- Rails 5.2 で
to_sql_and_bindsの実装が変わり、バインド値は AST (Arel::Nodes) に保持されるようになったため、to_sql/to_sql_and_bindsは外部から渡されたbindsを SQL 文字列の構築には使わなくなっています。 - それにもかかわらず、
to_sqlメソッドシグネチャにはbinds引数が残っており、歴史的経緯からドキュメント上は「公開 API」とみなされているため、即削除ではなく非推奨プロセスを踏む形になりました。
主なコード変更ポイント
to_sqlのbinds引数を非推奨に
activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb にて、to_sql の引数 binds を使った呼び出しに deprecation 警告が出るようになっています。
概念的には以下のようなイメージです(実際のコードはもう少し汎用的です):
def to_sql(arel_or_sql_string, binds = [])
if binds.present?
ActiveSupport::Deprecation.warn(
"Passing `binds` to `to_sql` is deprecated and will be removed in a future version. " \
"Bind parameters now live on the Arel AST."
)
end
# 実際の SQL 生成は binds ではなく arel_or_sql_string 側の情報を使う
...
end- 各アダプタ (
mysql,postgresql,sqlite3) のto_sqlシグネチャ修正
mysql/database_statements.rbpostgresql/database_statements.rbsqlite3/database_statements.rb
で、それぞれのアダプタ固有の to_sql 実装も、抽象クラスのシグネチャ変更に揃える形で軽微な修正が入っています(binds を受け取るが実際には利用しない・警告だけ出す、など)。
- テスト追加
activerecord/test/cases/database_statements_test.rb に、to_sql に binds を渡したときに deprecation が発生することを検証するテストが追加されています。
- CHANGELOG 追記
activerecord/CHANGELOG.md に、本変更(to_sql に binds を渡すのは非推奨)のエントリが追加されています。
- 影響範囲・注意点
影響を受けるコード
- 以下のようなコードを書いている場合に影響があります:
# 例: 既存のコード
arel = User.where(name: :name).arel
sql = ActiveRecord::Base.connection.to_sql(arel, [[nil, "bob"]])このように to_sql(arel, binds) の形式で呼んでいると、今後以下のような deprecation 警告が出るようになります。
- 「
to_sqlにbindsを渡すのは非推奨で、将来のバージョンで削除予定」という趣旨のメッセージ。
今後推奨される書き方
to_sqlを使う場合は、bindsを渡さず SQL 文字列の生成のみを行う メソッドとして扱う必要があります。
arel = User.where(name: "bob").arel
sql = ActiveRecord::Base.connection.to_sql(arel)- もし「SQL とバインド値をセットで扱いたい」用途であれば、バインド値は Arel AST 側から取り出すか、クエリ生成段階で別途管理する必要があります(この PR では具体的な代替 API の追加は行っていないため、既存の Arel / AR API をどう組み合わせるかは各アプリケーション側の設計次第です)。
移行時の注意点
- 現時点では 動作そのものは変わっていません。もともと
bindsは SQL 生成には使われておらず、今回の PR で「それを明示的に非推奨にした」だけです。 - ただし、将来のメジャーバージョンで
to_sql(binds)は削除される可能性が高いため、今のうちに以下を行うべきです:to_sqlへの第 2 引数を削除する- 「
to_sqlにbindsを渡すことで SQL 文字列に反映される」という前提のロジックがないか確認する
- 参考情報 (あれば)
- この PR で触れられている過去コミット:
- Rails 5.2 での変更(
to_sql_and_bindsがbindsに依存しなくなった):rails/rails@213796fb49 to_sqlが内部ユーティリティとして抽出されたときのコミット:rails/rails@7db90aa7c7
- Rails 5.2 での変更(
- 該当 PR:
https://github.com/rails/rails/pull/58310
#58309 Rename arel to arel_or_sql
マージ日: 2026/7/30 | 作成者: @kamipo
- 概要 (1-2文で)
このPRは、ActiveRecordの低レベルクエリメソッドの引数名を「arel」から「arel_or_sql」にリネームし、Arelオブジェクトと生SQL文字列の両方を受け取ることが分かるようにしたものです。機能的な挙動は変えずに、引数名の一貫性と可読性を向上させています。
- 変更内容の詳細
対象メソッド
ActiveRecordの DatabaseStatements 周りで、以下のようなメソッドの引数名が変更されています。
insertupdatedeleteexplainto_sqlto_sql_and_binds
従来:
- 多くのメソッドは引数名に
arelを使っており、 to_sql/to_sql_and_bindsだけがarel_or_sql_stringという名前を使っていました。
今回:
- すべて
arel_or_sqlに統一 - 内部ヘルパー
primary_key_for_insert/table_ref_for_insertで既に使われていた命名規則に合わせています。 update_with_resultは Arel 専用のため、引き続き引数名arelのままです。
典型的なシグネチャ変更イメージ
※ 実際のコードは概ね以下のような変化です(イメージ・擬似コード):
# 変更前
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
...
end
def to_sql_and_binds(arel_or_sql_string, binds)
...
end
# 変更後
def insert(arel_or_sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
...
end
def to_sql_and_binds(arel_or_sql, binds)
...
endこのように「引数名だけ」が変わっており、実装ロジックや受け取れる型(Arel manager または String)が変わったわけではありません。
- 影響範囲・注意点
影響が出る可能性があるのは、主に以下のケースです:
メソッドシグネチャをオーバーライドしているアダプタ/アプリケーションコード
- 例: 独自の DB アダプタや、
ActiveRecord::ConnectionAdapters::*Adapterのサブクラスでinsert,update,delete,explain,to_sql,to_sql_and_bindsをオーバーライドしている場合。 - Rubyでは引数名そのものは呼び出し側には影響しませんが、以下の場合は注意:
super呼び出しでキーワード引数を使っている- メタプログラミングで
method.parametersなどを参照して引数名に依存している
- 通常の位置引数で
superしているだけなら、実行時挙動は変わりません。
- 例: 独自の DB アダプタや、
メソッドの引数名をそのまま利用しているドキュメント/コメント/テスト
- 例: 「
insert(arel, ...)を呼ぶ」といった形で引数名を前提にした解説や spec を書いている場合は、表現をarel_or_sqlに追随させるとよいです。
- 例: 「
Arel 限定メソッド
update_with_result- 引数名は
arelのままです。 - ここには生SQL文字列を渡すことはサポートされない、という意図がより明確になります。
- もし
update_with_resultに文字列を渡していた場合は本来非想定利用なので、そのままでも挙動は(Ruby的には)変わりませんが、今後の互換性のためにも Arel を渡す形に直した方が安全です。
- 引数名は
実際のアプリケーションコードへの影響
通常のアプリケーションレイヤーではこれらのメソッドを直接呼ぶことは少ないため、多くのアプリでは実質的な影響はありません。
影響が出やすいのは以下のようなレイヤーです:
- オレオレアダプタ/DBごとのパッチ、Monkey Patch
- gemレベルで ActiveRecord の
DatabaseStatementsを拡張しているコード - トレーシング/ロギング/計測のために、これらのメソッドをラップ/差し替えしているコード
そういったコードで、特に「引数名に依存するリフレクション」をしていないかだけ確認しておくと安心です。
- 参考情報 (あれば)
- 対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract/database_statements.rbactiverecord/lib/active_record/connection_adapters/mysql/database_statements.rbactiverecord/lib/active_record/connection_adapters/postgresql/database_statements.rbactiverecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
- 命名の基準:
primary_key_for_insert/table_ref_for_insertなど、内部ヘルパーメソッドで既にarel_or_sqlが使われており、それに揃える形でのリファクタリングです。
- 機能変更ではなく、インターフェースの「名前の一貫性向上」のためのリネームという位置付けです。
#58296 Refactor insert returning: handling to take QueryIntent directly
マージ日: 2026/7/30 | 作成者: @kamipo
- 概要 (1-2文で)
INSERT ... RETURNINGの処理を、生のSQL文字列操作ではなくQueryIntentを直接扱う形にリファクタリングしたPRです。これにより Arel ベースの INSERT は AST レベルでRETURNINGを付与できるようになり、アダプタ間での拡張性・一貫性が向上しています。
- 変更内容の詳細
2-1. 旧実装: sql_for_insert(sql, binds, returning) の問題点
これまで INSERT 時の returning: オプションは sql_for_insert(sql, binds, returning) を経由して処理されていましたが、以下の問題がありました。
- すべての INSERT が生SQL文字列ベースの処理に引きずり込まれていた
- Arel で構築したクエリも、いったん SQL 文字列にしてから
RETURNING ...を付け足す形だった
- Arel で構築したクエリも、いったん SQL 文字列にしてから
binds引数が事実上 Oracle enhanced adapter 専用で、他のアダプタには不要- AST レベルでの
RETURNING操作ができず、update_with_resultとの一貫性もなかった
2-2. 新実装: apply_returning_to!(intent, returning)
新しく apply_returning_to!(intent, returning) が導入され、QueryIntent を直接受け取るようになりました。
- 引数:
intent:QueryIntent(INSERT 操作の意図を表すオブジェクト)returning:returning:オプションで指定された値(または省略時は後述のデフォルト)
役割:
QueryIntentから、Arel ノードか生SQLかなど、必要な情報をアダプタ側で取り出せる- Arel の INSERT の場合:
arel.returning(...)を呼んで AST にRETURNINGを設定update_with_resultと同じパターンでRETURNINGを扱えるようになる
- 生SQLの INSERT の場合:
- 従来同様、
"INSERT ... RETURNING <cols>"という文字列連結で対応
- 従来同様、
これにより、Arel ベースのクエリは「SQL生成 → 文字列操作 → 再パース」といった往復をせず、AST レベルで完結して RETURNING を付与できるようになりました。
2-3. returning: 省略時のデフォルト挙動の明示化
returning: が呼び出し元で省略された場合のデフォルトが整理されています。
- デフォルトは「テーブルの主キー (primary key)」
- ただし 複合主キーはスキップ(
RETURNINGで「単一の last-inserted id」と対応しないため) - このテーブル情報の取得処理は後述の
table_ref_for_insertに切り出し
概念的には次のような動きです(イメージ):
def apply_returning_to!(intent, returning)
table = table_ref_for_insert(intent)
returning ||= begin
pk = table.primary_key
pk if pk && !Array(pk).many? # 複合PKの場合は nil 扱い
end
# returning があれば intent に付与 (Arel or SQL)
end※実際のコードは多少異なる可能性がありますが、ロジックとしてはこういった内容です。
2-4. table_ref_for_insert ヘルパーの導入
INSERT 対象テーブルを特定する共通処理として、table_ref_for_insert が抽出されました。
- 共通化された責務:
- INSERT 先テーブル名(もしくはテーブルオブジェクト)の取得
- そこから primary key 情報を得る
- これにより、
apply_returning_to!内でテーブル単位のメタ情報を参照しやすくなり、アダプタ実装側も簡潔にできます。
2-5. PostgreSQL の suppress_composite_primary_key の削除
PostgreSQL adapter にあった suppress_composite_primary_key が削除されています。
- これまで:
- 複合主キーのときに
RETURNINGを付けないようにするロジックを PostgreSQL 側で個別に持っていた
- 複合主キーのときに
- これから:
apply_returning_to!側で「複合主キーはreturningのデフォルトから除外」という共通ロジックを持つため、- PostgreSQL アダプタ固有の
suppress_composite_primary_keyは不要になり削除
これにより、「複合PK時の RETURNING 非付与」という方針が アダプタ共通の扱い になります。
2-6. Trilogy adapter の小変更
Trilogy adapter 側の database_statements.rb も 1 行追加 / 3 行削除といった小さな変更が入っています。
- 目的としては他アダプタと同じく、新しい
apply_returning_to!/QueryIntentベースの経路に合わせる調整 - 従来の
sql_for_insert前提コードの削除と思われます
- 影響範囲・注意点
3-1. 開発者視点 (Active Record 利用者)
基本的には挙動互換を保ったリファクタリングであり、以下の点以外は意識せずに利用できます。
returning:を指定している場合- 明示的に指定している
returning:の挙動自体は変わらない想定 - Arel ベースのクエリであっても、内部的に AST で
RETURNINGが管理されるようになっただけ
- 明示的に指定している
returning:を省略している場合- これまでも「主キーを返す」前提の挙動だったはずですが、
- 今回、複合主キーの場合はデフォルトで
RETURNINGを付けないことが明確化され、実装に反映 - もし独自アダプタやカスタムコードで「複合主キーでも何かしら RETURNING を付ける」前提がある場合は注意
3-2. Adapter / ライブラリ開発者視点
特に影響があるのは ActiveRecord アダプタやプラグインを書く側です。
sql_for_insert(sql, binds, returning)に依存していた場合:- 代わりに
QueryIntentベースのフックに追随する必要があります bindsが必要だったのは Oracle enhanced adapter のみだったので、多くのアダプタではコードがシンプルになるはず
- 代わりに
returningの扱い:apply_returning_to!(intent, returning)経由で統一されたため、Arel / SQL どちらの場合もここを起点にすべき- Arel の場合は
arel.returning(...)を使う方向に寄せると、AST レベルでの最適化や一貫性が保ちやすい
- 複合主キー:
- デフォルト挙動としては「複合PKは
returning:省略時の対象から除外」 - どうしても複合PKで
RETURNINGを使いたい場合はreturning:を明示的に指定する運用が必要になる可能性があります
- デフォルト挙動としては「複合PKは
3-3. パフォーマンス・保守性
- Arel クエリが SQL 文字列を介さずに
RETURNINGを付与できるようになったことで:- 余計な文字列操作・再パースが不要になり、わずかですがパフォーマンス改善が期待できる
- クエリ生成の経路がシンプルになり、デバッグ・保守がしやすくなる
- 参考情報 (あれば)
- 類似実装:
update_with_result- 今回の変更は「UPDATE + RETURNING を AST レベルで扱う」
update_with_resultのパターンを、INSERT にも適用したものと位置づけられます。
- 今回の変更は「UPDATE + RETURNING を AST レベルで扱う」
- 関連しそうなコード:
activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb- 各種アダプタの
database_statements.rb(PostgreSQL, Trilogy, Oracle enhanced など)
QueryIntent自体の仕様や使い方を把握しておくと、今後の ActiveRecord のクエリ周りの変更にも追随しやすくなります。
#58299 Remove dead binds handling in AR log subscribers
マージ日: 2026/7/30 | 作成者: @kamipo
- 概要 (1-2文で)
Active Record のログ関連コードから、既に使われなくなったバインド値処理用のヘルパーメソッドとレガシー形式のバインド値サポートが削除されました。Rails 7.0 で実質廃止されていた旧形式の binds([[column, value], ...])へのフォールバックも StructuredEventSubscriber から完全に取り除かれています。
- 変更内容の詳細
背景
- #55900 以降、
ActiveRecord::LogSubscriber#sqlはStructuredEventSubscriberが事前に整形したpayload[:binds]をそのまま.inspectで出力するだけになっている。 - そのため、
LogSubscriber側でバインド値をレンダリング・型キャスト・フィルタリングしていたヘルパーは実質使われていなかった。 - さらに、
StructuredEventSubscriberには Rails 7.0 以前のレガシーな binds 形式([[column, value], ...])を扱うフォールバックが残っていたが、Rails 7.0 でその形式自体が削除されており(コミット2d821ef0c5)、こちらも不要になっていた。
active_record/log_subscriber.rb の変更
以下のようなメソッド群が削除されています(合計 23 行削除、追加なし):
render_bindtype_casted_bindsfilter
これらは主に次のような責務を持っていました:
render_bind: 1つの bind(バインド値)をログ出力用の文字列に整形- 例: カラム名や値、型情報を含めた表現への変換
type_casted_binds: 各 bind を DB への送信に近い形に型キャストした配列に変換filter: 機密情報(パスワードなど)をフィルタリングするための処理
しかし現在の LogSubscriber#sql は、payload[:binds] を直接 inspect してログ出力する実装になっており、これらのメソッドはどこからも呼ばれていない「死んだコード」でした。そのため、不要なコードとして削除されています。
イメージ (擬似コード)
# 以前のイメージ (概念的):
def sql(event)
binds = type_casted_binds(event.payload[:binds])
filtered_binds = filter(binds)
debug " SQL (#{event.duration}ms) #{event.payload[:sql]} #{render_bind(filtered_binds)}"
end
# 現在のイメージ:
def sql(event)
# StructuredEventSubscriber が整形した binds をそのまま使う
binds = event.payload[:binds]
debug " SQL (#{event.duration}ms) #{event.payload[:sql]} #{binds.inspect}"
endこれにより、LogSubscriber 側のバインド処理ヘルパーは完全に不要となった、という整理です。
structured_event_subscriber.rb の変更
StructuredEventSubscriber では、バインド値の表現を新しい構造化形式(ActiveRecord::Relation::QueryAttribute 等)に一本化し、古い形式へのフォールバックを削除しています(+2/-13)。
主な変更点:
render_bind内の「Arrayだった場合」の処理分岐を削除- 旧形式:
[[column, value], ...]のような配列形式を想定したwhen Arrayブランチ
- 旧形式:
attribute_nameを取得する際に、Arrayを扱う並列の分岐も削除
擬似的には次のようなイメージの変更です:
# 以前のイメージ (概念的):
def render_bind(bind)
case bind
when Array
column, value = bind
# 旧形式 [[column, value], ...] の処理
else
# 新形式 QueryAttribute の処理
end
end
def attribute_name(bind)
case bind
when Array
column, _ = bind
column.name
else
bind.name # 新形式
end
end
# 現在:
def render_bind(bind)
# 新形式のみを前提とした処理
end
def attribute_name(bind)
# 新形式のみを前提とした処理
endつまり、StructuredEventSubscriber は「新しい構造化された binds 形式のみを扱う」実装に整理され、旧来の Array ベースの binds には一切対応しない形になりました。
- 影響範囲・注意点
影響範囲
Rails 本体内部
LogSubscriberとStructuredEventSubscriber間のやり取りが前提としている binds 形式が、完全に「新形式のみ」になった。- 内部的なコードクリーンアップであり、通常のアプリケーションコードには直接の影響は基本的にありません。
メタプログラミング/モンキーパッチをしている場合
下記のようなケースでは影響がありえます:ActiveRecord::LogSubscriberのrender_bind/type_casted_binds/filterメソッドを直接呼んでいた、あるいはオーバーライドしていたStructuredEventSubscriberの binds 取り扱いがArray形式も受け付けることを前提に独自の拡張をしていた
これらは今回削除・単純化されているため、Rails のアップデート時に
NoMethodErrorや挙動の変化が起こる可能性があります。
注意点
旧形式の binds を渡すコードはサポートされない
- Rails 7.0 でそもそも非対応になっているため、アプリ側で
[[column, value], ...]のようなレガシー binds を内部 API に渡している場合は、この PR を機に完全に壊れます。 - そのようなコードが残っている場合は、
ActiveRecord::Relation::QueryAttribute等の新しい構造化形式に合わせて見直す必要があります。
- Rails 7.0 でそもそも非対応になっているため、アプリ側で
ログ出力カスタマイズの際の前提変更
- SQL ログのバインド値は、
StructuredEventSubscriberが整形したpayload[:binds]をそのまま.inspectしたもの、という前提がより強くなっています。 - バインド値の表現をカスタムしたい場合は、
LogSubscriberではなくStructuredEventSubscriber側の処理をフック/拡張する方が筋がよい構造になっています。
- SQL ログのバインド値は、
- 参考情報 (あれば)
- この PR:
Remove dead binds handling in AR log subscribers(#58299) - 関連 PR:
- #55900:
LogSubscriber#sqlがpayload[:binds]の.inspectに一本化された変更
- #55900:
- 関連コミット:
2d821ef0c5: Rails 7.0 でレガシーな binds 形式([[column, value], ...])が削除されたコミット
この PR は主に「内部実装の整理・デッドコード削除」であり、Rails のログ周り内部 API に直接依存していない通常のアプリケーション開発者にとっては、動作上の変化はほぼありません。ただし、ログのバインド処理を深くいじっている場合は、アップデート時に該当箇所を確認することを推奨します。
#58298 Make ActiveModel#model_name accessible from a Ractor:
マージ日: 2026/7/29 | 作成者: @Edouard-chin
- 概要 (1-2文で)
ActiveModel#model_nameが Ractor 内から安全に利用できるようにするため、model_nameが返すオブジェクトを「凍結(freeze)」しつつ、内部で使う値を事前に計算・設定する変更です。これにより Ractor 環境でもmodel_name.humanなどのメソッド呼び出しが行えるようになります。
- 変更内容の詳細
背景・問題点
Ruby 3 以降の Ractor では、「共有されるオブジェクト」は以下のような制約があります:
- 共有されるオブジェクトは原則として不変(
frozen)である必要がある - 共有可能でないオブジェクトを Ractor 間で渡そうとするとエラーになる
従来の ActiveModel::Naming#model_name は、返すオブジェクト (ActiveModel::Name インスタンス) が内部でメモ化や遅延評価を行うため、生成後に状態が変化し得る つくりになっていました。
そのため、この model_name オブジェクトを別 Ractor に渡して利用することができませんでした。
この PR は、
model_nameが返すオブジェクトを Ractor 共有可能にする- かつ、そのオブジェクト上で
#humanなどを安全に呼べるようにする
ために、生成時に必要な値をすべて確定させてから freeze する という方針を取っています。
コードレベルでの変更ポイント(概要)
対象ファイル: activemodel/lib/active_model/naming.rb
主な変更点:
model_nameが返すActiveModel::Nameインスタンスに対して:- 内部で利用する属性(例:
name,singular,plural,element,humanなど)の値を、生成時に eager に計算・セット する - その後、そのインスタンスを
freezeすることで Ractor 共有可能にする
- 内部で利用する属性(例:
遅延評価/メモ化に依存していた部分を、
「初回生成時にまとめて計算して、その後不変にする」
という設計に寄せている
具体的なコードは PR 本体を見ないと完全には書けませんが、イメージとしては以下のような対応です(擬似コード):
def model_name
@model_name ||= begin
name = ActiveModel::Name.new(self, nil, self.name)
# ここで内部の必要な値をすべて評価しておく
name.name
name.singular
name.plural
name.element
name.human
name.freeze # Ractor 共有可能にする
end
endこれにより、model_name を他の Ractor に渡しても、その後に内部状態が変化することはなく、Ractor が要求する「不変性」を満たせます。
テストの変更 (activemodel/test/cases/naming_test.rb)
テストは以下の点を確認する内容になっています:
新しいモデルクラスをその場で定義
- 既存テストで使われているモデルクラスだと、すでに
model_nameのメモ化が走っている可能性があるため、
「まっさらな状態」からの挙動を検証するために、新規クラスを定義してテストしています。
- 既存テストで使われているモデルクラスだと、すでに
I18n を stub している理由
- I18n ライブラリ自体が現状 Ractor セーフではないため、
テスト内で I18n の呼び出しが実際に走ると Ractor 関連のエラーでテストが落ちます。 - そこで、
model_name.humanなどで内部的に呼ばれる I18n を stub し、
「ActiveModel::Name のオブジェクト自体が Ractor 共有可能かどうか」だけをテスト対象にしています。
- I18n ライブラリ自体が現状 Ractor セーフではないため、
テストのイメージ(擬似コード):
Class.new(ActiveModel::Naming) do
# モデルクラス定義
end
I18n.stub(:t, "stubbed") do
r = Ractor.new(model_class) do |klass|
klass.model_name.human # Ractor 内から呼び出せるか
end
r.take
end- 影響範囲・注意点
影響範囲
ActiveModel#model_nameが返すオブジェクト (ActiveModel::Name) の内部実装が、
「遅延評価で必要になったときに値を計算する」
から
「生成時に一括で計算して freeze する」
に変わっています。- ほとんどの通常利用(
User.model_name.singular,model_name.human等)では挙動は変わらないはずです。
パフォーマンス面
- これまで「使われなかったかもしれない属性」も含めて、
model_name生成時にすべて計算されるようになるため、初回呼び出し時にわずかなオーバーヘッドが増える可能性があります。 - 一方で、後続の呼び出しではメモ化済みの frozen オブジェクトをそのまま使うため、大きな退行は想定しづらいです。
- これまで「使われなかったかもしれない属性」も含めて、
Ractor を使う場合のポイント
model_class.model_nameをメイン Ractor で生成して別 Ractor に渡す、
あるいは別 Ractor の中でmodel_class.model_nameを呼ぶ、といったユースケースで安全に動作するようになります。- ただし、
model_nameの内部で利用する I18n 側が未だ Ractor セーフではないため、
実運用で Ractor 内からmodel_name.humanなどを多用する場合は、I18n の利用パターンや stub/カスタム実装などを検討する必要があります。
互換性 / 破壊的変更の可能性
ActiveModel::Nameを継承/モンキーパッチしていて、「生成後にインスタンスの可変な状態に依存している」ようなコードがある場合は影響が出る可能性があります。- 通常の Rails アプリ(
model_nameをそのまま使っているだけ)であれば互換性問題はほぼ発生しないはずです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58298
- Ruby Ractor の制約ドキュメント:
- 関連クラス:
ActiveModel::NamingActiveModel::Name
この変更は、「Ractor 対応を進めるための ActiveModel 側の一手」であり、Rails 全体を完全に Ractor セーフにするものではありませんが、マルチ Ractor 環境でのモデルクラス利用に向けた基盤整備と位置づけられます。
#58304 Fix releaser_test/rubocop
マージ日: 2026/7/29 | 作成者: @skipkayhil
- 概要 (1-2文で)
Rails のリリース用テスト (tools/releaser/test/releaser_test.rb) において、環境変数GITHUB_ACTIONSの扱いを修正し、CI(GitHub Actions)上でもローカル同様に安定してテストできるようにした PR です。Active Support テストにあるwith_env実装をコピーして利用することで、環境変数を書き換えるテストの挙動を正しく制御しています。
- 変更内容の詳細
何を直したか
- releaser のテストの中で
GITHUB_ACTIONS環境変数に依存したコードパスをテストしているが、GITHUB_ACTIONS=trueの分岐は「テスト内で明示的に ENV を設定」していたため問題なし- 一方で
GITHUB_ACTIONS=falseの分岐は、「CI 上で既にGITHUB_ACTIONS=trueがセットされている」状況を考慮しておらず、GitHub Actions 上でテストすると期待通りに false 側の分岐を通れない
という問題があったため、その分岐も明示的に環境変数を制御するように修正しています。
with_env の導入
PR 説明にある通り、Active Support のテストで使われている with_env ヘルパをそのままコピーして利用しています。イメージとしては次のような実装です(実際のコードを要約した擬似コード):
def with_env(env_overrides)
old_env = ENV.to_hash
begin
env_overrides.each do |key, value|
if value.nil?
ENV.delete(key)
else
ENV[key] = value
end
end
yield
ensure
# テスト終了後に必ず元の ENV を復元
ENV.replace(old_env)
end
endこの with_env を使うことで、テストごとに一時的に GITHUB_ACTIONS を true / false / 未定義 に変更し、ブロックを抜けたタイミングで必ず元の状態へ戻します。
テストの修正イメージ
GITHUB_ACTIONS=trueのケースだけでなく、GITHUB_ACTIONS=false(もしくは未設定)のケースについても、with_env("GITHUB_ACTIONS" => "false")のようにして明示的に環境を作るようになっています。- これにより、
- ローカル環境(通常
GITHUB_ACTIONS未設定) - GitHub Actions 上(
GITHUB_ACTIONS=trueがデフォルト) どちらでも同じ条件でテストが実行されるようになります。
- ローカル環境(通常
- 影響範囲・注意点
- 影響範囲は
tools/releaser/test/releaser_test.rbのテストコードのみで、本番コードには影響しません。 - テストで環境変数を操作する際に、Active Support 同様の
with_envパターンを使うことで、副作用(ENV の汚染)が残らないようになっています。 - 今後 releaser のテストを追加・変更する際は、この
with_envを使って- CI 上でデフォルト設定されている
GITHUB_ACTIONS - その他の CI 固有の環境変数 を上書きするのが安全なパターンになります。
- CI 上でデフォルト設定されている
- 既存のテストが
ENV["GITHUB_ACTIONS"]に暗黙に依存している場合、今後他のテストでも同様の問題が起きうるため、必要に応じて同じヘルパを利用することが推奨されます。
- 参考情報 (あれば)
- この PR 内のコミット:
- GitHub Actions が自動的にセットする環境変数:
GITHUB_ACTIONS=true(公式ドキュメントより)
- 類似パターン: Active Support のテストヘルパ
with_env(ActiveSupport テストスイート内の実装を転用)
#58226 Deprecate TransactionState predicates and fix stale ivar in Persistence::ClassMethods#inherited
マージ日: 2026/7/29 | 作成者: @55728
- 概要 (1-2文で)
Active Record のTransactionStateが持つ一部の述語メソッドとnullify!が非推奨(deprecate)になり、Persistence::ClassMethods#inheritedで使っていたクエリ制約用インスタンス変数のリセット先が正しい名前に修正されました。いずれも挙動変更は最小限で、ドキュメント公開された API の整理と内部実装の整合性をとるための変更です。
- 変更内容の詳細
2-1. TransactionState のメソッドの非推奨化
対象クラス:ActiveRecord::ConnectionAdapters::TransactionState
非推奨になったメソッド:
#fully_committed?#fully_rolledback?#fully_completed?(#completed?のエイリアス)#nullify!
背景:
- これらのメソッドはすでに内部からの呼び出し元がすべて削除されており(コミット 77f7b2df3a, 8180c39610, 6c745b0c51)、実質的に未使用になっていた。
- ただし
TransactionStateはドキュメント公開された API であり、「ドキュメントに載っているメソッドを黙って削除する」のは互換性ポリシー違反になるため、削除ではなく deprecate とした。 fully_*?系で確認していた状態は、引き続き以下のメソッドから観測可能:#committed?#rolledback?#completed?(fully_completed?の元になっているメソッド)
実装的には:
- 各メソッド定義内で
ActiveSupport::Deprecation.warnなどにより警告を出すようになっている(テストが追加されていることからも明らか)。 CHANGELOGに非推奨化のエントリが追加されている。
利用側の推奨置き換え:
# 旧 (非推奨)
state.fully_committed?
state.fully_rolledback?
state.fully_completed?
state.nullify!
# 新 (推奨)
state.committed?
state.rolledback?
state.completed?
# nullify! 相当の用途がある場合は、呼び出し箇所の意図に応じて要見直し
# (内部実装依存のコードで使っていた場合はそもそもサポート外に近い)ポイント:
fully_completed?はcompleted?の単なる別名だったため、completed?を直接使えば OK。- 「fully〜」という名前が付いていたものの、実質的に expose している状態は
committed?/rolledback?/completed?と同じであり、多重ネーミングになっていたものを整理する意図がある。
2-2. Persistence::ClassMethods#inherited における ivar リセットの修正
対象メソッド:ActiveRecord::Persistence::ClassMethods#inherited
修正内容:
- クラス継承時に実行される
inheritedフック内で、クエリ制約関連のインスタンス変数をリセットしていた箇所が、古い名前のままになっていたのを修正。
変更前(概念的なイメージ):
def inherited(subclass)
super
subclass.instance_variable_set(:@_query_constraints_list, nil)
end変更後:
def inherited(subclass)
super
subclass.instance_variable_set(:@query_constraints_list, nil)
end背景:
- 過去のコミット
767cd52e76で、メモ化に使用するインスタンス変数名が@_query_constraints_listから@query_constraints_listに変更されていた。 - しかし
inherited内のリセット処理だけが古い名前のまま(@_query_constraints_list)だったため、「事前に定義しておく ivar」と、「メモ化で実際に使われる ivar」の名前がずれていた。 - この ivar はパフォーマンス最適化(オブジェクトの shape を揃える、ヒープへの割り当てパターンを固定するなど)のために「事前にインスタンス変数を生やしておく」目的で使われており、実質的に挙動は変えていない。
- 新しい ivar 名にあわせて reset 先も
@query_constraints_listに修正することで、意図通りに一貫した shape が確保される。
挙動について:
- ロジック的には、実行されるクエリや返り値が変わるわけではない。
- ただし Ruby の VM 観点では、インスタンス変数の shape が安定するため、微妙なパフォーマンスやメモリレイアウトに良い影響が出る可能性がある(Rails の内部実装的な配慮)。
- 影響範囲・注意点
3-1. TransactionState を直接利用しているコード
影響が出るケース:
- アプリケーションや gem が
ActiveRecord::ConnectionAdapters::TransactionStateを直接参照し、以下のメソッドを呼び出している場合:#fully_committed?#fully_rolledback?#fully_completed?#nullify!
影響内容:
- Rails 実行時に deprecation warning が出力されるようになる。
- 現時点では挙動は変わらないが、将来のメジャーバージョンで削除される可能性が高い。
対処:
- ログやテスト実行時の出力から deprecation warning を確認する。
- 上記の旧メソッド呼び出しを、以下のように置き換える:
fully_committed?→committed?fully_rolledback?→rolledback?fully_completed?→completed?nullify!→ 用途に応じて設計見直し(多くの場合、内部実装に依存しすぎているので別のアプローチを検討)
nullify! を使っていた場合の注意:
- トランザクション状態オブジェクトを「外から書き換える」ようなコードは、基本的に内部実装への強い依存であることが多く、Rails 本体の進化と整合しなくなるリスクが高い。
- 可能であれば、トランザクション境界を適切に構成し直す、あるいはコールバックやフックポイントの使い方を見直し、「状態オブジェクトを直接いじらない」設計に改めるのが望ましい。
3-2. inherited の ivar 修正に関する影響
通常のアプリケーションにおいては:
ActiveRecord::Baseを継承するモデルクラスを定義しているだけであれば、現実的な挙動変化はほぼない。@query_constraints_list/@_query_constraints_listを直接参照・操作しているアプリコードや gem があれば、それ自体が内部実装依存でありサポート外寄りだが、今回の修正でより一貫した挙動になる。
性能面:
- インスタンス変数の shape が継承クラスで事前に定義されるようになることで、軽微なパフォーマンス改善が期待できる程度で、大きなブレイクはない。
- 参考情報 (あれば)
- 当該 PR:
- 「Deprecate
TransactionStatepredicates and fix stale ivar inPersistence::ClassMethods#inherited」 - PR #58226 / merged at 2026-07-29
- 「Deprecate
- 関連 PR / コミット:
- フォローアップ元 PR: #58217
TransactionStateの最後の利用箇所を削除したコミット:- 77f7b2df3a
- 8180c39610
- 6c745b0c51
@_query_constraints_list→@query_constraints_listへのリネームを行ったコミット:- 767cd52e76
- 変更されたファイル:
activerecord/CHANGELOG.md(非推奨追加)activerecord/lib/active_record/connection_adapters/abstract/transaction.rb(deprecation 実装)activerecord/lib/active_record/persistence.rb(ivar 名修正)activerecord/test/cases/transactions_test.rb(deprecation テスト追加)
#58295 Return a Float from Time#- with a DateTime
マージ日: 2026/7/29 | 作成者: @55728
- 概要 (1-2文で)
Time#-にDateTimeを渡したときの戻り値をRationalではなくFloatに統一する変更です。これにより、Time#-(Time)やActiveSupport::TimeWithZone#-(DateTime)と同じく秒数の差分がFloatで返り、扱いやすくなります。
- 変更内容の詳細
これまでの挙動
Time - DateTime の結果が Rational になっていました:
elapsed = Time.utc(2000, 1, 2) - DateTime.civil(2000, 1, 1)
elapsed #=> (86400/1)
elapsed.class #=> Rational
"#{elapsed} seconds" #=> "86400/1 seconds"
elapsed.to_json #=> "\"86400/1\""
elapsed.round(2) #=> 86400/1 # Rational のまま一方で、以下は Float を返します:
Time.utc(2000, 1, 2) - Time.utc(2000, 1, 1)
#=> 86400.0 (Float)
Time.zone.parse("2000-01-02") - DateTime.civil(2000, 1, 1)
#=> 86400.0 (Float, ActiveSupport::TimeWithZone)Rails 6.1 付近のコミット (4d24a25acf) で Time - DateTime 計算の精度を高めるために Rational を使うようになった影響で、戻り値の型だけが他と不整合な状態になっていました。
今回の修正内容
activesupport/lib/active_support/core_ext/time/calculations.rbTime#-がDateTimeを引数に取る場合、内部ではこれまで通りRationalを用いて厳密に計算します。- 計算の最後に
to_fしてFloatを返すように変更されています。
擬似コードで表すと、イメージは以下のような変更です:
ruby# 変更前(イメージ) def -(other) if other.is_a?(DateTime) rational_diff = ... # Rational で厳密に差を計算 rational_diff # => Rational をそのまま返していた end end # 変更後(イメージ) def -(other) if other.is_a?(DateTime) rational_diff = ... # Rational で厳密に差を計算 rational_diff.to_f # => Float に変換して返す end endactivesupport/test/core_ext/time_ext_test.rb- 上記の挙動を保証するテストが追加され、
Time - DateTimeがFloatを返すことを検証しています。
- 上記の挙動を保証するテストが追加され、
動作サンプル
elapsed = Time.utc(2000, 1, 2) - DateTime.civil(2000, 1, 1)
elapsed #=> 86400.0
elapsed.class #=> Float
"#{elapsed} seconds" #=> "86400.0 seconds"
elapsed.to_json #=> "86400.0"
elapsed.round(2) #=> 86400.0内部計算は Rational のままなので、サブ秒精度を持つ DateTime でも精度を保ったうえで最終結果のみ Float になります。
- 影響範囲・注意点
- 型が変わる (Rational → Float)
- 既存コードで「
Time - DateTimeがRationalを返す前提」で処理していた場合は影響があります。- 例:
elapsed.numerator,elapsed.denominatorを直接使っている場合 - 例: 型チェック
elapsed.is_a?(Rational)に依存している場合
- 例:
- 既存コードで「
- 他の Time 差分 API との一貫性が向上
Time - TimeやTimeWithZone - DateTimeなどと戻り値の型が揃うため、多くのコードではむしろ扱いやすくなります。
- 精度面
- 計算そのものは Rational で行われているため、以前の変更で回復したサブ秒レベルの精度は維持されます。
- 最終的に IEEE 754 の
Floatに落とすため、極端に長い期間や非常に細かいサブ秒精度では通常のFloat精度限界の影響は受けますが、これは従来のTime - Timeと同等です。
- 互換性ポリシー的な位置づけ
- 戻り値の型変更はセマンティクスとしては破壊的変更になりうるため、Rails のメジャー/マイナーアップデートに追随する際にはリリースノート等でこの点を確認しておくのが安全です。
- 参考情報 (あれば)
- 該当 PR:
Return a Float from Time#- with a DateTime(#58295) - 関連実装ファイル:
activesupport/lib/active_support/core_ext/time/calculations.rb
- 関連テスト:
activesupport/test/core_ext/time_ext_test.rb
- 背景となった過去変更:
- コミット
4d24a25acfによるTime - DateTime計算の Rational 化(精度向上) - 今回はその「精度」は維持しつつ「インターフェース上の戻り値型」を従来の Time 差分 API と揃えるための調整です。
- コミット
#58067 Make ActiveSupport::TimeZone shareable
マージ日: 2026/7/29 | 作成者: @Edouard-chin
- 概要 (1-2文で)
ActiveSupport::TimeZoneオブジェクトを生成時に freeze することで、Ractor 間で共有可能(Ractor-shareable)にし、TimeWithZone経由でタイムゾーンにアクセスした際の Ractor Isolation Error を解消する変更です。クラス設計上ミュータブルである必要がないことを前提に、安全に共有できるようにしています。
- 変更内容の詳細
2-1. 何をしたか
主な変更点は ActiveSupport::TimeZone のインスタンスを生成直後に freeze するようにしたことです。
対象ファイル:
activesupport/lib/active_support/values/time_zone.rb- これに対応するテスト追加・修正:
activesupport/test/time_zone_test.rb- 関連する view helper のテスト修正:
actionview/test/template/date_helper_test.rb
疑似的には以下のような変更が入っているイメージです(実際のコードを要約):
module ActiveSupport
class TimeZone
def initialize(name, utc_offset = nil, tzinfo = nil)
# 既存の初期化処理 …
# @name, @utc_offset, @tzinfo などをセット
freeze # ← この PR で追加
end
end
endこれにより、ActiveSupport::TimeZone のインスタンスは生成後に変更不可能となり、Ruby の Ractor モデルにおける「shareable object(共有可能オブジェクト)」として扱えるようになります。
2-2. 背景: なぜ freeze すると Ractor で使えるのか
Ruby の Ractor は「共有できるオブジェクト」と「共有できないオブジェクト」を厳密に分けており、共有できるのは以下のようなものに限られます:
- イミュータブルなオブジェクト(
freeze済みで、内部にミュータブルな状態を持たない) - 一部のビルトインのスレッドセーフなオブジェクト など
ActiveSupport::TimeZone は:
- 公開 API 的にはインスタンスを後から書き換える手段がない
- 遅延初期化で ivar を後からセットするようなコードもない
という設計になっているため、コンストラクタで必要な状態をすべてセットした後に freeze してしまっても問題ない、と判断されています。
その結果:
zone = ActiveSupport::TimeZone["UTC"]
Ractor.new(zone) do |z|
# ここで z を安全に利用できるようになる
z.now # など
endといったコードで、これまで発生していた Ractor Isolation Error を回避できます。
2-3. テスト側の変更
テストでは主に以下を確認していると考えられます:
ActiveSupport::TimeZoneのインスタンスがfrozen?になっていること- 既存の機能(名前解決・オフセット計算・
TimeWithZoneとの連携など)が freeze によって壊れていないこと ActionView側のdate_helperなど、TimeZoneを内部で使うヘルパが従来通り動作すること
actionview/test/template/date_helper_test.rb の 1 行差し替えは、freeze されたオブジェクトを前提にした期待値や挙動に合わせるための微修正と考えられます。
- 影響範囲・注意点
3-1. 影響範囲
直接の影響:
ActiveSupport::TimeZoneを継承したり、メタプログラミングで内部状態を書き換えているようなコードがあれば、freezeによりRuntimeErrorが発生する可能性があります。- ただし、通常の利用(
Time.zone = "Tokyo"、Time.zone.now、in_time_zoneなど)では挙動は変わらず、むしろ Ractor 利用時の安定性が向上します。
Ractor を使うコードへのメリット:
Time.zoneやTimeWithZoneオブジェクトを Ractor 間で共有する際に、内部でActiveSupport::TimeZoneにアクセスしても Isolation Error が起きにくくなります。- タイムゾーン情報をグローバルにキャッシュして使うような並行処理コードでも扱いやすくなります。
3-2. 注意点・互換性
非推奨な内部操作をしている場合は破壊的変更になり得る:
例えば、以下のようなコードはこの PR 以降は動かなくなります:
rubyzone = ActiveSupport::TimeZone["UTC"] zone.instance_variable_set(:@name, "Hacked") # => RuntimeError: can't modify frozen ...公式には想定されていない使い方なので、これを機に依存を避けるべきです。
TimeZone内部の将来的な変更との整合性:- この PR は「現状、
TimeZoneはミュータブルではない(し、遅延初期化もしない)」という前提に立っています。 - 将来
TimeZoneに遅延初期化やミュータブルな挙動を加える場合、freezeとの整合性を考慮する必要があります(その場合は@mutexなど Ractor 非共有な状態も問題になるため、そもそも Ractor 対応方針の再検討が必要)。
- この PR は「現状、
- 参考情報 (あれば)
Ractor と shareable オブジェクトの仕様(Ruby 本体側の背景):
- Ractor は Ruby 3.0 以降の並列実行機構で、オブジェクト共有には
shareable?条件を満たす必要がある。 freeze済みで、内部に非 shareable オブジェクトを持たないオブジェクトは基本的に shareable と扱える。
- Ractor は Ruby 3.0 以降の並列実行機構で、オブジェクト共有には
Rails 内での
ActiveSupport::TimeZoneの主な用途:Time.zoneとしてアプリケーションデフォルトタイムゾーンの管理time_zone_selectなど View ヘルパの選択肢生成Time#in_time_zone,TimeWithZoneによる変換や表示
この PR は上記の利用パターンに影響を与えず、Ractor 対応を前進させる小さくも実用的な変更と言えます。
#58291 Expand insert_returning deprecation note with migration path
マージ日: 2026/7/29 | 作成者: @kamipo
- 概要 (1-2文で)
このPRは、insert_returning: falseオプションに関する非推奨(deprecation)メッセージを拡張し、代替手段としてprefetch_primary_key?を利用する移行パスを明示的に案内するものです。コード変更はなく、CHANGELOG上のドキュメント更新のみです。
- 変更内容の詳細
背景
insert_returning: falseは元々、トリガベースのパーティションテーブル対応のために導入されました(PR #5698)。- こうしたテーブルでは、
BEFORE INSERTトリガが親テーブルへのINSERTを子テーブルへ振り分けるため、親テーブルでINSERT ... RETURNINGを実行しても行が返ってきません。 - そこで「
RETURNING自体を抑制 (insert_returning: false) し、INSERT実行後にSELECT currval(...)で主キーを取得する」というワークアラウンドが採られていました。
今回の変更の主旨
- その
insert_returning: falseが非推奨になるにあたり、「代わりにどうすればよいか」という具体的な移行パスをCHANGELOGに追記しています。 - 推奨される代替は
prefetch_primary_key?を利用する方法です。prefetch_primary_key?はINSERT前にSELECT nextval(...)を発行して主キーを事前取得し、その値を使ってINSERTする仕組みで、Oracle enhanced adapter などですでに使われているパターンです。- これにより、
INSERT ... RETURNINGを使わなくても主キーを安定して取得できます。
- その
実際のコード変更
- 変更ファイルは
activerecord/CHANGELOG.mdのみで、+6行のドキュメント追加。 - Rails 内部コード(アダプタ・モデル・クエリ生成など)の挙動は一切変わっていません。
- 変更ファイルは
(サンプルイメージ:prefetch_primary_key? を用いたアダプタ側実装の概念)
# 擬似コード(実際の PR には含まれていません)
def prefetch_primary_key?(table_name = nil)
true
end
def next_sequence_value(sequence_name)
select_value("SELECT nextval('#{sequence_name}')")
end
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
if prefetch_primary_key?(table_name) && pk && !id_value
id_value = next_sequence_value(sequence_name || default_sequence_name(table_name, pk))
end
# ここで RETURNING を使わずに INSERT し、id_value を返す
end- 影響範囲・注意点
影響範囲
- 実行時の挙動は一切変わらず、既存アプリへの直接の影響はありません。
- 影響するのは、ドキュメントを参照して今後のバージョンアップに備える開発者です。
注意点 / 対応方針
- これまで PostgreSQL 等でトリガーベースのパーティションを使い、
insert_returning: falseを前提に主キー取得を行っていた場合、将来的な Rails バージョン(insert_returning: falseが削除されるタイミング)で問題になる可能性があります。 - 対応としては、アダプタまたはカスタムコネクションの実装で
prefetch_primary_key?を有効にし、SELECT nextvalベースで主キーを事前取得する方式へ移行することが推奨されます。 - すでに Oracle enhanced adapter で使われている実績あるパターンなので、それに倣う形で実装・移行するのが安全です。
- これまで PostgreSQL 等でトリガーベースのパーティションを使い、
- 参考情報 (あれば)
- 当該PR:
insert_returning: falseが導入された元PR:- Oracle enhanced adapter における
prefetch_primary_key?利用例(実際のコード参照先の一例):- https://github.com/rsim/oracle-enhanced (レポジトリ全体。
prefetch_primary_key?で検索)
- https://github.com/rsim/oracle-enhanced (レポジトリ全体。
このPR自体はドキュメント更新のみですが、将来の非互換点に対する明確な移行パスが示された点が重要です。
#58290 Support a single column name for #insert's returning:
マージ日: 2026/7/29 | 作成者: @kamipo
- 概要 (1-2文で)
insertメソッドのreturning:オプションで「単一カラム名」を直接指定できるようにし、その場合は配列ではなく「そのカラムの値」を直接返すようにした変更です。これにより、将来廃止予定のpk位置引数を内部的にreturning:へ移行できるようにしつつ、互換性を保っています。
- 変更内容の詳細
2-1. returning: の振る舞い拡張
従来の #insert / #exec_insert の returning: は「配列のみ」を受け付け、結果も「配列」で返していました。この PR では、以下のようなパターンをサポートします。
# 1) returning: nil
insert(..., returning: nil)
# => id_value || last_inserted_id(result)
# (従来どおり: 明示的な returning は使わず、従来の ID 取得ロジックにフォールバック)
# 2) returning: "id"
insert(..., returning: "id")
# => 挿入された行から "id" カラムの値1つを返す (スカラー値)
# 3) returning: ["id"]
insert(..., returning: ["id"])
# => ["id"] という1要素配列をカラム指定として解釈し、
# 1要素の配列 [id_value] を返す
# 4) returning: [c1, c2]
insert(..., returning: [c1, c2])
# => [value_for_c1, value_for_c2] を返す (従来どおり)要するに:
Stringを指定した場合: そのカラムの値(単一値)を返すArray<String>を指定した場合: カラム値の配列を返すnilの場合: 従来の「last_inserted_id 系」の仕組みにフォールバック
これにより、「今まで pk = "id" で 1 カラムの値だけ欲しかったケース」を、returning: "id" に書き換えればそのまま同じ形の戻り値を得られるようになりました(配列の unwrap が不要)。
2-2. 内部実装から pk 引数を削除
内部メソッド _exec_insert と sql_for_insert からは、pk 位置引数が完全に削除されました。代わりに、必要な情報は returning: で渡される前提に変更されています。
ただし、パブリック API である insert / exec_insert は、まだ従来どおり pk 位置引数を受け取ります。その上で内部的に「互換レイヤー(shim)」として returning: に変換します。
変換ロジック(互換 shim)のイメージ:
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [], returning: nil)
# まだ pk 引数をサポートするが、内部的には returning にマップする
effective_returning =
if returning != nil
returning
else
case pk
when String
pk # "id" -> "id" (単一カラム名)
when false
[] # false -> [] (何も返さない)
else
nil # その他は従来どおり nil (id_value || last_inserted_id(result))
end
end
_exec_insert(..., returning: effective_returning)
endポイント:
pk = "id"→returning: "id"に変換され、単一値を返すpk = false→returning: []に変換され、「何も返さない」挙動- 呼び出し側がすでに
returning:を指定している場合はpkよりreturning:が優先される想定(実装に準じる)
これにより、外部 API のシグネチャは現時点では変えずに、内部実装を「returning: ベース」に統一する準備が整いました。将来的には pk が正式に非推奨 → 削除される予定です。
2-3. PostgreSQL アダプタの挙動
PostgreSQL アダプタには、INSERT ... RETURNING を使わない場合に currval を用いてシーケンスから ID を取得するフォールバック (@use_insert_returning = false) が存在します。
この PR では、pk を速やかに完全削除してしまうと、このフォールバック経路で必要な sequence_name を求められなくなってしまう問題があるため、次のような対応が取られています。
- PostgreSQL の
insert/exec_insertのオーバーライド内で、まだpkを使ってsequence_nameを解決する処理を維持 - もしくは
pkが無い場合にもスキーマキャッシュからsequence_nameを導出できるようにしておき、@use_insert_returning = falseのフォールバックが引き続き機能するよう保持
つまり、PostgreSQL では:
- 外部 API シグネチャ上の
pkはまだ残る - ただし
_exec_insertやsql_for_insertのような下位メソッドからはpkが退場 sequence_nameの解決だけは当面pkも利用しつつ、将来的な削除に備えている
2-4. Trilogy アダプタ / テストの変更
trilogyアダプタのdatabase_statements.rbでも_exec_insert等のシグネチャ調整が行われ、pk排除・returning:ベースの API に追従しています(+2/-2 と軽微)。activerecord/test/cases/database_statements_test.rbに、returning:に単一カラム名を渡したときにスカラー値が返ることなどを確認するテストが追加されています(+13行)。
- 影響範囲・注意点
3-1. ライブラリ・アダプタ実装者への影響
自作アダプタや ActiveRecord を拡張している場合、以下を確認してください。
_exec_insert/sql_for_insertにpk位置引数を取っている場合、今回の変更に合わせて削除し、returning:オプションを解釈する実装に移行する必要があります。- まだ
insert(arel, nil, "id", ...)のようにpkを直接使っているコードは、将来的な非推奨化に備えてreturning:への移行を検討してください。
推奨の書き換え例:
# 旧来スタイル (今後非推奨予定)
connection.insert(arel, nil, "id")
# 推奨スタイル
connection.insert(arel, nil, nil, nil, nil, [], returning: "id")
# または、ActiveRecord::Persistence 経由の通常の save / create を使う3-2. アプリケーションコードへの実務的影響
アプリケーション側で insert / exec_insert を直接使っていない一般的な Rails アプリは、この PR の影響をほぼ受けません。
insert / exec_insert を直接使っている場合:
- すでに
returning: ["id"]のような形で使っているコードは、そのまま動きます(戻り値が配列であることも変わりません)。 - 「単一カラムだけが欲しい」場合に、今後は
returning: "id"を使うと、ラップ解除処理が要らなくなるため、実装をシンプルにできます。
例:
# 変更前: 配列から取り出していた
id = connection.insert(arel, nil, nil, nil, nil, [], returning: ["id"]).first
# 変更後: スカラーで返る
id = connection.insert(arel, nil, nil, nil, nil, [], returning: "id")注意点:
returning: "id"とreturning: ["id"]では戻り値の型が異なる(スカラー vs 配列)ので、意図に応じて使い分ける必要があります。- 既存コードが
returning: "id"をすでに使っていた場合は(ほぼ無い想定ですが)、今回の変更により戻り値の型が変わる可能性があります。テストがあれば検知できます。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58290
- 関連トピック:
- ActiveRecord
#insert/#exec_insertのpk引数の将来的な非推奨・削除の準備 - PostgreSQL アダプタにおける
INSERT ... RETURNINGとcurrvalフォールバックの共存戦略 - ActiveRecord での
returning:オプション活用パターン(単一値 vs 複数カラム)
- ActiveRecord
#58294 Allow to use ActiveModel#to_partial_path in a Ractor
マージ日: 2026/7/29 | 作成者: @Edouard-chin
- 概要 (1-2文で)
ActiveModel#to_partial_pathが Ractor 内から安全に呼び出せるように、返されるパス文字列の生成方法(およびメモ化の仕方)が Ractor フレンドリーになるよう修正した PRです。メモ化自体の Ractor 対応は別PRで行う前提ですが、本PRで少なくとも Ractor 内でのto_partial_path呼び出しが可能になります。
- 変更内容の詳細
何を直したか
ActiveModel#to_partial_path は通常、モデルインスタンスに対応するパーシャルのパスを文字列として返しますが、その内部実装で 文字列補間によって生成した文字列をメモ化 していました。
Ruby 3 の Ractor では、「Ractor間で共有されるオブジェクトは frozen であること」が要求されます。
今回の問題は:
- 文字列補間(
"#{...}")の結果が frozen ではない - その非 frozen な文字列をインスタンス変数にメモ化
- そのモデルオブジェクトを Ractor 間で共有・利用しようとするとエラー or Ractor 的に安全でない状態になる
という点でした。
このPRでは、文字列生成を Ractor で安全に扱えるように修正(=to_partial_path の戻り値が Ractor 的に問題にならないように)しています。
差分は activemodel/lib/active_model/conversion.rb に +2/-2 行と非常に小さく、おそらく以下のような変更が行われています(イメージ):
# 変更前(例)
@to_partial_path ||= "#{model_name.cache_key}"
# 変更後(例)
@to_partial_path ||= +"#{model_name.cache_key}" # あるいは .dup / .freeze など実際には、Ractor で共有可能な(frozen か、もしくは Ractor 内生成で共有しない)形になるような生成方法に変更されています。
PR本文からわかるのは:
- 「String interpolation doesn't return a frozen string so the memoized value can't be accessed inside a ractor. This patch fixes that.」
- =「文字列補間は frozen string を返さないので、メモ化された値は Ractor 内でアクセスできない → そこを直した」という趣旨
テスト
activemodel/test/cases/conversion_test.rb にテストが追加されています (+14行)。
内容的には、Ractor 内から to_partial_path を呼んでも例外が出ないことや、期待どおりのパスが返ることを検証していると考えられます。
- 影響範囲・注意点
- 対象:
ActiveModel::Conversionを include しているクラスの#to_partial_path- 典型的には Active Record モデル、もしくは ActiveModel 準拠オブジェクト
- 影響:
- Ractor を使うコードで
model.to_partial_pathを呼び出しても安全に動作するようになります。 - 通常の(Ractor を使わない)Rails アプリにとっては、挙動はこれまでと同じであり、互換性問題はほぼない想定です。
- Ractor を使うコードで
- 注意点:
- PR の NOTE にある通り「メモ化そのものの仕組みが Ractor 対応になったわけではない」点に注意が必要です。
- 例: 既にメイン Ractor でメモ化されたインスタンスを別 Ractor に渡す場合の扱いなど、より広い意味での Ractor 対応は別 PR で改めて行われる予定。
- Ractor を本格利用している場合は、この PR だけで全ての Ractor 絡みの問題が解決するとは限らず、後続 PR を追う必要があります。
- PR の NOTE にある通り「メモ化そのものの仕組みが Ractor 対応になったわけではない」点に注意が必要です。
- 参考情報 (あれば)
- 対象PR: https://github.com/rails/rails/pull/58294
- Ractor とオブジェクト共有の制約(Ruby公式ドキュメント):
- 「Ractor 間で共有できるのは frozen なオブジェクト、あるいは特定の型に限られる」という仕様が背景にあります。
- このPRは CHANGELOG 更新はしておらず、「比較的軽微なバグ修正」として扱われていることがチェックリストから読み取れます。
#57934 Separate <h2> through <h6> from neighbors in plain text conversion
マージ日: 2026/7/29 | 作成者: @55728
- 概要 (1-2文で)
このPRは、Action Textでリッチテキストをプレーンテキストに変換する際、<h1>だけでなく<h2>〜<h6>の見出しも前後に空行を挟んでブロック要素として扱うように修正したものです。これにより、見出しと本文がくっついて「Titlebody」のようになってしまう問題が解消されます。
- 変更内容の詳細 (サンプルコードを含む)
何が問題だったか
ActionText::Content#to_plain_text はHTMLをテキストに変換する際、<h1> はブロック扱い(前後に空行を入れる)をしていましたが、<h2>〜<h6> はインラインに近い扱いになっており、隣接コンテンツと連結されてしまっていました。
例:
ActionText::Content.new("<h2>Title</h2><div>body</div>").to_plain_text
# Before: "Titlebody"
# 見出しと本文がそのまま連結されてしまうMarkdown変換側では、すでに <h1>〜<h6> を見出しとして一貫して扱っており、プレーンテキスト変換との挙動不一致もありました。
何を修正したか
actiontext/lib/action_text/plain_text_conversion.rb 内で、HTMLのどのタグを「ブロック要素」として扱うかを定義している箇所に、h2〜h6 を追加しています。
イメージとしては以下のような変更です(擬似コード):
# 変更前(例)
BLOCK_TAGS = %w[
h1 p div br ...
]
# 変更後(例)
BLOCK_TAGS = %w[
h1 h2 h3 h4 h5 h6 p div br ...
]これにより、to_plain_text の変換結果は以下のようになります:
ActionText::Content.new("<h2>Title</h2><div>body</div>").to_plain_text
# After: "Title\n\nbody"つまり、見出し行のあとに空行(\n\n)が入るようになります。
テストの追加
actiontext/test/unit/plain_text_conversion_test.rb に、<h2>〜<h6> についてもブロックとして扱われることを確認するテストが追加されています。
テストではそれぞれのレベルの見出しについて、周囲のテキストと空行で区切られているかを検証していると考えられます。
- 影響範囲・注意点
影響範囲
ActionText::Content#to_plain_textを利用している箇所(例: メール本文のプレーンテキスト版生成、検索用インデックスのテキスト抽出、ログ出力など)で、<h2>〜<h6>を含むコンテンツのテキスト表現が変わります。- 具体的には、見出し行の前後に空行が追加されるため、テキストの改行位置や行数が変わります。
後方互換性の観点
- 内容そのもの(文字列の中身)は変わらず、「どこに改行が入るか」だけが変わります。
- ただし、以下のようなケースでは差分が発生する可能性があります:
- プレーンテキストの行数やフォーマットを前提にパースしている処理
- プレーンテキストのスナップショットをテスト期待値としてハードコードしているテスト
- これらの場合は、期待されるテキストフォーマットを新仕様に合わせて更新する必要があります。
挙動の一貫性
- Markdown変換では元々
<h1>〜<h6>を見出しとして扱っていたため、プレーンテキスト変換との整合性がとれるようになりました。 - HTML視点でも見出し要素はすべてブロックレベル要素なので、その意味でも自然な挙動です。
- Markdown変換では元々
- 参考情報 (あれば)
- 該当PR: https://github.com/rails/rails/pull/57934
- 関連API:
ActionText::Content#to_plain_textactiontext/lib/action_text/plain_text_conversion.rb(ブロック要素判定ロジック)
- 補足: プレーンテキスト変換を利用しているアプリケーションでは、主要な見出しタグ(
<h1>〜<h6>)を含むコンテンツの出力フォーマットが変わるため、テキスト出力を検証している箇所の再確認が推奨されます。
#58287 Fix Range#sum with a falsey initial value
マージ日: 2026/7/29 | 作成者: @Saidbek
- 概要 (1-2文で)
Range に対するsumメソッドで、初期値にnilやfalseのような “falsy” 値を渡したときの挙動が、Array/Enumerable と一致するように修正された PRです。これにより、Range#sumが初期値を 0 とみなして勝手に数値加算してしまう不整合な挙動が解消されています。
- 変更内容の詳細
何が問題だったか
Rails は Enumerable#sum を拡張しており、Range も Enumerable なので sum を使えますが、これまで Range に対する sum では「初期値が falsy のとき」に特別扱いされていました。
従来の挙動(Before):
(1..4).sum(nil) # => 10 # 実質的に sum(0) として扱われていた
(10..0).sum(nil) # => 0
(1..4).sum(false) # => 10一方で、Array に対しては次のような挙動をします:
[1,2,3,4].sum(nil) # => NoMethodError (nil に + を定義していないので)
[1,2,3,4].sum(false) # => NoMethodError
[].sum(nil) # => nil
[].sum(false) # => falseつまり Array と Range で挙動が食い違っていた のが問題でした。
修正後の挙動
この PR によって、Range に対する sum が Array/Enumerable の仕様に揃えられています。
修正後(After):
(1..4).sum(nil) # => NoMethodError ([1,2,3,4].sum(nil) と同じ)
(10..0).sum(nil) # => nil # 要素がないので初期値がそのまま返る
(1..4).sum(false) # => NoMethodError
(10..0).sum(false) # => false # 要素がないので初期値がそのまま返るポイント:
- Range が空でない場合
sum(nil)/sum(false)は、途中でnil + 1/false + 1のような計算になりNoMethodErrorになる(Array と同じ)。
- Range が空の場合
sum(initial)は、初期値をそのまま返す(これは Enumerable#sum の一般的仕様)。
実装面の変更(推測レベルの説明)
変更ファイルから推測できる内容:
activesupport/lib/active_support/core_ext/enumerable.rb- Range に対する
sumの最適化パス(おそらく「整数 Range のときに高速に足し合わせる処理」)で、nilやfalseを「0 とみなす」ような条件分岐が入っていたのを削除/修正したと考えられます。 - これにより、falsy 初期値であっても Enumerable 共通のロジックに従うようになったはずです。
- Range に対する
activesupport/test/core_ext/enumerable_test.rb- 上記「Before/After」のようなテストケースが追加され、Array と Range の結果が一致することを検証していると思われます。
activesupport/CHANGELOG.md- Active Support の挙動変更として「Range#sum の初期値が falsy な場合の挙動を修正した」旨が追記されています。
- 影響範囲・注意点
影響を受けるケース
Range#sumにnilやfalseを初期値として渡しているコード すべてが対象です。特に、以下のようなコードは振る舞いが変わります。
ruby(1..4).sum(nil) # 以前: 10 / これから: NoMethodError (1..4).sum(false) # 以前: 10 / これから: NoMethodError
互換性(Breaking Change)の可能性
以前は「たまたま動いていた」(初期値が 0 扱いされていた)コードが、今後は例外を投げる ようになります。
もし「Range が空かもしれないので初期値が欲しい」という意図で
nilやfalseを使っていた場合、Array と同様の仕様に合わせて、適切な初期値(0などの数値)を明示的に渡す必要があります。例:
ruby# こういう書き方をしていた場合 total = (start..finish).sum(nil) rescue nil # 期待動作ごとに修正を検討 total = (start..finish).sum(0) # 数値の合計が欲しい total = (start..finish).presence&.sum # そもそも空は nil にしたい 等
本来の Ruby 仕様/Enumerable 仕様との一貫性
- Ruby 本体の
Enumerable#sum仕様および Array の挙動と一致するようになったため、直感的かつ一貫性のある挙動になります。 - 新規コードでは Array と Range で同じ感覚で
sumを使えるようになるため、むしろバグを減らす方向の変更です。
- Ruby 本体の
- 参考情報 (あれば)
- 対象バージョン:
- この PR は Active Support(Rails)の変更であり、マージ日から見て Rails 7.x か 8 以降のいずれかのリリースに含まれる見込みです。実際にどのバージョンに入ったかは該当バージョンの CHANGELOG で確認してください。
- 関連仕様(Ruby 本体):
Enumerable#sumは「初期値が指定され、かつ要素が 1 つもない場合はその初期値を返す」「初期値にnilやfalseを渡してもそのまま使う(ので、演算子が定義されていなければ例外になる)」という仕様です。- この PR は、Active Support が提供する Range 用の最適化がこの一般仕様から外れていた点を修正するものと言えます。
#58241 Normalize mixed-case percent escapes in paths
マージ日: 2026/7/29 | 作成者: @afurm
- 概要 (1-2文で)
Rails のルーターが扱うパスの%エンコードを正規化する際、「%2f」「%2F」のように大小文字が混在した 16 進表記も含めて、大文字に統一するように修正した PR です。これにより、パスのパーセントエンコードが一貫して正規化され、テストでその挙動が担保されます。
- 変更内容の詳細
何をしているか
ActionDispatch::Journey::Router::Utils内の「パスの正規化処理」において、- これまで「完全に小文字の
%2fなど」は%2Fのように大文字化されていたが、 - 「
%2fや%2Fのような mixed-case(大小文字混在)」は正規化対象から漏れており、非正規化な形のまま残っていた。
- これまで「完全に小文字の
- この PR で、mixed-case の 16 進数エスケープも含め、パーセントエンコードされたバイト列を常に大文字に正規化するように修正しています。
変更行としては router/utils.rb の 1 行のロジック修正と、それをカバーするテスト (router/utils_test.rb) が 4 行追加された程度の小さな変更です。
挙動イメージ(擬似コード例)
(実際の実装とは正規表現などが多少異なる可能性がありますが、意図としては以下のような挙動になります。)
# 変更後のイメージ
def normalize_path(path)
path.gsub(/%[0-9A-Fa-f]{2}/) do |match|
match.upcase
end
end
normalize_path("/foo%2fbar") # => "/foo%2Fbar"
normalize_path("/foo%2Fbar") # => "/foo%2Fbar"
normalize_path("/foo%2a") # => "/foo%2A"- before: 小文字のみ (
%2f) だけが%2Fに正規化され、%2Fや%2aのような mixed-case が一貫して扱われないケースがあった。 - after:
%に続く 2 文字の 16 進数が いずれも大文字化 されるため、「全て%HH形式(H は大文字 16 進)」に統一される。
テストの追加
actionpack/test/journey/router/utils_test.rb に回帰テストが追加されています。
- mixed-case の
%エスケープを含むパスを入力し、出力がすべて大文字の%HH形式に揃っていることを確認するテストです。 - これにより、今後この仕様が崩れた場合にも検知できます。
- 影響範囲・注意点
影響範囲
- 対象: Action Pack のルーティング(
ActionDispatch::Journey)で扱う URL パス。 - 具体的には:
- Rack レベルで渡ってくる
PATH_INFOにパーセントエンコードが含まれている場合、 - Rails がそれをルーティング時に解釈・マッチングするときの 正規化の一貫性が向上 します。
- Rack レベルで渡ってくる
互換性・既存コードへの影響
- 変更は「見た目のパス文字列」(特に
%部分)の大文字/小文字の揺れをなくすのみで、デコード結果(実際の文字)は変わりません。 - 通常は後方互換性が高い変更と考えられますが、以下のような場合は影響し得ます:
- アプリ側で「パーセントエンコードされた文字列をそのまま比較」している場合
- 例:
request.pathをそのまま文字列比較しており、"/foo%2fbar"と"/foo%2Fbar"を区別していたようなロジック。 - この PR により、
request.path(あるいはその前後の処理)が常に%2Fのような大文字表現側に寄る可能性があります。
- 例:
- ログ・監査用途で「受信した URL の生の表記」を保持したい場合
- ルーティング前後でパスの文字列表現が多少変わる(エスケープ部分のみ大文字化)ため、「到着時の生パス」を正確に保ちたい場合は Rack 環境変数から直接取るなどの工夫が必要です。
- アプリ側で「パーセントエンコードされた文字列をそのまま比較」している場合
それ以外の一般的なユースケースでは、
- ルート定義やコントローラの動作、URL helpers の出力などに対する機能的な影響はほぼ無いと考えられますが、
- ルーター周辺でのカスタムミドルウェアやパス正規化処理を独自に書いている場合は、一度
%エンコードの取り扱いを確認しておくと安心です。
- 参考情報 (あれば)
- 対応 PR: https://github.com/rails/rails/pull/58241
- 関連コンポーネント:
ActionDispatch::Journey::Router::Utils(Action Pack / ルーティング) - 背景となる一般的な仕様:
- URL のパーセントエンコードでは
%HH(H は 16 進数)の大小文字は本来区別されませんが、正規化(canonicalization) の一環として大文字に統一するのが一般的です。 - この PR はその慣習に合わせ、mixed-case を含む全ての
%xxを%XXに統一するための修正です。
- URL のパーセントエンコードでは
#58280 Resolve the implicit template with a single lookup
マージ日: 2026/7/29 | 作成者: @etiennebarrie
- 概要 (1-2文で)
default_renderが暗黙テンプレートを探す際のテンプレート探索を「1回の lookup だけ」に統一し、同じテンプレートを二重に探索しないよう最適化するとともに、variants 周りで 406 が誤って返されうる不整合を解消する変更です。
- 変更内容の詳細
背景: これまでの動き
ActionController::Metal::ImplicitRender の default_render は、アクション内で render が呼ばれなかった場合に「暗黙的に」テンプレートを描画する処理です。概ね以下のような流れでした:
template_exists?(action_name, ...)でテンプレートの存在をチェック- 存在すれば
renderを呼ぶ
ここでrender内部はdetermine_templateを通じて再度テンプレートを解決します
つまり、同じアクションテンプレートを 1 回のレスポンスで 2 回 lookup していた、という無駄がありました。
また、この「存在チェック」と「実際のレンダリング」が別々の経路でテンプレート解決を行うため、
lookup_context.variantsは設定されている- しかし
request.variantは設定されていない
という状況で挙動の差異が発生していました。
template_exists?側はlookup_context.variantsを「上書き」する形の挙動となり、
該当するテンプレートがあるのに無いと判断されるケースがあった- その結果、暗黙レンダリングでは 406 (Not Acceptable) を返すのに、
同じアクション内でのrender明示呼び出しなら問題なくそのテンプレートを返す
という不整合が発生していました。
今回の変更内容
PR の概要にある通り、
Find it once with
lookup_context.findand pass the resolvedTemplatetorender, which uses it as-is.
という方針に修正されています。
コード上では:
template_exists?+renderという 2 段構えをやめるlookup_context.findを使って「実際に使うTemplateオブジェクト」を 1 回の lookup で取得- その
Templateをそのままrender template: resolved_templateのような形で渡す(determine_templateなどによる再解決をスキップ)
という形になっています。
これにより:
- 暗黙レンダリング時のテンプレート解決が 1 パスで完結
lookup_context.variantsが正しく尊重されるrequest.variantが未設定でも、lookup_context側の設定に基づき、
明示的renderと暗黙default_renderの挙動が一致
するようになっています。
- 影響範囲・注意点
- パフォーマンス面:
- 同じアクションテンプレートを 2 回 lookup していた箇所が 1 回に減るため、
テンプレート探索コストがわずかに削減されます(特に複雑な view path / variants / formats を多用しているアプリで効果あり)。
- 同じアクションテンプレートを 2 回 lookup していた箇所が 1 回に減るため、
- 挙動の変更点(互換性に関係しうるもの):
lookup_context.variantsを直接いじっている(かつrequest.variantを使っていない)アプリで、- これまで暗黙レンダリング(
renderを書いていないアクション)では 406 が返っていた - 明示的に
renderを呼ぶとテンプレートが返っていた という差異があった場合、その差異が解消され「どちらもテンプレートが返る」ようになります。
- これまで暗黙レンダリング(
- これは本来意図された一貫した挙動への修正であり、多くの場合はバグ修正として好ましい変更です。
- カスタムレンダリング / メタプログラミング:
default_renderの内部挙動に直接依存するようなメタプログラミングをしている場合
(例:template_exists?の呼び出し回数をフックして何かしている、など)には挙動が変わる可能性があります。- 通常の controller/view の使い方をしている範囲では、既存コードへの悪影響はほぼありません。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58280
- 関連するコンセプト:
ActionView::LookupContextと variants (lookup_context.variants,request.variant)ActionController::Metal::ImplicitRender#default_rendertemplate_exists?/lookup_context.find/determine_templateの関係
この変更は、**「暗黙レンダリングと明示的 render のテンプレート解決を同一ロジックに揃えた」**と理解すると把握しやすいです。
#58279 Resolve the single best template without sorting the whole set
マージ日: 2026/7/29 | 作成者: @etiennebarrie
- 概要 (1-2文で)
テンプレート解決(Resolver#find)のアルゴリズムを見直し、「候補を全部集めてソート」せずに1パスで最適なテンプレートを選ぶようにしたパフォーマンス改善のPRです。find/exists?は最初に見つかった最適テンプレートで即終了しつつ、find_all(ActionMailerのmultipartなど)は従来どおりマッチした全テンプレートを返す挙動を維持しています。
- 変更内容の詳細
2-1. これまでの挙動(概念的な流れ)
従来の Resolver#find は、おおよそ次のような処理をしていました:
- 対象パスに対する候補テンプレートをすべて列挙(
find_all) - 各候補について:
- マッチ判定(フォーマット・ハンドラ・locale など)
- ソート用のキーを生成(優先度計算)
- それらをソートし、最初のものを「ベストマッチ」として選択
- 選ばれたテンプレートに対して locals のバインド等を行う
つまり「フィルタ → ソート → 先頭を採用」という形で、候補数が多いほどオーバーヘッドが大きくなっていました。
2-2. 新しい挙動の概要
このPRでは、テンプレートの「マッチ判定」と「優先度付け」を 1回の走査(1パス)で済ませるようにしています。
ポイントは:
TemplateDetails#rank_forが導入・拡張され、matches?(このテンプレートはリクエスト条件にマッチするか)sort_key_for(どれくらい良いマッチかを示すソートキー)
を融合した「スコア付け+フィルタリング」の役割を担うようになった。
Resolver#findは、候補集合をソートせずに、「今までのベスト」より良いテンプレートが見つかるたびに更新していく形で1パス評価を行う。- 候補が1つしかない場合は、ランキング処理すら行わず、シンプルな
matches?チェックだけで決める。
これにより、
- ソート用の配列構築や全候補への locals バインドが不要になり、
- 大量のテンプレート候補がある場合でもオーバーヘッドを抑えられます。
2-3. Resolver#find / PathSet#find / PathSet#exists? の挙動
Resolver#find
- 以前は(実装上)
find_allでマッチしたテンプレートをすべて集め、その中から1つを選ぶような流れでした。 - 新実装では、「1テンプレートだけ返す」専用の高速パスを持ち、1パスでベストテンプレートを決定します。
- ただし、カスタム resolver との互換性維持のため:
- デフォルト実装の
Resolver#findは、外部インターフェースとしては依然としてfind_all経由で動作するパスも確保されています。 - 「
find_allをオーバーライドしているカスタム resolver」は、挙動を変えずに引き続き動作します。
- デフォルト実装の
PathSet#find
ActionView::PathSet#findは、登録された複数の resolver を順に問い合わせますが、
今回の変更で「各 resolver に対して最初から 1テンプレートだけを求める」形になっています。- 最初にベストマッチが見つかった時点で探索を打ち切るため、不要な resolver の呼び出しが減る場合があります。
PathSet#exists?
PathSet#exists?は、最初にヒットした resolver でtrueを返して即終了するように明示的に最適化されました。- これは以前もほぼ同様の意図でしたが、内部的に
find/find_all経由でフルセットを構築するようなパスがあり得たのが、
今回の1パス設計でよりストレートに「最初のヒットで終わる」挙動になっています。
2-4. find_all は従来どおり「全候補」を返す
PathSet#find_allは引き続き、全ての resolver から「条件にマッチするテンプレートの配列」を集め、マージして返します。- ActionMailer の multipart メール生成など、「1つではなく複数テンプレート(text + html など)」が必要なケースのために、この挙動は維持されています。
- 内部的には、
findが「1件だけ返す」高速パスで最適化された一方、find_allは「ソートやマージを含めたフルセット生成」を行うままです。
2-5. TemplateDetails#rank_for の導入・改善
TemplateDetails はテンプレートの属性(フォーマット、variant、locale、handler など)をもとに、
リクエストされた詳細(要求されるフォーマット・locale など)とのマッチ度を評価するクラスです。
このPRで:
rank_forメソッドが新設 or 拡張され、以下を1つのメソッドにまとめています:- マッチしているかどうか(
nilor false なら不採用) - マッチしている場合、その「良さ」を表すランク(数値やタプル等)
- マッチしているかどうか(
Resolver#findは、候補ごとにrank_forを呼び出し、nil→ マッチしないのでスキップ- 非nil → ランク値を取得し、現在のベストと比較してベターなら差し替え というロジックのみでベストテンプレートを選択します。
- 候補が1件だけのときは、
rank_forを経由せず簡易なmatches?チェックで済ませる最短パスが入っています。
※具体的なコードは省略されていますが、概念的には以下のような処理になります:
best_template = nil
best_rank = nil
candidates.each do |template|
rank = template.details.rank_for(requested_details)
next unless rank
if best_rank.nil? || rank_better_than?(rank, best_rank)
best_template = template
best_rank = rank
end
end
best_template- 影響範囲・注意点
3-1. 性能面の影響
- 多数のテンプレート候補が存在するビュー階層(多言語・多variant・複数フォーマットなど)で、
テンプレート解決のコストが下がることが期待できます。 - 特に以下のようなケースで恩恵がある可能性が高いです:
renderが頻繁に呼ばれ、かつ template cache が効きにくいケース- 1つの仮想パス(例:
"users/show")に対して、多数の_users/show.*テンプレートが存在するアプリ
3-2. 互換性(カスタム Resolver)
- このPRは「Custom resolvers that override find_all keep working」と明記しています。
- つまり、
MyResolver < ActionView::Resolverのようなクラスでfind_allをオーバーライドしている場合、
その実装に依存するアプリは挙動を変えずに動作し続けることを目指しています。
- つまり、
- ただし:
- もし独自に
findをオーバーライドし、独特の挙動(「常に配列を返す」など)をしていた場合は、
Rails 本体側の前提(findは単一テンプレートを返す)とのギャップが顕在化する可能性があります。 - 独自 resolver を持つアプリでは、このPRを含むバージョンに上げる際に
find/find_allの戻り値の型・意味- マッチング・ランキングロジック
を念のため確認しておくと安全です。
- もし独自に
3-3. 行動の変化があり得る微妙なケース
- 理論上は「既存のソートロジックを rank_for で再現している」ため、
ベストテンプレートの選択結果は変わらないことが期待されます。 - しかし、以前の実装が「安定ソート」や「同点時の決め方」に暗黙的な依存をしていた場合、
非常にレアなパターンで「どのテンプレートが選ばれるか」が変わる可能性があります。- 例: 同じ rank を持つテンプレートが複数存在し、かつその順序に依存していた場合。
- ほとんどのアプリでは問題にならないはずですが、
レンダリング結果の違いに極めて敏感なテストを持つ場合は、マイグレーション時に snapshot / regression テストを回して確認しておくと安心です。
- 参考情報 (あれば)
- 対象PR:
- タイトル: Resolve the single best template without sorting the whole set
- 番号: #58279
- 作成者: etiennebarrie
- マージ日時: 2026-07-29T05:25:02Z
- 関連クラス・モジュール:
ActionView::Template::ResolverActionView::PathSetActionView::TemplateDetails
- 関連するユースケース:
- ビューのテンプレート探索・レンダリング
- ActionMailer の multipart メール生成(
find_all維持) - 多言語対応・variant 対応ビューのパフォーマンス最適化
#58237 Fix Encoding::CompatibilityError in template digests.
マージ日: 2026/7/29 | 作成者: @Edilbek
- 概要 (1-2文で)
Rails のテンプレート依存関係トラッキング(DependencyTracker::RubyTracker)で、ファイルベースの Haml など非 ASCII テキストを含むテンプレートのダイジェスト計算時に発生していたEncoding::CompatibilityErrorを解消する修正です。RubyTrackerがテンプレートを処理する際に、通常のコンパイル時と同様にエンコーディングを揃えてからハンドラに渡すように変更されています。
- 変更内容の詳細
問題の背景
- Rails のビューキャッシュ(フラグメントキャッシュ)ではテンプレートの「ダイジェスト」を計算し、その内容と依存関係を見てキャッシュキーを生成します。
- ファイルベースのテンプレートは
File.binreadで読み込まれるため、デフォルトではASCII-8BIT(バイナリ) エンコーディングになります。 - 通常のテンプレートコンパイルフローでは、
Template#encode!を通して適切なエンコーディング(通常は UTF-8)に変換した後でテンプレートハンドラ(ERB, Haml など)に渡しています。 - しかし
DependencyTracker::RubyTrackerだけは「生のtemplate.source」(ASCII-8BIT のまま)をハンドラに渡しており、Haml/Temple 系のハンドラが「UTF-8 な静的文字列との連結」を行うとEncoding::CompatibilityErrorが発生していました。
→ 結果として、フラグメントキャッシュのダイジェスト計算中に 500 エラーになるケースがありました。
具体的な修正内容
RubyTracker がテンプレートハンドラに渡すソースを、通常のコンパイルと同じく template.encode! 済みのものに変更しています。
差分概要(意訳):
# 変更前(概念イメージ)
handler.call(template.source)
# 変更後(概念イメージ)
handler.call(template.encode!)これにより、
- ダイジェスト計算時(依存関係トラッキング時)も、
- 通常のレンダリング時(
Template#compiled_source)も、
同一のエンコーディング済みソースがハンドラに渡されるようになり、Haml/Temple のように「UTF-8 静的文字列を結合」する処理でもエラーにならないようになります。
テスト追加
actionview/test/template/dependency_tracker_test.rb に回帰試験が追加されています。
- Haml を直接依存に入れずに、Temple の「静的マージャー」に似た挙動を模倣する形でテストしている、という説明が PR にあります。
- テスト内容としては概ね、
- 非 ASCII 文字(例: 日本語、アクセント付き文字など)を含むテンプレート
- かつ
renderを呼び出して依存関係解析が走るケース - それに対して
DependencyTracker::RubyTrackerがエラーを出さずにダイジェスト計算できること を検証するものになっています。
- 影響範囲・注意点
影響範囲
- 影響を受けるケース:
- ファイルベーステンプレート(特に Haml/Temple ベースのテンプレート)
- 非 ASCII 文字を含む
- テンプレート内で
renderなどを呼び出し、DependencyTracker::RubyTrackerが依存パースを行う - かつフラグメントキャッシュやテンプレートダイジェスト計算が走る場合
- この PR により、上記条件下で発生していた
Encoding::CompatibilityErrorに起因する 500 エラーが解消されます。
互換性・リスク
RubyTrackerが扱うテンプレートソースのエンコーディングが、従来の「生の ASCII-8BIT」から「Template#encode!済み(通常 UTF-8)」に揃えられるだけであり、通常は望ましい挙動の統一です。- テンプレートハンドラ側で「ASCII-8BIT を前提にした非常に特殊な処理」をしている場合には、挙動が変わる可能性がありますが、Rails の標準的な ERB/Haml/Temple 系ハンドラにおいては、むしろこちらが正しい前提(UTF-8)です。
- 既存アプリケーションで、非 ASCII 含むテンプレートのキャッシュ周りで sporadic に発生していた
Encoding::CompatibilityErrorがなくなり、安定性が向上します。
運用上の注意
- もし独自テンプレートハンドラを実装していて
DependencyTracker::RubyTracker用のコードを持っている場合は、「引数として渡ってくるテンプレートソースは UTF-8 でエンコード済み」という前提で実装されているかを確認すると安全です。 - 以前にこの種のエラーを回避するために「無理矢理
force_encoding("UTF-8")」などをテンプレートハンドラ側で行っていた場合、今後は二重にエンコード操作されていないか注意しておくとよいです(多くの場合は実害はありませんが、ダブル処理の余計さは残ります)。
- 参考情報 (あれば)
- 対応 Issue: #58203
→ 非 ASCII テキストを含む Haml テンプレート +render+ キャッシュダイジェスト計算でのEncoding::CompatibilityError報告。 - 類似のバグ: #56904 / #56906
→ 同じくFile.binreadによるASCII-8BIT読み込みが原因となったエンコーディング関連の不具合群。 - 関連コード:
actionview/lib/action_view/template.rb(Template#encode!やcompiled_source周り)actionview/lib/action_view/dependency_tracker/ruby_tracker.rb(Ruby ベースビューの依存関係トラッキング)
#58278 Fix LazyRouteSet thrashing when url_helpers are included into Object
マージ日: 2026/7/29 | 作成者: @chaadow
- 概要 (1-2文で)
Rails の LazyRouteSet が、url_helpersをObjectに include した場合にほぼ全てのメソッド問い合わせでルート再読み込みを試みてしまう問題を修正し、*_path/*_urlだけを遅延ロードのトリガーに限定する変更です。これにより、大規模アプリでコンソールやスクリプト実行時に見える「ハング」のような挙動を防ぎます。
- 変更内容の詳細
背景
- #52353 で
LazyRouteSetにmethod_missing/respond_to_missing?を追加し、
未定義メソッドが呼ばれたときにreload_routes_unless_loadedを呼ぶことで、
最初のルート描画を「遅延ロード」できるようにしました。 - これは
posts_path/posts_urlなどの named route helper 用には正しい動きですが、Rails.application.routes.url_helpersをObjectに include すると、
「あらゆるオブジェクト」にこれらのフックが入り込みます。
include Rails.application.routes.url_helpers # Object 直下などで実行
[].respond_to?(:to_ary)
{}.respond_to?(:to_hash)のような、Ruby が内部で頻繁に呼ぶ respond_to? チェックがすべて LazyRouteSet 経由になり、
ルート描画中にも再度 reload_routes_unless_loaded が呼ばれ続ける、という再入問題が起きていました。
大規模アプリではこれが「固まったように見える」原因になります。
今回の修正ポイント
方針:
「ルート遅延ロードの対象となるメソッドを、*_path / *_url に限定する」
LazyRouteSet の method_missing / respond_to_missing? を次のような条件付きにしています:
- 対象とするメソッド名は、
_pathまたは_urlで終わるもののみ - それ以外のメソッドは、ルートの遅延ロードを試さず、素通りする(= 既存実装も呼ばない)
疑似コードイメージ:
def method_missing(name, *args, &block)
if name.to_s.end_with?("_path", "_url")
reload_routes_unless_loaded
end
super
end
def respond_to_missing?(name, include_private = false)
if name.to_s.end_with?("_path", "_url")
reload_routes_unless_loaded
end
super
end実際の diff では、上記のような「メソッド名のサフィックスチェック」を追加した上で、reload_routes_unless_loaded を呼ぶかどうかを分岐しています。
テスト追加
railties/test/engine/lazy_route_set_test.rb に以下のようなテストが追加されています。
_path/_urlで終わらないメソッド (to_ary,to_hashなど) についてはreload_routes_unless_loadedが呼ばれないこと_path/_urlの場合には、従来通り遅延ロードがトリガーされること
これにより、regression(再発)を防ぎます。
- 影響範囲・注意点
影響を受けるケース
Rails.application.routes.url_helpersをObjectや非常に広いスコープに include しているコード- 特に Rails console やスクリプトなどで、トップレベルに
include ...url_helpersを書いている場合- 以前は
respond_to?(:to_ary)などの内部的な呼び出しが全てルート再読み込みを試みており、
大規模アプリではほぼフリーズに見える挙動になり得ました。 - この PR により、そのような「内部メソッド問い合わせ」は遅延ロードの対象外になります。
- 以前は
影響を受けない/動作が変わらない部分
- 通常の named route helper (
users_path,root_urlなど) の挙動- これらは引き続き、最初の呼び出し時に LazyRouteSet がルートを描画します。
url_helpersをコントローラや view helper に include している一般的なパターン- Rails が推奨するスコープでの利用は、今回の修正の有無に関わらずほぼ同じ動きです。
- 通常の named route helper (
考慮点
- 今後、
*_path/*_url以外の名前でルーティング関連のヘルパーを自動生成したい、
という要件が出た場合、この「サフィックス前提」のロジックと整合性を取る必要があります。 - とはいえ Rails の慣習上、named routes は
*_path/*_url固定なので、現実的には問題になりにくいです。
- 今後、
- 参考情報 (あれば)
- 本 PR: #58278
- 関連 PR:
- #52353 — LazyRouteSet に
method_missing/respond_to_missing?を導入した最初の変更 - #58252 —
after_routes_loaded周りの無限 redraw 問題を修正した前回の follow-up
- #52353 — LazyRouteSet に
- 問題の根本:
- Ruby がコアで多用する
respond_to?(:to_ary)/respond_to?(:to_hash)などがurl_helpersをObjectに混ぜ込むことでフック対象となり、
さらに LazyRouteSet がルートロード中にもフックされて再入 → スレッジング・ハングに見える - 今回の修正は、ルートに関係ある可能性が高い「named route helper らしい名前」だけを
遅延ロードのトリガーにすることで、この再入パスを閉じています。
- Ruby がコアで多用する
#57854 Make Controller middlewares ractor safe
マージ日: 2026/7/29 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Rails のコントローラ専用ミドルウェアスタック(Rack ミドルウェアではない)を Ractor セーフにするため、コピーオンライト(Copy-on-Write)方式で共有可能オブジェクトとして扱えるようにする変更です。use経由だけでなく、スタックへの直接アクセスにも対応するためのプロキシ層が導入されています。
- 変更内容の詳細
背景と問題点
- 対象は
ActionController::Metalが持つ「コントローラミドルウェアスタック」で、config.middlewareなどの Rack ミドルウェアスタックとは別物です。 - Ractor セーフにするには、Ractor 間で共有されるオブジェクトが「freeze されていて、かつ内部的にも変更されない(shareable)」必要があります。
- 従来の実装では:
- コントローラクラスがクラスメソッド
useを呼んでミドルウェアを積み上げる。 - もしくは
middleware_stack(あるいは類似メソッド)を直接取得してinsert,deleteなどのミューテーションを行う。
- コントローラクラスがクラスメソッド
- このため:
- 一度 Ractor 間で共有した後にスタックを変更されると、Ractor セーフでなくなる。
- かといって無制限に変更禁止にすると、従来の「クラスレベルでスタックをいじる」書き方が破壊される。
対応方針: Copy-on-Write + Proxy
PRの説明によると、次のような戦略が取られています。
Copy-on-Write でのスタック管理
- 「ある時点で freeze 済みのミドルウェアスタック」を Ractor 共有用のベースとして扱い、変更要求が来たタイミングで:
- そのスタックを複製(コピー)して
- 複製側に対してミューテーションを行い
- 再度 freeze して shareable な状態に戻す
- こうすることで、「読み取り専用で共有しつつ、変更が必要なときだけコピーを作る」動作になり、Ractor の要求を満たします。
- 「ある時点で freeze 済みのミドルウェアスタック」を Ractor 共有用のベースとして扱い、変更要求が来たタイミングで:
直接スタックに触るケース向けの Proxy 導入
- 現在の Rails では、次の2パターンが存在します:rubyと
class ApplicationController < ActionController::Base use SomeMiddleware endrubyclass ApplicationController < ActionController::Base middleware_stack.insert_before(0, SomeMiddleware) end - 前者 (
use) はMiddlewareStackをラップしたメソッドなので、ここに Copy-on-Write ロジックを簡単に仕込めます。 - 後者のように「スタックオブジェクト自体」を返してしまっている場合、呼び出し側がどんなメソッドを呼ぶかわからないので、スタックを返す代わりに Proxy オブジェクトを返し、その Proxy 経由でミューテーションが発生したら毎回「コピーして再 freeze」する、というデザインになっています。
- Proxy はざっくりいうと:
- 未変更時は freeze 済みの共有スタックを指している
- 変更操作(
use,insert,insert_before,delete, などスタックを書き換えうるメソッド)が呼ばれたら:- 内部でスタックを dup(コピー)
- dup した側に対して本来のメソッドを実行
- 変更が終わったらスタックを再度 freeze
- 非変更系のメソッド(
each,to_aなど読み取り専用)はそのまま委譲
- 現在の Rails では、次の2パターンが存在します:
テスト (actionpack/test/controller/new_base/middleware_test.rb)
- 新しいテストは、おそらく以下の観点をカバーしています:
useでミドルウェアを追加してもスタックが期待どおりに構築される。- スタックを直接取得して操作した場合でも、内部的に Proxy 経由で Copy-on-Write が働く。
- 操作後もスタックが
frozen?であり、Ractor 共有可能な状態になっている(もしくは、Ractor 関連のエラーが出ない)。
- 具体的なテストコードは PR 情報からは見えませんが、
new_base/middleware_test.rbに 22 行追加されており、「新しい Proxy 実装が従来と同じ API を維持していること」「Ractor 対策で regress していないこと」を保証する内容になっているはずです。
- 新しいテストは、おそらく以下の観点をカバーしています:
action_controller/metal.rb の主な変更イメージ
実際のコードは抜粋されていませんが、典型的には次のような変更が入っていると想定されます(擬似コード):
module ActionController
class Metal
class << self
def middleware
@middleware_proxy ||= MiddlewareProxy.new(@_middleware_stack)
end
def use(*args, &block)
middleware.use(*args, &block)
end
# もしくは middleware_stack メソッドを proxy 返すようにしている可能性もある
def middleware_stack
middleware.stack
end
end
end
class MiddlewareProxy
def initialize(stack)
@stack = stack
@stack.freeze
end
def method_missing(name, *args, &block)
if mutating_method?(name)
# Copy-on-Write
@stack = @stack.dup
@stack.public_send(name, *args, &block)
@stack.freeze
else
@stack.public_send(name, *args, &block)
end
end
def respond_to_missing?(name, include_private = false)
@stack.respond_to?(name, include_private) || super
end
end
end※ 実装名や構造は異なる可能性がありますが、PR 説明文から読み取れる方針はこのようなものです。
- 影響範囲・注意点
影響範囲
ActionController::Metal(およびこれを継承するActionController::Base系)で定義される「コントローラ固有ミドルウェア」に影響します。MyController.use(SomeMiddleware)あるいはMyController.middleware_stack/middlewareなどの API を使っているコードが対象です。- Rack ミドルウェアスタック(
Rails.application.config.middlewareなど)には直接の影響はありません。
後方互換性
- 既存の API (
use,middleware_stack経由の操作) は維持される想定です。 - Proxy は
method_missing/ 委譲型で設計されているはずなので、ほとんどのコードは変更なしで動作します。 - ただし、以下のようなケースでは注意が必要です:
middleware_stackのクラスやclass.nameに直接依存しているようなメタプログラミング(Proxy クラスに変わる可能性)。object_idの恒常性を前提にしているような処理(Copy-on-Write により内部スタックが差し替わる)。
- 既存の API (
パフォーマンス面
- Copy-on-Write により、「スタック変更が発生した瞬間」に限って
dup+freezeのコストが発生します。 - コントローラクラスのミドルウェアスタックは通常「アプリ起動時・クラス定義時に一度構築して以後ほとんど触らない」ことが多いため、ランタイムリクエスト処理への影響は軽微と見込まれます。
- 動的にミドルウェアを差し替えるようなメタプログラミングを多用している場合は、変更頻度に応じてオーバーヘッドが増えます。
- Copy-on-Write により、「スタック変更が発生した瞬間」に限って
Ractor 利用時のメリット
- この変更により、コントローラとそのミドルウェアスタックを Ractor 間で安全に共有できるようになります。
- Rails をマルチ Ractor 環境で動かしたい場合の前提条件の一部が整備された形です。
- 参考情報 (あれば)
- Ractor と shareable オブジェクトの制約について(Ruby 本体のドキュメント)
- Rails におけるコントローラミドルウェア (
ActionController::Metal#useまわり) - 関連しそうなトピック:
- Rails の Ractor 対応状況全般(Active Record・Action Pack などの「グローバル状態」をどう扱うか)
- 他の Ractor 対応 PR でのパターン: 「Copy-on-Write」「Proxy 経由の freeze / dup 管理」など
#58285 Simplify insert callers to only pass what they need
マージ日: 2026/7/29 | 作成者: @kamipo
- 概要 (1-2文で)
connection.insertの呼び出し側から不要な位置引数(pk,id_value,sequence_name)を削除し、insert/sql_for_insert周りのインターフェイスと挙動を簡素化・最適化した変更です。あわせて、schema_migration.create_versionなど一部の API の挙動も見直し、無駄な DB ラウンドトリップやコメントを削減しています。
- 変更内容の詳細
2-1. connection.insert 呼び出しの簡素化
対象:
ActiveRecord::Persistence#_insert_recordActiveRecord::InternalMetadata.create_entryActiveRecord::SchemaMigration.create_version
これらが connection.insert に渡していた以下の位置引数が削除されました。
pkid_valuesequence_name
元々の insert シグネチャ(概念的には):
connection.insert(
arel_or_sql,
"SQL label",
pk = nil,
id_value = nil,
sequence_name = nil,
binds = [],
returning: nil
)今回の変更で、上記呼び出し側は 必要な情報だけ を渡すように整理されています。
_insert_record の整理点
説明にある通り:
_insert_recordは常にreturning:オプションを指定しており、戻り値としての id はreturning_column_valuesから取得しています。- すでに「事前に採番した主キー値」は
values[primary_key]に埋め込んで insert しているため、同じ値をid_value引数で別途渡す必要はありませんでした。
そのため _insert_record 内の connection.insert 呼び出しは、ざっくり次のような形になったと考えられます(擬似コード):
# 変更前(イメージ)
connection.insert(
arel,
"SQL",
primary_key,
id_value,
sequence_name,
binds,
returning: returning_columns
)
# 変更後(イメージ)
connection.insert(
arel,
"SQL",
binds,
returning: returning_columns
)結果として、_insert_record の内部は:
- 重複した情報(
id_value)の二重管理がなくなる insertのシグネチャ依存度が下がり、インターフェイスがすっきりする
という改善が入っています。
internal_metadata.create_entry
internal_metadata.create_entryはinsertの 戻り値を全く使っていなかった ため、pk/id_value/sequence_nameは完全に不要でした。- 今回それらの引数指定を削除し、必要最小限の呼び出しに変更。
schema_migration.create_version
ここが比較的大きめのロジック変更です。
これまで:
schema_migration.create_versionは、sql_for_insertが
「pkとreturningが両方nilのとき、自動的にRETURNING <pk>を SQL に付け足す」
という仕様に依存していました。- つまり、わざと
pk/returningを指定せずに呼び出し、DB 側から主キー値を返させる前提でした。
しかし:
schema_migrationにおける version は、呼び出し元が すでに知っている値(マイグレーション番号)です。- そのため、DB に「version いくつを insert した?」と確認するラウンドトリップ自体が不要です。
今回の変更:
create_versionはcreate_versionsに素直に委譲し、引数で受け取った version をそのまま返すだけに変更。- これにより、
- 「このメソッドは
create_versionsに委譲できない」というコメントは不要になり削除。 - 無駄な
RETURNINGベースの round trip を削減。
- 「このメソッドは
擬似コードイメージ:
# 変更前(イメージ)
def create_version(version)
connection.insert(
sql_for_insert(..., pk: nil, returning: nil), # => 自動で RETURNING <pk>
"SQL"
)
end
# 変更後(イメージ)
def create_version(version)
create_versions([version])
version
end2-2. sql_for_insert の挙動の引き締め
sql_for_insert では、これまで:
pkがnilかつreturningがnilのときに- スキーマキャッシュから
schema_cache.primary_keys(table_name)を引いて - 自動的に
RETURNING <pk>を付与する
- スキーマキャッシュから
という 主キー自動検出 + RETURNING 自動付与 を行っていました。
今回の修正では:
returningが明示的に指定されている場合は、主キー自動検出を行わない ように変更されています。
これにより:
returning:を自前で制御している呼び出し側に対して、- 余計な
schema_cache.primary_keysアクセスが発生しない - 挙動がより「書いたとおり」になり、暗黙の
RETURNING振る舞いに依存しなくなる
- 余計な
といったメリットがあります。
- 影響範囲・注意点
3-1. Rails 内部 API への影響
対象となるのは主に Rails 内部コードの呼び出し です:
_insert_recordinternal_metadata.create_entryschema_migration.create_versionsql_for_insertのreturning関連挙動
通常のアプリケーションコードが直接影響を受けるのは:
connection.insertを独自に呼び出している場合
特に、pk/id_value/sequence_nameを位置引数で渡しているようなコードは、
今後の Rails バージョンで互換性が崩れる可能性があります。- Rails の公開 API としてどこまで保証しているかにもよりますが、
この PR からは「pk/id_valueに依存した戻り値取得」は推奨されない方向が伺えます。
- Rails の公開 API としてどこまで保証しているかにもよりますが、
sql_for_insertをオーバーライド/直接利用しているアダプタ・拡張コードreturningを明示的に指定している場合に、これまで暗黙に行われていた主キー自動検出が走らなくなるので、
「pkが自動で拾われる前提」の独自コードがあると挙動が変わる可能性があります。
3-2. パフォーマンス上の影響
sql_for_insertでreturning:が明示されているケースでは、
余分なschema_cache.primary_keys呼び出しがなくなるため、微小ながらパフォーマンス改善が見込まれます。schema_migration.create_versionが DB から version を取り直さなくなることで、
マイグレーション実行時の無駄な round trip が 1 回分減ります(特に大量マイグレーション時にはわずかに効く可能性があります)。
3-3. マイグレーション API の利用者への影響
ActiveRecord::SchemaMigration.create_version を直接叩いているアプリケーションは少ないと思われますが、いた場合:
- これまで戻り値として「DB に挿入されたレコードの主キー値」を期待していたコードは、
今後は「引数で渡した version がそのまま返ってくる」という前提に合わせる必要があります。
ただし、通常利用(rails db:migrate 経由)では挙動は体感的に変わりません。
- 参考情報 (あれば)
- PR本体: https://github.com/rails/rails/pull/58285
- 関連しそうな内部 API:
ActiveRecord::ConnectionAdapters::DatabaseStatements#insertActiveRecord::Persistence#_insert_recordActiveRecord::SchemaMigrationActiveRecord::InternalMetadata
- もし自作アダプタや AR 拡張で
insert/sql_for_insertを直接扱っている場合は、
この PR の差分を参照して自分のコードが「pk/id_valueに暗黙依存していないか」「returning指定時の挙動を当てにしていないか」を確認すると安全です。
#57981 Only swallow the LoadError we intended to
マージ日: 2026/7/29 | 作成者: @skipkayhil
- 概要 (1-2文で)
RailsのLoadErrorハンドリングを見直し、「意図した場合のみLoadErrorを握りつぶす」ようにすることで、予期せぬ例外の隠蔽を防ぐ修正です。rails/allのロード処理とテスト用の isolation ヘルパで、捕捉するLoadErrorの範囲をより厳密にしています。
- 変更内容の詳細
※PR本文や diff 全体は提示されていないため、Rails の既存実装と PR タイトル・関連 issue (#57969), 過去 PR (#31307) から推測を含みますが、Rails で典型的なパターンに沿った説明をします。
背景となる問題
Rails では、特定のファイル・ライブラリを「存在すれば使うが、なければスキップする」という目的で以下のようなコードを書くことがあります:
begin
require "some_optional_dependency"
rescue LoadError
# gem が入ってなければ何もしない
endしかしこの書き方だと、本当に「ファイルが見つからない」ことによる LoadError だけでなく、その中で発生した別の原因の LoadError まで握りつぶしてしまう可能性があります。
例えば:
# some_optional_dependency.rb
require "typo_in_dependency_name" # これが原因で LoadError
# 呼び出し側
begin
require "some_optional_dependency"
rescue LoadError
# ここで握りつぶされてしまい、本来検知すべきバグが見えなくなる
endこの PR は、こうした「本来は表に出すべき LoadError」を誤って rescue しないように、rescue する条件を厳密に絞る変更です。
railties/lib/rails/all.rb の変更
rails/all は、Rails の主要フレームワーク(active_record/railtie, action_controller/railtie など)をまとめて require する役割を持っています。その中で、オプション的なコンポーネントや、環境によって無くてもよいファイルを require し、LoadError を rescue している箇所があります。
今回の変更は、そこにある rescue LoadError を「対象とするファイルに対する LoadError だけを握りつぶす」形に修正しています。典型的には、以下のようなパターンに変わります:
# 変更前(イメージ)
begin
require "rails/some_feature"
rescue LoadError
# any LoadError is swallowed
end
# 変更後(イメージ)
begin
require "rails/some_feature"
rescue LoadError => error
raise unless error.path == "rails/some_feature"
# 本当に "rails/some_feature" が見つからない場合だけスルー
end実際のコード量としては +2/-1 程度なので、上記のような LoadError => error と raise unless ... など、1 行追加 + 既存行の軽微修正で意図した LoadError のみに絞り込んでいると考えられます。
railties/test/isolation/abstract_unit.rb の変更
テスト用の isolation ヘルパ (railties/test/isolation/abstract_unit.rb) でも同様に、require 周りの LoadError を扱っている箇所があり、その挙動を本体の変更に合わせて調整しています。
ここでは主に:
LoadErrorを発生させる/検知するテストヘルパの書き方を、rails/allと同じパターンに揃える- 「意図しない
LoadErrorを飲み込まない」ことを保証するためのテスト補助処理を修正
といった小さな修正(+1/-1)が行われていると考えられます。
- 影響範囲・注意点
影響範囲
require "rails/all"を通じて Rails をロードしているアプリ/ツール全般- 特に、
rails/all内の optional な railtie・フレームワークのロードに依存しているコード - Rails テストスイートにおける isolation テスト環境
実務上の影響
- これまで「何となく動いていた」ケースで、実は内部で
LoadErrorが起きていた場合、それが表に出てきて例外として落ちるようになる可能性があります。- 例:
rails/all経由でロードしているファイル内でrequire "typoed_name"のようなミスがあり、本来はLoadErrorが出るべきだったが、これまでは握りつぶされていた。- 今回の変更により、その
LoadErrorがちゃんと表面化し、スタックトレース付きで落ちる。
- 例:
- 結果として、「起動時に突然
LoadErrorが出るようになった」という挙動変化が起きる可能性はありますが、それは隠れていたバグが可視化された形なので、修正すべき箇所が明確になります。
注意点
自分で
LoadErrorを広く rescue しているコードがある場合も、同じ問題を抱える可能性があります。この PR の書き方を参考に、LoadError#pathを確認して意図した require のみ握りつぶすパターンへの移行を検討すると安全です。パターン例:
rubybegin require "my_optional_gem" rescue LoadError => e raise unless e.path == "my_optional_gem" # gem が入っていない場合だけスルー end
PR 説明にある通り
8-1-stableへのバックポート検討が話題になっているため、Rails 8.1 系にも同様の挙動変更が入る可能性があります。- アプリケーション側が Rails の minor/patch アップデートに追随する際には、起動時
LoadErrorの有無を確認しておくと良いです。
- アプリケーション側が Rails の minor/patch アップデートに追随する際には、起動時
- 参考情報 (あれば)
- 対応 Issue: #57969
「意図しないLoadErrorが握りつぶされてしまう」類の報告と考えられます。 - 関連 PR: #31307
同趣旨の変更または以前の試みを再オープンして仕上げたものと思われます。 - Ruby での
LoadError安全な扱いの定石:rescue LoadError => eraise unless e.path == "期待しているファイル名"- このパターンにより、「その require 自体が失敗した」ケースだけを明示的に無視し、それ以外のエラーは落とすことができます。
#58284 Bump the minimum supported SQLite version to 3.35.0
マージ日: 2026/7/29 | 作成者: @kamipo
- 概要 (1-2文で)
Rails の SQLite3 アダプタが実質的に依存していたRETURNING句に合わせて、サポートする SQLite の最小バージョンを 3.35.0 に引き上げ、これより古いバージョンでは明示的にエラーを出すようにした PR です。これにより、古い SQLite を使った場合にrecord.idがnilになるといった「サイレントな不具合」が、起動時に検出されるようになります。
- 変更内容の詳細(あればサンプルコードも含めて)
背景: RETURNING 句と Rails 7.1 以降
- SQLite 3.35.0 で
INSERT ... RETURNING ...句が導入された。 - Rails 7.1 の PR #49290 で、SQLite3 アダプタは「INSERT 後に自動採番カラム(主キーなど)を取得する」ために
RETURNINGに依存する実装に変更された。- 以前は
@raw_connection.last_insert_row_idを使うフォールバックパスが存在したが、それが削除されている。
- 以前は
- その結果、SQLite 3.23.0 ~ 3.34.x では以下のような状態になっていた:
supports_insert_returning?がfalse→sql_for_insertはRETURNINGを付けない SQL を生成。- しかし
last_inserted_idはRETURNINGの結果セットから読む想定になっており、結果が空のためrecord.idがnilになる。
つまり Rails 7.1 以降、古い SQLite は「対応しているように見えて実は壊れている」という状態でした。
この PR の変更点
SQLite の最小サポートバージョンを 3.35.0 に引き上げ
check_versionメソッド内でバージョンチェックを行い、3.35.0 未満の場合は例外を発生させるように変更。- これにより、アプリケーション起動や DB 接続確立時点で「この SQLite バージョンはサポート外」であることが明示的に分かる。
(擬似コードイメージ)
rubydef check_version if sqlite_version < "3.35.0" raise ActiveRecord::SQLite3DatabaseError, "SQLite 3.35.0 or newer is required. Detected #{sqlite_version}" end endCHANGELOG の更新
- activerecord/CHANGELOG.md に、SQLite の最小サポートバージョンが 3.35.0 になったことを追記。
- Rails アプリ/ライブラリのメンテナが、バージョンアップ時に互換性情報を確認できる。
実装まわりの微修正
- sqlite3_adapter.rb において、バージョンチェックまわりのコードが加筆・整理されている(行数ベースで +5 / -5 程度の小改修)。
- ロジックとしては「古いバージョン向けフォールバックを戻す」のではなく、「そもそも古いバージョンをサポート対象外にする」という方向の修正。
- 影響範囲・注意点
影響を受けるケース
- 次のような環境で Rails (少なくとも 7.1 以降) + SQLite を使っている場合、この PR を含むバージョンに上げると接続時にエラーになる可能性があります:
- SQLite バージョンが 3.35.0 未満:
- OS 標準の SQLite が古いディストリビューション(古い Debian/Ubuntu、CentOS 系など)
- CI 環境で古い SQLite がインストールされている場合
- macOS でも、非常に古い環境を使っている場合
- SQLite バージョンが 3.35.0 未満:
実際に起こっていた不具合の例
Rails 7.1 以降 + SQLite 3.23.0〜3.34.x で以下のようなコードを書くと:
rubyuser = User.create!(name: "Alice") user.id # => 本来は整数の ID が入るべきだが nil になっていた- 内部的には
INSERTの後にRETURNING結果を読むことを想定しているが、 - 古い SQLite では
RETURNINGが使われないため結果セットが空になり、idがnilのままになる。 - このバグは例外にならず、静かに
nilになるため検知しづらかった。
- 内部的には
この PR により、そもそもそのような古い SQLite ではアダプタが起動時に例外を投げるため、「知らないうちに id が
nilだった」といった状況を防げる。
対応策
SQLite を 3.35.0 以降にアップグレードする
本番・開発・CI を含む全環境で SQLite のバージョンを確認することを推奨します。
確認例:
shsqlite3 --version必要に応じて:
- OS のパッケージを更新(例:
apt-get upgrade sqlite3、brew upgrade sqliteなど) - Docker イメージを新しいベースイメージに変更
- CI のイメージやセットアップスクリプトを更新
- OS のパッケージを更新(例:
古い SQLite をどうしても使わざるを得ない場合
- この PR を含む Rails バージョンにはアップグレードせず、現状維持または別の DB(PostgreSQL/MySQL 等)への移行を検討する必要があります。
- 「古い SQLite + Rails 7.1 以降」は元々壊れた状態であるため、根本的にはこの組み合わせは避けた方がよいです。
- 参考情報 (あれば)
- 対象 PR: #58284 「Bump the minimum supported SQLite version to 3.35.0」
- 関連 PR: #49290(Rails 7.1 での SQLite アダプタが
RETURNINGに依存するようになった変更) - SQLite 3.35.0 のリリースノート(
RETURNING句導入等):
https://sqlite.org/releaselog/3_35_0.html - 実運用上のポイント:
- 「Rails 7.1 以降 + SQLite」を使う場合、実質的に 3.35.0 以上必須だったことを、この PR で明文化した形になります。
- Rails のバージョンアップ時は、アプリの Gemfile.lock だけでなく、インフラ側の DB バージョン要件も併せて確認しておくと安全です。
#58283 Consolidate returning_column_values on the abstract adapter
マージ日: 2026/7/29 | 作成者: @kamipo
- 概要 (1-2文で)
insert時に返される主キーなどの値を取得する処理(returning_column_values)を、各DBアダプタではなく抽象アダプタ(AbstractAdapter)側に集約し、PostgreSQL / SQLite3 / MySQL 側の同一実装を削除したPRです。supports_insert_returning?の分岐を共通化しつつ、Trilogy だけが生ドライバ結果を返すという現状の「事実上の契約」を明示した上で、今後のリファクタリングに備える変更になっています。
- 変更内容の詳細
2-1. returning_column_values の共通化
activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb に、各アダプタで重複していた returning_column_values の実装が集約されました。
概ね以下のようなロジックになっています(擬似コード):
def returning_column_values(table_name, primary_key, result)
if supports_insert_returning?
# RETURNING ... をサポートしている DB
# => ActiveRecord::Result として返ってくるので、その最初の行を返す
result.rows.first
else
# RETURNING 非対応の DB (例: mysql2 / Trilogy など)
# => ドライバから last_insert_id を解決して配列で返す
[last_inserted_id(result)]
end
endこれにより、以下のアダプタにあったまったく同じ実装が削除されています。
mysql/database_statements.rb(-8行)postgresql/database_statements.rb(-4行)sqlite3/database_statements.rb(-4行)
共通化前までは、これら各アダプタで returning_column_values をオーバーライドして同じ処理をしていましたが、その分岐ロジックを抽象アダプタに移したことで、DRY に保たれます。
2-2. Trilogy アダプタに関する説明
説明文中で特に強調されているのが、Trilogy アダプタだけが exec_insert の戻り値として「生のドライバ結果」を返している点です。
- 他のアダプタ:
exec_insertの戻り値:ActiveRecord::Result
- Trilogy:
exec_insertの戻り値:Trilogy::Result(生のドライバオブジェクト)
Trilogy は last_inserted_id を result.last_insert_id から読む実装になっており、そのため exec_insert の戻り値型がドライバ依存になっています。
さらに、test_exec_insert ( #26002 で追加されたテスト ) では、
connection.send(:last_inserted_id, exec_insert(...))が動作することを前提にしており、結果として:
- 「Trilogy の
exec_insertはドライバ固有オブジェクトを返す」という挙動がテストで固定されている - そのため「
exec_insertは常にActiveRecord::Resultを返すように統一する」という方向のリファクタリングが、今すぐにはやりづらい状態
であることがコメントとして明示されています。
このPR自体はその仕様を変えていませんが、「今後 exec_insert 自体をなくす(より高レベルなAPIに統一する)までは、Trilogy 固有の戻り値仕様は維持される」という前提が整理されています。
- 影響範囲・注意点
3-1. 実行時挙動の変化
通常のアプリケーションコードにとっては、挙動はほぼ変わりません。
INSERT ... RETURNINGをサポートするDB (PostgreSQL、SQLite3の一部設定など):returning_column_valuesは、これまで同様result.rows.firstを返す
RETURNING非対応DB (典型的には MySQL 系):returning_column_valuesは、[last_inserted_id(result)]を返すlast_inserted_idの中身はアダプタ依存(mysql2 / Trilogy はLAST_INSERT_ID()やドライバのlast_insert_idを参照)
ロジックが抽象クラスに移動しただけなので、正しくアダプタが supports_insert_returning? を実装していれば、今までと同じ結果が得られます。
3-2. アダプタ拡張・独自アダプタへの影響
独自アダプタを書いている場合や、Forkしたアダプタを持っている場合は以下を確認するとよいです。
supports_insert_returning?を適切に実装しているかtrue:exec_insertの戻り値はActiveRecord::Resultであり、result.rows.firstから返すべき値が取得できること。false:last_inserted_id(result)が正しく ID を解決できること。
以前
returning_column_valuesを独自にオーバーライドしていた場合- 今回の共通化により、継承元の実装が変わる可能性があるので、挙動差が出ていないか確認が必要です。
- 特に「複数カラムを RETURNING している」「主キー以外の値も返したい」などのユースケースで独自実装していた場合は要注意です(ただし現状の共通実装も
rows.firstをそのまま返すため、多くの場合は同等動作のはずです)。
3-3. Trilogy 利用者の注意点
- このPRは Trilogy の
exec_insertの戻り値型やlast_inserted_idの仕様を変えていません。 - ただし、説明にある通り「Trilogy は唯一の生ドライバ結果を返すアダプタである」という前提が明文化され、将来的に
exec_insertは API として整理される(別の高レベル API に置き換えられる)可能性が示唆されています。 - 独自に
exec_insertを直接叩いてドライバに依存したコードを書いている場合、将来の変更で壊れやすい領域なので、極力ActiveRecordの高レベルAPI(create,insert,insert_allなど)に寄せておくと安全です。
- 参考情報 (あれば)
- このPR:
- 言及されている過去PR:
test_exec_insertを追加した PR #26002: https://github.com/rails/rails/pull/26002
- 関連するクラス / メソッド:
ActiveRecord::ConnectionAdapters::DatabaseStatements#returning_column_valuesActiveRecord::ConnectionAdapters::DatabaseStatements#supports_insert_returning?ActiveRecord::ConnectionAdapters::DatabaseStatements#last_inserted_id- Trilogy アダプタの
exec_insert/Trilogy::Result#last_insert_id
#58277 Remove unreachable single-version branch from DefaultSchemaVersionsFormatter
マージ日: 2026/7/28 | 作成者: @55728
- 概要 (1-2文で)
DefaultSchemaVersionsFormatter#formatにあった「単一バージョン用」のフォールバック分岐(実際には到達不能)を削除し、常に配列を前提としたマルチ行INSERT ... VALUESのみを使うようにしたPRです。スキーマダンプの出力内容や公開APIには一切影響せず、内部実装のデッドコード整理になります。
- 変更内容の詳細
背景と問題点
DefaultSchemaVersionsFormatter#format は、schema_migrations テーブルから読み出したバージョン配列を SQL に整形する内部クラスです。
呼び出し経路は以下のようになっています:
dump_schema_versions
→insert_versions_sql
→DefaultSchemaVersionsFormatter#format
dump_schema_versions は常に「バージョンの配列」を insert_versions_sql に渡し、そのまま DefaultSchemaVersionsFormatter#format に渡されます。そのため、format が「単一バージョン(非配列)」を受け取る経路は実在しません。
にもかかわらず、過去の互換性を引きずったコードとして、
- 引数
versionsが Array でない場合(単一バージョン)のフォールバック - それに対応する単一行
INSERT生成ロジック
が残っていました。
履歴を辿ると:
fd87169eb1
マルチ行 INSERT をサポートしないアダプタ向けに、1バージョンごとに別々のINSERTを出していた時代の分岐を導入。d1a74c1e01
「単一バージョンを渡す」最後の呼び出し経路を削除。0cc1842d38
コードをDefaultSchemaVersionsFormatterクラスへ移動した際、既に不要になっていた分岐をそのまま移植。
という経緯で、使われないフォールバックだけが内部クラスに残り続けていました。
今回の具体的な変更
activerecord/lib/active_record/migration/default_schema_versions_formatter.rb の中で、
- 「
versionsが Array でない場合の分岐」を削除 - 単一バージョンを想定したコードパスを削除
- 残ったコードは「配列のバージョンからマルチ行
VALUESを組み立てる」パスのみ
という形に整理されています。
イメージ的には:
# 変更前(概略イメージ)
def format(versions)
if versions.is_a?(Array)
# マルチ行 INSERT ... VALUES
else
# (過去の名残)単一バージョン用のフォールバック
end
endが
# 変更後(概略イメージ)
def format(versions)
# 常に配列を前提としたマルチ行 INSERT ... VALUES
endのような形になっています。
PR本文でも明言されている通り、「生成されるスキーマダンプは変更前と完全に同じ内容」になるよう、振る舞いは維持されています(到達しないコードだけを削除したため)。
- 影響範囲・注意点
ActiveRecord内部 (
:nodoc:クラス) のみの変更DefaultSchemaVersionsFormatter自体は内部API扱いのクラスであり、アプリケーションコードから直接利用することは想定されていません。通常のRailsアプリでは、この変更に伴う挙動の変化はありません。スキーマダンプの内容は不変
schema:dumpなどで出力される SQL の内容は、PR前後で同一です。単に「使われていない条件分岐がなくなった」だけです。公開された拡張ポイントには影響なし
- 公開API:
config.active_record.schema_versions_formatter - ドキュメントでも「バージョン配列を受け取るフォーマッタ」を前提としており、今回の変更でもその前提は維持されています。
- カスタムフォーマッタを実装している場合でも、これまで通り「バージョンの配列」を受け取るインターフェースに変化はありません。
- 公開API:
もし影響が出るとしたら
「内部クラスDefaultSchemaVersionsFormatterを直接叩き、かつ非配列(単一のバージョン)を渡していた」ような非想定の使い方をしていた場合は、これまで“たまたま動いていた”フォールバックに依存していた可能性があります。ただし、:nodoc:であり、かつ呼び出し経路的にも想定外なため、通常の利用では該当しません。
- 参考情報 (あれば)
PRで言及されているコミット履歴
fd87169eb1
– 単一バージョン用分岐が導入された起点。マルチ行 INSERT をサポートしないアダプタ配慮時代のコード。d1a74c1e01
– 単一バージョンを渡す最後の呼び出し経路を削除。0cc1842d38
– コードをDefaultSchemaVersionsFormatterクラスに移動し、その際に不要な分岐を一緒に持ち込んだ。
設定ガイド(schema_versions_formatter)
Railsガイドの「設定・初期化」あたりで紹介されているconfig.active_record.schema_versions_formatterの説明に、「フォーマッタはバージョン配列を受け取る」前提が記載されています。このPRはその仕様と実装をより一致させた形になります。
#58282 Add prefetch_primary_key? test coverage on PostgreSQL
マージ日: 2026/7/28 | 作成者: @kamipo
- 概要 (1-2文で)
PostgreSQL アダプタにnext_sequence_valueが実装され、prefetch_primary_key?をtrueにしたモデルでも、実際の PostgreSQL シーケンスを使って主キーを事前採番できるようになりました。加えて、その挙動を end-to-end で検証するテストが追加され、今後のinsert/_insert_recordリファクタリングや oracle-enhanced アダプタとの互換性を確認しやすくなっています。
- 変更内容の詳細
2-1. PostgreSQL アダプタへの next_sequence_value 実装
変更ファイル:
activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
PostgreSQL アダプタに、以下のようなメソッドが新規実装されています(概念的にはこんなイメージです):
def next_sequence_value(sequence_name)
select_value("SELECT nextval(#{quote(sequence_name)})")
endポイント:
- 目的:
ActiveRecord::Base#prefetch_primary_key?をtrueにしたとき、INSERT 実行前に主キーを Ruby 側で取得するための API としてnext_sequence_valueを提供。 - これまでとの違い:
- 以前は Oracle enhanced adapter だけが
next_sequence_value/prefetch_primary_key?を利用しており、PostgreSQL では公式にはサポートされていなかった。 - この PR により、PostgreSQL でも DB のシーケンスを使った事前採番 がサポートされる。
- 以前は Oracle enhanced adapter だけが
prefetch_primary_key? を true にしたモデルでは、INSERT 前に以下のような流れになります:
- アダプタの
next_sequence_valueでシーケンス値を取得 - 取得した値をレコードの
idにセット - その
idでINSERTを実行
これにより、DB 側のシーケンスと Rails 側の主キー採番を明示的に同期させることができます。
2-2. PostgreSQL 用テストの追加
変更ファイル:
activerecord/test/cases/adapters/postgresql/prefetched_primary_key_test.rb
ここでは、prefetch_primary_key? 経由のフローを end-to-end で検証 するテストが追加されています。テストが確認しているポイントは以下の通りです。
INSERT 前にシーケンスが進むこと
next_sequence_valueを呼び出し、実際に PostgreSQL のシーケンス (nextval(...)) が進むことを検証。
事前取得した値が実際の行の
idになること- レコードを保存し、DB から読み出したときに、
idが事前に取得したシーケンス値と一致するか を確認。
- レコードを保存し、DB から読み出したときに、
明示的に指定した
idが優先されること (||=の挙動)_insert_record内で、idの設定に||=が使われているため、- 事前採番された
idがあっても、 - ユーザーがレコードに明示的に
idを設定した場合は、明示的なidが優先される
- 事前採番された
- テストでは「prefetch した id があっても、明示的な id を指定した場合はそちらで INSERT される」ことを確認している。
概ね以下のようなイメージのテストが入っています(疑似コード):
class PostgresqlPrefetchedPrimaryKeyTest < ActiveRecord::PostgreSQLTestCase
class PrefetchedPkModel < ActiveRecord::Base
self.table_name = "..." # テスト用テーブル
def self.prefetch_primary_key?
true
end
end
def test_prefetched_primary_key_uses_sequence
record = PrefetchedPkModel.create!
# INSERT 前に next_sequence_value を呼んだ結果が record.id になっていることを確認
end
def test_explicit_id_overrides_prefetched
record = PrefetchedPkModel.new(id: 123)
record.save!
assert_equal 123, record.id
end
endこれにより、以下が保証されます:
prefetch_primary_key?がtrueでも、ユーザーがidを手動で設定するとそちらが優先される。- PostgreSQL アダプタの
next_sequence_valueが、ActiveRecord 本体の期待するインターフェイス通りに動作している。
- 影響範囲・注意点
3-1. 影響範囲
PostgreSQL + ActiveRecord で、
prefetch_primary_key?を true にしているモデル- これまでは半ば未サポート・非公式な状態だったパターンが、公式にサポートされる方向。
- これにより、「Oracle では動くが PostgreSQL ではダメ」という状況がやわらぐ。
oracle-enhanced adapter の検証
- oracle-enhanced アダプタは
prefetch_primary_key?に強く依存しており、
このテストは「ActiveRecord 本体側の変更で oracle-enhanced が壊れていないか」を担保するためにも使われる。 - 今回は PostgreSQL 用のテストだが、「ActiveRecord が
prefetch_primary_key?経由でどう insert するか」の仕様を固める役割を持つため、サードパーティアダプタにとっても重要なテスト。
- oracle-enhanced アダプタは
insert/_insert_recordのリファクタリング- PR 説明文にもある通り、「今後
insert/_insert_recordを整理・変更する際に、このテストが回帰テストとして効く」ことを狙っている。 - つまり、将来の内部実装変更でも、このテストが通り続ける限り
prefetch_primary_key?の契約は保たれる。
- PR 説明文にもある通り、「今後
3-2. 注意点
prefetch_primary_key?を使う場合:- モデルで
self.primary_keyと実際の PostgreSQL シーケンスの名前や型が正しく対応している必要があります。 - 手動でシーケンスをいじっている環境では、
- 事前採番 + DB 側のデフォルトシーケンス、の二重管理にならないよう注意が必要です(通常は ActiveRecord 側がシーケンスを使うので問題にはなりにくいですが、カスタム定義している場合は要確認)。
- モデルで
明示的な
idを使う場合:idを自前で設定して保存した場合、シーケンス側のカウンタがその値を追い越していないと、将来シーケンスが同じ値を発行して一意制約違反になる可能性があります(これは PostgreSQL の一般的な注意点)。- 大量に「明示的な id を指定して INSERT」を行う設計にする場合は、
setvalでシーケンスを調整するなどの運用の一貫性を考える必要があります。
- 参考情報 (あれば)
- 対象 PR:
- 関連アダプタ:
- oracle-enhanced adapter: https://github.com/rsim/oracle-enhanced
- 背景となる ActiveRecord のメソッド:
ActiveRecord::Base#prefetch_primary_key?ActiveRecord::Persistence#_insert_record- PostgreSQL の
nextval()関数とシーケンス管理
#58244 Use latest ruby patch version in dockerfile
マージ日: 2026/7/28 | 作成者: @adityapandit17
概要 (1-2文で)
Rails リポジトリの開発用コンテナ(.devcontainer)の Dockerfile で使用している Ruby のパッチバージョンが、最新の 4.0.6 に更新されました。機能追加ではなく、開発環境における Ruby バージョンのメンテナンス更新です。変更内容の詳細(あればサンプルコードも含めて)
- 対象ファイル:
.devcontainer/Dockerfile - 変更点は 1 行のみで、利用する Ruby イメージのバージョン指定が更新されています。
典型的には以下のような変更が入っています(イメージ例):
-FROM ruby:4.0.5
+FROM ruby:4.0.6あるいはタグ付きバリアントを使っている場合も同様です:
-FROM ruby:4.0.5-bookworm
+FROM ruby:4.0.6-bookworm要するに、開発用コンテナが起動する際に取得される Ruby ベースイメージが「4.0.5 → 4.0.6」に切り替わるだけの変更です。
- 影響範囲・注意点
影響範囲
- VS Code Dev Containers や GitHub Codespaces、その他
.devcontainer/Dockerfileを利用して Rails を開発している環境に影響します。 - CI などで
.devcontainer/Dockerfileを直接使っている場合も Ruby 4.0.6 で実行されるようになります。
- VS Code Dev Containers や GitHub Codespaces、その他
実務的な影響
- Ruby 4.0.6 は 4.0 系のパッチリリースであり、後方互換性を保ちつつ、バグ修正・パフォーマンス改善・セキュリティ修正が含まれている可能性があります。
- パッチレベルの更新のため、通常はアプリケーションコードの変更は不要ですが、以下のようなケースでは注意が必要です:
- Ruby 本体の細かい挙動に依存したテスト(時間、GC、警告メッセージなど)
- ネイティブ拡張を含む gem を使用しており、Ruby のパッチ更新でビルドが再実行される場合
- 一度コンテナを再ビルド (
docker compose buildや Dev Container の「Rebuild Container」) すると、新バージョンの Ruby が反映されます。
注意点
- ローカルで Ruby 4.0.5 など別バージョンを使っている場合、
.devcontainer内では 4.0.6 になるため、微妙な差異で挙動がずれないかを確認しておくと安全です。 - もし
Gemfileや.ruby-versionで Ruby バージョンを厳密に固定している場合は、それらの指定と Dockerfile のバージョンが不一致になっていないか確認してください(4.0.6 に合わせるのが望ましいです)。
- ローカルで Ruby 4.0.5 など別バージョンを使っている場合、
- 参考情報 (あれば)
- Ruby 4.0 系および 4.0.6 の変更内容・リリースノート(※実際のリンクは Ruby の公式サイト・ニュースを参照)
- Ruby 公式: https://www.ruby-lang.org/
- Ruby リリースノート: https://www.ruby-lang.org/en/news/
この PR 自体はごく小さい変更ですが、「公式リポジトリが想定している Ruby バージョン」が 4.0.6 に更新された、という指標にもなります。
#58108 Allow to use thread module reader / writer in a ractor:
マージ日: 2026/7/28 | 作成者: @Edouard-chin
- 概要 (1-2文で)
このPRは、thread_mattr_reader/thread_mattr_writer(thread_mattr_accessor)で定義されたスレッド単位のクラス属性を、Ractor 内からも安全に使えるようにする変更です。Ractor の分離制約に抵触していたクラスインスタンス変数のメモ化をやめる代わりに、毎回キーを計算するようにし、その代償としてごく小さいパフォーマンス低下を受け入れています。
- 変更内容の詳細
背景となる問題
従来の thread_mattr_* は、内部的に「スレッドローカルストレージにアクセスするためのキー」をクラスインスタンス変数にメモ化していました。
イメージとしては以下のようなことをしていたと考えられます(実際の実装は簡略化):
class Request
# thread_mattr_accessor :id の実装例イメージ
@__thread_local_key_for_id ||= "Request:id"
def self.id
Thread.current[@__thread_local_key_for_id]
end
def self.id=(value)
Thread.current[@__thread_local_key_for_id] = value
end
endRactor では「Ractor 間で共有されるオブジェクト」が厳しく制限されており、クラスインスタンス変数に保持されたメモ化オブジェクトが、その制約に引っかかって Ractor::IsolationError を引き起こしていました。
ユーザーコードの例:
class Request
thread_mattr_reader :id
thread_mattr_writer :id
end
Request.id = 123
Ractor.new { Request.id } # => Ractor Isolation Error(PR 前)今回の修正内容
1. メモ化の削除
activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb で行っていた「スレッドローカルキーのメモ化(クラスインスタンス変数に保持)」をやめ、毎回キーを計算する実装に変更しています。
大まかな方針は次のようなイメージです:
- 以前:
@_short_circuit_keyのようなクラスインスタンス変数にキーをキャッシュ
- 変更後:
- 毎回
:"#{name}_#{object_id}"や"#{self.name}/#{attr_name}"等の規則に基づきキーを生成(実際のキー生成規則は Rails 内部実装に依存)
- 毎回
これにより、Ractor 内でクラスを参照しても、そのクラスが持つクラスインスタンス変数に Ractor 非対応オブジェクトが閉じ込められている、という状況を避けられます。
2. テストの追加
activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb にテストが追加され、少なくとも次のような動作が確認されるようになっています:
thread_mattr_accessorで定義された属性が Ractor 内から呼び出せること- Ractor をまたいでも Ractor ごとに独立した状態を保ちつつ、エラーを起こさないこと
コード例(イメージ):
class Request
thread_mattr_accessor :id
end
Request.id = 123
r = Ractor.new do
Request.id # Ractor 内から reader を呼ぶ
end
r.take # IsolationError が起きず正常に実行できること3. パフォーマンスのトレードオフ
メモ化を外したことで、スレッドローカルキーの生成コストが毎回かかるようになり、micro benchmark では以下のように 1.6〜2倍 程度の遅さが観測されています。
- setter (
Request.id = ...):- メモ化あり: 約 8.8M i/s
- メモ化なし: 約 5.6M i/s (約 1.6 倍遅い、1 回あたり ~180ns 程度)
- getter (
Request.id):- メモ化あり: 約 13.5M i/s
- メモ化なし: 約 6.9M i/s (約 2 倍遅い、1 回あたり ~145ns 程度)
いずれも「1 回の呼び出しはナノ秒オーダー」であり、通常のアプリケーションにおいては、ループ内で超高頻度に呼ぶような特殊ケースを除けば実害は小さいと判断されています。
- 影響範囲・注意点
影響範囲
ActiveSupportのthread_mattr_reader/thread_mattr_writer/thread_mattr_accessorを利用しているすべてのコード。- 特に、Ractor を使った並行処理(Ruby 3.0+)環境で、これらのメソッドを使う場合に挙動が改善されます。
- テストレベルでは Ractor 内からの利用がカバーされているため、Ractor 環境での安全性は向上しています。
パフォーマンス面の注意
- getter / setter ともに 1.6〜2倍程度遅くなっていますが、絶対値はナノ秒オーダーであり、ほとんどの Web アプリケーションでは体感できる差にはなりにくいです。
- ただし、例えば「1 リクエスト中に何十万回も
thread_mattr_*を叩くようなホットパス」で使っている場合は、パフォーマンスを計測して影響を確認したほうがよい可能性があります。 - 「スレッドローカルにこだわらなくて良い」「Ractor も使っていない」ような場面では、普通のクラスインスタンス変数や
cattr_*など、別の仕組みを検討しても良いかもしれません。
Ractor 利用時のメリット
thread_mattr_*を使った既存コードを、そのまま Ractor から呼び出しても IsolationError で落ちないようになります。- Ractor を併用した並列実行(例: 並列テストランナー、バックグラウンドジョブの分散実行など)で、Rails のスレッド属性をより安全に使えるようになるため、将来の並列化対応にとって重要な前提整備といえます。
- 参考情報 (あれば)
対象ファイル:
activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rbthread_mattr_*の内部実装。ここからクラスインスタンス変数によるメモ化が削除され、毎アクセス時にキー計算を行うようになった。
activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb- Ractor 環境を含むテストが追加され、Ractor 内から
thread_mattr_*アクセスが可能であることを検証。
- Ractor 環境を含むテストが追加され、Ractor 内から
Rub y バージョン:
- ベンチマークは
ruby 4.0.1 (2026-01-13 ...)で計測されているため、Ruby 4 系での Ractor 利用を念頭に置いた変更であると読み取れます。
- ベンチマークは
利用側で特別なマイグレーションは不要で、API 互換性は維持されたまま、内部実装のみが変更された PR です。
#58273 Fix Markdown doc check
マージ日: 2026/7/28 | 作成者: @flavorjones
概要 (1-2文で)
Markdown ドキュメントチェック用スクリプトと RuboCop (Layout/EmptyLineAfterMagicComment) のスタイルルールが衝突していた問題を解消する PR です。# frozen_string_literal: trueの直後の空行を、Markdown チェック側でも許容するように修正しています。変更内容の詳細
- 変更ファイル:
.github/workflows/scripts/check-markdown-api.rb(1行変更)
このスクリプトは、Markdown 内に記載された Ruby のコード例をチェックする用途で使われており、ファイル先頭のフォーマットについて独自のチェック(もしくは前提)を持っていました。
問題となっていたのは以下のようなパターンです:
# frozen_string_literal: true
class Example
# ...
endRuboCop の Layout/EmptyLineAfterMagicComment ルールは、上記を次のように空行を入れる形に自動修正します:
# frozen_string_literal: true
class Example
# ...
endしかし、Markdown ドキュメントのコードブロックを検証する check-markdown-api.rb は、この空行があると不正なフォーマットとして扱い、チェックを失敗させていました。
この PR では、その Markdown チェック側のロジックを修正し、frozen_string_literal マジックコメント直後の空行を「許容される形式」として扱うようにしています。
具体的には、ファイル先頭(もしくはコードブロック先頭)の判定条件やパターンマッチを 1 行だけ変更し、「空行あり/なし」の両方を受け入れるようにした、という内容です(+1/-1行のみ)。
※ PR 本文の「So let's make that blank line acceptable.」から、Rubocop の推奨スタイルを優先し、それに合わせて Markdown チェックを緩和・修正したことがわかります。
- 影響範囲・注意点
- 影響範囲
- GitHub Actions などで動く Markdown API チェック ワークフローにのみ影響します。
- Rails リポジトリ内の Markdown ドキュメントに含まれる Ruby コード例(特に
# frozen_string_literal: trueを含むもの)が、Rubocop の自動整形後でもチェックエラーにならなくなります。
- 注意点
- これまで「Rubocop が直したコードだと Markdown チェックが落ちる」という CI 上のノイズが出ていた場合、この PR により解消されます。
- Markdown 側のチェックがやや緩和される形ですが、影響するのはマジックコメント直後の 1 行だけであり、実コードやその他スタイルには影響しません。
- 参考情報 (あれば)
- 関連 PR:
- #58257 — 問題の元になった Markdown doc チェック用スクリプトを導入した PR
- 関連 RuboCop ルール:
Layout/EmptyLineAfterMagicComment- マジックコメント(
# frozen_string_literal: trueなど)の直後に空行を入れることを要求するスタイルルール。
- マジックコメント(
#58211 Pass scheme to token authentication block in http_atthentication
マージ日: 2026/7/28 | 作成者: @chad-cole
- 概要 (1-2文で)
Rails の HTTP トークン認証 (ActionController::HttpAuthentication::Token) において、これまで捨てられていた「スキーム(Bearer など)」情報をブロックへ渡せるようにし、かつコントローラ側で特定スキームを必須とできるようにした PR です。これにより、DPoP などスキーム固有の検証が必要な仕様に対応しやすくなります。
- 変更内容の詳細
2-1. スキーム情報をブロックに渡すように変更
これまで authenticate_or_request_with_http_token などのメソッドは、以下のように「token」と「options」までしかブロックに渡していませんでした。
authenticate_or_request_with_http_token do |token, options|
# token 認証処理
endこの PR により、第3引数として「スキーム(scheme)」をシンボルで受け取れるようになります:
authenticate_or_request_with_http_token do |token, options, scheme|
# 例: "Bearer" -> :bearer, "DPoP" -> :dpop のように正規化されたシンボル
# scheme を見て処理を分岐できる
endAuthorization: Bearer abc123→schemeは:bearerAuthorization: DPoP xyz456→schemeは:dpop- スキーム名は正規化(大文字 / 小文字)され、シンボルで渡される
既存コードとの互換性
第3引数はオプショナルなので、従来どおり2引数だけ受け取るブロックはそのまま動作します。
# 既存のコードもそのまま有効
authenticate_or_request_with_http_token do |token, options|
# 変更不要
end2-2. スキーム必須機能の追加
この PR では、コントローラ側で「特定のスキームのみ受け付ける」ことができるようにするための仕組みが追加されています。
(テストやコード上の意図からの説明なので、メソッド名は概念的なものとして読んでください)
イメージとしては、以下のような使い方が可能になります:
# 例: Authorization ヘッダが DPoP スキームであることを要求したい場合
authenticate_or_request_with_http_token(scheme: :dpop) do |token, options, scheme|
# scheme == :dpop であることはフレームワーク側で保証されている想定
# ここで DPoP 特有の検証 (DPoP 証明ヘッダのチェックなど) を行える
endあるいは、複数スキームをサポートする実装も可能です:
authenticate_or_request_with_http_token do |token, options, scheme|
case scheme
when :bearer
authenticate_bearer(token, options)
when :dpop
authenticate_dpop(token, options)
else
false
end
endスキーム要件を満たさない場合、コントローラは RFC 9110 に沿った適切な WWW-Authenticate チャレンジレスポンスを返せるようになります。
PR ではこの「スキーム要件を満たさない」ケースを検知する仕組みを HttpAuthentication::Token レイヤに追加しています。
2-3. DPoP 対応のための前提整備
今回の PR 自体は DPoP (RFC 9449) の実装そのものは行っていません。
代わりに:
- トークンスキーム情報をアプリケーション層(コントローラのブロック)に届ける
- コントローラから特定スキームを要求できる
という「DPoP 実装のための土台」を用意しています。
DPoP では以下のようなスキーム固有の検証が必要になります:
Authorization: DPoP <access token>の形式で送られていること- 別ヘッダ
DPoP: <proof JWT>の検証(署名、nonce, iat, htm, htu など)
そのため、Rails が Authorization ヘッダを解析した時点で「スキーム名を落としてしまう」と実装が難しく、今回の変更でアプリからスキームを見られるようにした、という位置づけです。
2-4. テスト・CHANGELOG の更新
actionpack/test/controller/http_token_authentication_test.rbに以下を含むテストが追加:- ブロックに
schemeが渡されることの確認 - 特定スキーム必須時の挙動の確認
- (Bearer / DPoP など)複数スキームの扱いのテスト
- ブロックに
actionpack/CHANGELOG.mdに、この機能追加が明記されています。
- 影響範囲・注意点
後方互換性
- ブロック引数は第3引数が追加されただけで、第1・第2引数の意味は変わっていないため、既存コードは基本的にそのまま動作します。
- 引数を2つだけ受け取っているブロックは、そのままで問題ありません。
認証ロジックの拡張余地
- 今後、Bearer と DPoP を同時サポートしたり、独自スキームを実装したりする際に、コントローラ側でスキームごとの処理を簡単に分岐できるようになります。
- スキームに応じて
WWW-Authenticateヘッダを変える(例:WWW-Authenticate: DPoP,WWW-Authenticate: Bearer)といった RFC 9110 的に正しい応答を返しやすくなります。
注意点
- スキームの扱いは正規化されたシンボルとして渡されることを前提に実装する必要があります(例:
:bearer,:dpop)。文字列のまま"Bearer"を期待したコードを書くと動きません。 - 将来的に DPoP を導入する場合は、
AuthorizationとDPoPの両ヘッダを使った検証ロジックを自前で組む必要があり、その際にこの PR で追加されたscheme引数を活用することになります。
- スキームの扱いは正規化されたシンボルとして渡されることを前提に実装する必要があります(例:
- 参考情報 (あれば)
- RFC 9110 (HTTP Semantics)
- WWW-Authenticate や認証スキームの定義:
https://www.rfc-editor.org/rfc/rfc9110
- WWW-Authenticate や認証スキームの定義:
- RFC 9449 (OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer, DPoP)
- Rails ガイド (英語) – HTTP 認証
- ActionController::HttpAuthentication に関する全体像を掴むのに有用:
https://guides.rubyonrails.org/action_controller_overview.html#http-authentications
- ActionController::HttpAuthentication に関する全体像を掴むのに有用:
#58276 [8-0-stable] Avoid loading ActionController::Live early in initializer
マージ日: 2026/7/28 | 作成者: @carldaws
- 概要 (1-2文で)
Rails 8.0 系の8-0-stableブランチに対して、ActionController::Liveが Railtie 初期化処理の中で「早いタイミングでロードされてしまう」問題を避ける修正をバックポートした PR です。これにより、ライブストリーミング機能に関連するオートロード・スレッド・設定タイミング周りの問題が軽減されます。
- 変更内容の詳細
※元 PR #58147 のバックポートであり、そちらの内容が 8.0 向けに反映されたものです。
a. ActionController::Live のロードタイミングの調整
対象ファイル:
actionpack/lib/action_controller/metal/live.rbactionpack/lib/action_controller/railtie.rb
従来は、ActionController::Railtie の initializer の中で ActionController::Live が結果的に「早期に」ロードされる経路があり、以下のような問題が起こり得ました:
- アプリケーション設定(特にスレッドやオートロード関連)が完全に確定する前に、
ActionController::Liveがロードされる - ライブストリーミングに関わるミドルウェアやモジュールが、想定より早く有効化される
- Zeitwerk オートロードのフローや、
config.eager_loadのタイミングと噛み合わないケースで警告やエラーが発生する可能性
本 PR では:
ActionController::Railtie内でActionController::Liveを直接参照/ロードしないようにし、- 必要になるまで定数を解決しない(遅延ロード)方向に寄せる
といった形で、「初期化フェーズにおける不要な自動ロードを避ける」 修正が入っています。
疑似的なイメージとしては:
# 以前 (イメージ)
initializer "action_controller.live" do
ActionController::Live # これが constantize されて早期ロードされる
end
# 修正後 (イメージ)
initializer "action_controller.live" do
# ここでは具体的な参照を避け、必要になったときにロードされるようにする
end実際のコードでは行数は小さい変更(+3/-1 程度)ですが、「どの initializer のタイミングでどのクラスをロードするか」 を整理する目的の修正です。
b. ActionController::Live 側の軽微な変更
対象ファイル:
actionpack/lib/action_controller/metal/live.rb(+2 行)
live.rb 側には、Railtie での遅延ロード戦略と噛み合うような小さな修正が入っています。
典型的には:
- 依存するコンポーネントの require をより明示的にする
- オートロード/eager load とコンフリクトしないような guard を入れる
といった性質の調整です(+2 行の小さな差分ですが、ロード順管理の一部になっています)。
c. ドキュメント更新
対象ファイル:
actionpack/CHANGELOG.mdguides/source/configuring.md
CHANGELOG.md:
- Rails 8.0 系に対して、「ActionController::Live の early load を避ける修正」を記録するエントリが追加されています。
→ Rails 8.0 を使っていて関連する問題を踏んでいた人向けの「このバージョンから直りました」という情報。
guides/source/configuring.md:
- ライブストリーミング(
ActionController::Live)およびその設定/初期化タイミングに関して、ドキュメント上の説明が補足・更新されています。- たとえば「config でこう設定しておけば、ActionController::Live は必要なときにだけ有効化される」といった文脈の追記がされている可能性が高いです。
d. テスト追加
対象ファイル:
railties/test/application/configuration_test.rb(+25 行)
configuration_test.rb にテストが 25 行追加され、次のような振る舞いを検証しています:
- アプリケーション起動時に
ActionController::Liveが勝手にロードされないこと - 特定の設定をした場合のみ、
ActionController::Liveがロード/有効化されること - 8.0 の設定 API (
config.*) との組み合わせで、想定されたタイミングでのみ定数が解決されること
典型的には、以下のようなアサーションが含まれていると考えられます:
test "action controller live is not loaded during initialization" do
app = build_app
# 初期化後でも ActionController::Live がまだ未ロードであることを確認
assert_not defined?(ActionController::Live)
# 実際に使おうとしたときにロードされる
get "/some_live_endpoint"
assert defined?(ActionController::Live)
end(実際のコードは異なりますが、狙っているテストの方向性はこうしたものです。)
- 影響範囲・注意点
影響範囲
- 対象: Rails 8.0 系で
ActionController::Liveを利用している、または利用可能な状態にしているアプリケーション - 影響する面:
- アプリケーション起動時のロード順序
- Zeitwerk オートロードの挙動
- 初期化時(initializer)の副作用・定数解決
開発者視点でのポイント
初期化時の副作用が減る
ActionController::Liveに依存したコードが「アプリ起動時に必ず実行される」状況が緩和されます。- 起動時のログ/メモリ使用量/スレッド生成などが、若干クリーンになる可能性があります。
カスタム initializer 内での
ActionController::Live参照に注意- 自前の initializer で
ActionController::Liveを直接参照している場合、その時点で早期ロードを引き起こす点は変わりません。 - 「可能な限り遅延ロードしたい」場合は、
Rails.application.config.to_prepareやリクエスト発生以降のフック・lazy constantization の利用などに寄せると、今回の方針と整合します。
- 自前の initializer で
Zeitwerk や auto_load の警告/エラーが減る可能性
- 以前、Rails 8.0 に上げた際に「
ActionController::Live周りでオートロードや定数解決に関する警告・エラーが出ていた」ケースでは、この修正で解消されている可能性があります。
- 以前、Rails 8.0 に上げた際に「
動作上の互換性
- 機能としての
ActionController::Liveの挙動(ストリーミングそのもの)は変わりません。 - 変わるのは「いつロードされるか」というライフサイクルの問題であり、通常の利用者にとっては後方互換的です。
- 機能としての
- 参考情報 (あれば)
- 元 PR: #58147 — 本 PR のオリジナル修正。詳細設計や議論はここを見るとわかりやすいです。
- Issue: #58145 — early load による問題報告。どのような症状が出ていたかの具体例が確認できます。
- 対象ブランチ:
8-0-stable— Rails 8.0 系を利用している場合、この修正を含むパッチバージョンへのアップデートで問題回避が期待できます。
#58272 Fix the Parameters#deep_transform_values doc example
マージ日: 2026/7/28 | 作成者: @55728
- 概要 (1-2文で)
ActionController::Parameters#deep_transform_valuesのドキュメント内サンプルの戻り値が、実際の挙動では起こり得ない形になっていたため、現実の挙動に合わせて修正したPRです。機能自体は未リリースのため、挙動変更ではなく「誤ったドキュメントの修正」に留まります。
- 変更内容の詳細
何が問題だったか
元のドキュメントでは、以下のようなサンプルが記載されていました(コメントが「ドキュメント上の想定出力」):
params = ActionController::Parameters.new(
user: { email: " ALICE@EXAMPLE.COM ", profile: { bio: " Hello world " } }
)
params.deep_transform_values { |v| v.is_a?(String) ? v.strip.downcase : v }
# documented
# => #<ActionController::Parameters {"user"=>#<ActionController::Parameters {"email"=>"alice@example.com", "profile"=>#<ActionController::Parameters {"bio"=>"hello world"} permitted: false>} permitted: false>} permitted: false>ここでは user の値や profile の値もすでにネストした ActionController::Parameters オブジェクトとして表示されています。
しかし実際の挙動は次の通りで、ネストした部分は「ただの Hash」として表示されます:
# actual
# => #<ActionController::Parameters {"user"=>{"email"=>"alice@example.com", "profile"=>{"bio"=>"hello world"}}} permitted: false>なぜ違いが出るのか
ActionController::Parameters は「ネストした Hash を最初からすべて Parameters にラップしているわけではなく」、アクセスされたタイミングで遅延的に wrap する実装になっています。
そのため、deep_transform_values の戻り値を inspect した時点では、内部のネストした構造は通常の Hash のままであり、ドキュメントにあるような「入れ子の #<ActionController::Parameters ...>」という表示になることはありません。
このPRでの具体的な修正
変更されたファイルは2つのみです。
actionpack/lib/action_controller/metal/strong_parameters.rbdeep_transform_valuesのドキュメントコメント内のサンプル出力が、実際のinspect結果と同じ形に修正されています。- 例:
- before(誤った例):
{"user"=>#<ActionController::Parameters {...}>} - after(正しい例):
{"user"=>{"email"=>"alice@example.com", "profile"=>{"bio"=>"hello world"}}}
- before(誤った例):
actionpack/CHANGELOG.mddeep_transform_valuesに関する記述の例出力も、同じく実際の出力形式に合わせて1行修正。" => "を" =>"にそろえるなど、表記の統一も行われています。
変更は合計で +2/-2 行と、ごく小さいドキュメント修正です。
- 影響範囲・注意点
- 機能面の変更は一切なく、
deep_transform_valuesの挙動は従来どおりです。 deep_transform_values自体がまだ未リリース(#57340)であり、この誤ったドキュメントが公開版に含まれたことはありません。- そのため、
- 既存アプリケーションへの互換性影響: なし
- 今後
deep_transform_valuesを使う開発者にとって:- ドキュメントどおりに
inspectの出力を期待してデバッグしても、実際と一致するようになります。 - ネストが「自動で全部
Parametersになる」という誤解を招かなくなります。
- ドキュメントどおりに
注意点として、今後 Parameters を扱う際に、
- 「ネストしたキーも常に
ActionController::Parametersになる」とは限らず、 - 実際には「アクセス時にラップされることがある」という遅延ラップの仕様を意識する必要があります。
これは deep_transform_values に限らず、Strong Parameters 全般の性質です。
- 参考情報 (あれば)
- 対象メソッド:
ActionController::Parameters#deep_transform_values
(Ruby のHash#deep_transform_valuesに類似した API で、Strong Parameters 用のもの) - 関連PR:
- #57340 —
deep_transform_valuesの追加PR(このメソッド自体の導入)
- #57340 —
- Strong Parameters の遅延的な
Parametersラップ挙動については、ActionController::Parametersの実装および既存のメソッド(require,permitなど)のソースを読むと理解しやすいです。
#58235 Remove stale requires
マージ日: 2026/7/28 | 作成者: @55728
- 概要 (1-2文で)
このPRは、既にコードから使われていないrequireをまとめて削除し、不要な依存関係の読み込みを減らすクリーンアップです。挙動や公開APIの変更はなく、autoload に任せられる部分と参照されていない標準ライブラリのrequireを整理しています。
- 変更内容の詳細
2-1. 各種 LogSubscriber からの active_support/log_subscriber の削除
対象ファイル:
actionmailer/lib/action_mailer/log_subscriber.rbactionview/lib/action_view/log_subscriber.rbactivejob/lib/active_job/log_subscriber.rbactivestorage/lib/active_storage/log_subscriber.rb
これら4つのログサブスクライバは、いずれも以下のように記述されていました(イメージ):
require "active_support/log_subscriber" # ← これが削除対象
require "active_support/event_reporter"
module ActionMailer
class LogSubscriber < ActiveSupport::EventReporter::LogSubscriber
# ...
end
endしかし、実際に継承しているのは ActiveSupport::EventReporter::LogSubscriber であり、ActiveSupport::LogSubscriber はどこでも参照していません。このクラスは active_support/event_reporter.rb の autoload によって読み込まれるため、active_support/log_subscriber を明示的に require する必要はありません。
LogSubscriber 系の他のファイルとの比較:
| log subscriber ファイル | require "active_support/log_subscriber" |
|---|---|
active_record/log_subscriber.rb | なし |
action_controller/log_subscriber.rb | なし |
action_dispatch/log_subscriber.rb | なし |
action_mailer/log_subscriber.rb | あり → 削除 |
action_view/log_subscriber.rb | あり → 削除 |
active_job/log_subscriber.rb | あり → 削除 |
active_storage/log_subscriber.rb | あり → 削除 |
このPRで、この4つも他と同様に「不要な require を持たない」形に揃えています。
2-2. Active Storage Disk Service からの pathname / openssl の削除
対象ファイル:
activestorage/lib/active_storage/service/disk_service.rb
元々の先頭付近は概ね次のような構成になっていたと考えられます:
require "pathname" # ← 使われなくなった
require "openssl" # ← 使われなくなった
require "fileutils" # ← 継続利用
require "active_support/core_ext/numeric/bytes" # ← 継続利用変更点:
Pathnameはコミットc39e176eb5以降、コード中で一切参照されていないためrequire "pathname"を削除。OpenSSLもチェックサム処理が別の場所へ移動したコミットd1e31dd4f9以降、未使用のためrequire "openssl"を削除。- 一方で、
FileUtilsとactive_support/core_ext/numeric/bytesは引き続きコード中で使用されているため残しています。
実行パスに影響するのはあくまで「不要なライブラリを読み込まなくなる」点だけで、Disk Service の I/O やチェックサムの挙動自体は別のコードにすでに移されているため、このPRでは変わりません。
2-3. Action Text インストールジェネレータからの json の削除
対象ファイル:
actiontext/lib/generators/action_text/install/install_generator.rb
このジェネレータでは、以前は JSON クラスを利用していたため:
require "json" # ← 82e4432058 以降使われていない
require "pathname" # ← 引き続き使用中という状態でしたが、コミット 82e4432058 で JSON を使う処理が削除または書き換えられ、現在は JSON クラスを参照していません。そのため require "json" を削除し、まだ使っている pathname だけを残す形に整理しています。
- 影響範囲・注意点
ランタイム挙動:
- いずれの変更も「参照されていない定数を読み込むだけだった
requireを削除」するものであり、実際の処理フローや公開API、ログ出力内容などには影響しません。 - autoload 経由で読み込まれている
ActiveSupport::EventReporter::LogSubscriberは引き続き正常に機能します。
- いずれの変更も「参照されていない定数を読み込むだけだった
依存関係・初期化順序:
- 以前から
ActiveSupport::LogSubscriberに依存していなかったコード群なので、初期化順序の問題や依存解決の問題は発生しません。 - もしアプリ側が「これらのファイルが
active_support/log_subscriberを副作用的にrequireしてくれる」ことに暗黙依存していた場合のみ挙動が変わる可能性がありますが、通常はそのような前提は推奨されず、実務上もほぼない前提と考えられます。
- 以前から
パフォーマンス・メモリ:
- 読み込まれるライブラリが少し減るため、起動時のロード時間やメモリ使用量にわずかに良い影響がありますが、体感できるほどではないはずです。
gem / ライブラリ作者視点:
- Rails 内部の require が減ったことで、もし自作 gem が「そのファイルを require した結果として副次的に
pathname/openssl/json/active_support/log_subscriberが読まれること」に依存している場合は、自分の側で明示的にrequireする必要があります(ただし、そのような設計は避けるべきです)。
- Rails 内部の require が減ったことで、もし自作 gem が「そのファイルを require した結果として副次的に
- 参考情報 (あれば)
- 本PRで参照されている過去コミット:
Pathnameが不要になった:c39e176eb5- Disk Service からチェックサム処理が移動した:
d1e31dd4f9 - Action Text install generator から
JSON利用が消えた:82e4432058
- 類似の内部整理:
- すでに
active_record,action_controller,action_dispatchの各log_subscriberはactive_support/log_subscriberを require しておらず、今回の変更はそれらと仕様を揃える位置づけ。
- すでに
#58275 Don't swallow ::RangeError on write queries
マージ日: 2026/7/28 | 作成者: @kamipo
- 概要 (1-2文で)
QueryIntent#run_query!が全クエリで::RangeErrorを握りつぶしていた問題を修正し、SELECT では従来どおり「範囲外 WHERE → 空結果」を維持しつつ、INSERT/UPDATE/DELETE などの書き込みクエリではRangeError/ActiveModel::RangeErrorが正しく表に出るようにした PR です。これにより、本来シリアライズ時に発生すべき範囲外エラーが隠蔽されることなく、意図どおり例外として扱えるようになります。
- 変更内容の詳細
これまでの挙動と問題点
QueryIntent#run_query!内で::RangeErrorを rescue し、主に SELECT の WHERE 句で範囲外の値を指定した場合に例外ではなく「結果 0 件」にする、という挙動が実装されていました。- これは元々
select_allの近くでのみ行われており(0601a9f9aa)、読み取り専用の SELECT 問い合わせ向けの仕様でした。
- これは元々
- その後のリファクタリング(9051bc4332 など)で rescue が
QueryIntent#execute!→run_query!に移動した結果、
INSERT/UPDATE/DELETE 等の書き込みクエリにも同じRangeErrorrescue が効いてしまう状態になっていました。
ここで問題になるのが、書き込みクエリ + Arel 使用時です。
書き込みクエリで Arel ノードを使うと、以下の流れになります:
execute_intent
→processed_sql
→compile_arel!(この中で type/serialize が呼ばれる)
ここで属性の
serializeなどからActiveModel::RangeError(::RangeErrorのサブクラス)が発生しても、run_query!の rescue 範囲の中で発生するため、例外が握りつぶされる状態になっていました。その結果、本来は「シリアライズ時の範囲外値」という意味の
ActiveModel::RangeErrorが外に出ず、
代わりに後続のintent.affected_rows呼び出し時に:textRuntimeError: "Cannot call affected_rows after cast_result has been called"といった、文脈的に分かりづらい RuntimeError だけが表面化していました。
_exec_insertだけは、以下のようにintent.execute!を呼ぶ前にintent.raw_sqlを先に読む (= arel コンパイルが先に走る)- そのため、シリアライズ時の
RangeErrorがrun_query!の rescue 範囲の外側で起きる
という偶然により、問題を免れていました。
一方で
update/update_with_result/deleteは、そのような「先に raw_sql を読む」処理をしていなかったため、上記の問題の影響を受けていました。
今回の修正内容
QueryIntent#run_query!内で::RangeErrorを rescue している箇所に、
そのクエリが「書き込み(INSERT/UPDATE/DELETE)」かどうかで分岐し、書き込みの場合はRangeErrorを再スローするロジックを追加しています。- 読み取り(SELECT)は従来通り、「範囲外 → 空結果」の挙動を維持。
- 書き込みは「範囲外 → 例外発生」となるように戻しています。
※ PR の diff 自体は query_intent.rb に +1 行という小さな変更ですが、
中身としては「rescue 範囲の中で、書き込みクエリの場合は RangeError をそのまま再度 raise する」条件分岐が入っています。
テストの追加・変更
activerecord/test/cases/adapter_test.rb- 書き込みクエリに対して、
RangeError/ActiveModel::RangeErrorが正しく発生することを確認するテストを追加。 - 特に、Arel 経由の書き込み時に例外が握りつぶされず、元の例外が観測できることをテストしています。
- 書き込みクエリに対して、
activerecord/test/cases/adapters/abstract_mysql_adapter/unsigned_type_test.rb- MySQL 系アダプタでの unsigned 型など「範囲が厳密な型」に対し、
範囲外の値を書き込もうとした場合にActiveModel::RangeErrorが適切に発生することを確認するテストが追加・強化されています。
- MySQL 系アダプタでの unsigned 型など「範囲が厳密な型」に対し、
- 影響範囲・注意点
影響を受けるケース
- 対象: Rails 7 系(あるいはそれ以降)の Active Record で、以下の条件を満たすコード:
- Arel ノードを使って UPDATE / DELETE などの書き込みクエリを発行している
- カラムに対して範囲制約のある型 (unsigned int など) や、
カスタム type / serialize ロジックを使っており、その中でActiveModel::RangeErrorが起こり得る
- これまでは:
- 本来
ActiveModel::RangeErrorが起きるべきところで例外が握りつぶされ、 - 代わりに
intent.affected_rows呼び出し時のRuntimeError("Cannot call affected_rows ...")のみが見えていた
- 本来
- この PR 適用後は:
- 書き込みクエリにおいては
ActiveModel::RangeError/RangeErrorがそのまま表に出るようになります。 - そのため、これまで「よく分からない RuntimeError」として扱っていたケースが、「範囲外エラー」としてより意味のある形で見えるようになります。
- 書き込みクエリにおいては
後方互換性・挙動の違い
- SELECT クエリ(読み取り):
- WHERE 句に範囲外の値を渡した場合、「例外ではなく空結果を返す」という挙動はそのままです。
- INSERT/UPDATE/DELETE などの書き込みクエリ:
- 範囲外の値やシリアライズ時のエラーは 例外として表に出る 挙動に統一されます。
- 過去に一部アプリケーションが「RuntimeError を rescue していた」ような場合は、
今後はActiveModel::RangeError/RangeErrorを明示的に rescue する必要が出てくるかもしれません。
- 全体としては、「本来バグだった例外の握りつぶしをやめ、仕様に沿った挙動に戻す修正」であり、
意図しないサイレント失敗が減るため、多くのアプリケーションにとっては望ましい変更です。
- 参考情報 (あれば)
- 元々の実装経緯:
- 0601a9f9aa:
RangeErrorを SELECT の近くで捕捉する実装(select_all周り) - 9051bc4332: async 実行を
QueryIntentに移動する過程で rescue 範囲が広がり、結果として全クエリに適用されるようになってしまった
- 0601a9f9aa:
- 関連しうるコードパス:
ActiveRecord::ConnectionAdapters::QueryIntent#run_query!#execute_intent→#processed_sql→#compile_arel!_exec_insert,update,update_with_result,delete
- アプリ側の確認ポイント:
- これまで DB 書き込み時に謎の
RuntimeError: "Cannot call affected_rows after cast_result has been called"を見ていた場合、
この修正が入るバージョンでは、同じ操作でActiveModel::RangeErrorが代わりに出る可能性が高いです。
その場合は、範囲外値や型変換ロジックを見直すことで、問題箇所を特定しやすくなります。
- これまで DB 書き込み時に謎の
#58255 Deprecate insert_returning option and use_insert_returning?
マージ日: 2026/7/27 | 作成者: @kamipo
- 概要 (1-2文で)
PostgreSQL 用の DB 設定オプション:insert_returningと、それを参照するPostgreSQLAdapter#use_insert_returning?が「非推奨」としてマークされました。今後は RETURNING の有無はsupports_insert_returning?/supports_update_returning?などのサポート判定に一元化され、:insert_returningで挙動をコントロールすることはできなくなっていきます。
- 変更内容の詳細
2-1. 何が非推奨になったか
- PostgreSQL 用 DB 設定オプション:
:insert_returning- 例:
database.ymlなどで指定していた以下のようなオプションyamlproduction: adapter: postgresql database: my_app_production insert_returning: false - これが「deprecated(非推奨)」扱いになりました。
- 例:
- ActiveRecord アダプタメソッド:
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#use_insert_returning?:insert_returning設定値を読んで、INSERT に RETURNING を使うかどうか決めていたメソッド- これも非推奨としてマークされています。
2-2. なぜ非推奨になったか(背景)
:insert_returning は、単一行 INSERT を内部的に実行する _exec_insert 経路にしか影響していませんでした。
一方で、以下のような経路はすでに別判定で RETURNING を使っており、:insert_returning では制御できません:
insert_allupsert_allupdateに対する RETURNING
(supports_insert_returning?/supports_update_returning?による判定)
そのため:
- DB が RETURNING をサポートしている場合、
:insert_returningをfalseにしても「完全には RETURNING を止められない」 - オプションとしての一貫性がなく、役割が中途半端(事実上「名ばかり設定」= vestigial)
という状態だったため、オプションとそれを参照するメソッドごと非推奨に移行されました。
2-3. コード上の具体的な変更点(概要)
変更ファイルは以下の4つです。
activerecord/CHANGELOG.md- Rails の CHANGELOG に、
insert_returningオプションとuse_insert_returning?の非推奨が追記されています。
- Rails の CHANGELOG に、
activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb_exec_insert周辺で、use_insert_returning?の利用方法や条件が微調整されています(1行変更)。- 実質的には、「今後
use_insert_returning?に依存しない方向へ向かう」ことを示す変更です。
activerecord/lib/active_record/connection_adapters/postgresql_adapter.rbuse_insert_returning?メソッドに非推奨警告の付与:- メソッド呼び出し時に
ActiveSupport::Deprecationを通じて警告を出す形、もしくは同等の非推奨マークが追加されています。
- メソッド呼び出し時に
- DB 設定オプション
:insert_returningを読み取るロジックに対し、「この設定は非推奨」という位置づけのコメントや処理が追加されています。 - それに伴う小さなリファクタリング(行数的に +13/-3)も含まれています。
activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb- 新たに追加された非推奨挙動(
insert_returning設定やuse_insert_returning?)に対するテストが追加 (+3/-1) されています。 - これにより、将来のバージョンでも「非推奨警告がきちんと出る」挙動が担保されます。
- 新たに追加された非推奨挙動(
- 影響範囲・注意点
3-1. 既存アプリで insert_returning を使っている場合
database.yml などに insert_returning: false のような指定をしている場合:
- Rails のアップデート後、起動時または接続時に 非推奨警告(deprecation warning)がログに出る可能性 があります。
- ただし、もともとこのオプションは「単一行 INSERT の一部の経路」にしか影響しておらず、
insert_allupsert_allUPDATE ... RETURNINGなどの RETURNING は無効化できていませんでした。
- そのため、実際の挙動(RETURNING が発行されるかどうか)が大きく変わるわけではなく、「設定しても期待どおりに全体を制御できないオプションが、正式に非推奨と宣言された」という位置づけです。
今後は以下の方針が推奨されます:
insert_returning設定は削除する(Rails 推奨のデフォルト挙動に任せる)- RETURNING のサポート可否は:
supports_insert_returning?supports_update_returning?いずれも アダプタが DB の機能検出に基づいて自動判定 することを前提にする。
3-2. use_insert_returning? を直接呼んでいるライブラリ・アプリ
アプリや gem コードの中で、以下のような呼び出しをしている場合:
adapter = ActiveRecord::Base.connection
if adapter.use_insert_returning?
# RETURNING 前提の処理
end- このメソッド呼び出し時に deprecation warning が発生 するようになります。
- 将来のバージョンで削除される可能性が高いので、次のような対応が必要です:
- そもそも
use_insert_returning?に依存しない設計にする - どうしても必要であれば
supports_insert_returning?- 必要に応じて
supports_update_returning?を利用して判断する
- そもそも
3-3. RETURNING を完全にオフにしたいケース
この PR によって明確になった点として:
- Rails の内部は「DB が RETURNING をサポートしているなら使う」という前提で動いており、
- 単純な設定オプションで「一切 RETURNING を使わない」モードにすることは想定していない
という設計方針が確認できます。
もしアプリケーション都合で「RETURNING を使いたくない」場合は:
- アダプタレベルでパッチを当てる
- カスタムコネクションアダプタを作る
- Arel/SQL を自前で組み立てる
といった、より踏み込んだ対応が必要になることが示唆されます。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58255
- 関連機能:
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#supports_insert_returning?ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#supports_update_returning?
insert_all/upsert_allは、Rails 6 以降で導入されたバルク INSERT / UPSERT 機能で、PostgreSQL では RETURNING を活用することで挿入・更新後のレコード情報を効率的に取得する設計になっています。この PR は、その流れの中で「中途半端に RETURNING を止めようとする古い設定を整理する」位置づけと捉えると理解しやすいです。
#58259 Delegate ids back to pluck
マージ日: 2026/7/27 | 作成者: @kamipo
- 概要 (1-2文で)
Relation#idsの実装をpluck(primary_key)ベースに戻し、idsとpluckの挙動を再び揃えることで、これまで片方だけに入っていたバグ修正の非対称性を解消した PRです。includes利用時の重複 ID 除去だけを特別扱いとして残し、それ以外はpluckに処理を委譲するようになりました。
- 変更内容の詳細
2-1. 以前の経緯
- もともと (
#46503以前) のids:rubydef ids pluck(primary_key) end #46503での変更理由:includesを使ったときに JOIN の結果が重複し、idsが重複した ID を返していた:rubyPost.includes(:comments).where(id: 1).ids # 期待: [1] # 実際: [1, 1, 1, 1, 1] など- これを解消するため
idsがpluckとは別実装になり、「重複除去ロジック」を自前で持つようになった。
その後、ids と pluck の実装が分かれたことにより、次のような「どちらか片方だけ直す」状態が発生していました:
- #47762:
ids側では複合主キー (Composite Primary Key, CPK) をサポートできていなかった。 - #51167:
ids_readerが CPK の関連で重複行を返すバグがあり、idsまわりの仕様と実装が複雑化。 - #58004:
none.idsが実クエリSELECT ... WHERE (1=0)を投げてしまう一方で、none.pluck(:id)はショートサーキットで[]を返していた。
2-2. この PR の実装変更
この PR のポイントは:
- 「
idsの本体処理はpluck(primary_key)に全面委譲する」 - 「ただし
includes等でhas_include?(primary_key)が true のときだけ、ids側で重複除去する」
という形に戻したことです。
擬似コード的には以下のようなイメージです(実コードはもっと短くなっていますが、概念的にはこういう分岐):
def ids
if has_include?(primary_key)
# includes による重複行をまとめる
pluck(primary_key).uniq
else
# 通常ケースは pluck に丸投げ
pluck(primary_key)
end
end実際の diff では:
activerecord/lib/active_record/relation/calculations.rbのids独自実装に関するコードがほぼ全削除(約 39 行削除)pluck呼び出しと重複除去に関わる最小限のコードだけが残る(約 3 行追加)
- これにより、クエリ発行や
CPK対応、noneへの最適化などはすべてpluckに統一されます。
2-3. ログタグの変更
- これまで:
ids実行時のログタグが"Model Ids"になっていた - この PR 後:
pluckに委譲するため"Model Pluck"になる
つまり、ログは再び以下のような形に揃います:
Model Pluck (0.2ms) SELECT "posts"."id" FROM "posts" ...これは #46503 以前の挙動に戻り、pick ともログラベルが揃います。
- 影響範囲・注意点
3-1. 挙動の変化が起こりうるケース
none.idsの挙動- 以前:
none.idsが実際にSELECT ... WHERE (1=0)を叩いていた - 変更後:
none.pluck(:id)と同様に、DB クエリなしで[]を返す(ショートサーキット) - 影響:
- パフォーマンス的には改善 (無駄なクエリが消える)
- もしテストなどで「クエリが発行されること」を前提にしていた場合は失敗する可能性あり
- 以前:
複合主キー (CPK) を使っている場合
idsが内部的にpluck(primary_key)を使うため、CPK 対応はpluckの仕様に完全追従します。- これまで
idsとpluckの挙動がずれていた部分があれば、その差が解消される代わりに「pluckの仕様に統一される」ため、CPK まわりの微妙なワークアラウンドがあれば要確認です。
ログ監視・メトリクスで
Model Idsタグを使っていた場合- ログタグが
"Model Pluck"に変わるため、既存のログ解析・メトリクスがModel Idsに依存している場合は修正が必要です。
- ログタグが
3-2. 互換性・今後のメンテナンス性
idsとpluckを再び同じパスに乗せることで、将来pluckに対して行った最適化・バグ修正が、自動的にidsにも反映されるようになります。- コードベースとしても
idsのロジックが大きく削減されたため、保守コストが下がり、Relation計算関連の非対称バグが出にくくなります。
- 参考情報 (あれば)
- この PR で言及されている過去の issue / PR:
- #46503:
idsをpluckから分離し、includes時の重複 ID 問題を解消した元の変更 - #47762:
idsの複合主キー対応問題 - #51167: CPK の関連での
ids_reader重複問題 - #58004:
none.idsが無駄なクエリを発行していた問題
- #46503:
運用上は、ids を pluck(:id) のシンタックスシュガーとみなしつつ、「includes 等で ID が重複しうるときだけ uniq を噛ませるメソッド」と理解しておくとよいです。
#58258 Remove unused binds argument from update_with_result
マージ日: 2026/7/27 | 作成者: @kamipo
- 概要 (1-2文で)
update_with_resultメソッドの引数から、実際には使われていなかったbinds引数が削除されました。Arel が自前でバインド値を持つようになった現在の設計に沿って、インターフェイスを整理するリファクタリングです。
- 変更内容の詳細
対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
やっていることは非常に小さく、update_with_result メソッドのシグネチャから binds を取り除いただけです。
イメージとしては、以下のような差分です(擬似コードですが方向性はこれと同じ):
# 変更前
def update_with_result(arel, binds = [], table_name = nil, primary_key = nil, lock = nil, key = nil)
# ...
end
# 変更後
def update_with_result(arel, table_name = nil, primary_key = nil, lock = nil, key = nil)
# ...
end説明文で述べられている背景:
- コミット
213796fb4936dce1da2f0c097a054e1af5c25c2c以降、Arel オブジェクト自身がバインド値を持つようになった。 - そのため、Arel AST(
arel)と別途bindsを同時に渡すのは設計として禁止されている(to_sql_and_bindsを参照)。 update_with_result内ではarel.returning(...)を呼んでいるため、arelは常に Arel のステートメントマネージャ であり、生の SQL 文字列にはならない。- 実際の唯一の呼び出し元である
_update_record_with_resultもbindsを使っておらず、binds引数は実質的に「使われない」引数になっていた。
このため、「そもそも意味のない・使い道のない binds 引数」を削除して API をシンプルにした、という変更です。
- 影響範囲・注意点
Rails 内部実装に閉じたメソッドなら影響はほぼなし
説明によるとupdate_with_resultの呼び出し元は_update_record_with_resultのみで、そこでもbindsは利用されていませんでした。Rails 本体の中では動作上の挙動は変わらないと考えられます。Monkey patch や独自アダプタで
update_with_resultを直接呼んでいる場合に注意- もしアプリケーションや gem 側で
ActiveRecord::ConnectionAdapters::DatabaseStatements#update_with_resultを直接呼び出し、bindsを渡していた場合は、メソッドシグネチャの変更でエラーになります。 - ただし、Arel がバインド値を保持する現在の設計では、もともと
bindsを併用するのは「やってはいけない」使い方なので、これを機に見直す必要があります。 - 正しい形は、Arel の AST 構築時にバインド値(bind parameters)を設定するか、ActiveRecord が提供する高レベル API を通じて更新を行うことです。
- もしアプリケーションや gem 側で
生 SQL と
bindsを組み合わせてupdate_with_resultする用途には非対応
説明にもある通りupdate_with_resultはarel.returning(...)を前提としており、ここで渡すのは Arel オブジェクトのみです。生 SQL + binds で同等のことをしたい場合は、exec_update/exec_queryなど別のメソッドを使用すべきです。
- 参考情報 (あれば)
- この PR 内で参照されているコミット:
213796fb4936dce1da2f0c097a054e1af5c25c2c- ここから「Arel が自前でバインド値を持つ」設計になり、
to_sql_and_bindsで Arel と binds 併用は禁止されるようになった。
- ここから「Arel が自前でバインド値を持つ」設計になり、
- 関連メソッド:
ActiveRecord::ConnectionAdapters::DatabaseStatements#to_sql_and_bindsActiveRecord::ConnectionAdapters::DatabaseStatements#exec_updateActiveRecord::Relation#update_all(内部で最終的に更新系メソッドをたどる際にこのあたりが使われる)
#58256 [ci skip] Update Mailgun setup documentation links
マージ日: 2026/7/27 | 作成者: @ajaynomics
- 概要 (1-2文で)
Mailgun を使った Action Mailbox 連携のドキュメント内リンクが古くなっていたため、Mailgun が公式に案内しているforward()アクション用ドキュメントへのリンクに更新した PRです。Action Mailbox が依存しているmimeURL サフィックスとbody-mimeペイロード仕様を説明している正しいページを参照するようになります。
- 変更内容の詳細
対象箇所
actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rbguides/source/action_mailbox_basics.md
いずれも Mailgun の設定方法を案内する URL 文字列のみが差し替えられています(+1/-1 行ずつ)。
コード上のロジック(パラメータ処理、セキュリティチェックなど)には一切手が入っていません。
想定される変更イメージ
※PR本文から読み取れる情報をもとにした概念的な例です(実際の URL は Rails 本家リポジトリを参照してください)。
# 変更前(例)
# See Mailgun HTTP forwarding docs:
# https://documentation.mailgun.com/en/latest/user_manual.html#routes-http-forwarding
# 変更後(例)
# See Mailgun forward() action docs (mime/body-mime):
# https://documentation.mailgun.com/en/latest/user_manual.html#routes-forwardガイド側 (action_mailbox_basics.md) も同様に、Mailgun 設定手順の説明中に出てくるリンク先を、
- 「HTTP フォワーディング」の古い/不適切なページ
→ 「forward()アクションで受信する方法」を説明する正規のページ
に差し替えています。
このターゲットページには、Action Mailbox で Mailgun ingress を使う際に重要な
mimeURL サフィックスbody-mimeパラメータに raw MIME メッセージが載る
といった仕様が明示されているため、Rails 側の説明と Mailgun 側の仕様が対応するようになりました。
動作確認コマンド
PR では以下のコマンドで検証が行われています:
bundle exec rake guides:generate ONLY=action_mailbox_basics
bundle exec rake guides:lint:check_links ONLY=action_mailbox_basics
bundle exec rake rdoc- ガイドの生成が通ること
- ガイド内リンクがリンクチェッカでエラーにならないこと
- API ドキュメント (rdoc) が生成できること
を確認済みです。
- 影響範囲・注意点
影響範囲
- 実行時のアプリケーション動作には影響なし
- コントローラのロジックや Action Mailbox の挙動は一切変わっていません。
- ドキュメント閲覧時の UX 改善
- Mailgun 設定時に参照するリンクが、目的の情報(
forward()アクション、mimeサフィックス、body-mime)をきちんと含むページに変わるため、導入時の迷いが減ります。
- Mailgun 設定時に参照するリンクが、目的の情報(
注意点
- 既に Mailgun + Action Mailbox を運用しているユーザが、過去の Rails ガイドに書かれた古いリンクをブックマークしている場合、そのリンクは引き続き動作するかもしれませんが、最新の推奨設定はこの PR で指しているページ側になります。
- Action Mailbox 側は従来通り「
/rails/action_mailbox/mailgun/inbound_emails/mimeへの HTTP POST でbody-mimeを受け取る」という設計なので、Mailgun 側の route 設定がこれに合うように構成されているか再確認する際に、新しいリンク先が有用です。
- 参考情報 (あれば)
- Rails ガイド: Action Mailbox Basics(該当セクション:Mailgun ingress の設定)
- Mailgun 公式ドキュメント(
forward()アクション、mimeURL サフィックス、body-mimepayload の説明があるページ) - Rails リポジトリの該当ファイル:
actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rbguides/source/action_mailbox_basics.md
#58257 Require Markdown APIs in new files
マージ日: 2026/7/27 | 作成者: @fxn
- 概要 (1-2文で)
Rails の API ドキュメントを RDoc から Markdown へ段階的に移行している最中に、新規ファイルへ RDoc 記法が紛れ込むのを防ぐため、CI 上で「新しい Ruby ファイルの API コメントが Markdown かどうか」をチェックする仕組みが追加されました。
これにより、以降追加されるコードは原則として Markdown 形式でドキュメントを書くことが求められます。
- 変更内容の詳細(あればサンプルコードも含めて)
追加されたもの
変更は 2 ファイルのみで、どちらも GitHub Actions とその補助スクリプトです。
.github/workflows/check-markdown-api.yml- 新しい GitHub Actions ワークフローが追加されています。
- 目的: プルリクエスト内の「新規ファイル」に対して、API ドキュメントが Markdown で書かれているかを検査する。
- 実行タイミング:
- おそらく
pull_requestまたはpull_request_targetトリガー(ファイル名から推測)で、PR 作成時・更新時に走るチェックです。
- おそらく
- 処理内容(典型的な構成の推測を含む):
- リポジトリをチェックアウト
- Ruby をセットアップ
check-markdown-api.rbを実行して、PR に含まれる新規ファイルを走査- 規則に違反していればワークフローを失敗させる
.github/workflows/scripts/check-markdown-api.rb- 実際の検証ロジックを持つ Ruby スクリプトです。
- 主な役割:
- 「新規に追加されたファイル」を検出する
- 典型的には
git diff --name-status origin/main...HEAD等を見て、A(Added) のファイルのみ対象にする処理が含まれているはずです。
- 典型的には
- その中から対象とするファイル(例えば
*.rb)を抽出 - API ドキュメントコメントをチェックして、Markdown 以外(RDoc など)の記法が含まれていないかを検査
- 「新規に追加されたファイル」を検出する
- 具体的に何を NG とみなすかはスクリプト実装に依存しますが、例えば以下のような RDoc 特有の記法を禁止している可能性があります:
# :nodoc:,# :yields:などの RDoc タグ# == 見出しなど古いスタイルの見出し{ClassName}形式のリンク表現 など
- 逆に、Markdown として期待されるのは以下のような書き方です:rubyRails 本体では Yard タグと Markdown を組み合わせているケースもあるため、
# Public: ユーザーを作成する # # @example # User.create!(name: "Taro") # # @param name [String] ユーザー名 # @return [User] 作成されたユーザー class User # ... endcheck-markdown-api.rbはそうした現在のスタイルに準拠しているはずです。
- 影響範囲・注意点
- 対象: 「Rails 本体」への PR のうち、新規ファイルを追加するもの
既存ファイルの変更は現時点では直接対象外(ただし将来的にチェック範囲が広がる可能性はあります)。 - 実務上の影響:
- Rails に貢献する開発者は、以下を意識する必要があります:
- 新しく
.rbファイルを追加する場合、その API ドキュメントは Markdown 形式で書くこと。 - 旧来の RDoc 記法をうっかり使うと、CI(GitHub Actions)の
check-markdown-apiジョブが失敗して PR が red になります。
- 新しく
- Rails に貢献する開発者は、以下を意識する必要があります:
- プロジェクト内ポリシーとしての意味:
- API ドキュメントを段階的に Markdown に移行している最中のため:
- 既存の RDoc 記述は残っていてもよい(移行対象)
- 新規に RDoc を増やすことは禁止、というガードレールを CI で強制する形になっています。
- 移行期間中に混在がさらに悪化するのを防ぐための措置です。
- API ドキュメントを段階的に Markdown に移行している最中のため:
- 参考情報 (あれば)
- PR 本文:
- 「We are gradually converting our API to Markdown.
By requiring new files to be documented in Markdown we prevent RDoc markup from being introduced while the migration is ongoing.」
という説明からも、「完全移行はまだだが、新規分だけは Markdown を強制する」という方針が明示されています。
- 「We are gradually converting our API to Markdown.
- 類似の取り組み:
- 他プロジェクトでも、RDoc → Markdown, Javadoc → Markdown などの移行時に、新規コードへ旧形式を使わせないための CI チェックを挟むのはよくあるパターンです。
- Rails も同様に、技術的負債の増加を抑えつつドキュメントフォーマットを統一しようとしています。
#58252 Fix: infinite route reload when after_routes_loaded accesses routes
マージ日: 2026/7/26 | 作成者: @chaadow
- 概要 (1-2文で)
Rails のルーティング再読み込み処理において、after_routes_loadedフック内でルートセットにアクセスすると無限にルートが再読み込みされてしまう不具合を修正した PR です。execute_unless_loadedのクリティカルセクション(ルート定義とフック実行の一連の流れ)全体でロード状態を「loading」に保つように変更しています。
- 変更内容の詳細
問題の背景
- #58225 の変更後、
reload!実行時に@load_stateがnilに戻されるタイミングが早すぎました。 - 具体的には、ルートを描画したあと、
after_routes_loadedフックが実行される「前」に@load_stateがnilに戻っていたため、- フックの中でルートセット(例:
Rails.application.routesや関連メソッド)に触れると、 execute_unless_loadedが「まだロードされていない」と誤認して、再度ルート描画を開始- その中でまたフックが動き…と再帰的に呼ばれ続け、結果として 無限なルート再読み込み 状態に陥る可能性がありました。
- フックの中でルートセット(例:
対応方針
PR 説明の要約:
execute_unless_loadedのクリティカルセクション全体(ルートの描画とafter_routes_loadedフックの実行を含む)で状態を:loadingのまま保持し、フック完了後に初めて:loadedにする。失敗した場合はnilに戻してリトライを許可する。
つまり:
- これまで
- ルート描画完了 →
@load_state = nil→ その後after_routes_loaded実行
- ルート描画完了 →
- これから
execute_unless_loaded開始 →@load_state = :loading- ルート描画 →
after_routes_loaded実行 - 全て成功 →
@load_state = :loaded - 途中で失敗 →
@load_state = nil(次のリトライで再度描画できる)
コード上のポイント(概念レベル)
railties/lib/rails/application/routes_reloader.rb の変更の要点は以下です:
execute_unless_loaded内で- ルートの
draw(実際のルーティング定義) after_routes_loadedフックの呼び出し
を ひとまとまりの「ロード中」状態 (:loading) として扱うようにした。
- ルートの
処理がすべて正常に完了した時点でのみ
@load_state = :loadedにする。途中で例外などにより失敗した場合は
@load_stateをnilに戻すことで、- 次回の
execute_unless_loaded呼び出し時に再度ルート描画を行える(リトライ可能) - ただし「途中までロードされた不整合な状態」で
:loadedと誤認されないようにする。
- 次回の
テスト追加
railties/test/application/loading_test.rb に +33 行のテストが追加されており、主に以下を確認していると考えられます(PR の説明からの推測ですが、Rails の既存パターンと整合的です):
after_routes_loadedフックの中でルートにアクセスしても 無限再読み込みが発生しないこと。- フック実行中に
execute_unless_loadedを再入しても、@load_stateが:loadingのため再度の描画が走らないこと。 - 例外発生時には
@load_stateがnilに戻り、次回のロードが正しく行われること。
- 影響範囲・注意点
影響範囲
Rails アプリケーションで
after_routes_loadedフック(またはそれと等価の拡張ポイント)を利用している場合に挙動が変わります。- 以前は、フック内でルートに触れると意図せず
execute_unless_loadedが再実行される可能性がありました。 - この PR により、そのような再入が防がれるため、無限ループや過剰な再読み込みが解消されます。
- 以前は、フック内でルートに触れると意図せず
特に以下のようなコードを書いている場合に、この修正の恩恵があります:
ruby# 擬似コード(エンジンやアプリケーション設定で) config.after_initialize do Rails.application.routes_reloader.after_routes_loaded do # ここでルート情報に触れる Rails.application.routes.named_routes.helpers end endこうしたフック内のルートアクセスが、以前は
execute_unless_loadedを再入させていた可能性があります。
注意点
@load_stateのライフサイクルがより厳密になったため、- 「ルート描画は終わっているが、フックがまだ」という中途半端な状態は
:loadingとみなされます。 - この期間中に
execute_unless_loadedを呼び出しても、新たな描画は行われません(=期待する挙動)。
- 「ルート描画は終わっているが、フックがまだ」という中途半端な状態は
ルーティングやフック処理の中で意図的に再読み込みをトリガーするような「かなりトリッキーな」コードを書いていた場合、
- その前提が崩れる可能性がありますが、これはむしろバグ防止として望ましい方向です。
例外が発生した場合に
@load_stateがnilに戻るため、- 例外後の次の要求などでルートの再描画が走る可能性があります。
- ルート定義やフック内で例外が起きた場合の挙動を確認しておくと安心です。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58252
- 関連 PR(本件の原因となった変更): https://github.com/rails/rails/pull/58225
- 関連する内部クラス:
Rails::Application::RoutesReloaderexecute_unless_loaded/reload!周辺の実装
この PR により、ルートリロード時の状態管理がより一貫したものになり、after_routes_loaded を使う高度なカスタマイズが安全に行えるようになっています。
#58250 Fix number_to_human_size crashing above a terabyte
マージ日: 2026/7/26 | 作成者: @55728
- 概要 (1-2文で)
number_to_human_sizeが 1TB を超えるサイズをロケール依存でフォーマットする際、一部ロケールでTypeErrorが発生していた問題を修正し、ペタ/エクサ/ゼタバイトに対しても英語ベースのフォールバックが効くようになりました。これにより、対応する翻訳がないロケールでも"1 PB","1 EB","1 ZB"などの文字列を返し、例外を出さずに動作します。
- 変更内容の詳細
問題の内容
number_to_human_size は、locale で指定されたロケールに適切な翻訳があればそれを使い、なければ Ruby 側にハードコードされたフォールバック表 (DEFAULTS) を使う実装になっています。
- 既存の
DEFAULTSはbyte,kb,mb,gb,tbまでは定義されていたが、 petabyte,exabyte,zettabyteは追加されていなかったため、
例えば以下のような状況が起きていました:
# CS (チェコ語) ロケールの例: terabyte までは翻訳が存在し、それ以降はない
number_to_human_size(1024 ** 4, locale: :cs)
# Before: "1 TB" # TB の翻訳 or フォールバックは存在
number_to_human_size(1024 ** 5, locale: :cs)
# Before: TypeError: no implicit conversion of nil into String
# (PB 用のフォールバックがなく、nil を String にしようとしてエラー)これは、2015年(#22732) および 2023年(#47771) の PR でヘルパーと英語ロケールには petabyte, exabyte, zettabyte が追加された一方で、フォールバック表 (DEFAULTS) だけが更新されていなかったことによるギャップです。
この PR での修正内容
DEFAULTS(フォールバックで使う単位名テーブル)に、以下の 3 つの単位を追加しています。
petabyte->"PB"exabyte->"EB"zettabyte->"ZB"
※ 実際のコード上では、number_converter.rb 内の DEFAULTS 定数に 3 エントリ追加 (+4/-1) されています。
それに合わせて、activesupport/test/number_helper_i18n_test.rb にテストが追加されています (+3 行)。これにより、翻訳が存在しない状況でも以下のような動作になります:
number_to_human_size(1024 ** 4, locale: :cs)
# After: "1 TB" # ここは従来と同じ
number_to_human_size(1024 ** 5, locale: :cs)
# After: "1 PB" # 翻訳がなくてもフォールバックで "PB" を返すなお、number_to_human の decimal_units については、もともと英語ロケールと完全に揃ったフォールバックを持っており、この PR の影響は受けません。
- 影響範囲・注意点
影響するケース
number_to_human_sizeを利用しており、:en以外のロケールを指定していて、- そのロケールの翻訳ファイルに
petabyte,exabyte,zettabyteが定義されていない場合、 - かつ 1TB(
1024 ** 4)を超える値をフォーマットするとき。
挙動の変化
- 以前は上記条件で
TypeErrorが発生していたが、今後は"1 PB","1 EB","1 ZB"などの英語ベースの単位が返る。 - 対応する翻訳が存在するロケールでは、従来どおり翻訳が優先されるため挙動は変わらない。
- 以前は上記条件で
言語ミスマッチについて
- フォールバックは
byte,kb,mb,gb,tb同様、英語表記 ("PB","EB","ZB") です。 rails-i18nに含まれる 122 ロケールのうち:- 58 が
petabyte未翻訳 - 59 が
exabyte未翻訳 - 116 が
zettabyte未翻訳
- 58 が
- ただし、それらロケールの多くは既に
"TB"のような英字表記を使っているため、フォールバックの"PB"なども自然に見えるケースが多い。 - 全く別表記(例:
"ТБ","To","ترابایت"など)を使うロケールでは"PB"とのミスマッチは起こるが、例外で落ちるよりは明らかにまし、という判断になっています。
- フォールバックは
アプリ側での注意・対応案
- ロケール整備が行き届いているアプリで完全な翻訳を求める場合:
pb,eb,zbなどのキーを各ロケールに追加しておくことで、今回のフォールバックに頼らずに済みます。
- ログ/監視などで 1TB 超の値を扱っている場合:
- 今回まで気づいていなかった潜在的な
TypeErrorがあったかもしれませんが、この PR によって解消されます。
- 今回まで気づいていなかった潜在的な
- ロケール整備が行き届いているアプリで完全な翻訳を求める場合:
- 参考情報 (あれば)
- 当該 PR:
- rails/rails #58250 — Fix
number_to_human_sizecrashing above a terabyte
- rails/rails #58250 — Fix
- 関連 PR:
- #22732:
number_to_human_sizeに petabyte などが初めて追加された PR (2015) - #47771: exabyte, zettabyte 追加などを行った PR (2023)
- #22732:
- ロケール翻訳リポジトリ:
- rails-i18n: 各種ロケールでのストレージ単位翻訳の状態が確認可能
#58249 Extract :table_name as explicit kwarg in join table helpers
マージ日: 2026/7/26 | 作成者: @kamipo
- 概要 (1-2文で)
create_join_table,build_create_join_table_definition,drop_join_tableで:table_nameを「単なる options の1要素」から「明示的なキーワード引数」に変更し、optionsをその場で破壊的変更していた補助メソッドfind_join_table_nameとJoinTableモジュールを削除したリファクタリングです。これにより API が明示的になり、CommandRecorderからの不要なinclude JoinTableも除去されています。
- 変更内容の詳細
2-1. join table helper のシグネチャ変更
対象メソッド:
create_join_tablebuild_create_join_table_definitiondrop_join_table
これらは以前、
def create_join_table(table_1, table_2, **options)
# options[:table_name] を find_join_table_name が掘り出していた
endのように :table_name を **options に含めて受け取り、内部で
find_join_table_name(options)がoptions.delete(:table_name)して join table 名を確定- 残りの
optionsをcreate_table/drop_tableにそのまま渡す
という実装になっていました。
この PR では、:table_name を明示的なキーワード引数として受け取る形に変更しています。概念的には次のような形になります:
def create_join_table(table_1, table_2, table_name: nil, **options)
# table_name を明示的に扱い、options には含めない
# create_table(join_name, **options)
end
def drop_join_table(table_1, table_2, table_name: nil, **options)
# 同様に table_name を分離
end実際のコードでは、table_name が指定されていない場合は従来通り join_table_name(table_1, table_2) でデフォルトの join table 名を作成し、そのうえで create_table / drop_table に渡す、という挙動は変わりません。
この結果、options の中に :table_name が残ったまま下位メソッド (create_table など) に渡されることがなくなり、create_table 側のオプションバリデーションに引っかかることもなくなります(従来はこれを避けるために delete していた)。
2-2. ActiveRecord::Migration::JoinTable モジュールの削除
activerecord/lib/active_record/migration/join_table.rb から、JoinTable モジュールが削除されています。中身は以下の2メソッドのみでした:
find_join_table_namejoin_table_name(2つのテーブル名から join table 名を決めるもの)
今回の変更で
find_join_table_nameは不要になった(:table_nameをキーワード引数で直接受けるため)join_table_nameはSchemaStatements側に移された/すでに存在する方を使う形になった
ため、このファイル自体が不要になっています。
2-3. CommandRecorder からの dead code 除去
ActiveRecord::Migration::CommandRecorder には以前から include JoinTable が入っていましたが、PR 説明にもある通り:
- 2012年のコミット
bd155d2ae3で、invert_create_join_tableの戻り値が- 以前:
[:drop_table, [resolved_join_table_name]] - 以後:
[:drop_join_table, args, block]に変更された
- 以前:
- それ以降、
CommandRecorder内でJoinTableのメソッド (find_join_table_name等) を呼ぶパスは存在しなくなっていた
つまり、CommandRecorder における include JoinTable は死んだコードになっていたため、この PR で削除されています。
2-4. テストの微調整
activerecord/test/cases/migration/schema_definitions_test.rb で、create_join_table / drop_join_table 周りの仕様に沿うよう、table_name の扱いに関するテストが 2行追加・2行削除されています。振る舞いとしては同等になるように保たれており、外部 API 変更を示すものではありません。
- 影響範囲・注意点
3-1. パブリック API の互換性
Migration の DSL としての create_join_table / drop_join_table の使い方は、通常の利用方法では互換性が保たれています。
典型的な利用例:
create_join_table :authors, :books
create_join_table :authors, :books, table_name: :authorships
drop_join_table :authors, :books
drop_join_table :authors, :books, table_name: :authorships上記のような書き方は、従来と同じように動作します。
3-2. 影響を受ける可能性があるケース
影響があり得るのは、次のような「かなり内部寄り」の使い方をしている場合です。
optionsハッシュをいじる前提でcreate_join_tableを呼んでいるコード
例:rubyopts = { table_name: :authorships, force: true } create_join_table(:authors, :books, **opts) # 呼び出し後に opts[:table_name] が消えることを期待しているようなコード以前は
find_join_table_nameがoptions.delete(:table_name)を行っていたため、呼び出し元のハッシュが破壊的に変更されていました。
今回、:table_nameはキーワード引数として扱われるため、そのような副作用は発生しません。
この挙動に依存しているコードはほぼ無いと思われますが、「副作用が無くなる」変更点としては押さえておく価値があります。ActiveRecord::Migration::JoinTableモジュールやfind_join_table_nameに直接依存しているコード
Rails のパブリック API ではなく内部実装寄りなので、本来依存すべきではない部分ですが、もしJoinTableを直接 include してjoin_table_name/find_join_table_nameを呼んでいた場合、そのコードは壊れます。
こうした用途がある場合は、join_table_nameを定義している現行の場所(SchemaStatements配下)から利用するか、自前で join table 名決定ロジックを持つように修正する必要があります。CommandRecorder+ 独自の inversion ロジックを拡張しているケースCommandRecorderに対するinclude JoinTableが削除されたことで、CommandRecorder内でJoinTableのメソッドが見えなくなります。
ただし PR 説明通り、既に Rails 本体のコードからは利用されておらず dead code だったため、通常のアプリケーション / エンジンでは影響は無い想定です。
3-3. オプション検証の観点
今回の主眼の1つは、「create_table / drop_table のオプション検証において、想定外の :table_name キーが紛れ込まないようにする」点です。
- 以前:
optionsに:table_nameを入れておき、create_table呼び出し前にoptions.delete(:table_name)で削除 - 現在:
table_name:でキーワード引数として受け取り、最初からoptionsには入れない
上記により、create_table などのオプションバリデーションコードを素直に保ちやすくなっています。
この意味で、「内部実装の健全化」「将来的なオプション追加・検証強化への布石」という側面があります。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58249
- 関連する過去コミット:
invert_create_join_tableの戻り値変更bd155d2ae3(2012) —[:drop_table, [resolved_join_table_name]]→[:drop_join_table, args, block]に変更
- 該当コード周辺:
activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rbactiverecord/lib/active_record/migration/command_recorder.rb- テスト:
activerecord/test/cases/migration/schema_definitions_test.rb
実務的には「join table 用の Migration DSL の使い方はこれまで通り」でありながら、「内部実装はより明示的で安全になった」という整理と捉えると理解しやすい変更です。
#58248 Fix inaccurate docstring in CommandRecorder#inverse_of [ci-skip]
マージ日: 2026/7/26 | 作成者: @kamipo
概要 (1-2文で)
ActiveRecord::Migration::CommandRecorder#inverse_ofのドキュメントコメント(docstring)が、現在の実装と食い違っていたため、古い挙動を前提にした説明とサンプルが削除されました。コード本体の挙動変更はなく、ドキュメントのみの修正です。変更内容の詳細
対象ファイル:
activerecord/lib/active_record/migration/command_recorder.rb変更点:
CommandRecorder#inverse_ofのコメントから、以下のような内容が削除されています。- 「あるコマンドの逆操作が複数のコマンドから成る場合は、コマンドの配列を返す」という説明
- その具体例として挙げられていた
invert_remove_columnsの説明・サンプル
背景となる実装の変化:
- もともと
invert_remove_columnsは、「複数カラムの削除」に対する逆操作として、「複数の[:add_column, ...]コマンドの配列」を返していました。 - しかし 3582de977d (2020年) のリファクタで、逆操作は「単一の
[:add_columns, ...]コマンド」にフラット化されるようになりました。- 例(イメージ):
- 以前のイメージ:ruby
[ [:add_column, :users, :name, :string, **options1], [:add_column, :users, :age, :integer, **options2] ] - 現在のイメージ:ruby
[ [:add_columns, :users, [[:name, :string, options1], [:age, :integer, options2]]] ]
- 以前のイメージ:
- 例(イメージ):
- にもかかわらず docstring 側が「複数コマンドの配列を返す」という古い仕様のまま残っていたため、その部分の記述と例が削除されています。
- もともと
実コードのふるまい:
- この PR では Ruby コード自体は一切変更されていません(追加 0 行 / 削除 5 行、すべてコメント)。
CommandRecorder#inverse_ofの挙動や、マイグレーションの実行結果には一切影響しません。
- 影響範囲・注意点
影響範囲:
- 実行時の挙動・API には影響なし(ドキュメントのみの修正)。
- ただし、以下のようなケースで「前提としていた仕様」がドキュメントと食い違っていたことが明示化される:
CommandRecorder#inverse_ofが「複数コマンドの配列を返す」ことを前提に、自前でメタプログラミングしているツール・ライブラリ- 古い docstring を読んで、
invert_remove_columnsの戻り値形式を誤解していた開発者
注意点:
- もし
CommandRecorder#inverse_ofを直接利用し、「逆操作が来たら必ずコマンド配列になる」と決め打ちしているコードがある場合、実際の挙動はすでに数年前から違っている可能性が高いので、戻り値形式を確認した方がよいです。 - この PR 以降、公式コメントから「複数コマンドの配列を返す」という一般的な保証は読み取れなくなるため、
inverse_ofの戻り値は「単一コマンド or 複数コマンド」など、実装依存の部分があるものとして扱うのが安全です。
- もし
- 参考情報 (あれば)
- 該当 PR:
invert_remove_columnsを単一:add_columnsにまとめるようにした過去コミット:- 3582de977d (2020 年のリファクタ)
- 関連クラス・メソッド:
ActiveRecord::Migration::CommandRecorderActiveRecord::Migration::CommandRecorder#inverse_ofActiveRecord::Migration::CommandRecorder#invert_remove_columns
#58238 Remove ruby2_keywords usage
マージ日: 2026/7/26 | 作成者: @kamipo
- 概要 (1-2文で)
Rails がサポート Ruby バージョンを 3.3.1+ に引き上げたことに伴い、キーワード引数互換のために使っていたruby2_keywordsを全廃し、通常の引数 (*args,**kwargs,&block,...) に書き換えた PR です。
一部、外部インターフェースやシリアライズ形式を変えられない箇所では、Hash.ruby2_keywords_hashによる「末尾フラグ付きハッシュ」形式を維持しています。
- 変更内容の詳細
2-1. 全体方針
- 以前は「Ruby 2.7 と 3.x を両方サポート」するために
ruby2_keywordsを使い、キーワード引数の挙動差を吸収していました。 - Rails 7.2 以降(この PR 時点)では「Ruby 3.3.1 以上必須」となり、キーワード引数分離前後の互換はもう不要になったため、
ruby2_keywordsを削除。 - 代わりに以下のいずれかに置き換え:
- 明示的な可変長引数:
def foo(*args, **kwargs, &block)→bar(*args, **kwargs, &block) - 引数転送(Ruby 2.7+ 機能):
def foo(...)→bar(...) - 既存のバイナリ/ストレージ/ワイヤーフォーマット維持の必要がある箇所のみ
Hash.ruby2_keywords_hashを維持(Active Job など)
- 明示的な可変長引数:
2-2. 主なコンポーネント別の変更
※ファイル単位の diff は省略し、挙動レベルで説明します。
Action Mailer
対象ファイル:
actionmailer/lib/action_mailer/base.rbactionmailer/lib/action_mailer/message_delivery.rb
変更内容:
- メール送信メソッドなどで、
ruby2_keywordsを付けていたラッパーメソッドを、普通の可変長引数転送に変更。
- メール送信メソッドなどで、
イメージ:
ruby# 旧 ruby2_keywords def method_missing(name, *args, &block) mailer.__send__(name, *args, &block) end # 新 def method_missing(name, *args, **kwargs, &block) mailer.__send__(name, *args, **kwargs, &block) endまたは、引数のシグネチャを保てるところは:
rubydef method_missing(...) mailer.__send__(...) end
Action Pack (Controller / Request / Middleware / Test)
対象ファイル:
actionpack/lib/action_controller/metal.rbactionpack/lib/action_controller/test_case.rbactionpack/lib/action_dispatch/http/request.rbactionpack/lib/action_dispatch/middleware/stack.rb
主な変更点:
- コントローラの委譲メソッドやテストヘルパー、
Middleware::Stack周りでruby2_keywordsを廃止し、*args, **kwargs, &blockや...を利用した安全な引数転送に変更。 - ミドルウェアスタック内でアプリケーションやミドルウェアをラップする箇所も同様に整理。
- コントローラの委譲メソッドやテストヘルパー、
サンプルイメージ(ミドルウェアラップ):
ruby# 旧 ruby2_keywords def call(*args) @app.call(*args) end # 新 def call(env) @app.call(env) endまたは、元のインターフェースを保つ必要がある場合:
rubydef call(*args, **kwargs, &block) @app.call(*args, **kwargs, &block) endRuby 3 系ではキーワード引数が分離されているため、「位置引数配列にキーワードが混ざっている前提」から「
**kwargsで明示的に扱う」形に寄せています。
Action View
- 対象ファイル:
actionview/test/template/form_helper/form_with_test.rb
- 主にテストコードの修正:
ruby2_keywordsを前提とした呼び出し方をやめ、キーワード引数を明示的に渡す・受け取るように調整。
Active Job
対象ファイル:
activejob/lib/active_job/core.rbactivejob/lib/active_job/enqueuing.rb
ここが少し重要なポイントです。
変更内容:
- メソッドラッパーなどから
ruby2_keywordsを削除。 - 一方で、ジョブ引数のシリアライズ形式(wire format)を変えないために、一部で
Hash.ruby2_keywords_hashを維持。
- メソッドラッパーなどから
背景:
- Active Job はジョブ引数をシリアライズしてキューに積むため、「キーワード引数を最後のハッシュとして 1 個の引数にまとめる」という形式を他プロセスや他言語との互換のために残す必要があるケースがあります。
イメージ:
ruby# ジョブ実行時に kwargs を 1 つのハッシュとして格納 def serialize_arguments(args, kwargs) combined = Hash.ruby2_keywords_hash(kwargs) [*args, combined] endここでは Ruby 実行時互換のためではなく、「既存の永続化・キュー形式を壊さない」ために
ruby2_keywords_hashを利用している点がポイントです。
Active Model / Active Record
対象ファイル:
activemodel/lib/active_model/type/registry.rbactiverecord/lib/active_record/type/adapter_specific_registry.rbactiverecord/lib/active_record/migration.rbactiverecord/lib/active_record/migration/command_recorder.rbactiverecord/lib/active_record/scoping/named.rb
主な変更内容:
- 登録処理・委譲処理・スコープ定義などで
ruby2_keywordsを使っていた箇所を、*args, **kwargs, &blockまたは...に変更。 - Migration や CommandRecorder 内の記録・リプレイ処理で、キーワード引数が正しく維持されるよう、Ruby 3 のキーワード引数分離前提で改めて組み直し。
- 登録処理・委譲処理・スコープ定義などで
サンプルイメージ(Named Scope):
ruby# 旧 ruby2_keywords def scope(name, scope_options = nil, &block) # ... end # 新 def scope(name, scope_options = nil, **kwargs, &block) # kwargs を適宜 scope_options にマージするなど end
Active Support
対象ファイル:
activesupport/lib/active_support/core_ext/object/try.rbactivesupport/lib/active_support/core_ext/time/calculations.rbactivesupport/lib/active_support/deprecation/method_wrappers.rbactivesupport/lib/active_support/deprecation/proxy_wrappers.rb
主な変更内容:
Object#try系: 可変長+キーワード引数を安全に委譲するように変更。Time#advance等の計算系: キーワード引数が関わるヘルパーを Ruby 3 向けに整理。ActiveSupport::Deprecationの「メソッドラッパー/プロキシ」:- もともと
ruby2_keywordsで元メソッドへの引数をそのまま転送していたが、これを*args, **kwargs, &blockまたは...に変更。 - これにより、非推奨ラッパーが Ruby 3 の kwargs 仕様に忠実になります。
- もともと
例(deprecation wrapper イメージ):
ruby# 旧 ruby2_keywords def call(*args, &block) @target.__send__(@method, *args, &block) end # 新 def call(*args, **kwargs, &block) @target.__send__(@method, *args, **kwargs, &block) end
- 影響範囲・注意点
3-1. ライブラリ作者・アプリ開発者への実務的影響
- Rails 内部 API の挙動は Ruby 3 系に一致
- これまで Ruby 2.7 互換のために「キーワード引数を最後のハッシュとして扱う」ような曖昧な挙動が混ざっていた可能性がありますが、それが解消され、Ruby 3 のキーワード引数モデルに統一されます。
- アプリ側のコードへの直接的な破壊的変更は基本的にない前提
- この PR はほぼ「内部実装置き換え」ですが、以下のような場合は影響を受ける可能性があります:
- Reflection や
method(:foo).parametersなどで引数シグネチャを厳密に検査している場合(*argsのみ →*args, **kwargsのような変化) - Rails 内部メソッドをモンキーパッチしていて、
ruby2_keywords前提で動かしていた場合
- Reflection や
- この PR はほぼ「内部実装置き換え」ですが、以下のような場合は影響を受ける可能性があります:
- ActiveJob / Migration / Deprecation ラッパーなどは、引数の流れに敏感なコードが周辺にあることが多いため、そこを拡張しているライブラリは注意が必要です。
3-2. Ruby バージョン要件との関係
- Rails が Ruby 3.3.1+ 必須 になったため、Ruby 2.7 / 3.0 / 3.1 などでの動作はサポート対象外です。
ruby2_keywords自体が Ruby 本体側で将来的に非推奨になる提案(Feature #22205)と歩調を合わせており、Rails の利用者・エコシステム側もruby2_keywordsに依存しない設計へ移行すべきというメッセージとも読み取れます。
3-3. 自前ライブラリの移行のヒント
この PR に倣って、自分の gem / ライブラリから ruby2_keywords を外したい場合の方針:
- Ruby 3 専用にするなら:
- ラッパーメソッドは基本
def wrapper(*args, **kwargs, &block); target(*args, **kwargs, &block); end - もしくは
def wrapper(...); target(...); endを使う。
- ラッパーメソッドは基本
- 既存フォーマットの維持が必要なら:
- Active Job 同様、「シリアライズ形式」などインターフェースを守る部分だけ
Hash.ruby2_keywords_hashを使い、「Ruby 2.7 向け挙動のためのruby2_keywords」とは意味合いを切り分ける。
- Active Job 同様、「シリアライズ形式」などインターフェースを守る部分だけ
- 参考情報 (あれば)
- Ruby 本体の提案:
ruby2_keywordsの非推奨化提案: https://bugs.ruby-lang.org/issues/22205
- Ruby キーワード引数分離の背景:
- Ruby 2.7: kwargs 分離の警告導入、
ruby2_keywordsによる移行パス提供 - Ruby 3.0+: kwargs が完全に位置引数とは分離された仕様に
- Ruby 2.7: kwargs 分離の警告導入、
- 実コードを追う場合:
- https://github.com/rails/rails/pull/58238 の diff を見ると、「どのようなパターンで
ruby2_keywords→*args, **kwargs/...に置き換えているか」が分かるため、自分のプロジェクトからの移行の参考になります。
- https://github.com/rails/rails/pull/58238 の diff を見ると、「どのようなパターンで
#58240 Document that connected_to_all_shards raises with no shards
マージ日: 2026/7/25 | 作成者: @55728
- 概要 (1-2文で)
Rails 8.2 でのconnected_to_all_shardsの挙動変更(シャード未設定時にArgumentErrorを送出)に合わせて、RDoc(API ドキュメント)の説明を実装どおりに修正する PR です。これにより、ドキュメントと実際の挙動の不一致が解消されます。
- 変更内容の詳細
対象: activerecord/lib/active_record/connection_handling.rb のドキュメントコメント部分のみが変更されています。実装コードの挙動は既に変更済み(別 PR / コミット)であり、この PR ではそれに追随して説明文を直しています。
これまでのドキュメントの説明 (問題点)connected_to_all_shards の説明として、以下のようなニュアンスが書かれていました:
モデルが接続するように設定されている各
shardに対してconnected_toでブロックを実行し、その結果を配列で返します (もしシャードがあれば)
ここでの “(if any)”(「もしあれば」)という文言は、「シャードが一つもない場合は何もせずに空の配列を返す」ような挙動を示唆していました。
実際の挙動 (既に変更済み)
コミット 697fc4c0b9 以降、connected_to_all_shards は以下のように動きます:
- 対象モデルに一つもシャードが設定されていない状態で呼び出すと
ArgumentErrorを送出 する - CHANGELOG とテストコードは、この挙動に合わせて既に更新済み
- しかし RDoc だけが「シャードが無いケースも許容する」ような古い説明のままだった
この PR の修正内容
- ドキュメントから “(if any)” を削除し、「シャードが無いケースも許容される」という読み取りをできないようにした
- 代わりに、「対象モデルがどのシャードにも接続されていない場合は
ArgumentErrorを送出する」ことを明記
イメージとしては、以下のような内容が RDoc に反映される形になります(擬似例):
# Executes the given block in the context of each shard the model is
# configured to connect to and returns the results in an array.
#
# Raises ArgumentError if the model is not connected to any shards.
def connected_to_all_shards(...)
...
end※実際のコメント文面は多少異なる可能性がありますが、趣旨としては上記のように「エラーである」ことを明記する修正です。
- 影響範囲・注意点
実装の挙動はこの PR では変わらない
- 既に
connected_to_all_shardsは「シャード未設定時にArgumentErrorを投げる」ように実装されており、この PR はそれに合わせてドキュメントを更新しただけです。
- 既に
Rails 8.2 以降での期待される使い方
connected_to_all_shardsを呼ぶ前提として、「そのモデルに対してシャード設定が存在する」ことを開発者側が保証する必要があります。もし将来以下のようなコードを書いていると:
ruby# 以前の挙動を想定しているコード (疑似コード) results = User.connected_to_all_shards do # シャードがあれば実行、無ければ空配列 User.count endRails 8.2 では、User がどのシャードにも紐づいていない場合に
ArgumentErrorが発生します。この挙動自体はすでに導入済みであり、本 PR はそれをドキュメントに反映しただけですが、古いドキュメントを信じてコードを書いていた場合には注意が必要です。
マルチテナント / シャーディングを使うアプリのドキュメント参照者向け
- API ドキュメントを読んで
connected_to_all_shardsを利用している開発者は、「シャードがないケースは安全にスキップされるのではなく、エラーになる」という前提でロジック・例外処理を設計する必要があります。 - シャード数が動的に増減するようなシステムでは、「本当にシャード設定が存在するか」を呼び出し前に検証するか、「
ArgumentErrorを rescue してフォールバックする」などの対策が必要です。
- API ドキュメントを読んで
- 参考情報 (あれば)
- この PR は #58117 のフォローアップとして作られており、実装変更・CHANGELOG 変更 → テスト更新 → RDoc 反映という流れの最終段階にあたります。
- 挙動変更を導入したコミット:
697fc4c0b9 - Rails 8.2 での公式な API 挙動として、「シャード未設定時は
connected_to_all_shardsがArgumentErrorを送出する」ことがドキュメントレベルでも確定します。
#58236 Fix dangling Action Text autoloads for Configurator and Registry
マージ日: 2026/7/25 | 作成者: @55728
- 概要 (1-2文で)
Action Text が存在しないファイルに対して autoload を設定していたため、未定義定数参照時に本来期待されるNameErrorではなくLoadErrorが発生していた問題を修正した PR です。不要かつ誤った autoload を削除し、ActionText::ConfiguratorとActionText::Registry参照時にNameErrorを返すようにしています。
- 変更内容の詳細
問題点
ActionText モジュールは以下のような autoload を定義していました:
# イメージ(今回削除されたもの)
autoload :Configurator, "action_text/configurator"
autoload :Registry, "action_text/registry"しかし、action_text/configurator.rb および action_text/registry.rb というファイルは実際には存在しません。そのため、以下のようなコードを実行すると:
ActionText::Configurator
# => LoadError: cannot load such file -- action_text/configurator
ActionText::Registry
# => LoadError: cannot load such file -- action_text/registry- 定数自体が存在しないにもかかわらず
- autoload が「存在しないファイルを読もうとして失敗」しているため
- 「未定義定数」の
NameErrorではなく「ファイルが読み込めない」のLoadErrorが発生していました。
Ruby の定数解決として自然なのは、該当する定数がどこにも定義されていない場合に NameError になることなので、この状態は望ましくありません。
実際のクラス定義の場所
対象となるクラスは以下の通り、ActionText::Editor 名前空間の下に定義されています:
ActionText::Editor::Configurator # => ActionText::Editor::Configurator
ActionText::Editor::Registry # => ActionText::Editor::Registryこれらは action_text/editor.rb から autoload されており、トップレベル (ActionText::Configurator, ActionText::Registry) では定義されていません。また、両クラスとも内部用 (:nodoc:) です。
この PR の変更
actiontext/lib/action_text.rbから上記 2 つの autoload 行を削除- 追加行数 0
- 削除行数 2
- これにより:
ActionText::Configurator
# => NameError: uninitialized constant ActionText::Configurator
ActionText::Registry
# => NameError: uninitialized constant ActionText::Registryのように、通常の未定義定数と同様に NameError が発生するようになります。
テスト・CHANGELOG について
- テストは追加されていません
- 削除した autoload は「存在しないファイルを指しているだけ」であり、そもそも「正しい動作」と呼べるものが存在していないため
- CHANGELOG への記載もありません
- 定数は内部用 (
:nodoc:) かつ、この autoload 自体が未リリースのコミットで追加されたものだったため、ユーザー向けの変更として扱う必要がないため
- 定数は内部用 (
- 影響範囲・注意点
- 対象:
ActionText::ConfiguratorActionText::Registry
- 変更前:
- これらを参照すると
LoadErrorが発生
- これらを参照すると
- 変更後:
- これらを参照すると
NameErrorが発生
- これらを参照すると
フレームワーク本体やテストは、トップレベル (ActionText::Configurator) を参照しておらず、ActionText::Editor::Configurator / ActionText::Editor::Registry を使っているため、Rails 内部の挙動には影響しません。
注意点としては:
- もしアプリやライブラリ側で内部定数に依存していて、誤って
ActionText::Configurator/ActionText::Registryを直接参照していた場合:- これまでは「LoadError が出ていた」が
- 今後は「NameError が出る」挙動に変わります
- とはいえ、これらはそもそも内部用 (
:nodoc:) であり、外部から参照するのは推奨されていません。必要であればActionText::Editor::Configurator/ActionText::Editor::Registryを明示的に参照するべきです。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58236
- autoload が指すファイルが存在しない場合に
LoadErrorとなり、単に定数が未定義な場合はNameErrorとなる、という Ruby の定数解決・autoload の基本挙動を整理したい場合は Ruby リファレンスのModule#autoloadと定数解決に関する節が参考になります。
#58231 Fix reason phrase casing in the 404 error page title
マージ日: 2026/7/25 | 作成者: @55728
- 概要 (1-2文で)
404エラーページのHTMLタイトルに含まれる HTTP リーズンフレーズの表記を、仕様どおり「Not Found」に統一するPRです。ガイドや各エンジンのダミーアプリを含め、404 Not foundとなっていた箇所をすべて404 Not Foundに修正しています。
- 変更内容の詳細
何を直したか
Rails 7の新しい静的エラーページでは、タイトル末尾に HTTP ステータスコード+リーズンフレーズが入りますが、404 だけ表記が崩れていました。
- 400:
(400 Bad Request) - 404:
(404 Not found)← f が小文字 - 406:
(406 Not Acceptable) - 422:
(422 Unprocessable Entity) - 500:
(500 Internal Server Error)
HTTPの正式なリーズンフレーズは RFC 9110 §15.5.5 で Not Found と定義されているため、ここを修正しています。
具体的な変更箇所
以下の全てで、404 Not found → 404 Not Found に差し替え:
アプリ生成時に出力される 404 ページテンプレート
railties/lib/rails/generators/rails/app/templates/public/404.html
例(イメージ):
diff- <title>The page you were looking for doesn't exist (404 Not found)</title> + <title>The page you were looking for doesn't exist (404 Not Found)</title>Active Storage / Action Mailbox / Action Text の test/dummy アプリの 404 ページ
activestorage/test/dummy/public/404.htmlactionmailbox/test/dummy/public/404.htmlactiontext/test/dummy/public/404.html
ガイド内の記述
guides/source/configuring.mdguides/source/upgrading_ruby_on_rails.md
こちらもガイド文中の文字列を同様に
404 Not Foundに統一。
補足
- 404 ページの SVG 内の文言はすでに "Not Found" になっており、HTML
<title>側をそれに合わせた形です。 - 仕様(RFC 9110)に沿った正しいリーズンフレーズへのキャピタライゼーション修正のみで、文言や構造の変更はありません。
- 影響範囲・注意点
- 影響範囲
- 新規に
rails newでアプリを作った場合に生成されるpublic/404.htmlの<title>表記が変わります。 - Rails リポジトリ内のダミーアプリとガイドの表記が整合します。
- 新規に
- 実行時の挙動・レスポンス
- レスポンスステータスやヘッダ、ボディ構造には変更なし。
- 変更は静的HTML内のテキストだけであり、機能的な影響はありません。
- 既存アプリへの影響
- 既に生成済みの
public/404.htmlは自動では書き換わらないため、必要であれば手動でタイトルを修正してください。 - ログ収集やE2Eテスト等で
<title>の文字列を厳密比較している場合のみ、差異が出る可能性があります。
- 既に生成済みの
- 参考情報 (あれば)
- HTTP 404 Not Found (正式なリーズンフレーズ):
RFC 9110 §15.5.5
https://www.rfc-editor.org/rfc/rfc9110#name-404-not-found - 関連PR: 静的エラーページのリデザイン
- #53045
#58234 [ci-skip][doc] Update link styles in generator guide
マージ日: 2026/7/25 | 作成者: @hachi8833
- 概要 (1-2文で)
Rails ガイド「Generators Guide」の中で、ジェネレータ関連 API へのリンクを edge API から安定版 API に変更し、外部リンクの書き方を統一して IDE のシンタックスハイライトと相性が良いスタイルに整えたドキュメント修正の PR です。機能コードは一切変更されていません。
- 変更内容の詳細
リンク先の変更(edge → stable)
元のガイドでは、ジェネレータのアクション API への参照として edge API を指していました:
- https://edgeapi.rubyonrails.org/classes/Rails/Generators/Actions.html
+ https://api.rubyonrails.org/classes/Rails/Generators/Actions.htmlこれにより、以下のような効果があります:
- edgeapi.rubyonrails.org: 次期バージョン開発中の最新版ドキュメント(変更が多く不安定)
- api.rubyonrails.org: リリース済みの安定版ドキュメント
Generators ガイドは通常「リリース済み Rails を使う開発者」向けのため、edge ではなく安定版 API を参照するように修正されています。
外部リンク表記の統一
PR 説明では、IDE でのシンタックスハイライトに合わせて「外部リンクのスタイルを一貫させた」とあります。
実際の diff では guides/source/generators.md 内で、URL やリンクの書き方が複数箇所で編集されています(+69 / -56 行)。
典型的には、次のようなスタイル統一が想定されます:
- 裸の URL(
https://...をそのまま書く)から Markdown リンク形式へ:diff - https://api.rubyonrails.org/classes/Rails/Generators/Actions.html
- テキストと URL を分離して、IDE が URL だと認識しやすい/シンタックスハイライトの邪魔をしないよう整理
- コードブロック内ではコメントとして URL を書かないようにする、あるいは逆に「コードとして扱われない位置」に URL を移動する、等
PR のスクリーンショットからも、IDE 上でリンク部分の色分けが変わっていることが分かります。
意図としては:
- Markdown とコードブロックの混在部分で、
- 「コード(Ruby 等)」と
- 「ドキュメント上の外部リンク」
を明確に分けることで、IDE のハイライトと補完がより素直に働くようにする
という「見た目・編集体験の改善」が主です。
- 影響範囲・注意点
影響範囲
- Rails 本体の機能・挙動には一切影響しません。
- 影響するのは、
guides/source/generators.mdを元に生成される「Generators Guide」のみです。 - 読者がクリックしたときに、edge API ではなく安定版 API のドキュメントに飛ぶようになります。
注意点
- edge の機能やまだリリースされていない API を前提に解説しているドキュメントではないため、edge API へのリンクは今後も原則避けるべき、という運用上の示唆になります。
- ガイドから API ドキュメントへリンクを貼る際は、
edgeapiではなくapiを使うのが推奨される流れになりそうです。 - ドキュメント執筆・レビュー時に、
- URL ドメイン(edgeapi か api か)
- リンクの Markdown 記法の統一
をチェックするのが良さそうです。
- 参考情報 (あれば)
安定版 API ドキュメント:
https://api.rubyonrails.org/classes/Rails/Generators/Actions.htmledge(開発版)API ドキュメント:
https://edgeapi.rubyonrails.org/Rails Guides(Generators Guide の元となるリポジトリの場所):
guides/source/generators.md
#57211 Add config to raise on invalid TimeZone#parse strings
マージ日: 2026/7/25 | 作成者: @Saidbek
- 概要 (1-2文で)
ActiveSupport::TimeZone#parseに対して「不正な文字列を渡したときに必ずArgumentErrorを投げる」ための設定フラグActiveSupport.raise_on_invalid_time_zone_parseが追加されました。Rails 8.2 のconfig.load_defaults "8.2"ではこのフラグがデフォルトで有効になり、これまで曖昧だった挙動が Ruby 標準のTime.parseに揃えられます。
- 変更内容の詳細
これまでの問題点
ActiveSupport::TimeZone#parse は、無効な文字列に対して挙動が2パターンに分かれていました:
日付情報がまったく読み取れない文字列
例:"foobar"
→nilを返す一応パースはされるが、範囲外の値を含む文字列
例:"9000"(Date._parseが「月 90」と解釈してしまうケース)
→ArgumentErrorを raise
このため、「文字列が不正かどうか」を一貫した方法で扱いづらく、nil を見逃してバグの温床になり得ました。
今回の変更のゴール
- 不正な文字列は 常に
ArgumentErrorを raise させる - Ruby標準の
Time.parseと同様の挙動に揃える - ただし互換性のために、挙動変更はフラグで制御する
追加された設定フラグ
# 新しい設定フラグ
ActiveSupport.raise_on_invalid_time_zone_parse
# デフォルト: false
# ただし config.load_defaults "8.2" では trueActiveSupport::TimeZone#parse 内部で、このフラグの値に応じてエラーを投げるか、従来通り nil を返すかを切り替えるように変更されています。
挙動イメージ
旧挙動(Rails < 8.2、またはフラグ false の場合)
Time.zone.parse("foobar")
# => nil
Time.zone.parse("9000")
# => ArgumentError: argument out of range新挙動(ActiveSupport.raise_on_invalid_time_zone_parse = true の場合、または config.load_defaults "8.2")
Time.zone.parse("foobar")
# => ArgumentError: argument out of range (or similar)
Time.zone.parse("9000")
# => ArgumentError: argument out of rangeつまり、「パース不可能な文字列」も、「パースはされるが値が変」の文字列も同様に ArgumentError で統一されます。
Rails アプリでの設定方法
明示的に有効化する
# config/application.rb などで
config.after_initialize do
ActiveSupport.raise_on_invalid_time_zone_parse = true
endもしくは、initializer で:
# config/initializers/time_zone_parse.rb
ActiveSupport.raise_on_invalid_time_zone_parse = trueload_defaults 8.2 でのデフォルト
config/application.rb で:
config.load_defaults "8.2"とした場合、自動的に ActiveSupport.raise_on_invalid_time_zone_parse = true が適用されます。new_framework_defaults_8_2.rb のテンプレートにも、この設定をトグルできる項目が追記されています。
ドキュメント・テストの更新
activesupport/CHANGELOG.mdに挙動変更と設定フラグが追記guides/source/configuring.mdにフラグの説明が追加ActiveSupport::TimeZone#parseの Yard コメントで、invalid string に対する挙動が明記time_zone_test.rbおよび Rails 側の configuration テストで、フラグのデフォルト値や切り替え挙動をカバー
- 影響範囲・注意点
影響を受けるコード
影響が出るのは、主に次のようなコードです:
Time.zone.parseの戻り値がnilになるケースを前提にしているコード- 例:
"foobar"のような完全に不正な文字列はこれまでnilでしたが、フラグ有効時は例外になります。
- 例:
- ユーザー入力を直接
Time.zone.parseに渡しているフォーム処理・API エンドポイントなど
具体例
これまで動いていたコード:
time = Time.zone.parse(params[:start_at])
if time
# 有効なときの処理
else
# 不正なときの処理
endRails 8.2 のデフォルト(フラグ true)では、不正な入力時に ArgumentError が発生してこの if 分岐まで到達しません。
対応策としては、たとえば以下のように rescue する形になります:
begin
time = Time.zone.parse(params[:start_at])
# 有効なときの処理
rescue ArgumentError
# 不正なときの処理
endあるいは、どうしても旧挙動を維持したい場合はフラグを false に戻します。
# config/initializers/new_framework_defaults_8_2.rb などで
ActiveSupport.raise_on_invalid_time_zone_parse = falseマイグレーション戦略
既存アプリで Rails 8.2 に上げる際は:
- 移行初期は
ActiveSupport.raise_on_invalid_time_zone_parse = falseにして(旧挙動維持) - ログやテストで
Time.zone.parseまわりの挙動を確認 - 準備ができたタイミングで true に切り替え、
ArgumentErrorを適切にハンドリングするようコードを修正
- 移行初期は
新規アプリ(
load_defaults "8.2")では:- 最初から「不正入力 → 例外」が前提になるため、
Time.zone.parseを使う箇所は必ず例外処理を含める設計にしておくと安全です。
- 最初から「不正入力 → 例外」が前提になるため、
- 参考情報 (あれば)
- このPRは #57194 の follow-up で、
Time.zone.parseの不一致なエラー挙動を解消する目的のものです。 - Ruby 標準の
Time.parseに揃えたい場合は、このフラグを true にするのが推奨の方針になります。
#58225 Make lazy route loading thread-safe
マージ日: 2026/7/24 | 作成者: @grk
- 概要 (1-2文で)
Rails 8 で導入された「ルーティングの遅延読み込み (lazy route loading)」にスレッドセーフでない箇所があり、初回リクエストが並行すると一部ルートが 404 になったり URL ヘルパが未定義になるレースコンディションが発生していました。
このPRではRoutesReloaderに再入可能なロックと明示的なロード状態管理を導入し、初回ロードおよびリロードをスレッドセーフにする変更が行われています。
- 変更内容の詳細
背景となる問題
- Rails 8.0 以降(
config.eager_load = falseのとき)、ルーティングは「最初のリクエストまで描画(draw)を遅延」するようになった。 RoutesReloaderは「ルートが読み込まれたかどうか」を示すフラグ(@loaded)を 描画開始前にtrueにしていた。- マルチスレッド環境(開発用のスレッドサーバ、並列で動くテストなど)で、
- スレッドAがルート描画を開始している途中で
- スレッドBが
@loadedを見て「もうルートはできている」と誤認 - まだ定義されていないルート/URLヘルパにアクセスして 404 や
NoMethodErrorが発生
というレースコンディションが起きていた。
- さらに、二つのスレッドが同時にルート描画を開始してしまうこともあり得た。
一時的なワークアラウンドとして「テストの前に Rails.application.routes.eager_load! を呼ぶ」という対処は可能だが、根本原因の解消ではない。
新しいロード状態管理 (@load_state)
@loaded フラグを廃止し、代わりに @load_state を導入:
@load_stateの状態遷移は3値:nil: まだ一度もロードされていない:loading: 今まさにロード中(ルート描画中):loaded: ロード完了(after_routes_loadedまで全て終わっている)
この1つの状態で、
- 「すでにルートが読み込まれているか?」
- 「いま誰かがロード中か?」
- 「同じスレッドからの再入か?」
を区別・制御できるようにしている。
モニターロック (Monitor) によるスレッドセーフ化
RoutesReloader は、ルート描画処理を 再入可能な Monitor の中で実行するようになりました。
主なポイント:
すべての「ルートを描画する入り口」をMonitorで保護
- 実際にルートを描画する処理の直前で
@load_stateを:loadingにし、Monitor を取得してから描画。 - 描画終了後に
:loadedに変更(もしくは元の状態に戻す)し、Monitor を解放。
- 実際にルートを描画する処理の直前で
同一スレッドからの「再入」を安全に無視
- ルート定義中に
routes.drawを呼んだり、URLヘルパを呼んだりすると、「ルート描画を遅延ロード経由で再度呼ぶ」形で再入が起きうる。 - このとき、同じスレッドはすでに
:loading状態でMonitorを保持しているので、ネストした呼び出しは「すでにロード中である」とみなし、実際の描画処理には入らずショートサーキットする。 - これにより「無限再帰的な描画」や「同じスレッドで二重に描画しようとする」ことを防いでいる。
- ルート定義中に
他スレッドはMonitorでブロックされる
- 別スレッドが初回リクエストでルートに触れた場合、
@load_state == :loadingであることを検知し、Monitorで待機。 - 描画が終わると
:loadedになり、待っていたスレッドは「描画後の完全なルートセット」に対してディスパッチ / URLヘルパ解決を行える。
- 別スレッドが初回リクエストでルートに触れた場合、
after_routes_loadedまで完了してから:loadedにする- 初回ロード(遅延ロード)時に、ルート描画だけでなく
after_routes_loadedフックの実行も完了した段階で:loadedとする。 - これにより、2回目以降のリクエストはロックを取らずに「すでに完全にロード済み」であることを前提に素早く処理できる(fast path)。
- 初回ロード(遅延ロード)時に、ルート描画だけでなく
ロードに失敗した場合の扱い
- 描画中に例外が発生した場合でも、
ensure節で@load_stateを以前の状態に戻す(nilか:loaded)。 - 失敗したロードは決して
:loadedにはならない ため、待っているスレッドは再度ロードを試み、今度は本来のエラーを見られる。 - これにより「半分だけ描画されたルートセット」に対してディスパッチしてしまう事態を防ぐ。特別な rescue は不要。
- 描画中に例外が発生した場合でも、
reload! / reload_routes! の挙動の整理
reload! 内部に上述のガードが組み込まれたことで、以前 #54306 で入った reload_routes! のワークアラウンドが不要になりました。
- 以前は、「初回の遅延ロードがまだ行われていない状態で
reload_routes!を呼ぶ」場合の問題を避けるため、特別な回り道が入っていた。 - 現在は、
reload!を初回ロード前に呼んでも安全 になっており、reload_routes!はワンライナーに戻されている。
挙動上の微妙な変化:
- 初回遅延ロード前に強制
reload!を行った場合:- 以前:
after_routes_loadedフックが2回呼ばれる可能性があった(ワークアラウンド由来 + 本当の初回ロード)。 - 変更後: フックは 初回の「本当の初回ロード」時に1回だけ 呼ばれる。
- 以前:
RoutesReloader#loadedの reader/writer は、もはや使用されなくなったため削除された(@load_stateに統合されたため)。
テストの追加
新しいテストは、意図的に「ルート描画の途中で止める」仕掛けを入れ、別スレッドから以下を行ってレースを再現・検証しています。
- HTTPリクエストを投げてルーティングさせる
- URLヘルパ(
app.some_pathなど)を呼ぶ respond_to?でヘルパの存在チェックを行う
これらは現在の main ブランチでは失敗し、このPRの変更後には成功することが確認されている。
また、「ルート描画が失敗するケース」の単体テストも追加され、失敗時に中途半端な状態のルートセットでディスパッチされないことを確認している。
既存の reload_routes! のテスト(特に #54306 の rake タスク関連)も引き続きパスしている。
- 影響範囲・注意点
主に影響する環境
config.eager_load = falseな環境(開発・テスト)で、
かつマルチスレッドで初回のリクエストや初回の URL ヘルパ呼び出しが走るケース。- Rails 8 で導入された lazy route loading を有効活用しているアプリ全般。
期待される改善
- 開発サーバ(Puma などのスレッドサーバ)や並列テストで、
「ルート定義はあるのに初回だけ 404 /NoMethodErrorがランダムに出る」不定な挙動が解消される。 Rails.application.routes.eager_load!を初回前に手動で呼び出して回避する必要が減る。
- 開発サーバ(Puma などのスレッドサーバ)や並列テストで、
互換性 / 挙動差のポイント
- public API の挙動は基本的に変わらないが、
after_routes_loadedフックが「初回ロードにつき1回のみ」呼び出されるように揃えられた。 - もし「初回遅延ロード前に強制
reload!するとafter_routes_loadedが2回動く」ことに依存したコードがあれば挙動が変わるが、そのような依存は通常想定されていないはず。 RoutesReloader#loadedへの直接アクセス(非公式・内部API依存)をしていた場合は壊れる可能性があるが、一般的なアプリでは通常使わない部分。
- public API の挙動は基本的に変わらないが、
パフォーマンス
- 初回ロード中は Monitor によるロック・待機が入るが、これは元々「初回だけ重い」処理であり、
完了後は:loaded状態でロックフリーな fast path が使われる設計になっているため、
通常運用時のパフォーマンスへの影響は極小と考えられる。
- 初回ロード中は Monitor によるロック・待機が入るが、これは元々「初回だけ重い」処理であり、
- 参考情報 (あれば)
- Rails 8 の lazy route loading 導入PR: #52353(元: #51614)
- 過去の関連PR(
reload_routes!ワークアラウンド): #54306 - このPRが解決する典型的な症状:
- 並列テストのごく一部だけが 404 /
NoMethodErrorで落ちる - 開発環境でサーバ再起動直後、最初の2〜3リクエストのうちどれかだけが謎のルーティングエラーになる
- 並列テストのごく一部だけが 404 /
本PRは、lazy route loading を使う前提の Rails 8 系アプリでの「初回アクセス時の不定なバグ」を根本から解消するためのスレッドセーフ化パッチと位置付けられます。
#58199 Stop filtering i18n paths on initialize
マージ日: 2026/7/24 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Rails の i18n ロード処理とパスのグロブ処理から「初期化時の不要なフィルタリング」と「不要なソート」を取り除き、高負荷なアプリケーションでの起動・ロード時間を短縮する変更です。i18n のファイルパスの存在チェックを初期化時ではなくリロード時に遅延させることで、数千ファイル規模でもオーバーヘッドを抑えています。
- 変更内容の詳細
背景・問題点
- Rails は
config.i18n.load_pathに対して、- パスのグロブ展開(
**/*.ymlなど) - 存在しないファイルの除外(
File.exist?/File.stat相当)
を初期化時に行っていました。
- パスのグロブ展開(
- アプリケーションのロケールファイルが数千件になると、
- ファイル存在チェックが大量発生し、起動時・ロード時が顕著に遅くなる
- ベンチマーク例では i18n ロードが 400ms → 約 250ms まで短縮
- また、
Dir.globに対して明示的にsortしていましたが、Ruby 側で既に安定した順序を提供しているため二重ソートになっていました。
この PR はこれらの「重複した・不要な処理」を削るものです。
具体的なコードレベルの変更点
※実際の diff から推測されるロジックの整理です(変更ファイルは少量ですが、挙動に影響が出る部分です)。
1) i18n 初期化時のフィルタリングをやめる
対象: activesupport/lib/active_support/i18n_railtie.rb(1行差し替え)
- 以前は、
Rails::Paths::Pathの「存在するファイルだけを返す」系メソッド(例:existentのようなもの)を使ってconfig.i18n.load_pathを構築していた可能性があります。 - この PR では、その「初期化時のフィルタリング」をやめて、生の
expanded(または同等のメソッド)結果をそのままload_pathに突っ込むような変更になっています。 - 「存在しないファイルの除外」は、実際にリロードを行うタイミング(たとえばファイル監視で再読み込みする時など)に遅延させる方針に変更。
結果として:
- i18n 初期化時に行っていたファイル存在チェックのループがなくなり、起動時の負荷が軽減。
2) パスのグロブ処理から余計な処理を削減
対象: railties/lib/rails/paths.rb(+5/-1)
Rails::Paths::Root#addなどで指定されるglob: "**/*.{rb,yml}"の処理において、- 以前は
Dir.glob(glob).sortのような処理、もしくはフィルタリング+ソートの複合処理を行っていた。
- 以前は
- この PR で:
- Ruby が
Dir.globの結果順序をプラットフォーム間で安定させていることを前提に、明示的なsortを削除。 - もしくは、各 Path オブジェクト内の
expanded/existentといったメソッドでの二重処理を解消している。
- Ruby が
意図:
- グロブ結果に対する追加のソート・フィルタリングを削除し、I/O とメモリアロケーションを減らす。
- i18n に限らず、
Rails::Pathsを利用している他のパス(app/**/*,lib/**/*など)の展開にもメリットが出る。
3) テストと CHANGELOG
railties/test/paths_test.rb(+10)- 新しいグロブ動作・フィルタリングタイミングの仕様に合わせたテストを追加。
- 例として:
- グロブ結果に対して明示的ソートをしない前提のテスト
- 存在チェックをどのタイミングで行うかに依存しないテスト
railties/CHANGELOG.md(+5)- 「i18n のロードパフォーマンスが改善された」「パスのグロブ処理が軽量化された」といった記述を追加。
4) ベンチマークコードのポイント
PR に含まれているベンチマークスクリプトは、Rails 本体ではなく挙動確認用です:
root = Rails::Paths::Root.new(tmp)
root.add "config/locales", glob: GLOB
path = root["config/locales"]
filter_method = File.read(RAILTIE)[/load_path\.unshift\(\*value\.flat_map\(&:(\w+)\)\)/, 1].to_sym
Benchmark.ips do |x|
x.report("main") { path.public_send(filter_method) }
x.report("branch") { path.public_send(filter_method) }
endload_path.unshift(*value.flat_map(&:XXX))という記述からXXXを抜き出し、そのメソッド(expandedまたはexistentなど)をベンチマーク。- 「main」(マージ前)と「branch」(この PR 適用後)でどれくらい速くなるかを比較しており、
- 74.8 i/s → 119.2 i/s(約 1.6 倍)という結果。
- 影響範囲・注意点
影響範囲
- 主に影響するのは:
config.i18n.load_pathに大量のロケールファイルを指定しているアプリケーションRails::Pathsを通じて大規模なグロブ(app/**/*,config/**/*など)を行っているケース
- 期待される効果:
- Rails 起動時(初期化時)の i18n ロードが高速化
- ファイル数が多いほど効果が出やすい
- 他のパスグロブ処理も少なからず高速化される可能性がある
挙動上の注意点・互換性
- 「存在しないファイルが
load_pathに一時的に入る」可能性:- 初期化時にフィルタしないため、理論上は
config.i18n.load_pathに「今は存在しないが、後で作られるかもしれない」パスが残りやすくなります。 - ただし、実際に i18n がファイルを読むタイミング・リロードタイミングでフィルタリングされるよう設計しているため、通常利用では問題になりにくい想定です。
- 初期化時にフィルタしないため、理論上は
- ソート順依存のコードがある場合:
- これまでも Ruby 側の
Dir.glob依存だったはずですが、明示ソートが消えることで、
「たまたまsortに頼っていた」コードがあれば影響する可能性があります(ただし通常の Rails アプリではほぼ無関係)。
- これまでも Ruby 側の
- Rails 内部 API 的な
Rails::Pathsの挙動に依存したメタプログラミングをしている場合:expanded/existentのようなメソッド呼び出しタイミングや戻り値の詳細な仕様に依存していると、若干挙動が変わる可能性があります。
- 参考情報 (あれば)
- PR 本体:
https://github.com/rails/rails/pull/58199 - 変更対象の主なファイル:
activesupport/lib/active_support/i18n_railtie.rbrailties/lib/rails/paths.rb
- 関連する設計上のポイント:
- 大規模プロジェクトでは、ファイルの存在チェック (
File.exist?,File.stat) がボトルネックになりやすく、それを「必要なタイミングにだけ行う」ことがパフォーマンス改善の基本パターンである、という事例になっています。
- 大規模プロジェクトでは、ファイルの存在チェック (
- パフォーマンス目安(PR 提示値):
- i18n ロード時間: 400ms → 約 250ms
- ベンチマーク: ~1.6x 高速化(74.8 i/s → 119.2 i/s)
#58227 Remove dead branch in MySQL column introspection
マージ日: 2026/7/23 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
MySQL アダプタのカラム情報取得処理(カラム introspection)から、もはや到達しない条件分岐(デッドブランチ)が削除されました。MariaDB における関数デフォルト値の検出ロジックが別の場所に移されたため、古い最適化用の分岐が不要になったことによるクリーンアップです。
- 変更内容の詳細
対象ファイル:
activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb(2行削除、追加なし)
背景となる挙動:
- 4ccd79cdd6 の変更で、
default_typeメソッドがテーブルのデフォルト値を分類する際に、各カラムごとにSHOW CREATE TABLEを実行して:string:integer:function
などの種別を判定するようになっていました。
- ただし
SHOW CREATE TABLEは重いクエリのため、**「数値で始まるデフォルト値(/\A\d/)は数値とみなして即座に処理を打ち切る」**というショートカット分岐が入っていました。この分岐により、よくある数値デフォルト(例:0,1,100など)では高コストなSHOW CREATE TABLE呼び出しをスキップしていました。
その後:
- c2c158c760 の変更で、MariaDB における「関数デフォルト値(例:
CURRENT_TIMESTAMPなど)」の検出ロジックはcolumn_definitionsに移動しました。 - これにより、
default_typeの中で「関数デフォルト値かどうかを判定するためにSHOW CREATE TABLEを呼ぶ」必要性がなくなり、結果として/\A\d/で数値デフォルトをショートカットする最適化分岐も意味を失い、実際には到達しない(デッド)コードになっていた という状況です。
今回の PR:
- 上記の「数値で始まるデフォルト値の場合に早期 return するブランチ」を削除しています。
- 実際の差分は 2 行削除のみで、新たなロジック追加や挙動変更は行われていません。
(イメージ的には以下のような分岐が消えたイメージです:)
# 以前(簡略イメージ)
if default =~ /\A\d/
# 数値デフォルトとして扱って早期終了
else
# SHOW CREATE TABLE を使った詳細判定
end
# 今回、その if 分岐が削除され、共通経路だけが残る- 影響範囲・注意点
- 対象: ActiveRecord の MySQL(正確には
mysql2系)アダプタにおける スキーマ情報の introspection 処理 のみ。 - ロジック上の意味:
- すでに MariaDB の関数デフォルト検出は
column_definitionsで完結しているため、この分岐削除によって MariaDB 向けの動作が変わることはない という前提のクリーンアップです。 - 「デッドブランチ」であったことから、実行時の挙動に変化はない想定です(つまり、PR 前後で
schema_statementsの公開 API の振る舞いは変わらないはず)。
- すでに MariaDB の関数デフォルト検出は
- パフォーマンスへの影響:
- 本来は「高コストな
SHOW CREATE TABLEを避けるための最適化」が削除されたように見えますが、そもそもその経路が使われていなかった(別の場所で判定している)ため、実運用でのパフォーマンスへの影響はないと考えられます。
- 本来は「高コストな
- 互換性:
- 公開 API のシグネチャや戻り値の型などは一切変わっていないため、アプリケーションコードレベルの互換性問題は発生しない想定です。
- もし独自に ActiveRecord の MySQL アダプタを monkey patch しており、
default_typeやschema_statements内部の具体的な分岐に依存している場合は、念のため差分を確認すると安全です(しかし一般的にはそのような依存は稀)。
- 参考情報 (あれば)
- この PR の説明で言及されている関連コミット:
4ccd79cdd6:default_type内でSHOW CREATE TABLEを用いる実装と、それを回避する/\A\d/の最適化が導入されたコミット。c2c158c760: MariaDB の関数デフォルト検出ロジックをcolumn_definitionsに移したコミット。これにより今回のデッドブランチが不要になった。
- 関連するコンテキスト:
- 「関数デフォルト値」には
CURRENT_TIMESTAMP,NOW(),UUID()など、DB サーバー側で値が決定されるものが含まれます。 - ActiveRecord は、これらを文字列リテラルのデフォルト(例:
'foo')や数値リテラルデフォルト(例:0)と区別する必要があり、その判定の一部でSHOW CREATE TABLEを使っていました。今回のクリーンアップは、その過去の実装痕跡を削除するものです。
- 「関数デフォルト値」には
#58143 Carry disabled and form onto a multiple file field's hidden companion input
マージ日: 2026/7/23 | 作成者: @carldaws
- 概要 (1-2文で)
複数ファイルアップロード用のfile_field(multiple: true)かつinclude_hidden: trueのときに生成される「隠し入力(hidden)」に、元のファイル入力のdisabledとform属性を正しく引き継ぐようにしたバグ修正です。これにより、無効化されたフィールドや外部フォームに属するフィールドで、送信されるパラメータが意図どおりになるよう整合性が取られました。
- 変更内容の詳細
何が問題だったか
file_field を以下のように使うとします:
<%= form_with model: @import do |f| %>
<%= f.file_field :files, multiple: true, include_hidden: true %>
<% end %>include_hidden: true のとき、複数値フィールドと同様に
- 1つもファイルが選択されなかった場合でも
- 「空配列」としてパラメータを送るため
に、Rails は 補助的な hidden input を自動で生成します。
ところが、これまで hidden 側には
nameだけがコピーされdisabledform(外部フォーム用属性)
がコピーされていませんでした。
その結果:
disabled な file_field でも hidden が有効になってしまう
html<!-- before --> <input name="import[files][]" type="hidden" value="" /> <input multiple="multiple" disabled="disabled" type="file" name="import[files][]" id="import_files" />見た目のファイル入力は
disabledで送信されないはずですが、
hidden 側は有効なのでimport[files][]=が送信されてしまいます。
→ 本来「無効なフィールドからは何も送られないべき」との期待に反します。form属性による外部フォーム利用時に hidden がそのフォームに属さないhtml<!-- before --> <input name="import[files][]" type="hidden" value="" /> <input multiple="multiple" form="uploads" type="file" name="import[files][]" id="import_files" />この場合、
<form id="uploads">を送信しても、hidden はuploadsフォームに属していないため、- ファイル未選択時に
importパラメータ自体が送られない
という挙動になります(空配列を送りたいのに何も送られない)。
- ファイル未選択時に
何を直したか
hidden input を生成するロジックを変更し、file_field の visible な入力と同じく、
disabledform
を hidden 側にもコピーするようにしました。
修正後の HTML 出力例:
- disabled の場合
<!-- after -->
<input type="hidden" name="import[files][]" value="" disabled="disabled" />
<input id="import_files" disabled="disabled"
multiple="multiple" name="import[files][]" type="file" />- hidden も
disabledになるため、このフィールドに関する値は一切送信されません。
- 外部フォーム (
form属性) の場合
<!-- after -->
<input type="hidden" name="import[files][]" value="" form="uploads" />
<input id="import_files" multiple="multiple" form="uploads"
name="import[files][]" type="file" />visible input / hidden input ともに
form="uploads"になり、外部フォーム送信時に- ファイル未選択 →
import[files][]=(空配列)としてパラメータが送られる
- ファイル未選択 →
という、他の複数値フィールド同様の一貫した挙動になります。
実装位置・テスト
実装変更:
actionview/lib/action_view/helpers/tags/file_field.rb- hidden input 生成部分の属性セットを 1行修正(
disabled/formも含めるように)
- hidden input 生成部分の属性セットを 1行修正(
テスト追加:
actionview/test/template/form_helper_test.rb(+16 行)disabledな multiple file field の hidden も disabled になることform属性付き multiple file field の hidden にもformが付与されること
を確認するテストが追加されています。
また、この挙動はすでに
check_box用の hidden input (check_box.rb)select(multiple) の hidden input (select_renderer.rb)
では実装済みであり、本 PR はそれと同じ方針に file_field を揃える変更です。
- 影響範囲・注意点
影響範囲
対象となるのは:
file_field/form.file_fieldをmultiple: trueinclude_hidden: trueで使っているケース
です。単一ファイルの file_field や include_hidden: false の場合には影響しません。
具体的な影響:
disabled な multiple file_field
以前:
- ファイル入力は disabled でも hidden が有効なため
→import[files][]=が送信されていた
これから:
- hidden も disabled となり
→ パラメータ自体が送信されない
アプリ側で「disabled な multiple file フィールドなのに値が来る」という前提のワークアラウンドを入れていた場合は、そのロジックが不要になるか、挙動が変わる可能性があります。
- ファイル入力は disabled でも hidden が有効なため
外部フォーム(
form属性)を使った multiple file_field以前:
- ファイルを選ばなかったとき
→ 対象フォームから hidden が送信されず、パラメータが一切来ない
これから:
- hidden も対象フォームに属するため
→import[files][]=(空配列)としてパラメータが常に送信される
コントローラ側で「パラメータが存在しないとき」と「空配列が送られてきたとき」を区別して扱っている場合、挙動の確認が必要です。ただし、Rails が他の複数値フィールドで採用している一貫した仕様に揃えた形なので、一般的には改善とみなせます。
- ファイルを選ばなかったとき
後方互換性
- Rails 内の他のヘルパ(
check_box, multipleselect)の既存仕様と一致する方向への変更であり、仕様としては自然なバグ修正です。 - ただし、上記のように「これまでのバグに依存した挙動」を利用していた場合のみ、アプリ側のコードに影響が出る可能性があります。
- 参考情報 (あれば)
- 類似修正: multiple
selectの hidden input にdisabled/formをコピーした PR
→ #58064 - 実ブラウザ検証: Selenium + Chrome による確認済み
- disabled な multiple file field: 空配列を送信
- 外部フォームの file field: ファイル未選択の場合パラメータを送らない(フォーム構成に応じた期待どおりの挙動)
- 変更は Action View のみで、行数も小規模(+17 / -1)なピンポイント修正です。
#58179 Raise ConfigurationFile::FormatError for invalid YAML
マージ日: 2026/7/23 | 作成者: @nvasilevski
- 概要 (1-2文で)
ActiveSupport::ConfigurationFileで不正な YAML を読み込んだ際に、これまで汎用的なRuntimeErrorとして扱われていた例外を、本来用意されていた専用例外ActiveSupport::ConfigurationFile::FormatErrorで送出するように変更した PRです。これに伴い、Active Record の fixture ローダー側も、この専用例外を明示的に補足するように修正されています。
- 変更内容の詳細
2-1. ConfigurationFile の例外クラスの実利用
もともと ActiveSupport::ConfigurationFile には以下のような専用例外が定義されていました:
module ActiveSupport
class ConfigurationFile
class FormatError < StandardError; end
# ...
end
endしかし、実際に YAML パースエラーが起きた場合は、例外オブジェクトではなく「文字列を raise する」形になっており、その結果 Ruby の標準挙動として単なる RuntimeError が投げられていました。
この PR では、不正な YAML を検出した場合に、この専用例外クラスを使うように修正しています。イメージとしては以下のような変更です(擬似コード):
# 変更前(イメージ)
def read(path)
begin
YAML.load_file(path)
rescue Psych::Exception => e
raise "YAML syntax error occurred while parsing #{path}. #{e.message}"
# => RuntimeError が飛ぶ
end
end
# 変更後(イメージ)
def read(path)
begin
YAML.load_file(path)
rescue Psych::Exception => e
raise FormatError, "YAML syntax error occurred while parsing #{path}. #{e.message}"
# => ActiveSupport::ConfigurationFile::FormatError が飛ぶ
end
endポイント:
- 例外階層は
StandardErrorのまま(互換性は維持)。 - ただしクラスが固有になることで、アプリ側やフレームワーク側で「設定ファイルのフォーマットエラーだけ」を明示的に捕捉しやすくなります。
2-2. Fixture ローダー側の rescue の明確化
Active Record の fixture ローダー (ActiveRecord::FixtureSet::File 周辺) は、これまでも設定ファイルパースエラーを独自例外 ActiveRecord::Fixture::FormatError にラップし直していましたが、その際に RuntimeError を rescue 対象にしていました。
この PR では、ActiveSupport::ConfigurationFile::FormatError が実際に投げられるようになるため、fixture 側も次のように修正されています(イメージ):
# 変更前
rescue RuntimeError => error
raise ActiveRecord::Fixture::FormatError, error.message
end
# 変更後
rescue ActiveSupport::ConfigurationFile::FormatError => error
raise ActiveRecord::Fixture::FormatError, error.message
endこれにより、
- 想定しているのは「設定ファイル(YAML)のフォーマットエラー」だけ
- 他の
RuntimeErrorまで誤って飲み込まない
という意図どおりの挙動になります。
2-3. テストと CHANGELOG の更新
activesupport/test/configuration_file_test.rbに、Malformed YAML を与えたときにConfigurationFile::FormatErrorが発生することを確認するテストが追加されています。railties/test/application/configuration_test.rbも、エラークラスの変更に追随する形で1行更新。activesupport/CHANGELOG.mdに、この挙動変更(エラークラスがRuntimeError→ConfigurationFile::FormatErrorになること)が記載されています。
- 影響範囲・注意点
3-1. 例外クラス変更による互換性への影響
直接 ActiveSupport::ConfigurationFile を使っているコードで、以下のように RuntimeError を前提に rescue していた場合は、挙動が変わります:
# これまで動いていたパターン
begin
ActiveSupport::ConfigurationFile.new("config.yml").read
rescue RuntimeError => e
# YAML のパースエラーをここで処理していた
end今後は ConfigurationFile::FormatError を捕捉する必要があります:
begin
ActiveSupport::ConfigurationFile.new("config.yml").read
rescue ActiveSupport::ConfigurationFile::FormatError => e
# 設定ファイルのフォーマットエラーだけを処理
endStandardError 全体を rescue しているコードは、そのままでも動作しますが、今回の変更に合わせて、意図として「設定ファイルフォーマットエラーだけ」を対象にしたい場合は、専用例外クラスに切り替えるとより堅牢になります。
3-2. Active Record fixtures 利用側への影響
ActiveRecord::Fixture::FormatError は引き続き同じように発生するため、
- 「fixture の YAML が壊れている場合に
ActiveRecord::Fixture::FormatErrorが飛ぶ」 - そのエラーを rescue して独自処理をしている
といったコードは、基本的にはそのまま動作します。
内部的に RuntimeError を rescue していた部分が、より限定的な ActiveSupport::ConfigurationFile::FormatError の rescue に変わっただけなので、通常のアプリケーションコードへの影響はかなり限定的です。
3-3. エラー処理の改善ポイント
今回の変更により、以下のようなコードパターンが取りやすくなります:
begin
config = ActiveSupport::ConfigurationFile.new("config/my_config.yml").read
rescue ActiveSupport::ConfigurationFile::FormatError => e
Rails.logger.error "設定ファイルが不正です: #{e.message}"
# 必要に応じてフェイルファスト / デフォルト値にフォールバック 等
end以前は単なる RuntimeError だったため、他の runtime エラーと区別しにくかった部分が、専用例外により明確になります。
- 参考情報 (あれば)
- この例外クラスは、
ActiveSupport::ConfigurationFileが最初に導入されたコミット(ef7599fe91)で定義されていましたが、これまで実際には使われていませんでした。 - 関連 Issue / PR:
- #38067:
ActiveSupport::ConfigurationFile導入時の議論。ここで「フォーマットエラー用の専用例外を切り出す」こと自体はレビューで明示的に承認されており、本 PR はその意図に沿って挙動を是正したものです。
- #38067:
- 代替案としては「未使用の
FormatErrorを削除して、従来通りRuntimeErrorのままにする」選択肢もあったが、意図された設計に合わせて専用例外を活かす方針が採られています。
#58215 Remove unused ConnectionPool#remove_from_maintenance method
マージ日: 2026/7/23 | 作成者: @nvasilevski
- 概要 (1-2文で)
ActiveRecord のConnectionPoolに存在していた未使用のプライベートメソッドremove_from_maintenanceが削除され、実際に使われているメンテナンス経路(checkout_for_maintenance→return_from_maintenance)に合わせてドキュメントが更新されました。内部メンテナンス用 API の整理であり、通常のアプリケーションコードへの影響はほぼありません。
- 変更内容の詳細
背景
- 以前のコミット
5eab03f7b0で、コネクションプールの「メンテナンス用の貸し出し」API として以下の3つが導入されていました:checkout_for_maintenancereturn_from_maintenanceremove_from_maintenance
- しかし、実際のメンテナンスフロー(
sequential_maintenanceなど)では、借りた接続は常にreturn_from_maintenance経由で返却されており、remove_from_maintenanceは一度も呼ばれていませんでした。
今回の変更
主な変更点は1ファイルのみです:
activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
- 削除:
- プライベートメソッド
remove_from_maintenance- 役割としては「メンテナンス中に借りたコネクションをプールから完全に捨てる」ための片割れメソッドだったと考えられますが、実際にはどこからも呼ばれていませんでした。
- プライベートメソッド
- 変更:
checkout_for_maintenanceのドキュメントコメントを修正- 「
remove_from_maintenanceと組み合わせて使う」ようなペア関係の説明から、 - 実際に Rails が使用している「
checkout_for_maintenance→return_from_maintenance」のフローを説明する内容に更新。
- 「
※ 実際のコード上は、メソッド定義ごと remove_from_maintenance が削除され、checkout_for_maintenance のコメント中での記述が現実の振る舞いに合わせて調整された、という規模の変更です(+2 / -11 行)。
- 影響範囲・注意点
公開 API への影響:
ConnectionPoolのメンテナンス関連メソッドは元々「内部/プライベート」扱いであり、通常のアプリケーションコードから直接使うことは想定されていません。- そのため、通常の Rails アプリケーションには影響はありません。
内部 API・メンテナンスフローへの影響:
- Rails 本体のコードでも
remove_from_maintenanceは一度も呼ばれていなかったため、挙動の変更は実質ゼロです。 - 今後もメンテナンスフローは:
checkout_for_maintenanceで接続を借りる- 作業完了後に
return_from_maintenanceで接続を返却する
というパスのみが存在する、ということがコードとドキュメントの両方で明確になりました。
- Rails 本体のコードでも
独自に内部 API を使っている場合の注意点:
- gem やアプリ内で
ActiveRecord::ConnectionAdapters::ConnectionPool#remove_from_maintenanceを「内部事情を承知の上で」呼んでいた場合は、NoMethodError が発生します。 - そのようなコードがある場合は、そもそもその経路が Rails 本体では想定されていなかったため、設計の見直しが推奨されます。
- コネクションを「捨てたい」ケースがあるなら、通常は接続の
disconnect!/close、またはプール側のdisconnect!など、サポートされている経路を検討すべきです。
- gem やアプリ内で
- 参考情報 (あれば)
- 元コミット:
5eab03f7b0e8a12871bbe3929a5297491915f586checkout_for_maintenance/return_from_maintenance/remove_from_maintenanceが一緒に導入されたコミット。
- 本 PR の意図:
- 「Rails が実際には使っていない内部 API を残してドキュメントだけが示唆している状態」を解消し、実利用されているパスだけを残すことで、コードベースの一貫性と保守性を高めるものです。
- 今後もし「借りたメンテナンス用接続を破棄するフロー」が必要になった場合は、この PR 以前の Git 履歴から
remove_from_maintenanceの実装を復元し、その時点で初めて呼び出し元とテストをきちんと追加する、という方針が示されています。
#58216 Remove unused test runner path helper
マージ日: 2026/7/23 | 作成者: @nvasilevski
- 概要 (1-2文で)
Rails のテスト周りのコードから、既にどこからも呼ばれていないテストランナー用 helper メソッドと、そのための定数が削除されました。正規表現は必要な箇所(TestCommand)にインライン化され、不要なコードが整理されています。
- 変更内容の詳細
背景
- 過去のコミット
b1589fa7d9で、extract_filtersメソッド内からpath_argument?というガードが取り除かれた結果、そのプライベートメソッドpath_argument?はどこからも呼ばれない「死んだコード」になっていました。 PATH_ARGUMENT_PATTERN定数も、そのpath_argument?のために存在していたものの、唯一の利用箇所はTestCommand::EXACT_TEST_ARGUMENT_PATTERNの構築時だけになっていました。
実際の変更点
1) テストコマンド側 (railties/lib/rails/commands/test/test_command.rb)
PATH_ARGUMENT_PATTERN を経由せず、EXACT_TEST_ARGUMENT_PATTERN 側に正規表現をインライン化しました。概念的には:
# 変更前(イメージ)
PATH_ARGUMENT_PATTERN = /.../
EXACT_TEST_ARGUMENT_PATTERN = /\A#{PATH_ARGUMENT_PATTERN}\z/
# 変更後(イメージ)
EXACT_TEST_ARGUMENT_PATTERN = /\A...同じ中身の正規表現...\z/これにより PATH_ARGUMENT_PATTERN という中間の定数が不要になりました。
2) テストランナー側 (railties/lib/rails/test_unit/runner.rb)
未使用になっていた helper と定数が削除されています。イメージとしては:
# 削除されたもの(例)
PATH_ARGUMENT_PATTERN = /.../
private
def path_argument?(arg)
PATH_ARGUMENT_PATTERN.match?(arg)
endextract_filters などの既存のメソッドからは既に path_argument? が呼ばれていないため、この削除は挙動に影響を与えません。
- 影響範囲・注意点
- 外部 API / 公開インターフェースへの影響はほぼなし
削除されたのはrunner.rb内の private メソッドと内部用定数であり、通常の Rails アプリケーションや標準的なテスト実行フローには影響しません。 - Monkey patch や内部 API 依存がある場合は注意
もしアプリや gem がRails::TestUnit::RunnerのPATH_ARGUMENT_PATTERN定数path_argument?メソッド(private をsendで叩く等)
に依存していた場合、そのコードは壊れます。
そのような用途があるなら、呼び出し側で独自に正規表現を定義する形に変更する必要があります。
- 挙動の変更は意図されていない
正規表現自体はEXACT_TEST_ARGUMENT_PATTERNにインライン化されただけなので、Rails がコマンドライン引数からテストファイル・テスト名を判定するロジック自体は変わっていません。
- 参考情報 (あれば)
- この PR: https://github.com/rails/rails/pull/58216
- 背景となったコミット: https://github.com/rails/rails/commit/b1589fa7d9feac53ad2b957a240a88d0f8288244
- 関連コード:
railties/lib/rails/commands/test/test_command.rbrailties/lib/rails/test_unit/runner.rb
内部実装のクリーンアップであり、Rails のテストランナーを内側まで拡張・カスタマイズしていない限り、アップデート時に特別な対応は不要と考えてよい変更です。
#58170 Fix stale route recognition after clearing a route set
マージ日: 2026/7/23 | 作成者: @55728
- 概要 (1-2文で)
RouteSet を描き直してルートが 0 件になった場合でも、以前のルーティング情報でパス認識が行われてしまう不具合を修正する PR です。routes.clear実行時にルート認識用のキャッシュも無効化することで、再描画後は常に最新の(空の)ルート定義に基づいて認識されるようになります。
- 変更内容の詳細
不具合の内容
ActionDispatch::Journey::Routes は内部的に以下のような「ルート認識用のメモ化キャッシュ」を持っています。
ast(ルート定義をまとめた抽象構文木)simulator(astを元に実行時にパスをマッチさせるオブジェクト)
通常はルート追加 (add_route) 時に clear_cache! が呼ばれ、ast / simulator が再構築されます。しかし Routes#clear はルートテーブル自体は空にするものの、これらのキャッシュを無効化していませんでした。
そのため、以下のケースで「存在しないはずのルートがマッチしてしまう」状態が発生していました。
routes.draw { get "/foo", to: "foo#index" }
routes.recognize_path("/foo") # ここで simulator がメモ化済みになる
routes.draw { } # ここでルートは全削除されるが、キャッシュは生きたまま
routes.routes.size # => 0 (確かにルートは空)
routes.recognize_path("/foo") # => { controller: "foo", action: "index" } ← 本来は 404 相当になるべきRouteSet#draw は内部で clear! を呼んでからブロック内でルートを追加しますが、
- 「以前に recognize_path 等で simulator を構築しており」
- 「描き直し後のブロックで 1 本もルートを追加しない」
という条件が揃ったときだけバグが表面化するため、テストスイートの実行順や seed に依存して再現したり消えたりする、やや厄介な挙動になっていました。
修正内容
修正は非常に小さく、実質 1 行です。
actionpack/lib/action_dispatch/journey/routes.rbJourney::Routes#clearの中でルートテーブルを空にした後、clear_cache!を呼ぶように変更
擬似コード的には次のようなイメージです。
def clear
@routes.clear
clear_cache! # ← 今回追加された呼び出し
endこれにより、
- 次回
ast/simulatorへのアクセス時に、- ルートテーブル(この時点では空)から再生成される
- したがって、空の
RouteSetに対するrecognize_pathでは- 以前のルートは一切参照されず、通常通り
ActionController::RoutingErrorが発生する
- 以前のルートは一切参照されず、通常通り
ようになります。
テスト追加内容
挙動を保証するためのテストが 2 箇所に追加されています。
actionpack/test/journey/routes_test.rbclear実行後に:- AST が「空として」再構築されること
- simulator が以前のインスタンスと異なる(新たに再構築される)こと
- を確認。
actionpack/test/dispatch/routing/route_set_test.rb- 実際の
RouteSetを用いた End-to-End テストとして、/fooを認識できるルートを定義recognize_path("/foo")を 1 度呼んでキャッシュをウォームアップroutes.draw { }(空定義で再描画)- 再度
recognize_path("/foo")を呼ぶとActionController::RoutingErrorが発生すること
- を期待するテストを追加。
- 実際の
どちらのテストも「この 1 行の修正がないと失敗し、修正があると成功する」ことが確認されています。
CHANGELOG
actionpack/CHANGELOG.mdに本修正がバグフィックスとして追記されています。
- 影響範囲・注意点
対象となるのは、以下のようなコードパスです。
ActionDispatch::Routing::RouteSet#drawを複数回呼び出す(テストでよくある)- ある
drawの前にrecognize_pathなどルーティング認識を行っている - その後の
drawブロック内で 1 本もルートを定義しない(条件分岐により全スキップなど)
この PR により、以前は運良く(?) マッチしていたルートが、正しく 404 になるようになります。
つまり、既存コードがこの不具合に依存していた場合(本来は存在しないルートを、古い定義でマッチさせていた場合)、挙動が変わりますが、それは正しい方向への破壊的修正です。空の
RouteSetから AST / simulator を構築するコードパス自体は既に日常的に使われており(新規作成直後のRouteSetなど)、今回の変更で新しい状態が導入されるわけではありません。そのため、パフォーマンス・メモリなどの副作用も極めて限定的です。特に、テストで毎回アプリケーションのルーティングを再描画しているプロジェクト(Engine やマウント状況をテストごとに変える場合など)では、seed に依存した route spec の不安定な失敗が解消される可能性があります。
- 参考情報 (あれば)
- 実際にこのバグにより seed 依存のテスト失敗が発生していたプロジェクトとして、
doorkeeper-openid_connectのテストスイートが挙げられています。 - Rails のルーティング内部実装 (
ActionDispatch::Journey) は、ルートテーブル → AST → simulator という 3 段階で最適化されており、今回の修正はその「ルートテーブルとキャッシュの一貫性」を正すものです。
#58208 Remove unused WhereClause#referenced_columns
マージ日: 2026/7/23 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Relation::WhereClauseに定義されていた未使用メソッドreferenced_columnsが削除されました。過去の非推奨機能削除により呼び出し元がなくなっていたコードを整理した、クリーンアップ目的の変更です。
- 変更内容の詳細
- 対象ファイル:
activerecord/lib/active_record/relation/where_clause.rb - 変更: 6行削除、追加なし
- 削除されたもの:
WhereClause#referenced_columnsメソッド本体とその関連コード
PR説明によると:
- 以前のコミット
c313d9161360によって、predicates_unreferenced_byというメソッドが削除された- 同時に、
ActiveRecord::Relation#mergeにrewhereを渡すという非推奨サポートも削除された
WhereClause#referenced_columnsは、その削除されたpredicates_unreferenced_byからしか呼ばれていなかったため、それ以降は完全に未使用状態だった- 今回のPRでは、その未使用メソッドをコードベースから取り除いています
概念的には以下のようなメソッドが消えたイメージです(あくまでイメージの擬似コード):
# こういったメソッドが存在していたが、どこからも呼ばれていない状態だった
class ActiveRecord::Relation::WhereClause
def referenced_columns
predicates.flat_map { |predicate| predicate.referenced_columns }.uniq
end
end内部 API 用の補助メソッドであり、公開インターフェースとしてドキュメント化されていたものではありません。
- 影響範囲・注意点
通常のアプリケーションコードへの影響
- Rails の公式な公開 API ではない内部メソッドの削除のため、通常は影響はありません。
- 既に
rewhereをRelation#mergeに渡すことは非推奨サポートごと削除済みであり、その時点で互換性対応は済んでいる想定です。
影響が出る可能性があるケース
- gem やアプリケーションが Rails の内部実装に依存しており、
ActiveRecord::Relation::WhereClause#referenced_columnsWhereClauseの内部構造 に直接アクセスしている場合はエラーになります。
- ただし、
predicates_unreferenced_by削除時点ですでに壊れている可能性が高いため、今回の変更で新たに壊れるケースは限定的です。
- gem やアプリケーションが Rails の内部実装に依存しており、
アップグレード時の注意
- Rails のメジャー/マイナーアップグレード時に、独自パッチやメタプログラミングで
WhereClauseを触っているコードがないか確認しておくと安全です。 - 特に
where条件のパースや最適化を独自に行っている gem を利用している場合は、その gem 側の対応状況をチェックすると良いです。
- Rails のメジャー/マイナーアップグレード時に、独自パッチやメタプログラミングで
- 参考情報 (あれば)
- このPRで言及されている過去の変更:
- コミット
c313d9161360predicates_unreferenced_byの削除ActiveRecord::Relation#mergeにrewhereを渡す非推奨サポートの削除
- コミット
- 関連する概念:
WhereClauseは ActiveRecord の内部でWHERE句を抽象化するためのオブジェクトで、predicates(各条件)などを管理するクラスreferenced_columnsは「この WhereClause で参照しているカラム一覧」を返すための内部ユーティリティだったと考えられる
このPR自体は、機能追加や仕様変更ではなく、内部コードを整理しメンテナンス性を高めるためのクリーンアップ変更に該当します。
#58223 Remove unused code from Railties and Action Cable
マージ日: 2026/7/23 | 作成者: @55728
- 概要 (1-2文で)
Rails本体(Railties と Action Cable)から実際には使われていないコードを削除するクリーンアップPRです。最近の未使用コード削除PR(#58214, #58193)の続きで、Rails アプリジェネレータと Action Cable のイベントループ周りの不要な処理が整理されています。
- 変更内容の詳細
2-1. Railties: AppBase#dockerfile_chown_directories の削除
対象ファイル:
railties/lib/rails/generators/app_base.rb(+0/-9)
削除されたのは、Rails アプリ生成時のベースクラス AppBase に定義されていた以下のようなメソッド(イメージ)です:
# 例: こういったメソッドがあったと考えられる
def dockerfile_chown_directories
# Dockerfile 内で chown するディレクトリ群を返す
end背景:
- 以前は生成される Dockerfile 内で
chown -R ...を実行しており、そのためのディレクトリリストをこのメソッドで用意していた。 - しかしコミット
658c989d8bにより、Dockerfile はCOPY --chown=...を使うスタイルに変更され、明示的なchown -R行が削除された。 - その結果、このメソッドを参照していたテンプレート (
.tt) も存在せず、メソッド自体が完全に未使用となっていたため削除。
ポイント:
- Rails new で生成される Dockerfile は既に
COPY --chownベースになっており、このPRはその移行完了に伴う後始末です。 .ttテンプレートやその他ファイルを含めて grep 済みで、どこからも使われていないことが確認されています。
2-2. Action Cable: StreamEventLoop#detach の stream 引数削除
対象ファイル:
actioncable/lib/action_cable/server/stream_event_loop.rb(+1/-1)actioncable/lib/action_cable/server/socket/stream.rb(+1/-1)
変更内容
StreamEventLoop#detach のメソッドシグネチャから、使われていない stream 引数が削除されています。
変更前(イメージ):
# action_cable/server/stream_event_loop.rb
def detach(io, stream)
# 実際の処理は io のみを使っていた
@selector.deregister(io)
end
# action_cable/server/socket/stream.rb
event_loop.detach(@io, self)変更後(イメージ):
# action_cable/server/stream_event_loop.rb
def detach(io)
@selector.deregister(io)
end
# action_cable/server/socket/stream.rb
event_loop.detach(@io)背景:
- イベントループはコミット
322dca293bで導入されたが、そのときからdetach内ではioだけが使用され、stream引数は保持も参照もされていなかった。 attach側はstreamをどこかに記録する設計だが、detachの「登録解除」はioキーでのみ行っているためstreamは不要。- 呼び出し元は
StreamEventLoop#detachの単一呼び出し箇所のみだったので、そこも合わせてシグネチャ変更に追随。
ポイント:
- API 形だけ残っていた不要引数を削除し、実装と利用状況に整合するシンプルなインターフェースに整理した変更です。
- deregistration のキーは
ioのみ、という実装方針がこの変更によりより明確になります。
- 影響範囲・注意点
3-1. Railties (AppBase#dockerfile_chown_directories)
- Rails 本体内部でしか使われておらず、
.ttテンプレートも含めて未使用であることが確認されているため、標準的なrails newユーザーには影響ありません。 - 影響がありうるのは次のようなケースです:
Rails::Generators::AppBaseを継承して独自のアプリケーションジェネレータを作り、その中でdockerfile_chown_directoriesをオーバーライド or 呼び出している場合。- Rails 本体のジェネレータテンプレートをカスタムフォークしていて、このメソッドに依存している場合。
そのような場合:
- 生成される Dockerfile が既に
COPY --chownベースであるなら、このメソッドは単純に不要なので呼び出しやオーバーライドを削除できます。 - もし引き続き
chown -Rを使う古いスタイルの Dockerfile を維持したいなら、アプリ側に同等のメソッドを定義する/独自ジェネレータにロジックをコピーするといった対応が必要になります。
3-2. Action Cable (StreamEventLoop#detach のシグネチャ変更)
- Rails 内部コードと、Action Cable のイベントループ実装を直接触っていない通常のアプリケーションには影響ありません。
- 影響がありうるのは次のような場合です:
ActionCable::Server::StreamEventLoop#detachを直接呼び出している独自コードがある。StreamEventLoopを独自にサブクラス化してdetach(io, stream)シグネチャ前提でオーバーライドしている。
その場合の対応:
- 呼び出し側:
detach(io, stream)→detach(io)に引数を1つ減らしてください。 - オーバーライド側: メソッド定義を
def detach(io)に変更し、streamに依存する処理がある場合は、別の手段(attach時に自前でマッピングを保持するなど)に移行する必要があります。
- 参考情報 (あれば)
- このPRは未使用コード削除のシリーズの一部で、関連PR:
- #58214
- #58193
- コミット履歴:
- Dockerfile の
COPY --chownへの移行:658c989d8b StreamEventLoop導入:322dca293b
- Dockerfile の
- 変更統計:
- 変更ファイル数: 3
- 追加行数: 2
- 削除行数: 11
→ 小規模なリファクタリングであり、機能追加・挙動変更ではなく、コードベースの整備と明確化が主目的です。
#58218 Remove unused Active Support internals
マージ日: 2026/7/23 | 作成者: @55728
- 概要 (1–2文で)
Active Support 内部で使われていなかったパラメータ・インスタンス変数・古いシリアライザ用メソッドなどの「死んだコード」をまとめて削除・整理した PR です。外部 API の挙動変更はほぼなく、内部実装をスリムにするリファクタリングです。
- 変更内容の詳細
※すべて「使われていないことを grep などで確認したうえで削除」という方針の follow-up です。
2-1. ActiveSupport::Notifications::Fanout::Handle
# 変更前 (イメージ)
def initialize(group, notifier)
@group = group
@notifier = notifier # 実際にはどこからも読まれていない
end
# 変更後
def initialize(group)
@group = group
endFanout::Handle#initializeのnotifier引数が削除されました。- すでに
build_handle側でグループ構築を完結させるように変わっており、notifierはどこからも参照されていませんでした。 - コンストラクタは
:nodoc:扱いで、利用者は通常build_handle経由でしかHandleを取得しないため、外部コードへの影響は事実上ありません。
2-2. ContinuousIntegration::Group#execute_group
# 変更前 (イメージ)
def execute_group(name, group)
# name は使っていない
run(group)
end
# 変更後
def execute_group(group)
run(group)
endContinuousIntegration::Group#execute_groupのnameパラメータが削除されました。- メソッド導入時から未使用で、メソッド内部でも参照されていませんでした。
- 呼び出し側も同時に修正されているため、Active Support 外のコードでこのメソッドを直接呼び出していなければ影響はありません。
2-3. Testing::Parallelization::Server#stop_worker の引数整理
# 変更前 (イメージ)
def stop_worker(worker_id, worker_pid)
# worker_pid はここでは使っていない
# worker_id ベースで内部マップをクリーンアップ
end
# 変更後
def stop_worker(worker_id)
# worker_id ベースでクリーンアップ
endServer#stop_workerのworker_pid引数が削除されました。- 実装では
start_worker時に PID が内部マップに保存され、それをworker_idキーで参照して処理しており、stop_workerの引数として渡された PID は一切使われていませんでした。 - 対応する
worker.rbとテスト (server_test.rb) も引数数にあわせて修正されており、内部 API の一貫性が保たれています。
2-4. DeprecatedConstantProxy の書き込み専用インスタンス変数削除
# 変更前 (イメージ)
class DeprecatedConstantProxy
def initialize(old_const, new_const, message: default_message)
@old_const = old_const # 読まれていない
@new_const = new_const
@message = message
end
end
# 変更後
class DeprecatedConstantProxy
def initialize(old_const, new_const, message: default_message)
@new_const = new_const
@message = message
end
end@old_constがどこからも参照されていない「書き込み専用」になっていたため削除されました。- 以前は警告文生成に使っていたものの、後続のリファクタで
message:キーワード引数のデフォルトに折り込まれており、保持だけされていた状態です。 - Deprecation のメッセージ内容や挙動自体には影響しません。
2-5. ActiveSupport::Subscriber.attach_to の @inherit_all
# 変更前 (イメージ)
def self.attach_to(namespace, subscriber, inherit_all = false)
@inherit_all = inherit_all # インスタンス変数化されるがどこからも読まれない
inherit_all_local = inherit_all
# 以下はローカル変数 inherit_all_local を使用
end
# 変更後
def self.attach_to(namespace, subscriber, inherit_all = false)
inherit_all_local = inherit_all
# 処理はそのまま
end- クラスインスタンス変数
@inherit_allに値を書き込んでいたものの、どこからも読み出されていなかったため、その書き込みが削除されました。 - メソッド内部ではローカル変数だけが利用されており、
attach_toの挙動に変更はありません。
2-6. OrderedHash#to_yaml_type の削除
# 変更前
def to_yaml_type
"!ruby/ActiveSupport::OrderedHash"
end
# 変更後: メソッド削除OrderedHash#to_yaml_typeは Syck (古い YAML ライブラリ) 用のシリアライゼーションフックです。- Ruby 2.0 以降は Syck が削除され、標準は Psych になっており、Psych はすでに同ファイル内にある
encode_withを利用します。 - そのため、このメソッドは現在の Ruby / Active Support では実行経路に入らず、削除しても実質的な影響はありません。
2-7. ActiveSupport::Callbacks::Callback のアクセサ整理
# 変更前 (イメージ)
class Callback
attr_accessor :kind, :name
end
# 変更後
class Callback
attr_reader :kind, :name
endCallback#kind=/#name=がどこからも呼ばれておらず、「読み取り専用プロパティ」としてしか使われていなかったため、attr_accessor→attr_readerに変更されました。- これにより
kind=/name=メソッドは削除され、コールバックオブジェクトは生成後イミュータブルに近い形になります。 - PR 説明にもある通り、#58198 と同じパターンの整理です。
- 影響範囲・注意点
外部 API への影響はほぼなし
- 削除された/変更されたものは基本的に「内部実装」か、公式には利用を想定していない経路(
:nodoc:付きなど)です。 - ただし次のようなケースでは影響があります。
- 削除された/変更されたものは基本的に「内部実装」か、公式には利用を想定していない経路(
独自に内部 API を叩いている場合の互換性注意
ActiveSupport::Notifications::Fanout::Handle.new(group, notifier)
→ 2 引数で呼び出していると ArgumentError になります。build_handleを使うべきです。ContinuousIntegration::Group#execute_group(name, group)を 2 引数で呼んでいる独自コード
→ 1 引数に変更が必要です(nameはそもそも使われていなかったので、その情報はもともと捨てられていました)。Testing::Parallelization::Server#stop_worker(worker_id, worker_pid)を自前で呼んでいる場合
→ 2 番目の引数を削除し、stop_worker(worker_id)に合わせる必要があります。ActiveSupport::Callbacks::Callbackオブジェクトに対してcallback.kind = :beforeのように後から書き換えている場合
→NoMethodErrorになります。公式の使い方ではなく、副作用も未定義だったので、コールバックの再構築など別の手段に切り替える必要があります。
YAML シリアライズへの影響
OrderedHash#to_yaml_type削除は、現在サポートされている Ruby (Psych) では使われないフックなので、通常の環境では影響しません。- もし独自に Syck を復活させて使っているような非常に特殊な環境があれば、シリアライズ形式が変わる可能性がありますが、現代的な Rails/AS との組み合わせ自体が非サポートに近い状況です。
- 参考情報 (あれば)
- この PR は以下の未使用コードクリーンアップの続きです:
- #58214
- #58193
- 類似パターンのアクセサ整理: #58198
- Ruby の YAML 実装変更:
- Ruby 1.9 末期〜2.0 で Syck → Psych に移行し、Syck は削除済み
→ Active Support のencode_withなど Psych ベースの API だけを考慮すればよい状態になっています。
- Ruby 1.9 末期〜2.0 で Syck → Psych に移行し、Syck は削除済み
#58219 Remove unused Action Pack internals
マージ日: 2026/7/23 | 作成者: @55728
- 概要 (1-2文で)
Action Pack周辺の「参照されていない・実質デッドコード」だけを整理して削除したPRです。公開APIの仕様変更はほぼなく、内部実装やテストコードから使われていないメソッド・引数・インスタンス変数を取り除いています。
- 変更内容の詳細
それぞれ「いつから使われていなかったか」「どこからも呼ばれていないか」がコミット単位で確認されており、repo全体を grep(send/シンボル参照含む)してデッドコードであることを検証したうえで削除されています。
2-1. ActionDispatch::Routing::RouteSet#generate の method_name 引数削除
- 対象:
action_dispatch/routing/route_set.rb - 変更点:
RouteSet#generateの第3引数method_nameが削除されました。- すでに全ての呼び出し元が3引数を渡しておらず、
method_nameもメソッド内部で参照されていなかったための整理です。 - 背景として、
UrlGenerationErrorの生成がgenerateの外に移されたコミット(437ab2031e)以降不要になっていました。
※現行の呼び出し元はすでに新しいシグネチャに揃っているため、Rails 本体内部では挙動変化なし。ただし、内部APIに直接依存して RouteSet#generate を3引数で呼んでいるアプリ/ライブラリがあれば壊れる可能性があります。
2-2. ActionDispatch::DebugLocks#render_details の req 引数削除
- 対象:
action_dispatch/middleware/debug_locks.rb - 変更点:
render_detailsメソッドのシグネチャがdef render_details(req)→def render_detailsのように、未使用だったreq引数が削除されました。- メソッド本体では
reqは一切参照されていなかったため、純粋なクリーンアップです。
- デバッグミドルウェア内部で完結しており、外部から
render_detailsを直接呼ぶのは通常想定されていないため、一般的なアプリへの影響はありません。
2-3. Journey::Route#required_keys の削除
- 対象:
action_dispatch/journey/route.rb - 変更点:
Journey::Route#required_keysメソッドが削除されました。
- 理由:
- Journey が Action Pack に統合されて以降、このメソッドには呼び出し元がなくなっていました。
- 現在はルートフォーマッタは
required_partsとrequired_defaultsを直接利用しており、required_keysはその古いラッパーのような存在で、もはや使われていません。
Journey::Route を直接触る高度なメタプログラミングや独自ルーティング拡張で required_keys を叩いていた場合は NoMethodError になる可能性があります。
2-4. ResponseAssertions#parameterize の削除
- 対象:
action_dispatch/testing/assertions/response.rb - 変更点:
ResponseAssertions#parameterizeメソッド(テスト用アサーションヘルパ内のユーティリティ)が削除されました。
- 理由:
- 最後の呼び出し元はコミット
c3aaba0180で削除済みであり、以降はどこからも使われていませんでした。
- 最後の呼び出し元はコミット
ActionDispatch::IntegrationTest や独自のテスト基盤の中からこのメソッドを直接利用していた場合は壊れる可能性がありますが、Railsが提供する公式テストAPIとしてはドキュメント化されていない内部ユーティリティに近い位置付けです。
2-5. ActionDispatch::TestSession の @initially_empty 削除
- 対象:
action_controller/test_case.rb(経由でTestSession) - 変更点:
ActionDispatch::TestSessionのインスタンス変数@initially_empty(書き込みのみで読み出しゼロ)が削除されました。
- 理由:
- 追加されたコミット
f2c66ce392以降、一度も読み取られていなかった「書き込み専用」のデッドコード。 default_sessionのような Rack 側のフックでもないことを rack / rack-session 側まで確認済み。
- 追加されたコミット
セッション状態の判定に @initially_empty を monkey patch 等で使っている場合は影響しますが、通常の Rails アプリ・テストでは無関係です。
2-6. ParamsWrapper._set_wrapper_options の削除
- 対象:
action_controller/metal/params_wrapper.rb,params_wrapper_test.rb - 変更点:
- クラスメソッド
ParamsWrapper._set_wrapper_optionsが削除されました。 - テストでは、これまで
_set_wrapper_optionsを経由していた箇所を_wrapper_optionsクラス属性へ直接代入する形に変更。
- クラスメソッド
- 理由:
- 本番コードでは
wrap_parametersが_wrapper_optionsを直接設定しており、_set_wrapper_optionsは実質不要。 - メソッド自身も、その導入コミット
95ec448580以降、テストの中でしか呼ばれていない状態でした。
- 本番コードでは
ActionController::ParamsWrapper を直接叩いてラッパー設定をカスタマイズしている場合に _set_wrapper_options を呼んでいたならば破壊的です。ただしこれも内部ヘルパ的な位置付けで、一般的な利用では wrap_parameters 経由が想定されています。
2-7. Journey::Path::Pattern::MatchData#named_captures の削除
- 対象:
action_dispatch/journey/path/pattern.rb,pattern_test.rb - 変更点:
Journey::Path::Pattern::MatchData#named_capturesメソッドが削除され、それ専用のテストも削除されました。
- 理由:
- 自身のユニットテスト以外に呼び出し元が存在せず、機能としても外部から使われていませんでした。
Journey の内部表現を直接扱う高度なライブラリなどで、この MatchData を利用していた場合のみ影響します。
影響範囲・注意点
Rails本体の挙動は変わらない
- いずれの変更も「参照されていないコードの削除」に留まっており、通常のコントローラ・ルーティング・レスポンス・テストの振る舞いには変化がありません。
ただし内部APIへの依存があると壊れる可能性
以下のようなケースでのみ注意が必要です:ActionDispatch::Routing::RouteSet#generateを 3引数(method_name含む)で直接呼んでいる。ActionDispatch::DebugLocks#render_details(req)を 外から直接呼んでいる。Journey::Route#required_keysを使った独自ルーティング拡張をしている。ActionDispatch::Testing::ResponseAssertions#parameterizeをテストヘルパなどから直接使っている。ActionDispatch::TestSessionの@initially_emptyを読み取る monkey patch を入れている。ActionController::ParamsWrapper._set_wrapper_optionsを直接叩いている。Journey::Path::Pattern::MatchData#named_capturesに依存している。
これらはどれも「公開・安定API」というよりは 内部実装 or テスト用ユーティリティ に近いもので、通常のアプリケーションや一般的な gem のコードでは使われていない想定です。
アップグレード時のチェックポイント
- 自分のアプリ / ライブラリコードを static grep して、上記削除メンバ名が出てくるかを確認しておくと安全です。
- 出てきた場合は:
RouteSet#generate→ 公開されているルーティングヘルパ(url_for,*_url,*_path)を使う。ParamsWrapperやJourney周り → より上位の公開APIに寄せる、もしくは内部実装の変更を許容した上でコードを修正する。
- 参考情報 (あれば)
- このPRは以下の「未使用コードクリーンアップ」PRの継続です:
- #58214
- #58195
- #58196
- いずれも Action Pack / Journey / テスト関連の「参照されていない内部実装」を削除して、コードベースのメンテナンス性・可読性を高めることを目的としています。
#58220 Remove unused Active Model internals
マージ日: 2026/7/23 | 作成者: @55728
- 概要 (1–2文で)
Active Model / Active Record の内部で使われていない引数・メソッドを削除し、実装を整理する PR です。外部 API の挙動変更はなく、内部実装のクリーンアップが目的です。
- 変更内容の詳細
PR 全体として、「リポジトリ全体を grep した結果、一切参照されていないコード」を削除する、というポリシーで行われています。メタプロや send、シンボル参照も含めて探索済みで、「動的に呼ばれている可能性」も検証したうえで dead code と判断されています。
(1) AttrNames.define_attribute_accessor_method の owner 引数削除
対象:activemodel/lib/active_model/attribute_methods.rbactivemodel/lib/active_model/attributes.rbactiverecord/lib/active_record/attribute_methods/read.rbactiverecord/lib/active_record/attribute_methods/write.rb
背景:AttrNames.define_attribute_accessor_method は、属性に対応する accessor メソッドを定義するための内部ヘルパですが、昔は内部で owner.rename_method を呼んでいました。その処理が ca5542fed3 で削除されたため、owner 引数自体が使われなくなっていました。
今回の変更では、
- メソッド定義側から
owner引数を削除 - 呼び出し側(Active Model
Attributes、Active RecordRead/Write)も、渡していたownerを削除
といった形で シグネチャと呼び出しの両方 が揃えて変更されています。
簡略イメージ:
# 変更前(イメージ)
AttrNames.define_attribute_accessor_method(:attr_name, owner) do |attr|
owner.define_method(attr) { ... }
end
# 変更後(イメージ)
AttrNames.define_attribute_accessor_method(:attr_name) do |attr|
owner.define_method(attr) { ... } # owner は各呼び出し元のスコープで完結
endもともと、各呼び出し元は block の中で自分で owner に対して define_method しており、define_attribute_accessor_method の引数として渡している owner は一切使われていなかったため、削除しても振る舞いは変わりません。
(2) define_call の name 引数削除
対象:activemodel/lib/active_model/attribute_methods.rb
背景:
動的にメソッドを定義し、キャッシュする仕組みにおいて、define_call に渡されていた name 引数が、define_cached_method の実装変更(514d474836)により使われなくなっていました。今は mangled_name(内部用に加工された名前)をキーにキャッシュしており、元の name は用いられていません。
変更内容:
define_callメソッドのシグネチャからnameを削除- それに応じて、呼び出し側も
nameを渡さない形に修正
キャッシュのキーは既に mangled_name 基準に統一されていたため、内部実装の一貫性が増しつつ、不要な引数がなくなっただけで挙動は同じです。
(3) validation_context= (プライベート writer) の削除
対象:activemodel/lib/active_model/validations.rb
背景:valid? 呼び出し時の検証コンテキスト(on: :create など)を扱う内部 API として、validation_context= というプライベート writer が存在していました。しかし afecc8fd6c によって、valid? は context_for_validation.context = ... のように、別のオブジェクト・インターフェースへ直接アクセスする形に移行しており、この writer はどこからも呼ばれなくなっていました。
今回:
validation_context=の定義(プライベートメソッド)を削除
既にプライベートで、かつ完全に未使用なメソッドだったため、外側から依存しているコードは存在しない前提です。
(4) ConfirmationValidator#confirmation_value_equal? の未使用引数削除
対象:activemodel/lib/active_model/validations/confirmation.rb
背景:ConfirmationValidator は、validates :password, confirmation: true のようなバリデーションの実装です。その内部で、値の比較ロジックを行う confirmation_value_equal? というメソッドがありましたが、定義時(2438a1cf4e)から今まで、引数 record と attribute が実際には使われていませんでした。
変更内容:
- メソッドシグネチャから
record,attributeを削除し、実際に使用している引数(たとえばvalue,confirmedなど)のみに整理。 - 呼び出し側も、削除された引数を渡さない形に修正。
このメソッドは完全に内部用であり、引数を減らしても public API には影響しません。
- 影響範囲・注意点
- 影響範囲は Active Model / Active Record の内部実装に限定 されている想定です。
- 公開されているドキュメント化された API(
validates,attributes, Active Record の属性読み書きなど)の使い方や挙動は変わりません。 - ただし、以下のようなケースでは影響が出る可能性があります:
- Rails 内部の private / internal API に依存したメタプログラミング を行っている場合
- 例:
ActiveModel::Validationsの private メソッドvalidation_context=をsendで叩いている、AttrNames.define_attribute_accessor_methodを直接呼んでいる、等
- 例:
- 独自に
ActiveModel::Validations::ConfirmationValidatorを継承し、confirmation_value_equal?をオーバーライドしているが、古いシグネチャに依存している場合
(ただし通常はオーバーライド対象にもしていない前提のメソッド)
- Rails 内部の private / internal API に依存したメタプログラミング を行っている場合
そのような「内部 API に食い込む実装」をしていない一般的な Rails アプリケーションでは、影響は実質ゼロと考えてよいです。
テスト観点としては:
- 既存のモデルのバリデーションテスト(特に confirmation 系)の再実行
- 動的定義される属性の getter/setter(Active Record / Active Model Attributes)の動作確認
を行えば十分です。
- 参考情報 (あれば)
- この PR は、直前の unused-code クリーンアップ PR (#58214, #58193) のフォローアップとして位置づけられており、「未使用な内部コードの削除」を段階的に進めている流れの一部です。
- 各削除対象は「コミット履歴上いつから未使用になったか」が明示されており、それぞれ:
owner引数: ca5542fed3 以降未使用name引数: 514d474836 以降未使用validation_context=: afecc8fd6c 以降未使用confirmation_value_equal?のrecord/attribute引数: 2438a1cf4e で導入時から未使用
となっています。
このため、「意図せず呼ばれている可能性のあるコード」を削っていないことが履歴上も裏付けられています。
#58221 Remove unused Active Job parameters
マージ日: 2026/7/23 | 作成者: @55728
- 概要 (1-2文で)
Active Job内部で実際には使われていないメソッド引数を削除し、コードをシンプルにしたPRです。外部インターフェース(公開API)には影響せず、内部実装の整理に留まっています。
- 変更内容の詳細
2-1. ActiveJob::Continuation#run_step_inline の **options 削除
対象ファイル: activejob/lib/active_job/continuation.rb
- これまでのシグネチャ(イメージ):
def run_step_inline(name, start:, **options)
# `options` は一切参照されていなかった
end- 変更後:
def run_step_inline(name, start:)
end説明:
**optionsキーワード引数は、メソッド追加時(commit: 1b4484c43f)からずっと「受け取るだけで使っていない」状態だったと、リポジトリ全体のgrepで確認されています。- このメソッドの呼び出し側は
nameとstart:しか渡しておらず、実際にoptionsが指定されるケースもありませんでした。 - そのため、インターフェースから未使用のキーワード引数
**optionsを削除しています。
2-2. Async アダプタの Scheduler における queue_name: の削除
対象ファイル: activejob/lib/active_job/queue_adapters/async_adapter.rb
対象メソッド:
Scheduler#enqueueScheduler#enqueue_atこれまでのシグネチャ(イメージ):
def enqueue(job, queue_name:)
# queue_name は中で使われていなかった
end
def enqueue_at(job, timestamp, queue_name:)
# 同上
end- 変更後:
def enqueue(job)
end
def enqueue_at(job, timestamp)
end説明:
- 以前はキューごとに
AsyncJobを分ける実装だったものが、commit a66780bfff にて「単一のグローバル executor」を使う実装に変更されています。 - その際、
Scheduler側ではキュー名によるスケジューリングの分岐がなくなっており、queue_name引数はスケジューリングロジックに一切影響しない「死んだ引数」になっていました。 - 今回、その未使用引数
queue_name:を削除することで、実際の挙動とインターフェースを一致させています。 - 一方で、アダプタの外向きAPIである
AsyncAdapter#enqueue(job)/#enqueue_at(job, timestamp)などのシグネチャは変わっておらず、利用者の呼び出しコードはそのままで動作します。
- 影響範囲・注意点
Rails利用者 (アプリケーション側)
- Public API の変更はなく、通常の
ActiveJobの使い方(MyJob.perform_laterなど)には影響しません。 asyncアダプタを利用していても、標準的な利用であれば影響はありません。
- Public API の変更はなく、通常の
Rails内部を拡張している人・メンテナやプラグイン作者向け
ActiveJob::Continuation#run_step_inlineを直接呼び出していて、かつ**optionsを渡していた場合は、メソッドシグネチャが変わるためエラーになります。- ただしPR説明によれば、公式実装上は
optionsを渡している呼び出しは存在せず、基本的に想定されていない利用です。
- ただしPR説明によれば、公式実装上は
AsyncAdapterの内部Schedulerを直接利用し、queue_name:を渡していたコードがあれば、その部分は修正が必要です。- 具体的には、
enqueue(job, queue_name:)→enqueue(job)、enqueue_at(job, ts, queue_name:)→enqueue_at(job, ts)に変更する必要があります。
- 具体的には、
queue_nameを使ってスケジューラを拡張・差し替えしていた場合、そもそも a66780bfff 以降は「単一executor」実装になっているため、想定通り動いていない可能性があります。このPRはその事実をインターフェースにも反映した形です。
バージョンアップ時のマイグレーション観点
- 通常のアプリケーションでのマイグレーション作業は不要と思われます。
- gem やプラグインが
ActiveJob::ContinuationやActiveJob::QueueAdapters::AsyncAdapter::Schedulerを直接叩いている場合のみ、互換性確認が必要です。
- 参考情報 (あれば)
- このPRは未使用コード削除のフォローアップであり、直前の類似PR:
#58214を受けて追加で見つかった未使用パラメータを削除するものです。 - 関連する過去の変更:
Continuation#run_step_inline追加: commit1b4484c43f- Async アダプタが単一グローバル executor を使うようになった変更: commit
a66780bfff
→ この時点でqueue_nameベースのスケジューリングは実質無効化されていました。
#58222 Remove unused Action View internals
マージ日: 2026/7/23 | 作成者: @55728
- 概要 (1-2文で)
Action View 内部で使われていない引数・ブロック受け取りを整理し、「死んでいるコード(dead code)」を削除した PR です。外部 API の挙動や機能には実質的な変更はなく、内部実装のクリーンアップが目的です。
- 変更内容の詳細
全体像
- Action View 周辺の「もう参照されていない引数・ブロック」を削除
- すべて「呼び出し元を含めて grep で未使用を確認済み」
- 実際の処理ロジックは変えず、シグネチャの簡素化と不要コード削除のみ
2-1. ActionView::Template#handle_render_error の view 引数削除
対象ファイル
actionview/lib/action_view/template.rb
変更前のイメージ
def handle_render_error(view, e)
# 以前は error 時に refresh(view) などがあった
# その後 b8e6594181 で refresh(view) が削除された
end
# 同一ファイル内での唯一の呼び出し元
def render(view, locals, buffer = nil, &block)
...
rescue => e
handle_render_error(view, e)
endb8e6594181 でエラー発生時の refresh(view) フォールバックが削られて以降、handle_render_error 内で view は参照されていませんでした。
今回、その未使用引数 view をメソッドシグネチャと呼び出し元の両方から削除しています。
変更後のイメージ
def handle_render_error(e)
# view は使われていないので引数から除去
end
def render(view, locals, buffer = nil, &block)
...
rescue => e
handle_render_error(e)
end- 影響は
ActionView::Template内部に閉じており、外部から直接handle_render_errorを呼ぶことは通常ありません。
2-2. expanded_cache_key の template 引数削除
対象ファイル
actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb
背景
- 以前は
cache_fragment_nameにvirtual_pathを渡しており、そのためtemplate引数(template オブジェクト / virtual path を含む何か)がexpanded_cache_key経由で必要でした。 4671fe2040でvirtual_pathを渡さなくなり、expanded_cache_key内のtemplate引数が未使用になった。
変更前のイメージ
def expanded_cache_key(key, template)
# key は使うが template はもう使っていない
# 例: cache_fragment_name(key: key) だけになっている
end
def some_caching_method
template = ...
key = expanded_cache_key(record, template)
digest_path_from_template(template) # <- ここでは template を使う
endtemplate 自体はまだ呼び出し元側で digest_path_from_template(template) に使っているが、expanded_cache_key の引数として渡す必要はなくなっている状態でした。
変更内容
expanded_cache_keyメソッドの引数からtemplateを削除- 呼び出し元からも
templateを渡すのをやめる(ただし呼び出し元内では従来どおりdigest_path_from_template(template)などにtemplateを使用)
変更後のイメージ
def expanded_cache_key(key)
# key だけを使ってキャッシュキーを組み立てる
end
def some_caching_method
template = ...
key = expanded_cache_key(record) # template を渡さない
digest_path_from_template(template) # template 自体はまだここで利用
end- つまり「
expanded_cache_keyにtemplateを渡していたが、そこで使われていなかったのでやめた」というだけの変更です。
2-3. CacheExpiry::ViewReloader.create / #initialize のブロック引数削除
対象ファイル
actionview/lib/action_view/cache_expiry.rb
背景
CacheExpiry::ViewReloaderクラスは72abd6357dで導入されたが、その際からcreateクラスメソッドでブロックを受け取れるシグネチャになっていたinitializeでもブロックを受け取っていた
- しかし、ブロックを保存 (
@block = block) したり、yieldしたりしたことは一度もなく、呼び出し側もブロックを渡していなかった。
変更前のイメージ
class CacheExpiry::ViewReloader
def self.create(...)
new(...) do
# 実際にはここも使われていない
end
end
def initialize(..., &block)
# block をどこにも保存・実行していない
end
end変更内容
createからブロックパラメータを削除initializeから&blockを削除- 呼び出し側にもブロックを渡せない(渡す必要もない)形に統一
変更後のイメージ
class CacheExpiry::ViewReloader
def self.create(...)
new(...)
end
def initialize(...)
# ブロック関連の処理は完全に削除
end
end- 影響範囲・注意点
パブリック API への影響はほぼない想定
- 変更対象は Action View の内部実装 (
Template,PartialRendererの collection caching,CacheExpiry::ViewReloader) に閉じています。 - 通常のアプリケーションレベルでの
renderやcacheの使い方は変わりません。
- 変更対象は Action View の内部実装 (
直接内部メソッドを呼び出していた場合のみ互換性リスク
ActionView::Template#handle_render_error(view, e)を直接呼んでいたようなコードがあると、引数の個数不一致でエラーになります。expanded_cache_key(key, template)を直接呼び出しているカスタムコードがあれば、シグネチャ変更に追従が必要です(第二引数を削る)。CacheExpiry::ViewReloader.create { ... }/new { ... }のようにブロックを渡していた場合も、ブロックは受け取られず、引数エラーになる可能性があります。- ただし、これらは本来内部用であり、通常のアプリケーションが触るレイヤーではありません。
パフォーマンスやキャッシュキーの挙動は維持
expanded_cache_keyからtemplate引数が除かれても、もともとその値は使われていなかったため、キャッシュキーの内容に変化はありません。- 同様に、
handle_render_errorでviewを使っていなかったため、エラーハンドリングの実際の挙動も変更なしです。
- 参考情報 (あれば)
- 本 PR は以下の「未使用コード削除」PR のフォローアップ:
- #58214
- #58198
- 各削除対象は、以下のコミットで実質的に不要になっていたもの:
Template#handle_render_errorのview引数:b8e6594181expanded_cache_keyのtemplate引数:4671fe2040CacheExpiry::ViewReloaderのブロックパラメータ:72abd6357d
- Action View の内部 API に依存した拡張や monkey patch を行っている場合は、これらメソッドのシグネチャ変更有無を一度確認しておくと安全です。
#58213 Preserve sub-second precision when subtracting a DateTime from a Time
マージ日: 2026/7/23 | 作成者: @Saidbek
- 概要 (1-2文で)
Time - DateTimeの計算で、これまでFloat変換によりサブ秒精度(マイクロ秒)が失われていた問題を、Rationalを使った計算に切り替えることで解消した PR です。これにより、両者の差分がマイクロ秒単位まで正確に計算されるようになりました。
- 変更内容の詳細
何が問題だったか
元の実装では Time - DateTime(ActiveSupport が Time#- の上にレイヤを載せている部分)が、両方の時刻を to_f で Float に変換してから差分を計算していました。
# イメージ(実際の実装とは概念的な例)
(time.to_f - datetime.to_f) # Float 同士の引き算しかし Float は 2 進浮動小数のため、「任意の 1 マイクロ秒単位の値」を厳密に表現できません。
その結果、サブ秒(小数点以下の秒)部分を持つ DateTime との引き算では、約半分くらいのケースで誤差が発生していました。
PR 説明の例:
t = Time.utc(2000, 1, 1, 0, 0, 1)
dt = DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 1_000_000), "+0") # 0.000001 秒
t - dt
# 変更前: 0.9999990463256836 (Float、誤差あり)
# 変更後: (999999/1000000) (Rational、1 秒 - 1 マイクロ秒を厳密表現)整数秒差(例: Time.utc(2000, 1, 2) - DateTime.civil(2000, 1, 1))については元から誤差がなく、今回も挙動は変わっていません。
どう直したか
修正後は、Time と DateTime をどちらも有理数 (Rational) ベースで扱って差分を計算するように変更されています。
ポイント:
Time側はtime.to_rを使用Time#to_rは秒+サブ秒を含む精度の高い有理数を返します。
DateTime側は「整数秒部分 + 小数秒部分」を厳密に足し合わせた値を使うdatetime.to_i: 秒単位の整数部分datetime.sec_fraction: その秒の中の小数部分(Rational)- 「
datetime.to_i + datetime.sec_fraction」が、DateTimeの「正確な秒数表現」になります。
結果として、差分は以下のような形になります:
time.to_r - (datetime.to_i + datetime.sec_fraction)
# => 差分が Rational で返る(マイクロ秒精度を保持)PR によるサンプルのもう一つのケース:
t = Time.utc(2000, 1, 1, 0, 0, 1)
dt = DateTime.civil(2000, 1, 1, 0, 0, Rational(123_457, 1_000_000), "+0")
t - dt
# 変更前: 0.8765430450439453
# 変更後: (876543/1000000) # 1秒 - 0.123457秒 = 0.876543秒 を厳密に表現他の API との整合性
この変更により、以下の既存 API 群と計算方法が一貫しました:
Time.at(DateTime)(#58085 で Rational ベースに修正済み)ActiveSupport::TimeWithZone - DateTime- 内部的に
getutcを通じて、既に Rational ベースの正確なタイムスタンプを使用しています。
- 内部的に
つまり、Time / TimeWithZone / DateTime 相互の差分計算がすべて「Rational による厳密計算」に揃えられた形になります。
テストとドキュメント
activesupport/test/core_ext/time_ext_test.rbにサブ秒差分に対する新しいアサーションが追加され、マイクロ秒精度が保持されていることを保証。activesupport/CHANGELOG.mdに今回の変更内容が追記され、挙動変更が明示されています。
- 影響範囲・注意点
返り値の型が
FloatからRationalに変わるケースに注意これまで
Time - DateTimeの結果をFloat前提で使っていたコード(例:is_a?(Float)チェックやround(5)など)は影響を受けます。新挙動では
Rationalが返るため、そのまま演算に使う分には問題ありませんが、Floatが必須な場合は明示的にto_fする必要があります。rubydiff = t - dt # => Rational diff.to_f # => Float が欲しければ明示的に変換
整数秒差のみを扱っている既存コードは影響を受けにくい
- 整数秒差は Rational でも
86400のような整数として表現されるため、ほとんどのケースで以前と同様に動作します(== 86400のような比較もそのまま通る)。
- 整数秒差は Rational でも
時刻差をキーにしたハッシュ、シリアライズなどで型の違いによる影響が出る可能性
- 例: 差分を JSON で送っていたコードが、以前は小数、今は分数文字列になるといった変化を起こしうるため、そのような境界では型・フォーマットの確認が必要です。
- 参考情報 (あれば)
- 対応 PR: #58213 Preserve sub-second precision when subtracting a
DateTimefrom aTime - 関連 PR:
- #58085:
Time.at(DateTime)を Rational ベースに修正した PR
- #58085:
- Ruby 標準:
Time#to_r,DateTime#sec_fractionは Ruby 標準ライブラリの API で、どちらもサブ秒部分をRationalで安全に扱うためのメソッドです。
#58177 Preserve the declared parent order when dumping PostgreSQL INHERITS options
マージ日: 2026/7/23 | 作成者: @viralpraxis
- 概要 (1-2文で)
PostgreSQL のテーブル継承 (INHERITS) をスキーマダンプする際、親テーブルの並び順が宣言時の順序どおりになるように修正した PR です。これにより、schema.rb/structure.sqlのダンプ結果がより決定的(安定)になり、環境差異などによる不要な差分が減ります。
- 変更内容の詳細 (サンプルイメージ含む)
何が問題だったか
PostgreSQL の継承テーブルで、例えば次のようなテーブルを定義しているとします:
CREATE TABLE parent1 (...);
CREATE TABLE parent2 (...);
CREATE TABLE child (
...
) INHERITS (parent1, parent2);このとき、Rails が PostgreSQL のメタデータを元に INHERITS (...) をダンプするときの親テーブル順序が、PostgreSQL が返す順番依存になっており、宣言順 (parent1, parent2) が保証されていませんでした。
そのため、環境やクエリプラン・拡張などの影響で、たとえば以下のように順序が揺れる可能性がありました:
-- 環境A
CREATE TABLE child (...) INHERITS (parent1, parent2);
-- 環境B
CREATE TABLE child (...) INHERITS (parent2, parent1);この違い自体は動作上の問題にはなりにくいものの、スキーマダンプの diff が毎回出てしまう等、CI やレビューにおいてノイズになります。
どう修正したか
PostgreSQL の継承情報を格納する pg_inherits カタログには inhseqno というカラムがあり、これは「その子テーブルに対して、親テーブルが何番目に宣言されたか」を表します。
今回の修正では、この inhseqno で ORDER BY するように Rails のスキーマダンプロジックを変更しています。イメージとしては以下のような SQL になります(実際のコードはもう少し複雑ですが、趣旨は同じです):
SELECT
parent.relname
FROM
pg_inherits
JOIN pg_class parent ON parent.oid = inhparent
WHERE
inhrelid = 'child'::regclass
ORDER BY
inhseqno; -- 追加されたポイントこれにより、スキーマダンプ時の INHERITS ( ... ) 部分は、必ずテーブル宣言時の順序と一致します。
テストの変更
activerecord/test/cases/adapters/postgresql/schema_test.rb が 1 行差し替えられており、継承テーブルのダンプ順序が期待どおり(宣言順)になることを検証するように更新されています。
実質的に「順序依存の仕様をテストで固定化した」変更です。
- 影響範囲・注意点
対象:
- PostgreSQL を使っており、かつテーブル継承 (
INHERITS) を利用しているプロジェクト。 - スキーマダンプ(
schema.rbもしくはstructure.sql)をバージョン管理している環境。
- PostgreSQL を使っており、かつテーブル継承 (
影響:
- この変更を取り込むと、既存の継承テーブルの
INHERITS句の親テーブル順序が変わる可能性があります。- これまで「PostgreSQL が返す順任せ」だったものが、「宣言順に揃えられる」ため、初回適用時に
schema.rb/structure.sqlに差分が生じることがあります。 - 以後は決定的な順序になるため、同一スキーマであればダンプ結果は安定します。
- これまで「PostgreSQL が返す順任せ」だったものが、「宣言順に揃えられる」ため、初回適用時に
- この変更を取り込むと、既存の継承テーブルの
運用上の注意:
- 最初にこのバージョンへアップデートした直後のスキーマ差分は、仕様変更に伴う正しい差分である可能性が高いため、内容を確認した上でそのままコミットして問題ありません。
- 独自に
pg_inheritsにアクセスしているようなコードを書いている場合(かなりレアケース)でも、この変更は Rails 側の ORDER BY を追加しただけなので、直接の互換性問題は基本的に発生しません。
- 参考情報 (あれば)
- PostgreSQL 公式ドキュメント:
pg_inheritsカタログinhseqnoについての説明: 親テーブルが継承リスト中の何番目に現れるかを示すシーケンス番号。
- PR で挙げられている再現用拡張:
pg_disorder- PostgreSQL が返す行順を「わざと」不安定にし、順序に依存したコードの問題を顕在化させるための拡張。
- この拡張を使うと、今回のような「順序前提の暗黙的バグ」を簡単に再現できます。
#58214 Remove unused parameter
マージ日: 2026/7/22 | 作成者: @tenderlove
- 概要 (1-2文で)
Active Record のspawn_methods内で、もはや使用されていない可変長引数(*restのような引数)が削除されたメンテナンス用の変更です。rewhereサポート削除に伴って不要になっていた引数を整理しただけで、挙動の変更はほぼありません。
- 変更内容の詳細
背景
- 以前のコミット
c313d9161360f4539afeb36cc8353ad29ba6bef3でrewhere関連の機能が削除された。 - その際、
rewhereのために用意されていた「可変長引数(rest parameter)」だけがメソッドシグネチャに残り、実際には使われていない状態になっていた。
今回の変更
対象ファイル:
activerecord/lib/active_record/relation/spawn_methods.rbactiverecord/CHANGELOG.md
spawn_methods.rb 内のあるメソッド(merge! や spawn 周りのいずれか)で、以下のような形で使われていない引数が削除されています:
# 変更前(イメージ)
def some_method(other, *rest)
# rest は一切使われていない
# ...
end
# 変更後
def some_method(other)
# ...
end実際には 3 行削除・3 行追加なので、
- メソッド定義の引数リストから
*restを削除 - それに伴い、改行やインデントなどの微調整
- 仕様変更として
CHANGELOG.mdに記載を追加
という程度の小さな差分になっています。
- 影響範囲・注意点
主な影響対象:
- Active Record の内部 API (
ActiveRecord::Relationのspawn_methods内部) を「メソッドシグネチャレベルで」直接呼び出していたコード。 - 具体的には、Rails の内部実装に強く依存したメタプログラミングや monkey patch をしているライブラリ・アプリケーション。
- Active Record の内部 API (
通常のアプリケーションコードへの影響:
- 通常の
Model.where,merge,rewhere(既に削除済み)などのパブリック API を使っているだけであれば、挙動の変化はありません。 - 引数が削除されたメソッドは元々余分な
*restを受け取っていただけで、中では一切利用していなかったため、機能的な違いはありません。
- 通常の
バージョンアップ時の注意点:
もし
ActiveRecord::Relationの内部メソッドを monkey patch していて、以下のようなコードを書いている場合は、メソッド定義の引数を合わせる必要があります。ruby# 例: 元の内部メソッドに合わせたオーバーライド module MyPatch def some_method(other, *rest) # ... super end endこれを、
rubymodule MyPatch def some_method(other) # ... super end endのように引数のシグネチャを変更する必要が出る可能性があります。
- 参考情報 (あれば)
- この PR が依存している変更:
rewhereサポート削除コミット:c313d9161360f4539afeb36cc8353ad29ba6bef3
- 関連する場所:
ActiveRecord::Relationのクローンやマージ処理を担うspawn_methodsモジュール。
- 種別としては「未使用引数の削除」「内部 API の軽微なクリーンアップ」であり、機能追加・仕様変更ではなく技術的負債の整理に近い変更です。
#58197 Remove unused HABTM reflection storage
マージ日: 2026/7/22 | 作成者: @nvasilevski
- 概要 (1-2文で)
このPRは、has_and_belongs_to_many(HABTM)の内部で使われていなかったleft_reflectionの保存処理を削除する、極めて小さなクリーンアップ変更です。機能・挙動は一切変わらず、不要なコードが取り除かれただけです。
- 変更内容の詳細
何が削除されたか
対象ファイル:
activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
変更点:
- 内部の HABTM join モデルに対して
left_reflectionを保存していた処理(代入)が削除されました(2行削除・追加なし)。
イメージとしては、以下のようなコードがあったものが:
# 例: 内部 join モデルで reflection を保持していたイメージ
join_model = ...
join_model.left_reflection = reflection # ← この代入が行われていたこの left_reflection がどこからも参照されていなかったため、その代入自体が削除されています。
歴史的な経緯
PRの説明によると:
- 2013年のコミット
d3089adb1aでleft_reflectionの保存が導入され、middle_optionsが:sourceを埋める際に利用されていました。 - しかし 2019年のコミット
b30a23f53bで、その読み取り側(middle_optionsからleft_reflectionを読む処理)が削除されました。 - 結果として、代入だけが残り、どこからも使われない「死んだ状態」のフィールドになっていました。
- 今回のPRで、その不要な代入コード(および関連の保持)が取り除かれました。
HABTM 内部モデルと left_reflection
HABTM (has_and_belongs_to_many) は、内部的に「中間テーブルに対応する join モデル」を動的に定義して利用します。この join モデルに、元の関連(左側)のメタ情報として left_reflection を付与していた時期がありました。
left_reflection は Active Record の Reflection オブジェクト(関連のメタデータ)を指しており、それをもとに middle_options が source オプションを決めるなどの処理をしていましたが、その読み取りが 2019 年に削除されたことで、もはや join モデルに保持しておく意味がなくなっていた、という構造です。
- 影響範囲・注意点
一般的なアプリケーションへの影響
- 公開 API には一切変更がなく、通常の
has_and_belongs_to_manyの使い方や挙動は変化しません。 - マイグレーションやスキーマ定義、関連の宣言方法に変更は不要です。
- Rails アプリケーションコードの互換性への影響は基本的に「なし」と考えてよいです。
影響があり得るケース(かなりレア)
注意が必要になり得るのは、以下のように Rails の内部実装に依存したメタプログラミング をしているごく一部のコードです。
例えば:
- HABTM の内部 join モデルクラスに対して、
left_reflectionという属性・インスタンス変数・メソッドが存在することを前提にしていた場合。 ActiveRecord::ReflectionやActiveRecord::Associations::Builder::HasAndBelongsToManyの内部実装に直接アクセスし、left_reflectionを参照するようなパッチ・モンキーパッチを書いていた場合。
そのようなコードがあると、今回の削除により NoMethodError や nil 参照などが発生する可能性があります。ただし、このようなコードは極めてニッチで、一般的なアプリケーションやライブラリでは問題にならないと考えられます。
テスト・互換性
- 行数ベースでも 2 行削除のみであり、リスクはごく小さいクリーンアップです。
- 2019 年以降、すでに読み取り側がなくなっていたため、今回の変更はそれを整理しただけで、実質的な挙動は以前から変わっていません。
- 参考情報 (あれば)
- 該当 PR:
- 関連コミット:
left_reflection導入: https://github.com/rails/rails/commit/d3089adb1a76d0d0446e914e972cf3ef49964333left_reflection読み取り削除: https://github.com/rails/rails/commit/b30a23f53b52e59d31358f7b80385ee5c2ba3afe
- HABTM(
has_and_belongs_to_many)のガイド:- Rails Guides – Active Record Associations: https://guides.rubyonrails.org/association_basics.html#the-has-and-belongs-to-many-association
#58195 Remove unused Journey methods
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
Rails の内部ルーティングエンジン Journey において、既にどこからも呼ばれていない公開メソッドが 4 つ削除されました。外部 API ではなく内部クラス上のメソッド削除のため、通常のアプリケーションコードへの影響はほぼありません。
- 変更内容の詳細
削除されたメソッド
対象はいずれも ActionDispatch::Journey 配下の内部クラスです。
GTG::TransitionTable
ファイル: actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
削除メソッド:
# 削除されたメソッド(イメージ)
class ActionDispatch::Journey::GTG::TransitionTable
# ε-closure を返していた (NFA シミュレータ用)
def eclosure(state)
...
end
# 状態一覧を返していた
def states
...
end
end#eclosure- もともと
NFA::Simulatorをサポートするための互換メソッド。 - 該当シミュレータは 2020 年のコミット
639f5fda30で削除済みであり、以後このメソッドは実質的にデッドコードになっていた。
- もともと
#states- Journey 統合時 (
56fee39c39) に入ったものの、Rails 内に呼び出し元が存在しない。 - グラフ (DOT) 出力は
transitionsとaccepting_statesを使っておりstatesは不要。
- Journey 統合時 (
Nodes::Node / Nodes::Cat
ファイル: actionpack/lib/action_dispatch/journey/nodes/node.rb
削除メソッド:
# 削除されたメソッド(イメージ)
module ActionDispatch::Journey::Nodes
class Node
def cat?
false
end
end
class Cat < Node
def cat?
true
end
end
endNodes::Node#cat?/Nodes::Cat#cat?- ルーティング AST のノード種別として「連結 (concatenation) ノードかどうか」を判定するためのショートカット。
- 本来は
node.is_a?(Nodes::Cat)の代わりとしてnode.cat?を使う想定の API だった。 - しかし、カスタムルートの正規表現変更が不要になったコミット
0011e098a1で最後の呼び出し元が削除され、これ以降は未使用だった。
全体のコード差分
- 変更ファイル: 2
- 追加行: 0
- 削除行: 13
純粋に未使用メソッドの削除のみであり、既存挙動を変えるようなロジックの変更はありません。
- 影響範囲・注意点
影響がある可能性があるケース
Rails の内部クラスを直接触っているアプリ/ライブラリ
- 以下のいずれかを直接呼んでいた場合は
NoMethodErrorになります。ActionDispatch::Journey::GTG::TransitionTable#eclosureActionDispatch::Journey::GTG::TransitionTable#statesActionDispatch::Journey::Nodes::Node#cat?ActionDispatch::Journey::Nodes::Cat#cat?
- Journey は内部実装扱いであり、これらメソッドに依存するのは非推奨だったため、コード側の修正が必要です。
- 以下のいずれかを直接呼んでいた場合は
ルーター関連の拡張 / デバッグツール
- 独自にルーティング AST (Journey::Nodes) を解析したり、GTG::TransitionTable から状態情報を引き出しているツールがあれば確認が必要です。
- 典型的な Rails アプリケーションや普通のエンジニアリングでこのクラス群を直接使うことはほぼないため、一般的には影響はありません。
対応方針の例
cat?を使っていた場合:- 代わりにクラス判定を使用:ruby
node.is_a?(ActionDispatch::Journey::Nodes::Cat)
- 代わりにクラス判定を使用:
TransitionTable#statesを使っていた場合:- そもそも内部 API 依存なので、目的に応じて別の公開インターフェイス(ルーティング情報の取得方法)を検討すべきです。
- DOT レンダリングのような用途であれば、
transitionsやaccepting_statesを利用する方向に書き換えが必要になるかもしれません。
一般的なアプリ開発者への影響
config/routes.rbで通常のルーティング定義をしているだけ、もしくはrake routes等標準の機能だけを使っている場合は、この PR による実害はまずありません。- バグ修正ではなくクリーンアップ (デッドコード削除) であり、挙動変更は意図されていません。
- 参考情報 (あれば)
- 当該 PR:
- 関連コミット:
eclosureの呼び出し元だった NFA::Simulator 削除:statesが導入された Journey 統合:cat?の最後の呼び出し削除 (カスタムルート regex 周りの簡素化):
この PR は Rubydex と Ruby LSP を用いて「未参照メソッド」を洗い出した結果に基づくもので、Journey 周りの内部実装の整理・簡素化を目的としたものです。
#58198 Remove unused TemplateDetails readers
マージ日: 2026/7/22 | 作成者: @nvasilevski
- 概要 (1-2文で)
ActionView::TemplateDetails::Requestedクラスから、既にどこからも使われていない「生のテンプレート詳細情報」のリーダーメソッドと対応するインスタンス変数が削除されました。テンプレートマッチング処理がインデックスベース(*_idx)に完全移行したことに伴うデッドコード整理です。
- 変更内容の詳細
- 対象クラス:
ActionView::TemplateDetails::Requested - 削除されたもの:
- 「raw detail」を返すリーダーメソッド(例:
formats,variants,handlersといった、非_idxなリーダー) - それらに対応するインスタンス変数
- 「raw detail」を返すリーダーメソッド(例:
元々は以下のような構造だったと推測されます(イメージコード):
module ActionView
class TemplateDetails
class Requested
attr_reader :formats, :variants, :handlers
attr_reader :formats_idx, :variants_idx, :handlers_idx
def initialize(...)
@formats = formats
@variants = variants
@handlers = handlers
@formats_idx = ...
@variants_idx = ...
@handlers_idx = ...
end
end
end
endこのうち、テンプレート探索・ソート処理は後続のリファクタリング(eac19fea2b)で *_idx 系(formats_idx などの数値インデックス)だけを見るようになり、元の @formats / @variants / @handlers とそのリーダーは一切参照されなくなっていました。
今回のPRでは、*_idx 以外の上記リーダーとインスタンス変数が削除され、クラス定義が「実際に使われているインデックス情報だけ」を持つ形に整理されています。差分としては 1ファイルで6行削除のみという非常に小さい変更です。
- 影響範囲・注意点
- 公開APIへの影響:
ActionView::TemplateDetails::Requested自体は内部実装寄りのクラスであり、通常のアプリケーションコードでは直接触れません。- ただし、Rails内部APIや非公開クラスを参照しているメタプログラミング/パフォーマンスチューニング系のライブラリ・アプリが、
Requestedのformats/variants/handlers等の(非_idx)リーダーを呼び出していた場合はNoMethodErrorになる可能性があります。
- 実行時挙動:
- テンプレート解決のアルゴリズムや結果に変更はありません。既に
*_idxベースで動作していたため、削除されたリーダーは完全に未使用でした。
- テンプレート解決のアルゴリズムや結果に変更はありません。既に
- アップグレード時のチェック:
ActionView::TemplateDetails::Requestedに直接アクセスしていない限り、互換性上の問題はほぼありません。- gem や自前のパッチでこのクラスのインスタンス変数・メソッドに依存している場合は、
*_idx系の情報か、より安定した公開API(例えばActionView::LookupContextや通常のレンダリングAPI)を使うように修正した方が安全です。
- 参考情報 (あれば)
f8f9a085cc(2021):Requestedに raw detail リーダーおよびインスタンス変数を追加し、テンプレートマッチング・ソートに使用し始めたコミット。eac19fea2b: テンプレートインデックス周りのリファクタリングを行い、テンプレート探索で*_idx値のみを利用するように変更したコミット。これにより raw detail リーダーが実質デッドコード化。- 当該PR: https://github.com/rails/rails/pull/58198
#58196 Remove unused Journey endpoints reader
マージ日: 2026/7/22 | 作成者: @nvasilevski
- 概要 (1-2文で)
Rails のルーティングエンジン Journey 内のActionDispatch::Journey::GTG::Builder#endpointsという未使用のリーダーメソッドが削除されました。
追加も参照もされておらず常にnilしか返さない状態だったため、不要コードの整理として削除された変更です。
- 変更内容の詳細 (あればサンプルコードも含めて)
対象: actionpack/lib/action_dispatch/journey/gtg/builder.rb の 1 行変更(+1 / -1)
この PR では、ActionDispatch::Journey::GTG::Builder クラスに定義されていた「endpoints という読み取り用アクセサ(reader)」が削除されています。
イメージとしては、以下のようなコードが:
module ActionDispatch
module Journey
module GTG
class Builder
attr_reader :endpoints # ← これが削除対象
# ... ルーティング用のグラフ(オートマトン)を構築するロジック ...
end
end
end
endこの attr_reader :endpoints(もしくはそれに相当する reader 定義)が 1 行削除されています。
PR の説明によると:
- 2012年に Journey が Action Dispatch に統合されたコミット(
56fee39c39...)のタイミングで追加された - しかし Rails 本体のどこからも値が設定されず、メソッドとしても呼ばれていなかった
- そのため、この reader は常に
nilを返すだけの「死んでいる API」だった
という経緯があります。
- 影響範囲・注意点
Rails 本体:
- Rails コアコードでは
Builder#endpointsは一切使われていないため、挙動の変化は事実上ありません。 - ルーティングの振る舞い(マッチング・生成・制約処理など)には影響しません。
- Rails コアコードでは
アプリケーションコード・ライブラリ:
- 通常の Rails アプリケーションでは
ActionDispatch::Journey::GTG::Builderに直接触れることはほぼないため、多くのプロジェクトには影響しません。 - ただし、Journey/GTG に対して直接メタプログラミングや拡張を行っているような高度なミドルウェア・ルーティング拡張ライブラリがある場合、以下を確認してください:
ActionDispatch::Journey::GTG::Builder#endpointsを呼んでいないかrespond_to?(:endpoints)を前提としたコードがないか
- もし上記のようなコードがあれば、この PR 適用後は
NoMethodErrorとなるため、該当コードを削除・修正する必要があります。
- 通常の Rails アプリケーションでは
バージョン間互換性の観点:
- 実質的には「今まで
nilを返していたメソッドが、そもそも存在しなくなる」という変化なので、依存していなければ問題ありません。 - Rails の内部 API に依存していたコードに対しては、軽微ながら 後方互換性のない変更 と言えます。
- 実質的には「今まで
- 参考情報 (あれば)
- このメソッドが追加された元コミット:
56fee39c392788314c44a575b3fd66e16a50c8b5(Journey を Action Dispatch に統合したときのコミット)
- 関連コンポーネント:
ActionDispatch::Journey… Rails のルーティングをパース・コンパイル・マッチングするエンジンActionDispatch::Journey::GTG… ルーティングパターンをグラフ(Generalized Transition Graph)として表現・構築する部分
- この PR は「不要・未使用な内部 API の整理」によるコードベースのクリーンアップであり、バグ修正やパフォーマンス改善ではありません。
#58164 Generate omakase-compliant routes in the authentication generator
マージ日: 2026/7/21 | 作成者: @55728
- 概要 (1-2文で)
bin/rails generate authenticationが生成するconfig/routes.rbの記述スタイルを修正し、Rubocop(Layout/SpaceInsideArrayLiteralBrackets)に違反しない「おまかせ(omakase)」スタイルのルーティングが生成されるようにした PR です。これにより、生成直後のアプリが CI の Rubocop ジョブを素通りできるようになります。
- 変更内容の詳細
何を直したか
- 認証ジェネレータ (
rails g authentication) がconfig/routes.rbに差し込むルート定義の「配列リテラルのスペース」スタイルを修正。 - これまでは Rubocop の
Layout/SpaceInsideArrayLiteralBracketsに違反する形で配列が生成されていたため、新規アプリで即bin/rubocopが落ちていました。 - PR では、Rails が推奨する「omakase」スタイル(Rails 標準 Rubocop 設定に準拠)に合わせた出力に変更しています。
想定されるコードの変化イメージ
※実際の PR ではテンプレート内の文字列が 2 箇所修正されているだけですが、概念的には次のようなスタイル差分です。
修正前(Rubocop 違反例・角括弧の内側にスペースなし):
# config/routes.rb (生成物のイメージ)
authenticate :user, ->(user) { user.admin? } do
resources :users, only:[:index, :show]
end修正後(omakase 準拠・角括弧の内側にスペースあり):
authenticate :user, ->(user) { user.admin? } do
resources :users, only: [ :index, :show ]
end今回の PR で実際に行われたのは、authentication_generator.rb 内で routes.rb に挿入する文字列を、上記のように Rubocop が要求するスペース付き配列リテラルに揃える、という 2 行の修正です。
あわせて、authentication_generator_test.rb の期待値(生成結果を検証しているテスト)も同じスタイルに更新されています。
- 影響範囲・注意点
対象:
rails newした後にbin/rails generate authenticationを実行したときに生成されるconfig/routes.rbの該当ルート部分。- 認証ジェネレータによって生成されるルートのスタイルのみが変わり、ルートの挙動(パスや HTTP メソッド、コントローラ・アクションのマッピングなど)は一切変わりません。
影響:
- 生成直後に
bin/rubocopや CI の lint ジョブが Rubocop のLayout/SpaceInsideArrayLiteralBracketsで失敗しなくなる。 - 既存アプリには影響なし。既に生成済みの
config/routes.rbは自動で書き換えられないため、必要であれば手動でスタイルを揃える必要があります。
- 生成直後に
注意点:
- この PR はスタイル修正のみであり、機能的な変更はありません。
- Rubocop 側で
Layout/SpaceInsideArrayLiteralBracketsの設定をカスタマイズしている場合でも、Rails が「おまかせ」として想定しているデフォルトに合わせた出力になる点は変わりません。 - すでにこの Cop を無効化しているプロジェクトでは、見た目の違い以上の影響は特にありません。
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/58164
- 類似の修正: c67e9dfe19 / PR #50576(インストールされるマイグレーションで同じ Rubocop Cop を修正したもの)
- Rubocop 該当 Cop:
Layout/SpaceInsideArrayLiteralBrackets - Rails の「omakase」スタイル:
- Rails が公式に推奨するコードスタイルで、Rails 用 Rubocop 設定(
rubocop-railsなど)とも整合しており、今回のような PR は「生成コードがそのスタイルに即しているか」を担保するためのものです。
- Rails が公式に推奨するコードスタイルで、Rails 用 Rubocop 設定(
#58160 Fix NodeSource RPM URL in Development Dependencies Install guide [ci skip]
マージ日: 2026/7/21 | 作成者: @fuentesjr
- 概要 (1-2文で)
Railsガイド「Development Dependencies Install」の Fedora/CentOS 向け手順で使われている NodeSource RPM の URL が 404 になっていたため、正しい URL(pub_20.x)に修正した PR です。コード本体の変更はなく、ドキュメントのみの更新です。
- 変更内容の詳細
対象箇所:
guides/source/development_dependencies_install.md
修正前のガイドでは、Fedora / CentOS で Node.js をインストールする手順として、以下のような NodeSource RPM を利用する URL を案内していました:
sudo rpm -Uvh https://rpm.nodesource.com/pub_20/nodistro/repo/nodesource-release-nodistro-1.noarch.rpmしかし、この pub_20 パスは 404 を返すため、実際には利用できません。
NodeSource 側の現在の有効なパスは pub_20.x であり、PR ではその点を反映する形で該当行を以下のように修正しています:
sudo rpm -Uvh https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm差分としては pub_20 → pub_20.x という 1 セグメントのみの変更で、行の追加・削除も 1 行ずつです。
- 影響範囲・注意点
影響範囲:
- Rails 開発環境構築ガイドに従って Fedora / CentOS 上で開発用依存関係をセットアップするユーザー。
- 特に Node.js を NodeSource の RPM レポジトリからインストールしようとするケース。
実務的な影響:
- これまでガイドどおりに実行すると
rpmコマンドが 404 で失敗していた問題が解消されます。 - 正しい URL のため、Node.js のインストール手順がそのまま動作するようになります。
- これまでガイドどおりに実行すると
注意点:
- この PR はドキュメントのみで、Rails 本体のコードや挙動には一切影響しません。
- Node.js のメジャーバージョン 20 系を前提としている点は従来どおりであり、他バージョンを使いたい場合は別途 URL を読み替える必要があります(例:
pub_18.xなど)。 - NodeSource の URL 仕様変更に依存しているため、将来的に再度パスが変わる可能性があります。CI は
[ci skip]のためこの変更で自動検証は走っていませんが、ドキュメントのみなので問題はありません。
- 参考情報 (あれば)
該当ガイド:
- Development Dependencies Install – Fedora and CentOS セクション
https://edgeguides.rubyonrails.org/development_dependencies_install.html#fedora-and-centos
- Development Dependencies Install – Fedora and CentOS セクション
修正先の NodeSource RPM URL(20 系):
https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm
#58188 Remove unused V6_0::ReferenceDefinition
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
Rails 6.0 用の互換クラスV6_0::ReferenceDefinitionが、実際にはどこからも使われていない「死んだコード」であることが確認され、削除されました。これにより ActiveRecord のマイグレーション互換レイヤが少しシンプルになっています。
- 変更内容の詳細(あればサンプルコードも含めて)
- 変更ファイル:
activerecord/lib/active_record/migration/compatibility.rb - 変更内容:
V6_0::ReferenceDefinition関連の定義が削除され、合計 6 行が削除されています(追加なし)。
PR説明のポイントを整理すると:
RubydexとRuby LSPを使ってコード参照を調査した結果、V6_0::ReferenceDefinitionはどこからも参照されていないことが分かった。- 既存の
ReferenceDefinition.new呼び出しは、いずれもActiveRecord::ConnectionAdapters名前空間内に静的(レキシカル)に定義されており、常にConnectionAdapters::ReferenceDefinitionを指す。 - つまり、
V6_0::TableDefinitionがreferencesメソッドをprependで差し替えたうえでsuperを呼んでも、そのsuperの中で使われるReferenceDefinitionは互換クラスではなく、通常のConnectionAdapters::ReferenceDefinitionに解決される。
歴史的な流れ:
- 2020-10-30 (
e8437a68f6):
Rails 6.0 互換のreferences/add_referenceを提供するためにV6_0::ReferenceDefinitionが導入された。当時は互換メソッドから直接V6_0::ReferenceDefinition.newが呼ばれていた。 - 2022-03-14 (
7c5a8fc759f):
6.0 互換の挙動は、コンストラクタを差し替えるのではなく、_uses_legacy_reference_index_nameオプションを渡した上でsuperを呼ぶ方式に変更された。この時点でV6_0::ReferenceDefinitionはどこからも生成されなくなり、「死んだクラス」となった。
その結果、このPRでは単純にその未使用クラス定義を削除しただけ、という構造になっています。
- 影響範囲・注意点
- 公開APIへの影響:
- 通常のアプリケーションは
ActiveRecord::ConnectionAdapters::ReferenceDefinitionを使っており、V6_0::ReferenceDefinitionを直接参照するケースはまずありません。そのため、一般的なアプリには影響はありません。
- 通常のアプリケーションは
- 影響があり得るケース:
- Rails 内部の互換クラス (
ActiveRecord::Migration::Compatibility::V6_0::*) を、リフレクションやconst_getなどで直接参照して独自拡張しているような特殊なコードがあれば、V6_0::ReferenceDefinition削除でNameErrorになる可能性があります。
- Rails 内部の互換クラス (
- マイグレーション挙動:
- 互換挙動自体はすでに
_uses_legacy_reference_index_nameオプション+superで提供されており、このPRは単に使われていないクラスを削除しただけなので、references/add_referenceの実際の動きは変わりません。
- 互換挙動自体はすでに
- バージョン互換性:
- Rails 6.0 からのアップグレードパスのために残っていた「互換コードの残骸」を削除した形であり、Rails 7 系以降を前提にしたコードでは特に注意は不要です。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58188
- 関連コミット:
V6_0::ReferenceDefinition追加:e8437a68f6(2020-10-30)- 互換コンストラクタ削除・オプション化:
7c5a8fc759f(2022-03-14)
- 関連クラス:
ActiveRecord::ConnectionAdapters::ReferenceDefinitionActiveRecord::Migration::Compatibility::V6_0::TableDefinition
#58190 Deprecate Cookies::HTTP_HEADER constant
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
ActionDispatch::Cookies::HTTP_HEADER定数が非推奨(deprecated)になり、今後はRack::SET_COOKIEを使うことが推奨されるようにした PR です。既に内部的には未使用ですが、外部利用があるため、即削除ではなく非推奨期間を設けています。
- 変更内容の詳細
なにが非推奨になったか
- 対象:
ActionDispatch::Cookies::HTTP_HEADER定数 - 役割: 過去に cookie ミドルウェアが
Set-Cookieヘッダを正規化する際に使っていた定数 - 状況:
- 2010-05-18 に追加 (
d3e62fc57c) - 2023-01-21 の変更 (
706fb10ad5) で、Rack 2 / 3 両対応のため cookie 処理が再びRack::Responseに戻され、内部では使われなくなった - しかし API ドキュメントに公開されており、外部で参照しているコードがあるため、互換性のためすぐには削除せず「非推奨」として残す、という判断
- 2010-05-18 に追加 (
コード上の変更ポイント
PR で行われているのは主に次の3点です。
ActionDispatch::Cookies::HTTP_HEADERに deprecation を付与- CHANGELOG に非推奨の旨を追記
- 非推奨の挙動をテストで担保
実際のイメージとしては、cookies.rb では以下のように非推奨メッセージが出る形になっているはずです(概念的な例):
module ActionDispatch
class Cookies
# 既存: HTTP_HEADER = 'Set-Cookie'
HTTP_HEADER = 'Set-Cookie'
ActiveSupport::Deprecation.warn(
"ActionDispatch::Cookies::HTTP_HEADER は非推奨です。" \
"代わりに Rack::SET_COOKIE を使用してください。"
)
end
end※実装は ActiveSupport::Deprecation.deprecate_constant などを使った形になっている可能性が高いですが、意図としては「この定数を参照すると deprecation がかかる」というものです。
actionpack/test/dispatch/cookies_test.rb では、この定数に対して deprecation が発生すること・まだ定数自体は存在していることを確認するテストが追加されています。
- 影響範囲・注意点
影響を受ける可能性があるコード
- 以下のようなコードを書いているアプリ/ライブラリ:
ActionDispatch::Cookies::HTTP_HEADER # => 'Set-Cookie' を期待している- 上記のようなコードは:
- 今後の Rails バージョンで deprecation warning が出る
- 将来のメジャーバージョンで 定数自体が削除される可能性が高い
どう書き換えるべきか(推奨)
HTTP_HEADER を使っている箇所は、Rack::SET_COOKIE に置き換えが推奨されています。
# 変更前
header_name = ActionDispatch::Cookies::HTTP_HEADER
# 変更後
header_name = Rack::SET_COOKIERack を直接参照できない状況で Set-Cookie という文字列自体が欲しいだけなら、単に文字列リテラル "Set-Cookie" を使うのも選択肢ですが、この PR の説明にある通り「正準(canonical)な置き換え」は Rack::SET_COOKIE なので、Rack レベルのインターフェイスに従いたい場合はこちらを使うのが無難です。
注意点
- 今回は「非推奨化」であって即削除ではないため、現時点でアプリが壊れることはありません。
- ただし CI で deprecation をエラー扱いしているプロジェクトでは、警告が原因でテストが落ちる可能性があります。
- ミドルウェアやプラグインで Cookie / Header 処理をカスタマイズしている場合は、
ActionDispatch::Cookies::HTTP_HEADERを参照していないか確認しておくと安全です。
- 参考情報 (あれば)
- この PR の背景:
- 2010 年に「Rack::Response でラップしなくなったため、ミドルウェア側で
Set-Cookieを正規化する必要があった」ことからHTTP_HEADERが導入。 - 2023 年に Rack 2/3 両対応の流れで Cookie 処理が再び
Rack::Response側に戻され、HTTP_HEADERは内部では不要になった。
- 2010 年に「Rack::Response でラップしなくなったため、ミドルウェア側で
Rack::SET_COOKIEは Rack レベルでSet-Cookieヘッダ名を表す正準的な定数として提供されているものです。- 将来の Rails メジャーバージョンアップ時には、この定数が削除される可能性が高いので、外部ライブラリ作者は早めの移行が推奨されます。
#58191 Deprecate RedisCacheStore::DEFAULT_REDIS_OPTIONS
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
ActiveSupport::Cache::RedisCacheStore::DEFAULT_REDIS_OPTIONSが、Rails 内部では既に使われていないため、即削除ではなく「非推奨(deprecated)扱い」として残しつつ、将来的な削除に向けた移行パスを示した PR です。代わりに、アプリ側はRedisCacheStoreもしくは利用中の Redis クライアントに対して、タイムアウトなどのオプションを明示的に渡すように促されています。
- 変更内容の詳細
背景
DEFAULT_REDIS_OPTIONSは 2017-11-13 のコミット (9f8ec35352) で、標準の Redis キャッシュストア導入時に追加された定数。- しかし 2026-06-01 のコミット (5843a7b7f2) で redis-client への移行が行われ、その過程で ファイル内の呼び出し元がすべて削除 され、内部的には未使用状態になっていた。
- 一方で、この定数は API ドキュメントに公開されているため、外部アプリケーションが参照している可能性 がある。
この PR では、その事情を踏まえ、
- 直接削除せず、
- 値を保持したまま「非推奨」とし、
- 代替手段を案内する
という形に変更されています。
コード上の変更ポイント
(1)RedisCacheStore 内での定数定義の扱い変更DEFAULT_REDIS_OPTIONS 自体は 値を変えずに残しつつ、非推奨としてマーク されています。具体的には次のようなイメージです(実際のコードは概念的な例):
module ActiveSupport
module Cache
class RedisCacheStore < Store
# 以前から存在する定数
DEFAULT_REDIS_OPTIONS = {
# 例: connect_timeout: 1.0, read_timeout: 1.0, write_timeout: 1.0
}
# 新たに、利用時に非推奨警告を出すロジックが追加されている可能性が高い
def self.default_redis_options
ActiveSupport::Deprecation.warn(
"RedisCacheStore::DEFAULT_REDIS_OPTIONS は非推奨です。 " \
"代わりに RedisCacheStore.new または Redis クライアントに " \
"タイムアウト等のオプションを直接指定してください。"
)
DEFAULT_REDIS_OPTIONS
end
end
end
end実装としては、
DEFAULT_REDIS_OPTIONSの参照時にActiveSupport::Deprecation.warnを呼ぶ- または、
DEFAULT_REDIS_OPTIONS定数自体に対する Deprecation 表記・テストを追加
といった形で、「Rails が提供する公開 API だが、今後は使わないでほしい」という扱いになっています。
(2)CHANGELOG の更新activesupport/CHANGELOG.md に、以下のような内容が追記されています(意訳):
RedisCacheStore::DEFAULT_REDIS_OPTIONSを非推奨化したことこれまでこの定数に依存していたアプリケーションは、
RedisCacheStoreを初期化する際や、- 利用中の Redis クライアント (
redis-client等)
に対して、タイムアウト等のオプションを明示的に指定するように、というガイダンスが追記されています。
(3)テストの追加activesupport/test/cache/stores/redis_cache_store_test.rb に、
DEFAULT_REDIS_OPTIONSを使用したときに 非推奨警告が出ること を確認するテスト
などが追加されています。
これにより、今後の変更で誤ってこの定数を削除・変更してしまった場合にも検出できるようになっています。
- 影響範囲・注意点
影響を受ける可能性があるケース
以下のようなコードを書いているアプリケーションは影響を受けます。
# 例1: 定数をそのまま流用している
options = ActiveSupport::Cache::RedisCacheStore::DEFAULT_REDIS_OPTIONS.merge(
db: 1
)
# 例2: Redis クライアントにそのまま渡している
redis = Redis.new(
ActiveSupport::Cache::RedisCacheStore::DEFAULT_REDIS_OPTIONS.merge(url: ENV["REDIS_URL"])
)このようなコードは 動作自体は当面変わらないものの、Rails 実行時に deprecation warning が出るようになります。
対応方針
非推奨警告を見逃さない
- テスト実行時や開発環境のログに Deprecation Warning が出ていないか確認してください。
DEFAULT_REDIS_OPTIONSへの依存を廃止するDEFAULT_REDIS_OPTIONSを直接使わず、自分のアプリ側で必要なオプションを定義してください。
例:
rubyDEFAULT_REDIS_OPTIONS = { connect_timeout: 1.0, read_timeout: 1.0, write_timeout: 1.0 }.freeze redis = Redis.new(DEFAULT_REDIS_OPTIONS.merge(url: ENV["REDIS_URL"]))推奨される設定方法を使う
PR 説明にある通り、今後は以下のいずれかの方法でタイムアウト等のオプションを渡してください。RedisCacheStoreに直接指定:rubyconfig.cache_store = :redis_cache_store, { url: ENV["REDIS_URL"], connect_timeout: 1.0, read_timeout: 1.0, write_timeout: 1.0 }あるいは、
RedisClient/redis-client側のコンフィグで指定:rubyredis = RedisClient.new( url: ENV["REDIS_URL"], connect_timeout: 1.0, read_timeout: 1.0, write_timeout: 1.0 )
将来的な削除リスク
- この PR は「非推奨化」であり、将来のメジャーバージョンで
DEFAULT_REDIS_OPTIONSが削除される可能性が高い ことを意味します。 - 長期運用を見据える場合は、早めに依存を取り除いておく方が安全です。
- 参考情報 (あれば)
- PR 本文に記載されている関連コミット:
- 9f8ec35352:
RedisCacheStoreとDEFAULT_REDIS_OPTIONSが導入されたコミット - 5843a7b7f2:
redis-clientへの移行により、この定数の内部利用がなくなったコミット
- 9f8ec35352:
- Redis キャッシュストアの公式ガイド (英語):
- Rails Guides: Caching with Rails (
RedisCacheStoreセクション)
- Rails Guides: Caching with Rails (
- 実運用では、
DEFAULT_REDIS_OPTIONSの具体的な中身は Rails のバージョンにより異なる可能性があるため、
どうしても値を維持したい場合は、今利用しているバージョンでの中身を自前でコピーしておき、アプリ側の定数として保持するのが安全です。
#58193 Remove unused test methods
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
Rails 本体の「テスト専用コード」から、参照されていないメソッド/アクセサ計 24 個を削除するクリーンアップ PR です。
すべてtest/配下の変更であり、本番コードの挙動や公開 API には影響しません。
- 変更内容の詳細
全体像
- 対象は以下のコンポーネントの テストコードのみ:
- Action Pack
- Action View
- Active Job
- Active Record
- Active Support
- Railties
- Rubydex で「定義はあるがどこからも呼ばれていないメソッド」を洗い出し、
- リポジトリ全体で grep 確認
- コミット履歴と周辺コードを目視確認
- 動的ディスパッチで呼ばれる可能性があるもの(コールバックシンボル、インターポレーションされたアクション名、
*_attributes=などrespond_to?で叩かれるもの)は除外
したうえで、本当に誰からも呼ばれていないものだけを削除しています。
合計の変更量は「+1 / -98」で、実質的には不要メソッドの削除のみです。
コンポーネント別の主な削除内容
Action Pack (actionpack/test/...)
以下のような「テスト用コントローラのアクション」や「アサーションヘルパー」が削除されています。いずれも、過去のリファクタリングや非推奨機能削除の際に、呼び出し側だけ消えてテスト用メソッドが取り残されたものです。
例:
ActionPackAssertionsController#render_url- かつて
get :render_urlで使われていたが、非推奨タグサポート削除 (2ff60e8...) のときに呼び出しが消失。
- かつて
ControllerLayouts::ImplicitController#builder_override- 2009 年の新 base テストポートで追加されたが、ルーティングもテストからの呼び出しも無いまま。
- リダイレクト系:
RedirectController#dashboard_url- 旧
redirect_to(:symbol, *args)サポート用のテストだったが、非推奨 API 削除 (f770165...) とともに呼び出しが削除済み。
- 旧
- テストヘルパー:
ResourcesTest#assert_resource_methods- 旧
RouteSetAPI のテストから抽出されたアサーションだが、呼び出し元が削除済み。
- 旧
TestCaseTest::TestController#redirect_to_same_controller/#redirect_to_different_controller- 壊れていた functional test 実装の削除 (
db5839...) でテスト側が消えた。
- 壊れていた functional test 実装の削除 (
- Cookie 関連:
set_cookie_with_domain_all_as_string/delete_cookie_with_domain_all_as_string- 文字列
"all"を domain に使うテスト用に追加されたが、実際のテストはシンボルドメイン版のみ使用。
- 文字列
- パラメータパーシング用アクセサ:
UrlEncodedParamsParsingTest::TestController.last_request_type- multipart 向けに導入されたが、multipart 側のロジック削除 (
5a4390...) で参照が無くなった。
- multipart 向けに導入されたが、multipart 側のロジック削除 (
該当ファイル:
actionpack/test/controller/action_pack_assertions_test.rbactionpack/test/controller/new_base/render_layout_test.rbactionpack/test/controller/redirect_test.rbactionpack/test/controller/resources_test.rbactionpack/test/controller/test_case_test.rbactionpack/test/dispatch/cookies_test.rbactionpack/test/dispatch/request/url_encoded_params_parsing_test.rb(+1/-1 は不要アクセサ削除による微調整)
Action View (actionview/test/actionpack/controller/render_test.rb)
レンダリング関連のテスト用コントローラアクションのうち、呼び出しが存在しないものが削除されています。
例:
render_hello_world_with_last_modified_set/blank_response- ETag / conditional GET がミドルウェアに移動した (
74dd8a3...) 際に、これらを叩くリクエストテストが削除。
- ETag / conditional GET がミドルウェアに移動した (
render_action_hello_world_as_symbol- render テストのマージ (
a200c6...) でリクエスト側が削除済み。別途同名のインテグレーションフィクスチャが存在した時期もあるが、これは別定義で本メソッドを参照していない。
- render テストのマージ (
partial_collection_shorthand_with_different_types_of_records_with_counter- ルートもテストも無いまま追加されていたアクション。
partial_with_implicit_local_assignment- インスタンス変数からの暗黙ローカル代入が非推奨化・削除 (
7aa730...) されたときにテストが削除され、メソッドだけ残ったもの。
- インスタンス変数からの暗黙ローカル代入が非推奨化・削除 (
Active Job
ActiveJob::TestContinuation#capture_info_stdout- continuation テスト追加時 (
0ac3fb...) に導入されたが、一度も呼ばれていない。
- continuation テスト追加時 (
TestCaseHelpers#continuable_job_started- Sidekiq Active Job アダプタの非推奨削除 (
10c24f...) により最後の呼び出しが消失。
- Sidekiq Active Job アダプタの非推奨削除 (
該当ファイル:
activejob/test/cases/continuation_test.rbactivejob/test/support/integration/test_case_helpers.rb
Active Record
暗号化やトランザクションのテストヘルパーで、過去の大きなリファクタリングの副産物として「使われなくなった private ヘルパ」が削除されています。
EnvelopeEncryptionKeyProviderTest#assert_multiple_primary_keys- 元は
assert_multiple_master_keysとして抽出されたが、そもそも呼ばれておらず、その後名前だけ変更 (28145c3...) されても利用されていなかった。
- 元は
EncryptionHelpers#assert_encrypted_record- 大量暗号化テスト群削除 (
c41b35...) により呼び出しが消失。
- 大量暗号化テスト群削除 (
EncryptionHelpers#with_envelope_encryption- 暗号化ストレージの性能テスト削除 (
3e7ba5...) で最後の継承元からの呼び出しが消えた。
- 暗号化ストレージの性能テスト削除 (
TransactionTest#transaction_with_returnreturnによるロールバック挙動の非推奨化/削除 (eccc60...) により、このケース専用のヘルパーメソッドが不要に。- 同ファイルには、まだ使われている「浅い return 用」のヘルパーが残っており、こちらは温存。
該当ファイル:
activerecord/test/cases/encryption/envelope_encryption_key_provider_test.rbactiverecord/test/cases/encryption/helper.rbactiverecord/test/cases/transactions_test.rb
Active Support
CallbacksTest::ConditionalPerson#other_yes/#other_no- コールバック実装の書き換え (
21e7b8...) により、これらを参照していた condition 配列が削除されたが、メソッドは残っていた。
- コールバック実装の書き換え (
DeprecationTest#with_rails_application_deprecatorsActiveSupport::Deprecationインスタンスデリゲーションの非推奨削除 (c682bf...) の際に、これを使う 6 箇所の呼び出しがすべて削除済み。
該当ファイル:
activesupport/test/callbacks_test.rbactivesupport/test/deprecation_test.rb
Railties
TestHelpers::Paths#framework_path- 初期化処理のロードパス関連テスト (
4f6d6f7...) から使われていたが、そのテスト群が削除されて以降、どこからも使われていない。
- 初期化処理のロードパス関連テスト (
該当ファイル:
railties/test/isolation/abstract_unit.rb
- 影響範囲・注意点
影響範囲:
- Rails のテストコードのみに影響します。
- 本番コード (
app/,lib/等) や公開 API は一切変更されていません。 - CI 上で該当コンポーネントのテストはすべてグリーンであることが確認されています。
注意点:
- Rails コアのテストコードを参考にして「同名のメソッドを自前のテストスイートにコピペしている」場合でも、この PR はそれらには影響しません(自分のリポジトリの話になるため)。
- ただし、Rails 本体のテストを読み物/サンプルとして参照している場合は、「このメソッドどこにも呼ばれてないな」と思っていた箇所が整理された、という位置付けです。
- 動的に呼ばれていそうなメソッドは、意図的に削除対象から外しているため、
send,callbackシンボル,*_attributes=などを経由した隠れた参照を消してしまうリスクは抑制されています。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58193
- 主な過去コミット例(説明文中で参照されているもの):
- 非推奨タグサポート削除:
2ff60e86486141b5a5aae6e1a0a7521fbd85a269 - 旧
redirect_to(:symbol, *args)削除:f770165cc23a44ac329b5b1fbd47376a6c2e3f11 - 条件付き GET のミドルウェア移行:
74dd8a3681c6984ea35c879f88c6a87521b58ec2 - 暗号化関連テストの整理:
638a92f7344963c7e42eeb84eb816e4592bda7e4,c41b354bf065c9e59d65abbaeb22897f95ab47c0,3e7ba58439a434be5e6894993370a8cef403303e ActiveSupport::Deprecationの非推奨削除:c682bf26417f2a88b0efa46499e94dcf45955f4f
- 非推奨タグサポート削除:
この PR は、長年の互換性維持やリファクタリングの過程で取り残されていたテスト用ヘルパーを一掃する、テストスイートの健全性向上とノイズ削減のためのメンテナンスという位置付けです。
#58192 Remove unused Routing::HTTP_METHODS constant
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
ActionDispatch::Routing内で長らく使われていなかった定数HTTP_METHODSが削除されました。Rails本体からは既に参照されておらず、内部実装の古い残骸をクリーンアップする変更です。
- 変更内容の詳細
何が削除されたか
- 対象:
ActionDispatch::Routing::HTTP_METHODS定数 - 状態: もともと
:nodoc:扱いの内部定数であり、現在のRailsコードベースからは一切参照されていないことが確認されていました。
actionpack/lib/action_dispatch/routing.rb から、この未使用定数の定義が削除されています(+0/-3)。actionpack/lib/action_dispatch/routing/mapper.rb でも、関連する記述がわずかに調整されています(+1/-1)※主に定数削除に伴う整合性レベルの変更で、振る舞いの変化はありません。
元々の役割
PR説明によると:
- 2007-05-26 のコミット
dcaa074abfで追加- 用途: ルーティングで「メソッドが許可されていない (405 Method Not Allowed)」エラーを報告する際に、サポートしているHTTPメソッド一覧を列挙するための内部リストとして使用。
- 2009-12-11 のコミット
588225f885で実質的に未使用化- 理由: そのルーティング時のフォールバック実装が「hackが過ぎる」として削除され、この定数への参照もなくなった。
- 2010-03-31 のコミット
c10bf8205cでRouting実装詳細がRDoc対象外になり、定数も明示的に:nodoc:扱いに。
その後15年以上、Railsのどこからも参照されていない状態で残っていたところを、今回のPRで削除した、という経緯です。
- 影響範囲・注意点
影響が「ない」ケース
- アプリケーションコードやgemが
ActionDispatch::Routing::HTTP_METHODSを使っていない場合
→ 挙動の変化はありません。ルーティングの動作やHTTPメソッドの扱いには影響しません。
影響が「ある」可能性があるケース
内部定数として扱われていたため、本来は依存すべきではありませんが、もし以下のようなコードを書いていた場合はエラーになります。
# 例: Railsアプリやgem側で内部定数を直接参照していた場合
ActionDispatch::Routing::HTTP_METHODS # => NameError になるこのような箇所がある場合は、代替として自前で定義するか、Rack 由来の情報を使うなどに置き換える必要があります。
たとえば自前でサポートメソッドを定義する例:
SUPPORTED_HTTP_METHODS = %w[GET POST PUT PATCH DELETE HEAD OPTIONS].freeze※どのメソッドをサポートするかはアプリケーションやRackバージョンに依存するため、用途に応じて適宜定義してください。
バージョンアップ時のチェック
- Railsアップグレード時の非互換チェックとして:
ActionDispatch::Routing::HTTP_METHODSがコードベース・自作gem・社内ライブラリで使われていないか検索すると安心です。
- 参考情報 (あれば)
- このPRで言及されている過去コミット:
dcaa074abf(2007-05-26):HTTP_METHODS追加588225f885(2009-12-11): メソッド未許可時のフォールバック削除 → 定数が未使用にc10bf8205c(2010-03-31): Routing実装詳細がRDocから除外され、HTTP_METHODSも:nodoc:の内部定数として放置
- PR番号: #58192 「Remove unused Routing::HTTP_METHODS constant」
- マージ日時: 2026-07-21T22:56:57Z
#58194 Restore unregistered tests
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
このPRは、test_プレフィックスがついておらず Minitest によって実行されていなかった6つのテストメソッドを、正しくテストとして登録し直したものです。テスト内容自体は変えず、メソッド名だけを修正してテストが実行されるようにしています。
- 変更内容の詳細
背景
Minitest は、メソッド名が /^test_/ にマッチするインスタンスメソッドをテストとして自動検出します。
このPRで修正されたメソッドは:
- アサーションを含んでいる
- 他のどこからも呼び出されていない
- しかしメソッド名に
test_プレフィックスがなかった
ため、意図されたテストであるにもかかわらず一度も実行されていなかったという状態でした。
実際の変更内容
以下の 4 ファイルで、合計 6 個のテストメソッド名が修正されています。内容のロジックはそのままで、名前だけ変更されています。
対象ファイル:
actionpack/test/controller/action_pack_assertions_test.rbactiverecord/test/cases/finder_test.rbactiverecord/test/cases/primary_keys_test.rbrailties/test/generators/authentication_generator_test.rb
変更パターンはどれも同様で、例として以下のような修正が行われています:
# 修正前 (テストとして認識されない)
def missing_prefix_for_some_behavior
assert_equal 1, some_value
end
# 修正後 (Minitest によりテストとして検出される)
def test_missing_prefix_for_some_behavior
assert_equal 1, some_value
end行数ベースでは:
- 追加行: 6 (すべてプレフィックス付きメソッド名)
- 削除行: 6 (プレフィックスなしメソッド名)
であり、メソッド本体のアサーションやロジックには一切変更がありません。
- 影響範囲・注意点
影響範囲
- Rails 自身のテストスイートに限定され、本体のフレームワーク挙動や公開APIには影響しません。
- これまで「存在していたが実行されていなかった」内部テストが、CI 等で新たに実行されるようになります。
- もしこれらのテスト対象の挙動に不具合が既に紛れ込んでいた場合、このPR適用後に初めてテストが落ちる可能性があります。
注意点 (テストを書く側向け)
- Minitest を使う場合、必ず
def test_...という命名規約に従う必要があることを再確認できます。 - DSL (
test "description" do ... end) を使わず、素のメソッドで書いている箇所では特に命名ミスに注意が必要です。 - RSpec 等とは異なり、「それっぽい名前のメソッド」は自動で拾ってくれないので、IDE や RuboCop ルールなどで「
test_なしのテストメソッド」を検出する仕組みを入れておくと同様の問題を防げます。
- Minitest を使う場合、必ず
- 参考情報 (あれば)
- Minitest のテスト検出ルール:
Minitest::Testを継承したクラスのインスタンスメソッドで、名前が/^test_/にマッチするものが自動でテストとして実行される。
- 関連しうる改善アイデア:
- RuboCop のカスタム Cop や simplecov などと組み合わせて、「定義されているが一度も呼ばれていないテストっぽいメソッド」を検出することで、今回のような取りこぼしを防ぐことができます。
#58182 Remove unused ThreadMonitor#mon_try_enter method
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
RailsのActiveSupport::Concurrency::ThreadMonitorから、既にどこからも呼ばれていなかったmon_try_enterメソッドが削除されました。機能追加ではなく、不要コードの整理によるリファクタリングです。
- 変更内容の詳細
何が起きたか
- 過去のコミット
51d4feaabcでThreadLoadInterlockAwareMonitor→ThreadMonitorへ実装が差し替えられた際、ヘルパーメソッドmon_try_enterが旧実装からそのまま持ち込まれていました。 - 旧実装では
LoadInterlockAwareMonitorMixinがmon_try_enterを呼び出していましたが、このミックスイン自体が削除され、代わりにThreadMonitor#synchronizeがmon_enterを直接呼ぶようになりました。 - その結果、
ThreadMonitor内でmon_try_enterはどこからも参照されない「死んだメソッド」となっており、今回その定義が削除されています。
実際の変更
- 変更ファイル:
activesupport/lib/active_support/concurrency/thread_monitor.rb - 追加行: 0 / 削除行: 8
- 内容は
mon_try_enterメソッド定義の削除のみで、他のメソッドやクラス構造には手を加えていません。
イメージとしては、以下のようなコードがファイルからなくなった形です(概念的な例):
module ActiveSupport
module Concurrency
class ThreadMonitor
# これに相当するメソッド定義が削除された
def mon_try_enter
# MonitorMixin にある non-blocking な enter ラッパー相当の実装
end
end
end
endThreadMonitor#synchronize や mon_enter / mon_exit といった、実際に使われているメソッドはそのままです。
- 影響範囲・注意点
公式APIへの影響
ThreadMonitor#mon_try_enterは実質的に内部実装用のヘルパーで、これまでのコードパスでは利用されていなかったため、通常のRailsアプリケーションには影響しません。- 公開ドキュメントに載るような一般的APIでもありません。
独自に
ThreadMonitorを直接使っている場合- アプリ / gem 側で
ThreadMonitor#mon_try_enterを直接呼び出していた場合のみ、このPRを含むバージョンに上げるとNoMethodErrorになります。 - 該当するコードがある場合は、下記のいずれかへの置き換えが必要です:
- 本来の目的が「普通の排他制御」であれば、
synchronizeを利用する:rubymonitor.synchronize do # 排他処理 end - 「ブロックせずにロックを試みる」ようなtry-lock挙動が必要なら、
ThreadMonitor自体ではなく、別途MonitorやMutex相当の実装・パターンを採用する必要があります(ThreadMonitorはそうしたpublicなtry-lock APIを提供していません)。
- 本来の目的が「普通の排他制御」であれば、
- アプリ / gem 側で
挙動の変化
- 実際に動作するコードパスからは
mon_try_enterが一切呼ばれていなかったため、Rails内部のロック挙動やスレッドセーフティの意味での挙動変更はありません。 - これは純粋にデッドコード削除であり、パフォーマンスへの影響も事実上ありません(読み込むコード行がわずかに減る程度)。
- 実際に動作するコードパスからは
- 参考情報 (あれば)
- 関連コミット:
51d4feaabc- このコミットで
ThreadLoadInterlockAwareMonitor→ThreadMonitorへの置き換えが行われ、LoadInterlockAwareMonitorMixinが削除されました。 LoadInterlockAwareMonitorMixinから呼ばれていたmon_try_enterが、新実装では呼ばれなくなった経緯が説明されています。
- このコミットで
- このPRは、コードベースのクリーンアップ・保守性向上の一環として理解できます。
内部実装に依存したメソッド呼び出しを避けていれば、アップデート時に追加対応は不要です。
#58185 Remove unused ParamBuilder#new_depth_limit method
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
Rails のActionDispatch::Http::ParamBuilderクラスから、実際には一度も使われていなかったnew_depth_limitメソッドが削除されました。機能的な変更はなく、内部実装の不要コードの整理(リファクタリング)にあたります。
- 変更内容の詳細
- 対象ファイル:
actionpack/lib/action_dispatch/http/param_builder.rb - 変更内容:
ParamBuilderクラス内のnew_depth_limitメソッドを削除(4行削除、追加なし)
PR説明によると:
ParamBuilderが導入されたコミット(ce5f1817bc)の際に、Rack::QueryParserからnew_depth_limitメソッドがコピーされていた。- しかし Rails 側では、Rack に存在する「コール元(caller)」や「深さ制限を設定するためのメソッド」に相当するものを実装しておらず、そのためこのメソッドは導入当初から呼ばれていなかった。
- 結果として「未使用のヘルパーメソッド」となっていたため、今回削除された。
イメージとしては、以下のようなメソッドが消えた形です(実際の実装はPR本体を参照してください。あくまで概念イメージです):
# 変更前(イメージ)
module ActionDispatch
module Http
class ParamBuilder
# ... 他のメソッド ...
def new_depth_limit
# Rack::QueryParser からコピーされてきたが
# Rails 側では一度も呼ばれていない
end
end
end
end
# 変更後
module ActionDispatch
module Http
class ParamBuilder
# new_depth_limit が削除されただけで、
# 他の公開APIや挙動はそのまま
end
end
end- 影響範囲・注意点
公開APIへの影響:
ParamBuilder#new_depth_limitはもともと内部実装用で、実際にはどこからも呼ばれていなかったため、Rails の公式なパブリックAPIとして使われている想定はありません。- 一般的なアプリケーションコードに対する影響は実質ゼロと考えられます。
影響がありうるケース:
- Rails 内部実装に依存している高度なメタプログラミングや Monkey-patching をしていて、
ActionDispatch::Http::ParamBuilder#new_depth_limitを直接呼んでいた場合はNoMethodErrorになります。 - そのようなケースはかなりレアですが、もし独自に
ParamBuilderを拡張している場合は念のためgrepなどでnew_depth_limitの利用有無を確認すると安全です。
- Rails 内部実装に依存している高度なメタプログラミングや Monkey-patching をしていて、
動作上の変更:
- リクエストパラメータのパース挙動(深さ制限、ネスト制御など)には変更がありません。
- Rack 側の
QueryParserに存在する「ネストの深さ制限を調整するAPI」が Rails 側で使われていない状況は、以前から変わっていません。今回のPRは、それを明確化する形で未使用メソッドを削除しただけです。
- 参考情報 (あれば)
- 該当PR: https://github.com/rails/rails/pull/58185
ParamBuilder導入コミット:ce5f1817bc(PR説明より)- 元になった Rack のコード:
Rack::QueryParser(リクエストパラメータのパースとネスト深さ制限を扱うクラス)
今後、もし Rails が Rack の深さ制限設定をフックするAPIを提供する場合は、そのタイミングで改めて ParamBuilder 側に対応メソッドが追加される可能性がありますが、今回の変更はあくまで「使われていないコードの削除」に留まります。
#58176 Remove unused UriEncoder constants
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
Rails 内部クラスUriEncoderに定義されていた未使用の定数UTF_8とESCAPEDが削除されました。内部実装のデッドコード整理であり、公開APIの挙動には影響しません。
- 変更内容の詳細
対象ファイル:
actionpack/lib/action_dispatch/journey/router/utils.rb変更内容:
UriEncoderクラス内の、以下の2つの定数定義が削除されています(合計3行削除)。元のイメージ(簡略化例):
rubymodule ActionDispatch module Journey class Router class Utils class UriEncoder # :nodoc: UTF_8 = Encoding::UTF_8 ESCAPED = /%[0-9a-fA-F]{2}/ # ... end end end end endこの
UTF_8/ESCAPEDは、もともとUriEncoder#unescape_uriのために 2014年に追加されたもので、UTF_8:unescape_uriの出力文字列のエンコーディング指定ESCAPED: 「%xx形式のエンコード済みバイト列」にマッチさせる正規表現
として使われていました。
その後、2025-02-13 のコミット
dfc5e80e0fでunescape_uriが削除され、代わりにCGI.unescapeが使われるようになったことで、この2定数はどこからも参照されない「死んだコード」になっていました。今回のPRは、この未使用定数を削除して内部実装をクリーンアップするものです。
- 影響範囲・注意点
公開APIへの影響:
UriEncoder自体が内部用 (nodoc) クラスであり、Rails の公式な公開APIではありません。unescape_uriはすでに過去のコミットで削除済みのため、このPRで新たに公開挙動が変わることはありません。- アプリケーションコードや gem が 内部クラス
ActionDispatch::Journey::Router::Utils::UriEncoderの定数UTF_8/ESCAPEDに依存していない限り、影響はありません。
もし注意が必要なケース:
- Rails の内部実装に直接依存している高度なメタプログラミングやパッチングをしている場合で、rubyのように参照していた場合は、
ActionDispatch::Journey::Router::Utils::UriEncoder::UTF_8 ActionDispatch::Journey::Router::Utils::UriEncoder::ESCAPEDNameErrorになります。 - そのような場合は、用途に応じて:
- エンコーディングが欲しいだけなら
Encoding::UTF_8を直接使う - 「
%xx形式のエンコード済みバイト列」にマッチさせたいなら、自前で/%[0-9a-fA-F]{2}/を定義する などに変更する必要があります。
- エンコーディングが欲しいだけなら
- Rails の内部実装に直接依存している高度なメタプログラミングやパッチングをしている場合で、
URLデコード処理への影響:
unescape_uriはすでにCGI.unescapeに置き換えられているため、URLデコードの実際の挙動・互換性にはこのPRは関与しません。
- 参考情報 (あれば)
- 関連コミット:
a61792574d(2014-04-20):UriEncoder#unescape_uri向けにUTF_8とESCAPEDを追加。dfc5e80e0f(2025-02-13):unescape_uriを削除し、CGI.unescapeに置き換え。この時点で定数が未使用となる。
- 内部実装の今後の方針:
- Rails コアは内部 API のデッドコードを定期的に削除しており、今回もその一環。
内部クラス・内部定数への依存は将来のバージョンアップ時に壊れやすいため、可能な限り公開API(例:CGI.unescape等)を使うことが推奨されます。
- Rails コアは内部 API のデッドコードを定期的に削除しており、今回もその一環。
#58178 Remove unused Journey::GTG::MatchData
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
Rails の内部ルーティングエンジン Journey に存在していた未使用クラスJourney::GTG::MatchDataが、完全に参照されていない“死んだコード”であるため削除されました。これに伴い、そのクラスを前提としたコメント・コード片がsimulator.rbから取り除かれています。
- 変更内容の詳細
対象ファイル
actionpack/lib/action_dispatch/journey/gtg/simulator.rb
実際の変更
この PR では、MatchDataに関連するコードが 8 行削除されています(追加はなし)。
説明文から読み取れるポイントは以下の通りです。Journey::GTG::MatchDataは内部クラス(nodoc)であり、- コンストラクタ呼び出し(
MatchData.new) - クラス定数参照(
Journey::GTG::MatchData自体)
が一切残っていない状態になっていた。
- コンストラクタ呼び出し(
- 2017 年のコミット(
845aabbcd3)で、Simulator#simulateメソッドとその中のMatchData.new呼び出しが削除された時点で、このクラスは実質的に誰からも使われなくなっていた。 - 今回の PR は、その状況を整理し、
simulator.rbからMatchDataに紐づく残存コードを削除している。
実際のクラス定義自体は以前のコミットで消えている可能性が高く、この PR では「最後まで残っていた紐付け(コメントや不要な参照)」を掃除している、という位置付けと考えられます。
(差分は小さいため、サンプルコードとしてはイメージになりますが、以下のようなものが消えていると考えられます)
ruby# 例: すでに存在しない MatchData を想定したコード・コメント # result = MatchData.new(...) # または、MatchData に言及する内部用コメント など
- 影響範囲・注意点
公開 API への影響
Journey::GTG::MatchDataはnodocな内部クラスであり、Rails の公式公開 API ではありませんでした。- したがって、通常の Rails アプリケーションや一般的な gem への影響は基本的にありません。
影響があり得るケース
- Rails の内部実装(Action Dispatch / Journey)に直接依存し、
Journey::GTG::MatchDataを明示的に参照していたようなコードがもし存在すれば、ロード時にNameErrorとなる可能性があります。 - ただし、2017 年以降、コンストラクタ呼び出しも含めて実質的に死んだクラスだったため、そのようなコードが存在する可能性はかなり低いと考えられます。
- Rails の内部実装(Action Dispatch / Journey)に直接依存し、
マイグレーションの必要性
- アプリ側でこのクラスを参照していなければ、特別な対応やマイグレーションは不要です。
- もし独自に Journey の内部クラスを利用している場合は、今後も同様に「内部実装の変更・削除」に備えるべきです。
- 参考情報 (あれば)
- このクラスが入った経緯
- 2012-12-19 のコミット
56fee39c39で Journey が Action Pack 配下に移動した際にJourney::GTG::MatchDataが導入された。
- 2012-12-19 のコミット
- このクラスが実質的に死んだタイミング
- 2017-05-22 のコミット
845aabbcd3でSimulator#simulateと、その中のMatchData.new呼び出しが削除され、以後はどこからも参照されなくなった。
- 2017-05-22 のコミット
- 位置づけ
- 内部ルーティングエンジンの「デッドコード掃除(リファクタリング)」の一環であり、Rails コアの保守性向上・コードベースの簡素化を目的とした変更です。
#58180 Remove unused SAFE_FETCH_SITES constant
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
このPRでは、ActionController::RequestForgeryProtection内で定義されていた未使用のSAFE_FETCH_SITES定数が削除されています。CSRF保護の実装変更により参照されなくなっていた「死んだコード」をクリーンアップするものです。
- 変更内容の詳細
対象ファイル:
actionpack/lib/action_controller/metal/request_forgery_protection.rb(+0 / -5)
削除されたのは、Sec-Fetch-Site ヘッダをもとに CSRF 判定を行うために導入されていた SAFE_FETCH_SITES という プライベート定数 です。
元々は以下のような形だったと考えられます(イメージ例):
SAFE_FETCH_SITES = %w[same-origin same-site].freeze
private_constant :SAFE_FETCH_SITES
def valid_request_origin?
fetch_site = request.get_header("HTTP_SEC_FETCH_SITE")
# 古い実装イメージ:
SAFE_FETCH_SITES.include?(fetch_site)
endしかし、その後の変更(コミット 4a3bf76371)で、SAFE_FETCH_SITES.include? による判定ロジックが、Sec-Fetch-Site の値ごとに明示的な case 分岐などに置き換えられました:
def valid_request_origin?
fetch_site = request.get_header("HTTP_SEC_FETCH_SITE")
case fetch_site
when "same-origin"
# 同一オリジンとして許可
when "same-site"
# same-site として許可
else
# それ以外は拒否
end
endこのリファクタリング後、SAFE_FETCH_SITES はどこからも参照されなくなっていたため、本PRで定義自体が削除されています。結果として、当該ファイルから5行のみ削除され、挙動を変えるロジックの変更は一切ありません。
- 影響範囲・注意点
ランタイム挙動への影響はなし
SAFE_FETCH_SITESはプライベート定数かつ、既にどこからも参照されていなかったため、この削除によりアプリケーションの挙動が変わることはありません。アプリ側コードへの影響も基本的になし
private_constantとされていたため、通常の利用方法ではアプリケーション側から参照・上書きすることは想定されておらず、依存しているコードが存在する可能性は極めて低いです。- もし内部定数を無理に参照するようなメタプログラミング(
ActionController::RequestForgeryProtection.const_get("SAFE_FETCH_SITES")など)をしていた場合、そのコードはNameErrorを起こすようになりますが、これはフレームワークの非公開実装に依存しているため、サポート外と考えるべきです。
- もし内部定数を無理に参照するようなメタプログラミング(
CSRF保護ロジック自体には変更なし
既にSAFE_FETCH_SITES.include?を使わない形にロジックが移行済みであり、本PRはその後始末のみです。Sec-Fetch-Siteベースの CSRF 対策の動作仕様は変わりません。
- 参考情報 (あれば)
SAFE_FETCH_SITESが導入されたコミット:f5a1915f1d- 目的:
Sec-Fetch-Siteヘッダを用いた CSRF 保護の導入時に、安全とみなすサイト種別のホワイトリストとして導入。
- 目的:
SAFE_FETCH_SITESが実質的に不要になったコミット:4a3bf76371- 変更点:
SAFE_FETCH_SITES.include?による包括的チェックから、Sec-Fetch-Siteごとの明示的な分岐(same-origin,same-siteなど)へと判定ロジックを変更。
- 変更点:
- 本PR: #58180
- 役割: 上記ロジック変更後に残っていた未使用定数の削除によるコードクリーンアップ。
#58183 Remove unused ConnectionHandler#pool_managers method
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
Active Record のConnectionHandlerに定義されていた未使用メソッドpool_managersが削除されました。これは過去の非推奨パス削除後に呼び出し元がなくなっていたヘルパーメソッドの単純なクリーンアップです。
- 変更内容の詳細
対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract/connection_handler.rb- 追加行数: 0
- 削除行数: 4
- 削除されたメソッド:
ConnectionHandler#pool_managers
元々、この pool_managers メソッドは以下のような位置づけの「ヘルパー」として、
- コミット
74cb960e66でdeprecation_for_pool_handling向けに追加 - その後、コミット
64cbcd7a8cでその非推奨パス全体が削除
されていました。その結果、「呼び出し元が一切存在しないデッドコード」となっていたため、今回の PR でメソッド本体が丸ごと削除されています。
PR の diff 自体は 4 行削除のみのため、機能追加や挙動変更は一切なく、コードベースから未使用メソッドを取り除くリファクタリングに相当します。
- 影響範囲・注意点
フレームワーク内部への影響
- 現在の Rails 本体コードからは
pool_managersがまったく呼び出されておらず、挙動に影響はありません。 - コネクションプールやマルチ DB 等の挙動に変更はありません。
- 現在の Rails 本体コードからは
アプリケーションコード / プラグインへの影響
- 公開 API ではなく、内部実装寄りのメソッドですが、もし以下のようなコードを書いている場合は影響を受けます。rubyこのような呼び出しは
# 例: 内部メソッドに依存していた場合 ActiveRecord::Base.connection_handler.pool_managersNoMethodErrorとなるため、削除または別の公式 API を使う必要があります。 - 特に、独自の接続管理・コネクションプール管理を行うライブラリや、Rails 内部 API に依存するモンキーパッチを行っているコードは、一度
pool_managersへの依存がないか検索するのが安全です。
- 公開 API ではなく、内部実装寄りのメソッドですが、もし以下のようなコードを書いている場合は影響を受けます。
マイグレーションや設定への影響
database.ymlの書き方やestablish_connection等、通常の接続設定方法には影響しません。- コマンド (
rails console,rails db:migrateなど) の挙動も変更されません。
- 参考情報 (あれば)
- このメソッドが追加された経緯:
- コミット
74cb960e66にてdeprecation_for_pool_handling(コネクションプールの扱いに関する非推奨パス)を補助するためのヘルパーとして追加。
- コミット
- 削除に至った経緯:
- コミット
64cbcd7a8cで該当の非推奨パスが削除され、pool_managersの呼び出し元が消滅。 - 本 PR (#58183) にて未使用コードとして削除。
- コミット
内部 API に依存しない通常のアプリケーション開発者にとっては、特に対応不要のメンテナンス的変更です。
#58184 Remove unused cross_origin_request? method
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
ActionController::RequestForgeryProtection内に定義されていた未使用メソッドcross_origin_request?が削除されました。Fetch Metadata を用いた CSRF 対策実装の一部として追加されていたものの、実際にはどこからも呼ばれていなかったためのクリーンアップです。
- 変更内容の詳細(あればサンプルコードも含めて)
対象ファイル:
actionpack/lib/action_controller/metal/request_forgery_protection.rb行数の変化:
- 追加: 0 行
- 削除: 6 行
⇒ メソッド定義のみ削除され、他の処理や挙動は変わっていません。
削除された主な内容(イメージ):
ruby# 例: 実際の実装からの概略イメージ def cross_origin_request?(request) # Fetch Metadata ヘッダ (Sec-Fetch-Site など) を見て # same-origin / same-site かどうかを判定するようなロジック endPRの説明によると、このメソッドは Fetch Metadata ベースの CSRF 対策を導入したコミット
f5a1915f1dで追加されたものの、その後の実装ではこのメソッドを経由せず、「検証パス」と「警告パス」がそれぞれ直接同じ条件判定を行っている状態でした。そのためコード上は完全に未使用となっており、今回削除されています。ポイント:
- メソッド定義のみが削除され、ロジック自体は他の箇所にインラインで存在しているため、処理フローや条件そのものは変わっていません。
- Fetch Metadata CSRF 機能(
protect_from_forgery周辺の Fetch Metadata オプションなど)の挙動には一切変更なし。
- 影響範囲・注意点
パブリック API への影響:
cross_origin_request?は内部的にのみ使う想定のメソッドで、実際にも呼ばれていなかったため、通常のアプリケーションコードには影響はありません。- ただし、Rails 内部クラスをモンキーパッチ/リオープンしてこのメソッドを使っていた場合は影響を受ける可能性があります。
- 例:
ActionController::BaseあるいはActionController::RequestForgeryProtectionを開いてcross_origin_request?(request)を呼んでいた場合、NoMethodErrorとなります。
- 例:
想定される影響範囲:
- 通常の Rails アプリ: 影響なし。
- セキュリティまわりをフックする Gem や社内ライブラリ:
- Fetch Metadata CSRF 対策を拡張・上書きしており、その中で
cross_origin_request?を利用していた場合は修正が必要です。 - とはいえ、元々未使用であり公式に公開された API でもないため、そのような利用はかなりレアと考えられます。
- Fetch Metadata CSRF 対策を拡張・上書きしており、その中で
注意点:
- Fetch Metadata ベースの CSRF/リクエスト検査ロジック自体は残っているため、「CSRF 保護が弱くなる」「挙動が変わる」といった心配は不要です。
- 今後、Fetch Metadata 用の補助メソッドに依存した実装を書く場合は、今回削除されたような内部メソッドではなく、公開されている公式 API(ドキュメントに記載のある設定・メソッド)に依存する方が安全です。
- 参考情報 (あれば)
この PR のポイント:
- 未使用コードの削除によるメンテナンス性向上・内部 API の整理。
- Fetch Metadata CSRF 実装に関するリファクタリング(機能変更はなし)。
関連しそうな情報:
- Fetch Metadata ベースの CSRF 保護については、Rails ガイドの CSRF 章や、
ActionController::RequestForgeryProtectionのコメント・ドキュメントも参照すると、どのようなヘッダ(Sec-Fetch-Siteなど)を使って検査しているかを把握できます。 - 最初の導入コミット:
f5a1915f1d(PR 説明中で言及)
→ そこを見ると、当初どのようにcross_origin_request?が設計されていたかを確認できます。
- Fetch Metadata ベースの CSRF 保護については、Rails ガイドの CSRF 章や、
#58186 Remove unused Redis cache serialize_entries methods
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
Redis のキャッシュストア(RedisCacheStoreとDeprecatedRedisCacheStore)に残っていた未使用のヘルパーメソッドserialize_entriesが削除されました。実際の挙動には影響せず、不要コードの整理によるリファクタリングです。
- 変更内容の詳細
何が削除されたか
以下2ファイルから、未使用メソッド serialize_entries が削除されています。
activesupport/lib/active_support/cache/redis_cache_store.rbactivesupport/lib/active_support/cache/deprecated_redis_cache_store.rb
削除行数は計 12 行で、追加はありません。
背景と経緯
PR本文の説明を時系列で整理すると:
以前のコミット
f9a8646d81で、RedisCacheStore#serialize_entriesが導入された- 用途: Redis への一括書き込み用の
mapped_mset経路で利用するためのヘルパー - ざっくりいうと、「複数エントリを Redis にセットする前にまとめてシリアライズする」ためのメソッド
- 用途: Redis への一括書き込み用の
その後のコミット
f9fce850daで、mapped_msetによる一括書き込みは廃止され、- 各エントリごとに
pipelinedを使って書き込む方式に置き換えられた - 結果として
serialize_entriesの呼び出し元がなくなり、メソッドは「未使用」の状態に
- 各エントリごとに
さらにコミット
79b357bd89で、RedisCacheStoreのリネーム時に、この未使用メソッドがDeprecatedRedisCacheStore側にもコピーされてしまい、- 現行実装 (
RedisCacheStore) と Deprecated 実装の両方に、誰からも呼ばれないserialize_entriesが残り続けていた
- 現行実装 (
本 PR で、この未使用の
serialize_entriesが両クラスから削除された、という流れです。
削除されたメソッドのイメージ
PR本文から具体的なコード断片は提示されていませんが、典型的には以下のような形のヘルパーだったと推測されます(イメージであり、実際のコード断片ではありません):
# 参考イメージ: 実際のコードとは異なる可能性があります
def serialize_entries(entries, options)
entries.transform_values do |value|
serialize_entry(value, options)
end
endこのように、複数エントリを Redis に書き込む前にまとめて変換・シリアライズする役割のヘルパーが、書き込み経路変更により不要になった、という文脈です。
- 影響範囲・注意点
アプリケーションの挙動への影響は原則なし
- メソッド自体が完全に未使用だったため、Rails 内部の挙動は変わりません。
- キャッシュの保存・取得方法、Redis との通信方式(パイプラインでの書き込みなど)には変更ありません。
Monkey patch / メソッド呼び出しをしていた場合のみ注意
- 通常の使用では問題ありませんが、もし以下のような高度な使い方をしていた場合は注意が必要です:
ActiveSupport::Cache::RedisCacheStore/DeprecatedRedisCacheStoreに対してserialize_entriesを monkey patch していた- アプリ側から
store.send(:serialize_entries, ...)のように private メソッドを直接呼び出していた
- そのようなコードがある場合、この PR 以降のバージョンでは
NoMethodErrorになります。
- 通常の使用では問題ありませんが、もし以下のような高度な使い方をしていた場合は注意が必要です:
DeprecatedRedisCacheStore にも同様に適用
- Deprecated な Redis キャッシュストアにも同じ不要メソッドがあったため、両方から削除されています。
- 旧実装から新実装への移行を検討している場合でも、この変更が直接影響することは基本的にありません。
- 参考情報 (あれば)
このPR:
- タイトル:
Remove unused Redis cache serialize_entries methods - 番号:
#58186 - 作成者:
nvasilevski - マージ日時:
2026-07-21T19:06:07Z
- タイトル:
関連コミット(説明文中に言及されているもの):
f9a8646d81:RedisCacheStore#serialize_entries導入f9fce850da:mapped_mset経路を廃止し、エントリごとの pipelined 書き込みに変更79b357bd89:DeprecatedRedisCacheStoreへのリネーム時に未使用メソッドもコピー
位置づけ:
- これは「デッドコード削除・クリーンアップ」の類いのリファクタリングであり、機能追加や仕様変更ではありません。
- コードベースの保守性向上が主目的です。
#58147 [8-1-stable] Avoid loading ActionController::Live early in initializer
マージ日: 2026/7/21 | 作成者: @nurey
- 概要 (1-2文で)
Rails 8.1 系で、initializer 内でconfig.action_dispatch.default_headers = { ... }を丸ごと置き換え設定しても反映されないリグレッションを修正するバックポートです。原因だったActionController::Liveの早期ロードを避けるため、新しい load hook:action_controller_liveを導入し、設定が正しいタイミングで反映されるようにしています。
- 変更内容の詳細
問題の内容・原因
Rails 8.1.2 / 8.1.3(および 8.0.5+)で以下のような initializer を書いても:
# config/initializers/default_headers.rb
Rails.application.config.action_dispatch.default_headers = {
"X-Frame-Options" => "DENY",
"X-Content-Type-Options" => "nosniff"
}アプリケーションのレスポンスヘッダには反映されませんでした(「丸ごと置き換え」が無視される)。
動作が壊れているのは 8.1.2 / 8.1.3 / 8.0.5+ で、7.2 / 8.0.0–8.0.4 / 8.1.0–8.1.1 では問題ありません。
原因は、#56393 で追加された initializer action_controller.live_streaming_excluded_keys が、initialize! の過程で ActionController::Live を直接参照してしまうことにありました。
ActionController::Liveが参照される- そのロード過程で
action_dispatch/http/responseがrequireされる - これにより
ActiveSupport.on_load(:action_dispatch_response)のフックが「config/initializers/*.rb」より先に発火 - レスポンスのデフォルトヘッダが「initializer 実行前の値」で確定してしまう
結果として、initializer で config.action_dispatch.default_headers = { ... } を「後から丸ごと置き換え」しても、すでにキャプチャ済みの値には反映されず、無視されたように見える、という状態になっていました。
今回の修正内容
この PR は、main ブランチに入っている修正コミット f9c82c3f2a(およびテスト)を 8-1-stable にそのままバックポートしたものです。
ポイントは:
ActionController::Liveを initialization 中にロードしないようにした- そのために 新しい load hook
:action_controller_liveを導入 した - 問題のリグレッションを検出する 回帰テストを追加 した
1. ActionController::Live のロードタイミングを遅延させる
もともと action_controller.live_streaming_excluded_keys の initializer 内で、ActionController::Live を直接参照していましたが、これを以下のように ActiveSupport.on_load(:action_controller_live) でラップする形に変更しています(イメージ):
ActiveSupport.on_load(:action_controller_live) do
# ここで Live 関連の設定・除外キーなどを行う
endこの変更により:
ActionController::Liveを使うまではActionDispatch::Responseがロードされない- よって
:action_dispatch_responseの load hook も、config/initializers/*.rb実行後まで遅延される - その結果、initializer で上書きした
config.action_dispatch.default_headersが「最終値」として正しくフックから参照される
という流れになります。
2. 新しい load hook :action_controller_live の導入
ActionController::Live に紐づく処理を遅延させるために、ActiveSupport.on_load 用の新しいフックシンボル :action_controller_live が導入されています。
これに伴い:
actionpack/lib/action_controller/metal/live.rbactionpack/lib/action_controller/railtie.rbguides/source/configuring.md
などに、action_controller_live フックに関する定義・ドキュメントが追加されています。
今後は、ActionController::Live に依存する設定やモンキーパッチを、このフックを使って遅延実行することもできます(例):
ActiveSupport.on_load(:action_controller_live) do
# Live を include した後にしかできない設定など
end3. 回帰テストの追加
railties/test/application/configuration_test.rb に以下のようなテストが追加されています(要旨):
- テスト名:
test_config.action_dispatch.default_headers_can_be_set_in_an_initializer_and_is_applied_to_responses - 内容:
- initializer で
config.action_dispatch.default_headersを設定 - 実際のレスポンスヘッダに、設定した値が反映されていることを検証
- initializer で
このテストが:
cd railties
bin/test test/application/configuration_test.rb \
-i "test_config.action_dispatch.default_headers_can_be_set_in_an_initializer_and_is_applied_to_responses"で、修正前の 8-1-stable では落ち、今回の修正後には通ることが確認されています。
- 影響範囲・注意点
影響を受けるバージョン
- リグレッションのあるバージョン: 8.1.2, 8.1.3, 8.0.5+(PR 説明より)
- 本 PR は
8-1-stable向けバックポートなので、8.1.x 系の次のパッチリリースに入る想定です。
アプリケーション側での挙動の変化
config/initializers/内でconfig.action_dispatch.default_headers = { ... }
のように「ハッシュごと置き換え」しているアプリで、設定が正しくレスポンスに反映されるようになります。- 7.2 や 8.0.0–8.0.4 / 8.1.0–8.1.1 と同じ挙動に戻るだけなので、「仕様変更」というよりリグレッション修正です。
ActionController::Liveを使っているアプリへの影響ActionController::Liveやストリーミングを普通に使っているだけなら、挙動上の差はほぼありません。- ただし、独自に
ActionController::Liveのロードタイミングに依存していたり、on_load(:action_dispatch_response)と絡めて高度な初期化順序に頼っていたコードがある場合、ロード順が微妙に変わる可能性はあります。- そのような高度な使い方をしているケースは稀と思われますが、もし心当たりがあれば、8.1 系パッチ適用後に念のため確認すると安心です。
新しい load hook を stable ブランチに入れることについて
- PR 説明にもある通り、
:action_controller_liveは「新しく公開される load hook」であり、本来なら minor 以降で入れるのが綺麗です。 - ただし、今回は main ブランチの修正をほぼそのままバックポートして divergence を減らす意図があり、stable にも入れている、という判断になっています。
- 将来、ガイドや他の gem がこのフックを前提とする可能性があるため、「8.1 でも
:action_controller_liveが使える」という点は、gem 作者にとってはポジティブな影響です。
- PR 説明にもある通り、
- 参考情報 (あれば)
元 issue: #58145 — リグレッションの詳細な解析
元 PR: #56393 —
action_controller.live_streaming_excluded_keys追加の変更本 PR の元になった main 側の修正: コミット
f9c82c3f2a「Follow up to #56393」regression test (main 側): #58146
動作確認コマンド(PR 説明より再掲):
bashcd railties bin/test test/application/configuration_test.rb \ -i "test_config.action_dispatch.default_headers_can_be_set_in_an_initializer_and_is_applied_to_responses"
#58175 Remove unused EMPTY_BINARY_STRING constant
マージ日: 2026/7/21 | 作成者: @nvasilevski
- 概要 (1-2文で)
Rails の ActiveSupport::Cache コーダ関連コードから、どこからも参照されていなかった定数EMPTY_BINARY_STRINGが削除されました。機能的な挙動の変更はなく、完全にデッドコードのクリーンアップです。
- 変更内容の詳細
対象ファイル:
activesupport/lib/active_support/cache/coder.rb変更点:
EMPTY_BINARY_STRINGという定数定義が 1 行だけ削除されています。
元々は「キャッシュのデシリアライズエラー処理を改善する」変更 (コミットd51ad367e2, 2026-02-04) の一部として追加された定数ですが、そのタイミングでも実際には使用されておらず、その後も使われることがなかったため、今回削除されています。
イメージとしては以下のような定義が削除された形です(正確な内容は PR から省略されていますが、ニュアンスとして):
# 削除されたイメージ
# EMPTY_BINARY_STRING = +""Rubydex(コード参照解析ツール)とリポジトリ全体のコード検索により、この定数が一度も読み出されていないことが確認された上で削除されています。
- 影響範囲・注意点
影響範囲:
- Rails 本体の挙動には影響ありません。
- この定数は内部でも外部でも使用されていなかったことが確認済みのため、通常のアプリケーションコード・gem に対する実行時挙動の変化はありません。
注意点:
- 公開 API ではない内部定数ではありますが、もしアプリケーション側や独自 gem が「たまたま」
ActiveSupport::Cache::Coder内のEMPTY_BINARY_STRINGを直接参照していた場合、その参照はNameErrorになります。- 例:ruby
ActiveSupport::Cache::Coder::EMPTY_BINARY_STRING # => NameError (削除後)
- 例:
- そのような使い方は本来想定されていないため、基本的には互換性問題とはみなされない類の変更です。
- 公開 API ではない内部定数ではありますが、もしアプリケーション側や独自 gem が「たまたま」
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58175
- 当該定数が追加されたコミット:
d51ad367e2(2026-02-04、「キャッシュのデシリアライズエラー処理の改善」の一環) - 種別としては「内部実装のクリーンアップ / デッドコード削除」に該当し、アップグレード時に特別な対応は不要です。
#58171 Fix bundle install on ruby-dev
マージ日: 2026/7/21 | 作成者: @Earlopain
- 概要 (1-2文で)
ruby-dev(Ruby 開発版)でbundle installが失敗していた問題を、利用していた gem を更新して解消した PRです。Rails 本体のコードは変更せず、Gemfile.lockの依存バージョンのみを修正しています。
- 変更内容の詳細
- 変更ファイルは
Gemfile.lockのみで、6行追加・6行削除(= 一部依存 gem のバージョン入れ替え)です。 - 背景として、「deprecated(非推奨)だった Ruby の API が
ruby-devで削除され、その API を使っていた gem が動かなくなった」ため、bundle installが失敗していました。 - この PR では、その削除済み API に依存していた gem を、非推奨 API を使わない新しいバージョンに更新することで、
ruby-dev上でもbundle installが通るようにしています。
コードとしての変更は Rails アプリ(フレームワーク)の Ruby ファイルにはなく、あくまでロックファイルのバージョン調整です。
(PR情報からは、どの gem がどのバージョンに上がったかの詳細までは読み取れませんが、典型的には rubocop / listen / bootsnap / sqlite3 など、C拡張や Ruby 内部 API に触れる gem が対象になりやすいです。)
- 影響範囲・注意点
影響範囲
- Rails 本体の挙動や API には変更なし。
- 開発・CI で
ruby-dev(Ruby の HEAD / trunk)を使っている環境に対して、bundle installが成功するようになります。 - 通常の安定版 Ruby(3.2 / 3.3 など)を使っている場合は、依存 gem のマイナーなバージョンアップによる挙動差が発生しうる程度です。
注意点
- テスト追加は行われていないため、「
ruby-dev+ この Gemfile.lock」という組み合わせでのバンドル成功を、主に手動確認に頼っている状態です。 - Gem の更新により、間接的に:
- Ruby バージョン要件が厳しくなっていないか
- 開発ツール系(lint, test helper など)の挙動に変化がないか
を確認する価値があります。
Gemfile自体は変えていないので、ローカルでbundle updateなどを行うと、別の解決パターンになり得ます。Rails 本体の開発に参加する場合は、この PR マージ後のGemfile.lockを基準にするのが安全です。
- テスト追加は行われていないため、「
- 参考情報 (あれば)
- この種の PR は「Ruby 本体の trunk/HEAD で非推奨 API が削除された時」に定期的に発生し、Rails リポジトリでは主に以下を目的に行われます:
- Ruby の将来バージョンに対する事前互換性確保
- CI(特に Ruby HEAD を回しているジョブ)の安定運用
- 自分のプロジェクトで同様の問題が出た場合の対処の基本パターン:
ruby-devやruby-headで失敗している gem を特定(bundle installのログを見る)。- その gem の changelog / issue tracker を確認し、Ruby HEAD 対応版へ更新。
- 必要であれば
bundle update <gem名>→Gemfile.lockをコミット。
#58163 [ci-skip][doc] Fix inconsistent links in Active Job guide
マージ日: 2026/7/21 | 作成者: @hachi8833
概要 (1-2文で)
Active Jobガイド内のリンク表記ゆれを修正し、ドキュメント内のリンクスタイルを統一したPRです。コードや挙動の変更はなく、ドキュメントのみへの軽微な修正です。変更内容の詳細
- 対象ファイル:
guides/source/active_job_basics.md - 変更内容:
Active Jobガイド内にあるリンクの一部で、Markdownのリンク表記に使う角カッコ
[]が不足しており、他の箇所と形式が不一致だった部分を修正実質的には、以下のような「リンクの括弧漏れ」を直していると考えられます(例・イメージ):
md# 修正前(例) See Active Support Instrumentation(https://guides.rubyonrails.org/active_support_instrumentation.html) for more details. # 修正後(例) See [Active Support Instrumentation](https://guides.rubyonrails.org/active_support_instrumentation.html) for more details.追加行数2・削除行数2のため、2箇所程度で
[]の付け忘れや、他と異なるリンク表現を統一した変更とみられます。
- 影響範囲・注意点
- 影響範囲:
- Rails本体のコード、API挙動、Active Jobの仕様・インターフェースには一切影響しません。
- 影響するのは、Rails Guides(Active Job Basics)の表示のみです。
- ガイドをHTMLにビルドした際、該当箇所が正しくリンクとしてレンダリングされるようになります。
- 注意点:
- ドキュメント向けの変更なので、アプリケーション側で対応すべきことはありません。
- ドキュメントを翻訳・派生させているプロジェクトでは、同様のリンク表記ゆれがないか確認しておくと良いです。
- 参考情報 (あれば)
- PRタイトルの
[ci-skip]から、CIを走らせる必要がないドキュメント専用の変更として扱われていることがわかります。 - Rails GuidesのMarkdownでは、基本的に標準的なMarkdownリンク構文(
[テキスト](URL))で統一されているため、今後ガイドにPRを出す際もこのスタイルに合わせるのが望ましいです。
#58166 Let dump_schema_migrations_sort_by be :reverse by default
マージ日: 2026/7/20 | 作成者: @fxn
- 概要 (1-2文で)
db/schema.rbにschema_migrationsテーブルをダンプする機能について、デフォルトの並び順が「辞書順(昇順)」から「逆順(:reverse、最新のマイグレーションが先)」に変更されました。これにより、この機能を有効化したときに、マージコンフリクトを避けやすい並び順が最初から採用されます。
- 変更内容の詳細
背景
- #58134 で
schema_migrationsをdb/schema.rbに出力できるようになった際、デフォルトの並び順は「lexicographic(文字列昇順)」でした。 - しかし、PR 作成者自身が「painful」と表現していたように、実際に使うとマージコンフリクトの原因になりやすく、実用上望ましくないと判断され、今回デフォルトを変更しています。
デフォルト値の変更
dump_schema_migrations_sort_byという設定のデフォルトが以下のように変更されています:ruby# 以前 (#58134 時点) config.active_record.dump_schema_migrations_sort_by = :lexicographic # または nil 扱いで昇順 # 今回の PR 後 config.active_record.dump_schema_migrations_sort_by = :reverse実際のコード上はシンボル
:reverseをデフォルトとして使うように変更されており、
「最新のマイグレーションが先頭、古いものが後ろ」という逆順でschema_migrationsがdb/schema.rbに出力されるようになります。
ドキュメントの更新
以下のガイドが更新されています:
guides/source/active_record_migrations.mdschema_migrationsを schema.rb にダンプするオプションと、その並び順の説明が更新。- デフォルトが
:reverseであること、必要なら線形(昇順)にもできることが明記されていると考えられます。
guides/source/configuring.mdconfig.active_record.dump_schema_migrationsおよびconfig.active_record.dump_schema_migrations_sort_byの説明が更新。例として、以下のような設定がガイドに載っているはずです:
ruby# schema_migrations を schema.rb に書き出す config.active_record.dump_schema_migrations = true # デフォルト(今回の PR で :reverse になった) # 最新のマイグレーションが先頭に来る # config.active_record.dump_schema_migrations_sort_by = :reverse # 昇順の線形な並びにしたい場合(例) config.active_record.dump_schema_migrations_sort_by = :lexicographic # または、実装によっては :linear, :ascending 等の別名があるかもしれません
activerecord/CHANGELOG.md- 元々書かれていた「デフォルトが昇順である」という記述が修正され、 デフォルトが
:reverseであることと、その意図が追記されています。
- 元々書かれていた「デフォルトが昇順である」という記述が修正され、 デフォルトが
テストの更新
activerecord/test/cases/schema_dumper_test.rbschema_migrationsのダンプ結果に対するアサーションが、「昇順前提」から「逆順前提」に書き換えられています。- 例えば、最後のマイグレーションの ID が先頭付近に存在することをチェックするような形に変わっています。
railties/test/application/configuration_test.rb- アプリケーション設定 (
config.active_record.dump_schema_migrations_sort_by) のデフォルト値が:reverseであることを前提としたテストに修正されています。
- アプリケーション設定 (
- 影響範囲・注意点
影響範囲
- 対象となるのは 「
schema_migrationsをdb/schema.rbにダンプする設定を有効にしたアプリ」 のみです。- デフォルトでこの機能は オフ なので、多くの既存アプリには影響しません。
- すでに #58134 以降の Rails を使っていて、
config.active_record.dump_schema_migrations = trueを設定し、- さらに「デフォルトの並び順(昇順)である」という前提で何らかの処理やレビュー運用をしていた場合、
その前提が変わります。
実務的な注意点
これから
dump_schema_migrationsを有効にする場合:- 何も設定しなければ
:reverseになります。
→ マージコンフリクトを避ける、履歴を追いやすいといった、実用上望ましい挙動がそのまま得られます。
- 何も設定しなければ
線形(古いものから新しいものへ昇順)で並べたい場合:
明示的に設定が必要です:
rubyconfig.active_record.dump_schema_migrations_sort_by = :lexicographic # またはガイドで示されている昇順用のシンボル
CI やツールで
db/schema.rbのschema_migrations部分をパースしている場合:- 「常に昇順である」ことを前提にしたロジックがあるなら、逆順でも動くようにしておく必要があります。
- ただし、そもそもこの機能は opt-in であり、かつ新しめのバージョンの変更なので、
そのような前提を置いているツールは多くないと考えられます。
- 参考情報 (あれば)
- この PR (#58166) が前提としている機能追加 PR:
- #58134 –
schema_migrationsをdb/schema.rbにダンプする機能の追加。
- #58134 –
- 関連設定項目(概念整理):
config.active_record.dump_schema_migrationstrueでschema_migrationsをdb/schema.rbに書き出すようになる。
config.active_record.dump_schema_migrations_sort_by:reverse(今回の PR でのデフォルト): ID を逆順に並べる(新しいマイグレーションが先)。- 昇順で線形な履歴が欲しい場合は、ガイド記載のシンボル(例:
:lexicographic)を明示的に指定。
#58157 Assert @verified directly in the non-StandardError interrupt test
マージ日: 2026/7/19 | 作成者: @yahonda
- 概要 (1-2文で)
Rails 8-0-stable で落ちていたActiveRecord::AdapterConnectionTestのテストを、verified?メソッドではなくインスタンス変数@verifiedを直接検査するように変更して修正した PR です。テストの意図(「非 StandardError 系の割り込みが発生すると、接続が再検証対象になること」)は維持されたままです。
- 変更内容の詳細
何が問題だったか
対象テスト:
ActiveRecord::AdapterConnectionTest
#test_a_non-StandardError_interrupt_marks_the_connection_for_re-verification8-0-stable ブランチでは、以下のような失敗が deterministically(毎回確実に)起きていました:
Expected #<ActiveRecord::ConnectionAdapters::Mysql2Adapter:...> to respond to #verified?.原因は以下の通りです。
- このテストはバックポート PR #57953 によって 8-0-stable に入った
- テスト内では
assert_not_predicate @connection, :verified?(=refute_predicate @connection, :verified?)を使っていた - しかし 8-0-stable の
ConnectionAdaptersには:- インスタンス変数
@verified - それを
trueにするverified!メソッド
はあるものの、 verified?リーダーメソッドが存在しない
- インスタンス変数
- さらに、minitest 6.0.4 以降では
refute_predicateが事前にrespond_to?をチェックするようになったため、- メソッド呼び出し前に「
verified?が存在しない」という assertion エラーになってしまう
(以前の minitest だと NoMethodError で落ちるだけだが、いずれにせよテストは通らない)
- メソッド呼び出し前に「
一方、main ブランチでは以下の変更が入っていて問題にならない:
- コミット 28638775db ("Preconnect connections that are idle in the pool")
verified?リーダーを追加- テストが
refute_predicate @connection, :verified?を呼んでも正常に動作
このコミット自体は 8-0-stable に含まれていないため、テストだけバックポートされた結果、verified? が存在しない環境で verified? を要求するテストになっていた、という状態でした。
具体的な修正
テストコードの該当部分を、verified? メソッド呼び出しからインスタンス変数の直接参照に変更しています。
(イメージとしては以下のような 1 行の差分です)
- assert_not_predicate @connection, :verified?
+ assert_not @connection.instance_variable_get(:@verified)※ 実際のコードは adapter_test.rb 内の既存スタイルに合わせて書かれており、すでに次の行で同様に @last_activity を instance_variable_get で読んでいるため、そのパターンに揃えています。
ポイント:
mainブランチではverified?は単なる@verifiedのリーダーであり、ロジックはなく「@verifiedを返すだけ」のメソッド- したがって、このテストを「
verified?の代わりに@verifiedを直接見に行く」ように変えても、テストが検証している条件(@verifiedが false / nil であること)は変わらない - 8-0-stable 側に
verified?メソッドだけをバックポート / 抽出するのではなく、テスト側を微修正して整合を取る方針を採用している
動作確認
PR 説明によると、ローカルで以下を確認済み:
- 修正前:
ARCONN=sqlite3 bin/test test/cases/adapter_test.rb:649が上記と同じ失敗内容で再現
- 修正後:
- 同テストが sqlite3 / mysql2 でパス
AdapterConnectionTest全体(mysql2)も 28 テスト・0 失敗でパス
mainと7-2-stableはそもそもこの問題の影響を受けていない
- 影響範囲・注意点
- 影響範囲:
- 変更ファイルは
activerecord/test/cases/adapter_test.rbの 1 行のみ - 本番コード(ライブラリ本体)ではなくテストコードのみの変更
- 対象は 8-0-stable の ActiveRecord AdapterConnectionTest に限定される
- 変更ファイルは
- 実質的な挙動:
- 接続の検証フラグ
@verifiedの扱い自体は一切変わらない - 既存コードに対する互換性への影響はなし
- CI でのテスト失敗を解消する目的のテスト修正
- 接続の検証フラグ
- 注意点:
- 将来 8-0-stable に 28638775db(もしくは
verified?リーダーだけ)をバックポートする場合、- テストがインスタンス変数直読のままで問題はないが、
- 「API として
verified?を前提にするかどうか」を整理する余地はある
- テストがプライベート実装(インスタンス変数名)に依存しているため、
@verifiedの名前変更や構造変更を行う場合にはテストも同時に追従させる必要がある
- 将来 8-0-stable に 28638775db(もしくは
- 参考情報 (あれば)
- 該当テスト:
activerecord/test/cases/adapter_test.rb内test_a_non-StandardError_interrupt_marks_the_connection_for_re-verification - 関連するコミット / PR:
verified?を導入したコミット:28638775db
"Preconnect connections that are idle in the pool"- テストを 8-0-stable に持ってきたバックポート PR: #57953 (
9241586205) - 本 PR: #58157
- minitest の変更:
- 6.0.4 以降で
refute_predicate/assert_predicateがrespond_to?を事前チェックするようになった変更:minitest/minitest@b12f87f4f8
- 6.0.4 以降で
#58158 Fix RDoc markup in ActiveSupport::ContinuousIntegration [ci skip]
マージ日: 2026/7/19 | 作成者: @Yuhi-Sato
- 概要 (1-2文で)
ActiveSupport::ContinuousIntegration のクラスドキュメント内で、コード(等幅)表現に使われていた RDoc マークアップを修正し、API ドキュメント上で正しく固定幅表示されるようにした PRです。コードや挙動には一切変更はなく、ドキュメント表現のみの修正です。
- 変更内容の詳細
修正対象
対象は activesupport/lib/active_support/continuous_integration.rb 内の RDoc コメントです。
RDoc 6 では、バッククォート(`...`)が「コード表現」として扱われず、スマートクォート(‘…’)に変換されてしまうため、Rails の API ガイドラインに従い、コード(等幅)表現を +...+ 形式に変更しています。
イメージとしては以下のような変更です(実際の内容に近い例):
- # Run `bin/ci` to execute the CI suite locally.
+ # Run +bin/ci+ to execute the CI suite locally.この修正により、以下のようなページでの表示が改善されます:
従来は “bin/ci” などが通常の引用風(スマートクォート付き)に見えてしまっていたものが、今後はきちんと等幅フォントのコードとして表示されます。
- 影響範囲・注意点
- コードロジックや挙動には一切変更がなく、ランタイムの動作には影響しません。
- 影響範囲は ActiveSupport::ContinuousIntegration クラスの API ドキュメント表示のみです。
- RDoc 6 系環境で Rails API ドキュメントを生成している場合にのみ意味を持つ変更で、既存のアプリケーションコードには影響しません。
- CI スキップ指定([ci skip])がタイトルに入っている通り、テストは実行せず、純粋なドキュメント修正として扱われています。
- 参考情報 (あれば)
- Rails ドキュメントガイドライン(コード表現に
+...+を推奨)
https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation - 対象 API ドキュメントページ
https://edgeapi.rubyonrails.org/classes/ActiveSupport/ContinuousIntegration.html
#58036 Fix typos and inconsistencies in documentation
マージ日: 2026/7/18 | 作成者: @55728
- 概要 (1-2文で)
このPRは、Rails本体およびガイド・APIドキュメント・コメントに存在していた typo(スペルミス)や、サンプルコード/説明文の不整合をまとめて修正するものです。実行時の挙動には変更を加えず、ドキュメントとコメントの品質・信頼性を高めています。
- 変更内容の詳細
全体方針
- 対象:
- Guides(Getting Started, Product Reviews など)
- APIドキュメント(RDocコメント)
- コードコメント
- 一部テストの説明文
- 除外:
- CHANGELOG は変更対象外(履歴の正確性維持のため)
- 種類:
- 明確なスペルミスの修正のみ
- 文意レベルの書き換え・言い回し変更等はあえて行っていない
- CI:
- 実行時コードのロジックには手を入れていないため
[ci skip]でテストスキップ
- 実行時コードのロジックには手を入れていないため
Guides / ドキュメントの修正
1) Wishlists guide
- Getting Started Guide へのリンクの後に、閉じられていない括弧があったのを修正。
- 例:
... link to Getting Started Guide (→ 対応する)を追加して文章を正しく構成。
- 例:
2) Product Reviews guide
主に、サンプルと説明の不整合・明らかなtypoの修正です。
db:migrateのサンプル出力とマイグレーションの差異を修正- 問題: マイグレーションでは
default: 0を付けているのに、直後に掲載されているdb:migrateの出力例からdefault: 0が抜けていた。 - 対応: サンプル出力に
default: 0を追加し、上のマイグレーションとの整合性をとった。
- 問題: マイグレーションでは
誤字修正(文法・スペル)
"We need to a way to view"→"We need a way to view""calcuating"→"calculating"- 主語・動詞の一致:
"these hidden fields … also ensures"→"these hidden fields … also ensure""this fields"→"these fields"
コードスニペットの状態不整合
問題: Filtering セクションにある
review.rbの「現在の状態」スニペットから、「rating の数値妥当性バリデーション(numericality validation)」が抜けていた。- これはガイド前後の説明や別のスニペットでは存在しており、ガイド内で矛盾していた。
対応: 該当の
Reviewモデルの例に数値バリデーションを復元して、ガイド全体で一貫した状態にした。
(イメージ)rubyclass Review < ApplicationRecord validates :rating, numericality: { only_integer: true, in: 1..5 } # ... ほかのバリデーションや関連付け ... end
ファイルパスの誤記修正
- 問題: テストのパスの説明文が
test/integrations/reviews_test.rbとなっていたが、実際の generator 出力とコマンドはtest/integration/を使用している。 - 対応: 説明文を
test/integration/reviews_test.rbに修正し、generator出力と一致させた。
- 問題: テストのパスの説明文が
3) その他ガイド
getting_started.md,maintenance_policy.md,sign_up_and_settings.md,upgrading_ruby_on_rails.mdなどで、軽微な typo を修正(例: 単語のスペル・小文字/大文字の誤りなど)。
ライブラリコード内のコメント・RDocの修正
Railtie 関連
- 重複した冠詞の削除:
"the a lazy loaded"→"a lazy-loaded"- 「the a」と二重になっていたのを修正。
- 実コードではなくコメント/説明文の修正。
ProxyLogger (ActiveSupport::ProxyLogger)
- クラスコメント内のスペルミス:
"surpress"→"suppress"
- ログを「抑制する」説明文なので、辞書的に正しいスペルに統一。
ActionText::Attachment #to_plain_text の RDoc 例
- 問題:
RDoc のサンプルで定義されている
attachable_plain_text_representationが引数なし:rubydef attachable_plain_text_representation # ... end一方、同じサンプル内での呼び出しは
attachable_plain_text_representation(caption)のように引数付きで呼んでいて、これをコピペしたユーザはArgumentError(引数の数の不一致)に遭遇する。
- 対応:
RDoc のメソッド定義例を引数付きに変更し、ランタイムで実際に行われる呼び出しと一致させた:
rubydef attachable_plain_text_representation(caption) # ... endこれは
#to_markdownのオーバーライド例と同様のインタフェースになるようにそろえた、という説明がなされている。
「スペルミス一掃 (Spelling sweep)」
codespell を使って、ガイド/APIドキュメント/コメント全体にわたる綴りの誤りを一括で修正しています。
具体例(before → after):
charcacters→charactersbacklash(char) →backslashOauth→OAuthIlliad→Iliadderivated→derivedstriped(indentation の意味で) →strippedunverfied→unverifiedavise→advisetheses→theseintside→inside
使用コマンド:
shcodespell --builtin clear,rare,code --skip='*/CHANGELOG.md'--builtin clear,rare,codeにより、一般的なミス・あまり使われない誤綴り・コード中想定の誤綴りなどを検出。- CHANGELOG は
--skipで除外。
ポリシー:
- 動作に関係する識別子や固有名詞であっても、客観的に誤りと判定できるもの(例:
Oauth→OAuth,Illiad→Iliad)は修正。 - 文体・語順など主観が入りうる変更は意図的に避け、「辞書上誤りと分かる箇所」に限定。
- 動作に関係する識別子や固有名詞であっても、客観的に誤りと判定できるもの(例:
- 影響範囲・注意点
実行時挙動:
- ランタイムのコードロジックには一切変更なし。
- 変更対象はコメント、ドキュメント、テスト説明文、およびドキュメント用のサンプルコードのみ。
開発者への影響:
- ガイドや RDoc を参考にする際、より正確な文章・サンプルコードになっている。
- とくに
ActionText::Attachment#to_plain_textの RDocをコピペして使っていた/これから使う場合、引数の不一致によるArgumentErrorが起こりにくくなる。 - Product Reviews ガイドのマイグレーション出力例とテストパスの修正により、チュートリアルどおりに手を動かした際に「自分だけ挙動が違う」といった混乱が減る。
互換性:
- 公開APIのシグネチャや挙動に変更はないため、アップグレード時の互換性リスクはゼロに近いです。
- CI設定・設定ファイルなども未変更。
- 参考情報 (あれば)
このPR自体は、既存の typo cleanup の継続として位置づけられており、過去の類似PR:
- (説明中の文言として)
This continues the typo cleanup in #58036 and folds in #58127.
→ タイポ修正作業を段階的に進めている一環。
- (説明中の文言として)
使用ツール:
実務的には、RailsガイドやRDocをチーム内の教育・ドキュメントとして利用している場合、このPRの反映後のバージョンを前提にサンプルコードを参照すると、混乱やコピペミスが減らせます。
#58154 Pin json below 2.20 in the Gemfile
マージ日: 2026/7/18 | 作成者: @yahonda
- 概要 (1–2文で)
Rails 7.2 系ブランチの開発・CI環境において、jsongem 2.20.0 以降で導入された「コメント付き JSON の非推奨警告」が strict warnings CI を落としてしまう問題に対応するため、jsonを< 2.20にピン留めした PR です。アプリケーション側の依存バージョンには影響せず、Rails 本体の開発・テスト環境専用の制約です。
- 変更内容の詳細
変更点
Gemfile:
# 変更前(イメージ)
gem "json"
# 変更後
gem "json", "< 2.20"それに伴い Gemfile.lock の依存情報も更新されています(json の実際の固定バージョンは lock ファイル側で管理される)。
背景となる技術的事情
json2.20.0 での変更- JSON ドキュメント内のコメント(
//や/* ... */など)のサポートが非推奨になり、コメントを含む JSON をパースすると deprecation warning が出るようになった。 - Rails 7.2 ステーブルブランチの CI では「警告をエラー扱い(strict warnings)」にしているため、この deprecation warning によって CI が落ちる。
- JSON ドキュメント内のコメント(
main ブランチでの対応 (#57832)
JSON.loadにallow_comments: trueを渡すようにし、json2.20.0 の警告を抑制する対応が行われた。- これを 7-2-stable にも一度 backport したが、7.2 系では
ActiveSupport::JSON.decodeが引数 1 つしか受け取らない実装のため、allow_comments:オプション付きで_load経由で呼ばれるコードがArgumentErrorを起こし、JSON メッセージシリアライザが壊れることが判明しリバートされた。
メンテナンスフェーズの制約
- 公式のメンテナンスポリシー上、7.2.x はすでにバグフィックスフェーズを終了しており、セキュリティ修正のみ許容される状態。
- そのため、7.2 系に大きめの挙動変更(
ActiveSupport::JSON周辺のシグネチャ変更など)を持ち込むのは避けたい。 - 結果として、gem のバージョンを Gemfile でピン留めすることで CI 問題のみを解消する、という最小限の対処になっている。
サンプルイメージ(概念的なコード)
main ブランチ側で行われた対応はざっくりいうと:
JSON.load(json_string, allow_comments: true)のようにフラグを渡していましたが、7.2 ではこのようなオプションを安全にパススルーできる API になっておらず、その backport は破壊的になってしまうため採用できませんでした。その代わりに「問題のある json バージョン自体を使わない」というアプローチを取っています。
- 影響範囲・注意点
影響範囲
- 対象: Rails 7-2-stable ブランチの Rails 本体リポジトリ の開発・CI環境のみ。
- Rails を利用するアプリケーション側の Gemfile にはこの制約は伝播しない。アプリは引き続き
json2.20.0 以降を使う構成も可能。 - CI 上で
json2.20.0 以降をインストールしようとしていたケースでも、< 2.20制約により 2.19.x 以下が選ばれ、deprecation warning による CI 失敗が防がれる。
注意点 / 開発者視点での留意事項
- Rails 7.2 のバグ修正サポートは終了しているため、「
json2.20.0 以降でのコメント非推奨挙動に 7.2 が公式対応する」ことは今後も期待しづらい。 - Rails 7.2 を使うアプリ側で
json2.20.0 以降を利用していて、コメント付き JSON を扱っている場合は、アプリケーション自身で- コメントを含まない形式への移行
- もしくは
JSON.load(..., allow_comments: true)のような対応(※アプリ側のコードで可能な範囲で) を検討する必要がある。
- Rails 本体の開発に参加する場合、7.2 ブランチで
bundle update jsonなどを実施すると、この< 2.20制約があることに注意する必要がある。
- Rails 7.2 のバグ修正サポートは終了しているため、「
- 参考情報 (あれば)
- Rails メンテナンスポリシー:
https://rubyonrails.org/maintenance - 関連 PR:
- main ブランチでの対応: #57832
- 同内容の 8-0-stable 向け PR: #58153
- json gem の 2.20.0 におけるコメント非推奨化については、
jsongem の CHANGELOG / リリースノートを参照すると詳細な仕様変更が確認できます。
#58153 Pin json below 2.20 in the Gemfile
マージ日: 2026/7/18 | 作成者: @yahonda
- 概要 (1-2文で)
Rails 8.0 系ブランチで、jsongem を 2.20 未満に固定する変更です。json 2.20.0でコメント付き JSON に対する非推奨警告が出て CI の strict warnings を落としてしまう問題を、Gemfile 側でバージョンをピン止めすることで回避しています。
- 変更内容の詳細
背景となる問題
json 2.20.0から、JSON ドキュメント内のコメントが非推奨となり、警告が表示されるようになった。- Rails の CI(特に strict warnings モード)では、この非推奨警告が出るとテストが失敗する。
- main ブランチでは PR #57832 によって、
allow_comments: trueを渡す形でこの問題に対応済み。- しかし 8.0 系では
ActiveSupport::JSON.decodeが引数 1 つしか取らないため、そのまま_loadにallow_comments:を渡すバックポートはArgumentErrorを引き起こし、JSON メッセージシリアライザを壊してしまうことが判明し、一度マージ済みの修正がリバートされている。
- しかし 8.0 系では
加えて、Rails 公式のメンテナンスポリシー上、8.0.x はすでにバグフィックスサポートが終了し、セキュリティフィックスのみのフェーズに入っているため、8.0 ブランチに対して大きめの挙動変更を行うのは避けたい、という前提がある。
実際の変更内容
Gemfile の json の指定を以下のように変更しています(イメージ):
# 変更前(例)
gem "json"
# 変更後(例)
gem "json", "< 2.20"それに合わせて Gemfile.lock も更新され、8.0 ブランチでの開発および CI においては json 2.20.0 以上を使わないように制約を追加しています。
コードレベルのロジック変更(ActiveSupport::JSON.decode など)の修正は行わず、純粋に依存バージョン制約だけで対処している点がポイントです。
- 影響範囲・注意点
- 影響範囲
- Rails 本体の 開発環境 および CI に限定される変更です。
- Rails 8.0.x を利用している アプリケーション側の Gemfile / Gemfile.lock には一切制約を追加しません。
- つまり、アプリケーションは引き続き
json 2.20.0以降を含めて自由にバージョン指定可能です。
- つまり、アプリケーションは引き続き
- なぜアプリは影響を受けないのか
- Rails リポジトリ内の Gemfile は「Railsを開発・テストするための依存関係」を管理するものであり、Rails を利用するアプリケーションの Gemfile とは別物のためです。
- 開発者としての注意点
- Rails 8.0 ブランチで CI を走らせる際、
jsonのバージョンは< 2.20に固定されます。json 2.20の挙動(コメント非推奨など)を 8.0 ブランチの CI 上でテストしたい場合は、そのままではできません。 - 将来的に 8.0 系をフォークして独自にメンテナンスする場合、
jsonの仕様変更(コメント非推奨)に対応したいなら:ActiveSupport::JSON.decodeの API 変更(2引数化など)を含む修正を入れる、- または JSON 側のバージョンピンを外した上で警告を抑制する別の方法を検討する、 などの判断が必要になります。
- Rails 8.0 ブランチで CI を走らせる際、
- 参考情報 (あれば)
- Rails メンテナンスポリシー: https://rubyonrails.org/maintenance
- この PR でも言及されている通り、8.0.x は bug fix support が終了し、security fixes のみのフェーズ。
- 関連 PR:
- main で
allow_comments: trueを導入した PR: #57832 - 8.0-stable に一度バックポートされて revert されたコミット:
- 追加:
eb5a0633495b70e4484bea0e1562081d1de72de6 - Revert:
fdc6c1c9c1eab99b41b6a53c7bd8cdedbe36c9f0
- 追加:
- main で
#58146 Test that config.action_dispatch.default_headers set in an initializer is applied
マージ日: 2026/7/18 | 作成者: @nurey
- 概要 (1-2文で)
Rails 8.x 系で「initializer から設定したconfig.action_dispatch.default_headersがレスポンスに反映されない」というリグレッションに対し、その再発を防ぐためのテストが追加されています。既に main ブランチでは修正済みですが、その挙動を保証する回帰テストを追加する PR です。
- 変更内容の詳細
何をテストしているか
追加されたテストは、
config/initializers/*.rbからconfig.action_dispatch.default_headersを ハッシュごと置き換えた 場合でも、その設定が実際の HTTP レスポンスに適用されるか
を検証しています。
背景として、既存のテストはどれも以下のように「直接クラスに設定」していました:
ActionDispatch::Response.default_headers = { ... }これは Rails アプリのブートや Railtie、initializer の実行タイミングを一切通らないため、「設定がいつ・どの順番で読み込まれるか」という問題を検出できていませんでした。
今回のテストでは、実際にアプリをブートし、initializer から設定するパスを通して確認しています。
具体的なポイント
説明文から読み取れるテスト内容の要点は以下です。
- 対象ファイル:
railties/test/application/configuration_test.rb - 新しいテスト名:
test_config.action_dispatch.default_headers_can_be_set_in_an_initializer_and_is_applied_to_responses
テストのシナリオは概ね次のような流れになります(擬似コードレベル):
# アプリ定義の中で、config/initializers 内にファイルを作るテスト支援コードを使うイメージ
add_to_config <<-RUBY
# config/initializers/default_headers.rb 相当のコード
Rails.application.config.action_dispatch.default_headers = {
"X-Custom-Header" => "from_initializer"
}
RUBY
app("development")
# 実際に Rack 経由でリクエストを投げる
get "/"
# レスポンスヘッダに initializer で設定したヘッダが入っていることを確認
assert_equal "from_initializer", last_response.headers["X-Custom-Header"]ここで重要なのは、ヘッダの設定を
config.action_dispatch.default_headers["X-Custom-Header"] = "..."
# ではなく
config.action_dispatch.default_headers = { "X-Custom-Header" => "..." }のように「ハッシュ全体を置き換えている」点です。
なぜ「ハッシュ全体の置き換え」をテストするのか
問題になっていたケースは、ActionDispatch::Response が config/initializers より先にロードされてしまう状況でした。
- Rails 起動時に
ActionDispatch::Responseがロードされると、その時点のdefault_headersオブジェクト(ハッシュ)がフック(load hook)としてキャプチャされる - その後 initializer で 別のハッシュを代入すると、
ActionDispatch::Response側が保持しているのは「古いハッシュ」なので、新しく代入したヘッダがレスポンスに反映されない
一方で、以下のような「インプレース変更」は、オブジェクト自体は同じなのでたまたま動いてしまいます:
# これは「同じハッシュオブジェクト」に対する変更
config.action_dispatch.default_headers["X-Custom-Header"] = "foo"このため、実際にバグっていたのは「ハッシュごと置き換える」パターンであり、そのパターンをテストでカバーする必要があるというのがこの PR の趣旨です。
説明文にも明記されています:
The test replaces the headers hash wholesale in an initializer (rather than mutating it in place), because that is the case that actually breaks when
ActionDispatch::Responseis loaded beforeconfig/initializersrun — in-place mutation happens to survive since it mutates the same object the load hook captured.
- 影響範囲・注意点
- これは テストのみを追加する PR であり、ランタイムのコード変更はありません。
- 実際の影響は「8.x のリリースブランチにバグ修正をバックポートする際に、このテストがフェイルするかどうかで確認できる」点にあります。
- 既存の修正 (コミット
f9c82c3f2a) が 本当にリグレッションを直しているかを保証する役割を持ちます。- 説明によると、このコミットを一時的に revert するとテストは失敗することを確認済みです。
- アプリ開発者目線では:
- Rails 8.x で「initializer で
config.action_dispatch.default_headers = { ... }とまるごと代入したのに反映されない」という症状に遭遇していた場合、- main / 修正版 8.x では直っている
- その挙動はこのテストで今後も保証される
という状態になります。
- Rails 8.x で「initializer で
- 逆にいうと、今後
ActionDispatch::Responseのロードタイミングや default_headers の扱いを変える変更を行うと、このテストが落ちる可能性があるため、そのような変更をする際のセーフティネットになります。
- 参考情報 (あれば)
この PR がカバーするリグレッション: #58145
関連する過去の Issue/PR: #32592
挙動を保証している修正コミット:
f9c82c3f2a(load hook の実行タイミングの調整)対象テストの実行コマンド(PR 説明より):
bashcd railties bin/test test/application/configuration_test.rb \ -i "test_config.action_dispatch.default_headers_can_be_set_in_an_initializer_and_is_applied_to_responses"
アプリ側でのベストプラクティスとして、Rails 8.x 以降でも安心して:
# config/initializers/default_headers.rb
Rails.application.config.action_dispatch.default_headers = {
"X-Frame-Options" => "SAMEORIGIN",
"X-Custom" => "foo"
}のような「ハッシュごと置き換え」を使えるようになっており、その保証としてこのテストが追加された、という位置付けです。
#58148 Fix test_precompile_shouldn't_use_the_digests_present_in_manifest.json failure
マージ日: 2026/7/17 | 作成者: @yahonda
- 概要 (1–2文で)
Rails の Sprockets 資産プリコンパイルに関するテストが CI 上で時々失敗していた問題を、テスト側で 1 秒スリープを挟むことでフレークを解消した PR です。Sprockets がファイルの更新検知を「秒単位の mtime」と「キャッシュされたダイジェスト」に依存している仕様にテストを合わせています。
- 変更内容の詳細
対象テスト:ApplicationTests::SprocketsAssetsTest#test_precompile_shouldn't_use_the_digests_present_in_manifest.json
このテストは、次のようなシナリオを検証しています:
- 1回目の
assets:precompile実行後にapp/assets/images/rails.pngを書き換える - 2回目の
assets:precompileを実行したとき、既存のmanifest.json内のダイジェスト値に依存せず、新しいダイジェストが生成されることを確認する - 具体的には、
application.cssのダイジェスト付きファイル名が 1 回目と 2 回目で異なることを期待する
しかし、Sprockets 側の実装では:
- ファイルのダイジェスト計算結果を、ファイルの mtime(秒精度で丸め)をキーにキャッシュしている
- 実装箇所:
Sprockets::Base#file_digest - コミットコメントにも「精度はすでに 1 秒にハードコードされている」と明記されている
- 実装箇所:
- 同じ秒の間にファイルを書き換えると、mtime が変わらない → 既存キャッシュされたダイジェストが再利用される
もともとのテストは:
- 1回目のプリコンパイル
app/assets/images/rails.pngを上書き- 2回目のプリコンパイル
の 1–3 を非常に短時間で連続実行しており、高速なマシンだと 2. の書き換えが 1. と「同じ秒」の中に行われてしまうことがありました。
その場合、2回目のプリコンパイルでも Sprockets が古いダイジェストをキャッシュから返し続けるため、
Expected "application-xxxx.css"
to not be equal to "application-xxxx.css".という形で、「異なっていてほしいダイジェスト付きファイル名が同一になってしまう」フレークが発生していました。
この PR で行われた変更は非常に小さく、対象ファイルは 1 つ・追加は 1 行のみです:
railties/test/application/sprockets_assets_test.rbapp/assets/images/rails.pngを書き換える前にsleep 1を挟む行を追加
イメージ的には以下のような変更です(実際のコードとはメソッド名や細部が異なる可能性がありますが、意図を伝えるための擬似コードです):
def test_precompile_shouldnt_use_manifest_digests
# 1回目のプリコンパイル
precompile_assets
# ここで待たないと、同じ秒の中で書き換えが完了してしまうことがある
sleep 1
# 画像を書き換えて asset を変更
File.write("app/assets/images/rails.png", "new content")
# 2回目のプリコンパイル
precompile_assets
# application.css のダイジェスト付きファイル名が変わっていることを確認
assert_not_equal first_digest, second_digest
endこの 1 秒スリープにより、必ず「書き換え前」と「書き換え後」で mtime の秒が変わるようになり、Sprockets のキャッシュキーも変わるため、新しいダイジェストが確実に計算されます。
著者は、Ubuntu 26.04 / Ryzen 9 7940HS / Ruby 4.0.6 環境で 100 回ずつテストを実行し、以下の再現率を示しています:
- main ブランチ + sleep なし: 100 回中 47 回失敗
- main ブランチ + sleep あり: 100 回中 0 回失敗
- 影響範囲・注意点
- 実際のアプリケーションコードや Sprockets 本体の挙動には一切変更がなく、影響範囲はテストのみです。
- CI で sporadic に落ちていた
test_precompile_shouldn't_use_the_digests_present_in_manifest.jsonが安定することが期待できます。 - 根本にある仕様は「Sprockets は 1 秒単位の mtime を前提にキャッシュしている」というものであり、これは長年の設計として受け入れられているため、この PR は仕様にテストを合わせる対応です。
- 実運用上、「2 回の
assets:precompileの間に同じファイルを 1 秒未満の間隔で書き換える」というケースは現実的ではなく、テストが現実よりも「速すぎる」ことによる問題を吸収しています。
- 実運用上、「2 回の
- 将来、Sprockets 側で mtime 精度をナノ秒レベルなどに引き上げるような大きな変更が入った場合、この sleep は理論上不要になりますが、現時点では仕様に対する合理的なワークアラウンドと言えます。
- 参考情報 (あれば)
該当テストが落ちた CI ビルド例:
- 2026-07-02 railties (4.0) [rack-3-0]
https://buildkite.com/rails/rails/builds/130521#019f24ea-6805-4b97-96a2-f774754cf29e - 2026-07-16 railties [rack-2] (nightly)
https://buildkite.com/rails/rails-nightly/builds/4552#019f6cba-e4be-4732-b980-bacd627dfc21 - 2026-07-17 railties (3.3)
https://buildkite.com/rails/rails/builds/131149#019f7040-9396-4972-99b7-06deec0012db
- 2026-07-02 railties (4.0) [rack-3-0]
関連 Issue:
- Fixes #50364
Sprockets の該当設計 (参考となるコミットメッセージ):
Sprockets::Base#file_digestで mtime によるキャッシュを実装- 「fidelity is already hard coded to 1 second in lots of other spots」と記載されているコミット:
- rails/sprockets@55c3b563
- rails/sprockets@2fefbf4e
#58139 Harden load schema migrations
マージ日: 2026/7/17 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Schema.load_schema_migrationsが、実プロジェクトで起こりがちな2つのケース(CRLF 行末や末尾改行なし、重複バージョン)でも安全かつ一貫して動作するように強化された PR です。これにより、db/schema.rbのフォーマット差異や人手によるマージミスがあっても、スキーママイグレーションのロードがより堅牢になります。
- 変更内容の詳細
背景となる問題
この PR は #58134 で導入された ActiveRecord::Schema.load_schema_migrations(schema_path_or_file) に対するフォローアップで、以下2つの問題を解決しています。
(1) CRLF 行末/末尾改行なしの場合に __END__ を認識できない
db/schema.rb には次のような形で「マイグレーションバージョン一覧」を __END__ 以降に書く仕様になっています(#58134 による新機能):
ActiveRecord::Schema.define(version: 20260101000001) do
# ...
end
ActiveRecord::Schema.load_schema_migrations(__FILE__)
__END__
20260101000001
20260102000000Ruby 自体は:
- CRLF (
\r\n) の行末でも__END__を正しく認識する __END__がファイル末尾の行で、さらにその行に末尾改行がなくても認識する
にもかかわらず、load_schema_migrations 側の実装が「Ruby の行扱い」とずれていたため、以下のようなケースでエラーになっていました:
db/schema.rbが CRLF 行末でコミットされている__END__行が最後で、最後の行に改行がない
具体的な症状:
ActiveRecord::ActiveRecordError: No __END__ found in db/schema.rbRuby としては問題なく db/schema.rb を実行できているのに、load_schema_migrations だけが __END__ 不在と誤判定していた、という状態です。
対応内容:
この PR では、「Ruby が __END__ を認識するのと同じ行セマンティクスで __END__ を探す」ように実装を変更しています。
結果として:
- CRLF 行末の
db/schema.rb __END__がファイル末尾にあり、末尾改行がないdb/schema.rb
のどちらも、ActiveRecord::Schema.load_schema_migrations で正常に __END__ と以降のバージョンリストが読み込まれるようになりました。
なお、__END__ 自体が存在しないファイルは、以前と同じく ActiveRecord::ActiveRecordError: No __END__ found... を投げます(挙動変更なし)。
(2) バージョンリストに重複があると新規 DB ロードが失敗する
__END__ 以降のマイグレーションバージョン一覧に、同じバージョンが重複しているケース(典型的には手動マージ時のミス)があると、新規 DB に対する db:schema:load が途中で失敗していました。
典型的な壊れた例:
__END__
20260101000001
20260101000001 # 重複この状態で「まだ何もマイグレーションが入っていない DB」にロードすると、schema_migrations の version カラムに UNIQUE 制約があるため、次のようなアダプタレベルの例外になります:
ActiveRecord::RecordNotUnique: SQLite3::ConstraintException: UNIQUE constraint failed: schema_migrations.version一方で、「既にそのバージョンが schema_migrations に入っている状態」で同じ db/schema.rb をロードすると、INSERT がスキップされて「黙って通る」ため、
- 「新規 DB に対するロード」と
- 「既存 DB に対するインクリメンタルロード」
とで挙動が食い違う、という一貫性のない状態でした。
対応内容:
__END__ 以降から読み込んだバージョンリストを、schema_migrations に INSERT する前に 重複排除(dedup) するように変更しています。
ポイント:
schema_migrationsテーブル自体が「集合(set)」として振る舞うべきものなので、重複バージョンには意味がない- 事前に
uniqしてから INSERT することで、- 新規 DB へのロード
- 既存 DB への追加ロード のどちらでも同じ結果になる(=一貫した挙動)
PR の説明文にもある通り、「重複を黙って捨てる」のではなく「重複があればエラーにして該当バージョンを表示する」という方針に変更する余地は残されていますが、現時点では「静かに dedup」する実装になっています。
テスト追加
activerecord/test/cases/active_record_schema_test.rb に、以下のような観点のテストが追加されています:
- CRLF 行末の
db/schema.rbでもload_schema_migrationsが__END__を見つけてロードできること __END__が末尾行かつ末尾改行なしでもロードできること- バージョンリストに重複があっても、新規 DB ロードが UNIQUE 制約違反にならないこと(正しく dedup されること)
- 影響範囲・注意点
- 対象バージョン:
- この機能(
load_schema_migrations自体)がまだ未リリースのため、リリース済みの Rails には直接の影響はありません。将来この機能を含むバージョンを利用するときに効いてくる修正です。
- この機能(
- 想定ユースケース:
db/schema.rbにActiveRecord::Schema.load_schema_migrations(__FILE__)と__END__以降のバージョン一覧を埋め込む新方式を採用しているプロジェクト
- 注意点:
- これまで「重複バージョンがあると新規 DB ロードはコケる」ことで潜在的なマージミスに気付けていたケースは、この PR により「静かに dedup される」ようになります。
- とはいえ、
schema_migrationsは set として扱うべきであり、かつ「新規 vs 既存」の挙動差をなくすという意味で、今回の挙動の方が合理的です。 - プロジェクトとして「重複を絶対に許容したくない」場合は、CI などで
__END__以降のバージョンリストを解析して重複チェックするカスタム検査を足すのが安全です。
- とはいえ、
- CRLF/末尾改行なし周りは挙動が寛容になっただけなので、既存の正常ケースが壊れる心配はほぼありません。
- これまで「重複バージョンがあると新規 DB ロードはコケる」ことで潜在的なマージミスに気付けていたケースは、この PR により「静かに dedup される」ようになります。
- 参考情報 (あれば)
- 元となった機能追加 PR: #58134
ActiveRecord::Schema.load_schema_migrationsによって、schema.rbからschema_migrationsを再構成できるようにする試み。 - この PR の意図:
- 実運用でよくあるファイル形状(CRLF、末尾改行なし、マージミスによる重複行)に対しても壊れにくくし、「
schema.rbから安全に schema_migrations を復元する」という新機能の堅牢性を高めることにあります。
- 実運用でよくあるファイル形状(CRLF、末尾改行なし、マージミスによる重複行)に対しても壊れにくくし、「
#58113 Make AbstractController::Base#action_methods ractor safe
マージ日: 2026/7/17 | 作成者: @Edouard-chin
- 概要 (1-2文で)
AbstractController::Base#action_methodsが返すSetを凍結(freeze)し、Ractor(Rubyの並行実行モデル)で参照してもRactor::IsolationErrorが出ないようにした変更です。これに伴い、ActionMailerなどaction_methodsをオーバーライドしている箇所も、Ractorセーフになるよう調整されています。
- 変更内容の詳細
背景となる問題
AbstractController::Base#action_methodsは「アクションとして扱うメソッド名の集合」をSetで返します。- 返される
Setが mutable(変更可能) なオブジェクトのままだと、Ractor間で共有・参照しようとした際に Ruby の Ractor のルールに反してRactor::IsolationErrorが発生します。- Ractorでは「共有するオブジェクトは原則 immutable であること」が要求されるためです。
コアな変更点
1) AbstractController::Base#action_methods の戻り値を freeze
actionpack/lib/abstract_controller/base.rb の変更により、action_methods が返す Set が凍結されるようになりました(Set インスタンスに対して freeze を呼ぶだけのシンプルな変更)。
イメージとしては、以下のような形です(実際のコードとは多少異なる可能性がありますが意図は同じです):
def action_methods
@action_methods ||= begin
# 公開アクション用のメソッド名を集めて Set にする処理
methods = # ...
methods.to_set.freeze
end
endこれにより、controller.action_methods を Ractor の中で参照しても、Set が immutable なので Ractor 間で安全に共有できます。
2) ActionMailer 側の action_methods オーバーライド対応
ActionMailer は action_methods をオーバーライドして独自の「メール送信アクション」の集合を返していますが、こちらも Ractor セーフになるように修正されています。
具体的には:
actionmailer/lib/action_mailer/base.rbで、superから返ってくるaction_methods(既に freeze 済)に対して、Mailer 特有のロジックを噛ませた上で、最終的な戻り値も freeze されるように整備。- 既存コードが
action_methodsの戻り値を破壊的に変更(add,merge!,deleteなど)しているケースがあれば、今後は動かなくなるため、そのような前提が無い形に合わせている/テストで保証している、という位置づけです。
3) AbstractController::UrlFor 周りの微調整
actionpack/lib/abstract_controller/url_for.rb でも action_methods を利用しているため、freeze された Set を前提としたコードに軽微な修正が入っています。
ここでは主に「action_methods を操作しない(変更しない)利用の仕方」で動くように、あるいは前提を明示するための変更です。
4) テスト追加
actionpack/test/controller/base_test.rb にテストが追加されています。内容としてはおおよそ:
action_methodsがSetを返すこと- その
Setがfrozen?であること - (間接的に)アクション解決やルーティングが従来どおり動作すること
を確認するテストになっていると考えられます。
- 影響範囲・注意点
想定される影響範囲
- すべての
AbstractController::Baseを継承するクラス(ActionController::Base,ActionMailer::Baseなど)の#action_methods戻り値が 凍結された Set になります。 - これにより、Rails アプリケーションコードで以下のようなことをしている場合は壊れます:
class ApplicationController < ActionController::Base
def some_setup
# NG: これからは frozen Set なので例外が出る
self.class.action_methods.add("new_action")
end
endあるいは
class MyController < ActionController::Base
def self.action_methods
methods = super
# NG: 破壊的変更
methods.delete("index")
methods
end
endこのようなコードは、今後 FrozenError(旧 RuntimeError: can't modify frozen ...)を発生する可能性があります。
推奨される対応・書き方
action_methodsを 読み取り専用 として扱うことが前提になります。- フィルタリングや追加などのカスタマイズが必要な場合は、新しい Set を生成する ように書き換える必要があります。
例:
class MyController < ActionController::Base
def self.action_methods
super - %w[index] # 新しい Set(あるいは Enumerable)を返す
end
endあるいは
class MyController < ActionController::Base
def self.action_methods
super.to_set.merge(%w[extra_action]).freeze
end
end- Ractor を使わないアプリでも、この freeze による挙動変更の影響は受けるため、
action_methodsを破壊的に操作している独自コードがないか確認する価値があります。
Ractor 利用時のメリット
- コントローラやメイラーを Ractor 内でインスタンス化・利用する際、
action_methodsの参照でRactor::IsolationErrorが出なくなります。 - 将来的に Rails アプリを Ractor ベースで並列化する際の基盤整備の一環と言えます。
- 参考情報 (あれば)
- 変更対象クラス
AbstractController::Base#action_methodsActionMailer::Base#action_methods(オーバーライド)AbstractController::UrlFor内でのaction_methods利用箇所
- 関連する Ruby の仕様
- Ractor は「共有オブジェクトは immutable(freeze 済み)か、Ractor ローカル」であることを要求する。
- Rails 内部のキャッシュ済みオブジェクト(
Set,Hashなど)の freeze は、Ractor 対応で今後も増えていく可能性が高いです。
- CHANGELOG は今回の PR では更新されておらず、「仕様変更というよりはバグ修正(Ractor 非対応だった)」という位置づけに近いと考えられます。
#58134 Support dumping schema_migrations in db/schema.rb
マージ日: 2026/7/17 | 作成者: @fxn
- 概要 (1-2文で)
db/schema.rbにschema_migrationsの中身(適用済みマイグレーションのバージョン一覧)をダンプできるオプションが追加されました。これにより、スキーマファイルだけで DB のマイグレーション状態をより正確に再現でき、かつマイグレーションバージョンに起因するマージコンフリクトを緩和できるようになります。
- 変更内容の詳細
新機能の概要
- 新しいフラグにより、
:rubyフォーマットのスキーマ (db/schema.rb) にschema_migrationsの内容をダンプできるようになりました。 - デフォルトは 無効(従来通り)で、設定を有効にすると
db:schema:dump時に以下のような形式になります。
ActiveRecord::Schema[8.2].define do
...
end
ActiveRecord::Schema.load_schema_migrations(__FILE__)
__END__
20260716101900
20260716112003
20260716130752
...ポイント:
ActiveRecord::Schema.defineからversion:引数がなくなる- 例:
ActiveRecord::Schema[8.1].define(version: 2026_07_16_101907) do→ActiveRecord::Schema[8.2].define do
- 例:
schema_migrationsのバージョンは__END__以降に単純なテキストとして列挙される。ActiveRecord::Schema.load_schema_migrations(__FILE__)が__END__以降を読み取り、DB のschema_migrationsテーブルを埋める。
なぜ必要か (問題の整理)
Issue 1: スキーマだけでは状態が不完全だった
従来:
ActiveRecord::Schema[8.1].define(version: 2026_07_16_101907) do
...
endversion:で示しているのは「最後に適用されたマイグレーションのタイムスタンプ」だけ。schema:load時には、「スキーマに記録された version より古いマイグレーションはすべて適用済みとみなしてschema_migrationsに INSERT する」 という仮定で補完していた。
問題になるケースの例:
- ブランチ A にローカルマイグレーション
2026_07_16_101900があるが、まだ実行していない。 mainをマージすると、main側で新しいマイグレーション2026_07_16_101907が入っており、schema.rb のversionがそれに更新される。db:reset/db:schema:loadをすると、2026_07_16_101900は version より古いので「適用済み」とみなされschema_migrationsに登録される。- しかし実際のスキーマにはそのマイグレーションの変更は含まれていない(スキーマは
mainの定義だけを反映しているから)。 - その状態で
bin/rails db:migrateを叩いても、そのマイグレーションは「適用済み」と判断されて何も実行されない。
結果として、「スキーマと schema_migrations の内容が食い違う(論理的に矛盾した)DB状態」が生まれ得る。
この PR では:
schema.rb自体にschema_migrationsの中身をダンプすることで、schema:load時に「version より古いものを推測して INSERT」する必要がなくなる。- つまり、スキーマファイルだけで DB の論理状態(どのマイグレーションが適用済みか)を完全に復元できる。
Issue 2: スキーマバージョンによるマージコンフリクト
従来は define(version: xxxx) によって単一の version を 1 箇所に書いていたため、複数ブランチでマイグレーション追加 → schema.rb 更新 → その 1 行で高確率にコンフリクト、という状態になりやすかった。
この PR では:
version:引数そのものを生成しなくなる(単一行のコンフリクト要因を削除)。schema_migrationsのダンプ順をカスタマイズ可能にして、マージコンフリクトをさらに起きにくくできる。
ダンプ順序のカスタマイズ (dump_schema_migrations_sort_by)
- デフォルトでは 文字列の辞書順 で並ぶ。
- 設定で並び順を自由に変えられる:
# 例1: 文字列を reverse して比較する(要は末尾優先のソート)
config.active_record.dump_schema_migrations_sort_by = ->(version) { version.reverse }
# 同じことをシンボルの to_proc 経由で
config.active_record.dump_schema_migrations_sort_by = :reverseさらに、MD5 などで「ランダム風に散らす」ことも可能:
require "digest/md5"
config.active_record.dump_schema_migrations_sort_by = ->(version) { Digest::MD5.hexdigest(version) }- これにより、複数の開発者が別々のマイグレーションを追加しても、差分が特定の末尾行に集中せず、マージコンフリクトの確率が下がる。
※ :sql スキーマダンプでは既に SQL ジェネレータをカスタム実装する形で似た問題に対処しているが、:ruby ではブロック(proc)を渡すだけで済む。
ダンプの場所と形式
db/schema.rbの Ruby DSL 本体の後に:
ActiveRecord::Schema.load_schema_migrations(__FILE__)
__END__
20260716101900
...という形でプレーンテキストのリストが付く。
採用理由:
- DSL(Ruby コード)とデータ(マイグレーションバージョン列挙)を視覚的に分離できる。
- エディタのハイライトでも区別され、読むときに邪魔になりにくい。
- diff が最小限(インデントなし・配列リテラルなし)で済む。
- Ruby の
__END__以降は通常の実行に影響しないため、load_schema_migrationsが明示的に読み取る箇所をコントロールできる。
その他仕様の細部
db/migrateに存在しないマイグレーションバージョンはダンプされない
→ すでに削除されたマイグレーションはschema.rbの__END__部分にも出てこない。- フラグは DB単位でオーバーライド可能
→ マルチ DB 環境でも、チームごと・DB ごとに段階的に導入できる。 - 一方で
dump_schema_migrations_sort_byの proc は DB ごとの設定には対応していない。- YAML (
database.yml) 経由でシンボルを渡すケースなどを考えると、契約(受け入れ可能な値の型)が複雑化するため、グローバル設定のままとされている。
- YAML (
schema_migrationsのロードは冪等(同じschema.rbを何度ロードしても状態が壊れない)。
- 影響範囲・注意点
既存プロジェクトへの影響
- デフォルトでフラグは false のため、既存プロジェクトは即座には挙動変更されません。
- ただし Rails バージョンアップに伴い、
ActiveRecord::Schema[8.2].define doのようにversion:なしの形が新デフォルトになる可能性があります(schema_migrationsダンプを有効にしない場合でも)。 - この PR による主な挙動変化は、フラグを有効 (
true) にしてdb:schema:dump/db:schema:loadを利用した場合です。
有効化時の注意
schema.rbの__END__以降のリストが 単一の真実 になります。
→ その内容と DB のschema_migrationsが一致していることを前提にschema:loadされるため、schema.rbを手で編集するのは推奨されません(従来以上に危険)。- 既存の CI / ツールチェーンが
db/schema.rbの末尾に何かを追記している場合、__END__の扱いに注意が必要です。 - マイグレーション削除運用:
- マイグレーションファイルを削除すると、そのバージョンは次回ダンプから
__END__部分に含まれなくなります。 - 古い環境でまだそのマイグレーションが DB に残っているケースと整合をどう取るかは従来どおり「マイグレーションの削除ポリシー」の問題として残ります。
- マイグレーションファイルを削除すると、そのバージョンは次回ダンプから
マージコンフリクトについて
version:行がなくなる + 並び順のカスタマイズにより、多くのケースで schema.rb のマージコンフリクトは減る見込みです。- ただし、同じブロック(同じテーブル定義など)を編集した場合のコンフリクトは当然ながら従来通り発生します。
dump_schema_migrations_sort_byによるソート戦略はチーム内で合意しておくべきです。- デフォルト(辞書順)は「読みやすさ重視」だがコンフリクトは若干起きやすい。
- MD5 などの「diff 分散戦略」はコンフリクト低減には有効だが、人間には直感的な順序ではなくなります。
- 参考情報 (あれば)
実コード変更の主な場所
activerecord/lib/active_record/schema.rbActiveRecord::Schema.load_schema_migrations(path)の実装追加。
activerecord/lib/active_record/schema_dumper.rbschema_migrationsのダンプ処理とソートロジック。
activerecord/lib/active_record.rb,railtie.rb,database_configurations/*- 設定フラグ (
dump_schema_migrations/dump_schema_migrations_sort_by) の追加および DB ごとのオーバーライド対応。
- 設定フラグ (
guides/source/active_record_migrations.md,guides/source/configuring.md- ガイドに新機能の説明と設定方法を追加。
アイデアとして検討されていた代替案
schema_migrations用の専用ディレクトリを作り、各バージョンごとに空ファイルを配置して Git で管理する方式も検討された。- これはマージコンフリクトに非常に強い(ファイルの追加・削除は Git が得意)が、
ファイル数増加・コミット漏れ・ツール側の対応コスト増などのデメリットがあるため、現時点では採用されていない。
#58133 Don't force multi statement for MySQL tx isolation
マージ日: 2026/7/16 | 作成者: @edaroit
- 概要 (1-2文で)
MySQL系アダプタでトランザクション分離レベルを設定してBEGINする処理について、「再試行時に一体として扱う」安全性は維持しつつ、常にマルチステートメントを強制しないように変更されたPRです。これにより、DBへのラウンドトリップ増加や ProxySQL でのコネクション多重化(multiplexing)無効化といった副作用を避けられるようになっています。
- 変更内容の詳細
背景
以前のリファクタリングで、begin_isolated_db_transaction(MySQL 抽象アダプタの「指定した隔離レベルでトランザクションを開始する」処理)は次のような方針になっていました:
SET TRANSACTION ISOLATION LEVEL ...; BEGINを「マルチステートメント (multi statements)」として一括実行する- これにより、DB接続レベルの再試行(
with_raw_connectionなどのリトライ機構)が入った場合でも、「SETだけ成功してBEGINがリトライされる」/「BEGINだけリトライされて、意図した分離レベルが失われる」といった不整合を防ぐ
ただし、この方法には以下の問題がありました:
- マルチステートメントを有効/無効にするためのやり取りが増え、結果的に DB とのラウンドトリップ数が増える
- ProxySQL を使っている場合、
SET TRANSACTION ISOLATIONとマルチステートメントの組み合わせによりコネクション multiplexing が無効化される(ProxySQL 側の仕様・制限)
今回の変更の狙い
- 「
SET TRANSACTION ISOLATION LEVELとBEGINを再試行単位として一体に扱う」=一貫性と安全性は維持したい - ただしその実現方法として、常にマルチステートメントを強制するのはやめる
- 既にマルチステートメント利用を前提にしている設定では、互換性を保つため引き続き
execute_batch(マルチステートメント)を利用
実装上のポイント
主な変更は abstract_mysql_adapter.rb の begin_isolated_db_transaction 周辺です。
1. マルチステートメント利用有無で挙動を分岐
擬似コード的には次のような形になります(実際のコードとは多少異なりますが概念的にはこういう分岐):
def begin_isolated_db_transaction(isolation)
if supports_multi_statements?
# 以前と同様、マルチステートメントで一体として実行
execute_batch(<<~SQL)
SET TRANSACTION ISOLATION LEVEL #{quote_isolation(isolation)};
BEGIN
SQL
else
# マルチステートメントを使わない場合:
# with_raw_connection の allow_retry で「一体として再試行」する
with_raw_connection(allow_retry: true) do
execute("SET TRANSACTION ISOLATION LEVEL #{quote_isolation(isolation)}")
execute("BEGIN")
end
end
endポイント:
- マルチステートメントが有効な場合
- 既存と同じく
execute_batchを使い、SETとBEGINを一つの SQL バッチとして送る。
- 既存と同じく
- マルチステートメントが無効な場合
- 2つの独立したクエリ (
SET .../BEGIN) として送るが、 with_raw_connection(allow_retry: true)ブロック内で実行することで、「途中でネットワークエラーや切断が発生した場合はブロック全体を再試行」できるようにしている。- これにより「
BEGINだけがリトライされて isolation level が落ちる」といった不整合を防ぐ。
- 2つの独立したクエリ (
重要な点:
- マルチステートメントを使わないルートでは、
SETとBEGINは別クエリだが、再試行単位としては「1ブロック」にまとまっている。 - そのため、安全性(再試行時の一貫性)は維持しつつ、DB とのラウンドトリップ数は従来どおりで、余計な on/off 切り替えをしなくて良くなる。
2. Trilogy アダプタへの対応
trilogy/database_statements.rb にも小さな変更が加えられており、Trilogy ベースの MySQL アダプタでも上記ロジックが適切に動作するようになっています。
Trilogy は Shopify 発の MySQL クライアントライブラリで、Rails 7.1 以降で公式サポートされているものです。この PR では、抽象アダプタの挙動変更が Trilogy 経由の接続でも正しく機能するよう、必要なフック・メソッドを追加/調整しています。
3. テスト追加
abstract_mysql_adapter/transaction_test.rb にテストが大量に追加されています(+47行)。
想定されるテスト内容:
- マルチステートメント有効時:
SET TRANSACTION ISOLATION LEVEL ...; BEGINがexecute_batchでまとめて送信されること
- マルチステートメント無効時:
SETとBEGINが別々のクエリとして実行されること- それらが
with_raw_connection(allow_retry: true)の中で実行され、再試行単位が1つであること
- 隔離レベル指定が正しく反映されること(
:read_committed,:repeatable_read,:serializableなど)
これにより、リグレッション(前のリファクタリングで得た「一体としての再試行」保証が壊れていないか)を防いでいます。
- 影響範囲・注意点
影響範囲
- 対象: MySQL 系アダプタ(mysql2 / trilogy など)を利用しており、かつ
transaction(isolation: ...)のような「分離レベル指定付きトランザクション」を使っているアプリケーション
- 特に恩恵が大きいケース:
- ProxySQL 経由で MySQL に接続している構成
- マルチステートメントと
SET TRANSACTION ISOLATIONの組み合わせで multiplexing が無効化されていた問題が軽減/解消される
- マルチステートメントと
- ネットワークが不安定で、
with_raw_connectionの再試行に頼るケース- 分離レベルと
BEGINが一貫性を持って再試行される
- 分離レベルと
- ProxySQL 経由で MySQL に接続している構成
互換性・挙動の変化
- API レベルのインターフェース(
transaction(isolation: ...)など)は変わりません。 - 接続が「マルチステートメントをサポートしていない/利用しない」設定になっている場合:
- 以前はマルチステートメントを強制する方向の変更が入っていたのに対し、今回の PR により、マルチステートメントを無理に有効化しなくても安全に動くようになります。
- マルチステートメントを「明示的に利用している」アプリケーション:
- その設定がオンであれば従来通り
execute_batchを使うため、挙動面で大きな変化はありません(ただし、以前のPRで追加された「マルチステートメント on/off の切り替えラウンドトリップ」が削減される場合があります)。
- その設定がオンであれば従来通り
注意点
- ProxySQL利用時:
- この修正により isolation 付きトランザクションでの multiplexing 無効化は緩和されますが、ProxySQL 側のバージョンや設定によって挙動が異なる可能性があります。
- 重要なワークロードの場合は、本番適用前に「分離レベルを変更するトランザクションが期待通り multiplexing されるか」「接続数・レイテンシに変化がないか」を計測することを推奨します。
- エラー再試行ロジック:
with_raw_connection(allow_retry: true)を利用することで、アダプタ内部の「再試行ポリシー」に依存する部分が増えます。- これによる実害は基本的にありませんが、独自のパッチや monkey patch で
with_raw_connection周りをいじっている場合は影響が出る可能性があるため、差分を確認してください。
- 参考情報 (あれば)
- 関連コミット(前回のリファクタリング)
- https://github.com/rails/rails/commit/4908dc305fa48142449224581377f8450313ef40
- ここで「
SET TRANSACTION ISOLATION LEVEL+BEGINをマルチステートメントでまとめる」戦略が導入されている。
- ここで「
- https://github.com/rails/rails/commit/4908dc305fa48142449224581377f8450313ef40
- ProxySQL issue
- https://github.com/sysown/proxysql/issues/4896
SET TRANSACTION ISOLATIONとマルチステートメントが multiplexing を無効化してしまう問題。
- https://github.com/sysown/proxysql/issues/4896
- 変更ファイル
activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rbbegin_isolated_db_transactionの本体ロジック変更
activerecord/lib/active_record/connection_adapters/trilogy/database_statements.rb- Trilogy 用の対応
activerecord/test/cases/adapters/abstract_mysql_adapter/transaction_test.rb- テストの追加(挙動の回帰検証)
#58115 Add async test coverage for contradiction/none short-circuits in calculations
マージ日: 2026/7/16 | 作成者: @55728
- 概要 (1-2文で)
矛盾条件(contradiction)や@noneによる早期リターンが行われる計算系メソッドについて、非同期版(async_*)でも「DBクエリを発行しない」「期待値を返す」ことを検証するテストが追加・強化された PR です。従来は同期版のみで確認していた no-query パスを、Promiseベースの async パスでも同等にカバーするようにしています。
- 変更内容の詳細
背景・狙い
- Active Record の計算系メソッド(
count,sum,average,minimum,maximum、およびpluck,ids,pick)には、「結果が明らかに空・矛盾する」場合に DB に行かずに即座に値を返すショートサーキットがあります。- 例:
where("1 = 0").countのような明らかな矛盾条件。 @noneが立っている Relation(Model.none由来など)。
- 例:
- 非同期 API(
async_countなど)は、この早期リターンの結果をPromise::Completeでラップする実装になっていますが、テストは同期 API 経由しか通っていませんでした。 - 本 PR では、この「非同期 + ショートサーキット」経路もテストで明示的にカバーし、Promise が期待通りの値を返し、かつクエリ発行が 0 であることを保証します。
追加されたテスト・修正内容
変更はすべて activerecord/test/cases/calculations_test.rb に集約されています(+16 / -3)。
1. 非グループ計算 (execute_simple_calculation の contradiction パス)
対象メソッド:
async_countasync_sumasync_averageasync_minimumasync_maximum
やっていること:
- 既存の同期計算テスト(矛盾条件で早期リターンするケース)に、対応する
async_*呼び出しのアサーションを追加。 - テストでは以下を確認しているはずです:
assert_no_queriesまたはassert_queries_count(0)ブロックの中でasync_*を呼び、- 返り値が
Promiseであること、 - その
Promiseをvalue(またはsync/相当のメソッド)で解決したとき、同期版と同じ結果(例: 0 やnil等)になること。
疑似コード例:
assert_no_queries do
promise = relation_with_contradiction.async_count
assert_equal 0, promise.value
end2. グループ計算 (execute_grouped_calculation の contradiction パス)
対象メソッド:
async_countasync_sum
やっていること:
- グループ化された計算で矛盾条件により結果が空になるパスについても、
async_*で同様に no-query + 期待値を検証。 - ここでも
assert_queries_count(0)内でPromiseを評価する形になっているはずです。
3. pluck (@none / contradiction パス)
- 既に
async_pluckのテスト自体は存在していましたが、no-query を保証するブロックの外でアサートしていたため、「async 版が DB を叩いていない」ことは明示的に保証されていませんでした。 - これを修正し、既存の:
assert_no_queries do
assert_equal [], relation.pluck(:id)
end
# (以前はこの辺に async_pluck のアサートがあった)のような構造を、
assert_no_queries do
assert_equal [], relation.pluck(:id)
promise = relation.async_pluck(:id)
assert_equal [], promise.value
endのように async_pluck も assert_no_queries / assert_queries_count(0) の内側に移動しています。
4. ids (contradiction パス)
pluckと同様の修正。async_idsのアサーションをassert_queries_count(0)ブロック内に移し、非同期版でもクエリが 0 回であることをチェック。
5. pick (@none / contradiction / loaded パス)
test_pick_oneに対して、同期pickに加えてasync_pickのアサーションを追加。@none、矛盾条件、あるいは結果が明らかに一意となるケースなど、それぞれで期待値を検証。
- 新規に
test_async_pick_on_loaded_relationを追加。- Relation が既にロード済みの場合に
async_pickを呼んだとき、- 追加のクエリが走らず(キャッシュされたレコードから値を得られ)、
- 結果も同期版と一致することを確認しています。
- Relation が既にロード済みの場合に
- 影響範囲・注意点
- 本 PR はテストコードのみの変更であり、実際の実装(
async_*/Promise::Complete等)は変更されていません。 - ただし、これにより以下の仕様がテストで明示的にロックされます:
- 矛盾条件 /
@none/ loaded な Relation でのasync_*系メソッドはDB クエリを発行しないこと。 - その結果は同期版と同じ値であり、
Promise経由で取得できること。
- 矛盾条件 /
- 将来、
async_*の実装を変更する場合、- ショートサーキット時にクエリを発行するような変更、
- あるいは
Promiseの解決値が変わる変更 は、これらのテストにより検出されるようになります。
- Rails の async API を利用しているアプリケーションにとっては、
- 「同期版と同じ no-query 最適化が async 版にも効いている」
- 「矛盾条件や
noneのときにも無駄なクエリが走らない」 という前提がより堅く保証されたと言えます。
- 参考情報 (あれば)
- ベースになっている PR: #58058(計算系メソッドの async 対応 / リファクタリングと推測される)
- レビューコメント: https://github.com/rails/rails/pull/58058#pullrequestreview-4687080638
→ 「contradiction /@noneの早期リターン経路でも async のテストを追加してほしい」という指摘へのフォローアップ。
#58085 Preserve sub-second precision when coercing a DateTime in Time.at
マージ日: 2026/7/16 | 作成者: @55728
- 概要 (1–2文で)
Time.at(DateTime)したときに、サブ秒(マイクロ秒)精度が失われていた問題を修正し、DateTimeからTimeへの変換でもマイクロ秒まで正確に保持されるようにしたPRです。ActiveSupport::TimeWithZoneと同様に Rational ベースの変換を使うことで、浮動小数点の丸め誤差を排除しています。
- 変更内容の詳細
何が問題だったか
Time.at は ActiveSupport の拡張により、以下のように 1 引数で DateTime や ActiveSupport::TimeWithZone を受け取れます。
Time.at(datetime)
Time.at(time_with_zone)TimeWithZoneの場合:to_r(Rational)を使って正確なタイムスタンプで変換していたDateTimeの場合:to_f(Float)を使って変換しており、ここでサブ秒精度が落ちていた
Float では 1 マイクロ秒単位をすべて正確に表現できないため、約半分の値で丸め誤差が出て Time#usec がずれる、という挙動になっていました。
PR 説明にある例:
dt = DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 1_000_000)) # 2000-01-01 00:00:00.000001
Time.at(dt).usec
# 修正前 => 0
# 修正後 => 1
dt = DateTime.civil(2000, 1, 1, 0, 0, Rational(123_457, 1_000_000))
Time.at(dt).usec
# 修正前 => 123456
# 修正後 => 123457どちらも「元の DateTime に含まれるサブ秒が、Time に変換したときに 1 マイクロ秒分誤差を持ってしまう」例です。
何をどう直したか
修正内容はシンプルで、ActiveSupport の Time.at 拡張部で DateTime を扱う箇所を Rational ベースに変更しています。
- これまで:
datetime.to_fを使っていた(Float) - これから:
datetime.to_i + datetime.sec_fractionを使う(整数秒 + 有理数で表現された秒数の小数部分)
datetime.sec_fraction は Rational を返すため、全体として Rational のタイムスタンプになります。これにより、TimeWithZone と同じく「完全に正確な Rational 値から Time を生成」する形になり、丸め誤差がなくなります。
テストとしては、activesupport/test/core_ext/time_ext_test.rb に DateTime からの変換に関するテストケースが追加されており、マイクロ秒精度が期待通りに保たれることが確認されています。
- 影響範囲・注意点
影響を受けるコード
Time.at(datetime)のようにDateTimeを直接Time.atに渡しているコード- とくに、サブ秒(microseconds)を意識している箇所(ログタイムスタンプ、精密メトリクス、イベント順序制御など)
挙動の変化の具体例
- これまで「
DateTimeのサブ秒が、Timeに変換したあとに 1µs 前後ズレていた」ケースが、正確に一致するようになります。 Time#to_i(秒単位)や日付・時刻(年・月・日・時・分・秒)が変わるわけではありません。変化するのはusec/nsec相当のサブ秒部分のみです。
- これまで「
互換性・注意点
- 既存コードで「ズレた値」を前提にテストしていた場合(たとえば
Time.at(dt).usecの具体的な数値を fixture 的に固定していた場合)は、テストが落ちる可能性がありますが、それは元の挙動が不正確だったためで、このPRにより論理的に正しい値になります。 - パフォーマンス面では、Float から Rational への切り替えによる影響はごく小さいと考えられます(通常アプリでは問題にならないレベル)。
- 既存コードで「ズレた値」を前提にテストしていた場合(たとえば
タイムゾーンや DST, 負のタイムスタンプ
- PR 内で以下のケースが検証されていると明記されています:
- 整数秒(サブ秒なし)
- タイムゾーンオフセット(DST を含む)
- マイクロ秒境界(サブ秒あり)
- Unix エポックより前(負のタイムスタンプ)
- これらのケースでも、
datetime.to_i + datetime.sec_fractionで表される瞬間とTime.at(datetime)の結果が厳密に一致し、かつ戻り値は引き続きローカルタイム(getlocal済み)であることが保証されています。
- PR 内で以下のケースが検証されていると明記されています:
- 参考情報 (あれば)
- Ruby の
DateTime#sec_fractionドキュメント:
サブ秒をRationalで返し、精度の高い時間計算に使えることが知られています。 - 浮動小数点誤差に関する一般的な注意点:
時刻をFloat秒で扱うとマイクロ秒単位での精度が保証されないため、- データベースやメッセージングのタイムスタンプ
- イベントソーシングのイベント時間
などでは、このPRのように Rational または整数 + サブ秒整数(秒・ナノ秒のペア)のような表現を用いるのが望ましいです。
このPRにより、「DateTime → Time 変換時もマイクロ秒が正確に保持される」という、直感的かつ一貫した挙動が得られるようになっています。
#58119 Allow to access Controller#controller_path in a Ractor
マージ日: 2026/7/16 | 作成者: @Edouard-chin
- 概要 (1-2文で)
AbstractController#controller_pathが返す文字列を freeze することで、Ractor 内からも安全にcontroller_pathを呼び出せるようにした変更です。Ractor セーフティを満たすための最小限の修正で、既存のメモ化ロジックを活かしつつ対応しています。
- 変更内容の詳細(あればサンプルコードも含めて)
何をしたか
AbstractController::Base#controller_path が返す文字列を「凍結(freeze)」するように変更しています。
変更前(イメージ):
def controller_path
@controller_path ||= self.name.delete_suffix("Controller").underscore
end変更後:
def controller_path
@controller_path ||= self.name.delete_suffix("Controller").underscore.freeze
end実際の変更は actionpack/lib/abstract_controller/base.rb の 1 行のみで、controller_path の結果を freeze するのが本質です。
なぜ必要か (Ractor セーフティ)
Ruby の Ractor では、Ractor 間で共有されるオブジェクトは原則としてイミュータブル(変更不可)である必要があります。
これまで controller_path は毎回「非 frozen な String」を返していたため、その値を Ractor に渡そうとすると Ractor セーフティに違反し、例外が発生する可能性がありました。
例:
class PostsController < ApplicationController; end
Ractor.new(PostsController) do |controller|
# 変更前はここで Ractor::IsolationError 等になる可能性がある
controller.controller_path
endこの PR により、controller_path が返す文字列が frozen になるため、上記のようなコードが Ractor 内でも安全に動作します。
メモ化と Ractor セーフティ
controller_path はインスタンス変数などを使ってメモ化されますが、
- 値が frozen である
- 値が設定されるタイミングは既存の
inheritedフックを通じて行われる
という点から、Ractor の観点でも問題ないと判断されています。
つまり、「クラス定義時に一度だけ Ractor 外でセットされた、イミュータブルな値」を各 Ractor が読むだけ、という形になります。
テスト追加
actionpack/test/controller/base_test.rb に 14 行のテストが追加されており、少なくとも次のようなポイントを検証していると考えられます:
controller_pathが frozen な文字列を返していること- 継承したコントローラでも same behavior であること
- メモ化挙動が壊れていないこと(複数回呼び出しても同一オブジェクトが返る 等)
- 影響範囲・注意点
影響範囲
- すべてのコントローラ (
AbstractController::Baseを継承するクラス) のcontroller_pathが、今後は 常に frozen な String になります。 - これにより、
controller_pathを Ractor に渡す・Ractor 内で使用することが可能になります。 - Ractor を使わないアプリでも、「返ってくる String が frozen になる」という仕様変更の影響は受けます。
注意点
controller_pathを破壊的に変更しているコードがあると壊れる
たとえば、以下のようなコードがある場合は例外が発生します:
# NG になる例
path = SomeController.controller_path
path.gsub!("/", "_") # frozen string に対する破壊的変更で例外が出るこのような場合は、非破壊メソッドを使用するか、複製を取る必要があります:
# 推奨
path = SomeController.controller_path.tr("/", "_")
# または
path = SomeController.controller_path.dup
path.gsub!("/", "_")- テストや拡張コードでの monkey patch に注意
controller_pathを前提に何かしら monkey patch や特殊なキャッシュロジックを書いている場合、
それが「mutable string であること」に依存していないか確認が必要です。
- Ractor 利用時の前提
- この変更はあくまで
controller_pathの返り値を Ractor セーフにするものです。
コントローラ全体が Ractor セーフになったという意味ではないため、
Ractor 内でコントローラ・インスタンスをどこまで扱うかは別途注意が必要です。
- 参考情報 (あれば)
- 対象メソッド:
AbstractController::Base#controller_path- 典型的には
"Admin::PostsController"→"admin/posts"のようなパスを返す
- 典型的には
- 関連する Ruby の仕様:
- Ractor: オブジェクト共有は frozen オブジェクトなどに制限される
- frozen String への破壊的操作は
FrozenErrorなどを引き起こす
この PR は、Ractor 対応を進めるうえでの小さいが重要な一歩であり、
「framework が返す定型的な String を積極的に凍結していく」流れの一環といえます。
#58116 Guard tests for PostgreSQL 11, 12, and 15 changes
マージ日: 2026/7/15 | 作成者: @yahonda
- 概要 (1-2文で)
このPRは、Active Record のテスト群とテスト用スキーマを「PostgreSQL 10〜18の全サポート範囲で通る」ように調整し、特定バージョン以降でしか存在しない PostgreSQL 機能を利用するテストにガード(条件分岐・スキップ)を入れたものです。ライブラリ本体の動作変更は行わず、あくまでテストとスキーマ定義のみを修正しています。
- 変更内容の詳細
背景と目的
- Rails(Active Record)は PostgreSQL 10 以降をサポートしている一方で、CI は
postgres:alpine(現状 PostgreSQL 18)でしか実行されていません。 - 手元で
postgres:17〜postgres:10の Docker イメージを使ってrake test:postgresqlを実行すると、- 「最小サポートバージョン(10)より新しいサーバー機能」に依存したテストが、バージョン判定なしで実行されて失敗する
- PostgreSQL 10 ではテストスキーマ自体が読み込めず、テストがそもそも走らない という問題が見つかりました。
- このPRは、それらを「テスト側だけ」で吸収し、PostgreSQL 10〜18 すべてでテストが通るようにするものです。
PostgreSQL 15 向けのテストガード
対象ファイル:
activerecord/test/cases/schema_dumper_test.rb
問題となっていたテスト
SchemaDumperTest#test_schema_dumps_unique_constraints- PostgreSQL 14 での失敗例:
Expected /t\.unique_constraint\ \["position_4"\],\ nulls_not_distinct:\ true,\ name:\ "test_unique_constraints_position_nulls_not_distinct"/ to match ...UNIQUE NULLS NOT DISTINCT構文は PostgreSQL 15 以降でのみサポート。- 14 以前ではアダプタがこの句を付けずに制約を作成し、schema dump からも
nulls_not_distinct: trueが消えるため、正規表現マッチが失敗していました。
対応内容
- すでに同ファイル内にある
test_schema_dumps_nulls_not_distinctと同様に、supports_nulls_not_distinct?を利用してアサーションを分岐。supports_nulls_not_distinct? == true(PG 15+):nulls_not_distinct: trueを期待。- それ以外(PG 10〜14): そのフラグが無い出力を期待、あるいはその部分の検証をスキップ。
これにより、PostgreSQL 10〜14 でもテストが落ちず、15 以降では従来どおり機能確認が行われます。
PostgreSQL 12 向けのテストガード
対象ファイル:
activerecord/test/cases/persistence_test.rbactiverecord/test/cases/dirty_test.rbactiverecord/test/cases/adapters/postgresql/enum_test.rb
1) 生成列 (generated columns) 関連
エラー例(PostgreSQL 11.16):
NoMethodError: undefined method 'virtual_stored_number' for an instance of Defaultと
Failure:
DirtyTest#test_virtual_column_loaded_change_on_update:
Expected: [1050, 1400]
Actual: nilvirtual_stored_numberカラムは「生成列 (generated stored column)」で、PostgreSQL 12 以降でのみサポートされている機能。- テストスキーマではすでに
supports_virtual_columns?を条件に、このカラムの作成を制御していましたが、- カラム自体がない環境(PG 10, 11 等)でも、テスト
test_fills_auto_populated_columns_on_updateやtest_virtual_column_loaded_change_on_updateが無条件で実行されていたため、NoMethodError / 期待値不一致が発生していました。
- カラム自体がない環境(PG 10, 11 等)でも、テスト
対応:
- 上記 2 テストを
supports_virtual_columns?を使ってガードし、生成列をサポートしない環境ではテストをスキップするように変更。
2) ENUM 型の ALTER TYPE ... ADD VALUE のトランザクション制限
エラー例(PostgreSQL 11.16):
ActiveRecord::StatementInvalid: PG::ActiveSqlTransaction: ERROR: ALTER TYPE ... ADD cannot run inside a transaction block- PostgreSQL 12 より前は、
ALTER TYPE ... ADD VALUEをトランザクションブロック内で実行できません。 - 該当テスト:
PostgresqlEnumTest#test_schema_dump_added_enum_value
対応:
database_versionを参照し、12 未満(< 12_00_00)の場合はこのテストをスキップするように修正。- この機能には既存の
supports_*?プレディケートが無いため、バージョン数値を直接比較しています。
PostgreSQL 11 向けのテストガードとスキーマ修正
対象ファイル:
activerecord/test/schema/postgresql_specific_schema.rbactiverecord/test/cases/adapters/postgresql/postgresql_adapter_test.rbactiverecord/test/cases/adapters/postgresql/referential_integrity_test.rb
1) トリガー定義の構文変更
PostgreSQL 10.21 でのスキーマロードエラー:
PG::SyntaxError: ERROR: syntax error at or near "FUNCTION"
... test/schema/postgresql_specific_schema.rb:213- エラーの要因:
CREATE TRIGGER ... EXECUTE FUNCTION構文は PostgreSQL 11 で導入された新しい書き方。 - PostgreSQL 10 では、従来の
EXECUTE PROCEDUREを使う必要があります。
対応:
- 問題のトリガーを
EXECUTE FUNCTIONからEXECUTE PROCEDUREに変更。 - 同ファイル内の
partitioned_insert_triggerでもすでにEXECUTE PROCEDUREを使っていたため、それに合わせた形になります。 EXECUTE PROCEDUREは 10〜18 まで後方互換で通るため、すべてのサポートバージョンでスキーマがロード可能になります。
2) ドメイン型配列 (arrays over domains) とパーティションテーブル制約
スキーマがロードできるようになった結果、今度はテスト側で以下のエラーが発生:
PG::UndefinedObject: ERROR: type "postgresql_domain_base[]" does not exist
PG::UndefinedObject: ERROR: type "postgresql_domain_nested_after_bulk[]" does not existおよび:
PG::InFailedSqlTransaction: ERROR: current transaction is aborted, commands ignored until end of transaction block詳細:
- ドメイン型の配列は PostgreSQL 11 からサポート。
- これを利用しているのが
PostgreSQLAdapterTest内のtest_load_additional_types_cascades_dependency_lookupstest_load_additional_types_cascades_dependency_lookups_after_initial_bulk_loadなどのテスト。
- これを利用しているのが
- PostgreSQL 10 ではドメイン配列型自体が存在せず、その型に対する操作で
PG::UndefinedObjectが発生していました。 - また
PostgreSQLReferentialIntegrityTest#test_all_foreign_keys_valid_having_foreign_keys_with_partitioned_tableについて:- パーティションテーブル上の primary / foreign key 制約は PostgreSQL 11 以降の機能。
- PostgreSQL 10 では
CREATE TABLEの時点で「primary key constraints are not supported on partitioned tables」といったエラーが発生し、以後の SQL が失敗 (InFailedSqlTransaction) していました。 - このテストはパーティションテーブル“上に”外部キーを張るケースをテストしており、「パーティションテーブルを参照する外部キー」(PostgreSQL 12 以降)までは利用していません。
対応:
- 上記 3 つのテストは、
ensureブロックでクリーンアップを行います。 Minitest/SkipEnsureのルール上、ensure内でskipを呼べないため、「実行時スキップ」ではなく「定義時ガード」を採用。
具体的には:
if ActiveRecord::Base.lease_connection.database_version >= 11_00_00
class PostgreSQLAdapterTest < ActiveRecord::TestCase
def test_load_additional_types_cascades_dependency_lookups
...
end
def test_load_additional_types_cascades_dependency_lookups_after_initial_bulk_load
...
end
end
endのように、テストメソッド自体を「PostgreSQL 11 以上のときだけ定義する」形に変更しています
(実際のコードはこのイメージに沿った記述になっている、という理解で十分です)。
同様に、パーティションテーブル関連のテストについても database_version >= 11_00_00 を条件に定義するアプローチが取られています。
- 影響範囲・注意点
- 影響範囲は テストコードとテスト用 PostgreSQL スキーマのみ です。
activerecord/lib以下の本体コードには変更がありません。- アプリケーションや gem 利用者に対する挙動・互換性への影響はありません。
- これにより:
- PostgreSQL 10〜18 (Docker:
postgres:10〜postgres:18) の全バージョンでrake test:postgresqlが通ることが確認済みです。 - CI が現在使っている PostgreSQL 18.4 では、変更されたテストファイルについて「実行回数・アサーション数・skip 数」が main ブランチと同一であり、CI 上で新たにテストがスキップされることもありません。
- PostgreSQL 10〜18 (Docker:
- 今後、古い PostgreSQL バージョンでテストを手元実行したい場合、この PR のようなバージョンガードがある前提で、追加のテストを実装することが必要になります。
database_versionを直接比較するパターン(特に ALTER TYPE ... ADD VALUE のような細かい仕様差)は、将来的にsupports_*?プレディケートとして切り出す余地がありますが、現時点ではテスト内ローカルな判断にとどまっています。
- 参考情報 (あれば)
PostgreSQL 側の機能追加コミット:
- PostgreSQL 15
- UNIQUE 制約の NULL 取り扱いオプション追加
https://github.com/postgres/postgres/commit/94aa7cc5f707712f592885995a28e018c7c80488
- UNIQUE 制約の NULL 取り扱いオプション追加
- PostgreSQL 12
ALTER TYPE ... ADD VALUEのトランザクション制限緩和
https://github.com/postgres/postgres/commit/212fab9926b2f0f04b0187568e7124b70e8deee5
- PostgreSQL 11
CREATE TRIGGER構文をPROCEDUREからFUNCTIONに変更
https://github.com/postgres/postgres/commit/e0dc839e72d43e6c299deca892a8209e11dd88f6- ドメイン型配列サポート
https://github.com/postgres/postgres/commit/c12d570fa147d0ec273df53de3a2802925d551ba - パーティションテーブルへの UNIQUE 制約サポート
https://github.com/postgres/postgres/commit/eb7ed3f3063401496e4aa4bd68fa33f0be31a72f - パーティションテーブルに対する外部キーサポート
https://github.com/postgres/postgres/commit/3de241dba86f3dd000434f70aebba725fb928032
PR の要点をまとめると、「Active Record がサポートすると宣言している PostgreSQL 10〜18 すべてに対して、テストスイートが現実に成功するよう、バージョンごとの機能差をテスト側で吸収した」という位置づけになります。
#57702 Don't autovivify the PostgreSQL listener on shutdown
マージ日: 2026/7/15 | 作成者: @55728
- 概要 (1-2文で)
PostgreSQL 用の ActionCable サブスクリプションアダプタで、shutdown時に未使用のlistenerを新規生成してしまう不具合を修正し、Redis アダプタと同様に「既に存在する場合のみ shutdown する」挙動に統一した PR です。これにより、未購読・送信専用プロセスの終了時に不要なスレッド・DB接続が作られなくなります。
- 変更内容の詳細
問題となっていた既存コードは以下のようになっていました:
def shutdown
listener
end
def listener
@listener || @mutex.synchronize { @listener ||= Listener.new(self, executor) }
endshutdown が private メソッド listener を呼ぶことで、メモ化されている listener が未生成 (@listener が nil) の場合でも Listener.new が走ります。
Listener.newは- 即座にバックグラウンドスレッドを起動
- そのスレッド内で
with_subscriptions_connectionを呼び、新しい DB コネクションを開いてLISTENループに入る
- つまり、「購読を一度もしていない」「送信専用の」アダプタであっても、
shutdownを呼んだ瞬間に- 不要な
Listenerインスタンス - 不要な DB コネクション
- 不要なバックグラウンドスレッド が生成され、その直後に shutdown される挙動になっていました。
- 不要な
- さらに、そのスレッドは
abort_on_exception = trueで動くため、接続失敗などが予期しないタイミング・場所で例外として表面化する可能性もありました。
Redis アダプタでは同様のメソッドが既に以下のように書かれており、問題が発生していませんでした:
def shutdown
@listener.shutdown if @listener
end両方のアダプタとも initialize 内で @listener を nil 初期化しているため、インスタンス変数は常に定義済みです。このため、@listener に直接アクセスしてガードするのが安全であり、かつ「自動生成しない」挙動になります。
今回の修正で、PostgreSQL アダプタ側の shutdown は次のように変更されました:
def shutdown
@listener.shutdown if @listener
endつまり:
- すでに
listenerが作られている場合のみshutdownを呼ぶ - 存在しない (
@listenerがnil) 場合は何もしない(no-op)
となり、Redis アダプタと挙動が揃いました。
テスト (actioncable/test/subscription_adapter/postgresql_shutdown_test.rb) では:
- PostgreSQL アダプタを生成
- 一度も
subscribeなどを呼ばずにshutdownを実行 - 実行後も
@listenerがnilのままであることをアサート
という形で「autovivify(shutdown 時の自動生成)が起きないこと」を検証しています。
テスト内では with_subscriptions_connection をスタブしており、バグありのコードでは autovivified なスレッドが実 DB に触らないようにしています。修正後コードではそもそもスレッドが生成されず、テストがグリーンになります。
- 影響範囲・注意点
影響範囲:
- 対象:
ActionCable::SubscriptionAdapter::PostgreSQLを使っているアプリケーション - 特に影響があるケース:
- ActionCable で PostgreSQL アダプタを使っているが、プロセス側では購読を行わない(ブロードキャスト専用/送信専用)ワーカー・プロセス
- あるいは購読開始前にプロセスが終了する可能性があるケース
この変更により:
- これらのプロセスのシャットダウン時に、不要なスレッド・DB接続が生成されなくなる
- 終了時に「たまたま LISTEN 用コネクション確立に失敗し、
abort_on_exceptionでプロセスが異常終了」といった予期しない挙動が避けられます - 既存の正常な利用(購読を開始しているプロセス)では、
listenerはすでに生成済みであり、@listener.shutdownが呼ばれる点は従来と同じで、互換性に問題はほぼありません
注意点:
- もし独自コードで
@listenerに直接触っている/ライフサイクルを上書きしているような非常に特殊なケースがあれば、その挙動が Redis アダプタと同じ形に揃ったことを前提に再確認するとよいです。 - 一方で、
shutdownを「呼ぶことで listener を初期化する」ような依存関係は、今回の変更で成立しなくなりますが、そのような使い方はもともと意図されたものではありません。
- 参考情報 (あれば)
- 対応する Redis アダプタの実装(挙動を揃えた参照先)
actioncable/lib/action_cable/subscription_adapter/redis.rbのshutdown実装 - 関連クラス:
ActionCable::SubscriptionAdapter::PostgreSQL::Listenerwith_subscriptions_connection(LISTEN 用 DB コネクションを管理)
- この修正は 1 行の置き換えのみで、動作仕様は「未使用アダプタの shutdown は no-op」という形に明確化された、と捉えると理解しやすいです。
#58042 Add missing ActiveSupport core extension and feature requires
マージ日: 2026/7/15 | 作成者: @55728
- 概要 (1-2文で) Rails の各コンポーネントが Active Support のコア拡張メソッドや定数を 明示的に require していなかった問題をまとめて修正し、「特定の順序で require したときだけ動く」状態を解消する PR です。挙動そのものは変えず、ロード順に依存した
NoMethodError/NameErrorを潰すための require 追加のみが行われています。
- 変更内容の詳細
共通テーマ
- これまで多くのファイルが、
index_by,years,symbolize_keys!,to_sentence,deep_symbolize_keysActiveSupport::TimeZone,ActiveSupport::Duration,ActiveSupport::TimeWithZoneなど Active Support が提供する拡張や定数に依存しているにもかかわらず、対応するファイルを require していなかった。
- そのため、
- 「アプリ全体を
require "rails"でまとめてロード」する場合は、たまたまどこかで読み込まれるので動く - しかし、コンポーネント単位の利用 / cherry-pick require / ゲームや CLI などの軽量構成では
NoMethodErrorやNameErrorが出る
- 「アプリ全体を
- この PR では、その穴を埋める形で 不足している require を各ファイルに追加しています(コードロジックの変更はなし)。
コンポーネント別の具体的な修正
Action Pack
ActionController::Parameters#fetch_valuesArray#index_byを呼んでいるが、index_byを定義するコア拡張の require がなかった。actionpack単体ロードで:rubyrequire "action_controller" ActionController::Parameters.new(name: "F", age: 22).fetch_values(:name, :age) # => NoMethodError: undefined method `index_by'という状態になっていたのを、適切な require 追加で解消。
ActionController::MethodNotAllowed- エラーメッセージ構築に
Array#to_sentenceを使用しているが、to_sentenceのコア拡張を require していなかった。 require "action_controller/metal/exceptions"だけでこの例外を発生させるとNoMethodErrorになっていた問題を修正。
- エラーメッセージ構築に
ActionDispatch::Cookies::PermanentCookieJar- 期限計算に
20.yearsを使っているが、Numeric#yearsを定義する Active Support の time 拡張を require していなかった。 require "action_dispatch"ベースの Rack アプリで permanent cookie を使うとundefined method 'years'になる問題を修正。
- 期限計算に
Action View
time_zone_options_for_select/time_zone_selectActiveSupport::TimeZoneを参照しているが、これはactive_support/timeを require しないと autoload されない。- Action View 単体 (
require "action_view") で time zone 関連ヘルパーを呼ぶとNameErrorになっていたのを、ActiveSupport::TimeZoneを提供するファイルを明示的に require する形で修正。
ActionView::Rendering/ActionView::Layouts- モジュール定義時に
attr_internal_reader/attr_internal_writerを呼んでいるが、それらを定義する core_ext を require していなかった。 require "action_view"直後にこれらのモジュールを参照すると例外が起きうる状態だったため、他のファイル同様に対応する core_ext の require を追加。
- モジュール定義時に
ContentExfiltrationPreventionHelperhtml_safeな定数を定義しつつ、mattr_accessorも利用しているが、これらの機能を提供する- 文字列の output_safety 拡張
mattr_accessorを定義する Active Support のモジュール の require 順が崩れているとエラーになっていた。
form_tag_helper.rbがこの helper を、自前のstring/output_safetyrequire より先に読んでいたため、require "action_view/helpers/form_tag_helper"のみでNoMethodErrorが発生するケースを修正。
Action Cable
- 「ブロードキャスト専用プロセス」(Job などから
ActionCable.server.broadcastだけ行う構成)が、以下の順にNoMethodErrorを起こしていた:- ログ出力で
truncateが未ロード - PubSub アダプタ解決で
camelizeが未ロード - Redis アダプタ設定で
deep_symbolize_keysが未ロード
- ログ出力で
- 原因は、コア拡張をロードするトリガーが “Channel クラスの定義” に依存していたこと。
- 対策として、Action Cable の
configurationbroadcastingsubscription_adapter/redisなどに、必要な Active Support core_ext の require を明示的に追加し、「ブロードキャストだけ使う」パターンでも問題なく動くようにしている。
Active Job
v8.1.0 以降、シリアライザのインデックス化が eager になった影響で、
ActiveJob::Baseをロードする前にActiveJob::Serializersを触る(カスタムシリアライザ登録の推奨パターン)と、
ActiveSupport::Duration,DateTime,ActiveSupport::TimeWithZoneが未ロードでNameErrorになる状態だった。rubyrequire "active_job" ActiveJob::Serializers.serializers # => NameError: uninitialized constant ActiveSupport::Duration対象の serializer ファイル(
duration_serializer.rbなど)から、それぞれが依存する Active Support のクラス・拡張を明示的に require するよう修正。
Active Storage
ActiveStorage::Service#service_nameがString#removeを使っているが、removeを追加するコア拡張が require されていなかった。ActiveStorage::Service.configureのような Active Storage 単体利用時に、インストルメンテーションを伴う操作で必ずNoMethodErrorになる状態を、該当する core_ext を require することで解消。
Active Support
require "active_support/time"が壊れていた問題time_formats.rbがActiveSupport::Ractorsを参照するようになったが、その提供元を require していなかった。- さらに、その裏で
core_ext/time/conversions.rb・core_ext/date/conversions.rbのDATE_FORMATS非推奨 shim がActiveSupport::DeprecationActiveSupport.deprecatorを require なしで参照していた。
- 結果として、
require "active_support/time"単体がNameErrorで落ちるようになっていたのを、すべて依存元を require する形で修正。
ActiveSupport.error_reporter.set_context(foo: 1)symbolize_keys!を使っているが、Hash#symbolize_keys!を定義する core_ext を require しておらず、どの standalone プロセスでも必ずundefined method 'symbolize_keys!'になる状態を修正。
MessageVerifier#generate(data, expires_in: 3600)advanceを使った時間計算を行っているが、Time#advanceのコア拡張を require していなかった。- たまたま呼び出し側で
1.hourなどを使うと、その require 副作用で問題が隠れる、という「ロード順依存バグ」だったものを是正。
ActiveSupport::ActionableError,ActiveSupport::Testing::FileFixtures,StructuredEventSubscriber,EventReporter::LogSubscriber- これらが
class_attributeを使うが、active_support/core_ext/class/attributeを require していなかった。 - 特に
rspec-railsがこれらを直接 require するため、テスト環境限定でundefined method 'class_attribute'が噴出するケースがあった。 ActiveSupport::LogSubscriberが既に行っているのと同様に、class_attribute の core_ext を各ファイルで require。
- これらが
Redis cache store の接続引数の非推奨 shim
squishを使用しているが、String#squishの提供元を require しておらず、警告を出す代わりにundefined method 'squish'が出てしまう状態を修正。
ActiveSupport::Deprecation#behavior = :notifyunderscoreを使うが、String#underscoreの core_ext を require していなかった。- standalone な gem が自前の deprecator を定義して
behavior = :notifyを設定するとNoMethodErrorになっていたのを修正。
MessagePack シリアライズ (
serializer: :message_pack)json_createオブジェクトのシリアライズでas_jsonを呼ぶが、対応する Active Support 拡張がロードされていないとundefined method 'as_json'になる。- キャッシュ層などから MessagePack を使うケースでエラーが出ないよう、必要な拡張を require。
XmlMini/Hash#to_xmlとDate#to_fsXmlMiniがDate#to_fsを使うが、それを定義するdate+ Active Support のフォーマット拡張がロードされていない場合にNoMethodError。require "active_support"; require "active_support/core_ext/hash/conversions"のような 公式ドキュメント通りの cherry-pick ルートでも落ちていたので、XmlMini側で Date フォーマット関連の拡張を require して修正。
- 影響範囲・注意点
- 既存アプリへの挙動変更は基本的にありません。
- 追加されたのはすべて
requireのみで、メソッドの実装や内部ロジックは変えていません。 - これまで「たまたまどこかで require されていた」拡張を明示的に require するようになっただけです。
- 追加されたのはすべて
- 影響が出る可能性があるとすれば:
- ロードされるファイル数がわずかに増えるため、極限まで起動時間を最適化しているケースでは差分が出る可能性があります(とはいえ +35 行程度の require であり、現実的には誤差レベル)。
- これまで「例外が出ていた」パスが正しく動作するようになるため、テストの挙動が良い方向に変わる(例外が出なくなる)ことがあります。
- コンポーネント単体利用(Action Pack だけ、Action View だけ、Active Job だけ、など)や、
- Job からの Action Cable broadcast
- Active Storage の低レベル API
- Active Support の一部機能だけ cherry-pick のような使い方をしている場合は、これまで sporadic に発生していた NoMethodError / NameError が解消されるはずです。
- 参考情報 (あれば)
- この PR は、同様のクラスの修正である以下のコミットのフォローアップと明示されています:
5fe517563e12422d5d0a
- PR の趣旨:
- 「ファイルは自分が使うものを自分で require すべき」という原則を Rails コードベース全体に徹底することで、
- ロード順依存のバグを排除し、
- コンポーネント単位利用や gem / ライブラリからの cherry-pick 利用を安全にする、というものです。
#58132 Fix rubocop.yml indentation
マージ日: 2026/7/15 | 作成者: @jeromedalbert
- 概要 (1-2文で)
Rails がrails newなどで生成する.rubocop.ymlテンプレートのインデント幅を、他の YAML テンプレートと同じ「半角スペース 2 個」に揃える修正です。前回 PR(#57252) のフォローアップとして、4 スペースになっていた部分を 2 スペースに直しています。
- 変更内容の詳細
対象ファイル:railties/lib/rails/generators/rails/app/templates/rubocop.yml.tt
このテンプレートは、Rails アプリケーション生成時に .rubocop.yml として出力される元ファイルです。今回の変更は、その中のインデントを 4 スペース → 2 スペースに揃えるだけの細かなフォーマット修正です。
イメージとしては、例えば以下のような差分です(実際のルール名や内容は例示):
# 変更前 (4スペースインデント)
AllCops:
NewCops: enable
TargetRubyVersion: 3.3
# 変更後 (2スペースインデント)
AllCops:
NewCops: enable
TargetRubyVersion: 3.3Rails が生成する他の YAML (database.yml, storage.yml など) は 2 スペースインデントが標準になっているため、それと一貫させるための修正です。
- 影響範囲・注意点
新規に生成される Rails アプリのみが対象
PR マージ後にrails newで作成されるアプリの.rubocop.ymlのインデントが 2 スペースになります。既存アプリのファイルは自動では変更されません。動作上の影響は基本的にない
- YAML としては 4 スペースも 2 スペースもどちらも有効なため、Rubocop の挙動自体は変わりません。
- ただし、「インデント 2 スペース」を前提にした YAML の自動整形ツールやエディタ設定を使っている場合に、見た目や差分がより一貫したものになります。
既存プロジェクトとの一貫性を取りたい場合
- 既に生成済みの
.rubocop.ymlをこのテンプレートに揃えたい場合は、インデントを 2 スペースに統一しておくと、将来的なテンプレート更新の取り込みや、チーム内のスタイル統一がしやすくなります。
- 既に生成済みの
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58132
- フォローアップ元 PR: https://github.com/rails/rails/pull/57252
- RuboCop 設定ファイルリファレンス: https://docs.rubocop.org/rubocop/configuration.html
#58043 Respect a limit set on the relation in rails query
マージ日: 2026/7/15 | 作成者: @55728
- 概要 (1-2文で)
bin/rails queryコマンドで、すでにlimitが設定されている Active Record リレーションを評価した際に、そのlimitがページネーション用の件数 (per + 1) に上書きされてしまう問題を修正し、ユーザー指定のlimitをそのまま尊重するようにした PR です。これによりPost.limit(5)のようなクエリで、期待どおり 5 件だけが取得されるようになります。
- 変更内容の詳細
何が問題だったか
bin/rails query は、結果をページング表示するために内部で「1ページあたりの件数 + 1 件」(per + 1)を使ってクエリに LIMIT を付与していました。
その際、すでに limit が指定されているリレーション(例: Post.limit(5))に対しても一律にページサイズの LIMIT を上書きしてしまっていたため、次のような挙動になっていました:
bin/rails query "Post.limit(5)"
# 実際には LIMIT 101 相当が付与され、最大 101 件返ってきてしまうこれは、--sql オプションで生 SQL を投げた場合にユーザー指定の LIMIT が尊重される挙動 (execute_sql) とも不整合でした。
何を修正したか
railties/lib/rails/commands/query/query_command.rb にて、Relation にすでに limit が存在するかどうかを判定し、存在する場合はページネーション用の limit を付け足さないようにしました。
イメージとしては次のようなロジックになります(擬似コード):
relation = eval(expression) # "Post.limit(5)" など
if relation.is_a?(ActiveRecord::Relation)
if relation.limit_value # すでに limit が設定されている場合
# そのまま実行。表示は別途 truncate などで制御。
records = relation.to_a
else
# limit がない場合のみ、ページサイズ (per + 1) を付与
records = relation.limit(per + 1).to_a
end
end実際には「クエリ実行時の件数制御」と「画面・出力上のページングのための表示件数トランケーション」が分離され、「クエリに埋め込まれた LIMIT は尊重しつつ、表示上は paginate/truncate する」という構造に寄せられています。
テストの追加
railties/test/commands/query_test.rb にテストが追加され、以下のようなケースがカバーされています:
bin/rails query "Post.limit(5)"実行時に、クエリオブジェクト側のlimitが変更されず、その件数だけが取得・表示されること- ページング表示は引き続き機能しつつも、ユーザー指定の
limitを侵害しないこと
(実際のテストコードでは rails コマンドの出力をキャプチャして件数や SQL が期待どおりかどうかを検証していると考えられます。)
- 影響範囲・注意点
影響対象:
bin/rails queryコマンドで、Active Record リレーション(Post.where(...).limit(5)など)を評価して使っているケース。- 特に、クエリ文字列の中に
limitを含めて件数をコントロールしていた人にとって、今回からはそのlimitが正しく反映されるようになります。
Before / After の動き:
bashbin/rails query "Post.limit(5)" # Before: # 内部で LIMIT がページサイズ (例: 101) に置き換えられ、 # 最大 101 件まで返ってきていた。 # # After: # クエリに指定された LIMIT 5 がそのまま使われ、5 件のみ取得。 # 表示上のトランケーションは別レイヤーで行われる。互換性上の注意:
- これまで「ページサイズ分取れればいい」という前提で
bin/rails queryを使い、明示的なlimit指定をしていた場合、取得件数が減る可能性があります(意図はlimit(5)なので、本来の挙動に修正されたといえる)。 - もし「ページング件数に任せたい」場合は、クエリ内に
limitを書かず、where,orderなどのみに留めるべきです。
- これまで「ページサイズ分取れればいい」という前提で
--sqlとの一貫性:bin/rails query --sql "SELECT * FROM posts LIMIT 5"では元々LIMIT 5が尊重されていましたが、今回の修正により Active Record パスでも同様にLIMITが尊重されるようになり、両者の挙動が揃いました。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58043
- タイトル: Respect a limit set on the relation in
rails query
- タイトル: Respect a limit set on the relation in
- 関連するコード:
railties/lib/rails/commands/query/query_command.rbrailties/test/commands/query_test.rb
運用上は、「bin/rails query で件数を制御したい場合は、今後は素直に limit を使えばよくなった」と捉えると分かりやすいです。
#58063 Test that reverse_merge takes the default from the other hash
マージ日: 2026/7/15 | 作成者: @55728
- 概要 (1-2文で)
Hash#reverse_merge/Hash#reverse_merge!が「レシーバではなく、引数側のハッシュのdefault/default_procを引き継ぐ」という既存仕様をテストで明示的に保証する PR です。実装コードの変更はなく、テスト追加のみです。
- 変更内容の詳細
何をテストしたか
reverse_merge と reverse_merge! の内部実装は、ざっくり言うと以下のようなイメージです。
def reverse_merge(other_hash)
other_hash.merge(self)
endこの実装だと、「結果ハッシュ」が other_hash をベースとして構築されるため、結果の default / default_proc も other_hash のものが引き継がれます。
PR の説明にある挙動は次の通りです。
left = Hash.new(:left)
right = Hash.new(:right)
left.reverse_merge(right)[:missing] # => :right
left.reverse_merge!(right)
left[:missing] # => :rightleft.reverse_merge(right)の戻り値は「右側 (right) を土台にしたハッシュ」
⇒ 未定義キー:missingの default は:right- 破壊的な
reverse_merge!も同様に、結果としてleftがrightの default を持つようになる
この挙動自体は元々そうなっていたものの、テストでカバーされていなかったため、今回の PR で以下を追加しています。
Hash#reverse_mergeが- 引数側ハッシュの
defaultを引き継ぐこと - 引数側ハッシュの
default_procを引き継ぐこと
- 引数側ハッシュの
Hash#reverse_merge!でも同様の挙動になること
対象はあくまで「プレーンな Hash の core extension」であり、HashWithIndifferentAccess には手を加えていません(こちらは独自の reverse_merge! / replace 実装と default のセマンティクスを持つため)。
テストファイルの変更:
activesupport/test/core_ext/hash_ext_test.rbに 26 行のテストコードを追加
- 影響範囲・注意点
- プロダクションコードの変更は一切なく「挙動を固定するためのテスト追加のみ」です。
- 既に動いていた仕様をテストで明文化しただけなので、Rails をアップデートしてもこの PR 自体による挙動の変化はありません。
- 逆に言うと、「
reverse_mergeはレシーバの default を保つだろう」と期待していた場合、その期待は間違いであり、この PR によって「今後もそのようにはならない」ことが契約としてより強く保証されます。- レシーバ側の default を維持したい場合は、
merge系を使う順序や実装を見直す必要があります。
- レシーバ側の default を維持したい場合は、
HashWithIndifferentAccessはこの PR の対象外なので、そのreverse_merge!の default の扱いは別物です。Hash前提のテストと混同しないように注意が必要です。
- 参考情報 (あれば)
- 該当 PR: #58063 “Test that
reverse_mergetakes the default from the other hash” - 関連議論: #58056(
reverse_mergeの default 挙動がテストされていないことが話題になったスレッド) - ActiveSupport の
Hash#reverse_mergeは、「レシーバよりも引数側を優先するmerge」という API ですが、default の引き継ぎ元も「引数側」である点が直感とズレる可能性があるため、この PR によるテスト追加は仕様理解の上でも参考になります。
#58129 Fix Rubocop violation in config/puma.rb - Approach 1
マージ日: 2026/7/15 | 作成者: @jeromedalbert
- 概要 (1-2文で)
Rails アプリ生成時のconfig/puma.rbテンプレートに含まれていた RuboCop (Layout/SpaceInsideArrayLiteralBrackets) 違反を修正し、rails new myapp --mainで作成したアプリでbin/rubocopやbin/ciが失敗しないようにした PR です。
前の PR (#57424) による変更で入り込んだスタイル違反を、テンプレート側で1行だけ修正しています。
- 変更内容の詳細
対象ファイル:
railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt変更点は1行のみで、「配列リテラルのブラケット内のスペース」を削除し、Rubocop の
Layout/SpaceInsideArrayLiteralBracketsに準拠するようにしています。具体的には、(イメージですが) 例えば:
ruby# 修正前(Rubocop違反) workers [ 2, 4 ]のように
[と]の内側にスペースが入っていたものを:ruby# 修正後(Rubocop準拠) workers [2, 4]のようにブラケット直後/直前のスペースを削除する形に直した、という内容です。
実際の行が何であれ、「ブラケット内の先頭/末尾の不要な空白をなくした」というのが本質的な変更です。
この PR は、別アプローチの PR (#58130) をクローズして採用された「Approach 1」の方であり、#57424 で導入された Rubocop 違反をピンポイントに修正するフォローアップになっています。
- 影響範囲・注意点
影響範囲:
rails new myapp --mainで生成されるconfig/puma.rbの内容がわずかに変わります(スタイルのみ)。- これにより、新規アプリで実行する:
bin/rubocopbin/ciが、デフォルト状態で Rubocop のスタイル違反によって落ちることがなくなります。
既存アプリへの影響:
- 既に生成済みアプリの
config/puma.rbは自動では変更されないため、既存アプリで同じ Rubocop 違反が残っている場合は、手動で同様の修正(ブラケット内の不要な空白削除)を行う必要があります。 - 実行時の挙動や Puma の設定値自体は一切変わらず、あくまでコードスタイルのみの変更です。
- 既に生成済みアプリの
Rubocop 設定との関係:
- デフォルトの Rails/Rubocop 設定に合わせた修正のため、独自に
Layout/SpaceInsideArrayLiteralBracketsを無効化・緩和していない限り、PRマージ後のテンプレートの方が「Rails 標準設定に沿った書き方」となります。
- デフォルトの Rails/Rubocop 設定に合わせた修正のため、独自に
- 参考情報 (あれば)
- 関連 PR:
- 元となる変更: https://github.com/rails/rails/pull/57424
- 代替案としてクローズされた PR: https://github.com/rails/rails/pull/58130
- Rubocop Cop ドキュメント:
Layout/SpaceInsideArrayLiteralBrackets
https://docs.rubocop.org/rubocop/cops_layout.html#layoutspaceinsidearrayliteralbrackets
#55410 Introduce ActionView::Helper::NavigationHelper
マージ日: 2026/7/15 | 作成者: @seanpdoyle
- 概要 (1-2文で)
button_to/link_to系のヘルパー(link_to_unless_current,link_to_unless,link_to_ifを含む)が、新設のActionView::Helpers::NavigationHelperモジュールに切り出されました。挙動の変更はほぼなく、主に内部構造の整理・責務分離が目的のリファクタリングです。
- 変更内容の詳細
新モジュール ActionView::Helpers::NavigationHelper
- これまで
ActionView::Helpers::UrlHelperに定義されていた以下のメソッドが、ActionView::Helpers::NavigationHelperに移動しました:link_tobutton_tolink_to_unless_currentlink_to_unlesslink_to_if
- それに付随する内部的な
privateメソッド群も、呼び出し元と同じモジュール(NavigationHelper)側に移動しています。
Rails 全体としては、ActionView::Helpers が ActionView::Base に mixin される構造は変わらないため、ビューからの呼び出し方は従来通りです。
<%= link_to "Home", root_path %>
<%= button_to "Delete", post_path(@post), method: :delete %>といったコードはそのまま動作します。
URL / ルーティング周りとの接続
action_dispatch/routing/url_for.rbなどの関連ファイルで、url_forとの結合を保つための微修正(1行レベルの変更)が入っています。- ルーティングガイド (
guides/source/routing.md) とヘルパーガイド (guides/source/action_view_helpers.md) も、内部構造の変更に合わせて記述が微調整されていますが、API の使い方は変わっていません。
テスト構成の変更
NavigationHelper用の新しいテストファイル:actionview/test/template/navigation_helper_test.rb(約 1000 行)
- 従来
UrlHelperに紐づいていたlink_to/button_to周りのテストは、ほぼそのままnavigation_helper_test側に移されており、url_helper_testからは同等量が削除されています。
→ 挙動はテストでしっかり担保されており、「URL 生成」と「ナビゲーション(リンク/ボタン)」のテストが分離された、という整理です。
Railtie / 設定まわり
actionview/lib/action_view/helpers.rbおよびactionview/lib/action_view/railtie.rbで、新モジュールが読み込まれるように 1〜2 行レベルの追記・差し替えが行われています。railties/test/application/configuration_test.rbも 6 行ずつの変更があり、NavigationHelperを前提にした設定テストへ更新されています。
- 影響範囲・注意点
アプリケーション開発者への影響
- 通常のアプリ/ビューコードで
link_to/button_toなどを使っているだけなら、基本的に影響はありません。 - パブリック API としてのメソッドシグネチャや、
link_to/button_toの仕様は変わっていません(PR 説明およびテスト量から見て、挙動互換を重視したリファクタリング)。
メタプログラミング・Monkey patch をしている場合の注意
以下に当てはまる場合は、影響が出る可能性があります。
ActionView::Helpers::UrlHelperに対してprepend/include/alias_method_chain的なパッチを当てて、link_to/button_toを上書きしているUrlHelper内部のprivateメソッドに直接依存していたActionView::Baseへの include 順序(どの Helper が先に読み込まれるか)に依存したコードを書いている
この PR では:
link_to/button_toの定義モジュールがUrlHelperからNavigationHelperに変わりました。- それに伴い、関連する
privateメソッドもNavigationHelper側に移っています。
そのため、例えば次のようなコードは動かなくなる(または想定外の場所にパッチがあたる)可能性があります:
# 以前:UrlHelper にパッチを当てていた
module MyLinkPatch
def link_to(name = nil, options = nil, html_options = nil, &block)
super("[patched] #{name}", options, html_options, &block)
end
end
ActionView::Helpers::UrlHelper.prepend(MyLinkPatch)上記のような場合、今後は NavigationHelper に対してパッチを当てる必要があります:
ActionView::Helpers::NavigationHelper.prepend(MyLinkPatch)private メソッド境界について
- PR 本文にもある通り、
ActionView::Helpersモジュールは最終的にすべてActionView::Baseに混ぜ込まれるため、Ruby のprivateスコープは実質あまり意味を持っていませんでした。 - 今回、
privateメソッドが呼び出されるモジュール内にきちんと移されたことで、「どのモジュールが何を責務として持つか」が多少クリアになっています。 - 内部実装に依存していた場合、メソッドの「属するモジュール」が変わる可能性がある点に注意が必要です。
- 参考情報 (あれば)
- PR 本体:
https://github.com/rails/rails/pull/55410 - 関連 PR(モチベーションで参照されているもの):
https://github.com/rails/rails/pull/50312#discussion_r2220564548 CHANGELOG追記:actionview/CHANGELOG.mdに、NavigationHelperの導入とlink_to/button_toの移動に関する記述が追加されています(バージョンアップ時の変更点として明記)。
#58126 [RF-Docs] [ci-skip] Update the Rails Generators guide (#57371)
マージ日: 2026/7/15 | 作成者: @p8
- 概要 (1-2文で)
Rails Generators ガイド(guides/source/generators.md)を大きく書き換え・再構成したドキュメント更新PRです。Rails Foundation のドキュメントプロジェクトの一環で、Generators の説明を最新かつ体系的な内容に整えることが目的です。
- 変更内容の詳細
※コード上の挙動変更ではなく、ガイド内容(文章・サンプルコード)の更新です。そのため、以下は「どういう知識が追加/整理されたか」という観点の解説になります。
ガイド全体の大幅な改訂
- 追加 317 行 / 削除 153 行と差分が大きく、章立てや説明順序を含めて再構成されています。
- Rails Generators の概要から、カスタムジェネレータの作り方、テンプレートやフックの仕組みまで、一連の流れで理解しやすい構成に整理されたと考えられます。
- ドキュメント品質向上(表現の統一、例のアップデート、不要な重複の削除など)が主眼です。
Rails Foundation ドキュメントプロジェクト対応
- PR タイトルにある通り、Rails Foundation によるドキュメント整備プロジェクトの一部です。
- 既存ガイド(別の Generator 関連ガイド)を統合して一つのガイドにまとめる作業の一環であり、全体的に他ガイドとの整合性が取られた形になっているはずです。
「Application Templates」「Rails Generator API」セクションの統合
- 説明文にもある通り、以下の 2 セクションは他ガイドからマージされており、この PR では「レビュー対象外」と明記されています。
- Application Templates
- Rails Generator API
- これらはすでに別途フルレビュー済みの内容であり、本 PR では「ジェネレータガイドに組み込んだ」ことが主な変更点です。
- 実質的には、Rails アプリケーションの雛形を自動生成・カスタマイズするためのテンプレート機能や、Generator クラス/API の詳細な使い方が、Generators ガイドの中で一貫して読めるようになったと考えられます。
- 説明文にもある通り、以下の 2 セクションは他ガイドからマージされており、この PR では「レビュー対象外」と明記されています。
サンプルコード・実例の刷新(推測される内容)
- 変更行数の多さとガイド性質から、以下のようなサンプルが整理・追加されている可能性が高いです:
rails generateコマンドの基本例bashbin/rails generate model Article title:string body:text- カスタムジェネレータの作成例ruby
# lib/generators/report/report_generator.rb class ReportGenerator < Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) def create_report_file template "report.rb.erb", "app/reports/#{file_name}_report.rb" end end source_root,template,copy_file,migration_templateなどの Thor::Actions を使ったファイル生成例- Hooks / invoke / revoke の使い方、Rails プロジェクト内でのジェネレータの拡張・上書き方法
- これらの例が、最新の Rails のディレクトリ構成やコマンド (
bin/rails) に合わせて整理されていると考えられます。
- 変更行数の多さとガイド性質から、以下のようなサンプルが整理・追加されている可能性が高いです:
文書スタイル・記述の更新
- Rails ガイド全般のスタイルガイドに沿って、見出し構造、注意書き、補足の書き方などが統一された可能性があります。
- 古くなった記述(古い Rails バージョン、
script/railsのようなレガシーなコマンド例など)が最新の表現に置き換わっていると思われます。
- 影響範囲・注意点
ランタイム挙動への影響はなし
- 変更ファイルはガイド (
guides/source/generators.md) のみであり、Rails 本体のコードやテストは変更されていません。 - そのため、既存アプリケーションやジェネレータの動作には影響しません。
- 変更ファイルはガイド (
開発者への影響(知識面)
- Generators 周りのドキュメントが整理・充実したことで、以下のような場面で参照価値が高まります:
- Rails 標準ジェネレータの挙動を正しく理解したいとき
- 自作ジェネレータや plugin のジェネレータを実装・メンテナンスしたいとき
- Application Template を使って社内テンプレートやブートストラッププロジェクトを整備したいとき
- 既存のジェネレータ関連記事・ブログと内容が食い違う場合は、本ガイドがより新しいソースとして参考になります。
- Generators 周りのドキュメントが整理・充実したことで、以下のような場面で参照価値が高まります:
レビュー対象外セクションへの注意
- 「Application Templates」「Rails Generator API」はこの PR ではレビュー対象外とされていますが、すでに別ルートでのレビューを経ているため、そのまま利用して問題ない前提です。
- もしこれらのセクションに関して疑問点や不整合を見つけた場合は、過去の関連 PR / issue を併せて確認すると経緯が追いやすいと思われます。
- 参考情報 (あれば)
- 該当ファイル:
guides/source/generators.md
- 関連トピック(Rails ガイド内想定):
- Rails コマンドライン (
rails/bin/rails) - Rails Engine / Plugin の作成
- Thor / Rails::Generators API
- Application Templates (
rails new my_app -m template.rbのような利用方法)
- Rails コマンドライン (
実務的には、「カスタムジェネレータを書きたい」「アプリケーションひな型を自動生成したい」「既存ジェネレータをフック/上書きしたい」といった用途の際に、このガイドの最新版を一度通読しておくと設計・実装のベース知識として有用です。
#57371 [RF-Docs] [ci-skip] Update the Rails Generators guide
マージ日: 2026/7/15 | 作成者: @bhumi1102
- 概要 (1–2文で)
Rails Foundation のドキュメント刷新プロジェクトの一環として、「Rails Generators ガイド(generators.md)」が大幅に書き直されました。既存内容の整理・拡張に加え、他ガイドから統合されたセクションを含めて、ジェネレータの使い方・カスタマイズ方法がより体系的に説明されています。
- 変更内容の詳細
※PR 本文から分かる範囲と、Rails の一般的なジェネレータ機構を前提にした解説です。実際の文面は guides/source/generators.md の最新版を確認してください。
全体構成の見直し・拡張
guides/source/generators.mdが +317 / -153 行と大きく書き換えられており、章構成や説明順序が整理された可能性が高いです。- 典型的には次のような構成に整理されていると考えられます:
- 基本的なジェネレータの使い方 (
rails generate/rails g) - Rails に用意されている主要なジェネレータ一覧
- 生成されるファイル・ディレクトリ構成の解説
- 独自ジェネレータの作成方法
- テンプレートのカスタマイズ
- (別ガイドから統合された)Application Templates
- (別ガイドから統合された)Rails Generator API
- 基本的なジェネレータの使い方 (
代表的な内容(想定されるアップデート点)
1) 基本的な rails generate の説明の拡充
- 典型的なコマンド例:bash
bin/rails generate model Article title:string body:text published:boolean bin/rails generate controller Articles index show new create bin/rails generate scaffold Post title:string body:text - これらが生成するファイルの一覧や、それぞれがどのレイヤ(モデル・コントローラ・ルーティング・ビュー・テスト・マイグレーション)に影響するか、といった説明がより明示的になっている可能性があります。
2) Rails 標準ジェネレータの整理
- 代表的なジェネレータ(例):
model,controller,scaffold,migrationjob,mailer,channel,taskなど
- それぞれのオプションや、
--skip-*系オプション(--skip-routes,--skip-migration,--no-test-frameworkなど)の使い方が、現行 Rails バージョンに合わせて更新されていると考えられます。
3) 独自ジェネレータ作成ガイドの改善
lib/generators配下に独自ジェネレータを作るパターンが、より丁寧に解説されていると考えられます。例:
lib/generators/admin_scaffold/admin_scaffold_generator.rbruby# lib/generators/admin_scaffold/admin_scaffold_generator.rb class AdminScaffoldGenerator < Rails::Generators::NamedBase source_root File.expand_path("templates", __dir__) def create_controller template "controller.rb.tt", File.join("app/controllers/admin", class_path, "#{file_name.pluralize}_controller.rb") end def create_views available_views.each do |view| template "views/#{view}.html.erb.tt", File.join("app/views/admin", file_name.pluralize, "#{view}.html.erb") end end private def available_views %w[index show new edit] end endNamedBase,Base,Thorとの関係、source_rootの指定、template,copy_file,directoryなどの Thor::Actions の利用例が具体的に示されている可能性があります。
4) テンプレートファイル (*.tt) の扱い
ERBベースのテンプレート (.rb.tt,.html.erb.ttなど) によるコード生成例が整理され、@class_name,file_name,class_pathなど、ジェネレータから利用できる変数の説明がされていることが多いです。例:
controller.rb.ttrubyclass <%= class_name.pluralize %>Controller < ApplicationController def index @<%= file_name.pluralize %> = <%= class_name %>.all end end
5) Application Templates セクションの統合
PR 説明によると、「Application Templates」セクションは別ガイドから移植され、既にレビュー済みのため今回再レビュー対象外です。
内容としては、
rails new実行時に-mオプションで Ruby スクリプトを指定し、アプリ生成プロセスをフックする機能の解説が含まれます。例:
bashrails new blog -m https://example.com/rails_template.rbテンプレート内での
generate,rails_command,gem,environmentといった DSL の使い方が記載されていることが多いです。
6) Rails Generator API セクションの統合
- こちらも別ガイドから統合され、既にレビュー済み。
Rails::Generators::BaseやRails::Generators::NamedBase、Rails::Generators::AppBaseなどのクラスと、利用可能なメソッドの一覧・意味がまとまっていると考えられます。argument,class_optioninvoke,invoke_allno_color!,remove_file,empty_directoryなど
- 「API」という名前から、単純な使い方だけでなく、内部的なフックポイントや、他のジェネレータを呼び出す方法 (
invoke "model", [name]など) も説明されている可能性があります。
- 影響範囲・注意点
影響範囲:
- コード自体には一切変更がなく、ガイド(ドキュメント)のみの変更です。
- 既存アプリやジェネレータの挙動には影響しません。
- ただし、「正しい/推奨」とされる使い方やベストプラクティスの説明が最新状態に更新されているため、今後の学習・社内標準ドキュメント等はこの新ガイドに合わせるのが望ましいです。
注意点:
- 「Application Templates」と「Rails Generator API」は既に別ガイドとしてフルレビュー済みの内容が統合されているため、今回の PR では主に周辺テキスト・構成だけが調整されている可能性があります。
- 以前に
application_templates.mdやgenerator_api.mdのような別ガイドを参照していた場合、今後はgenerators.md一つに統合されているかを確認してください。 - 古いブログ記事や Qiita 記事等と今回のガイド内容が食い違う場合は、Rails Guides の方が現時点での公式・最新情報です。
- 参考情報 (あれば)
- 該当ファイル:
guides/source/generators.md(Rails リポジトリ内)
- 関連ドキュメント(最新版 Rails Guides):
- Generators Guide: https://guides.rubyonrails.org/generators.html
- Application Templates: https://guides.rubyonrails.org/rails_application_templates.html (統合前の単独ガイドが残っている場合あり)
- ソースレベル API:
Rails::Generators名前空間のクラス群(railties/lib/rails/generators配下)を読むと、ガイド中の API 解説と対応が取りやすいです。
#58124 Fix Relation#one? doc example calling any?
マージ日: 2026/7/15 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Relation#one?のドキュメント中のサンプルコードが誤ってany?を呼んでいた問題を修正し、説明文やコメントの文言・整形も合わせて調整したドキュメント専用のPRです。コードの挙動自体には一切変更はありません。
- 変更内容の詳細
以下は、activerecord/lib/active_record/relation.rb 内の Relation#one? / Relation#any? に関するドキュメントコメントのみの修正です。
(1) one? のサンプルが any? を呼んでいたのを修正
元のドキュメント例(問題のある状態・イメージ):
# Returns true if there is exactly one record.
#
# posts.one?(Post) # => true if exactly one record
# posts.any?(Comment) # => falseone? の説明セクション内の2つ目の例が any? になっており、ドキュメントとして「one? の使い方」を示せていませんでした。
修正後はおそらく次のようになります(意図される形):
# Returns true if there is exactly one record.
#
# posts.one?(Post) # => true if exactly one record matches
# posts.one?(Comment) # => falseつまり、2つ目の行も one? を使うように変更されています。
(2) 「records matches」の文言を「record matches」に統一
any? / one? 双方の説明文にあった以下のような文言:
- "returns true if at least one records matches"
- "returns true if exactly one records matches"
のような、英語として不自然な「records matches」が、単数形で文法的に正しい
- "returns true if at least one record matches"
- "returns true if exactly one record matches"
といった形に修正されています。
これにより、any? / one? の説明がどちらも英語として正しく読みやすくなります。
(3) # => コメントの整列
any? の使用例の # => の位置と、one? の例の # => の位置がずれていたのを揃えています。例:
# before (イメージ)
posts.any?(Post) # => true
posts.any?(Comment) # => false
# after (イメージ)
posts.any?(Post) # => true
posts.any?(Comment) # => false※変更内容としてはスペース数の調整だけで、意味は変わりませんが、ドキュメントの可読性を上げています。
(4) any? ドキュメント内の重複した空コメント行を削除
any? のドキュメントコメント中に、# のみの空行が重複して存在していた箇所を1つ削除し、余計な空行をなくしています。これも振る舞いには影響せず、スタイル調整に留まります。
- 影響範囲・注意点
実行時の挙動への影響:
- Ruby / Rails のコードロジック (
Relation#one?,Relation#any?の実装) は一切変更されていません。 - 本PRは「ドキュメント専用」(
Documentation-only) です。
- Ruby / Rails のコードロジック (
影響範囲:
- Rails の API ドキュメントおよび
relation.rb内コメントを参照する開発者が対象です。 - 既に
one?/any?を利用しているアプリケーションコードに影響はありません。
- Rails の API ドキュメントおよび
注意点(ドキュメントを読む側として):
- これまでのドキュメントのまま読むと、
one?の説明セクションにany?の例が混ざっており、one?の挙動に誤解を招く可能性がありました。 - 今後のバージョンでは、
one?のセクションは正しくone?だけを例示する形になります。
- これまでのドキュメントのまま読むと、
- 参考情報 (あれば)
関連メソッドの概要:
Relation#any?- 条件にマッチするレコードが1件以上存在するかどうかを返します。
- 例:
posts.any?/posts.any?(Post)/posts.any? { |post| post.published? }
Relation#one?- 条件にマッチするレコードが「ちょうど1件」存在するかどうかを返します。
- 例:
posts.one?/posts.one?(Post)/posts.one? { |post| post.featured? }
PRメタ情報:
- PR番号: #58124
- マージ日時: 2026-07-15T12:05:58Z
- 変更ファイル:
activerecord/lib/active_record/relation.rbのみ(+4 / -5)
#58117 Raise for empty shards on connected_to_all_shards
マージ日: 2026/7/14 | 作成者: @eileencodes
- 概要 (1-2文で)
connected_to_all_shardsを、実際にはシャーディングされていないアプリ(shard 設定が空のケース)で呼び出した場合に、静かに動作するのではなくエラーを送出するように変更した PR です。
「シャーディングされていない構成でconnected_to_all_shardsを使う」ことを明示的に不正利用として扱う方向に仕様が整理されています。
- 変更内容の詳細
2-1. コアの仕様変更
対象: ActiveRecord::ConnectionHandling#connected_to_all_shards まわりの処理
これまで:
connected_to_all_shardsは、内部的にshard_keys(アプリケーションで定義されているシャードのキー一覧)を前提に動いていた。- しかしアプリ側が実際にはシャーディングされておらず、
shard_keysが空配列のままでも、エラーにはならず「何も接続を切り替えない」状態でブロックを実行できてしまう余地があった。
変更後:
- アプリがシャーディングされておらず
shard_keysが空の場合にconnected_to_all_shardsを呼び出すと、例外を発生させるようになった。 - 「デフォルト接続だけを対象に
connected_to_all_shardsが動く」ような仕様にはしない、という判断が明確にされた。
PR 説明文のポイント:
- 「アプリが sharded である」と言うためには、少なくとも 2 つの異なるスキーマ(もしくは DB)を参照する接続設定が必要:
- 1 つはルーター (router)
- 1 つは shard(DB の 1x パーティションを表す)
- それが満たされていない状態で
connected_to_all_shardsを使うのは誤用なので、黙って動かすのではなく「エラーで気付かせる」挙動にする。
疑似コードイメージ(簡略化・概念的なもの):
def connected_to_all_shards(role: ActiveRecord::Base.current_role, &block)
shard_keys = self.shard_keys_for(role) # 実際には内部的な取得処理
if shard_keys.empty?
raise ArgumentError, "connected_to_all_shards cannot be used because no shards are configured"
end
shard_keys.each do |shard_key|
connected_to(role: role, shard: shard_key) do
yield
end
end
end実際の PR では 4 行程度の軽微な変更ですが、ロジックとしてはおおよそ上記のようなチェックが追加されたと考えてよいです。
2-2. CHANGELOG の更新
activerecord/CHANGELOG.md に、以下のような内容が追記されています(意訳):
connected_to_all_shardsを、シャードが 1 つも構成されていないアプリケーションで呼び出した場合にエラーを発生させるようにした。
これにより、将来バージョンにアップデートした際に挙動変更に気付きやすくなっています。
2-3. テストの追加・修正
activerecord/test/cases/shard_keys_test.rb にテストが追加・修正されています。
主なポイント:
- シャードが定義されていない環境で
connected_to_all_shardsを呼んだときに、例外が発生することを検証するテストが追加。 - 既存の「シャードがある」ケースについても、今回の仕様変更を踏まえて期待挙動が確認されるように微調整。
- 影響範囲・注意点
3-1. 影響が出る可能性があるケース
以下に当てはまるアプリケーションは挙動が変わります:
database.yml(もしくは DB 設定)上は実際にはシャーディングをしていない:- シャード用の接続設定がない、あるいは 1 つしかない。
- それにもかかわらず、コード上で
connected_to_all_shardsを呼び出している。
このようなコードは、以前は「何も起きないけど一応動いていた」可能性がありますが、今後は 例外が発生してアプリケーションが落ちる ようになります。
典型的な問題例:
# 実際には shard 設定がないのに…
ActiveRecord::Base.connected_to_all_shards do
User.some_maintenance_task
end→ この PR 適用後は「シャードが設定されていない」旨のエラー(ArgumentError など)が発生するようになります。
3-2. どう直せばよいか
- そもそもシャーディングしていないアプリなら:
connected_to_all_shardsを使うのをやめる。- 代わりに通常の接続コンテキスト (
connected_toなし、あるいはconnected_to(role: ...)) を使う。
- 将来的にシャーディングを導入する予定で先行して書いていたコードなら:
- 実際に複数スキーマ/複数 DB のシャード設定を用意する。
- あるいは、導入前は以下のようにガードを入れておく:
if ActiveRecord::Base.respond_to?(:shard_keys) && ActiveRecord::Base.shard_keys.present?
ActiveRecord::Base.connected_to_all_shards do
# shard 前提の処理
end
else
# 非 sharded 環境用のフォールバック処理
endただし、shard_keys の公開インターフェースはバージョンによって異なる可能性があるため、実際にはリリースノートや API ドキュメントを確認した上で、利用できるメソッドに合わせてガードを書く必要があります。
3-3. ライブラリ・エンジン開発者への影響
- gem や Rails Engine で
connected_to_all_shardsを内部的に呼び出している場合:- 利用先アプリが non-sharded である可能性があるので、この変更によってエラーが出るようになります。
- そのため、「シャードが存在すること」を前提にするのか、「シャードが無い環境では別コードパスにする」のか を明確化し、条件分岐を追加する必要があります。
- 参考情報 (あれば)
- この PR は以下の issue をクローズしています:
https://github.com/rails/rails/issues/58114
→ 「シャードがない場合は default だけを対象に動いてほしいのでは?」という意見に対し、「その場合そもそもアプリは sharded ではないので、connected_to_all_shardsを成功させるのは不自然」という判断が示されています。 - 関連する API:
ActiveRecord::Base.connected_to_all_shards- マルチ DB / シャーディング周辺の設定 (
database.ymlのshards:セクションなど)
- 実運用上は、「シャーディングを使わないアプリでは
connected_to_all_shardsは使わない」という設計方針がより強くなった、と理解しておくとよいです。
#58055 Use NOT ENFORCED toggle in check_all_foreign_keys_valid! on PostgreSQL 18.4+
マージ日: 2026/7/14 | 作成者: @yahonda
- 概要 (1-2文で)
Rails のActiveRecord::ConnectionAdapters::PostgreSQLAdapter#check_all_foreign_keys_valid!が、PostgreSQL 18.4 以降ではNOT ENFORCED/ENFORCEDトグルを用いて外部キーの再検証を行うようになり、スーパーユーザ権限が不要になりました。これにより、disable_referential_integrityと同様に、テーブル所有者権限だけでテスト用フィクスチャの外部キー検証を行えるようになります。
- 変更内容の詳細
2-1. 従来の実装
これまで check_all_foreign_keys_valid! は PostgreSQL のカタログテーブルを直接書き換えていました。
pg_catalog.pg_constraint.convalidatedをfalseに更新- その後
VALIDATE CONSTRAINTを実行して再検証
といった流れで、ざっくりいうと:
UPDATE pg_catalog.pg_constraint
SET convalidated = false
WHERE contype = 'f' -- foreign key
...;
ALTER TABLE ... VALIDATE CONSTRAINT ...;しかし pg_catalog の直接更新はスーパーユーザ権限が必須のため、#57378 で disable_referential_integrity からはスーパーユーザ依存を排除したものの、check_all_foreign_keys_valid! だけが依然として superuser を要求していました。
2-2. 新しい実装(PostgreSQL 18.4+)
PostgreSQL 18.4 以降に対しては、外部キーの再検証に ENFORCED / NOT ENFORCED のトグルを利用します。
実行されることのイメージ
対象となる外部キー制約に対して:
sqlALTER TABLE schema_name.table_name ALTER CONSTRAINT constraint_name NOT ENFORCED;直後に:
sqlALTER TABLE schema_name.table_name ALTER CONSTRAINT constraint_name ENFORCED;ENFORCEDへ戻すタイミングで、既存行に対する外部キー制約チェックがネイティブに実行されます。- 違反があれば PostgreSQL 側でエラーが発生し、それが
ActiveRecord::InvalidForeignKeyとしてラップされます。
- 違反があれば PostgreSQL 側でエラーが発生し、それが
取得対象となる外部キーの絞り込み
新しい実装では、トグル対象とする外部キーを次の条件で絞り込みます:
contype = 'f'(外部キー)- すでに
ENFORCEDな制約のみ(NOT ENFORCEDなものは触らない) - 対象テーブルに対して
pg_has_role(relowner, 'USAGE')がtrueとなるもの- つまり、接続ユーザがテーブル所有者(もしくは所有ロールのメンバ)であるテーブルのみが対象
- 単に DML 権限だけを持つテーブルは対象外
これにより、実際に ALTER TABLE ... ALTER CONSTRAINT を実行できるテーブルだけをクエリの時点で選んでいます。
スキーマ/テーブル周りの扱い
- すべてのスキーマが対象(
search_pathによるフィルタはなし) ALTER TABLEの対象はスキーマ修飾済みで実行されるため、search_path依存性は解消されています- パーティションテーブルの場合は、子ではなく 親テーブルの制約 をトグルするようになっています
- PostgreSQL 18.3 以前では、従来の
UPDATE pg_catalog.pg_constraint ...VALIDATE CONSTRAINTのコードパスをそのまま維持(挙動変更なし)
2-3. ロック形態の変更
この PR によって、再検証処理で取得するロックが変わります。
- 旧実装:
VALIDATE CONSTRAINTがSHARE UPDATE EXCLUSIVEロックを取得 - 新実装:
ALTER TABLE ... ALTER CONSTRAINTがACCESS EXCLUSIVEロックを取得
ACCESS EXCLUSIVE はより強いロックで、DML/DDL をほぼブロックしますが:
- 呼び出し元はテスト環境のフィクスチャ検証のみ
disable_referential_integrityも #57378 以降同程度のロックを取る
という前提から、運用上の影響は小さいと判断されています。
- 影響範囲・注意点
3-1. 実行権限と対象テーブル
PostgreSQL 18.4+ 環境で check_all_foreign_keys_valid! を呼ぶとき:
- これまでは「スーパーユーザなら全テーブルの外部キーを検証」
- 今後は:
- 非スーパーユーザでも動作する
- ただし 接続ユーザが所有していないテーブル の外部キーは検証されない
スーパーユーザで接続していれば、pg_has_role の条件をすべて満たすため、従来どおり全テーブルがカバーされます。
マルチスキーマ構成や、アプリ用ロールとスキーマ所有ロールが分離されている環境の場合:
- 「テスト用 DB ユーザが対象テーブルの owner ではない」構成だと、18.4+ では外部キー検証の対象から外れる可能性があります
- テストで全ての外部キーを検証したい場合:
- テーブル所有者として接続する
- もしくは所有ロールのメンバーになるようロール構成を見直す
ことを検討してください(所有権自体は GRANT できない点に注意)。
3-2. PostgreSQL バージョン依存
- PostgreSQL 18.4 以上:
- 新しい
NOT ENFORCEDトグル方式が使われる - スーパーユーザ不要、テーブル所有者権限で十分
- 新しい
- PostgreSQL 18.3 以下:
- 実装は従来どおり
pg_catalog.pg_constraintへの直接 UPDATE を伴うため、superuser が基本前提のまま
アプリケーション側から見える API (check_all_foreign_keys_valid! のメソッドシグネチャや例外種別) は変わりませんが、必要な DB 権限とロックの振る舞いがバージョンによって異なる点は意識しておく必要があります。
3-3. ロックによるテスト実行時間・ブロッキング
テスト環境であっても、DB を並列に共有しているようなケース(他プロセスが同じ DB にアクセスしている CI 構成など)では:
ACCESS EXCLUSIVEロックにより、一時的に他のクエリがブロックされる可能性があります- ただし呼び出しはフィクスチャ検証時に限定されているため、通常の Rails テスト構成では大きな問題にはなりにくい想定です
- 参考情報 (あれば)
- 元 PR:
- #58055 "Use NOT ENFORCED toggle in check_all_foreign_keys_valid! on PostgreSQL 18.4+"
- 関連 PR:
- #57378:
disable_referential_integrityから superuser 依存を排除した変更 - #57428:
conrelid::regclassのsearch_path依存性に関する議論(今回の 18.4+ 経路ではALTER TABLEがスキーマ修飾済みになったため、この問題は解消)
- #57378:
- 関連テスト:
activerecord/test/cases/adapters/postgresql/referential_integrity_test.rbactiverecord/test/cases/fixtures_test.rb- PostgreSQL 18.4(非スーパーユーザ含む)と 17.10 で動作検証済み
#58111 Fix the broken #with reference
マージ日: 2026/7/13 | 作成者: @codergeek121
概要 (1–2文で)
Rails の Active Record の#with_recursiveメソッドのドキュメント内で、壊れていた#withへの参照リンクが修正されました。コードの挙動には一切影響せず、ドキュメント上のリンク誤りのみを直す変更です。変更内容の詳細
- 対象ファイル:
activerecord/lib/active_record/relation/query_methods.rb - 変更内容は 1 行の置換のみで、
#with_recursiveのドキュメントコメント中に書かれていた#withへの参照形式(rdoc用のリンク記法)が誤っていたため、正しい形に修正されています。
イメージとしては、以下のようなコメント行が:
# Similar to #with_recursive, see #with for non-recursive CTEs.のように書かれていて、#with への参照が RDoc 上でリンク切れになっていたため、例えば:
# Similar to #with_recursive, see Relation#with for non-recursive CTEs.のようにクラス名付きにする、あるいは RDoc が解釈可能な形(#with → with メソッドシグネチャ形式など)に書き換えられた、という種類の修正です。
※正確な文字列は PR 本文からは分かりませんが、「参照の形式」を 1 箇所修正しているだけです。
実装コード (def with_recursive ... end やクエリロジック) には手が入っていません。
- 影響範囲・注意点
- 影響範囲:
- Rails API ドキュメント (RDoc / api.rubyonrails.org 等) を閲覧した際に、
#with_recursiveの説明から#withへのリンクが正しく飛べるようになります。 - ランタイムの挙動、パフォーマンス、互換性には一切影響しません。
- Rails API ドキュメント (RDoc / api.rubyonrails.org 等) を閲覧した際に、
- 注意点:
- 既存アプリケーション側で対応すべき変更はありません。
- CI などでドキュメント生成をしている場合も、生成物のリンクが正しくなるだけでビルド手順の変更は不要です。
- 参考情報 (あれば)
ActiveRecord::QueryMethods#with/#with_recursiveは CTE (Common Table Expression) を扱うためのメソッドで、例:rubyPost.with( popular_posts: Post.where("likes_count > 100") ).from("popular_posts AS posts")のように非再帰 CTE を
withで定義したり、rubyCategory.with_recursive( ancestors: Category.where(id: 10), union: Category.joins("JOIN ancestors ON categories.parent_id = ancestors.id") ).from("ancestors")のような再帰 CTE を
with_recursiveで扱うときに利用します。
この PR は、こうしたメソッドの使い方をドキュメント上で辿りやすくするための、リンク修正のみのメンテナンス変更です。
#58110 Remove unneeded parenthesis [ci-skip]
マージ日: 2026/7/13 | 作成者: @claudiob
- 概要 (1-2文で)
Railsのガイド「product_reviews.md」に記載されていたコード例から、不要な括弧(parenthesis)を取り除くドキュメント修正PRです。アプリの挙動には影響せず、ガイドのコードスタイル・可読性を改善する目的の変更です。
- 変更内容の詳細
- 対象ファイル:
guides/source/product_reviews.md - 変更内容: 1行のコード例について、Ruby的に不要な括弧を削除して、より慣習的な書き方に修正。
実際の差分は以下のようなイメージです(擬似例):
- User.create!(name: "Alice", email: "alice@example.com")
+ User.create! name: "Alice", email: "alice@example.com"もしくは、メソッド呼び出しで余計な括弧を外す形:
- deploy_to_production(app_name: "my_app")
+ deploy_to_production app_name: "my_app"PRタイトルから読み取れる通り、ロジックや手順自体ではなく、「見た目とスタイル」だけの変更です。
説明文中のリンク:
See https://edgeguides.rubyonrails.org/product_reviews.html#deploying-to-production
から、この括弧は「Deploying to production」節内のコード例に含まれていたと考えられます。
- 影響範囲・注意点
- 影響範囲:
- Rails本体のコード・動作には一切影響しません。
product_reviewsガイドを見てコードを書く開発者に対して、よりRubyらしい記法を示すようになります。
- 注意点:
- 既にガイドを見ながら同じ括弧付きのコードを書いている場合でも、そのコードは通常そのまま動作します(Rubyでは多くの場合、括弧は任意)。
- チームのスタイルガイドによっては「括弧を明示したい」場合もあるので、ガイドの記法と自分たちのプロジェクトのスタイルをどう合わせるかは別途検討が必要です。
- 参考情報 (あれば)
- 対象ガイド:
- Edge Guides: Product Reviews – Deploying to production
https://edgeguides.rubyonrails.org/product_reviews.html#deploying-to-production
- Edge Guides: Product Reviews – Deploying to production
- Rubyスタイルガイド(括弧利用の一般的な慣習に関する参考):
- https://rubystyle.guide/
- メソッド呼び出しにおける「括弧は必要な場合のみにする」スタイルがよく推奨されています。
- https://rubystyle.guide/
#57967 Change the order of Views and Controllers in the guides
マージ日: 2026/7/13 | 作成者: @p8
- 概要 (1-2文で)
Rails Guides のナビゲーション順序を、従来の「MVCのV→C」から「リクエストライフサイクル (route → controller → view → response)」に合わせて整理し直した PR です。具体的には、コントローラ関連ガイドをビューより前に出し、ルーティングとコントローラをまとめて扱う形に変更しています。
- 変更内容の詳細
この PR で変更されているのは guides/source/documents.yaml 1ファイルのみで、Rails Guides の「どのガイドをどの順番で表示するか」を定義している YAML の順序・名称が調整されています。
主なポイントは以下です。
(1) Guides の章構成の順序変更
これまで:
- 「Action View のガイド」が「Action Controller のガイド」より前に記載されていた
- これは MVC のアルファベット順 (M→V→C) や、概念的な構成としては自然だったが、
- 実際には一部の Action View ガイドでコントローラやルーティングの知識が前提になっていた
変更後:
- 「Controllers」に相当するセクションを、「Views」より前に移動
- そのうえで、ルーティングのガイドを各種コントローラガイドよりさらに前に配置
→ 読む順番が「Routing → Controllers → Views」となるように整理
Xavier による提案どおり、
request lifecycle order: route -> controller -> view -> response
にガイド構成を揃える形になっています。
(2) セクション名の変更: 「Controllers」→「Routing and Controllers」
- Guides のメニュー上での章タイトルが
- 旧:
Controllers - 新:
Routing and Controllers
- 旧:
- これに合わせて、同セクションの中に
- Routing のガイド
- Controllers 関連のガイド群 がまとめられるように順番・階層が調整されています。
(3) YAML 上の変更イメージ
PR では具体的な YAML が全文は示されていませんが、概ね以下のような差分が入っていると考えられます (擬似コード):
# 変更前(イメージ)
- name: Action View Overview
...
- name: Action Controller Overview
...
- name: Routing
...
# 変更後(イメージ)
- name: Routing and Controllers
children:
- name: Routing
...
- name: Action Controller Overview
...
- name: Other Controller Guides
...
- name: Action View Overview
...実際の diff は「+26 / -23」と行数は小さく、構造と並び順、見出しのラベル変更が中心です。
アプリケーションコードや API には一切変更がありません。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは Rails Guides (公式ドキュメントサイト) のメニュー構成と章タイトル のみです。
- Rails 本体の挙動、API、DSL、設定ファイル等には変更はありません。
- これにより、
- これから Rails Guides を読む人は、自然に
- ルーティング
- コントローラ
- ビュー の順で読むことになり、学習順序がリクエストの流れと揃うため、理解しやすくなります。
- 一方で、「以前は View ガイドが先に出てきた」ことを前提にしたブログ記事などが、画面キャプチャや手順の説明と微妙に合わなくなる可能性があります(位置が変わるだけで内容は同じ)。
- これから Rails Guides を読む人は、自然に
注意点
- ドキュメント内リンク・外部リンク:
- もし
guides.rubyonrails.orgの章名を直接文字列で参照している解説サイトがある場合、- 表示されるセクションのタイトルが「Routing and Controllers」に変わるため、文面の整合性が崩れることがあります。
- ただし、URL パス自体 (
/routing.htmlなど) が変わるという情報はないため、リンク切れのリスクは低いと考えられます。
- もし
- 社内資料等で「Rails Guides の『Controllers』セクションを参照」といった説明をしている場合は、
- 「Routing and Controllers」に文言を更新しておくと混乱を防げます。
- 参考情報 (あれば)
- PR 本文での議論の要点:
- Action View ガイドの一部がコントローラ・ルーティングの前提知識を要求しており、読者にとって学習順序が不自然になっていた。
- MVC の順序にこだわるよりも、「実際の HTTP リクエストの流れに合わせた構成」の方が理解しやすい、という判断。
- 関連しそうな公式ドキュメント:
- Rails Guides:
- Routing: https://guides.rubyonrails.org/routing.html
- Action Controller Overview: https://guides.rubyonrails.org/action_controller_overview.html
- Action View Overview: https://guides.rubyonrails.org/action_view_overview.html
- Rails Guides:
この PR 自体はドキュメント構成のみの変更のため、アプリケーション側でのコード修正やバージョン対応は不要です。
#57966 Comment ellipsis in guides examples
マージ日: 2026/7/13 | 作成者: @p8
- 概要 (1-2文で)
Rails Guides のコード例中に素の三点リーダ...をそのまま書かないようにし、多くをコメントアウトされた# ...に統一した PR です。Ruby では...が有効な構文であるため、ガイドのコードをコピペしたときに意図せず動作してしまう/構文エラーになる問題を避けることが目的です。
- 変更内容の詳細
背景となる問題
Rails Guides にはコード例で「中略」を表すために
...が使われている箇所があるRuby では
...は「3 点リーダレンジ演算子」として有効な構文であり、たとえば:ruby(1...5) # => 1,2,3,4 の範囲そのため、ガイドのサンプルコードをそのままコピーして実行すると、
- 「ここは適当に補ってね」という意味の
...が Ruby のコードとして解釈されてしまう - 望まない挙動や構文エラーにつながる可能性がある
- 「ここは適当に補ってね」という意味の
この PR は、ガイド上の「中略表示」と、実際に実行されうる Ruby コードを明確に分離するための修正です。
具体的な修正パターン
主な方針は以下です:
コード例中の省略を表す
...はコメントアウトする例:
rubydef some_method ... endのようなものを:
rubydef some_method # ... endのように変更。
コマンド出力例など、「コピペしてもそもそも動かない」部分では従来通り
...を使うことを許容rails g model ...のような「サンプル出力」「ターミナルの出力例」では...を維持してよい、というガイドラインに整理。
変更ファイルごとの内容イメージ
PR の diff から読み取れる意図レベルの説明になります:
guides/source/active_storage_overview.md- Active Storage の使用例コード内にあった素の
...を# ...へ変更 - 例: モデル定義やコントローラ例で「省略」を示す行がコメント化されていると考えられる
- Active Storage の使用例コード内にあった素の
guides/source/api_documentation_guidelines.md- API ドキュメント向けの例示コード内の
...の扱いを修正 - ドキュメントに掲載するサンプルコードでも、
...をコメントで示す、というスタイルに合わせた可能性が高い
- API ドキュメント向けの例示コード内の
guides/source/engines.md- Engine 関連のサンプルコード (
routes.rbやEngineクラス定義の例など) の省略部を# ...へ
- Engine 関連のサンプルコード (
guides/source/form_helpers.mdフォームヘルパーの利用例コード(フォームビルダ、ビューサンプルなど)での
...を同様にコメントアウトERB 例であれば:
erb<%= form_with(model: @post) do |form| %> ... <% end %>を
erb<%= form_with(model: @post) do |form| %> <%# ... %> <% end %>のように「ERB コメント」に変更している可能性もあります(ERB 中では
# ...ではなく<%# ... %>が適切)。実際の diff ではコンテキストに応じたコメント記法が選ばれているはずです。
guides/source/plugins.md- プラグイン作成例のコード中の省略を表現する
...をコメントに変更
- プラグイン作成例のコード中の省略を表現する
guides/source/ruby_on_rails_guides_guidelines.md- Rails Guides 執筆者向けガイドライン自体に、「省略を表すときの書き方」のルールを追加
- 追加された内容の意図:
- Ruby コード例では、可読性のための省略はコメントとして記述すること
- 実行可能なコードブロックに素の
...を書かないこと - Rails コマンドの出力例など、実行しないことが前提のブロックでは通常の
...を使ってもよい - これを示す具体例(ビフォー/アフターのサンプルコード)がガイドラインに追記されている
サンプルコードの一例(イメージ):
```ruby
class User < ApplicationRecord
# ...
end
ガイドラインではこのような形の例を推奨していると考えられます。
---
3. 影響範囲・注意点
- **影響範囲**
- 変更対象は Rails Guides(ドキュメント)のみであり、Rails 本体のコードや挙動には影響しません。
- ただし、今後ガイドを書く・更新する人にとってはスタイルガイドがアップデートされるため、執筆時に従うべきルールが明確になります。
- **注意点(ガイド執筆者・メンテナ向け)**
- Ruby コードブロック、ERB テンプレート例など、**ユーザーがそのままコピーして実行しうるコード**には、素の `...` を書かないこと
- Ruby: `# ...` などのコメントで表現
- ERB: `<%# ... %>` のように ERB コメントを使う
- `rails new`, `rails g`, `bin/rails` などの**コマンド出力例・ログ出力例**は、コピーしても実行できないことが明らかなので、従来通り `...` を使ってもよい
- 既存のガイドのメンテ時にも、`...` を見つけたら「コードとして解釈されるか?」を確認し、必要に応じてコメント化することが望ましい
- **読者側の注意点**
- この変更により、ガイドに載っているコードをコピー&ペーストしても、`...` による予期しない Ruby 構文解釈が起こりにくくなる
- ただし、`# ...` となっている行は「ここに何らかのコードが入る」という意味であり、そのまま実行しても**本来意図している完全な例ではない**ことに注意する必要があります
---
4. 参考情報 (あれば)
- Ruby における三点リーダレンジ (`...`) と二点レンジ (`..`) の違い:
- `1..5` は 1〜5 を含む範囲
- `1...5` は 1〜4 を含む範囲(終端を含まない)
- Rails Guides 執筆ガイドライン(`ruby_on_rails_guides_guidelines.md`)は、ガイド作成時のスタイル・コード例の書き方を決める基準となるため、ドキュメントに PR を出す場合はいちど目を通しておくとよいです。
---
## [#58098](https://github.com/rails/rails/pull/58098) Fix settings navigation test in authentication guide {#pr-58098}
**マージ日**: 2026/7/13 | **作成者**: [@Lemmah](https://github.com/Lemmah)
1. 概要 (1-2文で)
認証ガイド内のシステムテスト例において、「Store Settings」がリンクではなく見出し(`h4`)としてレンダリングされている実際のマークアップに合わせて、テストのセレクタを修正したドキュメント変更です。これにより、一般ユーザーと管理者ユーザーのテスト例の整合性が取り戻され、意図した挙動を正しく検証できる内容になっています。
---
2. 変更内容の詳細
対象: `guides/source/sign_up_and_settings.md` のテストコード例が 1 行修正されています。
元のガイドでは、「一般ユーザーは “Store Settings” を見られないこと」を検証する例として、おおよそ次のようなコードになっていました:
```ruby
# (修正前のイメージ)
assert_not_dom "a", "Store Settings"しかしガイド中の UI マークアップでは、「Store Settings」はリンク (<a>) ではなく見出し (<h4>Store Settings</h4>) として表示されているため、このセレクタは実際の HTML と整合していませんでした。
PR では、これを次のように修正しています:
# (修正後)
assert_not_dom "h4", "Store Settings"背景として:
- ガイドのサンプル UI では、「Store Settings」は
<h4>見出しとして描画される。 - 一般ユーザー用のテストでは「Store Settings が見えないこと」を検証したい。
- 管理者ユーザー用のテストでは、すでに「
h4セレクタで Store Settings を確認する」ように書かれており、こちらはマークアップと一致していた。
にもかかわらず、一般ユーザー側だけ a セレクタを使っていたため、次のような問題がありました:
- 実際の画面には
<a>Store Settings</a>はそもそも存在しない(一般ユーザーでも管理者でも)。 - そのため「一般ユーザーで
assert_not_dom "a", "Store Settings"が通る」のは、UI が正しいからではなく、「そもそも<a>がないから」通ってしまっていた。 - 本来検証したいのは「一般ユーザーには
<h4>Store Settings</h4>が出ない」という点なので、テストが意図どおりの挙動を検証していなかった。
この PR により、一般ユーザーのテスト例も管理者ユーザーのテスト例も、どちらも h4 を対象にすることで、ガイドに示されているマークアップとテスト内容が一致するようになっています。
- 影響範囲・注意点
- 影響範囲:
- Rails 本体のコードには一切影響せず、「認証ガイド(sign_up_and_settings.md)」内のサンプルテストコードのみの変更です。
- 実プロジェクトの動作やテストには直接影響しませんが、このガイドをコピペして使っている場合は、
aではなくh4を対象にしているか確認したほうがよいです。
- 注意点:
- UI 実装側で「Store Settings をリンクに変更した」など、実際のマークアップが ガイドと異なる場合は、自分のアプリのマークアップに合わせてセレクタを調整する必要があります。
- 「リンクテキストの有無を検証したい」のか、「見出しの有無を検証したい」のか、意図に応じて
assert_dom,assert_not_domのセレクタを選ぶ、というテスト設計上のポイントを示す変更でもあります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58098
- 対象ガイド: Authentication guide –
sign_up_and_settings.md(「Store Settings」セクションのシステムテスト例) - ガイドのスクリーンショットによると、「Store Settings」は
<h4>要素として描画されていることが確認されています。
#58101 Downcase Cache-Control key in asset pipeline guide example
マージ日: 2026/7/13 | 作成者: @meganemura
概要 (1-2文で)
Asset Pipeline ガイド内のサンプルコードにおけるCache-Controlヘッダー名を、Rack 3 の仕様に合わせて小文字の"cache-control"に修正したドキュメント更新です。実装コードではなく、ガイドの表記を現状のジェネレータ出力と整合させることが目的です。変更内容の詳細(あればサンプルコードも含めて)
対象:
guides/source/asset_pipeline.mdの「CDNs and the Cache-Control Header」セクション内のコード例変更前の例(イメージ):
rubyconfig.public_file_server.headers = { "Cache-Control" => "public, max-age=31536000" }変更後の例:
rubyconfig.public_file_server.headers = { "cache-control" => "public, max-age=31536000" }文字としての HTTP ヘッダー名を説明する本文中では従来どおり
Cache-Controlと表記し、コード例のみ Rack 3 SPEC に準拠した小文字キーに揃えています。背景として、#52653 で
config/environments/development.rb.ttとtest.rb.ttのテンプレートが"cache-control"に変更されており、新規アプリの実際の生成コードとの不整合を解消する修正です。
- 影響範囲・注意点
- 影響範囲:
- Rails ガイド(ドキュメント)のみであり、フレームワーク本体の動作や API には変更はありません。
- ただし、ガイドを参考に環境設定を書いている開発者にとって、Rack 3 仕様に即した正しい例が示されるようになります。
- 技術的なポイント・注意:
- Rack 3 ではレスポンスヘッダーは小文字キーで扱うことが要求される(仕様)ため、Rails のジェネレートされる環境ファイルも小文字
"cache-control"を使用するようになっています。 - HTTP ヘッダー名自体はプロトコル上は大文字・小文字を区別しませんが、Rack 3 のインターフェース要件に合わせる必要があります。
- 既存アプリで
"Cache-Control"をキーに使っていても、Rack 2 環境では通常問題になりませんが、Rack 3 への移行時には小文字化を検討するのが無難です(この PR はそのベストプラクティスの周知にもつながります)。
- Rack 3 ではレスポンスヘッダーは小文字キーで扱うことが要求される(仕様)ため、Rails のジェネレートされる環境ファイルも小文字
- 参考情報 (あれば)
- 対応元 Issue/PR: #52653(Rack 3 SPEC に合わせて環境テンプレートのレスポンスヘッダーキーを小文字化した変更)
- Rack 3 SPEC(概要):
- レスポンスヘッダーは
Hash<String, String>で、キーは小文字の ASCII 文字列であることが求められる。
- レスポンスヘッダーは
- 実務的には、Rails 7 以降で Rack 3 を使う、または今後のアップグレードを見据える場合、
config.public_file_server.headersやミドルウェアで設定するヘッダー名は小文字に揃えておくとよいです。
#58096 Read foreign key names from the schema on SQLite3
マージ日: 2026/7/12 | 作成者: @55728
- 概要 (1-2文で)
SQLite3 アダプタでforeign_keys(table)が外部キー制約名を正しく取得するようになり、remove_foreign_key ..., name: ...が意図した制約だけを削除できるように修正されています。これにより、テーブル再構築時もカスタム外部キー名が維持され、スキーマダンプにも正しく反映されます。
- 変更内容の詳細
背景・従来の挙動
SQLite3 では、テーブル定義に以下のように名前付き外部キーを定義していても:
add_foreign_key :crews, :ships, column: "ship_id", name: "fk_ship"
add_foreign_key :crews, :ships, column: "backup_ship_id", name: "fk_backup_ship"foreign_keys("crews") から取得できる ActiveRecord::ConnectionAdapters::ForeignKeyDefinition は、name が常に nil になっていました。
その結果:
foreign_keys("crews").map(&:name)
# before: [nil, nil]
# after : ["fk_ship", "fk_backup_ship"]remove_foreign_key(:crews, name: "fk_ship")
→ 名前で一致させられないため、最初に見つかった外部キーを削除してしまうremove_foreign_key(:crews, name: "nope", if_exists: true)
→ 「見つからなかったので何もしない」ではなく、やはり任意の1件を削除してしまうadd_foreign_keyやadd_columnでテーブル再構築(ALTER TABLE→ 一時テーブルへのコピー → 再作成)すると、foreign_keysの情報を元に再定義されるが、nameがnilのため、すべて自動生成名fk_rails_...に置き換わる- スキーマダンプ (
schema.rb) 時も、カスタム名が失われてしまう
という問題がありました。
今回の修正内容
1) SQLite3 adapter で制約名をスキーマから取得
SQLite3 のテーブル定義には、以下のような形で制約名が書かれます:
CONSTRAINT "fk_ship" FOREIGN KEY ("ship_id") REFERENCES "ships"(...)
CONSTRAINT "fk_backup_ship" FOREIGN KEY ("backup_ship_id") REFERENCES "ships"(...)この PR では、SQLite3 アダプタの foreign_keys(table_name) 実装を修正し、この CONSTRAINT "..." 部分から制約名をパースして ForeignKeyDefinition#name にセットするようになりました。
その結果:
foreign_keys("crews").map(&:name)
# before: [nil, nil]
# after : ["fk_ship", "fk_backup_ship"]となり、他の DB アダプタ(MySQL/PostgreSQL)と同様に、foreign_keys の戻り値に正しい名前が入るようになります。
2) SQLite3 の remove_foreign_key が :name を正しく使うように
これまで SQLite3 専用の remove_foreign_key 実装では、引数で渡された name: オプションが内部のマッチロジックに正しく伝搬しておらず、「名前指定しても最初の外部キーを削除する」挙動になっていました。
この PR では、抽象アダプタ(他 DB と共通の実装)と同様に、remove_foreign_key が :name をきちんとマッチ条件として使用するように変更されています。
挙動の比較:
| 操作 | 変更前 | 変更後 |
|---|---|---|
remove_foreign_key(:crews, name: "fk_ship") | 最初の外部キーを削除(任意) | "fk_ship" の外部キーだけを削除 |
remove_foreign_key(:crews, name: "nope", if_exists: true) | いずれかの外部キーを削除してしまう | 対象がないので no-op(削除されない) |
3) テーブル再構築時にカスタム名を維持
SQLite3 では ALTER TABLE の制約が厳しいため、カラム追加などの際に実質的にテーブルを「作り直す」処理(旧テーブルから一時テーブルへコピー → 新テーブルを作成 → データを戻す)を行います。
この際、外部キーも foreign_keys の戻り値を元に再作成されます。
今回の修正で foreign_keys に正しい name が入るようになったため:
- 変更前:
外部キー再作成時にnameがnil→ Rails が自動生成名fk_rails_...を付与 → カスタム名が失われる - 変更後:
元のカスタム名(例:"fk_ship")がそのまま再作成時に使われる → 名前が維持される
4) スキーマダンプでの挙動
SchemaDumper には fk_ignore_pattern があり、自動生成の外部キー名(fk_rails_*)はスキーマダンプから意図的に省かれます。
今回の変更で:
- 自動生成名
fk_rails_*
→ これまで通りダンプから除外(挙動変わらず) - カスタム名(例:
"fk_ship")
→ これまでnameがnilだったため落ちていたが、今後は正しい名前付きでadd_foreign_keyがschema.rbに出力されるようになる
これにより、「明示的に付けた外部キー名」がスキーマダンプ経由で正しくラウンドトリップするようになります。
- 影響範囲・注意点
- 対象:
- SQLite3 を使用しているアプリケーションのみ
- 明示的に
name:を指定してadd_foreign_keyしているケース remove_foreign_key ..., name: ...を利用しているケース
- 期待される改善:
remove_foreign_key(name: ...)が誤った外部キーを削除してしまうバグが解消される- テーブル再構築(
add_columnなど)によってカスタム外部キー名がfk_rails_*に変わってしまう問題が解消される schema.rbへのスキーマダンプで、カスタム名の外部キーが正しく出力される
- 互換性・注意点:
- 「これまでバグに依存して動いていたコード」がある場合、挙動が変わる可能性があります
- 例:
nameが存在しない前提で「最初の外部キーが削除される」ことに依存していた処理(通常はそういった依存は望ましくない)
- 例:
- スキーマダンプの内容が変わる可能性があります
- 以前は無名扱いで出力されていなかった外部キーが、「カスタム名を持つ外部キー」として
add_foreign_keyに名前付きで記録されるようになります
- 以前は無名扱いで出力されていなかった外部キーが、「カスタム名を持つ外部キー」として
- 「これまでバグに依存して動いていたコード」がある場合、挙動が変わる可能性があります
- テスト:
activerecord/test/cases/migration/foreign_key_test.rbにテストが追加・更新されており:- 外部キー名の取得
remove_foreign_key(name: ...)の挙動- スキーマダンプ・テーブル再構築時の名前保持
などがカバーされています。
- 参考情報 (あれば)
対応するファイル変更:
activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
→ 外部キーの読み取り・削除の挙動修正activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
→ スキーマ関連の細かいロジック修正activerecord/test/cases/migration/foreign_key_test.rb
→ 上記挙動を検証するテスト追加・更新activerecord/CHANGELOG.md
→ 振る舞い変更が changelog に追記
運用上のポイント:
- SQLite3 + 外部キーを積極的に使う開発・テスト環境では、この修正により本番(MySQL/PostgreSQL)との挙動差がさらに減少します。
- 既存アプリでは、アップデート後に
schema:loadやマイグレーションを走らせたときのschema.rb差分や、remove_foreign_keyの影響範囲を軽く確認するのがおすすめです。
#58064 Carry the form attribute onto a multiple select's hidden field
マージ日: 2026/7/12 | 作成者: @55728
- 概要 (1-2文で)
multiple: trueなselectヘルパが生成する「値クリア用の hidden フィールド」にも、form:オプションで指定したform属性を引き継ぐようにした変更です。これにより、<form>要素の外にあるmultiple selectでも、全ての選択を外したときに意図通り「空配列」が送信されるようになります。
- 変更内容の詳細
これまでの挙動
select ヘルパで multiple: true を指定すると、Rails は以下の 2 つの要素を出力します。
<%= select :post, :category, [], {}, { multiple: true, form: "my_form" } %>従来の出力(Before)は:
<input type="hidden" name="post[category][]" value="" autocomplete="off">
<select multiple="multiple" name="post[category][]" id="post_category" form="my_form"></select>ポイント:
selectにはform="my_form"が付く- しかし、ペアの hidden フィールドには
form属性が付かない
HTML の仕様上、<form> 要素の外側に置いたコントロールは、form 属性で対象フォームを明示しないと、そのフォームの送信対象になりません。
そのため、select 自体は my_form に紐づく一方、hidden フィールドはどのフォームにも紐づかず、全ての選択を外したときに送られるはずの「空の値」が my_form に送信されない状況になっていました。
変更後の挙動
この PR では、multiple select に付与された form 属性を hidden フィールドにもコピーするように変更しています。
After:
<input type="hidden" name="post[category][]" value="" autocomplete="off" form="my_form">
<select multiple="multiple" name="post[category][]" id="post_category" form="my_form"></select>結果として、以下が保証されます:
selectが<form id="my_form">の外にあってもform: "my_form"を指定していればselectと hidden の両方が同じフォームmy_formに紐づく- 全てのオプションを非選択にすると、hidden が空の値
post[category][]= ""をmy_formに送信してくれる
実装的なポイント
- 変更ファイル:
actionview/lib/action_view/helpers/tags/select_renderer.rbmultiple select用の hidden フィールド生成時に、form属性も他の属性と同様に渡すよう 1 行修正
- テスト:
actionview/test/template/form_options_helper_test.rbform: "my_form"指定時に hidden フィールドにもform="my_form"が付与されることを検証するテストを追加 (+8 行)
他ヘルパとの整合性
この修正は、既存の他のヘルパと仕様を揃えるものです。
check_box- hidden フィールド生成時に
"name", "disabled", "form"を引き継いでいる
- hidden フィールド生成時に
collection_check_boxes/collection_radio_buttons- hidden フィールドに
form: @html_options[:form]を指定している
- hidden フィールドに
multiple select だけが hidden 側に form を付けていなかったため、今回の変更で一貫性が取られました。
- 影響範囲・注意点
影響があるケース:
multiple: trueなselectを- 実際の
<form>要素の外に配置し form: "some_form_id"を使ってフォームに紐付けている場合
このケースで、全ての選択を外したときにも確実にパラメータが空配列として送信されるようになります。
具体的には、以前は何もパラメータが来なかったケースで、今後はpost[category]=>[""]→ コントローラ側でパラメータの正規化ロジックによって「選択なし」として扱われます(従来の multiple select + hidden の一般的な挙動)。formオプションを指定していない通常のケース:formがnilの場合は、ヘルパが属性を出力しないため HTML 出力はこれまでと完全に同一- つまり、既存のフォームレイアウト/サーバサイド処理への影響はありません
後方互換性:
- 修正は「hidden に
form属性を足すだけ」の純粋に加算的な変更であり、既存挙動を壊すような変更は行っていません
- 修正は「hidden に
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58064
- 関連する内部実装:
actionview/lib/action_view/helpers/tags/select_renderer.rbactionview/lib/action_view/helpers/tags/check_box.rbactionview/lib/action_view/helpers/form_options_helper.rb(collection 系ヘルパ)
- HTML 仕様:
form属性- フォームコントロール要素 (
input,select,textareaなど) はform属性でフォームと紐付け可能 - 要素が
<form>の外にあっても、form属性で指定されたフォームの送信時に値が送られる
- フォームコントロール要素 (
#58099 Refactor assume_migrated_upto_version
マージ日: 2026/7/12 | 作成者: @fxn
- 概要 (1-2文で)
ActiveRecord::Migration#assume_migrated_upto_versionの内部実装を、主に変数名とテーブル名の扱いの整理によってリファクタリングしたPRです。挙動の変更はなく、読みやすさ・一貫性・将来の保守性を高めるための内部改善です。
- 変更内容の詳細
背景: migrated という変数名の問題意識
もともとこのメソッド内には以下のようなコードがありました:
migrated = migration_context.get_all_versionsget_all_versionsは schema_migrations テーブルに記録されているバージョン一覧を返すメソッドです。- しかし、典型的な利用シナリオである「DBリセット直後」は、
db/schema.rbの内容は「どこまでマイグレーション済みか」を表している一方で、schema_migrationsテーブルは空 という状態になります。
- このとき
migratedは空配列ですが、変数名からは「すでにマイグレーションされたバージョン群」を意味しそうで誤解を招きます。
そのため、この PR では変数名を「schema_migrations テーブルに入っているバージョン」であることが明確な versions_in_schema_migrations に変更しています。
これにより、「実際に何が migrate 済みか」という論理概念と、「schema_migrations に記録されている値」という物理的な状態を別物として捉えやすくなっています。
作者が NOTE で触れている通り、db/schema.rb の :ruby フォーマットは「何が migrate 済みか」という履歴情報を完全には保持しておらず、structure.sql のような情報量はありません。この変数名変更は、その根本的な問題を意識しつつ、少なくともコード上の意味づけをクリアにする意図があります。
変更点1: migration context 経由で schema_migrations テーブル名を取得
これまでは、assume_migrated_upto_version の中で schema_migrations テーブルの名前を取得する際に、migration context とは別の経路を使っていました。
- この PR では migration context が提供する API を一貫して使うように変更 しています。
- 目的:
- アクセス経路を統一し、コードの読みやすさ・整合性を向上。
- 余分な文字列生成(allocation)も減るが、ここではパフォーマンスより一貫性が主目的。
結果として、「このメソッドは migration context を出入り口として動く」という構造がより明確になります。
変更点2: テーブル名の変数とクオート処理の整理
以前のコードでは:
sm_tableという変数名が使われていたが、これは「テーブル名そのもの」ではなく「クオート済みのテーブル名」を保持していました。- 一方で SQL の中では
- バージョン値(
versionカラム)にはクオートを施しているのに、 - テーブル名はすでにクオート済みの文字列をそのまま使う という状態で、一見したときに何がどこでクオートされているかが分かりづらくなっていました。
- バージョン値(
この PR では:
- 変数には「生のテーブル名」を保持するようにし、
- SQL を組み立てる際に、
- テーブル名もバージョン値も同じ場所・同じレイヤでクオートする
- という形にリファクタリングしています。
これにより:
- 変数名から「これはテーブル名か/クオート済み文字列か」が判断しやすくなります。
- SQL 組み立ての責務が明確になり、「どこでクオートされるか」がコードリーディング時に追いやすくなります。
変更点3: versions_in_db_migrate など、より説明的な変数名
このメソッド内には複数の「バージョンの集合」が登場します。今回のリファクタでは、それぞれの意味を明確にするために名称を整理しています。
- 旧:
versions - 新:
versions_in_db_migrate
→db/migrate配下に存在するマイグレーションファイルから得られるバージョン集合であることを示す。
既に触れた versions_in_schema_migrations と対になる命名で、
- 「ファイルシステム(db/migrate)上に存在するマイグレーション」
- 「DBの schema_migrations テーブルに記録されているマイグレーション」
を区別しやすくしています。
さらに、
versions_to_insertという名前も導入・整理されており、- 「これから schema_migrations に INSERT される対象のバージョン集合」 であることが直感的に分かるようになっています。
この命名パターンにより、set 演算(差集合・和集合など)を読むときの意図が格段に把握しやすくなります。
- 影響範囲・注意点
- 変更ファイルは
activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rbのみで、追加 13 行・削除 11 行の小規模なリファクタです。 - 公開 API (
assume_migrated_upto_version) のインターフェイスや挙動には変更がありません。- 既存アプリケーションがこのメソッドを利用している場合も、そのまま動作します。
- 変更内容は変数名やクオートの扱いといった内部実装の整理が中心であり、互換性上の懸念はほぼありません。
- 一方で、
schema.rb(:ruby フォーマット) が「何が migrate 済みか」という履歴情報を明示的には保持しないという構造的な制約には手を付けていません。- すなわち、この PR は「その制約のもとでコードを読みやすくした」ものであり、「履歴情報をどこかに残す」ような機能追加ではありません。
- 参考情報 (あれば)
- 対象メソッド:
ActiveRecord::Migration#assume_migrated_upto_version- 主な用途:
- 既存の DB に対して、過去のマイグレーションを「すでに適用済み」とみなして
schema_migrationsを埋める - DB リセット後に
db/schema.rbベースで状態を作り直しつつ、古いマイグレーションを再実行しないようにする など
- 既存の DB に対して、過去のマイグレーションを「すでに適用済み」とみなして
- 主な用途:
- 関連する話題:
db/schema.rb(:ruby) vsstructure.sql- :ruby は DB に依存しない形でスキーマを表現するが、何がどの順序で migrate されたかまでは保持しない
structure.sqlは DB の実際の構造や一部メタ情報も含めてダンプするため、「何が migrate 済みか」に近い情報をより忠実に残せる
この PR はそうした設計上の背景を意識しつつ、「少なくともコードから意味を誤読しないようにする」ためのリファクタと位置づけられます。
#58077 Strip only the commented line when detecting a LIMIT in rails query
マージ日: 2026/7/12 | 作成者: @55728
- 概要 (1-2文で)
rails query --sqlコマンドで SQL 内のLIMIT有無を判定する際、行コメント-- ...の処理が誤ってその後ろの全体を巻き込んでしまい、本来存在するLIMITを見落として二重にLIMITを付けてしまう不具合を修正した PR です。コメント除去の正規表現を修正し、「コメント行だけ」を正しく取り除いてからLIMITを検出するようにしています。
- 変更内容の詳細
問題となっていた挙動
rails query --sql は次のような流れで SQL を処理します:
- ユーザ入力の SQL からコメントを除去
- 残った SQL に
LIMIT句があるかを正規表現で検出 LIMITがなければデフォルトのLIMIT 101を付加
このとき、コメント除去に使われていた「行コメント --」用の正規表現が以下のような状態でした(説明用・擬似コード):
sql_without_comments = sql.gsub(/--.*$/m, "")ここで /m フラグが付いているため、. が改行も含めてマッチするようになっていました。その結果:
-- a comment
SELECT * FROM posts ORDER BY id LIMIT 2に対して --.*$ が
-- a comment\nSELECT * FROM posts ORDER BY id LIMIT 2全体を貪欲にマッチしてしまい、クエリ本体ごと削除されていました。
そのため、内部的には「LIMIT を含まない空 SQL」とみなされ、LIMIT 101 が追加されて最終的に:
SELECT * FROM posts ORDER BY id LIMIT 2 LIMIT 101のような不正な SQL が生成されていました。
修正内容
PR ではこの行コメント用の正規表現を修正し、「コメント行だけ」を削除するようにしています。
要点は:
/mフラグ利用時でも、--行コメントが「その行の終端まで」にしかマッチしないようにパターンを変更- これにより、改行をまたいでクエリ本体まで巻き込まない
実際のコード差分は 1 行の置き換えのみですが、ロジックとしては:
- 「
--からその行の終わりまで」を削除する - 改行文字をまたがないようにする(
[^\\n]などで制約する形)
という方向の修正です。
テストの追加
railties/test/commands/query_test.rb に 8 行のテストが追加されています。
テスト内容(要約):
def test_limit_is_not_duplicated_when_preceded_by_line_comment
sql = <<~SQL
-- a comment
SELECT * FROM posts ORDER BY id LIMIT 2
SQL
# rails query --sql が内部的に構築する最終的な SQL を検査
# 期待値: 既存の LIMIT 2 が尊重され、追加の LIMIT は付かない
endといった形で、
rails query --sql $'-- a comment\nSELECT * FROM posts ORDER BY id LIMIT 2'のケースでも、LIMIT 2 のみで実行されることを検証しています。
- 影響範囲・注意点
影響範囲
- 対象はあくまで
rails query --sqlCLI コマンドの内部処理です。 - アプリケーションコードでの Active Record の
limitや Arel には影響しません。 - 「SQL 文字列に
--行コメントが含まれていて、その後にLIMITが続く」ようなケースでの動作が正しくなります。
- 対象はあくまで
期待できる改善点
- コメント付きの SQL を
rails query --sqlに食わせても、不要な二重LIMITによる構文エラーが発生しなくなります。 - 特に、開発時のデバッグ・チューニングなどで、生 SQL をコピペしながらコメントを入れて試すワークフローが安定します。
- コメント付きの SQL を
注意点 / 想定される互換性
- もともと不正な動作(本来存在する
LIMITを見落としていた)を正した変更のため、「正しく書かれた SQL」の挙動が変わることは基本的にありません。 - 逆に、「今までバグに依存していた」ようなケース(例えば、コメントの後ろにあえて
LIMITを書いても無視されることを前提にしていた)では挙動が変わりますが、そのような使い方は通常想定されません。
- もともと不正な動作(本来存在する
- 参考情報 (あれば)
PR 本文に記載の再現コマンド:
bashrails query --sql $'-- a comment\nSELECT * FROM posts ORDER BY id LIMIT 2'- 修正前:
... LIMIT 2 LIMIT 101が生成され、DB が構文エラー - 修正後: 既存の
LIMIT 2が検出され、追加のLIMITは付加されない
- 修正前:
関連する箇所の関心ごと:
- コメント除去と
LIMIT検出は単純な正規表現ベースで行われており、SQL パーサではありません。 - 今後他のコメント形式(
/* ... */など)との絡みや、サブクエリ中のLIMIT等についても、正規表現の扱いには注意が必要です。
- コメント除去と
#58091 Honor if_not_exists: in SQLite3 add_check_constraint and add_foreign_key
マージ日: 2026/7/12 | 作成者: @55728
- 概要 (1-2文で)
SQLite3 アダプタのadd_check_constraint/add_foreign_keyが、オプションif_not_exists: trueを正しく解釈していなかった問題を修正し、既に存在する制約がある場合は何もせず(no-op)に済むようになりました。これにより、他のアダプタ(MySQL、Trilogy)やremove_*系メソッドとの挙動の一貫性が取れ、ロールバック時の再実行も安全になります。
- 変更内容の詳細(あればサンプルコードも含めて)
何が問題だったか
Rails のマイグレーションで以下のようなコードを書いたとします:
add_check_constraint :users, "age >= 0", name: "users_age_check", if_not_exists: true
add_foreign_key :comments, :posts, if_not_exists: true抽象アダプタ(MySQL, Trilogy など)では、同じ名前・同じ定義の制約が既に存在する場合は no-op となり、エラーも重複定義も起きません。
しかし SQLite3 アダプタでは:
- メソッドオーバーライドに
if_not_exists:が引数として宣言されていなかった - そのため
if_not_exists:は単に**optionsに埋もれて無視されていた - 結果として、同一制約が既に存在しても 重複して追加されてしまう 挙動になっていた
同時に、remove_check_constraint / remove_foreign_key の SQLite3 側オーバーライドは if_exists: をちゃんと解釈しており、ここだけ挙動が非対称でした。
今回の修正内容
activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb での変更が本体です。
ポイント:
add_check_constraintとadd_foreign_keyの SQLite3 実装にif_not_exists:を明示的な引数として追加- 実行前に「同じ制約が既に存在するか」をチェックし、存在する場合は 早期 return するように変更
- 外部キーについては、抽象アダプタと同様に、
foreign_key_optionsのロジックを使ってデフォルト列(通常は参照先テーブルのidカラム)を解決してから比較を行う- これにより、「同じテーブルだけど別カラムを参照する外部キー」は別物として扱われ、重複とみなさずに追加される
概念的なイメージ(※擬似コード):
def add_foreign_key(from_table, to_table, **options, if_not_exists: false)
if if_not_exists && foreign_key_already_exists?(from_table, to_table, options)
return # ここで no-op
end
# ここから従来通りの追加処理
super
endadd_check_constraint も同様に、既存のチェック制約の存在チェックを行い、見つかれば no-op にしています。
ロールバックの再実行(rollback replay)の修正
Rails のマイグレーションレコーダは、以下のように逆操作を自動生成します:
remove_foreign_key(..., if_exists: true)の逆操作としてadd_foreign_key(..., if_not_exists: true)を吐く
SQLite3 アダプタでは if_not_exists: が無視されていたため、ロールバックを繰り返し実行すると同じ外部キーが何度も追加されてしまいました。
修正後は if_not_exists: true が正しく働くため、ロールバックを何度繰り返しても 外部キーが重複しない・idempotent な挙動になります。
テストとドキュメント
check_constraint_test.rbとforeign_key_test.rbに、それぞれif_not_exists:を伴うケースのテストが追加されていますCHANGELOG.mdに今回の挙動修正が追記されています
- 影響範囲・注意点
- 対象:
- SQLite3 を使っている Rails アプリ
- マイグレーションで
add_check_constraint/add_foreign_keyをif_not_exists: true付きで利用している場合
- 期待される挙動の変化:
- 以前:
- SQLite3 だけ
if_not_exists: trueが効かず、同じ制約/外部キーが何度でも追加されていた
- SQLite3 だけ
- 今後:
- 「本当の意味で同一」の制約/外部キーが既に存在する場合は no-op になる
- 参照先テーブルは同じでも 参照カラムが違う外部キーは別物として追加される
- 以前:
- マイグレーション互換性:
if_not_exists:を使っていない既存のマイグレーションの挙動は 一切変わりません- 既に DB 内に重複する制約・外部キーが作られてしまっている場合:
- この PR 適用以降は、さらに重複が増えることはなくなりますが、
- 既存の重複定義を整理したい場合は、必要に応じて手動で削除(または専用マイグレーション)する必要があります
- 他アダプタとの一貫性:
- MySQL や Trilogy の挙動、および SQLite3 の
remove_*系メソッドと揃ったため、 - 「どの DB アダプタでも
if_not_exists:/if_exists:を使えば idempotent なマイグレーションが書ける」という前提がより信頼できるようになりました。
- MySQL や Trilogy の挙動、および SQLite3 の
- 参考情報 (あれば)
- 該当 PR: Honor
if_not_exists:in SQLite3add_check_constraintandadd_foreign_key(#58091) - 関連する ActiveRecord コード:
ActiveRecord::ConnectionAdapters::AbstractAdapter#add_foreign_keyActiveRecord::ConnectionAdapters::SQLite3::SchemaStatements
- マイグレーションにおける存在チェック系オプション:
add_*系:if_not_exists: trueremove_*系:if_exists: true
これらを組み合わせることで、「同じマイグレーションを何度実行しても壊れない」スクリプトが書きやすくなります。
#58092 Accept Date and numeric inputs in relative_time_in_words
マージ日: 2026/7/12 | 作成者: @55728
- 概要 (1-2文で)
relative_time_in_wordsヘルパーが、ドキュメント上は受け付けることになっていたDateや数値(秒数)を実際には扱えずArgumentErrorを出していた問題を修正した PR です。distance_of_time_in_wordsと同様に、引数を事前にTimeに正規化してから過去/未来の判定を行うように変更されています。
- 変更内容の詳細
問題点の整理
relative_time_in_words は以下のような処理をしていました(説明文内の擬似コード):
def relative_time_in_words(from_time, options = {})
now = Time.now
time = distance_of_time_in_words(from_time, now, options.except(:scope))
key = from_time > now ? :future : :past # <- ここで raw 引数を比較している
...
enddistance_of_time_in_words自体は、以下のような多様な型を受け付ける実装になっています。TimeDateDateTimeInteger(秒数)
- しかし
relative_time_in_words内では、正規化前の引数 (from_time) をTime.nowと直接比較して、:past/:futureどちらの翻訳キーを使うかを決めていました。 - その結果、Ruby の比較規則により、以下のようなエラーが出ていました:
relative_time_in_words(Date.tomorrow)
# => ArgumentError: comparison of Date with Time failed
relative_time_in_words(180)
# => ArgumentError: comparison of Integer with Time failedドキュメントでは time_ago_in_words と同様に Date や数値も受け付けると書かれているため、仕様と実装が食い違っている状態でした。
修正内容
この PR では、distance_of_time_in_words が既に使っているプライベートメソッドnormalize_distance_of_time_argument_to_time を利用して、relative_time_in_words の先頭で引数を Time に正規化するようにしています。
要するに、過去/未来の判定:
key = from_time > now ? :future : :pastに使われる値も、distance_of_time_in_words と同じ正規化ロジックを通った Time オブジェクトになるように変更されています。
これにより:
relative_time_in_words(Date.tomorrow) # OK
relative_time_in_words(180) # OK (今から180秒後 / 前)
relative_time_in_words(Time.current) # もともとOK
relative_time_in_words(DateTime.now) # もともとOKといった、ドキュメントに記載されているすべての入力型が、エラー無しで動作します。
テストとドキュメント
actionview/test/template/date_helper_test.rbに、Dateと数値入力を含むテストが追加されています。actionview/CHANGELOG.mdに、この挙動修正についてのエントリが追加されています。
- 影響範囲・注意点
- 対象:
ActionView::Helpers::DateHelper#relative_time_in_wordsを利用しているコード。 - 影響:
- これまで
DateやIntegerを渡すとArgumentErrorになっていたケースが、期待通りに動くようになります。 Timeを渡していた既存コードの挙動は、distance_of_time_in_wordsと同じ正規化ルールに沿った比較になるため、基本的には後方互換です。
- これまで
- 注意点:
distance_of_time_in_wordsと同じ変換ルール(タイムゾーンの扱いやDateの 00:00 化など)に依存するため、境界時刻(例: 日付のみを渡した場合の日付変更線付近)での「過去/未来」判定が、「生の値」で比較していた場合と微妙に異なる可能性はあります。ただし、これまでそもそもArgumentErrorで動作していなかった入力型に対する差分なので、実害はほぼありません。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58092
- 関連メソッド:
ActionView::Helpers::DateHelper#relative_time_in_wordsActionView::Helpers::DateHelper#time_ago_in_wordsActionView::Helpers::DateHelper#distance_of_time_in_words
#58093 Decouple schema loading from the versions formatter
マージ日: 2026/7/12 | 作成者: @fxn
- 概要 (1-2文で)
Rails 8.1 で導入された「version formatter」の契約(インターフェース)から、schema.rbローダー(:rubyローダー)の要件が漏れ出していた問題を解消するため、スキーマ読み込み処理とバージョンフォーマッタを疎結合にする変更です。schema.rbローダーとstructure.sqlダンパーが同じ内部 SQL 生成ロジックを共有していたのをやめ、それぞれの責務を分離しています。
- 変更内容の詳細
背景
Rails には2種類のスキーマダンプ方式があります:
schema.rb(:rubyローダー / ダンパー)structure.sql(:sqlダンパー)
どちらの場合も、スキーママイグレーションテーブル(通常は
schema_migrations)を「どのバージョンが適用済みか」で埋める必要があります。これまでは、ActiveRecord の内部にある「マイグレーションバージョンの SQL を生成するプライベートメソッド」が
:rubyローダー側: 生成した SQL をexecuteして実行:sqlダンパー側: 生成した SQL をstructure.sqlに追記 という形で共用されていました。
Rails 8.1 で「version formatter」が導入され、ユーザーが「どのような SQL で
schema_migrationsにバージョンを登録するか」をカスタマイズできるようになりましたが、もともとこれはstructure.sql生成を主眼にした設計です。しかし実装上は、同じ SQL 生成ロジックを
:rubyローダーも使っていたため、version formatter の契約が「schema.rbローダーからも呼ばれることを前提にしないと壊れる」状態になっていました(= 責務の漏れ・インターフェースの汚染)。
今回の変更の趣旨
「
schema.rbローダー側はローダー専用の内部 SQL を使い、version formatter はstructure.sqlダンパー専用の契約として扱う」
という形に変えて、スキーマ読み込み処理(ローダー)と version formatter を疎結合にすることが目的です。
実際のコード変更のイメージ
変更ファイルは1つだけです:
activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb(+3/-1)
差分は小さいですが、やっていることは概ね以下のようなことです(擬似コードイメージ):
# 以前: ローダー・ダンパー共通の SQL 生成ロジック
def sql_for_recording_schema_migration(versions)
# version formatter を通して SQL を生成
end
def load_schema_from_ruby
# ここからも上の共通 SQL 生成を呼んで execute していた
end
def dump_schema_information_to_sql
# 同じメソッドを通して SQL を structure.sql に書き込んでいた
end
# 今回: ローダーとダンパーを分離
def load_schema_from_ruby
# ローダー専用の、より直接的な SQL / 挿入処理を使う
# (version formatter 非依存)
end
def dump_schema_information_to_sql
# こちらは version formatter と連携して SQL を生成し、
# structure.sql に書き込む
end実際には3行の追加と1行の削除のみですが、
- 「ローダー側で共通の SQL 生成メソッドを呼ばないようにする」
- もしくは「ローダー専用の小さな SQL 片(またはメソッド)を持たせる」
といった形で、ローダーが version formatter 契約に依存しないようにしています。
- 影響範囲・注意点
影響範囲
一般的な Rails アプリケーション(デフォルト設定のみ使用)
- 動作上の差異はほぼありません。
schema.rb/structure.sqlの生成・ロードは、体感上これまで通りに動きます。
独自の version formatter を実装しているプロジェクト
- 一番影響を受ける可能性があるのはここです。
- これまでは暗黙のうちに「
schema.rbローダーからも呼ばれることがある」前提になっていたかもしれませんが、今後は「structure.sqlダンパー専用」としての契約に整理されます。 - つまり:
- formatter は「
structure.sqlに出力される SQL をどうフォーマットするか」だけを考えればよくなり、 schema.rbロード時の挙動や、データベースに対する直接executeの側面は考えなくてよい、という整理がされます。
- formatter は「
注意点
- この PR 自体は非常に小さな差分で、**現時点では破壊的変更というより「将来の契約の明確化のための内部整理」**に近いです。
- 作成者コメントにもあるように、version formatter の正式な契約仕様は別パッチでドキュメント化される予定です。
- version formatter を自作している場合は、そのドキュメントが公開されたタイミングで、
- 「
structure.sql生成時だけを対象としているか」 - 「ローダー向けの挙動に依存していないか」 を確認することをおすすめします。
- 「
- 参考情報 (あれば)
関連コード(8.0.0 時点の旧実装の参考)
https://github.com/rails/rails/blob/v8.0.0/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb#L1877-L1888関連 PR(リークした契約の問題指摘)
https://github.com/rails/rails/pull/58089
#58087 Fix MySQL POINT and MULTIPOINT columns being misreported as integers
マージ日: 2026/7/11 | 作成者: @kyuuri1791
- 概要 (1-2文で)
MySQL のPOINT/MULTIPOINTカラムが ActiveRecord 上で誤って整数型として扱われていた不具合を修正し、他の空間型と同様に「未知の型」として扱うようにした PR です。これにより、スキーマ情報の取得やスキーマ変換ツール利用時に不正な integer 定義が紛れ込む問題を防ぎます。
- 変更内容の詳細
問題の内容
MySQL で以下のようなテーブルを作成した場合:
connection = ActiveRecord::Base.lease_connection
connection.execute("CREATE TABLE spots (loc POINT, locs MULTIPOINT, shape POLYGON)")
columns = connection.columns("spots").index_by(&:name)
columns["loc"].type # => :integer (sql_type: "point") ← 誤り
columns["locs"].type # => :integer (sql_type: "multipoint") ← 誤り
columns["shape"].type # => nil (sql_type: "polygon") ← 未知の型として扱われているPOINT / MULTIPOINT という型名に "int" という文字列が含まれているため、AbstractAdapter 側の汎用ルール %r(int)i にマッチしてしまい、整数型として解釈されていました。
一方、POLYGON など他の空間型はこのルールにマッチせず、適切に「未知の型」(nil や ActiveRecord::Type::Value)として扱われています。
修正方針
- 汎用ルール
%r(int)i自体は他のアダプタ(特に SQLite の type affinity 対応など)で意図的に使われているため、ここは変えない。 - 代わりに、MySQL 系アダプタ用の抽象クラス
AbstractMysqlAdapter内で、POINT/MULTIPOINTを明示的に「未知の型」として登録することで上書き(オーバーライド)する。 - MySQL 用の 2 つのアダプタ(mysql2 / trilogy)で共通の挙動になるよう、抽象アダプタ (
AbstractMysqlAdapter) に実装を追加。
実際の変更点
1) AbstractMysqlAdapter#initialize_type_map の拡張
activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb にて、型マップ初期化処理に POINT / MULTIPOINT を登録する行が追加されています。イメージとしては以下のようなコードになります(実際のコードは多少文脈が付きますが、意味としてはこれと同じです):
def initialize_type_map(m = type_map)
super
# 既存の MySQL 固有型の登録に加えて……
register_class_with_limit m, %r(char)i, Type::String
# などなど…
# POINT / MULTIPOINT を未知の型 (Type::Value) として扱う
m.register_type(/point/i, ActiveRecord::Type::Value.new)
m.register_type(/multipoint/i, ActiveRecord::Type::Value.new)
endこれにより、POINT / MULTIPOINT は %r(int)i による整数型マッチよりも優先され、ActiveRecord::Type::Value として処理されます。
2) テスト追加
activerecord/test/cases/adapters/abstract_mysql_adapter/spatial_type_test.rb にテストが追加されています。
テストでは、POINT と MULTIPOINT のカラムが:
sql_typeとしてpoint/multipointを持ち- ActiveRecord の
columns(...).typeなどを通じて「integer ではない(= 数値系 type にマップされていない)」 - 他の空間型同様に
Type::Valueとして扱われる
ことを確認しています。
3) CHANGELOG 更新
activerecord/CHANGELOG.md にバグ修正としてエントリが追加され、今回の変更がアプリケーション挙動に影響しうることが明記されています。
- 影響範囲・注意点
影響を受けるケース
- MySQL を使っており、テーブルに
POINT/MULTIPOINT型カラムを定義しているアプリケーション。 - これまで
columns[name].type == :integerで判定していたようなコードがある場合、今回の修正により:integerではなく「未知の型」扱いとなり、挙動が変わります。- 例: 「整数カラム一覧」を作るロジックに
POINT/MULTIPOINTが紛れ込んでいた場合、今後はそこから除外されます(これは本来の正しい挙動)。
- 例: 「整数カラム一覧」を作るロジックに
- MySQL を使っており、テーブルに
ActiveRecord 上での扱い
POINT/MULTIPOINT向けの専用型(例えば PostGIS のようなジオメトリ型)が提供されるわけではなく、あくまで 「未知の型(Type::Value)」として扱うだけ です。- つまり、ActiveRecord 経由でこれらを扱う場合は、文字列やバイナリとして自前でパースする等の処理は引き続き必要です。
- ただし少なくとも「暗黙に integer とみなされる」という誤った状態は解消されます。
スキーマ・マイグレーション系ツールへの影響
- PR の動機にもある通り、MySQL → PostgreSQL のスキーマ変換ツールなどで、
POINT/MULTIPOINTが integer と誤認識された結果、変換先スキーマが壊れる問題が解消されます。 - ActiveRecord ベースで schema dumper / loader を実装しているツールでも、カラム型判定ロジックに依存している場合は挙動が変わり得ますので、一度確認するとよいです。
- PR の動機にもある通り、MySQL → PostgreSQL のスキーマ変換ツールなどで、
- 参考情報 (あれば)
PR 本文にあるツール:
- MySQL → PostgreSQL スキーマ同期ツール: https://github.com/kyuuri1791/schema_ferry
このツールの開発中に、POINT/MULTIPOINTが integer と誤検出される問題として発見されたものです。
- MySQL → PostgreSQL スキーマ同期ツール: https://github.com/kyuuri1791/schema_ferry
関連するコンセプト:
- ActiveRecord の type map (
initialize_type_map) は、DB の生のsql_type(例:"point","varchar(255)")を Ruby 側の型クラス (ActiveRecord::Type::Integer,ActiveRecord::Type::String,ActiveRecord::Type::Value等) にマッピングする仕組みであり、今回の修正はそのマッピングルールの一部を MySQL 専用に上書きしたものです。 %r(int)iのようにアンカーなし・大文字小文字無視の正規表現を使っているため、POINT/MULTIPOINTのような「int を部分的に含む型名」が誤マッチしうる、という背景知識があると理解しやすいです。
- ActiveRecord の type map (
#58076 Interpolate a fixture label verbatim, without backreference expansion
マージ日: 2026/7/11 | 作成者: @55728
- 概要 (1-2文で)
Rails の fixture で使える特殊変数$LABELを展開する際に、ラベル文字列中の\&や\1などが正規表現の後方参照として解釈されてしまう問題を修正し、ラベルを「そのままの文字列」として挿入するようにした PR です。これにより、バックスラッシュを含むラベルでも意図通りの値が fixture に設定されます。
- 変更内容の詳細
問題の背景
Rails の fixture では、以下のように値の中で $LABEL を使うと、その行のラベル名が展開されます:
backref_parrot:
name: $LABELここで、ラベルが back\&ref といった、バックスラッシュ付きの文字列だった場合に問題が発生していました。
PR 説明から読み取れる「以前の実装」は、$LABEL の置き換えに Ruby の String#gsub とブロックを使わず直接の置換をしており、その中で「置換後文字列として」ラベルが渡されていたと考えられます。Ruby の gsub では、置換「後」文字列中の:
\&… マッチした全体文字列\0–\9… キャプチャグループの後方参照\k<name>… 名前付きキャプチャへの後方参照
などが特別扱いされます。そのため、
- ラベルが
back\&refの場合、\&が「マッチ文字列(ここでは$LABEL)」に展開されてしまう \1などが含まれると、後方参照展開の結果としてその部分が消えたり不正な値になる
といった不具合が起きていました。
PR 内の「Before / After」の例:
back\&ref:
name: $LABEL- Before:
name=>back$LABELref\&が$LABELに展開されてしまった(本来は「&」として扱いたい)
- After:
name=>back\&ref- ラベル文字列そのものがそのまま入る
実際の変更点
activerecord/lib/active_record/fixture_set/table_row.rb の $LABEL 展開ロジックが 1 行変更されています(+1 / -1)。
本体コードは PR からは全文がわかりませんが、挙動からすると例えば以下のような変更が行われています:
以前 (イメージ):
rubyvalue.gsub(LABEL_RE, label) # 第2引数に生文字列を渡していたこれだと
label内の\&や\1が「置換後パターン」として解釈される。以後 (イメージ):
rubyvalue.gsub(LABEL_RE) { label } # ブロックで返すブロックから返した文字列はそのまま挿入されるため、
\を含んでいても特別扱いされない。
Ruby の gsub において、「置換後文字列を文字列リテラルで与える」場合だけ、\& などが特別扱いされますが、「ブロックの戻り値として与える」場合はそうした解釈はされません。そのため、今回のような「ラベルを verbatim(生の文字列)で入れたい」ケースではブロック形式が正しい実装になります。
テストの追加
2 つのテストファイルが更新されています。
activerecord/test/cases/fixtures_test.rb(+4)$LABELとバックスラッシュを含むラベルの組み合わせをカバーするテストが追加されています。例えば:rubytest "fixture label with backslash is interpolated verbatim" do parrot = parrots(:'back\&ref') assert_equal 'back\&ref', parrot.name endといった内容が追加されていると考えられます(ファイル追加行数・PR 説明からの推測を含みます)。
activerecord/test/fixtures/parrots.yml(+5)上記テスト用の fixture が追加されています。例えば:
ymlback\&ref: name: $LABELのようなデータが登録され、
$LABEL展開が期待どおりか確認しています。
- 影響範囲・注意点
影響対象
- ActiveRecord の fixtures 機能を利用し、値の中で
$LABELを使っているケース。 - 特に「ラベル名にバックスラッシュを含めている」かつ「そのラベルを
$LABELで展開している」ケースに影響があります。
- ActiveRecord の fixtures 機能を利用し、値の中で
互換性 / 破壊的変更の可能性
- 「普通の識別子スタイル」のラベル(英数字・アンダースコアのみなど)はそもそもバックスラッシュを含まないため、この変更の影響を受けません。
- 以前、ラベル中の
\&が「マッチ文字列に展開される」という挙動に依存していたコードがあれば、挙動が変わります。ただしそれは Rails の意図した仕様ではなく、gsubの副作用に依存した偶然の挙動なので、修正は妥当といえます。 - ラベル中の
\1などが「消えてしまう」不具合が解消されるため、そのようなラベルを使っていた場合は fixture の値が本来の意図通りに変わります。
注意点
- この修正により、今後
$LABEL展開結果が「ラベル名そのもの」に必ず一致することが保証されます。 - ラベル名に正規表現ライクな記号を含めても安全に
$LABELが使えるようになります。
- この修正により、今後
- 参考情報 (あれば)
- PR 本文の Summary にある Ruby 正規表現置換の特性:
- 置換後文字列における
\&,\0–\9,\k<name>は特別な意味を持つが、gsub { ... }のブロック戻り値には適用されない。
- 置換後文字列における
- 関連する Ruby ドキュメント:
この PR により、fixtures における $LABEL の仕様が「ラベル名を文字列としてそのまま挿入する」という直感的なものに揃えられ、特殊文字を含むラベルでも安全に利用できるようになっています。
#58080 Round-trip the null: false attribute modifier in GeneratedAttribute#to_s
マージ日: 2026/7/11 | 作成者: @55728
- 概要 (1-2文で)
Rails のジェネレータで使う属性表現(GeneratedAttribute#to_s)において、string!のような!修飾子(=null: false)が文字列表現に正しく反映されずラウンドトリップできなかった問題を修正し、!を含む属性指定がパース前後で同じ表記に戻るようにした PR です。これにより、属性文字列表現のシリアライズ/デシリアライズの一貫性と正しさが改善されます。
- 変更内容の詳細
背景: ! 修飾子と null: false
Rails のジェネレータでは、モデル生成などの際に以下のような書き方ができます。
rails g model User name:string! age:integerここで string! の ! は「NOT NULL 制約」を意味し、内部的には attr_options に null: false として保存されます。
問題は、この内部表現を文字列化する GeneratedAttribute#to_s(およびそこから呼ばれる print_attribute_options)において、
!に対応する「専用の文字列表現」が定義されていなかった- そのため
null: falseが他のオプションと同列に{ ... }の中に混ぜられてしまい、再パース時に意味が変わったり、パース不能になったりしていた
という点です。
Before の挙動
PR 説明にある「Before」の具体例:
name:string! → name:string{null} (no longer parses as that type)
name:integer{4}! → name:integer{4} (modifier silently dropped)
name:references{polymorphic}! → name:references{polymorphic,null}
(parses back as null: true)それぞれ解説すると:
name:string!- 入力:
name:string!(NOT NULL を意味) - 出力:
name:string{null} - 問題点:
{null}という形は、もともとの「!付き型表現」として正しく再パースできない- そもそも
{null}が何を意味するか曖昧で、実質壊れた表現
- 入力:
name:integer{4}!- 入力:
name:integer{4}! - 出力:
name:integer{4} - 問題点:
!由来のnull: falseが文字列表現から完全に消えてしまう(制約がサイレントに失われる)- ラウンドトリップ時に NOT NULL が落ちる
- 入力:
name:references{polymorphic}!- 入力:
name:references{polymorphic}! - 出力:
name:references{polymorphic,null} - 問題点:
{polymorphic,null}というオプション表現を再パースするとnull: true相当として解釈されてしまう- つまり
null: falseが逆転してnull: true相当の意味に変わる可能性がある
- 入力:
このように、「! 付きで指定した NOT NULL 制約」が、文字列表現化 → 再パースという往復(ラウンドトリップ)で:
- 消える
- 逆の意味になる
- あるいはパース不能になる
という不具合がありました。
After の挙動
PR 説明では「After — all three round-trip to themselves.」とある通り、修正後は次のように「往復しても同じ表記に戻る」ようになっています。
name:string! → name:string!
name:integer{4}! → name:integer{4}!
name:references{polymorphic}! → name:references{polymorphic}!この修正のポイントは:
GeneratedAttribute#to_s(とprint_attribute_options)で、- すでに
attr_optionsにあるnull: falseを、単に{null: false}のようなオプション表現に紛れ込ませるのではなく - 元の
!修飾子という「構文レベルの情報」として復元できるようにした
- すでに
- つまり、「
!というシンタックス」と「attr_options[:null] == falseという内部表現」を正しくラウンドトリップ(二方向)させるようにした
コード上は:
railties/lib/rails/generators/generated_attribute.rbto_sやprint_attribute_optionsまわりでnull: falseを検出し、!として出力する処理が追加・調整されている- 他のオプション(例:
{4},{polymorphic})との組み合わせでも、!が落ちたり意味が変わらないように出力順序や表現が整理されている
railties/test/generators/generated_attribute_test.rb- 上記 3 パターン(および関連ケース)がテストとして追加され、ラウンドトリップの正しさを保証している
- 影響範囲・注意点
- 対象範囲:
- Rails ジェネレータで使用される
GeneratedAttributeの文字列表現 (#to_s) に限定された変更です。 - 特に、
attribute!形式で NOT NULL を指定している場合、および{...}オプションと!を組み合わせているケースに影響します。
- Rails ジェネレータで使用される
- 影響内容:
- これまで
#to_sの出力を自前でパースして再利用していたツール・スクリプトがある場合、出力形式が変わる可能性があります。- 例: 以前は
name:string{null}のような形式が出ていたところが、name:string!のように変わる。 - ただし、これは本来意図されていた正しい表現への修正であり、ジェネレータ仕様としてはむしろ改善です。
- 例: 以前は
- 既存コードで
{null}や{polymorphic,null}のような「バグ起因の表現」を前提にパースしている場合は、パーサを見直す必要があります。
- これまで
- 実行時の挙動:
- 実際に生成されるマイグレーションや DB スキーマの挙動(NOT NULL 制約の有無)そのものは、この PR によって変わらないはずです。
- あくまで「属性の文字列表現」と「そのラウンドトリップ」が正しくなっただけで、DB 制約の解釈ロジックは従来どおりです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58080
- 関連クラス:
Rails::Generators::GeneratedAttribute
- 関連する使い方の例(ジェネレータ):bash
# NOT NULL な name カラムを持つ users テーブル rails g model User name:string! # 長さ指定や polymorphic オプションと ! の組み合わせ rails g model Product code:integer{4}! owner:references{polymorphic}!
#58086 Convert Symbol keys in a HashWithIndifferentAccess#transform_keys mapping
マージ日: 2026/7/11 | 作成者: @55728
- 概要 (1-2文で)
HashWithIndifferentAccess(以下 HWIA)のtransform_keys/transform_keys!に「変換マッピング用のハッシュ」を渡したとき、マッピング側のキーが Symbol の場合でも正しくキー変換されるように修正した PRです。これにより、Ruby 標準のHash#transform_keys(hash)と同じ挙動になり、HWIA 内の他メソッド (slice,dig,fetch,[]) との一貫性も取られました。
- 変更内容の詳細
これまでの問題点
with_indifferent_access したハッシュは、内部的には「文字列キー」で管理されていますが、アクセス時には Symbol / String を意識せずに使えるよう convert_key を通していました。
例:
h = { a: 1, b: 2 }.with_indifferent_access
h[:a] # => 1
h["a"] # => 1しかし、transform_keys(hash) に「旧キー → 新キー」のマッピングハッシュを渡す場合、そのマッピングの“キー”には convert_key が適用されていなかったため、Symbol キーのマッピングが内部の String キーと一致せず、変換が起きませんでした。
{ a: 1, b: 2 }.with_indifferent_access.transform_keys(a: :x)
# 期待: { "x" => 1, "b" => 2 }
# 実際(修正前): { "a" => 1, "b" => 2 } # 何も起こらない一方、マッピング自体を文字列キーで書いた場合は動いていました:
{ a: 1, b: 2 }.with_indifferent_access.transform_keys("a" => "x")
# => { "x" => 1, "b" => 2 }この挙動は:
- Ruby 標準の
Hash#transform_keys({ a: :x })と比べて直感に反する - HWIA の他の API (
slice,dig,fetch,[]) がすべて引数キーをconvert_keyに通しているのと不整合
という問題がありました。
今回の修正内容
今回の PR では、HashWithIndifferentAccess#transform_keys が「マッピング用ハッシュ」を受け取った場合、そのマッピングのキーを convert_key に通してから比較・適用するようにしました。
ポイント:
- マッピングハッシュのキーを
convert_keyするように変更(sliceと同様の扱い) - これにより、Symbol キーのマッピングでも内部の String キーとマッチする
transform_keys!はtransform_keysに委譲しているため、同じ修正で不具合が解消
修正後の挙動:
h = { a: 1, b: 2 }.with_indifferent_access
h.transform_keys(a: :x)
# => { "x" => 1, "b" => 2 }
# 文字列キーのマッピングは従来どおり
h.transform_keys("a" => "x")
# => { "x" => 1, "b" => 2 }ブロック形式や、マッピング + ブロックの複合形も挙動は変わらず、以下が確認されています:
- 文字列キーのマッピングは従来通り動作
- ブロック形式 (
transform_keys { |k| ... }) は影響なし - ハッシュ + ブロック併用時も、マッピングにないキーはブロック処理される挙動を維持
- 非破壊版
transform_keysはレシーバを変更しない - 非ハッシュ(
nil,42など)をマッピングに渡した場合は、従来通りTypeErrorが発生
変更は本体 1 行・テスト 21 行追加と、ごく小さなピンポイント修正です。
- 影響範囲・注意点
影響を受けるコード
- HWIA に対して
transform_keys(a: :x)のように「Symbol キーのマッピング」を渡していたコード - これまで「なぜか変換されていなかった」ケースが、期待どおり変換されるようになります。
- HWIA に対して
後方互換性の観点
- これまで「Symbol キーのマッピングは常に no-op」として rely しているコードがあれば挙動が変わりますが、これはバグ依存であり、Ruby 本体や HWIA の他メソッドと比べても修正後の方が一貫しているため、通常は望ましい互換性変更と考えられます。
- String キーで書いていたマッピング、ブロック形式、
transform_keys!の破壊的挙動には変更なし。
テスト
- HWIA 用のテストファイルにテストケースが追加されており、
- Symbol キーのマッピングで期待どおり変換されること
- 既存の動作が壊れていないこと が自動テストで担保されています。
- HWIA 用のテストファイルにテストケースが追加されており、
- 参考情報 (あれば)
- Ruby 本体の仕様:
Hash#transform_keys(hash)(Ruby 3.1 以降)
https://docs.ruby-lang.org/ja/latest/method/Hash/i/transform_keys.html
- 以前の関連 PR(
HashWithIndifferentAccessにtransform_keys(hash)形式を追加したもの):- Rails PR #46846 (言及のみ、本文中参照)
#58052 Combine equivalent owner-dependent preload scopes
マージ日: 2026/7/10 | 作成者: @rwalters44
- 概要 (1-2文で)
owner(親レコード)依存のスコープ付き関連(いわゆるインスタンス依存スコープ)の eager load 時に、実質同じ条件のスコープをまとめて 1 つの Preloader に統合するようにした PRです。これにより、SQL の発行回数は変わらないまま、Preloader オブジェクトの数とレコードスキャン回数が大幅に減り、高負荷ケースで性能が大きく改善されます。
- 変更内容の詳細
問題の背景
class Project < ActiveRecord::Base
has_many :tasks,
->(project) { where(account_id: project.account_id) }, # owner 依存スコープ
inverse_of: :project
endのように、ブロック(->(owner) { ... })を使った owner 依存スコープを持つ関連を preload/includes すると、各 owner ごとにスコープを評価します。
多くの owner が「同じ値」を持っている場合(例: 全ての Project の account_id が 1)、評価後に生成される Relation は実質同じになりますが、これまでの実装では
- owner ごとに別々の
reflection_scopeを生成 - それぞれに対して
Preloader::Associationを生成
しており、「同じクエリを発行する Preloader を無駄に多数作る」状態になっていました。
SQL 自体はその後のバッチングで 1 本にまとめられていたため、SQL カウントは増えない一方で、
- Preloader オブジェクトが owner 数だけ増える
- child レコードとのマッチング(
n x m問題)で無駄なループ・スキャンが増える
というパフォーマンス問題がありました。
何をしたか
ActiveRecord::Associations::Preloader::Branch#preloaders_for_reflection の挙動を変更し、owner 依存スコープに限って、「結果として同じ Relation になるスコープ」をまとめて 1 つの Preloader に統合するようにしました。
ポイント:
対象は owner 依存スコープのみ
rubyif reflection.scope && reflection.scope.arity != 0 # owner 依存スコープとして特別扱い endreflection.scope.arity != 0(引数ありのスコープブロック)の場合だけ新しい grouping ロジックを使います。非 owner 依存スコープは従来どおり。「等価なスコープ」の判定方法
生成された Relation を次のキーでグルーピングします:- 関連クラス (
klass) - テーブル名
- connection specification name(接続設定)
values_for_queries
→ これは既存の「preloader の SQL バッチング」で使われている Relation 等価判定と同じ指標で、SQL クエリとして同じものになる Relation を同一グループと見なします。
疑似コード的には次のようなイメージです(実際のコードはもう少し複雑ですが概念的にはこれ):
rubyreflection_records .group_by { |owner| klass = owner.association(association).klass reflection_scope = reflection.join_scopes(klass.arel_table, klass.predicate_builder, klass, owner).inject(&:merge!) [ klass, reflection_scope.table_name, klass.connection_specification_name, reflection_scope.values_for_queries ] } .map { |(_klass, _table, _conn, _values), owners_for_group| preloader_for(reflection).new(klass, owners_for_group, reflection, scope, reflection_scope, associate_by_default) }これにより、「owner は違うが、結果として同じ WHERE / JOIN 条件などを持つ Relation」を 1 つの Preloader にまとめます。
- 関連クラス (
非 owner 依存スコープは動作変更なし
scope.arity == 0の関連(λ の引数を取らない、もしくはscope自体が無いもの)は、従来どおり 1 つの Preloader にまとまり、今回の grouping ロジックの影響を受けません。テストの拡張
既存の「インスタンス依存 preload」テストを拡張し、「等価な owner 依存スコープを持つ 2 つの owner(PR 説明ではdavidが 2 レコード)」が 同じ Preloader を共有していることをアサートするようになりました。
これにより、「同一スコープが複数の Preloader にばらけない」ことをテストで保証しています。
ベンチマーク結果の要点
owner 依存スコープの micro benchmark
- 1000 owner / owner 依存 scope
ケース Before After 変化 全 owner が同じ scope(1 グループ) loaders: 1000 / SQL: 1 / 20.40ms loaders: 1 / SQL: 1 / 8.81ms 約 2.3x 高速、loader 1000x 減 scope が 10 グループに分かれる loaders: 1000 / SQL: 10 / 21.80ms loaders: 10 / SQL: 10 / 10.75ms 約 2.0x 高速、loader 100x 減 非 owner 依存 scope loaders: 1 / SQL: 1 / 9.52ms loaders: 1 / SQL: 1 / 9.11ms 変化ほぼなし - SQL の数は元からバッチングされていたので「変わらない」
- パフォーマンス改善は「Preloader 数削減 + n x m マッチングの削減」によるもの
Generic preloader fanout benchmark
条件:
- 親レコード 1,000 件
- 子レコード 2,000 件
- 親は全て同じ
account_id→ owner 依存スコープの結果は 1 つの等価スコープになる - sqlite3 in-memory / 50 iteration
ケース Loaders SQL Raw record scans Avg time 通常の has_many :items1 1 2,000 16.70ms 旧 owner 依存スコープ 1,000 1 2,000,000 1097.94ms 新 owner 依存スコープ 1 1 2,000 25.19ms - 旧実装では 2,000 レコードを 1,000 Preloader がそれぞれ 2,000 件ずつ見るような形になり、
2,000,000回のスキャンが発生 - 新実装では通常の
has_manyと同じく 1 Preloader で 2,000 件を見るだけになり、スキャン回数・時間ともに大幅削減
- 影響範囲・注意点
- 対象となるのは owner 依存スコープ付き関連を
preload/includes/eager_loadするコードです。- 例:
has_many :tasks, ->(project) { where(account_id: project.account_id) }
- 例:
- 非 owner 依存の関連、通常の
has_many/has_one/belongs_toなどには挙動変更はありません。 - SQL の内容・件数は(元々バッチングされていたケースでは)基本的に変わりません。
変わるのは「Preloader の個数」と「Ruby 側の matching コスト」であり、アプリ側ロジックから見える挙動は変わらない想定です。 - Relation 等価判定には
values_for_queries等を用いているため、- 「同じ SQL になる Relation は同じ Preloader にまとめる」
- 「異なる SQL になる Relation は別 Preloader のまま」 という、既存のバッチングロジックと整合的な振る舞いです。
- owner ごとのスコープ評価順序や内側の実装に依存しているような非常にトリッキーなコードがあると、理論上は影響しうるものの、普通のアプリではまず問題にならない設計変更です。
期待されるメリット:
- 大量の owner に対して同種の owner 依存スコープ付き関連を preload している処理で、メモリ使用量と CPU 時間が大幅に減る可能性があります。
- 特に「マルチテナントで account_id で絞る」などの典型的なパターンで、全 owner の account_id が同じ/少数種類というケースでは効きやすいです。
- 参考情報 (あれば)
関連クラス:
ActiveRecord::Associations::Preloader::Branch#preloaders_for_reflection
owner 依存スコープの典型例:
rubyclass Author < ApplicationRecord has_many :posts_mentioning_author, ->(author) { where("posts.body LIKE ?", "%#{author.name.downcase}%") } endこのような形の関連を大量の
Authorに対して preload している場合、本 PR によって Preloader 数とマッチングコストが削減される可能性が高いです。
#57742 Introduce ActiveJob::DeserializationError::RecordNotFound
マージ日: 2026/7/10 | 作成者: @rosa
- 概要 (1-2文で)
ActiveJob の引数デシリアライズ時に「レコードがない場合」と「それ以外のエラー」を区別するために、新しくActiveJob::DeserializationError::RecordNotFound例外が導入されました。これにより、レコード削除など「正当な」理由による失敗だけを discard し、ネットワーク障害など一時的なエラーは通常どおりリトライさせる運用が可能になります。
- 変更内容の詳細
背景にある問題
これまで ActiveJob では、ジョブ引数のデシリアライズ中に起きたあらゆるエラーを
ActiveJob::DeserializationErrorでラップしていました。多くのアプリは雛形コメントに従って以下のように設定していることが多いです:
ruby# Most jobs are safe to ignore if the underlying records are no longer available discard_on ActiveJob::DeserializationErrorしかし、実際には「レコードが存在しない」だけでなく、DB との一時的なネットワーク障害 (例:
TRILOGY_TRUNCATED_PACKET) などもこの例外に包まれます。その結果、短時間のネットワーク障害が発生しただけで、本来はリトライすべき正常なジョブがすべて「静かに discard される」危険があり、かなりのフットガンになっていました。
新しい例外クラスの導入
この PR では以下の新例外が追加されています:
# activejob/lib/active_job/exceptions.rb
module ActiveJob
class DeserializationError < StandardError
class RecordNotFound < DeserializationError; end
end
endポイント:
ActiveJob::DeserializationError::RecordNotFoundはActiveJob::DeserializationErrorを継承するサブクラス。- 「デシリアライズ時に、対象のレコードが見つからない場合にのみ」 発生します。
- それ以外の原因 (ネットワークエラーやクエリエラーなど) の場合は、従来どおり上位の
ActiveJob::DeserializationErrorが使われます。
GlobalID の fetch を利用した実装
- PR では GlobalID 側に導入された
GlobalID::Locator.fetch(https://github.com/rails/globalid/pull/206) を利用するように変更されています。 fetchは「見つからない場合に例外を投げる」インターフェースで、ここでGlobalID::Locator#locate+nilチェックではなく、明示的な例外とその原因を使ってRecordNotFoundを判定しています。activejob/lib/active_job/arguments.rb内で、GlobalID からレコードを復元する処理が更新され、ActiveRecord::RecordNotFound 等を拾ってActiveJob::DeserializationError::RecordNotFoundにラップする形になっています。
(概念イメージ)
def deserialize_global_id(gid)
GlobalID::Locator.fetch(gid)
rescue ActiveRecord::RecordNotFound => e
raise ActiveJob::DeserializationError::RecordNotFound.new(e.message)
rescue => e
raise ActiveJob::DeserializationError.new("Error while trying to deserialize arguments: #{e.message}")
end※ 実際のコードは多少異なりますが、ロジックとしては上記のように「RecordNotFound 系だけを特別扱い」しています。
使い方の変更例
これまで (問題があるパターン)
class ApplicationJob < ActiveJob::Base
discard_on ActiveJob::DeserializationError
endこの場合:
- レコードが消えていても discard
- ネットワーク障害でデシリアライズに失敗しても discard
→ 望ましくない
今後推奨されるパターン
class ApplicationJob < ActiveJob::Base
# 「レコードがもう存在しない」ケースだけ discard する
discard_on ActiveJob::DeserializationError::RecordNotFound
endこうすると:
- レコード削除に起因するデシリアライズ失敗 → discard
- DB ネットワーク障害やその他エラー → 通常のエラーハンドリング (リトライ等) が動作
ActiveRecord::RecordNotFound を使っているジョブへの影響
説明文にもある通り、以下のようにしていたジョブは注意が必要です:
# 旧来のパターン
discard_on ActiveRecord::RecordNotFoundこれは「perform 内でレコードアクセスして RecordNotFound が起きた場合」にも、「デシリアライズ中にレコードが見つからない場合」にも同じ ActiveRecord::RecordNotFound が使われ得ました。
この PR 後は:
- デシリアライズ中:
ActiveJob::DeserializationError::RecordNotFound - perform 実行中:
ActiveRecord::RecordNotFound
と分かれるので、「どちらのパスでも discard したい」場合は明示的に両方書く必要があります:
discard_on ActiveRecord::RecordNotFound,
ActiveJob::DeserializationError::RecordNotFound標準ジョブ / ガイド / ジェネレータの更新
- Active Storage の各種ジョブ (
AnalyzeJob,PurgeJobなど) や Action Mailbox のIncinerationJobがdiscard_onのターゲットをActiveJob::DeserializationErrorからActiveJob::DeserializationError::RecordNotFoundに変更。 guides/source/active_job_basics.mdの解説も更新され、「レコードが存在しない場合だけ discard したいならActiveJob::DeserializationError::RecordNotFoundを使う」旨の説明が追加。rails g jobなどで生成されるApplicationJobテンプレート (railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt) も同様にコメントが調整され、間違った使い方を誘発しないようになっています。
- 影響範囲・注意点
互換性
- 新しい
ActiveJob::DeserializationError::RecordNotFoundはActiveJob::DeserializationErrorのサブクラスなので、- 既存で
rescue_from ActiveJob::DeserializationErrorやdiscard_on ActiveJob::DeserializationErrorをしているコードは、そのままでも 動作は変わりません (RecordNotFound も引き続き捕捉されます)。
- 既存で
- つまり、この PR は「後方互換性を保ちつつ、より細かい制御ができるようにした」変更です。
実運用上の推奨対応
discard_on ActiveJob::DeserializationErrorをグローバルに設定している場合は、見直し推奨ネットワーク事故時にジョブが黙って捨てられるリスクがあるため、
可能なら
ActiveJob::DeserializationError::RecordNotFoundに絞るほうが安全です。用途に応じて、以下のように分けるとよいです:
ruby# デシリアライズでレコードがない場合だけ discard discard_on ActiveJob::DeserializationError::RecordNotFound # それ以外の DeserializationError は通常の retry_on ハンドリングに任せる retry_on ActiveJob::DeserializationError, wait: 5.seconds, attempts: 3
discard_on ActiveRecord::RecordNotFoundを使っているジョブで「デシリアライズ時も含めて discard したい」場合明示的に両方書いてください:
rubydiscard_on ActiveRecord::RecordNotFound, ActiveJob::DeserializationError::RecordNotFound
モニタリング・アラート
- これまで
ActiveJob::DeserializationErrorをひとまとめでアラートしていた場合、RecordNotFound とそれ以外 (ネットワーク障害など) を分けて集計した方が、原因分析と運用ポリシーの設計がしやすくなります。
- これまで
- 参考情報 (あれば)
- Rails フォーラム: Is
discard_on ActiveJob::DeserializationErrora footgun?
→ この PR の動機になっている議論 - GlobalID PR: https://github.com/rails/globalid/pull/206
→GlobalID::Locator.fetch導入 PR。今回の RecordNotFound 判定の基盤 - Active Job Basics ガイド (更新後):
guides/source/active_job_basics.md
→discard_on/retry_onの使い分けと、今回の新例外の説明が追加済み
#58061 Fix some FrozenErrors for ractorized application
マージ日: 2026/7/9 | 作成者: @hmcguire-shopify
- 概要 (1–2文で)
Rails アプリケーションをRails.application.ractorize!で共有可能(freeze)にした際に発生していたFrozenErrorを、キャッシュのローカルキャッシュキーとアプリケーションのrevisionを「後からではなく事前に」計算するようにすることで解消する PR です。Ractors 対応アプリでのエラー報告やRails.cache利用が、freeze 後でも安全に動作するようになります。
- 変更内容の詳細
背景となる問題
Rails.application.ractorize!を呼ぶと、Rails.applicationが shareable/frozen になり、その中に含まれるRails.cacheも再帰的に shareable/frozen になります。Rails.cacheがLocalCacheストラテジーを使っている場合、local_cache_keyは 最初にLocalCache::Middlewareがローカルキャッシュを作ろうとしたタイミングで初めて計算される、という遅延初期化になっていました。- しかし、その時点では
Rails.cacheが既に frozen になっており、local_cache_keyの計算で内部状態を書き換えようとしてFrozenErrorが発生していました。
- しかし、その時点では
- 同様に、
Rails.errorレポーターがエラーを扱う際に、まだ設定されていないRails.application.revisionを 初回アクセス時に遅延初期化 していたため、Rails.applicationが frozen の状態だとここでもFrozenErrorが発生していました。
この PR は、これら「遅延初期化(lazy)」のタイミングを「インスタンス生成/初期化時(eager)」に移すことで、freeze 後の書き込みをなくしています。
変更1: LocalCache の local_cache_key をインスタンス生成時に計算
対象ファイル: activesupport/lib/active_support/cache/strategy/local_cache.rb
ポイント:
- これまで:
LocalCacheモジュール内でlocal_cache_keyは最初に必要になったときに計算されていた(おそらくメモ化されたアクセサか、初アクセス時に代入)。- その結果、
Rails.cacheが freeze された後にlocal_cache_keyを計算しようとしてFrozenError発生。
- これから:
- キャッシュストアに
LocalCacheストラテジーがミックスインされる際(またはインスタンス生成時)に、local_cache_keyを先に計算しておくように変更。
- キャッシュストアに
イメージとしては次のような変更になります(擬似コード):
# 以前(イメージ)
def local_cache_key
@local_cache_key ||= :"local_cache_#{object_id}"
end
# 以後(イメージ)
def initialize(*)
super
@local_cache_key = :"local_cache_#{object_id}" # ここで eager に確定
end
attr_reader :local_cache_keyこれにより:
Rails.application.ractorize!実行前にlocal_cache_keyが既に確定- freeze 後に
local_cache_keyを計算・代入しようとすることが無くなり、FrozenErrorが解消
変更2: Rails.application.revision をイニシャライザで eager 初期化
対象ファイル: railties/lib/rails/application.rb
ポイント:
- これまで:
Rails.application.revisionは lazily な計算で、初めて参照されたタイミング(例:Rails.errorがエラーを報告する際など)で内部状態を書き換えてセットしていた。- しかし、
Rails.errorが実行されるときにはRails.applicationが ractorize によって frozen されている可能性があり、その場合にFrozenError。
- これから:
Rails.applicationの初期化プロセス(initializer)内でrevisionを前もって決定しておく。- 以後のアクセスはすべて既にセット済みの値を参照するだけなので、freeze 後でも安全。
イメージの例:
# どこかの initializer 内(イメージ)
initializer "rails.application.set_revision" do |app|
app.revision # ここで一度アクセスして内部的に確定させる
end正確なコードは PR に依存しますが、目的は「アプリケーションオブジェクトがまだ mutable なうちに revision を確定させる」ことです。
変更3: テストの追加・更新(ractors 対応)
対象ファイル: railties/test/application/ractors_test.rb
- Ractors を利用したアプリケーション環境で、今回の修正により
FrozenErrorが発生しないことを検証するテストを追加。 - 具体的には:
Rails.application.ractorize!を呼んだ上で、Rails.cache(LocalCache使用時)の利用がエラーにならないことRails.errorによるエラー報告でRails.application.revisionに起因するFrozenErrorが出ないこと を確認するテストが含まれていると考えられます。
- 影響範囲・注意点
- Rails の ractor 対応 (
Rails.application.ractorize!) を有効にしていないアプリ:- 動作上の大きな変化はほぼありません。
- 以前は「初アクセス時に計算されていた」
local_cache_keyとrevisionが、「アプリ起動・インスタンス生成時に計算される」ようになるため、わずかに起動時の処理が増える可能性はありますが、実務上はほとんど無視できるレベルでしょう。
- Ractors を積極的に使うアプリ:
Rails.cacheにLocalCacheストラテジーを使っていても、Rails.application.ractorize!実行後にFrozenErrorに遭遇しづらくなります。- エラー報告(
Rails.error)が ractorized 環境下でもより安定して動くようになります。
- 独自に
LocalCacheを拡張している場合:local_cache_keyの初期化タイミングに依存した特殊な実装をしていると、影響を受ける可能性があります。- 通常は問題ないはずですが、「オブジェクト ID に依存していて、後から変わることを期待している」ようなトリッキーなコードがあると挙動が変わる可能性があります。
Rails.application.revisionについて:- これまで「最初にアクセスされた時点で確定される」ことを前提とした処理(例えば、最初のアクセス時にのみ何かログを出す、など)を書いていた場合、そのタイミングが initializer 実行時に早まる点には留意してください。
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/58061
- 関連機能:
- Ractors 対応:
Rails.application.ractorize! - キャッシュストアのローカルキャッシュ:
ActiveSupport::Cache::Strategy::LocalCache - エラーレポート API:
Rails.error
- Ractors 対応:
- 類似の問題:
- Ruby / Rails における「freeze 後に遅延初期化しようとして
FrozenError」というパターンは、スレッドセーフ化・Ractors 対応の過程でよく発生するため、同様に「初期化は mutable なうちに済ませる」方針が複数箇所で採用されています。
- Ruby / Rails における「freeze 後に遅延初期化しようとして
#58068 Let dump_schema pass the given format down to schema_dump
マージ日: 2026/7/9 | 作成者: @fxn
- 概要 (1-2文で)
ActiveRecord::DatabaseTasks.dump_schemaが受け取ったformat引数を内部のschema_dump呼び出しに正しく引き渡すよう修正された PR です。これにより、dump_schemaに任意のスキーマフォーマットを指定した場合でも、その指定が確実に反映されるようになりました。
- 変更内容の詳細
問題点
元の実装は以下のようになっており:
def dump_schema(db_config, format = db_config.schema_format) # :nodoc:
return unless db_config.schema_dump
...
schema_dump # ← 引数なしで呼び出していた
end
def schema_dump(format = schema_format)
...
enddump_schemaは呼び出し側からformatを明示的に受け取れる(例::ruby,:sqlなど)- しかし内部で
schema_dumpを呼び出す際にformatを渡しておらず、schema_dump側のデフォルトschema_formatが常に使われていた
そのため、呼び出し側が dump_schema(db_config, :sql) のようにフォーマットを指定しても、それが無視されてしまうバグがありました。
修正内容
activerecord/lib/active_record/tasks/database_tasks.rb の該当箇所が以下のように修正されています。
変更前(概念的に):
def dump_schema(db_config, format = db_config.schema_format)
return unless db_config.schema_dump
# ... 省略 ...
schema_dump # format を渡していない
end変更後(概念的に):
def dump_schema(db_config, format = db_config.schema_format)
return unless db_config.schema_dump
# ... 省略 ...
schema_dump(format) # ここで引数として渡す
endこれにより、dump_schema に渡された format がそのまま schema_dump に伝播し、意図したフォーマットでスキーマダンプされます。
テスト追加
activerecord/test/cases/tasks/database_tasks_test.rb に 23 行のテストが追加されています。テストの意図としては:
dump_schemaに特定のformatを引数として渡した場合に、schema_dumpがそのformatを受け取って呼ばれているか
を検証するものです。モックまたはスタブを使い、schema_dump に渡された引数を検査する形になっているはずです。
- 影響範囲・注意点
影響を受けるケース
- Rake タスクや独自スクリプト等で
ActiveRecord::DatabaseTasks.dump_schema(config, :sql)など、デフォルトとは異なるformatを明示的に指定している場合、これまで無視されていた指定が、今後は正しく反映されるようになります。 - 以前の挙動に依存した(「指定しても実は効いていない」という)コードがあれば挙動が変わる可能性がありますが、通常それはバグ修正とみなせるはずです。
- Rake タスクや独自スクリプト等で
注意点
db_config.schema_formatを意図的にカスタマイズしていて、かつdump_schema呼び出しで別のformatを渡している場合、今後は「引数で渡したformatが優先」されます(より直感的な挙動)。- CI や本番でスキーマダンプを行うタスクをお持ちで、
formatを直接指定している場合は、一度生成物(schema.rb/structure.sql)が期待通りか確認しておくと安全です。
- 参考情報 (あれば)
- 該当メソッド:
ActiveRecord::DatabaseTasks.dump_schemaActiveRecord::DatabaseTasks.schema_dump
- 想定利用箇所:
bin/rails db:schema:dumpbin/rails db:structure:dump- 独自のメンテナンススクリプトからの呼び出し等
- PR: https://github.com/rails/rails/pull/58068
#58066 Add write_multi_entries method to batch writes for memcache store if dalli's set_multi is defined
マージ日: 2026/7/9 | 作成者: @ilianah
- 概要 (1-2文で)
このPRは、ActiveSupport::Cache::MemCacheStoreにwrite_multi_entriesメソッドを追加し、利用中の dalli にset_multiが定義されている場合はそれを使って複数キーを一括書き込みできるようにする変更です。これにより、memcached への書き込み時のラウンドトリップ数が削減され、パフォーマンス向上が見込まれます。
- 変更内容の詳細
追加されたもの
対象ファイル:
activesupport/lib/active_support/cache/mem_cache_store.rb(+17/-0)
MemCacheStore 内に、複数エントリを書き込むための内部メソッド write_multi_entries が追加されています。このメソッドは、dalli クライアントに set_multi が存在する場合はそれを利用し、存在しない場合は従来どおり個別に書くというフォールバック動作をします。
概ね以下のようなイメージの実装になっていると考えられます(擬似コード):
def write_multi_entries(entries, options)
# entries: { "key1" => value1, "key2" => value2, ... }
if @data.respond_to?(:set_multi)
# dalli の set_multi を利用して一括書き込み
# ここで memcached 用のオプションに変換する処理も挟まるはず
@data.set_multi(entries, expires_in: options[:expires_in], raw: options[:raw])
else
# 旧バージョンの dalli など set_multi がない場合は従来どおりループで書き込み
entries.each do |key, value|
write_entry(key, value, options)
end
end
endwrite_multi_entries 自体は Rails の内部 API で、write_multi(public API)などから呼ばれ、memcache ストアでの実際の書き込みを担当しているメソッドになります。
dalli の set_multi との連携
PRの説明にもある通り、dalli 側に以下のような変更が行われています:
- https://github.com/petergoldstein/dalli/pull/1059
→set_multiが追加され、複数キーの一括セットがサポートされた。
今回の Rails 側の変更は、この set_multi が利用可能な場合のみ、既存コードを変えずに透過的にバッチ書き込みを有効化するものです。
- 影響範囲・注意点
影響範囲
- 対象:
ActiveSupport::Cache::MemCacheStoreを利用し、かつキャッシュの複数キー書き込み (write_multiなど) を行うアプリケーション - 条件: dalli のバージョンが
set_multiを持っている場合set_multiがある → memcached への書き込みが 1 ラウンドトリップで済む(可能な限り)set_multiがない → 従来どおりキーごとに個別setを実行
そのため、dalli を更新した場合にだけ、透過的にパフォーマンスが向上しうる変更です。既存コード側での明示的な変更は不要です。
互換性
- 後方互換性は保たれています。
respond_to?(:set_multi)で有無をチェックしてから使う設計のため、古い dalli を使っていてもNoMethodErrorにはなりません。
- インターフェース(public API)としての
MemCacheStore#write_multiの使い方は変わりません。
パフォーマンス面での注意
- バッチ書き込みにより、1 回の
write_multiで多くのキーを書き込む場合に特に効果が出ます。 - ただし、一括で大量のデータを
set_multiすることで、単一リクエストあたりのペイロードが増える可能性はあります(ただし多くのケースでは往復回数削減のメリットの方が大きい)。
- 参考情報 (あれば)
- dalli の
set_multi実装 PR:
https://github.com/petergoldstein/dalli/pull/1059 - 関連しうる Rails API:
ActiveSupport::Cache::Store#write_multiActiveSupport::Cache::MemCacheStoreの実装(mem_cache_store.rb)
#58058 Skip the query for a grouped calculation on a contradictory relation
マージ日: 2026/7/8 | 作成者: @55728
- 概要 (1-2文で)
WHERE句が論理的に矛盾していて絶対に行がヒットしない Relation に対して、グルーピング付き集計(group(...).countなど)を行う場合、DBクエリを発行せずに即座に空ハッシュを返すように最適化した PR です。すでに非グループ集計で行っていた最適化を、グループ集計経路にも拡張した形です。
- 変更内容の詳細
何が問題だったか
例として、次のようなコードを考えます。
Post.where(id: []).group(:author_id).countwhere(id: []) は SQL 上 WHERE 1=0 相当になり、必ず 0 件になる「矛盾した条件」です。
戻り値は Ruby 的には常に {}(空のハッシュ)ですが、これまでは次のような無駄なクエリが DB に投げられていました。
SELECT COUNT(*) AS count_all, "posts"."author_id" AS author_id
FROM "posts"
WHERE 1=0
GROUP BY "posts"."author_id"結果は空ですが、無意味なクエリ 1 回分のオーバーヘッドが発生していました。
非グループの集計メソッド(count / sum / average / minimum / maximum)については、すでに
- Relation が「矛盾していて絶対に 0 行」だと分かる場合は
- クエリを飛ばさずに、Ruby 側で結果を返す
というショートサーキットが入っていました。
今回の PR は 「グループ付き集計」の経路にも同じショートサーキットを追加 するものです。
実際の挙動の変化
Before
scope = Post.where(id: [])
scope.group(:author_id).count
# => {} を返すが、DB にクエリを 1 回投げるAfter
scope = Post.where(id: [])
scope.group(:author_id).count
# => {} を返し、DB クエリは 0 回つまり、「返り値は従来と完全に同じだが、DB への無駄な問い合わせがなくなる」という変更です。
実装のポイント(推定)
ファイル変更箇所から見ると:
activerecord/lib/active_record/relation/calculations.rb- グループ付きの計算を行うコードパスの先頭付近で
- Relation が
none?など、「矛盾した where で必ず 0 行」と判定できる場合に - 即
{}を返す分岐を追加
- Relation が
- グループ付きの計算を行うコードパスの先頭付近で
activerecord/test/cases/calculations_test.rb- 矛盾した Relation に対する
group(...).countが- クエリを発行しないこと
{}を返すこと を保証するテストが追加
- 矛盾した Relation に対する
という形です。
非グループ集計側ですでに存在したロジック(@klass.none? や contradiction? に相当するチェック)を、グループ計算側でも流用・適用していると考えられます。
- 影響範囲・注意点
- 挙動(返り値)は従来と完全に同じ です
- もともと
{}だったものが、これからも{}のままです。 - つまり、アプリケーションのロジック上の互換性は維持されます。
- もともと
- 影響はパフォーマンス面のみ(正の方向)
where(id: [])などのパターンは、フィルタリングの結果としてよく発生します(例:検索条件が一つもヒットしなかった ID 集合に対してさらに集計したい場合など)。- そのようなケースで、無駄な SELECT … GROUP BY クエリが 1 回減る ため、DB 負荷・レイテンシが僅かに改善します。
- 適用されるのは「矛盾した Relation」でのみ
- 通常の
where(実際に行が返る可能性のある条件)に対しては従来通りクエリが実行されます。 - あくまで Rails が「この Relation は絶対に 0 行」と静的に判断できるケースに限られます。
- 通常の
- モニタリング/メトリクス上の違いに注意
- DB クエリ数を監視している場合、
group(...).countを多用するコードの一部でクエリ数が減る可能性があります。 - これはバグではなく最適化によるものです。
- DB クエリ数を監視している場合、
- 参考情報 (あれば)
- 類似 PR: #58004
none.idsがクエリなしで空配列を返すようにした変更で、本 PR と同じ「矛盾した Relation に対して DB を叩かない」クラスの最適化です。
- 関連 API:
ActiveRecord::Relation#noneActiveRecord::Calculations#count,#sum,#average,#minimum,#maximumActiveRecord::Relation#group
#58054 Remove some duplication in ColorField
マージ日: 2026/7/8 | 作成者: @p8
- 概要 (1-2文で)
ColorFieldとTextFieldで重複していた「値が渡されないとき/nil のときの value 属性の扱い」を共通化するリファクタリングです。TextFieldにfallback_valueメソッドを追加し、ColorField側はそれをオーバーライドすることで重複コードを削除しつつ、属性の出力順序も他フィールドとより一貫したものにしています。
- 変更内容の詳細
背景
これまで ColorField#render は以下のような挙動を持っていました(PR 説明からの要約):
- 値が渡されない場合(
valueキー自体がない / nil ではなく未指定)
→value属性に"#000000"をセット valueにnilが明示的に渡された場合
→value属性を出力しない- その後
super(TextField#render)を呼ぶが、TextFieldも同様のロジックを持っており、fallback の値だけが違うTextFieldはvalue_before_type_castを fallback に使う
つまり、「value がなければ fallback 値を使い、nil の場合は無視する」という処理が ColorField と TextField の両方に重複していました。
変更1: TextField#fallback_value の追加
TextField に新たに fallback_value メソッドが追加されます。意図としては:
class ActionView::Helpers::Tags::TextField
# 擬似コードイメージ
def render
options = @options.stringify_keys
value = options["value"]
if options.key?("value")
# 明示的に value が渡されている
if value.nil?
options.delete("value") # nil は無視(value 属性を出さない)
end
else
# value 未指定なら fallback_value を使う
if (fallback = fallback_value)
options["value"] = fallback
end
end
# 通常の input 要素生成
super
end
private
def fallback_value
value_before_type_cast
end
end※あくまで挙動を説明するためのイメージコードですが、
- 「value が未指定なら fallback_value を使う」
- 「value が指定されていても nil なら削除」
というロジックを共通化し、fallback の中身だけをfallback_valueで差し替え可能にした、という意図です。
TextField のデフォルト実装は value_before_type_cast を返し、従来の挙動を維持します。
変更2: ColorField から処理を削減し fallback_value をオーバーライド
ColorField 側では、これまで持っていた独自の render 内ロジック(#000000 を設定したり、キーを string 化したり)を削除し、代わりに fallback_value だけをオーバーライドする形になります。
イメージ:
class ActionView::Helpers::Tags::ColorField < TextField
private
def fallback_value
"#000000"
end
endポイント:
- 「value が渡されなかったときに
"#000000"を使う」という ColorField 特有の挙動は維持 - 「value が明示的に
nilのときは value 属性を出さない」という部分はTextField側の共通実装に委譲 - キーの string 化 (
stringify_keys) も TextField 側の共通コードに任せるため、ColorField 内での重複処理が不要に
変更3: 属性の出力順序の変化
render 実装の共通化により、生成される <input type="color" ...> の属性の出力順が少し変わります。PR 説明では「他のフィールドとより一貫した順序になる」とされています。
テスト (form_helper_test) もこの新しい属性順序に合わせて修正されているため、挙動として「属性の順番が変わる」ことが仕様として固定されます(HTML 的には属性順は意味を持たないので互換性への影響は軽微と考えられます)。
- 影響範囲・注意点
- ColorField の「デフォルト値 / nil の扱い」は実質的に従来通り
- value 未指定 →
#000000 - value: nil → value 属性なし
- これは TextField 側の汎用ロジック +
fallback_valueオーバーライドで表現されているだけで、意味的な挙動は変えていません。
- value 未指定 →
- フォームビルダーの出力 HTML を厳密に比較しているテスト・スナップショット等では、属性順の違いで差分が出る可能性があります。
- Rails 本体内のテストはすでに更新済みですが、アプリ側で独自に HTML を string 比較している場合は注意が必要です。
TextFieldを継承して独自フィールドを実装している場合、fallback_valueをオーバーライドすることで:- 「value がないときのデフォルト値の決め方」を簡潔にカスタマイズできるようになります。
- 逆に
renderをゴリゴリ上書きして同様のことをしている場合は、将来的にはfallback_value利用へのリファクタ余地があります。
- 参考情報 (あれば)
- この PR は直前の #58023 で追加された ColorField 関連コードを整理するための follow-up リファクタリングです。
- 技術的には:
- 値解決ロジックの共通化
- サブクラスごとの差分を小さなメソッド (
fallback_value) に閉じ込める という典型的なテンプレートメソッドパターン的なリファクタリングで、Form フィールドタグのコードベースの保守性を高める変更と言えます。
#58062 Ensure AV::Template::Sources::File is eager loaded
マージ日: 2026/7/8 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
ActionView::Template::Sources::FileがActionView.eager_load!実行時に確実に eager load されるように、Sources::Fileクラス定義を別ファイルからsources.rb内にインライン化した変更です。これにより、最初のテンプレートレンダリングまでSources::Fileが require されないという遅延ロード問題を解消します。
- 変更内容の詳細
背景
ActionView.eager_load!→ActionView::Template.eager_load!が呼ばれます。Template.eager_load!はSourcesをeager_autoloadしていますが、Sources::Fileまでは自動的にたどり着いていませんでした。Sources側には
eager_autoload do
autoload :File
endのような指定があり、Sources::File は「eager load の対象として登録」されてはいるものの、Sources.eager_load! 自体がどこからも呼ばれていないため、eager load フェーズでは実際には読み込まれていない状態でした。
- その結果、
Sources::Fileは「最初のテンプレートレンダリング時」に初めて require される挙動になっていました。
この PR の変更点
Sources::Fileのクラス定義をsources.rbにインライン化- もともと
ActionView::Template::Sources::Fileはactionview/lib/action_view/template/sources/file.rbに定義されていましたが、この PR でその中身をactionview/lib/action_view/template/sources.rb内に移動しています。 file.rb側の中身は削除され(-17行)、sources.rb側にFileクラス定義が直接書かれる形で +7 行追加されています。
イメージ的には、これまで:
ruby# sources.rb module ActionView class Template module Sources extend ActiveSupport::Autoload eager_autoload do autoload :File end end end endと
ruby# sources/file.rb module ActionView class Template module Sources class File # 実装... end end end endのように分かれていたものが、
sources.rbにrubymodule ActionView class Template module Sources class File # 実装... end end end endのように直接含まれる構成になった、という変更です (実際のコードは若干異なる可能性がありますが、構造としてはこのイメージ)。
- もともと
eager_autoloadチェインを増やさない方針- 別案としては、
Template.eager_load!→Sources.eager_load!→Sources::Fileというようにeager_load!チェインをつなぐことも可能でした。 - しかし、この PR では「
Sourcesの配下に他の定数が存在しない」「Sourcesを参照するならほぼ確実にFileも欲しい」という前提から、構造を簡略化するためにインライン化を選択しています。
- 別案としては、
- 影響範囲・注意点
eager load の挙動が変わる
ActionView.eager_load!実行時に、ActionView::Template::Sources::Fileが必ずロードされるようになります。- これにより本番環境(
config.eager_load = true)などで、最初のリクエスト時ではなくブート時にSources::Fileがロードされ、初回テンプレートレンダリングまでのレイテンシや「初回だけ遅い」現象が減る可能性があります。
オートロード階層に依存したコードへの影響
- 以前は
ActionView::Template::Sources::Fileが別ファイルにあり、autoload 機構により必要時にロードされていましたが、今後はsources.rb読み込み時点でクラス定義が存在します。 - 通常のアプリケーションコードではこの違いは意識しなくてよく、互換性問題はほぼ発生しないと考えられます。
- 以前は
Sources配下に今後クラスを増やす場合の設計- 現時点では
Sourcesの配下にFileしか存在しないことを前提にインライン化されています。 - 将来的に
Sources::<SomethingElse>を増やす場合、- それらも
sources.rbにまとめるか、 - 再び
eager_autoload+eager_load!チェインで細かく制御するか
の設計方針を検討する必要があります。
- それらも
- 現時点では
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58062
- 関連する Rails の設定:
config.eager_load = trueの環境では、今回の修正によりActionView.eager_load!時点でSources::Fileが確実にロードされます。
eager_autoload/eager_load!に関する参考:ActiveSupport::Autoloadのドキュメント- Rails ガイド「Rails のオートローディングとリローディング」 (Zeitwerk まわり)
#57662 Add ActiveSupport::Ractors.on_main to proxy work to the main Ractor
マージ日: 2026/7/8 | 作成者: @gmcgibbon
- 概要 (1-2文で)
このPRは、非メイン Ractor からメイン Ractor に処理を委譲するためのActiveSupport::Ractors.on_mainメソッドを追加し、クラスインスタンス変数のメモ化など「メイン Ractor でしか安全に行えない処理」をプロキシできるようにするものです。これにより、Ractors 対応のためにクラスレベルメモ化を完全に捨てずに、安全な形で保持できるようになります。
- 変更内容の詳細
追加された API: ActiveSupport::Ractors.on_main
- 新規ファイル(または新規モジュール実装):
activesupport/lib/active_support/ractors.rb - そこに
ActiveSupport::Ractors.on_mainが追加されています。 - 実装には外部ライブラリ
ractor-dispatchが利用されており、メイン Ractor に処理をディスパッチする仕組みをラップしています。
想定される利用方法は以下のとおりです。
従来のコード (Ractors 非対応で問題が出る可能性がある例)
class Something
def self.something
@ivar ||= :a
end
end- このパターンは「クラスインスタンス変数の遅延メモ化」ですが、Ractors 環境では
- 非メイン Ractor からクラスインスタンス変数に書き込みが行われる
- オブジェクト共有の制約との整合性 などの問題が出やすくなります。
Ractors 対応後のコード (この PR で意図している書き換え例)
class Something
def self.something
@ivar || ActiveSupport::Ractors.on_main { @ivar ||= :a }
end
end- 非メイン Ractor から
Something.somethingが呼ばれた場合でも、@ivarが未設定ならActiveSupport::Ractors.on_mainブロックが「メイン Ractor 上で」実行される- メイン Ractor 上で安全に
@ivarが設定/メモ化される
- すでにメイン Ractor 上で
@ivarがメモ化済みなら、ブロックを実行せずに@ivarをそのまま返す形になります(||によるガード)。
Active Record 側での利用前提
activerecord/lib/active_record/core.rbにも小さな変更があり、クラスレベルのメモ化ロジックの一部をActiveSupport::Ractors.on_main経由で実行できるようにする準備が進められています。- 別 PR(https://github.com/rails/rails/pull/57642)で Active Record のクラスレベルメモ化を整理する作業と連携しており、「どうしてもメモ化が必要な箇所」だけを安全にメイン Ractor へ委譲する目的があります。
テストと依存関係
activesupport/test/ractors_test.rbにテストが追加され、on_mainが- メイン Ractor から呼び出された場合
- 非メイン Ractor から呼び出された場合 の挙動をカバーしていると考えられます(+36/-1)。
activesupport/activesupport.gemspecおよびGemfile.lockに変更が入り、ractor-dispatchを依存関係として追加しています。
- 影響範囲・注意点
影響範囲
- Rails アプリケーションコードそのものにはすぐに大きな破壊的変更はありませんが、
- ActiveSupport が新たに
ractor-dispatchに依存する - 将来的に Active Record を含むフレームワーク内部が、この
on_mainを使って Ractors 対応のメモ化ロジックに書き換えられていく という形で影響が広がります。
- ActiveSupport が新たに
- Ractors を利用するアプリケーション・ライブラリは、この API を利用することでクラスインスタンス変数のメモ化などを安全に行いやすくなります。
注意点・設計上のトレードオフ
メイン Ractor へ処理が集中する
on_mainはあくまで「メイン Ractor に投げる」APIなので、呼び出しが多いとメイン Ractor がボトルネックになる可能性があります。- 高頻度で呼ばれる処理を全て
on_mainに載せると並列性が削がれるため、- 「本当にメイン Ractor でしか安全にできない初期化/メモ化」のみに絞る
- 一度メモ化した後は他 Ractor で読み取りだけを行う といった運用が必要です。
オートローダー連携案との比較
- 代替案として、Autoloader にフックしrbのような「Ractor 用の事前初期化メソッド」を呼ぶ方式も検討されました。
def ractorize! some_memo_method other_memo_method end - これは「Eager Load 的にすべてのメモ化を前もってやる」発想ですが、
- Autoloader と密結合になる
- Lazy Load との整合が複雑になる という理由で採用を見送られ、より汎用的で遅延評価とも両立しやすい
on_mainベースの方式を取っています。
- 代替案として、Autoloader にフックし
API の安定性
- この PR は ActiveSupport に新たな API と外部ライブラリを取り込むものであり、今後の Rails の Ractors サポートの基盤の一部になる可能性があります。
- とはいえ、まだ比較的新しい領域(Ractors 対応)なので、将来的な API の微調整・拡張は想定しておくべきです。
- 参考情報 (あれば)
- 対応 PR(クラスレベルメモ化の整理)
- 使用ライブラリ:
ractor-dispatch - この PR:
- タイトル: Add ActiveSupport::Ractors.on_main to proxy work to the main Ractor
- 番号: #57662
- 作成者: gmcgibbon
- マージ日時: 2026-07-08T17:59:45Z
#58057 Raise on ignored if_exists/if_not_exists in change_table check constraints
マージ日: 2026/7/8 | 作成者: @55728
- 概要 (1-2文で)
change_tableブロック内でt.check_constraint/t.remove_check_constraintに渡されたif_exists/if_not_existsオプションが無視されていた問題を修正し、他のメソッド同様にArgumentErrorを投げて誤用を検知できるようにした PR です。これにより、条件付きマイグレーションのつもりが実際には効いていない、というサイレントな失敗を防ぎます。
- 変更内容の詳細
背景
- Rails のマイグレーションでは、トップレベルのメソッド(例:
add_column,add_index,add_foreign_keyなど)に対してif_exists:/if_not_exists:オプションが使えます。 - 一方、
change_table :products do |t| ... endブロック内の DSL (t.column,t.index,t.foreign_keyなど) では、これらのオプションはサポートされていないため、渡されるとArgumentErrorを投げ、「条件付きの形(トップレベルのメソッド)を使ってください」というガイダンスを返す仕様になっています。 - しかし
t.check_constraintとt.remove_check_constraintだけはこのガードがなく、if_exists/if_not_existsを受け取ってそのままアダプタに渡してしまい、結果としてオプションが silently ignore(黙殺)される挙動になっていました。
この PR の変更点
対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rbactiverecord/test/cases/migration/change_table_test.rb
主な内容:
change_table内の check constraint メソッドにガードを追加t.check_constraintとt.remove_check_constraintがif_exists/if_not_existsを受け取った場合、他のメソッドと同じようにArgumentErrorを投げるように変更。- エラーメッセージの内容は、既に
column,index,foreign_key,remove_timestampsなどで使われているものと同系統で、「change_tableブロック内ではなく、条件付きのトップレベルメソッドを使うように」といったガイダンスが含まれます。
テストの追加
activerecord/test/cases/migration/change_table_test.rbに、次のようなコードがArgumentErrorを投げることを確認するテストが追加されています:
rubychange_table :products do |t| t.check_constraint "price > 0", name: "price_check", if_not_exists: true end change_table :products do |t| t.remove_check_constraint name: "price_check", if_exists: true endこれにより、「以前はこのコードが通っていたがオプションは無視されていた」「今後は明示的に例外が出て誤用に気付ける」という振る舞いの違いがテストで保証されます。
既存のガード (
remove_timestamps) のフォローアップ- 説明にもある通り、この PR は #57988(
remove_timestampsに同種のガードを追加した変更)のフォローアップで、change_tableDSL 全体で挙動を揃える意図があります。
- 説明にもある通り、この PR は #57988(
- 影響範囲・注意点
影響を受けるケース
既存のマイグレーションで、次のようなコードを書いている場合:
rubychange_table :products do |t| t.check_constraint "price > 0", name: "price_check", if_not_exists: true # あるいは t.remove_check_constraint name: "price_check", if_exists: true endこれまではマイグレーションが正常終了していたものの、
if_not_exists/if_existsは実際には効いていませんでした。この PR 適用後はマイグレーション時に
ArgumentErrorが発生するようになります。
どう直すべきか(推奨の書き方)
- 条件付きでチェック制約を追加・削除したい場合は、
change_tableブロックを使わず、トップレベルのマイグレーションメソッドを用いる必要があります。
例: チェック制約を「なければ作る」
rubydef change add_check_constraint :products, "price > 0", name: "price_check", if_not_exists: true end例: チェック制約を「存在すれば削除」
rubydef change remove_check_constraint :products, name: "price_check", if_exists: true end- もしくは
change_tableとは別にup/downを分けて書く、あるいは raw SQL を使うなど、これまでどおりの回避策も可能ですが、Rails が提供するトップレベル API を使うのが一貫性の観点で推奨されます。
- 条件付きでチェック制約を追加・削除したい場合は、
後方互換性の観点
- 仕様としては、「元々
change_table内の DSL でif_exists/if_not_existsをサポートしていなかった」という前提に合わせる修正なので「バグ修正」です。 - ただし、これまで「たまたま通っていた」既存マイグレーションが落ちる可能性があるため、Rails 更新後にマイグレーションを再実行する CI を回しておくと安全です。
- 仕様としては、「元々
- 参考情報 (あれば)
- 直接関連する PR:
- #57988 —
change_table内のremove_timestampsに対して同様のif_exists/if_not_existsガードを導入した PR。
- #57988 —
- 関連する API ドキュメント (Rails Guides / API):
- Active Record Migrations —
change_table,add_check_constraint,remove_check_constraint,if_exists/if_not_existsオプションの説明部分。
- Active Record Migrations —
- 設計上のポイント:
change_tableDSL はあくまで「テーブル変更をまとめて書きやすくするための糖衣構文」に近く、高度なオプション(条件付き実行など)はトップレベルメソッド側で提供する、という整理がこの PR で強化されています。
#57956 Bump MySQL Image version to 9.7
マージ日: 2026/7/8 | 作成者: @akhilgkrishnan
- 概要 (1-2文で)
Rails が内部で利用している MySQL のコンテナイメージバージョンを「9.7」に更新する PR です。Rails アプリ生成処理や関連テストで前提としている MySQL イメージタグを最新に追随させています。
- 変更内容の詳細
この PR が触っているのは、主に「Rails が自動生成する設定やテストコード内の MySQL イメージ指定」です。実装としては、旧バージョンの MySQL イメージタグを 9.7 に書き換えています。
具体的な箇所:
railties/lib/rails/generators/database.rb
Rails プロジェクト生成時に、選択された DB に応じて設定を組み立てるジェネレータクラスです。
MySQL を選択した場合のイメージ指定 (例: devcontainer / Docker 関連のデフォルト値) に使われているタグが 9.7 に更新されています。railties/lib/rails/generators/rails/app/templates/config/deploy.yml.tt
Rails アプリ生成時にconfig/deploy.ymlを作るためのテンプレートです。
ここでデプロイ・CI などに使う MySQL コンテナイメージ(例:image: mysql:9.7のような形)を参照しており、そのタグが 9.7 に更新されています。テストコード側の更新
railties/test/generators/app_generator_test.rbrailties/test/generators/db_system_change_generator_test.rbrailties/test/generators/devcontainer_generator_test.rb
これらはいずれも「ジェネレータが生成する設定ファイルに、期待した MySQL イメージバージョンが書き込まれているか」を検証するテストです。
旧バージョンの期待値から、9.7を期待するようにアサーションが修正されています。
行数レベルの変更であり、処理ロジックの変更はなく、ほぼ文字列(イメージタグ)の置き換えのみです。
- 影響範囲・注意点
新規に Rails アプリを生成するとき
- MySQL を選択した場合、生成される
config/deploy.ymlや devcontainer 関連設定で、MySQL 9.7 イメージが前提になります。 - これにより、開発・CI 環境で自動的に起動される MySQL のバージョンが上がる可能性があります。
- MySQL を選択した場合、生成される
既存プロジェクトへの影響
- 既存プロジェクトの
config/deploy.ymlや Docker 関連設定はこの PR では自動変更されません。 - ただし、Rails のバージョンアップに伴って
rails new相当をやり直したり、サンプルやガイドに倣って設定を書き換えると MySQL 9.7 を前提とするようになるため、互換性確認が必要です。
- 既存プロジェクトの
互換性・マイグレーション上の注意
- 9.7 というバージョンの MySQL イメージが、従来使っていたバージョンと後方互換でない場合、
- SQL モードのデフォルト変更
- 予約語の追加・挙動変更
- 文字コード・照合順序の変更
などによって、スキーマ定義やアプリケーションコードが影響を受ける可能性があります。
- バージョン間の Breaking change がないか、利用している MySQL イメージ(ベースディストリ/フォーク含む)のリリースノートを確認し、CI / ステージング環境でアップグレード検証を行うべきです。
- 9.7 というバージョンの MySQL イメージが、従来使っていたバージョンと後方互換でない場合、
テストへの影響
- Rails 自身のテストは、期待値が 9.7 に追随しているため問題ありません。
- 自前でジェネレータ出力を検証しているプロジェクト(テンプレート文字列をそのまま比較しているようなテスト)がある場合、MySQL イメージタグの期待値を更新する必要が生じる可能性があります。
- 参考情報 (あれば)
- この PR は主に「公式テンプレートで利用する MySQL イメージの推奨バージョン更新」であり、Rails の Active Record アダプタの実装などには手を入れていません。
- 実運用で MySQL 9.7 を利用する場合は、利用中の MySQL ディストリビューション(Oracle MySQL / MariaDB / Percona など)および該当イメージのドキュメント・リリースノートを確認のうえ、ローカル・CI・本番で同一メジャー/マイナーを揃えることが推奨されます。
#58050 Forward identifying options in remove_foreign_key's if_exists guard
マージ日: 2026/7/7 | 作成者: @55728
- 概要 (1-2文で)
remove_foreign_key ... if_exists: trueが、:nameや:to_tableで外部キーを特定した場合に例外を投げてしまう不具合を修正し、対象の外部キーが存在しなければ常に no-op になるよう統一されました。foreign_key_exists?の存在チェックに、後続の lookup と同じ識別オプションを正しく引き継ぐようにした変更です。
- 変更内容の詳細
問題の背景
remove_foreign_key には if_exists: true オプションがあり、「指定した外部キーがなければ何もしない(例外を出さない)」ことを保証するためのものです。
従来は以下のような位置引数形式では期待通りに動作していました。
remove_foreign_key :accounts, :branches, if_exists: true
# → 対象のFKがなければ何も起きないしかし、外部キーを :name や :to_table で指定したときに問題がありました。
remove_foreign_key :accounts, name: :special_fk, if_exists: true
remove_foreign_key :accounts, to_table: :owners, if_exists: trueこの場合、内部の存在チェック (foreign_key_exists?) に :column しか渡しておらず、:name や :to_table を使った「この外部キー」を正しく特定できていませんでした。
その結果:
- 存在チェックが「テーブルに何かしら外部キーが付いていれば true」となってしまう
if_existsガードが ショートサーキットされず、実際の削除処理に進んでしまう- 実際に指定された
:name/:to_tableの外部キーがなければArgumentErrorが発生
つまり、「存在しなければ何もしない」はずの if_exists: true を付けたのに、逆にエラーが出てしまうという、本来の意図と真逆の挙動になっていました。
これは特に「複数回実行されても安全なマイグレーション(idempotent migration)」を意図した場合に困る挙動です。
修正内容
この PR では、if_exists のための存在チェック部分に、後続の外部キー lookup と同じ識別オプションを渡すように修正しています。
具体的には:
foreign_key_exists?を呼ぶ際に、従来はcolumn:のみを渡していたが、:name,:to_table(および関連する識別情報)をそのまま転送するように変更
これにより、
remove_foreign_key :accounts, name: :special_fk, if_exists: true
remove_foreign_key :accounts, to_table: :owners, if_exists: trueのいずれも、該当する外部キーが存在しない場合は完全に no-opになり、ArgumentError は発生しません。
SQLite3 アダプタについて
SQLite3 アダプタ側でも同様の if_exists ガードがあり、こちらも :column のみを見ていたため同じ問題を抱えていました。
PR では SQLite3 の schema_statements も修正し、to_table によるスコープをきちんと尊重するようにしています。
- SQLite は外部キーに名前を持てないため、
:nameは効きませんが to_tableでスコープされるようになったことで、こちらでも期待どおり「対象FKがなければ no-op」という挙動になります。
テスト
activerecord/test/cases/migration/foreign_key_test.rb にテストが追加され、次のようなケースをカバーしています(要約):
remove_foreign_keyをname:指定 +if_exists: trueで呼び出し、対象外部キーが存在しない場合に例外が出ないこと- 同様に
to_table:指定 +if_exists: trueでも例外が出ないこと - 既存の位置引数形式(テーブル名/参照テーブル指定)との挙動が一貫していること
- 影響範囲・注意点
マイグレーションの安全性向上
- これまで、
name:やto_table:で外部キーを指定しつつif_exists: trueを付けていても、安全であるはずのマイグレーションがArgumentErrorで落ちる可能性がありました。 - 本修正により、
if_exists: trueの意図どおり、「外部キーがなくてもエラーにならない」ことが保証されます。
- これまで、
挙動の変化(注意点)
- もし既存コードが「
if_exists: trueを付けているが、実は例外が出ることを前提にしていた」ような特殊なケースがあると、今回の修正により例外が出なくなるため挙動変化になります。 - ただし
if_exists: trueのドキュメント上の意図からすれば、今回の修正後の挙動が正であり、以前のほうがバグと言えます。
- もし既存コードが「
SQLite3 での違い
- SQLite では外部キーに名前を付けられないため、
:nameでの特定は実質的に効きませんが、:to_tableのスコープは効くようになりました。 - そのため、
remove_foreign_key :accounts, to_table: :owners, if_exists: trueといった形は、SQLite でも安全に no-op になります。
- SQLite では外部キーに名前を付けられないため、
foreign_key_exists?の仕様に依存- 今回の修正は、
foreign_key_exists?/ForeignKeyDefinition#defined_for?がすでに:nameと:to_tableで一致判定を行う仕様に沿ったものです。この仕様が今後変わる場合は、この部分との整合性に注意が必要です。
- 今回の修正は、
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58050
- 類似修正:
remove_check_constraintに対する同様の修正コミットe27a878f4a - 関連メソッド:
ActiveRecord::ConnectionAdapters::SchemaStatements#remove_foreign_keyActiveRecord::ConnectionAdapters::SchemaStatements#foreign_key_exists?ActiveRecord::ConnectionAdapters::ForeignKeyDefinition#defined_for?
#58051 Freeze empty where clause predicates
マージ日: 2026/7/7 | 作成者: @gmcgibbon
- 概要 (1-2文で)
ActiveRecord のWhereClause::Emptyオブジェクトを freeze することで、Ractor(Ruby の並行実行モデル)に対応した PR です。where句が「空」の場合に使われる共有オブジェクトを不変にし、安全にスレッド/Ractor 間で扱えるようにしています。
- 変更内容の詳細
何をしているか
ActiveRecord::Relation::WhereClause::Emptyに相当する空の where 句プレディケートを「freeze(凍結=不変化)」する変更が 1 行だけ入っています。- 具体的には、
WhereClause内で共有される「空集合」を表す定数 or オブジェクト定義に.freezeを追加した形です(差分は +1/-1 行なので、EMPTY_PREDICATES = [].freezeのような書き換えが入ったと推測できます)。
Ruby の Ractor では、Ractor 間で共有できる値は「イミュータブル(不変)」である必要があります。freeze することで WhereClause::Empty が Ractor の「共有可能オブジェクト」の条件を満たすようになります。
関連背景
- 説明文中にある通り、Ractor 対応を進めた以前の PR(https://github.com/rails/rails/pull/57475)での漏れを補完するものです。
ActiveRecord::Relation::WhereClause::Emptyは、「where 条件が何も無い Relation」を表現するためによく再利用されるオブジェクト(シングルトン的な役割)です。このような共有オブジェクトを Ractor 安全にすることが目的です。
簡単なイメージコード
※実際のコードとは多少異なる可能性がありますが、イメージとしては以下のような修正です。
# 修正前(イメージ)
EMPTY = WhereClause.new([])
# 修正後(イメージ)
EMPTY = WhereClause.new([]).freezeもしくは配列ベースで:
# 修正前
EMPTY_PREDICATES = []
# 修正後
EMPTY_PREDICATES = [].freeze- 影響範囲・注意点
主な影響範囲
Model.where(...)などで、結果として「条件なし」になる Relation を扱うコード全般。ActiveRecord::Relation::WhereClause::Emptyを直接触ることは通常ほぼ無いので、一般的なアプリコードへの影響は極めて小さいです。- Ractor を使用している / これから使おうとしている Rails アプリケーションにとっては、Active Record の WhereClause 周りが一段と安全になります。
互換性・破壊的変更の可能性
- もともと「空の where 句」オブジェクトはライブラリ内部で共有・再利用される想定のため、アプリケーションコード側でこれをミューテート(破壊的変更)することは非推奨かつ異常な利用です。
- もし内部オブジェクトを直接いじっていた場合(例:
relation.where_clause.send(:predicates) << ...のようなハック)には、FrozenErrorが発生する可能性があります。 - ただし、そのようなケースはほとんど存在しないと考えられるため、通常の利用では後方互換性への影響は実質ゼロに近いです。
テスト・ドキュメント
- PR のチェックリスト上、「テスト追加」「CHANGELOG 更新」は行われていません。
- 挙動の変化は「内部オブジェクトが凍結されるようになった」という極小の実装詳細レベルであり、外部 API の仕様変更ではないため、テスト追加や CHANGELOG の更新は不要と判断された可能性が高いです。
- 参考情報 (あれば)
- Ractor について:
- Ruby 3 以降で導入された並行実行モデル。Ractor 間で共有できるのは「freeze 済みオブジェクト」や「コピーされたデータ」などに限られるため、ライブラリ側で共有の可能性があるオブジェクトを積極的に freeze する必要があります。
- 関連 PR:
- 以前の Ractor 対応 PR: https://github.com/rails/rails/pull/57475
- 今回の PR: https://github.com/rails/rails/pull/58051
#58048 Fix ActionController::Live streams hanging on client disconnect
マージ日: 2026/7/7 | 作成者: @wpeterson
- 概要 (1-2文で)
ActionController::Live でクライアント切断時にストリーム処理がハングし、スレッドが解放されない不具合を修正した PR です。Buffer#abort時にも確実にコンシューマループが終了できるよう、nil 終端の enqueue とブロードキャスト処理を追加し、その挙動を確認する回帰テストを導入しています。
- 変更内容の詳細
問題の背景
ActionController::Live では、レスポンスボディをバックグラウンドスレッドでストリーミングするために ActionController::Live::Buffer が使われています。
- プロデューサ側:
Buffer#write等でSizedQueueにチャンクを push - コンシューマ側:
Buffer#each_chunkでSizedQueue#popをループしつつ読み出し- 終了条件は「
popがnil(もしくは falsy)を返したとき」
- 終了条件は「
- 正常終了:
Buffer#closeが queue にnilを enqueue(終端マーカー)→each_chunkが抜ける
バグは「異常終了パス」である Buffer#abort の挙動にありました。
Buffer#abort- キューを
clearする @abortedフラグを立てる- しかし
nilを enqueue しない
- キューを
- 一方
each_chunkは、- 「
queue.popが falsy なら break」というロジックで、@abortedは一切見ていない
- 「
このため、以下のようにハングが発生します。
abort 発火時に、コンシューマが空キュー上で
pop待ちしているケース#abortはキューを clear(元々空)して@abortedを設定するだけで、push しない
→SizedQueue#popが永遠に戻らず、コンシューマスレッドがブロックし続ける
#closeと#abortの競合レース#closeが終端のnilを enqueue- 直後に
#abortがclearしてしまい、そのnilを消してしまう
→ コンシューマがnilを受け取れず、やはりpopでブロックし続ける
このブロックはリクエスト処理スレッドを解放しないため、特に RAILS_MAX_THREADS=1 のような少スレッド構成だと、1 本のハングしたストリームでワーカーが永久に詰まるという致命的な問題になります。
修正内容
ActionController::Live::Buffer#abort の実装を、#close と同様に「確実に終端マーカーを通知する」形に変更しています。
ポイント:
#abortでやること- キューを
clearする @abortedを立てる(既存)nilを enqueue する(新規)- ブロードキャストして待機中のスレッドを起こす(新規)
- キューを
実装上の意図:
clearの後でnilを pushSizedQueueが満杯の状態で#abortすると、clearにより中身が空になり、その後のpush(nil)は必ずノンブロッキングで成功する- これにより
#abort自体が queue のサイズ制限に引きずられてブロックすることを防ぐ
#closeと#abortの両方が呼ばれても安全closeもabortもnilを enqueue する可能性があるが、コンシューマ側は最初に読んだnilでループを抜ける
→ 終端マーカーが重複しても問題ない
概念的な疑似コードイメージ:
class Buffer
def abort
@queue.clear
@aborted = true
@queue << nil # 終端マーカー
@cond.broadcast # 待機スレッドに終了を知らせる
end
def each_chunk
while (chunk = @queue.pop)
yield chunk
end
# nil を受け取るとループを抜ける
end
end※実際のコードはもう少し複雑ですが、ロジックの本質はこのイメージです。
テストの追加
actionpack/test/controller/live_stream_test.rb に 2 つの回帰テストが追加されています。
「
each_chunk中に#abortされた場合に reader が解放される」テスト- 再現手順:
- 別スレッドで
buffer.each_chunkを呼んでブロックさせる - メインスレッドで
buffer.abortを呼ぶ
- 別スレッドで
- 期待:
each_chunkのスレッドがハングせずにjoinで終了することを確認
- 再現手順:
「
#closeの直後に#abortが走るレースでも、reader が解放される」テスト- 再現手順:
buffer.closeを呼んだ直後にbuffer.abortを呼ぶような並行パターンを構成
- 期待:
- このレース条件下でも
each_chunkを実行中のスレッドが終了できる
- このレース条件下でも
- 再現手順:
どちらのテストも、現行 main ブランチでは reader が join タイムアウトまでハングして失敗し、この修正を入れるとパスすることが確認されています。actionpack/test/controller/live_stream_test.rb 全体のテストもパスしています。
また、actionpack/CHANGELOG.md にはこの不具合修正に関するエントリが追加されています。
- 影響範囲・注意点
影響範囲
ActionController::Liveを利用しているアプリケーション全般、とくに以下に該当するもの:- SSE(Server-Sent Events) やロングポーリングなど、長時間ストリーミングするエンドポイント
RAILS_MAX_THREADSが小さい構成(1〜数スレッド)で、スレッド枯渇に敏感な環境
- クライアントがページ離脱やネットワーク切断などでコネクションを突然切るケースで、ワーカーがハングしにくくなります。
注意点
- アプリケーション側で独自に
ActionController::Live::Bufferを直接いじっていない限り、互換性リスクは非常に低いです。 #abortが呼ばれた場合でも、コンシューマ側のeach_chunkループは 「nil が返ってきた」という通常終了パスと同じように抜ける ため、@abortedを見て特別扱いしているような独自コードが無い限り、挙動は「ハングしなくなる」以外は変わりません。- 複数回
abort/closeが呼ばれるようなケースでも、余分なnilがキューに入るだけで、コンシューマは最初の 1 つで停止するため悪影響はありません。
- アプリケーション側で独自に
- 参考情報 (あれば)
- 対応 Issue: #58047
- 対応 PR: #58048(本 PR)
- 関連クラス/モジュール:
ActionController::LiveActionController::Live::BufferSizedQueue(標準ライブラリthread)
- テストファイル:
actionpack/test/controller/live_stream_test.rb
- 実運用上の示唆:
- Live ストリーミングを利用するアプリで、ワーカーが徐々に詰まっていく問題(特にクライアント切断が多い環境)に遭遇していた場合、この修正を含む Rails バージョンへのアップデートで改善が見込めます。
#58039 Support endless and beginless ranges in number_field_tag/range_field_tag
マージ日: 2026/7/7 | 作成者: @55728
- 概要 (1-2文で)
number_field_tag/range_field_tagに対して:in/:withinオプションに「終端なし(endless)」や「始端なし(beginless)」の Range を渡したときにRangeErrorになっていた問題を解消し、適切なmin/max属性を出力するようにした PR です。既に対応済みだったnumber_field/range_fieldと挙動を統一する変更です。
- 変更内容の詳細(サンプルコード含む)
変更前の挙動
:in / :within に endless / beginless range を渡すと、Range#max / Range#min を取得しようとして RangeError が発生していました。
number_field_tag("quantity", nil, in: 18..)
# RangeError: cannot get the maximum of endless range
range_field_tag("volume", nil, in: ..11)
# RangeError: cannot get the minimum of beginless range内部実装としては、options[:in] などに Range が渡された場合に range.min, range.max によって min / max 属性を決定しており、endless / beginless range に対して不正なアクセスをしていた形です。
変更後の挙動
endless / beginless range を正しく扱い、HTML 属性に反映できるようにしました。
- endless range (
18..) →min属性のみを付与、maxは付けない - beginless range (
..11) →max属性のみを付与、minは付けない
number_field_tag("quantity", nil, in: 18..)
# => <input name="quantity" id="quantity" type="number" min="18" />
range_field_tag("volume", nil, in: ..11)
# => <input name="volume" max="11" id="volume" type="range" />通常の「両端あり」の Range(1..10 や 1...10)の挙動は変わりません。特に排他的範囲(...)についてはこれまで通り「最大値 − 1」で max が設定されます。
# 変更後も従来通り
number_field_tag("quantity", nil, in: 1...10)
# => max="9"実装レベルの補足
- 修正は
ActionView::Helpers::FormTagHelper内の range オプション処理ロジックの 1 箇所のみ(+1/-1)で、endless / beginless range に対してmax/minを無条件に取得しないように分岐・ロジックを調整しています。 form_tag_helper_testに endless / beginless range を渡したケースのテストが追加され、期待されるmin/max属性の有無を検証しています。CHANGELOGに挙動変更が追記されています。
- 影響範囲・注意点
- 影響範囲は
number_field_tag/range_field_tagを:in/:withinオプション付きで使っている箇所に限定されます。 - 今まで endless / beginless range を渡していた場合は
RangeErrorで落ちていたため、今回の変更によりそのようなコードが動作するようになります(=「今まで動いていた挙動の変更」ではなく「今まで例外だったパターンがサポートされた」)。 - both-bounded range(通常の範囲)や排他的範囲(
...)の既存挙動は維持されているため、既存のmin/maxの値が変わることはありません。 - 既に
number_field/range_field(form_for / form_with 系ヘルパ)では endless / beginless range がサポートされており、今回の変更で*_tag系とも挙動が揃いました。form_for→form_tagなどへ書き換えているコードベースでは、endless / beginless range の扱いが一貫するようになります。
- 参考情報 (あれば)
- この PR は、
number_field/range_fieldで endless / beginless range をサポートしたコミット(530beb8550f140c21b790d6acdf990931e45e16b)との整合性をとる意図で行われています。 - HTML 側の仕様的には
min/maxはともに任意属性であり、どちらか一方だけの指定も問題なく許容されるため、この変更は HTML 仕様に沿ったものです。
#58040 Treat an empty id array as an empty set for composite primary keys
マージ日: 2026/7/7 | 作成者: @55728
- 概要 (1–2文で)
複合主キー(Composite Primary Key)を持つモデルに対してdestroy([])やupdate([], [])を呼ぶとNoMethodErrorになっていた問題を修正し、空配列を「空のID集合」として扱うようにしたPRです。単一主キーのモデルと同様に、これらの呼び出しが安全な no-op として[]を返すようになります。
- 変更内容の詳細
問題の挙動
複合主キーを持つモデルで、以下のような呼び出しをすると:
CompositePkModel.destroy([]) # NoMethodError (undefined method `destroy' for []:Array)
CompositePkModel.update([], []) # NoMethodError (undefined method `update' for []:Array)原因は、内部で「複数 ID を扱うコードパス」と「単一 ID を扱うコードパス」を分岐する条件が、空配列を正しく「複数 ID」とみなせていなかったためです。
destroy([])/update([], [])で渡された配列が- 「複数の ID」か
- 「単一 ID(複合主キーなら配列1つ)」か を判定するロジックが、
first.is_a?(Array)などのチェックを行っていましたが、空配列の場合はこの判定に入らず、単一 ID 用のコードパスに落ちてしまっていました。
その結果:
find([])が呼ばれるfindは[]を返す- その戻り値 (
[]) に対して.destroy/.updateを呼び、NoMethodErrorになる
一方、単一主キーのモデルでは、destroy([]) / update([], []) は既に「何もしないで [] を返す」挙動になっており、複合主キーだけが不整合な状態でした。
修正内容
2カ所の「複数IDかどうか」を判断するメソッドに、value.empty? のショートサーキットを追加しています。
対象のメソッド:
ActiveRecord::Key::Composite#expects_multiple_ids?Relation#destroyで使用される- 複合主キーに対して
Model.destroy(ids)を呼ぶ際の挙動に影響
ActiveRecord::Persistence::ClassMethods#update_multiple_ids?Model.update/Model.update!で使用される- 複合主キーでの
update([], [])の挙動に影響
どちらも以下のような形で修正されています(イメージ):
def expects_multiple_ids?(value)
return false if value.empty? # ここが追加・変更点
# 以降、first.is_a?(Array) などの既存ロジック
endvalue.empty? の時点で「複数 ID でも単一 ID でもなく、空集合」とみなして即座に処理を分岐させることで、単一 ID ブランチに落ちてエラーを起こすのを防いでいます。
既存の find_with_ids はすでに空配列に対してショートサーキットしており:
return [] if first_item.empty?というロジックを持っているため、find 自体の挙動は変更していません。
Before / After の挙動
CompositePkModel.destroy([]) # Before: NoMethodError / After: []
CompositePkModel.update([], []) # Before: NoMethodError / After: []テストも追加されており:
activerecord/test/cases/key_test.rbactiverecord/test/cases/persistence_test.rb
で、空配列を渡したときに [] が返ることが確認されています。
- 影響範囲・注意点
- 影響を受けるのは「複合主キーを持つ ActiveRecord モデル」に対するクラスメソッド:
Model.destroy([])(Relation#destroy経由も含む)Model.update([], [])/Model.update!([], [])
- これらが例外を投げず、単一主キーの場合と同様に「何もしないで
[]を返す」ようになります。
運用・コード上の影響:
- これまで
destroy([])/update([], [])がNoMethodErrorになることに依存したコードがある場合、その挙動は変わりますが、通常は依存していない前提のはずで、実用上は「バグ修正」と考えてよい変更です。 - 既に単一主キーでは同じ挙動であるため、「複合主キーだけ特別扱い」する必要がなくなります。汎用的なコード(単一主キー/複合主キーを意識しない共通処理)で扱いやすくなります。
- ロジックの変更箇所が小さく、また空配列に対する振る舞いのみを変えているため、既存の非空配列や単一 ID に対する挙動への影響は極めて限定的です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58040
- 関連する内部API:
ActiveRecord::Key::Composite#expects_multiple_ids?ActiveRecord::Persistence::ClassMethods#update_multiple_ids?ActiveRecord::FinderMethods#find_with_ids(空配列のショートサーキットあり)
#58041 Apply DISTINCT to average
マージ日: 2026/7/7 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Relation#averageがdistinct指定を無視していた問題を修正し、distinct.averageが SQL のAVG(DISTINCT column)を使うようになりました。これにより、distinct.count/distinct.sumと平均値の挙動が一貫します。
- 変更内容の詳細
何が問題だったか
distinct なリレーションに対して集計を行うとき、以下のように動作が揃っていませんでした:
# credit_limits: [50, 50, 60]
relation = firms.distinct # など、DISTINCT が効いている Relation を想定
relation.sum(:credit_limit) # => 110 (DISTINCT が効いている)
relation.count(:credit_limit) # => 2 (DISTINCT が効いている)
# しかし…
relation.average(:credit_limit) # => 53.33... (AVG(credit_limit) になり DISTINCT 無視)SQL レベルでも AVG(credit_limit) が発行されており、DISTINCT が付かないため、重複値を含めて平均してしまっていました。同様の問題は group(:firm_id).distinct.average(:credit_limit) のようなグループ集計時にも発生していました。
期待される動作は:
relation.distinct.average(:credit_limit)
# SQL: AVG(DISTINCT credit_limit)
# => 55.0 (重複 50 を 1 回として扱う)今回の修正内容
activerecord/lib/active_record/relation/calculations.rb の average 実装が修正され、以下の2パスで DISTINCT が平均計算に反映されるようになりました。
グルーピングなしの average
relation.distinct.average(:credit_limit)→AVG(DISTINCT credit_limit)を生成
group を伴う average
relation.group(:firm_id).distinct.average(:credit_limit)
→AVG(DISTINCT credit_limit)を用いたグループ集計を生成
これにより:
# 例: firm_id ごとの credit_limit が [50, 50, 60] で 50 が重複している場合
Firm.group(:firm_id).distinct.average(:credit_limit)
# これまで: { 1 => 53.33... } (DISTINCT 無視)
# これから: { 1 => 55.0 } (AVG(DISTINCT credit_limit))minimum / maximum への影響
minimum / maximum には DISTINCT を付けても結果が本質的に変わらないため、今回もこれらには DISTINCT を付けません。
たとえば:
MIN(column) == MIN(DISTINCT column)
MAX(column) == MAX(DISTINCT column)となるため、クエリの意味的にもパフォーマンス的にも特にメリットがない、という判断です。
テスト
activerecord/test/cases/calculations_test.rb にテストが追加されており、
- 非グループ average + distinct
- group + distinct + average
の両方でDISTINCTが平均値計算に効くことがカバーされています。
- 影響範囲・注意点
互換性 (Breaking change の可能性)
- 既存アプリで
distinct.averageを使っており、現在の「重複込みの平均値」を前提にしてロジックやテストを書いていた場合、結果が変わります。 - 具体的には、いままで
averageだけがdistinctを無視していた挙動が修正され、count/sumと一貫した扱いになるため、「バグ修正」ではあるものの互換性に影響しうる変更です。
- 既存アプリで
期待される利用方法
- 「重複レコードを除いた値集合の平均」を取りたい場合:ruby
Model.distinct.average(:column) - 「重複も含めてレコードごとの平均」を維持したい場合:
distinctを外すか、- 必要であればサブクエリやウィンドウ関数などを用いて、意図した集計を明示的に書く必要があります。
- 「重複レコードを除いた値集合の平均」を取りたい場合:
group との組み合わせ
group(:key).distinct.average(:value)を使っているコードも結果が変わる可能性があります。- 特に、
joins+distinct+groupで「重複JOINを消すためだけに distinct を付けている」ようなケースでは、平均値にDISTINCTが効いてしまう点に注意してください。
- 参考情報 (あれば)
- 同様の挙動は過去に
sumでも修正されており、そのときと同じポリシーに揃える変更です:sumの ungrouped な distinct 対応:566f1fd068(fixes #16791)sumの grouped な distinct 対応:00e399d315(#57896)
averageについての長年の報告: issue #39857 (stale でクローズされていたが、今回の PR で解決された形)。
#58044 Resolve default_priority lazily so a configured value is honored
マージ日: 2026/7/7 | 作成者: @55728
- 概要 (1-2文で)
ActiveJob でdefault_priorityをイニシャライザなどで後から設定してもジョブに反映されなかった問題を修正し、優先度を参照するタイミングでdefault_priorityを遅延評価するように変更した PR です。これによりdefault_queue_nameと同じ挙動・パターン(lambda での遅延デフォルト解決)に揃えられています。
- 変更内容の詳細
何が問題だったか
従来の挙動:
# config/initializers/active_job.rb
ActiveJob::Base.default_priority = 10
SomeJob.new.priority
# => nil (本来は 10 を期待する)原因:
ActiveJob::QueuePriorityconcern が include されるタイミングでpriorityクラス属性に「その時点のdefault_priorityの値」が固定的にコピーされてしまっていた。- Rails のロード順の関係で、initializer が走るより前に、このクラス属性の初期化が行われるケースがあり、 その後に
ActiveJob::Base.default_priority = 10としても、すでにクラス側にはnilがセット済みで、上書きされない。
結果として、「起動後に default_priority を設定しても、一部のジョブクラスでは反映されない」というバグになっていました。
今回の変更内容
default_priority を「値として即時にコピー」するのではなく、「lambda 経由で遅延解決」するように変更しています。default_queue_name ですでに使われているパターンに合わせた形です。
変更点の要旨:
activejob/lib/active_job/queue_priority.rbpriorityクラス属性のデフォルト値を直接な値ではなく、-> { default_priority }のような lambda(Proc)に変更。- これにより、ジョブが
priorityを読むたびに「その時点のdefault_priority」が評価される。
activejob/test/cases/queue_priority_test.rb- 上記バグを再現するテストを追加し、修正後の期待挙動を検証。
- 例: initializer 的なタイミング(ロード後)で
ActiveJob::Base.default_priority = 10をセットし、その後に生成したジョブインスタンスのpriorityが 10 になることを確認するテストが追加されている。
概念的なサンプル(イメージ):
# 変更前(イメージ)
class ActiveJob::Base
class_attribute :priority, default: default_priority # ここで値が固定的にコピーされる
end
# 変更後(イメージ)
class ActiveJob::Base
class_attribute :priority, default: -> { default_priority } # 読み出し時に default_priority を評価
endこのおかげで、initializer で default_priority を設定・変更すると、その後に生成されるジョブインスタンスから参照される priority に反映されるようになります。
テストで確認される挙動(PR の説明通り):
ActiveJob::Base.default_priority = 10
SomeJob.new.priority
# Before: nil
# After: 10- 影響範囲・注意点
設定方法
ActiveJob::Base.default_priority = 10を「initializer など、アプリケーション起動後のタイミング」で行っても、確実に反映されるようになります。- これまで「なぜか priority が反映されない」としてワークアラウンド(各ジョブで
self.priority = 10を書くなど)をしていた場合は、それらが不要になる可能性があります。
既存コードへの影響
- 一般的な use case では後方互換的な修正であり、既存アプリが壊れる可能性は低いです。
- ただし「
priorityクラス属性に直接値を入れていた」ようなコードがある場合は、その挙動とdefault_priorityの組み合わせを確認した方が安全です。SomeJob.priority = 5のような明示的な上書きは、これまで通り個別ジョブごとの設定として優先されるはずです(クラス属性の直接代入なので、lambda デフォルトの上にさらに値が乗る形)。
パフォーマンス影響
priorityのデフォルトが lambda に変わることで「読み出し時に 1 回 Proc を評価する」オーバーヘッドが増えますが、非常に軽微で、通常のジョブ実行規模では問題にならないと考えられます。
挙動の一貫性
default_queue_nameと同じ「遅延評価パターン」になるため、ActiveJob の API の一貫性が向上しています。- 「キュー名は後から変えられるのに、優先度は変わらない」というアンバランスさが解消されます。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58044
- 関連する既存の仕組み:
ActiveJob::Base.default_queue_name(すでに lambda ベースのデフォルト解決を採用)class_attributeにおけるデフォルト値の lambda 指定パターン(読取時評価)
#58034 Support log on ActiveSupport::ProxyLogger
マージ日: 2026/7/7 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::ProxyLoggerが標準ライブラリLoggerのインターフェイスと揃うように、logメソッドをaddのエイリアスとしてサポートする変更です。これにより、ProxyLogger経由でlogを呼び出した際にNoMethodErrorにならず、適切なレベル判定を行ってログが出力されるようになります。
- 変更内容の詳細
背景
- Ruby の標準ライブラリ
Loggerにはaddメソッドがあり、logはそのエイリアスとして定義されています。 - Rails には複数のラッパーロガーがあり、例えば
ActiveSupport::BroadcastLoggerはlogをaddに委譲しています。 - 一方で
ActiveSupport::ProxyLoggerは「ほぼ完全なLogger互換インターフェイス」を目指しているにもかかわらず、logが未定義だったため、log呼び出しでNoMethodErrorが発生していました。
logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error)
logger.log(Logger::ERROR, "msg") # これが NoMethodError になっていたこのPRでの変更点
コード上の変更 (activesupport/lib/active_support/proxy_logger.rb)
ProxyLoggerにlogをaddのエイリアスとして追加。
擬似的には次のような変更イメージです(実際のコードは1行のみ):
class ActiveSupport::ProxyLogger
# 既存: add は、指定の severity 以上かどうかを判定して元の logger に委譲する
# def add(severity, message = nil, progname = nil, &block)
# ...
# end
alias_method :log, :add
endこれにより、ProxyLogger は標準 Logger と同様、log(severity, message = nil, progname = nil, &block) という形での呼び出しが可能になります。
テストの追加・修正 (activesupport/test/proxy_logger_test.rb)
logを呼び出したときに、従来のadd経由と同様の挙動をすることを確認するテストを追加。- 具体的には以下のようなケースがカバーされていると考えられます:
ProxyLoggerに設定した severity 以上のログレベルでlogを呼ぶと、内部の logger へメッセージが転送される。- 設定した severity 未満のレベルの場合は無視される(元の
addと同じ判定ロジックの再利用)。
使用例
logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error)
# エラー以上だけログさせたい ProxyLogger
# これまでは:
# logger.add(Logger::ERROR, "error message") # OK
# logger.log(Logger::ERROR, "error message") # NoMethodError
# これからは:
logger.log(Logger::ERROR, "error message") # 標準 Logger と同じように動く
logger.log(Logger::INFO, "info message") # ProxyLogger のレベルが :error なので無視される- 影響範囲・注意点
影響範囲
ActiveSupport::ProxyLoggerを直接利用しているコードで、Logger互換インターフェイスを期待してlogを呼び出していた場合に挙動が変わります。- 以前:
NoMethodErrorが発生 - 今後:
addと同様にログ出力される(severity フィルタリング付き)
- 以前:
- つまり、これまで意図せず落ちていたコードが「正しく動く」ようになるため、実運用上は改善方向の変更です。
BroadcastLoggerは既にlogをサポートしているため、ProxyLogger/BroadcastLoggerを抽象的に扱うコードでインターフェイス差異を気にする必要がなくなります。
注意点
logメソッドのシグネチャはLogger#log/Logger#addに準拠しているため、第一引数に severity を整数(Logger::DEBUGなど)で渡すことを前提としています。ProxyLogger特有の severity 制御(ActiveSupport::ProxyLogger.new(underlying_logger, :error)のように指定するフィルタ)は、log呼び出しでもそのまま有効です。
そのため、「logなら常に素通しされる」といった特別扱いはなく、一貫してaddと同じルールでフィルタされます。NoMethodErrorを利用した分岐(例:respond_to?(:log)でProxyLoggerかどうか判断していたようなコード)があれば、その挙動は変わる可能性がありますが、一般的にはそのような依存は推奨されません。
- 参考情報 (あれば)
- Ruby 標準
Loggerドキュメント:logはaddのエイリアスとして定義されているlog(severity, message = nil, progname = nil) { ... }
- 関連クラス:
ActiveSupport::ProxyLogger- 既存のロガーに対して、severity でフィルタしつつ委譲するラッパー
ActiveSupport::BroadcastLogger- 複数のロガーに同報するラッパーで、既に
logをサポートしている
- 複数のロガーに同報するラッパーで、既に
このPRにより、ProxyLogger も「標準 Logger ほぼフル互換」という設計意図に沿った実装になり、標準 Logger を想定したコードとの互換性が向上します。
#58023 Honor an explicit value: nil in color_field
マージ日: 2026/7/7 | 作成者: @55728
- 概要 (1-2文で)
color_fieldヘルパーが、明示的にvalue: nilが指定された場合でもモデルの値で上書きしてしまう不具合を修正し、value:オプション(nilを含む)を正しく尊重するようにした PR です。これにより「入力欄をあえて空にしたい」というケースが期待通り動作します。
- 変更内容の詳細(あればサンプルコードも含めて)
問題点(Before)
form_with / form_for などで color_field を使うとき、次のように value: nil を渡しても:
color_field("car", "color", value: nil)本来は <input ...> の value 属性が付かないことを期待しますが、実際にはモデルの値(例: #000fff)が入っていました。
<!-- Before の実際の出力例 -->
<input id="car_color"
name="car[color]"
type="color"
value="#000fff" />つまり、「あえて値を空にしたい」という明示的な指定が無視されていた状態です。
修正内容(After)
今回の PR により:
value:オプションが指定された場合は、それがnilであっても優先されるvalue:オプションが指定されていない場合のみ、従来どおりモデルの属性値が使われる
結果として、同じコード:
color_field("car", "color", value: nil)は次のように value 属性なしでレンダリングされるようになります。
<!-- After の出力例 -->
<input id="car_color"
name="car[color]"
type="color" />内部的な変更は color_field タグ生成ロジックの 1 行のみで、value: が明示されているかどうかの判定方法を修正したものと考えられます(options.key?(:value) を見る形など)。
テスト・ドキュメント
actionview/test/template/form_helper_test.rbにテストが追加され、value: nilを指定したときにvalue属性が出力されないこと
が検証されるようになっています。
actionview/CHANGELOG.mdに今回の変更点が追記されています。
- 影響範囲・注意点
- 影響するのは
ActionView::Helpers::FormHelper#color_fieldのみです。 - これまで「
color_fieldにvalue: nilを渡しても結局モデル値が表示される」ことを前提に回避策を組んでいた場合、その挙動が変わります(ただし、そのような依存は通常あまりないと考えられます)。 - 同様の仕様(「
valueが明示されていればそれを優先、なければモデル値」)は他のヘルパーでも一般的なため、今回の変更によりcolor_fieldも他のフォームヘルパーと一貫した動きをする 形になります。 - もし「常にモデルの値を出したい」のであれば、
value:オプションを明示的に渡さないようにするのが正しい利用方法になります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58023
- 関連ファイル:
actionview/lib/action_view/helpers/tags/color_field.rbactionview/test/template/form_helper_test.rbactionview/CHANGELOG.md
#58035 Don't mutate the array passed to RedisCacheStore via :client
マージ日: 2026/7/7 | 作成者: @55728
- 概要 (1-2文で)
RedisCacheStore のコンストラクタにclient:オプションとして渡した配列を内部で破壊的に変更しないようにし、FrozenErrorが発生しないように修正した PR です。redis-clientベースの未リリース実装にのみ影響する挙動で、本修正によりドキュメントどおりの使い方が安全に行えるようになります。
- 変更内容の詳細
何が問題だったか
ActiveSupport::Cache::RedisCacheStore.new(client: ...) に以下のような配列を渡すと:
configs = [RedisClient.config(url: url1), RedisClient.config(url: url2)]
ActiveSupport::Cache::RedisCacheStore.new(client: configs)従来のコードでは、RedisCacheStore 内部処理で 呼び出し元が渡した configs 配列自体を破壊的に変更し、RedisClient::Config の配列を RedisClient::Pooled の配列に置き換えていました。
configs
# => [#<RedisClient::Pooled ...>, #<RedisClient::Pooled ...>]
# 本来は Config のままでいてほしいのに、呼び出し元の配列が書き換わってしまうさらに、呼び出し元が防御的に configs.freeze して渡すと、内部で配列を変更しようとして FrozenError が発生していました。
ActiveSupport::Cache::RedisCacheStore.new(client: configs.freeze)
# => FrozenError: can't modify frozen Arrayこの使い方自体は、RedisCacheStore の RDoc に公式に記載されているパターンです:
config.cache_store = :redis_cache_store, client: [
RedisClient.config(...),
RedisClient.config(...)
]つまり「公式に推奨している書き方が、内部実装の都合で配列を書き換えてしまう/freeze すると例外になる」という不整合がありました。
何を変えたか
修正後は以下のような動作になります。
RedisCacheStore内部で 独自の配列を新たに作成して プール (RedisClient::Pooled) を保持する- 呼び出し元から渡された
client:の配列オブジェクト- その中身の
RedisClient::Configオブジェクト を一切変更しない
- 結果として、
configsをfreezeして渡しても問題なく動作する
イメージとしては、以前はこうだったものを:
# 擬似コードイメージ(before)
@clients = options[:client] # ここで呼び出し元の配列をそのまま使う
@clients.map! { |config| RedisClient::Pooled.new(config) } # 呼び出し元の配列を破壊的変更このように書き換えた形です:
# 擬似コードイメージ(after)
configs = options[:client]
@clients = configs.map { |config| RedisClient::Pooled.new(config) } # 新しい配列を生成
# configs 自体も、その要素である config も変更しないテスト (activesupport/test/cache/stores/redis_cache_store_test.rb) には、
- 渡した
client配列とその要素が変化していないこと client配列をfreezeしても例外にならないこと
を確認するケースが追加されています。
なお、この挙動変更は redis-client ベースの未リリース実装 (コミット 5843a7b7f2) に対するもので、gem としてリリースされた Rails にはまだ載っていない段階の問題を修正しています。
また、今回の修正方針は、以前 Cache::Store#delete_multi に渡される names 配列をミューテートしないようにしたコミット (32d9437962) と同じ考え方に揃えたものです。
- 影響範囲・注意点
- 対象:
ActiveSupport::Cache::RedisCacheStoreを redis-client ベース実装で利用する場合のみ- まだ正式リリース前の実装向けの修正なので、現行の安定版 Rails には直接の影響はありません
- 振る舞いの変更点:
client:に渡した配列・中身が これまで暗黙に書き換えられていたケースがあれば、その前提は壊れます- ただし、そのような依存は非意図的かつ望ましくないため、多くの場合は「壊れる」のではなく「正しくなる」方向です
- freeze 対応:
client: configs.freezeのように、防御的にfreezeをかけて渡しても動作するようになります- 設定オブジェクトをイミュータブルに扱いたい場合に安全です
- 既存コードへの互換性:
- 「
RedisCacheStoreに渡した結果として、呼び出し元のconfigsがPooledの配列に変わることに依存している」ような特殊なコードがあれば挙動は変わりますが、そのような依存は避けるべきです - 通常の・ドキュメントどおりの使い方をしている限りは、実質的にはバグ修正であり互換性上の問題はほぼありません
- 「
- 参考情報 (あれば)
関連コミット:
- redis-client ベース実装導入:
5843a7b7f2 Cache::Store#delete_multiで引数配列をミューテートしないようにしたコミット:32d9437962
- redis-client ベース実装導入:
ドキュメント上の利用例(問題になっていたパターン):
rubyconfig.cache_store = :redis_cache_store, client: [ RedisClient.config(url: url1), RedisClient.config(url: url2) ]この書き方が、そのまま安全に動くように整合性が取られています。
#58037 Require enumerable core ext for Range#sole
マージ日: 2026/7/7 | 作成者: @55728
- 概要 (1-2文で)
Range#soleが内部的に依存している Enumerable 拡張が読み込まれていないため、require "active_support/core_ext/range"だけだと常にクラッシュしていた問題を修正する PR です。Range#sole定義ファイルでactive_support/core_ext/enumerableを明示的に require することで、単体ロード時でも正しく動作するようにしています。
- 変更内容の詳細(あればサンプルコードも含めて)
問題の原因
active_support/core_ext/range/sole.rbはRange#soleを定義しているが、その実装は以下2つに依存している:Enumerable#sole(super経由で呼び出す)- 例外クラス
ActiveSupport::EnumerableCoreExt::SoleItemExpectedError
- これらはいずれも
active_support/core_ext/enumerableに定義されている。 - しかし、以下のどのファイルからも
active_support/core_ext/enumerableが require されていなかった:active_support/core_ext/range/sole.rbactive_support/core_ext/range.rb- range 関連の require 連鎖上のどこからも
その結果、ドキュメントにあるような「require "active_support/core_ext/range" だけを行うスタンドアロン利用」の場合、Range#sole が必ず異常終了していた。
具体的な不具合挙動
ruby -I lib などの素の Ruby 環境で:
require "active_support/core_ext/range"
(1..1).sole
# => NoMethodError: super: no superclass method `sole` for an instance of Range
(..1).sole
# => NameError: uninitialized constant ActiveSupport::EnumerableCoreExt(1..1).soleは本来Enumerable#soleに処理を委譲すべきだが、- そもそも
Enumerable#soleが未定義のためsuperが失敗
- そもそも
(..1).soleは例外としてActiveSupport::EnumerableCoreExt::SoleItemExpectedErrorを参照しようとするが、- そのモジュール自体がロードされておらず
NameErrorになる
- そのモジュール自体がロードされておらず
この PR の修正内容
activesupport/lib/active_support/core_ext/range/sole.rb に以下のような require を追加(実質 +2 行):
require "active_support/core_ext/enumerable"これにより:
require "active_support/core_ext/range"
(1..1).sole
# => 1
(2..1).sole
# => ActiveSupport::EnumerableCoreExt::SoleItemExpectedError: no item found
(..1).sole
# => ActiveSupport::EnumerableCoreExt::SoleItemExpectedError:
# infinite range '..1' cannot represent a sole itemと、Enumerable#sole/関連例外クラスが正しく機能し、Range 拡張として期待通りの動作になる。
Rails フルロード時との関係
- Rails 全体をロードした場合は、もともとどこかで
active_support/core_ext/enumerableが require されており、この問題は表面化していなかった。 - 今回の修正により、Rails フルロード時の挙動には変更なし(後方互換性維持)。
- 影響範囲・注意点
- 影響範囲:
active_support/core_ext/rangeを単体で require している環境でのみ挙動が変わる。- 具体的には、Gem やスクリプト内で軽量に Range 拡張だけを取り込みたい、という使い方をしているケース。
- 挙動の変更点:
- これまで異常終了(
NoMethodError/NameError)していたRange#sole呼び出しが、Enumerable 拡張と同じ意味論で正常に動作するようになる。 - 例外も今後は
ActiveSupport::EnumerableCoreExt::SoleItemExpectedErrorに揃うため、例外型に依存したコードを書いている場合は、もともと壊れていたコードが正しく動く(または正しい型に rescue される)ようになる。
- これまで異常終了(
- パフォーマンス・ロードコスト:
active_support/core_ext/range/sole読み込み時にactive_support/core_ext/enumerableも読み込まれるため、ほんのわずかにロードコストが増える可能性はあるが、ActiveSupport 利用前提の環境では実質的な影響は軽微。
- 参考情報 (あれば)
Range#soleは 8.1.0 で導入(コミット: 044d2eac9fe3)。- この PR は、過去にも何度か行われている「missing require」の修正系列に属する:
- e4de1ca9b5
- 302fdb7677
- 358ac36edf
- a1c57cf69f
Enumerable#sole/SoleItemExpectedErrorの挙動を理解しておくと、Range#soleの挙動も把握しやすいです(要素が 0 個・複数個・無限列の場合に例外を投げる、など)。
#58032 Fix event timestamp type in EventReporter rdoc
マージ日: 2026/7/7 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::EventReporterのドキュメントで、イベントハッシュに含まれる:timestampの型が誤ってFloatと記載されていたのを、実際の実装に合わせてIntegerに修正した PR です。実際の値はナノ秒精度のエポックタイム(Process.clock_gettime(..., :nanosecond))であり、巨大整数である点が明示されました。
- 変更内容の詳細(あればサンプルコードも含めて)
何を直したか
ActiveSupport::EventReporter の rdoc(コメントドキュメント)において、以下2箇所でイベントハッシュ中の :timestamp キーの型が誤っていました。
- クラスレベルの subscriber 向けドキュメント
#subscribeメソッドのドキュメント
これらで
:timestamp # Float (The timestamp of the event, in nanoseconds)のように書かれていたものを、
:timestamp # Integer (The timestamp of the event, in nanoseconds)に修正しています(実際の diff は +2/-2 行の単純な型表記変更のみ)。
なぜ Integer なのか
ActiveSupport::EventReporter がイベントを報告するとき、タイムスタンプは以下のように取得されています(実際のコードは概念的にはこのイメージ):
Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
# => 1738964843208679035 のような整数- 返り値は「エポック(UNIX time)からのナノ秒」を表す Integer です。
- 例として rdoc に載っている
1738964843208679035のような値は、Float では正確に表現できないほど大きく、1738964843208679035.to_f.to_i == 1738964843208679035がfalseになります。
つまり Float にキャストすると情報が落ちるため、「Float 型の timestamp」というドキュメントは誤りでした。
この PR により、ドキュメント上の型表記が実装(Integer)と整合しました。
- 影響範囲・注意点
影響範囲
- 実装コードには変更なし で、ドキュメントのみの修正です。
- ただし、これまでドキュメントを信じて
Float前提で処理していた subscriber 実装には以下のような影響がありえます:- 型チェックで
Floatを期待していた場合に失敗する。 timestamp.to_fして小数として扱っているコードは、すでに精度喪失が発生していた可能性がある(この PR で振る舞いが変わるわけではない)。
- 型チェックで
実装側で意識すべきこと
- 正しい型:
:timestampは「エポックからのナノ秒」の Integer です。
- もし秒単位やミリ秒・マイクロ秒など別の単位にしたい場合は、自前でスケール変換する必要があります:
# nanosecond -> second(Float)
seconds = timestamp / 1_000_000_000.0
# nanosecond -> millisecond(Integer, 切り捨て)
milliseconds = timestamp / 1_000_000- 精度を重視する場合は、内部表現は Integer のまま扱い、外部とのインターフェースで必要に応じて変換するのが安全です。
- 参考情報 (あれば)
- 使用されている API:
Process.clock_gettimeProcess.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)はエポックからのナノ秒を Integer で返します。
- 関連するドキュメント修正:
- この PR は、同じファイルのドキュメント調整を行ったコミット
27f4e99e51の後続として位置づけられています。
- この PR は、同じファイルのドキュメント調整を行ったコミット
- 対象クラス:
ActiveSupport::EventReporterを使って独自 subscriber を実装している場合は、event[:timestamp]を Integer として扱うよう、実装と型注釈(Sorbet/RBS など)やコメントの整合を確認しておくと良いです。
#58033 Instrument the normalized key in Cache#fetch hit path
マージ日: 2026/7/7 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::Cache::Store#fetchがキャッシュヒット時に発行するcache_fetch_hit.active_support通知のpayload[:key]を、他のキャッシュ関連イベントと同様に「正規化済み(namespace 付与後)のキー」に統一した変更です。これにより、ヒットとミスで同じ論理エントリに対して異なるキーが通知される不整合が解消されます。
- 変更内容の詳細
これまでの挙動
ActiveSupport::Cache::Store#fetch を、例えば以下のような namespace 付きで使っているケースを考えます:
cache = ActiveSupport::Cache::MemoryStore.new(namespace: "myapp")
cache.fetch("counter") { 0 }通知周りの挙動は以下のようになっていました:
fetch ミス時
- キー
"counter"に対し、内部では"myapp:counter"に正規化される - 発行されるイベント例:
cache_generate.active_supportcache_read.active_supportcache_write.active_support
- 上記イベントの
payload[:key]はすべて"myapp:counter"(正規化済みキー)
- キー
fetch ヒット時
- キャッシュから値が取れた場合に発行される
cache_fetch_hit.active_support
- しかし、このイベントの
payload[:key]は"counter"(namespace 付与前の生キー)になっていた
- キャッシュから値が取れた場合に発行される
つまり、同じ論理キー「counter」について:
- ミス時に観測されるキー:
"myapp:counter" - ヒット時に観測されるキー:
"counter"
となっており、メトリクス集計やログ集約で「1つのキー」として扱うのが困難でした。
変更後の挙動
この PR により、cache_fetch_hit.active_support でも正規化済みキーを使うように修正されています。
fetch ミス時
- 今まで通り:
payload[:key] == "myapp:counter"(変化なし)
- 今まで通り:
fetch ヒット時
- 変更前:
payload[:key] == "counter" - 変更後:
payload[:key] == "myapp:counter"
- 変更前:
これで以下のイベントはすべて同じキー表現を使うようになります:
cache_read.active_supportcache_write.active_supportcache_delete.active_supportcache_exist?.active_supportcache_generate.active_supportcache_fetch_hit.active_support← 今回統一された
コードレベルのイメージ
実際の diff はごく小さく、概念的には以下のような変更です(擬似コード):
# 変更前(イメージ)
def fetch(name, **options)
# ...
instrument(:cache_fetch_hit, key: name) do
# ...
end
end
# 変更後(イメージ)
def fetch(name, **options)
# ...
key = normalize_key(name, options) # namespace を含む正規化
instrument(:cache_fetch_hit, key: key) do
# ...
end
endテスト (cache_instrumentation_behavior) では、namespace ありのストアで #fetch ヒット時の cache_fetch_hit 通知に含まれる :key が、他のイベントと同じ正規化済みキーになっていることを検証するテストケースが追加されています。
- 影響範囲・注意点
- 影響を受けるのは ActiveSupport のキャッシュ通知 (
ActiveSupport::Notifications) を購読しているコード です。 - とくに、
"cache_fetch_hit.active_support"を購読している処理で、payload[:key]を 「生のキー」として扱っていた場合 に挙動が変わります。- 例:
"counter"という文字列を前提にログのフィルタリングをしていた場合、今後は"myapp:counter"となるためマッチしなくなります。
- 例:
- 一方で、すでに他のイベント(
cache_read/cache_writeなど)に合わせて 正規化済みキー前提で処理している場合 は、むしろ整合性が取れるようになり、集計や監視ロジックがシンプルになります。 - namespace を使っていないストア (
namespace: nilまたはデフォルト) の場合は、正規化前後でキー文字列が同じになるため、実質的な変化はほぼありません。
互換性の観点
- 破壊的変更になり得るケース:
cache_fetch_hitのpayload[:key]を使って、- 独自メトリクス(例: Prometheus ラベル)を定義している
- ログやトレースでキー名をパースしている
- 上記の処理が「namespace なしのキー」を前提にしている場合は、コードの修正・確認が必要です。
- 多くのアプリケーションでは、むしろ「ヒットとミスでキー表現が揃う」ことで挙動が分かりやすくなり、監視などが改善されるはずです。
- 参考情報 (あれば)
- この変更は、他のキャッシュ関連の修正と一貫性を取るものです:
- c013352ff2:
MemoryStore#increment/#decrementの instrumentation 正規化 - #57915:
Cache#exist?イベントにおけるキー正規化の統一
- c013352ff2:
- 関連クラス・モジュール:
ActiveSupport::Cache::StoreActiveSupport::Notifications
- ドキュメント上も
payload[:key]は「実際のキャッシュキー」として扱うことが想定されており、この PR によってその期待に沿う挙動になります。
#57830 Make ActionController Renderers ractor safe
マージ日: 2026/7/7 | 作成者: @Edouard-chin
- 概要 (1-2文で)
ActionController の Renderers(js,xmlなど)を Ractor セーフにするため、レンダラー名を保持するオブジェクトの扱いを見直し、ミューテーションの停止と freeze を行った PRです。併せて、内部利用想定の定数を非推奨化しつつ、ActiveSupport の deprecation プロキシに「初期化後に参照先を書き換えられる」仕組みを追加しています。
- 変更内容の詳細
背景と問題点
- Ractor セーフにするには、Ractor 間で共有されるオブジェクトは基本的に不変(frozen)である必要がある。
- ActionController::Renderers では、利用可能な renderer 名(
js,xmlなど)を保持する Set を起動後に mutate(追加・削除)していたため、Ractor セーフではなかった。 - 「ただ freeze する」だけでは、boot 時に renderer を追加・削除できなくなってしまう。
この PR では「外からは不変に見える構造」を維持しつつ、内部的には差し替えで対応するような形に変更しています。
1. 定数の非推奨化
- ActionController::Renderers 周りで「ドキュメント化されていたが、本来は内部用としたい」定数を deprecate。
- この定数は renderer 名リスト(Set)に直接アクセスできるもので、外部コードからのミューテーションを招きうるため、Ractor セーフ化の妨げになりやすい。
イメージとしては、例えば以下のようなコードが外部から書ける状態だったものを:
# 例: 以前なら(または現在でも)こう書けていた可能性がある
ActionController::Renderers::RENDERERS.add :foo今後はこのような直接変更パターンを廃止方向に誘導するために deprecate しています。
(実際の定数名は PR で対象になっているものですが、ここではイメージとして記載しています)
2. Deprecated constant の参照先を書き換え可能に
activesupport/lib/active_support/deprecation/proxy_wrappers.rb に変更が加えられています。
- Deprecation 用の proxy オブジェクトが、「初期化後に参照ターゲット(実体)を書き換えられる」API/仕組みを追加。
- これにより、「古い定数 → 新しい定数 or 新しい構造体」へのラップを維持したまま、内部実装だけを差し替えることが可能になります。
技術的には、次のようなことができるようになります(擬似コード):
DeprecatedConstantProxy.new("OldConst", "NewConst")
# 後から NewConst の実体を差し替え
DeprecatedConstantProxy.for("OldConst").target = NewNewConstこれにより:
- 外部コードは依然として旧定数経由でアクセスできる(ただし deprecation warning は出る)。
- Rails 内部では Ractor セーフな新しいオブジェクトに差し替え可能。
- Ractor セーフ化のために「Set を凍らせたいが、boot 時の追加は許可したい」といった要件も満たしやすくなる。
3. Renderers の Set をミューテーションしない & freeze する
actionpack/lib/action_controller/metal/renderers.rb のコア変更です。
目的:
- レンダラー名を保持する Set を mutate せず、都度新しい Set を生成する形に変更。
- 最終的に、その Set を
freezeして Ractor セーフな共有ができるようにする。
典型的には、以前は次のようなイメージだったものが:
# 以前(イメージ)
RENDERERS = Set.new
def self.add_renderer(name, &block)
RENDERERS.add(name)
# その他の登録処理...
end
def self.remove_renderer(name)
RENDERERS.delete(name)
# その他の削除処理...
endこれを「不変な Set を差し替える」パターンへ変更します:
# イメージコード: 実際の実装とは異なる可能性があります
RENDERERS = Set.new.freeze
def self.add_renderer(name, &block)
new_set = RENDERERS.dup
new_set.add(name)
RENDERERS = new_set.freeze
end
def self.remove_renderer(name)
new_set = RENDERERS.dup
new_set.delete(name)
RENDERERS = new_set.freeze
end実際には、直接定数再代入ではなく、内部的なオブジェクトや deprecation proxy などを挟みつつ、
「外から見えるインターフェースは保ちつつ、内部の Set は常に frozen」になるように実装されています。
この変更により:
- Ractor 間で Renderers の情報を共有しても、共有されるオブジェクトは frozen な Set なので安全。
- 追加・削除は「新しい frozen Set を作って差し替える」ため、Ractor セーフを維持しつつ動的変更を可能にしている。
4. テストと CHANGELOG
actionpack/test/controller/renderers_test.rb- renderer の追加・削除が期待どおり動くこと、および Ractor セーフ化に関連する挙動がテストされています。
activesupport/test/deprecation_test.rb- 新たに追加された「deprecated constant の target を変更できる」機能のテストが追加。
actionpack/CHANGELOG.md- Action Pack の変更点として「Renderers が Ractor セーフになった」こと、および関連する挙動の変更が明記されています。
影響範囲・注意点
内部定数への依存コード
- 非推奨化された定数(レンダラー名の Set など)に直接アクセスしているアプリ/ライブラリは、deprecation warning が出るようになります。
- 今後のバージョンで削除される可能性が高いので、代替 API(通常は
ActionController::Renderers.add,remove, あるいは public に提供されているインターフェース)を利用するようにリファクタが必要です。
カスタム renderer の追加・削除
- 通常の追加・削除 (
ActionController::Renderers.add/remove) を使っている分には挙動は変わらない想定です。 - ただし、内部構造の変更に依存していたメタプログラミング的なコード(
RENDERERSへの直接アクセスやSetのミューテーションなど)は動作が変わる/壊れる可能性があります。
- 通常の追加・削除 (
Ractor 利用アプリ
- この PR により、少なくとも Renderers 周りは Ractor セーフになります。
- Rails を本格的に Ractor で並列実行する場合、他のコンポーネントの Ractor セーフ性も考慮する必要がありますが、ActionController 周辺の安全性は向上しています。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57830
- 関連概念:
- Ruby Ractor: 並列実行モデル。共有オブジェクトは基本的に frozen を要求。
ActiveSupport::Deprecation::DeprecatedConstantProxy系の機構: 定数名は維持しつつ、内部実装や参照先の移行を段階的に行うために使われる仕組み。
- 実運用での推奨:
- カスタム renderer は公式にドキュメントされている API を通して登録/削除する。
- 内部定数には依存せず、deprecation warning が出たら早めに対応する。
#57825 Introduce ractorize
マージ日: 2026/7/6 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails アプリケーションを Ractor から共有可能にするための土台として、Rails::Application#ractorize!が導入され、基本的な Rails アプリをRactor.make_shareableできるようにする PR です。あわせて、Ractor 非対応だったいくつかの内部実装(継承オプション、KeyGenerator、Mutex など)が Ractor セーフになるよう調整されています。
- 変更内容の詳細
新機能: Rails::Application#ractorize!
Rails::Application に以下のメソッドが追加されています。
class MyApp::Application < Rails::Application
config.eager_load = true
config.cache_store = :null_store
# Ractor から共有可能にする
config.after_initialize do
Rails.application.ractorize!
end
endractorize! が行うこと(PR 説明+差分から推測される内容):
- アプリケーションを Ractor 共有可能な状態に「準備」する
- Ractor セーフでないオブジェクト(Mutex など)を排除・解体
- Engine / Initializer 周りの内部状態を Ractor 共有可能な形に再編
- 最後に
Ractor.make_shareable(app)を呼び出し、Rails.application自体を Ractor から共有可能なオブジェクトにする
これにより、例えば「メイン Ractor でアプリをブート → サブ Ractor にアプリを共有 → 各 Ractor でリクエスト処理」みたいな構成の土台が整えられます。
制約事項
現時点(この PR 時点)で ractorize! を使うためには、次の条件が必要です:
- アプリケーションコードとルーティングは eager load 必須
config.eager_load = true- 遅延ロードだと、Ractor 間でクラス定義のタイミングや状態が不整合になるため
- Action Cable は非対応
- 内部的にスレッド/コネクション管理など Ractor セーフでない構造が多数あるため
- キャッシュストアは
:null_store必須- 他のキャッシュストア(メモリストアなど)が Ractor 共有できないデータ構造を使っているため
将来的にはこれらの制約を減らす予定とのことですが、この PR はあくまで最初の足場です。
継承オプション (Rails::Configuration / Rails::Engine / Rails::Initializable)
Rails::Configuration や Engine の継承オプション、initializer 系の設定は、これまで「通常の Ruby オブジェクト(しばしばミュータブル)」を前提にしていました。
この PR では:
- 「継承オプション」の保持方法が Ractor セーフになるよう調整
Hashのdefault_procがミューテーションを行わないようにしたり- 初期化後に freeze 可能な形に揃えたり
Rails::Engineごとにアプリケーションを eager に構築し、Ractor 非共有なオブジェクト(特に Mutex)を「初期化時に使って、共有前に破棄」するようなフローに変更Rails::Initializable周りで使われるProcなどを Ractor 共有可能な形に修正
これにより、Rails.application の内部構成(設定や初期化処理の一覧など)を Ractor.make_shareable に掛けられるようになります。
KeyGenerator を Ractor セーフに
ActiveSupport::KeyGenerator と、その利用箇所である ActiveSupport::Messages::RotationCoordinator に変更が入っています。
ポイント:
- これまで
KeyGeneratorは内部で「Concurrent Map(あるいは似た構造)」を使っており、これは Ractor 共有不可能でした。 - この PR では:
- 「キー生成用のキャッシュ(Map)」を Ractor ローカル にする設計に変更
- つまり「Ractor ごとに KeyGenerator のキャッシュを持つ」ようにして、
Rails.application自体は共有しつつも、共有不可能な Map は各 Ractor 側で持つ
影響として:
- 同じ
Rails.applicationを複数 Ractor から使っても、KeyGenerator自体は共有されるが、その内部キャッシュは各 Ractor で独立して持たれる。 - そのため、初回アクセス時のキー計算コストは Ractor ごとに一度ずつ発生するが、Ractor セーフネスとトレードオフにした pragmatic な対応になっている。
テスト (activesupport/test/key_generator_test.rb) も追加されており、Ractor 使用時の振る舞いを検証しています。
ActionDispatch Journey / Transition Table の Ractor 対応
actionpack/lib/action_dispatch/journey/gtg/transition_table.rb に追記:
- ルーティングエンジン(Journey)の状態遷移テーブル構築処理で、Ractor 共有可能でない要素(例: デフォルト Proc 付き Hash)が含まれていた部分を修正
- 遷移テーブル構造が
Ractor.make_shareableできるようにするための小変更(ミュータブルな default_proc の削除・freeze など)が行われていると考えられます。
これにより、Rails.application.routes の内部表現も Ractor 共有しやすくなります。
Ractor 関連テストの追加
activesupport/lib/active_support/testing/ractors_assertions.rb- Ractor 関連のアサーションヘルパを定義
- 例: 「このオブジェクトは Ractor から共有できるか」などをテストで確認する仕組み
railties/test/application/ractors_test.rb- 実際に Rails アプリをブートして
ractorize!を呼び、別 Ractor から利用できるかなどを検証する統合テスト - eager load / null_store などの前提条件を満たしたアプリで、
Ractor.make_shareableが成功することを確認していると推測できます。
- 実際に Rails アプリをブートして
- 影響範囲・注意点
影響範囲
- Rails 内部:
- アプリケーション設定 (
Rails::Configuration) - Engine の構造 (
Rails::Engine) - Initializer の登録・実行方法
- KeyGenerator / メッセージ暗号化系
- ルーティングエンジン(Journey)の内部構造
- アプリケーション設定 (
- 外部 API レベル:
- 新メソッド
Rails::Application#ractorize! - Ractor ベースで Rails を動かしたい人向けの初期サポート
- 新メソッド
既存アプリへの影響
- この PR 自体は、既存アプリが従来どおり(Ractor を使わずに)動く場合には、基本的に挙動変更を引き起こさないように配慮されています。
- ただし以下の点は意識しておくと良いです:
- internal な Hash の default_proc など、一部挙動が微妙に変わっている可能性はある(通常アプリではまず問題にならないレベルを目指しているはず)
- KeyGenerator の実装変更に伴い、Ractor を明示的に使う場合は「Ractor ごとに KeyGenerator のキャッシュが別になる」ことを理解しておく必要がある
Ractor を使いたい場合の注意点
- 必須条件:
config.eager_load = trueconfig.cache_store = :null_store- Action Cable を使わない
- 実運用で使うには、まだ制約が大きく、かつスレッド・プロセス・Ractor の組み合わせの設計が難しいため、現時点では「実験的」な段階と見なすのが妥当です。
- アプリケーションコード側も、自前で保持するグローバル状態やシングルトン、クラス変数などが Ractor セーフになっていないと、
ractorize!後のRactor.make_shareableに失敗する可能性があります。
- 参考情報 (あれば)
- この PR で触れられている関連 PR:
- Inheritable options の Ractor 対応に関する議論: https://github.com/rails/rails/pull/57757
- Ractor 自体の仕様:
- Ruby 公式ドキュメント「Ractor」: https://docs.ruby-lang.org/ja/latest/doc/ractor_md.html
- Ractor 対応の設計上の方針:
- 共有可能なオブジェクト (
Ractor.shareable?) のみを共有し、Ractor ローカルで持つべきもの(キャッシュや Mutex など)は Ractor ごとに分離する、という設計が Rails 内部にも適用され始めているのがこの PR のポイントです。
- 共有可能なオブジェクト (
#58018 Translate if_exists/if_not_exists guards when inverting remove_column and add_reference/remove_reference
マージ日: 2026/7/6 | 作成者: @55728
- 概要 (1-2文で)
remove_columnおよびadd_reference/remove_reference(とそのエイリアスadd_belongs_to/remove_belongs_to)について、マイグレーションの巻き戻し時にif_exists/if_not_existsオプションが正しく反転して適用されるように修正した PR です。これにより、これらのコマンドを使った冪等なマイグレーションのchange/revertが、期待通りに動作するようになります。
- 変更内容の詳細
背景: command_recorder と idempotency guard
Rails の ActiveRecord::Migration::CommandRecorder は、change マイグレーションや revert ブロック実行時に発行されたコマンドを記録し、その逆操作(inverse)を自動生成します。
既に以下のコマンドでは、if_exists と if_not_exists が「逆向きのガード」として正しく変換されていました:
add_columnadd_indexadd_foreign_key/remove_foreign_keyadd_check_constraint/remove_check_constraint
しかし、以下のコマンドファミリーではこの変換が抜けていました:
remove_columnadd_reference/remove_referenceadd_belongs_to/remove_belongs_to(add_reference/remove_referenceのエイリアス)
今回の PR は、この「抜けていた2ファミリー」に対してもガードの変換を適用するものです。
remove_column の変更
変更前の挙動
inverse_of :remove_column, [:users, :nickname, :string, if_exists: true]
# => [:add_column, [:users, :nickname, :string, { if_exists: true }], nil]remove_columnに渡したif_exists: trueが、そのまま逆操作のadd_columnにも渡されていました。- しかし、
add_columnはif_existsオプションを認識しないため、ロールバック時にはガードが完全に無視され、「意図に反する形で」動いてしまう可能性がありました。- 元々の意図: 「カラムが存在する場合だけ remove する」→ ロールバックでは「カラムが存在しない場合だけ add する」としたい
- 実際の挙動: ロールバック側でガードが消えるため、常に
add_columnを実行してしまう
変更後の挙動
inverse_of :remove_column, [:users, :nickname, :string, if_exists: true]
# => [:add_column, [:users, :nickname, :string, { if_not_exists: true }], nil]remove_column(if_exists: true)の逆操作はadd_column(if_not_exists: true)となり、- 削除側: 「あれば消す」
- 逆操作: 「なければ作る」
- というペアになり、冪等な前方・後方マイグレーションが成立するようになりました。
add_reference / remove_reference(およびエイリアス)の変更
変更前の挙動
inverse_of :add_reference, [:t, :u, if_not_exists: true]
# => [:remove_reference, [:t, :u, { if_not_exists: true }]] # ガードが変換されず無視される
inverse_of :remove_reference, [:t, :u, if_exists: true]
# => [:add_reference, [:t, :u, { if_exists: true }]] # 同上- コマンド名だけが
add↔removeで入れ替わり、オプションハッシュはそのまま渡されていました。 - その結果:
add_reference側はif_not_existsをサポートしていてもremove_reference側はif_not_exists/if_existsをサポートしていなかったり、逆にif_existsだけを見ていたり
- という非対称な API のせいで、片方のコマンドで指定したオプションが逆操作側で「無視」されていました。
変更後の挙動
add_reference(if_not_exists: true)の逆操作はremove_reference(if_exists: true)remove_reference(if_exists: true)の逆操作はadd_reference(if_not_exists: true)- となるように、
if_exists/if_not_existsを互いに反転して渡すように修正されました。
# 例: change マイグレーション
def change
add_reference :comments, :post, if_not_exists: true
end
# ロールバック時に内部的に解釈されるイメージ
# => remove_reference :comments, :post, if_exists: trueadd_belongs_to / remove_belongs_to は add_reference / remove_reference のエイリアスなので、この修正の恩恵をそのまま受けます。
- 影響範囲・注意点
- 対象:
changeマイグレーション(またはrevertブロック)でremove_column(..., if_exists: true)add_reference(..., if_not_exists: true)remove_reference(..., if_exists: true)- およびその
*_belongs_toエイリアス を使っているケース。
- 影響:
- これまで:
- 逆操作側でガードが「消える / 無視される」ことがあった。
- そのため、本番環境などで「途中の状態からロールバック」するようなケースで、不要なエラーや意図しない再作成などが起こりうる。
- これから:
if_exists/if_not_existsが逆操作側にも正しく反映され、前方・後方どちらの方向にも冪等性が担保されやすくなる。
- これまで:
- 互換性について:
- 挙動変更ではありますが、「ガードが無視されていた」状態から「意図通りガードされる」状態への修正なので、ほとんどの場合はバグ修正として望ましい変更です。
- もし「逆操作側でガードが付かないこと」を前提にした非常に特殊なワークアラウンドをしている場合は、今回の修正で挙動が変わる可能性があります(そのような依存は避けるべきです)。
- 参考情報 (あれば)
- 変更ファイル:
activerecord/lib/active_record/migration/command_recorder.rbinverse_ofのロジックに、remove_columnおよびadd_reference/remove_reference系に対するif_exists↔if_not_existsの変換処理を追加。
activerecord/test/cases/migration/command_recorder_test.rb- 上記の変換が正しく行われることを保証するテストを追加。
- 関連する既存仕様:
- すでに
add_column,add_index,add_foreign_key/remove_foreign_key,add_check_constraint/remove_check_constraintでは、同様の guard 変換が行われており、本 PR はその不整合を解消するものです。
- すでに
#58024 Derive the expected queue from the delivery job in assert_enqueued_email_with
マージ日: 2026/7/6 | 作成者: @55728
- 概要 (1-2文で)
assert_enqueued_email_withが、メール送信時のキュー名を「メイラー側の設定」ではなく「delivery_job 側の設定」から導出するように修正されました。これにより、カスタムdelivery_jobでqueue_asを指定している場合でも、正しくテストが通るようになります。
- 変更内容の詳細
これまでの挙動(問題点)
ActionMailer::TestHelper#assert_enqueued_email_with は、期待されるキュー名を mailer_class.deliver_later_queue_name から導出していました。
しかし、次のようにカスタム delivery_job を使い、そのジョブに queue_as を設定しているケースでは問題が発生していました。
class RegistrationDeliveryJob < ActionMailer::MailDeliveryJob
queue_as :notifications
end
class RegistrationMailer < ApplicationMailer
self.delivery_job = RegistrationDeliveryJob
end
RegistrationMailer.welcome.deliver_later
assert_enqueued_email_with RegistrationMailer, :welcome
# => No enqueued job found with {job: RegistrationDeliveryJob, ..., queue: "mailers"}
# Potential matches: {..., queue: "notifications", ...}- 実際には
queue: "notifications"にジョブが積まれている - しかしアサーションは
queue: "mailers"を期待してしまうため、失敗していました
これは deliver_later のドキュメント上「カスタム delivery_job を使う場合は、その job がキュー名を支配する」とある契約と食い違っていました。
今回の修正内容
変更ファイル:
actionmailer/lib/action_mailer/test_helper.rb(+1 / -1)actionmailer/test/test_helper_test.rb(+18 / 0)
テストヘルパの実装を微調整し、「期待されるキュー名の決定ロジック」を以下のように変更しています。
- 以前:
mailer_class.deliver_later_queue_nameからキュー名を導出 - 今回:
mailer_class.delivery_job側からキュー名を導出
→delivery_jobがqueue_asで指定したキューがある場合はそれを使う
結果として:
class RegistrationDeliveryJob < ActionMailer::MailDeliveryJob
queue_as :notifications
end
class RegistrationMailer < ApplicationMailer
self.delivery_job = RegistrationDeliveryJob
end
RegistrationMailer.welcome.deliver_later
assert_enqueued_email_with RegistrationMailer, :welcome
# 期待キュー名が "notifications" になり、正常にパスするテスト (test_helper_test.rb) には、この振る舞いを保証するためのケースが追加されています(カスタム delivery_job + queue_as を使った場合のアサーション成功を検証)。
なお、デフォルトの MailDeliveryJob を使うケースや queue_name_prefix を利用しているケースでも、最終的に解決されるキュー名は従来と変わらないように設計されています。
- 影響範囲・注意点
テストでの影響
- カスタム
delivery_jobを使っていてqueue_asを指定している場合、これまで不当に落ちていたassert_enqueued_email_withが通るようになります。 - 逆に言えば、「実運用ではカスタム delivery_job のキューに積まれている」のに、「テストで mailer 側のキュー名を前提にしていた」ような間違ったテストは、今回の修正で正しい挙動に修正されます。
- カスタム
既存コードへの互換性
- デフォルトの
MailDeliveryJobを使っている場合、これまでとキュー名の解決結果は同じになるように配慮されているため、通常は影響はありません。 queue_name_prefixを利用している場合も、最終的なキュー名は従来と同じになるように実装されています。
- デフォルトの
テストコード側で明示的に queue を指定している場合の確認
assert_enqueued_email_withにqueue:オプションで明示的にキューを指定している場合、その指定がカスタムdelivery_jobの設定と矛盾していないかは確認しておくと安全です。
- 参考情報 (あれば)
ActionMailer::TestHelper#assert_enqueued_email_with:- メイラーとメソッドを指定して、そのメールが正しくジョブとしてエンキューされているかを検証するためのテストヘルパ。
ActionMailer::Base#delivery_job:- メール送信に使用する Active Job クラスを指定するための設定。
deliver_laterのドキュメント上、「カスタム delivery_job を設定した場合、その job がキュー名 (queue_as) を決定する」と規定されている。
- 関連 PR: #58009(今回の修正はそのフォローアップとして位置付けられています)。
#58017 Preserve subsecond precision in the DateTime.now travel stub under with_usec
マージ日: 2026/7/6 | 作成者: @55728
- 概要 (1-2文で)
travel_to(time, with_usec: true)使用時に、DateTime.nowでもサブ秒(マイクロ秒)精度が正しく保持されるように修正したPRです。これにより、同じ凍結時刻を参照するTime.nowとDateTime.nowの挙動が一致します。
- 変更内容の詳細
何が問題だったか
ActiveSupport::Testing::TimeHelpers#travel_to には with_usec: オプションがあります。
with_usec: true
凍結する時刻のサブ秒(usec)まで含めて時間を固定する意図。with_usec: false(デフォルト)
秒単位で丸めて時間を固定する(usecは 0 になる)。
これまでは:
travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999), with_usec: true do
Time.now.usec # => 999999 # サブ秒が保持される
DateTime.now.usec # => 0 # サブ秒が失われる(バグ)
endと、Time.now は with_usec: true に従ってサブ秒を保持していた一方、DateTime.now のスタブは秒単位に切り捨ててしまっていました。
今回の修正内容
activesupport/lib/active_support/testing/time_helpers.rb で DateTime.now のスタブ生成部分が 1行変更され、with_usec: true のときにサブ秒を保持するようになりました。
PRの説明に対応する挙動は次の通りです:
travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999), with_usec: true do
Time.now.usec # => 999999
DateTime.now.usec # => 999999 # Time.now と一致するように修正
endまた、テスト (activesupport/test/time_travel_test.rb) に DateTime.now の usec を検証するケースが 13行追加されています。
これにより、with_usec: true で DateTime.now がサブ秒を保持すること、with_usec: false では保持しないこと(usec == 0)が回 regresion テストとして担保されます。
with_usec: false の挙動は変わらない
デフォルトの with_usec: false の場合は、これまで通り DateTime.now.usec は 0 のままです:
travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do # with_usec: false がデフォルト
Time.now.usec # => 0
DateTime.now.usec # => 0
end- 影響範囲・注意点
影響範囲:
- テストコードで
ActiveSupport::Testing::TimeHelpers#travel_toとDateTime.nowを併用しており、かつwith_usec: trueを指定しているケース。 - そういったコードでは、これまで「秒単位に丸められていた」
DateTime.nowが「マイクロ秒まで含めて凍結される」ように変わります。
- テストコードで
挙動変化により起こりうること:
- 以前の動作に依存して「
DateTime.now.usecは常に 0 である」と決め打ちしていたテストがあると、with_usec: trueの文脈では失敗する可能性があります。 - ただし、
with_usec: trueを明示している場合は「サブ秒を保持したい」という意図が通常のため、この変更は意図により沿ったものといえます。
- 以前の動作に依存して「
注意点:
- 秒単位で固定したいテストは、
with_usec:を明示的に指定するか、with_usec: falseを使うことを前提にするとよいです。 - ライブラリやアプリ側で、
DateTime.nowベースのロジックとTime.nowベースのロジックを混在させていた場合、これにより両者の挙動が揃うため、むしろ一貫性が向上します。
- 秒単位で固定したいテストは、
- 参考情報 (あれば)
- 該当コード:
ActiveSupport::Testing::TimeHelpers#travel_to(time_helpers.rb)
- 挙動の意図:
with_usec: true… テスト中の「凍結時刻」をサブ秒精度まで含めて再現したい場面向け。with_usec: false… 秒単位で十分なテスト向け(以前からのデフォルト挙動)。
#58021 Honor an explicit :selected option in time_zone_select
マージ日: 2026/7/6 | 作成者: @55728
- 概要 (1-2文で)
time_zone_selectヘルパーにselected:オプションを明示的に渡した場合、その値がきちんと選択状態として反映されるように修正したPRです。selected:を指定しない既存コードの挙動は変わりません。
- 変更内容の詳細
これまでの問題
time_zone_select は第4引数のオプションとして selected: を受け取れますが、実際には無視されており、常に「オブジェクトの属性値」もしくは「default: オプション」で選択タイムゾーンが決まっていました。
time_zone_select("firm", "time_zone", nil, selected: "B")
# 本来は "B" を選択させたいが、
# 実際には firm.time_zone(または default:)の値が選択されていたフォームオブジェクト側の値を一時的に上書きしたいケース(例: 管理画面で一括編集、確認画面で一時的に別の値を見せたい等)で、selected: が効かないというバグ的挙動になっていました。
修正後の挙動
このPRでは time_zone_select の内部で、「どの値を選択状態にするか」の決定ロジックにおいて、:selected が明示的に渡されていればそれを最優先で使うように修正しています。
- 優先順位(概念的には):
options[:selected]があればそれを選択値とする- なければ、従来通り「オブジェクトの属性値」または
options[:default]を使う
使用イメージ:
# 明示的に "B" を選択したい
time_zone_select("firm", "time_zone", nil, selected: "B")
# => HTML上も "B" に対応する <option> が selected になる
# これまで通りオブジェクトの値を使いたい場合
time_zone_select("firm", "time_zone")
# => firm.time_zone が selected
# オブジェクトに値がなければ default: を使う(従来通り)
time_zone_select("firm", "time_zone", nil, default: "UTC")コード上の変更点としては、ActionView::Helpers::Tags::TimeZoneSelect 内の選択値決定ロジックに 3行程度の追加があり、テスト (form_options_helper_test.rb) で「selected: を渡したときにそれが優先される」ことを検証するケースが追加されています。
- 影響範囲・注意点
影響を受けるヘルパー
ActionView::Helpers::FormOptionsHelper#time_zone_selectform_for/form_with等から呼ばれる同名ヘルパーも含む
既存コードへの影響
selected:を渡していないコードは挙動が変わりません(オブジェクト値 +default:の従来ロジックそのまま)。- これまで「
selected:を指定しても無視される」と思ってワークアラウンドをしていた場合は挙動が変わる可能性があります。
例えば:rubyこうしたコードがあると、今回から# selected: が無視される前提で、わざと「無関係な値」を渡していたような特殊なハック time_zone_select("user", "time_zone", nil, selected: "dummy")"dummy"(に対応するタイムゾーン)が本当に選択されてしまいます。
注意点 / ベストプラクティス
- 「モデルの値をそのまま使いたい」場合は
selected:を指定しない方が分かりやすく安全です。 - 「モデルの値よりも、フォーム側で明示的に指定した選択状態を優先したい」ケース(例: 確認画面での上書き、独自の初期値制御)では
selected:を積極的に使えるようになります。 default:はあくまで「オブジェクトの値がない時のフォールバック」として機能し、selected:を指定すればdefault:よりもselected:が優先されます。
- 「モデルの値をそのまま使いたい」場合は
- 参考情報 (あれば)
- 対象コード:
actionview/lib/action_view/helpers/tags/time_zone_select.rb - テスト:
actionview/test/template/form_options_helper_test.rb - 関連ヘルパー:
select,time_selectなど他の select 系ヘルパー も似た API を持つため、今後selected:の扱いがそれらと整合的になるかを見る際の参考になります。
#58012 Fix broken links in guides [ci skip]
マージ日: 2026/7/6 | 作成者: @fuentesjr
- 概要 (1-2文で)
このPRは、Rails Guides 内および外部サイトへの「リンク切れ」を3箇所修正するドキュメント専用の変更です。動作コードには一切手を入れておらず、ガイドの参照先を正しいURL/アンカーに差し替えています。
- 変更内容の詳細
変更はいずれも1行ずつのリンク先修正です。
2-1. Active Storage ガイド: ImageMagick のリンク修正
対象: guides/source/active_storage_overview.md の Requirements セクション
変更前:
https://imagemagick.org/index.phpこの URL は 404 になるため、サイトルートに変更。
変更後:
https://imagemagick.org/Rails 自体の機能には影響しませんが、Active Storage をセットアップする際に参照する ImageMagick の公式サイトへのリンクが正常に開けるようになります。
2-2. Action Mailer ガイド: Active Job へのアンカー修正
対象: guides/source/action_mailer_basics.md の「コントローラの編集」付近
Action Mailer から Active Job を使ってメールを非同期送信する (deliver_later) 説明へのリンクが壊れていました。
変更前:
active_job_basics.html#action-mailerActive Job ガイド側に #action-mailer というアンカーは存在せず、クリックしてもページトップ(もしくは何も起きない)状態でした。
変更後:
active_job_basics.html#example-sending-email#example-sending-email セクションは、Active Job 経由でのメール送信(deliver_later など)を具体的に解説している箇所であり、文脈的にも正しいターゲットです。
Action Mailer ガイドから「メールをジョブ経由で送るサンプル」に直接ジャンプできるようになります。
2-3. Configuring ガイド: キャッシュストアへのアンカー修正
対象: guides/source/configuring.md の config.cache_store の説明部分
config.cache_store の説明中で、キャッシュストアの詳細として Caching ガイド中の特定セクションへリンクしていますが、そのアンカー名が存在しませんでした。
変更前:
caching_with_rails.html#cache-storesCaching ガイドには #cache-stores セクションがなく、リンクはページトップ相当になっていました。
変更後:
caching_with_rails.html#other-cache-stores#other-cache-stores セクションは、config.cache_store の説明で列挙されているストアと一致する内容を扱っています:
:memory_store:file_store:mem_cache_store:null_store:redis_cache_store
これにより、config.cache_store の説明から、そのまま各キャッシュストアの詳細説明へジャンプできるようになります。
- 影響範囲・注意点
- 影響範囲は Rails Guides (ドキュメント) のみで、アプリケーションコードやフレームワークの挙動には一切影響しません。
- 既存の URL あるいはアンカーから別の場所へ「リダイレクト」されるようになったのではなく、もともと機能していなかったリンクを、意図した場所に正しく張り直した変更です。
- ドキュメント生成 (guides のビルド) が通るかどうかはほぼノーリスクですが、
- プレーンな URL (
https://imagemagick.org/) - 同一リポジトリ内の他のガイドへの相対リンク (
active_job_basics.html#example-sending-email,caching_with_rails.html#other-cache-stores)
の3つなので、ビルドエラー等の可能性も実質ありません。
- プレーンな URL (
- リンクターゲットとなるガイド側のアンカー名 (
#example-sending-email,#other-cache-stores) が今後変更されると再びリンク切れになるため、ドキュメント側でアンカーを変更する際は相互参照の確認が必要です。
- 参考情報 (あれば)
- 該当ガイド(edge guides):
- Active Storage Overview:
https://edgeguides.rubyonrails.org/active_storage_overview.html#requirements - Action Mailer Basics:
https://edgeguides.rubyonrails.org/action_mailer_basics.html#edit-the-controller - Active Job Basics(修正先アンカー):
https://edgeguides.rubyonrails.org/active_job_basics.html#example-sending-email - Configuring Rails Applications (
config.cache_store):
https://edgeguides.rubyonrails.org/configuring.html#config-cache-store - Caching with Rails(修正先アンカー):
https://edgeguides.rubyonrails.org/caching_with_rails.html#other-cache-stores
- Active Storage Overview:
運用面では、こうした broken link は CI で自動検出しづらいことが多いため、今後リンクチェッカー(外部URL & 内部アンカー)の導入を検討すると、同種の不具合を早期に防ぎやすくなります。
#58016 Forward keyword arguments through deprecation proxies
マージ日: 2026/7/6 | 作成者: @55728
- 概要 (1-2文で)
DeprecatedObjectProxy/DeprecatedInstanceVariableProxy経由でキーワード引数必須メソッドを呼ぶとArgumentErrorになっていた問題を修正し、正しくキーワード引数として転送されるようにした PR です。Ruby 3 以降の「キーワード引数の分離」による不具合を、ruby2_keywordsを使って最小限の変更で解消しています。
- 変更内容の詳細
何が問題だったか
Ruby 3 以降では、以下のような「最後の Hash をキーワード引数として解釈する」挙動が変わり、*args で受けるとキーワードにならず「位置引数の Hash」として扱われます。
def target(a:)
a
end
def caller(*args, &block)
target(*args, &block)
end
caller({ a: 1 })
# Ruby 3 では ArgumentError: wrong number of arguments ...ActiveSupport::Deprecation の以下のプロキシクラスが、まさにこのパターンでした。
ActiveSupport::Deprecation::DeprecatedObjectProxyActiveSupport::Deprecation::DeprecatedInstanceVariableProxy
これらはベースクラス DeprecationProxy#method_missing を継承しており、その定義は
def method_missing(method, *args, &block)
warn(method, caller)
@object.__send__(method, *args, &block)
endのような形でした。このため、プロキシ対象のメソッドが「必須キーワード引数」を持っていると、以下のようなエラーになっていました。
class Sample
def foo(a:)
a
end
end
sample = Sample.new
proxy = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(sample, "sample", ActiveSupport::Deprecation.new)
proxy.foo(a: 1)
# Before:
# ArgumentError: wrong number of arguments (given 1, expected 0; required keyword: a)Hash { a: 1 } が位置引数として転送されてしまい、キーワードとして解釈されなかったためです。
どう直したか
DeprecationProxy#method_missing に ruby2_keywords を付けました。
class DeprecationProxy
ruby2_keywords def method_missing(method, *args, &block)
warn(method, caller)
@object.__send__(method, *args, &block)
end
endruby2_keywords を付与すると、
- 呼び出し元での「キーワードっぽい Hash」としての情報が
*argsの中にフラグ付きで保持される - 呼び出し先(ここでは
@object.__send__)に転送したとき、再び「キーワード引数」として解釈される
という Ruby 2 互換の挙動が有効になります。
これにより、先ほどの例は次のように期待どおり動作します。
proxy.foo(a: 1)
# After:
# => 1
# かつ deprecation warning は従来どおり出るなぜ *args, **kwargs にしなかったのか
DeprecationProxy#method_missing は warn に args を渡し、特に DeprecatedInstanceVariableProxy#warn では args.inspect をそのままメッセージに埋め込んでいます。
def warn(method, callstack)
ActiveSupport::Deprecation.warn(
"some message... Args: #{args.inspect}",
callstack
)
endここでメソッドシグネチャを
def method_missing(method, *args, **kwargs, &block)
...
endのように分割してしまうと、args からキーワードが抜け落ち、警告メッセージ上では
- 以前:
Args: [1, {:a=>2}] - 変更後:
Args: [1](キーワードは別のkwargsにいる)
となり、互換性が崩れます。
ruby2_keywords を使えば、
- 呼び出し側・メッセージ上では「旧来どおり
argsに Hash が入っている」状態を維持 - 転送先メソッドでは Ruby 3 スタイルのキーワードとして正しく解釈
という両方を満たせるため、この一行だけで済ませています。
他との一貫性
同じファイルにある DeprecatedConstantProxy#method_missing は、すでに
def method_missing(...)
...
target.__send__(...)
endという「...(dot-dot-dot)」構文でキーワードを正しく転送する形に直されていました。この PR は、その sibling 実装と挙動を揃える修正でもあります。
テスト追加
activesupport/test/deprecation_test.rb にテストが追加され、以下のようなケースがカバーされています(要約):
DeprecatedObjectProxy経由で、必須キーワード引数を持つメソッドを呼べることDeprecatedInstanceVariableProxy経由でも同様に動作すること- その際に deprecation warning が出ること
- 引数の表示(
args.inspect)が従来どおりであること
- 影響範囲・注意点
影響範囲
ActiveSupport::Deprecation::DeprecatedObjectProxyActiveSupport::Deprecation::DeprecatedInstanceVariableProxy- 特に、キーワード引数を持つメソッドをこれらのプロキシ越しに呼んでいるコードは挙動が変わります(以前は
ArgumentErrorで落ちていたが、正常に呼べるようになる)。
後方互換性
- これまで例外が出ていた箇所が正しく動作するようになるため、基本的にはバグフィックスです。
warnに渡されるargsの内容・inspect文字列は維持されるので、ログ監視・テストでメッセージを厳密比較している場合でも影響はほぼありません。ruby2_keywords自体は Ruby 3 系では「非推奨だがまだ使える互換機能」として残っていますが、Rails 内部実装に閉じており、利用者側のコードに直ちに影響はありません。
注意点
- 将来 Ruby 本体から
ruby2_keywordsが削除される可能性はあります。その場合、この周辺実装を*args, **kwargsなどに書き換えつつ、警告メッセージの互換性をどう担保するか、という課題は残ります。 - プロキシ経由でのメソッド呼び出しに頼ったテストが、「例外が出ること」を前提にしていた場合は、テストが通らなくなる可能性があります。
- 将来 Ruby 本体から
- 参考情報 (あれば)
- Ruby のキーワード引数分離に関する変更点(Ruby 3.0)
ruby2_keywordsの挙動について(Ruby リファレンス)- 類似の修正(
DeprecatedConstantProxyのキーワード転送)- コミット
946e46ebcc(Rails 本体リポジトリ内)
- コミット
#58015 Strip surrounding whitespace before parsing a duration in Hash.from_xml
マージ日: 2026/7/6 | 作成者: @55728
- 概要 (1-2文で)
Hash.from_xmlがtype="duration"の要素をパースする際に、前後の空白文字(改行・インデント)を除去してからActiveSupport::Durationに変換するように修正された PR です。これにより、「人間が整形した XML」に含まれる改行やインデントのせいでParsingErrorが起きていた問題が解消されます。
- 変更内容の詳細
何が問題だったか
以下のように、整形された XML で type="duration" のノードに改行やインデントが含まれると:
Hash.from_xml(<<~XML)
<event>
<length type="duration">
PT1H
</length>
</event>
XML実際のテキストノードの値は " PT1H\n " のように前後に空白を含む形になり、ActiveSupport::Duration::ISO8601Parser がこれを正しく解釈できず、ParsingError を送出していました。
同じような書き方でも、type="date", type="datetime", type="integer", type="boolean" などは内部で strip されている(または許容度が高い)ため問題なくパースできていましたが、duration だけが厳格だった状態です。
今回の修正内容
activesupport/lib/active_support/xml_mini.rb で、type="duration" の値をパースする前に strip をかけるように 1 行差し替えが行われました。
イメージとしては:
when "duration"
# 変更前(概念的に):
ActiveSupport::Duration.parse(value)
# 変更後(概念的に):
ActiveSupport::Duration.parse(value.to_s.strip)
end※実際のコードは若干異なる可能性がありますが、意味としては「文字列に変換して前後の空白を削る」の 1 行の修正です。
テスト
activesupport/test/xml_mini_test.rb に 1 行のテストケースが追加されています。
内容は、インデント付きの type="duration" ノードを含む XML を Hash.from_xml に渡したときに、ActiveSupport::Duration(たとえば 1.hour)として正しくパースされることを検証するものです。
これにより、今回の回避策が将来のリグレッションで壊れないようにカバーされています。
- 影響範囲・注意点
影響範囲
ActiveSupport::XmlMini経由でHash.from_xmlを用い、type="duration"のノードを扱う箇所すべてに影響があります。- インデントや改行を含む「整形された XML」や、人手で書かれた XML を扱うアプリケーションで特に恩恵があります。
- これまで
type="duration"だけ他の型と比べて扱いがシビアだった点が解消され、dateやdatetimeなどと同様にプレーンテキストに余分な空白があっても安全にパースされるようになります。
後方互換性 / 互換性リスク
- 前後の空白を
stripするだけなので、通常の利用ではほぼリスクはありません。 - ISO8601 の duration 文字列の意味的に、先頭・末尾の空白に意味を持たせるケースは現実的には想定されず、互換性問題はほぼ発生しないと考えられます。
- もし「わざと先頭/末尾に空白を入れてパースエラーを起こさせる」といった挙動に依存しているコードがあれば、その挙動は変化します(エラーにならずパースに成功するようになる)が、そのような依存は一般的にはありえないはずです。
- 前後の空白を
運用・テスト上の注意
- 既存の XML をそのまま
Hash.from_xmlに渡してduration周りでエラーになっていた場合、今回の変更を含む Rails バージョンに上げることで自然に解消されます。 - XML の仕様やフォーマッタの変更は不要です。既存の pretty-print やインデント付き XML をそのまま利用できます。
- 既存の XML をそのまま
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58015
タイトル: Strip surrounding whitespace before parsing a duration inHash.from_xml
作者: @55728 - 関連クラス:
ActiveSupport::XmlMiniActiveSupport::Duration::ISO8601Parser
- 類似仕様:
type="date",type="datetime",type="integer",type="boolean"など他の型では、既にプレーンテキストの前後空白に起因する問題は発生していませんでしたが、durationもその挙動に揃えられた形になります。
#58014 Remove stale payload-based filter example in EventReporter#subscribe rdoc
マージ日: 2026/7/6 | 作成者: @55728
- 概要 (1-2文で)
EventReporter#subscribe の RDoc に載っていた「payload によるフィルタ」サンプルコードが、実際の挙動と矛盾しており誤解を招くため削除され、フィルタから参照できるのはイベント名のみであることを明示するドキュメント修正です。コードの挙動には一切変更はなく、ドキュメントのみの更新です。
- 変更内容の詳細(あればサンプルコードも含めて)
変更の背景
Rails.event.subscribe の RDoc には、フィルタの例として次の2パターンが記載されていました(問題のある方だけ抜粋):
Rails.event.subscribe(subscriber) { |event| event[:payload].is_a?(UserEvent) }しかし、実際にフィルタブロックに渡される event は「イベント名だけを持つハッシュ」であり、event[:payload] は常に nil になります。そのため、このサンプルをコピペして使うと、条件が常に false になり、イベントを一件も受け取れないという不具合を招きます(ただしコード上は例外も出ないため気づきにくい)。
モジュールレベルの「Filtered Subscriptions」の説明ではすでに:
Note that only the +:name+ key is available to filters, not the entire payload.
と明記されており、#subscribe の例だけがこれと矛盾していました。
実際の変更内容
変更ファイル: activesupport/lib/active_support/event_reporter.rb
行レベルでは以下が行われています(概要ベース):
#subscribeの RDoc から、event[:payload]を使ったフィルタ例を削除- 代わりに、「フィルタから使えるのは
:nameキーのみであり、payload 全体は参照できない」という注意書きを、クラス(モジュール)レベルの説明と同様に#subscribeのドキュメントにも追記
具体的な正しい利用イメージは、たとえば次のような形になります:
# OK: イベント名でフィルタする例
Rails.event.subscribe(->(event) { Rails.logger.info(event) }) do |event|
event[:name] == "user.created"
endevent で使えるのは event[:name] だけで、payload はここには来ない、という点を強調する形に修正されています。
- 影響範囲・注意点
- 影響範囲は「ドキュメントのみ」であり、
ActiveSupport::EventReporter/Rails.event.subscribeの挙動そのものは以前から変わっていません。 - すでに
event[:payload]をフィルタ内で参照していたコードがある場合、それはこの PR の前から期待通りには動いておらず、「常に false になってイベントが一件も流れない」状態だった可能性が高いです。- もしそのようなコードが見つかった場合:
- フィルタ部分では「イベント名のみ」を使ってフィルタする
- payload ベースの条件分岐は「サブスクライバー側(購読ブロックの中)」で行う
といった構成に変更する必要があります。
- もしそのようなコードが見つかった場合:
- テストや実装を壊す類の変更ではないため、Rails を更新したことで挙動が変わることはありませんが、「今までたまたま気づかずに誤ったサンプルを真似していた」ケースを洗い出す良いトリガーにはなります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58014
- 関連ドキュメント:
ActiveSupport::EventReporterの「Filtered Subscriptions」セクション- フィルタに渡されるハッシュは
{:name => "event_name"}のように:nameのみを保持し、payload は含まれないという仕様が明文化されています。
- フィルタに渡されるハッシュは
#58005 Make add_column(if_not_exists: true) reversible
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
add_column(..., if_not_exists: true)を含むマイグレーションをロールバックした際に、逆操作として不正なオプションを付けたremove_columnが生成されていた問題を修正し、正しくif_exists: trueが付くようにした PR です。これにより、該当マイグレーションのロールバックが安全かつ他のコマンドと一貫した挙動になります。
- 変更内容の詳細
何が問題だったか
以下のようなマイグレーションを書いた場合:
add_column :users, :age, :integer, if_not_exists: truerails db:rollback などで逆マイグレーションを生成・実行すると、これまでは CommandRecorder によって次のように解釈されていました:
# BEFORE (誤った逆操作)
remove_column :users, :age, :integer, if_not_exists: trueしかし remove_column は if_not_exists オプションを受け付けません。そのため:
- 逆マイグレーションとして不正なメソッド呼び出しが生成される
- 将来的にはエラーになったり、意図しない挙動を起こす可能性がある
という問題がありました。
今回の修正内容
ActiveRecord::Migration::CommandRecorder において、add_column の逆操作 (invert_add_column) を組み立てる際のオプション変換ロジックが追加されました。
挙動は次のように変わります:
# 正方向
add_column :users, :age, :integer, if_not_exists: true
# 逆方向 (今回の PR 以降)
remove_column :users, :age, :integer, if_exists: truePR の説明にもある通り:
# inverse of add_column(:table, :column, :type, if_not_exists: true)
# before: remove_column(:table, :column, :type, if_not_exists: true)
# after: remove_column(:table, :column, :type, if_exists: true)つまり、if_not_exists を if_exists に変換してから remove_column に渡すようになりました。
他のコマンドとの一貫性
add_index, add_foreign_key, add_check_constraint ではすでに同様の変換が実装されており:
add_*(..., if_not_exists: true)
の逆操作としてremove_* ( ..., if_exists: true)
が生成されています。
今回の PR で add_column もその設計方針に揃えられました。
テスト
activerecord/test/cases/migration/command_recorder_test.rb にテストが追加され、add_column(..., if_not_exists: true) を記録して逆変換した際に if_exists: true が付いた remove_column が生成されることが検証されています。
- 影響範囲・注意点
- 対象:
add_column :table, :column, :type, if_not_exists: trueを使用しているマイグレーション- それらを
rails db:rollbackやrevertで巻き戻すケース
- 影響:
- 以前は「見かけ上」
remove_column(..., if_not_exists: true)が生成されていましたが、これはそもそも正当な API ではありませんでした。 - 今後は正しく
remove_column(..., if_exists: true)になるため、「すでにカラムが削除されている場合」でもエラーにならない、より安全なロールバックになります。
- 以前は「見かけ上」
- 後方互換性:
- 公開されていない・公式にサポートされていない挙動(
remove_columnにif_not_existsを渡す)を修正する変更であり、通常のアプリケーションコードには実害はほぼありません。 - もしテストやアプリケーション側で
CommandRecorderの生の出力を文字列ベースで厳密に検証している場合は、if_not_exists→if_existsへの変更によりテストが変わる可能性があります。
- 公開されていない・公式にサポートされていない挙動(
- マイグレーションの書き方としての注意:
- 今後も、冪等にしたいマイグレーションでは:
- 追加系:
add_column ..., if_not_exists: true - 削除系:
remove_column ..., if_exists: trueを明示的に書くのが推奨されますが、今回の修正により、逆操作生成の際にもこのペアが自動で維持されるようになります。
- 追加系:
- 今後も、冪等にしたいマイグレーションでは:
- 参考情報 (あれば)
- 当該 PR:
rails/rails#58005 “Make add_column(if_not_exists: true) reversible” - 関連する他の reversible な API 例:
add_index(..., if_not_exists: true)⇔remove_index(..., if_exists: true)add_foreign_key(..., if_not_exists: true)⇔remove_foreign_key(..., if_exists: true)add_check_constraint(..., if_not_exists: true)⇔remove_check_constraint(..., if_exists: true)
これらと同様のパターンが add_column にも適用された、という位置づけの変更です。
#58004 Return no rows without a query for none.ids
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
Relation#noneに対してids/async_idsを呼び出したとき、これまでは実際にSELECT ... WHERE (1=0)のクエリが発行されていたのを、pluckと同様にクエリを投げずに常に空配列(またはそれを返す Promise)を返すように修正した PR です。noneが「DB に触れない null object」として一貫した動作をするようになります。
- 変更内容の詳細
背景
ActiveRecord::Relation#none は「必ず結果が 0 件で、かつ DB にクエリを投げない」ことを意図した null relation です。
- 既に
pluckなど一部メソッドは@noneフラグを見て、DB に触れずに結果を返していました。 - 一方で
idsはnoneを特別扱いしておらず、内部の条件が"1=0"という生の SQL だったため、矛盾判定(例:where(id: []))に引っかからず、実クエリが発行されていました。
その結果、以前は:
Post.none.ids # => 実際には SELECT "posts"."id" FROM "posts" WHERE (1=0)
Post.none.pluck(:id) # => [] (クエリ無し)という不整合な挙動になっていました。
何をしたか
calculations.rb 内の ids / async_ids に、pluck と同様の @none チェックを追加しています。
擬似コードレベルでは以下のようなガードが入っています(実装からの意訳):
def ids
if @none
[]
else
# 従来どおりの ids のロジック(pluck(:id) 相当)
end
end
def async_ids
if @none
# すでに完了した Promise(Future)を返し、その値は [] になる
else
# 従来どおり非同期で ids を取得
end
end結果として:
Post.none.ids # => [] (クエリ発行なし)
Post.none.pluck(:id) # => [] (クエリ発行なし)
Post.none.async_ids # => 完了済み Promise, value は [] (クエリ発行なし)となり、none の定義(「DB に触れず常に空」)とテストの期待どおりの挙動になります。
テスト
activerecord/test/cases/null_relation_test.rb にテストが追加され、null_relation に対する ids の挙動(クエリを投げずに空配列を返すこと)がカバーされています。既存の pluck, delete_all, update_all, exists?, count などのテストと並ぶ形で「null relation は DB に触れない」という性質の一貫性を確認しています。
- 影響範囲・注意点
対象メソッド
ActiveRecord::Relation#idsActiveRecord::Relation#async_ids
挙動の変化
Model.none.idsが、これまでの「SELECT ... WHERE (1=0)を実行して空配列を返す」から、「クエリを打たずに即座に空配列を返す」に変わります。async_idsも同様に、「クエリを打たない完了済み Promise(値は[])」を返します。
後方互換性
- 戻り値(
[])は変わらないため、普通にアプリ側でnone.idsを使っている限りは機能的な互換性は保たれています。 - 影響し得るのは、以下のようなケースです:
- 「
none.idsでクエリが走ること」を前提にしてメトリクス・ログ収集・モンキーパッチなどをしていた場合(ほぼ無いはずですが)。 - 「クエリが走るかどうか」を検査するテストを書いていた場合(
assert_sqlなどでWHERE (1=0)を期待しているようなテストは失敗する可能性があります)。
- 「
- 戻り値(
パフォーマンス
noneを通るパスについては、不必要なクエリが完全に削除されるため、ごくわずかですがパフォーマンス改善になります。- 特に大量に
noneが生成される複雑なスコープチェーンでidsを呼んでいるようなケースでは、微妙なオーバーヘッド削減になります。
一貫性
noneが関与する以下のメソッドの挙動が揃います:pluck,ids,async_ids,delete_all,update_all,exists?,count, …
- 「
noneからは絶対にクエリを出さない」という前提でコードや最適化・テストが書けるようになります。
- 参考情報 (あれば)
- 修正対象:
activerecord/lib/active_record/relation/calculations.rb - テスト:
activerecord/test/cases/null_relation_test.rb - 関連知識:
Relation#noneは「常に空」の relation を返すための標準 API で、default_scope, 条件付きスコープ, 認可ロジックなどで「このユーザーには 1 件も見せない」ことを表現するときに頻用されます。idsは内部的にpluck(primary_key)に近い実装になっており、今回の修正でnoneに対する挙動がpluckと明示的に揃えられました。
#58007 Instrument the normalized key in MemoryStore#increment/#decrement
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::Cache::MemoryStoreのincrement/decrementが発行する計測イベント(cache_increment/cache_decrement)について、これまで「生のキー」を送っていたものを、他のストア実装と同様に「正規化済み(名前空間付き)のキー」を送るように揃えた変更です。これにより、メモリストア利用時の計測イベントのキー情報が一貫したものになります。
- 変更内容の詳細
何が問題だったか
MemoryStore#increment / #decrement が ActiveSupport のインストゥルメンテーション(ActiveSupport::Notifications)で発行するイベント:
cache_increment.active_supportcache_decrement.active_support
のペイロード :key に、キャッシュストア内部で実際に使われている「正規化後キー」ではなく、呼び出し側が渡した「生のキー」をそのまま入れていました。
cache = ActiveSupport::Cache::MemoryStore.new(namespace: "myapp")
cache.increment("counter")
# 以前:
# payload[:key] #=> "counter" (namespace が反映されていない)
#
# 期待される & 今回の修正後:
# payload[:key] #=> "myapp:counter"他のストア (FileStore, MemCacheStore, RedisCacheStore) や同じ MemoryStore の read / write イベントでは、すでに「正規化済みキー」を使っていたため、インクリメント/デクリメントだけ挙動がずれていました。
具体的な修正内容
変更ファイル:
activesupport/lib/active_support/cache/memory_store.rbactivesupport/test/cache/behaviors/cache_instrumentation_behavior.rb
ポイント:
MemoryStore#increment/#decrementのインストゥルメンテーション時に、payload[:key]として 正規化後のキー を渡すように修正- 正規化処理は
normalize_key/normalize_key相当の内部ヘルパを通す形(既存の read/write と同じパス)に変更されたと考えられます。 - 実質的には
namespaceなどが反映された「実際にストア内で利用しているキー」をイベントのキーとして報告します。
- 正規化処理は
テスト (
cache_instrumentation_behavior) の修正- インクリメント/デクリメントの計測イベントにおける
payload[:key]が名前空間付きであることを確認するアサーションが追加・修正。 - これにより、将来的に再度「生のキー」を返すような退行があった場合に検出できるようになっています。
- インクリメント/デクリメントの計測イベントにおける
サンプル(概念的なイメージ):
cache = ActiveSupport::Cache::MemoryStore.new(namespace: "myapp")
ActiveSupport::Notifications.subscribe("cache_increment.active_support") do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
puts event.payload[:key]
end
cache.increment("counter")
# 修正前: "counter"
# 修正後: "myapp:counter"- 影響範囲・注意点
- 影響対象
ActiveSupport::Cache::MemoryStoreを使っていてcache_increment.active_support/cache_decrement.active_supportイベントを購読しpayload[:key]を直接参照しているコード
に影響があります。
挙動の変化
namespaceオプションを使っていない場合:
正規化後キーも元キーと同じになるため、実質的な変化はほぼありません。namespaceを使っている場合:
これまでpayload[:key]が"counter"だったものが、"myapp:counter"のように名前空間付きになります。- それ以外にも、
normalize_keyで行っている正規化(#to_paramや encoding 調整など)があれば、その結果が反映されます。
後方互換性の観点
- 監視基盤やメトリクス集計で、「このイベントの
keyは必ず非 namespaced な生キー」という前提でロジックを書いていた場合、集計キーが変わる可能性があります。 - 他ストア(File/MemCache/Redis)と挙動を合わせた変更のため、「複数ストアで共通の購読コード」を書いているプロジェクトでは、むしろ今回の変更で挙動が一貫し、扱いやすくなります。
- 監視基盤やメトリクス集計で、「このイベントの
対応の指針
- もし「アプリケーションレベルの論理名のキー」と「実際のストアキー(namespace 付き)」を区別してメトリクスを取りたい場合は、今後は以下のような設計を検討するとよいです:
- イベントから受け取った
payload[:key]を「実キー」とみなし、 - 論理キーは別途 payload に入す(独自インストゥルメンテーション)か、キー生成側でロギングする。
- イベントから受け取った
- もし「アプリケーションレベルの論理名のキー」と「実際のストアキー(namespace 付き)」を区別してメトリクスを取りたい場合は、今後は以下のような設計を検討するとよいです:
- 参考情報 (あれば)
- 該当 PR:
Instrument the normalized key in MemoryStore#increment/#decrement(#58007) - 関連する他ストアの挙動:
ActiveSupport::Cache::FileStoreActiveSupport::Cache::MemCacheStoreActiveSupport::Cache::RedisCacheStore
これらはいずれもインストゥルメンテーションで「正規化済みキー」を報告しており、今回の変更でMemoryStoreもそれに揃った形です。
- 関連テスト:
activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb
ここを読むと、ActiveSupport が「計測イベントにどういう情報を載せることを期待しているか」のリファレンスとしても利用できます。
#58008 Clear a stale enqueue_error when perform_all_later re-enqueues a job
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
perform_all_laterがenqueue_all未実装アダプタ向けに「1件ずつ enqueue」するフォールバック経路を使う際、過去の失敗時にセットされたenqueue_errorがクリアされず、状態が矛盾する問題を修正したPRです。これにより、再enqueueに成功したジョブはsuccessfully_enqueued? == trueかつenqueue_error == nilという一貫した状態になります。
- 変更内容の詳細
背景
- Active Job には複数ジョブをまとめてキューに積む
perform_all_laterがあり、アダプタがenqueue_allを実装していない場合は、内部的に各ジョブに対して個別にenqueueを呼ぶフォールバックが行われます。 - 単一ジョブの
enqueue経路では、enqueue を試行する前にenqueue_errorをクリアする仕様になっており、「前回の enqueue 失敗情報を引きずらない」ようになっています。 - しかし、
perform_all_laterのフォールバック経路ではsuccessfully_enqueuedフラグはリセットしていた一方で、enqueue_errorをクリアしていませんでした。
問題となっていた挙動
- あるジョブが何らかの理由で enqueue に失敗し、
enqueue_errorがセットされた状態になったとします。 - そのジョブを後で
perform_all_laterのフォールバック経路(=enqueue_all非対応アダプタ)経由で再enqueueし、今度は成功した場合でも、古いenqueue_errorが残ってしまっていました。 - この結果、同じジョブインスタンスに対して:
job.successfully_enqueued? # => true
job.enqueue_error # => #<SomeError ...> (古いエラーが残っている)という矛盾した状態が起きていました。
このPRでの修正内容
- フォールバック経路(各ジョブを個別に enqueue し直すロジック)の中で、再enqueueを試みる前に
enqueue_errorもクリアするように変更。 - 単一ジョブの
enqueue経路と同様に、「enqueue を試行する際には過去のエラー情報をリセットする」挙動に揃えています。
擬似的には以下のようなイメージです(実際のコードは簡略化):
jobs.each do |job|
job.successfully_enqueued = false
job.enqueue_error = nil # ← この行が追加されたイメージ
queue_adapter.enqueue(job)
endテストの追加
activejob/test/cases/queuing_test.rb にテストが追加され、以下のようなケースが検証されています:
- 以前の enqueue 失敗で
enqueue_errorがセットされているジョブがある。 perform_all_later実行時、アダプタがenqueue_allを実装していないため 1件ずつ enqueue される。- enqueue が成功したジョブは:
successfully_enqueued?がtrueenqueue_errorがnilであることを確認。
- 影響範囲・注意点
- 対象となるのは「
enqueue_allを実装していない Active Job アダプタ」を使用し、かつ「同じジョブインスタンスを再利用してperform_all_laterで再enqueue する」ようなケースです。 - これまで「ジョブは正常に enqueue されているのに
enqueue_errorが残っている」状態に依存したコード(例えば、enqueue_errorの有無だけで前回の失敗を判定していたりする処理)があれば、挙動が変わります。ただし、本来は不整合な状態だったため、通常は修正として望ましい挙動です。 enqueue_allを実装しているアダプタにはこの変更は直接影響しません(そちらは元々enqueue_allの実装側の責務になります)。
- 参考情報 (あれば)
- 目的は「単一ジョブ enqueue とバッチ enqueue(
perform_all_laterのフォールバック)の状態管理を対称に保つ」ことです。 - Active Job でカスタムアダプタや
enqueue_allの実装を行う場合も、同様に「成功したジョブにはenqueue_errorを残さない」という一貫性を持たせるのが望ましい設計になります。
#58009 Account for queue_name_prefix in assert_enqueued_email_with
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
ActiveJob::Base.queue_name_prefixを設定している場合でも、assert_enqueued_email_withが正しくジョブのキュー名を判定できるように修正した PR です。これにより、メール自体は正しくdeliver_laterされているのにテストだけが失敗する問題が解消されます。
- 変更内容の詳細
問題の背景
config.active_job.queue_name_prefix を設定すると、Active Job ではキュー名が以下のように変更されます:
config.active_job.queue_name_prefix = "prefix"
# 通常 "mailers" になるところが "prefix_mailers" になる
TestMailer.test.deliver_laterしかし、これまでの ActionMailer::TestHelper#assert_enqueued_email_with は、
「メール系ジョブは mailers キューに入るはず」という前提で期待キュー名を決めており、queue_name_prefix などの設定を考慮していませんでした。
そのため、以下のようなコードはジョブ自体は正しく prefix_mailers に enqueue されているにもかかわらず、テストが「mailers に入っていない」と誤判定して失敗していました。
config.active_job.queue_name_prefix = "prefix"
assert_enqueued_email_with(TestMailer, :test) do
TestMailer.test.deliver_later
end
# before: 失敗 (期待 "mailers" / 実際 "prefix_mailers")
# after: 成功具体的な修正点
変更ファイルは 2 つだけです。
actionmailer/lib/action_mailer/test_helper.rbassert_enqueued_email_with内で「期待するキュー名」を決定する処理を、実際の配送ジョブ(ActionMailer::MailDeliveryJob等)が enqueue 時にキュー名を決定するロジックと同じ方法で導出するように修正。- つまり、
queue_name_prefixやqueue_name_suffixが設定されていれば、それを含んだ完全なキュー名(例:"prefix_mailers","prefix_mailers_suffix")を期待値として使うようになりました。 - コード上は 1 行の差し替えで、直接
"mailers"を見るのではなく、ActiveJob が使うキュー名解決ロジックを経由する形になったと考えられます。
actionmailer/test/test_helper_test.rb- この挙動を保証するテストが追加されています (+15 行)。
- 内容としては、
config.active_job.queue_name_prefixを設定した状態でTestMailerのdeliver_laterを呼び、assert_enqueued_email_withがパスすることを検証するテストです。 - これにより、今後
queue_name_prefixの仕様変更やassert_enqueued_email_withの改修があっても、プレフィックス付きキュー名への対応が壊れないようにしています。
- 影響範囲・注意点
影響を受けるのは「
ActionMailer::TestHelper#assert_enqueued_email_withを使っており、かつconfig.active_job.queue_name_prefix(または suffix)を使っているプロジェクト」です。- これまで、そのようなプロジェクトでは「メールは正常にキューに積まれているのにテストが落ちる」という症状が出ていたはずです。
- この PR 適用後は、そのようなテストは通るようになります。
メール配送自体の挙動 (
deliver_later時の queue や実行タイミング) には変更がなく、「テスト用ヘルパーの期待値ロジックのみ」が修正対象です。そのため、本番環境の挙動変更リスクはほぼありません。もし既にワークアラウンドとして
- 自前で
queue: "prefix_mailers"を指定して検査していたり、 assert_enqueued_email_withを使わずに低レベルなassert_enqueued_jobsなどで検査していた といったケースがある場合、この修正によりワークアラウンドが不要になる可能性があります。
- 自前で
queue_name_suffixも「prefix と同様の仕組みで解決」されるため、この PR によって suffix を用いた構成でも期待どおりに動作するようになります(prefix/suffix を総合した「実際のキュー名」にテストが揃う)。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58009
- 関連する Rails ガイド:
- Active Job Basics: Queue configuration
config.active_job.queue_adapterconfig.active_job.queue_name_prefixconfig.active_job.queue_name_delimiterconfig.active_job.queue_name_suffix
- Active Job Basics: Queue configuration
- 関連 API:
ActionMailer::TestHelper#assert_enqueued_email_withActiveJob::Base.queue_name_prefix/queue_name_suffixActionMailer::MailDeliveryJob(メール配送ジョブの実装)
#58010 Support endless and beginless ranges in number_field/range_field
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
number_field/range_fieldヘルパで、in:/within:オプションに無限範囲 (18..など) や片側無限範囲 (..10など) を渡したときにRangeErrorになっていた問題を修正し、適切にmin/max属性へ変換できるようにした PR です。Ruby の end/beginless Range を正式にサポートしつつ、従来の閉区間・排他的区間の挙動は維持しています。
- 変更内容の詳細 (サンプルコード含む)
変更の主眼
- 対象ヘルパ:
ActionView::Helpers::FormOptionsHelper#number_fieldActionView::Helpers::FormOptionsHelper#range_field
- オプション:
in:/within:に渡されるRangeの扱いを修正
以前は、以下のような範囲を渡すと RangeError が発生していました。
number_field("order", "quantity", in: 18..) # endless range
number_field("order", "quantity", in: ..10) # beginless rangeこの PR により、以下のように HTML 属性へマッピングされるようになります。
Endless range (18..)
number_field("order", "quantity", in: 18..)
# 変更前: RangeError
# 変更後: <input type="number" name="order[quantity]" min="18" />
# (max 属性は付与されない)Beginless range (..10)
number_field("order", "quantity", in: ..10)
# 変更前: RangeError
# 変更後: <input type="number" name="order[quantity]" max="10" />
# (min 属性は付与されない)既存の bounded range の挙動はそのまま
通常の閉区間(両端あり、
..):rubynumber_field("order", "quantity", in: 1..10) # <input ... type="number" min="1" max="10" />排他的範囲 (
...) も従来通り:rubynumber_field("order", "quantity", in: 1...10) # => max は「上限 - 1」としてレンダリングされる # <input ... type="number" min="1" max="9" />
PR 説明によると、この既存の「exclusive range は max を 1 小さくする」ロジックもそのまま維持されています。
実装変更の概要
actionview/lib/action_view/helpers/tags/number_field.rbのRangeからmin/maxを決定する部分を、endless / beginless のRangeを許容するように 1 行修正。actionview/test/template/form_helper_test.rbにテストケースを追加 (+10 行):- endless range を渡したときに
minのみ設定されること - beginless range を渡したときに
maxのみ設定されること - 例外が発生しないこと
- endless range を渡したときに
テスト追加により、今後のリグレッションも検知できるようになっています。
- 影響範囲・注意点
影響範囲
number_field,range_fieldをin:/within:オプション付きで利用しているフォームビューが対象。- 特に Ruby 2.6+ などで導入された end/beginless range 構文を既に利用しているコードで、以前は例外が発生していたケースが正常に動作するようになります。
- フロントエンド的には、
min/max属性の有無だけが変わるため、HTML5 バリデーションロジックや、JavaScript 側でmin/maxの有無に依存している場合に影響し得ます。
仕様面の注意点
in:/within:に Range を渡した場合の挙動は以下に整理されます:- 両端あり (
a..b) →min=a,max=b - 排他的 (
a...b) →min=a,max=b-1 - endless (
a..) →min=a,maxなし - beginless (
..b) →max=b,minなし
- 両端あり (
- Ruby の「通常の Range オブジェクトを受け付ける」という Rails のドキュメント仕様と整合しており、
RangeErrorを投げてしまうこれまでの挙動がよりどころのない状態だったため、それを是正する変更といえます。 - これにより、サーバーサイドバリデーション (
validates :field, inclusion: a..) とフォーム側の表示 (number_field ..., in: a..) を、end/beginless range を含めて一貫して書きやすくなります。
後方互換性
- これまで
RangeErrorをトリガーする前提で rescue するようなコードを書いていた場合、その挙動は変わる可能性がありますが、通常のアプリケーションではそういう利用はまず想定されないため、実質的には後方互換性を損なわない変更と考えられます。 - bounded range (特に
1...10のような排他的 range) の既存挙動には一切変更がない点がポイントです。
- これまで
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58010
- 関連する Rails ドキュメント:
- Action View Form Helpers –
number_field,range_fieldの:in/:withinオプション
- Action View Form Helpers –
- Ruby の endless / beginless range 仕様:
a../..bは Ruby 2.6 以降で導入された Range の構文であり、通常のRangeオブジェクト ((18..) === 20 #=> true) として扱われます。
#58006 Return the current value for an unchanged store accessor's _was
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
store アクセッサに対する<key>_wasの挙動が、変更されていない場合にもnilを返してしまう不整合を修正し、ActiveModel::Dirtyの契約どおり「未変更なら現在値を返す」ように揃えた PR です。これにより、store カラムを使う場合も通常の属性と同様の Dirty API の挙動が得られます。
- 変更内容の詳細
挙動の変更点
修正前(未変更時):
# 例: jsonb カラム `settings` に対して store_accessor で color を定義
# record.settings = { "color" => "black" }
record.color # => "black"
record.color_was # => nil # ← 本来期待される挙動と異なる修正後(未変更時):
record.color # => "black"
record.color_was # => "black" # ← 通常の attribute と同じ挙動値を変更した後の挙動は従来どおりで、過去の値を返します:
record.color # => "black"
record.color = "white"
record.color_was # => "black" # 変更前の値
record.color_change # => ["black", "white"] # こちらは既に修正済みだった挙動と整合
record.color_changed? # => trueどのように修正されたか(概念レベル)
activerecord/lib/active_record/store.rbの store アクセッサ定義部分で、<key>_wasが未変更時にnilを返さないように 1 行分のロジックが修正されています。ActiveModel::Dirty#attribute_wasと同様、「その属性が変更されていなければ、_wasは現在値を返す」ように挙動を合わせています。- これまでに
_change(<key>_change)については未変更時の挙動が既に修正済みだったため、今回の変更で_wasも含めて Dirty 系メソッド間で一貫した挙動になります。
テスト
activerecord/test/cases/store_test.rb に 9 行追加されています。
想定されるテスト内容(要約):
- store アクセッサを持つモデルを用意し、レコードを保存した直後(未変更状態)に
<key>_wasを呼び出すと現在値を返すことを確認。 - 値を更新した後は、変更前の値を返すことを確認。
_changeや Dirty 判定との整合性を担保。
- 影響範囲・注意点
影響範囲
対象となるのは:
store/store_accessorを使っているモデル- かつ、
<key>_wasを明示的に参照しているコード
に限定されます。
これまで:
- 「store アクセッサの
<key>_wasは、未変更時にはnilを返す」と仮定したコード(ワークアラウンド)を書いていた場合、その前提が崩れます。
これから:
- 通常の ActiveRecord 属性と同様に、
- 未変更時:
<key>_wasは現在値 - 変更後:
<key>_wasは変更前の値 となります。
- 未変更時:
移行・互換性上の注意点
もし次のようなコードを書いていた場合は挙動が変わります:
ruby# 例: nil かどうかで「未設定かどうか」を判定しているケース if record.color_was.nil? # 以前は「未変更か、元々 nil」かを区別できないが、 # 実際には「未変更 = nil」「変更済み = 過去値(non-nil)」のように使っていた可能性がある endこのような用途には Dirty API を直接使う方が正確です:
rubyif record.color_changed? # 変更された場合の処理 before, after = record.color_change end「未変更かどうか」を知りたいなら
_wasではなく<key>_changed?<key>_changesaved_change_to_<key>?/saved_change_to_<key>(永続化後の変更検知) を利用するのが筋となります。
nil自体を「未設定」の意味で使っている store キーの場合、- 未変更時に
<key>_wasがnilを返す挙動は変わりません(値がnilだから)。 - ただし、以前は「未変更だが
nil」と「変更済みだが元値がnil」を_was.nil?では区別できなかった点は変わらず、Dirty API(_changed?等)で区別する必要があります。
- 未変更時に
- 参考情報 (あれば)
- 関連 API:
ActiveModel::Dirty#attribute_wasActiveModel::Dirty#attribute_changeActiveModel::Dirty#attribute_changed?
- store / store_accessor の概要:
store :settings, accessors: %i[color size]record.color/record.color=がsettings["color"]を透過的に扱う
- この PR で
_changeと_wasの挙動が揃ったため、store アクセッサも「普通のカラム属性」と同じ感覚で Dirty 情報を扱えるようになっています。
#58011 Honor an explicit :selected option in weekday_select
マージ日: 2026/7/5 | 作成者: @55728
- 概要 (1-2文で)
weekday_selectヘルパにselected:オプションを明示的に渡した場合でも、その値が正しく優先されるように挙動が修正されました。これにより、他のselect系ヘルパ (select,collection_select,grouped_collection_select,date_select) と一貫した動きになります。
- 変更内容の詳細
これまでの挙動
weekday_select は、フォームオブジェクト(例: @digest)側に既に曜日の値が入っていると、selected: オプションを無視してオブジェクトの値を選択状態にしていました。
# モデル側の値
@digest.send_day #=> "Monday"
# ビュー側
f.weekday_select(:send_day, selected: "Friday")上記のように書いても、実際に選択されるのは "Monday" でした。
修正後の挙動
同じコードを実行した場合、selected: "Friday" が優先され、 "Friday" が選択状態になります。
# 修正後
f.weekday_select(:send_day, selected: "Friday")
# => セレクトボックス上では "Friday" が選択される実装面の変更ポイント
actionview/lib/action_view/helpers/tags/weekday_select.rbのロジックが 1 行修正され、
「オブジェクトの属性値」よりも「オプションで明示された:selected」を優先するようになりました。actionview/test/template/form_options_helper_test.rbにテストが追加され、- オブジェクトに曜日がセットされている
- かつ
selected:オプションが指定されている
というケースで、明示的selectedが優先されることが保証されています。
他の select 系ヘルパと同様に、「明示的に指定された selected があればそれを使う」という Rails の一貫したポリシーに合わせた変更です。
- 影響範囲・注意点
影響範囲
weekday_selectを使用しており、かつモデルにデフォルトの曜日が格納されているフォームでselected:オプションも指定している場合、表示される初期選択が変わる可能性があります。- これまで「オブジェクトの値が優先される」前提で
selected:を“ダミー”的に書いていたコードは、明示指定の方が効くようになるため挙動が変わります。
互換性の観点
- 他の
select系ヘルパと同じルールになるため、API 全体としては一貫性が向上します。 - ただし、
weekday_selectだけを個別仕様として利用していた場合は挙動変更となるため、フォームの初期選択状態の変化に注意してください。
- 他の
テスト
- 対応するテストが追加されているため、
selected:の優先挙動は今後も守られることが期待できます。
- 対応するテストが追加されているため、
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58011
- 類似の挙動を持つヘルパ:
ActionView::Helpers::FormOptionsHelper#selectActionView::Helpers::FormOptionsHelper#collection_selectActionView::Helpers::FormOptionsHelper#grouped_collection_selectActionView::Helpers::DateHelper#date_select
これらと同じく、「オブジェクトの値 < 明示的な selected オプション」という優先順位になった、と理解しておくと良いです。
#58003 Fix belongs_to change tracking for composite foreign keys
マージ日: 2026/7/5 | 作成者: @anxkhn
- 概要 (1-2文で)
belongs_toに複合外部キー(Composite Foreign Key)を使っている場合に、association_changed?/association_previously_changed?が常にfalseを返してしまうバグを修正したPRです。単一カラムの外部キーには影響せず、複合キーのときだけ正しく変更検知できるようにしています。
- 変更内容の詳細
問題点
belongs_to で複合外部キーを使うと、例えば以下のような関連づけになります:
class Cpk::Order < ApplicationRecord
self.primary_keys = :shop_id, :id
end
class Cpk::Book < ApplicationRecord
belongs_to :order,
class_name: "Cpk::Order",
foreign_key: [:shop_id, :order_id]
endこのとき Rails 内部では reflection.foreign_key が:
["shop_id", "order_id"]という配列になっています。
ところが、BelongsToAssociation#target_changed? および #target_previously_changed? は、この reflection.foreign_key をそのまま「1つの名前」とみなして、スカラ用の dirty メソッドに渡していました:
attribute_changed?(reflection.foreign_key) # 実際のイメージ
attribute_previously_changed?(reflection.foreign_key) # 実際のイメージスカラの dirty メソッド (attribute_changed?) は引数に対して to_s するので、配列は:
["shop_id", "order_id"].to_s
# => '["shop_id", "order_id"]'となってしまい、「そんな属性名は存在しない」ため、常に false になっていました。その結果、belongs_to :order に対して自動生成される:
order_changed?
order_previously_changed?などの公開APIが、複合キーのときだけ常に「変化なし」と報告してしまう状態でした。
単一カラムの外部キー ("shop_id" など) では to_s しても "shop_id" のままなので、この問題が表に出ず、これまで見逃されていました。
修正内容
BelongsToAssociation#target_changed? と #target_previously_changed? を、「外部キーが配列のときは各カラムを個別にチェックする」ように変更しています。
擬似コードにすると、以下のようなイメージです:
def target_changed?
Array(reflection.foreign_key).any? { |fk| owner.attribute_changed?(fk) }
end
def target_previously_changed?
Array(reflection.foreign_key).any? { |fk| owner.attribute_previously_changed?(fk) }
endポイント:
Array(reflection.foreign_key)とすることで、- 複合キー:
["shop_id", "order_id"]→["shop_id", "order_id"] - 単一キー:
"shop_id"→["shop_id"]
のように正規化されます。
- 複合キー:
.any?で「どれか1つでも変わっていれば関連変更あり」と判断します。- これは同じファイル内で既に複合キー対応している他の処理 (
replace_keys,foreign_key_present?) と同じパターンで、実装を揃えた形になっています。
ポリモーフィック belongs_to への影響
BelongsToPolymorphicAssociation は super で上記ロジックを呼んだ上で、foreign_type もOR条件でチェックする実装になっています。
- 複合キーの belongs_to でポリモーフィックなケースは、
superの修正をそのまま継承。 - ポリモーフィック外部キー自体はスカラなので、今回の修正で挙動が変わることはありません。
テスト
activerecord/test/cases/associations/belongs_to_associations_test.rb に回帰テストが追加されています。
テストのシナリオ(概念的な流れ):
- 複合主キーを持つ
Cpk::Orderを2つ作成(同じshop_idでorder_idだけ違う)。 Cpk::Bookを、最初のorderにbelongs_to :orderで紐づけて保存。- その後、
book.order = 別の orderに付け替えて、shop_idは同じだがorder_idを変更。 - ここで:
book.order_changed?がtrueになることを確認。
book.save後に:book.order_previously_changed?がtrueになることを確認。
このテストは main ブランチでは両方 false になって失敗し、この修正を適用するとパスします。
- 影響範囲・注意点
影響範囲
- 対象:
belongs_toに複合外部キーを指定している関連。 - 修正されるAPI:
association_changed?(例:order_changed?)association_previously_changed?(例:order_previously_changed?)
- 単一カラム外部キーの
belongs_toの挙動は変わりません。 - ポリモーフィック
belongs_toの挙動も、実質的には従来通りです。
- 対象:
意図的に変更していない箇所
saved_change_to_target?は今回は変更していません。- これはカウンタキャッシュや
touchコールバック専用の内部APIで、ここだけ先に直すと:after_updateで新しい親のカウンタだけインクリメントされる- 一方で
decrement_counters_before_last_saveは古い親側のデクリメントをスキップしたまま
という不整合が起き、カウンタキャッシュが膨らむ危険があります。
- 複合キーに対するカウンタキャッシュ周りの扱いは #57608 で別途対応中であり、本PRでは「公開APIの
association_changed?/association_previously_changed?のバグ修正」にスコープを限定しています。
- これはカウンタキャッシュや
互換性面での注意
- これまで「複合外部キーの
belongs_toは常に*_changed?がfalseになる」ことを前提にワークアラウンドを書いていたコードがあれば、今回の修正で挙動が変わる可能性があります。- 例:
if !order_changed?を「常にここに入る」前提ロジックとして悪用(?)していた場合など。
- 例:
- 本来の意図した動作に修正されるものであり、一般的にはバグフィックスとして望ましい変更です。
- これまで「複合外部キーの
- 参考情報 (あれば)
- 該当PR: https://github.com/rails/rails/pull/58003
- 関連する既知課題(複合キーとカウンタキャッシュまわり): https://github.com/rails/rails/pull/57608
- 実際に影響を確認したい場合:
- 複合外部キーの
belongs_toを持つモデルでxxx_changed?/xxx_previously_changed?を呼び、外部キーの一部だけ変えたときにtrueになるかをテストすると、挙動差を確認できます。
- 複合外部キーの
#57991 Stop HTML-escaping JSON written by Coders::JSON
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
ActiveRecord::Coders::JSONが JSON を DB に書き込む際、意図されていた「HTML エスケープ無し (escape: false)」設定が実際には効いておらず、HTML エスケープされてしまっていた不具合を修正する PR です。これにより、JSON 型カラムやActiveRecord::Type::Jsonとの挙動が統一されます。
- 変更内容の詳細
問題点
ActiveRecord::Coders::JSON にはもともと、次のような方針がありました。
- JSON を DB カラムに書き込むとき、HTML エスケープ(
<→\u003cなど)をスキップするために、デフォルトオプションとしてescape: falseをマージする。
しかし実装上のバグにより:
- エンコーダは「マージ済みの
@options」ではなく、「呼び出し時に渡された生のoptions引数」で初期化されていた。 - その結果、
escape: falseが実際のエンコード処理に反映されておらず、HTML エスケープが有効のままだった。
具体例:
coder = ActiveRecord::Coders::JSON.new
coder.dump({ "k" => "<>&" })
# 修正前の実際の挙動:
# => "{\"k\":\"\\u003c\\u003e\\u0026\"}"
# 期待される挙動(ネイティブ JSON カラムと同じ):
# => "{\"k\":\"<>&\"}"PR の説明で明示されている通り、修正前は:
{"k":"\u003c\u003e\u0026"}
が返されており、これは:
- ネイティブ JSON カラム
ActiveRecord::Type::Json
によるシリアライズ結果と食い違っていました。
修正内容
ActiveRecord::Coders::JSON の内部で:
- マージ後のオプション(
escape: falseを含む)を保持している@options - 元の引数として渡された
options
のうち、エンコーダの初期化に @options を使うように 1 行だけ修正されています。
擬似コードで表すと、だいたい以下の違いです(実際のコードは若干異なりますが、意味としてはこういう修正):
# 修正前 (イメージ)
def initialize(options = {})
@options = DEFAULT_OPTIONS.merge(options) # escape: false をマージしている
@encoder = SomeJSONEncoder.new(options) # ← 生 options を使っていた
end
# 修正後 (イメージ)
def initialize(options = {})
@options = DEFAULT_OPTIONS.merge(options)
@encoder = SomeJSONEncoder.new(@options) # ← マージ後オプションを使う
endこれにより、ActiveRecord::Coders::JSON を使った場合も、デフォルトで escape: false がきちんと有効になります。
テストの追加
activerecord/test/cases/coders/json_test.rb に 5 行のテストが追加され、以下を確認しています:
Coders::JSON.new.dump({ "k" => "<>&" })の結果が{"k":"<>&"}となること- つまり、HTML エスケープされないこと
- 影響範囲・注意点
影響範囲
- 対象:
ActiveRecord::Coders::JSONを使って、DB の文字列カラムなどに JSON を保存しているケース。- 例:
serialize :settings, ActiveRecord::Coders::JSONのような実装
- 例:
- 変更内容: デフォルトで HTML エスケープされていた JSON 出力が、今後はエスケープされなくなります。
互換性の観点
- ネイティブ JSON カラム(
json/jsonb)やActiveRecord::Type::Jsonの挙動と揃うため、「Rails 内での一貫性」という意味では正しい方向です。 - 既存データ:
- 既に DB に保存済みの
"k":"\u003c\u003e\u0026"などの値はそのまま残り、新規保存分のみ"k":"<>&"風になります。 - 読み込み時はどちらも JSON として問題なく扱えるため、デシリアライズ時の挙動に違いはほぼありません。
- 既に DB に保存済みの
注意点
HTML への直接出力時のセキュリティ:
- この変更は「DB に保存する JSON の文字列の中身」がエスケープされなくなる、という変更です。
- Rails の view レイヤ (
<%= %>やraw,html_safeなど) での XSS 対策とは別物です。 - JSON をそのまま HTML に埋め込んでいるようなコード(例えば
<script>var data = <%= @model.settings.to_json %>;</script>のようなもの)は、元々も適切な HTML エスケープとjヘルパーなどによる JS エスケープを自前で考慮すべき領域です。この PR によってそこが安全になったり危険になったりするわけではありませんが、「DB に保存される値が変わる」ことは理解しておく必要があります。
テストの更新:
- 自前で
Coders::JSONのシリアライズ結果文字列を検証しているテストがある場合、"\u003c"を期待していた部分は"<"になるよう修正が必要です。
- 自前で
- 参考情報 (あれば)
関連するコンポーネント:
ActiveRecord::Coders::JSONActiveRecord::Type::Json- DB のネイティブ JSON 型(PostgreSQL の
json/jsonbなど)
この PR により、以下の 3 者の JSON シリアライズ結果が揃うことが期待されます:
- ネイティブ JSON カラム (
json,jsonb) ActiveRecord::Type::JsonActiveRecord::Coders::JSON(本 PR の対象)
- ネイティブ JSON カラム (
HTML エスケープに依存していたコードがないかだけ確認しつつ、基本的には「バグ修正 + 挙動の統一」として受け入れてよい変更です。
#57990 Match HTTP auth scheme case-insensitively for Token and Bearer
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
HTTP のAuthorizationヘッダにおけるToken/Bearer認証スキームの判定が、大文字小文字を区別しない(case-insensitive)形に修正されました。これにより、bearer abc123やTOKEN xyzといった非標準の大文字小文字でも正しくトークンが認識されるようになります。
- 変更内容の詳細
2-1. 変更の背景
- RFC 9110 §11.1 によると、
Authorizationヘッダ内のauth-scheme(Basic,Bearer,Tokenなどのスキーム名)は 大文字小文字を区別しない と規定されています。 - 現実のクライアントやプロキシは
Bearerだけでなくbearer,BEARERなどさまざまな表記で送ってくることがあります。 - しかし Rails では:
- コントローラ側の
Token認証 (ActionController::HttpAuthentication::Token) ActionDispatch::Request#bearer_token
がいずれも 大文字小文字を区別する 実装になっており、bearer abc123→ 無効として扱われ 401 を返すrequest.bearer_tokenもBearer以外の表記ではnilを返す
といった問題がありました。
- コントローラ側の
- 一方で、同じファイルにある
Basic認証はすでに case-insensitive で比較しており、振る舞いが不統一でした。
本 PR はこの不整合を解消し、RFC 準拠かつ Basic と同様に、Token / Bearer も case-insensitive に扱うようにしています。
2-2. コントローラ側 (Token 認証) の変更
対象:
actionpack/lib/action_controller/metal/http_authentication.rb
ActionController::HttpAuthentication::Token 内の TOKEN_REGEX が修正されています。
元から存在する TOKEN_REGEX は Authorization ヘッダをパースして Token スキームかどうかを判定するための正規表現ですが、スキーム部分を 大文字小文字を区別せずにマッチ するように変わりました。
イメージとしては、以下のような変更です(実際のコードからの擬似例):
# Before(概念的なイメージ)
TOKEN_REGEX = /^Token\s+(.+)$/ # "Token" と完全一致でマッチ
# After(概念的なイメージ)
TOKEN_REGEX = /^Token\s+(.+)$/i # "Token" を大文字小文字を区別せずにマッチこれにより、以下のようなヘッダがすべて同様にトークンとして認識されます:
Authorization: Token abc123
Authorization: token abc123
Authorization: TOKEN abc123
Authorization: ToKeN abc123コントローラ側で authenticate_or_request_with_http_token などを使っている場合、これら全てが有効なトークンとして扱われます。
2-3. リクエスト側 (Request#bearer_token) の変更
対象:
actionpack/lib/action_dispatch/http/request.rb
ActionDispatch::Request#bearer_token は、Authorization ヘッダから Bearer トークンを取り出すヘルパーメソッドです。
元は Bearer というスキーム名を 大文字小文字を区別して マッチさせていたため、以下のようなケースは全て nil を返していました:
Authorization: bearer abc123
Authorization: BEARER abc123
Authorization: Bearer abc123
# → Bearer 以外はトークンが取れないPR ではここも case-insensitive に変更され、Basic と同様の挙動になります。
擬似的なイメージ:
# Before(概念的なイメージ)
authorization&.match(/^Bearer\s+(.+)$/)
# After(概念的なイメージ)
authorization&.match(/^Bearer\s+(.+)$/i)これにより、以下のようなコードがすべて期待通りに動作します:
# 例: Rack ミドルウェアやコントローラ内
token = request.bearer_token
# Authorization: bearer abc123
# Authorization: BEARER abc123
# Authorization: Bearer abc123
# いずれも token == "abc123" になる2-4. テストの追加
対象:
actionpack/test/controller/http_token_authentication_test.rb(+9)actionpack/test/dispatch/request_test.rb(+5)
上記の動作が保証されるよう、以下の観点のテストが追加されています:
Token認証について、TOKEN,token,ToKeNなどさまざまな表記のAuthorizationヘッダを送っても正しく認証されること。Request#bearer_tokenについて、bearer,BEARERなど任意の大文字小文字の組み合わせでもトークンを抽出できること。
これにより、今後のリファクタリング等で再び case-sensitive に戻ってしまうリグレッションを防止します。
- 影響範囲・注意点
3-1. 影響範囲
- HTTP トークン認証 (
ActionController::HttpAuthentication::Token) を利用しているアプリケーション ActionDispatch::Request#bearer_tokenを直接、またはライブラリを通じて利用しているアプリケーション- 上記を利用しているライブラリ・ミドルウェア(OAuth2 実装、API 認証機構など)
基本的には後方互換な改善 であり、従来通り Token xxx / Bearer xxx もそのまま動作します。そのうえで、今まで 401 になっていた bearer xxx / TOKEN xxx などが通るようになります。
3-2. 注意点・想定外の影響になり得るケース
- もしアプリケーション側で「
Bearerという表記だけを許したい」「tokenは拒否したい」といった、あえて非 RFC 準拠な制約 を設けていた場合、その前提は崩れます。
ただし、auth-schemeを case-sensitive に扱うこと自体が RFC 9110 に反するため、そのような前提がある場合は設計の見直しが推奨されます。 - 同一クライアント・同一トークンで、これまでたまたま
Bearerになっていたケースでは何も変わらず、混在環境(クライアントの一部がbearerを送るなど)でのみ挙動が変わる可能性があります。
- 参考情報 (あれば)
- RFC 9110 §11.1: Authentication Scheme
auth-schemeは case-insensitive と定義されている。
- 関連する Rails コード:
ActionController::HttpAuthentication::Tokenactionpack/lib/action_controller/metal/http_authentication.rbActionDispatch::Request#bearer_tokenactionpack/lib/action_dispatch/http/request.rb
- 類似の既存挙動:
Basic認証はすでにスキーム名を case-insensitive に扱っており、本 PR によってToken/Bearerもそれに揃えられた形になります。
#57993 Fix incorrect number_to_percentage default precision in docs [ci skip]
マージ日: 2026/7/4 | 作成者: @anxkhn
- 概要 (1-2文で)
number_to_percentageヘルパーのドキュメントに書かれていた:precisionオプションのデフォルト値が誤っており、実際の挙動(デフォルト3桁)に合わせて「2」から「3」に修正した PR です。コードや挙動の変更はなく、ドキュメントのみの修正です。
- 変更内容の詳細
何が問題だったか
number_to_percentage の RDoc には、:precision オプションについて次のように書かれていました。
# [+:precision+]
# The level of precision, or +nil+ to preserve +number+'s precision.
# Defaults to 2.
#
# number_to_percentage(12.3456789, precision: 4) # => "12.3457%"しかし実際には、number_to_percentage(100) のデフォルト出力は "100.000%" であり、デフォルト精度は「3」です。
- ベースのフォーマット設定:
DEFAULTS[:format][:precision] = 3 :currencyだけが precision を 2 にオーバーライド:percentageは:delimiterと:formatしかオーバーライドしていないため、precision=3 を継承
そのため、「Defaults to 2.」という説明が実際の挙動および直前のサンプル (number_to_percentage(100) # => "100.000%") やテストと矛盾していました。
具体的な修正内容
activesupport/lib/active_support/number_helper.rb の1行だけが変更されています。
# [+:precision+]
# The level of precision, or +nil+ to preserve +number+'s precision.
-# Defaults to 2.
+# Defaults to 3.
#
# number_to_percentage(12.3456789, precision: 4) # => "12.3457%"number_to_currency 側の「Defaults to 2.」は正しいためそのままです。
- 影響範囲・注意点
実行時の挙動は一切変わりません。
もともとnumber_to_percentageのデフォルト precision は 3 であり、この PR はそれを説明するドキュメントを正しくしただけです。影響があるのは「ドキュメントを読んで実装していた人の認識」だけです。
- これまで「precision のデフォルトは 2 だ」と思い込んでいた場合、実際の出力とのギャップに気づいていなかった可能性があります。
- ただしコード側は以前から 3 桁出力で動いており、PR による挙動変更はありません。
もしアプリ側で「パーセンテージは必ず小数2桁にしたい」という要件がある場合は、明示的に precision を指定する必要があります。
rubynumber_to_percentage(12.3456) # => "12.346%" (デフォルト precision: 3) number_to_percentage(12.3456, precision: 2) # => "12.35%"他のヘルパーとの一貫性:
number_to_rounded,number_to_human_size,number_to_humanなど、同じくベースの precision を継承するヘルパーはすでに「Defaults to 3.」と記載されており、number_to_percentageだけがドキュメント上ズレていました。本 PR により説明の一貫性が取れています。
- 参考情報 (あれば)
デフォルト precision が 3 であることを示すソース:
activesupport/lib/active_support/number_helper/number_converter.rbDEFAULTS[:format][:precision] = 3DEFAULTS[:currency][:format][:precision] = 2DEFAULTS[:percentage]は:delimiterと:formatだけ上書きし、precision は継承
activesupport/lib/active_support/locale/en.ymlnumber.format.precision: 3number.currency.format.precision: 2number.percentage.formatは precision 未指定 → 3 を継承
テストコード
activesupport/test/number_helper_test.rbactivesupport/test/core_ext/numeric_ext_test.rb
いずれもnumber_to_percentage(100) == "100.000%"を前提にしており、precision=3 を前提にしていることが分かります。
#57986 Fix Guides Index flyout menu overflowing its panel
マージ日: 2026/7/4 | 作成者: @iuhoay
- 概要 (1-2文で)
Rails Guides のデスクトップ向け「Guides Index」フライアウトメニューで、3カラムレイアウトが横幅を超えて右にはみ出していた問題を、CSS のレイアウト指定を見直すことで解消した PR です。widthとmarginの組み合わせで 100% を超えていたものを、column-gapとcalc()を用いてちょうど 100% に収まるようにしています。
- 変更内容の詳細
対象: guides/assets/stylesrc/_main.scss
問題点:
.guides-section-container内の.guides-sectionが以下のように指定されていました:width: 33%;margin: 0 2em 0.5em 0;
- 3カラム × 33% = 99% に加え、各カラム右側に
2emマージンがあり、その分が合計で 100% を超えてしまい、右端のカラムがコンテナの外にはみ出していました。 - はみ出した結果、右端カラム内の長いリンクテキスト(例: "Installing Rails Core Development Dependencies")がパネル外へ溢れて見えてしまう状態になっていました。
修正内容:
.guides-section-container {
display: flex;
flex-direction: column;
flex-wrap: wrap;
+ column-gap: 2em;
max-height: 60em;
width: 100%;
.guides-section {
flex: auto;
- margin: 0 2em 0.5em 0;
+ margin: 0 0 0.5em 0;
text-align: left;
- width: 33%;
+ width: calc((100% - 4em) / 3);
}
}ポイント:
- カラム間のスペースの扱いを、各要素の
margin-rightではなく、コンテナ側のcolumn-gapに移行:- 以前:
.guides-sectionにmargin-right: 2em;相当の指定 - 以後:
.guides-section-containerにcolumn-gap: 2em;
- 以前:
.guides-sectionの幅を固定の33%から、カラム間ギャップを考慮したcalc((100% - 4em) / 3)へ変更:- 3カラムとその間に 2つのギャップ(
2em× 2 =4em)がある前提で、- コンテナの 100% 幅からギャップ合計
4emを引いた残りの幅を 3 等分。
- コンテナの 100% 幅からギャップ合計
- これにより「カラムの幅 × 3 + ギャップ × 2 = 100%」となり、パネル幅を超えなくなります。
- 3カラムとその間に 2つのギャップ(
.guides-sectionのmarginは下方向 (margin-bottom: 0.5em) のみ残し、左右の余白は0に変更。
結果として、3カラムとその間の余白がコンテナ幅にぴったり収まり、右端カラムや長いリンクテキストがパネル外へ溢れなくなります。
- 影響範囲・注意点
- 影響範囲:
- Rails Guides サイトのデスクトップビューにおける「Guides Index」フライアウトメニューのレイアウトに限定されます。
- それ以外のページレイアウトや、Rails 本体の機能・挙動には影響しません(スタイルシートのみの変更)。
- 技術的な注意点:
column-gapは比較的新しめの CSS プロパティですが、デスクトップ主要ブラウザでは概ねサポートされています。Rails Guides のターゲットブラウザを考えると問題になる可能性は低いと考えられます。calc((100% - 4em) / 3)によって、コンテナ幅とフォントサイズ(emに依存)が変わるとカラム幅も自動的に再計算されるため、レイアウトはある程度柔軟に対応できますが、「3カラム+ギャップ 2つ」であることが前提になっています。将来的にカラム数を変える場合は、この式も合わせてメンテナンスする必要があります。flex-direction: column; flex-wrap: wrap;という少し特殊な使い方をしているため、DOM 構造の変更やコンテナ高さの変更(max-height: 60em;)とあわせて動作を確認するのが望ましいです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57986
- CSS リファレンス:
- Flexbox と縦方向ラップ(
flex-direction: column; flex-wrap: wrap;)の挙動:
https://developer.mozilla.org/en-US/docs/Web/CSS/flex-wrap
#57997 Use Range#cover? for this_week?/this_month?/this_year?
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
Date/Time関連のヘルパーthis_week?/this_month?/this_year?が、内部判定にRange#include?ではなくRange#cover?を使うように変更され、性能が大きく改善されました。振る舞いは変えずに、呼び出しごとの無駄な日付走査を避けるリファクタリングです。
- 変更内容の詳細
何をしているか
this_week?, this_month?, this_year? は、引数の日時オブジェクトが「今週 / 今月 / 今年」に属するかどうかを判定するメソッドです。
もともとはだいたい次のようなイメージで実装されていました:
def this_week?
all_week.include?(self)
end
def this_month?
all_month.include?(self)
end
def this_year?
all_year.include?(self)
endここで all_week, all_month, all_year は Date の Range を返します。Range#include? は #succ で一つずつ値を進めながら探索する実装になるため、Date の場合:
this_week?→ 約 7 回succ(7日分)this_month?→ 最大 31 回succthis_year?→ 約 365 回succ
といった線形探索が毎回発生していました。
今回の変更では、これらを Range#cover? に差し替えています:
def this_week?
all_week.cover?(self)
end
def this_month?
all_month.cover?(self)
end
def this_year?
all_year.cover?(self)
endRange#cover? は「範囲の開始と終了の比較」だけで判定するため、内部で要素を1つずつたどることがなく、10〜100倍程度高速になることが PR 説明で言及されています。
背景
- 先行 PR #57963 で
this_quarter?はすでにcover?に変えられており、今回の PR はそのフォローアップとして、兄弟関数 (this_week?/this_month?/this_year?) を同じ実装方針に揃えたものです。 - テストの変更はなく、既存の境界条件テストもそのまま通ることが確認されています。
- 影響範囲・注意点
挙動面
Range#include?とRange#cover?は、Date/Timeのように比較が定義されたオブジェクトに対して、典型的な連続範囲を扱う場合は同じ結果を返します。- 既存テストが境界値(開始日・終了日を含むかどうか等)をカバーしており、そのままパスしているため、APIの論理的挙動は変わらないと見なして問題ありません。
パフォーマンス面
this_year?のように長い期間の Range を頻繁に判定しているコードでは、顕著な性能改善が見込めます。- 特に大量レコードに対して
this_*?を呼ぶ場面(バリデーション、集計、バッチ処理など)で恩恵が大きくなります。
後方互換性
- 公開 API のメソッドシグネチャや返り値の型に変更はなく、内部実装の最適化のみです。
Range#cover?/Range#include?の差異に依存した特殊なケース(例えば、離散的でない独自オブジェクトを Range に入れている等)をこのメソッド群が利用しているわけではないので、互換性上のリスクは非常に低いと考えられます。
- 参考情報 (あれば)
- この PR: https://github.com/rails/rails/pull/57997
- 先行 PR (
this_quarter?をcover?に変更): https://github.com/rails/rails/pull/57963 - Ruby リファレンス(
Range#cover?とRange#include?の違い)cover?: 始点・終点の比較で判定(O(1))include?/member?:===判定・succによる走査の可能性があり、離散オブジェクトでは O(n) になり得る
#57963 Add this_quarter? to Date/Time
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
Date,Time,DateTime,ActiveSupport::TimeWithZoneに対して「今期(今四半期)かどうか」を判定するthis_quarter?メソッドが追加されました。これにより、week/month/year に揃う形で quarter にもthis_*?プレディケートが用意され、日付処理の一貫性が向上しています。
- 変更内容の詳細
追加された機能
対象クラス:
DateTimeDateTimeActiveSupport::TimeWithZone
に、以下のプレディケートメソッドが追加されました:
date_or_time.this_quarter? # => true / false判定ロジックの概要this_month? や this_year? と同様に、「現在の四半期」を基準にして判定します。
- 基準となる“今”は
Date.current(=Time.zoneを考慮した現在日付) - その
Date.currentが属する四半期を「現在の四半期」とみなし、 - 受け取った日付/時刻オブジェクトがその四半期の範囲内にあるかどうかを判定
概念的には以下と同等の判定です:
def this_quarter?
Date.current.all_quarter.cover?(to_date)
end※実装は activesupport/lib/active_support/core_ext/date_and_time/calculations.rb に追加されており、既存の this_month? / this_year? と同じスタイルで this_quarter? が定義されています。
使用例
# 前提: Date.current が 2000-02-15 (Q1: 1〜3月) の場合
Date.new(2000, 3, 31).this_quarter? # => true (同じ四半期 Q1)
Date.new(2000, 4, 1).this_quarter? # => false (次の四半期 Q2)
Time.utc(2000, 1, 1).this_quarter? # => true (Q1 に含まれる)
DateTime.civil(2000, 4, 1).this_quarter? # => false (Q2)既存機能との関係
Rails には既に以下のような四半期関連のメソッドがありました:
beginning_of_quarterend_of_quarterall_quarternext_quarterprev_quarter
一方で、週・月・年にはある this_*? 系が四半期にだけ存在していませんでした:
| week | month | quarter | year | |
|---|---|---|---|---|
| beginning_of_* | ✓ | ✓ | ✓ | ✓ |
| end_of_* | ✓ | ✓ | ✓ | ✓ |
| all_* | ✓ | ✓ | ✓ | ✓ |
| next_* | ✓ | ✓ | ✓ | ✓ |
| prev_* | ✓ | ✓ | ✓ | ✓ |
| this_*? | ✓ | ✓ | — | ✓ |
今回の PR で、この — が this_quarter? により埋まり、API の一貫性が取れるようになりました。
テストとドキュメント
変更ファイル:
activesupport/lib/active_support/core_ext/date_and_time/calculations.rbthis_quarter?実装追加 (+5行)
activesupport/test/core_ext/date_time_ext_test.rbtest_this_quarter追加 (+9行)this_month?/this_year?と同じテストパターンで、四半期境界をカバー
activesupport/CHANGELOG.md- 新機能として
this_quarter?追加を明記 (+12行)
- 新機能として
テストでは、四半期の開始日・終了日・直前直後の日付などを用い、境界条件を確認しています。
- 影響範囲・注意点
影響範囲
- ActiveSupport の
Date/Time拡張を利用しているアプリケーション全般で、新たにthis_quarter?が利用可能になります。 - 既存メソッドの挙動変更や破壊的変更は含まれていません(追加のみ)。
実務上の利点
これまでよく見られたコード:
Date.current.all_quarter.cover?(record.created_at.to_date)
# または
Date.current.all_quarter.include?(record.created_at.to_date)を、より簡潔に:
record.created_at.this_quarter?と書けるようになります。
四半期ベースの:
- 請求期間・課金期間の判定
- OKR・目標管理の「今期」判定
- 財務・レポート系でのフィルタリング
などが読みやすく実装できます。
Time.zone / タイムゾーンへの注意
this_quarter? は Date.current を基準に判定するため、config.time_zone と Time.zone の設定に依存します。this_month? / this_year? と同じ挙動です。
例:
- アプリの
Time.zoneが"Tokyo"の場合: 「今期」は東京時間での日付を基準とする - DB が UTC でも、
TimeWithZoneで評価される場合はTime.zoneを考慮
タイムゾーンを切り替えるテストや、マルチタイムゾーン対応アプリでは、Time.use_zone を用いた検証を行うと安全です。
- 参考情報 (あれば)
- PR 本文(英語)でのディスカッションと動機:
- 事前議論が行われた Rails forum:
- 既存の
this_month?/this_year?実装の参考:activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
#57987 Make add_index(if_not_exists: true) reversible
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
add_index/remove_indexのマイグレーションを逆変換(rollback 時の自動生成)する際に、if_not_exists/if_existsオプションが正しく対応付けられるようになりました。これにより、既存/非存在なインデックスに対して安全にロールバックでき、これまで発生していた不要な例外が解消されます。
- 変更内容の詳細
何が問題だったか
Rails の Migration には「コマンドインバータ(ActiveRecord::Migration::CommandRecorder)」があり、change メソッド内で書いた操作から rollback 用の逆操作を自動的に生成します。
しかし、これまで以下のような問題がありました:
remove_index(..., if_exists: true)を反転するとadd_indexが生成されるが、
その際にif_exists: trueが 失われる(:if_exists は add 側には存在しないオプションのため、単に削除されるだけだった)。- 結果: rollback 時に、すでにインデックスが存在するケースで例外が発生する。
add_index(..., if_not_exists: true)を反転するとremove_indexが生成されるが、if_not_exists: trueがそのままremove_indexに渡されてしまい、remove_indexはif_not_existsを 理解しない ため、事実上オプションが無視される。- 結果: rollback 時に、インデックスが存在しないケースで例外が発生する。
つまり、「forward 方向では存在チェック付きで安全に動くように書いたマイグレーション」が、rollback 時にはその安全性が失われていた、という状態です。
今回の修正内容
CommandRecorder の invert_add_index / invert_remove_index において、if_not_exists / if_exists を相互に変換するようになりました。
概念的には以下のような変換が行われます:
# 変更前(概念図)
invert_remove_index(columns, if_exists: true)
# => [:add_index, [columns], {}] # :if_exists が落ちる
invert_add_index(columns, if_not_exists: true)
# => [:remove_index, [columns], { if_not_exists: true }] # remove_index では無視される
# 変更後(概念図)
invert_remove_index(columns, if_exists: true)
# => [:add_index, [columns], { if_not_exists: true }]
invert_add_index(columns, if_not_exists: true)
# => [:remove_index, [columns], { if_exists: true }]これにより:
- 「存在していたら削除」を rollback すると「存在していなかったら作成しない(存在しなくてもエラーにしない)」という安全な対になる関係になる
- 「なければ作らない」を rollback すると「すでになくてもエラーにしない削除」になる
サンプルコードイメージ
class AddIndexToUsersEmail < ActiveRecord::Migration[7.2]
def change
# インデックスがなければ作成
add_index :users, :email, if_not_exists: true
end
endこの change を rollback すると、従来は概ねこう解釈されていました:
remove_index :users, :email
# if_not_exists は無視されるので、インデックスがなければエラー今回の修正後は、内部的には以下のような remove_index が生成されます:
remove_index :users, :email, if_exists: true
# インデックスがなくてもエラーにならない逆に:
class RemoveIndexFromUsersEmail < ActiveRecord::Migration[7.2]
def change
remove_index :users, :email, if_exists: true
end
endを rollback すると、今までは(概念的に):
add_index :users, :email
# すでにインデックスがあるとエラーとなっていましたが、これからは:
add_index :users, :email, if_not_exists: true
# すでにインデックスがあってもエラーにならないとなります。
実装面の変更
activerecord/lib/active_record/migration/command_recorder.rbadd_index/remove_indexの invert ロジックに、オプションハッシュの変換処理を追加:if_not_exists→:if_exists、:if_exists→:if_not_existsのマッピングを行うようにした
activerecord/test/cases/migration/command_recorder_test.rb- 上記の変換が正しく行われているかどうかを検証するテストを追加
この挙動は前にマージされた invert_add_foreign_key / invert_remove_foreign_key(PR #57972)や check_constraint 周りの invert 処理と揃えた形です。
- 影響範囲・注意点
影響範囲
changeメソッド内でadd_index ... if_not_exists: trueまたはremove_index ... if_exists: trueを利用しているマイグレーションの rollback 挙動が、より安全かつ期待に沿ったものになります。- 既存のマイグレーションファイルに対しても、Rails 本体を上げれば新しい CommandRecorder のロジックで rollback が行われます。
期待されるメリット
- すでに適用済みのマイグレーションを本番・ステージングで rollback する際に、「インデックスがもうある / もうない」状況でも例外が出づらくなります。
- 並列デプロイや手動メンテなどでインデックス状態がずれがちな環境でも、マイグレーションの安全性が増します。
注意点
- 動作が「壊れる」方向ではなく、「これまで例外が出ていたケースで出なくなる」変更なので、互換性上の問題はほぼありません。
- ただし、あえて rollback 時にインデックスの存在 / 非存在を厳密に検出したい、という特殊なケースでは挙動が変わる(例外が起きにくくなる)点は認識しておくとよいです。
- Schema の整合性チェック等で、インデックスの存在を前提にした独自ツールを組んでいる場合は、「if_not_exists / if_exists 付きのマイグレーションでも rollback が失敗しにくくなった」ことを踏まえて運用を見直す余地があります。
- 参考情報 (あれば)
- この PR:
- Make
add_index(if_not_exists: true)reversible (#57987)
- Make
- 関連 PR:
- foreign key inverter を同様に修正した PR: #57972
- 該当クラス/メソッド:
ActiveRecord::Migration::CommandRecorder#invert_add_indexActiveRecord::Migration::CommandRecorder#invert_remove_index
#57992 Require fileutils in Cache::FileStore
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::Cache::FileStore内でFileUtilsを利用しているにもかかわらず"fileutils"を require していなかった問題を修正し、スタンドアロンな ActiveSupport 利用時に発生していたNameErrorを解消する PR です。Rails フルスタックでは暗黙に読み込まれていた依存を、明示的に宣言するようになりました。
- 変更内容の詳細
変更は 1 行のみで、ActiveSupport::Cache::FileStore のファイル先頭付近に require "fileutils" が追加されました。
イメージとしては以下のような変更です(擬似コード):
# activesupport/lib/active_support/cache/file_store.rb
# 旧:
# module ActiveSupport
# module Cache
# class FileStore < Store
# ...
# FileUtils.makedirs(...)
# FileUtils.rm_r(...)
# ...
# 新: fileutils を明示的に読み込む
require "fileutils"
module ActiveSupport
module Cache
class FileStore < Store
# 内部で FileUtils.makedirs, FileUtils.rm_r を使用
end
end
end背景として、このクラスはキャッシュディレクトリの作成や削除に FileUtils.makedirs や FileUtils.rm_r を使っていますが、これまでは同じプロセス内のどこか別の場所で "fileutils" が require されている前提になっていました。Rails 全体をロードしている場合は、他のコンポーネントが fileutils を読み込むため問題は表面化していませんでしたが、ActiveSupport 単体で ActiveSupport::Cache::FileStore を使うと次のようなエラーが起きていました。
require "active_support"
require "active_support/cache"
store = ActiveSupport::Cache::FileStore.new("/tmp/cache")
store.write("foo", "bar")
# => NameError: uninitialized constant ActiveSupport::Cache::FileStore::FileUtilsこの PR により、file_store.rb 自身が require "fileutils" するため、上記のような NameError は発生しなくなります。
- 影響範囲・注意点
影響範囲
ActiveSupport::Cache::FileStoreを利用する全てのコードに対し、挙動は「例外が出なくなる」以外は実質的な変更はありません。- 既に Rails フルスタックを利用しているアプリケーションでは、もともとどこかで
fileutilsがロードされていたため、挙動上の違いはほぼありません。 - ActiveSupport を単体で利用しているツール・スクリプト・ライブラリにおいて、
FileStoreを使った際のNameError (uninitialized constant FileUtils)が解消されます。
注意点
require "fileutils"は Ruby 標準ライブラリの読み込みであり、互換性上の問題を引き起こす可能性は低いです。- もしアプリ側で
FileUtils定数を独自にモンキーパッチしていた場合でも、この PR は単に標準ライブラリを require するだけなので、通常は影響ありません(既にロード済みなら再 require も安全)。 - 「他のファイルに依存して暗黙に定数をロードする」状態が解消され、今後は
file_store.rb単体で完結した依存関係になります。
- 参考情報 (あれば)
- 対象クラスのドキュメント(キャッシュストアの概要など)
- ActiveSupport::Cache::FileStore (現行 Rails ガイド / RDoc)
- 類似のパターンとして、標準ライブラリに依存しているクラス・モジュールは、そのファイル自身で
requireするのが推奨されます。今回の修正は、そのベストプラクティスに沿ったものといえます。
#57984 Quote the index name in MySQL enable_index/disable_index
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
MySQL / MariaDB 用アダプタのenable_index/disable_indexが、インデックス名をクォートせずに SQL を生成していた問題を修正し、予約語や特殊文字を含むインデックス名でも正常に動作するようにしました。rename_indexなど他のインデックス操作と一貫して、インデックス名にquote_column_nameを適用するようになっています。
- 変更内容の詳細
何が問題だったか
enable_index / disable_index は、内部的に MySQL / MariaDB の以下のような SQL を発行します:
ALTER TABLE `testings` ALTER INDEX order VISIBLE
ALTER TABLE `testings` ALTER INDEX order INVISIBLEここで order のように、
- SQL の予約語
- バッククォートすべき特殊文字を含む名前
をインデックス名に使っていると、インデックス名がクォートされていないため 構文エラー になります。
どのように修正されたか
abstract_mysql_adapter.rb の enable_index / disable_index 実装で、インデックス名を quote_column_name で包むように変更されました。
概念的には以下のような変更です:
# 変更前 (イメージ)
def enable_index(table_name, index_name)
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{index_name} VISIBLE")
end
# 変更後 (イメージ)
def enable_index(table_name, index_name)
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{quote_column_name(index_name)} VISIBLE")
end
def disable_index(table_name, index_name)
execute("ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{quote_column_name(index_name)} INVISIBLE")
endその結果、実際に発行される SQL は:
-- Before
ALTER TABLE `testings` ALTER INDEX order VISIBLE
-- After
ALTER TABLE `testings` ALTER INDEX `order` VISIBLEとなり、インデックス名がバッククォートで囲まれます。
quote_column_name は、MySQL アダプタではカラム名・インデックス名など識別子を `name` のようにクォートするためのメソッドで、既に rename_index など他のインデックス操作では使われていました。今回の修正で、インデックス関連操作のクォート戦略が統一されました。
テストの追加
activerecord/test/cases/migration/index_test.rb にテストが追加され、
- 予約語や特殊文字を含むインデックス名に対して
enable_index/disable_indexがエラーなく動作し、正しい SQL が生成される
ことが検証されています。
(具体的なテストコードは PR 情報にはありませんが、order のようなケースをカバーしていると考えられます。)
- 影響範囲・注意点
- 対象:
- Active Record を使った MySQL / MariaDB アプリケーション
enable_index/disable_indexを直接またはマイグレーション経由で利用している場合
- 影響:
- これまで「インデックス名が予約語等だと
enable_index/disable_indexで落ちる」ケースが解消されます。 - 既存の正常なケースについては、単にインデックス名がクォートされるだけなので、後方互換性への影響はほぼありません(MySQL / MariaDB は識別子をクォートしても同じオブジェクトを指すため)。
- これまで「インデックス名が予約語等だと
- 注意点:
- もしアプリ側で生 SQL を組み立てて
ALTER TABLE ... ALTER INDEX ...を発行している場合は、この修正は関係ありません。その場合も同様に、インデックス名はバッククォートでクォートするのが安全です。 - マイグレーション上で
index: { name: "order" }のような名前を指定しても、今回の修正によりenable_index/disable_index時のエラーは防げるようになりますが、アプリケーションコードや他の SQL からの参照でも予約語を使う場合は同様の注意が必要です。
- もしアプリ側で生 SQL を組み立てて
- 参考情報 (あれば)
- MySQL マニュアル:
- 「Schema Object Names」(識別子とクォート)
- 「Reserved Words」(予約語一覧)
- Rails ソース周辺:
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#quote_column_namerename_index実装 (同アダプタ内、インデックス名クォートの既存の扱いの参考になる)
#57988 Raise on ignored if_exists option in change_table's remove_timestamps
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
change_tableブロック内でt.remove_timestampsにif_exists:/if_not_exists:オプションを渡した場合、これまでは黙って無視されていましたが、他のメソッドと同様にArgumentErrorを投げて呼び出し側に誤用を知らせるように変更されました。これにより、サポートされないオプションの利用を早期に検知できるようになります。
- 変更内容の詳細
何が変わったか
- 対象メソッド:
ActiveRecord::ConnectionAdapters::Table#remove_timestamps - 変更点:
remove_timestampsが内部でraise_on_if_exist_optionsを呼ぶようになりました。- これにより、
if_exists:/if_not_exists:オプションが渡された場合は例外を発生させます。
従来の挙動:
change_table :users do |t|
t.remove_timestamps if_exists: true # ← オプションは黙って無視される
end
# 例外は発生しないが、if_exists: は一切効いていない今回の変更後:
change_table :users do |t|
t.remove_timestamps if_exists: true
end
# => ArgumentError: options :if_exists, :if_not_exists are not supported ...これは、既に以下のようなメソッドで行われている挙動と揃えたものです。
t.timestamps(if_exists: true)→ArgumentErrort.remove(:column, if_exists: true)→ArgumentErrort.remove_index(:column, if_exists: true)→ArgumentError
つまり、change_table ブロック内の「if_exists / if_not_exists をサポートしないヘルパー」は、全て一貫して例外を投げるようになりました。
テスト追加
activerecord/test/cases/migration/change_table_test.rb にテストが追加され、例えば以下のようなケースがカバーされていると考えられます。
t.remove_timestamps(if_exists: true)がArgumentErrorを投げることt.remove_timestamps(if_not_exists: true)がArgumentErrorを投げること
(実際のテストコードは数行で、この挙動を保証する内容になっています。)
- 影響範囲・注意点
影響を受けるケース
- 既存のマイグレーションで、
change_tableブロック内に以下のようなコードがある場合:
change_table :users do |t|
t.remove_timestamps if_exists: true
# または
t.remove_timestamps if_not_exists: true
endこれらは これまでは正常終了していたが、オプションは効いていなかった のに対し、今後は ArgumentError によってマイグレーションが失敗します。
なぜ例外にするのか
- サポートされていないオプションを黙って無視すると、開発者が「存在しない場合はスキップされるはず」と誤解したままになり、スキーマ不整合や本番環境でのバグの原因になります。
- 他の
change_tableヘルパー (timestamps,remove,remove_indexなど) はすでに同じポリシーで挙動しており、remove_timestampsだけ例外だったため、API 一貫性の観点からも修正されています。
対応方法
もし上記のようなコードが既に存在する場合は:
if_exists:/if_not_exists:を削除する (最も単純):rubychange_table :users do |t| t.remove_timestamps end「存在チェック込みで安全に消したい」という意図であれば、自前で条件分岐する:
rubydef up if column_exists?(:users, :created_at) && column_exists?(:users, :updated_at) change_table :users do |t| t.remove_timestamps end end end def down change_table :users do |t| t.timestamps end end
なお、
change_tableではなくremove_timestampsを直接呼び出すremove_timestamps(:users)のような DSL を使っている場合、この PR はchange_tableブロック内のt.remove_timestampsに対する変更なので、その部分には直接は影響しません (この PR の差分範囲に依存しますが、説明文からはchange_tableブロック内のヘルパーが対象と読み取れます)。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57988
- 関連する API:
change_tableブロック内ヘルパー:t.timestampst.removet.remove_indext.remove_timestamps
- オプション検証メソッド:
raise_on_if_exist_options(ActiveRecord 内部ユーティリティ)
この変更は「サイレントに失敗するマイグレーション」を減らすための一貫性改善であり、バグ修正に近い性質の変更と考えられます。
#57989 Fix color_field emitting invalid color values for strings containing a hex substring
マージ日: 2026/7/4 | 作成者: @55728
- 概要 (1-2文で)
color_fieldヘルパーの色値バリデーションが不正確だったため、<input type="color">にブラウザが解釈できない値がそのまま出力されていた問題を修正しています。正規表現を「文字列全体が#rrggbb形式かどうか」を見るように変更し、それ以外は#000000にフォールバックする挙動を統一しました。
- 変更内容の詳細
何が問題だったか
従来の color_field 内の validate_color_string は、値の中に「6桁の16進数」がどこかに含まれていれば OK と判定していました。
そのため、例えば以下のような値が「有効」とみなされていました。
"Not a color #123456 at all""#12345678"(8桁、実際には CSS の#rrggbbaaのような形式)- その他、文字列の一部に
#abcdefのようなパターンが含まれるもの
これらはそのまま <input type="color"> の value 属性に出力されるため、ブラウザ側では無効なカラー値として扱われ、期待どおり動作しない(色が選択されていない状態になるなど)問題がありました。
具体的な修正内容
validate_color_string の正規表現を、
- 「6桁の 16 進数の連続部分をどこかから拾ってくる」
から - 「文字列全体が
#rrggbb形式と完全一致しているかを確認する」
ように変更しています。
Ruby 的には、例えばこんなイメージです(実際のコード断片のイメージ):
def validate_color_string(value)
# 変更前(イメージ)
# value[/#[0-9a-fA-F]{6}/] || "#000000"
# 変更後(イメージ)
value =~ /\A#[0-9a-fA-F]{6}\z/ ? value : "#000000"
end/\A...\z/ でアンカーしているため、以下のような挙動になります:
"Not a color #123456 at all"→ 不一致 →"#000000"にフォールバック"#12345678"→ 不一致 →"#000000"にフォールバック"#1234TR"→ 不一致 →"#000000"(従来からのフォールバック挙動を踏襲)"#123456"→ 一致 → そのまま"#123456"を利用
テストの追加
actionview/test/template/form_helper_test.rb に 6 行ほどテストが追加され、上記のようなケースで:
- 部分一致する文字列や
- 8桁のカラーや
- その他の malformed な値
が渡されたときに、value="#000000" が出力されることが確認されています。
- 影響範囲・注意点
影響範囲
ActionView::Helpers::FormTagHelper#color_field_tagActionView::Helpers::FormHelper#color_field
など、Rails 標準のcolor_field/color_field_tagを通じて<input type="color">を生成している部分すべてに影響します。
挙動の変化
- これまでは「文字列の中に有効な
#rrggbbパターンが一部でも含まれていれば、それをそのまま value に出力する」挙動でした。 - 今後は「文字列全体が
#rrggbb形式である場合だけ受け入れ、それ以外は必ず#000000にフォールバック」になります。 - ブラウザ上の見た目として、今まで一見「謎の初期値」になっていたケースが、明示的に黒 (
#000000) で表示されるようになります。
- これまでは「文字列の中に有効な
後方互換性の観点
- もしアプリケーション側で、意図的に
"Some text #ff0000"のような値をcolor_fieldに渡し、ブラウザが無効値として扱うことに依存していた場合、その挙動は変わります(常に#000000になります)。 - 一般的には、HTML の仕様に沿ったより厳格なバリデーションとなるため、バグ修正としての互換性の破壊は最小です。
#rrggbbaa(8桁)のような色表現を将来的に扱いたい場合は、color_fieldではなくカスタムの input / JS を利用する必要があります(HTML<input type="color">自体が現状#rrggbbしか受けないため)。
- もしアプリケーション側で、意図的に
注意点
- サーバー側から
color_fieldに渡す値は、#+ 16進6桁の文字列(/\A#[0-9a-fA-F]{6}\z/)に正規化してから渡すようにしておくと、今回の変更に依存せず安全です。 - 不正な値をそのまま投げても自動で
#000000になるため、UI 的に「入力値がおかしい」ことがユーザーに分からない可能性があります。必要に応じてモデルバリデーションやクライアント側バリデーションで補完してください。
- サーバー側から
- 参考情報 (あれば)
- HTML 仕様上、
<input type="color">のvalueは#rrggbb形式(#+ 16 進 6 桁)以外は無効値として扱われます。 - Rails 側でのフォールバックポリシー:
- 有効な
#rrggbb形式 → そのまま出力 - それ以外 →
#000000(黒)に統一
という仕様が、この修正でより一貫した形になりました。
- 有効な
#57985 Update PostgreSQL database.yml sample in Command Line guide [ci skip]
マージ日: 2026/7/4 | 作成者: @fuentesjr
- 概要 (1-2文で)
Command Line ガイド内の PostgreSQL 用database.ymlサンプルを、現行のrails new ... --database=postgresqlが生成する内容に合わせて更新した PRです。合わせて、非推奨になりつつあるpoolキーの例を、新しいmax_connectionsキーの例に置き換えています。
- 変更内容の詳細
対象ファイル: guides/source/command_line.md の「Configure a Different Database」セクション内の PostgreSQL サンプル config/database.yml。
主な変更は2点です。
(1) pg_config のパスの更新
Homebrew でインストールした pg gem に対して pg_config の場所を指定するコメントを、Intel Mac 前提のパスから Apple Silicon 前提のパスに変更しています。
-# gem install pg -- --with-pg-config=/usr/local/bin/pg_config
+# gem install pg -- --with-pg-config=/opt/homebrew/bin/pg_configRails のジェネレータテンプレート(railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt)側が、すでに /opt/homebrew に切り替わっていたため、ガイドのサンプルもそれに追随させた形です。
(2) pool から max_connections への置き換え
database.yml 内の接続数関連のキー例を、旧来の pool から max_connections に変更しています。
- pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %>
+ max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>ポイント:
- キー名が
pool→max_connectionsに変更poolは、今後max_connections/min_connectionsに置き換わっていく方向で、ドキュメント上も新しいキーを教えるように修正されています。
- デフォルト値も
3→5に変更- これはジェネレータテンプレートに合わせたもので、実際に
rails new app --database=postgresqlした結果と一致させています。
- これはジェネレータテンプレートに合わせたもので、実際に
この変更により、ガイドに載っている YAML サンプルと、最新の Rails が自動生成する config/database.yml の内容が揃いました。
- 影響範囲・注意点
- 実装コードへの影響はなく、ドキュメントのみの変更です。
- ただし、これからガイドを読んで PostgreSQL 設定を行う開発者は、以下の点に注意が必要です:
- 新規:
- 新規アプリを Rails ジェネレータで作る場合、
pool:ではなくmax_connections:を使うのが推奨される形になります。 - コネクションプールサイズを調整したい場合は
max_connections/min_connectionsを前提に考えるとよいです。
- 新規アプリを Rails ジェネレータで作る場合、
- 既存アプリ:
- 既存アプリで
pool:を使っていても、この PR 自体が挙動を変えるわけではありません。 - ただし、今後のガイドや設定例は
max_connectionsベースになるため、タイミングを見て移行を検討すると、将来の非推奨・削除対応がしやすくなります。
- 既存アプリで
- 新規:
- Apple Silicon Mac(
/opt/homebrew)以外の環境では、pg_configのパスは当然異なり得ます:- Intel Mac(Homebrew):
/usr/local/bin/pg_config - Linux (例):
/usr/bin/pg_configなど
→ コメントはあくまで例示なので、自分の環境に合わせてパスは調整する必要があります。
- Intel Mac(Homebrew):
- 参考情報 (あれば)
- テンプレート本体:
railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.ttmax_connections/min_connectionsのドキュメント化に関する関連 PR:- #57736
- 対象ガイド(edge guides):
- Command Line > Configure a Different Database
https://edgeguides.rubyonrails.org/command_line.html#configure-a-different-database
- Command Line > Configure a Different Database
#57921 Expand Action Mailer delivery_method configuration docs [ci skip]
マージ日: 2026/7/4 | 作成者: @davidstosik
- 概要 (1-2文で)
Action Mailer のconfig.action_mailer.delivery_methodのドキュメントを拡充し、循環参照的で分かりにくかった記述を、実際に使えるレベルの説明に整理した PR です。あわせてsmtp_settings/sendmail_settings/file_settingsとの依存関係が分かるように、記述の順番も入れ替えています。
- 変更内容の詳細
※コード変更ではなく、guides/source/configuring.md(Rails Guides の設定ガイド)内の Action Mailer セクションの文章修正です。
実際の差分は概ね以下のような内容を含むと考えてよいです(擬似的な例を含みます):
2.1 delivery_method の説明を具体化
以前のドキュメントでは、delivery_method の説明が他の箇所を参照するだけで、実際に何をどう設定できるのかが分かりにくく、参照が行ったり来たりする「循環」状態でした。
この PR では、以下のような情報が一か所にまとまるように整理されています:
delivery_methodに指定できる主なシンボル例::smtp:sendmail:file:test- (Action Mailer アダプタを追加していれば)独自の delivery method クラス
- それぞれが「どのような形でメールを送る/保存する」のかという概要
:smtp→ SMTP サーバ経由で送信:sendmail→sendmailコマンド経由で送信:file→ 実際には送信せず、メール内容をファイルに書き出す:test→ActionMailer::Base.deliveries配列に溜めるだけ(テスト用)
たとえば、設定例として以下のようなコードがドキュメント内で示されているはずです:
# config/environments/production.rb
Rails.application.configure do
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: "smtp.example.com",
port: 587,
user_name: ENV["SMTP_USERNAME"],
password: ENV["SMTP_PASSWORD"],
authentication: :login,
enable_starttls_auto: true
}
end2.2 設定項目の順序を依存関係に沿って並べ替え
元のガイドでは、smtp_settings / sendmail_settings / file_settings が先に出てきて、その後に delivery_method が説明されていた、あるいは相互に行き来する形だったため、読み始めの人には依存関係が分かりにくくなっていました。
この PR では:
- まず
config.action_mailer.delivery_methodで「どの配信方法を使うか」を説明 - その後に、選択した配信方法ごとの設定 (
smtp_settings,sendmail_settings,file_settingsなど) を説明
という順番に変えています。
これにより、「delivery_method で :smtp を選んだ場合に、次に smtp_settings をこう書く」という流れが自然に理解できるようになります。
2.3 delivery_method と *_settings の関係を明記
config.action_mailer.smtp_settings などが「どの delivery method を選んだときに有効になるか」が明示されるように説明が補足されています。
例:
config.action_mailer.smtp_settingsはdelivery_method = :smtpのときに意味を持つconfig.action_mailer.sendmail_settingsはdelivery_method = :sendmailのときに使用されるconfig.action_mailer.file_settingsはdelivery_method = :file向け
この関係が文章としてはっきり書かれたことで、「設定を書いたのに効かない」という混乱を防げるようになっています。
- 影響範囲・注意点
- 影響範囲はドキュメント(Rails Guides)のみであり、Action Mailer の動作や API には変更はありません。
- アプリケーション側のコードを書き換える必要はありませんが、今後
delivery_methodを設定する際に、- どの method を選ぶべきか
- どの *_settings が効くか といった点が、今回のガイド更新を読むことで理解しやすくなります。
- 既存プロジェクトで「メールが飛ばない」「設定が効いていない」などのトラブルがある場合、まずこの更新後のガイドを確認すると原因把握に役立ちます。
- 参考情報 (あれば)
- 対応している Issue: #57876
→delivery_methodのドキュメントが循環参照的で分かりにくいという報告への対応。 - 具体的な差分は Rails 本体リポジトリの
guides/source/configuring.mdの変更箇所で確認できます。 - Action Mailer の基本的な設定全体は Rails Guides の「Configuring Rails Applications」内の Action Mailer セクションにまとまっています。
#57972 Make remove_foreign_key(if_exists: true) reversible
マージ日: 2026/7/3 | 作成者: @55728
- 概要 (1-2文で)
Rails のマイグレーションでremove_foreign_key if_exists: trueを実行した際、その逆操作(ロールバック)として生成されるadd_foreign_keyが誤ってif_exists: trueを含んでしまう問題を修正した PR です。これにより、ロールバック時にif_existsが正しくif_not_existsに変換され、操作がより安全かつ意図通りに動作するようになります。
- 変更内容の詳細
何が問題だったか
ActiveRecord::Migration::CommandRecorder は、マイグレーションのコマンドからロールバック用の逆コマンドを自動生成します。
従来は次のような挙動になっていました:
# forward
remove_foreign_key :orders, :users, if_exists: true
# generated inverse (before this PR)
add_foreign_key :orders, :users, if_exists: trueしかし add_foreign_key は if_exists: オプションをサポートしていません。
そのため:
- 生成されたロールバックコードが不正なオプションを含む
- ロールバックの「冪等性」(同じロールバックを何度実行しても問題にならないこと)が崩れる
という問題がありました。
修正内容
この PR では、invert_remove_foreign_key の処理を修正し、if_exists: true をロールバック側では if_not_exists: true に変換するようにしました。
要するに:
# forward
remove_foreign_key :orders, :users, if_exists: true
# generated inverse (after this PR)
add_foreign_key :orders, :users, if_not_exists: trueとなります。
これにより:
- すでに外部キーが存在しない場合でも
remove_foreign_keyが安全に実行できる (if_exists: true) - ロールバック時に外部キーが既に存在していても
add_foreign_key側でif_not_exists: trueにより二重作成を避けられる
という、前後両方向での冪等性が担保されます。
実装箇所
activerecord/lib/active_record/migration/command_recorder.rbinvert_remove_foreign_key(もしくはそれに相当するロジック)に 1 行追加し、オプション変換ロジックにif_exists→if_not_existsのマッピングを追加
activerecord/test/cases/migration/command_recorder_test.rb- 上記変換が正しく行われることを確認するテストを 5 行追加
実装自体はごく小さい変更ですが、挙動の一貫性と安全性を高める修正です。
- 影響範囲・注意点
対象:
remove_foreign_key ... if_exists: trueをマイグレーションで使用しているプロジェクト- 特に
rails db:rollback/rails db:migrate:downなどで自動生成される逆マイグレーションを信頼しているケース
期待される挙動:
- 既存マイグレーションの「前方」実行 (
db:migrate) の挙動は変わりません。 - 新しい Rails バージョンでロールバックすると、逆操作が
add_foreign_key ... if_not_exists: trueとして扱われ、外部キーがすでに存在している環境でもエラーになりにくくなります。
- 既存マイグレーションの「前方」実行 (
注意点:
add_foreign_keyはif_not_exists:をサポートしていることが前提です。古い Rails から途中バージョンを飛ばしてアップグレードする場合は、if_not_exists対応が入っているか(おおむね 7.1 以降)を確認してください。- もし、自前で
CommandRecorderを拡張している場合は、remove_foreign_key/add_foreign_keyまわりのオプション変換ロジックと競合しないか軽く確認すると安全です。
- 参考情報 (あれば)
- 類似の変換ロジック:
invert_add_foreign_keyでif_not_exists→if_existsに変換しているコミット:8d5c5bd2f5invert_remove_check_constraintでも同様にif_existsとif_not_existsの変換を行っており、それと整合する変更です。
- 該当 PR:
#57970 Make ActiveSupport::ProxyLogger#silence suppress logs
マージ日: 2026/7/3 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::ProxyLogger#silenceが期待どおりログを抑制するように修正された PR です。silenceブロック内で、指定した一時的なログレベル未満のメッセージが内部ロガーへ転送されないようになりました。
- 変更内容の詳細(サンプルコード含む)
背景となる問題
ActiveSupport::ProxyLogger は LoggerSilence を include しているものの、これまで silence を呼んでも実際には何も抑制されていませんでした。
proxy = ActiveSupport::ProxyLogger.new(ActiveSupport::Logger.new($stdout))
proxy.silence do
proxy.info("noise")
end
# 期待される動作(ActiveSupport::Logger と同じ):
# 何も出力されない
# 実際の動作(修正前):
# "noise" が出力されていたRails 本体では、マイグレーション処理などの内部で logger.silence を使って冗長なログを抑制しており、
「標準 Logger インターフェースをほぼサポートする」ことをドキュメントで謳っている ProxyLogger が silence を無視してしまうのは契約違反になる、というのが問題意識です。
この PR の主な修正点
ProxyLogger#silenceが、ブロック内でのログレベルを一時的に引き上げて実際に抑制するようになった。LoggerSilenceの期待する挙動に合わせ、ブロック内では一時的な severity(例:Logger::ERROR)より低いログを出さない。- これにより
ActiveSupport::Loggerと同じ挙動になります。
抑制は「その
ProxyLoggerインスタンスのみに限定」されるようになった。- 同じ「内部ロガー(
ActiveSupport::Loggerなど)」を共有している複数のProxyLoggerがある場合、 片方でsilenceしても、もう片方には影響しないように実装されています。 - これは
ActiveSupport::Logger#silenceの挙動と整合的です(ロガー単位ではなく、プロキシごとにスコープされる)。
- 同じ「内部ロガー(
テストの追加
activesupport/test/proxy_logger_test.rbに、次のような観点のテストが追加されています:silenceブロック内で、閾値より低い severity のログが出力されないこと。- 同じ underlying logger を共有する別の
ProxyLoggerが、片方のsilenceの影響を受けないこと。
※ 実際のコードは 4 行追加・1 行削除と小さい変更ですが、要点は「ProxyLogger 経由でログ出力される前に、自身のサイレンス状態を考慮してフィルタする」ようにしたことです。
- 影響範囲・注意点
影響範囲
ActiveSupport::ProxyLoggerを使い、かつproxy_logger.silence { ... }しているコードに影響します。- これまで「
silenceを呼んでも実際には抑制されていなかった」挙動が修正されるため、ログ出力量が減る 可能性があります。 - ただし、
ProxyLogger自体がまだリリース前のクラスであり、公開バージョンではこの挙動に依存しているアプリは基本的に存在しない想定です。
注意点
同じ underlying logger を共有する他の
ProxyLoggerに対しては、silenceは効きません。
例えば以下のケースでproxy1を silence しても、proxy2は通常どおりログを出します:rubybase_logger = ActiveSupport::Logger.new($stdout) proxy1 = ActiveSupport::ProxyLogger.new(base_logger) proxy2 = ActiveSupport::ProxyLogger.new(base_logger) proxy1.silence do proxy1.info("hidden") # → 抑制される proxy2.info("visible") # → 出力される end複数のプロキシ間で一括してサイレンスしたい場合は、基底ロガー側で制御するか、別途ラッピングが必要です。
ログレベルに依存する処理(例:
debugログだけをフックするミドルウェアなど)を ProxyLogger 経由で行っている場合、silenceブロック内ではその処理も呼ばれなくなる可能性があります。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57970
- 関連クラス:
ActiveSupport::ProxyLoggerActiveSupport::LoggerActiveSupport::LoggerSilence/LoggerSilence
logger.silenceは Rails において、マイグレーションや内部処理の一部で冗長なログを抑制する用途で使用されており、本 PR により ProxyLogger でも同じ期待値で利用できるようになります。
#57888 Make autosave association callbacks Ractor-safe
マージ日: 2026/7/3 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Active Record の autosave な関連(autosave: trueな association)で生成されるコールバックメソッドを、Ractor(並行実行環境)でも安全に動作するように実装を変更した PR です。define_method+ ブロックで作っていたメソッドをclass_eval+ シンボル呼び出しに変えることで、非共有Procをキャプチャしないようにしています。
- 変更内容の詳細
背景・問題点
- autosave な関連(例:
has_many :comments, autosave: true)では、内部的に例えば以下のようなメソッドが自動生成されます:autosave_associated_records_for_comments
- これらは Active Record 内部の
define_non_cyclic_methodというヘルパで定義されており、以前はだいたい次のような形で実装されていました(イメージ):
define_non_cyclic_method(:comments) do |owner|
owner.autosave_associated_records_for_comments
end- 実際には
define_methodとブロックを使ってメソッドが定義されており、そのブロックは Ruby のProcオブジェクトとしてクラスにぶら下がります。 - Ractor(Ruby 3+ の並行実行モデル)では、「共有不可能なオブジェクト(unshareable)」を他の Ractor にそのまま渡せません。ブロックから生成される
Procは通常共有不可能オブジェクトのため、それをキャプチャしたメソッドを非メイン Ractor から呼び出そうとすると制約に引っかかります。 - その結果、非メイン Ractor 内で autosave 関連のコールバックが呼び出されるケースで問題が起きうるため、Ractor-safe な形に書き換えた、というのが PR のモチベーションです。
具体的な変更点
PR 説明によると:
define_non_cyclic_methodimplementation to use class_eval and symbols instead ofdefine_methodand blocks.
つまり:
- 変更前:
define_method+ ブロック(Proc をキャプチャ) - 変更後:
class_eval+ メソッド名(シンボル)経由のディスパッチ
のような形になります。
中でやっていることのイメージを、かなり単純化したサンプルで表現すると:
変更前(イメージ)
def define_non_cyclic_method(name, &callback)
define_method("autosave_associated_records_for_#{name}") do
callback.call(self)
end
endここで &callback は Proc であり、define_method のブロックとしてクラスにぶら下がります。この Proc が Ractor 的には共有できないオブジェクトになりうる。
変更後(イメージ)
def define_non_cyclic_method(name, method_name)
class_eval <<~RUBY, __FILE__, __LINE__ + 1
def autosave_associated_records_for_#{name}
#{method_name} # あるいは send(:#{method_name}) 等
end
RUBY
end- 文字列を
class_evalしてメソッドを定義するため、メソッド本体は単に既存のメソッド名を呼び出すだけになります。 - ここでは共有不可能な
Procをクラスにキャプチャしないため、Ractor 間でも問題になりにくくなります。
実際のコードはもう少し複雑ですが、「ブロックをキャプチャして定義する」から「ソースコード文字列で定義し、実際の処理は別メソッド(シンボル)に委譲する」という方向に変わった、と理解しておくと良いです。
パフォーマンス
付属のベンチマークでは、autosave_associated_records_for_comments を大量に呼び出したときの性能を比較しています:
- before: 約 4,491,233 i/s
- after: 約 4,411,090 i/s
差は誤差範囲内("same-ish")とされており、パフォーマンス退行はほぼ無いとみなされています。
- 影響範囲・注意点
影響範囲
- Active Record の autosave 関連コールバックの生成メカニズム(
define_non_cyclic_method)が変更されています。 - 対象となるのは主に:
has_many :foo, autosave: truehas_one :bar, autosave: true- その他 autosave オプション付きの関連に付随する内部的なコールバックメソッド
- 表向きの API(
autosave: trueの指定方法や、関連オブジェクトの保存時挙動)は変わりません。
注意点 / 開発者視点でのチェックポイント
- Ractor を使っていないアプリに対する互換性
- 既存挙動を壊すような変更は意図されておらず、ベンチマーク上も挙動自体は変わらない前提です。
- ただし、メソッド定義の仕方が
define_methodからclass_evalに変わるため、メタプログラミングやmethod_source系のツールでクラス定義を解析している場合に、挙動が微妙に変わる可能性はあります(メソッドのsource_locationなど)。
- Ractor を利用している / 利用予定のアプリ
- この変更により、autosave な関連を持つモデルをサブ Ractor から扱ったときの安全性が向上します。
- ただし Ractor そのものは他にも多数の制約があるため、「これで Active Record が完全に Ractor-safe になった」という保証ではなく、「autosave association callbacks に関する一つの問題を解消した」に留まると考えるべきです。
- テストについて
- チェックリストでは「Tests are added or updated」が未チェックになっています。
- つまり、この PR 単体では新規テストやテスト更新が入っていない可能性があります。
- 既存テストで十分にカバーされている前提か、あるいは別途 Ractor 関連のテストが追加されるフェーズがあるかもしれません。
- Ractor 環境下の挙動についてはまだテストカバレッジが薄い可能性があるため、Ractor を本格利用する場合は手元での実験・検証を推奨します。
- チェックリストでは「Tests are added or updated」が未チェックになっています。
- 参考情報 (あれば)
- Ractor と shareable / unshareable オブジェクトの制約については、Ruby 本体のドキュメントが参考になります:
- Rails の autosave 機能自体の仕様:
autosave: trueの関連は、親オブジェクトのsave時に関連先も自動的にsave/destroyされる機能で、バリデーションやトランザクションと組み合わせて複雑なオブジェクトグラフの整合性を保つ用途でよく利用されます。
- 本 PR の趣旨:
- 「挙動を変えずに実装を Ractor フレンドリーにする」リファクタリングの一種であり、今後の Ruby 並行実行サポートに向けた地ならしと言えます。
#57927 Make Active Record scope methods Ractor-shareable
マージ日: 2026/7/3 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Active Record のscopeが Ractor(並列実行機構)上で利用できるように、スコープ定義で使われるProcを「Ractor-shareable」な形に変更した PR です。これにより、Ractor 内から AR モデルのスコープを安全に呼び出せるようになります。
- 変更内容の詳細
何をしたか
ActiveRecord::Scoping::Named内のscope定義処理を変更し、スコープ定義に使うProcを Ractor で共有可能なオブジェクト(Ractor-shareable)として扱うようにした。- それに合わせてテスト (
named_scoping_test.rb) を追加し、Ractor 上でもスコープが動作することを確認している。
Ruby 3 以降の Ractor では、スレッド安全性を保つために「共有できるオブジェクト」と「共有できないオブジェクト」が区別されており、通常の Proc やクロージャはしばしば「共有不可」とみなされます。この PR は、Active Record の scope が内部的に利用する Proc を、Ractor をまたいで共有できる形に組み立て直しています。
想定されるコードレベルの振る舞い
普段のスコープ定義:
class Post < ApplicationRecord
scope :published, -> { where(published: true) }
endこの PR 以前:
- 上記の
-> { ... }が作るProcは「Ractor-shareable でない」ケースがあり、
Ractor 内からPost.publishedを呼ぶとエラーになったり、利用に制限があった。
この PR 後:
scope :published, -> { ... }で内部的に生成されるProcが、Ractor で共有可能な形になるように処理される。- たとえば、以下のようなコードが動くことをテストしているはずです(イメージ):
r = Ractor.new do
# Ractor 内から Active Record のスコープを呼び出す
Post.published.to_a
end
r.takeテストファイル (named_scoping_test.rb) では、
- スコープが Ractor 内で問題なく呼び出せること
- スコープチェーンや既存の機能との互換性
といった点を確認するケースが追加されています。
- 影響範囲・注意点
影響範囲
- 対象:
ActiveRecord::Base.scopeで定義された全てのスコープ - 利用場面:
- Ractor を使った並列処理の中で Active Record のスコープを呼ぶ場合
- Ractor をまたいでモデルクラス自体を共有するような構成
既存コードのインターフェース(scope :name, -> { ... } の書き方や返り値など)は変わりません。
スコープに関係するのは主に「内部で使われる Proc の性質」なので、通常のアプリケーションで Ractor を使っていなければ、挙動は従来とほぼ同じです。
注意点
Ractor 非対応なオブジェクトをキャプチャするスコープ
スコープ定義内で、Ractor 非共有オブジェクト(ミューテーブルなオブジェクト、スレッドローカルな状態、IO など)をキャプチャしている場合、それが Ractor-shareable なProcとして扱えるかどうかは Ruby 本体/Active Record 側の実装に依存します。
原則として:- スコープには「Ractor 間で共有できる情報」だけを使う
- 外部の状態(特にミューテーブルなオブジェクト)を閉じ込めない
という形にしておくと安全です。
CHANGELOG 未更新
CHANGELOG はまだ更新されていませんが、挙動としては「新機能(Ractor 対応強化)」に近い変更です。将来的にドキュメントや変更履歴に追記される可能性があります。Ruby / Rails のバージョン依存
この PR は Ractor を前提としているため、Ruby 3 以降が前提です。Rails 側がサポートする Ruby バージョンとの組み合わせに依存するので、利用する際は公式のサポートマトリクスを確認してください。
- 参考情報 (あれば)
- Ractor 概要(Ruby公式ドキュメント)
- Active Record Scopes ガイド
- 類似の Ractor 対応 PR(Ruby / Rails 並列実行まわり)を追うと、今後のマルチRactor対応の方向性を把握しやすくなります。
#57961 Add ActiveSupport::Ractors.try_make_shareable
マージ日: 2026/7/3 | 作成者: @gmcgibbon
- 概要 (1-2文で)
ActiveSupport::Ractors.try_make_shareableという新しいメソッドが追加され、任意のオブジェクトを「Ractor 共有可能(shareable)」にしようと試みる共通ユーティリティが ActiveSupport に入りました。unshareable_proc_actionの設定に応じて、共有不可な Proc などにぶつかったときの挙動(例外・警告・無視)を切り替えられます。
- 変更内容の詳細
追加された API
activesupport/lib/active_support/ractors.rb に以下のようなメソッドが追加されています(概念的なイメージ):
module ActiveSupport
module Ractors
class << self
# すでに存在するもの (イメージ)
# def try_shareable_proc(proc, unshareable_proc_action: ...)
# ...
# end
# 今回追加されたもの
def try_make_shareable(object, unshareable_proc_action: Rails.application.config.active_support.unshareable_proc_action)
case unshareable_proc_action
when :raise
::Ractor.make_shareable(object) # Ractor::IsolationError がそのまま飛ぶ
when :warn
begin
::Ractor.make_shareable(object)
rescue Ractor::IsolationError => error
ActiveSupport::Deprecation.warn(<<~MSG)
Ractor isolation error while trying to make object shareable: #{error.message}
MSG
object # エラーは握りつぶして元のオブジェクトを返す/処理を続行
end
when nil
begin
::Ractor.make_shareable(object)
rescue Ractor::IsolationError
object # 完全に無視(ログ・警告も出さない)
end
else
# 想定外の設定値が来た場合のフォールバック(多くは :raise と等価)
::Ractor.make_shareable(object)
end
end
end
end
end※上記は PR 説明とテストから読み取れる挙動イメージです。実際のコードとはメソッド名や細部が多少異なる可能性があります。
Rails にはすでに「Proc 専用」の try_shareable_proc が存在しており、この PR は「任意オブジェクト版」の try_make_shareable を追加したものです。#57852 など他の PR/機能から、この共通メソッドを呼び出す前提になっています。
unshareable_proc_action による挙動の違い
unshareable_proc_action は ActiveSupport の既存設定で、Proc が Ractor 的に共有できない場合どう扱うかを決めるフラグです。この PR では**「オブジェクト全般を Ractor 共有化しようとしたときに発生する isolation error」に対しても、その設定を流用する**ようになっています。
:raise(デフォルト想定)Ractor.make_shareable(object)実行時にRactor::IsolationErrorが起これば、そのまま例外として呼び出し元に飛ぶ。- Ractor 安全性が最も厳密。
:warnRactor::IsolationError発生時に、ActiveSupport の警告(Deprecation.warnなど)としてログを出し、処理自体は継続。- 実際にはオブジェクトは共有可能にならない(/なれなかった)可能性があるが、アプリは止めずに挙動を観測できる。
nilRactor::IsolationErrorを完全に握りつぶし、何も出さない。- Ractor 共有化に失敗しても無視して続行したい場合のため。
テスト
activesupport/test/ractors_test.rb に 50 行超のテストが追加され、主に以下をカバーしていると考えられます:
try_make_shareableが:raise,:warn,nilの各設定値で期待通りの挙動をすること- 共有可能なオブジェクト(例: freeze 済みの単純な Hash/Array など)が問題なく共有可能になること
- 共有不可能なオブジェクト(Proc を含むオブジェクト)が、設定に応じて例外・警告・無視として扱われること
- 影響範囲・注意点
Ractor を利用している / 利用予定の Rails アプリ・ライブラリ
Ractor.make_shareableを直接呼ぶ代わりに、ActiveSupport::Ractors.try_make_shareableを使うことで、環境設定に応じてエラー処理を一元化できます。- Ractor 対応のコードを書くとき、例外をどう扱うかを都度実装する必要が減ります。
unshareable_proc_action設定の意味合いが広がる- これまでは「Proc を共有可能にするときの挙動」を制御する設定でしたが、この PR によって「任意オブジェクトを Ractor 共有可能にしようとするときの挙動」にも影響します。
- 既に
unshareable_proc_actionをカスタマイズしているアプリでは、オブジェクト共有化まわりの挙動にもそのポリシーが適用される点を意識する必要があります。
例外を握りつぶす設定に注意
:warnやnilを使うと、「本来は Ractor 共有されている前提でコードを書いたが、実際には共有化されていない」という状態になりうるため、マルチ Ractor 実行時のデータ競合や設計ミスに気付きにくくなります。- 本番運用ではまず
:raiseで問題の有無を洗い出し、問題箇所を修正した上で必要に応じて:warn/nilを検討するのが安全です。
- 参考情報 (あれば)
既存の Proc 用 API:
try_shareable_procを追加した PR: https://github.com/rails/rails/pull/57626本 PR で追加された API を利用予定の PR:
Ruby 公式ドキュメント(Ractor):
- https://docs.ruby-lang.org/ja/latest/class/Ractor.html
- 特に
Ractor.make_shareableとRactor::IsolationError周りが今回の変更と関連します。
#57976 Make ActiveSupport::EventReport::LogSubscriber ractor safe
マージ日: 2026/7/3 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveSupport::EventReport::LogSubscriberのevent_log_levelAPI が Ractor セーフになるように、内部で使っているlog_levelsの扱いを「コピー&追記&freeze」に変更した PRです。共有ハッシュをそのまま書き換えないことで、Ractor 間でのスレッドセーフ/Ractor セーフな利用ができるようになっています。
- 変更内容の詳細(あればサンプルコードも含めて)
※実際の差分は2行だけですが、説明のために典型的な変更前後イメージを示します(正確なメソッド名や変数名は若干異なる可能性があります)。
変更のポイント
log_levels(イベント名 => ログレベル のマッピング)を破壊的に更新しないようにした。- 新しいイベントログレベルを追加するときに
- 既存のハッシュを
dup - その dup したハッシュに新しいキー/値を追加
- そのハッシュを
freeze - それを元の参照に再代入
というフローに変更。
- 既存のハッシュを
このパターンは「イミュータブルなハッシュをバージョンアップしていく」やり方で、Ractor から見ても「freeze 済みのオブジェクト」を共有するだけになるため安全です。
イメージコード
変更前(イメージ):
def self.event_log_level(event_name, level)
log_levels[event_name.to_sym] = level
end変更後(イメージ):
def self.event_log_level(event_name, level)
new_log_levels = log_levels.dup
new_log_levels[event_name.to_sym] = level
@log_levels = new_log_levels.freeze
endここでのポイント:
- すでに
@log_levels自体がfreezeされている想定で、その frozen なハッシュを直接書き換えなくなった。 - 毎回 dup してから freeze することで、「書き換え可能なオブジェクト」が Ractor 間で共有されなくなる。
テストの追加
activesupport/test/event_reporter/log_subscriber_test.rb に6行追加されており、主に以下の点を検証していると考えられます:
event_log_levelを複数回呼んでも例外が出ないことlog_levelsが常にfreezeされていること- (Ruby が Ractor をサポートする環境であれば)Ractor 内から
event_log_levelを使ってもエラーにならないこと、など
- 影響範囲・注意点
- 対象:
ActiveSupport::EventReport::LogSubscriberを直接または経由して利用しているコード。- 主に「イベントレポートのログレベルを動的にカスタマイズしている」ようなコードが影響範囲です。
- 挙動面:
- 公開APIとしては
event_log_levelの使い方・結果は変わらない想定です(同じイベントに対するログレベルが期待通り設定される)。 - 内部実装のみがスレッド/Ractor セーフな書き方に変わっています。
- 公開APIとしては
- パフォーマンス:
event_log_level呼び出し毎にlog_levelsハッシュのdupとfreezeが入るため、わずかにオーバーヘッドは増えます。- ただし、この API は「起動時設定や初期化フェーズで数回呼ぶ」程度のことが多く、通常のリクエストパス上で高頻度に呼ぶことは想定されていないため、実務上は問題にならないケースがほとんどです。
- マルチスレッド/Ractor:
- 複数スレッドや Ractor から
event_log_levelを呼び出しても、共有の frozen ハッシュを直接書き換えないため、競合や Ractor 不正アクセスエラーを避けられます。 - 既に Ractor を使っているアプリケーションでは、この変更により「イベントログレベル設定」に起因するエラーが起きにくくなります。
- 複数スレッドや Ractor から
- 参考情報 (あれば)
- Ractor セーフにする際の典型パターン:
- 共有データ構造は
freezeして不変にする。 - 更新が必要な場合は「
dup→ 変更 →freeze→ 参照を差し替え」のパターンを取る。
- 共有データ構造は
- Rails / ActiveSupport における類似の対応:
- キャッシュ設定やログ設定など、グローバルなコンフィグを扱う箇所では、同様の「イミュータブルなハッシュ/配列を差し替える」手法が徐々に導入されています。
- 本 PR は CHANGELOG 更新は行われておらず、「外部API仕様は変えずに内部を Ractor フレンドリーにした小規模バグ修正/改善」という位置づけです。
#57978 Make Normalization.normalizes ractor safe
マージ日: 2026/7/3 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveModel::Normalization.normalizesが Ractor(マルチスレッド並列実行)環境でも安全に利用できるよう、正規化属性を追加する内部実装を「複製してから追記して凍結する」形に変更した PR です。挙動自体は変えずに、内部データ構造の扱い方だけをスレッド/Ractor安全寄りにしています。
- 変更内容の詳細
何をやったか
ActiveModel::Attributes::Normalization モジュール内で、normalizes 呼び出し時に使う内部配列(もしくは類似のコレクション)の扱いを以下のように変更しています。
- これまで: 既存の配列に対してそのまま
<<などで追記(破壊的変更) - 変更後:
- 既存の配列を
dupで複製 - 複製した配列に追記 (append)
- その配列を
freezeして不変オブジェクトとして保持
- 既存の配列を
これにより、ある Ractor で参照中の配列が、別の Ractor から破壊的に変更されることを防いでいます。
PR本文で書かれていた「Dup, append and freeze.」がまさにこのパターンです。
ざっくりしたイメージコード
実際のコードとは多少違う可能性がありますが、意図としては以下のような変化です:
# 変更前のイメージ
def normalizes(name, **options, &block)
@normalizations ||= []
@normalizations << build_normalization(name, options, &block)
end
# 変更後のイメージ
def normalizes(name, **options, &block)
@normalizations ||= [].freeze
new_normalizations = @normalizations.dup
new_normalizations << build_normalization(name, options, &block)
@normalizations = new_normalizations.freeze
endポイント:
- 共有されるインスタンス変数(例:
@normalizations)自体は置き換え(再代入)で更新し、
「すでに公開済みのオブジェクト」は一切破壊的に書き換えない。 - 新しく作る配列は最終的に
freezeされるため、Ractor 間で共有しても書き換え不可能。
テストの追加
activemodel/test/cases/attributes/normalization_test.rbに 7 行追加されています。- 目的は主に:
normalizes呼び出し後も内部状態が適切であること- 複数回
normalizesを呼んだときに前の定義が壊れないこと - 凍結配列を前提にした挙動でも問題ないこと
などを確認するものと考えられます(Ractor そのものを直接テストしているかはコード次第ですが、「破壊的変更しない実装」になっていることを保証するテストが入っているはずです)。
- 影響範囲・注意点
影響範囲
- 対象:
ActiveModel::Attributes::Normalizationを利用しているコード、具体的には:
class User
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Attributes::Normalization
attribute :email, :string
normalizes :email, with: ->(value) { value.to_s.strip.downcase }
endのように normalizes を使っている箇所。
- 表面的な挙動(値がどう正規化されるか)は変わりません。
- 主に内部の属性正規化情報テーブルの更新手順のみが、破壊的更新から「イミュータブルな再割り当て」方式に切り替わっています。
Ractor 安全性について
- この PR によって:
- 「normalized 属性を追加する処理」自体が Ractor セーフに近づいた
(既存オブジェクトの破壊的変更を避けるため)
- 「normalized 属性を追加する処理」自体が Ractor セーフに近づいた
- ただし PR 中でも明記されている通り:
Note, attribute registration that this relies on is still not ractor safe, its slightly more involved. Someone will do it next week probably.
- つまり:
- 「属性(
attribute)自体の登録」に関わるコードは、まだ Ractor セーフではない。 normalizes周辺だけが先行して Ractor セーフなスタイルにリファクタされている状態。
- 「属性(
- 結論として:
- この PR だけで ActiveModel 属性周り全体が完全に Ractor セーフになるわけではない
- とはいえ、将来的な完全 Ractor 対応に向けた一部ステップであり、
少なくともnormalizesによる内部配列の共有・書き換えに起因する Ractor 競合バグは出にくくなります。
パフォーマンス・互換性
dup+freezeにより:normalizes呼び出しごとに配列のコピーが発生するため、属性数・normalizes呼び出し回数が非常に多い場合には、わずかなオーバーヘッド増加があり得ます。- ただし
normalizes自体は通常アプリケーション起動時にクラス定義の中で呼ぶことが多く、リクエストごとに大量に呼ばれるものではないため、実務上は問題になりにくいと考えられます。
- API 互換性:
- 公開 API (
normalizesの引数や動作) は変わっていないため、既存コードは基本的にそのまま動作します。
- 公開 API (
- 参考情報 (あれば)
Ruby Ractor とイミュータブルデータ構造:
- Ractor 間で共有できるのは、基本的に「凍結されたオブジェクト」や Ractor セーフな型だけであり、可変オブジェクトを共有し破壊的変更するとエラーや未定義動作の原因になります。
- この PR の
dup→append→freezeパターンは、Ractor 対応で頻出する「内部状態をイミュータブルにする」ための典型的リファクタです。
今後の想定:
- PR 本文にある通り、属性登録周りの Ractor 対応が別 PR で行われる予定で、
それと組み合わさることで ActiveModel 属性システム全体がより Ractor フレンドリーになっていきます。
- PR 本文にある通り、属性登録周りの Ractor 対応が別 PR で行われる予定で、
#57979 Use latest ruby for GHA integration tests
マージ日: 2026/7/3 | 作成者: @skipkayhil
概要 (1-2文で)
このPRは、GitHub Actions 上で動作する Rails の各種インテグレーションテスト(devcontainer、rails new docker、rail_inspector)で使用する Ruby のバージョンを「最新の Ruby」に切り替える変更です。テストフロー自体は変えず、実行環境としての Ruby の指定のみを更新しています。変更内容の詳細(あればサンプルコードも含めて)
対象ファイルはいずれも GitHub Actions のワークフローファイルです。
.github/workflows/devcontainer-smoke-test.yml.github/workflows/rail_inspector.yml.github/workflows/rails-new-docker.yml
各ワークフロー内で、Ruby のバージョンを指定している箇所が 1 行ずつ書き換えられています。具体的には、従来は固定バージョン(例: 3.3, 3.2 など)を指定していたものを、「最新の安定版 Ruby」を指す指定に変更した形が想定されます。
典型的には、以下のような変更になっている可能性が高いです(参考イメージ):
# 変更前(例)
- uses: ruby/setup-ruby@v1
with:
ruby-version: '3.3'
# 変更後(例)
- uses: ruby/setup-ruby@v1
with:
ruby-version: 'ruby' # または '3.4' など、その時点の最新もしくは、matrix で定義している ruby: [ '3.3' ] を最新バージョンに上げる、という形の可能性もあります。
いずれにせよ、各ワークフローの「使う Ruby のバージョンを 1 行だけ差し替えている」以外の変更は行われていません。
- 影響範囲・注意点
影響範囲
- GitHub Actions 上で実行される以下のワークフローの Ruby 実行環境:
- devcontainer-smoke-test
- rail_inspector
- rails-new-docker
- これらが「常に最新の Ruby で Rails をテストする」状態になるため、Rails 本体が最新 Ruby に対して継続的に検証されるようになります。
- GitHub Actions 上で実行される以下のワークフローの Ruby 実行環境:
メリット
- Ruby の新バージョンがリリースされた際に、自動的にそのバージョンで検証が走るようになり、Ruby 側の変更による非互換や警告の早期検知が可能になります。
- Rails の「最新 Ruby サポート状況」が CI 上で常にカバーされるため、メンテナンス性の向上に寄与します。
注意点 / リスク
- Ruby の新バージョンで非互換やバグが出た場合、Ruby リリース直後に CI が急に壊れる可能性があります(=Rails 側が即座には対応できていないケース)。
- もし「特定の Ruby バージョンでの動作保証」を厳密にテストしたい場合は、別のジョブやマトリクスで LTS 的なバージョン(例:
3.1,3.2など)を並行して運用する必要があります。 - 新しい Ruby でのみ発生する警告(deprecation など)が増え、ログがノイジーになる可能性があります。
- 参考情報 (あれば)
ruby/setup-rubyアクションのバージョン指定方法(ruby-version: rubyで「最新安定版」を指す等)
https://github.com/ruby/setup-ruby#usage- Rails がサポートしている Ruby バージョンポリシー(ガイドライン)
https://guides.rubyonrails.org/ (「Ruby on Rails Guides」内の “Getting Started” や “Maintenance Policy” 関連) - Rails リポジトリ内の GitHub Actions 設定全体
https://github.com/rails/rails/tree/main/.github/workflows
#57974 Require useragent when loading ActionController::AllowBrowser
マージ日: 2026/7/3 | 作成者: @andrewn617
- 概要 (1-2文で)
ActionController::AllowBrowserを読み込むタイミングでuseragentをrequireするように変更し、これまで「最初のリクエスト時」に行われていたuseragentの読み込みを事前に行えるようにした PR です。主に eager load 環境や Ractor 対応時の初回読み込みタイミングの改善が目的です。
- 変更内容の詳細
※PR本文からはファイル単位の情報しかありませんが、内容的には以下のような極小変更です(+2/-2 行)。
対象ファイル:
actionpack/lib/action_controller/metal/allow_browser.rb
やっていることは要約すると:
- これまで:
AllowBrowserが使われるタイミング、あるいは最初のリクエスト処理のどこかでuseragentをrequireしていた(=eager load していても、実際の最初のリクエスト時に初回 require が走る)。
- これから:
ActionController::AllowBrowserクラス/モジュールがロードされる段階でrequire "useragent"を実行するように変更。
イメージとしては、以下のような差分になっている可能性が高いです(擬似コード):
# 変更前 (例)
module ActionController
class AllowBrowser
# どこかのメソッド内や遅延ロード処理の中で:
# require "useragent"
end
end
# 変更後 (例)
require "useragent" # ← ファイルロード時に実行
module ActionController
class AllowBrowser
# ここでは useragent はすでにロード済み
end
endこれにより、AllowBrowser を使う環境では、そのファイルがロードされた時点で useragent もロードされます。
- 影響範囲・注意点
- 影響範囲
ActionController::AllowBrowserを使用しているアプリ・ライブラリ。- 特に eager load を有効にしている本番環境や、Ractor を利用するような並行実行環境での挙動に影響します。
- 具体的な挙動の変化
- 以前: 最初のリクエスト処理中に
useragentのrequireが発生し、そのタイミングでロードコストやスレッド/Ractor との競合リスクがあった。 - 以後: Rails が
ActionController::AllowBrowserをロードした時点でuseragentが確定的にロードされるため、最初のリクエスト時のオーバーヘッドが減り、Ractor 的にも安全になりやすい。
- 以前: 最初のリクエスト処理中に
- パフォーマンス・メモリ
AllowBrowserを利用している環境では、「どうせいつかはuseragentを読む」ので、初回リクエスト前に読むか後に読むかの違いであり、全体的なメモリ使用増はほぼ変わりません。- ただし、
AllowBrowserを定義だけ読み込んで実際には一切使わないような特殊な構成では、以前よりも早いタイミングでuseragentがロードされる可能性があります。
- 互換性
useragent自体の API 変更はなく、あくまで require のタイミング変更のため、一般的なアプリでは後方互換性上の問題はほぼありません。useragentgem を明示的に Gemfile に入れていない状況で(たまたま別の依存関係から入っていたなど)、ロード順に依存していたような非常にレアなケースでは、LoadErrorの発生タイミングが変わる可能性があります。
- 参考情報 (あれば)
- Ractor との関連:
- Ractor 環境では、スレッドセーフでない初回 require や定数定義が、並行実行と絡んで問題を起こし得るため、「最初のリクエスト中に require される」ようなパターンは避けたい。
- 本PRにより、
AllowBrowserのロードタイミングでuseragentが確定的に読み込まれるため、Ractor 開始前に eager load を済ませる設計と相性が良くなります。
- 実運用での確認ポイント:
AllowBrowserを利用しているアプリでは、本番起動時(Rails boot 時)にuseragentが読み込まれることを前提に、必要ならば Gemfile にgem "useragent"が明示的に含まれているかを確認しておくと安全です。
#57977 Fix typographical errors from CHANGELOG files[ci skip]
マージ日: 2026/7/3 | 作成者: @nisusam
概要 (1-2文で)
このPRは、Rails の CHANGELOG ファイル内に含まれていた英単語のタイポ(綴り誤り)を修正するものです。コードや挙動は一切変更せず、ドキュメントの品質向上のみを目的としています。変更内容の詳細(あればサンプルコードも含めて)
対象ファイル
actionpack/CHANGELOG.mdactionview/CHANGELOG.mdrailties/CHANGELOG.md
変更内容
- 各 CHANGELOG に記載されている文章中の typographical error(スペルミスや細かな表記揺れ)を、それぞれ正しい綴りに修正しています。
- 追加行数 3 / 削除行数 3 から分かる通り、修正は行単位で 1 箇所ずつの軽微な置き換えであり、文面の意味や履歴内容自体は変えていません。
- 例としては以下のような変更が想定されます(イメージであり、実際の差分そのものではありません):diff
- * Fixes a behvaior where ... + * Fixes a behavior where ... - * Depreacted option ... + * Deprecated option ...
Ruby コード・設定ファイル・テストコードなど、実行時に読み込まれる部分への変更は行われていません。
- 影響範囲・注意点
影響範囲
- 影響するのはドキュメント(CHANGELOG)のみであり、Rails アプリケーションの実行時挙動、ビルド、テスト、API は一切影響を受けません。
- 既存のバージョン互換性・マイグレーション・設定などにも影響なし。
注意点
- CI スキップ指定(
[ci skip])がタイトルに入っており、テストが走っていない点はログ上の事実としてありますが、そもそも実行可能コードに触れていないため問題にはなりません。 - CHANGELOG の参照時に、以前のタイポに依存したリンクや検索(例: タイポした単語で全文検索していた)を行っていた場合は、今後は正しい綴りで検索する必要があります。
- CI スキップ指定(
- 参考情報 (あれば)
- PR タイトルから分かる通り、この変更は「CHANGELOG ファイル中の誤字修正」に限定されており、Rails の機能追加やバグ修正とは無関係です。
- Rails では、コードと直接関係ないドキュメント変更についても PR ベースで管理し、CI をスキップして軽量にマージする運用が行われています。
#57959 Fix polymorphic belongs_to primary key for sharded targets
マージ日: 2026/7/3 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
- シャーディング対応で「複合主キーを持つ has_many」と「単一外部キーを持つ polymorphic belongs_to」を組み合わせたときに、
AutosaveAssociation内で主キーと外部キーの対応付けがずれてActiveModel::MissingAttributeErrorが起きる回 regress を修正した PRです。 BelongsToReflection#association_primary_keyが、belongs_to 側が単一外部キーしか持たない場合でも、シャーディング用の複合主キー(query_constraints)をそのまま返してしまっていた挙動を是正しています。
- 変更内容の詳細
問題の背景
- シャーディング対象モデルは、典型的に複合主キーのような「問い合わせ上の主キー」を持ちます:
- 例:
[:shard_id, :id]をquery_constraintsとして扱う
- 例:
- 一方で、対応する
belongs_to側は単一の外部キーしか持たないケースがあります:- 例:
belongs_to :shipment, foreign_key: :shipment_id, polymorphic: trueのような形
- 例:
- Rails 7.2 系での refactor により、
AutosaveAssociation内で使われていたcompute_primary_keyが削除され、代わりにBelongsToReflection#association_primary_keyの戻り値がそのまま使われるようになりました(該当コミット)。 AutosaveAssociationは「関連先の primary_key」と「自分の foreign_key」をArray#zipして対応付ける実装になっているため、- primary_key 側が
[:shard_id, :id](長さ2) - foreign_key 側が
[:shipment_id](長さ1) のように長さが合わないと、 shard_idの対応 foreign_key が存在せずnil- それを読み出そうとしたときに
ActiveModel::MissingAttributeErrorが発生してしまう、という不具合が生じていました。
- primary_key 側が
原因になっていた association_primary_key の挙動
BelongsToReflection#association_primary_key は、関連の解決のために使う「関連先の主キー的なカラム」を求めるメソッドです。今回問題になったのは2つの分岐です:
inverse_ofがある場合の分岐:inverse.options[:primary_key]をそのまま採用していた- 通常の「単一カラムの custom primary key(例:
:uuid)」なら正しいが、 - シャーディング対応で
has_many側にprimary_key: [:shard_id, :id]などの「複合 primary_key もどき」が指定されている場合、belongs_to側は単一外部キーでも composite をそのまま採用してしまう
- 上記以外のフォールスルー分岐:
- ターゲットモデルが
has_query_constraints?の場合、query_constraints(複合主キー的な配列)を展開して返す - その際、
belongs_toが「明示的に単一foreign_keyを指定している」ことを考慮していなかった
- ターゲットモデルが
その結果、belongs_to 側が単一 foreign_key にもかかわらず、関連先の「複合 primary key」配列が返ってきてしまい、AutosaveAssociation 側の zip で長さが合わずに例外を起こしていました。
修正内容のポイント
今回のコミットは、過去に compute_primary_key で行っていた「2つのガード」を association_primary_key に復元する形で問題を解消しています。
inverse_ofブランチのガード復活- 目的:
inverse.options[:primary_key]が「複合キー由来」のときに採用しないようにする - 実装の考え方:
has_many側が「複合 foreign_key」を宣言しているとき、その情報は:query_constraintsにも反映される- したがって、
inverse.optionsに:query_constraintsが存在する場合、そこからは「複合キー前提」であることが分かる
- 修正:
association_primary_key内でinverse.options[:query_constraints]の有無を確認し、- ある場合: composite な inverse primary_key(≒query_constraints 由来)として扱うため、
inverse.options[:primary_key]をそのまま採用しない - ない場合: 本当に単一の custom primary_key(例:
uuid)を指定していると判断し、inverse.options[:primary_key]を採用する
- ある場合: composite な inverse primary_key(≒query_constraints 由来)として扱うため、
- 目的:
フォールスルーブランチのガード復活
- 目的:
belongs_to側が明示的に単一foreign_keyを指定しているときには、関連先も単一 primary_key に解決するようにする - 修正:
has_query_constraints?なターゲットであっても、belongs_toで「単一のforeign_keyが明示されている」場合は、query_constraintsを複合キーとしてそのまま返さず、- 単一の primary_key(
primary_keyないし id)に解決するようなロジックを追加
- これにより、
zipの対象が- primary_key 側:
[:id](または同等) - foreign_key 側:
[:shipment_id]となり、要素数が一致して安定して動作する
- primary_key 側:
- 目的:
テストの追加・変更
activerecord/test/cases/associations/belongs_to_associations_test.rb- polymorphic belongs_to + シャーディング対象モデル + inverse_of が絡むケースをカバーするテストを追加
activerecord/test/cases/autosave_association_test.rbAutosaveAssociation経由で保存されるシナリオ(accepts_nested_attributes_for等)の中で、例外が発生しないことを確認する回帰テストを追加
activerecord/test/models/adjustment.rb,shipment.rb,schema.rb- 上記テスト用のモデル・スキーマ定義を拡張
- シャーディング風の
query_constraints/複合 primary_key もどき + polymorphic belongs_to の組み合わせを表現するためのフィクスチャモデルを追加
- 影響範囲・注意点
- 影響を受ける可能性が高いケース:
- シャーディングやマルチテナント構成で
- モデルに
query_constraints(例:[:shard_id, :id]等)を設定している - そのモデルに対して polymorphic な
belongs_toを張っている - inverse 側に
has_many+primary_key: [:shard_id, :id]のような宣言をしている - かつ
AutosaveAssociation経由(accepts_nested_attributes_forなど)で保存している
- モデルに
- シャーディングやマルチテナント構成で
- そのようなアプリでは、Rails のバージョン更新後に以下のような症状が出ていた場合、この PR によって解消される可能性があります:
ActiveModel::MissingAttributeErrorがautosaveのタイミングで突然出る- メッセージの原因が「関連先モデルの
shard_idなど、複合キーの一部が nil とみなされている」ように見える
- 互換性の観点:
- 通常の(非シャーディング・非複合キー)
belongs_to / has_manyには影響しない設計になっています。 - 単一カラムの custom primary_key(例:
primary_key: :uuid)を使っている場合も、inverse.options[:query_constraints]が無い限りはこれまでどおりinverse.options[:primary_key]が採用されるため、挙動は維持されます。
- 通常の(非シャーディング・非複合キー)
- 注意点:
- アプリ側で「belongs_to は単一外部キーだが、関連先は複合 query_constraints を前提にしている」という、かなりトリッキーなカスタマイズをしていた場合、今回の「単一 foreign_key 優先の解決ロジック」により挙動が変わる可能性があります。
- 特に、独自に
query_constraintsをオーバーライドしているような高度なシャーディング実装を行っている場合は、この PR を含む Rails へのアップデート時に関連の保存・ロードの挙動をテストで確認することを推奨します。
- 参考情報 (あれば)
- 回帰のきっかけとなったコミット:
compute_primary_keyがAutosaveAssociationから削除された変更- 7b429b51183566dba3286645323eaf48a88220b0
- 関連クラス・メソッド:
ActiveRecord::Reflection::BelongsToReflection#association_primary_keyActiveRecord::AutosaveAssociationActiveRecord::Reflection#has_query_constraints?とquery_constraints
- 実際のコードを確認したい場合は、
activerecord/lib/active_record/reflection.rbのassociation_primary_key実装と、autosave_association_test.rbに追加されたテストケースを読むと、
「inverse に複合 primary_key がある / belongs_to に単一 foreign_key がある」という具体的な差分が把握しやすくなります。
#57942 Return all parameters from deconstruct_keys when no keys are requested
マージ日: 2026/7/3 | 作成者: @55728
- 概要 (1-2文で)
ActionController::Parametersに対するパターンマッチで、{ key:, **rest }のように 特定キーを指定せず rest バインディングだけを使った場合にマッチしなかった問題を修正し、Hash#deconstruct_keysと同じ挙動(キー指定なしなら全パラメータを返す)になるようにした変更です。
- 変更内容の詳細
これまでの問題点
Ruby 3 以降のパターンマッチ(case ... in)は、ハッシュライクなオブジェクトに対して deconstruct_keys を呼び出してパターンにマッチさせます。
ActionController::Parameters はこのインターフェイスを実装していますが、
パターン側で特定のキーをひとつも要求していない場合(in { **rest } や、キーは指定するけど rest のみを実質使うようなケース)に、deconstruct_keys が 空ハッシュ {} を返してしまう挙動になっており、結果としてマッチ失敗になっていました。
PR で説明されている再現コード:
params = ActionController::Parameters.new(name: "Bob", age: 22).permit!
case params
in { name:, **rest }
# Before: ここに来ない(マッチしない)
# After: name => "Bob", rest => { age: 22 }
end原因は、deconstruct_keys(nil)(=「特定のキーは要求していない」という意味)に対して、ActionController::Parameters が空ハッシュを返していたためです。
Ruby の Hash#deconstruct_keys は、引数が nil のとき「全キー・全値」を返す仕様なので、ActionController::Parameters だけ挙動が異なり、パターンマッチが期待通り動かない不整合がありました。
今回の修正内容
actionpack/lib/action_controller/metal/strong_parameters.rb の deconstruct_keys 実装が修正されました:
- 引数
keysがnilの場合に
→ 全パラメータを返すように変更 - これにより
Hash#deconstruct_keysと同じ挙動になります。
テスト (actionpack/test/controller/parameters/equality_test.rb) では、おそらく次のような性質が確認されています(実際のコードは要約):
ActionController::Parameters.new(name: "Bob", age: 22).permit!に対し、case params; in { name:, **rest }; ...; endがマッチすることnameが"Bob"になることrestが{ age: 22 }相当になること
動作イメージ(サンプル)
変更後の振る舞い:
params = ActionController::Parameters.new(
name: "Bob",
age: 22,
admin: true,
).permit!
case params
in { name:, **rest }
# deconstruct_keys(nil) が全パラメータを返すのでマッチする
# name => "Bob"
# rest => { "age" => 22, "admin" => true } あるいは Parameters 相当
end
case params
in { **rest }
# これもマッチする(rest にすべてが入る)
end- 影響範囲・注意点
- 影響を受けるコード
ActionController::Parametersを直接case ... inでマッチさせているコード- 特に
{ key:, **rest }や{ **rest }のような rest バインディングを使うパターン を利用している場合に挙動が変わります。
- 以前との違い
- 以前は「マッチしない」ケースが、「マッチして全パラメータが渡される」ようになります。
- つまり、以前は
else側に流れていた分岐が、in { ... }側に入るようになる可能性があります。
- 互換性
- Ruby 標準の
Hashと同じdeconstruct_keys挙動になるため、設計上はむしろ一貫性が向上した変更です。 - ただし、「
ActionController::Parametersではこのパターンはマッチしないはず」という前提で書かれていた奇妙なワークアラウンドがあれば、その前提は崩れます。
- Ruby 標準の
- Strong Parameters / permit の扱い
- 例では
permit!済みのオブジェクトになっており、通常は 許可されたパラメータの集合がdeconstruct_keysの対象です。 - 未許可のパラメータを含む場合、もともとの
ActionController::Parametersの振る舞い(フィルタリングの有無など)に依存するため、permit/requireの位置づけは引き続き意識する必要があります。
- 例では
- 参考情報 (あれば)
- Ruby 本体の仕様:
Hash#deconstruct_keys- 引数が
nilのとき「ハッシュ自身を返す」という仕様で、今回の変更はこれに追従しています。
- 引数が
- Rails側 PR:
Return all parameters from deconstruct_keys when no keys are requested(Rails PR #57942)ActionController::Parametersを Ruby のパターンマッチ機構とより自然に連携させるための小さな互換性改善です。
#57965 8 1 stable cp 1dbad939f8
マージ日: 2026/7/3 | 作成者: @p8
- 概要 (1-2文で)
このPRは、Rails 8.1 ステーブルブランチに対して「product_reviews」という新しいガイドを大きく追加し、既存の「wishlists」ガイドをわずかに調整する、ドキュメント中心の変更です。コード本体には手が入っておらず、開発者向けの学習・サンプルアプリケーション的なガイドが充実した形になります。
- 変更内容の詳細
2-1. guides/source/product_reviews.md の新規追加 (+1350行)
1350行規模の大きなガイドが新規に追加されています。内容からすると、以下のような「Rails 8.1 を用いた Product Reviews(商品レビュー)機能」のチュートリアル/サンプルアプリ的ガイドである可能性が高いです(Wishlists ガイドと同系統のシリーズであることが多いです)。
想定される構成・内容:
アプリケーション概要
- ECサイトや商品カタログに対して「レビュー」や「評価(rating)」を投稿できる機能を題材にしたガイド
- Rails 8.1 の標準的なスタック(Zeitwerk, importmap / jsbundling, turbo, stimulus など)を前提とした作り
モデル設計例
Product/User/Reviewなどの関連付け典型的には下記のような関連を解説していると考えられます:
rubyclass Product < ApplicationRecord has_many :reviews, dependent: :destroy end class Review < ApplicationRecord belongs_to :product belongs_to :user validates :rating, presence: true, inclusion: { in: 1..5 } validates :body, presence: true end class User < ApplicationRecord has_many :reviews, dependent: :nullify end
マイグレーションとスキーマ設計
reviewsテーブルのカラム例:product_id/user_id(外部キー)rating(整数)body(テキスト)created_at,updated_at
- インデックスや外部キー制約の付け方
コントローラとルーティング
resources :products do resources :reviews, only: %i[create update destroy] endのようなネストされたリソースルーティングReviewsControllerでのcreate,update,destroyアクション実装例rubyclass ReviewsController < ApplicationController before_action :set_product def create @review = @product.reviews.build(review_params.merge(user: current_user)) if @review.save redirect_to @product, notice: "Review was successfully created." else render "products/show", status: :unprocessable_entity end end private def set_product @product = Product.find(params[:product_id]) end def review_params params.require(:review).permit(:rating, :body) end end
ビュー・UI 実装の解説
- 商品詳細ページにレビュー一覧と投稿フォームを組み込む例
form_with model: [@product, @product.reviews.build]などネスト資源のフォーム作成- rating をセレクトボックスやラジオボタンで扱う例
バリデーション・エラーハンドリング
- バリデーションエラー時に同じページにレンダリングしなおすパターン
- Turbo 対応時のエラーレスポンスやステータスコードの選び方 (422 Unprocessable Entity)
認可・認証関連の話題(ある程度触れられる可能性)
- ログイン済みユーザーのみレビュー可能
- 自分のレビューしか編集・削除できない、といったポリシー例
テストのサンプル
- Minitest / RSpec いずれか、または Rails 標準のテスティングガイドに沿った形でのモデル・コントローラテスト例
- ルーティング・関連付け・バリデーションなどのテスト
要点として、このガイドは:
- Rails 8.1 で「ある程度現実的なドメイン(product reviews)」を使って
- モデル・ビュー・コントローラ・ルーティング・バリデーション・テスト
- そして場合によっては Turbo/Stimulus まで
を一通りつなげて見せる「実践的なチュートリアル」のような位置づけと思われます。
2-2. guides/source/wishlists.md の変更 (+1 / -8)
wishlists.md に対してはごく小さい修正のみです。
典型的にありえる内容は:
- Product Reviews ガイドとの整合性を取るためのリンク追加・章番号の調整
- 「次に読むガイド」として
product_reviewsへのリンクを追加 - 古いバージョン固有の記述や typo の修正・削除
行数的には 8 行削除 / 1 行追加なので、文章の差し替え・段落の統合・見出しレベルの変更など、小さな編集にとどまっています。機能仕様や API 解説のような本質的変更ではないと考えて問題ありません。
- 影響範囲・注意点
ランタイム挙動への影響:
- 変更はすべて
guides/source配下であり、Rails 本体のコードは一切変更されていません。 - そのため、アプリケーションの挙動・API・互換性への影響はありません。
- 変更はすべて
対象バージョン:
- タイトルから、「8 1 stable」ブランチ向けの cherry-pick と思われます。
- Rails 8.1 系の公式ガイドに Product Reviews チュートリアルが追加された、という位置づけです。
ドキュメント利用者への影響:
- Rails 初学者や、サンプルアプリを通じて 8.1 の構成に慣れたい開発者に有用です。
- Wishlists ガイドを既に読んでいる場合、その続き・姉妹編のような形で Product Reviews ガイドを読むことになる可能性があります。
- 実案件で reviews 機能を実装する際のモデル・コントローラ・ビュー構成の「リファレンス例」としても利用できます。
注意点としては以下程度です:
- ガイドはあくまでサンプルであり、そのまま本番環境に流用する場合は認可・バリデーション・攻撃耐性(スパムや大量投稿など)を自分の要件に合わせて補強する必要があります。
- 8.1 以外のバージョン(特に 7 系以前)では、ガイド中で前提とされている API やフロントエンド構成が異なる場合があります。
- 参考情報 (あれば)
Rails Guides 一覧(英語版):
https://guides.rubyonrails.org/類似ガイド(Wishlists など)は、公式ガイドの「Getting Started / Tutorial」系セクションにまとまっていることが多く、Product Reviews ガイドも同様のカテゴリに追加されている可能性があります。
Product Reviews ガイド内で使われているであろう要素(参考ドキュメント):
- Active Record Associations: https://guides.rubyonrails.org/association_basics.html
- Active Record Validations: https://guides.rubyonrails.org/active_record_validations.html
- Action Pack / Routing: https://guides.rubyonrails.org/routing.html
- Turbo / Hotwire (8.1 の場合、標準構成に含まれる可能性あり)
#57964 Fix casing in product reviews guide
マージ日: 2026/7/3 | 作成者: @p8
- 概要 (1-2文で)
このPRは、Rails ガイド「product_reviews.md」内の英単語の大文字・小文字表記(casing)を修正し、用語やコードの表記ゆれを整えるドキュメント修正です。機能や挙動の変更はなく、アプリケーションコードやテストには影響しません。
- 変更内容の詳細
- 対象ファイルは
guides/source/product_reviews.mdのみで、行単位で「+21 / -21」となっており、文章やコード例の一部表記が置き換えられています。 - タイトルの通り "casing" の修正が中心で、主に以下のような変更が想定されます(実際の差分から一般的に起こるパターンを整理):
- 英語の表記ゆれの統一
- 例: "Product reviews" → "Product Reviews"(ガイドタイトルや見出しとしての表記統一)
- 例: "api" → "API"、"json" → "JSON" などの一般的な頭字語の大文字化
- 例: "Postgresql" → "PostgreSQL" など、固有名詞の正しい大文字小文字への修正
- Rails 用語の表記統一
- 例: "Active record" → "Active Record"
- 例: "Action cable" → "Action Cable"
- コード例・識別子の区別を明確にするための修正
- 文中のクラス名・モジュール名を、実際の Ruby/Rails の定義と揃えた可能性があります
- 例:
productreview→ProductReview - 例:
productreviewscontroller→ProductReviewsController
- 例:
- 文章とコードの混在箇所で、コードは小文字スネークケース、クラス名はパスカルケースといった区別を明確化
- 文中のクラス名・モジュール名を、実際の Ruby/Rails の定義と揃えた可能性があります
- 英語の表記ゆれの統一
※差分から見ると行数は同じであるため、段落削除や追加ではなく、同じ行の中での文字置換(大文字小文字の変更)が中心です。
- 影響範囲・注意点
- ランタイム挙動への影響:
- ガイド文書のみの変更であり、Rails 本体のコード・API・挙動・テストには一切影響しません。
- 既存アプリケーションへの影響:
- 既存の Rails アプリやガイドに従って実装しているコードが壊れることはありません。
- 読者への影響 / 注意点:
- ガイドを参照している場合、用語やクラス名の表記が Rails 本体の実際の命名規則(クラス:
CamelCase、ファイル・テーブル:snake_caseなど)とより一致するように改善されています。 - もし過去に古いガイドの表記に合わせてクラス名などを誤って命名していた場合は、今後このガイドを参照するときには、改めて Rails の一般的な命名規則(例:
ProductReview/product_reviews/product_reviews_controller.rb)に合わせることを推奨します。
- ガイドを参照している場合、用語やクラス名の表記が Rails 本体の実際の命名規則(クラス:
- 参考情報 (あれば)
- この PR には CHANGELOG への追記はなく、Rails 的には「ドキュメントの軽微な修正」と位置づけられています。
- Rails ガイドと実際の API/クラス名を揃えることは、チュートリアルどおりに書いたのにエラーになる、といった学習者の混乱を防ぐ効果があります。
- Rails の命名規則の詳細は以下を参照するとよいです(英語):
- Rails Guides – Naming Conventions: https://guides.rubyonrails.org/active_record_basics.html#naming-conventions
#57244 [RF-DOCS][ci-skip] Add Product Reviews Tutorial
マージ日: 2026/7/3 | 作成者: @excid3
- 概要 (1-2文で)
このPRは、Rails公式ガイドの「ECサイトチュートリアル」の続きとして「商品レビュー(Product Reviews)」機能を実装する新しいチュートリアルを追加するドキュメント変更です。ユーザーによる星評価付きレビュー、画像アップロード、レビューに基づく商品の平均評価更新、フィルタリング・集計表示、管理画面でのレビュー編集などを一通りカバーしています。
- 変更内容の詳細
※コードベース本体ではなく「ガイド(Documentation)」のみの変更です。新規に product_reviews.md が追加され、既存の wishlists.md がわずかに修正されています。
2-1. 新チュートリアル: guides/source/product_reviews.md
このチュートリアルは、既存の「ECストア」チュートリアルに続けて「商品レビュー機能」を実装する流れを詳細に説明しています。主な要素は以下です。
モデル構成と関連付け
典型的には次のようなモデル構造を前提として解説されます:
Product- 多数の
Reviewを持つ - 平均評価(例:
ratingやaverage_ratingカラム)を持ち、レビュー投稿・更新のたびに再計算
- 多数の
Reviewbelongs_to :productbelongs_to :user(レビュアーを紐づけるケースが多い)- 属性例:
rating(1〜5の整数)body(本文)- 画像添付用の
has_many_attached :imagesなど (Active Storage を使用した例が多い)
サンプルイメージ:
class Product < ApplicationRecord
has_many :reviews, dependent: :destroy
def update_rating!
update!(
rating: reviews.average(:rating).to_f.round(1) # 小数1桁など
)
end
end
class Review < ApplicationRecord
belongs_to :product
belongs_to :user
has_many_attached :images
validates :rating, presence: true, inclusion: { in: 1..5 }
after_commit :update_product_rating
private
def update_product_rating
product.update_rating!
end
endコントローラとルーティング
レビューは、商品にネストしたリソースとして扱う構成が説明されます。
# config/routes.rb
resources :products do
resources :reviews, only: [:create, :edit, :update, :destroy]
endReviewsController では、以下のような操作をカバーします。
- レビュー作成 (
create) - レビュー編集・更新 (
edit/update) - レビュー削除 (
destroy)(場合によっては管理者のみ)
class ReviewsController < ApplicationController
before_action :set_product
before_action :set_review, only: [:edit, :update, :destroy]
before_action :authenticate_user!
def create
@review = @product.reviews.new(review_params.merge(user: current_user))
if @review.save
redirect_to @product, notice: "レビューを投稿しました。"
else
render "products/show", status: :unprocessable_entity
end
end
# ...
private
def set_product
@product = Product.find(params[:product_id])
end
def set_review
@review = @product.reviews.find(params[:id])
end
def review_params
params.require(:review).permit(:rating, :body, images: [])
end
end星評価入力(UI)とCSS
説明の中で特徴的なのが「星評価をラジオボタンで実装し、CSSで直感的な星アイコンにする」という点です。これにより、以下の利点があります。
- アクセシビリティを確保(スクリーンリーダーやキーボード操作に対応)
- フォーム送信時には通常の
ratingパラメータとして送られる - CSSのみで視覚的にはリッチな星評価UIを実現
フォーム例:
<%= form_with model: [@product, @review] do |f| %>
<div class="rating-input">
<% 5.downto(1) do |i| %>
<%= f.radio_button :rating, i, id: "review_rating_#{i}" %>
<%= f.label :"rating_#{i}" do %>
★
<% end %>
<% end %>
</div>
<%= f.text_area :body %>
<%= f.file_field :images, multiple: true %>
<%= f.submit "レビューを投稿" %>
<% end %>CSSで label を星形にスタイリングし、input:checked ~ label などのセレクタを使って、選択状態に応じて星の色が変わるようにする、というパターンがチュートリアルで解説されています。
商品の平均評価の自動更新
レビュー作成・更新・削除のたびに、Product 側の評価を自動で再集計する仕組みを解説しています。
- コールバック (
after_commitなど)でproduct.update_rating!を呼び出す - SQL の
AVG(rating)を利用して平均値を計算 Productモデルのratingカラムに保存しておくことで、一覧画面などで N+1 クエリを避ける
計算例:
def update_rating!
update!(
rating: reviews.average(:rating) || 0
)
endレビューの絞り込みと集計表示
商品詳細ページなどで、レビューを以下のように表示・集計する例が解説されています。
- 指定した星数のみを表示するフィルタ (例: 「星5のみ」「星4のみ」)
params[:rating]をもとに@product.reviews.where(rating: params[:rating])などで絞り込み
- 星ごとのレビュー数・割合をバーグラフで表示
- 例: 5〜1まで各星の件数をカウントし、全体に対する割合を計算
# Product モデル例
def rating_breakdown
total = reviews.count.to_f
return {} if total.zero?
(1..5).each_with_object({}) do |star, hash|
count = reviews.where(rating: star).count
hash[star] = {
count: count,
percent: ((count / total) * 100).round
}
end
endビュー側例 (擬似コード):
<% @product.rating_breakdown.sort.reverse_each do |star, data| %>
<div class="rating-row">
<span><%= star %>★</span>
<div class="bar">
<div class="bar-fill" style="width: <%= data[:percent] %>%"></div>
</div>
<span><%= data[:count] %>件 (<%= data[:percent] %>%)</span>
</div>
<% end %>画像アップロード(任意)のサポート
- Active Storage を使った画像添付例が含まれています
- 1レビューに複数画像を添付できるように
has_many_attached :imagesを使用 - ビューで
image_tagを使ってプレビュー表示する例が示されます
class Review < ApplicationRecord
has_many_attached :images
end<% @review.images.each do |image| %>
<%= image_tag image.variant(resize_to_limit: [400, 400]) %>
<% end %>管理画面でのレビュー管理
- 管理者がレビューを編集・削除できるようにするための UI / コントローラが説明されています
- 主な用途: 誤字修正、不適切表現の修正、スパム的レビューの削除など
- 実装例:
namespace :admin配下にAdmin::ReviewsControllerを作成- または既存の管理UI(例:
Admin::ProductsController)にネストして操作する
ルート例:
namespace :admin do
resources :products do
resources :reviews, only: [:index, :edit, :update, :destroy]
end
end権限管理の考慮(認証・認可)が軽く触れられている可能性があります(before_action :require_admin! など)。
2-2. guides/source/wishlists.md の微修正
wishlists.md は既存のECストアチュートリアルの別パートで、ここに対して:
- 他パート(今回追加された
product_reviews.mdなど)へのリンクの修正・追加 - あるいは構成の一貫性を保つための軽微な文章・見出しの変更
が行われています(+1/-8 行なので、ごく小さな調整に留まっています)。
- 影響範囲・注意点
影響範囲
- Rails本体のコード・APIには影響せず、公式ガイド(ドキュメント)のみの変更です。
- ただし「Rails Way」でのECサイトにおけるレビュー機能の実装パターンとして、今後このチュートリアルが事実上の「推奨例」になる可能性があります。
- 特に以下の設計方針が、Railsコミュニティにおけるデファクトな例として参照されやすくなります:
- 星評価をラジオボタン + CSS で実装するアプローチ
- モデルのコールバック(orサービスオブジェクト)で平均評価を自動更新するアプローチ
- Active Storage を利用したレビュー画像添付
注意点 / 実装上のポイント
- パフォーマンス:
- 大量レビューがある商品の平均値再計算は、
reviews.average(:rating)を毎回呼び出すと高コストになる可能性があります。- チュートリアルではシンプルな実装を重視しているため、実運用ではカウンタキャッシュ的な集計(
reviews_count,rating_sumなど)で最適化を検討するとよいです。
- チュートリアルではシンプルな実装を重視しているため、実運用ではカウンタキャッシュ的な集計(
- 大量レビューがある商品の平均値再計算は、
- 整合性:
- レビュー削除時にも平均値の更新が走るように、
after_commitなどのコールバックの種類やタイミングには注意が必要です。
- レビュー削除時にも平均値の更新が走るように、
- セキュリティ:
- 画像アップロードでは、ファイルサイズ・拡張子・Content-Typeの制限や、サムネイル生成時のImageMagick/ libvips 周りの安全性確保など、実運用では追加対策が必要になります。
- 権限/認可:
- 一般ユーザーが自分のレビューだけを編集・削除できるようにするのか、管理者のみ許可するのか、チュートリアルは1つの例に過ぎないため、自プロジェクトではポリシーを明確にする必要があります。
- パフォーマンス:
- 参考情報 (あれば)
Rails Guides (今後このチュートリアルが掲載されるであろう場所):
https://guides.rubyonrails.org/
(Product ReviewsもしくはE-Commerce関連のチュートリアルセクションに追加される想定)Active Storage ガイド(画像アップロードの詳細):
https://edgeguides.rubyonrails.org/active_storage_overview.htmlRails公式チュートリアル内の他のEC機能 (カート、ウィッシュリストなど) と合わせて読むと、フル機能のECサイト構築フローとして参考になります。
#57826 Deprecate Mime::SET, Mime::LOOKUP and Mime::EXTENSION_LOOKUP
マージ日: 2026/7/2 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Rails の MIME タイプ周りの内部レジストリを直接公開していたMime::SET,Mime::LOOKUP,Mime::EXTENSION_LOOKUPが非推奨化され、代わりに既存の公開 API(Mime.symbols,Mime[...],Mime::Type.lookup,Mime::Type.lookup_by_extensionなど)と新設のMime.extensionsを使うように誘導する変更です。
これにより、ミュータブルな内部状態への直接アクセスを抑制しつつ、これまで内部定数でしかできなかった操作も正式なパブリック API でカバーされるようになりました。
- 変更内容の詳細
非推奨になった定数
以下の 3 つの定数が非推奨になり、ActiveSupport::Deprecation::DeprecatedObjectProxy でラップされるようになりました。
Mime::SET- MIME タイプのレジストリ(シンボルなど)を保持していたセット。
Mime::LOOKUP- 文字列表現(例:
"text/html")から MIME タイプを引くためのハッシュ。
- 文字列表現(例:
Mime::EXTENSION_LOOKUP- 拡張子(例:
"html")から MIME タイプを引くためのハッシュ。
- 拡張子(例:
これらはまだ参照可能ですが、使用すると deprecation warning が出るようになっています。
推奨される置き換え API
PR 説明にある通り、用途ごとに次のように書き換えが推奨されます。
1) Mime::SET
用途: 登録済みの MIME タイプを列挙・取得したい場合
置き換え:
- 登録済みの MIME シンボル一覧が欲しいときruby
# 旧 Mime::SET # 非推奨 # 新 Mime.symbols - MIME タイプをキー(シンボルや文字列)から取得したいときruby
# 旧 type = Mime::SET[:html] # など # 新(推奨) type = Mime[:html] # または type = Mime::Type.lookup("text/html")
2) Mime::LOOKUP
用途: "text/html" のような MIME 文字列から Mime::Type を取得
置き換え:
# 旧
Mime::LOOKUP["text/html"] # 非推奨
# 新
Mime::Type.lookup("text/html")3) Mime::EXTENSION_LOOKUP
用途: ファイル拡張子 "html" から MIME タイプを取得したり、全拡張子を列挙したり
置き換え:
拡張子から MIME タイプを取得
ruby# 旧 Mime::EXTENSION_LOOKUP["html"] # 非推奨 # 新 Mime::Type.lookup_by_extension("html") # または Mime[:html] # 既存のショートカット登録済み拡張子を列挙したい(従来は
Mime::EXTENSION_LOOKUPでしかできなかった)
今回、新しくMime.extensionsが追加されています。rubyMime.extensions # => [:html, :text, :json, ...] (全ての拡張子・シノニムを含む)
これにより、「レジストリ(Hash/Set)自体」に触るのではなく、メソッド経由で取得・列挙するスタイルに統一されます。
コア実装の変更点(概要)
actionpack/lib/action_dispatch/http/mime_type.rb の変更が中心で、以下のような対応がされています。
Mime::SET,Mime::LOOKUP,Mime::EXTENSION_LOOKUPを内部実体に対するDeprecatedObjectProxyに差し替え- 実行時には従来通り動作するが、アクセスごとに deprecation warning を発生させる。
Mime.extensionsの追加Mime.symbols同様の公開メソッドとして、拡張子の一覧取得をサポート。
- 既存内部コードで、これら定数を直接使っていた箇所を公開 API ベースの呼び出しにリファクタリング
action_mailer.rb,action_dispatch.rb,abstract_controller/collector.rbなど。
テストの更新
actionpack/test/dispatch/mime_type_test.rbなどで:- 新メソッド
Mime.extensionsの挙動テストが追加。 - 旧定数使用時に deprecation が発生することの確認テストなどが追加または修正。
- 新メソッド
actionview/test/template/lookup_context_test.rbなど内部で MIME レジストリを使うテストも、新 API に合わせて微修正。
- 影響範囲・注意点
影響を受けるコード
以下に該当するアプリケーション/ライブラリは deprecation warning の対象になります。
Mime::SET/Mime::LOOKUP/Mime::EXTENSION_LOOKUPを直接参照しているコード- 例:ruby
Mime::SET.each do |type| # ... end if Mime::EXTENSION_LOOKUP["csv"] # ... end
- 例:
これらは Rails の内部実装詳細(ミュータブルなレジストリそのもの)に依存しているため、今後のメジャーバージョンで削除される可能性が高いです。
マイグレーションの指針
- 列挙用途
- MIME シンボル一覧:
Mime.symbols - 拡張子一覧:
Mime.extensions
- MIME シンボル一覧:
- ルックアップ用途
- MIME 文字列
"text/html"→Mime::Type.lookup("text/html") - 拡張子
"html"→Mime::Type.lookup_by_extension("html")またはMime[:html]
- MIME 文字列
- レジストリを直接変更している場合(かなりレアケース)
- 例えば
Mime::EXTENSION_LOOKUP["pdf"] = ...のように書いている場合は、元々非推奨なパターンであり、公式な登録 API(Mime::Type.registerなど)または提供されている拡張メソッド経由に置き換えるべきです。
- 例えば
注意点
- 今はまだ動きますが、DeprecatedObjectProxy 経由の使用は将来の削除を前提とした一時措置です。
deprecation メッセージが CI などで検出される場合は、早めの対応が必要です。 - 特に gem やエンジンなどのライブラリは、利用者のアプリケーションで deprecation noise を出さないよう、できるだけ早期に新 API へ移行しておくとよいです。
- 参考情報 (あれば)
- 置き換え API 一覧:
Mime::SET- →
Mime.symbols,Mime[...],Mime::Type.lookup
- →
Mime::LOOKUP- →
Mime::Type.lookup
- →
Mime::EXTENSION_LOOKUP- →
Mime.extensions,Mime::Type.lookup_by_extension,Mime[...]
- →
- 関連ファイル:
actionpack/lib/action_dispatch/http/mime_type.rb(MIME 周りの中核実装)actionpack/CHANGELOG.md(非推奨化の公式なアナウンスが追記済み)
#57917 Attribute methods ractor safe
マージ日: 2026/7/2 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails のActiveModel::AttributeMethods周りで使われるaliases_by_attribute_name/attribute_aliases/attribute_method_patternsを、Ractor 間で安全に共有できるよう「デフォルトで freeze される値オブジェクト&コレクション」に変更した PRです。これにより、Ractor 環境でも属性メソッド関連の情報を安心して参照できるようになり、並行実行時のデータ競合や Ractor 制限によるエラーを防ぎます。
- 変更内容の詳細
2-1. 目的となる3つのメソッドの Ractor-safe 化
Ractor 間で共有するには、「凍結されていて、かつ Ractor-shareable なオブジェクト」である必要があります。この PR では以下をその条件に合わせるための変更が行われています。
AttributeMethods.aliases_by_attribute_nameAttributeMethods.attribute_aliasesAttributeMethods.attribute_method_patterns
方針としては:
- 値オブジェクトおよび配列・ハッシュを「生成時に freeze」する
- 「必要になった時に書き換える(遅延・破壊的変更)」スタイルを避ける
- 継承時に子クラスへ「凍結済みのコピー/設定」を行っておく
という構造になっています。
2-2. AttributeMethodPattern の値オブジェクトをデフォルトで frozen に
AttributeMethodPattern は、属性メソッド (before_type_cast, ?, = など) を表現するための内部的な値オブジェクトです。
この PR での変更点:
AttributeMethodPatternのインスタンスを生成時にfreezeするように変更- これにより、
attribute_method_patternsが保持するパターンの配列全体が Ractor 間で共有可能になる
イメージとしては:
pattern = AttributeMethodPattern.new(prefix: "", suffix: "=")
pattern.frozen? # => trueのように、パターン自体に後から変更を加えない前提をコードで保証するようにしています。
2-3. aliases_by_attribute_name を「継承時に」初期化するよう変更
従来は、aliases_by_attribute_name は「最初にアクセスされたときに default_proc を使って遅延生成・更新」していた箇所がありました。これは「ハッシュにアクセスした瞬間に中身を書き換える」ため、Ractor から見ると共有が難しくなります。
この PR では:
aliases_by_attribute_nameを クラス継承時に確定させる 形に変更
→ サブクラス用のハッシュを、親クラスからコピー・設定するタイミングを明示的にdefault_proc内でハッシュを書き換えるのをやめ、「必要なキーが無い場合は、凍結された空配列などを返すだけ」に変更
イメージ:
# 旧: キー未定義時に default_proc 内でハッシュを更新していた
hash.default_proc = ->(h, k) { h[k] = [] }
# 新: ハッシュ自体は固定し、見つからないキーのときは
# 単に空の frozen 配列を返すだけにする
hash.default_proc = ->(_h, _k) { EMPTY_FROZEN_ARRAY }こうすることで、aliases_by_attribute_name 自体は「それ以上変更されないハッシュ」となり、Ractor 間で共有しても安全になります。
2-4. 「欠けているキー」に対しては凍結された配列を返す
aliases_by_attribute_name[attribute_name] が存在しない場合:
- 以前は default_proc 内で
hash[attribute_name] = []のように書き換えつつ空配列を返していた - 現在は、ハッシュを変更せず、単に「中身の無い・凍結された配列」を返す
これにより:
- 「読み取りのみ」のパターンになる
- 返される配列も frozen なので、Ractor 間で共有しても破壊的変更ができない設計になる
2-5. attribute_aliases / attribute_method_patterns の扱い
attribute_aliases も attribute_method_patterns も、本質的には「定義時に決まって、その後あまり変えたくない情報」です。
この PR により:
- それぞれの戻り値となるハッシュや配列がデフォルトで
freezeされるよう整理 - (
AttributeMethodPattern自体も frozen なので)attribute_method_patterns全体が Ractor-safe になる
Rails アプリ側から見ると、これらに対して「返ってきた配列/ハッシュを破壊的に変更する」コードは元々非推奨に近い使い方ですが、この変更によって 実際に破壊的操作ができなくなる(=例外になる) ため、Ractor 対応と同時に API の利用スタイルもより明示的になります。
- 影響範囲・注意点
3-1. 影響を受ける主なコード
ActiveModel::AttributeMethodsを継承して独自の attribute メタプログラミングをしているクラスattribute_method_patterns,attribute_aliases,aliases_by_attribute_nameを直接触っているメタプログラミング系コードや gem
特に注意が必要なのは次のようなコードです。
NG になりうるパターン例
# attribute_aliases に破壊的変更を加える
MyModel.attribute_aliases[:foo] = :bar
# or
MyModel.attribute_method_patterns << some_patternこれらは返り値が frozen になることで FrozenError になる可能性があります。
3-2. 対処法の方針例
返ってきたオブジェクトを「そのまま破壊的に変更する」代わりに:
dup/to_h/to_aしてから新しいオブジェクトを作成し、- 専用の設定メソッドや API を通じて反映する
といった形が推奨されます。
例:
# 旧: 直接破壊的に変更
MyModel.attribute_aliases[:foo] = :bar
# 新: 専用 API か、初期定義時にまとめて設定する
MyModel.attribute_alias(:foo, :bar)attribute_method_patterns も基本的には「クラス定義時にパターンを追加する」ための public API 経由で触るべきで、内部の配列を直接いじるスタイルは避けるとよいです。
3-3. Ractor を使わない場合でも影響はあるか
- Ractor を使わないアプリでも、「内部構造が freeze されるようになった」という意味で影響があります
- ただし、通常の利用(public API 経由で attribute alias やメソッドパターンを定義する)であれば実害はなく、テストも追加されているため後方互換性は基本的に保たれる想定です
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/57917
- Ractor の shareable オブジェクト要件(Ruby 3+)
freezeされていること- 参照グラフ全体が shareable であること
- Rails 内の関連コード
ActiveModel::AttributeMethods(属性メソッド定義のベース)ActiveRecord::Baseはこれを継承しているため、全 AR モデルに波及する仕組み
この PR により、 attribute 周りの内部メタデータ構造が「読み取り専用」設計に近づき、Ractor による並行実行が現実的に扱いやすくなっています。
#57757 Make InheritableOptions ractor safe
マージ日: 2026/7/2 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails のInheritableOptions(ActiveSupport::OrderedOptions系)の実装を見直し、freeze済みで Ractor 共有可能(Ractor-safe)にできるようにした PRです。親ハッシュへのフォールバックを行うデフォルト Proc を凍結時に除去し、Ractor の shareable 制約に抵触しない仕組みに変更しています。
- 変更内容の詳細
背景: なぜ Ractor-safe ではなかったか
InheritableOptions は、親オプションを継承しつつ子側で上書きできる仕組みで、内部的には「キーが見つからない時に親ハッシュを見る」ためのデフォルト Proc を持っています。
イメージとしては以下のような構造です:
class InheritableOptions < ActiveSupport::OrderedOptions
def initialize(parent = nil)
@parent = parent
default_proc = ->(hash, key) { @parent[key] if @parent && @parent.key?(key) }
super(&default_proc)
end
end- この
default_procはインスタンス変数@parentをキャプチャするため、- Proc 自体が Ractor 的に「shareable」になりにくい
- かつ、その Proc を持つオプションオブジェクトも shareable にできない
Ractor.make_shareableは深くオブジェクトグラフを辿るので、「freezeしたら勝手に@parentまで shareable 化する」のは副作用が大きく、freezeの責務としても不適切
そのため、「freeze 済み InheritableOptions を Ractor に渡す」ということが安全にできない状態でした。
方針: freeze 時に親参照を解消する
この PR では、
「凍結前は親を参照するが、凍結するときに親からのフォールバックを解消し、単純なハッシュとして完結させる」
という方針を取っています。
具体的には:
InheritableOptions#freezeが呼ばれた時に- 親ハッシュの内容を「reverse merge」の形で自分自身に取り込む
- =「自分に値がなければ親の値をコピーする」
- 親ハッシュの内容を「reverse merge」の形で自分自身に取り込む
- そのうえで
default_procを捨てる(親を参照しないようにする) - 結果として、凍結後は「見た目は同じ値を返すが、親にアクセスしない純粋な hash-like オブジェクト」になる
擬似コードで表現すると次のようなイメージです(実装は ordered_options.rb 内):
def freeze
unless frozen?
if @parent
# 親の値を自分自身に reverse_merge しておく
@parent.each do |k, v|
self[k] = v unless key?(k)
end
end
# 親を参照する default_proc を除去
self.default_proc = nil
super
end
end※実際のコードでは OrderedOptions / InheritableOptions の構造に合わせた実装になっています。
overridden? の実装変更
InheritableOptions には概ね次のようなメソッドが存在します:
options.overridden?(:foo) # 親から継承したのではなく、子で明示的に設定されているか?従来は「子にキーが存在するかどうか」で判定できていましたが、この PR 後は凍結時に親のキーも子へコピーされるため、そのままだと「すべて overridden 扱い」になってしまいます。
そこで、この PR では:
- 「どのキーが子で明示的に設定されたか」を内部で追跡するように変更
- 例:
@overridden_keysのようなセットに、値を代入したタイミングでキーを記録
- 例:
overridden?(key)は「@overridden_keysにそのキーが含まれているか」で判定するように変更
これにより、
- 凍結前後で
overridden?の意味・結果を変えずに維持 - かつ凍結後は「親キーが子へコピーされても、それだけでは overridden と判定されない」
という動作にできます。
テスト追加
activesupport/test/ordered_options_test.rb にテストが追加されており、主に以下を検証しています:
- Ractor-safe 化された
InheritableOptionsがこれまでどおりの値解決を行うこと freeze前後でoverridden?の結果が変わらないこと- 親オプション・子オプションの関係が、凍結後も期待通りに維持されること(挙動レベルで)
- 影響範囲・注意点
影響範囲
ActiveSupport::InheritableOptionsを直接/間接的に利用しているコード全般- 特にエンジンや Railtie の設定オプションなど、「親設定を継承して子で上書きする」パターン
overridden?を利用しているコード(Gem/アプリのメタプログラミング的な部分など)
互換性
- 通常ユースケースでは、凍結前後で返却される値は従来と一致するよう配慮されているため、挙動的な非互換は最小限
overridden?が「内部実装として、単にkey?か否かに依存している」と仮定していたような非常に内部寄りのコードがあれば、影響を受ける可能性はありますが、そのようなコードは稀と考えられます。
Ractor を使う場合の注意
- この PR により、「
freeze済み InheritableOptions を Ractor に渡す」パターンが実現しやすくなります。 - ただし、親オプション自体や、親にぶら下がる値オブジェクトが Ractor-safe かどうかは別問題であり、アプリ側で引き続き注意が必要です。
freezeはあくまで「親からのフォールバックを解消し Proc を除去する」だけであり、Ractor.make_shareableを内包してはいません。そのため、凍結=完全 shareable という保証ではない点には留意してください。
- この PR により、「
- 参考情報 (あれば)
- PR 本文の論点:
- Ractor の shareable 要件と
default_proc(Proc のクロージャ)が持つ参照の問題 freezeで勝手にRactor.make_shareableを呼ぶことの是非(副作用の大きさと API としての責務不一致)
- Ractor の shareable 要件と
- 関連するクラス/概念:
ActiveSupport::OrderedOptionsActiveSupport::InheritableOptions- Ractor /
Ractor.make_shareable
- 実際に Rails アプリを Ractor-safe にする場合は、他のコンポーネント(例えば logger、キャッシュストア、クラス変数に保持している設定値など)も含めて「Proc やミューテーブルなオブジェクトがどこに残るか」を総合的に見ていく必要があります。
#57953 Mark a connection unverified when a non-StandardError interrupts a query
マージ日: 2026/7/2 | 作成者: @edaroit
- 概要 (1-2文で)
- Active Record の DB コネクションプールで、
StandardError以外の例外(Timeoutや fiber scheduler のAsync::Stop/Async::Cancelなど)でクエリが中断された場合にも、そのコネクションを「未検証状態」にマークするようにし、後続リクエストに壊れたソケットが再利用されないようにする修正です。 - 中断されたクエリ自体の挙動(例外がそのまま飛ぶ)は変えず、「プールに戻されたコネクションの信頼性」だけを正しく扱うことで、EOFError などが後続の無関係なリクエストで発生するバグを防ぎます。
- 変更内容の詳細
背景となる問題
ConnectionAdapters::AbstractAdapter#with_raw_connection は内部で DB に対して「生の」クエリ実行を行い、ここで例外が起きたときにコネクション状態を調整しています。
従来の挙動:
def with_raw_connection(...)
# 省略
rescue => e # == rescue StandardError
downgrade_connection_after_error(e)
raise
endrescue => eは Ruby の仕様上StandardErrorだけを捕捉します。Timeoutや、多くの async/fiber ランタイム (Async::Stop,Async::Cancel) はExceptionの直下 or 途中のサブクラスであり、StandardErrorの外側です。- そのため、クエリ実行中にそうした例外で中断されると:
- 例外は
with_raw_connectionのrescueをすり抜けて外側へ伝播 - しかし、そのタイミングでコネクションオブジェクトは「正常・検証済み」としてプールに戻されてしまう
- 次にプールからこのコネクションを checkout したリクエストが、破綻したソケットを使おうとして
EOFErrorやActiveRecord::ConnectionFailedを受ける
→ 一つのリクエストの中断が、後続の無関係なリクエストを壊す
- 例外は
特に Falcon など fiber scheduler ベースのサーバでは、キャンセルを fiber 内に Async::Stop として直接投げ込むため、このパスに頻繁に乗りやすくなります。
追加された処理
このギャップを塞ぐため、with_raw_connection に rescue Exception を追加し、「StandardError 外の例外でもコネクションを“ダウングレード”」するようにしました。
追加コード(概略):
def with_raw_connection(...)
# 通常の処理
rescue => e # StandardError 系
downgrade_connection_after_error(e)
raise
rescue Exception
@last_activity = nil
@verified = false
raise
endここで重要なのは:
@verified = false@last_activity = nil
の2つの書き込みを行うことです。
なぜ @verified = false だけでは足りないのか
checkout 時の再検証ロジックはおおよそ次のようになっています:
elsif !@needs_reconnect && (last_activity = seconds_since_last_activity) && last_activity < verify_timeout
# 直近で使われているなら、検証をスキップしてよし、とみなすこの分岐は「@needs_reconnect が false で、かつ最近使われた(last_activity が小さい)」場合に、検証(verify!)をスキップします。
@verified = falseだけ変えても、- 「最近使われた」扱いだとこのショートカットに乗ってしまい、
- 実際には壊れているソケットを再検証せずに再利用してしまう可能性があります。
- そこで
@last_activity = nilを一緒にクリアすることで、- 「最近使われた」ショートカットに入らないようにし、
- 次の利用時に必ず検証経路に入り、そこで死んだソケットなら再接続されるようにしている。
なぜ @needs_reconnect は触らないのか
downgrade_connection_after_error は、エラーを「再試行可能な接続エラー」と判定できる場合にだけ @needs_reconnect = true を設定します。
- 今回対象の
Exception系の割り込みは、- DB ドライバのエラー分類の範囲外であり、
- 「接続エラーである」と決め打ちできる根拠がない。
- ただし
@verified/@last_activityをクリアすれば、次の利用時に verify が走り、そこで本当に再接続が必要なら@needs_reconnectが立つ or 実際に reconnect が行われます。
そのため、このパスでは「“とりあえずこのコネクションは信用しない” という最低限のフラグリセット」に留めています。
rescue Exception への懸念と既存の前例
rescue Exception は一般にアンチパターンとされますが、このファイルでは既に類似の用途に使われており、今回の変更もそれに倣っています。
attempt_configure_connectionではTimeout::ExitException(Exception派生)を補足するためにrescue Exceptionを使用済み。- 今回も「アプリケーションコードの例外を飲み込む」のではなく、
- 捕捉したら状態だけ直して
raiseし直す、という「透明な」挙動です。 - 例外の型やスタックトレースは変えません。
- 捕捉したら状態だけ直して
テストの追加
activerecord/test/cases/adapter_test.rb に以下のようなテストが追加されています(概要):
- あるアダプタに対して:
- まず普通のクエリを投げてコネクションを「使用済み・verified な状態」にする。
- 次に
with_raw_connectionの中でStandardError外の例外を発生させる。 - その後のコネクション状態として:
@verifiedが false になっていること@last_activityが nil になっていること をアサート。
このテストは、修正がないと失敗し(コネクションが verified のまま)、修正後は通ることが確認されています。sqlite3 と trilogy で検証済みです。
- 影響範囲・注意点
- 対象:
- Active Record 全般の DB 接続処理。
- 特に fiber scheduler(Falcon, async-http 等)や、
Timeoutをクエリ実行中に使う環境で恩恵が大きいです。
- 期待される改善:
- 非
StandardErrorの例外でクエリが中断された後に、別のリクエストで突然EOFErrorやActiveRecord::ConnectionFailedが起きる、といった「後続リクエストへの汚染」が起きにくくなります。 - プール内のコネクションの「整合性」が高まり、障害の局所化がしやすくなります。
- 非
- 互換性・副作用:
- 中断されたリクエスト側の例外の種類・伝播のされ方は変わりません(あくまでコネクション状態のフラグを書き換えるだけ)。
- 非
StandardErrorを故意にクエリ中断に使っている場合、次回そのコネクション利用時に verify が必ず走るようになるため、ごく軽微ですが再接続や追加の round-trip が増える可能性があります。 rescue Exceptionを追加していますが、即座に例外を再送出しており swallowing はしていません。挙動面の変化は「コネクションが unverified になるかどうか」だけです。
- 参考情報 (あれば)
- 該当コード(verify スキップ条件):
https://github.com/rails/rails/blob/b4379f0180a0b7f3e8e5e390c97e9586d556d817/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb#L1105-L1106 - 類似の
rescue Exception使用箇所:attempt_configure_connection(Timeout::ExitException対応) - 関連するエラー例:
EOFErrorActiveRecord::ConnectionFailedAsync::Stop,Async::Cancel(fiber scheduler のキャンセル用例外)
#57957 Bump Github Action cache version to 6
マージ日: 2026/7/1 | 作成者: @akhilgkrishnan
- 概要 (1-2文で)
Rails が生成する GitHub Actions ワークフロー(CI 用)の「キャッシュバージョン」を 6 に更新した PRです。アプリケーション/プラグイン用 generator が出力する.github/workflows/ci.ymlのテンプレートのみが対象です。
- 変更内容の詳細
対象ファイル:
railties/lib/rails/generators/rails/app/templates/github/ci.yml.ttrailties/lib/rails/generators/rails/plugin/templates/github/ci.yml.tt
これらは rails new や rails plugin new 実行時に .github/workflows/ci.yml として生成されるテンプレートです。
今回の変更は、GitHub Actions のキャッシュキーに使っている「内部バージョン番号」を 5 → 6 に bump しただけの非常に小さな変更です。
イメージとしては、テンプレート中に以下のような行があるとします(実際の PR でもこれに近い形です):
- uses: actions/cache@v4
with:
path: vendor/bundle
key: ${{ runner.os }}-gems-v6-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gems-v6-この v6 の部分が以前は v5 だったものを更新しています。
この「キャッシュバージョン」は、キャッシュの破棄や再生成のタイミングをコントロールするための任意のラベルで、
- キャッシュスキーマを変えた
- 中に入れるパス構成を変えた
- bundle の扱いを変えた
といったときに bump する、という運用がよくされています。
PR 説明にある followup: https://github.com/rails/rails/pull/56772 で行われた CI テンプレート関連変更(例: Bundler 周りや Ruby セットアップ周りの見直し)に続く整備として、キャッシュのバージョンだけを更新したものと考えられます。
- 影響範囲・注意点
影響範囲
- これから
rails new/rails plugin newで作成されるプロジェクト・プラグインのうち、--skip-githubせず GitHub CI を生成するケースにのみ影響します。 - 既存プロジェクトのワークフローは自動では書き換わらないため、この PR だけでは既存リポジトリには影響しません。
- これから
効果
- 新しく生成された
.github/workflows/ci.ymlは「バージョン 6」のキャッシュを使うので、過去に「バージョン 5」などのキーで作られていたキャッシュとは衝突せず、新しい条件でクリーンなキャッシュを作り直せるようになります。 - Bundle パスや Ruby セットアップの変更に不整合がある古いキャッシュを誤って使い続けるリスクを減らします。
- 新しく生成された
注意点 / 既存プロジェクトで追従したい場合
- Rails 7.2 以降の CI テンプレートに合わせたい場合は、既存の
.github/workflows/ci.ymlで、同様にキャッシュのキー中のバージョン(v5など)をv6に手動で変更することが推奨されます。 - 変更後の最初のワークフロー実行では、新しいキーでキャッシュが作り直されるため、1 回目だけは依存関係のインストールにやや時間がかかる可能性があります。
- Rails 7.2 以降の CI テンプレートに合わせたい場合は、既存の
- 参考情報 (あれば)
- 対応する前 PR(テンプレートや CI 設定の調整などが含まれる可能性あり)
- Rails の GitHub CI テンプレートは
railtiesの generator で管理されており、ci.yml.ttがrails new時に展開されます。- app:
railties/lib/rails/generators/rails/app/templates/github/ci.yml.tt - plugin:
railties/lib/rails/generators/rails/plugin/templates/github/ci.yml.tt
- app:
#57939 Apply source mappings to dynamic permissions policy sources
マージ日: 2026/7/1 | 作成者: @55728
- 概要 (1-2文で)
Permissions-Policy で「動的(callable)なソース指定」にキーワード(:selfなど)を返した場合も、静的指定と同様にマッピング('self'などへの変換)が適用されるようになりました。これにより、これまで動的ソースだけ不正なポリシー文字列(selfのような裸の単語)が出力されていた問題が修正されています。
- 変更内容の詳細
何が問題だったか
Permissions-Policy(旧 Feature-Policy)の設定で、ブロック(→ Proc)などを使って「動的に」ソースを返すケースがあります。
policy.geolocation -> { :self }このとき、:self のような「キーワードシンボル」を返しても、
静的指定:
policy.geolocation :self
→geolocation 'self'動的指定:
policy.geolocation -> { :self }
→ 以前はgeolocation self(クォートなし)になっていた
つまり、動的な場合だけ :self などのキーワードに対するマッピング処理が走らず、「素の self」が出力されてしまい、Permissions-Policy の文法上は不正なヘッダになっていました。
今回の修正内容
action_dispatch/http/permissions_policy.rb 内で、動的ソースの評価結果にも「ソースマッピング(:self → 'self' など)」を適用するように変更されています。
- 静的ソースと同じ経路でマッピングされるようになり、CSP(Content Security Policy)の実装と挙動が揃えられました。
- テスト (
actionpack/test/dispatch/permissions_policy_test.rb) が追加され、動的ソースに対しても'self'のように適切にクォートされた値が出力されることを検証しています。
挙動の具体例
修正前:
policy.geolocation :self
# => geolocation 'self'
policy.geolocation -> { :self }
# => geolocation self # クォートなしで不正修正後:
policy.geolocation :self
# => geolocation 'self'
policy.geolocation -> { :self }
# => geolocation 'self' # 静的と同じマッピングが適用される他のキーワード(例: :none など)についても同様に、静的指定時と同じルールでマッピング・整形されます。
- 影響範囲・注意点
- 影響を受けるのは、Permissions-Policy で ブロック / Proc などの動的ソース指定を使い、かつ
:selfなどのキーワードシンボルを返している場合 です。 - これまではそのようなコードが不正なヘッダ(
selfのようにクォートされていない単語を含む)を生成していた可能性があり、ブラウザによってはヘッダ全体が無視されていた可能性があります。 - この修正によりヘッダが正しく解釈されるようになり、以前は実質無効化されていた Permissions-Policy が今後は有効に適用されるようになることがあります。その結果、既存アプリで以下のような挙動変化が起こり得ます:
- geolocation, camera, microphone などの利用が、想定通りに制限され始める
- 開発・テスト環境では問題なかったが、本番でのみ Permissions-Policy を緩く解釈していたブラウザが、修正後は厳格に解釈するように見える場合がある
確認ポイント:
config.action_dispatch.permissions_policyなどで、Proc/ラムダを使ってソースを返している箇所を確認する:-> { :self }/-> { :none }/-> { :src }などキーワードを返していないか- 返り値が従来から想定していた文字列表現(
'self','none','src'など)になることを前提に書かれていないか
- ブラウザの DevTools で実際に送信されている
Permissions-Policy/Permissions-Policy-Report-Onlyヘッダを確認し、値が'self'など適切にクォートされていること、およびポリシーが想定通りに働いていることをチェックする。
- 参考情報 (あれば)
- この修正は、Content Security Policy における source mapping の挙動と整合を取る意図が明示されています。CSP での
:self→'self'のような扱いに慣れている場合、そのまま同じ感覚で Permissions-Policy の動的ソースも書けるようになります。 - 実際のコード差分は小さく(+16/-1)、
PermissionsPolicyクラス内のソース変換ロジックに対する一点修正と、それを保証するテスト追加が中心です。
#57938 Allow a trailing slash URL with a blank path to take query params
マージ日: 2026/7/1 | 作成者: @55728
- 概要 (1-2文で)
url_forでtrailing_slash: trueかつpath: ""(空文字)を指定した場合に、クエリパラメータやアンカーを付与するとFrozenErrorが発生していた不具合を修正した PR です。
これにより、"/"をベースに"/?a=1"のように正常にパラメータやアンカーを付与できるようになりました。
- 変更内容の詳細
問題の挙動
従来、以下のようなコードを実行すると:
url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 })- ベースのパスは
"path: "" + trailing_slash: true"により"/"になる - その後、この
"/"文字列に対してクエリパラメータ"?a=1"を「破壊的に」連結しようとする - しかし、内部的に使っている文字列が
freezeされたものだったため、破壊的変更ができずFrozenError: can't modify frozen String: "/"が発生
という状態になっていました。
修正内容
変更ファイル:
actionpack/lib/action_dispatch/http/url.rb(+1/-1)actionpack/test/dispatch/request_test.rb(+6)
url_for 内部の URL 生成ロジック(ActionDispatch::Http::URL 内)で:
trailing_slash: trueかつpathが空のケースで使われるパス文字列に対して
クエリパラメータやアンカーを付与する際に凍結された文字列を直接破壊的変更しないように修正
実装上は、凍結された文字列をそのまま << などで変更するのではなく、
必要に応じて複製(dup)した上でクエリやアンカーを連結する形に変えたと考えられます(+1/-1 なので単純な修正)。
サンプルコード
修正前
url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 })
# => FrozenError: can't modify frozen String: "/"修正後
url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 })
# => "/?a=1"
url_for(only_path: true, trailing_slash: true, path: "", anchor: "top")
# => "/#top"
url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 }, anchor: "top")
# => "/?a=1#top"テスト (actionpack/test/dispatch/request_test.rb) では、
上記のようなケースが追加されているはずで、FrozenError が出ないことと、
期待する文字列が得られることを確認しています。
- 影響範囲・注意点
影響範囲
url_for(およびそれをラップしている*_url/*_pathヘルパなど)でtrailing_slash: truepathを空文字列("")または実質的にブランクと解釈されるケース- クエリパラメータ (
params) またはアンカー (anchor) を付与
という組み合わせを利用しているコードが対象になります。
- これまではこの組み合わせが事実上「使用不能」だった(例外が出ていた)ため、
既存アプリケーションがこの挙動に依存している可能性は極めて低く、
変更は基本的に後方互換的で安全と考えられます。
注意点
- 今回の修正により、
"/"をベースとするパスでもクエリ・アンカーを問題なく付与できるようになるので、
「ルートパスに対してパラメータを付けたい」ケースで、path: ""+trailing_slash: trueを
安心して使えるようになります。 - 内部実装で文字列の
dupなどが行われている場合、微小なオブジェクト生成コストは増えますが、
実用上は無視できるレベルと考えてよいです。
- 今回の修正により、
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57938
- 該当コード周辺:
ActionDispatch::Http::URL(actionpack/lib/action_dispatch/http/url.rb) - 関連するヘルパー:
url_for, ルーティングヘルパー(root_path,root_urlなど)
#57951 Improve usage of Ractor.shareable_lambda
マージ日: 2026/7/1 | 作成者: @andrewn617
- 概要 (1-2文で)
Action Mailbox 内でRactor.shareable_lambdaの使い方を、&->を噛ませる形からブロックを直接渡す形に書き換えた、スタイル/可読性改善のための小さなリファクタリングです。挙動変更や機能追加はなく、既存の動作を保ったまま Ruby らしい書き方に揃えています。
- 変更内容の詳細(あればサンプルコードも含めて)
PRのモチベーションとして、以下のような「悪い例」が既にコードベースに入り込んでしまっていたので、今後は推奨しない書き方に揃えたい、という説明がなされています。
# bad
shareable_lambda(&->(foo) { foo })
# good
shareable_lambda { |foo| foo }Ractor.shareable_lambda は、Ractor(並列実行)間で共有可能なラムダを生成するためのメソッドで、actionmailbox のコールバック定義部分で使用されています。
この PR では actionmailbox/lib/action_mailbox/callbacks.rb 内の Ractor.shareable_lambda 呼び出しが、次のように変更されたと考えられます(実際のコードイメージ):
# 変更前(想定)
ActionMailbox::Callbacks.some_callback = Ractor.shareable_lambda(
&->(inbound_email) { process_inbound_email(inbound_email) }
)
# 変更後(想定)
ActionMailbox::Callbacks.some_callback = Ractor.shareable_lambda do |inbound_email|
process_inbound_email(inbound_email)
end技術的な意味としては:
&->(foo) { foo }は「ラムダを作ってから&でブロックに変換して渡す」二段構えshareable_lambda { |foo| foo }は「最初からブロックとして渡す」シンプルな形
であり、どちらも結果として shareable_lambda にブロックが渡され、そこから shareable な Proc(ラムダ)が生成される、という点では挙動は同じです。
この PR は、無駄な Proc 生成+ブロック変換をやめて、素直にブロックを渡すスタイルに統一する、というリファクタリングになります。
- 影響範囲・注意点
機能面の影響
- コールバックの挙動や Action Mailbox の外部 API は変わりません。
- ラムダの引数・戻り値の扱いも変わらないため、利用側のコード修正は不要です。
パフォーマンス
&->を経由しないため、わずかにオブジェクト生成が減る可能性はありますが、体感できるほどの差はほぼ期待できません。- 目的は最適化というより、スタイルと分かりやすさの改善です。
Ractor / スレッド安全性
Ractor.shareable_lambdaの使い方自体は変わらないため、Ractor 間共有の安全性に関する挙動も変わりません。
注意点
- 今後同様のコードを書く際は、
shareable_lambda(&-> { ... })ではなく、ブロックを直接渡す形で統一すると、コードベースと一貫しやすくなります。- 例:
Ractor.shareable_lambda { |mail| handle(mail) }
- 例:
- 今後同様のコードを書く際は、
- 参考情報 (あれば)
Ractor.shareable_lambdaについて- CRuby の Ractor API の一つで、「Ractor 間で共有しても安全なラムダ(Proc)」を生成するためのメソッドです。
- ブロック内で閉じ込めるオブジェクトは「共有可能 (shareable)」である必要があります(イミュータブルなオブジェクトや、
.freeze済みのオブジェクトなど)。
- コーディングスタイル的なポイント
- Ruby では通常のメソッド呼び出しに対し、
&-> {}で Proc を渡すよりもブロックをそのまま渡す方が標準的です。 - この PR は Rails コードベースのスタイルをそれに合わせる意図があります。
- Ruby では通常のメソッド呼び出しに対し、
#57952 Make parameter parsers assignment ractor safe
マージ日: 2026/7/1 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails のパラメータパーサー (parameter_parsers) を設定する処理が Ractor セーフになるように、代入時に渡されたハッシュをfreezeするよう変更した PRです。あわせて、デフォルトパーサーの設定にパブリック API を経由せず、直接インスタンス変数に代入するようにして、不要なハッシュの複製を避けています。
- 変更内容の詳細(あればサンプルコードも含めて)
2.1 parameters_parsers 代入時の Hash を freeze
ActionDispatch::Http::Parameters(ActionDispatch::Request を通して利用される)で、parameters_parsers= のようなメソッドを使ってパラメータパーサーを設定する際、渡されたハッシュをそのまま内部で変形していたため、Ractor セーフではありませんでした。
この PR では、「パラメータパーサーを代入するタイミングで、そのハッシュを freeze してからインスタンス変数に設定する」ようにしています。
イメージとしては、以下のような変更です(実際のコードは数行レベルですが、概念的なサンプル):
# 変更前(イメージ)
def parameter_parsers=(parsers)
@parameter_parsers = parsers
# @parameter_parsers を内部で mutate する可能性がある
end
# 変更後(イメージ)
def parameter_parsers=(parsers)
@parameter_parsers = parsers.freeze
endこれにより、
ActionDispatch::Request.parameter_parsers = {
Mime[:json].symbol => ->(raw) { ... }
}のように設定すると、そのハッシュは freeze され、Rails 内部で意図せず書き換えられず、かつ Ractor から参照しても安全になります。
2.2 デフォルトパーサー設定でパブリック API を経由しない
以前のコミット(f2be45aaa8...)で、デフォルトのパラメータパーサー自体は Ractor セーフにしていたものの、そのデフォルト値を設定する際に、一度パブリックな setter を通していたため、同じハッシュの内容を二重に持つ(複製される)ような構造になっていました。
この PR では、「デフォルトパーサーを設定するときは、パブリック API (parameter_parsers= 等) を通さず、直接インスタンス変数(たとえば @parameter_parsers)に代入する」ように変更しています。
デフォルト値については Rails 側で形(キーの形式など)を保証できるため、わざわざ public API を通して整形/複製する必要がない、という判断です。
概念的には以下のイメージです:
# 変更前(イメージ)
DEFAULT_PARAMETER_PARSERS = { ... }.freeze
def initialize
self.parameter_parsers = DEFAULT_PARAMETER_PARSERS
end
# 変更後(イメージ)
DEFAULT_PARAMETER_PARSERS = { ... }.freeze
def initialize
@parameter_parsers = DEFAULT_PARAMETER_PARSERS
endこの変更により、
- デフォルトハッシュの無駄な複製を避けられる
- Ractor セーフ性を担保しやすくなる(共有オブジェクトの構造が単純になる)
という効果があります。
2.3 Proc の扱いについての考え方
PR の説明にもある通り、parameter_parsers の値として入るのは Proc(-> で定義されるラムダなど)であり、これらは call されるものです。
- Proc オブジェクトを Rails 側が勝手に
Ractor.make_shareableすると、ユーザが定義したクロージャに対する制約や予期せぬエラーを招く可能性がある - そのため、Proc の「shareable 化」までは Rails は行わず、「ハッシュそのものを freeze する」ことで、少なくともハッシュの構造変形による非 Ractor セーフ化を防ぐ、という方針です。
Ractor 越しに parameter_parsers を共有したい場合は、ユーザ側が適切に shareable な Proc を用意する/Ractor 内で定義する必要があります。
影響範囲・注意点
parameter_parsers=に渡したハッシュがfreezeされるようになるこれまでは setter に渡したハッシュを後から変更しても動いていたコードが、今後は
FrozenErrorになる可能性があります。例えば、以下のようなコードは動かなくなります:
rubyparsers = { Mime[:json].symbol => ->(raw) { ... } } ActionDispatch::Request.parameter_parsers = parsers # 後から追加しようとする parsers[Mime[:xml].symbol] = ->(raw) { ... } # => FrozenError対策としては、設定用のハッシュを使い回して外部から mutate しない、または、都度新しいハッシュを作って代入するようにする必要があります。
Ractor を利用するアプリでの安全性向上
- Ractor を使うマルチスレッド/マルチ Ractor 環境で
parameter_parsersを共有する際に、ハッシュがミューテートされないことが保証されるため、競合状態や「shared object ではないものを Ractor 間で共有した」という類のエラーを避けやすくなります。 - ただし前述の通り、「Proc 自体を shareable にする責任はユーザ側」にあるため、Ractor 間で共有したい場合はそこに留意が必要です。
- Ractor を使うマルチスレッド/マルチ Ractor 環境で
パブリック API を通さずにデフォルトを設定することによる外部 API への影響はほぼなし
- デフォルトのパラメータパーサーは内部的な構造が少しシンプルになっただけで、挙動(JSON 等のパース結果)は変わりません。
ActionDispatch::Request.parameter_parsersの getter/setter といったパブリック API 自体のインターフェースは変わっていないため、通常の利用コードには影響しません。
テスト・CHANGELOG について
- この PR 単体ではテストや CHANGELOG の更新は行われていません(チェックリスト上も未チェック)。
- 変更がかなり小さい(2行追加・2行削除)こと、および既存のテストでカバーされる範囲と判断されている可能性があります。
- 参考情報 (あれば)
この PR で言及されている既存コミット(デフォルトパラメータパーサーを Ractor セーフ化したもの):
https://github.com/rails/rails/commit/f2be45aaa8447b0d6efbcb7b42e5c6dbeb6b30b6関連クラス:
ActionDispatch::Http::ParametersActionDispatch::Request(parameter_parsersを通じて HTTP リクエストパラメータのパースを行う)
Ractor を積極的に使う Rails アプリで、ActionDispatch::Request.parameter_parsers をカスタマイズしている場合は、設定用ハッシュが凍結されることと、Ractor 越しに共有する Proc の shareable 性を意識しておくと安全です。
#57955 Ractor safe counter cache configuration
マージ日: 2026/7/1 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveRecord のカウンタキャッシュ設定情報(_counter_cache_columnsとcounter_cached_association_names)をデフォルトでfreezeし、Ractor(Ruby の並行オブジェクト実行モデル)でも安全に扱えるようにした PR です。これにより、Ractor 間で共有されるカウンタキャッシュ設定が不変オブジェクトとなり、スレッド/Ractor セーフティが向上します。
- 変更内容の詳細
何をしているか
ActiveRecord::CounterCacheが内部的に保持している以下の情報を「初期化時に frozen にする」よう変更しています。ActiveRecord::CounterCaches._counter_cache_columnsActiveRecord::CounterCaches.counter_cached_association_names
これらは主に、belongs_to :foo, counter_cache: true のような関連付けに対して、「どのカラムがカウンタキャッシュとして使われているか」「どの関連がカウンタキャッシュを持つか」というメタデータを保持するための構造です。
コードレベルでのイメージ
実際の差分は小さいですが、イメージとしては次のような変更が入っています(擬似コード):
# 変更前(イメージ)
module ActiveRecord
module CounterCaches
def _counter_cache_columns
@counter_cache_columns ||= {}
end
def counter_cached_association_names
@counter_cached_association_names ||= Set.new
end
end
end
# 変更後(イメージ)
module ActiveRecord
module CounterCaches
def _counter_cache_columns
@counter_cache_columns ||= {}.freeze
end
def counter_cached_association_names
@counter_cached_association_names ||= Set.new.freeze
end
end
endあるいは、belongs_to のビルダー側で設定を行うときに、
_counter_cache_columns[model_name] = somethingのような「後から破壊的変更」をせず、
「新しい frozen ハッシュ/セットを作って差し替える」形になっている可能性があります。
いずれにせよポイントは:
- 内部の設定オブジェクトを ミュータブルなまま共有しない
- Ractor 間で共有できるように freeze 済みオブジェクト として扱う
という設計変更です。
テストの追加
activerecord/test/cases/counter_cache_test.rb で、主に次のようなことを検証するテストが追加されていると考えられます:
ActiveRecord::CounterCaches._counter_cache_columnsが frozen であることActiveRecord::CounterCaches.counter_cached_association_namesが frozen であること- カウンタキャッシュ機能自体が、freeze された設定オブジェクトのもとでも従来どおり動作すること
- 影響範囲・注意点
影響範囲
- 影響を受けるのは、「ActiveRecord のカウンタキャッシュ設定情報を内部 API として直接触っているコード」です。
- 通常の Rails アプリケーションで、以下のような典型的な使い方だけをしている場合は影響はほぼありません:rubyこのレベルであれば挙動は変わらず、カウンタキャッシュ機能はそのまま動作します。
class Comment < ApplicationRecord belongs_to :post, counter_cache: true end
注意点
内部構造への破壊的変更は不可になる
もしアプリや gem で、内部 API に直接触れて次のような操作をしている場合は、
FrozenErrorが発生する可能性があります:ruby# 悪い例(今後は FrozenError の可能性) ActiveRecord::CounterCaches._counter_cache_columns[:posts][:comments_count] = :some_value今後は、こうした「内部の Hash/Set を直接書き換える」前提のコードは動かなくなります。
Ractor を使う場合の安全性向上
- frozen 化により、これらの設定オブジェクトは Ractor 間で安全に共有できるようになります。
- Ractor を利用した並列実行をしている/今後検討している Rails アプリやライブラリでは、カウンタキャッシュ周りでのデータ競合・オブジェクト共有エラーのリスクが軽減されます。
カスタムパッチ・メタプログラミングに注意
- カウンタキャッシュ周りに独自の Monkey Patch やメタプログラミングを入れている場合、
- 「既存の Hash/Set を mutate するコード」
- 「freeze されていないことを前提にした処理」 がないかを確認する必要があります。
- カウンタキャッシュ周りに独自の Monkey Patch やメタプログラミングを入れている場合、
- 参考情報 (あれば)
- Ruby Ractor の仕様:
- Ractor 間で共有されるオブジェクトは、基本的に immutable(
freeze済み)である必要があるため、ライブラリ側では「設定・メタデータオブジェクトを初期化時に freeze しておく」パターンが一般的になりつつあります。
- Ractor 間で共有されるオブジェクトは、基本的に immutable(
- この PR は CHANGELOG 更新が行われていないため、Rails 側では「挙動変更というより内部実装の安全性向上」として扱われていると考えられますが、内部 API を触っているコードがある場合はアップデート時に注意が必要です。
#57922 Freeze the inflection rules after the app boots
マージ日: 2026/7/1 | 作成者: @Edouard-chin
- 概要 (1-2文で)
このPRは、Railsアプリケーションのブート完了後にActiveSupport::Inflectorの「変換ルール(inflection rules)」を変更できないようにし、すべての言語ごとの inflections を freeze する変更です。これにより、起動後の不定な挙動を防ぎつつ、将来的な Ractor 対応も見据えたスレッドセーフ性の向上を図っています。
- 変更内容の詳細
2-1. 何が変わったか
- 言語ごとに保持されている inflection インスタンス(単数・複数形変換、例外ルールなどを持つオブジェクト)を、アプリケーションブート完了時に
freezeするようになりました。 - freeze 後は、
ActiveSupport::Inflector.inflections経由でルールを追加・変更しようとすると、FrozenErrorが発生します。 - Rails のブートシーケンス(
ActiveSupport::Railtie)に、inflections を freeze するフックが追加されています。 - これに対応するテストが
railties/test/application/active_support_railtie_test.rbに追加されています。
2-2. 想定される処理の流れ
典型的な Rails アプリでは、config/initializers/inflections.rb などで起動時に inflection ルールを定義します。
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.irregular 'person', 'people'
inflect.acronym 'API'
endこの PR 適用後も、ブート前(初期化時)のこのコードは有効です。
アプリケーションがブートし終わると:
ActiveSupport::Inflector.inflections(:en).frozen? #=> trueとなり、以降は inflections を変更しようとするとエラーになります:
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.irregular 'mouse', 'mice'
end
# => FrozenError (can't modify frozen ActiveSupport::Inflector::Inflections)2-3. コードレベルのポイント (推測を含まない範囲で)
変更ファイルから読み取れる論点(PRの説明とdiffの規模から確実に言える範囲):
activesupport/lib/active_support/inflector/inflections.rb- Inflections インスタンスに対し、後から書き換えられないようにするための freeze ロジックを追加。
- 「言語ごとに1つ存在する inflection インスタンス」をまとめて凍結する仕組みが導入された可能性が高い(説明文より)。
activesupport/lib/active_support/railtie.rb- Rails アプリケーションブート完了時(
on_loadまたは初期化プロセスの終盤)に、全 inflection インスタンスを freeze するフックが追加。
- Rails アプリケーションブート完了時(
railties/test/application/active_support_railtie_test.rb- ブート後に inflections を変更できないことを確認するテストケースが追加。
- おそらく「アプリ起動後に inflections を変更しようとして FrozenError が起きる」ことや、「ブート時に定義したルールは正しく適用される」ことを検証。
- 影響範囲・注意点
3-1. 影響を受けるケース
以下のようなコードを書いていると、この変更で動かなくなる/エラーが出る可能性があります。
アプリケーションブート後に inflections を変更している場合
例:
- リクエストごとに inflection を変える
- マルチテナントごとに inflection を動的に追加する
- コンソール起動後に initializer 相当の処理を実行して inflection を追加している
ruby# 例: Rails 起動後 / リクエスト中に ActiveSupport::Inflector.inflections(:en) do |inflect| inflect.acronym 'JSONAPI' end # => FrozenError 発生これらは、アプリケーションブート前(initializer)に移動する必要があります。
gem やエンジンが遅いタイミングで inflections をいじる場合
- エンジンやライブラリが
to_prepareやミドルウェア・リクエスト処理の中でActiveSupport::Inflector.inflectionsを変更している場合、これも FrozenError の対象となりえます。 - gem 開発者は、inflection 変更を Rails の初期化プロセス中に移動し、ブート後に変更しない設計にする必要があります。
- エンジンやライブラリが
3-2. なぜこの仕様変更が妥当か
不定なコードパスの排除
起動後に inflection が変わると、例えば以下がリクエスト順序やタイミングによって変化する可能性がありました。- モデル名 → テーブル名 のマッピング
- ルーティングのパス名
- 他、inflector を用いる各種内部処理
これは「あるタイミング以降でだけ挙動が変わる」ため、バグの再現や原因特定が非常に難しくなります。本PRはこれを防ぎます。
Ractor セーフティの向上
Ruby 3 の Ractor を考えると、「起動後に共有オブジェクトを変更可能であること」は競合やメモリ可視性の問題を生みます。
inflections を freeze してイミュータブルにすることで、「起動前に構築 → 起動後は読み取り専用」という Ractor 向きのモデルになります。
3-3. マイグレーション指針
アプリ開発者:
- inflection の定義や変更は
config/initializers/inflections.rbなどのブート前処理に集約する。 - 動的に inflection を変える設計は避け、必要なら自前ロジック(独自の変換テーブルやサービスクラス)を用意する。
- inflection の定義や変更は
gem / エンジン開発者:
- Railtie / Engine の
initializerブロック内で inflections を設定し、to_prepareや実行時に inflections を再定義しない。 - どうしても追加設定のタイミングがブート後になるなら、inflector を使わない別アプローチを検討する。
- Railtie / Engine の
- 参考情報 (あれば)
関連概念:
ActiveSupport::Inflectorと inflections:ActiveSupport::Inflector.inflections(:en)で取得できるオブジェクトに対し、plural,singular,irregular,uncountable,acronymなどでルールを定義する仕組み。- Ractor セーフティ:
共有ミュータブル状態をなくし、起動前に構築したイミュータブルデータを並行に読む構造にすることで、マルチ Ractor 実行時の安全性を高める。
実運用のベストプラクティス:
- 以前からも「inflection の定義は initializer で行い、実行時に変更しない」ことが暗黙の前提/推奨だったため、その暗黙のルールを仕様として厳密に enforce した形の変更と捉えると理解しやすいです。
#57823 Initialize ActionView::LookupContext.view_context_class early
マージ日: 2026/7/1 | 作成者: @etiennebarrie
- 概要 (1-2文で)
ActionView::LookupContext.view_context_classをできるだけ早いタイミングで初期化することで、enable_reloading = falseな環境ではミューテックスに触らずに済むようにし、Ractor からも追加セットアップなしで安全にビューコンテキストクラスを利用できるようにした変更です。
- 変更内容の詳細
※PR本文とファイル差分の要約ベースでの解説です(実際のコードは概念的なイメージ)。
2-1. LookupContext での view_context_class 初期化タイミングの変更
actionview/lib/action_view/lookup_context.rb において:
これまで:
view_context_classは必要になったタイミングで決定 / キャッシュされ、その際にミューテックスを用いた同期が行われていた。- そのため、スレッド/Ractor の利用状況によっては、
view_context_classの決定時にロック処理が発生していた。
変更後:
LookupContextの初期化時、もしくは「リロードを必要としない (enable_reloading = false)」ことが分かっている場合に、あらかじめview_context_classを決定しておくようなロジックに変更。enable_reloading = falseの場合は、ビューコンテキストクラスが後から変化しない前提のため、一度決定してしまえば再度ミューテックスを取る必要がない。
その結果、enable_reloading = false の環境では:
view_context_classの取得がロックフリー(またはほぼロックフリー)になる。Ractorからも「既に決まっている不変のクラス定義」としてview_context_classをそのまま利用できる。
2-2. ActionView::Rendering 側の参照調整
actionview/lib/action_view/rendering.rb にも 1 行の変更があり、これは:
view_context_classの取得方法やタイミングを、LookupContextの新しい初期化タイミングに合わせて調整したものと考えられます。- 実質的には「ビューコンテキストを生成する際に、すでに初期化済みの
view_context_classを使う」の一貫性を保つ変更です。
2-3. テスト追加
actionview/test/template/lookup_context_test.rb にテストが 16 行追加されています。
テストの狙いとしては:
enable_reloading = falseのときにview_context_classが早期にセットされ、以降ミューテックスを介さずに参照できること。 -(間接的に)Ractor からも同じクラスを利用できる、もしくは並行実行時に問題が起きないこと。
といった挙動を担保していると考えられます。
- 影響範囲・注意点
3-1. パフォーマンスと並行性
- パフォーマンス改善:
enable_reloading = false(本番環境が多い)で、view_context_class取得時のミューテックス獲得が避けられるため、ビューのレンダリングにおけるロックコストが削減されます。
- Ractor 対応の強化:
- Ractor 内から
ActionViewを使う場合でも、追加のセットアップなしでview_context_classを共有しやすくなっています。 - Ractor では「オブジェクトがイミュータブル or Ractor シェア可能」である必要がありますが、
enable_reloading = falseなら「クラス定義が途中で差し替わらない」ため、Ractor から扱いやすいモデルになります。
- Ractor 内から
3-2. enable_reloading = true (開発環境など)の挙動
- 開発環境では通常
enable_reloading = trueのため:- 従来どおり、クラスの再定義やテンプレートの変更に対応する必要があり、必要な箇所ではミューテックスを使った同期が残ります。
- この PR の主な恩恵(ロック回避・Ractor での利用容易化)は主に
enable_reloading = false側で現れます。
3-3. アプリ側コードへの影響
- アプリコードで
ActionView::LookupContext.view_context_classを直接触っていない限り、既存アプリケーションの動作は基本的に変わらない想定です。 - もし以下のようなことをしている場合は注意が必要です:
view_context_classをモンキーパッチで動的にすげ替えている- 初回アクセス時に何か副作用を期待している
- 今回は「初回決定タイミングがより早くなった」という性質の変更なので、
view_context_class決定時の副作用に依存しているコードがあると、タイミングずれの可能性があります(通常はそのような依存は推奨されません)。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57823
ActionView::LookupContextドキュメント:
https://api.rubyonrails.org/classes/ActionView/LookupContext.html- Ractor について(Ruby 3 以降の並行性モデル):
https://docs.ruby-lang.org/en/master/doc/ractor_md.html
この変更は「本番運用でのロック削減」と「Rails + Ractor の相性改善」の両方に関連するため、Rails を Ruby 3 系で Ractor 活用したり、高負荷環境でのビューレンダリング性能に関心がある場合に重要なアップデートになります。
#57936 Deduplicate columns in default_order
マージ日: 2026/6/30 | 作成者: @55728
- 概要 (1-2文で)
default_orderに同じカラムを複数回渡した場合、ORDER BY句に重複して出力されないよう重複排除される変更です。これにより、すでに同様の挙動をしていたorderやregroupと一貫した動きになります。
- 変更内容の詳細
挙動の変更点
これまで:
User.default_order(:name, :name)
# ORDER BY "users"."name" ASC, "users"."name" ASCこの PR 適用後:
User.default_order(:name, :name)
# ORDER BY "users"."name" ASCdefault_order に渡した引数リストから、同じカラム指定を重複して使わないように、内部的に「重複カラムの除去(デデュプリケーション)」を行うようにしています。
実装上のポイント
- 変更ファイル:
activerecord/lib/active_record/relation/query_methods.rbdefault_orderで使用するカラムリストに対して、orderやregroup同様に重複カラムをまとめる処理を追加(1行)。
- テスト:
activerecord/test/cases/relations_test.rbdefault_order(:name, :name)のようなケースで、生成される SQL のORDER BY句に同じカラムが 1 回だけ現れることを確認するテストが追加(5行)。
実際には、既存の order / regroup の実装と同じような重複排除ロジック(例: uniq 的な処理)を default_order のカラム処理にも適用した形と考えられます。
- 影響範囲・注意点
主な影響範囲
default_orderを使っており、同じカラムを意図的/偶発的に複数回指定しているコードで、生成される SQL が変わります。例えば、scope やモデルレベルで
default_orderを定義しているコード:rubyclass User < ApplicationRecord default_scope { default_order(:name, :name, :created_at) } endこれまでは
nameが複数回出ていたのが 1 回にまとまります。
パフォーマンス・動作への影響
ORDER BY句の重複列が論理的な結果に違いを生むことは基本的にありません(同じ列・同じ方向での並び替えは冗長でしかないため)。- 従って、アプリケーションの機能的挙動が変わる可能性はほぼなく、SQL 文字列の見た目が変わる程度の影響にとどまります。
- 一部、生成される SQL 文字列そのものを文字列比較でテストしている場合は、テストが落ちる可能性があります。その場合は、新しい形式(重複のない
ORDER BY)に合わせて期待値を更新する必要があります。
一貫性の向上
- 既に
orderとregroupは同種の重複排除をしているため、default_orderだけが異なる挙動をしていた状態が解消されます。 - これにより、開発者は「ActiveRecord の並び替え関連メソッドは重複カラムを自動的にまとめる」という前提でコードを書けるようになります。
- 既に
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57936
- 関連するメソッド:
ActiveRecord::QueryMethods#orderActiveRecord::QueryMethods#regroupActiveRecord::QueryMethods#default_order(この PR の対象)
もし既存コードで default_order を多用している場合は、テストスイートを走らせて SQL 文字列比較テストの影響有無を確認する程度で十分です。
#57856 Load ActionDispatch::Request::Session and Utils at boot time
マージ日: 2026/6/30 | 作成者: @andrewn617
- 概要 (1–2文で)
ActionDispatch::Request::Sessionとその内部で使われるUtilsを「初回リクエスト時のオートロード」から「アプリ起動時のロード」に変更するためのリファクタリングです。- これにより ractor 利用時など並行環境でのオートロード問題や循環 require を避けつつ、
SessionをActionDispatch::Http::Sessionに移動して責務の位置付けを整理しています。
- 変更内容の詳細
背景
- 以前の変更(rails/rails@8ce6b0c)で、
ActionDispatch::Request::Sessionは循環 require を避けるために autoload されるようになっていました。 - しかし autoload は「初回アクセス時に読み込む」ため、Ractor などスレッド/並行実行環境では「実行中にクラス定義が変わる」状態を招きやすく、問題になります。
- この PR では、セッション関連クラスとユーティリティを「HTTP レイヤのクラス」として再配置し、
ActionDispatch::Request読み込み時点で一緒に require されるように変更しています。
主なリファクタリングポイント
1) ActionDispatch::Request::Session → ActionDispatch::Http::Session への移動
- 旧構成(イメージ):
# action_dispatch/http/request.rb
module ActionDispatch
class Request
class Session
# セッション関連のクラス定義
end
end
end- 新構成(イメージ):
# action_dispatch/http/session.rb
module ActionDispatch
module Http
class Session
# セッション関連のクラス定義
end
end
endRequest::Sessionはドキュメント化されていない内部 API という扱いであり、クラスの場所を変えても外部 API 互換性の問題は小さい、という判断になっています。ActionDispatch::RequestはActionDispatch::Http::Sessionに依存する側として整理され、循環参照を避ける構造になっています。
2) Utils を HTTP 層に移動し、起動時に require
UtilsもRequest::Sessionと同様に HTTP レイヤで使われる内部ユーティリティとして扱い、action_dispatch/http/utils.rbに整理。ActionDispatch::Httpに関連するファイル (request.rb,parameters.rb,param_builder.rbなど) から、Utilsを明示的に require する形に変更し、起動時にロードされます。
# actionpack/lib/action_dispatch.rb
require "action_dispatch/http/session"
require "action_dispatch/http/utils"
# など Http 関連の require が追加・整理3) autoload / require の整理
action_dispatch.rb,action_dispatch/http/request.rb,action_controller/metal.rb,action_dispatch/railtie.rbなどで、SessionとUtilsの読み込み経路が調整されています。- これまで「最初のリクエスト時に autoload」されていたものが、
ActionDispatch/Http読み込みタイミングでrequireされるように変更。
- テスト (
request_test.rb,session_test.rb他) も、新しいクラス名・モジュール構成に合わせて参照先を更新しています。
4) 仕様追加・挙動変更はなし
- 差分は主に「クラス/モジュールの配置 と ロードタイミング」であり、セッション機能自体の振る舞い(API やストアの挙動)は変えていません。
- テストの追加・変更も挙動変更ではなく、クラスパス変更に追随するための修正が中心です。
- 影響範囲・注意点
影響範囲
内部 API (
ActionDispatch::Request::Session) に依存しているコード- ドキュメント外 API ですが、もしアプリやライブラリが
ActionDispatch::Request::Sessionを直接参照している場合、今回の変更により壊れる可能性があります。 - 新しい正しい参照先は
ActionDispatch::Http::Sessionになります。
- ドキュメント外 API ですが、もしアプリやライブラリが
初期化時のロードコスト
- これまで「初回アクセス時にオートロード」されていた
Session/Utilsが「ブート時にロード」されるため、起動時間がわずかに増える可能性があります。 - ただし、ActionDispatch 周りの他のクラスもすでにブート時にロードされており、相対的なインパクトは小さいと考えられます。
- これまで「初回アクセス時にオートロード」されていた
Ractor / 並行実行環境での安定性向上
- Rails プロセス起動後、アプリケーションコード実行中に
Sessionクラスが新たに定義されることがなくなるため、Ractor やマルチスレッド環境でのオートロード競合・定義の途中読み取りなどのリスクが低減します。 - ractor 対応や高並行処理を視野に入れている場合はプラスの変更です。
- Rails プロセス起動後、アプリケーションコード実行中に
注意点・確認ポイント
- 自前で
autoloadしている箇所やconst_get("ActionDispatch::Request::Session")のようなメタプログラミングをしている場合、クラスの場所変更により動作が変わるので確認が必要です。 ActionDispatch::Http::Sessionを直接使うつもりがない通常の Rails アプリ(コントローラレベルでしかセッションを扱わないケース)では、ほぼ影響はありません。- 将来的に Ractor を有効にする、あるいは Zeitwerk の strict モード・autoload 制限を強めたい場合には、この変更がその下準備の一つになっています。
- 参考情報 (あれば)
- この PR の直接の動機: Ractor 対応作業中に「
ActionDispatch::Request::Sessionが初回リクエストまでロードされない」=並行環境で問題になり得る autoload パターンであることが発覚したため。 - 関連コミット:
rails/rails@8ce6b0c- ここで
ActionDispatch::Request::Sessionが autoload 化され、循環 require 回避の暫定措置が導入されていた。
- ここで
- 設計上の意味:
- 「HTTP 関連の低レベルクラス (
ActionDispatch::Http::Session,ActionDispatch::Http::Utils)」と - 「それを利用する高レベルな
ActionDispatch::Request」 - という依存の向きが明確化され、Rails のロード順や並行実行時の安全性の面で一貫した構造になっています。
- 「HTTP 関連の低レベルクラス (
#57684 Add a Ractor-shareable ActiveSupport::TaggedLogging.shareable_logger
マージ日: 2026/6/30 | 作成者: @etiennebarrie
- 概要 (1–2文で)
Ractor から安全に共有できるログ出力用 APIActiveSupport::TaggedLogging.shareable_loggerが追加され、複数 Ractor / スレッドから同じ logger を使える仕組みが導入されました。内部的にはバックグラウンドスレッドに書き込み処理を委譲するActiveSupport::ShareableLoggerを tagged logging でラップする構成です。
- 変更内容の詳細
新しく追加された概念とクラス
2-1. ActiveSupport::TaggedLogging.shareable_logger
従来の:
logger = ActiveSupport::TaggedLogging.logger(STDOUT)に対応する形で、Ractor 共有可能な logger を返すクラスメソッドが追加されています:
logger = ActiveSupport::TaggedLogging.shareable_logger(STDOUT)
# => ActiveSupport::TaggedLogging でラップされた ShareableLogger特徴:
- 返される logger は
ActiveSupport::ShareableLogger(::Loggerのサブクラス)を内包 - Ractor 間で共有可能になるよう設計されており、複数 Ractor から同じ logger に書ける
- TaggedLogging によるタグ付きログ (
tagged("request-id") { ... }) が通常通り使える
2-2. ActiveSupport::ShareableLogger の構成
コード的には以下のような構図になっています(ファイルは activesupport/lib/active_support/ractors/logger/*.rb):
Writerクラス- バックグラウンドで、実際の
logdev(IO / ファイルなど)に書き込む専用スレッド - 他の Ractor/スレッドから送られてきたメッセージをキューから取り出して順次書き込む
- 書き込み中に例外が起きた場合は:
- 元の logger と同様に
stderrにフォールバックして出力 - 呼び出し側には例外を伝播させない(ログ出力失敗がアプリの例外にならない)
- 元の logger と同様に
- バックグラウンドで、実際の
DeviceProxyクラスLogger::LogDeviceを直接触らず、その前段で「書き込み要求をキューに積む」役割write,close,reopenなどの操作を、Writer スレッドにメッセージとして渡す- これにより、アプリ側スレッド/Ractorは実ログデバイスに直接触らない
概念図:
[Ractor A] --\
[Ractor B] --- logger.info(...) -> DeviceProxy -> [queue] -> Writer thread -> logdev
[Ractor C] --/2-3. エラーハンドリング
説明文にある通り、ログ書き込みエラー時の挙動は既存 Logger と同等になるように設計されています。
- 書き込み中に IO エラーなどが発生
- Writer 側でエラーを検知
stderrへの書き出しにフォールバック- 呼び出し元 (
logger.infoしたコード) には例外を返さない
ActiveSupport::ErrorReporter を使う案も検討されたが、それ自体が logger にフォールバックする設計のため、循環参照的になって断念、という経緯が説明されています。
2-4. Ractor 対応のためのインフラ
activesupport/lib/active_support/ractors.rb に Ractor 関連のサポートコードが追加され、それを使って:
- Ractor 間で安全にやり取りできるオブジェクト/メッセージの制約
- Writer スレッドとの連携
などを行う仕組みが提供されています。
2-5. テスト
activesupport/test/ractor_logger_test.rb(約 260 行)が追加- 複数 Ractor から同一 logger に書き込んでも正しくシリアライズされるか
- flush の挙動
- エラー時の挙動(stderr へのフォールバックなど)
railties/test/application/configuration_test.rb- Rails アプリケーション設定との統合(config 経由での利用など)が壊れていないことの確認
- 影響範囲・注意点
3-1. 利用シナリオ(想定)
- Ractor ベースの並列化を行う Rails / ActiveSupport アプリケーションで、単一の logger を全 Ractor から安全に使いたいケース
- 通常のスレッドベースの Rails アプリでも「非同期ログ書き込み」を使ってみたいケース
- PR 説明では「通常の threaded Rails アプリでも使えるが、パフォーマンス向上があるかは不明」と明記
3-2. キューの無制限成長リスク
- 書き込み用キューに上限がないため、ログが大量に出るエンドポイントで「Producer(アプリ側)が早く、Writer が追いつかない」状態になると、キューが肥大化しメモリを圧迫しうる
- 緩和策:
- 「リクエストの終わりで logger を flush する」という契約を前提としている
- 例: Rack ミドルウェアや around_action で
logger.flush相当を呼ぶ設計を想定
- 例: Rack ミドルウェアや around_action で
- 「リクエストの終わりで logger を flush する」という契約を前提としている
- 作者は別ブランチで「一定しきい値を超えたら同期書き込みに切り替える」backpressure 付き実装を用意しているが:
ractor_safegem への依存が必要- そのため Rails 本体には入れず、オプション扱いに留めている
3-3. 互換性と今後の改善余地
- 現段階では「ドキュメント未掲載の内部的/実験的 API」という位置付け
- 公開メソッドだが、将来的な API 変更の可能性がある
- 将来的な効果として期待されているもの:
Logger::LogDevice内の mutex を無効化できる可能性(ログデバイスへの書き込みを Writer スレッドに集約するため)- これにより、マルチスレッド環境での logger 周りのロック競合を減らせるかもしれない
3-4. 導入時の実務的な注意点
- Ractor を本格利用していない通常の Rails アプリで導入する場合:
- 「本番での安定性・メモリ使用量」を慎重に観測しながらロールアウトするのが良さそう
- ログ量が多いバッチ処理・エンドポイントでは特に、flush のタイミングとキューのサイズに注意
- Ractor を使う場合:
- logger を Ractor に渡す前に、Ractor-shareable であることを確認する設計にする
- Ractor 内で logger の再構築を繰り返さず、極力共有して利用するほうがパフォーマンス上有利
- 参考情報 (あれば)
- この PR の説明内で参照されている follow-up ブランチ(backpressure 付き実装案)
- 関連するクラス/ファイル
ActiveSupport::TaggedLoggingshareable_loggerが追加され、Ractor 対応 logger を生成
activesupport/lib/active_support/ractors/logger/writer.rb- バックグラウンド Writer スレッド本体
activesupport/lib/active_support/ractors/logger/device_proxy.rb- Ractor から Writer への非同期ブリッジ
activesupport/test/ractor_logger_test.rb- 具体的な利用・挙動を把握するのに有用なテストケース群
#57937 Load database config when a connection is absent from the shared section
マージ日: 2026/6/30 | 作成者: @55728
- 概要 (1-2文で)
database.ymlでsharedセクションに存在しない接続名が環境ごとに定義されている場合、これまではNoMethodErrorで落ちていたのを、「その接続は shared マージ対象外として素通りさせる」ように修正した PR です。これにより、3階層構造のdatabase.ymlをより柔軟に書けるようになり、安全にロードできるようになりました。
- 変更内容の詳細
何が問題だったか
3階層の database.yml を以下のように書いたケースを考えます:
shared:
one:
migrations_path: "db/one"
development:
one:
adapter: sqlite3
two: # `shared`にtwoが無い
adapter: sqlite3Rails は Rails.application.config.database_configuration を構築する際に、
sharedセクションに同名のサブセクションがあれば、それを各環境の同名接続にマージする- 例:
shared.oneをdevelopment.oneにマージする
という処理を行っています。
しかし実装上、shared 側に 存在しない 接続名に対しても無条件に merge を呼んでおり、shared[:two] が nil なのに merge しようとして、
NoMethodError: undefined method `merge` for nilが発生していました。
このため、shared では一部の接続だけに共通設定を定義し、他の接続は shared 未定義のまま個別設定で使う、というごく自然な使い方ができませんでした。
どう直したか
railties/lib/rails/application/configuration.rb の shared マージ処理の中で、
- shared 側に該当のキーがあるときだけ
mergeする - shared 側にキーが無い場合は、その接続設定は何もせず「そのまま残す」
ように条件分岐を追加しています(実質 +1/-1 の小さな修正)。
疑似コードで表すと:
# 変更前(イメージ)
merged = env_config.merge(shared_config[name]) # shared_config[name] が nil でも merge する
# 変更後(イメージ)
if shared_config[name]
merged = env_config.merge(shared_config[name])
else
merged = env_config # sharedに無ければそのまま
endこれにより、次のような挙動になります:
Rails.application.config.database_configuration
# Before: NoMethodError: undefined method 'merge' for nil
# After: 正常にロードされ、"two" は shared のマージを受けずにそのまま残るテストの追加
railties/test/application/configuration_test.rb にテストが追加されており、
sharedに存在する接続 (one) には共通設定がマージされるsharedに存在しない接続 (two) は例外を出さず、そのままの設定で残る
ことを確認しています。行数的に見ると、上記の YAML 構造に近い形のテストケースが1つ追加された形です。
- 影響範囲・注意点
影響範囲
config/database.ymlで 3階層 (shared+ 各環境 + 接続名) 構造を使っているアプリケーションに影響します。- 特に、
sharedが「一部の接続だけ」を定義しているプロジェクトで、これまでNoMethodErrorに遭遇していたケースは、この修正で解消されます。 sharedにすべての接続名を定義していたプロジェクトでは挙動は変わりません。
互換性 / 注意点
- shared 側に無い接続名は、今後も shared の設定を一切マージされず「完全に独立した接続」として扱われます。
- もし「本当は shared にも書くつもりだったが、書き忘れていた」ような設定ミスは、この変更では検出されず、静かに素通りします。
- そのようなミスを検出したい場合は RuboCop などで独自ルールを追加するか、CI で
database.ymlの検証を自前で行う必要があります。
- Rails 本体としては「shared に存在しない接続はマージ対象にしない」という仕様を明確化したと言えます。
- shared 側に無い接続名は、今後も shared の設定を一切マージされず「完全に独立した接続」として扱われます。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57937
- 関連しそうなキーワード:
- 3-tier database.yml (
shared/development/ 接続名) - マルチ DB 構成 (
primary,replica,readonlyなど) Rails.application.config.database_configurationの解決ロジック
- 3-tier database.yml (
#57946 Allow the Evented LogSubscriber to be shareable
マージ日: 2026/6/30 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Evented な LogSubscriber(および関連する StructuredEventSubscriber)の実装から「Proc を含む定数ハッシュ」を排除し、Ractor でも共有可能(shareable)になるようにリファクタリングした PR です。Ractor.shareable_procを使わずに、設計側で Proc 依存を解消することで Ractor セーフティを高めています。
- 変更内容の詳細
※実際のコードを要約した概念的な説明です。
背景となる問題
- もともと
ActiveSupport::EventReporter::LogSubscriber(とそれに関連するActiveSupport::StructuredEventSubscriber)には、- 「イベント名 → 処理内容」を表す 定数ハッシュ が存在
- そのハッシュの値として Proc オブジェクト が格納されていた
- 定数自体は
freezeされていても、中に非 shareable なオブジェクト(Proc)があるため、Ractor 間で安全に共有できない状態だった。 Ractor.shareable_procでラップすれば shareable にできるが、- そのたびにラップするのが煩雑
- コードの可読性も下がる
という理由で、設計レベルで Proc を定数ハッシュから排除する方向にリファクタリングしている。
主なリファクタリング内容
変更ファイルは次の2つです。
activesupport/lib/active_support/event_reporter/log_subscriber.rb(+10/-6)activesupport/lib/active_support/structured_event_subscriber.rb(+2/-4)
おおまかな方向性は以下の通りです。
定数ハッシュから Proc を排除
以前は、例えばこういった形だったものを(※イメージです):rubyEVENT_PROCESSORS = { "sql.active_record" => ->(event) { process_sql_event(event) }, "render_template.action_view" => ->(event) { process_render_event(event) }, }.freezeこれを Proc を値に持たない形 に変更し、
- シンボルや識別子だけを格納する
- 実際の処理はメソッドディスパッチなどで行う
という設計に変えています。例えば次のような形に近づけている可能性が高いです:
rubyEVENT_PROCESSORS = { "sql.active_record" => :process_sql_event, "render_template.action_view" => :process_render_event, }.freeze def call(event) if (handler = EVENT_PROCESSORS[event.name]) send(handler, event) end endこうすることで、
EVENT_PROCESSORSは- 文字列/シンボルのみで構成される
- 完全に shareable なオブジェクト になり、Ractor 間で安全に共有できます。
Evented LogSubscriber のコードパスを Ractor 安全に
- Evented なログ購読ロジック(通知を受け取り、ログ出力や構造化イベントに変換する部分)が、
- Ractor 内から利用される
- かつグローバルに共有される可能性
を考慮して、「共有しても安全なオブジェクト(Ractor.shareable)」だけを定数として持つようになっています。
StructuredEventSubscriber側でも、同じ定数ハッシュや共通ロジックを参照している部分があるため、そこも Proc 非依存に合わせて微調整されています(+2/-4 程度の小修正)。
- Evented なログ購読ロジック(通知を受け取り、ログ出力や構造化イベントに変換する部分)が、
Ractor.shareable_procを使わない構成にRactor.shareable_procは、ブロックや Proc を Ractor 間共有可能にするための仕組みですが、- ラップが散在しがち
- 「どこまでが shareable でどこからが非 shareable か」が読み取りにくくなる
- この PR では、それを使う代わりに
- 「そもそも定数の中に Proc を入れない」
- 「Proc 的な振る舞いはインスタンスメソッドやクラスメソッドのディスパッチに寄せる」 方針で整理しています。
- 影響範囲・注意点
- 主な影響範囲
ActiveSupport::EventReporter::LogSubscriberActiveSupport::StructuredEventSubscriber
- これらを直接継承・拡張しているコードが、
- 定数ハッシュに Proc を追加・上書きしている
- 内部の実装に強く依存している 場合は、挙動の差異や互換性に注意が必要です。
- 公開 API としては、「イベント名に応じてログ/構造化イベントが処理される」という仕様自体は変わらない想定です。
- この PR 自体では テストは追加・更新されていない ため、
- Ractor を使う環境での Evented ログ出力
- StructuredEventSubscriber 経由のイベント処理
などを実際のアプリケーション・本番に近い環境で確認するのが無難です。
- 今後、Ractor を前提とした並行実行(例: 複数 Ractor から ActiveSupport のイベントを購読・ログ出力する)を行う際の 前提条件 として、この変更が効いてきます。
- 参考情報 (あれば)
- Ruby の Ractor と shareable オブジェクトの仕様:
- RDoc: https://docs.ruby-lang.org/en/master/Ractor.html
Ractor.shareable?,Ractor.make_shareable,Ractor.shareable_procなど
- Rails 内での関連コード:
ActiveSupport::LogSubscriberActiveSupport::Notificationsこれらと Evented LogSubscriber / StructuredEventSubscriber は密接に連携しているため、イベント処理まわりを拡張する場合はソースを一緒に読むと理解しやすいです。
#57816 Set browser binary when preloading system test driver
マージ日: 2026/6/30 | 作成者: @titusfortner
- 概要 (1-2文で)
Rails の system test で Selenium ドライバをプリロードする際に、Chrome/Firefox のブラウザ実行ファイルの場所(options.binary)が設定されず「cannot find Chrome binary」で落ちる問題を修正する PR です。
Selenium 側の仕様変更(DriverFinderがoptionsをミューテートしなくなった)に追従し、Rails 側で明示的にブラウザバイナリを設定するようにしています。
- 変更内容の詳細
背景
- Rails の system test で Selenium ドライバを「プリロード」する際、
action_dispatch/system_testing/browser.rbのBrowser#preloadが Selenium のChrome::Service.driver_path/Firefox::Service.driver_pathをグローバルに設定していました。 - 以前は Selenium の
DriverFinderが- ドライバのパス解決(
driver_path) - ブラウザ実行ファイルパスの設定(
options.binary) の両方を行っており、options.binaryを書き換える副作用に依存していました。
- ドライバのパス解決(
- Selenium 4.45 で
DriverFinderがoptionsをミューテートしなくなり、その副作用が消滅。- Rails 側では
Service.driver_pathだけが設定される - その結果、各 worker がドライバを作るとき Selenium Manager がスキップされ、「ドライバパスだけ判明し、ブラウザパスは未設定」の状態になる
options.binaryが空のままになり、「cannot find Chrome binary」で失敗する
- Rails 側では
この PR の対応
ポイントは「ドライバパスとブラウザパスを同じ DriverFinder から取得し、options.binary を Rails 側で明示的に設定する」ことです。
actionpack/lib/action_dispatch/system_testing/browser.rb の変更:
- これまで:
Browser#preload内でDriverFinderを呼び出してService.driver_pathを設定DriverFinderが内部で勝手にoptions.binaryも設定することに依存
- これから:
- 同じ
DriverFinder呼び出しで、ドライバパスとブラウザパスを両方取得 - 取得したブラウザパスを
options.binary = <resolved browser path>として明示的に代入
- 同じ
疑似コードイメージは以下のような形になります(実際の API 名は Selenium 実装に依存しますが、やっていることのイメージとして):
def preload
# options は system test 用の Selenium::WebDriver::Chrome::Options など
options = build_options
# DriverFinder からドライバとブラウザの両方を解決
driver_finder = Selenium::WebDriver::DriverFinder.new(browser: :chrome, options: options)
driver_path = driver_finder.driver_path
browser_path = driver_finder.browser_path # ここからブラウザパスも取得(新しく明示的に利用)
# ドライバパスをグローバル Service に設定(従来どおり)
Selenium::WebDriver::Chrome::Service.driver_path = driver_path
# ブラウザバイナリを明示的に設定(新しく追加された部分)
options.binary = browser_path
# …以降、プリロード完了
end※上記は PR の説明から再構成したイメージコードです。実際のメソッド名や引数は Selenium の実装に従います。
actionpack/test/dispatch/system_testing/driver_test.rb の変更:
- テスト側で、「プリロード時に
options.binaryが正しく設定されること」を検証するテストが追加・更新されています。 - 具体的には:
- これまで「ドライバパスが解決されているか」を見ていた箇所に加えて
- 「ブラウザバイナリパス(
options.binary)も期待どおりに設定されているか」をチェックするようになっています。
- 行数ベースでは +15/-11 程度の比較的小さな追加・修正で、既存ケースの調整+新アサーションの追加と考えられます。
- 影響範囲・注意点
- 影響対象:
- Rails の system test を Selenium(Chrome / Firefox)で動かしていて
config.system_testingで「ドライバのプリロード」を有効にしているプロジェクト- かつ Selenium 4.45 以降(
DriverFinder仕様変更後)を使っている環境
- この PR により:
- 「cannot find Chrome binary」などのエラーで system test が失敗していたケースが解消される見込みです。
- ドライバとブラウザのパス解決が同じ
DriverFinderによって一貫して行われるため、Selenium の>= 4.20.0サポート範囲内で後方互換性が保たれています。
- 注意点:
- 独自に
options.binaryを上書きしている(例: 特定の Chrome/Firefox バイナリを明示指定している)場合は、Rails 側のプリロード処理との競合がないか確認するとよいです。- ただし、この PR は「
DriverFinderが解決したブラウザパスを使う」だけなので、ほとんどのケースでは既存の期待どおりに動作します。
- ただし、この PR は「
- CI などで「Selenium Manager 無効化」「独自のドライバ/ブラウザ配置」をしている場合にも、
DriverFinderベースの解決に統一されるため、パス解決ロジックの挙動を一度確認しておくと安心です。
- 独自に
- 参考情報 (あれば)
- 元の Selenium 側 Issue:
https://github.com/SeleniumHQ/selenium/issues/17698
→DriverFinderがoptionsをミューテートしなくなった変更と、それによりoptions.binaryが設定されなくなる問題の報告。 - この PR は既存の Selenium サポート範囲(
selenium-webdriver >= 4.20.0)と互換性を保つように実装されています。
#57918 Make default scopes ractor safe
マージ日: 2026/6/30 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveRecord::Base.default_scopeの実装を見直し、デフォルトスコープを Ractor セーフ(スレッド/Ractor間で共有しても安全)にするために、default_scope の内部状態を freeze して不変オブジェクトとして扱うようにした PR です。加えて、この挙動を保証するテストが追加されています。
- 変更内容の詳細
何をしたか
ActiveRecord::Base.default_scopeが内部的に保持している- 「default scope の値オブジェクト」
- 「
default_scopes配列」 を、基本的に「凍結(freeze)」して不変オブジェクトとして扱うように変更。
default scope のブロック(proc)は
instance_execで評価されるため、ユーザーにとっても Ractor 共有可能な(Ractor.shareable?な)オブジェクトにしやすいと判断し、そこを前提に安全に共有できるようにしている。default_scopesに新しい要素を追加する際は、- 既存配列を
dup - 新しい scope を
append(push) - その結果の配列を
freeze
という手順で、「配列自体もイミュータブル」となるようにしている。
- 既存配列を
疑似コードイメージ(実際のコード構造を簡略化したイメージです):
# 変更前(イメージ)
def default_scope(scope = nil, &block)
self.default_scopes ||= []
new_scope = DefaultScope.new(scope || block)
self.default_scopes << new_scope # 既存配列を破壊的に変更
end
# 変更後(イメージ)
def default_scope(scope = nil, &block)
new_scope = DefaultScope.new(scope || block).freeze # 値オブジェクトをfreeze
# default_scopes 配列は、常に freeze された新しい配列オブジェクトに差し替える
scopes = (self.default_scopes || []).dup
scopes << new_scope
self.default_scopes = scopes.freeze
endテストの追加
activerecord/test/cases/scoping/default_scoping_test.rb にテストが追加され、主に以下のようなことが確認されていると考えられます(PR説明と統計から推測):
default_scope呼び出し後に、内部で保持される default scope の値オブジェクトがfrozen? == trueであること。default_scopes配列自身もfrozen? == trueであること。- 複数回
default_scopeを呼び出しても、配列の freeze が維持されつつ、新しい要素が正しく追加されること(dup + append + freezeの挙動が正しいこと)。
- 影響範囲・注意点
影響範囲
- 対象は
ActiveRecord::Base.default_scopeの内部実装 およびそれに依存するクラスです。 - 特に以下のようなケースに関わります:
default_scopeを多用するモデル- Ractor を使った並列実行(Ruby 3 以降)を意識しているアプリ / ライブラリ
default_scopeの内部状態(default_scopes配列など)に直接アクセス・変更しているメタプログラミング的なコード
注意点
- 破壊的変更の可能性:
default_scopes配列が常にfreezeされるようになるため、Model.default_scopes << somethingModel.default_scopes.push(...)Model.default_scopes[0] = ...
といった 配列を直接書き換えるコードは
FrozenErrorで落ちる 可能性があります。これらは本来 Rails が想定していない内部構造への依存なので「非推奨な使い方」ではありますが、メタプログラミングで触っている場合は注意が必要です。
- default scope の「値オブジェクト」も
freezeされるため、- もしライブラリ側で独自に
default_scopeの値オブジェクトを取り出して mutate(プロパティを書き換え)しているような場合も同様にFrozenErrorのリスクがあります。
- もしライブラリ側で独自に
- 通常の使い方(
default_scope { where(active: true) }など)をしているアプリケーションにとっては、挙動としてはほぼ影響なく、Ractor セーフになる分だけ安全性が向上します。
- 参考情報 (あれば)
- Ruby の Ractor と共有可能オブジェクト:
Ractor.shareable?(obj)がtrueになる条件として、「オブジェクトが再帰的にすべて frozen である」ことが重要です。今回の変更はdefault_scope関連オブジェクトをその条件に近づけるものです。
- Rails 内の類似の freeze 戦略:
- 最近の Rails では、設定オブジェクトや Arel ノード、relation 関連オブジェクトで freeze を積極的に使うことで、Ractor セーフ / スレッドセーフな実装を進めており、その一環と見ることができます。
#57930 Only document the public ActionView::DependencyTracker API [ci skip]
マージ日: 2026/6/30 | 作成者: @etiennebarrie
- 概要 (1-2文で)
ActionView::DependencyTracker のうち、実際に外部から利用されている「公開API」のみを公式ドキュメントに載せるように整理した PR です。register_trackerとERBTrackerを正式にドキュメント対象とし、それ以外の内部実装は引き続き非公開(:nodoc:)とすることで、サポート対象のエントリポイントを明確にしています。
- 変更内容の詳細
2-1. 何が「公開API」として文書化されたか
この PR では以下が「公式にサポートされる public API」として RDoc/YARD などの生成ドキュメントに現れるようになります。
ActionView::DependencyTrackerActionView::DependencyTracker.register_trackerActionView::DependencyTracker::ERBTracker
一方で、次は依然として内部用実装と位置付けられ、:nodoc: のままです。
find_dependenciesremove_tracker- 各種 tracker 用の正規表現(依存関係解析ロジックの詳細)
RubyTrackerクラス
(実際のコード変更は主にこれらに対するコメント・:nodoc: の付け外しであり、仕様や挙動を変えるロジック変更ではありません。)
2-2. register_tracker の役割
register_tracker は「新しいテンプレート言語の依存関係を ActionView に教える」ための公式な拡張ポイントです。
概ね以下のような API だと考えられます(※PR 文脈と既存の coffee-rails / haml-rails からの推測であり、実際のシグネチャは Rails 本体を参照してください):
# 例: foo という独自テンプレート拡張子向けに tracker を登録する
ActionView::DependencyTracker.register_tracker(
:foo, # テンプレートタイプ / 拡張子
MyApp::ViewDependency::FooTracker # 依存関係解析ロジック
)こうすることで、render の際に *.foo テンプレートから参照される partial / template などの依存関係を Rails が把握し、キャッシュの失効や再読み込みに反映できるようになります。
PR 説明でも触れられているように、すでに以下の gem がこの API を利用しています:
- coffee-rails
https://github.com/rails/coffee-rails/blob/v5.0.0/lib/coffee/rails/template_handler.rb#L25 - haml-rails
https://github.com/haml/haml-rails/blob/v3.0.0/lib/haml-rails.rb#L34
これらはすでに実運用されている事実上の public API でしたが、クラスごと :nodoc: になっていたため「非公開扱い」になっていました。今回それを公式に「ここが拡張ポイントです」と宣言する形です。
2-3. ERBTracker の役割
ActionView::DependencyTracker::ERBTracker は
- 「Ruby と同じような
render呼び出しを行うテンプレート言語」- 例:
render "partial",render partial: "users/user", locals: { user: @user }等
- 例:
- そのテンプレートから、どの partial / template が使われているかを解析して依存関係を抽出
するための再利用可能な tracker クラスです。
PR 説明によると、ERBTracker は「gem が自前のテンプレート言語を実装していても、その render 呼び出しが Ruby に似ているなら、そのまま使える共通の tracker」として想定されています。
例: 独自テンプレート *.foo が実質的に ERB と同じ記法で render を呼んでいるなら、
ActionView::DependencyTracker.register_tracker(:foo, ActionView::DependencyTracker::ERBTracker)のように再利用すれば、独自に依存関係解析ロジックを書かなくてもよくなります。
今回の PR によって ERBTracker 自体の挙動は変わっていませんが、「ここを使ってよい・ここは拡張してよい」という意味での公式ドキュメントが追加され、サポート範囲が明文化されています。
2-4. 内部APIの非公開化の継続
find_dependencies・remove_tracker・RubyTracker・内部の正規表現などは引き続き :nodoc: とされ、API ドキュメント生成の対象外になっています。これは
- これらを「いつでも変更可能な内部実装」と位置付ける
- gem 作者に対して「ここを直接触ると将来壊れる可能性が高い」とメッセージする
という意味があります。
- 影響範囲・注意点
実行時の挙動への影響は基本的にありません
変更はほぼドキュメントレベルであり、依存関係追跡のロジックやインターフェースのシグネチャ変更は含まれていません。すでに
register_tracker/ERBTrackerを使っている gem / アプリは、そのまま利用継続可能です
これまで「実質 public だがドキュメント上は private」という微妙な状態だったものが、正式に public API として認められた形です。将来的な後方互換性も、他の public API と同様のポリシーで扱われる期待が持てます。逆に、
find_dependenciesやRubyTracker等の内部 API に依存している場合は要注意
これらは今回も明確に非公開扱いとされました。- もし独自 gem 等で直接呼んでいる場合は、
register_trackerと独自 Tracker クラス、もしくはERBTrackerの再利用に移行すべきです。 - 将来的な Rails のマイナーアップデートで破壊的変更を受ける可能性があります。
- もし独自 gem 等で直接呼んでいる場合は、
ドキュメント生成時の見え方が変わります
Rails の API ドキュメントサイト(api.rubyonrails.org等)で、ActionView::DependencyTrackerとregister_tracker/ERBTrackerが見えるようになり、拡張方法がわかりやすくなります。
- 参考情報 (あれば)
register_trackerを利用している既存 gem の例:実際に独自テンプレートエンジンを実装する場合のざっくりした構成イメージ:
# 独自テンプレートエンジン Foo の Tracker
class FooTracker
# ActionView が呼び出す想定のインターフェース
def self.call(name, template)
# template.source を解析して render 呼び出しを拾い、
# 依存しているテンプレートパスの配列を返す等
# ここは Rails 本体の実装/Doc を参照して合わせる
end
end
# 初期化時に tracker 登録
ActionView::DependencyTracker.register_tracker(:foo, FooTracker)
# もしくは render 構文が Ruby/ERB とほぼ同じなら:
ActionView::DependencyTracker.register_tracker(:foo, ActionView::DependencyTracker::ERBTracker)独自エンジンを作る側は、今後はこのあたりが「公式なやり方」としてドキュメントで参照できるようになります。
#57932 Minor Active Support Cache refactorings
マージ日: 2026/6/30 | 作成者: @byroot
- 概要 (1-2文で)
ActiveSupport::Cache 周りの内部実装を少し整理したリファクタリング PR です。instrument/ログ出力処理と、ローカルキャッシュ (LocalCache) のincrement挙動が単純化され、不要なヘルパーやメソッドが削除されています。
- 変更内容の詳細
2-1. ActiveSupport::Cache#instrument の整理
やったこと
- 内部ヘルパー
_instrumentを削除し、instrumentの処理を直接書く形に変更。 - ログ出力において
logger.levelを手動で確認するのではなく、Logger#debugのブロック形式を利用するように変更。
目的・効果
_instrumentはinstrumentからしか呼ばれておらず、処理もほぼ同じだったため、メソッド分割による見通し改善よりも「追いにくさ」の方が勝っていたと判断して削除されています。logger.debug { ... }形式にすることで、デバッグログが無効なときにはブロックが評価されず、メッセージ構築コストを自動的に避けられます。
これにより、手動でif logger.debug?のようなレベルチェックを書く必要がなくなります。
イメージコード(概念的な Before / After)
Before(イメージ):
def instrument(operation, key, options = nil)
payload = { key: key }
_instrument(operation, payload, options) do
# 実処理
end
end
def _instrument(operation, payload, options)
if instrument_notifications?
ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) do
result = yield
logger.debug("Cache #{operation}: #{payload[:key]}") if logger && logger.debug?
result
end
else
yield
end
endAfter(イメージ):
def instrument(operation, key, options = nil)
payload = { key: key }
if instrument_notifications?
ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) do
result = yield
logger&.debug { "Cache #{operation}: #{payload[:key]}" }
result
end
else
result = yield
logger&.debug { "Cache #{operation}: #{payload[:key]}" }
result
end
end※ 上記は概念を伝えるための擬似コードであり、実際のコードとは細部が異なります。
2-2. Local Cache (ActiveSupport::Cache::Strategy::LocalCache) の increment の簡略化
変更点
- ローカルキャッシュ戦略における
increment実装がシンプルになった。 bypass_local_cacheメソッドが削除された。
以前の挙動(概念)
LocalCache 戦略は、プロセス内で使う「スレッドローカルのキャッシュ」のようなもので、元のストア(例: Redis, Memcached, MemoryStore)をラップしています。
以前は、increment を行う際に、
- 「ローカルキャッシュをバイパスして元のストアに直接
incrementを送る」
ような処理をしていたため、bypass_local_cacheといったヘルパーが存在していました。
今回の簡略化後(概念)
incrementを行うときに、わざわざローカルキャッシュをバイパスせず、よりストレートに本来のストア実装にフォワードする形に整理。- その結果、
bypass_local_cache自体が不要になり削除されています。
テスト側 (local_cache_behavior.rb) の変更から読み取れること:
increment実行時のローカルキャッシュとの整合性や、複数回呼び出し時のカウンタ挙動などについてテストが拡充されているため、挙動は維持・明確化された上で内部構造だけがシンプルになった、という位置づけです。
2-3. その他の小さな変更
memory_store.rbの微修正 (+1/-1):
ログ周り、あるいはincrement関連で統一的な呼び出しに合わせた小さなコード調整と思われます。- Redis ベースのキャッシュストアテスト
deprecated_redis_cache_store_test.rbredis_cache_store_test.rb
の両方で 1 行ずつ変更されています。主にincrementの扱い、もしくはローカルキャッシュを経由した場合の振る舞い確認のための微修正です。
- 影響範囲・注意点
外部 API の互換性
- 公開 API (
ActiveSupport::Cache::Store#increment,#fetch, etc.) の振る舞いは変えずに、内部実装を整理したリファクタリングです。 - 直接
_instrumentやbypass_local_cacheに依存していなければ、通常のアプリケーションコードに影響はありません。
- 公開 API (
モンキーパッチ / 内部依存している場合
ActiveSupport::Cacheに対して独自拡張・モンキーパッチをしている場合、以下を確認してください:_instrumentにフックしていたコードがあれば破綻します。ActiveSupport::Cache::Strategy::LocalCache::LocalStoreなどの内部クラス/bypass_local_cacheに依存していた場合も同様です。
- 特にキャッシュミドルウェアや独自キャッシュストアを書く際に、内部メソッドに依存していると影響を受ける可能性があります。
ロギング周り
- これまで
logger.debug?判定を前提にしていたようなモンキーパッチと組み合わせている場合、logger.debug { ... }形式への移行によって挙動が微妙に変わる(ログメッセージ生成タイミングなど)可能性はありますが、通常は問題にならない範囲です。
- これまで
テスト
- Local Cache 挙動を検証するテストが増えているため、
incrementの意味的な挙動は今後もこの PR の形が前提になります。 redis_cache_store関連テストも微修正されており、Redis + LocalCache 併用環境でも、想定どおりにincrementが動くことが確認されています。
- Local Cache 挙動を検証するテストが増えているため、
- 参考情報 (あれば)
Rails ガイド(キャッシュ)
https://guides.rubyonrails.org/caching_with_rails.html
ActiveSupport::Cache 全般の利用方法。今回の変更は主に内部実装であり、このガイドに書かれた使い方はそのまま有効です。ActiveSupport::Notifications とログの連携
多くのキャッシュ操作はcache_read.active_supportなどの通知イベントとして出るため、APM や独自メトリクス収集をしている場合はinstrument実装の読みやすさ向上がメンテナンス性に寄与します。
#57931 Implement ActiveSupport::ProxyLogger
マージ日: 2026/6/30 | 作成者: @byroot
- 概要 (1-2文で)
RailsのActiveSupportにActiveSupport::ProxyLoggerが追加され、既存のロガーにログを委譲しつつ「別のログレベルで」フィルタリングできるラッパーロガーが使えるようになりました。これにより、アプリ全体と同じ出力先を使いつつ、特定ライブラリだけログを抑制・制御することが容易になります。
- 変更内容の詳細
2-1. ProxyLoggerとは何か
ActiveSupport::ProxyLogger は「あるロガー(親ロガー)」へログ出力を“丸ごと委譲”しつつ、自身が持つ独立したログレベルでログを間引くクラスです。
SomeLibrary.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error)この例では:
- 実際の出力先・フォーマットなどは
Rails.loggerをそのまま利用 - しかし
SomeLibraryから出るログは「error 以上」に制限される
→infoやdebugは、ProxyLogger 側で捨てられる
アプリ本体のログレベル(例: :info)と、ライブラリのログレベル(例: :error)を分けられるのが主な目的です。
2-2. 対応しているインターフェース
PR 説明によると、標準ライブラリ Logger のインターフェースのほとんどをサポートしています。典型的には以下のようなメソッドが委譲・対応されていると考えてよいです(実装ファイルは active_support/proxy_logger.rb):
- レベル指定メソッド:
debug,info,warn,error,fatal,unknown
- 汎用ログメソッド:
add(severity, message = nil, progname = nil, &block)
- ログレベル関連:
level,level=,debug?,info?,warn?,error?,fatal?
- その他
Logger互換・ActiveSupport::Logger 互換のメソッドがほぼそのまま使える想定
挙動のイメージ:
# Rails.logger は通常通りアプリ全体のロガー
Rails.logger.level = :info
# noisy ライブラリはログが多いので、ライブラリ側だけ error 以上に絞る
NoisyLibrary.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error)
NoisyLibrary.logger.info("This will NOT be logged") # ProxyLogger のレベル :error により破棄
NoisyLibrary.logger.error("Something bad") # Rails.logger へ error として出力親ロガー (Rails.logger) のログレベルや出力先には影響を与えず、ProxyLogger 側だけでフィルタリングしているのがポイントです。
2-3. 実装位置・その他の変更
- 新クラス:
activesupport/lib/active_support/proxy_logger.rbに実装(176行追加)
- 読み込み:
active_support.rbにrequireが追加され、ActiveSupport をロードすれば利用可能
- 既存 Logger 周りの微調整:
active_support/logger.rbが 1 行だけ削除されており、内部の読み込み順や依存関係の整理と思われます
- テスト:
activesupport/test/proxy_logger_test.rbが追加され、主な挙動(ログレベル判定・委譲)がテストされています
- ドキュメント:
activesupport/CHANGELOG.mdにエントリ追加
- 影響範囲・注意点
3-1. 影響範囲
- 新機能であり、既存コードは基本的に非互換変更の影響を受けません。
- 既存の
Rails.logger/ActiveSupport::Loggerの挙動も変わりません。 - 外部ライブラリや自前コードで「ロガーを差し替えるポイント」があれば、そこで
ProxyLoggerを差し込むことができます。
3-2. 利用時の注意点・設計上の考慮
ログレベルの二重管理
- 親ロガーと ProxyLogger の両方にログレベルが存在します。
- 実際に出力されるかどうかは「ProxyLogger のレベル判定を通過し」「かつ親ロガー側で弾かれない」場合のみです。
- 例: 親が
:error、ProxyLogger が:infoだと、infoログは ProxyLogger 側で通っても親で捨てられます。
出力先・フォーマット変更は親ロガー側で行う
ProxyLoggerは基本的に委譲しかしないため、- ログの出力先(ファイル・標準出力・外部サービスなど)
- フォーマッタ、タグ付け、ログサブスクライバ
などはすべて「親ロガー側」で設定・制御します。
ライブラリのロガー差し替えが可能か確認
SomeLibrary.logger = ...のようにロガーを外から渡せる前提の仕組みを持つライブラリでのみ、この機能が直接活きます。- Rails の各種コンポーネント(ActiveRecord, ActionController など)や、Sidekiq など多くの gem がこのパターンを取っています。
パフォーマンス観点
- ProxyLogger で枝刈りすることにより、特に noisy なライブラリが
debugやinfoを大量に発行する場合に、出力やフォーマット処理の負荷を下げられます。 - ただしログメッセージ構築自体(文字列連結など)が呼び出し元で行われている場合、そのコストは残るため、可能ならブロック形式のログ (
logger.debug { expensive_computation }) を使うのが望ましいです。
- ProxyLogger で枝刈りすることにより、特に noisy なライブラリが
- 参考情報 (あれば)
- 追加されたクラス:
activesupport/lib/active_support/proxy_logger.rb
→ 実装詳細・対応しているメソッド一覧を確認可能 - 変更履歴:
activesupport/CHANGELOG.mdの ActiveSupport セクション - 利用イメージまとめ:ruby
# アプリ本体 Rails.logger.level = :info # ライブラリAはログがうるさいので error 以上だけ LibraryA.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error) # ライブラリBは debug も見たい LibraryB.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :debug)
このPRにより、「同じ出力先を共有しつつ、コンポーネントごとにログレベルを変える」というパターンが、Rails 標準の API だけでシンプルに書けるようになりました。
#57874 Make the Action View template handler registry Ractor-shareable
マージ日: 2026/6/30 | 作成者: @etiennebarrie
- 概要 (1-2文で)
Action View のテンプレートハンドラのレジストリを Ractor で共有可能(Ractor-shareable)にするため、内部実装がクラス変数ベースからモジュールインスタンス変数+不変(frozen)な構造に再設計された PR です。これにより、テンプレート検索・描画パスでのハンドラ参照が Ractor セーフかつスレッド安全になり、読み取り時に書き込みが発生しないようになっています。
- 変更内容の詳細
2-1. レジストリの格納場所の変更
従来:
ActionView::Template::HandlersをTemplateにextendし、- ハンドラのレジストリ(extension → handler)は クラス変数 に保持されていました。
- そのため、Ractor 的には共有しづらく、「読み取り時にレジストリを書き換える」ようなパターンも残っていました(遅延メモ化など)。
変更後:
- レジストリは
Template::Handlersモジュールの モジュールインスタンス変数 に保持されます。 Handlersモジュール自体が Ractor 間で共有され、非メイン Ractor からも「共有可能な値」として安全に読めるように設計されています。Template側の API (Template.handler_for_extension,Template::Handlers.extensionsなど) は、モジュール上の実装に委譲する薄いラッパーになりました。Templateは依然としてHandlersを extend していますが、実体はモジュール側に寄せた形です。
2-2. ハンドラオブジェクトを Ractor-shareable に
Ractor 間で共有するには、保持されるオブジェクト(ハンドラ)自体も shareable である必要があります。この PR では以下のように処理しています。
組み込みハンドラ
- 例: ERB ハンドラ、Builder ハンドラなど
- これらのインスタンスを
freezeして不変オブジェクトに。 - 不変にすることで、Ractor 間で安全に共有可能となります。
:rubyハンドラ:rubyテンプレート用のハンドラはActiveSupport::Ractors.shareable_lambdaを使って生成。shareable_lambdaは、Ractor 間で共有できるProc/lambdaを作るための ActiveSupport ヘルパです。
アプリケーション側で登録される Proc ハンドラ
- アプリケーションコードから
ActionView::Template.register_template_handlerなどで登録されるProc/lambdaについては、ActiveSupport::Ractors.try_shareable_procを経由して登録します。
try_shareable_procは、渡された Proc を shareable にできる場合はそうし、そうでなければ(たとえばクロージャで非共有な外部オブジェクトを閉じている場合など)適切に扱うためのヘルパです。- これにより、「アプリケーションが登録したハンドラが Ractor 的に問題を起こす」ケースを減らしつつ、できるだけ共有可能にする方針になっています。
- アプリケーションコードから
2-3. レジストリ更新方式の変更(コピーオンライト & イager 計算)
以前の問題点:
Template::Handlers.extensionsなどが「遅延メモ化(memoization)」しており、読み取りのタイミングで内部状態を書き換える可能性がありました。- Ractor・並行性の観点からは、読み取りが決して書き込みを行わない構造が望ましいです。
変更後:
レジストリ(extension → handler の Hash)は 登録時に新しい frozen Hash を作るコピーオンライト方式に変更されました。
例イメージ(実装イメージ・擬似コード):
rubydef register_handler(ext, handler) @handlers = @handlers.merge(ext.to_s => handler).freeze @extensions = @handlers.keys.freeze end
Template::Handlers.extensionsの内容(拡張子の一覧)も、- 登録時に毎回再計算して freeze
- 読み取りは計算済みの frozen オブジェクトを返すだけ
これにより、「読み取り時には一切書き込みが発生しない」設計となり、Ractor/並行実行での安全性と予測可能性が向上しています。
2-4. 初期登録の場所変更 (self.extended フックを排除)
従来:
TemplateがHandlersを extend する際のself.extendedフック(モジュールの extend 時に呼ばれるフック)内で、組み込みハンドラの初期登録を行っていました。
変更後:
- ハンドラのレジストリと登録ロジックはすべて
Handlersモジュール自身に集約されました。 - 組み込みハンドラは モジュール読み込み時に直接登録されます。
self.extendedフックは不要となり削除されています。- テスト用・公開 API としての
ActionView::Template.*インスタンスメソッドは、単にHandlersに委譲するだけです。
- テスト用・公開 API としての
2-5. Builder ハンドラの require メモ化方式変更
ActionView::Template::Handlers::Builder では、
- 以前は「
require "builder"をしたかどうか」を インスタンス変数でメモ化していました。 - しかし、この PR により Builder ハンドラも
freezeされるため、インスタンス変数への書き込み(=メモ化)が不可能になります。
そこで、
「
require "builder"が既に実行されたかどうか」をdefined?ガードで判定する方式に変えています。例イメージ(概念的なもの):
rubydef call(...) require "builder" unless defined?(::Builder) ... end
これにより、ハンドラインスタンス自体は完全に不変のまま、
requireの多重実行を避けることができます。
2-6. テストとツールの更新
actionview/test/template/handlers_test.rbに、Ractor-shareable な動作や新たな登録ロジックを検証するテストが追加。actionview/test/template/render_test.rbは、挙動変更に伴う小さな修正のみ。tools/strict_warnings.rbでも、今回の実装変更で発生する警告に対応するための微修正が入っています。
- 影響範囲・注意点
3-1. Ractor/並行実行まわり
- テンプレートハンドラのレジストリとハンドラオブジェクトが Ractor-shareable になったことで、
- 将来の Action View の Ractor 対応、
- マルチ Ractor でのレンダリング・テンプレート解決
への布石となります。
- 「読み取り時に書き込みが発生しない」ようになったため、従来まれに起こり得た競合や予期しない挙動がさらに減ります。
3-2. カスタムテンプレートハンドラを定義しているアプリケーションへの影響
アプリケーション側で Proc/lambda ベースのカスタムテンプレートハンドラを登録している場合:
- 登録処理は
ActiveSupport::Ractors.try_shareable_procを通るようになっています。 - 一般的なハンドラ(トップレベル定義のメソッドや、外部の Ractor 非共有な状態を閉じていないラムダ)であれば、そのまま問題なく動作するはずです。
- ただし、以下のようなケースでは Ractor-shareable にならない可能性があります:
- クラスインスタンス変数やスレッドローカルなど、「Ractor 非共有なオブジェクト」を閉じた Proc
- ミューテーブルな状態に依存するハンドラ
- その場合でも動作自体は維持される想定ですが、将来の完全な Ractor 対応の観点からは、
- ハンドラをできるだけ不変/再入可能に設計する、
- Ractor 非共有な状態を閉じないようにする
ことが推奨されます。
3-3. Builder ハンドラの挙動
Builderハンドラはrequire "builder"のメモ化の仕方が変わりましたが、- Ruby の
require自体が多重呼び出しに耐性があること、 - 実際には
defined?によるガードで一度だけ読まれることから、
- Ruby の
- アプリケーション側から見た挙動に変化は基本的にありません。
- 参考情報 (あれば)
- この PR で導入・利用されている Ractor 関連 API:
ActiveSupport::Ractors.shareable_lambdaActiveSupport::Ractors.try_shareable_proc
- 関連しそうなドキュメント/コード:
actionview/lib/action_view/template/handlers.rbactionview/lib/action_view/template/handlers/builder.rbactionview/test/template/handlers_test.rb
→ 新しいレジストリ実装の実例として読むと理解しやすいです。
#57916 Treat a Symbol stream name as a literal stream in channel test assertions
マージ日: 2026/6/30 | 作成者: @55728
- 概要 (1-2文で)
Action Cable のチャンネル用テストヘルパ (ActionCable::Channel::TestCase) において、シンボルで指定したストリーム名を本番動作と同じく「文字列に対応するリテラルなストリーム名」として扱うように修正した PR です。これにより、:my_roomと"my_room"を使ったストリーム/アサーションの挙動が一貫し、誤検知・見逃しが起きないようになります。
- 変更内容の詳細
背景: 本番環境での挙動
本番では以下のようなコードは:
stream_from :my_room内部的には "my_room" というリテラルなストリーム名に対して購読します。
つまり:
stream_from :my_room
stream_from "my_room"は同じストリームを指します。
また ActionCable.server.broadcast も、シンボル・文字列を同等に扱うストリーム名としてサポートしています。
問題となっていたテストアサーションの挙動
ActionCable::Channel::TestCase の以下のアサーションだけが、この仕様とズレていました:
assert_broadcasts(:my_room)assert_broadcast_on(:my_room)assert_has_stream(:my_room)assert_has_no_stream(:my_room)
1. assert_broadcasts / assert_broadcast_on の問題
シンボルで書いたときだけ、チャンネルスコープ付きの名前 に変換されていました。
assert_broadcasts(:my_room, 1) do
ActionCable.server.broadcast("my_room", { text: "hi" })
end
# エラーメッセージ:
# 1 broadcasts to my_channel:my_room expected, but 0 were sent.- 実際に送っているのは
"my_room"へのブロードキャスト - しかしアサーション側は
my_channel:my_roomという別のストリームを見てしまう
→ その結果、「0件しか送られていない」と誤判定。
文字列の場合は元々正しく動いており:
assert_broadcasts("my_room", 1) do
ActionCable.server.broadcast("my_room", { text: "hi" })
end
# これは成功していた2. assert_has_stream / assert_has_no_stream の問題
類似の問題がストリーム存在チェック系にもありました。
# 実際には stream_from :my_room が有効
assert_has_stream(:my_room)
# => "Stream my_room has not been started" で失敗逆に:
# 実際には stream_from :my_room が有効
assert_has_no_stream(:my_room)
# => 何も検出できず成功してしまう(false green)これは #57698 で指摘された assert_no_broadcasts と同じく、「テストが緑なのに本当は何も検証できていない」という危険な失敗パターンです。
具体的な修正内容
actioncable/lib/action_cable/channel/test_case.rb にて、以下のようなポリシーに統一:
シンボルで指定されたストリーム名は、テストアサーション内でも 等価な文字列として扱う。
つまり:
:my_room→"my_room"に変換し、チャンネルスコープを付けず にそのまま使う- これにより本番の
stream_from/broadcastと同じストリーム名解釈となる
結果として、次のように書いたとき:
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from :my_room
end
endテストで:
assert_has_stream :my_room # OK
assert_has_stream "my_room" # OK
assert_broadcasts :my_room, 1 do
ActionCable.server.broadcast(:my_room, text: "hi")
end
assert_broadcasts "my_room", 1 do
ActionCable.server.broadcast("my_room", text: "hi")
endといった書き方がいずれも期待どおり動きます。
テストの追加
actioncable/test/channel/test_case_test.rb に45行分のテストが追加され、主に以下をカバーしています:
- シンボル指定の
assert_broadcasts/assert_broadcast_onが、文字列指定と同じストリームを検証すること - シンボル指定の
assert_has_stream/assert_has_no_streamも、実際のストリーム開始状況と一致して判定されること - 既存の文字列パスが壊れていないこと
- 影響範囲・注意点
対象: Action Cable のチャンネルテスト (
ActionCable::Channel::TestCase) を書いているコードベース本番挙動への影響: なし(本番とテストのズレを埋める変更)
破壊的変更の可能性:
- これまで「たまたま」緑になっていたテストが、本来の挙動に合わせてレッドになる 可能性があります。
- 特に、以下のようなコードを書いていたプロジェクトは要注意です:ruby以前は「たまたま」
# 実際には my_channel:my_room にしかブロードキャストしていない stream_from "my_channel:my_room" # しかしテストで :my_room を想定していた assert_broadcasts :my_room, 1 do ActionCable.server.broadcast("my_channel:my_room", payload) end:my_roomがmy_channel:my_roomにスコープされて通っていたものが、
今後は"my_room"を指すようになるため失敗します。 - その場合は、明示的に望むストリーム名を文字列で指定する か、
テストと実装のストリーム名を揃える(:my_room/"my_room"いずれかに統一)必要があります。
ベストプラクティス:
- 本番コードでシンボルを使っているなら、テストも同じシンボル/文字列に揃える
- 「チャンネル名を含んだフルネーム」をあえて使いたいなら、必ず文字列で明示する:ruby
stream_from "chat_channel:my_room" assert_broadcasts "chat_channel:my_room", 1 do # ... end
- 参考情報 (あれば)
- PR本体: #57916 “Treat a Symbol stream name as a literal stream in channel test assertions”
- 関連PR:
- #57698: テストアダプタの
assert_no_broadcastsにおけるシンボル/文字列非対称問題の修正 - #57703, #57704: 同じファイル内の周辺レイヤーでの、シンボル vs 文字列非対称を個別に修正した PR(本PRで統合)
- #57698: テストアダプタの
#57912 Reference ActionCable::Configuration in the Action Cable guide
マージ日: 2026/6/30 | 作成者: @55728
- 概要 (1-2文で)
Action Cable ガイド内の「Configuration」セクションで参照していたクラス名を、古いActionCable::Server::Configurationから現在の正規名ActionCable::Configurationに修正したドキュメント更新です。バックワードコンパチ目的のエイリアスは残っていますが、ガイド上は新しい正規クラス名を指すようになりました。
- 変更内容の詳細
対象ファイル:
guides/source/action_cable_overview.md
変更内容は 1 行のみで、Action Cable の設定項目一覧へ誘導する箇所のクラス名を修正しています。
従来 (概念的なイメージ):
See `ActionCable::Server::Configuration` for the full list of configuration options.修正後:
See `ActionCable::Configuration` for the full list of configuration options.つまり、ドキュメント中で「全設定オプションの一覧は ActionCable::Server::Configuration を見てください」と案内していたのを、「ActionCable::Configuration を見てください」という案内に更新しただけの変更です。
Rails 本体側では、ActionCable::Configuration がトップレベルの正規クラスであり、ActionCable::Server::Configuration は互換性維持用のエイリアスとして残っている状態になっている前提です。この PR は、その実装にドキュメントを追従させたものです。
- 影響範囲・注意点
- 影響対象:
- 公式ガイドを見て設定方法を確認する開発者
ガイドに書かれているクラス名が、実際のコードベースが採用している正規の名前 (ActionCable::Configuration) と一致するようになります。
- 公式ガイドを見て設定方法を確認する開発者
- 実コードへの影響:
- コードの挙動や API 仕様そのものは変更されていません。
ActionCable::Server::Configurationは引き続きエイリアスとして存在するため、これを参照している既存コードが壊れることはありません。
- 注意点:
- 新規実装やリファクタリング時には、今後は
ActionCable::Configurationを前提にコードやドキュメントを書くのが推奨されます。 - 既存の記事・ブログ・社内ドキュメントなどで
ActionCable::Server::Configurationを紹介している場合、Rails の現行バージョンに合わせて表記だけActionCable::Configurationに更新するのが望ましいです。
- 新規実装やリファクタリング時には、今後は
- 参考情報 (あれば)
- Action Cable ガイド (Configuration セクション):
guides/source/action_cable_overview.mdの設定セクション - 関連する実装 (推定箇所):
ActionCable::Configurationクラス定義ActionCable::Server::Configuration = ActionCable::Configurationのようなエイリアス定義 (互換性維持用)
#57928 Eliminate a useless frame in AS::Notifications.instrument
マージ日: 2026/6/30 | 作成者: @byroot
- 概要 (1-2文で)
ActiveSupport::Notifications.instrument内の「無駄なスタックフレーム」を取り除き、スタックトレースやフレームグラフが少し読みやすくなるようにした変更です。性能への影響はほぼありませんが、デバッグやプロファイリング時の視認性が向上します。
- 変更内容の詳細
対象ファイル:
activesupport/lib/active_support/notifications.rb(+2/-2)
ActiveSupport::Notifications.instrument の実装から、実行上ほとんど意味を持たない「余計なメソッド呼び出し」や「余計なブロックラップ」を削り、スタックフレームを 1 段減らしています。
典型的には、以下のような構造になっていたものを:
def instrument(name, payload = {})
if notifier.listening?(name)
_instrument(name, payload) do
yield
end
else
yield
end
end
def _instrument(name, payload)
# 実際の計測・通知処理
endこれを、余計な中継レイヤをなくすような形に寄せています:
def instrument(name, payload = {})
if notifier.listening?(name)
# ここで直接計測処理を行う or 余計なラップを減らす
notifier.start(name, payload)
begin
yield
ensure
notifier.finish(name, payload)
end
else
yield
end
end実際の差分は +2/-2 行と非常に小さいもので、主な意図は「メソッドのネストを 1 段浅くする」「ブロック経由の無駄なコールを消す」ことで、結果的に:
- 例外発生時のバックトレースに出てくるフレームが 1 つ減る
- stackprof / rbspy / flamegraph などのプロファイリングツールでのコールスタック表示が少し簡潔になる
という効果があります。
- 影響範囲・注意点
- 公開 API (
ActiveSupport::Notifications.instrument) の挙動やシグネチャには変更なし - 通知されるイベント名・payload・タイミングも同一で、機能的な互換性は保たれています
- 変更はごく小さく、かつ「中継用のフレーム削除」にとどまるため、通常のアプリケーションコードが影響を受ける可能性はほぼありません
- backtrace を前提にした特殊な解析ツールや、スタックフレーム数に強く依存しているようなテストを組んでいる場合のみ、フレーム 1 つ分の違いが出る可能性があります
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57928
ActiveSupport::Notificationsのドキュメント(使い方の前提理解に):
https://api.rubyonrails.org/classes/ActiveSupport/Notifications.html
#57919 Make LogSubscriber.log_levels ractor safe
マージ日: 2026/6/30 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveSupport::LogSubscriberのsubscribe_log_levelが Ractor(マルチスレッド/マルチ Ractor)環境でも安全に動作するように、内部で持っているlog_levelsハッシュをフリーズするようにした PR です。- これにより、Ractor 間で
LogSubscriberを共有してもログレベル設定が原因でエラーやデータ競合が起きにくくなります。
- 変更内容の詳細
変更の主旨
元々、LogSubscriber には以下2点がありました:
- ログレベル判定用の定数
LEVEL_CHECKSはすでにfreeze済み - しかし、各サブスクライバごとに保持している
log_levelsハッシュはミューテーブルであり、Ractor セーフではない
この PR では、subscribe_log_level 呼び出し時に、この log_levels をフリーズするようにしています。すなわち、
- ハッシュを更新してから
freezeする - あるいは更新結果として新しい frozen ハッシュを持つようにする
という形を取ることで、Ractor が要求する「オブジェクトが不変であること」の条件を満たします。
具体的なイメージ
元のコード (イメージ):
def subscribe_log_level(*log_levels)
# self.log_levels は Hash
self.log_levels.merge!(derived_log_levels_from_args(log_levels))
endRactor では、Ractor 間で共有されるオブジェクトは基本的に frozen? である必要があるため、このままだと:
- ある Ractor で
log_levelsを更新 - 別の Ractor と共有しているときに Ractor セーフではなくなる
といった問題が起こりえます。
この PR では、以下のような形の変更がなされていると考えられます(正確なコードは PR 本文では省略されていますが、内容からの推測です):
def subscribe_log_level(*log_levels)
self.log_levels = self.log_levels.merge(derived_log_levels_from_args(log_levels)).freeze
endポイント:
merge!のような破壊的更新ではなく、新しい Hash をmergeで生成- 生成した Hash を
freeze log_levelsには常に frozen Hash を持たせるようにする
テスト (activesupport/test/log_subscriber_test.rb) も追加されており:
subscribe_log_level実行後にlog_levelsがfrozen?であること- 複数回呼び出しても正しいマージ結果と frozen 状態が保たれていること
といったことが検証されていると考えられます。
- 影響範囲・注意点
影響を受けるのは
ActiveSupport::LogSubscriberのsubscribe_log_levelを利用しているコードのみです。log_levelsが常に frozen Hash になるため、公開 API からlog_levelsを直接破壊的に書き換えていた場合はエラーになります。例(これまでなんとなく動いていたが、今後は壊れる可能性があるコード):
rubysubscriber = MyLogSubscriber.new subscriber.subscribe_log_level(:info) # こういった破壊的更新は frozen になった Hash に対しては例外を投げる subscriber.log_levels[:debug] = true # => FrozenError subscriber.log_levels.merge!(...) # => FrozenError望ましい書き方としては、
subscribe_log_levelの API を通して設定するか、どうしても差し替えたい場合は新しい Hash を作って setter に渡す形です:rubysubscriber.log_levels = subscriber.log_levels.merge(debug: true).freeze # ただし、このような使い方自体も将来の仕様変更に脆いので、可能なら避けるLogSubscriberを Ractor 間で共有するケース(あるいは Rails ロガー周りを Ractor 内で使うケース)では、今回の変更により Ractor セーフティが向上します。CHANGELOG の更新は行われていないため、「挙動変更」というよりは「安全性向上のための内部的変更」という扱いになっています。
- 参考情報 (あれば)
- Ruby の Ractor 仕様上、Ractor 間で共有されるオブジェクトは原則として immutable(
freeze)である必要があります。 - 以前から
LEVEL_CHECKS定数は freeze 済みでしたが、今回の PR によってlog_levelsも同様に不変化され、LogSubscriber全体として Ractor フレンドリーな設計に近づいています。 - マルチ Ractor 対応を見据えた Rails の内部改善の一環と位置付けられます。
#57920 Eagerly assign ExecutionWrapper's active key
マージ日: 2026/6/30 | 作成者: @andrewn617
- 概要 (1-2文で)
ExecutionWrapper のサブクラスにおけるactive_keyの代入タイミングを「継承時」に早めることで、最初のリクエストが非メイン Ractor で処理された場合に発生する Ractor の隔離エラーを防ぐ修正です。Ractor 対応を強化するための内部的な初期化順序の見直しになります。
- 変更内容の詳細
背景
Ruby の Ractor では、「メイン Ractor 以外からクラスインスタンス変数を初期設定する」といった操作に制約があり、適切に扱わないと「ractor isolation error(Ractor の隔離違反)」が発生します。
ExecutionWrapper は Rails の実行コンテキスト(ミドルウェア的な周辺処理)を扱うためのクラスで、そのサブクラスごとに active_key というクラスインスタンス変数を持ちます。
従来は、この active_key が「必要になったタイミング(=リクエスト処理時など)」で初期化されていたため、最初のリクエストがメイン Ractor ではない Ractor で処理されるケースで Ractor の制約に引っかかる可能性がありました。
今回の変更
active_keyの代入を 「サブクラス定義時 (継承フックinherited内)」に移動 しました。- これにより、ExecutionWrapper を継承したクラスがロードされた瞬間、まだメイン Ractor 上にいる段階で
active_keyが確実にセットされます。
実装イメージ(簡略化した擬似コード):
class ActiveSupport::ExecutionWrapper
class << self
# もともとどこか別のタイミングで行っていた active_key 代入を
# 継承時フックに移したイメージ
def inherited(subclass)
super
subclass.instance_variable_set(:@active_key, :"active_#{subclass.name}")
end
end
end※上記はあくまで概念的なサンプルです。実際のキー名や代入方法はコードに依存しますが、ポイントは「inherited フック内でサブクラスのクラスインスタンス変数をセットする」ことです。
- 影響範囲・注意点
- 主な影響範囲
ActiveSupport::ExecutionWrapperを継承しているクラス(およびそれを利用するコード全般)- マルチ Ractor 環境で Rails を動かしているアプリケーション
- 期待される効果
- 最初のリクエストがメイン Ractor 以外で処理された場合でも、
active_keyに関する Ractor 隔離エラーを防げる。 - ExecutionWrapper の初期化がより決定的(deterministic)になり、Ractor 利用時の不定動作の可能性が下がる。
- 最初のリクエストがメイン Ractor 以外で処理された場合でも、
- 互換性・既存コードへの影響
active_keyの値や外部 API が変わるわけではなく、「いつセットされるか」だけが変わるため、通常のアプリケーションコードへの影響はほぼありません。ExecutionWrapperのサブクラス化や@active_keyに対してメタプログラミングを行っているような高度なコードがある場合は、初期化タイミングの違いに注意してください(ただしそのようなケースは稀と思われます)。
- パフォーマンス
- 継承時にクラスインスタンス変数を一つセットするだけなので、事実上無視できるレベルのオーバーヘッドです。
- 参考情報 (あれば)
- Ruby Ractor の制約として、「Ractor 間で共有されるオブジェクトや、Ractor 境界を跨いだミューテーション」に厳しい制約があり、クラスインスタンス変数の扱いにも注意が必要になります。
inheritedフックは Ruby における標準的なメタプログラミングフックで、class Child < Parent; endのようにサブクラスが定義されたときにParent.inherited(Child)が呼ばれます。
今回はこの仕組みを利用して、「サブクラスロード時に Ractor セーフな形で内部状態 (active_key) を初期化する」ようにしています。