Skip to content

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. 概要 (1-2文で)
    read_attribute 内部で新しく導入された resolve_alias メソッドを使うように変更し、属性エイリアスを持たないモデルでの read_attribute 呼び出しを高速化した PR です。エイリアス付きモデルでの性能はほぼ変わらず、挙動も従来と同等になるよう保たれています。

  1. 変更内容の詳細

何をしたか ​

  • ActiveRecord::AttributeMethods::Read#read_attribute の実装の一部を差し替え、属性名の解決に新メソッド resolve_alias を利用するようにしました。
  • 変更は 1 行の追加 + 2 行の削除のみで、既存ロジックの一部を置き換える軽微なリファクタリングです。

(実際のコードは PR 本体を見ないと正確な行は書けませんが、イメージとしては以下のような差し替えです)

ruby
# 変更前 (イメージ)
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)
end

resolve_alias 側には以下のような最適化が入っていると考えられます:

  • モデルにエイリアス定義が無い場合は、即座にそのままの名前を返す軽い分岐(if @aliases など)があり、オーバーヘッドがほぼない。
  • エイリアスがある場合のみ、テーブル(ハッシュ)の lookup 等を行う。
  • 同じメソッドを属性読み取りの他の経路でも共有することで、属性名解決ロジックを一箇所にまとめている。

ベンチマーク結果の意味 ​

PR 説明にある 3 パターンのベンチマークは、read_attribute の呼び出しパターン別の性能を比較したものです。

  1. no-alias

    • モデルに属性エイリアスがないケース。
    • 変更前: 約 12.83M i/s
    • 変更後: 約 14.92M i/s
    • 約 1.16 倍高速化(1 回あたり ~67ns)
  2. aliased-canonical

    • エイリアスを定義しているが、「正規のカラム名」で読んでいるケース。
      例: alias_attribute :alias_name, :canonical_name があって、read_attribute("canonical_name") している場合。
    • 変更前 vs 変更後: 差分は誤差範囲内で「同等」。
  3. aliased-name

    • エイリアス名で読んでいるケース。
      例: read_attribute("alias_name")
    • 変更前 vs 変更後: こちらも誤差範囲内で「同等」。

つまり:

  • エイリアス無しモデル: read_attribute がはっきり速くなる。
  • エイリアス有りモデル: エイリアスの有無や呼び出し方に関係なく、性能は実質的に変わらない。

  1. 影響範囲・注意点

影響範囲 ​

  • 対象: ActiveRecord::Base#read_attribute を経由する全ての属性読み取り(model[:attr], 内部での read_attribute 呼び出しなどを含む)。
  • 主な影響:
    • API / 公開インタフェースとしての挙動は変わらない(同じ属性名を渡せば同じ値が返る想定)。
    • モデルにエイリアスが定義されていない場合のパフォーマンスが改善。
    • モデルにエイリアスがある場合も、少なくとも劣化はしないような実装になっている。

互換性・注意点 ​

  • resolve_alias は ActiveRecord 内部のヘルパーメソッドであり、これに依存したアプリケーションコードは通常存在しないはずです。
  • ただし、以下のようなケースでは注意が必要です:
    • ActiveRecord の内部実装(特に attribute 読み取りのフック)を monkey patch している。
    • read_attribute を再定義していて、内部実装の細かい挙動や引数の型(Symbol/String)に依存している。
  • そのような高度なカスタマイズをしている場合は:
    • resolve_alias の実装(型変換のタイミング、to_s の有無など)を確認し、既存パッチと矛盾がないかをチェックすると安全です。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    render partial: ..., collection: ... で、キャッシュや per-item レイアウトを使わない場合に限り、「要素ごとのバッファ」をやめて1つのバッファにまとめて描画するようにした最適化です。これにより、コレクション描画時のオブジェクト生成数と処理時間をわずかに削減しつつ、出力結果は従来と完全に同じになるよう保たれています。

  1. 変更内容の詳細

これまでの挙動 ​

render partial: "item", collection: @items のようなレンダリングでは、各要素ごとに:

  1. OutputBuffer を1つ生成
  2. その中に部分テンプレートを描画
  3. 描画結果を SafeBuffer にコピー
  4. それを RenderedTemplate オブジェクトでラップ
  5. 最後に RenderedTemplate たちの body を配列に集めて join し、もう1つ SafeBuffer を生成して最終的な文字列を得る

というフローでした。
結果として「各要素ごとに 5 個程度のオブジェクトが、最終的には結合されるだけのために」生まれていました。

新しい挙動の方針 ​

PRのポイントは:

「要素ごとの出力を個別に扱う必要がないケースでは、最初から1つのバッファに全部描画してしまう」

です。

要素ごとの出力(各アイテムの HTML)を個別に必要とするケースは2つだけと整理されています:

  1. コレクションキャッシュ

    • render partial: "item", collection: @items, cached: true などで、各要素ごとにキャッシュを保存したり取り出したりする場合
    • このときは「アイテム単位で」HTMLを扱う必要がある
  2. 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 を描画するケース(バッファネストの検証)

が追加されています。


  1. 影響範囲・注意点

影響範囲 ​

対象は ActionView のうち、collection_renderer.rb が担当する「コレクションレンダリングパス」です。具体的には:

  • render partial: "xxx", collection: @items
  • render @items(デフォルト partial推論を使うケース)
  • spacer_template を使ったコレクションレンダリング

などの内部処理が変わっています。

ただし、挙動面では:

  • 出力される HTML・テキスト
  • locals / as: / collection: / spacer_template: / layout: オプションの外部 API
  • キャッシュキーの扱い(キャッシュを使う場合は従来パスを維持)

はいずれも互換性維持が意図されており、テストでもカバーされています。

パフォーマンスへの影響 ​

PRでの計測結果(1レイアウト + 1テンプレート + 20要素のコレクション部分テンプレートを描画するケース):

指標beforeafter
allocations per request31723070
time per request858µs803µs
  • 約 100 オブジェクト強のアロケーション削減(20要素で5オブジェクト/要素 → 理解に合う値)
  • リクエストあたり ~6% 程度の時間短縮という軽量な最適化

大規模なコレクションレンダリングを多用している場合、GC 負荷軽減やレイテンシ削減の小さな積み重ねとして効いてくる可能性があります。

注意点・互換性リスク ​

  • API 変更はなく、アプリケーションコードの変更は不要な設計です。
  • ただし、ActionView の内部バッファ実装に依存した「裏技的」な metaprogramming や monkey patch をしている場合は、
    • 「アイテムごとに RenderedTemplate が返ってくる」ことを前提にしていたコード
    • コレクションレンダリング時の内部バッファ構造に依存していたコード
      などは影響を受ける可能性があります(通常のアプリではほぼ心配不要)。

  1. 参考情報 (あれば)
  • 変更ファイル

    • actionview/lib/action_view/renderer/collection_renderer.rb
      • コレクションレンダリングのメインロジック。
      • 「単一バッファパス」と「per-itemパス(キャッシュ・レイアウト向け)」の条件分岐と実装が追加。
    • actionview/test/template/render_test.rb
      • 上記挙動を保証するためのテストが2つ追加。
  • 類似コンテキスト

    • Rails では、ActionView::OutputBuffer / ActiveSupport::SafeBuffer を使った XSS セーフな文字列結合が行われており、
      本PRはその仕組みの上で「バッファ個数を減らす」ことにフォーカスしたマイクロ最適化です。

#59075 Fix composite-key association query edge cases ​

マージ日: 2026/10/15 | 作成者: @matthewd

  1. 概要 (1-2文で)
    このPRは、Active Record における複合主キー(composite key)を使った関連・クエリ周りのいくつかのエッジケースを、単一カラム主キーと同等の振る舞いになるように修正するものです。主に「空配列条件」「polymorphic な関連 + 複合キー」「preload 時の available_records」「複合キー関連への単一id指定」などの不整合・例外を改善しています。

  1. 変更内容の詳細

(1) where([:a, :b] => []) などで WHERE () が出ていた問題の修正 ​

現状の問題:

ruby
Model.where([:a, :b] => [])
# => これまで SQL 的に意味のない WHERE () が生成されていた

同様に、複合キーな関連に対して:

ruby
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件」という扱いにします。

イメージ:

ruby
# 単一主キー
Post.where(id: [])          # => 実行されず、結果は Relation(空)

# 複合キー (例: [:account_id, :code] が主キー)
Item.where([:account_id, :code] => [])
# => 以前: 変な WHERE () / または DB エラーの可能性
# => 今回から: 矛盾と認識してクエリスキップ(結果は空)

(2) where(polymorphic: relation) が複合キーをターゲットにすると落ちていた問題 ​

現状の問題:

polymorphic な関連に対して、relation をそのまま渡すパターン:

ruby
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 対象となる関連レコードのサブセットを事前に指定できます。

ruby
# 例: ある条件を満たす関連だけを 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 になっていました。

ruby
# 例: child は [:account_id, :code] の複合キーを持つ関連
parent.child.find(1)
# or where(child: 1)
# => これまで: NoMethodError など、意味不明な例外が発生

今回の修正:

  • PredicateBuilder::AssociationQueryValue にガードロジックを追加し、
    • 複合キー関連に対して 単一の id を渡した場合 に、
    • 意図を説明する ArgumentError を明示的に発生させるよう変更。

期待される挙動:

ruby
parent.child.where(id: 1)
# => ArgumentError: "Composite key association expects an array of ids"(趣旨として)

これにより、開発者は「複合キー関連には配列(あるいは適切な形のタプル集合)を渡す必要がある」ということをすぐに理解できます。


  1. 影響範囲・注意点
  • 複合主キーを使っていないプロジェクト
    • ほぼ影響はありません。内部実装のロジックは単一キー向けの挙動を変えない形で慎重に拡張されています。
  • 複合主キー + 関連を積極的に使っている場合
    • 以下の挙動変更があります:
      1. where([:a, :b] => []) や where(assoc: []) で、以前は壊れた SQL または DB 依存の挙動だったものが、「即座に結果0件」として扱われます。
        → 実運用上は「バグっていた状態が正される」ため、想定どおりの結果になるケースが多いはずです。
      2. polymorphic な関連に relation を渡すフォームで、複合キーターゲットが正しく扱われるようになります。以前は例外で落ちていたところが正常に動作するようになるため、「落ちなくなった」ことに注意してください(テストでそこを期待していたなら修正が必要)。
      3. preload の最適化や限定的 preload に available_records を使っている場合、複合キー関連でもその制御が効くようになります。
        → これにより、今まで「なぜか複合キー関連だけ余計に preload される」ような状況が解消されます。
      4. 複合キー関連に単一 id を渡していた隠れたバグがある場合、これまでたまたま NoMethodError などで気付きづらかったところが、ArgumentError としてより明確に露呈します。
        → テスト実行時に新たな ArgumentError が出たら、関連のキー指定方法を見直す必要があります。
  • 後方互換性
    • 仕様としては「これまで明確に定義されていなかった(あるいは壊れていた)挙動を、単一キーと整合する形に揃える」変更なので、基本的には後方互換性を向上させる方向の修正です。
    • ただし、「壊れた状態に依存していた」コードがある場合(特に例外の種類に依存している場合)はテストが落ちる可能性があります。

  1. 参考情報 (あれば)
  • 該当ファイル:
    • 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. 概要 (1-2文で)
    このPRは ActiveRecord の属性読み取りまわりを内部的に最適化し、属性取得とカラムエイリアス解決の高速化を行う変更です。通常ケース(属性が存在し、エイリアスも特に定義されていない)のパスを極力軽くし、Ruby インタプリタと ZJIT 双方でベンチマーク性能を改善しています。

  1. 変更内容の詳細

2-1. プライベートな属性リーダーメソッドの追加 ​

ActiveModel / ActiveRecord の属性取得処理に、内部専用の高速なリーダーメソッドが追加されています。

ポイント:

  • 「外部APIとしては公開しない」プライベートメソッド
  • 引数は必ず文字列(String)の属性名だけを受け取る前提
    • 呼び出し側で attr_name.to_s のような変換処理を済ませておく
  • ブロックで「見つからない時の処理」を渡すのではなく、
    • 通常ケース: 既存属性をそのまま即座に返す
    • 例外ケース: 新しい fetch_value! が 例外を投げる(missing 属性用の遅いパス)

従来は:

ruby
# 擬似コードイメージ
value = attributes.fetch(name) { handle_missing(name) }

のように fetch + ブロックで「見つからない場合ロジック」を渡すことが多かったところを、

ruby
# 擬似コードイメージ
def _read_attribute_internal(name_string)
  fetch_value!(name_string) # 無ければ例外
end

のように、「ほぼ常に存在する」ことを前提にした直線的なパスに寄せています。

理由:

  • 「属性が存在しない」ことは非常にレアケースであるため、そこにコストを支払うべきではない
  • ブロックを渡すスタイルは、インタプリタ・JIT ともに最適化しづらい
  • ブロックを使わず、例外による遅いパスに逃がすことで、ホットパスを最小限の分岐とコールだけにできる

結果として、Ruby インタプリタおよび ZJIT 双方で属性読み取りが軽くなり、ActiveRecord 全体のベンチマークが改善しています。

2-2. fetch_value! メソッドの追加 ​

新しい内部メソッド fetch_value! は、指定した属性が存在しない場合に 例外を投げることが保証される取得メソッドです。

特徴:

  • 「必ず存在しているはずの属性」を取るために使う
  • 見つからない場合の戻り値をブロックで指定するのではなく、即座に例外
  • そのため JIT がインライン化しやすく、分岐も単純になる

イメージコード:

ruby
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 処理に差し替える

擬似コードイメージ:

ruby
# デフォルト(エイリアス無し)
@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*.rb
  • activemodel/lib/active_model/attribute*

これらは次のような意図で書き換えられていると考えられます:

  • 旧来の「柔軟だが遅い」属性読み取りを、
    • 新しい内部専用 reader / fetch_value! を使うようにリファクタリング
  • 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 ベンチマークに基づく計測で、属性アクセスが多い実ワークロードを想定した最適化であることが分かります。


  1. 影響範囲・注意点

外部 API の互換性:

  • 追加されたメソッドは「プライベート reader」「内部用 fetch_value!」などであり、通常の ActiveRecord/ActiveModel 公開 API には手を出していません。
  • そのため、一般的な利用コードに対しては互換性は高く、挙動変更はほぼ無いと考えられます。

考えられる影響・注意点:

  1. 内部 API に依存している場合

    • プライベート/内部メソッドに依存したメタプログラミング、Monkey Patch をしているアプリ/ライブラリは、呼び出し先が変わっている可能性があります。
    • とくに attributes の直接参照や alias 解決に干渉しているコードは、行数の多い association 周りの変更の影響を受けるかもしれません。
  2. Missing Attribute の扱い

    • 「属性が存在しない」場合に例外を投げるパスがよりはっきり分離されたため、
      • 今まで「たまたま nil を返していた」ような非公認挙動に依存していた場合は問題になる余地があります。
    • とはいえ、MissingAttribute はもともと例外パスとして扱われることが多く、標準的な使い方では問題にならないことがほとんどです。
  3. カラムエイリアスの挙動

    • 「エイリアスを定義すると、それ以降そのモデルの alias 解決関数が差し替わる」という設計がより明示化されています。
    • API レベルの挙動は従来通り(定義されたエイリアスが優先)で、動作仕様が変わるというよりは実装の経路が変わったに近いです。
  4. パフォーマンス計測/チューニング

    • 大量のレコードを扱うアプリケーションや、高頻度で association を辿るコードで、恩恵が出やすい変更です。
    • ruby-head + Rails main を追いかけている場合は、独自最適化(キャッシュやラッパー)より先に、この最適化の効果を確認する価値があります。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    render_template.action_view のインストルメンテーション用 payload Hash を、「購読者がいる場合にだけ」生成するように変更し、特に本番環境での不要なオブジェクト割り当てを削減するパフォーマンス改善のPRです。イベント名や payload の中身・挙動自体は既存の購読者から見て互換性が保たれています。

  1. 変更内容の詳細

何が問題だったか ​

これまでの ActionView では、テンプレートやパーシャルをレンダリングするたびに、次のような流れになっていました:

  1. !render_template.action_view イベント向けの payload (Hash) を毎回生成
  2. ActiveSupport::Notifications.instrument("!render_template.action_view", payload) { ... } を実行
  3. 実際にはフレームワーク本体ではこのイベントに誰も subscribe しておらず、主にテストヘルパ (assert_template など) が利用する程度
  4. 結果として、本番環境ではほぼ毎レンダリングごとに「GC行きのHash」を作って捨てているだけ、という状態

特に、レイアウト + メインテンプレート + コレクションパーシャル多数、という典型的な画面では、テンプレートの数だけ無駄な Hash が作られていました。

今回の変更内容 ​

この PR では、

「購読者がいるかどうかを先に確認してから payload を作る」

という順序に変更しています。

Ruby レベルのイメージとしては、ざっくり次のような差分です:

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 が通知される
  • つまり、「購読者がいるときの観測可能な挙動」は完全に後方互換であることをテストで固定(ピン)している

  1. 影響範囲・注意点

影響範囲 ​

  • 対象: !render_template.action_view イベントのインストルメンテーション(テンプレート / レイアウト / パーシャルの render)
  • 変更点:
    • 購読者がいない場合
      • これまで: 毎回 payload Hash を生成し、instrument 呼び出しが行われる(が、誰も受け取らない)
      • これから: payload 生成も instrument 呼び出しも行われない
    • 購読者がいる場合
      • これまで: 毎回 payload を生成 → instrument → subscriber が受け取る
      • これから: 事前に「購読者あり」と判定された場合のみ、これまで通り payload を生成して instrument される
      • payload の中身とイベント名は変更なし(テストで保証)

パフォーマンスへの影響 ​

PR 説明にある計測例:

  • レイアウト + メインテンプレート + 20件のコレクションパーシャルを描画するページで
    • 1リクエストあたりのオブジェクト確保数: 3878 → 3857 (21 減少)
    • 「1レンダリングあたり 1オブジェクト減」と見なせる

規模としては小さい削減ですが、「プロダクションでは普通 subscribe しないテスト用イベント」のコストを削る、という意味で筋の良い最適化です。大量のテンプレート/パーシャルを描画するビューでじわじわ効いてきます。

注意点 / 互換性 ​

  • !render_template.action_view を listen しているコードがある場合:
    • そのコードの動作は、購読している限り変わりません(イベントは引き続き発火し、payload も同じ)
  • 逆に、「これまで誰も subscribe していなかったが、instrument の呼び出しだけを副作用として期待していた」ようなコードがもしあれば、動作が変わります
    • ただし、このイベントはそもそもそうした用途を想定しておらず、普通は問題にならないはずです
  • 通常のアプリケーション開発者視点では、コード変更や設定変更は不要で、そのままパフォーマンス改善の恩恵だけ受けられます

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Rails の tag ヘルパ内部で HTML 属性文字列を組み立てる処理を見直し、属性ごとに一時文字列を生成せずにバッファへ直接追記するようにして、オブジェクト生成数と処理時間を削減するパフォーマンス改善です。外部から見た挙動や出力 HTML は一切変わりません。

  1. 変更内容の詳細

何を変えたか ​

対象は 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")87
tag.input(type: "text", name: "q", value: text)107
tag.input(type: "checkbox", disabled: true, required: true)96
tag.div(text, data: { controller:, action: }, aria: { label: })2017
tag.span(text, class: ["a", "b"], data: { id: 1 }, id: "x")2623

時間もわずかに短縮:

  • tag.div(text, class: "card")
    1077 ns → 991 ns
  • tag.input(属性3つ)
    1498 ns → 1356 ns
  • 属性5つの tag.span
    3396 ns → 3031 ns

実際のページ描画例(リンク行 20、本フォームとボタンそれぞれ 1)のリクエスト単位では:

  • allocations: 3303 → 3052(約 7.6% 減)

  1. 影響範囲・注意点
  • 影響を受けるユーザー API:
    • tag, content_tag などの HTML タグ生成ヘルパを使うすべてのコード
    • ただし「挙動は一切変わらず、内部的に高速・省メモリになっただけ」
  • 変更されたメソッドは内部実装 (tag_options およびその下請け) のみで、public API・引数仕様・HTML 出力形式(属性の順序含む)は変化なし
  • TagHelper の内部メソッド (tag_option 等) を直接呼んでいる外部コードが理論上は存在し得るが、Rails 本体ではこのファイル外に使用箇所はないと明示されている
  • Boolean 属性や data-/aria- 属性の扱いロジックは変えていないため、disabled: true や data: { controller: ... } のような既存コードはそのまま動作
  • 実装がやや「低レベルな << ベタ書きスタイル」に近づくため、今後同ファイルを修正する際にはバッファ操作の順序やエスケープ処理を壊さないよう注意が必要

  1. 参考情報 (あれば)
  • 対象ファイル:
    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. 概要 (1-2文で)
    number_to_delimited を中心とする数値フォーマット系ヘルパーの内部実装を最適化し、桁区切り処理とフォーマットオプション組み立てのアロケーションを大幅に削減したパフォーマンス改善のPRです。出力結果や既存の挙動は変えずに、メモリアロケーション数・実行時間の両方を減らしています。

  1. 変更内容の詳細

2-1. number_to_delimited の桁区切り処理の最適化 ​

従来の実装:

  • 数値 → 文字列化
  • 整数部と小数部に split で分割
  • 整数部を右から3桁ごとに slice して配列化
  • その配列を join して区切り文字を挿入
  • 符号・小数部などを parts 配列に詰めて compact → join
  • というように「配列を何度も作る・文字列を何度も結合する」構造

このため、1234567 → "1,234,567" にするだけで十数個のオブジェクトが生成されていました。

新しい実装:

  • 整数部の文字列に対して「右から3桁ごとに区切り文字をインプレースで挿入」する形に変更
    • Ruby の Integer#to_s は毎回新しい String を返すので、その文字列をそのまま破壊的に加工しても安全
    • 文字列入力 ("1234567") の場合は一度プレーンな String にコピーしてから同じ処理を実行
  • 小数部については従来どおり 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)2612
number_to_delimited(1234567.89)2716
number_to_delimited("1234567")2815
number_to_currency(1234567.891)8568
number_to_percentage(12.345)8263
number_to_rounded(1234.5678)6045
number_to_human(1234567)184165
100,000 number_to_delimited0.269s0.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 ブランチと出力を比較し、すべて一致

これにより、「高速化したがフォーマット結果が変わってしまう」類のレグレッションがないことを確認しています。


  1. 影響範囲・注意点
  • 対象となる主なヘルパー
    • number_to_delimited
    • number_to_currency
    • number_to_percentage
    • number_to_rounded
    • number_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_* 系) に対する通常の利用であれば、挙動はそのままでパフォーマンスだけ向上する想定

  1. 参考情報 (あれば)
  • 変更ファイル:
    • activesupport/lib/active_support/number_helper/number_converter.rb
    • activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb
    • activesupport/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. 概要 (1-2文で)
    Rails の ActionView::Helpers::TagHelper にある token_list / class_names の内部実装が、余分な配列生成や不要な HTML アンエスケープ処理を減らす形にリファクタリングされました。挙動は一切変えずに、オブジェクト生成数と実行時間を削減するパフォーマンス改善です。

  1. 変更内容の詳細

対象メソッド ​

  • ActionView::Helpers::TagHelper#token_list
  • ActionView::Helpers::TagHelper#class_names(token_list のエイリアス)

Rails 7 以降でよく使われる:

ruby
tag.div(class: class_names("post", featured: true))
tag.div(class: token_list("a b", { c: false }, ["d"]))

のような「クラス名やトークンの組み立てロジック」の内部が最適化されています。

以前の問題点(背景) ​

元の token_list はだいたい次のような流れでした(疑似コード):

  1. 与えられた引数(文字列, 配列, Hash, etc.)をそれぞれ split して配列化
  2. それら配列をさらに flatten して 1 つの配列に
  3. さらに別の配列にユニーク化(重複除去)
  4. 各値を CGI.unescape_html でアンエスケープ(エンティティが含まれているかどうかに関係なく)
  5. 最後に safe_join で結合(この中でも flatten が走る)

つまり、「配列 → flatten → 新しい配列 → さらに flatten → join」という形で、中間配列とオブジェクトが多く生成されていました。また、& を含まない普通の文字列に対しても毎回 CGI.unescape_html をかけており、不要な処理コストがかかっていました。

この処理は class_names も同じ(alias)なので、View でクラス名を組み立てるたびにこのコストが発生していました。

今回の最適化内容 ​

PR の説明をもとにすると、新しい実装は以下のような方針になっています:

  1. split のブロック形式を使って 1 つの配列に直接集約
    Ruby の String#split には、戻り値ではなくブロックに対して分割結果を順次渡す形式があります:

    ruby
    tokens = []
    "a b c".split(/\s+/) { |t| tokens << t }

    これにより「1つの大きな配列を直接埋めていく」ことができ、中間配列や flatten が不要になります。

  2. 重複削除をインプレースで実施
    tokens.uniq! のような形で、同じ配列を破壊的にユニーク化します。
    これにより「元配列 → ユニーク化後の別配列」というコピーが不要になります。

  3. & を含む値だけ CGI.unescape_html
    文字列に & が含まれていなければ HTML エンティティは存在しない前提なので、アンエスケープ不要と判断できます:

    ruby
    if token.include?("&")
      token = CGI.unescape_html(token)
    end

    これで、ほとんどの普通のクラス名("post", "featured", "btn-primary" など)にはアンエスケープ処理をかけずに済みます。

  4. safe_join を使わず、自前でエスケープ+ join
    最終的なトークン配列はすでにフラットなので、safe_join による追加の flatten は不要です。

    • 各トークンを HTML エスケープする(ERB::Util.html_escape 相当)
    • スペースで join(" ") する

    といった処理で同等の結果が得られます。
    重要なのは、「出力結果(HTML エスケープの具合、空白処理、重複削除)」は従来と完全に同じであるように実装されている点です。既存のテストがそのまま通ることが PR 内で明記されています。

パフォーマンスの改善量 ​

PR 内の計測結果(1回あたりのオブジェクト数/またはアロケーション数と思われる指標、および実行時間):

ケース変更前変更後
class_names("post", featured: true)1811
class_names("a b", { c: false }, ["d"])2416
token_list("x")138
100,000 回の class_names 呼び出し0.140s0.108s

高速化率はケースにもよりますが、10〜20% 程度の改善が出ているようです。View で class_names を多用するアプリでは GC 負荷やレンダリング時間がわずかに改善されます。


  1. 影響範囲・注意点
  • 公開 API の挙動は変わらない想定

    • token_list / class_names の入力パターン(文字列、配列、Hash、nil、false 等の扱い)
    • whitespace の扱い(複数スペースを 1 個に潰すなど)
    • 重複トークンの除去
    • HTML エンティティの扱い

    これらは既存テストでカバーされており、「出力は従来と同じ」と明記されています。

  • 既存コード側での修正は不要
    メソッドシグネチャや戻り値の型は変わらないため、アプリケーションコード・ライブラリコード側での対応は不要です。

  • パフォーマンス指向のコードでの違い

    • カスタムで token_list を monkey patch していたり、内部実装に依存したテストを書いているような特殊なケースを除き、互換性の問題はほぼありません。
    • & を含むクラス名(例: "icon-&-text")やエンティティを意図的に埋め込んでいるケースでも、従来と同じ最終出力になるようになっています(必要な場合のみ CGI.unescape_html が呼ばれるだけ)。

総じて、「パフォーマンスが良くなっただけ」と考えて差し支えない変更です。


  1. 参考情報 (あれば)
  • 対象ファイル:

    • actionview/lib/action_view/helpers/tag_helper.rb(+11 / -2 の小さな変更)
  • 関連 API ドキュメント(参考):

    • ActionView::Helpers::TagHelper#tag / #content_tag
    • ActionView::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. 概要 (1-2文で)
    distance_of_time_in_words / time_ago_in_words の翻訳処理から I18n.with_options(内部で OptionMerger を生成)を排除し、代わりに小さなヘルパーメソッド経由で I18n.t を直接呼び出すようにして、パフォーマンスとメモリアロケーションを改善した変更です。
    挙動や使用される翻訳キー・スコープ・ロケールは従来どおりで、I18nのテストスイートで互換性が確認されています。

  1. 変更内容の詳細

背景・問題点 ​

従来の distance_of_time_in_words は、内部で以下のように I18n.with_options を使っていました(イメージ):

ruby
I18n.with_options(locale: locale, scope: :'datetime.distance_in_words') do |locale_scope|
  locale_scope.t(:less_than_x_seconds, count: 5)
  # ...
end

with_options は内部で OptionMerger オブジェクトを生成します。このオブジェクトは、実際に t が呼ばれるたびに

  • 共有オプション(locale, scope など)と
  • 各呼び出し固有のオプション(count など)

をマージした新しい Hash を毎回生成します。
time_ago_in_words は一覧画面などで「1行につき1回」レベルで大量に呼ばれることが多く、この余計なオブジェクト生成コストが無視できない状態でした。

新しいアプローチ ​

このPRでは、I18n.with_options をやめて、ロケールとスコープを直接 I18n.t に渡す小さなプライベートヘルパーを導入しています。

イメージとしては以下のような形です:

ruby
# 例: 新規に導入されたプライベートヘルパー(概念的な例)
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)
end

distance_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 の値
  • locale
  • scope

が I18n に渡される最終的な内容は従来と同一になるように実装されています。
scope: nil が明示的に渡された場合は「スコープなし」として扱われ、デフォルトスコープは fetch 経由で適用される設計になっているため、挙動の互換性も維持されています。

パフォーマンス上の効果 ​

time_ago_in_words を対象に計測した結果:

変更前変更後
1回あたりのオブジェクトアロケーション数2617
100,000 回呼び出し0.467 秒0.361 秒

OptionMerger を経由せずに I18n.t を直接呼ぶことで、1回あたり約35%程度のアロケーション削減、約23%程度の時間短縮が確認されています。
大量の行をレンダリングする画面(Activity feed、コメント一覧、ログ一覧など)で特に効果が期待できます。


  1. 影響範囲・注意点
  • 対象: 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 を前提にした独自コード)を確認する必要があります。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/59113
  • 関連クラス: ActionView::Helpers::DateHelper
    • distance_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. 概要 (1–2文で)
    Active Model / Active Record の属性まわりに「読み取り専用の AttributeSet」を導入し、従来 Ractor 対応のために削除されていたビルダーキャッシュの代わりとして再利用できるようにした PRです。これにより属性インスタンス生成(instantiate)のオブジェクト割り当て数が減り、Active Record のインスタンス生成性能が向上しています。

  1. 変更内容の詳細

背景 ​

  • 以前、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
  • インスタンス生成性能 (instantiate i/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 対応のコストが残っている)。


  1. 影響範囲・注意点

影響範囲 ​

  • 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)

注意点 / 互換性に関するポイント ​

ライブラリ・メタプログラミングをしている開発者が注意すべき点:

  1. AttributeSet / Attribute オブジェクトのミューテーションに依存しているコード

    • 読み取り専用セットや read-only 属性が導入されたため、「Active Record 内部が常にミュータブル」という前提で AttributeSet や Attribute を直接変更するコードは壊れる可能性があります。
    • 特に、内部 API にアクセスして Attribute オブジェクトを差し替えたり、ハッシュを直接書き換えるようなコードはリスクが高いです。
  2. AttributeSet のキャッシュに依存していたコード

    • 以前の「ビルダーキャッシュ」と実装が変わっているため、AttributeSet::Builder の内部構造やキャッシュの仕組みに依存したハックは動作しなくなっている可能性があります。
  3. Ractor / 並行実行環境での挙動

    • この PR 自体が Ractor セーフティを意識しており、読み取り専用オブジェクトの共有を前提としています。
    • 逆に言うと、Ractor 内で Active Record の内部オブジェクトを無理に共有していたコードは、新しい read-only モデルに沿うように見直す必要があります。
  4. 性能評価

    • 本 PR は性能改善を目的としており、特に大量のレコードインスタンス化を行うアプリケーションでは、アップグレード後に instantiate 周りのベンチマークを走らせる価値があります。
    • 以前のバージョンから大きく飛ばしてアップグレードする場合(例: 6.x → 8.x のようなケース)では、Ractor 対応による回帰 + 本 PR の改善をあわせてトータルで計測することをおすすめします。

  1. 参考情報 (あれば)
  • 元 PR: #53344 — read-only AttributeSet の初期提案と議論の履歴
  • 関連 PR: #59097 — Attribute builder のメモ化による instantiate パフォーマンス改善
  • ベンチマークスクリプト(PR に記載)
    自分のアプリケーション or Rails チェックアウトに対して以下のように実行可能:
    bash
    ruby bench.rb /path/to/rails
    これにより、Post.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. 概要 (1-2文で)
    Active Record の各レコードが「必ず」確保していた関連キャッシュ用 Hash を、関連に初めてアクセスしたときに「遅延生成」するように変更した PRです。これにより、関連を触らない多数のレコードを扱うケースで、オブジェクト生成コストとメモリ使用量が削減されます。

  1. 変更内容の詳細

背景・従来の挙動 ​

  • 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 用に「ロード済み関連」の一覧を返すが、キャッシュがないときは「空」と見なす。
  • reload
    • reload 後に返ってくる「新しいレコードインスタンス」が持つ関連キャッシュをそのまま使う。
    • もともと reload は「新インスタンスの状態を採用する」設計のため、今回の遅延生成方針と矛盾しない。

パフォーマンス改善のイメージ ​

PRで示されている簡易ベンチマーク:

  • 対象: 6カラム持つ sqlite モデル
  • 結果:
ベンチマーク変更前変更後
Post.instantiate(row)65
Post.all.to_a (100行)12851185

単位はPR本文だけでは明示されていませんが、相対的な数字として「どちらもわずかだが確実な削減」が見て取れます。関連を全く使わない大量のレコードを扱う場合は、GC負荷の低減なども含めて効いてきます。

サンプル的な理解 ​

以下のようなコードを想定します:

ruby
posts = Post.limit(1000).to_a
# ここでは posts の各レコードで関連に一切アクセスしない
  • 変更前: 1000件分の @association_cache = {} が必ず確保される。
  • 変更後: 関連を触らない限り 1000件どのインスタンスでも @association_cache 自体が生成されない。

一方で:

ruby
post = Post.first
post.comments.to_a
  • post.comments に初めてアクセスしたタイミングで、@association_cache が生成され、その中に :comments の Association オブジェクトが格納される、という流れになります。

  1. 影響範囲・注意点

影響範囲 ​

  • 影響を受けるのは「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 を返しうる」前提の有無を確認する必要があります。

ただし、通常のアプリケーションコードではこういった内部変数を直接触るべきではないため、一般的な利用者にとっては後方互換性は保たれていると考えて問題ありません。


  1. 参考情報 (あれば)
  • PRで影響を受けているファイル:
    • activerecord/lib/active_record/associations.rb
    • activerecord/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. 概要 (1-2文で)
    Active Record の Key 周りで、「キーが自分で値の形(単一値 or タプル)を決める」ための共通メソッド Key#map_value が追加され、これまで各所に散らばっていた composite? ? values : values.first のような条件分岐が集約されました。併せて、複合キーのキャスト処理での不要な配列・オブジェクトの割り当てが削減され、パフォーマンスとコードの一貫性が向上しています。

  1. 変更内容の詳細

2-1. Key#map_value の追加 ​

activerecord/lib/active_record/key.rb に新メソッド Key#map_value が追加されています。PR 説明から推測される役割は次のとおりです。

  • Key インスタンス(単一キー or 複合キー)が、「値をどういう形で返すべきか」 を自分で決める
  • 呼び出し側は composite? を意識せずに、単一キー/複合キーを一律に扱える

イメージとしては以下のような振る舞いです(概念的な例):

ruby
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.rb
  • activerecord/lib/active_record/associations/builder/belongs_to.rb

から、手書きの

ruby
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 で各カラムごとに行っていた不要なオブジェクト/配列の生成が削られています。

典型的には以下のようなイメージです。

変更前のイメージ:

ruby
def cast(values)
  columns.each_with_index.map do |column, i|
    column.cast(values[i])  # 各カラムで新たなオブジェクト生成が起きる/配列ラップなど
  end
end

変更後のイメージ:

ruby
def cast(values)
  # ループ中での中間配列/オブジェクトを極力作らない形に
  # 例: 既存配列を使い回す、block-less map 的な実装など
end

これによって:

  • 複合キーを多用している場合でも、キャストのたびに不要な GC 負荷をかけずに済む
  • 複合キー数 × カラム数に比例していたオブジェクト割り当てが減る

ため、パフォーマンス上のメリットがあります。

2-4. テストの追加 ​

activerecord/test/cases/key_test.rb に 27 行のテストが追加されています。

ここでは主に:

  • Key#map_value の振る舞い
    • 単一キー時にスカラを返すこと
    • 複合キー時に「キーの形」に合った値 (タプル/配列) を返すこと
  • Key::Composite のキャスト結果が期待どおりであること

などが検証されていると考えられます。
これにより、呼び出し側が map_value に依存することに対するリグレッション防止が図られています。


  1. 影響範囲・注意点
  • 影響範囲
    • 内部的には belongs_to アソシエーションと ActiveRecord::Key/Key::Composite 周辺
    • 特に複合キー対応している(あるいはそれに依存した)コードで、値の受け渡し形態が Key#map_value を経由するようになる
  • 公開 API への直接の変更は説明文からは見当たらず、主に内部実装のリファクタリング + パフォーマンス改善です
  • ただし、以下のような場合は注意が必要です:
    • ActiveRecord::Key/Key::Composite を直接触っているメタプログラミング的なコード
    • belongs_to 周りで、戻り値の形(スカラ or 配列)に依存した非公開 API 利用をしているコード
  • 期待されるメリット
    • コードの一貫性向上: 単一キーと複合キーの扱いの差を Key に閉じ込められる
    • メンテナンス性向上: 複合キー仕様変更時の影響範囲が局所化
    • パフォーマンス改善: 複合キーキャスト時の無駄な割り当て削減

通常のアプリケーションレベルでは、「挙動が変わる」というよりは「内部の実装が賢くなった」という性質の変更と考えてよいです。


  1. 参考情報 (あれば)
  • この PR は #59063 に続く変更で、#59063 では「余計な配列割り当ての削減」が行われ、この PR では「条件分岐の集約とさらなる割り当て削減」が行われています。
  • Rails で複合キーを(公式にはフルサポートしていない形で)扱っているプロジェクトやプラグインは、このあたりの内部 API に依存している場合があるため、アップグレード時には Key 周りを中心にテスト強化しておくと安全です。

#59074 Don't match through associations by direct keys ​

マージ日: 2026/10/15 | 作成者: @matthewd

  1. 概要 (1-2文で)
    Rails の has_many :through / has_one :through など「through association」に対して、関連オブジェクトを紐付ける際に「中間(source)の外部キー」を使って直接マッチングしようとしていた処理を無効化し、常に通常のロード経路を使うようにした修正です。これにより、自己参照モデルなどで、たまたま同じカラム名があるせいで誤った関連レコードが紐付くバグを防ぎます。

  1. 変更内容の詳細

問題の背景 ​

through な関連 (例: has_many :comments, through: :posts) の場合、

  • オーナー(owner): User など関連元のモデル
  • 中間(source): Post
  • ターゲット(target): Comment

という 3者の関係になります。このとき、オーナーとターゲットは直接は結びついておらず、中間モデルを経由して論理的に関連付いている状態です。

ところが、Active Record 内部で次の 2 つの処理が、中間(source)の外部キーを使って owner と target を「直接」マッチングしようとしていました。

  1. プリロード時に available_records を再利用する処理(#42654)
  2. クエリで読み込んだレコードに 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 を使った高速マッチングを行わない」と宣言するロジックが入ります。

擬似コードイメージ:

ruby
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 の外部キーで直接マッチングしないようにするための制御が追加されています。

擬似コードイメージ:

ruby
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.rb
  • activerecord/test/cases/associations_test.rb

に、自己参照や through を絡めたケースで「誤ったマッチングが発生しない」ことを検証するテストが追加されています。

おそらく以下のようなシナリオのテストが含まれていると考えられます:

ruby
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 に紐づいてしまうケースがあった。
# 今回の修正で、それが起きないことをテストしている。

  1. 影響範囲・注意点

影響範囲 ​

  • 対象: has_many :through, has_one :through など「through association」全般
  • 関連する内部機構:
    • association preloader (includes, preload, eager_load など)
    • inverse association の自動設定 (inverse_of を明示しない場合の推測を含む)

実際の影響 ​

  • 正の影響:

    • 自己参照モデルや、外部キー名が衝突しがちな複雑な関連構造で、誤ったレコードが関連に紐付くバグが解消されます。
    • バグとしては「たまにおかしなレコードが紐付いている」程度にしか見えないため、潜在的なデータ不整合を減らせる点は重要です。
  • 性能面の影響:

    • through association については、
      • preload 時の available_records 再利用
      • inverse 設定時のキー比較マッチング
        という 2 つの小さな最適化を捨てているため、ごく一部のケースでわずかにオーバーヘッドが増える可能性はあります。
    • とはいえ、through で正しくないマッチングを避けることの方が優先されており、
      「体感できるほどの性能悪化」はほとんどのアプリでは起こらないと考えられます。

既存アプリ側の注意点 ​

  • もし独自に ActiveRecord の内部 API (preloader / association / inverse 処理) を monkey patch している場合、
    • ThroughAssociation が「available_records を使わない」
    • inverse の自動設定に関与しない
      という前提に変わっていることを意識する必要があります。
  • 通常のアプリケーションコードで has_many :through / has_one :through を使っているだけであれば、コード変更は不要で、バグ修正のみの影響と考えてよいです。

  1. 参考情報 (あれば)
  • この PR の説明で触れられている関連 PR:
    • #42654: preloader の available_records 再利用周りの改善
    • #40042: inverse association の自動設定ロジックの改善
  • 関連して発生しうるパターン:
    • 自己参照関連 (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. 概要 (1-2文で)
    String#truncate の結果文字列を「1回のコピーで」生成するようにして、余計な文字列生成を減らすパフォーマンス改善 PR です。動作仕様(戻り値の内容・エンコーディング・freeze 状態)は変えずに、内部実装のみを最適化しています。

  1. 変更内容の詳細

何をしているか ​

従来の String#truncate は、おおよそ次のような流れでした:

  1. 元の文字列から先頭 N 文字を slice する
  2. そのスライス結果と省略記号(デフォルト "...")を、文字列補間などで新しい文字列にまとめる

つまり、スライス結果 (1つ目の新しい文字列) と 最終結果 (2つ目の新しい文字列) の2段階で文字列を生成していました。

今回の変更では:

  • すでに slice した時点で「新しい・変更可能な文字列」が手に入っているので、
  • そのスライス結果に対して << で省略記号を後ろに足す ことで、追加の文字列オブジェクトを作らずに 完結させています。

概念的なイメージ:

ruby
# 以前のイメージ
truncated = self.slice(0, length)     # 新しい String
result    = "#{truncated}#{omission}" # さらに新しい String を作る

# 今回の変更イメージ
result = self.slice(0, length) # 新しい String
result << omission             # その String に追記して完成

これにより、最終的に返されるオブジェクトは:

  • 非 frozen の文字列であること
  • レシーバ (self) と同じエンコーディングを持つこと

といった既存仕様を保ったまま、オブジェクト生成数だけを削減しています。元々のテストがこれらの性質を検証しているため、挙動の互換性もテストで担保されています。

パフォーマンス指標 ​

108文字の文字列で計測した例:

メソッド呼び出し変更前 (オブジェクト数)変更後 (オブジェクト数)
truncate(30)32
truncate(30, separator: " ")43

Rails のリストビューなどで「1行ごとのタイトルを truncate する」ようなケースだと、行数が多いほどこの差が効いてきます。


  1. 影響範囲・注意点
  • 対象メソッド
    • ActiveSupport の String#truncate(activesupport/lib/active_support/core_ext/string/filters.rb)のみ。
  • 仕様変更の有無
    • 返り値の内容、エンコーディング、凍結状態など外から見える挙動は変わりません。あくまで内部の生成コスト削減です。
  • ミューテーションに関する注意点
    • truncate が返す文字列は以前から「新しく生成された変更可能な文字列」でしたが、今回もその前提は維持されています。
    • 戻り値に対して << などで追記しても元のレシーバは変更されません(slice によって新規オブジェクトが返ってくるため)。
      この点も以前と同じです。
  • パフォーマンス影響
    • レコード数が多い index ページやログ出力など、truncate が大量に呼ばれる場面で GC 負荷とオブジェクト数がわずかに減ることが期待できます。
    • 単発の呼び出しでは体感しづらいですが、ホットパスでの細かい最適化としては妥当な変更です。

  1. 参考情報 (あれば)
  • 対象ファイル:
    • 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. 概要 (1-2文で)
    belongs_to / has_many :through に primary_key: を指定している場合に、グループ集計 (group(...).count) および through 経由での build 時に、そのカスタム主キーを正しく参照するように修正した PR です。これにより、従来 nil キーになってしまった集計結果や、誤った外部キーがセットされる問題が解消されます。

  1. 変更内容の詳細

2-1. grouped calculations でのカスタム primary key 対応 ​

問題点:
group(:belongs_to_association).count のように関連を指定して group 集計を行う際、関連側に primary_key: が設定されていても、それを無視して対象モデル自身の primary_key を見に行っていたため、結果がすべて nil キーになるなど、正しく結果をマッピングできていませんでした。

イメージ:

ruby
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: が無視されていました。

イメージ:

ruby
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 を使うべきか」を判断できるようになったと推測されます。

  1. 影響範囲・注意点
  • 影響を受けるのは主に次のパターンです:

    • 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 を多用しているアプリケーションほど、このあたりの関連操作・集計ロジックに対して回帰テストを実行することを推奨します。


  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/59079
  • 関連する Rails ガイド:
    • Active Record Association — primary_key / foreign_key オプション
    • Active Record Querying — Grouped Calculations (group, count, sum など)
  • カスタム primary key を使うモデルや関連を多用している場合、以下のようなケースで実際に動作確認しておくと安心です:
    • Model.group(:belongs_to_association).count
    • has_many :through な関連に対する build / create の挙動(外部キーの値が期待通りか)

#59076 Fix type matching in inverse polymorphic queries ​

マージ日: 2026/10/15 | 作成者: @matthewd

  1. 概要 (1-2文で)
    where(as_association: relation) で生成される「ポリモーフィック関連の型条件」に関する2つのバグが修正されています。特に、(1) 明示的な type 条件があっても無視されるケースと、(2) 抽象クラスなどで polymorphic_name が食い違うケースで誤った type が使われる問題が解消されました。

  1. 変更内容の詳細

修正点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") ですが、以下のようなカスタマイズが可能です:

ruby
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 値でクエリされてしまい、レコードが取得できない / 間違ったレコードになる可能性がありました。

この PR では:

  • TableMetadata や AssociationQueryValue を修正し、
    where(as_association: relation) が使う type 条件生成時にも owner の polymorphic_name を使う ように統一しています。
  • これにより、
    • 抽象クラス上にポリモーフィック関連を宣言している場合
    • サブクラスが polymorphic_name をオーバーライドしている場合
      でも、ロード・JOIN・as_association 経由の条件が全て同じ type 値を使うようになり、一貫した挙動になります。

  1. 影響範囲・注意点
  • 対象となる機能:

    • ActiveRecord::Relation#where(as_association: relation) を使った inverse polymorphic query(関連を元に逆方向の検索をするようなケース)
    • ポリモーフィック belongs_to を使っているアプリケーション全般だが、特に:
      • 抽象クラスに関連を定義している
      • polymorphic_name を独自にオーバーライドしている
      • type カラムに対して属性エイリアスを使っている(例: belongs_to :owner, polymorphic: true で owner_type を別名扱いしている)
  • 実務で意識すべき点:

    1. これまで「なぜかポリモーフィック関連を通した where が期待通りにヒットしない」「as_association を使うと結果が変になる」といった症状があった場合、この修正で直る可能性が高いです。
    2. 逆に、バグに依存した挙動(例えば:
      • 実際は polymorphic_name を変えているのに、宣言元抽象クラスの名前で保存・検索されていた
      • 明示的 type 条件を付けたつもりが、さらに Rails 側デフォルトが上乗せされていた )を前提にしたコードがあると、挙動が変わる可能性があります。
    3. テストでは where(as_association: ...) を使うケースで:
      • 属性エイリアスを通した type 指定
      • abstract class + subclass override な polymorphic_name
        などがカバーされているので、自前のアプリでも同様のパターンがあればテスト追加を検討すると安心です。

  1. 参考情報 (あれば)
  • 関連する内部クラス/モジュール:
    • ActiveRecord::Relation::PredicateBuilder
    • ActiveRecord::Relation::PredicateBuilder::AssociationQueryValue
    • ActiveRecord::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. 概要 (1-2文で)
    find_by(association: record) が複合主キー+PostgreSQLの配列カラムを含む場合に、バインド値が誤ってフラット化されるバグを修正し、既に正しく動いている where と同じ処理経路にフォールバックさせる変更です。これにより、配列型を含む複合キーを持つ関連オブジェクトを使った検索でも、配列が正しく1カラム分の値として扱われます。

  1. 変更内容の詳細

背景 ​

  • #50412 で、find_by(association: record) のように「関連オブジェクトそのもの」を渡した場合に、その関連の複合主キーからタプル(複数カラムの検索条件)を組み立てる機能が入った。
  • しかし、その実装では「値の妥当性チェック(unsupported-value check)」がスキップされており、
    • 複合キーの一部に「PostgreSQLの配列カラム」が含まれるケースで、
    • その配列が1つの値として扱われず、「後続カラムのバインド変数まで巻き込んでフラット化」されてしまう問題があった。
  • 通常の where ではこの問題は既に考慮されており、配列型を含む値も正しく扱えている。

本PRでの対応 ​

要約すると:

「複合キーを組み立てる特別扱いの find_by パスで、配列型コンポーネントをうまく扱えていないなら、そのケースでは無理に特別処理をせず、すでに安全な where 相当のパスにフォールバックする」

という方針の修正です。

コード変更点 ​

  • activerecord/lib/active_record/core.rb

    • find_by(もしくはその内部で呼ばれるクエリ生成ロジック)に1行追加され、
      • 複合キーからタプルを組み立てる際に「値がサポート外かどうか」をチェックし、問題がありそうな場合は特別処理をやめて標準の where に任せるようになっています。
    • 結果として、配列カラムなど「そのまま1値として扱うべきもの」がタプル構築の中で誤って分解されるのを防いでいます。
  • activerecord/test/cases/adapters/postgresql/array_test.rb

    • PostgreSQL アダプタ向けの配列テストに 11 行追加。

    • 想定される形としては、次のような構造のテストです(擬似コード):

      ruby
      class 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
    • 実際にはより具体的なモデル・スキーマに基づいていますが、
      「関連オブジェクト+複合キー+配列カラム」が絡むパターンをカバーするテストが追加されています。


  1. 影響範囲・注意点
  • 影響範囲:

    • 次の条件が揃うコードで挙動が変わります。
      • Active Record モデルに**複合主キー(または複数カラムからなる関連キー)**がある。
      • その複合キーの一部が PostgreSQL の配列型カラム である。
      • find_by(association: record) のように、関連オブジェクトを直接渡して検索している。
    • これまでは配列がフラットに展開されて誤ったSQL / バインドになり、
      • 想定通りにレコードが見つからない、
      • 稀にSQLエラーや不正な条件になる
        などのバグが起こり得ました。
    • 本PRにより、そのようなケースは安全な where 相当のロジックにフォールバックし、
      • 配列カラムは1カラム分の値として保持され、
      • where と同等の挙動で検索されます。
  • 注意点:

    • 挙動の変更点は「壊れていたケースが正しくなる」方向であり、
      既存の正常に動いていたケースへの副作用は最小限になるよう配慮されています。
    • もしPostgreSQL配列カラムを複合キーや関連の一部として利用しており、
      find_by(association: record) を使っている場合は、
      • テストを追加・実行して、期待通りにレコードが取得できるかを確認する価値があります。
    • 逆に、単なるスカラー値(integer, string など)だけの複合キーや find_by の利用には、
      今回の変更の影響はほぼありません。

  1. 参考情報 (あれば)
  • 対応元となった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. 概要 (1-2文で)
    ObjectSpace::WeakKeyMap をラップする内部用の ActiveSupport::WeakKeyMap クラスが追加され、これまで LeaseRegistry(connection_pool 内部)側に散在していたランタイム依存の同期処理が ActiveSupport に一本化されました。これにより、WeakMap 利用箇所で同じ同期ロジックを再利用しやすくなり、今後の利用拡大に備えた基盤整備が行われています。

  1. 変更内容の詳細

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 で抽象化する」流れの一環です。

典型的な利用イメージ(概念的な例):

ruby
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 around ObjectSpace::WeakKeyMap, instead of LeaseRegistry carrying its own.
(I have a new call-site coming, this isn't just moving for the sake of it.)

とあるため:

  • これは単なるコード整理ではなく、ActiveSupport::WeakKeyMap を別の場所からも使う予定がある前提の基盤整備
  • 同じような WeakMap 利用箇所が増える見込みで、それら全てで統一された同期・互換性処理を適用できるようにしている

と理解できます。


  1. 影響範囲・注意点

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 内部で統一的に行いやすくなる点は、開発者・運用者双方にメリットがあります。


  1. 参考情報 (あれば)
  • この PR:
    • #59071: Define an internal ActiveSupport::WeakKeyMap
  • 関連 PR:
    • #59039: ActiveSupport::WeakSet の追加(同様に Weak 構造を ActiveSupport で抽象化する取り組み)
  • 関連クラス・機能:
    • ObjectSpace::WeakKeyMap(Ruby 標準の WeakMap 相当機能)
    • ActiveRecord::ConnectionAdapters::ConnectionPool および内部の LeaseRegistry
    • ActiveSupport::WeakSet(WeakKeyMap と同じ思想のラッパー)

#59109 Skip the %Z substitution in TimeWithZone#strftime when the format has none ​

マージ日: 2026/10/15 | 作成者: @amatsuda

  1. 概要 (1-2文で)
    ActiveSupport::TimeWithZone#strftime が、フォーマット文字列に %Z が含まれない場合には置換処理をスキップするようになり、挙動は変えずにメモリアロケーションと実行時間を削減するパフォーマンス改善です。

  2. 変更内容の詳細(サンプルコード含む)

  • 従来の実装

    • 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" のようなエスケープされたシーケンスを含むケースでの挙動(既存テストでカバー済み)は変わらないことが明示されています。つまり、仕様面の振る舞いは完全に従来どおりです。

  • パフォーマンス計測結果(TimeWithZone in UTC)

    • オブジェクトアロケーション数:
      • strftime("%d %b %H:%M") : 7 → 4
      • to_fs(:short) : 6 → 2
      • strftime("%H:%M %Z") : 11 → 11(%Z を含むケースは変化なし)
    • 実行時間 (300,000 回呼び出し):
      • 0.184s → 0.147s

    %Z を使わないフォーマットが一般的であるため、実運用では多くのビュー / I18n フォーマットで恩恵があります。

  1. 影響範囲・注意点
  • 影響範囲

    • ActiveSupport::TimeWithZone#strftime を経由する日時フォーマット全般:
      • I18n の config/locales/*.yml に定義された日付・時刻フォーマット
      • to_fs / to_formatted_s 経由のフォーマット
      • アプリケーションコード内で Time.zone.now.strftime(...) などを呼び出している箇所
    • %Z を含まないフォーマットでメモリ使用量・速度が改善されます。
    • %Z を含むフォーマットや、"%%Z" のようなエスケープケースでは挙動・性能ともに従来と変わりません。
  • 注意点

    • 仕様の変更ではなく最適化のみであり、フォーマット結果(出力文字列)は全ケースで従来と同一になることが意図されています。
    • %Z の判定はシンプルな "format.include?("%Z")" タイプのチェックのため、%Z を含むか否かに関するユーザーの期待と食い違うケースは基本的にありません。
    • 既存テスト(特に "%%Z" を含むパターン)で互換性が確認されているため、通常のアプリケーションコード側で特別な対応は不要です。
  1. 参考情報 (あれば)
  • 対象メソッド: 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. 概要 (1-2文で)
    Enumerable#index_with の内部実装を、to_h 経由ではなくシンプルな each ループでハッシュを構築する形に変更し、挙動を変えずにオブジェクト割り当て数と処理速度を改善するPRです。特に Active Model の serializable_hash など、JSON シリアライズ時のホットパスでメモリアロケーションを削減します。

  1. 変更内容の詳細

これまでの実装(問題点) ​

Enumerable#index_with は、内部的に to_h + ブロックで実装されていました。
概念的には、以下のようなイメージです:

ruby
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 と代入で書き直し、ペア配列を介さずに直接ハッシュに代入するように変更しています。イメージは以下のような形です:

ruby
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 allocations1 allocation
index_with(default)7 allocations1 allocation
500,000 回呼び出し0.353s0.332s

また、26カラム × 20レコードの JSON を生成するケースで、1リクエストあたりの割り当て数が 1332 -> 1206 に減少しています。
時間的な改善は数パーセントですが、アロケーション削減効果が大きく、GC 負荷軽減にもつながります。


  1. 影響範囲・注意点
  • 挙動の互換性

    • 公開 API (Enumerable#index_with) の仕様・戻り値は変更されていません。
    • 既存のテストスイートのみでカバーされており、追加の挙動変更はありません。
    • そのため、アプリケーションコード側での修正は不要です。
  • パフォーマンス面の影響

    • ActiveSupport を通じて Enumerable#index_with を多用しているコード、特に:
      • Active Model / Active Record の serializable_hash
      • JSON レスポンス生成(大量レコードの index を組み立てるようなコード) などで、オブジェクト割り当て数削減とわずかな速度改善が期待できます。
    • 小さな配列・低頻度な呼び出しでは体感差はほぼありませんが、大量データや高トラフィック環境で積み上がるタイプの最適化です。
  • 互換性リスク

    • 実装詳細に依存したメタプログラミング(index_with 内部での to_h 呼び出しを前提とするようなコード)がもしあれば影響を受ける可能性はありますが、通常の利用ではそのような依存はまず無いはずです。
    • Ruby のバージョン依存の挙動変更も伴っておらず、ActiveSupport の範囲内の微修正です。

  1. 参考情報 (あれば)
  • 対象メソッド: 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. 概要 (1-2文で)
    polymorphic_url / polymorphic_path / url_for(record) などの「レコードを渡すURLヘルパ」の内部実装を整理し、不要な配列・ハッシュの生成を減らしてパフォーマンスを改善したPRです。生成されるURLそのものの挙動・仕様は変えずに、オブジェクト割り当て数を削減するリファクタリングになっています。

  1. 変更内容の詳細

2-1. 何を問題視していたか ​

従来の実装では:

  • polymorphic_url(record, options = {})
  • polymorphic_path(record, options = {})
  • url_for(record)

などが内部で次のような非効率なフローを辿っていました。

  1. polymorphic_url / polymorphic_path で、中身が空でも options ハッシュを複製していた
  2. HelperMethodBuilder.polymorphic_method から handle_model を呼び出し、
    • 引数リスト (arguments list)
    • [method, args] という2要素配列
      の両方を受け取る
  3. しかし、その後 named route helper を呼び出すときに、それらを再度 splat展開 (*args) して使い直しているだけだった
  4. url_for(record) も同じ handle_model 経由で処理

つまり、「中間配列や重複したハッシュの生成 → すぐにばらす」という無駄なメモリアロケーションが、全ての link_to record, button_to record で毎回発生していました。

2-2. どう改善したか ​

主な変更点は以下です。

  1. モデルケース専用のディスパッチに変更

    • モデル(ActiveRecord等)が渡された場合、polymorphic_url / polymorphic_path / url_for は
      直接 dispatch_model を通じて 対応するnamed route helperを即時呼び出すようにしました。
    • これにより、
      • 中間的な handle_model 呼び出し
      • [method, args] のペアを作る → すぐ展開する
        という余計なステップを削除。
  2. handle_model の削除

    • 旧来の中心メソッド handle_model は、他に呼び出し元がなくなったため削除されました。
    • 代わりに dispatch_model / handle_model_call というよりシンプルな経路で処理する構造に。
  3. options ハッシュの複製タイミングの見直し

    • エントリポイント側 (polymorphic_url / polymorphic_path 等) では、
      • これからキー削除などミューテーションを行う場合のみ options を dup するようにしました。
    • もともと空の options まで一律に複製していた無駄を削除。
  4. URLの挙動は完全維持

    • 現行のテストスイート
      • polymorphic routes
      • URL helper
      • form helper
        によって、生成されるURLやパスの 挙動が変わっていないことを確認しています。
    • つまり「インターフェースも挙動も変えずに内部実装だけを改善」する変更です。

2-3. アロケーション削減量 ​

resources :posts にネストされた comments があるケースでの、1呼び出しあたりのオブジェクト割り当て数は以下のように減少しています。

呼び出しbeforeafter
polymorphic_path(post)129
polymorphic_url(post)1714
polymorphic_path(new_post)118
edit_polymorphic_path(post)1412
url_for(post)1513
polymorphic_path([post, comment])1918

配列ベースのポリモーフィック ([post, comment]) の場合は、ほぼそのまま(1個減)で、単一モデルを渡すパターンでの削減が主眼になっています。


  1. 影響範囲・注意点
  • 挙動の変更は基本的に無し

    • 既存の polymorphic_url, polymorphic_path, url_for(record), form_for, form_with など、モデルを渡してURLを生成する箇所で URLの文字列表現は変わりません。
    • 外部APIとしてのインターフェース (引数、オプション) にも変更はありません。
  • 影響範囲

    • ActionDispatch::Routing::PolymorphicRoutes に依存する全てのヘルパーに内部的に関係します。
      • link_to post
      • button_to post
      • form_with model: post
      • polymorphic_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. 概要 (1-2文で)
    Rails の per-form CSRF トークン生成まわりの処理を見直し、主にオブジェクト割り当て(allocation)を大きく削減してパフォーマンスを改善した PR です。CSRF トークンの生成・検証処理の振る舞いは変えずに、URI 解析・Base64 デコード・ハッシュ構築などを効率化しています。

  1. 変更内容の詳細

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 トークンをリセットした際は、このメモも無効化されるようにし、その挙動をテストで保証

イメージ的には、コントローラにこんな感じのキャッシュが増えていると考えてよいです(擬似コード):

ruby
def decoded_csrf_token(encoded)
  @decoded_csrf_tokens ||= {}
  @decoded_csrf_tokens[encoded] ||= Base64.strict_decode64(encoded)
end

2-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) の大部分はこれに関連しています。


  1. 影響範囲・注意点

振る舞いは基本的に互換 ​

  • CSRF トークンの生成・検証の 外部仕様(トークンの構造・検証ロジック) は変えていません。
  • form_with, button_to など、per-form CSRF トークンを利用するフォームヘルパーの API は後方互換です。
  • token_tag が内部的に merge していた authenticity_token 情報は、そもそも誰も読んでいなかったため、外部コードへの影響はありません。

影響しうる潜在的ポイント ​

開発者視点で注意するならこの辺です:

  1. Action の正規化ロジック

    • normalize_action_path の fast path 追加により、
      「これまで偶然 URI.parse を経由することに依存した挙動」をしていた非常にレアなケースがあるとすると、影響しうるのはここです。
    • とはいえ fast path は「URI 的に有効な絶対パス」だけに限定されており、URI.parse してもそのまま返る文字列にしか適用されません。
      → 変化が出る余地はほぼありません。
  2. CSRF トークンの decode キャッシュ

    • コントローラインスタンス (リクエスト単位) でのメモ化なので、マルチスレッド・マルチリクエスト環境でも状態が共有されることはありません。
    • reset_authenticity_token 的な処理を独自に呼んでいるアプリでも、トークンリセット時にキャッシュが無効化されることがテストで確認されています。
  3. パフォーマンスへの体感

    • 大量のフォームを含むページや、高頻度で CSRF トークンを生成・検証するエンドポイントでは、GC 負荷の低減やスループットの向上が見込めます。
    • 指標として PR で示されているのは:
      • フォーム 1 個あたりの per-form トークン生成: 27 -> 10 allocations
      • グローバルトークン: 15 -> 8
      • per-form トークン検証: 32 -> 17
      • 100,000 per-form トークン生成: 0.82s -> 0.62s
      • button_to 20 個の index ページ: 3927 -> 3490 allocations / request

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/59106
  • 主に変更されたファイル:
    • actionpack/lib/action_controller/metal/request_forgery_protection.rb
    • actionpack/lib/action_controller/base.rb
    • actionpack/test/controller/request_forgery_protection_test.rb
    • actionview/lib/action_view/helpers/navigation_helper.rb
  • この PR は「per-form CSRF トークン有効時のコスト削減」が主眼で、セキュリティモデルや API 互換性は維持されています。

#59058 Reduce allocations across URL generation ​

マージ日: 2026/10/15 | 作成者: @amatsuda

  1. 概要 (1-2文で)
    Rails の URL 生成処理(*_path / *_url / polymorphic helpers / url_for)において、オブジェクト確保(allocation)を減らすための最適化を行った PRです。生成される URL の文字列自体は一切変えずに、メモリ効率とパフォーマンスを改善しています。

  1. 変更内容の詳細

2-1. Journey::Format#evaluate の最適化 ​

URL パス部分の組み立てを行う Journey::Format#evaluate で、以下のような無駄なメモリアロケーションが削減されています。

  • 以前は:
    • parts 配列を dup してから処理
    • パラメータを escape するたびに新しい文字列を作成
    • ネストしたオプションセグメントごとに join などで中間文字列を生成
  • 変更後:
    • 一つのバッファ文字列に対して「追記」していく形に
    • オプションセグメントは「含めるべきパラメータが存在するか」を事前チェックし、なければ何も追記しない(従来の 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 に余計なスラッシュを付けない)

コードイメージ:

ruby
# 従来: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: :json
  • format: ""(空文字)
  • 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 を削減できます。


  1. 影響範囲・注意点
  • 互換性:
    • 「生成される 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 を使用していない通常のアプリケーションでは、単に高速化・省メモリになります。

  1. 参考情報

PR 内のベンチマーク結果(1 回の呼び出しあたりの allocation 数、resources :posts do resources :comments end なルーティングセットを対象):

Callbeforeafter
post_path(post)1410
post_url(post)2521
post_url(post, format: :json)4921
polymorphic_path(post)1914
polymorphic_path([post, comment])2721
url_for(post)2823

特に 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. 概要 (1-2文で)
    Rails のコントローラ / ビューの設定 (config) を、リクエストごとに必ず複製していた挙動を「初回アクセス時にだけ複製する」ように変更し、不要なオブジェクト生成を減らしてパフォーマンスを改善する PR です。既存の API 振る舞いは維持しつつ、特にログ出力だけを行うような軽いアクションでのオブジェクト割り当て数が削減されています。

  1. 変更内容の詳細

背景: 何が問題だったか ​

  • コントローラには「クラスレベルの config」と「インスタンスレベルの config」があり、InheritableOptions で継承されます。
  • ビュー (ActionView::Base) 側にもコントローラから継承した config があり、これも InheritableOptions。
  • 従来の挙動:
    • 各リクエストのレンダリング時に:
      1. コントローラのインスタンス用 config のコピー
      2. ビュー用の 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.rb
  • actionview/lib/action_view/helpers/controller_helper.rb

従来 ​

  • ビューインスタンス生成時 (assign_controller など) に、必ず「ビュー用 config のコピー」が eagerly (即時) 生成されていた。
  • このビュー用 config は親 (コントローラ) の config を元に InheritableOptions で複製される。

変更後 ​

  • ビューの config コピーは view.config が最初に読まれたタイミングで初めて生成 されるようになった。
  • 具体的な性質:
    1. ビュー生成後、config に一度もアクセスしない場合
      → ビュー用 config は一切生成されない。
    2. view.config を初めて読む時点で:
      • その瞬間の「コントローラの config の内容」を元にビュー用 config を作る。
      • つまり、「ビュー生成後〜最初の view.config 読み込みの間」にコントローラの config が変更されていれば、その変更をちゃんと反映した状態でビュー用 config が作られる。
    3. ビュー側で view.config.some_option = ... などと書き換えた場合:
      • その書き込みは ビュー自身の config コピーにだけ 反映され、コントローラ側には影響しない(従来通りの isolation を維持)。

テスト (actionview/test/template/test_case_test.rb) では次を確認:

  • ビュー作成後にコントローラの config を変更しても、view.config 初回アクセス時にはその更新を反映した値が見える。
  • ビュー側で config を変更しても、コントローラ側に「漏れない」(view 専用のコピーに閉じている) こと。

  1. 影響範囲・注意点

パフォーマンス面 ​

PR の記載による割り当て数の変化 (ActionController::Base 経由):

ケース変更前 allocations変更後 allocations
render plain:133125
render inline: + link_to / tag ヘルパー使用212204
logger を読むだけの head :ok アクション9187
  • 軽量な API エンドポイントや、単純な render plain: のようなケースでオブジェクト生成が減少。
  • asset 系ヘルパーなど「ビューの config を読む」ケースでは、初回アクセス時に結局両方のコピー(コントローラインスタンス用、ビュー用)が作られるため、「config を使うページ」の挙動やコストはほぼ従来通り。

互換性・注意点 ​

  • API の表面的な挙動は維持 されています:
    • controller.config / view.config の値の見え方、継承関係、ビュー側の書き込みがコントローラに影響しない点などは変わらないようにテストで担保。
  • 挙動が変わる可能性のある「グレーゾーン」:
    • これまでは「ビュー生成と同時に config コピーが作られていた」ため、非常にタイミング依存なコード(たとえば「ビュー生成直後にコントローラの config を書き換えて、ビューのほうには反映させたくない」など)を書いていた場合、観測されるタイミングが変わる可能性があります。
      • ただし、通常のアプリではまずやらないパターンで、PR では逆に「ビュー生成後のコントローラ config 変更がビューにも反映される」ことを明示的に保証しています。
    • インスタンス config の存在チェックに依存したメタなコードを書いている場合も挙動が変わります:
      • 例: 「logger を一度でも読んだらインスタンス config が作られる」と仮定して controller.instance_variable_defined?(:@_config) などを見ていた場合、この PR 以降は作られないままになるケースがあります。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Rails の ActionDispatch::Request において、host/raw_host_with_port の取得結果をリクエスト単位でメモ化し、かつポートが含まれない場合はポート除去用の正規表現処理をスキップすることで、ホスト名取得まわりのオブジェクト生成と処理コストを削減する変更です。併せてテスト用リクエスト (TestRequest) が host= / port= で値を変えたときにメモ化された値もリセットされるようになりました。

  1. 変更内容の詳細

2-1. host / raw_host_with_port のメモ化 ​

これまで:

  • request.host
    • 毎回 env (HTTP_HOST, X-Forwarded-Host など) からホスト文字列を組み立て
    • 毎回「ポート番号を取り除く」ための正規表現 (gsub 相当) を実行
    • ポートが付いていなくても String と MatchData が毎回生成される
  • 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 に対して:

    ruby
    request.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= で、すでに読んだ後に値を変更しても正しく再解決されること

  1. 影響範囲・注意点
  • 影響範囲

    • ActionDispatch::Request#host / #raw_host_with_port を利用する全機能
      • URL ヘルパー (url_for, *_url)
      • リダイレクト (redirect_to)
      • Cookie ドメイン判定
      • config.hosts によるホスト認可
    • テストコードで 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

    数字は「オブジェクトアロケーション数」と見てよい程度の粒度で、ホスト取得周りのコストがわずかに減っていることを示しています。高トラフィック環境では GC 負荷の微減が期待できます。


  1. 参考情報 (あれば)
  • 対象クラス/モジュール
    • actionpack/lib/action_dispatch/http/request.rb
    • actionpack/lib/action_dispatch/http/url.rb
    • actionpack/lib/action_dispatch/testing/test_request.rb
  • テスト
    • actionpack/test/dispatch/request_test.rb
    • actionpack/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. 概要 (1-2文で)
    controller_class_for が毎回パラメータ文字列を underscore / camelize していた処理を、「パラメータ → コントローラ定数名文字列」の対応をキャッシュすることで高速化した PR です。キャッシュしているのは定数そのものではなく「名前文字列」なので、クラスリロードの安全性を保ったままリクエストあたりの文字列変換を削減しています。

  1. 変更内容の詳細

背景となる問題 ​

  • controller_class_for は、:controller パラメータからコントローラクラスを解決するために、毎回:
    • underscore
    • camelize などの inflector による文字列変換を行っていました。
  • ルーティングのパラメータエンコードのチェックの都合で、1リクエスト中に同じパラメータに対して複数回呼ばれ、結果として
    • 「1リクエストあたり 8 回分の inflector 呼び出し」 が発生していました。
  • これはパラメータ値にのみ依存する純粋関数的な変換なので、結果をキャッシュすれば再計算は不要です。

具体的な変更点 ​

1) 「パラメータ → 定数名文字列」のキャッシュを導入 ​

  • キャッシュ単位:
    • キー: params[:controller] の値(例: "users", "admin/users" など)
    • 値: 解決されたコントローラの定数名文字列(例: "UsersController", "Admin::UsersController")
  • キャッシュ格納場所:
    • Ractor ごとの Concurrent::Map
      → Ractor ローカルにしているのは、Ractor 間での共有を避けつつスレッドセーフなキャッシュにするためと思われます(Concurrent::Map はスレッドセーフな Hash 互換)。

イメージ的には、Rails 内部では次のようなキャッシュが保持されるようになります:

ruby
# 擬似コードイメージ
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 # ここは毎回実行する
end

2) キャッシュは「名前文字列」だけで、定数は毎回 constantize ​

  • キャッシュしているのは「"Admin::UsersController" のような文字列」であり、クラスオブジェクトそのものではありません。
  • そのため:
    • クラスリロードに強い:
      • 開発環境などで
        • 新しいコントローラが定義された
        • 既存のコントローラクラスが削除・再定義された
      • といった場合でも、constantize が毎回実行されるため、常に最新の定義に解決されます。

3) キャッシュミス時の挙動と上限について ​

  • キャッシュに存在しないパラメータが来た場合:
    • 従来と同じ処理を行って名前を導出し、その結果だけをキャッシュする。
  • 注目点:
    • 解決に成功した名前のみキャッシュ:
      • 実在しないコントローラ名(例: "foo/bar" に対応するコントローラがない)が来た場合は、従来通り例外を投げる / エラー処理を行い、その結果はキャッシュしません。
    • キャッシュサイズは「存在するコントローラの数」にほぼ限定される:
      • キャッシュされるのは「実際に解決できたコントローラ名」だけなので、暴走して無限に増える類のキャッシュではありません。
      • 動的な :controller セグメントはルーティング定義時に拒否されるため、params[:controller] は基本的に「routes.rb に書かれたコントローラに対応する値」に限られます。

4) リロードセーフティを担保するテスト追加 ​

actionpack/test/dispatch/request_test.rb に2つのテストが追加されています:

  1. 最初は見つからなかったコントローラが、定義後に解決されること

    • 手順イメージ:
      1. 存在しないコントローラ名で解決を試みて失敗
      2. そのコントローラを定義
      3. 再度同じパラメータで解決 → 今度は成功
    • これにより、「解決失敗時の結果はキャッシュしていない」ことと、「新しい定義を拾える」ことを保証。
  2. 既存コントローラを削除して再定義した場合に、新しいクラスに解決されること

    • 手順イメージ:
      1. あるコントローラを定義して解決
      2. その定数を削除し、別のクラスで同じ名前を再定義
      3. 再度 controller_class_for を呼ぶ → 新しい定義のクラスに解決
    • 「キャッシュされているのはクラスではなく『名前文字列』だけ」であることの確認になっています。

5) パフォーマンス計測結果 ​

head :ok だけ返すような非常にシンプルなアクションに対しての計測:

項目beforeafter
routed request9687
controller_class_for20
  • routed request の数字は、おそらく一定回数のリクエストに対する合計あるいは何らかのカウンタ値で、全体として呼び出し回数/処理コストが下がっていることを示しています。
  • controller_class_for のカウンタが 2 → 0 になっているのは、内部の文字列変換呼び出しなどが削減されていることを意味します(計測方法次第ですが、「無駄な 2 回分が削れた」と読んでよい変更)。

  1. 影響範囲・注意点

影響範囲 ​

  • 直接影響するのは 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 間で状態を共有せず安全に動作する設計と言えます。

  1. 参考情報 (あれば)
  • 関連コード位置(概念的な場所):

    • actionpack/lib/action_dispatch/http/request.rb
      • controller_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. 概要 (1-2文で)
    Rails のセッションアクセスで、キーが Symbol の場合に to_s ではなく Symbol#name を使って文字列化するようにして、余計な String オブジェクトの生成をなくすパフォーマンス改善です。セッションの API の挙動(Symbol/String キーの互換性)は維持したまま、アクセスごとのアロケーション削減と高速化を行っています。

  1. 変更内容の詳細

何をしたか ​

  • セッションアクセス時のキー変換ロジックを private メソッド stringify_key に集約し、以下の全ての箇所で利用するように変更:

    • []
    • []=
    • dig
    • has_key?(key?)
    • delete
    • fetch(成功パスとキー未定義パスの両方)
  • stringify_key の中で、Symbol が渡された場合は key.name を使い、String の場合は従来どおり key.to_s(=self を返すので追加アロケーションなし)を使う形になっています。

擬似コードで書くと、イメージは次のようになります:

ruby
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 キーで同じエントリを指す」ことを明示的にテストで固定化。
  • これにより、今回の実装変更で挙動が変わっていないことを保証し、今後のリグレッションも防いでいます。

  1. 影響範囲・注意点

影響範囲 ​

  • 対象:
    • 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
    • これらは新しい実装でも同じ結果になります。
  • String キーの場合は元々 to_s が self を返していたため、前後で挙動もパフォーマンスも変化しません。

パフォーマンス面 ​

  • Ruby 3.4.11 におけるベンチマーク(キー変換+ハッシュ lookup):
    • 1 アクセスあたりのアロケーション数:
      • 変更前: 1(毎回 String を 1 つ生成)
      • 変更後: 0
    • 5,000,000 回のアクセス:
      • 変更前: 0.46 秒
      • 変更後: 0.30 秒
  • セッションアクセスが多いアプリケーション(ページビューが多い、あるいはミドルウェア的な処理でセッションに頻繁にアクセスする)では、GC 負荷とレスポンス時間の両面でわずかに有利になります。

注意点 ​

  • カスタムミドルウェアやプラグインで、セッション内部の実装に強く依存しているコード(例: セッションハッシュに直接アクセスしてキーの class や frozen 状態を検査しているようなコード)があると、name による intern 文字列(frozen String)を前提とした挙動の違いに影響される可能性はありますが、通常のアプリケーションコードでは問題にならない想定です。
  • 今後 Ruby 本体の Symbol#to_s 実装が変わる可能性はあるものの、この PR は「現在の Ruby の挙動における最適化」であり、挙動を固定するテストも追加されているため、Rails 側としては一貫した動作が維持されます。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    ActionDispatch::Routing::RouteSet#call 内で、すでに正規化済みのパスについては再度正規化処理を行わないようにし、リクエストごとの不要な String アロケーションを削減するパフォーマンス改善です。挙動(どのパスがどう正規化されるか)は変えずに、既存の挙動を保ったまま高速化しています。

  1. 変更内容の詳細

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 と判定しない(後述の通り保守的な設計)

実装イメージ(厳密なコードではなく概念的なもの):

ruby
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
end

RouteSet#call 側での利用 ​

RouteSet#call は、これまで:

ruby
path = Utils.normalize_path(env[PATH_INFO])

のように、無条件に normalize_path を呼んでいましたが、変更後は:

ruby
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 に通しておき、安全側に倒す
  • 結果として、「これまで 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 のこれまでの挙動と矛盾していないこと」がテストで固定されています。


  1. 影響範囲・注意点

3-1. 挙動面の互換性 ​

  • normalize_path の仕様は変えていないため、「どの入力パスが最終的にどう変形されるか」は従来と同じです。
  • 変わるのは、「正規化処理を実際に呼ぶかどうか」という内部実装のみ。
  • 特に、次のようなパスはこれまでどおり正規化されます:
    • posts → /posts
    • /posts/ → /posts
    • /foo//bar → /foo/bar
    • /foo%2fbar など % を含むもの(大文字・小文字に関わらず)

3-2. パフォーマンスへの影響 ​

PR 説明にあるアロケーション数:

ルートbeforeafter
動的 /posts/:id1918
静的 /about65
  • 1リクエストあたりの String アロケーションが 1つ減っている(典型的なケースで)。
  • 高トラフィックアプリケーションでは、GC 負荷がわずかに軽減される可能性があります。

3-3. 互換性リスク・注意点 ​

通常の Rails アプリ開発者視点では、ほぼ影響を気にする必要はありませんが、以下のようなケースでは知っておく価値があります。

  • Rack ミドルウェアや自前コードで env[PATH_INFO] を読み取り・書き換えしている場合:
    • 「書き換えたあとの PATH_INFO が、従来は normalize_path によって別の文字列オブジェクトになっていた」のが、
    • 今回の変更により、「完全に同じ String オブジェクトが後段まで渡る」ことが増える可能性があります。
  • ただし、Rails 本体側ではその前提(オブジェクトIDに依存した挙動など)は基本的にしていないため、通常の用途では問題になりません。
  • normalize_path 自体の挙動を monkey patch しているような特殊ケースでは、RouteSet#call が normalize_path を呼ばないパスが増えるという意味で、間接的影響が出る可能性はあります。

  1. 参考情報 (あれば)
  • 目的: 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. 概要 (1-2文で)
    Rails の abstract controller logger に関するテストが「テスト実行順序に依存して失敗する」問題を解消するため、テストコードが修正されました。特定のシード値でのみ落ちる不安定テストを、クラス設定の汚染を避ける形で安定化しています。

  1. 変更内容の詳細

問題となっていた点 ​

actionpack/test/abstract/logger_test.rb に追加されていた「インスタンスレベルで logger を代入する」テストが、他のテストと クラスレベルの設定を共有・上書き してしまい、テスト順序によって結果が変わっていました。

説明文をコードレベルに落とすと、以下のような構造になっていたと考えられます(擬似コード):

ruby
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 の一時的なサブクラスを使う 形に変更されています。

イメージとしては以下のような変更です(概念的な擬似コード):

ruby
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 と小さな変更で、主にテスト対象クラスの扱い方を切り替えているだけです。


  1. 影響範囲・注意点
  • 影響範囲

    • 変更対象は actionpack/test/abstract/logger_test.rb のみで、本体コード(AbstractController 本体や logger 実装)には変更がありません。
    • したがって、実行時の挙動や API に対する影響はなく、既存アプリケーションの動作には影響しません。
    • 影響するのは Rails のテストスイート(特に actionpack の logger 関連テスト)の安定性のみです。
  • 注意点 / 開発者視点での示唆

    • Rails では設定がクラスレベルでキャッシュされる・継承されることが多いため、
      • テストでクラスの設定を書き換える場合は「元の状態への戻し方」によってはクラス自体を汚染しうる
      • 今回のように「明示的な nil の代入」が継承ロジックを壊すパターンはよくある落とし穴
    • 同様のケースを避けるためには:
      • テスト専用の無名サブクラス(Class.new(BaseClass))を積極的に使う
      • ensure/teardown での「リセット」が本当に元の状態に戻しているか(nil にすることが「未設定」と同義とは限らない)を見直す
    • CI で seed を変えてテストを回している場合、今回の修正により logger 関連テストのランダムな失敗は解消されるはずです。

  1. 参考情報 (あれば)
  • 関連 PR:
    • #59090: 今回修正対象となった abstract controller logger テストを追加した PR
  • テスト実行例:
    • 問題が発生していたコマンド:
      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. 概要 (1-2文で)
    Active Record のレコード生成時に使われる attributes_builder を「スキーマコンテキストごと」にメモ化し、毎回新しく計算・生成していたオブジェクトを再利用することで、レコードインスタンス化のオーバーヘッドを削減する変更です。これにより、レコード大量生成時のオブジェクトアロケーションと処理時間が大幅に改善されています。

  1. 変更内容の詳細

何が問題だったか ​

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_cache
    • attribute 登録
    • reset_column_information
    • ignored_columns=
  • primary_key=
    • モデル階層(親クラスを含む)全体のスキーマを再読み込みする

これらが呼ばれると schema context 自体が捨てられ、新しい context が作られるため、attributes_builder もそのタイミングで再構築されます。
つまり「スキーマが変わったら必ずビルダーも作り直される」ため、メモ化による不整合は発生しません。

さらに、レコード毎に持つ遅延属性セット(lazy attribute set)は:

  • 共有されている「デフォルト属性ハッシュ」を読むだけ
  • 実際にレコードに渡す前に各属性を dup して複製

という構造になっているため、「ビルダーは共有しても、レコードごとの属性インスタンスはちゃんと別物になる」ようになっています。
そのため、1 つの attributes_builder を全レコードで安全に共有できます。

テストで確認していること ​

追加されたテストは、おおむね以下のシナリオを確認しています:

  1. ビルダーの再利用が行われていること

    • 同じ schema context 内で複数回レコードをインスタンス化しても、同じビルダーが使い回されること
  2. reset_column_information 後にビルダーが再構築されること

    • カラム情報をリセット → スキーマ再読み込み
    • その後に使われるビルダーが新しいスキーマに基づくものであること
  3. 親モデルの primary key 変更が子モデルにも反映されること

    • 親クラスの primary_key を変更
    • 子クラス(継承したモデル)のスキーマ・ビルダーが再構築され
    • 新しい primary key がビルダーのデフォルトにも正しく反映されること

コード差分としては:

  • activerecord/lib/active_record/model_schema/schema_context.rb
    • Attributes に attributes_builder 相当のメモ化フィールドやロジックを追加
  • activerecord/test/cases/primary_keys_test.rb
    • 上記のビルダー再利用/再構築に関するテストケースを追加

といった最小限の変更にとどまっています。

パフォーマンス効果 ​

PR に記載されている計測結果(SQLite + 6 カラムのモデル):

処理内容変更前 allocations変更後 allocations
Post.instantiate(row)106
Post.all.to_a (100 行)16851285

さらに 30 万件インスタンス化のベンチ:

  • 以前: 0.26s
  • 以後: 0.14s

と約 2 倍近い速度改善が確認されています。
Rails の内部 API だけで実現しているため、アプリケーション側のコード変更なしに恩恵が受けられます。


  1. 影響範囲・注意点

影響範囲 ​

  • 主に Active Record モデルの大量インスタンス化パス に影響
    • Model.instantiate(row)
    • Model.find_each, Model.all.to_a, pagination ライブラリなど大量ロードを行う箇所
  • アプリコードから直接 attributes_builder や schema context に触れていない限り、API レベルの互換性は保たれます。

互換性・注意点 ​

  • スキーマ変更関連の以下の API に依存してメモ化がリセットされます:

    • reset_column_information
    • ignored_columns=
    • primary_key=
    • reload_schema_from_cache 周辺の内部処理
  • これらを正規の方法で使っている分には問題ありませんが、

    • Rails の内部構造を前提に「独自に schema context をキャッシュ・書き換えしている」ようなメタプログラミングをしている場合、
      メモ化の追加により挙動が変わる可能性はあります(一般的なアプリではほぼ無関係)。
  • デフォルト属性を共有しつつ、実体は dup したものをレコードごとに渡しているので:

    • 「あるモデルインスタンスのデフォルト属性を in-place で書き換えて、別インスタンスに影響させる」といった非推奨な前提は通用しません(そもそも以前から保証されていません)。

  1. 参考情報 (あれば)
  • 対応箇所:

    • activerecord/lib/active_record/model_schema/schema_context.rb
      • Attributes に attributes_builder のメモ化追加
    • activerecord/test/cases/primary_keys_test.rb
      • ビルダー再利用・再構築を検証するテスト
  • 関連する内部 API / 振る舞い:

    • ActiveRecord::Base.reset_column_information
    • ActiveRecord::Base.primary_key=
    • ActiveRecord::ModelSchema::ClassMethods#reload_schema_from_cache
    • ignored_columns= および attribute registration まわり
  • この PR は挙動変更というより「内部最適化」ですが、CHANGELOG への記載対象となるレベルのパフォーマンス改善として扱われています。


#59096 Spare the Content-Type parsing on the response for headers it set itself ​

マージ日: 2026/10/15 | 作成者: @amatsuda

  1. 概要 (1-2文で)
    Rails の ActionDispatch::Response において、レスポンス自身が設定した Content-Type ヘッダを再パースしないようにメモ化を導入し、不要な正規表現マッチとオブジェクト生成を削減するパフォーマンス改善です。これにより、response.media_type などの呼び出しでのアロケーションが減り、1リクエストあたりのオブジェクト生成数がわずかに削減されます。

  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 など「自分が知っている構成要素」からヘッダ文字列を組み立てると同時に、その要素をパース結果としてメモに登録する。

擬似コードイメージ:

ruby
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=) 経由で設定してから読み戻した場合 で同じ結果になること。

  1. 影響範囲・注意点

影響範囲 ​

  • 対象: ActionDispatch::Response の Content-Type 周り (media_type, charset, content negotiation, head, レンダリング等)

  • 典型的なコントローラ利用では、以下のようなオブジェクトアロケーション削減が報告されています(ミドルウェアなしの計測):

    ケース変更前変更後
    head :ok9384
    render plain:133128
    render json:10095
    render html:132127
  • response.media_type を 1 回読むだけのケースでのアロケーション:

    • 6 個 → 0 個

パフォーマンス/メモリアロケーションの「地味だが積み重なる」改善で、頻繁にレスポンスを生成するアプリケーションでは GC 負荷の軽減に寄与します。

注意点・互換性 ​

  • 挙動は「外部から渡された生のヘッダ文字列は必ずパースする」方針のため、従来と互換性を保っています。
  • headers['Content-Type'] を直接いじるようなコードを持っていても、
    • 値が変われば再パースされる
    • メモ化は「生文字列」キーなので、意図しない古いパース結果が使われることは基本的にありません。
  • Content-Type 未設定時に null オブジェクトが使われる実装詳細がありますが、公的 API の挙動(戻り値が nil 相当であることなど)は維持されるように設計されています。

アプリケーション側で特別な対応が必要になる種類の変更ではなく、「透明な最適化」に近いものです。


  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/59096
  • 変更ファイル:
    • actionpack/lib/action_dispatch/http/response.rb
    • actionpack/test/dispatch/response_test.rb
  • 主なトピック:
    • Content-Type ヘッダ解析のメモ化
    • 自前でセットしたヘッダに対してパーサを回さない最適化
    • 未設定ヘッダ用の共有 null オブジェクト
    • オブジェクトアロケーション削減による軽量化

#59095 Allocate nothing in the notifications fanout for silenced subscribers ​

マージ日: 2026/10/15 | 作成者: @amatsuda

  1. 概要 (1-2文で)
    ActiveSupport::Notifications の Fanout#groups_for が、サイレンス(silence)された購読者に対して不要な配列割り当てを行わないように最適化されました。これにより、特に sql.active_record などデバッグ用サブスクライバが常時サイレンスされている本番環境で、アロケーション削減とわずかな性能改善が見込めます。

  1. 変更内容の詳細

背景 ​

  • ActiveSupport::Notifications::Fanout#groups_for は、build_handle に到達するすべてのイベント(= Active Record の全 SQL クエリとトランザクション)で呼ばれます。
  • 通知には「サイレンス可能 (silenceable)」なサブスクライバが含まれる場合があり、ログレベルや silence の状態に応じてイベントを受け取らないようにできます。
  • 以前の実装では、「サイレンス可能なサブスクライバのグループ」を処理するたびに、
    • サイレンスされている購読者を reject して新しい配列に詰め直す
    • たとえ全員サイレンスされていても、毎回配列を割り当てる
      という動きをしていました。

典型的なケースとして、sql.active_record の debug-level サブスクライバ(構造化ログ用の subscriber)は、本番のように logger が debug でないときは常にサイレンスされています。この状態だと、実質的に「誰も受け取らない通知」に対して、毎回配列を 1 つ余計に作っていました。

新しいアルゴリズム ​

今回の変更では、次のようにロジックが変わっています:

  • 「有効な(サイレンスされていない)サブスクライバ一覧」を 一回のループで構築 する。
  • 少なくとも 1 つでも有効なサブスクライバが見つかった場合にだけ、配列を割り当てる。
  • すべてサイレンスされている場合は、配列を一切割り当てない(あるいは nil / 空扱いで終了)。

疑似コードイメージ:

ruby
# 旧: 各グループごとに 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

効果は「小さいが、ホットパスで常に発生する」タイプの最適化です。


  1. 影響範囲・注意点
  • 対象:
    • ActiveSupport::Notifications の内部実装 (Fanout) と、その上で動く Active Record の計測 (sql.active_record など)。
    • 特に本番環境など、debug-level の subscriber が恒常的にサイレンスされているケースで恩恵があります。
  • 公開 API の挙動:
    • 「サイレンスされた subscriber は通知を受け取らない」「他の subscriber は通常通り動く」「subscriber が全員サイレンスなら誰も受け取らない」という意味的な挙動は変わっていません。
    • そのため、通常の ActiveSupport::Notifications.subscribe / instrument を使っているアプリ側コードの修正は不要です。
  • 注意点:
    • 挙動変更はあくまで内部実装&パフォーマンス改善であり、外部から依存されるべきではない内部構造(配列の存在など)に依存しているような非公式なメタプログラミングがあれば、影響を受ける可能性はあります(通常のアプリケーションでは問題にならない想定)。
    • サイレンス機構(ActiveSupport::LogSubscriber / ActiveSupport::Notifications 周り)を拡張している場合は、各 subscriber が「イベントごとにサイレンス判定される」前提であることを再確認しておくと安全です。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/59095
  • 関連コンセプト:
    • ActiveSupport::Notifications
    • ActiveSupport::LogSubscriber
    • sql.active_record イベントと構造化ログ subscriber
  • この種の micro-optimization は、「計測・ログを大量に発行するが、その多くはログレベルなどで実際には捨てられる」というワークロードで特に効いてきます。Rails の内部でもこのパターンが多いため、同様の方針(必要になるまで配列などを割り当てない)を自前の計測コードに適用する際の参考になります。

#59093 Fix the link_to nil name documentation example ​

マージ日: 2026/10/15 | 作成者: @55728

  1. 概要 (1-2文で)
    link_to ヘルパで name に nil を渡したときの挙動について、ドキュメント内の出力例が実際の動作と食い違っていたため、正しい出力例に修正した PR です。コードの挙動自体は変わっておらず、ドキュメントのみの修正です。

  2. 変更内容の詳細

対象箇所: actionview/lib/action_view/helpers/navigation_helper.rb の link_to のドキュメントコメント

元のドキュメント例では、次のように書かれていました:

ruby
link_to nil, "http://example.com"
# => <a href="http://www.example.com">http://www.example.com</a>

しかし実際の link_to の挙動は、引数で渡した URL をそのまま href とリンクテキストの両方に使うため、正しい出力は:

ruby
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 で実行した結果とも一致しています。

  1. 影響範囲・注意点
  • ランタイムのコード変更は一切なく、link_to の動作は従来どおりです。
  • 影響があるのはドキュメントのみであり、既存アプリケーションの挙動に変更はありません。
  • これまでドキュメントを見て「nil を渡すと URL が正規化されて www.example.com のようになる」と誤解していた場合、その理解を改める必要があります。実際には「渡した URL 文字列がそのまま href とリンクテキストに使われる」仕様です。
  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Rails の before コールバック実行時に毎回生成されていた lambda(Thunk)を、特定の「終端処理オブジェクト(terminator)」については省略できるようにし、オブジェクト割り当てを大幅に削減する最適化です。run_callbacks の内部プロトコルを拡張しつつ、既存のカスタム terminator との互換性は維持されています。

  1. 変更内容の詳細

背景・問題点 ​

従来の ActiveSupport::Callbacks では、before コールバックを実行するたびに、以下のような流れになっていました:

  • コールバック本体を「あとで実行するための lambda」(= thunk) にラップ
  • terminator(コールバックチェーンの終端処理)に対し、
    terminator.call(target, thunk) のような形で渡す
    • デフォルト terminator は catch(:abort) で囲む
    • ActionPack の process_action terminator は controller.performed? をチェックする

この構造のせいで、

  • before コールバック 1 個につき最低 2 オブジェクト(Proc + 環境 or 関連オブジェクト)が毎回割り当てられる
  • リクエストあたり複数の callback chain(controller、model、job など)を通るため、GC 負荷・オーバーヘッドが無視できない

というパフォーマンス上の問題がありました。


新しい terminator インターフェース call_direct ​

この PR では、「terminator がコールバック本体をそのまま受け取り、自分で好きに呼び出す」ための新しいプロトコルを追加しています。

  • terminator が以下のシグネチャを実装できるようになりました:
ruby
def call_direct(target, callback, value)
  # target: コールバック対象オブジェクト(例: controller, model など)
  # callback: 実行すべきコールバック本体(Proc や callable)
  # value: コールバックチェーンで伝搬される値
end
  • Filters::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 対応させています。

  1. デフォルト terminator (ActiveSupport::Callbacks 標準)

    • これまで lambda { |target, block| catch(:abort) { block.call } } のような形だったものを
    • call メソッドを持つ 凍結オブジェクト に変更し、
    • さらに call_direct(target, callback, value) も実装

    イメージとしては以下のような動きになります(実コードは若干異なる可能性がありますが概念的にはこういうことをしています):

    ruby
    class 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
  2. Action Pack の process_action terminator

    • これも lambda から「call を持つオブジェクト + call_direct」に変更
    • controller.performed? のチェックを call_direct でも行う
  3. Action Mailbox の process terminator

    • 同様にオブジェクト化され、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.68s2.18s
  • before_action を 4 つ持つ controller リクエスト 1 回で「オブジェクト 10 個分の割り当て」が削減

  • ruby-bench の railsbench では、1 反復あたりのオブジェクト割り当て数が
    1,405,387 -> 1,361,447 (約 -3.1%)


  1. 影響範囲・注意点
  • アプリケーションコード側の変更は基本的に不要です。
    • 通常の before_action や model/job callbacks などは、そのままで内部的に恩恵を受けます。
  • ただし、以下に該当する場合は注意が必要です:
    1. 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)」などプロトコルベースのチェックに寄せるべきです。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/59094
  • 対象箇所:
    • activesupport/lib/active_support/callbacks.rb
    • actionpack/lib/abstract_controller/callbacks.rb
    • actionmailbox/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. 概要 (1-2文で)
    ActiveSupport::OrderedOptions / InheritableOptions(主に config.x で使われる)の動的読み取りが、オブジェクトを追加生成せずに済むよう最適化されたPRです。method_missing のシグネチャ変更とキーの扱いの見直しにより、読み取りが高速化・省メモリ化されます。

  1. 変更内容の詳細

背景 ​

config.x は内部的に ActiveSupport::OrderedOptions / InheritableOptions を使っており、

ruby
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、という無駄な変換がある

結果として、単なる読み取りでも不要なオブジェクト生成と変換が発生していました。

今回の主な変更 ​

  1. method_missing のシグネチャ変更
    旧:

    ruby
    def method_missing(method, *args)
      # ...
    end

    新:

    ruby
    def method_missing(method, arg = UNDEFINED)
      # ※ 実際の定数名や実装詳細は概略ですが、イメージとして
    end
    • 可変長引数 (*args) をやめて「引数1つ(省略可)」にしたことで、呼び出しごとの配列生成がなくなる。
    • read(options.foo)のような引数なし呼び出しでは「一切の追加オブジェクトを生成しない」形に。
  2. キーの 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
  3. 故意に変えた振る舞い(互換性のない部分)

    以前は:

    ruby
    options.send(:foo, 1, 2) # *args が余分な引数を「黙って飲み込む」

    といった呼び出しでもエラーにならず、「余計な引数があっても無視」されていました。

    今回からは:

    ruby
    options.send(:foo, 1, 2) # => ArgumentError を投げる

    と、余計な位置引数があると ArgumentError を明示的に起こします。

ベンチマーク結果 ​

1回の呼び出しあたり:

呼び出し種別変更前変更後
read, key あり1 object, 192 ns0 objects, 142 ns
read, key なし1 object, 200 ns0 objects, 149 ns
InheritableOptions 親経由 read1 object, 242 ns0 objects, 197 ns
bang read (foo!)2 objects, 262 ns1 object, 245 ns
write (foo =)2 objects, 232 ns1 object, 221 ns

特に「読み取り」が 0 allocation になっているのがポイントです。
config.x はリクエストごとに何度も読まれうるので、トータルのGC負荷軽減に効きます。


  1. 影響範囲・注意点

影響範囲 ​

  • Rails 全体およびアプリケーションコードで利用される config.x 系の設定オブジェクト
    • 例: Rails.application.config.x.feature_flags.some_flag
    • ActiveSupport::OrderedOptions を直接使っているコード
    • ActiveSupport::InheritableOptions を使う箇所

通常の利用パターン:

ruby
config.x.foo = 1
config.x.foo
config.x.foo!

などは、挙動は変わらずパフォーマンスだけが良くなります。

注意点・互換性 ​

  1. 余分な引数を渡しているケースがあれば壊れる

    例:

    ruby
    config.x.foo(1)      # 本来は foo はメソッドではなく設定キー
    options.send(:bar, 1, 2)

    以前: 何も言わずに無視されていた
    今回: ArgumentError が発生

    そのようなコードはそもそもバグの可能性が高く、「早期に壊れて知らせてくれる」変更とも言えますが、挙動変更ではあるので注意が必要です。

  2. method_missing の挙動に依存したメタプログラミング

    独自に OrderedOptions / InheritableOptions を継承し、method_missing のシグネチャや挙動に強く依存したコードを書いている場合、微妙な影響を受ける可能性があります。
    とはいえ、このPRは method_missing(method, arg = nil) 程度の素直な形なので、一般的なオーバーライドにはほぼ影響はないと考えられます。


  1. 参考情報 (あれば)
  • 対象クラス:
    • ActiveSupport::OrderedOptions
    • ActiveSupport::InheritableOptions
  • 代表的な利用箇所:
    • Rails.application.config.x
    • config.x を介した feature flag / カスタム設定
  • このPRの主な目的:
    • 「config設定の読み取りを allocation-free にして、GC負荷とレイテンシを削る」マイクロ最適化
    • 「不正な呼び出し(余分な引数付き)の早期検知」も副次的に達成

#59090 Read the controller logger from the config hash directly ​

マージ日: 2026/10/15 | 作成者: @amatsuda

  1. 概要 (1-2文で)
    Rails の AbstractController::Logger が、これまで config.logger(method_missing ベースの動的呼び出し)経由で読んでいたロガーを、config[:logger] を直接読む明示的アクセサに変更し、コントローラ/メイラー/ビューでのロガーアクセスを高速化する PR です。
    動作仕様(config.logger と logger の互換性や継承挙動)は維持したまま、オブジェクト生成と動的ディスパッチのコストを削減しています。

  1. 変更内容の詳細

これまでの実装の問題点 ​

  • AbstractController::Logger は logger / logger= を config にデリゲートしていた:
    ruby
    # イメージ
    delegate :logger, :logger=, to: :config
  • config は ActiveSupport::OrderedOptions ベースで、config.logger 呼び出しは method_missing によって解決される。
    • つまり、logger を読むたびに:
      • splat 配列の生成
      • method_missing による動的ディスパッチ が走る。
  • コントローラ、メイラー、ビュー(ビューの logger はコントローラの logger を使う)でログ出力を多用するアプリでは、このオーバーヘッドを毎回支払うことになる。

今回の変更点 ​

アクセサを明示的に定義 ​

AbstractController::Logger 内で、logger / logger= を config[:logger] に対する明示的アクセサとして定義し直しています。

イメージとしては以下のような形です(実際のコードは若干異なる可能性がありますが、本質は同じです):

ruby
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

多数のログ呼び出しがあるアクションやテンプレートでは、合算すると無視できない差になります。


  1. 影響範囲・注意点

影響範囲 ​

  • 影響を受けるコンポーネント:
    • AbstractController::Base をベースにしたコントローラ
    • ActionMailer::Base(内部で AbstractController を利用)
    • ビュー内の logger 呼び出し(コントローラの logger へフォワード)
  • 外部 API としての挙動は原則変更なし:
    • 既存コード:
      • logger(インスタンスメソッド)
      • self.class.logger
      • config.logger は同じように利用可能。

実装依存のコードを持っている場合の注意点 ​

通常のアプリでは破壊的変更はほぼありませんが、以下のようなケースがあると注意が必要です:

  • config(OrderedOptions)の method_missing をモンキーパッチして、logger 呼び出し時の挙動をいじっている場合:
    • 今回の変更で logger 読み取りは config.logger ではなく config[:logger] に変わったため、そのカスタマイズは通らなくなります。
  • logger アクセサ自体を Module#prepend 等で差し替えている場合:
    • 新しい明示的メソッド定義と干渉する可能性があります(これはどのみち fragile な拡張ですが)。

通常のアプリケーションレベルのコード(単に logger.info, config.logger = my_logger など)についてはそのまま動き、かつ速くなります。


  1. 参考情報 (あれば)
  • PR 番号: #59090
  • タイトル: Read the controller logger from the config hash directly
  • 作成者: amatsuda
  • マージ日時: 2026-10-15T08:24:17Z
  • 変更ファイル:
    • actionpack/lib/abstract_controller/logger.rb
    • actionpack/test/abstract/logger_test.rb

この変更は「ログアクセスのホットパスから OrderedOptions#method_missing を抜く」ことにフォーカスした、局所的なパフォーマンスチューニングであり、API 表面は保ちつつ内部実装をより直接的・低コストにしたものと言えます。


#59089 Memoize controller url_options with script_name across calls ​

マージ日: 2026/10/15 | 作成者: @amatsuda

  1. 概要 (1-2文で)
    ActionController::Metal::UrlFor#url_options が、script_name を含んだ形でもメモ化されるようになり、同一リクエスト内での繰り返し呼び出し時のオブジェクト生成・処理コストが削減されました。あわせて、テスト環境でのコントローラ再利用時や script_name 変更時のキャッシュ無効化も適切に行われるようになっています。

  1. 変更内容の詳細

2-1. これまでの問題点 ​

ActionController::Metal::UrlFor#url_options はベースとなるオプションハッシュ(ホスト名やプロトコルなど)をメモ化していましたが、実運用でよくあるケースである「そのコントローラ自身のルーティングからディスパッチされたリクエスト」の場合、毎回以下のような処理をしていました。

  • メモ化済みの @_url_options に対して
  • script_name: を追加した新しいハッシュ { **@_url_options, script_name: } を毎回生成
  • 生成したハッシュを毎回 freeze

そのため、一覧ページで 100 件分の URL を生成する場合、script_name が同じであっても、内部的に同じ内容のハッシュオブジェクトが 100 個作られていました。

2-2. 変更点の概要 ​

今回の PR では:

  1. 「script_name を含んだ url_options のハッシュ」を、ベースの @_url_options とは別にメモ化するインスタンス変数を追加
  2. ただし、同一リクエスト内で script_name が変わるケース(マウントされたエンジンなどでの再ディスパッチ)に対応するため、
    • 毎回「現在の script_name」を確認し、
    • 直前にメモしたものと script_name が変わっていたら、新しいハッシュを生成して再メモ化
  3. 新しく追加したインスタンス変数を PROTECTED_IVARS に登録し、assigns などビューに露出する変数一覧から除外
  4. recycle! 実行時(functional test でコントローラインスタンスを再利用するケース)に、この新しいメモもクリアし、前のリクエストの host や recall パラメータを使い回さないようにする
  5. 上記挙動を担保するテストを追加(同一オブジェクト性・script_name 変更時の無効化・recycle 時のクリア)

2-3. もう少し踏み込んだ動きのイメージ ​

※擬似コードレベルのイメージです(実装そのものではありません):

ruby
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)
end

2-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) のわずかなオブジェクト削減とレイテンシ短縮につながっています。


  1. 影響範囲・注意点
  • 影響を受ける範囲

    • 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 を数回しか生成しないようなケースでは、体感差はほぼないはずです。

  1. 参考情報 (あれば)
  • 対象クラス / モジュール
    • ActionController::Metal::UrlFor#url_options
    • ActionController::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. 概要 (1-2文で)
    ActiveRecord::Integration#to_param が、単一の主キー(integer / string)の場合に高速に処理されるよう最適化されました。これにより、通常の単一カラム主キーのモデルで、URLヘルパー使用時のオブジェクト生成数と処理時間が削減されています。

  1. 変更内容の詳細

これまでの to_param の挙動 ​

ActiveRecord::Integration#to_param は、主キーが単一であっても複合であっても常に以下のような処理を行っていました:

ruby
def to_param
  Array(id).join(self.class.primary_key_separator)
end
  • Array(id) によって id を一度配列に包む
  • join(delimiter) で区切り文字(複合主キー用)を使って文字列化
  • 単一主キーでも毎回
    • 配列オブジェクト 1つ
    • 結果文字列 1つ など、余計なオブジェクト生成と処理をしていた

複合主キーをサポートするための実装ですが、一般的な「単一主キーのレコード」に対しても常にこのコストを払っていました。

今回の変更点 ​

ロジックの最適化 ​

to_param 内で「単一主キー」と「複合主キー」を分岐し、単一主キーの場合は高速パスで処理するように変更されています。

イメージとしては次のような実装になります(擬似コード):

ruby
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変更前変更後
integer3 objects, 506 ns1 object, 168 ns
string2 objects, 246 ns1 object, 133 ns
composite5 objects, 607 ns4 objects, 497 ns

特に、一般的な単一整数主キーで

  • オブジェクト数: 3 → 1
  • 時間: 約 1/3

となっており、URLヘルパーが大量に呼ばれるアプリケーションでは GC 負荷やレスポンス時間の軽減が期待できます。


  1. 影響範囲・注意点

影響範囲 ​

  • 対象: ActiveRecord::Base#to_param を利用する全てのモデル
    • Rails の URL ヘルパー(user_path(@user) など)は内部で to_param を使うため、通常の全アプリで影響あり
  • 主に挙動が変わるケース:
    • 単一の整数主キー
    • 単一の文字列主キー
  • 挙動は従来と互換:
    • 表面的な 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 はその対応を壊さず、単一キーのみを高速化しています

  1. 参考情報 (あれば)
  • 対象メソッド: 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. 概要 (1-2文で)
    ActionDispatch::Request が内部で保持している Rack::Request オブジェクトの生成を「遅延生成(lazy)」に変更し、実際に必要になったときにだけ作るようにしたパフォーマンス改善のPRです。これにより、Rails標準スタックで1リクエストあたり複数回発生していた不要な Rack::Request の生成が削減されます。

  1. 変更内容の詳細

何をしているか ​

  • これまでは ActionDispatch::Request.new(env) を呼ぶたびに、内部で必ず Rack::Request.new(env) も一緒に生成していました(eager creation)。
  • 本PRでは、Rack::Request の生成を「最初に必要になったタイミング」で行うように変更しています(lazy creation)。
  • 外部APIとしての rack_request メソッドは引き続き公開のまま・戻り値も同じ型ですが、その中で初回アクセス時にだけ Rack::Request を生成してキャッシュする形になったと考えられます。

疑似コードイメージ(実際の実装イメージレベル):

ruby
# 変更前(イメージ)
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::RequestId
      • ActionDispatch::RemoteIp
      • Rails ロガー関連ミドルウェア
      • ActionDispatch::Executor
      • Cookies 関連
      • セッションストア
      • 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 objects1 object
    所要時間203 ns167 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 を省略している可能性があります。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Rails のルーティング(Journey)に「完全一致テーブル」が導入され、GET /posts のような静的ルートについては正規表現マッチやパラメータ抽出をスキップして、HTTPメソッド+パスだけで即座に対応ルートを特定できるようになりました。これにより、典型的なアプリが多く持つ静的ルートへのリクエスト性能が大幅に向上しつつ、マッチングの挙動自体は従来どおり維持されます。

  1. 変更内容の詳細

2-1. 何をしている変更か ​

  • Journey::Routes に「メソッド+パス → Route」の 完全一致テーブル(exact-match table) を追加
  • Router#serve がリクエスト処理時に
    1. まずこのテーブルを参照して即マッチできるか確認
    2. 見つからなければ従来どおり GTG シミュレータ+正規表現ベースのマッチングを実行
      というフローに変更
  • 対象は「完全に静的と言い切れる」ルートだけで、条件を満たさないルートは従来どおり通常のマッチング経路を通る

2-2. 完全一致テーブルに載るルートの条件 ​

以下の条件をすべて満たしたルートのみ、テーブルに事前登録されます。

  1. アンカー付きのルート

    • ルートパターンが先頭・末尾まで固定されている(中途半端にマッチしない)こと
  2. 単一の HTTP メソッドに対するルート

    • 例: GET だけなど
    • GET と POST 両方にマッチするようなパターンは対象外
    • HEAD の GET フォールバックはテーブル対象外(HEAD は従来フロー)
  3. 追加の request constraints がない

    • constraints ブロックや defaults など、リクエスト内容に依存した振る舞いをするものがない
    • たとえば constraints subdomain: 'admin' のようなルートは登録しない
  4. パスが「リクエスト依存でないリテラル」だけで構成される

    • 動的セグメント :id, *slug などがない
    • ただし「末尾のオプション部分(()内のオプション)を取り除いた上で静的」ならOK 例: get "/posts(.:format)"`
      • テーブルには "GET /posts" として登録される
      • 実際には /posts も /posts.json もこのルートとしてマッチするが、テーブル側は「ベースパス」だけ持っているイメージ
  5. ルート優先順位(precedence)を壊さないことが検証されている

    • 「そのメソッド+パスに対して、ルーティングシミュレータを使った通常のマッチングで最初に見つかるルート」と確定できる場合だけテーブルに載る
    • つまり、
      • もっと手前に「同じパスにマッチしうる別ルート」(静的/動的/マウントいずれでも)がある場合、そのパスはテーブルから除外
    • この「影になっていないか (shadowing) のチェック」は GTG シミュレータで行い、ルート数に対して線形時間になるよう配慮されている

2-3. テーブル利用時の挙動 ​

ルーティングの大まかなフローは次のようになります。

  1. Router#serve(env) が呼ばれる
  2. env から HTTP メソッドとパスを取り出し、完全一致テーブルを検索
    • 一致するエントリがあれば、その Route に対応する Rack アプリを即呼び出し
    • なければ、従来どおり GTG シミュレータ+正規表現マッチを実行
  3. テーブルからヒットしたルートのアプリが X-Cascade: pass を返した場合
    • そのルートを「スキップ済み」として扱い、残りのルートについて通常のマッチングフローを実行
    • そのルートは2回はディスパッチされない(1度だけ試してから通常走査にフォールバック)

このため、mounted Rack アプリなどが X-Cascade: pass を返すケースでも、 従来と同じルーティング結果になるように きちんとフォールバックが実装されています。

2-4. ビルドとキャッシュ ​

  • 完全一致テーブルは
    • ルートセットのビルド時にシミュレータと一緒に 起動時に eager build される
    • Routes#freeze 時にも構築される
  • ルートセットの既存の キャッシュ無効化メカニズムの背後 に置かれており
    • ルートが追加・変更されたら、このテーブルも再構築
  • Ractor 対応:
    • Ractor 共有可能なルートセット(eager load 前に finalize 済みのもの)でも、このテーブルが機能するように Routes#freeze 時にテーブル生成が行われる

2-5. パフォーマンス計測結果 ​

193ルート(32リソース分の静的+動的ルート)を持つアプリでの RouteSet#call 計測:

リクエスト変更前変更後
GET /1.4 µs0.8 µs
GET /things201.7–1.9 µs0.8 µs
GET /things20/new2.3 µs0.8 µs
GET /things20/7 (動的)2.4 µs2.5 µs
GET /nope (404)0.8 µs0.9 µs
  • 静的パスに対しては おおむね 40〜65% 程度のレイテンシ短縮
  • 動的ルートや 404 はほぼ変化なし(微小なオーバーヘッド程度)
  • テーブルビルド時間:
    • 3,000 ルートで約 0.02 秒
    • 12,000 ルートで約 0.06 秒
      → 実運用規模でも起動時オーバーヘッドとして十分許容範囲

  1. 影響範囲・注意点

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 など)
    • フロントエンドのルーティングに合わせた多数の静的ページ
  • 動的な show ルートや 404 については性能はほぼ変わらないため、
    • API サーバーなどで 一覧/作成画面やヘルスチェックが多く、ID付きの show より静的パスが多い 場合は特に恩恵が大きい

3-3. ルーティング設定上の注意 ​

開発者がこのPR適用後に特別な対応をする必要は基本的にありませんが、挙動を理解するうえで知っておくとよいポイントです。

  • get "/posts(.:format)" のような典型的な RESTful index ルートは
    • テーブル上では "GET /posts" キーで管理される
    • 実際のリクエスト /posts.json などはテーブルミス → 通常マッチングに回るかどうかは実装依存だが、結果としては従来と同じルートにたどり着く
  • constraints や defaults、サブドメインルーティングなど「リクエスト依存」な要素を増やすと
    • テーブルに載るルートが減り、静的ルート最適化の恩恵が小さくなる
  • ただし「ルートをシンプルに保てば保つほど速くなる」というだけなので、機能要件上の都合で constraints を使うこと自体を避ける必要はありません

  1. 参考情報 (あれば)
  • 変更ファイル:

    • actionpack/lib/action_dispatch/journey/router.rb
      • Router#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. 概要 (1-2文で)
    Rails の構造化イベント(EventReporter 経由のイベント)について、「最終的に誰も消費しないイベント」はそもそも生成しないようにして、リクエストごとの無駄なオブジェクト生成を削減する最適化が入っています。特にログレベルを上げている本番環境などで、action_controller.request_started / request_completed イベントの生成コストを大きく減らせます。

  1. 変更内容の詳細

背景・問題点 ​

  • 各リクエストでは必ず action_controller.request_started と request_completed のイベントが発火する。
  • しかし、アプリ側の logger のレベルがそれらのイベントのログレベルより高い場合(例: イベントは :info だが logger は :warn)、実際にはログ出力されない。
  • にもかかわらず、これまでは:
    • コントローラ側で params フィルタ済み payload を組み立てる
    • EventReporter が payload を resolve
    • タグ・コンテキスト・タイムスタンプ付きのイベントハッシュを生成
    • caller_locations を取ってソース位置を特定
    • → 最後に LogSubscriber 側で「ログレベルが合わない」として破棄
  • この一連の処理だけで、リクエストあたり約 30 個のオブジェクトが「出力されないログのために」生成されていた。

この PR は、**「どうせ誰も受け取らないなら、そのイベントは最初から組み立てない」**という最適化を実装しています。


consumes?(name) の追加 ​

  • すべての Subscriber(EventReporter に登録される購読者)は、任意で以下のメソッドを実装できるようになりました:
ruby
def consumes?(name)
  # このサブスクライバが、与えられたイベント名 name を「実際に処理する可能性があるか」を返す
  # true/false を返すことを想定
end
  • 挙動:
    • consumes?(name) を定義していない Subscriber
      → 従来通り「すべてのイベントを消費する」と見なす(常に true 扱い)。
    • 定義している Subscriber
      → EventReporter がイベント生成前に consumes? を呼び、一つも true を返さない場合にはイベントを構築せずリターンする。

EventReporter#notify / #emitting? の挙動 ​

EventReporter に以下が導入されています:

ruby
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

その結果、イベントの発行側は次のように書けます:

ruby
# 高コストな payload を組み立てる前に「そもそも誰かが聞いているか?」を確認できる
if Rails.event.emitting?("user.created")
  Rails.event.notify("user.created", expensive_payload_for(user))
end
  • notify 側でも emitting? 相当のロジックを使っているため:
    • notify を直接呼んだ場合でも、誰も consumes? しないイベント名ならイベント構築自体をスキップ。
    • 明示的に emitting? を呼ばなくても一定の最適化は効く。
  • ただし payload の構築コスト は notify を呼んだ時点で支払われてしまうため、payload が高コストな場合は上記のように emitting? を使うことで、payload 組み立て自体をスキップできる。

StructuredEventSubscriber / ActionController での利用 ​

  • ActiveSupport::StructuredEventSubscriber に emitting?(name) を追加:
ruby
class ActiveSupport::StructuredEventSubscriber
  def emitting?(name)
    Rails.event.emitting?(name)
  end
end
  • ActionController::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 に、この機能追加と最適化についてのエントリを追加。

  1. 影響範囲・注意点

影響範囲 ​

  • 既存の 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 allocs37 allocs
:warn32 allocs2 allocs
  • :info では変更なし。
  • :warn では 32 → 2 と大幅削減。
  • また、イベントを発行する側が emitting? を活用することで、構造化ログ以外の任意のイベントでも同様の削減を見込める。

注意点・設計上のポイント ​

  • consumes? はイベント名だけを受け取る:
    • subscription フィルタ(ブロックなど)は、「イベント名だけでなく、ほかの状態に依存する」ことがありうるため、毎回評価される。
      → 「フィルタは純粋関数である」とは仮定していない。
  • consumes? 実装のベストプラクティス:
    • できるだけ軽量にする(呼ばれる頻度が高いため)。
    • ロガーレベルのような、変わる可能性がある条件に依存する場合は、今回の LogSubscriber と同様「結果をメモ化しない」方が安全。
  • 新しくイベントを追加するときのガイド:
    • payload 組み立てが重い(DB アクセス、オブジェクト列挙、シリアライズなど)場合は:
      • if Rails.event.emitting?("some.event") でガードすることを検討するとよい。

  1. 参考情報 (あれば)
  • PR 番号: #59056
  • タイトル: Don't build structured events that no subscriber will consume
  • 主な変更ファイル:
    • activesupport/lib/active_support/event_reporter.rb
    • activesupport/lib/active_support/event_reporter/log_subscriber.rb
    • activesupport/lib/active_support/structured_event_subscriber.rb
    • actionpack/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. 概要 (1-2文で)
    ActionDispatch::Static / ActionDispatch::FileHandler が、存在しない静的ファイルを探すために毎回大量の stat(2) とオブジェクト生成を行っていたのを最適化し、動的リクエストに対するオーバーヘッドを大幅に削減する変更です。
    public/ 直下のトップレベル要素の存在確認を先に行うことで無駄な候補探索をスキップし、Rack::Request 生成や Accept-Encoding のパースも「静的ファイルが見つかった時だけ」行うようにしました。

  1. 変更内容の詳細

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/ 直下には存在しない
    → 毎回全部空振りしている

今回の変更点:

  1. トップレベルエントリの存在チェックを先に行う

    • リクエストパスを最初のセグメントに分解
      • /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 とそれらの圧縮版) の探索をまとめてスキップ
  2. .html 候補などへの配慮

    トップレベルパス /posts は、public/posts.html が存在すれば静的ファイルとして配信したいケースがあります。
    そのため:

    • 「素のパス候補」 /posts → public/posts
      • これに対しては public/posts の存在を確認
    • 「.html 付き候補」 /posts.html → public/posts.html
      • こちらは public/posts.html の存在を確認

    のように、候補ごとに「どのトップレベルパスを存在確認するか」を切り替えています。
    ルート / の場合は public/index.html を直接 probe します。

  3. なぜ「起動時に 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 ヘッダをパース
  • 結果として、静的ファイルがそもそも存在しないリクエストでも、これらのコストを常に払っていました。

今回の変更では:

  1. リクエストメソッドとパスは env から直接読む

    • env["REQUEST_METHOD"]
    • env["PATH_INFO"](または SCRIPT_NAME との組み合わせ)
    • GET/HEAD 以外は早期に弾くのはそのままですが、Rack::Request オブジェクトを作らずに判定
  2. 静的ファイルがヒットするまでは Rack::Request を作らない

    • ファイル候補探索 → 実際の File.file? で静的ファイルが見つかった後で初めて Rack::Request を生成し、レスポンスを組み立てる
  3. Accept-Encoding のパースも「圧縮版がある時だけ」実行

    • precompressed 版 (.br や .gz) が存在しないファイルについては、Accept-Encoding を見る必要もありません
    • そこで、precompressed 候補が見つかった時にだけ:
      • env["HTTP_ACCEPT_ENCODING"] を読む
      • そこから適切な encoding を選択する
    • さらに、その際に使う encoding ごとの正規表現を事前に用意 しておき、毎回文字列から正規表現を組み立てるコストも削減しています。

2-3. 効果 (PR 記載の計測結果) ​

Action Pack のテストフィクスチャをドキュメントルートにして計測した例:

リクエスト変更前変更後
/users/19 stats, 84 allocations1 stat, 28 allocations
/posts9 stats, 82 allocations2 stats, 30 allocations
/index.html ヒット6 stats, 74 allocations7 stats, 78 allocations
  • 静的ファイル非ヒット時:
    • stat 回数: 最大 9 → 1〜2 回程度に激減
    • オブジェクトアロケーション: おおよそ 1/3
  • 静的ファイルヒット時:
    • stat は 1 回増え、alloc もわずかに増える
      → これは「ほとんどのアプリでは静的ファイルは CDN やフロントサーバで配信し、Rails 本体での静的ファイルヒットはかなり少ない」前提のもと、十分許容できるトレードオフと位置付けられています。

  1. 影響範囲・注意点
  • 対象となる仕組み:
    • ActionDispatch::Static
    • 内部で使われる ActionDispatch::FileHandler
  • 影響する場面:
    • Rails 本体に静的ファイル配信を任せている環境(特に開発環境や CDN / リバースプロキシなしのシンプルな構成)
  • 期待されるメリット:
    • 動的リクエスト(/users/1 等)に対するミドルウェアオーバーヘッドの削減
    • アロケーション削減による GC 負荷の低減
  • 挙動上の互換性:
    • 静的ファイルが実際に存在する / しないという意味でのルーティングの結果は変わりません
    • public/ にファイルを後から追加するケースや symlink されたディレクトリにも対応したままです
    • ファイルシステムの大文字小文字の違いも従来通り FS 任せ
  • 注意点:
    • ごく一部のケースで、静的ファイルヒット時の stat 回数が 1 回増えます(パフォーマンスへの影響はごく軽微)
    • public/ のトップレベル構成や symlink の張り方が、これまでと違う解釈をされることはないはずですが、独自パッチや monkey patch を ActionDispatch::Static に当てている場合は、トップレベルエントリの存在チェックロジックと干渉しないか確認が必要です。

  1. 参考情報 (あれば)
  • PR 本文で言及されているユースケース:
    • CarrierWave 等で public/uploads にファイルを保存するような構成
    • Capistrano デプロイで public/assets をシンボリックリンクにしている構成
  • 実装が入っているファイル:
    • 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. 概要 (1-2文で)
    Action Text のシステムテストがローカル環境で失敗し、Chrome がオフライン状態のまま残ってしまう問題と、Puma 起動時に RACK_ENV がリークしたように見える問題を修正する PR です。テストコード側でネットワーク条件の扱いと RACK_ENV の初期化方法を変更することで、JSON 3.0 以降での例外や LeakChecker の誤検知を防いでいます。

  1. 変更内容の詳細

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 を読み取って保存・復元する方式をやめました。
  • 代わりに、オフラインにする際は「最初からシンボルキーで組み立てたハッシュ」を渡し、復帰時は「オフラインフラグだけを確実に戻す」方針に変更しています。

イメージとしては以下のような変更です(実際のコード断片イメージ):

ruby
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 から見て「テスト外での初期状態として既に存在する環境変数」となるため、リーク扱いされません。

イメージとしては以下のような行が追加されています:

ruby
ENV["RACK_ENV"] ||= "test"

  1. 影響範囲・注意点
  • 影響範囲は Action Text のシステムテスト (actiontext/test/system/*) と、そのベースクラス (ApplicationSystemTestCase) に限定されています。
  • while_offline の挙動変更により、「テスト実行前に Chrome に独自の network_conditions を設定していた場合」に、それを完全に復元しない可能性があります。ただし現状のテストスイートでは、単純にオンライン・オフラインを切り替える用途のみのため、実害はほぼない想定です。
  • RACK_ENV が "test" に固定されるのは「Action Text のシステムテストが読み込まれるタイミング」であり、通常のテスト実行(bin/test / rails test)ではもともと "test" であることが期待されるため、想定外の影響は小さいです。
  • 逆に、RACK_ENV をあえて未設定のまま特殊な挙動をテストしていた場合は、この変更によって条件が変わる可能性がありますが、一般的な Rails テスト環境ではほぼ想定されていません。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    ActiveRecord のキー処理で「単一キー(非複合主キー)」のケースを特別扱いし、不要な Array 経由のメソッド呼び出しを避けることでパフォーマンスを改善した PR です。RubyBench の ActiveRecord ベンチマークで、おおよそ 210ms → 201ms 程度(約 4〜5%)の短縮が報告されています。

  1. 変更内容の詳細(あればサンプルコードも含めて)

何をしているか ​

  • 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 呼び出しを回避。

※ 実際のコードイメージ(あくまでイメージ例):

ruby
# 以前(単一キーでも配列扱いしていたイメージ)
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 を多用するアプリケーションでは無視できない微小最適化です。


  1. 影響範囲・注意点

影響範囲 ​

  • 対象:
    • ActiveRecord のキー処理 (ActiveRecord::Key 周辺)。
    • 特に「単一主キーのモデル」(一般的な id を持つ標準的なテーブル)が最も恩恵を受ける。
  • 非対象/影響が小さい:
    • 複合主キー(複数カラムからなる主キー)の処理パスは従来通りで、動作は変わらない(パフォーマンスもほぼ据え置き)。

後方互換性 ​

  • API の変更やシグネチャ変更はなく、既存のアプリケーションコードに対しては完全に後方互換のパフォーマンス改善です。
  • ActiveRecord::Base や関連クラスを通常どおり使っている限り、明示的な対応やコード修正は不要です。

注意点 ​

  • 低レベルで ActiveRecord::Key を直接利用しているようなメタプログラミング・プラグイン・ライブラリを書いている場合は、挙動の違い(単一キーが別コードパスで扱われること)を前提にしていないかどうかを確認しておくと安全です。
    • もっとも、この PR 内容から判断する限りは「yield の呼び出し経路を変えただけ」であり、受け取る値の型や意味づけは変わらないはずです。
  • パフォーマンス改善は相対的に小さいため、アプリ全体のレスポンス改善量はワークロード次第です。
    • DB I/O がボトルネックなケースでは体感差はほぼ出ない可能性もあります。
    • 逆に、AR オブジェクト生成やキー操作を極端に大量に行うバッチ・バックグラウンド処理では有利になります。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    has_and_belongs_to_many が内部的に生成していた中間モデルの left_side 関連(belongs_to :left_side)が、実際にはまったく機能しておらず常に nil を返していたため、不要なコードとして削除された PR です。これにより、HABTM の実装が少しシンプルかつ一貫したものになります。

  1. 変更内容の詳細

何が問題だったか ​

has_and_belongs_to_many は、内部的に「中間モデル(join model)」相当の情報を持ちますが、その定義の中で長らく以下のような「左側への逆関連」を持っていました(擬似コード):

ruby
# 中間モデルの定義イメージ
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 に関連する行が削除されただけの、非常に小さなリファクタリングです。


  1. 影響範囲・注意点

影響範囲 ​

  • 通常の 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 + 明示的な中間モデルを定義した設計に移行することが推奨されます。

  1. 参考情報 (あれば)

関連するIssue / PR:

  • 元になった変更:
    • #58197: left_reflection の削除(今回のPRの前段となるリファクタリング)
  • 本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. 概要 (1–2文で)
    Active Job のリトライ処理で、デシリアライズ元のジョブデータ(特に exception_executions)が書き換えられてしまう問題を修正し、ジョブごとに独立したカウンタを持つようにした PR です。これにより、オリジナルのジョブデータが意図せず変化したり、凍結オブジェクトで FrozenError が発生する問題が解消されます。

  1. 変更内容の詳細

問題の背景 ​

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 を「元データからコピー」するようになりました。

イメージとしては次のような変更です(擬似コード):

ruby
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 にエントリが追加されており、

  • 「リトライ時にデシリアライズ元データを変更しないようにした」
    といった趣旨のバグ修正として記載されています(動作変更として利用者が把握すべき項目)。

  1. 影響範囲・注意点
  • 影響範囲

    • retry_on(および関連する例外リトライ機構)を利用している Active Job 全般。
    • performed_jobs / enqueued_jobs など、テストヘルパ経由でジョブデータを参照しているコード。
      以前は「後からリトライが走るとカウンタが書き換わってしまう」状態でしたが、今後は「記録時点の値のまま固定」される挙動になります。
    • ジョブのシリアライズペイロードを凍結したり、Ractor 間共有のために shareable にしている高度な利用パターン。
  • 互換性

    • 期待される挙動としては「バグ修正」であり、破壊的変更ではありません。
    • ただし、もしアプリ側で「同じハッシュオブジェクトが共有されている」ことに依存していた場合(ほぼありえない設計ですが)、今回の変更でその前提は成り立たなくなります。
    • performed_jobs の exception_executions カウンタ値が、これまでと変わる可能性があります(これまでは「最終リトライ回数」、今後は「各実行時点の回数」になる)。
  • 注意点

    • 「リトライを繰り返した結果としてのトータル回数」を一か所のジョブデータから読みたい、という用途には今後も Active Job 自体は特に API を提供していません。必要であればアプリ側で独自にカウンタを管理するほうが安全です。
    • ジョブペイロードを積極的に immutable にしている環境(Ractor など)では、今回の修正により、retry_on を使った場合の FrozenError が避けられるはずです。

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/58975
  • 関連ドキュメント:
  • 概念的には、「シリアライズされた状態(ストレージに保存されるペイロード)」と「実行時インスタンスの状態」を明確に分離し、実行中に後者を変えても前者には影響を与えないようにする、という設計上の是正です。

#58978 Make Active Job argument serialization work in non-main Ractors ​

マージ日: 2026/10/14 | 作成者: @yaroslav-shopify

  1. 概要 (1-2文で)
    Active Job が Ractor(特にメイン Ractor 以外)上でジョブ引数をシリアライズ / デシリアライズする際に Ractor::IsolationError が発生していた問題を修正し、Ractor 対応を強化する PR です。
    ractorize! 実行時に Active Job のシリアライザ関連オブジェクトを「shareable」にする仕組みを追加し、非メイン Ractor からでも安全に利用できるようにしています。

  1. 変更内容の詳細

問題の背景 ​

  • 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! 実行時に以下を行うようにしています。

  1. ActiveJob::Arguments をロード

    • これにより、ActiveSupport.on_load(:active_job) 等のフックを通して、カスタムシリアライザも含めた各種シリアライザがレジストリに登録される。
  2. 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 から利用可能であることの検証。

  1. 影響範囲・注意点

影響範囲 ​

  • 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 している場合は、それが壊れるリスクが高まります。

  1. 参考情報 (あれば)
  • 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. 概要 (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 上で安全に動作します。

  1. 変更内容の詳細

背景となる問題 ​

  • ractorize! を有効にして Job を非メイン Ractor で実行すると、ジョブ内で例外が発生したときに:
    • rescue_handlers(rescue_from のハンドラ)
    • after_discard_procs(discard_on・after_discard 周り)
  • これらが Ractor の shareable 制約を満たしていないため Ractor::IsolationError を起こし、結果として:
    • rescue_from
    • retry_on
    • discard_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 になりにくくなります。
ruby
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 フレンドリーに変更しています。

ポイント:

  1. ハンドラは「shareable な引数」から構築

    • exceptions, wait, attempts, queue, priority など、基本的にイミュータブル or freeze 済みの値から Proc を作る形にして、Proc 自体を shareable にしやすくしています。
  2. 「block を yield する」のではなく「block を直接 call」

    • 以前は「外側の Proc から yield する」ような構造になっていて、Ractor 的に扱いづらい(shareable 化しづらい)部分がありました。
    • これを「block を閉じ込めた Proc を作り、その Proc を直接 call する」ような形に変更することで、ActiveSupport::Ractors.unshareable_proc_action による shareable 化が適用しやすくなっています。

イメージ(簡略化サンプル、実際の実装とは異なりますが構造イメージ):

ruby
# 以前はこういうイメージだった:
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 コールバックと関係があります。

今回の変更で:

  1. after_discard に登録される Proc を shareable 化

    • rescue_from と同様に、ActiveSupport::Ractors.unshareable_proc_action の設定にしたがって Proc を shareable にします。
  2. 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 を出さずに動くこと。

  1. 影響範囲・注意点

影響範囲 ​

  • ractorize! を有効にしている(もしくは今後有効にする)アプリケーションにとっては、非メイン Ractor で実行される:
    • Active Job の例外処理 (rescue_from, retry_on, discard_on, after_discard)
    • controllers / mailers / channels の rescue_from
  • が、より安全に正しく動作するようになります。

互換性・挙動の変化 ​

  • パブリック API としてのシグネチャは変わっていないため、通常の(シングル Ractor / マルチスレッド)利用におけるコード修正は基本的に不要です。
  • ただし、例外ハンドラや after_discard ブロック内で「非 shareable なオブジェクト(例えばミューテーブルなグローバル状態など)」をキャプチャしている場合、Ractor 下では問題になる可能性があります。
    • 例: ハンドラブロックがクラスインスタンス変数やグローバルなキャッシュオブジェクトを閉じ込めている場合など。
  • after_discard_procs のデフォルトが frozen Array になった点は、内部利用前提のため通常のアプリ開発者には影響はありませんが、
    • メタプログラミング等で直接この配列を操作している場合は、dup してから変更する必要があります。

Ractor は依然として experimental な領域 ​

  • ractorize! 自体が :nodoc: で experimental な位置付けのため、将来的に API や挙動が変わる可能性があります。
  • 本 PR も「Ractor を本格的に使うための安定化の一歩」程度の位置づけであり、Ractor 利用時のすべてのケースを完全にカバーした保証ではありません。

  1. 参考情報 (あれば)
  • 対象 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. 概要 (1-2文で)
    このPRは、Arel::Table や Arel::Nodes::TableAlias から元の Active Record モデル (klass) を直接取得できるようにし、association_scope 内のスコープ適用処理が「テーブル」からモデルに自然に到達できるようにするリファクタリングです。これにより、関連スコープ構築時にリフレクション経由ではなくテーブルオブジェクト経由で PredicateBuilder を組み立てられるようになります。

  1. 変更内容の詳細

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)

概念的には以下のような委譲が追加されています (擬似コード):

ruby
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後は概ね次のような形になります (イメージ):

ruby
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 引数) だけを見ればよい」形になります。
  • 実装の責務分担としても、「テーブルが自分の元モデルを知る」「スコープは受け取ったテーブルからモデルを知る」という自然な流れになります。

  1. 影響範囲・注意点

3-1. 外部的な挙動への影響 ​

  • 公開APIの振る舞いとしては基本的に互換性を保ったリファクタリングであり、アプリケーションコードから見える挙動に直接の変更はほぼありません。
  • 主に内部実装 (association_scope・apply_scope・Arel のテーブル/別名周り) の整理です。

3-2. 期待されるメリット ​

  • コードの単純化と可読性の向上
    • association_scope 周りが、リフレクションに過度に依存せず、「テーブルからモデルへ」という一方向の依存に整理されます。
  • メンテナンス性
    • 将来的に関連スコープ構築処理 (特に多段 JOIN や別名テーブルを使う部分) をいじるときに、table.klass でモデルを取得できるので処理の見通しがよくなります。
  • 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 を前提としているため、通常のアプリケーションでは問題にならないはずです。

  1. 参考情報 (あれば)
  • このPRはコミット 6af2260f981af6812f8f9e4047ee15497fbfb57b のフォローアップと明記されています。
    • そのコミットでは、おそらく Arel::Table と Active Record モデルの関連付けや type_for_attribute 委譲まわりの改善が行われており、本PRはその仕上げとして klass への到達も統一したものと考えられます。
  • 関連する内部API:
    • ActiveRecord::PredicateBuilder
    • ActiveRecord::Associations::AssociationScope
    • Arel::Table, Arel::Nodes::TableAlias

#59066 Use an ivar for the primary key definition ​

マージ日: 2026/10/14 | 作成者: @tenderlove

  1. 概要 (1-2文で)
    このPRは、Active Record の primary_key_definition の計算結果をインスタンス変数にキャッシュすることで、Rails main で劣化していた ActiveRecord ベンチマークのパフォーマンスを一部取り戻す変更です。内部実装のみを触っており、外から見たAPIや挙動は(バグがなければ)変わりません。

  1. 変更内容の詳細

※ソース全文は提示されていないので、説明は 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)に保持し、同じインスタンスでは再計算しないようにしています。

典型的には以下のような変化が入っていると考えられます(イメージコード):

ruby
# 変更前(イメージ)
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 キャッシュも一緒にクリアされることが保証されるようなテストが追加されているはずです。


  1. 影響範囲・注意点

3-1. 性能面の影響 ​

Ruby-Bench の ActiveRecord ベンチマークにおいて:

  • Rails main: 平均 213ms
  • このブランチ: 平均 201ms
  • Rails v8.1.4: 平均 131ms

と報告されています。

この PR によって main ブランチの退行分を一部取り戻してはいるものの、まだ 8.1.4 よりもかなり遅い状態である、という位置付けです。
primary_key_definition の呼び出しが多いアプリ(特に大量のレコードを扱う、またはメタ情報に頻繁にアクセスするケース)では、わずかながらレスポンス改善が期待できます。

3-2. 挙動面の影響 ​

基本的には「内部的に結果をキャッシュするだけ」なので、通常の以下のようなコードの挙動は変わらない前提です。

ruby
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 更新時には一応テストを回しておくと安心です。


  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/59066
  • 関連箇所(参考用・現行の main とは差異がある可能性があります)
    • activerecord/lib/active_record/attribute_methods/primary_key.rb
    • ActiveRecord::Base#reset_column_information / スキーマキャッシュまわりのコード
  • 性能劣化の全体像としては、この PR だけで 8.1.4 の速度には戻っておらず、main ブランチでのパフォーマンス退行を戻すための「一手」という位置づけです。

#59078 Zip keys without going through Enumerable ​

マージ日: 2026/10/14 | 作成者: @matthewd

  1. 概要 (1-2文で)
    ActiveRecord の ActiveRecord::Key に専用の zip 実装を追加し、Enumerable 経由の汎用実装を避けることで、主キー/外部キーまわりのペア操作のメモリアロケーションを大幅に削減した PR です。これに伴い、replace_keys・keys_match?・join_scope などの内部ループが zip ベースに書き換えられ、パフォーマンス改善が期待できます。

  1. 変更内容の詳細

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 で走査する実装に置き換えた形です。

想定されるコードイメージ ​

ruby
# 以前: 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) でペアを生成し、その組をループで処理する

という変更です。

擬似コード:

ruby
# 以前のイメージ
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 代入
end

Key#zip の高速化により、インデックスベースよりも簡潔かつ低アロケーションで書けるようになっています。

変更された主な箇所:

  • belongs_to_association.rb
    • replace_keys など、関連のキー更新処理でのループを書き換え
  • reflection.rb
    • join_scope 構築時に、主キー/外部キーをペアにして結合条件を作る部分を zip ベースに変更

これにより、複合主キー・複合外部キーを持つ関連でも、結合条件やキーのセットアップ時のオーバーヘッドが減少します。


2-3. Key のテスト追加 (key_test.rb) ​

activerecord/test/cases/key_test.rb に Key#zip に関するテストが追加されています。

想定されるテスト内容:

  • 単一カラム同士の Key を zip したときの挙動
  • 複合カラム同士の Key を zip したとき、各ポジションが正しくペアになっているか
  • ブロック付き・ブロックなしの両方で正しく動作するか

Key の public API として zip を期待通りに扱えることを保証するための追加です。


  1. 影響範囲・注意点
  • 影響範囲

    • ActiveRecord 内部で ActiveRecord::Key#zip を使っている箇所(belongs_to の関連処理、join_scope の生成など)が対象
    • ユーザーコードで primary_key.zip(foreign_key) のように Key オブジェクトに対して zip を呼んでいる場合も、そのまま高速化の恩恵を受けます
    • 複合キー(Composite key)を利用した関連定義でも、キー比較・コピー・JOIN 条件生成の性能が向上
  • 互換性・挙動面の注意

    • Enumerable#zip と互換なインターフェイスを保つよう実装されているはずですが、zip の返り値の型や細かい挙動(例: 長さが違う key 同士の扱い、nil の扱いなど)は Key 側の実装に依存します
    • これまで Enumerable の実装前提でモンキーパッチを当てていた場合(かなりレアケース)には動作が変わる可能性があります
    • 公開 API として ActiveRecord::Key を直接扱うのは一般的ではないため、通常のアプリケーションコードには基本的に非互換影響はありませんが、ORM 内部の動作をフックしている高度なメタプログラミングコードは一応確認した方が安全です
  • パフォーマンス観点

    • 説明にある通り、特にブロック付きの zip におけるメモリアロケーションが顕著に削減されています
    • 大量のレコードを関連を辿りながら処理するケース、複合キーを多用するケースで GC 負荷軽減・スループット向上につながる可能性があります

  1. 参考情報 (あれば)
  • 元 PR / 関連 PR
    • #59063, #59068: 今回はこれらのフォローアップとして、each や map と同様に zip も Enumerable をバイパスする最適化を追加
  • 実装の意図
    • Ruby の Enumerable#zip は便利な一方、配列生成や中間オブジェクト生成によるアロケーションコストが高く、ActiveRecord 内部のホットパスに載せるには重い
    • Key はサイズが小さく構造も単純なため、自前ループで完結させることで GC 負荷を削減する方針
  • 実務的なポイント
    • アプリ側のコード変更は不要で、Rails をこのバージョン以降に上げるだけで、主キー/外部キー処理の一部が自動的に高速化される
    • ベンチマークを行っているプロジェクトであれば、関連を多用するクエリのパフォーマンス計測時に、この変更の効果が現れていないか確認する価値があります。

#58948 Apply the table name prefix to the index name 4.2 migrations compute ​

マージ日: 2026/10/14 | 作成者: @carlosdanielpohlod

  1. 概要 (1-2文で)
    Rails 4.2 互換レイヤのマイグレーションで、table_name_prefix が設定されている場合にインデックス名の計算が間違っていた問題を修正する PR です。index_exists? と remove_index が、プレフィックス付きの正しいテーブル名からインデックス名を計算するように変更されています。

  1. 変更内容の詳細

問題の背景 ​

ActiveRecord::Migration::Compatibility::V4_2 に定義されている index_exists? と remove_index は、以下のような流れになっています:

  1. まずコネクションに「インデックス名」を問い合わせて options[:name] を決定する
  2. その後 super を呼び出す
  3. super は最終的に Migration#method_missing にフォールスルーし、proper_table_name を使ってテーブル名にプレフィックスを付与する

ところが、すでに計算済みの options[:name](インデックス名)はそのまま で、プレフィックスはテーブル名にしか反映されません。

table_name_prefix を例にすると:

ruby
# config/application.rb 等で
config.active_record.table_name_prefix = "p_"

テーブル testings に対して foo カラムにインデックスを張ると、実テーブル名と実インデックス名は:

  • テーブル: p_testings
  • インデックス: index_p_testings_on_foo

になりますが、問題のバグがあると:

ruby
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(= プレフィックス適用済みテーブル名)を使う ように変更されています。

擬似コードレベルでは、従来:

ruby
def index_exists?(table_name, column, options = {})
  # table_name は "testings" のまま
  options[:name] ||= connection.index_name(table_name, column)
  super
end

のようにプレフィックスなしの table_name から index name を作っていたところを、

ruby
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, :foo
  • index_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 行のエントリが追加されています。

  1. 影響範囲・注意点
  • 影響範囲:
    • 対象は Rails のマイグレーション互換レイヤ ActiveRecord::Migration::Compatibility::V4_2 のみ です。
    • config.active_record.table_name_prefix を利用しているプロジェクトで、
      • Rails 4.2 形式のマイグレーションを互換モードで実行している
      • かつ index_exists? / remove_index を使用している 場合に、これまで正常に動いていなかった処理が正しく動くようになります。
  • 互換性:
    • 挙動変更は バグフィックス であり、設計意図に沿った動きに修正するものです。
    • 既に「バグありの挙動」に依存してしまっているコードがある場合(例: remove_index がエラーになる前提の rescue ロジックなど)は、エラーが発生しなくなることで挙動が変わる可能性がありますが、通常は望ましい修正と考えられます。
  • 実運用上の注意:
    • 過去に table_name_prefix を使っていて、4.2 形式マイグレーションで「インデックスが存在しない」と言われ続けていた場合、この修正適用後に
      • index_exists? が true を返すようになったり
      • remove_index が正常にインデックスを削除できるようになる 可能性があります。
    • 古いマイグレーションを再実行するケース(テスト環境・CI での db:test:prepare や db:reset 等)で挙動が変わりうるため、table_name_prefix 利用プロジェクトでは一度マイグレーションの通し実行を確認すると安心です。

  1. 参考情報 (あれば)
  • 類似修正: 6ca42300e8(V5_1#change_column での proper_table_name 適用)
    → この PR はそのパターンを 4.2 のインデックス関連メソッドに拡張したものです。
  • 関連コード:
    • ActiveRecord::Migration::Compatibility::V4_2
    • ActiveRecord::Migration#proper_table_name
  • 利用者向けまとめ:
    • table_name_prefix を使っている場合でも、Rails 4.2 互換マイグレーションで
      ruby
      index_exists?(:testings, :foo)
      remove_index :testings, :foo
      が 期待通りプレフィックス付きテーブルのインデックスを扱えるようになった、というのがこの PR の本質的な変更点です。

#59033 Skip ROLLBACK when SQLite already rolled back the transaction ​

マージ日: 2026/10/14 | 作成者: @cole-robertson

  1. 概要 (1-2文で)
    SQLite が自動でトランザクションをロールバックしている場合に、Rails 側で余計な ROLLBACK を発行しないようにした修正です。これにより、SQLite のディスクフルなどで発生する本来の例外が正しく表に出るようになり、トランザクション内のレコード状態や after_rollback コールバックの挙動も一貫性のあるものになります。

  1. 変更内容の詳細

問題の背景 ​

SQLite では、以下のようなエラーが発生した場合に SQLite 自身が自動的にトランザクションをロールバック します。

  • SQLITE_FULL(ディスクフル / ページ不足)
  • SQLITE_IOERR
  • SQLITE_NOMEM
  • など、致命的な書き込み系エラー

このとき、Rails の SQLite3 アダプタは従来どおり ROLLBACK を発行していましたが、既にトランザクションが終了しているため SQLite 側は:

text
SQLite3::SQLException: cannot rollback - no transaction is active

というエラーを返します。この二重エラーにより次の問題が起きていました。

  1. 本来のエラーが隠れてしまう

    • 期待される例外: 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
      のようなハンドリングが動作しない。
  2. ActiveRecord のトランザクション整合性が壊れる
    エラーが ROLLBACK の失敗で握り潰されることで、rollback_records が実行されず、次のような不整合が起きます。

    ruby
    Item.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? # => true
      • first.id # => 1
      • 実際のテーブル行数: 0
      • after_rollback コールバック呼び出し: 0
    • 修正後:
      • 例外: ActiveRecord::StatementInvalid: SQLite3::FullException: database or disk is full
      • first.persisted? # => false
      • first.id # => nil
      • after_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 の挙動が期待通りになること
    • を検証するテストです。
  • このテストは、変更前には失敗し(cannot rollback - no transaction is active が返ってくる)、変更後には成功します。

3. CHANGELOG の更新 ​

activerecord/CHANGELOG.md に、この挙動変更がバグ修正として追記されています。これにより、ライブラリの利用者は挙動変更を把握できます。


  1. 影響範囲・注意点

影響範囲 ​

  • 対象: SQLite3 アダプタを使っている Rails アプリケーション
  • 影響する状況:
    • トランザクション内のクエリが
      • ディスクフル (SQLITE_FULL)
      • IO エラー (SQLITE_IOERR)
      • メモリ不足 (SQLITE_NOMEM)
      • などで失敗し、SQLite 側が自動的にロールバックを行ったケース
  • 他の RDBMS(PostgreSQL / MySQL 等)のアダプタには影響しません。

挙動の変化 ​

  1. 例外ハンドリングのしやすさが向上
    これまでは、次のような例外ハンドリングは期待通り動かないことがありました。

    ruby
    begin
      Item.transaction do
        # 大きな書き込み処理
      end
    rescue SQLite3::FullException
      # ディスクフル時のリトライや通知処理
    end

    修正後は ActiveRecord::StatementInvalid 経由であっても、外側に現れる原因例外 (cause) として SQLite3::FullException などが出るため、rescue SQLite3::FullException による分類が正しく機能するようになります。

  2. トランザクション内オブジェクトの状態が DB と一致する

    • エラー発生後に persisted? が false になり、id が nil になる
    • after_rollback コールバックが正しく実行される

    これにより、「DB には存在しないのに、アプリケーションオブジェクト的には存在すると認識される」という不整合が解消されます。

注意点 ​

  • もし既存コード側で、「cannot rollback - no transaction is active というエラーメッセージに依存した挙動(文字列マッチなど)」をしている場合、そのコードは動作しなくなります。代わりに、例外クラスや cause に対する判定に移行することが推奨されます。
  • 例外のトップレベルは引き続き ActiveRecord::StatementInvalid でラップされるため、「ActiveRecord 例外としてハンドリングする」という方針自体は変わりません。必要に応じて e.cause で元の SQLite 例外を参照してください。
  • SQLite のバージョンや sqlite3 gem のバージョンによっては、エラークラス・メッセージが微妙に違う可能性があるため、厳密なメッセージ文字列ではなくクラスに基づいたハンドリングを行うのが安全です。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    PostgreSQL Adapter の supports_native_partitioning? メソッドが、サポート対象の PostgreSQL バージョンが 10 以上になったことで常に true しか返さなくなったため、非推奨化された PR です。これに合わせて、このメソッドに依存していた内部処理とテストが整理されています。

  1. 変更内容の詳細

背景 ​

  • #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 を返すが、外部からの利用は推奨されない。

イメージ:

ruby
# 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 NULL instead of pg_get_partkeydef() when the method returns false. This PR reads the partition key unconditionally

つまり、以前は:

  • supports_native_partitioning? が true → pg_get_partkeydef() でパーティションキー取得
  • false → 型無し NULL を選択していた

という分岐があったのが、今後は:

  • 常に pg_get_partkeydef() を使う

ようになります。

イメージ:

ruby
# 以前
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? の非推奨化が記載され、将来の削除候補であることが明示されています。

  1. 影響範囲・注意点

アプリケーション/ライブラリ側からの影響 ​

影響を受けるのは主に「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 扱いに書き換える ことを推奨します。

  1. 参考情報 (あれば)

この PR は「内部的にはもう意味のない feature flag を整理し、将来の API からも削除するための下準備」という位置づけと考えると分かりやすいです。


#59069 Match sequence reset rows by the table name given, not regclass::text ​

マージ日: 2026/10/14 | 作成者: @yahonda

  1. 概要 (1-2文で)
    PostgreSQL アダプタで reset_pk_sequence! / reset_column_sequences! が、スキーマ付きテーブル名(例: "public.posts")に対して NoMethodError になる不具合を修正した PR です。テーブルとシーケンスの紐付け方法を regclass::text ベースから OID ベースに変更することで、スキーマ付きのテーブル名でも安全にシーケンスリセットできるようにしています。

  1. 変更内容の詳細

問題の背景 ​

Rails 8.1 では、以下のようにスキーマ修飾したテーブル名を渡すことが許容されています:

ruby
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_sql
  • uuid_column_and_sequence_sql

について、テーブル名のマッチング方法を変更 しています。

従来:

  • すべての対象テーブルを PostgreSQL に投げて、
    pg_class などから主キー列・シーケンス名を取得
  • 結果は regclass::text 由来のテーブル名をキーとして返す
    • search_path 上の "public.posts" は "posts" として返ってくる
  • Ruby 側では、元々のテーブル名 ("public.posts") をキーにして結果を探すためミスマッチ

修正後:

  1. Ruby 側で対象テーブルの (name, oid) のペアをあらかじめ構築
    • name: 呼び出し元から渡されたテーブル名 ("public.posts" など)
    • oid: pg_class から取得したテーブル OID
  2. この (name, oid) の一覧を VALUES 句として SQL 内でインライン化し、一時テーブルのように扱う
  3. pg_class 等と OID で JOIN することで、regclass::text を使わずにテーブルを特定
  4. 結果セットには 最初に渡した 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! が正しく動作することが検証されています。


  1. 影響範囲・注意点
  • 影響を受ける主なケース
    • PostgreSQL を使用している
    • reset_pk_sequence! あるいは reset_column_sequences! を スキーマ付きテーブル名(例 "public.posts") で呼び出している
    • または、モデルの table_name を "public.posts" のようにしており、fixtures ロード時に内部でこれらが使われるケース
  • この PR によって、これらのケースで発生していた NoMethodError が解消されます。
  • クエリ構造は変わっていますが、
    • クエリ回数は同じ
    • OID での JOIN 自体は PostgreSQL では一般的で安定したやり方 のため、性能や安定性への大きな悪影響は想定しづらいです。
  • CHANGELOG は未更新(チェックリスト上も未チェック)なので、当面は「内部的なバグフィックス」として扱われる可能性があります。

  1. 参考情報 (あれば)
  • 関連 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. 概要 (1–2文で)
    Active Storage のサービス設定時に、既に定義済みのサービスクラスに対して不要な require を行わないように ActiveStorage::Service::Configurator が変更されています。これにより、独自サービスクラスを Active Storage 内部のディレクトリ構造に従って配置しなくても利用しやすくなります。

  1. 変更内容の詳細

問題背景 ​

従来の Active Storage では、storage.yml でカスタムサービスを指定する場合、Active Storage 側が

ruby
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 を試みる

という挙動に変わっています。

コードは概ね以下のようなイメージに近い変更です(擬似コード):

ruby
# 変更前 (イメージ)
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 する」という点にあります。

カスタムサービスの利用イメージ ​

この変更により、たとえば以下のような構成が自然に動きやすくなります。

ruby
# app/services/my_custom_storage_service.rb
class MyCustomStorageService < ActiveStorage::Service
  # 必要なメソッドを実装
end
yaml
# config/storage.yml
my_custom:
  service: MyCustomStorageService
  # 追加の設定...

上記のように、Rails の autoload パスに載っているクラス名を service に直接指定しておけば、すでにクラスがロード済みなら Active Storage が無理に require しないため、active_storage/service 配下にファイルを置く必要がありません。


  1. 影響範囲・注意点
  • 影響範囲

    • ActiveStorage::Service::Configurator を使ってサービスインスタンスを組み立てる全てのコードパスに影響します。
    • 公式・標準のサービス (Disk, S3, GCS など) はこれまで通り動作しますが、内部的には「定義済みチェック」が追加されただけなので、既存挙動に対する後方互換性は高いと考えられます。
    • カスタムサービスクラスを既に Active Storage 規定のパス (active_storage/service/...) で運用しているケースも、そのまま動きます。
  • 注意点

    • 「クラスが定義済みかどうか」の判定は、通常 const_defined? あるいは safe_constantize などで行われます。Zeitwerk / autoload のタイミングに依存するため、次の点に注意が必要です。
      • カスタムサービスクラスを利用する場合は、少なくとも Active Storage が configurator を呼ぶ時点までにクラスが autoload 可能な状態になっている必要があります(app/ 以下 or config.autoload_paths に追加されたディレクトリに配置するなど)。
      • Rails 起動時に eager load している環境では、config.eager_load_paths に含まれているかも確認するとよいです。
    • 万一「定義済みチェックが true だが中身が意図したクラスでない」といった定数衝突があると、これまでとは異なるクラスが使われてしまう可能性があります。そのようなケースは稀ですが、ライブラリ間で同名クラス(同じ定数名)を定義していると影響を受けえます。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Rails アプリを ractorize! した後でも、非メイン Ractor(ジョブやバックグラウンド処理など)で ActiveSupport の非推奨警告(deprecation)が本来の設定どおりに動作するようにする修正です。これにより、これまで非メイン Ractor 上で deprecation を扱おうとすると発生していた Ractor::IsolationError や FrozenError を回避します。

  1. 変更内容の詳細

2-1. 問題点 ​

ractorize! を呼ぶとアプリケーションの deprecator オブジェクトが freeze されます。この状態で、メイン Ractor 以外(例: ジョブワーカー Ractor)から:

  • 非推奨警告を発生させる
  • ActiveSupport::Deprecation#silence を使って一時的に警告を抑制する

といった操作を行うと、内部的に以下のような例外が発生していました。

  1. Concurrent::ThreadLocalVar::LOCALS へのアクセスで Ractor::IsolationError
    • 従来の実装では「スレッドローカルな状態」で deprecation のサイレンスカウンタなどを管理しており、その実装が Ractor 間共有に適合していなかった。
  2. 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 する」といった振る舞いが、従来通り(あるいはより一貫して)機能する

概念図イメージ:

ruby
# 例: 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 を避ける
      • パフォーマンス面での些細な改善(再構築を防ぐ)も副次的に期待できる

(D) テスト ​

追加・更新されたテスト:

  • activesupport/test/deprecation_test.rb (+37行)
    • IsolatedExecutionState 上で deprecation の silence や allow が期待どおりにカウント・適用されることを検証していると考えられます。
  • railties/test/application/ractors_test.rb (+21行)
    • Rails アプリを実際に ractorize! し、非メイン Ractor から deprecation を発生させても:
      • 例外 (Ractor::IsolationError / FrozenError) が出ない
      • 設定済みの deprecator の挙動が尊重される といったことを確認。

Ruby 4.0 と Ruby master で検証済みと明記されています。


  1. 影響範囲・注意点

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 のサイレンスを行っているような高度な使い方をしていると、期待と異なる可能性があります(一般的な使い方では問題にならないはずです)。
  • deprecator の freeze タイミング前後で挙動(設定可能かどうか)が変わる点には従来から注意が必要ですが、今回の変更はその制約を明確にしつつ Ractor でも安全に使えるようにしたものです。

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/58981
  • 関連クラス:
    • ActiveSupport::Deprecation
    • ActiveSupport::IsolatedExecutionState
    • LoggerThreadSafeLevel
  • コンテキスト:
    • 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. 概要 (1-2文で)
    Rails の PostgreSQL 用 SchemaDumper が、拡張機能(extensions)が存在するスキーマを漏れなく create_schema ... if_not_exists: true でダンプするようになり、dump_schemas に含まれないスキーマにインストールされた拡張でも db:schema:load が失敗しないように修正されています。特に btree_gin や pg_trgm のような「移動可能(relocatable)」な拡張をカスタムスキーマに入れているアプリのスキーマロード問題を解消します。

  1. 変更内容の詳細

何が問題だったか ​

  • Rails の schema.rb 生成時 (ActiveRecord::SchemaDumper) に、以下のようなケースがあった:

    • 拡張が public ではなく、別スキーマ(例: extensions)にインストールされている
    • しかし ActiveRecord.dump_schemas = "public" などで、dump 対象スキーマにそのスキーマが含まれていない
  • その場合、ダンプされた schema.rb は:

    ruby
    ActiveRecord::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 スキーマが存在しないため:

    text
    PG::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 には例えば次のような形で出力される:

    ruby
    ActiveRecord::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 が防がれる。

実装上のポイント ​

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_ プレフィックスはシステムスキーマ用に予約されている)ため。
  • 既存のダンプで 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 でグリーン。

  1. 影響範囲・注意点
  • 影響を受けるケース:
    • 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 外のスキーマにある拡張は有効化しない)はあえて採用しておらず、「アプリが必要としている拡張は必ず有効にする」側に振っている点に注意。

  1. 参考情報 (あれば)

#59065 Reduce method calls in fast path ​

マージ日: 2026/10/14 | 作成者: @tenderlove

  1. 概要 (1-2文で)
    primary_key_definition メソッドの「よく通るパス(fast path)」でのメソッド呼び出し回数を 2 回から 1 回に削減し、パフォーマンスを改善するための微調整が行われました。機能的な挙動は変えずに、内部実装の呼び出しコストを下げるリファクタリングです。

  1. 変更内容の詳細

※PR本文と変更行数(+4/-2)から推測を含みますが、意図はほぼ明確です。

  • 対象: ActiveRecord::AttributeMethods::PrimaryKey モジュール内の primary_key_definition メソッド
  • 背景:
    • primary_key_definition は Active Record 内で頻繁に呼ばれる「ホットスポット」となっている
    • 以前は、毎回 2 回のメソッド呼び出しを行っており、そのオーバーヘッドが積み重なっていた
  • 変更点:
    • 「fast path」では、1 回のメソッド呼び出しで済むようにロジックを再構成
    • 条件分岐により、一般的なケース(fast path)ではキャッシュされた値もしくは計算済みの値に直接アクセスし、余計なメソッドチェーンを避ける
    • 例外的なケース(slow path)のみ、従来通り 2 回のメソッド呼び出しを行う

おおよそ以下のようなイメージです(擬似コード):

ruby
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 などのコストの高いメソッドを呼ぶ

行数の増加が少ないことから、分岐追加とインスタンス変数キャッシュのような軽微な変更に留まっていると考えられます。


  1. 影響範囲・注意点
  • 影響範囲:
    • Active Record の主キー関連のメタデータ取得(primary_key_definition を内部的に利用する処理全般)
    • 特にテーブル定義情報を頻繁に参照するコードパス(クエリ生成、スキーマ情報アクセスなど)でのわずかなパフォーマンス改善が見込まれます。
  • 後方互換性:
    • メソッドの外部インターフェイス(返り値や例外など)は変わらず、内部の実装効率のみを改善しているため、通常のアプリケーションコードには互換性上の影響はほぼありません。
  • 注意点:
    • キャッシュ・メモ化を強める方向の変更である場合、
      • マイグレーション中にスキーマが変わった場合
      • 接続先 DB のメタ情報が動的に変わるような特殊ケース
        で、最新の主キー定義が即座に反映されない、といった懸念が理論上はありえます。
    • ただし Rails 本体としては、従来の挙動を維持するよう慎重に設計されているはずであり、通常の運用では問題にならない想定です。

  1. 参考情報 (あれば)
  • 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. 概要 (1–2文で)
    Key クラスの内部実装である @columns を外部から直接参照しないようにし、belongs_to 関連の実装を Key の公開インターフェイス経由で扱う形にリファクタリングした PR です。これにより ActiveRecord::Key のカプセル化が強化され、将来的な内部実装の変更に耐えやすくなっています。

  1. 変更内容の詳細

Key クラス側の変更 ​

activerecord/lib/active_record/key.rb に、従来インスタンス変数 @columns に直接アクセスしていた呼び出し側のための公開メソッド (もしくはそれに類する API) が追加されています。

イメージとしては次のような変更です(実際のコードは多少異なる可能性がありますが、意図はこうした形です):

ruby
# 変更前 (呼び出し側がやっていたこと)
key = ActiveRecord::Key.new(...)
columns = key.instance_variable_get(:@columns) # あるいは key.@columns 的な直接参照

# 変更後 (Key がカラム情報を提供するメソッドを持つ)
key = ActiveRecord::Key.new(...)
columns = key.columns # あるいは同等の公開 API

PR コメントの通り、「@columns は実装詳細なので呼び出し元から直接アクセスさせたくない」という意図で、Key のフィールドを隠蔽するためのアクセサやラッパーメソッドが追加されています。

belongs_to_association 側の変更 ​

activerecord/lib/active_record/associations/belongs_to_association.rb では、belongs_to 関連のロード・設定処理の中で、これまで Key の @columns に直接依存していたコードが、上記で追加された Key の公開メソッドを使うように変更されています。

たとえば、外部キーや主キーを比較したり、関連オブジェクト側のカラムを参照して値を読み書きしていた箇所が、次のようにリファクタリングされています。

(あくまでイメージコード)

ruby
# 変更前
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 クラス単体で自由に変更可能になります。


  1. 影響範囲・注意点
  • 主な影響範囲

    • ActiveRecord 内部の belongs_to アソシエーション実装(特に外部キー・主キーを扱う処理)。
    • ActiveRecord::Key の内部構造に依存していたコード(Rails 本体の中でも、この PR 時点では主に belongs_to_association のみが対象)。
  • アプリケーションコードへの影響

    • 通常の Rails アプリケーションでは、ActiveRecord::Key を直接扱うことは滅多にないため、基本的には互換性への影響はありません。
    • もし gem やアプリ側で ActiveRecord::Key の @columns に直接アクセスしているようなメタプログラミングをしている場合、この PR 以後は動かなくなる(もしくは将来的に壊れる)可能性が高いです。その場合は:
      • 公開メソッド(columns のような API)を利用する
      • それがない場合はそもそも Key の内部に依存する設計を見直す といった対応が必要です。
  • 今後の拡張のしやすさ

    • @columns を外部から隠蔽したことで、将来的に Key の内部表現を変えたり、Key に関連するロジックを改善したりする際に、呼び出し側との互換性を維持しやすくなります。
    • 複合キー対応や、キー定義周りのさらなるリファクタリング・最適化のための布石となる変更と考えられます。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    ActiveRecord の関連・主キー周りで使われる ActiveRecord::Key オブジェクトの生成を抑えるため、「definition メソッド」を導入し、Key 自体をキャッシュして再利用するようにした PR です。これにより複合キー情報の扱いを効率化しつつ、includes などの関連ロード時のオブジェクトアロケーションと実行時間を削減しています。

  1. 変更内容の詳細

2-1. 何が問題だったか ​

  • 以前は「主キー名・外部キー名などの“名前”」自体はキャッシュしていましたが、
  • 複合キー対応のために導入された ActiveRecord::Key オブジェクトは 毎回新しく生成 されていました。
  • そのため、Post.includes(:comments) のような典型的なクエリでも、多数の Key インスタンスが生成されてしまい、アロケーション数と処理時間が悪化していました。

ベンチマーク(100件の Post + 各 20件の comments):

allocations per runms per run
v8.1.437,64918.7
Before (複合キー寄りの変更後)62,21631.3
After (この PR)50,21627.8

複合キー関連の最適化追加で一度大きく悪化したパフォーマンスを、Key 自体のキャッシュによってある程度取り戻している、という位置づけです。


2-2. 「definition メソッド」とは何か ​

この PR のポイントは、Key の定義を返す専用メソッドを導入し、それをキャッシュの単位にしたことです。

イメージとして:

  • 以前:

    • ある関連の外部キーを参照するたびに ActiveRecord::Key.new(...) を呼び出しがち
    • 「キー名」はキャッシュされていても、「Key オブジェクト」は都度生成
  • 今回:

    • 「主キー」「外部キー」「スコープキー」などごとに **definition_*** メソッド** を導入し、そこから返される ActiveRecord::Key` をキャッシュ
    • 以降の利用側は「名前ではなく Key 定義」を参照する

概念的なコード例(あくまでイメージ):

ruby
# 以前(イメージ)
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_definition
    • reflection.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 のインターフェース保証がしやすくなります。

  1. 影響範囲・注意点

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 をとって見直すことを推奨します。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/59063
  • 関連しそうな内部クラス・モジュール:
    • ActiveRecord::Key
    • ActiveRecord::Reflection::AssociationReflection
    • ActiveRecord::Associations::BelongsToAssociation など各種 association クラス
    • ActiveRecord::Relation::PredicateBuilder
  • ベンチマークから読み取れるポイント:
    • v8.1.4 より allocations / 実行時間はまだ増えているものの、複合キー/Key 도입後に悪化したパフォーマンスをある程度巻き戻している段階の PR と見なせます。
    • 今後さらに Key 周辺が最適化される可能性があります。

#59062 Clean up how a relation's join values are sorted into the query ​

マージ日: 2026/10/13 | 作成者: @kamipo

  1. 概要 (1-2文で)
    ActiveRecord のリレーションに含まれる joins 情報の分類・並べ替えロジックが整理され、処理フローが単純化・一貫化されました。buckets[:stashed_join] まわりのデータ構造と join 値の分類処理がリファクタリングされ、CTE / 通常の Arel ノード / アソシエーション join の扱いがより明示的になっています。

  1. 変更内容の詳細

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 で不整合が指摘された

この 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 かどうかで再分岐する」といった二重ディスパッチが不要になった

イメージコード(擬似的な例・実コードとは多少異なります):

ruby
# 以前(イメージ)
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 情報を取ってくるか」が整理された形に。

  1. 影響範囲・注意点
  • アプリケーションコードから見える動作:

    • 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 / ライブラリも挙動が変わる、あるいはコンパイルできなくなる可能性があります。
    • 内部実装依存のコードがないか確認が必要です。

  1. 参考情報 (あれば)
  • この 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. 概要 (1-2文で)
    CTE を使った with + joins なリレーションに limit と eager_load を組み合わせたときに発生していた ActiveRecord::ConfigurationError を修正する PR です。CTE を誤って関連名として解釈していたロジックを見直し、limit を安全に適用できるかどうかの判定方法が改善されています。

  1. 変更内容の詳細

問題の内容 ​

次のようなコードで例外が発生していました:

ruby
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 をどう扱うかの判定が、以下のようなより包括的なメソッドに差し替えられています:

ruby
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 の関連名だとみなそうとして、結果的に

text
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 で組み合わせたときに
    • 例外が発生しないこと
    • 期待通りのクエリ・挙動になること

を保証するものです。


  1. 影響範囲・注意点

影響範囲 ​

  • 対象: 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 を含むバージョンでは普通に動作するようになります。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/59061
  • 関連箇所:
    • ActiveRecord::Relation::FinderMethods
    • ActiveRecord::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. 概要 (1-2文で)
    left_joins と merge で取り込んだ joins を組み合わせたときに、SQL の JOIN 順序が不正になり、存在しないテーブルを参照するようなクエリが生成される問題を修正した PR です。left_joins 由来の JOIN を「最初の方」に出すようにし、後からマージされる JOIN が正しく既存テーブルを参照できるようにしています。

  1. 変更内容の詳細

問題の背景 ​

Post.left_joins(:comments).merge(Comment.joins(:ratings)) のようなクエリを考えます。

  • Post.left_joins(:comments)
    • Post を基点に comments を LEFT OUTER JOIN する
  • Comment.joins(:ratings)
    • Comment を基点に ratings を INNER JOIN する
  • .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 の出力順が次のようになっていました:

  1. eager load 用の join dependency
  2. joins_values に溜めていた JOIN (merge された JOIN など)
  3. left_joins 由来の join dependency

このため、次のような不正な順序のクエリが生成されていました:

ruby
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 の並び順を変更し、以下の順に出力されるようにしています:

  1. eager load 用の join dependency
  2. left_joins 由来の join dependency ← ここに移動
  3. joins_values からの JOIN (merge された JOIN など)

つまり、left_joins によって結びつけられるテーブルが先に JOIN され、その後に merge から持ち込まれる JOIN が続くように変更されました。

これにより、上記の例は次のような正しいクエリになります:

sql
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.rb
    build_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 に追記。


  1. 影響範囲・注意点
  • 対象:

    • left_joins と merge を組み合わせ、その merge 側でさらに joins を行っているようなケース
      例:
      ruby
      Post.left_joins(:comments).merge(Comment.joins(:ratings))
      User.left_joins(:posts).merge(Post.joins(:tags))
  • 期待できる効果:

    • これまで DB によっては以下のような症状が出ていた可能性があります:
      • 「テーブル X が存在しない / 参照できない」といった SQL エラー
      • JOIN 順序起因の実行計画の不整合
    • この PR により、これらのケースで JOIN 順序が論理的に正しくなり、テーブルの参照関係が守られるようになります。
  • 互換性:

    • JOIN 順序が変わることで、同じ relation チェーンでも生成される SQL の並びが変わる可能性があります。
      • 結果セット自体は論理的には同じはずですが、DB の実行計画やパフォーマンスに影響する可能性はゼロではありません。
      • SQL の文字列を直接期待値として比較しているテストがある場合、JOIN の並びが変わったことでテストが落ちることがあります。その場合は、新しい JOIN 順序に合わせて期待値の SQL を更新する必要があります。
  • 注意点:

    • この修正は、「left_joins 由来の JOIN はベーステーブルにのみ依存しており、他の JOIN に依存しない」という前提に依拠しています。
      もしアプリケーション側で Arel を使ってかなりトリッキーな JOIN を追加している場合、JOIN 順序の前提が変わることで副作用が出る可能性はありますが、通常の Active Record の使い方では問題になることはほぼありません。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/59060
  • 関連 API:
    • ActiveRecord::QueryMethods#left_joins
    • ActiveRecord::QueryMethods#merge
    • ActiveRecord::QueryMethods#joins
  • 同様のパターンで問題が起きやすいコード例:
    ruby
    # 複数階層で merge + joins を行う場合
    Post
      .left_joins(:comments)
      .merge(
        Comment.joins(:ratings)
               .merge(Rating.joins(:user))
      )
    このような nested な merge でも、ベーステーブル → left_joins → それを前提とする joins の順に並ぶようになり、依存関係が破綻しにくくなります。

#59059 Fix join order when eager_load is combined with merged joins ​

マージ日: 2026/10/13 | 作成者: @kamipo

  1. 概要 (1-2文で)
    eager_load と merge 由来の joins/left_joins を組み合わせたときに、SQL の JOIN 順序が逆転して不正なクエリになる問題を修正した PRです。eager_load が生成する JOIN 群を、他のリレーションからマージされた JOIN よりも先に評価するようにして、参照順序の整合性を保証しています。

  1. 変更内容の詳細

問題の背景 ​

  • eager_load は内部的に JoinDependency を組み立てて、ベーステーブル(FROM で指定されるモデル)を起点に関連テーブルを LEFT OUTER JOIN します。
  • 一方で、merge によって別モデルのリレーション(joins, left_joins などを含む)をマージすると、そのリレーション内の JOIN は、そのリレーションが導入したテーブルに依存することがあります。

問題となるパターンの例:

ruby
Post.eager_load(:comments).merge(Comment.left_joins(:user))

期待されるイメージとしては:

  1. posts をベースに
  2. comments を eager_load で JOIN
  3. その上で Comment.left_joins(:user) による users への JOIN を載せる

といった依存関係になります。

しかし、バグがあった時点では、JOIN の構築順が逆転していました。

問題の挙動 ​

PR 説明にあるように、以前は build_join_buckets が

  • スタッシュされていた JOIN(merge で持ち込まれた JOIN 群)
  • eager_load の JoinDependency

をこの順番で連結しており、結果として eager_load 由来の JOIN が後ろに来ていた ため、以下のような不正な SQL が生成されていました:

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 を後ろに 並べるように順序を逆転させたことです。

つまり、

text
[以前]   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.rb
    • eager_load と merge を組み合わせた際の JOIN 順序を検証するテストが追加されています。
    • これにより、今後のリグレッションを防止。
  • activerecord/CHANGELOG.md
    • この問題修正が CHANGELOG に追記されており、Fixes #54181 と関連 Issue も明記。

query_methods.rb の差分は +1/-1 と非常に小さく、おそらく配列結合の順序入れ替えのような一点変更です。


  1. 影響範囲・注意点

影響範囲 ​

影響を受けるのは主に以下のようなクエリです:

  • eager_load(または includes(...).eager_load に解決されるケース)と
  • 別モデル側のリレーションを merge で組み合わせ
  • そのリレーションがさらに joins / left_joins を持っている

具体例:

ruby
Post.eager_load(:comments).merge(
  Comment.left_joins(:user).where(users: { active: true })
)

あるいは:

ruby
posts = Post.eager_load(:comments)
           .merge(Comment.joins(:tags))

こういったケースで、これまでは RDBMS によってはエラーになったり、意図しない実行計画・結果になった可能性がありますが、今回の修正により JOIN 順序が論理的依存関係に沿うようになります。

注意点 ​

  • JOIN の「順序」が変わるため、非常にまれに:
    • 一部 RDBMS のクエリオプティマイザが異なる実行計画を選び、パフォーマンスが変化する
    • 既にこのバグ前提で複雑な SQL を期待しているような(ほぼないはずの)コードが動き方を変える 可能性はあります。
  • ただし、依存テーブルが左側に現れるのは SQL 的に自然であり、バグ修正として妥当な変更です。
  • 実際には「今までたまたま動いていたが JOIN 定義としては危うかった」コードが、より安全になる方向の修正なので、互換性リスクはかなり低いと考えられます。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/59059
  • 関連 Issue: https://github.com/rails/rails/issues/54181
  • 該当クラス・メソッド:
    • ActiveRecord::Relation::QueryMethods#build_join_buckets
    • ActiveRecord::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. 概要 (1-2文で)
    このPRは、「Migrationのバージョン互換ロジックのうち、DBアダプタ依存の部分」を各アダプタ専用の CompatibilityBehavior クラスに切り出し、adapter_name の文字列判定に依存しない仕組みに移行したものです。これにより、PostgreSQL/MySQL など組み込みアダプタだけでなく、サードパーティやサブクラスアダプタでも、より自然にバージョン互換を実装・拡張できるようになります。

  1. 変更内容の詳細

2-1. これまでの問題点 ​

従来は ActiveRecord::Migration::Compatibility の各バージョンクラス(例: V5_0, V5_1, ...)の中で、

ruby
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 インスタンスを経由して実行する

全体の流れ(ざっくり):

ruby
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
end

execute_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 を呼びつつ引数だけを書き換える、というスタイルで挙動をオーバーライド

例(イメージに近い擬似コード):

ruby
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
end

super 先は「一つ新しい 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

ここも、アダプタごとの振る舞いをアダプタ側でコントロールできるように整理されました。

2-7. サブクラスアダプタでの継承戦略 ​

このPRの重要なポイントのひとつが、「PostgreSQL や MySQL を継承したアダプタ(例: CockroachDB)がどう振る舞うか」です。

  • デフォルトでは:
    • PostgreSQL / MySQL を継承するアダプタは、その親アダプタの CompatibilityBehavior も継承する
      => 文字列判定ではなくクラス継承に基づくため、「adapter_name が違うせいで PostgreSQL の互換処理が効かない」という問題が解決
  • ただし、「親アダプタのある一部の互換挙動だけは要らない/変えたい」というケースもある
    • その場合は compatibility_behavior_for をオーバーライドし、
      1. 親の Behavior を解決
      2. それを継承したサブクラスを用意し
      3. そこだけ特定の操作を上書きする

多くの組み込み互換挙動は「マイグレーションで省略されたオプションを埋める」だけなので、その場合は「サブクラス側でオプションを明示的に指定して super を呼ぶ」だけで済みます。

一方で、PostgreSQL の以下のようなケースでは super をスキップする必要があります:

  • deferrable: true を現行の :initially_deferred 相当などにマッピングする挙動
  • :datetime を現行の timestamp 系に変換する挙動

このため、PostgreSQL 7.0 の disable_extension では:

  • マイグレーションで force: が指定されていない場合のみ force: :cascade を付与
  • 既に force: が指定されている場合は上書きしない

というように、「覆い隠さず、必要な場合だけ足す」スタイルに変更されています。


  1. 影響範囲・注意点

3-1. 一般的なアプリケーション開発者への影響 ​

  • 既存のマイグレーションの挙動そのものは、基本的に変わらない想定です(挙動そのものではなく、その実装場所が整理されたPR)
  • ただし:
    • ログに出るメッセージは「Behavior 適用後のオプション」が表示されるようになったため、微妙に表示が変わる可能性があります
    • 不正な schema 操作名を呼んだときのエラーメッセージは、NoMethodError のみに簡略化

通常のアプリ開発者は、特別な対応はほぼ不要です。

3-2. アダプタ開発者・サードパーティアダプタへの影響 ​

  • 互換挙動を実装/カスタマイズしたい場合、今後は以下のパターンを推奨:
    1. アダプタ側に compatibility_behavior_for(migration_class) を実装
    2. CompatibilityBehavior::Resolver を利用した Behavior 名前空間(例: MyAdapter::CompatibilityBehavior)を定義
    3. 必要なバージョンの 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 が適用されます。
  • つまり、「マイグレーション経由」と「スキーマダンプ経由」で同じバージョン互換挙動が適用されることが、より明示的に保証される方向になっています。

  1. 参考情報 (あれば)
  • 元のスタックPR: #58162 と、それを置き換えた #58844, #58845, #58846, #58847
  • このPRで参照されている関連PR:
    • #59050 (MySQL の legacy bigint PK の auto-increment まわりの挙動 by @kamipo)
  • 実装の中心となるファイル:
    • activerecord/lib/active_record/migration/compatibility_behavior.rb
    • activerecord/lib/active_record/connection_adapters/postgresql/compatibility_behavior.rb
    • activerecord/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. 概要 (1-2文で)
    このPRは、Active Record のテストスイートをローカルで並列実行するときに、テストプロセス同士が同じデータベースを取り合って壊し合わないようにする仕組みを導入しています。アドバイザリロックと「スロット(複数のテスト用DB)」の概念を追加し、最大5並列程度まで安全にDBレベルで分離されたテスト実行ができるようになりました。

  1. 変更内容の詳細

全体像:「DB スロット」とアドバイザリロック ​

目的:

  • AR テストスイートを複数プロセスで同時に走らせても、同じ DB/schema を書き換えてテストが不安定にならないようにする。

アプローチ:

  1. スロット0 (デフォルトDB)

    • config.yml に書かれた通常のテストDB (例: activerecord_unittest) を「スロット0」とみなす。
    • 最初に起動したテストプロセスはこの DB を使うが、アドバイザリロックを取り続ける。
  2. スロット1〜4 (最大5スロット)

    • 2つ目以降のテストプロセスは、スロット0のロック取得を試みる。
    • 取得できなければ「このスロットは使用中」とみなし、代わりに:
      • DB名 + 数字サフィックス (例: activerecord_unittest1, activerecord_unittest2 …) という新しいDB名を候補にする。
      • そのDBに対して同様にアドバイザリロックを試みる。
      • 必要ならそのDBをオンデマンドで作成する。
    • これを最大スロット数 (このPRでは5) まで試し、どこかのスロットでロックが取れれば、そのDBをそのプロセス専用のテストDBとして使う。
  3. スロットが埋まっていた場合

    • 全スロットでロック取得に失敗した場合は即座に例外を投げて失敗させる。
    • これにより、DBを共有して誤ったテスト結果が出ることを防ぐ。
  4. アドバイザリロック非対応DB

    • アドバイザリロックがサポートされないDBでは、今まで通りの挙動になる (スロット機構は事実上使われない)。
    • サポートしているDB (PostgreSQL, MySQL 系の一部など) ではこの新機構が動く想定。

主な実装ポイント ​

1) test/support/database_slot.rb の新設 ​

このファイルがスロット管理の中核です (約100行追加)。

想定される責務:

  • 現在のプロセスが使用する「スロット番号」を決定する。
  • ベースとなるDB設定からスロット用のDB名を計算する。
    • 例:
      • スロット0: activerecord_unittest
      • スロット1: activerecord_unittest1
      • スロット2: activerecord_unittest2 …
  • アドバイザリロックの取得/解放を行う。
  • 必要であればスロット用DBの作成を行う (権限があれば)。

実際のコードは省略されていますが、概念的には以下のような流れになります:

ruby
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に接続される。

擬似イメージ:

ruby
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にアクセスするように整理されている可能性が高いです。

  1. 影響範囲・注意点

影響範囲 ​

  • 対象:
    • ActiveRecord のテストスイート実行環境 (Rails本体開発者、gem開発者が AR を vendor してテストしているケースなど)。
  • 適用されるDB:
    • アドバイザリロックをサポートするDB (PostgreSQL, MySQL / MariaDB / 一部の互換エンジンなど) ではスロット分散が有効になります。
    • サポートしないDBでは従来通り1つのテストDBを共有。

注意点 ​

  1. 最大スロット数 (デフォルト5) を超える並列実行は失敗

    • 6つ以上のテストプロセスが同時に走ると、ロックが取れずに例外が発生します。
    • CI などで並列度を上げる場合は、ARテストスイートをこの仕様に合わせて調整する必要があります (あるいは将来的にスロット数を設定可能にする拡張が想定されるかもしれません)。
  2. DB作成権限がない環境

    • 追加スロット用DBを自動作成しようとしたときに権限が無い場合、そのプロセスは失敗しますが、
    • これはあくまで「同時にテストを走らせたとき」にだけ発生する問題です。
    • 単一プロセスでテストを実行する分には従来と同じ挙動です。
  3. 既存の外部DBに対する影響

    • 外部のDBクラスタを使って AR テストを走らせている場合でも、アドバイザリロックがサポートされていれば「ほぼそのまま Just Work」する想定です。
    • ただし、テストが勝手に DB名1, DB名2, … といった追加DBを作る点に注意してください。
      • 本番共有インスタンス上でテストを動かしているような環境では、追加DBが増える可能性があります。
  4. テストの書き方に対する示唆

    • 以降、テストケースで「DB名を直接決め打ち」したり、「グローバルな1つのDBに副作用を残す」ような書き方は、並列テストを壊す原因になります。
    • 今回のPRに倣って、
      • ActiveRecord::Base.connection_db_config.database
      • テスト用ヘルパ (ARTest.config 等) 経由でDB名を参照する、というパターンに寄せていくのが望ましいです。

  1. 参考情報 (あれば)
  • 関連しそうなキーワード/背景:
    • Rails のテスト並列化 (Minitest 並列実行、PARALLEL_WORKERS 環境変数など) と組み合わせるとき、DBごと分離されるためテストのフレークが減る。
    • PostgreSQL の pg_advisory_lock / MySQL の GET_LOCK などが「アドバイザリロック」の代表的実装。
  • この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. 概要 (1–2文で)
    このPRは、クエリキャッシュを「接続プールごと」から「実行コンテキスト(ExecutionContext)ごと」に戻しつつ、その実行コンテキストが実際に利用した接続プールだけを対象に各種処理(トランザクション確認、接続クリア、クエリキャッシュクリアなど)を行うように最適化する変更です。多くのアプリでは体感差は小さいものの、プール数が多い・ジョブが多い環境ではCPU使用量削減が期待できます。

  1. 変更内容の詳細

背景: 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」を持つ。

イメージ:

ruby
# 疑似コードイメージ

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.rb
  • activerecord/lib/active_record/query_cache.rb

主なポイント:

  • クエリキャッシュを ExecutionContext にぶら下げる構造に戻した。
  • 接続プール側に持っていたキャッシュ用の状態を整理し、ExecutionContext に委譲。
  • クエリ発行時は「現在の ExecutionContext から、そのプールに対するキャッシュ状態を引く」形になる。

ざっくりしたイメージ:

ruby
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_transactions
  • clear_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 にテスト追加。コンソール起動〜終了のライフサイクルで接続やキャッシュ状態が適切に扱われることを確認。

  1. 影響範囲・注意点

影響があるケース ​

  • 多数の接続プールを持つアプリケーション:
    • マルチ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 とプールのひも付けロジック」を壊していないか確認が必要。

  1. 参考情報 (あれば)

関連PR:

  • Ruby 3.3.5 必須化: #58908
  • WeakKeyMap で ConnectionPool をキーにできるように: #58918
  • 以前の「ExecutionContext ベースのクエリキャッシュ」導入: #52622
  • その後の「プールごとのクエリキャッシュ」への移行: #52632

コードの入口になりそうな箇所:

  • ActiveRecord::ConnectionAdapters::Abstract::ConnectionPool
  • ActiveRecord::ConnectionAdapters::Abstract::ConnectionHandler
  • ActiveRecord::QueryCache
  • ActiveSupport::WeakSet / WeakKeyMap

これらを追うと、「ExecutionContext ⇔ ConnectionPool ⇔ QueryCache」の紐付けモデルと、どのタイミングでどのプール集合を参照しているかが把握しやすくなります。


#58028 Warn on broken links to other guides ​

マージ日: 2026/10/13 | 作成者: @p8

  1. 概要 (1-2文で)
    Rails ガイド生成時のリンク検証を強化し、「同一ガイド内のアンカー」だけでなく「別ガイドへのアンカーリンク」も壊れていれば警告を出すようにした PR です。あわせて、実際に壊れていたガイド内リンクの修正・削除も行われています。

  1. 変更内容の詳細

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.
  • 壊れたリンクだけでなく、「もしかしてこのアンカーでは?」という候補も出すようになっており、ガイドメンテナが修正先を見つけやすくなっている。

推測される処理の流れ(イメージコード):

ruby
# 各 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
end

b. 実在するガイドリンクの修正・削除 ​

PR 説明から読み取れる具体的変更:

  1. 壊れたアンカーリンクの更新

    • ガイド内に存在しなくなったアンカーへのリンクを、
      • 現在の相当箇所のアンカー、または
      • 古いバージョンのガイド(該当機能が削除済みの場合) に張り替え。
  2. 「Transactions」ガイドへのリンクを削除

    • active_record_basics.html が「トランザクション」を説明している Rails バージョンは存在しなかったため、
    • そこへのリンクを削除。
    • 別途、「Transactions」ガイド自体は作成作業中である旨が記載されている。
  3. active_record_querying.html#conditions-with-id へのリンク削除

    • #conditions-with-id セクション自体が「Composite Primary Keys」ガイドへ統合されたため、
    • リンクと、それを説明していた段落を削除。
    • つまり、クエリガイド側で説明していた内容は、今後は「複合主キー」のガイドを参照する形になる。

c. ガイド本文の軽微な修正 ​

対象ファイル:

  • guides/source/4_1_release_notes.md
  • guides/source/5_2_release_notes.md
  • guides/source/upgrading_ruby_on_rails.md

これらは行数から見て、主に次のような小規模変更が行われています:

  • 壊れたリンク先の修正・削除
  • 上記で削除されたセクションやガイドへの参照の整理
  • 文言レベルの修正(リンク削除に伴う説明文の削除など)

  1. 影響範囲・注意点
  • 影響するのはガイドの生成プロセスのみ

    • アプリケーションコードやランタイムの挙動には影響しません。
    • guides/ 以下でドキュメントをビルドしている人(Rails コア開発者、ドキュメントメンテナ、ディストリビューション用に独自にガイドをビルドしている人)が主な対象です。
  • ガイドビルド時に新たな警告が出る可能性

    • これまで見逃されていた「別ガイドへの壊れたリンク」があれば、rake guides:generate 等の実行時に [WARN] BROKEN LINK が出るようになります。
    • CI でガイドビルドを回している場合、ログ上のノイズが一時的に増える可能性がありますが、リンク修正の指針になるため、基本的には歓迎すべき変化です。
  • 古いバージョンのガイドやローカルカスタムガイドを持っている場合

    • 自前でガイドを追加・改変している場合は、今回のより厳密なリンクチェックに引っかかることがあります。
    • 壊れたアンカーリンクが検出されたら、
      • 対象ガイドの HTML 内にそのアンカーが本当に存在するかを確認し、
      • セクション名変更やガイド構成変更に追随してリンクを更新する必要があります。
  • トランザクション関連のドキュメント構成に注意

    • これまで active_record_basics.html や active_record_querying.html にあった/あると思われていた一部情報が、
      • 今後は「Transactions」専用ガイド、
      • または「Composite Primary Keys」ガイド に整理される流れです。
    • ドキュメントを参照している自前資料やブログなどが、その古いアンカーをリンクしている場合は、リンク切れになる可能性があります。

  1. 参考情報 (あれば)

#59042 Fix broken anchor links in composite primary keys guide cross-references [ci skip] ​

マージ日: 2026/10/13 | 作成者: @SatvikMishra08

  1. 概要 (1–2文で)
    Composite Primary Keys(複合主キー)ガイドの書き換え後に壊れていたガイド間リンク(アンカー)が3箇所修正されました。これにより、関連するRailsガイド同士の相互参照が正しく該当セクションにスクロールするようになります。

  1. 変更内容の詳細

今回の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中のリンク例・あくまで擬似コード):

md
[複合主キーを使った関連](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ガイドの「Using where」セクション(#using-where のようなID)
    → where / find_by と id に関する注意がまとまっている実際のセクションにリンク。

  • リンクテキストも変更
    「conditions with id」ではなく、「Using where」という、実際のセクション名(または内容)に合わせたテキストに変更。
    これにより、読者は「どこを読むべきか」がテキストからも分かりやすくなっています。

擬似的な例:

md
詳しくは [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についての注意が載っている場所。

擬似的な例:

md
詳しくは [find_by](active_record_querying.html#find-by) セクションを参照してください。

これにより、「CPKに関する注意はActive Record Queryingガイドの find_by 節にある」という導線が正しく機能するようになります。


  1. 影響範囲・注意点
  • 影響範囲

    • Rails本体の動作やAPI仕様には一切変更なし。
    • Rails Guides(特に edgeguides、開発版ガイド)をブラウザで読む際のナビゲーション性が改善されます。
    • 複合主キーと where / find_by(id: ...) の関係を調べる際に、正しいセクションへ一発で飛べるようになります。
  • 注意点

    • すでに外部ドキュメントや社内Wikiなどで、旧アンカー(存在しない #conditions-with-id 等)をハードコードしている場合、それらのリンクは依然として壊れたままです。
      → 必要であれば、新しいアンカー(例: #using-where, #find-by, #associations など)に張り替える必要があります。
    • このPRは edgeguides 上での実在アンカーを基準にしているため、もし将来またセクション名が変われば同様のリンク切れが再発しうる点には留意が必要です。

  1. 参考情報 (あれば)
  • このPRで明示的に参照されている過去PR:
    • Composite Primary Keysガイドを書き換えたPR: #57348
      → ここでセクション構造やアンカーIDが変わったことで、今回のリンク切れが発生したと推測されます。
  • 対象ガイド(edgeguides)のURL例(実際のURLはバージョンに依存):
    • https://edgeguides.rubyonrails.org/active_record_composite_primary_keys.html
    • https://edgeguides.rubyonrails.org/active_record_querying.html
    • https://edgeguides.rubyonrails.org/association_basics.html

#59040 Fix disable_joins through associations with a composite owner foreign key ​

マージ日: 2026/10/12 | 作成者: @federico-carrocera

  1. 概要 (1-2文で)
    disable_joins: true を使った has_many :through で、オーナー側の外部キーが複合キー(例: [:tenant_id, :id])の場合に発生していたバグを修正する PR です。複合外部キーを正しく読み取ることで、ArgumentError が出て関連がロードできない問題が解消されています。

  1. 変更内容の詳細

問題の背景 ​

  • マルチテナント + シャーディング構成で、テーブル行が [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.rb
    diff
    - 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 のテストパターンを踏襲しており、

ruby
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 + 複合外部キー」のバグが修正された旨が記載されています。

  1. 影響範囲・注意点

影響を受けるケース ​

この修正の直接の対象は、次のようなときです:

  • Active Record で複合主キー / 複合外部キーを使っている(composite_primary_keys gem 等の利用を含む)
  • 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 自体が別クエリ+メモリ上結合という高コストな経路
      を取ることを前提にしている
  • そのため、実用上問題になるレベルのパフォーマンス低下はほぼないと考えられます。

  1. 参考情報 (あれば)

複合キー + disable_joins: true な関連で問題が出ていた場合は、
この修正が入った Rails バージョンへ更新することで解消が見込めます。


#59039 Extract ActiveSupport::WeakSet as an internal API ​

マージ日: 2026/10/12 | 作成者: @byroot

  1. 概要 (1-2文で)
    ActiveSupport 内部で使っていた「弱参照による Set 相当の仕組み」を ActiveSupport::WeakSet として独立した内部 API に切り出した PRです。主な利用箇所だった ActiveSupport::DescendantsTracker から実装を分離し、再利用しやすい形に整理しています。

  1. 変更内容の詳細

2-1. ActiveSupport::WeakSet の新設 ​

新しく activesupport/lib/active_support/weak_set.rb が追加され、ActiveSupport::WeakSet クラスが定義されました。
これは「要素への弱参照を保持する Set 的コンテナ」で、GC によって参照先オブジェクトが解放されると、自動的にセットからも消えることを意図した実装です。

典型的なイメージ:

ruby
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 からは専用実装を取り除いています。

従来(イメージ):

ruby
# descendants_tracker.rb 内部に WeakSet 的な構造が直接書かれていた
class ActiveSupport::DescendantsTracker
  # @descendants = some_custom_weak_set_implementation
end

変更後(イメージ):

ruby
class ActiveSupport::DescendantsTracker
  # @descendants = ActiveSupport::WeakSet.new
end

2-3. active_support.rb からの読み込み ​

activesupport/lib/active_support.rb に 1 行追加され、ActiveSupport::WeakSet が ActiveSupport のロード時に読み込まれるようになっています。
これにより、ActiveSupport を require すれば ActiveSupport::WeakSet も利用可能(ただし「内部 API」として)になります。


  1. 影響範囲・注意点
  • 主な影響範囲

    • Rails 内部でクラスの継承関係を追跡する ActiveSupport::DescendantsTracker の実装が ActiveSupport::WeakSet を使うようリファクタリングされています。
    • 実際の挙動(どのクラスが descendants として列挙されるか)は変えずに、実装の責務分離を行っている変更です。
  • パブリック API ではなく「内部 API」

    • タイトルにある通り、「internal API」と明示されています。
    • したがってアプリケーションコードから直接 ActiveSupport::WeakSet を利用するのは非推奨です(将来の互換性が保証されない可能性が高い)。
    • gem・プラグインなどで「内部 API に依存する前提の拡張」をしているなら、WeakSet への依存は破壊的変更のリスクを理解した上で行う必要があります。
  • 動作上の変更は基本的に無し

    • DescendantsTracker の外形的な挙動はそのままのはずで、API 仕様変更は行われていません。
    • ただし、弱参照コレクション実装の差異により、極端な GC タイミングやメモリ境界ケースで挙動が微妙に変わる可能性はゼロではありません(例えば「いつ古いクラスが消えるか」のタイミングなど)。
  • パフォーマンスやメモリ挙動の安定化の布石

    • 弱参照 Set の仕組みが共通化されたことで、今後 GC との相性改善やパフォーマンス改善が 1 箇所のチューニングで全体に波及しやすくなります。
    • DescendantsTracker 以外でも「弱参照 Set」が必要になった場合に再利用できるため、重複実装・微妙に異なる挙動の乱立を防げます。

  1. 参考情報 (あれば)
  • この PR が参照している PR:

  • 関連コンポーネント:

    • ActiveSupport::DescendantsTracker: Rails がクラスの継承関係を追跡するための仕組み。ActiveRecord::Base.descendants などで利用される土台になっています。
    • 弱参照・WeakSet の一般的な目的: GC で解放されるべきオブジェクトに「強参照」を持たず、かつ「可能なら集合として追跡しておきたい」というニーズに応えるデータ構造です。

#59034 Use Ruby::Prism in ActionView::Template#spot when available ​

マージ日: 2026/10/12 | 作成者: @yahonda

  1. 概要 (1-2文で)
    Rails 8.1 の ActionView テンプレートエラー時のハイライト機能が、Ruby master の変更(ErrorHighlight の挙動変更と Ruby::Prism 組み込み)によって壊れていた問題を修正する PR です。テストが不安定に落ちる原因を解消しつつ、Ruby 本体に組み込まれた Ruby::Prism を優先的に使うようにして、テンプレートのエラーハイライトを正しく動作させます。

  1. 変更内容の詳細

背景となる Ruby 側の変更 ​

Ruby master で以下の変更が入っています:

  1. ErrorHighlight の autoload 化

    • ErrorHighlight が autoload されるようになり、定数がまだ解決されていない状態で remove_const(:ErrorHighlight) を呼ぶと「nil を返すだけでモジュール自体は解決されない」という状態が起きる。
    • その結果、後続のテストで ErrorHighlight が存在しないor中途半端な状態となり、Template#spot 内で ErrorHighlight.spot を呼ぶ際に undefined method '[]' for nil などのエラーが発生していた。
  2. Ruby 本体に Ruby::Prism が組み込まれた (Feature #22379)

    • Ruby 3.4 以降・一部 master では、Prism が組み込みの Ruby::Prism として提供される。
    • 同時に error_highlight が「Ruby::Prism のノードだけ受け付ける」ように変更 (error_highlight gem 側の変更) されたため、これまでのように「prism gem で解析したノード」を渡すと TypeError: Exception is expected になるケースがある。
    • つまり、「コンパイルに使ったのと別のパーサでパースして得たノード」を渡すと ID が噛み合わず、error_highlight が期待する形式と合わない。

この PR は上記2点に対応するものです。


1. テストの修正: ErrorHighlight の autoload 問題への対処 ​

対象ファイル:

  • actionpack/test/dispatch/exception_wrapper_test.rb
  • actionview/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.rb
  • actionview/CHANGELOG.md

現状の問題:

  • Template#spot は、「例外発生時に、そのテンプレートのどの行・どのカラムでエラーが起きたかをハイライトする」ために以下を行っています:

    1. 例外バックトレースからテンプレート内のコンパイル済み Ruby コードの位置を取得
    2. テンプレートの Ruby コードを prism gem(Prism.parse など)で再度パース
    3. そのノード情報を ErrorHighlight.spot に渡して、対象位置に対応するコード片を特定
  • しかし Ruby master では:

    • Ruby 本体の Ruby::Prism でテンプレートがコンパイルされている
    • error_highlight は Ruby::Prism のノードだけ受け付ける
    • prism gem 側のノードとは ID や内部表現が異なる

    ため、Template#spot が prism gem を使い続けると ErrorHighlight.spot が TypeError を投げてしまい、テンプレートエラーページを組み立てられなくなっていました。

修正方針:

  • Ruby 本体に Ruby::Prism があれば、それを使ってパースする。
  • Ruby 3.4 / 4.0 など、まだ Ruby::Prism がない環境では従来通り prism gem を利用。
  • さらに、Ruby が parse.y ベースでコンパイルしている場合は、従来どおり RubyVM::AbstractSyntaxTree を利用する、といった既存分岐は維持。

コード上のイメージ(簡略化):

ruby
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 を使うようにした旨、およびテンプレートエラーのハイライトに関する修正が追記されています。

  1. 影響範囲・注意点

影響範囲 ​

  • Rails 8.1 系 + Ruby master / 将来の Ruby (3.4/4.0 など) を使う環境

    • テンプレート内で例外が発生したとき、
      • 以前:
        • デバッグページ(ActionDispatch::DebugExceptions のエラー画面)が 500 public ページにフォールバックし、
          エラー原因もログに十分出ず、error_highlight がうまく動かない。
        • テストが seed によって落ちたり、TypeError や NoMethodError が発生。
      • この 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 するなど、ライブラリ側での依存関係を壊さないようにする必要があります。
  • 独自に ErrorHighlight.spot を呼び出しているコード:

    • Ruby master では Ruby::Prism のノードを渡すのが前提 になっているので、prism gem や AST gem など別パーサーのノードを渡している場合は今後エラーになる可能性があります。
    • Rails と同様に、「Ruby 本体が Ruby::Prism を持っているか」「どのパーサでコンパイルされたコードか」を意識して、ノードを揃える必要があります。
  • Ruby バージョン間の挙動差:

    • この PR は「Ruby master / 将来版で壊れないようにする」ための対応であり、現行安定版 Ruby では挙動はほぼ変わらないはずですが、CI で Ruby master を回している場合は ActionView 周りの挙動が変わるので、テンプレートエラー時の画面・ログの確認をしておくと安心です。

  1. 参考情報 (あれば)

この 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. 概要 (1-2文で)
    String#blank? の実装を微調整し、ASCII文字列かつ全体が空白かどうかを調べる処理で bytesize を何度も呼ばないようにすることで、一部ケースで高速化した PR です。特に先頭が空白の ASCII 文字列や空白のみの長い文字列で 1.1〜1.4倍程度の改善が確認されています。

  1. 変更内容の詳細

背景 ​

直前の #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? 実装は:

ruby
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 本体でもこれと同等の形(あるいはごく近い構造)になったと考えてよく、ポイントは:

  1. bytesize や長さの問い合わせをループのたびに行わない
  2. 先頭バイトで「空白でないこと」を早く判定する

の 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 による影響はほぼなし。

  1. 影響範囲・注意点
  • 影響範囲

    • 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 のような長い空白列を扱うケースでは顕著)。

  1. 参考情報 (あれば)
  • 関連 PR:
    • #58745: String#blank? を C 実装から Ruby 実装に書き換え、高速化・簡略化した元の変更。
  • 実装のポイント:
    • 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. 概要 (1-2文で)
    String#blank? の実装に「ASCII 文字列専用の高速パス」が追加され、ほとんどのアプリで多用される ASCII 文字列に対して、正規表現を使わずバイト列をスキャンすることで 3〜10倍程度高速化しています。非 ASCII 文字列や既存の判定ロジック・仕様は基本的にそのまま維持されます。

  1. 変更内容の詳細

これまでの実装 ​

Rails の String#blank? は、(空文字判定をしたあとで) すべての非空文字列に対して以下のような正規表現マッチを行っていました:

ruby
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? に追加されています:

  1. まず empty? なら true(従来どおり)
  2. 次に ascii_only? フラグを見て、ASCII だけからなる文字列かどうかをチェック
  3. ASCII だけなら、正規表現は使わず、バイト列を while ループでチェック
  4. ASCII 以外 (UTF-8 に非 ASCII を含む文字列、UTF-16/32、レガシーエンコーディング、壊れたバイト列など) は、従来どおり正規表現ベースのロジックへフォールバック

疑似コード的にはこんなイメージです (実際の実装は多少最適化されています):

ruby
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 を投げる

ループをヘルパーメソッドにしなかった理由 ​

ループを別メソッドに切り出して 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? が同じ結果を返すか (パリティ) を多数のケースで検証した上で性能計測を行っています。


  1. 影響範囲・注意点

性能面の影響 ​

ベンチマーク (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_blank gem を併用していた場合:
    • Rails 側の blank? がかなり速くなったため、「ASCII 領域だけを対象に fast_blank を使う」メリットはかなり小さくなります
    • それでも fast_blank は仕様を削っている分 (Unicode 空白を無視する分) まだ有利なケースはありますが、挙動が Active Support と完全一致しないことには引き続き注意が必要です

  1. 参考情報 (あれば)
  • 元ネタとなった 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. 概要 (1-2文で)
    composed_of で単一カラムにマッピングされた属性(例: balance に対する Money)に対して Range 条件を指定した際、SQL が BETWEEN NULL AND NULL になってしまう不具合を修正した PRです。Range の両端値を正しく元カラムの値にマッピングしてから既存のクエリ生成ロジックに渡すことで、BETWEEN 句が期待どおり生成されるようになりました。

  1. 変更内容の詳細(あればサンプルコードも含めて)

問題のあった挙動 ​

composed_of で、例えば以下のようなモデルがあるとします:

ruby
class Customer < ActiveRecord::Base
  composed_of :balance,
    class_name: 'Money',
    mapping: [%w(balance amount)]
end

ここで Money は Integer 等の1つのカラムにマッピングされた値オブジェクトです。

この状態で以下のようなクエリを書くと:

ruby
Customer.where(balance: Money.new(150)..Money.new(250)).to_sql

PR 前は次のような SQL が生成されていました:

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 後は、同じコード:

ruby
Customer.where(balance: Money.new(150)..Money.new(250)).to_sql

が次のような 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 は比較のために <=> を用いる場面があるため、このオペレータが必要になります。

  1. 影響範囲・注意点
  • 影響を受けるのは「単一カラムにマッピングされた 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 条件(スカラ値 / 配列 / サブクエリなど)の挙動には影響しない設計になっています。


  1. 参考情報 (あれば)
  • 対応 Issue:
    • #59012: 単一マッピング composed_of に対する Range 条件が BETWEEN NULL AND NULL になる不具合報告
    • #59016: 関連する議論・追跡 Issue(本 PR で Close)
  • 変更ファイル:
    • activerecord/lib/active_record/relation/predicate_builder.rb
      Range 端点をアグリゲーションマッピングに通すロジックを追加。
    • activerecord/test/cases/finder_test.rb
      バグ再現 + 修正確認テストを追加。
    • activerecord/test/models/customer.rb
      Money に <=> を実装。
    • activerecord/CHANGELOG.md
      上記バグ修正としてエントリ追加。

#59014 Combine constraint operations into bulk ALTER TABLE ​

マージ日: 2026/10/9 | 作成者: @kamipo

  1. 概要 (1-2文で)
    bulk_change_table などでの制約操作(外部キー・ユニーク・チェック・排他制約)を、単一の ALTER TABLE にまとめて発行できるようにするため、AlterTable を制約系の単一責任ポイントに整理した PR です。あわせて MySQL で remove_column と remove_foreign_key を同時に行う場合の二重 DROP 問題も解消しています。

  1. 変更内容の詳細

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 を除く)のメソッドが必要

ざっくりしたイメージ:

ruby
# 以前(概念的なイメージ)
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 ブロック内にあるケースを考えます:

ruby
change_table :posts, bulk: true do |t|
  t.remove_foreign_key :users
  t.remove :user_id
end

AlterTable がコマンドを一括して組み立てると、

  • 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_constraint
  • remove_unique_constraint
  • add_exclusion_constraint
  • remove_exclusion_constraint
  • add_check_constraint
  • remove_check_constraint

などが一貫的に扱われるようになっています。


2-5. テストの追加・更新 ​

次のテストが拡充されています:

  • migration/exclusion_constraint_test.rb
  • migration/unique_constraint_test.rb
  • migration_test.rb(+84行)

内容としては:

  • bulk_change_table で制約を同時に追加・削除するケース
  • 制約名の自動解決(*_for! 系メソッド)の挙動
  • MySQL での remove_column + remove_foreign_key の組合せ

など、今回の変更で壊れやすいパスをカバーしています。


  1. 影響範囲・注意点

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 かどうか)が変わりうる

3-2. MySQL 特有の注意点 ​

MySQL で次のようなパターンを使っている場合:

ruby
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 ステートメント丸ごと失敗」となるパターンが変化する可能性もある(トランザクション内であれば実害は限定的)

  1. 参考情報 (あれば)

#58998 Don't break the debug page on JRuby (w/o ErrorHighlight or RubyVM::AbstractSyntaxTree) ​

マージ日: 2026/10/9 | 作成者: @mjansing

  1. 概要 (1-2文で)
    このPRは、JRuby や --disable-error_highlight 環境の CRuby で、Rails のデバッグページ(開発時の「Action Controller: Exception caught」画面)が 500 の汎用エラーページにフォールバックしてしまう不具合を修正するものです。ErrorHighlight や RubyVM::AbstractSyntaxTree が存在しない環境では、それらを使ったカラム位置特定処理をスキップし、デバッグページ自体は正常表示されるようにします。

  1. 変更内容の詳細

問題の背景 ​

Rails のデバッグページでは、例外発生箇所をハイライト表示するために、以下を利用するルートがあります:

  • ErrorHighlight(Ruby 3.2 以降などで利用可能なエラー箇所強調機能)
  • RubyVM::AbstractSyntaxTree(CRuby 固有)

ところが以下の3メソッドでは、これらが存在するかを確認せずに直接参照していました:

  • Thread::Backtrace::Location#spot
    (ビュー以外の例外時、例: コントローラやモデルでの例外)
  • ExceptionWrapper::SourceMapLocation#spot
  • ActionView::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 を使うルートに入るかどうかを決めています。
  • 疑似コードでいうと、以下のような形になります(実際のコードは若干異なりますが本質はこのチェックです):
ruby
def spot
  return unless defined?(::RubyVM::AbstractSyntaxTree) && defined?(ErrorHighlight)
  # ... AST を使って位置特定処理 ...
end
  • defined?(::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.rb
    • actionview/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本文記載):

bash
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 チェックアウトに対して修正を反映してテスト

  1. 影響範囲・注意点

影響範囲 ​

  • 対象環境:
    • JRuby 全般(RubyVM が無い)
    • CRuby で --disable-error_highlight を付けた起動
    • 将来的に、何らかの理由で ErrorHighlight / RubyVM::AbstractSyntaxTree が利用不能なカスタムビルド Ruby
  • 対象の 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 による赤いカーソルのようなマーカー)が表示されない環境がある点に注意。
      ただしこれはあくまで「視覚的な改善の有無」の違いであり、機能的には従来どおりスタックトレースと該当行を確認できます。
  • 依存コードへの影響:
    • #spot が nil を返しうる前提は元々 ExceptionWrapper で考慮済みですが、もしアプリや gem 側で Thread::Backtrace::Location#spot / ActionView::Template#spot を直接呼び出しており、nil 非許容の前提を置いている場合は、理論上は挙動が変わる可能性があります(とはいえ一般的ではないユースケースです)。
  • テスト戦略:
    • Ruby の定数を一時的に remove_const するようなテストが含まれるので、同種のテクニックを真似する場合はテスト間の副作用に注意する必要があります(PR 内テストではテストスコープに閉じているため問題なし)。

  1. 参考情報 (あれば)
  • この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. 概要 (1-2文で)
    Rails 8.0 系で、JRuby や --disable-error_highlight 環境のときにデバッグページ(例外ページ)が壊れてしまう問題を修正した PR のバックポートです。ErrorHighlight や RubyVM::AbstractSyntaxTree が使えない環境でも、例外表示とソースコード抜粋まわりが安全に動くように調整されています。

  1. 変更内容の詳細

※元 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 では概ね以下のような方向の修正が入っています(疑似コード):

ruby
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 ガードが追加されています:

ruby
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 では存在しないメソッドを呼ばないようにする
  • 期待したフォーマットではない場合に安全にフォールバックする
ruby
# 例: to_s のフォーマットに依存しないようにするなど
def to_s
  if respond_to?(:path)
    "#{path}:#{lineno}:in `#{base_label}'"
  else
    super # 実装依存のデフォルト挙動へ
  end
end

2-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 に依存しない形でアサート

例(イメージ):

ruby
def test_source_extracts_works_without_error_highlight
  extracts = wrapper.source_extracts

  assert_equal "...", extracts[:source]
  # 8.0 では :trace がない前提なので、ここはチェックしない
end

render_test.rb ​

Action View のテンプレートレンダリングで例外が発生するケースを用意し、

  • CRuby + --disable-error_highlight
  • JRuby

といった環境で

  • 例外ページが 500 エラーにはなるが、内部エラーにはならずにレンダリングが完了する
  • 例外メッセージやソース抜粋が期待通りに含まれている

ことを確認するテストが追加されています。

疑似コード:

ruby
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

  1. 影響範囲・注意点
  • 対象バージョン

    • Rails 8.0 系 (8-0-stable ブランチへのバックポート)
    • 少なくとも 8.0.5.1 相当のコードベースで確認されている
  • 影響を受ける環境

    • 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] の存在を前提にしていると将来バージョン依存バグになる可能性があります。

  1. 参考情報 (あれば)
  • 元 PR(8.1 以降向けの変更)
  • 本 PR
  • ErrorHighlight について
    • Ruby 3.1+ で導入されたエラー行のハイライト機能で、Exception#detailed_message などを通じて利用されます。
    • JRuby などでは未実装、あるいは挙動が異なる場合があるため、この PR のように存在チェックやフォールバックが重要になります。

#59006 Make ActiveSupport::Ractors.shareable? version independent ​

マージ日: 2026/10/9 | 作成者: @kamipo

  1. 概要 (1-2文で)
    ActiveSupport::Ractors.shareable? の実装から Ruby バージョン依存のガードを外し、Ractor が存在する環境では常に Ractor.shareable? に委譲するように変更した PR です。これにより、shareable?(nil) など一部のケースで誤った偽値が返っていた不具合も修正されています。

  1. 変更内容の詳細

これまでの挙動 ​

  • 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 を使えない環境向けのフォールバック実装で

    ruby
    def shareable?(obj)
      obj
    end

    のような形になっていたため、例えば:

    ruby
    ActiveSupport::Ractors.shareable?(nil)   #=> nil (偽)
    ActiveSupport::Ractors.shareable?(false) #=> false (偽)

    と、実際には「共有可能」なオブジェクトにもかかわらず「偽」と判定されるバグがありました。

この PR の変更点 ​

  1. shareable? を Ruby バージョンに依存させない

    • RUBY_VERSION >= "4.0" という条件を取り除き、
    • 「Ractor クラスが定義されているかどうか」だけを見て挙動を切り替えるように変更しました。
  2. 実装方針の統一

    • Ractor が存在する MRI Ruby 等では、常に Ractor.shareable?(obj) を呼び出すように変更。
    • Ractor が存在しないランタイム(例: 現状の JRuby)では、shareable? は常に true を返すように変更。
      • これは make_shareable が Ractor 非対応環境では常に成功扱いである、という既存のポリシーと一貫しています。
  3. フォールバックの間違った挙動の修正

    • 前述の「shareable?(nil) が nil を返す」ようなケースは、常に真偽値を返す形に修正されました。

    • 新仕様のイメージ:

      ruby
      module ActiveSupport
        module Ractors
          def self.shareable?(obj)
            if defined?(::Ractor)
              ::Ractor.shareable?(obj)
            else
              true # Ractor 非対応環境では常に共有可能扱い
            end
          end
        end
      end

  1. 影響範囲・注意点
  • shareable? の戻り値が変わる可能性

    • Ractor が利用可能な Ruby (3.0+) では、これまでバージョンガードによって Ractor を使っていなかった場合でも、今後は Ractor.shareable? の正確な判定結果が返ってきます。
    • 以前は「単に obj 自体を返していた」環境では、nil / false を返していたケースが今後は true(Ractor 無し環境)または Ractor.shareable?(obj) の結果(Ractor あり環境)に変わります。
    • if ActiveSupport::Ractors.shareable?(obj) のように真偽値条件として使っていたコードが、「nil だから偽になることを期待」していた場合は意味が変わりますが、そのような使い方はそもそもバグ寄りです。
  • 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 を将来どう扱うか(例えば標準で無効化する等)に依存します。

  1. 参考情報 (あれば)

#58964 Speed up structured event logging ​

マージ日: 2026/10/9 | 作成者: @linyiru

  1. 概要 (1-2文で)
    本PRは、Railsの構造化イベントロギング(start_processing, process_action など)の内部実装を最適化し、ログの内容・挙動を変えずにリクエストあたり数%レベルで処理コストを削減する変更です。主に ActiveSupport::EventReporter と LogSubscriber、および ColorizeLogging の内部実装を見直し、不要なオブジェクト生成や命令数を抑えています。

  1. 変更内容の詳細

全体像 ​

対象となるのは、通常の本番運用で config.log_level = :info のとき、各リクエストごとに必ず通るイベントロギングの経路です:

  • ActiveSupport::EventReporter#notify
  • ActiveSupport::EventReporter::LogSubscriber
  • ActiveSupport::ColorizeLogging によるログ出力ヘルパ

ここで、「外から見える挙動・ログ出力」は変えずに、Ruby 3.4 + YJIT 環境での命令数・オブジェクト割り当て数を減らす最適化が行われています。


ActiveSupport::EventReporter#notify の最適化 ​

1) 購読者の収集: filter_map → each に変更 ​

元々は「購読者一覧から、条件に合うものだけを集める」処理に filter_map が使われていましたが、これをより単純な each ベースのループに変えています。

  • 理由: YJIT 下での命令数を約半分に出来る(著者の計測結果)
  • 挙動: 変化なし。特定のイベントに対して購読しているサブスクライバだけが呼び出されるという意味では同じ。

イメージとしては、以下のような差分です(実際のコードは多少異なりますが、やっていることのレベル感):

ruby
# 以前(イメージ)
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 に引数を渡すとき、これまでは:

ruby
resolve_payload(payload, **kwargs)

のように **kwargs で新しいハッシュを生成していました。PRではこれを 位置引数として渡す 形に変更しています(内部シグネチャに合わせている)。

ポイント:

  • Ruby の仕様上、メソッドの **kwargs は呼び出し側のハッシュそのものではなく、常に新しいハッシュになるため、** は常にコスト(ハッシュ生成)を伴う。
  • 「呼び出し側のハッシュが破壊されない」という安全性は、位置引数にした後も守られている(そもそも resolve_payload が引数を勝手に破壊しない前提/テストでカバー)。

これにより、イベントごとの不要なハッシュ生成を減らしています。


LogSubscriber.log_level_satisfied? の最適化 ​

ログレベル判定ロジックが変更されています。

変更前の問題点 ​

概ね以下のような処理をしていました(擬似コード):

ruby
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? チェックと文字列生成を削除
    • ハッシュ参照 + 既存の判定ロジック呼び出しだけで済むように

イメージ:

ruby
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 回だけ取る ように変更しています。

変更前の典型パターン(イメージ) ​

ruby
def info(message = nil, &block)
  if logger && logger.info?
    logger.info(message, &block)
  end
end

ここでは logger を 2 回呼んでいます(logger && と logger.info? のところ)。

変更後のパターン(イメージ) ​

ruby
def info(message = nil, &block)
  log = logger
  return unless log && log.info?

  log.info(message, &block)
end
  • logger 呼び出しは1回だけ
  • ローカル変数へのキャッシュは、Ruby処理系的に最適化しやすく、YJIT 下でも有利

テストでは、以下のパターンをカバー:

  • 各ログレベルメソッド(debug, info, warn, error, など)が
    • logger が nil のときに何もしないこと
    • logger がいるときに正しく呼び出されること
    • progname や block の有無を含めて正しいシグネチャで動くこと

テスト・検証と互換性 ​

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
  • 計測指標: perf stat によるユーザ空間命令数、および allocations

結果:

middleware 構成user-space instructionsallocations
trimmed stack724.5k → 699.5k (−3.4%)242 → 234
default stack857.6k → 835.2k (−2.6%)302 → 294
  • リクエストあたり 2.6〜3.4% 程度の命令数削減
  • allocations もわずかに削減(イベントごとのハッシュ/文字列生成などが効いている)

なお、著者は wall-clock 時間ではノイズが大きすぎるため、命令数を主指標にしています。


  1. 影響範囲・注意点

影響範囲 ​

  • 対象:

    • ActiveSupport::EventReporter を通じて行われる構造化イベントロギング全般
    • Action Pack / Action View など Rails コアが発行するイベント
    • ActiveSupport::LogSubscriber を継承している独自サブスクライバも含めて、基本的にすべてのイベント購読処理
  • 非対象(挙動に影響しない想定):

    • ログフォーマット(テキストログ/構造化ログ等)
    • ログ行数・メッセージ内容
    • パブリックAPIのシグネチャ(一般的な使い方の範囲)

注意点 ​

  • 「挙動は変わらない」前提の最適化ではあるものの、イベントの処理順序やフィルタの適用順に依存しているような かなりトリッキーな LogSubscriber 実装 をしている場合は、一応動作確認した方が安全です。
    • ただしPRでは「サブスクライバフィルタの順序」もテストでカバーしているため、通常の依存の仕方なら問題ないはずです。
  • log_level_satisfied? は内部実装が変わりましたが、外部から見た判定結果(どの level のときにログが出るか)は変わらないように注意深くテストされています。
  • YJIT 前提の最適化が多いものの、インタプリタや他のJITで悪影響が出るような変更(例: 大量の分岐追加など)はなく、単純化の方向なので副作用は小さいと思われます。

  1. 参考情報 (あれば)
  • このPRで触れられている対象クラス・モジュール:
    • ActiveSupport::EventReporter
    • ActiveSupport::EventReporter::LogSubscriber
    • ActiveSupport::ColorizeLogging
  • 構造化イベントロギング周りをカスタマイズしている場合(自作 LogSubscriber、独自の event_log_level ロジックなど)、このPRの変更を踏まえた挙動確認をテストに追加しておくと安心です。
  • 変更行数としては +75 / -17 と比較的小規模ですが、ホットパス(全リクエストで必ず通るロギング処理)に集中した改善なので、トラフィックの多いサービスほど恩恵が出やすいタイプのチューニングです。

#59011 Fix broken cross-guide anchor links [ci skip] ​

マージ日: 2026/10/9 | 作成者: @SatvikMishra08

  1. 概要 (1-2文で)
    Rails Guides 間のクロスリンクのうち、アンカー先のセクション名変更や移動によって壊れていたリンクを現状のアンカーIDに合わせて修正した PR です。ドキュメントのみの変更で、アプリケーションコードや挙動には影響しません。

  1. 変更内容の詳細

この 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.md
      • guides/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行の差分に収まっています。


  1. 影響範囲・注意点
  • 影響範囲

    • Rails Guides(特に edgeguides.rubyonrails.org)の閲覧体験のみが対象です。
    • これまでページ先頭に飛んでしまっていたリンクが、該当セクションの見出しまで正しくジャンプするようになります。
    • アプリケーションコード、API、挙動には一切影響しません。
  • 注意点

    • すでに外部のブログ記事や社内ドキュメントなどで、古いアンカー(例: #strong-parameters)に直接リンクしている場合、それらは引き続き 404 もしくはページ先頭へのジャンプになる可能性があります(この PR は Rails Guides 内部リンクのみ修正)。
    • アンカーIDはガイドの見出しを元に自動生成・管理されているため、今後見出し名を変更した場合は同様のリンク切れが再発する可能性があります。
      → PR 説明にもある通り、別 PR (#58028) で「ガイド生成時にクロスガイドリンクの壊れを検出して警告する仕組み」が導入されつつあり、今後は生成時に破損リンクを検出できるようになる想定です。

  1. 参考情報 (あれば)

#58953 [ci skip] Remove schema.rb column sorting from 8.1 guides ​

マージ日: 2026/10/9 | 作成者: @trysmr

  1. 概要 (1-2文で)
    Rails 8.1 で導入されたと説明されていた「schema.rb のカラム名アルファベット順ソート」の記述が、実際には 8.1.4 でリバートされていることに合わせて、公式ガイドから削除された PR です。挙動自体は既に元に戻っており、ドキュメントだけを実態に合わせて修正しています。

  1. 変更内容の詳細

背景 ​

  • 過去の変更 (#53281) で「schema.rb 内のテーブルカラムをアルファベット順に並べ替える」仕様が導入されました。
  • この仕様は Rails 8.1 のリリースノートおよび 8.0 → 8.1 アップグレードガイドにも「新しい挙動」として記載されました(コミット 54e6d80495)。
  • しかし後に #56842 でこのソート機能自体がリバートされ、Rails 8.1.4 には「ソートしない」状態で出荷されています。
  • にもかかわらず、ガイド(ドキュメント)側の記述が更新されておらず、「今もソートされる」かのように読める状態になっていました。

この PR が行ったこと ​

以下の 2 つのガイドファイルから、該当の説明文を削除しています。

  1. guides/source/8_1_release_notes.md

    • Active Record セクション内の "Notable changes" にあった
      「schema.rb のカラムがアルファベット順にソートされる」という記述を削除。
  2. guides/source/upgrading_ruby_on_rails.md

    • 「The table columns inside schema.rb are now sorted alphabetically.」という節ごと削除。

差分としては:

  • 追加行: 0
  • 削除行: 6
    のみで、コードではなくドキュメントだけの変更です。

コード挙動のイメージ ​

この PR でコードは一切変わっていないため、挙動としては「今まで(8.1.4 以降)と同じ」です。
schema.rb はこれまで通り、「マイグレーションの実行結果に基づく順序」でカラムが記録されます。例えば:

ruby
# 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 … のように並ぶはずですが、現状はマイグレーション定義順など従来のルールのままです。


  1. 影響範囲・注意点
  • 挙動の変更はない
    すでに #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 を整形する
        など独自対応が必要です。

  1. 参考情報 (あれば)
  • この PR:
    • #58953: [ci skip] Remove schema.rb column sorting from 8.1 guides
  • 関連する過去の変更:
    • #53281: schema.rb のカラムアルファベット順ソートを導入した PR
    • #56842: そのソート挙動をリバートした PR(Rails 8.1.4 に含まれる)
  • ドキュメント:
    • guides/source/8_1_release_notes.md(8.1 リリースノート)
    • guides/source/upgrading_ruby_on_rails.md(アップグレードガイド)
  • 挙動の履歴:
    • Active Record CHANGELOG (8-1-stable ブランチ) に導入とリバートの両方が記録されています。

#58961 Reuse a frozen Mime::Type when looking up / ​

マージ日: 2026/10/9 | 作成者: @linyiru

  1. 概要 (1-2文で)
    このPRは、Accept: */* のリクエストごとに毎回新しい Mime::Type オブジェクトが生成されていた非効率な挙動を改善し、"*/*" の lookup 時に単一の凍結済みインスタンスを再利用するようにしてパフォーマンスとメモリアロケーションを削減するものです。外部から見た挙動(request.format や respond_to の動作など)は変えずに最適化のみを行っています。

  1. 変更内容の詳細

背景 ​

  • 多くの 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("*/*") に対し、「常に同じ凍結済みインスタンスを返すショートカット」を追加しています。

疑似コードイメージ:

ruby
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 の観点で同一視できること。

  1. 影響範囲・注意点

影響範囲 ​

  • 主に影響を受けるのは、Accept: */* を送ってくるクライアント(curl や多くの HTTP ライブラリ)向けのリクエスト処理の内部パフォーマンス。
  • 外部 API はそのままで、挙動に関する確認も以下のように行われている:
    • 16パターンの Accept ヘッダについて、変更前後で:
      • request.accepts
      • request.formats
      • request.format
      • 各 Mime::Type の:
        • クラス
        • 文字列表現
        • シンボル
        • ref
        • html?, all?
        • Mime::ALL との == / eql? 比較
        • 新規に生成した Mime::Type との eql? / hash
      • respond_to を使ったレスポンス(format.any を含むケース / JSON のみのケース)
    • これらが、オブジェクトID(object_id に相当)以外は完全に一致することを確認。

そのため、現実的には「パフォーマンス改善のみで、挙動は変えない」変更とみなせます。

パフォーマンスへの影響 ​

ベンチマーク(YJIT, Ruby 3.4.10):

メソッドbeforeafter
Mime::Type.lookup("*/*")642 ns, 5 allocations39 ns, 0 allocations
Mime::Type.parse("*/*")1.14 µs, 8 allocations471 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 比較で扱うのは想定されていないため、非推奨な使い方と言えます。

  1. 参考情報 (あれば)
  • 関連ファイル:

    • actionpack/lib/action_dispatch/http/mime_type.rb
      • Mime::Type.lookup 周辺に、"*/*" を扱うショートカットが追加。
    • actionpack/test/dispatch/mime_type_test.rb
      • */* の動作確認、およびレジストリ優先の確認テストが追加。
  • パフォーマンス計測スクリプト(アプリ起動後に実行):

    ruby
    require "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. 概要 (1-2文で)
    Railsガイド(Active Record Query Interface)の「default_scope がレコード生成時にどう作用するか」の説明が、実際の挙動とずれていたため修正したドキュメント専用PRです。default_scope よりも「明示的に渡された属性」が優先されることを、文章とコード例で正しく示すようにしています。

  1. 変更内容の詳細

対象: 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 で確認されている):

ruby
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点が修正されています:

  1. 説明文の修正

    • 「default_scope によって新しく作られる Book の out_of_print が false になる」という趣旨の、より正確な記述に変更。
    • すなわち「新しく作成される本は、デフォルトで out_of_print: false が入る」ことを説明する。
  2. コード例の拡張
    元々あった Book.new の例に加えて、次の例を追記:

    ruby
    Book.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 自体を外せばデフォルトのフィールド値は付与されない」点も確認されています。


  1. 影響範囲・注意点
  • コード挙動への影響は一切なし
    変更はドキュメントのみ(.mdファイル)であり、Rails本体や Active Record の実装は変わっていません。

  • 開発者が誤解しやすかったポイントが是正された

    • 「default_scope が新規レコードに強制的に値を押し込む」ように誤解していると、
      • 「明示的に out_of_print: true 渡したのに false に上書きされるはずだ」と勘違いする可能性があった。
    • 今回の修正により、「明示的な属性が常に優先される」ことをガイドが明示するようになった。
  • 実務上のヒント

    • モデルの「デフォルト値」を default_scope で付けたい場合でも、
      • フォームやAPIから明示的に値が送られてくるケースでは、その値が優先される。
    • 「ユーザー入力を絶対に上書きしたい」ような用途には default_scope は不向きで、
      • コールバックやバリデーション、DBのデフォルト値など別の手段を検討すべき。

  1. 参考情報 (あれば)
  • 該当ガイド:
    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. 概要 (1-2文で)
    CSRF 関連のログイベント(csrf_token_fallback, csrf_request_blocked, csrf_javascript_blocked)が :info レベルで出力されるようになっていたため、config.log_level = :warn などの設定下でログに出なくなっていた問題を修正し、従来どおり :warn レベルで出るように戻した PR です。あわせて、ActiveSupport::EventReporter::LogSubscriber が :warn レベルのイベントを正しく処理できるようにし、関連するテストを追加しています。

  1. 変更内容の詳細

2-1. CSRF ログレベルの修正 (ActionController::LogSubscriber) ​

変更ファイル:

  • actionpack/lib/action_controller/log_subscriber.rb (+3/-3)

これまで:

  • CSRF 関連イベント
    • csrf_token_fallback
    • csrf_request_blocked
    • csrf_javascript_blocked
  • が ActionController::LogSubscriber 内で info レベルとして扱われていたため、config.log_level = :warn 以上の環境(本番など)ではログに一切出ない状態だった。

PR での修正:

  • 上記 3 つの CSRF イベントを :warn レベルでログ出力するように変更。
  • 挙動としては「#56355 前の RequestForgeryProtection が logger.warn を直接呼んでいた状態」に戻した形。

イメージとしては、以下のような変更になっています(疑似コード):

ruby
# 変更前(イメージ)
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
end

2-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 イベントを宣言しても問題なく処理・出力できるようにした。

擬似コードイメージ:

ruby
# 変更前
LOG_LEVELS = [:debug, :info, :error].freeze

# 変更後
LOG_LEVELS = [:debug, :info, :warn, :error].freeze

2-3. テスト追加 (actionpack/test/controller/log_subscriber_test.rb) ​

変更ファイル:

  • actionpack/test/controller/log_subscriber_test.rb (+29/-0)

追加されたテスト内容:

  • 全ての CSRF イベントについて、warn レベルでログが出力されることを確認するテスト:
    • csrf_token_fallback
    • csrf_request_blocked
    • csrf_javascript_blocked
  • config.action_controller.log_warning_on_csrf_failure = false 相当の設定(log_warning_on_csrf_failure = false)を有効にした場合、
    • 上記 CSRF ワーニングがログ出力されない(opt-out が機能している)ことを確認するテスト。

これにより、

  1. CSRF ログが :warn レベルで出力されること
  2. アプリ設定でワーニングを抑制できること
    が後方互換を含めてテストで保証されるようになっています。

  1. 影響範囲・注意点
  • ログレベルの変更によるログの出方

    • 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 以上をカウント”)を調整する必要があるかもしれません。
  • Rails バージョン間の挙動差の解消

    • #56355 以降のバージョンでだけ「CSRF 失敗時にログが出ないことがある」というレグレッションが発生していましたが、この PR により #56355 以前と同様の挙動に戻ります。
    • そのため、「Rails を上げたら CSRF ログが消えた」系の問題を抱えていた場合、この修正を取り込むことで解消が期待できます。

  1. 参考情報 (あれば)
  • 関連 Issue:
    • Fixes #58959 — 「CSRF ログが :info レベルになったことで log_level = :warn 以上だとログが出ない」問題の報告。
  • 関連 PR:
    • #56355 — CSRF 関連ログを ActionController::LogSubscriber に移した変更(今回のレグレッションの原因となった変更)。
  • 該当クラス・設定:
    • ActionController::RequestForgeryProtection
    • ActionController::LogSubscriber
    • ActiveSupport::EventReporter::LogSubscriber
    • config.action_controller.log_warning_on_csrf_failure
    • config.log_level

#58999 Enable summary output if -q/--quiet used with rails test ​

マージ日: 2026/10/9 | 作成者: @zenspider

  1. 概要 (1-2文で)
    rails test に -q / --quiet を付けたときでも、テストのサマリー出力(失敗数などの概要)が正しく表示されるようにする修正です。Minitest のレポーター構成まわりの挙動を、Rails プラグイン側で 1 行だけ調整しています。

  1. 変更内容の詳細

背景となる問題 ​

  • rails test -q を実行した場合:
    • 「静かに」実行したいオプションですが、現状では:
      • ProgressReporter が CompositeReporter に追加されない
      • その副作用として TestUnitReporter も追加されない
      • 結果として「失敗の詳細出力が一切出ない」という状態になっていた
  • つまり、「静かに」はいいが「失敗が分からない」は困る、という問題。

作者コメントによると:

If -q is 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 -q on 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 行差し替えで調整することで、「静かだが結果はわかる」状態にしています。


  1. 影響範囲・注意点
  • 対象:
    • rails test -q / rails test --quiet を利用している(利用する予定の)プロジェクト
  • 影響:
    • これまでは rails test -q だと失敗時にも何も出ず、CI などで非常に困る状況があったが、
    • 修正後は、詳細な進捗や冗長なログは抑えつつも、最終的なサマリーや失敗内容が表示されるようになる。
  • 注意点:
    • あくまで Minitest レポーターの構成と SuppressedSummaryReporter の挙動が変わるだけなので、テストそのものの実行ロジックには影響しない。
    • TestUnitReporter 自体が -q を理解するようになったわけではない点に注意(quiet の扱いは Rails 側のラッパーで吸収している)。

  1. 参考情報 (あれば)

#58881 Don't modify a connected_to stack shared with another thread ​

マージ日: 2026/10/9 | 作成者: @SulimanAbdulrazzaq

  1. 概要 (1-2文で)
    ActiveRecord::Base.connected_to 系が、スレッド間で共有されうる connected_to スタック配列を「その場で破壊的に変更」していたため、ActionController::Live 等でスレッドを共有した場合にロール/シャードが意図せず混線するバグを修正する PR です。スタック操作を「コピーして差し替える」方式に変更し、親スレッドと子スレッドでスタックが干渉しないようにしています。

  1. 変更内容の詳細

背景となる問題 ​

  • 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 の各メソッドで、スタック配列をインプレースで変更しないように変更しています。

変更前のイメージ(概念) ​

ruby
stack = ActiveRecord::Base.connected_to_stack
stack.push(new_frame)           # 同じ配列オブジェクトを mutate していた
yield
stack.pop

これだと stack がスレッド間で共有されている場合に、push/pop が他スレッドにも影響する。

変更後のイメージ(概念) ​

ruby
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.pop calls in base_test.rb) keeps working.

connected_to_stack 自体は依然として Array で、mutable です。
そのため、「スタックを直接いじるコード」があっても型が変わったりはせず動作します。

ただし、今後のライブラリコード側からは、基本的に「実行状態を書き換える」公共 API (connected_to など) 経由での変更が想定されており、直接スタックをいじるのは内部テストレベルの用途に留めるのが安全です。

テスト内容 ​

新規・更新されたテストで確認している振る舞い:

  1. BasicsTest (共有状態を持つ子スレッドの視点)

    • 親スレッドが connected_to / connected_to_many ブロックを抜けた後でも、share_with された子スレッド側からは元通り両方の role を見られることを確認。
      • 変更前は [false, false](ロールが見えない状態)になっていた。
  2. BasicsTest (子スレッドからの role 変更の影響)

    • share_with した子スレッドで connected_to(role: :reading) ブロックに入っても、親スレッドの current_role は変化しないことを確認。
      • 変更前は親スレッドの current_role が :reading に化けていた。
  3. ShardSelectorTest (ミドルウェア + ストリーミング的なシナリオ)

    • ミドルウェア (ActiveRecord::Middleware::ShardSelector) 内でスレッドを起動し、ミドルウェアのレスポンス処理が終了した後もスレッドを走らせ続ける(ActionController::Live と同様の振る舞い)。
    • そのスレッド内では、引き続き shard_one を見られることを確認。
      • 変更前は current_shard が :default に変わってしまっていた。

CI 上では Ruby 3.4 / SQLite3 テストでレグレッションが出ていないことが明示されています。


  1. 影響範囲・注意点

影響がある機能・ユースケース ​

  • ActiveRecord::Base.connected_to, connected_to_many, connecting_to を使った
    • マルチ DB (writing / reading ロール切り替え)
    • マルチシャード (shard_one / shard_two など)
  • 特に
    • 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 のみを利用する設計に寄せるのが安全です。

  1. 参考情報 (あれば)
  • 関連 issue: #58870
    • 本 PR で修正された「スレッド間で共有される connected_to スタックが壊れる」問題の報告。
  • 関連設定:
    • 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. 概要 (1-2文で)
    Active Record のガイド内で、存在しない「Transactions」セクションへのリンクが削除されたドキュメント修正PRです。壊れたリンクを取り除くだけの変更で、挙動やAPIには影響しません。

  2. 変更内容の詳細

  • 対象ファイル: guides/source/active_record_querying.md
  • 変更内容:
    • 「関連ガイド」リストの中から、active_record_basics.html#transactions へのリンクが削除されています。
    • Active Record Basics ガイドには #transactions セクションが存在しないため、そのアンカー付きリンクが 404 になる状態でした。
    • 元々 #56341 でトランザクション/ロッキングのガイドが将来的に追加されることを見越して残されていたものを、現状に合わせて一旦削除した形です。

疑似的には、以下のような行が消えたイメージです(実際のテキストは若干異なる可能性があります):

markdown
* [Transactions](active_record_basics.html#transactions)
  1. 影響範囲・注意点
  • 影響範囲:
    • Rails ガイド (特に Active Record Querying ガイド) から辿れるリンクが1つ減るのみで、アプリケーションコードやフレームワークの振る舞いには一切影響しません。
    • 外部サイトや社内Wikiなどで、この壊れたURL (active_record_basics.html#transactions) に直接リンクしている場合、それはもともと 404 だったため、今回の変更で悪化することも改善することもありません(公式ガイド内からの導線が消えるだけ)。
  • 注意点:
    • 今後「Transactions & Locking」などの新規ガイドが追加されたタイミングで、改めて正しいURLを指すリンクを追加する前提になっています。
    • ドキュメントを翻訳・ミラーしているプロジェクトでは、同様に該当リンクの削除を取り込んでおくと、ユーザー体験の向上につながります。
  1. 参考情報 (あれば)
  • 元になった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. 概要 (1-2文で)
    このPRは、Railsガイド(ドキュメント)内の enum の項目名が一箇所だけ他の箇所と不整合になっていた箇所を修正するものです。アプリケーションコードや挙動には影響せず、ドキュメント上の表記ゆれを揃えています。

  1. 変更内容の詳細(あればサンプルコードも含めて)

対象は guides/source/active_record_querying.md(日本語ガイドではなく英語ガイド)で、enum のサンプルに出てくる状態名が 1 箇所だけ誤っていました。

  • 修正前: being_packaged
  • 修正後: being_packed

おそらくガイド内では、他の箇所で以下のような enum 定義を例示しており:

ruby
class Order < ApplicationRecord
  enum status: {
    being_packed: 0,
    shipped:      1,
    delivered:    2,
  }
end

本文中でこの being_packed を参照すべきところが、誤って being_packaged と記載されていたため、それを揃えるという修正です。

[ci skip] がタイトルに付いていることからもわかるように、テストを走らせる必要のない単純なドキュメント修正として扱われています。


  1. 影響範囲・注意点
  • 影響範囲

    • Rails本体のコード、挙動、API には一切変更ありません。
    • 変更は Active Record Querying ガイド(ドキュメント)の 1 行のみです。
    • enum の項目名の公式な「推奨形」が being_packed であることが、より一貫した形で示されるようになります。
  • 注意点

    • 既存アプリケーションで being_packaged を使っていても、このPRによって壊れることはありません(あくまでドキュメント修正なので)。
    • ただし、ドキュメントをコピペして実装する場合は、今後は being_packed を使うことになるため、ドキュメントとコードの不整合を避ける意味でも、アプリ側の命名を合わせたい場合はリファクタリングを検討してもよいでしょう。
    • enum のキー名を変える場合は、DB の永続化済みデータや既存コードとの互換性に注意が必要です(このPR自体はそこに手を触れていません)。

  1. 参考情報 (あれば)
  • 対象 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. 概要 (1-2文で)
    Active Record Query Interface ガイドの reorder セクションで、サンプルコードと説明文の内容が食い違っていた箇所を修正したドキュメント専用のPRです。reorder が「何の ORDER BY を上書きするのか」という説明を、実際の default_scope ベースのサンプルに合うように書き換えています。

  1. 変更内容の詳細

背景となる問題 ​

  • 以前のガイドでは、reorder の説明に「association order(関連の order)」を例にしたコードがありました:
    ruby
    class Author < ApplicationRecord
      has_many :books, -> { order(year_published: :desc) }
    end
  • 後の修正でこの例は default_scope を使う形に変更されたにもかかわらず、文章中には依然として

    not just association order という文言が残っており、コード例と説明が一致していない状態になっていました。

今回の修正内容 ​

  1. 「association order」から「default scope」への表現の修正

    文中の説明を以下のように変更しています(英語ドキュメントですが、内容を日本語で説明します):

    • 旧:
      • 「reorder は、単なる関連での order だけでなく〜」
    • 新:
      • 「reorder は、単なる default scope 由来の order だけでなく〜」

    これにより、「前の例で使っているのは関連の order ではなく default_scope だ」という現在のコード例と説明が整合しました。

  2. default_scope + order + reorder の挙動説明の修正

    ガイド内の前提として、Book モデルには以下のような default_scope が定義されています:

    ruby
    class Book < ApplicationRecord
      default_scope { order(year_published: :desc) }
    end

    この状態で

    ruby
    Book.where("id > 100").order("id desc")

    を実行すると、生成される SQL の ORDER BY は

    sql
    ORDER BY year_published DESC, id desc

    のように、

    • default_scope の order(year_published: :desc)
    • チェーンされた order("id desc")

    の2つが結合されたものになります。

    ここでさらに

    ruby
    Book.where("id > 100").order("id desc").reorder("title ASC")

    を呼ぶと、reorder("title ASC") が 両方の ORDER BY をまとめて上書きする というのが正しい挙動です。

    今回のPRでは、この点を明示するよう文章を修正しています。
    具体的には、それまでの説明が「reorder が直前の order("id desc") だけを上書きする」という誤解を招きうる表現だったのを、「default_scope 由来の order も含めて、既存の ORDER BY 全体を置き換える」という趣旨が伝わるように変更しています。


  1. 影響範囲・注意点
  • 影響範囲:

    • コード変更はなく、ガイド(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 自体の設計を見直すことを検討すべきです。

  1. 参考情報 (あれば)
  • 該当ガイドのセクション:
    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. 概要 (1-2文で)
    Active Job ガイドの「Halting Callbacks」例で、環境変数の判定ロジックが常にジョブを中断してしまう不具合があったため、意図どおり「環境変数が "true" のときだけ中断」になるようにドキュメントが修正されています。コード本体ではなくドキュメントのみの変更です。

  1. 変更内容の詳細

問題になっていた元のサンプルコードは以下の行です。

ruby
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 では判定を次のように修正しています。

ruby
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" 比較に留めています。


  1. 影響範囲・注意点
  • 対象は ドキュメント (guides) のみであり、Rails 本体のコードには変更はありません。
  • これまでガイドの例をそのままコピーして使っていた場合:
    • 旧コードのままだと、環境変数の有無や値に関わらずジョブが常に中断されてしまいます。
    • 実運用で使っている場合は、ENV["VAR_NAME"] == "true" のような比較に修正することを推奨します。
  • 環境変数を boolean 的に扱うときの Ruby の落とし穴:
    • "false" や "0" も truthy であることを前提に、きちんと文字列比較やパーサを用いる必要があります。
    • 本番設定で「false を入れたのに有効になっている」というバグを防ぐ観点でも、この例はよい注意喚起になります。

  1. 参考情報 (あれば)
  • 該当ガイド:
    Active Job Basics – Halting Callbacks
    https://edgeguides.rubyonrails.org/active_job_basics.html#halting-callbacks

  • 環境変数の真偽値パースでよくあるパターン例(自前で使う場合の一案):

    ruby
    def 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. 概要 (1-2文で)
    このPRは、Active Job ガイド (active_job_basics.md) 内の単純な英単語のタイポを修正したドキュメント専用の変更です。機能コードには一切手が入っておらず、挙動の変更や互換性への影響はありません。

  2. 変更内容の詳細

  • 変更対象ファイル: guides/source/active_job_basics.md
  • 変更内容: 英文中の誤記 "users" を "uses" に修正

文脈としては、Active Job の使い方・利用例を説明するガイドの中で、
「〜を users する」ではなく「〜を uses する」が正しい文法になる箇所を修正したものです。

(擬似的な例)
修正前(イメージ):

markdown
Active Job users the configured queue adapter to enqueue jobs.

修正後(イメージ):

markdown
Active Job uses the configured queue adapter to enqueue jobs.

実際には、上記はイメージですが、同様に "users" → "uses" という単語レベルの修正のみが行われています。

  1. 影響範囲・注意点
  • 影響範囲

    • Railsガイド(ドキュメント)のみ。
    • フレームワークのコード、API、挙動、パフォーマンスには一切影響しません。
    • テストや本番環境の挙動も変化しません。
  • 注意点

    • ドキュメントの英語表現が自然になり、読解性が向上しただけの変更です。
    • コードベースの更新や設定変更など、開発者が追随する必要はありません。
  1. 参考情報 (あれば)
  • PRタイトルに [ci skip] が付いており、CI を走らせないドキュメント専用修正であることが明示されています。
  • 変更は 1 ファイル・1行の置換のみで、他機能との関連や副作用はありません。

#58767 Store query constraints in SchemaContext ​

マージ日: 2026/10/8 | 作成者: @adrianna-chang-shopify

  1. 概要 (1-2文で)
    このPRでは、モデルに紐づく「クエリ制約(query constraints)」を ActiveRecord::ModelSchema::SchemaContext に保存・管理するように変更し、スキーマ読み込み時に制約を解決(resolve)する仕組みに移行しています。これにより、クエリ制約の評価タイミングがスキーマロード時に統一され、モデルレベルのアクセスはスキーマコンテキスト経由の委譲に整理されます。

  1. 変更内容の詳細

背景 ​

  • 前提となる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 条件に相当するもの)を「最終的な形」に解決してリストとして保持します。

イメージ(あくまで概念的なサンプル):

ruby
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)」は従来どおりモデル側に残します。
    例:

    ruby
    class 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 の関係性が崩れていないかを確認するテストが追加されています。

  1. 影響範囲・注意点

影響範囲 ​

  • Active Record の内部 API:
    • クエリ制約の評価・格納場所がモデル本体から SchemaContext に移ったため、内部的にクエリ制約に依存するコード(特に persistence と associations 周り) の挙動が SchemaContext を前提に変わっています。
  • 拡張・メタプログラミングをしているコード:
    • Rails の内部クエリ制約機構に直接アクセスしている gem やアプリ内コードがある場合、
      • 以前は SomeModel.<constraint_xxx> 的にアクセスしていたところが
      • SomeModel.schema_context.<constraint_xxx> 的な経由が必要になる可能性があります(実際のメソッド名はソースを確認する必要がありますが、委譲経由かどうかで挙動が変わる場所があり得ます)。

注意点 ​

  • 「スキーマロード前/後」の差異に注意:
    • モデルに定義されているクエリ制約 DSL はスキーマロード前から利用できますが、「解決済みのクエリ制約リスト」はスキーマロード後でないと正しく構築されません。
    • クラス定義のタイミングなど、スキーマロード前に内部状態を覗き込むようなコードがあると、以前と挙動が変わる可能性があります。
  • 将来の最適化・キャッシュ基盤としての布石:
    • SchemaContext に集約されたことで、今後、スキーマベースのキャッシュや再読み込み戦略の洗練が行われる可能性が高く、
      「クエリ制約をモデルではなく SchemaContext にぶら下げる」という設計は、そのための前段階でもあります。
    • 内部構造への依存は今後も変化し得るため、アプリ/gem からは極力公開 API 経由でアクセスするのが安全です。

  1. 参考情報 (あれば)
  • ベースとなる PR:
  • 関連ソース (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. 概要 (1-2文で)
    Rails::StructuredEventSubscriber がフレームワーク起動時に自動で require されておらず、config.active_support.deprecation = :notify を設定しても rails.deprecation の構造化イベントが発火しない問題を修正した PR です。今後は require "rails" しただけでこのサブスクライバが読み込まれ、テストもその前提で動作するようになりました。

  1. 変更内容の詳細

問題の背景 ​

  • 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 しなくてもテストが通ってしまっていた(回帰バグに気付きにくい状態)。

具体的な変更点 ​

  1. 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 へ発行されるようになります。

  2. テストの修正

    • 変更ファイル: railties/test/structured_event_subscriber_test.rb (-1行)
    • テストコード内から直接の require "rails/structured_event_subscriber" を削除。
    • 代わりに、「require "rails" した結果として Rails::StructuredEventSubscriber が利用可能になっているか」を前提とするテストにすることで、
      • 今後もし rails.rb からの require を削除・変更してしまうとテストが落ちる
      • つまり「Rails 本体が subscriber をちゃんと読み込んでいる」ことを明示的に保証するテストになった、ということです。
  3. CHANGELOG の更新

    • 変更ファイル: railties/CHANGELOG.md (+4行)
    • このバグ修正についてのエントリが追加されています。これにより、Rails のリリースノートで今回の挙動変更を確認できるようになりました。

  1. 影響範囲・注意点

影響範囲 ​

  • 対象となるのは、以下を行っているアプリケーションです:

    • Rails 起動時に require "rails" をしている(通常の Rails アプリは該当)
    • かつ config.active_support.deprecation = :notify を設定している
    • もしくは、Rails::StructuredEventSubscriber / Rails.event を使って deprecation などのイベントを観測する仕組みを組んでいる
  • 今回の修正により:

    • これまで 出ていると思っていたが、実は出ていなかった rails.deprecation 構造化イベントが、実際に発火するようになります。
    • すでにログ基盤・監視基盤で Rails.event を購読している場合は、Rails 更新後に deprecation イベントが急に流れ始める可能性があります。

注意点 ​

  • ログ・メトリクスの増加:

    • 非推奨機能を多く使っているコードベースの場合、新たに rails.deprecation イベントが大量に流れ始める可能性があります。
    • ログの肥大化やメトリクスカウントの急増が懸念される場合、
      • ログフィルタ
      • サンプリング
      • deprecation の抑制・無効化(例: config.active_support.deprecation = :log や :silence)
        などの対応を検討した方がよいです。
  • テストでの期待値変更はなし:

    • もともと「構造化 deprecation イベントが飛ぶはず」という仕様に対して、実装側が追いついていなかった形なので、
      • アプリ側で「イベントが来ない前提」に依存したコードがあるケースは通常考えにくく、後方互換性の破壊は非常に低リスクです。
    • ただし「なぜかイベントが来なかったから独自のワークアラウンドを入れていた」ような特殊ケースでは挙動が変わる可能性があります。

  1. 参考情報 (あれば)
  • 関連 Issue:
    • #58960 — Rails::StructuredEventSubscriber が require されていないために rails.deprecation イベントが発火しない問題の報告。
  • この 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. 概要 (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 の添付ファイル関連にも自然に指定できます。

  1. 変更内容の詳細

何が問題だったか ​

従来:

ruby
class User < ApplicationRecord
  has_one_attached :avatar
end

has_one_attached は内部でだいたい以下のような関連を定義しています:

  • has_one :avatar_attachment, class_name: "ActiveStorage::Attachment", as: :record, dependent: :destroy
  • has_one :avatar_blob, through: :avatar_attachment, source: :blob

このとき has_one_attached に渡せるオプションはかなり限定されており:

  • dependent
  • service
  • strict_loading
  • analyze

のみがサポートされていました。そのため、次のような一般的な関連オプションを付けたい場合に困っていました:

  • 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) にだけ 渡すようになりました。

イメージ:

ruby
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 内の一貫した設計に合わせています。

サンプルコード ​

バリデーションを制御する ​

ruby
class User < ApplicationRecord
  # 添付関連に対する validation を無効化したいケース
  has_one_attached :avatar, validate: false
end

コールバックを使う ​

ruby
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 は

ruby
has_many :photos_attachments, class_name: "ActiveStorage::Attachment", ..., after_add: ..., after_remove: ...

に渡されます。

deprecated: など独自オプション ​

Rails が提供する deprecated: オプションや、gem が追加する関連オプションも素直に使えるようになります:

ruby
class User < ApplicationRecord
  has_one_attached :legacy_avatar, deprecated: "Use avatar instead"
end

  1. 影響範囲・注意点
  • 互換性

    • 既存の has_one_attached / has_many_attached 呼び出しはそのまま動作します。
    • 変更は「新しいオプションを受け付ける」方向のみで、従来の挙動を壊さないように実装されています。
  • オプションが適用される対象

    • 渡した追加オプションは Attachment 関連 (avatar_attachment, photos_attachments など) にのみ適用されます。
    • Blob との through 関連 (avatar_blob, photos_blobs など) には渡されません。
    • そのため、Blob 側に対する dependent や strict_loading を直接いじる用途には使えません (そもそも Active Storage が Blob 削除フローを制御しているため、基本的にはいじるべきではない部分です)。
  • 不正なオプション

    • 存在しない / サポートされていないオプションを渡すと、has_one / has_many 相当のレベルで ArgumentError などが発生します。
    • つまり、「Active Storage 独自のチェック」ではなく「通常の関連オプションのチェック」によって弾かれます。
  • 拡張や metaprogramming との相性

    • gem が ActiveRecord::Associations::Builder::Association.valid_options にオプションを追加しているような場合、そのオプションもそのまま ..._attachment 関連に指定可能になります。
    • これにより、「Active Storage の has_*_attached だけオプションが通らない」といった不整合が解消されます。

  1. 参考情報 (あれば)

#58988 [doc][ci skip] Fix internal link for Active Support Instrumentation guide ​

マージ日: 2026/10/8 | 作成者: @hachi8833

  1. 概要 (1-2文で)
    Active Support Instrumentationガイド内の内部リンク表記の誤りを修正した、ドキュメント専用のごく小さな変更です。コードや挙動には一切影響せず、ガイド内からContinuationクラスへのリンクが正しく表示されるようになります。

  1. 変更内容の詳細

変更ファイル:

  • guides/source/active_support_instrumentation.md(1行のみ変更)

実際の変更内容は、リンクテキストの表記揺れ/不整合を修正したもので、以下のようなイメージです。

diff
- Simple fix: [Continuation][]
+ Simple fix: [`Continuation`][]

Railsガイドでは、クラス名やメソッド名などのコード要素をリンクテキストとして使うときに、次のような慣習があります。

  • コード要素: バッククォートで囲む (例: `Continuation`)
  • リンクとして使う: [`Continuation`][] のようにMarkdownリンク構文で記述し、末尾の [] で参照スタイルリンクとする

今回の修正は、このガイド中にある Continuation に関する記述を、Railsガイドの他の箇所と同じスタイル(コードフォント + 参照リンク)に揃えることで、内部リンクレンダリングが正しく行われるようにしたものです。


  1. 影響範囲・注意点
  • 影響範囲

    • ドキュメント(Active Support Instrumentationガイド)のみ。
    • アプリケーションコード、フレームワークの挙動、API仕様には一切変更なし。
    • ガイドのHTML生成時に、Continuation へのリンクが正しく表示されるようになります(体裁・可読性向上)。
  • 注意点

    • ドキュメントビルド/サイト表示を確認する際、Continuation へのリンクが適切な箇所(該当クラスや節)に飛ぶことを確認する程度で十分です。
    • [ci skip] がタイトルに含まれている通り、CIはスキップしており、テスト実行対象にもなっていませんが、内容がドキュメントのみのため問題ありません。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Rails の ActiveSupport::Notifications において、Ractor が利用する「購読情報(subscriptions)のスナップショット」の実装を、Hash のコピーから「イミュータブルなレジストリオブジェクト」への差し替えにより、スレッド/Ractor セーフかつシンプルな形に整理した PR です。Ractor 間での通知購読状態の取り扱いを安全かつ効率的にするための内部実装変更で、外部 API は基本的に変わりません。

  1. 変更内容の詳細

背景: これまでの実装 ​

  • 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 オブジェクト」を指すだけになります。

イメージ的には:

ruby
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) しているので、理論上は読み取り中に変更が入る可能性があった。
  • 今回:
    • スナップショットは「イミュータブルな Fanout::Registry オブジェクト」そのもの。
    • スナップショットの取得は「1 つの frozen オブジェクトを読むだけ」になり、
    • Ractor 側でのロードは単なる「代入」になる:
ruby
# スナップショットを作る側
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 に依存しているため、その連携部分のテストが影響を受けていますが、機能仕様レベルでの大幅な変更はありません。

  1. 影響範囲・注意点
  • 外部 API への影響

    • ActiveSupport::Notifications.subscribe / unsubscribe などの公開 API の使い方は変わりません。
    • 通常のアプリケーションコードはこの変更の影響をほぼ受けません。
  • Ractor を使った場合の挙動

    • 以前と同様、「非メイン Ractor は自分専用の notifier / レジストリを持つ」という方針を維持しているため、
      • 「メイン Ractor で後から追加した subscriber が、自動的に他 Ractor にも見えるようになる」といった振る舞いは、この PR では導入されません。
    • Ractor ベースで細かく通知系を制御している場合は、引き続き「Ractor ごとに購読状態が独立している」前提で設計する必要があります。
  • パフォーマンス・メモリ

    • レジストリがイミュータブルになったことで、subscribe / unsubscribe のたびにレジストリオブジェクトが差し替えられます。
    • 通常は通知の購読変更はそれほど頻繁には行われないケースが多いため、このオーバーヘッドは限定的と考えられます。
    • 一方で、読み取り側は「単なるオブジェクト参照の読み取り」だけで済むため、Ractor 的にはより効率的で安全になります。
  • スレッド/Ractor セーフティ

    • ミューテックス外から内部 Hash を読む形ではなく、イミュータブルなスナップショットオブジェクトを使うことで、
      • データ競合や中途半端な状態の読み取りといった問題のリスクが小さくなっています。
    • 特に Ractor 間でオブジェクトを渡す場合、イミュータブルであることは大きな利点です。

  1. 参考情報 (あれば)
  • 関連 PR:
    • #58760: 本来この変更と一緒に入るはずだった、Ractor における購読伝播などを含む大きめの変更。
    • 9e8fe395: 「メイン Ractor だけがグローバルに記録し、非メインは Ractor ローカルに購読する」という方針を確立したコミット。
  • 関連クラス・モジュール:
    • ActiveSupport::Notifications::Fanout
    • ActiveSupport::Notifications::Fanout::Registry(本 PR の主な変更対象)
    • Concurrent::Map(以前のスナップショット先)
  • Ractor 利用時に気にすべきポイント:
    • 非メイン Ractor から通知を購読したい場合は、その Ractor 内で ActiveSupport::Notifications.subscribe する(メインからの自動伝播はこの PR では扱っていない)。

#58976 Make TimeZone.all and offset lookups work in non-main Ractors ​

マージ日: 2026/10/8 | 作成者: @yaroslav-shopify

  1. 概要 (1-2文で)
    ActiveSupport::TimeZone を Ractor 環境で安全に使えるようにするため、TimeZone.all や TimeZone[-5] などがメイン Ractor 以外でも Ractor::IsolationError を出さずに動くよう、内部データ構造を make_shareable! で共有可能オブジェクトにする修正です。これにより ractorize! 利用時のタイムゾーン関連メソッドの不具合が解消されます。

  1. 変更内容の詳細

背景 ​

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 にするように変更されています。

変更されたポイントの概要:

ruby
# 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 内でも:

ruby
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 していないことを自動テストで保証しています。


  1. 影響範囲・注意点
  • 影響範囲
    • ActiveSupport::TimeZone を Ractor 環境で利用している/今後利用したいアプリケーションやライブラリ。
    • 特に、バックグラウンド処理や並列実行で Ractor を採用し、かつタイムゾーン一覧やオフセット検索を使うコードに対して有益な修正です。
  • 非 Ractor 環境
    • 通常の(Ractor を使わない)Rails アプリケーションでは挙動の変化は事実上ありません。
      shareable 化は Ractor の有無にかかわらず安全な操作であり、既存コードに影響しない作りになっています。
  • パフォーマンス/メモリ
    • @zones / @zones_map は本来グローバルに1度構築されてほぼ読み取り専用で使われるデータであり、shareable 化によるオーバーヘッドは小さいと考えられます。
    • Ractor 間でコピーされずに共有されることで、むしろ Ractor 利用時のメモリ効率は改善の可能性があります。
  • 安定性・API 変更
    • 公開 API (TimeZone.all, TimeZone[-5], Time.find_zone 等) のシグネチャや戻り値の仕様は変わっていません。
    • ractorize! 自体は :nodoc: で実験的 API という扱いのままです。
  • CHANGELOG
    • Ractor 関連はこれまでも CHANGELOG に載せていない経緯があり、今回も更新されていません。
      そのため、Ractor を前提とした運用をしている場合は、CHANGELOG ではなく PR/コミットを直接追う必要があります。

  1. 参考情報 (あれば)
  • この 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. 概要 (1-2文で)
    Action View Overviewガイド中のヘルパーメソッドのシグネチャ表記で、誤って local: と書かれていた箇所を正しい locals: に修正したドキュメント専用PRです。コード本体には一切変更はなく、あくまでガイドの記述ミス修正です。

  1. 変更内容の詳細(あればサンプルコードも含めて)

対象ファイル:

  • guides/source/action_view_overview.md (+2/-2)

修正内容は、「Action View の各種ヘルパーのメソッドシグネチャを説明している部分」におけるオプション引数名の誤記です。

具体例(イメージ):

diff
- render partial: "product", object: @product, local: { editable: true }
+ render partial: "product", object: @product, locals: { editable: true }

Rails の render でパーシャルにローカル変数を渡す際は、オプションキーは単数形 local ではなく、正式には複数形 locals を使います。

そのため、ガイドのシグネチャ説明やサンプルにおいて local: となっていた部分を、実際のAPI仕様と整合するように locals: へ修正しています。


  1. 影響範囲・注意点
  • 実際の挙動・コードには一切影響しないドキュメント限定の修正です。
  • 影響範囲は Action View Overview ガイドを読む開発者のみですが、これまでガイドの誤記を信じて local: を使っていた場合、正しくは locals: であることを再確認する必要があります。
  • 既存のアプリでは、local: はオプションとして解釈されず、期待通りにローカル変数が渡っていなかった可能性があります。正しい書き方の例:
    ruby
    # 正しい
    render partial: "product", locals: { editable: true }
    
    # 誤り (このPRで修正された誤記に相当)
    render partial: "product", local: { editable: true }
  • CI スキップ ([ci skip]) 指定がタイトルにある通り、テスト実行も不要な軽微なドキュメント修正として扱われています。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Generators ガイド内のルーティング例で、PeopleController#index に対応するパスが誤って単数形 person になっていたのを、正しい複数形 people に修正するドキュメントのみの変更です。アプリケーションコードや挙動には一切影響しません。

  1. 変更内容の詳細(あればサンプルコードも含めて)
  • 対象ファイル: guides/source/generators.md
  • 修正内容: ガイド内のルーティング記述を、コントローラ名と一致する形に修正。

変更前(イメージ):

text
person GET  /person(.:format)  people#index

変更後:

text
people GET  /people(.:format)  people#index

もしくは同等の、「person → people」という単純な文字列修正です。
Generators ガイドの「6.12. route」節にある例で、PeopleController#index に対応するルート名・パスは RESTful な慣例に従い複数形であるべきところが、誤って単数形で書かれていたのを修正しています。


  1. 影響範囲・注意点
  • 影響範囲:
    • Rails ガイド(オンラインドキュメント)の表示内容のみ。
    • Rails 本体のコード、ジェネレータ、ルーティング機構には変更なし。
  • 注意点:
    • 実際の Rails アプリで resources :people などを使った場合、people_path / /people が生成されるのが正しい挙動であり、本 PR はその挙動とガイドの記述を一致させるものです。
    • 既存アプリのルーティング設定を変更する必要はありません。これはあくまでガイドの誤記修正です。

  1. 参考情報 (あれば)
  • 該当ガイド: 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. 概要 (1-2文で)
    config.active_record.queues.destroy の公式ガイドの説明文に、誤って「purge jobs」を指している箇所があり、実際に設定対象となる「destroy jobs」に修正したドキュメントのみの変更です。コードや挙動は一切変わっておらず、説明文の内容が実装に揃えられました。

  2. 変更内容の詳細

対象:
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 行のみの修正で、サンプルコード等はありません。

  1. 影響範囲・注意点
  • 影響範囲:
    • 公式ドキュメントを読んでキュー設定を行う開発者に対する認識修正のみ
    • Rails 本体の挙動や API は一切変更されていません
  • 注意点:
    • 今まで「purge jobs のキュー設定」と誤解して config.active_record.queues.destroy を使っていた場合、その理解が間違いだったことに注意が必要です。
    • destroy 関連(ActiveRecord::DestroyAssociationAsyncJob)と purge 関連(例: Active Storage や Active Record のクリーンアップ系ジョブ)のキュー設定を混同しないようにする必要があります。
    • queue 設定の確認時には、対応するジョブクラス(この場合 ActiveRecord::DestroyAssociationAsyncJob)とオプション名を突き合わせると安全です。
  1. 参考情報 (あれば)
  • 該当ガイド:
    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. 概要 (1-2文で)
    Rails Guides「Rails on Rack」で、未実行マイグレーション検知用ミドルウェア ActiveRecord::Migration::CheckPending を有効化する設定名が誤っていたため、正しい設定項目に修正し、併せてその設定を「Configuring Rails Applications」ガイドにも正式に追記したドキュメント修正です。アプリの動作には影響せず、ガイドの記述のみが更新されています。

  1. 変更内容の詳細

(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 は、以下の設定によって挿入される、という説明に修正されました:

ruby
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)と一致するように修正された形です。実装は次の通り:

ruby
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 で以下のように生成されます:

ruby
# config/environments/development.rb
config.active_record.migration_error = :page_load

この PR によって:

  • Rails on Rack ガイドから張られているリンクの遷移先(config.active_record.migration_error の説明)が新規に追加されたため、リンク切れが解消されました。
  • migration_error オプションの存在・挙動が正式なドキュメントとして明示された。

  1. 影響範囲・注意点
  • 影響範囲:
    • コード変更はなく、ガイド文書 (.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 を利用しているプロジェクトには変更は不要ですが、ガイドに沿って設定を見直すときに混乱が起きにくくなります。

  1. 参考情報 (あれば)
  • 対応する実装:
    • activerecord/lib/active_record/railtie.rb の initializer "active_record.migration_error" ブロック
  • 関連する設定:
    • config/environments/development.rb 内の config.active_record.migration_error = :page_load
    • Rack::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. 概要 (1-2文で)
    Railsガイド「caching_with_rails.md」において、権限ロール名の表記ゆれ/誤記を superusers から super_admins に修正したドキュメント専用のPRです。機能コードには一切変更がなく、挙動には影響しません。

  1. 変更内容の詳細
  • 対象ファイル: guides/source/caching_with_rails.md
  • 変更内容: 文字列の置き換えのみ
diff
- superusers
+ super_admins

文脈としては、キャッシュの例や権限管理の例示で使われているロール名において、他の記述と一貫した名称(super_admins)に揃えるための修正と考えられます。

ドキュメント中で、例えば以下のような記述になっている可能性があります(イメージ):

md
# 修正前(イメージ)
We don't want to cache pages for superusers.

# 修正後(イメージ)
We don't want to cache pages for super_admins.

つまり、ガイド全体またはサンプルコードで使われているロール名 super_admins に合わせたタイポ修正です。


  1. 影響範囲・注意点
  • 影響範囲

    • Rails本体のコード、API、動作には一切影響しません。
    • 影響するのは「キャッシュに関するRailsガイド」の表記のみです。
    • CIスキップ指定([ci skip])からも、ドキュメントだけの変更であることがわかります。
  • 注意点

    • もし自分のアプリ側で、ガイドをそのままコピペして superusers というロール名を使っている場合、ガイドの最新表記(super_admins)と異なる可能性があります。
      ただしこれはあくまで命名規則の問題であり、アプリが壊れる類のものではありません。
    • ドキュメントを読みながら実装する際は、ロール名などを自分のアプリのドメインに合わせて適宜変更して使うのが望ましいです。

  1. 参考情報 (あれば)
  • PRタイトルからわかる通り、[doc][ci skip] は「ドキュメント変更でありCIは回さない」ことを明示するRailsプロジェクト慣例のプレフィックスです。
  • 類似のドキュメント修正PRでは、表記ゆれ・スペルミス・古いサンプルコードの更新などが頻繁に行われており、ユーザーは最新ガイドを参照することでより正確な情報を得られます。

#58963 Use QueryResponse as raw result in ractor proxy adapters ​

マージ日: 2026/10/7 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    Rails の Ractor 用プロキシアダプタで使われていた独自のクエリ結果オブジェクトを廃止し、標準の QueryResponse を「生の結果」として採用する変更です。これにより、last_inserted_id などの扱いがシンプルかつ一貫した形になります。

  1. 変更内容の詳細

背景と目的 ​

  • 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 を前提とした実装に整理されたと考えられます。

ざっくりしたイメージ(疑似コード):

ruby
# 変更前(イメージ)
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 を、通常のアダプタと揃える

ざっくりとした変更イメージ:

ruby
# 変更前: 独自フィールドやメソッドを持っていた
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 を基準にコードを書けばよくなります。

  1. 影響範囲・注意点

影響範囲 ​

  • 対象: 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 参照 などを含む一連のテストを追加・実行しておくと安心です。

  1. 参考情報 (あれば)
  • 関連しうるコード:
    • ActiveRecord::ConnectionAdapters::QueryResponse 本体(通常アダプタで既に使われている共通クラス)
    • ractor_connection_handler 配下の他のアダプタ実装
  • 実務的な意味:
    • Ractor 対応コードの内部 API が通常アダプタと揃ってくることで、将来的に:
      • Ractor 対応まわりのリファクタリング
      • コネクションアダプタの共通化
      • 並行実行時の動作検証 がしやすくなります。

#58972 File.atomic_write: better cleanup on error ​

マージ日: 2026/10/7 | 作成者: @byroot

  1. 概要 (1-2文で)
    File.atomic_write で一時ファイルから本来のファイルへの rename が失敗した場合でも、一時ファイルを確実に削除するように後始末処理が改善されました。これにより、例外発生時に一時ファイルがディスク上に残り続ける問題が解消されます。

  1. 変更内容の詳細

※ PR本文からは振る舞いの概要のみが分かるため、Rails 既存の File.atomic_write 実装と一般的なパターンに基づいて技術的に説明します。

もともとの File.atomic_write の概要 ​

Active Support の File.atomic_write は概ね以下のような流れで動いています:

ruby
File.atomic_write("path/to/file") do |file|
  file.write("content")
end

内部的には:

  1. 同じディレクトリに一時ファイル (例: ".file-20261008-12345-1xyz.tmp") を作成
  2. ブロックに渡された IO に対して書き込み
  3. fsync などでフラッシュ (環境により挙動は異なる)
  4. File.rename(temp_file, target_path) で一時ファイルをターゲットパスにアトミックに置き換え
  5. 例外が起きた場合は一時ファイルを削除

この「例外が起きた場合」の扱いが従来はやや不十分でした。

今回の修正ポイント ​

PR の説明文:

If the rename fails, we still need to cleanup the temporary file.

から読み取れる変更点は次の通りです。

  • rename 実行時 (File.rename(temp_path, path)) に例外が発生した場合でも、
    • 一時ファイルを確実に unlink / delete するようにした
    • その上で、元の例外をそのまま呼び出し元に伝播させるような構造に変更

典型的には、従来はこんな感じのコードになっていたものが:

ruby
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 する」構造に修正していると考えられます:

ruby
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 など)を呼び出し元に伝播していることを確認

のようなケースが追加されているはずです。


  1. 影響範囲・注意点

影響範囲 ​

  • 対象: ActiveSupport の File.atomic_write を利用している全てのコード
    • 例:
      • Rails.cache の一部ストア実装
      • ActiveSupport::Cache::FileStore
      • ActiveSupport::EncryptedFile / EncryptedConfiguration
      • 各種設定ファイルや YAML をアトミックに更新するユーティリティ 等

実質的な挙動の変化 ​

  • rename に失敗したケースでの 一時ファイルの残存 が解消されます。
  • 正常に atomic_write が完了するケースの挙動は従来通りで、互換性に問題はないと考えられます。
  • rename が失敗した場合に投げられる例外クラスやメッセージは従来と同じ(Errno::EXDEV など)で、
    それに加えて「必ず一時ファイルが消える」という保証が強くなりました。

注意点 ​

  • 既存アプリ側で「atomic_write が失敗したとき、一時ファイルを自分で調査・回収する」ような運用やツールに依存している場合、その前提が変わります。
    • 一般的には望ましい変更であり、問題が出るケースはほぼないはずですが、
      デバッグ用途で「一時ファイルが残るのを頼りにしていた」場合は挙動が変わる点に注意してください。
  • rename 失敗の典型的な原因(異なるファイルシステム間での rename、パーミッションエラー、ディスクフル等)はこの変更で解消されるわけではありません。
    • それらは従来通り例外として呼び出し元でハンドリングが必要です。
  • cleanup ロジックが強化されたことで、ごくまれなタイミング依存バグ(例: 別プロセスが一時ファイルを掴んでいる場合など)が顕在化する可能性は理論上ありますが、通常の Rails アプリで問題になることはまずありません。

  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/58972
  • File.atomic_write の既存ドキュメント (Rails Guides / API):
  • 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. 概要 (1-2文で)
    Ractor 用の Active Record プロキシ接続プールが持つスキーマキャッシュの実装を、独自クラスではなく「BoundSchemaReflection」を用いたよりシンプルな仕組みに置き換えた PRです。これにより、Ractor 向けアダプタ周辺のスキーマキャッシュ用コードが整理・削減され、保守性と一貫性が向上しています。

  1. 変更内容の詳細

全体像 ​

  • これまで 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行)。

内容としては概ね以下のようなイメージになります(擬似コードレベル):

ruby
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)。

ここには以前、次のような「スキーマキャッシュメソッドを束ねるだけのクラス」や、それを生成して使うロジックがあったと考えられます:

ruby
# 以前のイメージ (削除された方向性のコード)
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 パスとの挙動が変わっていないこと

  1. 影響範囲・注意点

影響範囲 ​

  • 影響を受けるのは 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 ロジックと整合性を保ちやすくしている
      という設計上のメリットがあります。

  1. 参考情報 (あれば)
  • この 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. 概要 (1-2文で)
    Action Text の to_markdown が <code>/<pre> 含みの HTML を Markdown に変換する際に、テーブル内のコードや隣接するコード要素が原因でコードスパンが壊れ、結果として本来除去されるべき危険なリンク(javascript: など)がレンダリングされてしまう不具合を修正した PR です。併せて、「to_markdown はベストエフォートで unsafe link を落とすが、最終的な HTML は別途サニタイズすべき」という注意書きも追加されています。

  1. 変更内容の詳細

問題となっていた仕様と事象 ​

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:

html
<table><tr><td><code>  [x](javascript:alert(1))</code></td></tr></table>

旧 to_markdown 出力:

markdown
| ``  [x](javascript:alert(1))`` |

kramdown が行をセルへ分解する際にコードスパン(バッククォート列)を再構成し、その過程で が 1 つになり、先頭の backtick が「開き」にならずコードスパンが壊れる → 中身の[x](javascript:alert(1))` が Markdown のリンクとして解釈されてしまう。

ケース2: <code> 要素が隣接していると backtick がくっつく ​

HTML:

html
<p><code>a`b`</code><code>[x](https://example.com)</code></p>

旧出力:

markdown
`` a`b` ```[x](https://example.com)`

1 つ目のコードスパンの閉じ backtick 列と、2 つ目のコードスパンの開き backtick 列が隣接し、CommonMark 的には「連続した backtick 列」と解釈されてしまう → 二つの span が一体化して閉じなくなり、その後ろのリンク部分が通常テキストとしてパースされる → Commonmarker では <a> が生成される。

ケース3: テーブルセル内コードに |(パイプ)が含まれる ​

HTML:

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>

旧出力:

markdown
| `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 行追加)

  1. テーブル行内のコードスパン内容から前後の空白を除去

    • テーブル行(<tr> / <td> / <th>)を Markdown に変換する際、<code> 内テキストの先頭・末尾の空白文字(スペース、タブ、VT、FF など)を削除してからコードスパンに包むよう変更。

    • ケース1は次のように改善:

      markdown
      | `[x](javascript:alert(1))` |
    • これにより、kramdown 等に行を分割されても、先頭の空白によって backtick 解釈が壊れるリスクを避けられます。

  2. 隣接するコードスパンの間にスペースを挿入

    • Markdown テキスト生成時に、連続した <code> が backtick レベルで隣接しないよう、コードスパン同士が「隣り合う」場合には間に 1 文字分のスペースを自動で差し込む処理を追加。

    • ケース2は次のように改善:

      markdown
      `` a`b` `` `[x](https://example.com)`
    • これにより、CommonMark においても backtick 列が「一続き」と解釈されなくなり、それぞれ独立したコードスパンとして閉じ・開きが正しく認識されます。

    • 副作用として、「エスケープされた backtick で終わるプレーンテキストの直後にコードスパンが来るケース」 でも、このロジックにより間にスペースが入る可能性がある(= 見た目が 1 文字分変わる)点が PR 説明で明記されています。

  3. テーブル行内のコードスパン中の | をエスケープ

    • 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 には「元のバックスラッシュを完全に保ったまま | をセル区切りと見なさない」ための逃げ道がない)と説明されています。
  4. リンク URL 中の | を %7C にエンコード

    • HTML の <a href="..."> を Markdown にする際に、URL 内に | が含まれている場合はこれを %7C へ URL エンコードしてから Markdown リンクとして書き出すように変更。
    • これにより、テーブル内リンクであっても URL 内の | によってセルが分割されることを避けます。
  5. to_markdown のドキュメントに「unsafe link は best-effort で除去、HTML 側もサニタイズ推奨」を追記

    • ActionText::Content#to_markdown
    • ActionText::RichText#to_markdown

    それぞれに、以下のような注意書きが追加されています(要旨):

    • to_markdown は javascript: などの unsafe link を「できる限り」落とすベストエフォートの実装である。
    • しかし変換結果の Markdown を最終的に再び HTML にレンダリングする場合は、その HTML に対してもサニタイズを行うべきであり、to_markdown に完全なセキュリティの保証を期待すべきではない。

  1. 影響範囲・注意点
  • 対象バージョン

    • 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 対策)を行う前提で使うべきです。
  • 利用している Markdown レンダラ依存の挙動

    • GFM / Commonmarker を前提にした修正であり、Redcarpet など一部の実装とは完全には整合しません。
      • 例: Redcarpet はエスケープされた \| でもセル区切りにしてしまうため、ケース3は完全には解決しません。
    • もし独自のレンダラやバージョン違いを使う場合は、想定どおり unsafe link が消えているか・コードスパンが壊れていないかを検証することを推奨します。

  1. 参考情報 (あれば)
  • 対象コード:

    • actiontext/lib/action_text/markdown_conversion.rb
    • actiontext/app/models/action_text/rich_text.rb
    • actiontext/lib/action_text/content.rb
  • 関連 PR:

    • #58754, #58903
      • 同じく to_markdown におけるコードスパン/リンク処理まわりの不具合修正で、この PR はそれらに続く追加のセキュリティ/互換性修正です。
  • 関連仕様・実装:

    • GFM (GitHub Flavored Markdown) のテーブル仕様(セル区切りとしての |、エスケープルール)
    • CommonMark / kramdown / Redcarpet / Commonmarker のコードスパンおよびテーブル解釈ルール

#58949 [ci skip] Fix broken RDoc markup reference links in API guidelines ​

マージ日: 2026/10/6 | 作成者: @estebansa19

  1. 概要 (1-2文で)
    Rails のガイド「API Documentation Guidelines」に含まれていた RDoc のマークアップリファレンスへのリンクが 404 になっていたため、現在有効な URL に差し替えた修正です。コードには一切影響せず、ドキュメントのリンク切れのみを解消しています。

  1. 変更内容の詳細

対象ファイル:

  • 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 作成者が確認済み。

変更差分イメージ(擬似的な例):

markdown
- 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 の趣旨はこのようなパス変更です。


  1. 影響範囲・注意点
  • 影響範囲:

    • Rails ガイド(英語)の「API Documentation Guidelines」を閲覧する開発者向けのみの変更です。
    • Rails 本体の動作・API・ビルド・テストには一切影響ありません。
    • CI を不要と判断しているため、タイトルに [ci skip] が付いています。
  • 注意点:

    • プロジェクト内や社内ドキュメントなどで、同じ旧 URL (RDoc/MarkupReference.html 等) を参照している場合、今回と同様にリンク切れになっている可能性が高いです。
      → 自分たちのドキュメントでも、RDoc マークアップへのリンクが古くなっていないか確認するとよいです。
    • このリンクは過去にも 1 度切れており (#47282 で修正)、RDoc 側の URL 変更の影響を受けやすい箇所であることが分かります。将来の変更に備えて、RDoc の公式ドキュメント構造が変わった際には再度見直しが必要になるかもしれません。

  1. 参考情報 (あれば)

#58946 Build the insert values list once for raw on_duplicate upserts on MySQL ​

マージ日: 2026/10/6 | 作成者: @fabbyd

  1. 概要 (1-2文で)
    MySQL 8.0.19以降で、upsert_all / upsert に「生の SQL フラグメント」を on_duplicate: オプションとして渡した場合に、INSERT の VALUES 句が二重に組み立てられてしまう問題を解消し、Ruby 側の処理コストを大きく削減するパフォーマンス改善です。挙動や生成される SQL 自体は変わらず、同じバイト列を維持したまま内部の組み立て順序だけ最適化しています。

  1. 変更内容の詳細

何が問題だったか ​

対象:

  • DB: MySQL 8.0.19 以降 (MariaDB および MySQL < 8.0.19 は非対象)
  • メソッド: upsert_all, upsert
  • 条件: on_duplicate: に Arel.sql(...) などの「生 SQL フラグメント」を渡した場合
    例:
    ruby
    raw_update = Arel.sql("title = VALUES(title), pages = VALUES(pages)")
    Model.upsert_all(rows, on_duplicate: raw_update)

このケースで、AbstractMysqlAdapter#build_insert_sql が内部で以下のような動きをしていました:

  1. まず「行エイリアス付き INSERT」形式を組み立てようとする

    • 形式イメージ:
      sql
      INSERT INTO `table` (...) VALUES (...), (...) AS `table_values` (...)
    • その際に insert.values_list を呼び出し、「VALUES の全行・全カラム」を組み立てる。
  2. しかし on_duplicate が「生 SQL」の場合は、

    • 行エイリアスを使うと、on_duplicate 内のカラム参照が曖昧になる(エイリアスが前提になっていない)
    • そのため「行エイリアスなしの INSERT ... ON DUPLICATE KEY UPDATE ...」形式に切り替える必要がある。
  3. そこで一度作ったエイリアス付きの 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/s
    • update_only: が約 53 i/s
    • → 生 on_duplicate パスはコントロール比で約 1.8 倍遅い
  • 本 PR ブランチ (変更後)

    • on_duplicate: raw_sql が約 54 i/s
    • update_only: も約 54 i/s
    • → 2つはほぼ同じ速度になり、余分なオーバーヘッドが解消された

つまり、「生 on_duplicate を使うとやたら遅い」という現象が、MySQL 8.0.19+ においてはほぼ解消されます。


  1. 影響範囲・注意点

挙動・互換性 ​

  • 生成される SQL は PR 説明どおり「バイトレベルで完全に同一」であり、挙動の変更はありません。
  • 特にカラム名の解決や VALUES(...) 関数の挙動など、DB 側の結果に影響する変更は行っていません。
  • 既存テスト (insert_all_test.rb の
    • test_upsert_all_updates_using_provided_sql
    • test_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 を渡しているケース
  • 非対象/影響なし:

    • 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(例: ログ集約やバッチ処理)で特に効果が大きいため、そのようなワークロードがある場合はパフォーマンス計測をすると改善を確認しやすいです。

  1. 参考情報 (あれば)
  • 対象メソッド:
    • ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#build_insert_sql
  • 関連テスト:
    • activerecord/test/cases/insert_all_test.rb
      • test_upsert_all_updates_using_provided_sql
      • test_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. 概要 (1-2文で)
    Rails 8.2 用の new_framework_defaults_8_2.rb 初期化ファイルテンプレート内で、default_headers の設定例がそのままではエラーになる不備を修正した PR です。トップレベルの initializer でも正しく動作するよう、config を Rails.application.config に修正しています。

  1. 変更内容の詳細

問題になっていたのは、Rails が新規アプリ作成時などに生成するテンプレート
config/initializers/new_framework_defaults_8_2.rb の中のサンプルコードです。

元の例(誤り):

ruby
# Rails アプリ生成時にコメントアウトされた状態で入っている想定のコード例
config.action_dispatch.default_headers = {
  "X-Frame-Options" => "SAMEORIGIN",
  # ...
}

initializer は「トップレベル」で実行されるため、このスコープには config というローカル変数 / メソッドは定義されていません。そのため、コメントアウトを外して起動すると:

text
NameError: undefined local variable or method `config' for main:Object

というエラーになります。

この PR では、これを他のオプション記述と同様に Rails.application.config に揃えています。

修正後:

ruby
Rails.application.config.action_dispatch.default_headers = {
  "X-Frame-Options" => "SAMEORIGIN",
  # ...
}

ポイント:

  • Rails.application.config を明示することで、initializer のトップレベルからでも確実に設定できる。
  • ファイル内の他の設定例(例: Rails.application.config.active_record... など)と表記を統一。

変更はテンプレートファイル (*.rb.tt) の 1 行置き換えのみです。


  1. 影響範囲・注意点
  • 対象は Rails 8.2 のアプリ生成テンプレート だけであり、まだ「未リリースのファイル」に対する修正です。
    • すでに 8.2 の preview や edge を使ってアプリを生成しており、かつ new_framework_defaults_8_2.rb 内の default_headers 設定例をそのまま config... で書いている場合のみ影響します。
  • 実際のアプリ側の挙動に関する仕様変更ではなく、「サンプルコードが NameError を起こさないようにする」だけの修正です。
  • すでに自分のアプリで以下のように書いている場合は、特に変更不要です:
    ruby
    Rails.application.config.action_dispatch.default_headers = {
      "X-Frame-Options" => "SAMEORIGIN"
    }
  • 逆に、もし以下のような書き方をしている場合は、ブート時 NameError を避けるために書き換えが推奨されます:
    ruby
    # NG 例(initializer のトップレベルでは NameError)
    config.action_dispatch.default_headers = { ... }

  1. 参考情報 (あれば)

#58847 Compose compatibility TableDefinition modules in Migration::Current ​

マージ日: 2026/10/5 | 作成者: @yahonda

  1. 概要 (1-2文で)
    このPRは、マイグレーション互換クラス (V8_0, V7_0, V6_1, V6_0, V5_2, V5_0, V4_2) に重複していた compatible_table_definition 実装を、Migration::Current に一箇所へ集約し、TableDefinition モジュールの適用を動的に行うようにしたリファクタリングです。挙動は一切変えずに、DRYかつ保守しやすい構造に整理しています。

  1. 変更内容の詳細

これまでの構造 ​

各互換バージョンのマイグレーションクラス (例: ActiveRecord::Migration::Compatibility::V7_0) には、create_table / change_table ブロック内で使われる t オブジェクトに対して、そのバージョン固有の TableDefinition モジュールを prepend するためのメソッドが定義されていました。

各クラスで、ほぼ同じコードが繰り返されていました:

ruby
# 例: 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
  • やっていること:
    1. 現在のマイグレーションオブジェクトのクラス階層 (ancestors) をたどる
    2. その中から「TableDefinition モジュールを定義しているクラス」を探す
    3. 見つかった TableDefinition モジュールを、古いバージョンから順に t.singleton_class.prepend(...) する
  • 個々の互換バージョンクラス (V8_0, V7_0, V6_1, V6_0, V5_2, V5_0, V4_2) からは、上記の重複メソッド実装を削除し、「TableDefinition モジュールを定義する」だけに簡略化

イメージ的には、これまで:

ruby
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, ... と続く

だったものが、今後は:

ruby
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内で明言されている通り、「マイグレーション互換性テストは変更なしで通っているため、挙動は変わっていない」ことが確認されています。


  1. 影響範囲・注意点
  • 公開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 を行っており、実質的なコストはあまり変わりません。

  1. 参考情報 (あれば)
  • この重複実装が増えていった経緯:

    • 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. 概要 (1-2文で)
    Rails の ActiveRecord において、config.active_record.table_name_prefix を使っている環境で、ActiveRecord::Migration[5.1] 以前の change_column が誤ったテーブル(プレフィックスなし)に対して DEFAULT などを設定してしまう不具合を修正した PR です。これにより、古いバージョン互換モードのマイグレーションでも、テーブルプレフィックスが一貫して尊重されるようになります。

  1. 変更内容の詳細(あればサンプルコードも含めて)

問題の背景 ​

config.active_record.table_name_prefix = "p_" のようにテーブル名プレフィックスを設定している場合、本来 :posts は実テーブル p_posts にマッピングされます。

以下のような 5.1 互換マイグレーションがあるとします:

ruby
class AddDefaultToPostsTitle < ActiveRecord::Migration[5.1]
  def change
    change_column :posts, :title, :string, default: "untitled"
  end
end

期待される SQL はどちらも p_posts を対象にするべきです:

sql
ALTER TABLE "p_posts" ALTER COLUMN "title" TYPE character varying;
ALTER TABLE "p_posts" ALTER COLUMN "title" SET DEFAULT 'untitled';

しかし、問題となっていた挙動では PostgreSQL に対し次のようなクエリが発行されていました:

sql
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 は次のように統一されます:

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: the CommandRecorder replays 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 を使うようにするための修正)。
  • activerecord/test/cases/migration/compatibility_test.rb
    • table_name_prefix 設定下で、5.1 マイグレーションが正しいテーブル名に対して DEFAULT 等を発行することを確認するテストを追加。
  • activerecord/CHANGELOG.md
    • バグ修正としてエントリを追加。

  1. 影響範囲・注意点
  • 対象となるのは主に以下の条件に当てはまるプロジェクトです:

    • 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 適用後は、上記のような不整合は解消され、新規マイグレーション・再実行ともに一貫してプレフィックス付きテーブルに対してのみ変更が行われます。


  1. 参考情報 (あれば)

#58845 Honor an explicit type: on SQLite references in 6.0 migrations ​

マージ日: 2026/10/5 | 作成者: @yahonda

  1. 概要 (1-2文で)
    SQLite を使っている環境で、ActiveRecord::Migration[6.0] の add_reference に明示的な type: オプションを渡した場合に、その型指定(例: type: :bigint)が正しく反映されるようにする修正です。互換性レイヤで SQLite に対して常に :integer を強制していたコードを削除し、アダプタ本来の挙動を尊重するようにしています。

  1. 変更内容の詳細

これまでの問題点 ​

Rails 6.0 形式のマイグレーションで、SQLite を使っている場合:

ruby
class AddUserToPosts < ActiveRecord::Migration[6.0]
  def change
    add_reference :posts, :user, type: :bigint
  end
end

のように type: :bigint を明示しても、実際には無視されて次のような SQL になっていました。

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 行)

修正後の挙動 ​

明示的に type: を指定した場合:

ruby
class AddUserToPosts < ActiveRecord::Migration[6.0]
  def change
    add_reference :posts, :user, type: :bigint
  end
end
sql
ALTER TABLE "posts" ADD "user_id" bigint

type: を省略した場合の挙動は従来どおりで、SQLite3 アダプタが integer をデフォルトとして使います:

ruby
class AddUserToPosts < ActiveRecord::Migration[6.0]
  def change
    add_reference :posts, :user
  end
end
sql
ALTER TABLE "posts" ADD "user_id" integer

表示上の副作用 ​

マイグレーションの verbose 出力(ログ)において、これまで 6.0 互換レイヤが type: :integer を付与していたために

text
-- add_reference(:posts, :user, {:type=>:integer})

のように見えていたケースが、今後はアダプタ側のデフォルトに任されるため、明示的な {type: :integer} が表示されなくなります。ただし、実際に作成されるカラム型は従来と同じ integer のままです。


  1. 影響範囲・注意点
  • 対象:
    • 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 互換テストもすべてグリーンとのことなので、実務上この変更で壊れるケースは非常に限定的と考えられます。
  • 既存の schema.rb との不整合:
    • 古い Rails バージョンで生成された schema.rb が integer 前提になっていて、同じマイグレーションを新しい Rails で実行し直すと bigint になる、という差分が出る可能性があります。
    • とはいえ、SQLite かつ 6.0 互換マイグレーションを再実行する、という限定的状況なので、多くのアプリでは影響は軽微です。
  • マイグレーションログの差異:
    • 上記の通り、verbose ログに出る {type: :integer} の有無だけが変わるケースがあります。ログの内容でテストしているような特殊なケースがある場合は注意が必要ですが、通常は問題になりません。

  1. 参考情報 (あれば)
  • 本 PR が依存している PR: #58844
  • 元のバグ修正 PR / Issue:
    • #43295: SQLite で 6.0 マイグレーションを 6.1 以降で再実行した際に integer → bigint に変わってしまう問題への対処
    • #43168: 上記問題の Issue
  • 挙動が変わるに至った経緯:
    • #42350 にて V6_0#add_reference が super を呼ぶようになり、アダプタのデフォルト処理に戻ったにもかかわらず、SQLite に対する type: :integer 強制分岐だけが残ってしまっていた。
    • その結果、「元々直したかったケース」にはもう効いていないのに、「明示した type: を潰す」という副作用だけが残っていた。
    • 今回の PR ではその不要な分岐を削除し、アダプタ本来の挙動(型指定を尊重する)に揃えています。

#58932 Remove orphaned Active Support dependencies test fixtures ​

マージ日: 2026/10/5 | 作成者: @dpep

  1. 概要 (1-2文で)
    Active Support の dependencies 周りのテストフィクスチャ(8ファイル)が、どこからも参照されていない「孤立ファイル」になっていたため、まとめて削除した PR です。実行時の挙動や API には一切影響のない、テスト専用ファイルのクリーンアップです。

  1. 変更内容の詳細

削除対象のファイル ​

activesupport/test/dependencies/ 配下の以下 8 ファイルが削除されています。

  • check_warnings.rb
  • conflict.rb
  • cross_site_depender.rb
  • module_folder/lib_class.rb
  • raises_exception.rb
  • requires_nonexistent0.rb
  • service_one.rb
  • service_two.rb

行数ベースでは、合計 39 行の削除のみ(追加なし)です。

背景となるテスト構造の変化 ​

  • 以前は activesupport/test/dependencies/ 以下のテスト用 Ruby ファイル群を直接読み込むテストが存在していました。
  • しかし、以下の変更に伴い、そのテストは既に削除されています。
    • 「classic autoloader helpers」の削除(コミット af27a25f199)
    • require_dependency の非推奨化に対応する変更(コミット 57a5577169c)

57a5577169c では同じディレクトリ内の以下の 3 ファイルだけ削除されていましたが、残り 8 ファイルが取り残されていた状態でした。

  • mutual_one.rb
  • mutual_two.rb
  • requires_nonexistent1.rb

また、現在の dependencies_test.rb は、tmpdir 配下に自前でフィクスチャファイルを生成しており、activesupport/test/dependencies/ 配下のファイルは参照していません。

参照確認 ​

  • リポジトリ全体を検索した結果、削除対象となった 8 ファイルのパスや、そこに定義されている定数はどこからも参照されていないことが確認されています。
  • Active Support のテストスイートは、これらのファイルを削除した状態でも成功しています(ただし後述の CI のタイミングテストの揺らぎは発生)。

テストコード例そのものは PR に載っていませんが、これらは典型的な「autoload / require / 依存関係まわりの挙動検証用マイクロファイル」(例: 例外を投げる・存在しないファイルを require する・サービスクラスっぽい定義を置く 等)と見てよい構造です。


  1. 影響範囲・注意点

影響範囲 ​

  • 本番コード(activesupport/lib/... など)には一切変更がないため、Rails / Active Support を利用するアプリケーションの挙動・API には影響しません。
  • 変更対象は テストフィクスチャのみ であり、削除されたファイルは現行のテストコードからも参照されていません。
  • Active Support のテストを実行している環境でも、通常は影響ありません。

注意点 ​

  • もしローカルの独自テストや社内ツールなどが、Rails 本体の activesupport/test/dependencies/*.rb を直接 require していた というような特殊な運用をしていた場合、そのコードはファイル削除により壊れます。
    • Rails 本体のテストフィクスチャを外部から利用するのは非推奨のため、そのようなケースがある場合は、自分のリポジトリ側にフィクスチャをコピーして管理する形に修正すべきです。
  • CI では ContinuousIntegrationTest#test_parallel_group_timing で一度失敗が出ていますが、これは単なるタイミング系テストのフレークであり、この PR の変更とは無関係とされています(リトライ 3 回で通過)。

  1. 参考情報 (あれば)

#58933 Remove unused Contact#pseudonyms from Active Model tests ​

マージ日: 2026/10/5 | 作成者: @dpep

  1. 概要 (1-2文で)
    Active Model のテスト用モデル Contact に定義されていた未使用メソッド pseudonyms を削除する PR です。テストコード内でのみ利用されていたメソッドであり、本体機能への振る舞い変更はありません。

  1. 変更内容の詳細(あればサンプルコードも含めて)
  • 対象ファイル: activemodel/test/models/contact.rb

  • 変更内容: Contact モデルに定義されていた以下のメソッドが削除されました。

    ruby
    def pseudonyms
      ["Martin", "Martini"]
    end
  • このメソッドはもともと @contact.to_xml(methods: :pseudonyms) のように、XML シリアライズ時に追加メソッドとして出力する用途で使われていましたが、XML シリアライザ関連コードが f7ebdb1ac5 のコミットで削除された際に呼び出し元がなくなっていました。

  • レポジトリ全体を検索した結果、この pseudonyms メソッドへの他の参照は存在しないことが確認されています。


  1. 影響範囲・注意点
  • 影響範囲:

    • Active Model のテストスイート内のテスト用モデルに限定された変更です。
    • ライブラリ本体の API や機能に影響はありません。
    • Active Model のテストスイートは 1229 runs, 0 failures で問題なく通過しています。
  • 注意点:

    • activemodel/test/models/contact.rb はあくまで Rails 本体のテスト用コードなので、通常のアプリケーションコードから直接参照することは想定されていません。
    • もし独自に Rails 本体リポジトリのテスト用モデルを参照している特殊な環境があれば、その中で Contact#pseudonyms を呼んでいないかだけ確認が必要ですが、一般的な利用では影響はないと考えてよいです。

  1. 参考情報 (あれば)

#58844 Add regression tests for adapter-specific migration compatibility ​

マージ日: 2026/10/5 | 作成者: @yahonda

  1. 概要 (1-2文で)
    このPRは、Rails のバージョン付きマイグレーションが「当時のRails+DBアダプタで生成していたスキーマ」を継続して再現できているかを確認するためのリグレッションテストを追加するものです。コードの分岐ではなく「実際に生成されるスキーマ」を検証しており、挙動が壊れた場合に検知できるようにしています。

  1. 変更内容の詳細

全体像 ​

  • 変更ファイルは 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: を指定している場合でも、そのオプションが失われずに残ることを検証。

イメージ的なサンプル:

ruby
class CreateUsers < ActiveRecord::Migration[5.1]
  def change
    create_table :users, options: "ROW_FORMAT=DYNAMIC" do |t|
      t.string :name
    end
  end
end

このマイグレーションから生成されるテーブルが、MySQL上で例えば

sql
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 を実行しても失われないことを保証。

例:

ruby
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: を指定した場合 → その指定が優先される
  • 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] でも期待どおり動作していることを確認。

例:

ruby
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 :users
    • rename_table :users, :customers
    • remove_foreign_key :orders, :users
  • といった操作の verbose ログ上のテーブル名は、users / customers 等、「マイグレーションに書かれた名前」で表示されること。

特に、rename_table の「新しいテーブル名」や remove_foreign_key の「参照先テーブル名」のような「二つ目以降のテーブル名」も、正しくプレフィックスなしで表示されることをテストしています。


  1. 影響範囲・注意点
  • 実装コードに変更はなく、追加されたのはテストのみです。
  • 影響範囲:
    • Rails の互換レイヤ(ActiveRecord::Migration::Compatibility)が今後リファクタリングされたり挙動が変わった場合に、MySQL / PostgreSQL それぞれで「過去バージョンのマイグレーションが期待どおりのスキーマを生成しているか」を即座に検知できるようになります。
    • CI で MySQL / PostgreSQL 環境を使っている場合、これらのテストが走ることでアダプタ固有の互換性バグの検出力が上がります。
  • 注意点:
    • このPRを取り込んだあとに、アダプタごとの挙動を意図的に変えたい場合、これらのテストが落ちる可能性があります。その場合は「どのバージョンのマイグレーションまで互換性を維持するか」を明確にしたうえで、テストの期待値を更新する必要があります。
    • DB側の拡張(PostgreSQL の uuid-ossp 拡張など)がテスト環境で有効でないと、uuid_generate_v4() 周りのテストが落ちる可能性があります(Rails のテストスイートは通常その前提で構成されます)。

  1. 参考情報 (あれば)
  • ActiveRecord::Migration::Compatibility のコード(アダプタ名による分岐ロジック)を読むと、このPRでカバーされた互換挙動がどのように実装されているかを確認できます。
  • 公式ガイド:
  • 過去バージョンマイグレーションを使っているアプリケーションでは、このPRのようなテスト観点を自前で採り入れることで、「Rails アップグレード後も古いマイグレーションの再実行で同じスキーマが得られるか」を確認しやすくなります。

#58781 Stop setting the public-read ACL on S3 uploads for services configured with public: true ​

マージ日: 2026/10/5 | 作成者: @beauraF

  1. 概要 (1-2文で)
    Active Storage の S3 サービスが、public: true 設定時に必ず public-read ACL を付けてアップロードしていた挙動を制御できるようにし、ACL 無効("Bucket owner enforced")がデフォルトの S3 バケットでもエラーなく動作させられるようにした PR です。新しいフラグ config.active_storage.s3_public_uploads_via_acl を導入し、デフォルトでは現状維持、新しいアプリ(load_defaults 8.2)では ACL なしでの公開を前提とするように切り替えています。

  1. 変更内容の詳細

背景問題 ​

  • 現在の 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 に新しいグローバル設定が導入されています:

ruby
# config/application.rb など
config.active_storage.s3_public_uploads_via_acl = true # デフォルト
  • true(デフォルト)
    • これまで通り、public: true な S3 サービスでは acl: "public-read" を付けてアップロードします。
  • false
    • public: 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 の例:

yaml
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.md
    • config.active_storage.s3_public_uploads_via_acl の説明と、バケットポリシーで公開を制御する推奨パターンがドキュメント化。
  • activestorage/test/service/s3_public_service_test.rb
    • s3_public_uploads_via_acl が true / false の両パターンで、ACL の有無が期待通りになることを検証するテストが追加されています。
  • railties/lib/rails/application/configuration.rb
    • config.active_storage.s3_public_uploads_via_acl の設定項目追加。
  • new_framework_defaults_8_2.rb.tt
    • 新規アプリ向けのデフォルト値(false)と説明コメントを追加。

  1. 影響範囲・注意点

既存アプリ(アップグレード時) ​

  • config.load_defaults を 8.2 にしない、もしくは new_framework_defaults_8_2.rb を適用しない限り:
    • config.active_storage.s3_public_uploads_via_acl は true のまま。
    • public: true な S3 サービスは現状通り public-read ACL を付与してアップロードするため、挙動変化はありません。
  • ただし、すでに 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 無効)」に移行したい場合:
    1. Rails 側で config.active_storage.s3_public_uploads_via_acl = false に変更する。
    2. 既存の storage.yml から upload.acl の明示的指定を削除する(ACL 不要であれば)。
    3. バケットポリシーで公開ポリシーを設定し、public オブジェクトへの GET を許可する。
    4. 動作確認後に、S3 バケットのオブジェクト所有権を「Bucket owner enforced」に切り替える。

  1. 参考情報 (あれば)

#58941 Restore 100ms between checkins in test_checkout_fairness ​

マージ日: 2026/10/5 | 作成者: @yahonda

  1. 概要 (1-2文で)
    ActiveRecord::ConnectionAdapters::ConnectionPoolThreadTest#test_checkout_fairness が CI 上でときどき失敗する問題に対処するため、接続の「チェックイン間のスリープ時間」を 10ms から 100ms に戻し、テストの安定性を優先する変更です。テストの実行時間はわずかに増えますが、フェアネス検証の信頼性が向上します。

  1. 変更内容の詳細

対象は activerecord/test/cases/connection_pool_test.rb の test_checkout_fairness テストのみで、スリープ時間を 0.01 秒 → 0.1 秒に戻しています。

イメージとしては次のような変更です(疑似コード):

ruby
# 変更前(高速化後だが不安定)
# sleep 0.01

# 変更後(元の値に戻す)
sleep 0.1

テストの意図は「コネクションプールの checkout が、待っているスレッド間で公平(先に待っていたスレッドから順に取得できる)か」を確認することです。
問題になっていた失敗例:

diff
- 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 秒に短縮してテストを高速化していたが、その変更の一部を巻き戻して安定性を優先する。

  1. 影響範囲・注意点
  • 影響範囲

    • 本番コードには一切影響なく、Active Record のテストコードのみが変更対象です。
    • 該当テスト (test_checkout_fairness) の実行時間が約 0.11 秒 → 約 1.01 秒程度に増加します(ローカル計測ベースで +0.9 秒)。
    • CI 上の「テスト全体時間」に対してはごく軽微な増加にとどまる想定です。
  • 注意点 / 開発者視点での含意

    • スレッドスケジューリングやタイミングに依存するテスト(特にフェアネスや順序を検証するテスト)は、多少の時間的マージンを取らないと CI 環境でフレーク(非決定的な失敗)を起こしやすい、という典型例です。
    • 類似のテストを追加・変更する場合も、「ローカルでは十分に見える sleep でも CI では不足する可能性がある」点を意識して設計する必要があります。
    • テスト高速化のための sleep 短縮やタイミング依存ロジックの強化は、フレーク発生率とのトレードオフになるため、安定性を優先する指針がこの PR から読み取れます。

  1. 参考情報 (あれば)
  • 対象テスト:
    ActiveRecord::ConnectionAdapters::ConnectionPoolThreadTest#test_checkout_fairness
    (activerecord/test/cases/connection_pool_test.rb)

  • 関連コミット:

    • 元の 0.1 秒スリープを導入していたコミット: 02b23355637
    • 0.1 秒 → 0.01 秒へ短縮してテスト高速化したコミット: d0d74258547(今回の PR はこれを部分的に巻き戻し)
  • 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. 概要 (1-2文で)
    Rails の PostgreSQL アダプタに maintenance_database オプションが追加され、db:create, db:drop, db:purge が接続に使うデータベースを postgres 以外に変更できるようになりました。これにより、DigitalOcean など postgres データベースが存在しない / 接続できないマネージド PostgreSQL でも Rails の DB タスクが正常に動作します。

  1. 変更内容の詳細

新オプション maintenance_database の追加 ​

PostgreSQL アダプタに、メンテナンス用の接続先 DB を指定する maintenance_database オプションが導入されました。

  • 対象タスク:
    • bin/rails db:create
    • bin/rails db:drop
    • bin/rails db:purge
  • 従来:
    これらのタスクは常に postgres データベースに接続して CREATE DATABASE / DROP DATABASE を実行していた。
  • 変更後:
    maintenance_database に指定された DB に接続してコマンドを実行する。
    未指定時は従来通り postgres を使用する(後方互換あり)。

設定例: database.yml ​

yaml
production:
  adapter: postgresql
  database: blog_production
  maintenance_database: defaultdb

設定例: DATABASE_URL ​

DATABASE_URL にクエリパラメータとして指定可能:

bash
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 を指定した時のエラーハンドリング を検証するテストが追加。

  1. 影響範囲・注意点
  • デフォルト動作は変更なし

    • maintenance_database 未指定時は、これまで通り postgres に接続してタスクを実行する。
    • 既存アプリは設定を追加しない限り挙動は変わらない。
  • マネージド PostgreSQL サービスでの利用

    • DigitalOcean のように postgres データベースが存在しない/接続を拒否する環境では、
      maintenance_database を defaultdb など有効な DB に設定する必要がある。
    • PostgreSQL の仕様上、CREATE DATABASE / DROP DATABASE は 「どれか既存の DB に接続して実行できればよい」ので、defaultdb のような運用用 DB を指定すればよい。
  • メンテナンス DB の存在チェック

    • maintenance_database に指定した DB 自体が存在しない場合は、
      ConnectionNotEstablished としてエラーになる(アプリケーション DB がないとは解釈されない)。
    • これは設定ミスの検知として望ましいが、従来とは例外の種別が変わるので rescuing しているコードがある場合は注意。
  • template1 へのフォールバックは行わない

    • PostgreSQL の createdb コマンドは --maintenance-db 未指定時に postgres → template1 とフォールバックするが、 DigitalOcean では template1 への接続が拒否されるため、Rails ではこのフォールバックは採用していない。
    • Rails の挙動は「指定された maintenance_database か(未指定なら)postgres だけを使う」。

  1. 参考情報 (あれば)

運用上は、マネージドサービスの「メンテナンス DB」もしくは「デフォルト DB」とされているもの(例: DigitalOcean の defaultdb)を maintenance_database として指定するのが推奨される使い方となります。


#58934 Remove orphaned legacy adapter test fixtures ​

マージ日: 2026/10/5 | 作成者: @dpep

  1. 概要 (1-2文で)
    Rails Active Record のテスト用に一時的に置かれていた「レガシーアダプタ登録 API」向けのダミーアダプタ(fake_legacy_adapter.rb / fake_misleading_legacy_adapter.rb)が、もはやどこからも読み込まれておらず不要になったため削除された PR です。実行時の挙動やテスト結果に影響はなく、純粋なテストフィクスチャの掃除です。

  1. 変更内容の詳細

背景となる挙動の変化 ​

  • 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.rb
    • activerecord/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.rb
  • activerecord/test/active_record/connection_adapters/fake_misleading_legacy_adapter.rb

両方ともテスト環境専用の「レガシーアダプタっぽく見せるためのフィクスチャ」でした。PR によると:

  • リポジトリ全体を検索しても、この 2 ファイルやそこで定義されていた定数に対する require や参照は一切存在しない。
  • registration_test.rb はこれらのファイルがなくても通る。
  • Active Record の sqlite3 のテストスイートも問題なく通る。

サンプルコードとしての中身は削除済みで PR からは見えませんが、典型的には以下のような「アダプタ名に応じたクラス/メソッドを定義するだけのダミー」だったと想定されます:

ruby
# 例: 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

これらがなくても、現在のアダプタ登録・解決ロジックは動作し、テストも成功することが確認されています。


  1. 影響範囲・注意点
  • 実行時挙動:
    • 削除対象はテストディレクトリ以下のファイルのみであり、本体コード(ライブラリ/API)の挙動に一切影響はありません。
  • テストへの影響:
    • Rails 本体の Active Record テストにおいても、この 2 ファイルはすでに参照されていないため、テスト結果は変わりません。
    • もし、社内フォークや独自パッチなどでこれらのファイルを直接 require していた場合は、当然ロードエラーになりますが、公式リポジトリ側ではそのような利用は想定されていません。
  • レガシーアダプタサポートに関する誤解:
    • この PR は「レガシーアダプタのサポートをさらに削った」わけではなく、「すでに削られている/変更されているロジックのためのテストフィクスチャを片付けただけ」です。
    • したがって、7.2 でのレガシーアダプタ登録 API の非推奨方針自体に新たな変更があったわけではありません。

Rails アプリ開発者視点では、今回の変更を意識する必要はまずありません。Rails 本体にコントリビュートしている場合にのみ、「不要なテスト用ファイルが整理された」と認識しておけば十分です。


  1. 参考情報 (あれば)
  • 該当 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. 概要 (1-2文で)
    ActiveRecord::Persistence#reload が「何を」「どこまで」リロードするのかを明確にするため、ドキュメント文言の改善と、それに対応するテストが追加された PR です。
    主に「属性値と AR が管理する内部状態はリセットされるが、開発者が独自に持たせたカスタム状態はそのまま残る」ことが明示されました。

  1. 変更内容の詳細

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 にテストが追加され、ドキュメントで説明された挙動が実際の仕様として固定されます。

概念的には、次のようなテストが入っていると考えられます(擬似コード):

ruby
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 によって知られていないインスタンス変数はそのまま残る

という仕様をテストで保証しているという点です。


  1. 影響範囲・注意点
  • 実行時の挙動は変更されていない
    既存アプリケーションの reload の挙動は従来通りで、コード上の互換性を壊す変更はありません。
    変わったのは「説明とテスト」であり、現状の挙動を公式に仕様として固定した形です。

  • テストやキャッシュ実装での落とし穴
    このPRが明示している通り、次のようなケースでは注意が必要です。

    • モデルインスタンスに 外部サービスのレスポンスをキャッシュ している
    • memoization(@foo ||= heavy_calculation)で重い処理結果をインスタンス変数に保持している
    • テストで reload すれば「全部きれいになる」と思い込んでいる

    これらは reload ではリセットされないため、DBの状態とカスタムキャッシュの状態が食い違う 可能性があります。
    もし「リロード時にカスタム状態も消したい」場合は、after_find / after_initialize / 独自メソッドを使って明示的にリセットする必要があります。

  • 設計上の指針としての明文化
    今回のドキュメント更新により、「ARが責任を持つ範囲」と「アプリケーションが責任を持つ範囲」がよりはっきりしました。
    AR の reload はあくまで「DB由来の属性と、それに付随するAR内部状態」を対象とし、
    それ以外のインスタンス変数やオブジェクト状態のライフサイクル管理は、アプリケーション側の責任 であることが明確化されています。


  1. 参考情報 (あれば)
  • 該当PR: https://github.com/rails/rails/pull/58929
  • 関連ドキュメント(現行APIリファレンス):
    • ActiveRecord::Persistence#reload
      (PRマージ後は「属性値」「AR管理の内部状態」「カスタム状態」の扱いがより明確に記載される想定)

このPRを踏まえると、「reload すればモデルインスタンスが“完全に初期状態に戻る”わけではない」点を意識して、
外部キャッシュ・memoization・一時的なインスタンス変数の扱いを設計・テストするのが良いです。


#58869 Add test for OOB/in_order_of/CTE ​

マージ日: 2026/10/2 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    このPRは、Active Record の in_order_of と CTE (Common Table Expression) を組み合わせた場合に、Out-of-Band (OOB) ローディング周りの挙動が正しく動作することを確認するテストを追加するものです。機能追加や挙動変更はなく、既存機能の回 regressions 防止のためのテスト強化が目的です。

  1. 変更内容の詳細

追加されたテストファイル ​

  • 変更ファイル: 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行と短いテストで、おおよそ次のような構成が考えられます:

ruby
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 ローディングを組み合わせた時に指定順序通りに返ってくるか」を確認するタイプのテストであると考えられます。


  1. 影響範囲・注意点
  • 挙動変更なし
    このPRはテストコードのみの追加であり、実行時のライブラリコードには一切変更がないため、Rails を利用するアプリケーションの動作には影響しません。

  • 回 regressions 防止
    今後、in_order_of / OOB / CTE まわりの実装変更を行った際に、過去に発生した・あるいは懸念される不具合が再発しないことを自動で検証できるようになります。

  • OOB ローディング + CTE + in_order_of を使うアプリ
    これらを組み合わせている、あるいは将来的に組み合わせたいアプリにとっては、コア側でテストが整備されていることで信頼性が若干高まります。
    ただし、このPR単体では不具合修正ではない点に注意してください。もし過去に類似の問題があり、それが対応済みかどうかを確認したい場合は、参照されている Issue/PR (#44746, #57975) も合わせて確認する必要があります。


  1. 参考情報 (あれば)
  • 参照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. 概要 (1-2文で)
    ActiveSupport::EventReporter がサブスクライバに渡すイベントデータ(ハッシュ)を freeze することで、サブスクライバ間での副作用や順序依存を防ぐ変更です。
    これにより、イベントの内容を書き換えようとするサブスクライバは FrozenError となり、通知されるイベントの整合性が保証されます。

  1. 変更内容の詳細

背景問題 ​

ActiveSupport::EventReporter#notify は、1つのイベントハッシュをそのまま全サブスクライバに共有していました。
その結果:

  • あるサブスクライバが event[:payload][:foo] = :bar のようにイベントを書き換えると、その後に実行されるサブスクライバは書き換え後のデータを受け取ってしまう
  • パラメータフィルタリング等で削除された値を、後続のサブスクライバが「元に戻す」ことも理論上可能
  • 結果として、「どのデータが見えるか」が購読順序に依存してしまう

という問題がありました。

具体的な変更点 ​

このPRでは、notify 内で作られるイベントのトップレベル構造をすべて freeze するように変更しています。

対象:

  • イベントハッシュ自体
  • :payload ハッシュ
  • :source (source location を表すオブジェクト)
  • (元々)tags と context はすでに freeze 済み

方針:

  • Reporter 内で「自分で構築したコピー」に対してのみ freeze を行う
  • 呼び出し元が渡した payload ハッシュや、notify に直接渡されたイベントオブジェクトは freeze しない
    • つまり、呼び出し元のデータ構造の所有権を奪わない

イメージコード(擬似的):

ruby
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

エラー挙動 ​

  • サブスクライバ側で、例えば:

    ruby
    ActiveSupport::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 されることが明記されています。

  1. 影響範囲・注意点

影響範囲 ​

  • ActiveSupport::EventReporter を使ってイベント購読を行っているすべてのコード
    • 特に、ブロック/サブスクライバ内でイベントハッシュや payload を直接書き換えている場合

注意点 ​

  1. イベントオブジェクトは読み取り専用とみなす必要がある

    ruby
    reporter.subscribe("foo") do |event|
      # NG: 直接書き換え
      event[:payload][:foo] = "bar"   # FrozenError
    
      # OK: 読み取るだけ
      process(event[:payload][:foo])
    end
  2. freeze は浅い

    • payload 自体のハッシュは freeze されますが、payload 内の値(オブジェクト)は freeze されません。

    • 例えば:

      ruby
      reporter.notify("foo", payload: { data: [] })
      
      reporter.subscribe("foo") do |event|
        event[:payload][:data] << 1  # これは可能(配列自体は freeze されていない)
      end
    • 「サブスクライバに渡す前に、完全に不変にしたい」場合は、呼び出し元の側で深いコピーや deep freeze を行う必要があります。

  3. 既存コードの互換性

    • これまで「たまたま動いていた」イベントの書き換えコードは破壊的変更になります。
    • カスタムインストルメンテーションやモニタリングライブラリで:
      • イベントを加工して後続サブスクライバに渡す
      • イベントをそのまま保持して後で書き換える
    • といったパターンを使っている場合は、設計の見直しが必要です。
      • 例: イベントを複製してから加工する:

        ruby
        reporter.subscribe("foo") do |event|
          event_copy = event.deep_dup
          event_copy[:payload][:foo] = "bar"
          # 以降は event_copy を使う
        end
  4. パフォーマンス

    • 各サブスクライバごとにコピーを作らず、1イベントにつき1つのコピー+shallow freeze のみ。
    • EventReporter は比較的ホットパスになり得るため、deep freeze や subscriber ごとのコピーは避けていることが明示されています。

  1. 参考情報 (あれば)
  • 対応Issue: #58912
    「サブスクライバ間でイベントハッシュが共有されることによる副作用」の報告・議論に対する修正。
  • 変更対象:
    • activesupport/lib/active_support/event_reporter.rb
    • activesupport/test/event_reporter_test.rb
    • activesupport/CHANGELOG.md
  • 設計方針:
    • イベントの「観測結果」を安定させる(購読順序に依存させない)
    • 呼び出し元のデータ所有権は尊重する(元の payload などは凍結しない)
    • Hot path であることを考慮し、deep freeze・subscriber ごとのコピーは行わない

#58680 Cache ActiveSupport::ParameterFilter instances across requests ​

マージ日: 2026/10/2 | 作成者: @eregon

  1. 概要 (1-2文で)
    各リクエストごとに生成されていた ActiveSupport::ParameterFilter(内部で複数の Regexp を生成)が、同じフィルタ一覧であればグローバルに再利用されるようになり、ログ用パラメータフィルタの構築コストが削減されました。これにより特にフィルタ数が多いアプリケーションや config.precompile_filter_parameters を無効にしている環境で、リクエストあたりのオーバーヘッドが有意に減少します。

  1. 変更内容の詳細

何をしているか ​

  • 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 ありと同等の速度」になると説明されています。

  1. 影響範囲・注意点

影響範囲 ​

  • 影響する処理:

    • ログ出力時のパラメータフィルタリング(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 を新規に作る(内容が変われば当然キャッシュミスが増える)
  • フィルタを「リクエストスコープで変更・拡張する」ような高度な使い方をしている場合:
    • 引き続き動作はしますが、キャッシュ頻度が下がり、性能向上は限定的かもしれません。

  1. 参考情報 (あれば)
  • 過去の類似機能:

    • 2010年: ParameterFilter のコンパイル結果をキャッシュする機能が一度追加されていた(コミット 55ae903c3fd2)。
    • その後、スレッドセーフ化(45448a578877)を経て、2012年に「不要なキャッシュを削除」(fa3457dc3b30)というコミットで撤去されていたが、理由は明示されていなかった。
    • 今回の PR はこれをより明示的な設計とテストのもとで復活させた形。
  • プロファイル・計測の詳細:

    • 使用環境: 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. 概要 (1–2文で)
    before_commit コールバック内で TransactionRollbackError が発生した場合でも、トランザクションを正しくロールバックするように within_new_transaction の挙動が修正されました。これにより、トランザクションが開いたまま接続プールに戻ってしまう不具合が解消されます。

  1. 変更内容の詳細

問題の背景 ​

ActiveRecord::ConnectionAdapters::TransactionManager#within_new_transaction は、おおまかに次のような流れで処理をしています:

  1. トランザクションを開始
  2. ブロックを実行(モデルの保存やコールバック実行など)
  3. before_commit コールバック群 (before_commit_records) を実行
  4. DB に対して COMMIT を投げる (transaction.commit)
  5. 例外が起きた場合は ROLLBACK する … というのが本来の期待挙動

ところが既存コードでは、

  • within_new_transaction の中で TransactionRollbackError を rescue して「ROLLBACK せずに終了する」特別扱いが入っていた
  • この rescue 範囲に before_commit_records(COMMIT 実行前)が含まれていた

ために、

  • before_commit コールバック内で TransactionRollbackError が発生すると
    • 「COMMIT 失敗時を想定した特別な rescue」と同じ扱いになってしまう
    • まだ COMMIT 自体は発行されていないにもかかわらず、ROLLBACK をスキップ
    • 結果として「トランザクションが開いたまま」そのコネクションがプールに戻る

という不整合な状態が起きていました。

本PRでの修正方針 ​

修正のポイントは次の2つです。

  1. 「COMMIT 後の TransactionRollbackError だけ特別扱いする」ように rescue の位置を移動
  2. within_new_transaction レベルの TransactionRollbackError 専用 rescue を廃止し、before_commit 中の例外は通常のロールバック処理に通す

具体的には、activerecord/lib/active_record/connection_adapters/abstract/transaction.rb のトランザクション管理ロジックが変更されています。

元の構造(概略):

ruby
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

新しい構造のイメージ:

ruby
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 で例外を投げる
  • 期待結果
    • トランザクションはロールバックされていること
    • コネクションがトランザクション中のままプールに戻らないこと(後続クエリが通常通り動く)

これにより、今回のバグが再現していたケースが再度発生しないことを保証しています。


  1. 影響範囲・注意点

影響範囲 ​

  • 対象:
    • 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 の境界を意識して挙動を確認してください。

  1. 参考情報 (あれば)
  • 該当PR:
    • Roll back when a before_commit callback raises a rollback error (#58289)
  • 関連Issue:
    • #58288 — before_commit コールバック内での TransactionRollbackError により、トランザクションがロールバックされずに接続プールに戻ってしまう問題
  • 関連する過去のPR:
    • #56717 — COMMIT 自体がシリアライゼーションエラーなどで失敗した場合に、不要な ROLLBACK を回避する挙動を導入したPR(今回もこの挙動は維持されている)

アプリケーション側で before_commit コールバックや TransactionRollbackError を積極的に使っている場合は、テスト環境でトランザクションの挙動(特にネストしたトランザクションやエラー発生時の状態遷移)を一度確認しておくと安全です。


#58813 Include extra node state in Arel equality ​

マージ日: 2026/10/2 | 作成者: @e-akashsaini

  1. 概要 (1–2文で)
    Arel の一部ノード (Matches, DoesNotMatch, Regexp, NotRegexp, InfixOperation, UnaryOperation) について、SQL 出力に影響する追加状態(escape 文字や大文字小文字区別フラグ、演算子)を eql? / hash 判定に含めるようにし、異なる条件が where のマージや or 結合時に誤って同一視されて消えてしまう問題を修正する PR です。
    これにより、Relation#or / merge / WhereClause の重複排除が、より直感的かつ安全な挙動になります(Active Record が自前で生成する通常のクエリは影響なし)。

  1. 変更内容の詳細

問題の背景 ​

Arel の多くのノードクラスは Arel::Nodes::Binary / Unary を継承しており、デフォルトの eql? / hash は「クラス+オペランド」だけを比較対象にします。

しかし、以下のクラスは「クラス+オペランド以外の状態」が SQL に影響するにもかかわらず、その状態が eql? / hash に含まれていませんでした:

ノード無視されていた状態影響する SQL
Matches, DoesNotMatchescape, case_sensitiveESCAPE 句、LIKE vs ILIKE (PostgreSQL)
Regexp, NotRegexpcase_sensitive~ vs ~* (PostgreSQL)
InfixOperationoperator>, <, ->>, @> など任意の中置演算子
UnaryOperationoperator任意の prefix 演算子

その結果、異なる SQL を生成する 2 つのノードが eql? / hash 上は同一と見なされ、以下のような現象が起きます。

  • ActiveRecord::Relation::WhereClause は内部的に Array#- や Array#| を使って述語を重複排除しているため、一方の条件が捨てられる
  • Relation#or は self - other が空だと「同じ条件」とみなして idempotent ショートカットを使い、Or ノードを作らずに左辺の Relation をそのまま返す

具体例:

ruby
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 ​

ruby
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 ​

ruby
def hash
  super ^ [@case_sensitive].hash
end

def eql?(other)
  super &&
    self.case_sensitive == other.case_sensitive
end
alias :== :eql?
  • PostgreSQL の ~ / ~* を切り替える case_sensitive を考慮

InfixOperation ​

ruby
def hash
  super ^ [@operator].hash
end

def eql?(other)
  super &&
    self.operator == other.operator
end
alias :== :eql?
  • > と <、->> と @> など「演算子の違い」を equality に含める

UnaryOperation ​

ruby
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 のライブラリ変更がないと失敗します。


  1. 影響範囲・注意点
  • 影響するのは Arel ノードの equality (eql? / == / hash) を利用しているコード です
    • WhereClause 内部の重複排除
    • Relation#or のショートカット
    • ActiveSupport::Cache などで「Arel ノードを Hash のキーや Set の要素にしている」場合
  • 特に影響が出るケース:
    • Arel::Predications#matches, matches_regexp
      • escape や 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 が変わることはありません。


  1. 参考情報 (あれば)
  • バグは 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. 概要 (1-2文で)
    Ruby 3.3.5 未満向けのポリフィル(互換コード)を Active Record のコネクションプール/クエリキャッシュ周りから削除し、Ruby 3.3.5 以降を前提にした実装へ整理した PR です。これにより、Thread/Fiber 以外のキーを内部マップに使えるようになり、クエリキャッシュのクリア処理などで今後のパフォーマンス改善がしやすくなります。

  1. 変更内容の詳細

全体像 ​

  • 対象:
    • activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
    • activerecord/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 で導入された/安定した Map 系機能(例えば WeakMap 相当の機能や、内部 C 実装のマップ)を前提に、ポリフィル側のコードを削除。
    • その結果、マップのキーとして Thread/Fiber に限定されず、任意のオブジェクト(例えば ConnectionHandler など)を使えるようになります。

ざっくりとしたイメージ(実際のコードは異なりますが、構造の変化イメージ):

Before(旧 Ruby サポートのためのポリフィルあり)

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.new

After(Ruby 3.3.5 前提、ポリフィル削除)

ruby
# 常に Ruby 本体の新しい Map 実装を使用
@connections = RubyNativeMap.new

これにより、@connections のキーとして Thread/Fiber 以外も安全に使える前提が整います。

query_cache.rb の変更点 ​

  • こちらは差分 1 行程度の単純な修正で、上記の Map 利用方法の変更に追随したものと思われます。
    • 例えば、クエリキャッシュをスレッドローカルや Fiber ローカルに紐づけていた箇所を、より汎用的なキー(たとえばコネクションプールインスタンスや別の ID)で管理できるようにするための準備的変更です。
  • 結果として、クエリキャッシュの「どのキーで」「どの単位で」キャッシュを持つか、という戦略を柔軟に変更できるようになります。

  1. 影響範囲・注意点

影響範囲 ​

  • 対象コンポーネント:
    • Active Record のコネクションプール (ConnectionPool)
    • クエリキャッシュ (QueryCache)
  • 実運用上の主な影響:
    • この 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 ベースのキャッシュ管理に深く依存した独自実装がある場合は、該当箇所を再確認した方がよいです。

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    ActionDispatch::IntegrationTest の fixture_paths が二重に登録されてしまう問題を修正し、同じフィクスチャを何度もロードし直してしまう非効率さを解消する PR です。これにより、特にフィクスチャ数が多いアプリケーションで、テスト全体の実行時間が大きく短縮されます。

  1. 変更内容の詳細

問題の背景 ​

rails/test_help を require した Rails アプリで、以下のような状態になっていました。

ruby
ActiveSupport::TestCase.fixture_paths
# => ["/app/test/fixtures/"]

ActionDispatch::IntegrationTest.fixture_paths
# 本来は上記と同じ配列を継承していれば十分だが…
# => ["/app/test/fixtures/", "/app/test/fixtures/"]  # 同じパスが2回入る

原因は以下の2点です。

  1. ActionDispatch::IntegrationTest は ActiveSupport::TestCase を継承しており、fixture_paths も継承される
  2. さらに 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 をそのまま連結するのではなく「重複しないように追加する」ように変更しています。

イメージとしては以下のような修正です(擬似コード):

ruby
# 変更前(イメージ)
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 が親クラスと同じパスを二重に持たないこと

を検証しています。この新しいテストは、この修正を入れないと失敗するようになっています。


  1. 影響範囲・注意点

影響範囲 ​

  • 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 で整える形にして、挙動を揃えています。

  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/58919
  • 関連コミット:
    • 17cb852d72 — フィクスチャキャッシュのミス時にキャッシュをリセットする変更
    • 08d4e20ce3 / PR #58454 — ActiveSupport::TestCase の fixture_paths 重複を解消した変更
  • 修正ファイル:
    • railties/lib/rails/test_help.rb
    • railties/test/application/integration_test_case_test.rb

実運用上は、Rails をこの PR を含むバージョンに上げるだけで、統合テストと通常テスト間でフィクスチャキャッシュが共有されるようになり、大規模なフィクスチャを使うプロジェクトではテスト時間短縮が期待できます。


#58917 Remove dead code for Ruby < 3.3.1 ​

マージ日: 2026/10/1 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    Rubyのサポート下限バージョンが 3.3.1 になったことを前提に、Time 計算まわりで「Ruby 3.3.1 未満向けのワークアラウンド(死んだコード)」を削除したPRです。テストは 3.3.1 以降の動作確認として有用なため、そのまま維持されています。

  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)」になっていたため削除。

コードイメージ(あくまで概念的な例):

ruby
# 以前(イメージ)
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 以降で期待する動作が正しく行われているか」を保証する意味があるため、今回も残されています。


  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 以上に上げるのが正しい対応になります。

  1. 参考情報 (あれば)

#58908 Bump minimum Ruby version to 3.3.5 ​

マージ日: 2026/10/1 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    Rails がサポートする最低 Ruby バージョンを 3.3.5 に引き上げる PR です。
    これにより、Weak{Key,}Map の use-after-free バグを前提にした独自ポリフィルを廃止でき、今後 Active Record のクエリキャッシュ周りをより効率的にリファクタリングできるようにする意図があります。

  1. 変更内容の詳細

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 とガイドのバージョン宣言です。

代表的な変更例 (概念的なイメージ):

ruby
# 例: 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 です。


  1. 影響範囲・注意点

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 の拡張・プラグインを書いているような層に限られると考えられます

  1. 参考情報 (あれば)
  • 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. 概要 (1-2文で)
    Rails の公式ガイドドキュメント内に含まれていた誤字・軽微な文法ミスを修正する PR です。コードや挙動への影響はなく、ドキュメントの可読性と正確さを高める目的の変更です。

  1. 変更内容の詳細

修正対象はすべて 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.

純粋に読みやすさの改善のみです。

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.

  1. 影響範囲・注意点
  • 影響範囲

    • 対象はガイドドキュメント (guides/source/*.md) のみであり、Rails 本体のソースコードには一切変更がありません。
    • 挙動・API・設定仕様などは全く変わらないため、アプリケーション側での対応や検証は不要です。
    • ドキュメントを参照している記事・資料で、誤字をそのまま引用していた場合は、今後差異が生じる可能性はありますが、実害はありません。
  • 注意点

    • CI スキップ指定 ([ci skip]) がタイトルに入っている通り、テストは実行されていませんが、コード変更がないため問題にはなりません。
    • ドキュメントの diff を追う際、「挙動の変更かどうか」を確認するときには、この PR は完全に見た目・文章だけの修正であると理解しておけば十分です。

  1. 参考情報 (あれば)

この PR から分かるように、Rails では誤字修正のような小さなものでも独立した PR として歓迎されており、ドキュメント品質向上のためのコントリビュートの一例と捉えることができます。


#58898 Avoid reprocessing ActiveRecord::Result column names ​

マージ日: 2026/10/1 | 作成者: @dersam

  1. 概要 (1-2文で)
    ActiveRecord::Result が内部で「カラム名の再処理(主に String の intern / freeze)」を行うのをやめ、DBアダプタ側での処理に一元化したパフォーマンス改善のPRです。
    これにより、不要な文字列オブジェクトの割り当てを避けつつ、カラム名配列は引き続き freeze される形になります。

  1. 変更内容の詳細

何をしているか ​

  • 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 each version」
    → Result 生成時に Ruby 側で each を回しながら intern / freeze などをしていた分のオブジェクト割り当てを削減
  • 「We can now remove this and only freeze the array」
    → ActiveRecord 本体では、配列の不変性だけを保証し、要素(カラム名文字列)の処理は行わない方針

コード上のイメージ ​

実際の差分は1行追加・3行削除程度ですが、イメージとしては:

ruby
# 変更前(イメージ)
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 側で列名をあれこれ加工しない」方向への簡略化です。


  1. 影響範囲・注意点
  • 対象:

    • ActiveRecord を通じて DB クエリを発行し、その結果を ActiveRecord::Result として扱う全機能(低レベル API を含む)
  • 実質的な振る舞いの変更:

    • 結果としてのカラム名が frozen である点は維持される想定
    • 文字列の intern など、メモリ・パフォーマンスに関わる処理がアダプタ側に完全移譲される
  • 開発者が意識すべき点:

    • 通常の AR モデル利用 (User.select(...) など) では、API 的な挙動は変わらないはずで、明示的な対応は不要
    • 自前でアダプタを拡張・実装している場合:
      • カラム名の文字列処理(intern, freeze など)をアダプタ側で責任を持って行う必要がある
      • それをしていないと、Result のカラム名が想定より多くメモリを消費したり、GC 負荷が上がる可能性がある
    • 低レベルで ActiveRecord::Result を直接生成しているコードがあり、その中で「Result 側でカラム名を加工してくれる前提」に依存していた場合は挙動が変わる(ただし、そのような利用はかなりニッチ)
  • 後方互換性のリスク:

    • 公式にサポートされる通常の利用パターンでは、仕様変更というよりは実装の最適化に近く、互換性リスクは低い
    • ただし、カラム名オブジェクトの「同一性(object_id)」までテストしているようなコードがあると、intern のタイミング変更によってテストが不安定になる可能性は理論上ある

  1. 参考情報 (あれば)
  • 関連 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. 概要 (1-2文で)
    collection_radio_buttons / collection_check_boxes などで、値が空文字列 "" のときに生成される <input> の id と <label> の for が一致しなくなるバグを修正した PRです。nil の場合の修正 (#57209) を拡張し、「サニタイズ結果が空文字列になる値」全般に対して一貫した ID 付与になるようにしています。

  1. 変更内容の詳細

何が問題だったか ​

次のようなコードを考えます:

ruby
collection_radio_buttons :user, :active,
  [["Yes", true], ["Undefined", ""]],
  :last, :first

期待されるのは、value: "" の要素に対して、input と label が同じ ID を参照することです。

しかし従来は次のような HTML が生成されていました:

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_button
  • check_box(multiple: true の場合)
  • label(value: を指定した場合)

そのため、コレクション系だけでなく、単体呼び出しでも挙動が変わります。

例:

ruby
radio_button :user, :active, ""
label :user, :active, value: ""

従来:

html
<input type="radio" value="" name="user[active]" id="user_active_" />
<label for="user_active">...</label> <!-- あるいは for="user_active_" など不一致の可能性 -->

修正後:

html
<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 が一致することを確認

  1. 影響範囲・注意点

影響範囲 ​

  1. collection_radio_buttons / collection_check_boxes において、コレクションの値が空文字列 "" の要素
    → id / for の末尾から不要な _ が消え、user_active_ → user_active のように変わります。

  2. 次のような単体ヘルパ呼び出しでも同様に ID が変わります:

    • radio_button :user, :active, ""
    • check_box :user, :roles, { multiple: true }, ""
    • label :user, :active, value: ""
  3. これにより:

    • スクリーンリーダーやクリックターゲットの挙動が改善(label と input の紐付きが正しくなる)
    • CSS / JS が #user_active_ のような古い ID を前提にしている場合は壊れる可能性がある

注意点 ​

  • 破壊的変更の範囲は「ID 名が変わる可能性がある」に限定されますが、DOM を直接フックしているコード(テストを含む)があると影響を受けます。
  • 既に #57209 を適用済みの Rails 8.1 系で、空文字列値を使っているフォームを持つアプリは、この PR で ID が「より一貫した形」に揃えられます。
    → そのぶん不具合修正としての価値は高い一方、DOM 名の変更としては認識しておく必要があります。
  • heartcombo/simple_form などフォームビルダーを利用している場合、今回の修正はその上で動くヘルパにも波及します(根本の ID 生成ロジックが変わるため)。

  1. 参考情報 (あれば)

#58882 Fix the :validate default in the Association Basics guide [ci skip] ​

マージ日: 2026/10/1 | 作成者: @ramen2020

  1. 概要 (1-2文で)
    Association Basics ガイドの:validateオプションのデフォルト値説明が実装と食い違っていたため、関連の種類ごとに正しいデフォルト挙動が分かるように修正したドキュメント更新PRです。あわせて、has_and_belongs_to_many は :validate をサポートしないという誤った記述も削除されています。

  1. 変更内容の詳細(あればサンプルコードも含めて)

何が問題だったか ​

元の Association Basics ガイドでは:

  • :validate のデフォルトは常に false
  • has_and_belongs_to_many は :validate オプションをサポートしない

と読める記述になっていました。

しかし実際の実装 (ActiveRecord::Reflection#validate?) は以下のようになっています。

ruby
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 はデフォルト true
    • collection? == 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: falsetrue (保存成功)
has_and_belongs_to_many (デフォルト)false (保存失敗)
has_and_belongs_to_many validate: falsetrue (保存成功)
has_one (デフォルト)true (保存成功)
has_one autosave: truefalse (保存失敗)
belongs_to (デフォルト)true (保存成功)
belongs_to autosave: truefalse (保存失敗)

この挙動に合うようにガイドの文章が4行追加、5行削除で調整されています。

サンプルコードイメージ ​

例えば:

ruby
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
end
  • author.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 (保存失敗)

今回はこのような実際の挙動と整合するようにドキュメントだけが修正されています。


  1. 影響範囲・注意点
  • 影響範囲は ドキュメントのみ であり、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?)を参照するほうが確実です。

  1. 参考情報 (あれば)

#58883 Fix range_field_tag and time_field_tag examples in the API docs [ci skip] ​

マージ日: 2026/10/1 | 作成者: @ramen2020

  1. 概要 (1-2文で)
    FormTagHelper の API ドキュメント中で、range_field_tag / time_field_tag / datetime_field_tag のサンプルコードが実際の出力と食い違っていた問題を修正した PR です。実装の挙動は変えず、「ドキュメント上の例が正しく動く/正しく説明している状態」に揃えています。

  1. 変更内容の詳細

2-1. range_field_tag の例の修正 ​

range_field_tag のシグネチャは以下の通りです。

ruby
def range_field_tag(name, value = nil, options = {})

元のドキュメント例では:

ruby
range_field_tag 'quantity', in: 1...10

のように書かれており、この呼び出しは Ruby 的には

ruby
name   = 'quantity'
value  = { in: 1...10 }  # 第2引数にオプションハッシュが入ってしまう
options = {}

となります。そのため、実際の出力は:

html
<input type="range" name="quantity" id="quantity" value-in="&quot;1...10&quot;" />

のように、in: オプションが value-in という謎属性として出力されていました(本来期待している min="1" max="9" にはならない)。

これを修正するため、PR では例を次のように変更しています。

ruby
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 ドキュメントには、以下のような説明と例がありました(イメージ):

ruby
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: オプションを特別扱いしていません。そのため、実際の動作は:

ruby
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 対応を追加せず「ドキュメントを実装に合わせる」方向にしたのは、「挙動の変更ではなくドキュメントの修正に留める」という方針によるものです。


  1. 影響範囲・注意点
  • この PR は コードの挙動自体は一切変更しておらず、ドキュメント(コメント)のみを修正 しています。
    • 変更ファイル: actionview/lib/action_view/helpers/form_tag_helper.rb
    • 追加 3 行 / 削除 8 行(実装コードではなくコメント行)

開発者視点での注意点:

  1. range_field_tag の使い方

    • オプション (in:, min:, max:, step: など) を渡す場合、第2引数の value を省略すると Hash が value と解釈される ことに注意してください。
    • ドキュメント同様、value を省略したい場合は以下のように nil を入れるのが安全です。
      ruby
      range_field_tag :score, nil, min: 0, max: 100, step: 5
    • これは number_field_tag など他の *_field_tag にも共通するパターンです。
  2. 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 による挙動変更もない ため、動作が変わることはありません。ただし「ドキュメントで見た挙動」を期待していた場合は、フォームビルダーヘルパーへの移行を検討すべきです。

  1. 参考情報 (あれば)
  • 実際の出力比較(PR 説明より):

    修正前(main の挙動):

    ruby
    range_field_tag("quantity", in: 1...10)
    # => <input type="range" name="quantity" id="quantity" value-in="&quot;1...10&quot;" />
    
    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" />

    修正後のドキュメント例:

    ruby
    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" />
  • 関連する設計上のポイント:

    • *_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. 概要 (1-2文で) Rails のジェネレータ用テストが、テストごとに一時ディレクトリを使うようになり、tmp/generators 共有による並列実行時のファイル競合・テストのフレークが解消されました。これに伴い、テスト用の出力ディレクトリの削除タイミングが setup から teardown に変更されています。

  1. 変更内容の詳細

背景にある問題 ​

従来のジェネレータテストは:

  • すべて tmp/generators を destination_root として使用
  • prepare_destination が「各テストの setup で destination_root を削除 → 再作成」

という動きをしていました。

この設計だと:

  1. 並列テスト実行時の競合

    • テストAとテストBが同じ tmp/generators を使う
    • テストAの setup で tmp/generators を削除
    • その直後にテストBが同じディレクトリを前提に動こうとして失敗
    • → テストがフレークする(ときどき落ちる)
  2. 同じパスに同名ファイルを生成するテスト同士の競合

    • 異なるテストケースでも同じ 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 と小さな差分ですが、責務の位置づけ(いつ・どこで消すか)が変わっています。


  1. 影響範囲・注意点
  • 影響範囲

    • Rails 本体の ジェネレータテスト基盤(Rails::Generators::TestCase, Rails::Generators::Testing::Behavior)を使っているテストに影響します。
    • rails g generator で生成される 新規のジェネレータ用テストは、この新しい一時ディレクトリ方式を前提としたコードになります。
  • メリット

    • 並列実行時のジェネレータテストが安定する(フレークの低減)
    • テスト同士が同じファイルパスを使っても安全
    • テスト実行後に中間ファイルがきれいにクリーンアップされやすい(各テスト専用ディレクトリの削除)
  • 注意点 / 互換性

    • もし独自に destination_root を 固定パスに上書きしているテストがある場合、そのテストが新しいライフサイクル(setup / teardown)とどう干渉するかを確認する必要があります。
    • テスト中に destination_root を前提としたパスをハードコードしている場合(例: Rails.root.join("tmp/generators/...") を前提にしているなど)、一時ディレクトリ方式に合うよう見直した方がよい可能性があります。

  1. 参考情報 (あれば)

#58893 Remove support for dynamic controller/action in routes ​

マージ日: 2026/9/30 | 作成者: @tenderlove

  1. 概要 (1–2文で)
    このPRでは、Railsのルーティングから「URLパラメータから :controller や :action を動的に読み取る」仕組みが完全に削除されました。Rails 5.0 から非推奨だった get ":controller(/:action(/:id))" 形式などが今後はエラーとなり、コントローラとアクションを明示的に指定する書き方へ移行する必要があります。

  1. 変更内容の詳細

2-1. 動的 :controller / :action ルーティングの完全削除 ​

以下のような「URLの一部をそのままコントローラ名・アクション名として解決する」ルート定義は、もはや利用できず ArgumentError を投げるようになりました。

ruby
# これまでは動いていたが、今後は ArgumentError
get ":controller(/:action(/:id))"
get ":controller/:action"

これらは Rails 5.0 から非推奨 (deprecation) になっていて、「今まで警告は出ていたが動いていたもの」が、今回のPRで実際に削除されています。

推奨される書き換え ​

従来:

ruby
get ":controller(/:action(/:id))"
# /photos -> PhotosController#index
# /photos/1 -> PhotosController#show (慣習で)

今後:

ruby
get "photos",     to: "photos#index"
get "photos/:id", to: "photos#show"
  • どの URL がどのコントローラ#アクションに対応するかをルート定義側で明示するのが前提になります。
  • 暗黙的にアクション名を推測したり、URLの文字列をそのままクラスやメソッドにマッピングする仕組みは消えます。

2-2. コントローラクラスを直接渡す書き方の変更 ​

従来は、to: にコントローラクラスだけを渡すと、「動的アクション解決」と同じ仕組みに依存してアクションが補完される動作がありました。

ruby
# 以前: 暗黙に #show などを解決していたケースがある
get "show", to: PhotosController

この挙動は、内部的には「動的な controller/action 解決」と同じパスに乗っていたため、今回の削除に巻き込まれています。

今後はアクションを明示的に指定する必要があります:

ruby
get "show", to: PhotosController, action: "show"
# または
get "show", to: "photos#show"

2-3. :controller / :action に対する Regexp の扱い変更 ​

:controller や :action に Regexp 制約を付けていたようなルートは、もはや マッチし得ないルートとして即座にエラーを投げるようになりました。

例:

ruby
# これまでは「どうせマッチしないルート」が定義できてしまっていたケース
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 → コントローラの流れをより単純・明示的にする変更になっています。


  1. 影響範囲・注意点

3-1. 既存アプリのルーティングエラー ​

以下に当てはまるアプリは、Rails をこのバージョンに上げた瞬間から 起動時または最初のリクエスト時に ArgumentError となる可能性が高いです。

  • config/routes.rb に次のようなエントリが残っている:
ruby
get ":controller(/:action(/:id))"
get ":controller/:action/:id"
match ":controller(/:action(/:id))", via: :all
  • resources を使わず、上記のような「catch-all」なルーティングに頼っている古いアプリ。

対応方針:

  1. 該当パターンを全削除またはコメントアウト。

  2. 実際に必要なエンドポイントごとに、明示的なルートを定義し直す:

    ruby
    # NG (削除対象)
    get ":controller(/:action(/:id))"
    
    # OK (明示的に書く)
    resources :photos  # index/show/create/... が生成される
    get "dashboard", to: "admin/dashboard#index"
  3. 既存の URL が多く、後方互換を保ちたい場合は、

    • resources + member/collection ルート
    • scope / namespace を活用してパターンをまとめる などで段階的に整理していくのが現実的です。

3-2. コントローラクラスを to: に渡しているルート ​

以下のようなコードがある場合:

ruby
get "show", to: PhotosController

今後は必ず action: を付与してください:

ruby
get "show", to: PhotosController, action: "show"
# もしくは
get "show", to: "photos#show"

to: SomeController を使うパターンはそれ自体あまり一般的ではありませんが、エンジンや internal DSL で使っている場合は注意が必要です。

3-3. メタプログラミング的なルーティング生成 ​

以下のような「ループで :controller を動的に入れ替えつつルートを量産する」ようなメタプログラミングをしていた場合も、根本的な見直しが必要です。

ruby
# 例: 動的 controller 名をパラメータで受けていたようなケース
%w[photos posts].each do |name|
  get "#{name}/:action", controller: name
end
  • URL の一部をそのままクラス名等にマッピングする発想自体が危険・非推奨とみなされており、今回のPRで明確に締め出されています。
  • 必要なら resources :photos, resources :posts などに展開し、アクションを列挙する形が推奨されます。

  1. 参考情報
  • この機能は 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 などを使った「明示的ルーティング」の考え方が整理されています。
  • 変更内容は actionpack/CHANGELOG.md にも記載されており、「動的コントローラ/アクションルートの削除」が明示的な breaking change として扱われています。

このPRの本質は、「暗黙的で危険な自動マッピング」をやめて、コントローラ/アクションはルートで明示するという Rails の現在の設計方針をより徹底するものと理解すると良いです。


#58907 Document minimum Ruby version for 8.2 ​

マージ日: 2026/9/30 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    Rails 8.2 で必要となる「最低 Ruby バージョン」を、公式アップグレードガイドに明示したドキュメント修正の PR です。コード変更はなく、ガイドラインの追記のみが行われています。

  2. 変更内容の詳細

  • 変更ファイル: guides/source/upgrading_ruby_on_rails.md
  • 行追加: 1行(削除なし)

内容としては、「Rails 8.2 にアップグレードする際に要求される Ruby の最低バージョン」をアップグレードガイドに追記しています。
実際の文面は PR 情報からは読み取れませんが、典型的には以下のような形で記載されます:

md
Rails 8.2 requires Ruby X.Y.Z or newer.

もしくは、既存の「バージョンごとの必要 Ruby バージョン一覧」の表や箇条書きに、Rails 8.2 分が1行追加されている可能性が高いです。

コード例が変わったわけではなく、「この Rails バージョンで使ってよい Ruby の範囲」を明示するためのドキュメント更新です。

  1. 影響範囲・注意点
  • 実行時挙動・API には一切変更がなく、ドキュメントのみの変更です。
  • ただし、この記述により以下が明確になります:
    • Rails 8.2 にアップグレードする前に、使用中の Ruby が最低バージョンを満たしているかを確認する必要がある。
    • CI の Ruby バージョンマトリックスや .ruby-version, Gemfile の ruby 宣言などを、ドキュメントの要件に合わせて更新すべきかどうか判断しやすくなる。
  • 「まだ Rails 8.2 の最低 Ruby バージョンを誤って低く想定していた」プロジェクトでは、実際のアップグレード作業前に Ruby 自体のアップグレード計画が必要になる点に注意してください。
  1. 参考情報 (あれば)
  • 実際の最低 Ruby バージョンは、以下のいずれかを確認するのが確実です:

これらを確認して、Gemfile 等に:

ruby
ruby "X.Y.Z"

のように明示することで、チーム全体で Rails 8.2 対応の Ruby バージョンを統一できます。


#58905 Lazy call_arg inspection for attribute methods ​

マージ日: 2026/9/30 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    ActiveModel::AttributeMethods 内で、属性メソッド生成時の call_args の inspect 呼び出しを「必要になった時だけ実行する」ように変更し、無駄な String オブジェクトの生成を減らすパフォーマンス改善です。モデル × カラム × 属性メソッドごとに発生していた余計な文字列割り当てが、ほとんどのケースで発生しなくなります。

  1. 変更内容の詳細

これまでの動作 ​

ActiveModel::AttributeMethods は、未定義メソッド呼び出し時などに、call_args を元にエラーメッセージやデバッグ情報を組み立てることがあります。その過程で、次のような形で引数を一括で inspect 化していました:

ruby
call_args.map!(&:inspect)

この処理は、

  • すべての call_args について
  • まだ使われるかどうか分からない段階で
  • 文字列(#inspect の結果)を生成し、配列を書き換える

という性質があります。

PR の説明にある通り、その後のフローでは「カラム × 属性メソッド」の組み合わせのうち、最初の 1 回しか call_args の文字列表現を実際には使っていなかったにもかかわらず、モデル × カラム × 属性メソッドごとに常に inspect 付きの文字列を生成していました。

この PR での変更 ​

call_args.map!(&:inspect) を「呼び出しの早い段階で一括実行」するのではなく、実際に inspect された引数が必要になる箇所まで処理を遅延(lazy)させた、というのがこの PR の本質です。

イメージとしては、従来:

ruby
# 以前: 早い段階で必ず全引数を inspect していた
call_args.map!(&:inspect)
# 以降、エラーメッセージなどで call_args を使用

となっていたところを、変更後は:

ruby
# 必要になるまで inspect しない
# ...
if need_inspected_args
  inspected_call_args = call_args.map!(&:inspect)
  # inspected_call_args を使う処理
end

といった形に近づけた、という変更です(実際のコードは多少異なる可能性がありますが、意図としてはこのような「使う直前で初めて inspect する」形にリファクタリングされています)。

これにより、

  • 多くのケースでは inspect 自体が呼ばれない
  • したがって、不要な String オブジェクトの割り当てが行われない

という挙動になります。


  1. 影響範囲・注意点

影響範囲 ​

  • 対象: ActiveModel::AttributeMethods を利用している全てのモデル(ActiveRecord::Base を継承した通常の Rails モデルを含む)
  • 実際の影響: 主に内部のパフォーマンス改善であり、アプリ側のコード変更は不要

挙動の変化の可能性 ​

通常の利用において、外部に見える挙動(API)は変わらないようになっています。注意すべき点は次のような場合です:

  • call_args を inspect する前提で、AttributeMethods 内部の非公開 API に依存している(メタプログラミングや Monkey Patch など)
    • そのようなコードは本来非推奨ですが、この PR により、「常に call_args が文字列(inspect 済み)である」という前提は崩れます。
  • call_args の内容や型を内部フックで覗き見している場合
    • 一部のタイミングで、以前は String だったものが、元のオブジェクト(Symbol, Numeric, など)のままになっている可能性があります。

標準的な Rails アプリケーションで、公式にサポートされている API だけを使用している限りは、動作上の違いを感じることはなく、パフォーマンスがわずかに向上するだけと考えてよいです。


  1. 参考情報 (あれば)
  • 該当 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. 概要 (1-2文で)
    Rails の JSON エンコード処理において、特殊文字のエスケープを String#gsub! ベースから String#tr!(ハッシュ対応版)を用いた実装へ最適化し、大きめの JSON ドキュメントで大きなパフォーマンス向上を図った PR です。エンコードが不正な場合は従来どおり gsub! にフォールバックするため、互換性を保ちつつ高速化しています。

  1. 変更内容の詳細

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 の説明中にあるマッピングは概ね次のようなものです:

ruby
ESCAPES = {
  "\u2028" => '\u2028',
  "\u2029" => '\u2029',
  ">"      => '\u003e',
  "<"      => '\u003c',
  "&"      => '\u0026',
}

Rails 側でも同様のマッピングを JSON エンコード時に使用し、それを tr! に渡します。

2-3. 有効なエンコーディングのみ tr! を使う ​

String#tr! は「不正なエンコーディングの文字列」に対しては例外を上げる仕様です。そのため、Rails 側では:

ruby
def escape_with_tr(json)
  return escape_with_gsub(json) unless json.valid_encoding?
  json.tr!(ESCAPES)
  json
end

のように、

  1. valid_encoding? が true なら tr! を使用(高速パス)
  2. false なら従来どおり gsub! による処理(フォールバック)

という二段構成になっています。
これにより、既存のアプリで不正なエンコーディングを含む文字列が紛れ込んでいたケースでも、今までと同じ挙動(gsub! ベース)で動作し、例外で落ちることを避けています。

2-4. パフォーマンス特性 ​

PR で提示されているベンチマーク結果(Ruby 4.1 dev, arm64, Darwin)は次の通りです。

  • 小さい JSON(48 bytes, エスケープ対象なし)の場合

    • gsub!: 約 6.18M i/s
    • tr!: 約 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! 対応版に更新されています。

  1. 影響範囲・注意点

3-1. 影響範囲 ​

  • 影響を受けるのは「Rails で JSON を生成する全てのコード」です。
    例:

    • render json: object
    • object.to_json(Rails の ActiveSupport 拡張経由)
    • ActiveSupport::JSON.encode(object)
  • 特に HTML に埋め込む JSON(<script> タグ内の初期データなど)でのエスケープ処理はこの変更の直接的な恩恵を受けます。

3-2. 互換性と振る舞いの変化 ​

基本的には出力される JSON の内容は変わらない(バイト列レベルで一致するよう、テスト & ベンチで確認済み)ことが前提で実装されています。PR 中でも以下のように検査しています:

ruby
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! の方が有利」と判断されています。
    • 必要であればアプリケーションレベルで独自エンコーダを実装し直すことで、局所的にチューニングする余地はあります。

  1. 参考情報 (あれば)
  • Ruby 本体の PR:

  • Rails の変更ファイル(この PR):

    • activesupport/CHANGELOG.md
      • 今回の JSON エンコード最適化に関する変更履歴を追加。
    • activesupport/lib/active_support/json/encoding.rb
      • JSON エンコード時のエスケープ実装を gsub! 中心から tr! 中心へ変更。
    • activesupport/test/json/encoding_test.rb
      • 新しい実装に対するテスト追加。gsub! と tr! パスの出力が一致することなどを検証。

この PR による挙動差分は基本的に「性能のみ」であり、JSON のフォーマットやエスケープ結果は互換になるよう配慮されています。開発者としては、特別な対応なしにそのまま性能改善の恩恵を受けられる変更と考えて問題ありません。