Ruby on Rails PR Digest - 2026年 10月
このページは rails/rails リポジトリにマージされたPull Requestを自動的に収集し、AIで要約したものです。
#59121 Use new resolve_alias method in read_attribute too
マージ日: 2026/10/15 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
read_attribute内部で新しく導入されたresolve_aliasメソッドを使うように変更し、属性エイリアスを持たないモデルでのread_attribute呼び出しを高速化した PR です。エイリアス付きモデルでの性能はほぼ変わらず、挙動も従来と同等になるよう保たれています。
- 変更内容の詳細
何をしたか
ActiveRecord::AttributeMethods::Read#read_attributeの実装の一部を差し替え、属性名の解決に新メソッドresolve_aliasを利用するようにしました。- 変更は 1 行の追加 + 2 行の削除のみで、既存ロジックの一部を置き換える軽微なリファクタリングです。
(実際のコードは PR 本体を見ないと正確な行は書けませんが、イメージとしては以下のような差し替えです)
# 変更前 (イメージ)
def read_attribute(name, &block)
name = name.to_s
name = @aliases[name] if @aliases && @aliases.key?(name)
_read_attribute(name, &block)
end
# 変更後 (イメージ)
def read_attribute(name, &block)
name = resolve_alias(name)
_read_attribute(name, &block)
endresolve_alias 側には以下のような最適化が入っていると考えられます:
- モデルにエイリアス定義が無い場合は、即座にそのままの名前を返す軽い分岐(
if @aliasesなど)があり、オーバーヘッドがほぼない。 - エイリアスがある場合のみ、テーブル(ハッシュ)の lookup 等を行う。
- 同じメソッドを属性読み取りの他の経路でも共有することで、属性名解決ロジックを一箇所にまとめている。
ベンチマーク結果の意味
PR 説明にある 3 パターンのベンチマークは、read_attribute の呼び出しパターン別の性能を比較したものです。
no-alias
- モデルに属性エイリアスがないケース。
- 変更前: 約 12.83M i/s
- 変更後: 約 14.92M i/s
- 約 1.16 倍高速化(1 回あたり ~67ns)
aliased-canonical
- エイリアスを定義しているが、「正規のカラム名」で読んでいるケース。
例:alias_attribute :alias_name, :canonical_nameがあって、read_attribute("canonical_name")している場合。 - 変更前 vs 変更後: 差分は誤差範囲内で「同等」。
- エイリアスを定義しているが、「正規のカラム名」で読んでいるケース。
aliased-name
- エイリアス名で読んでいるケース。
例:read_attribute("alias_name") - 変更前 vs 変更後: こちらも誤差範囲内で「同等」。
- エイリアス名で読んでいるケース。
つまり:
- エイリアス無しモデル:
read_attributeがはっきり速くなる。 - エイリアス有りモデル: エイリアスの有無や呼び出し方に関係なく、性能は実質的に変わらない。
- 影響範囲・注意点
影響範囲
- 対象:
ActiveRecord::Base#read_attributeを経由する全ての属性読み取り(model[:attr], 内部でのread_attribute呼び出しなどを含む)。 - 主な影響:
- API / 公開インタフェースとしての挙動は変わらない(同じ属性名を渡せば同じ値が返る想定)。
- モデルにエイリアスが定義されていない場合のパフォーマンスが改善。
- モデルにエイリアスがある場合も、少なくとも劣化はしないような実装になっている。
互換性・注意点
resolve_aliasは ActiveRecord 内部のヘルパーメソッドであり、これに依存したアプリケーションコードは通常存在しないはずです。- ただし、以下のようなケースでは注意が必要です:
- ActiveRecord の内部実装(特に attribute 読み取りのフック)を monkey patch している。
read_attributeを再定義していて、内部実装の細かい挙動や引数の型(Symbol/String)に依存している。
- そのような高度なカスタマイズをしている場合は:
resolve_aliasの実装(型変換のタイミング、to_sの有無など)を確認し、既存パッチと矛盾がないかをチェックすると安全です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59121
- 関連箇所:
activerecord/lib/active_record/attribute_methods/read.rb(read_attributeの定義)resolve_aliasが定義されているモジュール(おそらく同ファイル内またはattribute_methods.rb付近)
- コンテキスト:
- 最近の Rails / ActiveRecord では、属性アクセス周りのオーバーヘッド削減(YJIT や PRISM 対応と合わせた高速化)が継続的に行われており、その一環として内部ヘルパーの統一と分岐の単純化が進められています。
#59118 Render collection items into one buffer when nothing needs them apart
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
render partial: ..., collection: ...で、キャッシュや per-item レイアウトを使わない場合に限り、「要素ごとのバッファ」をやめて1つのバッファにまとめて描画するようにした最適化です。これにより、コレクション描画時のオブジェクト生成数と処理時間をわずかに削減しつつ、出力結果は従来と完全に同じになるよう保たれています。
- 変更内容の詳細
これまでの挙動
render partial: "item", collection: @items のようなレンダリングでは、各要素ごとに:
OutputBufferを1つ生成- その中に部分テンプレートを描画
- 描画結果を
SafeBufferにコピー - それを
RenderedTemplateオブジェクトでラップ - 最後に
RenderedTemplateたちのbodyを配列に集めてjoinし、もう1つSafeBufferを生成して最終的な文字列を得る
というフローでした。
結果として「各要素ごとに 5 個程度のオブジェクトが、最終的には結合されるだけのために」生まれていました。
新しい挙動の方針
PRのポイントは:
「要素ごとの出力を個別に扱う必要がないケースでは、最初から1つのバッファに全部描画してしまう」
です。
要素ごとの出力(各アイテムの HTML)を個別に必要とするケースは2つだけと整理されています:
コレクションキャッシュ
render partial: "item", collection: @items, cached: trueなどで、各要素ごとにキャッシュを保存したり取り出したりする場合- このときは「アイテム単位で」HTMLを扱う必要がある
per-item レイアウト
render partial: "item", collection: @items, layout: "wrapper"など、各アイテムをレイアウトで包む場合- これも「アイテム単位で」バッファが必要
これら2つのケースについては、従来通り「要素ごとのバッファ」を使うパスを維持します。
一方、それ以外の一般的なケース(キャッシュなし・per-item レイアウトなし)では:
- コレクション全体を1つの
OutputBufferに描画 - その
OutputBufferをTemplate#renderに「既存の buffer 引数」として渡す - アイテム間の「スペーサー」(
spacer_templateなど)は、この単一バッファにアイテムの合間に append - 最終的に1つの
RenderedTemplateとして返す
という構造に変更されています。
出力の互換性
PR本文でも強調されていますが、以下は以前とまったく同じになるように保証されています:
- 出力 HTML 全体
- スペーサーテンプレートの扱い
counterやiterationオブジェクト(render ... collection: ...時に各要素に渡されるオブジェクト)- block 付き
renderの挙動 - 「親テンプレートの中からコレクションを描画し、そのアイテムの中でさらに nested partial を描画する」といったバッファの入れ子構造
これを検証するために:
- 既存の render 周りのテストスイート一式
- 新たに2つのテスト
- View レベルでの spacer テンプレートに関するテスト
- 親テンプレートからコレクションを描画し、各アイテムがさらに nested partial を描画するケース(バッファネストの検証)
が追加されています。
- 影響範囲・注意点
影響範囲
対象は ActionView のうち、collection_renderer.rb が担当する「コレクションレンダリングパス」です。具体的には:
render partial: "xxx", collection: @itemsrender @items(デフォルト partial推論を使うケース)spacer_templateを使ったコレクションレンダリング
などの内部処理が変わっています。
ただし、挙動面では:
- 出力される HTML・テキスト
locals/as:/collection:/spacer_template:/layout:オプションの外部 API- キャッシュキーの扱い(キャッシュを使う場合は従来パスを維持)
はいずれも互換性維持が意図されており、テストでもカバーされています。
パフォーマンスへの影響
PRでの計測結果(1レイアウト + 1テンプレート + 20要素のコレクション部分テンプレートを描画するケース):
| 指標 | before | after |
|---|---|---|
| allocations per request | 3172 | 3070 |
| time per request | 858µs | 803µs |
- 約 100 オブジェクト強のアロケーション削減(20要素で5オブジェクト/要素 → 理解に合う値)
- リクエストあたり ~6% 程度の時間短縮という軽量な最適化
大規模なコレクションレンダリングを多用している場合、GC 負荷軽減やレイテンシ削減の小さな積み重ねとして効いてくる可能性があります。
注意点・互換性リスク
- API 変更はなく、アプリケーションコードの変更は不要な設計です。
- ただし、ActionView の内部バッファ実装に依存した「裏技的」な metaprogramming や monkey patch をしている場合は、
- 「アイテムごとに
RenderedTemplateが返ってくる」ことを前提にしていたコード - コレクションレンダリング時の内部バッファ構造に依存していたコード
などは影響を受ける可能性があります(通常のアプリではほぼ心配不要)。
- 「アイテムごとに
- 参考情報 (あれば)
変更ファイル
actionview/lib/action_view/renderer/collection_renderer.rb- コレクションレンダリングのメインロジック。
- 「単一バッファパス」と「per-itemパス(キャッシュ・レイアウト向け)」の条件分岐と実装が追加。
actionview/test/template/render_test.rb- 上記挙動を保証するためのテストが2つ追加。
類似コンテキスト
- Rails では、
ActionView::OutputBuffer/ActiveSupport::SafeBufferを使った XSS セーフな文字列結合が行われており、
本PRはその仕組みの上で「バッファ個数を減らす」ことにフォーカスしたマイクロ最適化です。
- Rails では、
#59075 Fix composite-key association query edge cases
マージ日: 2026/10/15 | 作成者: @matthewd
- 概要 (1-2文で)
このPRは、Active Record における複合主キー(composite key)を使った関連・クエリ周りのいくつかのエッジケースを、単一カラム主キーと同等の振る舞いになるように修正するものです。主に「空配列条件」「polymorphic な関連 + 複合キー」「preload 時の available_records」「複合キー関連への単一id指定」などの不整合・例外を改善しています。
- 変更内容の詳細
(1) where([:a, :b] => []) などで WHERE () が出ていた問題の修正
現状の問題:
Model.where([:a, :b] => [])
# => これまで SQL 的に意味のない WHERE () が生成されていた同様に、複合キーな関連に対して:
parent.association(:children).reader.where(parent: [])
# あるいは where(assoc: [])のように「複合キーの値の配列」が空のとき、単一カラム主キー (where(id: [])) と違って「常に偽」と認識されず、WHERE () のような壊れた条件になっていました。
今回の修正:
where([:a, :b] => [])やwhere(assoc: [])(assoc が複合キー関連)の場合でも、- それを 矛盾した条件(常に結果が空になる条件) と認識し、
- 単一カラムの
where(id: [])と同じように 実際のクエリ実行をスキップ するように変更。
- 内部的には
where_clause.rbとarel/visitors/to_sql.rbに変更が入り、- 空タプル配列が来たときに「空 IN 句」を発行せず、「このクエリは結果が0件」という扱いにします。
イメージ:
# 単一主キー
Post.where(id: []) # => 実行されず、結果は Relation(空)
# 複合キー (例: [:account_id, :code] が主キー)
Item.where([:account_id, :code] => [])
# => 以前: 変な WHERE () / または DB エラーの可能性
# => 今回から: 矛盾と認識してクエリスキップ(結果は空)(2) where(polymorphic: relation) が複合キーをターゲットにすると落ちていた問題
現状の問題:
polymorphic な関連に対して、relation をそのまま渡すパターン:
Comment.where(commentable: SomeModel.where(...))- 単一主キーであれば、
SomeModel.where(...).pluck(:id)相当の処理が内部で行われ正常に動作。 - しかし
SomeModel側が複合主キー(複数カラム)である場合、- 既存コードが想定しておらず、
- polymorphic 用の処理経路で 例外(raise) が発生していた。
今回の修正:
ActiveRecord::Relation::PredicateBuilder::PolymorphicArrayValue周辺を修正し、- polymorphic な where 句で relation を受け取る場合に、
- 複合キーターゲットでも落ちないように処理を揃えた。
- 単一主キーの場合と同様に、適切な ID 群を取り出して IN 句相当を組み立てられるよう対応。
※複合キーと polymorphic の組み合わせは設計としてかなりレアケースですが、「落ちる」状態から少なくとも正常系に寄せる方向の修正です。
(3) preload 時に複合キー関連で available_records が無視されていた問題
現状の問題:
ActiveRecord::Associations::Preloader::Association には available_records という仕組みがあり、preload 対象となる関連レコードのサブセットを事前に指定できます。
# 例: ある条件を満たす関連だけを preload の対象にするような高度な使い方
preloader = ActiveRecord::Associations::Preloader.new(
records: owners,
associations: :items,
scope: Item.where(active: true),
available_records: Item.where(...)
)- 単一主キーの関連では、この
available_recordsが適切に利用される。 - しかし 複合キーの関連 では、
available_recordsが無視され、本来除外されるべきレコードも preload されてしまっていた。
今回の修正:
preloader/association.rbに 1行の修正が入り、- 複合キーの関連に対しても
available_recordsが正しく適用されるようになりました。
- 複合キーの関連に対しても
- これにより、preload の絞り込み挙動が単一キー/複合キーで一貫するようになります。
(4) 複合キー関連に「裸の id(単一値)」を渡すと NoMethodError になっていた問題
現状の問題:
複合キーな関連に対して、誤って「単一の id 値」を指定すると、内部処理で想定外の操作が入り NoMethodError になっていました。
# 例: child は [:account_id, :code] の複合キーを持つ関連
parent.child.find(1)
# or where(child: 1)
# => これまで: NoMethodError など、意味不明な例外が発生今回の修正:
PredicateBuilder::AssociationQueryValueにガードロジックを追加し、- 複合キー関連に対して 単一の id を渡した場合 に、
- 意図を説明する
ArgumentErrorを明示的に発生させるよう変更。
期待される挙動:
parent.child.where(id: 1)
# => ArgumentError: "Composite key association expects an array of ids"(趣旨として)これにより、開発者は「複合キー関連には配列(あるいは適切な形のタプル集合)を渡す必要がある」ということをすぐに理解できます。
- 影響範囲・注意点
- 複合主キーを使っていないプロジェクト
- ほぼ影響はありません。内部実装のロジックは単一キー向けの挙動を変えない形で慎重に拡張されています。
- 複合主キー + 関連を積極的に使っている場合
- 以下の挙動変更があります:
where([:a, :b] => [])やwhere(assoc: [])で、以前は壊れた SQL または DB 依存の挙動だったものが、「即座に結果0件」として扱われます。
→ 実運用上は「バグっていた状態が正される」ため、想定どおりの結果になるケースが多いはずです。- polymorphic な関連に relation を渡すフォームで、複合キーターゲットが正しく扱われるようになります。以前は例外で落ちていたところが正常に動作するようになるため、「落ちなくなった」ことに注意してください(テストでそこを期待していたなら修正が必要)。
- preload の最適化や限定的 preload に
available_recordsを使っている場合、複合キー関連でもその制御が効くようになります。
→ これにより、今まで「なぜか複合キー関連だけ余計に preload される」ような状況が解消されます。 - 複合キー関連に単一 id を渡していた隠れたバグがある場合、これまでたまたま NoMethodError などで気付きづらかったところが、
ArgumentErrorとしてより明確に露呈します。
→ テスト実行時に新たなArgumentErrorが出たら、関連のキー指定方法を見直す必要があります。
- 以下の挙動変更があります:
- 後方互換性
- 仕様としては「これまで明確に定義されていなかった(あるいは壊れていた)挙動を、単一キーと整合する形に揃える」変更なので、基本的には後方互換性を向上させる方向の修正です。
- ただし、「壊れた状態に依存していた」コードがある場合(特に例外の種類に依存している場合)はテストが落ちる可能性があります。
- 参考情報 (あれば)
- 該当ファイル:
activerecord/lib/active_record/relation/where_clause.rb- 空のタプル条件を矛盾として扱うロジックの追加。
activerecord/lib/arel/visitors/to_sql.rb- 空の IN 句や空タプルに対する SQL 生成の扱いを調整。
activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb- 複合キー関連に対する bare id の扱い(ArgumentError 付与)。
activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb- polymorphic + relation + 複合キー時の処理改善。
activerecord/lib/active_record/associations/preloader/association.rb- 複合キー関連でも
available_recordsを尊重するように修正。
- 複合キー関連でも
- テスト:
belongs_to_associations_test.rb,associations_test.rb,where_clause_test.rb,where_test.rbに多数の追加テストがあり、- 上記の挙動(空配列、複合キー + polymorphic, preload, ArgumentError)が網羅的にカバーされています。
このPRは「今後の内部リファクタリングの準備」として見つかった既存バグを潰したものなので、Rails 内部のキーまわりをいじる予定のある場合は特に、今回導入されたガードやエッジケース処理を念頭に置いておくとよいです。
#59116 Add a new private reader method, refactor alias resolution
マージ日: 2026/10/15 | 作成者: @tenderlove
- 概要 (1-2文で)
このPRは ActiveRecord の属性読み取りまわりを内部的に最適化し、属性取得とカラムエイリアス解決の高速化を行う変更です。通常ケース(属性が存在し、エイリアスも特に定義されていない)のパスを極力軽くし、Ruby インタプリタと ZJIT 双方でベンチマーク性能を改善しています。
- 変更内容の詳細
2-1. プライベートな属性リーダーメソッドの追加
ActiveModel / ActiveRecord の属性取得処理に、内部専用の高速なリーダーメソッドが追加されています。
ポイント:
- 「外部APIとしては公開しない」プライベートメソッド
- 引数は必ず文字列(String)の属性名だけを受け取る前提
- 呼び出し側で
attr_name.to_sのような変換処理を済ませておく
- 呼び出し側で
- ブロックで「見つからない時の処理」を渡すのではなく、
- 通常ケース: 既存属性をそのまま即座に返す
- 例外ケース: 新しい
fetch_value!が 例外を投げる(missing 属性用の遅いパス)
従来は:
# 擬似コードイメージ
value = attributes.fetch(name) { handle_missing(name) }のように fetch + ブロックで「見つからない場合ロジック」を渡すことが多かったところを、
# 擬似コードイメージ
def _read_attribute_internal(name_string)
fetch_value!(name_string) # 無ければ例外
endのように、「ほぼ常に存在する」ことを前提にした直線的なパスに寄せています。
理由:
- 「属性が存在しない」ことは非常にレアケースであるため、そこにコストを支払うべきではない
- ブロックを渡すスタイルは、インタプリタ・JIT ともに最適化しづらい
- ブロックを使わず、例外による遅いパスに逃がすことで、ホットパスを最小限の分岐とコールだけにできる
結果として、Ruby インタプリタおよび ZJIT 双方で属性読み取りが軽くなり、ActiveRecord 全体のベンチマークが改善しています。
2-2. fetch_value! メソッドの追加
新しい内部メソッド fetch_value! は、指定した属性が存在しない場合に 例外を投げることが保証される取得メソッドです。
特徴:
- 「必ず存在しているはずの属性」を取るために使う
- 見つからない場合の戻り値をブロックで指定するのではなく、即座に例外
- そのため JIT がインライン化しやすく、分岐も単純になる
イメージコード:
def fetch_value!(attr_name)
if @attributes.key?(attr_name)
@attributes[attr_name]
else
raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}"
end
end(実際の実装名や例外クラスは ActiveModel / ActiveRecord 側の定義に従いますが、概ねこの発想です)
このメソッドの上に前述の「文字列前提のプライベート reader」が乗る構造になっていると考えられます。
2-3. カラムエイリアス解決の「アイデンティティ関数」を導入
ActiveRecord には「DB カラム名と Ruby 側の属性名をマップするエイリアス機構」がありますが、その解決処理にも最適化が入っています。
従来:
- 毎回「エイリアスマップを見に行って名前解決」するような実装(= ある程度重いパス)になりがち
変更後:
- デフォルトでは「恒等写像 (identity function)」を持つ
- つまり、エイリアスを定義していない限り
alias_for(column_name) == column_nameという軽い処理
- つまり、エイリアスを定義していない限り
- ユーザがエイリアスを定義した場合にのみ、
- この identity function を より遅い lookup 処理に差し替える
擬似コードイメージ:
# デフォルト(エイリアス無し)
@resolve_column_alias = ->(name) { name }
# ユーザがエイリアスを定義したとき
@resolve_column_alias = ->(name) { @aliases[name] || name }これにより:
- 「エイリアスを使っていない(大多数)」ケースのパフォーマンスが向上
- エイリアスを使うと、そこだけ遅いパスに乗る(が、それは構成上明示的な選択)
2-4. ActiveRecord 各所での呼び出しパスの整理
変更ファイルには以下が含まれています:
activerecord/lib/active_record/associations/...activerecord/lib/active_record/attribute_methods*.rbactivemodel/lib/active_model/attribute*
これらは次のような意図で書き換えられていると考えられます:
- 旧来の「柔軟だが遅い」属性読み取りを、
- 新しい内部専用 reader /
fetch_value!を使うようにリファクタリング
- 新しい内部専用 reader /
- Association・Autosave・Preloader など、
- 頻繁に attribute を読む箇所が 新しいホットパスに乗るように統一
- テスト (
attribute_set_test.rbなど) の追加により、- 新しい内部メソッドの挙動(存在しない属性での例外など)をカバー
特に belongs_to_association や各種 association クラスは、レコードの読み出し/保存のたびに attribute を参照するため、この周辺が最適化されることで全体のスループット向上につながります。
2-5. ベンチマーク結果
PR 説明にある計測結果:
- ActiveRecord (インタプリタ)
- main: 平均 195ms → このブランチ: 173ms(約 11% 改善)
- ActiveRecord (ZJIT)
- main: 平均 74ms → このブランチ: 61ms(約 18% 改善)
いずれも ruby-bench の ActiveRecord ベンチマークに基づく計測で、属性アクセスが多い実ワークロードを想定した最適化であることが分かります。
- 影響範囲・注意点
外部 API の互換性:
- 追加されたメソッドは「プライベート reader」「内部用
fetch_value!」などであり、通常の ActiveRecord/ActiveModel 公開 API には手を出していません。 - そのため、一般的な利用コードに対しては互換性は高く、挙動変更はほぼ無いと考えられます。
考えられる影響・注意点:
内部 API に依存している場合
- プライベート/内部メソッドに依存したメタプログラミング、Monkey Patch をしているアプリ/ライブラリは、呼び出し先が変わっている可能性があります。
- とくに
attributesの直接参照や alias 解決に干渉しているコードは、行数の多い association 周りの変更の影響を受けるかもしれません。
Missing Attribute の扱い
- 「属性が存在しない」場合に例外を投げるパスがよりはっきり分離されたため、
- 今まで「たまたま nil を返していた」ような非公認挙動に依存していた場合は問題になる余地があります。
- とはいえ、MissingAttribute はもともと例外パスとして扱われることが多く、標準的な使い方では問題にならないことがほとんどです。
- 「属性が存在しない」場合に例外を投げるパスがよりはっきり分離されたため、
カラムエイリアスの挙動
- 「エイリアスを定義すると、それ以降そのモデルの alias 解決関数が差し替わる」という設計がより明示化されています。
- API レベルの挙動は従来通り(定義されたエイリアスが優先)で、動作仕様が変わるというよりは実装の経路が変わったに近いです。
パフォーマンス計測/チューニング
- 大量のレコードを扱うアプリケーションや、高頻度で association を辿るコードで、恩恵が出やすい変更です。
- ruby-head + Rails main を追いかけている場合は、独自最適化(キャッシュやラッパー)より先に、この最適化の効果を確認する価値があります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59116
- 関連しそうな読みどころ:
activerecord/lib/active_record/attribute_methods/read.rb
→ 新しい内部リーダー実装の中心activemodel/lib/active_model/attribute_set.rb/attribute.rb
→fetch_value!や attribute セット周りの変更attribute_set_test.rb
→ Missing attribute などのテストケースで新挙動を確認可能
- 背景:
- ruby-bench の ActiveRecord ベンチをターゲットにした最適化
- Ruby インタプリタと ZJIT の両方を意識し、「ブロックを渡さない」「共通ホットパスを単純化する」「エイリアス無し時の identity function を導入する」という JIT フレンドリーな設計方針が見て取れます。
#59112 Build the render_template instrumentation payload only when subscribed
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
render_template.action_viewのインストルメンテーション用 payload Hash を、「購読者がいる場合にだけ」生成するように変更し、特に本番環境での不要なオブジェクト割り当てを削減するパフォーマンス改善のPRです。イベント名や payload の中身・挙動自体は既存の購読者から見て互換性が保たれています。
- 変更内容の詳細
何が問題だったか
これまでの ActionView では、テンプレートやパーシャルをレンダリングするたびに、次のような流れになっていました:
!render_template.action_viewイベント向けの payload (Hash) を毎回生成ActiveSupport::Notifications.instrument("!render_template.action_view", payload) { ... }を実行- 実際にはフレームワーク本体ではこのイベントに誰も subscribe しておらず、主にテストヘルパ (
assert_templateなど) が利用する程度 - 結果として、本番環境ではほぼ毎レンダリングごとに「GC行きのHash」を作って捨てているだけ、という状態
特に、レイアウト + メインテンプレート + コレクションパーシャル多数、という典型的な画面では、テンプレートの数だけ無駄な Hash が作られていました。
今回の変更内容
この PR では、
「購読者がいるかどうかを先に確認してから payload を作る」
という順序に変更しています。
Ruby レベルのイメージとしては、ざっくり次のような差分です:
# 変更前 (イメージ)
def render(*)
payload = {
identifier: identifier,
layout: layout,
# ... 他にもキーがある
}
ActiveSupport::Notifications.instrument("!render_template.action_view", payload) do
render_without_instrumentation
end
end
# 変更後 (イメージ)
def render(*)
if ActiveSupport::Notifications.notifier.listening?("!render_template.action_view")
payload = {
identifier: identifier,
layout: layout,
# ... 既存と同じ中身
}
ActiveSupport::Notifications.instrument("!render_template.action_view", payload) do
render_without_instrumentation
end
else
# 誰も聞いていないならインストルメンテーション自体をスキップ
render_without_instrumentation
end
end※ 実際のコードでは notifier の API や条件判定の書き方は多少異なる可能性がありますが、やっていることはこのイメージ通りです。
テスト追加
actionview/test/template/template_test.rb にテストが追加され、以下を保証しています:
!render_template.action_viewに subscribe したとき、従来どおり同じイベント名・同じ payload が通知される- つまり、「購読者がいるときの観測可能な挙動」は完全に後方互換であることをテストで固定(ピン)している
- 影響範囲・注意点
影響範囲
- 対象:
!render_template.action_viewイベントのインストルメンテーション(テンプレート / レイアウト / パーシャルの render) - 変更点:
- 購読者がいない場合
- これまで: 毎回 payload Hash を生成し、
instrument呼び出しが行われる(が、誰も受け取らない) - これから: payload 生成も
instrument呼び出しも行われない
- これまで: 毎回 payload Hash を生成し、
- 購読者がいる場合
- これまで: 毎回 payload を生成 →
instrument→ subscriber が受け取る - これから: 事前に「購読者あり」と判定された場合のみ、これまで通り payload を生成して
instrumentされる - payload の中身とイベント名は変更なし(テストで保証)
- これまで: 毎回 payload を生成 →
- 購読者がいない場合
パフォーマンスへの影響
PR 説明にある計測例:
- レイアウト + メインテンプレート + 20件のコレクションパーシャルを描画するページで
- 1リクエストあたりのオブジェクト確保数: 3878 → 3857 (21 減少)
- 「1レンダリングあたり 1オブジェクト減」と見なせる
規模としては小さい削減ですが、「プロダクションでは普通 subscribe しないテスト用イベント」のコストを削る、という意味で筋の良い最適化です。大量のテンプレート/パーシャルを描画するビューでじわじわ効いてきます。
注意点 / 互換性
!render_template.action_viewを listen しているコードがある場合:- そのコードの動作は、購読している限り変わりません(イベントは引き続き発火し、payload も同じ)
- 逆に、「これまで誰も subscribe していなかったが、
instrumentの呼び出しだけを副作用として期待していた」ようなコードがもしあれば、動作が変わります- ただし、このイベントはそもそもそうした用途を想定しておらず、普通は問題にならないはずです
- 通常のアプリケーション開発者視点では、コード変更や設定変更は不要で、そのままパフォーマンス改善の恩恵だけ受けられます
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59112
- 関連箇所:
actionview/lib/action_view/template.rb- テンプレートレンダリング時のインストルメンテーション周りの実装
actionview/test/template/template_test.rb!render_template.action_viewイベントの payload 後方互換性を担保するテストが追加
開発者としては、「Notifications イベントを追加する際は、購読者がいるかどうかを先に確認してから、重い payload を組み立てる」というパターンの良い実例としても参考になります。
#59117 Append tag attributes without a string per attribute
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Rails のtagヘルパ内部で HTML 属性文字列を組み立てる処理を見直し、属性ごとに一時文字列を生成せずにバッファへ直接追記するようにして、オブジェクト生成数と処理時間を削減するパフォーマンス改善です。外部から見た挙動や出力 HTML は一切変わりません。
- 変更内容の詳細
何を変えたか
対象は ActionView::Helpers::TagHelper の内部メソッド群(tag_options, tag_option, prefix_tag_option, boolean_tag_option など)です。
従来の実装:
- 1属性ごとに
%( key="value")のような補間文字列を作成し、それをバッファに<<していた data-/aria-といった prefix 付き属性や boolean 属性も、やはり 1属性 = 1文字列を生成- Symbol キーは
to_sで文字列化しており、そのたびに新しい String を生成
この PR 後の実装:
- 「属性ごとに一時文字列を作る」のをやめ、「一つの出力バッファに
<<で直接書いていく」スタイルに変更例えばイメージとしては、以前は:
ruby# 擬似コード(従来) buffer = +"" options.each do |key, value| buffer << %( #{key}="#{value}") # ここで新しい String が1つできる endのような構造だったのが、
ruby# 擬似コード(新) buffer = +"" options.each do |key, value| buffer << " " buffer << key_string buffer << '="' buffer << value_string buffer << '"' endのように、1属性あたり複数回の
<<で直接バッファを伸長していくスタイルになっています。
data-/aria-prefix 付き属性や boolean 属性向けにも、同様に「直接追記」するための専用 append 系メソッドを追加- 例:
append_tag_option,append_prefix_tag_option,append_boolean_tag_option(名称は説明上のものですが、原理としてはこういう役割のメソッドができた)
- 例:
- 既存の
tag_option,prefix_tag_option,boolean_tag_optionは削除せず、「新しい append 系メソッドを内部で呼び出したうえで新しい String を返すだけの薄いラッパー」として残している- これにより、既存コードでこれらメソッドを呼び出している箇所(このファイル内のみ)はそのまま動作し、かつ API の意味も変わらない
Symbol キーの扱い改善
- 以前は
key.to_sで文字列化していたため、そのたびに新たな String を生成 - 今回
Symbol#nameを使うようにし、不要な String の割り当てを減らしているSymbol#nameはto_sと異なり、より低コストに内部名を取り出せるのが期待されている(Ruby のバージョン実装依存はあるが、PR の意図としては allocation 削減)
出力は完全に不変
- HTML のレンダリング結果(文字列内容)は一切変えておらず、
tagヘルパ、form ヘルパ、URL/asset ヘルパのテストで網羅的に確認済み tag_option,prefix_tag_option,boolean_tag_optionはこのファイル以外からは呼び出されておらず、外部の gem やアプリケーションが壊れるリスクが低い構造
ベンチマーク結果(allocations / time)
属性数に応じて allocation が減少:
| 呼び出し例 | 変更前 | 変更後 |
|---|---|---|
tag.div(text, class: "card") | 8 | 7 |
tag.input(type: "text", name: "q", value: text) | 10 | 7 |
tag.input(type: "checkbox", disabled: true, required: true) | 9 | 6 |
tag.div(text, data: { controller:, action: }, aria: { label: }) | 20 | 17 |
tag.span(text, class: ["a", "b"], data: { id: 1 }, id: "x") | 26 | 23 |
時間もわずかに短縮:
tag.div(text, class: "card")
1077 ns → 991 nstag.input(属性3つ)
1498 ns → 1356 ns- 属性5つの
tag.span
3396 ns → 3031 ns
実際のページ描画例(リンク行 20、本フォームとボタンそれぞれ 1)のリクエスト単位では:
- allocations: 3303 → 3052(約 7.6% 減)
- 影響範囲・注意点
- 影響を受けるユーザー API:
tag,content_tagなどの HTML タグ生成ヘルパを使うすべてのコード- ただし「挙動は一切変わらず、内部的に高速・省メモリになっただけ」
- 変更されたメソッドは内部実装 (
tag_optionsおよびその下請け) のみで、public API・引数仕様・HTML 出力形式(属性の順序含む)は変化なし TagHelperの内部メソッド (tag_option等) を直接呼んでいる外部コードが理論上は存在し得るが、Rails 本体ではこのファイル外に使用箇所はないと明示されている- Boolean 属性や
data-/aria-属性の扱いロジックは変えていないため、disabled: trueやdata: { controller: ... }のような既存コードはそのまま動作 - 実装がやや「低レベルな
<<ベタ書きスタイル」に近づくため、今後同ファイルを修正する際にはバッファ操作の順序やエスケープ処理を壊さないよう注意が必要
- 参考情報 (あれば)
- 対象ファイル:
actionview/lib/action_view/helpers/tag_helper.rb(+36 / -8) - 変更の主目的:
- HTML 属性生成の際の String allocation 削減
- シンボルキーの変換コスト削減 (
Symbol#name利用)
- テストカバレッジ:
- tag ヘルパ、form ヘルパ、URL ヘルパ、asset ヘルパのテストスイートで出力不変を確認済み
- 実運用上の期待効果:
- 属性の多いタグが大量にレンダリングされるページ(管理画面、SPA との境界など)で GC プレッシャー軽減・わずかなレスポンス改善が見込まれる
#59115 Delimit numbers in place and assemble format options in one pass
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
number_to_delimitedを中心とする数値フォーマット系ヘルパーの内部実装を最適化し、桁区切り処理とフォーマットオプション組み立てのアロケーションを大幅に削減したパフォーマンス改善のPRです。出力結果や既存の挙動は変えずに、メモリアロケーション数・実行時間の両方を減らしています。
- 変更内容の詳細
2-1. number_to_delimited の桁区切り処理の最適化
従来の実装:
- 数値 → 文字列化
- 整数部と小数部に
splitで分割 - 整数部を右から3桁ごとに
sliceして配列化 - その配列を
joinして区切り文字を挿入 - 符号・小数部などを parts 配列に詰めて
compact→join - というように「配列を何度も作る・文字列を何度も結合する」構造
このため、1234567 → "1,234,567" にするだけで十数個のオブジェクトが生成されていました。
新しい実装:
- 整数部の文字列に対して「右から3桁ごとに区切り文字をインプレースで挿入」する形に変更
- Ruby の
Integer#to_sは毎回新しいStringを返すので、その文字列をそのまま破壊的に加工しても安全 - 文字列入力 (
"1234567") の場合は一度プレーンなStringにコピーしてから同じ処理を実行
- Ruby の
- 小数部については従来どおり
splitを利用しており、次のような「変な」入力も挙動を変えずに維持:"1234.""1.2.3"
delimiter_patternを使ったカスタム区切りパターンは手を触れていないため、既存のカスタム挙動はそのまま
SafeBuffer への配慮:
- これまで、
number_to_delimitedにActiveSupport::SafeBufferを渡すと、内部でsplitされ「普通のString」として扱われていました。 - 新実装でも同じく一度プレーンな
Stringにコピーしてから変更を加えることで、- 戻り値は
String - デリミタが HTML エスケープされずそのまま挿入される
- 戻り値は
- という従来の挙動を維持するよう、新しいテストが追加されています。
2-2. フォーマットオプション組み立ての最適化
対象: ActiveSupport::NumberHelper::NumberConverter 内のオプションマージ処理。
従来:
- 2つのデフォルトハッシュ
- I18n 翻訳を 2 回 lookup(それぞれに空ハッシュをデフォルトとして渡す)
- さらにそれを
dup→mergeでマージ - 毎回複数のハッシュが生成・コピーされていた
新実装:
- すべてのフォーマットオプションを1回のパスで 1つの Hash に組み立てる
- 優先順位(呼び出し時オプション > I18n 設定 > デフォルト)自体は従来どおり
- I18n 翻訳結果はコピーせずにそのまま読む
- I18n lookup のデフォルト値として「共有の frozen な空 Hash」を使う
- 翻訳の namespace + key はコンバータクラスごとに1回だけ構築して再利用
これにより:
- 不要な Hash の
dup/mergeが削減 - GC プレッシャーが軽減
- 各種
number_to_*ヘルパーの全体的なオーバーヘッドを削減
2-3. ベンチマーク結果
PR 説明にある計測結果:
| メソッド | before (alloc) | after (alloc) |
|---|---|---|
number_to_delimited(1234567) | 26 | 12 |
number_to_delimited(1234567.89) | 27 | 16 |
number_to_delimited("1234567") | 28 | 15 |
number_to_currency(1234567.891) | 85 | 68 |
number_to_percentage(12.345) | 82 | 63 |
number_to_rounded(1234.5678) | 60 | 45 |
number_to_human(1234567) | 184 | 165 |
100,000 number_to_delimited | 0.269s | 0.193s |
- アロケーション数が 20〜40% 程度削減されているケースが多い
- 10万回実行で約 0.27s → 0.19s に高速化
2-4. 出力の互換性検証
- 対象: integers, floats, strings,
BigDecimal,Rational,Infinity,NaN,nil - オプション: sign 有無、
delimiter,separator,pattern,localeの組み合わせ - 合計 390 パターンで
mainブランチと出力を比較し、すべて一致
これにより、「高速化したがフォーマット結果が変わってしまう」類のレグレッションがないことを確認しています。
- 影響範囲・注意点
- 対象となる主なヘルパー
number_to_delimitednumber_to_currencynumber_to_percentagenumber_to_roundednumber_to_human- これらはすべて
NumberConverter/NumberToDelimitedConverterを経由するため、内部的には今回の変更の恩恵を受ける
- 挙動の変更有無
- フォーマット結果は 390 パターンの比較で完全一致
"1234."や"1.2.3"のような edge case も既存挙動を維持delimiter_patternを使うケースも未変更- I18n から読み込まれるフォーマット設定(区切り文字・小数点文字など)の優先順位も同じ
- SafeBuffer / エスケープ
SafeBufferを渡しても内部でプレーンStringとして扱われる点は従来どおり- 戻り値は
Stringで、デリミタがエスケープされない挙動も固定テストで担保済み - 「フォーマット済み文字列をそのまま HTML に差し込む」ようなコードでは、これまで通り呼び出し側で
html_safe等が必要な点に注意
- カスタマイズしている場合
ActiveSupport::NumberHelperを monkey patch していたり、内部実装に依存したコード(private メソッド呼び出しなど)がある場合は、今回のリファクタリングで壊れる可能性があるため要確認- 公開 API (
number_to_*系) に対する通常の利用であれば、挙動はそのままでパフォーマンスだけ向上する想定
- 参考情報 (あれば)
- 変更ファイル:
activesupport/lib/active_support/number_helper/number_converter.rbactivesupport/lib/active_support/number_helper/number_to_delimited_converter.rbactivesupport/test/number_helper_test.rb
- PR タイトルが示す通り:
- "Delimit numbers in place" → 数値の桁区切りをインプレースで実行
- "assemble format options in one pass" → フォーマットオプションを 1 パスで組み立てる
- 数値フォーマットのパフォーマンスがクリティカルな(大量レンダリング・レポート生成などの)アプリケーションでは、この変更により GC 圧が下がり、応答時間改善が期待できます。
#59114 Build token_list and class_names with fewer intermediate arrays
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Rails のActionView::Helpers::TagHelperにあるtoken_list/class_namesの内部実装が、余分な配列生成や不要な HTML アンエスケープ処理を減らす形にリファクタリングされました。挙動は一切変えずに、オブジェクト生成数と実行時間を削減するパフォーマンス改善です。
- 変更内容の詳細
対象メソッド
ActionView::Helpers::TagHelper#token_listActionView::Helpers::TagHelper#class_names(token_listのエイリアス)
Rails 7 以降でよく使われる:
tag.div(class: class_names("post", featured: true))
tag.div(class: token_list("a b", { c: false }, ["d"]))のような「クラス名やトークンの組み立てロジック」の内部が最適化されています。
以前の問題点(背景)
元の token_list はだいたい次のような流れでした(疑似コード):
- 与えられた引数(文字列, 配列, Hash, etc.)をそれぞれ
splitして配列化 - それら配列をさらに
flattenして 1 つの配列に - さらに別の配列にユニーク化(重複除去)
- 各値を
CGI.unescape_htmlでアンエスケープ(エンティティが含まれているかどうかに関係なく) - 最後に
safe_joinで結合(この中でも flatten が走る)
つまり、「配列 → flatten → 新しい配列 → さらに flatten → join」という形で、中間配列とオブジェクトが多く生成されていました。また、& を含まない普通の文字列に対しても毎回 CGI.unescape_html をかけており、不要な処理コストがかかっていました。
この処理は class_names も同じ(alias)なので、View でクラス名を組み立てるたびにこのコストが発生していました。
今回の最適化内容
PR の説明をもとにすると、新しい実装は以下のような方針になっています:
splitのブロック形式を使って 1 つの配列に直接集約
Ruby のString#splitには、戻り値ではなくブロックに対して分割結果を順次渡す形式があります:rubytokens = [] "a b c".split(/\s+/) { |t| tokens << t }これにより「1つの大きな配列を直接埋めていく」ことができ、中間配列や
flattenが不要になります。重複削除をインプレースで実施
tokens.uniq!のような形で、同じ配列を破壊的にユニーク化します。
これにより「元配列 → ユニーク化後の別配列」というコピーが不要になります。&を含む値だけCGI.unescape_html
文字列に&が含まれていなければ HTML エンティティは存在しない前提なので、アンエスケープ不要と判断できます:rubyif token.include?("&") token = CGI.unescape_html(token) endこれで、ほとんどの普通のクラス名(
"post","featured","btn-primary"など)にはアンエスケープ処理をかけずに済みます。safe_joinを使わず、自前でエスケープ+ join
最終的なトークン配列はすでにフラットなので、safe_joinによる追加のflattenは不要です。- 各トークンを HTML エスケープする(
ERB::Util.html_escape相当) - スペースで
join(" ")する
といった処理で同等の結果が得られます。
重要なのは、「出力結果(HTML エスケープの具合、空白処理、重複削除)」は従来と完全に同じであるように実装されている点です。既存のテストがそのまま通ることが PR 内で明記されています。- 各トークンを HTML エスケープする(
パフォーマンスの改善量
PR 内の計測結果(1回あたりのオブジェクト数/またはアロケーション数と思われる指標、および実行時間):
| ケース | 変更前 | 変更後 |
|---|---|---|
class_names("post", featured: true) | 18 | 11 |
class_names("a b", { c: false }, ["d"]) | 24 | 16 |
token_list("x") | 13 | 8 |
100,000 回の class_names 呼び出し | 0.140s | 0.108s |
高速化率はケースにもよりますが、10〜20% 程度の改善が出ているようです。View で class_names を多用するアプリでは GC 負荷やレンダリング時間がわずかに改善されます。
- 影響範囲・注意点
公開 API の挙動は変わらない想定
token_list/class_namesの入力パターン(文字列、配列、Hash、nil、false等の扱い)- whitespace の扱い(複数スペースを 1 個に潰すなど)
- 重複トークンの除去
- HTML エンティティの扱い
これらは既存テストでカバーされており、「出力は従来と同じ」と明記されています。
既存コード側での修正は不要
メソッドシグネチャや戻り値の型は変わらないため、アプリケーションコード・ライブラリコード側での対応は不要です。パフォーマンス指向のコードでの違い
- カスタムで
token_listを monkey patch していたり、内部実装に依存したテストを書いているような特殊なケースを除き、互換性の問題はほぼありません。 &を含むクラス名(例:"icon-&-text")やエンティティを意図的に埋め込んでいるケースでも、従来と同じ最終出力になるようになっています(必要な場合のみCGI.unescape_htmlが呼ばれるだけ)。
- カスタムで
総じて、「パフォーマンスが良くなっただけ」と考えて差し支えない変更です。
- 参考情報 (あれば)
対象ファイル:
actionview/lib/action_view/helpers/tag_helper.rb(+11 / -2 の小さな変更)
関連 API ドキュメント(参考):
ActionView::Helpers::TagHelper#tag/#content_tagActionView::Helpers::TagHelper#token_list/#class_names(Rails ガイドや API ドキュメント)
類似の最適化として、Rails では
safe_joinやhtml_escape周辺もこれまで何度かオブジェクト生成削減目的でリファクタされており、本 PR もその流れに沿った micro-optimization といえます。
#59113 Translate distance_of_time_in_words without an OptionMerger
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
distance_of_time_in_words/time_ago_in_wordsの翻訳処理からI18n.with_options(内部でOptionMergerを生成)を排除し、代わりに小さなヘルパーメソッド経由でI18n.tを直接呼び出すようにして、パフォーマンスとメモリアロケーションを改善した変更です。
挙動や使用される翻訳キー・スコープ・ロケールは従来どおりで、I18nのテストスイートで互換性が確認されています。
- 変更内容の詳細
背景・問題点
従来の distance_of_time_in_words は、内部で以下のように I18n.with_options を使っていました(イメージ):
I18n.with_options(locale: locale, scope: :'datetime.distance_in_words') do |locale_scope|
locale_scope.t(:less_than_x_seconds, count: 5)
# ...
endwith_options は内部で OptionMerger オブジェクトを生成します。このオブジェクトは、実際に t が呼ばれるたびに
- 共有オプション(
locale,scopeなど)と - 各呼び出し固有のオプション(
countなど)
をマージした新しい Hash を毎回生成します。time_ago_in_words は一覧画面などで「1行につき1回」レベルで大量に呼ばれることが多く、この余計なオブジェクト生成コストが無視できない状態でした。
新しいアプローチ
このPRでは、I18n.with_options をやめて、ロケールとスコープを直接 I18n.t に渡す小さなプライベートヘルパーを導入しています。
イメージとしては以下のような形です:
# 例: 新規に導入されたプライベートヘルパー(概念的な例)
def distance_of_time_translation(key, count:, locale:, scope: nil)
# scope が nil の場合は距離用のデフォルトスコープを適用する
effective_scope = scope || :'datetime.distance_in_words'
I18n.t(key, count: count, locale: locale, scope: effective_scope)
enddistance_of_time_in_words 内部では、今まで locale_scope.t(:less_than_x_seconds, count: ...) としていた箇所を、このヘルパー経由の I18n.t 呼び出しに書き換えています。
PRの説明にある通り:
- 翻訳キー(例:
:less_than_x_seconds,:x_minutes,:about_x_hoursなど) countの値localescope
が I18n に渡される最終的な内容は従来と同一になるように実装されています。scope: nil が明示的に渡された場合は「スコープなし」として扱われ、デフォルトスコープは fetch 経由で適用される設計になっているため、挙動の互換性も維持されています。
パフォーマンス上の効果
time_ago_in_words を対象に計測した結果:
| 変更前 | 変更後 | |
|---|---|---|
| 1回あたりのオブジェクトアロケーション数 | 26 | 17 |
| 100,000 回呼び出し | 0.467 秒 | 0.361 秒 |
OptionMerger を経由せずに I18n.t を直接呼ぶことで、1回あたり約35%程度のアロケーション削減、約23%程度の時間短縮が確認されています。
大量の行をレンダリングする画面(Activity feed、コメント一覧、ログ一覧など)で特に効果が期待できます。
- 影響範囲・注意点
- 対象:
ActionView::Helpers::DateHelperのうち、distance_of_time_in_words/time_ago_in_wordsでの I18n 呼び出しロジック - 公開APIのシグネチャ(
time_ago_in_wordsの引数など)は変わっていません。通常の利用方法をしている限り、アプリケーションコード側での修正は不要です。 - I18nの挙動
- 使用されるキー・ロケール・スコープは従来どおりになるようテストされており、既存の翻訳ファイル(
config/locales/*.yml)を変更する必要はありません。 - 明示的に
scope: nilを渡している場合でも、「スコープなし」として扱われる挙動は維持されています(デフォルトスコープはfetchで適用)。
- 使用されるキー・ロケール・スコープは従来どおりになるようテストされており、既存の翻訳ファイル(
- 互換性
date_helperとdate_helper I18nのテストスイートで、既存の挙動と同じ結果になることが確認済みです。- もしこの周辺のメソッドをモンキーパッチしている場合は、内部で
I18n.with_optionsが呼ばれなくなったことによる影響(例えばI18n::OptionMergerを前提にした独自コード)を確認する必要があります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59113
- 関連クラス:
ActionView::Helpers::DateHelperdistance_of_time_in_words(from_time, to_time = 0, include_seconds_or_options = {})time_ago_in_words(from_time, include_seconds_or_options = {})
- I18nの
with_options/OptionMerger挙動:I18n.with_options(locale: ..., scope: ...) { |locale_scope| locale_scope.t(:key, count: 1) }はI18n.t(:key, count: 1, locale: ..., scope: ...)と等価だが、毎回オプションマージ用オブジェクトとハッシュを生成するため、高頻度呼び出しではオーバーヘッドになる。
#59083 Add read-only attributes and use it instead of building a cache
マージ日: 2026/10/15 | 作成者: @tenderlove
- 概要 (1–2文で)
Active Model / Active Record の属性まわりに「読み取り専用の AttributeSet」を導入し、従来 Ractor 対応のために削除されていたビルダーキャッシュの代わりとして再利用できるようにした PRです。これにより属性インスタンス生成(instantiate)のオブジェクト割り当て数が減り、Active Record のインスタンス生成性能が向上しています。
- 変更内容の詳細
背景
- 以前、Ractor セーフティ確保のために AttributeSet のビルダーキャッシュが削除された結果、属性生成周りのオブジェクト生成が増え性能が悪化していました。
- #53344 で「変更不可能(読み取り専用)の AttributeSet を使いまわす」案があり、本 PR はその rebase + 仕上げ + 実際の利用への切り替えです。
- 「ビルダーキャッシュを再度持つ」のではなく、「不変な AttributeSet を共有」する設計により、スレッド/Ractor安全性と性能改善を両立しようとしています。
主な設計変更
1) AttributeSet に「読み取り専用セット」を導入
activemodel/lib/active_model/attribute_set.rb が大きく変更されています(+180/-1)。
ポイント:
- 以前は「AttributeSet を都度構築 + ビルダーキャッシュ」という形だったのを、「不変な読み取り専用セットを 1 度作って、そこから派生させる」方向にシフト。
- 読み取り専用セットは内部構造(Attribute オブジェクトなど)を凍結 or mutation 不可な前提で共有することで、複数のモデルインスタンス間で安全に使い回せるようになっています。
- 変更可能な操作(属性値の書き込み・dirty tracking 等)は、読み取り専用ベースから派生させた「書き込み用ビュー」で扱うイメージに近い構造と考えられます。
これに伴い:
attribute_set/builder.rbからキャッシュ周りのロジックが大幅削除 (-73/+10)- AttributeSet 側に、読み取り専用インスタンスの生成・複製・ラップに関するロジックが追加
といった責務の移動が行われています。
2) Attribute / UserProvidedDefault の read-only 対応
activemodel/lib/active_model/attribute.rb (+9)activemodel/lib/active_model/attribute/user_provided_default.rb (+4)
- Attribute オブジェクト自体も「読み取り専用として扱える」ようなフラグまたはモードを追加しています。
- ユーザー定義のデフォルト(
UserProvidedDefault)についても、読み取り専用セットと組み合わせて安全に共有できるような変更が入っています。 - これにより、「同じ定義の属性」を複数インスタンスで使いまわしつつ、各インスタンスごとに実際の値や dirty 情報を分離できるようにしていると考えられます。
3) AttributeRegistration / Attributes モジュールの更新
activemodel/lib/active_model/attribute_registration.rb (+18/-15)activerecord/lib/active_record/attributes.rb (+21/-19)
- モデルクラスに属性を登録するときに、「読み取り専用 AttributeSet を構築して保持する」ようにパスが変わっています。
- Active Record 側の
Attributesモジュールは、この読み取り専用セットを元にインスタンス固有の AttributeSet を生成するように書き換えられています。 - 結果として:
- モデルクラスごとに「定義済み属性の読み取り専用セット」が 1 つ
- レコードインスタンスごとに「値や dirty を持つインスタンス用セット」が必要な分だけ という二段構造になり、前者をキャッシュ相当として再利用する形になります。
4) Dirty tracking の対応
activemodel/lib/active_model/dirty.rb (+8/-4)
- dirty tracking(変更検知)が読み取り専用 AttributeSet を前提としても正しく動作するように調整されています。
- 読み取り専用の定義情報と、「どの属性が変更済みか / 以前の値は何か」といった状態情報を分離して扱うような形に整理されています。
- これにより「属性定義は共有」「変更状態はインスタンスごとに別管理」という構造が明確になります。
5) ActiveRecord::Base / Schema 周りの対応
activerecord/lib/active_record/core.rb (+3/-1)activerecord/lib/active_record/model_schema/schema_context.rb (+50/-26)
- スキーマ定義(カラム情報)から AttributeSet を組み立てる箇所が、読み取り専用セット生成を前提にした処理へ変更されています。
schema_contextが、スキーマ(テーブル定義)ごとの AttributeSet を効率よく組み立て・再利用する役割をより強く持つようにリファクタリングされています。
ベンチマーク結果
PR に記載のベンチマーク(6 カラムの sqlite モデルで 100 行)より:
- 単一レコード
Post.instantiate(row)の割り当てオブジェクト数:- #59097 前: 12
- main(#59097 済): 8
- この PR: 7
Post.all.to_a(100 行)の割り当てオブジェクト数:- main: 1182
- この PR: 1076
- インスタンス生成性能 (
instantiatei/s):- インタプリタ:
- main: 855k
- この PR: 939k (約 1.10x)
- YJIT:
- main: 2.95M
- この PR: 3.51M (約 1.19x)
- インタプリタ:
つまり、#59097 のメモ化改善にさらに上乗せで、AttributeSet の共有によりメモリ割り当てを削減し、instantiate のスループットを 10〜20% 程度改善しています。
ただし、v8.1.4 ブランチと比べると:
- v8.1.4: 134ms
- この PR 後: 170ms
とまだ約 40ms の差があり、完全には以前の性能に戻っていません(Ractor 対応のコストが残っている)。
- 影響範囲・注意点
影響範囲
- Active Model:
Attribute,AttributeSet,Dirty,AttributeRegistrationを直接触っているコード
- Active Record:
Attributesモジュール(attribute,attribute_before_type_castなど属性 API 全般)ActiveRecord::Base.instantiate,Relation#to_aなどインスタンス生成コードパス- スキーマ周り (
schema_context) にフックして独自拡張しているコード
フレームワーク利用者としては:
- 通常の範囲で Active Record を利用している分には、インターフェースの変更はなく、主に「内部実装の最適化」です。
- 属性が追加・変更されるタイミング(マイグレーション後の再読み込みなど)で、正しく新しい AttributeSet が構築されるかどうかはすでにテストが追加・更新されています。
activemodel/test/cases/attribute_set_test.rb(+61/-21)activerecord/test/cases/attributes_test.rb(+20)activerecord/test/cases/base_test.rb(+6)activerecord/test/cases/primary_keys_test.rb(+4/-2)activerecord/test/cases/relation_test.rb(+1)
注意点 / 互換性に関するポイント
ライブラリ・メタプログラミングをしている開発者が注意すべき点:
AttributeSet / Attribute オブジェクトのミューテーションに依存しているコード
- 読み取り専用セットや read-only 属性が導入されたため、「Active Record 内部が常にミュータブル」という前提で AttributeSet や Attribute を直接変更するコードは壊れる可能性があります。
- 特に、内部 API にアクセスして Attribute オブジェクトを差し替えたり、ハッシュを直接書き換えるようなコードはリスクが高いです。
AttributeSet のキャッシュに依存していたコード
- 以前の「ビルダーキャッシュ」と実装が変わっているため、
AttributeSet::Builderの内部構造やキャッシュの仕組みに依存したハックは動作しなくなっている可能性があります。
- 以前の「ビルダーキャッシュ」と実装が変わっているため、
Ractor / 並行実行環境での挙動
- この PR 自体が Ractor セーフティを意識しており、読み取り専用オブジェクトの共有を前提としています。
- 逆に言うと、Ractor 内で Active Record の内部オブジェクトを無理に共有していたコードは、新しい read-only モデルに沿うように見直す必要があります。
性能評価
- 本 PR は性能改善を目的としており、特に大量のレコードインスタンス化を行うアプリケーションでは、アップグレード後に instantiate 周りのベンチマークを走らせる価値があります。
- 以前のバージョンから大きく飛ばしてアップグレードする場合(例: 6.x → 8.x のようなケース)では、Ractor 対応による回帰 + 本 PR の改善をあわせてトータルで計測することをおすすめします。
- 参考情報 (あれば)
- 元 PR: #53344 — read-only AttributeSet の初期提案と議論の履歴
- 関連 PR: #59097 — Attribute builder のメモ化による instantiate パフォーマンス改善
- ベンチマークスクリプト(PR に記載)
自分のアプリケーション or Rails チェックアウトに対して以下のように実行可能:bashこれにより、ruby bench.rb /path/to/railsPost.instantiate(row)やPost.all.to_aの割り当て数と i/s を自前環境で比較できます。
この PR はインターフェースを変えずに Active Record の属性レイヤーを再設計しているため、「通常利用の互換性を保ちつつ、Ractor 対応後に落ちた性能を少しずつ取り戻すステップ」として位置付けられます。
#59111 Build the association cache on first access
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Active Record の各レコードが「必ず」確保していた関連キャッシュ用 Hash を、関連に初めてアクセスしたときに「遅延生成」するように変更した PRです。これにより、関連を触らない多数のレコードを扱うケースで、オブジェクト生成コストとメモリ使用量が削減されます。
- 変更内容の詳細
背景・従来の挙動
- Active Record の各インスタンスは、
init_internals内で常に「関連キャッシュ」用のHashを生成していました。- 例:
@association_cache = {}のようなものが、レコード生成時に必ず1つ確保される。
- 例:
- しかし、多くのレコードは「関連を1回も参照しないまま」生成されて捨てられます。
- 例: 単純な
Post.select(:id, :title).limit(100).to_aのような読み取り処理。
- 例: 単純な
この「使わないかもしれない Hash を毎回確保する」コストを削るのが主な目的です。
このPRでの新しい挙動
- レコード生成時 (
init_internals相当のタイミング) では 関連キャッシュを作らない。 - 関連に初めてアクセスされたタイミングで
association_instance_getが 必要に応じて関連キャッシュ Hash を生成 する。- もともと
association_instance_getは「キャッシュが無ければ作る」動作を許容する実装になっていたため、その設計を最大限活かす形。
- もともと
- 一方で、関連キャッシュを前提としているいくつかのメソッドに対しては、
- 「キャッシュがそもそも存在しない = 空のキャッシュ」という扱いをするように統一されています。
具体的に影響を受ける代表的なAPI:
association_cached?(name)- 関連キャッシュ Hash が存在しない場合でも、「キャッシュにそのキーがない」と同じ扱い (= false) になるように実装されている。
strict_loaded_associations- strict loading 用に「ロード済み関連」の一覧を返すが、キャッシュがないときは「空」と見なす。
reloadreload後に返ってくる「新しいレコードインスタンス」が持つ関連キャッシュをそのまま使う。- もともと
reloadは「新インスタンスの状態を採用する」設計のため、今回の遅延生成方針と矛盾しない。
パフォーマンス改善のイメージ
PRで示されている簡易ベンチマーク:
- 対象: 6カラム持つ sqlite モデル
- 結果:
| ベンチマーク | 変更前 | 変更後 |
|---|---|---|
Post.instantiate(row) | 6 | 5 |
Post.all.to_a (100行) | 1285 | 1185 |
単位はPR本文だけでは明示されていませんが、相対的な数字として「どちらもわずかだが確実な削減」が見て取れます。関連を全く使わない大量のレコードを扱う場合は、GC負荷の低減なども含めて効いてきます。
サンプル的な理解
以下のようなコードを想定します:
posts = Post.limit(1000).to_a
# ここでは posts の各レコードで関連に一切アクセスしない- 変更前: 1000件分の
@association_cache = {}が必ず確保される。 - 変更後: 関連を触らない限り 1000件どのインスタンスでも
@association_cache自体が生成されない。
一方で:
post = Post.first
post.comments.to_apost.commentsに初めてアクセスしたタイミングで、@association_cacheが生成され、その中に:commentsの Association オブジェクトが格納される、という流れになります。
- 影響範囲・注意点
影響範囲
- 影響を受けるのは「Active Record の関連キャッシュの内部表現・生成タイミング」のみです。
- Rails の公的APIレベルでは、以下の点が維持されます:
- 関連へのアクセス (
post.commentsなど) の挙動 reloadの挙動- strict loading 関連 (
strict_loading,strict_loaded_associationsなど) association_cached?marshal/dup周りの挙動
- 関連へのアクセス (
- PR本文によれば、既存の以下のテストスイートでカバーされている:
- association 関連テスト
- reload 関連テスト
- strict loading 関連テスト
- marshal, dup 関連テスト
注意点 / 互換性上の懸念
通常の使い方であれば挙動が変わることはほぼありませんが、以下のような「内部実装依存」のコードを書いている場合は注意が必要です。
- モデルインスタンスの
@association_cacheなど、内部インスタンス変数を直接参照している場合- 変更前: 生成済みで必ず
Hashが入っている前提 - 変更後: 存在しない (
nil/ 未定義) 場合がある
- 変更前: 生成済みで必ず
association_cache的なメソッドを直接叩いている(もしあれば)コードで、「nil を返しうる」前提の有無を確認する必要があります。
ただし、通常のアプリケーションコードではこういった内部変数を直接触るべきではないため、一般的な利用者にとっては後方互換性は保たれていると考えて問題ありません。
- 参考情報 (あれば)
- PRで影響を受けているファイル:
activerecord/lib/active_record/associations.rbactiverecord/lib/active_record/persistence.rb
- テスト・CHANGELOG 更新も含まれており、Association, reload, strict loading, marshal, dup といった関連機能全体での挙動確認が行われています。
- この種の「内部キャッシュの遅延生成」は、オブジェクト生成数が多いアプリケーションや高トラフィック環境で、GC回数の抑制やメモリのフットプリント削減に効いてくるため、パフォーマンス・チューニング上の地味ながら有効な最適化です。
#59077 Map key values in the key's own shape
マージ日: 2026/10/15 | 作成者: @matthewd
- 概要 (1-2文で)
Active Record のKey周りで、「キーが自分で値の形(単一値 or タプル)を決める」ための共通メソッドKey#map_valueが追加され、これまで各所に散らばっていたcomposite? ? values : values.firstのような条件分岐が集約されました。併せて、複合キーのキャスト処理での不要な配列・オブジェクトの割り当てが削減され、パフォーマンスとコードの一貫性が向上しています。
- 変更内容の詳細
2-1. Key#map_value の追加
activerecord/lib/active_record/key.rb に新メソッド Key#map_value が追加されています。PR 説明から推測される役割は次のとおりです。
Keyインスタンス(単一キー or 複合キー)が、「値をどういう形で返すべきか」 を自分で決める- 呼び出し側は
composite?を意識せずに、単一キー/複合キーを一律に扱える
イメージとしては以下のような振る舞いです(概念的な例):
class ActiveRecord::Key
# 単一キー用: ブロックで複数値を受け取ったとしても、最終的にはスカラにする
def map_value(*values)
if composite?
# 複合キーなら配列やタプルで返す
yield(values)
else
# 単一キーなら先頭要素を返す
yield(values).first
end
end
end実際の実装ではもう少し細かくなっているはずですが、要点は以下です。
- これまでのパターン:ruby
if key.composite? values else values.first end - 今後のパターン:ruby
key.map_value(values) { ...値の変換やキャスト... }
この結果、
- 呼び出し側での
composite? ? ... : ...が消え、条件分岐がKeyクラスに集約 - API 利用者(内部コードを含む)は「キーに値を渡すと、適切な形で返してくれる」という抽象化が使えるようになります
2-2. belongs_to 関連コードでの利用
belongs_to 関連クラスである:
activerecord/lib/active_record/associations/belongs_to_association.rbactiverecord/lib/active_record/associations/builder/belongs_to.rb
から、手書きの
key.composite? ? values : values.first
# あるいは似た条件分岐のようなコードが削除され、代わりに Key#map_value が使われるようになっています。
想定される利用シーン:
belongs_toの外部キー (foreign key) が- 単一カラムの場合 → 単一値
- 複数カラムから成る複合キーの場合 → タプル/配列
- この「キー形状による差」を、アソシエーション実装側では意識せず扱いたい
この PR によって、
- アソシエーションの取得・設定・キャスト周りで、複合キー対応コードが整理される
- 将来的な複合キー関連の変更が
Key周辺に閉じやすくなる
という利点が生まれます。
2-3. Key::Composite#cast の割り当て削減
PR 説明にある:
In passing, also avoid an allocation per column in
Key::Composite#cast.
から、Key::Composite#cast で各カラムごとに行っていた不要なオブジェクト/配列の生成が削られています。
典型的には以下のようなイメージです。
変更前のイメージ:
def cast(values)
columns.each_with_index.map do |column, i|
column.cast(values[i]) # 各カラムで新たなオブジェクト生成が起きる/配列ラップなど
end
end変更後のイメージ:
def cast(values)
# ループ中での中間配列/オブジェクトを極力作らない形に
# 例: 既存配列を使い回す、block-less map 的な実装など
endこれによって:
- 複合キーを多用している場合でも、キャストのたびに不要な GC 負荷をかけずに済む
- 複合キー数 × カラム数に比例していたオブジェクト割り当てが減る
ため、パフォーマンス上のメリットがあります。
2-4. テストの追加
activerecord/test/cases/key_test.rb に 27 行のテストが追加されています。
ここでは主に:
Key#map_valueの振る舞い- 単一キー時にスカラを返すこと
- 複合キー時に「キーの形」に合った値 (タプル/配列) を返すこと
Key::Compositeのキャスト結果が期待どおりであること
などが検証されていると考えられます。
これにより、呼び出し側が map_value に依存することに対するリグレッション防止が図られています。
- 影響範囲・注意点
- 影響範囲
- 内部的には
belongs_toアソシエーションとActiveRecord::Key/Key::Composite周辺 - 特に複合キー対応している(あるいはそれに依存した)コードで、値の受け渡し形態が
Key#map_valueを経由するようになる
- 内部的には
- 公開 API への直接の変更は説明文からは見当たらず、主に内部実装のリファクタリング + パフォーマンス改善です
- ただし、以下のような場合は注意が必要です:
ActiveRecord::Key/Key::Compositeを直接触っているメタプログラミング的なコードbelongs_to周りで、戻り値の形(スカラ or 配列)に依存した非公開 API 利用をしているコード
- 期待されるメリット
- コードの一貫性向上: 単一キーと複合キーの扱いの差を
Keyに閉じ込められる - メンテナンス性向上: 複合キー仕様変更時の影響範囲が局所化
- パフォーマンス改善: 複合キーキャスト時の無駄な割り当て削減
- コードの一貫性向上: 単一キーと複合キーの扱いの差を
通常のアプリケーションレベルでは、「挙動が変わる」というよりは「内部の実装が賢くなった」という性質の変更と考えてよいです。
- 参考情報 (あれば)
- この PR は #59063 に続く変更で、#59063 では「余計な配列割り当ての削減」が行われ、この PR では「条件分岐の集約とさらなる割り当て削減」が行われています。
- Rails で複合キーを(公式にはフルサポートしていない形で)扱っているプロジェクトやプラグインは、このあたりの内部 API に依存している場合があるため、アップグレード時には
Key周りを中心にテスト強化しておくと安全です。
#59074 Don't match through associations by direct keys
マージ日: 2026/10/15 | 作成者: @matthewd
- 概要 (1-2文で)
Rails のhas_many :through/has_one :throughなど「through association」に対して、関連オブジェクトを紐付ける際に「中間(source)の外部キー」を使って直接マッチングしようとしていた処理を無効化し、常に通常のロード経路を使うようにした修正です。これにより、自己参照モデルなどで、たまたま同じカラム名があるせいで誤った関連レコードが紐付くバグを防ぎます。
- 変更内容の詳細
問題の背景
through な関連 (例: has_many :comments, through: :posts) の場合、
- オーナー(owner):
Userなど関連元のモデル - 中間(source):
Post - ターゲット(target):
Comment
という 3者の関係になります。このとき、オーナーとターゲットは直接は結びついておらず、中間モデルを経由して論理的に関連付いている状態です。
ところが、Active Record 内部で次の 2 つの処理が、中間(source)の外部キーを使って owner と target を「直接」マッチングしようとしていました。
- プリロード時に
available_recordsを再利用する処理(#42654) - クエリで読み込んだレコードに inverse 関連を設定する処理(#40042)
「中間の外部キー」と「owner 側の同名カラム」がたまたま存在し、値も一致してしまうようなケース(典型例: 自己参照を含むモデル構造)では、
「本来関連していないレコード同士が、関連していると誤判定」され、無関係なレコードが紐付いてしまうというバグが発生します。
今回の方針
PR の説明にある通り:
There's no direct comparison that would be correct, so through associations now opt out of both, and just load normally.
「through な関連では、owner と target を直接キー比較で結びつける正しい方法は存在しないので、そのような最適化を丸ごとやめる」という方針です。
つまり:
available_recordsの再利用によるマッチングから through association を除外- inverse 設定時に行っていた「キー比較ベースのマッチング」から through association を除外
し、常に通常どおりのロード/関連付けロジックで処理するようになりました。
具体的な変更点(コードレベルのイメージ)
実際の差分は概略ですが、以下のような形になります:
1. Preloader の through_association 用フラグ追加
activerecord/lib/active_record/associations/preloader/through_association.rb に、
「この association は available_records を使った高速マッチングを行わない」と宣言するロジックが入ります。
擬似コードイメージ:
module ActiveRecord
module Associations
class Preloader
class ThroughAssociation < SingularAssociation
def use_available_records?
false # through では available_records を使わない
end
end
end
end
endこれにより、preload 時に「既に読み込まれているレコードをキー比較で流用する」ようなショートカットが、through association に対しては行われなくなります。
2. ThroughAssociation 本体に inverse 関連の特例
activerecord/lib/active_record/associations/through_association.rb では、
inverse 関連を設定するときに、owner/target を source の外部キーで直接マッチングしないようにするための制御が追加されています。
擬似コードイメージ:
class ThroughAssociation < Association
def can_use_inverse_of_for?(record)
false # through では inverse の自動マッチングに参加しない
end
endこうすることで、#40042 由来の「クエリで読み込んだレコードに対して自動で inverse を埋める」仕組みのうち、キー比較ベースのマッチングを through ではスキップします。
3. テスト追加
activerecord/test/cases/associations/inverse_associations_test.rbactiverecord/test/cases/associations_test.rb
に、自己参照や through を絡めたケースで「誤ったマッチングが発生しない」ことを検証するテストが追加されています。
おそらく以下のようなシナリオのテストが含まれていると考えられます:
class Employee < ApplicationRecord
has_many :subordinates, class_name: "Employee", foreign_key: "manager_id"
has_many :tasks, through: :subordinates
# manager_id と tasks 側の何かの外部キーが偶然同名…といった状況
end
# 以前は、manager_id と tasks 側の外部キーが一致したことで、
# 関連していない task が Employee#tasks に紐づいてしまうケースがあった。
# 今回の修正で、それが起きないことをテストしている。- 影響範囲・注意点
影響範囲
- 対象:
has_many :through,has_one :throughなど「through association」全般 - 関連する内部機構:
- association preloader (
includes,preload,eager_loadなど) - inverse association の自動設定 (
inverse_ofを明示しない場合の推測を含む)
- association preloader (
実際の影響
正の影響:
- 自己参照モデルや、外部キー名が衝突しがちな複雑な関連構造で、誤ったレコードが関連に紐付くバグが解消されます。
- バグとしては「たまにおかしなレコードが紐付いている」程度にしか見えないため、潜在的なデータ不整合を減らせる点は重要です。
性能面の影響:
- through association については、
- preload 時の
available_records再利用 - inverse 設定時のキー比較マッチング
という 2 つの小さな最適化を捨てているため、ごく一部のケースでわずかにオーバーヘッドが増える可能性はあります。
- preload 時の
- とはいえ、through で正しくないマッチングを避けることの方が優先されており、
「体感できるほどの性能悪化」はほとんどのアプリでは起こらないと考えられます。
- through association については、
既存アプリ側の注意点
- もし独自に ActiveRecord の内部 API (preloader / association / inverse 処理) を monkey patch している場合、
ThroughAssociationが「available_records を使わない」- inverse の自動設定に関与しない
という前提に変わっていることを意識する必要があります。
- 通常のアプリケーションコードで
has_many :through/has_one :throughを使っているだけであれば、コード変更は不要で、バグ修正のみの影響と考えてよいです。
- 参考情報 (あれば)
- この PR の説明で触れられている関連 PR:
- #42654: preloader の
available_records再利用周りの改善 - #40042: inverse association の自動設定ロジックの改善
- #42654: preloader の
- 関連して発生しうるパターン:
- 自己参照関連 (
belongs_to :parent, class_name: "Model",has_many :children, ...) と through を組み合わせたモデル - モデル間で偶然同名の外部キーカラムが存在するようなスキーマ設計
- 自己参照関連 (
- 今回の修正は、「今後予定している内部リファクタリング中に見つかった既存バグ」の先行修正として入っているため、
今後さらに association / preloader 周りが整理される可能性がありますが、この挙動(through が direct match に参加しない)は維持されることが期待されます。
#59110 Build String#truncate's result in one copy
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
String#truncateの結果文字列を「1回のコピーで」生成するようにして、余計な文字列生成を減らすパフォーマンス改善 PR です。動作仕様(戻り値の内容・エンコーディング・freeze 状態)は変えずに、内部実装のみを最適化しています。
- 変更内容の詳細
何をしているか
従来の String#truncate は、おおよそ次のような流れでした:
- 元の文字列から先頭 N 文字を
sliceする - そのスライス結果と省略記号(デフォルト
"...")を、文字列補間などで新しい文字列にまとめる
つまり、スライス結果 (1つ目の新しい文字列) と 最終結果 (2つ目の新しい文字列) の2段階で文字列を生成していました。
今回の変更では:
- すでに
sliceした時点で「新しい・変更可能な文字列」が手に入っているので、 - そのスライス結果に対して
<<で省略記号を後ろに足す ことで、追加の文字列オブジェクトを作らずに 完結させています。
概念的なイメージ:
# 以前のイメージ
truncated = self.slice(0, length) # 新しい String
result = "#{truncated}#{omission}" # さらに新しい String を作る
# 今回の変更イメージ
result = self.slice(0, length) # 新しい String
result << omission # その String に追記して完成これにより、最終的に返されるオブジェクトは:
- 非 frozen の文字列であること
- レシーバ (
self) と同じエンコーディングを持つこと
といった既存仕様を保ったまま、オブジェクト生成数だけを削減しています。元々のテストがこれらの性質を検証しているため、挙動の互換性もテストで担保されています。
パフォーマンス指標
108文字の文字列で計測した例:
| メソッド呼び出し | 変更前 (オブジェクト数) | 変更後 (オブジェクト数) |
|---|---|---|
truncate(30) | 3 | 2 |
truncate(30, separator: " ") | 4 | 3 |
Rails のリストビューなどで「1行ごとのタイトルを truncate する」ようなケースだと、行数が多いほどこの差が効いてきます。
- 影響範囲・注意点
- 対象メソッド
ActiveSupportのString#truncate(activesupport/lib/active_support/core_ext/string/filters.rb)のみ。
- 仕様変更の有無
- 返り値の内容、エンコーディング、凍結状態など外から見える挙動は変わりません。あくまで内部の生成コスト削減です。
- ミューテーションに関する注意点
truncateが返す文字列は以前から「新しく生成された変更可能な文字列」でしたが、今回もその前提は維持されています。- 戻り値に対して
<<などで追記しても元のレシーバは変更されません(sliceによって新規オブジェクトが返ってくるため)。
この点も以前と同じです。
- パフォーマンス影響
- レコード数が多い index ページやログ出力など、
truncateが大量に呼ばれる場面で GC 負荷とオブジェクト数がわずかに減ることが期待できます。 - 単発の呼び出しでは体感しづらいですが、ホットパスでの細かい最適化としては妥当な変更です。
- レコード数が多い index ページやログ出力など、
- 参考情報 (あれば)
- 対象ファイル:
activesupport/lib/active_support/core_ext/string/filters.rb(+3/-1)
- PR: [rails/rails#59110 — Build String#truncate's result in one copy]
- 関連する一般的な Ruby 文字列最適化の考え方:
- 不要な中間文字列を減らす(特に補間
"#{...}"よりも、既存文字列への<<を利用) - ホットパスでは「コピー回数」を減らすことが GC・メモリ効率向上につながる
- 不要な中間文字列を減らす(特に補間
#59079 Respect custom primary keys in grouped calculations and through builds
マージ日: 2026/10/15 | 作成者: @matthewd
- 概要 (1-2文で)
belongs_to/has_many :throughにprimary_key:を指定している場合に、グループ集計 (group(...).count) および through 経由での build 時に、そのカスタム主キーを正しく参照するように修正した PR です。これにより、従来nilキーになってしまった集計結果や、誤った外部キーがセットされる問題が解消されます。
- 変更内容の詳細
2-1. grouped calculations でのカスタム primary key 対応
問題点:group(:belongs_to_association).count のように関連を指定して group 集計を行う際、関連側に primary_key: が設定されていても、それを無視して対象モデル自身の primary_key を見に行っていたため、結果がすべて nil キーになるなど、正しく結果をマッピングできていませんでした。
イメージ:
class User < ApplicationRecord
self.primary_key = :uuid
has_many :posts
end
class Post < ApplicationRecord
belongs_to :user, primary_key: :uuid, foreign_key: :user_uuid
end
# 本来期待したいもの
Post.group(:user).count
# => { user1 => 3, user2 => 5 ... } のように user の primary_key(:uuid) に紐づいた結果この PR での対応:
activerecord/lib/active_record/relation/calculations.rbが修正され、グループ集計時に関連の情報から「どのキーでグルーピング/マッピングすべきか」を判断する際に、association のprimary_keyを尊重するようになりました。- 具体的には、「対象のテーブルの primary key」ではなく「関連 (
belongs_toなど) が指定しているprimary_key:」を使って集計結果のキー付けを行います。
テスト (activerecord/test/cases/calculations_test.rb) も追加されており、
- カスタム primary key を持つ関連に対して
group(:association).countを実行したときに、 - 結果が
nilではなく正しい関連レコードに対応したキーになること
が検証されています。
2-2. has_many through: の build でのカスタム primary key 対応
問題点:has_many :through かつ through: :singular な関連で、build (例: author.books.build) を行ったとき、
- 経由元の through レコードの
idをそのまま新規オブジェクトの外部キーに書き込んでしまい、 - source association に指定されている
primary_key:が無視されていました。
イメージ:
class Person < ApplicationRecord
self.primary_key = :uuid
has_one :profile, primary_key: :uuid, foreign_key: :person_uuid
end
class Profile < ApplicationRecord
belongs_to :person, primary_key: :uuid, foreign_key: :person_uuid
has_many :articles
has_many :comments, through: :articles, source: :commenter,
primary_key: :uuid # こういった指定があるイメージ
end
# through: :profile の has_many などで build したとき
person.comments.build
# => through の profile.id をそのまま外部キーにコピーしてしまっていたこの PR での対応:
activerecord/lib/active_record/associations/through_association.rbが修正され、has_many through:の build 時に、source association が持つprimary_key:を使って外部キーをセットするようになりました。- つまり、「through レコードの
id」ではなく、「source 側で指定されている primary key の値」を参照します。
対応に併せて、activerecord/test/cases/associations/has_many_through_associations_test.rb にテストが追加され、
through: :singularなhas_manyにおいて- source association がカスタム primary key を持つケースでも
- build された新レコードの外部キーが期待通りの値になること
が確認されています。
2-3. ActiveRecord::Key の拡張
activerecord/lib/active_record/key.rbに 14 行の追加があり、新たなユーティリティ・ロジックが導入されています。- ここで実装されているのは、「関連の primary key やテーブル・カラム情報に基づいて、値の取得/マッピングを行うための抽象化」と考えられます。
activerecord/test/cases/key_test.rbの追加により、この Key クラスの新機能(たとえば「あるレコード群から association の primary key に基づいて値を引く」など)がテストされています。
これにより、
- grouped calculations や
- through association の build のような処理が
共通の仕組みで「どの primary key を使うべきか」を判断できるようになったと推測されます。
- 影響範囲・注意点
影響を受けるのは主に次のパターンです:
belongs_to/has_manyなどでprimary_key:オプションを使っている関連- それらの関連を
group(:association)としてcount/sumなどの集計に利用しているコード has_many :through(特にthrough:がhas_one/belongs_toな「単数系」)で、build/createを呼び出しているコード
修正の性質としては「バグ修正」であり、本来そう振る舞うべきだった仕様に近づいているため、多くの場合は期待通りの挙動に近づきます。
ただし、以下の点には注意が必要です:
- これまで「誤った挙動」を前提にワークアラウンドしていたコードがある場合、そのワークアラウンドが不要・あるいは誤動作になる可能性があります。
- 例:
group(:association).countが常に{ nil => ... }を返すのを前提にしていたような処理。
- 例:
- テストコードで「nil キーになる」ことを暗黙に前提にしていた場合、期待値を修正する必要があります。
has_many through:+ カスタム primary key で、外部キーを後から手動で上書きしていたようなコードは再確認した方が安全です(今回の修正により自動で正しい値が入るため)。
- これまで「誤った挙動」を前提にワークアラウンドしていたコードがある場合、そのワークアラウンドが不要・あるいは誤動作になる可能性があります。
Rails のバージョンアップ時には、カスタム primary key を多用しているアプリケーションほど、このあたりの関連操作・集計ロジックに対して回帰テストを実行することを推奨します。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59079
- 関連する Rails ガイド:
- Active Record Association —
primary_key/foreign_keyオプション - Active Record Querying — Grouped Calculations (
group,count,sumなど)
- Active Record Association —
- カスタム primary key を使うモデルや関連を多用している場合、以下のようなケースで実際に動作確認しておくと安心です:
Model.group(:belongs_to_association).counthas_many :throughな関連に対するbuild/createの挙動(外部キーの値が期待通りか)
#59076 Fix type matching in inverse polymorphic queries
マージ日: 2026/10/15 | 作成者: @matthewd
- 概要 (1-2文で)
where(as_association: relation)で生成される「ポリモーフィック関連の型条件」に関する2つのバグが修正されています。特に、(1) 明示的な type 条件があっても無視されるケースと、(2) 抽象クラスなどでpolymorphic_nameが食い違うケースで誤った type が使われる問題が解消されました。
- 変更内容の詳細
修正点1: 明示的 type 条件が属性エイリアス経由だと認識されない問題
背景:where(as_association: relation) は、polymorphic な関連(例: belongs_to :owner, polymorphic: true)を辿ってクエリを組み立てる際、自動的に「type カラム = 関連元のクラス名」という条件を付与します。
本来の挙動としては:
- 呼び出し側が 明示的に type 条件を指定した場合
→ フレームワークが勝手に付けるデフォルトの type 条件は 上書きされるべき。
ところが以前の実装では:
where(owner_type: "User")のように カラム名を直接指定していれば認識できたが、where(owner: User)のように 属性エイリアス (attribute alias) を通した指定など、内部的に type カラムにマッピングされる条件が
「既に type 条件がある」として認識されず、Rails がさらにデフォルトの type 条件を付けてしまうことがありました。
この PR では:
PredicateBuilder/AssociationQueryValue周りが修正され、
「すでに type 条件が指定されているか」の判定に 属性エイリアスも考慮される ようになりました。- これにより、
where(as_association: relation)利用時に、
ユーザーが指定した type 条件が正しく優先され、
余計な AND 条件(例:owner_type = 'User' AND owner_type = 'Admin'のような矛盾)が付かなくなります。
※具体的なコード断片は PR 本体に依存しますが、テスト (where_test.rb) 側では
- エイリアス経由で type を指定したケース
- その上で
as_associationを使ったケース
が追加されているはずです。
修正点2: polymorphic_name の取り方の不整合
背景:
ポリモーフィック関連では、関連づけられたクラスを表すために polymorphic_name を用います。
Rails では通常、SomeModel.polymorphic_name はクラス名 ("SomeModel") ですが、以下のようなカスタマイズが可能です:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
class BaseItem < ApplicationRecord
self.abstract_class = true
def self.polymorphic_name
"base_item"
end
end
class Book < BaseItem
# 必要に応じてさらに override も可能
# def self.polymorphic_name
# "book"
# end
end以前の実装の問題点:
where(as_association: relation)が type 条件を組み立てる際に、
関連を宣言した側 (declaring class) のpolymorphic_nameを使っていました。- 一方で、レコードのロードや JOIN を組み立てる処理では、実際の所有者 (owner) の
polymorphic_nameを使っていました。 - 抽象クラス (
abstract_class) にbelongs_to :attachable, polymorphic: trueを定義し、
それを継承したクラスがpolymorphic_nameをオーバーライドしているようなケースだと:- ロード/JOIN 時: owner 側 (
SubClass.polymorphic_name) を使う as_associationの type 条件: declaring class (AbstractClass.polymorphic_name) を使う
→ 両者が食い違い、不正な type 値でクエリされてしまい、レコードが取得できない / 間違ったレコードになる可能性がありました。
- ロード/JOIN 時: owner 側 (
この PR では:
TableMetadataやAssociationQueryValueを修正し、where(as_association: relation)が使う type 条件生成時にも owner のpolymorphic_nameを使う ように統一しています。- これにより、
- 抽象クラス上にポリモーフィック関連を宣言している場合
- サブクラスが
polymorphic_nameをオーバーライドしている場合
でも、ロード・JOIN・as_association経由の条件が全て同じ type 値を使うようになり、一貫した挙動になります。
- 影響範囲・注意点
対象となる機能:
ActiveRecord::Relation#where(as_association: relation)を使った inverse polymorphic query(関連を元に逆方向の検索をするようなケース)- ポリモーフィック
belongs_toを使っているアプリケーション全般だが、特に:- 抽象クラスに関連を定義している
polymorphic_nameを独自にオーバーライドしている- type カラムに対して属性エイリアスを使っている(例:
belongs_to :owner, polymorphic: trueでowner_typeを別名扱いしている)
実務で意識すべき点:
- これまで「なぜかポリモーフィック関連を通した where が期待通りにヒットしない」「as_association を使うと結果が変になる」といった症状があった場合、この修正で直る可能性が高いです。
- 逆に、バグに依存した挙動(例えば:
- 実際は
polymorphic_nameを変えているのに、宣言元抽象クラスの名前で保存・検索されていた - 明示的 type 条件を付けたつもりが、さらに Rails 側デフォルトが上乗せされていた )を前提にしたコードがあると、挙動が変わる可能性があります。
- 実際は
- テストでは
where(as_association: ...)を使うケースで:- 属性エイリアスを通した type 指定
- abstract class + subclass override な
polymorphic_name
などがカバーされているので、自前のアプリでも同様のパターンがあればテスト追加を検討すると安心です。
- 参考情報 (あれば)
- 関連する内部クラス/モジュール:
ActiveRecord::Relation::PredicateBuilderActiveRecord::Relation::PredicateBuilder::AssociationQueryValueActiveRecord::TableMetadata
- 概念的なキーワード:
- inverse polymorphic association / inverse query
polymorphic_nameのオーバーライド- attribute alias と type カラム (
*_type) のマッピング
今回の修正は、今後予定されている internal refactor の過程で見つかった既存バグの是正であり、Rails のポリモーフィック関連を使った高度なクエリでより一貫した・直感的な挙動が得られるようになります。
#59073 Preserve array-valued composite keys in find_by
マージ日: 2026/10/15 | 作成者: @matthewd
- 概要 (1-2文で)
find_by(association: record)が複合主キー+PostgreSQLの配列カラムを含む場合に、バインド値が誤ってフラット化されるバグを修正し、既に正しく動いているwhereと同じ処理経路にフォールバックさせる変更です。これにより、配列型を含む複合キーを持つ関連オブジェクトを使った検索でも、配列が正しく1カラム分の値として扱われます。
- 変更内容の詳細
背景
- #50412 で、
find_by(association: record)のように「関連オブジェクトそのもの」を渡した場合に、その関連の複合主キーからタプル(複数カラムの検索条件)を組み立てる機能が入った。 - しかし、その実装では「値の妥当性チェック(unsupported-value check)」がスキップされており、
- 複合キーの一部に「PostgreSQLの配列カラム」が含まれるケースで、
- その配列が1つの値として扱われず、「後続カラムのバインド変数まで巻き込んでフラット化」されてしまう問題があった。
- 通常の
whereではこの問題は既に考慮されており、配列型を含む値も正しく扱えている。
本PRでの対応
要約すると:
「複合キーを組み立てる特別扱いの
find_byパスで、配列型コンポーネントをうまく扱えていないなら、そのケースでは無理に特別処理をせず、すでに安全なwhere相当のパスにフォールバックする」
という方針の修正です。
コード変更点
activerecord/lib/active_record/core.rbfind_by(もしくはその内部で呼ばれるクエリ生成ロジック)に1行追加され、- 複合キーからタプルを組み立てる際に「値がサポート外かどうか」をチェックし、問題がありそうな場合は特別処理をやめて標準の
whereに任せるようになっています。
- 複合キーからタプルを組み立てる際に「値がサポート外かどうか」をチェックし、問題がありそうな場合は特別処理をやめて標準の
- 結果として、配列カラムなど「そのまま1値として扱うべきもの」がタプル構築の中で誤って分解されるのを防いでいます。
activerecord/test/cases/adapters/postgresql/array_test.rbPostgreSQL アダプタ向けの配列テストに 11 行追加。
想定される形としては、次のような構造のテストです(擬似コード):
rubyclass PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase def test_find_by_association_with_composite_key_and_array_column # 例: Tagging モデルが (post_id, tags) といった複合キーを持ち、 # tags が Postgres の text[] カラムになっているイメージ post = posts(:some_post) tags = ["ruby", "rails"] record = Tagging.create!(post: post, tags: tags) # ここで `find_by(post: post)` のような形(実際は関連と複合キーに応じた記述)で # 配列が正しく1カラム分の値として扱われることを確認 found = Tagging.find_by(post: post, tags: tags) assert_equal record, found end end実際にはより具体的なモデル・スキーマに基づいていますが、
「関連オブジェクト+複合キー+配列カラム」が絡むパターンをカバーするテストが追加されています。
- 影響範囲・注意点
影響範囲:
- 次の条件が揃うコードで挙動が変わります。
- Active Record モデルに**複合主キー(または複数カラムからなる関連キー)**がある。
- その複合キーの一部が PostgreSQL の配列型カラム である。
find_by(association: record)のように、関連オブジェクトを直接渡して検索している。
- これまでは配列がフラットに展開されて誤ったSQL / バインドになり、
- 想定通りにレコードが見つからない、
- 稀にSQLエラーや不正な条件になる
などのバグが起こり得ました。
- 本PRにより、そのようなケースは安全な
where相当のロジックにフォールバックし、- 配列カラムは1カラム分の値として保持され、
whereと同等の挙動で検索されます。
- 次の条件が揃うコードで挙動が変わります。
注意点:
- 挙動の変更点は「壊れていたケースが正しくなる」方向であり、
既存の正常に動いていたケースへの副作用は最小限になるよう配慮されています。 - もしPostgreSQL配列カラムを複合キーや関連の一部として利用しており、
find_by(association: record)を使っている場合は、- テストを追加・実行して、期待通りにレコードが取得できるかを確認する価値があります。
- 逆に、単なるスカラー値(integer, string など)だけの複合キーや
find_byの利用には、
今回の変更の影響はほぼありません。
- 挙動の変更点は「壊れていたケースが正しくなる」方向であり、
- 参考情報 (あれば)
- 対応元となったPR: #50412
find_by(association: record)で複合主キーを扱う機能を導入したPR。
- 関連ファイル:
activerecord/lib/active_record/core.rb
->find_by/find_by!の実装、およびRelation生成ロジック。activerecord/test/cases/adapters/postgresql/array_test.rb
-> PostgreSQL配列サポートに関する回帰テスト。
- 本PRは、今後予定されている内部リファクタリング作業中に見つかったバグ修正であり、
外部APIのインターフェースを変更するものではなく、挙動の一貫性向上が主目的です。
#59071 Define an internal ActiveSupport::WeakKeyMap
マージ日: 2026/10/15 | 作成者: @matthewd
- 概要 (1-2文で)
ObjectSpace::WeakKeyMapをラップする内部用のActiveSupport::WeakKeyMapクラスが追加され、これまでLeaseRegistry(connection_pool 内部)側に散在していたランタイム依存の同期処理が ActiveSupport に一本化されました。これにより、WeakMap 利用箇所で同じ同期ロジックを再利用しやすくなり、今後の利用拡大に備えた基盤整備が行われています。
- 変更内容の詳細
2-1. ActiveSupport::WeakKeyMap の新設
activesupport/lib/active_support/weak_key_map.rb が新規追加され、ObjectSpace::WeakKeyMap をラップする内部的なヘルパークラスが定義されています。
狙いは以下の通りです:
- Ruby のランタイムごとの実装差(例: CRuby と JRuby での
ObjectSpace::WeakKeyMapの有無や挙動差)を ActiveSupport 側で吸収する - スレッドセーフに扱うための同期処理を一箇所で管理する
- 既存/将来の呼び出し元が、自前で同期や条件分岐を書く必要をなくす
Rails 7.2 で追加された ActiveSupport::WeakSet (#59039) と同じ設計思想で、「Weak 構造を ActiveSupport で抽象化する」流れの一環です。
典型的な利用イメージ(概念的な例):
require "active_support/weak_key_map"
weak_map = ActiveSupport::WeakKeyMap.new
obj = Object.new
weak_map[obj] = "value"
# obj が GC で回収されると、エントリも自動的に消える内部では:
- 利用可能なら
ObjectSpace::WeakKeyMapを使用 - 必要に応じて Mutex 等で同期
- 一部ランタイムでは代替実装にフォールバック
といった処理が隠蔽されていると考えられます(PR 文脈上、少なくとも「ランタイム依存の同期」を一箇所に寄せている)。
activesupport/lib/active_support.rb に 1 行追加されているのは、この新クラスの autoload / require を紐づけるためです。
2-2. ConnectionPool / LeaseRegistry からのロジック移動
activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb にある LeaseRegistry 周りの実装から、ObjectSpace::WeakKeyMap に関する自前同期ロジックが削除され、代わりに ActiveSupport::WeakKeyMap が使われるようになりました(+6/-27)。
元々 LeaseRegistry は:
- 「接続プール内で、あるオブジェクト(スレッドやファイバーなど)とコネクションのリース状態を紐付ける」ための仕組み
- GC で消えるべきキー(例: スレッド)が消えたら自動的にマップからもエントリが消えるよう、
ObjectSpace::WeakKeyMapを使用 - その際、ランタイムごと・スレッドセーフティの問題を自前でケアしていた
という状態でしたが、今回の PR により:
LeaseRegistry側のObjectSpace::WeakKeyMap直接使用 + 同期処理は削減- 代わりに
ActiveSupport::WeakKeyMapに置き換え
というリファクタリングが行われています。
この結果、LeaseRegistry のコード行数は減り(-27 行)、WeakMap 特有の処理は ActiveSupport 側に集約されました。
作者コメントにある通り:
As with
ActiveSupport::WeakSet(#59039), this gives us a single home for the runtime-specific synchronization aroundObjectSpace::WeakKeyMap, instead ofLeaseRegistrycarrying its own.
(I have a new call-site coming, this isn't just moving for the sake of it.)
とあるため:
- これは単なるコード整理ではなく、
ActiveSupport::WeakKeyMapを別の場所からも使う予定がある前提の基盤整備 - 同じような WeakMap 利用箇所が増える見込みで、それら全てで統一された同期・互換性処理を適用できるようにしている
と理解できます。
- 影響範囲・注意点
3-1. 公開 API ではなく「内部 API」と見なすべき
PR タイトル・説明や ActiveSupport::WeakSet と同様の扱いから、この ActiveSupport::WeakKeyMap は主に Rails 内部実装向けのクラスと考えられます。
- 公開ドキュメント化されていない場合、アプリケーションコードから直接依存するのは将来の互換性リスクがあります
- ただ、「内部実装に
WeakKeyMapが登場することがある」という前提で Rails の挙動を読む際には、クラス名を知っておくと追いかけやすくなります
3-2. ActiveRecord connection_pool の挙動は基本的に変わらない想定
LeaseRegistry の実装が ActiveSupport::WeakKeyMap に置き換わっただけなので、外部から見た:
- コネクションプールのインタフェース
- 接続の貸し出し / 返却の挙動
- GC によるスレッドやファイバーの終了時に、対応するリー ス情報が掃除される
といった仕様は変わらないはずです。
ただし、内部の同期ロジックが変わるため:
- 一部ランタイム(JRuby, TruffleRuby など)や特殊なマルチスレッド環境での挙動が 改善 される可能性
- 極端な並行負荷下での競合条件の有無が
ActiveSupport::WeakKeyMap実装に依存するようになる
といった影響はあり得ます。
Rails 本体のテストが通っている前提ですが、自前で connection_pool 周りにパッチや monkey patch を当てている場合は、今回の変更と干渉しないか確認が必要です。
3-3. Weak 構造を使った拡張実装のヒント
今後 PR 作者が言及している「new call-site」(WeakKeyMap を新たに使う箇所)が増えることで:
- キャッシュやレジストリで、GC に任せてキーを消したいパターン
- Thread / Fiber ごとのコンテキスト管理(context local storage)の実装
- Middleware や instrumentation での一時的な関連付け
などに ActiveSupport::WeakKeyMap が多用される可能性があります。
Weak な構造を利用する設計が増えることで:
- メモリリークしにくいレジストリ
- 終了済みオブジェクトの取り残しが発生しにくい設計
を Rails 内部で統一的に行いやすくなる点は、開発者・運用者双方にメリットがあります。
- 参考情報 (あれば)
- この PR:
- #59071: Define an internal ActiveSupport::WeakKeyMap
- 関連 PR:
- #59039: ActiveSupport::WeakSet の追加(同様に Weak 構造を ActiveSupport で抽象化する取り組み)
- 関連クラス・機能:
ObjectSpace::WeakKeyMap(Ruby 標準の WeakMap 相当機能)ActiveRecord::ConnectionAdapters::ConnectionPoolおよび内部のLeaseRegistryActiveSupport::WeakSet(WeakKeyMap と同じ思想のラッパー)
#59109 Skip the %Z substitution in TimeWithZone#strftime when the format has none
マージ日: 2026/10/15 | 作成者: @amatsuda
概要 (1-2文で)
ActiveSupport::TimeWithZone#strftimeが、フォーマット文字列に%Zが含まれない場合には置換処理をスキップするようになり、挙動は変えずにメモリアロケーションと実行時間を削減するパフォーマンス改善です。変更内容の詳細(サンプルコード含む)
従来の実装
TimeWithZone#strftimeは常に内部でgsubを使い、フォーマット文字列中の%Zをタイムゾーン略称(例: "UTC", "PST" など)に置き換える前処理を行っていました。- このため、フォーマット文字列に
%Zが含まれていなくても、毎回gsubによる新しい文字列の生成(アロケーション)が発生していました。
変更点
フォーマット文字列に
"%Z"が含まれているかどうかを事前にチェックし、含まれていない場合はgsubを走らせないようにしました。擬似コードで表すと、以下のようなイメージです(実際のコードは1行の変更のみ):
ruby# 変更前(イメージ) def strftime(format) format = format.gsub(/%Z/, zone_abbr) # 常に gsub 実行 time.strftime(format) end # 変更後(イメージ) def strftime(format) if format.include?("%Z") format = format.gsub(/%Z/, zone_abbr) end time.strftime(format) end"%%Z"のようなエスケープされたシーケンスを含むケースでの挙動(既存テストでカバー済み)は変わらないことが明示されています。つまり、仕様面の振る舞いは完全に従来どおりです。
パフォーマンス計測結果(
TimeWithZonein UTC)- オブジェクトアロケーション数:
strftime("%d %b %H:%M"): 7 → 4to_fs(:short): 6 → 2strftime("%H:%M %Z"): 11 → 11(%Z を含むケースは変化なし)
- 実行時間 (
300,000回呼び出し):- 0.184s → 0.147s
%Zを使わないフォーマットが一般的であるため、実運用では多くのビュー / I18n フォーマットで恩恵があります。- オブジェクトアロケーション数:
- 影響範囲・注意点
影響範囲
ActiveSupport::TimeWithZone#strftimeを経由する日時フォーマット全般:- I18n の
config/locales/*.ymlに定義された日付・時刻フォーマット to_fs/to_formatted_s経由のフォーマット- アプリケーションコード内で
Time.zone.now.strftime(...)などを呼び出している箇所
- I18n の
%Zを含まないフォーマットでメモリ使用量・速度が改善されます。%Zを含むフォーマットや、"%%Z"のようなエスケープケースでは挙動・性能ともに従来と変わりません。
注意点
- 仕様の変更ではなく最適化のみであり、フォーマット結果(出力文字列)は全ケースで従来と同一になることが意図されています。
%Zの判定はシンプルな"format.include?("%Z")"タイプのチェックのため、%Zを含むか否かに関するユーザーの期待と食い違うケースは基本的にありません。- 既存テスト(特に
"%%Z"を含むパターン)で互換性が確認されているため、通常のアプリケーションコード側で特別な対応は不要です。
- 参考情報 (あれば)
- 対象メソッド:
activesupport/lib/active_support/time_with_zone.rb内のTimeWithZone#strftime - 変更行数: +1 / -1(ロジック上は条件分岐を加えただけの非常に小さい変更)
- パフォーマンスに関心がある場合は、
Time.zone.now.to_fs(:short)やI18n.l(Time.zone.now)などを大量に実行する箇所でプロファイルを取ると、本変更の効果を観測しやすいです。
#59108 Build Enumerable#index_with without a pair array per element
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Enumerable#index_withの内部実装を、to_h経由ではなくシンプルなeachループでハッシュを構築する形に変更し、挙動を変えずにオブジェクト割り当て数と処理速度を改善するPRです。特に Active Model のserializable_hashなど、JSON シリアライズ時のホットパスでメモリアロケーションを削減します。
- 変更内容の詳細
これまでの実装(問題点)
Enumerable#index_with は、内部的に to_h + ブロックで実装されていました。
概念的には、以下のようなイメージです:
def index_with(default = (no_default = true))
if block_given?
to_h { |element| [yield(element), element] }
else
to_h { |element| [element, default] }
end
endこのような実装だと、各要素について [key, value] の 2 要素配列を毎回生成し、それを to_h 側で展開してハッシュに詰める、という無駄が発生します。
配列要素数が多い、あるいは頻繁に呼び出されるコードパス(例: Active Model の JSON シリアライゼーション)では、この「要素ごとにペア配列を1つ作る」コストが無視できなくなります。
新しい実装
このPRでは、index_with を each_with_object や単純な each と代入で書き直し、ペア配列を介さずに直接ハッシュに代入するように変更しています。イメージは以下のような形です:
def index_with(default = (no_default = true))
result = {}
if block_given?
each do |element|
result[yield(element)] = element
end
else
each do |element|
result[element] = default
end
end
result
end実際のパッチは activesupport/lib/active_support/core_ext/enumerable.rb の数行の差し替えで、to_h 呼び出しをやめ、index_by と同様のスタイル(each でハッシュを埋めていく)に寄せています。
API と返り値の仕様は一切変更されておらず、既存の index_with のテストで挙動互換性が確認されています。
ベンチマーク結果
PR 説明に示されている計測(6 要素配列に対する index_with)では:
| ケース | 変更前 | 変更後 |
|---|---|---|
index_with { ... } | 7 allocations | 1 allocation |
index_with(default) | 7 allocations | 1 allocation |
| 500,000 回呼び出し | 0.353s | 0.332s |
また、26カラム × 20レコードの JSON を生成するケースで、1リクエストあたりの割り当て数が 1332 -> 1206 に減少しています。
時間的な改善は数パーセントですが、アロケーション削減効果が大きく、GC 負荷軽減にもつながります。
- 影響範囲・注意点
挙動の互換性
- 公開 API (
Enumerable#index_with) の仕様・戻り値は変更されていません。 - 既存のテストスイートのみでカバーされており、追加の挙動変更はありません。
- そのため、アプリケーションコード側での修正は不要です。
- 公開 API (
パフォーマンス面の影響
- ActiveSupport を通じて
Enumerable#index_withを多用しているコード、特に:- Active Model / Active Record の
serializable_hash - JSON レスポンス生成(大量レコードの index を組み立てるようなコード) などで、オブジェクト割り当て数削減とわずかな速度改善が期待できます。
- Active Model / Active Record の
- 小さな配列・低頻度な呼び出しでは体感差はほぼありませんが、大量データや高トラフィック環境で積み上がるタイプの最適化です。
- ActiveSupport を通じて
互換性リスク
- 実装詳細に依存したメタプログラミング(
index_with内部でのto_h呼び出しを前提とするようなコード)がもしあれば影響を受ける可能性はありますが、通常の利用ではそのような依存はまず無いはずです。 - Ruby のバージョン依存の挙動変更も伴っておらず、ActiveSupport の範囲内の微修正です。
- 実装詳細に依存したメタプログラミング(
- 参考情報 (あれば)
- 対象メソッド:
Enumerable#index_with(ActiveSupport 拡張) - 近い実装スタイル:
Enumerable#index_by(こちらは以前からeachベースで実装済み) - PR: https://github.com/rails/rails/pull/59108
- 最適化のポイント:
to_h { [k, v] }のようなパターンは、ホットパスではeach+ 直接代入への書き換えでアロケーション削減が見込める、という Rails 内部最適化の好例です。
#59107 Dispatch polymorphic URL helpers for a model without intermediate arrays
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
polymorphic_url/polymorphic_path/url_for(record)などの「レコードを渡すURLヘルパ」の内部実装を整理し、不要な配列・ハッシュの生成を減らしてパフォーマンスを改善したPRです。生成されるURLそのものの挙動・仕様は変えずに、オブジェクト割り当て数を削減するリファクタリングになっています。
- 変更内容の詳細
2-1. 何を問題視していたか
従来の実装では:
polymorphic_url(record, options = {})polymorphic_path(record, options = {})url_for(record)
などが内部で次のような非効率なフローを辿っていました。
polymorphic_url/polymorphic_pathで、中身が空でも options ハッシュを複製していたHelperMethodBuilder.polymorphic_methodからhandle_modelを呼び出し、- 引数リスト (arguments list)
[method, args]という2要素配列
の両方を受け取る
- しかし、その後 named route helper を呼び出すときに、それらを再度 splat展開 (
*args) して使い直しているだけだった url_for(record)も同じhandle_model経由で処理
つまり、「中間配列や重複したハッシュの生成 → すぐにばらす」という無駄なメモリアロケーションが、全ての link_to record, button_to record で毎回発生していました。
2-2. どう改善したか
主な変更点は以下です。
モデルケース専用のディスパッチに変更
- モデル(ActiveRecord等)が渡された場合、
polymorphic_url/polymorphic_path/url_forは
直接dispatch_modelを通じて 対応するnamed route helperを即時呼び出すようにしました。 - これにより、
- 中間的な
handle_model呼び出し [method, args]のペアを作る → すぐ展開する
という余計なステップを削除。
- 中間的な
- モデル(ActiveRecord等)が渡された場合、
handle_modelの削除- 旧来の中心メソッド
handle_modelは、他に呼び出し元がなくなったため削除されました。 - 代わりに
dispatch_model/handle_model_callというよりシンプルな経路で処理する構造に。
- 旧来の中心メソッド
options ハッシュの複製タイミングの見直し
- エントリポイント側 (
polymorphic_url/polymorphic_path等) では、- これからキー削除などミューテーションを行う場合のみ options を
dupするようにしました。
- これからキー削除などミューテーションを行う場合のみ options を
- もともと空の options まで一律に複製していた無駄を削除。
- エントリポイント側 (
URLの挙動は完全維持
- 現行のテストスイート
- polymorphic routes
- URL helper
- form helper
によって、生成されるURLやパスの 挙動が変わっていないことを確認しています。
- つまり「インターフェースも挙動も変えずに内部実装だけを改善」する変更です。
- 現行のテストスイート
2-3. アロケーション削減量
resources :posts にネストされた comments があるケースでの、1呼び出しあたりのオブジェクト割り当て数は以下のように減少しています。
| 呼び出し | before | after |
|---|---|---|
polymorphic_path(post) | 12 | 9 |
polymorphic_url(post) | 17 | 14 |
polymorphic_path(new_post) | 11 | 8 |
edit_polymorphic_path(post) | 14 | 12 |
url_for(post) | 15 | 13 |
polymorphic_path([post, comment]) | 19 | 18 |
配列ベースのポリモーフィック ([post, comment]) の場合は、ほぼそのまま(1個減)で、単一モデルを渡すパターンでの削減が主眼になっています。
- 影響範囲・注意点
挙動の変更は基本的に無し
- 既存の
polymorphic_url,polymorphic_path,url_for(record),form_for,form_withなど、モデルを渡してURLを生成する箇所で URLの文字列表現は変わりません。 - 外部APIとしてのインターフェース (引数、オプション) にも変更はありません。
- 既存の
影響範囲
ActionDispatch::Routing::PolymorphicRoutesに依存する全てのヘルパーに内部的に関係します。link_to postbutton_to postform_with model: postpolymorphic_path(post)など
- ただし「内部実装のシンプル化 + パフォーマンス改善」であり、公開APIの互換性は保たれています。
カスタム/メタプログラミングしている場合の注意
- もしアプリやライブラリ側で
ActionDispatch::Routing::PolymorphicRoutesの 内部メソッド(特にhandle_model) をモンキーパッチしていたり直接呼び出している場合、このPRでhandle_modelが削除されているため、影響を受ける可能性があります。 - 通常のRailsアプリケーションで、公開されているヘルパーのみを使っている限りは問題ありません。
- もしアプリやライブラリ側で
パフォーマンス面
- 1件あたりの削減は小さいですが、一覧ページなどで
link_to recordを大量に描画するケースでは、総アロケーション数が減りGC負荷の軽減が見込めます。
- 1件あたりの削減は小さいですが、一覧ページなどで
- 参考情報 (あれば)
- 該当ファイル
actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
- PRメタ情報
- タイトル: Dispatch polymorphic URL helpers for a model without intermediate arrays
- 番号: #59107
- 作成者: amatsuda
- マージ日時: 2026-10-15T16:06:17Z
このPRは、Rails内部のURLヘルパ周りの実装をシンプルにしつつ軽量化する、純粋なパフォーマンス・リファクタリングとして理解しておくとよいです。
#59106 Cut allocations in per-form CSRF token generation
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Rails の per-form CSRF トークン生成まわりの処理を見直し、主にオブジェクト割り当て(allocation)を大きく削減してパフォーマンスを改善した PR です。CSRF トークンの生成・検証処理の振る舞いは変えずに、URI 解析・Base64 デコード・ハッシュ構築などを効率化しています。
- 変更内容の詳細
2-1. normalize_action_path の最適化
per-form CSRF トークン生成では「フォームの action URL から正規化された path を得る」処理が入りますが、従来は毎回 URI.parse を呼び出していました。
この PR では:
- 「先頭から絶対パス (
/path...) で、かつ RFC 3986 の path で許されている文字だけで構成される場合」は、
→URI.parseをスキップし、その文字列をそのまま path として扱う「fast path」を追加 - 以下のケースは従来通り
URI.parseを通す:- ネットワークパス
//host/path - クエリ文字列・フラグメント付き
/path?x=1#foo - フル URL
https://example.com/path - 相対パス
./foo../bar - 無効なパス(エラーになるもの)
- ネットワークパス
テストでは、「fast path で処理した結果が URI.parse を使った場合と同一になること」および「例外になる入力は引き続き例外になること」を固定しています。
→ 安全性を保ちつつ、典型的な /users, /posts/1 のようなパスでかかるコストを削減しています。
2-2. セッションの「本物の CSRF トークン」の Base64 デコードをメモ化
per-form では、内部的に「セッションに保存されたグローバルな CSRF トークン」を Base64 デコードした値を使ってマスク・HMAC を計算します。
従来:
- 各フォームごとに毎回 Base64 デコード
- 結果、フォーム数に比例して余計なオブジェクト割り当てが発生
修正後:
- 「あるエンコード済みトークン文字列」に対する「デコード済みバイト列」をコントローラ単位でメモ化
- 1 ページ内では同じ CSRF トークンを何度も使うので、Base64 デコードは 1 回で済む
- CSRF トークンをリセットした際は、このメモも無効化されるようにし、その挙動をテストで保証
イメージ的には、コントローラにこんな感じのキャッシュが増えていると考えてよいです(擬似コード):
def decoded_csrf_token(encoded)
@decoded_csrf_tokens ||= {}
@decoded_csrf_tokens[encoded] ||= Base64.strict_decode64(encoded)
end2-3. HMAC 計算まわりのオブジェクト生成削減
per-form トークンは、フォームごとに異なる HMAC を使ってマスク/検証していますが、そこでも余計なオブジェクトが作られていました。
この PR の変更点:
Digestオブジェクトを都度Digest::SHA256.newのように生成する代わりに、OpenSSL::HMAC.digestへ「ダイジェスト名を文字列で渡す」形へ寄せてオブジェクトを減らす- HMAC の識別子を
["foo", "bar"].joinのように配列を作って結合していた部分を、
単純な文字列補間"#{foo}-#{bar}"などに変更し、配列とjoinによる一時オブジェクトを削減
これらにより、HMAC 計算ひとつあたりの allocation を減らしています。
2-4. masked_authenticity_token の呼び出し簡略化
masked_authenticity_token にパラメータを渡す際、以前は values_at(:action, :method) でハッシュから値をまとめて取り出していました。
修正後:
options[:action],options[:method]のように個別に参照するように変更values_atが返す配列の分だけ一時オブジェクトが減る
ロジック自体の変更ではなく、純粋にメモリ割り当て削減を狙ったリファクタリングです。
2-5. token_tag での不要なハッシュマージ・ダブルスプラット削減
token_tag / hidden フィールドのヘルパーでは、以下のような無駄がありました:
form optionsにauthenticity_tokenキーをmergeで追加していたが、
実際にはこのマージ後の options ハッシュはどこからも読まれていなかった- hidden input を描画するときにオプションハッシュを
**optionsとダブルスプラットして渡し、
Hash -> キーワード引数への変換で余計なコピーが発生
今回の変更:
token_tagで options にauthenticity_tokenをマージする処理を削除(振る舞いに影響なし)- hidden input ヘルパーには単に
optionsをそのまま渡す形にし、ダブルスプラットによるコピーを避ける
ActionView まわりの変更 (navigation_helper.rb) の大部分はこれに関連しています。
- 影響範囲・注意点
振る舞いは基本的に互換
- CSRF トークンの生成・検証の 外部仕様(トークンの構造・検証ロジック) は変えていません。
form_with,button_toなど、per-form CSRF トークンを利用するフォームヘルパーの API は後方互換です。token_tagが内部的に merge していたauthenticity_token情報は、そもそも誰も読んでいなかったため、外部コードへの影響はありません。
影響しうる潜在的ポイント
開発者視点で注意するならこの辺です:
Action の正規化ロジック
normalize_action_pathの fast path 追加により、
「これまで偶然URI.parseを経由することに依存した挙動」をしていた非常にレアなケースがあるとすると、影響しうるのはここです。- とはいえ fast path は「URI 的に有効な絶対パス」だけに限定されており、
URI.parseしてもそのまま返る文字列にしか適用されません。
→ 変化が出る余地はほぼありません。
CSRF トークンの decode キャッシュ
- コントローラインスタンス (リクエスト単位) でのメモ化なので、マルチスレッド・マルチリクエスト環境でも状態が共有されることはありません。
reset_authenticity_token的な処理を独自に呼んでいるアプリでも、トークンリセット時にキャッシュが無効化されることがテストで確認されています。
パフォーマンスへの体感
- 大量のフォームを含むページや、高頻度で CSRF トークンを生成・検証するエンドポイントでは、GC 負荷の低減やスループットの向上が見込めます。
- 指標として PR で示されているのは:
- フォーム 1 個あたりの per-form トークン生成:
27 -> 10 allocations - グローバルトークン:
15 -> 8 - per-form トークン検証:
32 -> 17 - 100,000 per-form トークン生成:
0.82s -> 0.62s button_to20 個の index ページ:3927 -> 3490 allocations / request
- フォーム 1 個あたりの per-form トークン生成:
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/59106
- 主に変更されたファイル:
actionpack/lib/action_controller/metal/request_forgery_protection.rbactionpack/lib/action_controller/base.rbactionpack/test/controller/request_forgery_protection_test.rbactionview/lib/action_view/helpers/navigation_helper.rb
- この PR は「per-form CSRF トークン有効時のコスト削減」が主眼で、セキュリティモデルや API 互換性は維持されています。
#59058 Reduce allocations across URL generation
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Rails の URL 生成処理(*_path/*_url/ polymorphic helpers /url_for)において、オブジェクト確保(allocation)を減らすための最適化を行った PRです。生成される URL の文字列自体は一切変えずに、メモリ効率とパフォーマンスを改善しています。
- 変更内容の詳細
2-1. Journey::Format#evaluate の最適化
URL パス部分の組み立てを行う Journey::Format#evaluate で、以下のような無駄なメモリアロケーションが削減されています。
- 以前は:
- parts 配列を
dupしてから処理 - パラメータを escape するたびに新しい文字列を作成
- ネストしたオプションセグメントごとに
joinなどで中間文字列を生成
- parts 配列を
- 変更後:
- 一つのバッファ文字列に対して「追記」していく形に
- オプションセグメントは「含めるべきパラメータが存在するか」を事前チェックし、なければ何も追記しない(従来の early return の挙動を維持しつつ、バッファ方式に)
Router::Utilsのエスケープ処理は、「エスケープが不要な場合」は入力文字列をそのまま返し、新しい文字列を作らない
これにより、URL パス生成時の一時文字列の量が減り、GC 負荷も軽減されます。
2-2. *_url の format オプション専用の高速パス
post_url(post, format: :json) のように *_url 系ヘルパが「内部オプション(format など)」を受け取ると、従来は汎用的な url_for パイプラインにフォールバックしていました。
この PR では、以下の条件を満たす場合に限り、最適化された named route helper のパスを維持します:
- ルートに「オプションの format セグメント」がある
- その
:formatセグメントにデフォルト値が定義されていない - 渡されたオプションが「
formatだけ」
この高速パスでは、一般パスと同じルールを再現しつつ無駄な処理を避けています。
挙動のポイント:
formatが空文字列 ("") の場合
→ 末尾に.だけが付かないように、そのセグメント自体を落とすformatがそのルートのデフォルト値と等しい場合
→ これまでどおり一般パス(url_for)に委譲し、結果としてそのセグメントは出力されないformatが指定されている場合
→trailing_slashオプションの処理はスキップ(フォーマット付きの URL に余計なスラッシュを付けない)
コードイメージ:
# 従来:format を付けると遅い汎用パスに落ちていたケース
post_url(post, format: :json)
# 変更後:以下の条件を満たすなら高速パス
# routes.rb
resources :posts # posts/:id(.:format)
# controller / view
post_url(post, format: :json) # 依然として https://example.com/posts/1.json を返すが allocation は大幅減テストでは以下のケースで「高速パス」と「従来パス」の結果が同じであることを検証しています:
format: :jsonformat: ""(空文字)format: nil- ルート側で
defaultが設定されている場合 trailing_slashオプション指定時
2-3. Polymorphic URL ヘルパ (polymorphic_path, polymorphic_url) の最適化
polymorphic_path(post) や polymorphic_path([post, comment]) のような polymorphic helper では、内部的に「どのメソッド(post_path / post_comment_path など)を呼ぶか」を毎回計算して文字列連結していました。
この PR では、その「メソッド名の文字列」を route key(Post → "post", [Post, Comment] → "post_comment" のようなキー)ごとにメモ化します。
- メモ化は Ractor ごとのストレージ(Ractor-local storage)を用いる
- polymorphic builder オブジェクトが frozen かつ共有される設計のため、グローバルなメモ化ではなく Ractor 単位で行う
- 各 Ractor 内では、一度決定したメソッド名を再利用することで、毎回の文字列生成・連結を避ける
これにより、特に大量の polymorphic URL を生成する場面で allocation を削減できます。
- 影響範囲・注意点
- 互換性:
- 「生成される URL の文字列」は変わらないことが明示されています。
- API のシグネチャや、
*_path/*_url/url_for/ polymorphic helpers の外部仕様に変更はありません。
- 影響が大きい箇所:
- パフォーマンスクリティカルなコントローラや view、その中でも大量に URL を生成している箇所
resources :posts do resources :comments endのような典型的な RESTful ルーティング構成のアプリは恩恵を受けやすい
- 自前の拡張への影響:
Journey::FormatやRouter::Utilsを monkey patch している場合は注意が必要です(内部実装の前提が変わっているため)。- polymorphic ルーティングを独自に拡張している場合、Ractor-local なメモ化の挙動を前提にしないこと(あくまでも内部最適化)。
- Ractor との関係:
- メモ化は「Ractor マルチスレッド下でも安全な形」で行われています。
- Ractor を使用していない通常のアプリケーションでは、単に高速化・省メモリになります。
- 参考情報
PR 内のベンチマーク結果(1 回の呼び出しあたりの allocation 数、resources :posts do resources :comments end なルーティングセットを対象):
| Call | before | after |
|---|---|---|
post_path(post) | 14 | 10 |
post_url(post) | 25 | 21 |
post_url(post, format: :json) | 49 | 21 |
polymorphic_path(post) | 19 | 14 |
polymorphic_path([post, comment]) | 27 | 21 |
url_for(post) | 28 | 23 |
特に post_url(post, format: :json) のような「format 付き URL 生成」が、49 → 21 と大きく削減されているのがポイントです。
大量の API レスポンスやリンク生成を行うアプリケーションでは、GC 負荷の軽減とレスポンス時間の改善が期待できます。
#59105 Avoid materializing per-instance config copies for common reads
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Rails のコントローラ / ビューの設定 (config) を、リクエストごとに必ず複製していた挙動を「初回アクセス時にだけ複製する」ように変更し、不要なオブジェクト生成を減らしてパフォーマンスを改善する PR です。既存の API 振る舞いは維持しつつ、特にログ出力だけを行うような軽いアクションでのオブジェクト割り当て数が削減されています。
- 変更内容の詳細
背景: 何が問題だったか
- コントローラには「クラスレベルの config」と「インスタンスレベルの config」があり、
InheritableOptionsで継承されます。 - ビュー (
ActionView::Base) 側にもコントローラから継承したconfigがあり、これもInheritableOptions。 - 従来の挙動:
- 各リクエストのレンダリング時に:
- コントローラのインスタンス用
configのコピー - ビュー用の
configコピー
この 2 つが毎回作られていた。
- コントローラのインスタンス用
- しかし多くのリクエストでは、この
configは一度も読み取られないまま終わる。
- 各リクエストのレンダリング時に:
- つまり「ほぼ使われない設定オブジェクトのコピー」を、毎リクエストごとに 2 個作ってしまっていた。
これがオブジェクト割り当て増加・GC 負荷増大の原因になっていたため、lazy (遅延) 生成に変えるのがこの PR の目的です。
コントローラ側: logger アクセス時の挙動変更
変更ファイル: actionpack/lib/abstract_controller/logger.rb
- 従来:
- コントローラインスタンスの
loggerを読む際、インスタンス用configが毎回事前に用意され、そのconfig.loggerを見る形になりがちだった。
- コントローラインスタンスの
- 変更後:
- 「インスタンス用 config がまだない」状態では、クラスレベルの config 経由で logger を読むようにした。
- つまり:
- 単に
logger.infoなどでログを出すだけなら、インスタンス用configのコピーは生成されない。 - 明示的に
controller.logger = ...と「インスタンスに対して logger を代入」したときだけ、インスタンス用configが初めて生成される。
- 単に
テスト (actionpack/test/abstract/logger_test.rb) では次を確認:
loggerを読むだけではインスタンス用 config がまだ存在しない。controller.logger = custom_loggerのように代入した時点でインスタンス config が作られる。
ビュー側: config の遅延コピー
変更ファイル:
actionview/lib/action_view/base.rbactionview/lib/action_view/helpers/controller_helper.rb
従来
- ビューインスタンス生成時 (
assign_controllerなど) に、必ず「ビュー用configのコピー」が eagerly (即時) 生成されていた。 - このビュー用
configは親 (コントローラ) のconfigを元にInheritableOptionsで複製される。
変更後
- ビューの
configコピーはview.configが最初に読まれたタイミングで初めて生成 されるようになった。 - 具体的な性質:
- ビュー生成後、
configに一度もアクセスしない場合
→ ビュー用configは一切生成されない。 view.configを初めて読む時点で:- その瞬間の「コントローラの config の内容」を元にビュー用
configを作る。 - つまり、「ビュー生成後〜最初の
view.config読み込みの間」にコントローラのconfigが変更されていれば、その変更をちゃんと反映した状態でビュー用configが作られる。
- その瞬間の「コントローラの config の内容」を元にビュー用
- ビュー側で
view.config.some_option = ...などと書き換えた場合:- その書き込みは ビュー自身の
configコピーにだけ 反映され、コントローラ側には影響しない(従来通りの isolation を維持)。
- その書き込みは ビュー自身の
- ビュー生成後、
テスト (actionview/test/template/test_case_test.rb) では次を確認:
- ビュー作成後にコントローラの
configを変更しても、view.config初回アクセス時にはその更新を反映した値が見える。 - ビュー側で
configを変更しても、コントローラ側に「漏れない」(view 専用のコピーに閉じている) こと。
- 影響範囲・注意点
パフォーマンス面
PR の記載による割り当て数の変化 (ActionController::Base 経由):
| ケース | 変更前 allocations | 変更後 allocations |
|---|---|---|
render plain: | 133 | 125 |
render inline: + link_to / tag ヘルパー使用 | 212 | 204 |
logger を読むだけの head :ok アクション | 91 | 87 |
- 軽量な API エンドポイントや、単純な
render plain:のようなケースでオブジェクト生成が減少。 asset系ヘルパーなど「ビューのconfigを読む」ケースでは、初回アクセス時に結局両方のコピー(コントローラインスタンス用、ビュー用)が作られるため、「config を使うページ」の挙動やコストはほぼ従来通り。
互換性・注意点
- API の表面的な挙動は維持 されています:
controller.config/view.configの値の見え方、継承関係、ビュー側の書き込みがコントローラに影響しない点などは変わらないようにテストで担保。
- 挙動が変わる可能性のある「グレーゾーン」:
- これまでは「ビュー生成と同時に
configコピーが作られていた」ため、非常にタイミング依存なコード(たとえば「ビュー生成直後にコントローラの config を書き換えて、ビューのほうには反映させたくない」など)を書いていた場合、観測されるタイミングが変わる可能性があります。- ただし、通常のアプリではまずやらないパターンで、PR では逆に「ビュー生成後のコントローラ config 変更がビューにも反映される」ことを明示的に保証しています。
- インスタンス config の存在チェックに依存したメタなコードを書いている場合も挙動が変わります:
- 例: 「logger を一度でも読んだらインスタンス config が作られる」と仮定して
controller.instance_variable_defined?(:@_config)などを見ていた場合、この PR 以降は作られないままになるケースがあります。
- 例: 「logger を一度でも読んだらインスタンス config が作られる」と仮定して
- これまでは「ビュー生成と同時に
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59105
- 変更対象クラスの主な関連箇所:
ActionController::Base/AbstractController::Logger周りのlogger取得ロジックActionView::Base#configとActionView::Helpers::ControllerHelper#assign_controller
- 実務上の示唆:
- コントローラやビューの
configを「毎リクエストのホットパスで触らない」設計になっていれば、今回のような lazy 化の恩恵を受けやすいです。 - 逆に、
configを濫用してリクエストごとの状態を詰め込んでいるとパフォーマンス面での恩恵は限定的になるため、configは基本的に「設定用」、一時的なリクエストデータはインスタンス変数やrequest,paramsなどに保持するほうが望ましいです。
- コントローラやビューの
#59104 Memoize the host on the request and skip the port regexp when there is none
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Rails のActionDispatch::Requestにおいて、host/raw_host_with_portの取得結果をリクエスト単位でメモ化し、かつポートが含まれない場合はポート除去用の正規表現処理をスキップすることで、ホスト名取得まわりのオブジェクト生成と処理コストを削減する変更です。併せてテスト用リクエスト (TestRequest) がhost=/port=で値を変えたときにメモ化された値もリセットされるようになりました。
- 変更内容の詳細
2-1. host / raw_host_with_port のメモ化
これまで:
request.host- 毎回 env (
HTTP_HOST,X-Forwarded-Hostなど) からホスト文字列を組み立て - 毎回「ポート番号を取り除く」ための正規表現 (
gsub相当) を実行 - ポートが付いていなくても
StringとMatchDataが毎回生成される
- 毎回 env (
request.raw_host_with_port- 同様に毎回 env から組み立て
今回:
ActionDispatch::Requestインスタンス内に、既存の@portと同様に以下をメモ化@raw_host_with_port@host
- 一度計算したら同じリクエスト中は再計算しない
url_optionsの生成- リダイレクト (
redirect_to) - クッキー関連
- ホスト認可 (
config.hosts) などでのホスト参照が高速化
2-2. ポート除去の正規表現を条件付きに
- 以前は
hostを返すたびに「ホスト名からポート番号を取り除く正規表現」を必ず実行していた - 今回の変更で、
raw_host_with_portに:(コロン)が含まれている場合のみ正規表現でポートを取り除く- それ以外はそのまま(ポート除去用の正規表現は走らない)
結果として、典型的なプロキシ配下(HTTP_HOST にポートが含まれない)のケースでは大半のリクエストで MatchData / 追加の String が発生しなくなる。
2-3. TestRequest#host= / #port= がメモ化をクリア
テスト環境の
ActionDispatch::TestRequestに対して:rubyrequest.host # ← ここで @host / @raw_host_with_port がメモ化される request.host = "example.org" request.port = 3000以前:
@portだけがメモ化されていて、host=/port=後に値を変えても内部のメモ化状態が一部更新されず「古い値が使われ続ける」バグポテンシャルがあった(今回の PR によりテストで検出されるようになった)
今回:
TestRequest#host=および#port=が@hostと@raw_host_with_portのメモをクリアするようになった- テスト中にホストを読み取った後で
request.host = ...しても、再度hostを読むと新しい値が反映される
2-4. テストの追加
テストでは以下をカバー:
- ヘッダ由来のホストがポート付き/なしの両方の場合の挙動
- IPv6 リテラル(
[2001:db8::1]:3000のような形式)を含むケース X-Forwarded-Hostが指定された場合の挙動- メモ化が効いていること(同一リクエスト内の複数回の呼び出しで再計算されない)
TestRequest#host=/#port=で、すでに読んだ後に値を変更しても正しく再解決されること
- 影響範囲・注意点
影響範囲
ActionDispatch::Request#host/#raw_host_with_portを利用する全機能- URL ヘルパー (
url_for,*_url) - リダイレクト (
redirect_to) - Cookie ドメイン判定
config.hostsによるホスト認可
- URL ヘルパー (
- テストコードで
ActionDispatch::TestRequestを直接操作している場合
互換性・注意点
- 破壊的変更は基本的に無し
- もともとフレームワーク側は
request.hostから返されるStringを破壊的に変更していない前提で動いており、その前提を確認したうえでメモ化している - もしアプリケーション側で
request.host.upcase!のように「戻り値のStringを破壊的に変更」している場合、同一リクエスト内のほかの処理にその変更が影響する可能性がある(元々も似た影響はあったが、メモ化により顕在化しやすくなる)
- もともとフレームワーク側は
- テストコードで
request.host =/request.port =を使っている場合:- これまで暗黙に依存していた「ホストを一度読み取った後に setter を呼んでも内部状態が古いまま」というバグっぽい挙動は修正された
- 通常は望ましい修正であり、テストが壊れることはむしろ少ないはず
- 破壊的変更は基本的に無し
パフォーマンス影響(ActionDispatch::Request 測定値):
HTTP_HOSTがポートなしのケース- 最初の
host取得時の String/MatchData アロケーション- before: 2
- after: 0
- 2回目以降の
host取得- before: 5
- after: 0
- 最初の
- URL ヘルパー関連:
- リダイレクトで URL ヘルパーを呼ぶリクエスト
- before: 180
- after: 173
- ビューで URL ヘルパーをレンダリングするリクエスト
- before: 174
- after: 172
- リダイレクトで URL ヘルパーを呼ぶリクエスト
数字は「オブジェクトアロケーション数」と見てよい程度の粒度で、ホスト取得周りのコストがわずかに減っていることを示しています。高トラフィック環境では GC 負荷の微減が期待できます。
- 参考情報 (あれば)
- 対象クラス/モジュール
actionpack/lib/action_dispatch/http/request.rbactionpack/lib/action_dispatch/http/url.rbactionpack/lib/action_dispatch/testing/test_request.rb
- テスト
actionpack/test/dispatch/request_test.rbactionpack/test/dispatch/test_request_test.rb
- この PR の背景:
- プロキシ配下など、実運用環境で最も一般的な「ポートなしの
HTTP_HOST」ケースで、無駄なString/MatchData生成が多いことへの最適化 @portだけがメモ化されていた既存実装の不整合を是正し、host/raw_host_with_portも同じパターンで扱うことでコードの一貫性とテスト容易性を高めている
- プロキシ配下など、実運用環境で最も一般的な「ポートなしの
#59103 Cache derived controller constant names
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
controller_class_forが毎回パラメータ文字列を underscore / camelize していた処理を、「パラメータ → コントローラ定数名文字列」の対応をキャッシュすることで高速化した PR です。キャッシュしているのは定数そのものではなく「名前文字列」なので、クラスリロードの安全性を保ったままリクエストあたりの文字列変換を削減しています。
- 変更内容の詳細
背景となる問題
controller_class_forは、:controllerパラメータからコントローラクラスを解決するために、毎回:underscorecamelizeなどの inflector による文字列変換を行っていました。
- ルーティングのパラメータエンコードのチェックの都合で、1リクエスト中に同じパラメータに対して複数回呼ばれ、結果として
- 「1リクエストあたり 8 回分の inflector 呼び出し」 が発生していました。
- これはパラメータ値にのみ依存する純粋関数的な変換なので、結果をキャッシュすれば再計算は不要です。
具体的な変更点
1) 「パラメータ → 定数名文字列」のキャッシュを導入
- キャッシュ単位:
- キー:
params[:controller]の値(例:"users","admin/users"など) - 値: 解決されたコントローラの定数名文字列(例:
"UsersController","Admin::UsersController")
- キー:
- キャッシュ格納場所:
- Ractor ごとの
Concurrent::Map
→Ractorローカルにしているのは、Ractor 間での共有を避けつつスレッドセーフなキャッシュにするためと思われます(Concurrent::Mapはスレッドセーフな Hash 互換)。
- Ractor ごとの
イメージ的には、Rails 内部では次のようなキャッシュが保持されるようになります:
# 擬似コードイメージ
CONSTANT_NAME_CACHE = Concurrent::Map.new
def controller_constant_name_for(controller_param)
CONSTANT_NAME_CACHE.compute_if_absent(controller_param) do
# ここで underscore / camelize / module nesting を組み立てる
# 例: "admin/users" -> "Admin::UsersController"
end
end
def controller_class_for(controller_param)
name = controller_constant_name_for(controller_param)
name.constantize # ここは毎回実行する
end2) キャッシュは「名前文字列」だけで、定数は毎回 constantize
- キャッシュしているのは「
"Admin::UsersController"のような文字列」であり、クラスオブジェクトそのものではありません。 - そのため:
- クラスリロードに強い:
- 開発環境などで
- 新しいコントローラが定義された
- 既存のコントローラクラスが削除・再定義された
- といった場合でも、
constantizeが毎回実行されるため、常に最新の定義に解決されます。
- 開発環境などで
- クラスリロードに強い:
3) キャッシュミス時の挙動と上限について
- キャッシュに存在しないパラメータが来た場合:
- 従来と同じ処理を行って名前を導出し、その結果だけをキャッシュする。
- 注目点:
- 解決に成功した名前のみキャッシュ:
- 実在しないコントローラ名(例:
"foo/bar"に対応するコントローラがない)が来た場合は、従来通り例外を投げる / エラー処理を行い、その結果はキャッシュしません。
- 実在しないコントローラ名(例:
- キャッシュサイズは「存在するコントローラの数」にほぼ限定される:
- キャッシュされるのは「実際に解決できたコントローラ名」だけなので、暴走して無限に増える類のキャッシュではありません。
- 動的な
:controllerセグメントはルーティング定義時に拒否されるため、params[:controller]は基本的に「routes.rb に書かれたコントローラに対応する値」に限られます。
- 解決に成功した名前のみキャッシュ:
4) リロードセーフティを担保するテスト追加
actionpack/test/dispatch/request_test.rb に2つのテストが追加されています:
最初は見つからなかったコントローラが、定義後に解決されること
- 手順イメージ:
- 存在しないコントローラ名で解決を試みて失敗
- そのコントローラを定義
- 再度同じパラメータで解決 → 今度は成功
- これにより、「解決失敗時の結果はキャッシュしていない」ことと、「新しい定義を拾える」ことを保証。
- 手順イメージ:
既存コントローラを削除して再定義した場合に、新しいクラスに解決されること
- 手順イメージ:
- あるコントローラを定義して解決
- その定数を削除し、別のクラスで同じ名前を再定義
- 再度
controller_class_forを呼ぶ → 新しい定義のクラスに解決
- 「キャッシュされているのはクラスではなく『名前文字列』だけ」であることの確認になっています。
- 手順イメージ:
5) パフォーマンス計測結果
head :ok だけ返すような非常にシンプルなアクションに対しての計測:
| 項目 | before | after |
|---|---|---|
| routed request | 96 | 87 |
controller_class_for | 2 | 0 |
routed requestの数字は、おそらく一定回数のリクエストに対する合計あるいは何らかのカウンタ値で、全体として呼び出し回数/処理コストが下がっていることを示しています。controller_class_forのカウンタが2 → 0になっているのは、内部の文字列変換呼び出しなどが削減されていることを意味します(計測方法次第ですが、「無駄な 2 回分が削れた」と読んでよい変更)。
- 影響範囲・注意点
影響範囲
- 直接影響するのは
ActionDispatch::Request内部のcontroller_class_forを利用するルーティング経路解決処理です。 - エンドユーザのコードが呼ぶ API のシグネチャや振る舞いは基本的に変わりません:
params[:controller]の指定方法は従来通り。- コントローラのオートロードやリロードの挙動も変えないように設計されています。
開発者が意識しておくとよい点
動的な :controller セグメントが禁止される前提:
- Rails 本体側で、「動的に任意の文字列を :controller として受け取る」ルーティングは draw 時に拒否されるため、このキャッシュは「routes.rb に書かれた静的(もしくは限定的)なコントローラ」に対してのみ機能します。
- もし独自に低レベル API を叩いて同様のことをしている場合は、この前提を崩すとキャッシュキーが予期せず増えていく可能性があります(通常は関係ありません)。
リロードに依存した高度なメタプログラミング:
- クラスを頻繁に定義・削除するようなメタプログラミングをしている場合でも、
constantizeが毎回実行されるため基本的には従来通り動くはずです。 - ただし、「コントローラ名の解決ロジックそのものをモンキーパッチしている」ような環境では、キャッシュの存在が予想外の挙動を生む可能性があります(そのような用途は推奨されませんが)。
- クラスを頻繁に定義・削除するようなメタプログラミングをしている場合でも、
Ractor を使う環境:
- キャッシュは Ractor-local な
Concurrent::Mapに置かれているため、Ractor ごとに独立したキャッシュを持ちます。 - 複数 Ractor で Rails を動かしているような将来的な構成でも、Ractor 間で状態を共有せず安全に動作する設計と言えます。
- キャッシュは Ractor-local な
- 参考情報 (あれば)
関連コード位置(概念的な場所):
actionpack/lib/action_dispatch/http/request.rbcontroller_class_forおよびその周辺ロジックにキャッシュロジックが追加。
actionpack/test/dispatch/request_test.rb- リロードセーフティを確認する2つのテストが追加。
関連する Rails の典型的なコードパス:
- ルーティング →
ActionDispatch::Routing::RouteSet→ActionDispatch::Request#controller_class_for→ コントローラクラス解決 → アクション実行
この PR はこの「コントローラ解決」部分のオーバーヘッドを削る最適化です。
- ルーティング →
概念的なまとめ:
- 「同じ
params[:controller]に対して、何度もunderscore/camelizeするのは無駄なので、一度計算した『コントローラ定数名』文字列を安全な範囲でキャッシュしよう」という、リクエストあたりの微小コスト削減のための最適化 PR です。
- 「同じ
#59102 Use Symbol#name for session key stringification
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Rails のセッションアクセスで、キーが Symbol の場合にto_sではなくSymbol#nameを使って文字列化するようにして、余計な String オブジェクトの生成をなくすパフォーマンス改善です。セッションの API の挙動(Symbol/String キーの互換性)は維持したまま、アクセスごとのアロケーション削減と高速化を行っています。
- 変更内容の詳細
何をしたか
セッションアクセス時のキー変換ロジックを private メソッド
stringify_keyに集約し、以下の全ての箇所で利用するように変更:[][]=dighas_key?(key?)deletefetch(成功パスとキー未定義パスの両方)
stringify_keyの中で、Symbol が渡された場合はkey.nameを使い、String の場合は従来どおりkey.to_s(=self を返すので追加アロケーションなし)を使う形になっています。
擬似コードで書くと、イメージは次のようになります:
class ActionDispatch::Request::Session
private
def stringify_key(key)
case key
when Symbol
key.name # intern された frozen String を返す
else
key.to_s # String の場合は self、それ以外は従来通り
end
end
public
def [](key)
@delegate[stringify_key(key)]
end
def []=(key, value)
@delegate[stringify_key(key)] = value
end
def key?(key)
@delegate.key?(stringify_key(key))
end
def delete(key)
@delegate.delete(stringify_key(key))
end
def fetch(key, *args, &block)
@delegate.fetch(stringify_key(key), *args, &block)
end
def dig(*keys)
# 先頭キーを stringify するなど、全呼び出し点で共通化
end
endなぜ Symbol#name なのか
- Ruby 3.3〜4.0 のリリース版では、
Symbol#to_sは毎回新しい String を生成します。 - 一方、
Symbol#nameはその Symbol に対応する「内部的に intern 済みの frozen String」を返します:- 毎回同じオブジェクト(同一の frozen String)を返す
- よってアクセスごとに新しい String を生成しない
- Rails のセッションストア(内部的には Hash ライクな構造)は、キーに Symbol を渡すときも最終的には String をキーとして使う前提で設計されています:
:user_idと"user_id"で同じエントリを参照できる- これは
Hash#[]=がシンボルを受けたらto_sした結果をキーにする、という従来の仕様と合致する
Symbol#nameは、Hash#[]=が最終的に持つはずの String と同じ性質(intern 済みかつ frozen)を持っているため、目的に合致しています。
テストの追加内容
- これまで
fetchとdigについては、Symbol/String キーの互換性テストがあったが、- 書き込み (
[]=) → 読み込み ([]) key?/has_key?deleteについても「Symbol/String キーで同じエントリを指す」ことを明示的にテストで固定化。
- 書き込み (
- これにより、今回の実装変更で挙動が変わっていないことを保証し、今後のリグレッションも防いでいます。
- 影響範囲・注意点
影響範囲
- 対象:
ActionDispatch::Request::Session(Rails のセッションオブジェクト)の全ての主要アクセスメソッド- 典型的なコード例:ruby
session[:user_id] session[:user_id] = current_user.id session.key?(:user_id) session.delete(:user_id) session.fetch(:some_key) { ... } session.dig(:nested, :key) # (実際にはセッション構造次第)
- このあたりで Symbol をキーとして渡している場合に、以前はアクセスごとに String が 1 つ生成されていたものが、生成されなくなります。
互換性・挙動の変化
- 公開 API の挙動(機能面)は変わりません:
- Symbol と String のキーが同じエントリを指すruby
session[:user_id] = 1 session["user_id"] # => 1 session.key?("user_id") # => true session.delete("user_id") # => 1 - これらは新しい実装でも同じ結果になります。
- Symbol と String のキーが同じエントリを指す
- String キーの場合は元々
to_sが self を返していたため、前後で挙動もパフォーマンスも変化しません。
パフォーマンス面
- Ruby 3.4.11 におけるベンチマーク(キー変換+ハッシュ lookup):
- 1 アクセスあたりのアロケーション数:
- 変更前: 1(毎回 String を 1 つ生成)
- 変更後: 0
- 5,000,000 回のアクセス:
- 変更前: 0.46 秒
- 変更後: 0.30 秒
- 1 アクセスあたりのアロケーション数:
- セッションアクセスが多いアプリケーション(ページビューが多い、あるいはミドルウェア的な処理でセッションに頻繁にアクセスする)では、GC 負荷とレスポンス時間の両面でわずかに有利になります。
注意点
- カスタムミドルウェアやプラグインで、セッション内部の実装に強く依存しているコード(例: セッションハッシュに直接アクセスしてキーの class や frozen 状態を検査しているようなコード)があると、
nameによる intern 文字列(frozen String)を前提とした挙動の違いに影響される可能性はありますが、通常のアプリケーションコードでは問題にならない想定です。 - 今後 Ruby 本体の
Symbol#to_s実装が変わる可能性はあるものの、この PR は「現在の Ruby の挙動における最適化」であり、挙動を固定するテストも追加されているため、Rails 側としては一貫した動作が維持されます。
- 参考情報 (あれば)
- Ruby の
Symbol#nameについて::foo.to_sは毎回"foo"の新しい String を生成(Ruby 3.3〜4.0)。:foo.nameは常に同じ"foo"の intern 済み frozen String を返す。
- セッション API 利用例(本 PR の対象箇所のイメージ):ruby
# 読み取り current_user_id = session[:user_id] # 書き込み session[:user_id] = user.id # 存在確認 if session.key?(:user_id) # ... end # 削除 session.delete(:user_id) # デフォルト値付き取得 token = session.fetch(:csrf_token) { SecureRandom.hex(32) }
#59099 Skip path normalization for already-normalized request paths
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
ActionDispatch::Routing::RouteSet#call内で、すでに正規化済みのパスについては再度正規化処理を行わないようにし、リクエストごとの不要な String アロケーションを削減するパフォーマンス改善です。挙動(どのパスがどう正規化されるか)は変えずに、既存の挙動を保ったまま高速化しています。
- 変更内容の詳細
2-1. これまでの問題
- すべてのリクエストパスに対して
RouteSet#call内でnormalize_pathが呼ばれていた。 - 大半のパスはすでに「正規化済み」なので処理としてはほぼ何もしないものの、
normalize_pathは常に新しい String を返していた。- 理由: ルーティング定義時(boot 時)に mapper 側でその String を破壊的変更するため、呼び出し側にはコピーを返しておく必要がある設計になっている。
- 結果として、「実質的に何も変えないのに String を1個余分に作る」というコストを、すべてのリクエストが支払っていた。
2-2. 今回の対応方針
- 「そのパスはすでに正規化済みか?」を判定する述語メソッド
Utils.normalized_path?を追加。 RouteSet#call内では、この述語が true の場合はnormalize_pathを呼ばず、元のパス文字列をそのまま使う。- 正規化が必要なパス(例: 先頭スラッシュ欠落、末尾スラッシュ、連続スラッシュ、パーセントエンコード含み)は、従来どおり
normalize_pathを通す。
追加された述語: Utils.normalized_path?
PR本文から分かる要件を整理すると、normalized_path? が「正規化済み」とみなすのは、少なくとも以下を満たすパスです:
- 先頭に
/が付いている - 末尾に
/が付いていない(ルート/自体を除く) //のような連続スラッシュを含まない%を含まない
→ パーセントエンコードされたパスは「正規化済み」だとしても、あえて normalized と判定しない(後述の通り保守的な設計)
実装イメージ(厳密なコードではなく概念的なもの):
module ActionDispatch::Journey::Router::Utils
def self.normalized_path?(path)
return false unless path.is_a?(String)
return true if path == "/"
return false unless path.start_with?("/")
return false if path.end_with?("/")
return false if path.include?("//")
return false if path.include?("%") # エスケープは保守的に false 扱い
true
end
endRouteSet#call 側での利用
RouteSet#call は、これまで:
path = Utils.normalize_path(env[PATH_INFO])のように、無条件に normalize_path を呼んでいましたが、変更後は:
raw_path = env[PATH_INFO]
path =
if Utils.normalized_path?(raw_path)
raw_path # そのまま使う(コピーしない)
else
Utils.normalize_path(raw_path) # 従来通り正規化
endという形になり、「明らかに正規化済み」と判定できるケースでは新規 String を作らなくなります。
2-3. 保守的な判定ロジック(パーセントエンコードについて)
%を含むパス(例:/foo%20bar)は、たとえすでに大文字のエスケープ(%20)になっていても「正規化済み」とはみなさない。- そのため、そうしたパスは引き続き
normalize_pathを通ります。 - PR ではこれを「保守的な設計」と説明しており、
- 「normalized_path? が true と判定するパス」については、
normalize_pathを省略しても安全 %を含むなど少しでも怪しいパスは常に従来どおりnormalize_pathに通しておき、安全側に倒す
- 「normalized_path? が true と判定するパス」については、
- 結果として、「これまで normalize_path を通っていたパスが突然スキップされる」範囲を、非常に限定的にしているといえます。
2-4. テストの追加
actionpack/test/journey/router/utils_test.rb に、normalized_path? の挙動を normalize_path と比較するテストが追加されています。PR説明より:
normalized_path?が「そのまま」と判定するパス群normalize_pathが実際に変化を加えるパス群%を含み、normalized_path?が「normalized とは判定しない」パス群
それぞれについて、「結果が normalize_path のこれまでの挙動と矛盾していないこと」がテストで固定されています。
- 影響範囲・注意点
3-1. 挙動面の互換性
normalize_pathの仕様は変えていないため、「どの入力パスが最終的にどう変形されるか」は従来と同じです。- 変わるのは、「正規化処理を実際に呼ぶかどうか」という内部実装のみ。
- 特に、次のようなパスはこれまでどおり正規化されます:
posts→/posts/posts/→/posts/foo//bar→/foo/bar/foo%2fbarなど%を含むもの(大文字・小文字に関わらず)
3-2. パフォーマンスへの影響
PR 説明にあるアロケーション数:
| ルート | before | after |
|---|---|---|
動的 /posts/:id | 19 | 18 |
静的 /about | 6 | 5 |
- 1リクエストあたりの String アロケーションが 1つ減っている(典型的なケースで)。
- 高トラフィックアプリケーションでは、GC 負荷がわずかに軽減される可能性があります。
3-3. 互換性リスク・注意点
通常の Rails アプリ開発者視点では、ほぼ影響を気にする必要はありませんが、以下のようなケースでは知っておく価値があります。
- Rack ミドルウェアや自前コードで
env[PATH_INFO]を読み取り・書き換えしている場合:- 「書き換えたあとの PATH_INFO が、従来は
normalize_pathによって別の文字列オブジェクトになっていた」のが、 - 今回の変更により、「完全に同じ String オブジェクトが後段まで渡る」ことが増える可能性があります。
- 「書き換えたあとの PATH_INFO が、従来は
- ただし、Rails 本体側ではその前提(オブジェクトIDに依存した挙動など)は基本的にしていないため、通常の用途では問題になりません。
normalize_path自体の挙動を monkey patch しているような特殊ケースでは、RouteSet#callがnormalize_pathを呼ばないパスが増えるという意味で、間接的影響が出る可能性はあります。
- 参考情報 (あれば)
- 目的:
RouteSet#callでのパス正規化を「必要なときにだけ」行うことで、不要な String アロケーションを削減するパフォーマンス改善。 - 実装のポイント:
ActionDispatch::Journey::Router::Utils.normalized_path?を新設。- 保守的な判定(特に
%を含むパスは常に旧来の経路を通す)。 - テストで
normalized_path?とnormalize_pathの関係をしっかり固定。
- 結論として、「外から見える挙動はそのままに、RouteSet レベルでの微小な高速化をした PR」と理解しておくとよいです。
#59098 Make the abstract controller logger test order independent
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Rails のabstract controller loggerに関するテストが「テスト実行順序に依存して失敗する」問題を解消するため、テストコードが修正されました。特定のシード値でのみ落ちる不安定テストを、クラス設定の汚染を避ける形で安定化しています。
- 変更内容の詳細
問題となっていた点
actionpack/test/abstract/logger_test.rb に追加されていた「インスタンスレベルで logger を代入する」テストが、他のテストと クラスレベルの設定を共有・上書き してしまい、テスト順序によって結果が変わっていました。
説明文をコードレベルに落とすと、以下のような構造になっていたと考えられます(擬似コード):
class Parent < AbstractController::Base
end
class Child < Parent
end
class LoggerTest < ActiveSupport::TestCase
def test_inheritance
Parent.logger = some_logger
assert_equal some_logger, Child.logger # ← 継承の確認
end
def test_instance_assignment
begin
Child.logger = instance_logger
# ... 何らかの assertion ...
ensure
Child.logger = nil # ← ここが問題
end
end
endこの ensure ブロックで Child.logger = nil を明示的にセットしてしまうと、
Child自体の設定に「logger=nil」という値が保存される- その結果として、後続で
Parent.logger = xを設定しても、Child側では「自前の設定(nil)が存在する」と見なされて継承を上書きしてしまう - よって、
test_instance_assignment→test_inheritanceの順に実行された場合に、test_inheritance内のChild.loggerがnilになり、テストが失敗する
つまり、テストがクラスの設定を汚染しており、テスト順序依存の不具合になっていました。
PRで行われた修正
この問題を解消するために、インスタンス代入のテストで Child を直接いじるのをやめて、Parent の一時的なサブクラスを使う 形に変更されています。
イメージとしては以下のような変更です(概念的な擬似コード):
def test_instance_assignment
klass = Class.new(Parent) # ← 毎回新しい無名サブクラスを作る
instance = klass.new
instance.logger = instance_logger
assert_equal instance_logger, instance.logger
# 明示的に klass.logger = nil などはしない
# 無名クラスなのでテスト終了後に参照されず、設定は他のテストに影響しない
endポイント:
Child固有の設定 (Child.logger) を書き換えなくなった- 代わりに、そのテスト内だけで完結する「使い捨てのサブクラス」を作ることで、
→ 親クラスParentや本物のChildの設定を汚染しない - これにより、
loggerの継承テストとインスタンス代入テストが 完全に独立 し、実行順序が入れ替わっても結果が変わらなくなる
行数ベースでも、+11/-9 と小さな変更で、主にテスト対象クラスの扱い方を切り替えているだけです。
- 影響範囲・注意点
影響範囲
- 変更対象は
actionpack/test/abstract/logger_test.rbのみで、本体コード(AbstractController本体や logger 実装)には変更がありません。 - したがって、実行時の挙動や API に対する影響はなく、既存アプリケーションの動作には影響しません。
- 影響するのは Rails のテストスイート(特に
actionpackの logger 関連テスト)の安定性のみです。
- 変更対象は
注意点 / 開発者視点での示唆
- Rails では設定がクラスレベルでキャッシュされる・継承されることが多いため、
- テストでクラスの設定を書き換える場合は「元の状態への戻し方」によってはクラス自体を汚染しうる
- 今回のように「明示的な
nilの代入」が継承ロジックを壊すパターンはよくある落とし穴
- 同様のケースを避けるためには:
- テスト専用の無名サブクラス(
Class.new(BaseClass))を積極的に使う ensure/teardownでの「リセット」が本当に元の状態に戻しているか(nilにすることが「未設定」と同義とは限らない)を見直す
- テスト専用の無名サブクラス(
- CI で seed を変えてテストを回している場合、今回の修正により logger 関連テストのランダムな失敗は解消されるはずです。
- Rails では設定がクラスレベルでキャッシュされる・継承されることが多いため、
- 参考情報 (あれば)
- 関連 PR:
- #59090: 今回修正対象となった
abstract controller loggerテストを追加した PR
- #59090: 今回修正対象となった
- テスト実行例:
- 問題が発生していたコマンド:
bin/test test/abstract/logger_test.rb --seed 1(actionpack)
→ 本 PR により、シード値に依存した失敗が発生しなくなる想定です。
- 問題が発生していたコマンド:
- 一般的なパターンとして、Rails コアでも「設定の継承とテスト独立性」を担保するために
Class.new(Parent)パターンが頻繁に使われています。
#59097 Memoize the attribute builder per schema context
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Active Record のレコード生成時に使われるattributes_builderを「スキーマコンテキストごと」にメモ化し、毎回新しく計算・生成していたオブジェクトを再利用することで、レコードインスタンス化のオーバーヘッドを削減する変更です。これにより、レコード大量生成時のオブジェクトアロケーションと処理時間が大幅に改善されています。
- 変更内容の詳細
何が問題だったか
Post.instantiate(row) のように Active Record が DB 行からモデルインスタンスを生成するたびに、内部で以下のような処理が走っていました:
column_namesからプライマリキーを引いた配列の再計算- デフォルト属性ハッシュの
exceptコピー AttributeSet::Builderの新規生成- 上記をまとめた
attributes_builderの構築
つまり「レコード 1 件インスタンス化するたびに少なくとも 4 つのオブジェクトが余計に割り当てられていた」状態です。
今回の対応方針
- これらは「テーブル定義(スキーマ)に依存するが、レコードごとには変わらない」情報
- したがって、テーブルごと(より正確には「schema context」ごと)に一度だけ作って共有すればよい
という発想で、attributes_builder を schema context の Attributes オブジェクト上でメモ化 するようにしました。
既に schema context には以下がメモ化されていました:
types(カラム型情報)column_defaults(カラムのデフォルト値)
今回、ここに attributes_builder のキャッシュを追加したイメージです。
メモ化の安全性
attributes_builder の入力となる情報が変わるタイミングは、Active Record 内で整理されており、以下の操作にほぼ集約されています:
reload_schema_from_cacheattribute登録reset_column_informationignored_columns=
primary_key=- モデル階層(親クラスを含む)全体のスキーマを再読み込みする
これらが呼ばれると schema context 自体が捨てられ、新しい context が作られるため、attributes_builder もそのタイミングで再構築されます。
つまり「スキーマが変わったら必ずビルダーも作り直される」ため、メモ化による不整合は発生しません。
さらに、レコード毎に持つ遅延属性セット(lazy attribute set)は:
- 共有されている「デフォルト属性ハッシュ」を読むだけ
- 実際にレコードに渡す前に各属性を
dupして複製
という構造になっているため、「ビルダーは共有しても、レコードごとの属性インスタンスはちゃんと別物になる」ようになっています。
そのため、1 つの attributes_builder を全レコードで安全に共有できます。
テストで確認していること
追加されたテストは、おおむね以下のシナリオを確認しています:
ビルダーの再利用が行われていること
- 同じ schema context 内で複数回レコードをインスタンス化しても、同じビルダーが使い回されること
reset_column_information後にビルダーが再構築されること- カラム情報をリセット → スキーマ再読み込み
- その後に使われるビルダーが新しいスキーマに基づくものであること
親モデルの primary key 変更が子モデルにも反映されること
- 親クラスの
primary_keyを変更 - 子クラス(継承したモデル)のスキーマ・ビルダーが再構築され
- 新しい primary key がビルダーのデフォルトにも正しく反映されること
- 親クラスの
コード差分としては:
activerecord/lib/active_record/model_schema/schema_context.rbAttributesにattributes_builder相当のメモ化フィールドやロジックを追加
activerecord/test/cases/primary_keys_test.rb- 上記のビルダー再利用/再構築に関するテストケースを追加
といった最小限の変更にとどまっています。
パフォーマンス効果
PR に記載されている計測結果(SQLite + 6 カラムのモデル):
| 処理内容 | 変更前 allocations | 変更後 allocations |
|---|---|---|
Post.instantiate(row) | 10 | 6 |
Post.all.to_a (100 行) | 1685 | 1285 |
さらに 30 万件インスタンス化のベンチ:
- 以前: 0.26s
- 以後: 0.14s
と約 2 倍近い速度改善が確認されています。
Rails の内部 API だけで実現しているため、アプリケーション側のコード変更なしに恩恵が受けられます。
- 影響範囲・注意点
影響範囲
- 主に Active Record モデルの大量インスタンス化パス に影響
Model.instantiate(row)Model.find_each,Model.all.to_a, pagination ライブラリなど大量ロードを行う箇所
- アプリコードから直接
attributes_builderや schema context に触れていない限り、API レベルの互換性は保たれます。
互換性・注意点
スキーマ変更関連の以下の API に依存してメモ化がリセットされます:
reset_column_informationignored_columns=primary_key=reload_schema_from_cache周辺の内部処理
これらを正規の方法で使っている分には問題ありませんが、
- Rails の内部構造を前提に「独自に schema context をキャッシュ・書き換えしている」ようなメタプログラミングをしている場合、
メモ化の追加により挙動が変わる可能性はあります(一般的なアプリではほぼ無関係)。
- Rails の内部構造を前提に「独自に schema context をキャッシュ・書き換えしている」ようなメタプログラミングをしている場合、
デフォルト属性を共有しつつ、実体は
dupしたものをレコードごとに渡しているので:- 「あるモデルインスタンスのデフォルト属性を in-place で書き換えて、別インスタンスに影響させる」といった非推奨な前提は通用しません(そもそも以前から保証されていません)。
- 参考情報 (あれば)
対応箇所:
activerecord/lib/active_record/model_schema/schema_context.rbAttributesにattributes_builderのメモ化追加
activerecord/test/cases/primary_keys_test.rb- ビルダー再利用・再構築を検証するテスト
関連する内部 API / 振る舞い:
ActiveRecord::Base.reset_column_informationActiveRecord::Base.primary_key=ActiveRecord::ModelSchema::ClassMethods#reload_schema_from_cacheignored_columns=および attribute registration まわり
この PR は挙動変更というより「内部最適化」ですが、CHANGELOG への記載対象となるレベルのパフォーマンス改善として扱われています。
#59096 Spare the Content-Type parsing on the response for headers it set itself
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Rails のActionDispatch::Responseにおいて、レスポンス自身が設定したContent-Typeヘッダを再パースしないようにメモ化を導入し、不要な正規表現マッチとオブジェクト生成を削減するパフォーマンス改善です。これにより、response.media_typeなどの呼び出しでのアロケーションが減り、1リクエストあたりのオブジェクト生成数がわずかに削減されます。
- 変更内容の詳細
背景問題
従来の実装では、次のような場面で毎回 parsed_content_type_header が呼ばれ、正規表現マッチ → MatchData → ContentTypeHeader 構造体の生成が行われていました。
- コンテンツネゴシエーション (
response.media_type参照) - charset の setter / getter
headでのレスポンス生成- レスポンスコミット時 など
しかし多くのケースでは、set_content_type で Rails 自身が Content-Type を組み立ててヘッダにセットした直後に、その値をまたパースし直しているという無駄がありました。
今回のアプローチ
ポイントは「ヘッダ文字列ごとにパース結果をメモ化し、Rails 自身がセットしたものについては最初からパース済み情報をキャッシュしておく」という方針です。
メモ化の仕組み
- 「生の
Content-Type文字列」 → 「パース済みオブジェクト (ContentTypeHeader相当)」のマッピングをレスポンス内に持つ。 set_content_typeが呼ばれたときに、- media type
- charset
- params など「自分が知っている構成要素」からヘッダ文字列を組み立てると同時に、その要素をパース結果としてメモに登録する。
擬似コードイメージ:
def set_content_type(media_type, charset: nil, params: nil)
value = build_content_type_string(media_type, charset, params)
headers[CONTENT_TYPE] = value
# パース済み情報をメモ化
@parsed_content_type_header_cache[value] = ContentTypeHeader.new(
mime_type: media_type,
charset: charset,
params: params,
)
end
def parsed_content_type_header
raw = headers[CONTENT_TYPE]
return NULL_CONTENT_TYPE_HEADER if raw.nil?
# 既に同じ文字列をパース済みならそれを再利用
@parsed_content_type_header_cache[raw] ||= parse_content_type(raw)
end実際にはもう少し条件分岐がありますが、概念としてはこのようなイメージです。
「外部から来たヘッダ」は従来どおりパース
以下のようなケースは、従来どおり正規表現ベースのパーサが走ります。
headers['Content-Type'] = "text/plain; charset=utf-8"のように生文字列を直接設定した場合response.content_type = "text/html; charset=UTF-8"のように文字列で渡された場合- パラメータ・ホワイトスペース・クォート・空の charset など、パーサが解釈し直す可能性があるケース
Rails 側が 「自分の内部表現としての media_type / charset / params」から組み立てていないヘッダは安全のため必ずパースします。
ヘッダ未設定時の挙動
Content-Typeヘッダが存在しない場合:- パース結果はアロケーションしない「共有の null オブジェクト」を返す
- 未設定状態自体はメモ化せず、「nil → null オブジェクト」のマッピングを共通で使う形
ヘッダ変更時の扱い
- メモ化は「生のヘッダ文字列(value)」単位。
- headers ハッシュの
Content-Type値が別の文字列に置き換えられた場合:==で生値を比較しているため、新しい値に対するパースが再度走る
- つまり、「同じ文字列ならキャッシュ」「値が変われば再パース」という自然な挙動になっています。
テスト内容
テストでは主に以下をカバーしています。
Content-Typeを読み出した後に削除し、再度設定したようなケースでも正しく動作するか。- メモ化が効いていて、同じヘッダ値に対してパースが再実行されないこと。
content_type=に渡した各種曖昧な入力 (パラメータ・クォート・空 charset など) が、- ヘッダ文字列としてパースした場合と
- writer (
content_type=) 経由で設定してから読み戻した場合 で同じ結果になること。
- 影響範囲・注意点
影響範囲
対象:
ActionDispatch::ResponseのContent-Type周り (media_type,charset, content negotiation,head, レンダリング等)典型的なコントローラ利用では、以下のようなオブジェクトアロケーション削減が報告されています(ミドルウェアなしの計測):
ケース 変更前 変更後 head :ok93 84 render plain:133 128 render json:100 95 render html:132 127 response.media_typeを 1 回読むだけのケースでのアロケーション:- 6 個 → 0 個
パフォーマンス/メモリアロケーションの「地味だが積み重なる」改善で、頻繁にレスポンスを生成するアプリケーションでは GC 負荷の軽減に寄与します。
注意点・互換性
- 挙動は「外部から渡された生のヘッダ文字列は必ずパースする」方針のため、従来と互換性を保っています。
headers['Content-Type']を直接いじるようなコードを持っていても、- 値が変われば再パースされる
- メモ化は「生文字列」キーなので、意図しない古いパース結果が使われることは基本的にありません。
Content-Type未設定時に null オブジェクトが使われる実装詳細がありますが、公的 API の挙動(戻り値がnil相当であることなど)は維持されるように設計されています。
アプリケーション側で特別な対応が必要になる種類の変更ではなく、「透明な最適化」に近いものです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59096
- 変更ファイル:
actionpack/lib/action_dispatch/http/response.rbactionpack/test/dispatch/response_test.rb
- 主なトピック:
Content-Typeヘッダ解析のメモ化- 自前でセットしたヘッダに対してパーサを回さない最適化
- 未設定ヘッダ用の共有 null オブジェクト
- オブジェクトアロケーション削減による軽量化
#59095 Allocate nothing in the notifications fanout for silenced subscribers
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
ActiveSupport::Notifications のFanout#groups_forが、サイレンス(silence)された購読者に対して不要な配列割り当てを行わないように最適化されました。これにより、特にsql.active_recordなどデバッグ用サブスクライバが常時サイレンスされている本番環境で、アロケーション削減とわずかな性能改善が見込めます。
- 変更内容の詳細
背景
ActiveSupport::Notifications::Fanout#groups_forは、build_handleに到達するすべてのイベント(= Active Record の全 SQL クエリとトランザクション)で呼ばれます。- 通知には「サイレンス可能 (silenceable)」なサブスクライバが含まれる場合があり、ログレベルや
silenceの状態に応じてイベントを受け取らないようにできます。 - 以前の実装では、「サイレンス可能なサブスクライバのグループ」を処理するたびに、
- サイレンスされている購読者を
rejectして新しい配列に詰め直す - たとえ全員サイレンスされていても、毎回配列を割り当てる
という動きをしていました。
- サイレンスされている購読者を
典型的なケースとして、sql.active_record の debug-level サブスクライバ(構造化ログ用の subscriber)は、本番のように logger が debug でないときは常にサイレンスされています。この状態だと、実質的に「誰も受け取らない通知」に対して、毎回配列を 1 つ余計に作っていました。
新しいアルゴリズム
今回の変更では、次のようにロジックが変わっています:
- 「有効な(サイレンスされていない)サブスクライバ一覧」を 一回のループで構築 する。
- 少なくとも 1 つでも有効なサブスクライバが見つかった場合にだけ、配列を割り当てる。
- すべてサイレンスされている場合は、配列を一切割り当てない(あるいは nil / 空扱いで終了)。
疑似コードイメージ:
# 旧: 各グループごとに reject して常に新しい配列を作る
silenceable_subscribers.each do |group|
active = group.reject { |subscriber| subscriber.silenced? } # 常に配列作る
# ...
end
# 新: 1パスで有効なものだけを集め、1つもなければ配列すら作らない
active = nil
silenceable_subscribers.each do |subscriber|
next if subscriber.silenced?
(active ||= []) << subscriber # 最初に見つかったときだけ配列を確保
end
if active
# active なサブスクライバがいる場合のみ処理
else
# 誰もリッスンしていない → 何もしない/nil 相当
end※実際のコードは Fanout#groups_for の内部に統合されており、複数グループ・複数種の subscriber を扱いますが、本質的には「必要になるまで配列を作らない」という変更です。
テスト追加
activesupport/test/notifications_test.rb に以下のような観点のテストが追加されています:
- サイレンスされた subscriber は通知を受け取らない。
- 同じイベントに他の有効な subscriber がいる場合、その subscriber は従来どおり通知を受け取る。
- サイレンス状態の判定はイベントごとに再評価される(途中でサイレンス状態が変わっても正しく反映される)。
- 「該当する subscriber が全員サイレンスされているイベント」の場合、
nobody listeningな状況が正しく表現される。
これにより、Fanout の silenceable subscriber の振る舞いがテストで直接カバーされるようになりました(以前はここがテスト未カバーだった)。
ベンチマーク結果
build_handle / start / finish の 1 サイクルに対する計測:
- デバッグレベル subscriber がサイレンスされた状態:
- アロケーション数: 3 → 2 / cycle
- 1,000,000 cycles: 0.79s → 0.73s
- 同一イベントに「通常のブロック subscriber」を 1 つ追加した状態:
- アロケーション数: 8 → 7 / cycle
効果は「小さいが、ホットパスで常に発生する」タイプの最適化です。
- 影響範囲・注意点
- 対象:
ActiveSupport::Notificationsの内部実装 (Fanout) と、その上で動く Active Record の計測 (sql.active_recordなど)。- 特に本番環境など、debug-level の subscriber が恒常的にサイレンスされているケースで恩恵があります。
- 公開 API の挙動:
- 「サイレンスされた subscriber は通知を受け取らない」「他の subscriber は通常通り動く」「subscriber が全員サイレンスなら誰も受け取らない」という意味的な挙動は変わっていません。
- そのため、通常の
ActiveSupport::Notifications.subscribe/instrumentを使っているアプリ側コードの修正は不要です。
- 注意点:
- 挙動変更はあくまで内部実装&パフォーマンス改善であり、外部から依存されるべきではない内部構造(配列の存在など)に依存しているような非公式なメタプログラミングがあれば、影響を受ける可能性はあります(通常のアプリケーションでは問題にならない想定)。
- サイレンス機構(
ActiveSupport::LogSubscriber/ActiveSupport::Notifications周り)を拡張している場合は、各 subscriber が「イベントごとにサイレンス判定される」前提であることを再確認しておくと安全です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59095
- 関連コンセプト:
ActiveSupport::NotificationsActiveSupport::LogSubscribersql.active_recordイベントと構造化ログ subscriber
- この種の micro-optimization は、「計測・ログを大量に発行するが、その多くはログレベルなどで実際には捨てられる」というワークロードで特に効いてきます。Rails の内部でもこのパターンが多いため、同様の方針(必要になるまで配列などを割り当てない)を自前の計測コードに適用する際の参考になります。
#59093 Fix the link_to nil name documentation example
マージ日: 2026/10/15 | 作成者: @55728
概要 (1-2文で)
link_toヘルパで name にnilを渡したときの挙動について、ドキュメント内の出力例が実際の動作と食い違っていたため、正しい出力例に修正した PR です。コードの挙動自体は変わっておらず、ドキュメントのみの修正です。変更内容の詳細
対象箇所: actionview/lib/action_view/helpers/navigation_helper.rb の link_to のドキュメントコメント
元のドキュメント例では、次のように書かれていました:
link_to nil, "http://example.com"
# => <a href="http://www.example.com">http://www.example.com</a>しかし実際の link_to の挙動は、引数で渡した URL をそのまま href とリンクテキストの両方に使うため、正しい出力は:
link_to nil, "http://example.com"
# => <a href="http://example.com">http://example.com</a>となります。
この PR は、ドキュメント中の例を上記の正しい出力に差し替えただけです(www. がついている点も含めて修正)。
挙動が正しいことは、既存テスト test_link_tag_with_query_and_no_name(actionview/test/template/navigation_helper_test.rb)でも確認済みであり、実際に ActionView::Base.empty で実行した結果とも一致しています。
- 影響範囲・注意点
- ランタイムのコード変更は一切なく、
link_toの動作は従来どおりです。 - 影響があるのはドキュメントのみであり、既存アプリケーションの挙動に変更はありません。
- これまでドキュメントを見て「
nilを渡すと URL が正規化されてwww.example.comのようになる」と誤解していた場合、その理解を改める必要があります。実際には「渡した URL 文字列がそのままhrefとリンクテキストに使われる」仕様です。
- 参考情報 (あれば)
- PRで言及されている既存テスト:
actionview/test/template/navigation_helper_test.rbのtest_link_tag_with_query_and_no_name
→ クエリ付き URL でも、渡した URL と完全一致する文字列がhrefとリンクテキストに使われることを確認するテストです。 - 元の例が
www.付きだったのは、古いコミット46a4ac8a36での追加時からの記述ミスであり、今回の PR はその歴史的な誤りを訂正するものです。
#59094 Spare the thunk in before callbacks for known terminators
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Rails の before コールバック実行時に毎回生成されていたlambda(Thunk)を、特定の「終端処理オブジェクト(terminator)」については省略できるようにし、オブジェクト割り当てを大幅に削減する最適化です。run_callbacksの内部プロトコルを拡張しつつ、既存のカスタム terminator との互換性は維持されています。
- 変更内容の詳細
背景・問題点
従来の ActiveSupport::Callbacks では、before コールバックを実行するたびに、以下のような流れになっていました:
- コールバック本体を「あとで実行するための
lambda」(= thunk) にラップ - terminator(コールバックチェーンの終端処理)に対し、
terminator.call(target, thunk)のような形で渡す- デフォルト terminator は
catch(:abort)で囲む ActionPackのprocess_actionterminator はcontroller.performed?をチェックする
- デフォルト terminator は
この構造のせいで、
- before コールバック 1 個につき最低 2 オブジェクト(
Proc+ 環境 or 関連オブジェクト)が毎回割り当てられる - リクエストあたり複数の callback chain(controller、model、job など)を通るため、GC 負荷・オーバーヘッドが無視できない
というパフォーマンス上の問題がありました。
新しい terminator インターフェース call_direct
この PR では、「terminator がコールバック本体をそのまま受け取り、自分で好きに呼び出す」ための新しいプロトコルを追加しています。
- terminator が以下のシグネチャを実装できるようになりました:
def call_direct(target, callback, value)
# target: コールバック対象オブジェクト(例: controller, model など)
# callback: 実行すべきコールバック本体(Proc や callable)
# value: コールバックチェーンで伝搬される値
endFilters::Before(before コールバックのラッパ)は、フィルタ生成時に一度だけ terminator がcall_directを実装しているかどうかを判定し、その結果を ivar に保持- 実行時は、その ivar を見るだけで
call_direct経由で 直接 callback を渡す最適化パス- 従来通り thunk を作って
terminator.call(target, thunk)する 互換パス を切り替える
これにより、「call_direct を持つ terminator」については thunk を一切生成せずに済みます。
フレームワーク側 terminator の対応
この PR では、Rails 本体が用いている主な terminator を call_direct 対応させています。
デフォルト terminator (
ActiveSupport::Callbacks標準)- これまで
lambda { |target, block| catch(:abort) { block.call } }のような形だったものを callメソッドを持つ 凍結オブジェクト に変更し、- さらに
call_direct(target, callback, value)も実装
イメージとしては以下のような動きになります(実コードは若干異なる可能性がありますが概念的にはこういうことをしています):
rubyclass DefaultTerminator def call(target, thunk) # 既存互換インターフェース catch(:abort) { thunk.call } end def call_direct(target, callback, value) catch(:abort) { callback.call(target, value) } end end DEFAULT_TERMINATOR = DefaultTerminator.new.freeze- これまで
Action Pack の
process_actionterminator- これも lambda から「
callを持つオブジェクト +call_direct」に変更 controller.performed?のチェックをcall_directでも行う
- これも lambda から「
Action Mailbox の
processterminator- 同様にオブジェクト化され、
call_directを実装
- 同様にオブジェクト化され、
これらはいずれも「既存の call(target, thunk) も引き続きサポート」しており、過去バージョンと同じ呼び出し方も可能です。
カスタム terminator への影響
- 「
targetとthunkの 2 引数を取る lambda/Proc」を terminator として使っていたアプリケーション側コードは、そのまま動作します。 call_directは 任意実装 の新しいプロトコルであり、実装しない場合は従来の thunk ベースのコードパスにフォールバックします。- 高度なチューニングをしたいライブラリ作者などは、必要に応じて自作 terminator に
call_directを実装することで、同様の割り当て削減効果を得られます。
テスト追加
- terminator オブジェクトが
call_directを実装しているケースをテスト- コールバック本体が terminator に直接渡されること
- terminator 側でチェーンを停止したときに「ブロックが実行されず」
halted_callback_hookが呼ばれること を確認しています。
パフォーマンス計測結果
PR 説明中の数値より:
beforeコールバック 5 個 + デフォルト terminator のチェーンでrun_callbacksを実行した場合変更前 変更後 1 回あたりの割り当て 11 個 1 個 2,000,000 回実行 3.68s 2.18s before_actionを 4 つ持つ controller リクエスト 1 回で「オブジェクト 10 個分の割り当て」が削減ruby-bench の railsbench では、1 反復あたりのオブジェクト割り当て数が
1,405,387 -> 1,361,447 (約 -3.1%)
- 影響範囲・注意点
- アプリケーションコード側の変更は基本的に不要です。
- 通常の
before_actionや model/job callbacks などは、そのままで内部的に恩恵を受けます。
- 通常の
- ただし、以下に該当する場合は注意が必要です:
- terminator オブジェクトを自作している場合
- 2 引数 (
target, thunk) のcallもしくは Proc/lambda を terminator として渡しているなら、挙動は維持されます。 - さらに割り当て削減を狙いたい場合は、
call_direct(target, callback, value)を自作 terminator に追加することを検討できます。
- 2 引数 (
ActiveSupport::Callbacksの内部構造に依存したメタプログラミングをしている場合- terminator が lambda から「
callを持つオブジェクト」に変わっている箇所(Action Pack/Action Mailbox など)があるため、terminator.lambda?のような「lambda 前提」のチェックをしていると壊れる可能性があります。 - 正しくは「
respond_to?(:call)/respond_to?(:call_direct)」などプロトコルベースのチェックに寄せるべきです。
- terminator が lambda から「
- terminator オブジェクトを自作している場合
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59094
- 対象箇所:
activesupport/lib/active_support/callbacks.rbactionpack/lib/abstract_controller/callbacks.rbactionmailbox/lib/action_mailbox/callbacks.rb
- 関連概念キーワード:
- callback chain, before callbacks
- terminator / thunk / lambda allocation
run_callbacks,halted_callback_hook,catch(:abort)
#59091 Allocation-free reads through OrderedOptions#method_missing
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
ActiveSupport::OrderedOptions/InheritableOptions(主にconfig.xで使われる)の動的読み取りが、オブジェクトを追加生成せずに済むよう最適化されたPRです。method_missingのシグネチャ変更とキーの扱いの見直しにより、読み取りが高速化・省メモリ化されます。
- 変更内容の詳細
背景
config.x は内部的に ActiveSupport::OrderedOptions / InheritableOptions を使っており、
Rails.application.config.x.foo # => method_missing 経由でアクセス
Rails.application.config.x.bar! # => bang read
Rails.application.config.x.baz = 1 # => writeのようなアクセスはすべて method_missing(method, *args) を通っています。
従来実装の問題点:
*argsのスプラットで「毎回配列が1つ」確実に生成されるmethod(シンボル)→to_sで名前文字列化 → そこから再度シンボル化して lookup、という無駄な変換がある
結果として、単なる読み取りでも不要なオブジェクト生成と変換が発生していました。
今回の主な変更
method_missingのシグネチャ変更
旧:rubydef method_missing(method, *args) # ... end新:
rubydef method_missing(method, arg = UNDEFINED) # ※ 実際の定数名や実装詳細は概略ですが、イメージとして end- 可変長引数 (
*args) をやめて「引数1つ(省略可)」にしたことで、呼び出しごとの配列生成がなくなる。 read(options.foo)のような引数なし呼び出しでは「一切の追加オブジェクトを生成しない」形に。
- 可変長引数 (
キーの lookup 方法の変更
- 動的リーダー(
options.foo)では、methodシンボルをそのまま lookup キーとして使うようにし、to_s→to_symの往復をやめている。 bang read(options.foo!)やwrite(options.foo = 1)では、キー文字列は従来通り保持するが、こちらも*argsをやめて、不要な配列生成をなくしている。
ざっくりしたイメージ:
ruby# 読み取り def method_missing(name, arg = UNDEFINED) if arg == UNDEFINED # 引数なし => read # 以前: key = name.to_s; self[key.to_sym] # 今回: 変換せず symbol/文字列どちらかに合わせて直接 lookup else # 書き込みや bang read 用の分岐 end end- 動的リーダー(
故意に変えた振る舞い(互換性のない部分)
以前は:
rubyoptions.send(:foo, 1, 2) # *args が余分な引数を「黙って飲み込む」といった呼び出しでもエラーにならず、「余計な引数があっても無視」されていました。
今回からは:
rubyoptions.send(:foo, 1, 2) # => ArgumentError を投げると、余計な位置引数があると
ArgumentErrorを明示的に起こします。
ベンチマーク結果
1回の呼び出しあたり:
| 呼び出し種別 | 変更前 | 変更後 |
|---|---|---|
| read, key あり | 1 object, 192 ns | 0 objects, 142 ns |
| read, key なし | 1 object, 200 ns | 0 objects, 149 ns |
InheritableOptions 親経由 read | 1 object, 242 ns | 0 objects, 197 ns |
bang read (foo!) | 2 objects, 262 ns | 1 object, 245 ns |
write (foo =) | 2 objects, 232 ns | 1 object, 221 ns |
特に「読み取り」が 0 allocation になっているのがポイントです。config.x はリクエストごとに何度も読まれうるので、トータルのGC負荷軽減に効きます。
- 影響範囲・注意点
影響範囲
- Rails 全体およびアプリケーションコードで利用される
config.x系の設定オブジェクト- 例:
Rails.application.config.x.feature_flags.some_flag ActiveSupport::OrderedOptionsを直接使っているコードActiveSupport::InheritableOptionsを使う箇所
- 例:
通常の利用パターン:
config.x.foo = 1
config.x.foo
config.x.foo!などは、挙動は変わらずパフォーマンスだけが良くなります。
注意点・互換性
余分な引数を渡しているケースがあれば壊れる
例:
rubyconfig.x.foo(1) # 本来は foo はメソッドではなく設定キー options.send(:bar, 1, 2)以前: 何も言わずに無視されていた
今回:ArgumentErrorが発生そのようなコードはそもそもバグの可能性が高く、「早期に壊れて知らせてくれる」変更とも言えますが、挙動変更ではあるので注意が必要です。
method_missing の挙動に依存したメタプログラミング
独自に
OrderedOptions/InheritableOptionsを継承し、method_missingのシグネチャや挙動に強く依存したコードを書いている場合、微妙な影響を受ける可能性があります。
とはいえ、このPRはmethod_missing(method, arg = nil)程度の素直な形なので、一般的なオーバーライドにはほぼ影響はないと考えられます。
- 参考情報 (あれば)
- 対象クラス:
ActiveSupport::OrderedOptionsActiveSupport::InheritableOptions
- 代表的な利用箇所:
Rails.application.config.xconfig.xを介した feature flag / カスタム設定
- このPRの主な目的:
- 「config設定の読み取りを allocation-free にして、GC負荷とレイテンシを削る」マイクロ最適化
- 「不正な呼び出し(余分な引数付き)の早期検知」も副次的に達成
#59090 Read the controller logger from the config hash directly
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Rails のAbstractController::Loggerが、これまでconfig.logger(method_missingベースの動的呼び出し)経由で読んでいたロガーを、config[:logger]を直接読む明示的アクセサに変更し、コントローラ/メイラー/ビューでのロガーアクセスを高速化する PR です。
動作仕様(config.loggerとloggerの互換性や継承挙動)は維持したまま、オブジェクト生成と動的ディスパッチのコストを削減しています。
- 変更内容の詳細
これまでの実装の問題点
AbstractController::Loggerはlogger/logger=をconfigにデリゲートしていた:ruby# イメージ delegate :logger, :logger=, to: :configconfigはActiveSupport::OrderedOptionsベースで、config.logger呼び出しはmethod_missingによって解決される。- つまり、
loggerを読むたびに:- splat 配列の生成
method_missingによる動的ディスパッチ が走る。
- つまり、
- コントローラ、メイラー、ビュー(ビューの
loggerはコントローラのloggerを使う)でログ出力を多用するアプリでは、このオーバーヘッドを毎回支払うことになる。
今回の変更点
アクセサを明示的に定義
AbstractController::Logger 内で、logger / logger= を config[:logger] に対する明示的アクセサとして定義し直しています。
イメージとしては以下のような形です(実際のコードは若干異なる可能性がありますが、本質は同じです):
module AbstractController
module Logger
# クラスレベルの logger アクセサ
def logger
config[:logger]
end
def logger=(logger)
config[:logger] = logger
end
# インスタンスレベルの logger アクセサ
def logger
self.class.logger
end
def logger=(logger)
@_logger = logger
end
end
endポイント:
- 読み取りは
config.loggerではなくconfig[:logger]を直接参照:OrderedOptions#[]なのでmethod_missingを経由しない(動的ディスパッチの削減)。
- 動作仕様は維持:
- ロガーの保存場所は引き続き「コントローラの config オブジェクト」。
- そのため:
config.loggerとloggerは引き続き同じ値を指す。- サブクラスは親クラスの logger 設定を継承する。
テストの追加
actionpack/test/abstract/logger_test.rb にテストが追加され、次の点を保証しています:
config.loggerとloggerが互いに読み書き可能であること(インターチェンジ可能)。- 例:
controller.config.logger = loggerした後にcontroller.loggerで同じインスタンスが得られる。controller.logger = loggerした後にcontroller.config.loggerでも同じインスタンスが得られる。
- 例:
- サブクラスでの継承挙動:
- 親コントローラに設定した logger がサブクラスに引き継がれる。
- インスタンスレベルでのオーバーライドが機能すること:
- クラスレベルの logger と、特定インスタンスで差し替えた logger を区別できること。
パフォーマンス改善の具体的な数値
PR で示されている計測結果(1回あたり):
- コントローラインスタンスの
logger呼び出し:- 変更前: 1 オブジェクト生成, 250 ns
- 変更後: 0 オブジェクト生成, 94 ns
- コントローラクラスの
logger呼び出し:- 変更前: 1 オブジェクト生成, 211 ns
- 変更後: 0 オブジェクト生成, 52 ns
多数のログ呼び出しがあるアクションやテンプレートでは、合算すると無視できない差になります。
- 影響範囲・注意点
影響範囲
- 影響を受けるコンポーネント:
AbstractController::BaseをベースにしたコントローラActionMailer::Base(内部でAbstractControllerを利用)- ビュー内の
logger呼び出し(コントローラの logger へフォワード)
- 外部 API としての挙動は原則変更なし:
- 既存コード:
logger(インスタンスメソッド)self.class.loggerconfig.loggerは同じように利用可能。
- 既存コード:
実装依存のコードを持っている場合の注意点
通常のアプリでは破壊的変更はほぼありませんが、以下のようなケースがあると注意が必要です:
config(OrderedOptions)のmethod_missingをモンキーパッチして、logger呼び出し時の挙動をいじっている場合:- 今回の変更で
logger読み取りはconfig.loggerではなくconfig[:logger]に変わったため、そのカスタマイズは通らなくなります。
- 今回の変更で
loggerアクセサ自体をModule#prepend等で差し替えている場合:- 新しい明示的メソッド定義と干渉する可能性があります(これはどのみち fragile な拡張ですが)。
通常のアプリケーションレベルのコード(単に logger.info, config.logger = my_logger など)についてはそのまま動き、かつ速くなります。
- 参考情報 (あれば)
- PR 番号: #59090
- タイトル: Read the controller logger from the config hash directly
- 作成者: amatsuda
- マージ日時: 2026-10-15T08:24:17Z
- 変更ファイル:
actionpack/lib/abstract_controller/logger.rbactionpack/test/abstract/logger_test.rb
この変更は「ログアクセスのホットパスから OrderedOptions#method_missing を抜く」ことにフォーカスした、局所的なパフォーマンスチューニングであり、API 表面は保ちつつ内部実装をより直接的・低コストにしたものと言えます。
#59089 Memoize controller url_options with script_name across calls
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
ActionController::Metal::UrlFor#url_optionsが、script_nameを含んだ形でもメモ化されるようになり、同一リクエスト内での繰り返し呼び出し時のオブジェクト生成・処理コストが削減されました。あわせて、テスト環境でのコントローラ再利用時やscript_name変更時のキャッシュ無効化も適切に行われるようになっています。
- 変更内容の詳細
2-1. これまでの問題点
ActionController::Metal::UrlFor#url_options はベースとなるオプションハッシュ(ホスト名やプロトコルなど)をメモ化していましたが、実運用でよくあるケースである「そのコントローラ自身のルーティングからディスパッチされたリクエスト」の場合、毎回以下のような処理をしていました。
- メモ化済みの
@_url_optionsに対して script_name:を追加した新しいハッシュ{ **@_url_options, script_name: }を毎回生成- 生成したハッシュを毎回 freeze
そのため、一覧ページで 100 件分の URL を生成する場合、script_name が同じであっても、内部的に同じ内容のハッシュオブジェクトが 100 個作られていました。
2-2. 変更点の概要
今回の PR では:
- 「
script_nameを含んだurl_optionsのハッシュ」を、ベースの@_url_optionsとは別にメモ化するインスタンス変数を追加 - ただし、同一リクエスト内で
script_nameが変わるケース(マウントされたエンジンなどでの再ディスパッチ)に対応するため、- 毎回「現在の
script_name」を確認し、 - 直前にメモしたものと
script_nameが変わっていたら、新しいハッシュを生成して再メモ化
- 毎回「現在の
- 新しく追加したインスタンス変数を
PROTECTED_IVARSに登録し、assignsなどビューに露出する変数一覧から除外 recycle!実行時(functional test でコントローラインスタンスを再利用するケース)に、この新しいメモもクリアし、前のリクエストのhostや recall パラメータを使い回さないようにする- 上記挙動を担保するテストを追加(同一オブジェクト性・
script_name変更時の無効化・recycle 時のクリア)
2-3. もう少し踏み込んだ動きのイメージ
※擬似コードレベルのイメージです(実装そのものではありません):
def url_options
# 既存のベースオプション (host など) のメモ化は従来同様
base = (@_url_options ||= build_base_url_options)
current_script_name = request.script_name
if @_url_options_with_script_name &&
@_url_options_with_script_name_script_name == current_script_name
# script_name が同じなら、前回作ったハッシュをそのまま返す
return @_url_options_with_script_name
end
# script_name が変わっていたら、新しいハッシュを生成してメモ化
@_url_options_with_script_name =
if current_script_name.present?
base.merge(script_name: current_script_name).freeze
else
base # 空の場合は base そのもの(など、実装に応じた最適化)
end
@_url_options_with_script_name_script_name = current_script_name
@_url_options_with_script_name
end
def recycle!
super
# 既存: @_url_options のクリア
remove_instance_variable(:@_url_options) if defined?(@_url_options)
# 今回追加: script_name 付きのメモもクリア
remove_instance_variable(:@_url_options_with_script_name) if defined?(@_url_options_with_script_name)
remove_instance_variable(:@_url_options_with_script_name_script_name) if defined?(@_url_options_with_script_name_script_name)
end2-4. ベンチマーク結果
PR の説明によれば、1 回の呼び出しあたりの挙動は以下のように改善しています(ns = ナノ秒、オブジェクト数は GC 対象オブジェクトと思ってよいでしょう)。
url_options, 空のscript_name- 変更前: 1 オブジェクト, 123 ns
- 変更後: 0 オブジェクト, 88 ns
url_options,script_nameが/sub- 変更前: 2 オブジェクト, 159 ns
- 変更後: 0 オブジェクト, 87 ns
post_path(1)- 変更前: 12 オブジェクト, 1110 ns
- 変更後: 11 オブジェクト, 1068 ns
url_for(controller:, only_path:)- 変更前: 49 オブジェクト, 6910 ns
- 変更後: 48 オブジェクト, 6476 ns
url_options 自体のコストが下がることで、最終的な URL 生成 (*_path, *_url, url_for) のわずかなオブジェクト削減とレイテンシ短縮につながっています。
- 影響範囲・注意点
影響を受ける範囲
ActionController::Metal::UrlFor#url_optionsを通じて URL を組み立てるあらゆる場所 (*_path,*_url,url_for,polymorphic_urlなど) が間接的に影響を受けます。- 特に
script_nameを使う構成(サブディレクトリ配備、エンジンのマウント、Rack ミドルウェアやリバースプロキシでSCRIPT_NAMEをいじる構成)で恩恵が大きいです。
互換性・挙動の変化
- 外部から見える
url_optionsの内容は従来と論理的に同じであり、インターフェースの互換性は保たれています。 - 新しいインスタンス変数は
PROTECTED_IVARSに入っているため、assigns、view_assignsなどには露出しません。
→ これらに依存していたコード(普通はまず無い)があると、挙動が変わる可能性はありますが、通常のアプリケーションでは問題にならない想定です。 recycle!の挙動は「前リクエストの情報を持ち越さない」方向の修正であり、テスト環境でより正しい挙動になっています。
→ もし、recycle!を独自にオーバーライドしていて、@_url_options等の内部状態に依存した特殊なテストヘルパーを書いている場合は、一応確認したほうが安全です。
- 外部から見える
パフォーマンス面
- 多数の URL をレンダリングするビュー(長い index ページ、API レスポンスなど)で GC プレッシャーが若干軽減されます。
- 単一リクエストで URL を数回しか生成しないようなケースでは、体感差はほぼないはずです。
- 参考情報 (あれば)
- 対象クラス / モジュール
ActionController::Metal::UrlFor#url_optionsActionController::Base(PROTECTED_IVARSへの追加)ActionController::Metal::Testing#recycle!(テスト環境でのメモリクリア)
- 追加テスト
url_optionsの戻り値が、同じscript_nameであれば同一オブジェクトであること(メモ化の確認)script_nameが同一リクエスト中に変化した場合に、新しいオブジェクトに差し替えられることrecycle!後に前リクエストのurl_options情報が残らないこと
#59087 Fast path for single primary keys in to_param
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
ActiveRecord::Integration#to_paramが、単一の主キー(integer / string)の場合に高速に処理されるよう最適化されました。これにより、通常の単一カラム主キーのモデルで、URLヘルパー使用時のオブジェクト生成数と処理時間が削減されています。
- 変更内容の詳細
これまでの to_param の挙動
ActiveRecord::Integration#to_param は、主キーが単一であっても複合であっても常に以下のような処理を行っていました:
def to_param
Array(id).join(self.class.primary_key_separator)
endArray(id)によってidを一度配列に包むjoin(delimiter)で区切り文字(複合主キー用)を使って文字列化- 単一主キーでも毎回
- 配列オブジェクト 1つ
- 結果文字列 1つ など、余計なオブジェクト生成と処理をしていた
複合主キーをサポートするための実装ですが、一般的な「単一主キーのレコード」に対しても常にこのコストを払っていました。
今回の変更点
ロジックの最適化
to_param 内で「単一主キー」と「複合主キー」を分岐し、単一主キーの場合は高速パスで処理するように変更されています。
イメージとしては次のような実装になります(擬似コード):
def to_param
value = id
case value
when Integer
# 単一整数キーはそのまま to_s
value.to_s
when String
# 単一文字列キーは dup して返す(元の属性文字列を直接返さない)
value.dup
else
# 配列(複合キー)などは従来どおり join を使う
Array(value).join(self.class.primary_key_separator)
end
end主なポイント:
- 単一 Integer 主キー
- これまで:
Array(123).join("-")という遠回り - これから:
123.to_sのみ
- これまで:
- 単一 String 主キー
- これまで:
Array("abc").join("-")により新しい文字列が作られる - これから:
id.dupでコピーした文字列を返す- 呼び出し側がレコードの内部文字列(属性値オブジェクト)そのものを直接保持しない保証を維持
- これまで:
- 複合主キー
- 従来通り
Array(id).join(delimiter)を継続利用 - ただし
idを一度ローカル変数に読み出してから使用するため、idの呼び出しは 1回に減少
- 従来通り
テストの追加
- 文字列主キーのケースに対してテストが追加
to_paramの値が期待通りであること- かつ、戻り値が
idと 別オブジェクト(コピー) であることを検証
- 既存テストで
- 整数主キー
- 複合主キー
- 区切り文字のカスタマイズ(
primary_key_separator) についてはカバー済み
パフォーマンス改善(PR記載の計測結果)
1回の呼び出しあたりのオブジェクト数と時間:
| Primary key | 変更前 | 変更後 |
|---|---|---|
| integer | 3 objects, 506 ns | 1 object, 168 ns |
| string | 2 objects, 246 ns | 1 object, 133 ns |
| composite | 5 objects, 607 ns | 4 objects, 497 ns |
特に、一般的な単一整数主キーで
- オブジェクト数: 3 → 1
- 時間: 約 1/3
となっており、URLヘルパーが大量に呼ばれるアプリケーションでは GC 負荷やレスポンス時間の軽減が期待できます。
- 影響範囲・注意点
影響範囲
- 対象:
ActiveRecord::Base#to_paramを利用する全てのモデル- Rails の URL ヘルパー(
user_path(@user)など)は内部でto_paramを使うため、通常の全アプリで影響あり
- Rails の URL ヘルパー(
- 主に挙動が変わるケース:
- 単一の整数主キー
- 単一の文字列主キー
- 挙動は従来と互換:
- 表面的な
to_paramの返却値は従来と同じ(文字列) - 複合主キーも同じフォーマット(
join(primary_key_separator))で出力される
- 表面的な
注意点・互換性
to_paramの戻り値オブジェクトに依存しているコードがある場合- 文字列主キーの場合、もともと「毎回新しい文字列」が返ってきていたため、今回も
dupによりその性質は維持 - 複合主キーでも
joinにより新しい文字列が作られる点は変わらない object.to_param.equal?(object.id)のような「同一オブジェクトか」を見ている奇妙なコードがもしあれば、そもそも Rails の想定外であり、今回の変更以前から依存すべきでない
- 文字列主キーの場合、もともと「毎回新しい文字列」が返ってきていたため、今回も
idが Integer / String / Array 以外の特殊な型の場合- 従来同様
Array(id).joinの経路に乗る - カスタムプライマリキー型を使っている場合でも、互換性は基本的に保たれる
- 従来同様
複合主キーサポートとの関係
- Rails 本体は公式には複合主キーをフルサポートしていませんが、
to_paramでは暗黙に対応している部分があります - この PR はその対応を壊さず、単一キーのみを高速化しています
- Rails 本体は公式には複合主キーをフルサポートしていませんが、
- 参考情報 (あれば)
- 対象メソッド:
activerecord/lib/active_record/integration.rbのto_param - 追加テスト:
activerecord/test/cases/integration_test.rb - この変更はパフォーマンス改善のみで、外部 API(メソッドシグネチャや戻り値の型)には変更なし
- URL 生成が大量に発生する大規模アプリや API サーバーでは、特に GC の削減効果が見込めます。
#59088 Build ActionDispatch::Request's inner Rack::Request lazily
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
ActionDispatch::Requestが内部で保持しているRack::Requestオブジェクトの生成を「遅延生成(lazy)」に変更し、実際に必要になったときにだけ作るようにしたパフォーマンス改善のPRです。これにより、Rails標準スタックで1リクエストあたり複数回発生していた不要なRack::Requestの生成が削減されます。
- 変更内容の詳細
何をしているか
- これまでは
ActionDispatch::Request.new(env)を呼ぶたびに、内部で必ずRack::Request.new(env)も一緒に生成していました(eager creation)。 - 本PRでは、
Rack::Requestの生成を「最初に必要になったタイミング」で行うように変更しています(lazy creation)。 - 外部APIとしての
rack_requestメソッドは引き続き公開のまま・戻り値も同じ型ですが、その中で初回アクセス時にだけRack::Requestを生成してキャッシュする形になったと考えられます。
疑似コードイメージ(実際の実装イメージレベル):
# 変更前(イメージ)
class ActionDispatch::Request
def initialize(env)
@env = env
@rack_request = Rack::Request.new(env) # 常に生成
end
attr_reader :rack_request
end
# 変更後(イメージ)
class ActionDispatch::Request
def initialize(env)
@env = env
@rack_request = nil
end
def rack_request
@rack_request ||= Rack::Request.new(@env) # 初回アクセス時に生成
end
endなぜ必要か / 背景
- 多くの Rack ミドルウェアや Rails のコンポーネントが、
envを自分用にラップするためにActionDispatch::Requestを生成しています。- 例:
ActionDispatch::RequestIdActionDispatch::RemoteIp- Rails ロガー関連ミドルウェア
ActionDispatch::ExecutorCookies関連- セッションストア
- Content Security Policy / Permissions Policy ミドルウェア
Engine#build_request- コントローラディスパッチャ
- 例:
- これらは全て
ActionDispatch::Requestを new するたびに「セットでRack::Requestも new」していましたが、- 実際に
Rack::Requestが使われるのは「クエリパラメータ」や「POST パラメータ」をパースするときだけです。 - したがって、多くのケースでは作られた
Rack::Requestが一度も参照されないまま GC 対象になっていました。
- 実際に
どこで使われているか
ActionDispatch::Request内部でrack_requestを呼び出している箇所は3箇所のみで、いずれも- クエリストリングのパース
- POST パラメータのパース など、パラメータ解析経路に限定されています。
- そのため、「パラメータをパースしない」コードパスでは
Rack::Requestを生成しないようになります。
パフォーマンスへの効果
1回の
ActionDispatch::Request.new(env)あたり:変更前 変更後 生成オブジェクト数 2 objects 1 object 所要時間 203 ns 167 ns パラメータパース経路(
Rack::Requestを実際に使う場合)は、アロケーション数・時間とも従来と変わらないとされています。Rails のデフォルトのミドルウェアスタックでは、1リクエストあたり約9回
ActionDispatch::Requestが作られると想定されており、- そのうち1回分は最終的にパラメータパースで
Rack::Requestを使うが - 残り8回分は
Rack::Requestを作る必要がなくなる
という前提で「約8オブジェクト/リクエストの削減」と見積もられています。
- そのうち1回分は最終的にパラメータパースで
- 影響範囲・注意点
影響範囲
- 主に影響するのは
ActionDispatch::Requestを直接扱うコードです。 - とはいえ、公開APIである
rack_requestメソッドのインターフェースは変わっていないため、request.rack_requestを呼び出しているアプリケーションコードやライブラリコードは、そのまま動作します。
- 変更の主な影響は「内部実装のオブジェクト生成タイミング」であり、機能面・挙動面の変更は基本的にありません。
挙動上の注意点 / 互換性
rack_requestが「初回アクセス時に副作用としてRack::Requestを生成する」という意味で、“完全に副作用ゼロなアクセサ” ではなくなりますが、もともと eager に生成していたものなので、論理的な意味では同等です。ActionDispatch::Requestのサブクラスやモンキーパッチで、initialize内で@rack_requestに依存した処理をしていた@rack_requestを直接参照していた
などの非公開実装依存があると、動作が変わる可能性があります。- ただし通常のアプリケーションでは、そのような内部変数への依存はほぼないはずです。
テスト・ドキュメント
- PR のチェックリスト上では:
- テストの追加・更新: 未チェック([ ])
- CHANGELOG 更新: 未チェック([ ])
- とはいえ、core周辺の変更なので既存テストスイートにより回帰検出される前提と思われます。
- 外部向けの目に見える機能変更ではないため、CHANGELOG を省略している可能性があります。
- 参考情報 (あれば)
- PR本体: https://github.com/rails/rails/pull/59088
- 関連クラス:
ActionDispatch::Request(actionpack/lib/action_dispatch/http/request.rb)Rack::Request(Rack 本体)
- この変更は、ミドルウェア層での細かなオブジェクト削減によるパフォーマンス最適化の一環であり、特に高トラフィック環境でのGCプレッシャー軽減に寄与します。
#59054 Short-circuit route matching for static paths
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Rails のルーティング(Journey)に「完全一致テーブル」が導入され、GET /postsのような静的ルートについては正規表現マッチやパラメータ抽出をスキップして、HTTPメソッド+パスだけで即座に対応ルートを特定できるようになりました。これにより、典型的なアプリが多く持つ静的ルートへのリクエスト性能が大幅に向上しつつ、マッチングの挙動自体は従来どおり維持されます。
- 変更内容の詳細
2-1. 何をしている変更か
Journey::Routesに「メソッド+パス → Route」の 完全一致テーブル(exact-match table) を追加Router#serveがリクエスト処理時に- まずこのテーブルを参照して即マッチできるか確認
- 見つからなければ従来どおり GTG シミュレータ+正規表現ベースのマッチングを実行
というフローに変更
- 対象は「完全に静的と言い切れる」ルートだけで、条件を満たさないルートは従来どおり通常のマッチング経路を通る
2-2. 完全一致テーブルに載るルートの条件
以下の条件をすべて満たしたルートのみ、テーブルに事前登録されます。
アンカー付きのルート
- ルートパターンが先頭・末尾まで固定されている(中途半端にマッチしない)こと
単一の HTTP メソッドに対するルート
- 例:
GETだけなど GETとPOST両方にマッチするようなパターンは対象外HEADのGETフォールバックはテーブル対象外(HEAD は従来フロー)
- 例:
追加の request constraints がない
constraintsブロックやdefaultsなど、リクエスト内容に依存した振る舞いをするものがない- たとえば
constraints subdomain: 'admin'のようなルートは登録しない
パスが「リクエスト依存でないリテラル」だけで構成される
- 動的セグメント
:id,*slugなどがない - ただし「末尾のオプション部分(
()内のオプション)を取り除いた上で静的」ならOK 例:get "/posts(.:format)"`- テーブルには
"GET /posts"として登録される - 実際には
/postsも/posts.jsonもこのルートとしてマッチするが、テーブル側は「ベースパス」だけ持っているイメージ
- テーブルには
- 動的セグメント
ルート優先順位(precedence)を壊さないことが検証されている
- 「そのメソッド+パスに対して、ルーティングシミュレータを使った通常のマッチングで最初に見つかるルート」と確定できる場合だけテーブルに載る
- つまり、
- もっと手前に「同じパスにマッチしうる別ルート」(静的/動的/マウントいずれでも)がある場合、そのパスはテーブルから除外
- この「影になっていないか (shadowing) のチェック」は GTG シミュレータで行い、ルート数に対して線形時間になるよう配慮されている
2-3. テーブル利用時の挙動
ルーティングの大まかなフローは次のようになります。
Router#serve(env)が呼ばれるenvから HTTP メソッドとパスを取り出し、完全一致テーブルを検索- 一致するエントリがあれば、その
Routeに対応する Rack アプリを即呼び出し - なければ、従来どおり GTG シミュレータ+正規表現マッチを実行
- 一致するエントリがあれば、その
- テーブルからヒットしたルートのアプリが
X-Cascade: passを返した場合- そのルートを「スキップ済み」として扱い、残りのルートについて通常のマッチングフローを実行
- そのルートは2回はディスパッチされない(1度だけ試してから通常走査にフォールバック)
このため、mounted Rack アプリなどが X-Cascade: pass を返すケースでも、 従来と同じルーティング結果になるように きちんとフォールバックが実装されています。
2-4. ビルドとキャッシュ
- 完全一致テーブルは
- ルートセットのビルド時にシミュレータと一緒に 起動時に eager build される
Routes#freeze時にも構築される
- ルートセットの既存の キャッシュ無効化メカニズムの背後 に置かれており
- ルートが追加・変更されたら、このテーブルも再構築
- Ractor 対応:
- Ractor 共有可能なルートセット(eager load 前に finalize 済みのもの)でも、このテーブルが機能するように
Routes#freeze時にテーブル生成が行われる
- Ractor 共有可能なルートセット(eager load 前に finalize 済みのもの)でも、このテーブルが機能するように
2-5. パフォーマンス計測結果
193ルート(32リソース分の静的+動的ルート)を持つアプリでの RouteSet#call 計測:
| リクエスト | 変更前 | 変更後 |
|---|---|---|
GET / | 1.4 µs | 0.8 µs |
GET /things20 | 1.7–1.9 µs | 0.8 µs |
GET /things20/new | 2.3 µs | 0.8 µs |
GET /things20/7 (動的) | 2.4 µs | 2.5 µs |
GET /nope (404) | 0.8 µs | 0.9 µs |
- 静的パスに対しては おおむね 40〜65% 程度のレイテンシ短縮
- 動的ルートや 404 はほぼ変化なし(微小なオーバーヘッド程度)
- テーブルビルド時間:
- 3,000 ルートで約 0.02 秒
- 12,000 ルートで約 0.06 秒
→ 実運用規模でも起動時オーバーヘッドとして十分許容範囲
- 影響範囲・注意点
3-1. 挙動の互換性
- PR 説明に明記されている通り、マッチング結果(どのルートが選ばれるか)は従来と同じ になるよう設計されています
- precedence(定義順優先)・
X-Cascade: passの扱い・HEAD → GET フォールバックなどについても- 影響が出そうなところはすべてテーブル対象外にするか
- フォールバック経路を用意することで 従来挙動を維持しています
とはいえ、以下のような場合は念のためテストしておくと安心です。
- 複雑な
constraintsを使っているルートが多い /fooと/foo(.:format)のような似たパスを多用していて、かつルートの定義順で細かく挙動を制御している- Rack アプリの mount や
X-Cascadeベースのフォールバックを活用している
基本的には、そのようなルートはテーブル対象外になるだけなので「遅くなることはあっても壊れない」設計ですが、意図せぬ挙動の違いがないか回帰テストはしておくとよいです。
3-2. パフォーマンス観点
- 効果が大きいのは、以下のような「静的ルートが多く・アクセス頻度も高い」アプリ:
- RESTful resource での
index,newなど admin区画の各画面- ヘルスチェックエンドポイント (
/up,/healthzなど) - フロントエンドのルーティングに合わせた多数の静的ページ
- RESTful resource での
- 動的な show ルートや 404 については性能はほぼ変わらないため、
- API サーバーなどで 一覧/作成画面やヘルスチェックが多く、ID付きの show より静的パスが多い 場合は特に恩恵が大きい
3-3. ルーティング設定上の注意
開発者がこのPR適用後に特別な対応をする必要は基本的にありませんが、挙動を理解するうえで知っておくとよいポイントです。
get "/posts(.:format)"のような典型的な RESTful index ルートは- テーブル上では
"GET /posts"キーで管理される - 実際のリクエスト
/posts.jsonなどはテーブルミス → 通常マッチングに回るかどうかは実装依存だが、結果としては従来と同じルートにたどり着く
- テーブル上では
constraintsやdefaults、サブドメインルーティングなど「リクエスト依存」な要素を増やすと- テーブルに載るルートが減り、静的ルート最適化の恩恵が小さくなる
- ただし「ルートをシンプルに保てば保つほど速くなる」というだけなので、機能要件上の都合で constraints を使うこと自体を避ける必要はありません
- 参考情報 (あれば)
変更ファイル:
actionpack/lib/action_dispatch/journey/router.rbRouter#serveに完全一致テーブル利用ロジックを追加
actionpack/lib/action_dispatch/journey/routes.rb- ルートセットからテーブルを構築・管理する処理を追加
actionpack/test/journey/router_test.rb,actionpack/test/journey/routes_test.rb- 新しいテーブルの挙動と互換性を保証するテストを追加
actionpack/CHANGELOG.md- Action Pack の変更履歴としてこの最適化内容が追記済み
ベンチマークから見えるポイント:
- 静的ルートは 0.8 µs 程度に収束しており、ルート数が増えてもほぼ一定時間でマッチできる(ハッシュテーブル的なアクセス)
- 動的ルートや 404 は従来アルゴリズム依存のままなので、ルート数増加に応じてこれまでどおり増加しうるが、静的経路の大半がテーブルから即解決されるため、全体としての平均は良くなることが多いと考えられます。
#59056 Don't build structured events that no subscriber will consume
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
Rails の構造化イベント(EventReporter経由のイベント)について、「最終的に誰も消費しないイベント」はそもそも生成しないようにして、リクエストごとの無駄なオブジェクト生成を削減する最適化が入っています。特にログレベルを上げている本番環境などで、action_controller.request_started/request_completedイベントの生成コストを大きく減らせます。
- 変更内容の詳細
背景・問題点
- 各リクエストでは必ず
action_controller.request_startedとrequest_completedのイベントが発火する。 - しかし、アプリ側の logger のレベルがそれらのイベントのログレベルより高い場合(例: イベントは
:infoだが logger は:warn)、実際にはログ出力されない。 - にもかかわらず、これまでは:
- コントローラ側で params フィルタ済み payload を組み立てる
- EventReporter が payload を resolve
- タグ・コンテキスト・タイムスタンプ付きのイベントハッシュを生成
caller_locationsを取ってソース位置を特定- → 最後に LogSubscriber 側で「ログレベルが合わない」として破棄
- この一連の処理だけで、リクエストあたり約 30 個のオブジェクトが「出力されないログのために」生成されていた。
この PR は、**「どうせ誰も受け取らないなら、そのイベントは最初から組み立てない」**という最適化を実装しています。
consumes?(name) の追加
- すべての Subscriber(
EventReporterに登録される購読者)は、任意で以下のメソッドを実装できるようになりました:
def consumes?(name)
# このサブスクライバが、与えられたイベント名 name を「実際に処理する可能性があるか」を返す
# true/false を返すことを想定
end- 挙動:
consumes?(name)を定義していない Subscriber
→ 従来通り「すべてのイベントを消費する」と見なす(常に true 扱い)。- 定義している Subscriber
→EventReporterがイベント生成前にconsumes?を呼び、一つも true を返さない場合にはイベントを構築せずリターンする。
EventReporter#notify / #emitting? の挙動
EventReporter に以下が導入されています:
class ActiveSupport::EventReporter
# すでに存在しているが挙動が拡張
def notify(name, payload = nil)
# 1. name に対して consumes? が true になる Subscriber が一つも無ければ
# payload resolve やイベント構築をせずに即 return
# 2. そうでなければ、従来通り payload を resolve し、イベントハッシュを構築して emit
end
# 新規
def emitting?(name)
# この name のイベントを、少なくとも1つの Subscriber が「実際に消費する可能性があるか」
# 事前に問い合わせるためのユーティリティ
end
endその結果、イベントの発行側は次のように書けます:
# 高コストな payload を組み立てる前に「そもそも誰かが聞いているか?」を確認できる
if Rails.event.emitting?("user.created")
Rails.event.notify("user.created", expensive_payload_for(user))
endnotify側でもemitting?相当のロジックを使っているため:notifyを直接呼んだ場合でも、誰もconsumes?しないイベント名ならイベント構築自体をスキップ。- 明示的に
emitting?を呼ばなくても一定の最適化は効く。
- ただし payload の構築コスト は
notifyを呼んだ時点で支払われてしまうため、payload が高コストな場合は上記のようにemitting?を使うことで、payload 組み立て自体をスキップできる。
StructuredEventSubscriber / ActionController での利用
ActiveSupport::StructuredEventSubscriberにemitting?(name)を追加:
class ActiveSupport::StructuredEventSubscriber
def emitting?(name)
Rails.event.emitting?(name)
end
endActionController::StructuredEventSubscriber(や関連ハンドラ)は、このemitting?を用いて、コントローラ用イベントの payload を作る前にチェックします。- 例:
action_controller.request_started/request_completed用の payload を、誰もそのイベントを消費しないなら最初から組み立てない。
- 例:
これにより、ログレベルなどの条件で実際にはログ出力されない環境では、リクエストごとのオブジェクト生成が大きく削減されます。
EventReporter::LogSubscriber#consumes? の実装
構造化ログの Subscriber 側(EventReporter::LogSubscriber)が、consumes? を実装します。
consumes?(name)の中身:- 既存の「ログレベルによる出力可否」のチェックをそのまま利用。
- つまり、そのイベントのログレベルが現在の logger レベルで出力されないなら
falseを返す。
- 実装上の工夫:
- イベント名 → メソッド名(例:
"action_controller.request_started"→"action_controller_request_started")の変換を共有のキャッシュで持ち、毎回 substring を生成しないようにしている。 consumes?の結果はメモ化しない:- logger のレベルは動的に変わる可能性があるため、キャッシュしてしまうと不正確になる。
- イベント名 → メソッド名(例:
- 例外処理:
consumes?が例外を raise した場合、その Subscriber は「消費するもの」と見なされる(true と同等扱い)。- これにより、ex:
consumes?の実装バグなどがあっても、イベント自体は emit され、既存のemitのエラーハンドリングの中で適切に扱われる。
テストおよびドキュメント
activesupport/test/event_reporter_test.rb:emitting?の挙動consumes?を実装した Subscriber の有無による分岐- 例外発生時の扱い などをカバー。
activesupport/test/event_reporter/log_subscriber_test.rb:- ログレベルごとに
consumes?が正しく true/false を返すことの検証。
- ログレベルごとに
actionpack/test/controller/structured_event_subscriber_test.rb:StructuredEventSubscriber/ Controller ハンドラが payload の構築をスキップしているかを検証。
activesupport/CHANGELOG.mdに、この機能追加と最適化についてのエントリを追加。
- 影響範囲・注意点
影響範囲
- 既存の Subscriber:
consumes?を実装していない限り、「すべてのイベントを消費する」と見なされるため、既存コードの挙動は変わりません。
EventReporterを直接使っているコード:notifyの挙動自体は「誰も消費しない場合は早期 return」するようになっているが、イベントが誰にも届かない状況であれば元々副作用も無いため、実質的に挙動は同じ。
ActionControllerの構造化イベント:- ログレベルがデフォルトの
:infoのままなら、consumes?は常に true になるため、従来と同じだけイベントが構築される。 - ログレベルを
:warn以上に上げている場合などに、payload 構築やイベント生成がスキップされ、大きなオブジェクト削減が発生。
- ログレベルがデフォルトの
性能上の効果(PR の計測値)
- 対象:
request_startedとrequest_completed、コントローラ用 LogSubscriber + 名前空間付き LogSubscriber 6 個、logger 出力先/dev/null。 - 1 リクエストあたりのオブジェクトアロケーション数:
| Logger level | 変更前 | 変更後 |
|---|---|---|
:info (production default) | 37 allocs | 37 allocs |
:warn | 32 allocs | 2 allocs |
:infoでは変更なし。:warnでは 32 → 2 と大幅削減。- また、イベントを発行する側が
emitting?を活用することで、構造化ログ以外の任意のイベントでも同様の削減を見込める。
注意点・設計上のポイント
consumes?はイベント名だけを受け取る:- subscription フィルタ(ブロックなど)は、「イベント名だけでなく、ほかの状態に依存する」ことがありうるため、毎回評価される。
→ 「フィルタは純粋関数である」とは仮定していない。
- subscription フィルタ(ブロックなど)は、「イベント名だけでなく、ほかの状態に依存する」ことがありうるため、毎回評価される。
consumes?実装のベストプラクティス:- できるだけ軽量にする(呼ばれる頻度が高いため)。
- ロガーレベルのような、変わる可能性がある条件に依存する場合は、今回の LogSubscriber と同様「結果をメモ化しない」方が安全。
- 新しくイベントを追加するときのガイド:
- payload 組み立てが重い(DB アクセス、オブジェクト列挙、シリアライズなど)場合は:
if Rails.event.emitting?("some.event")でガードすることを検討するとよい。
- payload 組み立てが重い(DB アクセス、オブジェクト列挙、シリアライズなど)場合は:
- 参考情報 (あれば)
- PR 番号: #59056
- タイトル: Don't build structured events that no subscriber will consume
- 主な変更ファイル:
activesupport/lib/active_support/event_reporter.rbactivesupport/lib/active_support/event_reporter/log_subscriber.rbactivesupport/lib/active_support/structured_event_subscriber.rbactionpack/lib/action_controller/structured_event_subscriber.rb
- この変更は、主に「高ログレベル運用」や「高コストなイベント payload を多用するアプリ」で効果が出やすく、
EventReporterベースの拡張を書いている場合はconsumes?/emitting?を活用することでさらなる最適化が可能です。
#59053 Skip static file candidates whose top-level entry doesn't exist in public/
マージ日: 2026/10/15 | 作成者: @amatsuda
- 概要 (1-2文で)
ActionDispatch::Static/ActionDispatch::FileHandlerが、存在しない静的ファイルを探すために毎回大量のstat(2)とオブジェクト生成を行っていたのを最適化し、動的リクエストに対するオーバーヘッドを大幅に削減する変更です。public/直下のトップレベル要素の存在確認を先に行うことで無駄な候補探索をスキップし、Rack::Request生成やAccept-Encodingのパースも「静的ファイルが見つかった時だけ」行うようにしました。
- 変更内容の詳細
2-1. public/ 直下の存在確認による候補スキップ
従来の挙動:
- パス
/users/1に対して、以下の候補を毎回生成・チェックしていました(実際のコードはもう少し複雑ですが概念的には以下のようなもの):/users/1/users/1.html/users/1/index.html
- さらにそれぞれについて precompressed 版(
.br,.gzなど)も最大3パターンチェック
→ 合計 9 回のstat(2)呼び出し(File.file?相当)と多数の String オブジェクト生成
ほとんどの Rails アプリでは:
- 動的リクエストの大部分は
/users/1,/posts/42/editのようなパス - これらの先頭セグメント(
users,posts)はpublic/直下には存在しない
→ 毎回全部空振りしている
今回の変更点:
トップレベルエントリの存在チェックを先に行う
- リクエストパスを最初のセグメントに分解
/users/1→["users", "1"]→ top-level はusers/posts→["posts"]→ top-level はposts/index.html→ ルート直下のファイル扱い
- 最初に
public/usersが「存在するかだけ」をFile.exist?で確認 - 存在しなければ、
/users/1配下にあるはずのすべての候補 (/users/1,/users/1.html,/users/1/index.htmlとそれらの圧縮版) の探索をまとめてスキップ
- リクエストパスを最初のセグメントに分解
.html候補などへの配慮トップレベルパス
/postsは、public/posts.htmlが存在すれば静的ファイルとして配信したいケースがあります。
そのため:- 「素のパス候補」
/posts→public/posts- これに対しては
public/postsの存在を確認
- これに対しては
- 「
.html付き候補」/posts.html→public/posts.html- こちらは
public/posts.htmlの存在を確認
- こちらは
のように、候補ごとに「どのトップレベルパスを存在確認するか」を切り替えています。
ルート/の場合はpublic/index.htmlを直接 probe します。- 「素のパス候補」
なぜ「起動時に public/ をインデックスしない」のか
- 起動時に
public/をすべて走査してキャッシュしてしまえば、リクエスト時のstatをもっと減らせますが、そうしていません。 - 理由:
- アプリ稼働中に
public/にファイルが追加されるケース(例: CarrierWave 等でpublic/uploadsに保存) - Capistrano の
public/assetsのように symlink 越しのディレクトリ - OS / FS による大文字小文字の扱い(case sensitivity)は、今まで通りファイルシステムにそのまま委ねたい
- アプリ稼働中に
→ 毎リクエストで軽い
File.exist?を行うという形で妥協し、動的性と一貫性を確保しています。- 起動時に
2-2. Rack::Request / Accept-Encoding パースの遅延
従来は:
ActionDispatch::FileHandlerはリクエストごとに:Rack::Request.new(env)を毎回生成- 静的ファイルが見つかるかどうかに関係なく
Accept-Encodingヘッダをパース
- 結果として、静的ファイルがそもそも存在しないリクエストでも、これらのコストを常に払っていました。
今回の変更では:
リクエストメソッドとパスは env から直接読む
env["REQUEST_METHOD"]env["PATH_INFO"](またはSCRIPT_NAMEとの組み合わせ)- GET/HEAD 以外は早期に弾くのはそのままですが、
Rack::Requestオブジェクトを作らずに判定
静的ファイルがヒットするまでは Rack::Request を作らない
- ファイル候補探索 → 実際の
File.file?で静的ファイルが見つかった後で初めてRack::Requestを生成し、レスポンスを組み立てる
- ファイル候補探索 → 実際の
Accept-Encoding のパースも「圧縮版がある時だけ」実行
- precompressed 版 (
.brや.gz) が存在しないファイルについては、Accept-Encodingを見る必要もありません - そこで、precompressed 候補が見つかった時にだけ:
env["HTTP_ACCEPT_ENCODING"]を読む- そこから適切な encoding を選択する
- さらに、その際に使う encoding ごとの正規表現を事前に用意 しておき、毎回文字列から正規表現を組み立てるコストも削減しています。
- precompressed 版 (
2-3. 効果 (PR 記載の計測結果)
Action Pack のテストフィクスチャをドキュメントルートにして計測した例:
| リクエスト | 変更前 | 変更後 |
|---|---|---|
/users/1 | 9 stats, 84 allocations | 1 stat, 28 allocations |
/posts | 9 stats, 82 allocations | 2 stats, 30 allocations |
/index.html ヒット | 6 stats, 74 allocations | 7 stats, 78 allocations |
- 静的ファイル非ヒット時:
stat回数: 最大 9 → 1〜2 回程度に激減- オブジェクトアロケーション: おおよそ 1/3
- 静的ファイルヒット時:
statは 1 回増え、alloc もわずかに増える
→ これは「ほとんどのアプリでは静的ファイルは CDN やフロントサーバで配信し、Rails 本体での静的ファイルヒットはかなり少ない」前提のもと、十分許容できるトレードオフと位置付けられています。
- 影響範囲・注意点
- 対象となる仕組み:
ActionDispatch::Static- 内部で使われる
ActionDispatch::FileHandler
- 影響する場面:
- Rails 本体に静的ファイル配信を任せている環境(特に開発環境や CDN / リバースプロキシなしのシンプルな構成)
- 期待されるメリット:
- 動的リクエスト(
/users/1等)に対するミドルウェアオーバーヘッドの削減 - アロケーション削減による GC 負荷の低減
- 動的リクエスト(
- 挙動上の互換性:
- 静的ファイルが実際に存在する / しないという意味でのルーティングの結果は変わりません
public/にファイルを後から追加するケースや symlink されたディレクトリにも対応したままです- ファイルシステムの大文字小文字の違いも従来通り FS 任せ
- 注意点:
- ごく一部のケースで、静的ファイルヒット時の
stat回数が 1 回増えます(パフォーマンスへの影響はごく軽微) public/のトップレベル構成や symlink の張り方が、これまでと違う解釈をされることはないはずですが、独自パッチや monkey patch をActionDispatch::Staticに当てている場合は、トップレベルエントリの存在チェックロジックと干渉しないか確認が必要です。
- ごく一部のケースで、静的ファイルヒット時の
- 参考情報 (あれば)
- PR 本文で言及されているユースケース:
- CarrierWave 等で
public/uploadsにファイルを保存するような構成 - Capistrano デプロイで
public/assetsをシンボリックリンクにしている構成
- CarrierWave 等で
- 実装が入っているファイル:
actionpack/lib/action_dispatch/middleware/static.rb- テスト:
actionpack/test/dispatch/static_test.rb
- この変更は、Rails をアプリケーションサーバ単体で運用する構成における「静的ファイルミドルウェアのオーバーヘッド」を現実的な範囲で削る最適化であり、「静的ファイルはフロントで配るべき」という一般的な推奨とも整合しています。
#59082 Fix Action Text system tests leaving Chrome offline and leaking RACK_ENV
マージ日: 2026/10/14 | 作成者: @flavorjones
- 概要 (1-2文で)
Action Text のシステムテストがローカル環境で失敗し、Chrome がオフライン状態のまま残ってしまう問題と、Puma 起動時にRACK_ENVがリークしたように見える問題を修正する PR です。テストコード側でネットワーク条件の扱いとRACK_ENVの初期化方法を変更することで、JSON 3.0 以降での例外や LeakChecker の誤検知を防いでいます。
- 変更内容の詳細
2-1. Chrome の network_conditions の扱いを修正
対象: actiontext/test/system/rich_text_editor_test.rb の RichTextEditorTest#while_offline
問題点
while_offlineでは、いったん Chrome のnetwork_conditionsを取得し、それをブロック実行後にwithで復元していました。- Chrome から得られるハッシュは「文字列キー」で、Selenium が
network_conditions=を呼ぶときに「シンボルキーのデフォルト値」を追加します。 - その結果、「同じ意味のキーで文字列キーとシンボルキーが混在するハッシュ」ができ、json 3.0 以降ではこれをシリアライズする際に重複キーとして
JSON::GeneratorErrorが発生するようになりました(json 2.x では警告程度)。 - 例外が出るため、オフライン化した Chrome の状態が正常に復元されず、以降のテストが
net::ERR_INTERNET_DISCONNECTEDで落ちる、という副作用が発生していました。
対応内容
while_offline内で Chrome の現在のnetwork_conditionsを読み取って保存・復元する方式をやめました。- 代わりに、オフラインにする際は「最初からシンボルキーで組み立てたハッシュ」を渡し、復帰時は「オフラインフラグだけを確実に戻す」方針に変更しています。
イメージとしては以下のような変更です(実際のコード断片イメージ):
def while_offline
# 変更後(イメージ)
# オフラインにするときは自前でシンボルキーのハッシュを渡す
page.driver.browser.network_conditions = {
offline: true,
latency: 0,
download_throughput: 0,
upload_throughput: 0,
}
yield
ensure
# 復帰時は Chrome の状態を読み直して復元するのではなく、
# 「offline: false」を確実にセットして戻す
page.driver.browser.network_conditions = { offline: false }
endポイント:
- Chrome 側からハッシュを読み取らないようにしているため、「文字列キー+シンボルキーの混在」が起こらず、JSON シリアライズ時の
JSON::GeneratorErrorを回避できます。 ensureで常にoffline: falseを設定することで、ブロック中に例外が出ても必ずオンライン状態に戻るようにしています。- 元の Chrome のネットワーク条件を完全に復元するのではなく、「オフラインにしたら最後にオンラインに戻す」挙動に簡略化しています。
2-2. RACK_ENV のリーク扱いを回避
対象: actiontext/test/application_system_test_case.rb
問題点
- Capybara が Puma を起動する際、環境変数
RACK_ENVが未設定だと Puma 側でENV["RACK_ENV"] ||= "development"のように設定されます。 - Rails のテストでは
LeakCheckerが「テスト実行前後で増えた環境変数など」をリークとして報告しますが、この Puma によるRACK_ENVのセットが「テスト中に増えた環境変数」とみなされ、リークとして検出されていました。
対応内容
ApplicationSystemTestCaseのロード時に、RACK_ENVが未設定であれば"test"に明示的にセットするようにしました。- これにより Puma 側の
||=によるセットは発生しなくなり、LeakChecker から見て「テスト外での初期状態として既に存在する環境変数」となるため、リーク扱いされません。
イメージとしては以下のような行が追加されています:
ENV["RACK_ENV"] ||= "test"- 影響範囲・注意点
- 影響範囲は Action Text のシステムテスト (
actiontext/test/system/*) と、そのベースクラス (ApplicationSystemTestCase) に限定されています。 while_offlineの挙動変更により、「テスト実行前に Chrome に独自の network_conditions を設定していた場合」に、それを完全に復元しない可能性があります。ただし現状のテストスイートでは、単純にオンライン・オフラインを切り替える用途のみのため、実害はほぼない想定です。RACK_ENVが"test"に固定されるのは「Action Text のシステムテストが読み込まれるタイミング」であり、通常のテスト実行(bin/test/rails test)ではもともと"test"であることが期待されるため、想定外の影響は小さいです。- 逆に、
RACK_ENVをあえて未設定のまま特殊な挙動をテストしていた場合は、この変更によって条件が変わる可能性がありますが、一般的な Rails テスト環境ではほぼ想定されていません。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59082
- JSON gem 3.0 での重複キーの扱い変更により、文字列キー+シンボルキー混在ハッシュのシリアライズで
JSON::GeneratorErrorが発生するようになったことが、このバグ顕在化の背景です(json 2.x では非推奨警告にとどまっていた)。 - Mixed-key ハッシュを返す変更は以前のコミット
14f7f8d1で入り、この時点では致命的ではなかったものの、JSON 3.0 移行で問題化しました。 - CI ではまだ Action Text のシステムテストを実行しておらず、この種の不具合が検出されていなかったことが明示されています(対応 PR: rails/buildkite-config#33)。
#59068 ActiveRecord perf
マージ日: 2026/10/14 | 作成者: @tenderlove
- 概要 (1-2文で)
ActiveRecord のキー処理で「単一キー(非複合主キー)」のケースを特別扱いし、不要なArray経由のメソッド呼び出しを避けることでパフォーマンスを改善した PR です。RubyBench の ActiveRecord ベンチマークで、おおよそ 210ms → 201ms 程度(約 4〜5%)の短縮が報告されています。
- 変更内容の詳細(あればサンプルコードも含めて)
何をしているか
- ActiveRecord には「単一キー(主に通常の
id)」と「複合キー(複数カラムを合わせて主キーとするケース)」を扱うためのActiveRecord::Keyのような仕組みがあります。 - これまでは、単一キーであっても複合キーと同じ経路で処理されており、内部的に
Arrayを経由してブロックをyieldするような実装になっていました。 - この PR では、
- 「Single(単一キー)」向けのコードパスを追加し、
- 複合キー用の配列経由のメソッド呼び出しをスキップ することで、よくあるケース(非複合主キー)を高速化しています。
説明文中の:
This patch manually yields for "Single" keys so that we don't have do to a second method call through
Array.
から読み取れるポイント:
- 以前:
- 単一キーでも内部的に
Arrayを生成 or 経由し、array.each { |k| ... }のようなメソッドを通してブロックを呼び出していた。
- 単一キーでも内部的に
- 変更後:
- 単一キーの場合は、
yield keyのように直接yieldするコードパスを追加。ArrayオブジェクトやArray#each呼び出しを回避。
- 単一キーの場合は、
※ 実際のコードイメージ(あくまでイメージ例):
# 以前(単一キーでも配列扱いしていたイメージ)
def each_key
keys_array.each do |key|
yield key
end
end
# 変更後(Single 用の最適化)
def each_key
if single?
# 単一キー: 直接 yield
yield @key
else
# 複合キー: これまで通り配列で処理
@keys.each do |key|
yield key
end
end
end実際に追加されたコードは 21 行のみで削除はなく、activerecord/lib/active_record/key.rb の中に「Single キー向けの最適化された処理」が新たに定義または分岐として組み込まれています。
ベンチマーク結果
- main ブランチ(変更前):
- RubyBench ActiveRecord ベンチマーク
- non-warmup 33 回の平均: 210ms
- 本 PR ブランチ(変更後):
- non-warmup 35 回の平均: 201ms
およそ 4〜5% の改善で、ActiveRecord を多用するアプリケーションでは無視できない微小最適化です。
- 影響範囲・注意点
影響範囲
- 対象:
- ActiveRecord のキー処理 (
ActiveRecord::Key周辺)。 - 特に「単一主キーのモデル」(一般的な
idを持つ標準的なテーブル)が最も恩恵を受ける。
- ActiveRecord のキー処理 (
- 非対象/影響が小さい:
- 複合主キー(複数カラムからなる主キー)の処理パスは従来通りで、動作は変わらない(パフォーマンスもほぼ据え置き)。
後方互換性
- API の変更やシグネチャ変更はなく、既存のアプリケーションコードに対しては完全に後方互換のパフォーマンス改善です。
ActiveRecord::Baseや関連クラスを通常どおり使っている限り、明示的な対応やコード修正は不要です。
注意点
- 低レベルで
ActiveRecord::Keyを直接利用しているようなメタプログラミング・プラグイン・ライブラリを書いている場合は、挙動の違い(単一キーが別コードパスで扱われること)を前提にしていないかどうかを確認しておくと安全です。- もっとも、この PR 内容から判断する限りは「yield の呼び出し経路を変えただけ」であり、受け取る値の型や意味づけは変わらないはずです。
- パフォーマンス改善は相対的に小さいため、アプリ全体のレスポンス改善量はワークロード次第です。
- DB I/O がボトルネックなケースでは体感差はほぼ出ない可能性もあります。
- 逆に、AR オブジェクト生成やキー操作を極端に大量に行うバッチ・バックグラウンド処理では有利になります。
- 参考情報 (あれば)
- PR 情報:
- タイトル: ActiveRecord perf
- 番号: #59068
- 作成者: tenderlove
- マージ日時: 2026-10-14T04:44:38Z
- 変更ファイル:
activerecord/lib/active_record/key.rb(+21/-0)
- 統計:
- 変更ファイル数: 1
- 追加行数: 21
- 削除行数: 0
この PR は、ActiveRecord 内部のホットパスを小さく最適化する「マイクロパフォーマンスチューニング」の一種であり、Rails 本体が継続的に細かい最適化を積み重ねている例といえます。
#59081 Remove the unused left_side association from the habtm join model
マージ日: 2026/10/14 | 作成者: @kamipo
- 概要 (1-2文で)
has_and_belongs_to_manyが内部的に生成していた中間モデルのleft_side関連(belongs_to :left_side)が、実際にはまったく機能しておらず常にnilを返していたため、不要なコードとして削除された PR です。これにより、HABTM の実装が少しシンプルかつ一貫したものになります。
- 変更内容の詳細
何が問題だったか
has_and_belongs_to_many は、内部的に「中間モデル(join model)」相当の情報を持ちますが、その定義の中で長らく以下のような「左側への逆関連」を持っていました(擬似コード):
# 中間モデルの定義イメージ
class HABTM_JoinModel < ActiveRecord::Base
belongs_to :left_side
# ...
endこの left_side は、HABTM を定義した側(オーナー側)への belongs_to として宣言されていましたが、実際には以下の理由で「完全に死んでいるコード」でした。
- 読み出しに使われていたのは
middle_options[:source]だけだったが、- 中間関連自体は
has_manyであり:throughを使っていなかったため、そのオプションは動作に影響していなかった。
- 中間関連自体は
belongs_to :left_sideに対応する:foreign_keyが指定されておらず、Rails のデフォルト規約ではleft_side_idカラムを参照しようとするが、HABTM の中間テーブルにはそのようなカラムは存在しない。- その結果、
join_record.left_sideを読んでも常にnilが返るだけで、実用的に使えるものではなかった。
過去の変更での経緯:
middle_options[:source]を渡していたコードは b30a23f53b5 で削除済み。- それを支える
left_reflectionの保存処理は #58197 ですでに削除済み。 - 残っていたのは、もはやどこからも正しく参照されない
belongs_to :left_sideの宣言だけだった。
このPR (#59081) では、最終的にその未使用の left_side 関連定義を削除しています。
実際の差分イメージ
対象ファイル:activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
- 追加行: 0 行
- 削除行: 5 行
中身としては、HABTM の中間モデル定義部分から belongs_to :left_side に関連する行が削除されただけの、非常に小さなリファクタリングです。
- 影響範囲・注意点
影響範囲
- 通常の
has_and_belongs_to_manyの利用に対しては、実質的な挙動変更はありません。- もともと
left_sideは機能しておらず、参照してもnilしか返していなかったためです。
- もともと
- 公開APIとして
join_model.left_sideを公式にサポートしていたわけではないので、ドキュメントに沿った典型的な使い方をしているアプリには影響はありません。
注意点 / 互換性
もしアプリケーション側で「Rails の内部実装に依存して」HABTM 中間モデルの
left_sideを直接参照していた場合:- 以前:
nilが返るだけで、実質使い物にならない関連だった。 - 今回以降: そもそも関連メソッド/リフレクションが存在しなくなります(
NoMethodErrorやreflectionがnilなど)。 - したがって、理論上の「壊れ方」はありますが、そもそも意味のある値を返していなかったため、現実的にはそのような依存はほとんど無いと見なされています。
- 以前:
中間テーブルに独自に
left_side_idカラムを追加していたような「かなりトリッキーなハック」を行っている場合:- 以前: 偶然
belongs_to :left_sideがそのカラムを使って動く可能性があった。 - 今回: その逆関連は完全に消えるので、もしそういったハックに依存していた場合は
has_many :through+ 明示的な中間モデルを定義した設計に移行することが推奨されます。
- 以前: 偶然
- 参考情報 (あれば)
関連するIssue / PR:
- 元になった変更:
- #58197:
left_reflectionの削除(今回のPRの前段となるリファクタリング)
- #58197:
- 本PRがクローズするIssue:
- #57910
- #58550
- 歴史的なコミット:
ad7b5efb55b:belongs_to :left_sideが導入されたコミットb30a23f53b5:middle_options[:source](今回削除された関連の唯一の利用箇所)を削除したコミット
設計上の推奨:
- HABTM のような「匿名の中間モデル」に依存した設計よりも、
中間テーブルをモデルとして定義したhas_many :throughの利用が推奨されており、
このPRもその方向性(内部実装の単純化、不要な魔法的挙動の削減)と整合的な変更です。
#58975 Don't modify the job data a job was deserialized from when it's retried
マージ日: 2026/10/14 | 作成者: @yaroslav-shopify
- 概要 (1–2文で)
Active Job のリトライ処理で、デシリアライズ元のジョブデータ(特にexception_executions)が書き換えられてしまう問題を修正し、ジョブごとに独立したカウンタを持つようにした PR です。これにより、オリジナルのジョブデータが意図せず変化したり、凍結オブジェクトでFrozenErrorが発生する問題が解消されます。
- 変更内容の詳細
問題の背景
retry_on を用いたリトライ時、Active Job はジョブのシリアライズ/デシリアライズを行いますが、従来は次のような問題がありました。
exception_executions(例外ごとの実行回数を記録するハッシュ)が、- 最初に
perform_laterされたジョブ - そのジョブから派生したリトライジョブ
で共有されていた。
- 最初に
retry_onによるリトライ処理でexception_executionsの値をインクリメントすると、「元のジョブデータ」側のハッシュも同時に書き換えられてしまう。- その結果:
performed_jobsに記録されるジョブデータのカウンタが「最後のリトライ時点の値」になってしまい、本来の履歴がわかりにくくなる。- ジョブペイロードを
freezeしたり、Ractor 間共有用にshareableにしていると、リトライ時のインクリメントでFrozenErrorが発生する。
修正内容
activejob/lib/active_job/core.rb の deserialize 処理が変更され、ジョブ生成時に exception_executions を「元データからコピー」するようになりました。
イメージとしては次のような変更です(擬似コード):
def deserialize(job_data)
self.job_id = job_data["job_id"]
# 以前は参照をそのまま使っていた:
# self.exception_executions = job_data["exception_executions"]
# これからは独立したコピーを保持する:
if job_data["exception_executions"]
self.exception_executions = job_data["exception_executions"].dup
end
endこれにより、各リトライジョブは自分専用の exception_executions を持つようになり、オリジナルのジョブデータ(および他リトライジョブ)の内容は変更されません。
テストの追加
activejob/test/cases/exceptions_test.rb および activejob/test/cases/test_helper_test.rb にテストが追加されています。主に次の点を検証していると考えられます:
- リトライ発生時に
exception_executionsのカウントがインクリメントされても、元のジョブデータのハッシュが書き換えられないこと。 - 凍結されたジョブデータ(あるいは共有可能オブジェクト)を元にしても、リトライ処理時に
FrozenErrorを起こさないこと。 performed_jobs等のテストヘルパに記録されるジョブデータが、リトライによって後から書き換えられないこと。
CHANGELOG
activejob/CHANGELOG.md にエントリが追加されており、
- 「リトライ時にデシリアライズ元データを変更しないようにした」
といった趣旨のバグ修正として記載されています(動作変更として利用者が把握すべき項目)。
- 影響範囲・注意点
影響範囲
retry_on(および関連する例外リトライ機構)を利用している Active Job 全般。performed_jobs/enqueued_jobsなど、テストヘルパ経由でジョブデータを参照しているコード。
以前は「後からリトライが走るとカウンタが書き換わってしまう」状態でしたが、今後は「記録時点の値のまま固定」される挙動になります。- ジョブのシリアライズペイロードを凍結したり、Ractor 間共有のために
shareableにしている高度な利用パターン。
互換性
- 期待される挙動としては「バグ修正」であり、破壊的変更ではありません。
- ただし、もしアプリ側で「同じハッシュオブジェクトが共有されている」ことに依存していた場合(ほぼありえない設計ですが)、今回の変更でその前提は成り立たなくなります。
performed_jobsのexception_executionsカウンタ値が、これまでと変わる可能性があります(これまでは「最終リトライ回数」、今後は「各実行時点の回数」になる)。
注意点
- 「リトライを繰り返した結果としてのトータル回数」を一か所のジョブデータから読みたい、という用途には今後も Active Job 自体は特に API を提供していません。必要であればアプリ側で独自にカウンタを管理するほうが安全です。
- ジョブペイロードを積極的に immutable にしている環境(Ractor など)では、今回の修正により、
retry_onを使った場合のFrozenErrorが避けられるはずです。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58975
- 関連ドキュメント:
- Active Job ガイド – エラー処理とリトライ(
retry_on,discard_on)
https://guides.rubyonrails.org/active_job_basics.html#callbacks
- Active Job ガイド – エラー処理とリトライ(
- 概念的には、「シリアライズされた状態(ストレージに保存されるペイロード)」と「実行時インスタンスの状態」を明確に分離し、実行中に後者を変えても前者には影響を与えないようにする、という設計上の是正です。
#58978 Make Active Job argument serialization work in non-main Ractors
マージ日: 2026/10/14 | 作成者: @yaroslav-shopify
- 概要 (1-2文で)
Active Job が Ractor(特にメイン Ractor 以外)上でジョブ引数をシリアライズ / デシリアライズする際にRactor::IsolationErrorが発生していた問題を修正し、Ractor 対応を強化する PR です。ractorize!実行時に Active Job のシリアライザ関連オブジェクトを「shareable」にする仕組みを追加し、非メイン Ractor からでも安全に利用できるようにしています。
- 変更内容の詳細
問題の背景
ActiveJob::Serializersを経由する引数(例:Time,ActiveSupport::Duration,Symbol、カスタムシリアライザ対象の型)やHashを、非メイン Ractor からシリアライズ / デシリアライズしようとするとRactor::IsolationErrorが発生していました。- デシリアライズ時に起きた
Ractor::IsolationErrorはActiveJob::DeserializationErrorにラップされるため、discard_on ActiveJob::DeserializationErrorを設定しているジョブは 理由が分かりづらいまま捨てられる、という実害がありました。 - 原因として、Active Job のシリアライザレジストリやそのシングルトンインスタンスが「Ractor shareable」になっていなかったため、非メイン Ractor からアクセスすると IsolationError になる状態でした。
追加された仕組み: ActiveJob::Serializers.make_shareable!
この PR では、ractorize! 実行時に以下を行うようにしています。
ActiveJob::Argumentsをロード- これにより、
ActiveSupport.on_load(:active_job)等のフックを通して、カスタムシリアライザも含めた各種シリアライザがレジストリに登録される。
- これにより、
ActiveJob::Serializers.make_shareable!を呼び出す- 中で
try_make_shareableを用いて、- シリアライザレジストリ(登録済みのシリアライザ一覧)
- 各シリアライザのシングルトンインスタンス
を Ractor 的に共有可能(shareable)なオブジェクトに変換する。
- これにより、非メイン Ractor からでも Active Job の引数シリアライズ / デシリアライズが IsolationError なしで動作するようになる。
- 中で
Rails 側 (Rails::Application) の ractorize! 実装が、この新しい ActiveJob::Serializers.make_shareable! を呼ぶように変更されています。
既存定数の変更: ActiveJob::Arguments::RESERVED_KEYS を freeze
ActiveJob::Arguments::RESERVED_KEYSがfreezeされました。- Ractor では、shareable にするにはオブジェクトがイミュータブルである必要があるため、定数も明示的に不変にすることで Ractor との相性を良くしています。
テストの追加
以下の観点でテストが追加されています。
activejob/test/cases/argument_serialization_test.rb- 非メイン Ractor から Active Job の引数シリアライゼーションを行ってもエラーにならないことの検証(Time, Duration, Symbol, Hash など)。
activejob/test/cases/serializers_test.rb- シリアライザレジストリ・シリアライザインスタンスが shareable になっているか、Ractor 内で利用して問題ないか等を確認。
railties/test/application/ractors_test.rb- Rails アプリケーションで
ractorize!を呼んだ場合に Active Job 周りが正しく shareable 化され、Ractor から利用可能であることの検証。
- Rails アプリケーションで
- 影響範囲・注意点
影響範囲
Rails アプリケーションで
ractorize!を使っている、または今後使う予定がある場合:- メイン Ractor 以外からの Active Job 実行 / キュー投入 / 引数シリアライズが現実的に動くようになる改善です。
- 特に、
TimeやActiveSupport::Duration、Symbol、カスタムシリアライザ対象オブジェクトを引数に持つジョブが、Ractor 環境でも安全に扱えるようになります。
カスタムシリアライザ (
ActiveJob::Serializers::ObjectSerializerを継承したものなど) を定義しているプロジェクト:- ロードフック経由で登録される前提で
make_shareable!実行前にレジストリへ登録されるようになっているため、特別な対応は不要な想定です。 - ただし、カスタムシリアライザ自身の内部状態が「shareable でない」場合(ミュータブルなクラス変数を持つなど)には、Ractor で問題になる可能性があります。今回の PR は「レジストリとインスタンスを
try_make_shareableする」ところまでであり、各シリアライザの実装が Ractor セーフかどうかまでは保証しません。
- ロードフック経由で登録される前提で
注意点
ractorize!自体がまだ experimental かつ:nodoc:な API と位置付けられており、CHANGELOG 更新も行われていません。このため、将来的に挙動変更・API 変更の可能性があります。- Ractor を前提にライブラリやアプリケーションを実装する場合:
- Active Job 周りはこの PR によってだいぶ扱いやすくなりますが、アプリ側のジョブクラス内部の処理や使用している他のライブラリが Ractor セーフであるか は引き続き注意が必要です。
RESERVED_KEYSを含め、今後 Active Job 内部の定数や構造体が Ractor 対応のためにより広範に freeze / shareable 化されていく可能性があります。
内部定数を直接変更したり monkey patch している場合は、それが壊れるリスクが高まります。
- 参考情報 (あれば)
- Ruby Ractor 仕様:
Ractor::IsolationErrorは「非 shareable なオブジェクトを別 Ractor に渡そうとした」など、Ractor の分離性に違反したときに発生します。try_make_shareable(obj)は、オブジェクトをできる範囲で shareable に変換しようとする Ruby 側のヘルパーです(変換できない場合はエラー)。
- 関連しそうなキーワード・調査ポイント:
Rails::Application#ractorize!の実装と、他のコンポーネント(Active Record, Action Cable 等)の Ractor 対応状況- Active Job のカスタムシリアライザ:
ActiveJob::Serializers.add_serializers,ActiveJob::Serializers.serialize,ActiveJob::Serializers.deserialize - ジョブの例外ハンドリング:
discard_on ActiveJob::DeserializationErrorの挙動とログ出力・監視設定
#58979 Make Active Job's exception handlers work in non-main Ractors
マージ日: 2026/10/14 | 作成者: @yaroslav-shopify
- 概要 (1-2文で)
Active Job がマルチ Ractor(特に非メイン Ractor)で実行される際に、rescue_from/retry_on/discard_on/after_discardといった例外ハンドラがRactor::IsolationErrorで落ちていた問題を修正し、Ractor 対応の例外処理が正しく動作するようにした PR です。
これにより、ractorize!使用時でも Active Job / controllers / mailers / channels のrescue_fromが非メイン Ractor 上で安全に動作します。
- 変更内容の詳細
背景となる問題
ractorize!を有効にして Job を非メイン Ractor で実行すると、ジョブ内で例外が発生したときに:rescue_handlers(rescue_fromのハンドラ)after_discard_procs(discard_on・after_discard周り)
- これらが Ractor の shareable 制約を満たしていないため
Ractor::IsolationErrorを起こし、結果として:rescue_fromretry_ondiscard_on
- が期待通り動作しない、という不具合がありました。
主な対策の方向性
- 例外ハンドラとして保持する
Proc/Arrayを「Ractor 間で共有可能(shareable)」な形にする。 - そのためのフックとして、
ActiveSupport::Ractors.unshareable_proc_actionが設定されている場合に、Proc を shareable 化する処理を追加。 - 利用側(Active Job / controllers / mailers / channels)でこの仕組みを使ってハンドラを shareable に変換する。
変更点 1: rescue_from ハンドラの shareable 化
対象ファイル:
activesupport/lib/active_support/rescuable.rb(+2)
ActiveSupport::Rescuable(rescue_from を提供するモジュール)において:
- ハンドラを登録する際、
ActiveSupport::Ractors.unshareable_proc_actionが設定されていれば、そのポリシーに従って Proc を変換し、Ractor 間で安全に共有できるようにします。 - これにより、以下のようなコードが非メイン Ractor で実行されても
Ractor::IsolationErrorになりにくくなります。
class ApplicationJob < ActiveJob::Base
rescue_from(StandardError) do |exception|
# ログ出しや通知など
end
endこの仕組みは ActiveSupport::Rescuable を利用している以下にも波及します:
- controllers(ActionController)
- mailers(ActionMailer)
- channels(ActionCable)
変更点 2: retry_on / discard_on のハンドラの shareable 化
対象ファイル:
activejob/lib/active_job/exceptions.rb(+41/-29)
retry_on / discard_on は内部的に「例外にマッチしたときのハンドラ Proc」を組み立てていますが、その実装を Ractor フレンドリーに変更しています。
ポイント:
ハンドラは「shareable な引数」から構築
exceptions,wait,attempts,queue,priorityなど、基本的にイミュータブル or freeze 済みの値から Proc を作る形にして、Proc 自体を shareable にしやすくしています。
「block を yield する」のではなく「block を直接 call」
- 以前は「外側の Proc から
yieldする」ような構造になっていて、Ractor 的に扱いづらい(shareable 化しづらい)部分がありました。 - これを「block を閉じ込めた Proc を作り、その Proc を直接
callする」ような形に変更することで、ActiveSupport::Ractors.unshareable_proc_actionによる shareable 化が適用しやすくなっています。
- 以前は「外側の Proc から
イメージ(簡略化サンプル、実際の実装とは異なりますが構造イメージ):
# 以前はこういうイメージだった:
def retry_on(*exceptions, wait: 3.seconds, attempts: 5, &block)
handlers << proc do |job, error|
if block_given?
yield job, error # <- yield に依存
else
# デフォルトの retry ロジック
end
end
end
# 今はこういうイメージ:
def retry_on(*exceptions, wait: 3.seconds, attempts: 5, &block)
handler = proc do |job, error|
if block
block.call(job, error) # <- block を閉じ込めて call
else
# デフォルトの retry ロジック
end
end
handler = ActiveSupport::Ractors.share_proc(handler) if ActiveSupport::Ractors.enabled?
handlers << handler
end※ 上記は説明用の擬似コードです。
これにより、retry_on / discard_on を使ったジョブが非メイン Ractor 上で動いても、例外ハンドリングが正常に行われるようになります。
変更点 3: after_discard と after_discard_procs の shareable 化
対象ファイル:
activejob/lib/active_job/exceptions.rb(+41/-29)
discard_on は、例外を握りつぶしてジョブを破棄したあとに実行される after_discard コールバックと関係があります。
今回の変更で:
after_discardに登録される Proc を shareable 化rescue_fromと同様に、ActiveSupport::Ractors.unshareable_proc_actionの設定にしたがって Proc を shareable にします。
after_discard_procsのデフォルトを frozen Array に変更after_discard_procsは「登録された after_discard コールバックのリスト」ですが、これを Ractor 間で共有できるようにデフォルトを frozen Array にしています。- Ractor では「凍結されたイミュータブルなオブジェクト」は shareable になるため、それに合わせた設計です。
変更点 4: テストの追加・拡充
対象ファイル:
- Active Job 周り:
activejob/test/cases/exceptions_test.rb(+83)
- ActiveSupport の
rescue_from周り:activesupport/test/rescuable_test.rb(+41)
- Railties(アプリケーションレベルの Ractor 統合テスト):
railties/test/application/ractors_test.rb(+22)
主に以下の観点のテストが追加・強化されています:
- 非メイン Ractor 上でジョブを実行し、
rescue_from/retry_on/discard_on/after_discardが期待通りに動作すること。 - ActiveSupport::Rescuable(
rescue_from)が、Ractor 利用時に shareable なハンドラを作れること。 - Rails アプリケーション全体として
ractorize!を有効にした場合でも、例外ハンドラが Ractor::IsolationError を出さずに動くこと。
- 影響範囲・注意点
影響範囲
ractorize!を有効にしている(もしくは今後有効にする)アプリケーションにとっては、非メイン Ractor で実行される:- Active Job の例外処理 (
rescue_from,retry_on,discard_on,after_discard) - controllers / mailers / channels の
rescue_from
- Active Job の例外処理 (
- が、より安全に正しく動作するようになります。
互換性・挙動の変化
- パブリック API としてのシグネチャは変わっていないため、通常の(シングル Ractor / マルチスレッド)利用におけるコード修正は基本的に不要です。
- ただし、例外ハンドラや
after_discardブロック内で「非 shareable なオブジェクト(例えばミューテーブルなグローバル状態など)」をキャプチャしている場合、Ractor 下では問題になる可能性があります。- 例: ハンドラブロックがクラスインスタンス変数やグローバルなキャッシュオブジェクトを閉じ込めている場合など。
after_discard_procsのデフォルトが frozen Array になった点は、内部利用前提のため通常のアプリ開発者には影響はありませんが、- メタプログラミング等で直接この配列を操作している場合は、
dupしてから変更する必要があります。
- メタプログラミング等で直接この配列を操作している場合は、
Ractor は依然として experimental な領域
ractorize!自体が:nodoc:で experimental な位置付けのため、将来的に API や挙動が変わる可能性があります。- 本 PR も「Ractor を本格的に使うための安定化の一歩」程度の位置づけであり、Ractor 利用時のすべてのケースを完全にカバーした保証ではありません。
- 参考情報 (あれば)
- 対象 Ruby バージョン:
- Ruby 4.0, Ruby master で動作確認済みと明記されています。
- 関連する内部的な仕組み:
ActiveSupport::Ractors.unshareable_proc_action- Proc を shareable にする際のフックポイント(例えば Proc のラッピングや変換処理を差し込むためのもの)。
- ActiveSupport::Rescuable (
rescue_from)- Controllers / Mailers / Channels / Jobs などで共通して利用される例外ハンドラ DSL。
- 先行する Ractor 対応 PR でも
CHANGELOGには記載しておらず、本 PR もそれに倣っています(ractorize!がまだ公にサポートされていないため)。
#59080 Reach the joined table's model through the table itself
マージ日: 2026/10/14 | 作成者: @kamipo
- 概要 (1-2文で)
このPRは、Arel::TableやArel::Nodes::TableAliasから元の Active Record モデル (klass) を直接取得できるようにし、association_scope内のスコープ適用処理が「テーブル」からモデルに自然に到達できるようにするリファクタリングです。これにより、関連スコープ構築時にリフレクション経由ではなくテーブルオブジェクト経由でPredicateBuilderを組み立てられるようになります。
- 変更内容の詳細
2-1. 背景
Arel::Tableはすでに「どのモデル (ActiveRecord::Baseのサブクラス) から生成されたか」を知っています。Arel::Nodes::TableAliasは、もともとtable_nameやtype_for_attributeなど一部のメソッドを内部のArel::Tableに委譲していました。- しかし
klassの委譲がなく、association_scopeでapply_scopeを呼ぶ際に「関連の reflection を渡してklassにアクセスする」という遠回りをしていました。
このPRはその「遠回り」を解消し、「渡されたテーブルから素直に klass を取る」という構造に変えています。
2-2. Arel::Nodes::TableAlias に klass を委譲
変更ファイル: activerecord/lib/arel/nodes/table_alias.rb (+4)
概念的には以下のような委譲が追加されています (擬似コード):
module Arel
module Nodes
class TableAlias < Arel::Nodes::Binary
# すでにあった委譲のイメージ
delegate :table_name, :type_for_attribute, to: :left
# 今回追加された委譲
delegate :klass, to: :left
end
end
endポイント:
TableAlias#leftは元のArel::Tableを指すことが多く、そのArel::Tableがklassを持っています。- これにより、
users AS managers_usersのような結合・サブクエリ別名に対してもtable_alias.klassで元のUserクラスにアクセスできるようになります。
2-3. AssociationScope#apply_scope のモデル到達経路の簡潔化
変更ファイル:activerecord/lib/active_record/associations/association_scope.rb (+5/-6)
AssociationScope#apply_scope (またはそれに類するメソッド) では、関連のクエリを組み立てるときに PredicateBuilder を使って条件式 (WHERE 句など) を作っています。
従来は:
- 「関連の reflection (例:
has_many :comments)」を渡す - そこから
reflection.klassを取り出す - その
klassを使ってPredicateBuilderを構築する
といったコードになっていました。
PR後は概ね次のような形になります (イメージ):
def apply_scope(scope, table)
# 以前: reflection.klass を使っていた
# klass = reflection.klass
# 変更後: 与えられた Arel::Table / TableAlias から直接 klass を取得
klass = table.klass
predicate_builder = klass.predicate_builder
# 以下、predicate_builder を使って where 条件などを構築
# ...
endポイント:
apply_scopeに「reflection を連れてくる必要」がなくなり、「スコープ適用対象のテーブル (table引数) だけを見ればよい」形になります。- 実装の責務分担としても、「テーブルが自分の元モデルを知る」「スコープは受け取ったテーブルからモデルを知る」という自然な流れになります。
- 影響範囲・注意点
3-1. 外部的な挙動への影響
- 公開APIの振る舞いとしては基本的に互換性を保ったリファクタリングであり、アプリケーションコードから見える挙動に直接の変更はほぼありません。
- 主に内部実装 (
association_scope・apply_scope・Arel のテーブル/別名周り) の整理です。
3-2. 期待されるメリット
- コードの単純化と可読性の向上
association_scope周りが、リフレクションに過度に依存せず、「テーブルからモデルへ」という一方向の依存に整理されます。
- メンテナンス性
- 将来的に関連スコープ構築処理 (特に多段 JOIN や別名テーブルを使う部分) をいじるときに、
table.klassでモデルを取得できるので処理の見通しがよくなります。
- 将来的に関連スコープ構築処理 (特に多段 JOIN や別名テーブルを使う部分) をいじるときに、
- Arel まわりの一貫性
- すでに
table_name,type_for_attributeを委譲しているTableAliasにklassも揃えることで、Arel::TableとArel::Nodes::TableAliasを同等に扱いやすくなります。
- すでに
3-3. カスタムArel利用・メタプログラミングをしている場合の注意
- 自前で
Arel::Nodes::TableAliasを継承・モンキーパッチしている場合、klass委譲追加によるメソッド解決順序の変化が影響する可能性はあります (かなりレアケース)。 table.klassを前提にする内部処理が増えることで、もし独自にArel::Tableを生成していてklassが設定されていないような特殊なケースがあると、予期せぬNoMethodError/nil起因のエラーが起きる可能性はあります。- ただしこれは主に Rails 内部で生成する
Arel::Table/TableAliasを前提としているため、通常のアプリケーションでは問題にならないはずです。
- ただしこれは主に Rails 内部で生成する
- 参考情報 (あれば)
- このPRはコミット
6af2260f981af6812f8f9e4047ee15497fbfb57bのフォローアップと明記されています。- そのコミットでは、おそらく
Arel::Tableと Active Record モデルの関連付けやtype_for_attribute委譲まわりの改善が行われており、本PRはその仕上げとしてklassへの到達も統一したものと考えられます。
- そのコミットでは、おそらく
- 関連する内部API:
ActiveRecord::PredicateBuilderActiveRecord::Associations::AssociationScopeArel::Table,Arel::Nodes::TableAlias
#59066 Use an ivar for the primary key definition
マージ日: 2026/10/14 | 作成者: @tenderlove
- 概要 (1-2文で)
このPRは、Active Record のprimary_key_definitionの計算結果をインスタンス変数にキャッシュすることで、Rails main で劣化していた ActiveRecord ベンチマークのパフォーマンスを一部取り戻す変更です。内部実装のみを触っており、外から見たAPIや挙動は(バグがなければ)変わりません。
- 変更内容の詳細
※ソース全文は提示されていないので、説明は PR タイトル・説明文と Rails の既存構造からの推測を含みますが、方向性と意図は正確です。
2-1. 主な変更点
- 対象:
activerecord/lib/active_record/attribute_methods/primary_key.rb - 目的:
primary_key_definitionメソッドの呼び出しコスト削減(ホットスポットの最適化)
これまで primary_key_definition は、呼び出されるたびに「そのモデルの主キー定義」を都度計算 or 取得していた箇所があり、それが Ruby-Bench (ActiveRecord ベンチ) でボトルネックになっていました。
この PR では、primary_key_definition の結果をインスタンス変数(いわゆる「ivar」= instance variable)に保持し、同じインスタンスでは再計算しないようにしています。
典型的には以下のような変化が入っていると考えられます(イメージコード):
# 変更前(イメージ)
def primary_key_definition
# 毎回計算 or 検索
connection.schema_cache.primary_keys(table_name)
end
# 変更後(イメージ)
def primary_key_definition
# 一度だけ計算して @primary_key_definition にキャッシュ
@primary_key_definition ||= connection.schema_cache.primary_keys(table_name)
endあるいはクラスレベル・モジュールレベルで似たパターンの memoization が導入されているはずです。
2-2. テスト変更
activerecord/test/cases/primary_keys_test.rbが +20/-2 行変更されています。
ここでは、おそらく以下のような点がテストされていると考えられます。
primary_key_definitionのキャッシュが、モデルのスキーマが変わっても不整合を起こさないか- モデルの継承や
reset_column_information実行時にキャッシュが正しく無効化・再計算されるか - 複数テーブル/複合主キー/カスタム主キーのケースでも動くか
Rails はスキーマ変更や connection 再接続時にカラム情報をリセットする仕組みがあるため、そのフローの中で ivar キャッシュも一緒にクリアされることが保証されるようなテストが追加されているはずです。
- 影響範囲・注意点
3-1. 性能面の影響
Ruby-Bench の ActiveRecord ベンチマークにおいて:
- Rails main: 平均 213ms
- このブランチ: 平均 201ms
- Rails v8.1.4: 平均 131ms
と報告されています。
この PR によって main ブランチの退行分を一部取り戻してはいるものの、まだ 8.1.4 よりもかなり遅い状態である、という位置付けです。primary_key_definition の呼び出しが多いアプリ(特に大量のレコードを扱う、またはメタ情報に頻繁にアクセスするケース)では、わずかながらレスポンス改善が期待できます。
3-2. 挙動面の影響
基本的には「内部的に結果をキャッシュするだけ」なので、通常の以下のようなコードの挙動は変わらない前提です。
Post.primary_key # => "id"
Post.primary_key_definition # => 主キーの型情報など注意すべきは、キャッシュ導入によって以下のようなケースでバグが紛れ込む可能性がある点です(PR のテストでカバーされているはずですが、頭の片隅に置くとよいです):
- マイグレーションや
schema_cache.clear!後にprimary_key_definitionが古い情報を返さないか reset_column_informationのような API を呼んだあとも、ちゃんと再計算されるか- 複数 DB や
connected_toを使った接続切り替え環境で、接続ごとの primary key 定義が混ざらないか
アプリ側で primary_key_definition を直接叩くことはあまり多くありませんが、これに依存する内部機構(生成される Arel ノード、シリアライゼーション、スキーマ情報など)を強く叩く処理を書いている場合、Rails 更新時には一応テストを回しておくと安心です。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59066
- 関連箇所(参考用・現行の main とは差異がある可能性があります)
activerecord/lib/active_record/attribute_methods/primary_key.rbActiveRecord::Base#reset_column_information/ スキーマキャッシュまわりのコード
- 性能劣化の全体像としては、この PR だけで 8.1.4 の速度には戻っておらず、main ブランチでのパフォーマンス退行を戻すための「一手」という位置づけです。
#59078 Zip keys without going through Enumerable
マージ日: 2026/10/14 | 作成者: @matthewd
- 概要 (1-2文で)
ActiveRecord のActiveRecord::Keyに専用のzip実装を追加し、Enumerable経由の汎用実装を避けることで、主キー/外部キーまわりのペア操作のメモリアロケーションを大幅に削減した PR です。これに伴い、replace_keys・keys_match?・join_scopeなどの内部ループがzipベースに書き換えられ、パフォーマンス改善が期待できます。
- 変更内容の詳細
2-1. ActiveRecord::Key#zip の専用実装追加
従来、Key オブジェクトに対する zip 呼び出しは Enumerable#zip の汎用実装にフォールバックしており、
- 単一カラムのキーでも 1 回の
zip呼び出しで約 11〜13 個のオブジェクトが割り当てられる - ブロック付き (
key1.zip(key2) { ... }) でも同程度のアロケーションが発生
というオーバーヘッドがありました。
本 PR では Key#zip が Enumerable を経由せずに動作するように最適化され、
- 単一カラム key 同士を
zipする場合: アロケーション 0 - 複合 key 同士の場合: アロケーション 1
まで削減されています。
イメージとしては、Key の内部の配列(例: columns, values のようなもの)同士を、専用のループで 1:1 で走査する実装に置き換えた形です。
想定されるコードイメージ
# 以前: Enumerable に落ちるので汎用 zip 実装が使われる
primary_key.zip(foreign_key) do |pk, fk|
...
end
# 現在: ActiveRecord::Key に最適化された zip が生える
primary_key.zip(foreign_key) do |pk, fk|
# ほぼゼロアロケーションでペア処理が可能
endブロックなしの zip(配列を返す形式)にも対応していると考えられますが、説明文では特に「ブロック形式で 11–13 allocations → 単一カラムで 0」という点が強調されています。
2-2. replace_keys / keys_match? のループを zip ベースに変更
activerecord/lib/active_record/associations/belongs_to_association.rb などで定義されている、主キー/外部キーをペアで処理するメソッドが zip ベースに書き換えられています。
典型的には、
- 以前:
primary_key[i]とforeign_key[i]のようにインデックスアクセスでペア処理していた - 現在:
primary_key.zip(foreign_key)でペアを生成し、その組をループで処理する
という変更です。
擬似コード:
# 以前のイメージ
primary_key.each_with_index do |pk_value, i|
fk_value = foreign_key[i]
# pk_value と fk_value を比較 or 代入
end
# 現在のイメージ
primary_key.zip(foreign_key) do |pk_value, fk_value|
# pk_value と fk_value を比較 or 代入
endKey#zip の高速化により、インデックスベースよりも簡潔かつ低アロケーションで書けるようになっています。
変更された主な箇所:
belongs_to_association.rbreplace_keysなど、関連のキー更新処理でのループを書き換え
reflection.rbjoin_scope構築時に、主キー/外部キーをペアにして結合条件を作る部分をzipベースに変更
これにより、複合主キー・複合外部キーを持つ関連でも、結合条件やキーのセットアップ時のオーバーヘッドが減少します。
2-3. Key のテスト追加 (key_test.rb)
activerecord/test/cases/key_test.rb に Key#zip に関するテストが追加されています。
想定されるテスト内容:
- 単一カラム同士の
Keyをzipしたときの挙動 - 複合カラム同士の
Keyをzipしたとき、各ポジションが正しくペアになっているか - ブロック付き・ブロックなしの両方で正しく動作するか
Key の public API として zip を期待通りに扱えることを保証するための追加です。
- 影響範囲・注意点
影響範囲
- ActiveRecord 内部で
ActiveRecord::Key#zipを使っている箇所(belongs_toの関連処理、join_scopeの生成など)が対象 - ユーザーコードで
primary_key.zip(foreign_key)のようにKeyオブジェクトに対してzipを呼んでいる場合も、そのまま高速化の恩恵を受けます - 複合キー(Composite key)を利用した関連定義でも、キー比較・コピー・JOIN 条件生成の性能が向上
- ActiveRecord 内部で
互換性・挙動面の注意
Enumerable#zipと互換なインターフェイスを保つよう実装されているはずですが、zipの返り値の型や細かい挙動(例: 長さが違う key 同士の扱い、nilの扱いなど)はKey側の実装に依存します- これまで
Enumerableの実装前提でモンキーパッチを当てていた場合(かなりレアケース)には動作が変わる可能性があります - 公開 API として
ActiveRecord::Keyを直接扱うのは一般的ではないため、通常のアプリケーションコードには基本的に非互換影響はありませんが、ORM 内部の動作をフックしている高度なメタプログラミングコードは一応確認した方が安全です
パフォーマンス観点
- 説明にある通り、特にブロック付きの
zipにおけるメモリアロケーションが顕著に削減されています - 大量のレコードを関連を辿りながら処理するケース、複合キーを多用するケースで GC 負荷軽減・スループット向上につながる可能性があります
- 説明にある通り、特にブロック付きの
- 参考情報 (あれば)
- 元 PR / 関連 PR
- #59063, #59068: 今回はこれらのフォローアップとして、
eachやmapと同様にzipもEnumerableをバイパスする最適化を追加
- #59063, #59068: 今回はこれらのフォローアップとして、
- 実装の意図
- Ruby の
Enumerable#zipは便利な一方、配列生成や中間オブジェクト生成によるアロケーションコストが高く、ActiveRecord 内部のホットパスに載せるには重い Keyはサイズが小さく構造も単純なため、自前ループで完結させることで GC 負荷を削減する方針
- Ruby の
- 実務的なポイント
- アプリ側のコード変更は不要で、Rails をこのバージョン以降に上げるだけで、主キー/外部キー処理の一部が自動的に高速化される
- ベンチマークを行っているプロジェクトであれば、関連を多用するクエリのパフォーマンス計測時に、この変更の効果が現れていないか確認する価値があります。
#58948 Apply the table name prefix to the index name 4.2 migrations compute
マージ日: 2026/10/14 | 作成者: @carlosdanielpohlod
- 概要 (1-2文で)
Rails 4.2 互換レイヤのマイグレーションで、table_name_prefixが設定されている場合にインデックス名の計算が間違っていた問題を修正する PR です。index_exists?とremove_indexが、プレフィックス付きの正しいテーブル名からインデックス名を計算するように変更されています。
- 変更内容の詳細
問題の背景
ActiveRecord::Migration::Compatibility::V4_2 に定義されている index_exists? と remove_index は、以下のような流れになっています:
- まずコネクションに「インデックス名」を問い合わせて
options[:name]を決定する - その後
superを呼び出す superは最終的にMigration#method_missingにフォールスルーし、proper_table_nameを使ってテーブル名にプレフィックスを付与する
ところが、すでに計算済みの options[:name](インデックス名)はそのまま で、プレフィックスはテーブル名にしか反映されません。
table_name_prefix を例にすると:
# config/application.rb 等で
config.active_record.table_name_prefix = "p_"テーブル testings に対して foo カラムにインデックスを張ると、実テーブル名と実インデックス名は:
- テーブル:
p_testings - インデックス:
index_p_testings_on_foo
になりますが、問題のバグがあると:
remove_index :testings, :foo
# => "index_testings_on_foo" を探しに行くため
# ArgumentError: Index name 'index_testings_on_foo' on table 'testings' does not exist
# という例外が発生する
index_exists?(:testings, :foo)
# => 実際には index_p_testings_on_foo が存在していても false を返すという動作になります。
この PR での修正内容
PR の説明にある通り、5.1 互換層の change_column については既に同種のバグが 6ca42300e8 で修正されており、そのアプローチを 4.2 互換層のインデックス関連メソッドにも適用しています。
具体的には:
V4_2#index_exists?V4_2#remove_index
の中で インデックス名を計算する際に、直接 proper_table_name(= プレフィックス適用済みテーブル名)を使う ように変更されています。
擬似コードレベルでは、従来:
def index_exists?(table_name, column, options = {})
# table_name は "testings" のまま
options[:name] ||= connection.index_name(table_name, column)
super
endのようにプレフィックスなしの table_name から index name を作っていたところを、
def index_exists?(table_name, column, options = {})
actual_table_name = proper_table_name(table_name) # "p_testings"
options[:name] ||= connection.index_name(actual_table_name, column)
super
endのように、インデックス名の計算にも proper_table_name を用いる 形に揃えています。remove_index も同様です。
これにより、table_name_prefix が有効な環境では常に:
- 実テーブル名:
p_testings - 実インデックス名:
index_p_testings_on_foo remove_index :testings, :fooindex_exists?(:testings, :foo)
のすべてが一貫して p_testings 基準で計算され、インデックスが正しく検出・削除されるようになります。
テストと CHANGELOG
activerecord/test/cases/migration/compatibility_test.rbに 43 行分のテストが追加されており、table_name_prefixが設定されているケースでindex_exists?とremove_indexが正しく動作すること
をカバーしています。
activerecord/CHANGELOG.mdに 4.2 互換マイグレーションのバグ修正として 9 行のエントリが追加されています。
- 影響範囲・注意点
- 影響範囲:
- 対象は Rails のマイグレーション互換レイヤ
ActiveRecord::Migration::Compatibility::V4_2のみ です。 config.active_record.table_name_prefixを利用しているプロジェクトで、- Rails 4.2 形式のマイグレーションを互換モードで実行している
- かつ
index_exists?/remove_indexを使用している 場合に、これまで正常に動いていなかった処理が正しく動くようになります。
- 対象は Rails のマイグレーション互換レイヤ
- 互換性:
- 挙動変更は バグフィックス であり、設計意図に沿った動きに修正するものです。
- 既に「バグありの挙動」に依存してしまっているコードがある場合(例:
remove_indexがエラーになる前提の rescue ロジックなど)は、エラーが発生しなくなることで挙動が変わる可能性がありますが、通常は望ましい修正と考えられます。
- 実運用上の注意:
- 過去に
table_name_prefixを使っていて、4.2 形式マイグレーションで「インデックスが存在しない」と言われ続けていた場合、この修正適用後にindex_exists?がtrueを返すようになったりremove_indexが正常にインデックスを削除できるようになる 可能性があります。
- 古いマイグレーションを再実行するケース(テスト環境・CI での
db:test:prepareやdb:reset等)で挙動が変わりうるため、table_name_prefix利用プロジェクトでは一度マイグレーションの通し実行を確認すると安心です。
- 過去に
- 参考情報 (あれば)
- 類似修正:
6ca42300e8(V5_1#change_columnでのproper_table_name適用)
→ この PR はそのパターンを 4.2 のインデックス関連メソッドに拡張したものです。 - 関連コード:
ActiveRecord::Migration::Compatibility::V4_2ActiveRecord::Migration#proper_table_name
- 利用者向けまとめ:
table_name_prefixを使っている場合でも、Rails 4.2 互換マイグレーションでrubyが 期待通りプレフィックス付きテーブルのインデックスを扱えるようになった、というのがこの PR の本質的な変更点です。index_exists?(:testings, :foo) remove_index :testings, :foo
#59033 Skip ROLLBACK when SQLite already rolled back the transaction
マージ日: 2026/10/14 | 作成者: @cole-robertson
- 概要 (1-2文で)
SQLite が自動でトランザクションをロールバックしている場合に、Rails 側で余計なROLLBACKを発行しないようにした修正です。これにより、SQLite のディスクフルなどで発生する本来の例外が正しく表に出るようになり、トランザクション内のレコード状態やafter_rollbackコールバックの挙動も一貫性のあるものになります。
- 変更内容の詳細
問題の背景
SQLite では、以下のようなエラーが発生した場合に SQLite 自身が自動的にトランザクションをロールバック します。
SQLITE_FULL(ディスクフル / ページ不足)SQLITE_IOERRSQLITE_NOMEM- など、致命的な書き込み系エラー
このとき、Rails の SQLite3 アダプタは従来どおり ROLLBACK を発行していましたが、既にトランザクションが終了しているため SQLite 側は:
SQLite3::SQLException: cannot rollback - no transaction is activeというエラーを返します。この二重エラーにより次の問題が起きていました。
本来のエラーが隠れてしまう
- 期待される例外:
SQLite3::FullException: database or disk is full - 実際に表に出る例外:
ActiveRecord::StatementInvalid: SQLite3::SQLException: cannot rollback - no transaction is active - 本来の
SQLite3::FullExceptionはcauseのさらに奥に埋もれるため、rubyのようなハンドリングが動作しない。rescue SQLite3::FullException # リトライ処理… end
- 期待される例外:
ActiveRecord のトランザクション整合性が壊れる
エラーがROLLBACKの失敗で握り潰されることで、rollback_recordsが実行されず、次のような不整合が起きます。rubyItem.transaction do first = Item.create!(body: "x") 50.times { Item.create!(body: "y" * 20_000) } # ここで SQLITE_FULL を起こすよう調整 end- 修正前:
- 例外:
ActiveRecord::StatementInvalid: SQLite3::SQLException: cannot rollback - no transaction is active first.persisted? # => truefirst.id # => 1- 実際のテーブル行数:
0 after_rollbackコールバック呼び出し:0
- 例外:
- 修正後:
- 例外:
ActiveRecord::StatementInvalid: SQLite3::FullException: database or disk is full first.persisted? # => falsefirst.id # => nilafter_rollbackコールバック呼び出し:1
- 例外:
つまり、DB には書き込まれていないのに、AR 的には「保存済み」と見えてしまうバグがあったのを、正しくロールバック済み扱いに修正しています。
- 修正前:
実際の変更点
1. SQLite3Adapter#exec_rollback_db_transaction の挙動変更
activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb が変更されました。
変更前:
トランザクション終了時に常にROLLBACKを発行していた。変更後:
ロールバック時に、ローレベルの SQLite3 接続に対して「まだトランザクションが開いているか?」を確認し、開いていなければROLLBACKを発行しないようになりました。具体的には、
SQLite3::Database#transaction_active?(内部的には!sqlite3_get_autocommit)でオートコミット状態を確認し、トランザクションが既に終了している(= SQLite による自動ロールバック済み)場合は、SQL のROLLBACK実行をスキップします。
重要なのは、「SQL の ROLLBACK をスキップするだけ」であり、ActiveRecord 側のロールバック処理(rollback_records など)は従来通り実行される点です。これにより:
- DB に対しては余分な
ROLLBACKを投げない - しかし AR のトランザクションコールバックや record 状態の調整は正しく行われる
という挙動になります。
2. テストの追加
activerecord/test/cases/adapters/sqlite3/transaction_test.rb に新テスト SQLite3AutoRollbackTest が追加されています。
- 役割:
- SQLite が自動ロールバックした状況(
SQLITE_FULL)で、- 表に出る例外が
SQLite3::FullExceptionになること - レコードの
persisted?/idおよびafter_rollbackの挙動が期待通りになること
- 表に出る例外が
- を検証するテストです。
- SQLite が自動ロールバックした状況(
- このテストは、変更前には失敗し(
cannot rollback - no transaction is activeが返ってくる)、変更後には成功します。
3. CHANGELOG の更新
activerecord/CHANGELOG.md に、この挙動変更がバグ修正として追記されています。これにより、ライブラリの利用者は挙動変更を把握できます。
- 影響範囲・注意点
影響範囲
- 対象: SQLite3 アダプタを使っている Rails アプリケーション
- 影響する状況:
- トランザクション内のクエリが
- ディスクフル (
SQLITE_FULL) - IO エラー (
SQLITE_IOERR) - メモリ不足 (
SQLITE_NOMEM) - などで失敗し、SQLite 側が自動的にロールバックを行ったケース
- ディスクフル (
- トランザクション内のクエリが
- 他の RDBMS(PostgreSQL / MySQL 等)のアダプタには影響しません。
挙動の変化
例外ハンドリングのしやすさが向上
これまでは、次のような例外ハンドリングは期待通り動かないことがありました。rubybegin Item.transaction do # 大きな書き込み処理 end rescue SQLite3::FullException # ディスクフル時のリトライや通知処理 end修正後は
ActiveRecord::StatementInvalid経由であっても、外側に現れる原因例外 (cause) としてSQLite3::FullExceptionなどが出るため、rescue SQLite3::FullExceptionによる分類が正しく機能するようになります。トランザクション内オブジェクトの状態が DB と一致する
- エラー発生後に
persisted?がfalseになり、idがnilになる after_rollbackコールバックが正しく実行される
これにより、「DB には存在しないのに、アプリケーションオブジェクト的には存在すると認識される」という不整合が解消されます。
- エラー発生後に
注意点
- もし既存コード側で、「
cannot rollback - no transaction is activeというエラーメッセージに依存した挙動(文字列マッチなど)」をしている場合、そのコードは動作しなくなります。代わりに、例外クラスやcauseに対する判定に移行することが推奨されます。 - 例外のトップレベルは引き続き
ActiveRecord::StatementInvalidでラップされるため、「ActiveRecord 例外としてハンドリングする」という方針自体は変わりません。必要に応じてe.causeで元の SQLite 例外を参照してください。 - SQLite のバージョンや
sqlite3gem のバージョンによっては、エラークラス・メッセージが微妙に違う可能性があるため、厳密なメッセージ文字列ではなくクラスに基づいたハンドリングを行うのが安全です。
- 参考情報 (あれば)
- SQLite の自動ロールバック挙動:
エラーが発生したときのトランザクション処理については SQLite の公式ドキュメント("Transaction Control" および "Error and Recovery" あたり)が参考になります。 - Rails 側実装の位置:
ActiveRecord::ConnectionAdapters::SQLite3Adapter#exec_rollback_db_transaction- 及びトランザクションコールバック周り (
rollback_records) の処理パス
- テストで使われている再現方法:
PRAGMA max_page_countを用いて使用可能ページ数を制限し、INSERT多発でSQLITE_FULLを簡単に発生させている点は、ローカルでの再現・検証にも使えます。
#59072 Deprecate supports_native_partitioning? on the PostgreSQL adapter
マージ日: 2026/10/14 | 作成者: @yahonda
- 概要 (1-2文で)
PostgreSQL Adapter のsupports_native_partitioning?メソッドが、サポート対象の PostgreSQL バージョンが 10 以上になったことで常にtrueしか返さなくなったため、非推奨化された PR です。これに合わせて、このメソッドに依存していた内部処理とテストが整理されています。
- 変更内容の詳細
背景
- #55282 によって Active Record のサポートする PostgreSQL の最小バージョンが 10.0 になった。
- PostgreSQL 10 以降では「ネイティブパーティショニング」が標準機能として存在するため、
supports_native_partitioning?は実質的に常にtrue。 - 同じ理由で、以前の PR では
supports_pgcrypto_uuid?も非推奨化済み。
この PR は、その流れに沿って supports_native_partitioning? も非推奨にするものです。
主なコード変更ポイント
1. supports_native_partitioning? の非推奨化
具体的なコードは PR 本文にはありませんが、典型的には PostgreSQL Adapter 内で以下のような変更が行われています:
- メソッド定義に「非推奨」の注記が入り、将来的な削除が示唆される。
- 実装としては引き続き
trueを返すが、外部からの利用は推奨されない。
イメージ:
# activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
# 以前: (例)
def supports_native_partitioning?
postgresql_version >= 100000
end
# 現在: 非推奨化(実際の実装は多少異なる可能性あり)
def supports_native_partitioning?
ActiveSupport::Deprecation.warn(
"supports_native_partitioning? is deprecated because " \
"PostgreSQL 10+ always supports native partitioning."
)
true
end※実際のメッセージ文言や書き方は若干異なる可能性がありますが、趣旨はこのとおりです。
2. schema_statements のロジック変更
supports_native_partitioning? を使って分岐していた箇所が、常にパーティションキー情報を読むように変更されています。
PR 説明より:
the batched table options query, which selects an untyped
NULLinstead ofpg_get_partkeydef()when the method returnsfalse. This PR reads the partition key unconditionally
つまり、以前は:
supports_native_partitioning?がtrue→pg_get_partkeydef()でパーティションキー取得false→ 型無しNULLを選択していた
という分岐があったのが、今後は:
- 常に
pg_get_partkeydef()を使う
ようになります。
イメージ:
# 以前
selects << if supports_native_partitioning?
"pg_get_partkeydef(c.oid) AS partition_key"
else
"NULL AS partition_key"
end
# 変更後
selects << "pg_get_partkeydef(c.oid) AS partition_key"3. Ractor proxy での capability からの削除
stops listing the method among the Ractor proxy's prefetched capabilities
Ractor 対応のため、アダプタの「事前フェッチする機能一覧」のようなリストから supports_native_partitioning? が外されています。
これは「今後フレームワーク内部でこのメソッドに依存しない」ことを明確にする変更です。
4. テストの整理
schema_test.rbのパーティションテーブル関連テストで、supports_native_partitioning?を見てスキップしていた2つのテストのskipが削除。- 全サポート環境で PostgreSQL 10+ が前提になったため、「パーティションをサポートしない環境」はもはや存在しない扱いに。
connection_test.rbにsupports_native_partitioning?の挙動確認や deprecation を担保するテストが追加。adapter_helper.rbからも関連する分岐(おそらく「Native partitioning が無い DB のためのヘルパ」)が削除。
5. CHANGELOG の更新
activerecord/CHANGELOG.mdに、supports_native_partitioning?の非推奨化が記載され、将来の削除候補であることが明示されています。
- 影響範囲・注意点
アプリケーション/ライブラリ側からの影響
影響を受けるのは主に「Rails 外部で supports_native_partitioning? を直接使っているコード」です。
- PostgreSQL Adapter オブジェクト (
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) に対してconnection.supports_native_partitioning?
を呼び出しているコードは、今後 deprecation warning が出る可能性があります。 - PostgreSQL 10 未満はそもそも Rails 側でサポート対象外なので、条件分岐自体が意味を持たなくなっています。
→ 呼んでいる場合は、単純に「常に true」とみなすか、その条件分岐を削除して良いケースがほとんどです。
置き換えの方針:
- 「PostgreSQL ネイティブパーティショニングが使えるか?」の判定が目的なら、
→ Rails 8 (仮) 以降などを前提とするなら、判定そのものを削除して構いません(サポート対象では常に使える)。 - 将来の互換性を気にしていたとしても、「支援メソッドがなくても PostgreSQL 10+ 固定である」点は変わらないため、
supports_native_partitioning?を使い続ける意味はほぼありません。
フレームワーク内部の挙動の変化
- バッチでテーブル情報を取得するクエリが、常に
pg_get_partkeydef()を呼ぶようになりました。- 現状サポート対象の PostgreSQL バージョンを考えると、動作上の差は出ないはずです。
- PostgreSQL 10+ では
pg_get_partkeydef()が存在するため、互換性問題はありません。
将来的な削除リスク
- 非推奨化された API は、将来の Rails メジャーバージョンで削除される可能性が高いです。
- もしプラグインや社内ライブラリでこのメソッドを使っている場合は、いまのうちに呼び出しを削除 or 常に true 扱いに書き換える ことを推奨します。
- 参考情報 (あれば)
- 当該 PR: https://github.com/rails/rails/pull/59072
- 前提となる PR (PostgreSQL 10 以上を必須化 /
supports_pgcrypto_uuid?非推奨化): https://github.com/rails/rails/pull/55282 - PostgreSQL 10 のパーティショニング機能概要(公式ドキュメント):
https://www.postgresql.org/docs/10/ddl-partitioning.html
この PR は「内部的にはもう意味のない feature flag を整理し、将来の API からも削除するための下準備」という位置づけと考えると分かりやすいです。
#59069 Match sequence reset rows by the table name given, not regclass::text
マージ日: 2026/10/14 | 作成者: @yahonda
- 概要 (1-2文で)
PostgreSQL アダプタでreset_pk_sequence!/reset_column_sequences!が、スキーマ付きテーブル名(例:"public.posts")に対してNoMethodErrorになる不具合を修正した PR です。テーブルとシーケンスの紐付け方法をregclass::textベースから OID ベースに変更することで、スキーマ付きのテーブル名でも安全にシーケンスリセットできるようにしています。
- 変更内容の詳細
問題の背景
Rails 8.1 では、以下のようにスキーマ修飾したテーブル名を渡すことが許容されています:
ActiveRecord::Base.connection.reset_pk_sequence!("public.posts")しかし #55928 の変更後、SequenceReset クラスが「テーブル名とシーケンス情報のマッピング」を regclass::text で行うようになった結果、
- PostgreSQL は search_path 上のテーブル について
regclass::textを評価すると、スキーマ名を省略して返す"public.posts"::regclass::text→"posts"
- 一方で Rails 側は
"public.posts"というキーでテーブル情報を持っている
という不整合が起こり、 "public.posts" に対応する行を結果から見つけられなくなり、nil に対して column= を呼んでしまうことで NoMethodError が発生していました。
このコードパスは fixtures から reset_column_sequences! 経由でも使われるため、table_name が "public.posts" のモデルを fixtures で使うと同様に落ちます。
具体的な対応内容
SequenceReset の内部で使われる 2 つのクエリ:
primary_key_column_and_sequence_sqluuid_column_and_sequence_sql
について、テーブル名のマッチング方法を変更 しています。
従来:
- すべての対象テーブルを PostgreSQL に投げて、
pg_classなどから主キー列・シーケンス名を取得 - 結果は
regclass::text由来のテーブル名をキーとして返す- search_path 上の
"public.posts"は"posts"として返ってくる
- search_path 上の
- Ruby 側では、元々のテーブル名 (
"public.posts") をキーにして結果を探すためミスマッチ
修正後:
- Ruby 側で対象テーブルの
(name, oid)のペアをあらかじめ構築name: 呼び出し元から渡されたテーブル名 ("public.posts"など)oid:pg_classから取得したテーブル OID
- この
(name, oid)の一覧をVALUES句として SQL 内でインライン化し、一時テーブルのように扱う pg_class等と OID で JOIN することで、regclass::text を使わずにテーブルを特定- 結果セットには 最初に渡した
nameをそのまま含めて返す- つまり
"public.posts"というキーで結果が返ってくる
- つまり
こうすることで、
- DB 側では OID で厳密にテーブルを特定
- Ruby 側では「呼び出し元が渡した文字列(例:
"public.posts")」をそのままキーにして扱える
という形になり、スキーマ付き・なしの両方で安定してマッチするようになりました。
クエリ数自体は従来と変わらず、「1 回で対象テーブル分の情報をまとめて取ってくる」構造は維持されています。
テスト
activerecord/test/cases/adapters/postgresql/schema_test.rb にテストが追加されており、
- スキーマ修飾テーブル名
- search_path の影響を受けるケース
といった条件下で reset_pk_sequence! / reset_column_sequences! が正しく動作することが検証されています。
- 影響範囲・注意点
- 影響を受ける主なケース
- PostgreSQL を使用している
reset_pk_sequence!あるいはreset_column_sequences!を スキーマ付きテーブル名(例"public.posts") で呼び出している- または、モデルの
table_nameを"public.posts"のようにしており、fixtures ロード時に内部でこれらが使われるケース
- この PR によって、これらのケースで発生していた
NoMethodErrorが解消されます。 - クエリ構造は変わっていますが、
- クエリ回数は同じ
- OID での JOIN 自体は PostgreSQL では一般的で安定したやり方 のため、性能や安定性への大きな悪影響は想定しづらいです。
CHANGELOGは未更新(チェックリスト上も未チェック)なので、当面は「内部的なバグフィックス」として扱われる可能性があります。
- 参考情報 (あれば)
- 関連 PR: #55928(
SequenceReset周りの以前の変更。ここでregclass::textによるマッチングが導入されている) - 関連メソッド:
ActiveRecord::Base.connection.reset_pk_sequence!(table_name)ActiveRecord::Base.connection.reset_column_sequences!(table_name)
- 特に、マルチスキーマ構成や
search_pathを積極的に使っているアプリケーションでは、この変更により「スキーマを含めたテーブル名指定」がより安全に扱えるようになります。
#59070 Don't require ActiveStorage service that is already defined
マージ日: 2026/10/14 | 作成者: @bogdan
- 概要 (1–2文で)
Active Storage のサービス設定時に、既に定義済みのサービスクラスに対して不要なrequireを行わないようにActiveStorage::Service::Configuratorが変更されています。これにより、独自サービスクラスを Active Storage 内部のディレクトリ構造に従って配置しなくても利用しやすくなります。
- 変更内容の詳細
問題背景
従来の Active Storage では、storage.yml でカスタムサービスを指定する場合、Active Storage 側が
require "active_storage/service/#{name}_service"のように「active_storage/service 配下にある <name>_service.rb」を前提として require していました。
そのため、以下のような不便がありました。
- カスタムサービスクラスを
app/services/やlib/my_app/など、アプリ側の好みのディレクトリに置きにくい - 既に autoload や Zeitwerk で読み込まれているクラスであっても、Active Storage が強制的に
requireを試み、パス構造を揃えないとLoadErrorになる
PR の説明にもある通り、これにより
Before this patch you are forced to create
./lib/active_storage/service/<name>_service.rb
という制約が事実上発生していました。
変更の要点
この PR では ActiveStorage::Service::Configurator が修正され、
- すでに目的のサービスクラスが定義済みであれば
requireを行わない - 未定義である場合にのみ、これまでと同様のルールで
requireを試みる
という挙動に変わっています。
コードは概ね以下のようなイメージに近い変更です(擬似コード):
# 変更前 (イメージ)
def build(service_name, config)
require "active_storage/service/#{service_name}_service"
"ActiveStorage::Service::#{service_name.camelize}Service".constantize.new(config)
end
# 変更後 (イメージ)
def build(service_name, config)
service_class_name = "ActiveStorage::Service::#{service_name.camelize}Service"
# すでにクラスが定義されていれば require しない
unless Object.const_defined?(service_class_name)
require "active_storage/service/#{service_name}_service"
end
service_class_name.constantize.new(config)
end※実際のコードはこの擬似コードと多少異なる可能性がありますが、意図は「定義済みかどうかを確認してから require する」という点にあります。
カスタムサービスの利用イメージ
この変更により、たとえば以下のような構成が自然に動きやすくなります。
# app/services/my_custom_storage_service.rb
class MyCustomStorageService < ActiveStorage::Service
# 必要なメソッドを実装
end# config/storage.yml
my_custom:
service: MyCustomStorageService
# 追加の設定...上記のように、Rails の autoload パスに載っているクラス名を service に直接指定しておけば、すでにクラスがロード済みなら Active Storage が無理に require しないため、active_storage/service 配下にファイルを置く必要がありません。
- 影響範囲・注意点
影響範囲
ActiveStorage::Service::Configuratorを使ってサービスインスタンスを組み立てる全てのコードパスに影響します。- 公式・標準のサービス (
Disk,S3,GCSなど) はこれまで通り動作しますが、内部的には「定義済みチェック」が追加されただけなので、既存挙動に対する後方互換性は高いと考えられます。 - カスタムサービスクラスを既に Active Storage 規定のパス (
active_storage/service/...) で運用しているケースも、そのまま動きます。
注意点
- 「クラスが定義済みかどうか」の判定は、通常
const_defined?あるいはsafe_constantizeなどで行われます。Zeitwerk / autoload のタイミングに依存するため、次の点に注意が必要です。- カスタムサービスクラスを利用する場合は、少なくとも Active Storage が configurator を呼ぶ時点までにクラスが autoload 可能な状態になっている必要があります(
app/以下 orconfig.autoload_pathsに追加されたディレクトリに配置するなど)。 - Rails 起動時に eager load している環境では、
config.eager_load_pathsに含まれているかも確認するとよいです。
- カスタムサービスクラスを利用する場合は、少なくとも Active Storage が configurator を呼ぶ時点までにクラスが autoload 可能な状態になっている必要があります(
- 万一「定義済みチェックが true だが中身が意図したクラスでない」といった定数衝突があると、これまでとは異なるクラスが使われてしまう可能性があります。そのようなケースは稀ですが、ライブラリ間で同名クラス(同じ定数名)を定義していると影響を受けえます。
- 「クラスが定義済みかどうか」の判定は、通常
- 参考情報 (あれば)
- Active Storage ガイド(カスタムサービス関連の節)
https://edgeguides.rubyonrails.org/active_storage_overview.html - 類似の仕組みとして、
ActiveSupport::Cacheのカスタムストアなどは、特定のディレクトリ構造を強制していないため、この PR の動機として「似た機能と挙動を揃える」という意図が読み取れます。
#58981 Make deprecation warnings work in non-main Ractors
マージ日: 2026/10/14 | 作成者: @yaroslav-shopify
- 概要 (1-2文で)
Rails アプリをractorize!した後でも、非メイン Ractor(ジョブやバックグラウンド処理など)で ActiveSupport の非推奨警告(deprecation)が本来の設定どおりに動作するようにする修正です。これにより、これまで非メイン Ractor 上で deprecation を扱おうとすると発生していたRactor::IsolationErrorやFrozenErrorを回避します。
- 変更内容の詳細
2-1. 問題点
ractorize! を呼ぶとアプリケーションの deprecator オブジェクトが freeze されます。この状態で、メイン Ractor 以外(例: ジョブワーカー Ractor)から:
- 非推奨警告を発生させる
ActiveSupport::Deprecation#silenceを使って一時的に警告を抑制する
といった操作を行うと、内部的に以下のような例外が発生していました。
Concurrent::ThreadLocalVar::LOCALSへのアクセスでRactor::IsolationError- 従来の実装では「スレッドローカルな状態」で deprecation のサイレンスカウンタなどを管理しており、その実装が Ractor 間共有に適合していなかった。
- freeze 済み deprecator オブジェクトを書き換えようとして
FrozenError
結果として、非メイン Ractor では deprecation の振る舞い(ログ出力・例外発生・サイレンスなど)が正常に機能しませんでした。
2-2. 修正方針
(A) silence カウンタと allow 許可リストの保存先を変更
- 対象:
silenceによる「一時的に警告を抑制しているかどうか」のカウンタallow(deprecation)による「この非推奨は許可する」などの許可設定
- 変更点:
- これらの状態を
Concurrent::ThreadLocalVarではなく、ActiveSupport::IsolatedExecutionStateに移動 - キーは「deprecator ごと」に分離(1つの deprecator インスタンス単位で独立状態を持つ)
- これらの状態を
ActiveSupport::IsolatedExecutionStateは、設定されたisolation_levelに応じてスコープが変わる仕組みになっており:isolation_level = :fiberの場合: fiber ごとに独立した状態- それ以外(デフォルト)の場合: スレッド単位の状態
- ログレベルをスレッド/ファイバ毎に分ける
LoggerThreadSafeLevelと同様の仕組みに合わせることで、Ractor との相性も良くしています。
これにより以下が実現されます:
- Ractor ごとの deprecation 状態が安全に保持される
- 「この Ractor / この fiber 内だけ一時的に silence する」といった振る舞いが、従来通り(あるいはより一貫して)機能する
概念図イメージ:
# 例: Fiber ごとに deprecation を silence する
ActiveSupport::IsolatedExecutionState.isolation_level = :fiber
deprecator = ActiveSupport::Deprecation.new
Fiber.new do
deprecator.silence do
deprecator.warn("deprecated in fiber1") # => サイレンスされる
end
end.resume
Fiber.new do
deprecator.warn("deprecated in fiber2") # => 通常通りの挙動(ログや例外)
end.resume※ 実際のテストコードはもう少し低レベルですが、イメージとしてはこのようにファイバごとに独立。
(B) freeze 前に挙動をメモ化
deprecator を freeze するときに、後から変更される可能性のある挙動を事前に確定させておきます。
- 対象プロパティ:
behavior: 通常の deprecation の取り扱い方法(ログに出す、例外にするなど)disallowed_behavior: disallowed とマークした非推奨に対する挙動disallowed_warnings: disallowed 扱いする非推奨のリスト・パターン
- 変更点:
freeze実行時にこれらの値をメモ化(キャッシュ)してからオブジェクトを freeze- 実行時にはメモ化済みの情報に基づいて動き、freeze 済み deprecator の内部状態を直接書き換えない
これにより:
- freeze 済み deprecator を非メイン Ractor から使用しても
FrozenErrorを出さずに済む - Ractor 間で共有されても「設定されたとおりの挙動」が安定して再利用できる
(C) コールスタックマッチング用の定数を freeze
- 非推奨警告が発生した際、「どの呼び出し元から来たのか」を判定するためにコールスタックのパスパターンをマッチングする定数群があります。
- これらの「パスを表す定数配列 / 文字列」を freeze しました。
- 目的:
- Ractor 間共有される可能性のある定数を不変にすることで
Ractor::IsolationErrorを避ける - パフォーマンス面での些細な改善(再構築を防ぐ)も副次的に期待できる
- Ractor 間共有される可能性のある定数を不変にすることで
- 目的:
(D) テスト
追加・更新されたテスト:
activesupport/test/deprecation_test.rb(+37行)- IsolatedExecutionState 上で deprecation の
silenceやallowが期待どおりにカウント・適用されることを検証していると考えられます。
- IsolatedExecutionState 上で deprecation の
railties/test/application/ractors_test.rb(+21行)- Rails アプリを実際に
ractorize!し、非メイン Ractor から deprecation を発生させても:- 例外 (
Ractor::IsolationError/FrozenError) が出ない - 設定済みの deprecator の挙動が尊重される といったことを確認。
- 例外 (
- Rails アプリを実際に
Ruby 4.0 と Ruby master で検証済みと明記されています。
- 影響範囲・注意点
3-1. 影響範囲
- 直接的に影響を受けるのは:
ActiveSupport::Deprecationを利用しているコード全般- 特に
ractorize!を利用する(実験的な)Ractor 対応アプリケーション ActiveSupport::IsolatedExecutionState経由でスレッド/ファイバローカルの状態管理をしている部分との整合性
通常の(非 Ractor)Rails アプリでも内部実装が変わるため、理論上は挙動差分が生じる可能性はありますが、PR の意図としては:
- インターフェースや外から見える挙動は変えない(バグ修正)
- バックエンドの状態管理の仕組みだけを
ThreadLocalVar→IsolatedExecutionStateに差し替える
という位置づけです。
3-2. 注意点
ractorize!自体が experimental かつ:nodoc:のため、今後も仕様変更の余地があります。isolation_level = :fiberを利用している場合:- deprecation の
silence・allow状態が「スレッドではなくファイバ単位」で独立することになります。 - もしアプリケーション側で「スレッド全体で共有される前提」で deprecation のサイレンスを行っているような高度な使い方をしていると、期待と異なる可能性があります(一般的な使い方では問題にならないはずです)。
- deprecation の
- deprecator の freeze タイミング前後で挙動(設定可能かどうか)が変わる点には従来から注意が必要ですが、今回の変更はその制約を明確にしつつ Ractor でも安全に使えるようにしたものです。
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58981
- 関連クラス:
ActiveSupport::DeprecationActiveSupport::IsolatedExecutionStateLoggerThreadSafeLevel
- コンテキスト:
ractorize!は Rails アプリを Ractor 対応するための実験的 API で、まだ公式ドキュメントには載っていない(nodoc)。- 本 PR はその流れの中で、deprecation 周りを Ractor セーフにする一連の修正の一つと位置づけられています。
#59017 Dump create_schema if_not_exists for every schema that holds an extension
マージ日: 2026/10/14 | 作成者: @tusshar2000
- 概要 (1-2文で)
Rails の PostgreSQL 用 SchemaDumper が、拡張機能(extensions)が存在するスキーマを漏れなくcreate_schema ... if_not_exists: trueでダンプするようになり、dump_schemasに含まれないスキーマにインストールされた拡張でもdb:schema:loadが失敗しないように修正されています。特にbtree_ginやpg_trgmのような「移動可能(relocatable)」な拡張をカスタムスキーマに入れているアプリのスキーマロード問題を解消します。
- 変更内容の詳細
何が問題だったか
Rails の schema.rb 生成時 (
ActiveRecord::SchemaDumper) に、以下のようなケースがあった:- 拡張が
publicではなく、別スキーマ(例:extensions)にインストールされている - しかし
ActiveRecord.dump_schemas = "public"などで、dump 対象スキーマにそのスキーマが含まれていない
- 拡張が
その場合、ダンプされた schema.rb は:
rubyActiveRecord::Schema[8.2].define(version: 0) do enable_extension "extensions.btree_gin" enable_extension "plpgsql" endのように「スキーマ付きの
enable_extensionは書くが、create_schema "extensions"が出力されない状態になっていた。これを新規データベースに対して
db:schema:loadすると、PostgreSQL 側にまだextensionsスキーマが存在しないため:textPG::InvalidSchemaName: ERROR: schema "extensions" does not existで失敗していた。
#58855 では「スキーマが固定されている拡張」(例:
postgis_tiger_geocoder)については対応済みだが、btree_gin/pg_trgm/hstoreのような「移動可能(control ファイルに schema 指定がない)」拡張は依然として壊れたままだった。
この PR での方針
「ダンプ対象(
dump_schemas)に含まれるかどうか」に関わらず、- スキーマ修飾された拡張がダンプされるなら、それが属しているスキーマについては
create_schema "schema_name", if_not_exists: trueを出力するように変更。
結果として、schema.rb には例えば次のような形で出力される:
rubyActiveRecord::Schema[8.2].define(version: 0) do create_schema "extensions", if_not_exists: true enable_extension "extensions.btree_gin" enable_extension "plpgsql" endこれにより:
- スキーマがまだ存在しない新規 DB では
create_schemaがスキーマを作成し、enable_extensionが成功する。 - 既にスキーマが存在する環境(例: Supabase の
extensionsスキーマ)ではIF NOT EXISTSによってPG::DuplicateSchemaが防がれる。
- スキーマがまだ存在しない新規 DB では
実装上のポイント
activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb の主な変更:
schemasメソッド内で:connection.extensionsを一度だけ取得し、そこから- ダンプされる拡張のうち、「スキーマ修飾されているもの」のスキーマ名を抽出。
- そのスキーマに対して
create_schema "xxx", if_not_exists: trueをダンプ対象に追加。
- ただし、以下のスキーマは除外:
public(常に存在するし、明示的に作る必要がない)pg_*系システムスキーマ- PostgreSQL は
CREATE SCHEMA IF NOT EXISTS pg_catalogのようなものをそもそも受け付けない(pg_プレフィックスはシステムスキーマ用に予約されている)ため。
- PostgreSQL は
- 既存のダンプで
create_schema "xxx"のみだった箇所についても、次回のダンプではcreate_schema "xxx", if_not_exists: trueに変わる(互換性維持 + 冪等性向上)。 connection.extensionsの呼び出しをschemasとextensionsの両方で共有するようにして、無駄なクエリを減らしている。
テスト
- 新しいテストファイル
activerecord/test/cases/adapters/postgresql/extension_schema_dump_test.rbが追加され、- 再現スクリプト同様、カスタムスキーマに relocatable extension を入れて、
- ダンプ → DB を空に戻す → ダンプをロード
- までを通してパスすることを確認。
- 変更前はこのテストが落ちることも確認済み。
- 既存の PostgreSQL 関連テスト (
test/cases/adapters/postgresql/**/*_test.rbなど) も PostgreSQL 14 でグリーン。
- 影響範囲・注意点
- 影響を受けるケース:
- PostgreSQL を使用している Rails アプリで、
- 拡張(
btree_gin,pg_trgm,hstoreなど)をpublic以外のスキーマにインストールしており、 ActiveRecord.dump_schemasがそれらのスキーマを含んでいない(例:"public"のみ)場合。
- そのようなアプリでは:
- これまで
db:schema:load時にスキーマ不足で失敗していたのが、正常にロードできるようになる。
- これまで
- 生成される schema.rb の差分:
- カスタムスキーマに拡張がインストールされている場合、
create_schema "xxx", if_not_exists: trueが先頭付近に追加される。 - 既に手書きや過去バージョンで
create_schema "xxx"が入っている場合も、次回ダンプでif_not_exists: trueが付加される。
- カスタムスキーマに拡張がインストールされている場合、
- 互換性・副作用:
- 既にスキーマが存在する本番 DB でも、
CREATE SCHEMA IF NOT EXISTSは無害(何もしない)なので挙動は変わらない。 pg_*およびpublicは除外しているため、システムスキーマに対する不正なCREATE SCHEMAは発行されない。- 拡張そのものを「ダンプから省く」という選択肢(dump_schemas 外のスキーマにある拡張は有効化しない)はあえて採用しておらず、「アプリが必要としている拡張は必ず有効にする」側に振っている点に注意。
- 既にスキーマが存在する本番 DB でも、
- 参考情報 (あれば)
- 元 PR・関連 PR:
- PostgreSQL の拡張の「relocatable」については、extension の control ファイルの
relocatable/schema設定を参照すると挙動が分かります。 - Supabase など、既に
extensionsスキーマを持つマネージド PostgreSQL 環境とも両立する設計になっています。
#59065 Reduce method calls in fast path
マージ日: 2026/10/14 | 作成者: @tenderlove
- 概要 (1-2文で)
primary_key_definitionメソッドの「よく通るパス(fast path)」でのメソッド呼び出し回数を 2 回から 1 回に削減し、パフォーマンスを改善するための微調整が行われました。機能的な挙動は変えずに、内部実装の呼び出しコストを下げるリファクタリングです。
- 変更内容の詳細
※PR本文と変更行数(+4/-2)から推測を含みますが、意図はほぼ明確です。
- 対象:
ActiveRecord::AttributeMethods::PrimaryKeyモジュール内のprimary_key_definitionメソッド - 背景:
primary_key_definitionは Active Record 内で頻繁に呼ばれる「ホットスポット」となっている- 以前は、毎回 2 回のメソッド呼び出しを行っており、そのオーバーヘッドが積み重なっていた
- 変更点:
- 「fast path」では、1 回のメソッド呼び出しで済むようにロジックを再構成
- 条件分岐により、一般的なケース(fast path)ではキャッシュされた値もしくは計算済みの値に直接アクセスし、余計なメソッドチェーンを避ける
- 例外的なケース(slow path)のみ、従来通り 2 回のメソッド呼び出しを行う
おおよそ以下のようなイメージです(擬似コード):
def primary_key_definition
# 以前は毎回このような呼び出しをしていたイメージ
# primary_key, columns_hash など、2つ以上のメソッドを毎回呼んでいた
# after:
if fast_path_condition
# fast path: すでにわかっている/キャッシュ済みの情報から
# 1 回の呼び出しで値を取得
@primary_key_definition ||= compute_primary_key_definition_once
else
# slow path: レアケースでのみ、従来通り複数メソッド呼び出しで詳細を解決
compute_primary_key_definition_with_more_checks
end
end実際には、以下のようなことが行われている可能性が高いです:
@primary_key_definitionのようなインスタンス変数キャッシュを強化primary_key_definition内で、すでに確定している主キー定義を条件付きで即返却する- それ以外の場合のみ、
columns_hashなどのコストの高いメソッドを呼ぶ
行数の増加が少ないことから、分岐追加とインスタンス変数キャッシュのような軽微な変更に留まっていると考えられます。
- 影響範囲・注意点
- 影響範囲:
- Active Record の主キー関連のメタデータ取得(
primary_key_definitionを内部的に利用する処理全般) - 特にテーブル定義情報を頻繁に参照するコードパス(クエリ生成、スキーマ情報アクセスなど)でのわずかなパフォーマンス改善が見込まれます。
- Active Record の主キー関連のメタデータ取得(
- 後方互換性:
- メソッドの外部インターフェイス(返り値や例外など)は変わらず、内部の実装効率のみを改善しているため、通常のアプリケーションコードには互換性上の影響はほぼありません。
- 注意点:
- キャッシュ・メモ化を強める方向の変更である場合、
- マイグレーション中にスキーマが変わった場合
- 接続先 DB のメタ情報が動的に変わるような特殊ケース
で、最新の主キー定義が即座に反映されない、といった懸念が理論上はありえます。
- ただし Rails 本体としては、従来の挙動を維持するよう慎重に設計されているはずであり、通常の運用では問題にならない想定です。
- キャッシュ・メモ化を強める方向の変更である場合、
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59065
- 関連箇所:
activerecord/lib/active_record/attribute_methods/primary_key.rb- Active Record のスキーマ・メタデータ関連コード(
columns_hash,primary_key,schema_cacheなど)
- 類似の最適化:
- Rails ではホットパスに対して、メソッド呼び出し削減・インスタンス変数キャッシュ・条件分岐の順序最適化などのマイクロ最適化が定期的に行われています。
- 今回もその一環で、特に高トラフィックな環境や大量のモデルを扱うアプリケーションで、地味ながら有効な改善となります。
#59064 Stop directly asking for columns from Key
マージ日: 2026/10/14 | 作成者: @tenderlove
- 概要 (1–2文で)
Key クラスの内部実装である@columnsを外部から直接参照しないようにし、belongs_to関連の実装を Key の公開インターフェイス経由で扱う形にリファクタリングした PR です。これにより ActiveRecord::Key のカプセル化が強化され、将来的な内部実装の変更に耐えやすくなっています。
- 変更内容の詳細
Key クラス側の変更
activerecord/lib/active_record/key.rb に、従来インスタンス変数 @columns に直接アクセスしていた呼び出し側のための公開メソッド (もしくはそれに類する API) が追加されています。
イメージとしては次のような変更です(実際のコードは多少異なる可能性がありますが、意図はこうした形です):
# 変更前 (呼び出し側がやっていたこと)
key = ActiveRecord::Key.new(...)
columns = key.instance_variable_get(:@columns) # あるいは key.@columns 的な直接参照
# 変更後 (Key がカラム情報を提供するメソッドを持つ)
key = ActiveRecord::Key.new(...)
columns = key.columns # あるいは同等の公開 APIPR コメントの通り、「@columns は実装詳細なので呼び出し元から直接アクセスさせたくない」という意図で、Key のフィールドを隠蔽するためのアクセサやラッパーメソッドが追加されています。
belongs_to_association 側の変更
activerecord/lib/active_record/associations/belongs_to_association.rb では、belongs_to 関連のロード・設定処理の中で、これまで Key の @columns に直接依存していたコードが、上記で追加された Key の公開メソッドを使うように変更されています。
たとえば、外部キーや主キーを比較したり、関連オブジェクト側のカラムを参照して値を読み書きしていた箇所が、次のようにリファクタリングされています。
(あくまでイメージコード)
# 変更前
if key.@columns.size == 1
# カラム1つのときの処理
end
key.@columns.each_with_index do |col, i|
owner[col.name] = target[key_columns[i].name]
end
# 変更後
if key.columns.size == 1
# カラム1つのときの処理
end
key.columns.each_with_index do |col, i|
owner[col.name] = target[key.columns[i].name]
endポイントは、「Key の内部配列(@columns)」に直接触るのではなく、「Key が提供する API(columns など)」を通して情報を得るように書き換えられている点です。
これにより、Key の内部実装(配列で持つのか、別のオブジェクトで持つのか等)は呼び出し側から切り離され、Key クラス単体で自由に変更可能になります。
- 影響範囲・注意点
主な影響範囲
- ActiveRecord 内部の
belongs_toアソシエーション実装(特に外部キー・主キーを扱う処理)。 - ActiveRecord::Key の内部構造に依存していたコード(Rails 本体の中でも、この PR 時点では主に belongs_to_association のみが対象)。
- ActiveRecord 内部の
アプリケーションコードへの影響
- 通常の Rails アプリケーションでは、ActiveRecord::Key を直接扱うことは滅多にないため、基本的には互換性への影響はありません。
- もし gem やアプリ側で
ActiveRecord::Keyの@columnsに直接アクセスしているようなメタプログラミングをしている場合、この PR 以後は動かなくなる(もしくは将来的に壊れる)可能性が高いです。その場合は:- 公開メソッド(
columnsのような API)を利用する - それがない場合はそもそも Key の内部に依存する設計を見直す といった対応が必要です。
- 公開メソッド(
今後の拡張のしやすさ
@columnsを外部から隠蔽したことで、将来的に Key の内部表現を変えたり、Key に関連するロジックを改善したりする際に、呼び出し側との互換性を維持しやすくなります。- 複合キー対応や、キー定義周りのさらなるリファクタリング・最適化のための布石となる変更と考えられます。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59064
- 同系統の変更が続いている場合、ActiveRecord::Key やアソシエーション関連コード全体のリファクタリング・カプセル化が進行中である可能性があります。
- Rails 内部 API(特に ActiveRecord の関連やスキーマ情報クラス)を直接触る gem を書いている場合は、Rails のマイナーバージョンアップ時にこうした内部変更を追従できるよう、テストと依存箇所の洗い出しをしておくと安全です。
#59063 Introduce "definition" methods that return ActiveRecord::Key
マージ日: 2026/10/14 | 作成者: @tenderlove
- 概要 (1-2文で)
ActiveRecord の関連・主キー周りで使われるActiveRecord::Keyオブジェクトの生成を抑えるため、「definition メソッド」を導入し、Key自体をキャッシュして再利用するようにした PR です。これにより複合キー情報の扱いを効率化しつつ、includesなどの関連ロード時のオブジェクトアロケーションと実行時間を削減しています。
- 変更内容の詳細
2-1. 何が問題だったか
- 以前は「主キー名・外部キー名などの“名前”」自体はキャッシュしていましたが、
- 複合キー対応のために導入された
ActiveRecord::Keyオブジェクトは 毎回新しく生成 されていました。 - そのため、
Post.includes(:comments)のような典型的なクエリでも、多数のKeyインスタンスが生成されてしまい、アロケーション数と処理時間が悪化していました。
ベンチマーク(100件の Post + 各 20件の comments):
| allocations per run | ms per run | |
|---|---|---|
| v8.1.4 | 37,649 | 18.7 |
| Before (複合キー寄りの変更後) | 62,216 | 31.3 |
| After (この PR) | 50,216 | 27.8 |
複合キー関連の最適化追加で一度大きく悪化したパフォーマンスを、Key 自体のキャッシュによってある程度取り戻している、という位置づけです。
2-2. 「definition メソッド」とは何か
この PR のポイントは、Key の定義を返す専用メソッドを導入し、それをキャッシュの単位にしたことです。
イメージとして:
以前:
- ある関連の外部キーを参照するたびに
ActiveRecord::Key.new(...)を呼び出しがち - 「キー名」はキャッシュされていても、「
Keyオブジェクト」は都度生成
- ある関連の外部キーを参照するたびに
今回:
- 「主キー」「外部キー」「スコープキー」などごとに **
definition_*** メソッド** を導入し、そこから返されるActiveRecord::Key` をキャッシュ - 以降の利用側は「名前ではなく
Key定義」を参照する
- 「主キー」「外部キー」「スコープキー」などごとに **
概念的なコード例(あくまでイメージ):
# 以前(イメージ)
def foreign_key
@foreign_key ||= compute_foreign_key_name # 文字列/シンボル
end
def foreign_key_key
ActiveRecord::Key.new(foreign_key) # 毎回 new
end
# 今回(イメージ)
def foreign_key_definition
@foreign_key_definition ||= ActiveRecord::Key.new(compute_foreign_key_name)
end
# 呼び出し側は foreign_key_definition を受け取るので、
# そのまま Key オブジェクトとして扱える(再利用される)この「definition メソッド」が PR タイトルの “Introduce "definition" methods that return ActiveRecord::Key” に対応します。
2-3. 主な変更箇所と役割
※実ファイルの実装はもう少し複雑ですが、役割ベースで整理します。
activerecord/lib/active_record/key.rb (+58/-3)
ActiveRecord::Key自体が強化されています。Keyオブジェクトを「ある列群の定義」として長く保持し、何度も利用する前提に設計を寄せています。- 複合キーや関連テーブル同士の join 条件などに利用しやすい API を整備している可能性が高いです(テスト追加もこのファイルに集中している)。
activerecord/lib/active_record/reflection.rb (+116/-55)
- 最大の変更点で、関連リフレクション (
belongs_to,has_many,has_oneなど) が持つ「キー情報」の扱いが見直されています。 - 主な変更内容:
- これまで文字列やシンボルで持っていたキー名に対応して、
xxx_key_definitionのようなメソッドを追加。 - その中で
ActiveRecord::Keyを生成し、インスタンス変数にキャッシュ。 - 呼び出し側(association, scope, predicate builder など)は、キー名ではなく
Keyオブジェクトそのものを参照するように変更。
- これまで文字列やシンボルで持っていたキー名に対応して、
これにより、「リフレクション -> キーの定義 -> 実際のクエリ/比較」と一貫したフローで Key が共有されます。
各種 Association 関連 (association.rb, belongs_to_association.rb, collection_association.rb, foreign_association.rb, has_one_association.rb)
- 関連オブジェクトが、これまでは
reflection.foreign_key(シンボル/文字列)reflection.active_record_primary_keyなど「名前ベース」で扱っていた部分を、reflection.foreign_key_definitionreflection.primary_key_definitionのような「Key定義ベース」のアクセスに切り替えています。
- とくに
belongs_to_association.rbは +40/-18 と差分が大きく、belongs_toの- owner 側のキー
- target 側のキー
- optional / required によるキー扱い などで、
Keyを中心にした処理フローへ書き換えられていると考えられます。
クエリビルダ関連 (association_scope.rb, relation/predicate_builder/*.rb, relation/query_methods.rb)
WHERE条件や JOIN 条件を組み立てる際に、キーの組み合わせをActiveRecord::Key経由で扱うようになっています。- 具体的には、関連クエリ (
includes,eager_load,preload) や polymorphic 関連 (polymorphic_array_value) で、キーを比較・マッピングする処理がKeyの API を利用するように統一されています。
autosave_association.rb, persistence.rb, schema_context.rb
- 保存時やスキーマ関連で参照するキーも、可能な範囲で Definition ベースに切り替えられています。
- 特に
persistence.rbは +5 行と少ないですが、save/update時に主キーや外部キーを扱う箇所でKeyの再利用が進んでいると見られます。
テスト: activerecord/test/cases/key_test.rb (+36/-0)
ActiveRecord::Keyのふるまいや、definition メソッドを通した利用の挙動を検証するテストが追加されています。- これにより、今後複合キーや関連キー周りのリファクタリングを行っても、
Keyのインターフェース保証がしやすくなります。
- 影響範囲・注意点
3-1. 影響範囲
- 主に影響を受けるのは ActiveRecord 内部 API と、非公開 API に依存したコード です。
- 具体的には:
ActiveRecord::Reflectionの属性やメソッドのうち、「キー情報」を返していたものの挙動や型が微妙に変化している可能性があります。- 内部的には「文字列/シンボルのキー名」から「
ActiveRecord::Keyオブジェクト」への移行が進んでいるため、reflection オブジェクトのインスタンス変数や private メソッドを直接触っているコードは壊れるリスクがあります。 - association 関連クラスの挙動も
Keyを前提にリファクタリングされているため、Monkey Patch や独自 association 実装をしている場合は注意が必要です。
通常のアプリケーションコード(公開 API を素直に使っている範囲)であれば、挙動は互換で、パフォーマンスのみが改善される想定です。
3-2. 注意点・マイグレーションの観点
ActiveRecord::Keyや「definition」系メソッドは、現時点では 内部実装寄りの API と考えた方が安全です。- これに直接依存するのではなく、公開された reflection メソッドや association メソッドを使う方が将来の変更に強くなります。
- もし gem や engine 内で、reflection の内部に手を入れている場合:
- 主キー / 外部キーに関するメソッドの戻り値型(シンボル/文字列想定か
Key想定か)を暗黙に前提としていないか確認してください。 - Reflection のインスタンス変数 (例:
@primary_key,@foreign_key等) を直接参照している場合は、今回の変更で構造が変わっている可能性があるため、Rails 本体のコードと diff をとって見直すことを推奨します。
- 主キー / 外部キーに関するメソッドの戻り値型(シンボル/文字列想定か
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59063
- 関連しそうな内部クラス・モジュール:
ActiveRecord::KeyActiveRecord::Reflection::AssociationReflectionActiveRecord::Associations::BelongsToAssociationなど各種 association クラスActiveRecord::Relation::PredicateBuilder
- ベンチマークから読み取れるポイント:
- v8.1.4 より allocations / 実行時間はまだ増えているものの、複合キー/
Key도입後に悪化したパフォーマンスをある程度巻き戻している段階の PR と見なせます。 - 今後さらに
Key周辺が最適化される可能性があります。
- v8.1.4 より allocations / 実行時間はまだ増えているものの、複合キー/
#59062 Clean up how a relation's join values are sorted into the query
マージ日: 2026/10/13 | 作成者: @kamipo
- 概要 (1-2文で)
ActiveRecord のリレーションに含まれるjoins情報の分類・並べ替えロジックが整理され、処理フローが単純化・一貫化されました。buckets[:stashed_join]まわりのデータ構造と join 値の分類処理がリファクタリングされ、CTE / 通常の Arel ノード / アソシエーション join の扱いがより明示的になっています。
- 変更内容の詳細
2-1. build_join_buckets の整理
これまで:
build_join_bucketsはbucketsというHashを返却しており、buckets[:stashed_join]は以下のように複数回のconcat/unshiftで組み立てられていました。concat/unshiftの順序で join の最終的な並びが決まる- どこに何が入るか追いにくく、join 順序バグの温床になりやすい
この PR で:
bucketsはHashではなくJoinBucketsというStructベースのような専用オブジェクトになりました。- 例:
JoinBuckets.new(stashed: [...], ...)という形で、キー名がプロパティに昇格したイメージ
- 例:
buckets[:stashed_join]として扱われていた join 群は、一つの配列に統合され、どこでどの順番で積まれるかがコード上で追いやすくなっています。Hash+ default block +[join, join_type]いったタプル形式の雑多な扱いから、型(構造)で表現する方向に整理されています。
狙い:
- join の順序バグ修正 (#59059, #59060) で浮き彫りになった「配列の前後に unshift / concat していくスタイルだと可読性と予測可能性が低い」問題を根本的に解消するリファクタリング。
2-2. select_named_joins → classify_join_values への切り出し・一本化
これまで (select_named_joins の問題点):
select_named_joinsは:- 戻り値として「アソシエーション名の配列」を返す
- 引数で「stashed join dependencies 用の配列」を受け取り、副作用的に詰めていく (out parameter)
- ブロックに対して CTE join と Arel ノードをそのまま渡し、
- 呼び出し側が
CTEJoinラッパーの有無で CTE かそうでないかを再判定して分岐
- 呼び出し側が
- さらに、
- CTE を弾く処理が
select_named_joinsにはあるが、 - 似た役割を持つ
select_association_listにはそれがなく、#59061 で不整合が指摘された
- CTE を弾く処理が
この PR で:
select_named_joinsの責務を整理し、join 値の分類ロジックをclassify_join_valuesとして切り出し/再設計。classify_join_valuesの役割:- リレーションに設定された
joins_values(配列)を一つずつ見て、- アソシエーション名ベースの join
- Arel ノードベースの join
- CTE join
- その他(カスタム join, SQL 文字列など)
- といった「これは何の join なのか」を判定して分類結果を返す。
- リレーションに設定された
- これにより:
- 各呼び出し側は
classify_join_valuesの結果だけを見て一度だけ dispatch すればよい - 以前のように「
select_named_joinsでブロックに渡したものを、さらにCTEJoinかどうかで再分岐する」といった二重ディスパッチが不要になった
- 各呼び出し側は
イメージコード(擬似的な例・実コードとは多少異なります):
# 以前(イメージ)
select_named_joins(joins_values, stashed = []) do |join|
case join
when CTEJoin
handle_cte_join(join)
when Arel::Nodes::Join
handle_arel_join(join)
else
# ...
end
end
# stashed に副作用で join も詰める
# 今回の整理後のイメージ
buckets = classify_join_values(joins_values)
buckets.named_associations.each { |name| handle_association_join(name) }
buckets.cte_joins.each { |cte| handle_cte_join(cte) }
buckets.arel_joins.each { |arel| handle_arel_join(arel) }
# それぞれの bucket 単位で一度だけ dispatch効果:
- CTE join と通常 join の振る舞い差分が、
classify_join_valuesおよびJoinBucketsの構造に反映され、コード上で明示的になる。 - CTE フィルタリングの有無に関する
select_named_joins/select_association_list間の不整合が解消される。 - コードの責務分離(分類と利用側のロジック切り分け)により、将来的な join 種別追加・挙動変更が容易。
2-3. finder_methods.rb の変更
finder_methods.rb には +5 / -5 行の小さな変更が入っており、主に:
- 新しい join 分類の API (
classify_join_values/JoinBucketsなど) に合わせてincludes/eager_load/preloadといった finder 系メソッドが参照する join 情報の取り出し箇所が微調整されています。 - ロジック自体(どのアソシエーションを eager load するか)が大きく変わるのではなく、「どこから join 情報を取ってくるか」が整理された形に。
- 影響範囲・注意点
アプリケーションコードから見える動作:
joins,left_joins,includes,eager_load,preload,references,with(CTE) など、join 関連 API の振る舞いは後方互換性を意図した変更です。- ただし、この PR は #59059, #59060 で触れられている「join の並び順に関するバグ」の修正を前提にしたクリーンアップなので、
- 以前「たまたま」動いていた join 順依存のクエリが、正しい順序に矯正されることで挙動が変わる可能性はあります。
- 主に複数の join が同じテーブル・同じエイリアスを共有するような高度なケースや、CTE を多用しているクエリは要注意。
カスタム Arel / CTE を使っている場合:
joins(Arel::Nodes::Join.new(...))のような低レベル join の組み立てや、with(:cte_name, ...)などで CTE を多用している場合、- join がクエリにどの位置で差し込まれるか(例: FROM の直後か、別の join の後か)が微妙に変わる可能性があります。
- 特に、
LEFT OUTER JOINとINNER JOINの組み合わせや、ON句の依存関係が複雑なケースでは、実行計画レベルで変化しないか確認を推奨します。
モンキーパッチ・内部 API 依存:
ActiveRecord::Relationの内部でbuckets[:stashed_join]に直接アクセスしていたようなモンキーパッチ・プラグインがあれば壊れます。select_named_joinsに直接依存している gem / ライブラリも挙動が変わる、あるいはコンパイルできなくなる可能性があります。- 内部実装依存のコードがないか確認が必要です。
- 参考情報 (あれば)
- この PR が前提にしている・関連する PR:
- #59059: join の順序に関する修正 1
- #59060: join の順序に関する修正 2
- #59061:
select_named_joinsとselect_association_list間の CTE フィルタリング不整合の修正
- 実装の読み方:
activerecord/lib/active_record/relation/query_methods.rb内で、joins_valuesをどのようにclassify_join_valuesが分類し、JoinBucketsがどういうフィールドを持っているか
- を追うと、join が SQL 文字列に落ちるまでの経路が分かりやすくなっています。
- マイグレーションの必要はなく、スキーマや DB には影響しません。アプリ側で必要なのは主にテストによる動作確認です。
#59061 Fix eager_load with a limit on a relation that joins a CTE
マージ日: 2026/10/13 | 作成者: @kamipo
- 概要 (1-2文で)
CTE を使ったwith+joinsなリレーションにlimitとeager_loadを組み合わせたときに発生していたActiveRecord::ConfigurationErrorを修正する PR です。CTE を誤って関連名として解釈していたロジックを見直し、limitを安全に適用できるかどうかの判定方法が改善されています。
- 変更内容の詳細
問題の内容
次のようなコードで例外が発生していました:
Post.with(commented_posts: Comment.select(:post_id))
.joins(:commented_posts)
.eager_load(:author)
.limit(2)
# ActiveRecord::ConfigurationError:
# Can't join 'Post' to association named 'commented_posts'; perhaps you misspelled it?Post.with(commented_posts: ...)により、commented_postsという名前の CTE を定義.joins(:commented_posts)でその CTE を JOIN.eager_load(:author).limit(2)を組み合わせたときに、内部処理がcommented_postsを「関連名 (association)」として扱おうとし、ConfigurationErrorが投げられていました。
元々、eager_load + limit の組み合わせでは、
includes/eager_loadを行うときにlimitを join 済みのメインクエリ側に安全に押し込めるか(N+1 を避ける最適化)
を判断するため、select_association_list で「どの関連が join されているか」を解析していました。この過程で CTE 名も「関連名」として誤検出されてしまっていたのがバグの原因です。
今回の修正のポイント
1) 「limit を押し込めるか?」の判定ロジックを using_limitable_joins? に変更
eager_load 時に limit をどう扱うかの判定が、以下のようなより包括的なメソッドに差し替えられています:
using_limitable_joins?概念的には、次のようなことを行うメソッドです(実装イメージ):
- リレーションが
- has_many 等の「多側」関連を join していないか
- CTE など「1レコードの親に対して複数レコードが紐づく可能性があるもの」を join していないか
- そういった「多重に行が増えうる join」がある場合は、
limitをメインクエリにそのまま適用するとレコード数がずれる可能性があるため、falseを返す
この PR では、CTE を join しているリレーションに対して using_limitable_joins? が false を返すように なりました。
「CTE はモデルの外部キーで結合され、多数行を返しうるので、コレクション関連と同様に扱うべき」という設計判断。
2) CTE 名を関連名として扱わないように
旧来の実装では「join されている名前」を元に関連かどうかを判定していたため、commented_posts のような CTE 名も Post の関連名だとみなそうとして、結果的に
Can't join 'Post' to association named 'commented_posts'というエラーが発生していました。
今回の修正により、
- CTE を join しているケースは「limit を安全に押し込めない join」として扱う
- CTE 名を無理に関連として解釈しない
という方向にロジックが変わっています。
3) テストの追加
activerecord/test/cases/relation/with_test.rb にテストが追加されています。主な意図は:
with+ CTE を定義- それを
.joins+.eager_load+.limitで組み合わせたときに- 例外が発生しないこと
- 期待通りのクエリ・挙動になること
を保証するものです。
- 影響範囲・注意点
影響範囲
- 対象: CTE (
with) を使ったリレーションにjoins+eager_load+limitを組み合わせているケース- これまで
ActiveRecord::ConfigurationErrorが出ていたケースが、正しくクエリとして実行されるようになります。
- これまで
- 一般的な
includes/eager_load+limitの挙動にも、内部的には判定メソッドがusing_limitable_joins?に変わっているため、間接的な影響を受ける可能性があります。- ただし趣旨としては「危険な場合に limit を押し込まない」方向の保守的な変更であり、破壊的変更というよりは 安全側に倒した最適化 です。
注意点
- CTE を join しているリレーションでは、
using_limitable_joins?がfalseになるため、limitを押し込む最適化が行われない- 結果として、生成される SQL が微妙に変わりうる(副問い合わせ経由など)
ことがあります。
- もし、CTE を使った高度なクエリで
- 「特定のクエリプラン・SQL 形状」に依存しているようなコードや最適化を行っている場合は、Rails バージョンアップ後に実行される SQL を一度確認した方が安全です。
- 逆に、「今まで CTE + eager_load + limit を書くと落ちるので諦めていた」ようなコードは、この PR を含むバージョンでは普通に動作するようになります。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59061
- 関連箇所:
ActiveRecord::Relation::FinderMethodsActiveRecord::Relation::QueryMethods
- CTE (
with) とeager_load/includes/preloadを併用する際は、
「関連として解決される名前」 (has_many :comments) と
「CTE 名や Arel テーブル名」が衝突したときの挙動に注意する必要がありますが、
今回の修正で少なくとも「安易に関連扱いして例外になる」ケースは減っています。
#59060 Fix join order when left_joins is combined with merged joins
マージ日: 2026/10/13 | 作成者: @kamipo
- 概要 (1-2文で)
left_joinsとmergeで取り込んだjoinsを組み合わせたときに、SQL の JOIN 順序が不正になり、存在しないテーブルを参照するようなクエリが生成される問題を修正した PR です。left_joins由来の JOIN を「最初の方」に出すようにし、後からマージされる JOIN が正しく既存テーブルを参照できるようにしています。
- 変更内容の詳細
問題の背景
Post.left_joins(:comments).merge(Comment.joins(:ratings)) のようなクエリを考えます。
Post.left_joins(:comments)- Post を基点に
commentsを LEFT OUTER JOIN する
- Post を基点に
Comment.joins(:ratings)- Comment を基点に
ratingsを INNER JOIN する
- Comment を基点に
.merge(Comment.joins(:ratings))Comment.joins(:ratings)側の relation に含まれるjoinsを、元のPost.left_joins(:comments)側にマージする
ここで重要なのは:
left_joinsに渡した関連 (:comments) から組み立てられるJoinDependencyは、常に「ベーステーブル (ここでは posts)」を根にしているため、他の JOIN に依存しません。- 一方、
Comment.joins(:ratings)からマージされる JOIN は、commentsテーブルが既に存在していることを前提としてratingsを JOIN します。
正しい SQL としては、少なくとも comments が ratings より前に JOIN されている必要があります。
不具合の内容
既存の build_join_buckets のロジックでは、JOIN の出力順が次のようになっていました:
- eager load 用の join dependency
joins_valuesに溜めていた JOIN (merge された JOIN など)left_joins由来の join dependency
このため、次のような不正な順序のクエリが生成されていました:
Post.left_joins(:comments).merge(Comment.joins(:ratings))
# 実際に生成されていた SQL のイメージ
SELECT ... FROM "posts"
INNER JOIN "ratings" ON "ratings"."comment_id" = "comments"."id"
LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"ここでは ratings が comments より先に JOIN されているため、ON "ratings"."comment_id" = "comments"."id" で、まだ存在していない comments テーブルを参照してしまいます。DB によってはエラー、もしくは期待しない動作につながります。
PR の修正内容
この PR では、build_join_buckets の JOIN の並び順を変更し、以下の順に出力されるようにしています:
- eager load 用の join dependency
left_joins由来の join dependency ← ここに移動joins_valuesからの JOIN (merge された JOIN など)
つまり、left_joins によって結びつけられるテーブルが先に JOIN され、その後に merge から持ち込まれる JOIN が続くように変更されました。
これにより、上記の例は次のような正しいクエリになります:
SELECT ... FROM "posts"
LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"
INNER JOIN "ratings" ON "ratings"."comment_id" = "comments"."id"実装箇所
activerecord/lib/active_record/relation/query_methods.rbbuild_join_bucketsで JOIN をグルーピング・ソートする処理の順序を変更
→left_joinsによる join dependency を、joins_values起源の JOIN よりも前に評価するようにした。activerecord/test/cases/associations/left_outer_join_association_test.rb
失敗していた、または新たに必要になったテストを追加し、left_joins + merge(Comment.joins(...))のようなパターンで正しい SQL が出ることを検証。activerecord/CHANGELOG.md
この不具合と修正内容を CHANGELOG に追記。
- 影響範囲・注意点
対象:
left_joinsとmergeを組み合わせ、そのmerge側でさらにjoinsを行っているようなケース
例:rubyPost.left_joins(:comments).merge(Comment.joins(:ratings)) User.left_joins(:posts).merge(Post.joins(:tags))
期待できる効果:
- これまで DB によっては以下のような症状が出ていた可能性があります:
- 「テーブル X が存在しない / 参照できない」といった SQL エラー
- JOIN 順序起因の実行計画の不整合
- この PR により、これらのケースで JOIN 順序が論理的に正しくなり、テーブルの参照関係が守られるようになります。
- これまで DB によっては以下のような症状が出ていた可能性があります:
互換性:
- JOIN 順序が変わることで、同じ relation チェーンでも生成される SQL の並びが変わる可能性があります。
- 結果セット自体は論理的には同じはずですが、DB の実行計画やパフォーマンスに影響する可能性はゼロではありません。
- SQL の文字列を直接期待値として比較しているテストがある場合、JOIN の並びが変わったことでテストが落ちることがあります。その場合は、新しい JOIN 順序に合わせて期待値の SQL を更新する必要があります。
- JOIN 順序が変わることで、同じ relation チェーンでも生成される SQL の並びが変わる可能性があります。
注意点:
- この修正は、「
left_joins由来の JOIN はベーステーブルにのみ依存しており、他の JOIN に依存しない」という前提に依拠しています。
もしアプリケーション側で Arel を使ってかなりトリッキーな JOIN を追加している場合、JOIN 順序の前提が変わることで副作用が出る可能性はありますが、通常の Active Record の使い方では問題になることはほぼありません。
- この修正は、「
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59060
- 関連 API:
ActiveRecord::QueryMethods#left_joinsActiveRecord::QueryMethods#mergeActiveRecord::QueryMethods#joins
- 同様のパターンで問題が起きやすいコード例:rubyこのような nested な merge でも、ベーステーブル → left_joins → それを前提とする joins の順に並ぶようになり、依存関係が破綻しにくくなります。
# 複数階層で merge + joins を行う場合 Post .left_joins(:comments) .merge( Comment.joins(:ratings) .merge(Rating.joins(:user)) )
#59059 Fix join order when eager_load is combined with merged joins
マージ日: 2026/10/13 | 作成者: @kamipo
- 概要 (1-2文で)
eager_loadとmerge由来のjoins/left_joinsを組み合わせたときに、SQL の JOIN 順序が逆転して不正なクエリになる問題を修正した PRです。eager_loadが生成する JOIN 群を、他のリレーションからマージされた JOIN よりも先に評価するようにして、参照順序の整合性を保証しています。
- 変更内容の詳細
問題の背景
eager_loadは内部的にJoinDependencyを組み立てて、ベーステーブル(FROM で指定されるモデル)を起点に関連テーブルを LEFT OUTER JOIN します。- 一方で、
mergeによって別モデルのリレーション(joins,left_joinsなどを含む)をマージすると、そのリレーション内の JOIN は、そのリレーションが導入したテーブルに依存することがあります。
問題となるパターンの例:
Post.eager_load(:comments).merge(Comment.left_joins(:user))期待されるイメージとしては:
postsをベースにcommentsをeager_loadで JOIN- その上で
Comment.left_joins(:user)によるusersへの JOIN を載せる
といった依存関係になります。
しかし、バグがあった時点では、JOIN の構築順が逆転していました。
問題の挙動
PR 説明にあるように、以前は build_join_buckets が
- スタッシュされていた JOIN(
mergeで持ち込まれた JOIN 群) eager_loadの JoinDependency
をこの順番で連結しており、結果として eager_load 由来の JOIN が後ろに来ていた ため、以下のような不正な SQL が生成されていました:
SELECT ... FROM "posts"
LEFT OUTER JOIN "users" ON "users"."id" = "comments"."user_id"
LEFT OUTER JOIN "comments" ON "comments"."post_id" = "posts"."id"ここでは users の ON 句で "comments"."user_id" を参照していますが、comments の JOIN がまだ後ろにあるので、JOIN の依存関係的に整合していません。
修正内容
PR の要点は:
build_join_bucketsにおいてeager_loadの JoinDependency を先にmerge等で持ち込まれた JOIN を後ろに 並べるように順序を逆転させたことです。
つまり、
[以前] merged_joins + eager_load_joins
[修正後] eager_load_joins + merged_joinsという形になります。
この変更により、eager_load が導入するテーブル(例: comments)が先に JOIN され、その後で Comment.left_joins(:user) のように comments に依存する JOIN を安全に書けるようになります。
テスト・ドキュメントの変更
activerecord/test/cases/relation/merging_test.rbeager_loadとmergeを組み合わせた際の JOIN 順序を検証するテストが追加されています。- これにより、今後のリグレッションを防止。
activerecord/CHANGELOG.md- この問題修正が CHANGELOG に追記されており、
Fixes #54181と関連 Issue も明記。
- この問題修正が CHANGELOG に追記されており、
query_methods.rb の差分は +1/-1 と非常に小さく、おそらく配列結合の順序入れ替えのような一点変更です。
- 影響範囲・注意点
影響範囲
影響を受けるのは主に以下のようなクエリです:
eager_load(またはincludes(...).eager_loadに解決されるケース)と- 別モデル側のリレーションを
mergeで組み合わせ - そのリレーションがさらに
joins/left_joinsを持っている
具体例:
Post.eager_load(:comments).merge(
Comment.left_joins(:user).where(users: { active: true })
)あるいは:
posts = Post.eager_load(:comments)
.merge(Comment.joins(:tags))こういったケースで、これまでは RDBMS によってはエラーになったり、意図しない実行計画・結果になった可能性がありますが、今回の修正により JOIN 順序が論理的依存関係に沿うようになります。
注意点
- JOIN の「順序」が変わるため、非常にまれに:
- 一部 RDBMS のクエリオプティマイザが異なる実行計画を選び、パフォーマンスが変化する
- 既にこのバグ前提で複雑な SQL を期待しているような(ほぼないはずの)コードが動き方を変える 可能性はあります。
- ただし、依存テーブルが左側に現れるのは SQL 的に自然であり、バグ修正として妥当な変更です。
- 実際には「今までたまたま動いていたが JOIN 定義としては危うかった」コードが、より安全になる方向の修正なので、互換性リスクはかなり低いと考えられます。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/59059
- 関連 Issue: https://github.com/rails/rails/issues/54181
- 該当クラス・メソッド:
ActiveRecord::Relation::QueryMethods#build_join_bucketsActiveRecord::Associations::JoinDependency
- 再現の確認をしたい場合:
- 自分のアプリで
eager_load+merge(OtherModel.left_joins(...))のようなクエリがあれば、to_sqlして JOIN 順序が期待どおりか確認できます。
- 自分のアプリで
#58848 Move adapter-specific migration compatibility into per-adapter behaviors
マージ日: 2026/10/13 | 作成者: @yahonda
- 概要 (1-2文で)
このPRは、「Migrationのバージョン互換ロジックのうち、DBアダプタ依存の部分」を各アダプタ専用のCompatibilityBehaviorクラスに切り出し、adapter_nameの文字列判定に依存しない仕組みに移行したものです。これにより、PostgreSQL/MySQL など組み込みアダプタだけでなく、サードパーティやサブクラスアダプタでも、より自然にバージョン互換を実装・拡張できるようになります。
- 変更内容の詳細
2-1. これまでの問題点
従来は ActiveRecord::Migration::Compatibility の各バージョンクラス(例: V5_0, V5_1, ...)の中で、
case connection.adapter_name
when "PostgreSQL"
# PostgreSQL 向けの挙動
when "Mysql2"
# MySQL 向けの挙動
endのように adapter_name 文字列で分岐していました。
この設計により:
- サードパーティアダプタが独自の互換挙動を追加したい場合、これらバージョンクラスを monkey patch するしかなかった
- PostgreSQL を継承した
"CockroachDB"などのアダプタは、実際には PostgreSQL と同じ schema_statements を使っていても、adapter_nameが一致せず互換処理が効かない
という問題がありました。
2-2. 新しい仕組みの全体像
今回のPRでは、「Migrationバージョン互換のアダプタ依存部分」を各アダプタ配下の CompatibilityBehavior に委譲しています。
主な構造:
- 抽象的な仕組み:
ActiveRecord::Migration::CompatibilityBehavior - アダプタ側のフック:
connection.compatibility_behavior_for(migration_class)- 各アダプタが、このフックを実装して、適切な Behavior クラスを返す
Migration#method_missingは、実際の schema 操作(create_table,change_columnなど)を、この Behavior インスタンスを経由して実行する
全体の流れ(ざっくり):
class ActiveRecord::Migration
def method_missing(method, *args, &block)
behavior = connection.compatibility_behavior_for(self.class).new(self, connection)
execute_operation(behavior, method, args, &block)
end
endexecute_operation が say_with_time などの共通処理を担当し、その中で Behavior が実際の schema_statements を呼び出します。
2-3. CompatibilityBehavior と名前空間
新たに compatibility_behavior.rb が導入され、そこに共通の Resolver ロジックが実装されています。
特徴:
- 各アダプタは、
PostgreSQL::CompatibilityBehaviorやMySQL::CompatibilityBehaviorといった「名前空間モジュール」を定義 - それぞれ
CompatibilityBehavior::Resolverをextendしてfor(migration_class)を実装- これにより、
- 「Migrationクラスが継承している一番古いバージョンに対応する Behavior」を解決する
- その Behavior クラスは「自分とそれ以前のバージョン」を一括で担当できる(下位互換をまとめて扱う設計)
- これにより、
- Behavior クラス内部では、従来のバージョンクラスと同様に、
superを呼びつつ引数だけを書き換える、というスタイルで挙動をオーバーライド
例(イメージに近い擬似コード):
module ActiveRecord::ConnectionAdapters::PostgreSQL
module CompatibilityBehavior
extend ActiveRecord::Migration::CompatibilityBehavior::Resolver
class V5_1 < ActiveRecord::Migration::CompatibilityBehavior
def change_column(table_name, column_name, type, **options)
# :datetime を :timestamptz 風に扱うなどの互換調整
type = :timestamptz if type == :datetime
super(table_name, column_name, type, **options)
end
end
end
endsuper 先は「一つ新しい Behavior or 基底クラス」で、最終的には元の adapter の schema_statements にたどり着くイメージです。
2-4. PostgreSQL / MySQL の挙動移動
PostgreSQL と MySQL について、今まで compatibility.rb の中に書かれていたアダプタ固有の調整が、それぞれのアダプタ側へ移りました。
- PostgreSQL:
PostgreSQL::CompatibilityBehaviorを新設Migration[5.1]のchange_column時の:datetimeの扱いdeferrable: trueを現在のオプション形へマッピングする挙動 など
- MySQL:
MySQL::CompatibilityBehaviorを新設Migration[5.1]のcreate_tableの互換調整など
- SQLite3:
- 特に互換 Behavior は定義しておらず、ベースの Behavior のみ
結果として migration/compatibility.rb からは adapter_name 判定が削除され、アダプタ毎のフォルダ直下に責務が移動しています。
2-5. Migration#execute_operation の役割変更
これまで Migration#method_missing が持っていた say_with_time ブロック(ログ出力や prefix/suffix の付与など)を、Migration#execute_operation に切り出しました。
execute_operationの役割:- ログに出すメッセージ(操作名とオプション)を構築
- テーブル名に prefix / suffix を適用して実行
- 実際の実行ストラテジーを呼び出す
これにより:
- Behavior 側は「マイグレーションに書かれたテーブル名そのまま」を受け取る
- 以前のバージョン互換クラスと同様の前提で挙動を記述できる
- 例: PostgreSQL 5.1 の
change_column互換では、必要に応じて自分で prefix を適用してからchange_column_defaultなどを呼ぶ
また、メッセージは「Behavior 適用後の最終的なオプション」を元に組み立てられるため、「実際に実行されるオプション」がログに反映されます。
なお、未知の操作を呼んだ場合は、以前のように名前を表示してからではなく、単純に NoMethodError を投げるようになりました。
2-6. V5.0 の primary key default: nil 注入
V5_0 の役割のひとつは、「5.1 以前と同じく、integer 系の primary key が plain integer カラムのままになるように default: nil を注入する」ことです。
- どの型を「integer 的」とみなすかはアダプタごとに異なるため、
V5_0自体はconnection.inject_legacy_primary_key_default?(type)を呼んで判断- デフォルト実装:
integer/bigintを対象にする - MySQL:
- 過去互換の事情(legacy bigint PK が auto_increment になっていた)により、
integerのみ対象- 関連PR: #59050
- 過去互換の事情(legacy bigint PK が auto_increment になっていた)により、
ここも、アダプタごとの振る舞いをアダプタ側でコントロールできるように整理されました。
2-7. サブクラスアダプタでの継承戦略
このPRの重要なポイントのひとつが、「PostgreSQL や MySQL を継承したアダプタ(例: CockroachDB)がどう振る舞うか」です。
- デフォルトでは:
- PostgreSQL / MySQL を継承するアダプタは、その親アダプタの
CompatibilityBehaviorも継承する
=> 文字列判定ではなくクラス継承に基づくため、「adapter_nameが違うせいで PostgreSQL の互換処理が効かない」という問題が解決
- PostgreSQL / MySQL を継承するアダプタは、その親アダプタの
- ただし、「親アダプタのある一部の互換挙動だけは要らない/変えたい」というケースもある
- その場合は
compatibility_behavior_forをオーバーライドし、- 親の Behavior を解決
- それを継承したサブクラスを用意し
- そこだけ特定の操作を上書きする
- その場合は
多くの組み込み互換挙動は「マイグレーションで省略されたオプションを埋める」だけなので、その場合は「サブクラス側でオプションを明示的に指定して super を呼ぶ」だけで済みます。
一方で、PostgreSQL の以下のようなケースでは super をスキップする必要があります:
deferrable: trueを現行の:initially_deferred相当などにマッピングする挙動:datetimeを現行の timestamp 系に変換する挙動
このため、PostgreSQL 7.0 の disable_extension では:
- マイグレーションで
force:が指定されていない場合のみforce: :cascadeを付与 - 既に
force:が指定されている場合は上書きしない
というように、「覆い隠さず、必要な場合だけ足す」スタイルに変更されています。
- 影響範囲・注意点
3-1. 一般的なアプリケーション開発者への影響
- 既存のマイグレーションの挙動そのものは、基本的に変わらない想定です(挙動そのものではなく、その実装場所が整理されたPR)
- ただし:
- ログに出るメッセージは「Behavior 適用後のオプション」が表示されるようになったため、微妙に表示が変わる可能性があります
- 不正な schema 操作名を呼んだときのエラーメッセージは、
NoMethodErrorのみに簡略化
通常のアプリ開発者は、特別な対応はほぼ不要です。
3-2. アダプタ開発者・サードパーティアダプタへの影響
- 互換挙動を実装/カスタマイズしたい場合、今後は以下のパターンを推奨:
- アダプタ側に
compatibility_behavior_for(migration_class)を実装 CompatibilityBehavior::Resolverを利用した Behavior 名前空間(例:MyAdapter::CompatibilityBehavior)を定義- 必要なバージョンの Behavior クラス(
V5_0,V5_1など)だけを定義し、superでチェーンする
- アダプタ側に
- 既に
ActiveRecord::Migration::Compatibility::Vx_yを直接 monkey patch しているような実装は、将来的な互換性面で危険度が増しているため、この新たな仕組みに移行したほうがよいです。
注意点として:
- サブクラスアダプタが「独自の Behavior 名前空間」を定義する場合、
- 親アダプタの Behavior クラスチェーンに途中から「差し込む」ことはできない(Ruby のクラス継承チェーンとして不可能)
- したがって「親の挙動を全部引き継ぎつつ、一部だけ追加/キャンセル」したい場合は、
- 親の
compatibility_behavior_forが返すクラスをベースにサブクラスを作り、そのクラスだけをオーバーライドする、というラップ戦略が推奨
- 親の
3-3. schema:load への影響
ActiveRecord::Schema[x.y]もMigration[x.y]と同じように Behavior 解決を行うため、db:schema:load実行時にも CompatibilityBehavior が適用されます。- つまり、「マイグレーション経由」と「スキーマダンプ経由」で同じバージョン互換挙動が適用されることが、より明示的に保証される方向になっています。
- 参考情報 (あれば)
- 元のスタックPR: #58162 と、それを置き換えた #58844, #58845, #58846, #58847
- このPRで参照されている関連PR:
- #59050 (MySQL の legacy bigint PK の auto-increment まわりの挙動 by @kamipo)
- 実装の中心となるファイル:
activerecord/lib/active_record/migration/compatibility_behavior.rbactiverecord/lib/active_record/connection_adapters/postgresql/compatibility_behavior.rbactiverecord/lib/active_record/connection_adapters/mysql/compatibility_behavior.rb
- テスト:
activerecord/test/cases/migration/compatibility_test.rbに新しい Behavior ベースの互換テストが多数追加されており、挙動の例として読むのに有用です。
#59020 Avoid database collisions for concurrent AR test runs
マージ日: 2026/10/13 | 作成者: @matthewd
- 概要 (1-2文で)
このPRは、Active Record のテストスイートをローカルで並列実行するときに、テストプロセス同士が同じデータベースを取り合って壊し合わないようにする仕組みを導入しています。アドバイザリロックと「スロット(複数のテスト用DB)」の概念を追加し、最大5並列程度まで安全にDBレベルで分離されたテスト実行ができるようになりました。
- 変更内容の詳細
全体像:「DB スロット」とアドバイザリロック
目的:
- AR テストスイートを複数プロセスで同時に走らせても、同じ DB/schema を書き換えてテストが不安定にならないようにする。
アプローチ:
スロット0 (デフォルトDB)
config.ymlに書かれた通常のテストDB (例:activerecord_unittest) を「スロット0」とみなす。- 最初に起動したテストプロセスはこの DB を使うが、アドバイザリロックを取り続ける。
スロット1〜4 (最大5スロット)
- 2つ目以降のテストプロセスは、スロット0のロック取得を試みる。
- 取得できなければ「このスロットは使用中」とみなし、代わりに:
DB名 + 数字サフィックス(例:activerecord_unittest1,activerecord_unittest2…) という新しいDB名を候補にする。- そのDBに対して同様にアドバイザリロックを試みる。
- 必要ならそのDBをオンデマンドで作成する。
- これを最大スロット数 (このPRでは5) まで試し、どこかのスロットでロックが取れれば、そのDBをそのプロセス専用のテストDBとして使う。
スロットが埋まっていた場合
- 全スロットでロック取得に失敗した場合は即座に例外を投げて失敗させる。
- これにより、DBを共有して誤ったテスト結果が出ることを防ぐ。
アドバイザリロック非対応DB
- アドバイザリロックがサポートされないDBでは、今まで通りの挙動になる (スロット機構は事実上使われない)。
- サポートしているDB (PostgreSQL, MySQL 系の一部など) ではこの新機構が動く想定。
主な実装ポイント
1) test/support/database_slot.rb の新設
このファイルがスロット管理の中核です (約100行追加)。
想定される責務:
- 現在のプロセスが使用する「スロット番号」を決定する。
- ベースとなるDB設定からスロット用のDB名を計算する。
- 例:
- スロット0:
activerecord_unittest - スロット1:
activerecord_unittest1 - スロット2:
activerecord_unittest2…
- スロット0:
- 例:
- アドバイザリロックの取得/解放を行う。
- 必要であればスロット用DBの作成を行う (権限があれば)。
実際のコードは省略されていますが、概念的には以下のような流れになります:
slot = ActiveRecord::TestFixtures::DatabaseSlot.acquire(config)
# slot.index => 使用スロット番号 (0〜4)
# slot.config => 実際に使うべきDB接続設定 (DB名を書き換え済み)2) test/support/config.rb の更新 (+24/-1)
- テスト用DB設定 (
ARTest.config相当) をロードするときに、DatabaseSlot を考慮した設定へ書き換える処理を追加。 - これにより、テストコード側は「スロットのことを意識せず」、常に
ARTest.configやActiveRecord::Base.connectionを使っていれば、裏で適切なスロットDBに接続される。
擬似イメージ:
config = load_original_config # config.yml を読む
slot = ActiveRecord::TestFixtures::DatabaseSlot.acquire(config["arunit"])
config["arunit"] = slot.config # DB名を書き換えた設定に差し替え3) activerecord/Rakefile の変更 (+16/-4)
rake test,rake test:mysql2,rake test:postgresqlなど、ARのテストRakeタスクで スロット対応の設定を使うように調整。- 複数プロセスで
rake testを叩いても、それぞれが異なるスロットに割り当てられる前提になる。
4) 各種テストコードの「テスト衛生 (test hygiene)」改善
多数のテストファイルで微調整が入っています。主な方向性:
- DB名をハードコードしないようにする。
- 例:
"activerecord_unittest"と直接書いていたものを、設定や接続情報から取得するように修正。
- 例:
- スキーマ操作/DB作成・削除テストが、スロット付きのDB名でも動くようにする。
- SQLite, MySQL, PostgreSQL 各アダプタの rake テスト (
*_rake_test.rb) を、スロット概念を前提としても壊れないようにリファクタ。
例: test/cases/adapters/sqlite3/sqlite_rake_test.rb (+19/-25)
- テスト対象のDBパス/ファイル名の扱いを整理し、スロットごとに異なるパスをとっても問題ないようにしていると考えられます。
例: migration_test.rb (+23/-50)
- マイグレーション周りのテストで、
- 「特定のDB名を前提にしている」
- 「テスト間で副作用を残す」 といったコードを削り、より抽象化された設定経由でDBにアクセスするように整理されている可能性が高いです。
- 影響範囲・注意点
影響範囲
- 対象:
- ActiveRecord のテストスイート実行環境 (Rails本体開発者、gem開発者が AR を vendor してテストしているケースなど)。
- 適用されるDB:
- アドバイザリロックをサポートするDB (PostgreSQL, MySQL / MariaDB / 一部の互換エンジンなど) ではスロット分散が有効になります。
- サポートしないDBでは従来通り1つのテストDBを共有。
注意点
最大スロット数 (デフォルト5) を超える並列実行は失敗
- 6つ以上のテストプロセスが同時に走ると、ロックが取れずに例外が発生します。
- CI などで並列度を上げる場合は、ARテストスイートをこの仕様に合わせて調整する必要があります (あるいは将来的にスロット数を設定可能にする拡張が想定されるかもしれません)。
DB作成権限がない環境
- 追加スロット用DBを自動作成しようとしたときに権限が無い場合、そのプロセスは失敗しますが、
- これはあくまで「同時にテストを走らせたとき」にだけ発生する問題です。
- 単一プロセスでテストを実行する分には従来と同じ挙動です。
既存の外部DBに対する影響
- 外部のDBクラスタを使って AR テストを走らせている場合でも、アドバイザリロックがサポートされていれば「ほぼそのまま Just Work」する想定です。
- ただし、テストが勝手に
DB名1,DB名2, … といった追加DBを作る点に注意してください。- 本番共有インスタンス上でテストを動かしているような環境では、追加DBが増える可能性があります。
テストの書き方に対する示唆
- 以降、テストケースで「DB名を直接決め打ち」したり、「グローバルな1つのDBに副作用を残す」ような書き方は、並列テストを壊す原因になります。
- 今回のPRに倣って、
ActiveRecord::Base.connection_db_config.database- テスト用ヘルパ (
ARTest.config等) 経由でDB名を参照する、というパターンに寄せていくのが望ましいです。
- 参考情報 (あれば)
- 関連しそうなキーワード/背景:
- Rails のテスト並列化 (Minitest 並列実行、
PARALLEL_WORKERS環境変数など) と組み合わせるとき、DBごと分離されるためテストのフレークが減る。 - PostgreSQL の
pg_advisory_lock/ MySQL のGET_LOCKなどが「アドバイザリロック」の代表的実装。
- Rails のテスト並列化 (Minitest 並列実行、
- このPR内のファイル:
activerecord/test/support/database_slot.rb… スロット管理の中核activerecord/test/support/config.rb… テストDB設定をスロット対応させる- 各種
*_rake_test.rb,migration_test.rb,fixtures_test.rb… スロット付きDBでも壊れないようにしたテストの整理
#59018 Only consult the connection pools the execution context has used
マージ日: 2026/10/13 | 作成者: @matthewd
- 概要 (1–2文で)
このPRは、クエリキャッシュを「接続プールごと」から「実行コンテキスト(ExecutionContext)ごと」に戻しつつ、その実行コンテキストが実際に利用した接続プールだけを対象に各種処理(トランザクション確認、接続クリア、クエリキャッシュクリアなど)を行うように最適化する変更です。多くのアプリでは体感差は小さいものの、プール数が多い・ジョブが多い環境ではCPU使用量削減が期待できます。
- 変更内容の詳細
背景: Ruby 3.3.5 と WeakKeyMap
- Rails は Ruby 3.3.5 を必須に変更済み (#58908)。
- Ruby 3.3.5 では
WeakKeyMapを使って「オブジェクトをキーにした弱参照マップ」を安定して扱えるようになり、接続プール (ConnectionPool) をキーにできるようになった (#58918)。 - これにより、以前一度導入されていた「クエリキャッシュを実行コンテキスト単位に持つ方式」(#52622) を安全に復活できるようになった。
従来(#52632以降)の構成:
- クエリキャッシュは「接続プールごと」に保持される。
- グローバルな処理(
clear_active_connections!,all_open_transactions, クエリキャッシュクリアなど)は、基本的に「全接続プールを走査」する。
今回の変更後:
- クエリキャッシュは再び「実行コンテキストごと (ExecutionContext)」に紐付けられる。
- その実行コンテキストが「実際に使った接続プールの集合」を持つようになり、ループ処理は「その集合」に対してのみ行われる。
実行コンテキストと接続プールの関連付け
ActiveRecord::ConnectionAdapters::Abstract::ConnectionPool 周辺に以下のような変更が行われています(概念的な説明):
- 各
ConnectionPoolは、「どの ExecutionContext から利用されたか」を弱参照集合 (WeakSet / WeakKeyMap ベース) で管理。 - 一方、ExecutionContext 側にも「自分が利用した ConnectionPool の WeakSet」を持つ。
イメージ:
# 疑似コードイメージ
class ExecutionContext
# WeakSet of ConnectionPool
attr_reader :used_connection_pools
end
class ConnectionPool
def with_connection
# ... 既存の接続取得ロジック ...
execution_context = ActiveSupport::ExecutionContext.current
execution_context.used_connection_pools.add(self)
yield connection
end
end実際には WeakKeyMap を利用した ActiveSupport::WeakSet を使うことで、接続プールやコンテキストが GC で解放されたときに参照が自動的に掃除されるようになっています。
WeakSet の拡張
activesupport/lib/active_support/weak_set.rbに機能追加:WeakSetが ConnectionPool や ExecutionContext を扱えるよう調整。- テスト (
weak_set_test.rb) も追加され、GC との連携や参照の消滅タイミングが検証されています。
これにより、「どのプールを使ったか」を覚えつつも、それが GC を妨げないようになっています。
クエリキャッシュの実装変更
対象ファイル:
activerecord/lib/active_record/connection_adapters/abstract/query_cache.rbactiverecord/lib/active_record/query_cache.rb
主なポイント:
- クエリキャッシュを ExecutionContext にぶら下げる構造に戻した。
- 接続プール側に持っていたキャッシュ用の状態を整理し、ExecutionContext に委譲。
- クエリ発行時は「現在の ExecutionContext から、そのプールに対するキャッシュ状態を引く」形になる。
ざっくりしたイメージ:
module ActiveRecord
module ConnectionAdapters
module QueryCache
def select_all(...)
if query_cache_enabled_for_current_context_and_pool?
# ExecutionContext にぶら下がるキャッシュを参照
cache = current_execution_context.query_cache_for(pool: self)
cache.fetch(sql, binds) { super }
else
super
end
end
end
end
end「全プールをループする」操作の最適化
対象:
all_open_transactionsclear_active_connections!- クエリキャッシュクリア処理
- Ractor 対応の connection handler 周り (
ractor_connection_handler.rbなど)
従来:
- 登録されている全ての ConnectionPool をループして処理。
- プール数が多い場合(マルチDB、シャーディング、テナントごとプールなど)にオーバーヘッドが目立つ。
変更後:
- 「現在の ExecutionContext が実際に使ったプール集合」に対してのみ処理を行う。
- 例えば
after_all_transactions_commit(Active Job の enqueue 時に毎回呼ばれる)が、実際にはトランザクションを開いていない多数のプールをいちいち確認しないようになる。
具体的には Abstract::ConnectionHandler と RactorConnectionHandler において、プールを走査するメソッドが「ExecutionContext が把握しているプール集合」を使うように変更されています。
Ractor 対応
activerecord/lib/active_record/connection_adapters/ractor_connection_handler.rb.../proxy_connection_pool.rb- テスト:
ractor_connection_handling_test.rb
Ractor ごとに独立した接続ハンドラ/プール集合を持つ構造に合わせて、ExecutionContext ベースのプール追跡が正しく動くように調整。
- ProxyConnectionPool 経由で実際のプールにアクセスする場合でも、「利用したプールの記録」が正しく ExecutionContext に残るようにしている。
コンソール関連の微修正
rails console実行時の ExecutionContext 初期化・クリーンアップまわりを、上記の変更に追従。console_test.rbにテスト追加。コンソール起動〜終了のライフサイクルで接続やキャッシュ状態が適切に扱われることを確認。
- 影響範囲・注意点
影響があるケース
- 多数の接続プールを持つアプリケーション:
- マルチDB構成 (primary/replica, sharded DB など)
- テナントごとに別プールを持つマルチテナント構成
- Active Job を高頻度で enqueue するワーカー:
after_all_transactions_commitが毎回「全プールのトランザクション状態確認」をしていたオーバーヘッドが減るため、CPU使用率が下がる可能性が高い。
互換性・挙動面
- API レベルの互換性:
- 公開API(ActiveRecord::Base の使い方、接続ハンドラの基本API など)には変化はほぼない。
- 接続プールやクエリキャッシュの「内部構成」に依存しているメタプログラミング/パッチがある場合は注意が必要。
- クエリキャッシュのスコープ:
- 「プールごと」→「ExecutionContext ごと」に戻ることで、キャッシュのライフサイクルが「コンテキスト単位」に直感的に揃う。
- 通常の Web リクエストやジョブ処理では、ExecutionContext は 1 リクエスト/1 ジョブに対応するため、リクエストに閉じたクエリキャッシュが再び明確になる形。
- メモリ/GC との関係:
WeakKeyMap/WeakSetを使っているため、プールや ExecutionContext が不要になれば GC によって参照も自動的に解放される設計。- 長寿命なスレッド/Ractor でも、「使用済みプールの履歴が永遠に溜まり続ける」ことを避けられる。
移行時の注意
- Ruby バージョン:
- Rails 側で Ruby 3.3.5 が前提になっているため、それ未満で動かしているとこのあたりのコードに起因する問題が出る可能性がある(そもそもサポート対象外)。
- 独自のコネクションハンドリング:
ActiveRecord::Base.connection_handlerやConnectionPoolをラップ/モンキーパッチしている場合は、このPRで追加された「ExecutionContext とプールのひも付けロジック」を壊していないか確認が必要。
- 参考情報 (あれば)
関連PR:
- Ruby 3.3.5 必須化: #58908
- WeakKeyMap で ConnectionPool をキーにできるように: #58918
- 以前の「ExecutionContext ベースのクエリキャッシュ」導入: #52622
- その後の「プールごとのクエリキャッシュ」への移行: #52632
コードの入口になりそうな箇所:
ActiveRecord::ConnectionAdapters::Abstract::ConnectionPoolActiveRecord::ConnectionAdapters::Abstract::ConnectionHandlerActiveRecord::QueryCacheActiveSupport::WeakSet/WeakKeyMap
これらを追うと、「ExecutionContext ⇔ ConnectionPool ⇔ QueryCache」の紐付けモデルと、どのタイミングでどのプール集合を参照しているかが把握しやすくなります。
#58028 Warn on broken links to other guides
マージ日: 2026/10/13 | 作成者: @p8
- 概要 (1-2文で)
Rails ガイド生成時のリンク検証を強化し、「同一ガイド内のアンカー」だけでなく「別ガイドへのアンカーリンク」も壊れていれば警告を出すようにした PR です。あわせて、実際に壊れていたガイド内リンクの修正・削除も行われています。
- 変更内容の詳細
a. ガイド生成スクリプトのリンクチェック拡張
対象ファイル: guides/rails_guides/generator.rb
これまでの挙動:
- ガイド (
.md) から HTML を生成する際に、「同一ガイド内」のアンカー(#section-name)が存在しない場合のみ警告を出していた。
今回の拡張:
other_guide.html#some-anchorのような「別ガイドへのリンク」についても、リンク先ガイドの HTML 内に該当アンカーがなければ[WARN] BROKEN LINKを出すようになった。- 警告メッセージの例:log
[WARN] BROKEN LINK in active_record_composite_primary_keys.html: active_record_querying.html#conditions-with-id. Perhaps you meant active_record_querying.html#conditions-that-use-like. - 壊れたリンクだけでなく、「もしかしてこのアンカーでは?」という候補も出すようになっており、ガイドメンテナが修正先を見つけやすくなっている。
推測される処理の流れ(イメージコード):
# 各 HTML ガイドのアンカー一覧を事前に収集(guide => [anchors])
anchors_index = build_anchors_index
# リンク検証時
links.each do |link|
guide, anchor = parse_link(link) # e.g. "active_record_querying.html", "conditions-with-id"
if guide # 別ガイドへのリンク
unless anchors_index[guide]&.include?(anchor)
warn_broken_cross_guide_link(guide, anchor, anchors_index[guide])
end
else # 同一ガイド内
# 従来の自己参照アンカー検証
end
endb. 実在するガイドリンクの修正・削除
PR 説明から読み取れる具体的変更:
壊れたアンカーリンクの更新
- ガイド内に存在しなくなったアンカーへのリンクを、
- 現在の相当箇所のアンカー、または
- 古いバージョンのガイド(該当機能が削除済みの場合) に張り替え。
- ガイド内に存在しなくなったアンカーへのリンクを、
「Transactions」ガイドへのリンクを削除
active_record_basics.htmlが「トランザクション」を説明している Rails バージョンは存在しなかったため、- そこへのリンクを削除。
- 別途、「Transactions」ガイド自体は作成作業中である旨が記載されている。
active_record_querying.html#conditions-with-idへのリンク削除#conditions-with-idセクション自体が「Composite Primary Keys」ガイドへ統合されたため、- リンクと、それを説明していた段落を削除。
- つまり、クエリガイド側で説明していた内容は、今後は「複合主キー」のガイドを参照する形になる。
c. ガイド本文の軽微な修正
対象ファイル:
guides/source/4_1_release_notes.mdguides/source/5_2_release_notes.mdguides/source/upgrading_ruby_on_rails.md
これらは行数から見て、主に次のような小規模変更が行われています:
- 壊れたリンク先の修正・削除
- 上記で削除されたセクションやガイドへの参照の整理
- 文言レベルの修正(リンク削除に伴う説明文の削除など)
- 影響範囲・注意点
影響するのはガイドの生成プロセスのみ
- アプリケーションコードやランタイムの挙動には影響しません。
guides/以下でドキュメントをビルドしている人(Rails コア開発者、ドキュメントメンテナ、ディストリビューション用に独自にガイドをビルドしている人)が主な対象です。
ガイドビルド時に新たな警告が出る可能性
- これまで見逃されていた「別ガイドへの壊れたリンク」があれば、
rake guides:generate等の実行時に[WARN] BROKEN LINKが出るようになります。 - CI でガイドビルドを回している場合、ログ上のノイズが一時的に増える可能性がありますが、リンク修正の指針になるため、基本的には歓迎すべき変化です。
- これまで見逃されていた「別ガイドへの壊れたリンク」があれば、
古いバージョンのガイドやローカルカスタムガイドを持っている場合
- 自前でガイドを追加・改変している場合は、今回のより厳密なリンクチェックに引っかかることがあります。
- 壊れたアンカーリンクが検出されたら、
- 対象ガイドの HTML 内にそのアンカーが本当に存在するかを確認し、
- セクション名変更やガイド構成変更に追随してリンクを更新する必要があります。
トランザクション関連のドキュメント構成に注意
- これまで
active_record_basics.htmlやactive_record_querying.htmlにあった/あると思われていた一部情報が、- 今後は「Transactions」専用ガイド、
- または「Composite Primary Keys」ガイド に整理される流れです。
- ドキュメントを参照している自前資料やブログなどが、その古いアンカーをリンクしている場合は、リンク切れになる可能性があります。
- これまで
- 参考情報 (あれば)
- 当該 PR:
- PR で言及されている、壊れたリンクの具体例:
- 関連ファイル:
- ガイド生成ロジック:
guides/rails_guides/generator.rb - 該当リリースノート・アップグレードガイド:
guides/source/4_1_release_notes.mdguides/source/5_2_release_notes.mdguides/source/upgrading_ruby_on_rails.md
- ガイド生成ロジック:
#59042 Fix broken anchor links in composite primary keys guide cross-references [ci skip]
マージ日: 2026/10/13 | 作成者: @SatvikMishra08
- 概要 (1–2文で)
Composite Primary Keys(複合主キー)ガイドの書き換え後に壊れていたガイド間リンク(アンカー)が3箇所修正されました。これにより、関連するRailsガイド同士の相互参照が正しく該当セクションにスクロールするようになります。
- 変更内容の詳細
今回のPRは、ドキュメント(Rails Guides)の内部リンク先アンカーIDの不整合を直すだけの変更で、コード本体には影響しません。対象は以下3つのガイドです。
(1) association_basics → composite primary keys ガイドへのリンク修正
元のリンク先アンカー
active_record_composite_primary_keys.html#associations-between-models-with-composite-primary-keys
→ 実際のガイドにはこのIDが存在せず、ページ先頭に飛んでしまっていた。修正後
#associations
→ Composite Primary Keysガイド内の実際のセクションIDに合わせて修正。
イメージ(Markdown中のリンク例・あくまで擬似コード):
[複合主キーを使った関連](active_record_composite_primary_keys.html#associations)これにより、関連(Association)に関する解説セクションへ正しくジャンプするようになります。
(2) active_record_querying → composite primary keys ガイドへのリンク修正
Active Record Queryingガイド内にある find_by(id: ...) に関する注意書きの参照先が変更されています。
元のリンク先アンカー
active_record_composite_primary_keys.html#conditions-with-id
→ これも存在しないアンカーだったためページ先頭に飛んでいた。修正後のリンク先
CPKガイドの「Usingwhere」セクション(#using-whereのようなID)
→where/find_byとidに関する注意がまとまっている実際のセクションにリンク。リンクテキストも変更
「conditions with id」ではなく、「Usingwhere」という、実際のセクション名(または内容)に合わせたテキストに変更。
これにより、読者は「どこを読むべきか」がテキストからも分かりやすくなっています。
擬似的な例:
詳しくは [Using `where`](active_record_composite_primary_keys.html#using-where) セクションを参照してください。ここには「複合主キーのときに where(id: ...) や find_by(id: ...) をどう扱うか」といった注意書きがある想定です。
(3) composite primary keys → active_record_querying ガイドへのリンク修正
CPKガイド側から Active Record Querying ガイドへの逆リンクも修正されています。
元のリンク先アンカー
active_record_querying.html#conditions-with-id
→ こちらも実在しないアンカーだったためページ先頭へ。修正後のリンク先
#find-byセクション
→ Queryingガイド中のfind_byに関する説明部分で、CPKについての注意が載っている場所。
擬似的な例:
詳しくは [find_by](active_record_querying.html#find-by) セクションを参照してください。これにより、「CPKに関する注意はActive Record Queryingガイドの find_by 節にある」という導線が正しく機能するようになります。
- 影響範囲・注意点
影響範囲
- Rails本体の動作やAPI仕様には一切変更なし。
- Rails Guides(特に edgeguides、開発版ガイド)をブラウザで読む際のナビゲーション性が改善されます。
- 複合主キーと
where/find_by(id: ...)の関係を調べる際に、正しいセクションへ一発で飛べるようになります。
注意点
- すでに外部ドキュメントや社内Wikiなどで、旧アンカー(存在しない
#conditions-with-id等)をハードコードしている場合、それらのリンクは依然として壊れたままです。
→ 必要であれば、新しいアンカー(例:#using-where,#find-by,#associationsなど)に張り替える必要があります。 - このPRは edgeguides 上での実在アンカーを基準にしているため、もし将来またセクション名が変われば同様のリンク切れが再発しうる点には留意が必要です。
- すでに外部ドキュメントや社内Wikiなどで、旧アンカー(存在しない
- 参考情報 (あれば)
- このPRで明示的に参照されている過去PR:
- Composite Primary Keysガイドを書き換えたPR: #57348
→ ここでセクション構造やアンカーIDが変わったことで、今回のリンク切れが発生したと推測されます。
- Composite Primary Keysガイドを書き換えたPR: #57348
- 対象ガイド(edgeguides)のURL例(実際のURLはバージョンに依存):
https://edgeguides.rubyonrails.org/active_record_composite_primary_keys.htmlhttps://edgeguides.rubyonrails.org/active_record_querying.htmlhttps://edgeguides.rubyonrails.org/association_basics.html
#59040 Fix disable_joins through associations with a composite owner foreign key
マージ日: 2026/10/12 | 作成者: @federico-carrocera
- 概要 (1-2文で)
disable_joins: trueを使ったhas_many :throughで、オーナー側の外部キーが複合キー(例:[:tenant_id, :id])の場合に発生していたバグを修正する PR です。複合外部キーを正しく読み取ることで、ArgumentErrorが出て関連がロードできない問題が解消されています。
- 変更内容の詳細
問題の背景
- マルチテナント + シャーディング構成で、テーブル行が
[tenant_id, id]のような複合キーで識別されているケースを想定しています。 - DB をまたぐ
has_many :throughでdisable_joins: trueを使うと、
「オーナー側(has_manyを定義しているモデル)の条件を遠いテーブルに JOIN で押し込まず、別クエリで取得して自前で関連付ける」という動きをします。 - ところが、オーナーの外部キーが複合キーのときに、rubyのように「カラム名配列」をそのまま
owner.read_attribute(first_item.join_foreign_key)read_attributeに渡しており、nilが返ってしまうため、textが発生していました。ArgumentError: Expected corresponding value for ["shop_id", "id"] to be an Array
以前の PR #57534 では、「中継側(through 先)の複合キー対応」と「メモリ上でのグルーピング」は修正済みでしたが、
「最初の一跳ね目(owner 側)での複合キー取得」が未対応だったため、この PR でそこを埋めています。
コード変更の要点
変更ファイルのうち本質的なのは以下です:
activerecord/lib/active_record/associations/disable_joins_association_scope.rbdiff- owner_value = owner.read_attribute(first_item.join_foreign_key) + owner_value = ActiveRecord::Key.for(first_item.join_foreign_key).value_of(owner)
ここで使われている ActiveRecord::Key.for(...).value_of(owner) は、以下の役割を担います:
- 複合キーと単一キーの両方を抽象化するヘルパ
join_foreign_keyが- 単一キー(例:
"order_id")なら、従来と同じくowner.order_id相当の値を返す - 複合キー(例:
[:shop_id, :order_id])なら、owner.shop_idとowner.order_idをそれぞれ読み取り、配列や内部表現としてまとめて返す
- 単一キー(例:
これは BelongsToAssociation がカウンタキャッシュ更新などで外部キーを読むときと同じ経路を使っており、
既存の複合キーサポートの仕組みに合わせた形になっています。
テスト追加
activerecord/test/cases/associations/has_many_through_disable_joins_associations_test.rbに 9 行追加activerecord/test/models/cpk/book.rbにdisable_joins版の関連を1つ追加
テストの内容は #57534 のテストパターンを踏襲しており、
class Cpk::BookWithOrderAgreements < ActiveRecord::Base
belongs_to :order, foreign_key: [:shop_id, :order_id]
has_many :order_agreements, through: :order, disable_joins: true
endのような形で、
belongs_to :order, foreign_key: [:shop_id, :order_id]
→orderへの関連が複合外部キーを利用- それを
throughしたhas_many :order_agreements, disable_joins: true
→ オーナー(Book)のshop_idとorder_idを使って disable_joins 経由でロード
を行い、エラーにならず正しいレコードが取得できることを確認しています。
CHANGELOG の更新
activerecord/CHANGELOG.mdにバグ修正として 5 行追加
Active Record レイヤで「disable_joins + 複合外部キー」のバグが修正された旨が記載されています。
- 影響範囲・注意点
影響を受けるケース
この修正の直接の対象は、次のようなときです:
- Active Record で複合主キー / 複合外部キーを使っている(
composite_primary_keysgem 等の利用を含む) has_many :throughでdisable_joins: trueを指定している- かつ、「最初の through 元(owner 側)の外部キー」が複合キーである
これらを満たす場合、以前は関連ロード時に ArgumentError が発生していたのが、
この PR により正常にロードできるようになります。
既存コードへの後方互換性
- 単一カラムの外部キーについては、
ActiveRecord::Key.for(...).value_of(owner)が従来と等価な値を返すよう実装されているため、挙動の変更は実質ありません。 disable_joins: trueを使っていない関連には影響しません。- 複合キーを使っていても、
disable_joinsを指定していない has_many/has_many :through はこの変更の対象外です。
パフォーマンス面
owner.read_attribute(...)→Key.for(...).value_of(owner)への置き換えは呼び出しコストがわずかに増えますが、- 呼び出し頻度は関連読み出し時に限られる
disable_joins自体が別クエリ+メモリ上結合という高コストな経路
を取ることを前提にしている
- そのため、実用上問題になるレベルのパフォーマンス低下はほぼないと考えられます。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/59040
- 関連 PR(中継側・グルーピングの複合キー対応): https://github.com/rails/rails/pull/57534
- 複合キー読み取りで利用されているクラス:
ActiveRecord::Key(Active Record 内部 API)
複合キー + disable_joins: true な関連で問題が出ていた場合は、
この修正が入った Rails バージョンへ更新することで解消が見込めます。
#59039 Extract ActiveSupport::WeakSet as an internal API
マージ日: 2026/10/12 | 作成者: @byroot
- 概要 (1-2文で)
ActiveSupport 内部で使っていた「弱参照による Set 相当の仕組み」をActiveSupport::WeakSetとして独立した内部 API に切り出した PRです。主な利用箇所だったActiveSupport::DescendantsTrackerから実装を分離し、再利用しやすい形に整理しています。
- 変更内容の詳細
2-1. ActiveSupport::WeakSet の新設
新しく activesupport/lib/active_support/weak_set.rb が追加され、ActiveSupport::WeakSet クラスが定義されました。
これは「要素への弱参照を保持する Set 的コンテナ」で、GC によって参照先オブジェクトが解放されると、自動的にセットからも消えることを意図した実装です。
典型的なイメージ:
set = ActiveSupport::WeakSet.new
obj = Object.new
set << obj
set.include?(obj) # => true
obj = nil
GC.start
# GC 後、弱参照だけが残っているはずなので WeakSet 側からも消える
set.include?(obj) # => false(あるいは要素数が減っている)※実際の内部実装は PR からは細部までは読み取れませんが、Ruby の ObjectSpace や WeakRef 相当の仕組み/Finalizer を用いて「GC に追従して自動的にクリーンアップされるコレクション」として振る舞うことが想定されます。
2-2. ActiveSupport::DescendantsTracker から弱参照ロジックを削除
activesupport/lib/active_support/descendants_tracker.rb から 42 行分のコードが削除されています。
ここには「クラスの子孫クラスを弱参照で追跡するための自前 Set 実装 or ロジック」が内包されていましたが、それを ActiveSupport::WeakSet に移し、DescendantsTracker からは専用実装を取り除いています。
従来(イメージ):
# descendants_tracker.rb 内部に WeakSet 的な構造が直接書かれていた
class ActiveSupport::DescendantsTracker
# @descendants = some_custom_weak_set_implementation
end変更後(イメージ):
class ActiveSupport::DescendantsTracker
# @descendants = ActiveSupport::WeakSet.new
end2-3. active_support.rb からの読み込み
activesupport/lib/active_support.rb に 1 行追加され、ActiveSupport::WeakSet が ActiveSupport のロード時に読み込まれるようになっています。
これにより、ActiveSupport を require すれば ActiveSupport::WeakSet も利用可能(ただし「内部 API」として)になります。
- 影響範囲・注意点
主な影響範囲
- Rails 内部でクラスの継承関係を追跡する
ActiveSupport::DescendantsTrackerの実装がActiveSupport::WeakSetを使うようリファクタリングされています。 - 実際の挙動(どのクラスが descendants として列挙されるか)は変えずに、実装の責務分離を行っている変更です。
- Rails 内部でクラスの継承関係を追跡する
パブリック API ではなく「内部 API」
- タイトルにある通り、「internal API」と明示されています。
- したがってアプリケーションコードから直接
ActiveSupport::WeakSetを利用するのは非推奨です(将来の互換性が保証されない可能性が高い)。 - gem・プラグインなどで「内部 API に依存する前提の拡張」をしているなら、
WeakSetへの依存は破壊的変更のリスクを理解した上で行う必要があります。
動作上の変更は基本的に無し
DescendantsTrackerの外形的な挙動はそのままのはずで、API 仕様変更は行われていません。- ただし、弱参照コレクション実装の差異により、極端な GC タイミングやメモリ境界ケースで挙動が微妙に変わる可能性はゼロではありません(例えば「いつ古いクラスが消えるか」のタイミングなど)。
パフォーマンスやメモリ挙動の安定化の布石
- 弱参照 Set の仕組みが共通化されたことで、今後 GC との相性改善やパフォーマンス改善が 1 箇所のチューニングで全体に波及しやすくなります。
- DescendantsTracker 以外でも「弱参照 Set」が必要になった場合に再利用できるため、重複実装・微妙に異なる挙動の乱立を防げます。
- 参考情報 (あれば)
この PR が参照している PR:
- https://github.com/rails/rails/pull/59018
おそらく WeakSet 導入や descendants 追跡改善に関連する前段階の変更です。
- https://github.com/rails/rails/pull/59018
関連コンポーネント:
ActiveSupport::DescendantsTracker: Rails がクラスの継承関係を追跡するための仕組み。ActiveRecord::Base.descendantsなどで利用される土台になっています。- 弱参照・WeakSet の一般的な目的: GC で解放されるべきオブジェクトに「強参照」を持たず、かつ「可能なら集合として追跡しておきたい」というニーズに応えるデータ構造です。
#59034 Use Ruby::Prism in ActionView::Template#spot when available
マージ日: 2026/10/12 | 作成者: @yahonda
- 概要 (1-2文で)
Rails 8.1 の ActionView テンプレートエラー時のハイライト機能が、Ruby master の変更(ErrorHighlight の挙動変更と Ruby::Prism 組み込み)によって壊れていた問題を修正する PR です。テストが不安定に落ちる原因を解消しつつ、Ruby 本体に組み込まれたRuby::Prismを優先的に使うようにして、テンプレートのエラーハイライトを正しく動作させます。
- 変更内容の詳細
背景となる Ruby 側の変更
Ruby master で以下の変更が入っています:
ErrorHighlight の autoload 化
ErrorHighlightが autoload されるようになり、定数がまだ解決されていない状態でremove_const(:ErrorHighlight)を呼ぶと「nil を返すだけでモジュール自体は解決されない」という状態が起きる。- その結果、後続のテストで
ErrorHighlightが存在しないor中途半端な状態となり、Template#spot内でErrorHighlight.spotを呼ぶ際にundefined method '[]' for nilなどのエラーが発生していた。
Ruby 本体に
Ruby::Prismが組み込まれた (Feature #22379)- Ruby 3.4 以降・一部 master では、Prism が組み込みの
Ruby::Prismとして提供される。 - 同時に error_highlight が「
Ruby::Prismのノードだけ受け付ける」ように変更 (error_highlightgem 側の変更) されたため、これまでのように「prism gem で解析したノード」を渡すとTypeError: Exception is expectedになるケースがある。 - つまり、「コンパイルに使ったのと別のパーサでパースして得たノード」を渡すと ID が噛み合わず、error_highlight が期待する形式と合わない。
- Ruby 3.4 以降・一部 master では、Prism が組み込みの
この PR は上記2点に対応するものです。
1. テストの修正: ErrorHighlight の autoload 問題への対処
対象ファイル:
actionpack/test/dispatch/exception_wrapper_test.rbactionview/test/template/render_test.rb
問題点:
#58998 で追加されたテストは、ErrorHighlight が定義されているかどうかをテストするために remove_const を使っていますが、Ruby master では autoload のため、まだロードされていない状態で remove_const(:ErrorHighlight) すると 戻り値が nil で、モジュール本体も一度もロードされない という状態になっていました。
その結果、同じプロセス内で 別のテストが ErrorHighlight の存在を前提に動くと落ちる というテスト順依存のバグが発生していました。
修正内容:
テスト内で
ErrorHighlightをremove_constする前に、一度明示的に参照して autoload を発火させるように変更。イメージとしては以下のような形(実際のコードは多少異なりますが、やっていることはこれに近いです):
ruby# 変更前(擬似コード) Object.send(:remove_const, :ErrorHighlight) # 変更後(擬似コード) ErrorHighlight # ここで autoload を解決させる Object.send(:remove_const, :ErrorHighlight)
これにより
remove_constの戻り値は実際のモジュールになり、必要であればテスト内で復元できるし、少なくとも「autoload 未解決の nil を返す」ケースは回避されます。
2. ActionView::Template#spot の修正: Ruby::Prism を使うように変更
対象ファイル:
actionview/lib/action_view/template.rbactionview/CHANGELOG.md
現状の問題:
Template#spotは、「例外発生時に、そのテンプレートのどの行・どのカラムでエラーが起きたかをハイライトする」ために以下を行っています:- 例外バックトレースからテンプレート内のコンパイル済み Ruby コードの位置を取得
- テンプレートの Ruby コードを prism gem(
Prism.parseなど)で再度パース - そのノード情報を
ErrorHighlight.spotに渡して、対象位置に対応するコード片を特定
しかし Ruby master では:
- Ruby 本体の
Ruby::Prismでテンプレートがコンパイルされている - error_highlight は
Ruby::Prismのノードだけ受け付ける - prism gem 側のノードとは ID や内部表現が異なる
ため、
Template#spotが prism gem を使い続けるとErrorHighlight.spotがTypeErrorを投げてしまい、テンプレートエラーページを組み立てられなくなっていました。- Ruby 本体の
修正方針:
- Ruby 本体に
Ruby::Prismがあれば、それを使ってパースする。 - Ruby 3.4 / 4.0 など、まだ
Ruby::Prismがない環境では従来通りprismgem を利用。 - さらに、Ruby が parse.y ベースでコンパイルしている場合は、従来どおり
RubyVM::AbstractSyntaxTreeを利用する、といった既存分岐は維持。
コード上のイメージ(簡略化):
def spot(exception)
# ... 省略: バックトレースから位置情報を抽出 ...
node =
if defined?(Ruby::Prism)
# Ruby 本体組み込みの Prism を使用
Ruby::Prism.parse(source).value
else
# 従来どおり prism gem を使用
Prism.parse(source).value
end
ErrorHighlight.spot(exception, node:, path: identifier)
end実際の差分は行数レベルで +9/-4 程度で、Ruby::Prism 存在チェックを追加したうえで、使うパーサーを切り替えるようにしているだけです。
CHANGELOG:
actionview/CHANGELOG.mdに、Ruby master / 今後の Ruby バージョンでRuby::Prismを使うようにした旨、およびテンプレートエラーのハイライトに関する修正が追記されています。
- 影響範囲・注意点
影響範囲
Rails 8.1 系 + Ruby master / 将来の Ruby (3.4/4.0 など) を使う環境
- テンプレート内で例外が発生したとき、
- 以前:
- デバッグページ(ActionDispatch::DebugExceptions のエラー画面)が 500 public ページにフォールバックし、
エラー原因もログに十分出ず、error_highlight がうまく動かない。 - テストが seed によって落ちたり、
TypeErrorやNoMethodErrorが発生。
- デバッグページ(ActionDispatch::DebugExceptions のエラー画面)が 500 public ページにフォールバックし、
- この PR 適用後:
- デバッグページが正しく表示され、テンプレートソースの該当行がハイライトされる。
- テストは Ruby master 上でも安定して通るようになる。
- 以前:
- テンプレート内で例外が発生したとき、
既存の Ruby 3.2 / 3.3 / 3.4 等で
Ruby::Prismがない or prism gem を使っている環境- 振る舞いは基本的に従来どおり。
defined?(Ruby::Prism)チェックにより、Ruby 本体がRuby::Prismを持たないバージョンでは今まで通り prism gem やRubyVM::AbstractSyntaxTree経由の処理が使われるため、後方互換性は保たれている。
注意点・開発者が意識すべきこと
ErrorHighlight の remove_const をテスト・アプリケーションで行っている場合:
- Ruby master では autoload の関係で、単純に
remove_constすると戻り値が nil になり、定数の復旧ができなくなるリスクがあります。 - この PR のテスト修正のように、
ErrorHighlightを一度参照して autoload を解決してからremove_constするなど、ライブラリ側での依存関係を壊さないようにする必要があります。
- Ruby master では autoload の関係で、単純に
独自に
ErrorHighlight.spotを呼び出しているコード:- Ruby master では
Ruby::Prismのノードを渡すのが前提 になっているので、prism gem や AST gem など別パーサーのノードを渡している場合は今後エラーになる可能性があります。 - Rails と同様に、「Ruby 本体が
Ruby::Prismを持っているか」「どのパーサでコンパイルされたコードか」を意識して、ノードを揃える必要があります。
- Ruby master では
Ruby バージョン間の挙動差:
- この PR は「Ruby master / 将来版で壊れないようにする」ための対応であり、現行安定版 Ruby では挙動はほぼ変わらないはずですが、CI で Ruby master を回している場合は ActionView 周りの挙動が変わるので、テンプレートエラー時の画面・ログの確認をしておくと安心です。
- 参考情報 (あれば)
- Ruby の関連 Feature:
- ErrorHighlight autoload: https://bugs.ruby-lang.org/issues/21951
- Ruby における Prism 組み込み: https://bugs.ruby-lang.org/issues/22379
- エラーハイライト側の変更:
error_highlightがRuby::Prismノードのみを受け付けるようになった変更: https://github.com/ruby/error_highlight/pull/85
- この PR で言及されている Ractor バグ:
この PR により、「Ruby が実際にテンプレートをコンパイルしたときと同じパーサ(Ruby::Prism / prism gem / RubyVM::AST)で再パースして、そのノードを ErrorHighlight に渡す」という形が整えられ、今後の Ruby の進化に追従したエラーハイライト動作が保証されます。
#59030 Speed up String#blank? further by avoiding repeated bytesize calls
マージ日: 2026/10/11 | 作成者: @fatkodima
- 概要 (1-2文で)
String#blank?の実装を微調整し、ASCII文字列かつ全体が空白かどうかを調べる処理でbytesizeを何度も呼ばないようにすることで、一部ケースで高速化した PR です。特に先頭が空白の ASCII 文字列や空白のみの長い文字列で 1.1〜1.4倍程度の改善が確認されています。
- 変更内容の詳細
背景
直前の #58745 で String#blank? は大きく書き換えられており、ざっくり言うと:
- 空文字は即
true ascii_only?な場合は、バイト列を自前でなめて- タブ〜改行 (
\t〜\r/ 9〜13) - スペース (32) だけなら
true、それ以外のバイトがあればfalse
- タブ〜改行 (
- 非 ASCII の場合は、従来通り
BLANK_RE(空白判定用の正規表現)で判定
という構造になっています。
この PR は、その「ASCII かつループでバイトを走査する部分」の細かい最適化です。
具体的な変更内容
diff は activesupport/lib/active_support/core_ext/object/blank.rb の +2/-1 だけなので、実質 1 行程度のロジック調整です。Ruby 側ベンチマークに現れている最適化ポイントから推測すると、主に次のような変更が入っています:
- 変更前:ループの条件判断で暗黙に何度も
bytesize/lengthを参照していた(例えばwhile i < bytesizeのようなイディオムなどで、インライン展開・最適化次第ではオーバーヘッドになる)。 - 変更後:
- ループに先立って一度だけ
size = bytesizeなどでローカル変数にキャッシュする、あるいは while byte = getbyte(i)形式にして「長さチェック+バイト取得」を 1 ステップにまとめる
といった形で、同じ値を何度も取得しないようにしている。
- ループに先立って一度だけ
実際、この PR の説明文にある blank_after? 実装は:
if ascii_only?
return false if getbyte(0) > 32
i = 0
while true
byte = getbyte(i)
break unless byte
return false unless byte == 32 || (byte >= 9 && byte <= 13)
i += 1
end
true
else
# 非ASCIIは正規表現で判定
endという形になっており:
- 冒頭で
getbyte(0) > 32(= 先頭バイトが空白文字より大きければ即false)という早期リターンを入れることで、- 先頭が空白でない ASCII 文字列(典型的な「普通の英単語」など)の処理時間を短縮
- ループの終了条件を「バイトが取れなくなったら break」(= 末尾に到達)としているため、明示的な
bytesizeベースの判定が不要
となっています。PR 本体でもこれと同等の形(あるいはごく近い構造)になったと考えてよく、ポイントは:
bytesizeや長さの問い合わせをループのたびに行わない- 先頭バイトで「空白でないこと」を早く判定する
の 2 点です。
ベンチマーク結果から読み取れること
- 空文字 (
"")・1文字 ("a")・短い ASCII 文字列 ("this is a test")
→ ほぼ誤差レベル(速度差なし)。このあたりは元から十分速く、長さも短いためループの回数が少ない。 "\n"や" "、" this is a longer test"のように「先頭が空白 or 空白のみ」の ASCII 文字列
→ 1.1〜1.3倍程度の高速化:"\n": 1.10x" ": 1.29x" this is a longer test": 1.23x
- 特に
" " * 136のような「空白のみの長めの文字列」
→ 1.46x とかなり顕著に改善。ループ中でのbytesize相当の取得コストを減らした効果が出ていると考えられます。 - 非 ASCII 文字列(キリル文字、ひらがな、絵文字、マルチバイト空白など)
→ 多くが「ほぼ同じ速度」。ここは正規表現ベースの判定パスなので、この PR による影響はほぼなし。
- 影響範囲・注意点
影響範囲
ActiveSupportが提供するObject#blank?/String#blank?に依存するすべてのコード(Rails 全体/大半のアプリ)が影響を受けますが、挙動は変えずにパフォーマンスだけ改善する変更です。- 実質的に影響が大きいのは「ASCII かつ空白を含む / 空白だけの文字列」を大量に
blank?チェックするようなパス(ビューでのフォーム値チェック、パラメータ正規化など)。
挙動の互換性 / 注意点
- 判定基準(どの文字を「空白」とみなすか)は以前と同じで、BLANK_RE や ENCODED_BLANKS もそのまま使われています。
- ASCII パスでも扱っているのは
\t〜\rとスペース(0x09〜0x0d および 0x20)のみで、仕様変更はありません。 Encoding::CompatibilityErrorへの対応も従来通りで、非 UTF-8 などでも既存の動作を維持。
パフォーマンス面での実務的なインパクト
- Rails アプリでは
blank?は非常に頻繁に呼ばれるメソッドの一つであり、特にフォーム入力・paramsのバリデーション・view helper(present?含む)などで多用されます。 - この PR 自体は 1〜2 行の変更ですが、ハイボリュームのトラフィックで「ほぼ空白 or 空白から始まる文字列」の判定が多いアプリケーションでは、微妙ながら CPU 使用率が下がる可能性があります(特に
" " * Nのような長い空白列を扱うケースでは顕著)。
- Rails アプリでは
- 参考情報 (あれば)
- 関連 PR:
- #58745:
String#blank?を C 実装から Ruby 実装に書き換え、高速化・簡略化した元の変更。
- #58745:
- 実装のポイント:
bytesize/lengthなど、同じ値の繰り返し取得を避けるだけでも、ホットパスでは1〜数十 % の改善があり得る、という ActiveSupport の good practice の一例です。- 文字列処理系の最適化をする際に、
size/length/bytesizeをループ条件等で何度も呼んでいないかを見直す際の参考になります。
#58745 Speed up String#blank? by scanning bytes instead of running a regexp
マージ日: 2026/10/9 | 作成者: @yaroslav
- 概要 (1-2文で)
String#blank?の実装に「ASCII 文字列専用の高速パス」が追加され、ほとんどのアプリで多用される ASCII 文字列に対して、正規表現を使わずバイト列をスキャンすることで 3〜10倍程度高速化しています。非 ASCII 文字列や既存の判定ロジック・仕様は基本的にそのまま維持されます。
- 変更内容の詳細
これまでの実装
Rails の String#blank? は、(空文字判定をしたあとで) すべての非空文字列に対して以下のような正規表現マッチを行っていました:
def blank?
empty? ||
begin
BLANK_RE.match?(self) # /\A[[:space:]]*\z/
rescue Encoding::CompatibilityError
ENCODED_BLANKS[self.encoding].match?(self)
end
end/\A[[:space:]]*\z/は「全体が空白類文字だけか」を判定- Unicode や各エンコーディング固有の空白も、この正規表現が担当
Encoding::CompatibilityErrorが出るケースではENCODED_BLANKSというエンコーディング別テーブルで代替判定
このため、どんな短い ASCII 文字列 (例: "a", "test") に対しても毎回正規表現マッチが走るのがボトルネックでした。
新しい実装の概要
今回の PR では、次のような「ASCII 専用の早期判定パス」が blank? に追加されています:
- まず
empty?ならtrue(従来どおり) - 次に
ascii_only?フラグを見て、ASCII だけからなる文字列かどうかをチェック - ASCII だけなら、正規表現は使わず、バイト列を
whileループでチェック - ASCII 以外 (UTF-8 に非 ASCII を含む文字列、UTF-16/32、レガシーエンコーディング、壊れたバイト列など) は、従来どおり正規表現ベースのロジックへフォールバック
疑似コード的にはこんなイメージです (実際の実装は多少最適化されています):
def blank?
return true if empty?
if ascii_only?
# 先頭バイトが「印字可能文字」なら即座に false
b0 = getbyte(0)
return false if b0 > 32 && b0 != 127 # 大まかに「スペースより大きい印字可能 ASCII」
i = 0
n = bytesize
while i < n
b = getbyte(i)
# 許可されるのは ASCII における [[:space:]]: 9..13, 32 のみ
return false unless b == 32 || (9..13).include?(b)
i += 1
end
return true
end
# ここから先は従来どおりの実装 (Unicode 空白やレガシーエンコーディング対応)
begin
BLANK_RE.match?(self)
rescue Encoding::CompatibilityError
ENCODED_BLANKS[self.encoding].match?(self)
end
endポイント:
- ASCII 文字列に対しては:
- 先頭バイトが印字可能文字 (
> 32かつ!= 127) なら即座にfalse(空白ではない) - そうでなければ、全バイトが
32(スペース) か9..13(タブ, 改行, 復帰, 垂直タブ, 改ページ)
のいずれかかをループでチェックし、それ以外があればfalse
- ループ内でオブジェクトは一切確保しない・メソッド呼び出しも最小限に抑える設計
- 先頭バイトが印字可能文字 (
- ASCII 以外の文字列は:
- 1回
ascii_only?が呼ばれたあと、従来どおり/\A[[:space:]]*\z/+ENCODED_BLANKSによる判定へ - Unicode の空白やエンコーディング依存の空白 (例: KOI8-R の
0x9A, ISO-8859-1 の0xA0など) は引き続きこの正規表現が担当 - 不正なバイト列に対しても、従来どおり
ArgumentErrorを投げる
- 1回
ループをヘルパーメソッドにしなかった理由
ループを別メソッドに切り出して ascii_blank? のようにすると、メソッド呼び出しコストのせいで 空白のみ/空白始まりの文字列に対して 17〜22% 遅くなった、という計測結果がありました。
一方、blank? メソッド内でローカル変数を増やすことによるオーバーヘッドは測定誤差レベルだったため、あえて inlined な形でループを書いています。
String#present? について
present? は !blank? のままで、特別な変更はありません。
API の挙動としても従来どおりです。
テストの拡充
以下のようなケースがテストに明示的に追加されています:
- 先頭・末尾に空白を含む文字列
- NUL (
"\0") を含む文字列 - Unicode の各種空白文字 (ノーブレークスペース
U+00A0, 全角スペースU+3000, 改行系文字U+2028など) - エンコーディングごとの空白 (KOI8-R の
0x9Aなど) - 不正な UTF-8 列 (従来通り
ArgumentErrorが出ることを確認)
また、ベンチマークスクリプト内でも、PR 前後の blank? が同じ結果を返すか (パリティ) を多数のケースで検証した上で性能計測を行っています。
- 影響範囲・注意点
性能面の影響
ベンチマーク (Ruby 4.0.7 / YJIT / Apple M シリーズ, benchmark-ips) によると:
nil.blank?と""(空文字) の性能はほぼ変わらないnil.blank?: 508M i/s → 508M i/s (変化なし)"": ほぼ同等 (誤差範囲)
- ASCII かつ先頭が非空白な文字列 (例:
"a","this is a test","x" * 1000) は 6〜11倍程度高速化"a": 約 106ns → 約 9ns (11.5x)"this is a test": 約 62ns → 約 9.5ns (6.5x)"x" * 1000: 約 62ns → 約 9.4ns (6.5x)
- ASCII で空白のみ、または空白始まりの文字列も 3〜8倍程度高速化
" "(3スペース): 約 57ns → 約 19ns (3x)"\n": 約 90ns → 約 15ns (6x)" this is a longer test": 約 68ns → 約 16ns (4.3x)" " * 136: 約 718ns → 約 396ns (1.8x)
- 非 ASCII を含む文字列 (キリル文字、日本語、絵文字など) は 3〜6% 程度遅くなる可能性
ascii_only?を 1回呼ぶコスト (数 ns) が乗るため- もともと 55〜80ns 程度の正規表現マッチに+3〜5ns くらいというオーダーで、実用上は誤差レベルの差
Active Support の仕様どおり Unicode 空白をサポートしていない fast_blank C 拡張と比べると、ASCII 領域ではほぼ同等の速度、非 ASCII を含む領域では (fast_blank が仕様を削っている分) まだ敵わないものの、「Rails 標準で C 拡張なしでここまで出る」というラインには到達しています。
挙動 (仕様) 面の影響
- ASCII 文字列に対しての判定結果は完全に不変です
[[:space:]]が ASCII でマッチする範囲 (9..13, 32) と、ループで許容しているバイトは一致しているため
- 非 ASCII を含む文字列・レガシーエンコーディング・不正バイト列に対しては、従来どおり正規表現ベースのロジックが使われるため、仕様変更はありません
- ベンチマークスクリプトおよびテストで、多数のパターンについて「PR 前後で
blank?の結果が一致する」ことが検証されています present?やObject#blank?全体としてのパブリックインターフェースにも変更なし
→ CHANGELOG も更新されていません
したがって、アプリケーションコード側で挙動が変わるケースは想定されておらず、実質的には「内部実装の純粋な高速化」と見なして問題ありません。
注意点 (開発者視点)
ascii_only?による分岐が入ったことにより、「blank?を極端に大量に・かつ非 ASCII 文字列ばかりに対して呼ぶ」という非常に特殊なワークロードでは、ごくわずかに CPU 使用率が増える可能性があります- ただしベンチマークでは 3〜6% 程度の差であり、ほとんどのアプリでは誤差レベル
fast_blankgem を併用していた場合:- Rails 側の
blank?がかなり速くなったため、「ASCII 領域だけを対象にfast_blankを使う」メリットはかなり小さくなります - それでも
fast_blankは仕様を削っている分 (Unicode 空白を無視する分) まだ有利なケースはありますが、挙動が Active Support と完全一致しないことには引き続き注意が必要です
- Rails 側の
- 参考情報 (あれば)
- 元ネタとなった C 拡張: fast_blank
- ASCII のみ対象にしているため、Unicode 空白などの扱いが Active Support と異なります
- この PR の特徴:
- C 拡張に頼らず Ruby + YJIT だけで高速化を達成
- 「ASCII のみを安全に最適化する」ことで、Unicode やレガシーエンコーディングの複雑な仕様を壊さないよう配慮
- 実装を
blank?内にインライン化し、呼び出しコストも最小化
Rails アプリで blank? / present? を多用している場合、特に英数字中心 (ASCII 中心) の入力を扱う箇所では、この変更により目に見える CPU 削減やレイテンシ改善が見込めます。
#59013 Fix #59012: Range conditions on single-mapping composed_of aggregations
マージ日: 2026/10/9 | 作成者: @kushagr3
- 概要 (1-2文で)
composed_ofで単一カラムにマッピングされた属性(例:balanceに対するMoney)に対してRange条件を指定した際、SQL がBETWEEN NULL AND NULLになってしまう不具合を修正した PRです。Rangeの両端値を正しく元カラムの値にマッピングしてから既存のクエリ生成ロジックに渡すことで、BETWEEN句が期待どおり生成されるようになりました。
- 変更内容の詳細(あればサンプルコードも含めて)
問題のあった挙動
composed_of で、例えば以下のようなモデルがあるとします:
class Customer < ActiveRecord::Base
composed_of :balance,
class_name: 'Money',
mapping: [%w(balance amount)]
endここで Money は Integer 等の1つのカラムにマッピングされた値オブジェクトです。
この状態で以下のようなクエリを書くと:
Customer.where(balance: Money.new(150)..Money.new(250)).to_sqlPR 前は次のような SQL が生成されていました:
... WHERE ("customers"."balance" BETWEEN NULL AND NULL)Range の端点がそのまま Money オブジェクトとして処理されてしまい、内部で SQL 用の値に変換されず NULL になっていたためです。その結果、該当レコードが1件もヒットしない状態になっていました。
修正内容
修正は PredicateBuilder#expand_from_hash に対する変更です:
where(balance: some_range)のようにハッシュで渡された条件のうち、- 対象が
composed_ofの「単一マッピング」なアグリゲーションで、 - 値が
Rangeの場合
- 対象が
- その
Rangeのbegin/endを、一度アグリゲーションのマッピングを通して「実カラムの値」に変換してから既存の処理に回すようにしました。
これにより PR 後は、同じコード:
Customer.where(balance: Money.new(150)..Money.new(250)).to_sqlが次のような SQL になります:
... WHERE ("customers"."balance" BETWEEN 150 AND 250)対応しているケース
説明とテストから、以下のパターンがサポートされています:
- 通常の範囲 (
Money.new(150)..Money.new(250)) - 排他的範囲 (
Money.new(150)...Money.new(250)) - beginless range (
..Money.new(250)) - endless range (
Money.new(150)..) - 配列内の Range(例:
where(balance: [Money.new(0)..Money.new(100), Money.new(200)..Money.new(300)])のようなケース)
SQL の生成自体は既存のパス(BETWEEN や >=, <= などを組み立てる部分)をそのまま利用しており、「Range の端点をまず正しいスカラ値に射影する」という点だけを追加で行っています。
対応していないケース
- 「複数カラムにマッピングされる
composed_of」(複合アグリゲーション)は本 PR の対象外であり、挙動は従来通りです。
例:mapping: [%w(lat latitude), %w(lng longitude)]のようなケースでは今回の変化はありません。
テスト・補助変更
activerecord/test/cases/finder_test.rbに該当ケースを検証するテストを追加。activerecord/test/models/customer.rbのMoneyに<=>を追加し、beginless / endless Range を使った比較ができるようにしています。Ruby の Range は比較のために<=>を用いる場面があるため、このオペレータが必要になります。
- 影響範囲・注意点
影響を受けるのは「単一カラムにマッピングされた
composed_of属性に対し、whereでRangeを使っていた / 使う予定があるアプリケーション」です。- これまでヒットしていなかったクエリが、今回から期待通りにレコードを返すようになります。
- 過去にこの不具合を回避するために独自のスコープや SQL フラグメント (
where("balance BETWEEN ? AND ?", ...)) で対処していた場合、今後はシンプルにwhere(balance: money_range)と書けるようになります。
beginless / endless Range もサポートされるので、以下のような書き方が(単一マッピングの
composed_of限定で)自然に動きます:ruby# 下限なし Customer.where(balance: ..Money.new(200)) # 上限なし Customer.where(balance: Money.new(100)..)複合
composed_ofには依然として Range 条件は自然には適用できません。複数カラムをマッピングしている値オブジェクトについて Range を使いたい場合は、引き続き自前のスコープや SQL 条件で対応する必要があります。既存の
PredicateBuilderのコードパスを壊さないよう、Range の端点変換のみを追加しているため、その他の where 条件(スカラ値 / 配列 / サブクエリなど)の挙動には影響しない設計になっています。
- 参考情報 (あれば)
- 対応 Issue:
- #59012: 単一マッピング
composed_ofに対する Range 条件がBETWEEN NULL AND NULLになる不具合報告 - #59016: 関連する議論・追跡 Issue(本 PR で Close)
- #59012: 単一マッピング
- 変更ファイル:
activerecord/lib/active_record/relation/predicate_builder.rb
Range 端点をアグリゲーションマッピングに通すロジックを追加。activerecord/test/cases/finder_test.rb
バグ再現 + 修正確認テストを追加。activerecord/test/models/customer.rbMoneyに<=>を実装。activerecord/CHANGELOG.md
上記バグ修正としてエントリ追加。
#59014 Combine constraint operations into bulk ALTER TABLE
マージ日: 2026/10/9 | 作成者: @kamipo
- 概要 (1-2文で)
bulk_change_tableなどでの制約操作(外部キー・ユニーク・チェック・排他制約)を、単一のALTER TABLEにまとめて発行できるようにするため、AlterTableを制約系の単一責任ポイントに整理した PR です。あわせて MySQL でremove_columnとremove_foreign_keyを同時に行う場合の二重 DROP 問題も解消しています。
- 変更内容の詳細
2-1. 背景: AlterTable を「単一の真実のソース」に統一
前の PR (#58264) で、テーブル変更系の SQL (ALTER TABLE ...) を組み立てるロジックが AlterTable に集約され、bulk_change_table からは CommandRecorder に記録されたコマンドを、そのまま AlterTable のメソッドにディスパッチできるようになりました。
今回の PR はそのフォローアップとして、制約関連の操作(外部キー、ユニーク制約、チェック制約、排他制約)も同じ仕組みで AlterTable に統一しています。
ポイント:
bulk_change_tableなどで行われる複数の制約操作が、- 以前: 複数の
ALTER TABLEやバラバラな SQL に分かれていた - 今回以降: 1 回の
ALTER TABLEにまとめて発行されやすくなる
- 以前: 複数の
- 複数 DB アダプタ(MySQL/PostgreSQL)間で、制約操作のインタフェースと実装位置を揃えた
これにより、マイグレーション実行時の:
- パフォーマンス(往復回数の削減)
- SQL の一貫性
- コードの保守性
が改善されます。
2-2. 制約系ロジックを SchemaStatements から AlterTable へ移動
従来、以下のような制約関連の補助メソッドは SchemaStatements 側にありました。
- オプション正規化
foreign_key_options
- 既存制約の名前解決
foreign_key_for!check_constraint_for!unique_constraint_for!exclusion_constraint_for!
今回の変更で、これらが AlterTable クラス側に移動しました。
理由:
bulk_change_table→CommandRecorder→AlterTableという流れで、table_name以外の引数だけを渡してAlterTable側で完結させたい- そのため、
AlterTableにCommandRecorderと同じシグネチャ(※ただしtable_nameを除く)のメソッドが必要
ざっくりしたイメージ:
# 以前(概念的なイメージ)
SchemaStatements#add_foreign_key(table, to_table, **options)
-> foreign_key_options(...)
-> foreign_key_for!(...)
-> 実際の ALTER TABLE は別レイヤで組み立て
# 以降
AlterTable#add_foreign_key(to_table, **options)
-> foreign_key_options(...) # AlterTable 内に移動
-> foreign_key_for!(...) # 同上
-> 自身で ALTER TABLE 用の構文を組み立てそれにより、
bulk_change_table内での制約操作もAlterTableに直接流れる- 制約名の自動推論やオプション整理が
AlterTable内部で完結
(add_index/remove_indexで既にやっている構造に合わせた)
2-3. MySQL: remove_column と remove_foreign_key の二重 DROP を回避
MySQL には以下の仕様があります:
ALTER TABLE ... DROP COLUMN ...でカラムを削除すると、- そのカラムを参照する外部キー制約も 自動で削除される
今回、bulk_change_table などで次のような操作が 1 ブロック内にあるケースを考えます:
change_table :posts, bulk: true do |t|
t.remove_foreign_key :users
t.remove :user_id
endAlterTable がコマンドを一括して組み立てると、
DROP FOREIGN KEY ...DROP COLUMN user_id
が同じ ALTER TABLE の中に入ります。
しかし MySQL は:
DROP FOREIGN KEYで外部キーが削除される- その後、
DROP COLUMNによる暗黙の外部キー削除と競合・重複しうる
あるいは逆順の場合でも、同じ外部キーを 2 回 DROP しようとする SQL になり MySQL に拒否される可能性があります。
この PR では:
- MySQL の
remove_columnが「参照外部キーを自動で削除する」ことを前提に、 - すでに
remove_foreign_keyによってキューに積まれた同じ外部キーについては、remove_column側が DROP をスキップする ように調整
要するに、1 回の ALTER TABLE の中で同じ FK を 2 回 DROP しないようにしています。
2-4. PostgreSQL 側の制約サポートの整理
PostgreSQL 用の schema_definitions.rb / schema_statements.rb でも:
- ユニーク制約
- 排他制約
- チェック制約
周りのロジックが AlterTable に合うように整理されています。
補助メソッドの場所や責務が整理され、AlterTable を通じて:
add_unique_constraintremove_unique_constraintadd_exclusion_constraintremove_exclusion_constraintadd_check_constraintremove_check_constraint
などが一貫的に扱われるようになっています。
2-5. テストの追加・更新
次のテストが拡充されています:
migration/exclusion_constraint_test.rbmigration/unique_constraint_test.rbmigration_test.rb(+84行)
内容としては:
bulk_change_tableで制約を同時に追加・削除するケース- 制約名の自動解決(
*_for!系メソッド)の挙動 - MySQL での
remove_column+remove_foreign_keyの組合せ
など、今回の変更で壊れやすいパスをカバーしています。
- 影響範囲・注意点
3-1. アプリケーションコードへの影響
通常のマイグレーション DSL 利用(例: add_foreign_key, remove_foreign_key, add_check_constraint など)に対してインタフェースの変更は原則ありません。
ただし、次のようなケースでは注意が必要です:
- 内部 API (
SchemaStatementsにあったforeign_key_for!など) を直接壊しながら呼んでいるコード- → これらは
AlterTableに移動しているため、直接利用していると影響を受ける可能性
- → これらは
bulk_change_table/change_table ... bulk: true内で複数制約操作を行っていて、DB に流れる SQL の細かい形を前提にしているケース- → SQL のまとまり方(一括
ALTER TABLEかどうか)が変わりうる
- → SQL のまとまり方(一括
3-2. MySQL 特有の注意点
MySQL で次のようなパターンを使っている場合:
change_table :posts, bulk: true do |t|
t.remove_foreign_key :users # 外部キー削除
t.remove :user_id # カラム削除
end- 以前: 場合によっては 2 回 DROP される SQL が出てエラーの可能性があった
- 以降:
remove_column側で二重 DROP を避けるようになった
そのため、この種のブロックはより安全に動くようになっています。
3-3. 複数制約の一括変更時の挙動
bulk_change_table で以下を同時に行う場合:
- 外部キーの追加/削除
- ユニーク制約の追加/削除
- チェック制約の追加/削除
- 排他制約の追加/削除(PostgreSQL)
これらが 1 回の ALTER TABLE にまとめられる 傾向が強くなります。
その結果:
- 実行パフォーマンスは改善が見込まれる
- 一方で、以前は「途中まで成功していたもの」が、DB 側の制約により「1 ステートメント丸ごと失敗」となるパターンが変化する可能性もある(トランザクション内であれば実害は限定的)
- 参考情報 (あれば)
- 該当 PR:
- フォローアップ元 PR (
AlterTableを単一ソース化): - 関連してクローズされた PR:
- 変更概要は
activerecord/CHANGELOG.mdに追記されており、今後のリリースノートにも反映される想定です。
#58998 Don't break the debug page on JRuby (w/o ErrorHighlight or RubyVM::AbstractSyntaxTree)
マージ日: 2026/10/9 | 作成者: @mjansing
- 概要 (1-2文で)
このPRは、JRuby や--disable-error_highlight環境の CRuby で、Rails のデバッグページ(開発時の「Action Controller: Exception caught」画面)が 500 の汎用エラーページにフォールバックしてしまう不具合を修正するものです。ErrorHighlightやRubyVM::AbstractSyntaxTreeが存在しない環境では、それらを使ったカラム位置特定処理をスキップし、デバッグページ自体は正常表示されるようにします。
- 変更内容の詳細
問題の背景
Rails のデバッグページでは、例外発生箇所をハイライト表示するために、以下を利用するルートがあります:
ErrorHighlight(Ruby 3.2 以降などで利用可能なエラー箇所強調機能)RubyVM::AbstractSyntaxTree(CRuby 固有)
ところが以下の3メソッドでは、これらが存在するかを確認せずに直接参照していました:
Thread::Backtrace::Location#spot
(ビュー以外の例外時、例: コントローラやモデルでの例外)ExceptionWrapper::SourceMapLocation#spotActionView::Template#spot
(ビュー内での例外)
JRuby には RubyVM が存在せず、かつ ErrorHighlight も未サポートのため、上記いずれかから NameError が発生し、その NameError 自体により ShowExceptions が「安全側」としてパブリックな 500 ページにフォールバックしてしまっていました(ログにも残らない)。CRuby でも --disable-error_highlight を付けると同様の問題が起きます。
Rails 7.1 ではビュー内の例外、Rails 8.0 以降ではほぼすべての例外でこの問題が顕在化します。
修正方針
3つの #spot メソッドで共通の方針をとっています:
ErrorHighlight/RubyVM::AbstractSyntaxTreeが「定義されていない」環境では、それらを使うコードパスに入らずnilを返す。ExceptionWrapper側は既に「spotがnilを返す場合は、カラムハイライトなしの素のソース断片を表示する」ロジックを持っているため、挙動は「ハイライトだけ諦めて、デバッグページ自体は表示する」という状態に落ち着く。
1) ExceptionWrapper::SourceMapLocation#spot
- ここは
Delegator(=BasicObjectベース)を継承しているため、単にdefined?(RubyVM::AbstractSyntaxTree)と書いてもnilを返してしまう(BasicObjectには Kernel メソッドがない)という事情があります。 - そのため、
::RubyVMとトップレベル絶対参照でチェックするようにした上で、RubyVM::AbstractSyntaxTreeを使うルートに入るかどうかを決めています。 - 疑似コードでいうと、以下のような形になります(実際のコードは若干異なりますが本質はこのチェックです):
def spot
return unless defined?(::RubyVM::AbstractSyntaxTree) && defined?(ErrorHighlight)
# ... AST を使って位置特定処理 ...
enddefined?(::RubyVM)のようなチェックでNameError発生を防ぎつつ、JRuby のようにRubyVMそのものがない環境ではnilを返すだけにしています。
2) ActionView::Template#spot
- ビュー内のエラー時、テンプレートのどの行・カラムかを特定する処理でも同様の対応。
ErrorHighlightや AST を使える環境では従来通りカラムハイライトが行われ、使えない環境ではnilを返し、デバッグページではハイライトなしで該当行が表示されます。
3) Thread::Backtrace::Location#spot
- ビュー以外(コントローラ・モデルなど)の例外箇所についても同様にガードを追加。
- これにより、すべての例外経路で
ErrorHighlight/RubyVM::AbstractSyntaxTree非対応環境でもNameErrorが発生しないようになります。
4) テストの追加
- 以下の2つのテストファイルに変更が入っています:
actionpack/test/dispatch/exception_wrapper_test.rbactionview/test/template/render_test.rb
- テスト内で、一時的に
ErrorHighlightやRubyVM::AbstractSyntaxTreeといった定数を「削除」して実行し、JRuby 相当の環境を再現しています。 - 変更前だと
NameErrorでテストが落ちるのに対し、変更後はデバッグページが正常表示されることを確認できるようになっています。
5) 動作確認用スクリプト
PR本文に、debug_page_without_error_highlight.rb という自己完結した再現 / 回帰テスト用スクリプトが含まれています。ポイント:
RAILS_VERSIONもしくはRAILS_PATHから Rails をロード。- 簡易な
TestAppを定義し、/controller_errorはコントローラ内のタイポ (redirect_ot)/view_errorはビュー内のタイポ (lin_to)
- どちらのパスにアクセスしても、
- レスポンスコードが 500
- HTML 本文に
"Action Controller: Exception caught"が含まれる(= デバッグページ) - タイポしているメソッド名文字列が含まれる
ことを検証します。
実行例(PR本文記載):
ruby debug_page_without_error_highlight.rb # 通常の CRuby ではパス
ruby --disable-error_highlight debug_page_without_error_highlight.rb # 修正前は CRuby でも失敗
jruby debug_page_without_error_highlight.rb # 修正前の JRuby では失敗
RAILS_PATH=../rails ruby --disable-error_highlight debug_page_without_error_highlight.rb
# ローカル Rails チェックアウトに対して修正を反映してテスト- 影響範囲・注意点
影響範囲
- 対象環境:
- JRuby 全般(
RubyVMが無い) - CRuby で
--disable-error_highlightを付けた起動 - 将来的に、何らかの理由で
ErrorHighlight/RubyVM::AbstractSyntaxTreeが利用不能なカスタムビルド Ruby
- JRuby 全般(
- 対象の Rails バージョン:
- バグ自体:
- Rails 7.1: 主にビュー内エラーで影響
- Rails 8.0 以降: ほぼすべての例外で影響
- PR 自体は main ベースですが、8.1.4 へのクリーンなバックポートが可能と明記されています(
8-1-stableへのバックポート希望)。
- バグ自体:
実際の挙動の変化
- これまで(JRuby /
--disable-error_highlight環境):- 例外発生 → デバッグページ描画中に
NameError(ErrorHighlight/RubyVM未定義) →ShowExceptionsが 500 公開ページにフォールバック → 開発時でも中身が全く見えない。
- 例外発生 → デバッグページ描画中に
- これから:
- 例外発生 →
spotがnilを返す(ハイライト情報なし)→ExceptionWrapperが「素のソース断片のみ」を出力 → デバッグページは従来通り表示されるが、カラム単位のハイライトだけが無効になる。
- 例外発生 →
注意点
- 表示上の差分:
- カラム位置の強調表示(ErrorHighlight による赤いカーソルのようなマーカー)が表示されない環境がある点に注意。
ただしこれはあくまで「視覚的な改善の有無」の違いであり、機能的には従来どおりスタックトレースと該当行を確認できます。
- カラム位置の強調表示(ErrorHighlight による赤いカーソルのようなマーカー)が表示されない環境がある点に注意。
- 依存コードへの影響:
#spotがnilを返しうる前提は元々ExceptionWrapperで考慮済みですが、もしアプリや gem 側でThread::Backtrace::Location#spot/ActionView::Template#spotを直接呼び出しており、nil非許容の前提を置いている場合は、理論上は挙動が変わる可能性があります(とはいえ一般的ではないユースケースです)。
- テスト戦略:
- Ruby の定数を一時的に
remove_constするようなテストが含まれるので、同種のテクニックを真似する場合はテスト間の副作用に注意する必要があります(PR 内テストではテストスコープに閉じているため問題なし)。
- Ruby の定数を一時的に
- 参考情報 (あれば)
- このPRと類似のパターンとして、
Ractor非対応環境を考慮してdefined?(Ractor)チェックを追加した PR #58684 が挙げられています。
Rails 本体として「特定 Ruby 実装固有の機能は必ずdefined?でガードする」という方針を強化している流れといえます。 - このPRは
CHANGELOG更新は行っておらず、「軽微なバグ修正」として扱われていますが、JRuby ユーザーにとってはデバッグ体験に直結する実質的な修正です。JRuby あるいは--disable-error_highlightを使っている開発環境では早めの取り込み・バックポート検討が推奨されます。
#59015 [8-0-stable] Don't break the debug page on JRuby (w/o ErrorHighlight or RubyVM::AbstractSyntaxTree)
マージ日: 2026/10/9 | 作成者: @mjansing
- 概要 (1-2文で)
Rails 8.0 系で、JRuby や--disable-error_highlight環境のときにデバッグページ(例外ページ)が壊れてしまう問題を修正した PR のバックポートです。ErrorHighlightやRubyVM::AbstractSyntaxTreeが使えない環境でも、例外表示とソースコード抜粋まわりが安全に動くように調整されています。
- 変更内容の詳細
※元 PR #58998 のバックポートで、8-0-stable 向けにテストだけ微調整されています。実装の趣旨は同じです。
2-1. 例外ラッパー (exception_wrapper.rb) の修正
対象ファイル:
actionpack/lib/action_dispatch/middleware/exception_wrapper.rb(+1/-1)
例外ページで表示する「ソースコード抜粋」やハイライトの生成部分が、以下のような前提に依存していた可能性があります。
ErrorHighlight拡張が有効である(CRuby のみ)RubyVM::AbstractSyntaxTreeが存在する(CRuby 依存 API)- それらから得られる情報(例: エラー位置の詳細)が必ずある
JRuby や --disable-error_highlight の CRuby ではこれらが利用できないため、nil や未定義メソッドで落ちてしまい、結果としてデバッグページ自体が壊れていました。
この PR では概ね以下のような方向の修正が入っています(疑似コード):
def source_extracts
# 以前: ErrorHighlight 前提で trace 情報などにアクセスしていた
# 修正後: 機能が存在しない場合は安全にスキップ / フォールバック
if defined?(ErrorHighlight) && ErrorHighlight.respond_to?(...)
# 可能な範囲でハイライト付きの情報を組み立てる
else
# ErrorHighlight なしでも最低限のソース抜粋が表示できるようにする
end
end要点は「ErrorHighlight/AST がない場合にも例外ページのレンダリングが失敗しないようガードを入れる」ことです。
2-2. テンプレートレンダリング (template.rb) の修正
対象ファイル:
actionview/lib/action_view/template.rb(+2/-0)
Action View の Template クラスが、例外発生時に
- テンプレートのどの行で失敗したか
- その周辺のソースコード
などを抽出する処理を持っています。この過程でも ErrorHighlight や RubyVM 依存の API に触れていた部分があり、JRuby 等で壊れる原因になり得ます。
この PR では、例外情報から「ハイライト情報」を取り出す処理に対して、以下のような条件付きの分岐や nil ガードが追加されています:
def some_backtrace_or_source_helper(exception)
# Ruby 実装や機能の有無を確認
if defined?(ErrorHighlight) && exception.respond_to?(:detailed_message)
# 拡張された情報からハイライトを抽出
else
# 従来の backtrace のみを使うフォールバック
end
endこれにより、ErrorHighlight をサポートしていない Ruby でも例外表示が壊れず、かつサポートしている環境ではリッチな情報を利用できます。
2-3. Thread::Backtrace::Location 拡張の修正
対象ファイル:
activesupport/lib/active_support/core_ext/thread/backtrace/location.rb(+1/-1)
Thread::Backtrace::Location を拡張している Active Support のコードも微修正されています。
典型的には:
- backtrace の 1 要素からファイル・行番号・メソッド名などを抽出
- それを利用してログメッセージや UI 上のリンクを構築
といった用途があります。ここでも Ruby 実装の違いでインターフェイスが微妙に異なることがあり、JRuby で例外ページをレンダリングするときの互換性問題を招くことがあります。
1 行の差分ですが、例えば以下のような変更が想定されます。
- JRuby では存在しないメソッドを呼ばないようにする
- 期待したフォーマットではない場合に安全にフォールバックする
# 例: to_s のフォーマットに依存しないようにするなど
def to_s
if respond_to?(:path)
"#{path}:#{lineno}:in `#{base_label}'"
else
super # 実装依存のデフォルト挙動へ
end
end2-4. テストの追加・調整
対象ファイル:
actionpack/test/dispatch/exception_wrapper_test.rb(+23/-0)actionview/test/template/render_test.rb(+31/-0)
今回の PR の肝は「壊れていた挙動が再発しないこと」を担保するテスト追加です。
exception_wrapper_test.rb
説明文にある通り、#source_extracts の戻り値の仕様差を吸収するため、8.0 系では :trace キーが存在しない前提でテストを書き直しています。
- main:
source_extractsが:traceを含む(ErrorHighlight 対応が進んだ状態) - 8-0-stable:
:traceがまだないので、traceに依存しない形でアサート
例(イメージ):
def test_source_extracts_works_without_error_highlight
extracts = wrapper.source_extracts
assert_equal "...", extracts[:source]
# 8.0 では :trace がない前提なので、ここはチェックしない
endrender_test.rb
Action View のテンプレートレンダリングで例外が発生するケースを用意し、
- CRuby +
--disable-error_highlight - JRuby
といった環境で
- 例外ページが 500 エラーにはなるが、内部エラーにはならずにレンダリングが完了する
- 例外メッセージやソース抜粋が期待通りに含まれている
ことを確認するテストが追加されています。
疑似コード:
def test_debug_page_on_jruby_without_error_highlight
assert_nothing_raised do
get :action_which_raises
assert_response 500
assert_includes response.body, "RuntimeError"
assert_includes response.body, "app/views/..."
end
end- 影響範囲・注意点
対象バージョン
- Rails 8.0 系 (
8-0-stableブランチへのバックポート) - 少なくとも 8.0.5.1 相当のコードベースで確認されている
- Rails 8.0 系 (
影響を受ける環境
- JRuby (確認済: JRuby 10.0.6.0)
- CRuby で
--disable-error_highlightオプションを付けて実行している環境 RubyVM::AbstractSyntaxTreeを持たない Ruby 実装全般
ユーザー影響
- 上記環境で「例外が発生したときのデバッグページが内部エラーで落ちる」「開発環境でエラー画面が真っ白・500 になる」等の問題が解消されます。
- 本番環境で標準の 500 エラーページしか出さない構成なら、影響はほぼありません(内部的な例外ラップ処理が少し安定した程度)。
互換性・破壊的変更
- 公開 API の仕様変更は原則なく、「あるはずの機能がない場合に落ちないようにする」ガード追加が中心です。
- ただし、
source_extractsの返却値に含まれる情報が、Ruby 実装ごとに微妙に異なるケースがある点には注意してください。- 例: main ブランチにある
:traceキーが 8.0 にはまだない、など - アプリやライブラリ側で
source_extracts[:trace]の存在を前提にしていると将来バージョン依存バグになる可能性があります。
- 例: main ブランチにある
- 参考情報 (あれば)
- 元 PR(8.1 以降向けの変更)
- 本 PR
ErrorHighlightについて- Ruby 3.1+ で導入されたエラー行のハイライト機能で、
Exception#detailed_messageなどを通じて利用されます。 - JRuby などでは未実装、あるいは挙動が異なる場合があるため、この PR のように存在チェックやフォールバックが重要になります。
- Ruby 3.1+ で導入されたエラー行のハイライト機能で、
#59006 Make ActiveSupport::Ractors.shareable? version independent
マージ日: 2026/10/9 | 作成者: @kamipo
- 概要 (1-2文で)
ActiveSupport::Ractors.shareable?の実装から Ruby バージョン依存のガードを外し、Ractorが存在する環境では常にRactor.shareable?に委譲するように変更した PR です。これにより、shareable?(nil)など一部のケースで誤った偽値が返っていた不具合も修正されています。
- 変更内容の詳細
これまでの挙動
make_shareableとshareable?は、Ruby の Ractor 機能に対応するための ActiveSupport 側のラッパーです。make_shareableは以下の理由で Ruby のバージョンを見て挙動を変えていました:Ractor.make_shareable(obj)は対象オブジェクトを freeze する副作用がある- 場合によっては
Ractor::IsolationErrorを投げる - そのため、Rails が公式にサポートしていない Ruby バージョンでは「何もしない (no-op)」でそのまま
objを返す実装になっていた
一方、
shareable?は副作用のない述語メソッドであり、Ractor.shareable?は Ruby 3.0 から存在- Rails が要求する最小 Ruby バージョンより十分低い
- にもかかわらず、
make_shareableと同様のバージョンガード (RUBY_VERSION >= "4.0") に縛られていました。
さらに問題として、Ractor を使えない環境向けのフォールバック実装で
rubydef shareable?(obj) obj endのような形になっていたため、例えば:
rubyActiveSupport::Ractors.shareable?(nil) #=> nil (偽) ActiveSupport::Ractors.shareable?(false) #=> false (偽)と、実際には「共有可能」なオブジェクトにもかかわらず「偽」と判定されるバグがありました。
この PR の変更点
shareable?を Ruby バージョンに依存させないRUBY_VERSION >= "4.0"という条件を取り除き、- 「
Ractorクラスが定義されているかどうか」だけを見て挙動を切り替えるように変更しました。
実装方針の統一
Ractorが存在する MRI Ruby 等では、常にRactor.shareable?(obj)を呼び出すように変更。Ractorが存在しないランタイム(例: 現状の JRuby)では、shareable?は常にtrueを返すように変更。- これは
make_shareableが Ractor 非対応環境では常に成功扱いである、という既存のポリシーと一貫しています。
- これは
フォールバックの間違った挙動の修正
前述の「
shareable?(nil)がnilを返す」ようなケースは、常に真偽値を返す形に修正されました。新仕様のイメージ:
rubymodule ActiveSupport module Ractors def self.shareable?(obj) if defined?(::Ractor) ::Ractor.shareable?(obj) else true # Ractor 非対応環境では常に共有可能扱い end end end end
- 影響範囲・注意点
shareable? の戻り値が変わる可能性
- Ractor が利用可能な Ruby (3.0+) では、これまでバージョンガードによって Ractor を使っていなかった場合でも、今後は
Ractor.shareable?の正確な判定結果が返ってきます。 - 以前は「単に
obj自体を返していた」環境では、nil/falseを返していたケースが今後はtrue(Ractor 無し環境)またはRactor.shareable?(obj)の結果(Ractor あり環境)に変わります。 if ActiveSupport::Ractors.shareable?(obj)のように真偽値条件として使っていたコードが、「nilだから偽になることを期待」していた場合は意味が変わりますが、そのような使い方はそもそもバグ寄りです。
- Ractor が利用可能な Ruby (3.0+) では、これまでバージョンガードによって Ractor を使っていなかった場合でも、今後は
JRuby 等 Ractor 非対応環境での意味
- これまでも
make_shareableは常に成功扱い(実質 no-op)でしたが、shareable?もそれに揃えて常にtrueを返すようになりました。 - Ractor 非対応環境では「共有可能性を判定する」というより、「Ractor という概念自体がないので、制約なし」と解釈した API デザインになっています。
- これまでも
Ruby 4.0 への移行時の挙動変化はなくなる
- 以前は
RUBY_VERSION >= "4.0"をトリガーに挙動が変わる余地がありましたが、今回の変更でバージョン依存がなくなったため、Ruby 4.0 へのアップグレードによってshareable?の挙動が変わる、といった心配は減ります。 - かわりに、「Ractor が定義されているかどうか」でのみ分岐するため、Ruby が Ractor を将来どう扱うか(例えば標準で無効化する等)に依存します。
- 以前は
- 参考情報 (あれば)
- Ruby 公式ドキュメント:
Ractor.shareable?: https://docs.ruby-lang.org/en/master/Ractor.html#method-c-shareable-3FRactor.make_shareable: https://docs.ruby-lang.org/en/master/Ractor.html#method-c-make_shareable
- Rails 側関連コード:
activesupport/lib/active_support/ractors.rb(この PR で変更されたファイル)
- 背景知識:
- Ractor は Ruby 3.0 で導入された並行実行機構で、「共有可能オブジェクト (shareable object)」のみを Ractor 間で共有できるという制約があります。そのため Rails は、
make_shareableとshareable?を通じて Ractor 対応を抽象化しています。
- Ractor は Ruby 3.0 で導入された並行実行機構で、「共有可能オブジェクト (shareable object)」のみを Ractor 間で共有できるという制約があります。そのため Rails は、
#58964 Speed up structured event logging
マージ日: 2026/10/9 | 作成者: @linyiru
- 概要 (1-2文で)
本PRは、Railsの構造化イベントロギング(start_processing,process_actionなど)の内部実装を最適化し、ログの内容・挙動を変えずにリクエストあたり数%レベルで処理コストを削減する変更です。主にActiveSupport::EventReporterとLogSubscriber、およびColorizeLoggingの内部実装を見直し、不要なオブジェクト生成や命令数を抑えています。
- 変更内容の詳細
全体像
対象となるのは、通常の本番運用で config.log_level = :info のとき、各リクエストごとに必ず通るイベントロギングの経路です:
ActiveSupport::EventReporter#notifyActiveSupport::EventReporter::LogSubscriberActiveSupport::ColorizeLoggingによるログ出力ヘルパ
ここで、「外から見える挙動・ログ出力」は変えずに、Ruby 3.4 + YJIT 環境での命令数・オブジェクト割り当て数を減らす最適化が行われています。
ActiveSupport::EventReporter#notify の最適化
1) 購読者の収集: filter_map → each に変更
元々は「購読者一覧から、条件に合うものだけを集める」処理に filter_map が使われていましたが、これをより単純な each ベースのループに変えています。
- 理由: YJIT 下での命令数を約半分に出来る(著者の計測結果)
- 挙動: 変化なし。特定のイベントに対して購読しているサブスクライバだけが呼び出されるという意味では同じ。
イメージとしては、以下のような差分です(実際のコードは多少異なりますが、やっていることのレベル感):
# 以前(イメージ)
subscribers = @subscribers.filter_map { |sub| sub if sub.interested_in?(event) }
# 変更後(イメージ)
subscribers = []
@subscribers.each do |sub|
subscribers << sub if sub.interested_in?(event)
end機能的には同じですが、filter_map によるブロック呼び出しや内部実装コストを避けています。
2) kwargs の扱い: キーワード引数のスプラットを避ける
notify → resolve_payload に引数を渡すとき、これまでは:
resolve_payload(payload, **kwargs)のように **kwargs で新しいハッシュを生成していました。PRではこれを 位置引数として渡す 形に変更しています(内部シグネチャに合わせている)。
ポイント:
- Ruby の仕様上、メソッドの
**kwargsは呼び出し側のハッシュそのものではなく、常に新しいハッシュになるため、**は常にコスト(ハッシュ生成)を伴う。 - 「呼び出し側のハッシュが破壊されない」という安全性は、位置引数にした後も守られている(そもそも
resolve_payloadが引数を勝手に破壊しない前提/テストでカバー)。
これにより、イベントごとの不要なハッシュ生成を減らしています。
LogSubscriber.log_level_satisfied? の最適化
ログレベル判定ロジックが変更されています。
変更前の問題点
概ね以下のような処理をしていました(擬似コード):
def log_level_satisfied?(logger, level)
# 1. LOG_LEVELS.include?(level) でサポート対象かチェック
# 2. "#{level}?" という文字列メソッド名を作る
# 3. logger.send("#{level}?") で判定
end問題:
LOG_LEVELS.include?(level)でArray#include?が毎回走る"#{level}?"の文字列が毎回生成されるsendベースの動的ディスパッチ
変更後
- 「レベル → 判定用Proc(またはラムダ)」のような 凍結済みハッシュ を用意し、そこから直接判定関数を取得する方式に変更
- その結果:
include?チェックと文字列生成を削除- ハッシュ参照 + 既存の判定ロジック呼び出しだけで済むように
イメージ:
LOG_LEVEL_CHECKS = {
info: ->(logger) { logger.info? },
debug: ->(logger) { logger.debug? },
# ...
}.freeze
def log_level_satisfied?(logger, level)
check = LOG_LEVEL_CHECKS[level]
return false unless check # サポート外レベル
check.call(logger)
endこれにより、イベントごとに余計な文字列オブジェクトを生成せず、分岐も単純化されます。
ActiveSupport::ColorizeLogging の最適化
ColorizeLogging モジュールにある各レベルメソッド(例: debug, info, warn, …)で、logger の参照を 1 回だけ取る ように変更しています。
変更前の典型パターン(イメージ)
def info(message = nil, &block)
if logger && logger.info?
logger.info(message, &block)
end
endここでは logger を 2 回呼んでいます(logger && と logger.info? のところ)。
変更後のパターン(イメージ)
def info(message = nil, &block)
log = logger
return unless log && log.info?
log.info(message, &block)
endlogger呼び出しは1回だけ- ローカル変数へのキャッシュは、Ruby処理系的に最適化しやすく、YJIT 下でも有利
テストでは、以下のパターンをカバー:
- 各ログレベルメソッド(
debug,info,warn,error, など)が- logger が
nilのときに何もしないこと - logger がいるときに正しく呼び出されること
prognameや block の有無を含めて正しいシグネチャで動くこと
- logger が
テスト・検証と互換性
PR本文によると、かなり手厚く「挙動が変わっていない」ことが検証されています。
- 差分比較スクリプトで、旧実装・新実装で 1969 行分の出力を比較 → 完全一致
notifyのフィルタ有無・サブスクライバ有無- payload のキーが文字列/シンボル混在
- payload フィルタ on/off
- 想定外の引数パターン
- 全ての
event_log_level(サポート外も含む) ColorizeLoggingの各レベルメソッド + logger/progname/block の有無
- Rails のログ全体(アプリ単位)の diff を 20 通りの設定で比較 → 完全一致
- 新規テスト追加:
- サブスクライバフィルタの順序
- kwargs のキーのシンボル化
- サポート外イベントログレベルの扱い
ColorizeLoggingレベルメソッドの logger 有無パターン
- Active Support / Action Pack / Action View のテストスイートはすべてグリーン
- mutation testing 的な検証: 変更したコードに対する 25 個のミューテーションが、いずれも少なくとも 1 テストを落とす(テストの網羅性を確認)
ベンチマーク結果
計測環境:
- Ruby 3.4.10
- YJIT 有効
- 最小構成の production アプリ
- 1 アクションで
render plain: "Hello"のみ - ログ出力先は
/dev/null
- 1 アクションで
- 計測指標:
perf statによるユーザ空間命令数、および allocations
結果:
| middleware 構成 | user-space instructions | allocations |
|---|---|---|
| trimmed stack | 724.5k → 699.5k (−3.4%) | 242 → 234 |
| default stack | 857.6k → 835.2k (−2.6%) | 302 → 294 |
- リクエストあたり 2.6〜3.4% 程度の命令数削減
- allocations もわずかに削減(イベントごとのハッシュ/文字列生成などが効いている)
なお、著者は wall-clock 時間ではノイズが大きすぎるため、命令数を主指標にしています。
- 影響範囲・注意点
影響範囲
対象:
ActiveSupport::EventReporterを通じて行われる構造化イベントロギング全般- Action Pack / Action View など Rails コアが発行するイベント
ActiveSupport::LogSubscriberを継承している独自サブスクライバも含めて、基本的にすべてのイベント購読処理
非対象(挙動に影響しない想定):
- ログフォーマット(テキストログ/構造化ログ等)
- ログ行数・メッセージ内容
- パブリックAPIのシグネチャ(一般的な使い方の範囲)
注意点
- 「挙動は変わらない」前提の最適化ではあるものの、イベントの処理順序やフィルタの適用順に依存しているような かなりトリッキーな LogSubscriber 実装 をしている場合は、一応動作確認した方が安全です。
- ただしPRでは「サブスクライバフィルタの順序」もテストでカバーしているため、通常の依存の仕方なら問題ないはずです。
log_level_satisfied?は内部実装が変わりましたが、外部から見た判定結果(どの level のときにログが出るか)は変わらないように注意深くテストされています。- YJIT 前提の最適化が多いものの、インタプリタや他のJITで悪影響が出るような変更(例: 大量の分岐追加など)はなく、単純化の方向なので副作用は小さいと思われます。
- 参考情報 (あれば)
- このPRで触れられている対象クラス・モジュール:
ActiveSupport::EventReporterActiveSupport::EventReporter::LogSubscriberActiveSupport::ColorizeLogging
- 構造化イベントロギング周りをカスタマイズしている場合(自作 LogSubscriber、独自の event_log_level ロジックなど)、このPRの変更を踏まえた挙動確認をテストに追加しておくと安心です。
- 変更行数としては +75 / -17 と比較的小規模ですが、ホットパス(全リクエストで必ず通るロギング処理)に集中した改善なので、トラフィックの多いサービスほど恩恵が出やすいタイプのチューニングです。
#59011 Fix broken cross-guide anchor links [ci skip]
マージ日: 2026/10/9 | 作成者: @SatvikMishra08
- 概要 (1-2文で)
Rails Guides 間のクロスリンクのうち、アンカー先のセクション名変更や移動によって壊れていたリンクを現状のアンカーIDに合わせて修正した PR です。ドキュメントのみの変更で、アプリケーションコードや挙動には影響しません。
- 変更内容の詳細
この PR では、Rails Guides 内で別ガイドの特定セクションへ飛ぶリンク(#anchor 付きの URL)が、実際の生成済みページに存在するアンカーIDと一致するように修正されています。いずれも「ガイドA → ガイドBの特定セクション」へのリンクで、対象セクションのタイトル変更や別ガイドへの移動によってアンカーが無効になっていたものです。
修正されたリンク一覧は以下の通りです(左が旧リンク、右が新リンク):
Action View Overview ガイド内
- レイアウトとレンダリング関連のセクション:
layouts_and_rendering.html#template_lookup_hierarchy
→ 同一ガイド内の#template-lookup-hierarchy(セクションIDがハイフン区切りに変わった)layouts_and_rendering.html#setting_layouts_in_controllers
→#specifying-layouts-for-controllers
いずれも同じ Action View Overview ガイド内の別セクションを指しており、ファイル名付きの古いパスから、現状のアンカー(ハイフン区切り)に揃えています。
- レイアウトとレンダリング関連のセクション:
Working with JavaScript in Rails ガイド内 (同じ箇所が2回)
- アセットパイプラインのロードパス説明へのリンク:
asset_pipeline.html#load_paths
→#load-paths
こちらも、セクションIDがスネーク風 (
load_paths) からケバブケース (load-paths) に変更されていたため修正されています。- アセットパイプラインのロードパス説明へのリンク:
Testing ガイドから Action View のパーシャル解説へのリンク
- 旧:
layouts_and_rendering.html#using-partials
- 新:
action_view_overview.html#partials
パーシャルに関する説明が
layouts_and_renderingからaction_view_overviewに移動しており、かつアンカーIDがusing-partialsからpartialsに変わっているため、その両方を反映しています。- 旧:
Sign Up and Settings ガイドからネストレイアウト解説へのリンク
- 旧:
layouts_and_rendering.html#using-nested-layouts
- 新:
action_view_overview.html#nested-layouts
ネストレイアウトのドキュメントも Action View Overview に移動しており、それに伴うリンク先ファイル名とアンカーの更新です。
- 旧:
Form Helpers / Active Model Basics から Strong Parameters 解説へのリンク
- 対象ファイル:
guides/source/form_helpers.mdguides/source/active_model_basics.md
- 旧:
action_controller_overview.html#strong-parameters
- 新:
#securing-submitted-parameters
Strong Parameters のセクション名が「Strong Parameters」から「Securing submitted parameters」に変更され、ガイド内のアンカーも
#strong-parametersから#securing-submitted-parametersに変わったため、それに合わせています。
(リンク元はいずれも Action Controller Overview への参照で、ファイル名は省略しても同一ガイドとして解決される構成です。)- 対象ファイル:
Engines ガイドから I18n ガイドへのリンク
- 旧:
i18n.html#providing-translations-for-internationalized-strings
- 新:
#creating-locale-dictionaries
I18n ガイド内で、翻訳辞書の作り方を説明するセクションのタイトル変更に伴い、アンカーIDも更新されているため、それに追従しています。
- 旧:
修正はすべて Markdown で書かれた Guides ソースのリンク部分([テキスト](url#anchor))の URL 断片を書き換えるだけで、テキスト内容や構成自体は変更していません。
変更行数も各ファイル1〜2行程度で、全体で追加9行・削除9行の差分に収まっています。
- 影響範囲・注意点
影響範囲
- Rails Guides(特に edgeguides.rubyonrails.org)の閲覧体験のみが対象です。
- これまでページ先頭に飛んでしまっていたリンクが、該当セクションの見出しまで正しくジャンプするようになります。
- アプリケーションコード、API、挙動には一切影響しません。
注意点
- すでに外部のブログ記事や社内ドキュメントなどで、古いアンカー(例:
#strong-parameters)に直接リンクしている場合、それらは引き続き 404 もしくはページ先頭へのジャンプになる可能性があります(この PR は Rails Guides 内部リンクのみ修正)。 - アンカーIDはガイドの見出しを元に自動生成・管理されているため、今後見出し名を変更した場合は同様のリンク切れが再発する可能性があります。
→ PR 説明にもある通り、別 PR (#58028) で「ガイド生成時にクロスガイドリンクの壊れを検出して警告する仕組み」が導入されつつあり、今後は生成時に破損リンクを検出できるようになる想定です。
- すでに外部のブログ記事や社内ドキュメントなどで、古いアンカー(例:
- 参考情報 (あれば)
- 該当 PR:
- 関連 PR(クロスガイドリンクの警告・他ガイド修正):
- 対象となるガイド(edgeguides / master ブランチ相当):
- Action View Overview: https://edgeguides.rubyonrails.org/action_view_overview.html
- Working with JavaScript in Rails: https://edgeguides.rubyonrails.org/working_with_javascript_in_rails.html
- Testing: https://edgeguides.rubyonrails.org/testing.html
- Form Helpers: https://edgeguides.rubyonrails.org/form_helpers.html
- Active Model Basics: https://edgeguides.rubyonrails.org/active_model_basics.html
- Engines: https://edgeguides.rubyonrails.org/engines.html
- I18n: https://edgeguides.rubyonrails.org/i18n.html
#58953 [ci skip] Remove schema.rb column sorting from 8.1 guides
マージ日: 2026/10/9 | 作成者: @trysmr
- 概要 (1-2文で)
Rails 8.1 で導入されたと説明されていた「schema.rbのカラム名アルファベット順ソート」の記述が、実際には 8.1.4 でリバートされていることに合わせて、公式ガイドから削除された PR です。挙動自体は既に元に戻っており、ドキュメントだけを実態に合わせて修正しています。
- 変更内容の詳細
背景
- 過去の変更 (#53281) で「
schema.rb内のテーブルカラムをアルファベット順に並べ替える」仕様が導入されました。 - この仕様は Rails 8.1 のリリースノートおよび 8.0 → 8.1 アップグレードガイドにも「新しい挙動」として記載されました(コミット
54e6d80495)。 - しかし後に #56842 でこのソート機能自体がリバートされ、Rails 8.1.4 には「ソートしない」状態で出荷されています。
- にもかかわらず、ガイド(ドキュメント)側の記述が更新されておらず、「今もソートされる」かのように読める状態になっていました。
この PR が行ったこと
以下の 2 つのガイドファイルから、該当の説明文を削除しています。
guides/source/8_1_release_notes.md- Active Record セクション内の "Notable changes" にあった
「schema.rbのカラムがアルファベット順にソートされる」という記述を削除。
- Active Record セクション内の "Notable changes" にあった
guides/source/upgrading_ruby_on_rails.md- 「The table columns inside
schema.rbare now sorted alphabetically.」という節ごと削除。
- 「The table columns inside
差分としては:
- 追加行: 0
- 削除行: 6
のみで、コードではなくドキュメントだけの変更です。
コード挙動のイメージ
この PR でコードは一切変わっていないため、挙動としては「今まで(8.1.4 以降)と同じ」です。schema.rb はこれまで通り、「マイグレーションの実行結果に基づく順序」でカラムが記録されます。例えば:
# db/schema.rb (例)
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
t.boolean "admin"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
endアルファベット順ソートが有効なら admin, email, name … のように並ぶはずですが、現状はマイグレーション定義順など従来のルールのままです。
- 影響範囲・注意点
挙動の変更はない
すでに #56842 によって挙動はリバート済みであり、この PR は「ガイドの記述を実装に合わせる」だけです。既存アプリのschema.rbの生成・更新挙動は一切変わりません。ドキュメントを見て勘違いしていた場合の影響
以前の 8.1 リリースノートやアップグレードガイドを見て:- 「
schema.rbのカラムが絶対にアルファベット順になるはず」と思っていた - それを前提にレビューやツール(差分チェックなど)を設計した
といった場合、その前提は誤りであるとわかります。
実際には:
- Rails 8.1.0〜8.1.3 のどこかのタイミングで一時的にソートされていた期間がある
- しかし 8.1.4 で元に戻った(ソートされなくなった)
- ドキュメントだけが「ソートされる」まま放置されていた
という状態でした。
- 「
CHANGELOG との整合性
Active Record の CHANGELOG(8-1-stable)には「ソート導入」と「そのリバート」の両方が履歴として残っています。
過去のバージョン間の差分を調べる際は、CHANGELOG も合わせて読む必要があります。CI / ツール連携への影響
schema.rbの列順を前提に何か自動チェック・整形をしている場合、
「Rails 本体がソートしてくれる」とは期待できません。- 列順をアルファベット順に固定したい場合は、プロジェクト側で:
structure.sqlの利用に切り替える- カスタムタスク等で
schema.rbを整形する
など独自対応が必要です。
- 参考情報 (あれば)
- この PR:
- #58953: [ci skip] Remove schema.rb column sorting from 8.1 guides
- 関連する過去の変更:
- #53281:
schema.rbのカラムアルファベット順ソートを導入した PR - #56842: そのソート挙動をリバートした PR(Rails 8.1.4 に含まれる)
- #53281:
- ドキュメント:
guides/source/8_1_release_notes.md(8.1 リリースノート)guides/source/upgrading_ruby_on_rails.md(アップグレードガイド)
- 挙動の履歴:
- Active Record CHANGELOG (
8-1-stableブランチ) に導入とリバートの両方が記録されています。
- Active Record CHANGELOG (
#58961 Reuse a frozen Mime::Type when looking up /
マージ日: 2026/10/9 | 作成者: @linyiru
- 概要 (1-2文で)
このPRは、Accept: */*のリクエストごとに毎回新しいMime::Typeオブジェクトが生成されていた非効率な挙動を改善し、"*/*"の lookup 時に単一の凍結済みインスタンスを再利用するようにしてパフォーマンスとメモリアロケーションを削減するものです。外部から見た挙動(request.formatやrespond_toの動作など)は変えずに最適化のみを行っています。
- 変更内容の詳細
背景
- 多くの HTTP クライアント(curl など)は
Accept: */*を送る。 - Rails では
Request#acceptsを通じてMime::Type.lookup("*/*")が呼ばれるが、*/*は MIME タイプとして登録されていないため、毎回以下の処理が走っていた:- レジストリでの lookup → ヒットしない
- パラメータ分解 → それでもヒットしない
- 新しい
Mime::Typeオブジェクトを生成し、MIME_REGEXPでマッチング
- これが「すべての
Accept: */*リクエストで 1オブジェクトずつ余計に生成される」オーバーヘッドになっていた。
なお、ブラウザっぽい Accept ヘッダ(text/html,application/xhtml+xml,... など)は request.formats が特別扱いして accepts に流さないため、この問題の経路には乗らない。
具体的な変更点
Mime::Type.lookup("*/*") に対し、「常に同じ凍結済みインスタンスを返すショートカット」を追加しています。
疑似コードイメージ:
def self.lookup(string)
return ALL_STAR_TYPE if string == "*/*" # 新しく追加された経路(実際の定数名はイメージ)
# それ以外は従来通り:レジストリを見て、なければパースして生成
# ...
endここでのポイント:
- 返す
*/*は「レジストリに登録されていない、凍結済みのMime::Typeインスタンス」。 - 既存の
Mime::Type.new("*/*")などで生成されるインスタンスと比較しても:- クラス
to_sやto_strの結果to_sym(nil)hash値 が同じで、==/eql?も成立する「同じ値オブジェクト」として振る舞う。
- これは
Mime::ALLとは別物:Mime::ALL#html?と#all?はtrueだが、"*/*"用インスタンスはそれらがtrueにならないようにしている。- もし
Mime::ALLを返すようにすると、Accept: */*のリクエストでrequest.format.html?がtrueになってしまうなど、挙動の互換性が壊れるため、それを避けている。
- レジストリはこれまで通り最初にチェックされる:
- もしアプリケーション側で
*/*を MIME タイプとして登録している場合は、そちらが優先される(つまりこのショートカットは発動しない)。 - したがって、アプリケーションが独自に
*/*を登録しているケースとの互換性も保たれる。
- もしアプリケーション側で
テスト追加
actionpack/test/dispatch/mime_type_test.rb にテストが追加されており、主に次の点を確認しています。
lookup("*/*")がレジストリチェックより後にこのショートカットに到達すること(順番を変えるとテストが落ちる)。- ショートカットを削るとテストが失敗すること(最適化が確実に効いているかを保証)。
- 新しい
*/*用インスタンスが、従来の手順で生成されたインスタンスと==/eql?/hashの観点で同一視できること。
- 影響範囲・注意点
影響範囲
- 主に影響を受けるのは、
Accept: */*を送ってくるクライアント(curl や多くの HTTP ライブラリ)向けのリクエスト処理の内部パフォーマンス。 - 外部 API はそのままで、挙動に関する確認も以下のように行われている:
- 16パターンの Accept ヘッダについて、変更前後で:
request.acceptsrequest.formatsrequest.format- 各
Mime::Typeの:- クラス
- 文字列表現
- シンボル
refhtml?,all?Mime::ALLとの == / eql? 比較- 新規に生成した
Mime::Typeとのeql?/hash
respond_toを使ったレスポンス(format.anyを含むケース / JSON のみのケース)
- これらが、オブジェクトID(
object_idに相当)以外は完全に一致することを確認。
- 16パターンの Accept ヘッダについて、変更前後で:
そのため、現実的には「パフォーマンス改善のみで、挙動は変えない」変更とみなせます。
パフォーマンスへの影響
ベンチマーク(YJIT, Ruby 3.4.10):
| メソッド | before | after |
|---|---|---|
Mime::Type.lookup("*/*") | 642 ns, 5 allocations | 39 ns, 0 allocations |
Mime::Type.parse("*/*") | 1.14 µs, 8 allocations | 471 ns, 3 allocations |
実アプリでの 1リクエストあたりの計測(Accept: */*、シンプルな render plain: "Hello")では:
- user-space instructions: 859k → 852k(約 -0.9%)
- オブジェクトアロケーション: 302 → 297
- wall clock time はノイズ範囲内
大規模トラフィック環境や Accept: */* が多い API サーバでは、わずかながら GC 圧縮小や命令数削減の恩恵が出る可能性があります。
注意点 / 互換性
Mime::ALLとは別インスタンスであり、#html?/#all?の結果が違う点はこれまでと同じ(Accept: */*で突然 HTML 扱いになるなどの変化はしない)。- 独自に
*/*を MIME タイプとして登録しているアプリでは、これまで通り「レジストリに登録された*/*が優先される」ため、挙動変化はない。 object_idに依存した比較をしているようなコードがあれば(通常はありえませんが)、「"*/*"のインスタンスが固定される」ことで挙動が変わる可能性は理論的にはあります。ただし、Mime::Typeを ID 比較で扱うのは想定されていないため、非推奨な使い方と言えます。
- 参考情報 (あれば)
関連ファイル:
actionpack/lib/action_dispatch/http/mime_type.rbMime::Type.lookup周辺に、"*/*"を扱うショートカットが追加。
actionpack/test/dispatch/mime_type_test.rb*/*の動作確認、およびレジストリ優先の確認テストが追加。
パフォーマンス計測スクリプト(アプリ起動後に実行):
rubyrequire "benchmark/ips" Benchmark.ips do |x| x.report('lookup("*/*")') { Mime::Type.lookup("*/*") } x.report('lookup("application/json")') { Mime::Type.lookup("application/json") } x.report('parse("*/*")') { Mime::Type.parse("*/*") } x.report('parse("application/json")') { Mime::Type.parse("application/json") } end全体として、「
Accept: */*に対するMime::Type解決の内部実装を、意味論を変えずにキャッシュ(単一インスタンス共有)する最適化」と理解すると把握しやすいです。
#59010 [docs][ci skip] Fix description of default_scope when building records
マージ日: 2026/10/9 | 作成者: @hachi8833
- 概要 (1-2文で)
Railsガイド(Active Record Query Interface)の「default_scope がレコード生成時にどう作用するか」の説明が、実際の挙動とずれていたため修正したドキュメント専用PRです。default_scopeよりも「明示的に渡された属性」が優先されることを、文章とコード例で正しく示すようにしています。
- 変更内容の詳細
対象: guides/source/active_record_querying.md の「Applying a Default Scope」節。
何が問題だったか
元のガイドには次のような説明がありました(趣旨):
default_scopeでout_of_printをfalseにしているとき、out_of_print: trueを付けて新しい Book を作っても、default_scopeが適用される(=falseになる)かのように読める文章だった。
しかし、実際の Rails の挙動は以下のとおりです(PR内で Rails 8.1.4 で確認されている):
Book.new
# => #<Book id: nil, out_of_print: false> # default_scope が効いている
Book.new(out_of_print: true)
# => #<Book id: nil, out_of_print: true> # 明示属性が default_scope を上書きする
Book.unscoped.new
# => #<Book id: nil, out_of_print: nil> # default_scope 自体が無効なので nilつまり、
Book.newの デフォルト値 としてdefault_scopeの値が使われる- しかし
Book.new(out_of_print: true)のように 明示属性を渡した場合はそちらが優先 される
というのが正しい仕様です。
このPRでの修正内容
PRでは次の2点が修正されています:
説明文の修正
- 「
default_scopeによって新しく作られる Book のout_of_printがfalseになる」という趣旨の、より正確な記述に変更。 - すなわち「新しく作成される本は、デフォルトで
out_of_print: falseが入る」ことを説明する。
- 「
コード例の拡張
元々あったBook.newの例に加えて、次の例を追記:rubyBook.new(out_of_print: true)これにより:
Book.new→out_of_print: false(default_scope による初期値)Book.new(out_of_print: true)→out_of_print: true(明示的な属性が優先)
という対比がガイド上でも一目で分かる形になります。
なお、Book.unscoped.new の挙動(out_of_print: nil になる)が PR 説明で示されており、「default_scope 自体を外せばデフォルトのフィールド値は付与されない」点も確認されています。
- 影響範囲・注意点
コード挙動への影響は一切なし
変更はドキュメントのみ(.mdファイル)であり、Rails本体や Active Record の実装は変わっていません。開発者が誤解しやすかったポイントが是正された
- 「default_scope が新規レコードに強制的に値を押し込む」ように誤解していると、
- 「明示的に
out_of_print: true渡したのにfalseに上書きされるはずだ」と勘違いする可能性があった。
- 「明示的に
- 今回の修正により、「明示的な属性が常に優先される」ことをガイドが明示するようになった。
- 「default_scope が新規レコードに強制的に値を押し込む」ように誤解していると、
実務上のヒント
- モデルの「デフォルト値」を
default_scopeで付けたい場合でも、- フォームやAPIから明示的に値が送られてくるケースでは、その値が優先される。
- 「ユーザー入力を絶対に上書きしたい」ような用途には
default_scopeは不向きで、- コールバックやバリデーション、DBのデフォルト値など別の手段を検討すべき。
- モデルの「デフォルト値」を
- 参考情報 (あれば)
該当ガイド:
Active Record Query Interface Guide – Applying a Default Scope
https://guides.rubyonrails.org/active_record_querying.html#applying-a-default-scope関連知識:
Model.default_scope { where(... ) }は- クエリに対するデフォルト条件を付与する仕組みであり、
- 新規生成(
Model.new)時には「初期値」として影響することがあるが、 Model.new(attr: value)のように直接属性を指定した値はこれを上書きする。
#58991 Log CSRF warnings at :warn level
マージ日: 2026/10/9 | 作成者: @azmi2409
- 概要 (1-2文で)
CSRF 関連のログイベント(csrf_token_fallback,csrf_request_blocked,csrf_javascript_blocked)が:infoレベルで出力されるようになっていたため、config.log_level = :warnなどの設定下でログに出なくなっていた問題を修正し、従来どおり:warnレベルで出るように戻した PR です。あわせて、ActiveSupport::EventReporter::LogSubscriberが:warnレベルのイベントを正しく処理できるようにし、関連するテストを追加しています。
- 変更内容の詳細
2-1. CSRF ログレベルの修正 (ActionController::LogSubscriber)
変更ファイル:
actionpack/lib/action_controller/log_subscriber.rb(+3/-3)
これまで:
- CSRF 関連イベント
csrf_token_fallbackcsrf_request_blockedcsrf_javascript_blocked
- が
ActionController::LogSubscriber内でinfoレベルとして扱われていたため、config.log_level = :warn以上の環境(本番など)ではログに一切出ない状態だった。
PR での修正:
- 上記 3 つの CSRF イベントを
:warnレベルでログ出力するように変更。 - 挙動としては「#56355 前の
RequestForgeryProtectionがlogger.warnを直接呼んでいた状態」に戻した形。
イメージとしては、以下のような変更になっています(疑似コード):
# 変更前(イメージ)
def csrf_request_blocked(event)
info do
"Can't verify CSRF token authenticity..."
end
end
# 変更後(イメージ)
def csrf_request_blocked(event)
warn do
"Can't verify CSRF token authenticity..."
end
end2-2. :warn ログレベルのサポート追加 (ActiveSupport::EventReporter::LogSubscriber)
変更ファイル:
activesupport/lib/active_support/event_reporter/log_subscriber.rb(+1/-1)
これまで:
ActiveSupport::EventReporter::LogSubscriber::LOG_LEVELSに:debug,:info,:errorしか含まれていなかった。- そのため、
log_subscriber側でイベントを:warnレベルに設定しても、そのレベル自体が許可されておらず、「宣言しても実質的に何も出ない(イベントが丸ごと無効化される)」状態になっていた。
PR での修正:
LOG_LEVELSに:warnを追加し、ActionController::LogSubscriberから:warnレベルで CSRF イベントを宣言しても問題なく処理・出力できるようにした。
擬似コードイメージ:
# 変更前
LOG_LEVELS = [:debug, :info, :error].freeze
# 変更後
LOG_LEVELS = [:debug, :info, :warn, :error].freeze2-3. テスト追加 (actionpack/test/controller/log_subscriber_test.rb)
変更ファイル:
actionpack/test/controller/log_subscriber_test.rb(+29/-0)
追加されたテスト内容:
- 全ての CSRF イベントについて、
warnレベルでログが出力されることを確認するテスト:csrf_token_fallbackcsrf_request_blockedcsrf_javascript_blocked
config.action_controller.log_warning_on_csrf_failure = false相当の設定(log_warning_on_csrf_failure = false)を有効にした場合、- 上記 CSRF ワーニングがログ出力されない(opt-out が機能している)ことを確認するテスト。
これにより、
- CSRF ログが
:warnレベルで出力されること - アプリ設定でワーニングを抑制できること
が後方互換を含めてテストで保証されるようになっています。
- 影響範囲・注意点
ログレベルの変更によるログの出方
config.log_level = :warnや:errorなどに設定している本番環境で、これまで見えていなかった CSRF 関連の警告ログが再び出力されるようになります。- ログ量が増える可能性があるため、CSRF 対応状況やログローテーション設定などは確認しておくとよいです。
CSRF 警告ログの opt-out 設定
config.action_controller.log_warning_on_csrf_failure = false(もしくは相当の設定)を行っている場合、引き続き CSRF 警告ログは出力されません。- テストでこの挙動もカバーしているため、この設定を使っていたアプリケーションの動作が変わることはありません。
ログ解析・監視への影響
- 監視やアラートがログレベルに基づいている場合、CSRF 関連イベント(特に
csrf_request_blockedやcsrf_javascript_blocked)が新たに:warnとして検知されるようになります。 - これに合わせてダッシュボード・アラートルール(例: “WARN 以上をカウント”)を調整する必要があるかもしれません。
- 監視やアラートがログレベルに基づいている場合、CSRF 関連イベント(特に
Rails バージョン間の挙動差の解消
- #56355 以降のバージョンでだけ「CSRF 失敗時にログが出ないことがある」というレグレッションが発生していましたが、この PR により #56355 以前と同様の挙動に戻ります。
- そのため、「Rails を上げたら CSRF ログが消えた」系の問題を抱えていた場合、この修正を取り込むことで解消が期待できます。
- 参考情報 (あれば)
- 関連 Issue:
- Fixes #58959 — 「CSRF ログが
:infoレベルになったことでlog_level = :warn以上だとログが出ない」問題の報告。
- Fixes #58959 — 「CSRF ログが
- 関連 PR:
- #56355 — CSRF 関連ログを
ActionController::LogSubscriberに移した変更(今回のレグレッションの原因となった変更)。
- #56355 — CSRF 関連ログを
- 該当クラス・設定:
ActionController::RequestForgeryProtectionActionController::LogSubscriberActiveSupport::EventReporter::LogSubscriberconfig.action_controller.log_warning_on_csrf_failureconfig.log_level
#58999 Enable summary output if -q/--quiet used with rails test
マージ日: 2026/10/9 | 作成者: @zenspider
- 概要 (1-2文で)
rails testに-q/--quietを付けたときでも、テストのサマリー出力(失敗数などの概要)が正しく表示されるようにする修正です。Minitest のレポーター構成まわりの挙動を、Rails プラグイン側で 1 行だけ調整しています。
- 変更内容の詳細
背景となる問題
rails test -qを実行した場合:- 「静かに」実行したいオプションですが、現状では:
ProgressReporterがCompositeReporterに追加されない- その副作用として
TestUnitReporterも追加されない - 結果として「失敗の詳細出力が一切出ない」という状態になっていた
- 「静かに」実行したいオプションですが、現状では:
- つまり、「静かに」はいいが「失敗が分からない」は困る、という問題。
作者コメントによると:
If
-qis used, then no ProgressReporter is pushed into the CompositeReporter, causing TestUnitReporter to not be pushed either, and then NO failure output occurs.Since TestUnitReporter doesn't support
-qon its own, I chose not to fix the reject!/push part and fixed SuppressedSummaryReporter instead.
要約すると:
-qの扱いをTestUnitReporter側でサポートするのではなく、- Rails 側が使っている
SuppressedSummaryReporterの挙動を修正して対応した、という方針です。
実際の変更
- 変更ファイル:
railties/lib/minitest/rails_plugin.rb - 差分: 1 行追加 / 1 行削除(実質 1 行の置き換え)
コードの全貌は PR 情報からは見えませんが、文脈からほぼ確実に以下のような形の修正です:
SuppressedSummaryReporterが、-q使用時にも:- 進捗などの冗長な出力は抑制しつつ
- テストのサマリー(テスト数、失敗数、エラー数など)は出力する ように変更された。
Rails の Minitest プラグインは、標準の Minitest レポーター構成に対して:
- 進捗表示 (
ProgressReporter) - テストユニット互換出力 (
TestUnitReporter) - サマリー抑制用のラッパー (
SuppressedSummaryReporter)
などを組み合わせて CompositeReporter を構成しています。-q 指定時のレポーター構成ロジック自体はそのままに、SuppressedSummaryReporter の中身のみを 1 行差し替えで調整することで、「静かだが結果はわかる」状態にしています。
- 影響範囲・注意点
- 対象:
rails test -q/rails test --quietを利用している(利用する予定の)プロジェクト
- 影響:
- これまでは
rails test -qだと失敗時にも何も出ず、CI などで非常に困る状況があったが、 - 修正後は、詳細な進捗や冗長なログは抑えつつも、最終的なサマリーや失敗内容が表示されるようになる。
- これまでは
- 注意点:
- あくまで Minitest レポーターの構成と
SuppressedSummaryReporterの挙動が変わるだけなので、テストそのものの実行ロジックには影響しない。 TestUnitReporter自体が-qを理解するようになったわけではない点に注意(quiet の扱いは Rails 側のラッパーで吸収している)。
- あくまで Minitest レポーターの構成と
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58999
- 関連ファイル:
railties/lib/minitest/rails_plugin.rb - Minitest の Reporter 構成に関するドキュメント:
- https://github.com/minitest/minitest (Reporter API / CompositeReporter まわり)
#58881 Don't modify a connected_to stack shared with another thread
マージ日: 2026/10/9 | 作成者: @SulimanAbdulrazzaq
- 概要 (1-2文で)
ActiveRecord::Base.connected_to系が、スレッド間で共有されうるconnected_toスタック配列を「その場で破壊的に変更」していたため、ActionController::Live等でスレッドを共有した場合にロール/シャードが意図せず混線するバグを修正する PR です。スタック操作を「コピーして差し替える」方式に変更し、親スレッドと子スレッドでスタックが干渉しないようにしています。
- 変更内容の詳細
背景となる問題
ActiveSupport::IsolatedExecutionState.share_withは「実行状態のハッシュ」を複製しますが、その値(オブジェクト)は浅いコピーのままです。ActionController::Liveは、ストリーミング用のスレッドを立ち上げる際にこれを利用し、「リクエストスレッドの実行状態」を子スレッドに共有します。active_record_connected_to_stack(クエリルーティングで使うconnected_toスタック)もその一つで、ハッシュはコピーされるものの、配列インスタンス自体は同一オブジェクトを参照していました。- その結果:
- 親スレッドと子スレッドが同じスタック配列に対して push/pop している状態になっていた。
- 親スレッドがリクエストを返した後に
ShardSelectorミドルウェア等のconnected_toブロックを抜けると、その pop が子スレッド側のスタックにも効いてしまい、以降のストリームが:defaultロール・シャードで動いてしまう。 - 逆向きも同様で、子スレッド側で
connected_toすると、親スレッドのcurrent_role/current_shardも書き換わってしまう。
対応方針
connected_to, connected_to_many, connecting_to の各メソッドで、スタック配列をインプレースで変更しないように変更しています。
変更前のイメージ(概念)
stack = ActiveRecord::Base.connected_to_stack
stack.push(new_frame) # 同じ配列オブジェクトを mutate していた
yield
stack.popこれだと stack がスレッド間で共有されている場合に、push/pop が他スレッドにも影響する。
変更後のイメージ(概念)
stack = ActiveRecord::Base.connected_to_stack
# push 時にコピーしてから追加
new_stack = stack.dup
new_stack.push(new_frame)
ActiveRecord::Base.connected_to_stack = new_stack
begin
yield
ensure
# pop も元の new_stack を元にコピーしてから最後のフレームを落とす
popped_stack = ActiveRecord::Base.connected_to_stack.dup
popped_stack.pop
ActiveRecord::Base.connected_to_stack = popped_stack
endポイント:
- 「現在の実行状態」(IsolatedExecutionState) に保存しているスタック参照自体を新しい配列に差し替えることで、
- 親スレッドが保持している配列はそのまま
- 子スレッドが保持している配列もそのまま
connected_toに入った「そのスレッド側」の実行状態だけが、新しい配列参照に切り替わる
prohibit_shard_swappingはもともとprev_value.dup.add(...)とコピーして扱っていたので、それと同じ設計思想に揃えた形です。
互換性のための配慮
The stack is still a mutable array, so code that pushes or pops it directly (for example the
connected_to_stack.popcalls inbase_test.rb) keeps working.
connected_to_stack 自体は依然として Array で、mutable です。
そのため、「スタックを直接いじるコード」があっても型が変わったりはせず動作します。
ただし、今後のライブラリコード側からは、基本的に「実行状態を書き換える」公共 API (connected_to など) 経由での変更が想定されており、直接スタックをいじるのは内部テストレベルの用途に留めるのが安全です。
テスト内容
新規・更新されたテストで確認している振る舞い:
BasicsTest (共有状態を持つ子スレッドの視点)
- 親スレッドが
connected_to/connected_to_manyブロックを抜けた後でも、share_withされた子スレッド側からは元通り両方の role を見られることを確認。- 変更前は
[false, false](ロールが見えない状態)になっていた。
- 変更前は
- 親スレッドが
BasicsTest (子スレッドからの role 変更の影響)
share_withした子スレッドでconnected_to(role: :reading)ブロックに入っても、親スレッドのcurrent_roleは変化しないことを確認。- 変更前は親スレッドの
current_roleが:readingに化けていた。
- 変更前は親スレッドの
ShardSelectorTest (ミドルウェア + ストリーミング的なシナリオ)
- ミドルウェア (
ActiveRecord::Middleware::ShardSelector) 内でスレッドを起動し、ミドルウェアのレスポンス処理が終了した後もスレッドを走らせ続ける(ActionController::Liveと同様の振る舞い)。 - そのスレッド内では、引き続き
shard_oneを見られることを確認。- 変更前は
current_shardが:defaultに変わってしまっていた。
- 変更前は
- ミドルウェア (
CI 上では Ruby 3.4 / SQLite3 テストでレグレッションが出ていないことが明示されています。
- 影響範囲・注意点
影響がある機能・ユースケース
ActiveRecord::Base.connected_to,connected_to_many,connecting_toを使った- マルチ DB (
writing/readingロール切り替え) - マルチシャード (
shard_one/shard_twoなど)
- マルチ DB (
- 特に
ActionController::Liveなど、リクエストスレッドとは別のスレッドで処理を継続する仕組みActiveSupport::IsolatedExecutionState.share_withを自前で使っているコード にとっては挙動の改善(バグ修正)がそのまま影響します。
これまで:
- 同一リクエスト由来のスレッド間で
connected_toによるロール・シャードが「暗黙に共有」されてしまっていたため、- あるスレッドでの
connected_toブロック終了が、別スレッドでのクエリルーティングにまで影響していた。
- あるスレッドでの
これから:
- スレッド単位で独立したスタック参照を持つため、各スレッドの
connected_toライフサイクルがきちんと分離され、- 「どのスレッドでどのシャード/ロールを使っているか」が、直感どおりスレッドローカルに管理されます。
既存コードへの注意点 / 互換性
config.action_controller.live_streaming_excluded_keys = [:active_record_connected_to_stack]は従来通り動作すると明示されています。- ライブストリーミング時に「実行状態からどのキーを子スレッドへ共有しないか」の設定で、今回の修正によりこの設定の意味が変わることはありません。
- 実行状態 (
ActiveSupport::IsolatedExecutionState) に格納されている他の値は、引き続き浅いコピーのままです。active_record_connected_to_stack以外のキーについては、「同じオブジェクトを別スレッドで共有して mutate すると同様の問題が起こり得る」状態は変わっていません。- PR の対象はあくまで「クエリルーティングに使うスタック」のみです。
connected_to_stackを直接操作しているライブラリ/アプリケーションコードがあれば、今後の Rails バージョンアップでの互換性リスクになります。- 現時点では動作しますが、Public API ではないため、将来的には非推奨になる可能性があります。
- 可能なら
connected_to,connected_to_many,connecting_toのみを利用する設計に寄せるのが安全です。
- 参考情報 (あれば)
- 関連 issue: #58870
- 本 PR で修正された「スレッド間で共有される
connected_toスタックが壊れる」問題の報告。
- 本 PR で修正された「スレッド間で共有される
- 関連設定:
config.action_controller.live_streaming_excluded_keys- ライブストリーミング時に
IsolatedExecutionStateから子スレッドへ渡さないキーを指定する設定。 - 今回の修正により
:active_record_connected_to_stackを除外した場合でも期待通り動作することが保証されている。
- ライブストリーミング時に
- 実装方針としては、
prohibit_shard_swappingがもともと「コピーしてから変更」していた設計と整合しており、ActiveRecord 内で「実行状態にぶら下げた共有構造をどう扱うか」の指針をより明確にした変更とも言えます。
#59002 [doc][ci skip] Remove broken link to nonexistent Transactions section
マージ日: 2026/10/9 | 作成者: @hachi8833
概要 (1-2文で)
Active Record のガイド内で、存在しない「Transactions」セクションへのリンクが削除されたドキュメント修正PRです。壊れたリンクを取り除くだけの変更で、挙動やAPIには影響しません。変更内容の詳細
- 対象ファイル:
guides/source/active_record_querying.md - 変更内容:
- 「関連ガイド」リストの中から、
active_record_basics.html#transactionsへのリンクが削除されています。 - Active Record Basics ガイドには
#transactionsセクションが存在しないため、そのアンカー付きリンクが 404 になる状態でした。 - 元々 #56341 でトランザクション/ロッキングのガイドが将来的に追加されることを見越して残されていたものを、現状に合わせて一旦削除した形です。
- 「関連ガイド」リストの中から、
疑似的には、以下のような行が消えたイメージです(実際のテキストは若干異なる可能性があります):
* [Transactions](active_record_basics.html#transactions)- 影響範囲・注意点
- 影響範囲:
- Rails ガイド (特に Active Record Querying ガイド) から辿れるリンクが1つ減るのみで、アプリケーションコードやフレームワークの振る舞いには一切影響しません。
- 外部サイトや社内Wikiなどで、この壊れたURL (
active_record_basics.html#transactions) に直接リンクしている場合、それはもともと 404 だったため、今回の変更で悪化することも改善することもありません(公式ガイド内からの導線が消えるだけ)。
- 注意点:
- 今後「Transactions & Locking」などの新規ガイドが追加されたタイミングで、改めて正しいURLを指すリンクを追加する前提になっています。
- ドキュメントを翻訳・ミラーしているプロジェクトでは、同様に該当リンクの削除を取り込んでおくと、ユーザー体験の向上につながります。
- 参考情報 (あれば)
- 元になったPR・議論:
- #56341: Active Record 関連のガイド整備の中で、トランザクション/ロッキングのガイド案に紐づくリンクが追加されていた。
- #59002(本PR): その未実装セクションへのリンクを一旦削除して整合性を取ったもの。
- 今後、トランザクションについて学ぶ際の代替情報:
- 現在の Rails Guides では「Active Record Transactions」専用ガイド、または Locking に関する独立したセクションが存在するかどうかを、該当バージョンの Guides から別途確認する必要があります。
- 公式APIドキュメント (
ActiveRecord::Base.transactionなど) も、トランザクションの挙動確認には有用です。
#59005 [docs][ci skip] Fix inconsistent enum item
マージ日: 2026/10/9 | 作成者: @hachi8833
- 概要 (1-2文で)
このPRは、Railsガイド(ドキュメント)内の enum の項目名が一箇所だけ他の箇所と不整合になっていた箇所を修正するものです。アプリケーションコードや挙動には影響せず、ドキュメント上の表記ゆれを揃えています。
- 変更内容の詳細(あればサンプルコードも含めて)
対象は guides/source/active_record_querying.md(日本語ガイドではなく英語ガイド)で、enum のサンプルに出てくる状態名が 1 箇所だけ誤っていました。
- 修正前:
being_packaged - 修正後:
being_packed
おそらくガイド内では、他の箇所で以下のような enum 定義を例示しており:
class Order < ApplicationRecord
enum status: {
being_packed: 0,
shipped: 1,
delivered: 2,
}
end本文中でこの being_packed を参照すべきところが、誤って being_packaged と記載されていたため、それを揃えるという修正です。
[ci skip] がタイトルに付いていることからもわかるように、テストを走らせる必要のない単純なドキュメント修正として扱われています。
- 影響範囲・注意点
影響範囲
- Rails本体のコード、挙動、API には一切変更ありません。
- 変更は
Active Record Queryingガイド(ドキュメント)の 1 行のみです。 - enum の項目名の公式な「推奨形」が
being_packedであることが、より一貫した形で示されるようになります。
注意点
- 既存アプリケーションで
being_packagedを使っていても、このPRによって壊れることはありません(あくまでドキュメント修正なので)。 - ただし、ドキュメントをコピペして実装する場合は、今後は
being_packedを使うことになるため、ドキュメントとコードの不整合を避ける意味でも、アプリ側の命名を合わせたい場合はリファクタリングを検討してもよいでしょう。 - enum のキー名を変える場合は、DB の永続化済みデータや既存コードとの互換性に注意が必要です(このPR自体はそこに手を触れていません)。
- 既存アプリケーションで
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/59005
- 修正対象ガイド:
guides/source/active_record_querying.md - CI スキップの意味: タイトルの
[ci skip]は、GitHub Actions などの CI を走らせないことを示す慣習的な指定で、ドキュメントのみの軽微な変更によく使われます。
#59008 [docs][ci skip] Fix outdated reference to association order in reorder docs
マージ日: 2026/10/9 | 作成者: @hachi8833
- 概要 (1-2文で)
Active Record Query Interface ガイドのreorderセクションで、サンプルコードと説明文の内容が食い違っていた箇所を修正したドキュメント専用のPRです。reorderが「何の ORDER BY を上書きするのか」という説明を、実際のdefault_scopeベースのサンプルに合うように書き換えています。
- 変更内容の詳細
背景となる問題
- 以前のガイドでは、
reorderの説明に「association order(関連の order)」を例にしたコードがありました:rubyclass Author < ApplicationRecord has_many :books, -> { order(year_published: :desc) } end - 後の修正でこの例は
default_scopeを使う形に変更されたにもかかわらず、文章中には依然としてnot just association order という文言が残っており、コード例と説明が一致していない状態になっていました。
今回の修正内容
「association order」から「default scope」への表現の修正
文中の説明を以下のように変更しています(英語ドキュメントですが、内容を日本語で説明します):
- 旧:
- 「
reorderは、単なる関連での order だけでなく〜」
- 「
- 新:
- 「
reorderは、単なる default scope 由来の order だけでなく〜」
- 「
これにより、「前の例で使っているのは関連の
orderではなくdefault_scopeだ」という現在のコード例と説明が整合しました。- 旧:
default_scope+order+reorderの挙動説明の修正ガイド内の前提として、
Bookモデルには以下のようなdefault_scopeが定義されています:rubyclass Book < ApplicationRecord default_scope { order(year_published: :desc) } endこの状態で
rubyBook.where("id > 100").order("id desc")を実行すると、生成される SQL の ORDER BY は
sqlORDER BY year_published DESC, id descのように、
default_scopeのorder(year_published: :desc)- チェーンされた
order("id desc")
の2つが結合されたものになります。
ここでさらに
rubyBook.where("id > 100").order("id desc").reorder("title ASC")を呼ぶと、
reorder("title ASC")が 両方の ORDER BY をまとめて上書きする というのが正しい挙動です。今回のPRでは、この点を明示するよう文章を修正しています。
具体的には、それまでの説明が「reorderが直前のorder("id desc")だけを上書きする」という誤解を招きうる表現だったのを、「default_scope由来の order も含めて、既存の ORDER BY 全体を置き換える」という趣旨が伝わるように変更しています。
- 影響範囲・注意点
影響範囲:
- コード変更はなく、ガイド(
guides/source/active_record_querying.md)のみの修正です。 - Rails の挙動自体は以前から同じで、PRはそれに合わせてドキュメントを正しただけです。
- コード変更はなく、ガイド(
実務上の注意点:
default_scopeでorderを定義しているモデルに対してreorderを使うと、default_scopeの ORDER BY- チェーンした
orderで追加した ORDER BY の どちらもまとめてリセットされる 点を再確認しておく必要があります。
- もし「
default_scopeの並び順は維持したまま、後から足したorderだけ変えたい」といった挙動を期待してreorderを使っている場合、その認識は誤りです。
そのようなケースでは、reorderではなく、unscope(:order)で一部だけを外したり、default_scope自体の設計を見直すことを検討すべきです。
- 参考情報 (あれば)
- 該当ガイドのセクション:
Active Record Query Interface Guide –reorder - 関連知識:
default_scope { order(...) }は、そのモデルに対するすべてのクエリに ORDER BY を追加する。orderは既存の ORDER BY に「追加」される。reorderは既存の ORDER BY(default_scope由来を含む)を「全て置き換える」。
#59004 [docs][ci skip] Fix env var check in Active Job halting callbacks example
マージ日: 2026/10/9 | 作成者: @hachi8833
- 概要 (1-2文で)
Active Job ガイドの「Halting Callbacks」例で、環境変数の判定ロジックが常にジョブを中断してしまう不具合があったため、意図どおり「環境変数が"true"のときだけ中断」になるようにドキュメントが修正されています。コード本体ではなくドキュメントのみの変更です。
- 変更内容の詳細
問題になっていた元のサンプルコードは以下の行です。
throw :abort if ENV.fetch("DISABLE_GUESTS_CLEANUP_JOB", true)このコードの挙動:
- 環境変数
DISABLE_GUESTS_CLEANUP_JOBが設定されていない場合ENV.fetch("DISABLE_GUESTS_CLEANUP_JOB", true)はデフォルト値trueを返すため、throw :abortが常に実行され、ジョブは必ず中断される。 - 環境変数が設定されている場合
ENV[...]は常に「非空文字列」を返す("false"や"0"を含む)。
Ruby では空文字列も含め、文字列はすべて truthy なので、やはりthrow :abortが実行され、
結果として「常にジョブが中断される」状態になる。
例の意図(説明文)としては、「環境変数でジョブの実行可否を切り替える」想定なのに、実際のロジックだとどんな設定でもキュー投入されない、という矛盾がありました。
この PR では判定を次のように修正しています。
throw :abort if ENV["DISABLE_GUESTS_CLEANUP_JOB"] == "true"この挙動:
- 環境変数が未設定:
ENV["DISABLE_GUESTS_CLEANUP_JOB"]はnilとなり、nil == "true"はfalse。
→throw :abortは実行されず、ジョブは通常どおり enqueue される。 - 環境変数が
"true"に設定されている場合:ENV["DISABLE_GUESTS_CLEANUP_JOB"] == "true"はtrue。
→throw :abortが実行され、ジョブが中断される。 "false"や"0"など他の値:
いずれも"true"との比較はfalseになるためジョブは実行される。
つまり、「デフォルトではジョブが実行されるが、DISABLE_GUESTS_CLEANUP_JOB=true をセットするとそのジョブだけ実行しなくなる」という、ガイドの説明意図と整合する動きになりました。
なお、PR 説明にもある通り、この例は「環境変数を真偽値として厳密にパースする」ことが主題ではなく、「throw :abort によるコールバックの中断」が主題なので、あえて単純な "true" 比較に留めています。
- 影響範囲・注意点
- 対象は ドキュメント (guides) のみであり、Rails 本体のコードには変更はありません。
- これまでガイドの例をそのままコピーして使っていた場合:
- 旧コードのままだと、環境変数の有無や値に関わらずジョブが常に中断されてしまいます。
- 実運用で使っている場合は、
ENV["VAR_NAME"] == "true"のような比較に修正することを推奨します。
- 環境変数を boolean 的に扱うときの Ruby の落とし穴:
"false"や"0"も truthy であることを前提に、きちんと文字列比較やパーサを用いる必要があります。- 本番設定で「false を入れたのに有効になっている」というバグを防ぐ観点でも、この例はよい注意喚起になります。
- 参考情報 (あれば)
該当ガイド:
Active Job Basics – Halting Callbacks
https://edgeguides.rubyonrails.org/active_job_basics.html#halting-callbacks環境変数の真偽値パースでよくあるパターン例(自前で使う場合の一案):
rubydef env_flag(name, default: false) val = ENV[name] return default if val.nil? %w[1 true yes on].include?(val.downcase) end throw :abort if env_flag("DISABLE_GUESTS_CLEANUP_JOB")
#59003 [docs][ci skip] Fix typo in Active Job guide
マージ日: 2026/10/9 | 作成者: @hachi8833
概要 (1-2文で)
このPRは、Active Job ガイド (active_job_basics.md) 内の単純な英単語のタイポを修正したドキュメント専用の変更です。機能コードには一切手が入っておらず、挙動の変更や互換性への影響はありません。変更内容の詳細
- 変更対象ファイル:
guides/source/active_job_basics.md - 変更内容: 英文中の誤記
"users"を"uses"に修正
文脈としては、Active Job の使い方・利用例を説明するガイドの中で、
「〜を users する」ではなく「〜を uses する」が正しい文法になる箇所を修正したものです。
(擬似的な例)
修正前(イメージ):
Active Job users the configured queue adapter to enqueue jobs.修正後(イメージ):
Active Job uses the configured queue adapter to enqueue jobs.実際には、上記はイメージですが、同様に "users" → "uses" という単語レベルの修正のみが行われています。
- 影響範囲・注意点
影響範囲
- Railsガイド(ドキュメント)のみ。
- フレームワークのコード、API、挙動、パフォーマンスには一切影響しません。
- テストや本番環境の挙動も変化しません。
注意点
- ドキュメントの英語表現が自然になり、読解性が向上しただけの変更です。
- コードベースの更新や設定変更など、開発者が追随する必要はありません。
- 参考情報 (あれば)
- PRタイトルに
[ci skip]が付いており、CI を走らせないドキュメント専用修正であることが明示されています。 - 変更は 1 ファイル・1行の置換のみで、他機能との関連や副作用はありません。
#58767 Store query constraints in SchemaContext
マージ日: 2026/10/8 | 作成者: @adrianna-chang-shopify
- 概要 (1-2文で)
このPRでは、モデルに紐づく「クエリ制約(query constraints)」をActiveRecord::ModelSchema::SchemaContextに保存・管理するように変更し、スキーマ読み込み時に制約を解決(resolve)する仕組みに移行しています。これにより、クエリ制約の評価タイミングがスキーマロード時に統一され、モデルレベルのアクセスはスキーマコンテキスト経由の委譲に整理されます。
- 変更内容の詳細
背景
- 前提となるPR: https://github.com/rails/rails/pull/58437
- Active Record には「モデルレベルのクエリ制約」を定義する機構があり(例: マルチテナンシー用に必ず
tenant_idを入れる等)、従来はモデル側で直接管理していました。 - このPRでは、その「最終的に解決されたクエリ制約のリスト」をスキーマロード時に計算し、
SchemaContextに保持するように整理しています。
主要な設計変更
1) クエリ制約を SchemaContext に移動
変更ファイル:
activerecord/lib/active_record/model_schema/schema_context.rb(+24/-2)
ポイント:
- 各モデルのスキーマ情報をラップする
SchemaContextに、「解決済みクエリ制約」を格納するフィールド・APIが追加されています。 - スキーマがロードされたタイミングで、そのモデルが持つクエリ制約(おそらく
where条件に相当するもの)を「最終的な形」に解決してリストとして保持します。
イメージ(あくまで概念的なサンプル):
class Post < ApplicationRecord
# まだスキーマが読み込まれていない段階
query_constraints :tenant_id
# スキーマ読み込み時:
# - tenant_id カラムの存在などを考慮して
# - 実際に使われるクエリ制約リストを SchemaContext に保存
end
# 以後は SchemaContext 経由でクエリ制約を参照する
Post.schema_context.query_constraints # => 解決済みの制約一覧2) モデルから SchemaContext への委譲
変更ファイル:
activerecord/lib/active_record/model_schema.rb(+4/-4)activerecord/lib/active_record/persistence.rb(+9/-12)activerecord/lib/active_record/attribute_methods/primary_key.rb(+5/-0)
ポイント:
- これまでモデルインスタンス / クラスが直接参照していたクエリ制約関連の情報を、
schema_contextに委譲する形に変更しています。 - 例えば、保存や検索の処理 (
persistence.rb) の中でクエリ制約を利用する箇所は、self.schema_context経由で取得するようにリファクタされています。 - primary key 周りのロジック(
primary_key.rb)にもわずかに変更が入っており、主キーとクエリ制約の扱い・判定がSchemaContextの情報を前提に動くよう調整されています。
この構造により:
- モデルの論理(どういう制約を持つかの宣言)と
- スキーマに基づいた「実際に使う制約リスト」
が分離され、後者は SchemaContext に集約されます。
3) モデル側の「定義」は残しつつ、評価タイミングを分離
PR本文より:
Keep the explicit query constraint definition on the model because it exists before schema load.
モデルクラスに書く「クエリ制約の定義(DSL)」は従来どおりモデル側に残します。
例:rubyclass Account < ApplicationRecord query_constraints :tenant_id, :region endただし、この定義は「スキーマロード前」から存在できる一方、実際にどのような SQL 条件として利用されるか(カラム検証・型情報等を含む「解決済みリスト」)は、スキーマロード後に
SchemaContext上で確定させる、という役割分担になります。
4) テストの追加・更新
変更ファイル:
activerecord/test/cases/associations_test.rb(+20/-16)activerecord/test/cases/persistence_test.rb(+92/-0)activerecord/test/cases/primary_keys_test.rb(+28/-0)
ポイント:
- associations 周りで、関連を張るときにクエリ制約がどのように作用するかを
SchemaContextベースで検証するようにテストを修正。 persistence_test.rbでは、保存・更新・削除といった一連のライフサイクルで、クエリ制約が正しく適用されるかを新たに詳細にテストしています。primary_keys_test.rbでは、主キーを持つ/持たないモデル、複合主キー的なケースなどでクエリ制約と primary key の関係性が崩れていないかを確認するテストが追加されています。
- 影響範囲・注意点
影響範囲
- Active Record の内部 API:
- クエリ制約の評価・格納場所がモデル本体から
SchemaContextに移ったため、内部的にクエリ制約に依存するコード(特に persistence と associations 周り) の挙動がSchemaContextを前提に変わっています。
- クエリ制約の評価・格納場所がモデル本体から
- 拡張・メタプログラミングをしているコード:
- Rails の内部クエリ制約機構に直接アクセスしている gem やアプリ内コードがある場合、
- 以前は
SomeModel.<constraint_xxx>的にアクセスしていたところが SomeModel.schema_context.<constraint_xxx>的な経由が必要になる可能性があります(実際のメソッド名はソースを確認する必要がありますが、委譲経由かどうかで挙動が変わる場所があり得ます)。
- 以前は
- Rails の内部クエリ制約機構に直接アクセスしている gem やアプリ内コードがある場合、
注意点
- 「スキーマロード前/後」の差異に注意:
- モデルに定義されているクエリ制約 DSL はスキーマロード前から利用できますが、「解決済みのクエリ制約リスト」はスキーマロード後でないと正しく構築されません。
- クラス定義のタイミングなど、スキーマロード前に内部状態を覗き込むようなコードがあると、以前と挙動が変わる可能性があります。
- 将来の最適化・キャッシュ基盤としての布石:
SchemaContextに集約されたことで、今後、スキーマベースのキャッシュや再読み込み戦略の洗練が行われる可能性が高く、
「クエリ制約をモデルではなく SchemaContext にぶら下げる」という設計は、そのための前段階でもあります。- 内部構造への依存は今後も変化し得るため、アプリ/gem からは極力公開 API 経由でアクセスするのが安全です。
- 参考情報 (あれば)
- ベースとなる PR:
- https://github.com/rails/rails/pull/58437
- こちらで導入された or 整理された「クエリ制約」機能の後続として、本PRが位置づけられています。
- https://github.com/rails/rails/pull/58437
- 関連ソース (Rails リポジトリ内):
activerecord/lib/active_record/model_schema/schema_context.rb- スキーマコンテキストの中心的なクラス。スキーマごとにクエリ制約を含むメタデータを抱えるようになった部分を見ると、今回の変更の意図が把握しやすいです。
activerecord/lib/active_record/persistence.rb- 保存・更新時にクエリ制約がどう適用されるかの実装。
schema_context経由で何を触っているかを見ると、利用側 API がわかります。
- 保存・更新時にクエリ制約がどう適用されるかの実装。
#58992 Require rails/structured_event_subscriber on boot
マージ日: 2026/10/8 | 作成者: @azmi2409
- 概要 (1-2文で)
Rails::StructuredEventSubscriberがフレームワーク起動時に自動でrequireされておらず、config.active_support.deprecation = :notifyを設定してもrails.deprecationの構造化イベントが発火しない問題を修正した PR です。今後はrequire "rails"しただけでこのサブスクライバが読み込まれ、テストもその前提で動作するようになりました。
- 変更内容の詳細
問題の背景
Rails には
Rails::StructuredEventSubscriberという、構造化イベント(Rails.event)に対して購読・通知を行う仕組みがあります。非推奨警告(deprecation)を通知先に送る設定として、以下のような設定が可能です:
ruby# application.rb など config.active_support.deprecation = :notifyしかし、今回指摘された問題では:
Rails::StructuredEventSubscriberをフレームワーク本体が一度もrequireしていなかった。- そのため、
config.active_support.deprecation = :notifyにしても、Rails.eventにrails.deprecationという構造化イベントが「そもそも発行されていなかった」。
- 既存テストはテストファイル内で直接
require "rails/structured_event_subscriber"していたため、Rails 本体側が require しなくてもテストが通ってしまっていた(回帰バグに気付きにくい状態)。
具体的な変更点
Rails::StructuredEventSubscriberを Rails 起動時に require するように変更変更ファイル:
railties/lib/rails.rb(+1行)典型的には、以下のような行が追加されたと考えられます(PR説明からの推定例):
ruby# railties/lib/rails.rb require "rails/structured_event_subscriber"これにより、アプリケーションが
require "rails"を行うだけでRails::StructuredEventSubscriberが必ずロードされ、config.active_support.deprecation = :notifyのときにrails.deprecationイベントがRails.eventへ発行されるようになります。
テストの修正
- 変更ファイル:
railties/test/structured_event_subscriber_test.rb(-1行) - テストコード内から直接の
require "rails/structured_event_subscriber"を削除。 - 代わりに、「
require "rails"した結果としてRails::StructuredEventSubscriberが利用可能になっているか」を前提とするテストにすることで、- 今後もし
rails.rbからの require を削除・変更してしまうとテストが落ちる - つまり「Rails 本体が subscriber をちゃんと読み込んでいる」ことを明示的に保証するテストになった、ということです。
- 今後もし
- 変更ファイル:
CHANGELOG の更新
- 変更ファイル:
railties/CHANGELOG.md(+4行) - このバグ修正についてのエントリが追加されています。これにより、Rails のリリースノートで今回の挙動変更を確認できるようになりました。
- 変更ファイル:
- 影響範囲・注意点
影響範囲
対象となるのは、以下を行っているアプリケーションです:
- Rails 起動時に
require "rails"をしている(通常の Rails アプリは該当) - かつ
config.active_support.deprecation = :notifyを設定している - もしくは、
Rails::StructuredEventSubscriber/Rails.eventを使って deprecation などのイベントを観測する仕組みを組んでいる
- Rails 起動時に
今回の修正により:
- これまで 出ていると思っていたが、実は出ていなかった
rails.deprecation構造化イベントが、実際に発火するようになります。 - すでにログ基盤・監視基盤で
Rails.eventを購読している場合は、Rails 更新後に deprecation イベントが急に流れ始める可能性があります。
- これまで 出ていると思っていたが、実は出ていなかった
注意点
ログ・メトリクスの増加:
- 非推奨機能を多く使っているコードベースの場合、新たに
rails.deprecationイベントが大量に流れ始める可能性があります。 - ログの肥大化やメトリクスカウントの急増が懸念される場合、
- ログフィルタ
- サンプリング
- deprecation の抑制・無効化(例:
config.active_support.deprecation = :logや:silence)
などの対応を検討した方がよいです。
- 非推奨機能を多く使っているコードベースの場合、新たに
テストでの期待値変更はなし:
- もともと「構造化 deprecation イベントが飛ぶはず」という仕様に対して、実装側が追いついていなかった形なので、
- アプリ側で「イベントが来ない前提」に依存したコードがあるケースは通常考えにくく、後方互換性の破壊は非常に低リスクです。
- ただし「なぜかイベントが来なかったから独自のワークアラウンドを入れていた」ような特殊ケースでは挙動が変わる可能性があります。
- もともと「構造化 deprecation イベントが飛ぶはず」という仕様に対して、実装側が追いついていなかった形なので、
- 参考情報 (あれば)
- 関連 Issue:
- #58960 —
Rails::StructuredEventSubscriberが require されていないためにrails.deprecationイベントが発火しない問題の報告。
- #58960 —
- この PR によって、以下が事実になります:
config.active_support.deprecation = :notify- +
Rails.event/Rails::StructuredEventSubscriber
を組み合わせた deprecation の構造化イベント監視が、Rails を標準的に起動しただけで機能するようになります。
#58860 Forward other options from has_one_attached and has_many_attached
マージ日: 2026/10/8 | 作成者: @jsaubry
- 概要 (1-2文で)
has_one_attached/has_many_attachedが、dependent,service,strict_loading,analyze以外の関連オプションも受け取り、内部で生成されるActiveStorage::Attachment側のhas_one/has_manyにそのまま委譲できるようになりました。これにより、validate:,after_add:,deprecated:など通常の関連オプションを Active Storage の添付ファイル関連にも自然に指定できます。
- 変更内容の詳細
何が問題だったか
従来:
class User < ApplicationRecord
has_one_attached :avatar
endhas_one_attached は内部でだいたい以下のような関連を定義しています:
has_one :avatar_attachment, class_name: "ActiveStorage::Attachment", as: :record, dependent: :destroyhas_one :avatar_blob, through: :avatar_attachment, source: :blob
このとき has_one_attached に渡せるオプションはかなり限定されており:
dependentservicestrict_loadinganalyze
のみがサポートされていました。そのため、次のような一般的な関連オプションを付けたい場合に困っていました:
validate:deprecated:after_add:,before_add:,after_remove:等のコールバック- アプリや gem が拡張する独自の関連オプション
やろうとすると、自分で関連を再定義するしかなく、Active Storage 内部実装 (class_name, as:, inverse_of:, dependent: :destroy など) をコピーして書く必要があり、保守性が悪い状態でした。
何ができるようになったか
今回の変更で、has_one_attached / has_many_attached は以下のように「余剰キーワード引数」を受け取り、それらを 添付レコード用の関連 (..._attachment) にだけ 渡すようになりました。
イメージ:
has_one_attached :avatar, strict_loading: true, validate: false, after_add: :log_added
# ↑ここ以降のオプションが `has_one :avatar_attachment, ...` に転送される実装的には:
- これまでのキーワード引数 (
dependent,service,strict_loading,analyze) に加え、**optionsを受けるように変更 - その
**optionsは blob へのthrough関連には渡さず、..._attachmentだけに渡す optionsは最後に渡すので、Active Storage 側がデフォルトで設定するオプションを上書き可能- 不明なオプションは、
has_one/has_many側のオプション検証でエラーになる(= 従来の Rails の関連と同じ挙動)
これは delegated_type が余剰オプションを belongs_to に転送する実装と同じパターンで、既存の Rails 内の一貫した設計に合わせています。
サンプルコード
バリデーションを制御する
class User < ApplicationRecord
# 添付関連に対する validation を無効化したいケース
has_one_attached :avatar, validate: false
endコールバックを使う
class User < ApplicationRecord
has_many_attached :photos,
after_add: :after_photo_added,
after_remove: :after_photo_removed
private
def after_photo_added(attachment)
Rails.logger.info "Photo added: #{attachment.id}"
end
def after_photo_removed(attachment)
Rails.logger.info "Photo removed: #{attachment.id}"
end
endこの after_add / after_remove は
has_many :photos_attachments, class_name: "ActiveStorage::Attachment", ..., after_add: ..., after_remove: ...に渡されます。
deprecated: など独自オプション
Rails が提供する deprecated: オプションや、gem が追加する関連オプションも素直に使えるようになります:
class User < ApplicationRecord
has_one_attached :legacy_avatar, deprecated: "Use avatar instead"
end- 影響範囲・注意点
互換性
- 既存の
has_one_attached/has_many_attached呼び出しはそのまま動作します。 - 変更は「新しいオプションを受け付ける」方向のみで、従来の挙動を壊さないように実装されています。
- 既存の
オプションが適用される対象
- 渡した追加オプションは Attachment 関連 (
avatar_attachment,photos_attachmentsなど) にのみ適用されます。 - Blob との
through関連 (avatar_blob,photos_blobsなど) には渡されません。 - そのため、Blob 側に対する
dependentやstrict_loadingを直接いじる用途には使えません (そもそも Active Storage が Blob 削除フローを制御しているため、基本的にはいじるべきではない部分です)。
- 渡した追加オプションは Attachment 関連 (
不正なオプション
- 存在しない / サポートされていないオプションを渡すと、
has_one/has_many相当のレベルで ArgumentError などが発生します。 - つまり、「Active Storage 独自のチェック」ではなく「通常の関連オプションのチェック」によって弾かれます。
- 存在しない / サポートされていないオプションを渡すと、
拡張や metaprogramming との相性
- gem が
ActiveRecord::Associations::Builder::Association.valid_optionsにオプションを追加しているような場合、そのオプションもそのまま..._attachment関連に指定可能になります。 - これにより、「Active Storage の has_*_attached だけオプションが通らない」といった不整合が解消されます。
- gem が
- 参考情報 (あれば)
- 当該 PR: https://github.com/rails/rails/pull/58860
- 類似実装:
delegated_typeが余剰オプションをbelongs_toに転送する実装
https://github.com/rails/rails/blob/2cc9c08699bc8062c0851dab772cbea64affabda/activerecord/lib/active_record/delegated_type.rb#L231-L232 - CHANGELOG 追記:
activestorage/CHANGELOG.mdに本変更が機能追加として明記されています。
#58988 [doc][ci skip] Fix internal link for Active Support Instrumentation guide
マージ日: 2026/10/8 | 作成者: @hachi8833
- 概要 (1-2文で)
Active Support Instrumentationガイド内の内部リンク表記の誤りを修正した、ドキュメント専用のごく小さな変更です。コードや挙動には一切影響せず、ガイド内からContinuationクラスへのリンクが正しく表示されるようになります。
- 変更内容の詳細
変更ファイル:
guides/source/active_support_instrumentation.md(1行のみ変更)
実際の変更内容は、リンクテキストの表記揺れ/不整合を修正したもので、以下のようなイメージです。
- Simple fix: [Continuation][]
+ Simple fix: [`Continuation`][]Railsガイドでは、クラス名やメソッド名などのコード要素をリンクテキストとして使うときに、次のような慣習があります。
- コード要素: バッククォートで囲む (例:
`Continuation`) - リンクとして使う:
[`Continuation`][]のようにMarkdownリンク構文で記述し、末尾の[]で参照スタイルリンクとする
今回の修正は、このガイド中にある Continuation に関する記述を、Railsガイドの他の箇所と同じスタイル(コードフォント + 参照リンク)に揃えることで、内部リンクレンダリングが正しく行われるようにしたものです。
- 影響範囲・注意点
影響範囲
- ドキュメント(Active Support Instrumentationガイド)のみ。
- アプリケーションコード、フレームワークの挙動、API仕様には一切変更なし。
- ガイドのHTML生成時に、
Continuationへのリンクが正しく表示されるようになります(体裁・可読性向上)。
注意点
- ドキュメントビルド/サイト表示を確認する際、
Continuationへのリンクが適切な箇所(該当クラスや節)に飛ぶことを確認する程度で十分です。 [ci skip]がタイトルに含まれている通り、CIはスキップしており、テスト実行対象にもなっていませんが、内容がドキュメントのみのため問題ありません。
- ドキュメントビルド/サイト表示を確認する際、
- 参考情報 (あれば)
- Rails GuidesのMarkdown表記慣習(クラス・メソッド名のリンク例):
[`ActiveSupport::Notifications`][][`Time.zone`][]
- Active Support Instrumentationガイド全体を読むことで、
Continuationを含む通知・計測の仕組みの文脈が把握できます。
#58982 Make the Ractor snapshot of the subscriptions an immutable registry
マージ日: 2026/10/8 | 作成者: @kamipo
- 概要 (1-2文で)
Rails の ActiveSupport::Notifications において、Ractor が利用する「購読情報(subscriptions)のスナップショット」の実装を、Hash のコピーから「イミュータブルなレジストリオブジェクト」への差し替えにより、スレッド/Ractor セーフかつシンプルな形に整理した PR です。Ractor 間での通知購読状態の取り扱いを安全かつ効率的にするための内部実装変更で、外部 API は基本的に変わりません。
- 変更内容の詳細
背景: これまでの実装
- ActiveSupport::Notifications は
Fanoutクラスを使って、イベント名ごとの購読者(subscriber)を管理しています。 - Ractor 対応の一環として、「Ractor が通知を処理するために、
Fanout内部の購読状態のスナップショットを取って渡す」という仕組みがあります。 - 以前は、このスナップショットは以下のような流れで作られていました:
Fanout内部の@string_subscribers(イベント名 => subscriber の Hash 的な構造)から、@string_subscribers.keys.zip(@string_subscribers.values)のようにしてペアを作る- これを元に、Ractor 側では
Concurrent::Mapに組み直す
- しかもこの
keys.zip(values)が@string_subscribersを保護する mutex の外で読まれていたため、理論上は整合性・可視性の問題を引き起こし得る状態でした。
この PR での主な変更点
1. Fanout::Registry の導入/強化
Fanoutに対して「購読情報の不変(イミュータブル)なスナップショット」を表すFanout::Registryクラス(もしくはそれに相当する内部クラス)が導入・拡張されました。- レジストリは次のような性質を持つように設計されています:
- イミュータブル(内部状態を変更しない)
subscribe/unsubscribeが「レジストリを破壊的に変更する」のではなく、「新しいレジストリインスタンスを生成して差し替える」方式に変更される- これにより、「現在の購読状態のスナップショット」は「ただ 1 つの frozen オブジェクト」を指すだけになります。
イメージ的には:
class Fanout
def initialize
@registry = Registry.empty # イミュータブルなレジストリ
end
def subscribe(name, subscriber)
# @registry をミューテートせず、新しいレジストリを作って入れ替える
@registry = @registry.add(name, subscriber)
end
def unsubscribe(name, subscriber)
@registry = @registry.remove(name, subscriber)
end
def registry_snapshot
@registry # frozen なオブジェクトをそのまま返すだけ
end
end実際のコードはもっと込み入っていますが、設計思想としてはこのような形です。
2. Ractor へのスナップショット受け渡しの簡略化
- 以前:
- スナップショットは Hash をベースにした構造で、Ractor 側で再構築 (
Concurrent::Mapへの入れ替えなど) が必要だった。 - かつ、mutex 外で
keys.zip(values)しているので、理論上は読み取り中に変更が入る可能性があった。
- スナップショットは Hash をベースにした構造で、Ractor 側で再構築 (
- 今回:
- スナップショットは「イミュータブルな
Fanout::Registryオブジェクト」そのもの。 - スナップショットの取得は「1 つの frozen オブジェクトを読むだけ」になり、
- Ractor 側でのロードは単なる「代入」になる:
- スナップショットは「イミュータブルな
# スナップショットを作る側
snapshot = notifier.registry_snapshot # frozen Registry
# スナップショットを使う側(Ractor 内)
notifier.registry = snapshot # 代入だけで完了- これにより、
- ミューテックス外から内部用 Hash を生で読む必要がなくなり、
- Ractor 間で渡すデータ構造も単純で、安全なものになります。
3. 「メイン Ractor の購読を他 Ractor に引き継ぐか」の挙動は変更しない
- 説明文にある通り、以前の PR #58760 では
- 「メイン Ractor であとから追加された購読情報を他 Ractor にも拾わせる」挙動の変更も含まれていました。
- その後、コミット
9e8fe395によって- 「メイン Ractor だけがグローバルな購読を記録し、非メイン Ractor はそれぞれ自分専用の notifier を持って購読する(Ractor ローカル)」という方針に落ち着いています。
- そのため、
- 「メイン Ractor の購読変更を、すでに動いている他 Ractor にどう反映するか」は、別の設計上の問いになり、この PR では触らない方針になっています。
- この PR では「どのような構造でスナップショットを表現するか」だけを変更します。
4. テストの微修正
activerecord/test/cases/connection_adapters/ractor_connection_handling_test.rbが +2/-3 行だけ変更されています。- おそらく、Ractor 周りの挙動変更に伴うアサーションの微調整、あるいは新たな実装に合わせた小さな修正です。
- ActiveRecord 側では、Ractor 下でのコネクションハンドリングが ActiveSupport::Notifications に依存しているため、その連携部分のテストが影響を受けていますが、機能仕様レベルでの大幅な変更はありません。
- 影響範囲・注意点
外部 API への影響
ActiveSupport::Notifications.subscribe/unsubscribeなどの公開 API の使い方は変わりません。- 通常のアプリケーションコードはこの変更の影響をほぼ受けません。
Ractor を使った場合の挙動
- 以前と同様、「非メイン Ractor は自分専用の notifier / レジストリを持つ」という方針を維持しているため、
- 「メイン Ractor で後から追加した subscriber が、自動的に他 Ractor にも見えるようになる」といった振る舞いは、この PR では導入されません。
- Ractor ベースで細かく通知系を制御している場合は、引き続き「Ractor ごとに購読状態が独立している」前提で設計する必要があります。
- 以前と同様、「非メイン Ractor は自分専用の notifier / レジストリを持つ」という方針を維持しているため、
パフォーマンス・メモリ
- レジストリがイミュータブルになったことで、
subscribe/unsubscribeのたびにレジストリオブジェクトが差し替えられます。 - 通常は通知の購読変更はそれほど頻繁には行われないケースが多いため、このオーバーヘッドは限定的と考えられます。
- 一方で、読み取り側は「単なるオブジェクト参照の読み取り」だけで済むため、Ractor 的にはより効率的で安全になります。
- レジストリがイミュータブルになったことで、
スレッド/Ractor セーフティ
- ミューテックス外から内部 Hash を読む形ではなく、イミュータブルなスナップショットオブジェクトを使うことで、
- データ競合や中途半端な状態の読み取りといった問題のリスクが小さくなっています。
- 特に Ractor 間でオブジェクトを渡す場合、イミュータブルであることは大きな利点です。
- ミューテックス外から内部 Hash を読む形ではなく、イミュータブルなスナップショットオブジェクトを使うことで、
- 参考情報 (あれば)
- 関連 PR:
- #58760: 本来この変更と一緒に入るはずだった、Ractor における購読伝播などを含む大きめの変更。
9e8fe395: 「メイン Ractor だけがグローバルに記録し、非メインは Ractor ローカルに購読する」という方針を確立したコミット。
- 関連クラス・モジュール:
ActiveSupport::Notifications::FanoutActiveSupport::Notifications::Fanout::Registry(本 PR の主な変更対象)Concurrent::Map(以前のスナップショット先)
- Ractor 利用時に気にすべきポイント:
- 非メイン Ractor から通知を購読したい場合は、その Ractor 内で
ActiveSupport::Notifications.subscribeする(メインからの自動伝播はこの PR では扱っていない)。
- 非メイン Ractor から通知を購読したい場合は、その Ractor 内で
#58976 Make TimeZone.all and offset lookups work in non-main Ractors
マージ日: 2026/10/8 | 作成者: @yaroslav-shopify
- 概要 (1-2文で)
ActiveSupport::TimeZoneを Ractor 環境で安全に使えるようにするため、TimeZone.allやTimeZone[-5]などがメイン Ractor 以外でもRactor::IsolationErrorを出さずに動くよう、内部データ構造をmake_shareable!で共有可能オブジェクトにする修正です。これによりractorize!利用時のタイムゾーン関連メソッドの不具合が解消されます。
- 変更内容の詳細
背景
TimeZone.ractorize! は内部的に TimeZone.make_shareable! を呼び、Ractor 間で共有されるデータを「shareable (不変かつ Ractors 間で共有可能)」にします。
しかし、従来の make_shareable! は以下の内部変数を対象にしておらず:
@zones(利用可能な TimeZone オブジェクトの配列)@zones_map(オフセットや名前から TimeZone を引くためのマップ)
その結果、メイン Ractor 以外で以下の処理を行うと Ractor::IsolationError が発生していました:
TimeZone.all- オフセット検索:
TimeZone[-5] time_zone_select(内部でTimeZone.allなどを利用)Time.find_zone(-5)が nil を返す (マップが使えないため)
今回の修正内容
ActiveSupport::TimeZone.make_shareable! の中で、上記 2つの内部変数も shareable にするように変更されています。
変更されたポイントの概要:
# activesupport/lib/active_support/values/time_zone.rb (概略)
def self.make_shareable!
# 既存の shareable 化処理に加えて:
@zones&.shareable!
@zones_map&.shareable!
end※ 実際のコードは 2 行追加のみですが、意味としては上記のように@zones と @zones_map を shareable にする処理が追加されたと考えてよいです。
これにより、TimeZone.ractorize! 実行後に生成された Ractor 内でも:
Ractor.new do
TimeZone.all # IsolationError が出ずに一覧が取得できる
TimeZone[-5] # オフセット検索も可能
Time.find_zone(-5) # 期待どおり TimeZone オブジェクトが返る
endのような操作が安全に行えます。
テストの追加
activesupport/test/time_zone_test.rb に Ractor を使ったテストが追加されています。
内容としてはおおよそ以下を確認しているはずです:
TimeZone.ractorize!実行後に新しい Ractor を作成し、TimeZone.allがRactor::IsolationErrorを投げないことTimeZone[-5]などのオフセット検索が機能することTime.find_zone(-5)が nil ではなく有効な TimeZone を返すこと
これにより、Ractor 対応が回 regress していないことを自動テストで保証しています。
- 影響範囲・注意点
- 影響範囲
ActiveSupport::TimeZoneを Ractor 環境で利用している/今後利用したいアプリケーションやライブラリ。- 特に、バックグラウンド処理や並列実行で Ractor を採用し、かつタイムゾーン一覧やオフセット検索を使うコードに対して有益な修正です。
- 非 Ractor 環境
- 通常の(Ractor を使わない)Rails アプリケーションでは挙動の変化は事実上ありません。
shareable 化は Ractor の有無にかかわらず安全な操作であり、既存コードに影響しない作りになっています。
- 通常の(Ractor を使わない)Rails アプリケーションでは挙動の変化は事実上ありません。
- パフォーマンス/メモリ
@zones/@zones_mapは本来グローバルに1度構築されてほぼ読み取り専用で使われるデータであり、shareable 化によるオーバーヘッドは小さいと考えられます。- Ractor 間でコピーされずに共有されることで、むしろ Ractor 利用時のメモリ効率は改善の可能性があります。
- 安定性・API 変更
- 公開 API (
TimeZone.all,TimeZone[-5],Time.find_zone等) のシグネチャや戻り値の仕様は変わっていません。 ractorize!自体は:nodoc:で実験的 API という扱いのままです。
- 公開 API (
- CHANGELOG
- Ractor 関連はこれまでも CHANGELOG に載せていない経緯があり、今回も更新されていません。
そのため、Ractor を前提とした運用をしている場合は、CHANGELOG ではなく PR/コミットを直接追う必要があります。
- Ractor 関連はこれまでも CHANGELOG に載せていない経緯があり、今回も更新されていません。
- 参考情報 (あれば)
- この PR の主な目的は、Ractor 利用時に
ActiveSupport::TimeZoneの主要な機能:- タイムゾーン一覧取得 (
TimeZone.all) - オフセット検索 (
TimeZone[-5]など) time_zone_select/Time.find_zoneなどの派生機能
がRactor::IsolationErrorによって実質使えなかった状況を解消することです。
- タイムゾーン一覧取得 (
- Ractor を使っているコードで
TimeZone関連の IsolationError をワークアラウンドしていた場合(メイン Ractor で事前生成して渡す等)、この修正によりシンプルな実装に戻せる可能性があります。
#58985 [doc][ci skip] Fix local: to locals: in Action View Overview guide
マージ日: 2026/10/8 | 作成者: @hachi8833
- 概要 (1-2文で)
Action View Overviewガイド中のヘルパーメソッドのシグネチャ表記で、誤ってlocal:と書かれていた箇所を正しいlocals:に修正したドキュメント専用PRです。コード本体には一切変更はなく、あくまでガイドの記述ミス修正です。
- 変更内容の詳細(あればサンプルコードも含めて)
対象ファイル:
guides/source/action_view_overview.md(+2/-2)
修正内容は、「Action View の各種ヘルパーのメソッドシグネチャを説明している部分」におけるオプション引数名の誤記です。
具体例(イメージ):
- render partial: "product", object: @product, local: { editable: true }
+ render partial: "product", object: @product, locals: { editable: true }Rails の render でパーシャルにローカル変数を渡す際は、オプションキーは単数形 local ではなく、正式には複数形 locals を使います。
そのため、ガイドのシグネチャ説明やサンプルにおいて local: となっていた部分を、実際のAPI仕様と整合するように locals: へ修正しています。
- 影響範囲・注意点
- 実際の挙動・コードには一切影響しないドキュメント限定の修正です。
- 影響範囲は Action View Overview ガイドを読む開発者のみですが、これまでガイドの誤記を信じて
local:を使っていた場合、正しくはlocals:であることを再確認する必要があります。 - 既存のアプリでは、
local:はオプションとして解釈されず、期待通りにローカル変数が渡っていなかった可能性があります。正しい書き方の例:ruby# 正しい render partial: "product", locals: { editable: true } # 誤り (このPRで修正された誤記に相当) render partial: "product", local: { editable: true } - CI スキップ (
[ci skip]) 指定がタイトルにある通り、テスト実行も不要な軽微なドキュメント修正として扱われています。
- 参考情報 (あれば)
- Rails ガイド: Action View Overview
(最新版のガイドでは、本PRの反映後locals:が正しく使われているはずです) renderの公式APIドキュメント(ActionView::Base / ActionView::Helpers::RenderingHelper など)render(partial: ..., locals: {...})という形でlocals:オプションが定義されています。
#58973 [doc][ci skip] Fix incorrect route in Generators guide
マージ日: 2026/10/7 | 作成者: @hachi8833
- 概要 (1-2文で)
Generators ガイド内のルーティング例で、PeopleController#indexに対応するパスが誤って単数形personになっていたのを、正しい複数形peopleに修正するドキュメントのみの変更です。アプリケーションコードや挙動には一切影響しません。
- 変更内容の詳細(あればサンプルコードも含めて)
- 対象ファイル:
guides/source/generators.md - 修正内容: ガイド内のルーティング記述を、コントローラ名と一致する形に修正。
変更前(イメージ):
person GET /person(.:format) people#index変更後:
people GET /people(.:format) people#indexもしくは同等の、「person → people」という単純な文字列修正です。
Generators ガイドの「6.12. route」節にある例で、PeopleController#index に対応するルート名・パスは RESTful な慣例に従い複数形であるべきところが、誤って単数形で書かれていたのを修正しています。
- 影響範囲・注意点
- 影響範囲:
- Rails ガイド(オンラインドキュメント)の表示内容のみ。
- Rails 本体のコード、ジェネレータ、ルーティング機構には変更なし。
- 注意点:
- 実際の Rails アプリで
resources :peopleなどを使った場合、people_path//peopleが生成されるのが正しい挙動であり、本 PR はその挙動とガイドの記述を一致させるものです。 - 既存アプリのルーティング設定を変更する必要はありません。これはあくまでガイドの誤記修正です。
- 実際の Rails アプリで
- 参考情報 (あれば)
- 該当ガイド: Generators guide – 6.12. route
- Rails の RESTful ルーティング規約:
resources :people→/people,people#indexなど複数形resource :profile→/profile,profiles#showなど単数リソースの場合のみ単数形
今回の修正は、この一般的な RESTful 規約に沿った形にドキュメントを揃えるものです。
#58971 [doc][ci skip] Fix wrong job name in config.active_record.queues.destroy docs
マージ日: 2026/10/7 | 作成者: @hachi8833
概要 (1-2文で)
config.active_record.queues.destroyの公式ガイドの説明文に、誤って「purge jobs」を指している箇所があり、実際に設定対象となる「destroy jobs」に修正したドキュメントのみの変更です。コードや挙動は一切変わっておらず、説明文の内容が実装に揃えられました。変更内容の詳細
対象:
Rails ガイドの Configuring Rails Applications 内のセクションconfig.active_record.queues.destroy
元の説明では:
config.active_record.queues.destroy = nilの場合、
「purge jobs が default キューに送られる」
という趣旨の文言になっていました。
しかし、実際のオプションは:
ActiveRecord::DestroyAssociationAsyncJob(関連削除を非同期で行うジョブ)- つまり「destroy jobs」の実行キューを指定するオプション
であり、「purge jobs(おそらく ActiveRecord::Purge… 系のジョブ)」のキュー設定ではありません。
この PR では説明文中の「purge jobs」を「destroy jobs」に修正し、実装とドキュメントの整合性を取っています。
※ guides/source/configuring.md における 1 行のみの修正で、サンプルコード等はありません。
- 影響範囲・注意点
- 影響範囲:
- 公式ドキュメントを読んでキュー設定を行う開発者に対する認識修正のみ
- Rails 本体の挙動や API は一切変更されていません
- 注意点:
- 今まで「purge jobs のキュー設定」と誤解して
config.active_record.queues.destroyを使っていた場合、その理解が間違いだったことに注意が必要です。 - destroy 関連(
ActiveRecord::DestroyAssociationAsyncJob)と purge 関連(例: Active Storage や Active Record のクリーンアップ系ジョブ)のキュー設定を混同しないようにする必要があります。 - queue 設定の確認時には、対応するジョブクラス(この場合
ActiveRecord::DestroyAssociationAsyncJob)とオプション名を突き合わせると安全です。
- 今まで「purge jobs のキュー設定」と誤解して
- 参考情報 (あれば)
- 該当ガイド:
https://guides.rubyonrails.org/configuring.html#config-active-record-queues-destroy - 実装上関係するジョブクラス(参考):
ActiveRecord::DestroyAssociationAsyncJob- has_many :through などの関連削除を非同期で行う際に利用されるジョブ
- 類似オプション(混同注意):
config.active_record.queues.purge(もし別途存在する場合)など、purge 系のキュー設定オプションとは用途が異なる点に注意してください。
#58969 [doc][ci skip] Fix wrong config name for CheckPending in Rails on Rack guide
マージ日: 2026/10/7 | 作成者: @hachi8833
- 概要 (1-2文で)
Rails Guides「Rails on Rack」で、未実行マイグレーション検知用ミドルウェアActiveRecord::Migration::CheckPendingを有効化する設定名が誤っていたため、正しい設定項目に修正し、併せてその設定を「Configuring Rails Applications」ガイドにも正式に追記したドキュメント修正です。アプリの動作には影響せず、ガイドの記述のみが更新されています。
- 変更内容の詳細
(1) Rails on Rack ガイドの誤記修正
対象: guides/source/rails_on_rack.md
修正前の誤った説明
ActiveRecord::Migration::CheckPending の説明として、以下のような文脈になっていました(要旨):
config.action_dispatch.x_sendfile_header = :page_loadを設定するとActiveRecord::Migration::CheckPendingが使われる
しかし:
config.action_dispatch.x_sendfile_headerはRack::Sendfile向けの設定であり、ファイル送信ヘッダ (X-Sendfileなど) を指定するためのもの- 未実行マイグレーション検知とは無関係
修正後の正しい説明
未実行マイグレーションを検知するミドルウェア ActiveRecord::Migration::CheckPending は、以下の設定によって挿入される、という説明に修正されました:
config.active_record.migration_error = :page_loadリンク先も config.action_dispatch.x_sendfile_header ではなく、config.active_record.migration_error の説明セクションに飛ぶように変更されています。
実装コードとの対応
ガイドの説明が、実際の Rails 実装(activerecord/lib/active_record/railtie.rb)と一致するように修正された形です。実装は次の通り:
initializer "active_record.migration_error" do |app|
if config.active_record.migration_error == :page_load
config.app_middleware.insert_after ::ActionDispatch::Callbacks,
ActiveRecord::Migration::CheckPending,
file_watcher: app.config.file_watcher
end
endポイント:
config.active_record.migration_error == :page_loadのときにのみミドルウェアを挿入ActionDispatch::Callbacksの直後にActiveRecord::Migration::CheckPendingを挿入file_watcherオプションにapp.config.file_watcherを渡している
つまり:
- 「開発環境でページを開いたときに、未実行マイグレーションがあれば例外を出す」機能は、
config.active_record.migration_error = :page_loadで有効化される x_sendfile_headerとは全く別物
(2) Configuring ガイドへの設定オプションの追記
対象: guides/source/configuring.md
「Configuring Active Record」のセクションに、新たに config.active_record.migration_error の説明項目が追加されました。
要旨:
- 設定項目名:
config.active_record.migration_error - 想定される値:
:page_load— リクエスト時に未実行マイグレーションがあると例外を発生させる- 未設定または
nil— このチェックを行わない(ミドルウェアを挿入しない)
- 用途:
- 主に開発環境で、マイグレーションのやり忘れを早期に検知するため
Rails の Railtie 内ではデフォルト値は設定されておらず、通常は config/environments/development.rb で以下のように生成されます:
# config/environments/development.rb
config.active_record.migration_error = :page_loadこの PR によって:
- Rails on Rack ガイドから張られているリンクの遷移先(
config.active_record.migration_errorの説明)が新規に追加されたため、リンク切れが解消されました。 migration_errorオプションの存在・挙動が正式なドキュメントとして明示された。
- 影響範囲・注意点
- 影響範囲:
- コード変更はなく、ガイド文書 (
.md) のみの変更です。 - 実際のミドルウェア挿入ロジックや設定の挙動は従来通りで、アプリ動作への影響はありません。
- コード変更はなく、ガイド文書 (
- 注意点 / 開発者視点での示唆:
- これまでガイドを読んで「
config.action_dispatch.x_sendfile_header = :page_loadで pending migration エラーが出る」と思い込んでいた場合、その理解は誤りであり、正しくはconfig.active_record.migration_error = :page_loadであることを再確認する必要があります。 - 未実行マイグレーション検知をオフにしたい場合は、
config.active_record.migration_error = falseないしnilにする構成が一般的です(環境や Rails バージョンによって推奨値はドキュメントを参照)。 - 既にコードベースで
migration_errorを利用しているプロジェクトには変更は不要ですが、ガイドに沿って設定を見直すときに混乱が起きにくくなります。
- これまでガイドを読んで「
- 参考情報 (あれば)
- 対応する実装:
activerecord/lib/active_record/railtie.rbのinitializer "active_record.migration_error"ブロック
- 関連する設定:
config/environments/development.rb内のconfig.active_record.migration_error = :page_loadRack::Sendfile関連:config.action_dispatch.x_sendfile_header(X-Sendfile/X-Accel-Redirectなどを指定、ファイル送信のための設定で、本 PR の件とは無関係)
#58968 [doc][ci skip] Fix typo in caching_with_rails.md
マージ日: 2026/10/7 | 作成者: @hachi8833
- 概要 (1-2文で)
Railsガイド「caching_with_rails.md」において、権限ロール名の表記ゆれ/誤記をsuperusersからsuper_adminsに修正したドキュメント専用のPRです。機能コードには一切変更がなく、挙動には影響しません。
- 変更内容の詳細
- 対象ファイル:
guides/source/caching_with_rails.md - 変更内容: 文字列の置き換えのみ
- superusers
+ super_admins文脈としては、キャッシュの例や権限管理の例示で使われているロール名において、他の記述と一貫した名称(super_admins)に揃えるための修正と考えられます。
ドキュメント中で、例えば以下のような記述になっている可能性があります(イメージ):
# 修正前(イメージ)
We don't want to cache pages for superusers.
# 修正後(イメージ)
We don't want to cache pages for super_admins.つまり、ガイド全体またはサンプルコードで使われているロール名 super_admins に合わせたタイポ修正です。
- 影響範囲・注意点
影響範囲
- Rails本体のコード、API、動作には一切影響しません。
- 影響するのは「キャッシュに関するRailsガイド」の表記のみです。
- CIスキップ指定(
[ci skip])からも、ドキュメントだけの変更であることがわかります。
注意点
- もし自分のアプリ側で、ガイドをそのままコピペして
superusersというロール名を使っている場合、ガイドの最新表記(super_admins)と異なる可能性があります。
ただしこれはあくまで命名規則の問題であり、アプリが壊れる類のものではありません。 - ドキュメントを読みながら実装する際は、ロール名などを自分のアプリのドメインに合わせて適宜変更して使うのが望ましいです。
- もし自分のアプリ側で、ガイドをそのままコピペして
- 参考情報 (あれば)
- PRタイトルからわかる通り、
[doc][ci skip]は「ドキュメント変更でありCIは回さない」ことを明示するRailsプロジェクト慣例のプレフィックスです。 - 類似のドキュメント修正PRでは、表記ゆれ・スペルミス・古いサンプルコードの更新などが頻繁に行われており、ユーザーは最新ガイドを参照することでより正確な情報を得られます。
#58963 Use QueryResponse as raw result in ractor proxy adapters
マージ日: 2026/10/7 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Rails の Ractor 用プロキシアダプタで使われていた独自のクエリ結果オブジェクトを廃止し、標準のQueryResponseを「生の結果」として採用する変更です。これにより、last_inserted_idなどの扱いがシンプルかつ一貫した形になります。
- 変更内容の詳細
背景と目的
- Ractor ベースの接続ハンドラ (
ractor_connection_handler) 向けのプロキシアダプタでは、通常の ActiveRecord アダプタとは別に「専用のクエリ結果オブジェクト」が実装されていました。 - この二重構造は以下の問題を生みやすいです:
- 一般のアダプタとコードパスがずれることで、メンテナンスコストが増える
last_inserted_idなど、結果オブジェクト経由で参照する情報の扱いが複雑になる
- そこで、この PR ではプロキシアダプタ側も共通の
QueryResponseを使うようにして、実装を統一・簡素化しています。
主な変更点
1) 抽象プロキシアダプタのロジック変更
activerecord/lib/active_record/connection_adapters/ractor_connection_handler/abstract_proxy_adapter.rb
- プロキシアダプタが DB クエリ結果を保持/返却する際に、これまで使っていた独自の
Resultではなく、QueryResponseを「生(=raw) のクエリ結果」として扱うように変更。 - それに伴い、
last_inserted_idを取得する処理もQueryResponseを前提とした実装に整理されたと考えられます。
ざっくりしたイメージ(疑似コード):
# 変更前(イメージ)
def execute(sql, name = nil)
raw = @real_adapter.execute(sql, name)
Result.new(raw) # プロキシ用のラッパークラス
end
def last_inserted_id(result)
result.last_inserted_id
end
# 変更後(イメージ)
def execute(sql, name = nil)
QueryResponse.new(@real_adapter, sql, name)
end
def last_inserted_id(query_response)
query_response.last_inserted_id
end※ 実際のコードは多少異なりますが、独自 Result をやめて QueryResponse を直接使う方向に集約されています。
2) QueryResponse のプロキシ版の整理
activerecord/lib/active_record/connection_adapters/ractor_connection_handler/abstract_proxy_adapter/query_response.rb
- ここには、Ractor 用プロキシアダプタが利用する
QueryResponse関連のコードがあり、今回 8 行追加 / 12 行削除されています。 - 主な意図は:
- プロキシ専用の中途半端なラッパーを削り、共通の
QueryResponseインターフェイスに合わせて薄いラッパー or 単純な委譲に寄せる last_inserted_idを含むレスポンス API を、通常のアダプタと揃える
- プロキシ専用の中途半端なラッパーを削り、共通の
ざっくりとした変更イメージ:
# 変更前: 独自フィールドやメソッドを持っていた
class AbstractProxyAdapter::QueryResponse
def initialize(raw_result)
@raw = Result.new(raw_result)
end
def last_inserted_id
@raw.last_inserted_id
end
end
# 変更後: 生結果として QueryResponse をそのまま扱う/最低限の委譲に留める
class AbstractProxyAdapter::QueryResponse
def initialize(query_response)
@query_response = query_response
end
def last_inserted_id
@query_response.last_inserted_id
end
end(実際にはもう少し薄いか、あるいは完全に QueryResponse そのものを返す方向です。)
3) 独自 Result クラスの削除
activerecord/lib/active_record/connection_adapters/ractor_connection_handler/abstract_proxy_adapter/result.rb
- 23 行すべて削除されており、このファイル自体が不要になっています。
- これは「プロキシアダプタだけが持っていた独自 Result クラス」を廃止した、ということを意味します。
- 以後は、通常のアダプタ同様に
QueryResponseを基準にコードを書けばよくなります。
- 影響範囲・注意点
影響範囲
- 対象: Ractor ベースの
ractor_connection_handlerを使っている環境 + その配下のプロキシアダプタ。 - 通常の (非 Ractor) ActiveRecord 接続アダプタには影響しない設計になっています。
- 内部的な抽象化レイヤーの整理が中心であり、アプリケーションコードから見えるインターフェイスは基本的に変わりません。
注意点 / 互換性
- 公開 API ではなく内部クラス (
AbstractProxyAdapter::Result等) に依存したメタプログラミングや Monkey Patch をしていた場合は、壊れます。- 例:
ActiveRecord::ConnectionAdapters::RactorConnectionHandler::AbstractProxyAdapter::Resultを直接参照しているコードがあれば削除・変更が必要。
- 例:
last_inserted_idの取得元が統一されるため、Ractor 経由の接続でも通常アダプタと同じ挙動になることが期待されますが、Ractor 特有の並行実行環境で ID 取得周りをハックしていた場合は挙動確認を推奨します。- この PR 自体ではテスト追加はされておらず、既存テストでカバーされている前提の変更です。Ractor を本番利用している場合は、マージ後に:
- INSERT/UPSERT 系のクエリ
create直後のid参照 などを含む一連のテストを追加・実行しておくと安心です。
- 参考情報 (あれば)
- 関連しうるコード:
ActiveRecord::ConnectionAdapters::QueryResponse本体(通常アダプタで既に使われている共通クラス)ractor_connection_handler配下の他のアダプタ実装
- 実務的な意味:
- Ractor 対応コードの内部 API が通常アダプタと揃ってくることで、将来的に:
- Ractor 対応まわりのリファクタリング
- コネクションアダプタの共通化
- 並行実行時の動作検証 がしやすくなります。
- Ractor 対応コードの内部 API が通常アダプタと揃ってくることで、将来的に:
#58972 File.atomic_write: better cleanup on error
マージ日: 2026/10/7 | 作成者: @byroot
- 概要 (1-2文で)
File.atomic_writeで一時ファイルから本来のファイルへのrenameが失敗した場合でも、一時ファイルを確実に削除するように後始末処理が改善されました。これにより、例外発生時に一時ファイルがディスク上に残り続ける問題が解消されます。
- 変更内容の詳細
※ PR本文からは振る舞いの概要のみが分かるため、Rails 既存の File.atomic_write 実装と一般的なパターンに基づいて技術的に説明します。
もともとの File.atomic_write の概要
Active Support の File.atomic_write は概ね以下のような流れで動いています:
File.atomic_write("path/to/file") do |file|
file.write("content")
end内部的には:
- 同じディレクトリに一時ファイル (例:
".file-20261008-12345-1xyz.tmp") を作成 - ブロックに渡された IO に対して書き込み
fsyncなどでフラッシュ (環境により挙動は異なる)File.rename(temp_file, target_path)で一時ファイルをターゲットパスにアトミックに置き換え- 例外が起きた場合は一時ファイルを削除
この「例外が起きた場合」の扱いが従来はやや不十分でした。
今回の修正ポイント
PR の説明文:
If the rename fails, we still need to cleanup the temporary file.
から読み取れる変更点は次の通りです。
rename実行時 (File.rename(temp_path, path)) に例外が発生した場合でも、- 一時ファイルを確実に
unlink/deleteするようにした - その上で、元の例外をそのまま呼び出し元に伝播させるような構造に変更
- 一時ファイルを確実に
典型的には、従来はこんな感じのコードになっていたものが:
def atomic_write(file_name, temp_dir = Dir.tmpdir)
# temp_path を作る...
File.open(temp_path, "wb") do |file|
yield file
end
# ここで rename
File.rename(temp_path, file_name)
ensure
# 例外時だけ temp_path を削除したかったが、
# rename 例外がこの ensure よりも先に処理されて
# 一部のパスで cleanup が漏れる、などの問題があった
endこれを、以下のように「rename に失敗した場合でも必ず cleanup する」構造に修正していると考えられます:
def atomic_write(file_name, temp_dir = Dir.tmpdir)
# temp_path を作成
temp_path = ...
begin
File.open(temp_path, "wb") do |file|
yield file
end
begin
File.rename(temp_path, file_name)
rescue
# rename に失敗しても、一時ファイルを削除
File.delete(temp_path) if File.exist?(temp_path)
raise
end
ensure
# 正常終了 or 例外発生に関わらず、
# rename で既に削除されていなければ cleanup
File.delete(temp_path) if File.exist?(temp_path)
end
end実際の実装はもう少しコンパクト/安全に書かれているはずですが、
趣旨としては:
renameが成功したなら一時ファイルは既にターゲットファイルとして存在 → cleanup は不要かつ安全にスキップrenameが失敗したなら、かならずFile.delete(temp_path)する- どのコードパスを通っても「一時ファイルがディスクに残らない」
ようにロジックを整理しています。
テストの追加 (activesupport/test/core_ext/file_test.rb)
+12 行の追加から推測されるテスト内容は:
File.atomic_write実行中にFile.renameをわざと失敗させる(モック/スタブ)- その後、一時ファイルが存在しないことを検証
- かつ、
File.atomic_writeが元の例外(SystemCallErrorなど)を呼び出し元に伝播していることを確認
のようなケースが追加されているはずです。
- 影響範囲・注意点
影響範囲
- 対象:
ActiveSupportのFile.atomic_writeを利用している全てのコード- 例:
Rails.cacheの一部ストア実装ActiveSupport::Cache::FileStoreActiveSupport::EncryptedFile/EncryptedConfiguration- 各種設定ファイルや YAML をアトミックに更新するユーティリティ 等
- 例:
実質的な挙動の変化
renameに失敗したケースでの 一時ファイルの残存 が解消されます。- 正常に
atomic_writeが完了するケースの挙動は従来通りで、互換性に問題はないと考えられます。 renameが失敗した場合に投げられる例外クラスやメッセージは従来と同じ(Errno::EXDEVなど)で、
それに加えて「必ず一時ファイルが消える」という保証が強くなりました。
注意点
- 既存アプリ側で「
atomic_writeが失敗したとき、一時ファイルを自分で調査・回収する」ような運用やツールに依存している場合、その前提が変わります。- 一般的には望ましい変更であり、問題が出るケースはほぼないはずですが、
デバッグ用途で「一時ファイルが残るのを頼りにしていた」場合は挙動が変わる点に注意してください。
- 一般的には望ましい変更であり、問題が出るケースはほぼないはずですが、
rename失敗の典型的な原因(異なるファイルシステム間での rename、パーミッションエラー、ディスクフル等)はこの変更で解消されるわけではありません。- それらは従来通り例外として呼び出し元でハンドリングが必要です。
- cleanup ロジックが強化されたことで、ごくまれなタイミング依存バグ(例: 別プロセスが一時ファイルを掴んでいる場合など)が顕在化する可能性は理論上ありますが、通常の Rails アプリで問題になることはまずありません。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58972
File.atomic_writeの既存ドキュメント (Rails Guides / API):- https://api.rubyonrails.org/classes/File.html (Active Support による拡張部分)
File.rename失敗時の代表的なエラー:Errno::EXDEV(異なるデバイス間の rename)Errno::EACCES/Errno::EPERM(パーミッション関連)- これらは今回の変更後もそのまま呼び出し元に伝播するため、必要に応じて rescue して対処してください。
#58904 Use a BoundSchemaReflection for the Ractor proxy pool's schema cache
マージ日: 2026/10/6 | 作成者: @gmcgibbon
- 概要 (1-2文で)
Ractor 用の Active Record プロキシ接続プールが持つスキーマキャッシュの実装を、独自クラスではなく「BoundSchemaReflection」を用いたよりシンプルな仕組みに置き換えた PRです。これにより、Ractor 向けアダプタ周辺のスキーマキャッシュ用コードが整理・削減され、保守性と一貫性が向上しています。
- 変更内容の詳細
全体像
- これまで Ractor 用の proxy connection pool は「スキーマキャッシュ相当のメソッドを持つ独自クラス」を生成して利用していました。
- この PR では、その「自前のスキーマキャッシュプロキシ」を廃止し、Active Record が既に持っている BoundSchemaReflection を利用して、スキーマキャッシュをバインドした形で扱うように変更しています。
- それに伴い、mysql/postgresql/sqlite3 用の各 Ractor proxy adapter に散らばっていたスキーマキャッシュ関連のコードが削除され、抽象クラス/プール側にロジックが集約されています。
主な変更ポイント
1) abstract_proxy_adapter.rb にスキーマキャッシュ扱いの共通ロジックを追加
activerecord/lib/active_record/connection_adapters/ractor_connection_handler/abstract_proxy_adapter.rb に、Ractor 向け proxy adapter が共通で使うスキーマキャッシュ設定の仕組みが追加されています(+20行)。
内容としては概ね以下のようなイメージになります(擬似コードレベル):
module ActiveRecord
module ConnectionAdapters
module RactorConnectionHandler
class AbstractProxyAdapter
# ...
def schema_cache
@schema_cache ||= begin
# 実際の接続(または元アダプタ)のスキーマリフレクションに
# BoundSchemaReflection をバインドして返す
connection.schema_reflection.bind
end
end
end
end
end
endポイント:
- 各 DB ごとに個別実装していたスキーマキャッシュ関連コードを抽象アダプタで一本化。
BoundSchemaReflectionを使うことで、元の adapter の schema 情報を「束縛された」オブジェクトとして渡せるようにしている。
2) proxy_connection_pool.rb から独自のスキーマキャッシュ機構を削除
activerecord/lib/active_record/connection_adapters/ractor_connection_handler/proxy_connection_pool.rb が大きく整理されています(+2/-40)。
ここには以前、次のような「スキーマキャッシュメソッドを束ねるだけのクラス」や、それを生成して使うロジックがあったと考えられます:
# 以前のイメージ (削除された方向性のコード)
class SchemaCacheProxy
def initialize(real_schema_cache)
@real = real_schema_cache
end
# @real のメソッドをラップして転送する... など
end今回の変更で:
- こうした「転送専用のラッパークラス」や、それに付随するメタプログラミング的な仕掛けが 全て不要 になり削除。
- スキーマ関連は
BoundSchemaReflectionに一任する設計になったため、pool 側の責務が減り、より「接続管理」だけに集中したコードに。
3) 各 DB 用 Proxy Adapter からの重複コード削除
以下のファイルからスキーマキャッシュ関連のコードが削除されています。
mysql_proxy_adapter.rb(-8)postgresql_proxy_adapter.rb(-8)sqlite3_proxy_adapter.rb(-10)
おそらく以前は、各 adapter ごとに schema_cache= などをオーバーライドして、Ractor 向けの独自ラッパークラスをセットする処理がありましたが、それらが不要になっています。
これにより:
- スキーマキャッシュの扱いが DB ごとにバラけず、抽象アダプタ側で統一的に処理される。
- Ractor 対応まわりの分岐・条件分けが減り、将来的な adapter 追加・修正もシンプルになる。
4) deduplicable.rb と railtie.rb の軽微な変更
activerecord/lib/active_record/connection_adapters/deduplicable.rb(+3/-2)
BoundSchemaReflection を Ractor 間で共有/デデュプリケーション可能なオブジェクトとして扱うための対応が追加されている可能性があります。
例えば「このクラスは Ractor 間でデータを安全にコピー・共有できる」というマーカーや対応メソッドの調整など。activerecord/lib/active_record/railtie.rb(+1/-0)
Railtie 初期化時に、Ractor 用 schema cache / reflection のセットアップが正しく行われるよう、設定フックの追加・微調整が行われています。
5) テスト追加
activerecord/test/cases/connection_adapters/ractor_connection_handling_test.rb(+28/-0)
Ractor connection handling テストに、新しい BoundSchemaReflection ベースのスキーマキャッシュが正しく動作するか を検証するケースが追加されています。想定されるテスト内容:
- Ractor 内からも schema cache が参照できること
- Ractor 間で schema cache の扱いが破綻しないこと(例: 予期せぬ共有・書き換えがない)
- 既存の非 Ractor パスとの挙動が変わっていないこと
- 影響範囲・注意点
影響範囲
- 影響を受けるのは Ractor 接続ハンドラとそれに紐づく connection pool / proxy adapter 周辺のみです。
- 通常の(非 Ractor)Active Record 接続・スキーマキャッシュ処理には基本的に影響しない設計です。
- Ractor を使って DB アクセスをしている場合、内部的なスキーマキャッシュの扱いが変わりますが、外から見た API や挙動は原則互換です。
利用者側が意識すべきポイント
- アプリ側で Ractor 向け Active Record 接続に対して、スキーマキャッシュ用の独自フックや monkey patch を当てている場合、内部クラス名や構造の変更により影響を受ける可能性があります。
- 例:
ActiveRecord::ConnectionAdapters::RactorConnectionHandler::ProxyConnectionPoolの内部schema_cacheの型やメソッド構成に依存したコードがある場合は要確認。
- 例:
- 逆に、標準の API (
connection.schema_cache,connection_pool.schema_cacheなど) しか使っていない場合は、挙動の変更をほぼ意識せずにアップデートできる想定です。
Ractor と BoundSchemaReflection についての注意
- Ractor はオブジェクト共有に制約があるため、スキーマキャッシュも「安全にコピー/転送できるもの」にする必要があります。
- BoundSchemaReflection を利用することで:
- スキーマ情報を Ractor 境界越しに扱いやすくしつつ、
- かつ Active Record 内部の既存の reflection/caching ロジックと整合性を保ちやすくしている
という設計上のメリットがあります。
- 参考情報 (あれば)
- この PR は、Ractor 向け schema cache proxy 実装を「自前で賢くやりすぎていた」状態から、BoundSchemaReflection という既存の仕組みに乗せ直すリファクタリングです。
- Ractor サポートを前提に Active Record をカスタマイズしている場合は、以下のファイルの差分を一読すると意図が掴みやすいです:
abstract_proxy_adapter.rb:Ractor proxy adapter の共通インターフェースproxy_connection_pool.rb:Ractor 用 connection pool の schema_cache 処理まわりractor_connection_handling_test.rb:期待される挙動のサンプルとして参照可能
#58956 Fix to_markdown code spans broken by tables and adjacent code
マージ日: 2026/10/6 | 作成者: @flavorjones
- 概要 (1-2文で)
Action Text のto_markdownが<code>/<pre>含みの HTML を Markdown に変換する際に、テーブル内のコードや隣接するコード要素が原因でコードスパンが壊れ、結果として本来除去されるべき危険なリンク(javascript:など)がレンダリングされてしまう不具合を修正した PR です。併せて、「to_markdownはベストエフォートで unsafe link を落とすが、最終的な HTML は別途サニタイズすべき」という注意書きも追加されています。
- 変更内容の詳細
問題となっていた仕様と事象
ActionText::Content#to_markdown は、HTML から Markdown へ変換する際に、<code> や <pre> の中身をエスケープせず生で書き出し、周囲をバッククォート(`)で囲むことで「Markdown として解釈されないはず」という前提で動いています(inline_code 処理)。
ところが、以下のようなケースで、このコードスパンの区切りが Markdown パーサ側の挙動により崩れ、結果としてコード内部のテキストが Markdown として解釈されていました。そのため、本来 visit_a によってドロップされるはずの危険なリンクが <a> として残ってしまうことがありました。
対象レンダラ例:
- kramdown 2.5.2
- Redcarpet 3.6.1
- Commonmarker 2.10.0
問題パターンは次の 4 つです。
ケース1: テーブルセル内コードの先頭空白でコードスパンが壊れる
HTML:
<table><tr><td><code> [x](javascript:alert(1))</code></td></tr></table>旧 to_markdown 出力:
| `` [x](javascript:alert(1))`` |kramdown が行をセルへ分解する際にコードスパン(バッククォート列)を再構成し、その過程で が 1 つになり、先頭の backtick が「開き」にならずコードスパンが壊れる → 中身の[x](javascript:alert(1))` が Markdown のリンクとして解釈されてしまう。
ケース2: <code> 要素が隣接していると backtick がくっつく
HTML:
<p><code>a`b`</code><code>[x](https://example.com)</code></p>旧出力:
`` a`b` ```[x](https://example.com)`1 つ目のコードスパンの閉じ backtick 列と、2 つ目のコードスパンの開き backtick 列が隣接し、CommonMark 的には「連続した backtick 列」と解釈されてしまう → 二つの span が一体化して閉じなくなり、その後ろのリンク部分が通常テキストとしてパースされる → Commonmarker では <a> が生成される。
ケース3: テーブルセル内コードに |(パイプ)が含まれる
HTML:
<table>
<tr><th>a</th><th>b</th></tr>
<tr><td><code>x|[x](javascript:alert(1))</code></td><td>y</td></tr>
</table>旧出力:
| `x|[x](javascript:alert(1))` | y |GFM では、テーブルセル内の未エスケープの | はセル区切りとみなされるため、コードスパン内部の | でセルが分割され、[x](javascript:alert(1)) が別セルかつコードスパン外側になってしまう → Commonmarker で unsafe: true の場合に javascript: リンクがそのまま <a> として描画される。
ケース4: リンク URL 内の | でセルが分割される
テーブルセル内にあるリンク URL 自体が | を含む場合も同様にセルが分割され、リンクが壊れたり意図しない解釈になる問題がありました。
この PR の修正内容
主な修正ポイントは actiontext/lib/action_text/markdown_conversion.rb の to_markdown 周りでの変換ロジックです。(テスト: actiontext/test/unit/markdown_conversion_test.rb に 85 行追加)
テーブル行内のコードスパン内容から前後の空白を除去
テーブル行(
<tr>/<td>/<th>)を Markdown に変換する際、<code>内テキストの先頭・末尾の空白文字(スペース、タブ、VT、FF など)を削除してからコードスパンに包むよう変更。ケース1は次のように改善:
markdown| `[x](javascript:alert(1))` |これにより、kramdown 等に行を分割されても、先頭の空白によって backtick 解釈が壊れるリスクを避けられます。
隣接するコードスパンの間にスペースを挿入
Markdown テキスト生成時に、連続した
<code>が backtick レベルで隣接しないよう、コードスパン同士が「隣り合う」場合には間に 1 文字分のスペースを自動で差し込む処理を追加。ケース2は次のように改善:
markdown`` a`b` `` `[x](https://example.com)`これにより、CommonMark においても backtick 列が「一続き」と解釈されなくなり、それぞれ独立したコードスパンとして閉じ・開きが正しく認識されます。
副作用として、「エスケープされた backtick で終わるプレーンテキストの直後にコードスパンが来るケース」 でも、このロジックにより間にスペースが入る可能性がある(= 見た目が 1 文字分変わる)点が PR 説明で明記されています。
テーブル行内のコードスパン中の
|をエスケープGFM の仕様に従い、テーブル行内 (
<td>,<th>等) のコードスパン内に現れる|を\|に変換する処理を追加。ケース3は次のように改善:
markdown| `x\|[x](javascript:alert(1))` | y |これにより、GFM/ Commonmarker では
|がセル区切りと解釈されず、全体が 1 つのセルかつコードスパン内にとどまることになります。その結果[x](javascript:alert(1))はあくまで「コード文字列」として扱われ、リンクとしてレンダリングされません。ただし:
- Redcarpet は「エスケープされた
\|でもセル区切りとして扱う」という独自挙動があり、この PR ではそこまでワークアラウンドせず、GFM 準拠のエスケープだけ行う方針を取っています。 - もともとコード中に
\|が含まれている場合、この変換とパーサの扱いの関係で、Commonmarker / kramdown 側で「バックスラッシュが 1 つ増えたように見える」場合があり得る(GFM には「元のバックスラッシュを完全に保ったまま|をセル区切りと見なさない」ための逃げ道がない)と説明されています。
- Redcarpet は「エスケープされた
リンク URL 中の
|を%7Cにエンコード- HTML の
<a href="...">を Markdown にする際に、URL 内に|が含まれている場合はこれを%7Cへ URL エンコードしてから Markdown リンクとして書き出すように変更。 - これにより、テーブル内リンクであっても URL 内の
|によってセルが分割されることを避けます。
- HTML の
to_markdownのドキュメントに「unsafe link は best-effort で除去、HTML 側もサニタイズ推奨」を追記ActionText::Content#to_markdownActionText::RichText#to_markdown
それぞれに、以下のような注意書きが追加されています(要旨):
to_markdownはjavascript:などの unsafe link を「できる限り」落とすベストエフォートの実装である。- しかし変換結果の Markdown を最終的に再び HTML にレンダリングする場合は、その HTML に対してもサニタイズを行うべきであり、
to_markdownに完全なセキュリティの保証を期待すべきではない。
- 影響範囲・注意点
対象バージョン
to_markdown自体がまだ未リリースの機能であるため、「すでにリリース済みの Rails / Action Text でこの脆弱性の影響を受ける」ことはありません。- 今後
to_markdownを利用するバージョンにこの修正が入る形になります。
既存コードへの影響(互換性)
- 主な挙動の変化は:
- テーブル内
<code>前後の空白の除去 - コードスパン同士が隣接している場合に間にスペースが挿入される
- コードスパン内の
|が\|になる(テーブル行内) - URL 内の
|が%7Cになる
- テーブル内
- これらにより、「Markdown の見た目」や「その後の Markdown → HTML のレンダリング結果」が微妙に変わる可能性があります。
- 特にコードブロックを厳密に再現したいケースでは、前後の空白が落ちてしまう点に注意が必要です(ただし、今回の修正は主にテーブル行に限定されています)。
- backslash と pipe を多用しているコードをテーブル内で扱うようなケースでは、レンダラによってはバックスラッシュが増えたような見え方も起こり得ます。
- 主な挙動の変化は:
セキュリティ的注意
- この PR により、特定のパーサと特定の HTML 構造の組み合わせで、
javascript:等の危険なリンクが Markdown / HTML レンダリング結果に残ってしまうパターンが減ります。 - ただし PR 自身が明確に述べているように、「unsafe なリンクが完全に除去できる」という保証はしていません。
→to_markdown後に生成する HTML に対して、サーバサイド・クライアントサイドを問わず、必ず HTML サニタイズ(XSS 対策)を行う前提で使うべきです。
- この PR により、特定のパーサと特定の HTML 構造の組み合わせで、
利用している Markdown レンダラ依存の挙動
- GFM / Commonmarker を前提にした修正であり、Redcarpet など一部の実装とは完全には整合しません。
- 例: Redcarpet はエスケープされた
\|でもセル区切りにしてしまうため、ケース3は完全には解決しません。
- 例: Redcarpet はエスケープされた
- もし独自のレンダラやバージョン違いを使う場合は、想定どおり unsafe link が消えているか・コードスパンが壊れていないかを検証することを推奨します。
- GFM / Commonmarker を前提にした修正であり、Redcarpet など一部の実装とは完全には整合しません。
- 参考情報 (あれば)
対象コード:
actiontext/lib/action_text/markdown_conversion.rbactiontext/app/models/action_text/rich_text.rbactiontext/lib/action_text/content.rb
関連 PR:
- #58754, #58903
- 同じく
to_markdownにおけるコードスパン/リンク処理まわりの不具合修正で、この PR はそれらに続く追加のセキュリティ/互換性修正です。
- 同じく
- #58754, #58903
関連仕様・実装:
- GFM (GitHub Flavored Markdown) のテーブル仕様(セル区切りとしての
|、エスケープルール) - CommonMark / kramdown / Redcarpet / Commonmarker のコードスパンおよびテーブル解釈ルール
- GFM (GitHub Flavored Markdown) のテーブル仕様(セル区切りとしての
#58949 [ci skip] Fix broken RDoc markup reference links in API guidelines
マージ日: 2026/10/6 | 作成者: @estebansa19
- 概要 (1-2文で)
Rails のガイド「API Documentation Guidelines」に含まれていた RDoc のマークアップリファレンスへのリンクが 404 になっていたため、現在有効な URL に差し替えた修正です。コードには一切影響せず、ドキュメントのリンク切れのみを解消しています。
- 変更内容の詳細
対象ファイル:
guides/source/api_documentation_guidelines.md
背景:
- ガイド内「Links」セクションにある RDoc のマークアップリファレンスへの 2 つのリンクが、旧 URL
RDoc/MarkupReference.htmlを指しており、RDoc 側の構成変更により 404 になっていました。 - RDoc ドキュメント側でマークアップリファレンスは
doc/markup_reference/rdoc_rdoc.htmlに移動済みです。
この PR で行われたこと:
2 箇所のリンク先 URL を、以下のように更新:
「RDoc markup reference」系のリンク:
- 旧:
.../RDoc/MarkupReference.html - 新:
.../doc/markup_reference/rdoc_rdoc.html
- 旧:
「リンク記法」を説明する箇所のリンク:
- 旧:
.../RDoc/MarkupReference.html#label-Links - 新:
.../doc/markup_reference/rdoc_rdoc.html#label-Links
- 旧:
新しい URL にアクセスできること、および
#label-Linksアンカーが存在することを PR 作成者が確認済み。
変更差分イメージ(擬似的な例):
- See the RDoc markup reference: https://docs.ruby-lang.org/.../RDoc/MarkupReference.html
+ See the RDoc markup reference: https://docs.ruby-lang.org/.../doc/markup_reference/rdoc_rdoc.html
- See “Links” section: https://docs.ruby-lang.org/.../RDoc/MarkupReference.html#label-Links
+ See “Links” section: https://docs.ruby-lang.org/.../doc/markup_reference/rdoc_rdoc.html#label-Links※ 実際のベース URL は Ruby 公式ドキュメントサイトのものですが、PR の趣旨はこのようなパス変更です。
- 影響範囲・注意点
影響範囲:
- Rails ガイド(英語)の「API Documentation Guidelines」を閲覧する開発者向けのみの変更です。
- Rails 本体の動作・API・ビルド・テストには一切影響ありません。
- CI を不要と判断しているため、タイトルに
[ci skip]が付いています。
注意点:
- プロジェクト内や社内ドキュメントなどで、同じ旧 URL (
RDoc/MarkupReference.html等) を参照している場合、今回と同様にリンク切れになっている可能性が高いです。
→ 自分たちのドキュメントでも、RDoc マークアップへのリンクが古くなっていないか確認するとよいです。 - このリンクは過去にも 1 度切れており (#47282 で修正)、RDoc 側の URL 変更の影響を受けやすい箇所であることが分かります。将来の変更に備えて、RDoc の公式ドキュメント構造が変わった際には再度見直しが必要になるかもしれません。
- プロジェクト内や社内ドキュメントなどで、同じ旧 URL (
- 参考情報 (あれば)
- 本 PR: https://github.com/rails/rails/pull/58949
- 過去の類似修正: https://github.com/rails/rails/pull/47282
- RDoc 公式ドキュメント(トップ): https://ruby.github.io/rdoc/
- Markup Reference:
doc/markup_reference/rdoc_rdoc.html(RDoc 内のマークアップ記法のリファレンス)
- Markup Reference:
#58946 Build the insert values list once for raw on_duplicate upserts on MySQL
マージ日: 2026/10/6 | 作成者: @fabbyd
- 概要 (1-2文で)
MySQL 8.0.19以降で、upsert_all/upsertに「生の SQL フラグメント」をon_duplicate:オプションとして渡した場合に、INSERT のVALUES句が二重に組み立てられてしまう問題を解消し、Ruby 側の処理コストを大きく削減するパフォーマンス改善です。挙動や生成される SQL 自体は変わらず、同じバイト列を維持したまま内部の組み立て順序だけ最適化しています。
- 変更内容の詳細
何が問題だったか
対象:
- DB: MySQL 8.0.19 以降 (MariaDB および MySQL < 8.0.19 は非対象)
- メソッド:
upsert_all,upsert - 条件:
on_duplicate:にArel.sql(...)などの「生 SQL フラグメント」を渡した場合
例:rubyraw_update = Arel.sql("title = VALUES(title), pages = VALUES(pages)") Model.upsert_all(rows, on_duplicate: raw_update)
このケースで、AbstractMysqlAdapter#build_insert_sql が内部で以下のような動きをしていました:
まず「行エイリアス付き INSERT」形式を組み立てようとする
- 形式イメージ:sql
INSERT INTO `table` (...) VALUES (...), (...) AS `table_values` (...) - その際に
insert.values_listを呼び出し、「VALUES の全行・全カラム」を組み立てる。
- 形式イメージ:
しかし
on_duplicateが「生 SQL」の場合は、- 行エイリアスを使うと、
on_duplicate内のカラム参照が曖昧になる(エイリアスが前提になっていない) - そのため「行エイリアスなしの INSERT ... ON DUPLICATE KEY UPDATE ...」形式に切り替える必要がある。
- 行エイリアスを使うと、
そこで一度作ったエイリアス付きの SQL を破棄して、改めて「行エイリアスなし版」を組み立て直していた
- ここでも再度
insert.values_listを呼ぶため、- 各行 × 各カラムについて
- 型キャスト
- シリアライズ
- クオート
- SQL 文字列へのコンパイル
を「2回」行っていた。
- 各行 × 各カラムについて
- ここでも再度
結果として、行数 × カラム数に比例して Ruby 側のオーバーヘッドが倍増し、大きめのバッチでは upsert_all の実行時間の大きな部分を占めるようになっていました。
どう修正したか
修正方針は「生 on_duplicate のときは最初からエイリアス付きの経路に入らない」ようにすることです。
AbstractMysqlAdapter#build_insert_sql内で、- 先に「
on_duplicateが生 SQL かどうか」を判定 - 生 SQL の場合は、最初から「エイリアスなしの
INSERT ... VALUES ... ON DUPLICATE KEY UPDATE <raw>」の組み立てロジックを取る
- 先に「
- これにより:
insert.values_listの呼び出しは 1回だけ- 生成される SQL は従来と同じ(バイトレベルで同一)
- もともとこのケースでは行エイリアスを付けない形式だったため、ロジックの経路が変更されただけで出力は変わらない
コード的には abstract_mysql_adapter.rb の build_insert_sql の分岐ロジックが 6行追加・8行削除程度で調整されており、メモ化など新たな仕組みは導入していません。「呼び出し順序・条件分岐の整理」による最適化です。
ベンチマーク結果
条件:
- 1ステートメントあたり 1,000 行 × 7 カラム
- Trilogy アダプタ + MySQL 8.4.7
- 比較:
on_duplicate: raw_sql(今回影響を受けるパス)update_only:(生 SQL を使わないコントロール)
結果(要約):
main(変更前)on_duplicate: raw_sqlが約 30 i/supdate_only:が約 53 i/s- → 生
on_duplicateパスはコントロール比で約 1.8 倍遅い
本 PR ブランチ (変更後)
on_duplicate: raw_sqlが約 54 i/supdate_only:も約 54 i/s- → 2つはほぼ同じ速度になり、余分なオーバーヘッドが解消された
つまり、「生 on_duplicate を使うとやたら遅い」という現象が、MySQL 8.0.19+ においてはほぼ解消されます。
- 影響範囲・注意点
挙動・互換性
- 生成される SQL は PR 説明どおり「バイトレベルで完全に同一」であり、挙動の変更はありません。
- 特にカラム名の解決や
VALUES(...)関数の挙動など、DB 側の結果に影響する変更は行っていません。 - 既存テスト (
insert_all_test.rbのtest_upsert_all_updates_using_provided_sqltest_upsert_all_updates_using_values_function_on_duplicate_raw_sql) がそのまま通ることを前提としたリファクタリングです。
どの環境でメリットがあるか
対象:
- アダプタ: MySQL 系 (Trilogy / mysql2 等、
AbstractMysqlAdapterを使うもの) - サーバ: MySQL 8.0.19 以降
- メソッド:
upsert_all/upsert - オプション:
on_duplicate:にArel.sql(...)等の生 SQL を渡しているケース
- アダプタ: MySQL 系 (Trilogy / mysql2 等、
非対象/影響なし:
update_only:,unique_by:など、on_duplicate:に生 SQL を使わない通常の upsert- MariaDB
- MySQL 8.0.18 以前
実務上の注意点
- 破壊的変更はなく、アプリケーション側でコード変更は不要です。
- もし「MySQL 8.0.19+ で
on_duplicate: Arel.sql(...)を使ったupsert_allが異常に遅い」という体感があった場合、Rails をこの PR を含むバージョンに上げることで改善が期待できます。 - 大量行のバルク upsert(例: ログ集約やバッチ処理)で特に効果が大きいため、そのようなワークロードがある場合はパフォーマンス計測をすると改善を確認しやすいです。
- 参考情報 (あれば)
- 対象メソッド:
ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#build_insert_sql
- 関連テスト:
activerecord/test/cases/insert_all_test.rbtest_upsert_all_updates_using_provided_sqltest_upsert_all_updates_using_values_function_on_duplicate_raw_sql
- ベンチマークスクリプト: PR 本文の
<details>内に掲載upsert_allでon_duplicate: Arel.sql(...)とupdate_only:を比較benchmark-ipsを使用し、Trilogy + MySQL で複数回実行して統計を取得
#58943 Fix default_headers example in new_framework_defaults_8_2
マージ日: 2026/10/5 | 作成者: @excid3
- 概要 (1-2文で)
Rails 8.2 用のnew_framework_defaults_8_2.rb初期化ファイルテンプレート内で、default_headersの設定例がそのままではエラーになる不備を修正した PR です。トップレベルの initializer でも正しく動作するよう、configをRails.application.configに修正しています。
- 変更内容の詳細
問題になっていたのは、Rails が新規アプリ作成時などに生成するテンプレートconfig/initializers/new_framework_defaults_8_2.rb の中のサンプルコードです。
元の例(誤り):
# Rails アプリ生成時にコメントアウトされた状態で入っている想定のコード例
config.action_dispatch.default_headers = {
"X-Frame-Options" => "SAMEORIGIN",
# ...
}initializer は「トップレベル」で実行されるため、このスコープには config というローカル変数 / メソッドは定義されていません。そのため、コメントアウトを外して起動すると:
NameError: undefined local variable or method `config' for main:Objectというエラーになります。
この PR では、これを他のオプション記述と同様に Rails.application.config に揃えています。
修正後:
Rails.application.config.action_dispatch.default_headers = {
"X-Frame-Options" => "SAMEORIGIN",
# ...
}ポイント:
Rails.application.configを明示することで、initializer のトップレベルからでも確実に設定できる。- ファイル内の他の設定例(例:
Rails.application.config.active_record...など)と表記を統一。
変更はテンプレートファイル (*.rb.tt) の 1 行置き換えのみです。
- 影響範囲・注意点
- 対象は Rails 8.2 のアプリ生成テンプレート だけであり、まだ「未リリースのファイル」に対する修正です。
- すでに 8.2 の preview や edge を使ってアプリを生成しており、かつ
new_framework_defaults_8_2.rb内のdefault_headers設定例をそのままconfig...で書いている場合のみ影響します。
- すでに 8.2 の preview や edge を使ってアプリを生成しており、かつ
- 実際のアプリ側の挙動に関する仕様変更ではなく、「サンプルコードが NameError を起こさないようにする」だけの修正です。
- すでに自分のアプリで以下のように書いている場合は、特に変更不要です:ruby
Rails.application.config.action_dispatch.default_headers = { "X-Frame-Options" => "SAMEORIGIN" } - 逆に、もし以下のような書き方をしている場合は、ブート時 NameError を避けるために書き換えが推奨されます:ruby
# NG 例(initializer のトップレベルでは NameError) config.action_dispatch.default_headers = { ... }
- 参考情報 (あれば)
- 該当コミット:
https://github.com/rails/rails/commit/ed1157060f232ffea3c3ea004c0704f649e8a60e#diff-b9d74d9289754fd51e5760350a58209531406098b383e7cd2cdda9cfad6d2242R56 new_framework_defaults_8_2.rbは、Rails メジャーアップグレード時に新仕様を段階的に有効化するための initializer テンプレートで、生成直後は多くの設定がコメントアウトされた状態で含まれます。
#58847 Compose compatibility TableDefinition modules in Migration::Current
マージ日: 2026/10/5 | 作成者: @yahonda
- 概要 (1-2文で)
このPRは、マイグレーション互換クラス (V8_0, V7_0, V6_1, V6_0, V5_2, V5_0, V4_2) に重複していたcompatible_table_definition実装を、Migration::Currentに一箇所へ集約し、TableDefinitionモジュールの適用を動的に行うようにしたリファクタリングです。挙動は一切変えずに、DRYかつ保守しやすい構造に整理しています。
- 変更内容の詳細
これまでの構造
各互換バージョンのマイグレーションクラス (例: ActiveRecord::Migration::Compatibility::V7_0) には、create_table / change_table ブロック内で使われる t オブジェクトに対して、そのバージョン固有の TableDefinition モジュールを prepend するためのメソッドが定義されていました。
各クラスで、ほぼ同じコードが繰り返されていました:
# 例: V7_0, V6_1, ... などで同じような実装
def compatible_table_definition(t)
t.singleton_class.prepend(TableDefinition)
super
endこのメソッド自体はロジックを持たず、「super をたどりながら各バージョンの TableDefinition を順番に prepend する」ためだけに存在していました。
結果として、バージョンが増えるたびにこの同じメソッド定義をコピペする必要がありました。
変更後の構造
Migration::Current に compatible_table_definition の共通実装を移し、クラス階層を走査して TableDefinition をまとめて prepend するようにしました。
ポイント:
- 対象:
ActiveRecord::Migration::Current#compatible_table_definition - やっていること:
- 現在のマイグレーションオブジェクトのクラス階層 (
ancestors) をたどる - その中から「
TableDefinitionモジュールを定義しているクラス」を探す - 見つかった
TableDefinitionモジュールを、古いバージョンから順にt.singleton_class.prepend(...)する
- 現在のマイグレーションオブジェクトのクラス階層 (
- 個々の互換バージョンクラス (V8_0, V7_0, V6_1, V6_0, V5_2, V5_0, V4_2) からは、上記の重複メソッド実装を削除し、「
TableDefinitionモジュールを定義する」だけに簡略化
イメージ的には、これまで:
class V4_2
module TableDefinition; end
def compatible_table_definition(t)
t.singleton_class.prepend(TableDefinition)
super
end
end
class V5_0 < V4_2
module TableDefinition; end
def compatible_table_definition(t)
t.singleton_class.prepend(TableDefinition)
super
end
end
# ... V5_2, V6_0, ... と続くだったものが、今後は:
class V4_2
module TableDefinition; end
end
class V5_0 < V4_2
module TableDefinition; end
end
# ... 以降のバージョンも同様に module 宣言のみ
class Migration::Current
private
def compatible_table_definition(t)
# 先祖のクラスをたどり、TableDefinition を持つものを収集
table_definition_modules = self.class.ancestors.filter_map { |klass|
if klass.const_defined?(:TableDefinition, false)
klass.const_get(:TableDefinition)
end
}
# 古いバージョンから順に prepend(結果として新しいものが優先される)
table_definition_modules.reverse_each do |mod|
t.singleton_class.prepend(mod)
end
super
end
end※上記はあくまで擬似コードですが、PR説明にある通り「古いバージョンから走査して最後に新しいものが一番上に来る」順序になるように組まれています。
メソッド探索順の互換性
説明にもある通り、
- 「最も古い互換バージョン」→「その次」→…→「最新」の順に
TableDefinitionをprependしていき、 - Ruby の
prepend仕様により、最後に prepend されたモジュールが最も優先されるため、 - 結果として「最新バージョンの
TableDefinitionが一番手前にくる」= 旧実装のsuperチェーンと同じメソッド探索順
を維持しています。
PR内で明言されている通り、「マイグレーション互換性テストは変更なしで通っているため、挙動は変わっていない」ことが確認されています。
- 影響範囲・注意点
公開API/DSLの挙動は変わらない
create_table/change_tableのブロック内でのt.column,t.indexなどの呼び出し挙動や、各互換バージョンでのt.<something>の意味付けは従来通りです。- テストも変えていないため、「互換モードでのマイグレーション DSL 挙動」は影響なしと見なしてよいです。
影響を受けるのは「Rails 内部実装」および「それを前提にメタプロしているコード」
compatible_table_definitionを直接オーバーライドしていた独自コード (かなりレアケース) がある場合、挙動に影響が出る可能性があります。- クラス階層 (
ancestors) をたどってTableDefinitionを探すというロジックに依存する形になったため、独自のマイグレーションクラスを継承階層に挟んでいる場合などは、TableDefinition定義の場所/名前解決 (定数探索) に注意が必要です。- ただし、Rails 標準の継承構造を壊していなければ問題は出ない想定です。
新しい互換バージョンを追加する場合の利点
- これまで: 新バージョンを追加するたびに
compatible_table_definitionの同じ実装をコピペする必要があった - これから: 新Vx_y クラスに
module TableDefinitionを定義するだけで、共通ロジックが自動的にそれを拾ってprependしてくれる
→ バージョン追加時の手戻りやコピペミスのリスクが減ります。
- これまで: 新バージョンを追加するたびに
パフォーマンス面
- クラス階層をたどって数個のモジュールを
prependする程度なので、create_table/change_table実行時のオーバーヘッドはごく小さいと考えられます。 - 従来もバージョン数分の
prependを行っており、実質的なコストはあまり変わりません。
- クラス階層をたどって数個のモジュールを
- 参考情報 (あれば)
この重複実装が増えていった経緯:
- 214dc7d82d: V5_0, V4_2 に導入
- #34970: V5_2 追加
- #38717: V6_0 追加
- #41084: V6_1 追加
- #43904: V7_0 追加
- #55086: V8_0 追加
→ 各バージョンごとに「同じパターンでメソッドを足していった結果、7コピー溜まっていた」ものを今回整理。
実際に変更されたファイル:
activerecord/lib/active_record/migration.rb(+7行)Migration::Current#compatible_table_definitionの共通実装が追加された箇所。
activerecord/lib/active_record/migration/compatibility.rb(+4/-40)- 各互換バージョンクラスからの重複メソッド削除、およびモジュール宣言の簡素化など。
開発者視点では、「互換マイグレーションを追うときに compatible_table_definition がどこで何をしているか把握しやすくなった」「新バージョン追加時のやることが減った」という、内部実装のクリーンアップと捉えるとよいと思います。
#58846 Fix V5_1 change_column to honor table_name_prefix
マージ日: 2026/10/5 | 作成者: @yahonda
- 概要 (1-2文で)
Rails の ActiveRecord において、config.active_record.table_name_prefixを使っている環境で、ActiveRecord::Migration[5.1]以前のchange_columnが誤ったテーブル(プレフィックスなし)に対してDEFAULTなどを設定してしまう不具合を修正した PR です。これにより、古いバージョン互換モードのマイグレーションでも、テーブルプレフィックスが一貫して尊重されるようになります。
- 変更内容の詳細(あればサンプルコードも含めて)
問題の背景
config.active_record.table_name_prefix = "p_" のようにテーブル名プレフィックスを設定している場合、本来 :posts は実テーブル p_posts にマッピングされます。
以下のような 5.1 互換マイグレーションがあるとします:
class AddDefaultToPostsTitle < ActiveRecord::Migration[5.1]
def change
change_column :posts, :title, :string, default: "untitled"
end
end期待される SQL はどちらも p_posts を対象にするべきです:
ALTER TABLE "p_posts" ALTER COLUMN "title" TYPE character varying;
ALTER TABLE "p_posts" ALTER COLUMN "title" SET DEFAULT 'untitled';しかし、問題となっていた挙動では PostgreSQL に対し次のようなクエリが発行されていました:
ALTER TABLE "p_posts" ALTER COLUMN "title" TYPE character varying;
ALTER TABLE "posts" ALTER COLUMN "title" SET DEFAULT 'untitled';- 型変更 (
TYPE) はp_posts DEFAULTの設定はposts(プレフィックスなし)
posts テーブルが存在しない場合はマイグレーションが失敗し、たまたま存在する場合には意図しないテーブルが書き換えられてしまう、という危険なバグです。
なぜ起きていたか
Rails 5.1 互換モードでは、
change_column内で- 型変更と
:default,:null,:comment
を別々の SQL 文として発行するようになっている。
このうち後者(
defaultなどの適用)は、Migration#method_missingを経由せずに接続オブジェクト(connection)へ直接メソッドが呼ばれていた。テーブル名プレフィックスの適用 (
proper_table_name) はMigration#method_missing内で行われるため、直接 connection を叩いた部分だけプレフィックス処理が抜け落ちていた。
この PR の修正内容
5.1 互換モードにおける
change_columnが- 型変更
:default,:null,:comment
を分割して発行する際、「すべてのステートメントで
proper_table_nameを通したテーブル名」を使うように修正。その結果、上記サンプルの SQL は次のように統一されます:
ALTER TABLE "p_posts" ALTER COLUMN "title" TYPE character varying;
ALTER TABLE "p_posts" ALTER COLUMN "title" SET DEFAULT 'untitled';ロールバック時の挙動について
PR 説明にもある通り:
During a revert the prefix is not applied here, matching
method_missing: theCommandRecorderreplays the recorded command through the migration, and the prefix is applied at that point.
- ロールバック時は
CommandRecorderが記録済みコマンドを再生し、その経路でMigration#method_missingが通るので、そのタイミングでプレフィックス適用が行われる。 - この PR はその仕様に合わせており、「前方実行時に正しく
proper_table_nameを使う」変更にとどまっている。
変更されたコードの場所(概要)
activerecord/lib/active_record/migration/compatibility.rb- 5.1 互換レイヤーの
change_column周辺に 1 行追加(proper_table_nameを使うようにするための修正)。
- 5.1 互換レイヤーの
activerecord/test/cases/migration/compatibility_test.rbtable_name_prefix設定下で、5.1 マイグレーションが正しいテーブル名に対してDEFAULT等を発行することを確認するテストを追加。
activerecord/CHANGELOG.md- バグ修正としてエントリを追加。
- 影響範囲・注意点
対象となるのは主に以下の条件に当てはまるプロジェクトです:
config.active_record.table_name_prefixを使用しているActiveRecord::Migration[5.1]以前(5.1/5.0/4.x 互換)のマイグレーションをまだ実行する可能性がある- そのマイグレーションで
change_columnを使い、:default,:null,:commentのいずれかを指定している
該当する環境では、これまで
- マイグレーション失敗(
postsが存在しない場合) - 意図せぬテーブル更新(
postsが存在してしまう場合)
が起こり得たため、既存 DB に対する影響が心配な場合は、以下を確認するとよいです:
- 過去のスキーマ/マイグレーションを確認し、
table_name_prefix導入後にActiveRecord::Migration[5.1]でchange_columnを追加していないか - もし疑わしければ、
postsのようにプレフィックスなしのテーブルに不審な変更が入っていないか差分を確認
- マイグレーション失敗(
この PR 適用後は、上記のような不整合は解消され、新規マイグレーション・再実行ともに一貫してプレフィックス付きテーブルに対してのみ変更が行われます。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58846
- 依存 PR: https://github.com/rails/rails/pull/58845(スタックされた PR として言及)
table_name_prefixのドキュメント(Rails Guides・API)- CHANGELOG への追記は
activerecord/CHANGELOG.mdにあり、バージョンアップ時のリリースノートに含まれる想定です。
#58845 Honor an explicit type: on SQLite references in 6.0 migrations
マージ日: 2026/10/5 | 作成者: @yahonda
- 概要 (1-2文で)
SQLite を使っている環境で、ActiveRecord::Migration[6.0]のadd_referenceに明示的なtype:オプションを渡した場合に、その型指定(例:type: :bigint)が正しく反映されるようにする修正です。互換性レイヤで SQLite に対して常に:integerを強制していたコードを削除し、アダプタ本来の挙動を尊重するようにしています。
- 変更内容の詳細
これまでの問題点
Rails 6.0 形式のマイグレーションで、SQLite を使っている場合:
class AddUserToPosts < ActiveRecord::Migration[6.0]
def change
add_reference :posts, :user, type: :bigint
end
endのように type: :bigint を明示しても、実際には無視されて次のような SQL になっていました。
ALTER TABLE "posts" ADD "user_id" integer原因は、ActiveRecord::Migration::Compatibility::V6_0#add_reference の中に「SQLite のときは常に type: :integer をセットする」分岐が残っていたことです。この分岐が type オプションを上書きしていました。
今回の修正内容
Migration::Compatibility::V6_0#add_reference内の、SQLite のときにtype: :integerを強制する分岐を削除- 対象ファイル:
activerecord/lib/active_record/migration/compatibility.rb(-4 行)
- 対象ファイル:
- これにより、
add_referenceは 6.0 互換レイヤからアダプタの実装(SQLite3 アダプタ)に処理を委譲し、アダプタ側のデフォルトや指定されたtypeをそのまま尊重するようになります。 - テストを追加:
ActiveRecord::Migration[6.0]+ SQLite 環境でtype: :bigintを指定した場合に、その型が正しく反映されることを検証するテストを追加- 対象ファイル:
activerecord/test/cases/migration/compatibility_test.rb(+25 行)
- 対象ファイル:
- CHANGELOG 更新:
- SQLite + 6.0 互換マイグレーションでの
add_referenceの挙動が変更されたことをactiverecord/CHANGELOG.mdに追記(+25 行)
- SQLite + 6.0 互換マイグレーションでの
修正後の挙動
明示的に type: を指定した場合:
class AddUserToPosts < ActiveRecord::Migration[6.0]
def change
add_reference :posts, :user, type: :bigint
end
endALTER TABLE "posts" ADD "user_id" biginttype: を省略した場合の挙動は従来どおりで、SQLite3 アダプタが integer をデフォルトとして使います:
class AddUserToPosts < ActiveRecord::Migration[6.0]
def change
add_reference :posts, :user
end
endALTER TABLE "posts" ADD "user_id" integer表示上の副作用
マイグレーションの verbose 出力(ログ)において、これまで 6.0 互換レイヤが type: :integer を付与していたために
-- add_reference(:posts, :user, {:type=>:integer})のように見えていたケースが、今後はアダプタ側のデフォルトに任されるため、明示的な {type: :integer} が表示されなくなります。ただし、実際に作成されるカラム型は従来と同じ integer のままです。
- 影響範囲・注意点
- 対象:
- SQLite を使用しており、
ActiveRecord::Migration[6.0]形式でマイグレーションを書いていて、add_reference/t.referencesでtype:を明示しているプロジェクト。
- 型が変わる可能性があるケース:
- これまで「SQLite で
type: :bigintを渡してもintegerにされていた」マイグレーションを、今回の修正を含む Rails で再実行した場合、実際のカラム型がbigintになります。 - ただし PR コメントによると、元々の分岐は「6.0 → 6.1 で
schema.rbが bigint に変わってしまうバグ」を避けるためのもので、今はその用途には効いておらず、6.0 互換テストもすべてグリーンとのことなので、実務上この変更で壊れるケースは非常に限定的と考えられます。
- これまで「SQLite で
- 既存の
schema.rbとの不整合:- 古い Rails バージョンで生成された
schema.rbがinteger前提になっていて、同じマイグレーションを新しい Rails で実行し直すとbigintになる、という差分が出る可能性があります。 - とはいえ、SQLite かつ 6.0 互換マイグレーションを再実行する、という限定的状況なので、多くのアプリでは影響は軽微です。
- 古い Rails バージョンで生成された
- マイグレーションログの差異:
- 上記の通り、verbose ログに出る
{type: :integer}の有無だけが変わるケースがあります。ログの内容でテストしているような特殊なケースがある場合は注意が必要ですが、通常は問題になりません。
- 上記の通り、verbose ログに出る
- 参考情報 (あれば)
- 本 PR が依存している PR: #58844
- 元のバグ修正 PR / Issue:
- #43295: SQLite で 6.0 マイグレーションを 6.1 以降で再実行した際に
integer→bigintに変わってしまう問題への対処 - #43168: 上記問題の Issue
- #43295: SQLite で 6.0 マイグレーションを 6.1 以降で再実行した際に
- 挙動が変わるに至った経緯:
- #42350 にて
V6_0#add_referenceがsuperを呼ぶようになり、アダプタのデフォルト処理に戻ったにもかかわらず、SQLite に対するtype: :integer強制分岐だけが残ってしまっていた。 - その結果、「元々直したかったケース」にはもう効いていないのに、「明示した
type:を潰す」という副作用だけが残っていた。 - 今回の PR ではその不要な分岐を削除し、アダプタ本来の挙動(型指定を尊重する)に揃えています。
- #42350 にて
#58932 Remove orphaned Active Support dependencies test fixtures
マージ日: 2026/10/5 | 作成者: @dpep
- 概要 (1-2文で)
Active Support の dependencies 周りのテストフィクスチャ(8ファイル)が、どこからも参照されていない「孤立ファイル」になっていたため、まとめて削除した PR です。実行時の挙動や API には一切影響のない、テスト専用ファイルのクリーンアップです。
- 変更内容の詳細
削除対象のファイル
activesupport/test/dependencies/ 配下の以下 8 ファイルが削除されています。
check_warnings.rbconflict.rbcross_site_depender.rbmodule_folder/lib_class.rbraises_exception.rbrequires_nonexistent0.rbservice_one.rbservice_two.rb
行数ベースでは、合計 39 行の削除のみ(追加なし)です。
背景となるテスト構造の変化
- 以前は
activesupport/test/dependencies/以下のテスト用 Ruby ファイル群を直接読み込むテストが存在していました。 - しかし、以下の変更に伴い、そのテストは既に削除されています。
- 「classic autoloader helpers」の削除(コミット
af27a25f199) require_dependencyの非推奨化に対応する変更(コミット57a5577169c)
- 「classic autoloader helpers」の削除(コミット
57a5577169c では同じディレクトリ内の以下の 3 ファイルだけ削除されていましたが、残り 8 ファイルが取り残されていた状態でした。
mutual_one.rbmutual_two.rbrequires_nonexistent1.rb
また、現在の dependencies_test.rb は、tmpdir 配下に自前でフィクスチャファイルを生成しており、activesupport/test/dependencies/ 配下のファイルは参照していません。
参照確認
- リポジトリ全体を検索した結果、削除対象となった 8 ファイルのパスや、そこに定義されている定数はどこからも参照されていないことが確認されています。
- Active Support のテストスイートは、これらのファイルを削除した状態でも成功しています(ただし後述の CI のタイミングテストの揺らぎは発生)。
テストコード例そのものは PR に載っていませんが、これらは典型的な「autoload / require / 依存関係まわりの挙動検証用マイクロファイル」(例: 例外を投げる・存在しないファイルを require する・サービスクラスっぽい定義を置く 等)と見てよい構造です。
- 影響範囲・注意点
影響範囲
- 本番コード(
activesupport/lib/...など)には一切変更がないため、Rails / Active Support を利用するアプリケーションの挙動・API には影響しません。 - 変更対象は テストフィクスチャのみ であり、削除されたファイルは現行のテストコードからも参照されていません。
- Active Support のテストを実行している環境でも、通常は影響ありません。
注意点
- もしローカルの独自テストや社内ツールなどが、Rails 本体の
activesupport/test/dependencies/*.rbを直接 require していた というような特殊な運用をしていた場合、そのコードはファイル削除により壊れます。- Rails 本体のテストフィクスチャを外部から利用するのは非推奨のため、そのようなケースがある場合は、自分のリポジトリ側にフィクスチャをコピーして管理する形に修正すべきです。
- CI では
ContinuousIntegrationTest#test_parallel_group_timingで一度失敗が出ていますが、これは単なるタイミング系テストのフレークであり、この PR の変更とは無関係とされています(リトライ 3 回で通過)。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58932
dependencies_test.rb(現状のテストがどのようにフィクスチャを扱っているかの参照用):
https://github.com/rails/rails/blob/4088f9d2ef0/activesupport/test/dependencies_test.rb#L11-L18- 関連コミット:
- classic autoloader helpers 削除:
af27a25f199 require_dependency非推奨対応:57a5577169c
- classic autoloader helpers 削除:
- 孤立テストフィクスチャ検出に使われたツール: trekr
#58933 Remove unused Contact#pseudonyms from Active Model tests
マージ日: 2026/10/5 | 作成者: @dpep
- 概要 (1-2文で)
Active Model のテスト用モデルContactに定義されていた未使用メソッドpseudonymsを削除する PR です。テストコード内でのみ利用されていたメソッドであり、本体機能への振る舞い変更はありません。
- 変更内容の詳細(あればサンプルコードも含めて)
対象ファイル:
activemodel/test/models/contact.rb変更内容:
Contactモデルに定義されていた以下のメソッドが削除されました。rubydef pseudonyms ["Martin", "Martini"] endこのメソッドはもともと
@contact.to_xml(methods: :pseudonyms)のように、XML シリアライズ時に追加メソッドとして出力する用途で使われていましたが、XML シリアライザ関連コードが f7ebdb1ac5 のコミットで削除された際に呼び出し元がなくなっていました。レポジトリ全体を検索した結果、この
pseudonymsメソッドへの他の参照は存在しないことが確認されています。
- 影響範囲・注意点
影響範囲:
- Active Model のテストスイート内のテスト用モデルに限定された変更です。
- ライブラリ本体の API や機能に影響はありません。
- Active Model のテストスイートは
1229 runs, 0 failuresで問題なく通過しています。
注意点:
activemodel/test/models/contact.rbはあくまで Rails 本体のテスト用コードなので、通常のアプリケーションコードから直接参照することは想定されていません。- もし独自に Rails 本体リポジトリのテスト用モデルを参照している特殊な環境があれば、その中で
Contact#pseudonymsを呼んでいないかだけ確認が必要ですが、一般的な利用では影響はないと考えてよいです。
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58933
- 削除対象メソッドの旧定義箇所:
https://github.com/rails/rails/blob/4088f9d2ef/activemodel/test/models/contact.rb#L25-L27 - 削除のきっかけとなった XML シリアライズ関連の変更コミット:
f7ebdb1ac5 - 未使用コード検出に使われたツール: trekr
#58844 Add regression tests for adapter-specific migration compatibility
マージ日: 2026/10/5 | 作成者: @yahonda
- 概要 (1-2文で)
このPRは、Rails のバージョン付きマイグレーションが「当時のRails+DBアダプタで生成していたスキーマ」を継続して再現できているかを確認するためのリグレッションテストを追加するものです。コードの分岐ではなく「実際に生成されるスキーマ」を検証しており、挙動が壊れた場合に検知できるようにしています。
- 変更内容の詳細
全体像
- 変更ファイルは
activerecord/test/cases/migration/compatibility_test.rbのみ(+197行)。 ActiveRecord::Migration::Compatibilityが行っている「マイグレーションバージョン × アダプタごとの互換挙動」を、以下の観点でテストしています:- カラム型
- デフォルト値
- コメント
- テーブルオプション
- スキーマロード時の挙動
- verbose 出力時のテーブル名の扱い
すべて「テストだけ」であり、実際のランタイム挙動には変更を加えていません。
MySQL向けのテスト
1. Migration[5.1] の create_table と ENGINE=InnoDB
目的: 古いバージョンのマイグレーションが、当時のMySQL向けデフォルトである ENGINE=InnoDB を付与し続けることを保証する。
Migration[5.1]でcreate_tableを実行した場合に、生成されるテーブル定義にENGINE=InnoDBが含まれることをアサート。- かつ、マイグレーション側で明示的に
options:を指定している場合でも、そのオプションが失われずに残ることを検証。
イメージ的なサンプル:
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users, options: "ROW_FORMAT=DYNAMIC" do |t|
t.string :name
end
end
endこのマイグレーションから生成されるテーブルが、MySQL上で例えば
CREATE TABLE `users` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(255),
PRIMARY KEY (`id`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;のようになっていること(ENGINE=InnoDB が付く、かつ options も残る)をテストしています。
2. Migration[7.0] の change_column と collation の保持
目的: 明示的に指定した collation: が、Migration[7.0] で change_column を実行しても失われないことを保証。
例:
class ChangeUsersName < ActiveRecord::Migration[7.0]
def change
change_column :users, :name, :string, collation: "utf8mb4_bin"
end
endこの変更後も、users.name カラムの照合順序が utf8mb4_bin のままになっていることを、実際にスキーマ(カラム定義)を見て検証しています。
PostgreSQL向けのテスト
3. Migration[5.0] で create_table ... id: :uuid のデフォルト
目的: Rails 5.0 のマイグレーションで id: :uuid を使ったときに、PostgreSQL の uuid_generate_v4() がデフォルトとして使われる互換挙動を保証。
対象パターン:
Migration[5.0]でcreate_table :users, id: :uuidを定義- 何も default を指定しない場合 →
uuid_generate_v4()がデフォルト値として設定される
- 何も default を指定しない場合 →
- 明示的に
default:を指定した場合 → その指定が優先される Migration[5.1]では「現行のデフォルト挙動」を維持し、5.0 特有の振る舞いに変換しない
テストでは、id カラムの column.default_function や default を確認し、期待どおりの値になっているかを検証しています。
4. Migration[6.1] の add_column / change_column と :datetime → timestamp 変換
目的: PostgreSQL において、バージョン付きマイグレーションごとに :datetime の扱いが変わることを確認。
Migration[6.1]:add_column :events, :starts_at, :datetime- または
change_column :events, :starts_at, :datetime - 上記により、実DB上では
timestamp型のカラムが作成されることをテスト
Migration[7.0]:- 同様のコードでも
:datetimeがtimestampに強制変換されない(= 現行の挙動に従う)ことをテスト
- 同様のコードでも
これにより、6.1 互換レイヤが PostgreSQL 用に :datetime を timestamp にマッピングすること、7.0 以降ではその変換をしないことが保証されます。
5. Migration[5.1] の change_column と :comment
目的: PostgreSQL で change_column を用いてコメントを付与する(あるいは変更する)互換挙動が、Migration[5.1] でも期待どおり動作していることを確認。
例:
class CommentOnUsersName < ActiveRecord::Migration[5.1]
def change
change_column :users, :name, :string, comment: "User's full name"
end
endテストでは、pg_description 経由などで実テーブルのコメントを確認し、指定したコメントが反映されていることをチェックします。
6. ActiveRecord::Schema[6.1] のスキーマロード時互換
目的: スキーマを ActiveRecord::Schema[6.1] として定義・ロードした場合にも、6.1 マイグレーションと同じ互換ルールが適用されることを確認。
ポイント:
schema.rbやstructure.sqlからのスキーマロード時に、単に現行のロジックで解釈するのではなく、「Schema[6.1]」という宣言に基づき 6.1 互換の変換が行われることを保証。- たとえば上記の
:datetime→timestampルールなどが、スキーマロードでも適用されることをテストしていると考えられます。
Verbose 出力に関するテスト
目的: マイグレーションの verbose ログに出力されるテーブル名が、table_name_prefix 設定の有無にかかわらず、マイグレーションで書かれた名前そのまま になることを確認。
検証ポイント:
ActiveRecord::Base.table_name_prefix = "prefix_"のような設定があっても、create_table :usersrename_table :users, :customersremove_foreign_key :orders, :users
- といった操作の verbose ログ上のテーブル名は、
users/customers等、「マイグレーションに書かれた名前」で表示されること。
特に、rename_table の「新しいテーブル名」や remove_foreign_key の「参照先テーブル名」のような「二つ目以降のテーブル名」も、正しくプレフィックスなしで表示されることをテストしています。
- 影響範囲・注意点
- 実装コードに変更はなく、追加されたのはテストのみです。
- 影響範囲:
- Rails の互換レイヤ(
ActiveRecord::Migration::Compatibility)が今後リファクタリングされたり挙動が変わった場合に、MySQL / PostgreSQL それぞれで「過去バージョンのマイグレーションが期待どおりのスキーマを生成しているか」を即座に検知できるようになります。 - CI で MySQL / PostgreSQL 環境を使っている場合、これらのテストが走ることでアダプタ固有の互換性バグの検出力が上がります。
- Rails の互換レイヤ(
- 注意点:
- このPRを取り込んだあとに、アダプタごとの挙動を意図的に変えたい場合、これらのテストが落ちる可能性があります。その場合は「どのバージョンのマイグレーションまで互換性を維持するか」を明確にしたうえで、テストの期待値を更新する必要があります。
- DB側の拡張(PostgreSQL の
uuid-ossp拡張など)がテスト環境で有効でないと、uuid_generate_v4()周りのテストが落ちる可能性があります(Rails のテストスイートは通常その前提で構成されます)。
- 参考情報 (あれば)
ActiveRecord::Migration::Compatibilityのコード(アダプタ名による分岐ロジック)を読むと、このPRでカバーされた互換挙動がどのように実装されているかを確認できます。- 公式ガイド:
- Rails ガイド「Active Record マイグレーション」:
https://guides.rubyonrails.org/active_record_migrations.html - コントリビューションガイド(CHANGELOG/テスト方針など):
https://guides.rubyonrails.org/contributing_to_ruby_on_rails.html
- Rails ガイド「Active Record マイグレーション」:
- 過去バージョンマイグレーションを使っているアプリケーションでは、このPRのようなテスト観点を自前で採り入れることで、「Rails アップグレード後も古いマイグレーションの再実行で同じスキーマが得られるか」を確認しやすくなります。
#58781 Stop setting the public-read ACL on S3 uploads for services configured with public: true
マージ日: 2026/10/5 | 作成者: @beauraF
- 概要 (1-2文で)
Active Storage の S3 サービスが、public: true設定時に必ずpublic-readACL を付けてアップロードしていた挙動を制御できるようにし、ACL 無効("Bucket owner enforced")がデフォルトの S3 バケットでもエラーなく動作させられるようにした PR です。新しいフラグconfig.active_storage.s3_public_uploads_via_aclを導入し、デフォルトでは現状維持、新しいアプリ(load_defaults 8.2)では ACL なしでの公開を前提とするように切り替えています。
- 変更内容の詳細
背景問題
現在の AWS S3 の推奨設定は「Bucket owner enforced(ACL 無効)」であり、この設定ではオブジェクト ACL は一切受け付けられません。
一方で Rails の
ActiveStorage::Service::S3Serviceは、サービス設定がpublic: trueの場合、常に以下を実行していました:ruby@upload_options[:acl] = "public-read" if public?ACL 無効バケットに対して
x-amz-acl: public-readを付けて PUT すると、S3 側でAccessControlListNotSupportedエラーになり、Rails 側は回避手段を持っていませんでした。
追加された設定フラグ
ActiveStorage::Service::S3Service に新しいグローバル設定が導入されています:
# config/application.rb など
config.active_storage.s3_public_uploads_via_acl = true # デフォルトtrue(デフォルト)- これまで通り、
public: trueな S3 サービスではacl: "public-read"を付けてアップロードします。
- これまで通り、
falsepublic: trueであっても Active Storage 側では ACL を設定しません(x-amz-aclヘッダーを送らない)。- オブジェクトの公開は「バケットポリシー」を通じて行うことが期待されます。
この挙動変更は S3Service 内部で条件分岐されます(実際のコード上は既存1行の振る舞いを、この設定値をみて実行するかどうか切り替える形になっています)。
新規アプリのデフォルト変更(Rails 8.2)
config.load_defaults 8.2を使う新規アプリケーションでは、config/initializers/new_framework_defaults_8_2.rbに以下が生成されます:ruby# Active Storage が `public: true` の S3 サービスにアップロードするとき、 # ACL(public-read)で公開するかどうかを制御する。 # 8.2 以降の新規アプリでは、バケットポリシーで公開を制御するのを前提に false にする。 Rails.application.config.active_storage.s3_public_uploads_via_acl = false既存アプリはこのイニシャライザを自分で適用しない限り、
trueのままになるため、挙動は変わりません。
サービス個別で ACL を指定したい場合
バケット側がまだ ACL ベースの運用をしている場合、グローバル設定を false にしても、サービス定義で明示的に acl を指定すれば従来どおりの挙動を維持できます。
config/storage.yml の例:
amazon_public:
service: S3
bucket: my-public-bucket
public: true
upload:
acl: "public-read"- このように
upload.aclを明示すれば、config.active_storage.s3_public_uploads_via_acl = falseでもpublic-read付きでアップロードされます。 - 逆に「ACL 無効バケット向け」であれば、
uploadセクションでaclを指定せず、かつs3_public_uploads_via_acl = falseにしておくことで、ACL を一切送らなくなり、AccessControlListNotSupportedエラーを防げます。
ドキュメント・テスト・設定系の更新
activestorage/CHANGELOG.md- 新しい設定と、それに伴う挙動変更が記載されています。
guides/source/configuring.mdconfig.active_storage.s3_public_uploads_via_aclの説明と、バケットポリシーで公開を制御する推奨パターンがドキュメント化。
activestorage/test/service/s3_public_service_test.rbs3_public_uploads_via_aclが true / false の両パターンで、ACL の有無が期待通りになることを検証するテストが追加されています。
railties/lib/rails/application/configuration.rbconfig.active_storage.s3_public_uploads_via_aclの設定項目追加。
new_framework_defaults_8_2.rb.tt- 新規アプリ向けのデフォルト値(false)と説明コメントを追加。
- 影響範囲・注意点
既存アプリ(アップグレード時)
config.load_defaultsを 8.2 にしない、もしくはnew_framework_defaults_8_2.rbを適用しない限り:config.active_storage.s3_public_uploads_via_aclはtrueのまま。public: trueな S3 サービスは現状通りpublic-readACL を付与してアップロードするため、挙動変化はありません。
- ただし、すでに S3 バケット側を「Bucket owner enforced(ACL 無効)」にしている場合:
- 現状でも
AccessControlListNotSupportedが出ているはずで、この PR により解決手段として:config.active_storage.s3_public_uploads_via_acl = falseに変更する- かつ、バケットポリシーで公開設定を行う という選択肢が増えます。
- 現状でも
新規アプリ(Rails 8.2 デフォルト)
load_defaults 8.2を使うと、new_framework_defaults_8_2.rbでconfig.active_storage.s3_public_uploads_via_acl = falseとなります。- この場合:
public: trueな S3 サービスでも ACL は送られません。- オブジェクトをパブリックに見せたい場合、
- バケットポリシーで
s3:GetObjectをPrincipal: *に許可する、 - または CloudFront 経由など他の仕組みで公開する、 といった構成にしておく必要があります。
- バケットポリシーで
- もし「ACL ベースで public-read を使い続けたい」場合は:
- グローバルで
config.active_storage.s3_public_uploads_via_acl = trueに戻すか、 - サービス定義の
upload.acl: public-readを明示する必要があります。
- グローバルで
マイグレーションの指針
- S3 バケットを AWS 推奨の「Bucket owner enforced(ACL 無効)」に移行したい場合:
- Rails 側で
config.active_storage.s3_public_uploads_via_acl = falseに変更する。 - 既存の
storage.ymlからupload.aclの明示的指定を削除する(ACL 不要であれば)。 - バケットポリシーで公開ポリシーを設定し、
publicオブジェクトへの GET を許可する。 - 動作確認後に、S3 バケットのオブジェクト所有権を「Bucket owner enforced」に切り替える。
- Rails 側で
- 参考情報 (あれば)
- AWS 公式ドキュメント:
Controlling ownership of objects and disabling ACLs for your bucket
→ 「Bucket owner enforced」モードの説明。ACL 無効時に ACL ヘッダ付き PUT がエラーになる背景。 - Rails ガイド(該当 PR で更新済み):
Active Storage Overview – S3 service configuration
→public: trueとバケットポリシー/ACL の関係や、uploadセクションでのオプション指定方法が記載される想定です。
#58941 Restore 100ms between checkins in test_checkout_fairness
マージ日: 2026/10/5 | 作成者: @yahonda
- 概要 (1-2文で)
ActiveRecord::ConnectionAdapters::ConnectionPoolThreadTest#test_checkout_fairnessが CI 上でときどき失敗する問題に対処するため、接続の「チェックイン間のスリープ時間」を 10ms から 100ms に戻し、テストの安定性を優先する変更です。テストの実行時間はわずかに増えますが、フェアネス検証の信頼性が向上します。
- 変更内容の詳細
対象は activerecord/test/cases/connection_pool_test.rb の test_checkout_fairness テストのみで、スリープ時間を 0.01 秒 → 0.1 秒に戻しています。
イメージとしては次のような変更です(疑似コード):
# 変更前(高速化後だが不安定)
# sleep 0.01
# 変更後(元の値に戻す)
sleep 0.1テストの意図は「コネクションプールの checkout が、待っているスレッド間で公平(先に待っていたスレッドから順に取得できる)か」を確認することです。
問題になっていた失敗例:
- expected: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+ actual: [1, 3, 2, 4, 5, 6, 7, 8, 9, 10]このテストでは、各スレッドがコネクションを取得しては返却し、その順序を配列に記録して「1,2,3,...,10」のように整然とした順序を期待しています。しかし、チェックイン後にすぐ次のチェックインが走る(sleep が短い)と、スレッドスケジューラや CI の負荷によっては「起こされたスレッドが実際にコネクションを取得する前に次のスレッドが動き出す」ことがあり、順序の乱れが発生します。
PR では以下の点が明示されています:
test_checkout_fairness内の sleep は、「チェックイン後に起こされたスレッドが実際に接続を取得しきるまでの時間」を確保するために必要。- CI (Buildkite) のように負荷が高い環境では、10ms では十分でないケースがある。
- 元々 0.1 秒だったものを 0.01 秒に短縮してテストを高速化していたが、その変更の一部を巻き戻して安定性を優先する。
- 影響範囲・注意点
影響範囲
- 本番コードには一切影響なく、Active Record のテストコードのみが変更対象です。
- 該当テスト (
test_checkout_fairness) の実行時間が約 0.11 秒 → 約 1.01 秒程度に増加します(ローカル計測ベースで +0.9 秒)。 - CI 上の「テスト全体時間」に対してはごく軽微な増加にとどまる想定です。
注意点 / 開発者視点での含意
- スレッドスケジューリングやタイミングに依存するテスト(特にフェアネスや順序を検証するテスト)は、多少の時間的マージンを取らないと CI 環境でフレーク(非決定的な失敗)を起こしやすい、という典型例です。
- 類似のテストを追加・変更する場合も、「ローカルでは十分に見える sleep でも CI では不足する可能性がある」点を意識して設計する必要があります。
- テスト高速化のための sleep 短縮やタイミング依存ロジックの強化は、フレーク発生率とのトレードオフになるため、安定性を優先する指針がこの PR から読み取れます。
- 参考情報 (あれば)
対象テスト:
ActiveRecord::ConnectionAdapters::ConnectionPoolThreadTest#test_checkout_fairness
(activerecord/test/cases/connection_pool_test.rb)関連コミット:
- 元の 0.1 秒スリープを導入していたコミット:
02b23355637 - 0.1 秒 → 0.01 秒へ短縮してテスト高速化したコミット:
d0d74258547(今回の PR はこれを部分的に巻き戻し)
- 元の 0.1 秒スリープを導入していたコミット:
CI での失敗例ログ(Buildkite):
https://buildkite.com/rails/rails/builds/134031#01a0e1f7-4894-47e2-8610-ac3353e60979
#58923 Add maintenance_database option to the PostgreSQL adapter
マージ日: 2026/10/5 | 作成者: @yahonda
- 概要 (1-2文で)
Rails の PostgreSQL アダプタにmaintenance_databaseオプションが追加され、db:create,db:drop,db:purgeが接続に使うデータベースをpostgres以外に変更できるようになりました。これにより、DigitalOcean などpostgresデータベースが存在しない / 接続できないマネージド PostgreSQL でも Rails の DB タスクが正常に動作します。
- 変更内容の詳細
新オプション maintenance_database の追加
PostgreSQL アダプタに、メンテナンス用の接続先 DB を指定する maintenance_database オプションが導入されました。
- 対象タスク:
bin/rails db:createbin/rails db:dropbin/rails db:purge
- 従来:
これらのタスクは常にpostgresデータベースに接続してCREATE DATABASE/DROP DATABASEを実行していた。 - 変更後:
maintenance_databaseに指定された DB に接続してコマンドを実行する。
未指定時は従来通りpostgresを使用する(後方互換あり)。
設定例: database.yml
production:
adapter: postgresql
database: blog_production
maintenance_database: defaultdb設定例: DATABASE_URL
DATABASE_URL にクエリパラメータとして指定可能:
export DATABASE_URL="postgres://user:pass@host:25060/blog_production?maintenance_database=defaultdb"Rails 側では、この maintenance_database を「CREATE DATABASE / DROP DATABASE を実行するための接続先 DB 名」として扱います。
libpq には通常の dbname としてだけ渡され、maintenance_database というキー自体は渡されません。
エラーハンドリングの調整
maintenance_database が存在しない場合の挙動も調整されています。
背景:
PostgreSQLAdapter.new_clientは接続失敗時に以下のように例外を分けている:postgresへの接続失敗:ActiveRecord::ConnectionNotEstablished- それ以外の DB への接続失敗:
ActiveRecord::NoDatabaseError
ActiveRecord::Tasks::DatabaseTasks.dropは、NoDatabaseErrorを- 「削除対象のアプリケーション DB が存在しない」と解釈し、
"Database 'blog_production' does not exist"と表示して正常終了扱いにする。
問題になるケース:
maintenance_database: defaultdbのようにpostgres以外を指定し、それが存在しない場合- 本来は「メンテナンス DB に接続できない」=設定ミスとして扱うべき
- しかしそのままだと
NoDatabaseErrorが投げられ、db:dropが- 「アプリケーション DB が存在しない」と誤解して成功扱いになる
対策:
- PostgreSQL 用の DB タスク (
postgresql_database_tasks.rb) 側で
「メンテナンス DB 接続時のNoDatabaseErrorをConnectionNotEstablishedに変換」するラッパを追加 - これにより、メンテナンス DB に接続できない場合は
db:create/db:drop/db:purgeが失敗として扱われ、適切にエラーが見えるようになります。
new_client のシグネチャを変えず、かつ他のアダプタ (Mysql2, Trilogy, SQLite3) に影響を出さないために、タスク側で例外変換を行う設計になっています。
ドキュメント・テストの更新
activerecord/CHANGELOG.mdに新オプションの追加が記載。guides/source/configuring.mdにmaintenance_databaseの説明と使用例を追加。activerecord/test/cases/adapters/postgresql/postgresql_rake_test.rbにmaintenance_databaseを指定した場合のdb:create,db:drop,db:purgeの挙動- 存在しない
maintenance_databaseを指定した時のエラーハンドリング を検証するテストが追加。
- 影響範囲・注意点
デフォルト動作は変更なし
maintenance_database未指定時は、これまで通りpostgresに接続してタスクを実行する。- 既存アプリは設定を追加しない限り挙動は変わらない。
マネージド PostgreSQL サービスでの利用
- DigitalOcean のように
postgresデータベースが存在しない/接続を拒否する環境では、maintenance_databaseをdefaultdbなど有効な DB に設定する必要がある。 - PostgreSQL の仕様上、
CREATE DATABASE/DROP DATABASEは 「どれか既存の DB に接続して実行できればよい」ので、defaultdbのような運用用 DB を指定すればよい。
- DigitalOcean のように
メンテナンス DB の存在チェック
maintenance_databaseに指定した DB 自体が存在しない場合は、ConnectionNotEstablishedとしてエラーになる(アプリケーション DB がないとは解釈されない)。- これは設定ミスの検知として望ましいが、従来とは例外の種別が変わるので rescuing しているコードがある場合は注意。
template1へのフォールバックは行わない- PostgreSQL の
createdbコマンドは--maintenance-db未指定時にpostgres→template1とフォールバックするが、 DigitalOcean ではtemplate1への接続が拒否されるため、Rails ではこのフォールバックは採用していない。 - Rails の挙動は「指定された
maintenance_databaseか(未指定なら)postgresだけを使う」。
- PostgreSQL の
- 参考情報 (あれば)
- PR: https://github.com/rails/rails/pull/58923
- 関連 Issue:
- Fixes #55715
- Ref #48236
- PostgreSQL 公式ドキュメント:
CREATE DATABASE: https://www.postgresql.org/docs/10/manage-ag-createdb.htmlDROP DATABASE: https://www.postgresql.org/docs/10/sql-dropdatabase.htmlcreatedb --maintenance-dbオプション: https://www.postgresql.org/docs/10/app-createdb.html
運用上は、マネージドサービスの「メンテナンス DB」もしくは「デフォルト DB」とされているもの(例: DigitalOcean の defaultdb)を maintenance_database として指定するのが推奨される使い方となります。
#58934 Remove orphaned legacy adapter test fixtures
マージ日: 2026/10/5 | 作成者: @dpep
- 概要 (1-2文で)
Rails Active Record のテスト用に一時的に置かれていた「レガシーアダプタ登録 API」向けのダミーアダプタ(fake_legacy_adapter.rb/fake_misleading_legacy_adapter.rb)が、もはやどこからも読み込まれておらず不要になったため削除された PR です。実行時の挙動やテスト結果に影響はなく、純粋なテストフィクスチャの掃除です。
- 変更内容の詳細
背景となる挙動の変化
- Rails 7.2 で、旧来のアダプタ登録 API(「
active_record/connection_adapters/#{adapter_name}_adapterを require して#{adapter_name}_connectionメソッドの有無を確認する」という仕組み)が非推奨になりました。 - 過去のコミット
79fb41ddd9で、このレガシーな挙動をテストするために、以下 2 つのテスト用アダプタファイルが追加されました:activerecord/test/active_record/connection_adapters/fake_legacy_adapter.rbactiverecord/test/active_record/connection_adapters/fake_misleading_legacy_adapter.rb
- その後のコミット
37ba3536c8で、上記の「ファイルを require して*_connectionを探す」ロジック自体が削除されました。 - 現状では
ConnectionAdapters.resolve("fake_legacy")を呼び出すテスト(registration_test.rb)は残っているものの、その処理はこれらファイルを require せずに動作するため、ファイルは一切使われていません。
今回の削除内容
削除されたのは以下の 2 ファイルのみです:
activerecord/test/active_record/connection_adapters/fake_legacy_adapter.rbactiverecord/test/active_record/connection_adapters/fake_misleading_legacy_adapter.rb
両方ともテスト環境専用の「レガシーアダプタっぽく見せるためのフィクスチャ」でした。PR によると:
- リポジトリ全体を検索しても、この 2 ファイルやそこで定義されていた定数に対する
requireや参照は一切存在しない。 registration_test.rbはこれらのファイルがなくても通る。- Active Record の sqlite3 のテストスイートも問題なく通る。
サンプルコードとしての中身は削除済みで PR からは見えませんが、典型的には以下のような「アダプタ名に応じたクラス/メソッドを定義するだけのダミー」だったと想定されます:
# 例: fake_legacy_adapter.rb のイメージ
module ActiveRecord
module ConnectionAdapters
class FakeLegacyAdapter < AbstractAdapter
def initialize(connection, logger = nil, pool = nil)
super
end
# 旧API向けのフック用に、こういったメソッドが定義されていた
def self.fake_legacy_connection(config)
new(nil, nil, nil)
end
end
end
endこれらがなくても、現在のアダプタ登録・解決ロジックは動作し、テストも成功することが確認されています。
- 影響範囲・注意点
- 実行時挙動:
- 削除対象はテストディレクトリ以下のファイルのみであり、本体コード(ライブラリ/API)の挙動に一切影響はありません。
- テストへの影響:
- Rails 本体の Active Record テストにおいても、この 2 ファイルはすでに参照されていないため、テスト結果は変わりません。
- もし、社内フォークや独自パッチなどでこれらのファイルを直接 require していた場合は、当然ロードエラーになりますが、公式リポジトリ側ではそのような利用は想定されていません。
- レガシーアダプタサポートに関する誤解:
- この PR は「レガシーアダプタのサポートをさらに削った」わけではなく、「すでに削られている/変更されているロジックのためのテストフィクスチャを片付けただけ」です。
- したがって、7.2 でのレガシーアダプタ登録 API の非推奨方針自体に新たな変更があったわけではありません。
Rails アプリ開発者視点では、今回の変更を意識する必要はまずありません。Rails 本体にコントリビュートしている場合にのみ、「不要なテスト用ファイルが整理された」と認識しておけば十分です。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58934
- 言及されているテスト:
activerecord/test/cases/connection_adapters/registration_test.rb
- 関連コミット:
79fb41ddd9: レガシーアダプタ登録 API の非推奨対応時に、今回削除されたフィクスチャを追加したコミット37ba3536c8:ConnectionAdapters.resolveからレガシーなrequire+*_connection探索ロジックを削除したコミット
- PR 作者が使用したツール: trekr(未使用コードや参照されないファイルを発見するためのツール)
#58929 Clarify scope of record reloading
マージ日: 2026/10/3 | 作成者: @fxn
- 概要 (1-2文で)
ActiveRecord::Persistence#reloadが「何を」「どこまで」リロードするのかを明確にするため、ドキュメント文言の改善と、それに対応するテストが追加された PR です。
主に「属性値と AR が管理する内部状態はリセットされるが、開発者が独自に持たせたカスタム状態はそのまま残る」ことが明示されました。
- 変更内容の詳細
2-1. reload の挙動の明確化
これまで reload の説明は「レコードをDBから再読込する」といった粒度で、次の点が曖昧でした。
- 「再読込される」のが具体的に何なのか
- Active Record が管理する内部状態と、アプリ側が持たせたカスタムな状態の扱いの違い
PR での意図は以下です。
リロードされるもの
- データベースに保存されている 属性値 (attributes)
- それに整合するように、Active Record が内部で保持している AR 管理下の状態(変更トラッキング等)
リロードされないもの
- モデルインスタンスにぶら下げた カスタムなインスタンス変数・キャッシュ・外部サービス由来の状態 など
(例:@expensive_api_resultのようなもの)
- モデルインスタンスにぶら下げた カスタムなインスタンス変数・キャッシュ・外部サービス由来の状態 など
PR の説明文から要点を抽出すると:
reloadは 属性値をDBから読み直して上書きする メソッドである- それに応じて AR が内部的に管理している状態もリセットされる
- ただし、開発者が独自に追加した状態(インスタンス変数など)はそのまま残る
- これにより、たとえばテストで「DBだけリセットされたが、外部サービス結果のキャッシュが残っていた」というケースが起こり得るので、その挙動を明示した
2-2. ドキュメントの文言修正 (persistence.rb)
activerecord/lib/active_record/persistence.rb で、reload のコメント(APIドキュメント)が次のような方針で調整されています。
- 「レコードをリロードする」という抽象的な表現から、
→ 「属性値をDBの状態に合わせて読み直す」という具体的な表現へ - 「Active Record が制御するものは合わせてリセットされるが、それ以外(カスタム状態)は対象外」という境界を明示
- 内部実装の詳細(どの ivar・どのフラグがどう変わるか)はあえてぼかし、「AR が内部的に整合性を取る」という程度の説明に留める
※PR本文にもある通り、「何が AR 管理の状態で、何がカスタム状態なのか」をコードレベルで厳密に区別して自動的に処理するのは困難かつ後方互換性を壊す可能性があるため、挙動を変えるのではなく、あくまで「仕様として説明する」方向です。
2-3. テストの追加 (persistence_test.rb)
activerecord/test/cases/persistence_test.rb にテストが追加され、ドキュメントで説明された挙動が実際の仕様として固定されます。
概念的には、次のようなテストが入っていると考えられます(擬似コード):
def test_reload_resets_attributes_but_keeps_custom_state
user = User.create!(name: "Alice")
user.custom_cache = :cached_value # カスタムなインスタンス変数
user.name = "Changed locally"
user.reload
assert_equal "Alice", user.name # 属性値はDBの値に戻る
assert_equal :cached_value, user.custom_cache # カスタム状態は残る
endここでポイントになるのは:
- DB に保存されている
nameはreloadによって「正しい状態」に戻る - しかし
custom_cacheのような AR によって知られていないインスタンス変数はそのまま残る
という仕様をテストで保証しているという点です。
- 影響範囲・注意点
実行時の挙動は変更されていない
既存アプリケーションのreloadの挙動は従来通りで、コード上の互換性を壊す変更はありません。
変わったのは「説明とテスト」であり、現状の挙動を公式に仕様として固定した形です。テストやキャッシュ実装での落とし穴
このPRが明示している通り、次のようなケースでは注意が必要です。- モデルインスタンスに 外部サービスのレスポンスをキャッシュ している
- memoization(
@foo ||= heavy_calculation)で重い処理結果をインスタンス変数に保持している - テストで
reloadすれば「全部きれいになる」と思い込んでいる
これらは
reloadではリセットされないため、DBの状態とカスタムキャッシュの状態が食い違う 可能性があります。
もし「リロード時にカスタム状態も消したい」場合は、after_find/after_initialize/ 独自メソッドを使って明示的にリセットする必要があります。設計上の指針としての明文化
今回のドキュメント更新により、「ARが責任を持つ範囲」と「アプリケーションが責任を持つ範囲」がよりはっきりしました。
AR のreloadはあくまで「DB由来の属性と、それに付随するAR内部状態」を対象とし、
それ以外のインスタンス変数やオブジェクト状態のライフサイクル管理は、アプリケーション側の責任 であることが明確化されています。
- 参考情報 (あれば)
- 該当PR: https://github.com/rails/rails/pull/58929
- 関連ドキュメント(現行APIリファレンス):
- ActiveRecord::Persistence#reload
(PRマージ後は「属性値」「AR管理の内部状態」「カスタム状態」の扱いがより明確に記載される想定)
- ActiveRecord::Persistence#reload
このPRを踏まえると、「reload すればモデルインスタンスが“完全に初期状態に戻る”わけではない」点を意識して、
外部キャッシュ・memoization・一時的なインスタンス変数の扱いを設計・テストするのが良いです。
#58869 Add test for OOB/in_order_of/CTE
マージ日: 2026/10/2 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
このPRは、Active Record のin_order_ofと CTE (Common Table Expression) を組み合わせた場合に、Out-of-Band (OOB) ローディング周りの挙動が正しく動作することを確認するテストを追加するものです。機能追加や挙動変更はなく、既存機能の回 regressions 防止のためのテスト強化が目的です。
- 変更内容の詳細
追加されたテストファイル
- 変更ファイル:
activerecord/test/cases/relation/field_ordered_values_test.rb - 変更内容: +18行 (新しいテストケースの追加)
ファイル名から分かる通り、このテストは Active Record リレーションにおける「フィールドに基づく順序付け」の動作を検証するテスト群です。今回のPRではこの中に、以下のような条件を組み合わせたケースが追加されています:
in_order_ofを使った順序指定 (in_order_of(:id, [3, 1, 2])のようなもの)- CTE (
WITH ... AS (...) SELECT ...型のクエリ) を利用したリレーション - Out-of-Band (OOB) ローディング / Preloading が関わるケース
Ref #44746 および Ref #57975 から推測される文脈としては:
in_order_ofが導入/改良されたときの既知の課題・改善点- CTE を絡めた複雑なクエリと Active Record の order/in_order_of/プリロードの組み合わせ周りで発生しうるバグ
を回避するための regression テストとして追加されたものです。
想定されるテストのイメージ(擬似コード)
実際のコードは18行と短いテストで、おおよそ次のような構成が考えられます:
def test_in_order_of_with_cte_and_oob_loading
# CTE を使って一時的な結果セットを作成
posts = Post.with(
ranked_posts: Post.where(published: true)
).from("ranked_posts").in_order_of(:id, [3, 1, 2])
# OOB ローディング / preloading を絡めた取得
assert_equal [3, 1, 2], posts.map(&:id)
end※上記はイメージであり、実際のテストコードとは異なる可能性がありますが、「CTE + in_order_of + OOB ローディングを組み合わせた時に指定順序通りに返ってくるか」を確認するタイプのテストであると考えられます。
- 影響範囲・注意点
挙動変更なし
このPRはテストコードのみの追加であり、実行時のライブラリコードには一切変更がないため、Rails を利用するアプリケーションの動作には影響しません。回 regressions 防止
今後、in_order_of/ OOB / CTE まわりの実装変更を行った際に、過去に発生した・あるいは懸念される不具合が再発しないことを自動で検証できるようになります。OOB ローディング + CTE + in_order_of を使うアプリ
これらを組み合わせている、あるいは将来的に組み合わせたいアプリにとっては、コア側でテストが整備されていることで信頼性が若干高まります。
ただし、このPR単体では不具合修正ではない点に注意してください。もし過去に類似の問題があり、それが対応済みかどうかを確認したい場合は、参照されている Issue/PR (#44746, #57975) も合わせて確認する必要があります。
- 参考情報 (あれば)
参照PR / Issue:
- #44746
- #57975
関連機能:
ActiveRecord::Relation#in_order_of- CTE (
with/with_recursive等を利用したクエリ) - Out-of-Band ローディング・preloading 機構 (Active Record のロード戦略)
背景:
in_order_ofは「指定した ID/値の配列通りの順序で結果を返す」ためのヘルパーで、DBに依存したORDER BY FIELD(...)などを抽象化して利用しやすくする機能です。- CTE は複雑なサブクエリや一時的なビューを定義できる SQL 構文で、Rails でも
withなどで利用されます。 - この2つと OOB ローディングを組み合わせると、内部のクエリ構築が複雑になり、順序付けや結合の最適化の過程でバグが出やすいため、専用テストが追加されたと考えられます。
#58913 Freeze events emitted by ActiveSupport::EventReporter
マージ日: 2026/10/2 | 作成者: @keshavbiswa
- 概要 (1-2文で)
ActiveSupport::EventReporter がサブスクライバに渡すイベントデータ(ハッシュ)をfreezeすることで、サブスクライバ間での副作用や順序依存を防ぐ変更です。
これにより、イベントの内容を書き換えようとするサブスクライバはFrozenErrorとなり、通知されるイベントの整合性が保証されます。
- 変更内容の詳細
背景問題
ActiveSupport::EventReporter#notify は、1つのイベントハッシュをそのまま全サブスクライバに共有していました。
その結果:
- あるサブスクライバが
event[:payload][:foo] = :barのようにイベントを書き換えると、その後に実行されるサブスクライバは書き換え後のデータを受け取ってしまう - パラメータフィルタリング等で削除された値を、後続のサブスクライバが「元に戻す」ことも理論上可能
- 結果として、「どのデータが見えるか」が購読順序に依存してしまう
という問題がありました。
具体的な変更点
このPRでは、notify 内で作られるイベントのトップレベル構造をすべて freeze するように変更しています。
対象:
- イベントハッシュ自体
:payloadハッシュ:source(source location を表すオブジェクト)- (元々)
tagsとcontextはすでにfreeze済み
方針:
- Reporter 内で「自分で構築したコピー」に対してのみ
freezeを行う - 呼び出し元が渡した
payloadハッシュや、notifyに直接渡されたイベントオブジェクトは freeze しない- つまり、呼び出し元のデータ構造の所有権を奪わない
イメージコード(擬似的):
def notify(name, payload: {}, **options)
# 呼び出し元の payload はそのまま(freezeしない)
event_payload = payload.dup.freeze
event_source = build_source_location(...).freeze
event = {
name: name,
payload: event_payload,
source: event_source,
tags: tags.freeze, # もともと freeze されている
context: context.freeze, # もともと freeze されている
}.freeze
subscribers.each do |subscriber|
subscriber.call(event) # ここから event は読み取り専用
end
endエラー挙動
サブスクライバ側で、例えば:
rubyActiveSupport::EventReporter.subscribe("request") do |event| event[:payload][:user_id] = 123 # または event[:payload][:foo].upcase! endのように「イベントオブジェクトをインプレース変更」しようとすると
FrozenErrorが発生します。ActiveSupport::EventReporterは既存のエラーハンドリングポリシーに従い:- デフォルトでは、そのエラーを内部で報告(ログ等)しつつ処理継続
raise_on_errorオプションが有効な場合は、そのまま例外を再送出
Rails 本体に含まれる標準のサブスクライバはイベントを書き換えない設計なので、この変更によって Rails 内部で FrozenError が出ることは想定されていません。
テスト・ドキュメント
activesupport/test/event_reporter_test.rb- イベントが freeze されること
- 変更しようとすると
FrozenErrorになること - deep freeze されていない(ネストしたオブジェクトはそのまま)こと
などを確認するテストが追加。
activesupport/CHANGELOG.md- 振る舞いの変更として、イベントが freeze されることが明記されています。
- 影響範囲・注意点
影響範囲
- ActiveSupport::EventReporter を使ってイベント購読を行っているすべてのコード
- 特に、ブロック/サブスクライバ内でイベントハッシュや payload を直接書き換えている場合
注意点
イベントオブジェクトは読み取り専用とみなす必要がある
rubyreporter.subscribe("foo") do |event| # NG: 直接書き換え event[:payload][:foo] = "bar" # FrozenError # OK: 読み取るだけ process(event[:payload][:foo]) endfreeze は浅い
payload自体のハッシュは freeze されますが、payload 内の値(オブジェクト)は freeze されません。例えば:
rubyreporter.notify("foo", payload: { data: [] }) reporter.subscribe("foo") do |event| event[:payload][:data] << 1 # これは可能(配列自体は freeze されていない) end「サブスクライバに渡す前に、完全に不変にしたい」場合は、呼び出し元の側で深いコピーや deep freeze を行う必要があります。
既存コードの互換性
- これまで「たまたま動いていた」イベントの書き換えコードは破壊的変更になります。
- カスタムインストルメンテーションやモニタリングライブラリで:
- イベントを加工して後続サブスクライバに渡す
- イベントをそのまま保持して後で書き換える
- といったパターンを使っている場合は、設計の見直しが必要です。
例: イベントを複製してから加工する:
rubyreporter.subscribe("foo") do |event| event_copy = event.deep_dup event_copy[:payload][:foo] = "bar" # 以降は event_copy を使う end
パフォーマンス
- 各サブスクライバごとにコピーを作らず、1イベントにつき1つのコピー+shallow freeze のみ。
- EventReporter は比較的ホットパスになり得るため、deep freeze や subscriber ごとのコピーは避けていることが明示されています。
- 参考情報 (あれば)
- 対応Issue: #58912
「サブスクライバ間でイベントハッシュが共有されることによる副作用」の報告・議論に対する修正。 - 変更対象:
activesupport/lib/active_support/event_reporter.rbactivesupport/test/event_reporter_test.rbactivesupport/CHANGELOG.md
- 設計方針:
- イベントの「観測結果」を安定させる(購読順序に依存させない)
- 呼び出し元のデータ所有権は尊重する(元の payload などは凍結しない)
- Hot path であることを考慮し、deep freeze・subscriber ごとのコピーは行わない
#58680 Cache ActiveSupport::ParameterFilter instances across requests
マージ日: 2026/10/2 | 作成者: @eregon
- 概要 (1-2文で)
各リクエストごとに生成されていたActiveSupport::ParameterFilter(内部で複数のRegexpを生成)が、同じフィルタ一覧であればグローバルに再利用されるようになり、ログ用パラメータフィルタの構築コストが削減されました。これにより特にフィルタ数が多いアプリケーションやconfig.precompile_filter_parametersを無効にしている環境で、リクエストあたりのオーバーヘッドが有意に減少します。
- 変更内容の詳細
何をしているか
ActionDispatch::Requestが使うActiveSupport::ParameterFilterインスタンスを、- 「リクエスト単位のメモ化」から
- 「プロセス全体で共有されるキャッシュ」
に変更しています。
キャッシュは
Concurrent::Mapで実装されており、フィルタの配列(Array)の内容をキーにしてParameterFilterインスタンスを記憶します。- 同じ配列オブジェクトでなくても、
Array#==がtrueになる(中身が同じ)配列であれば同じキーとして扱われます。 env_filterのように毎回新しい配列を組み立てるコードでも、内容が同じならキャッシュヒットします。
- 同じ配列オブジェクトでなくても、
ParameterFilterは生成後に不変(イミュータブル)とみなせるため、複数リクエスト・複数スレッドでの共有が安全と判断されています。
キャッシュの上限と挙動
キャッシュは最大 100 エントリに制限されています。
- 101 個目以降が追加されそうになったら、キャッシュ全体をクリアします(単純な全クリア戦略)。
- 理由: フィルタリストを動的に大量生成するアプリケーションで、無制限にキャッシュが増え続けるのを防ぐため。
典型的なアプリケーションでは、フィルタ定義は数パターンに収まることが多いため、キャッシュミスはあまり発生しない想定です。
パフォーマンス背景
従来:
- 各リクエストで
ActiveSupport::ParameterFilter.new(filters)が呼ばれ、 - その中で
compile_filters!→Regexp.new,Regexp.escape,Regexp#to_sなどが実行されていました。 - 正規表現の生成はパース・バイトコード生成などがあり、比較的重い処理です。
- 各リクエストで
この PR 後:
- 同じ
filters配列であれば、最初の1回だけコンパイルされ、その後はキャッシュ済みのParameterFilterを再利用します。 - Ruby 4.0.6 / actionpack 8.1.1 の計測例では、1 回の
ParameterFilter構築が- キャッシュなし: ~5–6μs
- キャッシュヒット: ~0.9μs
に短縮されています。
- 同じ
ベンチマーク(railsbench, 2000 req/iteration, 中央値ベース):
config.precompile_filter_parameters = true(7.1 以降のデフォルト):- no JIT: 約 0.5% 高速化
- YJIT: 約 0.65% 高速化
config.precompile_filter_parameters = false(生のフィルタリストを毎回コンパイル):- YJIT: 約 7.9% 高速化
→ キャッシュにより「非 precompile 設定でも、precompile ありと同等の速度」になると説明されています。
- YJIT: 約 7.9% 高速化
- 影響範囲・注意点
影響範囲
影響する処理:
- ログ出力時のパラメータフィルタリング(
filter_parameters,config.filter_parametersを通じて設定されるもの) ActionDispatch::Request#filtered_parametersや関連メソッド内で利用されるActiveSupport::ParameterFilter
- ログ出力時のパラメータフィルタリング(
期待される効果:
- ほとんどのアプリケーションでリクエストごとのオーバーヘッドが減少。
- とくに:
- フィルタ数が多い(正規表現を多用している)アプリケーション
config.precompile_filter_parameters = falseな環境
で効果が出やすい。
互換性と安全性
ParameterFilterを共有するようになったが、インスタンスは不変前提で設計されているため、スレッドセーフ。- キャッシュキーは「フィルタ配列の内容」なので、同じフィルタ定義なら挙動は完全に従来どおりです。
- 例:
[:password, :token, /credit_card/]のような配列を再利用していれば、そのままキャッシュできます。
- 例:
- フィルタを都度動的に変える(例: リクエストごとにランダムなフィルタを足すなど)とキャッシュミスが多くなり、さらに 100 エントリ超でキャッシュがクリアされますが、
- 「キャッシュがなかった以前の状態」に戻るだけなので、機能的な後退やバグにはつながらない(最悪でも性能上の恩恵が減るだけ)。
開発者視点の注意点
- フィルタ定義をできるだけ同じ配列内容で再利用することで、キャッシュの恩恵を最大化できます。
- 例:
- 良い:
MY_FILTERS = [:password, :token]; ParameterFilter.new(MY_FILTERS) - 微妙: 毎回
[:password, :token] + extra_filtersを新規に作る(内容が変われば当然キャッシュミスが増える)
- 良い:
- 例:
- フィルタを「リクエストスコープで変更・拡張する」ような高度な使い方をしている場合:
- 引き続き動作はしますが、キャッシュ頻度が下がり、性能向上は限定的かもしれません。
- 参考情報 (あれば)
過去の類似機能:
- 2010年:
ParameterFilterのコンパイル結果をキャッシュする機能が一度追加されていた(コミット55ae903c3fd2)。 - その後、スレッドセーフ化(
45448a578877)を経て、2012年に「不要なキャッシュを削除」(fa3457dc3b30)というコミットで撤去されていたが、理由は明示されていなかった。 - 今回の PR はこれをより明示的な設計とテストのもとで復活させた形。
- 2010年:
プロファイル・計測の詳細:
- 使用環境: Ruby 4.0.6, actionpack 8.1.1
- ruby-bench の railsbench による測定(2000リクエスト/iteration, warmed-up 中央値, JIT の有無など)
→ PR 説明で具体的な数値が多数挙げられているため、性能検証の信頼度は比較的高いと考えられます。
関連 PR:
Regexp#to_sコスト削減に関する PR: https://github.com/rails/rails/pull/58678
(今回の変更と合わせて、フィルタ構築での正規表現関連コストをさらに削減する文脈にあります)
#58289 Roll back when a before_commit callback raises a rollback error
マージ日: 2026/10/2 | 作成者: @adam-h
- 概要 (1–2文で)
before_commitコールバック内でTransactionRollbackErrorが発生した場合でも、トランザクションを正しくロールバックするようにwithin_new_transactionの挙動が修正されました。これにより、トランザクションが開いたまま接続プールに戻ってしまう不具合が解消されます。
- 変更内容の詳細
問題の背景
ActiveRecord::ConnectionAdapters::TransactionManager#within_new_transaction は、おおまかに次のような流れで処理をしています:
- トランザクションを開始
- ブロックを実行(モデルの保存やコールバック実行など)
before_commitコールバック群 (before_commit_records) を実行- DB に対して
COMMITを投げる (transaction.commit) - 例外が起きた場合は
ROLLBACKする … というのが本来の期待挙動
ところが既存コードでは、
within_new_transactionの中でTransactionRollbackErrorを rescue して「ROLLBACK せずに終了する」特別扱いが入っていた- この rescue 範囲に
before_commit_records(COMMIT実行前)が含まれていた
ために、
before_commitコールバック内でTransactionRollbackErrorが発生すると- 「
COMMIT失敗時を想定した特別な rescue」と同じ扱いになってしまう - まだ
COMMIT自体は発行されていないにもかかわらず、ROLLBACKをスキップ - 結果として「トランザクションが開いたまま」そのコネクションがプールに戻る
- 「
という不整合な状態が起きていました。
本PRでの修正方針
修正のポイントは次の2つです。
- 「
COMMIT後のTransactionRollbackErrorだけ特別扱いする」ように rescue の位置を移動 within_new_transactionレベルのTransactionRollbackError専用 rescue を廃止し、before_commit中の例外は通常のロールバック処理に通す
具体的には、activerecord/lib/active_record/connection_adapters/abstract/transaction.rb のトランザクション管理ロジックが変更されています。
元の構造(概略):
def within_new_transaction
transaction = begin_transaction(...)
yield
add_to_commit_records
before_commit_records
begin
transaction.commit
rescue TransactionRollbackError
# COMMIT 失敗時は ROLLBACK をスキップする特別扱い
end
rescue Exception => e
transaction.rollback
raise
ensure
# cleanup ...
end新しい構造のイメージ:
def within_new_transaction
transaction = begin_transaction(...)
yield
add_to_commit_records
before_commit_records
# ★ invalidation(= COMMIT 後に ROLLBACK をスキップする処理)を
# transaction.commit のすぐ隣に移動
transaction.commit
rescue TransactionRollbackError => e
# ここは「COMMIT から上がってきたもの」だけを扱うようになる
# → COMMIT が失敗した場合は、トランザクションは DB 側ですでに終了しており、
# 余計な ROLLBACK を投げない
# (#56717 と同じ振る舞いを維持)
rescue Exception => e
# before_commit を含む「COMMIT 前」に発生した例外はここに落ちる
# → これまで通り ROLLBACK を実行する
transaction.rollback
raise
ensure
# cleanup ...
endポイント:
COMMITが「実際に送られた後」のTransactionRollbackErrorだけを特別扱いするようにした
→ これは、たとえば PostgreSQL でシリアライゼーションエラーなどによりCOMMITが失敗したケース(#56717)を想定した挙動で、DB 側ではすでにトランザクションが終わっているため、ROLLBACKを投げないのが正しい。- それより前(
before_commit含む)の例外は「通常のエラー」として扱われ、rescue Exception側でROLLBACKされるように戻した
→before_commit内でTransactionRollbackErrorを投げても、きちんとロールバックされる。
テスト追加内容
activerecord/test/cases/adapters/postgresql/transaction_test.rb にテストが追加されています(+22行)。
テストではおそらく次のようなシナリオが検証されています(概略):
- PostgreSQL アダプタ使用時
- モデルに
before_commitコールバックを定義し、その中でraise ActiveRecord::TransactionRollbackErrorを発生させる - そのモデルを
transactionブロック内で保存し、before_commitで例外を投げる - 期待結果
- トランザクションはロールバックされていること
- コネクションがトランザクション中のままプールに戻らないこと(後続クエリが通常通り動く)
これにより、今回のバグが再現していたケースが再度発生しないことを保証しています。
- 影響範囲・注意点
影響範囲
- 対象:
before_commit/after_commitなどのコミット直前・直後のコールバックを利用しているコードTransactionRollbackErrorをアプリケーションレベルで明示的に投げているロジック- PostgreSQL など、
COMMIT時にシリアライゼーションエラーなどでTransactionRollbackErrorが発生し得るDB
実質的な動作変更
変わる点
before_commit内でTransactionRollbackErrorが発生した場合に、明示的に ROLLBACK が実行されるようになる(以前はスキップされていたバグを修正)- これにより、トランザクションが開いたままプールに戻ることがなくなり、後続処理で「なぜか常にトランザクション内扱いになる」などの不可解な不具合が減る
変わらない点
- 実際に
COMMITが投げられた後に DB から返るTransactionRollbackError(例: シリアライゼーションエラー)は、これまで通り ROLLBACK を投げない
→ #56717 で導入された挙動は維持される
- 実際に
注意点
- もし既存コードが「
before_commitでTransactionRollbackErrorを投げてもトランザクションが継続している」という誤った前提に依存していた場合、その前提は崩れます。- とはいえ、この前提に依存するのはほぼバグに近い使い方なので、修正の影響は本来望ましい方向です。
- DB セッション状態を直接いじるような高度な実装(例えば生 SQL でトランザクション境界を操作しているなど)をしている場合は、今回の変更で「予期せぬ ROLLBACK が追加された」と誤解しないように、
before_commitとCOMMITの境界を意識して挙動を確認してください。
- 参考情報 (あれば)
- 該当PR:
- Roll back when a before_commit callback raises a rollback error (#58289)
- 関連Issue:
- #58288 —
before_commitコールバック内でのTransactionRollbackErrorにより、トランザクションがロールバックされずに接続プールに戻ってしまう問題
- #58288 —
- 関連する過去のPR:
- #56717 —
COMMIT自体がシリアライゼーションエラーなどで失敗した場合に、不要な ROLLBACK を回避する挙動を導入したPR(今回もこの挙動は維持されている)
- #56717 —
アプリケーション側で before_commit コールバックや TransactionRollbackError を積極的に使っている場合は、テスト環境でトランザクションの挙動(特にネストしたトランザクションやエラー発生時の状態遷移)を一度確認しておくと安全です。
#58813 Include extra node state in Arel equality
マージ日: 2026/10/2 | 作成者: @e-akashsaini
- 概要 (1–2文で)
Arel の一部ノード (Matches,DoesNotMatch,Regexp,NotRegexp,InfixOperation,UnaryOperation) について、SQL 出力に影響する追加状態(escape 文字や大文字小文字区別フラグ、演算子)をeql?/hash判定に含めるようにし、異なる条件がwhereのマージやor結合時に誤って同一視されて消えてしまう問題を修正する PR です。
これにより、Relation#or/merge/WhereClauseの重複排除が、より直感的かつ安全な挙動になります(Active Record が自前で生成する通常のクエリは影響なし)。
- 変更内容の詳細
問題の背景
Arel の多くのノードクラスは Arel::Nodes::Binary / Unary を継承しており、デフォルトの eql? / hash は「クラス+オペランド」だけを比較対象にします。
しかし、以下のクラスは「クラス+オペランド以外の状態」が SQL に影響するにもかかわらず、その状態が eql? / hash に含まれていませんでした:
| ノード | 無視されていた状態 | 影響する SQL |
|---|---|---|
Matches, DoesNotMatch | escape, case_sensitive | ESCAPE 句、LIKE vs ILIKE (PostgreSQL) |
Regexp, NotRegexp | case_sensitive | ~ vs ~* (PostgreSQL) |
InfixOperation | operator | >, <, ->>, @> など任意の中置演算子 |
UnaryOperation | operator | 任意の prefix 演算子 |
その結果、異なる SQL を生成する 2 つのノードが eql? / hash 上は同一と見なされ、以下のような現象が起きます。
ActiveRecord::Relation::WhereClauseは内部的にArray#-やArray#|を使って述語を重複排除しているため、一方の条件が捨てられるRelation#orはself - otherが空だと「同じ条件」とみなして idempotent ショートカットを使い、Orノードを作らずに左辺の Relation をそのまま返す
具体例:
title = Post.arel_table[:title]
a = Post.where(title.matches("100%", "!"))
b = Post.where(title.matches("100%"))
a.or(b).to_sql
# 修正前:
# SELECT "posts".* FROM "posts" WHERE "posts"."title" LIKE '100%' ESCAPE '!'
# 修正後:
# SELECT "posts".* FROM "posts" WHERE
# ("posts"."title" LIKE '100%' ESCAPE '!' OR "posts"."title" LIKE '100%')
a.merge(b).to_sql
# 修正前:
# ... WHERE "posts"."title" LIKE '100%' ESCAPE '!'
# 修正後:
# ... WHERE "posts"."title" LIKE '100%' ESCAPE '!' AND "posts"."title" LIKE '100%'修正前は、escape の違いを持つ Matches が同一視されており、or も merge も新しい条件が追加されませんでした。
同じ問題は:
InfixOperation.new(:">", ...)とInfixOperation.new(:"<", ...)- PostgreSQL における
LIKE(case_sensitive) とILIKE(case_insensitive) - 正規表現
~/~*(Regexp/NotRegexpのcase_sensitiveの違い)
などでも発生し得ます。どれも文法としては正しいが、「ユーザーが意図したよりも絞り込まれたクエリ」になるというバグです。
Rails が内部で生成するクエリでは、これらのノードに「異なる追加状態」を持たせるケースはなく、影響は基本的に Arel の API (matches, matches_regexp, InfixOperation など) を直接使っているコードに限られます。
具体的なコード変更
対象クラスごとに、追加状態を考慮した hash / eql? / == を定義しています。パターンは既存の Extract, NamedFunction, NamedWindow と同じです。
Matches / DoesNotMatch
def hash
super ^ [@escape, @case_sensitive].hash
end
def eql?(other)
super &&
self.escape == other.escape &&
self.case_sensitive == other.case_sensitive
end
alias :== :eql?superはBinary/Unaryのhash/eql?を呼ぶ- つまり「クラス+左オペランド+右オペランド」の比較は今まで通り維持
- その上で
escapeとcase_sensitiveも比較対象に追加 alias :== :eql?を各クラスでやり直しているのは、親クラス側のaliasが親の実装にバインドされてしまうため
Regexp / NotRegexp
def hash
super ^ [@case_sensitive].hash
end
def eql?(other)
super &&
self.case_sensitive == other.case_sensitive
end
alias :== :eql?- PostgreSQL の
~/~*を切り替えるcase_sensitiveを考慮
InfixOperation
def hash
super ^ [@operator].hash
end
def eql?(other)
super &&
self.operator == other.operator
end
alias :== :eql?>と<、->>と@>など「演算子の違い」を equality に含める
UnaryOperation
def hash
super ^ [@operator].hash
end
def eql?(other)
super &&
self.operator == other.operator
end
alias :== :eql?- prefix 演算子についても同様
既存クラスの微修正
Extract, NamedFunction, Window の hash 実装で super を使うような微調整も入っていますが、機能的にはこれまでと同じ意図を保ったまま実装スタイルを揃える程度です(+1/-1)。
テスト
新規・拡張テストで変更をカバー:
activerecord/test/cases/arel/nodes/matches_test.rb(新規)Matches/Regexpそれぞれについて:escapeが異なるとeql?/hashが異なるcase_sensitiveが異なると異なる- すべてのインスタンス変数が同じなら同一とみなされる(コントロール)
infix_operation_test.rb/unary_operation_test.rb- 既存の「オペランド差による不等」テストに加え、「
operatorが異なる場合も不等」となるケースを追加
- 既存の「オペランド差による不等」テストに加え、「
relation/or_test.rb- 実際に
Relation#orでMatchesの escape 違いが OR 条件として残ることを確認する回帰テスト
- 実際に
これら 6 テストは、この PR のライブラリ変更がないと失敗します。
- 影響範囲・注意点
- 影響するのは Arel ノードの equality (
eql?/==/hash) を利用しているコード ですWhereClause内部の重複排除Relation#orのショートカットActiveSupport::Cacheなどで「Arel ノードを Hash のキーや Set の要素にしている」場合
- 特に影響が出るケース:
Arel::Predications#matches,matches_regexpescapeやcase_sensitiveを変えた 2 つの predicate が、もはや「同一」扱いされません
Arel::Nodes::InfixOperation,UnaryOperationを直接使っている場合operatorが異なると必ず不等になります
これは「バグ修正」であり、**正しい表現としては「今まで誤って同一と扱われていたものが、きちんと区別されるようになる」**なので、多くの場合は望ましい挙動のはずです。
注意点:
- もし既存アプリケーションが「
InfixOperation/Matchesなどを、operator/escape/case_sensitive無視で同一視している」ことに依存していた場合、その前提は崩れます- たとえば「
>と<を同一視して SET で一意化する」ような実装があった場合、今後は 2 要素として扱われる
- たとえば「
- equality は
where/mergeのホットパスなので、Binary/Unaryで一括して「instance_variables反映による汎用 eq 実装」を導入する案は却下され、ピンポイント修正に留めています- パフォーマンスや意味的な変更リスク(例:
Arel::Table#hashは alias をあえて無視している)を避けるため
- パフォーマンスや意味的な変更リスク(例:
Active Record が自前で組み立てる通常のクエリは、そもそもこれらの「追加状態の差」を持つノードを同じカラムに対して同時に使っていないため、この PR によって既存の Rails 生成 SQL が変わることはありません。
- 参考情報 (あれば)
- バグは Arel が Active Record に統合された初期 (
17ca17072d) から存在し、Rails 6.0.0 以降のすべてのリリースに潜在していました。 - 修正対象は「ノードの equality 判定」のみであり、アダプタ(SQLite / MySQL / PostgreSQL)依存のロジックには触れていません。
- ローカルでは
ARCONN=sqlite3で Arel 関連テストおよび Relation 関連テスト一式、rake test:sqlite3がすべて成功したことが記載されています。MySQL / PostgreSQL は CI 前提ですが、変更内容的にアダプタ非依存です。
#58918 Remove dead code for Ruby < 3.3.5
マージ日: 2026/10/2 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
Ruby 3.3.5 未満向けのポリフィル(互換コード)を Active Record のコネクションプール/クエリキャッシュ周りから削除し、Ruby 3.3.5 以降を前提にした実装へ整理した PR です。これにより、Thread/Fiber 以外のキーを内部マップに使えるようになり、クエリキャッシュのクリア処理などで今後のパフォーマンス改善がしやすくなります。
- 変更内容の詳細
全体像
- 対象:
activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rbactiverecord/lib/active_record/connection_adapters/abstract/query_cache.rb
- 目的:
- Ruby 3.3.5 より前のバージョンを考慮したポリフィル(古い Ruby 向けの互換実装)を削除
- Ruby 本体が提供する新しい挙動を前提として内部実装を単純化
- Map(内部的なハッシュやストレージ)で Thread/Fiber 以外のオブジェクトをキーに使えるようにする足場を整備
connection_pool.rb の変更点
- 主な変更は「スレッド/ファイバーごとに接続情報を管理するための内部マップ」の扱いから、古い Ruby 向けの迂回実装を削除したことです。
- これまで:
- Ruby < 3.3.5 の制約や不具合を回避するために、
- Thread や Fiber をキーにした WeakMap/Map 相当のポリフィル
- あるいは GC されにくい/キーのライフサイクルに制約がある実装 を使っていた可能性があります。
- Ruby < 3.3.5 の制約や不具合を回避するために、
- これから:
- Ruby 3.3.5 で導入された/安定した Map 系機能(例えば WeakMap 相当の機能や、内部 C 実装のマップ)を前提に、ポリフィル側のコードを削除。
- その結果、マップのキーとして Thread/Fiber に限定されず、任意のオブジェクト(例えば ConnectionHandler など)を使えるようになります。
ざっくりとしたイメージ(実際のコードは異なりますが、構造の変化イメージ):
Before(旧 Ruby サポートのためのポリフィルあり)
# Ruby < 3.3.5 でも動くようなポリフィル
class LegacyMap
def initialize
@hash = {}
end
def [](key)
@hash[key.object_id]
end
def []=(key, value)
@hash[key.object_id] = value
end
end
# どこかで Ruby バージョンを見て Map 実装を切り替え
MAP_CLASS = if RUBY_VERSION < "3.3.5"
LegacyMap
else
RubyNativeMap # 例: Ruby が提供する新しい実装
end
@connections = MAP_CLASS.newAfter(Ruby 3.3.5 前提、ポリフィル削除)
# 常に Ruby 本体の新しい Map 実装を使用
@connections = RubyNativeMap.newこれにより、@connections のキーとして Thread/Fiber 以外も安全に使える前提が整います。
query_cache.rb の変更点
- こちらは差分 1 行程度の単純な修正で、上記の Map 利用方法の変更に追随したものと思われます。
- 例えば、クエリキャッシュをスレッドローカルや Fiber ローカルに紐づけていた箇所を、より汎用的なキー(たとえばコネクションプールインスタンスや別の ID)で管理できるようにするための準備的変更です。
- 結果として、クエリキャッシュの「どのキーで」「どの単位で」キャッシュを持つか、という戦略を柔軟に変更できるようになります。
- 影響範囲・注意点
影響範囲
- 対象コンポーネント:
- Active Record のコネクションプール (
ConnectionPool) - クエリキャッシュ (
QueryCache)
- Active Record のコネクションプール (
- 実運用上の主な影響:
- この PR 自体は主に「内部実装の整理」と「将来の最適化のための前準備」です。
- 現時点では API の挙動変更よりも、内部でどのようなキーでマップを管理しているか、というレベルの変更に留まります。
- ただし、内部のマップ実装が変わることで:
- クエリキャッシュのクリア処理(
clear_query_cache,clear_active_connections!など)の実装がシンプルかつ高パフォーマンスに書き直しやすくなります。 - Thread/Fiber 境界に強く依存していたキャッシュ管理から、「任意のコンテキスト単位」でキャッシュを持つ戦略にシフトしやすくなります。
- クエリキャッシュのクリア処理(
注意点 / マイグレーション観点
- この PR は Ruby 3.3.5 以降を前提にポリフィルを削除しているため、
- Rails のこのバージョン以降を使う場合、Ruby も 3.3.5 以上が事実上の前提になる可能性が高いです。
- Ruby 3.3.4 以下で Rails を動かす構成は、将来的にサポート外・非推奨になる/もしくは既になっていると考えた方が安全です。
- Active Record の内部クラス(
ConnectionPool,QueryCache)をモンキーパッチしている場合:- 内部の Map 実装に依存したコード(たとえば「Thread を直接キーにする前提で動くパッチ」など)は動かなくなる可能性があります。
Thread.currentベースのキャッシュ管理に深く依存した独自実装がある場合は、該当箇所を再確認した方がよいです。
- 参考情報 (あれば)
- PR 本体:
Remove dead code for Ruby < 3.3.5(#58918) - 関連 PR/Issue:
Ref #58908- ここで Ruby 3.3.5 へのバージョン引き上げと、その背景(新しい Map 機能の利用など)が説明されていると考えられます。
- 説明文中の参照コミット:
08bf480a070c1619272989c52d3c9fd17c95ea3a- Ruby 3.3.5 へのバンプ内容や、Map の仕様変更に関する詳細がコミットメッセージに含まれている可能性があります。
#58919 Don't duplicate integration tests' fixture paths
マージ日: 2026/10/2 | 作成者: @earlofurl
- 概要 (1-2文で)
ActionDispatch::IntegrationTestのfixture_pathsが二重に登録されてしまう問題を修正し、同じフィクスチャを何度もロードし直してしまう非効率さを解消する PR です。これにより、特にフィクスチャ数が多いアプリケーションで、テスト全体の実行時間が大きく短縮されます。
- 変更内容の詳細
問題の背景
rails/test_help を require した Rails アプリで、以下のような状態になっていました。
ActiveSupport::TestCase.fixture_paths
# => ["/app/test/fixtures/"]
ActionDispatch::IntegrationTest.fixture_paths
# 本来は上記と同じ配列を継承していれば十分だが…
# => ["/app/test/fixtures/", "/app/test/fixtures/"] # 同じパスが2回入る原因は以下の2点です。
ActionDispatch::IntegrationTestはActiveSupport::TestCaseを継承しており、fixture_pathsも継承される- さらに
action_dispatch_integration_testの load hook で、親クラスのfixture_pathsをもう一度IntegrationTest.fixture_pathsに追加していた
この結果、fixture_paths に同じパスが2回入った状態になっていました。
さらに、Rails のフィクスチャは「fixture_paths を含んだキー」でキャッシュされます。そのため:
- 通常のテスト (
ActiveSupport::TestCase派生) が使うキー - 統合テスト (
ActionDispatch::IntegrationTest) が使うキー
が異なる (後者は fixture_paths が重複した配列になる) ため、同じフィクスチャであってもキャッシュを共有できず、テストタイプをまたぐたびにフィクスチャを再ロードしていました。
加えて、コミット 17cb852d72 により、「キャッシュキーが見つからない場合はフィクスチャキャッシュをリセットして再挿入する」という挙動になったため、テストファイルを順番に走らせると、
- 通常テスト → 統合テスト → 通常テスト → … と切り替わるたびに、
- すべてのフィクスチャが毎回入れ直される
という性能上のペナルティが顕在化していました(8.1 では未適用だったため影響なし)。
今回の修正
railties/lib/rails/test_help.rb の、ActionDispatch::IntegrationTest に対する load hook 内で、fixture_paths をそのまま連結するのではなく「重複しないように追加する」ように変更しています。
イメージとしては以下のような修正です(擬似コード):
# 変更前(イメージ)
ActionDispatch::IntegrationTest.fixture_paths += ActiveSupport::TestCase.fixture_paths
# 変更後(イメージ)
ActionDispatch::IntegrationTest.fixture_paths |= ActiveSupport::TestCase.fixture_paths
# または .uniq を通す等、重複を排除※実際には #58454 (08d4e20ce3) で ActiveSupport::TestCase 向けに行われたのと同じ uniq ベースのアプローチと揃えています。
あわせて、railties/test/application/integration_test_case_test.rb にテストを追加し、
IntegrationTest.fixture_pathsが親クラスと同じパスを二重に持たないこと
を検証しています。この新しいテストは、この修正を入れないと失敗するようになっています。
- 影響範囲・注意点
影響範囲
rails/test_helpを読み込んでいるアプリケーションのActionDispatch::IntegrationTestを使った統合テスト- それ以外の
ActiveSupport::TestCaseベースのテスト
- 特に、フィクスチャファイル数が多く、かつ「統合テストと通常テストが混在している」プロジェクトで、フィクスチャロードにかかる時間が大幅に削減されます。
PR 内での測定結果:
- 対象: フィクスチャファイル 111 個、テストファイル 20 個(うち 10 個が統合テスト)、テストケース 738
- 8.1.3 ベースに
17cb852d72の挙動をパッチして再現 - Before:
- フィクスチャロード回数: 77 回
- フィクスチャロード時間: 28.0 秒
- 総テスト時間: 127 秒
- After:
- フィクスチャロード回数: 1 回
- フィクスチャロード時間: 0.8 秒
- 総テスト時間: 108 秒
小規模なアプリ(main 固定)でも、フィクスチャロード回数が 5 回 → 1 回になったと報告されています。
互換性・注意点
fixture_paths自体の意味や仕様は変わっていません。「同じパスが二重に入らなくなった」だけなので、通常のユースケースでの挙動変化はほぼありません。- もしアプリやライブラリ側で「
fixture_pathsの重複を前提にした特殊な処理」をしている場合は影響しうるものの、そのようなコードはまず存在しないと考えられます。 - PR 中でも言及されていますが、実は「IntegrationTest 用フックをやめて継承に完全に任せる」という選択肢もありえました。ただし、
ActiveSupport::TestCaseと同様のuniqで整える形にして、挙動を揃えています。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58919
- 関連コミット:
17cb852d72— フィクスチャキャッシュのミス時にキャッシュをリセットする変更08d4e20ce3/ PR #58454 —ActiveSupport::TestCaseのfixture_paths重複を解消した変更
- 修正ファイル:
railties/lib/rails/test_help.rbrailties/test/application/integration_test_case_test.rb
実運用上は、Rails をこの PR を含むバージョンに上げるだけで、統合テストと通常テスト間でフィクスチャキャッシュが共有されるようになり、大規模なフィクスチャを使うプロジェクトではテスト時間短縮が期待できます。
#58917 Remove dead code for Ruby < 3.3.1
マージ日: 2026/10/1 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
Rubyのサポート下限バージョンが 3.3.1 になったことを前提に、Time計算まわりで「Ruby 3.3.1 未満向けのワークアラウンド(死んだコード)」を削除したPRです。テストは 3.3.1 以降の動作確認として有用なため、そのまま維持されています。
- 変更内容の詳細
対象ファイル:
activesupport/lib/active_support/core_ext/time/calculations.rb(7行削除、追加なし)
内容としては、以前 Ruby 3.3.0 までの不具合や仕様差を吸収するために入っていた条件分岐・ワークアラウンドが削除されています。
背景となる流れ:
- 過去PR(例: #52347, #57028)で Ruby 3.3 系の挙動差への対応が入っていた。
- 2024年3月のコミット(
a9ed6377...)で Rails 自体の「サポートする Ruby の最小バージョン」が 3.3.1 に引き上げられた。 - その結果、「Ruby < 3.3.1 でのみ必要な回避コード」は実行されることがなくなり、「死んだコード (dead code)」になっていたため削除。
コードイメージ(あくまで概念的な例):
# 以前(イメージ)
if RUBY_VERSION < "3.3.1"
# 古い Ruby のバグ/挙動差を補正するコード
def some_time_calculation(...)
# workaround
end
else
# Ruby 3.3.1+ 向けの本来の実装
def some_time_calculation(...)
# normal path
end
end
# 今回のPR後
def some_time_calculation(...)
# Ruby 3.3.1+ を前提にした実装のみ
end実際の変更は 7 行削除のみで、API 仕様やメソッド名が変わっているわけではなく、「条件分岐・古いバージョン向けの特別な処理」がなくなっただけです。
また、このワークアラウンド追加時に入れられたテストは、「Ruby 3.3.1 以降で期待する動作が正しく行われているか」を保証する意味があるため、今回も残されています。
- 影響範囲・注意点
対象バージョン:
- Rails が公式にサポートする Ruby の最小バージョンが 3.3.1 のため、それより古い Ruby を使っているプロジェクトは、そもそもサポート対象外です。
- したがって「正式サポート範囲内での互換性破壊」はありません。
影響範囲:
ActiveSupportのTime/DateTime周辺計算(time/calculations)が関係しますが、実質的には「今まで 3.3.1+ で実行されていたコードが、そのまま唯一のコードパスになる」だけです。- Ruby 3.3.1 以降で Rails を使っている限り、挙動変更はなく、テストもすでにその前提で書かれています。
注意点:
- 古い Ruby(3.3.0 以前)上で「無理やり」この Rails バージョンを動かしている場合:
- 以前はワークアラウンドでたまたま動いていた箇所が、今回の削除により
Time計算まわりでバグったり、微妙に違う挙動になる可能性があります。 - ただし Rails コアとしてはそのケースはサポートしない前提です。Ruby を 3.3.1 以上に上げるのが正しい対応になります。
- 以前はワークアラウンドでたまたま動いていた箇所が、今回の削除により
- 古い Ruby(3.3.0 以前)上で「無理やり」この Rails バージョンを動かしている場合:
- 参考情報 (あれば)
- 本PR:
- 関連PR:
- Ruby 3.3 系対応の過去 PR: https://github.com/rails/rails/pull/52347
- 追加の修正 PR: https://github.com/rails/rails/pull/57028
- Rails が Ruby の最小バージョンを 3.3.1 に引き上げたコミット:
#58908 Bump minimum Ruby version to 3.3.5
マージ日: 2026/10/1 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
Rails がサポートする最低 Ruby バージョンを 3.3.5 に引き上げる PR です。
これにより、Weak{Key,}Map の use-after-free バグを前提にした独自ポリフィルを廃止でき、今後 Active Record のクエリキャッシュ周りをより効率的にリファクタリングできるようにする意図があります。
- 変更内容の詳細
2-1. どんなバグが背景にあるか
- Ruby 3.3.5 未満の WeakMap / WeakKeyMap 実装に use-after-free バグがある
- このバグは「比較的遭遇しやすい」とされており、安全に WeakKeyMap を使えない
- そのため Rails 側では、WeakKeyMap をそのまま使うのではなく「ポリフィル (代替実装+ラッパ)」を用意して、利用可能なキー種別を制限していました。
制限の具体例:
- WeakKeyMap のキーにできるのは
Thread/Fiberなど、respond_to?(:alive?)なオブジェクトだけ - つまり、任意オブジェクトを弱参照キーに使うことができず、設計の自由度が落ちていた
2-2. 最低 Ruby バージョンを 3.3.5 に上げる目的
PR 作成者のアプリケーションでは、クエリキャッシュのクリア処理が CPU を食っているという問題があります。
現在の実装イメージ:
- 各 DB 接続プールが、クエリキャッシュを「現在の実行コンテキスト (Thread/Fiber)」ごとに持っている
- 「このコンテキストに紐づくキャッシュをクリアしたい」ときに、
- 全ての connection pool を走査し、その pool 内の「現在の実行コンテキスト用キャッシュ」を消す
- → 利用していないプールも含めて毎回全プールを舐めるため、CPU コストが高い
理想の方向性:
- データ構造を「コンテキスト → そのコンテキストで使った connection pool の集合」に反転したい
- いま:
pool -> { context -> cache } - 目標:
context -> { pool -> cache }
- いま:
- こうすれば、「現在のコンテキストに紐づくプールだけ」を辿ればよく、総プール数ではなく、そのコンテキストで実際に使ったプール数だけを走査すればよい
→ パフォーマンスが大幅に改善する見込み
この「構造の反転」には、connection pool オブジェクトを WeakKeyMap のキーとして安全に使えることが前提になります。しかし、現状のポリフィルは「Thread / Fiber など alive? を持つものしかキーにできない」ため、connection pool をキーにできません。
そこで:
- Ruby 側の WeakKeyMap 実装バグが修正されている 3.3.5 以降を必須にする
- そうすれば、Rails 側の WeakKeyMap ポリフィルを削除し、任意オブジェクト (例: connection pool) を WeakKeyMap のキーとして使えるようになる
- その結果として、上記のクエリキャッシュ構造のリファクタリングが可能になる
この PR 自体は「最低バージョンを引き上げるだけ」で、WeakKeyMap ポリフィル削除やクエリキャッシュの実装変更は「後続 PR で行う」と明言されています。
2-3. 実際の差分の内容
変更されているのは主に gemspec とガイドのバージョン宣言です。
代表的な変更例 (概念的なイメージ):
# 例: activesupport/activesupport.gemspec (イメージ)
s.required_ruby_version = ">= 3.3.5"以下の各コンポーネントの .gemspec で、required_ruby_version が「3.3.5 以上」に変更されています。
- actioncable
- actionmailbox
- actionmailer
- actionpack
- actiontext
- actionview
- activejob
- activemodel
- activerecord
- activestorage
- activesupport
- rails (トップレベル)
- railties
- tools/rail_inspector
- tools/releaser
加えて、guides/source/upgrading_ruby_on_rails.md の Ruby バージョン要件の記載も 3.3.5 に更新されています。
コードロジックの変更はなく、あくまで「サポートする Ruby の下限バージョンを宣言上引き上げる」だけの PR です。
- 影響範囲・注意点
3-1. Rails 利用者側への影響
- Rails の次バージョン (この PR が入るバージョン) を使う場合、
- Ruby 3.3.4 以下では動作対象外になります
- CI / 本番環境ともに Ruby 3.3.5 以上に上げておく必要があります
- Gem の依存チェック (
required_ruby_version) によって、古い Ruby では bundle install 自体が失敗するようになります
移行時のチェックリスト:
ruby -vが 3.3.5 以上であることを本番 / 開発 / CI すべてで確認.ruby-versionやGemfileのruby "x.y.z"指定を 3.3.5 以上に変更- 使用している他の gem が Ruby 3.3.5 に対応しているかどうかを確認
- 特に C 拡張を含む gem (例: nokogiri, grpc, mysql2, pg など) は Ruby のマイナーバージョンアップでビルドし直しが必要な場合があります
3-2. 内部実装・今後の変更に関する影響
この PR の目的は「将来的なリファクタリングの前提条件づくり」です。
今後想定される変更:
- WeakKeyMap ポリフィルが削除される
- これにより、WeakKeyMap のキーとして使えるオブジェクトの種類が制限されなくなる
- Active Record のクエリキャッシュ実装が変更される
- 「全 connection pool を走査して現在コンテキストのキャッシュを消す」方式から、
- 「現在コンテキストに紐づいている pool のみを辿る」方式へと変更される見込み
- 結果として、
- マルチ DB / マルチテナントなどで connection pool が多い環境ほど、
- クエリキャッシュクリア時の CPU 使用量が下がる可能性が高い
ライブラリ作者視点の注意点:
- Rails 内部の WeakKeyMap 仕様に依存していた「ポリフィル前提の挙動」が、将来的に変わる可能性がある
- ただし通常のアプリケーションコードから WeakKeyMap を直接触ることは稀なので、影響を受けるのは主に Rails の拡張・プラグインを書いているような層に限られると考えられます
- 参考情報 (あれば)
- Ruby 3.3 系の WeakMap / WeakKeyMap バグ情報:
Ruby の公式チケット (Bug #xxxxx) や Changelog でWeakMap/WeakKeyMap/ use-after-free に言及があるはずです。
(PR テキストでは具体的なチケット番号は示されていませんが、Ruby 3.3.5 で修正されたことが前提になっています) - Rails ガイド:
guides/source/upgrading_ruby_on_rails.md
→ 今回の PR で Ruby バージョン要件が更新されているので、アップグレード時の公式手順を確認する際の参照先となります。
この PR 自体は単純なバージョン要件の更新ですが、将来の Active Record のパフォーマンス改善 (特に大量の connection pool を持つ環境) を可能にするための前段階という位置づけです。
#58911 Fix typos in guides [ci skip]
マージ日: 2026/10/1 | 作成者: @bastin27
- 概要 (1-2文で)
Rails の公式ガイドドキュメント内に含まれていた誤字・軽微な文法ミスを修正する PR です。コードや挙動への影響はなく、ドキュメントの可読性と正確さを高める目的の変更です。
- 変更内容の詳細
修正対象はすべて guides/source/ 以下の Markdown ドキュメントで、英語表記の誤りを正しいスペル・文法に直しています。
2.1 debugging_rails_applications.md
デバッグガイドに含まれていた複数のスペルミスが修正されています。
修正された箇所の例:
"Finshed"→"Finished"- 文脈的には、デバッガ出力やログ出力の例の中で、処理完了状態を示す文字列と思われます。
- 例(イメージ):diff
- (byebug) puts "Finshed processing" + (byebug) puts "Finished processing"
"propogated"→"propagated"- エラーや例外、あるいはブレークポイントの情報が上位フレームに「伝播」することを説明している箇所のスペル修正です。
"highlighed"→"highlighted"- コード例やトレースの一部が強調表示されることを説明する文章中のスペル修正です。
"interative"→"interactive"- byebug/pry のような「対話的 (interactive)」なデバッガの説明箇所の誤字修正です。
いずれも意味は明確ですが、英語として誤っていた綴りを正しただけの変更であり、サンプルコードの挙動そのものが変わるわけではありません。
2.2 layouts_and_rendering.md
レイアウトとレンダリングに関するガイドの文章中で、単語の重複が取り除かれています。
"options to to render"→"options to render"- Rails の
renderオプションの説明部分で、toが二重に書かれていたため、1つ削除しています。 - 例(イメージ):diff
- You have several options to to render templates. + You have several options to render templates.
- Rails の
純粋に読みやすさの改善のみです。
2.3 configuring.md
設定ガイド内の文法的なミスが修正されています。
"constraints built-in in browsers"→"constraints built into browsers"- HTML フォームやブラウザレベルの制約 (e.g.
maxlength,required) に言及しているような文脈で、- 不自然だった
"built-in in"を、 - 自然な
"built into"に置き換えています。
- 不自然だった
- 例(イメージ):diff
- These constraints are built-in in browsers. + These constraints are built into browsers.
- HTML フォームやブラウザレベルの制約 (e.g.
- 影響範囲・注意点
影響範囲
- 対象はガイドドキュメント (
guides/source/*.md) のみであり、Rails 本体のソースコードには一切変更がありません。 - 挙動・API・設定仕様などは全く変わらないため、アプリケーション側での対応や検証は不要です。
- ドキュメントを参照している記事・資料で、誤字をそのまま引用していた場合は、今後差異が生じる可能性はありますが、実害はありません。
- 対象はガイドドキュメント (
注意点
- CI スキップ指定 (
[ci skip]) がタイトルに入っている通り、テストは実行されていませんが、コード変更がないため問題にはなりません。 - ドキュメントの diff を追う際、「挙動の変更かどうか」を確認するときには、この PR は完全に見た目・文章だけの修正であると理解しておけば十分です。
- CI スキップ指定 (
- 参考情報 (あれば)
- 対象 PR: https://github.com/rails/rails/pull/58911
- Rails ドキュメント貢献ガイドライン:
https://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation
この PR から分かるように、Rails では誤字修正のような小さなものでも独立した PR として歓迎されており、ドキュメント品質向上のためのコントリビュートの一例と捉えることができます。
#58898 Avoid reprocessing ActiveRecord::Result column names
マージ日: 2026/10/1 | 作成者: @dersam
- 概要 (1-2文で)
ActiveRecord::Result が内部で「カラム名の再処理(主に String の intern / freeze)」を行うのをやめ、DBアダプタ側での処理に一元化したパフォーマンス改善のPRです。
これにより、不要な文字列オブジェクトの割り当てを避けつつ、カラム名配列は引き続き freeze される形になります。
- 変更内容の詳細
何をしているか
ActiveRecord::Result内で、カラム名 (@columns) に対して行っていた「都度 each で回して変換・加工する処理」を削除- 代わりに、アダプタ(MySQL, SQLite, PostgreSQL など)の段階で文字列の intern などを済ませる前提に変更
ActiveRecord::Result側では、カラム名の配列を受け取ったあとに- 追加の処理は行わず
- 配列自体を
freezeするだけに簡略化
PR本文中のポイント:
- 「String interning is now handled in the database adapters」
→ 文字列のインターン(同一内容の文字列を共有してメモリ節約・比較高速化する仕組み)がアダプタ側で完結する設計に統一 - 「Doing it in the adapters allows us to skip allocations that happen with the
eachversion」
→Result生成時に Ruby 側でeachを回しながらintern/freezeなどをしていた分のオブジェクト割り当てを削減 - 「We can now remove this and only freeze the array」
→ ActiveRecord 本体では、配列の不変性だけを保証し、要素(カラム名文字列)の処理は行わない方針
コード上のイメージ
実際の差分は1行追加・3行削除程度ですが、イメージとしては:
# 変更前(イメージ)
def initialize(columns, rows)
@columns = columns.map { |c| c.to_s.freeze } # / intern など
@columns.freeze
@rows = rows
end
# 変更後(イメージ)
def initialize(columns, rows)
@columns = columns.freeze # 中身の文字列はアダプタ側ですでに処理済み前提
@rows = rows
end※実際のコードとは多少異なる可能性がありますが、意図としてはこのような「Result 側で列名をあれこれ加工しない」方向への簡略化です。
- 影響範囲・注意点
対象:
- ActiveRecord を通じて DB クエリを発行し、その結果を
ActiveRecord::Resultとして扱う全機能(低レベル API を含む)
- ActiveRecord を通じて DB クエリを発行し、その結果を
実質的な振る舞いの変更:
- 結果としてのカラム名が frozen である点は維持される想定
- 文字列の intern など、メモリ・パフォーマンスに関わる処理がアダプタ側に完全移譲される
開発者が意識すべき点:
- 通常の AR モデル利用 (
User.select(...)など) では、API 的な挙動は変わらないはずで、明示的な対応は不要 - 自前でアダプタを拡張・実装している場合:
- カラム名の文字列処理(
intern,freezeなど)をアダプタ側で責任を持って行う必要がある - それをしていないと、Result のカラム名が想定より多くメモリを消費したり、GC 負荷が上がる可能性がある
- カラム名の文字列処理(
- 低レベルで
ActiveRecord::Resultを直接生成しているコードがあり、その中で「Result 側でカラム名を加工してくれる前提」に依存していた場合は挙動が変わる(ただし、そのような利用はかなりニッチ)
- 通常の AR モデル利用 (
後方互換性のリスク:
- 公式にサポートされる通常の利用パターンでは、仕様変更というよりは実装の最適化に近く、互換性リスクは低い
- ただし、カラム名オブジェクトの「同一性(
object_id)」までテストしているようなコードがあると、intern のタイミング変更によってテストが不安定になる可能性は理論上ある
- 参考情報 (あれば)
- 関連 PR: #58803
- こちらで「文字列の intern をアダプタ側で行う」方針の議論・実装が進められており、今回の PR はその仕上げとして ActiveRecord::Result 側の重複処理を削除した位置づけです。
- ActiveRecord::Result の役割:
- AR が SELECT などを発行した結果を、行列ベースの汎用的なコンテナとして表現するクラス
- カラム名 (
columns) と行データ (rows) を管理し、モデルにマッピングされる前段階の構造を担っているため、ここでのオブジェクト削減はクエリ全般のパフォーマンスに効きやすい箇所です。
#58892 Fix input id not matching label for when collection value is an empty string
マージ日: 2026/10/1 | 作成者: @55728
- 概要 (1-2文で)
collection_radio_buttons/collection_check_boxesなどで、値が空文字列""のときに生成される<input>のidと<label>のforが一致しなくなるバグを修正した PRです。nilの場合の修正 (#57209) を拡張し、「サニタイズ結果が空文字列になる値」全般に対して一貫した ID 付与になるようにしています。
- 変更内容の詳細
何が問題だったか
次のようなコードを考えます:
collection_radio_buttons :user, :active,
[["Yes", true], ["Undefined", ""]],
:last, :first期待されるのは、value: "" の要素に対して、input と label が同じ ID を参照することです。
しかし従来は次のような HTML が生成されていました:
<input type="radio" value="" name="user[active]" id="user_active_" />
<label for="user_active">Undefined</label>input側の ID:user_active_label側の for:user_active
つまり末尾のアンダースコア _ の有無が揃っておらず、label と input が正しく紐付かない状態でした。
背景:
label側はsanitize_attribute_nameの結果が空文字列のときは、末尾の_値を付けない実装になっていたinput側(add_default_name_and_field_for_value)は、nilのときだけ_値を付けない挙動になっていた- そのため「
nilは OK だが、空文字列""のときに不整合」が起きていた
どう直したか
actionview/lib/action_view/helpers/tags/base.rb の add_default_name_and_field_for_value のロジックを変更し、
- 「元の値が
nilかどうか」ではなく - 「
sanitize_attribute_name(value)した結果が空かどうか」
で、_sanitized_value を ID/名前に付けるかどうかを判定するようにしました。
これにより:
sanitize_attribute_name(value)が空文字列になる値(nilや""など)の場合
→user_active(サフィックスなし)- それ以外の値の場合
→user_active_true/user_active_yesのようにサフィックス付き
という挙動になります。label 側も同じロジックを使うため、常に一致するようになります。
API レベルで影響を受ける箇所
add_default_name_and_field_for_value は次のヘルパから共有利用されています:
radio_buttoncheck_box(multiple: trueの場合)label(value:を指定した場合)
そのため、コレクション系だけでなく、単体呼び出しでも挙動が変わります。
例:
radio_button :user, :active, ""
label :user, :active, value: ""従来:
<input type="radio" value="" name="user[active]" id="user_active_" />
<label for="user_active">...</label> <!-- あるいは for="user_active_" など不一致の可能性 -->修正後:
<input type="radio" value="" name="user[active]" id="user_active" />
<label for="user_active">...</label>テスト
actionview/test/template/form_collections_helper_test.rbにテストを追加collection_radio_buttonsとcollection_check_boxesの両方について、値が""の場合にid/forが一致することを確認
- 影響範囲・注意点
影響範囲
collection_radio_buttons/collection_check_boxesにおいて、コレクションの値が空文字列""の要素
→id/forの末尾から不要な_が消え、user_active_→user_activeのように変わります。次のような単体ヘルパ呼び出しでも同様に ID が変わります:
radio_button :user, :active, ""check_box :user, :roles, { multiple: true }, ""label :user, :active, value: ""
これにより:
- スクリーンリーダーやクリックターゲットの挙動が改善(
labelとinputの紐付きが正しくなる) - CSS / JS が
#user_active_のような古い ID を前提にしている場合は壊れる可能性がある
- スクリーンリーダーやクリックターゲットの挙動が改善(
注意点
- 破壊的変更の範囲は「ID 名が変わる可能性がある」に限定されますが、DOM を直接フックしているコード(テストを含む)があると影響を受けます。
- 既に #57209 を適用済みの Rails 8.1 系で、空文字列値を使っているフォームを持つアプリは、この PR で ID が「より一貫した形」に揃えられます。
→ そのぶん不具合修正としての価値は高い一方、DOM 名の変更としては認識しておく必要があります。 - heartcombo/simple_form などフォームビルダーを利用している場合、今回の修正はその上で動くヘルパにも波及します(根本の ID 生成ロジックが変わるため)。
- 参考情報 (あれば)
- 元 issue: heartcombo/simple_form#1840
https://github.com/heartcombo/simple_form/issues/1840#issuecomment-5872299154 - 関連 PR: #57209(
nilの場合の不一致修正) - 対象ブランチ:
main(8-1-stableにも backport 推奨とコメントあり)
#58882 Fix the :validate default in the Association Basics guide [ci skip]
マージ日: 2026/10/1 | 作成者: @ramen2020
- 概要 (1-2文で)
Association Basics ガイドの:validateオプションのデフォルト値説明が実装と食い違っていたため、関連の種類ごとに正しいデフォルト挙動が分かるように修正したドキュメント更新PRです。あわせて、has_and_belongs_to_manyは:validateをサポートしないという誤った記述も削除されています。
- 変更内容の詳細(あればサンプルコードも含めて)
何が問題だったか
元の Association Basics ガイドでは:
:validateのデフォルトは常にfalsehas_and_belongs_to_manyは:validateオプションをサポートしない
と読める記述になっていました。
しかし実際の実装 (ActiveRecord::Reflection#validate?) は以下のようになっています。
def validate?
if options.key?(:validate)
options[:validate]
else
options[:autosave] == true || collection?
end
endこれを踏まえると:
:validateが明示されていない場合collection? == trueであればtrue
→has_many,has_and_belongs_to_manyはデフォルトtruecollection? == falseかつautosave: trueであればtrue
→has_one autosave: true,belongs_to autosave: trueはtrue- それ以外は
false
→ 通常のhas_one,belongs_toはfalse
また、has_and_belongs_to_many は内部的に has_many を組み立てる際に :validate オプションをそのまま渡しており、validate: false なども有効です。
ガイドへの具体的な修正内容
:validate セクションの説明が、次のような趣旨の内容に変更されたと考えられます(意訳):
- デフォルト値は関連の種類によって異なる:
has_many,has_and_belongs_to_many- デフォルト:
validate: true
- デフォルト:
has_one,belongs_to- デフォルト:
validate: false - ただし
autosave: trueを指定した場合はvalidate: true相当になる
- デフォルト:
has_and_belongs_to_manyでは:validateが使えない、という記述を削除
PR の説明で示されていた動作確認結果は以下のとおりです(新規に1件の無効な子レコードを親に関連付けて save した場合):
| Association | 親の save 結果 |
|---|---|
has_many (デフォルト) | false (保存失敗) |
has_many validate: false | true (保存成功) |
has_and_belongs_to_many (デフォルト) | false (保存失敗) |
has_and_belongs_to_many validate: false | true (保存成功) |
has_one (デフォルト) | true (保存成功) |
has_one autosave: true | false (保存失敗) |
belongs_to (デフォルト) | true (保存成功) |
belongs_to autosave: true | false (保存失敗) |
この挙動に合うようにガイドの文章が4行追加、5行削除で調整されています。
サンプルコードイメージ
例えば:
class Author < ApplicationRecord
has_many :books # デフォルトで validate: true
has_one :profile # デフォルトで validate: false
has_one :account, autosave: true # 実質 validate: true
end
class Book < ApplicationRecord
belongs_to :author # デフォルトで validate: false
endauthor.books.build(invalid_attributes)してauthor.save
→has_manyはvalidate: trueなので、false(保存失敗)author.build_profile(invalid_attributes)してauthor.save
→has_oneはvalidate: falseなので、true(親は保存される)author.build_account(invalid_attributes)してauthor.save
→autosave: trueなので、false(保存失敗)
今回はこのような実際の挙動と整合するようにドキュメントだけが修正されています。
- 影響範囲・注意点
- 影響範囲は ドキュメントのみ であり、Rails 本体の挙動・API には一切変更はありません。
- ただし、これまでガイドだけを信じて「
has_manyなどでは関連先の検証はデフォルトで走らない」「has_and_belongs_to_manyではvalidate:は効かない」と思い込んでいた場合、その理解が誤りであったことに注意が必要です。 - 実運用では、次の点を意識すると安全です:
- 「親保存時に子のバリデーションも必ず走らせたい」
→has_one,belongs_toであればautosave: trueかvalidate: trueを明示する。 - 「親は保存したいが、無効な子がいてもかまわない」
→has_many,has_and_belongs_to_manyならvalidate: falseを明示する。
- 「親保存時に子のバリデーションも必ず走らせたい」
- 他のドキュメントやブログ記事に、古い(誤った)説明が残っている可能性があるため、挙動を確認したいときは API ドキュメントかソース(
Reflection#validate?)を参照するほうが確実です。
- 参考情報 (あれば)
- 該当実装:
ActiveRecord::Reflection#validate?
https://github.com/rails/rails/blob/7d52e0123842/activerecord/lib/active_record/reflection.rb#L741-L742 - API ドキュメント(
:validateのデフォルトについて正しい説明がある)has_many: デフォルトvalidate: truehas_and_belongs_to_many: デフォルトvalidate: truehas_one,belongs_to: デフォルトvalidate: false(ただしautosave: trueで実質有効化)
#58883 Fix range_field_tag and time_field_tag examples in the API docs [ci skip]
マージ日: 2026/10/1 | 作成者: @ramen2020
- 概要 (1-2文で)
FormTagHelperの API ドキュメント中で、range_field_tag/time_field_tag/datetime_field_tagのサンプルコードが実際の出力と食い違っていた問題を修正した PR です。実装の挙動は変えず、「ドキュメント上の例が正しく動く/正しく説明している状態」に揃えています。
- 変更内容の詳細
2-1. range_field_tag の例の修正
range_field_tag のシグネチャは以下の通りです。
def range_field_tag(name, value = nil, options = {})元のドキュメント例では:
range_field_tag 'quantity', in: 1...10のように書かれており、この呼び出しは Ruby 的には
name = 'quantity'
value = { in: 1...10 } # 第2引数にオプションハッシュが入ってしまう
options = {}となります。そのため、実際の出力は:
<input type="range" name="quantity" id="quantity" value-in=""1...10"" />のように、in: オプションが value-in という謎属性として出力されていました(本来期待している min="1" max="9" にはならない)。
これを修正するため、PR では例を次のように変更しています。
range_field_tag("quantity", nil, in: 1...10)
# => <input type="range" name="quantity" id="quantity" min="1" max="9" />
range_field_tag("quantity", nil, min: 1, max: 10, step: 2)
# => <input type="range" name="quantity" id="quantity" min="1" max="10" step="2" />ポイント:
- 第2引数に
nilを明示的に渡すことで、in:/min:/max:/step:が正しくoptionsとして解釈される。 - 出力 HTML の自己終了タグ
/>がドキュメントの例から抜けていた箇所も合わせて修正。
これにより、number_field_tag のドキュメント例と同じスタイル(value を指定しないときは第2引数に nil)に統一されています。
2-2. time_field_tag / datetime_field_tag の :include_seconds 例を削除
API ドキュメントには、以下のような説明と例がありました(イメージ):
time_field_tag("time", "01:01", include_seconds: true)
# => <input type="time" name="time" id="time" value="01:01:00.000" />しかし実装を見ると、time_field_tag / datetime_field_tag は単に text_field_tag をラップしているだけで、include_seconds: オプションを特別扱いしていません。そのため、実際の動作は:
time_field_tag("time", "01:01", include_seconds: true)
# => <input type="time" name="time" id="time" value="01:01" include_seconds="true" />のように、include_seconds がカスタム属性としてそのまま出力されるだけで、秒付きフォーマット(01:01:00.000 等)にはなりません。
秒の有無を解釈してくれるのは以下の「フォームビルダー版」のヘルパーのみです。
time_field/datetime_field(ActionView::Helpers::FormHelper)- 具体的には
Tags::TimeField/Tags::DatetimeFieldによる処理
*_tag 系はあくまで「単純に <input type="..."> を出すだけ」のヘルパーなので、include_seconds を知らないのが正しい挙動です。
PR では、この食い違いを解消するために:
time_field_tag/datetime_field_tagのドキュメントから:include_secondsオプションに関する記述と例を削除
しています。
あえて実装側に include_seconds 対応を追加せず「ドキュメントを実装に合わせる」方向にしたのは、「挙動の変更ではなくドキュメントの修正に留める」という方針によるものです。
- 影響範囲・注意点
- この PR は コードの挙動自体は一切変更しておらず、ドキュメント(コメント)のみを修正 しています。
- 変更ファイル:
actionview/lib/action_view/helpers/form_tag_helper.rb - 追加 3 行 / 削除 8 行(実装コードではなくコメント行)
- 変更ファイル:
開発者視点での注意点:
range_field_tagの使い方- オプション (
in:,min:,max:,step:など) を渡す場合、第2引数の value を省略すると Hash が value と解釈される ことに注意してください。 - ドキュメント同様、value を省略したい場合は以下のように
nilを入れるのが安全です。rubyrange_field_tag :score, nil, min: 0, max: 100, step: 5 - これは
number_field_tagなど他の*_field_tagにも共通するパターンです。
- オプション (
time_field_tag/datetime_field_tagではinclude_secondsは効かない- 秒を含めた入力補助やデフォルト値フォーマット等を期待するなら、タグヘルパー (
*_tag) ではなくフォームビルダーヘルパー を使う必要があります。erb<%= form_with model: @event do |f| %> <%= f.time_field :starts_at, include_seconds: true %> <%= f.datetime_field :starts_at, include_seconds: true %> <% end %> - すでに
time_field_tag(..., include_seconds: true)を使っているコードがあっても、- これまでも秒を扱うような特別な挙動はしておらず
- この PR による挙動変更もない ため、動作が変わることはありません。ただし「ドキュメントで見た挙動」を期待していた場合は、フォームビルダーヘルパーへの移行を検討すべきです。
- 秒を含めた入力補助やデフォルト値フォーマット等を期待するなら、タグヘルパー (
- 参考情報 (あれば)
実際の出力比較(PR 説明より):
修正前(main の挙動):
rubyrange_field_tag("quantity", in: 1...10) # => <input type="range" name="quantity" id="quantity" value-in=""1...10"" /> range_field_tag("quantity", min: 1, max: 10, step: 2) # => <input type="range" name="quantity" id="quantity" value-min="1" value-max="10" value-step="2" /> time_field_tag("time", "01:01", include_seconds: true) # => <input type="time" name="time" id="time" value="01:01" include_seconds="true" />修正後のドキュメント例:
rubyrange_field_tag("quantity", nil, in: 1...10) # => <input type="range" name="quantity" id="quantity" min="1" max="9" /> range_field_tag("quantity", nil, min: 1, max: 10, step: 2) # => <input type="range" name="quantity" id="quantity" min="1" max="10" step="2" />関連する設計上のポイント:
*_tag系ヘルパーは単純なタグ生成に特化しており、include_secondsのような「値のフォーマットを変える」オプションは扱わない。- モデルと紐づく
FormHelper(form_with/form_for内のf.*_field) は、値の型やオプションに応じたよりリッチな挙動を提供する。
#58864 Generator tests avoid collisions by using a temporary directory.
マージ日: 2026/10/1 | 作成者: @ChaelCodes
- 概要 (1-2文で) Rails のジェネレータ用テストが、テストごとに一時ディレクトリを使うようになり、
tmp/generators共有による並列実行時のファイル競合・テストのフレークが解消されました。これに伴い、テスト用の出力ディレクトリの削除タイミングがsetupからteardownに変更されています。
- 変更内容の詳細
背景にある問題
従来のジェネレータテストは:
- すべて
tmp/generatorsをdestination_rootとして使用 prepare_destinationが「各テストのsetupでdestination_rootを削除 → 再作成」
という動きをしていました。
この設計だと:
並列テスト実行時の競合
- テストAとテストBが同じ
tmp/generatorsを使う - テストAの
setupでtmp/generatorsを削除 - その直後にテストBが同じディレクトリを前提に動こうとして失敗
- → テストがフレークする(ときどき落ちる)
- テストAとテストBが同じ
同じパスに同名ファイルを生成するテスト同士の競合
- 異なるテストケースでも同じ
app/models/post.rbのようなパスを利用すると、状態が混ざる可能性がある
- 異なるテストケースでも同じ
という問題が起きていました。
この PR が行ったこと
1. テストごとに一時ディレクトリを使うように変更
- 各テストケース(もしくは各テストメソッド)ごとに、ユニークな一時ディレクトリを
destination_rootとして使うように変更しています。 - 実質的には、
Dir.mktmpdirのような仕組みで都度ランダムなディレクトリを作り、そこでジェネレータを走らせるイメージです。
これにより:
- テスト間で
destination_rootが共有されなくなり、並列実行でも互いの出力を壊さない - 同じパス・同名ファイルを生成するテストが複数あっても、安全に共存できる
というメリットがあります。
2. destination_root の削除タイミングを teardown に移動
以前は:
setup内でprepare_destinationを呼び、destination_root(=tmp/generators)を消して作り直していた
今回:
destination_rootが一時ディレクトリになり、テスト開始後にランダムに決まる- テスト完了後にそのディレクトリをクリーンアップ(削除)する必要がある
- しかし「どのディレクトリ名か」はテスト実行中にしか分からないため、削除処理を
teardown側に移動しています
つまり:
setup: 一時ディレクトリを作成し、それをdestination_rootにセット- テスト本体: そのディレクトリ配下にジェネレータがファイルを生成
teardown: 使い終わった一時ディレクトリを削除
というライフサイクルに変わりました。
3. ジェネレータ用テストテンプレートの更新
変更ファイルに generator_test.rb.tt が含まれていることから:
rails g generator Fooなどで作られる ジェネレータ用テストのテンプレートも、この新方式に追従するよう修正されています。- これにより、新しく作成されるジェネレータテストも、はじめから「テストごとに一時ディレクトリを使う」形になります。
4. testing/behavior.rb の調整
Rails::Generators::Testing::Behavior モジュール内の prepare_destination 周りが:
- グローバルな
tmp/generatorsを初期化する挙動から - 一時ディレクトリベースの
destination_rootと協調する挙動へ
と変更されています。行数的には +4/-2 と小さな差分ですが、責務の位置づけ(いつ・どこで消すか)が変わっています。
- 影響範囲・注意点
影響範囲
- Rails 本体の ジェネレータテスト基盤(
Rails::Generators::TestCase,Rails::Generators::Testing::Behavior)を使っているテストに影響します。 rails g generatorで生成される 新規のジェネレータ用テストは、この新しい一時ディレクトリ方式を前提としたコードになります。
- Rails 本体の ジェネレータテスト基盤(
メリット
- 並列実行時のジェネレータテストが安定する(フレークの低減)
- テスト同士が同じファイルパスを使っても安全
- テスト実行後に中間ファイルがきれいにクリーンアップされやすい(各テスト専用ディレクトリの削除)
注意点 / 互換性
- もし独自に
destination_rootを 固定パスに上書きしているテストがある場合、そのテストが新しいライフサイクル(setup/teardown)とどう干渉するかを確認する必要があります。 - テスト中に
destination_rootを前提としたパスをハードコードしている場合(例:Rails.root.join("tmp/generators/...")を前提にしているなど)、一時ディレクトリ方式に合うよう見直した方がよい可能性があります。
- もし独自に
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58864
- 問題の発端となった外部プロジェクトの PR(RubyEvents のジェネレータテスト改善):
- 関連コード:
Rails::Generators::Testing::Behavior#prepare_destination
https://github.com/rails/rails/blob/f3866028dd6a802599a0c8a243b2f7a2cccc0932/railties/lib/rails/generators/testing/behavior.rb#L100-L103
#58893 Remove support for dynamic controller/action in routes
マージ日: 2026/9/30 | 作成者: @tenderlove
- 概要 (1–2文で)
このPRでは、Railsのルーティングから「URLパラメータから:controllerや:actionを動的に読み取る」仕組みが完全に削除されました。Rails 5.0 から非推奨だったget ":controller(/:action(/:id))"形式などが今後はエラーとなり、コントローラとアクションを明示的に指定する書き方へ移行する必要があります。
- 変更内容の詳細
2-1. 動的 :controller / :action ルーティングの完全削除
以下のような「URLの一部をそのままコントローラ名・アクション名として解決する」ルート定義は、もはや利用できず ArgumentError を投げるようになりました。
# これまでは動いていたが、今後は ArgumentError
get ":controller(/:action(/:id))"
get ":controller/:action"これらは Rails 5.0 から非推奨 (deprecation) になっていて、「今まで警告は出ていたが動いていたもの」が、今回のPRで実際に削除されています。
推奨される書き換え
従来:
get ":controller(/:action(/:id))"
# /photos -> PhotosController#index
# /photos/1 -> PhotosController#show (慣習で)今後:
get "photos", to: "photos#index"
get "photos/:id", to: "photos#show"- どの URL がどのコントローラ#アクションに対応するかをルート定義側で明示するのが前提になります。
- 暗黙的にアクション名を推測したり、URLの文字列をそのままクラスやメソッドにマッピングする仕組みは消えます。
2-2. コントローラクラスを直接渡す書き方の変更
従来は、to: にコントローラクラスだけを渡すと、「動的アクション解決」と同じ仕組みに依存してアクションが補完される動作がありました。
# 以前: 暗黙に #show などを解決していたケースがある
get "show", to: PhotosControllerこの挙動は、内部的には「動的な controller/action 解決」と同じパスに乗っていたため、今回の削除に巻き込まれています。
今後はアクションを明示的に指定する必要があります:
get "show", to: PhotosController, action: "show"
# または
get "show", to: "photos#show"2-3. :controller / :action に対する Regexp の扱い変更
:controller や :action に Regexp 制約を付けていたようなルートは、もはや マッチし得ないルートとして即座にエラーを投げるようになりました。
例:
# これまでは「どうせマッチしないルート」が定義できてしまっていたケース
get ":controller/:action", controller: /admin_.+/このような「ダイナミック controller/action 前提の正規表現制約」は、そもそも今回の削除で意味をなさなくなったため、ルート定義時点でエラーとして弾くように変更されています。
2-4. 内部実装レベルでの主な変更ポイント
高レベルにまとめると:
ActionDispatch::Routing::Mapper:controller/:actionを動的に解決するパス (デフォルトルーティング、レガシーな auto-routing) が削除。- ルート定義時に
dynamic controller/actionを検出してArgumentErrorを上げる処理が追加。
ActionDispatch::Routing::RouteSetおよびJourney関連 (formatter.rb,visitors.rb)- URL生成・パス解析の際にコントローラ/アクションを URL から補完するロジックが整理・削除。
ActionDispatch::Http::Request- リクエストオブジェクトが params から controller/action を「後付けで決める」ようなコードが削除され、ルートが決めた値を前提とする形に単純化。
- 各種テスト (
actionpack/test/controller/...など)- 旧来の
get ":controller(/:action(/:id))"に依存したテストが軒並み削除/書き換え。 - 代わりに、明示的な
to: "photos#show"スタイルのルータに合わせてテストが修正。
- 旧来の
コード全体としては、古い「マジックなルーティングパス」を削ぎ落とし、ルーティング → params → コントローラの流れをより単純・明示的にする変更になっています。
- 影響範囲・注意点
3-1. 既存アプリのルーティングエラー
以下に当てはまるアプリは、Rails をこのバージョンに上げた瞬間から 起動時または最初のリクエスト時に ArgumentError となる可能性が高いです。
config/routes.rbに次のようなエントリが残っている:
get ":controller(/:action(/:id))"
get ":controller/:action/:id"
match ":controller(/:action(/:id))", via: :allresourcesを使わず、上記のような「catch-all」なルーティングに頼っている古いアプリ。
対応方針:
該当パターンを全削除またはコメントアウト。
実際に必要なエンドポイントごとに、明示的なルートを定義し直す:
ruby# NG (削除対象) get ":controller(/:action(/:id))" # OK (明示的に書く) resources :photos # index/show/create/... が生成される get "dashboard", to: "admin/dashboard#index"既存の URL が多く、後方互換を保ちたい場合は、
resources+member/collectionルートscope/namespaceを活用してパターンをまとめる などで段階的に整理していくのが現実的です。
3-2. コントローラクラスを to: に渡しているルート
以下のようなコードがある場合:
get "show", to: PhotosController今後は必ず action: を付与してください:
get "show", to: PhotosController, action: "show"
# もしくは
get "show", to: "photos#show"to: SomeController を使うパターンはそれ自体あまり一般的ではありませんが、エンジンや internal DSL で使っている場合は注意が必要です。
3-3. メタプログラミング的なルーティング生成
以下のような「ループで :controller を動的に入れ替えつつルートを量産する」ようなメタプログラミングをしていた場合も、根本的な見直しが必要です。
# 例: 動的 controller 名をパラメータで受けていたようなケース
%w[photos posts].each do |name|
get "#{name}/:action", controller: name
end- URL の一部をそのままクラス名等にマッピングする発想自体が危険・非推奨とみなされており、今回のPRで明確に締め出されています。
- 必要なら
resources :photos,resources :postsなどに展開し、アクションを列挙する形が推奨されます。
- 参考情報
- この機能は Rails 5.0 で depreciation notice が出ていたもので、Rails 6, 7 系を経てついに削除に至った、という位置づけです。
- 公式ガイドのルーティングのベストプラクティス:
- Rails Guides: Routing from the Outside In
https://guides.rubyonrails.org/routing.html
→resources,namespace,scope,matchなどを使った「明示的ルーティング」の考え方が整理されています。
- Rails Guides: Routing from the Outside In
- 変更内容は
actionpack/CHANGELOG.mdにも記載されており、「動的コントローラ/アクションルートの削除」が明示的な breaking change として扱われています。
このPRの本質は、「暗黙的で危険な自動マッピング」をやめて、コントローラ/アクションはルートで明示するという Rails の現在の設計方針をより徹底するものと理解すると良いです。
#58907 Document minimum Ruby version for 8.2
マージ日: 2026/9/30 | 作成者: @hmcguire-shopify
概要 (1-2文で)
Rails 8.2 で必要となる「最低 Ruby バージョン」を、公式アップグレードガイドに明示したドキュメント修正の PR です。コード変更はなく、ガイドラインの追記のみが行われています。変更内容の詳細
- 変更ファイル:
guides/source/upgrading_ruby_on_rails.md - 行追加: 1行(削除なし)
内容としては、「Rails 8.2 にアップグレードする際に要求される Ruby の最低バージョン」をアップグレードガイドに追記しています。
実際の文面は PR 情報からは読み取れませんが、典型的には以下のような形で記載されます:
Rails 8.2 requires Ruby X.Y.Z or newer.もしくは、既存の「バージョンごとの必要 Ruby バージョン一覧」の表や箇条書きに、Rails 8.2 分が1行追加されている可能性が高いです。
コード例が変わったわけではなく、「この Rails バージョンで使ってよい Ruby の範囲」を明示するためのドキュメント更新です。
- 影響範囲・注意点
- 実行時挙動・API には一切変更がなく、ドキュメントのみの変更です。
- ただし、この記述により以下が明確になります:
- Rails 8.2 にアップグレードする前に、使用中の Ruby が最低バージョンを満たしているかを確認する必要がある。
- CI の Ruby バージョンマトリックスや
.ruby-version,Gemfileのruby宣言などを、ドキュメントの要件に合わせて更新すべきかどうか判断しやすくなる。
- 「まだ Rails 8.2 の最低 Ruby バージョンを誤って低く想定していた」プロジェクトでは、実際のアップグレード作業前に Ruby 自体のアップグレード計画が必要になる点に注意してください。
- 参考情報 (あれば)
- 実際の最低 Ruby バージョンは、以下のいずれかを確認するのが確実です:
- 該当 PR (#58907) の diff(
upgrading_ruby_on_rails.md内の記述) - Rails ガイド: Upgrading Ruby on Rails
https://guides.rubyonrails.org/upgrading_ruby_on_rails.html - Rails の gemspec (
rails.gemspec) や各コンポーネントの gemspec に書かれているrequired_ruby_version
- 該当 PR (#58907) の diff(
これらを確認して、Gemfile 等に:
ruby "X.Y.Z"のように明示することで、チーム全体で Rails 8.2 対応の Ruby バージョンを統一できます。
#58905 Lazy call_arg inspection for attribute methods
マージ日: 2026/9/30 | 作成者: @hmcguire-shopify
- 概要 (1-2文で)
ActiveModel::AttributeMethods内で、属性メソッド生成時のcall_argsのinspect呼び出しを「必要になった時だけ実行する」ように変更し、無駄な String オブジェクトの生成を減らすパフォーマンス改善です。モデル × カラム × 属性メソッドごとに発生していた余計な文字列割り当てが、ほとんどのケースで発生しなくなります。
- 変更内容の詳細
これまでの動作
ActiveModel::AttributeMethods は、未定義メソッド呼び出し時などに、call_args を元にエラーメッセージやデバッグ情報を組み立てることがあります。その過程で、次のような形で引数を一括で inspect 化していました:
call_args.map!(&:inspect)この処理は、
- すべての
call_argsについて - まだ使われるかどうか分からない段階で
- 文字列(
#inspectの結果)を生成し、配列を書き換える
という性質があります。
PR の説明にある通り、その後のフローでは「カラム × 属性メソッド」の組み合わせのうち、最初の 1 回しか call_args の文字列表現を実際には使っていなかったにもかかわらず、モデル × カラム × 属性メソッドごとに常に inspect 付きの文字列を生成していました。
この PR での変更
call_args.map!(&:inspect) を「呼び出しの早い段階で一括実行」するのではなく、実際に inspect された引数が必要になる箇所まで処理を遅延(lazy)させた、というのがこの PR の本質です。
イメージとしては、従来:
# 以前: 早い段階で必ず全引数を inspect していた
call_args.map!(&:inspect)
# 以降、エラーメッセージなどで call_args を使用となっていたところを、変更後は:
# 必要になるまで inspect しない
# ...
if need_inspected_args
inspected_call_args = call_args.map!(&:inspect)
# inspected_call_args を使う処理
endといった形に近づけた、という変更です(実際のコードは多少異なる可能性がありますが、意図としてはこのような「使う直前で初めて inspect する」形にリファクタリングされています)。
これにより、
- 多くのケースでは
inspect自体が呼ばれない - したがって、不要な String オブジェクトの割り当てが行われない
という挙動になります。
- 影響範囲・注意点
影響範囲
- 対象:
ActiveModel::AttributeMethodsを利用している全てのモデル(ActiveRecord::Baseを継承した通常の Rails モデルを含む) - 実際の影響: 主に内部のパフォーマンス改善であり、アプリ側のコード変更は不要
挙動の変化の可能性
通常の利用において、外部に見える挙動(API)は変わらないようになっています。注意すべき点は次のような場合です:
call_argsをinspectする前提で、AttributeMethods内部の非公開 API に依存している(メタプログラミングや Monkey Patch など)- そのようなコードは本来非推奨ですが、この PR により、「常に
call_argsが文字列(inspect済み)である」という前提は崩れます。
- そのようなコードは本来非推奨ですが、この PR により、「常に
call_argsの内容や型を内部フックで覗き見している場合- 一部のタイミングで、以前は String だったものが、元のオブジェクト(Symbol, Numeric, など)のままになっている可能性があります。
標準的な Rails アプリケーションで、公式にサポートされている API だけを使用している限りは、動作上の違いを感じることはなく、パフォーマンスがわずかに向上するだけと考えてよいです。
- 参考情報 (あれば)
- 該当 PR: https://github.com/rails/rails/pull/58905
- 関連コンポーネント:
ActiveModel::AttributeMethods(Active Record モデルの動的な属性メソッド生成ロジック)
- 類似の最適化として、Rails 本体では「例外メッセージの構築を遅延させる」「バックトレース生成を必要時のみ行う」など、
inspectや文字列生成を遅らせるパターンが多数採用されており、本 PR もその一環の位置づけといえます。
#58746 Escape JSON with String#tr!
マージ日: 2026/9/30 | 作成者: @federico-carrocera
- 概要 (1-2文で)
Rails の JSON エンコード処理において、特殊文字のエスケープをString#gsub!ベースからString#tr!(ハッシュ対応版)を用いた実装へ最適化し、大きめの JSON ドキュメントで大きなパフォーマンス向上を図った PR です。エンコードが不正な場合は従来どおりgsub!にフォールバックするため、互換性を保ちつつ高速化しています。
- 変更内容の詳細
2-1. 何をしている PR か
Ruby 本体側の新機能として、String#tr!(hash) が導入されました(1パスで UTF-8 文字列を書き換え可能、正規表現不要)。
この PR では、Rails の JSON エンコーダ(特に JSONGemCoderEncoder)が JSON エスケープ処理にこの tr! を使うように変更されています。
従来の実装は:
- 文字列をバイナリエンコーディングに変更 (
ASCII-8BIT) - エスケープすべきバイト列を
Regexp.unionとgsub!(regex, hash)で置換 - UTF-8 に戻す
という流れでしたが、新しい実装では、UTF-8 のまま tr!(hash) によって一発変換します。
2-2. エスケープ対象文字とロジック
Rails の JSON エンコードで問題になるのは主に:
- U+2028 (LINE SEPARATOR)
- U+2029 (PARAGRAPH SEPARATOR)
<,>,&
これらは HTML 埋め込み時の XSS やスクリプトコンテキストでの問題を避けるためにユニコードエスケープへ変換されます。
PR の説明中にあるマッピングは概ね次のようなものです:
ESCAPES = {
"\u2028" => '\u2028',
"\u2029" => '\u2029',
">" => '\u003e',
"<" => '\u003c',
"&" => '\u0026',
}Rails 側でも同様のマッピングを JSON エンコード時に使用し、それを tr! に渡します。
2-3. 有効なエンコーディングのみ tr! を使う
String#tr! は「不正なエンコーディングの文字列」に対しては例外を上げる仕様です。そのため、Rails 側では:
def escape_with_tr(json)
return escape_with_gsub(json) unless json.valid_encoding?
json.tr!(ESCAPES)
json
endのように、
valid_encoding?がtrueならtr!を使用(高速パス)falseなら従来どおりgsub!による処理(フォールバック)
という二段構成になっています。
これにより、既存のアプリで不正なエンコーディングを含む文字列が紛れ込んでいたケースでも、今までと同じ挙動(gsub! ベース)で動作し、例外で落ちることを避けています。
2-4. パフォーマンス特性
PR で提示されているベンチマーク結果(Ruby 4.1 dev, arm64, Darwin)は次の通りです。
小さい JSON(48 bytes, エスケープ対象なし)の場合
gsub!: 約 6.18M i/str!: 約 4.51M i/s
-> 小さい & 置換不要な文字列では、事前コンパイル済み Regexp を使うgsub!の方が速い(tr!は毎回翻訳テーブルを作るため)。
大きな JSON(約 51KB, HTML を 900 回繰り返し)の場合
gsub!: 約 1.95k i/s (~512 µs / call)tr!: 約 20.45k i/s(~49 µs / call)
-> 約 10.5 倍速い。
Rails 実運用では JSON が数 KB〜数十 KB になるケースが多いため、この変更によって全体としては十分な性能改善が見込める、と判断されています。
2-5. どのエンコーダが対象か
- Rails には複数の JSON エンコーダがありますが、
JSON::Coderが利用可能な環境ではデフォルトでJSONGemCoderEncoderが使われます。 - この PR では「両方のエンコーダ」が更新されていますが、現実的には
JSONGemCoderEncoderの影響が最も大きくなります。 activesupport/lib/active_support/json/encoding.rbにあるエスケープ処理がtr!対応版に更新されています。
- 影響範囲・注意点
3-1. 影響範囲
影響を受けるのは「Rails で JSON を生成する全てのコード」です。
例:render json: objectobject.to_json(Rails の ActiveSupport 拡張経由)ActiveSupport::JSON.encode(object)
特に HTML に埋め込む JSON(
<script>タグ内の初期データなど)でのエスケープ処理はこの変更の直接的な恩恵を受けます。
3-2. 互換性と振る舞いの変化
基本的には出力される JSON の内容は変わらない(バイト列レベルで一致するよう、テスト & ベンチで確認済み)ことが前提で実装されています。PR 中でも以下のように検査しています:
raise "paths disagree" unless escape_gsub(src.dup).b == escape_tr(src.dup).bこのため、アプリケーションコード側で想定している JSON フォーマットやエスケープ結果は従来どおりです。
ただし、次のようなケースには注意してください:
エンコーディングが不正な文字列を JSON に流し込んでいる場合
- その文字列は
valid_encoding? == falseになり、tr!ではなく旧来のgsub!パスへ回ります。 - これ自体は挙動の変化ではありませんが、「
tr!による高速化の恩恵を受けない」ため、そうした文字列を事前に正規化しておくと性能改善につながります。
- その文字列は
Ruby バージョンとの対応
String#tr!(hash)は Ruby 本体側の新機能に依存します。- Rails 側では Ruby の該当バージョン以降をターゲットとしているため問題ありませんが、もし独自に
String#tr!をモンキーパッチしていた場合には、挙動が変わる可能性があります。
3-3. パフォーマンス面の実務的な影響
- 大量の JSON を返す API、もしくは
render json:を大量に叩く SPA バックエンドなどでは、CPU 使用率の低減やスループット向上が期待できます。 - 逆に、非常に小さな JSON を「頻繁に」「ほぼエスケープ不要な形で」生成するマイクロサービスの場合、理論上は旧来の
gsub!の方が速いケースもありますが、Rails 側のベンチでは「全体としてはtr!の方が有利」と判断されています。- 必要であればアプリケーションレベルで独自エンコーダを実装し直すことで、局所的にチューニングする余地はあります。
- 参考情報 (あれば)
Ruby 本体の PR:
- https://github.com/ruby/ruby/pull/18647
String#tr!がHashを受け取れるようになる変更。UTF-8 文字列に対する 1 パス変換を提供。
- https://github.com/ruby/ruby/pull/18647
Rails の変更ファイル(この PR):
activesupport/CHANGELOG.md- 今回の JSON エンコード最適化に関する変更履歴を追加。
activesupport/lib/active_support/json/encoding.rb- JSON エンコード時のエスケープ実装を
gsub!中心からtr!中心へ変更。
- JSON エンコード時のエスケープ実装を
activesupport/test/json/encoding_test.rb- 新しい実装に対するテスト追加。
gsub!とtr!パスの出力が一致することなどを検証。
- 新しい実装に対するテスト追加。
この PR による挙動差分は基本的に「性能のみ」であり、JSON のフォーマットやエスケープ結果は互換になるよう配慮されています。開発者としては、特別な対応なしにそのまま性能改善の恩恵を受けられる変更と考えて問題ありません。