Ruby on Rails PR Digest - 2026年 5月
このページは rails/rails リポジトリにマージされたPull Requestを自動的に収集し、AIで要約したものです。
#57438 Preserve IPv6 prefix when dumping PostgreSQL cidr/inet defaults to schema.rb
マージ日: 2026/5/30 | 作成者: @55728
- 概要 (1-2文で)
PostgreSQL のcidr/inet型のデフォルト値をschema.rbにダンプする際、IPv6 アドレスのプレフィックスが/32のときに失われてしまうバグを修正する PR です。IPv4 と IPv6 で「フルマスク」とみなすプレフィックスを正しく判定することで、IPv6 のネットワーク情報がスキーマの往復で壊れないようにしています。
- 変更内容の詳細
問題のポイント
ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Cidr#type_cast_for_schema は、schema.rb に書き出す Ruby 文字列リテラルを生成するメソッドです。既存実装では次のようになっていました:
def type_cast_for_schema(value)
# If the subnet mask is equal to /32, don't output it
if value.prefix == 32
"\"#{value}\""
else
"\"#{value}/#{value.prefix}\""
end
end/32を「フルマスク」とみなし、その場合はサフィックス/32を省略して"192.168.1.0"のように出力する、という前提でした。- これは IPv4 では正しいものの、IPv6 では「フルマスク」は
/128であり、/32は普通に使われるネットワークプレフィックスです。
そのため、たとえば:
create_table :networks do |t|
t.cidr :address, default: "::/32"
endというマイグレーションを作ると:
db:schema:dumpで::/32が"::"としてschema.rbに書き出される- その後
db:schema:loadで再読込すると、"::"はIPAddr.new("::")により::/128として解釈される
結果として、マイグレーションで意図したデフォルト (::/32) と、スキーマから再生成したデフォルト (::/128) が食い違う、というサイレントなデータ破壊が起きていました。
修正内容
フルマスクの条件を「IPv4 なら /32、IPv6 なら /128」とするように変更しました:
def type_cast_for_schema(value)
# If the subnet mask covers the full address, don't output it
if value.prefix == (value.ipv6? ? 128 : 32)
"\"#{value}\""
else
"\"#{value}/#{value.prefix}\""
end
endこれにより:
- IPv4:
192.168.1.0/32→"192.168.1.0"(従来通り)192.168.1.0/24→"192.168.1.0/24"(従来通り)
- IPv6:
::/32→"::/32"(これまでは"::"になっていたのが修正される)fe80::/10→"fe80::/10"::/0→"::/0"::1/128→"::1"(新たに/128を省略するようになるが、IPAddr.new("::1")するともともと/128なので意味的には同じ)
OID::Inet は OID::Cidr を継承しており type_cast_for_schema をオーバーライドしていないため、この修正は cidr と inet 両方の型に効きます。
追加テスト
activerecord/test/cases/adapters/postgresql/cidr_test.rb に、これまでなかった type_cast_for_schema 用のテストが追加されています。
- IPv4 の既存挙動(/32 省略・その他は保持)が変わっていないことを確認
- IPv6 で:
/32や/10,/0のような部分プレフィックスがそのまま保持されること/128のときだけ省略されること
を検証しています。
- 影響範囲・注意点
- 影響を受けるケース:
- PostgreSQL の
cidrまたはinetカラムに IPv6 のネットワーク (/128 以外) をデフォルト値として設定している アプリケーション。 - これまで
db:schema:dump→db:schema:loadの往復で、元のデフォルト値から異なる値が設定されていた可能性があります。
- PostgreSQL の
- 具体的な問題例:
- 以前の挙動:
default: "::/32"→schema.rbには"::"→ 再ロードすると::/128 - この PR 適用後:
schema.rbには"::/32"と出力され、db:schema:loadによる再現で::/32が維持されます。
- 以前の挙動:
- 既存 IPv4 の挙動には変更なし:
192.168.1.0/24などの既存テストケースはそのまま通っており、互換性に問題はありません。
- 仕様上の“変化”:
- IPv6 の
/128フルホストをデフォルトにしている場合、schema.rb上の見た目が"::1/128"から"::1"に変わることがあります。 - これは
IPAddrの解釈が変わるわけではなく、あくまで表示上・スキーマ上の表記ゆれが解消されるだけです(IPv4 の挙動との対称性がよくなります)。
- IPv6 の
アップグレード時に、もし schema.rb に IPv6 のデフォルトがある場合は、ダンプ結果の差分を一度確認すると安心です。「これまで silent に壊れていたものが正しく直る」という修正なので、通常は望ましい差分だけのはずです。
- 参考情報 (あれば)
- 変更ファイル:
activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rbtype_cast_for_schemaのフルマスク判定ロジック変更
activerecord/test/cases/adapters/postgresql/cidr_test.rb- 新規テストの追加
activerecord/CHANGELOG.md- IPv6
cidr/inetデフォルト値のスキーマダンプに関するバグ修正として追記
- IPv6
- 背景:
- 元の
/32前提の実装は 2014 年 (commit: 4321cd09a5) から存在しており、長年 IPv6 に対して不正確な挙動をしていたことになります。 - ただし影響を受けるのは「IPv6 ネットワークをデフォルトとして持つ」比較的ニッチなケースに限られます。
- 元の
#57506 Fix NullPool#server_version deadlock
マージ日: 2026/5/29 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
Rails の Active Record で「単体接続(NullPool)」利用時にserver_versionを最初に呼ぶとデッドロックする不具合があり、NullPoolが内部で使うロックをMutexからMonitorに変更することで解消した PR です。通常のConnectionPoolと同じ再入可能なロック方式に揃えた、比較的小さなバグ修正です。
- 変更内容の詳細
問題の背景
新規に確立した DB 接続で、最初に
server_versionを取得するフロー:#connect!#configure_connection#check_version
⇨ この中で再度プールの#server_versionが呼ばれる
プール側の
#server_versionは同期化されたメソッド:- 通常の
ConnectionPool:- ロックに
Monitorを使用(再入可能なので、同じスレッドがネストしてロックを取り直しても OK)
- ロックに
- 単体接続用の
NullPool:- ロックに
Mutexを使用(再入不可のため、同じスレッドでserver_versionを再入するとデッドロック)
- ロックに
- 通常の
結果として、
- 「コネクションプールを使わずに単体接続を扱うパス(NullPool)」で
- 「接続直後に
server_versionを取りにいく」ケース
でデッドロックが発生していた。
修正内容
NullPoolが利用する同期プリミティブをMutex→Monitorに変更:- これにより、
server_version呼び出し中にconnect!→configure_connection→check_version→ 再度server_versionという再入が起きても、同一スレッドであればロックが再取得可能になりデッドロックが発生しなくなる。 - 実装レベルでは
ConnectionPool側と同じロック戦略に統一。
- これにより、
テスト追加:
activerecord/test/cases/connection_adapters/standalone_connection_test.rbに、スタンドアロン接続(NullPool 経由)でのこの挙動を検証するテストが追加されている。- 狙いは「最初の問い合わせで
server_versionを取得するようなコードパスでもハングしないこと」の担保。
CHANGELOG 更新:
- Active Record の
CHANGELOG.mdに、このバグ修正が追記されている(挙動に影響するバグ修正に該当するため)。
- Active Record の
- 影響範囲・注意点
影響を受ける可能性があるケース:
- Active Record で「コネクションプールを使わずに単一接続を扱うパス(NullPool)」を利用している場合
- かつ、その接続で最初に実行する処理が
server_versionの問い合わせ、またはcheck_versionを内部で呼ぶような接続設定処理である場合 - このような環境では、これまでデッドロックやハングに見える現象が起きていた可能性がある
変更による挙動:
- デッドロックが解消される以外の挙動変更は基本的になし
- 並行実行に関する同期の性質も、
ConnectionPoolに合わせた形になるため、プール利用時と単体接続時の一貫性が増す
パフォーマンス/互換性:
Mutex→Monitorへの変更は Ruby の標準的な同期プリミティブの置き換えであり、一般的なユースケースで性能面の顕著な悪化は想定しにくい- インターフェースや設定項目には変更がないため、アプリケーションコード側での対応は不要
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57506
- 関連クラス:
ActiveRecord::ConnectionAdapters::NullPoolActiveRecord::ConnectionAdapters::ConnectionPool
- Ruby の
MonitorとMutexの違い:Mutex: 非再入ロック。同じスレッドが再度lockするとデッドロック。Monitor: 再入可能ロック。同じスレッドであればネストしたロック取得が可能。
今回はこの「再入可能性」が必要なパスだったため、Monitorへの切り替えとなっている。
#57475 Active Record Ractor-safe freezing
マージ日: 2026/5/29 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Active Record 内のいくつかのモジュール/クラスで、オブジェクトのfreezeや定数化(constantize)を進めることで、Ractor(Ruby 3 の並列実行モデル)でも安全に利用できるようにするための変更です。機能追加というより、Ractor 対応のための内部実装の堅牢化に近い位置づけです。
- 変更内容の詳細(あればサンプルコードも含めて)
※PR本文からは抽象的な説明しかありませんが、ファイル一覧と目的から推測できる範囲での技術的な整理です。
共通の方向性
- 「Ractor セーフ」にするために行っているのは主に次の 2 つです:
- オブジェクトを
freezeする
Ractor 間で共有されるオブジェクトは、原則として「イミュータブル(変更不可)」である必要があるため。 - 可能な限り定数として定義する
実行時に書き換えられない形(凍結された定数など)で持つことで、Ractor 間共有が安全になる。
- オブジェクトを
ファイルごとの変更イメージ
実際の差分が小さい(+8 / -3 行)ことから、各ファイルで行われているのは「既存のクラス変数・定数・ハッシュ等に freeze を付与する」「動的に構築していたものを、定数化+凍結する」といったものと考えられます。
1) activerecord/lib/active_record/readonly_attributes.rb (+1/-1)
- 読み取り専用属性(
attr_readonlyなど)の管理ロジックのどこかで、集合(配列・set など)やハッシュのfreezeを追加した可能性が高いです。 - 例としては、次のような変更が典型です(あくまでイメージ):
# 変更前のイメージ
READONLY_ATTRIBUTES = %i[created_at updated_at]
# 変更後のイメージ
READONLY_ATTRIBUTES = %i[created_at updated_at].freeze- 目的は「読み取り専用属性の定義そのものをイミュータブルにして、Ractor 間で安全に共有できるようにする」ことです。
2) activerecord/lib/active_record/relation/delegation.rb (+1/-0)
- ActiveRecord::Relation が Array や Enumerable に委譲しているメソッド定義のテーブルなどに
freezeが入った可能性があります。 - よくあるパターン:
SINGLE_VALUE_METHODS = [:limit, :offset, :order]
# → これに .freeze を付けた、あるいは Hash/Array の中身を凍結
SINGLE_VALUE_METHODS = [:limit, :offset, :order].freeze- Relation のデリゲーション設定自体は実行時に変化しない前提なので、凍結しても通常のアプリ開発者には影響が出ない想定です。
3) activerecord/lib/active_record/relation/where_clause.rb (+3/-1)
WhereClauseは SQL の WHERE 句を表現する内部クラスで、述語(predicates)の配列などを持っています。- Ractor セーフティの観点では、以下のようなことが行われた可能性があります:
- 空の
WhereClauseを表すオブジェクトを「共有定数+freeze」にする - 条件リストなどを
freezeしてイミュータブルに扱う
- 空の
例(イメージ):
# 変更前
EMPTY_WHERE = new([])
# 変更後
EMPTY_WHERE = new([].freeze).freeze- こうしておくと、Ractor 間で
EMPTY_WHEREを共有しても、後から変更される心配がなくなります。
4) activerecord/lib/active_record/type.rb (+3/-1)
- Active Record の型システム(
ActiveRecord::Type)に関連する定数やレジストリを凍結したと考えられます。 - 典型的な変更パターン:
# 変更前
TYPES = {
integer: Type::Integer.new,
string: Type::String.new,
}
# 変更後
TYPES = {
integer: Type::Integer.new,
string: Type::String.new,
}.freeze- もしくは、型のリスト/マップを定数化して
freezeすることで、Ractor 間で型情報を安全に共有する狙いです。
- 影響範囲・注意点
影響範囲
- 対象はほぼ内部実装であり、一般的な Rails アプリで直接触れる API(
Model.whereなど)の表面的な挙動は変わらない想定です。 - ActiveRecord::Relation、WhereClause、型レジストリ、readonly attributes まわりの内部オブジェクト構造がやや「よりイミュータブル寄り」になっています。
注意点
内部定数を書き換えていたコードは壊れる可能性
ActiveRecord::Relation::Delegation::SOMETHING << :fooのように、Rails の内部定数を直接変更していた場合、freezeによりFrozenErrorが発生し得ます。- そもそも非推奨な使い方なので、これを機にやめるべきです。
Ractor を使う場合の前提整備
- この PR 単体で「Active Record が完全に Ractor セーフになる」わけではなく、「Ractor セーフに近づけるための一歩」です。
- Ractor を本格的に使う場合は、他の PR(#57323 など)も含めて Ruby / Rails のバージョンと互換性を確認する必要があります。
freeze によるパフォーマンス面の懸念はほぼ小さい
- 変更されているのは多くが定数や内部の再利用オブジェクトで、一度生成して共有する性質のものです。
- 通常の CRUD 操作で目に見えるパフォーマンス劣化はまず起きないと考えられます。
- 参考情報 (あれば)
- 類似 PR:
- https://github.com/rails/rails/pull/57323
→ 同じく「Ractor セーフティ向上のために freeze/constantize を進める」系の変更。
- https://github.com/rails/rails/pull/57323
- Ractor の制約(公式ドキュメント):
- Ruby 3.0 Ractor 概要: https://docs.ruby-lang.org/ja/latest/doc/spec=2fractor.html
- 「共有できるオブジェクトは基本的に『凍結された(freeze 済みの)オブジェクト』に限られる」などの制約が説明されています。
- Ruby 3.0 Ractor 概要: https://docs.ruby-lang.org/ja/latest/doc/spec=2fractor.html
- 実務的には:
- Rails で Ractor を使いたい場合は、Active Record を含む各コンポーネントがどこまで Ractor セーフ対応されているかを、PR/CHANGELOG ベースで追いながら検証する必要があります。
#57478 Acquire reaper_lock in preconnect to avoid racing with the reaper
マージ日: 2026/5/29 | 作成者: @yahonda
- 概要 (1-2文で)
ActiveRecord の接続プールでpreconnect実行時にreaper_lockを取得するようにし、リーパー(メンテナンス用スレッド)との競合状態(race condition)で CI のtest_preconnectが落ちる問題を解消する PR です。disconnectやdiscard!と同じ「プール全体に影響する操作は reaper_lock で囲む」という設計に揃えています。
- 変更内容の詳細
問題の背景
- ActiveRecord の接続プールには、バックグラウンドで定期的にメンテナンスを行う「reaper」スレッドがあります。
- reaper はメンテナンスサイクルの中で
preconnectを呼びますが、このときすでにreaper_lockを取得した状態で動きます。 - テスト
ActiveRecord::ConnectionAdapters::ConnectionPoolThreadTest#test_preconnectでは、アプリケーション側からも同時にpool.preconnectを呼ぶケースがあり、ここで reaper とユーザースレッドが並行にpreconnectを叩くことがあります。 - reaper スレッド側は
checkout_for_maintenanceにより一時的にコネクションをチェックアウトし、その間は接続が「遷移中」の状態になります:allow_preconnect = false@raw_connection = nil
- この「遷移中」状態で、別スレッドの
preconnectが実行されると、接続がconnected?にならないまま終わり、テストが失敗することがありました(CI での intermittent failure)。
変更点
ConnectionPool#preconnect を reaper_lock で囲むように変更しています。
イメージとしては、以下のような変更です(概念的なサンプル):
def preconnect
reaper_lock.synchronize do
# 既存の preconnect ロジック
with_exclusively_acquired_all_connections do |conn|
conn.verify!
end
end
end実際には:
- 既存の
preconnectメソッドの本体を、そのままreaper_lock.synchronizeの中に移動 - すでに
disconnect,discard!,with_exclusively_acquired_all_connectionsが同様にreaper_lockで囲まれており、そのパターンに揃えた形
なぜ reaper 自身はブロックされないのか
reaper_lockは再入可能 (reentrant) なMonitorを使っています。- reaper スレッドがメンテナンスサイクル内で
reaper_lockを取得済みの状態でpreconnectを呼んでも、同じスレッドからの再取得なのでブロックされません。 - 逆に、ユーザースレッドからの
preconnectはreaper_lockを取りに行くため、reaper がメンテナンス中であれば待機し、コネクションが中途半端な状態のまま処理されることを防ぎます。
再現と効果
- Docker 上の MariaDB 11.4 (
--cpus=0.02 --memory=128mでかなりリソースを絞った状態) かつ並行負荷をかけることでローカル再現。 - 修正前:
test_preconnectが 7, 16, 36, 65 回目など複数回 fail。 - 修正後: 100 回以上の繰り返し実行でも失敗せず、そのうち 86 回以上は実際に
reaper_lockの待ちが発生していることをログで確認。
- 影響範囲・注意点
- 影響範囲
- ActiveRecord の
ConnectionPool#preconnectを明示的に使っているコード(アプリ起動時やジョブワーカー起動時などで「事前接続」しているケース)が対象です。 - 内部的には、reaper が動いている環境すべてでロック順序がより一貫したものになります。
- ActiveRecord の
- パフォーマンス
preconnect自体が「全体に影響する重めの操作」であり、頻繁に呼ぶべきではないことから、reaper_lockで囲むことによるオーバーヘッドは実質的に無視できる範囲と考えられます。- むしろ、メンテナンススレッドとの競合で接続が壊れたりテストが flaky になるリスクを減らすメリットが大きいです。
- デッドロックの懸念
reaper_lockはすでにdisconnectやdiscard!等で使われており、そこにpreconnectが加わるだけなので、ロック設計としては既存方針の拡張です。Monitorベースで再入可能な点と、reaper 側がもともとreaper_lock→preconnectの順で呼んでいる点から、新たなロック順序逆転は発生していません。
- 参考情報 (あれば)
- この PR は次の変更のフォローアップです:
- e45d8acc746「Lock the pool while performing global operations」
→ 接続プール全体に影響する操作をreaper_lockで保護する方針を導入したコミット
- e45d8acc746「Lock the pool while performing global operations」
- デバッグ用ブランチ(問題再現とログ出力付き):
- 対象テスト:
ActiveRecord::ConnectionAdapters::ConnectionPoolThreadTest#test_preconnect
#57501 Return a completed promise for async_count with limit(0)
マージ日: 2026/5/28 | 作成者: @fallintoplace
- 概要 (1-2文で)
Relation#async_countがlimit(0)のときに整数0を直接返してしまう不整合が修正され、常にActiveRecord::Promiseを返すようになりました。これにより、async_countの戻り値でvalue,then,pending?などの Promise API を一貫して利用できます。
- 変更内容の詳細
何が問題だったか
async_count はドキュメント上「ActiveRecord::Promise を返す」とされていますが、次のようなケースでは Promise ではなく Integer が返っていました。
result = Post.limit(0).async_count
result.class
# => Integer (修正前)原因は execute_simple_calculation にある「limit(0) のショートカット」です。limit(0) の場合は「レコード数は必ず 0 になる」と判断して DB クエリを実行せず、早期 return していましたが、その早期 return が「async 用のラップ処理」よりも先に実行されていたため、async_count 経由でも生の 0 が返ってしまっていました。
その結果:
- 呼び出し側は
Promiseを期待しているのにIntegerを受け取ることがある value,then,pending?などPromise向けメソッドが使えない- 「常に Promise を返す」という API 契約が破られていた
何を直したか
ポイントは「limit(0) の高速パス(ショートカット)は維持しつつ、async のときは完成済み Promise を返す」ようにしたことです。
- 同期パス:
Post.limit(0).count→ これまで通りクエリ無しで0を返す
- 非同期パス (
async_count):- 以前:
Post.limit(0).async_count→ クエリ無しだが 生の0が返る - 以後:
Post.limit(0).async_count→ クエリ無しで 値が0の完了済みActiveRecord::Promiseを返す
- 以前:
修正後の挙動:
result = Post.limit(0).async_count
result.class
# => ActiveRecord::Promise
result.value
# => 0
result.pending?
# => false内部的には、execute_simple_calculation の limit(0) での早期 return の取り扱いを調整し、「async の場合は早期 return も Promise で包まれる」ように揃えています。
クエリを飛ばさない最適化自体(「0件確定ならクエリしない」)はそのまま残っています。
テスト
既存の「limit(0) で count はクエリ無しで 0 を返す」テストを拡張し、async_count でも同じくクエリ無しで動作しつつ、返り値が Promise になっていることを検証するようになっています。
実行されたテスト(一部):
test_count_should_shortcut_with_limit_zerotest_count_with_empty_in
- 影響範囲・注意点
async_count利用コードへの影響- これまで
limit(0).async_countの戻り値を「Integerだと仮定して」扱っていたコードがある場合、型がActiveRecord::Promiseに変わるため注意が必要です。- 例:
Post.limit(0).async_count == 0→ 修正後はpromise == 0になるので常に false - 正しくは:
Post.limit(0).async_count.value == 0
- 例:
- 一方で、本来の API 契約(常に Promise を返す)に沿ったコードは今回の変更でむしろ安全になります。
- これまで
パフォーマンス
limit(0)のショートカットによる「クエリを飛ばさない」最適化は維持されているため、パフォーマンス的な後退はありません。- async でも DB にアクセスせず、即時完了済み Promise が得られます。
API の一貫性向上
async_count,async_sum,async_averageなど、非同期集計 API が「条件によっては生の値を返す」ようなバラつきをなくす方向の一貫した修正の一つです(PR 説明にも、以前修正された「矛盾する条件での async aggregation の早期 return」と同種の問題とあります)。
- 参考情報 (あれば)
該当ファイル:
activerecord/lib/active_record/relation/calculations.rbexecute_simple_calculation内のlimit(0)周りのロジック
activerecord/test/cases/calculations_test.rbtest_count_should_shortcut_with_limit_zeroにasync_count分の検証が追加
実務でのポイント:
- 非同期集計を使う場合、戻り値は常に Promise 前提で書く(
.valueか、thenでつなぐ)ようにしておくと、今回のような修正に強いコードになります。 limit(0)を絡めたクエリで「結果が常に 0 になる」という前提に依存していても、今回の変更でその前提自体は変わっていません(0であることとクエリを打たないことは維持されています)。
- 非同期集計を使う場合、戻り値は常に Promise 前提で書く(
#50979 Action Cable server adapterization
マージ日: 2026/5/28 | 作成者: @palkan
- 概要 (1-2文で)
Action Cable の「WebSocket + スレッド実装」に強く結びついていた部分を分離し、「サーバーアダプタ化」するリファクタリングです。既存の Rails 標準 Action Cable の挙動はほぼ変えずに、代替サーバー・Fiber 実行モデル・SSE などに乗せ替えやすくするための基盤整備が行われています。
- 変更内容の詳細
2-1. 非同期実行基盤(Executor)の整理・抽象化
これまで
- Action Cable には主に2種類のスレッドプールが存在:
- Worker: アプリケーションコード(チャネルの
performなど)実行用 - Pub/Sub 用 Executor: Redis などの pub/sub に絡む非同期処理用
- Worker: アプリケーションコード(チャネルの
- 後者は
StreamEventLoopの中に埋め込まれており、I/O ループと Executor が密結合でした。 - また、Concurrent Ruby の Executor をそのまま使っていたため、実行モデルの差し替えがしづらい状態でした。
今回の変更
pub/sub 用 Executor を
StreamEventLoopから切り離し、ActionCable::Serverの属性として独立。Executor を小さなインターフェイスでラップ:
rubyclass AsyncExecutor # 非同期にブロックを実行 def post(&block); end # interval 秒ごと(or 後)にブロックを実行するタイマー # 戻り値はキャンセル用ハンドルなど def timer(interval, &block); end # シャットダウン処理 def shutdown; end end内部実装としては Concurrent Ruby のスレッドプールを使うが、このラッパを挟むことで将来的に Fiber ベースの Executor などに差し替え可能。
この PR 自体には
config.action_cable.async_executor = :fiberのような設定追加は含まず、まずは内部構造だけ抽象化。
ポイント:
「Action Cable の非同期実行モデル」を 1 箇所で差し替えられるようにするための第一歩。WebSocket に限らず、ほかの I/O モデルでも同じ Connection/Channel コードを再利用しやすくなります。
2-2. 非同期処理の責務を pub/sub レイヤーに集約
これまで
Channelの内部ロジックからevent_loop(=WebSocket 用 I/O ループ)へ直接アクセスして、非同期にstream_fromをセットするなど、層をまたいだ依存があった。- pub/sub アダプタ側でも非同期実行していたため、
stream_from一回につき二重に非同期呼び出しが走る可能性があった。
今回の変更
- pub/sub 周りの非同期処理を
SubscriberMap::Asyncサブクラスに集約。 - Channel レイヤーからは「非同期かどうか」を意識せず、pub/sub アダプタ側が必要なら内部で async 実行する形に統一。
これにより:
- Channel 抽象は「ストリームを張る・外す」だけに集中し、「どう非同期化するか」はアダプタに委譲。
- WebSocket 実装に依存していた箇所から
event_loopの直参照が消える。 stream_fromの実行パスがシンプルになり、別サーバー実装でも Channel コードをそのまま使いやすくなる。
2-3. Connection クラスの分割(アプリ層とサーバ層)
これまで
ActionCable::Connection::Base が以下を全部抱えていました:
- アプリケーションレベルの責務
- 接続の認証 (
connect)、拒否 (reject_unauthorized_connection) - 接続状態に紐づくロジック、Channel 管理など
- 接続の認証 (
- サーバー実装レベルの責務
- WebSocket イベントの購読・ハンドリング (
onopen,onmessage,oncloseの設定など) - Rails executor のラップ
- ソケット I/O、バッファリング
- WebSocket イベントの購読・ハンドリング (
このため:
- WebSocket 以外のトランスポート(SSE など)で Connection ロジックだけ再利用するのが難しい。
- テスト時にも EM などの I/O ループを必要としてしまい、重く・複雑になりがち。
今回の変更
Connection レイヤーを 2 つに分割:
ActionCable::Connection::Base- アプリケーションレベルの Connection クラス(従来利用していたクラス)。
- ユーザーが定義する
ApplicationCable::Connectionは引き続きこれを継承。 - 「ソケットがどう届くか」「どのサーバーか」を意識せず、抽象的な Socket/Server を相手にする。
ActionCable::Server::Connection- Web サーバー側から見た、低レベル Connection 実装。
- WebSocket や HTTP SSE などトランスポートに近い部分と、
Connection::Baseの橋渡しを担う。 - Rails executor ラップや各種ハンドラ登録など、「サーバー実装としての責務」を持つ。
両者の間は以下の抽象インターフェイスでやり取りします(PR 説明の UML より):
_Serverpubsub,executor,configを提供。
_Socketlogger,env,request,protocoltransmit(payload)/close()/perform_work(receiver, method_name, *args)
_Connectionhandle_open,handle_incoming(payload),handle_close,beat,statistics
概念的な流れ:
Webサーバー (Puma/Iodine/etc.)
↓
ActionCable::Server::Socket::* (WebSocket, SSE, etc.)
↓ (抽象 _Socket として)
ActionCable::Server::Connection
↓
ActionCable::Connection::Base (ApplicationCable::Connection)
↓
各 Channelメリット:
- WebSocket 以外のトランスポート(SSE など)でも、
Server::Socketレイヤーを差し替えるだけでConnection::Base/ Channel のコードはそのまま使える。 - テストでは「純 Ruby のクラス」として Connection/Channel を扱えるようになり、I/O ループ依存が軽減。
- Rails executor のラップや pub/sub との連携など、サーバー固有の事情を
Server::Connection側に閉じ込められる。
2-4. Server::Socket レイヤーの新設
削除されたファイル(旧構造):
action_cable/connection/client_socket.rbaction_cable/connection/web_socket.rbaction_cable/connection/message_buffer.rbaction_cable/connection/stream.rb
新たに追加されたファイル(新構造):
action_cable/server/socket.rbaction_cable/server/socket/client_socket.rbaction_cable/server/socket/web_socket.rbaction_cable/server/socket/stream.rbaction_cable/server/socket/message_buffer.rb
役割の整理:
ActionCable::Server::Socketおよびそのサブクラス群は「実際のトランスポート実装」を担当。ClientSocket:クライアントとのエンドポイント抽象WebSocket:WebSocket プロトコルの扱いStream、MessageBuffer:ソケット I/O のためのバッファリング・ストリーム処理
Connection::Base側からは、これらは_Socket抽象として見える。
これにより:
- Iodine など独自の WebSocket 実装・pub/sub を持つサーバでも、「Action Cable アダプタ」として実装しやすくなる。
- 同じインターフェイスで、WebSocket 以外のトランスポートを差し込める。
2-5. テストサポートの改善 (ActionCable::Connection::TestCase, Channel::TestCase)
主なポイント:
- Connection / Channel のテストで、より実際に近いクラスを使えるようにリファクタ。
- 以前:EventMachine / WebSocket 実装に近いテストヘルパが必要になるケースがあった。
- 今回:純粋な Ruby オブジェクトとして Connection / Channel を扱える構成になり、テストがシンプルに。
ActionCable::Connection::TestCaseに多くの変更(+117 / -34)- 実際のサーバアダプタに依存しない形で、接続確立・メッセージ送信・切断などをテスト。
ActionCable::Channel::TestCaseも内部ロジック刷新(+58 / -73)- 実運用に近い形で stream/subscription 周りをテスト可能。
また、periodic timers のテスト用に新ヘルパーが追加:
# テスト内でタイマーを進める
advance_time(5.seconds)これにより、Channel の periodic :foo, every: 5.seconds のような処理を、実時間を待たずにテストできます。
2-6. 購読エラーの取り扱い改善
- 以前は、以下のような不正な状態に対して:
AlreadySubscribedChannelNotFoundなどを、単にログに出すだけ / 無視するだけのことが多かった。
- 今回は、Subscriptions レイヤーが「どう扱うか」を決めず、例外として必ず raise する設計に変更。
- その代わり、例外をどこでキャッチしてどう応答するかは、より上位(サーバ/接続)レイヤーで制御。
目的:
- エラー処理ポリシーを一箇所に集約しやすくし、実サーバ実装ごとの振る舞い(ログを出す、クライアントにエラーを返す等)を明確に切り替えられるようにする。
2-7. 既存 Action Cable API への影響
- PR 説明にもある通り、「目に見える大きな新機能 or 破壊的変更」は導入していません。
ApplicationCable::Connection,ApplicationCable::Channel, クライアント JavaScript から見た Action Cable の API は基本的に変わりません。- あくまで内部構造のリファクタリングと抽象化が中心で、現時点では Rails ユーザーが設定を変えなくてもそのまま動作します。
- 影響範囲・注意点
3-1. 影響範囲
- Action Cable の内部構造:
- Connection / Socket / Executor / PubSub の責務分担がかなり変わっており、Action Cable 内部に依存したハックや monkey patch を書いている場合は影響大。
ActionCable::Connection::Baseの中にあったソケット関係のクラス(ClientSocket,WebSocket,Stream,MessageBuffer)はActionCable::Server::Socket::*側へ移動しているため、クラス名・モジュールパスの変更に注意。
- テストヘルパー:
ActionCable::Connection::TestCase,ActionCable::Channel::TestCaseを独自に拡張している場合は挙動・呼び出し順序の変化に注意。- 新しい構造に対応した rspec-rails 側の変更も別リポジトリで進行中(リンクあり)。
3-2. 注意点 / マイグレーション観点
- 通常の Rails アプリが公式にサポートされている使い方(
ApplicationCable::Connection/Channelを素直に継承しているだけ)であれば、基本的にコード変更は不要と想定されています。 - ただし、以下に該当する場合はコードの見直しが必要になる可能性があります:
ActionCable::Connection::Baseに対する monkey patch / private メソッドへの依存。ActionCable::Connection::*配下のクラス(旧ClientSocketなど)を直接参照している。- Action Cable の pub/sub 実装をカスタムしていて、
event_loopに直接触っている。
- actioncable-next gem(Rails 7+ 向けの先行実装)と同等の構造に近いため、すでにそれを試している場合は互換性の確認がしやすいです。
- 参考情報
- PR 本文での詳細な背景説明・設計意図(外部ドキュメント)
- actioncable-next gem(この PR の機能を Rails 7+ で先行利用可能)
- playground(実験用・サンプル・ベンチマーク)
- Action Cable over SSE のスケッチ実装
- Iodine サーバーアダプタ実装例
- AnyCable Rails 統合の新バージョン(本 PR ベース)
- rspec-rails 側の対応 PR
この PR は「Action Cable を 1 つのサーバ・実行モデルに固定しないための抽象化」が主目的なので、Action Cable を他サーバで動かしたい・Fiber 対応を進めたい・SSE など複数トランスポートを併用したいといったニーズがある場合に、今後の基盤になります。
#57268 Fix: registers ERBTracker instead of RubyTracker when eager_load = true
マージ日: 2026/5/28 | 作成者: @joaoGabriel55
- 概要 (1-2文で)
Rails 8.1 デフォルト設定かつeager_load: trueなどの条件下で、ERB テンプレートの依存関係トラッキングが正しく動かない不具合を修正した PR です。ActionView.render_trackerの設定タイミングをafter_initializeから専用 initializer に移し、ERB が適切にRubyTrackerへ紐づくようにしています。
- 変更内容の詳細
不具合の背景
元々のコードでは、ActionView.render_tracker の設定が「ネストした config.after_initialize」内で行われていました:
config.after_initialize do |app|
config.after_initialize do
ActionView.render_tracker = config.action_view.render_tracker
end
# ...
endafter_initialize は Rails のブートシーケンスのかなり後ろで呼ばれますが、ここでさらにネストした after_initialize を呼んでいたため、
- 外側の
after_initializeが完了した後 - すでに eager load 等の処理が終わった後
に ActionView.render_tracker が設定されていました。
その結果:
eager_load: trueenable_reloading: falseconfig.load_defaults 8.1
といった構成だと、ERB ハンドラが ActionView::DependencyTracker::RubyTracker に紐づく前に、依存関係トラッカーのセットアップ処理が終わってしまい、ERB の dependency tracking が機能しない、というバグが出ていました(Issue #57265)。
修正内容:initializer 化
この PR では、ActionView.render_tracker の設定を専用 initializer に切り出し、ブートシーケンス上の適切なタイミングで確実に実行されるように変更しています。
修正後のコード:
initializer "action_view.set_render_tracker" do |app|
ActionView.render_tracker = app.config.action_view.render_tracker
endポイント:
config.after_initializeではなく、Railtie のinitializerを使うことで、- 他の initializer 処理
eager_load!after_initializeコールバック より前にActionView.render_trackerが設定されるようになる。
app.config.action_view.render_trackerの値(設定されたトラッカー)が、ActionView 全体のrender_trackerとして適切に反映される。
テストの追加
以下のような条件で、ERB ハンドラが正しく RubyTracker に紐づくことを検証する回帰テストが追加されています。
load_defaults 8.1eager_load = trueconfig.enable_reloading = false
ActionView::DependencyTracker に対して、ERB (:erb) に対応するトラッカーとして ActionView::DependencyTracker::RubyTracker が登録されていることを確認するテストです。
また、actionview/test/template/dependency_tracker_test.rb 側にもテストが追加され、依存トラッキングの期待挙動が守られていることをより直接的に保証しています。
- 影響範囲・注意点
影響範囲
- Action View の dependency tracking(特に ERB テンプレート)の初期化タイミングに依存しているコード。
eager_load: trueかつenable_reloading: falseのような、本番相当の設定での挙動に特に影響します。- Rails 8.1 以降のデフォルト設定(
load_defaults 8.1)で恩恵を受けるケースが中心です。
実務上の影響
- これまで、ERB テンプレート変更に対する依存関係検出やキャッシュ無効化等が正しく行われていなかった環境では、今回の修正により期待通りに動作するようになります。
ActionView.render_trackerをアプリ側で独自に上書きしていた場合でも、initializer の実行順序によっては今回の変更の影響を受ける可能性があります。- Railtie/Engine 自作時に initializer の順序を細かく制御している場合は、
"action_view.set_render_tracker"より前後どちらで動くべきかを意識しておくと安全です。
- Railtie/Engine 自作時に initializer の順序を細かく制御している場合は、
注意点
- 挙動が「後ろにずれる」のではなく「前にくる」方向の変更なので、
after_initializeの中でActionView.render_trackerに依存していたコードがある場合、今回の修正でむしろ安定する方向の変更です。 - CHANGELOG の更新は行われていないため、マイナーなバグフィックス扱いとなっていますが、本番環境で依存関係トラッキングに依存している場合は挙動確認した方がよい変更です。
- 挙動が「後ろにずれる」のではなく「前にくる」方向の変更なので、
- 参考情報 (あれば)
- 対応 Issue: #57265
- PR 本体: #57268 「Fix: registers ERBTracker instead of RubyTracker when eager_load = true」
- 該当箇所の主なファイル:
actionview/lib/action_view.rbactionview/lib/action_view/railtie.rbactionview/test/template/dependency_tracker_test.rbrailties/test/application/configuration_test.rb
#39525 Add support for default order
マージ日: 2026/5/28 | 作成者: @udan11
- 概要 (1-2文で)
ActiveRecord::Relationに「デフォルトの並び順」を定義できる#default_orderが追加され、関連(has_manyなど)にもdefault_order:オプション/スコープで同等の機能を指定できるようになりました。これにより、アプリ側で「常にこの順で取りたいが、明示的に order を書いたときはそちらを優先したい」というパターンを Rails レベルで表現できます。
- 変更内容の詳細
2-1. Relation#default_order の追加
ActiveRecord::Relation に新たに default_order が追加されました(query_methods.rb)。
挙動イメージ:
# モデル側でスコープとして定義
class Comment < ApplicationRecord
scope :popular_first, -> { default_order(likes: :desc) }
end
# 使用例
Comment.popular_first # likes DESC で並び替え
Comment.popular_first.to_sql # SELECT ... ORDER BY "comments"."likes" DESC
# ただし、明示的に order を指定した場合はそちらが優先
Comment.popular_first.order(created_at: :asc)
# => ORDER BY "comments"."created_at" ASCポイント:
default_order(...)は「他に order が何も指定されていないときだけ有効になる order 条件」を relation に付与する。- 通常の
orderと違い、あとからorderを呼ぶと「マージ」ではなく、明示的な order のほうが優先される設計(=デフォルトは上書きされることを前提とした order)。
implicit_order_column との違い:
implicit_order_columnは主にlast,first,find_eachなどの暗黙の order を必要とする場面用(モデル単位、1カラム前提のことが多い)。default_orderは relation / scope / association 単位で任意の order 式を指定可能。
2-2. アソシエーション(特に has_many)での default_order サポート
has_many 定義に default_order オプション、あるいはラムダスコープ中で default_order(...) を指定できるようになりました。
PR の例:
class Post < ApplicationRecord
has_many :comments, default_order: :likes
# .. または ..
has_many :comments, -> { default_order(:likes) }
end
post = Post.first
post.comments # likes でソートされて返る
post.comments.order(:created_at) # ここでは created_at の order が優先される実装的には:
associations/builder/has_many.rbなどでdefault_order:オプションを許容。association_scope.rb/collection_association.rb側で、関連のスコープに default_order を反映させる。- アソシエーションの
default_order:オプションと、スコープ内のdefault_orderを組み合わせられる(ただし、やはり明示的なorderのほうが優先される、というルールは同じ)。
2-3. Finder 系メソッドとの連携
finder_methods.rb / relation.rb への変更により、default_order が first, last, take, find_by などの finder メソッドを呼び出した際にも適切に考慮されるようになっています。
- すでにある
implicit_order_columnとの兼ね合いで、- 明示的な
order default_orderimplicit_order_column
の優先順位や適用タイミングが整理されています(テストが追加されている)。
- 明示的な
2-4. テスト・ドキュメント
has_many_associations_test.rb,relations_test.rb,relation/mutation_test.rb,test/models/post.rbにテスト追加。CHANGELOG.mdに機能追加として記載。
- 影響範囲・注意点
影響範囲:
- ActiveRecord 全般(特に
has_manyを多用するコード)で、default_orderを利用できるようになる。 default_orderを導入しても、既存のorder呼び出しはそのまま優先されるため、既存コードへの破壊的な影響は基本的にない。implicit_order_columnをすでに使っているアプリでは、default_orderとの組み合わせで order の振る舞いが変わるケースがありうるため、重要なクエリは挙動確認が推奨される。
注意点:
default_orderは「隠れた order」になりうる- relation をチェインしている場所で、意図せずソートされている(パフォーマンス・結果順序)ことに気づきにくくなる可能性があります。
- チーム内コーディング規約として、どこに
default_orderを使うかを明確にするのが良さそうです(モデル全体のポリシー vs 特定アソシエーション限定など)。
明示的な
orderの振る舞いdefault_orderは「明示的orderが存在しない場合のみ有効」なので、後続のorderがあると無視されます。- デフォルトに加えてさらに order を積み上げたい場合は、
order(:likes).order(:created_at)のような従来どおりの書き方を使うほうが明確です(default_orderはそうした「積み上げ」用途にはあまり向かない)。
複合キー・複雑な order 式
default_order(likes: :desc, created_at: :asc)のように複数列も指定可能ですが、DB インデックスと整合しているか・パフォーマンスに問題が出ないかは各アプリで要確認です。
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/39525
- 提案ディスカッション (Rails Forum):
https://discuss.rubyonrails.org/t/default-order-for-relations/75554
この機能を使うと、例えば「コメントはデフォルトで最新順」「いいね数順」などをモデル側に閉じ込めつつ、必要な画面だけ明示的な order で挙動を変える、という設計がやりやすくなります。
#53101 [Fix #52229] Respect #reject in Action Cable before_subscribe callbacks
マージ日: 2026/5/28 | 作成者: @joshuay03
- 概要 (1-2文で)
Action Cable のbefore_subscribeコールバック内でrejectを呼んだ場合に、これまで実行されてしまっていたsubscribedを呼ばないようにするバグ修正です。before_subscribeによる購読拒否が、期待どおりチャンネル購読開始処理全体を止めるようになります。
- 変更内容の詳細
2-1. 問題の背景
- Action Cable では、チャンネルに接続する際に
before_subscribeコールバックを定義できます。 - 期待される挙動としては、
before_subscribe内でreject(またはreject_subscription)を呼ぶと、そのクライアントのチャンネル購読は拒否され、subscribedは呼ばれないはずです。 - しかし、バグにより、
before_subscribe内でrejectを呼んでも、その後にsubscribedが実行されてしまうケースがありました。
この PR は、その不整合を解消して Issue #52229 をクローズしています。
2-2. 主なコード変更
a. ActionCable::Channel::Base#subscribe_to_channel の修正
subscribe_to_channel は、WebSocket 接続からチャンネル購読を開始する際のメインエントリです。このメソッドの中で:
before_subscribeコールバックの実行- 実際の購読処理(
subscribedの呼び出し、ストリームの開始など)
が順に行われています。
今回の修正では、「1. before_subscribe 実行後にチャンネルが reject 済みであれば、2. 以降を実行しない」という条件分岐が追加されました。
擬似コードで表すと、次のような変化です(あくまで概念的なイメージ):
def subscribe_to_channel
run_before_subscribe_callbacks
# 変更前: 常に subscribed を呼んでいた
# subscribed
# 変更後: reject 済みの場合は subscribed を呼ばない
unless rejected?
subscribed
end
endここで rejected? は、チャンネルが購読拒否されたかどうかを示すフラグ的な状態を確認するメソッド(もしくはそれに準ずる判定)で、reject 実行時にセットされるようになっています。
b. ActionCable::Channel::Callbacks の拡張
actioncable/lib/action_cable/channel/callbacks.rb に、before_subscribe コールバックからの reject を正しく検知できるような処理が追加されています。
イメージとしては:
before_subscribe実行中にrejectが呼ばれたら、そのチャンネルインスタンスに「reject 済み」を記録する- その記録を
subscribe_to_channel側で参照して、後続処理(subscribed)をスキップする
という連携です。
c. テストの追加
actioncable/test/channel/base_test.rb にテストが追加・修正され、以下を検証しています:
before_subscribeコールバック内でrejectを呼んだ場合に、subscribedが呼ばれないこと- (必要なら)ソケット側の状態・メッセージ送信が期待通りになっていること
テストソケット (actioncable/test/stubs/test_socket.rb) にも 1 行追加されており、テストで reject 状態を検証するための最小限の変更が入っています。
d. CHANGELOG の更新
actioncable/CHANGELOG.md にバグ修正として追記されており、外部公開 API 挙動として修正されたことが明記されています。
- 影響範囲・注意点
- 影響を受けるのは、Action Cable のチャンネルで
before_subscribeを使い、その中でrejectを呼んでいる場合です。 - これまで:
before_subscribeでrejectしてもsubscribedが呼ばれてしまっていたため、subscribed内で「購読成功前提」の処理(DB 更新・ログ・他チャンネルへのブロードキャストなど)を書いていると、実は reject されているのに動いてしまう、というバグがありました。
- 修正後:
before_subscribeでrejectすると、その後のsubscribedは確実に呼ばれません。- 「旧バグ前提」でワークアラウンドを書いていた場合(例:
subscribed側でrejected?をチェックして早期 return していた等)、そのコードは不要になるか、挙動が変わる可能性があります。
サンプルイメージ:
class ChatChannel < ApplicationCable::Channel
before_subscribe :ensure_allowed
def ensure_allowed
reject unless current_user&.allowed_in_chat?
end
def subscribed
# 修正前: allowed_in_chat? == false でも呼ばれてしまうことがあった
# 修正後: allowed_in_chat? == false ならここは呼ばれない
stream_from "chat_#{params[:room_id]}"
end
endこのようなコードは、修正後にようやく「意図した通りの安全な挙動」になります。
- 参考情報 (あれば)
- 対応 Issue: https://github.com/rails/rails/issues/52229
- PR 本体: https://github.com/rails/rails/pull/53101
- 関連コンポーネント:
ActionCable::Channel::BaseActionCable::Channel::Callbacks- コールバック種別:
before_subscribe/subscribed/reject(reject_subscription)
#57500 Fix Array#to_query producing double && when array contains empty hash
マージ日: 2026/5/28 | 作成者: @bogdan
- 概要 (1-2文で)
Array に空 Hash ({}) が含まれている場合にArray#to_queryが&&を連続させた不正なクエリ文字列を生成してしまうバグを修正した PR です。空 Hash 由来の空文字列を結合前に捨てることで、仕様に沿ったapplication/x-www-form-urlencoded形式になるようにしています。
- 変更内容の詳細
問題点
従来の挙動:
{ a: [1, {}, 2] }.to_query
# => "a%5B%5D=1&&a%5B%5D=2"ここで a%5B%5D は a[] を URL エンコードした文字列です。&& の間には何もないため、application/x-www-form-urlencoded としてパースすると「名前も値も空のペア」が 1 つ挿入されたのと同じ扱いになります(WHATWG URL Standard 上は & 区切りで 3 要素と解釈される)。
原因:
Hash#to_queryは「空 Hash の場合は""(空文字)を返す」仕様になっている。Array#to_queryは、要素ごとにto_queryを呼んだ結果を&で join している。- 配列に空 Hash
{}が含まれると、その要素が""になり、join の結果value1 && value2のような文字列ができてしまう。
修正内容
Array#to_query 内で、各要素を to_query した結果の中から空文字列 ("") を除外してから & で連結するように変更しました。
疑似コードイメージ:
# 変更前 (概念的に)
array.map { |v| v.to_query(key) }.join("&")
# 変更後 (概念的に)
array.
map { |v| v.to_query(key) }.
reject { |s| s.empty? }.
join("&")これにより、空 Hash が途中に含まれていてもクエリ文字列は次のようになります。
{ a: [1, {}, 2] }.to_query
# 変更後 => "a%5B%5D=1&a%5B%5D=2"テスト
activesupport/test/core_ext/object/to_query_test.rb に、空 Hash を含む配列を使ったテストが追加されています。
期待値として && ではなく単一の & で繋がることを確認しています。
- 影響範囲・注意点
- 影響範囲:
- ActiveSupport の
Array#to_queryを利用しているコード全般。 - 特に、クライアントサイド・サーバサイド問わず、Rails 側でクエリ文字列を組み立てて外部 API / 内部エンドポイントにリクエストしている箇所で、配列に空 Hash が紛れ込む可能性がある場合に挙動が変わります。
- ActiveSupport の
- 後方互換性:
- 以前から出力されていた
&&付きのクエリは仕様的には誤りに近く、多くの実装では「空のパラメータ 1 個を余計に含む」か「無視される」だけなので、バグ修正として見なせます。 - 逆に「
&&が出ること」自体に依存したコード(文字列比較テストなど)がある場合はテストが落ちます。クエリ文字列の期待値を書いているテストは見直しが必要な場合があります。
- 以前から出力されていた
- 注意点:
Hash#to_queryはもともとトップレベルでは空 Hash をスキップする実装になっているため、今回の変更はその振る舞いと Array のケースを揃える目的があります。- 「空の要素をどう扱うか」に関する仕様がより一貫したので、今後は
Array#to_queryに空 Hash を混ぜても安全に扱えると考えてよいです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57500
- WHATWG URL Standard: https://url.spec.whatwg.org/#concept-urlencoded-parser
application/x-www-form-urlencodedでは&区切りで name/value ペアに分解するため、&&は「空のペア」を 1 つ追加したのと同じことになる。
- 該当コード:
activesupport/lib/active_support/core_ext/object/to_query.rbArray#to_query実装部付近の変更で、要素ごとのto_param/to_query結果から空文字を弾いている。
#57467 Add shims for Ractor shareability methods
マージ日: 2026/5/28 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails(Active Support)に、RubyのRactor関連メソッドをラップする「シム(shim)」が追加され、RubyバージョンごとのRactor有無を意識せずに「Ractor共有可能化」を呼び出せる仕組みが導入されました。今後のRactor対応を見据え、Kernel拡張として統一インターフェースを提供する変更です。
- 変更内容の詳細
追加されたメソッド (Kernel拡張)
ActiveSupport に以下の4つのメソッドが Kernel のコア拡張として追加されています。
ractor_shareable?(obj)ractor_make_shareable(obj)ractor_shareable_proc(&block)ractor_shareable_lambda(&block)(typo ではなく PR 説明どおり)
これらは、実行環境が Ractor をサポートしているかどうかで挙動が変わります。
1. Ractor が利用可能な場合
Ruby 3.0 以降などで Ractor が存在し、対応メソッドが定義されている場合は、そのまま Ractor のメソッドを呼び出す「薄いラッパー」になります。
イメージとしては以下のような実装です(擬似コード):
module Kernel
if defined?(Ractor) && Ractor.respond_to?(:make_shareable)
def ractor_shareable?(obj)
Ractor.shareable?(obj)
end
def ractor_make_shareable(obj)
Ractor.make_shareable(obj)
end
def ractor_shareable_proc(&block)
Ractor.shareable_proc(&block)
end
def ractor_shareable_lambda(&block)
Ractor.shareable_lambda(&block)
end
else
# Ractor がない環境向け (no-op 実装。後述)
end
endこれにより、Rails側のコードは Ruby のバージョンごとに以下のような条件分岐を書かずに済みます。
# 変更前イメージ
if defined?(Ractor) && Ractor.respond_to?(:make_shareable)
Ractor.make_shareable(obj)
end
# 変更後
ractor_make_shareable(obj)2. Ractor が利用できない場合 (古い Ruby など)
Ractor そのものが存在しない、または該当メソッドがない環境では「no-op(何もしない)」実装になります。
ractor_shareable?(obj)
→ 例えば常にfalseを返す、または「常に shareable とみなす」といった実装が想定される(実装詳細は PR diff 参照)。ractor_make_shareable(obj)
→ そのままobjを返すだけなど、副作用を持たない。ractor_shareable_proc,ractor_shareable_lambda
→ 通常のproc/lambdaをそのまま返すか、単にブロックをラップするだけで特別な共有化処理は行わない。
いずれにしても「Ractor対応コードを書いても、Ractor非対応環境では安全にスルーできる」設計になっています。
実装場所
activesupport/lib/active_support/core_ext/kernel/ractor_shareability.rb
上記4メソッドを定義するコア拡張ファイル。activesupport/lib/active_support/core_ext/kernel.rb
ここからractor_shareability拡張が読み込まれるよう 1 行追加。
テストとドキュメント
activesupport/test/core_ext/kernel/ractor_shareability_test.rb
上記メソッド群が期待どおり Ractor 環境/非Ractor環境で動くことを確認するテストが追加。guides/source/active_support_core_extensions.md
Active Support のコア拡張ガイドに、新しいKernel拡張としてこれらのメソッドの使い方が追記。activesupport/CHANGELOG.md
Active Support に新しい Ractor-related Kernel 拡張が入ったことが記載。
- 影響範囲・注意点
影響範囲
- Rails 内部コード
今後、フレームワーク内のさまざまな箇所で Ractor 対応が進む際に、この4メソッドを通じて Ractor shareability を扱うようになります。Ruby バージョンチェックを個別に書く必要がなくなります。 - アプリケーションコード
require "active_support/core_ext/kernel"または Rails 環境下であれば、自前のコードからも同じインターフェースを利用可能です。
例:rubyRactor がない環境では# Ractor 対応コードを書きたい場合 config = { foo: "bar" } ractor_make_shareable(config) Ractor.new(config) do |cfg| # cfg は共有可能なオブジェクトとして扱える endractor_make_shareableは no-op なので、このコード自体はエラーにならずに動作します(ただし当然ながら Ractor 自体は使えません)。
注意点
Kernel 拡張であること
Kernelにメソッドを増やすのは Rails でも慎重に扱われる部分で、名前衝突リスクがあります。- 既に
ractor_shareable?などの名前をアプリが独自定義していると衝突する可能性があります。 - ただし
Ractor自体がかなりニッチかつ新しめの機能であり、かつプレフィックス付きの名前 (ractor_...) なので、衝突リスクは比較的低いと考えられています。
- 既に
no-op 実装であることの意味
Ractor非対応環境では、- 「Ractor 用に設計した安全性」が実際には担保されない
- それでもコードは「動いてしまう」
ため、Ractor あり/なしで挙動(特に並行性まわり)が変わる可能性があります。
そのため、Ractor を本気で使うコードでは、テストを Ractor 対応 Ruby で実行することが重要です。
挙動の違いを前提にしたロジックに注意
例えば以下のような分岐は、Ractor非対応環境では意味が変わる可能性があります。rubyif ractor_shareable?(obj) # Ractor-safe な処理… else # 非Ractor-safe として扱う処理… endno-op 実装で何を返すか(true/false)が重要になるため、実装を確認しておくとよいです。
- 参考情報 (あれば)
- Rails PR: 「Add shims for Ractor shareability methods」 (#57467)
Ractor 対応を進めるための基盤的変更として、まずは「Ruby バージョンに依存しないインターフェース」を提供する目的で導入されています。 - Ruby 公式ドキュメント(参考)
Ractor.make_shareableRactor.shareable?Ractor.shareable_procRactor.shareable_lambda
これら Ruby のメソッドの仕様は、今回追加された Kernel メソッド群の「Ractorあり環境における挙動」とほぼ1対1に対応すると考えてよいです。
#57493 [ci skip] Add step to create path for redirection before launching the application
マージ日: 2026/5/28 | 作成者: @maurobusso
- 概要 (1-2文で)
Railsガイド「Sign Up and Settings」チュートリアルの手順に、パスワード更新後にリダイレクトするためのルート(settings_profile_path)を事前に作成するステップが追加されました。これにより、まだ定義されていないパスヘルパーをチュートリアル中で先に使ってしまう問題が解消されています。
- 変更内容の詳細
- 対象ファイル:
guides/source/sign_up_and_settings.md - 変更内容はドキュメントのみで、Rails本体のコードや挙動は変わっていません。
- 具体的には、チュートリアルの中で「パスワード更新後に
settings_profile_pathへリダイレクトする」コードを示す前に、そのパスが存在するようにルート定義を追加する手順が挿入されています。
おおよそ、以下のような内容がガイドに明示的に加わったと考えられます(イメージ例):
# config/routes.rb
namespace :settings do
resource :profile, only: [:show, :update]
endもしくは:
get "settings/profile", to: "settings/profiles#show", as: :settings_profileといった形で、settings_profile_path が利用できるようになるルーティングを定義するステップが説明として追加されています。
背景として、もともとのチュートリアルでは、コントローラなどで以下のようなコードを先に紹介していました:
redirect_to settings_profile_path, notice: "Your password has been updated."しかし、その時点ではガイド中に settings_profile_path を生成するルート定義を行う記述がなく、読み進める人にとって「このパスヘルパーどこから来たの?」という混乱を生む状態でした。このPRはそのギャップを埋めています。
- 影響範囲・注意点
- 影響範囲は「ガイドの内容・手順」に限定されており、RailsフレームワークのAPIや挙動に変更はありません。
- これからチュートリアルに沿ってアプリを作る人にとって:
settings_profile_path未定義によるエラー(undefined method 'settings_profile_path'など)が起きにくくなります。- ルーティング定義 → コントローラ実装 → リダイレクトの流れがより自然に理解できるようになります。
- 既存アプリケーションへのコード変更は不要です。あくまでドキュメント改善です。
- 参考情報 (あれば)
- 関連Issue: #57471
「チュートリアル内でsettings_profile_pathが定義前に利用されている」という指摘に対する修正PRです。 - PR番号: #57493
CIスキップ指定([ci skip])が付いていることからも分かる通り、コードベースではなくドキュメントのみの変更です。
#57483 Make exception wrapper ractor safe
マージ日: 2026/5/28 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails のActionDispatch::ExceptionWrapperを Ractor セーフにするため、内部で保持している例外関連の設定ハッシュを凍結・再生成する形に変更し、あわせてwrapper_exceptionsとsilent_exceptionsに対する公式な設定インターフェースが追加されました。これによりマルチ Ractor 実行時のスレッドセーフ性を高めつつ、例外ハンドリングをより柔軟に設定できるようになっています。
- 変更内容の詳細
2-1. 例外ラッパーの Ractor セーフ化
対象クラス: ActionDispatch::ExceptionWrapper
もともと以下のようなハッシュをクラスレベルで持っており、Rails 起動後に変更され得る実装になっていました:
rescue_responses: 例外クラス → HTTP ステータスコード / シンボルrescue_templates: 例外クラス → 使用するテンプレート名wrapper_exceptions: ラップ対象の例外 (ActionController::RoutingErrorなど)silent_exceptions: ログなど出力を抑制する例外
Ractor セーフにするため、PR では:
- これらの「ベースになるハッシュ」を
freezeする - 設定で値を追加・上書きする際は「元ハッシュを複製してマージし、それを凍結した新しいハッシュ」を使う
という方針になっています。
これにより「共有されているミュータブルな状態」を排除し、Ractor 間で同じオブジェクトを安全に共有できるようにしています。
2-2. engine 設定を経由したハッシュの構成
ActionDispatch::ExceptionWrapper に関して、既に以下はエンジン設定として存在していました:
config.action_dispatch.rescue_responsesconfig.action_dispatch.rescue_templates
PR では:
- これらの設定値を読み込む際に、元のデフォルトハッシュを
freezeした上で - 「デフォルト + 設定で上書き/追加した値」の新しいハッシュを作成し、それを
freeze
という処理に統一しています。
イメージとしては以下のような動きです (実際のコードは多少異なりますが概念的にはこのような感じ):
DEFAULT_RESCUE_RESPONSES = {
"ActionController::RoutingError" => :not_found,
# ...
}.freeze
def rescue_responses
@rescue_responses ||= begin
merged = DEFAULT_RESCUE_RESPONSES.merge(configured_rescue_responses)
merged.freeze
end
end2-3. wrapper_exceptions と silent_exceptions の公開設定追加
これまで wrapper_exceptions と silent_exceptions は内部定義のみで、config からカスタマイズするための公式な API が存在しませんでした。
PR で:
config.action_dispatch.wrapper_exceptionsconfig.action_dispatch.silent_exceptions
が新たに導入され、config/application.rb や各環境ファイルで設定可能になります。
ガイド (guides/source/configuring.md) にも設定方法が追記されています。
例としては以下のように使えます:
# config/application.rb
module MyApp
class Application < Rails::Application
# ラップ対象の例外を追加/変更
config.action_dispatch.wrapper_exceptions.merge!(
'MyApp::BadRequestError' => :bad_request
)
# ログを出したくない例外を追加
config.action_dispatch.silent_exceptions += [
'MyApp::IgnoredError'
]
end
end内部的には、これらの設定も rescue_responses と同様に:
- デフォルト値は frozen なハッシュ/配列
- 設定で変更が入ると「新しい frozen オブジェクト」を生成
という形で扱われます。
2-4. Railtie 側の変更
actionpack/lib/action_dispatch/railtie.rb では、上記の設定項目を扱うための初期化処理が拡張されています:
config.action_dispatch.wrapper_exceptions/silent_exceptionsの初期値をデフォルト定義から設定- アプリケーション設定を読むタイミングで、ExceptionWrapper に反映し直す処理を追加
これにより:
- 既存のデフォルト挙動は維持しつつ
- config による上書きが Rails 標準の設定フローの中で安全に適用
されるようになっています。
影響範囲・注意点
Ractor を使うアプリケーションでは安全性が向上
- 例外ハンドリング関連のクラス変数/クラスインスタンス変数が不変オブジェクトとして扱われるようになり、Ractor 間でのデータ競合やエラー発生リスクが減ります。
ランタイムでの直接ミューテーションは非推奨 (事実上困難に)
- 以前:
ActionDispatch::ExceptionWrapper.rescue_responses['FooError'] = :not_foundのような書き換えが技術的には可能だった - 今回: 内部ハッシュが
freezeされるため、こうした直接変更はFrozenErrorを発生させる可能性があります - 推奨パターン: すべて
config.action_dispatch.*を通した設定に切り替えること
- 以前:
既存アプリとの互換性
configを通して設定していたアプリは、基本的に互換性あり- アプリコードや initializer で「クラスの内部ハッシュを直接 mutate していた」場合はエラーになる可能性があるため、そのような箇所がないか確認が必要です。
- その場合は以下のように書き換える必要があります:
ruby# NG (これからは FrozenError の可能性) ActionDispatch::ExceptionWrapper.rescue_responses['MyError'] = :not_found # OK: config を通す Rails.application.config.action_dispatch.rescue_responses.merge!( 'MyError' => :not_found )テスト・ドキュメント面
- この PR 自体では新しいテストは追加されていませんが、CHANGELOG とガイドは更新されています。
- ドキュメント更新により、例外ハンドリングの変更方法として
config.action_dispatch.wrapper_exceptions/silent_exceptionsの利用が正式に案内されるようになります。
- 参考情報 (あれば)
対象ファイル:
actionpack/lib/action_dispatch/middleware/exception_wrapper.rb- 例外ラップ処理と、
rescue_responsesなどの保持方法が変更
- 例外ラップ処理と、
actionpack/lib/action_dispatch/railtie.rbconfig.action_dispatch.*の初期化と ExceptionWrapper への反映処理を更新
guides/source/configuring.mdwrapper_exceptions/silent_exceptionsの新しい設定方法が追記
actionpack/CHANGELOG.md- Action Pack の挙動変更として記録
関連する設計上のポイント:
- Ractor セーフ化の基本戦略: 共有オブジェクトは「イミュータブル」にする
- Rails では、クラスレベルの可変な設定を
config経由で受け取り、最終的に frozen したオブジェクトとして共有する方向に少しずつ移行している流れの一環と見ることができます。
#57344 Update Active Job Attributes documentation
マージ日: 2026/5/28 | 作成者: @bdewater-thatch
- 概要 (1-2文で)
Active Job の「Attributes」機能に関するドキュメントを補強し、Railsガイドや Continuation API からも参照できるようにしたうえで、サンプルコードが実際に属性を「使う」例になるよう修正した PR です。機能追加ではなく、主にドキュメントとサンプルの改善によって意図を分かりやすくしています。
- 変更内容の詳細
2-1. Active Job Attributes の説明改善
activejob/lib/active_job/attributes.rb
- Active Job の Attributes 機能の説明コメントおよびサンプルコードが修正されています。
- 以前は「属性に値を保存する」ことは示されていたものの、その値を実際にどう使うかが分かりづらかったため、例を「保存して終わり」ではなく、ジョブ内で実際に
payment_tokenを利用する形に変更しています。
イメージとしては、例えば以下のような流れになります(疑似コードのイメージ):
class ChargeCustomerJob < ApplicationJob
# Active Job Attributes の宣言
attribute :payment_token
def perform
# 以前: payment_token をどこかに保存するだけ、あるいはそもそも使っていなかった
# 今回: 実際に payment_token を使って決済処理をするような例に修正
PaymentGateway.charge(token: payment_token)
end
end
# ジョブのエンキュー例
ChargeCustomerJob.set(attributes: { payment_token: "tok_xyz" }).perform_laterポイント:
attribute :payment_tokenのように「ジョブの属性」を宣言。performメソッドの中で、単に保持するだけでなく、ビジネスロジックに利用する例に変更され、「Attributes を使うと何が嬉しいのか」が伝わりやすくなっています。
2-2. Continuation API からのリンク追加
activejob/lib/active_job/continuation.rb
- Continuation API(ジョブのチェーンや再開処理に関係する API)のドキュメントコメントから、Active Job Attributes へのリンクが追加されています。
- 「Continuation と一緒に Attributes をどう使うか / 使えるのか」を探していた人が、直接該当ドキュメントへたどり着きやすくなっています。
2-3. Rails ガイド (Active Job Basics) の更新
guides/source/active_job_basics.md
- Active Job ガイドに新しく「Attributes」についてのセクション/説明が追加されています。
- Active Job の標準的な使い方の流れ(キュー指定、引数、コールバックなど)の中に、「Attributes」という項目が組み込まれた形になっているはずです。
- ガイドから Active Job Attributes のリファレンスへリンクされており、
「まずガイドで概要をつかんでから、詳細は docs(RDoc/YARDコメント)を読む」という自然な導線ができています。 - 具体的には:
- Attributes の定義方法(
attribute :foo, :barなど) - ジョブのインスタンス変数との違い/使いどころ
- Attributes 付きでジョブを enqueue するパターン
といった内容が、ガイドの中で触れられるようになっています。
- Attributes の定義方法(
- 影響範囲・注意点
- この PR はコードの挙動にはほぼ影響しないドキュメント/サンプル修正です。
- 既存の Active Job の実行パスや API には変更がなく、互換性への影響は基本的にありません。
- ただし、今後 Active Job Attributes/Continuation を使おうとする人は、
「Railsガイドおよびコメントの意図した使い方」に徐々に寄せていくことになるので、
サンプルに引きずられる形で設計方針が変わる可能性はあります。 - 既に Attributes を独自解釈で使っていた場合でも、この PR による破壊的変更はなく、
ただ「公式推奨の使い方」がより明文化された、と捉えて問題ありません。
- 参考情報 (あれば)
- 元となった PR: https://github.com/rails/rails/pull/57208
- Active Job Attributes 機能そのものの追加/改善 PR です。
- PR 内で言及されている混乱の発端:
- https://ruby.social/@getajobmike/116540627534276308
- Attributes の用途や意図が分かりにくかったというフィードバックを受けて、
ドキュメントを明確化したのが本 PR の背景です。
#57496 Fix docs for ActiveSupport::SecurityUtils.fixed_length_secure_compare
マージ日: 2026/5/28 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveSupport::SecurityUtils.fixed_length_secure_compareの RDoc コメントが API ドキュメントに正しく表示されていなかった問題を、コメント位置を修正することで解消したPRです。機能や挙動は一切変えず、ドキュメント生成のためのメタ情報のみを調整しています。
- 変更内容の詳細(あればサンプルコードも含めて)
- 対象:
activesupport/lib/active_support/security_utils.rb - 目的:
fixed_length_secure_compareの説明コメントが、Rails APIドキュメント上に表示されるようにすること。
Rails の RDoc/YARD では、「直前に書かれたコメント」がそのメソッドのドキュメントとして紐づきます。このPRでは:
fixed_length_secure_compareに対応するコメントブロックを、最初のメソッド定義の直前に移動しただけです。- その結果、下記URLにある
fixed_length_secure_compareの説明が正しく出るようになります。
https://api.rubyonrails.org/classes/ActiveSupport/SecurityUtils.html#method-c-fixed_length_secure_compare
擬似的なイメージ(実際のコードから簡略化):
module ActiveSupport
module SecurityUtils
# 固定長の2つの文字列を、タイミング攻撃耐性のある方法で比較します。
# 例:
# ActiveSupport::SecurityUtils.fixed_length_secure_compare(a, b)
#
# 引数の長さが異なる場合は false を返します。
# (コメント位置をこの定義の直前に移動)
def self.fixed_length_secure_compare(a, b)
# 実装は既存のまま
end
end
end実際の差分は、コメントがどの def の直前にあるかを調整しただけで、ロジックの変更はありません。
- 影響範囲・注意点
- 実行時の挙動・APIシグネチャ
- メソッドの処理内容・引数・戻り値は一切変更されていません。
- したがって、既存コードへの影響はゼロです。
- 影響範囲
- 影響するのは 生成されるドキュメント(Rails APIサイトやRDoc出力) のみです。
- ドキュメント利用者にとっては、このメソッドの説明が読めるようになり、
fixed_length_secure_compareの意図や使い方が分かりやすくなります。
- 注意点
- テストは不要と判断されており追加されていません(挙動変更がないため)。
- CHANGELOG も更新されていません(ドキュメント修正のみのため、Rails の運用ポリシーに沿った対応)。
- 参考情報 (あれば)
- 対象メソッドのAPIドキュメント:
https://api.rubyonrails.org/classes/ActiveSupport/SecurityUtils.html#method-c-fixed_length_secure_compare - 関連PR(作業中と書かれているもの):
https://github.com/rails/rails/pull/57467
(こちらの作業中PRの中でドキュメント表示の不具合に気づき、本PRで切り出して修正した形と思われます)
#57489 Improve documentation/testing of abort in Action Mailer `before_action
マージ日: 2026/5/28 | 作成者: @bensheldon
- 概要 (1-2文で)
Action Mailer のbefore_actionコールバックで「送信を中断(abort)する」挙動について、ドキュメントが実装とずれていた部分を修正し、その挙動を保証するテストが追加されています。古いbodyメソッドに依存した誤った説明をやめ、現状の API に即した正しい書き方に更新した PRです。
- 変更内容の詳細
ドキュメント修正 (guides/source/action_mailer_basics.md)
- Action Mailer のコールバック (
before_actionなど) で「メールを中断する」方法の説明が更新されています。 - 以前のドキュメントでは、すでに 2009 年に非推奨になっている
bodyメソッドを前提とした説明が含まれており、現在の実装と齟齬がありました。 - この PR で、以下の点が是正されています。
- 「何をすれば送信処理が abort されるのか」を、現行のインターフェイスに沿って説明し直し。
- 読み手が「body をいじる/body に依存する」ようなコードを書かないように、表現を整理。
PR テキストから推測される典型的な書き方イメージ:
class UserMailer < ApplicationMailer
before_action :ensure_can_be_emailed
def welcome_email(user)
@user = user
mail to: @user.email, subject: "Welcome"
end
private
def ensure_can_be_emailed
# 条件を満たさない場合にメール送信を中断
throw :abort if @user.unsubscribed?
end
end上記のように、コールバック内で throw :abort することで、そのメールの送信処理全体が中止される、という形で説明が明確化されていると考えられます。
テスト追加
1) actionmailer/test/mailers/callback_mailer.rb
- テスト用の
CallbackMailerに、before_actionで abort する振る舞いを持つメソッド/コールバックが追加されています。 - 例: (実際のコードは多少異なる可能性がありますが、概ね以下のようなイメージです)
class CallbackMailer < ActionMailer::Base
before_action :abort_sending, only: :aborted_mail
def aborted_mail
mail to: "test@example.com", subject: "Should not be delivered"
end
private
def abort_sending
throw :abort
end
end2) actionmailer/test/callbacks_test.rb
- 上記
CallbackMailerの動きを検証するテストが追加されています。 - 主な意図:
before_action内でthrow :abortが呼ばれた場合、メールが「生成・送信されない」こと。ActionMailer::Baseの既存のコールバックチェーン実装が、throw :abortを正しく扱えていること。
- 具体的には:
CallbackMailer.aborted_mail.deliver_now/deliver_later等を呼び出した結果として、ActionMailer::Base.deliveriesにメールが追加されないこと、あるいはmailオブジェクトがnilもしくは送信されない状態であること等を assertion しています。
- 影響範囲・注意点
- 機能追加や挙動変更ではなく、「ドキュメントの修正」と「テストの追加」が中心です。
- すでに
before_action内でthrow :abortを使って送信中断しているアプリケーションは、そのままの書き方で問題ありません。PR の目的は「以前から存在していた挙動を、ドキュメントとテストで正確にカバーする」ことです。 - 逆に、古いドキュメントを見て
bodyなどの古い API に依存した中断ロジックを書いていた場合:- 今回のドキュメント更新を機に、「
throw :abortによる中断」に書き換えるのが推奨されます。
- 今回のドキュメント更新を機に、「
- コールバックで
throw :abortした場合:- 後続のフィルタやメール本体の処理が実行されないことに留意してください。
- コールバック内で副作用(ログ出力、フラグ切り替え等)が必要な場合、
throw :abortを呼ぶ位置・条件を明示的に設計しておくと安全です。
- 参考情報 (あれば)
- 関連 PR: Action Mailer ドキュメントに以前手を入れていた PR: #47630
body非推奨化の関連コミット:- 418c3f801c / 4964d3b02c (2009 年頃に Action Mailer の
bodyが deprecated)
- 418c3f801c / 4964d3b02c (2009 年頃に Action Mailer の
- ガイド: Action Mailer Basics (edgeguides)
- 本 PR マージ後、このガイドの「コールバック」「before_action / abort」に関する節が最新の内容になります。
#57495 Reject malformed Action Mailbox raw email params
マージ日: 2026/5/28 | 作成者: @afurm
- 概要 (1–2文で)
Mailgun / Postmark / SendGrid の Action Mailbox ingress において、「生メール(raw email)」パラメータの型を検証し、文字列以外(例: 配列)が渡された場合に 422 Unprocessable Content を返すように修正した PR です。これにより、従来 TypeError になっていたケースが、他の不正入力と同様に適切にハンドリングされます。
- 変更内容の詳細
背景となる問題
- 対象: Action Mailbox の以下の Ingress Controller
- Mailgun:
ActionMailbox::Ingresses::Mailgun::InboundEmailsController - Postmark:
ActionMailbox::Ingresses::Postmark::InboundEmailsController - SendGrid:
ActionMailbox::Ingresses::Sendgrid::InboundEmailsController
- Mailgun:
- これらは、各サービスから Webhook で飛んでくる「生メール」を以下のパラメータで受け取ります:
- Mailgun:
body-mime - Postmark:
RawEmail - SendGrid:
email
- Mailgun:
- もともと「値が必須」であることは前提にしていたものの、「型」が正しいか(文字列かどうか)はチェックしていませんでした。
- その結果、何らかの理由で Array など「配列型」の値がここに入り込むと、後続の処理 (
ActionMailbox::InboundEmail.create_and_extract_message_id!) 内でチェックサム抽出時にTypeErrorが発生してしまい、本来想定している「不正入力としての 422 応答」ではなく例外クラッシュのような扱いになっていました。
今回の修正ポイント
各 ingress controller で raw email パラメータが String であることを検証 し、そうでない場合は 422 を返すようにしました。
擬似コードで表すと、以下のようなイメージです(実際のコードは各コントローラごとに書かれていますが、ロジックは概ね共通):
def create
raw_email = params["body-mime"] # Mailgun の場合の例
# ここが今回追加されたロジックに相当
unless raw_email.is_a?(String)
head :unprocessable_entity
return
end
# 正常な場合のみ ActionMailbox に処理を渡す
ActionMailbox::InboundEmail.create_and_extract_message_id!(raw_email)
head :ok
endPostmark / SendGrid も同様で、それぞれ:
- Postmark:
params["RawEmail"]が String かチェック - SendGrid:
params["email"]が String かチェック
テストの追加
各 ingress ごとに「raw email が不正(配列など)な場合に 422 を返す」ことを確認するテストが追加されています。
たとえば Mailgun 側テストのイメージ:
test "returns 422 for malformed raw email" do
post ingress_mailgun_inbound_emails_url,
params: { "body-mime" => ["not-a-string"] }
assert_response :unprocessable_entity
endPostmark / SendGrid も同様のテストが追加され、「配列型などの不正な raw email パラメータ → 422」という振る舞いが明示的に保証されるようになりました。
CHANGELOG の更新
actionmailbox/CHANGELOG.md に、この不具合修正が記載され、リリースノートから追跡できるよう更新されています。
- 影響範囲・注意点
- 影響するのは Action Mailbox を Mailgun / Postmark / SendGrid ingress で利用しているアプリケーションです。
- これまでは:
- 不正な raw email(配列など)が来ると
TypeErrorが発生し、スタックトレースが出たり、500 系エラーとして扱われる可能性がありました。
- 不正な raw email(配列など)が来ると
- 今後は:
- 同じケースで 422 Unprocessable Content を返す安定した挙動になります。
- 他の「不正な受信者情報や envelope メタデータ」と同じコードパスで処理されるため、エラー挙動が一貫します。
アプリ側での注意点:
- Webhook のレスポンスコードに応じた リトライ制御やアラート連携を行っている場合、
- 以前: サーバ内部エラー(500 系)として扱っていたかもしれない
- 今回以降: 422 でクライアント側(送信側)起因の不正データとみなされる
ため、アラート閾値やリトライポリシーを 422 向けに見直す必要があるかもしれません。
- raw email パラメータに対して独自ミドルウェア等で加工を行っている場合、誤って配列・ハッシュを代入してしまうと、今後は 422 が返るようになります。
通常は外部サービス(Mailgun/Postmark/SendGrid)が正しく送ってくるため問題ありませんが、中継処理を挟んでいる場合はご注意ください。
- 参考情報 (あれば)
- 対応 Issue: #57494
- 対応 PR: #57495 「Reject malformed Action Mailbox raw email params」
- 対象ファイル(主なもの):
actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rbactionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rbactionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb- 各種テスト:
actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rbactionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rbactionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb
#57110 Reduce number of queries during MariaDB column introspection
マージ日: 2026/5/28 | 作成者: @ilianah
- 概要 (1-2文で)
MariaDB 接続時のカラム情報取得処理(カラム introspection)において、無駄なSHOW CREATE TABLEクエリが大量に発行されていた問題を、クエリ回数を最小限に抑える形で修正した PR です。MariaDB の「関数っぽいデフォルト値」を検出するロジックを集約し、非 MariaDB 環境では追加クエリが発生しないようになりました。
- 変更内容の詳細
背景となる問題
- #44654 で「MariaDB における関数デフォルトの検出」が修正された際、
AbstractMysqlAdapter#new_column_from_field内に MariaDB 向けのチェックが追加された。 - その結果、カラムごとに
SHOW CREATE TABLEを実行する実装になっており、- スキーマのカラム数が多いアプリケーションでは、
columns呼び出し 1 回につき大量のSHOW CREATE TABLEクエリが発行される - MariaDB ではない(MySQL 等の)接続でも不要なフックが走る
というパフォーマンス上の問題が出ていた。
- スキーマのカラム数が多いアプリケーションでは、
今回の方針
MariaDB 固有の「関数デフォルト検出」を
new_column_from_fieldから切り離し、より高いレイヤであるcolumn_definitionsに移動。フローとしては次のように変更:
- まず
SHOW FULL FIELDSを実行して、テーブルの各カラムの情報を取得。 - 接続先が MariaDB の場合のみ、取得したフィールドの
Defaultを走査し、関数呼び出しのように見える値があるかを確認。- 例:
CURRENT_TIMESTAMP,uuid(),now()など、xxx(...)の形や、MariaDB で関数扱いされる既知の値。
- 例:
- 少なくとも 1 つでも「関数っぽい Default」が見つかった場合に限り、テーブルごとに 1 回だけ
SHOW CREATE TABLEを発行。 - その
SHOW CREATE TABLEの結果を解析し、該当するカラムをDEFAULT_GENERATEDとしてメタデータにマークする。 - こうしてマークされたカラムは、既に MySQL 側で使われている
new_column_from_fieldのDEFAULT_GENERATED分岐に乗るようになり、MariaDB でも正しく「関数デフォルト」として扱われる。
- まず
コード上の主な変更点
1) abstract_mysql_adapter.rb
AbstractMysqlAdapter に、MariaDB 専用のデフォルト値判定ロジックと、それを踏まえたカラム定義処理が追加されています。おおまかなイメージ:
def column_definitions(table_name)
fields = exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}")
if mariadb?
# 1. Default が関数っぽいフィールドがあるか確認
if fields.any? { |f| function_like_default?(f["Default"]) }
# 2. 必要な場合だけ SHOW CREATE TABLE を 1 回だけ実行
table_ddl = exec_query("SHOW CREATE TABLE #{quote_table_name(table_name)}").first["Create Table"]
# 3. DDL から DEFAULT_GENERATED なカラムを特定し、fields にメタデータを付与
mark_generated_defaults_from_ddl!(fields, table_ddl)
end
end
fields.map { |field| new_column_from_field(field) }
endポイント:
- MariaDB でない場合 (
mariadb?が false) には、この判定はそもそも実行されない。 - 「関数っぽい Default」を持つカラムが 0 件であれば
SHOW CREATE TABLE自体をスキップする。
2) mysql/schema_statements.rb
元々ここにあった MariaDB 向けの関数デフォルト検出ロジックが整理・削減され、AbstractMysqlAdapter 側に集約されています。
これにより、MariaDB と MySQL で「DEFAULT_GENERATED を用いた関数デフォルトの扱い」を共通化しつつ、MariaDB 特有のクエリは最小限に抑える構成になりました。
3) テスト (migration_test.rb)
- 前述の挙動変更に合わせて、マイグレーション関連テストの期待値が微調整されています。
- 主に「カラムのデフォルトが関数の場合に、スキーマ上どう解釈されるか」に関する部分が、今回の新しい経路(
DEFAULT_GENERATEDマーク →new_column_from_fieldの MySQL 共通ルート)に沿うように更新されています。
- 影響範囲・注意点
対象DB:
- MariaDB 接続時の
columns/ スキーマ introspection 処理が主対象。 - MySQL / 他 DB アダプタには影響しない設計(MariaDB 判定を条件分岐している)になっている。
- MariaDB 接続時の
性能面の影響:
- 以前 (#44654 後) は「カラムごとに
SHOW CREATE TABLE」だったケースが、「テーブルごとに最大 1 回」に減少。 - カラム数が多いテーブルを多数持つアプリでは、スキーマロード・マイグレーション・コンソール起動時などに発行されるクエリ数が大幅に削減される可能性が高い。
- 以前 (#44654 後) は「カラムごとに
機能面の影響:
- MariaDB 上で「関数をデフォルト値に持つカラム」を introspect したとき、
- 以前と同様、関数デフォルトとして正しく扱われる(#44654 の修正は維持)。
- 実装上の経路は変わったが、最終的な
ActiveRecord::ConnectionAdapters::Columnの属性は互換になるよう配慮されている。
- MariaDB 上で「関数をデフォルト値に持つカラム」を introspect したとき、
注意点:
- MariaDB の
SHOW CREATE TABLE出力フォーマットに依存したパース・マッピング処理を行うため、- 将来 MariaDB 側で DDL 出力フォーマットが大きく変わると、
DEFAULT_GENERATEDマーク付けがずれる可能性はある。
- 将来 MariaDB 側で DDL 出力フォーマットが大きく変わると、
- ただし、従来も同様に MariaDB 固有の挙動に依存していたため、リスクは増えてはいない。
- MariaDB の
- 参考情報 (あれば)
- 関連PR:
- #44654 — MariaDB の関数デフォルト検出を最初に導入した PR。
本 PR は、その実装によるクエリ数増大の副作用を解消する位置づけ。
- #44654 — MariaDB の関数デフォルト検出を最初に導入した PR。
- 実装上のキーワード:
SHOW FULL FIELDS FROM <table>SHOW CREATE TABLE <table>DEFAULT_GENERATEDメタデータAbstractMysqlAdapter#column_definitions- MariaDB 判定 (
mariadb?など)
MariaDB を使っていてスキーマが大きいアプリケーションでは、アップデートにより introspection 時のクエリ数削減・レスポンス改善が期待できます。
#57470 Fix number_to_delimited mangling non-finite floats
マージ日: 2026/5/28 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::NumberHelper.number_to_delimitedがFloat::INFINITYなどの非有限浮動小数点数を不正に区切ってしまうリグレッションを修正し、非有限値はそのまま文字列表現を返すようにした PR です。これにより"Infinity"が"In,fin,ity"になる問題が解消されます。
- 変更内容の詳細
問題点
number_to_delimited は数値を3桁ごとに区切るヘルパですが、最近入ったパフォーマンス改善の影響で、Float::INFINITY 等に対して以下のような誤った結果を返していました。
ActiveSupport::NumberHelper.number_to_delimited(Float::INFINITY)
# => "In,fin,ity" 💀
ActiveSupport::NumberHelper.number_to_delimited(-Float::INFINITY)
# => "-In,fin,ity"
ActiveSupport::NumberHelper.number_to_delimited(Float::NAN)
# => "NaN" # たまたま3文字なので崩れなかった原因は、NumberToDelimitedConverter に導入された「手動スライスによる高速パス」が Float::INFINITY.to_s # => "Infinity" を「数字列」と見なして右から3文字ずつ区切ってしまったためです。
以前は正規表現ベースの実装で、Infinity は数字を含まないためマッチせず、そのまま返されていました。
修正内容
NumberToDelimitedConverter#parts の最初で、「有限でない数値」を特別扱いし、区切り処理に入らないようにしました。
差分(概略):
def parts
+ return [number.to_s] if number.respond_to?(:finite?) && !number.finite?
+
left, right = number.to_s.split(".")
if delimiter_pattern
# 既存の区切り処理
else
# 高速パス (手動スライス)
end
endポイント:
number.respond_to?(:finite?) && !number.finite?Floatなどfinite?を持つオブジェクトで、かつ有限でないもの (Infinity,-Infinity,NaN) を検出。
- その場合は
number.to_sをそのまま 1 要素の配列で返し、最終的なparts.joinで区切りなしの文字列が得られる。
修正後の挙動:
ActiveSupport::NumberHelper.number_to_delimited(Float::INFINITY)
# => "Infinity"
ActiveSupport::NumberHelper.number_to_delimited(-Float::INFINITY)
# => "-Infinity"
ActiveSupport::NumberHelper.number_to_delimited(Float::NAN)
# => "NaN"これは既に number_to_phone(Float::INFINITY) # => "Infinity" が行っている挙動と整合的です。
テスト・ドキュメント
activesupport/test/number_helper_test.rbに、Float::INFINITY/-Float::INFINITY/Float::NANに対する期待値を確認するテストを追加。activesupport/CHANGELOG.mdに本バグと修正内容を追記。
- 影響範囲・注意点
- 影響範囲
ActiveSupport::NumberHelper.number_to_delimited(およびそれを内部的に利用しているヘルパ)で、Float::INFINITY/-Float::INFINITY/Float::NANを扱うケースに影響します。- 以前(リグレッション前)の挙動に戻る形で、
Infinity/-Infinity/NaNをそのまま返すようになります。
- 互換性
- 直近数リリース(高速化導入後)でだけ
"In,fin,ity"のような壊れた文字列が返っていたため、それに依存しているコードがある可能性はほぼありません。 - 数値フォーマット周りで「非有限値をどう扱うか」は API デザイン上の論点ですが、この PR は既存の
number_to_phoneに合わせただけで、新たな仕様変更ではありません。
- 直近数リリース(高速化導入後)でだけ
- 他 API との整合性
number_to_human/number_to_human_sizeはFloat::INFINITYに対してFloatDomainErrorを投げ続けており、この PR では変更されていません(追って検討する候補として #57451 で言及)。
- 参考情報 (あれば)
- このバグを引き起こしたパフォーマンス関連コミット:
2d485aecf5(2026-03-04, CVE-2026-33169): 3文字ごとの手動スライスによる高速パス導入。33fbedb1b1(2026-03-26):delimiter_patternのデフォルトをnilに変更し、高速パスをデフォルト経路に。
- 関連 PR:
- #57451: 別のコンバータにおける
FloatDomainError対応 (number_to_human系) に関する議論。
- #57451: 別のコンバータにおける
#57451 Format non-finite numbers consistently in NumberHelper significant mode
マージ日: 2026/5/28 | 作成者: @55728
- 概要 (1-2文で)
ActiveSupport::NumberHelperのnumber_to_roundedなどでsignificant: trueを指定した際、Infinity/-Infinity/NaNを渡すとFloatDomainErrorで落ちていた不具合を修正し、非 significant モードと同様に"Inf"/"-Inf"/"NaN"として一貫してフォーマットされるようにした PR です。
これにより、同じヘルパー内で significant オプションの有無による挙動の不整合が解消されています。
- 変更内容の詳細
問題のあった挙動
もともと significant: false(デフォルト)では、無限大や NaN をよしなに文字列化していました:
ActiveSupport::NumberHelper.number_to_rounded(Float::INFINITY, precision: 3)
# => "Inf"
ActiveSupport::NumberHelper.number_to_rounded(Float::NAN, precision: 3)
# => "NaN"ところが significant: true を加えると同じ値で例外が発生していました:
ActiveSupport::NumberHelper.number_to_rounded(Float::INFINITY, precision: 3, significant: true)
# => FloatDomainError: Infinityこの例外は RoundingHelper を経由する以下のヘルパーにも波及します:
number_to_roundednumber_to_percentagenumber_to_currencynumber_to_delimited
典型例:
total = 100.0
seconds = 0
rate = total / seconds # => Infinity
number_to_rounded(rate, precision: 3, significant: true)
# 期待: "Inf"
# 実際: FloatDomainError原因
例外は RoundingHelper#digit_count 内の Math.log10 呼び出しで発生していました。
def digit_count(number)
return 1 if number.zero?
(Math.log10(number.abs) + 1).floor
# Math.log10(Infinity) #=> Infinity
# Infinity.floor #=> FloatDomainError
enddigit_count は以下の場面で使われます:
significant: true && precision > 0のときのabsolute_precision計算NumberToRoundedConverter#convert内で有効桁数に応じて precision を調整する処理
つまり、Infinity / NaN を significant モードで扱うと必ずここを通り、Math.log10 が例外の原因となっていました。
修正内容
digit_count に「有限数かどうか」のチェックを追加し、非有限値(Infinity, -Infinity, NaN)の場合は 1 桁として扱うようにしました。
差分イメージ(説明用・概念的なコード):
def digit_count(number)
return 1 if number.zero?
return 1 unless number.respond_to?(:finite?) && number.finite?
(Math.log10(number.abs) + 1).floor
endここで 1 を返す理由・妥当性:
absolute_precisionはおおむねprecision - digit_count(number)のように使われるため、digit_count == 1ならprecision - 1として自然な形で処理が継続できるBigDecimal("Infinity").round(n)はどんなnでもInfinityのまま変化しないその後の
NumberToRoundedConverter#convertにはすでにrubyif rounded_number.finite? # 通常のフォーマット else "%f" % rounded_number endという有限値チェックがあり、ここで
"Inf","-Inf","NaN"にフォーマットされる設計になっているもともと 0 の場合も
digit_countは1を返しており、その分岐は今回の変更より前に評価されるため、以前正常に動いていたケースはそのまま維持される
結果として、「非有限値をうまくやり過ごして既存の分岐まで処理を届ける」ための、最小限で局所的な防御を追加した形になっています。
テスト・検証
activesupport/test/number_helper_test.rb に以下のようなテストが追加されています(内容の要旨):
number_to_roundedにInfinity,-Infinity,NaNを渡し、significant: trueでそれぞれ"Inf","-Inf","NaN"になること- 同じく
number_to_percentage,number_to_currencyでも"Inf%","$Inf"など期待される文字列になること
全体として:
- 23 テスト, 843 アサーション, 失敗・エラーなし
activesupport/CHANGELOG.md も更新され、挙動変更(バグ修正)が明記されています。
- 影響範囲・注意点
影響を受けるヘルパー(挙動が改善されるもの)
以下で significant: true を指定した際に、Infinity / -Infinity / NaN を渡しても例外が出ず、非 significant モードと同じ文字列を返すようになります。
number_to_roundednumber_to_percentagenumber_to_currencynumber_to_delimited
期待される新しい挙動の例:
number_to_rounded(Float::INFINITY, precision: 3, significant: true)
# => "Inf"
number_to_percentage(Float::INFINITY, precision: 3, significant: true)
# => "Inf%"
number_to_currency(Float::INFINITY, precision: 3, significant: true)
# => "$Inf"
number_to_rounded(Float::NAN, precision: 3, significant: true)
# => "NaN"影響を受けないヘルパー(依然として例外を出しうるもの)
number_to_humannumber_to_human_size
これらは RoundingHelper 経由ではなく、独自の calculate_exponent や exponent の中で Math.log10 / Math.log を直接呼び出しているため、Infinity を渡すと引き続き FloatDomainError になる可能性があります。
この PR はあくまで RoundingHelper 経路の修正に限定されており、number_to_human 系の扱い("Inf" / "NaN" が人間にとって読みやすいかどうか等)は別 PR で検討する前提になっています。
後方互換性
- 有限値に対するフォーマットロジック(桁数の計算や rounding 動作)は変えていないため、通常の数値入力での出力形式は変わりません。
- 無限大や NaN については、これまで「例外が出ていた」ケースが「文字列を返す」ようになるだけなので、既存コードが rescue していた場合でも後方互換性上の問題はほぼありません(ログに出る内容等だけ注意)。
- 参考情報 (あれば)
- 本 PR で参考にされている旧 PR:
- 2016 年の試み #25946
number_to_rounded/number_to_percentage/number_to_humanを一気に直そうとして、number_to_humanの"Inf"/"NaN"表現が人間向けでないとの指摘でクローズ- 今回は
RoundingHelper経路(=number_to_rounded/_percentage/_currency/_delimited)に限定することで、その批判ポイントを避け、同一ヘルパー内の整合性回復に scope を絞っています。
- 2016 年の試み #25946
- 実装設計上のポイント:
digit_countは初期実装時からMath.log10(number.abs).floorベースだったが、NumberToRoundedConverter側にはすでにfinite?チェックがあり、設計的には非有限値もサポートする意図があったと推測されるため、その意図に合わせてガードを追加した形になっている。
#57492 Reject malformed Action Mailbox original recipients
マージ日: 2026/5/28 | 作成者: @afurm
- 概要 (1-2文で)
- Action Mailbox の Mailgun/Postmark インバウンド受信処理で、オプションの「元の宛先」パラメータが不正フォーマットだった場合に
TypeErrorで落ちていた問題を修正し、422 Unprocessable Content を返して安全に拒否するようにした PR です。 - これにより、Mailgun の
recipientと Postmark のOriginalRecipientが壊れていてもアプリ側が例外で落ちず、クライアントに正しいエラーを返せるようになります。
- 変更内容の詳細
背景
- Mailgun / Postmark は、受信メールを Webhook で Rails に渡す際に「元々の宛先」を表す値を送ることがあります:
- Mailgun:
recipientパラメータ - Postmark:
OriginalRecipientパラメータ
- Mailgun:
- Action Mailbox ではこれらを使って、RAW メールに
X-Original-Toヘッダを付ける処理をしていました。 - しかし、これらの値が「文字列ではない」「文字列として扱えない」などで不正な場合、そのまま
String#prependに渡されてTypeErrorが発生していました。
この PR の主な対応
1) Mailgun 受信コントローラのバリデーション追加
対象ファイル:actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb
- もともとやっていたこと (概念的には):
raw_email = params.require(:body_mime)
if (original_recipient = params["recipient"]).present?
raw_email.prepend("X-Original-To: #{original_recipient}\n")
end- PR で追加された点:
recipientが「to_sした結果をメールアドレスとして扱える / ヘッダとして妥当」かを事前チェック。- 不正な場合は 422 Unprocessable Content を返し、
String#prependを呼ばない。
イメージとしては以下のような流れになります (実装の雰囲気):
def create
original_recipient = params[:recipient]
if original_recipient.present? && !valid_original_recipient?(original_recipient)
head :unprocessable_content # 422
return
end
raw_email = params.require(:body_mime)
if original_recipient.present?
raw_email.prepend("X-Original-To: #{original_recipient}\n")
end
# 既存のメール処理へ…
end
private
def valid_original_recipient?(value)
value.is_a?(String) && value.present?
# 実際にはもっと厳密 or 既存の Action Mailbox のバリデーションに沿ったチェック
end※ 実際のコードは Rails 内部のヘルパや共通メソッドを使っている可能性がありますが、概ねこのように「事前チェック → NG なら 422」という構造になっています。
2) Postmark 受信コントローラのバリデーション追加
対象ファイル:actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb
- もともとの処理イメージ:
raw_email = params.require(:raw_email)
if (original_recipient = params["OriginalRecipient"]).present?
raw_email.prepend("X-Original-To: #{original_recipient}\n")
end- Mailgun と同様、
OriginalRecipientが不正な場合に 422 を返して処理を中断するチェックが追加されています。 - バリデーションの方針は Mailgun と同様で、「
String#prependに安全に渡せるか」を最低限保証するものです。
3) テストの追加
対象ファイル:
actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rbactionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb
主なテスト内容:
- 「malformed recipient / malformed original recipient」という名前のテストが追加されており、以下を検証:
- 不正な
recipient/OriginalRecipientを含むリクエストを送る。 - レスポンスが 422 Unprocessable Content になること。
- サーバ側で
TypeErrorなどの例外が発生せず、エラーとして正しく終了すること。
- 不正な
4) CHANGELOG の更新
対象ファイル:actionmailbox/CHANGELOG.md
- Action Mailbox の変更点として「Malformed な original recipient を拒否するようになった」旨が追記されています。
- 将来的な Rails バージョンアップ時に、この挙動変更がリリースノートとして分かるようになります。
- 影響範囲・注意点
- 対象: Action Mailbox を使っており、Mailgun または Postmark の Ingress を利用しているアプリケーション。
- 今まで:
- Mailgun/Postmark 側のバグや悪意ある入力により
recipient/OriginalRecipientが壊れていた場合、 - Rails 側で
TypeErrorが発生 → 500 エラーになる、もしくはジョブが落ちる、ログが汚れる、などの不安定な挙動となる可能性がありました。
- Mailgun/Postmark 側のバグや悪意ある入力により
- これから:
- そうした不正な値は検出され、422 Unprocessable Content として明示的に拒否されます。
- アプリ側は「そのメールは受理されなかった」ことを前提に扱う必要がありますが、これはもともと正常に処理できなかったケースなので仕様として自然です。
- ログ・監視上の注意:
- この変更後、Mailgun/Postmark 経由の Webhook で 422 応答が出る可能性があります。
- ログやエラーレポートで 500 が減って 422 が増える可能性があるため、監視閾値やアラート設定をしている場合は挙動の変化に注意してください。
- 互換性:
- 正常な
recipient/OriginalRecipientを送っている限り、挙動は従来通りで互換性があります。 - エラーコードが「500 から 422 に変わる」程度なので、Webhook 側で応答コードに依存した独自リトライやハンドリングをしている場合は確認するとよいです。
- 正常な
- 参考情報 (あれば)
- 関連 Issue: #57491
「Mailgun / Postmark の original recipient が壊れていると Action Mailbox で TypeError が出る」問題報告。 - PR タイトル: Reject malformed Action Mailbox original recipients (#57492)
- 影響するプロバイダ:
- Mailgun Ingress:
ActionMailbox::Ingresses::Mailgun::InboundEmailsController - Postmark Ingress:
ActionMailbox::Ingresses::Postmark::InboundEmailsController
- Mailgun Ingress:
- 追加のテストコマンド例 (PR 説明より):
- Mailgun の malformed recipient テストのみ:
cd actionmailbox && bin/test test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb -i "/malformed recipient/"
- Postmark の malformed original recipient テストのみ:
cd actionmailbox && bin/test test/controllers/ingresses/postmark/inbound_emails_controller_test.rb -i "/malformed original recipient/"
- Mailgun の malformed recipient テストのみ:
#57477 Refactor ActiveJob::TestHelper to no longer walk descendants
マージ日: 2026/5/28 | 作成者: @byroot
- 概要 (1-2文で)
ActiveJob::TestHelper の内部実装がリファクタされ、テスト時の設定リセットで ActiveJob::Base の子クラスを総なめ(descendants walk)しなくてよくなりました。これにより、テスト teardown が高速かつ安定になり、関連する不具合 (#57440, #57441) が修正されています。
- 変更内容の詳細
何をやめたのか
これまで ActiveJob::TestHelper は、class_attribute を使って ActiveJob::Base 直下のクラスに設定を伝播していました。class_attribute の値をテストごとに上書きすると、teardown 時に「どのクラスに変更が入ったか」を把握するために ActiveJob::Base.descendants を走査する必要がありました。
- 問題点:
- descendants の走査はクラス数に比例して O(n) でコストがかかる
- 再読み込みや autoload のタイミングによって不安定になることがある
- 一部ケースで設定がきちんとリセットされず、バグ報告 (#57440, #57441) の原因になっていた
何に置き換えたのか
この PR では「全子孫クラスを探しに行く」というアプローチをやめて、「どの値をどう上書きしたか」を内部のデータ構造に直接記録する方式に変えています。
- ポイント:
- ActiveJob::TestHelper が使う設定のオーバーライド情報を、専用の内部ハッシュやセットに保持
- teardown 時には、
class_attributeを巻き戻す代わりに、これらの内部データ構造を O(1) でクリアするだけで済む - つまり「グローバルな一括リセット」→「テストヘルパー内部の局所状態のクリア」に変わったイメージ
具体的な API の振る舞いや public インターフェイス(例: perform_enqueued_jobs, perform_enqueued_at_jobs, assert_enqueued_jobs など)が変わったわけではなく、内部的な状態管理の方法のみが変更されています。
Active Storage のテストの追加
変更ファイルに railties/test/application/active_storage/analyzers_integration_test.rb が含まれており、ここにテストが追加されています (+9 行)。
- 追加されたのは、ActiveJob::TestHelper の新しい実装により、Active Storage のアナライザーがジョブ実行まわりで期待通り動くかを確認するためのテスト
- ActiveStorage の integration test で
- ActiveJob のテストヘルパーによるキュー実行/管理
- その teardown 時の状態リセット などが正常に機能することを保証する意図があります。
- 影響範囲・注意点
影響するのは主にテスト実行時のみ
ActiveJob::TestHelper の内部実装変更なので、本番コード(ジョブの enqueue や perform)には直接の影響はありません。パブリック API は原則そのまま
既存の次のようなコードは、基本的にそのまま動作します。rubyclass MyJobTest < ActiveJob::TestCase include ActiveJob::TestHelper def teardown clear_enqueued_jobs clear_performed_jobs end endもし内部実装に依存するメタプログラミングをしていなければ、互換性問題はほぼありません。
パフォーマンスと安定性の向上
- 大量の Job クラスを定義しているプロジェクトで、テストの teardown が速くなる可能性があります。
- クラス再読み込みや並行テスト環境で起こりうる、descendants 走査に起因する妙な不具合(設定が残留する、特定ジョブのテストだけ落ちる等)が減ることが期待されます。
ActiveJob::Base.descendants への依存が減る
- ActiveJob::TestHelper 内部で descendants に依存しなくなったため、Rails の autoload / Zeitwerk reload 周りと競合しにくくなります。
- もし自分のテストヘルパーで「ActiveJob::TestHelper の teardown を前提に descendants をいじる」ような特殊なことをしている場合は、挙動が変わる可能性があります(かなりニッチなケースです)。
- 参考情報 (あれば)
対応 issue:
関連するクローズ済み PR:
対象コード:
activejob/lib/active_job/test_helper.rb- 状態管理の仕組みが
class_attributeベースから、内部データ構造ベースに刷新
- 状態管理の仕組みが
railties/test/application/active_storage/analyzers_integration_test.rb- Active Storage の integration test が追加され、新実装の回りの動作がカバーされている
#57481 Simplify more activerecord tests with NotificationAssertions helpers
マージ日: 2026/5/28 | 作成者: @Edilbek
- 概要 (1-2文で)
Active Record のテストコードで、ActiveSupport::Notifications.subscribeを直接使っていた箇所の多くを、NotificationAssertionsのcapture_notificationsなどのヘルパーに置き換えたリファクタリングです。挙動の変更はなく、テストの可読性・メンテナンス性の向上のみを目的としています。
- 変更内容の詳細
全体方針
- 既存テストでは、通知の検証に以下のような「手書き subscribe パターン」が多く使われていました:
ActiveSupport::Notifications.subscribe("sql.active_record")で購読- ブロック内でフラグを立てたり (
asserted = true)、直接assert_equal等を実行 ensureでunsubscribeする、またはsubscribedを使う
- これを、
NotificationAssertionsモジュールのヘルパー (特にcapture_notifications) に置き換えています。 - 例外的に、非同期スレッドから発火する通知を検証するテストでは、従来の
subscribeを維持しています (subscribedがスレッドローカルであり、クロススレッドの通知を確実に拾えないため)。
asynchronous_queries_test.rb の変更
対象: Active Record の非同期クエリ (load_async など) に関するテスト。
置き換え前(イメージ):
rubyasserted = false subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| event = ActiveSupport::Notifications::Event.new(*args) if event.payload[:name] == "User Load" asserted = true assert_in_delta 0.1, event.duration end end begin User.load_async.to_a ensure ActiveSupport::Notifications.unsubscribe(subscriber) end assert asserted置き換え後(イメージ):
rubyevents = capture_notifications("sql.active_record") do User.load_async.to_a end user_events = events.select { _1.payload[:name] == "User Load" } assert_not_empty user_events assert_in_delta 0.1, user_events.first.duration
ポイント
assertedフラグやensureでのunsubscribeが不要になり、テストが「何を検証しているのか」が読みやすくなっています。capture_notificationsは、指定したイベント名の通知を配列として返すため、後からフィルタして通常のオブジェクトに対するアサーションと同じ感覚で書けます。
connection_pool_test.rb の変更
対象: コネクションプール関連のテスト。
ActiveSupport::Notificationsで発火される、例えばsql.active_recordやプール関連のイベントに対するテストをcapture_notificationsベースに移行。典型的なパターン:
置き換え前:
rubyasserted = false ActiveSupport::Notifications.subscribed(->(*args) { event = ActiveSupport::Notifications::Event.new(*args) asserted = true if event.payload[:connection_id] == connection.object_id }, "sql.active_record") do # テスト対象処理 end assert asserted置き換え後:
rubyevents = capture_notifications("sql.active_record") do # テスト対象処理 end assert events.any? { _1.payload[:connection_id] == connection.object_id }
ポイント
ActiveSupport::Notifications.subscribedの入れ子構造がなくなり、テストの main path に集中しやすくなっています。- 失敗時も、
eventsの中身をデバッグしやすくなります(どの通知も飛んでいないのか、条件に合わないのかが切り分けやすい)。
relation/load_async_test.rb の変更
対象: Relation#load_async 関連のテスト。
PR 説明に明記されている通り、非同期スレッド側で発火する通知を対象にしているテストについては、引き続き 直接
ActiveSupport::Notifications.subscribeを使用 しています。理由:
capture_notificationsの内部実装はActiveSupport::Notifications.subscribedを使っており、これはスレッドローカル。- そのため、テストスレッド以外(非同期クエリスレッド)で出る
sql.active_recordイベントを確実に取れない。
一方で、テストスレッド上で通知が発火するケース(例:
wait_for_async_query後にto_aすることでロード完了時に通知が出るようなケース)はcapture_notificationsに移行。イメージ:
rubyevents = capture_notifications("sql.active_record") do relation = User.all.load_async relation.wait_for_async_query relation.to_a # このあたりでテストスレッド上で通知が出る end assert events.any? { _1.payload[:name] == "User Load" }
ポイント
- スレッド境界をまたぐ通知テストは
subscribeを維持し、そうでない箇所だけcapture_notificationsに寄せており、ヘルパー導入に伴うテスト信頼性の低下を避けています。 - 「どこから発火する通知なのか(どのスレッドか)」を意識してヘルパー適用範囲を調整しているのがポイントです。
- 影響範囲・注意点
影響範囲:
- 変更は
activerecord/test/cases以下の 3 ファイルのみで、本番コードには一切変更なし。 - テストの書き方が変わっただけなので、仕様・パブリック API・パフォーマンスへの影響はありません。
- 変更は
注意点 (テストを書く側向け):
capture_notificationsはActiveSupport::Notifications.subscribedベースであり、スレッドローカル であることを意識する必要があります。- 非同期処理で別スレッドから発火する通知をテストしたい場合は、従来どおり
ActiveSupport::Notifications.subscribeを使うべきです。
- 非同期処理で別スレッドから発火する通知をテストしたい場合は、従来どおり
- 通知テストを新たに書く際は、以下を目安に選ぶとよいです:
- 同期処理 or 同一スレッドの通知 →
capture_notifications推奨 - 別スレッド、バックグラウンドジョブなど → 手動
subscribe/unsubscribeor 専用ラッパー
- 同期処理 or 同一スレッドの通知 →
- 参考情報 (あれば)
前提 PR: #57320
NotificationAssertionsヘルパーの導入・適用を始めた PR。今回の PR はそのフォローアップで、残りのactiverecordテストへの適用を進めたものです。NotificationAssertionsの代表的な使い方(概念的な例):rubyevents = capture_notifications("sql.active_record") do User.first end sqls = events.map { _1.payload[:sql] } assert_includes sqls, "SELECT \"users\".* FROM \"users\" ORDER BY \"users\".\"id\" ASC LIMIT ?"
この PR を踏まえると、新規の通知検証テストはまず NotificationAssertions を使うことを検討し、非同期・マルチスレッドなケースのみ旧来の subscribe パターンを残す、という方針が推奨されます。
#57485 Reject malformed Mailgun signatures
マージ日: 2026/5/28 | 作成者: @afurm
- 概要 (1-2文で)
Mailgun からの Webhook 受信(Action Mailbox Mailgun ingress)で、署名パラメータsignatureの形が不正な場合に、secure_compare実行前に認証エラーとして 401 を返すようにした修正です。これにより、配列などの想定外な形でsignatureが送られてきたときに例外で落ちるのではなく、きちんと認証失敗として扱われるようになります。
- 変更内容の詳細
何を直したか
既存コードは「Mailgun の署名パラメータ signature は必ず文字列」という前提で、次のような流れで処理していました:
# イメージ(実際のコードから要約)
if ActiveSupport::SecurityUtils.secure_compare(provided_signature, expected_signature)
# 認証成功
else
head :unauthorized
endところが、リクエストのパラメータ解釈によって params[:signature] が配列(Array)など、想定外の型になるケースがあり、その場合 secure_compare が内部で型エラーを起こして例外が発生していました。本来は 401 Unauthorized を返したいのに、アプリケーションエラーになってしまうのが問題です。
具体的な修正内容
signatureが文字列かを明示的にチェック
actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb にて、secure_compare を呼ぶ前に、signature が String であることを確認する条件が追加されています。
イメージとしては次のような形になります(実際のコードから簡略化):
provided_signature = params[:signature]
unless provided_signature.is_a?(String)
# 文字列でなければ認証失敗として扱う
head :unauthorized and return
end
if ActiveSupport::SecurityUtils.secure_compare(provided_signature, expected_signature)
# 認証成功の処理
else
head :unauthorized
end元々は is_a?(String) のような型チェックがなく、配列が来たときに secure_compare 内部でエラーになっていましたが、それを防いでいます。
- 回帰テストの追加
actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb に、署名パラメータが配列のケースを再現するテストが追加されています。
ざっくりとしたテストのイメージ:
test "rejects requests with array-shaped signature" do
post action_mailbox_ingress_mailgun_url,
params: { signature: ["not-a-string"], # ← わざと配列にする
...他の必須パラメータ... }
assert_response :unauthorized
endこれにより、今後の変更で同様の不具合が再発してもテストで検知できるようになります。
- CHANGELOG の更新
actionmailbox/CHANGELOG.md に、Mailgun ingress の署名検証まわりが修正されたことが追記されています。
アプリケーションの観点からは挙動の「安定性」が増した変更であり、外から見える仕様としては「不正な signature 形状でも 401 が返る」という改善になります。
- 影響範囲・注意点
- 対象機能
- Action Mailbox の Mailgun ingress (
ActionMailbox::Ingresses::Mailgun::InboundEmailsController) の認証処理のみが対象です。
- Action Mailbox の Mailgun ingress (
- 本番挙動への影響
- 今まで「例外になって 500 系エラーやアプリケーションエラーとして扱われていたケース」が、「401 Unauthorized として正常に拒否される」挙動になります。
- Mailgun 側の正しい実装(
signatureが常に文字列)で運用している限り、通常系の挙動は変わりません。
- セキュリティ面
- 予期しないパラメータ形状でアプリケーションが落ちる穴を塞いでおり、堅牢性が向上しています。
- 型を確認してから
secure_compareするのは一般的なベストプラクティス(タイミング攻撃対策関数に不正な型を渡さない)にも沿っています。
- 移行・対応の必要性
- 既存のアプリケーションコードに変更を加える必要は基本的にありません。
- 独自に Mailgun ingress をラップしていたり、
params[:signature]を上書きしているような特殊な実装がある場合は、一応ログを見て 401 が増えていないか確認すると安心です。
- 参考情報 (あれば)
- 対応している Issue:
#57484— 「Mailgun ingress が配列型の signature で例外を投げる」旨の報告に対する修正。 - 関連クラス:
ActionMailbox::Ingresses::Mailgun::InboundEmailsControllerActiveSupport::SecurityUtils.secure_compare
- テストと静的解析:
cd actionmailbox && bin/test test/controllers/ingresses/mailgun/inbound_emails_controller_test.rbbundle exec rubocop actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
#57087 Only set Postgres timezone when needed
マージ日: 2026/5/27 | 作成者: @matthewd
- 概要 (1-2文で)
PostgreSQL アダプタがサーバ側のTimeZone設定を「常に SET する」のではなく、「必要なときだけ変更する」ように調整された PR です。
あわせて、PostgreSQL のparameter_statusから取得したTimeZoneやvariablesを、大文字・小文字の違いに影響されない(case-insensitive)形で扱うようになっています。
- 変更内容の詳細
※ PR #57070 のフォローアップであり、「サーバの現在設定(parameter status)を見て、Active Record が必要とする設定と差分がある場合だけ SET する」という方針を TimeZone 周りにも適用した変更です。実際のコード断片は概念的に再構成しています(正確な行番号や細部はリポジトリを参照してください)。
2-1. PostgreSQL の TimeZone 設定ロジックの見直し
これまでの挙動(推定):
- 接続時/リセット時などに
SET time zone 'UTC'のように、Active Record 側の設定に合わせて無条件にSETを発行していた。
変更後の挙動:
- PostgreSQL が接続ごとに返す
parameter_status(例:SHOW ALL相当の情報)から、サーバ側の現在のTimeZoneを取得。 - Active Record 側が期待するタイムゾーン(例:
ActiveRecord::Base.default_timezoneやconfig.time_zone等から導かれる値)と比較。 - 異なっている場合だけ
SET TIME ZONE ...を実行する。
このために、PostgreSQL アダプタ内で、
- 接続時に得られる
variables/parameter_statusを Ruby のハッシュとして扱う際、 - キー(例:
"TimeZone","timezone","TIMEZONE")を 大文字小文字を区別せずに扱えるように正規化 する
といった処理が追加・変更されています。
疑似コードイメージ:
# 接続確立時に parameter_status をハッシュとして取得している想定
@parameter_status = normalize_keys(connection.parameter_status)
def normalize_keys(hash)
hash.transform_keys { |k| k.to_s.downcase } # 例: "TimeZone" -> "timezone"
end
def configure_connection
super
configure_time_zone
end
def configure_time_zone
desired_tz = lookup_desired_time_zone # Rails の設定から決める
current_tz = @parameter_status["timezone"]
# サーバの timezone が既に期待通りなら何もしない
return if current_tz && current_tz == desired_tz
execute("SET TIME ZONE #{quote(desired_tz)}", "SCHEMA")
end上記はあくまでイメージですが、PR 説明にある通り:
- TimeZone 用の特別な処理が追加されたこと
variables/parameter_statusのキーを case-insensitive に扱うようにしたこと
がポイントです。
2-2. variables キーの大文字小文字を無視する扱い
PostgreSQL 側の実装やクライアントライブラリ(libpq)によっては parameter_status のキー名が "TimeZone" だったり "timezone" だったりする可能性があります。
これに対応するため、
- アダプタ内部ではキーを一律で小文字に変換して扱う
- 以降の参照は
"timezone"のような一定のキー名に統一
といった実装になっていると考えられます。
これにより、環境差やドライバの仕様差で "TimeZone" が取れたり取れなかったりする問題が解消されます。
2-3. テストの強化
activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb に大きな変更があり、主に以下がテストされていると考えられます。
- PostgreSQL サーバ側の
TimeZoneが Active Record の期待値と同じときはSET TIME ZONEを発行しないこと - 異なるときだけ
SET TIME ZONEを発行すること parameter_status(あるいはvariables)のキーが"TimeZone","timezone","TIMEZONE"など、さまざまな表記でも正しく検出できること- TimeZone 関連の挙動が既存の Rails 設定(
config.time_zone,default_timezoneなど)と整合していること
テスト行数が +146/-76 と大きく変わっているので、従来よりも「TimeZone と parameter_status の組み合わせ」について細かいケースがカバーされるようになったと見てよいです。
- 影響範囲・注意点
3-1. 影響範囲
- 対象: PostgreSQL を使っている Rails アプリケーション(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
- 主な影響:
- 接続時・リセット時などに発行される
SET TIME ZONEの回数が減る(不要な変更が抑制される) - サーバ側でデフォルト TimeZone を適切に設定している場合、Rails からは「何もしない」ケースが増える
- パフォーマンス的には微小ながらクエリ数が減る可能性がある
- DB ログ上で
SET TIME ZONEが頻発していた場合、その回数が減る/場合によっては消える
- 接続時・リセット時などに発行される
3-2. 注意点・挙動変化の可能性
サーバのデフォルト TimeZone に依存する度合いが明確になる
以前は Rails が毎回SET TIME ZONEしていたため、サーバ設定が何であっても Rails 設定が優先されるように見えていたケースがあります。
今回の変更により、「Rails が期待する TimeZone とサーバの TimeZone が既に一致している場合は、あえて変えない」という挙動になります。TimeZone 表記の正規化方法によってはマッチングの仕方が変わる可能性
例: PostgreSQL 側はEtc/UTC、Rails 側はUTCのように、意味的には同じだが文字列としては異なる TimeZone を指定している環境では、
「異なる」と判定されて毎回SET TIME ZONEが行われるか、もしくは逆に「同じ」と判定されてセットされないか、
実装次第で挙動が変わります。
そのため、DB ログやアプリの期待挙動を確認しておくと安心です。独自に PostgreSQL の
TimeZoneを操作しているアプリ・ライブラリとの相互作用- アプリケーションコードやミドルウェアが独自に
SET TIME ZONEを発行している場合、Rails が「現在の TimeZone 値」を見てSET TIME ZONEの要・不要を判断します。 - 接続プーリング(pgbouncer など)を利用しており、接続元ごとに TimeZone がいろいろ変化するような構成では、挙動が複雑になる可能性があります。
- そのような構成の場合、接続のライフサイクルごとに、どのタイミングで誰が TimeZone を設定しているのかを意識した方がよいです。
- アプリケーションコードやミドルウェアが独自に
- 参考情報 (あれば)
この PR:
- タイトル: Only set Postgres timezone when needed
- 番号: #57087
- 作者: matthewd
- マージ日時: 2026-05-27T11:33:57Z
関連 PR:
- #57070: 「parameter_status を使い、必要なときのみ PostgreSQL の設定(おそらく encoding / locale / time zone など)を変更する」という方針を導入した元 PR。
本 PR はその follow-up で、特に TimeZone と case-insensitive なvariables取り扱いにフォーカスした修正。
- #57070: 「parameter_status を使い、必要なときのみ PostgreSQL の設定(おそらく encoding / locale / time zone など)を変更する」という方針を導入した元 PR。
実運用でのチェックポイント:
- 本番環境で PostgreSQL の
SHOW ALL;/SHOW TIME ZONE;の結果と Rails のconfig.time_zone/Time.zone.nameを比べておく - DB ログで
SET TIME ZONEの頻度の変化を確認する - 接続プールや DB ミドルウェア(pgbouncer など)を挟んでいる場合、そのレイヤでの TimeZone 設定ポリシーも確認する
- 本番環境で PostgreSQL の
#56340 [RF-Docs] [ci-skip] Active Storage Guide
マージ日: 2026/5/27 | 作成者: @bhumi1102
- 概要 (1-2文で)
Active Storage ガイド(active_storage_overview.md)が全面的に再構成され、概念説明・セットアップ手順・表示/配信方法などが大幅に整理・追記されました。あわせてテストガイド(testing.md)に Active Storage 向けのテスト方法が追記されています。
- 変更内容の詳細
2.1 ガイド全体の再構成・トーン調整
- 章構成を入れ替え・統合し、「何ができるか → どうセットアップするか → どう使うか → 配信モード・パフォーマンス → 高度なトピック」という流れに近づけています。
- 「〜しろ」という命令口調だった部分が、「なぜそれをやるのか」の背景説明を含む記述に変更されています。
- Active Storage 固有の用語・概念(サービス、アタッチメント、blob、representations など)に、より明確な説明がつきました。
2.2 「What is Active Storage?」と「Setup」の整理
- Requirements(必要条件)の説明を「What is Active Storage?」から「Setup」セクションに移動。
- 初めから S3/GCS を出すのではなく、「Disk サービスを使う最小構成」を先に示し、その後にクラウドストレージ(S3, GCS など)の詳細設定を別セクションで説明する構成になっています。
例: Disk サービスの設定(イメージ)
# config/storage.yml
local:
service: Disk
root: <%= Rails.root.join("storage") %># config/environments/development.rb
config.active_storage.service = :local「まずは Disk でローカル動作させ、その後必要に応じて S3/GCS に進む」という導線が明示されているのがポイントです。
2.3 サービスアダプタと「バケット」の説明の強化
- 「サービスアダプタ」が「Active Storage がファイルシステムや S3/GCS などのストレージとやり取りするための抽象レイヤー」であることを説明し、Disk や S3 がその具体例であることを明示。
- S3/GCS 設定の前に「bucket = ストレージコンテナ」という用語説明を追加し、「バケット名」とは何かが初心者にもわかるようになっています。
- S3/GCS の
CacheControl設定について、「何のために必要か(ブラウザキャッシュに関する振る舞い)」を明示的に説明。- 例: 画像のレスポンスに
Cache-Controlヘッダを付けることで、ブラウザ側でキャッシュが効き、同じ画像を何度もダウンロードしないようにする、など。
- 例: 画像のレスポンスに
2.4 has_many_attached と .attach の挙動の訂正・明確化
- 既存ガイドでは
.attachが「既存の添付を置き換える」ように読める部分がありましたが、実際には:has_one_attached:.attachは添付を置き換える(1つだけだから)has_many_attached:.attachは「追加」する(既存の添付は保持される)
- この挙動が正しく説明されるよう文面を修正。
例: has_many_attached の .attach
class User < ApplicationRecord
has_many_attached :photos
end
user = User.find(1)
user.photos.attach(io: File.open("a.jpg"), filename: "a.jpg")
user.photos.attach(io: File.open("b.jpg"), filename: "b.jpg")
# => photos には a.jpg と b.jpg の2つが添付される(a.jpg が消えるわけではない)2.5 processed / preprocessed オプションの記述追加
has_many_attachedに関して、preprocessedオプションに触れられていなかったため、新たに説明が追加されています(API ドキュメント未記載なのも課題として言及)。- variant / preview を事前に処理しておくオプション(※実装詳細の正確なコードは PR から直接は読めませんが、少なくとも「そういうオプションの存在と役割」がガイドに反映された)。
2.6 表示関連(画像・動画・PDF)の章の整理
- 8章「Displaying Images, Videos, and PDFs」の構成を見直し:
- 「variant」→「preview」→「representable」のように、利用頻度の高いものから説明。
- これまで見出しがなかった
representableの説明に独立した小節(または明確な説明)が追加。
- 3種類の「プレビュー的なもの」について概念を整理:
variant: 画像用の変形(リサイズ、トリミングなど)preview: 動画や PDF からサムネイルを生成する処理representable: 添付ファイルを一覧などに表示するときに、代替表示可能なもの
- 「Lazy vs Immediate Loading(遅延 vs 即時ロード)」の節を、上記3種類の説明のあとに移動し、「全タイプに共通する話」として整理。
2.7 Proxy モード vs Redirect モードの明確化
- 「5.2 Proxy Mode」などの配信モード説明が初心者にはわかりづらかったため、概念説明と例を増補:
- Redirect モード:
- Rails が署名付き URL を返し、ブラウザがストレージ(S3 など)へ直接アクセス。
- Rails サーバの負荷が低いが、ファイルの実体 URL がクライアントに知られる。
- Proxy モード:
- クライアントからのリクエストは Rails に届き、Rails がストレージから読み出した内容を「プロキシ」として返す。
- 認可ロジックをアプリ側できめ細かく制御しやすく、ストレージの URL を隠せるが、Rails サーバの帯域負荷は増える。
- Redirect モード:
- それぞれの使いどころやセキュリティ・パフォーマンス上のトレードオフを、より平易な言葉で説明するようになっています。
2.8 N+1 問題・Eager Loading の文脈追加
- 8.1 の中で N+1 クエリが唐突に出ていたのを修正し、「N+1 問題とは何か」「Active Storage でどのように発生しやすいか(添付を 1 件ごとに個別クエリで取りにいくパターン)」の背景を補足。
includes(:attached)的な使い方で eager loading して N+1 を防ぐ、といった形の流れがわかるような説明になっています。
2.9 直アップロード(Direct Upload)と CORS の説明拡充
- 9.1 Usage にある「サードパーティストレージサービスに CORS を設定する必要がある」という記述に対し、
- 「具体的な設定方法は、このあとすぐの節で解説している」という誘導リンク・文言を追加。
- この PR により、「ガイドの途中だけ読んでいる人」でも CORS の詳細がどこにあるか迷いにくくなります。
2.10 未使用アップロード(Unattached Upload)の削除セクションの整理
- 12章「Purging Unattached Uploads」は内容が短く、9.7 のヘルプボックスと分断されていたため、
- 9.7 にすでに触れられている内容と統合される、または相互に明確な参照関係が作られています(PR 説明からは統合の方向性が示唆されている)。
- 結果として、未使用 blob/purge の扱いについて、散在していた説明が整理されています。
2.11 ポリモーフィック関連へのリンク追加
- セットアップのテーブル内で「polymorphic associations(ポリモーフィック関連)」という用語が出てくる箇所に、Rails ガイド内の該当説明へのリンクが追加されました。
- Active Storage を初めて触る初学者が、ポリモーフィック関連という用語でつまずきにくくなります。
2.12 Testing ガイド(guides/source/testing.md)の追加
- Active Storage のテスト方法に関する節が追加されています(+188 行)。
- 典型的には以下のような観点が含まれていると推測されます:
- テスト環境用の
diskサービス設定 - 添付ファイル/ダウンロードのテスト
- 直アップロードやバックグラウンド処理に絡むテスト(ジョブの実行)
- テスト環境用の
- 典型的には以下のような観点が含まれていると推測されます:
- これにより、Active Storage を使うアプリでの統合テスト・システムテストのベストプラクティスが公式ガイド上で参照しやすくなっています。
- 影響範囲・注意点
- **主な影響は「ドキュメントのみ」**であり、Active Storage の API や既存の挙動が変わる PR ではありません。
- ただし、これまで誤解されやすかったポイント(
has_many_attachedの.attachの挙動、proxy/redirect の違いなど)が明確化されているため、「今まで思い込みで使っていた」部分が修正される可能性はあります。
- ただし、これまで誤解されやすかったポイント(
- 新しいオプションの把握
processed/preprocessedの説明が追加されたことで、これらのオプションを知らずに都度オンデマンドで処理していたケースについて、改善の余地があることに気づけるようになります。
- パフォーマンス/セキュリティ設計の再確認
- Proxy/Redirect の違いや Cache-Control の説明が増えたことで、自身のアプリの設定が要件(セキュリティレベル・帯域コスト・キャッシュ戦略)に合っているかを見直すきっかけになります。
- テスト戦略の改善
- testing.md の更新により、これまで「なんとなく」書いていた Active Storage を含むテストを、公式推奨の形に合わせて整理できる可能性があります。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/56340
- ガイド(最新版):
- Active Storage Overview:
guides/source/active_storage_overview.md(HTML 版は https://guides.rubyonrails.org/active_storage_overview.html に反映予定) - Testing:
guides/source/testing.md(HTML 版は https://guides.rubyonrails.org/testing.html)
- Active Storage Overview:
- 関連する概念:
- Polymorphic Associations: https://guides.rubyonrails.org/association_basics.html#polymorphic-associations
- Active Storage API ドキュメント: https://api.rubyonrails.org/classes/ActiveStorage.html
#57243 Fix NoMethodError for malformed multiparameter attribute keys
マージ日: 2026/5/26 | 作成者: @55728
- 概要 (1-2文で)
- multiparameter 属性のキーが壊れている(
written_on(のように(だけで)が無い)場合に、素のNoMethodErrorで落ちていたのを、想定どおりActiveRecord::MultiparameterAssignmentErrorsとして扱うように修正した PR です。 - これにより、既存コードが rescue している
MultiparameterAssignmentErrorsで一貫してハンドリングできるようになります。
- 変更内容の詳細
何が問題だったか
_assign_attributes は、属性キーに ( が含まれていると「multiparameter 属性」とみなして、multiparameter 用の処理系に流します。
ここで、multiparameter のインデックス((1i) の 1 など)をパースするメソッド find_parameter_position が以下のようになっていました:
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i- 期待されるキー例:
"written_on(1i)","written_on(2i)"など - 壊れたキー例:
"written_on("((だけで)が無い)
"written_on(" のようなキーでは scan の結果が [] になり、.first が nil を返し、.first をさらに呼んで NoMethodError: undefined method 'first' for nil が発生していました。
この例外は、multiparameter 用のラッパである execute_callstack_for_multiparameter_attributes が期待している「想定済みのエラー」ではなく、ただの NoMethodError なので、呼び出し側が
rescue ActiveRecord::MultiparameterAssignmentErrorsとしていても捕捉できませんでした。
今回の修正
1. find_parameter_position の挙動変更
- 正規表現がマッチしない(つまり
(はあるが)を含まない、等)場合に、NoMethodErrorではなく 明示的にArgumentErrorを raise するように変更。
擬似コードイメージ:
def find_parameter_position(multiparameter_name)
match = multiparameter_name.scan(/\(([0-9]*).*\)/).first
raise ArgumentError, "could not parse parameter position from #{multiparameter_name.inspect}" unless match
match.first.to_i
endこれにより「インデックスをパースできない壊れた multiparameter キー」であることを意図的に表現する例外になります。
2. その ArgumentError を multiparameter エラーとしてラップ
- multiparameter の callstack を組み立てる
extract_callstack_for_multiparameter_attributes側で、find_parameter_positionから飛んでくるArgumentErrorを捕捉。 - それを
AttributeAssignmentErrorに包み、既存の仕組みどおりActiveRecord::MultiparameterAssignmentErrorsにまとめて raise するようにしました。
結果として、壊れた multiparameter 属性も他の不正な multiparameter 入力(例: 型変換に失敗した値など)と同じ経路・同じ例外クラスで扱われるようになります。
実行例
修正前
Topic.new("written_on(" => "2024")
# => NoMethodError: undefined method 'first' for nil
# activerecord/lib/active_record/attribute_assignment.rb:79:in `find_parameter_position'修正後
Topic.new("written_on(" => "2024")
# => ActiveRecord::MultiparameterAssignmentErrors:
# 1 error(s) on assignment of multiparameter attributes
# [invalid multiparameter attribute name "written_on("
# (could not parse parameter position from "written_on(")]つまり、「壊れたキーだが multiparameter として扱おうとしている」状況を、ちゃんと multiparameter 用のエラーとして通知してくれるようになりました。
テスト追加
activerecord/test/cases/multiparameter_attributes_test.rb に 2 ケース追加されています。
(を含むが)を含まないキー
例:"written_on("
→ActiveRecord::MultiparameterAssignmentErrorsが発生することを確認。壊れたキー + 正常な multiparameter キー混在パターン
rubyTopic.new( "written_on(1i)" => "2024", "written_on(2i)" => "05", "written_on(3i)" => "01", "written_on(" => "oops" )- 正常な
"written_on(1i)",(2i),(3i)は問題なく扱われる - 壊れた
"written_on("のみが 1 件のエラーとしてMultiparameterAssignmentErrorsに含まれる
- 正常な
という挙動を確認しています。
- 影響範囲・注意点
影響を受けるケース
- multiparameter 属性のキーが壊れている(
(だけや、正規表現にマッチしない形式のキー)場合に、これまでNoMethodErrorで落ちていたところが、ActiveRecord::MultiparameterAssignmentErrorsに変わります。 - これにより「今まで気づかず rescue されていなかったエラー」が、アプリ側の
rescue ActiveRecord::MultiparameterAssignmentErrorsによってハンドリングされるようになる可能性があります。
- multiparameter 属性のキーが壊れている(
既存の正常系には影響なし
- 正常な multiparameter キー(
written_on(1i)のように(...)が正しく閉じていてインデックスがパース可能なもの)の挙動は変わりません。 _assign_attributesの「(を含んでいれば multiparameter 扱いにする」というガード自体も変更されていません。
つまり、「どのキーが multiparameter 処理に乗るか」という判定ロジックは従来どおりです。
- 正常な multiparameter キー(
例外クラスの変化に依存しているコードへの注意
- もしアプリ側で「壊れた multiparameter キーが来るのはバグだから、
NoMethodErrorをそのまま落とすべき」といった前提でいた場合は、今回からMultiparameterAssignmentErrorsになる点に注意が必要です。 - 一般的には、multiparameter の入力バリデーションをしているアプリケーションにとっては、むしろ期待どおりの一貫した挙動になったと言えます。
- もしアプリ側で「壊れた multiparameter キーが来るのはバグだから、
- 参考情報 (あれば)
対象コミットは ActiveRecord (
activerecord) のみで、変更ファイルは次の 3 つです。activerecord/lib/active_record/attribute_assignment.rb- multiparameter のパース (
find_parameter_position) と callstack 抽出 (extract_callstack_for_multiparameter_attributes) の挙動変更
- multiparameter のパース (
activerecord/test/cases/multiparameter_attributes_test.rb- 壊れた multiparameter キーのテスト追加
activerecord/CHANGELOG.md- 本修正のエントリ追加
multiparameter 属性は、Rails フォームヘルパ(
date_select,datetime_selectなど)が生成するmodel[attr(1i)],model[attr(2i)]のようなパラメータを 1 つの属性に組み立てるための仕組みです。この PR は、それらの組み立て時のエラーをより一貫して扱えるようにするものです。
#57472 Active Job: pass job to stopping? for fine-grained control over interruption
マージ日: 2026/5/26 | 作成者: @bdewater-thatch
- 概要 (1-2文で)
Active Job の継続ジョブ (continuations) が「中断可能かどうか」を判定する際に、キューアダプタ側でstopping?にjobオブジェクトを受け取れるようにした変更です。これにより、Solid Queue / Good Job / Sidekiq Pro などがジョブの属性(キュー名など)に応じて長時間実行ジョブをきめ細かく中断できるようになります。
- 変更内容の詳細
2-1. 背景と問題点
- Active Job には、長時間実行処理を複数ステップに分割して再実行できるようにする「continuation(継続)」機能があります (
ActiveJob::Continuable/ActiveJob::Continuation)。 - 一部のバックエンド(Solid Queue, Good Job, Sidekiq Pro など)は、キューごと・タグごとなど「ジョブの属性ベース」で処理を一時停止できる機能を持っています。
- しかし、一度実行を開始した「長時間ジョブ(継続ジョブ)」は、バックエンド側がそのキューを pause しても最後まで走り続けてしまい、途中で中断されないという課題がありました。
- これまでも Active Job 側に「ジョブ実行を中断すべきか?」を問い合わせる
stopping?の仕組みはありましたが、その判定にjobオブジェクトが渡されなかったため、バックエンド側は「どのジョブか」を知らずに中断条件を決めざるを得なかった、という制約がありました。
この PR は、stopping? に job を渡すことで、「特定キューだけ中断」「特定のジョブクラスだけ中断」といった粒度の中断制御を可能にします。
2-2. 具体的なコードレベルの変更点
※PR本文には差分の中身までは載っていませんが、ファイル名と変更行数、および機能説明から推測できるレベルで解説しています。
2-2-1. ActiveJob::QueueAdapters::AbstractAdapter#stopping?
従来(推定):
module ActiveJob
module QueueAdapters
class AbstractAdapter
# 継続処理内で「いま中断すべきか?」を問い合わせるためのフック
def stopping?
false
end
end
end
end変更後(概念的なイメージ):
module ActiveJob
module QueueAdapters
class AbstractAdapter
# job を引数に受け取れるように
def stopping?(job = nil)
false
end
end
end
endポイント:
- シグネチャが
stopping?()→stopping?(job)に変更され、ジョブインスタンスを引数として受け取れるようになりました。 - 既存アダプタとの互換性確保のためにデフォルト引数が付いているか、もしくは引数があっても使わない実装になっていると考えられます。
2-2-2. 継続ジョブ側からの stopping? 呼び出し
ActiveJob::Continuable / ActiveJob::Continuation から stopping? を呼び出す箇所で、今実行中のジョブオブジェクトを引数として渡すように変更されています。
イメージ:
module ActiveJob
module Continuable
private
def should_stop_continuation?
# 以前: queue_adapter.stopping?
queue_adapter.stopping?(self)
end
end
endこれにより、継続処理の各ステップの途中で「このジョブに対して、いま中断すべきか?」をアダプタに問い合わせることができます。
2-2-3. テストアダプタ・テストヘルパの更新
activejob/lib/active_job/queue_adapters/test_adapter.rbstopping?のインターフェース変更に追従し、job引数を受け取る形に修正。- テスト内で「特定条件で stopping? が true を返す」シナリオを作りやすくなります。
activejob/lib/active_job/continuation/test_helper.rb- 継続ジョブのテストをしやすくするヘルパが
stopping?(job)を想定した形に修正。 - たとえば「このジョブの
queue_nameがlow_priorityなら stopping? は true にする」など、より実際のアダプタに近いテストが書けるようになっていると考えられます。
- 継続ジョブのテストをしやすくするヘルパが
activejob/test/cases/continuation_test.rb- 新しい挙動(
stopping?に job が渡されること、およびその結果として継続処理が中断されること)をカバーするテストが追加。 - シナリオ例(推定):
- 特定の条件にマッチする job に対してだけ
stopping?が true を返し、継続が途中で止まっていることを検証。
- 特定の条件にマッチする job に対してだけ
- 新しい挙動(
2-2-4. CHANGELOG 更新
activejob/CHANGELOG.mdに、Active Job における新しいstopping?(job)フックの追加(あるいは既存フックの拡張)が記載されています。- これにより、Rails アップグレード時に「継続ジョブの中断フックが job を受け取れるようになった」ということが分かるようになっています。
2-3. バックエンドアダプタ側で想定される実装例
Solid Queue や Good Job のようなアダプタが、次のように stopping? を実装できるイメージです。
class MyCoolAdapter < ActiveJob::QueueAdapters::AbstractAdapter
def stopping?(job)
# 例: 一時停止リストに入っている queue のジョブなら中断
paused_queues.include?(job.queue_name)
end
endあるいは、クラス単位やタグ単位など:
def stopping?(job)
return true if paused_queues.include?(job.queue_name)
return true if paused_job_classes.include?(job.class.name)
return true if job.arguments.first == :emergency_stop
false
end- 影響範囲・注意点
3-1. 影響範囲
- 影響を受けるのは Active Job のキューアダプタ実装(特に独自アダプタ)と、継続ジョブ(continuations)を利用しているアプリケーションです。
- Rails が標準で提供するアダプタ(Async, Que, Delayed Job, Sidekiq, Solid Queue など)は、順次この変更に追随していくはずですが、サードパーティ製 / 自前実装のアダプタは注意が必要です。
3-2. 後方互換性
AbstractAdapter#stopping?に引数が追加されたので、自作アダプタがdef stopping?をオーバーライドしている場合、シグネチャ不一致で警告やエラーになる可能性があります。- Ruby のバージョンや書き方によっては問題にならない場合もありますが、将来的な互換性のためにも
def stopping?(job = nil)のように修正しておくのが安全です。
- Ruby のバージョンや書き方によっては問題にならない場合もありますが、将来的な互換性のためにも
- これまで
stopping?を使っていなかったアダプタは、そのままでも機能的には問題ありません(常にfalse扱い)。ただし、「ジョブ単位での中断制御」を利用したい場合は、新しい引数を利用するように実装を追加する必要があります。
3-3. アプリケーション開発者視点での注意点
- アプリケーション側の Active Job コード(
MyJob < ApplicationJob)は、基本的にこの変更で壊れませんが、長時間実行する継続ジョブでは「途中で中断されうる」ことを前提に設計する必要があります。- たとえば:
- 中断ポイントごとに状態を保存しておく(idempotent な処理にする)。
- 途中で
ActiveRecord::ConnectionNotEstablishedなどの例外が出てもリトライ可能なように書く。
- たとえば:
- バックエンド側が pause したときに「ジョブがいつ中断されるか」は、継続ジョブ側の中断チェック頻度に依存します(
stopping?をどのくらいの粒度で呼び出しているか)。
→ 非常に大きなループなどの中で継続を回している場合は、適切な頻度でstopping?を確認するようにするのが望ましいです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57472
- 関連するバックエンドの pause 機能:
- Active Job Continuations の概要(参考ドキュメントは今後更新される可能性あり):
- Rails ガイドの Active Job セクション、および
ActiveJob::Continuable,ActiveJob::Continuationのソースコードコメントを参照すると、継続ジョブの設計意図が分かります。
- Rails ガイドの Active Job セクション、および
#54542 Duplicate context hash in ActiveSupport::ErrorHandler#report
マージ日: 2026/5/26 | 作成者: @andrewn617
- 概要 (1-2文で)
ActiveSupport::ErrorHandler#reportが複数サブスクライバに同じcontextハッシュオブジェクトを共有して渡していた問題を修正し、サブスクライバごとにdeep_dupしたハッシュを渡すようにしたPRです。これにより、あるサブスクライバがcontextを破壊的変更しても、他のサブスクライバへの影響が出ないようになります。
- 変更内容の詳細
何が問題だったか
ActiveSupport::ErrorReporter / ActiveSupport::ErrorHandler#report は、例外報告時に以下のような「コンテキスト情報」をハッシュで受け取り、複数のサブスクライバ(エラーレポーティングサービスなど)に渡せる仕組みになっています。
error_reporter.report(exception, handled: true, context: { user_id: 1, request_id: "abc" })しかし、内部実装では「同じ context ハッシュインスタンス」がそのまま全サブスクライバに渡されていました。そのため、あるサブスクライバで:
subscriber.call(error, context) do
Foo.bar(baz: context.delete(:baz))
endのような「破壊的操作 (delete, merge!, []=, etc.)」をすると、その変更が後続のサブスクライバにも反映されてしまう、というバグ/フットガンになっていました。
何を変えたか
PRでは、サブスクライバへ渡す直前に context を deep_dup してから渡すように変更しています。
イメージとしては、これまで:
subscribers.each do |subscriber|
subscriber.call(error, context)
endだったものが、以下のようになる変更です(実際のコードは若干異なる可能性がありますが、概念的にはこういう変更):
subscribers.each do |subscriber|
subscriber.call(error, context&.deep_dup)
endポイント:
deep_dupを使用することで、トップレベルのハッシュのみならず、ネストしたハッシュや配列なども含めて複製します。- 各サブスクライバは「論理的に同じ内容を持つが別オブジェクトの
context」を受け取るため、サブスクライバ内での破壊的変更は他サブスクライバに波及しません。
テストの追加
activesupport/test/error_reporter_test.rb にテストが追加され、たとえば以下のような振る舞いが検証されていると考えられます:
- 複数のサブスクライバを登録する
contextにネストしたハッシュ/配列を含めた状態でreportを呼ぶ- 1つ目のサブスクライバで
contextを破壊的変更する(deleteや[]=など) - 2つ目のサブスクライバに渡る
contextにはその変更が反映されていないことを確認する
また、この挙動変更が activesupport/CHANGELOG.md にも追記されています。
- 影響範囲・注意点
影響範囲
- 対象:
ActiveSupport::ErrorReporter/ActiveSupport::ErrorHandler#reportを使っており、contextを渡しているコード、およびそれを受け取るサブスクライバ。 - 特に影響するのは「複数のサブスクライバを登録しており、かつそのうち1つ以上が
contextを破壊的に変更している」ケースです。
これまでと変わる挙動
- 以前は「サブスクライバAが
contextを変更すると、サブスクライバBにも変更済みのcontextが渡される」という暗黙の共有状態がありましたが、今後は「サブスクライバごとに独立したcontextのコピー」が渡されます。 - もし「意図的に」この共有状態に依存したコードを書いていた場合は、その前提が崩れます。ただし、そのような依存はほぼバグと見なされるため、この変更は後方互換性を高める方向の修正といえます。
パフォーマンス面の注意
deep_dupはcontextのサイズやネストの深さに応じてコストがかかるため、大きなコンテキストや非常にネストの深いオブジェクトを頻繁に渡している場合はオーバーヘッドが増える可能性があります。- 一方で、エラーレポートは通常それほど高頻度ではないことが多く、かつバグ回避のメリットが大きいので、ほとんどのアプリケーションでは妥当なトレードオフです。
- パフォーマンスを気にする場合は、
contextに巨大なオブジェクトを渡さないように設計する(IDなど必要最小限の情報だけを渡す)ことが推奨されます。
これからの実装上の指針
- サブスクライバ側では、
contextを破壊的に変更しても他サブスクライバや呼び出し元には影響しない、と考えてよくなりました。 - 「サブスクライバ間で情報を共有したい」場合は、
contextの変更に頼らず、別の共有メカニズム(スレッドローカル、専用のストレージ、イベントごとのID経由で参照するストアなど)を考えるべきです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/54542
ActiveSupport::ErrorReporterドキュメント(参考として現行Railsガイドなど)Object#deep_dupの挙動: Active Support による深いコピー機能で、ハッシュ/配列/文字列などを再帰的に複製する
このPRは、エラーハンドリング用のコンテキスト情報を安全に扱えるようにするための、「細かいが実務上ありがちなバグを防ぐ」タイプの改善です。
#55638 [ci skip] Wording correction for url_from documentation
マージ日: 2026/5/26 | 作成者: @dajabe
- 概要 (1-2文で)
ActionController::Redirecting#url_fromのドキュメント内の文言を修正し、「サブドメインは安全扱いされない(UnsafeRedirectError になり得る)」という実際の挙動がより明示的に分かるようにしたドキュメント専用 PR です。コードの挙動自体には一切変更はありません。
- 変更内容の詳細
- 対象ファイル:
actionpack/lib/action_controller/metal/redirecting.rb - 変更内容:
url_from(リダイレクト時に使われる内部ヘルパ)の 挙動説明コメント(ドキュメント)に 2 語追加 し、「サブドメインの扱い」に関する説明をより正確にした。
元 PR の Motivation にある通り、従来の説明だと:
- 「サブドメインは安全とみなされる」ような印象を与える書き方になっていた
- 実際には、サブドメイン宛てのリダイレクトでも
UnsafeRedirectErrorが発生し得る - そのギャップにより、挙動がバグに見え、デバッグに時間を取られた
という問題があったため、
- 実例として挙がっている
UnsafeRedirectErrorの動作と ドキュメントが一致するよう、 - 「ホスト名の検証/サブドメインの扱い」に関する記述を明示的に修正
した、という内容です。
実装コードは変わっていないので、url_from の実際の挙動は従来通りです。
(例: redirect_to "https://foo.example.com" のような外部 URL / サブドメインへのリダイレクトは、config.hosts や allow_other_host などの設定を満たさなければ UnsafeRedirectError になり得る、という既存仕様のままです。)
- 影響範囲・注意点
挙動の変更なし
- ランタイム挙動・API 仕様・互換性に影響はありません。
- テストや
CHANGELOGの更新も不要と判断されているドキュメント-only の修正です。
開発者目線での注意点
url_from/redirect_toまわりで「サブドメインなら自動で安全とみなされる」と期待していた場合、その理解は誤りであり、この PR によりドキュメントもそのような誤解を与えないようになります。- サブドメイン宛てリダイレクトで
UnsafeRedirectErrorが起きる場合は、Rails のホスト検証(config.hostsやallow_other_host: true、もしくは自前検証)が必要です。 - 既存アプリ側でドキュメントを参照しつつ実装している場合でも、今回の修正で 「今までの挙動が変わった」わけではない ことに注意してください。あくまでドキュメントが実装に追いついただけです。
- 参考情報 (あれば)
UnsafeRedirectErrorドキュメント:
https://api.rubyonrails.org/classes/ActionController/Redirecting/UnsafeRedirectError.htmlActionController::Redirecting(redirect_to / url_from の実装・解説):
https://api.rubyonrails.org/classes/ActionController/Redirecting.html- 関連するホスト検証設定の例:
config.hosts << "example.com"redirect_to some_url, allow_other_host: true
#57463 Reject malformed Mandrill inbound events without raw messages
マージ日: 2026/5/26 | 作成者: @afurm
- 概要 (1-2文で)
Mandrill のインバウンドイベントでmsg.raw_msg(生 MIME メッセージ)が欠落または文字列でない場合、それを不正なイベントとして 422 を返して明示的に拒否するようにした変更です。これにより、従来 500 (TypeError) を返していたケースが正常にハンドリングされ、他の不正ペイロードと一貫した振る舞いになります。
- 変更内容の詳細
背景となる問題
- Mandrill の inbound webhook は、イベントの配列を JSON で送ってくる。
- その各イベントは
event["msg"]["raw_msg"]に、受信メールの「生 MIME メッセージ」を含む想定。 - しかし、実際には以下のような「パース自体は成功するが中身が足りない」ケースがある:
msg自体がないmsg["raw_msg"]が存在しないmsg["raw_msg"]がnilや文字列以外の値
Rails 側では ActionMailbox::InboundEmail.create_and_extract_message_id! にこの raw_msg を渡しているため、nil を渡すと TypeError が発生し、結果的に 500 エラーになっていた。
今回の対応方針
ActionMailbox::Ingresses::Mandrill::InboundEmailsControllerで、各イベントごとにmsg.raw_msgが「存在していて、かつ String であること」を検証する。- 検証に失敗したイベント(raw message 不正)は、既にある「不正なイベント」扱いのエラーを発生させるようにし、HTTP レスポンスとして 422 Unprocessable Content を返す。
- これは元々存在していた「不正ペイロード」ハンドリングと同じ振る舞いに合わせたもの。
コード面でのイメージ
実際のコードは PR 内にありますが、概念的には以下のようなチェックが追加されています(擬似コード):
events.each do |event|
raw_message = event.dig("msg", "raw_msg")
unless raw_message.is_a?(String)
# 既存の「不正イベント」扱いの例外を投げる
raise ActionMailbox::Ingresses::Mandrill::InboundEmailsController::MalformedEventError
end
ActionMailbox::InboundEmail.create_and_extract_message_id!(raw_message)
endポイント:
nilだけでなく、IntegerやHashなど 「文字列以外の型」も NG として扱われる。- 検証自体は Ingress コントローラ(Mandrill 用)に閉じており、
ActionMailbox::InboundEmail側には修正は入っていない。
テストの追加
テストファイル:actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
主な追加:
- 「raw message がない場合」のテストケースが追加され、以下を検証している:
- リクエストが 422 を返すこと
- InboundEmail が作成されないこと
- Mandrill ingress 全体のテストスイートが更新されている (
bin/test test/controllers/ingresses/mandrill/inbound_emails_controller_test.rbなどが通ることを確認済み)。
また、actionmailbox/CHANGELOG.md にこの挙動変更が追記されており、Action Mailbox の挙動として公式にドキュメント化されています。
- 影響範囲・注意点
影響範囲
- 対象: Mandrill 経由で Action Mailbox を使っているアプリケーション。
- 変更前:
msg.raw_msgが欠落/非文字列 →TypeError→ Rails が 500 Internal Server Error を返す。
- 変更後:
- 同じ条件 → 「不正イベント」扱い → 422 Unprocessable Content を返す。
- アプリケーションコード側の
ActionMailbox::InboundEmail利用方法は変わりませんが、外部から見える HTTP ステータスコードが 500 → 422 に変わる点が挙動変更になります。
注意点
- Mandrill 側・クライアント側で 5xx をリトライ条件にしている場合:
- 今後は 422 が返るため、自動リトライされなくなる可能性があります(Mandrill の実装依存)。その場合、「イベントが不正である」という前提で運用・監視を行う必要があります。
- エラーの性質が「サーバ側バグ(500)」から「クライアント側/Webhook ペイロードの問題(422)」として整理されるため、
- アプリのエラーログにおける 500 の件数は減り、
- 代わりに 422 として検知されるようになります。
raw_msgに依存した独自処理(例えば、Mandrill イベント JSON を直接処理しているコード)がある場合、- 今回は Rails 内部の Ingress レイヤーでのみチェックしているので直接な互換性問題はありませんが、
- ペイロード生成側(Mandrill 側の統合)で
raw_msgが必ず含まれるようチェックしておくと安全です。
- 参考情報 (あれば)
- 関連 Issue: #57462
→msg.raw_msg欠落時にTypeErrorで 500 が返る問題の報告・追跡 Issue。 - 関連ファイル:
actionmailbox/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rbactionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rbactionmailbox/CHANGELOG.md
- テストコマンド(PR 基準):
cd actionmailbox && bin/test test/controllers/ingresses/mandrill/inbound_emails_controller_test.rbcd actionmailbox && bin/test test/controllers/ingressescd actionmailbox && bundle exec rubocop app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
#57459 Fix typos in README, CONTRIBUTING, RELEASING_RAILS, and component docs
マージ日: 2026/5/26 | 作成者: @sagarjunnarkar
- 概要 (1-2文で)
Rails 本体リポジトリ内の README やドキュメント類に含まれていた英語の誤字・文法ミス・記号抜けなどをまとめて修正した PR です。コードの挙動や API には一切影響せず、ドキュメントの品質向上のみを目的としています。
- 変更内容の詳細
各ファイルごとの修正ポイントは以下の通りです。
CONTRIBUTING.md
英語の不自然な表現・文法ミスを修正:
"instead to refer"→"instead refer"- 「〜の代わりに参照してください」という文脈で、不要な "to" が入っていたのを削除。
"Those change should"→"Those changes should"- 単数形 "change" を複数形 "changes" に修正。
これにより、コントリビュートガイドラインの英語として自然な文章になっています。
RELEASING_RAILS.md
リリース手順ドキュメントの文法・スタイル修正:
"releases. Publish"→"releases, publish"- 文が途中で不自然に切れていた(文断片)ので、カンマ区切りで一文につなげて読みやすく修正。
"To release, Run"→"To release, run"- 文中で不要に大文字になっていた "Run" を小文字に修正し、通常の英文スタイルに統一。
このファイルはリリース作業手順のベースですが、意味内容は変えず、読みやすさと一貫性だけを整えています。
actiontext/README.md
動詞の形を修正:
"checkin"→"check in"- "check in" は本来スペースを含む句動詞であり、名詞 "checkin" では意味が異なるため(または不自然なため)、正しい句動詞表記に修正。
Action Text の利用手順説明文の自然さを改善しています。
activemodel/README.rdoc
サンプルコードと文中の記号抜けを修正:
Ruby コード例のクォート抜け:
'bob → 'bob'本来は以下のような形を想定しているコード例です(例):
rubyuser = User.new(name: 'bob')この
'bob'の閉じクォートが落ちていて、RDoc としても Ruby コードとしても不正な形になっていたものを修正。
テキスト中のコロン追加:
"on GitHub"の後にコロンを追加。- 例: 「ソースコードは GitHub にあります: https://github.com/rails/rails」
- リンクの前にコロンをつけるスタイルに他のドキュメントとあわせて統一。
これにより、README 内のサンプルコードがコピー&ペースト可能な正しい Ruby コードになり、記述スタイルも一貫します。
actionview/README.rdoc
"API documentation is at"の後にコロン:を追加。- 例:
API documentation is at: https://api.rubyonrails.org/...といった形に揃えたものと思われます。 - 表記ゆれの修正であり、意味的な変更はありません。
- 例:
actionmailer/README.rdoc
- こちらも
API documentation is atの後にコロン:を追加。- Action View と同様に API ドキュメントリンクの記法を統一。
railties/README.rdoc
"on GitHub"の後にコロン:を追加。"API documentation is at"の後にもコロン:を追加。
これにより、Rails 本体 (railties) に関する情報のリンク表記が他コンポーネントと統一されます。
guides/README.md
"wholistically"→"holistically"- "wholistically" は誤綴りまたはかなり稀な変種で、一般的な英語としては "holistically" が正しいため修正。
- 「ガイドを全体的に(holistically)改善する」といったニュアンスの文脈で用いられている部分と思われます。
- 影響範囲・注意点
影響範囲
- 変更対象はすべてドキュメント (Markdown / RDoc / README 類) のみです。
- Ruby コード、API、挙動、テスト、設定ファイルには一切手が入っていません。
- そのため、アプリケーションのビルド・実行・テスト結果・互換性に影響はありません。
注意点
- 外部から README をスクレイピングして自動処理しているツール(パーサやリンクチェッカーなど)がある場合、文言の変更でごくわずかに差分が出る可能性はありますが、「タイポ修正」レベルであり、URL 等は変わっていません。
- サンプルコード(
activemodel/README.rdoc)は、今回の修正で正しい Ruby 構文になっているので、古いバージョンの README をもとにしていた場合は最新のドキュメントのほうが正確です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57459
- 変更ファイル数: 8
- 行数差分: 追加 12 行 / 削除 12 行(実質的にはテキストの微修正のみ)
ドキュメントコントリビューションのガイドラインに沿った、タイポ・文法・表記統一に関するクリーンアップ PR として位置づけられます。
#55156 Fix crash when trying to attach a Blob to a Record:
マージ日: 2026/5/26 | 作成者: @Edouard-chin
- 概要 (1-2文で)
Active Storage でActiveStorage.touch_attachment_records = falseのとき、既存の Blob をレコードにアタッチするとクラッシュしていた不具合を修正した PR です。Blob が自動で Attachment を保存しなくなった仕様変更により、未保存の Attachment をtouchしようとして例外が出ていた箇所にガードを入れています。
- 変更内容の詳細
問題の背景
- 対象機能: Active Storage の direct upload(事前に Blob を作成し、それを後からレコードに attach する流れ)
- 設定:
ActiveStorage.touch_attachment_records = falseにした場合のみクラッシュ - 状況:
- 既存 Blob を Record に attach するとき、Active Storage は関連する
Attachmentをtouchしようとする。 - しかし、その
Attachmentはまだ DB に保存(persisted?)されておらず、touchすると例外が発生していた。
- 既存 Blob を Record に attach するとき、Active Storage は関連する
以前 (#53623 以前) は:
- Blob を作成
- Blob が自動的に自分の Attachment を autosave
- 保存された Attachment を
touch
という流れで、Attachment はすでに保存済みだったため例外は発生しませんでした(ただし 3) の SQL は無駄でした)。
#53623 によって「Blob が自分の Attachment を autosave しない」ように変わったため、Attachment が未保存のまま touch を呼ぶケースが生じ、その結果クラッシュするようになっていました。
この問題は ActiveStorage.touch_attachment_records = true(デフォルト)では発生しません。理由は、Blob 側の attachments_to_touch 的な処理で、レコード未関連の Attachment に対しては空の Relation を返すようになっており、そのパスを通るため未保存の Attachment に touch しに行かないからです。
具体的な修正内容
修正は activestorage/app/models/active_storage/blob.rb の 1 行のみのロジック変更と、それをカバーするテスト追加です。
概念的には以下のような変更です(実際のコードは Rails 本体を参照):
# 変更前(イメージ)
attachments.each do |attachment|
attachment.touch # attachment が未保存でも呼んでしまう
end
# 変更後(イメージ)
attachments.each do |attachment|
next unless attachment.persisted?
attachment.touch
endつまり:
- Blob から関連する Attachment に対して
touchする際、- その Attachment が
persisted?である場合にのみtouchを呼ぶ - 未保存 (
new_record?) の Attachment には何もしない
- その Attachment が
テスト (activestorage/test/models/attachment_test.rb) では、以下のような条件を確認するケースが追加されています:
ActiveStorage.touch_attachment_records = falseに設定- 既存 Blob をレコードに attach する
- この一連の操作で例外が発生しないことを検証
加えて activestorage/CHANGELOG.md に、このバグ修正が明記されています。
- 影響範囲・注意点
- 影響対象:
- Active Storage を利用しており
ActiveStorage.touch_attachment_records = falseを明示的に設定している- direct upload などで「既存 Blob を後からレコードに attach」するフローを使っているアプリケーション
- これらのアプリケーションでは、アップグレード後に:
- これまでクラッシュしていたケースが正常に動作するようになります。
- 変更内容は「未保存 Attachment への
touch呼び出しをスキップする」だけなので、既存の永続化済み Attachment へのtouchの挙動は変わりません。 touchが呼ばれなくなるのは「そもそもまだ DB に保存されていない Attachment」に対してのみなので、監査用のupdated_atやキャッシュ更新といった用途に影響はありません(保存されていない行はそもそも存在しないため)。
運用上の注意:
- Rails をバージョンアップした際、この挙動変更に伴うマイグレーションや設定変更は不要です。
- もし独自で Active Storage 周りを monkey patch して
touchのタイミングをいじっている場合は、同様にpersisted?チェックを入れているかを確認した方が安全です。
- 参考情報 (あれば)
- 関連 issue: #55144(この PR で修正されたバグレポート)
- 関連 PR: #53623(Blob が Attachment を autosave しなくなった挙動変更の PR。今回の不具合の原因となった仕様変更)
- 設定項目:
ActiveStorage.touch_attachment_recordstrue(デフォルト): アタッチされたレコードを更新したときに Attachment をtouchして、Blob のupdated_at連鎖などを制御false: そのようなtouchを行わない(今回のバグは、この設定時の一部パスで未保存 Attachment にtouchしようとしていた)
#57458 Drop test databases when use_postgresql / use_mysql2 is given a block
マージ日: 2026/5/26 | 作成者: @yahonda
- 概要 (1-2文で)
Rails のテスト用ヘルパuse_postgresql/use_mysql2にブロックを渡した場合、そのブロック内で作成されたrailties_${Process.pid}_%形式のテストDBを自動で削除するようにしたPRです。これにより、per-PID / per-worker で作られる一時テストDBがテスト実行後に残り続ける問題が解消されます。
- 変更内容の詳細
2-1. 何が追加されたか
TestHelpers::Generation にある use_postgresql / use_mysql2 が「ブロックを受け取る」形で使われた場合、そのブロック実行前後で DB のスナップショットを取り、差分として新規作成された railties_${Process.pid}_% DB を後始末する仕組みが入りました。
内部的には新しいヘルパ with_test_database_cleanup(adapter) が導入され、以下のような動きをします:
- ブロックに入る前に、指定アダプタで存在する
railties_${Process.pid}_%な DB 一覧を取得してスナップショット - ブロックを実行
ensureで再度railties_${Process.pid}_%一覧を取得し、差分(=ブロック中に新規作成されたDB)を DROP
これにより、テストコードが ActiveRecord::TestDatabases.create_and_load_schema などを通じて per-PID/per-worker のテストDBを作成していても、config/database.yml に記述がなくても確実に削除されます。
2-2. 既存テストコードの更新
今回の PR では、すでに use_postgresql / use_mysql2 を使っていて、かつ DB を実際に作っていたテストたちが「ブロック形式」に書き換えられています。
例(イメージ):
変更前(イメージ)
use_postgresql
# ここで DB 作成系のタスクや create_and_load_schema を実行
rails "db:drop" # ensure 句などで手動クリーンアップ変更後(イメージ)
use_postgresql do
# ここで DB 作成系のタスクや create_and_load_schema を実行
# 明示的な rails "db:drop" は不要
endPR 説明文でも述べられている通り、以下のようなテストが対象です(ファイル単位):
railties/test/application/rake/dbs_postgresql_test.rbrailties/test/application/rake/multi_dbs_test.rbrailties/test/application/runner_test.rbrailties/test/application/test_runner_test.rbrailties/test/application/test_test.rbrailties/test/isolation/abstract_unit.rb
これらでは、従来 ensure rails "db:drop" のように明示的に drop していた部分が削除され、新しいヘルパによる自動クリーンアップに任せる形になっています。
2-3. Devcontainer のテストは対象外
Rails::Command::DevcontainerTest の use_mysql2 呼び出しは config/database.yml への書き込みのみを行い、実際の DB 作成はしていないため、今回の自動クリーンアップの影響は受けません。このテストはコード的にも変更なしです。
2-4. 動作確認
PostgreSQL では以下のように、テスト前後で pg_database をスナップショットして railties_* が残らないことを確認済みです:
psql -d postgres -At -c 'SELECT datname FROM pg_database'上記をテスト前後で比較し、以下のコマンドで走らせた 199 テストケース実行後に railties_* DB が残っていないことを確認:
cd railties && bin/test \
test/application/bin_setup_test.rb \
test/application/rake/dbs_postgresql_test.rb \
test/application/rake/multi_dbs_test.rb \
test/application/runner_test.rb \
test/application/test_runner_test.rb \
test/application/test_test.rb \
test/isolation/abstract_unit.rb- 影響範囲・注意点
テストヘルパ API の実質的拡張
use_postgresql/use_mysql2を「ブロック付き」で呼び出すことが推奨パターンになり、そのブロック内で作成された一時テストDBはヘルパが責任を持って削除します。- 既存の「ブロックなし呼び出し」も動作は維持されますが、ブロック外で作られた
railties_${Process.pid}_%DB は自動削除されません。
独自テストヘルパを持っている場合の注意
- Rails 本体に倣って
use_postgresql/use_mysql2をラップした社内ヘルパなどがある場合、それらも「ブロックを取る」ように書き換えることで、今回のクリーンアップロジックの恩恵を受けられます。 - テスト終了後に手動で
rails db:dropを叩いているようなパターンは、ブロック化とこのヘルパに任せる形に移行すると安全かつシンプルになります。
- Rails 本体に倣って
落とされる DB の条件
- 名前が
railties_${Process.pid}_%にマッチするもののみが対象です。それ以外の DB(例:app_development,app_test)は別 PR (#57457) で扱われており、この PR のクリーンアップ対象ではありません。 - 「差分のみ」を drop するため、ブロック実行前から存在していた同名 DB は削除されません。
- 名前が
テスト失敗時・例外時の挙動
- クリーンアップは
ensureで行われるため、ブロック内で例外が発生しても一時テストDBは drop されます。テストが失敗してもDBリークしにくい構造になっています。
- クリーンアップは
- 参考情報 (あれば)
- 対応する sibling PR: #57457
app_development/app_testのリーク対策を別途行っている PR。今回の PR はrailties_${Process.pid}_%系の per-PID/per-worker DB にフォーカスしています。
- 関連箇所:
ActiveRecord::TestDatabases.create_and_load_schema- per-PID/per-worker テストDBを作成し、それが今回のクリーンアップ対象となる。
TestHelpers::Generation#use_postgresql,#use_mysql2- ブロック対応 &
with_test_database_cleanupを通じた自動 drop を行う。
- ブロック対応 &
#57465 Clean up rails console subprocesses spawned by FullStackConsoleTest
マージ日: 2026/5/26 | 作成者: @yahonda
- 概要 (1-2文で)
FullStackConsoleTest がrails consoleをサブプロセスとして起動したまま放置していた問題を修正し、テスト終了時に必ずサブプロセスを kill/wait してクリーンアップするようにした PR です。これによりテスト実行時に zombie プロセスや不要なrails consoleプロセスが残らなくなります。
- 変更内容の詳細
対象: railties/test/application/console_test.rb に 8 行追加。
問題点
FullStackConsoleTest#spawn_consoleでProcess.spawnによりrails consoleを起動しているが、- その PID をどこかに記録して後で kill / wait する処理がなかった
- そのためテストファイルを 1 プロセスで実行すると、呼び出しごとにサブプロセスが増え続け、以下のように zombie と生きたままの console が大量に残る:
text=== ruby processes (PPid = test runner ruby) === 112454 Z(zombie) <- earlier spawn, exited, never reaped 112608 Z(zombie) ... 113373 S(sleeping) <- still-running rails console, 4 threads 114296 S(sleeping) ...console_test.rb内の 29 個のテストメソッドがそれぞれ 1 回以上 (中には 2 回)spawn_consoleを呼び出すため、リークが積み上がる構造。
対応内容
テストクラス内に「起動した console サブプロセスを記録し、テスト後に確実に終了させる」処理が追加されています。具体的なイメージは以下のようなコードです(実際のコードの構造を反映した擬似例):
class FullStackConsoleTest < ActiveSupport::TestCase
def setup
super
@console_pids = []
end
def spawn_console(...)
pid = Process.spawn(...)
@console_pids << pid
# 既存の spawn_console の戻り値や PTY 設定などは維持
pid
end
def teardown
@console_pids&.each do |pid|
begin
# Rails の「正常終了」は不要なので SIGKILL で即時終了
Process.kill("KILL", pid)
rescue Errno::ESRCH
# 既に終了している場合は無視
end
begin
Process.wait(pid)
rescue Errno::ECHILD
# 他で wait 済み or 既に reaped 済みの場合は無視
end
end
super
end
endポイント:
spawn_console呼び出し時に PID を配列などに溜める。teardown(テストごと)あるいはteardown相当のフックで:Process.kill("KILL", pid)で SIGKILL を送信し、即時終了させる。- 動機: テストは Rails の「きれいなシャットダウン」を検証したいわけではなく、PTY を通じてコンソールの動作を確認するだけなので、
TERMでの優雅な終了処理は不要。
- 動機: テストは Rails の「きれいなシャットダウン」を検証したいわけではなく、PTY を通じてコンソールの動作を確認するだけなので、
Process.wait(pid)でプロセスの終了を回収し、zombie を出さない。- 既にどこかで wait 済み/内部的に回収済みの可能性があるため、
Errno::ECHILDを rescue して安全に無視。
- 既にどこかで wait 済み/内部的に回収済みの可能性があるため、
- 影響範囲・注意点
- 影響範囲:
- Rails 本体の「実行時の挙動」や API ではなく、テストコード (
FullStackConsoleTest) のみ に限定された変更です。 rails consoleの動作やアプリケーションコードには一切影響しません。
- Rails 本体の「実行時の挙動」や API ではなく、テストコード (
- 開発者・CI 観点でのメリット:
- テスト実行後に zombie プロセスや不要な
rails consoleが残らなくなるため、- CI コンテナやローカル環境でのリソースリークが防げる
- 長時間テストを回す環境でのメモリ・プロセス数の肥大を抑えられる
ps/topなどで Ruby プロセスが大量発生する現象が解消される。
- テスト実行後に zombie プロセスや不要な
- 注意点:
SIGKILL使用のため、コンソール側で「終了時フック」や「後処理」を行うようなテストには不向きだが、このテストではそうしたシナリオを扱っていないため問題なし。Process.waitがErrno::ECHILDを rescue しているので、二重 wait 等によるテスト失敗は発生しない設計になっています。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57465
- 修正対象ファイル:
railties/test/application/console_test.rb - 関連トピック:
- Ruby のプロセス管理:
Process.spawn,Process.kill,Process.wait - テストでのサブプロセス管理のベストプラクティス(PID の記録と、
ensure/teardownでの kill + wait)
- Ruby のプロセス管理:
#57466 Dump enum types with fully qualified names for PostgreSQL
マージ日: 2026/5/25 | 作成者: @fatkodima
- 概要 (1-2文で)
PostgreSQL の enum 型を schema.rb にダンプするときに、「スキーマ付きの完全修飾名(schema.enum_name)」で出力するように変更した PR です。複数スキーマや同名 enum がある環境で、schema.rb の再ロード時に正しく enum を復元できるようになります。
- 変更内容の詳細
何をしているか
対象ファイル:
activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rbactiverecord/test/cases/adapters/postgresql/enum_test.rb
主な変更点:
- PostgreSQL の enum 型ダンプ時に、型名を「単純名」ではなく「完全修飾名(
schema_name.enum_name)」として扱うようにした。 - これに伴い、テスト (
enum_test.rb) にスキーマ付き enum 名のダンプを検証するケースを追加。
元 PR #56278 をリベース・再調整したものとしてマージされています。
想定されるコードイメージ
実際の内部実装の 1 行差し替えなので概念的なサンプルですが、イメージとしては:
# 変更前(イメージ)
create_enum "status", ["draft", "published"]
# 変更後(イメージ)
create_enum "public.status", ["draft", "published"]
# あるいは別スキーマ
create_enum "accounting.status", ["draft", "closed"]のように、schema.rb にダンプされる enum 型名が「スキーマ付き」になる方向の修正です。
テストでは、例えば以下のような観点を検証していると考えられます(概念的な例):
assert_schema_dump(expected_schema) do
connection.create_enum "custom.status", %w[draft published]
endここで expected_schema 側に create_enum "custom.status" が含まれることを確認するなど。
- 影響範囲・注意点
影響範囲
対象:
- DB に PostgreSQL を使用しており
enum型を使用していてschema.rb(ActiveRecord::Schema)形式でスキーマを管理しているプロジェクト
影響内容:
- 今後
rails db:schema:dumpなどで生成されるschema.rbのcreate_enum呼び出しが、スキーマ付きの名前で出力されるようになります。 - 複数スキーマに同名 enum が存在する場合でも、schema.rb をロードした際に正しいスキーマ・型が再現されるようになります。
- 今後
互換性・マイグレーション時の注意点
- 既存の
schema.rbとの差分:- 以前に生成済みの
schema.rbを、今回の修正が入った Rails で再生成すると、enum 型の名前部分にスキーマが付与されるため、git diffで差分が多く出る可能性があります。
- 以前に生成済みの
- スキーマを前提とした enum 名の扱い:
- すでに手作業で
schema.rbを編集していた場合、スキーマ名の有無や名前解決ロジックに依存しているコードがあれば影響が出ないか確認が必要です。
- すでに手作業で
- 良い影響:
- これまで、
search_pathや複数スキーマ構成に依存して enum 型を曖昧に解決していたケースで、ダンプ・ロードの再現性が向上し、より安全になります。
- これまで、
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57466
- 関連 PR(元の作業): https://github.com/rails/rails/pull/56278
- PostgreSQL ドキュメント(enum 型):
https://www.postgresql.org/docs/current/datatype-enum.html - PostgreSQL では、
schema.enum_typeという完全修飾名で enum 型を指定でき、search_pathに依存せずに型を一意に特定できるため、今回の変更はそのベストプラクティスに沿ったものといえます。
#56979 PostgreSQL::SchemaDumper drops index opclass when using persistent schemas alongside dump_schemas #56966
マージ日: 2026/5/25 | 作成者: @nicolasva
- 概要 (1-2文で)
dump_schemasと永続スキーマ(別スキーマに拡張を置く構成)を併用したときに、PostgreSQL のインデックス opclass がschema.rbから黙って落ちてしまう不具合を修正した PRです。
スキーマ修飾された opclass 名(shared_extensions.gin_trgm_opsなど)を正しくパースして、schema.rbに反映できるようにしています。
- 変更内容の詳細
問題の背景
within_each_schemaを使ってスキーマごとにダンプする際、schema_search_pathが「ダンプ対象のスキーマ」だけになります(例:"public")。- 一方、拡張機能(例:
pg_trgm)を別スキーマ(例:shared_extensions)にインストールしているケースがあります。 - このとき、PostgreSQL が
pg_get_indexdefで返すインデックス定義には、opclass がスキーマ付きで出てきます:- 例:
... USING gin (name shared_extensions.gin_trgm_ops)
- 例:
- 既存の実装では opclass を抽出する正規表現が
\w+_opsで、ドットを含むshared_extensions.gin_trgm_opsにマッチせず、opclass 情報が捨てられていました。 - 結果として、
schema.rbから opclass 指定が消え、db:schema:loadなどでインデックス定義が変わってしまう可能性がありました。
具体的な修正点
activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb の opclass 抽出処理が変更されています。
1) 正規表現の拡張
以前:
/opclass:\s*(\w+_ops)/修正後(イメージ):
/opclass:\s*((?:\w+\.)?\w+_ops)/(?:\w+\.)?を追加 → 「schema_name.があってもなくてもよい」という形に拡張- これにより、以下どちらもマッチ可能になります:
gin_trgm_opsshared_extensions.gin_trgm_ops
2) スキーマ修飾部分の除去
マッチした opclass 名に対して:
opclass_name = match[1].split(".").last.to_symという形で、shared_extensions.gin_trgm_ops → "gin_trgm_ops" に変換したうえで Symbol 化します。
- これにより、スキーマ修飾を気にせず、従来どおり
using: :gin, opclass: :gin_trgm_opsのような形でschema.rbに書き出せます。
3) テストの追加
activerecord/test/cases/adapters/postgresql/schema_test.rb に SchemaOpclassParsingTest が追加されています。
2ケース:
スキーマ修飾なしの opclass
- 例:
USING gin (name gin_trgm_ops) - 期待: 正しく
:gin_trgm_opsとしてパースされること
- 例:
スキーマ修飾ありの opclass
- 例:
USING gin (name shared_extensions.gin_trgm_ops) - 期待: スキーマ部分を無視して
:gin_trgm_opsとしてパースされること
- 例:
これにより、今回のバグが再発しないように回帰テストが整備されています。
- 影響範囲・注意点
- 対象:
- PostgreSQL を使用し、
pg_trgmなどの拡張を「別スキーマ」にインストールしており、dump_schemasと永続スキーマ(複数スキーマ運用)を組み合わせているプロジェクト
- 影響:
- これまで
schema.rbから黙って落ちていた opclass 指定が正しく保持されるようになります。 - インデックスの再作成時(
db:schema:loadなど)に、期待どおりの opclass でインデックスが張られるようになります。
- これまで
- 互換性:
- スキーマ修飾のない opclass(従来ケース)も従来どおりパースされ、影響はありません。
- 正規表現は「
(schema_name.)?name_ops」というよくあるパターンのみを許容しているため、意図せず別の文字列を誤認識するリスクも小さいです。
実運用上は、この修正を含む Rails にアップデートした後、db:schema:dump をやり直すことで、schema.rb に正しい opclass 情報が反映されます。
- 参考情報 (あれば)
- 元 issue: #56966
「PostgreSQL: opclass silently dropped from schema.rb when using dump_schemas」 - この PR: #56979
「PostgreSQL::SchemaDumper drops index opclass when using persistent schemas alongside dump_schemas」 - 関連コンポーネント:
ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatementswithin_each_schema/dump_schemas- 拡張例:
pg_trgm(gin_trgm_opsopclass)
#57457 Drop generated app databases after test_bin_setup_output
マージ日: 2026/5/25 | 作成者: @yahonda
- 概要 (1-2文で)
Rails のテストApplicationTests::BinSetupTest#test_bin_setup_outputが PostgreSQL 上に作成していたapp_development/app_testデータベースを、テスト終了時に確実に削除するようにした PR です。テスト実行後に不要なデータベースが残り続ける問題を解消し、クリーンなテスト環境を保つことが目的です。
- 変更内容の詳細(あればサンプルコードも含めて)
何が問題だったか
- 対象テスト:
railties/test/application/bin_setup_test.rbのApplicationTests::BinSetupTest#test_bin_setup_output - このテストでは以下を行っていました:
- 生成したダミーアプリの DB アダプタを PostgreSQL に切り替える
- そのアプリに対して
bin/setupを実行する
bin/setupによりapp_developmentとapp_testという PostgreSQL データベースが作成される- しかし、テスト終了時に実行される
teardown_appは「生成したアプリのディレクトリを削除する」だけで、PostgreSQL のデータベースそのものは削除していなかった - その結果、テストを実行するとローカルの PostgreSQL に
app_development/app_testが残り続けてしまう
今回の修正内容
- テスト内で
Dir.chdir(app_path)しているブロックの中にensure節を追加し、そこで生成アプリ用の DB を削除するようにしました。 - 既に同じテストメソッド内で使われている
rails "db:drop"の呼び出しに合わせて、allow_failure: trueオプションを付けています(DB が存在しないなどで失敗してもテストを落とさない)。
変更イメージは以下のようなものです(簡略化した擬似コード):
Dir.chdir(app_path) do
begin
# ...(既存の検証処理: bin/setup の実行や出力チェックなど)
ensure
# 生成アプリの PostgreSQL DB を削除
rails "db:drop", allow_failure: true
end
end実際の差分は +2 行のみで、ensure 内で db:drop を実行するコードが足された形です。
動作確認
- PR 説明によると、以下の手順で検証済み:
psql -d postgres -At -c 'SELECT datname FROM pg_database'で DB 一覧を取得しスナップショットcd railties && bin/test test/application/bin_setup_test.rbを実行- 再度同じ
psqlコマンドで一覧を取得 - 実行後に
app_development/app_testが存在しないことを確認
- 影響範囲・注意点
- 対象は Rails コアのテストコードのみであり、アプリケーション本体の挙動や API 仕様には一切影響しません。
- PostgreSQL を使って Rails 本体のテスト(
railties)を回している開発者にとってのみ影響があり、「テスト実行後にapp_development/app_testが残らなくなる」という改善になります。 allow_failure: trueにより、万一 DB が既に削除済み / 作成されなかったなどでdb:dropが失敗してもテストが落ちないため、テストの安定性や既存の挙動を壊さないよう配慮されています。- 実運用のアプリケーション DB には一切触れない(生成アプリのコンテキスト内だけで
db:dropを実行)ため、誤削除のリスクはありません。
- 参考情報 (あれば)
- 対象テストファイル:
railties/test/application/bin_setup_test.rb - 目的: 「生成アプリを使ったテストが DB を汚さないようにする」というテストクリーンアップの一環
- 類似のパターン:
- 生成アプリを使うテストでは、ディレクトリ削除だけでなく「外部リソース(DB、キュー、ストレージなど)のクリーンアップ」を
ensureなどで行うのが望ましい、という設計指針を示している変更と言えます。
- 生成アプリを使うテストでは、ディレクトリ削除だけでなく「外部リソース(DB、キュー、ストレージなど)のクリーンアップ」を
#57158 Fix touch_record for composite foreign keys
マージ日: 2026/5/24 | 作成者: @romulostorel
- 概要 (1–2文で)
複合外部キーを持つbelongs_toでtouch: trueを指定した場合、関連先が変更されたときに「古い側」のレコードがtouchされない不具合を修正するPRです。touch_recordの実装を、単一キー/複合キーの両方で正しく旧レコードを特定できるように見直しています。
- 変更内容の詳細
問題の状況
例えば次のようなモデル構成:
class Order < ActiveRecord::Base
self.primary_key = [:shop_id, :order_number]
end
class Book < ActiveRecord::Base
belongs_to :order,
foreign_key: [:shop_id, :order_number],
primary_key: [:shop_id, :order_number],
touch: true
endこのとき:
order1 = Order.create!(shop_id: 1, order_number: 100, name: "Order 1")
order2 = Order.create!(shop_id: 1, order_number: 200, name: "Order 2")
book = Book.create!(title: "My Book", shop_id: 1, order_number: 100)
sleep 1
book.update!(order_number: 200)期待される挙動は:
order2…updated_atが更新される(Book が紐づいた側)order1… 旧関連先としてupdated_atが touch される(関連解消時の touch)
しかし現状(修正前)は:
order2のupdated_atのみ更新され、order1のupdated_atは更新されない(= サイレントに無視される)
原因は、belongs_to の touch_record 実装が「複合外部キー」を想定しておらず、saved_changes(または changes)の扱いが単一カラム前提になっていたことです。
根本原因の詳細
touch_record内で、外部キーの変更をchanges[foreign_key]で取得しようとしていた。- 複合外部キーの場合、
foreign_keyは[:shop_id, :order_number]のような配列。 - 一方で
saved_changesは{"shop_id" => [...], "order_number" => [...]}のように「カラム名ごと」にキーを持つ。 - そのため
changes[[:shop_id, :order_number]]というハッシュ参照は常にnilを返し、「外部キーが変わっていない」と誤認して旧レコード側を touch しない、というバグになっていた。
修正内容
activerecord/lib/active_record/associations/builder/belongs_to.rb の touch_record を以下の方針で修正しています:
外部キーを常に配列として扱う
foreign_key(reflection.foreign_key)をArray(foreign_key)で正規化し、- 単一キーと複合キーの処理経路を統一。
各外部キーカラムごとに変更有無をチェック
saved_changes(もしくはchanges)を参照し、- 例えば
[:shop_id, :order_number]であれば、saved_changes["shop_id"]/saved_changes["order_number"]
- のように「カラム名ごと」に以前の値・新しい値を取り出す。
「旧側」の複合キー値を組み立てるロジックの追加
- 各カラムごとに:
- そのカラムが変更されていれば
saved_changes[column].first(old value)を採用 - 変更されていなければ
read_attribute(column)(現在の値 = old/new共通)を採用
- そのカラムが変更されていれば
- こうして、
old_foreign_id = [old_shop_id, old_order_number]のように「旧側の複合外部キー値」を組み立てる。
- 各カラムごとに:
複合主キーにも対応した
find_byの組み立て- 関連先モデルの主キー(
primary_key)もArray(primary_key)で配列化。 Array(primary_key).zip(old_foreign_id).to_hで検索条件を生成し、- 単一キーなら
{ "id" => old_id } - 複合キーなら
{ "shop_id" => old_shop_id, "order_number" => old_order_number }
- 単一キーなら
- これを
find_byに渡して旧レコードを特定し、touchを実行。
- 関連先モデルの主キー(
この結果、外部キーが単一の場合も複合の場合も、同一ロジックで「以前紐づいていた関連レコード」を正しく見つけて touch できるようになります。
テストの追加・変更
activerecord/test/cases/timestamp_test.rb- 複合主キー+複合外部キー環境で、
- 関連先変更により「新しい Order が touch されること」
- 「古い Order も touch されること」
- を確認するテストが追加。
- 複合主キー+複合外部キー環境で、
activerecord/test/models/cpk/book.rb/activerecord/test/schema/schema.rb- 上記シナリオを実現するためのモデル・スキーマが微調整されています(複合キー前提のbook/orderモデルなど)。
- 影響範囲・注意点
- 影響を受けるケース
belongs_toにおいて:foreign_keyを配列で指定(複合外部キー)primary_keyも単一または複合キーで明示touch: trueを指定
- 上記条件で「関連先を別のレコードに付け替える」ケース。
- 期待される挙動の変化
- これまで「古い関連先が touch されていなかった」ケースで、
- 古い関連先レコードの
updated_at(等のタイムスタンプ)が更新されるようになる。
- 古い関連先レコードの
- ログや監査用途で
updated_atを見ている場合、アップグレード後に「以前は変わっていなかったレコードのタイムスタンプが動く」ことがありますが、これはもともとの仕様意図に沿った振る舞いです。
- これまで「古い関連先が touch されていなかった」ケースで、
- 互換性
- 単一外部キーの
belongs_to ... touch: trueの挙動は仕様上変わりません(内部実装の共通化のみ)。 primary_keyが単一か複数かにかかわらず対応するようになったため、カスタム主キーや複合主キーを活用しているアプリでもより一貫した挙動になります。
- 単一外部キーの
- 参考情報 (あれば)
- 関連するAPI/リファレンス:
- 概念的には、
dependent: :destroy等と同様、「関連付けの変更に伴い、旧関連先にも副作用(ここではtouch)を及ぼす」系の機能の一部であり、複合キー利用時のサポート強化/バグフィックスと言えます。
#57163 Document that value_method and text_method arguments of options_from_collection_for_select accept Proc [ci skip]
マージ日: 2026/5/24 | 作成者: @cgunther
- 概要 (1-2文で)
options_from_collection_for_selectとoption_groups_from_collection_for_selectのvalue_method/text_method/group_method/group_label_methodに、従来から存在していたProcのサポートを公式ドキュメントに明記した PR です。コードの挙動は変わらず、ドキュメントのみの更新です。
- 変更内容の詳細
対象ヘルパー:
ActionView::Helpers::FormOptionsHelper#options_from_collection_for_selectActionView::Helpers::FormOptionsHelper#option_groups_from_collection_for_select
これらのメソッドの以下の引数について、「シンボル/文字列によるメソッド名」だけでなく「Proc も渡せる」ことがコメント(ドキュメント)として追記されました。
options_from_collection_for_selectvalue_methodtext_method
option_groups_from_collection_for_selectgroup_methodgroup_label_method- (内部的には各グループの
value_method/text_methodも同様に Proc を取れる)
コード上はもともと、object.public_send(value_method) するような単純な実装ではなく、渡された引数が呼び出し可能であれば call するようなロジックで、Proc を受け付ける設計になっていました。今回の PR では、その既存仕様が Yard ドキュメントコメントに反映されただけです。
想定される利用イメージは以下のような形です。
options_from_collection_for_select での Proc 利用例
# コレクション
users = User.all
# value を任意のロジックで組み立てる
value_proc = ->(user) { "#{user.id}-#{user.account_code}" }
# text も任意のフォーマットで組み立てる
text_proc = ->(user) { "#{user.name} (#{user.email})" }
options_from_collection_for_select(users, value_proc, text_proc)従来ドキュメントには :id, :name のようにメソッド名を渡す例しか書かれていませんでしたが、今回の変更で上記のような Proc 利用が公式にサポートされていることが明示されます。
option_groups_from_collection_for_select での Proc 利用例
# groups: 例として Team モデルの配列
teams = Team.includes(:users)
group_method = ->(team) { team.users } # 各グループに属する items
group_label_method = ->(team) { "#{team.name} (#{team.users.count} users)" }
value_proc = ->(user) { user.id }
text_proc = ->(user) { "#{user.name} <#{user.email}>" }
option_groups_from_collection_for_select(
teams,
group_method,
group_label_method,
value_proc,
text_proc
)上記のように、グループの展開 (group_method) や表示ラベル (group_label_method) も Proc で柔軟に制御できることがドキュメントに追記されています。
- 影響範囲・注意点
影響範囲:
- ライブラリの挙動変更はなく、ドキュメントの更新のみです。
- すでに
Procを渡して利用していたコードに影響はありませんが、「非公式っぽいテクニック」という心理的ハードルが下がり、今後は積極的に利用してよいと判断できます。
注意点:
- ドキュメントが公式に
Procを許容したことで、将来のバージョンでもProcサポートを維持すべき API 契約として扱われる可能性が高くなりました。 Proc内で例外を投げると、selectのレンダリング時に例外が発生します。ビューロジックとして適切な範囲の処理に留めることが望ましいです。Procは1引数(各要素)を受け取る形で設計されているので、->(record) { ... }という形にする必要があります。
- ドキュメントが公式に
- 参考情報 (あれば)
該当ファイル:
actionview/lib/action_view/helpers/form_options_helper.rb
(メソッドコメント内にProcを受け付ける旨の記述が追加されている)関連ヘルパーの RDoc / API ドキュメント:
options_from_collection_for_select(collection, value_method, text_method, selected = nil)option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
今回の PR によって、これらのメソッドの *_method 引数で「メソッド名(Symbol/String)または Proc」を選べることが公式仕様として明確になります。
#56973 [ci skip] Update ActiveRecord Migrations documentation concerning dat…
マージ日: 2026/5/24 | 作成者: @joshmfrankel
- 概要 (1-2文で)
Active Record Migrations ガイドの「データマイグレーション」節に、script/ディレクトリを使った一度きりのスクリプトやデータ移行の配置場所についての公式な説明が追記された PR です。既存のmaintenance_tasksgem の紹介に加えて、Rails 本体に用意されているスクリプト実行の仕組みとの関係を明示しています。
- 変更内容の詳細
※PR本文から読み取れる範囲での要約です(実際の差分は guides/source/active_record_migrations.md で 11 行追加・5 行削除)。
追加された主な内容
- Active Record Migrations ガイド内の「Data Migrations(データマイグレーション)」に、一度きりのスクリプト/データ移行の置き場所として
script/ディレクトリを挙げる文書が追加された。 - すでに紹介されていた
maintenance_tasksgem を使う方法 に並べて、「公式に推奨される整理パターン」としてscript/ディレクトリを解説する形になっている。
典型的なイメージ:
- スキーマ変更(テーブル追加・カラム追加など)
→ 通常どおりdb/migrateのマイグレーションファイル - 一時的・一度きりのデータ処理(既存データの修正、バックフィル、データ移行など)
→script/ディレクトリ内の Ruby スクリプト
またはmaintenance_tasksgem を用いたタスクとして管理
想定されるサンプルコード(イメージ)
ドキュメント中では、例えば以下のような形が説明されている可能性があります(Rails コミュニティの一般的なパターンに基づく例):
# 一時的なデータ移行スクリプト例
script/
backfill_user_timezone.rb# script/backfill_user_timezone.rb
User.find_each do |user|
next if user.timezone.present?
user.update!(timezone: "UTC")
endそして、Railties が用意するスクリプト実行の仕組みや bin/rails runner などを使って実行する使い方が説明されている可能性があります:
bin/rails runner script/backfill_user_timezone.rbまた、同じ用途を maintenance_tasks を使って行う場合の対比(バックフィルタスクを class として定義し、UI やジョブキューと連携させる等)にも触れていると考えられます。
- 影響範囲・注意点
- 実装・挙動への影響はない
この PR はドキュメントのみの変更であり、Rails 本体のコードや挙動、API に変更はありません。既存アプリケーションへの互換性問題もありません。 - データマイグレーションの「置き場所」の公式な指針が明確に
これまで議論されがちだった「一度きりのスクリプトやデータ移行ロジックをどこに置くか」について、Rails ガイドとしてdb/migrateはスキーマ変更にフォーカスする- データ移行・一時スクリプトは
script/(またはmaintenance_tasks)に置く という分類が文章として明示されるため、チーム内のベストプラクティスを決める際の根拠にしやすくなります。
- 新規プロジェクトでの構成方針に影響
- 「一度きりのデータ修正をマイグレーションに書くか?」という議論の際に、「公式ガイドでは
script/やmaintenance_tasksを推奨している」という形で合意形成しやすくなります。 - 既存プロジェクトで
script/ディレクトリをあまり使っていない場合でも、今後のデータ移行タスクの配置先として採用していくきっかけになります。
- 「一度きりのデータ修正をマイグレーションに書くか?」という議論の際に、「公式ガイドでは
- 参考情報 (あれば)
- PR 本文で参照されている議論:
- 一度きりのスクリプトの置き場所に関するスレッド
https://discuss.rubyonrails.org/t/is-there-any-official-way-to-organize-one-off-scripts/74186/3 - 関連 PR (
scriptディレクトリ関連の議論・機能追加)
https://github.com/rails/rails/pull/52335
- 一度きりのスクリプトの置き場所に関するスレッド
maintenance_tasksgem:- GitHub: https://github.com/Shopify/maintenance_tasks
(長時間実行のメンテナンスジョブ/データ移行を UI 付きで実行・再開できる gem)
- GitHub: https://github.com/Shopify/maintenance_tasks
#57455 Fix typo in metaprogramming comment
マージ日: 2026/5/24 | 作成者: @nertzy
- 概要 (1-2文で)
ActiveSupportのString#output_safety周りのコメント中のタイポ(**が誤って&&と記載されていた)を修正した PR です。実装自体の挙動は変わらず、コメントだけが正しい内容に整合するように修正されています。
- 変更内容の詳細 (サンプルコード例を含む)
- 対象ファイル:
activesupport/lib/active_support/core_ext/string/output_safety.rb - 変更内容: コメント中の演算子表記の誤りを修正
- 誤:
&& - 正:
**
- 誤:
このファイルは、ActiveSupport::SafeBuffer / String#html_safe など、HTML セーフティ関連の拡張が定義されている部分で、メタプログラミングを用いて文字列操作メソッドをラップ・委譲する処理に関するコメントが書かれています。
そのコメント中で「どのような演算子・メソッドをラップするか」を説明している箇所に、実装に合わせるとべき記号演算子 **(べき乗演算子)が、本来意図していない &&(論理 AND 演算子)と書かれていたのを修正したものです。
実コード(Ruby)のイメージとしては、例えば以下のように、演算子メソッドを列挙して動的に定義しているコメントに対応していると考えられます:
# 例: 実装イメージ(実コードとは異なる可能性があります)
# %i[+ << concat prepend gsub gsub! sub sub! * **].each do |method|
# # これらのメソッドは SafeBuffer に対して〜〜
# endここの説明コメントに「**」と書くべきところが、以前のコミットで誤って「&&」になっていたため、今回の PR で修正されています。
- 影響範囲・注意点
- コードロジック・API・挙動の変更は一切ありません。
String/SafeBufferのメソッド呼び出し動作には全く影響しません。- 影響はドキュメンテーションレベル(コメント)だけであり、既存アプリケーションやライブラリへの互換性上の懸念は不要です。
- コメントを参照して実装を読む開発者に対する誤解のリスクが減るという、読みやすさ・保守性の向上のみです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57455
- 問題の導入コミット:
47cec2bb17b5a65df66385325b7997bd32b409fc- このコミットでコメント修正時に
**→&&の誤置換が発生したと説明されています。
- このコミットでコメント修正時に
#56582 Fix PostgreSQL COMMENT ON INDEX for schema-qualified tables
マージ日: 2026/5/23 | 作成者: @eglitobias
- 概要 (1-2文で)
PostgreSQL でスキーマ付きテーブル(例:"other_schema.widgets")に対してadd_indexをコメント付きで作成した際、COMMENT ON INDEXがスキーマ無しのインデックス名で発行されてエラーになる問題を修正する PR です。これにより、インデックスコメントもインデックス作成時と同様にスキーマ修飾された名前を使うようになります。
- 変更内容の詳細
問題の内容
add_index に comment: オプションを付けて、スキーマ修飾済みのテーブル名を渡すと次のようなコードになります。
add_index "other_schema.widgets", :id, name: "index_widgets_on_id", comment: "some comment"現状(修正前)、Active Record はインデックス自体は正しいスキーマに作りますが、インデックスにコメントを付ける SQL を次のように生成していました。
COMMENT ON INDEX "index_widgets_on_id" IS 'some comment'PostgreSQL では、インデックスに対するコメントは スキーマ付きで指定する必要があり、正しくは:
COMMENT ON INDEX "other_schema"."index_widgets_on_id" IS 'some comment'でなければなりません。そのため、other_schema にテーブル・インデックスが存在していても、スキーマを付けない COMMENT ON INDEX により PG::UndefinedTable が発生していました。
修正内容のポイント
この PR では、以下のような対応が行われています。
スキーマ付きテーブル名からスキーマ名を考慮してインデックス名を構築
add_index呼び出し時に、テーブル名が"schema.table"の形式で与えられていた場合、- 生成するインデックスコメント用の識別子に、そのスキーマ名を含めるように変更。
- Rails 内部ではおおよそ以下のような挙動に近づく形になります(擬似コード):
ruby# 例: table_name = "other_schema.widgets" schema, plain_table_name = extract_schema_and_table(table_name) index_name = options[:name] || index_name_for(plain_table_name, column_names) index_identifier = schema ? %("#{schema}"."#{index_name}") : %("#{index_name}") execute <<~SQL COMMENT ON INDEX #{index_identifier} IS #{quote(comment)} SQLPostgreSQL アダプタ側の
schema_statementsの修正- PostgreSQL 固有の
schema_statements実装でコメント付与ロジックを修正し、テーブル名にスキーマが含まれる場合はインデックス名にも同じスキーマを付けるようにしています。 - インデックス自体の作成時のスキーマ解決ロジックと、コメント生成時のロジックの整合性がとられました。
- PostgreSQL 固有の
テストの追加
activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rbにテストが追加され、スキーマ付きテーブルに対してadd_index+comment:を行った場合に、例外が出ず正しくコメントが適用されることを確認しています。- これにより、回帰テストとしても機能します。
- 影響範囲・注意点
影響を受けるのは PostgreSQL を使っていて、かつスキーマ付きテーブル名を文字列で渡しているケースのみです。
例:add_index "other_schema.widgets", :id, comment: "..."既存のマイグレーションについて:
- これまで「マイグレーション実行時に
PG::UndefinedTableで落ちていたもの」は、この修正以降は正常に通るようになります。 - すでに「コメントなしで回避」「別の方法でコメントを付けていた」場合でも挙動が壊れることはありません。
- これまで「マイグレーション実行時に
インデックス名を明示指定していない場合でも、Rails が自動生成するインデックス名にスキーマを正しく付けて
COMMENT ON INDEXを実行するため、特別な対応は不要です。他の DB アダプタ(MySQL, SQLite など)には影響しません。PostgreSQL 固有の挙動修正です。
- 参考情報 (あれば)
- 対応 Issue: #56581
- 対応 PR: #56582 「Fix PostgreSQL COMMENT ON INDEX for schema-qualified tables」
- PostgreSQL ドキュメント (COMMENT):
https://www.postgresql.org/docs/current/sql-comment.htmlCOMMENT ON INDEXの構文として、[schema_name.]index_nameが必要であることに起因する問題です。
#57335 Handle invalid SendGrid envelope input in Action Mailbox
マージ日: 2026/5/23 | 作成者: @afurm
- 概要 (1-2文で)
Action Mailbox の SendGrid 受信エンドポイントにおいて、envelopeパラメータが「JSONとしては正しいが、想定した構造ではない」場合に、422 Unprocessable Content を返してメールを作成しないようにする防御的なバリデーションが追加されました。これにより、既存の「JSONとして不正なenvelope」と同様の扱いになり、挙動の一貫性と堅牢性が向上しています。
- 変更内容の詳細
背景
- 既存実装:
- SendGrid ingress(
ActionMailbox::Ingresses::Sendgrid::InboundEmailsController)は、envelopeパラメータが「無効なJSON」の場合、422 Unprocessable Contentを返す。 - しかし、「JSONとしてはパースできるが、期待する形(スキーマ)ではない」
envelopeについては明確な防御がなく、コントローラ内部で想定外の構造を前提に処理する可能性があった。
- SendGrid ingress(
- このPRでは、JSONパース後に
envelope["to"]の型や存在を検証し、想定されない構造の場合は 422 を返すよう統一しています。
具体的な挙動の変更
envelopeのバリデーション追加- これまで:
envelopeが JSON としてパースできれば、そのままenvelope["to"]を使おうとする実装。
- 変更後:
- パース後の
envelopeに対して、以下を満たすかをチェック:envelope["to"]キーが存在することenvelope["to"]が「文字列の配列(Array<String>)」であること
- これらを満たさない場合は、既存の「unprocessable」なリクエストとして扱い、422を返し、InboundEmail を作成しない。
- パース後の
疑似コードイメージ:
rubyenvelope = JSON.parse(params[:envelope]) if params[:envelope] unless valid_envelope?(envelope) # 422 Unprocessable Content を返し、処理を打ち切る render_unprocessable_entity return end recipients = envelope["to"] # Array<String> であることが保証されるvalid_envelope?の中で、「toが存在し、配列であり、各要素が String」であるかをチェックするイメージです。- これまで:
「オプションパラメータとしての
envelope」の扱いの明確化PR文中で「optional
envelopeparameter」とあるように、envelope自体は必須ではありませんが、送られてきた場合は形を厳格にチェックするようになりました。envelopeが送られてこない場合の挙動は従来通り。- 送られてきていても:
- JSONとして不正 → 422
- JSONとしては正しいが、
toが欠けている /toの型が不正 → 422 - 正常な形 → 通常処理(InboundEmail作成処理)に進む
テストの追加・更新
変更されたテストファイル:
actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb
主に次のようなケースがテストされていると考えられます:
envelopeにtoが存在しない場合 → 422 が返却され、メールが作成されないことenvelope["to"]が配列でない場合(例: 文字列やオブジェクト) → 422envelope["to"]が配列だが、要素が文字列以外を含む場合 → 422- 正常な
envelope(toが文字列配列) → 200/202相当で、InboundEmail が作成されること
CHANGELOG の更新
actionmailbox/CHANGELOG.mdに 4 行の追記があり、
「SendGrid ingressにおけるenvelopeバリデーション追加」に関する記述(バグ修正/振る舞い変更としてのメモ)が追加されています。
- 影響範囲・注意点
SendGrid 連携を利用しているアプリケーションへの影響:
- 正常な SendGrid 本番トラフィック(公式仕様に従ったペイロード)であれば、この変更による影響は基本的にありません(PR説明にも「not a known SendGrid production payload」と明記されています)。
- ただし、独自の実装やテスト等で、
envelopeのフォーマットが厳密でないリクエストを送っている場合、- これまではたまたま受け入れられていたケースが、
- 今後は 422 Unprocessable Content で拒否されるようになります。
防御的プログラミングの一環としての変更:
- 不正・異常な入力に対して Rails 側が早期にエラーを返すことで、
- 内部処理(メール作成ロジック)側の前提が崩れにくくなる
- 予期せぬ例外やデータ破壊のリスクを減らす
- HTTP レベルでの一貫した応答(「解釈不可能なコンテンツ」は 422)を保つ目的があります。
- 不正・異常な入力に対して Rails 側が早期にエラーを返すことで、
挙動の確認ポイント(既存アプリの開発者向け):
- 自前で SendGrid からの Webhook をモックしているテストやツールがあれば、
- その
envelopeJSON が以下を満たしているか確認すると安全です:envelopeがJSONオブジェクトenvelope["to"]が存在するenvelope["to"]が["user@example.com", ...]のような配列- 各要素がメールアドレス文字列(少なくとも String 型)である
- その
- 自前で SendGrid からの Webhook をモックしているテストやツールがあれば、
- 参考情報 (あれば)
- 関連 Issue: #57334
- 「
envelopeが期待する形でない場合の扱い」に関する報告・議論がなされていると考えられます。
- 「
- 変更対象コントローラ:
actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb- SendGrid 受信 Webhook エンドポイントを扱う主要なクラスであり、ここに
envelope構造チェックが追加されています。
- 実行された検証コマンド:
- 個別テスト:
cd actionmailbox && bin/test test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb
- Action Mailbox 全体テスト:
cd actionmailbox && bin/test
- 静的解析 (Rubocop):
bundle exec rubocop actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb
- 個別テスト:
このPRは、「挙動の仕様を変える」というよりは、「これまで暗黙的だった前提(envelope["to"] は配列の文字列である)をコード上で明示し、異常系を 422 で早期に弾く」ための安全性・一貫性向上の修正と捉えると理解しやすいです。
#57452 Handle malformed Mandrill events payloads in Action Mailbox
マージ日: 2026/5/23 | 作成者: @Messier81
- 概要 (1-2文で)
Mandrill の Action Mailbox ingress が、JSON の「形」が不正な payload を受け取った際に 500 を返していた問題を修正し、422 Unprocessable Content を返すようにした PRです。mandrill_eventsが「配列の Hash であること」を検証し、そうでない場合は新設のMalformedEventsErrorを投げて既存ハンドリングに乗せています。
- 変更内容の詳細
背景の問題点
従来の Mandrill ingress の挙動:
mandrill_eventsパラメータを JSON としてパース- パースに失敗 (
JSON::ParserError) した場合
→ 422 Unprocessable Content を返す (OK な挙動) - しかし、JSON としては妥当だが、想定している構造と違う場合:
- 例:
"null","1","{}","[1,2,3]","[null]"など - コントローラ内で
events.select { ... }のように配列前提でメソッドを呼ぶため、nil.select→NoMethodError- 配列中に Hash 以外が入り、それを
.[]などしてTypeError
- これにより 500 Internal Server Error が発生していた
- 例:
本 PR での方針は、「JSON 文字列として妥当か」だけでなく、「イベント配列として妥当か」も検証し、不正なら 422 にする、というものです。
具体的な実装変更
1. 形の検証と MalformedEventsError
Mandrill ingress コントローラ (actionmailbox/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb) にて:
mandrill_eventsをパースした結果に対して、以下を満たすかをチェック:- オブジェクト全体が
Arrayである - かつ、その要素がすべて
Hash(Mandrill イベントオブジェクト) である
- オブジェクト全体が
擬似コードイメージ:
def mandrill_events
events = JSON.parse(params[:mandrill_events])
unless events.is_a?(Array) && events.all? { |event| event.is_a?(Hash) }
raise MalformedEventsError
end
events
rescue JSON::ParserError, MalformedEventsError
# 422 を返し、InboundEmail は作らない
end※実際のコードは Rails コーディングスタイルに合わせてもう少し分割されているはずですが、ロジックとしてはこのイメージです。
MalformedEventsErrorは、この「形が不正」ケース専用の例外として新設され、- 既存の
rescue JSON::ParserErrorとまとめて扱われることで、レスポンスを 422 に統一します。
2. Mailgun テストの調整
actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb も変更されているのは、
テストヘルパーやエラー検証ロジックを共通化・調整したためです。
考えられる変更点:
- 「不正 payload を送ったとき 422 になること」を汎用的にチェックするヘルパーメソッドの追加
- Mandrill 用に追加した
MalformedEventsErrorや、ステータスコードの検証に合わせて Mailgun 側の期待値・書き方を揃えた
これにより、Mandrill / Mailgun ingress 双方で「不正入力に対して 500 を出さない」ことをテストで保証できます。
3. テストの追加・拡充
actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb に多くの行が追加されています (+51/-2)。
主な内容:
- 形が不正な JSON payload についてのテストケース
null, スカラー値 (数値や文字列), Hash ルート, 配列だが要素が Hash でない場合など- これらを
mandrill_eventsとして送信し、- レスポンスが 422 Unprocessable Content であること
ActionMailbox::InboundEmailが作成されないこと
- 正常な payload (配列の Hash) に対する既存テストの維持
actionmailbox/test/test_helper.rb も +9/-1 行の変更があり、
テスト共通ヘルパーの追加 (例: 不正 payload を送るヘルパー、エラーレスポンス検証など) が行われています。
4. CHANGELOG の更新
actionmailbox/CHANGELOG.md に以下のようなエントリが追加:
- Mandrill ingress で malformed な Mandrill events payload に対して 500 ではなく 422 を返すようになったことを明記。
- 将来アップグレードする利用者が、この挙動の修正を把握できるようにしている。
- 影響範囲・注意点
- 対象:
- Mandrill を Action Mailbox ingress として利用しているアプリケーション
- 挙動の変化:
- これまで 500 が出ていた「JSON だが形が不正」な
mandrill_eventsに対して 422 が返るようになります。 - 正常な payload (イベント配列) に対する処理や、既に 422 を返していた「純粋な JSON パースエラー」の挙動は変わりません。
- これまで 500 が出ていた「JSON だが形が不正」な
- 互換性:
- アプリ側で「Mandrill ingress が 500 を返したときだけ特別な処理をする」といったロジックを組んでいない限り、後方互換性の問題はほぼありません。
- 監視周りでは、「Mandrill inbound でたまに 500 が出ていた」のが「422 に減る/なくなる」ため、エラーレートが改善する可能性があります。
- セキュリティ・堅牢性:
- 不正な入力で内部例外 (
NoMethodError,TypeError) を発生させず、安全に 422 を返すようになったため、
ingress エンドポイントの堅牢性が向上しています。 - エラーレスポンスとして 500 スタックトレースなどが外に漏れる可能性も下がります (本番設定では元々抑制されているはずですが、開発環境などでもより明確な 422 になります)。
- 不正な入力で内部例外 (
- 参考情報 (あれば)
- 対応する PR: https://github.com/rails/rails/pull/57452
- 対象コンポーネント:
Action Mailbox→Mandrill ingress - 検証コマンド (PR 説明より):
cd actionmailbox && bin/test test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
cd actionmailbox && bin/test
bundle exec rubocop \
actionmailbox/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb \
actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
git diff --checkMandrill を使っている場合は、アップグレード後に「意図せず 500 が発生していないか」「422 のレスポンスを受けた際の再試行戦略」があるかを確認しておくと安心です。
#57453 Fix HTML-aware ERB compiler errors in bundled templates so they apss strict validations
マージ日: 2026/5/23 | 作成者: @thomaswitt
- 概要 (1-2文で)
Herb 0.10+ の「HTML 構文・属性位置を検証する ERB コンパイラ」で Rails 同梱のテンプレートがコンパイルエラーになる問題を解消し、すべての同梱.erbテンプレートが Herb の厳密検証を通るようにした PR です。ついでに 2013 年から潜在していたレスキュー画面のテキストレスポンス内の余計な文字と、<option>タグ内の余計なスペースも解消されています。
- 変更内容の詳細
2-1. routing_error.text.erb の不正な閉じタグ削除
対象ファイル:actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb
問題点:
- テキストレスキュー用テンプレート内に、対応する
<code>開始タグのない「余計な</code>」がそのまま生テキストとして残っていた。 - HTML としても不正な構造であり、Herb の HTML-aware パーサが「開きタグなしの閉じタグ」として検出しコンパイルエラーを出す。
対応:
- 該当の
</code>を削除。 - 実行時の差分としては、XHR/Text で返している routing error レスポンス内から、今までこっそり出力されていた
</code>という文字列が消えるだけ。
2-2. mailer preview テンプレート内 <option> の ERB 記法修正
対象ファイル:railties/lib/rails/templates/rails/mailers/email.html.erb
問題点:
<option>タグのselected属性を、属性名スロットに ERB 出力を書き込む形で実装していた。
典型的な(問題のある)例は以下のイメージです:
<option <%= condition ? 'selected' : '' %> value="foo">
Foo
</option>HTML 的に見ると、<option value="foo"> のように、
- 「空文字が出力されることで二重スペースが入る」
- Herb 的には「属性名の位置に動的な ERB 出力がある」ため検証が難しく、セキュリティ/妥当性チェックに引っかかる
という問題があります。
対応:
- 制御構文 ERB (
<% %>) による条件分岐に変更し、属性名とテキストは純粋に静的な HTML として出すように変更。
イメージ:
<option<% if condition %> selected<% end %> value="foo">
Foo
</option>ポイント:
selected属性自体は静的文字列としてレンダリングされる(ERB 出力で属性名を書かない)。conditionが false の場合は<option value="foo">となり、
以前のような<option value="foo">(二重スペース)が解消される。selectedが true の場合の出力(例:<option selected value="foo">)は従来とバイト列レベルで同等。
2-3. mailer preview のテスト更新
対象ファイル:railties/test/application/mailer_previews_test.rb
問題点:
- 既存テストが、「二重スペースを含む
<option value="...">」文字列を期待していた。- これは、前述の
<%= '' %>による空文字 + スペースが原因。
- これは、前述の
対応:
- 期待値を以下のように変更:
'<option value="..."'→'<option value="..."'(二重スペース → 単一スペース)
'<option selected value="..."'を期待している箇所は出力が変わらないためそのまま。
2-4. Guides レイアウトのバージョンスイッチャー修正
対象ファイル:guides/source/layout.html.erb
問題点:
- Guides のバージョン切替用
<select>にある<option>で、selectedの付与に<%= " selected" if cond %>という 属性位置での ERB 出力を使っていた。
例(問題のあるパターン):
<option value="<%= version %>"<%= " selected" if current_version?(version) %>>
<%= version %>
</option>これは Herb から見ると、「属性リストの中に動的な文字列が挿入されている」構造で、セキュリティ/妥当性チェックが難しい。
対応:
- メーラーテンプレート同様、制御構文に変換。
例(イメージ):
<option value="<%= version %>"<% if current_version?(version) %> selected<% end %>>
<%= version %>
</option>- true/false の両パターンで、レンダリング結果は従来とバイトレベルで同一となるように調整されている。
- 影響範囲・注意点
Herb 利用アプリのブート障害解消
- Herb 0.10+ を ERB エンジンとして使っているアプリで、Rails の同梱テンプレート読込時に発生していた
Herb::Engine::InvalidNestingErrorHerb::Engine::SecurityError
などのコンパイルエラーが解消されます。
herb compile(0.10.1)で全 66 ファイルの.erbテンプレートを検証した結果:- 変更前: 3 件失敗(1 MissingOpeningTag, 2 SecurityError)
- 変更後: 0 件
- Herb 0.10+ を ERB エンジンとして使っているアプリで、Rails の同梱テンプレート読込時に発生していた
実行時の見える差分
- routing error の text レスポンス:
- 余計な文字列
</code>が出力されなくなる。
- 余計な文字列
- メーラープレビュー/Guides の
<option>:- 「未選択の
<option>に入っていた二重スペース」が単一スペースになる。 - それ以外の HTML 構造・属性順序は、selected/unselected ともに既存動作と同等。
- 「未選択の
- routing error の text レスポンス:
Rails 8.1.x 系への影響
- 本 PR は main への修正で、Herb 利用者が Rails 8.1.x でブートできない問題への根本対処として、Rails 側の不正マークアップを正しています。
rescues/routing_error.html.erb側の修正は既に別コミット(84023dfd1f)で main に入っており、本 PR は残りのテンプレートをすべてクリーンにする「締め」の位置づけです。- 8.1-stable へは、この PR と
84023dfd1fのバックポートが Herb 利用者にとって重要になります。
互換性上のリスク
- HTML 出力の差分はごく小さく、仕様的にはより正しい HTML に近づいているため、互換性リスクは低いと考えられます。
- ただし「生 HTML を文字列で比較している極端に厳しいテスト・スクレイパ」が
option内の空白数まで固定で見ている場合には、テストの更新が必要になる可能性があります(今回の mailer preview テスト更新と同様)。
- 参考情報 (あれば)
Herb 側の関連 Issue:
https://github.com/marcoroth/herb/issues/1744
→ Herb に「パスごとのバリデータスコープ」を導入する downstream 対策も議論されているが、この PR では Rails 側のマークアップを正して根本的にエラーを消しています。既存の関連コミット:
84023dfd1f—rescues/routing_error.html.erbで<p><h2/><ol/></p>という不正な入れ子を修正したコミット(今回と同じ文脈での修正)。
#57450 Deep-freeze NumberConverter::DEFAULTS
マージ日: 2026/5/23 | 作成者: @paracycle
- 概要 (1-2文で)
ActiveSupport::NumberHelper::NumberConverter::DEFAULTS定数を「深い凍結(deep-freeze)」することで、Ractor で共有可能な完全イミュータブルなオブジェクトにした PR です。外側の Hash だけでなく、ネストされた Hash もすべて freeze されています。
- 変更内容の詳細
何をしているか
- 対象:
activesupport/lib/active_support/number_helper/number_converter.rb内のNumberConverter::DEFAULTS - 外側の
Hashは既に.freeze済みだったが、値として入っているネストされたHashがミュータブルのままだった。 - そのため、
DEFAULTSは「見かけ上は」定数だが、内部は変更可能であり、Ractor 間での共有ができない状態だった。 - 各ネストした Hash リテラルに
.freezeを追加し、DEFAULTS配下のオブジェクトツリー全体を凍結した。
イメージとしては、もともと以下のような構造だったものが:
DEFAULTS = {
format: {
delimiter: ",",
separator: ".",
},
currency: {
unit: "$",
format: {
# ...
},
},
}.freezeこの PR により、ネストした Hash もすべて freeze されるように変更されています:
DEFAULTS = {
format: {
delimiter: ",",
separator: ".",
}.freeze,
currency: {
unit: "$",
format: {
# ...
}.freeze,
}.freeze,
}.freeze(具体的なキー名・構造は例示ですが、実際にはこのような形で各レベルの Hash に .freeze が付いている変更です)
背景
- 目的は「Ractor 対応の一環として、Rails 内部で“不変であるべき定数”を実際に不変(deep-frozen)にする」こと。
- 既に同様の対応を行った PR #57323 の続きで、ActiveSupport のナンバーヘルパー部分に対象を広げたものです。
- RuboCop の
Style/MutableConstantCop によって外側の.freezeは付いていたが、それでは「浅い freeze」にとどまり、内部要素は依然として書き換え可能だったため、Ractor が要求する「shareable?」なオブジェクト要件を満たしていませんでした。
- 影響範囲・注意点
影響範囲
ActiveSupport::NumberHelper::NumberConverterを通じて数値フォーマット・通貨フォーマットなどを行っているコード全般に影響しますが、「正しく使っている限り」挙動は変わりません。- 影響が出るのは、
NumberConverter::DEFAULTSの内部 Hash を直接書き換えていたコード です。
たとえば、以下のようなコードは、この PR 以降は FrozenError を起こします:
# 以前はできてしまっていた(推奨されない)書き換え
ActiveSupport::NumberHelper::NumberConverter::DEFAULTS[:format][:delimiter] = '.'
# => この PR 以降は FrozenError注意点 / マイグレーション
- もしアプリやライブラリで上記のように DEFAULTS を直接変更して独自設定を注入していた場合:
- 今後は
DEFAULTSをベースに dup / deep_dup してから変更 するか、 - Rails が提供している公式な設定インターフェース(ヘルパーのオプション指定や I18n 経由の設定など)を利用する必要があります。
- 今後は
例:
# 悪い例(今後 FrozenError)
defaults = ActiveSupport::NumberHelper::NumberConverter::DEFAULTS
defaults[:format][:delimiter] = '.'
# よい例(自前でコピーしてから変更)
defaults = Marshal.load(Marshal.dump(
ActiveSupport::NumberHelper::NumberConverter::DEFAULTS
))
defaults[:format][:delimiter] = '.'あるいは、単にヘルパーの呼び出し時にオプションを渡す方法が推奨されます:
number_to_delimited(1000, delimiter: '.')- 参考情報 (あれば)
- 関連 PR: #57323 – Rails を Ractor で使うための定数 freeze 対応の一部。
- Ruby Ractor と shareable オブジェクト: Ractor 間でオブジェクトを共有するには、そのオブジェクトが凍結され、内部もミュータブルでない必要がある(deep-freeze が必要)ため、本 PR のような対応が行われています。
#48549 Qualify association unscope values with table names
マージ日: 2026/5/23 | 作成者: @philip-maina
- 概要 (1-2文で)
Rails のunscopeが関連(association)経由のクエリで使われたときに、どのテーブルのカラムを外すのかが曖昧になりうる問題を、テーブル名付きで条件を解釈するように修正した PR です。これにより、特にhas_many :throughなど複数テーブルが絡むクエリでのunscopeの挙動が、より正確かつ予測可能になります。
- 変更内容の詳細
※実際のコード断片は概念レベルの説明です(PR 本文には詳細がないため、Rails の既存実装からの推測を含みます)。
背景となる問題
ActiveRecord::Relation#unscope は、スコープやチェインされたクエリから特定の条件(where, order, select など)を取り除くための API です。
Post.where(published: true).order(:created_at).unscope(:order)
# => ORDER BY が取り除かれるところが、関連を辿ったクエリ(特に has_many :through や includes / joins で複数テーブルが絡む場合)で unscope を使うと、以下のような曖昧さが出ます。
postsとcommentsが両方id,created_atといった共通カラムを持っているunscope(where: :id)のようにカラム名だけを指定すると、「どのテーブルのidを外すのか」がはっきりしない- 内部的に生成される
AssociationRelation/ association scope 側でunscopeの指定が正しくテーブルに紐づかないと、- 意図と違うテーブルの条件が外れる
- あるいは何も外れない
といったバグが起きる
Issue #48548 で報告されているのは、この種の「関連経由クエリ + unscope + カラム名の曖昧さ」による誤動作です。
変更点の概要
この PR の要点は以下です。
unscopeが受け取る「どの値(カラム・条件)を外すか」の指定を、テーブル名付きで解釈・適用するようにした- Association 経由 (
has_many,has_many :throughなど) のクエリにunscopeが適用される際に、関連先のテーブルに正しくスコープされるように修正した - これに合わせて
activerecord/lib/active_record/relation/query_methods.rbにunscope用のロジックを追加(+24)activerecord/lib/active_record/associations/association_scope.rbの関連スコープ構築時の挙動を 1 行修正
- 挙動を保証するためのテストをいくつか追加
has_many_through_associations_test.rbに、has_many :through+unscopeのテストを追加(+22)relation/select_test.rbでselectとunscopeの組み合わせに関するテストの修正(+1/-1)author.rb,comment.rb,post.rbのテスト用モデルにフィールドや関連定義を追加schema.rbにそれに対応するカラムを追加
イメージしやすい挙動例
(実テストコードとは異なる可能性がありますが、変更内容のイメージ用です)
class Author < ApplicationRecord
has_many :posts
has_many :comments, through: :posts
end
class Post < ApplicationRecord
belongs_to :author
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
# 例: Author#comments 経由の has_many :through
author = Author.first
# 関連にデフォルトスコープや where 条件が含まれているとして
relation = author.comments.where(approved: true).where(post_id: 1)
# ここで unscope を使う
relation = relation.unscope(:where) # すべての where を外す
relation = relation.unscope(where: :approved) # approved に対する where だけを外すこの PR 以前は、内部的に生成される SQL / relation ツリーの中で、approved や post_id がどのテーブルに属すのかが曖昧なケースがあり、unscope(where: :approved) が comments ではなく posts 側の同名カラムにマッチしようとしたり、逆に何も外せなかったりするケースがありました。
この PR では、association scope 構築時に「どのテーブルのスコープか」を明確に持たせ、unscope に渡された値を内部的に「テーブル名付きのカラム」として扱うことで、期待どおりのテーブルから条件を外すようにしています。
- 影響範囲・注意点
- 影響を受けるのは主に以下のケースです
- 関連経由(
has_many,has_many :through,belongs_to …)で構築されたRelationに対して unscopeを呼び出し- かつ複数テーブルにまたがる同名カラムや条件が存在する場合
- 関連経由(
- 期待される変化
- これまで「動くには動いていたが、どの条件が外れているか分かりづらかった」「バグっぽい挙動だった」
unscopeが、テーブル名レベルで正確に動くようになります - Issue #48548 で報告されているような誤動作は解消されます
- これまで「動くには動いていたが、どの条件が外れているか分かりづらかった」「バグっぽい挙動だった」
- 互換性・注意点
- SQL レベルで生成されるクエリが多少変わる可能性があります(対象テーブルが明確になるため)
- もし既存コードが「たまたま」曖昧な挙動に依存していた場合(例: 本来外してほしくない方が外れていた/その逆)、この PR 適用後は挙動が変わる可能性があります
- 複数テーブルにまたがる複雑なクエリで
unscope(where: :column_name)のような指定をしている場合は、テストを一度走らせて SQL の差分を確認しておくと安全です
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/48549
- 関連 Issue: https://github.com/rails/rails/issues/48548
- 関連コード周辺(参考用)
ActiveRecord::QueryMethods#unscopeActiveRecord::Associations::AssociationScopehas_many :throughなど association まわりのスコープ構築処理
#57434 Run LoadAsync*ThreadPoolExecutorTest on the replaced pool to prevent connection leak
マージ日: 2026/5/22 | 作成者: @yahonda
- 概要 (1-2文で)
LoadAsyncMultiThreadPoolExecutorTestとLoadAsyncMixedThreadPoolExecutorTestで、接続プールの差し替えタイミングを変更し、テスト実行時に PostgreSQL コネクションがリークしてtoo many clientsになる問題を防ぐ修正です。テスト専用の変更であり、本番コードには影響しません。
- 変更内容の詳細
問題の原因
対象テストでは、「非同期ロードをマルチスレッド用プールで動かす」ことを検証するために、ActiveRecord::Base / ARUnit2Model の接続プールを :multi_thread_pool に差し替えています。
以前は以下のような流れになっていました:
- Rails のテストフレームワークが Transactional fixtures により
- テスト開始時 (
setupより前) にarunit/arunit2の「元の接続プール」でトランザクションを開始する。
- テスト開始時 (
- 各テストの
setup内でestablish_connectionを呼び出し、ActiveRecord::Base/ARUnit2Modelの接続プールを:multi_thread_poolに差し替える。
- 元の接続プール上には、すでに fixtures 用トランザクションを張ったままのコネクションが 1 本いる。
- テスト終了時にプールを切り替え直す/切り離す (
disconnect!) 処理を行うが、ConnectionPool#disconnect!が「そのトランザクション保持中のコネクションを排他的に取る」ことができずタイムアウトする。- この結果、PG のソケットが閉じられずにリークし、テストスイート全体としては
FATAL: sorry, too many clients alreadyを引き起こす。
以前は setup の ensure で接続をすぐ戻していたため、そもそも :multi_thread_pool が十分に行使されておらず、リークも目立っていませんでした。ensure 削除によってテスト自体は正しくなった一方で、上記のリークが表面化しています。
今回の修正方針
トランザクション開始より前に接続プールを差し替え、
トランザクション終了後に元に戻すよう、フックポイントを調整しています。
- これまで:
- プール差し替え:
setup - プール復元:
teardown相当 (以前はensureで即時復元、その後の変更でそこがなくなった)
- プール差し替え:
- これから:
- プール差し替え:
before_setupで行い、その後にsuperを呼ぶ
→ Transactional fixtures が開始する時点で、すでに:multi_thread_pool側のプールが使われる - プール復元:
after_teardownで行い、その前にsuperを呼ぶ
→ テストと fixtures トランザクションが完全に終わってから、元のプールに戻す
- プール差し替え:
これにより、
- フィクスチャのトランザクションは「差し替え後のプール」で開始される。
- テスト終了時には、トランザクションも含めてすべてが差し替え後プール上で完結しているため、
disconnect!がブロックされる状況が発生しない。 - 結果として PG ソケットのリークが解消される。
コードイメージ(擬似的な例)
実際のパッチは 10 行追加・4 行削除程度ですが、イメージとしては以下のような構造になります:
class LoadAsyncMultiThreadPoolExecutorTest < ActiveRecord::TestCase
def before_setup
# ここで既存プールの保存 & :multi_thread_pool への差し替え
swap_to_multi_thread_pool
super # ← ここで transactional fixtures などが実行される
end
def after_teardown
super # ← すべてのテスト処理・トランザクション終了後
restore_original_pool
end
# テスト本体は従来どおり
endポイントは、
before_setupで 先に プールを差し替え、その 後にsuperを呼ぶことafter_teardownで 先にsuperでテスト/トランザクションを完全に終わらせ、その 後に プールを元に戻すこと
という「フックの順序」を明確に変えている点です。
- 影響範囲・注意点
- 影響範囲:
- 対象は
activerecordのテストコード (特にload_async_test.rb内のLoadAsyncMultiThreadPoolExecutorTest/LoadAsyncMixedThreadPoolExecutorTest) のみです。 - ランタイムの Active Record や接続プール実装には一切変更はなく、Rails アプリケーションの本番挙動には影響しません。
- 対象は
- 効果:
- PostgreSQL のコネクションリークが、計測上ほぼ「フィクスチャが最低限使う 2 コネクション」にまで削減されています。
- これにより、CI の
activerecord postgresqlジョブで発生していたtoo many clientsに起因する失敗が軽減/解消されると期待できます。
- 注意点:
- この PR は「テストフレームワークと connection pool 置き換えを併用する場合の落とし穴」の具体例であり、同様に
establish_connectionをテスト中で動的に切り替えている他のテストがないか注意するとよいです。 - 特に、Transactional fixtures 利用時は「トランザクション開始前に接続プールをセットアップする」ことが安全なパターンとして再確認できます。
- この PR は「テストフレームワークと connection pool 置き換えを併用する場合の落とし穴」の具体例であり、同様に
- 参考情報 (あれば)
- この PR の経緯・計測方法:
- 以前の試行: #57412
- 正しいリーク計測手順:
yahonda/pg-leak-repro
- テスト実行条件 (リーク確認に使用したもの):
- Docker:
postgres:alpine(PostgreSQL 18) - 設定:
max_connections=100 - 対象テスト:
-n /LoadAsync(Multi|Mixed)ThreadPoolExecutorTest/ - シード:
--seed=15223(ベースラインで最悪ケース) - N=5 回実行、毎回コンテナ再起動
- Docker:
- 計測結果:
- 修正前ピーク接続数:
25, 23, 23, 26, 28 - 修正後ピーク接続数:
2, 2, 2, 2, 2(fixtures 基本分のみ)
- 修正前ピーク接続数:
#57432 Rename reset_connection to reset_pool; have reset_connection reset just the existing connection
マージ日: 2026/5/22 | 作成者: @yahonda
- 概要 (1-2文で)
このPRは、テストで使われているreset_connectionヘルパーの意味と実装を整理し、コネクションプールごと作り直す挙動を新しいreset_poolに切り出すことで、PostgreSQL テスト群で発生していたコネクションリーク(too many clients)を解消するものです。既存のテストはメソッド名そのままで、より安全な「既存コネクションのリセット」だけを行うようになります。
- 変更内容の詳細
背景・問題点
PostgresqlRangeTest,PostgresqlEnumTest,PostgresqlCompositeTest,PostgresqlDomainTestなどが teardown でreset_connectionを呼んでいた。旧
reset_connectionの実装は実質的に:ruby# 旧: reset_connection のイメージ def reset_connection ActiveRecord::Base.remove_connection ActiveRecord::Base.establish_connection endのように「接続プールを破棄して作り直す(プールの差し替え)」動作をしていた。
しかし Rails のテストは「トランザクション・フィクスチャ」を使っており、テスト用トランザクションがすでにあるコネクションを掴みっぱなしにしている。
その状態でプールを差し替えようとすると
ConnectionPool#disconnect!がタイムアウトしてしまい、古い PG ソケットが閉じられずリークする。その結果、CI でFATAL: sorry, too many clients alreadyが発生。
新しい方針
テストの目的は「キャッシュされたクエリプランやセッション状態を捨てたい」のであって、「プールを完全に作り直したい」わけではないテストがほとんど。
そこで今回のPRでは、ヘルパーを次のように整理しています。
reset_connectionの意味を変更- 「プールを作り直す」のではなく、「今使っているコネクションの状態だけリセットする」ヘルパーに変更。
- 実装としては、アダプタの
#reset!を呼び出す形になる(PostgreSQL アダプタではDISCARD ALLを発行してセッション状態をリセットする)。
イメージ:
ruby# 新: reset_connection def reset_connection @connection&.reset! end※実際には
@connectionの取得方法などはconnection_helper.rb内で適切に行われている。旧来の「プール差し替え」挙動を
reset_poolにリネーム- これまでの
reset_connection(= remove + establish)の実装はreset_poolという別メソッド名に切り出される。 - 「アダプタそのものを入れ替えたい」テストだけが
reset_poolを明示的に呼ぶように修正される。
たとえば:
ruby# 新: 接続プール毎リセットしたいとき reset_pool- これまでの
実際の変更箇所のポイント
変更は次のファイルに限定されています。
activerecord/test/support/connection_helper.rbreset_connectionの実装を「現在の接続をreset!する」に変更。- 旧来の「プール差し替え」ロジックを
reset_poolとして定義。
activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rbactiverecord/test/cases/adapters/postgresql/referential_integrity_test.rbactiverecord/test/cases/adapters/abstract_mysql_adapter/active_schema_test.rbactiverecord/test/cases/connection_adapters/mysql_type_lookup_test.rb- これらのテストのうち、本当に「新しいアダプタインスタンス/プールを要求する」ケースだけが
reset_connectionからreset_poolに呼び出しを変更。 - 特に
PostgreSQLReferentialIntegrityTestはアダプタにモジュールをextendして挙動を変えるため、「その変更を取り除いた新しいアダプタ」が必要 →reset_poolを使うべき対象。
- これらのテストのうち、本当に「新しいアダプタインスタンス/プールを要求する」ケースだけが
一方で、リークの原因だった4テスト (PostgresqlRangeTest, PostgresqlEnumTest, PostgresqlCompositeTest, PostgresqlDomainTest) は PR の diff には出てきません。
これらはメソッド名としては引き続き reset_connection を呼び続けますが、中身の実装が変わったことで、コネクションプールを交換せず、既存コネクションのリセットだけを行うようになり、リークが解消されるという構造になっています。
効果測定
テスト対象: 4つのリーク元テストファイルのみ
環境:
postgres:alpine(PostgreSQL 18・max_connections=100)--seed=22021(リーク量が最大になったシード)- N=5 回実行(毎回コンテナを再起動して計測)
結果(ピーク接続数っぽいメトリクス):
状態 peak (5 runs) BEFORE 9, 12, 7, 7, 8 AFTER 2, 2, 2, 2, 2 (= フィクスチャのベースライン)
→ テストが max_connections に近づくことなく、リークが抑制されている。
- 影響範囲・注意点
影響範囲は基本的に「テストコードのみ」
- 変更は
activerecord/test/...とactiverecord/test/support/...に限られており、ランタイムの ActiveRecord コードや本番アプリケーションの挙動には影響しません。 - PR 作者も「CHANGELOG は不要、test-only fix」と明記。
- 変更は
reset_connectionの意味が変わる点に注意- これまで「プールごと作り直す」意味合いで
reset_connectionを使っていた独自テストヘルパーが Rails 本体にあった場合は、このPRを取り込むと挙動が変わります。 - Rails 本体側では、プールを再構築したい箇所はすべて
reset_poolに移行しているので、同様のパターンを自分のプロジェクトで真似しているなら、呼び出し先の名前・意味が変わっていないか要確認です。
- これまで「プールごと作り直す」意味合いで
トランザクションフィクスチャ + コネクションプール操作の組み合わせは要注意
- このPRが示している通り、トランザクションフィクスチャがアクティブな状態で
remove_connectionなどを行うと、コネクションがプールからうまく返却されずにソケットリークを引き起こす可能性があります。 - テストで「セッション状態だけリセットしたい」場合は、プールを再構築するよりもアダプタの
#reset!を呼ぶアプローチの方が安全です。
- このPRが示している通り、トランザクションフィクスチャがアクティブな状態で
- 参考情報 (あれば)
- PR本体: https://github.com/rails/rails/pull/57432
- 関連の試行 PR(メトリクスの取り方に問題があったもの): https://github.com/rails/rails/pull/57409
- 計測用リポジトリ・手法:
yahonda/pg-leak-repro
https://github.com/yahonda/rails/tree/yahonda/pg-leak-repro
このPRは「reset_connection = 既存接続の DISCARD ALL」「reset_pool = remove + establish でプール再構築」という明確な役割分担を導入し、テストでのコネクション管理をより安全にした修正と捉えると分かりやすいです。
#57446 Include valid values in ActiveRecord::Enum invalid-value error
マージ日: 2026/5/22 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveRecord の enum に無効な値を代入したときに発生するArgumentErrorのメッセージに、「どの値が有効か」を列挙して表示するようにした変更です。例外クラスや発生タイミングは変わらず、エラーメッセージの内容だけが改善されています。
- 変更内容の詳細
これまでの挙動
class Book < ApplicationRecord
enum status: {
proposed: 0,
written: 1,
published: 2,
}
end
Book.new(status: "bogus")
# ArgumentError: 'bogus' is not a valid status無効な値(enum に定義されていない値)を渡すと ArgumentError になりますが、メッセージには「何が有効なのか」が含まれていませんでした。
変更後の挙動
この PR では、同じ ArgumentError に、有効な値のリストを含めるようにしています:
Book.new(status: "bogus")
# ArgumentError: 'bogus' is not a valid status. Valid values are: "proposed", "written", "published"- 例外クラス: 変更なし (
ArgumentError) - 例外の発生条件: 変更なし(enum に未定義の値を渡したとき)
- メッセージ内容:
- 先頭の
" 'bogus' is not a valid status"部分は据え置き - 後ろに
Valid values are: ...と有効な enum のキー一覧を追記
- 先頭の
実装のポイント
- 変更は
activerecord/lib/active_record/enum.rbの 1 行のみ。 - enum の定義に使われたキー(シンボル/文字列)の「キー側」(例えば
{ proposed: 0 }の:proposed)をそのまま列挙。 - 出力には
Symbol#inspectを使っているため、以下のように常に引用符付きで表現され、曖昧さを避けています::proposed→"proposed""proposed"→"proposed"
いずれの場合も"proposed"として表示されるので、開発者にとって読みやすくなっています。
テストの変更
activerecord/test/cases/enum_test.rb にある、従来のエラーメッセージ文字列を厳密に比較していたテストを修正しています。
- 以前: 全メッセージ文字列を
assert_equal的に厳密比較していた。 - 変更後:
- 先頭部分(従来からある
"is not a valid"まで)をassert_matchで確認。 - さらに
"proposed"など既知の有効値が含まれているかをassert_matchで確認。
→ 将来的に enum のマッピング順序が変わっても、テストが壊れにくい構成になっています。
- 先頭部分(従来からある
CHANGELOG
activerecord/CHANGELOG.mdに今回の挙動変更を追記。- 変更前後の例(エラーメッセージの Before / After)が記載されており、利用者が挙動変更をすぐに把握できるようになっています。
- 影響範囲・注意点
影響範囲:
- ActiveRecord の
enumを利用しているすべてのアプリケーションで、「無効な enum 値を代入したときのエラーメッセージ」が変わります。 - 実行時の挙動としては「メッセージ文字列だけ」が変わるため、アプリケーションロジックへの直接的な影響はほぼありません。
- ActiveRecord の
注意点:
- 例外メッセージの文字列をテストで厳密に比較している場合(
assert_equal/==などで全文比較している場合)、今回の変更でテストが失敗する可能性があります。その場合はassert_matchやinclude?などに修正した方がよいです。 - エラーメッセージをパースして何らかの処理をしている(ログ収集の正規表現マッチなど)場合は、新しい末尾部分(
Valid values are: ...)が追加されることを考慮する必要があります。
- 例外メッセージの文字列をテストで厳密に比較している場合(
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57446
- テストコマンド(PR での検証内容):
ARCONN=sqlite3_mem bundle exec ruby -Itest test/cases/enum_test.rbbundle exec rubocop activerecord/lib/active_record/enum.rb activerecord/test/cases/enum_test.rb
- この変更は軽量(+13/-2 行)かつ後方互換性が高い UX 改善であり、enum 利用時のデバッグ体験が向上します。
#57437 Match HTML attribute quotes to actual output in ActionView docs [ci skip]
マージ日: 2026/5/22 | 作成者: @55728
- 概要 (1-2文で)
ActionView のヘルパー (simple_format,debug,buttonなど) が実際に出力する HTML と、RDoc のサンプルコード内の HTML 属性のクォート表記(シングル/ダブル)が食い違っていたため、ドキュメント側を実際の出力(ダブルクォート)に揃えた PR です。コードの挙動は一切変えず、ドキュメントのみの修正です。
- 変更内容の詳細
背景
- Rails の ActionView ヘルパーは HTML 属性を ダブルクォート(
class="description")で出力する。 - 一方で、RDoc のサンプルには シングルクォート(
class='description')で書かれている例がいくつか存在しており、これをそのままテストの期待値などにコピペすると、実際のヘルパー出力と一致しない状況になっていた。- 例:
simple_format/button/debugのサンプル。
- 例:
具体的な修正箇所
対象は RDoc のコメント中のサンプルコードのみで、Ruby コード自体は変更されていません。
actionview/lib/action_view/helpers/text_helper.rbsimple_formatの例で以下のような不一致があったものを修正:修正前(ドキュメントの例):
ruby# => "<p class='description'>Look ma! A class!</p>"修正後:
ruby# => "<p class=\"description\">Look ma! A class!</p>"Ruby の文字列リテラル内に HTML を書いているため、内側のダブルクォートは
\"でエスケープされています。他のsimple_formatの例と同じスタイルに揃えています。
actionview/lib/action_view/helpers/debug_helper.rbdebugヘルパーの出力例を、実際の出力に合わせて修正:修正前(イメージ):
html<pre class='debug_dump'>修正後:
html<pre class="debug_dump">
actionview/lib/action_view/helpers/form_helper.rbbuttonヘルパーの 3 つの例を修正(いずれも素の HTML 例):修正前(例):
html<button name='button' type='submit'>Create article</button>修正後:
html<button name="button" type="submit">Create article</button>
あえて変更していない箇所
form_tag_helper.rb:177のselect_tagのドキュメントに出てくるselected='selected'は、その文字列をraw(...)で「そのまま流し込む」例であり、Rails が生成した属性ではないため、意図的にシングルクォートのまま残しています。
検証方法
ローカルで各ヘルパーを実行し、ダブルクォートで出力されることを確認:
rubysimple_format("Look ma! A class!", class: "description") # => "<p class=\"description\">Look ma! A class!</p>" debug(some_user) # => "<pre class=\"debug_dump\">…</pre>" button("Create article") # => "<button name=\"button\" type=\"submit\">Create article</button>"grep -rnE "# =>.*<[a-z]+ [a-z]+='[^']+'" actionview/lib actionpack/libで、シングルクォート属性を含む RDoc サンプルを洗い出し、本 PR で修正した箇所以外に不整合がないことを確認。残っているヒットは上記の意図的なselect_tagのケースのみ。
- 影響範囲・注意点
影響範囲
- 実行時の挙動には一切変更なし。
- 影響があるのは RDoc ベースのドキュメントおよびそれを参照している読者のみ。
注意点・開発者目線でのポイント
- これまでドキュメント例をそのままテストの期待値などに使っていた場合、今後生成されるドキュメントを見てコピペすると、実際のヘルパー出力と正しく一致するようになる。
- 既存のコードやテストで、ドキュメント例(シングルクォート)に合わせた期待値を書いている場合は、元々実際の出力とズレている可能性があるので、一度確認するとよい。
- HTML としてはシングル/ダブルどちらも構文的には正しいが、Rails のヘルパーはダブルクォートを一貫して使っているため、今後もドキュメントはダブルクォートを前提に読むとよい。
CI・互換性
[ci skip]が付いており、テストも不要なドキュメント専用変更。- 互換性やマイグレーションは不要。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57437
- 対象モジュール:
ActionView::Helpers::TextHelper#simple_formatActionView::Helpers::DebugHelper#debugActionView::Helpers::FormHelper#button
- Rails のヘルパーが生成する HTML の属性クォートは「ダブルクォート」がデファクトである点は、他のヘルパー出力や既存テストからも裏付けられている。
#57445 Fix incorrect SQL examples in ActiveRecord::QueryMethods docs
マージ日: 2026/5/22 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveRecord::QueryMethods のドキュメント中に記載されていた SQL のサンプルが、実際に Rails が生成する SQL と食い違っていた問題を修正した PRです。コードの挙動には一切影響せず、コメント(ドキュメント)のみの修正です。
- 変更内容の詳細
対象ファイル: activerecord/lib/active_record/relation/query_methods.rb
変更はいずれも「コメント中に記載された SQL 例」のみです。
(1) where.associated の JOIN 例で ON が抜けていた
元のドキュメント例では、JOIN 句に ON がなく、SQL として不正な形式になっていました。
修正前:
# # LEFT OUTER JOIN "authors" "authors"."id" = "posts"."author_id"修正後:
# # LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"Rails 実装が実際に生成する SQL には ON が入るため、それに合わせた形です。
(2) where.associated のテーブル名が単数形 "author" になっていた
同じく where.associated の例で、WHERE 句のテーブル名が実際とは異なっていました。
修正前:
# # WHERE "author"."id" IS NOT NULL問題点:
"author"と単数形になっており、他の例や実際の SQL と不整合- コメントのインデントも他行とずれている(
# # ...)
修正後:
# # WHERE "authors"."id" IS NOT NULL- テーブル名を
"authors"に変更 - インデントを他の行と揃え (
# # ...)、ブロック内の整形を統一
(3) regroup の MySQL サンプルで * をバッククォートで囲っていた
MySQL のバックスラッシュ付き識別子引用(`...`)はカラム名やテーブル名などの「識別子」に使うべきですが、* はワイルドカードであり識別子ではありません。
従来の例は次のようになっており、実際には SQL エラーになる形式でした。
修正前:
# # SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`title`, `posts`.`body`
# # SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`title`修正後:
# # SELECT `posts`.* FROM `posts` GROUP BY `posts`.`title`, `posts`.`body`
# # SELECT `posts`.* FROM `posts` GROUP BY `posts`.`title`この形式は、ファイル内の他の regroup / group の MySQL 例とも整合しており、かつ実際に有効な SQL です。
- 影響範囲・注意点
- 実行コードは一切変更されておらず、挙動・パフォーマンス・互換性への影響はゼロです。
- ドキュメントの SQL サンプルが正しくなったことで、
- ドキュメントの SQL をそのままコピー&ペーストして利用してもエラーになりにくくなります。
- Rails が実際に生成する SQL とドキュメントの表記が一致するため、デバッグ・学習時の混乱が減ります。
- CHANGELOG は Rails の慣例に従い、ドキュメントのみの修正のため更新されていません。
- CI も
[ci skip]付きコミットでスキップされており、テストランはありません (必要でもない変更内容です)。
- 参考情報 (あれば)
- 対象クラス:
ActiveRecord::Relation::QueryMethodswhere.associated/regroup等のクエリビルダ API のドキュメントコメント
- 本 PR:
rails/railsリポジトリの #57445- タイトル: “Fix incorrect SQL examples in ActiveRecord::QueryMethods docs”
- マージ日時: 2026-05-22T21:55:13Z
#57444 Freeze LookupContext default proc internals
マージ日: 2026/5/22 | 作成者: @paracycle
概要 (1-2文で)
ActionView のActionView::LookupContext::Details::LookupContext::Accessorsにおいて、テンプレート詳細情報のDEFAULT_PROCSの扱いを見直し、「実質的に不変」な内部状態を freeze できるようにした PR です。Ractor 対応の一環として、変更可能な定数をやめ、毎回凍結されたクラスインスタンス変数を差し替える方式にしています。変更内容の詳細
背景・目的
- Rails を Ractor 対応するためには、「本来不変であるべき定数」やグローバル状態を凍結する必要があります。
- しかし
LookupContext::Accessors::DEFAULT_PROCSは、実行時にregister_detailなどで更新され得るため、そのままfreezeすると壊れてしまいます。 - この PR では、「定数として公開されているオブジェクト」は凍結しつつ、「更新が必要なときには新しい凍結済みオブジェクトに差し替える」パターンに移行しています。
具体的な変更点
※ 実際の diff は 9 行追加 / 7 行削除のみの小改修ですが、概念的には以下のような変更です。
1. DEFAULT_PROCS 定数の実体を「クラスインスタンス変数」に移動
従来(イメージ):
module ActionView
class LookupContext
module Details
module Accessors
DEFAULT_PROCS = {
formats: -> { ... },
locale: -> { ... },
# ...
}
class << self
attr_reader :registered_details
# ...
end
end
end
end
endDEFAULT_PROCS は定数としつつも、内部的には DEFAULT_PROCS[:something] = proc のように変更されていたため、「不変」とは言えない状態でした。
今回(イメージ):
module ActionView
class LookupContext
module Details
module Accessors
class << self
# 凍結された Hash を保持するクラスインスタンス変数
def default_procs
@default_procs
end
def register_detail(name, &block)
# 既存の @default_procs から dup して書き換え、
# 最後に freeze した上で @default_procs に再代入
new_default_procs = @default_procs.dup
new_default_procs[name] = block
@default_procs = new_default_procs.freeze
# registered_details もここで更新 …
end
end
# 旧 DEFAULT_PROCS 定数は「初期値の参照」程度 or 廃止方向
end
end
end
endポイント:
DEFAULT_PROCSという「(見かけ上)不変であってほしい定数」は、実際には中身を書き換えない。- 更新が必要なときは、
- 現在の
@default_procsをdup - 新しいエントリを追加
freezeして@default_procsに置き換え
- 現在の
- これにより「常に凍結済みの Hash を参照している」状態が保たれます。
2. registered_details と DEFAULT_PROCS のキーを一本化
説明文にある通り:
we can use the keys of the same list as the
registered_detailslist, thus removing a class instance variable in the process.
- これまで:
registered_details用の配列/セットDEFAULT_PROCSのキー が「ほぼ同じ情報を別々に持っていた」状態。
- これを整理し、
default_procsのキー情報をそのままregistered_detailsとして利用- もしくはその逆、といった形で「二重管理」していたクラスインスタンス変数を削減。
これにより、状態管理がシンプルになり、Ractor 対応上の「共有状態」の表面積も減ります。
- 影響範囲・注意点
影響範囲
- 対象クラス/モジュール:
ActionView::LookupContext::Details::Accessorsおよびその周辺のみ。
- 主に影響を受けるのは以下のようなケースです:
- ActionView の
LookupContextをカスタマイズし、register_detail相当を直接呼び出しているコード DEFAULT_PROCS定数を直接読み書きしている独自拡張
- ActionView の
挙動上の変化
- 「標準的な使い方」(ビューの
formats,locale,variants等の通常の解決処理)における挙動は、原則として変わりません。 - ただし、
DEFAULT_PROCSの中身を書き換える(DEFAULT_PROCS[:formats] = -> { ... }など) といったハック的なコードを書いていた場合:- 今後そのような直接書き換えはできなくなる(もしくは非推奨)で、
register_detailなどの正式 API を通す必要が出てきます。
- マルチスレッド / Ractor 環境下では、
default_procsが常に凍結されているため、読み取り専用のデータ構造として安全に共有できることが期待されます。
パフォーマンス面
register_detail実行時にHash#dup+freezeを行うオーバーヘッドが増えますが、register_detail自体は通常アプリケーション起動時やフレームワーク初期化時にのみ呼ばれることがほとんどのため、実行時のリクエスト処理に対する影響は極小と考えられます。
- 参考情報 (あれば)
- この PR は以下の PR のフォローアップです:
- https://github.com/rails/rails/pull/57323
→ Rails の定数/グローバル状態を freeze して Ractor 対応を進める流れの一部。
- https://github.com/rails/rails/pull/57323
- Ractor 対応の文脈:
- Ruby 3 以降の Ractor では、「ミュータブルオブジェクト」を Ractor 間で共有できないため、「本来不変なはずの設定/定義オブジェクト」は積極的に
freezeする方向に設計が進んでいます。 - Rails では、ActionView を含む多くのコンポーネントで、こうした「不変オブジェクトの明確化 / 凍結」が進められています。
- Ruby 3 以降の Ractor では、「ミュータブルオブジェクト」を Ractor 間で共有できないため、「本来不変なはずの設定/定義オブジェクト」は積極的に
#57427 Fix #57401: Stop DRb service when shutting down parallel test workers
マージ日: 2026/5/22 | 作成者: @mugitti9
- 概要 (1-2文で)
Rails の並列テスト(プロセス並列)実行時に、親プロセス側の DRb サービスが終了せずテストプロセスが終了しないことがある不具合が修正されました。shutdown時にDRb.stop_serviceを呼ぶようにし、DRb の受け付けスレッドが残らないことをテストで保証しています。
- 変更内容の詳細(あればサンプルコードも含めて)
背景
ActiveSupport::Testing::Parallelizationは、プロセス並列実行 (parallelize/PARALLEL_WORKERS) のときに DRb を使って親子プロセス間のキューイングを行っています。- 親プロセスは
Parallelization#initialize内でDRb.start_serviceを呼んで DRb サービスを起動します。 - しかし、これまでの
#shutdown実装では- キューサーバのシャットダウン
- ワーカープロセスの
Process.waitpidによる回収 までは行うものの、親プロセス側でDRb.stop_serviceを呼んでいなかったため、DRb の accept-loop スレッドが生き残ってしまっていました。
- 環境によってはこのスレッドが原因でプロセスが終了せず、CI などで「テストは全部終わって
0 failuresが出ているのにプロセスが落ちずタイムアウトする」という症状が発生していました。 - 子プロセス(ワーカー)は fork 後に
DRb.stop_serviceを呼んでおり、親だけが後始末をしていない状態でした。
コード上の変更
親プロセスのシャットダウンで DRb を止める
ActiveSupport::Testing::Parallelization#shutdown の末尾に、DRb.stop_service 呼び出しが追加されています。
イメージとしては、次のような変更です(実際のコードは行位置や細部が多少異なる可能性がありますが、意図はこのとおりです):
def shutdown
# キューサーバ停止
@queue.close
# 子プロセスの wait
@workers.each_value do |pid|
Process.waitpid(pid)
end
# ★ 親プロセス側の DRb サービスを終了
DRb.stop_service
endこれにより、親プロセスで立ち上げた DRb の accept-loop スレッドが確実に終了するようになります。
回帰テストの追加/拡充
activesupport/test/parallelization_test.rb に、DRb のスレッドリークを検出するテストが追加されています(112 行追加・80 行削除と、テストコードは比較的大きく整理/追加されています)。
ポイント:
shutdown実行後に、Ruby VM 内のスレッド一覧を確認し、「DRb の accept スレッド」が残っていないことをアサートするテストが追加されています。- このテストは、修正ありの場合はパスし、
DRb.stop_serviceをコメントアウトすると DRb スレッドがリークして失敗することが手元で検証されています。
テスト内容のイメージ:
def test_shutdown_stops_the_drb_service
parallel = ActiveSupport::Testing::Parallelization.new(workers: 2)
# 何らかの形で parallel を動かして…
parallel.shutdown
# shutdown 後に DRb の accept-loop スレッドが残っていないことを確認
drb_threads = Thread.list.select { |t| t.name&.include?("drb") || t.backtrace&.any? { |l| l.include?("drb") } }
assert_empty drb_threads
end(実際の実装は名前や backtrace などでもう少し堅牢に判定している可能性がありますが、狙いは「DRb の受け付けスレッドが残っていないことを検証する」点です。)
- 影響範囲・注意点
- 対象となるのは、プロセスベースの並列テストを使っているケースです:
parallelize(workers: N)を使っている- もしくは環境変数
PARALLEL_WORKERSを設定している
- この変更によって、テスト完了後にプロセスが終了しない/CI がタイムアウトする、といった問題が 解消されることが期待されます。
- 動作の変更点は「テスト終了時に DRb を正しく止める」だけであり、通常のテスト実行フローには影響しません。
- 子プロセス側ではすでに
DRb.stop_serviceを呼んでおり、今回の変更は親プロセスの後始末を揃えるものなので、後方互換性への影響は極めて小さいです。 - もし DRb に依存した独自拡張をしており、親プロセスの DRb サービスをテスト後も使い続けるような特殊なコードがある場合は、その前提が崩れる可能性がありますが、Rails 標準の利用方法では問題にならないはずです。
- 参考情報 (あれば)
- 対応 Issue: #57401
「プロセス並列テストで DRb スレッドが残りプロセスが終了しない」系の報告がここで議論されていたと考えられます。 - 対応 PR: #57427
タイトル: “Fix #57401: Stop DRb service when shutting down parallel test workers” - 変更ファイル:
activesupport/lib/active_support/testing/parallelization.rb
→ シャットダウン処理にDRb.stop_serviceを追加activesupport/test/parallelization_test.rb
→ DRb スレッドリークを検証する回帰テストを追加・整理
#57447 Improve insert_all error message to show key mismatch
マージ日: 2026/5/22 | 作成者: @hammadxcm
- 概要 (1-2文で)
insert_all/upsert_allで、挿入するレコード同士のキーが揃っていない場合に発生するArgumentErrorのメッセージが改善され、どのキーが「余分」または「不足」しているかがエラー文言に含まれるようになりました。これにより、大量データやカラム数が多いテーブルでのデバッグがしやすくなっています。
- 変更内容の詳細
これまでの挙動
insert_all / upsert_all は、渡された配列の各ハッシュが同じキー集合を持っていないと ArgumentError を投げますが、そのメッセージは以下のように非常にざっくりしたものでした。
Book.insert_all [
{ name: "Rework", author_id: 1 },
{ name: "Remote", author_id: 1, isbn: "1974522598" }
]
# ArgumentError: All objects being inserted must have the same keysどのキーが問題なのか分からないため、広いテーブルや大量レコードのバッチ挿入では原因特定が大変でした。
変更後の挙動
エラーメッセージに「どのキーが余分か / 足りないか」が付加されます。
- あるレコードに「余分なキー」が含まれているケース:
Book.insert_all [
{ name: "Rework", author_id: 1 },
{ name: "Remote", author_id: 1, isbn: "1974522598" }
]
# ArgumentError: All objects being inserted must have the same keys (extra: ["isbn"])- 逆に、「他のレコードにはあるキーが欠けている」ケース:
Book.insert_all [
{ name: "Remote", author_id: 1, isbn: "1974522598" },
{ name: "Rework", author_id: 1 }
]
# ArgumentError: All objects being inserted must have the same keys (missing: ["isbn"])extra / missing それぞれについて、キーの配列がメッセージに含まれます。
実装のポイント
- 対象ファイル:
activerecord/lib/active_record/insert_all.rb verify_attributes内で、キー集合の差分(対称差分)をその場で計算し、エラー送出直前にメッセージへ追記しています。- ハッピーパス(キーが揃っている場合)では差分計算は行われないため、通常ケースのパフォーマンスへの影響は最小限です。
- 新たなヘルパーメソッド追加などは行わず、既存ロジックの近くに小さく閉じた変更として実装されています(KISS/YAGNI の方針)。
テスト
- 対象ファイル:
activerecord/test/cases/insert_all_test.rb - 以下 2 パターンのテストが追加されています。
extraキーがある場合のエラーメッセージ検証missingキーがある場合のエラーメッセージ検証
- 既存テストで古いメッセージ文字列を前提としたものは存在しないことを
grepで確認済み。
- 影響範囲・注意点
影響範囲
ActiveRecord::Persistence#insert_allおよび#upsert_allを利用しているコードで、挿入データのキー不一致によりArgumentErrorが発生する場合、そのメッセージ内容が変わります。- エラークラスや発生タイミングは変わらないため、
rescue ArgumentErrorなどでの制御フローには影響ありません。
注意点
- もしアプリ側で「エラーメッセージの文字列を厳密一致で判定」しているようなコード(例: ログフィルタや i18n を絡めた独自ラッパなど)がある場合は、メッセージ末尾に
(extra: [...])/(missing: [...])が付与されることで期待とズレる可能性があります。 - 実運用の観点では、エラー文言の部分一致やパターンマッチ(正規表現)などにしておく方が、今後の Rails 側の文言変更にも強くなります。
- もしアプリ側で「エラーメッセージの文字列を厳密一致で判定」しているようなコード(例: ログフィルタや i18n を絡めた独自ラッパなど)がある場合は、メッセージ末尾に
- 参考情報 (あれば)
- 変更ファイル:
activerecord/lib/active_record/insert_all.rbverify_attributes内のキー検証とエラーメッセージ組み立てロジックが変更。
activerecord/test/cases/insert_all_test.rbextra/missingパターン用のテストが追加。
activerecord/CHANGELOG.mdinsert_all/upsert_allのエラーメッセージ改善が追記。
この変更はあくまで「エラーメッセージ改善」に留まるため、既存コードの動作仕様(挿入時にキー集合が一致している必要がある点)は変わっていません。
#57448 Fix grammar in ActiveRecord errors and integration doc comments
マージ日: 2026/5/22 | 作成者: @hammadxcm
- 概要 (1-2文で)
ActiveRecord のエラーメッセージおよびto_paramのドキュメントコメントにおける英語の文法ミスを修正した PR です。コードの挙動やテストには一切影響せず、コメントのみの変更です。
- 変更内容の詳細
activerecord/lib/active_record/errors.rb
ActiveRecord::Errors 内の ValueTooLong 例外の説明文が文法的に不完全だったため修正されています。
Before:
# Raised when a record cannot be inserted or updated because a value too long for a column type.「because 以下」に動詞がなく、文として不自然でした。
After:
# Raised when a record cannot be inserted or updated because a value is too long for a column type.sibling entries と同様に、「because 以下」が完全な節(節内に主語+動詞)になるように修正されています。
このコメントは、例外クラスの RDoc/yard などの API ドキュメントに反映される部分です。
activerecord/lib/active_record/integration.rb
to_param のドキュメントコメントの文法が誤解を招く表現だったため修正されています。
Before:
# or +nil+ if this record's unsaved.record's が「所有格(record の unsaved という属性)」として読めてしまい、
意図している「record is unsaved(レコードが未保存である場合)」という意味が伝わりにくい表現でした。
After:
# or +nil+ if this record is unsaved.これにより、record is unsaved という状態を表す、明確な英文になっています。to_param の説明を読むユーザーにとって、「未保存レコードのときに nil を返しうる」という意味がより正確に伝わります。
- 影響範囲・注意点
実行時挙動への影響なし
変更はコメントのみであり、クラス定義やメソッド本体には一切手が入っていません。
そのため、アプリケーションコードの挙動・既存テスト・マイグレーション等に影響はありません。対象バージョン
この PR がマージされたブランチ(通常は main / master)以降の Rails で、
API ドキュメントやエディタのホバー表示などに反映されます。ドキュメント生成・参照時の改善
- RDoc / YARD / Rails API サイトなどで、より自然で誤解のない英語説明が表示されます。
- 英語コメントをリファレンスにしている開発者にとって、理解しやすくなります。
CI や CHANGELOG
- コメントのみの修正のため、Rails の慣例として CHANGELOG エントリは追加されていません。
- コミットメッセージに
[ci skip]が付いており、CI はスキップされています。
- 参考情報 (あれば)
該当ファイル:
activerecord/lib/active_record/errors.rbValueTooLong例外クラス(または関連定義)のコメント
activerecord/lib/active_record/integration.rbActiveRecord::Integration#to_paramのドキュメントコメント
仕様・挙動に関する追加の読みどころ:
ValueTooLongを含む ActiveRecord のデータベース例外クラス一覧to_paramのオーバーライド例(resourcesルーティングでの URL 生成をカスタマイズする場合など)
#57431 fix: validate subcommand in rails plugin command
マージ日: 2026/5/22 | 作成者: @ruyrocha
- 概要 (1-2文で)
rails pluginコマンドでnew以外のサブコマンドが指定された場合、これまでは無視されてrails plugin newとして実行されていた挙動を修正し、不正なサブコマンドであればエラーを表示して終了するようにした PR です。これにより、タイプミスや誤ったコマンド指定による意図しないプラグイン生成を防ぎます。
- 変更内容の詳細
これまでの挙動
# 本来は Rails プラグインのサブコマンドは "new" のみ
rails plugin foo bar本来であれば "foo" は不正なサブコマンドですが、従来は:
fooが黙って無視されるbarがプラグイン名として扱われる- 実質的に
rails plugin new barが実行される
という挙動になっていました。
つまり、サブコマンドをタイプミスしても検知されず、意図せずプラグインが作られてしまう可能性がありました。
修正後の挙動
PluginCommand#perform でサブコマンドの検証を行うように変更されています。
- サブコマンドが
new以外の場合:- エラーメッセージを表示
- プロセスをステータスコード 1 で終了
おおよそ以下のようなフローになっていると考えられます(擬似コード):
def perform(*args)
subcommand = args.shift
if subcommand != "new"
$stderr.puts "Unknown command #{subcommand}. Only `rails plugin new` is supported."
exit 1
end
# ここから先は従来通り `new` サブコマンドの処理
# ...
end※実際の文言は PR のコードに依存しますが、趣旨としては「new 以外はエラーにする」というものです。
テストの追加
railties/test/commands/plugin_test.rb にテストが追加されており、主に次を検証しています:
- 不正なサブコマンド (
fooなど) を指定した場合に:- 適切なエラーメッセージが出力されること
- プロセスがエラー終了すること
- 正常なサブコマンド (
new) については従来の挙動が保たれていること
CHANGELOG (railties/CHANGELOG.md) にも、この挙動変更がバグ修正として追記されています。
- 影響範囲・注意点
影響を受けるコマンド:
rails pluginサブコマンド全般(現時点では事実上rails plugin newのみが有効)
これまで「たまたま」動いていた以下のような書き方は、今後エラーになります:
bash# これまでは "foo" を無視して plugin 名 "bar" として扱われていた rails plugin foo bar # => 今後はエラーCI やスクリプトで
rails pluginを呼び出している場合:- サブコマンド指定に誤りがあると、以前は成功していた処理が失敗するようになります
- ただし、正しい形式
rails plugin new my_pluginで呼んでいれば影響はありません
新たに
rails pluginにサブコマンドを追加しようとする場合:PluginCommand#performでのバリデーションロジックを更新しないと、新サブコマンドはエラー扱いになります- 将来的にサブコマンドを増やしたい場合は、このバリデーションを拡張する必要があります
- 参考情報 (あれば)
- 対応 Issue: #57430
- 修正対象クラス:
railties/lib/rails/commands/plugin/plugin_command.rb - 変更の種類: バグ修正(挙動の明示的なエラー化・安全側への変更)
#56195 Allow create_join_table to accept a primary key
マージ日: 2026/5/22 | 作成者: @gsamokovarov
- 概要 (1-2文で)
create_join_tableが:primary_keyオプションを受け付けるようになり、従来は不可能だった結合テーブルへの主キー(特に複合主キー)の定義が可能になりました。デフォルトの挙動はこれまで通り主キー無し(id: false)のまま維持され、後方互換性も確保されています。
- 変更内容の詳細(あればサンプルコードも含めて)
これまでの問題点
create_join_table は内部的に create_table を呼び出す際に常に id: false を指定していました。
# イメージ(従来)
create_table(name, id: false, **options) do |t|
...
endこのため、マイグレーション側で primary_key: を指定しても、id: false が優先されて主キーが一切作られませんでした。
- Postgres の論理レプリケーションでは「全テーブルに主キーが必要」という制約があるため
- join テーブルにも主キーを付けたいユースケース(特に複合主キー)に対応できていませんでした
その結果、create_join_table が使えず、
- 代わりに
create_tableを直接使う - あるいはカスタムメソッド+手動で reversible なマイグレーションを書く
といった回避策が必要でした。
今回の変更点
id: falseをハードコードしないように変更内部で
create_tableを呼び出す際に、これまで固定で渡していたid: falseをやめ、呼び出し元からのオプションを尊重するようにしました。デフォルトは引き続き
id: false- 何も指定しない場合は、従来通り「id カラムなし・主キーなしの join テーブル」が作られます。
- つまり、既存アプリのマイグレーションは挙動が変わりません。
:primary_keyを指定すると:idが自動的に有効化されるオプションに
primary_key:が指定された場合、内部的にid:オプションが自動で設定され、主キーありのテーブル定義になります。想定される UX は以下のようなものです:
ruby# 新しい推奨の書き方(複合主キー) create_join_table :assemblies, :parts, primary_key: [:assembly_id, :part_id]これによって、基になる
create_table呼び出しでは:rubycreate_table :assemblies_parts, id: :primary_key, # 内部的に自動設定されるイメージ primary_key: [:assembly_id, :part_id] do |t| t.references :assembly t.references :part endのような扱いになり、複合主キーがちゃんと生成されるようになります。
作者のコメントにもある通り、従来のインタフェースとしては:
ruby# 冗長だが理屈としてはこう書ける形だった create_join_table :assemblies, :parts, id: :primary_key, primary_key: [:assembly_id, :part_id]という形もありえましたが、利用者にわかりやすくするために、
- 利用者は
primary_key:だけ指定すればよい id:はライブラリ側でよしなに設定する
というインタフェースに整理されています。
- 利用者は
テスト・ドキュメントの更新
activerecord/CHANGELOG.mdに、この変更がリリースノートとして追記されています。activerecord/test/cases/migration/create_join_table_test.rbに、primary_key:を使った join テーブル生成のテストが追加されています。- コード中の
build_create_join_table_definitionという:nodoc:なテスト専用メソッドに関して、「使われていなそうなので削除してよいか?」という問いかけが PR 説明に記載されています(この PR では削除されていません)。
- 影響範囲・注意点
後方互換性
create_join_tableのデフォルトは依然としてid: falseであり、既存のマイグレーションは動作・生成スキーマともに変わりません。- 新たに
primary_key:を指定した場合にのみ挙動が変わります。
Postgres 論理レプリケーションとの相性
- これにより、論理レプリケーションを利用するアプリケーションでも
create_join_tableを安全に利用しやすくなります。 - 特に、
has_and_belongs_to_many形式の join テーブルに複合主キーを付与したい場合に有用です。
- これにより、論理レプリケーションを利用するアプリケーションでも
設計上の注意点
- 複合主キーを採用すると、Active Record モデルの扱いが複雑になりがちです(Rails の標準 API は単一カラム主キーを前提にしている箇所が多いため)。
- join テーブルをモデルとして直接扱わず、あくまで中間テーブルに留める(
has_and_belongs_to_manyなど)ケースでは問題になりにくいですが、「中間モデルとして ActiveRecord モデルを定義する」場合は挙動をよく確認した方がよいです。 - 今回の機能は「スキーマ定義として複合主キーを張れるようにする」ことが主目的であり、Active Record レベルでの複合主キーの完全サポートを追加したわけではありません。
既存プロジェクトへの導入
- 既に
create_tableで join テーブルを作成している場合、同じスキーマをcreate_join_table + primary_key:で再現できるようになりますが、マイグレーションの差分・ロールバック可能性をよく確認のうえで移行する必要があります。(既存テーブルを削除・再作成するようなマイグレーションは本番環境では慎重に検討すべきです。)
- 既に
- 参考情報 (あれば)
追加された主な API イメージ:
ruby# 主キーなし(従来どおり) create_join_table :users, :roles # 単一主キーを指定したい場合(例: join テーブル用の surrogate key を持たせる) create_join_table :users, :roles, primary_key: :id # 複合主キー(今回の主なユースケース) create_join_table :assemblies, :parts, primary_key: [:assembly_id, :part_id]Postgres 論理レプリケーション要件(全テーブルに主キー必須)との整合性確保のための変更であることが、PR 本文で明示されています。
#56193 Updates diagrams for dark mode compatibility and add EPUB fallback for SVG images [Guides]
マージ日: 2026/5/22 | 作成者: @shivabhusal
- 概要 (1-2文で)
Rails Guides の関連ガイドに使われている図版を、ライト/ダークモードに自動対応する SVG に差し替えるとともに、EPUB 生成時に SVG を他形式へフォールバックする仕組みが追加されています。これにより、Web 版ではダークモードでも読みやすくなり、EPUB(Kindle/Apple Books など)では「画像が表示されない」問題が軽減されます。
- 変更内容の詳細
図版(アソシエーションの図)のアップデート
対象は association_basics ガイド内の以下の関連図です。
belongs_tohas_onehas_manyhas_many :throughhas_and_belongs_to_many(habtm)
それぞれについて:
- 新たにダークモード対応の SVG 版ファイルが追加
- 例:
guides/assets/images/association_basics/belongs_to.svg.../has_many.svgなど
- 例:
- 既存の PNG も
*-light.png/*-dark.pngといった形で整理されており、テーマに応じて適切に表示できるようになっています。
SVG 図版のポイント:
- ライト/ダークモードに自動で追従するようなデザイン・色指定になっている(CSS の
prefers-color-schemeなどを前提とした配色)。 - ベクタ画像なので、拡大しても劣化せず、ガイドの図がより鮮明に表示される。
※ PR 本文では「アニメーションやロスレス品質のために SVG を選択」と言及されていますが、実際にアニメーションしているかはコードを見ないとわかりません。少なくとも「将来的にアニメ可能で高品質」という意図を持った形式選定です。
EPUB 生成処理での SVG フォールバック
EPUB 向けには、以下の課題がありました。
- 多くの EPUB リーダー(特にネイティブアプリ)が SVG を完全にはサポートしていない
- Apple Books で Rails Guides の EPUB を開いたとき、一部の図版がレンダリング失敗する
- ダークモード対応も EPUB 側では問題になっていた
この PR では、rake guides:generate:kindle(EPUB 生成)実行時に:
epub.rb内で SVG ファイルを別形式に差し替える処理 を追加- SVG がそのまま EPUB に入らないようにし、PNG 等の互換性の高い形式に置き換えて埋め込む
- これにより、EPUB 出力で画像が欠落する/表示されない問題を改善
疑似的なイメージコード(実際のコードとは異なる可能性がありますが、やっていることは概ねこういう処理です):
# guides/epub.rb のどこかで
def image_path_for_epub(path)
if File.extname(path) == ".svg"
# 同名 PNG を探して差し替える、など
fallback = path.sub(/\.svg\z/, ".png")
return fallback if File.exist?(fallback)
end
path
end実際には、HTML 生成時に埋め込まれる <img src="..."> を走査し、拡張子 .svg のものをフォールバックパスに書き換える処理などが入っているはずです。
テスト手順(PR 記載内容)
- Web 版:
- PR 作成者の提供するプレビュー URL(例:
association_basics#belongs-to)にアクセス - OS やブラウザのテーマをライト/ダークで切り替え、図版が見やすく変化するかを確認
- PR 作成者の提供するプレビュー URL(例:
- EPUB 版:
- 該当ブランチ
auto-color-scheme-svgを checkout cd guides && rake guides:generate:kindleoutput/epub/ruby_on_rails_guides_[hash].epubを Apple Books などの非 Web ベースリーダーで開く- 公開中の公式 EPUB (
v8.1.1) と比較し、図版が欠けずに表示されているかを確認
- 該当ブランチ
- 影響範囲・注意点
対象範囲:
- Rails Guides のうち「Active Record Associations(association_basics)」に掲載されている図版全般
epub.rbを通じて生成される EPUB/Kindle 用ガイドすべて(他のガイドで SVG を使った場合もフォールバック処理の影響を受ける)
既存コンテンツとの互換性:
- Web 版:
- 既存の PNG 図版から SVG に差し替わるため、古いブラウザでの SVG サポートが極端に弱い環境では表示に問題が出る可能性がありますが、現代的なブラウザならほぼ問題ありません。
- EPUB 版:
- これまで「画像が表示されない」状況だった環境では改善が見込まれます。
- フォールバック用の画像ファイル(PNG 等)がきちんと存在することが前提になるため、将来別の SVG を追加する際は「EPUB 用の代替画像も用意されているか」「
epub.rbの変換ロジックで拾える配置・命名か」を注意する必要があります。
- Web 版:
ダークモード対応:
- SVG のスタイルが
prefers-color-schemeに依存している場合、ブラウザや EPUB リーダー側がこれをサポートしていないとライトモード前提の配色で見える可能性があります。 - EPUB 側はフォールバック画像がどちらのテーマ前提になっているか(ライト/ダークまたは中庸な配色か)が実際の表示結果に影響します。
- SVG のスタイルが
ビルド関連:
rake guides:generate:kindleのフローが変わるため、自前でガイドの EPUB を生成している人は、一度生成物を確認して差分・レイアウト崩れがないかを見ることを推奨します。
- 参考情報 (あれば)
- 該当 PR:
https://github.com/rails/rails/pull/56193 - 既存の公式 EPUB(比較用):
https://guides.rubyonrails.org/epub/ruby_on_rails_guides_v8.1.1.epub - Rails Guides の EPUB 生成タスク:
- コマンド:
cd guides && rake guides:generate:kindle - 生成物:
guides/output/epub/ruby_on_rails_guides_*.epub
- コマンド:
#57435 Add missing docs for Rails.autoloaders
マージ日: 2026/5/22 | 作成者: @fxn
- 概要 (1-2文で)
Rails 7系(?) で導入されているRails.autoloadersの公開インターフェイスについて、ガイドには書かれていたものの、APIドキュメント(RDoc/YARD)側に欠けていた説明を追加するPRです。コードの挙動には手を入れず、railties/lib/rails.rbにドキュメントコメントを追加するだけの変更です。
- 変更内容の詳細
- 対象ファイル:
railties/lib/rails.rb - 変更内容:
Rails.autoloaders関連の公開APIに対し、RDoc/YARD形式のドキュメントコメントを約 53 行分追加
主なポイント:
Rails.autoloadersメソッドおよびそこからアクセスできるインターフェイスについて、- どのようなオブジェクトを返すのか
- どのような用途で使うのか(例: main / once autoloader へのアクセス、設定の変更など)
- 既存のガイド(
autoloading_and_reloading_constants)に書かれている内容と整合を取った説明
を API ドキュメント形式で明示。
動作やシグネチャの変更はなく、「ガイドにしか書かれていなかった説明」を
Rails.autoloadersのメソッド定義の直上にコメントとして移植/再構成したイメージです。
サンプル的な利用イメージ(PRそのものには含まれていないが、今回ドキュメントで説明される想定の使い方):
# メインのオートローダー (再読み込みあり)
Rails.autoloaders.main
# 一度だけロードするオートローダー (再読み込みなし)
Rails.autoloaders.once
# 設定例: あるパスをオートロードの対象から除外
Rails.autoloaders.main.ignore(Rails.root.join("lib/tasks"))このようなパターンが、APIリファレンスからもたどれるようになります。
- 影響範囲・注意点
実行時挙動・パブリックAPIの変更は一切なし
- 既存コードへの互換性に影響はありません。
- テストの追加や削除もなく、ロジック部分の差分はゼロです。
影響範囲は「ドキュメント生成時」に限定
rails doc:railsなどで生成される API ドキュメントにRails.autoloadersの説明が追加されます。- エディタ/IDE が RDoc/YARD コメントを参照する場合、補完時のヘルプ情報が充実する可能性があります。
注意点
- 内容的には既にガイドに存在していた情報なので、新しい機能の追加や非推奨化の宣言ではありません。
- 「Rails.autoloaders が正式に文書化された」という意味で、今後この API がより「パブリック」な位置づけとして扱われやすくなる可能性はありますが、このPR単体で API のステータスが変わったわけではありません。
- 参考情報 (あれば)
元ガイド:
Autoloading and Reloading Constants (Zeitwerk モード)
https://guides.rubyonrails.org/autoloading_and_reloading_constants.htmlこのPRは上記ガイド内で既に説明されていた
Rails.autoloadersの内容を、APIドキュメント側に反映したものです。関連クラス/概念(補足)
Rails.autoloaders.main- 通常のアプリコード(
app/models,app/controllersなど)用。開発環境でのリロード対象。
- 通常のアプリコード(
Rails.autoloaders.once- 一度だけ読み込んで変更を前提としないコード(初期化処理・一部ライブラリなど)用。
これらの使い分けやメソッドの詳細が、今後は API ドキュメントからも参照しやすくなります。
#57406 [#57402] Fix Active Record Pool Reaper thread leak after Parallelization#shutdown
マージ日: 2026/5/22 | 作成者: @ruyrocha
- 概要 (1-2文で)
Rails のテスト並列実行後に Active Record の Pool Reaper スレッドが親プロセス側にリークし、接続プールやファイルディスクリプタが解放されない問題を修正する PR です。Parallelization#shutdown時にクリーンアップフックを実行し、Active Record が自前でプール破棄用フックを登録し、かつ Reaper スレッドを正しく停止・回収するようにしています。
- 変更内容の詳細
問題の構造
PR 説明によると、以下 3 層のバグが組み合わさって、並列テスト終了後も「Active Record Pool Reaper」というスレッドが親プロセスに残り続けていました:
Parallelization#shutdownがクリーンアップフック (run_cleanup_hooks) を呼ばない- 並列テスト実行でフォーク/スレッドを使い終わった後、本来は親プロセス側で「テストが終わった後片付け」を行うためのフックがあるが、これが呼び出されていなかった。
Active Record がクリーンアップフックに自分の後始末(接続プール破棄)を登録していない
- フック自体が呼ばれていなかった上に、AR も「テスト並列実行が終わったら接続プールを破棄する」処理をフックに登録していなかった。
clear_all_connections!がdisconnect!を呼んでいて Reaper スレッドを止めない- たとえクリーンアップフックが呼ばれても、
disconnect!は「接続を切る」だけで、プールに紐づく Reaper スレッドを停止させないため、スレッドが生き残る。
- たとえクリーンアップフックが呼ばれても、
この結果、長寿命な CI プロセスなどでテストスイートを順に何度も実行すると、親プロセスに Pool Reaper スレッドと DB コネクションがどんどん溜まっていく、という問題になっていました。
具体的な修正点
1. Parallelization#shutdown でクリーンアップフックを実行
対象ファイル: activesupport/lib/active_support/testing/parallelization.rb
shutdownメソッドの最後で、ワーカー終了後にrun_cleanup_hooksを呼ぶように変更。- これにより、「テスト並列実行が終わったタイミング」で各種ライブラリ(Active Record など)が登録したクリーンアップ処理が確実に実行される。
イメージとしては:
def shutdown
# 既存: workers を止める処理など
# ...
run_cleanup_hooks # ← 新規追加
end2. Active Record がクリーンアップフックを登録し、discard! でプールを破棄
対象ファイル:
activerecord/lib/active_record/test_databases.rbactiverecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
主な変更点:
Active Record が テスト用セットアップのどこかで「クリーンアップフック」を登録し、その中で 全ての接続プールに対して
discard!を呼ぶようにした。
(従来のclear_all_connections!/disconnect!ではなく、プール自体を破棄する方向に寄せている)ConnectionPool#discard!内で、後述の Reaper 側の処理が呼ばれるようになっている。
discard! の役割は、単にコネクションを切るだけでなく「このプールはもう使わない」と宣言し、関連するバックグラウンド処理(ここでは Reaper)も止める、という意味合いになります。
3. ConnectionPool#discard! が Reaper 登録からも確実に外す
対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb
メインの追加ロジック:
Reaper.discard_pool(pool)を実装し、ConnectionPool#discard!からこれを呼ぶようにした。Reaper.discard_poolの中で:- 対象のプールを Reaper の管理対象リストから削除。
- その「reaping_frequency(監視間隔)」に紐づいたプールが 1 つも残っていない場合は:
- 対応する Reaper スレッドを 即座に kill & join する(スレッドを停止し、リソースとして確実に回収)。
結果として、「もう使わない接続プールを破棄するときに、そのプールを監視していた Reaper スレッドも同時に正しく終了」するようになります。
4. テストの追加
対象ファイル:
activerecord/test/cases/reaper_test.rbactivesupport/test/parallelization_test.rb
主な内容:
Active Record 側:
- 特定の
reaping_frequencyを持つ複数のプールを登録し、順にdiscard!していったときに:- 対応する Reaper スレッドがプール 0 件になったタイミングで停止・解放されることを確認するテスト。
- 特定の
Active Support 側:
Parallelization#shutdownを呼び出した後に、クリーンアップフック経由で Active Record の Reaper スレッドが生き残っていないことを確認するテスト。- PR 説明にある再現コードに近い形で、「shutdown 後に
Thread.listにActive Record Pool Reaperがいないこと」をアサートしている。
- 影響範囲・注意点
対象シナリオ
- ActiveSupport::Testing::Parallelization を用いた 並列テスト実行環境(Minitest ベースの Rails テスト)が主な対象です。
- CI 上で 1 プロセスの中で複数のテストスイートを順次実行するような構成だと、この修正によりスレッド・接続リークの蓄積が止まります。
影響範囲
- ActiveRecord の接続プール管理 (
ConnectionPool#discard!, Reaper) に手が入っているため、テスト以外でも明示的にdiscard!を呼んでいるコードがあれば、そのタイミングで Reaper スレッドも終了するようになります。 - ただし、通常のアプリケーション運用(
establish_connectionして自動的に Reaper を使うだけ)では動作は従来と互換性があり、破棄しない限り Reaper は動き続けます。
- ActiveRecord の接続プール管理 (
破壊的変更の可能性
disconnect!ではなくdiscard!でプールを破棄する流れになったことで、「テスト終了後にプールが完全に捨てられる」ようになりました。そのため:- テストプロセスを生かしっぱなしで、同じ AR::Base の接続プールをそのまま再利用するようなカスタムコードがある場合は、後続処理で「既存プールがなくなっている」ことを前提に見直しが必要になる可能性があります(一般的な Rails テスト運用では問題にならない想定)。
パフォーマンス / リソース
- リークしていた Reaper スレッドと DB コネクションが確実に解放されるため、長時間動作する CI プロセスのメモリ消費や DB 接続数の安定性向上が期待できます。
- 参考情報 (あれば)
対応 Issue: Fixes #57402
PR に記載された再現スクリプト(要点):
rubyActiveRecord::Base.establish_connection( adapter: "sqlite3", database: ":memory:", reaping_frequency: 5 ) ActiveRecord::Base.connection_pool.with_connection { |c| c.execute("SELECT 1") } sleep 0.1 class ARPoolReaperLeakTest < Minitest::Test def test_ar_pool_reaper_is_cleaned_up_after_parallelization_shutdown assert Thread.list.find { |t| t.name == "Active Record Pool Reaper" } parallelization = ActiveSupport::Testing::Parallelization.new(1) parallelization.start parallelization.shutdown assert_empty Thread.list.select { |t| t.alive? && t.name == "Active Record Pool Reaper" } end end変更ファイル概要:
- ActiveRecord:
connection_pool.rb/reaper.rb: 接続プール破棄と Reaper スレッド停止の連携強化test_databases.rb: テスト用クリーンアップフック登録reaper_test.rb: Reaper の挙動テスト追加
- ActiveSupport:
parallelization.rb:shutdown時にrun_cleanup_hooksを呼ぶparallelization_test.rb: フック実行を検証するテスト追加
- ActiveRecord:
#57433 Disconnect test-local PostgreSQL adapters to prevent connection leak
マージ日: 2026/5/22 | 作成者: @yahonda
- 概要 (1-2文で)
PostgreSQLAdapterTest内で一時的に生成していた PostgreSQL アダプタ接続を明示的に切断することで、テスト実行時の PostgreSQL 接続リークを防ぐ修正です。PostgreSQLAdapter.newを直接呼び出していた箇所を、必ずdisconnect!されるヘルパーメソッド経由に統一しています。
- 変更内容の詳細
背景・問題点
activerecord postgresqlのナイトリー job がFATAL: sorry, too many clients alreadyで落ちることがあり、その一因としてテストコード側の接続リークが特定されました。PostgreSQLAdapterTestでは、多くのテストが以下のように「使い捨てのアダプタインスタンス」を直接生成していました:rubydef test_something connection = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.new( ... ) # connection を使って何らかのテスト # ここでメソッド終了 ⇒ 参照がなくなるが、明示的なdisconnectはしない endこの場合、ソケットのクローズは pg gem の Cレベルの finalizer に任されており、Ruby の GC タイミング次第となるため、
max_connections=100環境では GC されるまで接続が溜まり続けます。
新しいヘルパーメソッド: with_postgresql_adapter
上記問題を解決するため、テストクラスにプライベートヘルパー
with_postgresql_adapter(config_hash) { |connection| ... }が追加されました。役割:
- 渡された設定ハッシュから
PostgreSQLAdapterを生成 - ブロックに接続オブジェクトを渡して処理を実行
ensureで必ずdisconnect!を呼び出し、テスト終了時に確実に接続を閉じる
- 渡された設定ハッシュから
イメージとしては次のような構造です(実際のコードイメージ):
rubyprivate def with_postgresql_adapter(config) connection = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.new(**config) yield connection ensure connection&.disconnect! endこれに伴い、今まで
PostgreSQLAdapter.newを直接使っていたテストは、原則として以下のように書き換えられています:ruby# 修正前 def test_xxx conn = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.new(config) assert_equal "UTF8", conn.encoding end # 修正後 def test_xxx with_postgresql_adapter(config) do |conn| assert_equal "UTF8", conn.encoding end end
connection_without_insert_returning のリファクタ
既存のヘルパー
connection_without_insert_returningも、内部でPostgreSQLAdapter.newを使っていましたが、ここも接続リークの温床になりうるため、ブロックを取る形に変更されています。新構造では、このメソッドも内部的に
with_postgresql_adapterを利用し、必ずdisconnect!されるようになりました:ruby# イメージ def connection_without_insert_returning config = @connection_db_config.configuration_hash.merge(insert_returning: false) with_postgresql_adapter(config) do |conn| yield conn end endこれにより、
- 呼び出し側は
connection_without_insert_returningにブロックを渡す - そのブロック内だけ一時的な接続を利用し、処理後は自動的に切断 という形に整理されています。
- 呼び出し側は
計測結果
テスト:
postgresql_adapter_test.rbのみ、postgres:alpine(PostgreSQL 18),max_connections=100,--seed=37604(ベースラインで最悪ケース)、各5回計測。ピーク同時接続数(最大値)の推移:
状態 peak (5 runs) BEFORE 13, 15, 17, 16, 13 AFTER 4, 5, 5, 5, 5 ベースライン(テスト実行に最低限必要なセッション数)より 2–3 多い分は、プール管理されている
@connectionを経由したverify!/reconnect!由来であり、本PRのスコープ外と明示されています。
- 影響範囲・注意点
- 対象は テストコードのみ(
activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb)であり、本番コード・API の挙動には影響しません。 - 今後、
PostgreSQLAdapterTest内で一時的なアダプタ接続を新たに追加する場合は、PostgreSQLAdapter.newを直接呼ぶのではなく、with_postgresql_adapterまたはconnection_without_insert_returning(ブロック形式)を使うべき、というコーディング規約上の示唆になります。
- pg gem など GC に依存したクローズに任せない、というテスト設計上のベストプラクティスが反映されており、同様のパターンが他のアダプタテストにも存在する場合、同じアプローチでの修正が必要になる可能性があります。
- 接続プール管理の
@connectionを通じたverify!/reconnect!周りの残りのリーク要因は未対応のため、ナイトリーのtoo many clientsが完全には解消しない可能性はありますが、このPRにより一時接続由来のリークは大きく抑制されています。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57433
- 関連する調査・計測用リポジトリ:
yahonda/pg-leak-repro
https://github.com/yahonda/rails/tree/yahonda/pg-leak-repro - 閉じられた先行 PR(誤った計測に基づいていたもの): #57410
#57421 Document endless range inconsistency for where [ci-skip]
マージ日: 2026/5/22 | 作成者: @p8
- 概要 (1-2文で)
whereで「終端のない Range(endless range)」を使ったとき、start..(閉区間)とstart...(開区間)がどちらも同じ SQL(>= start)になるという挙動がガイドに明記されました。
挙動自体の変更ではなく、「一見すると直感に反するが、そういう仕様である」ことを公式ドキュメントに追記した PR です。
- 変更内容の詳細 (サンプルコード例あり)
対象ファイル:
guides/source/active_record_querying.mdに説明文を 5 行追加
内容としては、Active Record のクエリインターフェイスで、次のようなコードを書いたとき:
# endless range(終端なし Range)を使った where
Model.where(created_at: start_time..) # 例: 2024-01-01 00:00:00..
Model.where(created_at: start_time...) # 例: 2024-01-01 00:00:00...Ruby 的には .. は「終端を含む」、... は「終端を含まない」Range ですが、
「終端がない」Range(endless range)の場合、どちらも同じ意味になり、生成される SQL は同じになる ことが明文化されました。
生成される SQL イメージ:
SELECT "models".*
FROM "models"
WHERE "models"."created_at" >= '2024-01-01 00:00:00'Model.where(created_at: start_time..)Model.where(created_at: start_time...)
のどちらも、>= start_time という条件になります(> にはならない)。
PR の説明にもある通り、これは以前から存在する挙動で、以下のような GitHub Issue で何度か議論されていましたが、
これまで正式なドキュメントに記載されていなかったため、今回ガイドに追記された、という位置付けです。
- 影響範囲・注意点
- 挙動変更はなし(ドキュメントのみの更新)
- アプリケーションコードやクエリの生成結果が変わることはありません。
- endless range では
..と...の違いは無視されるstart..end/start...end(終端あり Range)は従来通り明確に挙動が異なりますが、start../start...(endless range)ではどちらも>= startとなります。- そのため、「厳密に
>にしたいから...を使う」といった意図は、endless range では反映されません。
- コードレビュー・スタイル上の注意
- チームメンバーが
where(start...)を「>条件になる」と誤解して書いている可能性があるので、
コードレビューで意図を確認しておくと安全です。 - 「
>=条件」であることを明確にしたいなら、素直にwhere("created_at >= ?", start_time)のように書く、
またはコメントで意図を補足するのも一案です。
- チームメンバーが
- 参考情報 (あれば)
- PR 本体:
- 関連する議論:
- Ruby の endless range 自体の仕様:
- Ruby 2.6 以降で導入された
begin../...構文。
Active Record はこれを Ruby の Range として受け取り、内部で>= beginな条件に解釈している。
- Ruby 2.6 以降で導入された
#57209 Fix label for not matching input id when collection value is nil
マージ日: 2026/5/21 | 作成者: @55728
- 概要 (1–2文で)
collection_radio_buttons/collection_check_boxesのコレクション内にnil値がある場合に、labelのfor属性と対応するinputのidが食い違ってしまう不具合を修正する PR です。nilのときにfor側だけ末尾に不要なアンダースコア(_)が付いていた問題を解消し、正しくクリックで選択できるようにしています。
- 変更内容の詳細
問題の具体例
以下のようなコード:
collection_radio_buttons(:user, :active, [["Yes", true], ["Undefined", nil]], :last, :first)現状(修正前)は次のような HTML が生成されていました:
<!-- ✅ true のケース: for と id が一致 -->
<input id="user_active_true" type="radio" value="true" />
<label for="user_active_true">Yes</label>
<!-- ❌ nil のケース: 末尾のアンダースコアで不一致 -->
<input id="user_active" type="radio" value="" />
<label for="user_active_">Undefined</label>label の for="user_active_" と input の id="user_active" が一致しないため、「Undefined」のラベルをクリックしてもラジオボタンが選択されませんでした。
不具合の原因
input の id と label の for は別々のメソッドで計算されており、nil の扱いが揃っていませんでした。
1. Tags::Base#add_default_name_and_field_for_value(id を生成)
def add_default_name_and_field_for_value(tag_value, options, field = "id")
if tag_value.nil?
add_default_name_and_field(options, field) # suffix を付けない → id="user_active"
else
# ...
options[field] += "_#{sanitized_value(tag_value)}"
end
endtag_value が nil の場合は「値によるサフィックス(_true など)」を付けず、単に user_active のような ID になります。
2. CollectionHelpers#sanitize_attribute_name(label for を生成)
def sanitize_attribute_name(value)
"#{sanitized_method_name}_#{sanitized_value(value)}"
# nil → sanitized_value(nil) = "" → "active_" → for="user_active_"
endこちらは value が nil でも必ず "#{メソッド名}_#{値}" という形で結合するため、サニタイズ後の値が空文字でも _ が残ってしまい、"active_" → for="user_active_" という不整合が発生していました。
修正内容
sanitize_attribute_name を修正し、「サニタイズ後の値が空文字のときはアンダースコアを付けない」ように変更しています。
def sanitize_attribute_name(value)
sanitized = sanitized_value(value)
if sanitized.empty?
sanitized_method_name.dup
else
"#{sanitized_method_name}_#{sanitized}"
end
endこれにより、value が nil → sanitized_value(value) が "" の場合は:
- これまで:
"active_"→for="user_active_" - 修正後:
"active"→for="user_active"
となり、input 側の id="user_active" と一致するようになります。
テスト
actionview/test/template/form_collections_helper_test.rb に、以下をカバーする回帰テストが追加されています。
collection_radio_buttonsでコレクションにnilを含むケースcollection_check_boxesで同様のケース
どちらも、生成される label の for と対応する input の id が一致することを検証しています。
- 影響範囲・注意点
影響範囲:
ActionViewのform_collections_helperを通じて呼ばれるcollection_radio_buttons/collection_check_boxesにおいて、値がnil(またはサニタイズ結果が空文字になる値)のときの ID / for の生成ロジックが変わります。labelのforの値が、これまで"user_active_"のような形だったケースは"user_active"に変更されます。
期待されるポジティブな影響:
labelをクリックしても対応するinputが選択されない / フォーカスされないといったアクセシビリティ上の不具合が解消されます。- HTML 的にも
forとidの整合性が取れます。
注意点:
- もし既存コードや CSS / JavaScript が、
forやidの末尾のアンダースコア(_)を前提に依存している場合、今回の変更でセレクタなどがマッチしなくなる可能性があります。- 例:
#user_active_やlabel[for="user_active_"]といったセレクタを使っている場合。
- 例:
- ただし、
nil値を持つラジオボタン/チェックボックスに対して、こうした依存をしているケースは比較的レアと思われますし、本来不正な紐づけだったため、修正後の挙動が妥当と考えられます。
- もし既存コードや CSS / JavaScript が、
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57209
- 問題の原因となっていたメソッド導入コミット(2012年)
sanitize_attribute_nameが導入されたコミット: https://github.com/rails/rails/commit/36cb7150fd
- 変更されたファイル:
actionview/CHANGELOG.mdactionview/lib/action_view/helpers/tags/collection_helpers.rbactionview/test/template/form_collections_helper_test.rb
#57400 Reset lock_version after a nested savepoint rollback
マージ日: 2026/5/21 | 作成者: @55728
- 概要 (1-2文で)
requires_new: trueなネストしたトランザクション(セーブポイント)がロールバックされたときに、メモリ上のlock_versionをDBの状態に戻すようにして、次回のsave/update!でActiveRecord::StaleObjectErrorが発生する不具合を修正するPRです。前回の #57363 で対応できていなかった「ネストしたセーブポイントのロールバック」ケースを補完します。
- 変更内容の詳細
問題の背景
Railsの楽観的ロック(lock_version)は更新時に WHERE lock_version = ? を付与することで、他トランザクションによる更新競合を検出します。
しかし、次のようなケースで不整合が発生していました。
widget = Widget.create!(name: "a") # lock_version: 0
Widget.transaction do
Widget.transaction(requires_new: true) do
widget.update!(name: "b") # lock_version: 0 -> 1
raise ActiveRecord::Rollback # セーブポイントのロールバック: DB上は lock_version -> 0 に戻る
end
widget.lock_version # => 1 (メモリ上は 1 のまま / DBは 0)
widget.update!(name: "c") # ActiveRecord::StaleObjectError 発生
endセーブポイント内の更新がロールバックされてDB上の lock_version は元に戻る一方で、Rubyオブジェクト側はインクリメント済み (1) のまま保持しているため、次の update! 時の WHERE 句は lock_version = 1 となり、行がヒットせず StaleObjectError になります。
外側のトランザクションがコミットされても同じ問題が出ます:
Widget.transaction do
Widget.transaction(requires_new: true) do
widget.update!(name: "b")
raise ActiveRecord::Rollback
end
end
widget.update!(name: "c") # 同様に StaleObjectError前PR (#57363) では「最も外側のトランザクションがロールバックされたとき」に lock_version をリセットする対応のみで、ネストされた requires_new: true セーブポイントのロールバックには対応できていませんでした。
原因となっていた実装の挙動
トランザクションロールバック時には restore_transaction_record_state が呼ばれますが、以下の条件でショートサーキットしていました:
force_restore_stateがfalserestore_state[:level] > 1(= ネストしたセーブポイントレベル)- このパスは
SavepointTransaction#full_rollback? = falseのときに通る
その結果、「ネストしたセーブポイントのロールバック時」にはレコードの属性が何も復元されず、lock_version もインクリメントされたまま残ってしまっていました。
メソッド上のコメントは本来やりたいことを正しく表していました:
# Call the #after_rollback callbacks. The +force_restore_state+ argument indicates if the record
# state should be rolled back to the beginning or just to the last savepoint.つまり「トランザクション開始時点に戻す(full rollback)」と「最後のセーブポイント時点に戻す(savepoint rollback)」の両方をサポートするつもりだったものの、ロック用カラムについては後者が実装されていなかった、という状態です。
このPRの実装内容
activerecord/lib/active_record/transactions.rb の restore_transaction_record_state まわりに以下のロジックを追加しています(概念的な説明):
- 既存:
force_restore_stateがtrue(完全ロールバック時)は、スナップショットを元に全体の状態を復元するブランチがある。 - 追加:
force_restore_stateはfalseだがlocking_enabled?なモデルについては、- トランザクションスナップショットに保存している
original_valueを使い、 write_from_databaseによりロックカラム(lock_version等)だけをDB由来の値に戻す- この復元処理は #57363 で最外レベル用に導入されたものと同じアプローチ
- トランザクションスナップショットに保存している
これにより、セーブポイントロールバック時に「そのセーブポイント開始時点の lock_version」相当の値にメモリ上のオブジェクトも戻すことができます。結果として、次回の save / update! は
- WHERE 句の
lock_versionがDBと一致する - Dirty tracking(変更検知)の状態も整合した形になる
ため、正しく更新が通り StaleObjectError が起きなくなります。
追加されたテスト
activerecord/test/cases/locking_test.rb の OptimisticLockingRollbackTest で次の2テストが追加されています(このテストクラスは self.use_transactional_tests = false):
test_lock_version_restored_after_savepoint_rollback- パターン: 「外側Tx内でネストした
requires_new: trueのセーブポイントがロールバックし、その同じ外側Txの中で再度save」 - 期待: 2回目の
saveが成功する(StaleObjectErrorにならない)
- パターン: 「外側Tx内でネストした
test_lock_version_restored_after_savepoint_rollback_when_outer_commits- パターン: 「内側セーブポイントロールバック → 外側Txはコミット → ブロックの外で再度
save」 - 期待: ブロックの外での
saveが成功する
- パターン: 「内側セーブポイントロールバック → 外側Txはコミット → ブロックの外で再度
どちらも main では ActiveRecord::StaleObjectError で落ち、パッチ適用後にグリーンになることが確認されています。
- 影響範囲・注意点
影響範囲
- 対象となるのは「楽観的ロック(
locking_enabled?= true)を有効にしているモデル」で、 - 「トランザクション内で
requires_new: trueなネストしたトランザクション(セーブポイント)を使い、その中でロック対象レコードをUPDATEし、そのセーブポイントをロールバックする」ケースです。 - 上記条件を満たすアプリケーションでは、
- これまで
ActiveRecord::StaleObjectErrorが発生していたシナリオが正しく成功するようになります。 - 既存の挙動を前提に(例: ロールバック後にStaleObjectErrorをトリガーとして何かしていた等)奇妙なワークアラウンドを書いていた場合、それらの挙動が変わる可能性はあります。
- これまで
通常は「バグ修正」として期待通りの方向の変更であり、互換性上のリスクは小さいと考えられます。
なお、未対応のコーナーケース
PR説明にもある通り、次のような「さらに一段複雑なケース」はまだ未解決です:
Widget.transaction do
widget.update!(name: "outer") # ここで lock_version が 0 -> 1 に
Widget.transaction(requires_new: true) do
widget.update!(name: "inner") # ここで lock_version が 1 -> 2 に
raise ActiveRecord::Rollback # セーブポイントロールバック: DB上は lock_version 1 に戻るはず
end
# スナップショットに残っている original_value が
# 「outer の前」の値 (0) だったりすると、
# どの値に戻すべきか分からない問題が残る
end- 現状のスナップショットは「トランザクション開始時点」の値のみ持っており、「各セーブポイント前の値」を持っているわけではありません。
- そのため、「外側トランザクションの中でも同じレコードを更新している」場合に、
「セーブポイント直前の正しいlock_version」を復元するための情報が足りません。 - このPRでは、そのようなケースの症状は変わらず
StaleObjectErrorの発生が続きます(ただし値の不一致内容は変化し得る)。
→ 挙動が悪化するのではなく、「よくあるパターンだけ直し、レアで複雑なパターンは現状維持」という位置づけです。
根本的にこのコーナーケースを解決するには、remember_transaction_record_state でセーブポイントごとにスナップショットを持つような、より大きな設計変更が必要になります。
- 参考情報 (あれば)
- 対応済みの前PR:
- #57363 — 「最外トランザクションのロールバック時に
lock_versionを復元する」修正
- #57363 — 「最外トランザクションのロールバック時に
- このPRでの検証:
- SQLiteを使った自己完結型の再現スクリプト
/tmp/lock_savepoint_rollback.rb(mainではStaleObjectError、本PR後は成功) - Active Record test suite(sqlite3)での通過:
locking_test.rbtransactions_test.rbautosave_association_test.rb
- SQLiteを使った自己完結型の再現スクリプト
- 変更ファイル:
activerecord/lib/active_record/transactions.rbactiverecord/test/cases/locking_test.rbactiverecord/CHANGELOG.md(バグ修正として追記)
#57429 Preserve SafeBuffer encoding in ActiveSupport::MessagePack
マージ日: 2026/5/21 | 作成者: @Copilot
- 概要 (1-2文で)
ActiveSupport::MessagePack でActiveSupport::SafeBufferをシリアライズ/デシリアライズした際に、元の文字エンコーディング(特に UTF-8)が失われないようにする修正です。これにより、キャッシュなどから復元したSafeBufferを UTF-8 文字列と安全に連結できるようになります。
- 変更内容の詳細
問題の背景
従来の実装では、ActiveSupport::SafeBuffer を MessagePack によって round-trip(dump → load)すると、復元後のオブジェクトのエンコーディングが ASCII-8BIT(バイナリ)になっていました。その結果、例えば以下のようなケースでエラーや不具合が発生し得ます。
loaded = ActiveSupport::MessagePack.load(
ActiveSupport::MessagePack.dump(ActiveSupport::SafeBuffer.new("Mäßig"))
)
loaded.encoding
# 旧実装: #<Encoding:ASCII-8BIT> などになり得る
"prefix " + loaded
# Encoding::CompatibilityError などを引き起こす可能性特に、キャッシュレイヤ(例: Rails.cache)で SafeBuffer を保存し、後から取り出してビューやヘルパーで UTF-8 文字列と連結するようなパターンで問題になっていました。
主な変更点
1. シリアライズ方法の変更
対象ファイル: activesupport/lib/active_support/message_pack/extensions.rb
- これまでは
SafeBufferを MessagePack の「生の拡張バイト列(raw extension bytes)」として扱っていたため、デコード時にエンコーディング情報が失われやすい状態でした。 - 本PRでは、
SafeBufferを「内部に通常の MessagePack 文字列ペイロードをネストして持つ」形でエンコードするように変更しています。
イメージとしては:
# 変更前 (概念的に)
# SafeBuffer -> EXT(binary) -> load で ASCII-8BIT な文字列になりがち
# 変更後 (概念的に)
# SafeBuffer -> "中身の String を MessagePack 文字列としてエンコードしたデータ" を持つ拡張
# load 時にその文字列を decode してから SafeBuffer を再構築2. デシリアライズ時の SafeBuffer 再構築
- load 時には、ネストされた MessagePack 文字列を decode し、その結果の Ruby 文字列(この時点で元のエンコーディング情報を保持している)から
ActiveSupport::SafeBufferを再生成します。 - これにより、UTF-8 のまま round-trip されるようになります。
動作例(PR 説明より):
loaded = ActiveSupport::MessagePack.load(
ActiveSupport::MessagePack.dump(ActiveSupport::SafeBuffer.new("Mäßig"))
)
loaded.encoding
# => #<Encoding:UTF-8>
"prefix " + loaded
# => "prefix Mäßig"ここで確認できる保証:
loadedはEncoding::UTF_8"prefix " + loadedが例外なく動作loadedはActiveSupport::SafeBufferかつhtml_safe? == trueのまま
3. 回帰テスト(回帰防止)の追加
対象ファイル: activesupport/test/message_pack/shared_serializer_tests.rb
以下の点をカバーするテストが追加されています。
- MessagePack シリアライザで
SafeBufferを dump → load した結果:- オブジェクトクラスが
ActiveSupport::SafeBufferのまま html_safe?がtrueのままencodingがEncoding::UTF_8のまま- UTF-8 の通常文字列との連結が例外なく可能であること
- オブジェクトクラスが
- 影響範囲・注意点
影響範囲
- 対象:
ActiveSupport::MessagePackを使ってActiveSupport::SafeBufferをシリアライズ/デシリアライズしているコード(特にキャッシュやバックグラウンドジョブ間の引き渡しなど)。 - 具体的な影響:
- これまで ASCII-8BIT になってしまっていたケースが UTF-8 のまま復元されます。
- その結果、ビューやヘルパーで UTF-8 文字列との連結時に発生していた
Encoding::CompatibilityError等が解消されます。 - HTML セーフ属性 (
html_safe?) も保持されるため、既存の XSS 対策の挙動は変わりません。
互換性・注意点
- API の表面的なインターフェース(
ActiveSupport::MessagePack.dump/loadのシグネチャ)は変わっていません。 - 変更点はシリアライズフォーマット(内部表現)であり、MessagePack のバイナリ互換性については:
- 旧形式でシリアライズ済みの SafeBuffer データを新しい実装で load しようとするケースが理論上あり得ます(長期保存されたキャッシュ等)。
- PR 説明上、後方互換フォーマットへの配慮については明示されていませんが、通常 Rails の MessagePack 拡張は同じアプリケーションプロセス内で完結する用途が多く、実運用で問題になる可能性は低めです。
- もし長期保管された MessagePack バイナリを別バージョンの Rails で読むケースがある場合は、切り替え時にキャッシュクリアや再生成を検討した方が安全です。
- 参考情報 (あれば)
- PR タイトル: Preserve SafeBuffer encoding in ActiveSupport::MessagePack
- PR 番号: #57429
- 変更ファイル:
activesupport/lib/active_support/message_pack/extensions.rbactivesupport/test/message_pack/shared_serializer_tests.rb
- 対象クラス:
ActiveSupport::SafeBufferActiveSupport::MessagePack
この変更は「SafeBuffer をキャッシュして取り出したらエンコーディングがおかしくなり、ビュー側で文字列連結時に落ちる」といった、よくあるけれど厄介なバグに対するピンポイントな修正と捉えると理解しやすいです。
#57404 Update permissions policy list
マージ日: 2026/5/21 | 作成者: @rubenarakelyan
- 概要 (1–2文で)
Rails のActionDispatch::PermissionsPolicyが、ブラウザ側で新たに定義・拡充された Permissions Policy(旧 Feature Policy)のディレクティブ一覧に追随して更新されました。これにより、config.permissions_policyなどから最新のポリシー項目を公式に扱えるようになります。
- 変更内容の詳細
2-1. 何が更新されたか
ActionDispatch::PermissionsPolicy 内で定義されている「許可されるディレクティブ名のリスト」が、ブラウザ仕様の最新状態に合わせて拡張されています。追加された主なディレクティブは以下です(PR 本文の列挙と同じ):
attribution-reportingbatterybluetoothch-uach-ua-archch-ua-bitnessch-ua-full-versionch-ua-full-version-listch-ua-high-entropy-valuesch-ua-mobilech-ua-modelch-ua-platformch-ua-platform-versionch-ua-wow64compute-pressurecross-origin-isolateddirect-socketsexecution-while-not-renderedexecution-while-out-of-viewportidentity-credentials-getmediasessionnavigation-overrideotp-credentialspublickey-credentials-getstorage-accesswindow-managementxr-spatial-tracking
これらが ActionDispatch::PermissionsPolicy の「サポートされるディレクティブ名」として登録されたため、Rails の DSL から安全に扱えるようになっています。
2-2. 実際のコードでの利用例
config/application.rb もしくは各環境ごとの config/environments/*.rb での設定例:
# config/application.rb
module YourApp
class Application < Rails::Application
config.permissions_policy do |policy|
# Attribution Reporting API を同一オリジンのみ許可
policy.attribution_reporting :self
# Bluetooth API を完全に禁止
policy.bluetooth :none
# User-Agent Client Hints 関連
policy.ch_ua :self
policy.ch_ua_platform :self
policy.ch_ua_full_version_list :self
# WebAuthn / Credential Management 関連
policy.publickey_credentials_get :self
policy.identity_credentials_get :none
policy.otp_credentials :self
# XR / WebXR 関連
policy.xr_spatial_tracking :self
# ウィンドウ管理やストレージアクセス
policy.window_management :self
policy.storage_access :self
end
end
endActionDispatch::PermissionsPolicy は、上記のような設定から HTTP レスポンスヘッダ Permissions-Policy を生成します。
例: 上記設定の一部に対応するヘッダ断片は次のようになります。
Permissions-Policy:
attribution-reporting=(self),
bluetooth=(),
ch-ua=(self),
ch-ua-platform=(self),
publickey-credentials-get=(self),
...※ 実際のヘッダフォーマットは Rails / Rack の実装とブラウザ仕様に応じて変わりますが、概ねこのような形で送出されます。
- 影響範囲・注意点
新しいディレクティブを使えるようになった
これまで Rails 側でサポートされていなかった最新の Permissions Policy ディレクティブを、アプリケーション設定から明示的に制御できます。
例:policy.bluetooth,policy.compute_pressure,policy.direct_socketsなど。既存アプリへの互換性
既存のアプリケーションで、これら新ディレクティブを明示的に使っていなければ、挙動は変わりません。
逆に、「手動でヘッダ文字列を組み立てていた」「ミドルウェアを自作していた」場合は、Rails 標準のconfig.permissions_policyに移行することでコードの簡素化と将来の追随性向上が見込めます。ブラウザ側のサポート状況に依存
Rails にディレクティブが追加されたことで、ヘッダ上は設定可能になりますが、
実際に機能が制御されるかどうかは、各ブラウザが該当ディレクティブをサポートしているかに依存します。
新しめの API(attribution-reporting,direct-sockets,compute-pressureなど)は、ブラウザ実装状況を確認した上で利用してください。ポリシーのデフォルト値に注意
新しいディレクティブは、設定しなければブラウザのデフォルト挙動に従います。
たとえばセキュリティ・プライバシー要件が高い環境では、未使用の強力な API(Bluetooth, Direct Sockets, Window Management など)は:noneで明示的に閉じる、といったポリシー設計が推奨されます。
- 参考情報 (あれば)
- Rails ガイド(英語):
- Action Dispatch:
ActionDispatch::PermissionsPolicy
https://api.rubyonrails.org/classes/ActionDispatch/PermissionsPolicy.html (※バージョン更新後に反映予定)
- Action Dispatch:
- 仕様・ブラウザ側ドキュメント(英語):
- Permissions Policy
https://w3c.github.io/webappsec-permissions-policy/ - MDN: Permissions-Policy ヘッダ
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy
- Permissions Policy
この PR によって、Rails アプリから最新のブラウザ機能制御ポリシーを公式に扱えるようになった点が主な価値です。
#57171 Parallelize exist? checks and uploads in MirrorService#mirror
マージ日: 2026/5/21 | 作成者: @Mordorreal
- 概要 (1-2文で)
Active Storage のMirrorService#mirrorにおいて、複数ミラーへのexist?チェックとアップロード処理をスレッドプール上で並列化し、特にネットワークレイテンシが支配的な環境で大幅な速度向上を図る変更です。既存の@executor(スレッドプール)をmirrorでも活用するようにした改善 PR です。
- 変更内容の詳細
2-1. これまでの問題点
ActiveStorage::Service::MirrorService#mirror は、複数のミラーサービスに対して:
- それぞれに
exist?(key)を順番に実行 exist?がfalseのミラーに対して、順番にupload実行
という「ミラーの数 N に比例して処理時間が伸びる O(N) の逐次処理」になっていました。
一方で、同じクラス内の delete や delete_prefixed は、すでに @executor(Concurrent::ThreadPoolExecutor 相当)を使って並列実行されていますが、mirror だけはその恩恵を受けていませんでした。
ネットワーク(S3/GCSなど)のレイテンシが支配的な場面では、「順番に待つ」よりも「まとめて投げて並列に待つ」方が圧倒的に速くなるため、mirror もスレッドプールを使って並列化すべき、というのがモチベーションです。
2-2. 変更の要点
(1) exist? チェックとアップロードの両方を並列化
Concurrent::Promise と既存の @executor を用いて、
- フェーズ1: 各ミラーに対する
exist?(key)チェックを並列で実行 - フェーズ2: 「存在しなかったミラー」に対する
uploadを並列で実行
するように変更されました。
概念的な流れは以下のようになります(擬似コードイメージ):
def mirror(key, io, checksum: nil, **options)
# io を安全に複数スレッドで扱うため、後述のように一度内容を読みきる
# 1. 各ミラーに対して exist? を並列チェック
exist_promises = mirrors.map do |service|
Concurrent::Promise.execute(executor: @executor) do
service.exist?(key)
end
end
# 結果を待ち、存在しないミラーだけを抽出
missing_services = mirrors.zip(exist_promises).reject { |_, p| p.value }.map(&:first)
# 2. 存在しなかったミラーに対して upload を並列実行
upload_promises = missing_services.map do |service|
Concurrent::Promise.execute(executor: @executor) do
# 各ミラーに専用の IO を渡して upload
service.upload(key, build_io_for(service), checksum: checksum, **options)
end
end
# すべてのアップロード完了を待つ(エラーは従来通り伝播)
upload_promises.each(&:value!)
end実際のコードはもう少し凝っていますが、流れとしてはこういう2フェーズ構造を、どちらも Concurrent::Promise + @executor で並列化した形です。
(2) IO の扱いをスレッドセーフにする工夫
複数スレッドから同じ IO オブジェクトを読み書きすると競合するため、次の工夫が入っています:
ファイル内容を一度だけ読み出し、凍結された文字列に保存
- 元の
ioからStringを1回だけreadし、その文字列をfreezeして共有データにします。 - これにより、並列処理の中で「共有のミュータブル IO を読む」ことを避けます。
- 元の
各ミラー用に専用の
StringIOを生成- 上記の「凍結された文字列」をもとに、各ミラーごとに
StringIO.new(frozen_string)を生成してuploadに渡します。 - こうすることで、各スレッドは自分専用の IO を読むだけなのでスレッドセーフになります。
- 上記の「凍結された文字列」をもとに、各ミラーごとに
io.rewindを呼んで EOF 問題を回避mirrorが呼ばれたとき、渡されたioがすでに EOF(末尾)にある可能性があります。- 最初に
io.rewindを実行してから読み出すことで、「もともとの処理系が io をどこまで読んだか」に依存せず、mirrorの中でファイル全体を正しく読めるようにしています。
このあたりは、Active Storage が Tempfile やストリームなど様々な IO を扱うことを前提に、慎重に対応している部分です。
(3) 既存スレッドプール (@executor) の再利用
MirrorService はコンストラクタで、ミラー数に応じたスレッドプールをすでに持っています:
@executor = Concurrent::ThreadPoolExecutor.new(
min_threads: 1,
max_threads: mirrors.size,
# ... 略 ...
)delete / delete_prefixed ではすでにこのプールを使っていましたが、mirror だけは同期処理だったため、ここでも @executor を利用するように統一されました。
これにより、並列度は「ミラーの数」に上限があり、無制限にスレッドが増えることはありません。
2-3. ベンチマーク結果
説明中のベンチマークでは、1ミラーあたり 50ms のクラウドレイテンシ(S3/GCS の P50 想定)を負荷として与えたときの測定結果が示されています:
| Mirrors | Sequential | Parallel | Speedup |
|---|---|---|---|
| 2 | 100ms | 50ms | ~2x |
| 3 | 150ms | 50ms | ~3x |
| 5 | 250ms | 50ms | ~5x |
各ミラーでの処理がほぼ「待ち時間だけ」で占められるため、並列実行すると「最大レイテンシ ≒ 1ミラー分のレイテンシ」で済み、ミラー数に比例したスピードアップが得られている、という結果です。
また、ファイルサイズ(50KB–20MB)は性能にほとんど影響しないことも確認されており、I/O のスループットよりもネットワークのラウンドトリップがボトルネックであることが示唆されています。
- 影響範囲・注意点
影響範囲
- 対象: Active Storage を使っているアプリケーションのうち、
ActiveStorage::Service::MirrorServiceを利用している環境- 特に S3 + GCS など複数クラウドストレージにミラーしているケース
- 変更点:
mirror動作の外から見たインターフェイス (mirror(key, io, ...)) は従来通り- 内部実装が並列実行になることで、全体の処理時間が短縮される可能性が高い
注意点
並列実行に伴う例外伝播
Concurrent::Promiseを使っているため、並列タスク内で例外が発生した場合の伝播は、実装のvalue!呼び出しなどを通して行われます。- 従来と同様に、いずれかのミラーで
uploadが失敗したときは例外が上がる想定ですが、「どのミラーで失敗したか」のデバッグにはスタックトレースやログを確認する必要があります(ここは元々の挙動と本質的に大きくは変わらないはずです)。
IO の扱いが変わることによる副作用
mirror内でio.rewindとio.readを行い、その内容からStringIOを生成する実装になっているため、mirror直後に同じioを外側で再利用しているようなコードがある場合、rewindの影響を受ける可能性があります。- 一般的には Active Storage 内部が所有する IO を渡す形なので問題になりにくいですが、
mirrorメソッドを直接呼び出して独自に拡張している場合は念のため挙動を確認すると安心です。
スレッドプール負荷
- スレッドプールの
max_threadsはmirrors.sizeに合わせているため、極端にスレッドが増えることはありませんが、ミラー数が多い環境ではスレッド数が相応に増えます。 - CPU が細い環境でミラー数を増やしすぎると、スレッドスケジューリングのオーバーヘッドが相対的に増える可能性はあります(ただしネットワーク待ち主体なので通常は問題になりにくい)。
- スレッドプールの
テスト追加による挙動保証
activestorage/test/service/mirror_service_test.rbにテストが多数追加されており、mirrorが並列実行になっても既存仕様が保たれていることを確認しています。- 特に「存在チェックの後にアップロードされる」「一度読み込んだ IO 内容を各ミラーで正しくアップロードできる」といった振る舞いが検証されていると考えられます。
- 参考情報 (あれば)
- 関連 PR: #57170
MirrorServiceや Active Storage のミラーリング周りで、今回の並列化と合わせて改善・リファクタリングが進んでいる可能性があります。
- 変更ファイル:
activestorage/CHANGELOG.md- この変更が挙動面の改善として CHANGELOG に追記されているため、今後のリリースノートでも確認できます。
activestorage/lib/active_storage/service/mirror_service.rb- 実際の並列化実装(
Concurrent::Promiseや@executorの使用、IO の扱いの変更)がここに含まれます。
- 実際の並列化実装(
activestorage/test/service/mirror_service_test.rb- 並列化に伴う挙動検証テストが追加されており、具体的な利用シナリオや期待される振る舞いを確認するのに参考になります。
この変更により、複数ストレージへのミラーリングを使っている環境では、特にレイテンシの高いクラウドストレージを組み合わせている場合に、アップロード系のレスポンス改善が見込めます。
#57423 Fix ShareLock ownership under :fiber isolation
マージ日: 2026/5/21 | 作成者: @joeljunstrom
- 概要 (1-2文で)
ActiveSupport::Concurrency::ShareLockの「ロック所有者」の識別方法を、Thread.currentベースからActiveSupport::IsolatedExecutionState.contextベースに変更し、config.active_support.isolation_level = :fiberのときでも正しく排他制御できるようにした PR です。これにより、Falcon などの Fiber スケジューラ使用時に、リロード後のランダムなNoMethodErrorが発生する不具合を修正しています。
- 変更内容の詳細
背景と問題点
従来の ShareLock は、以下のように「今ロックを持っている主体」を Thread.current で識別していました:
- スレッド隔離 (
isolation_level = :thread, デフォルト) では、- 1 リクエスト = 1 スレッド が一般的
→ リクエストごとに別スレッドなので、Thread.currentで「所有者」を区別できる
- 1 リクエスト = 1 スレッド が一般的
- しかし Fiber 隔離 (
isolation_level = :fiber) では、- 複数リクエスト Fiber が 1 つのスレッド上で実行される
→ どの Fiber も同じThread.currentになるため、ShareLockから見ると「全部同じ所有者」に見えてしまう
- 複数リクエスト Fiber が 1 つのスレッド上で実行される
この結果:
- リロード用 Fiber が
:unload(排他) ロックを取得しようとしたとき、
「同じ所有者が既に共有ロック中」と誤認して、排他ロック取得が通ってしまうケースがある - 実際には別のリクエスト Fiber がまだ共有ロックを保持しているのに、
- リロード Fiber が
:unload排他ロックを取得 - 定数をクリア
- 進行中リクエストから見ると、途中で定数が消える
→ ビュー描画などでランダムなNoMethodErrorが発生
→ 特に「起動後最初のファイル変更時」に顕在化
- リロード Fiber が
解決策: 所有者を IsolatedExecutionState に紐づける
ShareLock の「所有者」を ActiveSupport::IsolatedExecutionState.context ベースで判定するように変更しています。
ActiveSupport::IsolatedExecutionStateは、Rails がCurrentAttributes- その他「現在の実行コンテキスト」を意識する仕組み
で既に使っているコンテキスト管理
isolation_level = :threadのとき:- コンテキストはスレッドごと
→ これまで通りスレッド単位で所有者が区別される
- コンテキストはスレッドごと
isolation_level = :fiberのとき:- コンテキストは Fiber ごと
→ Fiber ごとに別の所有者として扱われる
- コンテキストは Fiber ごと
結果として:
- 「ロックの所有者」の定義が Rails 全体で一貫 (
CurrentAttributesなどと同じ概念) - Fiber スケジューラ環境でも、
- 各リクエスト Fiber が別オーナーとして ShareLock を取得
- ある Fiber が共有ロックを持っている間、別 Fiber が排他ロックを誤って取得することを防止
実装上の主な変更点
ShareLockの内部フィールド名の見直し以前は:
@exclusive_threadraw_state内の:threadキー
という名前で、「スレッド」を前提にした命名でした。
これを以下のように変更:
@exclusive_owner- 所有者はコンテキスト (スレッド or Fiber) によって変わるので、中立的な名前に変更
raw_stateの:thread→:owner- 実際には Fiber を保持し得るので、
threadと名乗らないようにした
- 実際には Fiber を保持し得るので、
:threadisolation 時の挙動自体は変わらず、「名前だけ」より正確になっています。ActionDispatch::DebugLocksの更新ActionDispatch::DebugLocksは/rails/locksからロック状態をデバッグ表示するためのミドルウェアで、ShareLock#raw_stateの唯一の利用者です。ここで以下の対応がなされています:
raw_stateの:thread→:ownerに追従- 所有者が Fiber の場合を考慮し、
Fiber#statusが存在しないため、素直に呼ぶと例外になる
→ ここを安全に扱うよう修正 (Threadとは別の型の場合でも落ちないように)
これにより、Fiber 環境でも
/rails/locksを安全に表示できます。テストの追加・強化
- 既存の
ShareLockTestに加え、ShareLockFiberTestを新設 - Fiber isolation (
isolation_level = :fiber) における所有権の挙動をテストstart_exclusive(no_wait: true)と手動Fiber.resumeを利用し、
スケジューラ不要で自己完結したテストにしている
- さらに、「
isolation_level = :threadの場合は複数 Fiber を同一所有者として扱う」回帰テストも追加
→ デフォルト構成での互換性が保たれていることを確認
なお、
wait_for内のスケジューラ連携 (実際のブロッキング挙動) まではカバーしていません。
これは別途 Fiber スケジューラ用のテストハーネスが必要になるため、この PR ではスコープ外としています。- 既存の
- 影響範囲・注意点
Rails アプリケーション側の挙動
config.active_support.isolation_level = :fiberを有効にし、Falcon などの Fiber 対応サーバを使っている場合:- リクエスト中にコードを変更 → リロード → ランダムな
NoMethodErrorが出る
といった問題が解消される可能性が高いです。
- リクエスト中にコードを変更 → リロード → ランダムな
- デフォルト (
:thread) の場合:- 所有者判定はスレッド単位のままで、挙動変更は意図的に最小限に抑えられています。
- 既存アプリへの影響はほぼありません (互換性テストも追加済み)。
ミドルウェア / 拡張を書いている場合
ActiveSupport::Concurrency::ShareLock#raw_stateを直接触っている独自コードがもしあれば::threadキー →:ownerキーに変更された点に注意してください。- 値として
ThreadだけでなくFiberが入る可能性があります。
型チェックやstatus呼び出しなどをしている場合は要見直しです。
- 通常のアプリケーションコードでは
raw_stateに触れることはほぼないため、一般的な利用者への影響は限定的です。
デバッグ用途 (
/rails/locks)- Fiber 環境でもクラッシュせずロック情報を確認できるようになりました。
- ロックの所有者表示が「スレッド ID 前提」ではなくなるため、
Fiber 環境での見え方が以前と変わる可能性があります。
- 参考情報 (あれば)
- 対応する設定:
config.active_support.isolation_level = :thread(デフォルト)config.active_support.isolation_level = :fiber
- 関連しそうなコンポーネント:
ActiveSupport::IsolatedExecutionStateActiveSupport::CurrentAttributesActiveSupport::Concurrency::ShareLockActionDispatch::DebugLocks(/rails/locks)
- 想定サーバ:
- Falcon などの Fiber スケジューラ対応 Rack サーバ環境で特に効果がある修正です。
#57425 Release reloader share on rack hijack in ActionDispatch::Executor
マージ日: 2026/5/21 | 作成者: @joeljunstrom
- 概要 (1-2文で)
ActionDispatch::Executor が WebSocket や Rack hijack(rack.hijack_io)による長寿命コネクションでもリクエスト完了処理(executor state の完了・reloader share の解放)を適切なタイミングで行うように変更されました。これにより、開発環境でのリロード待ちのデッドロックや、クリーンアップ処理がソケットクローズまで遅延する問題が解消されます。
- 変更内容の詳細
背景 / 問題点
従来の ActionDispatch::Executor の挙動:
- Executor の「完了」(
state.complete!)は以下のタイミングで行われていた:- Rack レスポンスボディの
closeコールバック - もしくはサーバが提供する
rack.response_finishedコールバック
- Rack レスポンスボディの
- WebSocket アップグレード (HTTP 101) や Rack hijack (
rack.hijack_io) を使ったストリーミングでは:- レスポンスボディの
closeはソケットクローズ時まで呼ばれない - つまり Executor の完了もコネクション終了まで遅延する
- レスポンスボディの
これに起因する問題:
- 開発環境では reloader が
before_class_unloadでShareLockの:unload用の排他ロックを取りに行くが、- あるリクエストが reloader share を保持したまま長時間生き続けると(WebSocket など)
:unloadロックが取れず、コードリロードがブロックされる
- 特に fiber スケジューラなサーバ(Falcon + async-cable など)の場合:
- リクエスト用 fiber がそのまま hijacked ソケットの read loop を続けるため
- その fiber 上で reloader share がコネクション存続中ずっと保持される
- Puma では:
- Action Cable の hijack がワーカープールスレッドに処理を委譲するため
- 元のリクエストスレッドは解放され、この問題が表面化しにくい
本番環境では reloader が動いていないためデッドロックは起こらないものの:
- Executor のライフサイクルが「リクエスト〜レスポンス」ではなく「コネクション存続期間全体」に引き伸ばされており、
- クエリキャッシュのクリーンアップ
CurrentAttributesのリセット- アプリ側で登録した
to_completeコールバック
などが「ソケットが閉じた時点」でしか実行されない、という設計上望ましくない状態になっていた。
今回の変更の要点
1) hijack / HTTP 101 の検出と早期 finalize
ActionDispatch::Executor#call 内で、レスポンスが hijack されたかどうかを検知し、レスポンスボディの close を待たずに executor state を完了させるようになりました。
新たに導入されたプライベートメソッド:
def hijacked?(status, headers, env)
status == 101 || env["rack.hijack_io"]
endenv["rack.hijack_io"]の存在 … 実際に hijack が行われたことを示す- HTTP ステータス 101 (Switching Protocols) … WebSocket などのプロトコルアップグレード
注意点として:
rack.hijack?は「サーバが hijack をサポートしているか」を示すだけで、「実際に hijack されたか」は分からないため検出条件には使っていません。
Executor#call の中では、以下のような流れになります(疑似コードイメージ):
def call(env)
state, to_complete = run_callbacks # ここで executor state を開始
finalize = -> do
# idempotent(複数回呼ばれても一度しか効かない)にしている
state.complete!
to_complete.each(&:call)
end
status, headers, body = @app.call(env)
if hijacked?(status, headers, env)
# hijack / 101 の場合はここで即座に executor を完了
finalize.call
else
# rack.response_finished / body.close 経由の完了などに finalize をフック
attach_finalize_to_response_lifecycle(finalize, status, headers, body, env)
end
[status, headers, body]
rescue => e
finalize.call
raise
end※実際の実装は多少異なる可能性がありますが概念的にはこのような構造です。
2) finalize クロージャによる完了処理の一元化
これまで state.complete! が複数箇所から直接呼ばれていましたが、今回:
finalizeというクロージャを 1 つ作りstate.complete!to_completeコールバック群の実行
- をひとまとめにして、idempotent(何度呼んでも一度しか効かない) にしています。
これにより:
- hijack 検出時の「早期 finalize」
rack.response_finished- レスポンスボディの
close - 例外発生時の ensure 的なパス
など、複数の経路から finalize が呼ばれても二重に complete! されないようになります。
3) Action Cable などの長寿命接続との整合性
- Action Cable などの長寿命 hijack consumer は、既に
before_class_unloadフックで「クラスアンロード前に接続を閉じる」ことによってリロード安全性を確保しています。 - 今回の変更で「hijack タイミングで reloader share を解放」しても、
- 彼らはアンロード前にクリーンに切断されるため
- 定数が消える/差し替わることによる不整合は生じない、と判断されています。
4) 関連 PR #57423 との関係
- #57423 では
ActiveSupport::Concurrency::ShareLockがisolation_levelを正しく考慮するよう修正されています。 - これがないと、fiber スケジューラなサーバで「全 request fiber を同一オーナーとみなす」バグがあり、今回の PR が解決するはずのデッドロックが別の形で隠れてしまうとのことです。
- 両者はロジック上は独立ですが、開発環境で fiber ベースサーバを使う場合はどちらも取り込まれていることが望ましいです。
- 影響範囲・注意点
影響範囲
- 対象:
ActionDispatch::Executorを利用する Rack アプリ(ほぼ全ての Rails アプリ)- WebSocket / Action Cable / 任意の Rack hijack (
rack.hijack_io) を用いる機能
- 振る舞いの変化:
- これまで「ソケットクローズ時」だった以下の処理が、「HTTP 101 / hijack 応答を返した時点」で実行されるようになります:
- Active Record query cache のクリーンアップ
CurrentAttributesのリセットconfig.action_dispatch.executor_to_completeなどで登録したto_completeコールバック
- これまで「ソケットクローズ時」だった以下の処理が、「HTTP 101 / hijack 応答を返した時点」で実行されるようになります:
期待されるメリット
- 開発環境:
- 長時間開いた WebSocket / hijacked 接続があっても、リロードのための
ShareLockの:unloadロック取得がブロックされにくくなる - Falcon + async-cable のような fiber スケジューラ環境でのデッドロックが解消される
- 長時間開いた WebSocket / hijacked 接続があっても、リロードのための
- 本番環境:
- Executor のライフサイクルが「リクエスト単位」に戻り、クエリキャッシュ等のリソース管理が従来想定により近い形になる
CurrentAttributesなどスレッド/リクエストローカルな状態が、長寿命接続に引きずられずリクエスト完了時にリセットされる
注意点
- WebSocket / hijack コネクション内で「Executor の存在を前提としたリクエストスコープの状態」に依存していた場合:
- 例えば、Executor のライフタイムにのみ依存して
Currentを使い回していたりすると、 - 今回からは アップグレード後すぐに Executor が完了するため、その前提は崩れます。
- 通常の Action Cable の利用パターンでは問題ない設計ですが、自前で複雑な hijack をしている場合は確認が必要です。
- 例えば、Executor のライフタイムにのみ依存して
- fiber ベースのサーバで ShareLock を正しく使いたい場合:
- #57423 の
ShareLock修正も合わせて取り込まれていることを前提に挙動検証を行うとよいです。
- #57423 の
- 参考情報 (あれば)
- 該当 PR:
- 関連 PR(ShareLock の修正):
- 関連コード:
actionpack/lib/action_dispatch/middleware/executor.rbactionpack/test/dispatch/executor_test.rb
- CHANGELOG:
actionpack/CHANGELOG.mdにこの振る舞い変更が追記されています。
#57397 [ci skip] Clarify Active Job queue backends
マージ日: 2026/5/21 | 作成者: @sandstrom
- 概要 (1-2文で)
Active Job で利用できるキューイングバックエンドに関するガイド文書を整理し、「どのバックエンドを・なぜ挙げているのか」をより明確にしたドキュメント変更です。組み込みアダプタの今後の扱い(外部抽出の方針)とも整合するように説明とリンク構成が調整されています。
- 変更内容の詳細
※このPRは guides/source/active_job_basics.md のみを編集するドキュメント変更です。コードや挙動の変更はありません。
主なポイントは以下です。
Active Job の「キュー backend 一覧」の説明を明確化
- 旧来の文章だと「なぜこのバックエンドが載っていて、他は載っていないのか」が分かりづらかったため、
- 「このリストはあくまで代表的な実装例である」
- 「Rails本体に同梱される・されない、とは必ずしもイコールではない」
といったニュアンスが分かるように文言が整理されたと思われます。
- 「将来的にビルトインアダプタは別gemに切り出す計画」であることがガイドに書かれているため、その方針と矛盾しないような書き方に調整されています。
- 旧来の文章だと「なぜこのバックエンドが載っていて、他は載っていないのか」が分かりづらかったため、
バックエンド一覧の並び順を GitHub スター数ベースに変更
- Sidekiq, Resque, Delayed Job, Que, GoodJob など、代表的なバックエンドの列挙順が「GitHub スター数を元にした人気順」に再ソートされています。
- Rails 公式として「特定ベンダー推し」に見えないよう、かつ利用者が直感的に「よく使われている順」に見えるような並びにした意図が説明されています。
ドキュメント中のリンクを更新
- 各バックエンドの公式ドキュメントや GitHub リポジトリへのリンクが最新のものに更新されています。
- 古くなっていたURLやリダイレクトされるリンクなどが、より適切なものに差し替えられたと考えられます。
位置づけや選定基準に関する説明のトーン調整
- 「Rails が公式にサポートするのはこのバックエンドだけ」と誤解されないような書き方に寄せられている可能性が高いです。
- 「他にも多くのバックエンドが存在する」「このリストはあくまで代表的なもの」という趣旨が明示的になっているはずです。
(このPRにはコード例の追加・変更はほぼ含まれていないと思われるため、サンプルコードは割愛します。)
- 影響範囲・注意点
ランタイム挙動への影響はなし
- 実装コードには一切手が入っていないため、Active Job の挙動・API・対応アダプタに即時の変更はありません。
- テストやCHANGELOGの更新も不要と見なされている純粋なドキュメント修正です。
今後の「ビルトインアダプタ外出し」への布石
- ガイド文言が「ビルトインアダプタは別gemとして抽出する計画」と整合するよう調整されているため、将来的に:
activejobが最小限のインタフェースのみを持ち- Sidekiq などのアダプタは外部gemとして提供
という構成になっても、ガイドの意味が破綻しないようになっています。
- すでにActive Jobを使っているプロジェクトは、このPR自体で直ちに何かを変更する必要はありませんが、
「アダプタは今後も独立したgemとして管理されていく」ことを意識しておくとよいです。
- ガイド文言が「ビルトインアダプタは別gemとして抽出する計画」と整合するよう調整されているため、将来的に:
バックエンド選定時の参考情報としての読みやすさ向上
- 「どれがよく使われているのか」「どのリンクを見れば良いか」が整理されたので、
新しくバックエンドを選ぶ人や、既存バックエンドからの移行を検討している人にとってはガイドが使いやすくなっています。 - ただし、並び順がスター数ベースになったことで「Rails公式の推奨度」とは無関係である点には注意が必要です。
あくまで「人気の目安」であり、自身のユースケース(信頼性、性能、ライセンス、キューの特性など)に合わせて選ぶ必要があります。
- 「どれがよく使われているのか」「どのリンクを見れば良いか」が整理されたので、
- 参考情報 (あれば)
該当ファイル:
guides/source/active_job_basics.md
→ Active Job の概要・使い方・バックエンド一覧が記載される公式ガイド章関連する将来方針(ガイド上で触れられているもの)
- Active Job の「ビルトイン」アダプタを別gemとして切り出す計画
- そのため、Active Job は「統一インタフェース」としての役割がより明確になり、
各バックエンドとは疎結合な関係になる方向性が示唆されています。
このPRは「実装ではなくガイドを読んだときの理解を改善する」ことにフォーカスした変更なので、
開発者としては「Active Jobバックエンド選定や構成を見直す際に、最新ガイドを読み直す」といった対応があれば十分です。
#57170 Fix MirrorService#mirror IntegrityError with nil checksum
マージ日: 2026/5/21 | 作成者: @Mordorreal
- 概要 (1-2文で)
Active Storage のMirrorService#mirrorが、チェックサムがnilの場合でも整合性チェックを実行してしまいActiveStorage::IntegrityErrorを投げていた不具合を修正した PRです。チェックサムが存在する場合にのみ整合性検証を行うようにし、ドキュメントの仕様と実装を一致させています。
- 変更内容の詳細
問題の背景
ActiveStorage::Service::MirrorServiceは、複数のストレージサービスへ同時に書き込むためのサービスです。MirrorJobからmirror(key, checksum: ...)が呼ばれる際、track_variants: falseな場合などにchecksum: nilで呼ばれるケースがあります。- これまでの実装では、
primary.open(key, checksum: checksum)の中でverify_integrity_ofが呼ばれ、実ファイルの MD5 と引数のchecksumを比較していました。 checksumがnilの場合でも比較が行われるため、常に不一致となりActiveStorage::IntegrityErrorが発生していました(#57164)。
修正内容
MirrorService#mirror 内の primary.open 呼び出しに verify オプションを追加し、チェックサムが存在する場合のみ検証するように変更しています。
変更前(イメージ):
primary.open(key, checksum: checksum) do |io|
# ...
end変更後:
primary.open(key, checksum: checksum, verify: checksum.present?) do |io|
# ...
endポイント:
checksum.present?がtrueのときだけverifyが有効になり、MD5 の整合性チェックが走ります。checksumがnilまたは空の場合はverify: falseとなり、整合性チェック自体をスキップします。- これは「チェックサムが存在する場合のみ整合性チェックを行う」という Active Storage のドキュメントに記載された仕様と一致します。
テストと CHANGELOG
activestorage/test/service/mirror_service_test.rbに、checksum: nilでmirrorしてもIntegrityErrorが発生しないことを確認するテストが追加されています(+15行)。activestorage/CHANGELOG.mdに今回のバグ修正についての項目が追加されています(+5行)。
- 影響範囲・注意点
- 対象: Active Storage を利用しており、
MirrorServiceを使っている環境、特にtrack_variants: false等の設定でチェックサムが保存されない・渡されないケース。 - 挙動の変化:
- 以前:
checksum: nilでも実データとの MD5 比較を行おうとして、必ずActiveStorage::IntegrityErrorが発生。 - 以後:
checksum: nilの場合は整合性チェックをスキップし、通常通りミラーリングを続行。
- 以前:
- 互換性:
- 「チェックサムがない場合は整合性チェックしない」というもともとの設計・ドキュメントに沿った修正のため、意図せぬ互換性破壊は基本的にありません。
- ただし、「チェックサムを渡さないことであえて IntegrityError を発生させる」といった特殊な前提で依存しているコードがあれば、その前提は崩れます(通常はない想定)。
- セキュリティ・信頼性:
- そもそもチェックサムが保存・渡されていないケースでは、これまでも「正しい検証」はできていなかった(常に失敗していた)ため、今回の修正は「ドキュメントに沿ってチェック自体を行わない」方向です。
- データの整合性を確実に検証したい場合は、アップロード時にチェックサムを正しく保存し、
MirrorServiceにも渡るような設定を維持する必要があります。
- 参考情報 (あれば)
- 対応 issue: #57164
- 関連 PR: #57171
- 修正対象クラス:
ActiveStorage::Service::MirrorService - 仕様上の前提: 「チェックサムが存在する場合にのみ整合性チェックを行う」(Active Storage ドキュメント記載の挙動)
#57223 Fix duplicate where conditions in create_or_find_by
マージ日: 2026/5/21 | 作成者: @y-dashev
- 概要 (1–2文で)
create_or_find_byがユニーク制約違反 (RecordNotUnique) 後のリトライ時に重複したwhere条件を生成してしまい、結果としてRecordNotFoundが起き得るバグを修正した PR です。where(...).find_by!(...)をrewhere(...).take!に置き換えることで、競合時の再検索ロジックを正しく動作させています。
- 変更内容の詳細
バグの背景
create_or_find_by は、概ね次のような流れで動きます:
- 渡された attributes で INSERT を試みる
- DB のユニーク制約に引っかかって
RecordNotUniqueが起きたら - 既に存在しているレコードを SELECT で取り直す
元の実装では、そのリトライ用の SELECT が以下のような形でした(イメージ):
relation.where(attributes).find_by!(attributes)relation 側にすでに何らかの where が付いていたり、create_or_find_by 自身の前半の処理で scope が汚れていた場合に、where(attributes) と find_by!(attributes) の両方で同じ条件が積み上がり、結果として「条件が二重についた不正な Relation」が生成されることがありました。
その結果、本当は存在するはずのレコードを見つけられずに RecordNotFound が発生する、というバグになっていました。
修正内容
1. where → rewhere への変更
where(attributes) を rewhere(attributes) に変更:
# 変更前(概念的)
relation.where(attributes).find_by!(attributes)
# 変更後(概念的)
relation.rewhere(attributes).take!rewhere は、同じキーを持つ既存の where 条件を置き換えるメソッドです。
これにより、リトライ時には「その attributes に関する条件はすべて上書きされる」ため、以前に積み上がってしまった衝突する条件(重複 where)をクリーンにできます。
結果:
- 同じカラムに対して複数の矛盾する
whereが積み上がることを防止 - 競合後の再検索クエリが、意図した attributes に対してシンプルな条件になる
2. find_by! → take! への変更
find_by! を take! に変更しています。
find_by!(attributes)は与えた attributes を再度条件に含めてクエリを組み直します。- すでに
rewhere(attributes)済みの Relation からレコードを1件取るだけで良いので、単にtake!で十分です。
つまり:
- 条件の指定は
rewhere(attributes)に一本化 - レコード取得は
take!で「その Relation から1件必ず取る」ことに限定
これにより「条件指定の責務」と「レコード取得の責務」が分離され、スコープの衝突が起きにくくなっています。
3. テスト修正
test_find_or_create_by_race_condition のスタブを :find_by! から :take! に変更:
- 実装が
find_by!を呼ばなくなったため、テストのモック・スタブも合わせて変更 - レースコンディション(ユニーク制約競合)の再現テストが、新実装に対しても妥当な形で動くように調整
4. CHANGELOG 更新
activerecord/CHANGELOG.md に、このバグ修正についての記述が追加されています。
今後のリリースノートに、この挙動変更(バグ修正)が載る想定です。
- 影響範囲・注意点
影響対象:
ActiveRecordのcreate_or_find_byを使っていて- かつ DB のユニーク制約競合が実際に発生し得るケース
- かつ Relation にすでにスコープや
whereが積まれているケース(例:User.active.create_or_find_by(email: ...)のようなもの)
ポジティブな影響:
- 競合発生時に本来は存在しているレコードを見つけられず、
RecordNotFoundが不定期に飛ぶ、という不安定な挙動が解消されます。 - スコープが複雑な Relation 上でも、
create_or_find_byのリトライ動作がより堅牢になります。
- 競合発生時に本来は存在しているレコードを見つけられず、
挙動の変化の有無:
- 意図としては「バグ修正」であり API の表面上の仕様変更ではありません。
- ただし、これまでバグに依存した挙動(たとえば「競合時にたまたま
RecordNotFoundが出て rescue していた」など)に依存している場合は挙動が変わる可能性があります。 - 正常系・期待される使い方においては、より直感的な動作になります。
パフォーマンス:
- クエリの本質的な回数は変わらず、形が変わるだけなので目立った性能劣化・向上はありません。
- 重複条件を減らすという意味では、極端なケースでクエリプランがむしろシンプルになる可能性はあります。
- 参考情報 (あれば)
- 対応 issue: Fix #57192
- 関連メソッド:
Relation#rewhere: 既存の条件をキー単位で置き換えるwhereRelation#take!: レコードを1件取得し、なければActiveRecord::RecordNotFoundを発生させる
- 修正箇所:
activerecord/lib/active_record/relation.rbactiverecord/test/cases/relations_test.rbactiverecord/CHANGELOG.md
#57424 Update SOLID_QUEUE_IN_PUMA handling in puma.rb template
マージ日: 2026/5/21 | 作成者: @jordan-brough
- 概要 (1-2文で)
Rails が生成するconfig/puma.rbのテンプレートにおいて、ENV["SOLID_QUEUE_IN_PUMA"]を真偽解釈するロジックが変更され、nil/ 空文字列""/"0"/"false"を「false 相当」と扱うようになりました。これにより、環境変数に"false"や"0"を設定したときに、直感に反して機能が有効になってしまう問題を避けられます。
- 変更内容の詳細
何が変わったか
rails new で生成される、または rails app:update で更新される config/puma.rb テンプレート (railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt) 内で、ENV["SOLID_QUEUE_IN_PUMA"] の扱いが以下のような「独自の真偽値判定」に変更されています。
おおまかには、以下のようなロジックになります(疑似コード):
solid_queue_in_puma_env = ENV["SOLID_QUEUE_IN_PUMA"]
SOLID_QUEUE_IN_PUMA = ![nil, "", "0", "false"].include?(solid_queue_in_puma_env)もしくはほぼ等価な条件分岐で、
- 以下の値は 「無効(false)」として扱う
nil(環境変数が未設定)""(空文字)"0""false"
- それ以外の値(例:
"1","true","yes","on","anything"など)は 「有効(true)」として扱う
という挙動になります。
なぜこの変更が必要か
config/deploy.yml にはデフォルトで以下のような設定コメントがあります:
# Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
# When you start using multiple servers, you should split out job processing to a dedicated machine.
SOLID_QUEUE_IN_PUMA: trueこれを見た開発者が、「無効化したいから false と書けばいいだろう」と考えやすい一方で、Rails 側のコードが「文字列としての ENV["SOLID_QUEUE_IN_PUMA"] が 存在すれば true」と解釈していると、"false" という文字列でも true 扱いになり、意図に反して Solid Queue Supervisor が Puma 内で動作し続けてしまうという落とし穴がありました。
今回の変更で、"false" や "0" を「false」として扱うようになり、環境変数の直感的な運用がしやすくなります。
- 影響範囲・注意点
影響対象
- 新規に
rails newで作成するアプリ- 生成される
config/puma.rbは、最初からこの新しい真偽判定ロジックを持ちます。
- 生成される
- 既存アプリで
rails app:updateを実行した場合config/puma.rbが更新候補として出てきて、差分を取り込むかどうかを選ぶ形になります。
挙動が変わるケース
これまで:ENV["SOLID_QUEUE_IN_PUMA"] が以下のような値の場合でも、単に「設定されている」という理由だけで true 扱いされていた可能性があります(以前の実装次第ですが、多くの「簡易な実装」ではこの挙動になりがちです)。
ENV["SOLID_QUEUE_IN_PUMA"] = ""ENV["SOLID_QUEUE_IN_PUMA"] = "0"ENV["SOLID_QUEUE_IN_PUMA"] = "false"
これから:
上記の値は false 扱いになります。その結果:
- 「
""/"0"/"false"を入れて あえて有効にしていた」ようなケースがもしあれば、
→ この PR に相当する変更を取り込んだ後は無効になってしまいます。
ただし PR 作成者も述べている通り、そのような運用をしているケースは極めて稀と想定されています。
今後、どう設定すべきか
- 有効にしたい場合
- 推奨:
"true"もしくは"1"など、true として扱われることが明らかな値を設定する - 例:bash
SOLID_QUEUE_IN_PUMA=true # または SOLID_QUEUE_IN_PUMA=1
- 推奨:
- 無効にしたい場合
"false"や"0"、空文字を設定するか、そもそも環境変数を設定しない- 例:bash
# 無効にするいずれかの方法 SOLID_QUEUE_IN_PUMA=false # または SOLID_QUEUE_IN_PUMA=0 # または unset SOLID_QUEUE_IN_PUMA
マージ後の実務的な注意点
rails app:updateを実行した際にconfig/puma.rbに差分が出る場合があるため、- Git diff を確認し、
SOLID_QUEUE_IN_PUMA周りの処理をどうするかを明示的に判断する必要があります。
- Git diff を確認し、
- CI / 本番環境で
SOLID_QUEUE_IN_PUMAを設定している場合、- 現在どんな値を設定しているか(特に
"false"/"0"/ 空文字)を確認し、 - この PR 相当の変更を取り込むタイミングで挙動が変わらないかをチェックするべきです。
- 現在どんな値を設定しているか(特に
- 参考情報 (あれば)
- 本 PR:
- 関連 PR:
これらの PR は、Solid Queue と Puma の連携や、Rails におけるジョブ処理の標準的な構成(Web サーバ + ジョブ実行プロセスの分離/統合)に関する改善の流れの一部です。
#57284 Don't bump lock_version on records during blob analysis
マージ日: 2026/5/21 | 作成者: @g-pavlik
- 概要 (1-2文で)
Active Storage の非同期分析(ActiveStorage::AnalyzeJob)がlock_versionを意図せず更新してしまい、楽観ロック中のレコードでActiveRecord::StaleObjectErrorが起きる問題を修正する PR です。
Blob 分析時の関連レコードtouchではupdated_atは更新しつつ、lock_versionの更新だけを抑制する仕組みが Active Record 側に追加されています。
- 変更内容の詳細
背景問題の整理
- Active Storage の
ActiveStorage::AnalyzeJobは Blob のメタデータ (Blob#metadata) だけを書き換える想定。 - しかし、Blob には
touch_attachmentsによる「親レコードへの cascade touch」があり、以下のような流れになっていた:- Blob 分析 → Blob#metadata 更新
- Blob#touch_attachments により、添付元モデル (および
belongs_to ..., touch: trueな親) をtouch - その
touchが楽観ロック (lock_versionカラム) を持つレコードに対してlock_versionをインクリメント
- その結果:
- ユーザーがフォーム編集中にファイルを添付
- その直後に分析ジョブが走って親レコードの
lock_versionが上がる - ユーザーが古い
lock_version付きで更新を送る →ActiveRecord::StaleObjectError - 実際にはアプリ的な意味での「競合した更新」は発生していないのにエラーになる
config.active_storage.touch_attachment_records = false で cascade 自体を止めることはできるが、それをすると #45567 で問題になった「キャッシュが更新されない」バグが再発するため、不十分な回避策だった。
新しい仕組み: preserve_lock_version_on_touch
ActiveRecord の楽観ロックモジュールに以下が追加されました:
ActiveRecord::Locking::Optimistic.preserve_lock_version_on_touch do
record.touch
endこのブロック内で発生した touch については:
lock_versionを 更新しない(インクリメントしない)UPDATEのWHERE句にlock_version = ?を 含めない- ただし
updated_atなど通常のtouch対象カラムは 更新される
これにより:
- 親レコードのキャッシュキーや
updated_atベースのキャッシュはちゃんと更新される - 楽観ロックの競合検出には影響を与えない(=「内容が変わっていないのに lock_version だけ上がる」ケースを防ぐ)
実装のポイント
_touch_rowの変更- 通常、
touch時はlock_versionも「変更された属性」に含めて dirty tracking する。 preserve_lock_version_on_touchが有効な場合、_touch_rowがlock_versionを@_touch_attr_namesに追加しないようにしている。- これにより、「
touchの一環としてlock_versionを変更する」という動作自体を抑制。
- 通常、
_update_rowの変更Locking::Optimistic#_update_row内で、attempted_action == "touch"かつ「preserve_lock_version フラグが立っている」場合は、楽観ロック用のロジックをスキップして、Persistence#_update_rowにフォールバックする。- フォールバック先では
lock_versionをインクリメントせず、WHERE lock_version = ?も付かない、通常の更新として実行される。 attempted_action == "touch"でのみ有効にしているため、通常のsaveやupdateではこれまで通りロックが効く。
Deferred touch (
touch_later) への対応belongs_to ..., touch: trueなどは「コミット前コールバック (before_committed!) で親をtouch_laterする」という非同期タッチになる。preserve_lock_version_on_touchのブロックはtouchを呼び出したスレッドでのみ有効なので、そのままだとコミット時点ではブロックのスコープを外れてしまう。- そこで、新たに prepended された
DeferredTouchモジュールで:touch_later呼び出し時に「lock_version を preserve したいかどうか」の意図をレコードインスタンス上のフラグとして保存- 実際に defer された
touchが実行されるタイミングで、そのフラグを参照してロックバージョンの preserve を適用
- このフラグは一度使ったら消費される(使い回しされない)よう考慮されている。
Active Storage Blob 側の変更
ActiveStorage::Blob#touch_attachments が以下のように変更されています(概念的な例):
def touch_attachments
ActiveRecord::Locking::Optimistic.preserve_lock_version_on_touch do
attachments.includes(:record).find_each do |attachment|
attachment.record&.touch if attachment.record&.respond_to?(:touch)
end
end
end(実際のコードは多少異なる可能性がありますが、意図としてはこのように「Blob からの cascade touch 全体を preserve ブロックで囲む」形です。)
これにより:
- Blob の分析 → Blob 更新 → 親レコード
touch→ 親レコードのupdated_at/ キャッシュキーは更新 - しかし親レコードの
lock_versionは変わらない
という挙動になるため、フォーム編集中ユーザーの更新が不必要に StaleObjectError にならなくなります。
テストとドキュメント
activerecord/test/cases/locking_test.rbにpreserve_lock_version_on_touchの挙動を検証するテストが追加。activestorage/test/models/blob_test.rbに、Blob 分析時に lock_version が変わらないことを確認するテストが追加。activestorage/CHANGELOG.mdに今回の変更が追記されています。
- 影響範囲・注意点
影響範囲
ActiveRecord
- 楽観ロック (
locking/optimistic) の内部実装に新しいコードパスが追加。 - 通常の
save/update/destroyなど、attempted_action != "touch"な更新はこれまで通りの挙動。 preserve_lock_version_on_touchを明示的に使うコードのみ新挙動の影響を受ける。
- 楽観ロック (
Active Storage
- 標準の Blob 分析ジョブ (
ActiveStorage::AnalyzeJob) 経由で発生する親レコードtouchは、今後lock_versionを変えない。 config.active_storage.analyzeの利用がある通常のアプリでは、この変更によりフォームの「謎のStaleObjectError」が解消される可能性が高い。
- 標準の Blob 分析ジョブ (
注意点・開発者視点でのポイント
- 「
touchが lock_version を上げない」という挙動はpreserve_lock_version_on_touchブロックの中だけ で有効。 - 独自に以下のようなコードを書いている場合、今回の機能を流用できる可能性があります:
- 「キャッシュ無効化のためだけに
touchしているが、実際の競合検出には使いたくない」箇所 - 「自動ジョブが
touchするだけでユーザー更新と競合扱いされる」のを避けたい箇所
- 「キャッシュ無効化のためだけに
- 一方で、「
touchをトリガーに実質的な更新だと見なしてほしい」ワークフロー(例: lock_version の変化を監査に使っている等)がある場合は、このブロックを安易に使わないほうがよいです。 - 既存アプリで特別な対応をしなくても、Rails をこのコミット以降に上げれば Active Storage 周りの問題は自動的に改善されます(
config.active_storage.touch_attachment_recordsを false にしていなければ)。
- 参考情報 (あれば)
- 修正対象の issue:
- #55764 – Active Storage の非同期分析に起因する
ActiveRecord::StaleObjectError
- #55764 – Active Storage の非同期分析に起因する
- 挙動ドキュメント PR:
- #55799 – 現象のドキュメント化(この PR が入れば不要になる見込み)
- cascade touch の導入経緯:
- #45567 / commit 294c271062 – Blob 分析後もキャッシュを更新するための cascade 追加
- 既存設定オプション:
- #49723 –
config.active_storage.touch_attachment_records追加(今回はこれをオフにせずに問題を解消するアプローチ)
- #49723 –
#57422 Fixes to query command
マージ日: 2026/5/21 | 作成者: @andyjeffries
- 概要 (1-2文で)
rails queryコマンドのバグ修正 PR で、
- 読み取り専用接続をバイパスして書き込みできてしまう危険な挙動
- 実用上「使えない」レベルの挙動不良
を修正し、安全かつ実用的に使えるようにしたものです。
- 変更内容の詳細
※PR本文から分かるのは主に「何を直したか(意図)」であり、「どう直したか(具体的実装)」は diff がないと細部までは追えません。以下は PR 説明・変更ファイルから推測できるレベルでまとめています。
2-1. 対応しているバグ
PR は以下 2 件の Issue を同時に解決しています。
- #57419:
rails queryの「実用上の致命的な不具合」- 作成者が「LLM から
rails queryを使いたいが、これがあると現実的に無理」と表現しているため、- 入出力の扱い(プロンプトや結果表示)
- コマンドの引数解釈や終了コード
- あるいはインタラクティブな利用方法
のどれかが壊れていた可能性が高いです。
- 作成者が「LLM から
- #57420: 読み取り専用 DB 接続がバイパスされる危険な挙動
rails query実行時、本来readonlyな接続に対しても書き込み系クエリが実行できてしまう問題を修正しています。- これは「本番のレプリカに対して誤って更新をかける」といった事故につながるので、セキュリティ・運用上重大です。
PR の説明文から:
one DANGEROUS bypassing of the database connection being readonly #57420
one real deal breaker of an aspect of usage that is broken #57419
と明記されており、意図としては
rails queryは DB の接続モード(特に read-only)を正しく尊重する- CLI として実際に日常的に使える UX / 挙動にする
という 2 点を達成する修正です。
2-2. 変更されたファイル
railties/lib/rails/commands/query/query_command.rb
+14 / -5 行の変更が入っています。
このファイルは
rails queryサブコマンドの本体ロジックを持つクラス (例:Rails::Command::QueryCommand) が定義されている場所です。ここで行われているであろう主な修正は次のようなものが考えられます:
DB 接続の扱いの修正
ActiveRecord::Base.connectionではなく、
Rails が「現在のコマンド・環境に対応する接続」を適切に解決する仕組みを用いている可能性があります。- あるいは、接続に対してrubyのようなチェックを行い、書き込みクエリの実行を拒否する、または例外を投げる形に変えた可能性があります。
connection.readonly? # あるいは role / type を見る
クエリ実行方法・例外処理の調整
- 失敗時のメッセージや終了コードがツール連携(LLM など)で扱いやすいように整理されている可能性があります。
- これにより、「使い物にならない」問題 (#57419) が解消されていると考えられます。
入出力の改善
- 標準入力・標準出力の扱いを、非対話的な利用(例:
echo "SELECT ..." | rails query)でも安定して動くように調整している可能性があります。 - LLM 連携を意識しているため、出力のフォーマット・エラー時の出力なども見直されている可能性があります。
- 標準入力・標準出力の扱いを、非対話的な利用(例:
具体的なコード例は diff がないと正確に書けませんが、イメージとしては以下のようなロジックが追加・修正されている可能性があります:
# イメージ: 書き込みクエリを read-only 接続で実行しないようチェック
def execute(query)
connection = ActiveRecord::Base.connection
if connection.respond_to?(:read_only?) && connection.read_only? && write_query?(query)
raise "Cannot execute write query on a read-only connection"
end
connection.exec_query(query)
endrailties/test/commands/query_test.rb
+52 行でテストが大幅に追加されています。
追加されたテストの性質から、この PR の意図と仕様がある程度推測できます:
バグ再現テスト + 修正確認
- #57419 で報告されていた「使えない挙動」が再現されるケースをテスト化。
rails query実行時の入出力や終了コード、例外の出方などを検証しているはずです。
read-only 接続の尊重テスト
ActiveRecordの接続を read-only に設定した状態でrails queryを実行し、- 書き込みクエリ(
INSERT,UPDATE,DELETEなど)がブロックされる/失敗すること - 読み取り専用クエリ(
SELECT)は通ること
を確認するテストが追加されていると考えられます。
- 書き込みクエリ(
- これにより、#57420 に対する回 regress しない保証を提供しています。
- 影響範囲・注意点
3-1. 影響範囲
- 影響を受けるのは
rails queryコマンドのみ です。 - アプリケーションコード(コントローラやモデルなど)や他の Rails コマンド (
rails consoleなど) には影響しません。
ただし、rails query を使っている運用・ツール連携では次の挙動が変わります:
read-only 接続での書き込みクエリが通らなくなる
- これまで「たまたま通っていた」ケースは、今後は失敗あるいはエラーになり得ます。
- 本来は望ましい挙動ですが、既存のスクリプトやツールがこれに依存していると挙動が変わります。
エラー処理や終了コードの改善
- #57419 の修正により、失敗時の終了コードやメッセージが変わっている可能性があります。
- CI や自動化ツールから
rails queryを呼び出している場合、終了コードや出力を前提にしている部分があれば確認した方が安全です。
3-2. 注意点
- PR 作者も書いている通り、「バグ修正ではあるが、挙動の変更はそれなりに大きい」可能性があります。
- 特に以下のような運用をしているチームは影響確認がおすすめです:
- 本番で
rails queryを使って ad-hoc クエリを投げている - read replica / primary を役割分担していて、
readonly接続の上でrails queryを実行している - LLM や bot が
rails queryを経由して DB にアクセスしている
- 本番で
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57422
- 関連 Issue:
- 読み取り専用接続バイパス問題: https://github.com/rails/rails/issues/57420
- 実用上の「致命的な」挙動不良: https://github.com/rails/rails/issues/57419
この PR により、rails query は
- DB 接続の read-only 設定をきちんと守り
- CLI ツールや LLM 連携からも実用的に扱える
コマンドとして使えるようになる、という位置付けの変更です。
#54930 Add referential integrity note in AR::FixtureSet docs [ci skip]
マージ日: 2026/5/21 | 作成者: @danimashu
- 概要 (1-2文で)
ActiveRecord::FixtureSetのAPIドキュメントに、「フィクスチャ読み込み時にRailsがリレーショナル整合性(外部キー制約など)を一時的に無効化する」ことを説明する注意書きが追加されました。これにより、特にDBトリガーや外部キー検証に依存しているテストで、意図せず「非整合」なテストデータが生じうる点への注意喚起が強化されています。
- 変更内容の詳細
追加された内容の趣旨
修正は activerecord/lib/active_record/fixtures.rb に対する ドキュメントコメントの2行追加のみ です。機能的な挙動は一切変わっていません。
追加されたドキュメントの要点は次のとおりです:
ActiveRecord::FixtureSetがテストごとにフィクスチャを削除・挿入するとき、- RDBMSのリレーショナル整合性機構(外部キー制約、PostgreSQLのトリガーなど)を一時的に無効化する
- そのため、「外部キーの依存関係どおりの順序でINSERTする必要がない」形でデータを投入している
- PostgreSQL:
ALTER TABLE ... DISABLE TRIGGER ALLを実行し、外部キー制約だけでなく ユーザー定義トリガーもすべて無効化 してフィクスチャをロードしているconfig.active_record.verify_foreign_keys_for_fixturesが有効な場合、後から外部キー制約のチェックを実行し、違反があれば検出する- ただし「その他のトリガーに基づくビジネスルール」は 検証されないまま になりうる
- MySQL:
- 外部キー再検証に相当する
config.active_record.verify_foreign_keys_for_fixturesの機構は まだ未実装 - したがってテストDBに外部キー不整合が紛れ込んでも、自動的には検出されない可能性がある
- 外部キー再検証に相当する
イメージしやすいサンプルシナリオ
PostgreSQL + トリガーを使っているケース:
CREATE FUNCTION check_business_rule() RETURNS trigger AS $$
BEGIN
-- 何らかのビジネスルール検証
IF NEW.status = 'paid' AND NEW.paid_at IS NULL THEN
RAISE EXCEPTION 'paid_at must be set if status is paid';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER orders_business_rule
BEFORE INSERT OR UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION check_business_rule();このようなトリガーに依存したビジネスルールがあっても、フィクスチャ読み込み時には
ALTER TABLE orders DISABLE TRIGGER ALL;
-- fixture の INSERT / DELETE
ALTER TABLE orders ENABLE TRIGGER ALL;という流れになるため、トリガーが発火せず、ビジネスルールを満たしていないテストデータが入ってしまう可能性があります。
外部キーについては、config.active_record.verify_foreign_keys_for_fixtures = true で後検証できますが、上記のような 任意トリガーに基づくルールは検証されない、という点が今回のドキュメントで明示的に注意喚起されるようになりました。
- 影響範囲・注意点
コードの動作は一切変わっていないため、既存アプリへのランタイム影響はありません。
ただし、開発者目線での「理解すべきポイント」「テスト戦略上の注意」は次のとおりです。
フィクスチャがDBの整合性検証をバイパスすることがある
- テスト環境のDBには、本番環境では許されないようなデータ(外部キー不整合やトリガー違反)が入り込む可能性があります。
- その結果、「本番では失敗するはずの操作がテストでは通ってしまう」といった齟齬が起こりえます。
PostgreSQLでの外部キー検証オプション
config.active_record.verify_foreign_keys_for_fixtures = trueをconfig/environments/test.rbに設定すると、フィクスチャ読み込み後に外部キー制約の違反チェックが走ります。- ただし、前述の通り「ユーザー定義トリガーのロジック」までは検証されません。
- トリガーに重めのロジックを載せている場合、テストでそのロジックを信頼しすぎないほうが安全です(モデルレイヤーやサービス層側での検証をテストする、など)。
MySQLでは外部キー再検証が未サポート
config.active_record.verify_foreign_keys_for_fixturesを指定しても、MySQLアダプタ側はまだ対応しておらず、外部キー不整合が残る可能性があります。- MySQLを使っている場合、フィクスチャの使い方にはより注意が必要です:
- テストデータ投入順序や、関連オブジェクトの存在をアプリ側ロジックやモデルバリデーションで担保する
- 必要なら、フィクスチャではなく FactoryBot などでアプリケーション層経由でデータを作る
トリガーに依存したビジネスルールのテスト
- トリガーによるチェックを前提にすると、「テスト中だけそのチェックが無効化されている」というギャップが発生します。
- そのルールをアプリケーションコード(モデルバリデーションやサービスオブジェクト)でも再現し、そちらをテストするか、
- あるいはトリガーを前提としたテストだけはフィクスチャではなく直接SQLを叩く/別の仕組みで準備する、といった戦略も検討が必要です。
- 参考情報 (あれば)
Rails本体の関連箇所やガイド:
PostgreSQLアダプタの参照整合性制御:
ALTER TABLE ... DISABLE TRIGGER ALL/ ENABLE TRIGGER ALL の実装
https://github.com/rails/rails/blob/1ac40035478cba2e21d698ffa57b43355f5bed7b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb#L7-L39- フィクスチャ後の外部キー検証 (
verify_foreign_keys_for_fixtures)
https://github.com/rails/rails/blob/1ac40035478cba2e21d698ffa57b43355f5bed7b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb#L41-L65
設定オプション
config.active_record.verify_foreign_keys_for_fixturesの説明:
https://guides.rubyonrails.org/configuring.html#config-active-record-verify-foreign-keys-for-fixturesTesting Guide でのフィクスチャと外部キーに関する記述:
https://github.com/rails/rails/blob/1ac40035478cba2e21d698ffa57b43355f5bed7b/guides/source/testing.md#L761-L767
このPR自体はドキュメントのみの微修正ですが、フィクスチャとDB制約の関係を正しく理解しておくと、「テストは通るのに本番で落ちる」系の問題を減らすのに役立ちます。
#52871 Allow to pass array values to .in_order_of
マージ日: 2026/5/21 | 作成者: @doits
- 概要 (1–2文で)
.in_order_ofにネストを含む配列を渡して、「値のグルーピング + グループの順序付け」ができるようになりました。これにより、ENUM などを使った状態カラムを「いくつかの状態をまとめて 1 グループとして扱いつつ、そのグループ自体の順序も制御する」といったクエリが書きやすくなります。
- 変更内容の詳細
これまでの .in_order_of
従来の .in_order_of は、基本的に「単純な値の並び順」を指定するためのものでした。
Post.in_order_of(:state, [:published, :archived, :canceled])
# => state が :published → :archived → :canceled の順に並ぶこのとき SQL は概ね次のような CASE 式になります。
ORDER BY CASE
WHEN "posts"."state" = 1 THEN 1
WHEN "posts"."state" = 2 THEN 2
WHEN "posts"."state" = 3 THEN 3
END ASC(ENUM の内部値に応じて 1, 2, 3 などに type cast される)
今回追加された機能: 値のグルーピング
この PR では、.in_order_of に 配列をネストして渡す ことで「同じ順位のグループ」を作れるようになりました。
PR の例:
Posts.in_order_of(:state, [[:published, :canceled], :archived])
.order(created_at: :desc)生成される SQL(概略)は以下のようになります:
SELECT "posts".*
FROM "posts"
WHERE "posts"."state" IN (1, 2, 3)
ORDER BY CASE
WHEN "posts"."state" IN (1, 2) THEN 1 -- :published, :canceled がグループ1
WHEN "posts"."state" = 3 THEN 2 -- :archived がグループ2
END ASC,
"posts"."created_at" DESCポイント:
- 第一引数は従来通り「カラム名」 (
:state)。 - 第二引数で「順序」を指定するが、ネストした配列を渡せる:
[:published, :canceled]→ 1つのグループとして扱われる(IN (1, 2)):archived→ 別グループ(= 3)
- その後に
.order(created_at: :desc)など通常のorderを追加することで
「まずグループ順、次にグループ内ではcreated_at DESC」という並びになる。
実用パターンの例
「公開中 or キャンセル済み」を上にまとめて表示したい:
rbPost.in_order_of(:state, [[:published, :canceled], :archived, :draft])「active 系の状態をまとめて、inactive を最後にまわす」など:
rbUser.in_order_of(:status, [[:active, :trial, :pending], :inactive])
実装的なポイント (コード上での変更点)
ファイル単位では次の 3 つが更新されています。
activerecord/lib/active_record/relation/query_methods.rb.in_order_ofの実装を拡張し、値として配列が渡された場合にはIN (...)句を使ったCASEを生成するように変更。- 具体的には、ORDER 用の条件式を組み立てる箇所で、
- 単一値:
WHEN column = value THEN n - 配列値:
WHEN column IN (value1, value2, ...) THEN nの2パターンを扱うようになっている。
- 単一値:
- また、これに伴いtype cast の単位が「配列全体」ではなく「配列の各要素」になっている。
activerecord/test/cases/relation/field_ordered_values_test.rb- 新機能向けのテストが追加。
- ネストした配列を渡したときに期待通りの SQL/並び順になることを検証。
activerecord/CHANGELOG.md- Active Record に「
.in_order_ofで配列をグルーピング指定に使えるようになった」旨を記載。
- Active Record に「
- 影響範囲・注意点
主な影響範囲
- Active Record の
.in_order_ofを利用しているコード全般が対象ですが、- 既存の「単純な配列」を渡すケースはそのまま動作します(後方互換性あり)。
- 新たに「配列の中に配列を含める」ケースがサポートされた形です。
注意点・懸念点(PR 内で述べられているもの)
type cast の方式変更
- 以前は「配列を 1 つの値として type cast」していた部分が、 今回の対応により「配列の各要素を個別に type cast」する挙動になります。
- 通常の整数/文字列/ENUM などでは問題になりにくいですが、
- DB カラム自体が「配列型」の場合(PostgreSQL の
integer[]など) - あるいはアダプタが「配列値を 1 単位で扱う」ことを仮定している場合 には、従来と挙動が変わる可能性があります。
- DB カラム自体が「配列型」の場合(PostgreSQL の
- そのようなケースを使っている場合は、
.in_order_ofの利用有無を確認した方が安全です。
パフォーマンス面
- PR 作成者は「
Array.wrapを使った方がコードはすっきりするが、パフォーマンスへの影響があり得る」と言及。 - 現行実装は主に
values.is_a?(Array)で分岐しており、あえてArray.wrapは使っていません。 .in_order_ofで多数の値・多数のグループを指定するとCASEが大きくなるため、DB 側の最適化・インデックス利用に注意が必要です。
- PR 作成者は「
複雑なネストは非想定
- 想定されているのは「1段階のネスト」(
[[:a, :b], :c, [:d, :e]]のような形)です。 - それ以上の多重ネスト(
[[:a, [:b, :c]]]など)は可読性も悪く、将来の互換性も保証しにくいため、避けるのが無難です。
- 想定されているのは「1段階のネスト」(
- 参考情報 (あれば)
- PR 本文の主なモチベーション:
.in_order_ofを使って「レコードをグルーピングし、そのグループをさらにソート」したいニーズから実装された。- 既存の Rails 機能で同じことを簡潔に表現する方法が見当たらなかったため、コアへの機能追加となった。
- 関連しそうな既存 API:
order(Arel.sql(...))で自前のCASE WHENを書けば類似のことは可能だが、Enum などの type cast を自分で面倒見る必要がある。.in_order_ofは Active Record の type system を使って value をキャストした上で CASE を組み立ててくれる点が利点。
#57416 [RF-Docs][ci-skip] Caching with Rails (#57107)
マージ日: 2026/5/21 | 作成者: @p8
- 概要 (1-2文で)
Rails ガイド「Caching with Rails」を現行のRailsの挙動・用語に合わせて大幅に書き直し、構成も整理しつつ、低レベルキャッシュ・フラグメントキャッシュ・Conditional GET・SQL/クエリキャッシュ・Solid Cache・非リクエストコンテキストでのキャッシュなどをより実践的かつ詳細に解説するドキュメント改訂PRです。機能追加や挙動変更ではなく、既存の仕組みを正しく理解してもらうためのドキュメント改善です。
- 変更内容の詳細
全体構成・導入の見直し
- ガイド冒頭(イントロ)と「このガイドを読み終えるとできるようになること」リストを、実際の内容とずれないように書き換え。
- 「Types of Caching」セクションの並びを整理し、Rails の主なキャッシュ手法を理解しやすい順序に変更:
- 低レベルキャッシュ(
Rails.cache) - フラグメントキャッシュ
- Conditional GET(HTTP キャッシュ)
- SQL キャッシュ(= Active Record クエリキャッシュ)
- 低レベルキャッシュ(
低レベルキャッシュ (Rails.cache) の拡充
次のトピックについて詳しい説明・サンプルが追加/拡充されています。
Rails.cache.fetchの挙動キャッシュヒット/ミスとは何か、
fetchブロックがいつ実行されるかを丁寧に説明。典型的な利用例:
rubyresult = Rails.cache.fetch("expensive_api:#{user.id}", expires_in: 10.minutes) do ExternalApi.call(user) end
キャッシュヒット/ミスの概念と運用上の考え方
- ミス時に高コストな処理が走ること、そのためのキー設計や TTL 設計の重要性など。
キャッシュのクリア(
Rails.cache.clear)- アプリ全体のキャッシュをクリアする操作であること、ローカル/開発環境やメンテナンス時の使い方などに触れている模様。
キャッシュキー生成と
cache_key_with_versionActive Record オブジェクト由来のキーは
#cache_key_with_versionを使うのが基本であることを強調。rubyRails.cache.fetch(article.cache_key_with_version) do render_to_string partial: "articles/article", locals: { article: article } end
Active Record オブジェクトを「そのまま」キャッシュするのがよくない理由
- モデルのスキーマ変更時の整合性問題
- キャッシュに古いオブジェクトが長期に残ることによるバグ
- シリアライズ/デシリアライズコストやMarshal互換性など
→ 代わりに、IDやcache_keyをキーにして「レンダリング結果」や「計算済み値」をキャッシュする流儀を推奨。
fetchの高度なオプション(例:race_condition_ttl)「ドッグパイル(キャッシュスタンピード)」対策として、期限切れ直後の一時的な猶予時間を設けるパターンを説明。
rubyRails.cache.fetch("slow-report", expires_in: 10.minutes, race_condition_ttl: 30.seconds) do HeavyReport.generate end
フラグメントキャッシングの改善
テンプレートツリーダイジェストの説明
- ERB/partial の構造に応じて自動生成されるダイジェストがキャッシュキーに含まれること、
- 部分テンプレートを編集するとキーが自動的に変わり、フラグメントが正しく無効化されるメカニズムを明示。
コレクションキャッシュの挙動
render partial: "article", collection: @articlesのような場合のキャッシュキー生成ルールや、- 各要素ごとのフラグメントキャッシュ + コレクションをまとめるラッパの動き、
expires_in等のオプションがコレクションレンダリングにどう適用されるかが整理されている。
キャッシュキーと有効期限オプションの扱い
- フラグメントキャッシュ (
cacheビュー helper など) におけるexpires_in,namespace,skip_digest等がどのように効くかを明解化していると考えられます。
- フラグメントキャッシュ (
依存関係管理のセクションをフラグメントキャッシュ配下へ移動・再構成
- 「暗黙的な依存」「明示的な依存」「外部依存」の3種類を整理して説明。
- view helper 経由でレンダリングされる場合など、依存関係が見えづらいパターンでも、どの変更でどのフラグメントが無効になるかを理解しやすくした構成になっています。
ロシアン・ドール・キャッシング(Russian doll caching)の明確化
ネストしたフラグメントキャッシュがどのように連携して更新されるのかを整理。
親モデル側で
touch: trueを指定することで、子モデル更新時に親のupdated_atが変わり、それに紐づく親フラグメントも無効化される流れを強調。rubyclass Comment < ApplicationRecord belongs_to :post, touch: true end
共有パーシャルキャッシュの再説明
- MIME タイプごとのキャッシュ分離(HTML, JSON, etc.)について、どのようにキーに反映されるかを説明。
- ビューパス上のパーシャル解決ルールとキャッシュキーの関係が、より追いかけやすい形で書き直されている。
Conditional GET (HTTP キャッシュ) の拡充
stale?とfresh_whenの説明強化If-None-Match/If-Modified-Sinceと ETag / Last-Modified の関係を踏まえ、- それぞれをどう使い分けるか、返るレスポンス (200 vs 304) の違いを、より丁寧に説明。
strict_freshnessの現在の挙動- 条件付きリクエストヘッダがある場合でも、どの条件でコンテンツを再送するか/しないかの基準を明確化。
http_cache_foreverをいつ使うべきか- コンテンツが実質的に不変な場合に限るべき理由、将来の変更をどう扱うか(パスやクエリにバージョンを含める等)などを説明。
強いETag / 弱いETag の新セクション
W/プレフィックスの意味- バイナリ完全一致が必要な場面 vs 論理的な同一性でよい場面の違いなど。
SQL キャッシュ(Active Record クエリキャッシュ)の更新
- SQL キャッシュが「Active Record query caching」であり、リクエスト/ジョブなどの「実行コンテキスト」単位で自動的に効く機能であることを明示。
- 同一コネクション・同一コンテキスト内で同じSQLが繰り返されるとメモリ上から返ること、
config.action_controller.perform_cachingとは別の仕組みであることを強調している可能性が高いです。 - クエリキャッシュのスコープ(たとえばミドルウェアや
ActiveRecord::Base.cacheブロック)についても現行仕様に即して説明。
Solid Cache セクションの更新
- Rails での現在の正式名称・用語に合わせて文章を更新。
- 新規 Rails アプリ生成時のデフォルト設定について追記。
- DB バックエンドとして Solid Cache を使う設定例(
database.yml/config/environments/*.rb)を最新の形で提示。 - 開発環境でのセットアップ手順を現行の recommended flow に合わせて案内。
- トランザクションとキャッシュストア選択の説明をより厳密に:
- DBトランザクションとキャッシュ書き込みの整合性、
- Solid Cache を他のストア(Memcached, Redis等)とどう使い分けるかといった観点を明確化。
Advanced Caching Patterns セクションの新設
新しく「高度なキャッシュパターン」セクションが追加され、特に以下をカバー:
バックグラウンドジョブや非リクエストコンテキストでのキャッシュ
- Active Job / Sidekiq等から
Rails.cacheを使う際の考慮点(名前空間、キー設計、キャッシュのライフサイクルなど)。 - 「リクエスト単位ではなく、プロセス全体・複数ワーカーで共有される」ことを意識した設計の必要性。
- Active Job / Sidekiq等から
ローカルキャッシュ (
Rails.cache.with_local_cache) の挙動単一リクエスト/ジョブ内でだけ有効なインメモリキャッシュレイヤーを追加し、同じキーへの多重アクセスを高速化するパターン。
rubyRails.cache.with_local_cache do # このブロック内では同一キーの読み出しがより高速化される user = Rails.cache.fetch("user:#{id}") { User.find(id) } # 以降の同一キーfetchはローカルキャッシュから返る end
メタ的な変更
- ETag の見出し/リンクなど、ガイド内のアンカーや内部参照を修正し、broken link を解消。
guides/source/documents.yamlを更新し、ガイド一覧・ナビゲーション上での位置づけやタイトル等を最新化。
- 影響範囲・注意点
これは ドキュメントのみの変更 であり、Rails 本体のキャッシュ機能の挙動変更は行っていません。
ただし、説明がかなり現行仕様に寄せて具体的になったため、以下のような「認識のアップデート」が必要になる可能性があります。
- 「Active Record オブジェクトを丸ごとキャッシュしてもいい」という古い習慣は非推奨であること。
- フラグメントキャッシュのキー/ダイジェスト/依存関係の理解が、以前よりも厳密に整理されているため、既存実装を見直すきっかけになり得る。
- Conditional GET / ETag 周りのベストプラクティスが明文化された結果、既存アプリで「とりあえず
fresh_when」しているコードを改善したくなるかもしれない。 - Solid Cache の推奨設定が最新版に合わされているので、古い記事・ブログ等と食い違う場合は、このガイドを優先すべき。
ガイド内のサンプルコードや推奨パターンが刷新されているため、新しくキャッシュを導入する際は、このPR後のドキュメントを前提に設計するのがよいです。
- 参考情報 (あれば)
- Rails Guides(edge版): https://edgeguides.rubyonrails.org/caching_with_rails.html
(このPRマージ後の内容が反映される想定) - クエリキャッシュの実装周り:
ActiveRecord::QueryCacheミドルウェア、およびActiveRecord::Base.cacheブロック - Solid Cache プロジェクト: https://github.com/rails/solid_cache (Rails本体と一体化が進んでいるため、最新版はガイドを参照)
#57417 Update the Rails on Rack guide (#56918) [ci-skip]
マージ日: 2026/5/21 | 作成者: @p8
- 概要 (1-2文で)
- Rails公式ガイド「Rails on Rack」を大幅に書き直し・構成見直ししたドキュメント改善PRです。
- Rackミドルウェアスタックの最新情報反映、初期化処理へのリンク追加、カスタムミドルウェアやコントローラからRack APIを使う高度な利用方法などが追記されています。
- 変更内容の詳細
2-1. 全体的な文言修正・構成改善
- 説明文の表現を整理し、他のガイド(特に Configuration / Instrumentation ガイド)とスタイルを揃えるように再構成。
- 古くなっていた説明や用語を現行Railsに即した形にアップデート。
- 末尾の「Resources」セクションを削除し、外部リンクや参照は本文中の関連箇所に直接埋め込む形に変更。
これにより、ガイド全体が「今のRailsのRack周りの実装と運用」を前提に読みやすくなっています。
2-2. bin/rails server と初期化ガイドの連携
Rails::Serverの起動フローをこのガイド内に詳細に書かず、 Initialization ガイドの「Rails server start」セクションへのリンクを明示的に追加。- 例: 「
bin/rails serverが内部で何をしているか」については Initialization ガイドに委譲。
- 例: 「
意図:
- 起動処理の情報源を1か所に集約し、将来の変更時にドキュメントの同期ズレを起こしにくくするため。
- Rackガイドでは「Rackとの関係性・ミドルウェア」を主題にし、起動ロジックの詳細は別ガイドへ分離。
2-3. デフォルトミドルウェアスタックの更新
- Railsアプリがデフォルトで読み込む Rack ミドルウェア一覧と、その説明を最新の構成に合わせて更新。
- 各ミドルウェア名から、対応する API ドキュメント (
api.rubyonrails.orgなど) へのリンクを追加。- 例:
ActionDispatch::Static,ActionDispatch::Cookies,Rack::Runtimeなどに対して、それぞれのクラス API ドキュメントへのリンク。
- 例:
ポイント:
- 以前のガイドでは古いミドルウェア名や構成が残っていたり、Sprockets 前提の説明が含まれていた可能性があるのを整理。
- Propshaft 利用時のミドルウェア
Propshaft::QuietAssetに対応(後述)。
2-4. 内部ミドルウェアスタックセクションの再構成
- 「Internal Middleware Stack」のセクション構成を全面的に見直し、Configuration ガイドと似たフォーマットに変更。
- 各ミドルウェアを箇条書き、もしくは小見出し + 説明 + リンクという形式で整理。
- ミドルウェアごとに「何をしているのか」「いつ役に立つのか」が分かりやすく説明されるように。
- 中には「Railsが自動的に挿入するもの」と「環境や設定により条件付きで挿入されるもの」が区別されて書かれている可能性が高いです。
開発者視点での価値:
- “なぜこのミドルウェアがスタックにいるのか”“これを削ると何が壊れそうか”を判断しやすくなる。
- カスタムスタックを組む際に、標準スタックを参考にしやすい。
2-5. Sprockets から Propshaft への更新
- 監査メモにもある通り、
Sprockets::Rails::QuietAssetsは現行ではPropshaft::QuietAssetに置き換えられているため、ガイドの記述を Propshaft ベースに更新。 - 特に development 環境における asset リクエストのログ出力抑制など、「静的アセットに対するQuietモード」の説明がPropshaft前提に修正されていると考えられます。
意味合い:
- Rails 7.1 以降のデフォルトに沿った形で、asset パイプライン周りの説明が一貫する。
- Sprockets 前提の古い情報を参照してしまうことを防げる。
2-6. カスタムミドルウェアをスタックに追加する方法のセクション追加
ガイドに「カスタムミドルウェアを追加する」ための新しい専用セクションが追加されています。
典型的には、以下のような内容・サンプルが含まれていると考えられます(あくまで代表的な例):
# app/middleware/my_custom_middleware.rb
class MyCustomMiddleware
def initialize(app)
@app = app
end
# Rack API: call(env) -> [status, headers, body]
def call(env)
# 前処理 (logging, 計測など)
status, headers, response = @app.call(env)
# 後処理 (header 追加など)
[status, headers, response]
end
end# config/application.rb など
config.middleware.use MyCustomMiddleware
# あるいは順序指定
config.middleware.insert_before Rack::Runtime, MyCustomMiddleware
config.middleware.insert_after ActionDispatch::Static, MyCustomMiddlewareガイドには、おそらく以下のような点も説明されています:
config.middleware.use/insert_before/insert_after/deleteの使い分け。envHash に入ってくるrack.*キー、action_dispatch.*キーの例。- ミドルウェアをproductionだけ・developmentだけで読み込む
Rails.env.production?などのパターン。
メリット:
- Rackレベルでログ、セキュリティヘッダ、A/Bテスト用cookie処理などを挟みたいときの、Railsとしての公式的なやり方が明文化された。
2-7. コントローラ内でRack APIを使う高度な利用方法のセクション追加
新しく「高度な使い方」的なセクションが加わり、「RailsコントローラからRack APIを直接利用する方法」が説明されています。
典型的には次のようなユースケースの説明が含まれることが多いです:
Rack準拠レスポンス(
[status, headers, body])を自前で返す:rubyclass RawRackController < ApplicationController def show body = ['Hello from Rack!'] headers = { 'Content-Type' => 'text/plain', 'Content-Length' => body.sum(&:bytesize).to_s } status = 200 # Rack風に直接返すこともできるが、Rails的には render / send_data を使うことが多いはず、という比較説明など [status, headers, body] end endrequest.envを通じて Rack レベルの情報(rack.session,rack.url_scheme,HTTP_*ヘッダなど)を扱う例:rubyclass EnvController < ApplicationController def index logger.info request.env['HTTP_USER_AGENT'] head :ok end endActionDispatch::Responseと純 Rack レスポンスとの対応関係や、RackアプリをRailsルーティングでマウントする例:ruby# config/routes.rb mount SomeRackApp, at: '/rack_app'
狙い:
- 「RailsはRack上で動いている」ことを実感しつつ、必要に応じて素のRack APIに近い制御もできることを示す。
- 既存のRackアプリを Rails に統合するときの参考になる。
- 影響範囲・注意点
コードへの直接的な変更はなし
変更対象はguides/source/rails_on_rack.mdのみで、アプリケーションやフレームワーク本体の挙動には影響しません。ただし「正しい理解」に影響する
- これまで古いガイドに基づいてミドルウェア構成や起動フローを理解していた場合、その前提が古くなっている可能性があります。
- 特に asset 関連(Sprockets vs Propshaft)や、デフォルトミドルウェアの一覧・順序の理解を、この新ガイドをもとにアップデートした方がよいです。
ドキュメント参照パスが変わる
- Rails起動処理については、Rails on Rack ガイドだけでなく Initialization ガイドも必ず参照する前提になります。
- カスタムミドルウェアの追加・調整を行う際は、この新しいセクションの方が現行ベストプラクティスに近いため、既存のブログ記事よりも公式ガイドを優先して読むのがおすすめです。
- 参考情報 (あれば)
- Rails Guides: Rails on Rack(このPRで更新されたガイド)
- Rails Initialization Guide(
bin/rails serverの起動フロー) - ミドルウェアの構成・操作全般(Configurationガイド内)
config.middlewareに関するセクション
- 個々のミドルウェアのAPIドキュメント
- Rails API: https://api.rubyonrails.org/
- 主要クラス例:
ActionDispatch::Static,ActionDispatch::Cookies,ActionDispatch::Session::CookieStore,Rack::Sendfileなど
#57415 [RF Docs] Active Record Query Interface (#56341)
マージ日: 2026/5/21 | 作成者: @p8
- 概要 (1–2文)
Active Record Query Interface ガイド(active_record_querying.md)を大幅に書き直し・再構成し、関連する API コメントおよび Composite Primary Keys ガイドとのリンクも整理した PRです。SQL や複合主キー、スコープ、ロック、パフォーマンス関連への導線を明確にしつつ、例示・用語の整合性や安全性(SQL インジェクション等)の説明を強化しています。
- 変更内容の詳細
2-1. Active Record Query Interface ガイド全体の再構成
対象: guides/source/active_record_querying.md
変更量が最も大きく、実質的に「全面的なリファイン」です。
主なポイント:
セクション構成・見出しの整理
- 「Conditions」をより意味が明確なタイトル(例: 「Where 条件」「Filtering Results」系)に変更し、
whereによるフィルタリングであることを明示。 - 「Ordering」を「Ordering Results」のような、結果セットに対する並び替えであることが直感的に分かる名称に変更。
- 「Retrieving Objects from the Database」配下の構成を見直し:
- 単一オブジェクト取得 (
find,find_by, Dynamic Finders) と - 複数オブジェクト取得 (
all,where, バッチ処理) をより分かりやすく整理。 Customer.allのような基本的な複数取得の例を明示的に示す、あるいはその旨を本文で説明。
- 単一オブジェクト取得 (
- 「Dynamic Finders」セクションを
find_byの説明後、「Retrieving a Single Object」系の場所に移動し、関連する finder 群として連続して理解できるように変更。 - 「Selecting Specific Fields」セクションから
distinctを切り離し、limit/offset周辺(セクション名を「Limiting Results」等に変更)と合わせて、結果セットの「件数・一意性の制御」という観点で再配置。 - 「Overriding Conditions」は
whereだけでなくorderなど他の SQL 句の上書きも扱っているため、「Overriding Clauses」等に名称を見直し、カバー範囲に即したセクション名に。
API・関連ガイドへのリンク強化
- 文中で触れているが別のガイドで詳説されるトピックへリンクを追加:
- Composite Primary Keys ガイド(新規にリンク追加、詳細は 2-2 を参照)
- トランザクションやロックに関する別ガイド(将来の「Active Model Transactions & Locking」)への導線を意識した記述。
order,default_scope,lock,with_lock,referencesなど、説明されていない(または補足が必要な)メソッドへの API ドキュメントリンクを追加。
セキュリティと SQL リテラシーの補強
- 「条件(string 条件)」での SQL インジェクション注意喚起の部分を整理し、Rails Security Guide の関連節へのリンクを追加。
- 配列条件 (
where(["name = ?", value])など) の説明部でも、セキュリティの観点(プレースホルダを使う/使わないの差)を簡潔に整理し、同じく Security Guide を参照。 - Active Record Basics ガイドにある「SQL の知識があると理解しやすい」という旨の注意書き・学習用リンクと同様のノートを、この Query Interface ガイドにも追加し、SQL 初学者がどこから学べばよいかを明示。
サンプルコード・表記の整合性と改善
- ブール値の例で
0/1を使っていた箇所をfalse/trueに修正し、DB 実装に依存しない直感的な例に統一。 created_atを「日付」として扱っていた例のうち、GROUP BY を含むものは実際には timestamp 単位でグループされるため不正確になりうることから、statusやcustomer_idなど、日付変換ロジックに依存しないグルーピング例へ変更。has_and_belongs_to_manyの例に含まれるjoin_tableオプションは、本来必須ではないが、実運用に近い「包括的な例」として現状維持(コメントにもその意図を明記)。- 日付レンジ条件(hash range 条件)で
Date.yesterday.all_dayなどを使って簡潔に書く提案はあったが、Date.yesterday.all_dayがデフォルトで UTC 評価され、config.time_zoneがローカルと異なる場合に subtle なズレを生む ことを避けるため、Time.now.midnightあるいはTime.zone.now.midnight系の例に留める判断を明文化(Timezone 混乱を避けるため、例はより明示的に)。
スコープ・メソッドチェーン・Fluent Interface の説明強化
out_of_print_and_expensiveのような複合スコープ例について:out_of_print.costs_more_than(500)のように既存スコープを再利用して組み立てる形も可能である点を踏まえつつ、- チェーン可能なスコープのデモとして利用していることを明示し、初期のシンプルな例からは外す整理。
- 「scopes とクラスメソッドは似ているが、1つ重要な違いがある」という既存の説明に加え:
- クラスメソッド内で
selfを返すと、スコープとほぼ同様にメソッドチェーンが行える例を追加。
- クラスメソッド内で
- 「The Active Record pattern implements Method Chaining」という文言の誤解を解消:
- Active Record パターン自体と Method Chaining は無関係であり、ここで説明したいのはむしろ「Fluent Interface」であることを明確化する方向で表現を修正。
- 「Understanding Method Chaining」節に NOTE を追加し、
- Rails Console では
inspectが暗黙に呼ばれるため、 - メソッドチェーン結果が意図せずクエリ実行をトリガすることがある という挙動を明記(開発者が「いつ SQL が投げられるのか」を誤解しないようにする)。
- Rails Console では
Find / Create 系メソッドの拡充と注意点
- 「Find or Build a New Object」セクションを拡充し、以下を含める:
find_or_create_by/find_or_create_by!の基本的な使い方・例。- 新しい
create_or_find_by/create_or_find_by!の紹介と使いどころ。 find_or_create_by系は DB レベルでアトミックではないため、競合状態(race condition)により同一レコードが二重作成される可能性がある Caveat を明示。
- 上記に伴い、「Retrieving Objects from the Database」セクションから「finder」と呼べないもの(
create_withなど)はリストから除外し、概念上の一貫性を確保。
SQL 由来メソッドの説明整理
- 「Finding by SQL」セクション内に混在していた内容を整理:
find_by_sqlは独立した見出しを付けて説明し、戻り値や用途(任意の SQL 実行)を明確化。- 一方で、
pluck,pick,idsなど「カラムを直接配列で取得する」系は、クエリインターフェイスの中でも「Retrieving Columns」といったサブセクションにまとめ、find_by_sqlとは別の振る舞い(Relation ではなく Ruby オブジェクト配列を返す等)を強調。
Eager Loading・EXPLAIN・パフォーマンス関連
includesセクション内で触れられているreferencesに API リンクを付与。eager_loadのクエリ例が実挙動と乖離していた点を修正(別 PR #51940 で補完する旨のコメントあり)。- 一方で:
- Eager Loading
- EXPLAIN の実行 など「パフォーマンス」の文脈に強く依存するトピックは、将来的に「Active Record Performance ガイド」へ切り出す構想を明文化。
ただしこの PR では実際の切り出しは行わず、別タスクで検討する前提に留めています。
ロッキング・トランザクション・Read-only
- ロッキング (
lock,with_lock) に API リンクを追加。 - ロックやトランザクション、
readonly指定などはクエリインターフェースとはやや別軸の話題であるため、- 「Active Model Transactions & Locking」的な新ガイドへの切り出しを行う方針を記載。
- ただし、実際の分離は今後の別タスクとして defer(この PR では現行ガイド内に残したまま)。
2-2. Composite Primary Keys ガイドの補強
対象: guides/source/active_record_composite_primary_keys.md
- Query Interface ガイドから「複合主キー」の具体的な説明をなるべくこちらに集約するように再整理し、「クエリインターフェースガイドはこれ以上重くしない」方針。
- Query Interface ガイドの中では、複合主キー関連の例はなるべく控えめにし、代わりにこのガイドへのリンクを設置。
- 「composite」や「transaction」を検索したときに、必ず少なくともどこかにガイドがヒットするよう、用語とリンクを意識して追加。
2-3. QueryMethods のコメント/ドキュメント微修正
対象: activerecord/lib/active_record/relation/query_methods.rb (+8/-8)
変更は実装というよりドキュメント寄り:
only,except,reorder,lockなどクエリビルダ的メソッドの Yard ドキュメントやコメント表現を、ガイドの説明と整合するよう修正。onlyに対するexcept(逆操作)を説明に追加。reorderは「default_scope を上書きする」だけでなく、「これまでに積み上げられた order 句全般を上書きする」ことを明示。
- メソッドの役割がガイド内のサンプルと噛み合うよう、文言を統一。
- 影響範囲・注意点
- 実装コードへの影響は
query_methods.rbのコメントレベルで、挙動変更はありません。既存アプリの動作には影響しません。 - 影響が大きいのは「ドキュメントの読み方・導線」です:
- Query Interface ガイドの見出しやサンプル位置が変わっているため、古いバージョンのガイドを前提に「◯◯セクション参照」としていた社内資料・ブログ等は、リンク切れ・参照箇所ずれの可能性があります。
find_or_create_byとcreate_or_find_byの関係、レースコンディションの caveat が明示されたことで、今後は「ユニーク制約付き + create_or_find_by の利用」など、より安全なパターンへ誘導されます。既存コードを見直す際の指針としても参照できる反面、今まで暗黙に許容していた非アトミックなパターンについて、チーム内の認識差が表面化する可能性があります。- Timezone と日付条件に関する例がより厳密になったため、これをきっかけに
Time.zone.now系を採用するかどうか、プロジェクトポリシーを確認するとよいです。
- ロッキング・トランザクション・パフォーマンス(Eager Loading / EXPLAIN / async_*)は「別ガイドに分離する構想」の段階であり:
- 近い将来、さらにドキュメントの構成変更(ガイドの分割)が発生する見込みがある点に注意してください。
- 社内 wiki や学習資料を書き直す際は、「今後別ガイドに移りそうなトピック」としてモジュール化しておくと追従しやすくなります。
- 参考情報
- この PR 本体:
https://github.com/rails/rails/pull/57415 - Active Record Query Interface (Edge Guides):
https://edgeguides.rubyonrails.org/active_record_querying.html - Active Record Composite Primary Keys ガイド:
https://edgeguides.rubyonrails.org/active_record_composite_primary_keys.html - 関連 PR(
eager_loadのクエリ例修正):
https://github.com/rails/rails/pull/51940 - 想定されている新ガイド:
- Active Model Transactions & Locking(Basecamp タスク参照)
- Active Record Performance(Eager Loading, EXPLAIN, async_* などの統合先として検討中)
#57414 Cache fixtures parsing
マージ日: 2026/5/21 | 作成者: @byroot
- 概要 (1-2文で)
このPRは、テスト実行時のフィクスチャ(YAML)の「パース処理」をキャッシュすることで、use_transactional_tests = falseなテストが混在するケースでの大幅なパフォーマンス低下を緩和する変更です。DBのリセットは従来どおり行いつつ、YAML ファイルの再解析を避けてフィクスチャ読み込みのオーバーヘッドを削減しています。
- 変更内容の詳細
背景と課題
use_transactional_tests = falseのテストが走ると、
「どのテーブル・レコードが変更されたかわからない」という前提から、
全フィクスチャのリセットが必要になります。- 特に Megatest のように「テストメソッド単位」で実行・並列化される環境では、 非トランザクションテストとトランザクションテストが入り混じることで、 「フィクスチャキャッシュの破棄 → 再ロード」が頻発し、
その際の YAML パースコストが全体の約半分を占めるほどのボトルネックになっていた、という説明です。
このPRは「DBのリセットは必要だが、YAML の再パースまでは不要」という点に着目して、
パース結果をメモ化して使い回せるようにする、という小規模な最適化を行っています。
主なコード上の変更点
(実際の差分は概略レベルでの説明になります)
1) フィクスチャファイル読み込み周り (fixture_set/file.rb)
ActiveRecord::FixtureSet::Fileあたりにある「YAML ファイルを読み込んでパースする処理」に対して、 パース済みデータをキャッシュする仕組みが導入されています。
イメージとしては:
# ざっくりしたイメージ(実装は異なる可能性あり)
class ActiveRecord::FixtureSet::File
def rows
@rows ||= parse_yaml_file
end
private
def parse_yaml_file
# 実際の YAML 読み込み・パース処理
end
endのように、一度パースした結果をインスタンス変数に保存しておき、
同じフィクスチャファイルに対しては何度も YAML パースしないようにしています。
2) フィクスチャ読み込みロジック (fixtures.rb)
ActiveRecord::FixtureSetがフィクスチャをロード/リセットする際の処理経路が少し整理され、- DB をリセットする部分と
- YAML をパースして Ruby オブジェクトにする部分
が論理的に分けやすい形になっています。
これにより「DB を全部リセットしたいタイミングでも、YAML の再パースはスキップし、
既にメモ化されたパース結果を使い回す」ことが可能になります。
内部的には、reset_cache 相当の処理から「YAML パース結果の破棄」が外され、
DB 側だけをクリアするような設計に寄せている形です。
3) テスト (fixtures_test.rb)
- 新しいキャッシュ挙動を担保するためのテストが追加・修正されています。
- フィクスチャのリセットを何度か繰り返しても、
YAML のパース回数が増えない/期待どおりキャッシュが効いていること - 逆に、フィクスチャファイルそのものが変化したとき(ファイル更新など)は、 適切にキャッシュが無効化されること(あれば)
- フィクスチャのリセットを何度か繰り返しても、
などを検証する内容になっていると考えられます。
- 影響範囲・注意点
影響範囲
主に影響を受けるのは テスト実行時のフィクスチャ周りのパフォーマンスです。
特に、
use_transactional_tests = falseなテストを混在させているテストスイート- Megatest 等でテストメソッド単位で並列化・分割実行している環境
では、フィクスチャリセットの回数が多くなるため、
この変更による テスト時間の短縮が比較的大きくなることが期待できます。
挙動の互換性/注意点
- アプリケーションの外部から見た API は基本的に変わらず、
挙動としても「DB の状態」は従来どおりリセットされるため、 テスト結果の意味論的な互換性は維持されています。 - 変更点は「YAML パースを毎回やり直さない」だけなので、 次のようなケースにだけ注意が必要です:
- 「テスト実行中にフィクスチャの YAML ファイルを書き換える」ような変則的なワークフロー
(通常はほぼ無いと思われます)
- 「テスト実行中にフィクスチャの YAML ファイルを書き換える」ような変則的なワークフロー
- もしそのようなワークフローを取っている場合、
同一プロセス内の後続テストから 古いパース結果が使われ続ける可能性があるため、 テストプロセスの再起動などで回避する必要があります。
将来的なリファクタリングの示唆
PR 説明にあるとおり、現状のフィクスチャコードは:
- 「テーブルのリセット」という単純な目的のために
- 「YAML パース → オブジェクト構築 → 挿入ロジック …」といった、 かなり複雑な処理を毎回行っている
という状態になっており、
理想的には「事前に計算した SQL のリストを流すだけ」にできるはずだ、という問題意識が示されています。
今回の変更はその一歩として「YAML パースの結果だけでもキャッシュする」もので、
今後さらに大きなリファクタリングが行われる可能性があります。
- 参考情報 (あれば)
- 元 PR(今回の抜き出し元):
https://github.com/rails/rails/pull/57326 - 対象ファイル:
activerecord/lib/active_record/fixture_set/file.rbactiverecord/lib/active_record/fixtures.rbactiverecord/test/cases/fixtures_test.rb
- 関連する設定:
ActiveSupport::TestCase.use_transactional_tests- Minitest / Megatest によるテスト実行戦略の違い(クラス単位 vs メソッド単位)
#57413 Optimize fixtures lookup pattern
マージ日: 2026/5/21 | 作成者: @byroot
- 概要 (1-2文で)
Rails の ActiveRecord fixtures で、フィクスチャ名の検索(lookup)に使うパターンが最適化され、約 50% 高速かつコードも簡潔になるように変更されています。変更は 1 行のみですが、フィクスチャ読み込み周りのパフォーマンスに影響します。
- 変更内容の詳細
※ この PR は #57326 から切り出された一部で、activerecord/lib/active_record/fixtures.rb 内の「フィクスチャの検索用パターン」を 1 行差し替えています。
実際の差分は「旧パターン → 新パターン」の置き換えで、主に以下のような意図を持っています:
- fixtures 内でレコードを参照するときに使う「名前 → レコード ID」の解決に使用されるパターン(おそらく正規表現か文字列検索ロジック)が、
- マッチングの仕方を見直して不要な分岐・バックトラッキングや重いメソッド呼び出しを減らす
- Ruby VM / Regexp エンジンにとって処理しやすい形に単純化する
ことで高速化されている。
- byroot 氏の PR でよくあるパターンですが、「動作仕様を変えずに実装をシンプルにして速くする」タイプの変更です。
典型的には次のような変更が行われている可能性があります(イメージ):
# 例: 以前
LOOKUP_PATTERN = /\A(.+)\((.+)\)\z/
# 例: 以後(アンカーやキャプチャの仕方を整理)
LOOKUP_PATTERN = /\A([^()]+)\(([^()]+)\)\z/あるいは、複雑な gsub などを介したパターン構築から、よりストレートな正規表現・マッチングに簡略化しているケースです。
PR の説明からも、「約 50% 高速」「かつシンプル」 という点から、フィクスチャ名をパースする部分の正規表現やパターン構築ロジックが整理されただけで、外から見える API や挙動は変わっていないと考えられます。
- 影響範囲・注意点
影響範囲
- ActiveRecord の fixtures を使っているテストコード全般。
- 特に、フィクスチャのラベル参照・関連参照(
posts(:one),comments(:one_for_post_one)など)に関わる「ラベル名の解釈・検索」部分の内部実装が対象です。 - RSpec / Minitest など、Rails 標準 fixtures を利用しているテスト環境で恩恵があります。
互換性
- 作者コメント的にも「最適化」としか述べておらず、かつ 1 行差分のみであることから、既存のフィクスチャ名の書き方・解決ルールは変わらない前提です。
- ただし、元のパターンが「ややゆるくマッチしていた」場合、新パターンで厳密になりすぎてしまうと、
- かなり変な名前(特殊文字を多用したり、想定外のフォーマット)を使ったフィクスチャ
- 非公式に依存していた挙動(バグ寄りの仕様) があれば、テストで解決できなくなる可能性はあります。
- そのため、Rails をアップデートしたあとに「一部のフィクスチャだけ見つからない」「関連の解決に失敗する」といった問題があれば、フィクスチャ名の形式や、
<table>(<label>)のような書き方をしていないかを確認するとよいです。
パフォーマンス
- Rails 本体のテスト・ベンチマークにおいて、フィクスチャ lookup が約 50% 高速化しているということなので、大量のフィクスチャを使う大規模テストスイートでは実測でのテスト時間短縮が期待できます。
- 参考情報 (あれば)
- 元 PR: https://github.com/rails/rails/pull/57413
- 抽出元のより大きな PR: https://github.com/rails/rails/pull/57326
- フィクスチャ関係の広範な最適化・リファクタリングが含まれている可能性が高く、この PR はその一部を独立させたものです。
- fixtures の仕組み自体のドキュメント:
- Rails Guides – Active Record Fixtures:
https://guides.rubyonrails.org/testing.html#the-low-down-on-fixtures
- Rails Guides – Active Record Fixtures:
#57405 Improve Date::DATE_FORMATS and Time::DATE_FORMAT deprecation
マージ日: 2026/5/21 | 作成者: @byroot
- 概要 (1-2文で)
Rails 7.2系(?)で進んでいるDate::DATE_FORMATS/Time::DATE_FORMATS廃止方針に関するフォローアップで、非推奨警告の出し方を整理し、新しい推奨APIを警告メッセージに含めるようにしつつ、コア拡張とフォーマット定義モジュールの関係を整理したPRです。実質的には「どう警告を出すか」「どこにフォーマットが定義されるか」の内部構造の変更であり、既存アプリへの影響は主に非推奨警告の出方にとどまります。
- 変更内容の詳細
2-1. ActiveSupport::Deprecation を使った警告出力
以前のPR (#57345) で Date::DATE_FORMATS / Time::DATE_FORMATS の利用が非推奨になりましたが、このPRでは:
- ActiveSupport 独自の
ActiveSupport::Deprecation(=いわゆる ActiveSupport deprecator)を使って警告を出すように変更 - これにより:
- Railsの標準的なやり方で deprecation が管理される
config.active_support.report_deprecationsなど既存の設定でまとめて制御できる- Backtrace フィルタなども一貫して扱える
以前は独自に warn を使っていた、あるいは ActiveSupport deprecator を十分に活用していなかった部分をリファクタして、標準のdeprecation仕組みに寄せた形です。
2-2. 警告メッセージに「新しいAPI」を明示
非推奨警告の文言が改善され、「代わりに何を使えばよいか」が明示されるようになりました。
おおよそ以下のようなメッセージになるイメージです(正確な文言はPR本文参照):
Date::DATE_FORMATS[:short]を使ったとき:
→ 「Date::DATE_FORMATSは非推奨です。代わりにActiveSupport::DateFormats(またはActiveSupport::TimeWithZoneのフォーマッタ) を使ってください」- 時刻系 (
Time::DATE_FORMATS) も同様に、ActiveSupport::TimeFormatsなど新API(あるいはI18n.l/to_fs系)の利用を促す内容に変更
これにより、deprecation を見た開発者が「何をすればいいか」まで即座に分かりやすくなります。
2-3. コア拡張とフォーマットモジュールの関係を逆転
ファイル構成上の意味で:
- 以前は:
core_ext/date/conversions.rb/core_ext/time/conversions.rbでフォーマット定義もかなり握っていたdate_formats.rb/time_formats.rbは「追加的な定義」あるいは補助的な関係になっていた
- 今回の変更後:
activesupport/lib/active_support/date_formats.rbactivesupport/lib/active_support/time_formats.rb
のほうがフォーマット定義の「本体」となりcore_ext/date/conversions.rb/core_ext/time/conversions.rbはそれを利用する側(拡張を提供する側)として、依存関係が「逆転」した
つまり:
- 「日付/時刻のフォーマット定義」そのものは ActiveSupport の専用モジュール (
ActiveSupport::DateFormats/ActiveSupport::TimeFormats相当) に集約 Date/Timeのコア拡張はそのモジュールを利用してto_s/to_formatted_sなどを提供
という、より分離された構造になったと考えられます。
2-4. 実装上の変更点イメージ
正確なコードは一部省略しますが、概念的には以下のような変化です。
旧: コア拡張が直接定数を管理
# activesupport/lib/active_support/core_ext/date/conversions.rb
class Date
DATE_FORMATS = {
short: "%e %b",
# ...
}
def to_s(format = :default)
# DATE_FORMATS を直接参照
end
end新: フォーマットモジュールに集約し、そこ経由で利用
# activesupport/lib/active_support/date_formats.rb
module ActiveSupport
module DateFormats
FORMATS = {
short: "%e %b",
# ...
}
# ここで deprecation 経由で Date::DATE_FORMATS をラップする等
end
end
# activesupport/lib/active_support/core_ext/date/conversions.rb
class Date
# include か委譲などの形で ActiveSupport::DateFormats を利用
# Date::DATE_FORMATS を触るときはActiveSupport::Deprecation経由で警告
endまたテスト (date_ext_test.rb / time_ext_test.rb) も更新され、
- 非推奨警告が ActiveSupport deprecator から出ること
- 新しい構造で format が期待どおりに解決されること
などが確認されるようになっています。
- 影響範囲・注意点
- すでに #57345 の時点で
Date::DATE_FORMATS/Time::DATE_FORMATSは非推奨になっているため、「初めて非推奨になった」わけではありません。 - このPRによる主な実務的影響は:
- deprecation メッセージが変わる(より具体的になる)
- deprecation が ActiveSupport の仕組みに統一されるため、
ActiveSupport::Deprecationを使ったサプレッションやログ出力制御がしやすくなる
アプリケーションコード側での注意点
- まだ
Date::DATE_FORMATS/Time::DATE_FORMATSに直接フォーマットを追加・参照している場合は、deprecation 警告がよりはっきりした形で出るようになります。 - 推奨される置き換えはアプリの使い方次第ですが、典型的には:
- 独自フォーマットは
I18n.lと locale の date/time フォーマットで管理 - もしくは
to_fs(:your_format)と ActiveSupport のフォーマット API を利用 - Rails が提供する標準フォーマットに依存しているだけなら、特に自前で
DATE_FORMATSを触らないようにする
- 独自フォーマットは
既存の to_s(:short) などは、内部構造が変わっても同じ結果になるよう維持されているため、即時に壊れる可能性は低いですが、Date::DATE_FORMATS / Time::DATE_FORMATS の直接操作は確実に廃止方向なので、警告を見つけたら早めに移行するのが無難です。
- 参考情報 (あれば)
- 元PR: https://github.com/rails/rails/pull/57405
- 先行PR (初回のdeprecation導入): https://github.com/rails/rails/pull/57345
- ActiveSupport::Deprecation ガイド:
- フォーマット系 API の現行推奨形:
to_fs/to_formatted_s系: https://api.rubyonrails.org/classes/ActiveSupport/TimeWithZone.html#method-i-to_fs- I18n ローカライズ: https://guides.rubyonrails.org/i18n.html#adding-date-time-formats
#57345 Introduce ActiveSupport::TimeFormats and ActiveSupport::DateFormats for registering custom date formats
マージ日: 2026/5/20 | 作成者: @paracycle
- 概要 (1-2文で)
Time#to_fs/Date#to_fsで使うカスタム日付フォーマットを、グローバル定数Time::DATE_FORMATS/Date::DATE_FORMATSを直接いじらずに登録できるActiveSupport::TimeFormats/ActiveSupport::DateFormatsが導入されました。従来の定数ベースのカスタマイズは非推奨となり、次期Railsで削除予定です。
- 変更内容の詳細
新しいAPI: ActiveSupport::TimeFormats / ActiveSupport::DateFormats
to_fs 用のフォーマット登録方法が次のように変わります。
これから推奨される書き方
# Time 用
ActiveSupport::TimeFormats.register(:month_and_year, '%B %Y')
# Date 用(Proc も使える)
ActiveSupport::DateFormats.register(
:short_ordinal,
->(date) { date.strftime("%B #{date.day.ordinalize}") }
)
Time.current.to_fs(:month_and_year) # => "February 2024"
Date.current.to_fs(:short_ordinal) # => "February 21st"register(name, format)でフォーマットを登録formatはstrftime形式の文字列、または(time_or_date) -> stringな Proc を想定
Time#to_fs(:format_name)/Date#to_fs(:format_name)から利用可能- フォーマットの実体は内部的に
ActiveSupport::TimeFormats.list/ActiveSupport::DateFormats.listに保持される
これまでの書き方(非推奨)
Time::DATE_FORMATS[:month_and_year] = '%B %Y'
Date::DATE_FORMATS[:short_ordinal] = ->(date) { ... }
Time.current.to_fs(:month_and_year)
Date.current.to_fs(:short_ordinal)- これらの定数は引き続き解決に使われますが「deprecated」と明示され、次の Rails メジャーバージョンで削除予定です。
互換性のための挙動
- 初期状態では:
ActiveSupport::TimeFormats.listはTime::DATE_FORMATSの内容から初期化ActiveSupport::DateFormats.listはDate::DATE_FORMATSの内容から初期化- 両方とも 凍結されておらず、従来どおり
Time::DATE_FORMATS[:foo] = ...などで追加したものもリストに反映される
- ただし 一度でも
registerが呼ばれたら:- そのタイミングで
list側を書き換え&freezeされる - 以後は「新APIを使う前提」とみなされ、グローバル定数をいじることによる副作用の拡散を抑える設計
- そのタイミングで
これによって、既存アプリはそのまま動かしつつ、新APIに opt-in したプロジェクトでは日付フォーマットのスコープと一貫性を強化できます。
関連する内部コードの変更
activesupport/lib/active_support/core_ext/{date,time,date_time}/conversions.rbto_fs/to_formatted_sがフォーマットを解決する際に、ActiveSupport::TimeFormats.list/ActiveSupport::DateFormats.list- (互換性のため)
Time::DATE_FORMATS/Date::DATE_FORMATS
を見るような形に調整されています(実装詳細はファイル差分に依存)。
ActiveRecord::Integrationなど、to_fs/ 日付フォーマットに依存する部分のテストが新APIに沿うよう修正されています。activesupport/CHANGELOG.mdに変更点、非推奨事項として追記。
- 影響範囲・注意点
実装上の影響
- 今すぐ壊れる変更ではありませんが、次のメジャーバージョンで
Time::DATE_FORMATS/Date::DATE_FORMATSが削除予定である点が最重要です。 - ライブラリやアプリケーションで以下のようなコードを書いている場合は、将来の互換性のために移行が必要です。
Time::DATE_FORMATS[:foo] = ...Date::DATE_FORMATS[:bar] = ...
- 新APIは ActiveSupport 依存なので、純粋な Ruby オブジェクトで
Time/Dateに手を入れていた gem は、Rails 環境ではできるだけこの API 経由に寄せる方が安全です。
マイグレーションの方針
アプリ側での推奨対応:
# config/initializers/time_formats.rb などで
# 旧:
# Time::DATE_FORMATS[:ymd_hms] = '%Y-%m-%d %H:%M:%S'
# Date::DATE_FORMATS[:ymd] = '%Y-%m-%d'
# 新:
ActiveSupport::TimeFormats.register(:ymd_hms, '%Y-%m-%d %H:%M:%S')
ActiveSupport::DateFormats.register(:ymd, '%Y-%m-%d')gem / engine の場合:
Time::DATE_FORMATS/Date::DATE_FORMATSを直接触るのはやめ、ActiveSupport::TimeFormats.register/ActiveSupport::DateFormats.registerを使うように変更- Rails の該当バージョン未満をサポートする必要がある場合は、
「respond_to?(:register)なら新 API、そうでなければ従来の定数」を使う二段構えも検討できます。
if defined?(ActiveSupport::TimeFormats) && ActiveSupport::TimeFormats.respond_to?(:register)
ActiveSupport::TimeFormats.register(:foo, '%H:%M')
else
Time::DATE_FORMATS[:foo] = '%H:%M'
endライブラリ間の衝突回避
- グローバルな
Time::DATE_FORMATS/Date::DATE_FORMATSに勝手に追加する設計は、複数の gem が同じキーを使ったときに衝突しやすい問題がありました。 - 今回の変更で「フォーマット管理を ActiveSupport 名義に閉じる」方向に寄せることで、衝突の起点を Rails 側に集約しつつ、将来的にはより厳密な管理(例えばキー競合時の警告など)もしやすくなります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57345
- 関連コメント(設計の背景):
- 変更点の公式記録:
activesupport/CHANGELOG.md内の該当セクション(ActiveSupport::TimeFormats/ActiveSupport::DateFormatsの追加とTime::DATE_FORMATS/Date::DATE_FORMATS廃止予定について)
#53636 fix referential_integrity for postgres partitioned tables
マージ日: 2026/5/20 | 作成者: @aandrieu
- 概要 (1-2文で)
PostgreSQL のパーティショニングされたテーブルに対してActiveRecord::FixtureSet.create_fixturesなどを利用した際、外部キー制約が意図せずNOT VALIDになる不具合を修正する PR です。referential_integrity実装内で行っていた制約の無効化 SQL を、対象テーブルにスコープするように変更しています。
- 変更内容の詳細
問題の背景
ActiveRecord::FixtureSet.create_fixtures は、テストデータ投入時などに一時的に外部キー制約を無効化・再検証するため、内部的に ActiveRecord::Base.connection.disable_referential_integrity を利用します。
PostgreSQL アダプタでは、この処理の一部として ActiveRecord::ConnectionAdapters::PostgreSQL::ReferentialIntegrity#all_foreign_keys_valid! が呼ばれ、pg_catalog.pg_constraint を直接更新して以下のようなことを行っていました:
convalidated = falseにして制約を「未検証 (NOT VALID)」状態にするALTER TABLE ... VALIDATE CONSTRAINT ...で再検証する
しかし、PostgreSQL の パーティショニングされたテーブル では、次のような状況が起こります。
- 親テーブルとそのパーティションに、同名の外部キー制約 が存在しうる
- これまでの実装では
UPDATE pg_catalog.pg_constraint ...がテーブルにスコープされておらず、同名制約を持つ全パーティションの制約をまとめてNOT VALIDにしてしまう - その後の
VALIDATE CONSTRAINTは「1テーブル分」しか行わないため、その他のパーティションに対する制約がNOT VALIDのまま残る
その結果、rails db:migrate やスキーマのダンプ (structure.sql) を実行すると、ほとんどすべてのパーティションテーブル上の外部キー制約に NOT VALID が付与された差分が出続ける、という問題が発生していました。
具体的な変更
変更ファイル:
activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb(+1/-1)activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb(+33/-0)
本質的な変更は 1 行で、
# 変更前(イメージ)
UPDATE pg_catalog.pg_constraint
SET convalidated = false
WHERE conname = #{quote(constraint_name)}
# 変更後(イメージ)
UPDATE pg_catalog.pg_constraint
SET convalidated = false
WHERE conname = #{quote(constraint_name)}
AND conrelid = #{quote(table_oid)}のように、UPDATE pg_catalog.pg_constraint を 対象テーブルの OID (conrelid) でスコープする 形に修正しています。
これにより:
- あるテーブル
parent_tableの外部キー制約fk_parent_fooを再検証したい場合
→conname = 'fk_parent_foo'かつconrelid = 'parent_table' の OIDの行だけがNOT VALIDになる - 同名の制約
fk_parent_fooを持つ別パーティションparent_table_2024_01などは影響を受けない
という挙動になり、パーティション全体へ「巻き込み」無効化される問題を防ぎます。
テスト
activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb に、PostgreSQL のパーティションテーブルと外部キー制約を用いたテストが追加されています。内容としては概ね:
- パーティション構造を持つテーブルと外部キー制約を作成
disable_referential_integrity→all_foreign_keys_valid!相当の処理を実行- パーティション側の制約が
NOT VALIDのまま残らないことを検証
というシナリオで、今回のバグ再現と修正確認を行っています。
- 影響範囲・注意点
対象:
- PostgreSQL を使用している Rails アプリケーション
- 特に、パーティションテーブル を使用しつつ
fixtures/ActiveRecord::FixtureSet.create_fixtures/disable_referential_integrityといった機能を利用しているプロジェクト
期待される改善:
rails db:migrate後にstructure.sqlを見ると、パーティションテーブル上の外部キーにNOT VALIDが大量に付加される差分が出続ける問題が解消される- 実運用中の DB でも、テストやバッチ処理で
disable_referential_integrityを利用しても、関係ないパーティションの外部キー制約が勝手にNOT VALIDにならない
注意点:
- 既に「壊れた状態」で
NOT VALIDになっている制約は、この PR で自動的には直りません。必要に応じて、手動でALTER TABLE ... VALIDATE CONSTRAINT ...;を実行する必要があります。 - 変更は
pg_constraintのUPDATEにフィルタ条件を足しているだけなので、パーティションを使っていない環境に対して挙動の後退はほぼありません(むしろ限定的になった)。
- 既に「壊れた状態」で
- 参考情報 (あれば)
対応クラス:
ActiveRecord::ConnectionAdapters::PostgreSQL::ReferentialIntegrity
→disable_referential_integrity/all_foreign_keys_valid!の Postgres 実装部分関連挙動:
- PostgreSQL ドキュメント: 外部キー制約と
NOT VALID/VALIDATE CONSTRAINT
https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-VALIDATE-CONSTRAINT - パーティションテーブルと制約の扱い
親テーブルと各パーティションは、それぞれpg_constraint上では独立したエントリを持ち、conrelidによってどのテーブルの制約かが区別される。
今回はこのconrelidを利用して「どのテーブルの制約を無効化するか」を限定する形に修正している。
- PostgreSQL ドキュメント: 外部キー制約と
#57367 Prevent development welcome route from duplicating on route reload
マージ日: 2026/5/20 | 作成者: @curi
- 概要 (1-2文で)
開発環境でreload_routes!などによりルーティングを再読み込みすると、Rails が内部的に用意している「開発用ウェルカムページ」のGET /ルートが毎回追加され、重複してしまう問題を解消する PR です。アプリ側で定義した root ルートの優先順位はこれまでどおり維持しつつ、内部ウェルカムルートの登録を「アプリ起動時の一度だけ」に制限しています。
- 変更内容の詳細
問題の背景
- Rails は開発環境で
config/routes.rbに root が定義されていない場合、GET /に内部の「Welcome」ページを表示するルートを自動的に追加しています。 - この内部ルートは
routes_reloader.run_after_load_paths内でapp.routes.appendにブロックを渡す形で登録されていました。 RouteSet#appendは「渡されたブロックを永続的に保持する」一方で、「ルートの再読み込み時には実体化されたルートだけがクリアされる」という挙動を持ちます。- そのため、
reload_routes!が呼ばれるたびにappendが積み増しされ、結果として内部のGET /ルートが何度も追加される、というバグが発生していました。
対応方針
- 「内部ウェルカムルートを登録する append ブロック」をアプリのブート時に一度だけ登録するように変更。
- ルートの並び順は従来どおりに維持し、アプリ側が定義した root ルートが内部ウェルカムルートよりも優先される挙動はそのまま残す。
実際の変更ポイント
※ 実際のコード全文は省略し、要点のみ説明します。
1. routes_reloader に「一度だけ追加する」仕組みを導入
railties/lib/rails/application/routes_reloader.rb
- 内部的に「開発用ウェルカムルート append ブロックを既に登録したかどうか」を判定するフラグ/条件を追加。
run_after_load_pathsが呼ばれるたびに無条件でapp.routes.appendしていた処理を、「まだ登録していない場合のみ append する」ように変更。
イメージとしては:
# 疑似コードイメージ
unless @welcome_route_registered
app.routes.append do
# 内部ウェルカムルートの定義
end
@welcome_route_registered = true
endのような形で、一度だけ append されるようになっています。
2. finisher の微調整
railties/lib/rails/application/finisher.rb
run_after_load_pathsの呼び出しやそのフックの扱いに合わせて 1 行レベルの修正が入っています。- 目的はあくまで「内部ルートの append ブロックが多重に登録されない」ようにすることで、通常のルート再読み込みフロー自体は保持されています。
3. テスト追加
railties/test/application/routing_test.rb
- 以下のような観点のテストが追加されています:
- 「開発用ウェルカムルートがルート再読み込みによって重複しないこと」
- 「アプリケーション側で root を定義した場合、そのルートが内部ウェルカムルートより優先されること(append による順序性が保たれていること)」
テスト名の一部:
/not duplicated/にマッチするテスト: 開発用 root の重複が起きないことを検証。/appended root/にマッチするテスト: アプリ側の root が append 後のルート順に沿って優先されることを検証。
4. CHANGELOG の追記
railties/CHANGELOG.md
- この挙動修正についての短いエントリが追加され、開発環境でのルーティング動作の変更点が明示されています。
- 影響範囲・注意点
- 対象:
- 主に development 環境で
reload_routes!やコードリロード後のルート再読み込みに関わる挙動。 config/routes.rbに root が定義されていない状態で開発用ウェルカムページが使われるケース。
- 主に development 環境で
- 期待される挙動の変化:
- 複数回のルート再読み込みをしても、
GET /の内部ウェルカムルートが 1 つだけに保たれるようになります。 - もし以前、
rails routes等で root が複数行出ていた場合、それが解消されます。
- 複数回のルート再読み込みをしても、
- 従来から変わらない点:
- アプリケーション側で
root "some#controller"を定義すれば、それが 最優先 され、内部ウェルカムルートはマッチしません。 - ルート定義の順序性(
appendで追加されるルートが後ろに来る)自体は維持されています。
- アプリケーション側で
- 互換性:
- 重複していた root ルートに依存しているアプリは通常想定されないため、実質的にはバグ修正であり、互換性上のリスクは低いと考えられます。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57367
- 関連するクラス・モジュール:
Rails::Application::RoutesReloaderActionDispatch::Routing::RouteSet#append
- テスト実行例(PR 内で言及されているもの):
cd railties && bin/test test/application/routing_test.rb -i '/not duplicated|appended root/'cd railties && bin/test test/application/routing_test.rb
#57340 Add ActionController::Parameters#deep_transform_values
マージ日: 2026/5/20 | 作成者: @Edilbek
- 概要 (1-2文で)
ActionController::Parametersにdeep_transform_values/deep_transform_values!が追加され、Strong Parameters の「許可状態(permitted?)」を保ったままネストした値を再帰的に変換できるようになりました。これにより、これまで必要だったto_unsafe_h.deep_transform_valuesによるガードレールの迂回が不要になります。
- 変更内容の詳細
追加されたメソッド
ActionController::Parameters に以下が追加されています。
#deep_transform_values(&block)
自身をコピーして、すべてのネストした値に対してブロックを再帰的に適用し、新しいActionController::Parametersを返します。#deep_transform_values!(&block)
自身を破壊的に書き換えます。戻り値は変換後のself(ActionController::Parameters)。
いずれも Active Support の Hash#deep_transform_values とインターフェース・挙動を揃えつつ、戻り値を ActionController::Parameters のままに保ち、permitted? の状態も維持します。
動作イメージ
PR本文の例を少し噛み砕くと:
params = ActionController::Parameters.new(
user: {
email: " ALICE@EXAMPLE.COM ",
profile: { bio: " Hello world " }
}
)
# 非破壊版: 新しい Parameters を返す
normalized = params.deep_transform_values { |v|
v.is_a?(String) ? v.strip.downcase : v
}
# => ActionController::Parameters オブジェクトを維持
# user, profile もすべて Parameters としてネストされたまま
# 破壊版: 自身を書き換える
params.deep_transform_values! { |v|
v.is_a?(String) ? v.strip : v
}
# permitted? 状態の引き継ぎ
params.permit!
params.permitted? # => true
params.deep_transform_values { |v| v }.permitted? # => trueここで重要なのは以下の点です:
- ネストしたハッシュ部分も
ActionController::Parametersのまま再構築される permit!を呼んだ後にdeep_transform_valuesしても、結果のParametersもpermitted? == trueのままHash#deep_transform_valuesと同様、「値」に対してのみブロックが適用され、キーは変更されない
これにより、典型的なユースケースである:
- 入力値の前後空白除去
- 文字列の正規化(小文字化、全角/半角変換など)
- boolean/number へのキャスト
- nil/空文字の統一処理
といった「バリデーション前の一括整形」を、Strong Parameters を崩さずに行えます。
実装のポイント(推測含む)
コード上の詳細は以下のような方針になっています(テスト/慣例から読み取れる範囲):
- 既存の
Parameters#deep_transform_keys実装パターンに倣っている - 再帰的に
Parameters/Hash/Arrayを辿り、最終的なスカラー値にブロックを適用 - 返却時に
ActionController::Parameters.new(hash_like)でラップしつつ、元のpermitted?フラグをコピー - bang 版は
@parametersの中身を書き換える形で実装されている
- 影響範囲・注意点
影響範囲
- Rails コントローラで Strong Parameters を使っているアプリケーション全般が、より安全に値変換ロジックを組み込みやすくなります。
- これまで以下のようなコードを書いていたケースを、
deep_transform_valuesに置き換え可能です:
# 旧: Strong Parameters をいったん Hash にしてしまう
normalized = params.to_unsafe_h.deep_transform_values { ... }
# 新: Strong Parameters を維持したまま変換
normalized = params.deep_transform_values { ... }- Strong Parameters の「許可/非許可」判定ロジックには変更はなく、その状態を保ったまま値変換を追加で行えるだけなので、既存コードに対して互換性上の破壊的変更は基本的にありません。
注意点 / 使い方上の留意
- 変換タイミング:
- 多くの場合は、
permitの前に正規化を行うと、キー・構造に依存しない変換処理を簡潔に書けます。 - 一方で、「許可された値だけを変換したい」場合は
permit後に呼びます。
- 多くの場合は、
- 破壊的メソッドの利用:
deep_transform_values!はインスタンスを書き換えるので、後続で元の値が必要な場合は非破壊版を使うかdupしてから呼び出してください。
- Strong Parameters の性質は変わらない:
deep_transform_values自体は「何を permit するか」を決めるわけではなく、あくまで「値の変形」だけを行います。- 変換後も
require/permitの書き方は従来どおりで、キー名はそのままです(キー変換はdeep_transform_keysを使う)。
- ブロックの責務:
- 文字列・配列・数値など、すべての「値」にブロックが渡されるため、型チェック (
is_a?) をきちんと行わないと意図しない変換が走る可能性があります。 - 特に boolean や nil を扱う際は、
Stringのみを対象にするなどの条件分岐が推奨されます。
- 文字列・配列・数値など、すべての「値」にブロックが渡されるため、型チェック (
- 参考情報 (あれば)
- 類似 API:
Hash#deep_transform_values(Active Support)ActionController::Parameters#deep_transform_keys/#deep_transform_keys!
- 想定ユースケース:
- 入力フォーム全体のホワイトスペース除去、メールアドレスの downcase
- API 受信パラメータの型・形式の一括正規化(ただしビジネスロジック的な変換はモデル/サービス層に寄せるのが望ましい)
- 実際に導入する際のパターン例:
class UsersController < ApplicationController
def create
normalized = user_params.deep_transform_values { |v|
v.is_a?(String) ? v.strip : v
}
@user = User.new(normalized)
...
end
private
def user_params
params.require(:user).permit(:email, :name, profile: [:bio])
end
endこのように、Strong Parameters の枠組みを崩さずに「入力前処理」を行えるようになった、というのがこの PR の本質的な改善点です。
#57399 doc: escape "API" for strong_parameters doc
マージ日: 2026/5/20 | 作成者: @okuramasafumi
概要 (1-2文で)
strong_parameters のドキュメント中で、「API」という単語が誤ってリンク扱いになっていた問題を、Markdown エスケープによって修正した PR です。コードの挙動や API 仕様には一切変更はなく、純粋なドキュメント修正です。変更内容の詳細(あればサンプルコードも含めて)
- 対象ファイル:
actionpack/lib/action_controller/metal/strong_parameters.rb - strong_parameters のクラスコメント(YARD/rdoc用のドキュメントコメント)内にある「API」という単語が、そのまま書かれていることで自動的にリンクとして解釈されていました。
- この PR では、その「API」をバックスラッシュ(
\)でエスケープして、ドキュメント生成時にリンクにならないようにしています。
イメージとしては、ドキュメントコメント中の:
APIのような箇所が、以下のように変更された形です:
\APIこれにより、rdoc/Markdown のパーサが「API」をリンク候補として扱わず、単なるテキストとして表示します。
- 影響範囲・注意点
- 影響範囲:
- Rails の公式ドキュメント(strong_parameters の API ドキュメント)の見た目のみが変わります。
- 生成される HTML ドキュメント上で、「API」という文字列がリンクではなくプレーンテキストとして表示されるようになります。
- 注意点:
- 実行時の挙動、パブリック API、型、メソッドのシグネチャ等には一切変更がありません。
- テスト追加のチェックボックスにはチェックが入っていますが、この変更自体はドキュメントのみのため、特にテストは本質的には不要な種類の修正です。
- CHANGELOG の更新も不要な範囲(ドキュメントのみの微修正)として扱われています。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57399
- strong_parameters ドキュメントは「どのパラメータを許可するか」の説明で “API” という語を頻繁に使うため、余計な自動リンクを防ぐのは可読性向上・誤解回避のために有効です。
#57368 Limit the size of strings we call to_i on in ActiveRecord
マージ日: 2026/5/20 | 作成者: @tenderlove
- 概要 (1-2文で)
このPRは、ActiveRecord/ActiveModel が文字列を自動的に整数へ変換するときに、to_iをかける文字列長を制限し、超長い文字列による DoS の可能性を下げる変更です。カラムのバイトサイズに基づき、先頭_limit * 4バイトだけを整数化の対象にします。
- 変更内容の詳細
何をしているか
ActiveModel の整数型キャスト(ActiveModel::Type::Integer)で、文字列から整数への変換時に以下の制約を導入しました。
- 対象が文字列のとき、
「文字列の先頭_limit * 4バイト」だけを切り出してto_iする - ここで
_limitは「カラムが DB 上で使うバイト数」- 4バイト
integer→_limit = 4→ 先頭 16バイトを使用 - 8バイト
bigint→_limit = 8→ 先頭 32バイトを使用 limit: 1の tinyint 的カラム →_limit = 1→ 先頭 4バイトを使用
- 4バイト
Ruby の String#to_i は文字列長に比例して時間が伸びるため、極端に長い文字列を渡されると DoS の温床になります。この変更で、内部で勝手にやる「自動整数化」のコストを上限付きにしています。
なぜ _limit * 4 なのか
- 表現可能な桁数に対して十分な余裕がある:
- 4バイト int: 最大 10 桁程度
- 8バイト bigint: 最大 19 桁程度
_limit * 4だと:- 4バイト int → 16バイト → 10桁 + 符号 + ちょっとした suffix を入れても足りる
- 8バイト bigint → 32バイト → 19桁 + 符号 + slug っぽい suffix も許容
当初は「DB カラムの幅」ベースで厳密に制限しようとしたが、以下のような「slug の自動整数化」ケースが壊れるため断念し、4倍のマージンを取った形です。
# URL: /posts/123-hello-world
Post.where(id: params[:id]).first
# params[:id] == "123-hello-world" だが、id カラムは integer/bigint
# ActiveRecord は先頭の整数部分だけを見て、id: 123 として検索するslug 全体を入れるとカラムの桁幅は超えるが、"123-hello-world" のような短い slug は _limit * 4 の範囲に収まりやすく、これまで通り動作します。
コードイメージ
※ 実際のコードはもう少し細かいですが、概念的にはこんな処理が ActiveModel::Type::Integer に入ります:
def cast_value(value)
case value
when String
limit_bytes = @limit&.*(4) # カラムの limit(バイト) * 4
if limit_bytes && value.bytesize > limit_bytes
value = value.byteslice(0, limit_bytes)
end
value.to_i
else
value.to_i
end
end説明文にあった example:
ActiveRecord::Base.connection.create_table :users do |t|
t.integer :age, limit: 1
t.timestamps
end
class User < ActiveRecord::Base; end
User.create(age: '9' * 5_000_000)- 変更前: 500万文字すべてを
to_iが走査 - 変更後:
limit: 1→_limit = 1→_limit * 4 = 4バイトだけを見てto_i
→ageキャストに要する時間が、一定上限までに抑えられる
テスト・ドキュメント
activemodel/CHANGELOG.md- この振る舞い変更が追加されている旨を記載
activemodel/test/cases/type/integer_test.rb- 長大な文字列をキャストした場合に、先頭部分のみ考慮されることをテスト
- 影響範囲・注意点
影響を受けるケース
- ActiveRecord / ActiveModel が内部で行う「文字列 → integer 変換」全般
- モデルの integer/bigint カラムへの代入
- コントローラで
where(id: params[:id])等をした時の ID キャスト - フォーム送信で数値カラムに長い文字列を送ってきた場合
破壊的になりうる挙動の変化
- 極端に長い数値文字列を渡したとき、末尾部分は無視されるようになります。
- 例:
id: "1234567890" * 100000のような入力
→ 先頭_limit * 4バイトだけを見てto_i
- 例:
- ただし、実際には DB 側の整数カラムに収まらない値は、これまでもオーバーフロー・バリデーションエラー・DB エラーなどが発生していたため、「正しく扱えていた巨大入力」が壊れるケースは実用上ほぼないと考えられます。
セキュリティ・パフォーマンス上の意味
- 非常に長い文字列を
to_iに渡すことで CPU 時間を消費させる DoS のリスクが軽減されます。 - Rails アプリ側で:
- Strong Parameters やフォームオブジェクトでの length validation
- Rack ミドルウェアでのボディサイズやパラメータ長の制限
などと組み合わせることで、より広範な防御が可能です。
ただし、この変更だけで「任意の長さの入力による全 DoS」を防げるわけではありません。
注意点 / 既存コードで意識すべきこと
- 仕様として「長過ぎる文字列を渡した場合、Rails は先頭
_limit * 4バイトだけでto_iする」ことを知っておく必要があります。 - 「文字列の末尾まで含めて意味を持つ形式(特殊なエンコード値など)を integer カラムにそのまま突っ込んで、
to_iに頼っている」ような設計がある場合、誤変換に気づきにくくなる可能性があるため、そもそもスキーマ設計やキャスト方法を見直すべきです。 - DoS 対策としては有効ですが、ユーザ入力に対する明示的な length validation は依然として推奨されます。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57368
- 関連クラス:
ActiveModel::Type::Integer(activemodel/lib/active_model/type/integer.rb) - 参考: Ruby
String#to_iの計算量は文字列長に比例するため、無制限な長さの入力はパフォーマンスリスクとなる。
#57376 Fix non-working time-only example in ActiveModel::Type::DateTime doc [ci skip]
マージ日: 2026/5/20 | 作成者: @55728
- 概要 (1-2文で)
ActiveModel の:datetime型のドキュメントに「時刻だけの文字列も受け付ける」という誤った記述とサンプルがあったため、それを削除し、時刻のみを扱いたい場合は:time型を使うよう誘導する修正です。実装には変更がなく、ドキュメントのみの修正です。
- 変更内容の詳細(あればサンプルコードも含めて)
何が問題だったか
ActiveModel::Type::DateTime の RDoc に、次のような趣旨の記述がありました(要約):
:datetime型は「部分的な時刻のみのフォーマット」も受け付ける- 例:
event.start = "06:07:08+09:00"→Timeオブジェクトが返る
しかし実際の挙動は異なり、"06:07:08+09:00" のような 日付を含まない時刻のみの文字列 を与えると nil が返ります。
内部的には Date._parse で文字列を解析していますが、時刻のみだと :year が含まれないパース結果になり、new_time が nil を返すためです。
# 実際の挙動イメージ
type = ActiveModel::Type::DateTime.new
type.cast("2024-01-01 06:07:08+09:00")
# => 2024-01-01 06:07:08 +0900 (Time/ActiveSupport::TimeWithZone など)
type.cast("06:07:08+09:00")
# => nil # ドキュメントの記述と食い違っていたこの説明と例は、ActiveModel::Type::Time のドキュメントからほぼコピーされたもので、Time 型のほうでは「時刻のみのパース」が正しくサポートされています。
具体的な修正内容
ActiveModel::Type::DateTimeの RDoc から:- 「部分的な time-only フォーマットを受け付ける」といった説明文を削除
event.start = "06:07:08+09:00"のサンプルを削除
- 代わりに:
- 時刻のみの値を扱いたい場合は
:time型 (ActiveModel::Type::Time) を使うように誘導する文言を追加
- 時刻のみの値を扱いたい場合は
コード上のロジックや挙動には一切変更がなく、activemodel/lib/active_model/type/date_time.rb のコメント(RDoc)のみが 2 行追加・5 行削除で修正されています。
- 影響範囲・注意点
挙動の変更は一切なし
既存の:datetime型のキャスト挙動はこれまでどおりで、時刻だけの文字列は引き続きnilになります。ドキュメントを信じてコードを書いていた場合の実害が可視化された
以前のドキュメントを見て次のように書いていた場合:rubyattribute :start_at, :datetime # 「時刻だけもいけるはず」と思っている例 model.start_at = "06:07:08+09:00" # 期待: その日の 06:07:08+09:00 の Time # 実際: nilこの違いはドキュメントの誤りが原因で、コードのバグではありません。
「日付を省略した時刻のみの文字列」を扱いたい場合は、次のように:time型を使う必要があります。rubyattribute :start_at, :time model.start_at = "06:07:08+09:00" # => 有効な Time オブジェクトとしてパースされる(Time 型)今後の設計の指針
- 「日付+時刻」の値 →
:datetime - 「時刻のみ」の値 →
:time
と役割を明確に分けるべきであり、:datetimeに「日付なしの時刻文字列」を渡しても補完はされない、という点を前提にコードを書く必要があります。
- 「日付+時刻」の値 →
- 参考情報 (あれば)
- 該当ファイル:
activemodel/lib/active_model/type/date_time.rb(RDoc のみ変更) - 関連する型:
ActiveModel::Type::DateTimeActiveModel::Type::Time(time-only パースがサポートされている)
- 内部的なパース挙動の背景:
- Ruby 標準の
Date._parseは時刻のみの文字列を{:hour, :min, :sec}などで返すが:yearを含まない ActiveModel::Type::DateTime側のnew_timeは:yearがないケースをカバーしておらずnilを返すため、結果として time-only 文字列はnilになる
- Ruby 標準の
#50862 Accept Tempfile as ActiveStorage attachable
マージ日: 2026/5/20 | 作成者: @shouichi
- 概要 (1-2文で)
Active Storage の「添付可能オブジェクト(attachable)」としてTempfileが正式にサポートされました。これにより、Fileと同様にTempfileをそのままhas_one_attached/has_many_attachedに渡して添付できるようになります。
- 変更内容の詳細
2-1. 変更の主旨
- もともと Active Storage では
Fileオブジェクトを attachable として扱えましたが、同様の用途でよく使われるTempfileはサポートされていませんでした。 - この PR によって、
TempfileもFileと同様に扱えるようになり、例えば一時ファイルを生成してからそのまま添付する、といったワークフローが自然に書けるようになります。
2-2. コード上の変更点
activestorage/lib/active_storage/attached/changes/create_one.rb
CreateOne クラスは has_one_attached の変更を扱う内部クラスで、「渡されたオブジェクトを attachable としてどう扱うか」を判定する役割があります。
この PR では、ここでの型判定が修正され、「File だけでなく Tempfile も attachable として扱う」ように変更されています。
イメージとしては、以下のようなロジックになっていると考えられます(実際のコードを簡略化した擬似例):
def change
case attachable
when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
# 既存: アップロード済みファイル
when File, Tempfile
# 今回: File に加えて Tempfile もここで扱う
when Hash
# 既存: IO として扱うなど
else
# 既存: ActiveStorage::Blob や Signed ID など
end
end具体的な差分としては、File 単体で判定していた箇所が File と Tempfile の両方を許容するような条件に書き換えられています(is_a?(File) から is_a?(File) || is_a?(Tempfile)、または配列での when File, Tempfile など)。
テスト追加: activestorage/test/models/attached/one_test.rb
has_one_attached に対して Tempfile を渡した時に、問題なく Blob が作られ添付されることを確認するテストが追加されています。イメージとしては以下のようなテストです:
test "attaching Tempfile" do
record = User.create! # has_one_attached :avatar など
Tempfile.create(["avatar", ".png"]) do |tempfile|
tempfile.write("dummy")
tempfile.rewind
record.avatar.attach(tempfile)
end
assert record.avatar.attached?
endCHANGELOG 追記: activestorage/CHANGELOG.md
- Active Storage の変更履歴に、
Tempfileが attachable としてサポートされたことが追記されています。 - 次のリリースに含まれる機能としてドキュメント化された形です。
- 影響範囲・注意点
3-1. 想定される利用パターン
これまで、Tempfile を Active Storage に添付するために以下のようなワークアラウンドを書いていた場合:
file = Tempfile.new(["avatar", ".png"])
# ... 書き込み処理 ...
file.rewind
# 以前は明示的に IO として扱ったり、UploadedFile に包むなどしていたケース
user.avatar.attach(
io: file,
filename: "avatar.png",
content_type: "image/png"
)今後は次のように、File と同様にそのまま渡せるようになります:
Tempfile.create(["avatar", ".png"]) do |file|
file.write("dummy")
file.rewind
user.avatar.attach(file)
endあるいは、Web API クライアントやバッチ処理で一時ファイルを作って添付する、といったケースでもシンプルになります。
3-2. 互換性・破壊的変更の有無
- 既存の
Fileattachable の挙動は変えていないため、後方互換性の上での破壊的変更はほぼありません。 TempfileはFileを継承している環境がほとんどですが、実装によっては異なる型として扱われている可能性もあるため、それを Active Storage 側で明示的に許可した形になります。- attachable の型判定分岐に
Tempfileが追加されたのみで、既存のインターフェイス (attach,attachables) に変更はありません。
3-3. 運用上の注意 (Tempfile のライフサイクル)
Tempfileは Ruby の GC によって自動的にクローズ・削除されるとはいえ、ブロック形式で使うことが推奨されます:rubyTempfile.create(["data", ".csv"]) do |tempfile| # Active Storage への attach はこのブロック内で完結させる model.file.attach(tempfile) end- Active Storage は添付時にファイル内容をサービス(S3 など)へアップロードするため、
attach完了後にTempfileが削除されても問題ありません。ただし、非同期ジョブ側でファイルシステムに依存しているような独自処理がある場合は挙動を確認した方が安全です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/50862
- 関連: Active Storage ガイド(attachable の種類・
attachの使い方)
https://guides.rubyonrails.org/active_storage_overview.html - Ruby
Tempfileクラスドキュメント(ライフサイクル・削除タイミングなどの詳細)
https://docs.ruby-lang.org/en/master/Tempfile.html
#57393 Load image processing backend upfront
マージ日: 2026/5/20 | 作成者: @janko
- 概要 (1-2文で)
このPRは、Active Storage で初回の画像バリアント生成時に発生していた「画像処理バックエンドのロード遅延」をなくすため、ImageMagick / vips のバックエンドをあらかじめrequireしておくようにした変更です。これにより、初回アクセスのレイテンシ低減と、prefork 型アプリサーバにおけるメモリ共有効率の向上が期待できます。
- 変更内容の詳細
背景
- Active Storage の画像変換は
image_processinggem を通して ImageMagick もしくは vips を利用します。 image_processingは使用されるまでバックエンド(image_processing/mini_magickやimage_processing/vips)を autoload する仕組みですが、- デプロイ直後の「最初のバリアント生成時」にバックエンド読み込みコスト(約 50–100ms + 追加メモリ)が発生する
- prefork 型サーバ(Puma clustered / Unicorn, Passenger など)では、フォーク後にロードするとコピーオンライトが効きにくくなり、各ワーカーでメモリが二重・三重に確保される
- 特に
image_processing/vipsのロードは、image_processing単体に比べて 約 19MB の追加メモリ を消費するとの測定結果が示されています。
実際の変更点
このPRでは、Active Storage の各トランスフォーマークラスが初期化時にバックエンドを明示的に require するようにしています。
対象ファイルは次の2つです。
activestorage/lib/active_storage/transformers/image_magick.rbactivestorage/lib/active_storage/transformers/vips.rb
具体的には、各トランスフォーマーファイルの先頭付近で、次のような形のコードが追加されているイメージです(疑似コード/要約):
# image_magick.rb
require "image_processing/mini_magick"
module ActiveStorage
module Transformers
class ImageMagick
# 既存の変換ロジック
end
end
end# vips.rb
require "image_processing/vips"
module ActiveStorage
module Transformers
class Vips
# 既存の変換ロジック
end
end
endこれにより、アプリケーションが Active Storage のトランスフォーマークラスをロードした段階で、対応する image_processing バックエンドも同時にロードされます。
CHANGELOG
activestorage/CHANGELOG.md にエントリが追加されており、「画像処理バックエンドを事前に読み込む」挙動変更が明示されています。
これは、挙動に軽微ながらもパフォーマンス上のインパクトがあるため、利用者がアップグレード時に気付きやすいようにする目的と考えられます。
- 影響範囲・注意点
性能・メモリ面の影響
- 初回リクエスト遅延の減少
デプロイ直後に最初のバリアント生成を行うリクエストからは、image_processingバックエンドのロード時間(50–100ms 程度、環境による)が除去されます。 - prefork 型サーバでのメモリ削減
- マスタープロセスが
image_processing/vipsをロードした状態でforkされるため、追加の約 19MB がワーカー間でコピーオンライトにより共有されやすくなります。 - 結果として、ワーカー数が多い構成ほど総メモリ使用量の削減効果が見込まれます。
- マスタープロセスが
起動時のオーバーヘッド
- アプリケーションプロセスの起動時(マスタープロセス起動時)に、
image_processingバックエンドを読み込む分の時間・メモリコストが前倒しで発生します。 - ただし、これは「いつか必ず発生するコスト」を「最初のリクエスト」ではなく「起動時」に移動しただけとも言えます。
- Bootsnap 利用環境や Linux の本番サーバでの具体的な数値は作者も未計測とのことですが、一般的には「初回リクエストのレスポンス改善」「メモリ共有効率の向上」のメリットが勝るケースが多いと考えられます。
アプリケーション側での対応
- 通常は特別な変更は不要で、Rails をアップグレードすればこの挙動が自動的に有効になります。
- もしアプリ側で独自に
image_processingを lazy load していたとしても、Active Storage が require 済みであれば二重 require による問題は基本的に発生しません(Ruby のrequireは同一ファイルを2回ロードしない)。 - メモリが極端にシビアな環境で「画像変換機能を一切使っていないのにバックエンドを読み込みたくない」といったニッチな要件がある場合は、
- Active Storage 自体を使わない構成にする
- または独自に autoload/require のタイミングを制御する などの検討が必要になるかもしれませんが、一般的な利用では問題にならないはずです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57393
- 関連Gem:
- image_processing: https://github.com/janko/image_processing
- ruby-vips: https://github.com/libvips/ruby-vips
- 背景技術:
- Copy-on-write と prefork サーバでのメモリ共有 (Puma clustered, Unicorn, Passenger など)
- Rails における require / autoload のタイミング最適化と、初回リクエスト遅延のトレードオフ
#57396 Invalidate query cache when UPDATE goes through update_with_result
マージ日: 2026/5/20 | 作成者: @kylekeesling
- 概要 (1-2文で)
PostgreSQL の GENERATED STORED カラムを持つテーブルで UPDATE が行われた際、update_with_result経由の更新でもクエリキャッシュが正しく無効化されるようにした修正です。これにより、同一キャッシュスコープ内で UPDATE 後に古いレコードが返ってしまう不具合が解消されます。
- 変更内容の詳細
何が問題だったか
Rails edge では、PostgreSQL の GENERATED ... STORED カラムが存在するテーブルに対しては、UPDATE 時に RETURNING を使うルート(update_with_result)が使われるようになっています(関連 PR: #48628)。
しかしクエリキャッシュの実装では、キャッシュを「汚す」(invalidate) 対象として update は登録されていた一方で、update_with_result がリストに含まれていませんでした。
その結果、次のような状況が発生していました:
ActiveRecord::Base.connection.cache do
p = Post.create!(counter1: 1)
Post.find(p.id) # この SELECT がクエリキャッシュに載る
p.update!(counter1: 99) # GENERATED STORED カラムがあり update_with_result ルートを通る
Post.find(p.id).counter1 # => 1 (本当は 99 であるべきなのに、キャッシュから古い値が返る)
endupdate_with_result がキャッシュ無効化の対象になっていないため、2 回目の Post.find が UPDATE 前のキャッシュをそのまま使ってしまう、というバグです。
具体的な修正内容
dirties_query_cacheに:update_with_resultを追加対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract/query_cache.rbここに定義されている「この種の SQL が実行されたらクエリキャッシュを無効化する」というメソッド名のリストに、
update_with_resultを追加しています。イメージとしては以下のようなイメージ変更です(擬似コード):
ruby# 以前 dirties_query_cache :insert, :update, :delete # 変更後 dirties_query_cache :insert, :update, :update_with_result, :deleteこれにより、
update_with_resultが呼ばれたタイミングでも、現在の接続のクエリキャッシュがフラッシュされ、後続の SELECT は DB から最新状態を再取得するようになります。回帰テストの追加
対象ファイル:
activerecord/test/cases/query_cache_test.rbすでに
test_updateというクエリキャッシュと UPDATE の連携を検証するテストがあり、それに隣接して、新たにupdate_with_result経由の更新でもキャッシュが無効化されることを確認するテストが追加されています。主なポイント:
- アダプタが PostgreSQL であること
supports_virtual_columns?が true であること
(GENERATED ... STOREDカラムがサポートされていること)
という条件でテストを実行します。
既存の
Defaultモデルにはすでにvirtual_stored_numberという GENERATED STORED カラムがあるため、それを利用して:- クエリキャッシュ有効範囲 (
connection.cache do ... end) を張る - 1 度 SELECT してキャッシュに載せる
update_with_resultルートを通る UPDATE を実行- 再度同じレコードを SELECT し、更新結果が反映されていることを期待
という流れのテストになっています。
- 影響範囲・注意点
影響を受けるケース
- DB アダプタ: PostgreSQL
- テーブルに
GENERATED ... STOREDカラム(Rails 上ではvirtual :..., stored: true等)が存在する save!/update!等で UPDATE が発生する- その前後の SELECT が同じ
ActiveRecord::Base.connection.cacheブロック(または同等のクエリキャッシュスコープ)の中で行われている
この条件が揃っている場合、これまでは UPDATE 後も古い行が SELECT で返る可能性がありましたが、この PR によって解消されます。
既存アプリへの影響
- これまでは「たまたま」キャッシュが無効化されずに古いデータが返っていたところが、正しく最新データを返すようになります。
- クエリキャッシュが無効化されるタイミングが 1 パターン増えるため、極めて細かいレベルで見ると、「UPDATE 直後の SELECT がキャッシュを参照して高速になっていた」ケースはなくなります。ただしそれは本来バグであり、正しい挙動は今回の修正後です。
- インターフェイスの変更や API の互換性に関わる変更はなく、あくまで内部のキャッシュ・インバリデーションの修正です。
注意点
- この PR は「
GENERATED STOREDカラムがある場合の UPDATE ルート」に限定された修正で、他の DB(MySQL、SQLite など)や、GENERATED カラムを持たないテーブルの挙動は従来どおりです。 - もしアプリ側で独自に
update_with_resultをフックしている・モンキーパッチしている場合は、クエリキャッシュとの連携周りで挙動の変化がないかを確認しておくと安心です。
- この PR は「
- 参考情報 (あれば)
- この PR が修正している経路の追加元:
- #48628 – PostgreSQL での UPDATE RETURNING パス (
update_with_result) 追加の PR
- #48628 – PostgreSQL での UPDATE RETURNING パス (
- 関連クラス:
ActiveRecord::ConnectionAdapters::QueryCache- UPDATE 経路:
updateupdate_with_result(RETURNING を伴う UPDATE、GENERATED STORED カラムがある場合に利用)
#57378 Use NOT ENFORCED in disable_referential_integrity on PostgreSQL 18.4+
マージ日: 2026/5/20 | 作成者: @yahonda
- 概要 (1-2文で)
PostgreSQL 18.4 以降を対象に、disable_referential_integrityが内部で外部キー制約を「NOT ENFORCED / ENFORCED」に切り替える方式に変更され、DISABLE TRIGGER ALLを使わなくなりました。これにより superuser 権限が不要になりつつ、トランザクションで安全に元の制約状態を復元できるようになっています。
- 変更内容の詳細
背景・目的
- これまで PostgreSQL アダプタの
disable_referential_integrityはDISABLE TRIGGER ALL/ENABLE TRIGGER ALLを使って外部キー制約を一時的に無効化していました。 - この方法は多くの場合 スーパーユーザー権限 が必要であり、マネージド DB や権限が制限された環境では使いづらいという問題がありました。
- PostgreSQL 18.4+ では、FK 制約を
NOT ENFORCED/ENFORCEDで切り替えることで、テーブル所有者権限だけで 実質的に制約の有効/無効を切り替えられるようになります。 - さらに
NOT ENFORCEDでは ON DELETE CASCADE / SET NULL / SET DEFAULT などの参照アクションも抑止 されるため、古い実装で目指していた「一時的に参照整合性に伴う副作用を止める」ことも実現できます(revert された #27636 が意図していた挙動に相当)。
主な変更点
1) PostgreSQL 18.4+ での disable_referential_integrity の実装切り替え
PostgreSQL 18.4 以上の場合、disable_referential_integrity は以下のようなイメージのクエリを実行するようになります(擬似コード):
def disable_referential_integrity
if postgresql_version >= 18.4
transaction do
# 対象テーブルの外部キー制約を NOT ENFORCED に
foreign_keys(table_name).each do |fk|
execute("ALTER TABLE #{quote_table_name(fk.from_table)} ALTER CONSTRAINT #{quote_column_name(fk.name)} NOT ENFORCED")
end
yield
# 実行後、元々 ENFORCED だった制約について ENFORCED に戻す
foreign_keys(table_name).each do |fk|
execute("ALTER TABLE #{quote_table_name(fk.from_table)} ALTER CONSTRAINT #{quote_column_name(fk.name)} ENFORCED")
end
end
else
# 既存実装: DISABLE TRIGGER ALL / ENABLE TRIGGER ALL
end
end実際には、Rails 側で「もともと ENFORCED だったか / もともと NOT ENFORCED だったか」を区別できるように管理しており、元の状態を正しく復元 します。
2) トランザクションでのラップを強化
disable_referential_integrityの内部で、「NOT ENFORCED にする」「ブロックを実行する」「元に戻す」 という一連の処理を 単一のトランザクションでラップ するように変更されています。これにより、ユーザーが外側で明示的にトランザクションを張っていなくても、以下のような状況を防げます:
ALTER CONSTRAINT ... NOT ENFORCEDがコミット済み
- ブロック内で制約違反のデータを入れてしまい、
ENFORCEDに戻す際にエラー
- ブロック内で制約違反のデータを入れてしまい、
- その結果、もともと ENFORCED だった制約が NOT ENFORCED のまま DB に残る
トランザクションでラップすることで、途中でエラーが出た場合はトランザクションがロールバックされ、制約の状態は呼び出し前に完全に戻る ことが保証されます。
Rails 内部の利用箇所:
insert_fixtures_set- 既にトランザクションでラップされているが、
disable_referential_integrity自体も内部トランザクションで保護される。
- 既にトランザクションでラップされているが、
truncate_tables- 同様に、内部で
disable_referential_integrityを利用。
- 同様に、内部で
3) check_all_foreign_keys_valid! の挙動変更
- PostgreSQL の
NOT ENFORCED制約にはVALIDATE CONSTRAINTを適用できないため、check_all_foreign_keys_valid!は NOT ENFORCED な制約を検査対象から除外 するようになっています。 - これにより、アプリケーション上で意図的に「NOT ENFORCED のまま運用している制約」がある場合でも、
check_all_foreign_keys_valid!の呼び出しが失敗したりしません。
4) テストの追加・更新
activerecord/test/cases/adapters/postgresql/referential_integrity_test.rbに大幅なテスト追加 (+208 行) が行われており、主に以下をカバーしていると考えられます:- PostgreSQL 18.4+ で
NOT ENFORCED / ENFORCEDが使われること - 元々 ENFORCED だった制約の状態がブロック実行後に正しく復元されること
- もともと NOT ENFORCED だった制約は、実行前後で状態が変わらないこと
- 制約違反が発生した場合でも、トランザクションのロールバックで状態が壊れないこと
- PostgreSQL 18.4+ で
fixtures_test.rbなども更新され、フィクスチャ投入まわりでの挙動がカバーされています。
- 影響範囲・注意点
影響範囲
- 対象: PostgreSQL 18.4 以上を使っている Rails アプリケーションで、以下を利用しているケース
ActiveRecord::Base.connection.disable_referential_integrity- フィクスチャ (
insert_fixtures_set) 利用 truncate_tables利用
- これらの場面では、従来よりも低い権限(テーブル所有者権限)で外部キー制約の一時無効化が行われます。
開発者視点でのメリット
- DB 管理者が superuser 権限をアプリケーションユーザーに渡さなくても、テストやデータロード処理が動かしやすくなる。
NOT ENFORCEDによって ON DELETE CASCADE なども抑止されるため、データ移行・テストデータロード時に「予期せぬカスケード削除」が起こりにくい。
注意点
NOT ENFORCED 制約の存在に対する前提の見直し
- アプリケーション側で「すべての外部キーは常に ENFORCED である」という前提を置いているコードがある場合、
disable_referential_integrity実行中はその前提が崩れます。 - ただし、この PR では Rails 側がコール前後で状態を復元するため、「呼び出し前後での恒常的な状態」は保たれます。
- アプリケーション側で「すべての外部キーは常に ENFORCED である」という前提を置いているコードがある場合、
check_all_foreign_keys_valid!のカバレッジNOT ENFORCEDの制約はcheck_all_foreign_keys_valid!の対象外になります。- 「DB 全体で参照整合性が有効であることを完全に保証したい」場合は、
- 「意図的に NOT ENFORCED にしている制約」がないか
- 必要に応じて運用側で個別チェックを追加する といった設計上の考慮が必要です。
PostgreSQL バージョン依存
- 18.4 未満の PostgreSQL では、従来通り
DISABLE TRIGGER ALL/ENABLE TRIGGER ALLが利用されます。 - 同じアプリを複数バージョンの PostgreSQL で運用する場合、環境ごとに内部動作が変わることになるため、 -権限要件(superuser が必要か) -性能特性・ロック挙動 に若干の違いが出る可能性があります。
- 18.4 未満の PostgreSQL では、従来通り
- 参考情報 (あれば)
- 元 PR: https://github.com/rails/rails/pull/57378
- NOT ENFORCED / ENFORCED の詳細な挙動説明 PR: https://github.com/rails/rails/pull/57377
- 関連する過去の変更・議論:
- rails/rails#27636 (ON DELETE CASCADE などを抑止する狙いがあったが、一度 revert されている)
- PostgreSQL ドキュメント(NOT ENFORCED まわりはバージョン 18 系のリリースノート・リファレンスを参照)
#57394 Stabilize say_with_time regression tests
マージ日: 2026/5/20 | 作成者: @yahonda
- 概要 (1-2文で)
say_with_timeの回帰テストが CI 環境でタイミングぶれにより不安定になっていた問題を、期待値を「固定文字列」から「フォーマットのみを保証する正規表現マッチ」に変更することで安定化した PR です。ログ出力の形式は維持したまま、経過時間の微小な違いでテストが落ちないようにしています。
- 変更内容の詳細
対象は activerecord/test/cases/migration_test.rb 内の以下の 2 つのテストです。
MigrationTest#test_migration_say_with_time_with_integer_returning_in_blockMigrationTest#test_migration_say_with_time_with_non_integer_returning_in_block
元々のテストは、say_with_time が出力するログ全文を「完全一致」で比較していました:
expected = <<~MSG
-- Bar
-> 0.0000s
-> 123 rows
MSG
assert_equal expected, outputこのように "-> 0.0000s" という文字列に固定していたため、ブロックの実行時間が 0.0001s や 0.0002s になると、CI で以下のように失敗していました:
"-- Bar
- -> 0.0000s
+ -> 0.0001s
-> 123 rows
"今回の修正では、経過時間の具体的な値ではなく、フォーマットだけを検証するように変更しています。説明文によると、次の条件を満たす正規表現を使って assert_match する形になっています:
- 先頭の
"-- Bar"行 (ヘッダ行) があること - 次の行にサブ項目がインデント付きで表示されること (スペース +
->) - 経過時間は
%.4f(小数点以下 4 桁)の形式であること- 例:
0.0000s,0.0001s,0.1234s, etc.
- 例:
- 単位の
sが末尾につくこと - (該当テストでは) 行数出力
"-> 123 rows"が続くパターンと、行数なしのパターンの両方でフォーマットを保証 - 改行の有無・数もある程度固定 (アンカー付き
\A ... \zなど) で検証
イメージとしては、次のような形に近いテストになっていると考えられます(実際のコードを概念的に表現):
assert_match(
/\A-- Bar\n -> \d+\.\d{4}s\n -> 123 rows\n\z/,
output
)および:
assert_match(
/\A-- Bar\n -> \d+\.\d{4}s\n\z/,
output
)これにより、0.0000s 〜 0.0002s などの微小な揺らぎは許容しつつ、出力レイアウトとフォーマット仕様は担保できるようになっています。
- 影響範囲・注意点
- 影響範囲は Active Record のテストコードのみ であり、フレームワーク本体の挙動 (
say_with_timeの実装) には変更はありません。 - Rails を利用するアプリケーションの本番挙動やログフォーマットには影響しません。
- 目的は「CI の flakiness(ときどき落ちる不安定テスト)の解消」であり、
- 経過時間の数値自体に意味を持たせたいテストではなく
- 「
say_with_timeの出力フォーマット回帰テスト」であることから、この変更は妥当です。
- 同様に、時間・タイミングに依存する他のテストでも、厳密値ではなくフォーマットと範囲をテストする方が CI を安定化しやすい、という実例として参考にできます。
- 参考情報 (あれば)
- PR 本文にある CI 失敗ログ (Rails Nightly / Buildkite):
- フォローアップ元 PR:
https://github.com/rails/rails/pull/45705
(say_with_time関連の回帰テストを導入した、あるいは変更した PR の後続対応と位置づけられています)
#50431 Add exclusion_constraint_exists? and unique_constraint_exists? helpers
マージ日: 2026/5/19 | 作成者: @fatkodima
- 概要 (1-2文で)
PostgreSQL アダプタに、排他制約(exclusion constraint)とユニーク制約(unique constraint)が既に存在するかをチェックするためのヘルパーexclusion_constraint_exists?とunique_constraint_exists?が追加されました。これにより、制約を「存在しなければ追加する」といった形で、より安全かつ冪等的にマイグレーションを書くことができます。
- 変更内容の詳細(あればサンプルコードも含めて)
2-1. 追加されたヘルパーメソッド
PostgreSQL 用の ActiveRecord::ConnectionAdapters::PostgreSQLAdapter に以下が追加されています。
exclusion_constraint_exists?unique_constraint_exists?
既存の check_constraint_exists? や foreign_key_exists?, index_exists? と同じ思想の API です。
想定されるインターフェイス
PRの説明・差分構成から、以下のような呼び出しが可能と推測されます(実際の引数名は他の *_constraint_exists? メソッドとほぼ同様になる想定):
# 排他制約の存在確認
exclusion_constraint_exists?(
:bookings,
name: "bookings_no_overlap"
)
# 条件付きユニーク制約の存在確認
unique_constraint_exists?(
:users,
name: "index_users_on_email_where_active"
)あるいは、change_table ブロック内の DSL からも利用できるようにテストが追加されているため、例えば次のようなスタイルがサポートされています:
change_table :bookings do |t|
unless t.exclusion_constraint_exists?(name: "bookings_no_overlap")
t.exclusion_constraint :room_id,
using: :gist,
with: "&&",
where: "cancelled_at IS NULL",
name: "bookings_no_overlap"
end
endchange_table :users do |t|
unless t.unique_constraint_exists?(name: "unique_active_email")
t.unique_constraint :email, where: "deleted_at IS NULL", name: "unique_active_email"
end
end※ 正確な DSL シグネチャは、既存の check_constraint / check_constraint_exists? とほぼ同じパターンになるよう実装されています。
2-2. 実装箇所
schema_definitions.rbChangeTableDefinition/TableDefinitionなどに、exclusion_constraint_exists?およびunique_constraint_exists?を委譲するメソッドが追加されています。- これにより、
change_tableブロック内部からも直感的に存在確認が可能になります。
schema_statements.rb- PostgreSQL アダプタに
exclusion_constraint_exists?/unique_constraint_exists?の実装が追加。 - 実装としては PostgreSQL の
pg_constraintやpg_indexなどのシステムカタログ/情報スキーマを用いたクエリになっており、指定されたtable_nameとconstraint name(あるいは対象カラム等)に一致する制約があるかどうかをチェックしています。 - 既存の
check_constraint_exists?と同様のコードパス・クエリスタイルで整合性が保たれています。
- PostgreSQL アダプタに
テスト (
exclusion_constraint_test.rb,unique_constraint_test.rb,change_table_test.rb)- 「制約を追加 →
*_constraint_exists?がtrueを返す」パス。 - 「制約が存在しない場合に
falseを返す」パス。 change_tableブロック内からの利用をカバーするテスト。- これにより、API が今後も後方互換性を保ちながら動作することが保証されています。
- 「制約を追加 →
- 影響範囲・注意点
3-1. 影響範囲
- 対象は PostgreSQL アダプタのみ(
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)。 - すでに存在していた
- 排他制約サポート (
add_exclusion_constraintなど) - ユニーク制約サポート (
add_unique_constraintなど) を補完する「存在確認」機能の追加であり、既存の API の挙動を変えるものではありません。
- 排他制約サポート (
- 追加メソッドを利用しない既存コードには影響しません。
3-2. 開発者が意識するとよい点
マイグレーションの冪等性確保:
今後は排他制約・ユニーク制約も以下のようなパターンで安全に記述できます。
rubydef up unless exclusion_constraint_exists?(:bookings, name: "bookings_no_overlap") add_exclusion_constraint :bookings, "tsrange(start_at, end_at)", using: :gist, name: "bookings_no_overlap" end end def down remove_exclusion_constraint :bookings, name: "bookings_no_overlap" endrubydef up unless unique_constraint_exists?(:users, name: "unique_active_email") add_unique_constraint :users, :email, name: "unique_active_email", where: "deleted_at IS NULL" end end
スキーマ管理ツールやプラグイン:
- 「実 DB と schema.rb / structure.sql の差分を調べて同期する」といったツールから、
*_constraint_exists?を通じて排他制約・ユニーク制約も安全に扱えるようになります。
- 「実 DB と schema.rb / structure.sql の差分を調べて同期する」といったツールから、
RDBMS 依存性:
- これらのメソッドは PostgreSQL 固有機能(排他制約・複雑なユニーク制約)に依存しているため、MySQL / SQLite など他アダプタでは利用できません。
- アプリケーションが DB 抽象化を重視する場合は、呼び出し前に
connection.adapter_nameで PostgreSQL かどうかを確認するなどの考慮が必要です。
- 参考情報 (あれば)
- 排他制約の追加 PR: https://github.com/rails/rails/pull/40224
- ユニーク制約の追加 PR: https://github.com/rails/rails/pull/46192
- 本 PR: Add
exclusion_constraint_exists?andunique_constraint_exists?helpers (#50431) - 類似 API:
check_constraint_exists?foreign_key_exists?index_exists?と同じパターンで利用できるため、これらの既存ドキュメントを参照すると使用感をつかみやすいです。
#57377 Add enforced: option for foreign keys on PostgreSQL 18.4+
マージ日: 2026/5/19 | 作成者: @yahonda
- 概要 (1-2文で)
PostgreSQL 18.4 以降を対象に、Active Record の外部キーにenforced:オプションが追加され、「参照整合性チェックを一時的に無効化して後から再検証する」ことを Rails の API 経由で安全に行えるようになりました。これにより、これまで superuser 権限やトリガ無効化に頼っていたバルクロードなどのユースケースを、テーブル所有者権限のみで実現できます。
- 変更内容の詳細
新しくできることの概要
add_foreign_keyでenforced: falseを指定可能(PostgreSQL 18.4+ のみ)- 対応する外部キー制約は
NOT ENFORCEDで作成される - DML(INSERT/UPDATE/DELETE)時の参照整合性チェックがスキップされる
- 対応する外部キー制約は
change_foreign_keyで既存外部キーの enforced 状態をトグル可能- 一時的に
NOT ENFORCEDにしてバルクロード → 再度ENFORCEDに戻す、といった運用が Rails のマイグレーションだけで完結
- 一時的に
- スキーマダンプ(schema.rb / structure.sql)に
enforced:とvalidate:が正しく反映される
PostgreSQL 側の仕様との対応
- PostgreSQL 18.0 で
NOT ENFORCEDな外部キー制約が導入されたが、18.3 まではDEFERRABLE属性が失われる不具合があった - PostgreSQL 18.4 で「外部キーの deferrability が失われるバグ」が修正されたため、Rails は「18.4 以降のみ対応」というポリシーにしている
NOT ENFORCEDな外部キーは内部的にNOT VALIDとして扱われるVALIDATE CONSTRAINTは適用できない- そのため schema dumper は
enforced: falseとvalidate: falseの両方を出力する
具体的な API 変更と使い方
1) add_foreign_key に enforced: オプション追加
# 例: posts.user_id -> users.id の外部キーを「一時的にチェックなし」で作る
add_foreign_key :posts, :users, enforced: falsePostgreSQL 18.4+ では、これが内部的におおよそ以下のような SQL に相当します(実際の SQL は Rails が組み立てます):
ALTER TABLE posts
ADD CONSTRAINT fk_posts_users
FOREIGN KEY (user_id)
REFERENCES users(id)
NOT ENFORCED;この状態では:
- 外部キー制約は存在するが、DML 時に参照整合性チェックは行われない
- ただし制約オブジェクト自体はあるため、後から ENFORCED に戻すことができる
2) change_foreign_key による enforced 状態のトグル
新メソッド change_foreign_key が追加され、既存制約の enforced/NOT ENFORCED を切り替え可能です。
# 既存外部キーを NOT ENFORCED にする
change_foreign_key :posts, :users, enforced: false
# 再度 ENFORCED に戻す(このとき PostgreSQL が全行を再検証する)
change_foreign_key :posts, :users, enforced: trueenforced: trueに戻す際、PostgreSQL はテーブル全行をチェックして不正データがないか検証します- 不正な行があればエラーになり、ENFORCED への切り替えは失敗します
- これは「トリガ再有効化では既存行は再検証されない」という従来の問題を解消するポイントです
3) 抽象層・アダプタ周りの変更
主な変更ポイント:
ActiveRecord::ConnectionAdapters::AbstractAdaptersupports_enforced_constraints?(名称は概念的; 実際は POSTGRES 18.4+ チェック用の capability フラグ)を導入している形- PostgreSQL アダプタが 18.4 以上かどうかを判定するロジックを追加
schema_statements.rb/postgresql/schema_statements.rbadd_foreign_key/change_foreign_keyでenforced:を考慮して SQL を組み立てる実装を追加
schema_creation.rb- constraint 生成時に
NOT ENFORCEDを付与するロジックを追加
- constraint 生成時に
schema_dumper.rb- 外部キー出力時に
enforced: falseとvalidate: falseの両方を出すよう変更
- 外部キー出力時に
schema_definitions.rb- 外部キー定義オブジェクトに
enforced属性を追加
- 外部キー定義オブジェクトに
4) テストの追加
activerecord/test/cases/migration/foreign_key_test.rb に大量のテストが追加されています:
enforced: falseで作成された外部キーが期待通りNOT ENFORCEDになることchange_foreign_keyで ENFORCED/NOT ENFORCED を正しくトグルできること- schema dump / load(マイグレーション → schema.rb → 再ロード)の往復で
enforced:の状態が保持されること
- 影響範囲・注意点
対応バージョン制約
- PostgreSQL 18.4 未満では
NOT ENFORCED外部キーの仕様が不完全なため、この機能は PostgreSQL 18.4+ 専用 機能です- Rails はバージョンをチェックし、サポートしないバージョンで
enforced:を使った場合は例外を投げる/無視する(実装に依存)可能性があります - 実際に利用する際は DB バージョンを厳密に確認してください
- Rails はバージョンをチェックし、サポートしないバージョンで
NOT ENFORCED の使いどころとリスク
- 主な用途は「バルクロード・大量データ移行時の一時的な制約無効化」
- 例えば:
- 参照先テーブル・参照元テーブルを任意順でロード
- ロード中は
enforced: falseにしておく - ロード完了後に
change_foreign_key enforced: trueで再検証しつつ有効化
- 例えば:
- ただし
NOT ENFORCEDの間は不整合なデータが普通に入り得る ため、- ENFORCED に戻すときにエラーになる可能性がある
- ロードスクリプトやマイグレーションのロールバック戦略をきちんと設計する必要がある
既存コードへの影響
- 既存の
add_foreign_key/remove_foreign_key/add_referenceなどは、enforced:を指定しない限り動作は変わりません schema.rb/structure.sqlの出力形式に、外部キー周りでenforced: falseおよびvalidate: falseが追加される可能性があります- スキーマ差分を CI でチェックしている場合、PostgreSQL 18.4+ に上げたタイミングで差分が発生することがあります
将来の変更との関係
- PR 説明にもある通り、「次の PR で
disable_referential_integrityをNOT ENFORCEDベースに書き換える」予定になっています- 現状(PostgreSQL 17 以前)は
DISABLE TRIGGER ALLに依存しており、superuser 権限が必要 - これが
NOT ENFORCEDベースに置き換わると、superuser 不要でより安全に一時的な外部キー無効化が行えるようになる見込みです
- 現状(PostgreSQL 17 以前)は
- 参考情報 (あれば)
- PostgreSQL 18.0 リリースノート
- CHECK / 外部キー制約への
NOT ENFORCED導入 - https://www.postgresql.org/docs/release/18.0/
- CHECK / 外部キー制約への
- PostgreSQL 18.4 リリースノート
- 「外部キーの deferrability が失われるバグ」修正
- https://www.postgresql.org/docs/release/18.4/
- Rails 側での過去の試み
SET CONSTRAINTS ALL DEFERREDによる回避は、「NO ACTION 以外の参照動作は defer できない」問題によりリバート済み- https://github.com/rails/rails/commit/2812694aa65e4d0591e0ea6c5f7d8c97927be7dc
#57388 http_cache_forever now accept an optional last_modified: keyword parameter.
マージ日: 2026/5/19 | 作成者: @byroot
- 概要 (1-2文で)
http_cache_foreverがオプション引数last_modified:を受け取れるようになり、デフォルトの日付(2011-01-01)の代わりに実際の更新時刻を使った長期キャッシュ制御が可能になりました。あわせて Active Storage のProxyControllerで、この新オプションを使うように変更されています。
- 変更内容の詳細
2-1. http_cache_forever に last_modified: キーワード引数が追加
これまで:
http_cache_forever do
# ...
end- 内部的に
Last-Modifiedが固定日付(2011-01-01)として扱われる挙動だった。 - 実際には「永遠に変わらない静的なリソース」を想定したヘルパだったが、
Last-Modifiedを明示的に指定することはできなかった。
今回の変更後:
http_cache_forever(last_modified: some_time) do
# ...
endlast_modified:に任意のTime/ActiveSupport::TimeWithZone等を渡せるようになった。- 引数を渡さなかった場合は、引き続きデフォルトの 2011-01-01 が使われる(後方互換性維持)。
イメージ例:
class AssetsController < ApplicationController
def show
asset = Asset.find(params[:id])
http_cache_forever(last_modified: asset.updated_at) do
send_data asset.data, type: asset.content_type, disposition: "inline"
end
end
endこれにより、
Cache-Control: public, max-age=...(「実質 forever」な長期キャッシュ)Last-Modified: asset.updated_at
のようなヘッダを組み合わせて返せるため、クライアントは If-Modified-Since を使った条件付きリクエストを行えます。
2-2. Active Storage ProxyController での利用
activestorage/app/controllers/active_storage/blobs/proxy_controller.rb が 1 行変更され、http_cache_forever 呼び出し時に last_modified: を渡すようになりました。
ざっくりしたイメージ:
# 変更前
http_cache_forever do
# blob をプロキシして返す
end
# 変更後
http_cache_forever(last_modified: @blob.created_at) do
# あるいは updated_at / service_metadata 由来の時刻等
end実際には Active Storage の blob モデルが持つタイムスタンプ(例: @blob.created_at や @blob.updated_at)が Last-Modified に反映されるようになっています。
これにより、Active Storage のプロキシエンドポイント(例: /rails/active_storage/blobs/proxy/...)でも、実際の BLOB の最終更新時刻に基づく HTTP キャッシュ制御が行われます。
2-3. テスト・CHANGELOG の更新
actionpack/test/controller/render_test.rbに、last_modified:を指定した場合の挙動を検証するテストが追加。- 指定した
last_modifiedがレスポンスヘッダに反映されるか If-Modified-Sinceを含む条件付きリクエストに対して 304 が返るか
などが確認されていると考えられます。
- 指定した
actionpack/CHANGELOG.mdとactivestorage/CHANGELOG.mdに、この機能追加についての記述が追加。
- 影響範囲・注意点
- 互換性
http_cache_foreverの既存呼び出しは、引数なしでこれまで通り利用可能です。デフォルトの日付も変わっていないため、既存コードの挙動は基本的に変わりません。
last_modifiedの指定に関する注意- 実際に内容が変わらない限りタイムスタンプを更新しない値(例: モデルの
updated_atやファイルの実更新日時)を渡す必要があります。 - 内容が変わっていないのに
last_modifiedが頻繁に変わる値(現在時刻など)を渡すと、クライアント側キャッシュが無効化されて毎回 200 で返ってしまい、「forever キャッシュ」のメリットが失われます。
- 実際に内容が変わらない限りタイムスタンプを更新しない値(例: モデルの
- Active Storage を使っている場合
- Active Storage の ProxyController 経由でアクセスしているクライアントは、BLOB の更新に応じて
Last-Modifiedが変わるようになります。 - これにより、ブラウザや CDN がより正確にキャッシュの再検証(304 Not Modified)を行えるようになり、帯域やレスポンス時間の改善が期待できます。
- Active Storage の ProxyController 経由でアクセスしているクライアントは、BLOB の更新に応じて
- ETag との組み合わせ
- コード側で ETag を併用している場合、
Last-Modifiedと整合的な運用を行うべきです(内容が変わった時に両方が変わる状態が望ましい)。
- コード側で ETag を併用している場合、
- 参考情報 (あれば)
- Rails Guides: Caching with Rails
- 特に「HTTP Caching」の章(
expires_in,fresh_when,stale?,etag,last_modifiedなど)
- 特に「HTTP Caching」の章(
- Rails ソース:
actionpack/lib/action_controller/metal/conditional_get.rbhttp_cache_forever,fresh_when,stale?の実装周辺
activestorage/app/controllers/active_storage/blobs/proxy_controller.rb- Active Storage の BLOB プロキシ配信の流れと、キャッシュヘッダの付与部分
#46262 Support proc and symbol for NumericalityValidator's :in option
マージ日: 2026/5/19 | 作成者: @ThomasSevestre
- 概要 (1-2文で)
NumericalityValidatorの:inオプションで、固定の Range だけでなく「Proc(ラムダ)」と「シンボル(インスタンスメソッド参照)」も指定できるようになりました。これにより、モデルインスタンスの状態に応じて動的に数値の許容範囲を変えられます。
- 変更内容の詳細
何ができるようになったか
これまで:
validates_numericality_of :price, in: 0..100のように、:in には固定の Range しか渡せませんでした。
このPRにより、以下のように Proc および シンボル も使えるようになります。
Proc を使うパターン
validates_numericality_of :price, in: ->(record) { 0..record.max_price }- ブロックは
(record)(検証対象のモデルインスタンス)を引数に取り、Rangeを返す。 - レコードごとに
max_priceが違う場合でも、その値を元に許容範囲を計算可能。
シンボルを使うパターン
class Product < ApplicationRecord
validates_numericality_of :price, in: :price_range
def price_range
0..max_price
end
end:inに渡したシンボル名のインスタンスメソッドが呼ばれ、その戻り値をRangeとして扱う。#price_rangeはRangeを返す必要がある。
コード上の変更ポイント(概要)
activemodel/lib/active_model/validations/numericality.rb にて:
:inオプションを解決する際に、Range→ そのまま利用Proc→call(record)して得た値を利用Symbol→record.public_send(symbol)して得た値を利用 という分岐が追加された形になっています(実装としてはこれに準じるロジック)。
それに応じたテストが numericality_validation_test.rb に追加され、
- Proc 指定で正しく Range が評価されること
- シンボル指定でインスタンスメソッドが呼び出されること
- 期待どおりにバリデーションが通る/落ちること
がカバーされています。
また、activemodel/CHANGELOG.md にこの新機能が記載され、外部API変更として明示されています。
- 影響範囲・注意点
戻り値は必ず Range を返す必要がある
Proc/メソッド(シンボル指定)ともに、戻り値はRangeオブジェクトを想定しています。nilや他の型を返すと例外発生や意図しない挙動となる可能性が高いです。毎回評価される
Proc やメソッドはバリデーション実行時に都度呼ばれるため、- 副作用のある処理
- 重い計算・外部API呼び出し
などは避けるべきです。
既存コードへの互換性
既存のRangeを直接指定しているコードはそのまま動作し、挙動変更はありません。
追加で Proc / シンボルが許可されただけなので、互換性の観点でのリスクは低いです。マルチスレッド環境での安全性
評価対象はレコードインスタンスに紐づく Proc / メソッド呼び出しなので、
クラス変数やグローバル状態を使った Range 生成を行う場合は、スレッドセーフかどうか注意してください。
- 参考情報 (あれば)
- 該当PR: https://github.com/rails/rails/pull/46262
- 近い機能として、
NumericalityValidator以外のバリデータ(e.g.inclusion,exclusion)でも、:inに Proc / シンボルを渡して動的なコレクションを指定するパターンが既に存在します。このPRはそれらとインターフェイスを揃える形の拡張と言えます。
#57386 Use ... and anonymous splats when possible
マージ日: 2026/5/19 | 作成者: @byroot
- 概要 (1-2文で)
このPRは、activesupportのString拡張(output_safety関連)で、可変長引数の扱いを「*argsから.../ 匿名スプラット」へ書き換えるリファクタリングです。機能仕様は変えずに、わずかなオブジェクト生成削減と、よりモダンで読みやすいRubyコードに整理しています。
- 変更内容の詳細
対象ファイル:
activesupport/lib/active_support/core_ext/string/output_safety.rb
やっていることは一貫していて、「明示的な引数配列 (*args)」→「...(引数フォワーディング)と匿名スプラット」を使うスタイルへの移行です。
典型的な変更パターンは次のようなものです(概念的なイメージ):
# Before
def concat(*args)
safe_concat(*args)
end
# After (Ruby 2.7+ の引数フォワーディング)
def concat(...)
safe_concat(...)
endあるいは、ブロック付きでメソッドに引数をそのまま渡すケースで:
# Before
def some_method(*args, &block)
super(*args, &block)
end
# After
def some_method(...)
super(...)
end「anonymous splats」という表現は、def foo(*) のように引数を受け取るが名前を付けない(参照しない)パターンや、(... ) のように「配列として受け取らずにそのままフォワードする」ことを指しています。
このPRでは、output_safety.rb 内で、html_safe を扱うためのラッパーメソッド・デリゲーションメソッドなどに対して、上記のような書き換えが行われています。
結果として:
- メソッド内で引数の配列(
args)が不要な箇所は匿名化し、 - ただ他メソッドへそのまま委譲するだけのメソッドは
...によるフォワーディングに変更
という統一的なスタイルになっています。
- 影響範囲・注意点
- 公開APIの挙動変更はありません。
- 引数の受け取り方や返り値、
html_safe系メソッドの意味的な振る舞いは変わりません。
- 引数の受け取り方や返り値、
- 内部実装として、
*argsによる配列生成が減るため、わずかにメモリアロケーションとオーバーヘッドが減ります(PR本文の "minor allocation reduction")。 - Ruby 2.7 以降で導入された
...構文を前提としているため、ランタイムのサポートバージョンと合致していることが前提です(Rails本体のサポート範囲内なので問題はありません)。 - output_safety 周り (
String#html_safe、ActiveSupport::SafeBuffer系) をモンキーパッチしている場合:- 引数の配列名(例:
args)に依存していたり、メソッドシグネチャを厳密に反射しているようなコードがあれば影響する可能性がありますが、一般的には稀なケースです。
- 引数の配列名(例:
- 参考情報 (あれば)
- Rubyの引数フォワーディング (
def foo(...)) の公式ドキュメント:- Ruby 3.0+ リファレンス(英語): https://docs.ruby-lang.org/en/master/syntax/methods_rdoc.html#label-Argument+Forwarding
- Railsの
output_safety/SafeBuffer周りの解説:- Railsガイド「レイアウトとレンダリング」内の
html_safe説明など: https://guides.rubyonrails.org/layouts_and_rendering.html
- Railsガイド「レイアウトとレンダリング」内の
- PR: https://github.com/rails/rails/pull/57386
#57385 fix start_with_delimiter? has a false-positive when the number coincidentally begins with the delimiter string in number_to_phone
マージ日: 2026/5/19 | 作成者: @tahsin352
- 概要 (1-2文で)
Rails のnumber_to_phoneで、電話番号の数字列がたまたま区切り文字(delimiter)と同じ並びで始まる場合に、先頭の数字が誤って削られてしまうバグを修正した PR です。先頭の区切り文字を事後的に削るロジックをやめ、gsubの置換ブロック内で「最初のグループが空かどうか」を見て直接出力を制御するように変更しています。
- 変更内容の詳細
問題の背景
number_to_phone は内部で正規表現と gsub を使って、数字列を「市外局番・市内局番・加入者番号」のように 3 パートに分割し、delimiter で連結しています。
元の実装では、先頭グループ (\d{0,3}) が「0〜3桁」の可変長であり、空文字列になるケース(たとえば桁数が少ないなど)を考慮するために、以下のようなパターンを取っていました。
- まず
gsubで「常にpart1 + delimiter + part2 + delimiter + part3」という形の文字列にする - その後、
start_with_delimiter?で「先頭が delimiter で始まっているか」をチェックし、始まっている場合はslice!で先頭の delimiter 分を削る
ここで問題なのは、start_with_delimiter? が
number.start_with?(delimiter)という単純な文字列判定で、**「先頭の delimiter が、本当に“空の第1グループ”由来なのか」**を区別できないことです。
PR の説明にある具体例:
number_to_phone(9001234, delimiter: "900")
# gsub の結果: "900900-1234"
# - \1 = "900"(空ではない)
# - しかし文字列は "900" で始まるので:
start_with_delimiter?("900900-1234") # => true
# slice!(0, 3) により:
# => "900-1234" となり、先頭の「900」が消えてしまう(不正)ここでは「最初の 3 桁 900」が本物の番号の一部なのに、「delimiter と同じ値だから」という理由だけで削られてしまっています。
具体的な修正内容
この PR では、上記の「置換後に先頭の delimiter を見て削る」という構造を改め、gsub の置換ブロックの中で $1(最初のグループ)の内容を見て、その場で出力を決定するように変えています。
変更のポイント:
start_with_delimiter?メソッドを 完全に削除gsubの置換ロジックを以下のように変更(擬似コード):rubyformatted = number.to_s.gsub(phone_pattern) do part1, part2, part3 = $1, $2, $3 if part1.empty? # 最初のグループが空の場合は delimiter を出さない "#{part2}#{delimiter}#{part3}" else # 最初のグループがある場合のみ、先頭に part1 + delimiter "#{part1}#{delimiter}#{part2}#{delimiter}#{part3}" end endこれにより、「最初の delimiter を出すかどうか」は「
$1が空かどうか」だけで決まるようになり、
「数字列の先頭がたまたま delimiter と同じ文字列だった」というだけでは削除されなくなります。テスト (
activesupport/test/number_helper_test.rb) に、マルチバイト/複数文字のdelimiterと今回のバグケースをカバーする 回帰テストを 2 つ追加。
これにより、将来的に同種のロジック変更で再発しないようにしています。
- 影響範囲・注意点
- 対象メソッドは
ActiveSupport::NumberHelper#number_to_phoneのみです。 - 影響するのは主に以下の条件を満たすケースです:
number_to_phoneを使っているdelimiterに複数文字の文字列を指定している(例:"900","---"など)- かつ「電話番号の先頭の数字列」が、その
delimiterの文字列と一致する場合
- これまでそのようなケースでは、先頭の数桁が誤って削られていた可能性があります。今回の修正によって、本来の数字が保持されるようになるため、挙動が変わりますが、これはバグ修正として正しい方向の互換性変更です。
start_with_delimiter?が内部ロジックとして削除されただけで、パブリック API には影響ありません(number_to_phoneの引数・戻り値の形は変わらない)。
- 参考情報 (あれば)
- この PR は直前の修正 PR https://github.com/rails/rails/pull/57375 をフォローアップする位置づけで、
そこではマルチキャラ delimiter 周りのバグを扱っており、その副作用として今回の false positive 問題が顕在化したものと思われます。 - 実装上の設計として、「正規表現のキャプチャ結果(
$1〜)に依存するなら、その情報が生きているgsubブロック内でロジックを完結させる」方針になっており、後処理で文字列だけを見て判断するより安全です。
#57384 Accept and discard unused block in ReflectionProxy#all_includes
マージ日: 2026/5/19 | 作成者: @joshuay03
- 概要 (1-2文で)
Ruby 3.4 のstrict_unused_block警告に対応するため、Active Record のReflectionProxy#all_includesが渡されたブロックを明示的に受け取りつつ破棄するように変更されました。挙動の変更はなく、警告を抑制するだけのメンテナンス的修正です。
- 変更内容の詳細
対象:activerecord/lib/active_record/associations/association_scope.rb 内の ReflectionProxy#all_includes
背景:
- Ruby 3.4 で
strict_unused_blockという警告カテゴリが導入され、
「ブロックを受け取っている呼び出し元」→「ブロックを宣言していない呼び出し先」
というパターンで警告が出るようになりました。 ReflectionProxy#all_includesは、add_constraintsからブロック付きで呼ばれるものの、「何もしない(No-op、ブロックを意図的に無視する)」実装になっており、そのためstrict_unused_blockに引っかかっていました。
変更内容:ReflectionProxy#all_includes がブロックを匿名転送で受け取り、即座に捨てるように修正されています。
イメージとしては、以下のような変更です(実際のコードは行数的に +1/-1 なので細部は多少異なる可能性がありますが、概念的にはこれ):
# 変更前(イメージ)
def all_includes
reflection.all_includes
end
# 変更後(イメージ)
def all_includes(&)
reflection.all_includes(&)
endあるいは、ReflectionProxy 側で「ブロックを受け取るが使わない」ことをはっきりさせるため、次のような書き方をしている可能性もあります:
def all_includes(&)
# ブロックは受け取るが、ReflectionProxy では意図的に何もしない
reflection.all_includes
endいずれにせよ、「ブロックを受け取ることをメソッドシグネチャで明示」することで、Ruby が「ブロックが意図的に無視されている」と判断できるようにし、strict_unused_block 警告が出ないようにしています。
ポイント:
ReflectionProxy#all_includesは、本来add_constraintsが組み立てるincludes情報を拾うためのフックですが、ReflectionProxyはそのチェーン上で「特に何もしない」役割のため、元々ブロックを利用していません。- その「何もしない」こと自体は変えずに、「ブロックを受け取っている」という事実だけを宣言的に追加した変更です。
- 影響範囲・注意点
- 実行時の挙動・SQL・関連のロードロジックなど、Active Record のユーザが体感する動作は変わりません。
- 影響は主に以下のケースでの「警告メッセージの有無」のみです:
- Ruby 3.4 以降
strict_unused_block警告を有効 (Warning[:strict_unused_block] = trueなど) にしている環境
- 既存アプリケーションのコード変更は不要です。
ReflectionProxyを直接拡張している、あるいはモンキーパッチしているような高度なメタプログラミングをしていない限り、互換性上の問題は発生しないと考えられます。
- 参考情報 (あれば)
- Ruby 3.4 の
strict_unused_block警告カテゴリは、「ブロックが呼び出し元から渡されているのに、呼び出し先で一切宣言されていない・使われていない」ケースを検出するためのものです。 - この PR は Active Record を Ruby 3.4 の警告ポリシーに合わせるための、事実上の互換性・静的品質改善 PR です。
- 類似の変更は、他の gem やフレームワークでも Ruby 3.4 対応として行われることが予想されます。
#46486 Preserve attachment changes when converting record to another class using STI
マージ日: 2026/5/18 | 作成者: @fatkodima
- 概要 (1-2文で)
STI(Single Table Inheritance)でレコードのtypeを変更して別クラスに「キャスト」した際に、Active Storage の添付ファイル変更(添付・削除)が失われてしまう不具合を修正したPRです。becomes/becomes!を使ってクラスを変換した場合でも、添付ファイルの変更が正しく引き継がれるようになりました。
- 変更内容の詳細
問題の背景
- Active Record では、
becomes/becomes!を使って STI のサブクラス間でオブジェクトを変換できます。rubyuser = User.find(1) # type: "User" admin = user.becomes(AdminUser) # type: "AdminUser" (オブジェクトのクラスが変わる) - しかし、Active Storage の添付(
has_one_attached/has_many_attached)に対してrubyのようにクラス変換を挟むと、「attach/delete の pending な変更」が新しいオブジェクト側にコピーされず、添付の変更が失われるケースがありました。record.avatar.attach(...) record = record.becomes(OtherType) record.save - Issue #45778 でこの挙動が報告され、このPRで修正されています。
主なコード変更ポイント
1) Active Storage モデルモジュールへの対応追加
activestorage/lib/active_storage/attached/model.rb に、STI 変換時に添付の変更状態を引き継ぐためのフックが追加されています。
イメージとしては、Active Record が becomes / becomes! 時に内部的にクローンを作るタイミングで、Active Storage 側が「添付の変更情報(change オブジェクト)」を新しいインスタンスへコピーする仕組みを加えた、という形です。
具体的には以下のような処理が入っていると考えられます(実際のコードは多少異なりますが、概念的にはこういうことをしています):
module ActiveStorage::Attached::Model
# ...
def init_internals_for_dup
super
copy_active_storage_attachment_changes
end
private
def copy_active_storage_attachment_changes
# 各 attachment にぶら下がっている変更オブジェクトを
# 新しいレコードインスタンスに紐付け直す
end
endポイントは「Active Record が内部的に dup/clone するフローにフックし、Active Storage の変更状態も一緒に複製する」ことです。
2) 変更オブジェクト (CreateOne/CreateMany/DeleteOne/DeleteMany) の微調整
以下の4つのクラスに1行ずつ変更が入っています。
activestorage/lib/active_storage/attached/changes/create_one.rbactivestorage/lib/active_storage/attached/changes/create_many.rbactivestorage/lib/active_storage/attached/changes/delete_one.rbactivestorage/lib/active_storage/attached/changes/delete_many.rb
record を内部で持っているこれらの「変更オブジェクト」が、STI 変換後の新しいレコードにも正しく紐づくように、dup / clone 時の挙動や record の参照の持ち方が調整されています。
概念的には、以下のような方向の修正です:
- 変更オブジェクトが特定のレコードインスタンスに強く結びついており、
becomes後のインスタンスとは無関係になってしまう → 新しいインスタンスに紐付け直せるようにする・あるいはレコードの差し替えができるようにする。
3) テストの追加
activestorage/test/models/attached/one_test.rbactivestorage/test/models/attached/many_test.rb
にテストが追加され、次のようなシナリオがカバーされています:
has_one_attached/has_many_attachedを持つ STI モデルで- 添付を追加 or 削除する
becomesで別のサブクラスに変換する- 保存する
- このとき、添付の追加・削除が期待通り DB に反映されることを確認
また、test_helper や migration (create_users_migration.rb) に1行ずつ追加があり、テスト用に STI の型カラムや対象モデル定義が準備されています。
4) CHANGELOG の更新
activestorage/CHANGELOG.mdに、上記バグ修正がリリースノートとして追記されています。
- 影響範囲・注意点
- 対象:
- Active Storage を利用しており、かつ Active Record の STI を使っているアプリケーション
- 特に、
becomes/becomes!/typeを書き換えるロジックの中で添付ファイルの変更を行うケース
- 振る舞いの変更点:
- これまでは、STI クラス変換の途中で行った
attach/purgeなどが保存時に失われることがあったが、今後は保持されるようになります。 - すでにこのバグを前提としたワークアラウンド(例えば「
becomesする前に先に save しておく」「添付変更はクラス変換とは別トランザクションにする」など)を組んでいた場合、その必要がなくなります。
- これまでは、STI クラス変換の途中で行った
- 互換性:
- 正しい動作に近づく方向の修正であり、通常は望ましい互換性改善です。
- もし「STI 変換中の添付変更をあえて捨てる」ような特殊な依存をしている場合は挙動が変わる可能性があります。
- パフォーマンス:
- 添付の「変更オブジェクト」を複製・引き継ぐ処理が増えましたが、通常は微小なオーバーヘッドに留まると考えられます。
- 参考情報 (あれば)
- 対象Issue: https://github.com/rails/rails/issues/45778
- 当該PR: https://github.com/rails/rails/pull/46486
- 関連ドキュメント:
STI モデルで Active Storage を利用している場合は、この修正が含まれる Rails バージョンにアップデートすることで、becomes 前後で添付の変更が安全に扱えるようになります。
#57375 Fix number_to_phone dropping only one char of a multi-char delimiter
マージ日: 2026/5/18 | 作成者: @55728
- 概要 (1-2文で)
number_to_phoneに複数文字の:delimiterを指定し、かつ市外局番なし(7桁程度)の番号を渡した場合に、先頭に区切り文字の“欠片”が残ってしまうバグを修正したPRです。slice!(0, 1)で1文字だけ削っていた処理を、指定された区切り文字列全体の長さぶん削るように変更しています。
- 変更内容の詳細
問題の挙動
number_to_phone で :delimiter に複数文字の文字列を渡すと、番号に市外局番がないケースで先頭に中途半端な区切りが残っていました:
number_to_phone(5551234, delimiter: " - ")
# 期待: "555 - 1234"
# 実際: "- 555 - 1234"
number_to_phone(5551234, delimiter: ", ")
# 期待: "555, 1234"
# 実際: " 555, 1234"背景として、number_to_phone 内部の convert_without_area_code では、電話番号を正規表現で3桁 + 4桁のように分解する際、先頭グループ(\d{0,3})が空になる場合があります(≒ 市外局番なしの7桁)。
このとき、内部表現として組み立てた文字列が「区切り文字 + 残り番号」のような形になり、余計な先頭区切りを slice!(0, 1) で削っていました。
しかし slice!(0, 1) は「先頭1文字だけ削る」ため、区切り文字が " - " や ", " のように複数文字の場合は、一文字分だけが削られて残りが先頭に残ってしまう、というのが今回のバグです。
このバグは、
- 7桁以下(市外局番なし)
:delimiterが2文字以上
の組合せでのみ発生し、かつテストは「1文字デリミタ」か「10桁番号」のケースしかカバーしていなかったため長年検知されていませんでした。
修正内容
該当処理を以下のように修正しています:
# 変更前(概念的なコード)
number.slice!(0, 1) if start_with_delimiter?(number)
# 変更後
number.slice!(0, delimiter.length) if start_with_delimiter?(number)ポイント:
start_with_delimiter?の実装上、delimiterが空文字列では呼ばれないためdelimiter.length >= 1は保証されている。String#slice!は文字(コードポイント)単位の削除なので、マルチバイト文字列のデリミタでも正しく動作する。
これにより、例えば:
number_to_phone(5551234, delimiter: " - ")
# => "555 - 1234"
number_to_phone(5551234, delimiter: "—")
# => "555—1234"
number_to_phone(5551234, delimiter: "・")
# => "555・1234"のように、ASCIIでもマルチバイトでも期待どおりのフォーマットになります。
テスト追加
activesupport/test/number_helper_test.rb に以下のようなケースが追加されています(概念的な例):
- ASCIIの複数文字デリミタ:
delimiter: " - " - マルチバイトデリミタ:
delimiter: "—"など
これにより、今回のバグパターン(7桁 + 複数文字/マルチバイトデリミタ)が今後退行しないようになっています。
- 影響範囲・注意点
- 影響を受けるのは以下の条件の
number_to_phone呼び出しのみ:- 引数の数字が7桁程度で、市外局番(
:area_code)を付けていない :delimiterに2文字以上の文字列、もしくはマルチバイト文字列を指定している
- 引数の数字が7桁程度で、市外局番(
- 以下は動作が変わりません:
:delimiterが1文字(従来も正しく slice されていた)- 10桁以上の番号(米国形式の典型的な10桁番号など)
:area_code,:country_code,:extensionを指定しているパターン:delimiterが空文字の場合
- 変更内容はユーザーに見えるフォーマットの修正であるため、CHANGELOG にも追記済みです。
- これまで「バグを前提にしたワークアラウンド」(例: わざと先頭に空白が出る前提で文字列を post-process している等)がある場合は挙動が変わりますが、通常の利用では「期待どおりになった」だけであり互換性リスクはきわめて小さいと考えられます。
- 参考情報 (あれば)
- 該当コードは
ActiveSupport::NumberHelper::NumberToPhoneConverter(activesupport/lib/active_support/number_helper/number_to_phone_converter.rb) 内の、エリアコードなし用の変換ロジック。 - 問題のもとになった
slice!(0, 1)は 2011年の #3314 で導入されて以降、テストがカバーしていなかったため、今回まで放置されていました。 - テストスイート (
number_helper_test.rb) はこの修正後も全てグリーン (22 runs, 819 assertions, 0 failures)。
#56918 [RF-Docs] [ci-skip] Update the Rails on Rack guide
マージ日: 2026/5/18 | 作成者: @ayushn21
- 概要 (1-2文で)
Railsガイドの「Rails on Rack」章が大幅に書き直され、現行のRailsの挙動やミドルウェア構成に即した内容へアップデートされました。Railsサーバ起動プロセス、ミドルウェアスタック、Rack APIの高度な使い方、カスタムミドルウェアの組み込み方法などが整理・拡充されています。
- 変更内容の詳細
2-1. 全体的な書き換え・コピー改善
- ガイド全体の文章表現が見直され、最新のガイドラインや他のガイド(Configuration, Instrumentation など)とトーン・構成を揃えるようにリライト。
- 古い情報や現状とズレた記述を削除・更新し、現在のRailsバージョンに合わせた内容に調整。
2-2. bin/rails server と初期化プロセスへのリンク
bin/rails serverが内部的にどのように起動し、Rails::Serverがどのように構成されるかについて、詳細説明をここに書きすぎず、代わりに Initialization ガイドの該当セクションへリンクする形に整理。例:
- 「Railsサーバ起動の詳細なライフサイクル説明」は Initialization ガイド(
Rails::Serverstart-up の節)を参照するよう誘導。 - 本ガイドでは「RackアプリケーションとしてのRails」という観点にフォーカスさせる構成になっていると考えられます。
- 「Railsサーバ起動の詳細なライフサイクル説明」は Initialization ガイド(
2-3. デフォルトミドルウェアスタックの更新
- Rails が起動時に自動でロードする「デフォルトのRackミドルウェアスタック」の説明を最新状態に更新。
- 各ミドルウェアについて、簡単な説明に加えて API ドキュメントへのリンクを付与:
- 例:
ActionDispatch::Static,Rack::MethodOverride,ActionDispatch::ShowExceptions,Rack::Runtimeなど、Rails::Application のconfig.middlewareによって構成される標準スタックの各要素を API リファレンスに誘導。
- 例:
- Sprockets 時代の記述を修正:
Sprockets::Rails::QuietAssets→Propshaft::QuietAssetに更新。
→ 新しいアセットパイプライン(Propshaft)を前提にした構成へアップデートされています。
2-4. 「Internal Middleware Stack」セクションの再構成
- 「内部ミドルウェアスタック」の章構成を、Configuration ガイドや Instrumentation ガイドと同じような見出し・表形式等に合わせて再フォーマット。
- 開発者視点で参照しやすくするため、
- 各ミドルウェア名
- 役割(例:例外処理、静的ファイル配信、セッション管理、ロギングなど)
- 設定方法(
config.middleware.useやconfig.action_dispatch.*など) - APIドキュメントへのリンク
というスタイルに近い形に整理されたと考えられます。
2-5. カスタムミドルウェア追加方法のセクション追加
独自のRackミドルウェアをRailsアプリのスタックに差し込む方法を説明するセクションが新設。
おそらく概ね以下のような内容・コード例が含まれています:
ruby# シンプルなRackミドルウェア例 class RequestTimer def initialize(app) @app = app end def call(env) started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) status, headers, body = @app.call(env) elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at Rails.logger.info "Request took #{elapsed} seconds" [status, headers, body] end endruby# config/application.rb または特定環境の config/environments/*.rb class Application < Rails::Application # スタック末尾に追加 config.middleware.use RequestTimer # 既存ミドルウェアの前後に挿入 config.middleware.insert_before Rack::Runtime, RequestTimer config.middleware.insert_after ActionDispatch::Executor, RequestTimer endどのタイミング・どのクラスの前後に挿入するべきかについて、Rack の仕様と Rails のミドルウェア順序を踏まえた説明が加えられているはずです。
2-6. 「高度な使い方: コントローラ内でのRack API利用」セクション追加
Railsコントローラ内から低レベルのRack APIを利用する「高度な使い方」を説明するセクションが追加。
主に以下のような内容がカバーされていると考えられます:
request.envへの直接アクセス(生の Rack env ハッシュ)Rack::ResponseやRack::Requestを使ったレスポンス構築renderを使わずに Rack レスポンス三要素[status, headers, body]を返すパターンなど
例イメージ:
rubyclass RawRackController < ApplicationController def index # Rack レベルの情報に直接触る rack_env = request.env status = 200 headers = { "Content-Type" => "text/plain" } body = ["Hello from Rack layer"] # Rails でも Rack 互換レスポンス三要素をそのまま返せる [status, headers, body] end endただしこのような使い方は「普通のRailsアプリでは推奨されない、特殊用途(プロキシ、ストリーミング、低レイテンシAPIなど)」向けである旨の注意も含まれている可能性が高いです。
2-7. リソースセクション削除とリンクの本文組み込み
- ガイド末尾の「Resources(参考リンク集)」のようなセクションを削除し、代わりに本文中の適切な場所で関連ドキュメントへ直接リンクするスタイルに変更。
- これにより、読み進めながら自然に関連リソースに移動しやすくなっています。
- 影響範囲・注意点
- コード変更はなく、ドキュメントのみの更新です。
→ 既存アプリの挙動には影響ありません。 - ただし、以下の点は開発者が情報源として参照する際に注意が必要です:
- デフォルトミドルウェアの情報が刷新されているため、古いガイドやブログ記事を参照している場合は、この新しいガイドを優先した方が正確です。
- Sprockets ベースの説明から Propshaft への移行が反映されているため、Sprockets を使い続けているアプリでは一部のミドルウェア名・挙動説明が自分の環境と異なる可能性があります。
- 「高度な使い方(Rack APIをコントローラで直叩き)」は、フレームワークの抽象レイヤをバイパスするため、保守性やテスト容易性に影響しうる点に留意が必要です。
- 参考情報 (あれば)
- 該当ガイド(更新後):
- Rails Guides – Rails on Rack(
guides/source/rails_on_rack.md) - 関連ガイド:
- Initialization Guide(
rails server起動プロセス)
https://guides.rubyonrails.org/initialization.html#rails-server-start - Configuration Guide(ミドルウェア設定との対応が取られた)
https://guides.rubyonrails.org/configuring.html - Instrumentation Guide(構成スタイルを合わせた)
https://guides.rubyonrails.org/active_support_instrumentation.html
- Initialization Guide(
- 各ミドルウェアのAPIドキュメント(例):
ActionDispatch::Static
https://api.rubyonrails.org/classes/ActionDispatch/Static.htmlPropshaft::QuietAsset
https://www.rubydoc.info/gems/propshaft/Propshaft/QuietAsset(※例、実URLは環境に依存)
#45705 Add explicit regression tests for ActiveRecord::Migration's #say & #say_with_time
マージ日: 2026/5/18 | 作成者: @nfedyashev
- 概要 (1-2文で)
ActiveRecord::Migration の#sayと#say_with_timeに対して、明示的なリグレッションテストが追加されています。加えて、say_with_timeをブロックなしで呼び出した場合に明示的なエラーになる挙動をテストで保証するようになっています。
- 変更内容の詳細
2-1. どこに何が追加されたか
- 変更ファイル:
activerecord/test/cases/migration_test.rb - 追加行数: 48行(テストコードのみ)
この PR は実装コードではなくテストコードのみを変更しており、Migration の出力周りの挙動をより厳密に検証するためのテストが追加されています。
2-2. Migration#say のテスト
元々 activerecord/test/cases/migrator_test.rb には「何回 puts が呼ばれたか」程度のテストはありましたが、この PR では以下のような点をより直接的に確認するテストが追加されていると考えられます:
say "message"を呼んだときに、期待どおりの文字列がログ(あるいは出力バッファ)に記録される- インデント(
subitem: trueオプションなど)がある場合、適切に字下げされたメッセージが出力される verboseが無効な場合は出力されない(もしくは、出力が抑制される)こと
イメージ的には以下のようなテストが追加されているはずです(擬似コード):
def test_say_outputs_message
migration = Class.new(ActiveRecord::Migration[7.1]) do
def change
say "Hello"
end
end.new
output = capture(:stdout) { migration.migrate(:up) }
assert_includes output, "== Hello\n"
end実際には ActiveRecord::Migration::CommandRecorder やテスト用ヘルパに合わせた書き方になっているはずですが、趣旨としては「say がどう表示されるかを直接アサートする」テストです。
2-3. Migration#say_with_time のテスト
#say_with_time は以下のようなインターフェイスを持つメソッドです:
say_with_time "Doing something" do
# 測定対象の処理
end- ブロック実行前に
"-- Doing something"のようなメッセージを出力 - ブロックの実行にかかった時間を計測し、
(0.1234s)のような形で表示 - ブロックの戻り値を必要に応じて
"-> 123 rows"のように後置表示
PR 説明の「after this update it fails with an explicit error in case #say_with_time was called without a block」から推測されるテスト内容:
say_with_timeをブロックなしで呼び出した場合に、ArgumentErrorもしくは独自メッセージつきの例外が発生することを確認するテスト- 正常系として、ブロック付きで呼んだときに
- メッセージ部分が期待どおりに表示される
- 実行時間の表示フォーマットが期待どおりである
- ブロックの戻り値に応じて後続情報(例: "rows")が付与される
といった点を検証していると考えられます。
擬似コード例:
def test_say_with_time_requires_block
migration = Class.new(ActiveRecord::Migration[7.1]) do
def change
say_with_time "Doing something"
end
end.new
error = assert_raises(ArgumentError) do
migration.migrate(:up)
end
assert_match "must be called with a block", error.message
end実際にどの例外クラス・メッセージになっているかは実装依存ですが、「ブロックなし呼び出しを明示的に禁止する」という仕様をテストで固定しています。
- 影響範囲・注意点
影響範囲(実装)
この PR 自体はテストファイルのみ変更しており、Rails 本体のロジックは変更されていません。したがって、現時点ではランタイム挙動に直接の変更はありません。仕様固定による将来影響
ただし、テストが「say_with_timeをブロックなしで呼ぶと明示的エラーになる」ことを前提としているため、将来的に対応する実装変更(例:raise ArgumentError, "..." unless block_given?)が導入されることが前提に近い状態になっています。
その結果として、- もし既存アプリケーションで
say_with_time "msg"をブロックなしで呼んでいるコードがあれば、将来の Rails バージョンアップ時に例外で落ちる可能性があります。 say_with_timeは「ブロックつきで使うもの」という前提で書かれており、今後はそれが明示的に強制されると考えておくとよいです。
- もし既存アプリケーションで
回帰テストとしての役割
say/say_with_time周りのフォーマット・動作(メッセージ、インデント、時間計測、エラー条件など)が、今後のリファクタリングや内部実装変更で壊れないようにする「安全網」として機能します。
ログフォーマットやマイグレーション出力に依存したツール・スクリプトなどがある場合、仕様のズレをより早く検知できるようになります。
- 参考情報 (あれば)
- 元々の類似テスト:
activerecord/test/cases/migrator_test.rb
ここでは主にputsの呼び出し回数など簡易的な検証のみ行われていた。 - 関連 API:
ActiveRecord::Migration#say(message, subitem = false)ActiveRecord::Migration#say_with_time(message) { ... }
いずれもマイグレーション実行時のコンソール出力用ヘルパで、開発者が進捗表示や計測ログを簡単に出すために使うメソッド。
- 実装側で今後入りそうな変更(推測):
def say_with_time(message)の先頭でraise ArgumentError, "say_with_time must be called with a block" unless block_given?のようなチェックが追加されることが予想される。
#57370 Prevent false leaked-connection reports during reaper maintenance
マージ日: 2026/5/18 | 作成者: @curi
- 概要 (1-2文で)
ActiveRecord のテスト用リーク検知 (ActiveRecord::TestCase#check_connection_leaks) が、コネクションプールの Reaper 実行中に誤って「リーク」と判定してしまう問題を修正する PRです。プールのreaper_lockと同期してリークチェックを行うことで、Reaper が正当な理由で保持しているコネクションを誤検知しないようにしています。
- 変更内容の詳細
問題の背景
- テストの
teardown時にActiveRecord::TestCase#check_connection_leaksが、各ConnectionPoolをスキャンして「解放されずに残っている(スレッドがオーナーのままの)コネクション」がないかを検査しています。 - 一方、
ConnectionPool::Reaperはバックグラウンドスレッド(典型的にはスレッド名AR Pool Reaper)として定期的にプールのメンテナンスを行い、その間reaper_lockを取りつつコネクションを保持することがあります。 - これまでの実装では、
check_connection_leaksがpool.connectionsを読む際にpool.reaper_lockを取得しておらず、- Reaper がちょうどメンテナンス中にコネクションを握っている
- そのタイミングで
teardownが走ってリークチェックする
というレースが発生すると、
「ownerがAR Pool Reaperのコネクション = テストがリークさせた」と誤判定し、リークとして報告するとともに Reaper スレッドを kill してしまう、という問題が起きていました。
主な修正点
1) リークチェック処理を reaper_lock と同期
activerecord/test/cases/test_case.rb 内の check_connection_leaks 相当の処理が修正され、
pool.connectionsの走査- 「どのスレッドがコネクションを握っているか」の検査
を行う直前に、pool.reaper_lockを取得してから処理するようになりました。
これにより:
- Reaper がメンテナンス中にコネクションを保持している場合、
teardown側はreaper_lockの取得で待たされるので、Reaper がメンテを終えてロックを解放するまでリークチェックが実行されません。 - 結果として、Reaper による「正当な一時的占有」をリークとしてカウントしなくなります。
コードイメージ(擬似コード):
def check_connection_leaks
ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
pool.with_reaper_lock do
pool.connections.each do |conn|
next unless conn.owner
# ここで owner (スレッド名など) を見てリーク判定
# Reaper が maintenance 中に持っているだけのケースは
# ロック同期によりここに入ってこなくなる
end
end
end
end実際の修正は +11/-8 行程度で、既存のリークチェックロジックに reaper_lock を組み合わせる形での最小限の変更になっています。
2) 回帰テストの追加
activerecord/test/cases/test_case_test.rb に新たなテストが追加されています(+69 行)。
このテストは以下を再現するためのものです:
- スレッド名が
"AR Pool Reaper"のスレッドを立ち上げる。 - そのスレッドが
ConnectionPool::Reaperと同様にコネクションを保持してメンテナンス中の状態を模倣する。 - その状態で
ActiveRecord::TestCaseのリークチェックを実行する。 - 旧実装(
origin/main)では、- この「メンテ中に保持しているコネクション」がリークと誤判定され、テストが失敗することを確認。
- 新実装では、
reaper_lockによる同期のおかげで、このケースではリーク扱いされず、テストが通る。
これにより、今回の PR が解決しようとしている「Reaper がオーナーのコネクションを誤検知する」問題の再発を防ぐことができます。
- 影響範囲・注意点
影響対象:
- 主に ActiveRecord を使った Rails のテストコード(
ActiveRecord::TestCaseを継承しているテストクラス)。 - 特に DB コネクションの Reaper(
ConnectionPool::Reaper)が有効な環境で、CI などでまれに「リーク接続あり」と報告されていたケース。
- 主に ActiveRecord を使った Rails のテストコード(
期待される改善:
- CI などでときどき出ていた「AR Pool Reaper がコネクションをリークしている」というような偽陽性の failure が解消されます。
- テストが Reaper スレッドを誤って kill することがなくなり、テスト後の状態がより安定します。
パフォーマンス/デッドロック懸念:
reaper_lockの取得は、リークチェック(テストの teardown 時)のみで、かつ短時間の読み取り処理のため、通常のアプリ実行時パフォーマンスへの影響は限定的です。reaper_lockはもともと Reaper のメンテナンス処理で利用されているロックであり、今回の変更はそれに対する正しい同期を追加しただけなので、適切に設計されていればデッドロックリスクも増えていません。
アプリコード側での変更の必要性:
- アプリケーション側のコードや設定を変更する必要はありません。
- Rails の ActiveRecord テスト基盤の挙動が安全側に改善されるだけです。
- 参考情報 (あれば)
- 関連 PR: #57367 の CI 実行中に、今回のリーク誤検知問題が発覚したと説明されています。
- 実行されたテスト:
cd activerecord && ARCONN=postgresql bundle exec ruby -Itest test/cases/test_case_test.rb -vcd activerecord && ARCONN=postgresql bundle exec ruby -Itest test/cases/reaper_test.rbcd activerecord && ARCONN=postgresql bundle exec ruby -Itest test/cases/associations/has_and_belongs_to_many_associations_test.rbbundle exec rubocop activerecord/test/cases/test_case.rb activerecord/test/cases/test_case_test.rb
→ ActiveRecord テスト基盤および Reaper 挙動まわりに限定した変更で、既存テストに対しても後方互換性が確認されています。
#57380 Fix pre-serialized String values skipping cast in insert_all hot path
マージ日: 2026/5/18 | 作成者: @tahsin352
- 概要 (1-2文で)
insert_allの高速化パスで「シリアライズ済みの文字列」を受け取った場合に、値が二重エンコードされてしまう不具合を修正し、createと同じキャスト挙動になるようにした PR です。Hash/Array などの通常ケースでのパフォーマンス改善は維持しつつ、String の場合だけcastをスキップしないようにしています。
- 変更内容の詳細
背景
- 以前の PR #56855 で、
InsertAll::Builder#values_listに「シリアライズされたカラムではtype.cast(value)をスキップする」高速パスが導入されました。 - 目的は、
serializeなカラムに対して Hash や Array を渡したときに行われる以下のような「無駄なラウンドトリップ」を避けることでした:
# Mutable#cast は実質的に:
cast(value) == deserialize(serialize(value))
# serialize: coder.dump(value)
# deserialize: coder.load(...)- ところが「value がすでにエンコード済みの String」の場合、この最適化により
castが完全にスキップされ、結果として DB に二重エンコードされたゴミが入るケースが発生していました。
問題の具体例
insert_all の(問題のあった)経路:
# 以前の insert_all 内部(シリアライズされたカラム + 高速パス)
SerializeCastValue.serialize(type, "already_encoded")
# => type.serialize("already_encoded")
# => coder.dump("already_encoded") # ここでさらに dump がかかる
# => "encoded:encoded:..." のような二重エンコードcreate の経路:
type.cast("already_encoded")
# => coder.load(coder.dump("already_encoded")) # 一度 round-trip して正規化
# その後 create 側の serialize 処理
# => coder.dump(normalised_value) # 結果として正しい一回分のエンコードつまり:
createは「入力が String の場合も一度castする」ため、結果は常に「一回だけエンコードされた値」になる。insert_allは「シリアライズカラムでは無条件にcastをスキップ」していたため、すでにエンコード済みの String にさらにdumpをかけてしまい、二重エンコードが起きていた、という不整合がありました。
修正内容
修正は最小限で、以下のポリシーになりました:
シリアライズされたカラムに対して
insert_allをする場合、
- 値が String 以外(Hash, Array, Custom Object など)なら
castをスキップしてよい- 値が String の場合は
castをスキップせず、createと同じ処理を通す
コード上では insert_all の hot path に
unless value.is_a?(String)
# cast をスキップして SerializeCastValue.serialize に直行
endに相当する条件が追加され、String のときだけ type.cast(value) を経由するようになっています。
これにより:
- Hash/Array/custom object:
- もともと
coder.dump/coder.loadのラウンドトリップは「同じ Ruby オブジェクトを返すだけ」で冗長なので、引き続きスキップしてパフォーマンスを維持。
- もともと
- String:
- 「これはすでに DB 表現なのか? ただの Ruby 文字列なのか?」という曖昧さがあるため、一度
castを通してcreateと同じ規則で正規化してからserializeする。
- 「これはすでに DB 表現なのか? ただの Ruby 文字列なのか?」という曖昧さがあるため、一度
テスト
activerecord/test/cases/insert_all_test.rb に4つのテストが追加されています。
ポイント:
topics.content(textカラム +serialize)を対象にテスト。ReverseCoderという「非冪等な coder」を使っている:dump("foo") # => "encoded:foo"- もう一度
dumpすると"encoded:encoded:foo"となるため、二重エンコードが起きるとすぐに検出できる。
- これにより:
createとinsert_allの結果が一致すること- 特に String を渡したときに二重エンコードにならないこと
をアサートしています。
- 影響範囲・注意点
- 対象:
ActiveRecord::Persistence#insert_all系を使っていて、serialize(あるいはType::Serialized)されたカラムを持ち、- そのカラムに「String を直接渡している」ケース。
- 行動差分:
- 以前: その String がすでにエンコード済みでも、そのまま
serializeされて二重エンコードになる可能性があった。 - 以後:
createと同様に必ずcast経由になるため、一回ぶんの正規化+エンコードに収束する。
- 以前: その String がすでにエンコード済みでも、そのまま
- パフォーマンス:
- String については、これまでスキップしていた
castを呼ぶようになるため、わずかに遅くなりますが、- そもそも「すでにエンコード済みの String を直接渡す」というのは一般的なユースケースではなく、
- 非 String(Hash/Array など)については引き続き最適化が効く
ため、全体としてはほぼ PR #56855 の性能メリットを維持します。
- String については、これまでスキップしていた
- 後方互換性:
- 「二重エンコードされた値をあえて DB に保存していた」ような極めて特殊なケースがない限り、期待される挙動は「バグ修正」として自然に改善されます。
createとinsert_allの間で動作が揃うようになったため、API 間での一貫性は向上しています。
- 参考情報 (あれば)
- 対象 PR:
- 本 PR: https://github.com/rails/rails/pull/57380
- 高速化を導入した前提 PR: https://github.com/rails/rails/pull/56855
- 関連クラス:
ActiveRecord::InsertAll::Builder#values_listActiveRecord::Type::SerializedActiveRecord::AttributeMethods::Serialization/SerializeCastValueActiveRecord::Type::Mutable(Mutable#cast=deserialize(serialize(value)))
#57382 Remove support for Rails 6.1 Active Record Marshal format
マージ日: 2026/5/18 | 作成者: @byroot
- 概要 (1-2文で)
Rails 7.2 以降で、Rails 6.1 時代の Active Record オブジェクトの Marshal フォーマット(シリアライズ形式)を**「正式にはサポート対象外」とする**変更です。これにより Active Record 内部表現を将来的に大きく変更しやすくします。
- 変更内容の詳細
2-1. 何が「サポート終了」になったのか
- 対象: 「Rails 6.1 の Active Record オブジェクトを
Marshal.dump / Marshal.loadしたときのバイナリ形式」を安定仕様として維持することがやめられました。 - 実装的には、Rails の設定オプションやガイド上の「6.1 形式をサポートします」という記述が削除・緩和されています。
ポイント:
- いまはまだ「たまたま動いている」状態
PR 説明にもあるとおり、現時点では「Rails 6.1 で生成された Marshal データをロードすること自体」はまだテストも残されており、動作します。 - ただし、Active Record の内部 API (
Attribute/AttributeSetなど) を将来変更した際に互換性が壊れてもバグではない扱いになります。
2-2. コードレベルの変更概要
変更ファイル:
activerecord/lib/active_record/marshalling.rb- Marshal 対応周りのサポート表現を微調整。
- 6.1 フォーマットを安定的にサポートする、という前提を捨てる方向の変更(+2/-3)。
guides/source/configuring.md- アプリケーション設定ガイドから、6.1 Marshal フォーマットサポートに関する記述が削除・簡略化(+2/-7)。
- 例えば、
config.active_record.use_legacy_marshalling的な説明や、6.1 互換モードが永続的にあるかのような記述があれば、それが削られている可能性が高いです。
guides/source/upgrading_ruby_on_rails.md- Rails バージョンアップガイドに、**「6.1 Marshal フォーマットが今後壊れる可能性がある」**という趣旨の説明・注意喚起が追加(+7)。
- どういうケースで影響を受けるか、移行にどう備えるかといった情報が含まれていると考えられます。
railties/lib/rails/application/configuration.rb- アプリケーション設定から 6.1 形式サポートに関わるオプションか関連コードが 1 行削除されている(+0/-1)。
- 例: 6.1 互換モードを明示的に有効にするような設定の公式サポートがなくなった、など。
2-3. deprecation warning が出ない理由
PR 説明より:
Unfortunately I don't see any realistic solution for emitting deprecation warnings if the format is still in use by an application.
- Marshal のバイナリフォーマットが使われているかどうかを、Rails 側から安全に・確実に検知する手段がほぼないため、
- 「まだ 6.1 フォーマットを使っていますよ」という deprecation warning をアプリケーション実行中に出すことが現実的でないと判断されています。
- そのため、明示的な非推奨メッセージやログなしで、あるタイミングで突然壊れる可能性がある、という性質の変更になります。
- 影響範囲・注意点
3-1. 影響を受ける可能性が高いケース
Rails アプリケーションで、以下のような用途で Active Record オブジェクトをそのまま Marshal.dump / Marshal.load している場合です。
- キャッシュ層に Marshal を使っているケース
- 例:
Rails.cache.write("user:#{id}", user_record)(かつバックエンドが Redis, Memcached など) - これ自体は Rails が内部でやることもありますが、6.1 時代に生成した Marshal バイナリを長く保持していると、将来ロード時に壊れる可能性があります。
- 例:
- ジョブキューやバックグラウンドジョブの引数に Marshal を利用
- 独自実装のキューや一部のライブラリで AR オブジェクトを丸ごとシリアライズしているケース。
- 独自の永続化層に Marshal を使用
- ファイル、Key-Value ストア、その他ストレージに AR オブジェクトを Marshal で保存している場合。
3-2. いま何が起きるか / これから何が起こりうるか
現在(この PR マージ直後):
- Rails 6.1 で生成された Marshal バイナリを、最新版 Rails の Active Record で
Marshal.loadしても、まだロードは成功するように保たれています。 - ただし「6.1 互換フォーマットを意図的に維持する」という約束がなくなったため、
- 将来の Rails アップデートで内部表現が変わると、
- 6.1 時代のデータが突然ロードできなくなる(
TypeError/ArgumentError/NoMethodErrorなど)可能性があります。
3-3. 開発者が取るべき対策・推奨パターン
AR オブジェクトを Marshal で長期保存しない
- 長寿命のキャッシュや永続ストレージには、AR オブジェクト丸ごとではなく、必要フィールドのみ or ID のみを保存するのが安全です。
- 例:ruby
# 悪手(将来壊れやすい) Rails.cache.write("user:#{id}", user_record) # 推奨: ID だけキャッシュして、必要時に取り直す Rails.cache.write("user_id_for_something:#{id}", user_record.id) # または属性ハッシュに落として保存(自前で互換性管理できるなら) Rails.cache.write("user_snapshot:#{id}", user_record.attributes)
どうしてもシリアライズが必要なら JSON / MessagePack 等に寄せる
as_json/to_json、あるいはActiveModel::Serializers::JSONなどを利用して、アプリ側でスキーマ互換性を管理しやすいフォーマットにしておくほうが将来のアップグレードで安全です。
既存の Marshal データの「ガーベジ期間」を設ける
- 6.1 時代から残っている可能性のある古いキャッシュ・キュー・ストレージを、
- TTL を短くする
- ロールアウト前に一度クリアする
- などで「古いフォーマットが環境内に残り続けない」ようにしておくと、将来の互換性問題を避けやすくなります。
- 6.1 時代から残っている可能性のある古いキャッシュ・キュー・ストレージを、
独自シリアライズ層をラップする
- どうしてもバイナリシリアライズが必要な場合、
- バージョン番号付きで自前のラッパー形式(
{version: 1, payload: ...})を用意する - Active Record オブジェクトを、そのラッパーが理解できるプレーンデータ構造に変換する
- バージョン番号付きで自前のラッパー形式(
- など、Rails の内部表現に依存しないシリアライズ方式に移行するのが望ましいです。
- どうしてもバイナリシリアライズが必要な場合、
- 参考情報 (あれば)
この PR が修正として言及している PR:
https://github.com/rails/rails/pull/53362
(Active Record の Marshal 関連の挙動・後方互換性に関する問題が背景にあると考えられます)対応ファイルの役割:
activerecord/lib/active_record/marshalling.rb
Active Record オブジェクトの Marshal 対応(_dump/_load周り)のコア実装。guides/source/configuring.md
Rails アプリの設定方法ガイド。6.1 互換モードに関する説明の削除・簡略化。guides/source/upgrading_ruby_on_rails.md
Rails のバージョンアップ時に確認すべき項目(今回の Marshal フォーマット非サポート化の注意が追加)。railties/lib/rails/application/configuration.rbconfig.*で設定できる項目の定義。6.1 Marshal 関連の設定が削除されている可能性。
この PR の実質的な意味は「6.1 Marshal フォーマットを、今後の Rails バージョンアップにおける互換性保証の対象から外した」という宣言なので、AR オブジェクトの Marshal 利用をしているかどうかを改めて棚卸ししておくのが安全です。
#56565 Revert "Sort schema cache columns and indexes per table when dumping"
マージ日: 2026/5/18 | 作成者: @byroot
- 概要 (1-2文で)
schema_cache.yml生成時にテーブルごとにカラム・インデックスをソートしていた変更 (#54960) を元に戻し、スキーマキャッシュ内では定義順を維持するようにした PR です。
理由は、スキーマキャッシュのソートがpluck("*")などカラム順に依存する挙動を変えてしまい、実際にバグを引き起こし得るためです。
- 変更内容の詳細
元に戻したもの(revert の対象)
- 以前の PR #54960 では、以下を行っていました:
schema.rbの出力時に、テーブル内のカラムやインデックスをソートschema_cache.yml(スキーマキャッシュ)にも同様のソートロジックを適用
今回の PR はこのうち「スキーマキャッシュ側のソート」をやめています(schema.rb 側はこの PR では触っていない)。
コードレベルでの変更点(概念的な説明)
対象ファイル:
activerecord/lib/active_record/connection_adapters/schema_cache.rbactiverecord/test/cases/connection_adapters/schema_cache_test.rb
差分は少ないですが、意味合いとしては:
以前(revert 前)のイメージ
# 疑似コードイメージ
def columns(table_name)
@columns[table_name].sort_by!(&:name) if @columns[table_name]
end
def indexes(table_name)
@indexes[table_name].sort_by!(&:name) if @indexes[table_name]
end今回の PR 後(revert 後)のイメージ
# ソートは行わず、DB / schema 定義順をそのまま保持
def columns(table_name)
@columns[table_name]
end
def indexes(table_name)
@indexes[table_name]
endテスト側 (schema_cache_test.rb) では、
- カラムやインデックスが「ソートされていること」を前提としていたテストを削除・修正し、
- 「定義順が維持されること」を前提とした挙動に戻しています。
- 影響範囲・注意点
影響するもの
schema_cache.ymlの内容の順序- カラム・インデックスの順番が、以前の(Rails 7 などの)挙動と同じく「定義順」に戻ります。
- すでに schema cache をコミットしている場合、再生成すると diff に「順序の変更」が出る可能性があります(ただし、実行時挙動を安定させるための変更)。
カラム順に依存するコード
- 代表例として PR 説明に挙がっているのが
pluck("*")です。 pluck("*")は「DB から返ってくるカラム順」を前提としているケースがあり、スキーマキャッシュ内でカラムをソートしてしまうと、- 期待する順序と異なる順で値が返る
- 位置ベースで配列を解釈しているコードが壊れる
といった不具合が発生し得ます。
- 今回の revert により、そのような順序依存コードは「以前の Rails の挙動」に戻るため、潜在バグを回避する方向です。
- 代表例として PR 説明に挙がっているのが
なぜ schema.rb と扱いを分けるのか
schema.rb:- Git コンフリクト削減のために「ソートしたい」というニーズがある。
- かつ、順序を保持したい場合の「エスケープハッチ(設定・オプション)」が用意されている。
schema_cache.yml:- 主にランタイム最適化用の内部キャッシュであり、Git コンフリクトの問題は相対的に小さい。
- 一方、ここでのソートは挙動そのもの(カラム順依存のコード)を変えてしまう。
- かつ、順序を保持するためのエスケープハッチがない。
→ そのため「スキーマキャッシュをソートする変更」はリスクが高く、今回 revert された。
注意点
- Rails のアップデート時に:
schema_cache.ymlを再生成するタイミングで、ファイル上の順序が変わる可能性があります。- もし、独自に schema cache を解析するツールやメタプログラミングをしている場合、順序に依存していないかを確認する価値があります(依存しているなら、今回の変更はむしろ安定方向)。
- 参考情報 (あれば)
- 元のソート導入 PR:
- rails/rails#54960 — 「Sort schema cache columns and indexes per table when dumping」
- 本 PR:
- rails/rails#56565 — 「Revert "Sort schema cache columns and indexes per table when dumping"」
- スキーマキャッシュ関連のドキュメント(英語):
- Rails Guides: Active Record Migrations -> Schema dumping and loading
- ActiveRecord::ConnectionAdapters::SchemaCache クラスの RDoc / API ドキュメント
#57267 Fix bulk job and email enqueueing methods with no arguments
マージ日: 2026/5/17 | 作成者: @fatkodima
- 概要 (1-2文で)
Action Mailer と Active Job の「引数なし」での一括エンキュー (*_all,*_laterなど) が正しく動作しない問題 (#57264) を、内部ロジックの配置を整理することで修正した PR です。実質的な仕様変更や挙動追加はなく、「コードの場所を変えてバグを解消した」リファクタリング寄りの修正です。
- 変更内容の詳細
何が問題だったか (Issue #57264 の概要)
- Active Job / Action Mailer には
- 複数ジョブをまとめてキューに積む「bulk enqueue」
- メール送信を一括で
deliver_laterする
ための API があり、引数なしで呼び出した場合の挙動に不具合があった。
- 例として、ジョブ/メールの enqueuing をラップするメソッドが「可変長引数」や「ブロック引数」を前提にしており、引数なし時のパスが正しくカバーされていない/再利用しづらい場所にあった、という類の問題が Issue 側で議論されている。
この PR では、そのロジックをより適切な場所に「移動」することで、引数なしのケースも自然に面倒を見られるようにしている、という形です。
実際の変更点
1) Active Job 側 (activejob/lib/active_job.rb, activejob/lib/active_job/enqueuing.rb)
ActiveJob::Enqueuingモジュールにあった一部の enqueuing ロジックがActiveJob本体 (active_job.rb) に移動。具体的には、以下のような「クラスメソッド系の enqueue ヘルパ」が
ActiveJob側に集約されたと考えられます(擬似コードイメージ):ruby# 以前: active_job/enqueuing.rb にあった module ActiveJob::Enqueuing module ClassMethods def perform_all_later(jobs) # ... end end end # 以後: active_job.rb 側に移動 class ActiveJob::Base class << self def perform_all_later(jobs) # 同じ実装をこちらに移動 end end endこれにより:
ActiveJob::Baseクラスメソッドとしての一括エンキュー API の見通しが良くなる- 引数なしや単一ジョブなどのバリエーションへの対応が「クラス本体側」で完結し、モジュール経由の呼び出しとの齟齬が減る
enqueuing.rb の該当コードは削除されている(-30行)ので、API 自体はそのままに、定義位置だけが変わった形です。
2) Action Mailer 側 (actionmailer/lib/action_mailer.rb, actionmailer/lib/action_mailer/message_delivery.rb)
これまで
ActionMailer::MessageDelivery(個々のメールオブジェクト)側にあった一部の「enqueue 関連コード」が、ActionMailer本体 (action_mailer.rb) に移動。典型的には
deliver_later,deliver_later!, あるいは「メールを Active Job に乗せる」ための共通処理と、その bulk 版をまとめている箇所です。擬似コードイメージ:
ruby# 以前: message_delivery.rb class ActionMailer::MessageDelivery def enqueue_delivery # ... end end # 以後: action_mailer.rb module ActionMailer class Base def self.enqueue_delivery(message) # 同じ処理をこちらに移動 end end endメールを一括で
deliver_laterするようなヘルパも、ActionMailer 側に寄せられたことで、引数なし(あるいはデフォルト値のみ)での呼び出しを含めた全パターンが正しく処理されるようになっています。
- 影響範囲・注意点
影響範囲
- 対象:
- Active Job の一括エンキュー API(
perform_all_laterなど、クラスメソッド経由の enqueue helper) - Action Mailer の一括
deliver_later系 API(内部で Active Job に乗せる部分)
- Active Job の一括エンキュー API(
- これらを引数なし/引数オプション少なめで呼び出していた場合、以前はエラーや不正な挙動になっていたケースが、正常に動作するようになります。
下位互換性
- PR 説明の通り「コードの移動のみ」で、メソッドシグネチャや公開 API は変えていないと明言されています。
- ただし以下のようなケースでは影響の可能性があります:
- Rails 内部の
ActiveJob::Enqueuing/ActionMailer::MessageDeliveryの private メソッドや内部構造に依存していたメタプログラミング をしている - これらのクラス/モジュールを reopen して、内部メソッドを monkey patch していた
- Rails 内部の
このような「内部実装に強く依存した拡張」をしている場合は、Rails のアップデート後に一度動作確認を行うことを推奨します。
対応すべきこと
- 通常のアプリケーションコードであれば、特別な対応は不要です。
- 以下に心当たりがあれば、アップデート後にテストを重点的に確認:
- 独自 wrapper で Active Job の bulk enqueue を叩いている
- Mailer まわりで
deliver_laterをラップして独自ログやメトリクスを仕込んでいる - job/mailer の enqueue execution chain を monkey patch している
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57267
- Issue (問題の詳細な議論): https://github.com/rails/rails/issues/57264
Issue 側に、「どのメソッドがどのような引数で失敗したか」「どのようなユースケースで顕在化したか」が記載されているので、自分のコードに影響がありそうか判断したい場合は Issue をあわせて読むと把握しやすいです。
#54587 Expose all BatchEnumerator attributes
マージ日: 2026/5/17 | 作成者: @fatkodima
- 概要 (1-2文で)
ActiveRecord::Batches::BatchEnumeratorが内部的に持っていたcursor,order,use_rangesを、外部から読み取れる公開属性として追加し、バッチ処理の挙動に応じた高度なカスタマイズができるようになりました。これにより、バッチイテレーションの状態や戦略を外部のライブラリ/アプリケーションから参照可能になります。
- 変更内容の詳細
2-1. 何が変わったか
BatchEnumerator に以下の3つの属性リーダーが追加されています。
cursororderuse_ranges
もともと一部の属性は attr_reader で公開されていましたが、今回のPRで「すべての属性を公開したい」というニーズに応え、残りの内部属性も外から参照できるように統一されています。
対象クラスは以下です:
# activerecord/lib/active_record/relation/batches/batch_enumerator.rb
class ActiveRecord::Batches::BatchEnumerator
# ここに attr_reader :cursor, :order, :use_ranges が追加されたイメージ
end加えて、activerecord/test/cases/batches_test.rb にテストが追加され、BatchEnumerator のインスタンスからこれらの属性が参照できることが検証されています。
2-2. どう使えるようになるか (サンプルコード)
例えば、in_batches を使った処理の中で、Enumerator 自体にアクセスし、その設定を元に処理を分岐させることができます。
# 例: in_batches から BatchEnumerator を取得
enumerator = User.in_batches(of: 1000, order: :id, use_ranges: true)
# 新しく公開された属性にアクセス
puts enumerator.cursor # => 現在のカーソル情報(id など)
puts enumerator.order # => :id など、バッチ処理に使われている order
puts enumerator.use_ranges # => true / false(範囲ベースのバッチ処理かどうか)
enumerator.each_record do |user|
# enumerator の状態によって処理を変えたい場合
if enumerator.use_ranges
# 範囲ベースのバッチに対するロジック
else
# オフセット/IDベースなど別のロジック
end
endあるいは、バッチ処理をラップする gem や社内ライブラリ側で、BatchEnumerator を受け取って、その設定内容をログに出したり、再実行用のメタデータとして保存したりといった利用も想定されています。
- 影響範囲・注意点
後方互換性
- 既存の挙動は変わっておらず、「新しい public な読み取り用属性が増えた」だけなので、通常のアプリケーションコードに対する破壊的変更はありません。
- これまで内部実装に直接依存していた(インスタンス変数に直接触れる、
sendで無理やり読みに行く等)コードがあった場合は、これら新しい属性リーダーを使うことで、より安全で将来の変更に強い実装に置き換えられます。
APIの安定化傾向
- 元々内部実装寄りだった情報を公式に公開した形になるため、
cursor,order,use_rangesは今後も ActiveRecord のパブリック API としてある程度の安定性が期待できます。 - これらに依存したライブラリ/gem(PR説明にあるように、作者の gem など)も、より安心して
BatchEnumeratorを利用できるようになります。
- 元々内部実装寄りだった情報を公式に公開した形になるため、
利用時の前提
cursorの具体的な中身や意味は、利用しているバッチ戦略(ID カーソル、レンジなど)に依存します。実体のクラスや構造に過度に依存せず、「カーソルとして使える値」程度の抽象度で扱うのが安全です。use_rangesが true の場合、find_each/in_batchesが ID の範囲やカスタムレンジを使った処理になっていることを前提にしたロジックが書けますが、実装詳細には依存しすぎないように注意してください。
- 参考情報 (あれば)
- このPRは #42312 のフォローアップとして行われており、そこで
BatchEnumeratorの一部属性がすでに公開されていた流れがあります。 - 対象PR: https://github.com/rails/rails/pull/54587
BatchEnumeratorの基本的な使い方はActiveRecord::Batches(find_each,in_batches) のドキュメント・ガイドも合わせて確認すると理解しやすいです。
#45201 Adds ability to get the combined bytes_size of all attached blobs
マージ日: 2026/5/17 | 作成者: @sandip-mane
- 概要 (1-2文で)
Active Storage のhas_many_attachedに対して、紐づいている複数ファイルの総サイズ(バイト数)を一括で取得できるbyte_sizeメソッドが追加されました。これにより、Gallery.new.photos.byte_sizeのようにして、複数添付ファイルの合計容量を簡単に取得できます。
- 変更内容の詳細
追加された機能
ActiveStorage::Attached::Many に byte_size メソッドが追加され、複数添付(has_many_attached)に対して以下のような呼び出しが可能になります。
class Gallery < ApplicationRecord
has_many_attached :photos
end
gallery = Gallery.find(1)
gallery.photos.byte_size # => photos に紐づく全 Blob の byte_size 合計 (Integer)単一添付(has_one_attached)に対しては以前から record.attachment.byte_size が使えていましたが、今回の変更で「複数添付にも同じインターフェースで合計サイズを取得できる」ようになった形です。
実装イメージ
PR 本文からは詳細なコードは示されていませんが、ActiveStorage::Attached::Many に、内部で attachments あるいは blobs を辿って合計を取るような実装が追加されています。概ね下記のような処理が行われていると考えられます。
def byte_size
attachments.sum { |attachment| attachment.blob.byte_size }
endテスト (activestorage/test/models/attached/many_test.rb) によって、以下が確認されているはずです。
- 複数ファイルを添付した場合、各 Blob の
byte_sizeの合計が返ること - 添付が無い場合に期待される挙動(0 を返すか、nil を返すか)が明確になっていること
(Active Storage の一貫性からすると 0 を返す実装である可能性が高い)
また、activestorage/CHANGELOG.md にもこの機能追加が記載され、次バージョンの変更点として明示されています。
- 影響範囲・注意点
- 対象:
- Rails の Active Storage を利用しており、
has_many_attachedを使っているすべてのアプリケーションで利用可能な新 API です。
- Rails の Active Storage を利用しており、
- 互換性:
- 既存の API を壊す変更ではなく、メソッド追加のみの後方互換性のある変更です。
- パフォーマンス面の注意:
byte_sizeは内部で関連する Blob 情報を参照するため、- 添付ファイル数が非常に多い
- N+1 クエリが発生する状況で呼び出す といった場合には、クエリ数やメモリアクセスが増える可能性があります。
- 大量データで頻繁に呼ぶ場合は、
includes(:blob)などで事前ロードしておく、キャッシュする、といった対策を検討してください。
- 空の場合の挙動:
- 添付が1つもないときの返り値が仕様としてどうなるか(0 か nil か)を、実際のコード/ドキュメントで確認したうえで利用すると安心です。
通常は「サイズ合計」という意味合いから 0 が自然ですが、アプリのロジック上、nil チェックが必要なケースもあり得ます。
- 添付が1つもないときの返り値が仕様としてどうなるか(0 か nil か)を、実際のコード/ドキュメントで確認したうえで利用すると安心です。
- 参考情報 (あれば)
- Rails (Active Storage) の CHANGELOG:
activestorage/CHANGELOG.mdに今回の変更が追記されており、利用可能になる Rails バージョンが確認できます。
- 類似 API との関係:
- 単一添付:
user.avatar.byte_size - 複数添付:
gallery.photos.byte_size(今回新たに追加)
- 単一添付:
- 合計サイズからの派生利用例:
- MB/GB 単位に変換して UI 表示する:ruby
size_in_mb = gallery.photos.byte_size.to_f / (1024 * 1024) - 上限チェックに使う:ruby
if gallery.photos.byte_size > 100.megabytes errors.add(:photos, "の合計サイズが 100MB を超えています") end
- MB/GB 単位に変換して UI 表示する:
#44746 Fix ActiveRecord::QueryMethods#in_order_of when passing an out-of-range Integer.
マージ日: 2026/5/17 | 作成者: @tejanium
- 概要 (1–2文で)
ActiveRecord::QueryMethods#in_order_ofに、整数型カラムに対して範囲外の整数を渡したときにActiveModel::RangeErrorが発生する不具合があり、それを修正する PR です。修正後は、範囲外など「シリアライズ不可能な値」は無視され、エラーを出さずにORDER BY句が構築されます。
- 変更内容の詳細
問題となっていた挙動
in_order_of は、指定したフィールドに対して与えられた値の配列順にレコードを並べるためのクエリメソッドです:
Post.in_order_of(:id, [3, 1, 2])
# => id が 3, 1, 2 の順になるように ORDER BY を組み立てるしかし、整数カラム (integer, bigint など) に対して、カラム型の範囲を超える大きな整数を渡すと、ActiveModel::Type::Integer によるキャストで ActiveModel::RangeError が発生していました。
Post.in_order_of(:id, [1, 9999999999999])
# ActiveModel::RangeError: 9999999999999 is out of range for ActiveModel::Type::Integer with limit 4 bytes["1", "9999999999999"] のように文字列で渡しても、内部でキャストされるため同様にエラーになります。
この挙動は、以前の PR (#43322) によって in_order_of が渡された全ての値を一律に type_cast_for_database でキャストするようになったことが原因です。
その結果、DB 上で表現できない値(int4 に対する 9999999999999 など)も無理にキャストしようとして例外が飛ぶようになっていました。
修正方針
この PR では、次のような変更が行われています。
- これまで:
type_cast_for_databaseを使って、in_order_ofに渡された全ての値を DB 向けにキャストしていた- → 範囲外の整数などもキャスト対象になり、
ActiveModel::RangeErrorが発生
- これから:
type_cast_for_databaseの代わりに、caster.serialize(value) if caster.serializable?(value)という条件付きシリアライズを使用serializable?(value)がfalseを返す値(型・範囲が不適切な値)は「無効な値」とみなし、シリアライズせずに無視する
具体的には、in_order_of が内部で Arel の CASE 式相当の ORDER BY を構築する際、
「この値は現在のカラム型に対してシリアライズ可能か?」を serializable? でチェックし、
シリアライズ不可能なら CASE 式からその値を除外するようにしています。
このアプローチは、Arel 内の他の実装と揃えるためのものでもあります:
Arel::Nodes::HomogeneousIn実装
https://github.com/rails/rails/blob/61d6eb1/activerecord/lib/arel/nodes/homogeneous_in.rb#L47-L56Arel::Visitors::ToSqlの関連部分
https://github.com/rails/rails/blob/61d6eb1/activerecord/lib/arel/visitors/to_sql.rb#L579-L593
これらと同様に、「キャストできない値は ORDER 条件から外す」という方針に統一されています。
動作イメージ(修正後の挙動)
※テストコード自体は PR からは全文わかりませんが、挙動はおおよそ以下のようになります。
# id は 4byte integer (PostgreSQL で integer / MySQL で int など) とする
Post.in_order_of(:id, [1, 9999999999999])
# これまでは RangeError を raise していたが、
# 修正後は 9999999999999 が無効値として無視される。
# 実質的に [1] だけが ORDER の対象になる。
#
# ORDER BY CASE
# WHEN "posts"."id" = 1 THEN 0
# END ASC, "posts"."id" ASC -- のようなイメージ
Post.in_order_of(:id, ["1", "9999999999999"])
# "1" はキャスト可能なので含まれるが、
# "9999999999999" はキャスト不可のため無視される。- 影響範囲・注意点
- 影響範囲:
ActiveRecord::Relation#in_order_ofを整数カラムなどに対して使用しており、かつクエリ引数にカラム型の範囲外の整数や、型にマッチしない値が混ざる可能性がある場合に挙動が変わります。
- 以前との違い:
- 以前: 範囲外や不正な値が 1 つでも含まれると
ActiveModel::RangeErrorが発生してクエリ自体が失敗 - 今回: そのような値は「無効値」として ORDER 句から無視され、残りの有効値だけを元に順序づけが行われる
- 以前: 範囲外や不正な値が 1 つでも含まれると
- 注意点:
- 「不正な値を渡したらエラーで気づきたい」という用途には向かなくなります。
in_order_ofを入力検証の代わりに使うのではなく、コントローラやフォームオブジェクト側で別途バリデーションするのがよいです。 - 無効値がすべて除外された結果、
in_order_ofの指定リストが空になった場合は、実質的に通常のorder(デフォルトのスコープ内 order)だけが適用されるような挙動になります。 - 「ユーザー入力をそのまま
in_order_ofに流す」ようなケースでは、これまでエラーになっていたケースが「静かに無視される」ようになる点を理解しておく必要があります。
- 「不正な値を渡したらエラーで気づきたい」という用途には向かなくなります。
- 参考情報 (あれば)
- クローズされた関連 Issue:
https://github.com/rails/rails/issues/44745 - 本 PR:
https://github.com/rails/rails/pull/44746 in_order_ofの導入経緯・仕様:- 参考にされた Arel の実装:
Arel::Nodes::HomogeneousIn
https://github.com/rails/rails/blob/61d6eb119fc53678d8f5028bd73ae77e69b91fb6/activerecord/lib/arel/nodes/homogeneous_in.rb#L47-L56Arel::Visitors::ToSql
https://github.com/rails/rails/blob/61d6eb119fc53678d8f5028bd73ae77e69b91fb6/activerecord/lib/arel/visitors/to_sql.rb#L579-L593
#57107 [RF-Docs][ci-skip] Caching with Rails
マージ日: 2026/5/17 | 作成者: @Ridhwana
- 概要 (1-2文で)
Rails公式ガイド「Caching with Rails」の内容を、現行のRailsの挙動に合わせて大幅に書き直し・再構成したドキュメント改善PRです。既存の構成やトーンは維持しつつ、Rails.cache、フラグメントキャッシュ、Conditional GET、SQLキャッシュ、Solid Cache、ETagなどの解説がかなり具体的・実践的になりました。
- 変更内容の詳細
ガイド全体の構成・イントロの見直し
- 冒頭の導入文と「このガイドを読んだ後にわかること」を、実際の内容に即して書き直し。
- 「Types of Caching」セクションの並びを整理し、学びやすい順に再構成:
- ローレベルキャッシュ (
Rails.cache) - フラグメントキャッシュ
- Conditional GET
- SQL(Active Recordクエリ)キャッシュ
- ローレベルキャッシュ (
ローレベルキャッシュ (Rails.cache) の拡充
Rails.cache.fetch とヒット/ミス
cache.fetchの基本的な挙動(ミス時にブロック実行して保存、以後は保存済み値を返す)を丁寧に説明。- 「キャッシュヒット」「キャッシュミス」の概念が明示的に説明されるようになりました。
Rails.cache.fetch("articles/#{article.id}") do
# キャッシュミス時にだけ実行される
expensive_query_for_article(article.id)
endキャッシュクリア
- 全体をクリアする
Rails.cache.clearの説明が追加。- どのような状況で使うか、開発・本番での扱いに触れている可能性が高いです。
キャッシュキーと cache_key_with_version
- Active Recordオブジェクトの
cache_key_with_versionを用いたキー生成ロジックを詳しく説明。- レコードの
id+ 更新時刻などから一意なキーを生成し、「内容が変わったらキーが変わる」というRails流の依存関係管理の基礎を整理。
- レコードの
- これに基づき、「どのようにキー設計をすると、オブジェクトの更新に追従しつつキャッシュの整合性を保ちやすいか」を解説。
Active Recordオブジェクトをそのままキャッシュしない理由
- 「ARオブジェクトを丸ごとキャッシュするのは通常よくない」という点を明示的に解説。
- 例: 接続状態・lazy loadされた関連・環境差(プロセス間など)で壊れやすい。
- 一般にはシリアライズしやすいプレーンなデータ構造(Hash / JSONなど)をキャッシュする方が安全、という指針が強調されていると考えられます。
fetch の高度なオプション(race_condition_ttl など)
race_condition_ttl等のオプションの使い方を追記し、「スラッシング(多数のプロセスが同時にミスして高負荷になる)」を避けるためのパターンを説明。
Rails.cache.fetch("stats", expires_in: 10.minutes, race_condition_ttl: 30.seconds) do
heavy_stats_calculation
endフラグメントキャッシュ周りの改善
テンプレートツリーダイジェスト
- ビューの「ツリー」単位でのダイジェスト(テンプレート+部分テンプレート)について説明が強化。
- テンプレートの内容変更に応じて自動的にフラグメントキーが変わる仕組みを、より明示的に記述。
コレクションキャッシュ
render @articlesやrender partial: "article", collection: @articlesのようなコレクションレンダリング時のキャッシュ挙動を詳細に説明。- 各要素ごとのキャッシュキー
- コレクション全体のキー/
expires_inなどオプションがどう解釈されるか - 個々の要素の更新がコレクション表示にどう影響するか
<%= render partial: "articles/article", collection: @articles, cached: true %>などのパターンで、キー・有効期限がどう構成されるかの解説が整理されているはずです。
依存関係管理の整理(フラグメントキャッシュ配下へ移動)
- 依存関係管理に関する説明をフラグメントキャッシュの章の中へ移動し、内容も書き直し。
- 次の3種類を区別して説明:
- 暗黙の依存(テンプレートのツリーダイジェストに含まれるもの)
- 明示的な依存(
cache_key_with_versionなどで直接キーに組み込む) - 外部依存(ヘルパー経由で値が決まるなど、テンプレート外の要素に依存するもの)
- ヘルパー経由のレンダリング時に、どのようにキャッシュキーを構成すると安全か、などが明確化。
ロシアンドールキャッシュと touch: true
- 「ロシアンドールキャッシュ」(入れ子構造のフラグメント)の説明が整理され、
touch: trueの役割が明確に。- 親モデルが
has_many :comments, touch: trueのように設定されていると、子の更新時に親のupdated_atが変わり、親フラグメントのキーが変わる→キャッシュ更新される、という流れを丁寧に説明。
- 親モデルが
共有パーシャルのキャッシュ(MIMEタイプや解決ルール)
- 複数フォーマット(HTML, JSON など)で共有されるパーシャルをキャッシュする際の挙動を再説明。
- MIMEタイプごとに別キャッシュになるのか
- パーシャル解決(どのパーシャルファイルが選ばれるか)がキャッシュキーへどう反映されるか
- これにより、APIとHTMLで同じパーシャルを使う場合などの混乱を減らす意図があります。
Conditional GET(HTTPキャッシュ)セクションの拡充
stale? と fresh_when
- 両メソッドの役割の違いと使い分けがより具体的に説明されました。
stale?: 条件付きリクエストヘッダとサーバ側のETag/Last-Modifiedを比較し、「レスポンス生成が必要か?」を判定するインタフェース。fresh_when: ETag/Last-Modifiedの設定と条件付きレスポンスの判定を包括的に行う高レベルメソッド。
def show
@article = Article.find(params[:id])
if stale?(@article)
# ここに来たときのみ、テンプレートをレンダリング
render
end
enddef show
@article = Article.find(params[:id])
fresh_when(@article)
endstrict_freshness
- 現在の
strict_freshnessの挙動について説明を強化。- ETagやLast-Modifiedの扱いをより厳格にするオプションや設定がある場合、その意味が明文化されています。
http_cache_forever
http_cache_foreverが適切となるケース(ほぼ不変のアセットや静的コンテンツなど)と、その注意点を明示。
強いETagと弱いETag
- 新しく「strong vs weak ETags」のサブセクションを追加。
- 強いETag: レスポンスボディが完全に同一であることを前提に比較される。
- 弱いETag: 意味的に同じであれば良いが、バイトレベルでは変わっていてもよい、という比較。
- ブラウザ・HTTPキャッシュとの相互運用や、どちらを使うべきかの指針を示しています。
SQLキャッシュ(Active Recordクエリキャッシュ)の明確化
- SQLキャッシュが「Active Record query caching」であることを明確にし、リクエストや実行コンテキストの中で自動的に働く仕組みを説明。
- 同一リクエスト中で同じSQLが複数回実行される場合、二度目以降はキャッシュされた結果が使われる。
- どの境界(Rackミドルウェア・ジョブ・コンソール等)でクエリキャッシュが有効かをより理解しやすくなっています。
Solid Cache セクションの更新
- Railsの最新デフォルト構成と用語に合わせて説明を更新。
rails newで生成されるアプリがどうSolid Cacheを設定しているかを反映。
- データベース設定例を更新し、実際の
config/database.ymlやconfig.cache_storeの例を提示。 - 開発環境でのセットアップガイドを追記。
- トランザクションとキャッシュストア選択に関する表現をより正確にし、どのパターンでSolid Cacheを選択すべきかがわかりやすくなりました。
Advanced Caching Patterns(新セクション)
バックグラウンドジョブや非リクエストコンテキストでのキャッシュ
- Active Job / Sidekiq等のジョブ内や、Rakeタスク、コンソール、サービスオブジェクトなど「リクエスト以外のコンテキスト」でのキャッシュの使い方を整理。
- リクエスト境界に依存するクエリキャッシュやローカルキャッシュとの違い。
- そのコンテキストで
Rails.cacheをどう扱うべきか。
ローカルキャッシュと Rails.cache.with_local_cache
- ローカルキャッシュの挙動 (
ActiveSupport::Cache::Strategy::LocalCache) を説明。 Rails.cache.with_local_cache do ... endを使うことで、そのブロック中だけ同プロセス内での超高速メモリキャッシュを利用できることを解説。
Rails.cache.with_local_cache do
100.times do
Rails.cache.fetch("expensive") { expensive_operation }
end
end- 上記のようなケースで、リモートストアへの往復を抑えて性能を向上できる点などを説明していると考えられます。
その他ドキュメント修正
guides/source/documents.yamlを更新して、キャッシングガイドのタイトル・アンカーが最新構成に合うよう修正。- ガイド内部の参照・アンカー(特にETag周りの見出し/リンク)を修正して、リンク切れや古い見出し名を解消。
- 影響範囲・注意点
コード挙動の変更はなく、あくまで公式ドキュメントの更新です。
- 本PRのマージ自体がアプリケーションの動作を変えることはありません。
ただし以下の観点で「実務的な影響」はあります:
- 既に「古いガイド」に基づいてパターンを決めているチームは、
Active Recordオブジェクトの直接キャッシュやロシアンドールキャッシュの設計・ETagの扱いなどを見直すきっかけになる。 - Solid Cacheや
with_local_cache、race_condition_ttlなど、これまであまり使っていなかった高度なオプションを導入しやすくなる。 - Conditional GET / ETagの理解がアップデートされることで、APIやSPA向けのHTTPキャッシュ設計が改善できる可能性が高い。
- 既に「古いガイド」に基づいてパターンを決めているチームは、
注意点として:
- ガイドに書かれているベストプラクティスは「最新のRailsを前提」としているため、古いRailsバージョン (5系, 6.0など) では一部のオプション・デフォルト挙動が異なる可能性があります。
- 特に
strict_freshnessやhttp_cache_forever、Solid Cache周りはバージョン依存の差分を意識して読む必要があります。
- 参考情報 (あれば)
- 対象ガイド:
guides/source/caching_with_rails.md
→ 最新のRails公式ガイド(英語)を読むと、本PRで説明された内容がすべて反映されています。 - 関連トピック:
ActiveSupport::Cacheの各ストア実装(MemCacheStore, RedisCacheStore, SolidCacheStoreなど)- HTTP仕様のETag / Last-Modified / Conditional GET (
If-None-Match,If-Modified-Since) - Active Recordクエリキャッシュ (
config.active_record.cache_versioning等の設定)
#56341 [RF Docs] Active Record Query Interface
マージ日: 2026/5/17 | 作成者: @Ridhwana
- 概要 (1–2文で)
- Active Record Query Interface ガイド全体を大幅に書き直し・整理し、用語・構成・サンプルコード・相互リンクを現状の Rails に合わせてアップデートしたドキュメント改善 PRです。
- 併せて
ActiveRecord::Relation#only/#exceptまわりなど、クエリメソッドのドキュメントコメントも微調整され、複合主キーガイドとの連携も強化されています。
- 変更内容の詳細
2.1 Active Record Query Interface ガイド全体の再構成
guides/source/active_record_querying.md が約 2,200 行追加・1,400 行削除されており、実質的に「全面改訂」に近い変更です。主なポイント:
構成・見出しの整理
- 冒頭:
- 「What is the Active Record Query Interface?」直後にあった書店サンプルを別の適切なセクションへ移動。
- コード例の前にあった小ネタ的な「Tip」の位置を移動し、「モデルの一覧を示す導入文 → サンプルコード」という流れを阻害しないように整理。
- セクション名の明確化:
Conditions→ 「Where 条件 / Filtering Results with where」のような、クエリの意味が一目でわかる名称に変更。Ordering→ 「Ordering Results」的な名前へ変更し、「結果のソート」であることを明確化。Overriding Conditions→where以外の句も対象であることを踏まえ、「Overriding SQL Clauses」等、より包括的な名前に変更。
- 「Retrieving Objects from the Database」:
- これまで手薄だった「複数レコード取得」の説明を補強。
Customer.allのような基本例を追加し、その後にwhere,order,limit,groupなどの各節につながるようにガイド全体のストーリーを再構成。Dynamic Finders部分を、find_byの説明の直後に移動し、「単一レコード取得」の文脈にまとめて配置。
- 「Selecting Specific Fields」セクション:
distinctの説明がここにあるのは不自然という指摘に基づき、LIMIT/OFFSET周辺、もしくは「Limiting Results」的なセクションへ再配置して整合性を改善。
ガイド内リンク・他ガイドへのリンク強化
- SQL 知識への言及:
- Active Record Basics 同様、「SQL をある程度知っているとこのガイドが読みやすい」という旨の注意書きを追加。
- SQL 学習用の外部リソースリンクを追加。
- 他の Rails ガイドとのクロスリンク:
- 複合主キーに言及している箇所から
active_record_composite_primary_keys.mdへのリンクを追加。 transaction,lockingなど、他ガイドのトピックについては、少なくとも検索でヒットして他ガイドに辿れるよう、導入部にまとめてリンクを配置する方向に。
- 複合主キーに言及している箇所から
- セキュリティガイドへのリンク:
"conditions with strings"セクションで SQL インジェクションを警告している箇所から Security Guide の該当セクションへリンク。whereに配列引数を渡すセクションと合わせて記述を少し簡潔にしつつ、インジェクション回避のベストプラクティスを明確化。
サンプルコードの見直し
- boolean の扱い:
WHERE active = 0/1のような例をfalse/trueに統一し、DB 実装に依存しない・Rails 的に読みやすい形に修正。
- 日付・時刻の例:
created_atを日付扱いしてgroupする例が実際には「timestamp で group している」問題を是正。- DB 依存の
DATE(created_at)などを多用せずに済むよう、statusやstatus, customer_idなど、日付変換不要なカラムでのグルーピングに置き換え。
- DB 依存の
- 時間範囲 (
Hash range conditions) の例:Date.yesterday.all_dayなどのシンタックスは簡潔だが、all_dayが基本的に UTC で評価されることに起因するタイムゾーン混乱を避けるため、採用しない判断。- 代わりに
Time.now.midnight/ 実運用を意識するならTime.zone.now.midnightを使った例にして、「Time.zone を前提にしたタイムゾーン意識」のメッセージを強化。
- join の例:
has_and_belongs_to_manyでjoin_tableを書くかどうかは意見が割れたが、「現実的で包括的な例を見せたい」という意図から現状維持を選択。
- eager_load の例:
- 「2 クエリ実行されるように見えるが実際にはそうならない」不正確な例を修正するための別 PR (#51940) が前提の形になっていた箇所を調整。
lock,with_lock,references:- これらのメソッドを紹介している箇所から、それぞれの API ドキュメントへのリンクを追加。
- スコープの例:
out_of_print_and_expensiveをout_of_print.costs_more_than(500)で再定義できることを踏まえ、スコープのチェーンを理解しやすい示し方に整理。- スコープとクラスメソッドの類似性を説明する節で、「self を返すクラスメソッドはスコープとほぼ同様に振る舞う」ことを明示した例を追加。
メソッド・機能のカバレッジ拡張
only/except:- ガイド内で
onlyに触れていた部分に加え、exceptも存在することを説明。 - 「
onlyは指定した句だけを残す」「exceptは指定した句だけを取り除く」というSpawnMethodsの役割を理解しやすいように説明を補強。
- ガイド内で
reorder:- 「default_scope で指定された order を上書きする」だけでなく、「それまでにチェーンされたすべての order を上書きする」ことを明確に記述。
create_with:- Finder メソッドの一覧から
create_withを削除。create_withは検索ではなく「後続の create 用のデフォルト値を指定する」メソッドであり、分類上誤解を招くため。
- Finder メソッドの一覧から
create_or_find_by/create_or_find_by!:- 既存の
find_or_create_by/find_or_create_by!に加え、新しいcreate_or_find_by/create_or_find_by!をガイドに追加。 find_or_create_byがトランザクションを跨いだときにレースコンディションで二重作成し得る非原子的な性質を明記。- それに対し
create_or_find_byは DB の一意制約エラーを利用して、原子性を保ちつつ「作成 or 取得」を実現するテクニックであること、およびユニークインデックス前提であることを説明。
- 既存の
pluck,pick,ids,find_by_sql:- それぞれを一括して「Finding by SQL」という名前に置くのは誤解を招くため、
find_by_sqlに専用の見出しを用意。 pluck/pick/idsは「特定カラムの値を配列で取得する」系のユーティリティとして、「Retrieving Objects from the Database」内に「Retrieving Columns」的な節を新設して整理。
- それぞれを一括して「Finding by SQL」という名前に置くのは誤解を招くため、
- メソッドチェーンの注意点:
- 「Method Chaining」の節に、Rails コンソールで
inspectやto_aが自動的に呼ばれ、想定よりも早くクエリが実行されることがあるという注意書きを追加。 - Active Record パターンと Method Chaining の関係について、「Active Record パターン自体が Method Chaining を意味するわけではない」ことを明確化し、代わりに Martin Fowler の「Fluent Interface」に言及。
- 「Method Chaining」の節に、Rails コンソールで
ロッキング・トランザクション・パフォーマンス関連
- ロッキング・トランザクション:
lock/with_lockなどの基本的な説明は残しつつ、「本来はトランザクションやロッキングをまとめた新しいガイド(Active Model Transactions & Locking)で扱うべき」という方向性を明言し、この PR では移動せず据え置き。- 別タスクとして Basecamp 上に「Active Model Transactions & Locking - Documentation Updates」を登録済み。
- パフォーマンス:
EXPLAIN実行や eager loading など、パフォーマンスに強く紐づくトピックを「Active Record Performance」ガイドへ移す構想を記述。- この PR ではまだ分離せず、今後のフォローアップで切り出す方針。
2.2 Active Record Composite Primary Keys ガイドの更新
guides/source/active_record_composite_primary_keys.md にも 30 行程度の追記があります。ポイント:
- Query Interface ガイドからのリンク先としての役割を明確化。
- クエリ生成・
where条件・orderなど、複合主キーを用いる際に典型的にハマりやすい点を補足するテキストを追加(主にドキュメント相互参照のための微調整)。
2.3 クエリメソッド (query_methods.rb) のコメント調整
activerecord/lib/active_record/relation/query_methods.rb に +8/-8 の変更が入っていますが、実装ロジックではなくドキュメントレベルの変更です。
only,except,reorderなど、この PR でガイド側の説明を強化したメソッドの doc コメントを合わせて調整。- 意味合いをガイドと揃えるための文言修正(例:
reorderが「default_scope だけでなく既存の order すべてを置き換える」ことをより明確に表現)。
- 影響範囲・注意点
- 実装コードへの影響:
query_methods.rbの変更はコメントのみであり、挙動変更や API 変更はありません。アプリケーションコードが壊れる可能性は実質ゼロです。
- ドキュメントを参照している二次資料への影響:
- ガイドの見出し名やセクション構造がかなり変わっているため、特定セクションへのリンク(アンカー)を持つブログ記事・社内 Wiki・スライド等は、リンク切れや内容不一致が発生する可能性があります。
- 特に「Conditions」「Ordering」「Finding by SQL」など、名前や位置が変わった箇所は要確認です。
- ベストプラクティスの変化:
find_or_create_byの非原子的な性質と、create_or_find_by推奨の流れが明示されたため、古いコードサンプルをそのまま教科書にしている場合は見直しを検討したほうが良いです。- boolean を 0/1 ではなく
true/falseで扱うサンプルが標準になったので、ガイドに合わせるなら新規コードは論理値ベースで書くのが望ましいです。
- 時刻・タイムゾーン関連:
all_day/beginning_of_dayをあえて避けている説明があるため、「Rails の Time.zone / config.time_zone 設定と all_day の挙動」をしっかり把握していないと、安易に真似るとサンプルと違う結果を得ることがあります。- ガイドは慎重な立場を取っているので、運用では
Time.zone.nowを前提に range を構成する習慣を持つことが推奨されます。
- 参考情報 (あれば)
- この PR 本体:
https://github.com/rails/rails/pull/56341 - 関連 PR (eager_load の例修正):
https://github.com/rails/rails/pull/51940 only/exceptを提供するSpawnMethodsAPI:
https://edgeapi.rubyonrails.org/classes/ActiveRecord/SpawnMethods.htmlcreate_or_find_by/create_or_find_by!:
https://edgeapi.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-create_or_find_by- Fluent Interface に関する Martin Fowler の記事:
https://martinfowler.com/bliki/FluentInterface.html
#43736 Introduce **options argument in the DestroyAssociationAsyncJob#perform method
マージ日: 2026/5/16 | 作成者: @smt116
- 概要 (1-2文で)
ActiveRecord::DestroyAssociationAsyncJob#performの引数に**optionsが追加され、将来的にオプションを拡張しても既存アプリを壊さないようにするための後方互換性改善が行われました。実質的な挙動は現状変わらず、ごく小さなシグネチャ変更のみです。
- 変更内容の詳細
activerecord/lib/active_record/destroy_association_async_job.rb の perform メソッドの引数リストが、キーワード引数を余分に受け取れるように変更されました。
想定される変更イメージは以下のようなものです(実際のコードはほぼこれと同等です):
# 変更前(イメージ)
def perform(owner_model_name, owner_id, association_name)
# ...
end
# 変更後(イメージ)
def perform(owner_model_name, owner_id, association_name, **options)
# options は現状は使われないが、引数として受け取れる
# ...
endこれにより、将来的に以下のようにオプション付きでジョブがキューイングされても:
DestroyAssociationAsyncJob.perform_later(
"User",
1,
"posts",
some_flag: true,
timeout: 30
)既存の perform 実装を壊さずに、追加のオプションを受け取れる余地ができます。
現段階では **options は内部で利用されていない(あるいは少なくともこのPRでは使われていない)ため、動作は従来と同じです。
背景として、関連PR/コメント(#42452 のレビューなど)で「後からキーワード引数を追加すると、既存のメソッドシグネチャと衝突して互換性問題が起きる」ことが指摘されており、その対策として最初から **options を受ける形にしておく、という方針になっています。
- 影響範囲・注意点
既存コードへの影響
- 既存の
DestroyAssociationAsyncJobを直接使っているアプリは、そのままでも挙動は変わりません。 - すでに
performをオーバーライドしている場合は、Rails 側の定義とシグネチャの差異に注意が必要です。例: アプリ側で
rubydef perform(owner_model_name, owner_id, association_name) endと定義していても Ruby 的には問題なく動きますが、将来オプションを利用する形に移行したい場合は:
rubydef perform(owner_model_name, owner_id, association_name, **options) endと揃えておくと安全です。
- 既存の
将来の拡張性
- Rails 本体がこのジョブに新しいキーワード引数オプションを追加しても、
**optionsが受け止めるため、メソッド呼び出し側との互換性が保ちやすくなります。 - gem やアプリが独自にこのジョブをラップしてオプションを付加するようなユースケースにも対応しやすくなります。
- Rails 本体がこのジョブに新しいキーワード引数オプションを追加しても、
非互換リスクの軽減
- キーワード引数周りは Ruby 2.7〜3.0 で挙動が変わっており、将来的に Rails が新しいキーワード引数を追加した際に、「位置引数/キーワード引数の不一致」によるエラーを避ける狙いがあります。
- 参考情報 (あれば)
- 該当PR: https://github.com/rails/rails/pull/43736
- 関連ディスカッション・コメント:
- 関連Issue/PR: [#42430](DestroyAssociationAsyncJob や関連削除非同期化機能に関する改善)
#56842 Revert: Sort table columns by name when dumping schema
マージ日: 2026/5/15 | 作成者: @bert-mccutchen
- 概要 (1-2文で)
スキーマダンプ時にテーブルのカラムを名前順にソートする挙動を導入した変更(#53281)を、丸ごと取り消すPRです。これにより、schema.rbなどでのカラムの出力順は、従来どおり「DBが返す順序(定義順)」に戻ります。
- 変更内容の詳細
2-1. 何をリバートしたか
PR本文より:
- 「Reverted all code changes made in #53281.」
- 「Updated the ActiveRecord CHANGELOG.」
つまり、#53281 で導入された「カラムを名前でソートする」ロジックと、それに紐づくテスト変更を削除し、挙動を元に戻した上で、Active Record の CHANGELOG にその旨を追記しています。
実際の差分(要約):
activerecord/lib/active_record/schema_dumper.rb- カラム出力時に
columns.sort_by(&:name)のような処理をしていた部分を削除し、ソートしない元の実装に戻している(+1/-1 行なので、ほぼ 1 箇所の挙動差)。
- カラム出力時に
activerecord/test/cases/schema_dumper_test.rb- 「カラムが名前順に並んでいること」を前提にしたテストを削除(-8 行)。
activerecord/CHANGELOG.md- このリバートに関する変更点を記載(+6 行)。
2-2. 元の変更 (#53281) の文脈
元の PR (#53281) は、「スキーマダンプ時にテーブルカラムを名前順で並べる」ことで、「マイグレーションの順番や定義の揺れによる schema.rb の差分ノイズを減らしたい/安定化したい」という動機で導入されたものと考えられます。
しかし、本 PR (#56842) では:
Based on my findings, the original approach of sorting schema columns is not the appropriate solution to the problem the original developer was facing.
とあり、当初の問題の解決策として「ダンプ時にカラムをソートする」のは不適切である、との結論になっています。
- 影響範囲・注意点
3-1. schema.rb / structure.sql の出力順が元に戻る
- Rails 7.2 以降のどこかのタイミング(#53281 マージ以降)で一度「カラムが名前順で出力される」状態を経験している場合、この PR が入ったバージョンでは、再び「DBの定義順(追加した順など)」で出力される挙動に戻ります。
- そのため、Rails のバージョンアップ時に
db:schema:dumpなどを実行すると、カラム順の変化による大量の差分が出る可能性があります。- 例:
usersテーブルのemail,name,created_atの順にダンプされていたものが、DB 定義順に戻ることでname,email,created_atのように並びが変わる、など。
- 例:
3-2. 差分ノイズ対策をカラムソート前提で組んでいた場合
- 独自ツールや CI 等で「schema.rb の差分がないか」を見ている場合、以前「名前順ソート」を前提にしてロジックを書いていたなら、今回のリバートによって前提が崩れます。
- カラム順の安定性をどう担保するかは、アプリケーション側/運用側のポリシーに委ねられる形に戻ります:
- 例: マイグレーションの定義順を厳格に管理する
- 例: 比較時に「行の並びを無視」するような diff ツールを用いる など
3-3. テストが壊れる可能性
- アプリ側テストで、
schema.rbなどを読み取り「カラムがアルファベット順で書かれていること」を前提にしたチェックを書いていた場合、それらは失敗するようになります。 - 同様に、生成されたスキーマのテキストを丸ごと snapshot テストしている場合も、カラム順の変化で snapshot の更新が必要になる可能性があります。
- 参考情報 (あれば)
- 本 PR: https://github.com/rails/rails/pull/56842
- リバート対象になった PR: https://github.com/rails/rails/pull/53281
- リバートの判断の元となった議論・コメント:
#57374 Revert "Add default #render_in implementation to ActiveModel::Conversion"
マージ日: 2026/5/15 | 作成者: @seanpdoyle
- 概要 (1-2文で)
ActiveModel::Conversionに追加されていたデフォルト実装の#render_inを、一旦まるごと巻き戻した PR です。まだ未リリースの機能だったため、コントローラ起点のパーシャル名前空間解決に関する問題を踏まえて、仕様を見直した上で再度出し直す方針になっています。
- 変更内容の詳細
なにを Revert したか
元 PR(#57349)で導入されていた以下の変更がすべて取り消されています。
ActiveModel::Conversion#render_inのデフォルト実装の追加- それに伴うテスト (
conversion_testなど) の追加 ActiveModel::Lintによるインターフェースチェックの変更- ガイド (
active_model_basics.md) におけるrender_in関連の記述 - CHANGELOG のエントリ
コード的には、以下のようなイメージで追加されていたものが、まるごと削除された状態になります(擬似コード):
module ActiveModel
module Conversion
# これがまるごと無かったことになる
def render_in(view_context, &block)
view_context.render(partial: to_partial_path, object: self, &block)
end
end
endこれにより、ActiveModel::Conversion を include しただけのオブジェクトは、render_in を持たない元の状態に戻ります(ViewComponent などの「render_in を前提とする仕組み」に、そのまま ActiveModel を乗せる想定だった部分が撤回された形)。
Revert の理由
元 PR に対するコメントで、
render呼び出しが、レンダーコンテキストにもとづく「コントローラ単位でのパーシャル名前空間付け(暗黙のディレクトリ解決)」を考慮していない
という指摘がありました。
Rails では、コントローラから render :foo / render "foo" / render partial: "foo" などを呼んだ場合、暗黙に以下のような名前空間解決が行われます:
PostsControllerのindexからrender :post→app/views/posts/_post.html.*が優先される- ネストされたコントローラ・エンジンなどのコンテキストを含めた lookup path が使われる
今回の ActiveModel::Conversion#render_in の実装では、この「呼び出し元コントローラ・テンプレートに応じたパーシャル名前空間の解決」が適切に反映されていなかったため、一旦リリース前に取り消し、問題を織り込んだ形で再設計することになっています。
- 影響範囲・注意点
リリース済みのバージョンには影響なし
ActiveModel::Conversion#render_inはまだどのリリースにも含まれていない状態だったため、公開済みの Rails を使っているアプリケーションには直接の破壊的変更はありません。render_inを ActiveModel に期待する設計は現状できない
今回の revert により、以下のような使い方は引き続き非サポートです:rubyclass User include ActiveModel::Model include ActiveModel::Conversion # to_partial_path は従来通りだが… def to_partial_path "users/user" end end user = User.new # NG: まだ user.render_in(view_context) は定義されない user.render_in(view_context)「ActiveModel を include していれば
render_inも使える」という API にはなっていない点に注意が必要です。render_inを使いたい場合は、従来通り ViewComponent など、render_inを実装している専用のクラス/gem を利用する必要があります。ドキュメントも元に戻っている
guides/source/active_model_basics.mdからrender_inに関する記述が削除されているため、最新ガイドを参照している限り、この機能を前提とした実装例は登場しません。将来的には改めて導入される可能性が高い
PR の本文で「subsequent re-submission(後続の再投稿)」と明言されているため、- コントローラコンテキストに応じたパーシャル名前空間解決
- 既存の
to_partial_path/renderの挙動との整合性 を考慮した上で、改めてActiveModel::Conversion#render_inが提案される見込みです。
- 参考情報 (あれば)
- Reverted 元 PR:
- https://github.com/rails/rails/pull/57349
- ActiveModel オブジェクトにデフォルトの
render_in実装を追加していた PR
- ActiveModel オブジェクトにデフォルトの
- https://github.com/rails/rails/pull/57349
- 問題指摘コメント:
- https://github.com/rails/rails/pull/57349#issuecomment-4460561567
- コントローラベースのパーシャル名前空間解決が考慮されていない点に言及
- https://github.com/rails/rails/pull/57349#issuecomment-4460561567
- 現状の
ActiveModel::Conversionの役割:#to_model/#to_key/#to_param/#to_partial_pathなどを提供し、「Rails の view / routing と相互運用しやすい plain Ruby object」を定義するためのモジュールであり、render_inはまだ正式な一部ではない状態に戻りました。
#57364 Enumerate columns in SELECT when only_columns is set
マージ日: 2026/5/15 | 作成者: @sarub0b0
- 概要 (1-2文で)
only_columnsを設定したときに、SQL のSELECT句がtable.*のままになってしまう不具合を修正し、ドキュメントどおり「指定したカラムだけを取得する」挙動になるようにした PR です。ignored_columns使用時と同様に、only_columnsでも SELECT 句でカラムを列挙するようにしています。
- 変更内容の詳細
何が問題だったか
only_columns は ignored_columns の逆の機能として導入されていますが、挙動に不整合がありました。
only_columns設定時:load_schema!内でcolumns_hashは正しくフィルタされる
→model.column_namesなどでは期待どおりのカラムだけが見える- しかしクエリは
SELECT "table".* FROM ...のまま
→ DB からは全カラムが返り、結果的にレコードには全カラムが埋まる
例:
class User < ApplicationRecord
self.only_columns = %w[id name]
end
User.all.to_sql
# 期待: SELECT "users"."id", "users"."name" FROM "users"
# 実際: SELECT "users".* FROM "users"
User.first.attributes.keys
# 期待: ["id", "name"]
# 実際: ["id", "name", "email", "created_at", "updated_at"]原因は ActiveRecord::QueryMethods#build_select が
ignored_columnsがある場合enumerate_columns_in_select_statementsが有効な場合
のみ SELECT 句を明示的に列挙する分岐に入っており、only_columns はその判定に含まれていなかったためです。
実際の変更内容
activerecord/lib/active_record/relation/query_methods.rb の build_select 内の条件を 1 箇所だけ変更:
- elsif model.ignored_columns.any? || model.enumerate_columns_in_select_statements
+ elsif model.ignored_columns.any? || model.only_columns.any? || model.enumerate_columns_in_select_statements
arel.project(*model.column_names.map { |field| table[field] })ポイント:
model.column_namesはcolumns_hashを元にしており、load_schema!時点でonly_columnsによりすでにフィルタ済み。- そのため、
only_columns用に特別な処理を追加する必要はなく、「明示的に列挙するかどうか」の条件にmodel.only_columns.any?を足すだけでよい。
この変更により:
class User < ApplicationRecord
self.only_columns = %w[id name]
end
User.all.to_sql
# => SELECT "users"."id", "users"."name" FROM "users"
User.first.attributes.keys
# => ["id", "name"]という、ドキュメントどおりの挙動になります。
テスト追加
activerecord/test/cases/base_test.rb にテストを追加:
既存の
ignored_columns_not_included_in_SELECTテストのすぐそばに既存の
OnlyColumnsDeveloperフィクスチャモデルを利用テスト内容のイメージ:
- モデルに
self.only_columns = ...を設定 to_sqlを評価して SELECT 句に指定カラムだけが列挙されていることを確認
- モデルに
新テストは:
- この修正あり → パス
- この修正なし → 失敗
となることを SQLite3 / PostgreSQL / MySQL で確認済みです。
- 影響範囲・注意点
- 対象:
ActiveRecordのonly_columnsを利用しているモデル - 実際の挙動変化:
- これまで:
only_columnsを設定しても DB レベルではSELECT *が発行されていた - これから:
SELECT句でカラムが列挙され、DB からも指定カラムのみ取得される
- これまで:
主な影響:
- 期待される挙動に修正されるため、基本的には「バグ修正」です。
- ただし、これまで「
only_columnsを設定しているが、実は裏でSELECT *が効いていた」ことで成り立っていたコードがある場合は挙動が変わります:- 例:
only_columns = %w[id name]にもかかわらず、record.emailなど「含めていないはずのカラム」にアクセスしていた場合
→ 今後はそのカラムは SELECT されず、nilになるか、場合によってはattribute未定義に近い扱いになります(アプリのコード・バージョンによる)。
- 例:
- パフォーマンス面では:
- カラム数が多いテーブルで
only_columnsを使っている場合、今回の修正により本来の目的どおり「不要なカラムを取得しない」形になり、ネットワーク転送量・デシリアライズの削減につながります。
- カラム数が多いテーブルで
注意点:
only_columnsは「取得するカラムをホワイトリストする」ものであり、「それ以外のカラムにアプリケーション側から普通にアクセスできる」ことは今後保証されません。ignored_columnsと同様に、「スキーマ的には存在しているがアプリケーションからは見せたくない/使いたくない」カラムの制御に利用する想定です。
- 参考情報 (あれば)
- 対応する既存機能:
ignored_columns,enumerate_columns_in_select_statements - 関連 PR:
only_columnsの導入 PR #55121 - 今回の変更は「ドキュメントどおりの挙動に合わせる」ものであり、新しい公開 API の追加や互換性のない仕様変更ではなく、バグフィックス扱いです。
#57372 Remove caller supplied :object option in render_in ActiveModel helper
マージ日: 2026/5/15 | 作成者: @tahsin352
- 概要 (1-2文で)
ActiveModel のrender_inヘルパーで、呼び出し元が:objectオプションを渡しても無視し、常にselfを描画対象オブジェクトとして使うようにした PR です。これにより、キーワード引数の重複エラーや、意図せずselfが上書きされる挙動を防ぎます。
- 変更内容の詳細
背景となる問題
ActiveModel の Conversion モジュールには、ViewComponent などで使われる render_in(view_context, **options) 的なヘルパー(to_partial_path と組み合わせるレンダリングサポート)があり、内部で次のような呼び出しをしていると想定できます:
view_context.render(object: self, **options)ここで、呼び出し側が options の中に object: を含めてしまうと、Ruby のキーワード引数ルール上、2つの問題が起きます。
render(object: self, **{ object: other })- Ruby レベルでは
ArgumentError: duplicated keyword argument :objectが発生する
- Ruby レベルでは
- もしくは、内部で
**optionsをそのまま受け取るようなラッパーがいると、- 呼び出し元の
object:がselfを上書きしてしまう(本来「self が優先」という設計意図と逆)
- 呼び出し元の
この PR は「render_in 系のヘルパーでは必ず self を object: として使い、呼び出し元は object: をいじれないようにする」ことで、この2つの問題をまとめて解消します。
実際の変更点
activemodel/lib/active_model/conversion.rb に 1 行だけ変更が入り、render_in 相当のメソッドで options から :object キーを取り除く処理が追加されています(options = options.except(:object) のようなイメージ)。
擬似コードで表すと、以前は:
def render_in(view_context, **options)
view_context.render(object: self, **options)
endだったものが、今回の PR により次のような動きになります:
def render_in(view_context, **options)
# 呼び出し元が渡した :object は必ず削除
options = options.except(:object)
# 常に self が object として使われる
view_context.render(object: self, **options)
endこれにより、次のような呼び出しは:
person_component.render_in(view_context, object: other_person, locals: { foo: :bar })内部的には:
view_context.render(object: person_component, locals: { foo: :bar })として扱われ、object: other_person は完全に無視されます。
テストの追加
activemodel/test/cases/conversion_test.rb に 18 行のテストが追加され、以下のような点が検証されています:
render_inにobject:を渡しても、レンダリング対象は常にselfであること- 呼び出し元が
object:を含めても例外にならず、かつselfが上書きされないこと - その他のオプション(
locals,partial,layoutなど)は従来通り素通しされること
- 影響範囲・注意点
影響対象:
- ActiveModel の
render_in(およびそれに依存する機能)を使っているコード - 特に、
render_in呼び出し時にobject:をオプションとして渡していたコード
- ActiveModel の
互換性:
- これまで「たまたま」
render_in(..., object: something_else)が通っていて、selfではなく別オブジェクトをobject:として使えていたケースは、今回の PR により必ずselfが使われるようになります。 - 本来の設計意図(「
render_inは自分自身をレンダリングする」)に沿った締め付けなので、仕様としては妥当ですが、そうした“裏技”的な利用は壊れます。
- これまで「たまたま」
注意点:
render_in経由でレンダリング対象を差し替えたいユースケースがある場合は、renderを直接呼ぶ、あるいは別のヘルパー/APIを用意すべきです。- もし既存コードで
render_in(..., object: ...)としている箇所があれば、それはもはや意味を持たず無視されることになるので、リファクタリングの対象になります。
- 参考情報 (あれば)
フォローアップ元の PR:
https://github.com/rails/rails/pull/57349
(render_in周りの挙動・インターフェイスを整理している一連の変更の一部)Ruby のキーワード引数と
**optionsの仕様:foo(bar: 1, **{ bar: 2 })のような呼び方はArgumentError: duplicated keyword argument :barになる- これを避けるためには、
**options展開前にoptions.delete(:bar)などで重複を取り除く必要がある
#48628 PostgreSQL reload virtual columns on update via RETURNING clause
マージ日: 2026/5/14 | 作成者: @abaldwin88
- 概要 (1-2文で)
PostgreSQL を使う場合、Active Record が UPDATE クエリに RETURNING 句を付けて実行し、DB 側で計算・更新される「仮想カラム(virtual columns, generated columns)」を自動的に再ロードするようになりました。これにより、更新直後のモデル上で仮想カラムの値を正しく参照するためにreloadを呼ぶ必要がなくなります。
- 変更内容の詳細
2-1. 何ができるようになったか
PostgreSQL で「stored な virtual column(generated column)」を使っている場合:
create_table :posts do |t|
t.integer :upvotes_count
t.integer :downvotes_count
t.virtual :total_votes_count, type: :integer,
as: "upvotes_count + downvotes_count",
stored: true
end今後は、以下のようなコードで:
post = Post.find(1)
post.update(upvotes_count: 2, downvotes_count: 2)
# ここで `post.reload` は不要
post.total_votes_count # => 4 になるupdate 成功直後の post オブジェクトに、DB が計算した total_votes_count が反映されるようになります。
2-2. 実装の概要
主なポイントは「UPDATE + RETURNING」で更新後の値を一度に取得して、その結果でレコードの属性を更新するようにしたことです。
a. 抽象アダプタ側の拡張
ActiveRecord::ConnectionAdapters::DatabaseStatements/AbstractAdapterに「UPDATE 結果を返すためのフック」が追加されています。- PostgreSQL アダプタはこのフックを実装し、UPDATE 文に
RETURNINGを付けた SQL を組み立て・実行し、返ってきた行を ActiveRecord に返すようになっています。 - MySQL / SQLite アダプタ側にもメソッドシグネチャの変更など最小限の対応が入っていますが、RETURNING による自動リロードは「未サポート」のままです。
b. カラム情報に「virtual かどうか」を持たせる
ActiveRecord::ConnectionAdapters::Columnに「virtual カラム」であるかどうかを示すフラグが追加されています。- スキーマ情報(
model_schema.rbなど)から、t.virtualで定義されたカラムがわかるようになり、PostgreSQL アダプタでもそれを認識できるようになっています。
c. UPDATE 時に返してもらうカラムの選択
ActiveRecord::Persistenceの更新ロジックが拡張され、「どのカラムを RETURNING で返してもらうか」を決定する処理が入っています。- 基本的には「仮想カラム(stored virtual / generated columns)」を RETURNING 対象に含めます。
- それ以外にも、必要に応じてデフォルト値のあるカラムや、DB 側で書き換わる可能性のあるカラムを RETURNING させる準備が入っています(関連 PR を踏まえた一連の流れ)。
UPDATE 実行後、返ってきた行の値で該当属性を上書きするため、Ruby 側のオブジェクトが DB 上の最新の状態になります。
d. Arel 側の対応
arel/visitors/postgresql.rbに変更が入り、PostgreSQL 用の Arel Visitor が RETURNING 句付き UPDATE に対応しました。- 対応に合わせて
activerecord/test/cases/arel/visitors/postgres_test.rbが追加され、UPDATE + RETURNING の SQL 生成がテストされています。
e. テスト・その他
- 仮想カラム (
Column#virtual?的な仕様) に対するテストが追加。 dirty_test.rb/persistence_test.rbに、「更新後に virtual column の値が正しく反映されるか」「余計な reload をしなくても良いか」などのテストが追加。- SQLite 用スキーマ定義(
sqlite_specific_schema.rb)などが、virtual カラムまわりの振る舞いに合わせて微調整されています。 activerecord/CHANGELOG.mdに、この新挙動が追記されています。
影響範囲・注意点
対象は PostgreSQL のみ
- MySQL / SQLite アダプタには現時点でこの挙動は実装されていません。
- マルチ DB で、「PostgreSQL のときだけ update 後に virtual column が自動更新される」という差異が生じます。
対象は virtual / generated columns(特に stored)
- Rails の
t.virtual ... stored: trueで定義されているカラムが主対象です。 - DB 側で値が計算される(トリガーや DEFAULT 式など)カラムについても、今後/周辺 PR で RETURNING を利用する設計が進んでいるため、仮想カラム以外でも挙動が変わる可能性がありますが、本 PR は主に virtual columns 向けです。
- Rails の
reloadが不要になるが、既存コードはそのままでも安全- これまで
post.update(...); post.reloadと書いていたコードは、そのままでも動作しますが、単純にDB往復が増えるだけになります。 - パフォーマンスを気にする場合は、PostgreSQL + virtual column 使用箇所では
reloadを削れるか検討する余地があります。
- これまで
SQL の形が変わる可能性
- PostgreSQL での UPDATE 文に RETURNING 句が付くようになります。
- カスタムログ集計や、SQL パターンに依存した処理(プロキシ、監査、FWのシグネチャなど)を書いている場合は、SQL 形状の変化に注意してください。
dirty tracking との整合性が改善
- DB 側で書き換わる virtual column について、更新直後にモデルオブジェクトが最新状態を持つようになるため、
changes,saved_changesなどの結果がより直感的になります。 - とはいえ、virtual column 自体は「保存対象の属性」というより「結果属性」のため、どこまで dirty tracking に期待するかは仕様理解が必要です。
- DB 側で書き換わる virtual column について、更新直後にモデルオブジェクトが最新状態を持つようになるため、
- 参考情報 (あれば)
- この PR は以下の PR / Issue と関連しています:
- Follow-up: PR #48241
- Related: PR #48434
- Closes: Issue #48423(PostgreSQL 使用時)
- 機能としては「PostgreSQL の UPDATE ... RETURNING による楽観的な再読み込み」を ActiveRecord レベルで一般化していく流れの一部です。
- PostgreSQL の
GENERATED ALWAYS AS (...) STOREDカラムや、Rails のt.virtual ... stored: trueを多用しているアプリケーションでは、更新後の属性不整合・reload 忘れによるバグを減らしつつ、DB 往復を減らせる変更になります。
#56855 fix: skip coder round-trip for Hash/Array values during insert_all
マージ日: 2026/5/14 | 作成者: @elasticspoon
- 概要 (1-2文で)
insert_all/insert_all!がシリアライズ属性(YAML/JSON など)を扱う際に、不要な cast(YAML.dump → YAML.load の往復)をスキップするホットパスを入れて、性能劣化を解消した PR です。通常の属性代入など他の挙動に影響を与えず、insert_allのみを高速化します。
- 変更内容の詳細
2-1. 問題の背景
対象: シリアライズ属性(
serialize,typed_store, JSON/YAML カラムなど)を持つテーブルに対するinsert_all/insert_all!回帰(regression)の発生元: PR #48139
- そこでは、
insert/insert_allが通常のcreateと同じ DB 表現になるように、型の正規化(cast)を追加した。 - 例:ruby
# YAML シリアライズを想定 Book.create!(name: ["Array"]) # => DB には "---\n- Array\n" が保存される Book.insert!({ name: ["Array"] }) # => 以前は '["Array"]' など、別フォーマットで保存されていた - この不整合を直すため、
insert!でもcastを通すようになった。そのinsert!が内部でinsert_all!を使うため、insert_allにも cast が入るようになった。
- そこでは、
問題: シリアライズ型は
ActiveModel::Type::Helpers::Mutableを include しており、cast(value)が次のように定義されている:rubydef cast(value) deserialize(serialize(value)) end- つまり、毎回
serialize(例:YAML.dump)→deserialize(例:YAML.load)を行う。 insert_allに渡す値はすでにHash/Arrayであることが多く、本来であれば DB へのシリアライズだけで済むところを、余計な往復変換を挟んでいた。- その結果、YAML/JSON カラムを含む大規模
insert_allが、raw SQL に比べて約 1.8 倍遅くなっていた。
- つまり、毎回
2-2. 検討された別案(却下されたアプローチ)
Serialized型のcastをオーバーライドし、値がすでにHash/Arrayの場合はショートサーキットしてdeserialize(serialize())をスキップする案を試した。- しかしこれは「属性代入」全体に影響し、次のような問題が起きる:ruby
# JSON coder を想定 Book.content = { a: :b } # cast をサボると、メモリ上では { a: :b } のままになる Book.save! # DB 保存時には {"a" => "b"} になる- Rails のシリアライズ属性は「メモリ上の値も正規化(キーを String 化など)」されることを前提としており、ここを変えると
dirty trackingやアプリロジックに影響するため不適切。
- Rails のシリアライズ属性は「メモリ上の値も正規化(キーを String 化など)」されることを前提としており、ここを変えると
2-3. 採用された解決策
グローバルな
castの挙動はいじらず、insert_allの内部だけでホットパスを追加する。insert_allは「DB に直接書き込むだけ」で、メモリ上の属性オブジェクト状態は気にしなくてよい場面。
そのため、既に「シリアライズ済み(serialized?)」と判断できる型については、deserialize(serialize())を省略してよい。実装イメージ(挙動レベルの説明):
- これまでの流れ(問題のあるパス):
- 型情報を元に
type = types[key]を取得 value = type.cast(value)を呼ぶSerializeCastValue.serialize(type, value)で DB 向けに最終シリアライズ
- 型情報を元に
- 新しい流れ(ホットパス適用時):
type.serialized?をチェックserialized? == trueの場合はtype.castを呼ばず、直接SerializeCastValue.serialize(type, value)に渡すserialized? == falseの通常型は従来どおりcastを通す
- これまでの流れ(問題のあるパス):
要点:
- 「値が
Hash/Arrayだからスキップする」のではなく「型がシリアライズ型かどうか (serialized?) で分岐」しているため、ロジックがinsert_allのみ・シリアライズ属性のみの最小範囲に閉じる。 insert_allでは「メモリ上のオブジェクトの正規化」は不要で、「DB に正しく保存できること」が唯一の関心事なので、deserialize(serialize())による正規化処理はカットしても安全。
- 「値が
2-4. パフォーマンス
ベンチマーク(10000 行・37 カラム・YAML シリアライズ 1 カラム)の結果:
insert_all! (with cast):約 1.0 i/sinsert_all! (array / hash hot path):約 1.82 i/sraw_sql:約 2.07 i/s
array / hash hot path(今回の変更のイメージに近いパス)は:
- cast を入れる以前 (
no cast) とほぼ同じ速度 with castに比べて約 2 倍高速- raw SQL に対して約 1.13〜1.14 倍遅い程度にまで近づく
- 影響範囲・注意点
- 影響範囲:
ActiveRecord::InsertAll(insert_all/insert_all!/upsert_all等)でシリアライズ属性を含むレコードを挿入するケースが対象。- 通常の
create/save/update/ 属性代入 (model.attr = ...) のシリアライズ挙動は変更されない。
- 挙動面:
- 以前の PR #48139 による修正(
insertとcreateのシリアライズ結果の整合性を取る)は維持されている。 - 今回は「すでにシリアライズ型として認識できる値について
insert_all内部で cast の往復を省略する」だけなので、DB に保存される最終的な値のフォーマットは従来と同じになる想定。
- 以前の PR #48139 による修正(
- 注意点:
- 自前で独自のシリアライザや型を実装している場合で、
serialized?の実装に依存している・もしくは上書きしているようなケースでは、理論上このホットパスに乗るかどうかが変わる可能性がある。 - ただし、この PR 自体は非常に局所的な変更(
insert_all内の型分岐)であり、互換性への影響は小さいと考えられる。
- 自前で独自のシリアライザや型を実装している場合で、
- テスト:
activerecord/test/cases/serialized_attribute_test.rbにテスト追加済み。- シリアライズ属性を含む
insert_allの結果が想定どおりになること、および回帰修正 (#48139) で期待された動作が維持されていることを検証している。
- 参考情報 (あれば)
- 関連 Issue: #56163
- 回帰の原因となった PR: #48139(
insert系でのシリアライズ結果の正規化) - この PR が主に対処しているポイント:
- シリアライズ属性での
Mutable#castのコスト(deserialize(serialize()))を、insert_allに限ってショートカットする。 - シリアライズ属性を多用した大規模バルクインサートのパフォーマンスを、raw SQL に比較的近い水準まで戻す。
- シリアライズ属性での
#57363 Reset lock_version after transaction rollback
マージ日: 2026/5/14 | 作成者: @55728
- 概要 (1-2文で)
このPRは、楽観的ロック (lock_version) を使ったレコードをトランザクション内で更新した後にロールバックした際、メモリ上のlock_versionとDB上の値が不整合になり、次回の保存でActiveRecord::StaleObjectErrorが発生するバグを修正します。ロールバック時にトランザクション前のlock_versionを正しく復元するように、トランザクション状態の復元処理を調整しています。
- 変更内容の詳細
問題の挙動
widget = Widget.create!(name: "a") # lock_version: 0
Widget.transaction do
widget.update!(name: "b") # lock_version: 0 -> 1 (DBも1になる)
raise ActiveRecord::Rollback # トランザクション全体がロールバック
end
widget.lock_version # => 1 (メモリ上は1, DBは再び0に戻っている)
widget.update!(name: "c") # => ActiveRecord::StaleObjectError- DB側はロールバックにより
lock_version: 0に戻る - しかしRubyオブジェクト
widgetのlock_versionは 1 のまま - その状態で再度
update!すると、WHERE lock_version = 1で更新を試みるためヒットせずStaleObjectErrorになる
従来は「ロールバック後に record.reload してね」という運用回避策のみで、失敗が次の保存時まで顕在化しないためデバッグしづらく、また「トランザクション内で更新 → ロールバック → リトライ」というパターンが一般的であることから、ロールバック時点でオブジェクト状態をDBに揃えるべきと判断されています。
根本原因
トランザクション開始時にとる「状態スナップショット」の持ち方と、lock_version のインクリメントの仕方の相性が悪かったことにあります。
remember_transaction_record_stateが記録するスナップショットは@attributesを参照渡ししている:ruby@_start_transaction_state ||= { ... attributes: @attributes, # deep_dup ではなく参照 ... }楽観的ロックの更新 (
Locking::Optimistic#_update_row) ではself[locking_column] += 1と、同じAttributeSetをインプレースに更新している保存が成功した後、
Dirty#forget_attribute_assignmentsによってレコード本体の@attributesは新しいAttributeSetに置き換えられるが、スナップショット側は古い(かつすでに書き換わった)AttributeSetを保持し続ける- 結果として、スナップショット内では
lock_version属性が「value = N+1, original = N」という “dirty 状態” のまま残る
- 結果として、スナップショット内では
ロールバック時の
restore_transaction_record_stateでは、スナップショットと現在値が同じなら復元処理をスキップするロジックがある:ruby# 擬似コードイメージ snapshot_attr = snapshot[attribute_name] current_attr = @attributes[attribute_name] # value が同じなら何もしない next if snapshot_attr.value == current_attr.valueしかし
lock_versionの場合はsnapshot_attr.value= N+1current_attr.value= N+1
となっていて「同じ」なので復元をスキップしてしまう
その結果、ロールバック後もレコードの
lock_version属性はvalue: N+1original_value: N
のまま残り、「dirty なlock_version」として扱われる。その状態で次の保存を行うとWHERE lock_version = N+1を使ってしまい、DB側のNと不整合になってStaleObjectErrorになる。
修正内容
このPRでは、restore_transaction_record_state 内でロックカラムだけを特別扱いして、スナップショットに記録されている original_value から属性を再構築 するようにしています。
- 対象: 楽観的ロックに使用されるカラム (
locking_column、通常はlock_version) - ロールバック時の処理:
- スナップショット中の
lock_versionのoriginal_value(保存前の値、N) を取り出す with_value_from_databaseを使ってAttribute(value=N, original=N)という「DBと同期が取れたクリーンな属性」を生成- それを現在の
@attributesにセットし直す
- スナップショット中の
これにより:
- メモリ上の
lock_versionがDBと同じ値 (N) に戻る - dirty tracking 上も
lock_versionは変更されていないと判断される - 次の
update/saveのWHERE句では N を使うため、StaleObjectErrorは発生しない
テスト
- 新しいテストクラス
OptimisticLockingRollbackTestを追加self.use_transactional_tests = falseに設定
→ Railsのテストフレームワークが外側をトランザクションでラップしてしまうと、内部でrequires_new: trueなセーブポイントが使われ、「外側のトランザクションレベル=1でのロールバック」にならず、今回の修正箇所 (force_restore_state: trueブランチ) が実行されないため、それを避けるための設定
- 追加テストは、main ブランチ (修正前) だとロールバック後の
lock_versionが 1 のままで失敗し、このPRのパッチ適用後は 0 に戻ってテストが通ることが確認されている
- 影響範囲・注意点
影響範囲
対象:
- Active Record の楽観的ロック (
lock_versionカラム) を利用しているアプリケーション - かつ、最外層のトランザクション がロールバックされるパターンで、ロールバック後も同じレコードインスタンスを使って再度更新するケース
- Active Record の楽観的ロック (
期待できる改善:
ロールバック後に
record.reloadを明示的に呼ばなくても、メモリ上のlock_versionがDBと同期した状態に戻るよくあるパターン:
rubyWidget.transaction do widget.update!(...) raise ActiveRecord::Rollback end # 以前はここで reload が必要だった widget.update!(...) # ← これがそのまま正常動作するようになる
まだ解決していないケース
PR説明にもある通り、セーブポイントレベルでのロールバック(requires_new: true など)には今回の修正は効きません。
例:
Widget.transaction do
Widget.transaction(requires_new: true) do
widget.update!(...)
raise ActiveRecord::Rollback # ← セーブポイントのロールバック
end
# ここで widget を使って再び update すると、
# lock_version の不整合が起こる可能性は依然としてある
end- 現状、深いレベルのセーブポイントロールバックでは
restore_transaction_record_state自体をスキップしているため、今回のロジックも実行されません。 - これを完全に解決するには「各レベルごとのスナップショット管理」や「すべてのレベルで復元処理を走らせる」など、より大きな変更が必要であり、このPRのスコープ外とされています。
互換性・副作用の可能性
- 通常のユースケースでは、「ロールバック後も
lock_versionがロールバック前の値のまま残っている」こと自体がバグなので、このPRはバグ修正として安全側に働きます。 - もしアプリケーションコード側が、この不整合な挙動に依存したワークアラウンド(例: ロールバック後の
lock_versionの値をそのまま参照して何かする)をしていた場合は挙動が変わる可能性がありますが、そうしたコードは一般的ではないと思われます。
- 参考情報 (あれば)
変更ファイル:
activerecord/lib/active_record/transactions.rbrestore_transaction_record_stateに楽観的ロック列の特別扱いロジックを追加
activerecord/test/cases/locking_test.rbOptimisticLockingRollbackTest追加
activerecord/CHANGELOG.md- バグ修正としてエントリ追加
PR内で言及されている追加リソース:
/tmp/lock_rollback_deep.rb
自己完結なSQLite用再現スクリプト(このPRの前後でStaleObjectErrorの有無が変わることを検証済み)- 関連テストスイート:
locking_test.rbtransactions_test.rbtransaction_callbacks_test.rbdirty_test.rbautosave_association_test.rbnested_attributes_test.rb
#53812 Action Text: Dispatch Active Storage events with id and file
マージ日: 2026/5/14 | 作成者: @seanpdoyle
- 概要 (1-2文で)
Action Text が発火する Active Storage 直アップロード用の JavaScript イベントについて、event.detailにidとfileを含め、さらに 2 種類のイベントを追加しつつ、ドキュメントとテストを実装に揃えた変更です。あわせて、イベントターゲットが<input>ではなく<trix-editor>であることや、attachmentプロパティの存在もガイドに明記しています。
- 変更内容の詳細
2-1. 直アップロードイベントの event.detail に id と file を追加
Action Text が添付ファイルの direct upload を行う際に発火する Active Storage 由来のイベントについて、Active Storage 本体の DirectUploadController と同じスタイルで event.detail を構築するように変更されています。
従来はドキュメント上は id, file があるとされていたものの、Action Text から dispatch されるイベントでは実装が追従しておらず不整合がありました。この PR で実装がドキュメントに揃えられています。
イベント例(Active Storage の direct upload と同等になる想定):
document.querySelector("trix-editor").addEventListener("direct-upload:initialize", (event) => {
const { id, file, attachment } = event.detail
// id: アップロードごとの一意な ID (例: "direct-upload-123")
// file: File オブジェクト
// attachment: Trix.Attachment インスタンス
})Action Text 側の attachment_upload.js で、Active Storage 側 (DirectUploadController) と同じ形で detail を組み立て、各フェーズで同じキー (id, file, xhr, など) を含めて dispatch するようにしています。
2-2. 2つのイベントを新たにサポート
Action Text のガイドには載っていなかったものの、Active Storage 本体では存在している 2 つのイベントが Action Text でも明示的にサポートされるようになりました。
追加されたイベントと event.detail:
direct-upload:before-blob-request- タイミング: アプリケーションに direct upload 用メタデータ(blob の作成)を問い合わせる直前
event.detailの内容:{ id, file, xhr, attachment }- 想定用途: メタデータリクエストに対してカスタムヘッダの追加や、リクエスト前の検証など
direct-upload:before-storage-request- タイミング: 実際にストレージサービス(S3 など)へ PUT/POST する直前
event.detailの内容:{ id, file, xhr, attachment }- 想定用途: ストレージリクエストに対して署名ヘッダの差し替え、進捗トラッキングの準備、キャンセル制御など
サンプルコード:
const editor = document.querySelector("trix-editor")
editor.addEventListener("direct-upload:before-blob-request", (event) => {
const { id, file, xhr } = event.detail
xhr.setRequestHeader("X-Upload-ID", id)
})
editor.addEventListener("direct-upload:before-storage-request", (event) => {
const { file, xhr } = event.detail
console.log("About to upload to storage:", file.name)
})2-3. イベントターゲットと attachment プロパティをドキュメントに反映
- これまでのドキュメントでは、direct upload イベントのターゲットが
<input>要素であるかのように書かれていましたが、実際には Action Text のAttachmentUploadクラスは<trix-editor>要素をターゲットとしてイベントを dispatch しています。 - この PR ではガイド (
guides/source/action_text_overview.md) を修正し、「イベントは<trix-editor>に対して発火する」ことを明記しています。 - さらに、
event.detail.attachmentとして Trix のAttachmentインスタンスが渡されることをドキュメントに追記しています。これにより、イベントハンドラ内で Trix の API を利用した高度な制御が可能になります。
例:
editor.addEventListener("direct-upload:initialize", (event) => {
const { attachment } = event.detail
// 例: プログレス用の属性を Trix 側に持たせる
attachment.setAttributes({ "data-upload-state": "initializing" })
})2-4. ESM / Sprockets 両方のビルドを更新
変更は次のファイルで行われており、ESM ビルド (actiontext.esm.js) と Sprockets 用ビルド (actiontext.js) の両方に反映されています。
actiontext/app/assets/javascripts/actiontext.esm.jsactiontext/app/assets/javascripts/actiontext.jsactiontext/app/javascript/actiontext/attachment_upload.js
これにより、どのビルドパスを使っていても同じイベント仕様になります。
2-5. システムテストの拡充
actiontext/test/system/rich_text_editor_test.rb に、直アップロードイベントが想定どおりの detail を持って <trix-editor> から dispatch されることを確認するシステムテストが追加されています。
- 追加テストでは、ブラウザ上で実際にファイルを添付し、イベントリスナで
event.detail.id,file,xhr,attachmentなどが正しく渡っているかを検証しています。 - 新規イベント (
before-blob-request,before-storage-request) もテストの対象になっているため、将来的なリグレッションを防ぐ意図があります。
- 影響範囲・注意点
対象バージョン
Rails 8 系の Action Text / Active Storage の挙動に合わせた変更であり、Rails 8 をターゲットにしたアプリや gem が影響を受けます。既存コードへの影響
- これまで Action Text 経由の direct upload イベントをフックしていたコードは、以下の点を確認するとよいです:
- イベントターゲットとして
<trix-editor>を前提にしているか(inputを前提にしていた場合は修正推奨) event.detailにid,file,attachmentが存在することを前提にしてカスタマイズできるようになる(破壊的ではないが、よりリッチに書き換える余地があります)
- イベントターゲットとして
- 新しい 2 つのイベント (
before-blob-request,before-storage-request) は後方互換性を壊さない追加なので、既存コードはそのまま動作しつつ、必要であればこれらのイベントを利用できます。
- これまで Action Text 経由の direct upload イベントをフックしていたコードは、以下の点を確認するとよいです:
Active Storage 本体との一貫性
Action Text が dispatch するイベント仕様が Active Storage のDirectUploadControllerと揃ったため、- 素の
<input type="file" data-direct-upload-url="...">での direct upload と <trix-editor>+ Action Text での direct upload
で、ほぼ同じイベントハンドラを共有できるようになります。
- 素の
テスト/デバッグ上の注意
- テストにおいて直アップロードイベントを検証している場合、ターゲット要素や
event.detailの期待値を新仕様に合わせておくとよいです。 - イベントを利用して XHR にヘッダ等を差し込んでいる場合、新しく利用可能になった
before-blob-request/before-storage-requestに処理を移した方が、より正確なタイミングを取れる可能性があります。
- テストにおいて直アップロードイベントを検証している場合、ターゲット要素や
- 参考情報 (あれば)
- 元 PR: #52680 – 今回はその follow-up
- Active Storage Direct Upload JS Events:
https://guides.rubyonrails.org/v8.0.0/active_storage_overview.html#direct-upload-javascript-events - Action Text Attachment Direct Upload JS Events:
https://guides.rubyonrails.org/v8.0.0/action_text_overview.html#attachment-direct-upload-javascript-events - Active Storage
DirectUploadController実装:
https://github.com/rails/rails/blob/v8.0.0/activestorage/app/javascript/activestorage/direct_upload_controller.js#L44-L48 - Trix Attachment API:
https://github.com/basecamp/trix/?tab=readme-ov-file#inserting-a-file
#47161 Add support for RETURNING to INSERT/UPDATE/DELETE statements
マージ日: 2026/5/14 | 作成者: @benedikt
- 概要 (1-2文で)
Arel のINSERT/UPDATE/DELETEにRETURNING句を付けてクエリを組み立てられるようにした PR です。PostgreSQL と SQLite で、データを変更しつつ同じクエリでその結果(変更後の行や集計値など)を取得する用途を Arel レベルでサポートします。
- 変更内容の詳細
2-1. 何ができるようになったか
Arel の各マネージャ・ステートメントクラスに returning が追加され、以下のようなクエリを組み立てられます。
Arel::InsertManager#returningArel::UpdateManager#returningArel::DeleteManager#returning- 対応するステートメントクラス(
InsertStatement,UpdateStatement,DeleteStatement)にreturningノードが追加
これにより、次のような SQL を Arel で生成可能になります。
INSERT ... RETURNING ...
UPDATE ... RETURNING ...
DELETE ... RETURNING ...2-2. 使用イメージ(サンプル)
2-2-1. DELETE + RETURNING を使った集計例(PR の例)
counter_queue = Arel::Table.new(:counter_queue)
flush = Arel::Table.new(:flush)
delete_manager = Arel::DeleteManager.new
.from(counter_queue)
.returning([counter_queue[:post_id], counter_queue[:delta]])
select_manager = Arel::SelectManager.new
.with(
Arel::Nodes::TableAlias.new(
Arel::Nodes::Grouping.new(delete_manager.ast),
flush.name,
)
)
.from(flush)
.project(flush[:post_id], flush[:delta].sum)
.group(flush[:post_id])生成される SQL:
WITH "flush" AS (
DELETE FROM
"counter_queue"
RETURNING
"counter_queue"."post_id",
"counter_queue"."delta"
)
SELECT
"flush"."post_id",
SUM("flush"."delta")
FROM
"flush"
GROUP BY
"flush"."post_id"DELETE と同時に削除対象行の post_id / delta を返し、それを CTE として集計しています。
2-2-2. INSERT + RETURNING のシンプルな例
(テストや PR から想定される使い方)
users = Arel::Table.new(:users)
manager = Arel::InsertManager.new
.into(users)
.insert([[users[:name], "Alice"]])
.returning(users[:id])
sql = manager.to_sql
# => INSERT INTO "users" ("name") VALUES ('Alice') RETURNING "users"."id"2-2-3. UPDATE + RETURNING の例
users = Arel::Table.new(:users)
manager = Arel::UpdateManager.new
.table(users)
.set([[users[:name], "Bob"]])
.where(users[:id].eq(1))
.returning([users[:id], users[:name]])
sql = manager.to_sql
# => UPDATE "users" SET "users"."name" = 'Bob'
# WHERE "users"."id" = 1
# RETURNING "users"."id", "users"."name"2-3. 実装面での主な変更点
InsertStatement,UpdateStatement,DeleteStatementreturning属性(Arel::Nodes::Returningを格納する想定)が追加され、AST ノードとしてRETURNINGを保持できるようになった。- 既存の
InsertStatement/UpdateStatementの初期化・dup まわりもreturningを考慮するよう修正。
InsertManager,UpdateManager,DeleteManagerdef returning(expr)形式のメソッドを追加。引数には Arel のカラムノード・配列などを渡す。- 呼び出すと内部ステートメント (
@ast) のreturningにノードを設定する。
Arel::Visitors::ToSqlvisit_Arel_Nodes_InsertStatement等で、returningがセットされていれば末尾にRETURNING ...を付けるロジックを追加。
Arel::Visitors::Dot- DOT 出力にも
returningノードを含めるようにテスト・実装を追加(可視化の一貫性確保)。
- DOT 出力にも
テスト (*_test.rb) もそれぞれのマネージャ・ステートメント・ビジターに対して RETURNING を含むケースが追加されています。
- 影響範囲・注意点
対象は Arel レイヤーのみ
- この PR では ActiveRecord のパブリック API(
Model.create,update_allなど)にはRETURNINGサポートは追加されていません。 - あくまで Arel を直接使う(または ActiveRecord 内部で Arel を駆使する)場面向けの機能拡張です。
- この PR では ActiveRecord のパブリック API(
対応 DB
RETURNINGがサポートされるのは PostgreSQL と SQLite3。- PostgreSQL:
INSERT/UPDATE/DELETEすべてでサポート - SQLite3: 同上(最近のバージョンで対応)
- PostgreSQL:
- MySQL は
RETURNINGをサポートしていないため、この機能を使って生成した SQL は MySQL ではエラーになります。- アプリケーション側で DB ごとの分岐やアダプタごとの挙動に注意する必要があります。
既存コードへの影響
- 既存の
INSERT/UPDATE/DELETEの SQL 生成ロジックに後方互換性のある形でRETURNING句をオプションとして追加しているため、returningを呼んでいない既存コードの SQL は変わりません。 ToSql/Dotvisitor の挙動はreturningがnil(または空)であれば従来通り。
- 既存の
パフォーマンス/トランザクション的な観点
- これまで Ruby から複数クエリで行っていた「変更 + 取得」を 1 クエリで行えるため、往復回数削減やロック時間短縮などのメリットが期待できます。
- 一方で、返ってくる行数が多いケースでは結果セットのサイズが増える点に注意が必要です(設計時に考慮が必要)。
- 参考情報 (あれば)
対応 DB のドキュメント
- PostgreSQL
- SQLite3
ActiveRecord の
RETURNINGサポートに関する既存ディスカッション- #39968, #42955(この PR ではそこまで踏み込まず、まずは Arel だけ拡張している)
この PR によって、PostgreSQL / SQLite を使うプロジェクトでは、Arel を直接用いることで「データ変更とその結果取得を 1 クエリで行う」高度なクエリ構築がしやすくなります。
#57215 Fix ParameterFilter hash-lookup optimization for line-anchored regexps
マージ日: 2026/5/14 | 作成者: @alexcwatt
- 概要 (1-2文で)
ActiveSupport::ParameterFilterの正規表現最適化で、/^token$/と/\Atoken\z/を同一視していたことによる不正なフィルタリング挙動を修正し、行アンカー (^/$) と文字列アンカー (\A/\z) を正しく区別するようにした PR です。これにより、複数行を含むパラメータキーでも本来の正規表現 semantics に従って安全にフィルタリングできるようになります。
- 変更内容の詳細
背景となる最適化
前の PR (#57166) で、パフォーマンス向上のために以下のような「完全一致」正規表現をハッシュルックアップに変換する最適化が入りました。
# 例: フィルタ対象キーを正規表現で指定
filter = ActiveSupport::ParameterFilter.new([/\Atoken\z/, /^secret$/])このとき、/\Atoken\z/ も /^token$/ も
- 「
tokenに完全一致するキー」とみなして @exact_keysのようなハッシュに"token"を登録し- 実行時は正規表現マッチではなく、ハッシュのキー一致で判定する
という扱いになっていました。
しかし Ruby の正規表現では:
\A/\z: 文字列全体の先頭・末尾アンカー(multi-line を意識しない)^/$: 行頭・行末アンカー("token\nfoo"のような複数行文字列では 2 行目以降にもマッチしうる)
という違いがあり、/^token$/ と /\Atoken\z/ は同じ意味ではありません。
問題となっていたケース
/^token$/ の場合、本来は以下のようなキーもマッチし得ます:
key = "token\nfoo"
key.match?(/^token$/) # => true(1行目 "token" にマッチ)ところが前回の最適化では "token\nfoo" というキーに対して
- 「完全一致の
"token\nfoo"としてのみ」ハッシュ検索を行う "token"という行に対するマッチ可能性を考慮していない
ため、本来 ^/$ の semantics ではマッチすべきケースを見逃す可能性がありました。
今回の修正内容
今回の PR では、この最適化を次のように細分化・修正しています。
\A...\zは「本当の完全一致」として@exact_keysに保持
例:/\Atoken\z/- キーが
"token"のときのみマッチ "token\nfoo"はマッチしない
→ ハッシュでの完全一致判定と意味が一致するため、従来どおり最適化対象にしてよい。
- キーが
^...$は@exact_line_keysのような「行単位完全一致」用の別ハッシュに保持
例:/^token$/- まずキー全体 (
"token\nfoo"など) についてハッシュを引く - さらにキーに
"\n"が含まれている場合は、key.split("\n")の各行に対してもハッシュを参照
→ 「行単位でtokenに一致するか」をハッシュで疑似的に再現する。
イメージコード:
rubyif exact_line_keys[key] # マッチしたとみなす elsif key.include?("\n") key.each_line do |line| if exact_line_keys[line.chomp] # マッチしたとみなす end end end- まずキー全体 (
混在アンカー
/\Atoken$/,/^token\z/は最適化しないで素直に正規表現マッチへフォールバック/\Atoken$/: 文字列全体の先頭 + 行末/^token\z/: 行頭 + 文字列全体の末尾
いずれも、ハッシュで表現するには意味が非対称で複雑であり、簡単には正しく再現できないため、「危ない最適化」は捨てて元の regexp マッチのままとしています。
テスト
activesupport/test/parameter_filter_test.rb に新規テストが追加され、次のような点がカバーされていると考えられます(テスト名から推測される範囲):
/\A...\z/が完全一致として正しく扱われること/^...\$/が複数行キーに対して行単位で正しくマッチすること- 混在アンカーの regexp はハッシュ最適化されず、正しい挙動を保つこと
- 影響範囲・注意点
- 対象:
ActiveSupport::ParameterFilterを使用し、フィルタパターンにアンカー付きの正規表現(特に^/$)を使っているコード。 - 行アンカー (
^/$) を使ったフィルタ:- これまで誤って「完全一致」と解釈されていたケースが、本来の Ruby 正規表現どおりの挙動に戻るため、複数行キーを持つ(ややレアな)環境ではフィルタ結果が変わる可能性があります。
- パフォーマンス:
/\A...\z/のパターンは引き続き高速なハッシュルックアップで処理されます。/^...\$/も別ハッシュ + 行分割で依然として最適化されており、フル regexp スキャンよりは有利と考えられます。/\A...\$/や/^...\z/のような混在アンカーは最適化されなくなるため、そのようなパターンを大量に使うと、ごくわずかに性能が下がる可能性がありますが、正しさ優先の妥当なトレードオフです。
注意点として、もしアプリ側で
[/^token$/]のようなフィルタを「本当に文字列全体の完全一致」として意図していた場合、Ruby の意味論とはもともとずれていたので、意図どおりなら /\Atoken\z/ に変えておく方が安全で明示的です。
- 参考情報 (あれば)
- 対象コード:
activesupport/lib/active_support/parameter_filter.rb - 関連 PR:
- #57166: ハッシュルックアップによる ParameterFilter 正規表現マッチの初期最適化
- Ruby のアンカー仕様:
\A/\z: 文字列全体の先頭・末尾^/$: 行頭・行末(/mフラグや複数行文字列で挙動が変わる)
#57313 Remove framework_defaults if load_defaults matches
マージ日: 2026/5/14 | 作成者: @runephilosof
- 概要 (1-2文で)
config.load_defaultsで指定したバージョンと実際の Rails バージョンが一致している場合は、config/initializers/new_framework_defaults_*.rbを生成・維持しないようにして、「使っていない new_framework_defaults が散らかる」問題を解消する変更です。rails newだけでなくrails app:update実行時にも、不要な new_framework_defaults が勝手に再作成されなくなります。
- 変更内容の詳細
背景となる挙動
従来の挙動:
rails new:- 一番新しい
new_framework_defaults_x_y_z.rbは生成されない(削除される)
- 一番新しい
rails app:update:new_framework_defaults_x_y_z.rbは常に生成される
一方で、アプリ開発者の一般的な運用としては:
- Rails をアップグレードしたら、まず
config/initializers/new_framework_defaults_x_y_z.rbを使って個別に新デフォルトを有効/無効にしつつ動作確認 - 安定したら
config.load_defaults x.yを新バージョンに上げる - そのタイミングで
new_framework_defaults_x_y_z.rbは不要になるので削除する
ところが、(3) のあとに rails app:update を再度実行すると、不要になった new_framework_defaults がまた作られてしまい、初期化ファイルがノイズになる問題がありました。
今回の変更のコアアイデア
「アプリの load_defaults バージョンと Rails 本体のデフォルトバージョンが一致しているなら、そのアプリは“すでに新デフォルトに追随済み”とみなせるので、new_framework_defaults は不要」とみなして、生成しないようにします。
app_generator.rb の変更
railties/lib/rails/generators/rails/app/app_generator.rb に、new_framework_defaults 関連ファイルを生成するかどうかを判定するロジックが追加されています。概ね次のような条件分岐が入ったと考えられます(疑似コード):
if load_defaults_version == Rails::VERSION::STRING # または適切なバージョン比較
# すでに最新デフォルトに揃っているので new_framework_defaults は不要
# → 生成しない
else
# 移行途中のアプリなので new_framework_defaults を生成する
endこれにより:
rails new実行時:config.load_defaultsが最新バージョンを指す新規アプリでは、基本的にnew_framework_defaults_*.rbは生成されない
rails app:update実行時:- すでに
config.load_defaultsを最新に上げているアプリでは、削除済みのnew_framework_defaults_*.rbが再作成されない
- すでに
テストの追加
railties/test/generators/app_generator_test.rb にテストが22行追加されています。内容としては:
- 特定の
load_defaultsバージョン設定のもとでrails new/rails app:updateを動かし config/initializers/new_framework_defaults_*.rbが生成される/されないことを確認する
といったケースが追加されていると考えられます。
CHANGELOG の更新
railties/CHANGELOG.md に、上記の挙動変更が明記されました。これにより、Rails アップデート時に「new_framework_defaults が自動で作られなくなった」理由が追えるようになっています。
- 影響範囲・注意点
影響対象:
rails newrails app:updateで生成されるconfig/initializers/new_framework_defaults_*.rb周りの挙動
実用上の効果:
- すでに
config.load_defaultsを最新バージョンに上げているアプリでは:rails app:updateを何度再実行しても、new_framework_defaultsが勝手に復活しない
→ 初期化ファイルディレクトリがスッキリし、マイグレーション完了後のノイズが減る
- まだ
load_defaultsを古いバージョンにしているアプリでは:- これまで通り
new_framework_defaults_x_y_z.rbが作られ、個別フラグでデフォルト挙動を切り替えながら移行できる
- これまで通り
- すでに
注意点:
new_framework_defaultsを「将来的なメモ」として意図的に残しておきたい運用をしている場合、config.load_defaultsを最新に上げた瞬間から、新しくrails app:updateを実行してもそのファイルは再生成されません。
→ その場合は、load_defaultsを上げる前に内容を別の場所へ移すか、自前の initializer に整理しておく必要があります。- 古い Rails からの移行ドキュメントで「
app:updateを走らせると new_framework_defaults が生成される」と書かれているケースがありますが、この PR 以降のバージョンでは「load_defaultsが最新でない場合のみ生成」という前提に読み替える必要があります。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57313
- 関連概念:
config.load_defaults x.y
→ 「Rails x.y 時点のフレームワークデフォルトを一括適用」するメソッドconfig/initializers/new_framework_defaults_x_y_z.rb
→ 新バージョンで変わる挙動を個別フラグでオン/オフしながら段階的に移行するためのテンプレートファイル
#55100 Fix #55099 allowing array syntax for expression indexes in add_index
マージ日: 2026/5/14 | 作成者: @AlexandreCamillo
- 概要 (1-2文で)
add_index/t.indexで「式インデックス(expression index)」を定義する際、これまで文字列でしか書けなかったものを、通常のマルチカラムインデックスと同様に配列シンタックスでも書けるようにした修正です。これにより、["lower(email)", :status]のように、式とカラムを一貫した形で指定できます。
- 変更内容の詳細
これまでの問題点
Rails のマイグレーションで式インデックスを張る場合:
create_table :users do |t|
t.string :email
t.string :status
end
add_index :users, 'lower(email), status'
# または
t.index 'lower(email), status'というように、式インデックスは 生 SQL 文字列 で書く必要がありました。
これだと:
- 通常のインデックスが
[:email, :status]のような配列指定なのと書き方が揃わない - 式と通常カラムを混在させるときに見通しが悪い
- Ruby 側での処理(例:
Array前提の処理)と噛み合わない
といった不便がありました。
本 PR の変更点
add_index / t.index で 式インデックスを配列シンタックスでも指定可能 にしました。
新たに許可される書き方
# 式 + カラム
t.index ["lower(email)", :status]
# 式だけ
t.index ["lower(email)"]
# もちろん従来通りのカラムのみ指定も可能
t.index [:email, :status]これにより:
- 配列の中に「文字列で表現した式」と「シンボル(または文字列)で表した通常カラム」を混在させて指定できる
- 従来の
"lower(email), status"のような カンマ区切りの 1 つの文字列 ではなく、要素ごとに分割された配列 として指定できる
という改善が入っています。
実装レベルのポイント(推測を含む)
activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb の add_index 内部で:
- これまで「引数が文字列ならそのまま SQL 断片として扱う」という処理が主だったところに、
- 「配列が渡された場合、その要素がシンボルならカラム名としてクオート、文字列なら式として扱う」ロジックが追加
されている形です。index_test.rb 側では、以下のようなケースがテストされているはずです:
t.index ["lower(email)"]でインデックスが作れることt.index ["lower(email)", :status]で複合インデックスが作れること- 生成されるインデックス名・SQL が期待通りであること
CHANGELOG.md にも「配列シンタックスで expression index を書けるようになった」というエントリが追加されています。
- 影響範囲・注意点
互換性
- 既存の文字列指定(
'lower(email), status')はそのまま動作し、破壊的変更にはなっていません。 - 既存マイグレーションを書き換える必要はありませんが、今後は配列シンタックスを使うことで可読性を上げられます。
- 既存の文字列指定(
書き方の指針
- 「SQL の式として扱いたい部分」は 文字列 で書く必要があります:ruby
t.index ["lower(email)", :status] # OK t.index [:lower_email, :status] # これは「:lower_email カラム」と解釈される - 通常のカラムは従来どおりシンボル/文字列で指定可能です。
- 「SQL の式として扱いたい部分」は 文字列 で書く必要があります:
データベース固有の式
"lower(email)"のような式自体は DB 依存(PostgreSQL での LOWER 関数など)なので、DB を跨ぐマイグレーションでは従来同様、式の可搬性には注意が必要です。- 本 PR はあくまで「指定方法」の改善であり、式そのものの互換性までは保証しません。
コードの一貫性とリファクタリング
- 既存のマイグレーションをリファクタする場合は、下記のような置き換えが可能です:ruby
# Before t.index 'lower(email), status' # After t.index ["lower(email)", :status] - RuboCop ルールなどで「配列シンタックスを推奨する」ようなガイドラインを設けると、コードスタイルを統一しやすくなります。
- 既存のマイグレーションをリファクタする場合は、下記のような置き換えが可能です:
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/55100
- 関連 Issue: #55099
- 関連ドキュメント(現行 Rails Guides — Active Record マイグレーション / Indexes)も、今後この配列シンタックスについて追記される可能性があります。
#54416 ActiveStorage: implement attach!
マージ日: 2026/5/14 | 作成者: @quentindemetz
- 概要 (1-2文で)
ActiveStorage のattachに対応する「バン付き」メソッドattach!が、一つ/複数添付の両方(has_one_attached,has_many_attached)に実装されました。attach!は保存に失敗した場合に例外を投げることで、添付がサイレントに失敗することを防ぎます。
- 変更内容の詳細
何が追加されたか
ActiveStorage の以下2クラスに attach! が追加されています。
ActiveStorage::Attached::One#attach!ActiveStorage::Attached::Many#attach!
挙動は ActiveRecord の save / save! と同様の関係になります。
attach- 内部で
record.saveを呼ぶ - バリデーション等で保存に失敗した場合でも 例外は投げずに false を返す/添付されない
- 内部で
attach!- 内部で
record.save!相当の処理を呼ぶ - 保存に失敗した場合
ActiveRecord::RecordInvalidなどの例外を発生させる
- 内部で
これにより、「必ず添付できていること」を前提にしたコードを書きやすくなります。
想定される実装イメージ
実際のコードは PR 参照ですが、ざっくりしたイメージは以下のようなものです(擬似コード):
# activestorage/lib/active_storage/attached/one.rb
def attach(*args)
change = build_change(*args)
change.save # => 失敗しても例外は上げない
end
def attach!(*args)
change = build_change(*args)
change.save! # => 失敗時に ActiveRecord::RecordInvalid などの例外
endMany 版も同様のパターンで実装されています。
テスト
次のファイルに attach! 用のテストが追加されています。
activestorage/test/models/attached/one_test.rbactivestorage/test/models/attached/many_test.rb
テスト内容の方向性:
attach!が正常系で添付を行えること- バリデーション失敗など「保存できない条件」の場合に例外を投げること
また、activestorage/CHANGELOG.md にもこの新機能が追記されています。
- 影響範囲・注意点
影響範囲
- 既存の
attachの挙動は 一切変更されていない ため、既存コードへの互換性は基本的に保たれています。 - 新たに
attach!が利用可能になり、以下のような書き方ができるようになります。
class User < ApplicationRecord
has_one_attached :avatar
has_many_attached :photos
end
user = User.find(1)
# 1件添付(失敗したら例外)
user.avatar.attach!(io: File.open("/path/to/file"), filename: "avatar.png")
# 複数添付(失敗したら例外)
user.photos.attach!([
{ io: File.open("/path/to/1.png"), filename: "1.png" },
{ io: File.open("/path/to/2.png"), filename: "2.png" }
])注意点
attach!はバリデーションや DB 制約などで添付が保存できなかった場合に例外を投げます。- 例:
ActiveRecord::RecordInvalid, DB のユニーク制約違反由来の例外など
- 例:
- 例外ハンドリングを明示的に行う場合、以下のようなコードが必要になります。
begin
user.avatar.attach!(...) # or user.photos.attach!(...)
rescue ActiveRecord::RecordInvalid => e
# エラーメッセージ: e.record.errors.full_messages など
end- 「失敗しても無視して進めたい」ケースでは、従来どおり
attachを使う方が適切です。 - モデル側で
validate :xxx_on_attachmentsのように添付に絡むバリデーションを行っている場合、attach!利用時に例外発生の可能性がある点に注意してください。
- 参考情報 (あれば)
- この PR の背景になっている ActiveRecord のバリデーションと
save/save!について:
https://guides.rubyonrails.org/active_record_basics.html#validations - ActiveStorage の添付に関するガイド(今後
attach!が追記される可能性あり):
https://guides.rubyonrails.org/active_storage_overview.html
#57360 Fix renderable arity checks when method is overridden
マージ日: 2026/5/14 | 作成者: @chaadow
- 概要 (1-2文で)
Rails のrenderが「レンダリング可能オブジェクト」のrender_inの引数個数(arity)を判定する際に、自前でmethodメソッドを定義しているオブジェクトだと壊れていた問題を修正した PRです。Kernel#methodを明示的に呼ぶことで、render_inの arity 判定が常に正しく行われるようになりました。
- 変更内容の詳細(あればサンプルコードも含めて)
問題の背景
Rails では、以下のような「レンダリング可能オブジェクト」(view component 的なもの)を render で扱うことができます:
class MyRenderable
def render_in(view_context, &block)
# ...
end
end
render MyRenderable.newこのとき Rails 側では、「このオブジェクトは render_in を持っているか」「render_in はどんな arity (引数の個数) か」をチェックして挙動を切り替えたりします。
以前の実装では、おそらく以下のようなコードに相当する処理をしていました:
renderable.method(:render_in).arityところが、レンダリング対象のオブジェクトが method を独自実装している場合に問題が起こります:
class MyRenderable
# 独自の method メソッドを定義している
def method(name)
# 何か全然違うことをする or 対応していない
end
def render_in(view_context)
# 実際のレンダリング処理
end
endこの場合、renderable.method(:render_in) は Ruby 組み込みの Kernel#method ではなく、このクラスの method が呼ばれてしまい、
NoMethodErrorや- 不正なオブジェクトが返る
などによりレンダリングが壊れてしまいます。
PRでの修正内容
actionview/lib/action_view/template/renderable.rb での arity チェック処理を、次のように「必ず Kernel 実装を使う」ように修正しています (擬似コード):
# 修正前 (イメージ)
renderable.method(:render_in).arity
# 修正後 (イメージ)
Kernel.instance_method(:method).bind(renderable).call(:render_in).arity
# あるいは内部的には `Kernel#method` を明示的に呼び出している要点としては:
Object#method(=Kernel#method) を明示的に呼び出すことで、- オブジェクトが
methodをオーバーライドしていても、 render_inのメソッドオブジェクトと arity を正しく取得できるようにした。
テストの追加
actionview/test/template/render_test.rb に回帰テストが追加されています。内容としては:
methodを独自定義したレンダリング可能オブジェクトを用意- そのオブジェクトを
renderしようとしたときに、例外にならず正常に動作すること - かつ、
render_inが正しく呼ばれていること
を確認するテストです。
今回のバグが将来の変更で再発しないようにするための回帰テストになっています。
- 影響範囲・注意点
- 影響を受けるのは「
render可能なオブジェクト」のうち、methodを独自に定義しているクラスです。- 例: コンポーネント/デコレーターなどでメタプログラミングをしており、
methodをフックしているようなケース。
- 例: コンポーネント/デコレーターなどでメタプログラミングをしており、
- これらのクラスが
render_inを実装していても、以前はrender時の arity チェックで壊れる可能性がありましたが、この PR によって正しく動くようになります。 - 変更は internal な arity 判定ロジックのみであり、パブリック API のシグネチャや一般的な使い方には影響しません。
method_missingをカスタマイズしているだけでmethodを直接オーバーライドしていない場合は、もともと大きな影響はなく、この PR による挙動変化もほぼありません。
運用上の注意という観点では:
- 自作コンポーネントや view object で
methodをオーバーライドしている場合、Rails のバージョンアップで「今まで壊れていたレンダリングが直る」可能性があります。 - 逆に、「壊れていた前提でワークアラウンドを書いていた場合」は、そのワークアラウンドが不要になるか確認した方が良いかもしれません。
- 参考情報 (あれば)
- 当該 PR:
https://github.com/rails/rails/pull/57360 - この修正の背景となる議論(元のバグレポート・文脈):
https://github.com/rails/rails/pull/50623#discussion_r3230073756
このあたりを読むと、「なぜ renderable の arity 判定が必要なのか」「どのように API 互換性を保ちながら実装しているのか」が補足的に理解できます。
#57366 Freeze more immutable constants
マージ日: 2026/5/14 | 作成者: @andrewn617
- 概要 (1-2文で)
Rails内部で「本来不変であるべき定数」に.freezeを追加し、特に配列・ハッシュ・Set などのミュータブルなオブジェクトを不変化することで、Ractor 対応(並列実行の安全性向上)を進めるPRです。前回PR (#57323) でカバーしきれなかった箇所を追加で凍結しています。
- 変更内容の詳細
全体方針
- 対象:
- Hash / Array / Set / オブジェクトインスタンスを値に持つ定数
- RuboCop の
Style/MutableConstantでは検出されにくいパターン(例:Set[]で定義された定数、Object.newを代入している定数など)
- 対応:
- それらの定数定義に
.freezeを付与 - 既存の定数の意味や構造は変えず、「あとから変更できない」ようにするだけの変更
- それらの定数定義に
コードレベルではほぼすべて、次のような形の修正です(ファイルごとに具体的なキー/値は異なるがパターンは同じ):
# 変更前
SOME_CONSTANT = {
foo: :bar,
baz: :qux,
}
# 変更後
SOME_CONSTANT = {
foo: :bar,
baz: :qux,
}.freeze# 変更前
SOME_SET = Set[:foo, :bar]
# 変更後
SOME_SET = Set[:foo, :bar].freeze# 変更前
SOME_ARRAY = [1, 2, 3]
# 変更後
SOME_ARRAY = [1, 2, 3].freeze主な変更箇所の役割イメージは以下の通りです(実際のキー名などはファイルごとに異なります):
actionpack/lib/action_dispatch/http/headers.rb- HTTPヘッダの扱いに関する定数のハッシュ/配列を
.freeze。
- HTTPヘッダの扱いに関する定数のハッシュ/配列を
actionpack/lib/action_dispatch/routing/mapper.rb- ルーティング定義で使われる内部定数(例えばHTTPメソッドやオプション関連)を
.freeze。
- ルーティング定義で使われる内部定数(例えばHTTPメソッドやオプション関連)を
actionview/lib/action_view/helpers/javascript_helper.rb- JavaScriptヘルパ内のオプション・テンプレート関連の定数(配列/ハッシュ)を
.freeze。
- JavaScriptヘルパ内のオプション・テンプレート関連の定数(配列/ハッシュ)を
actionview/lib/action_view/helpers/tag_helper.rb- HTMLタグ描画ヘルパの定数(属性名リスト等)を
.freeze。
- HTMLタグ描画ヘルパの定数(属性名リスト等)を
actionview/lib/action_view/template.rb- テンプレートに関する内部オプション・ハンドラの定数を
.freeze。
- テンプレートに関する内部オプション・ハンドラの定数を
activejob/lib/active_job/exceptions.rb- リトライ戦略などで利用される例外関連の定数(リスト/マッピング)を
.freeze。
- リトライ戦略などで利用される例外関連の定数(リスト/マッピング)を
activemodel/lib/active_model/attribute.rb- 属性ハンドリングに使用する定数(型・フラグなど)を
.freeze。
- 属性ハンドリングに使用する定数(型・フラグなど)を
activerecord/*(primary_key, attributes, relation, relation/merger, relation/query_methods)- クエリメソッドで使う予約語のSet/配列
- マージ戦略、primary key関連で使う内部ハッシュ
を.freeze。
activesupport/lib/active_support/cache.rb- キャッシュオプションやプリセット設定の定数を
.freeze。
- キャッシュオプションやプリセット設定の定数を
activesupport/lib/active_support/hash_with_indifferent_access.rb- 内部変換/初期値に関する定数を
.freeze。
- 内部変換/初期値に関する定数を
activesupport/lib/active_support/testing/assertions.rb- テスト用アサーションで使うメッセージやオプションの定数を
.freeze。
- テスト用アサーションで使うメッセージやオプションの定数を
activesupport/lib/active_support/xml_mini.rb- XML パーサ関連のアダプタ名やオプション定数を
.freeze。
- XML パーサ関連のアダプタ名やオプション定数を
純粋に「可変なオブジェクトを定数に置いている箇所を、不変にした」という修正であり、ロジック変更や新機能の追加はありません。
- 影響範囲・注意点
- 外部API/動作仕様の変化:
- 公開APIの挙動としては変化はほぼありません。
- ただし「Rails内部の定数を実行時に書き換えていた」ようなコードがある場合は、
FrozenErrorが発生する可能性があります。- 例:
ActiveRecord::Relation::SOME_INTERNAL_CONSTANT << :new_valueのようなコードは動かなくなります。
- 例:
- Ractor 対応・スレッドセーフティへの影響:
- Ractor では、共有されるオブジェクトが原則不変である必要があり、このPRはその前提を満たすための一歩です。
- 定数にぶら下がるミュータブルなオブジェクトを凍結することで、マルチRactor環境での安全性と予測可能性が向上します。
- パフォーマンス:
- 参照時のオーバーヘッドはありません。
- 初期化時に
.freezeが1回走るだけなので、実質的なパフォーマンス影響は無視できるレベルと考えられます。
- テスト:
- 新しいテストの追加は明示されていませんが、「挙動が変わらないリファクタリング」扱いで既存テストがパスすることを前提とした変更です。
- 参考情報 (あれば)
- このPRの背景となる前回PR:
- https://github.com/rails/rails/pull/57323
- 同様に「不変であるべき定数を
.freezeする」対応を行ったもの。
- 同様に「不変であるべき定数を
- https://github.com/rails/rails/pull/57323
- Ractor と不変オブジェクトの関係(Ruby本体のドキュメント):
- Ractorはスレッドセーフな並列実行モデルであり、Ractor間で共有できるのは「不変のオブジェクト」または「Ractor間共有を許可されたオブジェクト」に限られます。
- Rails内部の定数を凍結することは、RailsをRactor対応させる上での前提条件の1つです。
#51627 [Fix #51524] Fix strict_loading violations ignored when using pluck
マージ日: 2026/5/14 | 作成者: @shes50103
- 概要 (1-2文で)
strict_loadingモードが有効な関連に対してpluckを呼んだ場合でも、他の場合と同様に strict loading 違反としてエラー or ログ出力されるように修正した PRです。
あわせて、「事前に preload されている関連」は strict loading 違反とみなされないように判定ロジックが調整されています。
- 変更内容の詳細
2-1. pluck で strict_loading 違反を検出するように
対象: ActiveRecord::Calculations#pluck(とそれを使う CollectionProxy#pluck)
元々の問題:strict_loading を有効にしていても、関連オブジェクトに対して pluck を呼ぶと strict loading のチェックをすり抜けてしまい、N+1 などを防ぐ目的が達成されないケースがありました。
例(イメージ):
class Post < ApplicationRecord
has_many :comments
end
post = Post.strict_loading.first
# post.comments のアクセスが strict loading 対象
# 本来は strict loading 違反としたいケース
post.comments.pluck(:content)この PR により、pluck が内部で関連をロードする際にも strict loading チェックが走り、設定に応じて:
strict_loading_by_default = trueかつ違反 → 例外を raise- ログモードの場合 → 違反としてログ出力
といった、他の関連ロードと同等の挙動になります。
PR本文では ActiveRecord::Calculations#pluck の更新と書かれていますが、差分としては CollectionProxy 周りにも pluck の strict loading 対応コードが追加されており、関連コレクション経由の pluck も対象になっています。
2-2. preload 済み関連は strict_loading 違反にしないように
対象: ActiveRecord::Associations::Association#violates_strict_loading?
これまでの挙動だと、「preload で事前にロードしておいた関連」にアクセスしても strict loading 違反として扱われるケースがありました。
この PR では、violates_strict_loading? の判定ロジックを変更し:
- strict_loading が有効
- まだロードされていない関連に初回アクセス → 違反
- preload・eager_load・includes 等で事前ロード済み → 違反ではない
という形に整理されています。
具体的には、Association 内部で「preloaded な状態」を考慮した条件分岐に変更されており、preload を活用しつつ strict_loading を使うユースケースがサポートされます。
2-3. テスト・CHANGELOG
activerecord/test/cases/strict_loading_test.rbに、pluckと preload に絡む strict loading のテストケースが追加。activerecord/CHANGELOG.mdに挙動変更として追記。
- 影響範囲・注意点
strict_loading を使っているアプリでの挙動変化
- これまで問題なく動いていた
association.pluck(:column)が、strict_loading 違反として例外を投げたりログ出力されるようになる可能性があります。 strict_loadingを「N+1 検出用のガード」として期待していた場合は、より一貫した挙動になりますが、既存コードが壊れることもあり得るので確認が必要です。
- これまで問題なく動いていた
preload を正しく使っていればエラーは出ない
includes/preload/eager_loadなどで事前ロードしておけば、strict_loading 違反にはなりません。strict_loading +
pluckを使う場合は、以下のような書き方を推奨できます:rubyposts = Post.strict_loading.includes(:comments).to_a # preload 済みなので OK posts.first.comments.pluck(:content)
関連 PR とのコンフリクト
- PR本文にある通り、似た問題を別のシナリオで扱う PR (#50389) とコンフリクトしうるため、strict_loading 周りをいじっている場合は upstream のマージ状況に注意が必要です。
- 参考情報 (あれば)
- 対応 Issue: https://github.com/rails/rails/issues/51524
- 関連 PR(コンフリクト懸念あり): https://github.com/rails/rails/pull/50389
- strict_loading の概要(Rails ガイド)
- Active Record Query Interface – Eager Loading Associations / Strict Loading 付近を参照すると、この変更による意図(一貫した N+1 検出)が理解しやすいです。
#57358 Document ActiveStorage::Blob#custom_metadata [ci-skip]
マージ日: 2026/5/14 | 作成者: @p8
- 概要 (1-2文で)
Active Storage のActiveStorage::Blob#custom_metadata/#custom_metadata=メソッドについて、既に存在していたものの公式に説明されていなかったため、コメントでドキュメントを追加した PR です。挙動や API 仕様の変更はなく、コード上のドキュメント整備のみです。
- 変更内容の詳細(あればサンプルコードも含めて)
- 対象:
activestorage/app/models/active_storage/blob.rb - 追加されたのは 8 行程度のコメント(YARD / RDoc 用のドキュメント)で、
ActiveStorage::Blobに定義されているcustom_metadata系メソッドが何をするのかを説明しています。 - これらのメソッド自体は、過去のコミット
e106a4a1d29ですでに導入済みで、今回は「ドキュメントがなかったものに説明を足しただけ」という位置づけです。
custom_metadata は、Active Storage が内部で使う標準の metadata とは別枠で、アプリケーション側が自由にキー・バリューを保存するための領域として使う想定の機能です。典型的には、以下のように利用します。
blob = ActiveStorage::Blob.create_and_upload!(
io: File.open("/path/to/file.jpg"),
filename: "file.jpg",
content_type: "image/jpeg",
)
# 任意のメタデータを設定
blob.custom_metadata = {
"source" => "user_upload",
"license" => "CC-BY-SA",
"tag" => "profile_image"
}
blob.save!
# 読み出し
blob.custom_metadata
# => { "source" => "user_upload", "license" => "CC-BY-SA", "tag" => "profile_image" }ドキュメントの追加により、例えば「metadata と custom_metadata の違い」や「ユーザー定義の属性をどこに持たせるべきか」といった点が、公式リファレンスから読み取りやすくなります。
- 影響範囲・注意点
- 影響範囲:
- 実行時の挙動・API のシグネチャは一切変わらず、既存コードへの影響はありません。
- Rails ガイドや API ドキュメント生成時に、
custom_metadataメソッドが正式に説明されるようになります。
- 注意点:
- ドキュメントが追加されたことで、今後
custom_metadataが「公式にサポートされた API」として認識されやすくなります。既に内部的に使っていたプロジェクトは、その使い方がドキュメントと合っているか、改めて確認するとよいです。 - PR タイトルに
[ci-skip]とある通り、テスト実行を伴わないドキュメント専用変更であるため、実装側のテストカバレッジや品質に関する変化はありません。
- ドキュメントが追加されたことで、今後
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57358
custom_metadataが導入された元コミット:e106a4a1d29- Active Storage ガイド(メタデータ関連の章に今後反映される可能性があります):
https://guides.rubyonrails.org/active_storage_overview.html
#57362 [ci skip] Update the debugging guide to link the actively maintained Exception Notifier repo
マージ日: 2026/5/14 | 作成者: @KDunc11
概要 (1-2文で)
Railsガイド「debugging_rails_applications.md」に記載されていた Exception Notifier へのリンクが、メンテナンスされていないリポジトリを指していたため、現在アクティブにメンテナンスされているリポジトリへのリンクに差し替えたドキュメント修正PRです。コードや挙動の変更はなく、あくまでガイド文書の更新のみです。変更内容の詳細(あればサンプルコードも含めて)
- 対象ファイル:
guides/source/debugging_rails_applications.md - 変更内容:
- Exception Notifier の GitHub リポジトリ URL を、非メンテナンスのものから「現在アクティブにメンテされている Exception Notifier リポジトリ」に差し替え。
- 具体的には 1 行のみの変更で、古いリンクを新しいリンクに置き換えています(ガイド上の説明文やサンプルコード等は変更なし)。
(実際の diff イメージ)※URLは例示
- * Exception Notifier: https://github.com/old-org/exception_notifier
+ * Exception Notifier: https://github.com/railsware/exception_notification※ 実際の組織名・リポジトリ名は PR の差分に依存しますが、趣旨としては「正しい現行リポジトリに差し替え」という変更です。
- 影響範囲・注意点
- 影響範囲:
- Rails ガイドを読みながら例外通知の設定を行う開発者が、メンテされていない古い repo ではなく、現行でメンテされている Exception Notifier の実装・README にアクセスできるようになります。
- ランタイムの挙動・Rails 本体の API・テストなどには一切影響しません(ドキュメントのみ、CI スキップ指定
[ci skip]あり)。
- 注意点:
- すでに旧リポジトリを参照して設定していた場合は、ガイド更新をきっかけに「どの repo を使うべきか」を見直したほうがよい可能性があります(依存 gem の更新状況やセキュリティ修正などの観点)。
- 本 PR 自体にはテストや CHANGELOG 更新は不要と判断されており、実質的には「参照先 URL の正規化」のみです。
- 参考情報 (あれば)
- Rails ガイド(英語版):
guides/source/debugging_rails_applications.md - Exception Notifier(例外通知)の代表的な利用例(概念的な例):rubyこのような設定方法を確認する際に、今回の PR によって開発が続いている最新版の README / Wiki を参照しやすくなります。
# Gemfile gem 'exception_notification' # config/environments/production.rb Rails.application.configure do config.middleware.use ExceptionNotification::Rack, email: { email_prefix: "[MyApp ERROR] ", sender_address: %{"notifier" <notifier@example.com>}, exception_recipients: %w[dev-team@example.com] } end
#57359 Fix formatting in Active Storage controller docs
マージ日: 2026/5/13 | 作成者: @p8
- 概要 (1-2文で)
Active Storage のコントローラに書かれているドキュメントコメント内のインラインコード表記を、+foo+から<tt>foo</tt>に変更して、Rails API ドキュメント上での表示を整えた PRです。機能や挙動の変更は一切なく、ドキュメント表示の体裁を整えるだけの修正です。
- 変更内容の詳細
対象ファイルは以下の 4 つのコントローラです。
activestorage/app/controllers/active_storage/blobs/proxy_controller.rbactivestorage/app/controllers/active_storage/blobs/redirect_controller.rbactivestorage/app/controllers/active_storage/representations/proxy_controller.rbactivestorage/app/controllers/active_storage/representations/redirect_controller.rb
これらのファイル内の「クラスコメント (YARD/RDoc形式のドキュメントコメント)」に含まれる、インラインコードの書き方が修正されています。
以前は例えばこんな書き方になっていたものが:
# Returns the +Content-Type+ of the blob.これが <tt> を使った書き方に変更されています:
# Returns the <tt>Content-Type</tt> of the blob.Rails の API ドキュメントは RDoc ベースで生成されていますが、+...+ 表記だと文脈によっては意図したスタイルにならなかったり、フォント幅や高さが崩れて読みにくくなります。この PR では、スクリーンショットにあるように、インラインコード部分が明示的に <tt>...</tt> でマークアップされることで、フォントと高さが統一され、API ドキュメント上の見た目が改善されています。
コードロジックへの変更 (メソッド定義やルーティング、レスポンス挙動等) はなく、差分は各ファイルともコメント行で +...+ → <tt>...</tt> の 1 行置換のみです。
- 影響範囲・注意点
ランタイムの挙動への影響:
- コントローラの処理内容やレスポンスには一切影響しません。
- テストやアプリケーションコードの変更は不要です。
対象:
- Rails API ドキュメント (特に Active Storage の blob/representation 用プロキシ/リダイレクトコントローラのクラスリファレンス) の見た目のみが変わります。
ci-skipが付いている通り、ビルドやテスト観点では無視してよい純粋なドキュメント整形変更です。
注意点 (Rails にコントリビュートする側向け):
- Active Storage に限らず、Rails 本体のコメントで「インラインコード」を書くときは、
+...+ではなく<tt>...</tt>を使うのが今後の推奨スタイルであることが読み取れます。 - ドキュメントのスタイルを合わせたい場合、この PR と同じ方針で
<tt>への統一を検討するとよいです。
- Active Storage に限らず、Rails 本体のコメントで「インラインコード」を書くときは、
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57359
- Rails の RDoc スタイル (RDoc 本体のドキュメント):
- https://docs.ruby-lang.org/en/master/RDoc/Markup.html
- Rails API ドキュメント生成では
<tt>がコードフォントとして利用されるため、見た目を安定させる目的で採用されています。
#57357 Remove reference to removed QueAdapter
マージ日: 2026/5/13 | 作成者: @p8
- 概要 (1-2文で)
QueAdapter 本体がすでに削除されているにもかかわらず、ActiveJob内に残っていた QueAdapter への参照を整理・削除する PR です。
これにより、存在しないアダプタへの不整合な参照がなくなり、Active Job のキューアダプタ定義が実装と一致するようになりました。
- 変更内容の詳細
変更ファイル:
activejob/lib/active_job/queue_adapters.rb(+0 / -2)
このファイルは、Active Job で利用可能なキューアダプタをまとめて管理している場所です。
ここから QueAdapter に関する2行が削除されています。
おおまかなイメージとしては、以下のようなコードが:
module ActiveJob
module QueueAdapters
# 例: 他のアダプタ
class AsyncAdapter; end
class InlineAdapter; end
# ...
class QueAdapter; end # ← こういった参照が残っていた
end
endこのうち、QueAdapter に関する参照だけが取り除かれた、という形になります(実際のコード行は多少異なりますが、意味としては「QueAdapter という名前をもう queue_adapters.rb では扱わない」変更です)。
PR の説明にあるとおり、QueAdapter 自体は以前のコミット cb22eb2b3628de428171175063371155d70a136c ですでに削除されており、今回の PR はその「後始末」として、残っていた参照だけを消しています。
- 影響範囲・注意点
Rails本体の挙動
- Active Job のアダプタ一覧から、実装のない QueAdapter 参照が完全に消えるだけであり、既に QueAdapter は使えない状態だったため、実質的な挙動の変化はほとんどありません。
- 「参照だけ残っていたものを消した」整理 PR なので、既に QueAdapter なしでテストが通っていた環境では影響はありません。
アプリケーション側への影響
- すでに前のコミット(
cb22eb2b3...)の時点で QueAdapter は削除されているため、その時点から以下のような設定はそもそも動きません:rubyconfig.active_job.queue_adapter = :que - この PR によって新しく壊れるものが増えるわけではなく、「まだどこかに QueAdapter の名残りがあって、それに依存していた」ようなメタプログラミングをしていない限り、追加の影響はありません。
- すでに前のコミット(
ライブラリ/ジェネレータへの影響
ActiveJob::QueueAdaptersモジュールを列挙してアダプタ一覧を動的に取得しているツールがあった場合、そこからQueAdapterが消えます。- ただし、QueAdapter 実装が既に存在しないため、そのようなツールはすでに壊れていたか、もしくは QueAdapter を除外するように対応済みであるべきです。
- 参考情報 (あれば)
- 本PRで言及されている削除コミット:
cb22eb2b3628de428171175063371155d70a136c- このコミットで QueAdapter 本体が Rails から取り除かれた(詳細はそのコミットログを参照)。
- Que を使いたい場合の一般的な方針:
- Rails 標準から QueAdapter が消えたため、Que を利用する場合は外部 gem(例: community 製の ActiveJob 用 Que アダプタ)を使う、もしくは自前でアダプタを実装する形になります。
- 自作アダプタを使う場合は、例えば以下のような形で設定します:ruby
# config/initializers/que_adapter.rb class QueAdapter < ActiveJob::QueueAdapters::Base # perform_later されたジョブを Que に投げる処理を実装 end # config/application.rb config.active_job.queue_adapter = QueAdapter.new - ただし、具体的な推奨方法や公式サポート状況は、使用する Rails のバージョンと Que 側のドキュメントを確認する必要があります。
#57351 Document ActiveStorage::Blob#content_type and Blob#metadata
マージ日: 2026/5/13 | 作成者: @p8
- 概要 (1–2文で)
Active Storage のActiveStorage::Blob#content_typeと#metadataについて、「パブリックにサポートされているメソッドである」ことをコード上で明示するドキュメントコメントが追加されました。実行時の挙動やシグネチャは変わらず、仕様の位置づけが明確化されたドキュメント改善のPRです。
- 変更内容の詳細
主な変更点
activestorage/app/models/active_storage/blob.rbにおいて、以下の2つの属性メソッドのドキュメントが追加:ActiveStorage::Blob#content_typeActiveStorage::Blob#metadata
これらは ActiveRecord のカラムに対応する通常のアクセサですが、「public API として想定されている」ことがコメントベースのドキュメントとして明示されました。
おおよそ次のような説明がコードコメントとして追記されているイメージです(疑似例):
# Returns the content type of the file associated with this blob.
# This is a public accessor and can be relied upon in application code.
#
# Examples:
#
# blob.content_type # => "image/png"
#
class ActiveStorage::Blob < ActiveRecord::Base
# ...
end# Returns a Hash with metadata extracted from the file (e.g. width, height, etc.).
# This is a public accessor and part of the supported API.
#
# Examples:
#
# blob.metadata # => { "identified" => true, "width" => 800, "height" => 600 }
#
class ActiveStorage::Blob < ActiveRecord::Base
# ...
end※実際のコメント文言は多少異なる可能性がありますが、意図としては「よく使われる ActiveRecord アクセサであり、パブリック API としてドキュメント化した」というものです。
サンプル利用コード
すでに一般的に行われている使い方が、公式に「OK」と明文化された形です。
blob = ActiveStorage::Blob.last
# コンテンツタイプの取得
if blob.content_type.start_with?("image/")
# 画像向けの処理
end
# メタデータの参照(幅・高さなど)
width = blob.metadata["width"]
height = blob.metadata["height"]- 影響範囲・注意点
実装上の挙動変更はなし
追加・削除ともにドキュメントコメントのみであり、メソッドの実装や DB スキーマ、シリアライズ形式などには一切変更がありません。API の位置づけがより公式に
以前から利用可能だったcontent_type/metadataが、「内部実装詳細」ではなく「公にサポートされるアクセサ」であることが明文化されたため、ライブラリ作者やアプリケーション開発者がこれらに依存しやすくなります。将来の後方互換性の期待値向上
ドキュメントされた public メソッドは、Rails が後方互換性をより強く意識する対象になるため、今後のアップグレード時にも安心して使い続けられる可能性が高くなります。注意点 (すでに使っている場合)
- 今まで通りそのまま使って問題ありません。
- 逆に言うと、「今まで非推奨だったものが急に推奨に変わった」わけではなく、慣例的に使われていたものがドキュメントに追いついた形です。
- 参考情報 (あれば)
- Active Storage ガイド(既存の全体像把握用)
https://guides.rubyonrails.org/active_storage_overview.html ActiveStorage::Blobクラスのソース (対象メソッドの実装位置)
https://github.com/rails/rails/blob/main/activestorage/app/models/active_storage/blob.rb
このPR自体はドキュメントの補強のみですが、今後 Gem や社内ライブラリで ActiveStorage::Blob を拡張する際に、content_type / metadata へ依存することの妥当性を説明しやすくなる変更と言えます。
#57355 Fix docs formatting for ActionController::Live [ci skip]
マージ日: 2026/5/13 | 作成者: @drjayvee
- 概要 (1-2文で)
ActionController::Liveのクラスドキュメントの「書式崩れ」「読みづらさ」を修正するために、コメントのフォーマット(RDoc/YARD向け)を整理した PR です。コードの挙動や API は一切変更されておらず、生成ドキュメントの見た目と読みやすさだけが改善されています。
- 変更内容の詳細
※PR は actionpack/lib/action_controller/metal/live.rb 内のコメントのみを変更しています。実装コードには差分がありません。
主な変更ポイント(推定含む):
ドキュメントの段落・インデント調整
- サンプルコードや説明文が、RDoc 生成時や IDE(RubyMine 等)で正しく整形されるように、空行やインデントを整理。
- 行頭のスペース・
#コメントの並びを調整することで、コードブロックとして認識されやすくしています。
コードブロックのマークアップ改善
- RDoc では、コメント中のインデントや
# codeのようなパターンによってコードブロック扱いされます。このルールに合わせて、ActionController::Liveのサンプル(ストリーミングレスポンスの例など)のフォーマットを修正しています。 - Web ドキュメントで「1 行として横に伸びてしまう」「改行が効いていない」などの問題を解消するためのレイアウト調整が主眼です。
- RDoc では、コメント中のインデントや
箇条書き・説明文の構造化
- 文章の途中で改行位置が悪く、RDoc で繋がって表示されていたものを段落として分離するなど、読みやすい構造になるように変更。
- メソッドや注意事項の説明が、IDE 上でもまとまったブロックとして読めるようにしています。
サンプルイメージ(あくまで典型的な修正例):
# Before:
# def stream
# response.headers['Content-Type'] = 'text/event-stream'
# 100.times { |i|
# response.stream.write "data: #{i}\n\n"
# }
# ensure
# response.stream.close
# end
# After:
# def stream
# response.headers["Content-Type"] = "text/event-stream"
#
# 100.times do |i|
# response.stream.write "data: #{i}\n\n"
# end
# ensure
# response.stream.close
# end上記はあくまでイメージですが、このようにインデントや空行を整えることで、RDoc で「コードブロック」としてきれいに表示されるようにする変更です。
- 影響範囲・注意点
ランタイム挙動への影響なし
- コメントのみの変更であり、
ActionController::Liveの API・挙動・パフォーマンスには一切影響しません。 - テストや本番環境で何かを確認・対応する必要はありません。
- コメントのみの変更であり、
影響するのは主に以下:
- Rails API ドキュメントサイト(https://api.rubyonrails.org)の
ActionController::Liveページの表示。 - RubyMine などの IDE でホバー表示されるドキュメント、あるいは「定義へジャンプ」時に表示されるコメント。
- Rails API ドキュメントサイト(https://api.rubyonrails.org)の
ドキュメントを参考に実装している場合のメリット
- ストリーミングレスポンスや SSE を
ActionController::Liveで実装する際のサンプルコードが見やすくなり、誤読やコピペミスのリスクが軽減されます。 - 「どこからどこまでがサンプルコードなのか」「どこから説明文なのか」が視覚的に明確になります。
- ストリーミングレスポンスや SSE を
- 参考情報 (あれば)
- 対象クラス:
ActionController::Live- Rack レスポンスを逐次書き込みできるようにし、HTTP ストリーミング / SSE などを実現するためのモジュール。
- 公開 API ドキュメント(変更反映後がそのうち閲覧可能に)
- ドキュメント生成:
- Rails のドキュメントは主に RDoc ベースで生成されており、コメント内のインデントや空行がレイアウトに大きく影響するため、今回のようなフォーマット修正は定期的に行われています。
#56872 Add start_day argument to this_week? for consistency with all_week
マージ日: 2026/5/12 | 作成者: @55728
- 概要 (1-2文で)
Date/Time#this_week?にstart_day引数が追加され、all_week/beginning_of_week/end_of_weekと同様に呼び出しごとに週の開始曜日を指定できるようになりました。これにより、グローバルなDate.beginning_of_weekを書き換えずに、ロケールや用途ごとに柔軟に「今週かどうか」を判定できます。
- 変更内容の詳細
追加されたインターフェース
これまで:
date.this_week? # 常に Date.beginning_of_week に依存このPR後:
date.this_week? # 従来どおり、Date.beginning_of_week を使用
date.this_week?(:sunday) # 週の開始を日曜とみなして「今週か?」を判定
date.this_week?(:monday) # 週の開始を月曜とみなして「今週か?」を判定this_week?(start_day = Date.beginning_of_week)という形で、start_dayがオプション引数として追加されています。- デフォルト値は従来どおり
Date.beginning_of_weekなので、既存コードは挙動変更なしで動作します。
他メソッドとの一貫性
既存の関連メソッドはすでに start_day を取っています:
date.all_week(start_day = Date.beginning_of_week)
date.beginning_of_week(start_day = Date.beginning_of_week)
date.end_of_week(start_day = Date.beginning_of_week)今回、this_week? にも同じシグネチャが導入されたことで、以下のように一貫したAPIで扱えるようになります:
range = date.all_week(:monday)
# ...
date.this_week?(:monday) # 同じ基準(Mon–Sun)の「今週か?」判定想定ユースケース
マルチテナント / 多言語アプリ
- USテナント: 週の開始は
:sunday - EUテナント: 週の開始は
:monday
グローバル設定を書き換えずに、テナントごと:
rubystart_day = current_tenant.week_start_day # :sunday or :monday date.this_week?(start_day)- USテナント: 週の開始は
レポート・ダッシュボード用途
1つの画面で異なる週の定義を並列表示:ruby# 「今週 (Mon–Sun)」か? date.this_week?(:monday) # 「今週 (Sun–Sat)」か? date.this_week?(:sunday)
- 影響範囲・注意点
後方互換性
- 引数なし呼び出し (
this_week?) の挙動は変わりません。 - 既存コードに対して破壊的な変更はありません。
- 引数なし呼び出し (
並行実行環境での安全性向上
- これまで「リクエストごとに
Date.beginning_of_week=を変更する」ような実装は、マルチスレッド環境で競合するリスクがありました。 - 今後は
this_week?(start_day)を使うことで、グローバル状態を書き換えずに要件を満たせます。
- これまで「リクエストごとに
設計上の一貫性
- 日付計算系のメソッド群で
start_dayを引数で受けられるようになり、API利用時の混乱が減ります。 - 新しくコードを書く場合は、
all_week/beginning_of_week/end_of_week/this_week?を同じ基準で揃えて使うことが推奨されます。
- 日付計算系のメソッド群で
- 参考情報 (あれば)
Rails フォーラムでの議論:
https://discuss.rubyonrails.org/t/proposal-add-start-day-argument-to-this-week-for-consistency-with-all-week-and-beginning-of-week/90168関連メソッド(ActiveSupport::CoreExtensions::DateAndTime::Calculations)
all_week(start_day = Date.beginning_of_week)beginning_of_week(start_day = Date.beginning_of_week)end_of_week(start_day = Date.beginning_of_week)
#57349 Add default #render_in implementation to ActiveModel::Conversion
マージ日: 2026/5/12 | 作成者: @seanpdoyle
- 概要 (1-2文で)
ActiveModel::Conversion にデフォルトの#render_in実装が追加され、Active Model / Active Record オブジェクトを Action View がどのように HTML / JSON にレンダリングするかをモデル側でカスタマイズしやすくなりました。
既存の「partial + object」ベースのレンダリング挙動は保ったまま、render model/render renderable: modelをフックできる統一的な入口が用意されています。
- 変更内容の詳細
2-1. ActiveModel::Conversion#render_in のデフォルト実装追加
ActiveModel::Conversion モジュールに、デフォルトの #render_in が追加されました。
(実際のコードは概ね以下のようなイメージです)
module ActiveModel
module Conversion
# ...
# Action View から:
# render user
# render renderable: user
# などと呼ばれたときに使われる
def render_in(view_context, &block)
view_context.render(partial: to_partial_path, object: self, &block)
end
# ...
end
endポイント:
render_in(view_context)が呼ばれると、partial: to_partial_pathobject: selfを指定して、従来どおりの「パーシャル推論 + オブジェクト割り当て」でレンダリングします。
- つまり、今まで
render @person/render personで使われていたto_partial_pathベースの仕組みを、render_in経由に抽象化した形です。 ActiveModel::Conversionを include しているクラス(Active Record モデルなど)は、 そのままこのデフォルト実装を利用できます。
2-2. ビュー側での利用例
PR説明にある通り、以下のようなパーシャルがあったとします:
<%# app/views/people/_person.html.erb %>
<%= local_assigns[:shout] ? person.name.upcase : person.name %>Person モデルが ActiveModel::Conversion を含んでいれば、従来どおり次のようにレンダリングできます:
person = Person.new(name: "Ralph")
render person # => "Ralph"
render person, shout: true # => "RALPH"
render renderable: person # => "Ralph"
render renderable: person, locals: { shout: true } # => "RALPH"ここで、render person や render renderable: person が最終的に person.render_in(view_context) を呼び出し、
その中で partial: person.to_partial_path, object: person を指定した view_context.render が行われます。
2-3. カスタマイズ例(ViewComponent との連携など)
この PR の狙いは、「モデル側が render_in をオーバーライドすることで、ビュー側での描画方法を差し替えられるようにする」ことです。
ViewComponent を例にすると、モデルにこんな実装が可能になります:
class Person < ApplicationRecord
include ActiveModel::Conversion
def render_in(view_context, &block)
# Person を表示するためのコンポーネントに委譲する
PersonComponent.new(person: self).render_in(view_context, &block)
end
endこれで、コントローラやビューからは単に:
render @person
# または
render renderable: @personと呼ぶだけで、内部的には ViewComponent によるレンダリングが行われるようになります。
HTML だけでなく、JSON などの別フォーマットに対するレンダリングロジックも、この render_in を起点に制御しやすくなります。
2-4. Lint / ガイド / テストの更新
activemodel/lib/active_model/lint.rb- Lint モジュールに
#render_inの存在チェックが追加されています。 - Active Model 準拠オブジェクトが
render_inを実装していることが、API 契約の一部として明確になりました。
- Lint モジュールに
guides/source/active_model_basics.md- ActiveModel Basics ガイドに
render_in追加と、その使い方の説明が追記されました。
- ActiveModel Basics ガイドに
actionview/activemodelのテストrenderの挙動がrender_inと連携すること、- ログ出力 / 部分テンプレートレンダリングとの互換性が保たれていること がテストでカバーされています。
- 影響範囲・注意点
- 互換性:
- デフォルトの
render_inは従来のrender partial: to_partial_path, object: selfと同じ挙動を行うため、既存のrender @model/render model:の挙動は基本的に変わりません。
- デフォルトの
- モデル側で
render_inをオーバーライドする場合:- そのモデルを引数にした
renderの挙動は、すべてそのrender_in実装に従います。 - パーシャルレンダリングに依存しているコードがある場合、
render_inの実装でそれ相当の処理(view_context.render partial: ..., object: ...)を維持するか、明示的に互換性を取る必要があります。
- そのモデルを引数にした
- Lint チェック:
- ActiveModel::Lint を使用している場合、新たに
render_inを実装しているかどうかがチェックされるようになります。 ActiveModel::Conversionを include していればデフォルト実装が付与されるため、通常は追加作業不要ですが、 独自に ActiveModel インターフェイスを定義しているクラスや Proxy オブジェクトでは要確認です。
- ActiveModel::Lint を使用している場合、新たに
- ライブラリ側の活用:
- ViewComponent のように、オブジェクトレンダリングを差し替えたいライブラリは、 モデルやラッパークラスに
render_inを実装させることで、Action View のrenderAPI と自然に統合できるようになります。
- ViewComponent のように、オブジェクトレンダリングを差し替えたいライブラリは、 モデルやラッパークラスに
- 参考情報 (あれば)
- この PR のフォローアップ元:
- #50623 —
renderにおけるrenderable:オプションなど、オブジェクトレンダリングの拡張 - #46202 — モデルレンダリングのインターフェイス設計に関する議論
- #50623 —
- 関連ドキュメント:
- Rails Guides: Active Model Basics(
render_inに関する新セクションが追加)
- Rails Guides: Active Model Basics(
- 関連ライブラリ例:
- view_component: https://github.com/ViewComponent/view_component
→ この PR によって、モデル → コンポーネントの委譲が公式にサポートしやすくなります。
- view_component: https://github.com/ViewComponent/view_component
#57356 Fix ActionText::Editor::Tag#render_in to accept kwargs
マージ日: 2026/5/12 | 作成者: @flavorjones
- 概要 (1-2文で)
Action Text のActionText::Editor::Tag#render_inが、Action View 側の新しい呼び出し規約(kwargs 付き)に対応していなかったバグを修正した PRです。これにより、独自エディタアダプタのサブクラスがsuperを呼んだときにArgumentErrorが発生する問題が解消されます。
- 変更内容の詳細(あればサンプルコードも含めて)
背景: Action View の render_in 呼び出し仕様の変更
コミット d24d0b71(PR #50623)以降、Action View は「#render_in を持つオブジェクト」を以下のように呼び出すようになりました:
renderable.render_in(context, locals: locals, &@block)render_inの arity が 1 でない場合、この形で呼び出される- つまり、
render_inはrender_in(view_context, locals: ..., &block)のようにキーワード引数を受け取る形にしておく必要がある
問題点: ActionText::Editor::Tag#render_in のシグネチャが古い
既存の ActionText::Editor::Tag#render_in は以下のように「第1引数だけ」を取る形になっていました(概念的に):
def render_in(view_context)
# ...
endAction View 側は
tag.render_in(view_context, locals: locals)のように第2引数以降を渡すため、サブクラスがドキュメント通りに
def render_in(view_context, ...)
# カスタム処理
super
endと書いていると、super 呼び出し時に
ArgumentError: wrong number of arguments (given 2, expected 1)が発生していました。
Rails に同梱されている TrixEditor::Tag は super を呼ばずに本体を再実装しているため、この問題に当たっていなかった一方、Lexxy など外部のアダプタで問題が顕在化しました。
この PR の修正内容
ActionText::Editor::Tag#render_in のメソッドシグネチャを「可変長引数を受け取る形」に広げました:
# 変更前(イメージ)
def render_in(view_context)
# ...
end
# 変更後(イメージ)
def render_in(view_context, ...)
# 既存の実装は基本的にそのまま
endポイント:
...(Ruby 2.7+ の引数フォワーディング構文)を使うことで、- 位置引数
- キーワード引数(
locals:など) - ブロック引数
をそのまま受け取れる
これにより、サブクラス側はこれまで通り
rubydef render_in(view_context, ...) # 何か前処理 super endのように書いても、Action View から渡された
locals:などを壊さずにsuperに引き渡せる
テストの追加
actiontext/test/unit/editor/tag_test.rb にテストが追加されています(+19 行)。
テストで確認していることのイメージ:
ActionText::Editor::Tagを継承したクラスでrender_in(view_context, ...)を実装し、superを呼ぶ- そのサブクラスを Action View 経由で
renderしたときに- 例外(
ArgumentError)が発生しない - 期待する HTML/出力が得られる
- 例外(
- 影響範囲・注意点
- 影響を受けるのは Action Text のエディタアダプタ (
ActionText::Editor::Tagのサブクラス) のみ- 特に、Rails のガイドや既存のパターンに従って
render_in(view_context, ...)を定義しsuperを呼んでいる実装
- 特に、Rails のガイドや既存のパターンに従って
- この PR による挙動の後方互換性
- 既存の
render_in(view_context)実装は、そのままでも動作します - ベースクラス側が
...を受け取れるようになっただけなので、既存アダプタのシグネチャを変更する必要はありません
- 既存の
- Action View との整合性が取れたことで、今後のバージョンでも安全
- Action View が
render_in(context, locals: locals, &block)で呼ぶ前提と整合したため、今後このインターフェイス前提でアダプタを実装しても安全です
- Action View が
運用面での注意:
- Rails をアップデートした際に、Action Text のカスタムエディタアダプタで
ArgumentError: wrong number of argumentsが出ていた場合、この PR の入ったバージョンに上げることで解消する可能性が高いです - 逆に、
render_inのシグネチャを独自にいじっている場合(例: 明示的にlocals:を定義しているなど)は、super呼び出しで引数を正しくフォワードしているか確認する価値があります
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57356
- 背景となった Action View の変更: https://github.com/rails/rails/pull/50623
- 問題を発見した実プロジェクト(Lexxy)のアダプタ実装:
#57342 Disable freezing of NATIVE_DATABASE_TYPES for SQLite3 adapter
マージ日: 2026/5/12 | 作成者: @tahsin352
- 概要 (1-2文で)
SQLite3 アダプタのNATIVE_DATABASE_TYPESをfreezeしないようにし、外部の gem などがこのハッシュを書き換えてカスタム型を登録できるように戻した PR です。
MySQL アダプタで行われた同種の対応 (#57333) を SQLite3 版でも行うフォローアップです。
- 変更内容の詳細
何をしたか
activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb内でNATIVE_DATABASE_TYPESが.freezeされていたのをやめた(もしくは freeze していた行を削除/変更)。- これにより、実行時に他のライブラリが
NATIVE_DATABASE_TYPESハッシュを変更できるようになります。
- これにより、実行時に他のライブラリが
その挙動を保証するテストを追加:
activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rbに「NATIVE_DATABASE_TYPESを変更できること」を確認するテストが追加されています。
背景・モチベーション
NATIVE_DATABASE_TYPESは Active Record アダプタにおける「型定義の拡張ポイント」として昔から使われており、- 例:
neighborやactiverecord-enhancedsqlite3-adapterのような gem が、vector型など独自型を登録するために、このハッシュを直接変更しています。
- 例:
- 一方で、最近の変更でこのハッシュに
.freezeが入った結果、rubyといったエラーが「ロード時(load time)」に発生し、これらの gem がクラッシュするようになりました。ActiveRecord::ConnectionAdapters::SQLite3Adapter::NATIVE_DATABASE_TYPES[:vector] = { name: "vector" } # => FrozenError: can't modify frozen Hash - PostgreSQL アダプタのドキュメントにも「この定数を編集してカスタム型を追加できる」とあり、 この振る舞いは公式に想定された拡張ポイントである、という前提に立っています:
- すでに MySQL アダプタでは同様の理由で
.freezeを外す対応(#57333)が入っており、 それに合わせて SQLite3 アダプタでも同じ方針に揃えた、という位置づけです。
(想定されるコード例)
この PR によって、以下のようなコードが再び正常に動くようになります:
# config/initializers/sqlite_types.rb など
ActiveRecord::ConnectionAdapters::SQLite3Adapter::NATIVE_DATABASE_TYPES[:vector] = {
name: "vector",
limit: 1536,
}freeze されているとこの書き換えが FrozenError になりますが、PR 後は問題なく通ります。
- 影響範囲・注意点
- 影響を受けるのは SQLite3 アダプタを使っているアプリケーションと、その上で動く拡張 gem です。
NATIVE_DATABASE_TYPESを書き換える系の gem(neighbor,activerecord-enhancedsqlite3-adapterなど)は、これによりエラーが解消されるはずです。
- セマンティクスの変更点:
- 「
NATIVE_DATABASE_TYPESは immutable な定数ハッシュ」という前提は成り立たなくなり、
他のコードがこれを変更しうる という前提で扱う必要があります。 - これは以前の Rails の挙動(freeze 前)への“ロールバック”に近いため、
「古くからの慣習に戻る」という意味では後方互換的な変更です。
- 「
- パフォーマンス面:
- freeze による微小な最適化(変更検知や GC の観点)は失われますが、通常のアプリケーションで体感できるレベルの影響はほぼないと考えられます。
- セキュリティ/予期せぬ変更のリスク:
- 任意の初期化コードや gem が
NATIVE_DATABASE_TYPESを上書きできるため、 誤った変更により型マッピングが壊れる可能性はあります。 - ただし、これは freeze 前の世界と同様のリスクであり、新たに増えたものではありません。
- 任意の初期化コードや gem が
- 参考情報 (あれば)
- この PR の直接のフォローアップ元:
- MySQL アダプタで同様の変更を行った PR:
- PostgreSQL アダプタのドキュメント(
NATIVE_DATABASE_TYPESを拡張ポイントと見なしている記述):
#55978 Enable config.asset_host to read from environment by default
マージ日: 2026/5/12 | 作成者: @stevepolitodesign
- 概要 (1-2文で)
Rails のconfig.asset_hostがデフォルトでENV["CDN_HOST"]を参照するようになり、環境変数を設定するだけで CDN 経由で静的アセットを配信できるようになりました。ENV["CDN_HOST"]が未設定の場合はこれまで通りasset_host = nilのままで、挙動は変わりません。
- 変更内容の詳細
何が変わったか
config/environments/production.rb のデフォルト設定(およびテスト用ダミーアプリ)が、以下のような形になりました。
# 旧来のイメージ(例)
# config.asset_host = "https://assets.example.com"
# 今回の変更後(テンプレート)
config.asset_host = ENV["CDN_HOST"]railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt- 新規に生成される Rails アプリの
production.rbテンプレートがconfig.asset_host = ENV["CDN_HOST"]という形に変更。
- 新規に生成される Rails アプリの
actionmailbox/test/dummy/.../production.rbactiontext/test/dummy/.../production.rbactivestorage/test/dummy/.../production.rb- これらテスト用ダミーアプリの production 設定も同様に
ENV["CDN_HOST"]を参照するよう更新。
- これらテスト用ダミーアプリの production 設定も同様に
railties/CHANGELOG.md- 上記の仕様追加が CHANGELOG に追記されています。
挙動の詳細
ENV["CDN_HOST"]が「設定されていない」場合- Ruby では
ENV["CDN_HOST"] #=> nilとなるため、config.asset_host = nilと同等になります。 - asset_host 無しの従来挙動と変わらない(=相対パスでアセットを配信)。
- Ruby では
ENV["CDN_HOST"]が「設定されている」場合- 例:
CDN_HOST=https://cdn.example.com
→ Rails 側でconfig.asset_host = "https://cdn.example.com"が自動的にセットされる。 - 明示的に
config.asset_hostを書き換えなくても、CDN からアセットを配信する設定になる。
- 例:
サンプル: 実際の利用イメージ
1) 本番環境の設定(Rails 側)
config/environments/production.rb は、生成直後から次のようになっている想定です:
Rails.application.configure do
# ...
config.asset_host = ENV["CDN_HOST"]
# ...
end開発者はここを編集せず、そのまま使えます。
2) デプロイ先での環境変数設定
- Heroku / Render / ECS / Kubernetes などで以下を設定:
export CDN_HOST="https://cdn.example.com"もしくは YAML など:
env:
- name: CDN_HOST
value: "https://cdn.example.com"これだけで、Rails のアセット URL が https://cdn.example.com/assets/... のように生成されるようになります。
- 影響範囲・注意点
影響範囲
- 新たに生成される Rails アプリの production 環境設定
- Rails リポジトリ内のダミーアプリ(Action Mailbox / Action Text / Active Storage)の production 設定
- 既に
production.rbをカスタマイズしている既存アプリには即時影響はありません(既存アプリのファイルは勝手に書き換えられないため)。
注意点
CDN_HOST が誤設定されていると、全アセット URL が壊れる
- 例:
CDN_HOST=cdn.example.com(スキーム無し)
→ ブラウザ側の解釈によっては意図しない URL になる可能性があります。 - 一般には
https://cdn.example.comのようにスキームを含めて設定すべきです。
- 例:
config.asset_hostを手動で設定している場合の優先度- 既存アプリで
config.asset_host = "https://old-cdn.example.com"のように明示設定している場合、その値が使われます。 - 今回の PR は新規生成テンプレートのデフォルトを変えたものであり、手動設定を上書きするものではありません。
- 既存アプリで
環境変数ベースの設定ポリシーへの移行
- Rails ガイド(Asset Pipeline ガイド)の例と整合する形で
CDN_HOSTが採用されています。 - 組織で
ASSET_HOSTなど別名を使っている場合は、今まで通りproduction.rbを編集するか、ENV["ASSET_HOST"] || ENV["CDN_HOST"]のように独自に拡張する選択肢もあります。
- Rails ガイド(Asset Pipeline ガイド)の例と整合する形で
- 参考情報 (あれば)
Rails Guides: Asset Pipeline – Set up a CDN to serve static assets
https://guides.rubyonrails.org/asset_pipeline.html#set-up-a-cdn-to-serve-static-assets
(今回のENV["CDN_HOST"]はこのガイド中のサンプルと名前を合わせています)config.asset_hostの公式ドキュメント(Action Pack / Rails Guides)- アセット URL の生成、メールテンプレートでの URL 生成などにも影響するため、CDN 利用時はここを理解しておくと運用しやすくなります。
#57352 Properly reset fixtures cache after non-transactional tests
マージ日: 2026/5/12 | 作成者: @byroot
- 概要 (1-2文で)
このPRは、トランザクションを使わないテスト実行後に、fixtures のキャッシュが正しくリセットされない問題を修正し、ActiveRecord のテストフィクスチャ機構の一貫性を高めたものです。あわせて、この挙動を検証するテストと、リークチェッカー側のサポートを追加しています。
- 変更内容の詳細
※実際の差分は要約ベースですが、挙動としては以下のような変更が行われています。
2-1. ActiveRecord::TestFixtures におけるキャッシュリセットの修正
activerecord/lib/active_record/test_fixtures.rb において、
- トランザクションを使うテスト (
use_transactional_tests = true)- これまでもテストごとにトランザクションのロールバックでDB状態は戻っていたが、
- フィクスチャの内部キャッシュ(ロード済みフィクスチャなど)については、
「トランザクション前提」でしかリセットされていない箇所があった。
- トランザクションを使わないテスト (
use_transactional_tests = false、あるいはself.use_transactional_tests = falseなテストケース)- DBはtruncateや手動cleanupなど別手段でリセットされる一方、
- ActiveRecord のフィクスチャキャッシュ自体は前のテストの状態が残りうる、という不整合があり得た。
このPRでは、上記の不整合を解消するために、
**「非トランザクションテストが終わるときにも、fixtures のキャッシュを確実にリセットする」**ようロジックが修正されています。
イメージとしては、以下のような処理が追加・修正されています(擬似コード):
def after_teardown
super
# 以前は transactional な場合にしか呼ばれていなかった、あるいは条件が不十分だった
ActiveRecord::FixtureSet.reset_cache # ← これを非トランザクションテストでも確実に実行
end実際には FixtureSet まわりや fixtures_cache のクリア処理が適切なフックから呼ばれるように整理されているイメージです。
2-2. regression テストの追加
activerecord/test/cases/fixtures_test.rb にテストが追加・拡張されています(+6/-2)。
テスト内容としては概ね:
- 非トランザクションなテストケースをセットアップし、
- 複数のテストでフィクスチャを読み込ませる、
- テスト間でのフィクスチャキャッシュの状態を確認し、
- PRで導入したリセット処理がないと失敗する(= バグを再現できる)テストを追加
といった「退行テスト (regression test)」になっているはずです。
これにより、今後同様の変更が行われた場合にも、
非トランザクションテストにおけるフィクスチャキャッシュのクリア漏れが CI 上で検知できるようになります。
2-3. tools/support/leak_checker.rb の拡張
tools/support/leak_checker.rb に +2 行の変更が入っています。
これは Rails の internal テストツールで、
オブジェクトやグローバル状態の「リーク」を検知するためのサポートコードです。
ここでは、
- フィクスチャのキャッシュ(おそらく
ActiveRecord::FixtureSet周辺)に関するリーク、 - もしくはテスト後にクリーンアップされるべき内部状態
をリークチェッカーの監視対象に追加するための1〜2行の設定が加えられています。
これによって、fixtures キャッシュがリセットされないような退行が起きた場合、
リークチェッカーが失敗することで問題を早期に検知できるようになります。
- 影響範囲・注意点
3-1. 影響範囲
- 対象:
ActiveRecord::TestFixturesを利用している Rails のテスト環境全般- 特に
use_transactional_tests = falseを利用しているテストスイート - RSpec などから
ActiveSupport::TestCaseベースのフィクスチャを利用している場合も間接的に影響
- 特に
- 主な影響:
- テスト間でフィクスチャキャッシュが持ち越されないようになるため、
- これまで「たまたま」前のテストのキャッシュに依存していたケースがあれば、挙動が変わる可能性はあります。
- ただし本来の仕様からすると、今回の修正が正しい挙動であり、依存していたコードの方が脆いと考えられます。
3-2. 開発者が意識すべき点
- もしプロジェクトで
use_transactional_tests = falseを明示的に使っていて、- フィクスチャの読み込みが「一回だけ」であることに暗黙依存している処理があれば挙動が変わるかもしれません。
- しかし、通常の Rails の使い方では、
- 「テストケースごとにフィクスチャ状態はクリーンである」ことが期待されるため、
- このPRによってより予測可能で一貫性のあるテスト挙動になります。
- また、CI や開発環境で:
- リークチェッカーが有効な場合、フィクスチャキャッシュのクリア漏れによる失敗が検出されやすくなります。
- これは基本的にはメリットであり、特別な対応は不要です。
- 参考情報 (あれば)
- 抽出元PR: https://github.com/rails/rails/pull/57326
- このPRは、より大きな改善の一部を切り出したものと思われます。
- 関連コード:
activerecord/lib/active_record/test_fixtures.rbActiveRecord::TestFixturesモジュール: fixtures のロードとキャッシュ、トランザクションテストまわりのロジック
ActiveRecord::FixtureSet(内部的に利用)reset_cache/all_cached_fixtures等のメソッドがキャッシュ管理を担当
- 類似の文脈:
- DB Cleaner や並列テストなど、トランザクションに依存しないテスト戦略と fixtures を併用しているプロジェクトでは、この種のキャッシュ挙動がバグの温床になりやすく、今回の修正はそうした問題の一つを潰す意図があります。
#36433 Fix unexpected behavior for dependent: :purge
マージ日: 2026/5/11 | 作成者: @moveson
- 概要 (1-2文で)
has_one_attached/has_many_attachedにdependent: :purgeやdependent: :purge_laterを指定した場合、親レコード削除時に添付ファイルが「detach(関連のみ削除)」されてしまっていた不具合を修正し、「実際に purge(blob も含め削除)」されるようにした PR です。これによりdependent: :detach/:purge/:purge_laterがそれぞれ意図どおりに動作するようになります。
- 変更内容の詳細
何が問題だったか
Active Storage の関連定義:
class User < ApplicationRecord
has_one_attached :photo, dependent: :purge
endこのように dependent: :purge を指定していても、user.destroy を呼んだときの挙動は「purge ではなく detach」になっていました。
期待される挙動:
dependent: :detach
→ attachment レコードと親の紐付けのみ削除(blob 本体は残る)dependent: :purge
→ attachment レコード+blob レコード+ストレージ上のファイルをまとめて削除dependent: :purge_later
→ 上記 purge を非同期ジョブで実行
実際の挙動(修正前):
dependent: :purge/:purge_laterでも、親 destroy 時は detach 相当の処理になっていた
つまり dependent: オプションで指定した値が destroy 連鎖時にうまく反映されていない、というバグです。
PR での主な変更点
1) ActiveStorage::Attachment の dependent 動作の修正
activestorage/app/models/active_storage/attachment.rb が修正され、dependent の指定に従って正しくメソッドが呼ばれるようになっています。
ざっくりいうと、以下のようなマッピングがきちんと効くようになった形です(実際のコードはもう少し抽象化されています):
# 擬似的なイメージ
case dependent
when :detach
attachment.destroy
when :purge
attachment.purge
when :purge_later
attachment.purge_later
end従来は destroy のフック内で destroy 相当しか呼ばれておらず、dependent が無視される経路があったため、ここが修正対象になっています。
2) モデル側の attached 定義の修正
activestorage/lib/active_storage/attached/model.rb もわずかに変更されています。
ここは has_one_attached / has_many_attached 定義時に dependent: オプションを受け取り、ActiveStorage::Attachment の関連に適切な dependent 動作を委譲する部分です。
今回の修正によって:
class User < ApplicationRecord
has_one_attached :photo, dependent: :purge
has_many_attached :images, dependent: :purge_later
endのような設定が、destroy 連鎖時にちゃんと purge / purge_later で処理されるようになっています。
3) テストの追加・強化
以下のテストが追加・拡張されています。
activestorage/test/models/attached/one_test.rbhas_one_attached+dependent: :purge/:purge_laterが destroy 時に正しく動くかのテスト追加
activestorage/test/models/attached/many_test.rbhas_many_attached版の同様のテスト追加
activestorage/test/models/reflection_test.rb- Active Storage の関連リフレクションが
dependent:を正しく解釈しているかのテストを大幅追加
- Active Storage の関連リフレクションが
activestorage/test/test_helper.rb- テストヘルパーに purge / purge_later 検証のための補助が足された可能性が高いです
これにより、dependent: :detach / :purge / :purge_later のそれぞれがリグレッションテストでカバーされるようになっています。
4) CHANGELOG の更新
activestorage/CHANGELOG.mdに「dependent: :purgeの挙動修正」が明記され、挙動の変更がドキュメント化されています。
- 影響範囲・注意点
挙動が「仕様どおり」に変わる点に注意
これまで誤って「detach 相当」になっていた部分が、今後は「purge / purge_later 相当」で動作するようになります。
具体的には:
class User < ApplicationRecord
has_one_attached :photo, dependent: :purge
has_many_attached :images, dependent: :purge_later
endで user.destroy を呼んだとき:
- 修正前:
- attachment レコードと User の紐付けだけが消え、blob や実ファイルは残る(detach 相当)
- 修正後:
photoは即時削除(purge)imagesは Active Job 経由で非同期に削除(purge_later)
そのため、「これまで誤った挙動に依存していたコード」があると、以下のような影響が出ます。
- 親レコード削除後もファイルを残して再利用していたケース
- 「消えないと思っていた」ファイルが実際には削除されるようになる
- 削除ジョブ(purge_later)が増えることによるジョブキューの負荷増大
既存アプリでの確認ポイント
has_one_attached/has_many_attachedでdependent: :purge/:purge_laterを既に使っている場合:- その添付ファイルは「親 destroy 時に完全削除される」想定で OK かを再確認する
- もし「detach したいだけ」なら
dependent: :detachに変更する
- Storage(S3, GCS, ローカルなど)からの物理削除が増えるので、誤削除がないかステージング環境で挙動確認する
purge_laterを多用している場合:- ActiveJob / キューワーカーのスループットとジョブ量をモニタリングする
- 参考情報 (あれば)
- PR 本体:
https://github.com/rails/rails/pull/36433 - 関連 Issue(PR 説明中で言及): Fixes #36423
→dependent: :purgeが detach になってしまうバグ報告 - Active Storage ガイド(公式ドキュメント、英語):
https://guides.rubyonrails.org/active_storage_overview.html- 「Removing files」の章に purge / detach の違いが説明されています。
#50623 Pass render options and block to calls to #render_in
マージ日: 2026/5/11 | 作成者: @seanpdoyle
- 概要 (1-2文で)
renderにオブジェクトを渡したときに呼ばれる#render_inに対して、呼び出し元のオプション(locals など)とブロックを正しく引き渡せるようにした変更です。既存のrender_in(view_context)というシグネチャは非推奨となり、今後はオプションとブロックを受け取る形への移行が求められます。
- 変更内容の詳細
何が問題だったか
- Rails では、
render some_objectのように「#render_inを持つオブジェクト」を描画できる仕組みが既にありました(ViewComponent などが利用)。 - しかし従来の実装は、呼び出し側で指定したオプション(例:
locals: { ... }やname: "...")やブロックを#render_inに渡していませんでした。 - そのため、
render obj, name: "Local"やrender(obj) { ... }と書いても、obj#render_in側からはそれらにアクセスできず、「自分自身のコンテキストだけで完結して描画する」前提になっていました。
今回の変更のポイント
renderが#render_inを呼ぶ際に、キーワード引数・ブロックを渡すように拡張- これにより、
render呼び出し時の オプション(locals 相当)とブロックを、#render_in側で受け取って活用 できるようになりました。
例(PR に記載のサンプル):
rubyclass Greeting def render_in(view_context, **options, &block) if block view_context.render html: block.call else view_context.render inline: <<~ERB.strip, **options Hello, <%= name %> ERB end end end render(Greeting.new) # => "Hello, World" render(Greeting.new, name: "Local") # => "Hello, Local" render(Greeting.new) { "Hello, Block" } # => "Hello, Block"render(Greeting.new, name: "Local")のname: "Local"がrender_inに渡され、inlineテンプレートでnameとして参照可能になります。- ブロック付き
render(Greeting.new) { ... }のブロックも&blockとしてrender_inに届きます。
- これにより、
render_in(view_context)シグネチャの非推奨化既存の多くのライブラリ(例: ViewComponent など)は以下のようなシグネチャで実装していました:
rubydef render_in(view_context) # ... end今回の変更では、オプションとブロックを受け取れるシグネチャへの移行を推奨し、それに伴い従来の「引数は
view_contextのみ」の形を deprecate しました。推奨される形は、少なくとも可変キーワード引数とブロックを受け取れるようにすることです:
rubydef render_in(view_context, **options, &block) # options と block を使う/無視するのは自由 endこれにより、将来的に
renderから渡される情報を失わずに受けられるようになります。
Rails 内部のレンダリング処理全体の対応
actionpack/actionview両方のレンダラ関連クラスが修正され、renderableオブジェクトを扱う箇所で- オプション引き回し
- ブロックの受け渡し を行うように整理されています。
- コントローラ側 (
ActionController::Metal#renderなど) とビュー側 (ActionView::Rendererなど) の両レイヤでテストが追加され、「コントローラからの render / ビュー内からの render」の両方で挙動が保証されています。 - ガイド (layouts_and_rendering.md) と 8.1 のリリースノートにも、
render_inを持つオブジェクトの使い方とこの挙動が追記されています。
- 影響範囲・注意点
影響があるコード
#render_in(view_context)を実装しているクラス全般
例:- 独自の「レンダラブル」オブジェクト
- ViewComponent や類似のコンポーネントライブラリ
- テンプレート以外のオブジェクトを
renderしている gem / アプリのコード
注意点・移行ガイド
render_inのシグネチャを広げる既存:
rubydef render_in(view_context) # ... end推奨:
rubydef render_in(view_context, **options, &block) # options や block を使わない場合でも、 # 受け取れるようにはしておくと将来互換性が高い。 end- こうしておくことで、
render(obj, foo: :bar)やrender(obj) { ... }のfoo:やブロックが消えずに届きます。 - シグネチャを広げるだけなら後方互換は保たれる(既存の呼び出しでも動く)ため、安全な変更です。
- こうしておくことで、
options/blockを使いたい場合の実装例locals 的に扱う:
rubydef render_in(view_context, **options, &block) view_context.render inline: <<~ERB, locals: options Hello, <%= name %> ERB end # 呼び出し側 render(Greeting.new, name: "Rails") # => "Hello, Rails"ブロックを部分テンプレートの中で評価したい:
rubydef render_in(view_context, **options, &block) if block # capture 等でラップしてもよい content = view_context.capture(&block) view_context.render inline: "<div>#{content}</div>" else view_context.render inline: "<div>No block</div>" end end
ディプリケーション警告への対応
- Rails 8.1 以降で、
render_in(view_context)だけを実装しているクラスは 非推奨警告が出る可能性があります(実際の警告メッセージ内容はガイド・CHANGELOG を参照)。 - ライブラリ作者は、この変更に追随しておかないと将来的な Rails のメジャーアップデート時に動かなくなるリスクがあります。
- Rails 8.1 以降で、
テストの更新
- 自作の
render_in実装を持つコンポーネントをテストしている場合、render(component, foo: :bar)のようなテストrender(component) { "block content" }のようなテスト を追加すると、今回の新仕様にきちんと乗れているかを検証できます。
- 自作の
- 参考情報 (あれば)
- 元 issue / PR:
- ドキュメント:
- guides/source/layouts_and_rendering.md
→renderにオブジェクトを渡すパターンとrender_inの説明が更新 - guides/source/8_1_release_notes.md
→ 本変更のハイライトが簡潔に記載
- guides/source/layouts_and_rendering.md
- 実装の詳細を追いたい場合:
actionview/lib/action_view/template/renderable.rbactionview/lib/action_view/renderer/renderer.rbactionpack/lib/action_controller/metal/rendering.rb
あたりを見ると、renderable経由の処理フローとオプション/ブロックの引き回しが確認できます。
#57346 Rewrite Arel tests using ActiveSupport::TestCase instead of Minitest::Spec
マージ日: 2026/5/11 | 作成者: @rafaelfranca
- 概要 (1-2文で)
Rails の Arel 周辺テストを、Minitest::SpecスタイルからActiveSupport::TestCaseベースのクラシックな xUnit スタイル (test "..." do) に全面書き換えした PRです。これにより megatest などで spec 構文をサポートする必要がなくなり、テスト基盤の統一とシンプル化が図られています。
- 変更内容の詳細
主眼: Minitest::Spec → ActiveSupport::TestCase への書き換え
この PR のほぼすべては「テスト記述スタイルの移行」です。Arel の各種テストが、以下のような spec スタイルから:
# 旧: Minitest::Spec スタイルの例
describe Arel::Nodes::And do
it "joins two predicates with AND" do
node = Arel::Nodes::And.new([left, right])
node.to_sql.must_equal "(foo = 1 AND bar = 2)"
end
endActiveSupport::TestCase を継承するクラシックなテストクラス・スタイルへ変換されています:
# 新: ActiveSupport::TestCase スタイルの例
class ArelNodesAndTest < ActiveSupport::TestCase
def test_joins_two_predicates_with_and
node = Arel::Nodes::And.new([left, right])
assert_equal "(foo = 1 AND bar = 2)", node.to_sql
end
end代表的な変換パターン:
describe Foo do→class FooTest < ActiveSupport::TestCaseit "does something"→def test_does_somethingbefore do→def setupafter do→def teardown- 期待値:
must_equal→assert_equalwont_equal→refute_equalmust_be_nil→assert_nilmust_be :>, 1→assert_operator obj, :>, 1- など、Minitest::Spec 用マッチャを xUnit 形式のアサーションに置き換え
ファイル単位では、以下のような Arel テストが対象になっています (抜粋):
activerecord/test/cases/arel/attributes/attribute_test.rbactiverecord/test/cases/arel/attributes/math_test.rbactiverecord/test/cases/arel/collectors/*_test.rbactiverecord/test/cases/arel/crud_test.rbactiverecord/test/cases/arel/delete_manager_test.rbactiverecord/test/cases/arel/insert_manager_test.rbactiverecord/test/cases/arel/nodes/*_test.rb- 他 Arel 関連 50 ファイル超
+4195 / -4705 という大きな差分ですが、ほぼ「構文変換」「アサーション API の置き換え」であり、テストの意図やカバレッジそのものは極力維持されています。
Arel テストヘルパの調整
activerecord/test/cases/arel/helper.rb も修正されています。ここでは:
Minitest::Spec前提だった require / include / DSL の削除・整理ActiveSupport::TestCaseから継承するためのヘルパーメソッドの位置・モジュール化調整
などが行われていると考えられます。これにより、Arel 用のテストクラスがすべて同じテスト基盤 (ActiveSupport::TestCase) 上に乗るように統一されています。
本体コード (プロダクションコード) への軽微な変更
activerecord/lib/arel/nodes/casted.rb にも +2 行の追加があり、ここだけは本体コードが触られています。ただしテストリライトに付随する「より正確なテストのための public API 補助」や「警告回避」「互換性向上」のような、小さな調整である可能性が高いです。
例としてありそうな変更イメージ:
inspect/to_sの振る舞いをテストしやすくする- 属性のデフォルト値や型情報を一貫させる
- Ruby のバージョン差異対応
いずれにせよ、PR の主眼はテスト移行であり、このファイルの変更はそれを支える補助的なものです。
- 影響範囲・注意点
影響範囲
- 外部 API / 振る舞い:
- Rails / Active Record / Arel の公開 API やクエリ生成ロジックには原則として変更なし。
- ユーザーコードへの直接的な破壊的変更は想定されません。
- テスト基盤:
- Rails コアの Arel テストは
Minitest::Specに依存しなくなり、ActiveSupport::TestCaseに統一されます。 - megatest (巨大テストスイートの統合ランナー的なもの) は spec DSL をサポートしなくてよくなり、メンテナンスが簡素化されます。
- Rails コアの Arel テストは
注意点 (Rails コントリビュータ / メンテナ向け)
- Arel 周辺で新たにテストを書く/直す時は:
describe/it/must_*などの spec 記法は使用せず、class SomeFeatureTest < ActiveSupport::TestCase+def test_...+assert_*を使う必要があります。
- 既存の Arel テストを参照する際は、「クラス名ベース」「メソッド名ベース」の xUnit スタイルになります。
- テストのグルーピングを
describeのネストに頼っていた場合、その構造がクラス/メソッドへフラットに実装されているので読み替えが必要です。
- テストのグルーピングを
- 外部で Rails テストスタイルを真似ている社内コード / gem 等がある場合:
- 「Rails 本体が spec スタイルを使っているから自分たちも」という前提は崩れるので、今後は
ActiveSupport::TestCaseを基準にするのが無難です。
- 「Rails 本体が spec スタイルを使っているから自分たちも」という前提は崩れるので、今後は
リグレッションリスク
- 「テストスタイルのみの変更」とはいえ、大規模な置き換えでテストのロジックが変わっている可能性には注意が必要です。
- アサーションの引数順 (expected / actual) の入れ替えミス
before→setup変換時の shared state の取り扱いミス- spec DSL 特有のコンテキスト共有パターンがうまく xUnit に写せていない箇所
- Rails 本体の CI が通ってからマージされているため、大きな不具合は出尽くしていると考えられますが、Arel の挙動を細かく利用しているような高度なユースケースでは、問題切り分けの際に「テストの意図」を再確認する場面が出るかもしれません。
- 参考情報 (あれば)
- この PR は
Related to #57326とされており、#57326 側で:- megatest やテスト実行環境の整理
- Minitest::Spec 依存の削減・削除 といった、より広いテスト基盤の方針が議論されていると推測されます。
- Rails の推奨テストスタイル:
- コアは一貫して
ActiveSupport::TestCaseベースに寄せる方向にあり、RSpec や Minitest::Spec スタイルは「外部で自由に使うもの」という整理になりつつあります。
- コアは一貫して
- 実務上、
- Arel の挙動をカスタムで拡張している場合でも、この PR が直接 API を壊すことはほぼなく、通常はアップデート時の追加作業は不要です。
- コントリビュートする際だけ、新しいテストスタイルに合わせれば十分です。
#57347 Document Active Job continuation instrumentation events
マージ日: 2026/5/11 | 作成者: @bdewater-thatch
- 概要 (1-2文で)
Active Job の「ジョブ継続 (continuation)」機能で発火する Active Support Instrumentation のイベントについて、公式ガイド(active_support_instrumentation.md)にドキュメントを追加した PR です。機能自体の実装変更はなく、「どんなイベントがいつ、どういうペイロードで通知されるか」を明文化しています。
- 変更内容の詳細
※ このPRは guides/source/active_support_instrumentation.md のドキュメント追記のみです。コードの挙動は変わっていません。
追加された内容のポイント
- Active Job の「Continuation」関連で発火するイベントが、新たなセクションとして Active Support Instrumentation ガイドに追加されています。
- 具体的には、以下のようなイベントが対象になっていると考えられます(#55127のフォローアップであり、「Continuation」によるチェーン実行をトレースするためのもの):
enqueue_continuation.active_jobperform_start_continuation.active_job/perform_continuation.active_job相当のイベント- それぞれのイベントで受け取れる payload の key(例:
job,adapter,queue,enqueued_at,continuation_tokenなど)がガイド内で説明されている構成です。
ガイドでは一般的に、Instrumentation イベントごとに以下の形式で説明されますが、今回の追加もこれに合わせています:
- イベント名 (例:
enqueue_continuation.active_job) - 発火タイミング
- Continuation を伴うジョブがキューに積まれるとき
- 前のジョブ完了後に次のジョブとして実行がスケジュールされるとき など
- payload の内容
job: 実行されるジョブオブジェクトjob_id: ジョブIDqueue: キュー名adapter: 使用されるキューアダプタ- Continuation 特有の識別情報(トークンや元となるジョブ情報) など
- 購読例(サンプルコード)
サンプルコード(イメージ・ガイドに沿った形):
ActiveSupport::Notifications.subscribe("enqueue_continuation.active_job") do |event_name, started, finished, unique_id, payload|
Rails.logger.info(
"[#{event_name}] continuation for job=#{payload[:job].class} " \
"job_id=#{payload[:job_id]} queue=#{payload[:queue]}"
)
endあるいは、ActiveSupport::Notifications と組み合わせてメトリクス用ミドルウェアから利用できる形で説明されています。
- 影響範囲・注意点
アプリの動作には影響なし
コードベースの変更はなく、ドキュメントのみの追加なので、既存アプリケーションの挙動に影響はありません。Observability / Monitoring の精度向上
- Continuation を利用してジョブをチェーン実行しているアプリケーションで、
- 「どのジョブからどのジョブに引き継がれたか」
- 「どのタイミングで enqueue / perform されたか」
を ActiveSupport::Notifications で正しくトレースしやすくなります。
- メトリクス収集ツール(StatsD, Prometheus, OpenTelemetry など)と連携させる際に、正しいイベント名・payload キーを参照できるようになります。
- Continuation を利用してジョブをチェーン実行しているアプリケーションで、
バージョンに注意
PR説明にある通り、「ガイドに表示させるために backport が必要」とコメントされています。- Continuation 機能およびそのイベントが導入された Rails バージョンよりも古いガイドには、この記述が載らない可能性があります。
- 実際に自分の使っている Rails バージョンでこれらのイベントが存在するかどうかは、
ActiveSupport::Notificationsで簡単な subscribe を書いて確認するか、該当バージョンの CHANGELOG / API ドキュメントを確認するのが安全です。
- 参考情報 (あれば)
- 元の機能追加 PR(Continuation 実装・イベント実装本体)
- Active Support Instrumentation ガイド(該当セクションに今回の追記が入る)
- Active Support Notifications API ドキュメント
#57341 Add tests for constants that are expected to be mutated
マージ日: 2026/5/11 | 作成者: @paracycle
- 概要 (1-2文で)
Rails本体で「本来ミューテート(変更)されることが前提の定数」に対して、その期待をテストで明示するPRです。
mutable な定数を一律に freeze した対応 (#57323) のあとに見つかった「実は変更されることが仕様」というケースをテストで保証し、今後のリグレッションを防ぐ目的があります。
- 変更内容の詳細
背景
- 先行PR #57323 で「ミュータブルな定数を freeze する」対応が入りました。
- その結果、以下2件のPRで「freeze してはいけない(利用者が変更する前提の)定数」が判明し、修正されています。
- #57332
- #57333
- しかし、これらの「定数がミューテートされることを許容する」という仕様がテストに明文化されておらず、将来的にまた誤って freeze されるリスクがありました。
このPR #57341 では、その2つのケースについて「ミューテート可能であること」を確認するテストを追加しています。
具体的な変更点
変更ファイルは2つで、いずれもテストの追加のみです(削除はなし、合計16行追加)。
activerecord/test/cases/adapters/abstract_mysql_adapter/sql_types_test.rb(+8)activesupport/test/core_ext/date_ext_test.rb(+8)
※実際のコードはPR本文には載っていませんが、文脈から以下のような内容になっていると考えられます。
1) MySQL アダプタの SQL 型関連の定数に対するテスト
対象: ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter まわりの「SQL型マッピング」を表す定数(例: NATIVE_DATABASE_TYPES など)
テストの意図:
- 「ユーザー(アプリケーション側)がこの定数をミューテートしてカスタマイズする」ことを前提としている箇所に対し、
- テスト内で定数を変更(あるいは
<<,[]=,merge!などの破壊的操作)してもエラー(FrozenError)にならない - かつ、その変更が実際にアダプタの振る舞いに反映される
ことを確認しているはずです。
- テスト内で定数を変更(あるいは
サンプルイメージ(概念的な例):
def test_mysql_native_types_are_mutable
types = ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::NATIVE_DATABASE_TYPES
assert_nothing_raised do
types[:foo] = { name: "foo" }
end
assert_equal "foo", types[:foo][:name]
end※あくまでイメージですが、実際には「#57332 で修正した定数」に対する操作がテストされています。
2) Active Support の Date 拡張の定数に対するテスト
対象: ActiveSupport の Date 拡張 (core_ext/date) 内で定義されている、フォーマットや曜日名などを表す定数(例: DATE_FORMATS 相当のもの)
テストの意図:
- Rails アプリが
Date系のフォーマット用定数を、初期化コードなどで上書き・追加することを正式なユースケースとして想定している。 - そのため、freeze してはいけない定数であることをテストで保証する。
サンプルイメージ(概念的な例):
def test_date_formats_are_mutable
formats = Date::DATE_FORMATS
assert_nothing_raised do
formats[:short] = "%Y-%m"
end
assert_equal "%Y-%m", formats[:short]
end実際には #57333 で修正された対象の定数を操作するテストになっているはずです。
- 影響範囲・注意点
- 実装コードへの変更はなく、テストが増えただけ なので、このPR単体では挙動の変更や互換性への影響はありません。
- ただし、テストが追加されたことで次のような「仕様」が明文化されたと言えます:
- 対象となっている定数は、今後もアプリケーション側からミューテートできることが Rails の前提 である。
- これらの定数を将来 freeze したり、イミュータブルなオブジェクトに置き換えるような変更は、テストが落ちることで検知される。
- アプリケーション開発者の視点では:
- すでに #57332 / #57333 で「誤って freeze されていた問題」は解消済みであり、このPRはその動作をテストでロックしたものです。
- 上記のような定数(MySQL アダプタの型マッピングや Date 拡張のフォーマット設定など)を「カスタマイズポイント」として利用している場合、その利用パターンが今後も正式にサポートされやすくなった と捉えてよいです。
- 参考情報 (あれば)
- 定数 freeze 導入PR:
https://github.com/rails/rails/pull/57323 - ミューテート前提の定数を元に戻した(あるいは対応した)PR:
このPRは、上記2つのPRで復旧・修正された「mutable な定数」をテストで守るためのフォローアップPRという位置付けです。
#57336 Fix broken link in changelog
マージ日: 2026/5/11 | 作成者: @RDIL
概要 (1-2文で)
Rails 4.0 のリリースノート内に記載されていたリンク切れを修正した PR です。コードの挙動には一切影響せず、ドキュメント(ガイド)の品質向上のみを目的としています。変更内容の詳細
- 対象ファイル:
guides/source/4_0_release_notes.md - 変更内容: リリースノート中に含まれていた URL を、現在も有効にアクセスできるリンクへ差し替えています(1行の更新: 1行削除 + 1行追加)。
- 背景:
- 2014年頃に書かれた Rails 4.0 リリースノート中のあるリンクが、その後のサイト構成変更などにより 404 になるなど「リンク切れ」状態になっていた。
- PR 作成者は古いアプリのアップグレード作業中に該当リンク切れに気づき、同様に古いバージョンからのアップグレード情報を参照する開発者が困らないよう、リンクを修正した。
※変更対象は「文章中の URL のみ」であり、サンプルコードや設定例などの技術内容には変更はありません。
- 影響範囲・注意点
- 影響範囲:
- Rails ガイド(特に Rails 4.0 Release Notes)を参照する開発者が対象です。
- アプリケーションコード、API、挙動、互換性には一切影響しません。
- 注意点:
- Rails 4.0 という既に古いバージョンのリリースノートの変更のため、現行バージョンでの挙動確認に直接関係するものではありませんが、レガシーアプリからのアップグレード調査時には、正しいリンク先に飛べるようになります。
- ドキュメント生成(
guidesのビルド)でのみ反映される変更です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57336
- Rails ガイド(貢献ガイドライン):
https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation
#57333 Disable freezing of NATIVE_DATABASE_TYPES for MySQL adapter
マージ日: 2026/5/10 | 作成者: @chaadow
- 概要 (1-2文で)
MySQL アダプタのNATIVE_DATABASE_TYPESを凍結(freeze)しないように変更し、これを前提に上書き・拡張しているコード(例: neighbor gem)がFrozenErrorで落ちないようにした PR です。PostgreSQL アダプタと同様の挙動に揃えるフォローアップ変更でもあります。
- 変更内容の詳細
何をしたか
activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb 内で、MySQL アダプタの NATIVE_DATABASE_TYPES を凍結していたコードをやめました。
イメージとしては以下のような変更です(実際のコードは簡略化):
# 変更前(イメージ)
NATIVE_DATABASE_TYPES = {
primary_key: "bigint auto_increment PRIMARY KEY",
string: { name: "varchar", limit: 255 },
# ...
}.freeze
# 変更後(イメージ)
NATIVE_DATABASE_TYPES = {
primary_key: "bigint auto_increment PRIMARY KEY",
string: { name: "varchar", limit: 255 },
# ...
}
# freeze しない関連 PR (#57323) で行った変更の MySQL 版であり、PostgreSQL アダプタ側の定義と挙動を揃えるための修正です。
なぜ必要か
neighbor gem では、MySQL 用に NATIVE_DATABASE_TYPES を拡張する実装を行っています:
# https://github.com/ankane/neighbor/blob/2978d482d48340ab16477f76406c7f1522427d68/lib/neighbor/mysql.rb#L9
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::NATIVE_DATABASE_TYPES[:vector] = { name: "vector" }
# などのような拡張をしているイメージRails 側で NATIVE_DATABASE_TYPES を .freeze してしまうと、このようなコードが実行されたときに FrozenError が発生します:
can't modify frozen Hash: { primary_key: "bigint auto_increment PRIMARY KEY", ... } (FrozenError)この PR により、MySQL アダプタでも PostgreSQL と同様に NATIVE_DATABASE_TYPES を後から拡張可能な状態に戻し、既存の拡張コード(neighbor ほか)がエラーなく動作するようになります。
- 影響範囲・注意点
影響を受けるのは主に MySQL / MariaDB を使うアプリケーション
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter(およびそれを継承するMysql2Adapterなど)でNATIVE_DATABASE_TYPESを直接参照・変更しているコードに影響します。メリット
neighborなどの gem がNATIVE_DATABASE_TYPESに対して[]=,merge!などで型定義を追加・変更できるようになります。- PostgreSQL アダプタと同じパターンに揃うため、アダプタ間でカスタマイズ方法が一貫します。
デメリット / 注意点
NATIVE_DATABASE_TYPESはミュータブルな Hash のままになるため、アプリケーション or gem がこれを破壊的に変更すると、プロセス内の他の接続やコードにも影響が及びます。- 例:
NATIVE_DATABASE_TYPES.clearや意味のある既存キーの上書きは危険。
- 例:
- スレッドセーフではない変更(マルチスレッド環境での同時書き換え)にも注意が必要です。
- フレームワーク側の型定義を直接いじるのではなく、できるだけ公式 API(拡張機構)があればそちらを使うのが望ましいです。
現行アプリへの実務的な影響
- 変更前に
FrozenErrorが出ていたようなコード(特に gem 側)は、そのままで正常に動くようになります。 - これまで freeze されていたことを「前提」にしていたコード(たとえば
NATIVE_DATABASE_TYPES.dupして安全を確保していたようなコード)は、そのままでも動きますが、今後は「元のもの自体が書き換えられる可能性」があることを考慮すべきです。
- 変更前に
- 参考情報 (あれば)
- 本 PR:
- 関連フォローアップ PR:
- PostgreSQL アダプタでの類似コード(freeze しない実装の参照先):
neighborgem の該当コード:
#57332 fix: unfreeze Date::DATE_FORMATS
マージ日: 2026/5/10 | 作成者: @luizkowalski
- 概要 (1–2文で)
Date::DATE_FORMATSが凍結(freeze)されていたことで、アプリ側から日付フォーマットを追加できなくなっていた不具合を解消し、従来どおりフォーマットを後から登録できるようにした PR です。Time周りの実装(Time::DATE_FORMATS)と整合を取る形で、意図しない freeze を取り消しています。
- 変更内容の詳細
対象ファイル:
activesupport/lib/active_support/core_ext/date/conversions.rb
このファイル内で定義されている Date::DATE_FORMATS の定義から freeze が外されました。
(実際の差分はおおよそ以下のようなイメージです)
# 変更前(イメージ)
Date::DATE_FORMATS = {
# いくつかのデフォルトフォーマット定義
}.freeze
# 変更後
Date::DATE_FORMATS = {
# いくつかのデフォルトフォーマット定義
}これにより、アプリケーションコードから以下のように自由にフォーマットを追加・変更できる状態に戻ります。
# 例: アプリケーション初期化時など
Date::DATE_FORMATS[:short_ymd] = '%Y/%m/%d'
Date::DATE_FORMATS[:ja_long] = '%Y年%m月%d日'
Date.today.to_s(:short_ymd) # => "2026/05/11"
Date.today.to_s(:ja_long) # => "2026年05月11日"PR 説明にもある通り、Time の拡張 (activesupport/lib/active_support/core_ext/time/conversions.rb) ではすでに Time::DATE_FORMATS を freeze していないため、それと同じ挙動に揃えた形です。
- 影響範囲・注意点
影響範囲
Date::DATE_FORMATSにアプリケーション側からフォーマットを追加・上書きしているコードが、再び正常に動作します。- 直前のバージョン(
DATE_FORMATSが誤って freeze されていた期間)で発生していたFrozenError(もしくは類するエラー)が解消される可能性があります。
注意点
- 定数が「ミュータブルなハッシュ」である状態に戻るため、ライブラリやアプリケーションが意図せず
Date::DATE_FORMATSを書き換えると、グローバルな影響が出ます。- 特に gem やエンジンがフォーマットを変更する場合は、名前衝突しないキー名を使う、もしくは局所的に
I18n.lなど別の手段でフォーマットすることを検討してください。
- 特に gem やエンジンがフォーマットを変更する場合は、名前衝突しないキー名を使う、もしくは局所的に
- 既に「freeze されている前提」でワークアラウンド(例:
dupして使うなど)を書いていた場合、そのコードは依然として動きますが、不要になっている可能性があります。
- 定数が「ミュータブルなハッシュ」である状態に戻るため、ライブラリやアプリケーションが意図せず
- 参考情報 (あれば)
- Rails ガイドや API ドキュメントで紹介されているフォーマット追加パターンは、今回の PR により再びそのまま利用可能です。
例:ruby# config/initializers/date_formats.rb Date::DATE_FORMATS[:default] = '%Y-%m-%d' Date::DATE_FORMATS[:db] = '%Y-%m-%d' - 関連する実装:
activesupport/lib/active_support/core_ext/time/conversions.rbのTime::DATE_FORMATS定義(freeze していないのと同じ方針に揃えた変更)。
#57330 Handle missing Mandrill ingress signatures
マージ日: 2026/5/10 | 作成者: @afurm
- 概要 (1-2文で)
Action Mailbox の Mandrill ingress で、署名ヘッダX-Mandrill-Signatureが欠落している場合にNoMethodErrorが発生していた問題を修正し、欠落時も正しく 401 Unauthorized を返すようにした PR です。これにより、署名改ざんと同様に署名欠落も認証失敗として安全に扱われます。
- 変更内容の詳細
問題点
従来の実装では、Mandrill ingress の認証ロジックが以下のような形で動いていたと考えられます:
# 擬似コードイメージ
def authenticate
signature = request.headers["X-Mandrill-Signature"]
expected = generate_expected_hmac
# ここで signature が nil の場合でも secure_compare を呼んでしまう
ActiveSupport::SecurityUtils.secure_compare(signature, expected)
endActiveSupport::SecurityUtils.secure_compare は両方の引数に #bytesize を要求するため、signature が nil の場合に以下の例外が発生していました:
NoMethodError: undefined method `bytesize' for nil
activesupport/lib/active_support/security_utils.rb:34:in `secure_compare'本来であれば「署名がない=認証失敗」として 401 を返すべきところが、例外で処理が中断していました。
修正内容
PR では Mandrill 用の認証処理を次のように変更しています:
X-Mandrill-Signatureが 存在していることを前提条件 とし、空・nil の場合はsecure_compareを呼び出さず即座に認証失敗とする。- 署名が欠落しているリクエストは、偽造(HMAC 不一致)の場合と同じ扱いで
401 Unauthorizedを返す。
コード上は 1 行の変更ですが、実質的には下記のようなロジックになります(実際のコードを簡略化したイメージです):
def authenticated?
signature = request.headers["X-Mandrill-Signature"]
return false if signature.blank? # 新たに「署名が必須」であることを明示
expected = generated_mandrill_hmac
ActiveSupport::SecurityUtils.secure_compare(signature, expected)
endテストの追加
actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb に以下のようなテストが追加されています:
- 「
X-Mandrill-Signatureヘッダがない Mandrill リクエスト」を送る - レスポンスが 401 Unauthorized であること
ActionMailbox::InboundEmailが作成されていないこと
これにより、以前は NoMethodError で落ちていたケースが、意図どおり「認証失敗」として扱われることが検証されています。
- 影響範囲・注意点
- 影響対象: Mandrill を経由して Action Mailbox にメールを取り込んでいるアプリケーション。
- これまで:
X-Mandrill-Signatureが欠落していると、コントローラ内部でNoMethodErrorが発生し、アプリによっては 500 系エラーとして扱われる可能性があった。
- これから:
X-Mandrill-Signature欠落時は、偽造署名時と同じく 401 Unauthorized を返す。- 署名ヘッダがなければ絶対にメールは取り込まれない(これはセキュリティ上望ましい挙動)。
- ログやモニタリングの観点:
- 以前は 500 系エラー(例外)で検知されていたものが、401 応答(認証失敗)として観測されるようになる可能性があるため、モニタリングしているステータスコードの分布が変わることがあります。
- API 互換性:
- 正常な Mandrill 連携(正しい署名ヘッダを送っている場合)には影響がありません。
- Mandrill 側設定ミスなどで署名が送られていない環境では、今後 inbound email が一切作成されなくなるので、Mandrill の webhook 設定とシークレットの設定を確認すべきです。
- 参考情報 (あれば)
- 対応 Issue: #57329
- 修正対象コンポーネント:
Action Mailbox/Mandrill ingress - 追加で実行された検証コマンド:
- 個別テスト:
cd actionmailbox && bin/test test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb -i "/without a signature/" - ファイル全体テスト:
cd actionmailbox && bin/test test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb - RuboCop:
bundle exec rubocop actionmailbox/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb git diff --checkによる差分・スタイルチェック
- 個別テスト:
この PR は、本番で意図しない 500 エラーを 401 Unauthorized に正規化し、Mandrill ingress の認証フローをより安全かつ一貫したものにするバグフィックスです。
#57331 Remove isolated test tasks
マージ日: 2026/5/10 | 作成者: @byroot
- 概要 (1-2文で)
Rails 本体に定義されていた「テストを isolated(プロセス分離)して実行する系の Rake タスク」が一括で削除されました。
ビルド時間のわりに得られるメリットが小さく、CI 構成も見直されたため、メンテナンス対象から外した形です。
- 変更内容の詳細
2-1. 削除されたものの概要
各コンポーネントごと(actionpack, activerecord, activesupport など)の Rakefile から、以下のような「isolated テスト」タスク群が削除されています。
test:*:isolatedあるいはそれに類するタスク- Active Record まわりの複雑な test/DB 組み合わせタスク(特に
activerecord/Rakefileから大幅削除) activejobでも複数アダプタ・バックエンドを isolated にテストするようなタスクが整理・削除
PR 本文で触れられている通り:
These are much slower to run and don't catch much. Not worth the complexity.
という判断で、「時間だけかかって、得られる追加の検知能力が薄いテスト実行モード」をやめています。
2-2. Rakefile 周りの変更
Rakefile(ルート)と各コンポーネントの Rakefile の共通的な変更はおおむね以下のようなものです。
test:*:isolated系のタスク定義・依存関係を削除AGENTS.mdやguides/source/contributing_to_ruby_on_rails.mdから、それら isolated テストに言及していた記述を削除- 代わりに 通常のテストタスク (e.g.
bundle exec rake test,test:units,test:integrationなど) に一本化する方向に整理
activerecord/Rakefile は -101 行と差分が大きく、ここでは特に以下のような処理が削られたと考えられます(実際のコードは PR 本体参照):
- 複数データベースアダプタ(SQLite / PostgreSQL / MySQL など)や構成を、個別プロセスで走らせるためのタスク群
- CI 専用の複雑なテストマトリクスを作る Rake タスク
Rails の CI 構成は rails/buildkite-config リポジトリで管理されており、今回の PR はその follow-up として、本体側からも不要なタスクを削ぎ落としたものです。
2-3. ドキュメントの修正
guides/source/contributing_to_ruby_on_rails.md と AGENTS.md から:
- 「Rails にコントリビュートする際は、この isolated テストも実行してね」のような記述が削られ、
- 単純なテストコマンド(通常の
rake testなど)に関する説明に整理されています。
これにより、コントリビューターが読む公式なガイドラインからも isolated テストの存在が消え、今後は「通常のテストを回せばよい」という分かりやすい形になります。
- 影響範囲・注意点
Rails 開発者(コントリビューター)への影響
- これまでドキュメントに従って
rake test:XXX:isolatedのようなタスクを手動で叩いていた場合、それらは今後存在しない可能性が高いです。 - 代わりに、通常の
bundle exec rake testや、コンポーネントごとの基本的なテストタスクのみを実行すればよい形になります。
- これまでドキュメントに従って
CI / ビルド環境への影響
- Rails 本家での CI 構成(Buildkite)が
rails/buildkite-config側で既に見直されており、その follow-up として本体のタスクが消されています。 - 自前で Rails 本体をフォークしており、独自にこれらの isolated タスクを CI から呼び出していた場合は、ブランチ更新後に CI が失敗する可能性があります。
- 対応としては、CI 設定から該当の Rake タスク呼び出しを削除し、通常のテストタスクに切り替える必要があります。
- Rails 本家での CI 構成(Buildkite)が
アプリケーション開発者への影響
- Rails「本体リポジトリのビルドタスク」に対する変更であり、通常の Rails アプリケーションで一般的に触るタスク (
db:migrate,test,assets:precompileなど) には基本的に影響しません。 - 独自に Rails 本体の
Rakefileを参照してタスクを上書きしたりしていない限り、ほぼ無視してよい変更です。
- Rails「本体リポジトリのビルドタスク」に対する変更であり、通常の Rails アプリケーションで一般的に触るタスク (
挙動の変化に関する注意
- isolated テストは「別プロセス実行」や「環境一式を毎回クリアして実行する」などの特性を持つことが多く、環境汚染・グローバル状態のリーク等を検出しやすい一面がありました。
- PR 説明にある通り、それでも得られるバグ検知効果が限定的で、パフォーマンスやメンテナンスコストの方が重いという判断がなされています。
- 仮に isolated 実行でしか再現しないようなバグを狙ったテストを書いていた場合は、自分たちのリポジトリ側で
rakeタスクを定義し直すなど、独自対応が必要です。
- 参考情報 (あれば)
- 該当 PR:
- 関連する CI 設定の follow-up PR:
- コントリビュートガイド(変更対象ドキュメント):
guides/source/contributing_to_ruby_on_rails.md(Rails リポジトリ内)
#57323 Enable Style/MutableConstant cop with literals style
マージ日: 2026/5/10 | 作成者: @paracycle
- 概要 (1-2文で)
Style/MutableConstantCop を RuboCop 設定で有効化し、「リテラルで定義される定数は基本的に凍結して不変にする」という方針を Rails 全体に適用した PRです。これにより、Ractor でクラス/モジュールを安全に共有しやすくし、Ractor-safe なフレームワーク化を進めています。
- 変更内容の詳細
RuboCop 設定の変更
.rubocop.yml にて Style/MutableConstant Cop が literals スタイルで有効化されています。
literalsスタイル:
「リテラルで定義された定数 (配列、ハッシュ、文字列など) は変更されない前提なので、凍結されるべき」というポリシーを強制するモードです。
例:ruby# NG (MutableConstant) NAMES = ["Alice", "Bob"] # OK NAMES = ["Alice", "Bob"].freeze
Rails コードベース全体でこのルールが有効になり、リテラル定数のほぼ全てに .freeze が付与されるか、あるいは mutable に使いたい箇所では Cop が明示的に無効化されています。
各ファイルでの主なパターン
変更された 100 ファイルの多くが、以下いずれかのパターンに該当します。
1. リテラル定数の凍結
配列・ハッシュ・文字列などで定義される定数に .freeze を付ける、または既に freeze されている書き方に統一する変更です。
(簡略例・実際と似たイメージ)
# 変更前
DEFAULT_FORMATS = { html: Mime[:html], json: Mime[:json] }
# 変更後
DEFAULT_FORMATS = { html: Mime[:html], json: Mime[:json] }.freeze# 変更前
ACCEPTABLE_TYPES = %w[foo bar baz]
# 変更後
ACCEPTABLE_TYPES = %w[foo bar baz].freezeこれにより、フレームワーク側が「一度定義した定数の内容を書き換えない」ことを静的に保証しやすくなります。
2. 「意図的にミュータブル」な定数の除外
一部の定数は、キャッシュ用の Hash や、利用者が拡張する前提のマップ・配列などとして「中身を書き換える」設計になっています。
そうした箇所は、rubocop:disable Style/MutableConstant コメントで明示的に Cop を無効化したうえで、これまで通り mutable なままにしています。
イメージ:
# rubocop:disable Style/MutableConstant
MIME_TYPES = {} # ランタイムで追加される設計
# rubocop:enable Style/MutableConstantこれにより、「ここは mutable である」という意図がコード上にも明示され、将来的に改善(例えば Ractor 用の別経路を設ける、Thread/Ractor 局所キャッシュに分離する等)を行うべき箇所が可視化されています。
3. Ractor safety を意識した定数設計
PR の説明にもある通り、Ractor では「共有可能 (shareable)」であるオブジェクトだけが Ractor 間で共有できます。
Ruby 3 系の Ractor.make_shareable などの文脈では、freeze されていて再帰的に不変なオブジェクト が shareable の候補になります。
- クラス/モジュールを Ractor 間で共有する場合、その中の定数も shareable である必要がある
- よって「単なる設定値・メタデータ」としての定数は極力 freeze して不変にしておく必要がある
この PR は、その前提を RuboCop による静的チェックとして Rails コア全体に導入し、「Ractor-safe な設計かどうか」を壊れにくくしています。
- 影響範囲・注意点
ランタイム挙動への影響
ほとんどの定数は元々「書き換えない前提」で使われていたはずなので、多くのケースでは挙動は変わりません。
しかし、アプリやプラグイン側が Rails の定数を直接書き換えていた場合は破壊的変更になり得ます。
例:
ruby# 以前は(たまたま)動いていたかもしれないコード ActionController::Base::DEFAULT_PROTECTED_PARAMS << :fooこの PR でその配列が
freezeされると、上記はFrozenErrorを起こします。
Rails の定数を拡張したい場合
この PR のコメントにもある通り、「利用者が拡張する前提」の定数は mutable のままにしてありますが、その数はかなり限定的です。
既存アプリで Rails の定数を上書き・破壊的変更して依存している場合は、以下のような形へのリファクタリングを検討すべきです。
- 自分側の設定オブジェクトを定義して、Rails の定数は「読み取り専用」として参照だけ行う
- モンキーパッチが必要な場合でも、定数そのものを mutate せず、クラスメソッド・インスタンスメソッドの override で対応する
Ractor を使う場合の安心感
- Rails のコアにある「単なる設定・メタデータ的な定数」は、原則として Ractor 間で共有しても安全な状態に近づきました。
- まだ mutable な定数も残っていますが、それらは
rubocop:disable Style/MutableConstantによって明示されており、将来的な Ractor 対応の検討対象が明確になっています。
- 参考情報 (あれば)
- RuboCop ドキュメント:
Style/MutableConstantCop
https://docs.rubocop.org/rubocop/cops_style.html#stylemutableconstant - Ractor と shareable オブジェクトの概要 (Ruby 3):
https://docs.ruby-lang.org/ja/latest/doc/spec=2fractor.html (日本語仕様書・Ractor セクション)
この PR は、Rails 本体のスタイルと静的保証のレベルを一段上げつつ、Ractor 対応への足場を整えるためのインフラ的な変更と捉えると理解しやすいです。
#57328 Fix some Active Record flakes
マージ日: 2026/5/9 | 作成者: @byroot
- 概要 (1-2文で)
Active Recordのテストがときどき落ちる「フレークテスト(flaky tests)」を減らすため、ActiveRecord::RuntimeRegistryと一部関連テストを微調整したPRです。主にスレッドローカルな状態管理やテスト用モデル定義の見直しによる安定性向上が目的で、アプリケーション側の挙動変更はほぼありません。
- 変更内容の詳細
※ 実際の差分を要約した内容であり、行数ベース・ファイル名ベースでの説明です。
2-1. activerecord/lib/active_record/runtime_registry.rb (+2/-1)
ActiveRecord::RuntimeRegistry は、接続情報やスコープなど、スレッドローカルに紐づくActive Record内部の状態を管理するクラスです。
このPRでは、テストがフレークになる原因となっていた状態の残り方・初期化タイミングを修正する1行+αの変更が入っています。
典型的には以下のようなパターンの修正が考えられます:
- スレッドローカル変数のデフォルト値を明示する
nilチェックを追加し、安全にリセットする- テスト時にのみ状態をクリーンアップするためのガードを入れる など
イメージとしては、以下のような変更です(擬似コード・イメージ例):
module ActiveRecord
class RuntimeRegistry
# before
# def connection_handler
# ActiveSupport::IsolatedExecutionState[:active_record_connection_handler]
# end
# after (例)
def connection_handler
ActiveSupport::IsolatedExecutionState[:active_record_connection_handler] ||= default_connection_handler
end
end
endこの種の修正により、テストケース間で状態が持ち越されてしまうことを防ぎ、ランダム順序でテストを走らせたときにも安定するようになります。
2-2. activerecord/test/cases/associations/join_model_test.rb (+1/-1)
join_model_test.rb は、多対多関連(has_many :through など)で使用される中間モデルの挙動をテストするファイルです。
ここでは1行のみの修正で、以下のような調整が入っている可能性があります。
- 期待値の微修正(たとえば順序依存をなくす)
- 一時的に作るレコードや関連の作り方を修正
- モデル名の変更や読み込みタイミングの調整
例としては、以下のような「フレークになりやすいテスト」の修正が典型です(イメージ例):
# before
assert_equal [car1, car2], owner.cars
# after (順序に依存しないように)
assert_equal [car1, car2].sort_by(&:id), owner.cars.sort_by(&:id)このように、DBの並び順や実行タイミングに依存してしまう部分を排除し、どの環境でも安定して通るようにしています。
2-3. activerecord/test/models/car.rb (+2/-0)
テスト用モデル Car に2行の追加が行われています。join_model_test.rb の安定化のために、以下のような属性や関連の定義が明示的に追加された可能性があります。
イメージ例:
class Car < ActiveRecord::Base
# PRで追加された可能性が高いもの
has_many :ownerships
has_many :owners, through: :ownerships
endあるいは、バリデーションやデフォルトスコープを追加/削除して、テストで期待される状態と実際のモデル定義を一致させるような調整が行われています。
これにより、テストが暗黙の前提(「この関連があるはず」「この順で返るはず」など)に依存しないようにしています。
- 影響範囲・注意点
影響範囲
- 主な対象はRails本体のActive Recordテストスイートです。
RuntimeRegistryの挙動は一部アプリケーション側から直接参照される可能性があるものの、今回の変更は内部状態の初期化・クリーンアップに関するごく小さな修正で、通常のアプリケーションコードには影響しない設計になっています。Carモデルやjoin_model_test.rbはテスト専用のため、Railsを利用する側のアプリケーションには影響しません。
注意点
- 自前で
ActiveRecord::RuntimeRegistryの内部実装に依存したメタプログラミングをしている場合には、挙動の差異が出る可能性がありますが、そのようなケースはかなりレアです。 - フレーク修正PRのため、主眼は「テストの安定化」であり、「機能仕様の変更」ではありません。アップグレード時にこのPR単体でアプリケーションの仕様が変わることは基本的にないと考えて差し支えありません。
- 自前で
- 参考情報 (あれば)
- このPRは以下のPRから抽出されています:
- https://github.com/rails/rails/pull/57326
→ より大きな変更の中から、テストのフレーク対応部分だけを切り出したものと考えられます。
- https://github.com/rails/rails/pull/57326
ActiveRecord::RuntimeRegistryはスレッドローカルな状態管理に関するクラスであり、マルチスレッド環境(Puma など)やActiveSupport::IsolatedExecutionStateに関心がある場合の参考実装としても読む価値があります。
#57324 Use wait_for helper in reaper and async query tests
マージ日: 2026/5/9 | 作成者: @bogdan
- 概要 (1-2文で)
Active Record のテストコード内に残っていた独自実装のポーリングループを、テスト用ヘルパーwait_forに置き換えた PR です。これにより、テストがハングしたり、タイムアウト時に誤解を生むアサーションへ進んでしまう問題を防ぎ、失敗箇所が明確になります。
- 変更内容の詳細
背景
- #57309 に続くフォローアップで、Active Record のテストにまだ 3 つのポーリングループが残っていた。
- そのうち:
- 1つは上限のないループで、条件が満たされないと永遠にハングする可能性がある。
- 残り 2つは、一定時間ポーリングした後に条件未達でもそのまま後続のアサーションに進んでしまい、「本当の原因は条件未達なのに、別のアサーション失敗として見えてしまう」という誤解を招く挙動だった。
具体的な修正内容
対象ファイル:
activerecord/test/cases/asynchronous_queries_test.rbactiverecord/test/cases/reaper_test.rb
上記ファイル内の「自前のポーリングループ」を、テストヘルパー wait_for の呼び出しに統一しました。
wait_for は典型的に以下のような形で使われます (イメージ):
wait_for do
# ここで期待する条件が true になるまで待つ
some_async_state.ready?
endあるいはタイムアウトを指定して:
wait_for(timeout: 5.seconds) do
connection_pool.active_connection_count.zero?
endこの PR では、元々次のようなパターンになっていたコードを:
# 擬似例: 独自ポーリング
timeout_at = Time.now + 5
until something_happened? || Time.now > timeout_at
sleep(0.1)
end
# ここで「起きているはず」という前提でアサートするが
# 実際には起きていない場合もある
assert something_happened?wait_for による明示的な待機に置き換えています:
wait_for do
assert something_happened?
endあるいは:
wait_for do
something_happened?
endwait_for は、指定時間内にブロックが成功しない場合に Timeout::Error を発生させるため、
- 「どの条件待ちが失敗したか」がスタックトレース上で明確になる
- 無限ループによるハングを避けられる
というメリットがあります。
テスト対象の文脈としては:
asynchronous_queries_test.rb: 非同期クエリの完了・結果取得を待つためのポーリングをwait_forに置き換えreaper_test.rb: コネクションプールの reaper(アイドル接続のクリーンアップを行うスレッド)の動作確認で、reaper が実際に動作するのを待つ部分をwait_forに置き換え
- 影響範囲・注意点
影響範囲
- 本 PR は「テストコードのみ」の変更であり、本体のライブラリ挙動やパブリック API には影響しません。
- Active Record の非同期クエリとコネクションプール reaper に関するテストの信頼性・デバッグ容易性が向上します。
注意点
- 今後、同様に非同期処理やバックグラウンドスレッドの状態を待つテストを書く場合は、独自のループ構築ではなく
wait_forの利用が推奨されます。 wait_forのデフォルトタイムアウト値や挙動は test helper に依存するため、タイムアウトがシビアなケースではtimeout:オプション指定を検討するとよいです。- 従来は「原因が曖昧なハング」や「別のアサーションでの失敗」として現れていた問題が、
Timeout::Errorとしてはっきり表面化するようになるため、既存 CI 上でまれに「新しく落ちるように見える」テストが出る可能性がありますが、それは潜在的なテスト不備やタイミング依存バグの顕在化と考えられます。
- 今後、同様に非同期処理やバックグラウンドスレッドの状態を待つテストを書く場合は、独自のループ構築ではなく
- 参考情報 (あれば)
- この PR: https://github.com/rails/rails/pull/57324
- 前提となる PR (#57309):
wait_forの導入や既存テストの置き換えが実施された PR (詳細は該当 PR 参照) wait_forは Rails テストスイートで非同期挙動を扱う際の標準的ヘルパーであり、Active Job・Action Cable・非同期クエリ・バックグラウンドスレッドなど、状態が安定するまでポーリングする必要がある箇所での利用が推奨されます。
#57327 Active Record connection pool tests avoid always skipped tests
マージ日: 2026/5/9 | 作成者: @byroot
- 概要 (1-2文で)
Active Record の connection pool に関するテストのうち、実行環境的に「常に skip されてしまうテスト」がそもそも定義されないように整理された PR です。これにより、テストスイートのノイズが減り、実際に実行されるテストだけが定義される構成になりました。
- 変更内容の詳細
※この PR は activerecord/test/cases/connection_pool_test.rb を大量にリファクタリングしており、コード上の挙動を変えるというより「テストの構造・定義条件」を大きく整理しています。
主なポイント:
「常に skip されるテスト」の削除/条件付き定義
- 以前は、例えば特定のアダプタや機能が有効になっていないときに
skipされるテストが、多数クラス定義されていました。 - しかし「どの CI/開発環境でも前提が満たされず、永遠に実行されないテスト」が存在しており、それらがテストスイートに残り続けていました。
- この PR では、そうしたテストを
- そもそも
if 条件を満たさない場合はtest自体を定義しない - もしくはテストクラス自体を定義しない という形に変更しています。
- そもそも
- 以前は、例えば特定のアダプタや機能が有効になっていないときに
テスト定義条件の見直し
- 例としてイメージしやすいパターン:
- 変更前(イメージ):ruby
class ConnectionPoolFooTest < ActiveRecord::TestCase def setup skip "Adapter X is required" unless ActiveRecord::Base.connection.adapter_name == "X" end def test_foo_behavior # ... end end - 変更後(イメージ):ruby
if ActiveRecord::Base.connection.adapter_name == "X" class ConnectionPoolFooTest < ActiveRecord::TestCase def test_foo_behavior # ... end end end
- 変更前(イメージ):
- このように、実行対象になり得るケースでのみテストを定義するようにしています。
- よくある
SKIP_MESSAGE = "..."を毎回使うのではなく、条件に応じてクラス単位・ブロック単位でテストを囲む構造になっているはずです。
- 例としてイメージしやすいパターン:
テストの整理・グルーピング
- connection pool のテストが、アダプタ種別や機能有無(例: reaping, async, multiple databases など)ごとに整理され、条件つきで定義される形にまとまっている可能性が高いです。
- その結果、行数としては+437/-456 で、ほぼ全体が書き換えに近いリファクタリングになっていますが、挙動変化ではなく「どのケースでどのテストが存在するか」の整理が中心です。
スキップの可視性から「実行されるテストの明確化」へ
- 以前は
rake testや CI 上で「たくさん skip が出るが、実際には二度と動かないテスト」というノイズが混ざっていた可能性があります。 - 今回はそれを根本的に解消し、「意味のある skip」だけが残り、「どうやっても実行されないテスト」は定義しない、という方針に寄せています。
- 以前は
- 影響範囲・注意点
Rails 本体の挙動への直接的な影響は原則なし
- 変更は
activerecord/test/cases/connection_pool_test.rbのみで、本番コード(lib/active_record/...)には触れていません。 - そのため、アプリケーションの connection pool の挙動自体が変わることはありません。
- 変更は
テストスイートの見え方が変わる
- Rails を fork してテストを流している場合や、Rails 本体にコントリビュートしている場合、
- これまで「skip」として出ていたテストが丸ごと表示されなくなる
- テスト総数 / skip 数のカウントが変わる といった違いが出ます。
- 「昔は connection pool 周りで X 個 skip があったのに今は無い」といった変化は、この PR によるものです。
- Rails を fork してテストを流している場合や、Rails 本体にコントリビュートしている場合、
環境依存テストの追加・変更時の注意
- 今後 connection pool 関連でテストを追加する場合、
- 「条件を満たさないときは
skipする」のではなく - 「条件を満たす場合にだけテスト/クラスを定義する」 というこの PR のスタイルに揃えるほうが整合的です。
- 「条件を満たさないときは
- 例えばカスタムアダプタ向けテストや、特定の設定オプションに依存するテストを書く場合は、テスト定義そのものを条件でラップする設計が推奨されます。
- 今後 connection pool 関連でテストを追加する場合、
古い/未サポート機能向けテストの棚卸し
- 今回のように「どの環境でも実行されないテスト」がある場合、それは
- もはや使われていない/削除済み機能
- サポートを終了したアダプタ 向けのテストであることが多いです。
- 自分のプロジェクトでも connection pool 周りのテストを長く維持している場合は、「常に skip されているテスト」はそもそも削除 or 条件付き定義にし直す、という観点で棚卸しするとよいです。
- 今回のように「どの環境でも実行されないテスト」がある場合、それは
- 参考情報 (あれば)
この PR:
- タイトル: Active Record connection pool tests avoid always skipped tests
- 番号: #57327
- 作成者: byroot
- マージ日時: 2026-05-09T12:43:55Z
関連する観点:
- Rails コアでは、CI の安定性・実行時間短縮のため、「実行されないテストを減らす」「意味のない skip を無くす」流れが継続しており、本 PR もその一環と考えられます。
- connection pool 周りの仕様・挙動自体を確認したい場合は、
activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb付近の実装と、周辺テスト (connection_pool_test.rbのうち実行されているテスト) をあわせて読むと理解が深まります。
#57325 Fix BigDecimal rendering failure in Arel tests
マージ日: 2026/5/9 | 作成者: @byroot
- 概要 (1-2文で)
Arel の ToSQL ビジターがBigDecimalを文字列化する際、指数表記 (0.214e1) になってしまい、期待される通常の小数表記 (2.14) とズレてテストが落ちていた問題を修正する PR です。
テストコードと依存バージョンの微調整により、BigDecimalの SQL 文字列表現が期待どおり安定して比較できるようにしています。
- 変更内容の詳細
問題の内容
テスト失敗メッセージ:
1) Failure:
Arel::Visitors::ToSQLTest::the to_sql visitor#test_0032_should visit_BigDecimal [test/cases/arel/visitors/to_sql_test.rb:350]:
Expected: "2.14"
Actual: "0.214e1"Arel::Visitors::ToSQL が BigDecimal("2.14") 相当の値を SQL に変換した結果が、Ruby / BigDecimal / 依存ライブラリのバージョン差などにより "2.14" ではなく "0.214e1" という指数表記になってしまい、テストが失敗していました。
実際の変更点
変更ファイルは2つです。
1) activerecord/test/cases/arel/visitors/to_sql_test.rb (+1/-1)
BigDecimal を扱うテスト (should visit_BigDecimal) の期待値、あるいは比較方法が調整されています。
PR 説明から読み取れる状況:
- 元の期待値:
"2.14" - 実際の出力:
"0.214e1"
この差分を解消するために、
- 期待値側を、実際の
ToSQL出力のフォーマットに合わせる
もしくは - 出力を正規化した上で比較する(例:
to_dして数値として比較、指数表記を通常表記に変換して比較)
といった形でテストが修正されています。
(PR情報的に、実装コードは触られておらずテスト側だけ変更されているので、「SQL 生成ロジック自体は問題なく数値としては正しいが、文字列表現が環境依存で揺れる」ことを前提に、テストを安定させる方向の修正と考えられます。)
サンプルイメージ:
# 例: 期待値を単純な文字列ではなく BigDecimal として比較する形に寄せるなど
assert_equal BigDecimal("2.14"), BigDecimal(sql_value_from_visitor)実際の1行差分は、例えば以下のようなイメージです(あくまで擬似コード):
- assert_equal "2.14", visit(BigDecimal("2.14"))
+ assert_equal "0.214e1", visit(BigDecimal("2.14"))
# または
+ assert_equal BigDecimal("2.14"), BigDecimal(visit(BigDecimal("2.14")))2) Gemfile.lock (+2/-2)
BigDecimal の文字列表現に影響する可能性がある依存ライブラリや Ruby / gem バージョンの更新・固定が行われています。
目的は、開発・CI 環境で BigDecimal の出力フォーマットがブレないようにすることです。
具体的には、例えば以下のような変更が想定されます(実際の gem 名は PR 本体参照):
- Ruby パッチバージョン / bigdecimal gem / アダプタ関連 gem のバージョンアップ・ダウン
- バージョンの固定(
~> X.Y→= X.Y.Zなど)によるフォーマットの安定化
- 影響範囲・注意点
- 影響範囲は 基本的にテストコードと依存のバージョン管理に限定 されており、Rails / Arel を利用するアプリケーションの動作や公開 API には変更はありません。
- ただし以下のようなケースでは注意が必要です:
- アプリケーション側で、Arel による BigDecimal の SQL 出力を「文字列として」厳密比較している(例:
"2.14"であることを前提に正規表現マッチや文字列置換を行っている)場合- 環境によって
"0.214e1"のような表記になる可能性自体は以前から存在しており、本 PR はその事実をテスト側に反映させた形です。 - 今後も「数値的には同じだが文字列表現が違う」出力があり得る前提でコードを書く方が安全です。
- 環境によって
- アプリケーション側で、Arel による BigDecimal の SQL 出力を「文字列として」厳密比較している(例:
- Rails コアの方針としても、BigDecimal の SQL 文字列表現は環境差が出得る ため、アプリ側では数値として扱うか、フォーマットを自前で正規化するのが望ましいです。
- 参考情報 (あれば)
BigDecimal の文字列表現の違いの例(素の Ruby でも起こりうる):
rubyrequire "bigdecimal" BigDecimal("2.14").to_s # => "0.214E1" BigDecimal("2.14").to_s("F") # => "2.14" # 通常の小数表記Rails / Arel の実装が
to_sなのかto_s("F")なのか、または他のフォーマッタを使うかによって、出力が指数表記になるかが変わります。関連しそうなコード:
Arel::Visitors::ToSQLのvisit_Arel_Nodes_BindParam/visit_BigDecimal付近の実装- 以前から BigDecimal のフォーマット周りは Ruby / bigdecimal gem のバージョンによる差異でテストが不安定になりやすく、今回の PR もその一環での安定化対応と考えられます。
#57136 Reject trailing dash in PostgreSQL adapter
マージ日: 2026/5/8 | 作成者: @yoLotus
- 概要 (1-2文で)
PostgreSQL アダプタが UUID の末尾に余分なハイフン (-) を含む文字列を誤って有効とみなしていたバグを修正し、正しくActiveRecord::RecordNotFound(説明文ではNotFoundと書いていますが実際の例外クラスは通常これ)を発生させるようにした PR です。UUID のバリデーションに使う正規表現を修正し、Trailing dash を含む UUID を拒否するように変更されています。
- 変更内容の詳細
問題の挙動
PostgreSQL 自体はドキュメントどおりの UUID 形式のみを受け付けますが、Rails の PostgreSQL アダプタ側で UUID 判定に使っている正規表現に不備があり、以下のような「末尾にダッシュが 1 つある」値を UUID として扱ってしまっていました。
User.find('a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11-') # 末尾にハイフンこの場合、本来であれば「不正な UUID なのでレコードが見つからない」= ActiveRecord::RecordNotFound が発生してほしいところですが、アダプタ側の処理で「末尾の - を取り除いたうえで」UUID として扱ってしまい、結果として既存レコードがあれば普通に返ってきてしまう、という問題がありました。
つまり、Rails が「不正な UUID を暗黙に修正してしまう」ことで、アプリケーション側のバグや入力不正を検知しにくくなっていました。
修正内容
変更ファイル:
activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rbactiverecord/test/cases/adapters/postgresql/uuid_test.rb
主な修正は UUID のパースに使われる正規表現の 1 行だけです。
旧来の正規表現は「UUID の標準形式 8-4-4-4-12 のパターン」に加えて、末尾の 1 文字分を曖昧に許容してしまうような形になっており、その結果「最後に余計な - が 1 つ付いた文字列」もマッチしていました。
この PR では、PostgreSQL のドキュメントに基づいた UUID 形式(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx、[0-9a-fA-F] の 8-4-4-4-12 桁、ダッシュ位置固定)にきっちり一致する場合だけ有効とみなすように正規表現を修正しています。
これにより、以下のような値はすべて不正な UUIDとして扱われます。
# 不正として拒否される例
User.find('a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11-') # 末尾の余計なダッシュ
User.find('a0ee-bc99-9c0b-4ef8-bb6d-6bb9bd38-0a11') # ダッシュ位置がずれている
User.find('a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a1') # 桁数不足
User.find('g0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11') # 16進数以外の文字を含むテスト (uuid_test.rb) も更新されており、「trailing dash を含む UUID が拒否される」ことを確認するケースが追加されています。
- 影響範囲・注意点
影響対象
- PostgreSQL を使用しており、UUID 型の主キーやカラムを使っているアプリケーション。
- 特に
Model.find(...)/find_by/whereなどに「末尾に-がついた UUID 文字列」やその他フォーマット不正な UUID を渡していた場合、以前は通っていたものが「不正」として扱われます。
具体的な挙動変化
- 以前:
User.find('uuid-with-trailing-dash-')で、内部的に末尾の-が削られ、存在すればレコードが返る(サイレント修正)。 - 今後: その値は「正しい UUID 形式ではない」と判定され、PostgreSQL アダプタ側で UUID として扱われなくなり、最終的に
ActiveRecord::RecordNotFoundなど、期待されるエラーが発生するようになります(呼び出しパスによる)。
- 以前:
既存コードへの影響
- クライアントや外部 API から渡ってくる UUID 文字列が厳密な形式になっていない場合、今後はエラーが表面化します。
- 今まで「なぜか動いていた」ケースが、形式不正として落ちる可能性があります。
- 一方で、セキュリティや不正入力検知の観点では正しい動作に近づくため、意図しない ID 改変を早期に検出しやすくなります。
- クライアントや外部 API から渡ってくる UUID 文字列が厳密な形式になっていない場合、今後はエラーが表面化します。
対応のポイント
- 外部入力(URL パラメータ、JSON、ヘッダなど)で UUID を受け取る箇所では、クライアント側 / サーバ側双方で UUID フォーマットを厳密にバリデートしておくとよいです。
- もし「末尾にスペースやダッシュが付いてしまう」といった前処理ミスがある場合は、この PR 適用後に 400 / 404 が増える可能性があるため、入力前処理を修正する必要があります。
- 参考情報 (あれば)
PostgreSQL UUID ドキュメント
https://www.postgresql.org/docs/18/datatype-uuid.html関連して挙動変化を導入した可能性がある PR として言及されているもの:
- #49934 (この PR で導入された正規表現の変更が今回のバグの原因の一つかもしれないとコメントされています)
対象コード:
activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb- PostgreSQL アダプタで UUID 型を扱うための OID マッピングとキャストロジックが定義されているファイルで、ここにある正規表現が今回修正された箇所です。
#56575 Support Multiple Arguments in ActionController::Parameters#merge in Consistent with Ruby's Hash#merge
マージ日: 2026/5/8 | 作成者: @berniechiu
- 概要 (1-2文で)
ActionController::Parameters#merge/#merge!が Ruby 3.2 以降のHash#mergeと同様に「複数引数」を受け取れるように拡張された PR です。Rails の Strong Parameters の API と Ruby 標準 Hash API の挙動を揃えることで、一貫性と使い勝手を向上させています。
- 変更内容の詳細
何が変わったか
これまで:
params = ActionController::Parameters.new(a: 1)
# OK
params.merge(b: 2)
# NG(ArgumentError になる)
params.merge({ b: 2 }, { c: 3 })この PR 以降:
params = ActionController::Parameters.new(a: 1)
# 1つの引数
params.merge(b: 2) # => #<ActionController::Parameters {"a"=>1, "b"=>2} ...>
# 複数の引数 (Hash / Parameters など)
params.merge({ b: 2 }, { c: 3 }) # => {"a"=>1, "b"=>2, "c"=>3}
# bang メソッドも同様に複数引数対応
params.merge!({ d: 4 }, { e: 5 }) # params 自身が {"a"=>1,"b"=>2,"c"=>3,"d"=>4,"e"=>5} に更新Ruby 3.2 の Hash#merge が以下のような形をサポートしているのに合わせたものです:
h = { a: 1 }
h.merge({ b: 2 }, { c: 3 }) # => { a: 1, b: 2, c: 3 }この PR では、ActionController::Parameters#merge / #merge! の内部実装を変更し、可変長引数 (*others) を受け取り、それらを順番にマージするようにしています。これにより、Ruby の Hash#merge とほぼ同じ感覚で Strong Parameters を扱えます。
テスト
actionpack/test/controller/parameters/parameters_permit_test.rb にテストが追加されており、
Parameters#mergeが複数のParametersやHashを引数に取れること- Strong Parameters の性質(permit/unpermit の扱い)が保たれていること
などが確認されています。
- 影響範囲・注意点
対象クラス
ActionController::Parametersの#merge/#merge!のみが対象です。他のmerge実装には影響しません。既存コードへの影響
単一引数で使っている既存コードはそのまま動作します。インターフェースが拡張されただけで、1引数時の挙動互換性は維持されています。複数引数のマージ順序
Hash#mergeと同様に「左から右へ」順番にマージされ、後ろの引数のキーが前のものを上書きします。rubyparams.merge({ a: 1 }, { a: 2 }) # => a は 2 になるStrong Parameters と permit の扱い
Parameters#mergeは元々、permit状態や strong parameters の性質に注意が必要なメソッドです。- マージ相手が単なる
Hashの場合、その値は「未許可パラメータ」として扱われうるため、コントローラ内でのpermitの呼び方・位置には従来どおり注意が必要です。 - この PR はあくまで「複数引数を受け取れるようにした」だけで、Strong Parameters のポリシー自体は変えていません。
- マージ相手が単なる
Ruby バージョン依存性との一貫性
Rails 側の API が Ruby 3.2+ のHash#mergeと一貫するようになったため、Ruby のHashで書いていたコードパターンをそのまま Strong Parameters に適用しやすくなります。
- 参考情報 (あれば)
- 当該 PR: https://github.com/rails/rails/pull/56575
- 以前の関連 PR: https://github.com/rails/rails/pull/56410
- Ruby 3.2
Hash#mergeドキュメント:
https://docs.ruby-lang.org/en/3.2/Hash.html#method-i-merge - 変更ログ:
actionpack/CHANGELOG.mdに、この挙動変更が追記されています。
#54189 Allow configuring Active Storage base controller parent class
マージ日: 2026/5/8 | 作成者: @zzak
- 概要 (1-2文で)
Active Storage のコントローラ (ActiveStorage::BaseController) が継承する親クラスをアプリ側から設定できるようになり、ActionController::APIを親にして API-only 構成でも利用しやすくなりました。これに伴い、新しい設定フラグとテストが追加されています。
- 変更内容の詳細
2-1. 何ができるようになったか
- これまで Active Storage のコントローラは Rails 側で固定の親クラスを継承していましたが、
- この PR により「Active Storage のベースコントローラの親クラス」をアプリ側で構成可能になりました。
- 特に、
ActionController::APIを継承させることで、API-only アプリケーションでも Active Storage を素直に使えるようにするのが主目的です。
具体的には、次のような構成が可能になります(例:config/application.rb など):
# 例: API-only アプリで ActiveStorage のコントローラを API ベースにする
config.active_storage.draw_routes = true
config.active_storage.resolve_model_to_route = :rails_storage_proxy
# 新しく追加されたであろう設定(名称は概念的な例)
config.active_storage.routes_api_only = true
# または
config.active_storage.use_api_only_controller = true※実際の「フラグ名」は PR 本文では flag as suggested としか書かれていないため、ここでは概念的に表現しています。最終的な設定名は Rails の最新ドキュメント / activestorage/lib/active_storage/engine.rb を確認して利用してください。
2-2. コードレベルの変更ポイント
主な変更ファイルと役割は次の通りです。
active_storage/base_controller.rb
- これまで固定だった親クラス継承を、Active Storage の設定値に応じて決定する形に変更。
- 内容イメージ(疑似コード):
module ActiveStorage
class BaseController < ActiveStorage.config.base_controller # のような形
# もしくは Engine 側で Module#prepend などを使って決めている可能性もあり
end
end実際には、ActiveStorage::Engine 内の設定やフラグに基づいて、
- 通常:
ActionController::Baseを継承 - API-only 設定時:
ActionController::APIを継承
といった切り替えをしています。
active_storage/disk_controller.rb
DiskControllerはBaseControllerを継承しているため、BaseControllerの親クラス変更の恩恵を受けます。- ここでは
inheritするクラスの参照の仕方や、BaseController側のインターフェース変更に追従する軽微な修正が入っています。
active_storage/lib/active_storage.rb
- Active Storage の設定項目に「親コントローラクラス関連の設定/フラグ」が追加。
- 例として、下記のような設定アクセサが増えているイメージです(あくまで概念的な例):
module ActiveStorage
class << self
attr_accessor :api_only # あるいは :routes_api_only など
end
endactive_storage/lib/active_storage/engine.rb
- Engine 初期化時に、上記設定フラグを読み取り、
BaseControllerの親クラスとして何を使うか決めるロジックを追加。 - Rails の設定 (
config.active_storage.xxx) をActiveStorageモジュール配下の設定値にブリッジする箇所もここに含まれます。
railties/test/application/active_storage/direct_uploads_integration_test.rb
- 変更が正しく動作することを保証するため、API-only アプリを想定した統合テストが大幅に追加されています(+192 行)。
- 想定されるテスト内容:
- API-only アプリの
config.api_only = true+ 新フラグを有効にした状態で、Active Storage の direct upload エンドポイントに対するリクエストが通るか。 - レスポンス形式や CSRF 周りの振る舞いが期待通りか。
- ルーティング・マウントが正しく行われているか。
- API-only アプリの
activestorage/CHANGELOG.md
- 今回の機能追加が CHANGELOG に追記されています(新規設定・API-only サポートが明記されているはずです)。
- 影響範囲・注意点
3-1. 既存アプリへの影響
- 既存アプリで特に設定を追加しなければ、従来通りの親クラス(
ActionController::Base)が使われることが想定されます。 - したがって、アップデート直後に Active Storage が壊れるリスクは低い設計です。
3-2. API-only アプリでの利用時の注意
- API-only モード (
config.api_only = true) では、ActionController::APIを継承することで自動的に HTML 関連の機能(view レンダリング、cookie ベースのセッション、CSRF 対策など)が無効/簡略化されます。 - そのため:
- Active Storage のルーティングや direct upload を 完全に API として使う 場合にはメリットが大きいです。
- 一方で、ブラウザフォーム + direct upload + authenticity_token など、従来の HTML フローを期待している場合は、
ActionController::APIに切り替えると一部機能が使えなくなる可能性があります。
- どの親クラスにするかを決める前に、「アプリ内で Active Storage をどう使っているか(純 API / ブラウザ UI / admin 画面など)」を確認した方が安全です。
3-3. カスタムコントローラを持っている場合
- もしアプリ側で
ActiveStorage::BaseControllerやDiskControllerを継承して独自拡張している場合:- 親クラスが変わることで利用できるヘルパーメソッドやフィルタが変わる可能性があります。
- 特に
before_action :authenticate_user!のような Devise / 認証系を組み込んでいると、ActionController::APIだと挙動が変わるケースがあります(セッション利用など)。
- そのような場合は、親クラスを変えた後に自分のカスタムコントローラも含めて一通り動作確認する必要があります。
- 参考情報 (あれば)
- 対応する Issue:
- https://github.com/rails/rails/issues/32208
→ 「ActiveStorage を API-only アプリで利用する際に、コントローラをActionController::APIから継承したい」という要望。
- https://github.com/rails/rails/issues/32208
- 元 PR(今回の Rebase 元):
- コメントで提案された「フラグで親クラスを切り替える」アプローチ:
実際に利用する際は、Rails の該当バージョンのガイド・API ドキュメント、activestorage/lib/active_storage/engine.rb 内の config.active_storage 関連設定を確認して、導入するフラグ名・設定方法を正確に把握してください。
#55134 Move prevent_writes defaulting to params of role switching methods
マージ日: 2026/5/8 | 作成者: @joshuay03
- 概要 (1-2文で)
このPRは、Active Record の接続ロール切り替え系メソッドにおけるprevent_writesのデフォルト挙動を「メソッド本体で上書き」する形から「メソッド引数レベルで明示的に定義・検証」する形に変更し、:readingロールでprevent_writes: falseを指定した場合はArgumentErrorを発生させるようにしたものです。あわせてドキュメントとテストが、この新しい仕様に合わせて整理・強化されています。
- 変更内容の詳細
対象となるメソッド
ActiveRecord のパブリックな「ロール切り替え」関連メソッド(例)に影響します。
connected_toconnected_to_manywith_rolewith_shardwith_role_and_shard(内部実装用メソッドも含む)
これらのメソッドは、role: :reading のときに自動的に prevent_writes を true にする挙動を持っていましたが、その実装位置と仕様表現が変わっています。
これまでの挙動
従来は、メソッド本体の中で「もし role == :reading なら prevent_writes を強制的に true に上書き」していました。
そのため、次のようなコードを書いても:
ActiveRecord::Base.connected_to(role: :reading, prevent_writes: false) do
# ...
end実際には prevent_writes は内部で true に上書きされ、書き込みは抑止されていました。ただしこの仕様は:
- メソッドシグネチャ(引数定義)からは読み取りにくい
prevent_writes: falseを渡しても無視されるためバグを見落としやすい- 一部メソッドではドキュメントに明示されていない
という問題がありました。
新しい仕様・実装
このPRでは、次のように振る舞いが整理されています。
メソッド引数レベルでのデフォルト指定
- ロール切り替えメソッドのパラメータ定義に、「
role: :readingのときはprevent_writesはデフォルトでtrue」であることを反映。 - 具体的には、メソッドの引数・キーワード引数の宣言や、内部でのパラメータ処理において、このデフォルトが明示されるよう変更されています(PR説明より)。
- ロール切り替えメソッドのパラメータ定義に、「
:reading+prevent_writes: falseはArgumentErrorに- これまでは「与えられた値を上書き」していましたが、
- これからは次のような呼び出しはエラーになります。
rubyActiveRecord::Base.connected_to(role: :reading, prevent_writes: false) do # => ArgumentError を発生させるべきケースに変更 end理由:
- 読み取り専用の接続ロールで「書き込みを防がない (
prevent_writes: false)」という指定は論理的に破綻している - かつ、実装上もその指定は既に無視されていたため、明示的にエラーとした方がバグに気付きやすい
with_role_and_shardを含む一貫した挙動とドキュメント整備- これまで一部のメソッドは内部でロール判定と
prevent_writes上書きをしていましたが、ロジックの位置を「パラメータ処理」に寄せることで挙動が一貫するようになっています。 - すべてのロール切り替え系メソッドで、「
role: :readingの場合prevent_writesがデフォルトでtrueになる」ことがドキュメントに明示されました。
- これまで一部のメソッドは内部でロール判定と
役割 (role) とシャード (shard) の引数仕様のドキュメント修正
ドキュメント中で次が整理されています:
- 「role 引数が必須で、shard 引数はオプション」のメソッド
- 「role または shard、あるいはその両方を受け取る(and/or)」メソッド
これにより、各メソッドの使い方がより正確に伝わるようになっています。
テスト・CHANGELOG
activerecord/test/cases/base_test.rbactiverecord/test/cases/shard_keys_test.rb
にテストが追加・更新され、新しい
prevent_writesの扱いやエラー条件がカバーされています。activerecord/CHANGELOG.mdにも、この挙動変更が記載されました。
- 影響範囲・注意点
APIの挙動変更(潜在的な破壊的変更)
これまで「黙って
prevent_writesをtrueに上書き」していたケースで、今後はArgumentErrorになり得ます。特に、既存コードで以下のようなパターンがあるとエラーになる可能性が高いです。
ruby# NG になるようになった例 ActiveRecord::Base.connected_to(role: :reading, prevent_writes: false) do # ... end # role を動的に渡しているコードも要注意 role = compute_role # :reading になることがある ActiveRecord::Base.connected_to(role: role, prevent_writes: some_flag) do # some_flag が false の場合にエラー end以前は「実質無視されていた」「意図しない指定」に対して、明示的なエラーが出るようになるので、テスト時にこの種のバグがあれば早期に発見できます。
読み取りロールで「書き込み許可」はできない
- このPRにより、「
role: :readingだが一時的に書き込みを許可したい」というような設計は、少なくとも公式APIとしてはサポートしない方向性がより明確になりました。 - 書き込みが必要な処理は、明示的に書き込み可能なロール (
:writingなど) に切り替える必要があります。
- このPRにより、「
ドキュメントに沿った引数使用が重要
- 「role が必須か」「shard はオプションか」「role/shard のどちらか/両方を渡せるのか」といった仕様がドキュメントで整理されたので、それに沿って引数を指定することが重要です。
- 既存コードで role/shard の指定の仕方が曖昧だった場合、今後はより厳密にチェック・把握しておくと安全です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/55134
タイトル: Moveprevent_writesdefaulting to params of role switching methods - 関連コンテキスト:
- Rails 6 以降のマルチDB・ロール/シャード対応 (
connected_to,with_role,with_shardなど) に関する設計の一環として、「読み取りロールは常に書き込み禁止」という前提が明確化された変更と捉えられます。 prevent_writesは、読み取り用レプリカなどに対して誤って書き込みを行わないためのガードとして使われるフラグですが、その仕様がより宣言的かつ安全になっています。
- Rails 6 以降のマルチDB・ロール/シャード対応 (
#57091 Fix bug with escaped characters when "Copy as text" is clicked
マージ日: 2026/5/8 | 作成者: @bogdan
- 概要 (1-2文で)
Rails の例外画面にある「Copy as text」ボタンでコピーされるテキストに、HTML エスケープされた文字('など)が混ざってしまう不具合を修正した PR です。テキストの格納先を<script>要素の body から<textarea>要素のvalueに変更することで、自動エスケープを回避しています。
- 変更内容の詳細
何が問題だったか
- Rails のエラーページ(
ActionDispatch::DebugExceptionsが描画する rescue テンプレート)には、「Copy as text」ボタンがあり、スタックトレースなどをそのままコピーできるようになっています。 - 以前は、このコピー用テキストを HTML テンプレート内の
<script>タグの中に埋め込み、それを JS 側で読み出してクリップボードにコピーしていました。 - しかし
<script>タグ内のテキストは、テンプレート描画時に HTML エスケープされるため、例外メッセージ中のシングルクォート'が'になるなど、コピー結果が本来の文字列と異なってしまう問題がありました。
PR 説明にある例:
app/controllers/admin/assets_uploads_controller.rb:73:in 'Admin::AssetsUploadsController#resource_params'本来は:
app/controllers/admin/assets_uploads_controller.rb:73:in 'Admin::AssetsUploadsController#resource_params'とコピーされるべきところがエスケープされてしまっていた、という状況です。
修正方針
- コピー対象のテキストを
<script>タグではなく<textarea>要素のvalue属性として保持するように変更しています。 <textarea>のvalueはフォームの値として扱われ、<や'などがそのまま保持され、不要な HTML エスケープが入りません。- JS からは
textarea.valueを参照してクリップボードへコピーすることで、画面表示と同じ生のテキストがコピーされます。
コードレベルの変更イメージ
actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb の中で:
- 以前: エラーテキストを
<script>要素内のテキストとして埋め込んでいた。 - 以後: 同じテキストを
<textarea id="...">のvalueとして配置し、JS 側でdocument.getElementById("...").valueを参照する形へ変更。
テスト (actionpack/test/dispatch/debug_exceptions_test.rb) も更新され、「Copy as text」で取得される文字列に HTML エスケープされた文字が含まれないことを検証するようになっています。
- 影響範囲・注意点
- 影響範囲は Rails の開発・デバッグ時に表示されるエラーページ(rescue テンプレート)のみで、本番環境の挙動やアプリケーションコードには影響しません。
- 既存の「Copy as text」ボタンを利用している開発者にとっては、コピーされる内容がより正確(表示されているものと一致) になります。
- エスケープが解消されることにより、例外メッセージ内のクォートを含むコードフラグメントを、そのままエディタや issue tracker に貼り付けやすくなります。
- 内部的な実装が
<script>→<textarea>に変わっているため、もし独自に rescue テンプレートを上書きして「Copy as text」機能に依存している場合は、テンプレートの差分を確認するとよいです(ただし一般的な Rails アプリではこのカスタマイズは稀です)。
- 参考情報 (あれば)
- 変更ファイル:
actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erbactionpack/test/dispatch/debug_exceptions_test.rb
- 機能としては「例外ページの UX 改善・バグ修正」であり、アプリ側のコード変更は不要です。
- エラーページや
ActionDispatch::DebugExceptionsのカスタマイズをしている場合は、今後のバージョンアップ時にこの変更とコンフリクトしないか確認してください。
#57309 Prevent test_checkout_fairness_by_group from hanging indefinitely
マージ日: 2026/5/8 | 作成者: @bogdan
- 概要 (1-2文で)
activerecordの一部テストが CI 上で無限にハングする可能性があった問題に対して、該当テストを含む複数のテストで待機処理をタイムアウト付きにするなどのリファクタリングを行い、「落ちるときはきちんと失敗として終了する」ようにした PR です。アプリケーションコードには影響せず、テスト専用の変更です。
- 変更内容の詳細
ハングしていたテストの問題点
対象となったのは activerecord/test/cases/connection_pool_test.rb 内の test_checkout_fairness_by_group で、スレッドの完了を次のような形でポーリングしていました(イメージ):
loop do
sleep 0.1
break if success || error
endここで、何らかの理由で success / error のいずれにも到達しない場合、このループが永遠に回り続け、CI ジョブ全体がエージェント側のジョブタイムアウトまでハングしてしまう、という問題がありました。
対応内容: Timeout でループに上限を付与
このポーリングループを Timeout.timeout(5) でラップし、最大 5 秒でタイムアウトさせるようにしています。
概念的には以下のような形です:
Timeout.timeout(5) do
loop do
sleep 0.1
break if success || error
end
endこうすることで、もしテストロジックのバグや環境要因でスレッドが期待どおり完了しない場合でも、
- テストは 5 秒で
Timeout::Errorとなり - 通常のテスト失敗としてレポートされる
ようになり、CI のビルド全体が延々とハングする状況を避けられます。
テストヘルパの共通化・微修正
activerecord/test/cases/helper.rb が +14/-6 行と最も変更多くなっていることから、以下のような共通化/ユーティリティ追加が行われています:
- 複数のテストで使われている「スレッド・非同期動作を待ち合わせるためのユーティリティ」を helper に寄せる、または
- Timeout 付きの待機処理をヘルパーメソッドとして追加し、個々のテストからそれを利用するように変更
この結果として、以下のテストファイルで +1/-1 程度の小さな変更が入っています(主にヘルパの呼び出し名の変更・統一などが想定されます):
associations/belongs_to_associations_test.rbassociations/has_many_associations_test.rbassociations/has_one_associations_test.rbrelation/load_async_test.rb
変更規模から、個々のテスト内のインライン実装を「helper に切り出した共通メソッド呼び出しに置き換えた」程度の修正と考えて差し支えありません。
将来的な改善案への言及
PR 説明にある通り、「ポーリング + sleep」は本質的に不安定で遅いので、将来的には同じファイル内で既に使われている Concurrent::CountDownLatch への置き換えを検討している、というコメントがあります。
この PR ではそこまでは行わず、まずは CI のハングを解消するための最小限の Timeout 導入に留めています。
- 影響範囲・注意点
影響範囲
- Rails 本体のプロダクションコードには一切変更がなく、影響は Rails 自身のテストスイートに限定されます。
- これまで稀に「CI が止まったように見える」原因になっていた
activerecord関連テスト(特に接続プールの公平性テスト)の挙動が、ハングではなく 5 秒程度でのテスト失敗として顕在化するようになります。 - 他の association / async load 関連テストも、非同期処理の待ち方が helper を介する形に揃えられており、将来的な保守性や flakiness 改善に寄与します。
注意点
- 5 秒というタイムアウト値は CI を止めないための安全弁としては妥当ですが、環境によっては「本当に重いが正しく進行しているテスト」が稀に
Timeout::Errorになる可能性もゼロではありません。ただし、それ自体が「テストの設計が遅すぎる/脆い」サインとも言えるため、問題が顕在化したらテストの作りを見直す良いきっかけになります。 - Rails を利用するアプリ側のテストには何も影響しませんが、Rails 本体のテストを自前で回している開発者は、今後このテストがタイムアウトで落ちるようなら、接続プールまわりの挙動や環境依存の問題を疑う必要があります。
- 5 秒というタイムアウト値は CI を止めないための安全弁としては妥当ですが、環境によっては「本当に重いが正しく進行しているテスト」が稀に
- 参考情報 (あれば)
- ハングが観測されたビルドログ:
https://buildkite.com/rails/rails/builds/128041 - 類似のテスト flakiness に関する過去の issue:
https://github.com/rails/rails/pull/53591 - 非同期テストでの
sleepポーリングの代替として言及されているConcurrent::CountDownLatchはconcurrent-rubyの同期プリミティブで、特定数のイベント発生まで待機する用途に向いています。Rails テスト内でも既に別箇所で利用されています。
#57320 Simplify more activerecord tests with NotificationAssertions helpers
マージ日: 2026/5/8 | 作成者: @Saidbek
- 概要 (1-2文で)
activerecordの一部テストで、手書きしていたActiveSupport::Notificationsの subscribe/unsubscribe ロジックを、共通のNotificationAssertionsヘルパー(capture_notificationsなど)に置き換えて簡潔にした PR です。テストの重複コードを減らし、通知の検証ロジックを統一するリファクタリングであり、挙動やパブリック API の仕様変更はありません。
- 変更内容の詳細
全体方針
- 対象ファイル:
trilogy_adapter_test.rbinstrumentation_test.rbquery_cache_test.rb
- いずれも、これまで以下のようなパターンで通知を検証していました:
ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| ... end- テスト内で直接 subscribe し、ブロック内でアサーション (
assert_equal,refute, フラグのセットなど) を行う - 明示的に unsubscribe する、または
ActiveSupport::Notifications.subscribedを使う
- これを
NotificationAssertionsモジュールが提供するヘルパー(主にcapture_notifications)に統一し、- テストの本体では「どのイベントが発火したか」「ペイロード内容」をシンプルに検証するだけにする
- subscribe/unsubscribe の煩雑さやフラグ管理を隠蔽する という形に整理しています。
以下は変更の代表的なイメージです(疑似コードレベルです。実コードのメソッド名やペイロードキーはテスト内容に依存します)。
変更前の典型パターン(手動 subscribe)
def test_something_is_instrumented
asserted = false
callback = lambda do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
if event.name == "sql.active_record"
asserted = true
assert_match(/SELECT/, event.payload[:sql])
end
end
ActiveSupport::Notifications.subscribe("sql.active_record", &callback)
begin
# 実際の処理
Model.where(id: 1).load
ensure
ActiveSupport::Notifications.unsubscribe(callback)
end
assert asserted, "sql.active_record が発火していない"
end変更後の典型パターン(capture_notifications 利用)
def test_something_is_instrumented
events = capture_notifications("sql.active_record") do
# 実際の処理
Model.where(id: 1).load
end
sql_events = events.select { |e| e.payload[:sql].match?(/SELECT/) }
assert_not_empty sql_events
# あるいは最初のイベントだけを検査
assert_match(/SELECT/, sql_events.first.payload[:sql])
endポイント:
- イベントを配列として受け取る
capture_notifications("sql.active_record") { ... }が、ブロック実行中に発火した該当イベントをActiveSupport::Notifications::Eventオブジェクトの配列として返すため、- 手動で
Event.new(*args)を呼ぶ処理が不要 - assert 用のフラグ (
asserted = true) も不要
- 手動で
- フィルタリングとアサーションが明確
select/find/count等でイベント配列をフィルタリングし、その結果に対して普通のアサーションを書くスタイルになっており、テストの意図(どのイベントを何件期待するか)が読み取りやすくなっています。
trilogy_adapter_test.rb の変更
- Trilogy アダプタ(MySQL 向けアダプタ)のテストで、SQL 実行や接続に関する instrumentation が期待通り発火しているかを検証する箇所を
capture_notificationsベースに変更。 - 代表的な変更パターン:
- 以前は
ActiveSupport::Notifications.subscribe("sql.active_record")内で SQL 文・アダプタ名・バインドなどを直接検証していた。 - 今回は、テスト本体で:
events = capture_notifications("sql.active_record") { 実際のクエリ実行 }trilogy_events = events.select { |e| e.payload[:adapter] == "Trilogy" }assert_equal 1, trilogy_events.sizeなどで検証
- このスタイルに統一されています。
- 以前は
instrumentation_test.rb の変更
ActiveRecordの各種 instrumentation(例: クエリ実行、キャッシュヒット/ミス、トランザクション開始終了など)のテストにNotificationAssertionsを使用。- 以前はテストごとに subscribe ブロックを用意して、
if name == "sql.active_record"のような条件分岐を入れていましたが、今回:capture_notificationsで対象のイベント名だけをキャプチャし、- 期待される回数やペイロード内容(
payload[:name],payload[:connection_id]など)を配列経由で検証する形に変更されています。
subscribedパターン(ActiveSupport::Notifications.subscribed(callback, "sql.active_record") { ... })も同様にcapture_notificationsにまとめられています。
query_cache_test.rb の変更
- クエリキャッシュに関する通知(キャッシュヒット・ミス・書き込み等)を検証するテストを
NotificationAssertionsに移行。 - 代表的な検証例:
- 以前: subscribe して、
payload[:name]やpayload[:sql]でキャッシュヒットかどうかを判定し、即座にassert。 - 現在:
events = capture_notifications("sql.active_record") { 同じクエリを2回実行 }- 1回目のイベントでは
payload[:cached]がfalse(ミス)、2回目ではtrue(ヒット)であることをevents[0].payload[:cached]のように検証。
- 以前: subscribe して、
- これにより、キャッシュまわりの挙動が「どのタイミングでどんなイベントが出るか」という観点で読みやすくなっています。
- 影響範囲・注意点
機能・挙動への影響
- 変更はすべて
activerecord/test/...配下のテストコードのみであり、本体のライブラリコードやパブリック API の挙動には影響しません。 - したがって Rails を利用するアプリケーション側への互換性影響はありません。
- 変更はすべて
テストコードを書く側への示唆
- 今後
ActiveSupport::Notificationsを使ったテストを書く際は、基本的にNotificationAssertionsのヘルパーを使うのが推奨される流れになります。 - 既存の手書き subscribe パターンは、同様に
capture_notificationsにリファクタリングできる余地がある場合、同じスタイルに寄せるとテストの一貫性が上がります。 capture_notificationsが返すのはActiveSupport::Notifications::Eventの配列なので、ペイロードを見たい場合はevent.payload[:key]、時間情報を見たい場合はevent.time,event.end,event.durationを使えます。
- 今後
注意点
capture_notificationsは指定したイベント名(もしくは正規表現など)を対象とするため、複数のイベント名をまとめて扱いたい場合は、テスト側でフィルタリングを行う必要があります。- テストの意図として「イベントが1つも来なかったこと」を検証したい場合でも、これまでの
assertedフラグではなくevents.empty?/assert_empty events/assert_equal 0, events.sizeの形で表現されるようになります。
- 参考情報 (あれば)
- フォローアップ元 PR: #56989
- こちらで
NotificationAssertionsヘルパーが導入され、すでに一部テストが移行済み。
- こちらで
- 関連モジュール:
ActiveSupport::NotificationsActiveSupport::Notifications::EventNotificationAssertions(Rails テスト用ヘルパー。capture_notificationsなどを提供)
- 今回の PR はテストリファクタリングのみのため、
CHANGELOGなどは更新されていません。
#51244 [Fix #50118] :prepend option not working with run_after_transaction_callbacks_in_order_defined config
マージ日: 2026/5/8 | 作成者: @joshuay03
- 概要 (1-2文で)
config.active_record.run_after_transaction_callbacks_in_order_defined = trueを有効にしている場合でも、after_commit/after_rollbackに渡したprepend: trueオプションが正しく効くようにしたバグ修正です。トランザクション後コールバックの実行順に関する仕様どおりの挙動が保証されるようになりました。
- 変更内容の詳細
何が問題だったか
Active Record には以下の2つの要素があります:
- モデルに定義した順に
after_commit/after_rollbackを実行したい、という設定rubyconfig.active_record.run_after_transaction_callbacks_in_order_defined = true - 個別のコールバックを「一番最初に実行させたい」ためのオプションruby
after_commit :do_something, prepend: true
本来は、「基本は定義順だが、prepend: true を付けたものは先頭に来る」という挙動になるべきですが、この設定を有効にすると prepend: true が無視されるケースがありました(Issue #50118)。
そのため、既存アプリが prepend: true を前提としていると、Rails 7.1 以降などで実行順が変わるというバグが起きていました。
このPRでの修正内容
主に以下のポイントで修正が入っています。
1. コールバック登録ロジックの見直し
activerecord/lib/active_record/transactions.rb 内で、トランザクション後コールバック(after_commit / after_rollback)を登録・実行順序を決める処理が見直されています。
run_after_transaction_callbacks_in_order_defined = trueの場合でも、prepend: trueが指定されたコールバックは「同じ kind(commit/rollback)・同じタイミングの中で先頭」になる- それ以外のコールバックは、従来どおりモデルへ定義された順番で実行される
- つまり、「定義順」(config)と「prepend」(個別オプション)の両方を満たすように順序制御が調整されています。
実装上は、トランザクションコールバックの内部配列への挿入位置や、グルーピング/ソートの方法が変更されています(+19 / -16 行程度の変更)。
2. テスト追加・修正
activerecord/test/cases/transaction_callbacks_test.rb に以下のようなテストが追加・修正されています。
run_after_transaction_callbacks_in_order_defined = trueを有効にした状態で、- 通常の
after_commit/after_rollback prepend: trueを付けたafter_commit/after_rollback
を混在させ、実際の実行順が期待と一致することを検証。
- 通常の
イメージとしては、次のようなケースがテストされています(簡略化した疑似コード):
class Post < ActiveRecord::Base
after_commit :callback_a
after_commit :callback_b
after_commit :callback_c, prepend: true
end期待される実行順:
callback_c(prepend 指定)callback_a(定義順)callback_b(定義順)
after_rollback についても同様のテストが追加されています。
3. CHANGELOG の更新
activerecord/CHANGELOG.md に今回のバグ修正が追記され、
「run_after_transaction_callbacks_in_order_defined 設定と prepend オプションの組み合わせで起きていた不具合を修正した」旨が明示されました。
- 影響範囲・注意点
影響を受けるケース
次の両方を満たすアプリケーションは、トランザクション後コールバックの実行順が変わる可能性があります。
config.active_record.run_after_transaction_callbacks_in_order_defined = trueを設定しているafter_commitまたはafter_rollbackでprepend: trueを使っている
この PR 前までは「バグによって prepend: true が効いていなかった」ため、その壊れた挙動に依存しているコードがあれば、順序が変わり挙動が変化します。
特に注意が必要なのは:
- コールバック内で外部サービスへの通知や非同期ジョブ投入を行っていて、「どの処理が先に走るか」に依存している場合
- 親子モデルや複数レコード間で after_commit の順序が前提になっている場合
マイグレーション/設定変更の必要性
- 設定キーやメソッドの API 変更はなく、コードの書き換えは不要です。
- ただし、「
prepend: trueを付けたのに、なぜか定義順に扱われている」ようなコードがあった場合は、今回の修正により本来意図した順番(=先頭実行)に戻るため、
必要に応じてテストで実行順を確認することが推奨されます。
- 参考情報 (あれば)
- 対応 Issue:
- 関連 PR(背景となる実装変更):
- https://github.com/rails/rails/pull/46992
(run_after_transaction_callbacks_in_order_definedを導入した/挙動を変更した PR)
- https://github.com/rails/rails/pull/46992
- Issue コメント中の検証スクリプト(この PR でも検証に使用されたもの):
#57316 Reject malformed Mailgun ingress timestamps
マージ日: 2026/5/8 | 作成者: @afurm
- 概要 (1-2文で)
Mailgun 用 Action Mailbox ingress が、壊れた形式のtimestampを受け取ったときに 500 エラーになっていた問題を修正し、認証失敗(401/403 想定)として処理するようにした PR です。
HMAC 計算には従来どおり生の文字列を使い、別途パースしたタイムスタンプで時刻の妥当性チェックを行うことで、パースエラーを例外ではなく「認証失敗」として扱うようにしています。
- 変更内容の詳細
問題点
既存実装では、Mailgun からのリクエストに含まれる timestamp を以下のように扱っていました:
- HMAC 検証用の入力として
timestamp(文字列)を使用 - 同時に
Integer(timestamp)で数値に変換し、リクエストの「古さ(recency)」をチェック
このとき timestamp が不正な文字列(例: "abc", 空文字, フォーマット不良など)の場合、Integer(timestamp) が ArgumentError を送出し、認証ロジックの「結果として false を返す」のではなく、サーバ内部エラー(500)として落ちる挙動になっていました。
本来は「不正な署名 or 不正な timestamp」として 401/403 を返したいケースです。
修正内容
PR の要点は次の 2 点です。
HMAC 用の timestamp と、時刻チェック用の timestamp を分離
- HMAC 検証には 生の
timestamp文字列をそのまま使う(従来どおり)。 - 時刻の妥当性チェック用には、別変数にパースした timestamp(整数)を使う。
- HMAC 検証には 生の
timestamp のパース時に例外を投げず、認証失敗として扱う
Integer(timestamp, exception: false)を用いてパース。- パースに失敗した場合は
nilが返るため、これを検知して 「認証失敗」として扱う 。 - その結果、401/403(Action Mailbox の既存挙動)として「拒否」され、500 にはならない。
概念的なイメージコードは以下のようになります(実際のコードの簡略版):
# 変更前(イメージ)
def authenticate_request
raw_timestamp = params[:timestamp]
token = params[:token]
signature = params[:signature]
# ここで壊れた timestamp だと ArgumentError が発生して 500 エラーとなる
timestamp = Integer(raw_timestamp)
hmac_valid = valid_signature?(raw_timestamp, token, signature)
recent = recent_enough?(timestamp)
hmac_valid && recent
end
# 変更後(イメージ)
def authenticate_request
raw_timestamp = params[:timestamp]
token = params[:token]
signature = params[:signature]
# HMAC 入力には raw_timestamp をそのまま利用(Mailgun 仕様どおり)
hmac_valid = valid_signature?(raw_timestamp, token, signature)
# recency チェック用に別途 timestamp をパース
parsed_timestamp = Integer(raw_timestamp, exception: false)
# パースに失敗(nil)した場合は recency チェック NG として扱う
recent = parsed_timestamp && recent_enough?(parsed_timestamp)
hmac_valid && recent
endテスト追加
actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb にテストが追加されています(+18行)。
追加されたテストでは:
timestampに不正な値(数値化できない文字列など)を渡した場合に- サーバが 500 を返さないこと
- リクエストが正しく拒否されること(ステータスコード・レスポンス内容) を検証していると考えられます。
また、PR 説明にある通り、以下で検証済みです:
cd actionmailbox && bin/test test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
cd actionmailbox && bin/test
bundle exec rubocop actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
git diff --check- 影響範囲・注意点
- 対象は Mailgun ingress を使っている Rails アプリ です(
ActionMailbox::Ingresses::Mailgun::InboundEmailsController)。 - これまで:
timestampが壊れているリクエスト → 500 エラーになっていた可能性がある。
- 変更後:
- 同じリクエストは 認証失敗として扱われ、401/403(または 4xx)で拒否される挙動に変わる。
- これにより、攻撃的・誤ったリクエストでアプリが 500 を返すことを防げる。
注意点・補足:
- HMAC 計算時の入力(
timestampの生文字列)は従来の仕様のままなので、正しくフォーマットされた Mailgun リクエストへの互換性は保たれる想定です。 - Mailgun 側が公式に指定する署名検証手順は「生の
timestamp文字列+token+署名」で HMAC を取る方式なので、この PR はその仕様に忠実です。 - もしアプリ側で「Mailgun ingress への 500 エラー増加」を監視していた場合、本修正導入後は 4xx に変わるため、メトリクスやアラート設定に影響する可能性があります。
- 参考情報 (あれば)
- 対応する Issue: #57315
→ 「Mailgun ingress が壊れた timestamp で 500 を返してしまう」旨の報告と考えられます。 - Mailgun 署名検証の仕様(公式ドキュメント)
- 概要:
HMAC-SHA256(api_key, timestamp + token)を計算し、signatureと比較。 - ここでの
timestampは文字列(Mailgun が送ってくるまま)で扱うのが前提。
- 概要:
- 対象ファイル:
actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rbactionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
#57174 Update EnvConfiguration class doc examples and drop stale commented require
マージ日: 2026/5/8 | 作成者: @Saidbek
- 概要 (1-2文で)
EnvConfiguration クラスの RDoc (ドキュメントコメント) に書かれているoption/envifyのサンプルコードが、実際の挙動に即した正しい ENV キー・fetchの使い方になるよう更新されました。あわせて、不要になっていたコメントアウトされたrequire行が削除されています。
- 変更内容の詳細
※ この PR は実装ロジックではなく「ドキュメント (RDoc) のサンプルコード」の修正です。
- 対象ファイル:
activesupport/lib/active_support/env_configuration.rb - 主な変更点:
EnvConfigurationクラスの doc コメント内にあるoptionとenvifyの使用例が修正された。- ENV から値を取る際のキー名が、実際の
envifyが生成するキーと一致するように修正。 fetchの呼び方(引数の形、デフォルト値の渡し方など)が、実際に使われているインターフェイスに沿う形に更新。
- ENV から値を取る際のキー名が、実際の
- コメントアウトされた古い
require行(既に不要だが残骸として残っていたもの)が削除された(+2/-4 行という小さな差分のうち、これが一部を占める)。
EnvConfiguration はだいたい次のような役割のクラスです:
option :foo, default: ...といった形で設定項目を定義するenvifyによって、ENV から値を取ってきてConfig.fooなどに反映するための仕組みを提供する
この PR では、その説明用の RDoc で:
ENV["SOME_KEY"]のキー名が間違っている/不自然ENV.fetchの引数の与え方が、実際のenvifyのインターフェイスとズレている
といった「サンプルコードとして見たときに誤解を招く」部分が修正されています。
そのため、ドキュメントを見ながら EnvConfiguration と envify を使う開発者が、正しい ENV キーおよび fetch の使い方をそのままコピペできるようになっています。
- 影響範囲・注意点
- ランタイム挙動への影響: なし
- 変更は doc コメントとコメントアウトコードの削除のみで、クラスの実装は一切変わっていません。
- 既存アプリケーションが壊れる可能性: なし
- 注意点:
- これまで古い RDoc の例をそのままコピペして使っていた場合、今回の PR によって「どこが本来の正しい使い方なのか」が明確になります。
envify利用時に ENV キー名やfetchの使い方で迷っていた場合は、最新のドキュメントを見直す価値があります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57174
- 対象クラス:
ActiveSupport::EnvConfiguration - Rails 公式ガイド(英語・一般的な貢献ルール):
https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation
#57321 Fix running AbstractMySQL tests when testing Trilogy and Mysql2
マージ日: 2026/5/8 | 作成者: @byroot
- 概要 (1-2文で)
Active Record の MySQL 抽象テスト (AbstractMySQLテスト群) を、Trilogy アダプタと Mysql2 アダプタの両方で正しく実行できるように、テスト用のサポートコード (activerecord/test/support/tools.rb) が修正されています。主にテスト実行時のアダプタ切り替えロジック(またはアダプタ判定・ロード周り)の不備を解消する小さな変更です。
- 変更内容の詳細
※このPRは activerecord/test/support/tools.rb に+7/-2行の変更のみで、説明文もないため、Rails の既存構造とPRタイトルから推測を含めて解説します。
背景
Active Record のテストスイートには、MySQL 向けの共通テスト(AbstractMySQL)があり、
- Mysql2 アダプタ
- Trilogy アダプタ(Shopify 発の MySQL 互換アダプタ)
など複数のアダプタに対して同じテストを流すための仕組みがあります。
しかし、現状では:
- 特定のアダプタでしか動かない前提でツールが書かれていた
- もしくは、アダプタを切り替えてテストを実行するための指定・検出ロジックに不整合があった
といった理由で、「AbstractMySQL テストを Trilogy と Mysql2 の両方で回す」ことがうまくいっていなかったと考えられます。
activerecord/test/support/tools.rb の修正内容の方向性
このファイルには、テストスイートを実行するための補助メソッドや、アダプタ・DB設定の切り替えロジックなどが定義されています。
PR タイトルから推定すると、以下のような変更が行われた可能性が高いです(代表的なパターンを列挙):
アダプタ名・ENV 変数の扱い修正
例:
ENV["ARCONN"]や独自の環境変数経由でどのアダプタでテストを走らせるかを指定しているが、"trilogy""mysql2"
など両者を正しく識別できていなかった、あるいは AbstractMySQL 用の共通パスに乗らなかった問題を修正。
典型的にはこんなロジックの修正が入りがちです:
ruby# 修正前(例) if ENV["ARCONN"] == "mysql2" # MySQL 用テストを有効化 end # 修正後(例) if %w[mysql2 trilogy].include?(ENV["ARCONN"]) # MySQL 抽象テストを両アダプタで有効化 end
AbstractMySQL テストのスキップ条件を整理
- 例えば「MySQL 系アダプタ以外ならスキップ」という判定が mysql2 だけを想定しており、Trilogy だと誤ってスキップされていた可能性があります。
- それを「MySQL 互換アダプタのグループ」として扱うように変更した、など。
テストランナーのユーティリティに、アダプタ毎の実行パターンを追加
- 例:
run_abstract_mysql_tests_for(adapter)のようなヘルパがあり、Trilogy 用にも呼び出せるようになった、あるいはマルチアダプタ対応の DSL が整備された、など。
- 例:
実際の差分は数行レベルのため、概ね「アダプタの判定条件を拡張した」「テスト実行時に AbstractMySQL テストが意図したアダプタの設定で走るようにした」といった小規模なロジック修正と考えるのが妥当です。
- 影響範囲・注意点
影響範囲
- Rails 自体のランタイム動作・アプリケーションコードには影響せず、Active Record のテストスイートのみに影響する変更です。
- 特に、MySQL 関連の共通テスト(AbstractMySQL)を、複数アダプタ(Trilogy / Mysql2)で回す際のテスト環境構築・実行方法に影響します。
- Rails コアのCIや、自前で Rails 本体をテストしている開発者(フレームワークコントリビュータ)が主な対象です。
注意点
- 自前で Rails 本体のテストスイートを回していて、
ARCONN=mysql2やARCONN=trilogyなどでアダプタを切り替えている場合、- 今後は 同じ AbstractMySQL テストが両方のアダプタに対して適切に実行される ようになります。
- もし、これまで「mysql2 のときだけテストが動く」前提のスクリプトやCI設定を組んでいた場合は、Trilogy に切り替えたときの挙動も合わせて確認しておくと安全です。
- 一部のアダプタ特有の制約や未対応機能が AbstractMySQL テストで表面化する可能性があるため、Trilogy アダプタの開発・検証をしている場合は、新たに失敗するテストが出ていないかに注意してください。
- 自前で Rails 本体のテストスイートを回していて、
- 参考情報 (あれば)
- 対象PR: https://github.com/rails/rails/pull/57321
- 関連しうるもの:
- Active Record のアダプタ毎テストについては
activerecord/test以下の各アダプタ用テスト (mysql2,trilogyなど) やabstract_mysql系テストクラスを参照すると、どのテストが共通化されているか確認できます。 - Trilogy アダプタ自体の仕様・互換性は、Trilogy のリポジトリや関連PR(Trilogy サポート追加系の PR)を見ると理解しやすいです。
- Active Record のアダプタ毎テストについては
#57304 [ci skip] Fix getting started guide: remove autocomplete="off" from authenticity_token example
マージ日: 2026/5/7 | 作成者: @ryu
- 概要 (1-2文で)
Rails の Getting Started ガイド内にあるフォームのサンプルコードから、authenticity_token用の hidden フィールドに付いていたautocomplete="off"を削除し、実際の Rails 8.1.0 以降の出力に合わせてドキュメントを修正した PRです。機能追加や挙動変更ではなく、ドキュメントの追随・整合性を取るための変更です。
- 変更内容の詳細
対象ファイル:
guides/source/getting_started.md
変更前(ガイド内の HTML サンプル):
<input type="hidden" name="authenticity_token" value="..." autocomplete="off">変更後:
<input type="hidden" name="authenticity_token" value="...">背景となる仕様/挙動:
- Rails 8.1.0 から、
token_tagなどが生成する hidden フィールドにはautocomplete="off"が自動的には付与されなくなりました。 - これは #53512 の変更により、HTML 仕様上
input type="hidden"にautocomplete属性は適用されないため削除されたものです。 - 今回の PR は、その実装変更(#53512)に合わせて Getting Started ガイドのサンプルコードを修正し、現行の Rails が実際に生成する HTML と一致させるものです。
- タイトルに
[ci skip]が付いており、CI を走らせないドキュメントのみの微修正として扱われています。
- 影響範囲・注意点
- 実装コードやランタイムの挙動は一切変更されておらず、「ガイドのサンプルが最新の出力に追いついた」だけです。
- 既存アプリで、hidden フィールド(特に authenticity_token)に
autocomplete="off"を付けている場合:- HTML 仕様上意味のない属性であり、ブラウザの挙動には基本的に影響しませんが、不要な属性として削除しても問題ないことが改めて確認できます。
- ドキュメントを見ながらフォームを手書きしている場合:
- 旧ガイドを参照していると
autocomplete="off"を付けてしまうかもしれませんが、付ける必要はありません。 - Rails ヘルパ(
form_withなど)を使っていれば、Rails 8.1.0 以降は最初から付かないため、ガイドと実際の出力が食い違うことがこの PR で解消されています。
- 旧ガイドを参照していると
- 参考情報 (あれば)
- 関連 PR: #53512 – Remove autocomplete="off" from hidden inputs
- 対応する Rails バージョン: 8.1.0 以降
- HTML 仕様:
input type="hidden"はユーザーの入力対象ではないため、autocomplete属性は意味を持たず、仕様上も対象外とされています。
#57266 Replace Europe/Kiev with Europe/Kyiv and Asia/Rangoon with Asia/Yangon
マージ日: 2026/5/7 | 作成者: @tsymbalenkovlad
- 概要 (1-2文で)
Rails のタイムゾーンマッピングから廃止された IANA タイムゾーン名「Europe/Kiev」「Asia/Rangoon」を使うのをやめ、「Europe/Kyiv」「Asia/Yangon」に置き換える PR です。既存の古い名称ではActiveSupport::TimeZoneが解決できない問題へのフォローアップ対応です。
- 変更内容の詳細(あればサンプルコードも含めて)
変更対象ファイル
activesupport/lib/active_support/values/time_zone.rb
ActiveSupport::TimeZone::MAPPING(Rails 独自のタイムゾーン名と IANA タイムゾーン名の対応表)の中で、以下のような変更が行われています(概念的なイメージ):
# 変更前(イメージ)
MAPPING = {
# ...
"Kyiv" => "Europe/Kiev",
"Yangon" => "Asia/Rangoon",
# ...
}
# 変更後(イメージ)
MAPPING = {
# ...
"Kyiv" => "Europe/Kyiv",
"Yangon" => "Asia/Yangon",
# ...
}ポイント:
- 参照している IANA タイムゾーン名を「非推奨・廃止された別名」から「現在の正式名称」に置き換え。
- 実際の差分は +2/-2 行で、純粋に文字列を置き換えているだけです。
背景
PR の Motivation にある通り、この PR は以前の PR(https://github.com/rails/rails/pull/55494)で行われたタイムゾーンまわりの整理のフォローアップです。
作者が提示しているチェック方法:
ActiveSupport::TimeZone::MAPPING.keys.select { ActiveSupport::TimeZone.new(it).nil? }このコードで、
MAPPINGに登録されているキーからActiveSupport::TimeZone.newで解決できないもの(= 実際には有効でない/廃止された IANA 名を参照しているもの) を洗い出し、そのうち「Europe/Kiev」「Asia/Rangoon」を修正した、という位置づけです。
- 影響範囲・注意点
影響範囲
ActiveSupport::TimeZoneを通じて「Kyiv」「Yangon」タイムゾーンを利用しているアプリケーション。- 例:
Time.zone = "Kyiv"やTime.zone = "Yangon"、config.time_zone = "Kyiv"など。
- 例:
- 内部的に対応する IANA 名が
Europe/Kiev→Europe/Kyiv、Asia/Rangoon→Asia/Yangonに切り替わります。
通常、IANA 側では古い名前はエイリアス(リンク)として扱われることが多いため、動作上の違いはほぼありませんが、「新しい正式名に追随しておく」ことで以下のようなメリットがあります。
- 将来、OS / tzdata の更新で古いエイリアスが削除・変更されても安全になりやすい。
- ログや設定画面に表示されるタイムゾーン名を、現在の公式名称に揃えられる。
注意点
- アプリケーションコード上で「Europe/Kiev」「Asia/Rangoon」という IANA 名を“直接”ハードコードしている場合、この PR 自体はそれらを変更しません(あくまで
ActiveSupport::TimeZone::MAPPINGの更新だけ)。- そのため、Rails アップグレードと合わせて、必要に応じてアプリ側でも IANA 名を正式名称へリネームしていくことが推奨されます。
- テストなどで
"Europe/Kiev"を前提とした文字列比較をしている場合は、正式名称に合わせる方向で見直した方がよいです。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/57266
- 関連 PR(フォローアップ元): https://github.com/rails/rails/pull/55494
- IANA Time Zone Database (tzdata) では以下のように名称が更新・整理されています:
Europe/Kiev→Europe/KyivAsia/Rangoon→Asia/Yangon
#57297 Document Active Storage routes_prefix scope options
マージ日: 2026/5/7 | 作成者: @Ivanov-Anton
- 概要 (1-2文で)
Active Storage のconfig.active_storage.routes_prefixが、単なるパス文字列だけでなくscopeが受け付けるあらゆるオプション(ハッシュなど)を受け取れることを公式ドキュメントに明記した PR です。これにより、サブドメイン制約などを含む柔軟なルーティング設定が「サポートされていること」がハッキリしました。
- 変更内容の詳細
何がドキュメント化されたか
Active Storage のルート定義は内部的に以下のようになっています:
scope ActiveStorage.routes_prefix do
# Active Storage のエンジンルートがここにマウントされる
endここで ActiveStorage.routes_prefix は、これまで主に「パスプレフィックスを表す文字列」として紹介されていましたが、実際には scope が受け取れる任意の値(文字列またはハッシュ)を渡せます。
この PR では:
activestorage/lib/active_storage.rbActiveStorage.routes_prefixの API コメントを更新し、「文字列だけでなくscopeが受け付けるルーティングオプション全般を渡せる」ことを説明
- ガイドの更新
guides/source/configuring.mdconfig.active_storage.routes_prefixの説明に「文字列 or ハッシュを渡せる」ことを追記- 単なる
/filesのようなパス指定だけでなく、scopeのオプションをそのまま渡せるという説明を追加
guides/source/active_storage_overview.md- Active Storage のルーティングについての節に、上記と整合する説明と具体例を追加
想定されるサンプルコード
PR の本文には具体例コードは書かれていませんが、scope の仕様と今回の説明から、以下のような設定が正式に「OK」と明示された形になります。
1) 従来通りの単純なパスプレフィックス
# config/application.rb または各環境の config
config.active_storage.routes_prefix = '/files'内部的には:
scope '/files' do
# Active Storage ルート
end2) サブドメイン制約つき + パスプレフィックス
config.active_storage.routes_prefix = {
path: '/files',
constraints: { subdomain: 'assets' }
}内部的に:
scope path: '/files', constraints: { subdomain: 'assets' } do
# Active Storage ルート
end3) サブドメイン制約のみ(パスはデフォルト)
config.active_storage.routes_prefix = {
constraints: { subdomain: 'assets' }
}など、scope にそのまま渡せるオプション構造であればよい、ということがドキュメント上も明示されました。
- 影響範囲・注意点
影響範囲
- コード(挙動)の変更はなく、ドキュメントと API コメントのみの更新です。
- すでに
config.active_storage.routes_prefixにハッシュを渡して運用していたアプリは、今回の PR によってその使い方が「公式にドキュメント化された」形になります。 - まだハッシュ指定を利用していなかったアプリでも、Active Storage のエンドポイントをサブドメインや追加の
constraints付きで切り出す設計を検討しやすくなります。
注意点
- 実際に利用するオプションは
scopeの仕様に従う必要があります(例:path:,constraints:,defaults:,module:など)。 routes_prefixにハッシュを渡した場合、そのままscopeに渡されるため、他のルーティングとの競合・優先度には通常のscopeと同じ注意が必要です。- 動作自体は以前から可能だったものなので、Rails をこの PR を含むバージョンに上げることで急に挙動が変わることはありません。
- 実際に利用するオプションは
- 参考情報 (あれば)
- 対応 Issue: #48915
- マージされた PR: #57297 – Document Active Storage routes_prefix scope options
- 関連ドキュメント(更新対象):
- Configuring Rails Applications ガイドの
config.active_storage.routes_prefix節 - Active Storage Overview ガイドのルーティング解説節
- Configuring Rails Applications ガイドの
scopeのオプション一覧はActionDispatch::Routing::Mapper#scopeの API ドキュメントを参照すると具体的な指定方法を確認できます。
#57122 railties: fix misleading production cache store comment
マージ日: 2026/5/7 | 作成者: @wwenrr
- 概要 (1-2文で)
Rails が生成するconfig/environments/production.rbのコメント文が、実際のデフォルトキャッシュストア設定と食い違っていた問題を修正する PR です。実装は変えず、コメントだけを「メモリストア前提」から「ファイルストア前提」に合わせています。
- 変更内容の詳細
何が問題だったか
Rails::Application::Configurationのデフォルト:ruby@cache_store = [ :file_store, "#{root}/tmp/cache/" ]→ デフォルトはファイルベースのキャッシュストア(
:file_store)。一方、アプリ生成時テンプレートの
config/environments/production.rb.ttに書かれていたコメント:ruby# Replace the default in-process memory cache store with a durable alternative.→ あたかも「デフォルトはプロセス内メモリキャッシュ」であるかのような書き方で、実際の挙動と矛盾していた。
どう直したか
対象ファイル:
railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt(1ファイルのみの変更)
コメントを以下のように修正:
Before:
# Replace the default in-process memory cache store with a durable alternative.After:
# Replace the default file-based cache store with a more robust alternative.コードそのもの(@cache_store のデフォルト値など)は一切変更されておらず、説明文のみが実態に揃えられています。
- 影響範囲・注意点
影響範囲
- 新しく
rails newなどでアプリを生成する際のconfig/environments/production.rbに埋め込まれるコメントが変わるだけです。 - 実際のデフォルトキャッシュストア設定(
:file_store)やランタイム挙動には影響ありません。 - 既存アプリの設定ファイルはこの PR では自動変更されないため、既に生成済みの
production.rbのコメントはそのままです。
- 新しく
注意点 / 開発者視点での読み替え
- これまで「production ではデフォルトがメモリキャッシュ」と誤解してコメントを読んでいた場合、実際には:
- デフォルト:
:file_store(tmp/cache配下) - メモリ常駐ではなくファイルシステム依存
- デフォルト:
- 本番環境でスケールアウトする場合やパフォーマンス・信頼性を求める場合、コメントの通り、次のようなストアへの移行が推奨されます:ruby
config.cache_store = :mem_cache_store, "cache.example.com" # もしくは config.cache_store = :redis_cache_store, { url: ENV.fetch("REDIS_URL") } - 「default が何か」に依存したドキュメントや教育資料を書いている場合、この PR で示された事実(ファイルストアがデフォルト)と整合しているか注意するとよいです。
- これまで「production ではデフォルトがメモリキャッシュ」と誤解してコメントを読んでいた場合、実際には:
- 参考情報 (あれば)
- この PR がクローズした Issue: #56089
- 関連コード:
- デフォルト設定:
railties/lib/rails/application/configuration.rbの@cache_store初期化部 - テンプレート:
railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
- デフォルト設定:
- デフォルトキャッシュストアと選択肢に関しては公式ガイド「Caching with Rails: An Overview」も参照すると、
file_store,mem_cache_store,redis_cache_storeなどの使い分けを確認できます。
#57317 Ractor-safe class attribute implementation
マージ日: 2026/5/7 | 作成者: @jhawthorn
- 概要 (1-2文で)
Rails のclass_attribute実装が Ractor セーフになるように内部実装が変更され、Ractor でクラス属性を安全に扱えるようになりました。性能劣化は 10% 未満に抑えられつつ、singleton class に対してinstance_readerが誤って定義される既存バグも修正されています。
- 変更内容の詳細
2-1. 従来の仕組み (問題点)
従来の class_attribute は、継承チェーンに応じた値解決を以下のような形で実装していました。
__class_attr__nameのようなメソッドをdefine_methodで定義し、- そのメソッド内の「クロージャに閉じ込めた値」を返す
- クラス階層で値が変わるたびに、この
define_methodでメソッド自体を再定義していた
- この「クロージャに値を閉じ込める」実装は Ractor セーフではありません
(Ractor 間で共有する Proc/メソッドに、Ractor セーフでないオブジェクトを閉じ込める可能性があるため)
そのため、Ractor から安全に class_attribute を利用できない、あるいは将来的な Ractor 対応の妨げになっていました。
2-2. 新しい仕組みのポイント
この PR では、値そのものではなく「オーナークラス」を返すメソッドに役割を分離することで、Ractor セーフを達成しています。
変更前
__class_attr__name- クロージャに値を持ち、呼び出すと値を返す
- クラスごとにこのメソッドを再定義していた
変更後
__class_attr__name_owner- 「どのクラスが現在の値のオーナーか」を返すメソッド
- 値そのものではなくクラスオブジェクトを返す
- クラス階層でオーナーが変わる都度、このメソッドだけを再定義
attr_reader :__class_attr__name- 実際の値はインスタンス変数 +
attr_readerで管理 - こちらは再定義不要
- 実際の値はインスタンス変数 +
Ruby のクラスオブジェクトは Ractor-shareable なので、
__class_attr__name_ownerを返す Proc / メソッドは Ractor で共有可能- 実際の値へは、そのクラスのインスタンス変数を通してアクセスすることで、
- 値が shareable なら全 Ractor から参照可能
- shareable でない場合は Ruby の通常の Ractor ivar セマンティクス (基本的にはメイン Ractor) に従う
という、Ruby 本体の Ractor セマンティクスに則った挙動になります。
イメージコード
概念的には、class_attribute :setting があると以下のような構造になります(擬似コード):
class Base
# オーナークラスを返すメソッド(継承関係で変化)
def self.__class_attr__setting_owner
Base
end
# 実際の値を持つインスタンス変数への reader(再定義不要)
class << self
attr_reader :__class_attr__setting
end
# class_attribute :setting の public API
def self.setting
__class_attr__setting_owner.__class_attr__setting
end
def self.setting=(value)
@__class_attr__setting = value
# owner の付け替えなどは内部でやる
end
end- 継承先クラスで
Sub.setting = :worldのようにオーバーライドしたときは、Sub.__class_attr__setting_ownerのみ Sub 用に再定義- 実際の値は
@__class_attr__settingを Sub クラス側に保持
この分割により、Ractor 間で共有されるのは「どのクラスがオーナーか」という情報だけになり、値自体を Proc/クロージャに閉じ込める必要がなくなっています。
2-3. instance_reader のバグ修正
PR 説明にある通り、以下のようなケースでバグがありました:
- singleton class(
class << obj; self; endのようなクラス)に対してclass_attributeを定義すると、instance_readerが常に定義されてしまっていた
想定としては:
class << some_object
class_attribute :foo, instance_accessor: false
endのようなケースではインスタンスリーダーは定義されるべきではありませんが、従来は 無条件に定義されていた 模様です。
この PR で、singleton class に対しても instance_reader の有無がオプションどおりに振る舞うように修正されています。
2-4. パフォーマンス
ベンチマークの結果:
- クラスメソッド読み出し (
Base.setting,Sub.setting,SubSub.setting)- おおむね 2~3% 程度の低下 (約 50M i/s → 約 49M i/s)
- インスタンスメソッド経由での読み出し (
obj_base.setting,obj_sub.setting)- むしろわずかに向上 (約 42.4M i/s → 約 42.8M i/s)
全体として 10% 未満の差に収まっており、実務的にはほぼ気にならないレベルのオーバーヘッドです。
- 影響範囲・注意点
対象
ActiveSupport::ClassAttributeおよびcore_ext/class/attributeを利用する全てのコードが対象class_attribute,mattr_accessor等を通じてクラス属性を使っているコードは、内部実装がこの新方式に切り替わります
挙動の互換性
- API レベルでは互換性が保たれており、通常の使い方で挙動が変わるケースは想定されていません
- ただし、
__class_attr__*系の内部メソッドやインスタンス変数に直接触れている場合は挙動が変わっています- そのようなコードは本来非推奨な内部依存なので、今回の変更を機に見直した方が良いです
Ractor 利用時の注意
- class attribute の値が Ractor-shareable でないオブジェクト(例: 通常の
Array,Hash,String)の場合、- その値を別 Ractor から直接参照することはできません
- これは Ruby の Ractor 仕様そのものであり、今回の変更でより明示的にその制約に沿うようになった形です
- Ractor 間で読み取りたいクラス属性には、
freezeされたオブジェクトや純データ構造(Ractor-shareable なオブジェクト)を使うのが安全です- 例:
default: [1, 2, 3].freeze
- 例:
- class attribute の値が Ractor-shareable でないオブジェクト(例: 通常の
singleton class +
class_attributeを使っている場合- 従来「たまたま」インスタンスリーダーが定義されていて依存していた場合、今回のバグ修正でそのメソッドがなくなる可能性があります
- そのようなコードがないかを確認し、必要であれば
instance_accessor: trueなどを明示することを推奨します
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/57317
- 関連ファイル:
activesupport/lib/active_support/class_attribute.rbactivesupport/lib/active_support/core_ext/class/attribute.rb- テスト:
activesupport/test/core_ext/class/attribute_test.rb
- Ractor と shareable オブジェクトについて:
- Ruby 公式ドキュメント: https://docs.ruby-lang.org/en/master/doc/ractor_md.html (Ractor, shareable objects)
#57318 Add datalist to FormBuilder
マージ日: 2026/5/7 | 作成者: @tahsin352
- 概要 (1-2文で)
FormBuilder に HTML の<datalist>要素を生成するdatalistメソッドが追加され、form_with/form_forからオートコンプリート付きテキスト入力を素直な記述で扱えるようになりました。text_fieldとの連携用にidが自動生成され、list:オプションで簡単に紐付けられます。
- 変更内容の詳細
新しく追加された機能
ActionView::Helpers::FormOptionsHelper に datalist メソッドが追加され、FormBuilder から以下のように使えるようになりました。
<%= form_with model: @post do |f| %>
<%# text_field 側で list: に datalist の id を指定 %>
<%= f.text_field :country, list: f.field_id(:country, :datalist) %>
<%= f.datalist :country, ["Argentina", "Brazil", "Chile"] %>
<% end %>上記は、次のような HTML を生成します。
<input list="post_country_datalist" type="text"
name="post[country]" id="post_country" />
<datalist id="post_country_datalist">
<option value="Argentina">Argentina</option>
<option value="Brazil">Brazil</option>
<option value="Chile">Chile</option>
</datalist>ID の自動生成仕様
- FormBuilder のオブジェクト名と属性名から、
<datalist>用のidが自動生成されます。- 例:
form_with model: @postかつ:countryの場合、id="post_country_datalist"
- 例:
text_field側は、list:オプションでこのidを参照する必要があります。- 推奨パターンは
list: f.field_id(:country, :datalist)のようにfield_idを使う形です
(:datalistサフィックスを付けた ID を生成)。
- 推奨パターンは
実装面のポイント(推測される仕様)
PR で触れられている範囲から読み取れる点:
FormOptionsHelperに新メソッドdatalist(method, options_array, **html_options)相当が追加。methodからidを決定- 第二引数の配列から
<option>要素を列挙
- テスト (
form_options_helper_test.rb) が追加され、少なくとも以下を確認していると考えられます:- 自動生成される
idのフォーマット - 渡した配列が
<option>のvalueと本文両方に反映される - フォームビルダを通した出力が期待どおりの HTML になる
- 自動生成される
CHANGELOG には「FormBuilder に datalist を追加した」旨のエントリが追記されています。
- 影響範囲・注意点
影響範囲
ActionViewの FormBuilder API が拡張される「追加機能」なので、既存コードは基本的に非互換影響を受けません。form_options_helper周辺に手を入れているため、カスタム FormBuilder を作っている場合は、同名メソッドの競合にだけ注意が必要です。
注意点 / 使い方上の落とし穴
<datalist>はあくまで「ブラウザの補完候補」であり、バリデーションや制約にはならない点は HTML の仕様どおりです。サーバ側での検証は別途必要です。- オートコンプリートを有効にするには 必ず
text_field側のlist:に<datalist>のidを指定 する必要があります。- IDs の手入力はヒューマンエラーを生むので、PR で示されているように
f.field_id(:country, :datalist)で生成するのが安全です。
- IDs の手入力はヒューマンエラーを生むので、PR で示されているように
- モデル・属性名に応じた ID 生成ルール(
object_name_attribute_datalist)に依存するため、field_idを使わずに自前でlist:を書く場合は、このフォーマットを守る必要があります。
- 参考情報 (あれば)
- 追加機能の該当ファイル:
actionview/lib/action_view/helpers/form_options_helper.rb- テスト:
actionview/test/template/form_options_helper_test.rb - 変更履歴:
actionview/CHANGELOG.md
- HTML
<datalist>の仕様 (MDN):
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalist
#56807 Rails.cache.read: add support to new option delete: true
マージ日: 2026/5/7 | 作成者: @glaucocustodio
- 概要 (1-2文で)
RedisCacheStoreのRails.cache.readにdelete: trueオプションが追加され、Redis のGETDELコマンドを使って「読み出しと削除」をアトミックに行えるようになりました。これにより「一度だけ読めればよい」ワンタイム読み取りパターンをレースコンディションなしで実装できます。
- 変更内容の詳細(サンプルコード含む)
新オプション delete: true
対象: RedisCacheStore を使っている場合の Rails.cache.read(内部的には read_serialized_entry)
# 従来
value = Rails.cache.read("one_time_token")
Rails.cache.delete("one_time_token") # ここにレースコンディションの可能性
# 今回の変更後
value = Rails.cache.read("one_time_token", delete: true)
# Redis の GETDEL で value を返しつつ key を同時に削除read_serialized_entry(key, **options)がdelete:キーワード引数を受け取るようになった。delete: trueのとき:- Redis クライアント呼び出しが
c.get(key)→c.getdel(key)に切り替わる。 - 1回の Redis コマンドで「値の取得」と「キー削除」を実行。
- Redis クライアント呼び出しが
delete: false(デフォルト)もしくは未指定の場合の挙動は従来どおりGET。
ローカルキャッシュ層 (local_cache strategy) の変更
RedisCacheStore は local_cache strategy と組み合わせて使うことができます(Rails.cache.fetch 時のプロセスメモリ内キャッシュなど)。
今回の変更では delete: true 指定時のローカルキャッシュとの整合性を担保するために、以下のような挙動になります。
delete: trueの場合:- ローカルキャッシュをバイパスして、必ず Redis に対して
GETDELを実行する
→ ローカルに古い値が残っていても、それを返さない。 - 対象キーのローカルキャッシュエントリを削除
→ Redis 側で削除された状態と揃える。
- ローカルキャッシュをバイパスして、必ず Redis に対して
これにより、「プロセス内キャッシュにだけ値が残り続ける」「GETDEL で消えたはずなのにローカルから読める」といった不整合を防いでいます。
テストでカバーされているケース
activesupport/test/cache/stores/redis_cache_store_test.rb で以下が確認されています。
- 基本的な「読み取り + 削除」動作
read(..., delete: true)で値が返り、その後同じキーを読み取るとnilになる。
- 存在しないキー
delete: trueでも単にnilが返る(余計なエラーは出ない)。
raw: trueな値の取り扱い- 期限切れ(expired)エントリの挙動
- Redis 側でキーが実際に削除されていることの確認
- ローカルキャッシュとの連携
- ローカルキャッシュが登録されていても
delete: true時は Redis を必ず見ること - 読み取り + 削除後にローカルキャッシュ側のエントリも消えていること
- ローカルキャッシュが登録されていても
- 影響範囲・注意点
対象ストア:
delete: trueの実質的な意味を持つのはRedisCacheStoreのみ。- 他のストアはこのオプションを受け取っても 無視 される(
**options経由で素通りする設計)。
Redis バージョン要件:
- 内部で使用する
GETDELは Redis 6.2+ で導入されたコマンド。 - それより古い Redis を使っている場合:
- 実運用では
GETDELが存在せずエラーになる可能性があるため、Redis バージョンに注意が必要。 - PR本文でも「古いバージョンをケアすべきか?」という問いがあり、少なくともドキュメント/運用面での周知が前提になりそう。
- 実運用では
- 内部で使用する
互換性(BC):
deleteオプションは新規追加で、デフォルトはfalse。- 既存コード(
delete: trueを指定していないもの)の挙動は変わらない。 local_cachestrategy の挙動のうち、delete: trueのケースのみが新たに定義されているため、既存のread/fetchには影響しない。
ユースケース:
- ワンタイムトークン・一度きりのジョブペイロード・一回だけ表示するメッセージなど、 「確実に一度しか読まれない」ことが重要なデータの取り扱いに向く。
- これまで
read→deleteの2ステップで起きていたレースコンディション(他プロセスが同じ値を二重に読む可能性)を防げる。
- 参考情報 (あれば)
- Redis
GETDELコマンド仕様:
https://redis.io/docs/latest/commands/getdel/ - 追加されたオプションは
RedisCacheStore固有機能のため、マルチストア構成や抽象レイヤとしてRails.cacheを利用している場合は、裏側のストアによって挙動が変わることに留意してください。
#57310 fix: has_json accessors after requiring active_model
マージ日: 2026/5/7 | 作成者: @tellodaniel
- 概要 (1-2文で)
ActiveModel::SchematizedJsonが内部でString#removeを使っているにもかかわらず、対応する Active Support の拡張を require していなかった問題を修正した PRです。has_jsonアクセサを使うとNoMethodErrorが出るケースを、依存関係を明示的に require することで解消しています。
- 変更内容の詳細
問題点
ActiveModel::SchematizedJson内部でString#removeを呼び出している。String#removeはactive_support/core_ext/string/filtersで定義される Active Support のコア拡張メソッド。- しかし
active_model/schematized_jsonからこのファイルを require しておらず、
「他のどこかでたまたま Active Support の String 拡張が読み込まれている」ことを前提に動いていた。 - そのため、純粋に Active Model だけを読み込んで
has_jsonを使うと、String#removeが定義されておらずNoMethodErrorが発生する。
再現コード(PR説明より)
bundle exec ruby -Iactivesupport/lib -Iactivemodel/lib -e '
require "active_model";
account = Class.new {
include ActiveModel::Attributes
include ActiveModel::SchematizedJson
attribute :settings
has_json :settings, restricts_access: true
}.new
p account.settings.restricts_access
'上記のように、Active Record などをロードせず、ActiveModel だけで動かそうとしたときに落ちる状況です。
修正内容
変更ファイル:
activemodel/lib/active_model/schematized_json.rb
+ require "active_support/core_ext/string/filters"実質的にはこの 1 行だけの変更で、ActiveModel::SchematizedJson が自前で String#remove を提供する Active Support の拡張を明示的に読み込むようになりました。
これにより:
ActiveModel::SchematizedJsonを単体で使うhas_jsonアクセサを呼ぶ
という最小構成でも、NoMethodError が発生せずに正常に動作するようになります。
- 影響範囲・注意点
影響範囲
- 直接の影響:
ActiveModel::SchematizedJsonとhas_jsonを使っているコード。 - 特に、Active Record や Rails 全体をロードせず、Active Model 単体で使っているスクリプト/ライブラリで恩恵が大きいです。
- 既に Rails フルスタックで動かしているアプリでは、もともと Active Support の String 拡張が読み込まれていることが多いため、挙動上の変化はほぼありません(バグが顕在化していなかったケース)。
- 直接の影響:
後方互換性
- require が 1 行増えただけで、既存の API 変更や挙動変更はありません。
- 既存コードが壊れるリスクは極めて低く、実質バグ修正のみです。
テスト
- PR 説明では「スタンドアロンの Active Model スクリプト」と「Active Model のテストスイート」で動作確認済みとあります。
- 追加テストは書かれていませんが、挙動としては依存の明示化のみなので、リスクは小さいと考えられます。
- 参考情報 (あれば)
- 関連クラス/モジュール:
ActiveModel::SchematizedJsonActiveModel::Attributesactive_support/core_ext/string/filters(String#removeなど String のフィルタ系拡張を提供)
- この PR は「暗黙的な Active Support 依存を顕在化して、Active Model を単体利用しやすくする」という性質の修正であり、今後も同様の暗黙依存を減らしていく方向性が示唆されます。
#57312 Fix changelog url
マージ日: 2026/5/7 | 作成者: @morgoth
概要 (1-2文で)
このPRは、Action View の CHANGELOG に記載されていた URL の誤った変更を元に戻し、正しいリンク先に修正するものです。コード本体の挙動には影響せず、ドキュメント上のリンク整備のみを行っています。変更内容の詳細
- 対象ファイル:
actionview/CHANGELOG.md - 変更点: 1行分の URL を「意図せず変更されてしまった状態」から元の正しい URL に差し戻しています。
- 具体的には、以前の PR(#52137)で誤って変更された changelog 内のリンクが、今回の PR で正しい URL に修正されています。
サンプル(イメージ):
- See the changelog at https://example.com/wrong/path
+ See the changelog at https://example.com/correct/path※ 実際の URL は PR 本体を参照してください。
- 影響範囲・注意点
- ランタイムの挙動・ビルド結果・API 仕様などには一切影響しません。
- 影響範囲はドキュメント(
actionview/CHANGELOG.md)内のリンク参照のみです。 - CHANGELOG から関連情報や diff を辿る際に、正しい URL にアクセスできるようになります。
- アプリケーション側で特別な対応は不要です。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/57312
- 元になった不要変更を含む PR: https://github.com/rails/rails/pull/52137/changes
- 対象コンポーネント: Action View (
actionviewgem)
#57311 Accept Active Record Encryption credentials as ENV
マージ日: 2026/5/7 | 作成者: @claudiob
- 概要 (1-2文で)
Active Record Encryption で必要なprimary_key,deterministic_key,key_derivation_saltを、Rails.apps.credsに加えて環境変数からも設定できるようになりました。これにより、Rails の資格情報ファイルを使わずに、12-factor 的な運用(環境変数ベース)で暗号化キーを扱いやすくなります。
- 変更内容の詳細
新たにサポートされた環境変数
ActiveRecord::Encryption が参照するキーを、以下の環境変数で与えられるようになりました:
ACTIVE_RECORD_ENCRYPTION__PRIMARY_KEYACTIVE_RECORD_ENCRYPTION__DETERMINISTIC_KEYACTIVE_RECORD_ENCRYPTION__KEY_DERIVATION_SALT
これらは、もともと config.active_record.encryption.primary_key などで設定していた値と同等の役割を持ちますが、Rails 資格情報 (config/credentials.yml.enc など) や Rails.apps.creds に加えて、ENV 経由でも指定できるようになった、という位置づけです。
Rails.apps.creds との連携
この PR は、先行して導入された Rails.apps.creds (#56455) を前提にしています。Rails.apps.creds は、アプリケーションの「資格情報」的な値を一元的に扱う API で、この PR ではそこから Active Record Encryption のキーを解決できるようにしたうえで、「同じ値を ENV からも渡せる」ようにした、という流れです。
内部的には、ActiveRecord::Railtie 内で暗号化設定を初期化する処理が変更されており、設定値の解決順序に「ENV による上書き」が組み込まれています(差分は +3/-3 行程度の軽微な変更)。
ガイドの更新
guides/source/active_record_encryption.md では、暗号化キーの設定方法の説明が更新され、Rails.apps.creds と環境変数の利用パターンが反映されています。
以前は「credentials 経由でこう書く」といった説明が中心だったものが、Rails.apps.creds + ENV という新しい標準的な書き方を前提に整理されています(行数としては削除 9 行 / 追加 3 行程度の軽微なドキュメント修正)。
テストの追加
railties/test/application/creds_test.rbrailties/test/application/envs_test.rb
にそれぞれ 1〜2 行ほどテストが追加され、Rails アプリケーション起動時に ENV 経由で Active Record Encryption のキーが正しく認識されることが検証されています。
- 影響範囲・注意点
環境変数でキーを渡せるようになった
Docker / Kubernetes / Heroku など、環境変数ベースでシークレットを注入する運用にフィットします。これまで credentials でしか管理していなかったキーを、必要に応じてシークレットマネージャ + ENV という構成に移行しやすくなります。優先順位・競合への注意
実装詳細は PR 本体を確認する必要がありますが、一般的には「明示的設定 > creds > ENV」といった優先順位になることが多く、config.active_record.encryption.primary_keyを直接設定Rails.apps.credsで設定- 上記に加えて ENV も設定
といった状況では、どれが最終的に使われるかを意識する必要があります。特に移行期間中「credentials にも ENV にも同じキーを書いている」状態だと、将来の変更時にどちらが効いているか分かりにくくなるので、できるだけ片方に寄せるのが無難です。
キーの取り扱いは依然として慎重に
暗号化キーを ENV に出せるようになったとはいえ、- シェル履歴に残さない
- CI ログ/アプリログに出さない
- 本番と検証環境でのキーの分離
といったセキュリティ上の配慮は引き続き必要です。特にPRIMARY_KEYなどを単一キーで複数環境に使い回すと、漏洩時の影響範囲が広くなります。
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/57311
Rails.apps.credsを導入した PR: https://github.com/rails/rails/pull/56455- ガイド:
guides/source/active_record_encryption.md(Active Record Encryption の全体像や設定方法)
#52137 Add datalist_tag to create datalist form elements
マージ日: 2026/5/6 | 作成者: @willianveiga
- 概要 (1-2文で)
Rails のActionView::Helpers::FormTagHelperに、HTML の<datalist>要素を簡潔に生成するためのdatalist_tagヘルパーが追加されました。これにより<option>の配列からdatalistを組み立てる処理を、素のcontent_tag連打なしで記述できます。
- 変更内容の詳細
新ヘルパー: datalist_tag
ActionView::Helpers::FormTagHelper に datalist_tag が追加されています。
基本的な呼び出しイメージは PR に記載の通りです。
datalist_tag(
'countries_datalist',
[
'Argentina',
['Brazil', { class: 'brazilian_option' }],
['Chile', 'CL', { disabled: true }]
],
{ class: 'sa-countries-sample' }
)
# => <datalist id="countries_datalist" class="sa-countries-sample">
# <option value="Argentina">Argentina</option>
# <option value="Brazil" class="brazilian_option">Brazil</option>
# <option value="CL" disabled="disabled">Chile</option>
# </datalist>引数の構造(推定仕様)
※テストコードから読み取れる典型的な Rails のヘルパー設計に基づく説明です。
第1引数:
id(または全体の HTML オプションに流し込まれる識別子)- 上記例では
"countries_datalist"が<datalist id="countries_datalist">となる。
- 上記例では
第2引数:
<option>を表す配列- 各要素は以下のいずれかの形式:
"Argentina"形式(文字列のみ)valueと表示テキストが同じになる:html<option value="Argentina">Argentina</option>
['Brazil', { class: 'brazilian_option' }]形式- 第1要素 …
value兼 表示テキスト - 第2要素 … HTML オプション(class, data-*, disabled など)
- 第1要素 …
['Chile', 'CL', { disabled: true }]形式- 第1要素 … 表示テキスト
- 第2要素 …
value属性の値 - 第3要素 … HTML オプション
- 各要素は以下のいずれかの形式:
第3引数:
<datalist>自体に付与する HTML オプション- 例:
{ class: 'sa-countries-sample' }→<datalist class="sa-countries-sample">
- 例:
従来の記述との比較
これまで <datalist> を Rails で生成するには、以下のように content_tag をネストする必要がありました:
<%= content_tag(:datalist, nil, id: :payees_datalist, 'data-autocomplete-input-target' => :datalist) do %>
<% @payees.each do |payee| %>
<%= content_tag(
:option,
nil,
value: payee.name,
data: { id: payee.id },
'data-autocomplete-input-target' => 'datalistOption'
) %>
<% end %>
<% end %>datalist_tag を使うと、概ね次のようにシンプルな記述で済ませられます(イメージ):
<%= datalist_tag(
:payees_datalist,
@payees.map { |p| [p.name, { data: { id: p.id }, 'data-autocomplete-input-target': 'datalistOption' }] },
'data-autocomplete-input-target': :datalist
)
%>実装まわり
- 追加ファイル・変更:
actionview/lib/action_view/helpers/form_tag_helper.rbdatalist_tagメソッドが追加。- 内部で
content_tag(:datalist, ...)とcontent_tag(:option, ...)を組み合わせて出力していると考えられます。
actionview/test/template/form_tag_helper_test.rbdatalist_tagに対するテストが追加され、引数バリエーション・HTML オプション付与などがカバーされている。
actionview/CHANGELOG.md- 新機能として
datalist_tagが追記。
- 新機能として
- 影響範囲・注意点
- 互換性:
- 既存アプリへの破壊的変更はなく、新規ヘルパーの追加のみ。
- 既存のフォームヘルパーには影響しない。
- 利用上の注意:
options配列の各要素の書き方(1要素/2要素/3要素)で、 「text と value のどちらを何にするか」が変わる点に注意。"foo"→<option value="foo">foo</option>["foo", "bar"]→<option value="bar">foo</option>(である可能性が高い)["foo", { ... }]→<option value="foo" ...>foo</option>
- 既存の
options_for_selectなどと似たインターフェイスになるはずなので、混同しないように(どちらも<option>を作るが、datalist_tagは<datalist>向けに最適化されている)。
- ブラウザ側仕様:
<datalist>は<input list="...">と組み合わせて使う HTML5 機能。- 対応ブラウザでのみ補完候補として機能するため、レガシーブラウザを対象にする場合は別途フォールバック(JS オートコンプリートなど)が必要。
- 参考情報 (あれば)
<datalist>MDN ドキュメント(仕様確認用)
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/datalistRails の類似 API(使い方・引数構造の参考)
options_for_selectselect_tag
datalist を多用するオートコンプリート UI を Rails で作る場合、標準ヘルパーとして datalist_tag を使えるようになったことで、ビューコードの可読性と一貫性が向上します。
#49368 Action View: Document TagBuilder as part of the public interface
マージ日: 2026/5/6 | 作成者: @seanpdoyle
- 概要 (1-2文で)
ActionView のtagヘルパで内部的に使われているActionView::Helpers::TagHelper::TagBuilderを「公式な公開API」として位置づけ直し、APIドキュメント検索からも辿れるようにした PR です。あわせてTagBuilderの責務を整理し、一部のメソッドを@view_contextに委譲するようにして公開インターフェイスを絞り込んでいます。
- 変更内容の詳細
2-1. TagBuilder を公開APIとして明示
- これまで
TagBuilderクラス自体に:nodoc:が付いており、「内部実装」扱いで公式ドキュメントに出てきませんでした。 - しかし実際には
tagヘルパの説明の中でtag.attributestag.div,tag.h1などの動的メソッド
が言及されており、利用が事実上想定されているクラスでした。
- そこで今回:
- クラス自体は「Public API」としてドキュメント上可視にする
- ただし、全メソッドをベタに公開するとインターフェイスが広すぎるため、メソッドには
:nodoc:を付けてドキュメント上の露出を抑える という折衷案が取られています。
2-2. RubyDoc コメントを追加して検索可能に
目的は「ユーザが API ドキュメント上で tag.attributes などを検索した時に、きちんとヒットさせること」です。
そのために:
#attributesメソッドに RubyDoc コメントを追加tag.div,tag.aなど、method_missing経由で生成されるタグビルダーメソッドについても、RubyDoc 上で説明できるようなコメントを追加
これにより:
tag.attributesで属性ハッシュを HTML 属性文字列へ変換する方法tag.div,tag.span,tag.aなどの使い方
が、Rails API ドキュメント上で検索しやすくなります。
想定される利用イメージ
# attributes の利用例
tag_builder = view_context.tag
tag_builder.attributes(class: "btn", data: { controller: "modal" })
# => "class=\"btn\" data-controller=\"modal\""
# 動的タグメソッドの利用例
tag.div("Hello", class: "greeting")
# => "<div class=\"greeting\">Hello</div>"
tag.a("リンク", href: "/path", data: { turbo: false })
# => "<a href=\"/path\" data-turbo=\"false\">リンク</a>"tag オブジェクトの実態が TagBuilder であり、そのメソッドや挙動がドキュメントから追いやすくなります。
2-3. CaptureHelper / OutputSafetyHelper をクラスから切り離し委譲
これまで TagBuilder は以下のモジュールを include していました:
ActionView::Helpers::CaptureHelperActionView::Helpers::OutputSafetyHelper
これにより TagBuilder 自身が capture 系・HTML セーフティ系のメソッドを多数持っている状態でしたが、これらは本来「ビューコンテキストの責務」であり、TagBuilder が直接の公開インターフェイスとして持つべきかは微妙でした。
今回の変更では:
TagBuilderから上記モジュールの include を削除- その代わりに、これらに相当する機能が必要な箇所は内部の
@view_contextに委譲するよう実装を変更
という整理が行われています。
イメージ
# 変更前(イメージ)
class TagBuilder
include CaptureHelper
include OutputSafetyHelper
def some_helper(...)
# self の capture, safe_join などを直接呼ぶ
end
end
# 変更後(イメージ)
class TagBuilder
def initialize(view_context)
@view_context = view_context
end
def some_helper(...)
# @view_context.capture, @view_context.safe_join などに委譲
end
endこれにより:
TagBuilderの「表向きの」メソッド一覧は減り、API としての表面積が小さくなる- 実際の振る舞い(出力のキャプチャや HTML セーフティ)は、今までどおり
view_contextによって提供されるので、既存の動作は維持される
- 影響範囲・注意点
3-1. 公開APIとしての安定性向上
TagBuilderが公的に認められた API になることで、tagヘルパとそのメソッドの仕様変更がより慎重になります。- アプリや gem で
tag.attributesやtag.divなどを明示的に使っている場合も、「内部実装に依存している」という不安はやや軽減されます。
3-2. TagBuilder に対するメタプログラミング依存は要注意
- クラス自体は公開されますが、多くのメソッドが
:nodoc:で非推奨レベルに近い扱い(少なくとも「積極的にはドキュメント化しない」)となっています。 - もしアプリ側で
TagBuilderに対するinclude/prependTagBuilderの private/protected メソッドの利用TagBuilderがCaptureHelper/OutputSafetyHelperを直接 include していることへの依存 などをしている場合、今回のようなリファクタリングで壊れる可能性があります。
- 公開APIとして期待できるのは主に:
tagオブジェクトとしての一般的なタグ生成メソッド (tag.div,tag.a, …)tag.attributesなどであり、それ以外への踏み込みは今後も破壊的変更リスクがあります。
3-3. 互換性
- 外部から見える典型的な使い方(
tag.div,tag.span,tag.attributesなど)は挙動維持が前提でテストも追加・更新されています。 TagBuilderのインクルードモジュール構成に依存したコードがある場合は挙動確認が必要です。
- 参考情報 (あれば)
- Rails API ドキュメント
- PR 本体(#49368)がマージされていれば、今後の Rails バージョンでは
TagBuilderのクラス定義とコメントを API ドキュメント上で確認できるようになります。
#56384 Add ActionController::Parameters#fetch_values for multi-key fetching
マージ日: 2026/5/6 | 作成者: @Saidbek
- 概要 (1-2文で)
ActionController::Parametersに、複数キーを一度に必須パラメータとして取得できる#fetch_valuesが追加されました。Ruby のHash#fetch_valuesに近いインターフェイスで、複数パラメータ取得時のコードを簡潔に書けるようになります。
- 変更内容の詳細
新メソッド: ActionController::Parameters#fetch_values
params.fetch_values(:key1, :key2, ...) という形で、複数のキーを一度に取得できます。
基本的な挙動は Ruby の Hash#fetch_values に倣いつつ、Rails のストロングパラメータ仕様 (fetch と同じく ActionController::ParameterMissing を出す) に準拠しています。
基本的な使い方
params = ActionController::Parameters.new(name: "Francesco", age: 22)
params.fetch_values(:name, :age)
# => ["Francesco", 22]戻り値は配列で、指定したキー順に値が並びます。
パラメータが欠けている場合
指定したキーのいずれかが存在しない・空・無効な場合には、既存の fetch と同様に ActionController::ParameterMissing が発生します。
params = ActionController::Parameters.new(name: "Francesco", age: 22)
params.fetch_values(:name, :none)
# => ActionController::ParameterMissing:
# param is missing or the value is empty or invalid: noneエラーメッセージも既存の params.fetch(:none) と同じ形式です。
ブロック付き呼び出し
Ruby の Hash#fetch_values 同様、ブロックを渡すと キーが見つからなかった場合のフォールバック値 を指定できます。
見つかったキーは通常どおりの値、見つからないキーはブロックの戻り値が使われます。
params = ActionController::Parameters.new(name: "Francesco", age: 22)
params.fetch_values(:name, :none) { |key| key }
# => ["Francesco", :none]この例では :none キーが無いため、ブロックが呼ばれ、key シンボル自体が値として使われています。
実装上のポイント(推測される仕様)
- 内部的には
#fetch相当のロジックをキーごとに適用し、結果を配列に積む形。 - ブロックが渡されていない場合、どれか 1 つでも欠けていれば即
ParameterMissingを raise。 - ブロックが渡されている場合は、欠けたキーごとにブロックを呼び、例外は出さない。
代表的な利用イメージ
コントローラ内での「複数の必須パラメータ」の取得が簡潔になります。
class UsersController < ApplicationController
def create
name, email, age = user_params.fetch_values(:name, :email, :age)
# name, email, age を安全に利用できる
end
private
def user_params
params.require(:user).permit(:name, :email, :age)
end
endフォールバックを与えたい場合:
name, nickname = user_params.fetch_values(:name, :nickname) { |key|
key == :nickname ? "anonymous" : nil
}- 影響範囲・注意点
影響範囲
ActionController::Parametersの public API が 1 つ増えただけで、既存メソッドの挙動変更はありません。- 既存コードには互換性の影響はほぼありません(名前の衝突がない限り後方互換)。
注意点
- 単一キーなら
params.fetch(:key)との機能差はありませんが、複数キーを取る場合は#fetch_valuesの方が簡潔で、例外メッセージやバリデーション挙動もfetchと同等になるよう設計されています。 - ブロックを渡した場合は 原則として
ParameterMissingが出ない 想定なので、「存在していなければ例外で落としたい」というケースではブロックを渡さないこと。 permit前のパラメータに対してfetch_valuesを使うと、Strong Parameters のフィルタリングが効かない点は従来どおりです。必須パラメータチェックとストロングパラメータの適用順序には注意してください。
- 単一キーなら
- 参考情報 (あれば)
- 元になった Ruby コアのメソッド:
- 既存の類似 API:
ActionController::Parameters#fetchActionController::Parameters#requireActionController::Parameters#permit
#57290 Fix image analyzer reporting wrong dimensions for mirrored EXIF orientations
マージ日: 2026/5/6 | 作成者: @bogdan
- 概要 (1-2文で)
Active Storage の画像アナライザ(Vips / ImageMagick)が、EXIF のミラー(反転)系オリエンテーションを持つ画像に対して width/height を誤って入れ替えて記録していたバグを修正する PR です。90°/270° 回転を含む 4 種類の EXIF orientation(5, 6, 7, 8)の場合だけ幅・高さを入れ替えるようにロジックを修正しています。
- 変更内容の詳細
問題の背景・バグの内容
Active Storage では、画像のメタデータ解析時に rotated_image? というメソッドを使い、EXIF orientation に応じて「縦横を入れ替える必要があるか」を判定しています。この判定は Vips / ImageMagick 両方のアナライザで使われています。
本来、EXIF orientation のうち「90°または270°の回転を含むもの」だけが、見かけ上の向き(portrait/landscape)が入れ替わるため、width と height をスワップする必要があります。
しかし従来コードでは、以下の2つの誤りがありました:
誤ってスワップしていた:
- 2: TopRight(水平反転のみ)
- 4: BottomLeft(垂直反転のみ)
→ これらは回転なしの「反転のみ」なので、見かけ上の縦横は変わらず、本来はスワップしてはいけない。
本来スワップすべきなのにスワップしていなかった:
- 5: LeftTop(反転 + 270°回転)
- 7: RightBottom(反転 + 90°回転)
→ これらは 90°/270° 回転を含むため、portrait/landscape が入れ替わるにもかかわらず、従来ロジックではスワップ対象に入っていなかった。
この結果、ミラー系の EXIF orientation を持つ画像について、blob.metadata 上の width/height が逆転して記録されるケースがありました。たとえば、実際は横長画像なのに width < height となって portrait だと誤認される、などです。
なお、このバグは メタデータ上の width/height のみに影響し、サムネイルや variant の生成(実際の画像の回転処理自体)は vips の auto_rot により正しく行われていたため、出力画像の見た目には影響しません。
具体的な修正内容
方針rotated_image? が「スワップすべき orientation だけを true にする」ように整理し、90°/270°回転を含む 4 種類に限定しました:
- 5 (
Left-top) : Flip + rotate 270° CW - 6 (
Right-top): Rotate 90° CW - 7 (
Right-bottom): Flip + rotate 90° CW - 8 (
Left-bottom): Rotate 270° CW
結果として、EXIF orientation ごとの挙動はこうなります:
| EXIF | 変換内容 | width/height スワップ | 変更前 | 変更後 |
|---|---|---|---|---|
| 2 | 水平反転 | しない | ○(誤) | ×(正) |
| 4 | 垂直反転 | しない | ○(誤) | ×(正) |
| 5 | 反転 + 270°回転 | する | ×(誤) | ○(正) |
| 6 | 90°回転 | する | ○(正) | ○(正) |
| 7 | 反転 + 90°回転 | する | ×(誤) | ○(正) |
| 8 | 270°回転 | する | ○(正) | ○(正) |
コード上の変更 (概念的なイメージ)
Vips と ImageMagick 双方のアナライザで、rotated_image? の条件が修正されています。イメージとしては:
# 変更前(例:一部の mirrored も含めていた)
def rotated_image?(orientation)
[2, 4, 6, 8].include?(orientation)
end
# 変更後(90°/270°回転を含む4つに限定)
def rotated_image?(orientation)
[5, 6, 7, 8].include?(orientation)
end※実際の実装とはメソッド名・定数名などが異なる場合がありますが、ロジックの意図は上記の通りです。
テスト・フィクスチャの追加
- 新たに 2 種類のテスト用 JPEG を追加:
racecar_mirrored.jpg→ EXIF orientation 2racecar_mirrored_rotated.jpg→ EXIF orientation 5
- これらを使い、Vips / ImageMagick の両方のアナライザテストを追加:
- orientation 2: 以前のコードだと width/height が誤って入れ替わる → 修正後は入れ替わらないことを検証
- orientation 5: 以前のコードだと入れ替わらない → 修正後は入れ替わることを検証
- これにより、未修正コードではテストが fail し、修正後に pass することが確認されます。
CHANGELOG の更新
activestorage/CHANGELOG.mdに、このバグ修正についてのエントリが追加されています。
→ ライブラリ利用者がリリースノートから挙動変更を把握できるようになっています。
- 影響範囲・注意点
- 影響するのは Active Storage の
blob.metadata[:width]/[:height]の値のみです。- サムネイル/variant のピクセル自体はもともと正しく回転されており、この PR での変更はありません。
- 過去にアップロード済みの blob については、既に保存されているメタデータは自動では修正されません。
- orientation 2, 4, 5, 7 を使った画像が大量にあり、そのメタデータを正したい場合は、任意で再解析処理を走らせる必要があります(例:
ActiveStorage::Blob.find_each { |blob| blob.analyze if blob.image? }など、環境に応じて)。
- orientation 2, 4, 5, 7 を使った画像が大量にあり、そのメタデータを正したい場合は、任意で再解析処理を走らせる必要があります(例:
- これに依存するアプリ側のロジック(例:
width > heightかどうかでレイアウトを変えるなど)は、- 以前は誤った判定をしていた可能性があり、
- この修正を含む Rails へのアップデート後は正しい判定になるため、挙動が変わる可能性があります。
- この変更はあくまでバグ修正であり、新しい API 追加や破壊的なインターフェース変更はありません。
- ただし「以前のバグ依存の挙動」を前提にしていたコードがある場合は注意が必要です。
- 参考情報 (あれば)
- 関連 Issue/PR:
- #41940: EXIF orientation 周りの以前の議論/不具合
- #42005: mirrored orientation を扱うようにしようとして今回のバグを導入してしまった PR
- EXIF orientation 一覧(一般的な資料):
- 1: Normal
- 2: Mirror horizontal
- 3: Rotate 180°
- 4: Mirror vertical
- 5: Mirror horizontal + rotate 270° CW
- 6: Rotate 90° CW
- 7: Mirror horizontal + rotate 90° CW
- 8: Rotate 270° CW
→ このうち、画面上で縦横が入れ替わるのは 5, 6, 7, 8 の 4 種類であり、今回の修正はこの定義に合わせたものです。
#57271 Gracefully handle nil during multi-parameter assignment
マージ日: 2026/5/5 | 作成者: @seanpdoyle
- 概要 (1-2文で)
マルチパラメータ属性(例:Dateカラム)の代入時に、これまで""のときだけ正常に「値なし」として扱われていた挙動を、nilに対しても例外なく同様に扱えるようにした修正です。nilを渡した際に発生していたNoMethodError: undefined method 'empty?' for nilを解消します。
- 変更内容の詳細
何が問題だったか
Rails のマルチパラメータ属性("last_read(1i)", "last_read(2i)", "last_read(3i)" のような形式のキー)に対して、値として空文字列 "" を渡すと「値なし」として扱われていました:
topic = Topic.where.not(last_read: nil).first
topic.attributes = {
"last_read(1i)" => "",
"last_read(2i)" => "",
"last_read(3i)" => "",
}
topic.last_read # => nil一方で、同じ状況で "" を nil に置き換えると、内部実装で value.empty? のようなチェックをしていたため、NilClass に empty? が実装されておらず NoMethodError が発生していました:
topic = Topic.where.not(last_read: nil).first
topic.attributes = {
"last_read(1i)" => nil,
"last_read(2i)" => nil,
"last_read(3i)" => nil,
}
# => NoMethodError: undefined method `empty?' for nil:NilClass何をどう直したか
activerecord/lib/active_record/attribute_assignment.rb のマルチパラメータ処理部分で、
- これまで:
value.empty?で「空」を判定していた - これから:
value.blank?で判定する
という変更が行われました(実質 1 行の差し替え)。
blank? は Rails 拡張であり、
String#blank?は内部でempty?などを使って空文字・空白文字列を判定NilClass#blank?も定義されていてtrueを返す
ため、"" だけでなく nil に対しても同じように「値なし」という扱いができます。
この修正後は次のように動作します:
topic = Topic.where.not(last_read: nil).first
topic.attributes = {
"last_read(1i)" => nil,
"last_read(2i)" => nil,
"last_read(3i)" => nil,
}
topic.last_read # => nil (例外は発生しない)テストとドキュメント
activerecord/test/cases/multiparameter_attributes_test.rbに、nilを渡したケースが正しくnilに設定され、例外が出ないことを確認するテストが追加されています。activerecord/CHANGELOG.mdに、この挙動変更(バグフィックス)が追記されています。
- 影響範囲・注意点
対象となる機能
- Active Record のマルチパラメータ属性(
"column(1i)","column(2i)","column(3i)"など)を使った代入ロジック全般が対象です。典型的にはフォームからのDate/Time/DateTime入力のパラメータ処理で利用されます。
- Active Record のマルチパラメータ属性(
アプリケーションへの実質的な影響
- これまで「
nilを渡すと例外が出る」ために、ワークアラウンドとしてnilを""に変換していた箇所があれば、その必要がなくなります。 ""を「値なし」として扱っていた挙動は維持されつつ、nilも同様に「値なし」として安全に扱われるようになります。blank?を使うことで" "(空白のみの文字列)なども「空」とみなされる点は、従来のempty?との違いになり得ますが、フォーム入力的には通常期待される挙動です(※既存コードでも多くの場合blank?を利用しているので、違和感は少ないはずです)。
- これまで「
後方互換性の観点
- これまで
nilによって意図的に例外を発生させていた、というような特殊な利用をしていない限り、挙動は「エラーにならなくなる」という改善として受け取れます。 - もしマルチパラメータ用の値に、
" "(スペースのみ)を意図的に「非空」として扱いたいような非常にニッチなケースがあれば、今回のblank?への変更で「空」と見なされるようになる可能性があります。
- これまで
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57271
- 対象メソッド関連:
- String#empty?(Ruby 標準)
https://ruby-doc.org/3.4.1/String.html#method-i-empty-3F - NilClass(Rails API)
https://edgeapi.rubyonrails.org/classes/NilClass.html - String#blank?(Rails 拡張)
https://edgeapi.rubyonrails.org/classes/String.html#method-i-blank-3F - NilClass#blank?(Rails 拡張)
https://edgeapi.rubyonrails.org/classes/NilClass.html#method-i-blank-3F
- String#empty?(Ruby 標準)
#50889 Introduce ActiveSupport::TestCase.around
マージ日: 2026/5/5 | 作成者: @seanpdoyle
- 概要 (1-2文で)
Rails のActiveSupport::TestCaseにaroundコールバック(フック)が追加され、各テストのsetup〜teardownを1つの「囲い込み」処理として扱えるようになりました。これにより、RSpec などで馴染みのある「around フック」を Rails 標準のテストでもシンプルに使えるようになります。
- 変更内容の詳細
2-1. ActiveSupport::TestCase.around が追加
ActiveSupport::TestCase に around コールバックが導入され、以下の順序で実行される処理全体を 1 つのフックで囲めます。
setup- 実際のテストメソッド (
test "..." do ... end) teardown
PR の説明にある通り、ブロックには テストクラスのインスタンス (test_case) と実行ブロック (&block) が渡されます。
class MyTest < ActiveSupport::TestCase
around do |test_case, &block|
# 例: あるクライアントをスタブした状態でテストを実行
Client.with(stubbed: true, &block)
end
def setup
# 通常の setup
end
test "something" do
# この中身が Client.with(stubbed: true) の中で実行される
end
def teardown
# 通常の teardown
end
end上記の around ブロックは、内部的には
around do |test_case, &block|
# setup → test → teardown を表す「ひとかたまりの block」が渡される
Client.with(stubbed: true) do
block.call
end
endというイメージで動作します。
2-2. 実装位置
実装は activesupport/lib/active_support/testing/setup_and_teardown.rb に追加されています。ActiveSupport::TestCase が持つ既存の setup / teardown に続く、テスト用コールバック機構の一部として around が組み込まれました。
Rails には既に Controller / Job / Mailer などで利用されている Active Support のコールバックシステムがあるため、それと同じ仕組み・記法で around を利用できます。
2-3. テストとファイル構成の変更
- 新規テスト:
activesupport/test/testing/around_callback_test.rbaroundコールバックの挙動(順序・ネスト・複数定義など)がテストされています。 - 既存の teardown 周りのテストファイル名を整理するため、
after_teardown_test.rbがcallbacks_test.rbにリネームされています(PR 説明にあるが、この diff ではリネームというより around 用テスト追加がメイン)。
2-4. 典型的な利用例
- 一時的なコンテキストや接続の差し替え:
class ApiClientTest < ActiveSupport::TestCase
around do |test_case, &block|
ApiClient.with_base_url("https://stubbed.example.com") do
block.call
end
end
end- グローバル設定の一時変更:
class FeatureFlagTest < ActiveSupport::TestCase
around do |test_case, &block|
old_value = Feature.enabled?(:new_ui)
Feature.enable!(:new_ui)
block.call
ensure
Feature.set!(:new_ui, old_value)
end
end- トランザクション・外部リソースのラップ(※DB は Rails が標準でトランザクションフィクスチャを持つので、別リソース向けなどに向いている):
class ExternalServiceTest < ActiveSupport::TestCase
around do |test_case, &block|
ExternalService.transaction do
block.call
raise ActiveRecord::Rollback
end
end
end- 影響範囲・注意点
- 対象:
ActiveSupport::TestCaseを継承する Rails 標準のテスト(モデルテスト、コントローラテスト、システムテストなど)。
ただし、Minitest::Testを直接継承しているテストには影響しません。 - 既存の
setup/teardownとの関係:aroundは「setup + test本体 + teardownの外側」を囲むイメージです。- したがって、
around内でblock.callを実行しないと、setup/ テスト本体 /teardownが一切実行されません。
- 複数の around の組み合わせ:
- 複数の
aroundを定義した場合、コールバック順序(LIFO / FIFO)やネストのされ方は Active Support の一般的なコールバックと同様です。 - 内外の順番を意識しないと、想定と違う順序で環境のセットアップ・クリーンアップが行われる可能性があります。
- 複数の
- minitest-hooks との競合:
既にminitest-hooksを使ってaroundを導入している場合、ActiveSupport::TestCase側のaroundと競合する可能性があります。
Rails 標準のaroundに移行する場合は、二重適用・メソッド名衝突などに注意してください。 - 実行コスト・複雑さ:
aroundはテスト全体をラップできるため便利な一方で、ロジックが重いものを入れるとテスト全体のパフォーマンス悪化や理解コスト上昇につながります。
簡潔なスコープ制御・設定/復元処理に留めるのが望ましいです。
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/50889
- Minitest が
aroundをサポートしない経緯: - 既存の around フック利用例に近いもの(別ライブラリ):
- minitest-hooks: https://github.com/jeremyevans/minitest-hooks
- Active Support コールバックの一般的な仕組み:
- Rails Guides: Active Support Callbacks(英語版)
https://guides.rubyonrails.org/active_support_callbacks.html
- Rails Guides: Active Support Callbacks(英語版)
#50320 Tag Builders: render keywords as dasherized HTML attributes
マージ日: 2026/5/5 | 作成者: @seanpdoyle
- 概要 (1-2文で)
Rails のtagヘルパー等で、属性にネストした Hash を渡したときに、キーを「ダッシュ区切り(dasherize)した HTML 属性名」として展開して出力できるようになりました。これにより、Stimulus/Turbo のdata-*だけでなく、Alpine.js のx-*や htmx のhx-*形式の属性も、Ruby 側で自然なハッシュ記法のまま生成しやすくなっています。
- 変更内容の詳細(サンプルコードを含めて)
2-1. ネストした Hash を HTML 属性に展開する挙動の追加
これまでの Rails では、data: については data: { controller: "..." } → data-controller="..." のような展開がありましたが、hx: や x: など任意のプレフィックスに対して、かつ「任意にネストした Hash」を属性に落とし込むような標準サポートはありませんでした。
この PR により、tag ビルダー (例: tag.div, tag.button) で、次のようなコードが書けます:
tag.button "POST to /clicked",
hx: { post: "/clicked", swap: :outerHTML, data: { json: true } }出力は以下のようになります:
<button
hx-post="/clicked"
hx-swap="outerHTML"
hx-data="{"json":true}"
>
POST to /clicked
</button>ポイント:
- トップレベルの
hx:キーが「プレフィックス」として扱われる - その中の Hash のキーは dasherize されて、
hx-post,hx-swap,hx-dataという属性名になるpost→hx-postswap→hx-swapdata→hx-data
- 値がさらにネストした Hash (
data: { json: true }) の場合、適切に JSON へシリアライズされ、属性値としてエスケープされて格納される
→hx-data="{"json":true}"
Stimulus/Turbo でおなじみの:
tag.div data: { controller: "users", action: "click->users#save" }
# => <div data-controller="users" data-action="click->users#save"></div>というスタイルが、hx: や x: にも自然に拡張できるイメージです。
2-2. 実装箇所
actionview/lib/action_view/helpers/tag_helper.rb- Tag ビルダーで、属性ハッシュを処理するロジックに、任意のネスト Hash を「ダッシュ区切り属性名」に展開する処理が追加。
- 具体的には、
hx: { foo_bar: ... }のような場合にhx-foo-bar="..."という属性に変換するような仕組み。
2-3. テストの追加
以下にテストが追加されています。
actionview/test/template/tag_helper_test.rbtagヘルパー単体で、ネスト Hash → 属性展開の挙動を確認するテストが追加。
actionview/test/template/form_helper_test.rbactionview/test/template/form_helper/form_with_test.rbform_tagやform_with経由でも同様の属性構築が期待通り動くことを確認するテストが追加。
2-4. CHANGELOG の更新
actionview/CHANGELOG.md- Action View に「タグビルダーがネストした Hash から dasherized な HTML 属性を生成できるようになった」という新機能として記載。
- 影響範囲・注意点
対象範囲
ActionView::Helpers::TagHelperを利用しているすべてのコードtag.div,tag.button,form_with,form_for,form_tag内で生成される各種タグの属性処理に影響します。
後方互換性
- 既存の
data: { ... }の挙動はそのまま維持されつつ、より一般的なプレフィックス (hx:,x:等) にも同様の変換ロジックが適用される形です。 - 通常の単層 Hash (
class: "btn",hx_post: "/..."など) は従来通り動作します。
ネストさせたときにのみ、新しい展開ロジックが効きます。
- 既存の
注意点・設計上のポイント
- 「トップレベルのキー」(例:
hx:) と「その値の Hash のキー」(例:post,swap,data) を組み合わせて属性名を構築する前提になっています。 - ネストがさらに深くなる場合 (
hx: { data: { json: { foo: :bar } } }) については、基本的に JSON シリアライズされるため、「プレフィックス + さらにダッシュでつなぐ」形ではなく「1 属性の JSON 値」として表現されます。この点はdata: { foo: { bar: :baz } }と同じ考え方に沿うと考えられます。 - Hash のキーは dasherize されるため、
foo_bar→foo-barになります。JS 側の属性名と揃えるために、キー名・キャメルケース/スネークケースの扱いには注意してください。
- 「トップレベルのキー」(例:
- 参考情報 (あれば)
PR 本文で挙げられている対象フレームワーク
- Stimulus: https://stimulus.hotwired.dev
- Turbo: https://turbo.hotwired.dev
- Alpine.js: https://alpinejs.dev
- htmx: https://htmx.org
利用イメージ (Alpine.js など)
tag.div x: { data: { foo: :bar }, on: { click: "doSomething()" } }
# => <div x-data="{"foo":"bar"}" x-on-click="doSomething()"></div>このように、サーバーサイドで Ruby の Hash を自然に書きつつ、各種フロントエンドフレームワーク用の属性を柔軟に生成できるようになった、というのが本 PR の主旨です。
#57240 Move generated kamal gem into development group
マージ日: 2026/5/5 | 作成者: @jaykava
- 概要 (1-2文で)
Rails のアプリケーション生成時に自動で追加されるkamalgem を、トップレベルではなくgroup :development内に移動する変更です。これにより、Kamal が本番環境の Bundle からデフォルトで外れ、開発時専用のデプロイツールとして扱われます。
- 変更内容の詳細
背景・動機
- 現状の
rails newでは、生成されるGemfileに以下のような行がトップレベルに入ります:rubygem "kamal", require: false - その結果、「デプロイ用ツールである Kamal」が、本番環境用の
bundle install --without development testなどをしていても、トップレベル依存として本番バンドルに含まれてしまう問題がありました。 - この PR は「Kamal は開発時のデプロイツールであり、本番コードの実行に不要」という前提に沿って、Gemfile 上の位置づけを見直しています。
Gemfile テンプレートの変更
変更対象: railties/lib/rails/generators/rails/app/templates/Gemfile.tt
- これまで:ruby
# 例: トップレベルに kamal gem "kamal", require: false - 変更後:
group :development内に移動rubygroup :development do gem "kamal", require: false end
実際には他の development 向け gem(web-console, listen など)と同じグループに Kamal が含まれる形になります。
これにより、RAILS_ENV=production bundle install などで development グループを除外した際、Kamal はインストール対象から外れます。
ジェネレータの挙動とテスト
変更対象テストファイル:
railties/test/generators/app_generator_test.rbrailties/test/generators/api_app_generator_test.rb
テストで担保しているポイント:
- 通常のアプリ生成 (
rails new my_app)- 生成された
Gemfileにおいて、Kamal がgroup :development内に含まれていることを確認。
- 生成された
- API モードのアプリ生成 (
rails new my_app --api)- API アプリでも同様に Kamal が development グループに入っていることを確認。
- 既存の API 用 Gemfile 形状(特に assets 周りを含む Kamal の設定)との整合性をテスト。
--skip-dev-gemsオプションrails new my_app --skip-dev-gemsでは、development グループ用の gem が Gemfile に含まれないため、Kamal も Gemfile に現れないことを確認。
--skip-kamalオプション- 既存オプションの挙動維持:
rails new my_app --skip-kamalを指定した場合は、Kamal 自体が Gemfile に出現しないことを確認。
- 既存オプションの挙動維持:
開発者向けに想定される生成結果イメージ:
# Gemfile (抜粋)
# 通常の gem 群
gem "rails", "~> 7.2.0"
gem "pg"
# ...
group :development do
gem "kamal", require: false
# その他 development 用 gem
end- 影響範囲・注意点
影響範囲
- 新しく
rails newで生成するアプリケーションのみが対象です。 - 既存アプリの Gemfile は自動で書き換えられませんが、同じ問題(Kamal がトップレベルにある)が気になる場合、自分で
group :developmentに移動しても良いです。 - 通常/ API モードいずれのアプリ生成でも、Kamal は「開発グループの gem」として扱われます。
注意点
- 本番環境で Kamal を実行したい場合
- サーバ上でデプロイ作業を行う際に
bundle exec kamal ...を使う運用を想定している場合、production 環境でbundle install --without developmentのようにしていると Kamal がインストールされません。 - その場合:
- デプロイ用の環境(ローカルCIサーバなど)だけは development グループも含めて
bundle installする - もしくは「アプリ実行環境」と「Kamal 実行環境」を分ける(ローカル or CI で Kamal からリモートにデプロイする) などの運用設計が必要です。
- デプロイ用の環境(ローカルCIサーバなど)だけは development グループも含めて
- サーバ上でデプロイ作業を行う際に
--skip-dev-gems/--skip-kamalの組み合わせ--skip-dev-gemsを付けると Kamal を含む全ての development グループ gem が除外されます。--skip-kamalは Kamal 単体の除外用であり、development グループの他の gem には影響しません。- これらの挙動は引き続き既存通りであることがテストで担保されています。
- 参考情報 (あれば)
- 対応 issue: #57205 — 「Kamal が本番 bundle に含まれてしまう」問題への対応 PR。
- 実行されたテストコマンド:
- 一般アプリのジェネレータテスト:bash
bundle exec ruby -Irailties/test \ railties/test/generators/app_generator_test.rb \ -n "/kamal|skip_dev_gems/" - API アプリのジェネレータテスト:bash
bundle exec ruby -Irailties/test \ railties/test/generators/api_app_generator_test.rb \ -n "/test_api_modified_files|test_kamal_deploy_yml_excludes_asset_path_for_api_apps/"
- 一般アプリのジェネレータテスト:
- 変更ファイル:
- Gemfile テンプレート:
railties/lib/rails/generators/rails/app/templates/Gemfile.tt - ジェネレータテスト:
railties/test/generators/app_generator_test.rbrailties/test/generators/api_app_generator_test.rb
- Gemfile テンプレート:
#57301 Document _came_from_user? and _for_database attribute method suffixes [ci skip]
マージ日: 2026/5/5 | 作成者: @joaomarcos96
- 概要 (1-2文で)
Rails のActiveRecord::AttributeMethods::BeforeTypeCastが内部的に提供している*_for_databaseと*_came_from_user?という属性メソッドサフィックスについて、公式ドキュメントに追記した PR です。挙動の変更は一切なく、ドキュメントとコメントの整備のみです。
- 変更内容の詳細(あればサンプルコードも含めて)
対象となるモジュール
ActiveRecord::AttributeMethods::BeforeTypeCast では、以下のように3つのサフィックス付き属性メソッドを定義しています:
attribute_method_suffix "_before_type_cast", "_for_database", parameters: false
attribute_method_suffix "_came_from_user?", parameters: false従来は _before_type_cast だけが rdoc に説明されており、_for_database / _came_from_user? は、コードを読まない限りほぼ知られない状態でした。
追加されたドキュメントの要点
この PR では、モジュールレベルの rdoc に下記 2 つのサフィックスの説明と、Book モデル+enum を用いた簡単な例が追加されています。
*_for_database
- 対象:
book.status_for_databaseのように、属性名 +_for_databaseというメソッド - 役割:
「その属性が実際に DB に保存される際の値」(=type キャスト・enum 変換などを適用した後の値)を取得するためのディスパッチ先 - 元々存在していた API:
read_attribute_for_database(:status)attributes_for_databaseからも利用できる機能だが、*_for_databaseというメソッド形式はドキュメント化されていなかった、という位置づけです。
イメージ例(enum を利用した Book モデル):
class Book < ApplicationRecord
enum status: { draft: 0, published: 1 }
end
book = Book.new(status: :published)
book.status # => "published" (enum のキー)
book.status_before_type_cast # => "published" (元入力)
book.status_for_database # => 1 (DB に保存される実際の整数値)*_came_from_user?
- 対象:
book.status_came_from_user?のように、属性名 +_came_from_user?というメソッド - 役割:
「その属性の現在の値が、ユーザー入力(属性代入)由来かどうか」を判定するためのディスパッチ先 - これも内部的には以前から存在していましたが、コメントや rdoc に説明がなかったため、今回説明を追加しています。
イメージ例(フォーム入力などを想定):
book = Book.new
book.status_came_from_user? # => false (まだ何も代入されていない)
book.status = 'published'
book.status_came_from_user? # => true (ユーザー入力によってセットされた値)※ 実際の判定ロジックは ActiveRecord の内部実装に依存しますが、概念としては「DB 読み込みやデフォルト値ではなく、アプリ側からの代入でセットされた値か」を知るためのフラグです。
コメントの追加
内部メソッドに、他の _before_type_cast と同様のコメントが追加されました。
attribute_for_databaseに:
# Dispatch target for *_for_database attribute methods.attribute_came_from_user?に:
# Dispatch target for *_came_from_user? attribute methods.これにより、*_for_database / *_came_from_user? がどのメソッドにディスパッチされるのか、コードリーディング時にも分かりやすくなっています。
- 影響範囲・注意点
挙動変更なし
既存の動作・API に一切変更はありません。
・*_for_database/*_came_from_user?メソッドは以前から利用可能であり、今回の PR はそれを公式に文書化しただけです。ドキュメント依存のユーザーにとっての影響
- これまで「知られざる内部 API」っぽかった
status_for_database/status_came_from_user?などが、公式にサポートされている振る舞いとして分かりやすくなります。 - enum・独自 type・暗号化 attribute などを扱うときに、
- 「DB に実際に保存される値」を直接確認したい
- 「これはユーザー入力起点の値かどうか」を判別したい
といった場面で、これらのメソッドを安心して利用しやすくなります。
- これまで「知られざる内部 API」っぽかった
将来の互換性の観点
- ドキュメントに明示されたことで、「半ば内部実装」に見えていたこれらの方法が、よりパブリックな契約に近づいたと解釈できます。
- gem やアプリケーションがこれらに依存しても、将来的に破壊的変更になりにくくなる、という意味でプラスです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57301
- 関連する API:
ActiveRecord::AttributeMethods::BeforeTypeCastModel#attribute_before_type_castModel#read_attribute_for_database(name)Model#attributes_for_database
#57129 Use Hash#each_key instead of Hash#keys.each
マージ日: 2026/5/5 | 作成者: @ashwin47
- 概要 (1-2文で)
Rails内部で、Hash#keys.eachを使っていた箇所の一部をHash#each_keyに置き換え、余計な配列生成を避けることでパフォーマンスとメモリ効率をわずかに改善するリファクタリングです。ハッシュを反復中に変更しない箇所のみを安全に置き換えており、挙動の変更はほぼありません。
- 変更内容の詳細
背景・意図
Hash#keys.eachは以下のようなコードです:rubyhash.keys.each do |key| # ... endこれは
hash.keysで「全キーを配列にコピー」- その配列に対して
eachでループ
という2段階処理になり、中間配列の生成コスト(メモリ・GC)が発生します。
一方、
Hash#each_keyは:rubyhash.each_key do |key| # ... endのように、ハッシュ内部構造を直接たどってキーを反復するため、中間配列を生成せずに済みます。
PR #17099 でも同種のクリーンアップが行われており、その続きとして今回のPRが入っています。
実際の変更点
PR本文では以下2ファイルと書かれていますが、今回の diff では3ファイルが変更対象として挙がっています(実際のコード差分は1行ずつの軽微な修正):
activemodel/lib/active_model/schematized_json.rbactiverecord/test/cases/connection_adapters/connection_handler_test.rbactivesupport/test/env_configuration_test.rb
いずれも本質的には同じパターンの置き換えです:
変更前(イメージ):
hash.keys.each do |key|
# ハッシュはこのループ中に変更されない
end変更後:
hash.each_key do |key|
# ハッシュはこのループ中に変更されない
end重要なのは、「ループ中に delete などでハッシュを破壊的に変更しない」箇所だけが置き換え対象になっている点です。
置き換え対象に していない 箇所
PR説明中では、以下の3箇所はそのまま keys.each が残されていると明記されています:
hash/keys.rbstrong_parameters.rbrouting/mapper.rb
これらではループ内で delete などによりハッシュを変更しているため、先に keys でスナップショット(配列コピー)を取ってから反復する必要があるケースです。このような場合に each_key に変えると、繰り返し中に対象ハッシュが変化してバグや予期せぬ動作を招く可能性があるため、意図的に残されています。
- 影響範囲・注意点
影響範囲は内部実装のパフォーマンスに限られ、外部APIの仕様変更や互換性破壊はありません。
変更対象となった箇所ではループ中にハッシュを変更しておらず、挙動は
keys.eachと論理的に等価です。メモリ使用量がごくわずかに減り、GC負荷もわずかに軽くなる可能性がありますが、多くのアプリケーションでは体感できない程度の微小な改善です。
自分のアプリ・ライブラリ側でも、以下のようなループは
each_key/each_value/each_pairに置き換え可能か検討できます:ruby# 変更前 hash.keys.each do |key| # ループ中に hash を変更しない end # 推奨パターン hash.each_key do |key| # ... endただし、ループ中に
hash.delete(key)やhash[some_key] = ...等で構造を変える場合は、従来どおりhash.keys.eachでスナップショットを取る方が安全です。
- 参考情報 (あれば)
- Rubyのドキュメント:
- 類似のクリーンアップPR(本PRで参照されているもの):
- Rails PR #17099:
Use each_key and each_value instead of keys.each and values.each(ハッシュ反復周りの最適化)
- Rails PR #17099:
#55826 Add conflicting record ID to validates_uniqueness_of error details
マージ日: 2026/5/5 | 作成者: @bvicenzo
- 概要 (1-2文で)
validates_uniqueness_of/validates :field, uniqueness: trueの検証に失敗した場合、errors.detailsに「衝突している既存レコードの主キーID(existing_id)」が含まれるようになりました。これにより、どのレコードと一意制制約がぶつかったかをアプリ側で特定しやすくなります。
- 変更内容の詳細
2-1. 何が変わったか
ActiveRecord の ActiveRecord::Validations::UniquenessValidator の重複チェック実装が以下のように変更されています。
以前:
検証対象と同じ値を持つレコードが存在するかを.exists?で確認していた
→ 存在するかどうか (true/false) しか分からない変更後:
.pick(klass.primary_key)を使って、衝突している既存レコードの主キー値を取得する
→ 存在する場合は、その ID をerrors.detailsに:existing_idとして格納
具体的には、バリデーションが失敗したときのエラー詳細が次のように変わります。
変更前
class Topic < ApplicationRecord
validates :title, uniqueness: true
end
topic = Topic.new(title: "Existing Topic")
topic.valid? # => false
topic.errors.details[:title]
# => [{ error: :taken, value: "Existing Topic" }]変更後
topic.errors.details[:title]
# => [{ error: :taken, value: "Existing Topic", existing_id: 1 }]existing_id には、衝突しているレコードの primary_key(通常は id)が入ります。
スコープ付き一意性 (validates :title, uniqueness: { scope: :user_id } など) に対しても同様に適用されます。
2-2. 実装上のポイント
- DB問い合わせ部分で
.exists?→.pick(klass.primary_key)に変更。- どちらも
LIMIT 1で 1 行だけ取得するクエリになるため、パフォーマンス特性はほぼ同等。 .pickは該当行があれば主キーIDを返し、なければnilを返す。
- どちらも
- 衝突レコードが見つかった場合のみ、
errors.detailsの要素ハッシュにexisting_id: <主キー値>を追加。 - I18n 関連・ユニークネス関連のテストがこの新しい振る舞いに合わせて更新されています。
activerecord/CHANGELOG.mdに仕様変更として追記済み(公式にサポートされる振る舞い)。
2-3. 利用イメージ
JSON API で 409/422 エラーに「どのレコードと衝突したか」を載せたいケースなどで、そのまま使いやすくなります。
# 例: API レスポンス整形
record.valid?
if record.errors.added?(:email, :taken)
existing_id = record.errors.details[:email].first[:existing_id]
render json: {
error: "email_taken",
message: "Email is already taken by another user",
conflict_user_id: existing_id,
conflict_user_url: user_url(existing_id)
}, status: :unprocessable_entity
end- 影響範囲・注意点
errors.detailsのスキーマが変わる
一意性バリデーションエラーの details 要素に:existing_idキーが増えます。
既存コードでerrors.details[:attr].firstをそのままシリアライズしている場合、追加フィールドがクライアントに見えるようになります。- もし既存クライアントが「キーを厳密にチェックしている」場合は、API 仕様として許容するか、サーバ側でフィルタリングが必要な可能性があります。
セキュリティ / 情報漏えい面の検討
existing_idによって、既存レコードの ID 情報がクライアントに渡せるようになります。- 自分以外のリソースIDをユーザーに見せたくないケース(マルチテナントでID自体を秘匿したい、など)の場合は、レスポンス整形で
existing_idを露出しないようにする必要があります。 - 逆に「ID をキーにしたリンクを返したい」など、積極的に ID を利用したい API では便利になります。
パフォーマンス
ベンチマーク(SQLite, Ruby 3.4.6, AR 8.1.0.beta1)では:.exists?: 約 9.6k i/s.pick(:id): 約 9.7k i/s
と誤差範囲内の差で、実質的なパフォーマンス劣化はほぼ無しとみなせます。
主キーがカスタムな場合
klass.primary_keyを使用しているため、id以外を主キーにしているモデルでも、その主キー値がexisting_idとして格納されます。existing_primary_keyではなくexisting_idというキー名である点は注意(意味的には「existing recordの主キー」)。
- 参考情報 (あれば)
- Rails Discuss の提案スレッド
https://discuss.rubyonrails.org/t/proposal-include-existing-record-id-in-uniqueness-validation-error-details/89810 - 変更ファイル(主に見るべきもの)
activerecord/lib/active_record/validations/uniqueness.rbactiverecord/test/cases/validations/uniqueness_validation_test.rb
- 実際に取りたい情報は
record.errors.details[:attr].map { |d| d[:existing_id] }.compactのようにして複数エラーに対応可能です。
#57247 Use ** in .dockerignore template for recursive directory exclusions
マージ日: 2026/5/5 | 作成者: @bogdan
- 概要 (1-2文で)
Rails がrails new --dockerなどで生成する.dockerignoreテンプレートを修正し、/log/*や/tmp/*といった「1階層だけマッチするパターン」を/log/**のような「再帰的にすべてのサブディレクトリを含めてマッチするパターン」に変更した PR です。これにより、ログ・tmp・storage などの配下にある深い階層のファイルも Docker build context から確実に除外されるようになります。
- 変更内容の詳細
具体的な変更
.dockerignore テンプレート (railties/lib/rails/generators/rails/app/templates/dockerignore.tt) が以下のように変更されています。
パターンの変更
単一階層マッチ * → 再帰マッチ ** に変更:
- /log/*
- /tmp/*
- /storage/*
- /app/assets/builds/*
+ /log/**
+ /tmp/**
+ /storage/**
+ /app/assets/builds/**Docker の .dockerignore では:
dir/*…dir直下のファイル/ディレクトリのみマッチdir/**…dir配下のあらゆる階層のファイル/ディレクトリを再帰的にマッチ
となるため、** にすることで、深い階層のファイルも確実に build context から除外されます。
冗長なエントリの削除
もともと .dockerignore には、/tmp 配下を個別に除外する行がありましたが、/tmp/** を導入したことで冗長になったため削除されています。
- /tmp/pids/**
- /tmp/storage/**/tmp/** が /tmp/pids や /tmp/storage の中身もすべてカバーするためです。
.keep の例外指定を整理
tmp 以下に置かれた .keep ファイルを context に含めるための否定パターン(除外ルールの例外)をまとめ直しています:
/tmp/**
!/tmp/.keep
!/tmp/pids/.keep
!/tmp/storage/.keep/tmp/**ですべてを除外- その後
!プレフィックスで.keepファイルだけを再度含める(.dockerignore の挙動: 後勝ち)
という形で、tmp 配下のディレクトリ構造を保ちつつ、中身の実データは送らないようにしています。
- 影響範囲・注意点
どこに影響するか
- Rails が生成する
.dockerignoreが変わるため、今後rails newで Docker 対応のアプリを作成したときのデフォルト挙動が変わります。 - 既存プロジェクトには自動で適用されないので、恩恵を受けたい場合は自分で
.dockerignoreを更新する必要があります。
具体的に改善される点
旧パターンのままだと除外されていなかった代表例:
tmp/cache/assets/sprockets/v4.0.0/ab/abcdef1234...
→ Sprockets のアセットキャッシュが深くネストされており、/tmp/*では除外しきれなかったが、/tmp/**により除外される。tmp/pids/server.pid
→ サーバ PID ファイルもtmp/pids/以下のため、旧パターンでは build context に含まれうる。storage/ab/cd/abcdefghijklmn
→ Active Storage の Disk サービスはキーをプレフィックスで2階層に分けて保存するため、ユーザのアップロードファイルが Docker イメージの build context に含まれてしまう可能性があった。
これらがすべて .dockerignore により除外されるようになります。
結果として:
- Docker build context が小さくなり、ビルド時間・アップロード時間(リモートビルドの場合)が短縮される。
- 誤ってユーザーのアップロードファイルやキャッシュ・PID などを Docker イメージに含めてしまうリスクが下がる。
注意点
.dockerignoreの挙動は.gitignoreと完全に同じではありません。特に**の再帰マッチは Docker 固有の挙動です。
カスタマイズしている場合は、Docker ドキュメントを前提にパターンを確認してください。- 既存プロジェクトで手動マージする際:
- すでに
/tmp/**などを独自に書いている場合は重複しないように統合する。 /tmp配下であえて含めたいファイル(例: 独自に何か生成しているもの)がある場合は、!/tmp/your_fileのように例外指定が必要です。
- すでに
- 参考情報 (あれば)
- Docker 公式ドキュメント:
.dockerignoreのマッチング仕様
https://docs.docker.com/build/concepts/context/#matching - この PR:
https://github.com/rails/rails/pull/57247
#57208 Add ActiveJob::Attributes to persist data between steps
マージ日: 2026/5/5 | 作成者: @bdewater-thatch
- 概要 (1-2文で)
Rails 8.1で導入された Active Job Continuation(マルチステップジョブ)に対して、ステップ間で一時的なデータを安全に保持できるActiveJob::Attributesが追加されました。Active Model Attributes をベースに、ジョブのシリアライズ/デシリアライズを意識せずに型付き属性を使って値を永続化できます。
- 変更内容の詳細
ActiveJob::Attributes の追加
- 新モジュール
ActiveJob::Attributesが追加されました。 - 中身は基本的に
ActiveModel::Attributesの API をそのまま使えるようにしたものです。attributeDSL による宣言- 型付き属性 (
:string,:integer,:datetime, 独自 type など) - デフォルト値、lazy default(Proc)、cast、serialize など Active Model Attributes の機能を利用可能
イメージとしては「Active Job 版 Active Model Attributes」で、ジョブ引数とは別に「ジョブの状態を持つための属性レイヤー」を提供するものです。
Continuable へのデフォルト組み込み
ActiveJob::Continuableにinclude ActiveJob::Attributesが追加されています。- つまり、Continuation を使ったマルチステップジョブでは、特別な設定なしに
attributeが使えます。
class EnrollmentJob < ApplicationJob
include ActiveJob::Continuable
# ステップ間で保持したいデータ
attribute :payment_token, :string
attribute :user_metadata, :json
step :tokenize_payment
step :charge_payment
def tokenize_payment(step)
self.payment_token = PaymentGateway.tokenize(enrollment.user.payment_instrument)
# この時点で payment_token はジョブのペイロードに埋め込まれる
step.advance!
end
def charge_payment(step)
# 再キューイング後でも payment_token が復元されている
PaymentGateway.charge(token: payment_token)
step.advance!
end
end上記のように、step の間で payment_token をインスタンス変数や外部テーブルに自前保存することなく、安全に保持できます。
既存案(step の戻り値を自動保存)を採用しなかった理由
PR本文で触れられているが、以下のような API は採用されていません:
payment_token = step(:tokenize_payment_instrument, store: true) do
PaymentGateway.tokenize(enrollment.user.payment_instrument)
endこの案を見送った理由:
attributeベースの方がシンプルかつ明示的である- Continuable がカーソルを
step.cursor/set!/advance!で明示的に扱っている現状の API と整合的
(フレームワークが「勝手に戻り値を保存する」より、「属性として明示的に状態を持つ」方針に揃えている)
Active Model 側の微調整
activemodel/lib/active_model/attributes.rbにも 1 行変更があり、ActiveJob::Attributesからの利用に必要な小さな調整が入っています(主に include まわりの都合付けで、既存利用への影響は極小と考えられます)。
- 影響範囲・注意点
対象範囲
- 新機能なので既存ジョブはそのまま動作します。
ActiveJob::Continuableを使うジョブでは、デフォルトでActiveJob::Attributesが使えるようになります。- Continuable を使っていない通常の Active Job でも、
include ActiveJob::Attributesすれば利用可能です。
使いどころ
- マルチステップ処理の「中間状態」を一時的に保持したいが、DB のカラム追加や専用テーブル作成まではしたくないケース。
- 例:
- 外部 API のトークン、セッション情報
- ステップ 1 で計算した一時的な集計値
- 後続ステップでのみ必要な、生存期間の短いデータ
ジョブ引数との違い
- ジョブ引数は「ジョブ作成時に渡すもの」であり、通常は immutable な前提で扱われます。
ActiveJob::Attributesは「ジョブのライフサイクルの中で変化する“状態”」を持つための仕組みです。- ジョブを再キューイング・再開しても、これらの属性はシリアライズ/デシリアライズされて維持されます。
シリアライズサイズへの配慮
- 属性はジョブのペイロードに載るため、扱うデータはできるだけコンパクトなものに限定するべきです。
- 大きなオブジェクト・バイナリなどを保存すると、キューシステム(Sidekiq, Resque など)のメッセージサイズ制限に引っかかる可能性があります。
- 「必要な識別子だけ属性に保存し、実データは再度取得する」ような設計が推奨されます。
型・変換の注意
- Active Model Attributes の型システムに依存するため、
- カスタム type を使うなら、Active Model 側で type を定義してから利用する
- JSON や Array、Hash などもサポートされるが、シリアライズ表現(JSON / String 化)に注意する
といった点は既存の Active Model Attributes と同様です。
- Active Model Attributes の型システムに依存するため、
- 参考情報 (あれば)
- PR 本体: https://github.com/rails/rails/pull/57208
- 背景ディスカッション: https://discuss.rubyonrails.org/t/proposal-activejob-attributes-to-persist-data-between-steps/91051
- Active Job Continuation: https://api.rubyonrails.org/classes/ActiveJob/Continuation.html
- Active Model Attributes: https://api.rubyonrails.org/classes/ActiveModel/Attributes.html
この PR により、Active Job のマルチステップ処理で「一時的だが、再開時にも必要な状態」を持つための公式なパターンが用意され、従来の serialize/deserialize のオーバーライドや独自ストレージ実装が不要になることが期待されます。
#56450 Fix ActionMailer::Base.mail dispatch in 8.1+
マージ日: 2026/5/5 | 作成者: @afurm
- 概要 (1-2文で)
Rails 8.1 でActionMailer::Base.mailをクラスメソッドとして直接呼び出した際にActionNotFoundが発生するリグレッションを修正し、8.0 と同じ挙動(クラスレベル配送)を復元する PR です。mailが ActionMailer の「アクション」として常に認識されるようにし、その挙動を担保する回帰テストが追加されています。
- 変更内容の詳細 (サンプルコード含む)
問題の背景
Rails 8.1 以降で、以下のようなコードが動かなくなっていました:
ActionMailer::Base.mail(
to: "user@example.com",
from: "no-reply@example.com",
subject: "Hello",
body: "Hi"
).deliver_now従来(〜Rails 8.0)はこれが許可されていましたが、8.1 では mail が「有効なメールアクション(action_methods)」として扱われなくなり、Action Mailer の内部ディスパッチで ActionNotFound が発生するようになっていました。
修正内容
1) ActionMailer::Base の action_methods に常に mail を含める
actionmailer/lib/action_mailer/base.rb に変更が入り、ベースクラス (ActionMailer::Base) を直接使う場合、mail が常にアクションとしてみなされるようになりました。
イメージとしては、Action Mailer が action_methods を決定するロジックに「ActionMailer::Base に関しては mail を特別扱いして常に含める」という処理が入った形です。
その結果、ActionMailer::Base.mail 呼び出し時も内部的には通常の Action Mailer のアクションディスパッチフローに乗り、ActionNotFound が起きなくなります。
2) 回帰テストの追加
actionmailer/test/base_test.rb に、以下のようなテストが追加されています:
ActionMailer::Base.mailを直接呼び出してdeliver_now/deliver_laterできること- その呼び出しが Action Mailer のアクションディスパッチを通って成功すること(=
ActionNotFoundにならないこと)
テストは PR 説明にある通り、以下で実行確認されています。
bundle exec ruby -Iactionmailer/test actionmailer/test/base_test.rb -n "/ActionMailer::Base.mail/"利用イメージ
この PR 適用後は、Rails 8.1+ でも以下のような「簡易ワンショットメール送信」が再び安全に使えます。
# メイラークラスを定義しなくても、ベースクラスだけで送信可能
ActionMailer::Base.mail(
to: "user@example.com",
from: "no-reply@example.com",
subject: "Quick mail",
body: "This is a quick one-off email."
).deliver_now- 影響範囲・注意点
影響範囲
ActionMailer::Base.mail(...)をクラスメソッドとして直接呼んでいる既存コードに対して、Rails 8.1 で発生していたActionNotFoundが解消され、Rails 8.0 と同じ挙動になります。- 独自のメイラークラス(
UserMailer < ApplicationMailerなど)で、通常のアクション(def welcome_email ... end)を使っているケースには基本的に影響はありません。
互換性
- 明確に「8.0 の挙動を復元する」ことが目的のため、既存の 8.0 向けコードとの後方互換性を回復する変更です。
ActionMailer::Baseを特殊な形で継承し、action_methodsをカスタマイズしているような高度なメタプログラミングをしていない限り、副作用の可能性は低いと考えられます。
注意点
- PR 時点では CHANGELOG は更新されていないため、この挙動変更は一見分かりづらい可能性があります。Rails を 8.1 系に上げた際に
ActionMailer::Base.mail周辺が壊れた場合、この修正が入っているバージョンかを確認するとよいです。 ActionMailer::Baseを直接使ったメール送信はあくまで「簡易用途」向けであり、大量のメール送信や複雑なロジックには従来通り独自のメイラークラスの定義を推奨します。
- PR 時点では CHANGELOG は更新されていないため、この挙動変更は一見分かりづらい可能性があります。Rails を 8.1 系に上げた際に
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/56450
- 対応 Issue: https://github.com/rails/rails/issues/56449
- 対象クラス:
ActionMailer::Base - テスト実行コマンド(当該変更部分のみ):bash
bundle exec ruby -Iactionmailer/test actionmailer/test/base_test.rb -n "/ActionMailer::Base.mail/"
#57288 Define as_json on ActiveStorage::Attached::One and Attached::Many
マージ日: 2026/5/5 | 作成者: @onyxblade
- 概要 (1-2文で)
has_one_attached/has_many_attachedの名前が実カラム(あるいは ignored_columns に入ったカラム)と衝突しているときに、record.to_jsonでSystemStackError(無限再帰)が発生するバグを修正するPRです。ActiveStorage::Attached::One/Manyに明示的なas_jsonを定義し、レコードの JSON シリアライズ時に安全に変換されるようにしました。
- 変更内容の詳細
問題の背景
以下のようなモデルがあるとします:
class Product < ApplicationRecord
self.ignored_columns = [:photo]
has_one_attached :photo
end
Product.select('*').first.to_json
# => SystemStackError: stack level too deepポイント:
- DB 上には
photoカラムが存在する。 - Rails 側では
self.ignored_columns = [:photo]で無視しつつ、同名でhas_one_attached :photoを定義。 Product.select('*')により、無視されるはずのphotoカラムも結果セットには含まれる。- その状態で
record.to_jsonを呼ぶと、ActiveModel の JSON シリアライズ処理の途中で、Attached::One/Attached::ManyのインスタンスがObject#as_jsonにフォールバック。 Object#as_jsonはinstance_values(インスタンス変数をすべてハッシュ化)を返すため、その中に含まれる@recordからまたas_jsonが呼ばれ…というサイクルが発生し、スタックオーバーフローになる。
スタックトレース上では、Attached::One / Many が Object#as_json を通ってしまうことが原因で、record へのバックリファレンスがシリアライズ対象になり無限再帰が発生していました。
対応内容
ActiveStorage::Attached::One と ActiveStorage::Attached::Many に as_json を定義し、Object#as_json に落ちないようにする、というローカルな修正です。
Attached::One#as_json
- 振る舞い:
- 添付済みなら、そのアタッチメント(
ActiveStorage::Attachmentレコード)のas_jsonを返す。 - 未添付なら
nilを返す。
- 添付済みなら、そのアタッチメント(
擬似コードイメージ:
# activestorage/lib/active_storage/attached/one.rb
def as_json(*)
if attached?
attachment.as_json
else
nil
end
end結果として、Product.first.to_json の中で photo が JSON 化されるときは、以下のような JSON に展開されます(概念的な例):
{
"id": 1,
"name": "Sample product",
"photo": {
"id": 10,
"blob_id": 99,
"record_id": 1,
"record_type": "Product",
"name": "photo",
"created_at": "2026-05-05T12:34:56Z"
}
}Attached::Many#as_json
- 振る舞い:
- アタッチされている全ての
ActiveStorage::Attachmentのas_jsonを配列で返す。
- アタッチされている全ての
擬似コードイメージ:
# activestorage/lib/active_storage/attached/many.rb
def as_json(*)
attachments.map(&:as_json)
endJSON は以下のような形になります(概念的な例):
{
"id": 1,
"name": "Sample product",
"photos": [
{
"id": 10,
"blob_id": 99,
"record_id": 1,
"record_type": "Product",
"name": "photos",
"created_at": "2026-05-05T12:34:56Z"
},
{
"id": 11,
"blob_id": 100,
"record_id": 1,
"record_type": "Product",
"name": "photos",
"created_at": "2026-05-05T12:35:56Z"
}
]
}なぜこれで安全なのか
ActiveStorage::Attachmentは以下のようなスカラーカラムのみを持つシンプルなモデルです:id,blob_id,name,record_id,record_type,created_at
- これらのカラムには
ActiveStorage::Attached::One/Manyのような「レコード自身へのバックリファレンス」は含まれていないため、Attachment#as_jsonを呼び出しても再帰ループは発生しません。 - そのため
Attached::*インスタンスの JSON 化は、Attachmentレコードの配列(または単一オブジェクト)に収束し、安全にシリアライズが完了します。
代替案として検討されたもの
PR説明で触れられている代替案:
ActiveRecord::Persistence#instantiate_instance_ofの中で、ignored_columnsに入っているカラムを row hash から取り除く。- これにより、
select('*')を書いていても、ignored_columnsに入っているカラムはattribute_namesに現れないようにできる。
- これにより、
- しかし、この方法は:
- ユーザーが明示的に SQL で取得したカラムが「サイレントにドロップされる」挙動変更であり、影響範囲が広い。
- ActiveStorage 以外にも「レコードをバックリファレンスする任意の Object サブクラス」には効かない。
そのため、より局所的で副作用の少ない「Attached::* プロキシに as_json を生やす」方針が選ばれています。
テスト・ドキュメント
activestorage/test/models/attached/one_test.rbactivestorage/test/models/attached/many_test.rb
にテストが追加され、今回の as_json の挙動が保証されています。
activestorage/CHANGELOG.mdにも変更が記載され、挙動変更として明示されています。
- 影響範囲・注意点
対象:
- Active Storage を利用しているアプリケーションのうち、
has_one_attached/has_many_attachedの名前が実カラム名と衝突している、- または
ignored_columnsと組み合わせてカラムを無視しつつ、同名で attachment を定義している、 - さらに
select('*')や特定のselectで無視したはずのカラムを結果に含めてしまう、 といったケースでrecord.to_jsonを呼んでいるプロジェクト。
- Active Storage を利用しているアプリケーションのうち、
この PR により、これらのケースで
SystemStackErrorが発生しなくなります。JSON 形式の変化:
- これまで
to_jsonを呼んでもエラーで落ちていたようなケースでは、今回初めて JSON が正常に返ることになります。 has_one_attachedのフィールドは「単一のAttachmentオブジェクト ornil」、has_many_attachedは「Attachmentオブジェクトの配列」として JSON に含まれるようになります。- もし
to_json/as_jsonをオーバーライドして独自形式を返したい場合は、アプリ側でas_jsonを追加実装する必要があります。
- これまで
Rails の他部分への影響:
- 修正箇所は Active Storage の
Attached::One/Manyに限定されており、ActiveRecord の汎用的なインスタンス生成や ignored_columns の扱いには手を触れていません。 - そのため、ActiveRecord 全体の挙動変更のリスクは小さいです。
- 修正箇所は Active Storage の
- 参考情報 (あれば)
- 対応する issue: #57287
「has_one_attached/has_many_attachedが属性名をシャドウするときのSystemStackError」を扱うものです。 - 対象クラス:
ActiveStorage::Attached::OneActiveStorage::Attached::ManyActiveStorage::Attachment
- 類似の問題を避けるための指針:
- カラム名と ActiveStorage の attachment 名を可能な限り衝突させない。
- どうしても同名にする必要がある場合でも、今回の修正により少なくとも
to_json時の無限再帰は解消されます。
#57162 Skip CreateUsers migration when User model already exists
マージ日: 2026/5/5 | 作成者: @johntopley
- 概要 (1-2文で)
Rails のrails generate authenticationで、すでにUserモデルが存在する場合にCreateUsersマイグレーションを自動生成しないようにし、「table already exists」エラーを避ける修正です。既存アプリに後から認証機能を追加しやすくなる変更です。
- 変更内容の詳細
2-1. 挙動の変更点
これまで:
rails generate authentication Userなどを実行すると、- 常に
db/migrate/xxxxxx_create_users.rbのようなCreateUsersマイグレーションを生成 - 既に
usersテーブルがある(=既存の User モデル+マイグレーションがある)状態でdb:migrateを実行すると、- 「table already exists」エラーで失敗
- 常に
今回の変更後:
- ジェネレータ実行前に
app/models/user.rbが存在するかをチェック - 存在する場合は
CreateUsersマイグレーションの生成をスキップ - そのため、既存の
usersテーブルがあるアプリに後から authentication generator を適用しても、テーブル作成マイグレーションが重複せずに動く
2-2. コードレベルでの変更点 (概要)
railties/lib/rails/generators/rails/authentication/authentication_generator.rb にて:
- User モデルファイルの存在チェックロジックを追加
- 例:
File.exist?(Rails.root.join("app/models/user.rb"))のような条件を導入し、真の場合はCreateUsersマイグレーションのテンプレート生成を行わないようにしている(正確なメソッド名・条件はPR本文からの推測だが、実装方針はこれ)
- 例:
- 既存のマイグレーション生成処理の前後に条件分岐を挿入し、「Userモデルがないときだけ
create_usersマイグレーションを生成」するように変更
railties/test/generators/authentication_generator_test.rb では:
- 新しい挙動に対するテストが追加
app/models/user.rbがある状態でジェネレータを実行したときにCreateUsersマイグレーションが生成されないことを検証- User モデルがない状態では従来どおりマイグレーションが生成されることも担保
railties/CHANGELOG.md:
- 上記の仕様変更が CHANGELOG に追記され、振る舞いの変更として明文化されています。
- 影響範囲・注意点
- 影響対象:
- Rails 付属の
authenticationgenerator を利用しているプロジェクト - 既存アプリに後から
rails generate authentication Userを適用するケースで特に恩恵がある
- Rails 付属の
- 注意点:
- 今回の判定は「
app/models/user.rbが存在するかどうか」で行われているため、- テーブル名は
usersだがモデルファイルが別名/別ディレクトリにある、 - あるいは
Userモデルがあってもファイル名が慣例どおりでない
といった特殊な構成をしている場合は、期待どおりに判定されない可能性があります。
- テーブル名は
- 逆に、
app/models/user.rbだけ存在していてusersテーブルがまだない場合は、マイグレーションが生成されずテーブルが作成されないので、そのような構成をとる場合は手動でマイグレーションを用意する必要があります。
- 今回の判定は「
- バックワード互換性:
- 一般的な Rails アプリ(
Userモデル=app/models/user.rb、テーブル=users)では、既存の挙動が「壊れる」ことは基本的になく、むしろ二重マイグレーションのエラーが防止される方向の変更です。
- 一般的な Rails アプリ(
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57162
- 閉じられた Issue(動機となったバグ報告): #57149
- 「既に User モデルと
usersテーブルが存在するアプリで authentication generator を使うと、db:migrateで ‘table already exists’ が発生する」という報告に対応した変更です。
- 「既に User モデルと
#57298 Fix eager_load for CPK models with explicit select
マージ日: 2026/5/5 | 作成者: @fatkodima
- 概要 (1-2文で)
複合主キー(CPK: Composite Primary Key)を持つモデルに対して、selectを明示したクエリにeager_loadを使うと正しく動かない不具合が修正されました。eager_load時の主キー列の扱いを見直し、CPK モデルでも関連を正しくプリロードできるようになっています。
- 変更内容の詳細
※ パッチ自体は非常に小さく、join_dependency.rb の条件分岐を 1 行変更し、それに対応するテストを eager_test.rb に追加したものです。実際のコード断片はリポジトリを直接参照する必要がありますが、変更の意図と動きは次のように整理できます。
背景
- ActiveRecord の
eager_loadは、関連を 1 クエリで取得するためにLEFT OUTER JOINを使います。 - その際、関連付けの整合性をとるために、主キー列(
idなど)を必ず SELECT 句に含めるロジックがあります。 - しかし、複合主キーを持つモデル(内部的には
primary_keyが複数列)で、かつselectを明示的に指定している場合、- 必要な複合主キーのカラムが SELECT に含まれない/含め方が不適切
- → eager_load の JOIN 結果を元にレコードを組み立てる処理で不整合・エラーが生じる
という問題 (#57296) がありました。
join_dependency.rb の修正
ActiveRecord::Associations::JoinDependency には、eager load 用クエリに含めるカラムを決定するロジックがあります。
今回の修正は:
- 「CPK モデル + explicit
select」というケースで- そのモデルの すべての主キーカラムを必ず SELECT に含める
- という条件付けが正しく働くよう、条件式を微調整したものです。
イメージとしては、次のような動きをするようになったと考えてください(擬似コード):
# 擬似コードレベルのイメージ
if using_eager_load?
# 以前は単一主キー前提 or 条件が甘く、CPK + select 時に不足
columns_to_select += Array(klass.primary_key) # CPK の場合は ["key1", "key2"] など
endこれにより、たとえば以下のようなケースが正しく動くようになります。
# モデル Post が複合主キー (year, code) を持っているとする
Post.select(:title) # 明示的 select
.eager_load(:comments) # 関連を eager_load
.to_a以前は year や code が SELECT 句に含まれず、eager_load が期待通りのレコード構築を行えないことがありましたが、修正後は内部的に主キー列も自動で SELECT に追加されます。
テストの追加
activerecord/test/cases/associations/eager_test.rb に 11 行のテストが追加されています。内容の趣旨は次の通りです。
- 複合主キーを持つモデルを用意
selectで一部カラムのみを指定しつつeager_loadを使う- 結果がエラーにならず、関連が正しく読み込まれることを検証
このテストが、回 regress しないことを保証する役割を持ちます。
- 影響範囲・注意点
影響範囲
- 対象:
- ActiveRecord で複合主キーを扱っているアプリケーション
- かつ、そのモデルに対して
selectを明示しつつeager_loadを使っているコード
- 具体的な影響:
- 以前は
- エラーが起きる
- もしくは、関連が正しくロードされない/結果が不完全になる
- といった挙動が出ていた箇所が、正常に動作するようになります。
- 内部的には SELECT されるカラムに主キー列(複合主キーの全列)が追加されます。
- 以前は
注意点
- 明示的に
selectを使っていて、「本当にそのカラムしか SELECT したくない」という前提でコードを書いていた場合でも、eager_loadを使う限り、内部的には主キー列が追加で SELECT されます。- これは単一主キーのときも同様の設計思想であり、CPK でもそれに揃えた形です。
- そのため、
- 「SELECT 対象カラムを厳密に制御してパフォーマンスを最適化したい」
- 「SELECT されるカラム数を完全にコントロールしたい」 といったユースケースでは、
eager_loadではなくincludes+references/preloadを使う設計を検討したほうがよい場合があります。
- 参考情報 (あれば)
- 関連 Issue: #57296
- 変更ファイル:
activerecord/lib/active_record/associations/join_dependency.rbactiverecord/test/cases/associations/eager_test.rb
- キーワード:
- Composite Primary Key (CPK)
eager_load- 明示的
select ActiveRecord::Associations::JoinDependency
#57255 Update AGENTS.md to remove PR reference
マージ日: 2026/5/5 | 作成者: @nateberkopec
- 概要 (1-2文で)
このPRは、AGENTS.mdに含まれていた特定のプルリクエスト(PR)への言及を削除する、ドキュメントのみの微修正です。Rails本体の機能や挙動には一切変更がありません。
- 変更内容の詳細
- 対象ファイル:
AGENTS.md - 差分: 1行削除(追加行なし)
AGENTS.md は、Rails 内で利用される「エージェント」に関するドキュメント(例: User-Agent や各種クライアント識別、もしくは内部的な“agent”コンポーネントの解説)をまとめたファイルと考えられます。
今回削除されたのは、その中にあった「特定の PR 番号(あるいは PR への直接リンク)を参照する一文」です。
説明文にある:
Maybe I'm missing something, but this seems way too specific to be intentional.
というコメントから、この PR 参照は意図せず紛れ込んだか、あるいは履歴上のごく一時的な情報をドキュメントに固定してしまっていたため、「ドキュメントとしては不自然なほど具体的すぎる」ので削除した、という判断です。
擬似的なイメージとしては、以下のような行が削除されたと考えられます(実際の文面は異なる可能性があります):
- See PR #12345 for more details about this behavior.あるいは、もっと具体的なケース:
- This behavior was introduced in PR #56789 and should only affect FooBar agents.Rails のガイドライン的にも、ユーザ向けドキュメントに「開発時の個別 PR の履歴」を直接書き込むことはあまり望まれず、安定した概念・API・設定方法に焦点を当てるべきなので、その整理の一環と見てよいです。
- 影響範囲・注意点
- 機能影響: なし
- ソースコード・設定・API などへの変更は一切なく、あくまでドキュメント行削除のみです。
- 互換性への影響: なし
- アプリケーションコードを修正する必要も、Rails のバージョンアップにおける互換性問題も発生しません。
- 開発者視点の注意点:
AGENTS.mdを直接参照していた場合、その中にあった「特定の PR に飛んで詳細を見る」という導線がなくなります。- ただし、その情報は主に内部開発者・コントリビュータ向けの履歴情報であり、通常のアプリケーション開発者にとってはほぼ影響ありません。
- 今後、ドキュメントに履歴的な情報を書く場合は「PR番号を直接書く」よりも、「その機能が何をするのか」「いつから利用可能か(バージョン)」といった抽象度の高い情報にとどめるのが望ましい、という方向性が改めて示されたとも言えます。
- 参考情報 (あれば)
- 該当PR: https://github.com/rails/rails/pull/57255
- Rails のドキュメント方針(参考):
- Guides: https://guides.rubyonrails.org/
- Contributing Guide: https://github.com/rails/rails/blob/main/CONTRIBUTING.md
(ドキュメントにどう書くか・何を書くかの方針が議論されることが多い)
この変更は「クリーンアップ系のドキュメント整理」と捉えてよく、Rails を利用する側としては特別な対応は不要です。
#57280 Fix ActionCable Overview JS example: localStorage.get → getItem
マージ日: 2026/5/5 | 作成者: @Edilbek
概要 (1–2文で)
Action Cableガイド内の JavaScript サンプルコードで、localStorageの誤った呼び出し方 (get) を正しいgetItemに修正したドキュメント変更です。ブラウザでコピペして動く形になるように、動的 WebSocket URL の例を実用的なコードに直しています。変更内容の詳細
対象ファイル: guides/source/action_cable_overview.md の「Connect Consumer」セクション内 getWebSocketURL の例。
もともとのガイドでは (概念的には) こんなコードになっていました:
function getWebSocketURL() {
const token = localStorage.get('auth-token') // ← これが誤り
return `wss://example.com/cable?token=${token}`
}この PR で以下のように修正されています:
function getWebSocketURL() {
const token = localStorage.getItem('auth-token') // ← 正しい Web Storage API
return `wss://example.com/cable?token=${token}`
}技術的背景:
- ブラウザの Web Storage API (
Storageインターフェイス) はgetItem(key: string): string | nullを提供します。 localStorage.getというメソッドは存在しないため、サンプルをそのまま実行するとTypeError: localStorage.get is not a functionが発生します。- これを
getItemに直すことで、Action Cable の接続 URL をlocalStorageのトークンで動的に組み立てる例が、実際にブラウザで動くコードになります。
- 影響範囲・注意点
影響範囲:
- Rails 本体のランタイム動作には一切影響なし (ガイド文書のみの変更)。
- Action Cable の「概要」ガイドを読みながら実装する開発者が、ブラウザ上でエラーに遭遇しなくなる。
注意点:
- すでにドキュメントを参考にして
localStorage.get(...)を書いてしまっているコードがある場合は、手元の実装をgetItem(...)に修正する必要があります。 getItemはキーが存在しない場合nullを返すため、本番コードではnullチェックや未ログイン時のハンドリングを追加することが推奨されます (例: トークンがなければ接続しない、ログイン画面へ誘導するなど)。
- すでにドキュメントを参考にして
- 参考情報 (あれば)
- Web Storage API 仕様:
https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem - 該当ガイド (最新 edge):
https://edgeguides.rubyonrails.org/action_cable_overview.html (「Connect Consumer」セクションの WebSocket URL 例)
#56209 Update Configurable deprecaton warning
マージ日: 2026/5/5 | 作成者: @skipkayhil
- 概要 (1-2文で)
Rails のActiveSupport::Configurableを非推奨にする際に表示される警告文が、「class_attributeを使え」と単一の代替手段だけを示していたのをやめ、複数の代替案を提案する、より中立的な文言に変更した PR です。機能的な変更はなく、あくまで deprecation warning のメッセージ内容のみが更新されています。
- 変更内容の詳細
変更ファイルは 1 行のみで、ActiveSupport::Configurable 使用時に表示される非推奨警告文の文言が修正されています。
背景:
- 以前の PR (ref: https://github.com/rails/rails/pull/53970) で
ActiveSupport::Configurableが非推奨になり、その際の警告文で「class_attributeを使うように」と強く推奨していました。 - その結果、実際には単純な
attr_accessorや別の仕組みで十分なケースでも、ライブラリ作者やアプリ開発者が「とりあえずclass_attribute」を選びがちになる、という誤誘導が発生していた、という問題意識があります。
今回の PR の意図:
- 「
Configurable→class_attributeに置き換えるべき」という “一択” のガイダンスをやめる。 - 用途によっては
attr_accessor等のよりシンプルな代替の方が適切であることを反映し、複数の選択肢を警告文に列挙する。 - これにより、「自分の使い方なら何を使うべきか」を、開発者自身が検討しやすくする。
実際のコード変更のイメージ(擬似例):
# 変更前(イメージ)
ActiveSupport::Deprecation.warn(
"ActiveSupport::Configurable is deprecated. " \
"Use `class_attribute` instead."
)
# 変更後(イメージ)
ActiveSupport::Deprecation.warn(
"ActiveSupport::Configurable is deprecated. " \
"Use an alternative such as `attr_accessor`, `mattr_accessor`, " \
"`class_attribute`, or another configuration pattern as appropriate."
)※上記は説明用の擬似コードです。実際の文言は英語・1 行差分のみですが、趣旨としては「class_attribute 一択 → 複数案提示」に変わっています。
- 影響範囲・注意点
ランタイム挙動:
ActiveSupport::Configurableの機能自体や挙動にはいっさい変更ありません。- 影響があるのは 警告メッセージの内容のみ です。
移行ガイドとしての影響:
- これまで警告に従って機械的に
class_attributeへ置き換えていた場合、それが必ずしもベストプラクティスではないことを再検討すべきケースがあります。 - 特に以下のような場合は、よりシンプルな手段が適している可能性があります:
- 単に「設定値を保持するだけ」のクラスで、クラスインスタンス変数 +
attr_accessorで事足りる。 - クラス階層間での設定の継承やスレッドローカルな設定を必要としていない。
- 単に「設定値を保持するだけ」のクラスで、クラスインスタンス変数 +
- 一方で、以下のような要件がある場合は
class_attribute・mattr_accessorなどを含めた検討が必要です:- サブクラスごとに別々の設定値を持たせたい。
- 設定値をクラスレベルで参照・変更したいが、一定の継承挙動も欲しい。
- これまで警告に従って機械的に
gem・ライブラリ作者への注意:
- 既に「Configurable 非推奨 → class_attribute に一律置換」という対応をしている gem は、その選択が妥当かを再確認してもよいです。
- 今後 Configurable の非推奨警告を見た開発者には、「いくつか選択肢があるので、自分のユースケースに合うものを選ぶ」ことが推奨されます。
- 参考情報
- 元の deprecation PR:
- https://github.com/rails/rails/pull/53970
ActiveSupport::Configurableを非推奨にした変更本体。
- https://github.com/rails/rails/pull/53970
- 今回の PR:
- https://github.com/rails/rails/pull/56209
非推奨メッセージの文言調整のみを行う小規模 PR。
- https://github.com/rails/rails/pull/56209
この PR 自体にはコード上の API 変更や破壊的変更はなく、「移行を促すメッセージの質の改善」が目的です。
#57285 Deactivate ViewReloader hooks when reloaders are cleared
マージ日: 2026/5/4 | 作成者: @ariens
- 概要 (1-2文で)
ViewReloader が登録する「ビューの監視用フック」がapp.reloaders.clear実行後も残り続けて無駄なファイルスキャンを行っていた問題を修正し、リローダー削除時にフックも確実に解除されるようにした PR です。これにより、特に Spring のフォークワーカーなどで、不要なDir[]/File.mtime呼び出しが発生しなくなります。
- 変更内容の詳細
背景
ViewReloaderは、レイルタイ初期化時にPathRegistry.file_system_resolver_hooksにrebuild_watcherフックを登録します(#57269 で導入)。app.reloadersにはViewReloaderなどのリローダーオブジェクトが格納されていて、例えば Spring のテストワーカーなどのフォーク後プロセスではapp.reloaders.clearが呼ばれます。- しかしこれまでは:
app.reloadersの配列からViewReloader自体は削除される- 登録済みの
file_system_resolver_hooks内のフックは生き残る
- 結果として、
prepend_view_pathなどが呼ばれるたびに、もはや必要のないDir.[]+File.mtimeを全ビューディレクトリに対して実行してしまう、という無駄な処理が行われていました。
この PR は「リローダーが Rails から取り除かれるときは、その副作用で登録していたフックも無効化する」という責務を明示化しています。
ViewReloader に #deactivate を追加
actionview/lib/action_view/cache_expiry.rb にある ViewReloader に新しく deactivate メソッドが追加されました。
イメージとしては以下のような処理です(疑似コード):
class ActionView::CacheExpiry::ViewReloader
def initialize(...)
# railtie 側で hook 登録するようになった (#57269)
end
def deactivate
# PathRegistry.file_system_resolver_hooks から
# 自分が登録した hook を取り除く処理
end
endポイント:
ViewReloaderが登録したfile_system_resolver_hooks内のエントリを削除するロジックをカプセル化。- これにより、「リローダーのライフサイクル」と「フックのライフサイクル」が同期される。
app.reloaders の Array を ReloadersCollection に差し替え
railties/lib/rails/application.rb で、これまで単なる Array だった app.reloaders の実体が、ReloadersCollection クラスに差し替えられました。
新クラスは railties/lib/rails/application/reloaders_collection.rb に定義され、主な役割は:
Array互換のインターフェイスを提供しつつ、clear/deleteなどで要素が削除されるときに、その要素に#deactivateがあれば呼び出すこと。
ざっくりしたイメージ:
class Rails::Application::ReloadersCollection
include Enumerable
def initialize
@reloaders = []
end
def <<(reloader)
@reloaders << reloader
end
def clear
@reloaders.each { |r| r.deactivate if r.respond_to?(:deactivate) }
@reloaders.clear
end
def delete(reloader)
if @reloaders.delete(reloader)
reloader.deactivate if reloader.respond_to?(:deactivate)
end
end
# 他にも each / [] / size など Array っぽい API を実装
endこれにより:
- 既存コードは基本的に
app.reloadersを Array として扱い続けられる(<<,each,clearなど)。 - しかし内部的には、
clearやdeleteのタイミングで自動的にdeactivateが呼ばれ、ViewReloaderのフックが確実に外される。 respond_to?(:deactivate)チェックをしているため、ViewReloader以外のリローダー(deactivateを定義していない)には影響しない。
Railtie 側の変更
actionview/lib/action_view/railtie.rb では、#57269 の変更を前提にしたわずかな修正が入っていますが、今回の PR の中心は以下の2点です:
- フック登録の実装の位置は前 PR (#57269) で railtie に移っているため、今回の変更は「登録したフックをどう解除するか」にフォーカス。
ViewReloaderがアプリケーションのリローダー一覧に登録されている前提のもと、その削除時にdeactivateが呼ばれるようになった。
テスト追加
以下のテストが追加されています。
actionview/test/template/cache_expiry_test.rbViewReloaderのdeactivateが実際にfile_system_resolver_hooksからフックを取り除くかを確認するテスト。prepend_view_path時にフックが呼ばれなくなることを検証していると推測されます。
railties/test/application/reloaders_collection_test.rbReloadersCollectionの挙動テスト:clearが各リローダーのdeactivateを呼ぶか。deleteが対象リローダーのdeactivateを呼ぶか。Enumerable的な動作を保っているか。
- 影響範囲・注意点
主に影響を受けるケース
- Spring など、Rails アプリケーションをフォークしてワーカーを立ち上げる環境で、
- フォーク後に
app.reloaders.clearを呼んでいる場合。
- フォーク後に
- これまではフォーク後のワーカーで:
ViewReloader自体は無効化されたつもりでも、PathRegistry.file_system_resolver_hooksのフックだけは生き残り、prepend_view_path(たとえばテストでパスを切り替えるなど)のたびに余計なファイルシステムスキャンが走っていた。
- 今回の変更により:
- その無駄な
Dir[]/File.mtime呼び出しがなくなり、フォークワーカー上でのオーバーヘッドが減る。 - フォーク後のプロセスでファイル監視が不要な典型ケース(テストワーカーなど)で特に有効。
- その無駄な
注意点・後方互換性
app.reloadersは依然として Array ライクなオブジェクトとして振る舞うため、普通に<<,each,clear,deleteをしている分にはコード変更は不要。- ただし、リフレクションや
is_a?(Array)に依存したコードを書いている場合は注意が必要です。- そのようなコードがあれば
ReloadersCollectionを許容する形に直す必要があります。
- そのようなコードがあれば
- 独自リローダーを
app.reloadersに追加していて「削除時に何かクリーンアップしたい」場合:- 任意で
#deactivateを定義しておけば、app.reloaders.clear/delete時に自動で呼ばれるようになります。 - 逆に、そうしたクリーンアップを意図していなければ、特に何もしなくてよい(
respond_to?(:deactivate)で守られている)。
- 任意で
パフォーマンス・動作上の効果
- フォークワーカー上で、不要な view ディレクトリの走査がなくなるため、テスト起動・実行のパフォーマンス向上が見込めます。
- ビューのリロード自体の挙動(開発環境などでの通常のオートリロード)は、今回の PR によって変化しないはずです。
- 影響が出るのは「リローダーを明示的に
clear/deleteしたとき」のみ。
- 影響が出るのは「リローダーを明示的に
- 参考情報 (あれば)
- この PR が前提にしている変更:
- #57269:
ViewReloaderのフック登録をinitializeから railtie へ移動し、watcher の生成を遅延させることでブート時の無駄な再構築を防いだ PR。
- #57269:
- Spring 側の関連 PR:
- https://github.com/rails/spring/pull/754
Spring.after_environment_loadによるアプリケーションレベルのプリロード制御。- 本 PR は Rails 内部のクリーンアップ(
app.reloaders.clearが自動的にフックも無効化する)を担当し、Spring 側のフックとは役割分担されています。
- https://github.com/rails/spring/pull/754
- 変更ログ:
actionview/CHANGELOG.md,railties/CHANGELOG.mdに今回の修正内容の要約が追記されています。
#57289 Raise ArgumentError when MemCacheStore is created with Dalli::Client
マージ日: 2026/5/4 | 作成者: @p8
- 概要 (1-2文で)
Rails のActiveSupport::Cache::MemCacheStoreに、Dalli::Clientインスタンスを渡して初期化しようとした場合にArgumentErrorを明示的に発生させる変更が入りました。これにより、すでに非推奨・削除されていたDalli::Clientベースの初期化方法が、エラーメッセージどおりに完全に禁止されます。
- 変更内容の詳細
対象:activesupport/lib/active_support/cache/mem_cache_store.rb の初期化ロジックが 1 行だけ変更されています。
背景:
- 以前のコミット
78b74e9で「Dalli::ClientをMemCacheStoreに渡す」方法は非推奨となっていました。 - さらに
c33e2d2e49dでそのサポートが削除されています。 - しかし、コード上・エラーメッセージ上は「空配列・アドレス文字列・アドレス配列のみ許可」となっているにもかかわらず、
Dalli::Clientを渡したときに明示的なArgumentErrorを投げていませんでした。
今回のPRでは、この不整合を解消し、Dalli::Client が渡された場合は必ず ArgumentError を投げるようにしています。
イメージとしては以下のようなチェックが入っていると考えてください(実際のコードは 1 行変更のみですが、概念的にはこのような条件分岐です):
# 例: MemCacheStore のコンストラクタ内のイメージ
def initialize(addresses = nil, **options)
# 許可されるのは:
# - nil または空配列
# - "localhost:11211" のような address 文字列
# - ["host1:11211", "host2:11211"] のようなアドレス配列
#
# Dalli::Client は明示的にエラーにする
if addresses.is_a?(Dalli::Client)
raise ArgumentError,
"MemCacheStore: addresses must be empty array, address string, or array of addresses (Dalli::Client is no longer supported)"
end
# ... 残りの初期化処理 ...
endPR 説明にある通り、エラーメッセージの仕様
only "empty array, address, or array of addresses" are allowed
と実際の挙動を揃えるのが主目的です。
- 影響範囲・注意点
影響があるケース:
- アプリケーションやライブラリで、以下のように
Dalli::Clientオブジェクトを直接渡してMemCacheStoreを作っている場合:
# 以前(非推奨だが技術的には動いていたパターン)
client = Dalli::Client.new("localhost:11211")
Rails.application.config.cache_store = :mem_cache_store, clientこの PR 適用後は、上記のようなコードは ArgumentError で落ちます。
正しい移行方法(従来から推奨されていた書き方):
MemCacheStoreには アドレス文字列またはアドレス配列 を渡してください。
# 推奨パターン
Rails.application.config.cache_store = :mem_cache_store, "localhost:11211"
# 複数サーバー
Rails.application.config.cache_store = :mem_cache_store,
["cache1.example.com:11211", "cache2.example.com:11211"]- Dalli を直接使いたい場合は、
MemCacheStore経由ではなく、自前で Dalli をラップする、あるいは Dalli 専用の設定パス(gem・ミドルウェアなど)を利用する必要があります。
注意点:
- この変更は後方互換性を壊す可能性がありますが、
- そもそも過去のコミットで非推奨になっていた
- 対応するエラーメッセージも既に存在していた
という経緯から、「非推奨状態からの明示的なエラー化」という整理にあたります。
- テストと CHANGELOG が更新されているため、この挙動は Rails の正式な仕様変更として扱われます。
- 参考情報 (あれば)
- このPR:
Raise ArgumentError when MemCacheStore is created with Dalli::Client(#57289) - 関連コミット(PR本文で言及されているもの)
78b74e9:Dalli::Clientを渡す方法の非推奨化c33e2d2e49d:Dalli::Clientサポートの削除
MemCacheStoreのサポートする引数は、公式ドキュメントおよびエラーメッセージ上、- 空配列
- 単一のアドレス文字列
- アドレス文字列の配列
に限定される仕様になっています。
#57291 [ci skip] Fix typos in test descriptions
マージ日: 2026/5/2 | 作成者: @jackbowlin
- 概要 (1-2文で)
このPRは、テストコード内の説明文字列(テスト名)のスペルミスを修正したものです。テストの挙動やアプリケーションコードには一切変更はなく、影響は極めて限定的です。
- 変更内容の詳細
対象は以下2つのテストファイルです。
activesupport/test/env_configuration_test.rbactivestorage/test/models/named_variant_test.rb
修正されたのは「テストの説明文字列(テスト名)」だけで、テスト内容(アサーションや実行ロジック)は変更されていません。
ActiveSupport::EnvConfigurationTest
誤字:
reqiure→ 正:require
例(イメージ):
# 変更前(イメージ)
test "env configuration reqiure something" do
# ...
end
# 変更後(イメージ)
test "env configuration require something" do
# ...
end実際には "reqiure" という文字列が "require" に修正されています。
ActiveStorage::NamedVariantTest
誤字:
explicity→ 正:explicitly
例(イメージ):
# 変更前(イメージ)
test "named variants are applied explicity" do
# ...
end
# 変更後(イメージ)
test "named variants are applied explicitly" do
# ...
endこちらも "explicity" が "explicitly" に修正されたのみです。
テスト実行について
PRの説明どおり、「テストの名前(説明)」しか変えていないため、テストは実行されていません。Ruby/Railsのテストランナーは通常、test "..." do の文字列を人間向けの説明として扱うだけで、テストの実行ロジックには影響しません。
- 影響範囲・注意点
テスト・本番動作への影響
- テストの実行結果や挙動、本番コードの動作には影響ありません。
- スナップショットツールや外部のテストレポートに「テスト名で紐づけている」場合、名称の変更として認識される可能性があります(例: テストレポートビューアでのテストケース名が少し変わる程度)。
テスト選択・フィルタリングへの影響
- Railsの
ruby -Itest test/path/to/file_test.rb -n "..."のように、テスト説明文字列を指定してフィルタリングしている場合、旧スペルで指定しているスクリプトはマッチしなくなる可能性があります。 - そのような用途で使っている場合は、新しいテスト名(正しいスペル)に更新する必要があります。
- Railsの
リファクタリング観点
- 本PRは技術的負債の解消というよりは、テストのメタデータの整備に近い変更です。
- このような誤字修正は、ドキュメント生成・テストレポート・検索性(grepなど)におけるノイズ削減に貢献します。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/57291
- 変更ファイル:
activestorage/test/models/named_variant_test.rb(+3/-3)activesupport/test/env_configuration_test.rb(+2/-2)
いずれも文字列の修正のみで、テストケースの構造やアサーションロジックの変更はありません。
#57269 Defer ViewReloader build when no view paths are registered
マージ日: 2026/5/1 | 作成者: @Korri
- 概要 (1-2文で)
View 用のファイル更新監視(ViewReloader / FileUpdateChecker)の生成タイミングをさらに遅延させ、「まだ view path が1つも登録されていない状態」でupdated?が呼ばれた場合でも不要な watcher を作らないようにした PRです。これにより、特に Spring 利用時のテスト実行など、開発環境での起動〜初回リクエスト前の無駄なディレクトリ走査が大幅に削減されます。
- 変更内容の詳細
背景と問題点
前回の変更 (#51308) で、「最初の updated? チェックまでは View watcher を作らない(lazy build)」という仕組みが導入されていました。しかし、以下のケースで問題が残っていました。
dirs_to_watch(監視対象ディレクトリの配列)が まだ空の状態 でupdated?が呼ばれる- その時点で
#build_watcherが呼ばれ、中身が空のFileUpdateCheckerでもとりあえず作成して@watcherにセットしてしまう - 以降は
@watcherが non-nil なので#rebuild_watcherのreturn unless @watcherガードが常に通り、
後から各エンジンがprepend_view_pathするたびに「全ディレクトリ集合に対するDir[*globs] + File.mtimeのフルスキャン」が走る
Spring を使ったテスト環境ではこれが顕在化します。
- Spring の preloader は
Rails.application.reloadersを直接見てreload!が必要か判断します(Reloader.check!は通さない) - 多くのエンジンの
prepend_view_pathはActiveSupport.on_load(:action_mailer)/:action_controller_baseのフック内で行われるため、preload フェーズではまだ呼ばれていない - そのため「view path が1つも登録されていない状態」で
updated?が呼ばれ、空 watcher が固定されてしまう - 結果として、その後の各
prepend_view_pathごとにフルスキャンが走り、フォークごとに高コストなディスク I/O が発生
PR の説明中にある挙動比較は以下の通りです。
修正前
- 早期ブート時の
updated?呼び出し- → watcher が空のまま作られる(軽いが「作ってしまう」)
- その後の
prepend_view_path×10- → 10回ともフル再構築(ディレクトリ再走査)
- 最初のリクエスト時の
updated?- → すでにウォッチャーがあるので再構築なし
修正後
- 早期ブート時の
updated?呼び出し- → view path が空なので watcher を作らない
- その後の
prepend_view_path×10- →
@watcherがまだ nil のため 0回再構築
- →
- 最初のリクエスト時の
updated?- → ここで初めて watcher を build(1回だけディレクトリ走査)
実際のコード上の変更点(概念的な説明)
※実際の diff からの要約であり、擬似コードを含みます。
1) ActionView::CacheExpiry 内の watcher ビルドロジック
actionview/lib/action_view/cache_expiry.rb の主な変更点は:
build_watcher/rebuild_watcher周りの条件分岐が見直され、- 「監視対象ディレクトリが空のときは watcher を作らない」 というガードが追加された
イメージとしては以下のような流れになります(擬似コード):
def build_watcher
return if dirs_to_watch.empty? # 追加されたガード: 何も監視対象がなければ何もしない
@watcher = ActiveSupport::FileUpdateChecker.new(dirs_to_watch, &callback)
end
def rebuild_watcher
return unless @watcher # 既存のガード: watcher がないなら何もしない
return if dirs_to_watch.empty? # 追加(もしくは強化)された条件: 空なら再構築もしない
@watcher = @watcher.rebuild(dirs_to_watch)
endポイント:
- 初回
updated?@watcherが nil かつdirs_to_watchが空 →build_watcherしても何も作らない →@watcherは nil のまま
- view path が登録されたあと
- 初めて
dirs_to_watchが非空の状態でupdated?が走ったタイミングで watcher を生成
- 初めて
- その後のパス追加
- すでに
@watcherがある状態でdirs_to_watchが変化したときにだけrebuild_watcherが動く
- すでに
これにより「空の watcher が早期に固定される」パターンが潰され、prepend_view_path のたびにフルスキャンが走るのを防いでいます。
2) Railtie 側の微調整
actionview/lib/action_view/railtie.rb では 1 行だけの追加ですが、ViewReloader の登録や初期化タイミングに関する配慮が行われています。
- ねらいとしては、「reloader の初期化そのものは行うが、実際の FileUpdateChecker の生成は view path が揃うまで遅延させる」という設計に沿うようになっています。
3) テストの追加
actionview/test/template/cache_expiry_test.rb に約 94 行のテストが追加されています。
主に以下を確認する内容と考えられます。
- view path 未登録時に
updated?を呼んでも watcher が作られない こと - その後に
prepend_view_pathを複数回呼んでも、不必要に watcher を再構築しないこと - 最初に「非空の view path セット」で
updated?を呼んだときにのみ watcher が1回だけ作られ、その後の挙動が正しいこと
これにより、今回の遅延ロジックが将来的に regress しないように自動テストでカバーしています。
4) CHANGELOG
actionview/CHANGELOG.md に今回の修正の概要が追記され、Action View の挙動変更として明示されました。
- 影響範囲・注意点
影響範囲
- 対象:
- ActionView の ViewReloader / CacheExpiry 周り
- 主に 開発環境・Spring 利用時 に影響
- 影響しないケース:
- production 等で
cache_classes = trueの場合
→ ViewReloader 自体がスキップされるため、挙動は従来通り - 「最初の reloader チェック(
updated?)よりも先に、全ての view path が登録される」ような小規模アプリ
→ もともと問題が顕在化しないケースなので挙動上の差はほぼない
- production 等で
性能面のインパクト
- Shopify の Rails モノレポでの計測では、Spring を使ったテストループ(小さめのワークロード)で
- 5.16 秒 → 3.66 秒(約 29% の短縮)
- 実際には、
prepend_view_pathを多用している大規模アプリケーションほど恩恵が大きくなります。
互換性・注意点
- 監視対象ディレクトリが「存在しない or 空」である場合には watcher が作られない挙動になりますが、
- 「view path が 1 つも登録されていないのに view キャッシュの更新検知をしようとする」というケースはそもそも意味がないため、実質的には妥当な制約です。
- 独自に
ActionView::CacheExpiryを拡張しているような特殊なコードベースがある場合は、@watcherが nil であり続けるパターンが増えることを前提にしているかどうかを確認した方が安全です。
- view path の追加タイミングに依存して「最初の
updated?時に必ず watcher がある」と仮定しているコードがあると、
その前提が崩れる可能性はありますが、通常の Rails アプリではまず問題にならない想定です。
- 参考情報 (あれば)
- 本 PR:
- Defer ViewReloader build when no view paths are registered (#57269)
- 直前の関連 PR:
- Do not build View watcher until the first
updated?check (#51308)
- Do not build View watcher until the first
- コード参照:
actionview/lib/action_view/cache_expiry.rb- View の更新検知周り(
build_watcher,rebuild_watcher,updated?など)
- View の更新検知周り(
actionview/lib/action_view/railtie.rb- ViewReloader の登録・初期化
actionview/test/template/cache_expiry_test.rb- 遅延ビルドと再構築ロジックのテスト
- Spring 側の関連コード(参考):
spring/lib/spring/application.rb:197Rails.application.reloadersを直接見て reload が必要か判定するロジック
#57283 Avoid assert_nil for Mime::NullType request format
マージ日: 2026/5/1 | 作成者: @mugitti9
- 概要 (1-2文で)
Railsのテストコードで、未知のフォーマット要求時のrequest.formatをassert_nilで検証していた箇所を、Mime::NullTypeを前提とした検証に変更し、最新の Minitest の挙動変更に追従したテスト専用の修正です。実行時の挙動や公開APIには影響せず、テストの互換性・堅牢性を高めるための変更です。
- 変更内容の詳細
背景となる問題
- Rails では、未知のフォーマットがリクエストされた場合、
request.formatはnilではなくMime::NullTypeのインスタンスを返します。 - もともとテストでは、このケースを
assert_nil request.formatのように「nil であること」で検証していました。 - しかし Minitest 側の変更(
assert_nilがobj.nil?ではなくnil == objで比較するようになった)により、- 以前:
obj.nil?を呼ぶ →Mime::NullType#nil?がtrueを返せばテストが通る - 現在:
nil == objを評価 → どんなにobj.nil?がtrueでも、objが実際にnilでないため失敗
という不整合が発生していました。
- 以前:
このPRでの主な修正ポイント
1) request.format のテストを Mime::NullType 前提に変更
actionpack/test/dispatch/request_test.rb のテストが、以下のような形に修正されています(擬似コード):
format = request.format
# 以前:
# assert_nil format
# 以後: 「NullType であること」と「nil? が true であること」を明示的に検証
assert_instance_of Mime::NullType, format
assert_predicate format, :nil?
# 既存の html?/xml?/json? などの否定チェックも維持
refute_predicate format, :html?
refute_predicate format, :xml?
refute_predicate format, :json?ポイント:
- 単に「nil と等しいか」ではなく、「
Mime::NullTypeというクラスのオブジェクトであり、その#nil?がtrueを返す」という Rails 独自の仕様を明示的にテスト。 - すでに存在していた
html?,xml?,json?などのフォーマット判定メソッドのテストは維持され、Unknown フォーマット時にはどれもfalseであることを引き続き確認しています。
2) actiontext 側のテストも同様に修正
actiontext/test/unit/model_test.rb にも同種の assert_nil を使ったテストがあり、同じ問題が起きる可能性があるため、こちらも同じ方針で Mime::NullType + nil? の検証に書き換えられています。
3) Gemfile.lock の微調整
Gemfile.lock に数行の変更 (+3/-2) が入っており、Minitest を含むテスト関連依存のバージョンが更新されています(今回の assert_nil の仕様変更を取り込んだバージョンに追従するためのものと考えられます)。
本体コードではなくテスト環境に関連する変更です。
- 影響範囲・注意点
実行時の Rails コード (
Mime::Type,Mime::NullType,ActionDispatch::Requestなど) の挙動は変わりません。あくまでテストコードのみの変更です。ただし、以下のようなプロジェクトには実務的な示唆があります:
Minitest を新しいバージョンに上げた Rails アプリ
- 自前のテストで
assert_nil some_request.formatのように「nil?に依存したテスト」を書いている場合、今回と同じ理由で壊れる可能性があります。 - 未知フォーマットで
Mime::NullTypeを返す箇所に対しては、assert_instance_of Mime::NullType, objassert_predicate obj, :nil?
など、Rails の実際の戻り値仕様に沿ったテストに修正するのが望ましいです。
- 自前のテストで
nil?をオーバーライドしているクラスに対するassert_nil- Minitest の仕様変更により、「
nil?をオーバーライドした擬似 Nil オブジェクト」をassert_nilでテストする手法は通用しなくなります。 - そのようなオブジェクトについては、このPRと同様に
- 型 (
assert_instance_of/assert_kind_of) - ふるまい (
assert_predicate obj, :nil?) で個別に検証するスタイルに切り替える必要があります。
- 型 (
- Minitest の仕様変更により、「
本PRはテストの期待値を Minitest 実装依存から切り離し、「Rails 側が本来保証したい仕様(NullType +
nil?)」を直接テストする形に整理しているため、長期的にはテストの安定性が向上します。
- 参考情報 (あれば)
- 関連する Minitest の変更:
assert_nil(obj)が内部でobj.nil?を呼ぶ実装から、nil == objで比較する実装に変更されたコミット:minitest/minitest@c471347
- Rails の
Mime::NullType仕様:- 未知の MIME タイプに対するフォーマットオブジェクトであり、
nil?をtrueにすることで「nil に準ずる」扱いを可能にしているが、オブジェクトとしてはnilとは異なる、という設計になっています。
- 未知の MIME タイプに対するフォーマットオブジェクトであり、