Skip to content

Ruby on Rails PR Digest - 2026年 7月

このページは rails/rails リポジトリにマージされたPull Requestを自動的に収集し、AIで要約したものです。

#58311 Fix connection.explain(arel) for Arel input with AST binds

マージ日: 2026/7/30 | 作成者: @kamipo

  1. 概要 (1-2文で)
    connection.explain(arel) に Arel(AST binds 付き)を渡した場合に、バインド変数が無視されて EXPLAIN が失敗していた問題を修正した PR です。to_sql ではなく to_sql_and_binds を使うことで、EXPLAIN 実行時にプレースホルダへ正しくバインド値が渡されるようになりました。

  1. 変更内容の詳細

何が問題だったか

Arel オブジェクトを connection.explain(arel) に渡すと、内部的に次のような流れで処理されていました:

  1. Arel を to_sql で SQL 文字列にコンパイル
  2. 生成された SQL を EXPLAIN ... して実行

しかし to_sql は「抽出したバインド値(binds)」を返さず、SQL 文字列だけを返します。そのため、PostgreSQL / SQLite3 / MySQL の explain メソッドの実装では、以下のような「プレースホルダだけあるが値のない」状態で EXPLAIN を実行していました:

sql
EXPLAIN SELECT "cars".* FROM "cars" WHERE "cars"."name" = $1
-- しかし $1 に対応する bind が無い

結果として PostgreSQL などでは:

text
ActiveRecord::StatementInvalid: PG::UndefinedParameter: ERROR:  there is no parameter $1

のようなエラーが発生していました。

何を変えたか

PostgreSQL / SQLite3 / MySQL の各アダプタの explain 実装で、Arel を扱う部分を to_sql から to_sql_and_binds に変更し、抽出したバインド値を select_all に渡すように修正しています。

概念的には、次のような変更です(実際のコードはアダプタごとに少しずつ違いますが、意図は共通):

修正前(イメージ):

ruby
def explain(arel, binds = [])
  sql = to_sql(arel)        # SQL だけ生成、binds は捨てられる
  sql = "EXPLAIN #{sql}"
  select_all(sql)           # binds を渡さずに実行
end

修正後(イメージ):

ruby
def explain(arel, binds = [])
  sql, arel_binds = to_sql_and_binds(arel) # SQL と binds を両方取得
  all_binds = binds + arel_binds           # 既存の binds とマージ(実装依存)
  sql = "EXPLAIN #{sql}"
  select_all(sql, nil, all_binds)          # binds を渡して実行
end

これにより、Arel 内で管理されている AST のバインド変数(AST binds)が EXPLAIN 実行時にも正しく利用されます。

テストの追加

activerecord/test/cases/explain_test.rb にテストが 6 行追加されています。
内容としては、「AST binds を持つ Arel を explain したときにエラーにならず、正しく EXPLAIN が実行できること」を検証するテストが追加されていると考えられます(行数からすると 1 ケース程度の小さな追加)。


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

    • Arel オブジェクトを直接 connection.explain(arel) に渡しているコード
    • 特に、where 句などにバインド変数(プレースホルダ)を含んだ Arel を EXPLAIN したいケース
  • 対象 DB:

    • PostgreSQL, SQLite3, MySQL の ActiveRecord アダプタ(database_statements.rb への変更)
  • 影響を受けないもの:

    • 生の SQL 文字列を connection.explain("SELECT ...") で渡しているコード
      • これは「現在通り exec_explain 経由のパス」であり、バインド処理がもともと正しく行われているため、挙動は変わりません。
  • 実務上の意味:

    • 「Arel ベースでクエリを組み立て、explain で実行計画を見る」といったユースケースで、バインド付き条件を含めてもエラーにならず、実際のクエリと同じ形で EXPLAIN を確認できるようになりました。
    • これまでは、PostgreSQL 等で「parameter $1 が無い」というエラーに悩まされていたケースが解消されます。

  1. 参考情報 (あれば)
  • 関連 API:
    • ActiveRecord::Relation#explain
    • ActiveRecord::ConnectionAdapters::DatabaseStatements#explain
    • ActiveRecord::ConnectionAdapters::DatabaseStatements#to_sql_and_binds
  • この PR の趣旨は、Rails 内部の「Arel → SQL + binds」変換を EXPLAIN 実行にも一貫して適用し、Arel ベースのクエリデバッグ・チューニングをしやすくすることです。

#58310 Deprecate passing binds to to_sql

マージ日: 2026/7/30 | 作成者: @kamipo

  1. 概要 (1-2文で)
    Rails 5.2 以降、SQL バインド値は Arel AST 側で管理されており、to_sqlbinds 引数を渡しても実質使われていませんでした。この PR では to_sql(binds) の利用を非推奨 (deprecate) にし、将来削除するための警告を追加しています。

  1. 変更内容の詳細

背景

  • Rails 5.2 で to_sql_and_binds の実装が変わり、バインド値は AST (Arel::Nodes) に保持されるようになったため、to_sql / to_sql_and_binds は外部から渡された binds を SQL 文字列の構築には使わなくなっています。
  • それにもかかわらず、to_sql メソッドシグネチャには binds 引数が残っており、歴史的経緯からドキュメント上は「公開 API」とみなされているため、即削除ではなく非推奨プロセスを踏む形になりました。

主なコード変更ポイント

  1. to_sqlbinds 引数を非推奨に

activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb にて、to_sql の引数 binds を使った呼び出しに deprecation 警告が出るようになっています。

概念的には以下のようなイメージです(実際のコードはもう少し汎用的です):

ruby
def to_sql(arel_or_sql_string, binds = [])
  if binds.present?
    ActiveSupport::Deprecation.warn(
      "Passing `binds` to `to_sql` is deprecated and will be removed in a future version. " \
      "Bind parameters now live on the Arel AST."
    )
  end

  # 実際の SQL 生成は binds ではなく arel_or_sql_string 側の情報を使う
  ...
end
  1. 各アダプタ (mysql, postgresql, sqlite3) の to_sql シグネチャ修正
  • mysql/database_statements.rb
  • postgresql/database_statements.rb
  • sqlite3/database_statements.rb

で、それぞれのアダプタ固有の to_sql 実装も、抽象クラスのシグネチャ変更に揃える形で軽微な修正が入っています(binds を受け取るが実際には利用しない・警告だけ出す、など)。

  1. テスト追加

activerecord/test/cases/database_statements_test.rb に、to_sqlbinds を渡したときに deprecation が発生することを検証するテストが追加されています。

  1. CHANGELOG 追記

activerecord/CHANGELOG.md に、本変更(to_sqlbinds を渡すのは非推奨)のエントリが追加されています。


  1. 影響範囲・注意点

影響を受けるコード

  • 以下のようなコードを書いている場合に影響があります:
ruby
# 例: 既存のコード
arel = User.where(name: :name).arel
sql  = ActiveRecord::Base.connection.to_sql(arel, [[nil, "bob"]])

このように to_sql(arel, binds) の形式で呼んでいると、今後以下のような deprecation 警告が出るようになります。

  • to_sqlbinds を渡すのは非推奨で、将来のバージョンで削除予定」という趣旨のメッセージ。

今後推奨される書き方

  • to_sql を使う場合は、binds を渡さず SQL 文字列の生成のみを行う メソッドとして扱う必要があります。
ruby
arel = User.where(name: "bob").arel
sql  = ActiveRecord::Base.connection.to_sql(arel)
  • もし「SQL とバインド値をセットで扱いたい」用途であれば、バインド値は Arel AST 側から取り出すか、クエリ生成段階で別途管理する必要があります(この PR では具体的な代替 API の追加は行っていないため、既存の Arel / AR API をどう組み合わせるかは各アプリケーション側の設計次第です)。

移行時の注意点

  • 現時点では 動作そのものは変わっていません。もともと binds は SQL 生成には使われておらず、今回の PR で「それを明示的に非推奨にした」だけです。
  • ただし、将来のメジャーバージョンで to_sql(binds) は削除される可能性が高いため、今のうちに以下を行うべきです:
    • to_sql への第 2 引数を削除する
    • to_sqlbinds を渡すことで SQL 文字列に反映される」という前提のロジックがないか確認する

  1. 参考情報 (あれば)
  • この PR で触れられている過去コミット:
    • Rails 5.2 での変更(to_sql_and_bindsbinds に依存しなくなった):
      rails/rails@213796fb49
    • to_sql が内部ユーティリティとして抽出されたときのコミット:
      rails/rails@7db90aa7c7
  • 該当 PR:
    https://github.com/rails/rails/pull/58310

#58309 Rename arel to arel_or_sql

マージ日: 2026/7/30 | 作成者: @kamipo

  1. 概要 (1-2文で)
    このPRは、ActiveRecordの低レベルクエリメソッドの引数名を「arel」から「arel_or_sql」にリネームし、Arelオブジェクトと生SQL文字列の両方を受け取ることが分かるようにしたものです。機能的な挙動は変えずに、引数名の一貫性と可読性を向上させています。

  1. 変更内容の詳細

対象メソッド

ActiveRecordの DatabaseStatements 周りで、以下のようなメソッドの引数名が変更されています。

  • insert
  • update
  • delete
  • explain
  • to_sql
  • to_sql_and_binds

従来:

  • 多くのメソッドは引数名に arel を使っており、
  • to_sql / to_sql_and_binds だけが arel_or_sql_string という名前を使っていました。

今回:

  • すべて arel_or_sql に統一
  • 内部ヘルパー primary_key_for_insert / table_ref_for_insert で既に使われていた命名規則に合わせています。
  • update_with_result は Arel 専用のため、引き続き引数名 arel のままです。

典型的なシグネチャ変更イメージ

※ 実際のコードは概ね以下のような変化です(イメージ・擬似コード):

ruby
# 変更前
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
  ...
end

def to_sql_and_binds(arel_or_sql_string, binds)
  ...
end

# 変更後
def insert(arel_or_sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
  ...
end

def to_sql_and_binds(arel_or_sql, binds)
  ...
end

このように「引数名だけ」が変わっており、実装ロジックや受け取れる型(Arel manager または String)が変わったわけではありません。


  1. 影響範囲・注意点

影響が出る可能性があるのは、主に以下のケースです:

  1. メソッドシグネチャをオーバーライドしているアダプタ/アプリケーションコード

    • 例: 独自の DB アダプタや、ActiveRecord::ConnectionAdapters::*Adapter のサブクラスで insert, update, delete, explain, to_sql, to_sql_and_binds をオーバーライドしている場合。
    • Rubyでは引数名そのものは呼び出し側には影響しませんが、以下の場合は注意:
      • super 呼び出しでキーワード引数を使っている
      • メタプログラミングで method.parameters などを参照して引数名に依存している
    • 通常の位置引数で super しているだけなら、実行時挙動は変わりません。
  2. メソッドの引数名をそのまま利用しているドキュメント/コメント/テスト

    • 例: 「insert(arel, ...) を呼ぶ」といった形で引数名を前提にした解説や spec を書いている場合は、表現を arel_or_sql に追随させるとよいです。
  3. Arel 限定メソッド update_with_result

    • 引数名は arel のままです。
    • ここには生SQL文字列を渡すことはサポートされない、という意図がより明確になります。
    • もし update_with_result に文字列を渡していた場合は本来非想定利用なので、そのままでも挙動は(Ruby的には)変わりませんが、今後の互換性のためにも Arel を渡す形に直した方が安全です。

実際のアプリケーションコードへの影響

通常のアプリケーションレイヤーではこれらのメソッドを直接呼ぶことは少ないため、多くのアプリでは実質的な影響はありません。

影響が出やすいのは以下のようなレイヤーです:

  • オレオレアダプタ/DBごとのパッチ、Monkey Patch
  • gemレベルで ActiveRecord の DatabaseStatements を拡張しているコード
  • トレーシング/ロギング/計測のために、これらのメソッドをラップ/差し替えしているコード

そういったコードで、特に「引数名に依存するリフレクション」をしていないかだけ確認しておくと安心です。


  1. 参考情報 (あれば)
  • 対象ファイル:
    • activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
    • activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
    • activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
    • activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb
  • 命名の基準:
    • primary_key_for_insert / table_ref_for_insert など、内部ヘルパーメソッドで既に arel_or_sql が使われており、それに揃える形でのリファクタリングです。
  • 機能変更ではなく、インターフェースの「名前の一貫性向上」のためのリネームという位置付けです。

#58296 Refactor insert returning: handling to take QueryIntent directly

マージ日: 2026/7/30 | 作成者: @kamipo

  1. 概要 (1-2文で)
    INSERT ... RETURNING の処理を、生のSQL文字列操作ではなく QueryIntent を直接扱う形にリファクタリングしたPRです。これにより Arel ベースの INSERT は AST レベルで RETURNING を付与できるようになり、アダプタ間での拡張性・一貫性が向上しています。

  1. 変更内容の詳細

2-1. 旧実装: sql_for_insert(sql, binds, returning) の問題点

これまで INSERT 時の returning: オプションは sql_for_insert(sql, binds, returning) を経由して処理されていましたが、以下の問題がありました。

  • すべての INSERT が生SQL文字列ベースの処理に引きずり込まれていた
    • Arel で構築したクエリも、いったん SQL 文字列にしてから RETURNING ... を付け足す形だった
  • binds 引数が事実上 Oracle enhanced adapter 専用で、他のアダプタには不要
  • AST レベルでの RETURNING 操作ができず、update_with_result との一貫性もなかった

2-2. 新実装: apply_returning_to!(intent, returning)

新しく apply_returning_to!(intent, returning) が導入され、QueryIntent を直接受け取るようになりました。

  • 引数:
    • intent: QueryIntent (INSERT 操作の意図を表すオブジェクト)
    • returning: returning: オプションで指定された値(または省略時は後述のデフォルト)

役割:

  • QueryIntent から、Arel ノードか生SQLかなど、必要な情報をアダプタ側で取り出せる
  • Arel の INSERT の場合:
    • arel.returning(...) を呼んで AST に RETURNING を設定
    • update_with_result と同じパターンで RETURNING を扱えるようになる
  • 生SQLの INSERT の場合:
    • 従来同様、"INSERT ... RETURNING <cols>" という文字列連結で対応

これにより、Arel ベースのクエリは「SQL生成 → 文字列操作 → 再パース」といった往復をせず、AST レベルで完結して RETURNING を付与できるようになりました。

2-3. returning: 省略時のデフォルト挙動の明示化

returning: が呼び出し元で省略された場合のデフォルトが整理されています。

  • デフォルトは「テーブルの主キー (primary key)」
  • ただし 複合主キーはスキップRETURNING で「単一の last-inserted id」と対応しないため)
  • このテーブル情報の取得処理は後述の table_ref_for_insert に切り出し

概念的には次のような動きです(イメージ):

ruby
def apply_returning_to!(intent, returning)
  table = table_ref_for_insert(intent)

  returning ||= begin
    pk = table.primary_key
    pk if pk && !Array(pk).many? # 複合PKの場合は nil 扱い
  end

  # returning があれば intent に付与 (Arel or SQL)
end

※実際のコードは多少異なる可能性がありますが、ロジックとしてはこういった内容です。

2-4. table_ref_for_insert ヘルパーの導入

INSERT 対象テーブルを特定する共通処理として、table_ref_for_insert が抽出されました。

  • 共通化された責務:
    • INSERT 先テーブル名(もしくはテーブルオブジェクト)の取得
    • そこから primary key 情報を得る
  • これにより、apply_returning_to! 内でテーブル単位のメタ情報を参照しやすくなり、アダプタ実装側も簡潔にできます。

2-5. PostgreSQL の suppress_composite_primary_key の削除

PostgreSQL adapter にあった suppress_composite_primary_key が削除されています。

  • これまで:
    • 複合主キーのときに RETURNING を付けないようにするロジックを PostgreSQL 側で個別に持っていた
  • これから:
    • apply_returning_to! 側で「複合主キーは returning のデフォルトから除外」という共通ロジックを持つため、
    • PostgreSQL アダプタ固有の suppress_composite_primary_key は不要になり削除

これにより、「複合PK時の RETURNING 非付与」という方針が アダプタ共通の扱い になります。

2-6. Trilogy adapter の小変更

Trilogy adapter 側の database_statements.rb も 1 行追加 / 3 行削除といった小さな変更が入っています。

  • 目的としては他アダプタと同じく、新しい apply_returning_to! / QueryIntent ベースの経路に合わせる調整
  • 従来の sql_for_insert 前提コードの削除と思われます

  1. 影響範囲・注意点

3-1. 開発者視点 (Active Record 利用者)

基本的には挙動互換を保ったリファクタリングであり、以下の点以外は意識せずに利用できます。

  • returning: を指定している場合
    • 明示的に指定している returning: の挙動自体は変わらない想定
    • Arel ベースのクエリであっても、内部的に AST で RETURNING が管理されるようになっただけ
  • returning: を省略している場合
    • これまでも「主キーを返す」前提の挙動だったはずですが、
    • 今回、複合主キーの場合はデフォルトで RETURNING を付けないことが明確化され、実装に反映
    • もし独自アダプタやカスタムコードで「複合主キーでも何かしら RETURNING を付ける」前提がある場合は注意

3-2. Adapter / ライブラリ開発者視点

特に影響があるのは ActiveRecord アダプタやプラグインを書く側です。

  • sql_for_insert(sql, binds, returning) に依存していた場合:
    • 代わりに QueryIntent ベースのフックに追随する必要があります
    • binds が必要だったのは Oracle enhanced adapter のみだったので、多くのアダプタではコードがシンプルになるはず
  • returning の扱い:
    • apply_returning_to!(intent, returning) 経由で統一されたため、Arel / SQL どちらの場合もここを起点にすべき
    • Arel の場合は arel.returning(...) を使う方向に寄せると、AST レベルでの最適化や一貫性が保ちやすい
  • 複合主キー:
    • デフォルト挙動としては「複合PKは returning: 省略時の対象から除外」
    • どうしても複合PKで RETURNING を使いたい場合は returning: を明示的に指定する運用が必要になる可能性があります

3-3. パフォーマンス・保守性

  • Arel クエリが SQL 文字列を介さずに RETURNING を付与できるようになったことで:
    • 余計な文字列操作・再パースが不要になり、わずかですがパフォーマンス改善が期待できる
    • クエリ生成の経路がシンプルになり、デバッグ・保守がしやすくなる

  1. 参考情報 (あれば)
  • 類似実装: update_with_result
    • 今回の変更は「UPDATE + RETURNING を AST レベルで扱う」update_with_result のパターンを、INSERT にも適用したものと位置づけられます。
  • 関連しそうなコード:
    • activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
    • 各種アダプタの database_statements.rb(PostgreSQL, Trilogy, Oracle enhanced など)
  • QueryIntent 自体の仕様や使い方を把握しておくと、今後の ActiveRecord のクエリ周りの変更にも追随しやすくなります。

#58299 Remove dead binds handling in AR log subscribers

マージ日: 2026/7/30 | 作成者: @kamipo

  1. 概要 (1-2文で)
    Active Record のログ関連コードから、既に使われなくなったバインド値処理用のヘルパーメソッドとレガシー形式のバインド値サポートが削除されました。Rails 7.0 で実質廃止されていた旧形式の binds([[column, value], ...])へのフォールバックも StructuredEventSubscriber から完全に取り除かれています。

  1. 変更内容の詳細

背景

  • #55900 以降、ActiveRecord::LogSubscriber#sqlStructuredEventSubscriber が事前に整形した payload[:binds] をそのまま .inspect で出力するだけになっている。
  • そのため、LogSubscriber 側でバインド値をレンダリング・型キャスト・フィルタリングしていたヘルパーは実質使われていなかった。
  • さらに、StructuredEventSubscriber には Rails 7.0 以前のレガシーな binds 形式([[column, value], ...])を扱うフォールバックが残っていたが、Rails 7.0 でその形式自体が削除されており(コミット 2d821ef0c5)、こちらも不要になっていた。

active_record/log_subscriber.rb の変更

以下のようなメソッド群が削除されています(合計 23 行削除、追加なし):

  • render_bind
  • type_casted_binds
  • filter

これらは主に次のような責務を持っていました:

  • render_bind: 1つの bind(バインド値)をログ出力用の文字列に整形
    • 例: カラム名や値、型情報を含めた表現への変換
  • type_casted_binds: 各 bind を DB への送信に近い形に型キャストした配列に変換
  • filter: 機密情報(パスワードなど)をフィルタリングするための処理

しかし現在の LogSubscriber#sql は、payload[:binds] を直接 inspect してログ出力する実装になっており、これらのメソッドはどこからも呼ばれていない「死んだコード」でした。そのため、不要なコードとして削除されています。

イメージ (擬似コード)

ruby
# 以前のイメージ (概念的):
def sql(event)
  binds = type_casted_binds(event.payload[:binds])
  filtered_binds = filter(binds)
  debug "  SQL (#{event.duration}ms)  #{event.payload[:sql]}  #{render_bind(filtered_binds)}"
end

# 現在のイメージ:
def sql(event)
  # StructuredEventSubscriber が整形した binds をそのまま使う
  binds = event.payload[:binds]
  debug "  SQL (#{event.duration}ms)  #{event.payload[:sql]}  #{binds.inspect}"
end

これにより、LogSubscriber 側のバインド処理ヘルパーは完全に不要となった、という整理です。

structured_event_subscriber.rb の変更

StructuredEventSubscriber では、バインド値の表現を新しい構造化形式(ActiveRecord::Relation::QueryAttribute 等)に一本化し、古い形式へのフォールバックを削除しています(+2/-13)。

主な変更点:

  • render_bind 内の「Array だった場合」の処理分岐を削除
    • 旧形式: [[column, value], ...] のような配列形式を想定した when Array ブランチ
  • attribute_name を取得する際に、Array を扱う並列の分岐も削除

擬似的には次のようなイメージの変更です:

ruby
# 以前のイメージ (概念的):
def render_bind(bind)
  case bind
  when Array
    column, value = bind
    # 旧形式 [[column, value], ...] の処理
  else
    # 新形式 QueryAttribute の処理
  end
end

def attribute_name(bind)
  case bind
  when Array
    column, _ = bind
    column.name
  else
    bind.name # 新形式
  end
end

# 現在:
def render_bind(bind)
  # 新形式のみを前提とした処理
end

def attribute_name(bind)
  # 新形式のみを前提とした処理
end

つまり、StructuredEventSubscriber は「新しい構造化された binds 形式のみを扱う」実装に整理され、旧来の Array ベースの binds には一切対応しない形になりました。


  1. 影響範囲・注意点

影響範囲

  • Rails 本体内部

    • LogSubscriberStructuredEventSubscriber 間のやり取りが前提としている binds 形式が、完全に「新形式のみ」になった。
    • 内部的なコードクリーンアップであり、通常のアプリケーションコードには直接の影響は基本的にありません。
  • メタプログラミング/モンキーパッチをしている場合
    下記のようなケースでは影響がありえます:

    • ActiveRecord::LogSubscriberrender_bind / type_casted_binds / filter メソッドを直接呼んでいた、あるいはオーバーライドしていた
    • StructuredEventSubscriber の binds 取り扱いが Array 形式も受け付けることを前提に独自の拡張をしていた

    これらは今回削除・単純化されているため、Rails のアップデート時に NoMethodError や挙動の変化が起こる可能性があります。

注意点

  • 旧形式の binds を渡すコードはサポートされない

    • Rails 7.0 でそもそも非対応になっているため、アプリ側で [[column, value], ...] のようなレガシー binds を内部 API に渡している場合は、この PR を機に完全に壊れます。
    • そのようなコードが残っている場合は、ActiveRecord::Relation::QueryAttribute 等の新しい構造化形式に合わせて見直す必要があります。
  • ログ出力カスタマイズの際の前提変更

    • SQL ログのバインド値は、StructuredEventSubscriber が整形した payload[:binds] をそのまま .inspect したもの、という前提がより強くなっています。
    • バインド値の表現をカスタムしたい場合は、LogSubscriber ではなく StructuredEventSubscriber 側の処理をフック/拡張する方が筋がよい構造になっています。

  1. 参考情報 (あれば)
  • この PR: Remove dead binds handling in AR log subscribers (#58299)
  • 関連 PR:
    • #55900: LogSubscriber#sqlpayload[:binds].inspect に一本化された変更
  • 関連コミット:
    • 2d821ef0c5: Rails 7.0 でレガシーな binds 形式([[column, value], ...])が削除されたコミット

この PR は主に「内部実装の整理・デッドコード削除」であり、Rails のログ周り内部 API に直接依存していない通常のアプリケーション開発者にとっては、動作上の変化はほぼありません。ただし、ログのバインド処理を深くいじっている場合は、アップデート時に該当箇所を確認することを推奨します。


#58298 Make ActiveModel#model_name accessible from a Ractor:

マージ日: 2026/7/29 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    ActiveModel#model_name が Ractor 内から安全に利用できるようにするため、model_name が返すオブジェクトを「凍結(freeze)」しつつ、内部で使う値を事前に計算・設定する変更です。これにより Ractor 環境でも model_name.human などのメソッド呼び出しが行えるようになります。

  1. 変更内容の詳細

背景・問題点

Ruby 3 以降の Ractor では、「共有されるオブジェクト」は以下のような制約があります:

  • 共有されるオブジェクトは原則として不変(frozen)である必要がある
  • 共有可能でないオブジェクトを Ractor 間で渡そうとするとエラーになる

従来の ActiveModel::Naming#model_name は、返すオブジェクト (ActiveModel::Name インスタンス) が内部でメモ化や遅延評価を行うため、生成後に状態が変化し得る つくりになっていました。
そのため、この model_name オブジェクトを別 Ractor に渡して利用することができませんでした。

この PR は、

  • model_name が返すオブジェクトを Ractor 共有可能にする
  • かつ、そのオブジェクト上で #human などを安全に呼べるようにする

ために、生成時に必要な値をすべて確定させてから freeze する という方針を取っています。

コードレベルでの変更ポイント(概要)

対象ファイル: activemodel/lib/active_model/naming.rb

主な変更点:

  1. model_name が返す ActiveModel::Name インスタンスに対して:

    • 内部で利用する属性(例: name, singular, plural, element, human など)の値を、生成時に eager に計算・セット する
    • その後、そのインスタンスを freeze することで Ractor 共有可能にする
  2. 遅延評価/メモ化に依存していた部分を、
    「初回生成時にまとめて計算して、その後不変にする」
    という設計に寄せている

具体的なコードは PR 本体を見ないと完全には書けませんが、イメージとしては以下のような対応です(擬似コード):

ruby
def model_name
  @model_name ||= begin
    name = ActiveModel::Name.new(self, nil, self.name)
    # ここで内部の必要な値をすべて評価しておく
    name.name
    name.singular
    name.plural
    name.element
    name.human

    name.freeze  # Ractor 共有可能にする
  end
end

これにより、model_name を他の Ractor に渡しても、その後に内部状態が変化することはなく、Ractor が要求する「不変性」を満たせます。

テストの変更 (activemodel/test/cases/naming_test.rb)

テストは以下の点を確認する内容になっています:

  1. 新しいモデルクラスをその場で定義

    • 既存テストで使われているモデルクラスだと、すでに model_name のメモ化が走っている可能性があるため、
      「まっさらな状態」からの挙動を検証するために、新規クラスを定義してテストしています。
  2. I18n を stub している理由

    • I18n ライブラリ自体が現状 Ractor セーフではないため、
      テスト内で I18n の呼び出しが実際に走ると Ractor 関連のエラーでテストが落ちます。
    • そこで、model_name.human などで内部的に呼ばれる I18n を stub し、
      「ActiveModel::Name のオブジェクト自体が Ractor 共有可能かどうか」だけをテスト対象にしています。

テストのイメージ(擬似コード):

ruby
Class.new(ActiveModel::Naming) do
  # モデルクラス定義
end

I18n.stub(:t, "stubbed") do
  r = Ractor.new(model_class) do |klass|
    klass.model_name.human # Ractor 内から呼び出せるか
  end
  r.take
end

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

    • ActiveModel#model_name が返すオブジェクト (ActiveModel::Name) の内部実装が、
      「遅延評価で必要になったときに値を計算する」
      から
      「生成時に一括で計算して freeze する」
      に変わっています。
    • ほとんどの通常利用(User.model_name.singular, model_name.human 等)では挙動は変わらないはずです。
  • パフォーマンス面

    • これまで「使われなかったかもしれない属性」も含めて、model_name 生成時にすべて計算されるようになるため、初回呼び出し時にわずかなオーバーヘッドが増える可能性があります。
    • 一方で、後続の呼び出しではメモ化済みの frozen オブジェクトをそのまま使うため、大きな退行は想定しづらいです。
  • Ractor を使う場合のポイント

    • model_class.model_name をメイン Ractor で生成して別 Ractor に渡す、
      あるいは別 Ractor の中で model_class.model_name を呼ぶ、といったユースケースで安全に動作するようになります。
    • ただし、model_name の内部で利用する I18n 側が未だ Ractor セーフではないため、
      実運用で Ractor 内から model_name.human などを多用する場合は、I18n の利用パターンや stub/カスタム実装などを検討する必要があります。
  • 互換性 / 破壊的変更の可能性

    • ActiveModel::Name を継承/モンキーパッチしていて、「生成後にインスタンスの可変な状態に依存している」ようなコードがある場合は影響が出る可能性があります。
    • 通常の Rails アプリ(model_name をそのまま使っているだけ)であれば互換性問題はほぼ発生しないはずです。

  1. 参考情報 (あれば)

この変更は、「Ractor 対応を進めるための ActiveModel 側の一手」であり、Rails 全体を完全に Ractor セーフにするものではありませんが、マルチ Ractor 環境でのモデルクラス利用に向けた基盤整備と位置づけられます。


#58304 Fix releaser_test/rubocop

マージ日: 2026/7/29 | 作成者: @skipkayhil

  1. 概要 (1-2文で)
    Rails のリリース用テスト (tools/releaser/test/releaser_test.rb) において、環境変数 GITHUB_ACTIONS の扱いを修正し、CI(GitHub Actions)上でもローカル同様に安定してテストできるようにした PR です。Active Support テストにある with_env 実装をコピーして利用することで、環境変数を書き換えるテストの挙動を正しく制御しています。

  1. 変更内容の詳細

何を直したか

  • releaser のテストの中で GITHUB_ACTIONS 環境変数に依存したコードパスをテストしているが、
    • GITHUB_ACTIONS=true の分岐は「テスト内で明示的に ENV を設定」していたため問題なし
    • 一方で GITHUB_ACTIONS=false の分岐は、「CI 上で既に GITHUB_ACTIONS=true がセットされている」状況を考慮しておらず、GitHub Actions 上でテストすると期待通りに false 側の分岐を通れない
      という問題があったため、その分岐も明示的に環境変数を制御するように修正しています。

with_env の導入

PR 説明にある通り、Active Support のテストで使われている with_env ヘルパをそのままコピーして利用しています。イメージとしては次のような実装です(実際のコードを要約した擬似コード):

ruby
def with_env(env_overrides)
  old_env = ENV.to_hash
  begin
    env_overrides.each do |key, value|
      if value.nil?
        ENV.delete(key)
      else
        ENV[key] = value
      end
    end
    yield
  ensure
    # テスト終了後に必ず元の ENV を復元
    ENV.replace(old_env)
  end
end

この with_env を使うことで、テストごとに一時的に GITHUB_ACTIONStrue / false / 未定義 に変更し、ブロックを抜けたタイミングで必ず元の状態へ戻します。

テストの修正イメージ

  • GITHUB_ACTIONS=true のケースだけでなく、GITHUB_ACTIONS=false(もしくは未設定)のケースについても、with_env("GITHUB_ACTIONS" => "false") のようにして明示的に環境を作るようになっています。
  • これにより、
    • ローカル環境(通常 GITHUB_ACTIONS 未設定)
    • GitHub Actions 上(GITHUB_ACTIONS=true がデフォルト) どちらでも同じ条件でテストが実行されるようになります。

  1. 影響範囲・注意点
  • 影響範囲は tools/releaser/test/releaser_test.rb のテストコードのみで、本番コードには影響しません。
  • テストで環境変数を操作する際に、Active Support 同様の with_env パターンを使うことで、副作用(ENV の汚染)が残らないようになっています。
  • 今後 releaser のテストを追加・変更する際は、この with_env を使って
    • CI 上でデフォルト設定されている GITHUB_ACTIONS
    • その他の CI 固有の環境変数 を上書きするのが安全なパターンになります。
  • 既存のテストが ENV["GITHUB_ACTIONS"] に暗黙に依存している場合、今後他のテストでも同様の問題が起きうるため、必要に応じて同じヘルパを利用することが推奨されます。

  1. 参考情報 (あれば)
  • この PR 内のコミット:
  • GitHub Actions が自動的にセットする環境変数:
    • GITHUB_ACTIONS=true(公式ドキュメントより)
  • 類似パターン: Active Support のテストヘルパ with_env(ActiveSupport テストスイート内の実装を転用)

#58226 Deprecate TransactionState predicates and fix stale ivar in Persistence::ClassMethods#inherited

マージ日: 2026/7/29 | 作成者: @55728

  1. 概要 (1-2文で)
    Active Record の TransactionState が持つ一部の述語メソッドと nullify! が非推奨(deprecate)になり、Persistence::ClassMethods#inherited で使っていたクエリ制約用インスタンス変数のリセット先が正しい名前に修正されました。いずれも挙動変更は最小限で、ドキュメント公開された API の整理と内部実装の整合性をとるための変更です。

  1. 変更内容の詳細

2-1. TransactionState のメソッドの非推奨化

対象クラス:
ActiveRecord::ConnectionAdapters::TransactionState

非推奨になったメソッド:

  • #fully_committed?
  • #fully_rolledback?
  • #fully_completed?#completed? のエイリアス)
  • #nullify!

背景:

  • これらのメソッドはすでに内部からの呼び出し元がすべて削除されており(コミット 77f7b2df3a, 8180c39610, 6c745b0c51)、実質的に未使用になっていた。
  • ただし TransactionState はドキュメント公開された API であり、「ドキュメントに載っているメソッドを黙って削除する」のは互換性ポリシー違反になるため、削除ではなく deprecate とした。
  • fully_*? 系で確認していた状態は、引き続き以下のメソッドから観測可能:
    • #committed?
    • #rolledback?
    • #completed?fully_completed? の元になっているメソッド)

実装的には:

  • 各メソッド定義内で ActiveSupport::Deprecation.warn などにより警告を出すようになっている(テストが追加されていることからも明らか)。
  • CHANGELOG に非推奨化のエントリが追加されている。

利用側の推奨置き換え:

ruby
# 旧 (非推奨)
state.fully_committed?
state.fully_rolledback?
state.fully_completed?
state.nullify!

# 新 (推奨)
state.committed?
state.rolledback?
state.completed?
# nullify! 相当の用途がある場合は、呼び出し箇所の意図に応じて要見直し
# (内部実装依存のコードで使っていた場合はそもそもサポート外に近い)

ポイント:

  • fully_completed?completed? の単なる別名だったため、completed? を直接使えば OK。
  • 「fully〜」という名前が付いていたものの、実質的に expose している状態は committed? / rolledback? / completed? と同じであり、多重ネーミングになっていたものを整理する意図がある。

2-2. Persistence::ClassMethods#inherited における ivar リセットの修正

対象メソッド:
ActiveRecord::Persistence::ClassMethods#inherited

修正内容:

  • クラス継承時に実行される inherited フック内で、クエリ制約関連のインスタンス変数をリセットしていた箇所が、古い名前のままになっていたのを修正。

変更前(概念的なイメージ):

ruby
def inherited(subclass)
  super
  subclass.instance_variable_set(:@_query_constraints_list, nil)
end

変更後:

ruby
def inherited(subclass)
  super
  subclass.instance_variable_set(:@query_constraints_list, nil)
end

背景:

  • 過去のコミット 767cd52e76 で、メモ化に使用するインスタンス変数名が @_query_constraints_list から @query_constraints_list に変更されていた。
  • しかし inherited 内のリセット処理だけが古い名前のまま(@_query_constraints_list)だったため、「事前に定義しておく ivar」と、「メモ化で実際に使われる ivar」の名前がずれていた。
  • この ivar はパフォーマンス最適化(オブジェクトの shape を揃える、ヒープへの割り当てパターンを固定するなど)のために「事前にインスタンス変数を生やしておく」目的で使われており、実質的に挙動は変えていない。
  • 新しい ivar 名にあわせて reset 先も @query_constraints_list に修正することで、意図通りに一貫した shape が確保される。

挙動について:

  • ロジック的には、実行されるクエリや返り値が変わるわけではない。
  • ただし Ruby の VM 観点では、インスタンス変数の shape が安定するため、微妙なパフォーマンスやメモリレイアウトに良い影響が出る可能性がある(Rails の内部実装的な配慮)。

  1. 影響範囲・注意点

3-1. TransactionState を直接利用しているコード

影響が出るケース:

  • アプリケーションや gem が ActiveRecord::ConnectionAdapters::TransactionState を直接参照し、以下のメソッドを呼び出している場合:
    • #fully_committed?
    • #fully_rolledback?
    • #fully_completed?
    • #nullify!

影響内容:

  • Rails 実行時に deprecation warning が出力されるようになる。
  • 現時点では挙動は変わらないが、将来のメジャーバージョンで削除される可能性が高い。

対処:

  • ログやテスト実行時の出力から deprecation warning を確認する。
  • 上記の旧メソッド呼び出しを、以下のように置き換える:
    • fully_committed?committed?
    • fully_rolledback?rolledback?
    • fully_completed?completed?
    • nullify! → 用途に応じて設計見直し(多くの場合、内部実装に依存しすぎているので別のアプローチを検討)

nullify! を使っていた場合の注意:

  • トランザクション状態オブジェクトを「外から書き換える」ようなコードは、基本的に内部実装への強い依存であることが多く、Rails 本体の進化と整合しなくなるリスクが高い。
  • 可能であれば、トランザクション境界を適切に構成し直す、あるいはコールバックやフックポイントの使い方を見直し、「状態オブジェクトを直接いじらない」設計に改めるのが望ましい。

3-2. inherited の ivar 修正に関する影響

通常のアプリケーションにおいては:

  • ActiveRecord::Base を継承するモデルクラスを定義しているだけであれば、現実的な挙動変化はほぼない。
  • @query_constraints_list / @_query_constraints_list を直接参照・操作しているアプリコードや gem があれば、それ自体が内部実装依存でありサポート外寄りだが、今回の修正でより一貫した挙動になる。

性能面:

  • インスタンス変数の shape が継承クラスで事前に定義されるようになることで、軽微なパフォーマンス改善が期待できる程度で、大きなブレイクはない。

  1. 参考情報 (あれば)
  • 当該 PR:
    • 「Deprecate TransactionState predicates and fix stale ivar in Persistence::ClassMethods#inherited
    • PR #58226 / merged at 2026-07-29
  • 関連 PR / コミット:
    • フォローアップ元 PR: #58217
    • TransactionState の最後の利用箇所を削除したコミット:
      • 77f7b2df3a
      • 8180c39610
      • 6c745b0c51
    • @_query_constraints_list@query_constraints_list へのリネームを行ったコミット:
      • 767cd52e76
  • 変更されたファイル:
    • activerecord/CHANGELOG.md(非推奨追加)
    • activerecord/lib/active_record/connection_adapters/abstract/transaction.rb(deprecation 実装)
    • activerecord/lib/active_record/persistence.rb(ivar 名修正)
    • activerecord/test/cases/transactions_test.rb(deprecation テスト追加)

#58295 Return a Float from Time#- with a DateTime

マージ日: 2026/7/29 | 作成者: @55728

  1. 概要 (1-2文で)
    Time#-DateTime を渡したときの戻り値を Rational ではなく Float に統一する変更です。これにより、Time#-(Time)ActiveSupport::TimeWithZone#-(DateTime) と同じく秒数の差分が Float で返り、扱いやすくなります。

  1. 変更内容の詳細

これまでの挙動

Time - DateTime の結果が Rational になっていました:

ruby
elapsed = Time.utc(2000, 1, 2) - DateTime.civil(2000, 1, 1)
elapsed       #=> (86400/1)
elapsed.class #=> Rational

"#{elapsed} seconds" #=> "86400/1 seconds"
elapsed.to_json      #=> "\"86400/1\""
elapsed.round(2)     #=> 86400/1  # Rational のまま

一方で、以下は Float を返します:

ruby
Time.utc(2000, 1, 2) - Time.utc(2000, 1, 1)
#=> 86400.0 (Float)

Time.zone.parse("2000-01-02") - DateTime.civil(2000, 1, 1)
#=> 86400.0 (Float, ActiveSupport::TimeWithZone)

Rails 6.1 付近のコミット (4d24a25acf) で Time - DateTime 計算の精度を高めるために Rational を使うようになった影響で、戻り値の型だけが他と不整合な状態になっていました。

今回の修正内容

  • activesupport/lib/active_support/core_ext/time/calculations.rb

    • Time#-DateTime を引数に取る場合、内部ではこれまで通り Rational を用いて厳密に計算します。
    • 計算の最後に to_f して Float を返すように変更されています。

    擬似コードで表すと、イメージは以下のような変更です:

    ruby
    # 変更前(イメージ)
    def -(other)
      if other.is_a?(DateTime)
        rational_diff = ... # Rational で厳密に差を計算
        rational_diff      # => Rational をそのまま返していた
      end
    end
    
    # 変更後(イメージ)
    def -(other)
      if other.is_a?(DateTime)
        rational_diff = ...  # Rational で厳密に差を計算
        rational_diff.to_f   # => Float に変換して返す
      end
    end
  • activesupport/test/core_ext/time_ext_test.rb

    • 上記の挙動を保証するテストが追加され、Time - DateTimeFloat を返すことを検証しています。

動作サンプル

ruby
elapsed = Time.utc(2000, 1, 2) - DateTime.civil(2000, 1, 1)
elapsed       #=> 86400.0
elapsed.class #=> Float

"#{elapsed} seconds" #=> "86400.0 seconds"
elapsed.to_json      #=> "86400.0"
elapsed.round(2)     #=> 86400.0

内部計算は Rational のままなので、サブ秒精度を持つ DateTime でも精度を保ったうえで最終結果のみ Float になります。


  1. 影響範囲・注意点
  • 型が変わる (Rational → Float)
    • 既存コードで「Time - DateTimeRational を返す前提」で処理していた場合は影響があります。
      • 例: elapsed.numerator, elapsed.denominator を直接使っている場合
      • 例: 型チェック elapsed.is_a?(Rational) に依存している場合
  • 他の Time 差分 API との一貫性が向上
    • Time - TimeTimeWithZone - DateTime などと戻り値の型が揃うため、多くのコードではむしろ扱いやすくなります。
  • 精度面
    • 計算そのものは Rational で行われているため、以前の変更で回復したサブ秒レベルの精度は維持されます。
    • 最終的に IEEE 754 の Float に落とすため、極端に長い期間や非常に細かいサブ秒精度では通常の Float 精度限界の影響は受けますが、これは従来の Time - Time と同等です。
  • 互換性ポリシー的な位置づけ
    • 戻り値の型変更はセマンティクスとしては破壊的変更になりうるため、Rails のメジャー/マイナーアップデートに追随する際にはリリースノート等でこの点を確認しておくのが安全です。

  1. 参考情報 (あれば)
  • 該当 PR: Return a Float from Time#- with a DateTime (#58295)
  • 関連実装ファイル:
    • activesupport/lib/active_support/core_ext/time/calculations.rb
  • 関連テスト:
    • activesupport/test/core_ext/time_ext_test.rb
  • 背景となった過去変更:
    • コミット 4d24a25acf による Time - DateTime 計算の Rational 化(精度向上)
    • 今回はその「精度」は維持しつつ「インターフェース上の戻り値型」を従来の Time 差分 API と揃えるための調整です。

#58067 Make ActiveSupport::TimeZone shareable

マージ日: 2026/7/29 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    ActiveSupport::TimeZone オブジェクトを生成時に freeze することで、Ractor 間で共有可能(Ractor-shareable)にし、TimeWithZone 経由でタイムゾーンにアクセスした際の Ractor Isolation Error を解消する変更です。クラス設計上ミュータブルである必要がないことを前提に、安全に共有できるようにしています。

  1. 変更内容の詳細

2-1. 何をしたか

主な変更点は ActiveSupport::TimeZone のインスタンスを生成直後に freeze するようにしたことです。

対象ファイル:

  • activesupport/lib/active_support/values/time_zone.rb
  • これに対応するテスト追加・修正:
    • activesupport/test/time_zone_test.rb
    • 関連する view helper のテスト修正:
      • actionview/test/template/date_helper_test.rb

疑似的には以下のような変更が入っているイメージです(実際のコードを要約):

ruby
module ActiveSupport
  class TimeZone
    def initialize(name, utc_offset = nil, tzinfo = nil)
      # 既存の初期化処理 …
      # @name, @utc_offset, @tzinfo などをセット

      freeze  # ← この PR で追加
    end
  end
end

これにより、ActiveSupport::TimeZone のインスタンスは生成後に変更不可能となり、Ruby の Ractor モデルにおける「shareable object(共有可能オブジェクト)」として扱えるようになります。

2-2. 背景: なぜ freeze すると Ractor で使えるのか

Ruby の Ractor は「共有できるオブジェクト」と「共有できないオブジェクト」を厳密に分けており、共有できるのは以下のようなものに限られます:

  • イミュータブルなオブジェクト(freeze 済みで、内部にミュータブルな状態を持たない)
  • 一部のビルトインのスレッドセーフなオブジェクト など

ActiveSupport::TimeZone は:

  • 公開 API 的にはインスタンスを後から書き換える手段がない
  • 遅延初期化で ivar を後からセットするようなコードもない

という設計になっているため、コンストラクタで必要な状態をすべてセットした後に freeze してしまっても問題ない、と判断されています。

その結果:

ruby
zone = ActiveSupport::TimeZone["UTC"]
Ractor.new(zone) do |z|
  # ここで z を安全に利用できるようになる
  z.now # など
end

といったコードで、これまで発生していた Ractor Isolation Error を回避できます。

2-3. テスト側の変更

テストでは主に以下を確認していると考えられます:

  • ActiveSupport::TimeZone のインスタンスが frozen? になっていること
  • 既存の機能(名前解決・オフセット計算・TimeWithZone との連携など)が freeze によって壊れていないこと
  • ActionView 側の date_helper など、TimeZone を内部で使うヘルパが従来通り動作すること

actionview/test/template/date_helper_test.rb の 1 行差し替えは、freeze されたオブジェクトを前提にした期待値や挙動に合わせるための微修正と考えられます。


  1. 影響範囲・注意点

3-1. 影響範囲

  • 直接の影響:

    • ActiveSupport::TimeZone を継承したり、メタプログラミングで内部状態を書き換えているようなコードがあれば、freeze により RuntimeError が発生する可能性があります。
    • ただし、通常の利用(Time.zone = "Tokyo"Time.zone.nowin_time_zone など)では挙動は変わらず、むしろ Ractor 利用時の安定性が向上します。
  • Ractor を使うコードへのメリット:

    • Time.zoneTimeWithZone オブジェクトを Ractor 間で共有する際に、内部で ActiveSupport::TimeZone にアクセスしても Isolation Error が起きにくくなります。
    • タイムゾーン情報をグローバルにキャッシュして使うような並行処理コードでも扱いやすくなります。

3-2. 注意点・互換性

  • 非推奨な内部操作をしている場合は破壊的変更になり得る:

    • 例えば、以下のようなコードはこの PR 以降は動かなくなります:

      ruby
      zone = ActiveSupport::TimeZone["UTC"]
      zone.instance_variable_set(:@name, "Hacked") # => RuntimeError: can't modify frozen ...
    • 公式には想定されていない使い方なので、これを機に依存を避けるべきです。

  • TimeZone 内部の将来的な変更との整合性:

    • この PR は「現状、TimeZone はミュータブルではない(し、遅延初期化もしない)」という前提に立っています。
    • 将来 TimeZone に遅延初期化やミュータブルな挙動を加える場合、freeze との整合性を考慮する必要があります(その場合は @mutex など Ractor 非共有な状態も問題になるため、そもそも Ractor 対応方針の再検討が必要)。

  1. 参考情報 (あれば)
  • Ractor と shareable オブジェクトの仕様(Ruby 本体側の背景):

    • Ractor は Ruby 3.0 以降の並列実行機構で、オブジェクト共有には shareable? 条件を満たす必要がある。
    • freeze 済みで、内部に非 shareable オブジェクトを持たないオブジェクトは基本的に shareable と扱える。
  • Rails 内での ActiveSupport::TimeZone の主な用途:

    • Time.zone としてアプリケーションデフォルトタイムゾーンの管理
    • time_zone_select など View ヘルパの選択肢生成
    • Time#in_time_zone, TimeWithZone による変換や表示

この PR は上記の利用パターンに影響を与えず、Ractor 対応を前進させる小さくも実用的な変更と言えます。


#58291 Expand insert_returning deprecation note with migration path

マージ日: 2026/7/29 | 作成者: @kamipo

  1. 概要 (1-2文で)
    このPRは、insert_returning: false オプションに関する非推奨(deprecation)メッセージを拡張し、代替手段として prefetch_primary_key? を利用する移行パスを明示的に案内するものです。コード変更はなく、CHANGELOG 上のドキュメント更新のみです。

  1. 変更内容の詳細
  • 背景

    • insert_returning: false は元々、トリガベースのパーティションテーブル対応のために導入されました(PR #5698)。
    • こうしたテーブルでは、BEFORE INSERT トリガが親テーブルへの INSERT を子テーブルへ振り分けるため、親テーブルで INSERT ... RETURNING を実行しても行が返ってきません。
    • そこで「RETURNING 自体を抑制 (insert_returning: false) し、INSERT 実行後に SELECT currval(...) で主キーを取得する」というワークアラウンドが採られていました。
  • 今回の変更の主旨

    • その insert_returning: false が非推奨になるにあたり、「代わりにどうすればよいか」という具体的な移行パスを CHANGELOG に追記しています。
    • 推奨される代替は prefetch_primary_key? を利用する方法です。
      • prefetch_primary_key?INSERT 前に SELECT nextval(...) を発行して主キーを事前取得し、その値を使って INSERT する仕組みで、Oracle enhanced adapter などですでに使われているパターンです。
      • これにより、INSERT ... RETURNING を使わなくても主キーを安定して取得できます。
  • 実際のコード変更

    • 変更ファイルは activerecord/CHANGELOG.md のみで、+6行のドキュメント追加。
    • Rails 内部コード(アダプタ・モデル・クエリ生成など)の挙動は一切変わっていません。

(サンプルイメージ:prefetch_primary_key? を用いたアダプタ側実装の概念)

ruby
# 擬似コード(実際の PR には含まれていません)
def prefetch_primary_key?(table_name = nil)
  true
end

def next_sequence_value(sequence_name)
  select_value("SELECT nextval('#{sequence_name}')")
end

def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [])
  if prefetch_primary_key?(table_name) && pk && !id_value
    id_value = next_sequence_value(sequence_name || default_sequence_name(table_name, pk))
  end

  # ここで RETURNING を使わずに INSERT し、id_value を返す
end

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

    • 実行時の挙動は一切変わらず、既存アプリへの直接の影響はありません。
    • 影響するのは、ドキュメントを参照して今後のバージョンアップに備える開発者です。
  • 注意点 / 対応方針

    • これまで PostgreSQL 等でトリガーベースのパーティションを使い、insert_returning: false を前提に主キー取得を行っていた場合、将来的な Rails バージョン(insert_returning: false が削除されるタイミング)で問題になる可能性があります。
    • 対応としては、アダプタまたはカスタムコネクションの実装で prefetch_primary_key? を有効にし、SELECT nextval ベースで主キーを事前取得する方式へ移行することが推奨されます。
    • すでに Oracle enhanced adapter で使われている実績あるパターンなので、それに倣う形で実装・移行するのが安全です。

  1. 参考情報 (あれば)

このPR自体はドキュメント更新のみですが、将来の非互換点に対する明確な移行パスが示された点が重要です。


#58290 Support a single column name for #insert's returning:

マージ日: 2026/7/29 | 作成者: @kamipo

  1. 概要 (1-2文で)
    insert メソッドの returning: オプションで「単一カラム名」を直接指定できるようにし、その場合は配列ではなく「そのカラムの値」を直接返すようにした変更です。これにより、将来廃止予定の pk 位置引数を内部的に returning: へ移行できるようにしつつ、互換性を保っています。

  1. 変更内容の詳細

2-1. returning: の振る舞い拡張

従来の #insert / #exec_insertreturning: は「配列のみ」を受け付け、結果も「配列」で返していました。この PR では、以下のようなパターンをサポートします。

ruby
# 1) returning: nil
insert(..., returning: nil)
# => id_value || last_inserted_id(result)
#   (従来どおり: 明示的な returning は使わず、従来の ID 取得ロジックにフォールバック)

# 2) returning: "id"
insert(..., returning: "id")
# => 挿入された行から "id" カラムの値1つを返す (スカラー値)

# 3) returning: ["id"]
insert(..., returning: ["id"])
# => ["id"] という1要素配列をカラム指定として解釈し、
#    1要素の配列 [id_value] を返す

# 4) returning: [c1, c2]
insert(..., returning: [c1, c2])
# => [value_for_c1, value_for_c2] を返す (従来どおり)

要するに:

  • String を指定した場合: そのカラムの値(単一値)を返す
  • Array&lt;String> を指定した場合: カラム値の配列を返す
  • nil の場合: 従来の「last_inserted_id 系」の仕組みにフォールバック

これにより、「今まで pk = "id" で 1 カラムの値だけ欲しかったケース」を、returning: "id" に書き換えればそのまま同じ形の戻り値を得られるようになりました(配列の unwrap が不要)。


2-2. 内部実装から pk 引数を削除

内部メソッド _exec_insertsql_for_insert からは、pk 位置引数が完全に削除されました。代わりに、必要な情報は returning: で渡される前提に変更されています。

ただし、パブリック API である insert / exec_insert は、まだ従来どおり pk 位置引数を受け取ります。その上で内部的に「互換レイヤー(shim)」として returning: に変換します。

変換ロジック(互換 shim)のイメージ:

ruby
def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = [], returning: nil)
  # まだ pk 引数をサポートするが、内部的には returning にマップする
  effective_returning =
    if returning != nil
      returning
    else
      case pk
      when String
        pk   # "id" -> "id" (単一カラム名)
      when false
        []   # false -> [] (何も返さない)
      else
        nil  # その他は従来どおり nil (id_value || last_inserted_id(result))
      end
    end

  _exec_insert(..., returning: effective_returning)
end

ポイント:

  • pk = "id"returning: "id" に変換され、単一値を返す
  • pk = falsereturning: [] に変換され、「何も返さない」挙動
  • 呼び出し側がすでに returning: を指定している場合は pk より returning: が優先される想定(実装に準じる)

これにより、外部 API のシグネチャは現時点では変えずに、内部実装を「returning: ベース」に統一する準備が整いました。将来的には pk が正式に非推奨 → 削除される予定です。


2-3. PostgreSQL アダプタの挙動

PostgreSQL アダプタには、INSERT ... RETURNING を使わない場合に currval を用いてシーケンスから ID を取得するフォールバック (@use_insert_returning = false) が存在します。

この PR では、pk を速やかに完全削除してしまうと、このフォールバック経路で必要な sequence_name を求められなくなってしまう問題があるため、次のような対応が取られています。

  • PostgreSQL の insert / exec_insert のオーバーライド内で、まだ pk を使って sequence_name を解決する処理を維持
  • もしくは pk が無い場合にもスキーマキャッシュから sequence_name を導出できるようにしておき、@use_insert_returning = false のフォールバックが引き続き機能するよう保持

つまり、PostgreSQL では:

  • 外部 API シグネチャ上の pk はまだ残る
  • ただし _exec_insertsql_for_insert のような下位メソッドからは pk が退場
  • sequence_name の解決だけは当面 pk も利用しつつ、将来的な削除に備えている

2-4. Trilogy アダプタ / テストの変更

  • trilogy アダプタの database_statements.rb でも _exec_insert 等のシグネチャ調整が行われ、pk 排除・returning: ベースの API に追従しています(+2/-2 と軽微)。
  • activerecord/test/cases/database_statements_test.rb に、returning: に単一カラム名を渡したときにスカラー値が返ることなどを確認するテストが追加されています(+13行)。

  1. 影響範囲・注意点

3-1. ライブラリ・アダプタ実装者への影響

自作アダプタや ActiveRecord を拡張している場合、以下を確認してください。

  • _exec_insert / sql_for_insertpk 位置引数を取っている場合、今回の変更に合わせて削除し、returning: オプションを解釈する実装に移行する必要があります。
  • まだ insert(arel, nil, "id", ...) のように pk を直接使っているコードは、将来的な非推奨化に備えて returning: への移行を検討してください。

推奨の書き換え例:

ruby
# 旧来スタイル (今後非推奨予定)
connection.insert(arel, nil, "id")

# 推奨スタイル
connection.insert(arel, nil, nil, nil, nil, [], returning: "id")
# または、ActiveRecord::Persistence 経由の通常の save / create を使う

3-2. アプリケーションコードへの実務的影響

アプリケーション側で insert / exec_insert を直接使っていない一般的な Rails アプリは、この PR の影響をほぼ受けません。

insert / exec_insert を直接使っている場合:

  • すでに returning: ["id"] のような形で使っているコードは、そのまま動きます(戻り値が配列であることも変わりません)。
  • 「単一カラムだけが欲しい」場合に、今後は returning: "id" を使うと、ラップ解除処理が要らなくなるため、実装をシンプルにできます。

例:

ruby
# 変更前: 配列から取り出していた
id = connection.insert(arel, nil, nil, nil, nil, [], returning: ["id"]).first

# 変更後: スカラーで返る
id = connection.insert(arel, nil, nil, nil, nil, [], returning: "id")

注意点:

  • returning: "id"returning: ["id"] では戻り値の型が異なる(スカラー vs 配列)ので、意図に応じて使い分ける必要があります。
  • 既存コードが returning: "id" をすでに使っていた場合は(ほぼ無い想定ですが)、今回の変更により戻り値の型が変わる可能性があります。テストがあれば検知できます。

  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/58290
  • 関連トピック:
    • ActiveRecord #insert / #exec_insertpk 引数の将来的な非推奨・削除の準備
    • PostgreSQL アダプタにおける INSERT ... RETURNINGcurrval フォールバックの共存戦略
    • ActiveRecord での returning: オプション活用パターン(単一値 vs 複数カラム)

#58294 Allow to use ActiveModel#to_partial_path in a Ractor

マージ日: 2026/7/29 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    ActiveModel#to_partial_path が Ractor 内から安全に呼び出せるように、返されるパス文字列の生成方法(およびメモ化の仕方)が Ractor フレンドリーになるよう修正した PRです。メモ化自体の Ractor 対応は別PRで行う前提ですが、本PRで少なくとも Ractor 内での to_partial_path 呼び出しが可能になります。

  1. 変更内容の詳細

何を直したか

ActiveModel#to_partial_path は通常、モデルインスタンスに対応するパーシャルのパスを文字列として返しますが、その内部実装で 文字列補間によって生成した文字列をメモ化 していました。

Ruby 3 の Ractor では、「Ractor間で共有されるオブジェクトは frozen であること」が要求されます。
今回の問題は:

  • 文字列補間("#{...}")の結果が frozen ではない
  • その非 frozen な文字列をインスタンス変数にメモ化
  • そのモデルオブジェクトを Ractor 間で共有・利用しようとするとエラー or Ractor 的に安全でない状態になる

という点でした。

このPRでは、文字列生成を Ractor で安全に扱えるように修正(=to_partial_path の戻り値が Ractor 的に問題にならないように)しています。
差分は activemodel/lib/active_model/conversion.rb に +2/-2 行と非常に小さく、おそらく以下のような変更が行われています(イメージ):

ruby
# 変更前(例)
@to_partial_path ||= "#{model_name.cache_key}"

# 変更後(例)
@to_partial_path ||= +"#{model_name.cache_key}"  # あるいは .dup / .freeze など

実際には、Ractor で共有可能な(frozen か、もしくは Ractor 内生成で共有しない)形になるような生成方法に変更されています。
PR本文からわかるのは:

  • 「String interpolation doesn't return a frozen string so the memoized value can't be accessed inside a ractor. This patch fixes that.」
    • =「文字列補間は frozen string を返さないので、メモ化された値は Ractor 内でアクセスできない → そこを直した」という趣旨

テスト

activemodel/test/cases/conversion_test.rb にテストが追加されています (+14行)。
内容的には、Ractor 内から to_partial_path を呼んでも例外が出ないことや、期待どおりのパスが返ることを検証していると考えられます。


  1. 影響範囲・注意点
  • 対象:
    • ActiveModel::Conversion を include しているクラスの #to_partial_path
    • 典型的には Active Record モデル、もしくは ActiveModel 準拠オブジェクト
  • 影響:
    • Ractor を使うコードで model.to_partial_path を呼び出しても安全に動作するようになります。
    • 通常の(Ractor を使わない)Rails アプリにとっては、挙動はこれまでと同じであり、互換性問題はほぼない想定です。
  • 注意点:
    • PR の NOTE にある通り「メモ化そのものの仕組みが Ractor 対応になったわけではない」点に注意が必要です。
      • 例: 既にメイン Ractor でメモ化されたインスタンスを別 Ractor に渡す場合の扱いなど、より広い意味での Ractor 対応は別 PR で改めて行われる予定。
    • Ractor を本格利用している場合は、この PR だけで全ての Ractor 絡みの問題が解決するとは限らず、後続 PR を追う必要があります。

  1. 参考情報 (あれば)
  • 対象PR: https://github.com/rails/rails/pull/58294
  • Ractor とオブジェクト共有の制約(Ruby公式ドキュメント):
    • 「Ractor 間で共有できるのは frozen なオブジェクト、あるいは特定の型に限られる」という仕様が背景にあります。
  • このPRは CHANGELOG 更新はしておらず、「比較的軽微なバグ修正」として扱われていることがチェックリストから読み取れます。

#57934 Separate <h2> through <h6> from neighbors in plain text conversion

マージ日: 2026/7/29 | 作成者: @55728

  1. 概要 (1-2文で)
    このPRは、Action Textでリッチテキストをプレーンテキストに変換する際、<h1> だけでなく <h2><h6> の見出しも前後に空行を挟んでブロック要素として扱うように修正したものです。これにより、見出しと本文がくっついて「Titlebody」のようになってしまう問題が解消されます。

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

何が問題だったか

ActionText::Content#to_plain_text はHTMLをテキストに変換する際、<h1> はブロック扱い(前後に空行を入れる)をしていましたが、<h2><h6> はインラインに近い扱いになっており、隣接コンテンツと連結されてしまっていました。

例:

ruby
ActionText::Content.new("<h2>Title</h2><div>body</div>").to_plain_text
# Before: "Titlebody"
# 見出しと本文がそのまま連結されてしまう

Markdown変換側では、すでに <h1><h6> を見出しとして一貫して扱っており、プレーンテキスト変換との挙動不一致もありました。

何を修正したか

actiontext/lib/action_text/plain_text_conversion.rb 内で、HTMLのどのタグを「ブロック要素」として扱うかを定義している箇所に、h2h6 を追加しています。

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

ruby
# 変更前(例)
BLOCK_TAGS = %w[
  h1 p div br ...
]

# 変更後(例)
BLOCK_TAGS = %w[
  h1 h2 h3 h4 h5 h6 p div br ...
]

これにより、to_plain_text の変換結果は以下のようになります:

ruby
ActionText::Content.new("<h2>Title</h2><div>body</div>").to_plain_text
# After: "Title\n\nbody"

つまり、見出し行のあとに空行(\n\n)が入るようになります。

テストの追加

actiontext/test/unit/plain_text_conversion_test.rb に、<h2><h6> についてもブロックとして扱われることを確認するテストが追加されています。
テストではそれぞれのレベルの見出しについて、周囲のテキストと空行で区切られているかを検証していると考えられます。


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

    • ActionText::Content#to_plain_text を利用している箇所(例: メール本文のプレーンテキスト版生成、検索用インデックスのテキスト抽出、ログ出力など)で、<h2><h6> を含むコンテンツのテキスト表現が変わります。
    • 具体的には、見出し行の前後に空行が追加されるため、テキストの改行位置や行数が変わります。
  • 後方互換性の観点

    • 内容そのもの(文字列の中身)は変わらず、「どこに改行が入るか」だけが変わります。
    • ただし、以下のようなケースでは差分が発生する可能性があります:
      • プレーンテキストの行数やフォーマットを前提にパースしている処理
      • プレーンテキストのスナップショットをテスト期待値としてハードコードしているテスト
    • これらの場合は、期待されるテキストフォーマットを新仕様に合わせて更新する必要があります。
  • 挙動の一貫性

    • Markdown変換では元々 <h1><h6> を見出しとして扱っていたため、プレーンテキスト変換との整合性がとれるようになりました。
    • HTML視点でも見出し要素はすべてブロックレベル要素なので、その意味でも自然な挙動です。

  1. 参考情報 (あれば)
  • 該当PR: https://github.com/rails/rails/pull/57934
  • 関連API:
    • ActionText::Content#to_plain_text
    • actiontext/lib/action_text/plain_text_conversion.rb (ブロック要素判定ロジック)
  • 補足: プレーンテキスト変換を利用しているアプリケーションでは、主要な見出しタグ(<h1><h6>)を含むコンテンツの出力フォーマットが変わるため、テキスト出力を検証している箇所の再確認が推奨されます。

#58287 Fix Range#sum with a falsey initial value

マージ日: 2026/7/29 | 作成者: @Saidbek

  1. 概要 (1-2文で)
    Range に対する sum メソッドで、初期値に nilfalse のような “falsy” 値を渡したときの挙動が、Array/Enumerable と一致するように修正された PRです。これにより、Range#sum が初期値を 0 とみなして勝手に数値加算してしまう不整合な挙動が解消されています。

  1. 変更内容の詳細

何が問題だったか

Rails は Enumerable#sum を拡張しており、Range も Enumerable なので sum を使えますが、これまで Range に対する sum では「初期値が falsy のとき」に特別扱いされていました。

従来の挙動(Before):

ruby
(1..4).sum(nil)    # => 10  # 実質的に sum(0) として扱われていた
(10..0).sum(nil)   # => 0
(1..4).sum(false)  # => 10

一方で、Array に対しては次のような挙動をします:

ruby
[1,2,3,4].sum(nil)   # => NoMethodError (nil に + を定義していないので)
[1,2,3,4].sum(false) # => NoMethodError
[].sum(nil)          # => nil
[].sum(false)        # => false

つまり Array と Range で挙動が食い違っていた のが問題でした。

修正後の挙動

この PR によって、Range に対する sum が Array/Enumerable の仕様に揃えられています。

修正後(After):

ruby
(1..4).sum(nil)     # => NoMethodError ([1,2,3,4].sum(nil) と同じ)
(10..0).sum(nil)    # => nil       # 要素がないので初期値がそのまま返る
(1..4).sum(false)   # => NoMethodError
(10..0).sum(false)  # => false     # 要素がないので初期値がそのまま返る

ポイント:

  • Range が空でない場合
    • sum(nil) / sum(false) は、途中で nil + 1 / false + 1 のような計算になり NoMethodError になる(Array と同じ)。
  • Range が空の場合
    • sum(initial) は、初期値をそのまま返す(これは Enumerable#sum の一般的仕様)。

実装面の変更(推測レベルの説明)

変更ファイルから推測できる内容:

  • activesupport/lib/active_support/core_ext/enumerable.rb
    • Range に対する sum の最適化パス(おそらく「整数 Range のときに高速に足し合わせる処理」)で、nilfalse を「0 とみなす」ような条件分岐が入っていたのを削除/修正したと考えられます。
    • これにより、falsy 初期値であっても Enumerable 共通のロジックに従うようになったはずです。
  • activesupport/test/core_ext/enumerable_test.rb
    • 上記「Before/After」のようなテストケースが追加され、Array と Range の結果が一致することを検証していると思われます。
  • activesupport/CHANGELOG.md
    • Active Support の挙動変更として「Range#sum の初期値が falsy な場合の挙動を修正した」旨が追記されています。

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

    • Range#sumnilfalse を初期値として渡しているコード すべてが対象です。

    • 特に、以下のようなコードは振る舞いが変わります。

      ruby
      (1..4).sum(nil)     # 以前: 10 / これから: NoMethodError
      (1..4).sum(false)   # 以前: 10 / これから: NoMethodError
  • 互換性(Breaking Change)の可能性

    • 以前は「たまたま動いていた」(初期値が 0 扱いされていた)コードが、今後は例外を投げる ようになります。

    • もし「Range が空かもしれないので初期値が欲しい」という意図で nilfalse を使っていた場合、Array と同様の仕様に合わせて、適切な初期値(0 などの数値)を明示的に渡す必要があります。

      例:

      ruby
      # こういう書き方をしていた場合
      total = (start..finish).sum(nil) rescue nil
      
      # 期待動作ごとに修正を検討
      total = (start..finish).sum(0)          # 数値の合計が欲しい
      total = (start..finish).presence&.sum   # そもそも空は nil にしたい 等
  • 本来の Ruby 仕様/Enumerable 仕様との一貫性

    • Ruby 本体の Enumerable#sum 仕様および Array の挙動と一致するようになったため、直感的かつ一貫性のある挙動になります。
    • 新規コードでは Array と Range で同じ感覚で sum を使えるようになるため、むしろバグを減らす方向の変更です。

  1. 参考情報 (あれば)
  • 対象バージョン:
    • この PR は Active Support(Rails)の変更であり、マージ日から見て Rails 7.x か 8 以降のいずれかのリリースに含まれる見込みです。実際にどのバージョンに入ったかは該当バージョンの CHANGELOG で確認してください。
  • 関連仕様(Ruby 本体):
    • Enumerable#sum は「初期値が指定され、かつ要素が 1 つもない場合はその初期値を返す」「初期値に nilfalse を渡してもそのまま使う(ので、演算子が定義されていなければ例外になる)」という仕様です。
    • この PR は、Active Support が提供する Range 用の最適化がこの一般仕様から外れていた点を修正するものと言えます。

#58241 Normalize mixed-case percent escapes in paths

マージ日: 2026/7/29 | 作成者: @afurm

  1. 概要 (1-2文で)
    Rails のルーターが扱うパスの % エンコードを正規化する際、「%2f」「%2F」のように大小文字が混在した 16 進表記も含めて、大文字に統一するように修正した PR です。これにより、パスのパーセントエンコードが一貫して正規化され、テストでその挙動が担保されます。

  1. 変更内容の詳細

何をしているか

  • ActionDispatch::Journey::Router::Utils 内の「パスの正規化処理」において、
    • これまで「完全に小文字の %2f など」は %2F のように大文字化されていたが、
    • %2f%2F のような mixed-case(大小文字混在)」は正規化対象から漏れており、非正規化な形のまま残っていた。
  • この PR で、mixed-case の 16 進数エスケープも含め、パーセントエンコードされたバイト列を常に大文字に正規化するように修正しています。

変更行としては router/utils.rb の 1 行のロジック修正と、それをカバーするテスト (router/utils_test.rb) が 4 行追加された程度の小さな変更です。

挙動イメージ(擬似コード例)

(実際の実装とは正規表現などが多少異なる可能性がありますが、意図としては以下のような挙動になります。)

ruby
# 変更後のイメージ
def normalize_path(path)
  path.gsub(/%[0-9A-Fa-f]{2}/) do |match|
    match.upcase
  end
end

normalize_path("/foo%2fbar")   # => "/foo%2Fbar"
normalize_path("/foo%2Fbar")   # => "/foo%2Fbar"
normalize_path("/foo%2a")      # => "/foo%2A"
  • before: 小文字のみ (%2f) だけが %2F に正規化され、%2F%2a のような mixed-case が一貫して扱われないケースがあった。
  • after: % に続く 2 文字の 16 進数が いずれも大文字化 されるため、「全て %HH 形式(H は大文字 16 進)」に統一される。

テストの追加

actionpack/test/journey/router/utils_test.rb に回帰テストが追加されています。

  • mixed-case の % エスケープを含むパスを入力し、出力がすべて大文字の %HH 形式に揃っていることを確認するテストです。
  • これにより、今後この仕様が崩れた場合にも検知できます。

  1. 影響範囲・注意点

影響範囲

  • 対象: Action Pack のルーティング(ActionDispatch::Journey)で扱う URL パス
  • 具体的には:
    • Rack レベルで渡ってくる PATH_INFO にパーセントエンコードが含まれている場合、
    • Rails がそれをルーティング時に解釈・マッチングするときの 正規化の一貫性が向上 します。

互換性・既存コードへの影響

  • 変更は「見た目のパス文字列」(特に % 部分)の大文字/小文字の揺れをなくすのみで、デコード結果(実際の文字)は変わりません。
  • 通常は後方互換性が高い変更と考えられますが、以下のような場合は影響し得ます:
    1. アプリ側で「パーセントエンコードされた文字列をそのまま比較」している場合
      • 例: request.path をそのまま文字列比較しており、"/foo%2fbar""/foo%2Fbar" を区別していたようなロジック。
      • この PR により、request.path(あるいはその前後の処理)が常に %2F のような大文字表現側に寄る可能性があります。
    2. ログ・監査用途で「受信した URL の生の表記」を保持したい場合
      • ルーティング前後でパスの文字列表現が多少変わる(エスケープ部分のみ大文字化)ため、「到着時の生パス」を正確に保ちたい場合は Rack 環境変数から直接取るなどの工夫が必要です。

それ以外の一般的なユースケースでは、

  • ルート定義やコントローラの動作、URL helpers の出力などに対する機能的な影響はほぼ無いと考えられますが、
  • ルーター周辺でのカスタムミドルウェアやパス正規化処理を独自に書いている場合は、一度 % エンコードの取り扱いを確認しておくと安心です。

  1. 参考情報 (あれば)
  • 対応 PR: https://github.com/rails/rails/pull/58241
  • 関連コンポーネント: ActionDispatch::Journey::Router::Utils(Action Pack / ルーティング)
  • 背景となる一般的な仕様:
    • URL のパーセントエンコードでは %HH(H は 16 進数)の大小文字は本来区別されませんが、正規化(canonicalization) の一環として大文字に統一するのが一般的です。
    • この PR はその慣習に合わせ、mixed-case を含む全ての %xx%XX に統一するための修正です。

#58280 Resolve the implicit template with a single lookup

マージ日: 2026/7/29 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    default_render が暗黙テンプレートを探す際のテンプレート探索を「1回の lookup だけ」に統一し、同じテンプレートを二重に探索しないよう最適化するとともに、variants 周りで 406 が誤って返されうる不整合を解消する変更です。

  1. 変更内容の詳細

背景: これまでの動き

ActionController::Metal::ImplicitRenderdefault_render は、アクション内で render が呼ばれなかった場合に「暗黙的に」テンプレートを描画する処理です。概ね以下のような流れでした:

  1. template_exists?(action_name, ...) でテンプレートの存在をチェック
  2. 存在すれば render を呼ぶ
    ここで render 内部は determine_template を通じて再度テンプレートを解決します

つまり、同じアクションテンプレートを 1 回のレスポンスで 2 回 lookup していた、という無駄がありました。

また、この「存在チェック」と「実際のレンダリング」が別々の経路でテンプレート解決を行うため、

  • lookup_context.variants は設定されている
  • しかし request.variant は設定されていない

という状況で挙動の差異が発生していました。

  • template_exists? 側は lookup_context.variants を「上書き」する形の挙動となり、
    該当するテンプレートがあるのに無いと判断されるケースがあった
  • その結果、暗黙レンダリングでは 406 (Not Acceptable) を返すのに、
    同じアクション内での render 明示呼び出しなら問題なくそのテンプレートを返す
    という不整合が発生していました。

今回の変更内容

PR の概要にある通り、

Find it once with lookup_context.find and pass the resolved Template to render, which uses it as-is.

という方針に修正されています。

コード上では:

  • template_exists? + render という 2 段構えをやめる
  • lookup_context.find を使って「実際に使う Template オブジェクト」を 1 回の lookup で取得
  • その Template をそのまま render template: resolved_template のような形で渡す(determine_template などによる再解決をスキップ)

という形になっています。

これにより:

  • 暗黙レンダリング時のテンプレート解決が 1 パスで完結
  • lookup_context.variants が正しく尊重される
  • request.variant が未設定でも、lookup_context 側の設定に基づき、
    明示的 render と暗黙 default_render の挙動が一致

するようになっています。


  1. 影響範囲・注意点
  • パフォーマンス面:
    • 同じアクションテンプレートを 2 回 lookup していた箇所が 1 回に減るため、
      テンプレート探索コストがわずかに削減されます(特に複雑な view path / variants / formats を多用しているアプリで効果あり)。
  • 挙動の変更点(互換性に関係しうるもの):
    • lookup_context.variants を直接いじっている(かつ request.variant を使っていない)アプリで、
      • これまで暗黙レンダリング(render を書いていないアクション)では 406 が返っていた
      • 明示的に render を呼ぶとテンプレートが返っていた という差異があった場合、その差異が解消され「どちらもテンプレートが返る」ようになります。
    • これは本来意図された一貫した挙動への修正であり、多くの場合はバグ修正として好ましい変更です。
  • カスタムレンダリング / メタプログラミング:
    • default_render の内部挙動に直接依存するようなメタプログラミングをしている場合
      (例: template_exists? の呼び出し回数をフックして何かしている、など)には挙動が変わる可能性があります。
    • 通常の controller/view の使い方をしている範囲では、既存コードへの悪影響はほぼありません。

  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/58280
  • 関連するコンセプト:
    • ActionView::LookupContext と variants (lookup_context.variants, request.variant)
    • ActionController::Metal::ImplicitRender#default_render
    • template_exists? / lookup_context.find / determine_template の関係

この変更は、**「暗黙レンダリングと明示的 render のテンプレート解決を同一ロジックに揃えた」**と理解すると把握しやすいです。


#58279 Resolve the single best template without sorting the whole set

マージ日: 2026/7/29 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    テンプレート解決(Resolver#find)のアルゴリズムを見直し、「候補を全部集めてソート」せずに1パスで最適なテンプレートを選ぶようにしたパフォーマンス改善のPRです。find/exists? は最初に見つかった最適テンプレートで即終了しつつ、find_all(ActionMailerのmultipartなど)は従来どおりマッチした全テンプレートを返す挙動を維持しています。

  1. 変更内容の詳細

2-1. これまでの挙動(概念的な流れ)

従来の Resolver#find は、おおよそ次のような処理をしていました:

  1. 対象パスに対する候補テンプレートをすべて列挙(find_all
  2. 各候補について:
    • マッチ判定(フォーマット・ハンドラ・locale など)
    • ソート用のキーを生成(優先度計算)
  3. それらをソートし、最初のものを「ベストマッチ」として選択
  4. 選ばれたテンプレートに対して locals のバインド等を行う

つまり「フィルタ → ソート → 先頭を採用」という形で、候補数が多いほどオーバーヘッドが大きくなっていました。


2-2. 新しい挙動の概要

このPRでは、テンプレートの「マッチ判定」と「優先度付け」を 1回の走査(1パス)で済ませるようにしています。

ポイントは:

  • TemplateDetails#rank_for が導入・拡張され、
    • matches?(このテンプレートはリクエスト条件にマッチするか)
    • sort_key_for(どれくらい良いマッチかを示すソートキー)
      を融合した「スコア付け+フィルタリング」の役割を担うようになった。
  • Resolver#find は、候補集合をソートせずに、「今までのベスト」より良いテンプレートが見つかるたびに更新していく形で1パス評価を行う。
  • 候補が1つしかない場合は、ランキング処理すら行わず、シンプルな matches? チェックだけで決める。

これにより、

  • ソート用の配列構築や全候補への locals バインドが不要になり、
  • 大量のテンプレート候補がある場合でもオーバーヘッドを抑えられます。

2-3. Resolver#find / PathSet#find / PathSet#exists? の挙動

Resolver#find

  • 以前は(実装上)find_all でマッチしたテンプレートをすべて集め、その中から1つを選ぶような流れでした。
  • 新実装では、「1テンプレートだけ返す」専用の高速パスを持ち、1パスでベストテンプレートを決定します。
  • ただし、カスタム resolver との互換性維持のため:
    • デフォルト実装の Resolver#find は、外部インターフェースとしては依然として find_all 経由で動作するパスも確保されています。
    • find_all をオーバーライドしているカスタム resolver」は、挙動を変えずに引き続き動作します。

PathSet#find

  • ActionView::PathSet#find は、登録された複数の resolver を順に問い合わせますが、
    今回の変更で「各 resolver に対して最初から 1テンプレートだけを求める」形になっています。
  • 最初にベストマッチが見つかった時点で探索を打ち切るため、不要な resolver の呼び出しが減る場合があります。

PathSet#exists?

  • PathSet#exists? は、最初にヒットした resolver で true を返して即終了するように明示的に最適化されました。
  • これは以前もほぼ同様の意図でしたが、内部的に find/find_all 経由でフルセットを構築するようなパスがあり得たのが、
    今回の1パス設計でよりストレートに「最初のヒットで終わる」挙動になっています。

2-4. find_all は従来どおり「全候補」を返す

  • PathSet#find_all は引き続き、全ての resolver から「条件にマッチするテンプレートの配列」を集め、マージして返します。
  • ActionMailer の multipart メール生成など、「1つではなく複数テンプレート(text + html など)」が必要なケースのために、この挙動は維持されています。
  • 内部的には、find が「1件だけ返す」高速パスで最適化された一方、find_all は「ソートやマージを含めたフルセット生成」を行うままです。

2-5. TemplateDetails#rank_for の導入・改善

TemplateDetails はテンプレートの属性(フォーマット、variant、locale、handler など)をもとに、
リクエストされた詳細(要求されるフォーマット・locale など)とのマッチ度を評価するクラスです。

このPRで:

  • rank_for メソッドが新設 or 拡張され、以下を1つのメソッドにまとめています:
    • マッチしているかどうか(nil or false なら不採用)
    • マッチしている場合、その「良さ」を表すランク(数値やタプル等)
  • Resolver#find は、候補ごとに rank_for を呼び出し、
    • nil → マッチしないのでスキップ
    • 非nil → ランク値を取得し、現在のベストと比較してベターなら差し替え というロジックのみでベストテンプレートを選択します。
  • 候補が1件だけのときは、rank_for を経由せず簡易な matches? チェックで済ませる最短パスが入っています。

※具体的なコードは省略されていますが、概念的には以下のような処理になります:

ruby
best_template = nil
best_rank     = nil

candidates.each do |template|
  rank = template.details.rank_for(requested_details)
  next unless rank

  if best_rank.nil? || rank_better_than?(rank, best_rank)
    best_template = template
    best_rank     = rank
  end
end

best_template

  1. 影響範囲・注意点

3-1. 性能面の影響

  • 多数のテンプレート候補が存在するビュー階層(多言語・多variant・複数フォーマットなど)で、
    テンプレート解決のコストが下がることが期待できます。
  • 特に以下のようなケースで恩恵がある可能性が高いです:
    • render が頻繁に呼ばれ、かつ template cache が効きにくいケース
    • 1つの仮想パス(例: "users/show")に対して、多数の _users/show.* テンプレートが存在するアプリ

3-2. 互換性(カスタム Resolver)

  • このPRは「Custom resolvers that override find_all keep working」と明記しています。
    • つまり、MyResolver < ActionView::Resolver のようなクラスで find_all をオーバーライドしている場合、
      その実装に依存するアプリは挙動を変えずに動作し続けることを目指しています。
  • ただし:
    • もし独自に find をオーバーライドし、独特の挙動(「常に配列を返す」など)をしていた場合は、
      Rails 本体側の前提(find は単一テンプレートを返す)とのギャップが顕在化する可能性があります。
    • 独自 resolver を持つアプリでは、このPRを含むバージョンに上げる際に
      • find / find_all の戻り値の型・意味
      • マッチング・ランキングロジック
        を念のため確認しておくと安全です。

3-3. 行動の変化があり得る微妙なケース

  • 理論上は「既存のソートロジックを rank_for で再現している」ため、
    ベストテンプレートの選択結果は変わらないことが期待されます。
  • しかし、以前の実装が「安定ソート」や「同点時の決め方」に暗黙的な依存をしていた場合、
    非常にレアなパターンで「どのテンプレートが選ばれるか」が変わる可能性があります。
    • 例: 同じ rank を持つテンプレートが複数存在し、かつその順序に依存していた場合。
  • ほとんどのアプリでは問題にならないはずですが、
    レンダリング結果の違いに極めて敏感なテストを持つ場合は、マイグレーション時に snapshot / regression テストを回して確認しておくと安心です。

  1. 参考情報 (あれば)
  • 対象PR:
    • タイトル: Resolve the single best template without sorting the whole set
    • 番号: #58279
    • 作成者: etiennebarrie
    • マージ日時: 2026-07-29T05:25:02Z
  • 関連クラス・モジュール:
    • ActionView::Template::Resolver
    • ActionView::PathSet
    • ActionView::TemplateDetails
  • 関連するユースケース:
    • ビューのテンプレート探索・レンダリング
    • ActionMailer の multipart メール生成(find_all 維持)
    • 多言語対応・variant 対応ビューのパフォーマンス最適化

#58237 Fix Encoding::CompatibilityError in template digests.

マージ日: 2026/7/29 | 作成者: @Edilbek

  1. 概要 (1-2文で)
    Rails のテンプレート依存関係トラッキング(DependencyTracker::RubyTracker)で、ファイルベースの Haml など非 ASCII テキストを含むテンプレートのダイジェスト計算時に発生していた Encoding::CompatibilityError を解消する修正です。RubyTracker がテンプレートを処理する際に、通常のコンパイル時と同様にエンコーディングを揃えてからハンドラに渡すように変更されています。

  1. 変更内容の詳細

問題の背景

  • Rails のビューキャッシュ(フラグメントキャッシュ)ではテンプレートの「ダイジェスト」を計算し、その内容と依存関係を見てキャッシュキーを生成します。
  • ファイルベースのテンプレートは File.binread で読み込まれるため、デフォルトでは ASCII-8BIT (バイナリ) エンコーディングになります。
  • 通常のテンプレートコンパイルフローでは、Template#encode! を通して適切なエンコーディング(通常は UTF-8)に変換した後でテンプレートハンドラ(ERB, Haml など)に渡しています。
  • しかし DependencyTracker::RubyTracker だけは「生の template.source」(ASCII-8BIT のまま)をハンドラに渡しており、Haml/Temple 系のハンドラが「UTF-8 な静的文字列との連結」を行うと Encoding::CompatibilityError が発生していました。
    → 結果として、フラグメントキャッシュのダイジェスト計算中に 500 エラーになるケースがありました。

具体的な修正内容

RubyTracker がテンプレートハンドラに渡すソースを、通常のコンパイルと同じく template.encode! 済みのものに変更しています。

差分概要(意訳):

ruby
# 変更前(概念イメージ)
handler.call(template.source)

# 変更後(概念イメージ)
handler.call(template.encode!)

これにより、

  • ダイジェスト計算時(依存関係トラッキング時)も、
  • 通常のレンダリング時(Template#compiled_source)も、

同一のエンコーディング済みソースがハンドラに渡されるようになり、Haml/Temple のように「UTF-8 静的文字列を結合」する処理でもエラーにならないようになります。

テスト追加

actionview/test/template/dependency_tracker_test.rb に回帰試験が追加されています。

  • Haml を直接依存に入れずに、Temple の「静的マージャー」に似た挙動を模倣する形でテストしている、という説明が PR にあります。
  • テスト内容としては概ね、
    • 非 ASCII 文字(例: 日本語、アクセント付き文字など)を含むテンプレート
    • かつ render を呼び出して依存関係解析が走るケース
    • それに対して DependencyTracker::RubyTracker がエラーを出さずにダイジェスト計算できること を検証するものになっています。

  1. 影響範囲・注意点

影響範囲

  • 影響を受けるケース:
    • ファイルベーステンプレート(特に Haml/Temple ベースのテンプレート)
    • 非 ASCII 文字を含む
    • テンプレート内で render などを呼び出し、DependencyTracker::RubyTracker が依存パースを行う
    • かつフラグメントキャッシュやテンプレートダイジェスト計算が走る場合
  • この PR により、上記条件下で発生していた Encoding::CompatibilityError に起因する 500 エラーが解消されます。

互換性・リスク

  • RubyTracker が扱うテンプレートソースのエンコーディングが、従来の「生の ASCII-8BIT」から「Template#encode! 済み(通常 UTF-8)」に揃えられるだけであり、通常は望ましい挙動の統一です。
  • テンプレートハンドラ側で「ASCII-8BIT を前提にした非常に特殊な処理」をしている場合には、挙動が変わる可能性がありますが、Rails の標準的な ERB/Haml/Temple 系ハンドラにおいては、むしろこちらが正しい前提(UTF-8)です。
  • 既存アプリケーションで、非 ASCII 含むテンプレートのキャッシュ周りで sporadic に発生していた Encoding::CompatibilityError がなくなり、安定性が向上します。

運用上の注意

  • もし独自テンプレートハンドラを実装していて DependencyTracker::RubyTracker 用のコードを持っている場合は、「引数として渡ってくるテンプレートソースは UTF-8 でエンコード済み」という前提で実装されているかを確認すると安全です。
  • 以前にこの種のエラーを回避するために「無理矢理 force_encoding("UTF-8")」などをテンプレートハンドラ側で行っていた場合、今後は二重にエンコード操作されていないか注意しておくとよいです(多くの場合は実害はありませんが、ダブル処理の余計さは残ります)。

  1. 参考情報 (あれば)
  • 対応 Issue: #58203
    → 非 ASCII テキストを含む Haml テンプレート + render + キャッシュダイジェスト計算での Encoding::CompatibilityError 報告。
  • 類似のバグ: #56904 / #56906
    → 同じく File.binread による ASCII-8BIT 読み込みが原因となったエンコーディング関連の不具合群。
  • 関連コード:
    • actionview/lib/action_view/template.rbTemplate#encode!compiled_source 周り)
    • actionview/lib/action_view/dependency_tracker/ruby_tracker.rb(Ruby ベースビューの依存関係トラッキング)

#58278 Fix LazyRouteSet thrashing when url_helpers are included into Object

マージ日: 2026/7/29 | 作成者: @chaadow

  1. 概要 (1-2文で)
    Rails の LazyRouteSet が、url_helpersObject に include した場合にほぼ全てのメソッド問い合わせでルート再読み込みを試みてしまう問題を修正し、*_path / *_url だけを遅延ロードのトリガーに限定する変更です。これにより、大規模アプリでコンソールやスクリプト実行時に見える「ハング」のような挙動を防ぎます。

  1. 変更内容の詳細

背景

  • #52353 で LazyRouteSetmethod_missing / respond_to_missing? を追加し、
    未定義メソッドが呼ばれたときに reload_routes_unless_loaded を呼ぶことで、
    最初のルート描画を「遅延ロード」できるようにしました。
  • これは posts_path / posts_url などの named route helper 用には正しい動きですが、
    Rails.application.routes.url_helpersObject に include すると、
    「あらゆるオブジェクト」にこれらのフックが入り込みます。
ruby
include Rails.application.routes.url_helpers  # Object 直下などで実行

[].respond_to?(:to_ary)
{}.respond_to?(:to_hash)

のような、Ruby が内部で頻繁に呼ぶ respond_to? チェックがすべて LazyRouteSet 経由になり、
ルート描画中にも再度 reload_routes_unless_loaded が呼ばれ続ける、という再入問題が起きていました。
大規模アプリではこれが「固まったように見える」原因になります。

今回の修正ポイント

方針:
「ルート遅延ロードの対象となるメソッドを、*_path / *_url に限定する」

LazyRouteSetmethod_missing / respond_to_missing? を次のような条件付きにしています:

  • 対象とするメソッド名は、_path または _url で終わるもののみ
  • それ以外のメソッドは、ルートの遅延ロードを試さず、素通りする(= 既存実装も呼ばない)

疑似コードイメージ:

ruby
def method_missing(name, *args, &block)
  if name.to_s.end_with?("_path", "_url")
    reload_routes_unless_loaded
  end

  super
end

def respond_to_missing?(name, include_private = false)
  if name.to_s.end_with?("_path", "_url")
    reload_routes_unless_loaded
  end

  super
end

実際の diff では、上記のような「メソッド名のサフィックスチェック」を追加した上で、
reload_routes_unless_loaded を呼ぶかどうかを分岐しています。

テスト追加

railties/test/engine/lazy_route_set_test.rb に以下のようなテストが追加されています。

  • _path / _url で終わらないメソッド (to_ary, to_hash など) については
    reload_routes_unless_loaded が呼ばれないこと
  • _path / _url の場合には、従来通り遅延ロードがトリガーされること

これにより、regression(再発)を防ぎます。


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

    • Rails.application.routes.url_helpersObject や非常に広いスコープに include しているコード
    • 特に Rails console やスクリプトなどで、トップレベルに include ...url_helpers を書いている場合
      • 以前は respond_to?(:to_ary) などの内部的な呼び出しが全てルート再読み込みを試みており、
        大規模アプリではほぼフリーズに見える挙動になり得ました。
      • この PR により、そのような「内部メソッド問い合わせ」は遅延ロードの対象外になります。
  • 影響を受けない/動作が変わらない部分

    • 通常の named route helper (users_path, root_url など) の挙動
      • これらは引き続き、最初の呼び出し時に LazyRouteSet がルートを描画します。
    • url_helpers をコントローラや view helper に include している一般的なパターン
      • Rails が推奨するスコープでの利用は、今回の修正の有無に関わらずほぼ同じ動きです。
  • 考慮点

    • 今後、*_path / *_url 以外の名前でルーティング関連のヘルパーを自動生成したい、
      という要件が出た場合、この「サフィックス前提」のロジックと整合性を取る必要があります。
    • とはいえ Rails の慣習上、named routes は *_path / *_url 固定なので、現実的には問題になりにくいです。

  1. 参考情報 (あれば)
  • 本 PR: #58278
  • 関連 PR:
    • #52353 — LazyRouteSet に method_missing / respond_to_missing? を導入した最初の変更
    • #58252 — after_routes_loaded 周りの無限 redraw 問題を修正した前回の follow-up
  • 問題の根本:
    • Ruby がコアで多用する respond_to?(:to_ary) / respond_to?(:to_hash) などが
      url_helpersObject に混ぜ込むことでフック対象となり、
      さらに LazyRouteSet がルートロード中にもフックされて再入 → スレッジング・ハングに見える
    • 今回の修正は、ルートに関係ある可能性が高い「named route helper らしい名前」だけを
      遅延ロードのトリガーにすることで、この再入パスを閉じています。

#57854 Make Controller middlewares ractor safe

マージ日: 2026/7/29 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    Rails のコントローラ専用ミドルウェアスタック(Rack ミドルウェアではない)を Ractor セーフにするため、コピーオンライト(Copy-on-Write)方式で共有可能オブジェクトとして扱えるようにする変更です。use 経由だけでなく、スタックへの直接アクセスにも対応するためのプロキシ層が導入されています。

  1. 変更内容の詳細

背景と問題点

  • 対象は ActionController::Metal が持つ「コントローラミドルウェアスタック」で、config.middleware などの Rack ミドルウェアスタックとは別物です。
  • Ractor セーフにするには、Ractor 間で共有されるオブジェクトが「freeze されていて、かつ内部的にも変更されない(shareable)」必要があります。
  • 従来の実装では:
    • コントローラクラスがクラスメソッド use を呼んでミドルウェアを積み上げる。
    • もしくは middleware_stack(あるいは類似メソッド)を直接取得して insert, delete などのミューテーションを行う。
  • このため:
    • 一度 Ractor 間で共有した後にスタックを変更されると、Ractor セーフでなくなる。
    • かといって無制限に変更禁止にすると、従来の「クラスレベルでスタックをいじる」書き方が破壊される。

対応方針: Copy-on-Write + Proxy

PRの説明によると、次のような戦略が取られています。

  1. Copy-on-Write でのスタック管理

    • 「ある時点で freeze 済みのミドルウェアスタック」を Ractor 共有用のベースとして扱い、変更要求が来たタイミングで:
      • そのスタックを複製(コピー)して
      • 複製側に対してミューテーションを行い
      • 再度 freeze して shareable な状態に戻す
    • こうすることで、「読み取り専用で共有しつつ、変更が必要なときだけコピーを作る」動作になり、Ractor の要求を満たします。
  2. 直接スタックに触るケース向けの Proxy 導入

    • 現在の Rails では、次の2パターンが存在します:
      ruby
      class ApplicationController < ActionController::Base
        use SomeMiddleware
      end
      ruby
      class ApplicationController < ActionController::Base
        middleware_stack.insert_before(0, SomeMiddleware)
      end
    • 前者 (use) は MiddlewareStack をラップしたメソッドなので、ここに Copy-on-Write ロジックを簡単に仕込めます。
    • 後者のように「スタックオブジェクト自体」を返してしまっている場合、呼び出し側がどんなメソッドを呼ぶかわからないので、スタックを返す代わりに Proxy オブジェクトを返し、その Proxy 経由でミューテーションが発生したら毎回「コピーして再 freeze」する、というデザインになっています。
    • Proxy はざっくりいうと:
      • 未変更時は freeze 済みの共有スタックを指している
      • 変更操作(use, insert, insert_before, delete, などスタックを書き換えうるメソッド)が呼ばれたら:
        1. 内部でスタックを dup(コピー)
        2. dup した側に対して本来のメソッドを実行
        3. 変更が終わったらスタックを再度 freeze
      • 非変更系のメソッド(each, to_a など読み取り専用)はそのまま委譲
  3. テスト (actionpack/test/controller/new_base/middleware_test.rb)

    • 新しいテストは、おそらく以下の観点をカバーしています:
      • use でミドルウェアを追加してもスタックが期待どおりに構築される。
      • スタックを直接取得して操作した場合でも、内部的に Proxy 経由で Copy-on-Write が働く。
      • 操作後もスタックが frozen? であり、Ractor 共有可能な状態になっている(もしくは、Ractor 関連のエラーが出ない)。
    • 具体的なテストコードは PR 情報からは見えませんが、new_base/middleware_test.rb に 22 行追加されており、「新しい Proxy 実装が従来と同じ API を維持していること」「Ractor 対策で regress していないこと」を保証する内容になっているはずです。
  4. action_controller/metal.rb の主な変更イメージ

実際のコードは抜粋されていませんが、典型的には次のような変更が入っていると想定されます(擬似コード):

ruby
module ActionController
  class Metal
    class << self
      def middleware
        @middleware_proxy ||= MiddlewareProxy.new(@_middleware_stack)
      end

      def use(*args, &block)
        middleware.use(*args, &block)
      end

      # もしくは middleware_stack メソッドを proxy 返すようにしている可能性もある
      def middleware_stack
        middleware.stack
      end
    end
  end

  class MiddlewareProxy
    def initialize(stack)
      @stack = stack
      @stack.freeze
    end

    def method_missing(name, *args, &block)
      if mutating_method?(name)
        # Copy-on-Write
        @stack = @stack.dup
        @stack.public_send(name, *args, &block)
        @stack.freeze
      else
        @stack.public_send(name, *args, &block)
      end
    end

    def respond_to_missing?(name, include_private = false)
      @stack.respond_to?(name, include_private) || super
    end
  end
end

※ 実装名や構造は異なる可能性がありますが、PR 説明文から読み取れる方針はこのようなものです。


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

    • ActionController::Metal(およびこれを継承する ActionController::Base 系)で定義される「コントローラ固有ミドルウェア」に影響します。
    • MyController.use(SomeMiddleware) あるいは MyController.middleware_stack / middleware などの API を使っているコードが対象です。
    • Rack ミドルウェアスタック(Rails.application.config.middleware など)には直接の影響はありません。
  • 後方互換性

    • 既存の API (use, middleware_stack 経由の操作) は維持される想定です。
    • Proxy は method_missing / 委譲型で設計されているはずなので、ほとんどのコードは変更なしで動作します。
    • ただし、以下のようなケースでは注意が必要です:
      • middleware_stack のクラスや class.name に直接依存しているようなメタプログラミング(Proxy クラスに変わる可能性)。
      • object_id の恒常性を前提にしているような処理(Copy-on-Write により内部スタックが差し替わる)。
  • パフォーマンス面

    • Copy-on-Write により、「スタック変更が発生した瞬間」に限って dup + freeze のコストが発生します。
    • コントローラクラスのミドルウェアスタックは通常「アプリ起動時・クラス定義時に一度構築して以後ほとんど触らない」ことが多いため、ランタイムリクエスト処理への影響は軽微と見込まれます。
    • 動的にミドルウェアを差し替えるようなメタプログラミングを多用している場合は、変更頻度に応じてオーバーヘッドが増えます。
  • Ractor 利用時のメリット

    • この変更により、コントローラとそのミドルウェアスタックを Ractor 間で安全に共有できるようになります。
    • Rails をマルチ Ractor 環境で動かしたい場合の前提条件の一部が整備された形です。

  1. 参考情報 (あれば)

#58285 Simplify insert callers to only pass what they need

マージ日: 2026/7/29 | 作成者: @kamipo

  1. 概要 (1-2文で)
    connection.insert の呼び出し側から不要な位置引数(pk, id_value, sequence_name)を削除し、insert/sql_for_insert 周りのインターフェイスと挙動を簡素化・最適化した変更です。あわせて、schema_migration.create_version など一部の API の挙動も見直し、無駄な DB ラウンドトリップやコメントを削減しています。

  1. 変更内容の詳細

2-1. connection.insert 呼び出しの簡素化

対象:

  • ActiveRecord::Persistence#_insert_record
  • ActiveRecord::InternalMetadata.create_entry
  • ActiveRecord::SchemaMigration.create_version

これらが connection.insert に渡していた以下の位置引数が削除されました。

  • pk
  • id_value
  • sequence_name

元々の insert シグネチャ(概念的には):

ruby
connection.insert(
  arel_or_sql,
  "SQL label",
  pk = nil,
  id_value = nil,
  sequence_name = nil,
  binds = [],
  returning: nil
)

今回の変更で、上記呼び出し側は 必要な情報だけ を渡すように整理されています。

_insert_record の整理点

説明にある通り:

  • _insert_record は常に returning: オプションを指定しており、戻り値としての id は returning_column_values から取得しています。
  • すでに「事前に採番した主キー値」は values[primary_key] に埋め込んで insert しているため、同じ値を id_value 引数で別途渡す必要はありませんでした。

そのため _insert_record 内の connection.insert 呼び出しは、ざっくり次のような形になったと考えられます(擬似コード):

ruby
# 変更前(イメージ)
connection.insert(
  arel,
  "SQL",
  primary_key,
  id_value,
  sequence_name,
  binds,
  returning: returning_columns
)

# 変更後(イメージ)
connection.insert(
  arel,
  "SQL",
  binds,
  returning: returning_columns
)

結果として、_insert_record の内部は:

  • 重複した情報(id_value)の二重管理がなくなる
  • insert のシグネチャ依存度が下がり、インターフェイスがすっきりする

という改善が入っています。

internal_metadata.create_entry

  • internal_metadata.create_entryinsert戻り値を全く使っていなかった ため、pk / id_value / sequence_name は完全に不要でした。
  • 今回それらの引数指定を削除し、必要最小限の呼び出しに変更。

schema_migration.create_version

ここが比較的大きめのロジック変更です。

これまで:

  • schema_migration.create_version は、sql_for_insert
    pkreturning が両方 nil のとき、自動的に RETURNING <pk> を SQL に付け足す」
    という仕様に依存していました。
  • つまり、わざと pk/returning を指定せずに呼び出し、DB 側から主キー値を返させる前提でした。

しかし:

  • schema_migration における version は、呼び出し元が すでに知っている値(マイグレーション番号)です。
  • そのため、DB に「version いくつを insert した?」と確認するラウンドトリップ自体が不要です。

今回の変更:

  • create_versioncreate_versions に素直に委譲し、引数で受け取った version をそのまま返すだけに変更。
  • これにより、
    • 「このメソッドは create_versions に委譲できない」というコメントは不要になり削除。
    • 無駄な RETURNING ベースの round trip を削減。

擬似コードイメージ:

ruby
# 変更前(イメージ)
def create_version(version)
  connection.insert(
    sql_for_insert(..., pk: nil, returning: nil), # => 自動で RETURNING <pk>
    "SQL"
  )
end

# 変更後(イメージ)
def create_version(version)
  create_versions([version])
  version
end

2-2. sql_for_insert の挙動の引き締め

sql_for_insert では、これまで:

  • pknil かつ returningnil のときに
    • スキーマキャッシュから schema_cache.primary_keys(table_name) を引いて
    • 自動的に RETURNING <pk> を付与する

という 主キー自動検出 + RETURNING 自動付与 を行っていました。

今回の修正では:

  • returning が明示的に指定されている場合は、主キー自動検出を行わない ように変更されています。

これにより:

  • returning: を自前で制御している呼び出し側に対して、
    • 余計な schema_cache.primary_keys アクセスが発生しない
    • 挙動がより「書いたとおり」になり、暗黙の RETURNING 振る舞いに依存しなくなる

といったメリットがあります。


  1. 影響範囲・注意点

3-1. Rails 内部 API への影響

対象となるのは主に Rails 内部コードの呼び出し です:

  • _insert_record
  • internal_metadata.create_entry
  • schema_migration.create_version
  • sql_for_insertreturning 関連挙動

通常のアプリケーションコードが直接影響を受けるのは:

  1. connection.insert を独自に呼び出している場合
    特に、pk / id_value / sequence_name を位置引数で渡しているようなコードは、
    今後の Rails バージョンで互換性が崩れる可能性があります。
    • Rails の公開 API としてどこまで保証しているかにもよりますが、
      この PR からは「pk / id_value に依存した戻り値取得」は推奨されない方向が伺えます。
  2. sql_for_insert をオーバーライド/直接利用しているアダプタ・拡張コード
    • returning を明示的に指定している場合に、これまで暗黙に行われていた主キー自動検出が走らなくなるので、
      pk が自動で拾われる前提」の独自コードがあると挙動が変わる可能性があります。

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

  • sql_for_insertreturning: が明示されているケースでは、
    余分な schema_cache.primary_keys 呼び出しがなくなるため、微小ながらパフォーマンス改善が見込まれます。
  • schema_migration.create_version が DB から version を取り直さなくなることで、
    マイグレーション実行時の無駄な round trip が 1 回分減ります(特に大量マイグレーション時にはわずかに効く可能性があります)。

3-3. マイグレーション API の利用者への影響

ActiveRecord::SchemaMigration.create_version を直接叩いているアプリケーションは少ないと思われますが、いた場合:

  • これまで戻り値として「DB に挿入されたレコードの主キー値」を期待していたコードは、
    今後は「引数で渡した version がそのまま返ってくる」という前提に合わせる必要があります。

ただし、通常利用(rails db:migrate 経由)では挙動は体感的に変わりません。


  1. 参考情報 (あれば)
  • PR本体: https://github.com/rails/rails/pull/58285
  • 関連しそうな内部 API:
    • ActiveRecord::ConnectionAdapters::DatabaseStatements#insert
    • ActiveRecord::Persistence#_insert_record
    • ActiveRecord::SchemaMigration
    • ActiveRecord::InternalMetadata
  • もし自作アダプタや AR 拡張で insert / sql_for_insert を直接扱っている場合は、
    この PR の差分を参照して自分のコードが「pk/id_value に暗黙依存していないか」「returning 指定時の挙動を当てにしていないか」を確認すると安全です。

#57981 Only swallow the LoadError we intended to

マージ日: 2026/7/29 | 作成者: @skipkayhil

  1. 概要 (1-2文で)
    RailsのLoadErrorハンドリングを見直し、「意図した場合のみ LoadError を握りつぶす」ようにすることで、予期せぬ例外の隠蔽を防ぐ修正です。rails/all のロード処理とテスト用の isolation ヘルパで、捕捉する LoadError の範囲をより厳密にしています。

  1. 変更内容の詳細

※PR本文や diff 全体は提示されていないため、Rails の既存実装と PR タイトル・関連 issue (#57969), 過去 PR (#31307) から推測を含みますが、Rails で典型的なパターンに沿った説明をします。

背景となる問題

Rails では、特定のファイル・ライブラリを「存在すれば使うが、なければスキップする」という目的で以下のようなコードを書くことがあります:

ruby
begin
  require "some_optional_dependency"
rescue LoadError
  # gem が入ってなければ何もしない
end

しかしこの書き方だと、本当に「ファイルが見つからない」ことによる LoadError だけでなく、その中で発生した別の原因の LoadError まで握りつぶしてしまう可能性があります。
例えば:

ruby
# some_optional_dependency.rb
require "typo_in_dependency_name"  # これが原因で LoadError

# 呼び出し側
begin
  require "some_optional_dependency"
rescue LoadError
  # ここで握りつぶされてしまい、本来検知すべきバグが見えなくなる
end

この PR は、こうした「本来は表に出すべき LoadError」を誤って rescue しないように、rescue する条件を厳密に絞る変更です。

railties/lib/rails/all.rb の変更

rails/all は、Rails の主要フレームワーク(active_record/railtie, action_controller/railtie など)をまとめて require する役割を持っています。その中で、オプション的なコンポーネントや、環境によって無くてもよいファイルを require し、LoadError を rescue している箇所があります。

今回の変更は、そこにある rescue LoadError を「対象とするファイルに対する LoadError だけを握りつぶす」形に修正しています。典型的には、以下のようなパターンに変わります:

ruby
# 変更前(イメージ)
begin
  require "rails/some_feature"
rescue LoadError
  # any LoadError is swallowed
end

# 変更後(イメージ)
begin
  require "rails/some_feature"
rescue LoadError => error
  raise unless error.path == "rails/some_feature"
  # 本当に "rails/some_feature" が見つからない場合だけスルー
end

実際のコード量としては +2/-1 程度なので、上記のような LoadError => errorraise unless ... など、1 行追加 + 既存行の軽微修正で意図した LoadError のみに絞り込んでいると考えられます。

railties/test/isolation/abstract_unit.rb の変更

テスト用の isolation ヘルパ (railties/test/isolation/abstract_unit.rb) でも同様に、require 周りの LoadError を扱っている箇所があり、その挙動を本体の変更に合わせて調整しています。

ここでは主に:

  • LoadError を発生させる/検知するテストヘルパの書き方を、rails/all と同じパターンに揃える
  • 「意図しない LoadError を飲み込まない」ことを保証するためのテスト補助処理を修正

といった小さな修正(+1/-1)が行われていると考えられます。


  1. 影響範囲・注意点

影響範囲

  • require "rails/all" を通じて Rails をロードしているアプリ/ツール全般
  • 特に、rails/all 内の optional な railtie・フレームワークのロードに依存しているコード
  • Rails テストスイートにおける isolation テスト環境

実務上の影響

  • これまで「何となく動いていた」ケースで、実は内部で LoadError が起きていた場合、それが表に出てきて例外として落ちるようになる可能性があります。
    • 例:
      • rails/all 経由でロードしているファイル内で require "typoed_name" のようなミスがあり、本来は LoadError が出るべきだったが、これまでは握りつぶされていた。
      • 今回の変更により、その LoadError がちゃんと表面化し、スタックトレース付きで落ちる。
  • 結果として、「起動時に突然 LoadError が出るようになった」という挙動変化が起きる可能性はありますが、それは隠れていたバグが可視化された形なので、修正すべき箇所が明確になります。

注意点

  • 自分で LoadError を広く rescue しているコードがある場合も、同じ問題を抱える可能性があります。この PR の書き方を参考に、LoadError#path を確認して意図した require のみ握りつぶすパターンへの移行を検討すると安全です。

    • パターン例:

      ruby
      begin
        require "my_optional_gem"
      rescue LoadError => e
        raise unless e.path == "my_optional_gem"
        # gem が入っていない場合だけスルー
      end
  • PR 説明にある通り 8-1-stable へのバックポート検討が話題になっているため、Rails 8.1 系にも同様の挙動変更が入る可能性があります。

    • アプリケーション側が Rails の minor/patch アップデートに追随する際には、起動時 LoadError の有無を確認しておくと良いです。

  1. 参考情報 (あれば)
  • 対応 Issue: #57969
    「意図しない LoadError が握りつぶされてしまう」類の報告と考えられます。
  • 関連 PR: #31307
    同趣旨の変更または以前の試みを再オープンして仕上げたものと思われます。
  • Ruby での LoadError 安全な扱いの定石:
    • rescue LoadError => e
    • raise unless e.path == "期待しているファイル名"
    • このパターンにより、「その require 自体が失敗した」ケースだけを明示的に無視し、それ以外のエラーは落とすことができます。

#58284 Bump the minimum supported SQLite version to 3.35.0

マージ日: 2026/7/29 | 作成者: @kamipo

  1. 概要 (1-2文で)
    Rails の SQLite3 アダプタが実質的に依存していた RETURNING 句に合わせて、サポートする SQLite の最小バージョンを 3.35.0 に引き上げ、これより古いバージョンでは明示的にエラーを出すようにした PR です。これにより、古い SQLite を使った場合に record.idnil になるといった「サイレントな不具合」が、起動時に検出されるようになります。

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

背景: RETURNING 句と Rails 7.1 以降

  • SQLite 3.35.0 で INSERT ... RETURNING ... 句が導入された。
  • Rails 7.1 の PR #49290 で、SQLite3 アダプタは「INSERT 後に自動採番カラム(主キーなど)を取得する」ために RETURNING に依存する実装に変更された。
    • 以前は @raw_connection.last_insert_row_id を使うフォールバックパスが存在したが、それが削除されている。
  • その結果、SQLite 3.23.0 ~ 3.34.x では以下のような状態になっていた:
    • supports_insert_returning?falsesql_for_insertRETURNING を付けない SQL を生成。
    • しかし last_inserted_idRETURNING の結果セットから読む想定になっており、結果が空のため record.idnil になる。

つまり Rails 7.1 以降、古い SQLite は「対応しているように見えて実は壊れている」という状態でした。

この PR の変更点

  1. SQLite の最小サポートバージョンを 3.35.0 に引き上げ

    • check_version メソッド内でバージョンチェックを行い、3.35.0 未満の場合は例外を発生させるように変更。
    • これにより、アプリケーション起動や DB 接続確立時点で「この SQLite バージョンはサポート外」であることが明示的に分かる。

    (擬似コードイメージ)

    ruby
    def check_version
      if sqlite_version < "3.35.0"
        raise ActiveRecord::SQLite3DatabaseError,
              "SQLite 3.35.0 or newer is required. Detected #{sqlite_version}"
      end
    end
  2. CHANGELOG の更新

    • activerecord/CHANGELOG.md に、SQLite の最小サポートバージョンが 3.35.0 になったことを追記。
    • Rails アプリ/ライブラリのメンテナが、バージョンアップ時に互換性情報を確認できる。
  3. 実装まわりの微修正

    • sqlite3_adapter.rb において、バージョンチェックまわりのコードが加筆・整理されている(行数ベースで +5 / -5 程度の小改修)。
    • ロジックとしては「古いバージョン向けフォールバックを戻す」のではなく、「そもそも古いバージョンをサポート対象外にする」という方向の修正。

  1. 影響範囲・注意点

影響を受けるケース

  • 次のような環境で Rails (少なくとも 7.1 以降) + SQLite を使っている場合、この PR を含むバージョンに上げると接続時にエラーになる可能性があります:
    • SQLite バージョンが 3.35.0 未満:
      • OS 標準の SQLite が古いディストリビューション(古い Debian/Ubuntu、CentOS 系など)
      • CI 環境で古い SQLite がインストールされている場合
      • macOS でも、非常に古い環境を使っている場合

実際に起こっていた不具合の例

  • Rails 7.1 以降 + SQLite 3.23.0〜3.34.x で以下のようなコードを書くと:

    ruby
    user = User.create!(name: "Alice")
    user.id # => 本来は整数の ID が入るべきだが nil になっていた
    • 内部的には INSERT の後に RETURNING 結果を読むことを想定しているが、
    • 古い SQLite では RETURNING が使われないため結果セットが空になり、idnil のままになる。
    • このバグは例外にならず、静かに nil になるため検知しづらかった。
  • この PR により、そもそもそのような古い SQLite ではアダプタが起動時に例外を投げるため、「知らないうちに id が nil だった」といった状況を防げる。

対応策

  1. SQLite を 3.35.0 以降にアップグレードする

    • 本番・開発・CI を含む全環境で SQLite のバージョンを確認することを推奨します。

    • 確認例:

      sh
      sqlite3 --version
    • 必要に応じて:

      • OS のパッケージを更新(例: apt-get upgrade sqlite3brew upgrade sqlite など)
      • Docker イメージを新しいベースイメージに変更
      • CI のイメージやセットアップスクリプトを更新
  2. 古い SQLite をどうしても使わざるを得ない場合

    • この PR を含む Rails バージョンにはアップグレードせず、現状維持または別の DB(PostgreSQL/MySQL 等)への移行を検討する必要があります。
    • 「古い SQLite + Rails 7.1 以降」は元々壊れた状態であるため、根本的にはこの組み合わせは避けた方がよいです。

  1. 参考情報 (あれば)
  • 対象 PR: #58284 「Bump the minimum supported SQLite version to 3.35.0」
  • 関連 PR: #49290(Rails 7.1 での SQLite アダプタが RETURNING に依存するようになった変更)
  • SQLite 3.35.0 のリリースノート(RETURNING 句導入等):
    https://sqlite.org/releaselog/3_35_0.html
  • 実運用上のポイント:
    • 「Rails 7.1 以降 + SQLite」を使う場合、実質的に 3.35.0 以上必須だったことを、この PR で明文化した形になります。
    • Rails のバージョンアップ時は、アプリの Gemfile.lock だけでなく、インフラ側の DB バージョン要件も併せて確認しておくと安全です。

#58283 Consolidate returning_column_values on the abstract adapter

マージ日: 2026/7/29 | 作成者: @kamipo

  1. 概要 (1-2文で)
    insert 時に返される主キーなどの値を取得する処理(returning_column_values)を、各DBアダプタではなく抽象アダプタ(AbstractAdapter)側に集約し、PostgreSQL / SQLite3 / MySQL 側の同一実装を削除したPRです。
    supports_insert_returning? の分岐を共通化しつつ、Trilogy だけが生ドライバ結果を返すという現状の「事実上の契約」を明示した上で、今後のリファクタリングに備える変更になっています。

  1. 変更内容の詳細

2-1. returning_column_values の共通化

activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb に、各アダプタで重複していた returning_column_values の実装が集約されました。

概ね以下のようなロジックになっています(擬似コード):

ruby
def returning_column_values(table_name, primary_key, result)
  if supports_insert_returning?
    # RETURNING ... をサポートしている DB
    # => ActiveRecord::Result として返ってくるので、その最初の行を返す
    result.rows.first
  else
    # RETURNING 非対応の DB (例: mysql2 / Trilogy など)
    # => ドライバから last_insert_id を解決して配列で返す
    [last_inserted_id(result)]
  end
end

これにより、以下のアダプタにあったまったく同じ実装が削除されています。

  • mysql/database_statements.rb (-8行)
  • postgresql/database_statements.rb (-4行)
  • sqlite3/database_statements.rb (-4行)

共通化前までは、これら各アダプタで returning_column_values をオーバーライドして同じ処理をしていましたが、その分岐ロジックを抽象アダプタに移したことで、DRY に保たれます。

2-2. Trilogy アダプタに関する説明

説明文中で特に強調されているのが、Trilogy アダプタだけが exec_insert の戻り値として「生のドライバ結果」を返している点です。

  • 他のアダプタ:
    • exec_insert の戻り値: ActiveRecord::Result
  • Trilogy:
    • exec_insert の戻り値: Trilogy::Result (生のドライバオブジェクト)

Trilogy は last_inserted_idresult.last_insert_id から読む実装になっており、そのため exec_insert の戻り値型がドライバ依存になっています。

さらに、test_exec_insert ( #26002 で追加されたテスト ) では、

ruby
connection.send(:last_inserted_id, exec_insert(...))

が動作することを前提にしており、結果として:

  • 「Trilogy の exec_insert はドライバ固有オブジェクトを返す」という挙動がテストで固定されている
  • そのため「exec_insert は常に ActiveRecord::Result を返すように統一する」という方向のリファクタリングが、今すぐにはやりづらい状態

であることがコメントとして明示されています。

このPR自体はその仕様を変えていませんが、「今後 exec_insert 自体をなくす(より高レベルなAPIに統一する)までは、Trilogy 固有の戻り値仕様は維持される」という前提が整理されています。


  1. 影響範囲・注意点

3-1. 実行時挙動の変化

通常のアプリケーションコードにとっては、挙動はほぼ変わりません。

  • INSERT ... RETURNING をサポートするDB (PostgreSQL、SQLite3の一部設定など):
    • returning_column_values は、これまで同様 result.rows.first を返す
  • RETURNING 非対応DB (典型的には MySQL 系):
    • returning_column_values は、[last_inserted_id(result)] を返す
    • last_inserted_id の中身はアダプタ依存(mysql2 / Trilogy は LAST_INSERT_ID() やドライバの last_insert_id を参照)

ロジックが抽象クラスに移動しただけなので、正しくアダプタが supports_insert_returning? を実装していれば、今までと同じ結果が得られます。

3-2. アダプタ拡張・独自アダプタへの影響

独自アダプタを書いている場合や、Forkしたアダプタを持っている場合は以下を確認するとよいです。

  1. supports_insert_returning? を適切に実装しているか

    • true: exec_insert の戻り値は ActiveRecord::Result であり、result.rows.first から返すべき値が取得できること。
    • false: last_inserted_id(result) が正しく ID を解決できること。
  2. 以前 returning_column_values を独自にオーバーライドしていた場合

    • 今回の共通化により、継承元の実装が変わる可能性があるので、挙動差が出ていないか確認が必要です。
    • 特に「複数カラムを RETURNING している」「主キー以外の値も返したい」などのユースケースで独自実装していた場合は要注意です(ただし現状の共通実装も rows.first をそのまま返すため、多くの場合は同等動作のはずです)。

3-3. Trilogy 利用者の注意点

  • このPRは Trilogy の exec_insert の戻り値型や last_inserted_id の仕様を変えていません。
  • ただし、説明にある通り「Trilogy は唯一の生ドライバ結果を返すアダプタである」という前提が明文化され、将来的に exec_insert は API として整理される(別の高レベル API に置き換えられる)可能性が示唆されています。
  • 独自に exec_insert を直接叩いてドライバに依存したコードを書いている場合、将来の変更で壊れやすい領域なので、極力 ActiveRecord の高レベルAPI(create, insert, insert_all など)に寄せておくと安全です。

  1. 参考情報 (あれば)
  • このPR:
  • 言及されている過去PR:
  • 関連するクラス / メソッド:
    • ActiveRecord::ConnectionAdapters::DatabaseStatements#returning_column_values
    • ActiveRecord::ConnectionAdapters::DatabaseStatements#supports_insert_returning?
    • ActiveRecord::ConnectionAdapters::DatabaseStatements#last_inserted_id
    • Trilogy アダプタの exec_insert / Trilogy::Result#last_insert_id

#58277 Remove unreachable single-version branch from DefaultSchemaVersionsFormatter

マージ日: 2026/7/28 | 作成者: @55728

  1. 概要 (1-2文で)
    DefaultSchemaVersionsFormatter#format にあった「単一バージョン用」のフォールバック分岐(実際には到達不能)を削除し、常に配列を前提としたマルチ行 INSERT ... VALUES のみを使うようにしたPRです。スキーマダンプの出力内容や公開APIには一切影響せず、内部実装のデッドコード整理になります。

  1. 変更内容の詳細

背景と問題点

DefaultSchemaVersionsFormatter#format は、schema_migrations テーブルから読み出したバージョン配列を SQL に整形する内部クラスです。

呼び出し経路は以下のようになっています:

  • dump_schema_versions
    insert_versions_sql
    DefaultSchemaVersionsFormatter#format

dump_schema_versions は常に「バージョンの配列」を insert_versions_sql に渡し、そのまま DefaultSchemaVersionsFormatter#format に渡されます。そのため、format が「単一バージョン(非配列)」を受け取る経路は実在しません。

にもかかわらず、過去の互換性を引きずったコードとして、

  • 引数 versions が Array でない場合(単一バージョン)のフォールバック
  • それに対応する単一行 INSERT 生成ロジック

が残っていました。

履歴を辿ると:

  • fd87169eb1
    マルチ行 INSERT をサポートしないアダプタ向けに、1バージョンごとに別々の INSERT を出していた時代の分岐を導入。
  • d1a74c1e01
    「単一バージョンを渡す」最後の呼び出し経路を削除。
  • 0cc1842d38
    コードを DefaultSchemaVersionsFormatter クラスへ移動した際、既に不要になっていた分岐をそのまま移植。

という経緯で、使われないフォールバックだけが内部クラスに残り続けていました。

今回の具体的な変更

activerecord/lib/active_record/migration/default_schema_versions_formatter.rb の中で、

  • versions が Array でない場合の分岐」を削除
  • 単一バージョンを想定したコードパスを削除
  • 残ったコードは「配列のバージョンからマルチ行 VALUES を組み立てる」パスのみ

という形に整理されています。

イメージ的には:

ruby
# 変更前(概略イメージ)
def format(versions)
  if versions.is_a?(Array)
    # マルチ行 INSERT ... VALUES
  else
    # (過去の名残)単一バージョン用のフォールバック
  end
end

ruby
# 変更後(概略イメージ)
def format(versions)
  # 常に配列を前提としたマルチ行 INSERT ... VALUES
end

のような形になっています。

PR本文でも明言されている通り、「生成されるスキーマダンプは変更前と完全に同じ内容」になるよう、振る舞いは維持されています(到達しないコードだけを削除したため)。


  1. 影響範囲・注意点
  • ActiveRecord内部 (:nodoc: クラス) のみの変更
    DefaultSchemaVersionsFormatter 自体は内部API扱いのクラスであり、アプリケーションコードから直接利用することは想定されていません。通常のRailsアプリでは、この変更に伴う挙動の変化はありません。

  • スキーマダンプの内容は不変
    schema:dump などで出力される SQL の内容は、PR前後で同一です。単に「使われていない条件分岐がなくなった」だけです。

  • 公開された拡張ポイントには影響なし

    • 公開API: config.active_record.schema_versions_formatter
    • ドキュメントでも「バージョン配列を受け取るフォーマッタ」を前提としており、今回の変更でもその前提は維持されています。
    • カスタムフォーマッタを実装している場合でも、これまで通り「バージョンの配列」を受け取るインターフェースに変化はありません。
  • もし影響が出るとしたら
    「内部クラス DefaultSchemaVersionsFormatter を直接叩き、かつ非配列(単一のバージョン)を渡していた」ような非想定の使い方をしていた場合は、これまで“たまたま動いていた”フォールバックに依存していた可能性があります。ただし、:nodoc: であり、かつ呼び出し経路的にも想定外なため、通常の利用では該当しません。


  1. 参考情報 (あれば)
  • PRで言及されているコミット履歴

    • fd87169eb1
      – 単一バージョン用分岐が導入された起点。マルチ行 INSERT をサポートしないアダプタ配慮時代のコード。
    • d1a74c1e01
      – 単一バージョンを渡す最後の呼び出し経路を削除。
    • 0cc1842d38
      – コードを DefaultSchemaVersionsFormatter クラスに移動し、その際に不要な分岐を一緒に持ち込んだ。
  • 設定ガイド(schema_versions_formatter)
    Railsガイドの「設定・初期化」あたりで紹介されている config.active_record.schema_versions_formatter の説明に、「フォーマッタはバージョン配列を受け取る」前提が記載されています。このPRはその仕様と実装をより一致させた形になります。


#58282 Add prefetch_primary_key? test coverage on PostgreSQL

マージ日: 2026/7/28 | 作成者: @kamipo

  1. 概要 (1-2文で)
    PostgreSQL アダプタに next_sequence_value が実装され、prefetch_primary_key?true にしたモデルでも、実際の PostgreSQL シーケンスを使って主キーを事前採番できるようになりました。加えて、その挙動を end-to-end で検証するテストが追加され、今後の insert / _insert_record リファクタリングや oracle-enhanced アダプタとの互換性を確認しやすくなっています。

  1. 変更内容の詳細

2-1. PostgreSQL アダプタへの next_sequence_value 実装

変更ファイル:

  • activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb

PostgreSQL アダプタに、以下のようなメソッドが新規実装されています(概念的にはこんなイメージです):

ruby
def next_sequence_value(sequence_name)
  select_value("SELECT nextval(#{quote(sequence_name)})")
end

ポイント:

  • 目的:
    ActiveRecord::Base#prefetch_primary_key?true にしたとき、INSERT 実行前に主キーを Ruby 側で取得するための API として next_sequence_value を提供。
  • これまでとの違い:
    • 以前は Oracle enhanced adapter だけが next_sequence_value/prefetch_primary_key? を利用しており、PostgreSQL では公式にはサポートされていなかった。
    • この PR により、PostgreSQL でも DB のシーケンスを使った事前採番 がサポートされる。

prefetch_primary_key?true にしたモデルでは、INSERT 前に以下のような流れになります:

  1. アダプタの next_sequence_value でシーケンス値を取得
  2. 取得した値をレコードの id にセット
  3. その idINSERT を実行

これにより、DB 側のシーケンスと Rails 側の主キー採番を明示的に同期させることができます。


2-2. PostgreSQL 用テストの追加

変更ファイル:

  • activerecord/test/cases/adapters/postgresql/prefetched_primary_key_test.rb

ここでは、prefetch_primary_key? 経由のフローを end-to-end で検証 するテストが追加されています。テストが確認しているポイントは以下の通りです。

  1. INSERT 前にシーケンスが進むこと

    • next_sequence_value を呼び出し、実際に PostgreSQL のシーケンス (nextval(...)) が進むことを検証。
  2. 事前取得した値が実際の行の id になること

    • レコードを保存し、DB から読み出したときに、
      • id が事前に取得したシーケンス値と一致するか を確認。
  3. 明示的に指定した id が優先されること (||= の挙動)

    • _insert_record 内で、id の設定に ||= が使われているため、
      • 事前採番された id があっても、
      • ユーザーがレコードに明示的に id を設定した場合は、明示的な id が優先される
    • テストでは「prefetch した id があっても、明示的な id を指定した場合はそちらで INSERT される」ことを確認している。

概ね以下のようなイメージのテストが入っています(疑似コード):

ruby
class PostgresqlPrefetchedPrimaryKeyTest < ActiveRecord::PostgreSQLTestCase
  class PrefetchedPkModel < ActiveRecord::Base
    self.table_name = "..." # テスト用テーブル
    def self.prefetch_primary_key?
      true
    end
  end

  def test_prefetched_primary_key_uses_sequence
    record = PrefetchedPkModel.create!
    # INSERT 前に next_sequence_value を呼んだ結果が record.id になっていることを確認
  end

  def test_explicit_id_overrides_prefetched
    record = PrefetchedPkModel.new(id: 123)
    record.save!
    assert_equal 123, record.id
  end
end

これにより、以下が保証されます:

  • prefetch_primary_key?true でも、ユーザーが id を手動で設定するとそちらが優先される。
  • PostgreSQL アダプタの next_sequence_value が、ActiveRecord 本体の期待するインターフェイス通りに動作している。

  1. 影響範囲・注意点

3-1. 影響範囲

  • PostgreSQL + ActiveRecord で、prefetch_primary_key? を true にしているモデル

    • これまでは半ば未サポート・非公式な状態だったパターンが、公式にサポートされる方向。
    • これにより、「Oracle では動くが PostgreSQL ではダメ」という状況がやわらぐ。
  • oracle-enhanced adapter の検証

    • oracle-enhanced アダプタは prefetch_primary_key? に強く依存しており、
      このテストは「ActiveRecord 本体側の変更で oracle-enhanced が壊れていないか」を担保するためにも使われる。
    • 今回は PostgreSQL 用のテストだが、「ActiveRecord が prefetch_primary_key? 経由でどう insert するか」の仕様を固める役割を持つため、サードパーティアダプタにとっても重要なテスト。
  • insert / _insert_record のリファクタリング

    • PR 説明文にもある通り、「今後 insert / _insert_record を整理・変更する際に、このテストが回帰テストとして効く」ことを狙っている。
    • つまり、将来の内部実装変更でも、このテストが通り続ける限り prefetch_primary_key? の契約は保たれる。

3-2. 注意点

  • prefetch_primary_key? を使う場合:

    • モデルで self.primary_key と実際の PostgreSQL シーケンスの名前や型が正しく対応している必要があります。
    • 手動でシーケンスをいじっている環境では、
      • 事前採番 + DB 側のデフォルトシーケンス、の二重管理にならないよう注意が必要です(通常は ActiveRecord 側がシーケンスを使うので問題にはなりにくいですが、カスタム定義している場合は要確認)。
  • 明示的な id を使う場合:

    • id を自前で設定して保存した場合、シーケンス側のカウンタがその値を追い越していないと、将来シーケンスが同じ値を発行して一意制約違反になる可能性があります(これは PostgreSQL の一般的な注意点)。
    • 大量に「明示的な id を指定して INSERT」を行う設計にする場合は、
      setval でシーケンスを調整するなどの運用の一貫性を考える必要があります。

  1. 参考情報 (あれば)

#58244 Use latest ruby patch version in dockerfile

マージ日: 2026/7/28 | 作成者: @adityapandit17

  1. 概要 (1-2文で)
    Rails リポジトリの開発用コンテナ(.devcontainer)の Dockerfile で使用している Ruby のパッチバージョンが、最新の 4.0.6 に更新されました。機能追加ではなく、開発環境における Ruby バージョンのメンテナンス更新です。

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

  • 対象ファイル: .devcontainer/Dockerfile
  • 変更点は 1 行のみで、利用する Ruby イメージのバージョン指定が更新されています。

典型的には以下のような変更が入っています(イメージ例):

diff
-FROM ruby:4.0.5
+FROM ruby:4.0.6

あるいはタグ付きバリアントを使っている場合も同様です:

diff
-FROM ruby:4.0.5-bookworm
+FROM ruby:4.0.6-bookworm

要するに、開発用コンテナが起動する際に取得される Ruby ベースイメージが「4.0.5 → 4.0.6」に切り替わるだけの変更です。

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

    • VS Code Dev Containers や GitHub Codespaces、その他 .devcontainer/Dockerfile を利用して Rails を開発している環境に影響します。
    • CI などで .devcontainer/Dockerfile を直接使っている場合も Ruby 4.0.6 で実行されるようになります。
  • 実務的な影響

    • Ruby 4.0.6 は 4.0 系のパッチリリースであり、後方互換性を保ちつつ、バグ修正・パフォーマンス改善・セキュリティ修正が含まれている可能性があります。
    • パッチレベルの更新のため、通常はアプリケーションコードの変更は不要ですが、以下のようなケースでは注意が必要です:
      • Ruby 本体の細かい挙動に依存したテスト(時間、GC、警告メッセージなど)
      • ネイティブ拡張を含む gem を使用しており、Ruby のパッチ更新でビルドが再実行される場合
    • 一度コンテナを再ビルド (docker compose build や Dev Container の「Rebuild Container」) すると、新バージョンの Ruby が反映されます。
  • 注意点

    • ローカルで Ruby 4.0.5 など別バージョンを使っている場合、.devcontainer 内では 4.0.6 になるため、微妙な差異で挙動がずれないかを確認しておくと安全です。
    • もし Gemfile.ruby-version で Ruby バージョンを厳密に固定している場合は、それらの指定と Dockerfile のバージョンが不一致になっていないか確認してください(4.0.6 に合わせるのが望ましいです)。
  1. 参考情報 (あれば)

この PR 自体はごく小さい変更ですが、「公式リポジトリが想定している Ruby バージョン」が 4.0.6 に更新された、という指標にもなります。


#58108 Allow to use thread module reader / writer in a ractor:

マージ日: 2026/7/28 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    このPRは、thread_mattr_reader / thread_mattr_writerthread_mattr_accessor)で定義されたスレッド単位のクラス属性を、Ractor 内からも安全に使えるようにする変更です。Ractor の分離制約に抵触していたクラスインスタンス変数のメモ化をやめる代わりに、毎回キーを計算するようにし、その代償としてごく小さいパフォーマンス低下を受け入れています。

  1. 変更内容の詳細

背景となる問題

従来の thread_mattr_* は、内部的に「スレッドローカルストレージにアクセスするためのキー」をクラスインスタンス変数にメモ化していました。
イメージとしては以下のようなことをしていたと考えられます(実際の実装は簡略化):

ruby
class Request
  # thread_mattr_accessor :id の実装例イメージ
  @__thread_local_key_for_id ||= "Request:id"

  def self.id
    Thread.current[@__thread_local_key_for_id]
  end

  def self.id=(value)
    Thread.current[@__thread_local_key_for_id] = value
  end
end

Ractor では「Ractor 間で共有されるオブジェクト」が厳しく制限されており、クラスインスタンス変数に保持されたメモ化オブジェクトが、その制約に引っかかって Ractor::IsolationError を引き起こしていました。

ユーザーコードの例:

ruby
class Request
  thread_mattr_reader :id
  thread_mattr_writer :id
end

Request.id = 123

Ractor.new { Request.id } # => Ractor Isolation Error(PR 前)

今回の修正内容

1. メモ化の削除

activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb で行っていた「スレッドローカルキーのメモ化(クラスインスタンス変数に保持)」をやめ、毎回キーを計算する実装に変更しています。

大まかな方針は次のようなイメージです:

  • 以前:
    • @_short_circuit_key のようなクラスインスタンス変数にキーをキャッシュ
  • 変更後:
    • 毎回 :"#{name}_#{object_id}""#{self.name}/#{attr_name}" 等の規則に基づきキーを生成(実際のキー生成規則は Rails 内部実装に依存)

これにより、Ractor 内でクラスを参照しても、そのクラスが持つクラスインスタンス変数に Ractor 非対応オブジェクトが閉じ込められている、という状況を避けられます。

2. テストの追加

activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb にテストが追加され、少なくとも次のような動作が確認されるようになっています:

  • thread_mattr_accessor で定義された属性が Ractor 内から呼び出せること
  • Ractor をまたいでも Ractor ごとに独立した状態を保ちつつ、エラーを起こさないこと

コード例(イメージ):

ruby
class Request
  thread_mattr_accessor :id
end

Request.id = 123

r = Ractor.new do
  Request.id # Ractor 内から reader を呼ぶ
end

r.take # IsolationError が起きず正常に実行できること

3. パフォーマンスのトレードオフ

メモ化を外したことで、スレッドローカルキーの生成コストが毎回かかるようになり、micro benchmark では以下のように 1.6〜2倍 程度の遅さが観測されています。

  • setter (Request.id = ...):
    • メモ化あり: 約 8.8M i/s
    • メモ化なし: 約 5.6M i/s (約 1.6 倍遅い、1 回あたり ~180ns 程度)
  • getter (Request.id):
    • メモ化あり: 約 13.5M i/s
    • メモ化なし: 約 6.9M i/s (約 2 倍遅い、1 回あたり ~145ns 程度)

いずれも「1 回の呼び出しはナノ秒オーダー」であり、通常のアプリケーションにおいては、ループ内で超高頻度に呼ぶような特殊ケースを除けば実害は小さいと判断されています。


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

    • ActiveSupportthread_mattr_reader / thread_mattr_writer / thread_mattr_accessor を利用しているすべてのコード。
    • 特に、Ractor を使った並行処理(Ruby 3.0+)環境で、これらのメソッドを使う場合に挙動が改善されます。
    • テストレベルでは Ractor 内からの利用がカバーされているため、Ractor 環境での安全性は向上しています。
  • パフォーマンス面の注意

    • getter / setter ともに 1.6〜2倍程度遅くなっていますが、絶対値はナノ秒オーダーであり、ほとんどの Web アプリケーションでは体感できる差にはなりにくいです。
    • ただし、例えば「1 リクエスト中に何十万回も thread_mattr_* を叩くようなホットパス」で使っている場合は、パフォーマンスを計測して影響を確認したほうがよい可能性があります。
    • 「スレッドローカルにこだわらなくて良い」「Ractor も使っていない」ような場面では、普通のクラスインスタンス変数や cattr_* など、別の仕組みを検討しても良いかもしれません。
  • Ractor 利用時のメリット

    • thread_mattr_* を使った既存コードを、そのまま Ractor から呼び出しても IsolationError で落ちないようになります。
    • Ractor を併用した並列実行(例: 並列テストランナー、バックグラウンドジョブの分散実行など)で、Rails のスレッド属性をより安全に使えるようになるため、将来の並列化対応にとって重要な前提整備といえます。

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

    • activesupport/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb
      • thread_mattr_* の内部実装。ここからクラスインスタンス変数によるメモ化が削除され、毎アクセス時にキー計算を行うようになった。
    • activesupport/test/core_ext/module/attribute_accessor_per_thread_test.rb
      • Ractor 環境を含むテストが追加され、Ractor 内から thread_mattr_* アクセスが可能であることを検証。
  • Rub y バージョン:

    • ベンチマークは ruby 4.0.1 (2026-01-13 ...) で計測されているため、Ruby 4 系での Ractor 利用を念頭に置いた変更であると読み取れます。
  • 利用側で特別なマイグレーションは不要で、API 互換性は維持されたまま、内部実装のみが変更された PR です。


#58273 Fix Markdown doc check

マージ日: 2026/7/28 | 作成者: @flavorjones

  1. 概要 (1-2文で)
    Markdown ドキュメントチェック用スクリプトと RuboCop (Layout/EmptyLineAfterMagicComment) のスタイルルールが衝突していた問題を解消する PR です。# frozen_string_literal: true の直後の空行を、Markdown チェック側でも許容するように修正しています。

  2. 変更内容の詳細

  • 変更ファイル:
    • .github/workflows/scripts/check-markdown-api.rb(1行変更)

このスクリプトは、Markdown 内に記載された Ruby のコード例をチェックする用途で使われており、ファイル先頭のフォーマットについて独自のチェック(もしくは前提)を持っていました。

問題となっていたのは以下のようなパターンです:

ruby
# frozen_string_literal: true
class Example
  # ...
end

RuboCop の Layout/EmptyLineAfterMagicComment ルールは、上記を次のように空行を入れる形に自動修正します:

ruby
# frozen_string_literal: true

class Example
  # ...
end

しかし、Markdown ドキュメントのコードブロックを検証する check-markdown-api.rb は、この空行があると不正なフォーマットとして扱い、チェックを失敗させていました。

この PR では、その Markdown チェック側のロジックを修正し、frozen_string_literal マジックコメント直後の空行を「許容される形式」として扱うようにしています。
具体的には、ファイル先頭(もしくはコードブロック先頭)の判定条件やパターンマッチを 1 行だけ変更し、「空行あり/なし」の両方を受け入れるようにした、という内容です(+1/-1行のみ)。

※ PR 本文の「So let's make that blank line acceptable.」から、Rubocop の推奨スタイルを優先し、それに合わせて Markdown チェックを緩和・修正したことがわかります。

  1. 影響範囲・注意点
  • 影響範囲
    • GitHub Actions などで動く Markdown API チェック ワークフローにのみ影響します。
    • Rails リポジトリ内の Markdown ドキュメントに含まれる Ruby コード例(特に # frozen_string_literal: true を含むもの)が、Rubocop の自動整形後でもチェックエラーにならなくなります。
  • 注意点
    • これまで「Rubocop が直したコードだと Markdown チェックが落ちる」という CI 上のノイズが出ていた場合、この PR により解消されます。
    • Markdown 側のチェックがやや緩和される形ですが、影響するのはマジックコメント直後の 1 行だけであり、実コードやその他スタイルには影響しません。
  1. 参考情報 (あれば)
  • 関連 PR:
    • #58257 — 問題の元になった Markdown doc チェック用スクリプトを導入した PR
  • 関連 RuboCop ルール:
    • Layout/EmptyLineAfterMagicComment
      • マジックコメント(# frozen_string_literal: true など)の直後に空行を入れることを要求するスタイルルール。

#58211 Pass scheme to token authentication block in http_atthentication

マージ日: 2026/7/28 | 作成者: @chad-cole

  1. 概要 (1-2文で)
    Rails の HTTP トークン認証 (ActionController::HttpAuthentication::Token) において、これまで捨てられていた「スキーム(Bearer など)」情報をブロックへ渡せるようにし、かつコントローラ側で特定スキームを必須とできるようにした PR です。これにより、DPoP などスキーム固有の検証が必要な仕様に対応しやすくなります。

  1. 変更内容の詳細

2-1. スキーム情報をブロックに渡すように変更

これまで authenticate_or_request_with_http_token などのメソッドは、以下のように「token」と「options」までしかブロックに渡していませんでした。

ruby
authenticate_or_request_with_http_token do |token, options|
  # token 認証処理
end

この PR により、第3引数として「スキーム(scheme)」をシンボルで受け取れるようになります:

ruby
authenticate_or_request_with_http_token do |token, options, scheme|
  # 例: "Bearer" -> :bearer, "DPoP" -> :dpop のように正規化されたシンボル
  # scheme を見て処理を分岐できる
end
  • Authorization: Bearer abc123scheme:bearer
  • Authorization: DPoP xyz456scheme:dpop
  • スキーム名は正規化(大文字 / 小文字)され、シンボルで渡される

既存コードとの互換性
第3引数はオプショナルなので、従来どおり2引数だけ受け取るブロックはそのまま動作します。

ruby
# 既存のコードもそのまま有効
authenticate_or_request_with_http_token do |token, options|
  # 変更不要
end

2-2. スキーム必須機能の追加

この PR では、コントローラ側で「特定のスキームのみ受け付ける」ことができるようにするための仕組みが追加されています。
(テストやコード上の意図からの説明なので、メソッド名は概念的なものとして読んでください)

イメージとしては、以下のような使い方が可能になります:

ruby
# 例: Authorization ヘッダが DPoP スキームであることを要求したい場合
authenticate_or_request_with_http_token(scheme: :dpop) do |token, options, scheme|
  # scheme == :dpop であることはフレームワーク側で保証されている想定
  # ここで DPoP 特有の検証 (DPoP 証明ヘッダのチェックなど) を行える
end

あるいは、複数スキームをサポートする実装も可能です:

ruby
authenticate_or_request_with_http_token do |token, options, scheme|
  case scheme
  when :bearer
    authenticate_bearer(token, options)
  when :dpop
    authenticate_dpop(token, options)
  else
    false
  end
end

スキーム要件を満たさない場合、コントローラは RFC 9110 に沿った適切な WWW-Authenticate チャレンジレスポンスを返せるようになります。
PR ではこの「スキーム要件を満たさない」ケースを検知する仕組みを HttpAuthentication::Token レイヤに追加しています。

2-3. DPoP 対応のための前提整備

今回の PR 自体は DPoP (RFC 9449) の実装そのものは行っていません
代わりに:

  • トークンスキーム情報をアプリケーション層(コントローラのブロック)に届ける
  • コントローラから特定スキームを要求できる

という「DPoP 実装のための土台」を用意しています。

DPoP では以下のようなスキーム固有の検証が必要になります:

  • Authorization: DPoP <access token> の形式で送られていること
  • 別ヘッダ DPoP: <proof JWT> の検証(署名、nonce, iat, htm, htu など)

そのため、Rails が Authorization ヘッダを解析した時点で「スキーム名を落としてしまう」と実装が難しく、今回の変更でアプリからスキームを見られるようにした、という位置づけです。

2-4. テスト・CHANGELOG の更新

  • actionpack/test/controller/http_token_authentication_test.rb に以下を含むテストが追加:
    • ブロックに scheme が渡されることの確認
    • 特定スキーム必須時の挙動の確認
    • (Bearer / DPoP など)複数スキームの扱いのテスト
  • actionpack/CHANGELOG.md に、この機能追加が明記されています。

  1. 影響範囲・注意点
  • 後方互換性

    • ブロック引数は第3引数が追加されただけで、第1・第2引数の意味は変わっていないため、既存コードは基本的にそのまま動作します。
    • 引数を2つだけ受け取っているブロックは、そのままで問題ありません。
  • 認証ロジックの拡張余地

    • 今後、Bearer と DPoP を同時サポートしたり、独自スキームを実装したりする際に、コントローラ側でスキームごとの処理を簡単に分岐できるようになります。
    • スキームに応じて WWW-Authenticate ヘッダを変える(例: WWW-Authenticate: DPoP, WWW-Authenticate: Bearer)といった RFC 9110 的に正しい応答を返しやすくなります。
  • 注意点

    • スキームの扱いは正規化されたシンボルとして渡されることを前提に実装する必要があります(例: :bearer, :dpop)。文字列のまま "Bearer" を期待したコードを書くと動きません。
    • 将来的に DPoP を導入する場合は、AuthorizationDPoP の両ヘッダを使った検証ロジックを自前で組む必要があり、その際にこの PR で追加された scheme 引数を活用することになります。

  1. 参考情報 (あれば)

#58276 [8-0-stable] Avoid loading ActionController::Live early in initializer

マージ日: 2026/7/28 | 作成者: @carldaws

  1. 概要 (1-2文で)
    Rails 8.0 系の 8-0-stable ブランチに対して、ActionController::Live が Railtie 初期化処理の中で「早いタイミングでロードされてしまう」問題を避ける修正をバックポートした PR です。これにより、ライブストリーミング機能に関連するオートロード・スレッド・設定タイミング周りの問題が軽減されます。

  1. 変更内容の詳細

※元 PR #58147 のバックポートであり、そちらの内容が 8.0 向けに反映されたものです。

a. ActionController::Live のロードタイミングの調整

対象ファイル:

  • actionpack/lib/action_controller/metal/live.rb
  • actionpack/lib/action_controller/railtie.rb

従来は、ActionController::Railtie の initializer の中で ActionController::Live が結果的に「早期に」ロードされる経路があり、以下のような問題が起こり得ました:

  • アプリケーション設定(特にスレッドやオートロード関連)が完全に確定する前に、ActionController::Live がロードされる
  • ライブストリーミングに関わるミドルウェアやモジュールが、想定より早く有効化される
  • Zeitwerk オートロードのフローや、config.eager_load のタイミングと噛み合わないケースで警告やエラーが発生する可能性

本 PR では:

  • ActionController::Railtie 内で ActionController::Live を直接参照/ロードしないようにし、
  • 必要になるまで定数を解決しない(遅延ロード)方向に寄せる

といった形で、「初期化フェーズにおける不要な自動ロードを避ける」 修正が入っています。

疑似的なイメージとしては:

ruby
# 以前 (イメージ)
initializer "action_controller.live" do
  ActionController::Live  # これが constantize されて早期ロードされる
end

# 修正後 (イメージ)
initializer "action_controller.live" do
  # ここでは具体的な参照を避け、必要になったときにロードされるようにする
end

実際のコードでは行数は小さい変更(+3/-1 程度)ですが、「どの initializer のタイミングでどのクラスをロードするか」 を整理する目的の修正です。

b. ActionController::Live 側の軽微な変更

対象ファイル:

  • actionpack/lib/action_controller/metal/live.rb (+2 行)

live.rb 側には、Railtie での遅延ロード戦略と噛み合うような小さな修正が入っています。
典型的には:

  • 依存するコンポーネントの require をより明示的にする
  • オートロード/eager load とコンフリクトしないような guard を入れる

といった性質の調整です(+2 行の小さな差分ですが、ロード順管理の一部になっています)。

c. ドキュメント更新

対象ファイル:

  • actionpack/CHANGELOG.md
  • guides/source/configuring.md

CHANGELOG.md:

  • Rails 8.0 系に対して、「ActionController::Live の early load を避ける修正」を記録するエントリが追加されています。
    → Rails 8.0 を使っていて関連する問題を踏んでいた人向けの「このバージョンから直りました」という情報。

guides/source/configuring.md:

  • ライブストリーミング(ActionController::Live)およびその設定/初期化タイミングに関して、ドキュメント上の説明が補足・更新されています。
    • たとえば「config でこう設定しておけば、ActionController::Live は必要なときにだけ有効化される」といった文脈の追記がされている可能性が高いです。

d. テスト追加

対象ファイル:

  • railties/test/application/configuration_test.rb (+25 行)

configuration_test.rb にテストが 25 行追加され、次のような振る舞いを検証しています:

  • アプリケーション起動時に ActionController::Live勝手にロードされないこと
  • 特定の設定をした場合のみ、ActionController::Live がロード/有効化されること
  • 8.0 の設定 API (config.*) との組み合わせで、想定されたタイミングでのみ定数が解決されること

典型的には、以下のようなアサーションが含まれていると考えられます:

ruby
test "action controller live is not loaded during initialization" do
  app = build_app
  # 初期化後でも ActionController::Live がまだ未ロードであることを確認
  assert_not defined?(ActionController::Live)
  
  # 実際に使おうとしたときにロードされる
  get "/some_live_endpoint"
  assert defined?(ActionController::Live)
end

(実際のコードは異なりますが、狙っているテストの方向性はこうしたものです。)


  1. 影響範囲・注意点

影響範囲

  • 対象: Rails 8.0 系で ActionController::Live を利用している、または利用可能な状態にしているアプリケーション
  • 影響する面:
    • アプリケーション起動時のロード順序
    • Zeitwerk オートロードの挙動
    • 初期化時(initializer)の副作用・定数解決

開発者視点でのポイント

  1. 初期化時の副作用が減る

    • ActionController::Live に依存したコードが「アプリ起動時に必ず実行される」状況が緩和されます。
    • 起動時のログ/メモリ使用量/スレッド生成などが、若干クリーンになる可能性があります。
  2. カスタム initializer 内での ActionController::Live 参照に注意

    • 自前の initializer で ActionController::Live を直接参照している場合、その時点で早期ロードを引き起こす点は変わりません。
    • 「可能な限り遅延ロードしたい」場合は、Rails.application.config.to_prepare やリクエスト発生以降のフック・lazy constantization の利用などに寄せると、今回の方針と整合します。
  3. Zeitwerk や auto_load の警告/エラーが減る可能性

    • 以前、Rails 8.0 に上げた際に「ActionController::Live 周りでオートロードや定数解決に関する警告・エラーが出ていた」ケースでは、この修正で解消されている可能性があります。
  4. 動作上の互換性

    • 機能としての ActionController::Live の挙動(ストリーミングそのもの)は変わりません。
    • 変わるのは「いつロードされるか」というライフサイクルの問題であり、通常の利用者にとっては後方互換的です。

  1. 参考情報 (あれば)
  • 元 PR: #58147 — 本 PR のオリジナル修正。詳細設計や議論はここを見るとわかりやすいです。
  • Issue: #58145 — early load による問題報告。どのような症状が出ていたかの具体例が確認できます。
  • 対象ブランチ: 8-0-stable — Rails 8.0 系を利用している場合、この修正を含むパッチバージョンへのアップデートで問題回避が期待できます。

#58272 Fix the Parameters#deep_transform_values doc example

マージ日: 2026/7/28 | 作成者: @55728

  1. 概要 (1-2文で)
    ActionController::Parameters#deep_transform_values のドキュメント内サンプルの戻り値が、実際の挙動では起こり得ない形になっていたため、現実の挙動に合わせて修正したPRです。機能自体は未リリースのため、挙動変更ではなく「誤ったドキュメントの修正」に留まります。

  1. 変更内容の詳細

何が問題だったか

元のドキュメントでは、以下のようなサンプルが記載されていました(コメントが「ドキュメント上の想定出力」):

ruby
params = ActionController::Parameters.new(
  user: { email: "  ALICE@EXAMPLE.COM  ", profile: { bio: "  Hello world  " } }
)
params.deep_transform_values { |v| v.is_a?(String) ? v.strip.downcase : v }

# documented
# => #&lt;ActionController::Parameters {"user"=>#&lt;ActionController::Parameters {"email"=>"alice@example.com", "profile"=>#&lt;ActionController::Parameters {"bio"=>"hello world"} permitted: false>} permitted: false>} permitted: false>

ここでは user の値や profile の値もすでにネストした ActionController::Parameters オブジェクトとして表示されています。

しかし実際の挙動は次の通りで、ネストした部分は「ただの Hash」として表示されます:

ruby
# actual
# => #&lt;ActionController::Parameters {"user"=>{"email"=>"alice@example.com", "profile"=>{"bio"=>"hello world"}}} permitted: false>

なぜ違いが出るのか

ActionController::Parameters は「ネストした Hash を最初からすべて Parameters にラップしているわけではなく」、アクセスされたタイミングで遅延的に wrap する実装になっています。

そのため、deep_transform_values の戻り値を inspect した時点では、内部のネストした構造は通常の Hash のままであり、ドキュメントにあるような「入れ子の #&lt;ActionController::Parameters ...>」という表示になることはありません。

このPRでの具体的な修正

変更されたファイルは2つのみです。

  • actionpack/lib/action_controller/metal/strong_parameters.rb
    • deep_transform_values のドキュメントコメント内のサンプル出力が、実際の inspect 結果と同じ形に修正されています。
    • 例:
      • before(誤った例): {"user"=>#&lt;ActionController::Parameters {...}>}
      • after(正しい例): {"user"=>{"email"=>"alice@example.com", "profile"=>{"bio"=>"hello world"}}}
  • actionpack/CHANGELOG.md
    • deep_transform_values に関する記述の例出力も、同じく実際の出力形式に合わせて1行修正。
    • " => "" =>" にそろえるなど、表記の統一も行われています。

変更は合計で +2/-2 行と、ごく小さいドキュメント修正です。


  1. 影響範囲・注意点
  • 機能面の変更は一切なく、deep_transform_values の挙動は従来どおりです。
  • deep_transform_values 自体がまだ未リリース(#57340)であり、この誤ったドキュメントが公開版に含まれたことはありません。
  • そのため、
    • 既存アプリケーションへの互換性影響: なし
    • 今後 deep_transform_values を使う開発者にとって:
      • ドキュメントどおりに inspect の出力を期待してデバッグしても、実際と一致するようになります。
      • ネストが「自動で全部 Parameters になる」という誤解を招かなくなります。

注意点として、今後 Parameters を扱う際に、

  • 「ネストしたキーも常に ActionController::Parameters になる」とは限らず、
  • 実際には「アクセス時にラップされることがある」という遅延ラップの仕様を意識する必要があります。

これは deep_transform_values に限らず、Strong Parameters 全般の性質です。


  1. 参考情報 (あれば)
  • 対象メソッド: ActionController::Parameters#deep_transform_values
    (Ruby の Hash#deep_transform_values に類似した API で、Strong Parameters 用のもの)
  • 関連PR:
    • #57340 — deep_transform_values の追加PR(このメソッド自体の導入)
  • Strong Parameters の遅延的な Parameters ラップ挙動については、ActionController::Parameters の実装および既存のメソッド(require, permit など)のソースを読むと理解しやすいです。

#58235 Remove stale requires

マージ日: 2026/7/28 | 作成者: @55728

  1. 概要 (1-2文で)
    このPRは、既にコードから使われていない require をまとめて削除し、不要な依存関係の読み込みを減らすクリーンアップです。挙動や公開APIの変更はなく、autoload に任せられる部分と参照されていない標準ライブラリの require を整理しています。

  1. 変更内容の詳細

2-1. 各種 LogSubscriber からの active_support/log_subscriber の削除

対象ファイル:

  • actionmailer/lib/action_mailer/log_subscriber.rb
  • actionview/lib/action_view/log_subscriber.rb
  • activejob/lib/active_job/log_subscriber.rb
  • activestorage/lib/active_storage/log_subscriber.rb

これら4つのログサブスクライバは、いずれも以下のように記述されていました(イメージ):

ruby
require "active_support/log_subscriber"  # ← これが削除対象
require "active_support/event_reporter"

module ActionMailer
  class LogSubscriber < ActiveSupport::EventReporter::LogSubscriber
    # ...
  end
end

しかし、実際に継承しているのは ActiveSupport::EventReporter::LogSubscriber であり、ActiveSupport::LogSubscriber はどこでも参照していません。このクラスは active_support/event_reporter.rb の autoload によって読み込まれるため、active_support/log_subscriber を明示的に require する必要はありません。

LogSubscriber 系の他のファイルとの比較:

log subscriber ファイルrequire "active_support/log_subscriber"
active_record/log_subscriber.rbなし
action_controller/log_subscriber.rbなし
action_dispatch/log_subscriber.rbなし
action_mailer/log_subscriber.rbあり → 削除
action_view/log_subscriber.rbあり → 削除
active_job/log_subscriber.rbあり → 削除
active_storage/log_subscriber.rbあり → 削除

このPRで、この4つも他と同様に「不要な require を持たない」形に揃えています。


2-2. Active Storage Disk Service からの pathname / openssl の削除

対象ファイル:

  • activestorage/lib/active_storage/service/disk_service.rb

元々の先頭付近は概ね次のような構成になっていたと考えられます:

ruby
require "pathname"                                # ← 使われなくなった
require "openssl"                                 # ← 使われなくなった
require "fileutils"                               # ← 継続利用
require "active_support/core_ext/numeric/bytes"   # ← 継続利用

変更点:

  • Pathname はコミット c39e176eb5 以降、コード中で一切参照されていないため require "pathname" を削除。
  • OpenSSL もチェックサム処理が別の場所へ移動したコミット d1e31dd4f9 以降、未使用のため require "openssl" を削除。
  • 一方で、FileUtilsactive_support/core_ext/numeric/bytes は引き続きコード中で使用されているため残しています。

実行パスに影響するのはあくまで「不要なライブラリを読み込まなくなる」点だけで、Disk Service の I/O やチェックサムの挙動自体は別のコードにすでに移されているため、このPRでは変わりません。


2-3. Action Text インストールジェネレータからの json の削除

対象ファイル:

  • actiontext/lib/generators/action_text/install/install_generator.rb

このジェネレータでは、以前は JSON クラスを利用していたため:

ruby
require "json"       # ← 82e4432058 以降使われていない
require "pathname"   # ← 引き続き使用中

という状態でしたが、コミット 82e4432058JSON を使う処理が削除または書き換えられ、現在は JSON クラスを参照していません。そのため require "json" を削除し、まだ使っている pathname だけを残す形に整理しています。


  1. 影響範囲・注意点
  • ランタイム挙動:

    • いずれの変更も「参照されていない定数を読み込むだけだった require を削除」するものであり、実際の処理フローや公開API、ログ出力内容などには影響しません。
    • autoload 経由で読み込まれている ActiveSupport::EventReporter::LogSubscriber は引き続き正常に機能します。
  • 依存関係・初期化順序:

    • 以前から ActiveSupport::LogSubscriber に依存していなかったコード群なので、初期化順序の問題や依存解決の問題は発生しません。
    • もしアプリ側が「これらのファイルが active_support/log_subscriber を副作用的に require してくれる」ことに暗黙依存していた場合のみ挙動が変わる可能性がありますが、通常はそのような前提は推奨されず、実務上もほぼない前提と考えられます。
  • パフォーマンス・メモリ:

    • 読み込まれるライブラリが少し減るため、起動時のロード時間やメモリ使用量にわずかに良い影響がありますが、体感できるほどではないはずです。
  • gem / ライブラリ作者視点:

    • Rails 内部の require が減ったことで、もし自作 gem が「そのファイルを require した結果として副次的に pathname / openssl / json / active_support/log_subscriber が読まれること」に依存している場合は、自分の側で明示的に require する必要があります(ただし、そのような設計は避けるべきです)。

  1. 参考情報 (あれば)
  • 本PRで参照されている過去コミット:
    • Pathname が不要になった: c39e176eb5
    • Disk Service からチェックサム処理が移動した: d1e31dd4f9
    • Action Text install generator から JSON 利用が消えた: 82e4432058
  • 類似の内部整理:
    • すでに active_record, action_controller, action_dispatch の各 log_subscriberactive_support/log_subscriber を require しておらず、今回の変更はそれらと仕様を揃える位置づけ。

#58275 Don't swallow ::RangeError on write queries

マージ日: 2026/7/28 | 作成者: @kamipo

  1. 概要 (1-2文で)
    QueryIntent#run_query! が全クエリで ::RangeError を握りつぶしていた問題を修正し、SELECT では従来どおり「範囲外 WHERE → 空結果」を維持しつつ、INSERT/UPDATE/DELETE などの書き込みクエリでは RangeError / ActiveModel::RangeError が正しく表に出るようにした PR です。これにより、本来シリアライズ時に発生すべき範囲外エラーが隠蔽されることなく、意図どおり例外として扱えるようになります。

  1. 変更内容の詳細

これまでの挙動と問題点

  • QueryIntent#run_query! 内で ::RangeError を rescue し、主に SELECT の WHERE 句で範囲外の値を指定した場合に例外ではなく「結果 0 件」にする、という挙動が実装されていました。
    • これは元々 select_all の近くでのみ行われており(0601a9f9aa)、読み取り専用の SELECT 問い合わせ向けの仕様でした。
  • その後のリファクタリング(9051bc4332 など)で rescue が QueryIntent#execute!run_query! に移動した結果、
    INSERT/UPDATE/DELETE 等の書き込みクエリにも同じ RangeError rescue が効いてしまう状態になっていました。

ここで問題になるのが、書き込みクエリ + Arel 使用時です。

  • 書き込みクエリで Arel ノードを使うと、以下の流れになります:

    • execute_intent
      processed_sql
      compile_arel!(この中で type/serialize が呼ばれる)
  • ここで属性の serialize などから ActiveModel::RangeError::RangeError のサブクラス)が発生しても、
    run_query! の rescue 範囲の中で発生するため、例外が握りつぶされる状態になっていました。

  • その結果、本来は「シリアライズ時の範囲外値」という意味の ActiveModel::RangeError が外に出ず、
    代わりに後続の intent.affected_rows 呼び出し時に:

    text
    RuntimeError: "Cannot call affected_rows after cast_result has been called"

    といった、文脈的に分かりづらい RuntimeError だけが表面化していました。

  • _exec_insert だけは、以下のように

    • intent.execute! を呼ぶ前に intent.raw_sql を先に読む (= arel コンパイルが先に走る)
    • そのため、シリアライズ時の RangeErrorrun_query! の rescue 範囲の外側で起きる
      という偶然により、問題を免れていました。
  • 一方で update / update_with_result / delete は、そのような「先に raw_sql を読む」処理をしていなかったため、上記の問題の影響を受けていました。

今回の修正内容

  • QueryIntent#run_query! 内で ::RangeError を rescue している箇所に、
    そのクエリが「書き込み(INSERT/UPDATE/DELETE)」かどうかで分岐し、書き込みの場合は RangeError を再スローするロジックを追加しています。
    • 読み取り(SELECT)は従来通り、「範囲外 → 空結果」の挙動を維持。
    • 書き込みは「範囲外 → 例外発生」となるように戻しています。

※ PR の diff 自体は query_intent.rb に +1 行という小さな変更ですが、
中身としては「rescue 範囲の中で、書き込みクエリの場合は RangeError をそのまま再度 raise する」条件分岐が入っています。

テストの追加・変更

  • activerecord/test/cases/adapter_test.rb
    • 書き込みクエリに対して、RangeError / ActiveModel::RangeError が正しく発生することを確認するテストを追加。
    • 特に、Arel 経由の書き込み時に例外が握りつぶされず、元の例外が観測できることをテストしています。
  • activerecord/test/cases/adapters/abstract_mysql_adapter/unsigned_type_test.rb
    • MySQL 系アダプタでの unsigned 型など「範囲が厳密な型」に対し、
      範囲外の値を書き込もうとした場合に ActiveModel::RangeError が適切に発生することを確認するテストが追加・強化されています。

  1. 影響範囲・注意点

影響を受けるケース

  • 対象: Rails 7 系(あるいはそれ以降)の Active Record で、以下の条件を満たすコード:
    • Arel ノードを使って UPDATE / DELETE などの書き込みクエリを発行している
    • カラムに対して範囲制約のある型 (unsigned int など) や、
      カスタム type / serialize ロジックを使っており、その中で ActiveModel::RangeError が起こり得る
  • これまでは:
    • 本来 ActiveModel::RangeError が起きるべきところで例外が握りつぶされ、
    • 代わりに intent.affected_rows 呼び出し時の RuntimeError("Cannot call affected_rows ...") のみが見えていた
  • この PR 適用後は:
    • 書き込みクエリにおいては ActiveModel::RangeError / RangeError がそのまま表に出るようになります。
    • そのため、これまで「よく分からない RuntimeError」として扱っていたケースが、「範囲外エラー」としてより意味のある形で見えるようになります。

後方互換性・挙動の違い

  • SELECT クエリ(読み取り):
    • WHERE 句に範囲外の値を渡した場合、「例外ではなく空結果を返す」という挙動はそのままです。
  • INSERT/UPDATE/DELETE などの書き込みクエリ:
    • 範囲外の値やシリアライズ時のエラーは 例外として表に出る 挙動に統一されます。
    • 過去に一部アプリケーションが「RuntimeError を rescue していた」ような場合は、
      今後は ActiveModel::RangeError / RangeError を明示的に rescue する必要が出てくるかもしれません。
  • 全体としては、「本来バグだった例外の握りつぶしをやめ、仕様に沿った挙動に戻す修正」であり、
    意図しないサイレント失敗が減るため、多くのアプリケーションにとっては望ましい変更です。

  1. 参考情報 (あれば)
  • 元々の実装経緯:
    • 0601a9f9aa: RangeError を SELECT の近くで捕捉する実装(select_all 周り)
    • 9051bc4332: async 実行を QueryIntent に移動する過程で rescue 範囲が広がり、結果として全クエリに適用されるようになってしまった
  • 関連しうるコードパス:
    • ActiveRecord::ConnectionAdapters::QueryIntent#run_query!
    • #execute_intent#processed_sql#compile_arel!
    • _exec_insert, update, update_with_result, delete
  • アプリ側の確認ポイント:
    • これまで DB 書き込み時に謎の RuntimeError: "Cannot call affected_rows after cast_result has been called" を見ていた場合、
      この修正が入るバージョンでは、同じ操作で ActiveModel::RangeError が代わりに出る可能性が高いです。
      その場合は、範囲外値や型変換ロジックを見直すことで、問題箇所を特定しやすくなります。

#58255 Deprecate insert_returning option and use_insert_returning?

マージ日: 2026/7/27 | 作成者: @kamipo

  1. 概要 (1-2文で)
    PostgreSQL 用の DB 設定オプション :insert_returning と、それを参照する PostgreSQLAdapter#use_insert_returning? が「非推奨」としてマークされました。今後は RETURNING の有無は supports_insert_returning? / supports_update_returning? などのサポート判定に一元化され、:insert_returning で挙動をコントロールすることはできなくなっていきます。

  1. 変更内容の詳細

2-1. 何が非推奨になったか

  • PostgreSQL 用 DB 設定オプション:
    • :insert_returning
      • 例: database.yml などで指定していた以下のようなオプション
        yaml
        production:
          adapter: postgresql
          database: my_app_production
          insert_returning: false
      • これが「deprecated(非推奨)」扱いになりました。
  • ActiveRecord アダプタメソッド:
    • ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#use_insert_returning?
      • :insert_returning 設定値を読んで、INSERT に RETURNING を使うかどうか決めていたメソッド
      • これも非推奨としてマークされています。

2-2. なぜ非推奨になったか(背景)

:insert_returning は、単一行 INSERT を内部的に実行する _exec_insert 経路にしか影響していませんでした。
一方で、以下のような経路はすでに別判定で RETURNING を使っており、:insert_returning では制御できません:

  • insert_all
  • upsert_all
  • update に対する RETURNING
    supports_insert_returning? / supports_update_returning? による判定)

そのため:

  • DB が RETURNING をサポートしている場合、:insert_returningfalse にしても「完全には RETURNING を止められない」
  • オプションとしての一貫性がなく、役割が中途半端(事実上「名ばかり設定」= vestigial)

という状態だったため、オプションとそれを参照するメソッドごと非推奨に移行されました。

2-3. コード上の具体的な変更点(概要)

変更ファイルは以下の4つです。

  1. activerecord/CHANGELOG.md

    • Rails の CHANGELOG に、insert_returning オプションと use_insert_returning? の非推奨が追記されています。
  2. activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb

    • _exec_insert 周辺で、use_insert_returning? の利用方法や条件が微調整されています(1行変更)。
    • 実質的には、「今後 use_insert_returning? に依存しない方向へ向かう」ことを示す変更です。
  3. activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

    • use_insert_returning? メソッドに非推奨警告の付与:
      • メソッド呼び出し時に ActiveSupport::Deprecation を通じて警告を出す形、もしくは同等の非推奨マークが追加されています。
    • DB 設定オプション :insert_returning を読み取るロジックに対し、「この設定は非推奨」という位置づけのコメントや処理が追加されています。
    • それに伴う小さなリファクタリング(行数的に +13/-3)も含まれています。
  4. activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb

    • 新たに追加された非推奨挙動(insert_returning 設定や use_insert_returning?)に対するテストが追加 (+3/-1) されています。
    • これにより、将来のバージョンでも「非推奨警告がきちんと出る」挙動が担保されます。

  1. 影響範囲・注意点

3-1. 既存アプリで insert_returning を使っている場合

database.yml などに insert_returning: false のような指定をしている場合:

  • Rails のアップデート後、起動時または接続時に 非推奨警告(deprecation warning)がログに出る可能性 があります。
  • ただし、もともとこのオプションは「単一行 INSERT の一部の経路」にしか影響しておらず、
    • insert_all
    • upsert_all
    • UPDATE ... RETURNING などの RETURNING は無効化できていませんでした。
  • そのため、実際の挙動(RETURNING が発行されるかどうか)が大きく変わるわけではなく、「設定しても期待どおりに全体を制御できないオプションが、正式に非推奨と宣言された」という位置づけです。

今後は以下の方針が推奨されます:

  • insert_returning 設定は削除する(Rails 推奨のデフォルト挙動に任せる)
  • RETURNING のサポート可否は:
    • supports_insert_returning?
    • supports_update_returning? いずれも アダプタが DB の機能検出に基づいて自動判定 することを前提にする。

3-2. use_insert_returning? を直接呼んでいるライブラリ・アプリ

アプリや gem コードの中で、以下のような呼び出しをしている場合:

ruby
adapter = ActiveRecord::Base.connection
if adapter.use_insert_returning?
  # RETURNING 前提の処理
end
  • このメソッド呼び出し時に deprecation warning が発生 するようになります。
  • 将来のバージョンで削除される可能性が高いので、次のような対応が必要です:
    • そもそも use_insert_returning? に依存しない設計にする
    • どうしても必要であれば
      • supports_insert_returning?
      • 必要に応じて supports_update_returning? を利用して判断する

3-3. RETURNING を完全にオフにしたいケース

この PR によって明確になった点として:

  • Rails の内部は「DB が RETURNING をサポートしているなら使う」という前提で動いており、
  • 単純な設定オプションで「一切 RETURNING を使わない」モードにすることは想定していない

という設計方針が確認できます。

もしアプリケーション都合で「RETURNING を使いたくない」場合は:

  • アダプタレベルでパッチを当てる
  • カスタムコネクションアダプタを作る
  • Arel/SQL を自前で組み立てる

といった、より踏み込んだ対応が必要になることが示唆されます。


  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/58255
  • 関連機能:
    • ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#supports_insert_returning?
    • ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#supports_update_returning?
  • insert_all / upsert_all は、Rails 6 以降で導入されたバルク INSERT / UPSERT 機能で、PostgreSQL では RETURNING を活用することで挿入・更新後のレコード情報を効率的に取得する設計になっています。この PR は、その流れの中で「中途半端に RETURNING を止めようとする古い設定を整理する」位置づけと捉えると理解しやすいです。

#58259 Delegate ids back to pluck

マージ日: 2026/7/27 | 作成者: @kamipo

  1. 概要 (1-2文で)
    Relation#ids の実装を pluck(primary_key) ベースに戻し、idspluck の挙動を再び揃えることで、これまで片方だけに入っていたバグ修正の非対称性を解消した PRです。includes 利用時の重複 ID 除去だけを特別扱いとして残し、それ以外は pluck に処理を委譲するようになりました。

  1. 変更内容の詳細

2-1. 以前の経緯

  • もともと (#46503 以前) の ids:
    ruby
    def ids
      pluck(primary_key)
    end
  • #46503 での変更理由:
    • includes を使ったときに JOIN の結果が重複し、ids が重複した ID を返していた:
      ruby
      Post.includes(:comments).where(id: 1).ids
      # 期待: [1]
      # 実際: [1, 1, 1, 1, 1] など
    • これを解消するため idspluck とは別実装になり、「重複除去ロジック」を自前で持つようになった。

その後、idspluck の実装が分かれたことにより、次のような「どちらか片方だけ直す」状態が発生していました:

  • #47762: ids 側では複合主キー (Composite Primary Key, CPK) をサポートできていなかった。
  • #51167: ids_reader が CPK の関連で重複行を返すバグがあり、ids まわりの仕様と実装が複雑化。
  • #58004: none.ids が実クエリ SELECT ... WHERE (1=0) を投げてしまう一方で、none.pluck(:id) はショートサーキットで [] を返していた。

2-2. この PR の実装変更

この PR のポイントは:

  • ids の本体処理は pluck(primary_key) に全面委譲する」
  • 「ただし includes 等で has_include?(primary_key) が true のときだけ、ids 側で重複除去する」

という形に戻したことです。

擬似コード的には以下のようなイメージです(実コードはもっと短くなっていますが、概念的にはこういう分岐):

ruby
def ids
  if has_include?(primary_key)
    # includes による重複行をまとめる
    pluck(primary_key).uniq
  else
    # 通常ケースは pluck に丸投げ
    pluck(primary_key)
  end
end

実際の diff では:

  • activerecord/lib/active_record/relation/calculations.rb
    • ids 独自実装に関するコードがほぼ全削除(約 39 行削除)
    • pluck 呼び出しと重複除去に関わる最小限のコードだけが残る(約 3 行追加)
  • これにより、クエリ発行や CPK 対応、none への最適化などはすべて pluck に統一されます。

2-3. ログタグの変更

  • これまで: ids 実行時のログタグが "Model Ids" になっていた
  • この PR 後: pluck に委譲するため "Model Pluck" になる

つまり、ログは再び以下のような形に揃います:

text
Model Pluck (0.2ms)  SELECT "posts"."id" FROM "posts" ...

これは #46503 以前の挙動に戻り、pick ともログラベルが揃います。


  1. 影響範囲・注意点

3-1. 挙動の変化が起こりうるケース

  1. none.ids の挙動

    • 以前: none.ids が実際に SELECT ... WHERE (1=0) を叩いていた
    • 変更後: none.pluck(:id) と同様に、DB クエリなしで [] を返す(ショートサーキット)
    • 影響:
      • パフォーマンス的には改善 (無駄なクエリが消える)
      • もしテストなどで「クエリが発行されること」を前提にしていた場合は失敗する可能性あり
  2. 複合主キー (CPK) を使っている場合

    • ids が内部的に pluck(primary_key) を使うため、CPK 対応は pluck の仕様に完全追従します。
    • これまで idspluck の挙動がずれていた部分があれば、その差が解消される代わりに「pluck の仕様に統一される」ため、CPK まわりの微妙なワークアラウンドがあれば要確認です。
  3. ログ監視・メトリクスで Model Ids タグを使っていた場合

    • ログタグが "Model Pluck" に変わるため、既存のログ解析・メトリクスが Model Ids に依存している場合は修正が必要です。

3-2. 互換性・今後のメンテナンス性

  • idspluck を再び同じパスに乗せることで、将来 pluck に対して行った最適化・バグ修正が、自動的に ids にも反映されるようになります。
  • コードベースとしても ids のロジックが大きく削減されたため、保守コストが下がり、Relation 計算関連の非対称バグが出にくくなります。

  1. 参考情報 (あれば)
  • この PR で言及されている過去の issue / PR:
    • #46503: idspluck から分離し、includes 時の重複 ID 問題を解消した元の変更
    • #47762: ids の複合主キー対応問題
    • #51167: CPK の関連での ids_reader 重複問題
    • #58004: none.ids が無駄なクエリを発行していた問題

運用上は、idspluck(:id) のシンタックスシュガーとみなしつつ、「includes 等で ID が重複しうるときだけ uniq を噛ませるメソッド」と理解しておくとよいです。


#58258 Remove unused binds argument from update_with_result

マージ日: 2026/7/27 | 作成者: @kamipo

  1. 概要 (1-2文で)
    update_with_result メソッドの引数から、実際には使われていなかった binds 引数が削除されました。Arel が自前でバインド値を持つようになった現在の設計に沿って、インターフェイスを整理するリファクタリングです。

  1. 変更内容の詳細

対象ファイル:

  • activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb

やっていることは非常に小さく、update_with_result メソッドのシグネチャから binds を取り除いただけです。

イメージとしては、以下のような差分です(擬似コードですが方向性はこれと同じ):

ruby
# 変更前
def update_with_result(arel, binds = [], table_name = nil, primary_key = nil, lock = nil, key = nil)
  # ...
end

# 変更後
def update_with_result(arel, table_name = nil, primary_key = nil, lock = nil, key = nil)
  # ...
end

説明文で述べられている背景:

  • コミット 213796fb4936dce1da2f0c097a054e1af5c25c2c 以降、Arel オブジェクト自身がバインド値を持つようになった。
  • そのため、Arel AST(arel)と別途 binds を同時に渡すのは設計として禁止されている(to_sql_and_binds を参照)。
  • update_with_result 内では arel.returning(...) を呼んでいるため、arel は常に Arel のステートメントマネージャ であり、生の SQL 文字列にはならない。
  • 実際の唯一の呼び出し元である _update_record_with_resultbinds を使っておらず、binds 引数は実質的に「使われない」引数になっていた。

このため、「そもそも意味のない・使い道のない binds 引数」を削除して API をシンプルにした、という変更です。


  1. 影響範囲・注意点
  • Rails 内部実装に閉じたメソッドなら影響はほぼなし
    説明によると update_with_result の呼び出し元は _update_record_with_result のみで、そこでも binds は利用されていませんでした。Rails 本体の中では動作上の挙動は変わらないと考えられます。

  • Monkey patch や独自アダプタで update_with_result を直接呼んでいる場合に注意

    • もしアプリケーションや gem 側で ActiveRecord::ConnectionAdapters::DatabaseStatements#update_with_result を直接呼び出し、binds を渡していた場合は、メソッドシグネチャの変更でエラーになります。
    • ただし、Arel がバインド値を保持する現在の設計では、もともと binds を併用するのは「やってはいけない」使い方なので、これを機に見直す必要があります。
    • 正しい形は、Arel の AST 構築時にバインド値(bind parameters)を設定するか、ActiveRecord が提供する高レベル API を通じて更新を行うことです。
  • 生 SQL と binds を組み合わせて update_with_result する用途には非対応
    説明にもある通り update_with_resultarel.returning(...) を前提としており、ここで渡すのは Arel オブジェクトのみです。生 SQL + binds で同等のことをしたい場合は、exec_update / exec_query など別のメソッドを使用すべきです。


  1. 参考情報 (あれば)
  • この PR 内で参照されているコミット:
    • 213796fb4936dce1da2f0c097a054e1af5c25c2c
      • ここから「Arel が自前でバインド値を持つ」設計になり、to_sql_and_binds で Arel と binds 併用は禁止されるようになった。
  • 関連メソッド:
    • ActiveRecord::ConnectionAdapters::DatabaseStatements#to_sql_and_binds
    • ActiveRecord::ConnectionAdapters::DatabaseStatements#exec_update
    • ActiveRecord::Relation#update_all(内部で最終的に更新系メソッドをたどる際にこのあたりが使われる)

#58256 [ci skip] Update Mailgun setup documentation links

マージ日: 2026/7/27 | 作成者: @ajaynomics

  1. 概要 (1-2文で)
    Mailgun を使った Action Mailbox 連携のドキュメント内リンクが古くなっていたため、Mailgun が公式に案内している forward() アクション用ドキュメントへのリンクに更新した PRです。Action Mailbox が依存している mime URL サフィックスと body-mime ペイロード仕様を説明している正しいページを参照するようになります。

  1. 変更内容の詳細

対象箇所

  • actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb
  • guides/source/action_mailbox_basics.md

いずれも Mailgun の設定方法を案内する URL 文字列のみが差し替えられています(+1/-1 行ずつ)。
コード上のロジック(パラメータ処理、セキュリティチェックなど)には一切手が入っていません。

想定される変更イメージ

※PR本文から読み取れる情報をもとにした概念的な例です(実際の URL は Rails 本家リポジトリを参照してください)。

ruby
# 変更前(例)
# See Mailgun HTTP forwarding docs:
# https://documentation.mailgun.com/en/latest/user_manual.html#routes-http-forwarding

# 変更後(例)
# See Mailgun forward() action docs (mime/body-mime):
# https://documentation.mailgun.com/en/latest/user_manual.html#routes-forward

ガイド側 (action_mailbox_basics.md) も同様に、Mailgun 設定手順の説明中に出てくるリンク先を、

  • 「HTTP フォワーディング」の古い/不適切なページ
    → 「forward() アクションで受信する方法」を説明する正規のページ

に差し替えています。

このターゲットページには、Action Mailbox で Mailgun ingress を使う際に重要な

  • mime URL サフィックス
  • body-mime パラメータに raw MIME メッセージが載る

といった仕様が明示されているため、Rails 側の説明と Mailgun 側の仕様が対応するようになりました。

動作確認コマンド

PR では以下のコマンドで検証が行われています:

bash
bundle exec rake guides:generate ONLY=action_mailbox_basics
bundle exec rake guides:lint:check_links ONLY=action_mailbox_basics
bundle exec rake rdoc
  • ガイドの生成が通ること
  • ガイド内リンクがリンクチェッカでエラーにならないこと
  • API ドキュメント (rdoc) が生成できること

を確認済みです。


  1. 影響範囲・注意点

影響範囲

  • 実行時のアプリケーション動作には影響なし
    • コントローラのロジックや Action Mailbox の挙動は一切変わっていません。
  • ドキュメント閲覧時の UX 改善
    • Mailgun 設定時に参照するリンクが、目的の情報(forward() アクション、mime サフィックス、body-mime)をきちんと含むページに変わるため、導入時の迷いが減ります。

注意点

  • 既に Mailgun + Action Mailbox を運用しているユーザが、過去の Rails ガイドに書かれた古いリンクをブックマークしている場合、そのリンクは引き続き動作するかもしれませんが、最新の推奨設定はこの PR で指しているページ側になります。
  • Action Mailbox 側は従来通り「/rails/action_mailbox/mailgun/inbound_emails/mime への HTTP POST で body-mime を受け取る」という設計なので、Mailgun 側の route 設定がこれに合うように構成されているか再確認する際に、新しいリンク先が有用です。

  1. 参考情報 (あれば)
  • Rails ガイド: Action Mailbox Basics(該当セクション:Mailgun ingress の設定)
  • Mailgun 公式ドキュメント(forward() アクション、mime URL サフィックス、body-mime payload の説明があるページ)
  • Rails リポジトリの該当ファイル:
    • actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb
    • guides/source/action_mailbox_basics.md

#58257 Require Markdown APIs in new files

マージ日: 2026/7/27 | 作成者: @fxn

  1. 概要 (1-2文で)
    Rails の API ドキュメントを RDoc から Markdown へ段階的に移行している最中に、新規ファイルへ RDoc 記法が紛れ込むのを防ぐため、CI 上で「新しい Ruby ファイルの API コメントが Markdown かどうか」をチェックする仕組みが追加されました。
    これにより、以降追加されるコードは原則として Markdown 形式でドキュメントを書くことが求められます。

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

追加されたもの

変更は 2 ファイルのみで、どちらも GitHub Actions とその補助スクリプトです。

  1. .github/workflows/check-markdown-api.yml

    • 新しい GitHub Actions ワークフローが追加されています。
    • 目的: プルリクエスト内の「新規ファイル」に対して、API ドキュメントが Markdown で書かれているかを検査する。
    • 実行タイミング:
      • おそらく pull_request または pull_request_target トリガー(ファイル名から推測)で、PR 作成時・更新時に走るチェックです。
    • 処理内容(典型的な構成の推測を含む):
      • リポジトリをチェックアウト
      • Ruby をセットアップ
      • check-markdown-api.rb を実行して、PR に含まれる新規ファイルを走査
      • 規則に違反していればワークフローを失敗させる
  2. .github/workflows/scripts/check-markdown-api.rb

    • 実際の検証ロジックを持つ Ruby スクリプトです。
    • 主な役割:
      • 「新規に追加されたファイル」を検出する
        • 典型的には git diff --name-status origin/main...HEAD 等を見て、A (Added) のファイルのみ対象にする処理が含まれているはずです。
      • その中から対象とするファイル(例えば *.rb)を抽出
      • API ドキュメントコメントをチェックして、Markdown 以外(RDoc など)の記法が含まれていないかを検査
    • 具体的に何を NG とみなすかはスクリプト実装に依存しますが、例えば以下のような RDoc 特有の記法を禁止している可能性があります:
      • # :nodoc:, # :yields: などの RDoc タグ
      • # == 見出し など古いスタイルの見出し
      • {ClassName} 形式のリンク表現 など
    • 逆に、Markdown として期待されるのは以下のような書き方です:
      ruby
      # Public: ユーザーを作成する
      #
      # @example
      #   User.create!(name: "Taro")
      #
      # @param name [String] ユーザー名
      # @return [User] 作成されたユーザー
      class User
        # ...
      end
      Rails 本体では Yard タグと Markdown を組み合わせているケースもあるため、check-markdown-api.rb はそうした現在のスタイルに準拠しているはずです。

  1. 影響範囲・注意点
  • 対象: 「Rails 本体」への PR のうち、新規ファイルを追加するもの
    既存ファイルの変更は現時点では直接対象外(ただし将来的にチェック範囲が広がる可能性はあります)。
  • 実務上の影響:
    • Rails に貢献する開発者は、以下を意識する必要があります:
      • 新しく .rb ファイルを追加する場合、その API ドキュメントは Markdown 形式で書くこと。
      • 旧来の RDoc 記法をうっかり使うと、CI(GitHub Actions)の check-markdown-api ジョブが失敗して PR が red になります。
  • プロジェクト内ポリシーとしての意味:
    • API ドキュメントを段階的に Markdown に移行している最中のため:
      • 既存の RDoc 記述は残っていてもよい(移行対象)
      • 新規に RDoc を増やすことは禁止、というガードレールを CI で強制する形になっています。
    • 移行期間中に混在がさらに悪化するのを防ぐための措置です。

  1. 参考情報 (あれば)
  • PR 本文:
    • 「We are gradually converting our API to Markdown.
      By requiring new files to be documented in Markdown we prevent RDoc markup from being introduced while the migration is ongoing.」
      という説明からも、「完全移行はまだだが、新規分だけは Markdown を強制する」という方針が明示されています。
  • 類似の取り組み:
    • 他プロジェクトでも、RDoc → Markdown, Javadoc → Markdown などの移行時に、新規コードへ旧形式を使わせないための CI チェックを挟むのはよくあるパターンです。
    • Rails も同様に、技術的負債の増加を抑えつつドキュメントフォーマットを統一しようとしています。

#58252 Fix: infinite route reload when after_routes_loaded accesses routes

マージ日: 2026/7/26 | 作成者: @chaadow

  1. 概要 (1-2文で)
    Rails のルーティング再読み込み処理において、after_routes_loaded フック内でルートセットにアクセスすると無限にルートが再読み込みされてしまう不具合を修正した PR です。execute_unless_loaded のクリティカルセクション(ルート定義とフック実行の一連の流れ)全体でロード状態を「loading」に保つように変更しています。

  1. 変更内容の詳細

問題の背景

  • #58225 の変更後、reload! 実行時に @load_statenil に戻されるタイミングが早すぎました。
  • 具体的には、ルートを描画したあと、after_routes_loaded フックが実行される「前」に @load_statenil に戻っていたため、
    • フックの中でルートセット(例: Rails.application.routes や関連メソッド)に触れると、
    • execute_unless_loaded が「まだロードされていない」と誤認して、再度ルート描画を開始
    • その中でまたフックが動き…と再帰的に呼ばれ続け、結果として 無限なルート再読み込み 状態に陥る可能性がありました。

対応方針

PR 説明の要約:

execute_unless_loaded のクリティカルセクション全体(ルートの描画と after_routes_loaded フックの実行を含む)で状態を :loading のまま保持し、フック完了後に初めて :loaded にする。失敗した場合は nil に戻してリトライを許可する。

つまり:

  • これまで
    • ルート描画完了 → @load_state = nil → その後 after_routes_loaded 実行
  • これから
    • execute_unless_loaded 開始 → @load_state = :loading
    • ルート描画 → after_routes_loaded 実行
    • 全て成功 → @load_state = :loaded
    • 途中で失敗 → @load_state = nil(次のリトライで再度描画できる)

コード上のポイント(概念レベル)

railties/lib/rails/application/routes_reloader.rb の変更の要点は以下です:

  1. execute_unless_loaded 内で

    • ルートの draw(実際のルーティング定義)
    • after_routes_loaded フックの呼び出し
      ひとまとまりの「ロード中」状態 (:loading) として扱うようにした。
  2. 処理がすべて正常に完了した時点でのみ @load_state = :loaded にする。

  3. 途中で例外などにより失敗した場合は @load_statenil に戻すことで、

    • 次回の execute_unless_loaded 呼び出し時に再度ルート描画を行える(リトライ可能)
    • ただし「途中までロードされた不整合な状態」で :loaded と誤認されないようにする。

テスト追加

railties/test/application/loading_test.rb に +33 行のテストが追加されており、主に以下を確認していると考えられます(PR の説明からの推測ですが、Rails の既存パターンと整合的です):

  • after_routes_loaded フックの中でルートにアクセスしても 無限再読み込みが発生しないこと
  • フック実行中に execute_unless_loaded を再入しても、@load_state:loading のため再度の描画が走らないこと。
  • 例外発生時には @load_statenil に戻り、次回のロードが正しく行われること。

  1. 影響範囲・注意点

影響範囲

  • Rails アプリケーションで after_routes_loaded フック(またはそれと等価の拡張ポイント)を利用している場合に挙動が変わります。

    • 以前は、フック内でルートに触れると意図せず execute_unless_loaded が再実行される可能性がありました。
    • この PR により、そのような再入が防がれるため、無限ループや過剰な再読み込みが解消されます。
  • 特に以下のようなコードを書いている場合に、この修正の恩恵があります:

    ruby
    # 擬似コード(エンジンやアプリケーション設定で)
    config.after_initialize do
      Rails.application.routes_reloader.after_routes_loaded do
        # ここでルート情報に触れる
        Rails.application.routes.named_routes.helpers
      end
    end

    こうしたフック内のルートアクセスが、以前は execute_unless_loaded を再入させていた可能性があります。

注意点

  • @load_state のライフサイクルがより厳密になったため、

    • 「ルート描画は終わっているが、フックがまだ」という中途半端な状態は :loading とみなされます。
    • この期間中に execute_unless_loaded を呼び出しても、新たな描画は行われません(=期待する挙動)。
  • ルーティングやフック処理の中で意図的に再読み込みをトリガーするような「かなりトリッキーな」コードを書いていた場合、

    • その前提が崩れる可能性がありますが、これはむしろバグ防止として望ましい方向です。
  • 例外が発生した場合に @load_statenil に戻るため、

    • 例外後の次の要求などでルートの再描画が走る可能性があります。
    • ルート定義やフック内で例外が起きた場合の挙動を確認しておくと安心です。

  1. 参考情報 (あれば)

この PR により、ルートリロード時の状態管理がより一貫したものになり、after_routes_loaded を使う高度なカスタマイズが安全に行えるようになっています。


#58250 Fix number_to_human_size crashing above a terabyte

マージ日: 2026/7/26 | 作成者: @55728

  1. 概要 (1-2文で)
    number_to_human_size が 1TB を超えるサイズをロケール依存でフォーマットする際、一部ロケールで TypeError が発生していた問題を修正し、ペタ/エクサ/ゼタバイトに対しても英語ベースのフォールバックが効くようになりました。これにより、対応する翻訳がないロケールでも "1 PB", "1 EB", "1 ZB" などの文字列を返し、例外を出さずに動作します。

  1. 変更内容の詳細

問題の内容

number_to_human_size は、locale で指定されたロケールに適切な翻訳があればそれを使い、なければ Ruby 側にハードコードされたフォールバック表 (DEFAULTS) を使う実装になっています。

  • 既存の DEFAULTSbyte, kb, mb, gb, tb までは定義されていたが、
  • petabyte, exabyte, zettabyte は追加されていなかったため、

例えば以下のような状況が起きていました:

ruby
# CS (チェコ語) ロケールの例: terabyte までは翻訳が存在し、それ以降はない

number_to_human_size(1024 ** 4, locale: :cs)
# Before: "1 TB"   # TB の翻訳 or フォールバックは存在

number_to_human_size(1024 ** 5, locale: :cs)
# Before: TypeError: no implicit conversion of nil into String
#        (PB 用のフォールバックがなく、nil を String にしようとしてエラー)

これは、2015年(#22732) および 2023年(#47771) の PR でヘルパーと英語ロケールには petabyte, exabyte, zettabyte が追加された一方で、フォールバック表 (DEFAULTS) だけが更新されていなかったことによるギャップです。

この PR での修正内容

DEFAULTS(フォールバックで使う単位名テーブル)に、以下の 3 つの単位を追加しています。

  • petabyte -> "PB"
  • exabyte -> "EB"
  • zettabyte -> "ZB"

※ 実際のコード上では、number_converter.rb 内の DEFAULTS 定数に 3 エントリ追加 (+4/-1) されています。

それに合わせて、activesupport/test/number_helper_i18n_test.rb にテストが追加されています (+3 行)。これにより、翻訳が存在しない状況でも以下のような動作になります:

ruby
number_to_human_size(1024 ** 4, locale: :cs)
# After: "1 TB"   # ここは従来と同じ

number_to_human_size(1024 ** 5, locale: :cs)
# After: "1 PB"   # 翻訳がなくてもフォールバックで "PB" を返す

なお、number_to_humandecimal_units については、もともと英語ロケールと完全に揃ったフォールバックを持っており、この PR の影響は受けません。


  1. 影響範囲・注意点
  • 影響するケース

    • number_to_human_size を利用しており、
    • :en 以外のロケールを指定していて、
    • そのロケールの翻訳ファイルに petabyte, exabyte, zettabyte が定義されていない場合、
    • かつ 1TB(1024 ** 4)を超える値をフォーマットするとき。
  • 挙動の変化

    • 以前は上記条件で TypeError が発生していたが、今後は "1 PB", "1 EB", "1 ZB" などの英語ベースの単位が返る。
    • 対応する翻訳が存在するロケールでは、従来どおり翻訳が優先されるため挙動は変わらない。
  • 言語ミスマッチについて

    • フォールバックは byte, kb, mb, gb, tb 同様、英語表記 ("PB", "EB", "ZB") です。
    • rails-i18n に含まれる 122 ロケールのうち:
      • 58 が petabyte 未翻訳
      • 59 が exabyte 未翻訳
      • 116 が zettabyte 未翻訳
    • ただし、それらロケールの多くは既に "TB" のような英字表記を使っているため、フォールバックの "PB" なども自然に見えるケースが多い。
    • 全く別表記(例: "ТБ", "To", "ترابایت" など)を使うロケールでは "PB" とのミスマッチは起こるが、例外で落ちるよりは明らかにまし、という判断になっています。
  • アプリ側での注意・対応案

    • ロケール整備が行き届いているアプリで完全な翻訳を求める場合:
      • pb, eb, zb などのキーを各ロケールに追加しておくことで、今回のフォールバックに頼らずに済みます。
    • ログ/監視などで 1TB 超の値を扱っている場合:
      • 今回まで気づいていなかった潜在的な TypeError があったかもしれませんが、この PR によって解消されます。

  1. 参考情報 (あれば)
  • 当該 PR:
    • rails/rails #58250 — Fix number_to_human_size crashing above a terabyte
  • 関連 PR:
    • #22732: number_to_human_size に petabyte などが初めて追加された PR (2015)
    • #47771: exabyte, zettabyte 追加などを行った PR (2023)
  • ロケール翻訳リポジトリ:
    • rails-i18n: 各種ロケールでのストレージ単位翻訳の状態が確認可能

#58249 Extract :table_name as explicit kwarg in join table helpers

マージ日: 2026/7/26 | 作成者: @kamipo

  1. 概要 (1-2文で)
    create_join_table, build_create_join_table_definition, drop_join_table:table_name を「単なる options の1要素」から「明示的なキーワード引数」に変更し、options をその場で破壊的変更していた補助メソッド find_join_table_nameJoinTable モジュールを削除したリファクタリングです。これにより API が明示的になり、CommandRecorder からの不要な include JoinTable も除去されています。

  1. 変更内容の詳細

2-1. join table helper のシグネチャ変更

対象メソッド:

  • create_join_table
  • build_create_join_table_definition
  • drop_join_table

これらは以前、

ruby
def create_join_table(table_1, table_2, **options)
  # options[:table_name] を find_join_table_name が掘り出していた
end

のように :table_name**options に含めて受け取り、内部で

  • find_join_table_name(options)options.delete(:table_name) して join table 名を確定
  • 残りの optionscreate_table / drop_table にそのまま渡す

という実装になっていました。

この PR では、:table_name を明示的なキーワード引数として受け取る形に変更しています。概念的には次のような形になります:

ruby
def create_join_table(table_1, table_2, table_name: nil, **options)
  # table_name を明示的に扱い、options には含めない
  # create_table(join_name, **options)
end

def drop_join_table(table_1, table_2, table_name: nil, **options)
  # 同様に table_name を分離
end

実際のコードでは、table_name が指定されていない場合は従来通り join_table_name(table_1, table_2) でデフォルトの join table 名を作成し、そのうえで create_table / drop_table に渡す、という挙動は変わりません。

この結果、options の中に :table_name が残ったまま下位メソッド (create_table など) に渡されることがなくなり、create_table 側のオプションバリデーションに引っかかることもなくなります(従来はこれを避けるために delete していた)。

2-2. ActiveRecord::Migration::JoinTable モジュールの削除

activerecord/lib/active_record/migration/join_table.rb から、JoinTable モジュールが削除されています。中身は以下の2メソッドのみでした:

  • find_join_table_name
  • join_table_name (2つのテーブル名から join table 名を決めるもの)

今回の変更で

  • find_join_table_name は不要になった(:table_name をキーワード引数で直接受けるため)
  • join_table_nameSchemaStatements 側に移された/すでに存在する方を使う形になった

ため、このファイル自体が不要になっています。

2-3. CommandRecorder からの dead code 除去

ActiveRecord::Migration::CommandRecorder には以前から include JoinTable が入っていましたが、PR 説明にもある通り:

  • 2012年のコミット bd155d2ae3 で、
    • invert_create_join_table の戻り値が
      • 以前: [:drop_table, [resolved_join_table_name]]
      • 以後: [:drop_join_table, args, block] に変更された
  • それ以降、CommandRecorder 内で JoinTable のメソッド (find_join_table_name 等) を呼ぶパスは存在しなくなっていた

つまり、CommandRecorder における include JoinTable は死んだコードになっていたため、この PR で削除されています。

2-4. テストの微調整

activerecord/test/cases/migration/schema_definitions_test.rb で、create_join_table / drop_join_table 周りの仕様に沿うよう、table_name の扱いに関するテストが 2行追加・2行削除されています。振る舞いとしては同等になるように保たれており、外部 API 変更を示すものではありません。


  1. 影響範囲・注意点

3-1. パブリック API の互換性

Migration の DSL としての create_join_table / drop_join_table の使い方は、通常の利用方法では互換性が保たれています。

典型的な利用例:

ruby
create_join_table :authors, :books
create_join_table :authors, :books, table_name: :authorships
drop_join_table :authors, :books
drop_join_table :authors, :books, table_name: :authorships

上記のような書き方は、従来と同じように動作します。

3-2. 影響を受ける可能性があるケース

影響があり得るのは、次のような「かなり内部寄り」の使い方をしている場合です。

  1. options ハッシュをいじる前提で create_join_table を呼んでいるコード
    例:

    ruby
    opts = { table_name: :authorships, force: true }
    create_join_table(:authors, :books, **opts)
    # 呼び出し後に opts[:table_name] が消えることを期待しているようなコード

    以前は find_join_table_nameoptions.delete(:table_name) を行っていたため、呼び出し元のハッシュが破壊的に変更されていました。
    今回、:table_name はキーワード引数として扱われるため、そのような副作用は発生しません。
    この挙動に依存しているコードはほぼ無いと思われますが、「副作用が無くなる」変更点としては押さえておく価値があります。

  2. ActiveRecord::Migration::JoinTable モジュールや find_join_table_name に直接依存しているコード
    Rails のパブリック API ではなく内部実装寄りなので、本来依存すべきではない部分ですが、もし JoinTable を直接 include して join_table_name / find_join_table_name を呼んでいた場合、そのコードは壊れます。
    こうした用途がある場合は、join_table_name を定義している現行の場所(SchemaStatements 配下)から利用するか、自前で join table 名決定ロジックを持つように修正する必要があります。

  3. CommandRecorder + 独自の inversion ロジックを拡張しているケース
    CommandRecorder に対する include JoinTable が削除されたことで、CommandRecorder 内で JoinTable のメソッドが見えなくなります。
    ただし PR 説明通り、既に Rails 本体のコードからは利用されておらず dead code だったため、通常のアプリケーション / エンジンでは影響は無い想定です。

3-3. オプション検証の観点

今回の主眼の1つは、「create_table / drop_table のオプション検証において、想定外の :table_name キーが紛れ込まないようにする」点です。

  • 以前: options:table_name を入れておき、create_table 呼び出し前に options.delete(:table_name) で削除
  • 現在: table_name: でキーワード引数として受け取り、最初から options には入れない

上記により、create_table などのオプションバリデーションコードを素直に保ちやすくなっています。
この意味で、「内部実装の健全化」「将来的なオプション追加・検証強化への布石」という側面があります。


  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/58249
  • 関連する過去コミット: invert_create_join_table の戻り値変更
    • bd155d2ae3 (2012) — [:drop_table, [resolved_join_table_name]][:drop_join_table, args, block] に変更
  • 該当コード周辺:
    • activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
    • activerecord/lib/active_record/migration/command_recorder.rb
    • テスト: activerecord/test/cases/migration/schema_definitions_test.rb

実務的には「join table 用の Migration DSL の使い方はこれまで通り」でありながら、「内部実装はより明示的で安全になった」という整理と捉えると理解しやすい変更です。


#58248 Fix inaccurate docstring in CommandRecorder#inverse_of [ci-skip]

マージ日: 2026/7/26 | 作成者: @kamipo

  1. 概要 (1-2文で)
    ActiveRecord::Migration::CommandRecorder#inverse_of のドキュメントコメント(docstring)が、現在の実装と食い違っていたため、古い挙動を前提にした説明とサンプルが削除されました。コード本体の挙動変更はなく、ドキュメントのみの修正です。

  2. 変更内容の詳細

  • 対象ファイル:
    activerecord/lib/active_record/migration/command_recorder.rb

  • 変更点:
    CommandRecorder#inverse_of のコメントから、以下のような内容が削除されています。

    • 「あるコマンドの逆操作が複数のコマンドから成る場合は、コマンドの配列を返す」という説明
    • その具体例として挙げられていた invert_remove_columns の説明・サンプル
  • 背景となる実装の変化:

    • もともと invert_remove_columns は、「複数カラムの削除」に対する逆操作として、「複数の [:add_column, ...] コマンドの配列」を返していました。
    • しかし 3582de977d (2020年) のリファクタで、逆操作は「単一の [:add_columns, ...] コマンド」にフラット化されるようになりました。
      • 例(イメージ):
        • 以前のイメージ:
          ruby
          [
            [:add_column, :users, :name, :string, **options1],
            [:add_column, :users, :age, :integer, **options2]
          ]
        • 現在のイメージ:
          ruby
          [
            [:add_columns, :users, [[:name, :string, options1], [:age, :integer, options2]]]
          ]
    • にもかかわらず docstring 側が「複数コマンドの配列を返す」という古い仕様のまま残っていたため、その部分の記述と例が削除されています。
  • 実コードのふるまい:

    • この PR では Ruby コード自体は一切変更されていません(追加 0 行 / 削除 5 行、すべてコメント)。
    • CommandRecorder#inverse_of の挙動や、マイグレーションの実行結果には一切影響しません。
  1. 影響範囲・注意点
  • 影響範囲:

    • 実行時の挙動・API には影響なし(ドキュメントのみの修正)。
    • ただし、以下のようなケースで「前提としていた仕様」がドキュメントと食い違っていたことが明示化される:
      • CommandRecorder#inverse_of が「複数コマンドの配列を返す」ことを前提に、自前でメタプログラミングしているツール・ライブラリ
      • 古い docstring を読んで、invert_remove_columns の戻り値形式を誤解していた開発者
  • 注意点:

    • もし CommandRecorder#inverse_of を直接利用し、「逆操作が来たら必ずコマンド配列になる」と決め打ちしているコードがある場合、実際の挙動はすでに数年前から違っている可能性が高いので、戻り値形式を確認した方がよいです。
    • この PR 以降、公式コメントから「複数コマンドの配列を返す」という一般的な保証は読み取れなくなるため、
      • inverse_of の戻り値は「単一コマンド or 複数コマンド」など、実装依存の部分があるものとして扱うのが安全です。
  1. 参考情報 (あれば)
  • 該当 PR:
  • invert_remove_columns を単一 :add_columns にまとめるようにした過去コミット:
    • 3582de977d (2020 年のリファクタ)
  • 関連クラス・メソッド:
    • ActiveRecord::Migration::CommandRecorder
    • ActiveRecord::Migration::CommandRecorder#inverse_of
    • ActiveRecord::Migration::CommandRecorder#invert_remove_columns

#58238 Remove ruby2_keywords usage

マージ日: 2026/7/26 | 作成者: @kamipo

  1. 概要 (1-2文で)
    Rails がサポート Ruby バージョンを 3.3.1+ に引き上げたことに伴い、キーワード引数互換のために使っていた ruby2_keywords を全廃し、通常の引数 (*args, **kwargs, &block, ...) に書き換えた PR です。
    一部、外部インターフェースやシリアライズ形式を変えられない箇所では、Hash.ruby2_keywords_hash による「末尾フラグ付きハッシュ」形式を維持しています。

  1. 変更内容の詳細

2-1. 全体方針

  • 以前は「Ruby 2.7 と 3.x を両方サポート」するために ruby2_keywords を使い、キーワード引数の挙動差を吸収していました。
  • Rails 7.2 以降(この PR 時点)では「Ruby 3.3.1 以上必須」となり、キーワード引数分離前後の互換はもう不要になったため、ruby2_keywords を削除。
  • 代わりに以下のいずれかに置き換え:
    • 明示的な可変長引数: def foo(*args, **kwargs, &block)bar(*args, **kwargs, &block)
    • 引数転送(Ruby 2.7+ 機能): def foo(...)bar(...)
    • 既存のバイナリ/ストレージ/ワイヤーフォーマット維持の必要がある箇所のみ Hash.ruby2_keywords_hash を維持(Active Job など)

2-2. 主なコンポーネント別の変更

※ファイル単位の diff は省略し、挙動レベルで説明します。

Action Mailer

  • 対象ファイル:

    • actionmailer/lib/action_mailer/base.rb
    • actionmailer/lib/action_mailer/message_delivery.rb
  • 変更内容:

    • メール送信メソッドなどで、ruby2_keywords を付けていたラッパーメソッドを、普通の可変長引数転送に変更。
  • イメージ:

    ruby
    # 旧
    ruby2_keywords def method_missing(name, *args, &block)
      mailer.__send__(name, *args, &block)
    end
    
    # 新
    def method_missing(name, *args, **kwargs, &block)
      mailer.__send__(name, *args, **kwargs, &block)
    end

    または、引数のシグネチャを保てるところは:

    ruby
    def method_missing(...)
      mailer.__send__(...)
    end

Action Pack (Controller / Request / Middleware / Test)

  • 対象ファイル:

    • actionpack/lib/action_controller/metal.rb
    • actionpack/lib/action_controller/test_case.rb
    • actionpack/lib/action_dispatch/http/request.rb
    • actionpack/lib/action_dispatch/middleware/stack.rb
  • 主な変更点:

    • コントローラの委譲メソッドやテストヘルパー、Middleware::Stack 周りで ruby2_keywords を廃止し、*args, **kwargs, &block... を利用した安全な引数転送に変更。
    • ミドルウェアスタック内でアプリケーションやミドルウェアをラップする箇所も同様に整理。
  • サンプルイメージ(ミドルウェアラップ):

    ruby
    # 旧
    ruby2_keywords def call(*args)
      @app.call(*args)
    end
    
    # 新
    def call(env)
      @app.call(env)
    end

    または、元のインターフェースを保つ必要がある場合:

    ruby
    def call(*args, **kwargs, &block)
      @app.call(*args, **kwargs, &block)
    end

    Ruby 3 系ではキーワード引数が分離されているため、「位置引数配列にキーワードが混ざっている前提」から「**kwargs で明示的に扱う」形に寄せています。

Action View

  • 対象ファイル:
    • actionview/test/template/form_helper/form_with_test.rb
  • 主にテストコードの修正:
    • ruby2_keywords を前提とした呼び出し方をやめ、キーワード引数を明示的に渡す・受け取るように調整。

Active Job

  • 対象ファイル:

    • activejob/lib/active_job/core.rb
    • activejob/lib/active_job/enqueuing.rb
  • ここが少し重要なポイントです。

  • 変更内容:

    • メソッドラッパーなどから ruby2_keywords を削除。
    • 一方で、ジョブ引数のシリアライズ形式(wire format)を変えないために、一部で Hash.ruby2_keywords_hash を維持。
  • 背景:

    • Active Job はジョブ引数をシリアライズしてキューに積むため、「キーワード引数を最後のハッシュとして 1 個の引数にまとめる」という形式を他プロセスや他言語との互換のために残す必要があるケースがあります。
  • イメージ:

    ruby
    # ジョブ実行時に kwargs を 1 つのハッシュとして格納
    def serialize_arguments(args, kwargs)
      combined = Hash.ruby2_keywords_hash(kwargs)
      [*args, combined]
    end

    ここでは Ruby 実行時互換のためではなく、「既存の永続化・キュー形式を壊さない」ために ruby2_keywords_hash を利用している点がポイントです。

Active Model / Active Record

  • 対象ファイル:

    • activemodel/lib/active_model/type/registry.rb
    • activerecord/lib/active_record/type/adapter_specific_registry.rb
    • activerecord/lib/active_record/migration.rb
    • activerecord/lib/active_record/migration/command_recorder.rb
    • activerecord/lib/active_record/scoping/named.rb
  • 主な変更内容:

    • 登録処理・委譲処理・スコープ定義などで ruby2_keywords を使っていた箇所を、*args, **kwargs, &block または ... に変更。
    • Migration や CommandRecorder 内の記録・リプレイ処理で、キーワード引数が正しく維持されるよう、Ruby 3 のキーワード引数分離前提で改めて組み直し
  • サンプルイメージ(Named Scope):

    ruby
    # 旧
    ruby2_keywords def scope(name, scope_options = nil, &block)
      # ...
    end
    
    # 新
    def scope(name, scope_options = nil, **kwargs, &block)
      # kwargs を適宜 scope_options にマージするなど
    end

Active Support

  • 対象ファイル:

    • activesupport/lib/active_support/core_ext/object/try.rb
    • activesupport/lib/active_support/core_ext/time/calculations.rb
    • activesupport/lib/active_support/deprecation/method_wrappers.rb
    • activesupport/lib/active_support/deprecation/proxy_wrappers.rb
  • 主な変更内容:

    • Object#try 系: 可変長+キーワード引数を安全に委譲するように変更。
    • Time#advance 等の計算系: キーワード引数が関わるヘルパーを Ruby 3 向けに整理。
    • ActiveSupport::Deprecation の「メソッドラッパー/プロキシ」:
      • もともと ruby2_keywords で元メソッドへの引数をそのまま転送していたが、これを *args, **kwargs, &block または ... に変更。
      • これにより、非推奨ラッパーが Ruby 3 の kwargs 仕様に忠実になります。
  • 例(deprecation wrapper イメージ):

    ruby
    # 旧
    ruby2_keywords def call(*args, &block)
      @target.__send__(@method, *args, &block)
    end
    
    # 新
    def call(*args, **kwargs, &block)
      @target.__send__(@method, *args, **kwargs, &block)
    end

  1. 影響範囲・注意点

3-1. ライブラリ作者・アプリ開発者への実務的影響

  • Rails 内部 API の挙動は Ruby 3 系に一致
    • これまで Ruby 2.7 互換のために「キーワード引数を最後のハッシュとして扱う」ような曖昧な挙動が混ざっていた可能性がありますが、それが解消され、Ruby 3 のキーワード引数モデルに統一されます。
  • アプリ側のコードへの直接的な破壊的変更は基本的にない前提
    • この PR はほぼ「内部実装置き換え」ですが、以下のような場合は影響を受ける可能性があります:
      • Reflection や method(:foo).parameters などで引数シグネチャを厳密に検査している場合(*args のみ → *args, **kwargs のような変化)
      • Rails 内部メソッドをモンキーパッチしていて、ruby2_keywords 前提で動かしていた場合
  • ActiveJob / Migration / Deprecation ラッパーなどは、引数の流れに敏感なコードが周辺にあることが多いため、そこを拡張しているライブラリは注意が必要です。

3-2. Ruby バージョン要件との関係

  • Rails が Ruby 3.3.1+ 必須 になったため、Ruby 2.7 / 3.0 / 3.1 などでの動作はサポート対象外です。
  • ruby2_keywords 自体が Ruby 本体側で将来的に非推奨になる提案(Feature #22205)と歩調を合わせており、Rails の利用者・エコシステム側も ruby2_keywords に依存しない設計へ移行すべきというメッセージとも読み取れます。

3-3. 自前ライブラリの移行のヒント

この PR に倣って、自分の gem / ライブラリから ruby2_keywords を外したい場合の方針:

  • Ruby 3 専用にするなら:
    • ラッパーメソッドは基本 def wrapper(*args, **kwargs, &block); target(*args, **kwargs, &block); end
    • もしくは def wrapper(...); target(...); end を使う。
  • 既存フォーマットの維持が必要なら:
    • Active Job 同様、「シリアライズ形式」などインターフェースを守る部分だけ Hash.ruby2_keywords_hash を使い、「Ruby 2.7 向け挙動のための ruby2_keywords」とは意味合いを切り分ける。

  1. 参考情報 (あれば)
  • Ruby 本体の提案:
  • Ruby キーワード引数分離の背景:
    • Ruby 2.7: kwargs 分離の警告導入、ruby2_keywords による移行パス提供
    • Ruby 3.0+: kwargs が完全に位置引数とは分離された仕様に
  • 実コードを追う場合:
    • https://github.com/rails/rails/pull/58238 の diff を見ると、「どのようなパターンで ruby2_keywords*args, **kwargs / ... に置き換えているか」が分かるため、自分のプロジェクトからの移行の参考になります。

#58240 Document that connected_to_all_shards raises with no shards

マージ日: 2026/7/25 | 作成者: @55728

  1. 概要 (1-2文で)
    Rails 8.2 での connected_to_all_shards の挙動変更(シャード未設定時に ArgumentError を送出)に合わせて、RDoc(API ドキュメント)の説明を実装どおりに修正する PR です。これにより、ドキュメントと実際の挙動の不一致が解消されます。

  1. 変更内容の詳細

対象: activerecord/lib/active_record/connection_handling.rb のドキュメントコメント部分のみが変更されています。実装コードの挙動は既に変更済み(別 PR / コミット)であり、この PR ではそれに追随して説明文を直しています。

これまでのドキュメントの説明 (問題点)
connected_to_all_shards の説明として、以下のようなニュアンスが書かれていました:

モデルが接続するように設定されている各 shard に対して connected_to でブロックを実行し、その結果を配列で返します (もしシャードがあれば)

ここでの “(if any)”(「もしあれば」)という文言は、「シャードが一つもない場合は何もせずに空の配列を返す」ような挙動を示唆していました。

実際の挙動 (既に変更済み)
コミット 697fc4c0b9 以降、connected_to_all_shards は以下のように動きます:

  • 対象モデルに一つもシャードが設定されていない状態で呼び出すと ArgumentError を送出 する
  • CHANGELOG とテストコードは、この挙動に合わせて既に更新済み
  • しかし RDoc だけが「シャードが無いケースも許容する」ような古い説明のままだった

この PR の修正内容

  • ドキュメントから “(if any)” を削除し、「シャードが無いケースも許容される」という読み取りをできないようにした
  • 代わりに、「対象モデルがどのシャードにも接続されていない場合は ArgumentError を送出する」ことを明記

イメージとしては、以下のような内容が RDoc に反映される形になります(擬似例):

ruby
# Executes the given block in the context of each shard the model is
# configured to connect to and returns the results in an array.
#
# Raises ArgumentError if the model is not connected to any shards.
def connected_to_all_shards(...)
  ...
end

※実際のコメント文面は多少異なる可能性がありますが、趣旨としては上記のように「エラーである」ことを明記する修正です。


  1. 影響範囲・注意点
  • 実装の挙動はこの PR では変わらない

    • 既に connected_to_all_shards は「シャード未設定時に ArgumentError を投げる」ように実装されており、この PR はそれに合わせてドキュメントを更新しただけです。
  • Rails 8.2 以降での期待される使い方

    • connected_to_all_shards を呼ぶ前提として、「そのモデルに対してシャード設定が存在する」ことを開発者側が保証する必要があります。

    • もし将来以下のようなコードを書いていると:

      ruby
      # 以前の挙動を想定しているコード (疑似コード)
      results = User.connected_to_all_shards do
        # シャードがあれば実行、無ければ空配列
        User.count
      end

      Rails 8.2 では、User がどのシャードにも紐づいていない場合に ArgumentError が発生します。この挙動自体はすでに導入済みであり、本 PR はそれをドキュメントに反映しただけですが、古いドキュメントを信じてコードを書いていた場合には注意が必要です。

  • マルチテナント / シャーディングを使うアプリのドキュメント参照者向け

    • API ドキュメントを読んで connected_to_all_shards を利用している開発者は、「シャードがないケースは安全にスキップされるのではなく、エラーになる」という前提でロジック・例外処理を設計する必要があります。
    • シャード数が動的に増減するようなシステムでは、「本当にシャード設定が存在するか」を呼び出し前に検証するか、「ArgumentError を rescue してフォールバックする」などの対策が必要です。

  1. 参考情報 (あれば)
  • この PR は #58117 のフォローアップとして作られており、実装変更・CHANGELOG 変更 → テスト更新 → RDoc 反映という流れの最終段階にあたります。
  • 挙動変更を導入したコミット: 697fc4c0b9
  • Rails 8.2 での公式な API 挙動として、「シャード未設定時は connected_to_all_shardsArgumentError を送出する」ことがドキュメントレベルでも確定します。

#58236 Fix dangling Action Text autoloads for Configurator and Registry

マージ日: 2026/7/25 | 作成者: @55728

  1. 概要 (1-2文で)
    Action Text が存在しないファイルに対して autoload を設定していたため、未定義定数参照時に本来期待される NameError ではなく LoadError が発生していた問題を修正した PR です。不要かつ誤った autoload を削除し、ActionText::ConfiguratorActionText::Registry 参照時に NameError を返すようにしています。

  1. 変更内容の詳細

問題点

ActionText モジュールは以下のような autoload を定義していました:

ruby
# イメージ(今回削除されたもの)
autoload :Configurator, "action_text/configurator"
autoload :Registry, "action_text/registry"

しかし、action_text/configurator.rb および action_text/registry.rb というファイルは実際には存在しません。そのため、以下のようなコードを実行すると:

ruby
ActionText::Configurator
# => LoadError: cannot load such file -- action_text/configurator

ActionText::Registry
# => LoadError: cannot load such file -- action_text/registry
  • 定数自体が存在しないにもかかわらず
  • autoload が「存在しないファイルを読もうとして失敗」しているため
  • 「未定義定数」の NameError ではなく「ファイルが読み込めない」の LoadError が発生していました。

Ruby の定数解決として自然なのは、該当する定数がどこにも定義されていない場合に NameError になることなので、この状態は望ましくありません。

実際のクラス定義の場所

対象となるクラスは以下の通り、ActionText::Editor 名前空間の下に定義されています:

ruby
ActionText::Editor::Configurator # => ActionText::Editor::Configurator
ActionText::Editor::Registry     # => ActionText::Editor::Registry

これらは action_text/editor.rb から autoload されており、トップレベル (ActionText::Configurator, ActionText::Registry) では定義されていません。また、両クラスとも内部用 (:nodoc:) です。

この PR の変更

  • actiontext/lib/action_text.rb から上記 2 つの autoload 行を削除
    • 追加行数 0
    • 削除行数 2
  • これにより:
ruby
ActionText::Configurator
# => NameError: uninitialized constant ActionText::Configurator

ActionText::Registry
# => NameError: uninitialized constant ActionText::Registry

のように、通常の未定義定数と同様に NameError が発生するようになります。

テスト・CHANGELOG について

  • テストは追加されていません
    • 削除した autoload は「存在しないファイルを指しているだけ」であり、そもそも「正しい動作」と呼べるものが存在していないため
  • CHANGELOG への記載もありません
    • 定数は内部用 (:nodoc:) かつ、この autoload 自体が未リリースのコミットで追加されたものだったため、ユーザー向けの変更として扱う必要がないため

  1. 影響範囲・注意点
  • 対象:
    • ActionText::Configurator
    • ActionText::Registry
  • 変更前:
    • これらを参照すると LoadError が発生
  • 変更後:
    • これらを参照すると NameError が発生

フレームワーク本体やテストは、トップレベル (ActionText::Configurator) を参照しておらず、ActionText::Editor::Configurator / ActionText::Editor::Registry を使っているため、Rails 内部の挙動には影響しません。

注意点としては:

  • もしアプリやライブラリ側で内部定数に依存していて、誤って ActionText::Configurator / ActionText::Registry を直接参照していた場合:
    • これまでは「LoadError が出ていた」が
    • 今後は「NameError が出る」挙動に変わります
  • とはいえ、これらはそもそも内部用 (:nodoc:) であり、外部から参照するのは推奨されていません。必要であれば ActionText::Editor::Configurator / ActionText::Editor::Registry を明示的に参照するべきです。

  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/58236
  • autoload が指すファイルが存在しない場合に LoadError となり、単に定数が未定義な場合は NameError となる、という Ruby の定数解決・autoload の基本挙動を整理したい場合は Ruby リファレンスの Module#autoload と定数解決に関する節が参考になります。

#58231 Fix reason phrase casing in the 404 error page title

マージ日: 2026/7/25 | 作成者: @55728

  1. 概要 (1-2文で)
    404エラーページのHTMLタイトルに含まれる HTTP リーズンフレーズの表記を、仕様どおり「Not Found」に統一するPRです。ガイドや各エンジンのダミーアプリを含め、404 Not found となっていた箇所をすべて 404 Not Found に修正しています。

  1. 変更内容の詳細

何を直したか

Rails 7の新しい静的エラーページでは、タイトル末尾に HTTP ステータスコード+リーズンフレーズが入りますが、404 だけ表記が崩れていました。

  • 400: (400 Bad Request)
  • 404: (404 Not found) ← f が小文字
  • 406: (406 Not Acceptable)
  • 422: (422 Unprocessable Entity)
  • 500: (500 Internal Server Error)

HTTPの正式なリーズンフレーズは RFC 9110 §15.5.5 で Not Found と定義されているため、ここを修正しています。

具体的な変更箇所

以下の全てで、404 Not found404 Not Found に差し替え:

  1. アプリ生成時に出力される 404 ページテンプレート

    • railties/lib/rails/generators/rails/app/templates/public/404.html

    例(イメージ):

    diff
    - <title>The page you were looking for doesn't exist (404 Not found)</title>
    + <title>The page you were looking for doesn't exist (404 Not Found)</title>
  2. Active Storage / Action Mailbox / Action Text の test/dummy アプリの 404 ページ

    • activestorage/test/dummy/public/404.html
    • actionmailbox/test/dummy/public/404.html
    • actiontext/test/dummy/public/404.html
  3. ガイド内の記述

    • guides/source/configuring.md
    • guides/source/upgrading_ruby_on_rails.md

    こちらもガイド文中の文字列を同様に 404 Not Found に統一。

補足

  • 404 ページの SVG 内の文言はすでに "Not Found" になっており、HTML <title> 側をそれに合わせた形です。
  • 仕様(RFC 9110)に沿った正しいリーズンフレーズへのキャピタライゼーション修正のみで、文言や構造の変更はありません。

  1. 影響範囲・注意点
  • 影響範囲
    • 新規に rails new でアプリを作った場合に生成される public/404.html<title> 表記が変わります。
    • Rails リポジトリ内のダミーアプリとガイドの表記が整合します。
  • 実行時の挙動・レスポンス
    • レスポンスステータスやヘッダ、ボディ構造には変更なし。
    • 変更は静的HTML内のテキストだけであり、機能的な影響はありません。
  • 既存アプリへの影響
    • 既に生成済みの public/404.html は自動では書き換わらないため、必要であれば手動でタイトルを修正してください。
    • ログ収集やE2Eテスト等で <title> の文字列を厳密比較している場合のみ、差異が出る可能性があります。

  1. 参考情報 (あれば)

#58234 [ci-skip][doc] Update link styles in generator guide

マージ日: 2026/7/25 | 作成者: @hachi8833

  1. 概要 (1-2文で)
    Rails ガイド「Generators Guide」の中で、ジェネレータ関連 API へのリンクを edge API から安定版 API に変更し、外部リンクの書き方を統一して IDE のシンタックスハイライトと相性が良いスタイルに整えたドキュメント修正の PR です。機能コードは一切変更されていません。

  1. 変更内容の詳細

リンク先の変更(edge → stable)

元のガイドでは、ジェネレータのアクション API への参照として edge API を指していました:

diff
- https://edgeapi.rubyonrails.org/classes/Rails/Generators/Actions.html
+ https://api.rubyonrails.org/classes/Rails/Generators/Actions.html

これにより、以下のような効果があります:

  • edgeapi.rubyonrails.org: 次期バージョン開発中の最新版ドキュメント(変更が多く不安定)
  • api.rubyonrails.org: リリース済みの安定版ドキュメント

Generators ガイドは通常「リリース済み Rails を使う開発者」向けのため、edge ではなく安定版 API を参照するように修正されています。

外部リンク表記の統一

PR 説明では、IDE でのシンタックスハイライトに合わせて「外部リンクのスタイルを一貫させた」とあります。
実際の diff では guides/source/generators.md 内で、URL やリンクの書き方が複数箇所で編集されています(+69 / -56 行)。

典型的には、次のようなスタイル統一が想定されます:

  • テキストと URL を分離して、IDE が URL だと認識しやすい/シンタックスハイライトの邪魔をしないよう整理
  • コードブロック内ではコメントとして URL を書かないようにする、あるいは逆に「コードとして扱われない位置」に URL を移動する、等

PR のスクリーンショットからも、IDE 上でリンク部分の色分けが変わっていることが分かります。
意図としては:

  • Markdown とコードブロックの混在部分で、
    • 「コード(Ruby 等)」と
    • 「ドキュメント上の外部リンク」
      を明確に分けることで、IDE のハイライトと補完がより素直に働くようにする

という「見た目・編集体験の改善」が主です。


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

    • Rails 本体の機能・挙動には一切影響しません。
    • 影響するのは、guides/source/generators.md を元に生成される「Generators Guide」のみです。
    • 読者がクリックしたときに、edge API ではなく安定版 API のドキュメントに飛ぶようになります。
  • 注意点

    • edge の機能やまだリリースされていない API を前提に解説しているドキュメントではないため、edge API へのリンクは今後も原則避けるべき、という運用上の示唆になります。
    • ガイドから API ドキュメントへリンクを貼る際は、edgeapi ではなく api を使うのが推奨される流れになりそうです。
    • ドキュメント執筆・レビュー時に、
      • URL ドメイン(edgeapi か api か)
      • リンクの Markdown 記法の統一
        をチェックするのが良さそうです。

  1. 参考情報 (あれば)

#57211 Add config to raise on invalid TimeZone#parse strings

マージ日: 2026/7/25 | 作成者: @Saidbek

  1. 概要 (1-2文で)
    ActiveSupport::TimeZone#parse に対して「不正な文字列を渡したときに必ず ArgumentError を投げる」ための設定フラグ ActiveSupport.raise_on_invalid_time_zone_parse が追加されました。Rails 8.2 の config.load_defaults "8.2" ではこのフラグがデフォルトで有効になり、これまで曖昧だった挙動が Ruby 標準の Time.parse に揃えられます。

  1. 変更内容の詳細

これまでの問題点

ActiveSupport::TimeZone#parse は、無効な文字列に対して挙動が2パターンに分かれていました:

  • 日付情報がまったく読み取れない文字列
    例: "foobar"
    nil を返す

  • 一応パースはされるが、範囲外の値を含む文字列
    例: "9000"Date._parse が「月 90」と解釈してしまうケース)
    ArgumentError を raise

このため、「文字列が不正かどうか」を一貫した方法で扱いづらく、nil を見逃してバグの温床になり得ました。

今回の変更のゴール

  • 不正な文字列は 常に ArgumentError を raise させる
  • Ruby標準の Time.parse と同様の挙動に揃える
  • ただし互換性のために、挙動変更はフラグで制御する

追加された設定フラグ

ruby
# 新しい設定フラグ
ActiveSupport.raise_on_invalid_time_zone_parse
# デフォルト: false
# ただし config.load_defaults "8.2" では true

ActiveSupport::TimeZone#parse 内部で、このフラグの値に応じてエラーを投げるか、従来通り nil を返すかを切り替えるように変更されています。

挙動イメージ

旧挙動(Rails < 8.2、またはフラグ false の場合)

ruby
Time.zone.parse("foobar")
# => nil

Time.zone.parse("9000")
# => ArgumentError: argument out of range

新挙動(ActiveSupport.raise_on_invalid_time_zone_parse = true の場合、または config.load_defaults "8.2"

ruby
Time.zone.parse("foobar")
# => ArgumentError: argument out of range (or similar)

Time.zone.parse("9000")
# => ArgumentError: argument out of range

つまり、「パース不可能な文字列」も、「パースはされるが値が変」の文字列も同様に ArgumentError で統一されます。

Rails アプリでの設定方法

明示的に有効化する

ruby
# config/application.rb などで
config.after_initialize do
  ActiveSupport.raise_on_invalid_time_zone_parse = true
end

もしくは、initializer で:

ruby
# config/initializers/time_zone_parse.rb
ActiveSupport.raise_on_invalid_time_zone_parse = true

load_defaults 8.2 でのデフォルト

config/application.rb で:

ruby
config.load_defaults "8.2"

とした場合、自動的に ActiveSupport.raise_on_invalid_time_zone_parse = true が適用されます。
new_framework_defaults_8_2.rb のテンプレートにも、この設定をトグルできる項目が追記されています。

ドキュメント・テストの更新

  • activesupport/CHANGELOG.md に挙動変更と設定フラグが追記
  • guides/source/configuring.md にフラグの説明が追加
  • ActiveSupport::TimeZone#parse の Yard コメントで、invalid string に対する挙動が明記
  • time_zone_test.rb および Rails 側の configuration テストで、フラグのデフォルト値や切り替え挙動をカバー

  1. 影響範囲・注意点

影響を受けるコード

影響が出るのは、主に次のようなコードです:

  • Time.zone.parse の戻り値が nil になるケースを前提にしているコード
    • 例: "foobar" のような完全に不正な文字列はこれまで nil でしたが、フラグ有効時は例外になります。
  • ユーザー入力を直接 Time.zone.parse に渡しているフォーム処理・API エンドポイントなど

具体例

これまで動いていたコード:

ruby
time = Time.zone.parse(params[:start_at])
if time
  # 有効なときの処理
else
  # 不正なときの処理
end

Rails 8.2 のデフォルト(フラグ true)では、不正な入力時に ArgumentError が発生してこの if 分岐まで到達しません。
対応策としては、たとえば以下のように rescue する形になります:

ruby
begin
  time = Time.zone.parse(params[:start_at])
  # 有効なときの処理
rescue ArgumentError
  # 不正なときの処理
end

あるいは、どうしても旧挙動を維持したい場合はフラグを false に戻します。

ruby
# config/initializers/new_framework_defaults_8_2.rb などで
ActiveSupport.raise_on_invalid_time_zone_parse = false

マイグレーション戦略

  • 既存アプリで Rails 8.2 に上げる際は:

    1. 移行初期は ActiveSupport.raise_on_invalid_time_zone_parse = false にして(旧挙動維持)
    2. ログやテストで Time.zone.parse まわりの挙動を確認
    3. 準備ができたタイミングで true に切り替え、ArgumentError を適切にハンドリングするようコードを修正
  • 新規アプリ(load_defaults "8.2")では:

    • 最初から「不正入力 → 例外」が前提になるため、Time.zone.parse を使う箇所は必ず例外処理を含める設計にしておくと安全です。

  1. 参考情報 (あれば)
  • このPRは #57194 の follow-up で、Time.zone.parse の不一致なエラー挙動を解消する目的のものです。
  • Ruby 標準の Time.parse に揃えたい場合は、このフラグを true にするのが推奨の方針になります。

#58225 Make lazy route loading thread-safe

マージ日: 2026/7/24 | 作成者: @grk

  1. 概要 (1-2文で)
    Rails 8 で導入された「ルーティングの遅延読み込み (lazy route loading)」にスレッドセーフでない箇所があり、初回リクエストが並行すると一部ルートが 404 になったり URL ヘルパが未定義になるレースコンディションが発生していました。
    このPRでは RoutesReloader に再入可能なロックと明示的なロード状態管理を導入し、初回ロードおよびリロードをスレッドセーフにする変更が行われています。

  1. 変更内容の詳細

背景となる問題

  • Rails 8.0 以降(config.eager_load = false のとき)、ルーティングは「最初のリクエストまで描画(draw)を遅延」するようになった。
  • RoutesReloader は「ルートが読み込まれたかどうか」を示すフラグ(@loaded)を 描画開始前に true にしていた
  • マルチスレッド環境(開発用のスレッドサーバ、並列で動くテストなど)で、
    • スレッドAがルート描画を開始している途中で
    • スレッドBが @loaded を見て「もうルートはできている」と誤認
    • まだ定義されていないルート/URLヘルパにアクセスして 404 や NoMethodError が発生
      というレースコンディションが起きていた。
  • さらに、二つのスレッドが同時にルート描画を開始してしまうこともあり得た。

一時的なワークアラウンドとして「テストの前に Rails.application.routes.eager_load! を呼ぶ」という対処は可能だが、根本原因の解消ではない。


新しいロード状態管理 (@load_state)

@loaded フラグを廃止し、代わりに @load_state を導入:

  • @load_state の状態遷移は3値:
    • nil : まだ一度もロードされていない
    • :loading : 今まさにロード中(ルート描画中)
    • :loaded : ロード完了(after_routes_loaded まで全て終わっている)

この1つの状態で、

  • 「すでにルートが読み込まれているか?」
  • 「いま誰かがロード中か?」
  • 「同じスレッドからの再入か?」

を区別・制御できるようにしている。


モニターロック (Monitor) によるスレッドセーフ化

RoutesReloader は、ルート描画処理を 再入可能な Monitor の中で実行するようになりました。

主なポイント:

  1. すべての「ルートを描画する入り口」をMonitorで保護

    • 実際にルートを描画する処理の直前で @load_state:loading にし、Monitor を取得してから描画。
    • 描画終了後に :loaded に変更(もしくは元の状態に戻す)し、Monitor を解放。
  2. 同一スレッドからの「再入」を安全に無視

    • ルート定義中に routes.draw を呼んだり、URLヘルパを呼んだりすると、「ルート描画を遅延ロード経由で再度呼ぶ」形で再入が起きうる。
    • このとき、同じスレッドはすでに :loading 状態でMonitorを保持しているので、ネストした呼び出しは「すでにロード中である」とみなし、実際の描画処理には入らずショートサーキットする。
    • これにより「無限再帰的な描画」や「同じスレッドで二重に描画しようとする」ことを防いでいる。
  3. 他スレッドはMonitorでブロックされる

    • 別スレッドが初回リクエストでルートに触れた場合、@load_state == :loading であることを検知し、Monitorで待機。
    • 描画が終わると :loaded になり、待っていたスレッドは「描画後の完全なルートセット」に対してディスパッチ / URLヘルパ解決を行える。
  4. after_routes_loaded まで完了してから :loaded にする

    • 初回ロード(遅延ロード)時に、ルート描画だけでなく after_routes_loaded フックの実行も完了した段階で :loaded とする。
    • これにより、2回目以降のリクエストはロックを取らずに「すでに完全にロード済み」であることを前提に素早く処理できる(fast path)。
  5. ロードに失敗した場合の扱い

    • 描画中に例外が発生した場合でも、ensure 節で @load_state を以前の状態に戻す(nil:loaded)。
    • 失敗したロードは決して :loaded にはならない ため、待っているスレッドは再度ロードを試み、今度は本来のエラーを見られる。
    • これにより「半分だけ描画されたルートセット」に対してディスパッチしてしまう事態を防ぐ。特別な rescue は不要。

reload! / reload_routes! の挙動の整理

reload! 内部に上述のガードが組み込まれたことで、以前 #54306 で入った reload_routes! のワークアラウンドが不要になりました。

  • 以前は、「初回の遅延ロードがまだ行われていない状態で reload_routes! を呼ぶ」場合の問題を避けるため、特別な回り道が入っていた。
  • 現在は、reload! を初回ロード前に呼んでも安全 になっており、reload_routes! はワンライナーに戻されている。

挙動上の微妙な変化:

  • 初回遅延ロード前に強制 reload! を行った場合:
    • 以前: after_routes_loaded フックが2回呼ばれる可能性があった(ワークアラウンド由来 + 本当の初回ロード)。
    • 変更後: フックは 初回の「本当の初回ロード」時に1回だけ 呼ばれる。
  • RoutesReloader#loaded の reader/writer は、もはや使用されなくなったため削除された(@load_state に統合されたため)。

テストの追加

新しいテストは、意図的に「ルート描画の途中で止める」仕掛けを入れ、別スレッドから以下を行ってレースを再現・検証しています。

  • HTTPリクエストを投げてルーティングさせる
  • URLヘルパ(app.some_path など)を呼ぶ
  • respond_to? でヘルパの存在チェックを行う

これらは現在の main ブランチでは失敗し、このPRの変更後には成功することが確認されている。
また、「ルート描画が失敗するケース」の単体テストも追加され、失敗時に中途半端な状態のルートセットでディスパッチされないことを確認している。
既存の reload_routes! のテスト(特に #54306 の rake タスク関連)も引き続きパスしている。


  1. 影響範囲・注意点
  • 主に影響する環境

    • config.eager_load = false な環境(開発・テスト)で、
      かつマルチスレッドで初回のリクエストや初回の URL ヘルパ呼び出しが走るケース。
    • Rails 8 で導入された lazy route loading を有効活用しているアプリ全般。
  • 期待される改善

    • 開発サーバ(Puma などのスレッドサーバ)や並列テストで、
      「ルート定義はあるのに初回だけ 404 / NoMethodError がランダムに出る」不定な挙動が解消される。
    • Rails.application.routes.eager_load! を初回前に手動で呼び出して回避する必要が減る。
  • 互換性 / 挙動差のポイント

    • public API の挙動は基本的に変わらないが、after_routes_loaded フックが「初回ロードにつき1回のみ」呼び出されるように揃えられた。
    • もし「初回遅延ロード前に強制 reload! すると after_routes_loaded が2回動く」ことに依存したコードがあれば挙動が変わるが、そのような依存は通常想定されていないはず。
    • RoutesReloader#loaded への直接アクセス(非公式・内部API依存)をしていた場合は壊れる可能性があるが、一般的なアプリでは通常使わない部分。
  • パフォーマンス

    • 初回ロード中は Monitor によるロック・待機が入るが、これは元々「初回だけ重い」処理であり、
      完了後は :loaded 状態でロックフリーな fast path が使われる設計になっているため、
      通常運用時のパフォーマンスへの影響は極小と考えられる。

  1. 参考情報 (あれば)
  • Rails 8 の lazy route loading 導入PR: #52353(元: #51614)
  • 過去の関連PR(reload_routes! ワークアラウンド): #54306
  • このPRが解決する典型的な症状:
    • 並列テストのごく一部だけが 404 / NoMethodError で落ちる
    • 開発環境でサーバ再起動直後、最初の2〜3リクエストのうちどれかだけが謎のルーティングエラーになる

本PRは、lazy route loading を使う前提の Rails 8 系アプリでの「初回アクセス時の不定なバグ」を根本から解消するためのスレッドセーフ化パッチと位置付けられます。


#58199 Stop filtering i18n paths on initialize

マージ日: 2026/7/24 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    Rails の i18n ロード処理とパスのグロブ処理から「初期化時の不要なフィルタリング」と「不要なソート」を取り除き、高負荷なアプリケーションでの起動・ロード時間を短縮する変更です。i18n のファイルパスの存在チェックを初期化時ではなくリロード時に遅延させることで、数千ファイル規模でもオーバーヘッドを抑えています。

  1. 変更内容の詳細

背景・問題点

  • Rails は config.i18n.load_path に対して、
    • パスのグロブ展開(**/*.yml など)
    • 存在しないファイルの除外(File.exist? / File.stat 相当)
      を初期化時に行っていました。
  • アプリケーションのロケールファイルが数千件になると、
    • ファイル存在チェックが大量発生し、起動時・ロード時が顕著に遅くなる
    • ベンチマーク例では i18n ロードが 400ms → 約 250ms まで短縮
  • また、Dir.glob に対して明示的に sort していましたが、Ruby 側で既に安定した順序を提供しているため二重ソートになっていました。

この PR はこれらの「重複した・不要な処理」を削るものです。


具体的なコードレベルの変更点

※実際の diff から推測されるロジックの整理です(変更ファイルは少量ですが、挙動に影響が出る部分です)。

1) i18n 初期化時のフィルタリングをやめる

対象: activesupport/lib/active_support/i18n_railtie.rb(1行差し替え)

  • 以前は、Rails::Paths::Path の「存在するファイルだけを返す」系メソッド(例:existent のようなもの)を使って config.i18n.load_path を構築していた可能性があります。
  • この PR では、その「初期化時のフィルタリング」をやめて、生の expanded(または同等のメソッド)結果をそのまま load_path に突っ込むような変更になっています。
  • 「存在しないファイルの除外」は、実際にリロードを行うタイミング(たとえばファイル監視で再読み込みする時など)に遅延させる方針に変更。

結果として:

  • i18n 初期化時に行っていたファイル存在チェックのループがなくなり、起動時の負荷が軽減。

2) パスのグロブ処理から余計な処理を削減

対象: railties/lib/rails/paths.rb(+5/-1)

  • Rails::Paths::Root#add などで指定される glob: "**/*.{rb,yml}" の処理において、
    • 以前は Dir.glob(glob).sort のような処理、もしくはフィルタリング+ソートの複合処理を行っていた。
  • この PR で:
    • Ruby が Dir.glob の結果順序をプラットフォーム間で安定させていることを前提に、明示的な sort を削除。
    • もしくは、各 Path オブジェクト内の expanded/existent といったメソッドでの二重処理を解消している。

意図:

  • グロブ結果に対する追加のソート・フィルタリングを削除し、I/O とメモリアロケーションを減らす。
  • i18n に限らず、Rails::Paths を利用している他のパス(app/**/*, lib/**/* など)の展開にもメリットが出る。

3) テストと CHANGELOG

  • railties/test/paths_test.rb (+10)
    • 新しいグロブ動作・フィルタリングタイミングの仕様に合わせたテストを追加。
    • 例として:
      • グロブ結果に対して明示的ソートをしない前提のテスト
      • 存在チェックをどのタイミングで行うかに依存しないテスト
  • railties/CHANGELOG.md (+5)
    • 「i18n のロードパフォーマンスが改善された」「パスのグロブ処理が軽量化された」といった記述を追加。

4) ベンチマークコードのポイント

PR に含まれているベンチマークスクリプトは、Rails 本体ではなく挙動確認用です:

rb
root = Rails::Paths::Root.new(tmp)
root.add "config/locales", glob: GLOB
path = root["config/locales"]

filter_method = File.read(RAILTIE)[/load_path\.unshift\(\*value\.flat_map\(&:(\w+)\)\)/, 1].to_sym

Benchmark.ips do |x|
  x.report("main")   { path.public_send(filter_method) }
  x.report("branch") { path.public_send(filter_method) }
end
  • load_path.unshift(*value.flat_map(&:XXX)) という記述から XXX を抜き出し、そのメソッド(expanded または existent など)をベンチマーク。
  • 「main」(マージ前)と「branch」(この PR 適用後)でどれくらい速くなるかを比較しており、
    • 74.8 i/s → 119.2 i/s(約 1.6 倍)という結果。

  1. 影響範囲・注意点

影響範囲

  • 主に影響するのは:
    • config.i18n.load_path に大量のロケールファイルを指定しているアプリケーション
    • Rails::Paths を通じて大規模なグロブ(app/**/*, config/**/* など)を行っているケース
  • 期待される効果:
    • Rails 起動時(初期化時)の i18n ロードが高速化
    • ファイル数が多いほど効果が出やすい
    • 他のパスグロブ処理も少なからず高速化される可能性がある

挙動上の注意点・互換性

  • 「存在しないファイルが load_path に一時的に入る」可能性:
    • 初期化時にフィルタしないため、理論上は config.i18n.load_path に「今は存在しないが、後で作られるかもしれない」パスが残りやすくなります。
    • ただし、実際に i18n がファイルを読むタイミング・リロードタイミングでフィルタリングされるよう設計しているため、通常利用では問題になりにくい想定です。
  • ソート順依存のコードがある場合:
    • これまでも Ruby 側の Dir.glob 依存だったはずですが、明示ソートが消えることで、
      「たまたま sort に頼っていた」コードがあれば影響する可能性があります(ただし通常の Rails アプリではほぼ無関係)。
  • Rails 内部 API 的な Rails::Paths の挙動に依存したメタプログラミングをしている場合:
    • expanded/existent のようなメソッド呼び出しタイミングや戻り値の詳細な仕様に依存していると、若干挙動が変わる可能性があります。

  1. 参考情報 (あれば)
  • PR 本体:
    https://github.com/rails/rails/pull/58199
  • 変更対象の主なファイル:
    • activesupport/lib/active_support/i18n_railtie.rb
    • railties/lib/rails/paths.rb
  • 関連する設計上のポイント:
    • 大規模プロジェクトでは、ファイルの存在チェック (File.exist?, File.stat) がボトルネックになりやすく、それを「必要なタイミングにだけ行う」ことがパフォーマンス改善の基本パターンである、という事例になっています。
  • パフォーマンス目安(PR 提示値):
    • i18n ロード時間: 400ms → 約 250ms
    • ベンチマーク: ~1.6x 高速化(74.8 i/s → 119.2 i/s)

#58227 Remove dead branch in MySQL column introspection

マージ日: 2026/7/23 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    MySQL アダプタのカラム情報取得処理(カラム introspection)から、もはや到達しない条件分岐(デッドブランチ)が削除されました。MariaDB における関数デフォルト値の検出ロジックが別の場所に移されたため、古い最適化用の分岐が不要になったことによるクリーンアップです。

  1. 変更内容の詳細

対象ファイル:

  • activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb(2行削除、追加なし)

背景となる挙動:

  • 4ccd79cdd6 の変更で、default_type メソッドがテーブルのデフォルト値を分類する際に、各カラムごとに SHOW CREATE TABLE を実行して
    • :string
    • :integer
    • :function
      などの種別を判定するようになっていました。
  • ただし SHOW CREATE TABLE は重いクエリのため、**「数値で始まるデフォルト値(/\A\d/)は数値とみなして即座に処理を打ち切る」**というショートカット分岐が入っていました。この分岐により、よくある数値デフォルト(例: 0, 1, 100 など)では高コストな SHOW CREATE TABLE 呼び出しをスキップしていました。

その後:

  • c2c158c760 の変更で、MariaDB における「関数デフォルト値(例: CURRENT_TIMESTAMP など)」の検出ロジックは column_definitions に移動しました。
  • これにより、default_type の中で「関数デフォルト値かどうかを判定するために SHOW CREATE TABLE を呼ぶ」必要性がなくなり、結果として /\A\d/ で数値デフォルトをショートカットする最適化分岐も意味を失い、実際には到達しない(デッド)コードになっていた という状況です。

今回の PR:

  • 上記の「数値で始まるデフォルト値の場合に早期 return するブランチ」を削除しています。
  • 実際の差分は 2 行削除のみで、新たなロジック追加や挙動変更は行われていません。

(イメージ的には以下のような分岐が消えたイメージです:)

ruby
# 以前(簡略イメージ)
if default =~ /\A\d/
  # 数値デフォルトとして扱って早期終了
else
  # SHOW CREATE TABLE を使った詳細判定
end

# 今回、その if 分岐が削除され、共通経路だけが残る

  1. 影響範囲・注意点
  • 対象: ActiveRecord の MySQL(正確には mysql2 系)アダプタにおける スキーマ情報の introspection 処理 のみ。
  • ロジック上の意味:
    • すでに MariaDB の関数デフォルト検出は column_definitions で完結しているため、この分岐削除によって MariaDB 向けの動作が変わることはない という前提のクリーンアップです。
    • 「デッドブランチ」であったことから、実行時の挙動に変化はない想定です(つまり、PR 前後で schema_statements の公開 API の振る舞いは変わらないはず)。
  • パフォーマンスへの影響:
    • 本来は「高コストな SHOW CREATE TABLE を避けるための最適化」が削除されたように見えますが、そもそもその経路が使われていなかった(別の場所で判定している)ため、実運用でのパフォーマンスへの影響はないと考えられます。
  • 互換性:
    • 公開 API のシグネチャや戻り値の型などは一切変わっていないため、アプリケーションコードレベルの互換性問題は発生しない想定です。
    • もし独自に ActiveRecord の MySQL アダプタを monkey patch しており、default_typeschema_statements 内部の具体的な分岐に依存している場合は、念のため差分を確認すると安全です(しかし一般的にはそのような依存は稀)。

  1. 参考情報 (あれば)
  • この PR の説明で言及されている関連コミット:
    • 4ccd79cdd6: default_type 内で SHOW CREATE TABLE を用いる実装と、それを回避する /\A\d/ の最適化が導入されたコミット。
    • c2c158c760: MariaDB の関数デフォルト検出ロジックを column_definitions に移したコミット。これにより今回のデッドブランチが不要になった。
  • 関連するコンテキスト:
    • 「関数デフォルト値」には CURRENT_TIMESTAMP, NOW(), UUID() など、DB サーバー側で値が決定されるものが含まれます。
    • ActiveRecord は、これらを文字列リテラルのデフォルト(例: 'foo')や数値リテラルデフォルト(例: 0)と区別する必要があり、その判定の一部で SHOW CREATE TABLE を使っていました。今回のクリーンアップは、その過去の実装痕跡を削除するものです。

#58143 Carry disabled and form onto a multiple file field's hidden companion input

マージ日: 2026/7/23 | 作成者: @carldaws

  1. 概要 (1-2文で)
    複数ファイルアップロード用の file_fieldmultiple: true)かつ include_hidden: true のときに生成される「隠し入力(hidden)」に、元のファイル入力の disabledform 属性を正しく引き継ぐようにしたバグ修正です。これにより、無効化されたフィールドや外部フォームに属するフィールドで、送信されるパラメータが意図どおりになるよう整合性が取られました。

  1. 変更内容の詳細

何が問題だったか

file_field を以下のように使うとします:

ruby
<%= form_with model: @import do |f| %>
  <%= f.file_field :files, multiple: true, include_hidden: true %>
<% end %>

include_hidden: true のとき、複数値フィールドと同様に

  • 1つもファイルが選択されなかった場合でも
  • 「空配列」としてパラメータを送るため

に、Rails は 補助的な hidden input を自動で生成します。

ところが、これまで hidden 側には

  • name だけがコピーされ
  • disabled
  • form (外部フォーム用属性)

がコピーされていませんでした。

その結果:

  1. disabled な file_field でも hidden が有効になってしまう

    html
    <!-- before -->
    <input name="import[files][]" type="hidden" value="" />
    <input multiple="multiple" disabled="disabled"
           type="file" name="import[files][]" id="import_files" />

    見た目のファイル入力は disabled で送信されないはずですが、
    hidden 側は有効なので import[files][]= が送信されてしまいます。
    → 本来「無効なフィールドからは何も送られないべき」との期待に反します。

  2. form 属性による外部フォーム利用時に hidden がそのフォームに属さない

    html
    <!-- before -->
    <input name="import[files][]" type="hidden" value="" />
    <input multiple="multiple" form="uploads"
           type="file" name="import[files][]" id="import_files" />

    この場合、<form id="uploads"> を送信しても、hidden は uploads フォームに属していないため、

    • ファイル未選択時に import パラメータ自体が送られない

    という挙動になります(空配列を送りたいのに何も送られない)。

何を直したか

hidden input を生成するロジックを変更し、file_field の visible な入力と同じく、

  • disabled
  • form

を hidden 側にもコピーするようにしました。

修正後の HTML 出力例:

  1. disabled の場合
html
<!-- after -->
<input type="hidden" name="import[files][]" value="" disabled="disabled" />
<input id="import_files" disabled="disabled"
       multiple="multiple" name="import[files][]" type="file" />
  • hidden も disabled になるため、このフィールドに関する値は一切送信されません。
  1. 外部フォーム (form 属性) の場合
html
<!-- after -->
<input type="hidden" name="import[files][]" value="" form="uploads" />
<input id="import_files" multiple="multiple" form="uploads"
       name="import[files][]" type="file" />
  • visible input / hidden input ともに form="uploads" になり、外部フォーム送信時に

    • ファイル未選択 → import[files][]= (空配列)としてパラメータが送られる

という、他の複数値フィールド同様の一貫した挙動になります。

実装位置・テスト

  • 実装変更:
    actionview/lib/action_view/helpers/tags/file_field.rb

    • hidden input 生成部分の属性セットを 1行修正(disabled/form も含めるように)
  • テスト追加:
    actionview/test/template/form_helper_test.rb (+16 行)

    • disabled な multiple file field の hidden も disabled になること
    • form 属性付き multiple file field の hidden にも form が付与されること
      を確認するテストが追加されています。

また、この挙動はすでに

  • check_box 用の hidden input (check_box.rb)
  • select (multiple) の hidden input (select_renderer.rb)

では実装済みであり、本 PR はそれと同じ方針に file_field を揃える変更です。


  1. 影響範囲・注意点

影響範囲

対象となるのは:

  • file_field / form.file_field
    • multiple: true
    • include_hidden: true で使っているケース

です。単一ファイルの file_fieldinclude_hidden: false の場合には影響しません。

具体的な影響:

  1. disabled な multiple file_field

    以前:

    • ファイル入力は disabled でも hidden が有効なため
      import[files][]= が送信されていた

    これから:

    • hidden も disabled となり
      → パラメータ自体が送信されない

    アプリ側で「disabled な multiple file フィールドなのに値が来る」という前提のワークアラウンドを入れていた場合は、そのロジックが不要になるか、挙動が変わる可能性があります。

  2. 外部フォーム(form 属性)を使った multiple file_field

    以前:

    • ファイルを選ばなかったとき
      → 対象フォームから hidden が送信されず、パラメータが一切来ない

    これから:

    • hidden も対象フォームに属するため
      import[files][]= (空配列)としてパラメータが常に送信される

    コントローラ側で「パラメータが存在しないとき」と「空配列が送られてきたとき」を区別して扱っている場合、挙動の確認が必要です。ただし、Rails が他の複数値フィールドで採用している一貫した仕様に揃えた形なので、一般的には改善とみなせます。

後方互換性

  • Rails 内の他のヘルパ(check_box, multiple select)の既存仕様と一致する方向への変更であり、仕様としては自然なバグ修正です。
  • ただし、上記のように「これまでのバグに依存した挙動」を利用していた場合のみ、アプリ側のコードに影響が出る可能性があります。

  1. 参考情報 (あれば)
  • 類似修正: multiple select の hidden input に disabled / form をコピーした PR
    → #58064
  • 実ブラウザ検証: Selenium + Chrome による確認済み
    • disabled な multiple file field: 空配列を送信
    • 外部フォームの file field: ファイル未選択の場合パラメータを送らない(フォーム構成に応じた期待どおりの挙動)
  • 変更は Action View のみで、行数も小規模(+17 / -1)なピンポイント修正です。

#58179 Raise ConfigurationFile::FormatError for invalid YAML

マージ日: 2026/7/23 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    ActiveSupport::ConfigurationFile で不正な YAML を読み込んだ際に、これまで汎用的な RuntimeError として扱われていた例外を、本来用意されていた専用例外 ActiveSupport::ConfigurationFile::FormatError で送出するように変更した PRです。これに伴い、Active Record の fixture ローダー側も、この専用例外を明示的に補足するように修正されています。

  1. 変更内容の詳細

2-1. ConfigurationFile の例外クラスの実利用

もともと ActiveSupport::ConfigurationFile には以下のような専用例外が定義されていました:

ruby
module ActiveSupport
  class ConfigurationFile
    class FormatError < StandardError; end
    # ...
  end
end

しかし、実際に YAML パースエラーが起きた場合は、例外オブジェクトではなく「文字列を raise する」形になっており、その結果 Ruby の標準挙動として単なる RuntimeError が投げられていました。

この PR では、不正な YAML を検出した場合に、この専用例外クラスを使うように修正しています。イメージとしては以下のような変更です(擬似コード):

ruby
# 変更前(イメージ)
def read(path)
  begin
    YAML.load_file(path)
  rescue Psych::Exception => e
    raise "YAML syntax error occurred while parsing #{path}. #{e.message}"
    # => RuntimeError が飛ぶ
  end
end

# 変更後(イメージ)
def read(path)
  begin
    YAML.load_file(path)
  rescue Psych::Exception => e
    raise FormatError, "YAML syntax error occurred while parsing #{path}. #{e.message}"
    # => ActiveSupport::ConfigurationFile::FormatError が飛ぶ
  end
end

ポイント:

  • 例外階層は StandardError のまま(互換性は維持)。
  • ただしクラスが固有になることで、アプリ側やフレームワーク側で「設定ファイルのフォーマットエラーだけ」を明示的に捕捉しやすくなります。

2-2. Fixture ローダー側の rescue の明確化

Active Record の fixture ローダー (ActiveRecord::FixtureSet::File 周辺) は、これまでも設定ファイルパースエラーを独自例外 ActiveRecord::Fixture::FormatError にラップし直していましたが、その際に RuntimeError を rescue 対象にしていました。

この PR では、ActiveSupport::ConfigurationFile::FormatError が実際に投げられるようになるため、fixture 側も次のように修正されています(イメージ):

ruby
# 変更前
rescue RuntimeError => error
  raise ActiveRecord::Fixture::FormatError, error.message
end

# 変更後
rescue ActiveSupport::ConfigurationFile::FormatError => error
  raise ActiveRecord::Fixture::FormatError, error.message
end

これにより、

  • 想定しているのは「設定ファイル(YAML)のフォーマットエラー」だけ
  • 他の RuntimeError まで誤って飲み込まない

という意図どおりの挙動になります。

2-3. テストと CHANGELOG の更新

  • activesupport/test/configuration_file_test.rb に、Malformed YAML を与えたときに ConfigurationFile::FormatError が発生することを確認するテストが追加されています。
  • railties/test/application/configuration_test.rb も、エラークラスの変更に追随する形で1行更新。
  • activesupport/CHANGELOG.md に、この挙動変更(エラークラスが RuntimeErrorConfigurationFile::FormatError になること)が記載されています。

  1. 影響範囲・注意点

3-1. 例外クラス変更による互換性への影響

直接 ActiveSupport::ConfigurationFile を使っているコードで、以下のように RuntimeError を前提に rescue していた場合は、挙動が変わります:

ruby
# これまで動いていたパターン
begin
  ActiveSupport::ConfigurationFile.new("config.yml").read
rescue RuntimeError => e
  # YAML のパースエラーをここで処理していた
end

今後は ConfigurationFile::FormatError を捕捉する必要があります:

ruby
begin
  ActiveSupport::ConfigurationFile.new("config.yml").read
rescue ActiveSupport::ConfigurationFile::FormatError => e
  # 設定ファイルのフォーマットエラーだけを処理
end

StandardError 全体を rescue しているコードは、そのままでも動作しますが、今回の変更に合わせて、意図として「設定ファイルフォーマットエラーだけ」を対象にしたい場合は、専用例外クラスに切り替えるとより堅牢になります。

3-2. Active Record fixtures 利用側への影響

ActiveRecord::Fixture::FormatError は引き続き同じように発生するため、

  • 「fixture の YAML が壊れている場合に ActiveRecord::Fixture::FormatError が飛ぶ」
  • そのエラーを rescue して独自処理をしている

といったコードは、基本的にはそのまま動作します。

内部的に RuntimeError を rescue していた部分が、より限定的な ActiveSupport::ConfigurationFile::FormatError の rescue に変わっただけなので、通常のアプリケーションコードへの影響はかなり限定的です。

3-3. エラー処理の改善ポイント

今回の変更により、以下のようなコードパターンが取りやすくなります:

ruby
begin
  config = ActiveSupport::ConfigurationFile.new("config/my_config.yml").read
rescue ActiveSupport::ConfigurationFile::FormatError => e
  Rails.logger.error "設定ファイルが不正です: #{e.message}"
  # 必要に応じてフェイルファスト / デフォルト値にフォールバック 等
end

以前は単なる RuntimeError だったため、他の runtime エラーと区別しにくかった部分が、専用例外により明確になります。


  1. 参考情報 (あれば)
  • この例外クラスは、ActiveSupport::ConfigurationFile が最初に導入されたコミット(ef7599fe91)で定義されていましたが、これまで実際には使われていませんでした。
  • 関連 Issue / PR:
    • #38067: ActiveSupport::ConfigurationFile 導入時の議論。ここで「フォーマットエラー用の専用例外を切り出す」こと自体はレビューで明示的に承認されており、本 PR はその意図に沿って挙動を是正したものです。
  • 代替案としては「未使用の FormatError を削除して、従来通り RuntimeError のままにする」選択肢もあったが、意図された設計に合わせて専用例外を活かす方針が採られています。

#58215 Remove unused ConnectionPool#remove_from_maintenance method

マージ日: 2026/7/23 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    ActiveRecord の ConnectionPool に存在していた未使用のプライベートメソッド remove_from_maintenance が削除され、実際に使われているメンテナンス経路(checkout_for_maintenancereturn_from_maintenance)に合わせてドキュメントが更新されました。内部メンテナンス用 API の整理であり、通常のアプリケーションコードへの影響はほぼありません。

  1. 変更内容の詳細

背景

  • 以前のコミット 5eab03f7b0 で、コネクションプールの「メンテナンス用の貸し出し」API として以下の3つが導入されていました:
    • checkout_for_maintenance
    • return_from_maintenance
    • remove_from_maintenance
  • しかし、実際のメンテナンスフロー(sequential_maintenance など)では、借りた接続は常に return_from_maintenance 経由で返却されており、remove_from_maintenance は一度も呼ばれていませんでした。

今回の変更

主な変更点は1ファイルのみです:

activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb

  • 削除:
    • プライベートメソッド remove_from_maintenance
      • 役割としては「メンテナンス中に借りたコネクションをプールから完全に捨てる」ための片割れメソッドだったと考えられますが、実際にはどこからも呼ばれていませんでした。
  • 変更:
    • checkout_for_maintenance のドキュメントコメントを修正
      • remove_from_maintenance と組み合わせて使う」ようなペア関係の説明から、
      • 実際に Rails が使用している「checkout_for_maintenancereturn_from_maintenance」のフローを説明する内容に更新。

※ 実際のコード上は、メソッド定義ごと remove_from_maintenance が削除され、checkout_for_maintenance のコメント中での記述が現実の振る舞いに合わせて調整された、という規模の変更です(+2 / -11 行)。


  1. 影響範囲・注意点
  • 公開 API への影響:

    • ConnectionPool のメンテナンス関連メソッドは元々「内部/プライベート」扱いであり、通常のアプリケーションコードから直接使うことは想定されていません。
    • そのため、通常の Rails アプリケーションには影響はありません。
  • 内部 API・メンテナンスフローへの影響:

    • Rails 本体のコードでも remove_from_maintenance は一度も呼ばれていなかったため、挙動の変更は実質ゼロです。
    • 今後もメンテナンスフローは:
      1. checkout_for_maintenance で接続を借りる
      2. 作業完了後に return_from_maintenance で接続を返却する
        というパスのみが存在する、ということがコードとドキュメントの両方で明確になりました。
  • 独自に内部 API を使っている場合の注意点:

    • gem やアプリ内で ActiveRecord::ConnectionAdapters::ConnectionPool#remove_from_maintenance を「内部事情を承知の上で」呼んでいた場合は、NoMethodError が発生します。
    • そのようなコードがある場合は、そもそもその経路が Rails 本体では想定されていなかったため、設計の見直しが推奨されます。
    • コネクションを「捨てたい」ケースがあるなら、通常は接続の disconnect! / close、またはプール側の disconnect! など、サポートされている経路を検討すべきです。

  1. 参考情報 (あれば)
  • 元コミット: 5eab03f7b0e8a12871bbe3929a5297491915f586
    • checkout_for_maintenance / return_from_maintenance / remove_from_maintenance が一緒に導入されたコミット。
  • 本 PR の意図:
    • 「Rails が実際には使っていない内部 API を残してドキュメントだけが示唆している状態」を解消し、実利用されているパスだけを残すことで、コードベースの一貫性と保守性を高めるものです。
  • 今後もし「借りたメンテナンス用接続を破棄するフロー」が必要になった場合は、この PR 以前の Git 履歴から remove_from_maintenance の実装を復元し、その時点で初めて呼び出し元とテストをきちんと追加する、という方針が示されています。

#58216 Remove unused test runner path helper

マージ日: 2026/7/23 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    Rails のテスト周りのコードから、既にどこからも呼ばれていないテストランナー用 helper メソッドと、そのための定数が削除されました。正規表現は必要な箇所(TestCommand)にインライン化され、不要なコードが整理されています。

  1. 変更内容の詳細

背景

  • 過去のコミット b1589fa7d9 で、extract_filters メソッド内から path_argument? というガードが取り除かれた結果、そのプライベートメソッド path_argument? はどこからも呼ばれない「死んだコード」になっていました。
  • PATH_ARGUMENT_PATTERN 定数も、その path_argument? のために存在していたものの、唯一の利用箇所は TestCommand::EXACT_TEST_ARGUMENT_PATTERN の構築時だけになっていました。

実際の変更点

1) テストコマンド側 (railties/lib/rails/commands/test/test_command.rb)

PATH_ARGUMENT_PATTERN を経由せず、EXACT_TEST_ARGUMENT_PATTERN 側に正規表現をインライン化しました。概念的には:

ruby
# 変更前(イメージ)
PATH_ARGUMENT_PATTERN = /.../

EXACT_TEST_ARGUMENT_PATTERN = /\A#{PATH_ARGUMENT_PATTERN}\z/

# 変更後(イメージ)
EXACT_TEST_ARGUMENT_PATTERN = /\A...同じ中身の正規表現...\z/

これにより PATH_ARGUMENT_PATTERN という中間の定数が不要になりました。

2) テストランナー側 (railties/lib/rails/test_unit/runner.rb)

未使用になっていた helper と定数が削除されています。イメージとしては:

ruby
# 削除されたもの(例)
PATH_ARGUMENT_PATTERN = /.../

private
  def path_argument?(arg)
    PATH_ARGUMENT_PATTERN.match?(arg)
  end

extract_filters などの既存のメソッドからは既に path_argument? が呼ばれていないため、この削除は挙動に影響を与えません。


  1. 影響範囲・注意点
  • 外部 API / 公開インターフェースへの影響はほぼなし
    削除されたのは runner.rb 内の private メソッドと内部用定数であり、通常の Rails アプリケーションや標準的なテスト実行フローには影響しません。
  • Monkey patch や内部 API 依存がある場合は注意
    もしアプリや gem が Rails::TestUnit::Runner
    • PATH_ARGUMENT_PATTERN 定数
    • path_argument? メソッド(private を send で叩く等)
      に依存していた場合、そのコードは壊れます。
      そのような用途があるなら、呼び出し側で独自に正規表現を定義する形に変更する必要があります。
  • 挙動の変更は意図されていない
    正規表現自体は EXACT_TEST_ARGUMENT_PATTERN にインライン化されただけなので、Rails がコマンドライン引数からテストファイル・テスト名を判定するロジック自体は変わっていません。

  1. 参考情報 (あれば)

内部実装のクリーンアップであり、Rails のテストランナーを内側まで拡張・カスタマイズしていない限り、アップデート時に特別な対応は不要と考えてよい変更です。


#58170 Fix stale route recognition after clearing a route set

マージ日: 2026/7/23 | 作成者: @55728

  1. 概要 (1-2文で)
    RouteSet を描き直してルートが 0 件になった場合でも、以前のルーティング情報でパス認識が行われてしまう不具合を修正する PR です。routes.clear 実行時にルート認識用のキャッシュも無効化することで、再描画後は常に最新の(空の)ルート定義に基づいて認識されるようになります。

  1. 変更内容の詳細

不具合の内容

ActionDispatch::Journey::Routes は内部的に以下のような「ルート認識用のメモ化キャッシュ」を持っています。

  • ast(ルート定義をまとめた抽象構文木)
  • simulatorast を元に実行時にパスをマッチさせるオブジェクト)

通常はルート追加 (add_route) 時に clear_cache! が呼ばれ、ast / simulator が再構築されます。しかし Routes#clear はルートテーブル自体は空にするものの、これらのキャッシュを無効化していませんでした。

そのため、以下のケースで「存在しないはずのルートがマッチしてしまう」状態が発生していました。

ruby
routes.draw { get "/foo", to: "foo#index" }
routes.recognize_path("/foo") # ここで simulator がメモ化済みになる

routes.draw { }               # ここでルートは全削除されるが、キャッシュは生きたまま

routes.routes.size            # => 0 (確かにルートは空)
routes.recognize_path("/foo") # => { controller: "foo", action: "index" } ← 本来は 404 相当になるべき

RouteSet#draw は内部で clear! を呼んでからブロック内でルートを追加しますが、

  • 以前に recognize_path 等で simulator を構築しており
  • 描き直し後のブロックで 1 本もルートを追加しない

という条件が揃ったときだけバグが表面化するため、テストスイートの実行順や seed に依存して再現したり消えたりする、やや厄介な挙動になっていました。

修正内容

修正は非常に小さく、実質 1 行です。

  • actionpack/lib/action_dispatch/journey/routes.rb
    • Journey::Routes#clear の中でルートテーブルを空にした後、clear_cache! を呼ぶように変更

擬似コード的には次のようなイメージです。

ruby
def clear
  @routes.clear
  clear_cache!  # ← 今回追加された呼び出し
end

これにより、

  • 次回 ast / simulator へのアクセス時に、
    • ルートテーブル(この時点では空)から再生成される
  • したがって、空の RouteSet に対する recognize_path では
    • 以前のルートは一切参照されず、通常通り ActionController::RoutingError が発生する

ようになります。

テスト追加内容

挙動を保証するためのテストが 2 箇所に追加されています。

  1. actionpack/test/journey/routes_test.rb

    • clear 実行後に:
      • AST が「空として」再構築されること
      • simulator が以前のインスタンスと異なる(新たに再構築される)こと
    • を確認。
  2. actionpack/test/dispatch/routing/route_set_test.rb

    • 実際の RouteSet を用いた End-to-End テストとして、
      1. /foo を認識できるルートを定義
      2. recognize_path("/foo") を 1 度呼んでキャッシュをウォームアップ
      3. routes.draw { }(空定義で再描画)
      4. 再度 recognize_path("/foo") を呼ぶと ActionController::RoutingError が発生すること
    • を期待するテストを追加。

どちらのテストも「この 1 行の修正がないと失敗し、修正があると成功する」ことが確認されています。

CHANGELOG

  • actionpack/CHANGELOG.md に本修正がバグフィックスとして追記されています。

  1. 影響範囲・注意点
  • 対象となるのは、以下のようなコードパスです。

    • ActionDispatch::Routing::RouteSet#draw を複数回呼び出す(テストでよくある)
    • ある draw の前に recognize_path などルーティング認識を行っている
    • その後の draw ブロック内で 1 本もルートを定義しない(条件分岐により全スキップなど)
  • この PR により、以前は運良く(?) マッチしていたルートが、正しく 404 になるようになります。
    つまり、既存コードがこの不具合に依存していた場合(本来は存在しないルートを、古い定義でマッチさせていた場合)、挙動が変わりますが、それは正しい方向への破壊的修正です。

  • 空の RouteSet から AST / simulator を構築するコードパス自体は既に日常的に使われており(新規作成直後の RouteSet など)、今回の変更で新しい状態が導入されるわけではありません。そのため、パフォーマンス・メモリなどの副作用も極めて限定的です。

  • 特に、テストで毎回アプリケーションのルーティングを再描画しているプロジェクト(Engine やマウント状況をテストごとに変える場合など)では、seed に依存した route spec の不安定な失敗が解消される可能性があります。


  1. 参考情報 (あれば)
  • 実際にこのバグにより seed 依存のテスト失敗が発生していたプロジェクトとして、doorkeeper-openid_connect のテストスイートが挙げられています。
  • Rails のルーティング内部実装 (ActionDispatch::Journey) は、ルートテーブル → AST → simulator という 3 段階で最適化されており、今回の修正はその「ルートテーブルとキャッシュの一貫性」を正すものです。

#58208 Remove unused WhereClause#referenced_columns

マージ日: 2026/7/23 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveRecord::Relation::WhereClause に定義されていた未使用メソッド referenced_columns が削除されました。過去の非推奨機能削除により呼び出し元がなくなっていたコードを整理した、クリーンアップ目的の変更です。

  1. 変更内容の詳細
  • 対象ファイル: activerecord/lib/active_record/relation/where_clause.rb
  • 変更: 6行削除、追加なし
  • 削除されたもの: WhereClause#referenced_columns メソッド本体とその関連コード

PR説明によると:

  • 以前のコミット c313d9161360 によって、
    • predicates_unreferenced_by というメソッドが削除された
    • 同時に、ActiveRecord::Relation#mergerewhere を渡すという非推奨サポートも削除された
  • WhereClause#referenced_columns は、その削除された predicates_unreferenced_by からしか呼ばれていなかったため、それ以降は完全に未使用状態だった
  • 今回のPRでは、その未使用メソッドをコードベースから取り除いています

概念的には以下のようなメソッドが消えたイメージです(あくまでイメージの擬似コード):

ruby
# こういったメソッドが存在していたが、どこからも呼ばれていない状態だった
class ActiveRecord::Relation::WhereClause
  def referenced_columns
    predicates.flat_map { |predicate| predicate.referenced_columns }.uniq
  end
end

内部 API 用の補助メソッドであり、公開インターフェースとしてドキュメント化されていたものではありません。


  1. 影響範囲・注意点
  • 通常のアプリケーションコードへの影響

    • Rails の公式な公開 API ではない内部メソッドの削除のため、通常は影響はありません。
    • 既に rewhereRelation#merge に渡すことは非推奨サポートごと削除済みであり、その時点で互換性対応は済んでいる想定です。
  • 影響が出る可能性があるケース

    • gem やアプリケーションが Rails の内部実装に依存しており、
      • ActiveRecord::Relation::WhereClause#referenced_columns
      • WhereClause の内部構造 に直接アクセスしている場合はエラーになります。
    • ただし、predicates_unreferenced_by 削除時点ですでに壊れている可能性が高いため、今回の変更で新たに壊れるケースは限定的です。
  • アップグレード時の注意

    • Rails のメジャー/マイナーアップグレード時に、独自パッチやメタプログラミングで WhereClause を触っているコードがないか確認しておくと安全です。
    • 特に where 条件のパースや最適化を独自に行っている gem を利用している場合は、その gem 側の対応状況をチェックすると良いです。

  1. 参考情報 (あれば)
  • このPRで言及されている過去の変更:
    • コミット c313d9161360
      • predicates_unreferenced_by の削除
      • ActiveRecord::Relation#mergerewhere を渡す非推奨サポートの削除
  • 関連する概念:
    • WhereClause は ActiveRecord の内部で WHERE 句を抽象化するためのオブジェクトで、predicates(各条件)などを管理するクラス
    • referenced_columns は「この WhereClause で参照しているカラム一覧」を返すための内部ユーティリティだったと考えられる

このPR自体は、機能追加や仕様変更ではなく、内部コードを整理しメンテナンス性を高めるためのクリーンアップ変更に該当します。


#58223 Remove unused code from Railties and Action Cable

マージ日: 2026/7/23 | 作成者: @55728

  1. 概要 (1-2文で)
    Rails本体(Railties と Action Cable)から実際には使われていないコードを削除するクリーンアップPRです。最近の未使用コード削除PR(#58214, #58193)の続きで、Rails アプリジェネレータと Action Cable のイベントループ周りの不要な処理が整理されています。

  1. 変更内容の詳細

2-1. Railties: AppBase#dockerfile_chown_directories の削除

対象ファイル:

  • railties/lib/rails/generators/app_base.rb (+0/-9)

削除されたのは、Rails アプリ生成時のベースクラス AppBase に定義されていた以下のようなメソッド(イメージ)です:

ruby
# 例: こういったメソッドがあったと考えられる
def dockerfile_chown_directories
  # Dockerfile 内で chown するディレクトリ群を返す
end

背景:

  • 以前は生成される Dockerfile 内で chown -R ... を実行しており、そのためのディレクトリリストをこのメソッドで用意していた。
  • しかしコミット 658c989d8b により、Dockerfile は COPY --chown=... を使うスタイルに変更され、明示的な chown -R 行が削除された。
  • その結果、このメソッドを参照していたテンプレート (.tt) も存在せず、メソッド自体が完全に未使用となっていたため削除。

ポイント:

  • Rails new で生成される Dockerfile は既に COPY --chown ベースになっており、このPRはその移行完了に伴う後始末です。
  • .tt テンプレートやその他ファイルを含めて grep 済みで、どこからも使われていないことが確認されています。

2-2. Action Cable: StreamEventLoop#detachstream 引数削除

対象ファイル:

  • actioncable/lib/action_cable/server/stream_event_loop.rb (+1/-1)
  • actioncable/lib/action_cable/server/socket/stream.rb (+1/-1)

変更内容

StreamEventLoop#detach のメソッドシグネチャから、使われていない stream 引数が削除されています。

変更前(イメージ):

ruby
# action_cable/server/stream_event_loop.rb
def detach(io, stream)
  # 実際の処理は io のみを使っていた
  @selector.deregister(io)
end

# action_cable/server/socket/stream.rb
event_loop.detach(@io, self)

変更後(イメージ):

ruby
# action_cable/server/stream_event_loop.rb
def detach(io)
  @selector.deregister(io)
end

# action_cable/server/socket/stream.rb
event_loop.detach(@io)

背景:

  • イベントループはコミット 322dca293b で導入されたが、そのときから detach 内では io だけが使用され、stream 引数は保持も参照もされていなかった。
  • attach 側は stream をどこかに記録する設計だが、detach の「登録解除」は io キーでのみ行っているため stream は不要。
  • 呼び出し元は StreamEventLoop#detach の単一呼び出し箇所のみだったので、そこも合わせてシグネチャ変更に追随。

ポイント:

  • API 形だけ残っていた不要引数を削除し、実装と利用状況に整合するシンプルなインターフェースに整理した変更です。
  • deregistration のキーは io のみ、という実装方針がこの変更によりより明確になります。

  1. 影響範囲・注意点

3-1. Railties (AppBase#dockerfile_chown_directories)

  • Rails 本体内部でしか使われておらず、.tt テンプレートも含めて未使用であることが確認されているため、標準的な rails new ユーザーには影響ありません。
  • 影響がありうるのは次のようなケースです:
    • Rails::Generators::AppBase を継承して独自のアプリケーションジェネレータを作り、その中で dockerfile_chown_directories をオーバーライド or 呼び出している場合。
    • Rails 本体のジェネレータテンプレートをカスタムフォークしていて、このメソッドに依存している場合。

そのような場合:

  • 生成される Dockerfile が既に COPY --chown ベースであるなら、このメソッドは単純に不要なので呼び出しやオーバーライドを削除できます。
  • もし引き続き chown -R を使う古いスタイルの Dockerfile を維持したいなら、アプリ側に同等のメソッドを定義する/独自ジェネレータにロジックをコピーするといった対応が必要になります。

3-2. Action Cable (StreamEventLoop#detach のシグネチャ変更)

  • Rails 内部コードと、Action Cable のイベントループ実装を直接触っていない通常のアプリケーションには影響ありません。
  • 影響がありうるのは次のような場合です:
    • ActionCable::Server::StreamEventLoop#detach を直接呼び出している独自コードがある。
    • StreamEventLoop を独自にサブクラス化して detach(io, stream) シグネチャ前提でオーバーライドしている。

その場合の対応:

  • 呼び出し側: detach(io, stream)detach(io) に引数を1つ減らしてください。
  • オーバーライド側: メソッド定義を def detach(io) に変更し、stream に依存する処理がある場合は、別の手段(attach 時に自前でマッピングを保持するなど)に移行する必要があります。

  1. 参考情報 (あれば)
  • このPRは未使用コード削除のシリーズの一部で、関連PR:
    • #58214
    • #58193
  • コミット履歴:
    • Dockerfile の COPY --chown への移行: 658c989d8b
    • StreamEventLoop 導入: 322dca293b
  • 変更統計:
    • 変更ファイル数: 3
    • 追加行数: 2
    • 削除行数: 11
      → 小規模なリファクタリングであり、機能追加・挙動変更ではなく、コードベースの整備と明確化が主目的です。

#58218 Remove unused Active Support internals

マージ日: 2026/7/23 | 作成者: @55728

  1. 概要 (1–2文で)
    Active Support 内部で使われていなかったパラメータ・インスタンス変数・古いシリアライザ用メソッドなどの「死んだコード」をまとめて削除・整理した PR です。外部 API の挙動変更はほぼなく、内部実装をスリムにするリファクタリングです。

  1. 変更内容の詳細

※すべて「使われていないことを grep などで確認したうえで削除」という方針の follow-up です。

2-1. ActiveSupport::Notifications::Fanout::Handle

ruby
# 変更前 (イメージ)
def initialize(group, notifier)
  @group = group
  @notifier = notifier # 実際にはどこからも読まれていない
end

# 変更後
def initialize(group)
  @group = group
end
  • Fanout::Handle#initializenotifier 引数が削除されました。
  • すでに build_handle 側でグループ構築を完結させるように変わっており、notifier はどこからも参照されていませんでした。
  • コンストラクタは :nodoc: 扱いで、利用者は通常 build_handle 経由でしか Handle を取得しないため、外部コードへの影響は事実上ありません。

2-2. ContinuousIntegration::Group#execute_group

ruby
# 変更前 (イメージ)
def execute_group(name, group)
  # name は使っていない
  run(group)
end

# 変更後
def execute_group(group)
  run(group)
end
  • ContinuousIntegration::Group#execute_groupname パラメータが削除されました。
  • メソッド導入時から未使用で、メソッド内部でも参照されていませんでした。
  • 呼び出し側も同時に修正されているため、Active Support 外のコードでこのメソッドを直接呼び出していなければ影響はありません。

2-3. Testing::Parallelization::Server#stop_worker の引数整理

ruby
# 変更前 (イメージ)
def stop_worker(worker_id, worker_pid)
  # worker_pid はここでは使っていない
  # worker_id ベースで内部マップをクリーンアップ
end

# 変更後
def stop_worker(worker_id)
  # worker_id ベースでクリーンアップ
end
  • Server#stop_workerworker_pid 引数が削除されました。
  • 実装では start_worker 時に PID が内部マップに保存され、それを worker_id キーで参照して処理しており、stop_worker の引数として渡された PID は一切使われていませんでした。
  • 対応する worker.rb とテスト (server_test.rb) も引数数にあわせて修正されており、内部 API の一貫性が保たれています。

2-4. DeprecatedConstantProxy の書き込み専用インスタンス変数削除

ruby
# 変更前 (イメージ)
class DeprecatedConstantProxy
  def initialize(old_const, new_const, message: default_message)
    @old_const = old_const # 読まれていない
    @new_const = new_const
    @message   = message
  end
end

# 変更後
class DeprecatedConstantProxy
  def initialize(old_const, new_const, message: default_message)
    @new_const = new_const
    @message   = message
  end
end
  • @old_const がどこからも参照されていない「書き込み専用」になっていたため削除されました。
  • 以前は警告文生成に使っていたものの、後続のリファクタで message: キーワード引数のデフォルトに折り込まれており、保持だけされていた状態です。
  • Deprecation のメッセージ内容や挙動自体には影響しません。

2-5. ActiveSupport::Subscriber.attach_to の @inherit_all

ruby
# 変更前 (イメージ)
def self.attach_to(namespace, subscriber, inherit_all = false)
  @inherit_all = inherit_all  # インスタンス変数化されるがどこからも読まれない
  inherit_all_local = inherit_all
  # 以下はローカル変数 inherit_all_local を使用
end

# 変更後
def self.attach_to(namespace, subscriber, inherit_all = false)
  inherit_all_local = inherit_all
  # 処理はそのまま
end
  • クラスインスタンス変数 @inherit_all に値を書き込んでいたものの、どこからも読み出されていなかったため、その書き込みが削除されました。
  • メソッド内部ではローカル変数だけが利用されており、attach_to の挙動に変更はありません。

2-6. OrderedHash#to_yaml_type の削除

ruby
# 変更前
def to_yaml_type
  "!ruby/ActiveSupport::OrderedHash"
end

# 変更後: メソッド削除
  • OrderedHash#to_yaml_type は Syck (古い YAML ライブラリ) 用のシリアライゼーションフックです。
  • Ruby 2.0 以降は Syck が削除され、標準は Psych になっており、Psych はすでに同ファイル内にある encode_with を利用します。
  • そのため、このメソッドは現在の Ruby / Active Support では実行経路に入らず、削除しても実質的な影響はありません。

2-7. ActiveSupport::Callbacks::Callback のアクセサ整理

ruby
# 変更前 (イメージ)
class Callback
  attr_accessor :kind, :name
end

# 変更後
class Callback
  attr_reader :kind, :name
end
  • Callback#kind= / #name= がどこからも呼ばれておらず、「読み取り専用プロパティ」としてしか使われていなかったため、attr_accessorattr_reader に変更されました。
  • これにより kind= / name= メソッドは削除され、コールバックオブジェクトは生成後イミュータブルに近い形になります。
  • PR 説明にもある通り、#58198 と同じパターンの整理です。

  1. 影響範囲・注意点
  • 外部 API への影響はほぼなし

    • 削除された/変更されたものは基本的に「内部実装」か、公式には利用を想定していない経路(:nodoc: 付きなど)です。
    • ただし次のようなケースでは影響があります。
  • 独自に内部 API を叩いている場合の互換性注意

    • ActiveSupport::Notifications::Fanout::Handle.new(group, notifier)
      → 2 引数で呼び出していると ArgumentError になります。build_handle を使うべきです。
    • ContinuousIntegration::Group#execute_group(name, group) を 2 引数で呼んでいる独自コード
      → 1 引数に変更が必要です(name はそもそも使われていなかったので、その情報はもともと捨てられていました)。
    • Testing::Parallelization::Server#stop_worker(worker_id, worker_pid) を自前で呼んでいる場合
      → 2 番目の引数を削除し、stop_worker(worker_id) に合わせる必要があります。
    • ActiveSupport::Callbacks::Callback オブジェクトに対して callback.kind = :before のように後から書き換えている場合
      NoMethodError になります。公式の使い方ではなく、副作用も未定義だったので、コールバックの再構築など別の手段に切り替える必要があります。
  • YAML シリアライズへの影響

    • OrderedHash#to_yaml_type 削除は、現在サポートされている Ruby (Psych) では使われないフックなので、通常の環境では影響しません。
    • もし独自に Syck を復活させて使っているような非常に特殊な環境があれば、シリアライズ形式が変わる可能性がありますが、現代的な Rails/AS との組み合わせ自体が非サポートに近い状況です。

  1. 参考情報 (あれば)
  • この PR は以下の未使用コードクリーンアップの続きです:
    • #58214
    • #58193
    • 類似パターンのアクセサ整理: #58198
  • Ruby の YAML 実装変更:
    • Ruby 1.9 末期〜2.0 で Syck → Psych に移行し、Syck は削除済み
      → Active Support の encode_with など Psych ベースの API だけを考慮すればよい状態になっています。

#58219 Remove unused Action Pack internals

マージ日: 2026/7/23 | 作成者: @55728

  1. 概要 (1-2文で)
    Action Pack周辺の「参照されていない・実質デッドコード」だけを整理して削除したPRです。公開APIの仕様変更はほぼなく、内部実装やテストコードから使われていないメソッド・引数・インスタンス変数を取り除いています。

  1. 変更内容の詳細

それぞれ「いつから使われていなかったか」「どこからも呼ばれていないか」がコミット単位で確認されており、repo全体を grep(send/シンボル参照含む)してデッドコードであることを検証したうえで削除されています。

2-1. ActionDispatch::Routing::RouteSet#generatemethod_name 引数削除

  • 対象: action_dispatch/routing/route_set.rb
  • 変更点:
    • RouteSet#generate の第3引数 method_name が削除されました。
    • すでに全ての呼び出し元が3引数を渡しておらず、method_name もメソッド内部で参照されていなかったための整理です。
    • 背景として、UrlGenerationError の生成が generate の外に移されたコミット(437ab2031e)以降不要になっていました。

※現行の呼び出し元はすでに新しいシグネチャに揃っているため、Rails 本体内部では挙動変化なし。ただし、内部APIに直接依存して RouteSet#generate を3引数で呼んでいるアプリ/ライブラリがあれば壊れる可能性があります

2-2. ActionDispatch::DebugLocks#render_detailsreq 引数削除

  • 対象: action_dispatch/middleware/debug_locks.rb
  • 変更点:
    • render_details メソッドのシグネチャが def render_details(req)def render_details のように、未使用だった req 引数が削除されました。
    • メソッド本体では req は一切参照されていなかったため、純粋なクリーンアップです。
  • デバッグミドルウェア内部で完結しており、外部から render_details を直接呼ぶのは通常想定されていないため、一般的なアプリへの影響はありません。

2-3. Journey::Route#required_keys の削除

  • 対象: action_dispatch/journey/route.rb
  • 変更点:
    • Journey::Route#required_keys メソッドが削除されました。
  • 理由:
    • Journey が Action Pack に統合されて以降、このメソッドには呼び出し元がなくなっていました。
    • 現在はルートフォーマッタは required_partsrequired_defaults を直接利用しており、required_keys はその古いラッパーのような存在で、もはや使われていません。

Journey::Route を直接触る高度なメタプログラミングや独自ルーティング拡張で required_keys を叩いていた場合は NoMethodError になる可能性があります。

2-4. ResponseAssertions#parameterize の削除

  • 対象: action_dispatch/testing/assertions/response.rb
  • 変更点:
    • ResponseAssertions#parameterize メソッド(テスト用アサーションヘルパ内のユーティリティ)が削除されました。
  • 理由:
    • 最後の呼び出し元はコミット c3aaba0180 で削除済みであり、以降はどこからも使われていませんでした。

ActionDispatch::IntegrationTest や独自のテスト基盤の中からこのメソッドを直接利用していた場合は壊れる可能性がありますが、Railsが提供する公式テストAPIとしてはドキュメント化されていない内部ユーティリティに近い位置付けです。

2-5. ActionDispatch::TestSession@initially_empty 削除

  • 対象: action_controller/test_case.rb(経由で TestSession
  • 変更点:
    • ActionDispatch::TestSession のインスタンス変数 @initially_empty(書き込みのみで読み出しゼロ)が削除されました。
  • 理由:
    • 追加されたコミット f2c66ce392 以降、一度も読み取られていなかった「書き込み専用」のデッドコード。
    • default_session のような Rack 側のフックでもないことを rack / rack-session 側まで確認済み。

セッション状態の判定に @initially_empty を monkey patch 等で使っている場合は影響しますが、通常の Rails アプリ・テストでは無関係です。

2-6. ParamsWrapper._set_wrapper_options の削除

  • 対象: action_controller/metal/params_wrapper.rb, params_wrapper_test.rb
  • 変更点:
    • クラスメソッド ParamsWrapper._set_wrapper_options が削除されました。
    • テストでは、これまで _set_wrapper_options を経由していた箇所を _wrapper_options クラス属性へ直接代入する形に変更。
  • 理由:
    • 本番コードでは wrap_parameters_wrapper_options を直接設定しており、_set_wrapper_options は実質不要。
    • メソッド自身も、その導入コミット 95ec448580 以降、テストの中でしか呼ばれていない状態でした。

ActionController::ParamsWrapper を直接叩いてラッパー設定をカスタマイズしている場合に _set_wrapper_options を呼んでいたならば破壊的です。ただしこれも内部ヘルパ的な位置付けで、一般的な利用では wrap_parameters 経由が想定されています。

2-7. Journey::Path::Pattern::MatchData#named_captures の削除

  • 対象: action_dispatch/journey/path/pattern.rb, pattern_test.rb
  • 変更点:
    • Journey::Path::Pattern::MatchData#named_captures メソッドが削除され、それ専用のテストも削除されました。
  • 理由:
    • 自身のユニットテスト以外に呼び出し元が存在せず、機能としても外部から使われていませんでした。

Journey の内部表現を直接扱う高度なライブラリなどで、この MatchData を利用していた場合のみ影響します。


  1. 影響範囲・注意点

  2. Rails本体の挙動は変わらない

    • いずれの変更も「参照されていないコードの削除」に留まっており、通常のコントローラ・ルーティング・レスポンス・テストの振る舞いには変化がありません。
  3. ただし内部APIへの依存があると壊れる可能性
    以下のようなケースでのみ注意が必要です:

    • ActionDispatch::Routing::RouteSet#generate3引数(method_name 含む)で直接呼んでいる
    • ActionDispatch::DebugLocks#render_details(req)外から直接呼んでいる
    • Journey::Route#required_keys を使った独自ルーティング拡張をしている。
    • ActionDispatch::Testing::ResponseAssertions#parameterize をテストヘルパなどから直接使っている。
    • ActionDispatch::TestSession@initially_empty を読み取る monkey patch を入れている。
    • ActionController::ParamsWrapper._set_wrapper_options を直接叩いている。
    • Journey::Path::Pattern::MatchData#named_captures に依存している。

    これらはどれも「公開・安定API」というよりは 内部実装 or テスト用ユーティリティ に近いもので、通常のアプリケーションや一般的な gem のコードでは使われていない想定です。

  4. アップグレード時のチェックポイント

    • 自分のアプリ / ライブラリコードを static grep して、上記削除メンバ名が出てくるかを確認しておくと安全です。
    • 出てきた場合は:
      • RouteSet#generate → 公開されているルーティングヘルパ(url_for, *_url, *_path)を使う。
      • ParamsWrapperJourney 周り → より上位の公開APIに寄せる、もしくは内部実装の変更を許容した上でコードを修正する。

  1. 参考情報 (あれば)
  • このPRは以下の「未使用コードクリーンアップ」PRの継続です:
    • #58214
    • #58195
    • #58196
  • いずれも Action Pack / Journey / テスト関連の「参照されていない内部実装」を削除して、コードベースのメンテナンス性・可読性を高めることを目的としています。

#58220 Remove unused Active Model internals

マージ日: 2026/7/23 | 作成者: @55728

  1. 概要 (1–2文で)
    Active Model / Active Record の内部で使われていない引数・メソッドを削除し、実装を整理する PR です。外部 API の挙動変更はなく、内部実装のクリーンアップが目的です。

  1. 変更内容の詳細

PR 全体として、「リポジトリ全体を grep した結果、一切参照されていないコード」を削除する、というポリシーで行われています。メタプロや send、シンボル参照も含めて探索済みで、「動的に呼ばれている可能性」も検証したうえで dead code と判断されています。

(1) AttrNames.define_attribute_accessor_methodowner 引数削除

対象:
activemodel/lib/active_model/attribute_methods.rb
activemodel/lib/active_model/attributes.rb
activerecord/lib/active_record/attribute_methods/read.rb
activerecord/lib/active_record/attribute_methods/write.rb

背景:
AttrNames.define_attribute_accessor_method は、属性に対応する accessor メソッドを定義するための内部ヘルパですが、昔は内部で owner.rename_method を呼んでいました。その処理が ca5542fed3 で削除されたため、owner 引数自体が使われなくなっていました。

今回の変更では、

  • メソッド定義側から owner 引数を削除
  • 呼び出し側(Active Model Attributes、Active Record Read / Write)も、渡していた owner を削除

といった形で シグネチャと呼び出しの両方 が揃えて変更されています。

簡略イメージ:

ruby
# 変更前(イメージ)
AttrNames.define_attribute_accessor_method(:attr_name, owner) do |attr|
  owner.define_method(attr) { ... }
end

# 変更後(イメージ)
AttrNames.define_attribute_accessor_method(:attr_name) do |attr|
  owner.define_method(attr) { ... } # owner は各呼び出し元のスコープで完結
end

もともと、各呼び出し元は block の中で自分で owner に対して define_method しており、define_attribute_accessor_method の引数として渡している owner は一切使われていなかったため、削除しても振る舞いは変わりません。

(2) define_callname 引数削除

対象:
activemodel/lib/active_model/attribute_methods.rb

背景:
動的にメソッドを定義し、キャッシュする仕組みにおいて、define_call に渡されていた name 引数が、define_cached_method の実装変更(514d474836)により使われなくなっていました。今は mangled_name(内部用に加工された名前)をキーにキャッシュしており、元の name は用いられていません。

変更内容:

  • define_call メソッドのシグネチャから name を削除
  • それに応じて、呼び出し側も name を渡さない形に修正

キャッシュのキーは既に mangled_name 基準に統一されていたため、内部実装の一貫性が増しつつ、不要な引数がなくなっただけで挙動は同じです。

(3) validation_context= (プライベート writer) の削除

対象:
activemodel/lib/active_model/validations.rb

背景:
valid? 呼び出し時の検証コンテキスト(on: :create など)を扱う内部 API として、validation_context= というプライベート writer が存在していました。しかし afecc8fd6c によって、valid?context_for_validation.context = ... のように、別のオブジェクト・インターフェースへ直接アクセスする形に移行しており、この writer はどこからも呼ばれなくなっていました。

今回:

  • validation_context= の定義(プライベートメソッド)を削除

既にプライベートで、かつ完全に未使用なメソッドだったため、外側から依存しているコードは存在しない前提です。

(4) ConfirmationValidator#confirmation_value_equal? の未使用引数削除

対象:
activemodel/lib/active_model/validations/confirmation.rb

背景:
ConfirmationValidator は、validates :password, confirmation: true のようなバリデーションの実装です。その内部で、値の比較ロジックを行う confirmation_value_equal? というメソッドがありましたが、定義時(2438a1cf4e)から今まで、引数 recordattribute が実際には使われていませんでした。

変更内容:

  • メソッドシグネチャから record, attribute を削除し、実際に使用している引数(たとえば value, confirmed など)のみに整理。
  • 呼び出し側も、削除された引数を渡さない形に修正。

このメソッドは完全に内部用であり、引数を減らしても public API には影響しません。


  1. 影響範囲・注意点
  • 影響範囲は Active Model / Active Record の内部実装に限定 されている想定です。
  • 公開されているドキュメント化された API(validates, attributes, Active Record の属性読み書きなど)の使い方や挙動は変わりません。
  • ただし、以下のようなケースでは影響が出る可能性があります:
    • Rails 内部の private / internal API に依存したメタプログラミング を行っている場合
      • 例: ActiveModel::Validations の private メソッド validation_context=send で叩いている、
        AttrNames.define_attribute_accessor_method を直接呼んでいる、等
    • 独自に ActiveModel::Validations::ConfirmationValidator を継承し、
      confirmation_value_equal? をオーバーライドしているが、古いシグネチャに依存している場合
      (ただし通常はオーバーライド対象にもしていない前提のメソッド)

そのような「内部 API に食い込む実装」をしていない一般的な Rails アプリケーションでは、影響は実質ゼロと考えてよいです。

テスト観点としては:

  • 既存のモデルのバリデーションテスト(特に confirmation 系)の再実行
  • 動的定義される属性の getter/setter(Active Record / Active Model Attributes)の動作確認

を行えば十分です。


  1. 参考情報 (あれば)
  • この PR は、直前の unused-code クリーンアップ PR (#58214, #58193) のフォローアップとして位置づけられており、「未使用な内部コードの削除」を段階的に進めている流れの一部です。
  • 各削除対象は「コミット履歴上いつから未使用になったか」が明示されており、それぞれ:
    • owner 引数: ca5542fed3 以降未使用
    • name 引数: 514d474836 以降未使用
    • validation_context=: afecc8fd6c 以降未使用
    • confirmation_value_equal?record/attribute 引数: 2438a1cf4e で導入時から未使用
      となっています。
      このため、「意図せず呼ばれている可能性のあるコード」を削っていないことが履歴上も裏付けられています。

#58221 Remove unused Active Job parameters

マージ日: 2026/7/23 | 作成者: @55728

  1. 概要 (1-2文で)
    Active Job内部で実際には使われていないメソッド引数を削除し、コードをシンプルにしたPRです。外部インターフェース(公開API)には影響せず、内部実装の整理に留まっています。

  1. 変更内容の詳細

2-1. ActiveJob::Continuation#run_step_inline**options 削除

対象ファイル: activejob/lib/active_job/continuation.rb

  • これまでのシグネチャ(イメージ):
ruby
def run_step_inline(name, start:, **options)
  # `options` は一切参照されていなかった
end
  • 変更後:
ruby
def run_step_inline(name, start:)
end

説明:

  • **options キーワード引数は、メソッド追加時(commit: 1b4484c43f)からずっと「受け取るだけで使っていない」状態だったと、リポジトリ全体のgrepで確認されています。
  • このメソッドの呼び出し側は namestart: しか渡しておらず、実際に options が指定されるケースもありませんでした。
  • そのため、インターフェースから未使用のキーワード引数 **options を削除しています。

2-2. Async アダプタの Scheduler における queue_name: の削除

対象ファイル: activejob/lib/active_job/queue_adapters/async_adapter.rb

対象メソッド:

  • Scheduler#enqueue

  • Scheduler#enqueue_at

  • これまでのシグネチャ(イメージ):

ruby
def enqueue(job, queue_name:)
  # queue_name は中で使われていなかった
end

def enqueue_at(job, timestamp, queue_name:)
  # 同上
end
  • 変更後:
ruby
def enqueue(job)
end

def enqueue_at(job, timestamp)
end

説明:

  • 以前はキューごとに AsyncJob を分ける実装だったものが、commit a66780bfff にて「単一のグローバル executor」を使う実装に変更されています。
  • その際、Scheduler 側ではキュー名によるスケジューリングの分岐がなくなっており、queue_name 引数はスケジューリングロジックに一切影響しない「死んだ引数」になっていました。
  • 今回、その未使用引数 queue_name: を削除することで、実際の挙動とインターフェースを一致させています。
  • 一方で、アダプタの外向きAPIである AsyncAdapter#enqueue(job) / #enqueue_at(job, timestamp) などのシグネチャは変わっておらず、利用者の呼び出しコードはそのままで動作します。

  1. 影響範囲・注意点
  • Rails利用者 (アプリケーション側)

    • Public API の変更はなく、通常の ActiveJob の使い方(MyJob.perform_later など)には影響しません。
    • async アダプタを利用していても、標準的な利用であれば影響はありません。
  • Rails内部を拡張している人・メンテナやプラグイン作者向け

    • ActiveJob::Continuation#run_step_inline を直接呼び出していて、かつ **options を渡していた場合は、メソッドシグネチャが変わるためエラーになります。
      • ただしPR説明によれば、公式実装上は options を渡している呼び出しは存在せず、基本的に想定されていない利用です。
    • AsyncAdapter の内部 Scheduler を直接利用し、queue_name: を渡していたコードがあれば、その部分は修正が必要です。
      • 具体的には、enqueue(job, queue_name:)enqueue(job)enqueue_at(job, ts, queue_name:)enqueue_at(job, ts) に変更する必要があります。
    • queue_name を使ってスケジューラを拡張・差し替えしていた場合、そもそも a66780bfff 以降は「単一executor」実装になっているため、想定通り動いていない可能性があります。このPRはその事実をインターフェースにも反映した形です。
  • バージョンアップ時のマイグレーション観点

    • 通常のアプリケーションでのマイグレーション作業は不要と思われます。
    • gem やプラグインが ActiveJob::ContinuationActiveJob::QueueAdapters::AsyncAdapter::Scheduler を直接叩いている場合のみ、互換性確認が必要です。

  1. 参考情報 (あれば)
  • このPRは未使用コード削除のフォローアップであり、直前の類似PR: #58214 を受けて追加で見つかった未使用パラメータを削除するものです。
  • 関連する過去の変更:
    • Continuation#run_step_inline 追加: commit 1b4484c43f
    • Async アダプタが単一グローバル executor を使うようになった変更: commit a66780bfff
      → この時点で queue_name ベースのスケジューリングは実質無効化されていました。

#58222 Remove unused Action View internals

マージ日: 2026/7/23 | 作成者: @55728

  1. 概要 (1-2文で)
    Action View 内部で使われていない引数・ブロック受け取りを整理し、「死んでいるコード(dead code)」を削除した PR です。外部 API の挙動や機能には実質的な変更はなく、内部実装のクリーンアップが目的です。

  1. 変更内容の詳細

全体像

  • Action View 周辺の「もう参照されていない引数・ブロック」を削除
  • すべて「呼び出し元を含めて grep で未使用を確認済み」
  • 実際の処理ロジックは変えず、シグネチャの簡素化と不要コード削除のみ

2-1. ActionView::Template#handle_render_errorview 引数削除

対象ファイル

  • actionview/lib/action_view/template.rb

変更前のイメージ

ruby
def handle_render_error(view, e)
  # 以前は error 時に refresh(view) などがあった
  # その後 b8e6594181 で refresh(view) が削除された
end

# 同一ファイル内での唯一の呼び出し元
def render(view, locals, buffer = nil, &block)
  ...
rescue => e
  handle_render_error(view, e)
end

b8e6594181 でエラー発生時の refresh(view) フォールバックが削られて以降、handle_render_error 内で view は参照されていませんでした。
今回、その未使用引数 view をメソッドシグネチャと呼び出し元の両方から削除しています。

変更後のイメージ

ruby
def handle_render_error(e)
  # view は使われていないので引数から除去
end

def render(view, locals, buffer = nil, &block)
  ...
rescue => e
  handle_render_error(e)
end
  • 影響は ActionView::Template 内部に閉じており、外部から直接 handle_render_error を呼ぶことは通常ありません。

2-2. expanded_cache_keytemplate 引数削除

対象ファイル

  • actionview/lib/action_view/renderer/partial_renderer/collection_caching.rb

背景

  • 以前は cache_fragment_namevirtual_path を渡しており、そのため template 引数(template オブジェクト / virtual path を含む何か)が expanded_cache_key 経由で必要でした。
  • 4671fe2040virtual_path を渡さなくなり、expanded_cache_key 内の template 引数が未使用になった。

変更前のイメージ

ruby
def expanded_cache_key(key, template)
  # key は使うが template はもう使っていない
  # 例: cache_fragment_name(key: key) だけになっている
end

def some_caching_method
  template = ...
  key = expanded_cache_key(record, template)
  digest_path_from_template(template) # <- ここでは template を使う
end

template 自体はまだ呼び出し元側で digest_path_from_template(template) に使っているが、expanded_cache_key の引数として渡す必要はなくなっている状態でした。

変更内容

  • expanded_cache_key メソッドの引数から template を削除
  • 呼び出し元からも template を渡すのをやめる(ただし呼び出し元内では従来どおり digest_path_from_template(template) などに template を使用)

変更後のイメージ

ruby
def expanded_cache_key(key)
  # key だけを使ってキャッシュキーを組み立てる
end

def some_caching_method
  template = ...
  key = expanded_cache_key(record) # template を渡さない
  digest_path_from_template(template) # template 自体はまだここで利用
end
  • つまり「expanded_cache_keytemplate を渡していたが、そこで使われていなかったのでやめた」というだけの変更です。

2-3. CacheExpiry::ViewReloader.create / #initialize のブロック引数削除

対象ファイル

  • actionview/lib/action_view/cache_expiry.rb

背景

  • CacheExpiry::ViewReloader クラスは 72abd6357d で導入されたが、その際から
    • create クラスメソッドでブロックを受け取れるシグネチャになっていた
    • initialize でもブロックを受け取っていた
  • しかし、ブロックを保存 (@block = block) したり、yield したりしたことは一度もなく、呼び出し側もブロックを渡していなかった。

変更前のイメージ

ruby
class CacheExpiry::ViewReloader
  def self.create(...)
    new(...) do
      # 実際にはここも使われていない
    end
  end

  def initialize(..., &block)
    # block をどこにも保存・実行していない
  end
end

変更内容

  • create からブロックパラメータを削除
  • initialize から &block を削除
  • 呼び出し側にもブロックを渡せない(渡す必要もない)形に統一

変更後のイメージ

ruby
class CacheExpiry::ViewReloader
  def self.create(...)
    new(...)
  end

  def initialize(...)
    # ブロック関連の処理は完全に削除
  end
end

  1. 影響範囲・注意点
  • パブリック API への影響はほぼない想定

    • 変更対象は Action View の内部実装 (Template, PartialRenderer の collection caching, CacheExpiry::ViewReloader) に閉じています。
    • 通常のアプリケーションレベルでの rendercache の使い方は変わりません。
  • 直接内部メソッドを呼び出していた場合のみ互換性リスク

    • ActionView::Template#handle_render_error(view, e) を直接呼んでいたようなコードがあると、引数の個数不一致でエラーになります。
    • expanded_cache_key(key, template) を直接呼び出しているカスタムコードがあれば、シグネチャ変更に追従が必要です(第二引数を削る)。
    • CacheExpiry::ViewReloader.create { ... } / new { ... } のようにブロックを渡していた場合も、ブロックは受け取られず、引数エラーになる可能性があります。
      • ただし、これらは本来内部用であり、通常のアプリケーションが触るレイヤーではありません。
  • パフォーマンスやキャッシュキーの挙動は維持

    • expanded_cache_key から template 引数が除かれても、もともとその値は使われていなかったため、キャッシュキーの内容に変化はありません。
    • 同様に、handle_render_errorview を使っていなかったため、エラーハンドリングの実際の挙動も変更なしです。

  1. 参考情報 (あれば)
  • 本 PR は以下の「未使用コード削除」PR のフォローアップ:
    • #58214
    • #58198
  • 各削除対象は、以下のコミットで実質的に不要になっていたもの:
    • Template#handle_render_errorview 引数: b8e6594181
    • expanded_cache_keytemplate 引数: 4671fe2040
    • CacheExpiry::ViewReloader のブロックパラメータ: 72abd6357d
  • Action View の内部 API に依存した拡張や monkey patch を行っている場合は、これらメソッドのシグネチャ変更有無を一度確認しておくと安全です。

#58213 Preserve sub-second precision when subtracting a DateTime from a Time

マージ日: 2026/7/23 | 作成者: @Saidbek

  1. 概要 (1-2文で)
    Time - DateTime の計算で、これまで Float 変換によりサブ秒精度(マイクロ秒)が失われていた問題を、Rational を使った計算に切り替えることで解消した PR です。これにより、両者の差分がマイクロ秒単位まで正確に計算されるようになりました。

  1. 変更内容の詳細

何が問題だったか

元の実装では Time - DateTime(ActiveSupport が Time#- の上にレイヤを載せている部分)が、両方の時刻を to_fFloat に変換してから差分を計算していました。

ruby
# イメージ(実際の実装とは概念的な例)
(time.to_f - datetime.to_f) # Float 同士の引き算

しかし Float は 2 進浮動小数のため、「任意の 1 マイクロ秒単位の値」を厳密に表現できません。
その結果、サブ秒(小数点以下の秒)部分を持つ DateTime との引き算では、約半分くらいのケースで誤差が発生していました。

PR 説明の例:

ruby
t  = Time.utc(2000, 1, 1, 0, 0, 1)
dt = DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 1_000_000), "+0") # 0.000001 秒

t - dt
# 変更前: 0.9999990463256836  (Float、誤差あり)
# 変更後: (999999/1000000)   (Rational、1 秒 - 1 マイクロ秒を厳密表現)

整数秒差(例: Time.utc(2000, 1, 2) - DateTime.civil(2000, 1, 1))については元から誤差がなく、今回も挙動は変わっていません。

どう直したか

修正後は、TimeDateTime をどちらも有理数 (Rational) ベースで扱って差分を計算するように変更されています。

ポイント:

  • Time 側は time.to_r を使用
    • Time#to_r は秒+サブ秒を含む精度の高い有理数を返します。
  • DateTime 側は「整数秒部分 + 小数秒部分」を厳密に足し合わせた値を使う
    • datetime.to_i : 秒単位の整数部分
    • datetime.sec_fraction : その秒の中の小数部分(Rational
    • datetime.to_i + datetime.sec_fraction」が、DateTime の「正確な秒数表現」になります。

結果として、差分は以下のような形になります:

ruby
time.to_r - (datetime.to_i + datetime.sec_fraction)
# => 差分が Rational で返る(マイクロ秒精度を保持)

PR によるサンプルのもう一つのケース:

ruby
t  = Time.utc(2000, 1, 1, 0, 0, 1)
dt = DateTime.civil(2000, 1, 1, 0, 0, Rational(123_457, 1_000_000), "+0")

t - dt
# 変更前: 0.8765430450439453
# 変更後: (876543/1000000)  # 1秒 - 0.123457秒 = 0.876543秒 を厳密に表現

他の API との整合性

この変更により、以下の既存 API 群と計算方法が一貫しました:

  • Time.at(DateTime) (#58085 で Rational ベースに修正済み)
  • ActiveSupport::TimeWithZone - DateTime
    • 内部的に getutc を通じて、既に Rational ベースの正確なタイムスタンプを使用しています。

つまり、Time / TimeWithZone / DateTime 相互の差分計算がすべて「Rational による厳密計算」に揃えられた形になります。

テストとドキュメント

  • activesupport/test/core_ext/time_ext_test.rb にサブ秒差分に対する新しいアサーションが追加され、マイクロ秒精度が保持されていることを保証。
  • activesupport/CHANGELOG.md に今回の変更内容が追記され、挙動変更が明示されています。

  1. 影響範囲・注意点
  • 返り値の型が Float から Rational に変わるケースに注意

    • これまで Time - DateTime の結果を Float 前提で使っていたコード(例: is_a?(Float) チェックや round(5) など)は影響を受けます。

    • 新挙動では Rational が返るため、そのまま演算に使う分には問題ありませんが、Float が必須な場合は明示的に to_f する必要があります。

      ruby
      diff = t - dt       # => Rational
      diff.to_f           # => Float が欲しければ明示的に変換
  • 整数秒差のみを扱っている既存コードは影響を受けにくい

    • 整数秒差は Rational でも 86400 のような整数として表現されるため、ほとんどのケースで以前と同様に動作します(== 86400 のような比較もそのまま通る)。
  • 時刻差をキーにしたハッシュ、シリアライズなどで型の違いによる影響が出る可能性

    • 例: 差分を JSON で送っていたコードが、以前は小数、今は分数文字列になるといった変化を起こしうるため、そのような境界では型・フォーマットの確認が必要です。

  1. 参考情報 (あれば)
  • 対応 PR: #58213 Preserve sub-second precision when subtracting a DateTime from a Time
  • 関連 PR:
    • #58085: Time.at(DateTime) を Rational ベースに修正した PR
  • Ruby 標準:
    • Time#to_r, DateTime#sec_fraction は Ruby 標準ライブラリの API で、どちらもサブ秒部分を Rational で安全に扱うためのメソッドです。

#58177 Preserve the declared parent order when dumping PostgreSQL INHERITS options

マージ日: 2026/7/23 | 作成者: @viralpraxis

  1. 概要 (1-2文で)
    PostgreSQL のテーブル継承 (INHERITS) をスキーマダンプする際、親テーブルの並び順が宣言時の順序どおりになるように修正した PR です。これにより、schema.rb / structure.sql のダンプ結果がより決定的(安定)になり、環境差異などによる不要な差分が減ります。

  1. 変更内容の詳細 (サンプルイメージ含む)

何が問題だったか

PostgreSQL の継承テーブルで、例えば次のようなテーブルを定義しているとします:

sql
CREATE TABLE parent1 (...);
CREATE TABLE parent2 (...);

CREATE TABLE child (
  ...
) INHERITS (parent1, parent2);

このとき、Rails が PostgreSQL のメタデータを元に INHERITS (...) をダンプするときの親テーブル順序が、PostgreSQL が返す順番依存になっており、宣言順 (parent1, parent2) が保証されていませんでした。

そのため、環境やクエリプラン・拡張などの影響で、たとえば以下のように順序が揺れる可能性がありました:

sql
-- 環境A
CREATE TABLE child (...) INHERITS (parent1, parent2);

-- 環境B
CREATE TABLE child (...) INHERITS (parent2, parent1);

この違い自体は動作上の問題にはなりにくいものの、スキーマダンプの diff が毎回出てしまう等、CI やレビューにおいてノイズになります。

どう修正したか

PostgreSQL の継承情報を格納する pg_inherits カタログには inhseqno というカラムがあり、これは「その子テーブルに対して、親テーブルが何番目に宣言されたか」を表します。

今回の修正では、この inhseqno で ORDER BY するように Rails のスキーマダンプロジックを変更しています。イメージとしては以下のような SQL になります(実際のコードはもう少し複雑ですが、趣旨は同じです):

sql
SELECT
  parent.relname
FROM
  pg_inherits
  JOIN pg_class parent ON parent.oid = inhparent
WHERE
  inhrelid = 'child'::regclass
ORDER BY
  inhseqno; -- 追加されたポイント

これにより、スキーマダンプ時の INHERITS ( ... ) 部分は、必ずテーブル宣言時の順序と一致します。

テストの変更

activerecord/test/cases/adapters/postgresql/schema_test.rb が 1 行差し替えられており、継承テーブルのダンプ順序が期待どおり(宣言順)になることを検証するように更新されています。
実質的に「順序依存の仕様をテストで固定化した」変更です。


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

    • PostgreSQL を使っており、かつテーブル継承 (INHERITS) を利用しているプロジェクト。
    • スキーマダンプ(schema.rb もしくは structure.sql)をバージョン管理している環境。
  • 影響:

    • この変更を取り込むと、既存の継承テーブルの INHERITS 句の親テーブル順序が変わる可能性があります
      • これまで「PostgreSQL が返す順任せ」だったものが、「宣言順に揃えられる」ため、初回適用時に schema.rb / structure.sql に差分が生じることがあります。
      • 以後は決定的な順序になるため、同一スキーマであればダンプ結果は安定します。
  • 運用上の注意:

    • 最初にこのバージョンへアップデートした直後のスキーマ差分は、仕様変更に伴う正しい差分である可能性が高いため、内容を確認した上でそのままコミットして問題ありません。
    • 独自に pg_inherits にアクセスしているようなコードを書いている場合(かなりレアケース)でも、この変更は Rails 側の ORDER BY を追加しただけなので、直接の互換性問題は基本的に発生しません。

  1. 参考情報 (あれば)
  • PostgreSQL 公式ドキュメント: pg_inherits カタログ
    • inhseqno についての説明: 親テーブルが継承リスト中の何番目に現れるかを示すシーケンス番号。
  • PR で挙げられている再現用拡張: pg_disorder
    • PostgreSQL が返す行順を「わざと」不安定にし、順序に依存したコードの問題を顕在化させるための拡張。
    • この拡張を使うと、今回のような「順序前提の暗黙的バグ」を簡単に再現できます。

#58214 Remove unused parameter

マージ日: 2026/7/22 | 作成者: @tenderlove

  1. 概要 (1-2文で)
    Active Record の spawn_methods 内で、もはや使用されていない可変長引数(*rest のような引数)が削除されたメンテナンス用の変更です。rewhere サポート削除に伴って不要になっていた引数を整理しただけで、挙動の変更はほぼありません。

  1. 変更内容の詳細

背景

  • 以前のコミット c313d9161360f4539afeb36cc8353ad29ba6bef3rewhere 関連の機能が削除された。
  • その際、rewhere のために用意されていた「可変長引数(rest parameter)」だけがメソッドシグネチャに残り、実際には使われていない状態になっていた。

今回の変更

対象ファイル:

  • activerecord/lib/active_record/relation/spawn_methods.rb
  • activerecord/CHANGELOG.md

spawn_methods.rb 内のあるメソッド(merge!spawn 周りのいずれか)で、以下のような形で使われていない引数が削除されています:

ruby
# 変更前(イメージ)
def some_method(other, *rest)
  # rest は一切使われていない
  # ...
end

# 変更後
def some_method(other)
  # ...
end

実際には 3 行削除・3 行追加なので、

  • メソッド定義の引数リストから *rest を削除
  • それに伴い、改行やインデントなどの微調整
  • 仕様変更として CHANGELOG.md に記載を追加

という程度の小さな差分になっています。


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

    • Active Record の内部 API (ActiveRecord::Relationspawn_methods 内部) を「メソッドシグネチャレベルで」直接呼び出していたコード。
    • 具体的には、Rails の内部実装に強く依存したメタプログラミングや monkey patch をしているライブラリ・アプリケーション。
  • 通常のアプリケーションコードへの影響:

    • 通常の Model.where, merge, rewhere(既に削除済み)などのパブリック API を使っているだけであれば、挙動の変化はありません。
    • 引数が削除されたメソッドは元々余分な *rest を受け取っていただけで、中では一切利用していなかったため、機能的な違いはありません。
  • バージョンアップ時の注意点:

    • もし ActiveRecord::Relation の内部メソッドを monkey patch していて、以下のようなコードを書いている場合は、メソッド定義の引数を合わせる必要があります。

      ruby
      # 例: 元の内部メソッドに合わせたオーバーライド
      module MyPatch
        def some_method(other, *rest)
          # ...
          super
        end
      end

      これを、

      ruby
      module MyPatch
        def some_method(other)
          # ...
          super
        end
      end

      のように引数のシグネチャを変更する必要が出る可能性があります。


  1. 参考情報 (あれば)
  • この PR が依存している変更:
    • rewhere サポート削除コミット: c313d9161360f4539afeb36cc8353ad29ba6bef3
  • 関連する場所:
    • ActiveRecord::Relation のクローンやマージ処理を担う spawn_methods モジュール。
  • 種別としては「未使用引数の削除」「内部 API の軽微なクリーンアップ」であり、機能追加・仕様変更ではなく技術的負債の整理に近い変更です。

#58197 Remove unused HABTM reflection storage

マージ日: 2026/7/22 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    このPRは、has_and_belongs_to_many(HABTM)の内部で使われていなかった left_reflection の保存処理を削除する、極めて小さなクリーンアップ変更です。機能・挙動は一切変わらず、不要なコードが取り除かれただけです。

  1. 変更内容の詳細

何が削除されたか

対象ファイル:

  • activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb

変更点:

  • 内部の HABTM join モデルに対して left_reflection を保存していた処理(代入)が削除されました(2行削除・追加なし)。

イメージとしては、以下のようなコードがあったものが:

ruby
# 例: 内部 join モデルで reflection を保持していたイメージ
join_model = ...
join_model.left_reflection = reflection  # ← この代入が行われていた

この left_reflection がどこからも参照されていなかったため、その代入自体が削除されています。

歴史的な経緯

PRの説明によると:

  • 2013年のコミット d3089adb1aleft_reflection の保存が導入され、middle_options:source を埋める際に利用されていました。
  • しかし 2019年のコミット b30a23f53b で、その読み取り側(middle_options から left_reflection を読む処理)が削除されました。
  • 結果として、代入だけが残り、どこからも使われない「死んだ状態」のフィールドになっていました。
  • 今回のPRで、その不要な代入コード(および関連の保持)が取り除かれました。

HABTM 内部モデルと left_reflection

HABTM (has_and_belongs_to_many) は、内部的に「中間テーブルに対応する join モデル」を動的に定義して利用します。この join モデルに、元の関連(左側)のメタ情報として left_reflection を付与していた時期がありました。

left_reflection は Active Record の Reflection オブジェクト(関連のメタデータ)を指しており、それをもとに middle_optionssource オプションを決めるなどの処理をしていましたが、その読み取りが 2019 年に削除されたことで、もはや join モデルに保持しておく意味がなくなっていた、という構造です。


  1. 影響範囲・注意点

一般的なアプリケーションへの影響

  • 公開 API には一切変更がなく、通常の has_and_belongs_to_many の使い方や挙動は変化しません。
  • マイグレーションやスキーマ定義、関連の宣言方法に変更は不要です。
  • Rails アプリケーションコードの互換性への影響は基本的に「なし」と考えてよいです。

影響があり得るケース(かなりレア)

注意が必要になり得るのは、以下のように Rails の内部実装に依存したメタプログラミング をしているごく一部のコードです。

例えば:

  • HABTM の内部 join モデルクラスに対して、left_reflection という属性・インスタンス変数・メソッドが存在することを前提にしていた場合。
  • ActiveRecord::ReflectionActiveRecord::Associations::Builder::HasAndBelongsToMany の内部実装に直接アクセスし、left_reflection を参照するようなパッチ・モンキーパッチを書いていた場合。

そのようなコードがあると、今回の削除により NoMethodErrornil 参照などが発生する可能性があります。ただし、このようなコードは極めてニッチで、一般的なアプリケーションやライブラリでは問題にならないと考えられます。

テスト・互換性

  • 行数ベースでも 2 行削除のみであり、リスクはごく小さいクリーンアップです。
  • 2019 年以降、すでに読み取り側がなくなっていたため、今回の変更はそれを整理しただけで、実質的な挙動は以前から変わっていません。

  1. 参考情報 (あれば)

#58195 Remove unused Journey methods

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    Rails の内部ルーティングエンジン Journey において、既にどこからも呼ばれていない公開メソッドが 4 つ削除されました。外部 API ではなく内部クラス上のメソッド削除のため、通常のアプリケーションコードへの影響はほぼありません。

  1. 変更内容の詳細

削除されたメソッド

対象はいずれも ActionDispatch::Journey 配下の内部クラスです。

GTG::TransitionTable

ファイル: actionpack/lib/action_dispatch/journey/gtg/transition_table.rb

削除メソッド:

ruby
# 削除されたメソッド(イメージ)
class ActionDispatch::Journey::GTG::TransitionTable
  # ε-closure を返していた (NFA シミュレータ用)
  def eclosure(state)
    ...
  end

  # 状態一覧を返していた
  def states
    ...
  end
end
  • #eclosure
    • もともと NFA::Simulator をサポートするための互換メソッド。
    • 該当シミュレータは 2020 年のコミット 639f5fda30 で削除済みであり、以後このメソッドは実質的にデッドコードになっていた。
  • #states
    • Journey 統合時 (56fee39c39) に入ったものの、Rails 内に呼び出し元が存在しない。
    • グラフ (DOT) 出力は transitionsaccepting_states を使っており states は不要。

Nodes::Node / Nodes::Cat

ファイル: actionpack/lib/action_dispatch/journey/nodes/node.rb

削除メソッド:

ruby
# 削除されたメソッド(イメージ)
module ActionDispatch::Journey::Nodes
  class Node
    def cat?
      false
    end
  end

  class Cat < Node
    def cat?
      true
    end
  end
end
  • Nodes::Node#cat? / Nodes::Cat#cat?
    • ルーティング AST のノード種別として「連結 (concatenation) ノードかどうか」を判定するためのショートカット。
    • 本来は node.is_a?(Nodes::Cat) の代わりとして node.cat? を使う想定の API だった。
    • しかし、カスタムルートの正規表現変更が不要になったコミット 0011e098a1 で最後の呼び出し元が削除され、これ以降は未使用だった。

全体のコード差分

  • 変更ファイル: 2
  • 追加行: 0
  • 削除行: 13

純粋に未使用メソッドの削除のみであり、既存挙動を変えるようなロジックの変更はありません。


  1. 影響範囲・注意点

影響がある可能性があるケース

  1. Rails の内部クラスを直接触っているアプリ/ライブラリ

    • 以下のいずれかを直接呼んでいた場合は NoMethodError になります。
      • ActionDispatch::Journey::GTG::TransitionTable#eclosure
      • ActionDispatch::Journey::GTG::TransitionTable#states
      • ActionDispatch::Journey::Nodes::Node#cat?
      • ActionDispatch::Journey::Nodes::Cat#cat?
    • Journey は内部実装扱いであり、これらメソッドに依存するのは非推奨だったため、コード側の修正が必要です。
  2. ルーター関連の拡張 / デバッグツール

    • 独自にルーティング AST (Journey::Nodes) を解析したり、GTG::TransitionTable から状態情報を引き出しているツールがあれば確認が必要です。
    • 典型的な Rails アプリケーションや普通のエンジニアリングでこのクラス群を直接使うことはほぼないため、一般的には影響はありません。

対応方針の例

  • cat? を使っていた場合:
    • 代わりにクラス判定を使用:
      ruby
      node.is_a?(ActionDispatch::Journey::Nodes::Cat)
  • TransitionTable#states を使っていた場合:
    • そもそも内部 API 依存なので、目的に応じて別の公開インターフェイス(ルーティング情報の取得方法)を検討すべきです。
    • DOT レンダリングのような用途であれば、transitionsaccepting_states を利用する方向に書き換えが必要になるかもしれません。

一般的なアプリ開発者への影響

  • config/routes.rb で通常のルーティング定義をしているだけ、もしくは rake routes 等標準の機能だけを使っている場合は、この PR による実害はまずありません。
  • バグ修正ではなくクリーンアップ (デッドコード削除) であり、挙動変更は意図されていません。

  1. 参考情報 (あれば)

この PR は Rubydex と Ruby LSP を用いて「未参照メソッド」を洗い出した結果に基づくもので、Journey 周りの内部実装の整理・簡素化を目的としたものです。


#58198 Remove unused TemplateDetails readers

マージ日: 2026/7/22 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    ActionView::TemplateDetails::Requested クラスから、既にどこからも使われていない「生のテンプレート詳細情報」のリーダーメソッドと対応するインスタンス変数が削除されました。テンプレートマッチング処理がインデックスベース(*_idx)に完全移行したことに伴うデッドコード整理です。

  1. 変更内容の詳細
  • 対象クラス: ActionView::TemplateDetails::Requested
  • 削除されたもの:
    • 「raw detail」を返すリーダーメソッド(例: formats, variants, handlers といった、非 _idx なリーダー)
    • それらに対応するインスタンス変数

元々は以下のような構造だったと推測されます(イメージコード):

ruby
module ActionView
  class TemplateDetails
    class Requested
      attr_reader :formats, :variants, :handlers
      attr_reader :formats_idx, :variants_idx, :handlers_idx

      def initialize(...)
        @formats      = formats
        @variants     = variants
        @handlers     = handlers

        @formats_idx  = ...
        @variants_idx = ...
        @handlers_idx = ...
      end
    end
  end
end

このうち、テンプレート探索・ソート処理は後続のリファクタリング(eac19fea2b)で *_idx 系(formats_idx などの数値インデックス)だけを見るようになり、元の @formats / @variants / @handlers とそのリーダーは一切参照されなくなっていました。

今回のPRでは、*_idx 以外の上記リーダーとインスタンス変数が削除され、クラス定義が「実際に使われているインデックス情報だけ」を持つ形に整理されています。差分としては 1ファイルで6行削除のみという非常に小さい変更です。


  1. 影響範囲・注意点
  • 公開APIへの影響:
    • ActionView::TemplateDetails::Requested 自体は内部実装寄りのクラスであり、通常のアプリケーションコードでは直接触れません。
    • ただし、Rails内部APIや非公開クラスを参照しているメタプログラミング/パフォーマンスチューニング系のライブラリ・アプリが、Requestedformats / variants / handlers 等の(非 _idx)リーダーを呼び出していた場合は NoMethodError になる可能性があります。
  • 実行時挙動:
    • テンプレート解決のアルゴリズムや結果に変更はありません。既に *_idx ベースで動作していたため、削除されたリーダーは完全に未使用でした。
  • アップグレード時のチェック:
    • ActionView::TemplateDetails::Requested に直接アクセスしていない限り、互換性上の問題はほぼありません。
    • gem や自前のパッチでこのクラスのインスタンス変数・メソッドに依存している場合は、*_idx 系の情報か、より安定した公開API(例えば ActionView::LookupContext や通常のレンダリングAPI)を使うように修正した方が安全です。

  1. 参考情報 (あれば)
  • f8f9a085cc (2021): Requested に raw detail リーダーおよびインスタンス変数を追加し、テンプレートマッチング・ソートに使用し始めたコミット。
  • eac19fea2b: テンプレートインデックス周りのリファクタリングを行い、テンプレート探索で *_idx 値のみを利用するように変更したコミット。これにより raw detail リーダーが実質デッドコード化。
  • 当該PR: https://github.com/rails/rails/pull/58198

#58196 Remove unused Journey endpoints reader

マージ日: 2026/7/22 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    Rails のルーティングエンジン Journey 内の ActionDispatch::Journey::GTG::Builder#endpoints という未使用のリーダーメソッドが削除されました。
    追加も参照もされておらず常に nil しか返さない状態だったため、不要コードの整理として削除された変更です。

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

対象: actionpack/lib/action_dispatch/journey/gtg/builder.rb の 1 行変更(+1 / -1)

この PR では、ActionDispatch::Journey::GTG::Builder クラスに定義されていた「endpoints という読み取り用アクセサ(reader)」が削除されています。

イメージとしては、以下のようなコードが:

ruby
module ActionDispatch
  module Journey
    module GTG
      class Builder
        attr_reader :endpoints  # ← これが削除対象

        # ... ルーティング用のグラフ(オートマトン)を構築するロジック ...
      end
    end
  end
end

この attr_reader :endpoints(もしくはそれに相当する reader 定義)が 1 行削除されています。
PR の説明によると:

  • 2012年に Journey が Action Dispatch に統合されたコミット(56fee39c39...)のタイミングで追加された
  • しかし Rails 本体のどこからも値が設定されず、メソッドとしても呼ばれていなかった
  • そのため、この reader は常に nil を返すだけの「死んでいる API」だった

という経緯があります。


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

    • Rails コアコードでは Builder#endpoints は一切使われていないため、挙動の変化は事実上ありません。
    • ルーティングの振る舞い(マッチング・生成・制約処理など)には影響しません。
  • アプリケーションコード・ライブラリ:

    • 通常の Rails アプリケーションでは ActionDispatch::Journey::GTG::Builder に直接触れることはほぼないため、多くのプロジェクトには影響しません。
    • ただし、Journey/GTG に対して直接メタプログラミングや拡張を行っているような高度なミドルウェア・ルーティング拡張ライブラリがある場合、以下を確認してください:
      • ActionDispatch::Journey::GTG::Builder#endpoints を呼んでいないか
      • respond_to?(:endpoints) を前提としたコードがないか
    • もし上記のようなコードがあれば、この PR 適用後は NoMethodError となるため、該当コードを削除・修正する必要があります。
  • バージョン間互換性の観点:

    • 実質的には「今まで nil を返していたメソッドが、そもそも存在しなくなる」という変化なので、依存していなければ問題ありません。
    • Rails の内部 API に依存していたコードに対しては、軽微ながら 後方互換性のない変更 と言えます。

  1. 参考情報 (あれば)
  • このメソッドが追加された元コミット:
    • 56fee39c392788314c44a575b3fd66e16a50c8b5(Journey を Action Dispatch に統合したときのコミット)
  • 関連コンポーネント:
    • ActionDispatch::Journey … Rails のルーティングをパース・コンパイル・マッチングするエンジン
    • ActionDispatch::Journey::GTG … ルーティングパターンをグラフ(Generalized Transition Graph)として表現・構築する部分
  • この PR は「不要・未使用な内部 API の整理」によるコードベースのクリーンアップであり、バグ修正やパフォーマンス改善ではありません。

#58164 Generate omakase-compliant routes in the authentication generator

マージ日: 2026/7/21 | 作成者: @55728

  1. 概要 (1-2文で)
    bin/rails generate authentication が生成する config/routes.rb の記述スタイルを修正し、Rubocop(Layout/SpaceInsideArrayLiteralBrackets)に違反しない「おまかせ(omakase)」スタイルのルーティングが生成されるようにした PR です。これにより、生成直後のアプリが CI の Rubocop ジョブを素通りできるようになります。

  1. 変更内容の詳細

何を直したか

  • 認証ジェネレータ (rails g authentication) が config/routes.rb に差し込むルート定義の「配列リテラルのスペース」スタイルを修正。
  • これまでは Rubocop の Layout/SpaceInsideArrayLiteralBrackets に違反する形で配列が生成されていたため、新規アプリで即 bin/rubocop が落ちていました。
  • PR では、Rails が推奨する「omakase」スタイル(Rails 標準 Rubocop 設定に準拠)に合わせた出力に変更しています。

想定されるコードの変化イメージ

※実際の PR ではテンプレート内の文字列が 2 箇所修正されているだけですが、概念的には次のようなスタイル差分です。

修正前(Rubocop 違反例・角括弧の内側にスペースなし):

ruby
# config/routes.rb (生成物のイメージ)
authenticate :user, ->(user) { user.admin? } do
  resources :users, only:[:index, :show]
end

修正後(omakase 準拠・角括弧の内側にスペースあり):

ruby
authenticate :user, ->(user) { user.admin? } do
  resources :users, only: [ :index, :show ]
end

今回の PR で実際に行われたのは、authentication_generator.rb 内で routes.rb に挿入する文字列を、上記のように Rubocop が要求するスペース付き配列リテラルに揃える、という 2 行の修正です。
あわせて、authentication_generator_test.rb の期待値(生成結果を検証しているテスト)も同じスタイルに更新されています。


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

    • rails new した後に bin/rails generate authentication を実行したときに生成される config/routes.rb の該当ルート部分
    • 認証ジェネレータによって生成されるルートのスタイルのみが変わり、ルートの挙動(パスや HTTP メソッド、コントローラ・アクションのマッピングなど)は一切変わりません。
  • 影響:

    • 生成直後に bin/rubocop や CI の lint ジョブが Rubocop の Layout/SpaceInsideArrayLiteralBrackets で失敗しなくなる
    • 既存アプリには影響なし。既に生成済みの config/routes.rb は自動で書き換えられないため、必要であれば手動でスタイルを揃える必要があります。
  • 注意点:

    • この PR はスタイル修正のみであり、機能的な変更はありません。
    • Rubocop 側で Layout/SpaceInsideArrayLiteralBrackets の設定をカスタマイズしている場合でも、Rails が「おまかせ」として想定しているデフォルトに合わせた出力になる点は変わりません。
    • すでにこの Cop を無効化しているプロジェクトでは、見た目の違い以上の影響は特にありません。

  1. 参考情報 (あれば)
  • PR 本体: https://github.com/rails/rails/pull/58164
  • 類似の修正: c67e9dfe19 / PR #50576(インストールされるマイグレーションで同じ Rubocop Cop を修正したもの)
  • Rubocop 該当 Cop: Layout/SpaceInsideArrayLiteralBrackets
  • Rails の「omakase」スタイル:
    • Rails が公式に推奨するコードスタイルで、Rails 用 Rubocop 設定(rubocop-rails など)とも整合しており、今回のような PR は「生成コードがそのスタイルに即しているか」を担保するためのものです。

#58160 Fix NodeSource RPM URL in Development Dependencies Install guide [ci skip]

マージ日: 2026/7/21 | 作成者: @fuentesjr

  1. 概要 (1-2文で)
    Railsガイド「Development Dependencies Install」の Fedora/CentOS 向け手順で使われている NodeSource RPM の URL が 404 になっていたため、正しい URL(pub_20.x)に修正した PR です。コード本体の変更はなく、ドキュメントのみの更新です。

  1. 変更内容の詳細

対象箇所:

  • guides/source/development_dependencies_install.md

修正前のガイドでは、Fedora / CentOS で Node.js をインストールする手順として、以下のような NodeSource RPM を利用する URL を案内していました:

bash
sudo rpm -Uvh https://rpm.nodesource.com/pub_20/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm

しかし、この pub_20 パスは 404 を返すため、実際には利用できません。
NodeSource 側の現在の有効なパスは pub_20.x であり、PR ではその点を反映する形で該当行を以下のように修正しています:

bash
sudo rpm -Uvh https://rpm.nodesource.com/pub_20.x/nodistro/repo/nodesource-release-nodistro-1.noarch.rpm

差分としては pub_20pub_20.x という 1 セグメントのみの変更で、行の追加・削除も 1 行ずつです。


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

    • Rails 開発環境構築ガイドに従って Fedora / CentOS 上で開発用依存関係をセットアップするユーザー。
    • 特に Node.js を NodeSource の RPM レポジトリからインストールしようとするケース。
  • 実務的な影響:

    • これまでガイドどおりに実行すると rpm コマンドが 404 で失敗していた問題が解消されます。
    • 正しい URL のため、Node.js のインストール手順がそのまま動作するようになります。
  • 注意点:

    • この PR はドキュメントのみで、Rails 本体のコードや挙動には一切影響しません。
    • Node.js のメジャーバージョン 20 系を前提としている点は従来どおりであり、他バージョンを使いたい場合は別途 URL を読み替える必要があります(例: pub_18.x など)。
    • NodeSource の URL 仕様変更に依存しているため、将来的に再度パスが変わる可能性があります。CI は [ci skip] のためこの変更で自動検証は走っていませんが、ドキュメントのみなので問題はありません。

  1. 参考情報 (あれば)

#58188 Remove unused V6_0::ReferenceDefinition

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    Rails 6.0 用の互換クラス V6_0::ReferenceDefinition が、実際にはどこからも使われていない「死んだコード」であることが確認され、削除されました。これにより ActiveRecord のマイグレーション互換レイヤが少しシンプルになっています。

  1. 変更内容の詳細(あればサンプルコードも含めて)
  • 変更ファイル: activerecord/lib/active_record/migration/compatibility.rb
  • 変更内容: V6_0::ReferenceDefinition 関連の定義が削除され、合計 6 行が削除されています(追加なし)。

PR説明のポイントを整理すると:

  • RubydexRuby LSP を使ってコード参照を調査した結果、V6_0::ReferenceDefinition はどこからも参照されていないことが分かった。
  • 既存の ReferenceDefinition.new 呼び出しは、いずれも ActiveRecord::ConnectionAdapters 名前空間内に静的(レキシカル)に定義されており、常に ConnectionAdapters::ReferenceDefinition を指す。
  • つまり、V6_0::TableDefinitionreferences メソッドを prepend で差し替えたうえで super を呼んでも、その super の中で使われる ReferenceDefinition は互換クラスではなく、通常の ConnectionAdapters::ReferenceDefinition に解決される。

歴史的な流れ:

  • 2020-10-30 (e8437a68f6):
    Rails 6.0 互換の references / add_reference を提供するために V6_0::ReferenceDefinition が導入された。当時は互換メソッドから直接 V6_0::ReferenceDefinition.new が呼ばれていた。
  • 2022-03-14 (7c5a8fc759f):
    6.0 互換の挙動は、コンストラクタを差し替えるのではなく、_uses_legacy_reference_index_name オプションを渡した上で super を呼ぶ方式に変更された。この時点で V6_0::ReferenceDefinition はどこからも生成されなくなり、「死んだクラス」となった。

その結果、このPRでは単純にその未使用クラス定義を削除しただけ、という構造になっています。


  1. 影響範囲・注意点
  • 公開APIへの影響:
    • 通常のアプリケーションは ActiveRecord::ConnectionAdapters::ReferenceDefinition を使っており、V6_0::ReferenceDefinition を直接参照するケースはまずありません。そのため、一般的なアプリには影響はありません。
  • 影響があり得るケース:
    • Rails 内部の互換クラス (ActiveRecord::Migration::Compatibility::V6_0::*) を、リフレクションや const_get などで直接参照して独自拡張しているような特殊なコードがあれば、V6_0::ReferenceDefinition 削除で NameError になる可能性があります。
  • マイグレーション挙動:
    • 互換挙動自体はすでに _uses_legacy_reference_index_name オプション+super で提供されており、このPRは単に使われていないクラスを削除しただけなので、references / add_reference の実際の動きは変わりません。
  • バージョン互換性:
    • Rails 6.0 からのアップグレードパスのために残っていた「互換コードの残骸」を削除した形であり、Rails 7 系以降を前提にしたコードでは特に注意は不要です。

  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/58188
  • 関連コミット:
    • V6_0::ReferenceDefinition 追加: e8437a68f6 (2020-10-30)
    • 互換コンストラクタ削除・オプション化: 7c5a8fc759f (2022-03-14)
  • 関連クラス:
    • ActiveRecord::ConnectionAdapters::ReferenceDefinition
    • ActiveRecord::Migration::Compatibility::V6_0::TableDefinition

#58190 Deprecate Cookies::HTTP_HEADER constant

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    ActionDispatch::Cookies::HTTP_HEADER 定数が非推奨(deprecated)になり、今後は Rack::SET_COOKIE を使うことが推奨されるようにした PR です。既に内部的には未使用ですが、外部利用があるため、即削除ではなく非推奨期間を設けています。

  1. 変更内容の詳細

なにが非推奨になったか

  • 対象: ActionDispatch::Cookies::HTTP_HEADER 定数
  • 役割: 過去に cookie ミドルウェアが Set-Cookie ヘッダを正規化する際に使っていた定数
  • 状況:
    • 2010-05-18 に追加 (d3e62fc57c)
    • 2023-01-21 の変更 (706fb10ad5) で、Rack 2 / 3 両対応のため cookie 処理が再び Rack::Response に戻され、内部では使われなくなった
    • しかし API ドキュメントに公開されており、外部で参照しているコードがあるため、互換性のためすぐには削除せず「非推奨」として残す、という判断

コード上の変更ポイント

PR で行われているのは主に次の3点です。

  1. ActionDispatch::Cookies::HTTP_HEADER に deprecation を付与
  2. CHANGELOG に非推奨の旨を追記
  3. 非推奨の挙動をテストで担保

実際のイメージとしては、cookies.rb では以下のように非推奨メッセージが出る形になっているはずです(概念的な例):

ruby
module ActionDispatch
  class Cookies
    # 既存: HTTP_HEADER = 'Set-Cookie'
    HTTP_HEADER = 'Set-Cookie'

    ActiveSupport::Deprecation.warn(
      "ActionDispatch::Cookies::HTTP_HEADER は非推奨です。" \
      "代わりに Rack::SET_COOKIE を使用してください。"
    )
  end
end

※実装は ActiveSupport::Deprecation.deprecate_constant などを使った形になっている可能性が高いですが、意図としては「この定数を参照すると deprecation がかかる」というものです。

actionpack/test/dispatch/cookies_test.rb では、この定数に対して deprecation が発生すること・まだ定数自体は存在していることを確認するテストが追加されています。


  1. 影響範囲・注意点

影響を受ける可能性があるコード

  • 以下のようなコードを書いているアプリ/ライブラリ:
ruby
ActionDispatch::Cookies::HTTP_HEADER # => 'Set-Cookie' を期待している
  • 上記のようなコードは:
    • 今後の Rails バージョンで deprecation warning が出る
    • 将来のメジャーバージョンで 定数自体が削除される可能性が高い

どう書き換えるべきか(推奨)

HTTP_HEADER を使っている箇所は、Rack::SET_COOKIE に置き換えが推奨されています。

ruby
# 変更前
header_name = ActionDispatch::Cookies::HTTP_HEADER

# 変更後
header_name = Rack::SET_COOKIE

Rack を直接参照できない状況で Set-Cookie という文字列自体が欲しいだけなら、単に文字列リテラル "Set-Cookie" を使うのも選択肢ですが、この PR の説明にある通り「正準(canonical)な置き換え」は Rack::SET_COOKIE なので、Rack レベルのインターフェイスに従いたい場合はこちらを使うのが無難です。

注意点

  • 今回は「非推奨化」であって即削除ではないため、現時点でアプリが壊れることはありません。
  • ただし CI で deprecation をエラー扱いしているプロジェクトでは、警告が原因でテストが落ちる可能性があります。
  • ミドルウェアやプラグインで Cookie / Header 処理をカスタマイズしている場合は、ActionDispatch::Cookies::HTTP_HEADER を参照していないか確認しておくと安全です。

  1. 参考情報 (あれば)
  • この PR の背景:
    • 2010 年に「Rack::Response でラップしなくなったため、ミドルウェア側で Set-Cookie を正規化する必要があった」ことから HTTP_HEADER が導入。
    • 2023 年に Rack 2/3 両対応の流れで Cookie 処理が再び Rack::Response 側に戻され、HTTP_HEADER は内部では不要になった。
  • Rack::SET_COOKIE は Rack レベルで Set-Cookie ヘッダ名を表す正準的な定数として提供されているものです。
  • 将来の Rails メジャーバージョンアップ時には、この定数が削除される可能性が高いので、外部ライブラリ作者は早めの移行が推奨されます。

#58191 Deprecate RedisCacheStore::DEFAULT_REDIS_OPTIONS

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    ActiveSupport::Cache::RedisCacheStore::DEFAULT_REDIS_OPTIONS が、Rails 内部では既に使われていないため、即削除ではなく「非推奨(deprecated)扱い」として残しつつ、将来的な削除に向けた移行パスを示した PR です。代わりに、アプリ側は RedisCacheStore もしくは利用中の Redis クライアントに対して、タイムアウトなどのオプションを明示的に渡すように促されています。

  1. 変更内容の詳細

背景

  • DEFAULT_REDIS_OPTIONS は 2017-11-13 のコミット (9f8ec35352) で、標準の Redis キャッシュストア導入時に追加された定数。
  • しかし 2026-06-01 のコミット (5843a7b7f2) で redis-client への移行が行われ、その過程で ファイル内の呼び出し元がすべて削除 され、内部的には未使用状態になっていた。
  • 一方で、この定数は API ドキュメントに公開されているため、外部アプリケーションが参照している可能性 がある。

この PR では、その事情を踏まえ、

  • 直接削除せず、
  • 値を保持したまま「非推奨」とし、
  • 代替手段を案内する
    という形に変更されています。

コード上の変更ポイント

(1)RedisCacheStore 内での定数定義の扱い変更
DEFAULT_REDIS_OPTIONS 自体は 値を変えずに残しつつ、非推奨としてマーク されています。具体的には次のようなイメージです(実際のコードは概念的な例):

ruby
module ActiveSupport
  module Cache
    class RedisCacheStore < Store
      # 以前から存在する定数
      DEFAULT_REDIS_OPTIONS = {
        # 例: connect_timeout: 1.0, read_timeout: 1.0, write_timeout: 1.0
      }

      # 新たに、利用時に非推奨警告を出すロジックが追加されている可能性が高い
      def self.default_redis_options
        ActiveSupport::Deprecation.warn(
          "RedisCacheStore::DEFAULT_REDIS_OPTIONS は非推奨です。 " \
          "代わりに RedisCacheStore.new または Redis クライアントに " \
          "タイムアウト等のオプションを直接指定してください。"
        )
        DEFAULT_REDIS_OPTIONS
      end
    end
  end
end

実装としては、

  • DEFAULT_REDIS_OPTIONS の参照時に ActiveSupport::Deprecation.warn を呼ぶ
  • または、DEFAULT_REDIS_OPTIONS 定数自体に対する Deprecation 表記・テストを追加
    といった形で、「Rails が提供する公開 API だが、今後は使わないでほしい」という扱いになっています。

(2)CHANGELOG の更新
activesupport/CHANGELOG.md に、以下のような内容が追記されています(意訳):

  • RedisCacheStore::DEFAULT_REDIS_OPTIONS を非推奨化したこと

  • これまでこの定数に依存していたアプリケーションは、

    • RedisCacheStore を初期化する際や、
    • 利用中の Redis クライアント (redis-client 等)

    に対して、タイムアウト等のオプションを明示的に指定するように、というガイダンスが追記されています。

(3)テストの追加
activesupport/test/cache/stores/redis_cache_store_test.rb に、

  • DEFAULT_REDIS_OPTIONS を使用したときに 非推奨警告が出ること を確認するテスト
    などが追加されています。
    これにより、今後の変更で誤ってこの定数を削除・変更してしまった場合にも検出できるようになっています。

  1. 影響範囲・注意点

影響を受ける可能性があるケース

以下のようなコードを書いているアプリケーションは影響を受けます。

ruby
# 例1: 定数をそのまま流用している
options = ActiveSupport::Cache::RedisCacheStore::DEFAULT_REDIS_OPTIONS.merge(
  db: 1
)

# 例2: Redis クライアントにそのまま渡している
redis = Redis.new(
  ActiveSupport::Cache::RedisCacheStore::DEFAULT_REDIS_OPTIONS.merge(url: ENV["REDIS_URL"])
)

このようなコードは 動作自体は当面変わらないものの、Rails 実行時に deprecation warning が出るようになります。

対応方針

  1. 非推奨警告を見逃さない

    • テスト実行時や開発環境のログに Deprecation Warning が出ていないか確認してください。
  2. DEFAULT_REDIS_OPTIONS への依存を廃止する

    • DEFAULT_REDIS_OPTIONS を直接使わず、自分のアプリ側で必要なオプションを定義してください。

    例:

    ruby
    DEFAULT_REDIS_OPTIONS = {
      connect_timeout: 1.0,
      read_timeout: 1.0,
      write_timeout: 1.0
    }.freeze
    
    redis = Redis.new(DEFAULT_REDIS_OPTIONS.merge(url: ENV["REDIS_URL"]))
  3. 推奨される設定方法を使う
    PR 説明にある通り、今後は以下のいずれかの方法でタイムアウト等のオプションを渡してください。

    • RedisCacheStore に直接指定:

      ruby
      config.cache_store = :redis_cache_store, {
        url: ENV["REDIS_URL"],
        connect_timeout: 1.0,
        read_timeout: 1.0,
        write_timeout: 1.0
      }
    • あるいは、RedisClient / redis-client 側のコンフィグで指定:

      ruby
      redis = RedisClient.new(
        url: ENV["REDIS_URL"],
        connect_timeout: 1.0,
        read_timeout: 1.0,
        write_timeout: 1.0
      )

将来的な削除リスク

  • この PR は「非推奨化」であり、将来のメジャーバージョンで DEFAULT_REDIS_OPTIONS が削除される可能性が高い ことを意味します。
  • 長期運用を見据える場合は、早めに依存を取り除いておく方が安全です。

  1. 参考情報 (あれば)
  • PR 本文に記載されている関連コミット:
    • 9f8ec35352: RedisCacheStoreDEFAULT_REDIS_OPTIONS が導入されたコミット
    • 5843a7b7f2: redis-client への移行により、この定数の内部利用がなくなったコミット
  • Redis キャッシュストアの公式ガイド (英語):
    • Rails Guides: Caching with Rails (RedisCacheStore セクション)
  • 実運用では、DEFAULT_REDIS_OPTIONS の具体的な中身は Rails のバージョンにより異なる可能性があるため、
    どうしても値を維持したい場合は、今利用しているバージョンでの中身を自前でコピーしておき、アプリ側の定数として保持するのが安全です。

#58193 Remove unused test methods

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    Rails 本体の「テスト専用コード」から、参照されていないメソッド/アクセサ計 24 個を削除するクリーンアップ PR です。
    すべて test/ 配下の変更であり、本番コードの挙動や公開 API には影響しません。

  1. 変更内容の詳細

全体像

  • 対象は以下のコンポーネントの テストコードのみ:
    • Action Pack
    • Action View
    • Active Job
    • Active Record
    • Active Support
    • Railties
  • Rubydex で「定義はあるがどこからも呼ばれていないメソッド」を洗い出し、
    • リポジトリ全体で grep 確認
    • コミット履歴と周辺コードを目視確認
    • 動的ディスパッチで呼ばれる可能性があるもの(コールバックシンボル、インターポレーションされたアクション名、*_attributes= など respond_to? で叩かれるもの)は除外
      したうえで、本当に誰からも呼ばれていないものだけを削除しています。

合計の変更量は「+1 / -98」で、実質的には不要メソッドの削除のみです。


コンポーネント別の主な削除内容

Action Pack (actionpack/test/...)

以下のような「テスト用コントローラのアクション」や「アサーションヘルパー」が削除されています。いずれも、過去のリファクタリングや非推奨機能削除の際に、呼び出し側だけ消えてテスト用メソッドが取り残されたものです。

例:

  • ActionPackAssertionsController#render_url
    • かつて get :render_url で使われていたが、非推奨タグサポート削除 (2ff60e8...) のときに呼び出しが消失。
  • ControllerLayouts::ImplicitController#builder_override
    • 2009 年の新 base テストポートで追加されたが、ルーティングもテストからの呼び出しも無いまま。
  • リダイレクト系:
    • RedirectController#dashboard_url
      • redirect_to(:symbol, *args) サポート用のテストだったが、非推奨 API 削除 (f770165...) とともに呼び出しが削除済み。
  • テストヘルパー:
    • ResourcesTest#assert_resource_methods
      • RouteSet API のテストから抽出されたアサーションだが、呼び出し元が削除済み。
  • TestCaseTest::TestController#redirect_to_same_controller / #redirect_to_different_controller
    • 壊れていた functional test 実装の削除 (db5839...) でテスト側が消えた。
  • Cookie 関連:
    • set_cookie_with_domain_all_as_string / delete_cookie_with_domain_all_as_string
      • 文字列 "all" を domain に使うテスト用に追加されたが、実際のテストはシンボルドメイン版のみ使用。
  • パラメータパーシング用アクセサ:
    • UrlEncodedParamsParsingTest::TestController.last_request_type
      • multipart 向けに導入されたが、multipart 側のロジック削除 (5a4390...) で参照が無くなった。

該当ファイル:

  • actionpack/test/controller/action_pack_assertions_test.rb
  • actionpack/test/controller/new_base/render_layout_test.rb
  • actionpack/test/controller/redirect_test.rb
  • actionpack/test/controller/resources_test.rb
  • actionpack/test/controller/test_case_test.rb
  • actionpack/test/dispatch/cookies_test.rb
  • actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb (+1/-1 は不要アクセサ削除による微調整)

Action View (actionview/test/actionpack/controller/render_test.rb)

レンダリング関連のテスト用コントローラアクションのうち、呼び出しが存在しないものが削除されています。

例:

  • render_hello_world_with_last_modified_set / blank_response
    • ETag / conditional GET がミドルウェアに移動した (74dd8a3...) 際に、これらを叩くリクエストテストが削除。
  • render_action_hello_world_as_symbol
    • render テストのマージ (a200c6...) でリクエスト側が削除済み。別途同名のインテグレーションフィクスチャが存在した時期もあるが、これは別定義で本メソッドを参照していない。
  • partial_collection_shorthand_with_different_types_of_records_with_counter
    • ルートもテストも無いまま追加されていたアクション。
  • partial_with_implicit_local_assignment
    • インスタンス変数からの暗黙ローカル代入が非推奨化・削除 (7aa730...) されたときにテストが削除され、メソッドだけ残ったもの。

Active Job

  • ActiveJob::TestContinuation#capture_info_stdout
    • continuation テスト追加時 (0ac3fb...) に導入されたが、一度も呼ばれていない。
  • TestCaseHelpers#continuable_job_started
    • Sidekiq Active Job アダプタの非推奨削除 (10c24f...) により最後の呼び出しが消失。

該当ファイル:

  • activejob/test/cases/continuation_test.rb
  • activejob/test/support/integration/test_case_helpers.rb

Active Record

暗号化やトランザクションのテストヘルパーで、過去の大きなリファクタリングの副産物として「使われなくなった private ヘルパ」が削除されています。

  • EnvelopeEncryptionKeyProviderTest#assert_multiple_primary_keys
    • 元は assert_multiple_master_keys として抽出されたが、そもそも呼ばれておらず、その後名前だけ変更 (28145c3...) されても利用されていなかった。
  • EncryptionHelpers#assert_encrypted_record
    • 大量暗号化テスト群削除 (c41b35...) により呼び出しが消失。
  • EncryptionHelpers#with_envelope_encryption
    • 暗号化ストレージの性能テスト削除 (3e7ba5...) で最後の継承元からの呼び出しが消えた。
  • TransactionTest#transaction_with_return
    • return によるロールバック挙動の非推奨化/削除 (eccc60...) により、このケース専用のヘルパーメソッドが不要に。
      • 同ファイルには、まだ使われている「浅い return 用」のヘルパーが残っており、こちらは温存。

該当ファイル:

  • activerecord/test/cases/encryption/envelope_encryption_key_provider_test.rb
  • activerecord/test/cases/encryption/helper.rb
  • activerecord/test/cases/transactions_test.rb

Active Support

  • CallbacksTest::ConditionalPerson#other_yes / #other_no
    • コールバック実装の書き換え (21e7b8...) により、これらを参照していた condition 配列が削除されたが、メソッドは残っていた。
  • DeprecationTest#with_rails_application_deprecators
    • ActiveSupport::Deprecation インスタンスデリゲーションの非推奨削除 (c682bf...) の際に、これを使う 6 箇所の呼び出しがすべて削除済み。

該当ファイル:

  • activesupport/test/callbacks_test.rb
  • activesupport/test/deprecation_test.rb

Railties

  • TestHelpers::Paths#framework_path
    • 初期化処理のロードパス関連テスト (4f6d6f7...) から使われていたが、そのテスト群が削除されて以降、どこからも使われていない。

該当ファイル:

  • railties/test/isolation/abstract_unit.rb

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

    • Rails のテストコードのみに影響します。
    • 本番コード (app/, lib/ 等) や公開 API は一切変更されていません。
    • CI 上で該当コンポーネントのテストはすべてグリーンであることが確認されています。
  • 注意点:

    • Rails コアのテストコードを参考にして「同名のメソッドを自前のテストスイートにコピペしている」場合でも、この PR はそれらには影響しません(自分のリポジトリの話になるため)。
    • ただし、Rails 本体のテストを読み物/サンプルとして参照している場合は、「このメソッドどこにも呼ばれてないな」と思っていた箇所が整理された、という位置付けです。
    • 動的に呼ばれていそうなメソッドは、意図的に削除対象から外しているため、send, callback シンボル, *_attributes= などを経由した隠れた参照を消してしまうリスクは抑制されています。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58193
  • 主な過去コミット例(説明文中で参照されているもの):
    • 非推奨タグサポート削除: 2ff60e86486141b5a5aae6e1a0a7521fbd85a269
    • redirect_to(:symbol, *args) 削除: f770165cc23a44ac329b5b1fbd47376a6c2e3f11
    • 条件付き GET のミドルウェア移行: 74dd8a3681c6984ea35c879f88c6a87521b58ec2
    • 暗号化関連テストの整理: 638a92f7344963c7e42eeb84eb816e4592bda7e4, c41b354bf065c9e59d65abbaeb22897f95ab47c0, 3e7ba58439a434be5e6894993370a8cef403303e
    • ActiveSupport::Deprecation の非推奨削除: c682bf26417f2a88b0efa46499e94dcf45955f4f

この PR は、長年の互換性維持やリファクタリングの過程で取り残されていたテスト用ヘルパーを一掃する、テストスイートの健全性向上とノイズ削減のためのメンテナンスという位置付けです。


#58192 Remove unused Routing::HTTP_METHODS constant

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    ActionDispatch::Routing 内で長らく使われていなかった定数 HTTP_METHODS が削除されました。Rails本体からは既に参照されておらず、内部実装の古い残骸をクリーンアップする変更です。

  1. 変更内容の詳細

何が削除されたか

  • 対象: ActionDispatch::Routing::HTTP_METHODS 定数
  • 状態: もともと :nodoc: 扱いの内部定数であり、現在のRailsコードベースからは一切参照されていないことが確認されていました。

actionpack/lib/action_dispatch/routing.rb から、この未使用定数の定義が削除されています(+0/-3)。
actionpack/lib/action_dispatch/routing/mapper.rb でも、関連する記述がわずかに調整されています(+1/-1)※主に定数削除に伴う整合性レベルの変更で、振る舞いの変化はありません。

元々の役割

PR説明によると:

  • 2007-05-26 のコミット dcaa074abf で追加
    • 用途: ルーティングで「メソッドが許可されていない (405 Method Not Allowed)」エラーを報告する際に、サポートしているHTTPメソッド一覧を列挙するための内部リストとして使用。
  • 2009-12-11 のコミット 588225f885 で実質的に未使用化
    • 理由: そのルーティング時のフォールバック実装が「hackが過ぎる」として削除され、この定数への参照もなくなった。
  • 2010-03-31 のコミット c10bf8205c でRouting実装詳細がRDoc対象外になり、定数も明示的に :nodoc: 扱いに。

その後15年以上、Railsのどこからも参照されていない状態で残っていたところを、今回のPRで削除した、という経緯です。


  1. 影響範囲・注意点

影響が「ない」ケース

  • アプリケーションコードやgemが ActionDispatch::Routing::HTTP_METHODS を使っていない場合
    → 挙動の変化はありません。ルーティングの動作やHTTPメソッドの扱いには影響しません。

影響が「ある」可能性があるケース

内部定数として扱われていたため、本来は依存すべきではありませんが、もし以下のようなコードを書いていた場合はエラーになります。

ruby
# 例: Railsアプリやgem側で内部定数を直接参照していた場合
ActionDispatch::Routing::HTTP_METHODS # => NameError になる

このような箇所がある場合は、代替として自前で定義するか、Rack 由来の情報を使うなどに置き換える必要があります。

たとえば自前でサポートメソッドを定義する例:

ruby
SUPPORTED_HTTP_METHODS = %w[GET POST PUT PATCH DELETE HEAD OPTIONS].freeze

※どのメソッドをサポートするかはアプリケーションやRackバージョンに依存するため、用途に応じて適宜定義してください。

バージョンアップ時のチェック

  • Railsアップグレード時の非互換チェックとして:
    • ActionDispatch::Routing::HTTP_METHODS がコードベース・自作gem・社内ライブラリで使われていないか検索すると安心です。

  1. 参考情報 (あれば)
  • このPRで言及されている過去コミット:
    • dcaa074abf (2007-05-26): HTTP_METHODS 追加
    • 588225f885 (2009-12-11): メソッド未許可時のフォールバック削除 → 定数が未使用に
    • c10bf8205c (2010-03-31): Routing実装詳細がRDocから除外され、HTTP_METHODS:nodoc: の内部定数として放置
  • PR番号: #58192 「Remove unused Routing::HTTP_METHODS constant」
  • マージ日時: 2026-07-21T22:56:57Z

#58194 Restore unregistered tests

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    このPRは、test_ プレフィックスがついておらず Minitest によって実行されていなかった6つのテストメソッドを、正しくテストとして登録し直したものです。テスト内容自体は変えず、メソッド名だけを修正してテストが実行されるようにしています。

  1. 変更内容の詳細

背景

Minitest は、メソッド名が /^test_/ にマッチするインスタンスメソッドをテストとして自動検出します。
このPRで修正されたメソッドは:

  • アサーションを含んでいる
  • 他のどこからも呼び出されていない
  • しかしメソッド名に test_ プレフィックスがなかった

ため、意図されたテストであるにもかかわらず一度も実行されていなかったという状態でした。

実際の変更内容

以下の 4 ファイルで、合計 6 個のテストメソッド名が修正されています。内容のロジックはそのままで、名前だけ変更されています。

対象ファイル:

  • actionpack/test/controller/action_pack_assertions_test.rb
  • activerecord/test/cases/finder_test.rb
  • activerecord/test/cases/primary_keys_test.rb
  • railties/test/generators/authentication_generator_test.rb

変更パターンはどれも同様で、例として以下のような修正が行われています:

ruby
# 修正前 (テストとして認識されない)
def missing_prefix_for_some_behavior
  assert_equal 1, some_value
end

# 修正後 (Minitest によりテストとして検出される)
def test_missing_prefix_for_some_behavior
  assert_equal 1, some_value
end

行数ベースでは:

  • 追加行: 6 (すべてプレフィックス付きメソッド名)
  • 削除行: 6 (プレフィックスなしメソッド名)

であり、メソッド本体のアサーションやロジックには一切変更がありません。


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

    • Rails 自身のテストスイートに限定され、本体のフレームワーク挙動や公開APIには影響しません。
    • これまで「存在していたが実行されていなかった」内部テストが、CI 等で新たに実行されるようになります。
    • もしこれらのテスト対象の挙動に不具合が既に紛れ込んでいた場合、このPR適用後に初めてテストが落ちる可能性があります。
  • 注意点 (テストを書く側向け)

    • Minitest を使う場合、必ず def test_... という命名規約に従う必要があることを再確認できます。
    • DSL (test "description" do ... end) を使わず、素のメソッドで書いている箇所では特に命名ミスに注意が必要です。
    • RSpec 等とは異なり、「それっぽい名前のメソッド」は自動で拾ってくれないので、IDE や RuboCop ルールなどで「test_ なしのテストメソッド」を検出する仕組みを入れておくと同様の問題を防げます。

  1. 参考情報 (あれば)
  • Minitest のテスト検出ルール:
    • Minitest::Test を継承したクラスのインスタンスメソッドで、名前が /^test_/ にマッチするものが自動でテストとして実行される。
  • 関連しうる改善アイデア:
    • RuboCop のカスタム Cop や simplecov などと組み合わせて、「定義されているが一度も呼ばれていないテストっぽいメソッド」を検出することで、今回のような取りこぼしを防ぐことができます。

#58182 Remove unused ThreadMonitor#mon_try_enter method

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    RailsのActiveSupport::Concurrency::ThreadMonitorから、既にどこからも呼ばれていなかったmon_try_enterメソッドが削除されました。機能追加ではなく、不要コードの整理によるリファクタリングです。

  1. 変更内容の詳細

何が起きたか

  • 過去のコミット 51d4feaabcThreadLoadInterlockAwareMonitorThreadMonitor へ実装が差し替えられた際、ヘルパーメソッド mon_try_enter が旧実装からそのまま持ち込まれていました。
  • 旧実装では LoadInterlockAwareMonitorMixinmon_try_enter を呼び出していましたが、このミックスイン自体が削除され、代わりに ThreadMonitor#synchronizemon_enter を直接呼ぶようになりました。
  • その結果、ThreadMonitor 内で mon_try_enter はどこからも参照されない「死んだメソッド」となっており、今回その定義が削除されています。

実際の変更

  • 変更ファイル: activesupport/lib/active_support/concurrency/thread_monitor.rb
  • 追加行: 0 / 削除行: 8
  • 内容はmon_try_enterメソッド定義の削除のみで、他のメソッドやクラス構造には手を加えていません。

イメージとしては、以下のようなコードがファイルからなくなった形です(概念的な例):

ruby
module ActiveSupport
  module Concurrency
    class ThreadMonitor
      # これに相当するメソッド定義が削除された
      def mon_try_enter
        # MonitorMixin にある non-blocking な enter ラッパー相当の実装
      end
    end
  end
end

ThreadMonitor#synchronizemon_enter / mon_exit といった、実際に使われているメソッドはそのままです。


  1. 影響範囲・注意点
  • 公式APIへの影響

    • ThreadMonitor#mon_try_enter は実質的に内部実装用のヘルパーで、これまでのコードパスでは利用されていなかったため、通常のRailsアプリケーションには影響しません。
    • 公開ドキュメントに載るような一般的APIでもありません。
  • 独自に ThreadMonitor を直接使っている場合

    • アプリ / gem 側で ThreadMonitor#mon_try_enter直接呼び出していた場合のみ、このPRを含むバージョンに上げると NoMethodError になります。
    • 該当するコードがある場合は、下記のいずれかへの置き換えが必要です:
      • 本来の目的が「普通の排他制御」であれば、synchronize を利用する:
        ruby
        monitor.synchronize do
          # 排他処理
        end
      • 「ブロックせずにロックを試みる」ようなtry-lock挙動が必要なら、ThreadMonitor 自体ではなく、別途MonitorMutex相当の実装・パターンを採用する必要があります(ThreadMonitorはそうしたpublicなtry-lock APIを提供していません)。
  • 挙動の変化

    • 実際に動作するコードパスからは mon_try_enter が一切呼ばれていなかったため、Rails内部のロック挙動やスレッドセーフティの意味での挙動変更はありません。
    • これは純粋にデッドコード削除であり、パフォーマンスへの影響も事実上ありません(読み込むコード行がわずかに減る程度)。

  1. 参考情報 (あれば)
  • 関連コミット: 51d4feaabc
    • このコミットで ThreadLoadInterlockAwareMonitorThreadMonitor への置き換えが行われ、LoadInterlockAwareMonitorMixin が削除されました。
    • LoadInterlockAwareMonitorMixin から呼ばれていた mon_try_enter が、新実装では呼ばれなくなった経緯が説明されています。
  • このPRは、コードベースのクリーンアップ・保守性向上の一環として理解できます。
    内部実装に依存したメソッド呼び出しを避けていれば、アップデート時に追加対応は不要です。

#58185 Remove unused ParamBuilder#new_depth_limit method

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    Rails の ActionDispatch::Http::ParamBuilder クラスから、実際には一度も使われていなかった new_depth_limit メソッドが削除されました。機能的な変更はなく、内部実装の不要コードの整理(リファクタリング)にあたります。

  1. 変更内容の詳細
  • 対象ファイル: actionpack/lib/action_dispatch/http/param_builder.rb
  • 変更内容: ParamBuilder クラス内の new_depth_limit メソッドを削除(4行削除、追加なし)

PR説明によると:

  • ParamBuilder が導入されたコミット(ce5f1817bc)の際に、Rack::QueryParser から new_depth_limit メソッドがコピーされていた。
  • しかし Rails 側では、Rack に存在する「コール元(caller)」や「深さ制限を設定するためのメソッド」に相当するものを実装しておらず、そのためこのメソッドは導入当初から呼ばれていなかった。
  • 結果として「未使用のヘルパーメソッド」となっていたため、今回削除された。

イメージとしては、以下のようなメソッドが消えた形です(実際の実装はPR本体を参照してください。あくまで概念イメージです):

ruby
# 変更前(イメージ)
module ActionDispatch
  module Http
    class ParamBuilder
      # ... 他のメソッド ...

      def new_depth_limit
        # Rack::QueryParser からコピーされてきたが
        # Rails 側では一度も呼ばれていない
      end
    end
  end
end

# 変更後
module ActionDispatch
  module Http
    class ParamBuilder
      # new_depth_limit が削除されただけで、
      # 他の公開APIや挙動はそのまま
    end
  end
end

  1. 影響範囲・注意点
  • 公開APIへの影響:

    • ParamBuilder#new_depth_limit はもともと内部実装用で、実際にはどこからも呼ばれていなかったため、Rails の公式なパブリックAPIとして使われている想定はありません。
    • 一般的なアプリケーションコードに対する影響は実質ゼロと考えられます。
  • 影響がありうるケース:

    • Rails 内部実装に依存している高度なメタプログラミングや Monkey-patching をしていて、ActionDispatch::Http::ParamBuilder#new_depth_limit を直接呼んでいた場合は NoMethodError になります。
    • そのようなケースはかなりレアですが、もし独自に ParamBuilder を拡張している場合は念のため grep などで new_depth_limit の利用有無を確認すると安全です。
  • 動作上の変更:

    • リクエストパラメータのパース挙動(深さ制限、ネスト制御など)には変更がありません。
    • Rack 側の QueryParser に存在する「ネストの深さ制限を調整するAPI」が Rails 側で使われていない状況は、以前から変わっていません。今回のPRは、それを明確化する形で未使用メソッドを削除しただけです。

  1. 参考情報 (あれば)
  • 該当PR: https://github.com/rails/rails/pull/58185
  • ParamBuilder 導入コミット: ce5f1817bc(PR説明より)
  • 元になった Rack のコード: Rack::QueryParser(リクエストパラメータのパースとネスト深さ制限を扱うクラス)

今後、もし Rails が Rack の深さ制限設定をフックするAPIを提供する場合は、そのタイミングで改めて ParamBuilder 側に対応メソッドが追加される可能性がありますが、今回の変更はあくまで「使われていないコードの削除」に留まります。


#58176 Remove unused UriEncoder constants

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    Rails 内部クラス UriEncoder に定義されていた未使用の定数 UTF_8ESCAPED が削除されました。内部実装のデッドコード整理であり、公開APIの挙動には影響しません。

  1. 変更内容の詳細
  • 対象ファイル:
    actionpack/lib/action_dispatch/journey/router/utils.rb

  • 変更内容:
    UriEncoder クラス内の、以下の2つの定数定義が削除されています(合計3行削除)。

    元のイメージ(簡略化例):

    ruby
    module ActionDispatch
      module Journey
        class Router
          class Utils
            class UriEncoder # :nodoc:
              UTF_8   = Encoding::UTF_8
              ESCAPED = /%[0-9a-fA-F]{2}/
              # ...
            end
          end
        end
      end
    end

    この UTF_8 / ESCAPED は、もともと UriEncoder#unescape_uri のために 2014年に追加されたもので、

    • UTF_8: unescape_uri の出力文字列のエンコーディング指定
    • ESCAPED: 「%xx 形式のエンコード済みバイト列」にマッチさせる正規表現

    として使われていました。

    その後、2025-02-13 のコミット dfc5e80e0funescape_uri が削除され、代わりに CGI.unescape が使われるようになったことで、この2定数はどこからも参照されない「死んだコード」になっていました。今回のPRは、この未使用定数を削除して内部実装をクリーンアップするものです。


  1. 影響範囲・注意点
  • 公開APIへの影響:

    • UriEncoder 自体が内部用 (nodoc) クラスであり、Rails の公式な公開APIではありません。
    • unescape_uri はすでに過去のコミットで削除済みのため、このPRで新たに公開挙動が変わることはありません。
    • アプリケーションコードや gem が 内部クラス ActionDispatch::Journey::Router::Utils::UriEncoder の定数 UTF_8 / ESCAPED に依存していない限り、影響はありません。
  • もし注意が必要なケース:

    • Rails の内部実装に直接依存している高度なメタプログラミングやパッチングをしている場合で、
      ruby
      ActionDispatch::Journey::Router::Utils::UriEncoder::UTF_8
      ActionDispatch::Journey::Router::Utils::UriEncoder::ESCAPED
      のように参照していた場合は、NameError になります。
    • そのような場合は、用途に応じて:
      • エンコーディングが欲しいだけなら Encoding::UTF_8 を直接使う
      • %xx 形式のエンコード済みバイト列」にマッチさせたいなら、自前で /%[0-9a-fA-F]{2}/ を定義する などに変更する必要があります。
  • URLデコード処理への影響:

    • unescape_uri はすでに CGI.unescape に置き換えられているため、URLデコードの実際の挙動・互換性にはこのPRは関与しません。

  1. 参考情報 (あれば)
  • 関連コミット:
    • a61792574d (2014-04-20): UriEncoder#unescape_uri 向けに UTF_8ESCAPED を追加。
    • dfc5e80e0f (2025-02-13): unescape_uri を削除し、CGI.unescape に置き換え。この時点で定数が未使用となる。
  • 内部実装の今後の方針:
    • Rails コアは内部 API のデッドコードを定期的に削除しており、今回もその一環。
      内部クラス・内部定数への依存は将来のバージョンアップ時に壊れやすいため、可能な限り公開API(例: CGI.unescape 等)を使うことが推奨されます。

#58178 Remove unused Journey::GTG::MatchData

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    Rails の内部ルーティングエンジン Journey に存在していた未使用クラス Journey::GTG::MatchData が、完全に参照されていない“死んだコード”であるため削除されました。これに伴い、そのクラスを前提としたコメント・コード片が simulator.rb から取り除かれています。

  1. 変更内容の詳細
  • 対象ファイル

    • actionpack/lib/action_dispatch/journey/gtg/simulator.rb
  • 実際の変更
    この PR では、MatchData に関連するコードが 8 行削除されています(追加はなし)。
    説明文から読み取れるポイントは以下の通りです。

    • Journey::GTG::MatchData は内部クラス(nodoc)であり、
      • コンストラクタ呼び出し(MatchData.new
      • クラス定数参照(Journey::GTG::MatchData 自体)
        が一切残っていない状態になっていた。
    • 2017 年のコミット(845aabbcd3)で、Simulator#simulate メソッドとその中の MatchData.new 呼び出しが削除された時点で、このクラスは実質的に誰からも使われなくなっていた。
    • 今回の PR は、その状況を整理し、simulator.rb から MatchData に紐づく残存コードを削除している。

    実際のクラス定義自体は以前のコミットで消えている可能性が高く、この PR では「最後まで残っていた紐付け(コメントや不要な参照)」を掃除している、という位置付けと考えられます。

    (差分は小さいため、サンプルコードとしてはイメージになりますが、以下のようなものが消えていると考えられます)

    ruby
    # 例: すでに存在しない MatchData を想定したコード・コメント
    # result = MatchData.new(...)
    # または、MatchData に言及する内部用コメント など

  1. 影響範囲・注意点
  • 公開 API への影響

    • Journey::GTG::MatchDatanodoc な内部クラスであり、Rails の公式公開 API ではありませんでした。
    • したがって、通常の Rails アプリケーションや一般的な gem への影響は基本的にありません
  • 影響があり得るケース

    • Rails の内部実装(Action Dispatch / Journey)に直接依存し、Journey::GTG::MatchData を明示的に参照していたようなコードがもし存在すれば、ロード時に NameError となる可能性があります。
    • ただし、2017 年以降、コンストラクタ呼び出しも含めて実質的に死んだクラスだったため、そのようなコードが存在する可能性はかなり低いと考えられます。
  • マイグレーションの必要性

    • アプリ側でこのクラスを参照していなければ、特別な対応やマイグレーションは不要です。
    • もし独自に Journey の内部クラスを利用している場合は、今後も同様に「内部実装の変更・削除」に備えるべきです。

  1. 参考情報 (あれば)
  • このクラスが入った経緯
    • 2012-12-19 のコミット 56fee39c39 で Journey が Action Pack 配下に移動した際に Journey::GTG::MatchData が導入された。
  • このクラスが実質的に死んだタイミング
    • 2017-05-22 のコミット 845aabbcd3Simulator#simulate と、その中の MatchData.new 呼び出しが削除され、以後はどこからも参照されなくなった。
  • 位置づけ
    • 内部ルーティングエンジンの「デッドコード掃除(リファクタリング)」の一環であり、Rails コアの保守性向上・コードベースの簡素化を目的とした変更です。

#58180 Remove unused SAFE_FETCH_SITES constant

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    このPRでは、ActionController::RequestForgeryProtection 内で定義されていた未使用の SAFE_FETCH_SITES 定数が削除されています。CSRF保護の実装変更により参照されなくなっていた「死んだコード」をクリーンアップするものです。

  1. 変更内容の詳細

対象ファイル:

  • actionpack/lib/action_controller/metal/request_forgery_protection.rb (+0 / -5)

削除されたのは、Sec-Fetch-Site ヘッダをもとに CSRF 判定を行うために導入されていた SAFE_FETCH_SITES という プライベート定数 です。

元々は以下のような形だったと考えられます(イメージ例):

ruby
SAFE_FETCH_SITES = %w[same-origin same-site].freeze
private_constant :SAFE_FETCH_SITES

def valid_request_origin?
  fetch_site = request.get_header("HTTP_SEC_FETCH_SITE")
  # 古い実装イメージ:
  SAFE_FETCH_SITES.include?(fetch_site)
end

しかし、その後の変更(コミット 4a3bf76371)で、SAFE_FETCH_SITES.include? による判定ロジックが、Sec-Fetch-Site の値ごとに明示的な case 分岐などに置き換えられました:

ruby
def valid_request_origin?
  fetch_site = request.get_header("HTTP_SEC_FETCH_SITE")

  case fetch_site
  when "same-origin"
    # 同一オリジンとして許可
  when "same-site"
    # same-site として許可
  else
    # それ以外は拒否
  end
end

このリファクタリング後、SAFE_FETCH_SITES はどこからも参照されなくなっていたため、本PRで定義自体が削除されています。結果として、当該ファイルから5行のみ削除され、挙動を変えるロジックの変更は一切ありません。


  1. 影響範囲・注意点
  • ランタイム挙動への影響はなし
    SAFE_FETCH_SITES はプライベート定数かつ、既にどこからも参照されていなかったため、この削除によりアプリケーションの挙動が変わることはありません。

  • アプリ側コードへの影響も基本的になし
    private_constant とされていたため、通常の利用方法ではアプリケーション側から参照・上書きすることは想定されておらず、依存しているコードが存在する可能性は極めて低いです。

    • もし内部定数を無理に参照するようなメタプログラミング(ActionController::RequestForgeryProtection.const_get("SAFE_FETCH_SITES") など)をしていた場合、そのコードは NameError を起こすようになりますが、これはフレームワークの非公開実装に依存しているため、サポート外と考えるべきです。
  • CSRF保護ロジック自体には変更なし
    既に SAFE_FETCH_SITES.include? を使わない形にロジックが移行済みであり、本PRはその後始末のみです。Sec-Fetch-Site ベースの CSRF 対策の動作仕様は変わりません。


  1. 参考情報 (あれば)
  • SAFE_FETCH_SITES が導入されたコミット: f5a1915f1d
    • 目的: Sec-Fetch-Site ヘッダを用いた CSRF 保護の導入時に、安全とみなすサイト種別のホワイトリストとして導入。
  • SAFE_FETCH_SITES が実質的に不要になったコミット: 4a3bf76371
    • 変更点: SAFE_FETCH_SITES.include? による包括的チェックから、Sec-Fetch-Site ごとの明示的な分岐(same-origin, same-site など)へと判定ロジックを変更。
  • 本PR: #58180
    • 役割: 上記ロジック変更後に残っていた未使用定数の削除によるコードクリーンアップ。

#58183 Remove unused ConnectionHandler#pool_managers method

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    Active Record の ConnectionHandler に定義されていた未使用メソッド pool_managers が削除されました。これは過去の非推奨パス削除後に呼び出し元がなくなっていたヘルパーメソッドの単純なクリーンアップです。

  1. 変更内容の詳細

対象ファイル:

  • activerecord/lib/active_record/connection_adapters/abstract/connection_handler.rb
    • 追加行数: 0
    • 削除行数: 4
    • 削除されたメソッド: ConnectionHandler#pool_managers

元々、この pool_managers メソッドは以下のような位置づけの「ヘルパー」として、

  • コミット 74cb960e66deprecation_for_pool_handling 向けに追加
  • その後、コミット 64cbcd7a8c でその非推奨パス全体が削除

されていました。その結果、「呼び出し元が一切存在しないデッドコード」となっていたため、今回の PR でメソッド本体が丸ごと削除されています。

PR の diff 自体は 4 行削除のみのため、機能追加や挙動変更は一切なく、コードベースから未使用メソッドを取り除くリファクタリングに相当します。


  1. 影響範囲・注意点
  • フレームワーク内部への影響

    • 現在の Rails 本体コードからは pool_managers がまったく呼び出されておらず、挙動に影響はありません。
    • コネクションプールやマルチ DB 等の挙動に変更はありません。
  • アプリケーションコード / プラグインへの影響

    • 公開 API ではなく、内部実装寄りのメソッドですが、もし以下のようなコードを書いている場合は影響を受けます。
      ruby
      # 例: 内部メソッドに依存していた場合
      ActiveRecord::Base.connection_handler.pool_managers
      このような呼び出しは NoMethodError となるため、削除または別の公式 API を使う必要があります。
    • 特に、独自の接続管理・コネクションプール管理を行うライブラリや、Rails 内部 API に依存するモンキーパッチを行っているコードは、一度 pool_managers への依存がないか検索するのが安全です。
  • マイグレーションや設定への影響

    • database.yml の書き方や establish_connection 等、通常の接続設定方法には影響しません。
    • コマンド (rails console, rails db:migrate など) の挙動も変更されません。

  1. 参考情報 (あれば)
  • このメソッドが追加された経緯:
    • コミット 74cb960e66 にて deprecation_for_pool_handling(コネクションプールの扱いに関する非推奨パス)を補助するためのヘルパーとして追加。
  • 削除に至った経緯:
    • コミット 64cbcd7a8c で該当の非推奨パスが削除され、pool_managers の呼び出し元が消滅。
    • 本 PR (#58183) にて未使用コードとして削除。

内部 API に依存しない通常のアプリケーション開発者にとっては、特に対応不要のメンテナンス的変更です。


#58184 Remove unused cross_origin_request? method

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    ActionController::RequestForgeryProtection 内に定義されていた未使用メソッド cross_origin_request? が削除されました。Fetch Metadata を用いた CSRF 対策実装の一部として追加されていたものの、実際にはどこからも呼ばれていなかったためのクリーンアップです。

  1. 変更内容の詳細(あればサンプルコードも含めて)
  • 対象ファイル:
    actionpack/lib/action_controller/metal/request_forgery_protection.rb

  • 行数の変化:

    • 追加: 0 行
    • 削除: 6 行
      ⇒ メソッド定義のみ削除され、他の処理や挙動は変わっていません。
  • 削除された主な内容(イメージ):

    ruby
    # 例: 実際の実装からの概略イメージ
    def cross_origin_request?(request)
      # Fetch Metadata ヘッダ (Sec-Fetch-Site など) を見て
      # same-origin / same-site かどうかを判定するようなロジック
    end

    PRの説明によると、このメソッドは Fetch Metadata ベースの CSRF 対策を導入したコミット f5a1915f1d で追加されたものの、その後の実装ではこのメソッドを経由せず、「検証パス」と「警告パス」がそれぞれ直接同じ条件判定を行っている状態でした。そのためコード上は完全に未使用となっており、今回削除されています。

  • ポイント:

    • メソッド定義のみが削除され、ロジック自体は他の箇所にインラインで存在しているため、処理フローや条件そのものは変わっていません。
    • Fetch Metadata CSRF 機能(protect_from_forgery 周辺の Fetch Metadata オプションなど)の挙動には一切変更なし。

  1. 影響範囲・注意点
  • パブリック API への影響:

    • cross_origin_request? は内部的にのみ使う想定のメソッドで、実際にも呼ばれていなかったため、通常のアプリケーションコードには影響はありません。
    • ただし、Rails 内部クラスをモンキーパッチ/リオープンしてこのメソッドを使っていた場合は影響を受ける可能性があります。
      • 例: ActionController::Base あるいは ActionController::RequestForgeryProtection を開いて cross_origin_request?(request) を呼んでいた場合、NoMethodError となります。
  • 想定される影響範囲:

    • 通常の Rails アプリ: 影響なし。
    • セキュリティまわりをフックする Gem や社内ライブラリ:
      • Fetch Metadata CSRF 対策を拡張・上書きしており、その中で cross_origin_request? を利用していた場合は修正が必要です。
      • とはいえ、元々未使用であり公式に公開された API でもないため、そのような利用はかなりレアと考えられます。
  • 注意点:

    • Fetch Metadata ベースの CSRF/リクエスト検査ロジック自体は残っているため、「CSRF 保護が弱くなる」「挙動が変わる」といった心配は不要です。
    • 今後、Fetch Metadata 用の補助メソッドに依存した実装を書く場合は、今回削除されたような内部メソッドではなく、公開されている公式 API(ドキュメントに記載のある設定・メソッド)に依存する方が安全です。

  1. 参考情報 (あれば)
  • この PR のポイント:

    • 未使用コードの削除によるメンテナンス性向上・内部 API の整理。
    • Fetch Metadata CSRF 実装に関するリファクタリング(機能変更はなし)。
  • 関連しそうな情報:

    • Fetch Metadata ベースの CSRF 保護については、Rails ガイドの CSRF 章や、ActionController::RequestForgeryProtection のコメント・ドキュメントも参照すると、どのようなヘッダ(Sec-Fetch-Site など)を使って検査しているかを把握できます。
    • 最初の導入コミット: f5a1915f1d(PR 説明中で言及)
      → そこを見ると、当初どのように cross_origin_request? が設計されていたかを確認できます。

#58186 Remove unused Redis cache serialize_entries methods

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    Redis のキャッシュストア(RedisCacheStoreDeprecatedRedisCacheStore)に残っていた未使用のヘルパーメソッド serialize_entries が削除されました。実際の挙動には影響せず、不要コードの整理によるリファクタリングです。

  1. 変更内容の詳細

何が削除されたか

以下2ファイルから、未使用メソッド serialize_entries が削除されています。

  • activesupport/lib/active_support/cache/redis_cache_store.rb
  • activesupport/lib/active_support/cache/deprecated_redis_cache_store.rb

削除行数は計 12 行で、追加はありません。

背景と経緯

PR本文の説明を時系列で整理すると:

  1. 以前のコミット f9a8646d81 で、RedisCacheStore#serialize_entries が導入された

    • 用途: Redis への一括書き込み用の mapped_mset 経路で利用するためのヘルパー
    • ざっくりいうと、「複数エントリを Redis にセットする前にまとめてシリアライズする」ためのメソッド
  2. その後のコミット f9fce850da で、mapped_mset による一括書き込みは廃止され、

    • 各エントリごとに pipelined を使って書き込む方式に置き換えられた
    • 結果として serialize_entries の呼び出し元がなくなり、メソッドは「未使用」の状態に
  3. さらにコミット 79b357bd89 で、RedisCacheStore のリネーム時に、この未使用メソッドが DeprecatedRedisCacheStore 側にもコピーされてしまい、

    • 現行実装 (RedisCacheStore) と Deprecated 実装の両方に、誰からも呼ばれない serialize_entries が残り続けていた
  4. 本 PR で、この未使用の serialize_entries が両クラスから削除された、という流れです。

削除されたメソッドのイメージ

PR本文から具体的なコード断片は提示されていませんが、典型的には以下のような形のヘルパーだったと推測されます(イメージであり、実際のコード断片ではありません):

ruby
# 参考イメージ: 実際のコードとは異なる可能性があります
def serialize_entries(entries, options)
  entries.transform_values do |value|
    serialize_entry(value, options)
  end
end

このように、複数エントリを Redis に書き込む前にまとめて変換・シリアライズする役割のヘルパーが、書き込み経路変更により不要になった、という文脈です。


  1. 影響範囲・注意点
  • アプリケーションの挙動への影響は原則なし

    • メソッド自体が完全に未使用だったため、Rails 内部の挙動は変わりません。
    • キャッシュの保存・取得方法、Redis との通信方式(パイプラインでの書き込みなど)には変更ありません。
  • Monkey patch / メソッド呼び出しをしていた場合のみ注意

    • 通常の使用では問題ありませんが、もし以下のような高度な使い方をしていた場合は注意が必要です:
      • ActiveSupport::Cache::RedisCacheStore / DeprecatedRedisCacheStore に対して serialize_entries を monkey patch していた
      • アプリ側から store.send(:serialize_entries, ...) のように private メソッドを直接呼び出していた
    • そのようなコードがある場合、この PR 以降のバージョンでは NoMethodError になります。
  • DeprecatedRedisCacheStore にも同様に適用

    • Deprecated な Redis キャッシュストアにも同じ不要メソッドがあったため、両方から削除されています。
    • 旧実装から新実装への移行を検討している場合でも、この変更が直接影響することは基本的にありません。

  1. 参考情報 (あれば)
  • このPR:

    • タイトル: Remove unused Redis cache serialize_entries methods
    • 番号: #58186
    • 作成者: nvasilevski
    • マージ日時: 2026-07-21T19:06:07Z
  • 関連コミット(説明文中に言及されているもの):

    • f9a8646d81: RedisCacheStore#serialize_entries 導入
    • f9fce850da: mapped_mset 経路を廃止し、エントリごとの pipelined 書き込みに変更
    • 79b357bd89: DeprecatedRedisCacheStore へのリネーム時に未使用メソッドもコピー
  • 位置づけ:

    • これは「デッドコード削除・クリーンアップ」の類いのリファクタリングであり、機能追加や仕様変更ではありません。
    • コードベースの保守性向上が主目的です。

#58147 [8-1-stable] Avoid loading ActionController::Live early in initializer

マージ日: 2026/7/21 | 作成者: @nurey

  1. 概要 (1-2文で)
    Rails 8.1 系で、initializer 内で config.action_dispatch.default_headers = { ... } を丸ごと置き換え設定しても反映されないリグレッションを修正するバックポートです。原因だった ActionController::Live の早期ロードを避けるため、新しい load hook :action_controller_live を導入し、設定が正しいタイミングで反映されるようにしています。

  1. 変更内容の詳細

問題の内容・原因

Rails 8.1.2 / 8.1.3(および 8.0.5+)で以下のような initializer を書いても:

ruby
# config/initializers/default_headers.rb
Rails.application.config.action_dispatch.default_headers = {
  "X-Frame-Options" => "DENY",
  "X-Content-Type-Options" => "nosniff"
}

アプリケーションのレスポンスヘッダには反映されませんでした(「丸ごと置き換え」が無視される)。
動作が壊れているのは 8.1.2 / 8.1.3 / 8.0.5+ で、7.2 / 8.0.0–8.0.4 / 8.1.0–8.1.1 では問題ありません。

原因は、#56393 で追加された initializer action_controller.live_streaming_excluded_keys が、initialize! の過程で ActionController::Live を直接参照してしまうことにありました。

  1. ActionController::Live が参照される
  2. そのロード過程で action_dispatch/http/responserequire される
  3. これにより ActiveSupport.on_load(:action_dispatch_response) のフックが「config/initializers/*.rb」より先に発火
  4. レスポンスのデフォルトヘッダが「initializer 実行前の値」で確定してしまう

結果として、initializer で config.action_dispatch.default_headers = { ... } を「後から丸ごと置き換え」しても、すでにキャプチャ済みの値には反映されず、無視されたように見える、という状態になっていました。


今回の修正内容

この PR は、main ブランチに入っている修正コミット f9c82c3f2a(およびテスト)を 8-1-stable にそのままバックポートしたものです。

ポイントは:

  1. ActionController::Live を initialization 中にロードしないようにした
  2. そのために 新しい load hook :action_controller_live を導入 した
  3. 問題のリグレッションを検出する 回帰テストを追加 した

1. ActionController::Live のロードタイミングを遅延させる

もともと action_controller.live_streaming_excluded_keys の initializer 内で、ActionController::Live を直接参照していましたが、これを以下のように ActiveSupport.on_load(:action_controller_live) でラップする形に変更しています(イメージ):

ruby
ActiveSupport.on_load(:action_controller_live) do
  # ここで Live 関連の設定・除外キーなどを行う
end

この変更により:

  • ActionController::Live を使うまでは ActionDispatch::Response がロードされない
  • よって :action_dispatch_response の load hook も、config/initializers/*.rb 実行後まで遅延される
  • その結果、initializer で上書きした config.action_dispatch.default_headers が「最終値」として正しくフックから参照される

という流れになります。

2. 新しい load hook :action_controller_live の導入

ActionController::Live に紐づく処理を遅延させるために、ActiveSupport.on_load 用の新しいフックシンボル :action_controller_live が導入されています。

これに伴い:

  • actionpack/lib/action_controller/metal/live.rb
  • actionpack/lib/action_controller/railtie.rb
  • guides/source/configuring.md

などに、action_controller_live フックに関する定義・ドキュメントが追加されています。

今後は、ActionController::Live に依存する設定やモンキーパッチを、このフックを使って遅延実行することもできます(例):

ruby
ActiveSupport.on_load(:action_controller_live) do
  # Live を include した後にしかできない設定など
end

3. 回帰テストの追加

railties/test/application/configuration_test.rb に以下のようなテストが追加されています(要旨):

  • テスト名:
    test_config.action_dispatch.default_headers_can_be_set_in_an_initializer_and_is_applied_to_responses
  • 内容:
    • initializer で config.action_dispatch.default_headers を設定
    • 実際のレスポンスヘッダに、設定した値が反映されていることを検証

このテストが:

bash
cd railties
bin/test test/application/configuration_test.rb \
  -i "test_config.action_dispatch.default_headers_can_be_set_in_an_initializer_and_is_applied_to_responses"

で、修正前の 8-1-stable では落ち、今回の修正後には通ることが確認されています。


  1. 影響範囲・注意点
  • 影響を受けるバージョン

    • リグレッションのあるバージョン: 8.1.2, 8.1.3, 8.0.5+(PR 説明より)
    • 本 PR は 8-1-stable 向けバックポートなので、8.1.x 系の次のパッチリリースに入る想定です。
  • アプリケーション側での挙動の変化

    • config/initializers/ 内で
      config.action_dispatch.default_headers = { ... }
      のように「ハッシュごと置き換え」しているアプリで、設定が正しくレスポンスに反映されるようになります。
    • 7.2 や 8.0.0–8.0.4 / 8.1.0–8.1.1 と同じ挙動に戻るだけなので、「仕様変更」というよりリグレッション修正です。
  • ActionController::Live を使っているアプリへの影響

    • ActionController::Live やストリーミングを普通に使っているだけなら、挙動上の差はほぼありません。
    • ただし、独自に ActionController::Live のロードタイミングに依存していたり、on_load(:action_dispatch_response) と絡めて高度な初期化順序に頼っていたコードがある場合、ロード順が微妙に変わる可能性はあります。
      • そのような高度な使い方をしているケースは稀と思われますが、もし心当たりがあれば、8.1 系パッチ適用後に念のため確認すると安心です。
  • 新しい load hook を stable ブランチに入れることについて

    • PR 説明にもある通り、:action_controller_live は「新しく公開される load hook」であり、本来なら minor 以降で入れるのが綺麗です。
    • ただし、今回は main ブランチの修正をほぼそのままバックポートして divergence を減らす意図があり、stable にも入れている、という判断になっています。
    • 将来、ガイドや他の gem がこのフックを前提とする可能性があるため、「8.1 でも :action_controller_live が使える」という点は、gem 作者にとってはポジティブな影響です。

  1. 参考情報 (あれば)
  • 元 issue: #58145 — リグレッションの詳細な解析

  • 元 PR: #56393 — action_controller.live_streaming_excluded_keys 追加の変更

  • 本 PR の元になった main 側の修正: コミット f9c82c3f2a「Follow up to #56393」

  • regression test (main 側): #58146

  • 動作確認コマンド(PR 説明より再掲):

    bash
    cd railties
    bin/test test/application/configuration_test.rb \
      -i "test_config.action_dispatch.default_headers_can_be_set_in_an_initializer_and_is_applied_to_responses"

#58175 Remove unused EMPTY_BINARY_STRING constant

マージ日: 2026/7/21 | 作成者: @nvasilevski

  1. 概要 (1-2文で)
    Rails の ActiveSupport::Cache コーダ関連コードから、どこからも参照されていなかった定数 EMPTY_BINARY_STRING が削除されました。機能的な挙動の変更はなく、完全にデッドコードのクリーンアップです。

  1. 変更内容の詳細
  • 対象ファイル:
    activesupport/lib/active_support/cache/coder.rb

  • 変更点:
    EMPTY_BINARY_STRING という定数定義が 1 行だけ削除されています。
    元々は「キャッシュのデシリアライズエラー処理を改善する」変更 (コミット d51ad367e2, 2026-02-04) の一部として追加された定数ですが、そのタイミングでも実際には使用されておらず、その後も使われることがなかったため、今回削除されています。

イメージとしては以下のような定義が削除された形です(正確な内容は PR から省略されていますが、ニュアンスとして):

ruby
# 削除されたイメージ
# EMPTY_BINARY_STRING = +""

Rubydex(コード参照解析ツール)とリポジトリ全体のコード検索により、この定数が一度も読み出されていないことが確認された上で削除されています。


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

    • Rails 本体の挙動には影響ありません。
    • この定数は内部でも外部でも使用されていなかったことが確認済みのため、通常のアプリケーションコード・gem に対する実行時挙動の変化はありません。
  • 注意点:

    • 公開 API ではない内部定数ではありますが、もしアプリケーション側や独自 gem が「たまたま」ActiveSupport::Cache::Coder 内の EMPTY_BINARY_STRING を直接参照していた場合、その参照は NameError になります。
      • 例:
        ruby
        ActiveSupport::Cache::Coder::EMPTY_BINARY_STRING # => NameError (削除後)
    • そのような使い方は本来想定されていないため、基本的には互換性問題とはみなされない類の変更です。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58175
  • 当該定数が追加されたコミット: d51ad367e2(2026-02-04、「キャッシュのデシリアライズエラー処理の改善」の一環)
  • 種別としては「内部実装のクリーンアップ / デッドコード削除」に該当し、アップグレード時に特別な対応は不要です。

#58171 Fix bundle install on ruby-dev

マージ日: 2026/7/21 | 作成者: @Earlopain

  1. 概要 (1-2文で)
    ruby-dev(Ruby 開発版)で bundle install が失敗していた問題を、利用していた gem を更新して解消した PRです。Rails 本体のコードは変更せず、Gemfile.lock の依存バージョンのみを修正しています。

  1. 変更内容の詳細
  • 変更ファイルは Gemfile.lock のみで、6行追加・6行削除(= 一部依存 gem のバージョン入れ替え)です。
  • 背景として、「deprecated(非推奨)だった Ruby の API が ruby-dev で削除され、その API を使っていた gem が動かなくなった」ため、bundle install が失敗していました。
  • この PR では、その削除済み API に依存していた gem を、非推奨 API を使わない新しいバージョンに更新することで、ruby-dev 上でも bundle install が通るようにしています。

コードとしての変更は Rails アプリ(フレームワーク)の Ruby ファイルにはなく、あくまでロックファイルのバージョン調整です。
(PR情報からは、どの gem がどのバージョンに上がったかの詳細までは読み取れませんが、典型的には rubocop / listen / bootsnap / sqlite3 など、C拡張や Ruby 内部 API に触れる gem が対象になりやすいです。)


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

    • Rails 本体の挙動や API には変更なし。
    • 開発・CI で ruby-dev(Ruby の HEAD / trunk)を使っている環境に対して、bundle install が成功するようになります。
    • 通常の安定版 Ruby(3.2 / 3.3 など)を使っている場合は、依存 gem のマイナーなバージョンアップによる挙動差が発生しうる程度です。
  • 注意点

    • テスト追加は行われていないため、「ruby-dev + この Gemfile.lock」という組み合わせでのバンドル成功を、主に手動確認に頼っている状態です。
    • Gem の更新により、間接的に:
      • Ruby バージョン要件が厳しくなっていないか
      • 開発ツール系(lint, test helper など)の挙動に変化がないか
        を確認する価値があります。
    • Gemfile 自体は変えていないので、ローカルで bundle update などを行うと、別の解決パターンになり得ます。Rails 本体の開発に参加する場合は、この PR マージ後の Gemfile.lock を基準にするのが安全です。

  1. 参考情報 (あれば)
  • この種の PR は「Ruby 本体の trunk/HEAD で非推奨 API が削除された時」に定期的に発生し、Rails リポジトリでは主に以下を目的に行われます:
    • Ruby の将来バージョンに対する事前互換性確保
    • CI(特に Ruby HEAD を回しているジョブ)の安定運用
  • 自分のプロジェクトで同様の問題が出た場合の対処の基本パターン:
    1. ruby-devruby-head で失敗している gem を特定(bundle install のログを見る)。
    2. その gem の changelog / issue tracker を確認し、Ruby HEAD 対応版へ更新。
    3. 必要であれば bundle update <gem名>Gemfile.lock をコミット。

#58163 [ci-skip][doc] Fix inconsistent links in Active Job guide

マージ日: 2026/7/21 | 作成者: @hachi8833

  1. 概要 (1-2文で)
    Active Jobガイド内のリンク表記ゆれを修正し、ドキュメント内のリンクスタイルを統一したPRです。コードや挙動の変更はなく、ドキュメントのみへの軽微な修正です。

  2. 変更内容の詳細

  • 対象ファイル: guides/source/active_job_basics.md
  • 変更内容:
    • Active Jobガイド内にあるリンクの一部で、Markdownのリンク表記に使う角カッコ [] が不足しており、他の箇所と形式が不一致だった部分を修正

    • 実質的には、以下のような「リンクの括弧漏れ」を直していると考えられます(例・イメージ):

      md
      # 修正前(例)
      See Active Support Instrumentation(https://guides.rubyonrails.org/active_support_instrumentation.html) for more details.
      
      # 修正後(例)
      See [Active Support Instrumentation](https://guides.rubyonrails.org/active_support_instrumentation.html) for more details.
    • 追加行数2・削除行数2のため、2箇所程度で [] の付け忘れや、他と異なるリンク表現を統一した変更とみられます。

  1. 影響範囲・注意点
  • 影響範囲:
    • Rails本体のコード、API挙動、Active Jobの仕様・インターフェースには一切影響しません。
    • 影響するのは、Rails Guides(Active Job Basics)の表示のみです。
    • ガイドをHTMLにビルドした際、該当箇所が正しくリンクとしてレンダリングされるようになります。
  • 注意点:
    • ドキュメント向けの変更なので、アプリケーション側で対応すべきことはありません。
    • ドキュメントを翻訳・派生させているプロジェクトでは、同様のリンク表記ゆれがないか確認しておくと良いです。
  1. 参考情報 (あれば)
  • PRタイトルの [ci-skip] から、CIを走らせる必要がないドキュメント専用の変更として扱われていることがわかります。
  • Rails GuidesのMarkdownでは、基本的に標準的なMarkdownリンク構文([テキスト](URL))で統一されているため、今後ガイドにPRを出す際もこのスタイルに合わせるのが望ましいです。

#58166 Let dump_schema_migrations_sort_by be :reverse by default

マージ日: 2026/7/20 | 作成者: @fxn

  1. 概要 (1-2文で)
    db/schema.rbschema_migrations テーブルをダンプする機能について、デフォルトの並び順が「辞書順(昇順)」から「逆順(:reverse、最新のマイグレーションが先)」に変更されました。これにより、この機能を有効化したときに、マージコンフリクトを避けやすい並び順が最初から採用されます。

  1. 変更内容の詳細

背景

  • #58134 で schema_migrationsdb/schema.rb に出力できるようになった際、デフォルトの並び順は「lexicographic(文字列昇順)」でした。
  • しかし、PR 作成者自身が「painful」と表現していたように、実際に使うとマージコンフリクトの原因になりやすく、実用上望ましくないと判断され、今回デフォルトを変更しています。

デフォルト値の変更

  • dump_schema_migrations_sort_by という設定のデフォルトが以下のように変更されています:

    ruby
    # 以前 (#58134 時点)
    config.active_record.dump_schema_migrations_sort_by = :lexicographic # または nil 扱いで昇順
    
    # 今回の PR 後
    config.active_record.dump_schema_migrations_sort_by = :reverse

    実際のコード上はシンボル :reverse をデフォルトとして使うように変更されており、
    「最新のマイグレーションが先頭、古いものが後ろ」という逆順で schema_migrationsdb/schema.rb に出力されるようになります。

ドキュメントの更新

以下のガイドが更新されています:

  • guides/source/active_record_migrations.md

    • schema_migrations を schema.rb にダンプするオプションと、その並び順の説明が更新。
    • デフォルトが :reverse であること、必要なら線形(昇順)にもできることが明記されていると考えられます。
  • guides/source/configuring.md

    • config.active_record.dump_schema_migrations および
      config.active_record.dump_schema_migrations_sort_by の説明が更新。

    • 例として、以下のような設定がガイドに載っているはずです:

      ruby
      # schema_migrations を schema.rb に書き出す
      config.active_record.dump_schema_migrations = true
      
      # デフォルト(今回の PR で :reverse になった)
      # 最新のマイグレーションが先頭に来る
      # config.active_record.dump_schema_migrations_sort_by = :reverse
      
      # 昇順の線形な並びにしたい場合(例)
      config.active_record.dump_schema_migrations_sort_by = :lexicographic
      # または、実装によっては :linear, :ascending 等の別名があるかもしれません
  • activerecord/CHANGELOG.md

    • 元々書かれていた「デフォルトが昇順である」という記述が修正され、 デフォルトが :reverse であることと、その意図が追記されています。

テストの更新

  • activerecord/test/cases/schema_dumper_test.rb

    • schema_migrations のダンプ結果に対するアサーションが、「昇順前提」から「逆順前提」に書き換えられています。
    • 例えば、最後のマイグレーションの ID が先頭付近に存在することをチェックするような形に変わっています。
  • railties/test/application/configuration_test.rb

    • アプリケーション設定 (config.active_record.dump_schema_migrations_sort_by) のデフォルト値が :reverse であることを前提としたテストに修正されています。

  1. 影響範囲・注意点

影響範囲

  • 対象となるのは schema_migrationsdb/schema.rb にダンプする設定を有効にしたアプリ」 のみです。
    • デフォルトでこの機能は オフ なので、多くの既存アプリには影響しません。
  • すでに #58134 以降の Rails を使っていて、
    • config.active_record.dump_schema_migrations = true を設定し、
    • さらに「デフォルトの並び順(昇順)である」という前提で何らかの処理やレビュー運用をしていた場合、
      その前提が変わります。

実務的な注意点

  • これから dump_schema_migrations を有効にする場合:

    • 何も設定しなければ :reverse になります。
      → マージコンフリクトを避ける、履歴を追いやすいといった、実用上望ましい挙動がそのまま得られます。
  • 線形(古いものから新しいものへ昇順)で並べたい場合:

    • 明示的に設定が必要です:

      ruby
      config.active_record.dump_schema_migrations_sort_by = :lexicographic
      # またはガイドで示されている昇順用のシンボル
  • CI やツールで db/schema.rbschema_migrations 部分をパースしている場合:

    • 「常に昇順である」ことを前提にしたロジックがあるなら、逆順でも動くようにしておく必要があります。
    • ただし、そもそもこの機能は opt-in であり、かつ新しめのバージョンの変更なので、
      そのような前提を置いているツールは多くないと考えられます。

  1. 参考情報 (あれば)
  • この PR (#58166) が前提としている機能追加 PR:
    • #58134schema_migrationsdb/schema.rb にダンプする機能の追加。
  • 関連設定項目(概念整理):
    • config.active_record.dump_schema_migrations
      • trueschema_migrationsdb/schema.rb に書き出すようになる。
    • config.active_record.dump_schema_migrations_sort_by
      • :reverse(今回の PR でのデフォルト): ID を逆順に並べる(新しいマイグレーションが先)。
      • 昇順で線形な履歴が欲しい場合は、ガイド記載のシンボル(例: :lexicographic)を明示的に指定。

#58157 Assert @verified directly in the non-StandardError interrupt test

マージ日: 2026/7/19 | 作成者: @yahonda

  1. 概要 (1-2文で)
    Rails 8-0-stable で落ちていた ActiveRecord::AdapterConnectionTest のテストを、verified? メソッドではなくインスタンス変数 @verified を直接検査するように変更して修正した PR です。テストの意図(「非 StandardError 系の割り込みが発生すると、接続が再検証対象になること」)は維持されたままです。

  1. 変更内容の詳細

何が問題だったか

対象テスト:

ruby
ActiveRecord::AdapterConnectionTest
  #test_a_non-StandardError_interrupt_marks_the_connection_for_re-verification

8-0-stable ブランチでは、以下のような失敗が deterministically(毎回確実に)起きていました:

Expected #&lt;ActiveRecord::ConnectionAdapters::Mysql2Adapter:...> to respond to #verified?.

原因は以下の通りです。

  • このテストはバックポート PR #57953 によって 8-0-stable に入った
  • テスト内では assert_not_predicate @connection, :verified?(=refute_predicate @connection, :verified?)を使っていた
  • しかし 8-0-stable の ConnectionAdapters には:
    • インスタンス変数 @verified
    • それを true にする verified! メソッド
      はあるものの、
    • verified? リーダーメソッドが存在しない
  • さらに、minitest 6.0.4 以降では refute_predicate が事前に respond_to? をチェックするようになったため、
    • メソッド呼び出し前に「verified? が存在しない」という assertion エラーになってしまう
      (以前の minitest だと NoMethodError で落ちるだけだが、いずれにせよテストは通らない)

一方、main ブランチでは以下の変更が入っていて問題にならない:

  • コミット 28638775db ("Preconnect connections that are idle in the pool")
    • verified? リーダーを追加
    • テストが refute_predicate @connection, :verified? を呼んでも正常に動作

このコミット自体は 8-0-stable に含まれていないため、テストだけバックポートされた結果、verified? が存在しない環境で verified? を要求するテストになっていた、という状態でした。

具体的な修正

テストコードの該当部分を、verified? メソッド呼び出しからインスタンス変数の直接参照に変更しています。

(イメージとしては以下のような 1 行の差分です)

diff
-    assert_not_predicate @connection, :verified?
+    assert_not @connection.instance_variable_get(:@verified)

※ 実際のコードは adapter_test.rb 内の既存スタイルに合わせて書かれており、すでに次の行で同様に @last_activityinstance_variable_get で読んでいるため、そのパターンに揃えています。

ポイント:

  • main ブランチでは verified? は単なる @verified のリーダーであり、ロジックはなく「@verified を返すだけ」のメソッド
  • したがって、このテストを「verified? の代わりに @verified を直接見に行く」ように変えても、テストが検証している条件(@verified が false / nil であること)は変わらない
  • 8-0-stable 側に verified? メソッドだけをバックポート / 抽出するのではなく、テスト側を微修正して整合を取る方針を採用している

動作確認

PR 説明によると、ローカルで以下を確認済み:

  • 修正前:
    • ARCONN=sqlite3 bin/test test/cases/adapter_test.rb:649 が上記と同じ失敗内容で再現
  • 修正後:
    • 同テストが sqlite3 / mysql2 でパス
    • AdapterConnectionTest 全体(mysql2)も 28 テスト・0 失敗でパス
  • main7-2-stable はそもそもこの問題の影響を受けていない

  1. 影響範囲・注意点
  • 影響範囲:
    • 変更ファイルは activerecord/test/cases/adapter_test.rb の 1 行のみ
    • 本番コード(ライブラリ本体)ではなくテストコードのみの変更
    • 対象は 8-0-stable の ActiveRecord AdapterConnectionTest に限定される
  • 実質的な挙動:
    • 接続の検証フラグ @verified の扱い自体は一切変わらない
    • 既存コードに対する互換性への影響はなし
    • CI でのテスト失敗を解消する目的のテスト修正
  • 注意点:
    • 将来 8-0-stable に 28638775db(もしくは verified? リーダーだけ)をバックポートする場合、
      • テストがインスタンス変数直読のままで問題はないが、
      • 「API として verified? を前提にするかどうか」を整理する余地はある
    • テストがプライベート実装(インスタンス変数名)に依存しているため、
      • @verified の名前変更や構造変更を行う場合にはテストも同時に追従させる必要がある

  1. 参考情報 (あれば)
  • 該当テスト:
    activerecord/test/cases/adapter_test.rb
    test_a_non-StandardError_interrupt_marks_the_connection_for_re-verification
  • 関連するコミット / PR:
    • verified? を導入したコミット: 28638775db
      "Preconnect connections that are idle in the pool"
    • テストを 8-0-stable に持ってきたバックポート PR: #57953 (9241586205)
    • 本 PR: #58157
  • minitest の変更:
    • 6.0.4 以降で refute_predicate / assert_predicaterespond_to? を事前チェックするようになった変更:
      minitest/minitest@b12f87f4f8

#58158 Fix RDoc markup in ActiveSupport::ContinuousIntegration [ci skip]

マージ日: 2026/7/19 | 作成者: @Yuhi-Sato

  1. 概要 (1-2文で)
    ActiveSupport::ContinuousIntegration のクラスドキュメント内で、コード(等幅)表現に使われていた RDoc マークアップを修正し、API ドキュメント上で正しく固定幅表示されるようにした PRです。コードや挙動には一切変更はなく、ドキュメント表現のみの修正です。

  1. 変更内容の詳細

修正対象

対象は activesupport/lib/active_support/continuous_integration.rb 内の RDoc コメントです。
RDoc 6 では、バッククォート(`...`)が「コード表現」として扱われず、スマートクォート(‘…’)に変換されてしまうため、Rails の API ガイドラインに従い、コード(等幅)表現を +...+ 形式に変更しています。

イメージとしては以下のような変更です(実際の内容に近い例):

diff
- # Run `bin/ci` to execute the CI suite locally.
+ # Run +bin/ci+ to execute the CI suite locally.

この修正により、以下のようなページでの表示が改善されます:

従来は “bin/ci” などが通常の引用風(スマートクォート付き)に見えてしまっていたものが、今後はきちんと等幅フォントのコードとして表示されます。


  1. 影響範囲・注意点
  • コードロジックや挙動には一切変更がなく、ランタイムの動作には影響しません。
  • 影響範囲は ActiveSupport::ContinuousIntegration クラスの API ドキュメント表示のみです。
  • RDoc 6 系環境で Rails API ドキュメントを生成している場合にのみ意味を持つ変更で、既存のアプリケーションコードには影響しません。
  • CI スキップ指定([ci skip])がタイトルに入っている通り、テストは実行せず、純粋なドキュメント修正として扱われています。

  1. 参考情報 (あれば)

#58036 Fix typos and inconsistencies in documentation

マージ日: 2026/7/18 | 作成者: @55728

  1. 概要 (1-2文で)
    このPRは、Rails本体およびガイド・APIドキュメント・コメントに存在していた typo(スペルミス)や、サンプルコード/説明文の不整合をまとめて修正するものです。実行時の挙動には変更を加えず、ドキュメントとコメントの品質・信頼性を高めています。

  1. 変更内容の詳細

全体方針

  • 対象:
    • Guides(Getting Started, Product Reviews など)
    • APIドキュメント(RDocコメント)
    • コードコメント
    • 一部テストの説明文
  • 除外:
    • CHANGELOG は変更対象外(履歴の正確性維持のため)
  • 種類:
    • 明確なスペルミスの修正のみ
    • 文意レベルの書き換え・言い回し変更等はあえて行っていない
  • CI:
    • 実行時コードのロジックには手を入れていないため [ci skip] でテストスキップ

Guides / ドキュメントの修正

1) Wishlists guide

  • Getting Started Guide へのリンクの後に、閉じられていない括弧があったのを修正。
    • 例: ... link to Getting Started Guide ( → 対応する ) を追加して文章を正しく構成。

2) Product Reviews guide

主に、サンプルと説明の不整合・明らかなtypoの修正です。

  • db:migrate のサンプル出力とマイグレーションの差異を修正

    • 問題: マイグレーションでは default: 0 を付けているのに、直後に掲載されている db:migrate の出力例から default: 0 が抜けていた。
    • 対応: サンプル出力に default: 0 を追加し、上のマイグレーションとの整合性をとった。
  • 誤字修正(文法・スペル)

    • "We need to a way to view""We need a way to view"
    • "calcuating""calculating"
    • 主語・動詞の一致:
      • "these hidden fields … also ensures""these hidden fields … also ensure"
      • "this fields""these fields"
  • コードスニペットの状態不整合

    • 問題: Filtering セクションにある review.rb の「現在の状態」スニペットから、「rating の数値妥当性バリデーション(numericality validation)」が抜けていた。

      • これはガイド前後の説明や別のスニペットでは存在しており、ガイド内で矛盾していた。
    • 対応: 該当の Review モデルの例に数値バリデーションを復元して、ガイド全体で一貫した状態にした。
      (イメージ)

      ruby
      class Review < ApplicationRecord
        validates :rating, numericality: { only_integer: true, in: 1..5 }
        # ... ほかのバリデーションや関連付け ...
      end
  • ファイルパスの誤記修正

    • 問題: テストのパスの説明文が test/integrations/reviews_test.rb となっていたが、実際の generator 出力とコマンドは test/integration/ を使用している。
    • 対応: 説明文を test/integration/reviews_test.rb に修正し、generator出力と一致させた。

3) その他ガイド

  • getting_started.md, maintenance_policy.md, sign_up_and_settings.md, upgrading_ruby_on_rails.md などで、軽微な typo を修正(例: 単語のスペル・小文字/大文字の誤りなど)。

ライブラリコード内のコメント・RDocの修正

Railtie 関連

  • 重複した冠詞の削除:
    • "the a lazy loaded""a lazy-loaded"
    • 「the a」と二重になっていたのを修正。
    • 実コードではなくコメント/説明文の修正。

ProxyLogger (ActiveSupport::ProxyLogger)

  • クラスコメント内のスペルミス:
    • "surpress""suppress"
  • ログを「抑制する」説明文なので、辞書的に正しいスペルに統一。

ActionText::Attachment #to_plain_text の RDoc 例

  • 問題:
    • RDoc のサンプルで定義されている attachable_plain_text_representation が引数なし:

      ruby
      def attachable_plain_text_representation
        # ...
      end
    • 一方、同じサンプル内での呼び出しは attachable_plain_text_representation(caption) のように引数付きで呼んでいて、これをコピペしたユーザは ArgumentError(引数の数の不一致)に遭遇する。

  • 対応:
    • RDoc のメソッド定義例を引数付きに変更し、ランタイムで実際に行われる呼び出しと一致させた:

      ruby
      def attachable_plain_text_representation(caption)
        # ...
      end
    • これは #to_markdown のオーバーライド例と同様のインタフェースになるようにそろえた、という説明がなされている。


「スペルミス一掃 (Spelling sweep)」

codespell を使って、ガイド/APIドキュメント/コメント全体にわたる綴りの誤りを一括で修正しています。

  • 具体例(before → after):

    • charcacterscharacters
    • backlash (char) → backslash
    • OauthOAuth
    • IlliadIliad
    • derivatedderived
    • striped (indentation の意味で) → stripped
    • unverfiedunverified
    • aviseadvise
    • thesesthese
    • intsideinside
  • 使用コマンド:

    sh
    codespell --builtin clear,rare,code --skip='*/CHANGELOG.md'
    • --builtin clear,rare,code により、一般的なミス・あまり使われない誤綴り・コード中想定の誤綴りなどを検出。
    • CHANGELOG は --skip で除外。
  • ポリシー:

    • 動作に関係する識別子や固有名詞であっても、客観的に誤りと判定できるもの(例: OauthOAuth, IlliadIliad)は修正。
    • 文体・語順など主観が入りうる変更は意図的に避け、「辞書上誤りと分かる箇所」に限定。

  1. 影響範囲・注意点
  • 実行時挙動:

    • ランタイムのコードロジックには一切変更なし。
    • 変更対象はコメント、ドキュメント、テスト説明文、およびドキュメント用のサンプルコードのみ。
  • 開発者への影響:

    • ガイドや RDoc を参考にする際、より正確な文章・サンプルコードになっている。
    • とくに ActionText::Attachment#to_plain_text の RDocをコピペして使っていた/これから使う場合、引数の不一致による ArgumentError が起こりにくくなる。
    • Product Reviews ガイドのマイグレーション出力例とテストパスの修正により、チュートリアルどおりに手を動かした際に「自分だけ挙動が違う」といった混乱が減る。
  • 互換性:

    • 公開APIのシグネチャや挙動に変更はないため、アップグレード時の互換性リスクはゼロに近いです。
    • CI設定・設定ファイルなども未変更。

  1. 参考情報 (あれば)
  • このPR自体は、既存の typo cleanup の継続として位置づけられており、過去の類似PR:

    • (説明中の文言として)This continues the typo cleanup in #58036 and folds in #58127.
      → タイポ修正作業を段階的に進めている一環。
  • 使用ツール:

  • 実務的には、RailsガイドやRDocをチーム内の教育・ドキュメントとして利用している場合、このPRの反映後のバージョンを前提にサンプルコードを参照すると、混乱やコピペミスが減らせます。


#58154 Pin json below 2.20 in the Gemfile

マージ日: 2026/7/18 | 作成者: @yahonda

  1. 概要 (1–2文で)
    Rails 7.2 系ブランチの開発・CI環境において、json gem 2.20.0 以降で導入された「コメント付き JSON の非推奨警告」が strict warnings CI を落としてしまう問題に対応するため、json< 2.20 にピン留めした PR です。アプリケーション側の依存バージョンには影響せず、Rails 本体の開発・テスト環境専用の制約です。

  1. 変更内容の詳細

変更点

Gemfile:

ruby
# 変更前(イメージ)
gem "json"

# 変更後
gem "json", "< 2.20"

それに伴い Gemfile.lock の依存情報も更新されています(json の実際の固定バージョンは lock ファイル側で管理される)。

背景となる技術的事情

  • json 2.20.0 での変更

    • JSON ドキュメント内のコメント(///* ... */ など)のサポートが非推奨になり、コメントを含む JSON をパースすると deprecation warning が出るようになった。
    • Rails 7.2 ステーブルブランチの CI では「警告をエラー扱い(strict warnings)」にしているため、この deprecation warning によって CI が落ちる。
  • main ブランチでの対応 (#57832)

    • JSON.loadallow_comments: true を渡すようにし、json 2.20.0 の警告を抑制する対応が行われた。
    • これを 7-2-stable にも一度 backport したが、7.2 系では ActiveSupport::JSON.decode が引数 1 つしか受け取らない実装のため、allow_comments: オプション付きで _load 経由で呼ばれるコードが ArgumentError を起こし、JSON メッセージシリアライザが壊れることが判明しリバートされた。
  • メンテナンスフェーズの制約

    • 公式のメンテナンスポリシー上、7.2.x はすでにバグフィックスフェーズを終了しており、セキュリティ修正のみ許容される状態。
    • そのため、7.2 系に大きめの挙動変更(ActiveSupport::JSON 周辺のシグネチャ変更など)を持ち込むのは避けたい。
    • 結果として、gem のバージョンを Gemfile でピン留めすることで CI 問題のみを解消する、という最小限の対処になっている。

サンプルイメージ(概念的なコード)

main ブランチ側で行われた対応はざっくりいうと:

ruby
JSON.load(json_string, allow_comments: true)

のようにフラグを渡していましたが、7.2 ではこのようなオプションを安全にパススルーできる API になっておらず、その backport は破壊的になってしまうため採用できませんでした。その代わりに「問題のある json バージョン自体を使わない」というアプローチを取っています。


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

    • 対象: Rails 7-2-stable ブランチの Rails 本体リポジトリ の開発・CI環境のみ。
    • Rails を利用するアプリケーション側の Gemfile にはこの制約は伝播しない。アプリは引き続き json 2.20.0 以降を使う構成も可能。
    • CI 上で json 2.20.0 以降をインストールしようとしていたケースでも、< 2.20 制約により 2.19.x 以下が選ばれ、deprecation warning による CI 失敗が防がれる。
  • 注意点 / 開発者視点での留意事項

    • Rails 7.2 のバグ修正サポートは終了しているため、「json 2.20.0 以降でのコメント非推奨挙動に 7.2 が公式対応する」ことは今後も期待しづらい。
    • Rails 7.2 を使うアプリ側で json 2.20.0 以降を利用していて、コメント付き JSON を扱っている場合は、アプリケーション自身で
      • コメントを含まない形式への移行
      • もしくは JSON.load(..., allow_comments: true) のような対応(※アプリ側のコードで可能な範囲で) を検討する必要がある。
    • Rails 本体の開発に参加する場合、7.2 ブランチで bundle update json などを実施すると、この < 2.20 制約があることに注意する必要がある。

  1. 参考情報 (あれば)
  • Rails メンテナンスポリシー:
    https://rubyonrails.org/maintenance
  • 関連 PR:
    • main ブランチでの対応: #57832
    • 同内容の 8-0-stable 向け PR: #58153
  • json gem の 2.20.0 におけるコメント非推奨化については、json gem の CHANGELOG / リリースノートを参照すると詳細な仕様変更が確認できます。

#58153 Pin json below 2.20 in the Gemfile

マージ日: 2026/7/18 | 作成者: @yahonda

  1. 概要 (1-2文で)
    Rails 8.0 系ブランチで、json gem を 2.20 未満に固定する変更です。json 2.20.0 でコメント付き JSON に対する非推奨警告が出て CI の strict warnings を落としてしまう問題を、Gemfile 側でバージョンをピン止めすることで回避しています。

  1. 変更内容の詳細

背景となる問題

  • json 2.20.0 から、JSON ドキュメント内のコメントが非推奨となり、警告が表示されるようになった。
  • Rails の CI(特に strict warnings モード)では、この非推奨警告が出るとテストが失敗する。
  • main ブランチでは PR #57832 によって、allow_comments: true を渡す形でこの問題に対応済み。
    • しかし 8.0 系では ActiveSupport::JSON.decode が引数 1 つしか取らないため、そのまま _loadallow_comments: を渡すバックポートは ArgumentError を引き起こし、JSON メッセージシリアライザを壊してしまうことが判明し、一度マージ済みの修正がリバートされている。

加えて、Rails 公式のメンテナンスポリシー上、8.0.x はすでにバグフィックスサポートが終了し、セキュリティフィックスのみのフェーズに入っているため、8.0 ブランチに対して大きめの挙動変更を行うのは避けたい、という前提がある。

実際の変更内容

Gemfile の json の指定を以下のように変更しています(イメージ):

ruby
# 変更前(例)
gem "json"

# 変更後(例)
gem "json", "< 2.20"

それに合わせて Gemfile.lock も更新され、8.0 ブランチでの開発および CI においては json 2.20.0 以上を使わないように制約を追加しています。

コードレベルのロジック変更(ActiveSupport::JSON.decode など)の修正は行わず、純粋に依存バージョン制約だけで対処している点がポイントです。


  1. 影響範囲・注意点
  • 影響範囲
    • Rails 本体の 開発環境 および CI に限定される変更です。
    • Rails 8.0.x を利用している アプリケーション側の Gemfile / Gemfile.lock には一切制約を追加しません
      • つまり、アプリケーションは引き続き json 2.20.0 以降を含めて自由にバージョン指定可能です。
  • なぜアプリは影響を受けないのか
    • Rails リポジトリ内の Gemfile は「Railsを開発・テストするための依存関係」を管理するものであり、Rails を利用するアプリケーションの Gemfile とは別物のためです。
  • 開発者としての注意点
    • Rails 8.0 ブランチで CI を走らせる際、json のバージョンは < 2.20 に固定されます。json 2.20 の挙動(コメント非推奨など)を 8.0 ブランチの CI 上でテストしたい場合は、そのままではできません。
    • 将来的に 8.0 系をフォークして独自にメンテナンスする場合、json の仕様変更(コメント非推奨)に対応したいなら:
      • ActiveSupport::JSON.decode の API 変更(2引数化など)を含む修正を入れる、
      • または JSON 側のバージョンピンを外した上で警告を抑制する別の方法を検討する、 などの判断が必要になります。

  1. 参考情報 (あれば)
  • Rails メンテナンスポリシー: https://rubyonrails.org/maintenance
    • この PR でも言及されている通り、8.0.x は bug fix support が終了し、security fixes のみのフェーズ。
  • 関連 PR:
    • main で allow_comments: true を導入した PR: #57832
    • 8.0-stable に一度バックポートされて revert されたコミット:
      • 追加: eb5a0633495b70e4484bea0e1562081d1de72de6
      • Revert: fdc6c1c9c1eab99b41b6a53c7bd8cdedbe36c9f0

#58146 Test that config.action_dispatch.default_headers set in an initializer is applied

マージ日: 2026/7/18 | 作成者: @nurey

  1. 概要 (1-2文で)
    Rails 8.x 系で「initializer から設定した config.action_dispatch.default_headers がレスポンスに反映されない」というリグレッションに対し、その再発を防ぐためのテストが追加されています。既に main ブランチでは修正済みですが、その挙動を保証する回帰テストを追加する PR です。

  1. 変更内容の詳細

何をテストしているか

追加されたテストは、

config/initializers/*.rb から config.action_dispatch.default_headersハッシュごと置き換えた 場合でも、その設定が実際の HTTP レスポンスに適用されるか

を検証しています。

背景として、既存のテストはどれも以下のように「直接クラスに設定」していました:

ruby
ActionDispatch::Response.default_headers = { ... }

これは Rails アプリのブートや Railtie、initializer の実行タイミングを一切通らないため、「設定がいつ・どの順番で読み込まれるか」という問題を検出できていませんでした。

今回のテストでは、実際にアプリをブートし、initializer から設定するパスを通して確認しています。

具体的なポイント

説明文から読み取れるテスト内容の要点は以下です。

  • 対象ファイル: railties/test/application/configuration_test.rb
  • 新しいテスト名:
    test_config.action_dispatch.default_headers_can_be_set_in_an_initializer_and_is_applied_to_responses

テストのシナリオは概ね次のような流れになります(擬似コードレベル):

ruby
# アプリ定義の中で、config/initializers 内にファイルを作るテスト支援コードを使うイメージ
add_to_config <<-RUBY
  # config/initializers/default_headers.rb 相当のコード
  Rails.application.config.action_dispatch.default_headers = {
    "X-Custom-Header" => "from_initializer"
  }
RUBY

app("development")

# 実際に Rack 経由でリクエストを投げる
get "/"

# レスポンスヘッダに initializer で設定したヘッダが入っていることを確認
assert_equal "from_initializer", last_response.headers["X-Custom-Header"]

ここで重要なのは、ヘッダの設定を

ruby
config.action_dispatch.default_headers["X-Custom-Header"] = "..."
# ではなく
config.action_dispatch.default_headers = { "X-Custom-Header" => "..." }

のように「ハッシュ全体を置き換えている」点です。

なぜ「ハッシュ全体の置き換え」をテストするのか

問題になっていたケースは、ActionDispatch::Responseconfig/initializers より先にロードされてしまう状況でした。

  • Rails 起動時に ActionDispatch::Response がロードされると、その時点の default_headers オブジェクト(ハッシュ)がフック(load hook)としてキャプチャされる
  • その後 initializer で 別のハッシュを代入すると、ActionDispatch::Response 側が保持しているのは「古いハッシュ」なので、新しく代入したヘッダがレスポンスに反映されない

一方で、以下のような「インプレース変更」は、オブジェクト自体は同じなのでたまたま動いてしまいます:

ruby
# これは「同じハッシュオブジェクト」に対する変更
config.action_dispatch.default_headers["X-Custom-Header"] = "foo"

このため、実際にバグっていたのは「ハッシュごと置き換える」パターンであり、そのパターンをテストでカバーする必要があるというのがこの PR の趣旨です。

説明文にも明記されています:

The test replaces the headers hash wholesale in an initializer (rather than mutating it in place), because that is the case that actually breaks when ActionDispatch::Response is loaded before config/initializers run — in-place mutation happens to survive since it mutates the same object the load hook captured.


  1. 影響範囲・注意点
  • これは テストのみを追加する PR であり、ランタイムのコード変更はありません。
    • 実際の影響は「8.x のリリースブランチにバグ修正をバックポートする際に、このテストがフェイルするかどうかで確認できる」点にあります。
  • 既存の修正 (コミット f9c82c3f2a) が 本当にリグレッションを直しているかを保証する役割を持ちます。
    • 説明によると、このコミットを一時的に revert するとテストは失敗することを確認済みです。
  • アプリ開発者目線では:
    • Rails 8.x で「initializer で config.action_dispatch.default_headers = { ... } とまるごと代入したのに反映されない」という症状に遭遇していた場合、
      • main / 修正版 8.x では直っている
      • その挙動はこのテストで今後も保証される
        という状態になります。
  • 逆にいうと、今後 ActionDispatch::Response のロードタイミングや default_headers の扱いを変える変更を行うと、このテストが落ちる可能性があるため、そのような変更をする際のセーフティネットになります。

  1. 参考情報 (あれば)
  • この PR がカバーするリグレッション: #58145

  • 関連する過去の Issue/PR: #32592

  • 挙動を保証している修正コミット: f9c82c3f2a (load hook の実行タイミングの調整)

  • 対象テストの実行コマンド(PR 説明より):

    bash
    cd railties
    bin/test test/application/configuration_test.rb \
      -i "test_config.action_dispatch.default_headers_can_be_set_in_an_initializer_and_is_applied_to_responses"

アプリ側でのベストプラクティスとして、Rails 8.x 以降でも安心して:

ruby
# config/initializers/default_headers.rb
Rails.application.config.action_dispatch.default_headers = {
  "X-Frame-Options" => "SAMEORIGIN",
  "X-Custom"        => "foo"
}

のような「ハッシュごと置き換え」を使えるようになっており、その保証としてこのテストが追加された、という位置付けです。


#58148 Fix test_precompile_shouldn't_use_the_digests_present_in_manifest.json failure

マージ日: 2026/7/17 | 作成者: @yahonda

  1. 概要 (1–2文で)
    Rails の Sprockets 資産プリコンパイルに関するテストが CI 上で時々失敗していた問題を、テスト側で 1 秒スリープを挟むことでフレークを解消した PR です。Sprockets がファイルの更新検知を「秒単位の mtime」と「キャッシュされたダイジェスト」に依存している仕様にテストを合わせています。

  1. 変更内容の詳細

対象テスト:
ApplicationTests::SprocketsAssetsTest#test_precompile_shouldn't_use_the_digests_present_in_manifest.json

このテストは、次のようなシナリオを検証しています:

  • 1回目の assets:precompile 実行後に app/assets/images/rails.png を書き換える
  • 2回目の assets:precompile を実行したとき、既存の manifest.json 内のダイジェスト値に依存せず、新しいダイジェストが生成されることを確認する
  • 具体的には、application.css のダイジェスト付きファイル名が 1 回目と 2 回目で異なることを期待する

しかし、Sprockets 側の実装では:

  • ファイルのダイジェスト計算結果を、ファイルの mtime(秒精度で丸め)をキーにキャッシュしている
    • 実装箇所: Sprockets::Base#file_digest
    • コミットコメントにも「精度はすでに 1 秒にハードコードされている」と明記されている
  • 同じ秒の間にファイルを書き換えると、mtime が変わらない → 既存キャッシュされたダイジェストが再利用される

もともとのテストは:

  1. 1回目のプリコンパイル
  2. app/assets/images/rails.png を上書き
  3. 2回目のプリコンパイル

の 1–3 を非常に短時間で連続実行しており、高速なマシンだと 2. の書き換えが 1. と「同じ秒」の中に行われてしまうことがありました。
その場合、2回目のプリコンパイルでも Sprockets が古いダイジェストをキャッシュから返し続けるため、

text
Expected "application-xxxx.css"
to not be equal to "application-xxxx.css".

という形で、「異なっていてほしいダイジェスト付きファイル名が同一になってしまう」フレークが発生していました。

この PR で行われた変更は非常に小さく、対象ファイルは 1 つ・追加は 1 行のみです:

  • railties/test/application/sprockets_assets_test.rb
    • app/assets/images/rails.png を書き換える前に sleep 1 を挟む行を追加

イメージ的には以下のような変更です(実際のコードとはメソッド名や細部が異なる可能性がありますが、意図を伝えるための擬似コードです):

ruby
def test_precompile_shouldnt_use_manifest_digests
  # 1回目のプリコンパイル
  precompile_assets

  # ここで待たないと、同じ秒の中で書き換えが完了してしまうことがある
  sleep 1

  # 画像を書き換えて asset を変更
  File.write("app/assets/images/rails.png", "new content")

  # 2回目のプリコンパイル
  precompile_assets

  # application.css のダイジェスト付きファイル名が変わっていることを確認
  assert_not_equal first_digest, second_digest
end

この 1 秒スリープにより、必ず「書き換え前」と「書き換え後」で mtime の秒が変わるようになり、Sprockets のキャッシュキーも変わるため、新しいダイジェストが確実に計算されます。

著者は、Ubuntu 26.04 / Ryzen 9 7940HS / Ruby 4.0.6 環境で 100 回ずつテストを実行し、以下の再現率を示しています:

  • main ブランチ + sleep なし: 100 回中 47 回失敗
  • main ブランチ + sleep あり: 100 回中 0 回失敗

  1. 影響範囲・注意点
  • 実際のアプリケーションコードや Sprockets 本体の挙動には一切変更がなく、影響範囲はテストのみです。
  • CI で sporadic に落ちていた test_precompile_shouldn't_use_the_digests_present_in_manifest.json が安定することが期待できます。
  • 根本にある仕様は「Sprockets は 1 秒単位の mtime を前提にキャッシュしている」というものであり、これは長年の設計として受け入れられているため、この PR は仕様にテストを合わせる対応です。
    • 実運用上、「2 回の assets:precompile の間に同じファイルを 1 秒未満の間隔で書き換える」というケースは現実的ではなく、テストが現実よりも「速すぎる」ことによる問題を吸収しています。
  • 将来、Sprockets 側で mtime 精度をナノ秒レベルなどに引き上げるような大きな変更が入った場合、この sleep は理論上不要になりますが、現時点では仕様に対する合理的なワークアラウンドと言えます。

  1. 参考情報 (あれば)

#58139 Harden load schema migrations

マージ日: 2026/7/17 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveRecord::Schema.load_schema_migrations が、実プロジェクトで起こりがちな2つのケース(CRLF 行末や末尾改行なし、重複バージョン)でも安全かつ一貫して動作するように強化された PR です。これにより、db/schema.rb のフォーマット差異や人手によるマージミスがあっても、スキーママイグレーションのロードがより堅牢になります。

  1. 変更内容の詳細

背景となる問題

この PR は #58134 で導入された ActiveRecord::Schema.load_schema_migrations(schema_path_or_file) に対するフォローアップで、以下2つの問題を解決しています。

(1) CRLF 行末/末尾改行なしの場合に __END__ を認識できない

db/schema.rb には次のような形で「マイグレーションバージョン一覧」を __END__ 以降に書く仕様になっています(#58134 による新機能):

ruby
ActiveRecord::Schema.define(version: 20260101000001) do
  # ...
end

ActiveRecord::Schema.load_schema_migrations(__FILE__)

__END__
20260101000001
20260102000000

Ruby 自体は:

  • CRLF (\r\n) の行末でも __END__ を正しく認識する
  • __END__ がファイル末尾の行で、さらにその行に末尾改行がなくても認識する

にもかかわらず、load_schema_migrations 側の実装が「Ruby の行扱い」とずれていたため、以下のようなケースでエラーになっていました:

  • db/schema.rb が CRLF 行末でコミットされている
  • __END__ 行が最後で、最後の行に改行がない

具体的な症状:

text
ActiveRecord::ActiveRecordError: No __END__ found in db/schema.rb

Ruby としては問題なく db/schema.rb を実行できているのに、load_schema_migrations だけが __END__ 不在と誤判定していた、という状態です。

対応内容:

この PR では、「Ruby が __END__ を認識するのと同じ行セマンティクスで __END__ を探す」ように実装を変更しています。
結果として:

  • CRLF 行末の db/schema.rb
  • __END__ がファイル末尾にあり、末尾改行がない db/schema.rb

のどちらも、ActiveRecord::Schema.load_schema_migrations で正常に __END__ と以降のバージョンリストが読み込まれるようになりました。

なお、__END__ 自体が存在しないファイルは、以前と同じく ActiveRecord::ActiveRecordError: No __END__ found... を投げます(挙動変更なし)。

(2) バージョンリストに重複があると新規 DB ロードが失敗する

__END__ 以降のマイグレーションバージョン一覧に、同じバージョンが重複しているケース(典型的には手動マージ時のミス)があると、新規 DB に対する db:schema:load が途中で失敗していました。

典型的な壊れた例:

ruby
__END__
20260101000001
20260101000001 # 重複

この状態で「まだ何もマイグレーションが入っていない DB」にロードすると、schema_migrationsversion カラムに UNIQUE 制約があるため、次のようなアダプタレベルの例外になります:

text
ActiveRecord::RecordNotUnique: SQLite3::ConstraintException: UNIQUE constraint failed: schema_migrations.version

一方で、「既にそのバージョンが schema_migrations に入っている状態」で同じ db/schema.rb をロードすると、INSERT がスキップされて「黙って通る」ため、

  • 「新規 DB に対するロード」と
  • 「既存 DB に対するインクリメンタルロード」

とで挙動が食い違う、という一貫性のない状態でした。

対応内容:

__END__ 以降から読み込んだバージョンリストを、schema_migrations に INSERT する前に 重複排除(dedup) するように変更しています。

ポイント:

  • schema_migrations テーブル自体が「集合(set)」として振る舞うべきものなので、重複バージョンには意味がない
  • 事前に uniq してから INSERT することで、
    • 新規 DB へのロード
    • 既存 DB への追加ロード のどちらでも同じ結果になる(=一貫した挙動)

PR の説明文にもある通り、「重複を黙って捨てる」のではなく「重複があればエラーにして該当バージョンを表示する」という方針に変更する余地は残されていますが、現時点では「静かに dedup」する実装になっています。

テスト追加

activerecord/test/cases/active_record_schema_test.rb に、以下のような観点のテストが追加されています:

  • CRLF 行末の db/schema.rb でも load_schema_migrations__END__ を見つけてロードできること
  • __END__ が末尾行かつ末尾改行なしでもロードできること
  • バージョンリストに重複があっても、新規 DB ロードが UNIQUE 制約違反にならないこと(正しく dedup されること)

  1. 影響範囲・注意点
  • 対象バージョン:
    • この機能(load_schema_migrations 自体)がまだ未リリースのため、リリース済みの Rails には直接の影響はありません。将来この機能を含むバージョンを利用するときに効いてくる修正です。
  • 想定ユースケース:
    • db/schema.rbActiveRecord::Schema.load_schema_migrations(__FILE__)__END__ 以降のバージョン一覧を埋め込む新方式を採用しているプロジェクト
  • 注意点:
    • これまで「重複バージョンがあると新規 DB ロードはコケる」ことで潜在的なマージミスに気付けていたケースは、この PR により「静かに dedup される」ようになります。
      • とはいえ、schema_migrations は set として扱うべきであり、かつ「新規 vs 既存」の挙動差をなくすという意味で、今回の挙動の方が合理的です。
      • プロジェクトとして「重複を絶対に許容したくない」場合は、CI などで __END__ 以降のバージョンリストを解析して重複チェックするカスタム検査を足すのが安全です。
    • CRLF/末尾改行なし周りは挙動が寛容になっただけなので、既存の正常ケースが壊れる心配はほぼありません。

  1. 参考情報 (あれば)
  • 元となった機能追加 PR: #58134
    ActiveRecord::Schema.load_schema_migrations によって、schema.rb から schema_migrations を再構成できるようにする試み。
  • この PR の意図:
    • 実運用でよくあるファイル形状(CRLF、末尾改行なし、マージミスによる重複行)に対しても壊れにくくし、「schema.rb から安全に schema_migrations を復元する」という新機能の堅牢性を高めることにあります。

#58113 Make AbstractController::Base#action_methods ractor safe

マージ日: 2026/7/17 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    AbstractController::Base#action_methods が返す Set を凍結(freeze)し、Ractor(Rubyの並行実行モデル)で参照しても Ractor::IsolationError が出ないようにした変更です。これに伴い、ActionMailer など action_methods をオーバーライドしている箇所も、Ractorセーフになるよう調整されています。

  1. 変更内容の詳細

背景となる問題

  • AbstractController::Base#action_methods は「アクションとして扱うメソッド名の集合」を Set で返します。
  • 返される Setmutable(変更可能) なオブジェクトのままだと、Ractor間で共有・参照しようとした際に Ruby の Ractor のルールに反して Ractor::IsolationError が発生します。
    • Ractorでは「共有するオブジェクトは原則 immutable であること」が要求されるためです。

コアな変更点

1) AbstractController::Base#action_methods の戻り値を freeze

actionpack/lib/abstract_controller/base.rb の変更により、action_methods が返す Set が凍結されるようになりました(Set インスタンスに対して freeze を呼ぶだけのシンプルな変更)。

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

ruby
def action_methods
  @action_methods ||= begin
    # 公開アクション用のメソッド名を集めて Set にする処理
    methods = # ...
    methods.to_set.freeze
  end
end

これにより、controller.action_methods を Ractor の中で参照しても、Set が immutable なので Ractor 間で安全に共有できます。

2) ActionMailer 側の action_methods オーバーライド対応

ActionMaileraction_methods をオーバーライドして独自の「メール送信アクション」の集合を返していますが、こちらも Ractor セーフになるように修正されています。
具体的には:

  • actionmailer/lib/action_mailer/base.rb で、super から返ってくる action_methods(既に freeze 済)に対して、Mailer 特有のロジックを噛ませた上で、最終的な戻り値も freeze されるように整備。
  • 既存コードが action_methods の戻り値を破壊的に変更(add, merge!, delete など)しているケースがあれば、今後は動かなくなるため、そのような前提が無い形に合わせている/テストで保証している、という位置づけです。

3) AbstractController::UrlFor 周りの微調整

actionpack/lib/abstract_controller/url_for.rb でも action_methods を利用しているため、freeze された Set を前提としたコードに軽微な修正が入っています。
ここでは主に「action_methods を操作しない(変更しない)利用の仕方」で動くように、あるいは前提を明示するための変更です。

4) テスト追加

actionpack/test/controller/base_test.rb にテストが追加されています。内容としてはおおよそ:

  • action_methodsSet を返すこと
  • その Setfrozen? であること
  • (間接的に)アクション解決やルーティングが従来どおり動作すること

を確認するテストになっていると考えられます。


  1. 影響範囲・注意点

想定される影響範囲

  • すべての AbstractController::Base を継承するクラス(ActionController::Base, ActionMailer::Base など)の #action_methods 戻り値が 凍結された Set になります。
  • これにより、Rails アプリケーションコードで以下のようなことをしている場合は壊れます:
ruby
class ApplicationController < ActionController::Base
  def some_setup
    # NG: これからは frozen Set なので例外が出る
    self.class.action_methods.add("new_action") 
  end
end

あるいは

ruby
class MyController < ActionController::Base
  def self.action_methods
    methods = super
    # NG: 破壊的変更
    methods.delete("index")
    methods
  end
end

このようなコードは、今後 FrozenError(旧 RuntimeError: can't modify frozen ...)を発生する可能性があります。

推奨される対応・書き方

  • action_methods読み取り専用 として扱うことが前提になります。
  • フィルタリングや追加などのカスタマイズが必要な場合は、新しい Set を生成する ように書き換える必要があります。

例:

ruby
class MyController < ActionController::Base
  def self.action_methods
    super - %w[index]        # 新しい Set(あるいは Enumerable)を返す
  end
end

あるいは

ruby
class MyController < ActionController::Base
  def self.action_methods
    super.to_set.merge(%w[extra_action]).freeze
  end
end
  • Ractor を使わないアプリでも、この freeze による挙動変更の影響は受けるため、action_methods を破壊的に操作している独自コードがないか確認する価値があります。

Ractor 利用時のメリット

  • コントローラやメイラーを Ractor 内でインスタンス化・利用する際、action_methods の参照で Ractor::IsolationError が出なくなります。
  • 将来的に Rails アプリを Ractor ベースで並列化する際の基盤整備の一環と言えます。

  1. 参考情報 (あれば)
  • 変更対象クラス
    • AbstractController::Base#action_methods
    • ActionMailer::Base#action_methods(オーバーライド)
    • AbstractController::UrlFor 内での action_methods 利用箇所
  • 関連する Ruby の仕様
    • Ractor は「共有オブジェクトは immutable(freeze 済み)か、Ractor ローカル」であることを要求する。
    • Rails 内部のキャッシュ済みオブジェクト(Set, Hash など)の freeze は、Ractor 対応で今後も増えていく可能性が高いです。
  • CHANGELOG は今回の PR では更新されておらず、「仕様変更というよりはバグ修正(Ractor 非対応だった)」という位置づけに近いと考えられます。

#58134 Support dumping schema_migrations in db/schema.rb

マージ日: 2026/7/17 | 作成者: @fxn

  1. 概要 (1-2文で)
    db/schema.rbschema_migrations の中身(適用済みマイグレーションのバージョン一覧)をダンプできるオプションが追加されました。これにより、スキーマファイルだけで DB のマイグレーション状態をより正確に再現でき、かつマイグレーションバージョンに起因するマージコンフリクトを緩和できるようになります。

  1. 変更内容の詳細

新機能の概要

  • 新しいフラグにより、:ruby フォーマットのスキーマ (db/schema.rb) に schema_migrations の内容をダンプできるようになりました。
  • デフォルトは 無効(従来通り)で、設定を有効にすると db:schema:dump 時に以下のような形式になります。
ruby
ActiveRecord::Schema[8.2].define do
  ...
end

ActiveRecord::Schema.load_schema_migrations(__FILE__)
__END__
20260716101900
20260716112003
20260716130752
...

ポイント:

  1. ActiveRecord::Schema.define から version: 引数がなくなる
    • 例: ActiveRecord::Schema[8.1].define(version: 2026_07_16_101907) doActiveRecord::Schema[8.2].define do
  2. schema_migrations のバージョンは __END__ 以降に単純なテキストとして列挙される。
  3. ActiveRecord::Schema.load_schema_migrations(__FILE__)__END__ 以降を読み取り、DB の schema_migrations テーブルを埋める。

なぜ必要か (問題の整理)

Issue 1: スキーマだけでは状態が不完全だった

従来:

ruby
ActiveRecord::Schema[8.1].define(version: 2026_07_16_101907) do
  ...
end
  • version: で示しているのは「最後に適用されたマイグレーションのタイムスタンプ」だけ。
  • schema:load 時には、「スキーマに記録された version より古いマイグレーションはすべて適用済みとみなして schema_migrations に INSERT する」 という仮定で補完していた。

問題になるケースの例:

  1. ブランチ A にローカルマイグレーション 2026_07_16_101900 があるが、まだ実行していない。
  2. main をマージすると、main 側で新しいマイグレーション 2026_07_16_101907 が入っており、schema.rb の version がそれに更新される。
  3. db:reset / db:schema:load をすると、2026_07_16_101900 は version より古いので「適用済み」とみなされ schema_migrations に登録される。
  4. しかし実際のスキーマにはそのマイグレーションの変更は含まれていない(スキーマは main の定義だけを反映しているから)。
  5. その状態で bin/rails db:migrate を叩いても、そのマイグレーションは「適用済み」と判断されて何も実行されない。

結果として、「スキーマと schema_migrations の内容が食い違う(論理的に矛盾した)DB状態」が生まれ得る。

この PR では:

  • schema.rb 自体に schema_migrations の中身をダンプすることで、
    schema:load 時に「version より古いものを推測して INSERT」する必要がなくなる。
  • つまり、スキーマファイルだけで DB の論理状態(どのマイグレーションが適用済みか)を完全に復元できる

Issue 2: スキーマバージョンによるマージコンフリクト

従来は define(version: xxxx) によって単一の version を 1 箇所に書いていたため、複数ブランチでマイグレーション追加 → schema.rb 更新 → その 1 行で高確率にコンフリクト、という状態になりやすかった。

この PR では:

  1. version: 引数そのものを生成しなくなる(単一行のコンフリクト要因を削除)。
  2. schema_migrations のダンプ順をカスタマイズ可能にして、マージコンフリクトをさらに起きにくくできる。

ダンプ順序のカスタマイズ (dump_schema_migrations_sort_by)

  • デフォルトでは 文字列の辞書順 で並ぶ。
  • 設定で並び順を自由に変えられる:
ruby
# 例1: 文字列を reverse して比較する(要は末尾優先のソート)
config.active_record.dump_schema_migrations_sort_by = ->(version) { version.reverse }

# 同じことをシンボルの to_proc 経由で
config.active_record.dump_schema_migrations_sort_by = :reverse

さらに、MD5 などで「ランダム風に散らす」ことも可能:

ruby
require "digest/md5"

config.active_record.dump_schema_migrations_sort_by = ->(version) { Digest::MD5.hexdigest(version) }
  • これにより、複数の開発者が別々のマイグレーションを追加しても、差分が特定の末尾行に集中せず、マージコンフリクトの確率が下がる。

:sql スキーマダンプでは既に SQL ジェネレータをカスタム実装する形で似た問題に対処しているが、:ruby ではブロック(proc)を渡すだけで済む。

ダンプの場所と形式

  • db/schema.rb の Ruby DSL 本体の後に:
ruby
ActiveRecord::Schema.load_schema_migrations(__FILE__)
__END__
20260716101900
...

という形でプレーンテキストのリストが付く。

採用理由:

  • DSL(Ruby コード)とデータ(マイグレーションバージョン列挙)を視覚的に分離できる。
  • エディタのハイライトでも区別され、読むときに邪魔になりにくい。
  • diff が最小限(インデントなし・配列リテラルなし)で済む。
  • Ruby の __END__ 以降は通常の実行に影響しないため、load_schema_migrations が明示的に読み取る箇所をコントロールできる。

その他仕様の細部

  • db/migrate に存在しないマイグレーションバージョンはダンプされない
    → すでに削除されたマイグレーションは schema.rb__END__ 部分にも出てこない。
  • フラグは DB単位でオーバーライド可能
    → マルチ DB 環境でも、チームごと・DB ごとに段階的に導入できる。
  • 一方で dump_schema_migrations_sort_by の proc は DB ごとの設定には対応していない
    • YAML (database.yml) 経由でシンボルを渡すケースなどを考えると、契約(受け入れ可能な値の型)が複雑化するため、グローバル設定のままとされている。
  • schema_migrations のロードは冪等(同じ schema.rb を何度ロードしても状態が壊れない)。

  1. 影響範囲・注意点

既存プロジェクトへの影響

  • デフォルトでフラグは false のため、既存プロジェクトは即座には挙動変更されません。
  • ただし Rails バージョンアップに伴い、ActiveRecord::Schema[8.2].define do のように version: なしの形が新デフォルトになる可能性があります(schema_migrations ダンプを有効にしない場合でも)。
  • この PR による主な挙動変化は、フラグを有効 (true) にして db:schema:dump / db:schema:load を利用した場合です。

有効化時の注意

  • schema.rb__END__ 以降のリストが 単一の真実 になります。
    → その内容と DB の schema_migrations が一致していることを前提に schema:load されるため、
    schema.rb を手で編集するのは推奨されません(従来以上に危険)。
  • 既存の CI / ツールチェーンが db/schema.rb の末尾に何かを追記している場合、__END__ の扱いに注意が必要です。
  • マイグレーション削除運用:
    • マイグレーションファイルを削除すると、そのバージョンは次回ダンプから __END__ 部分に含まれなくなります。
    • 古い環境でまだそのマイグレーションが DB に残っているケースと整合をどう取るかは従来どおり「マイグレーションの削除ポリシー」の問題として残ります。

マージコンフリクトについて

  • version: 行がなくなる + 並び順のカスタマイズにより、多くのケースで schema.rb のマージコンフリクトは減る見込みです。
  • ただし、同じブロック(同じテーブル定義など)を編集した場合のコンフリクトは当然ながら従来通り発生します。
  • dump_schema_migrations_sort_by によるソート戦略はチーム内で合意しておくべきです。
    • デフォルト(辞書順)は「読みやすさ重視」だがコンフリクトは若干起きやすい。
    • MD5 などの「diff 分散戦略」はコンフリクト低減には有効だが、人間には直感的な順序ではなくなります。

  1. 参考情報 (あれば)
  • 実コード変更の主な場所

    • activerecord/lib/active_record/schema.rb
      • ActiveRecord::Schema.load_schema_migrations(path) の実装追加。
    • activerecord/lib/active_record/schema_dumper.rb
      • schema_migrations のダンプ処理とソートロジック。
    • activerecord/lib/active_record.rb, railtie.rb, database_configurations/*
      • 設定フラグ (dump_schema_migrations / dump_schema_migrations_sort_by) の追加および DB ごとのオーバーライド対応。
    • guides/source/active_record_migrations.md, guides/source/configuring.md
      • ガイドに新機能の説明と設定方法を追加。
  • アイデアとして検討されていた代替案

    • schema_migrations 用の専用ディレクトリを作り、各バージョンごとに空ファイルを配置して Git で管理する方式も検討された。
    • これはマージコンフリクトに非常に強い(ファイルの追加・削除は Git が得意)が、
      ファイル数増加・コミット漏れ・ツール側の対応コスト増などのデメリットがあるため、現時点では採用されていない。

#58133 Don't force multi statement for MySQL tx isolation

マージ日: 2026/7/16 | 作成者: @edaroit

  1. 概要 (1-2文で)
    MySQL系アダプタでトランザクション分離レベルを設定して BEGIN する処理について、「再試行時に一体として扱う」安全性は維持しつつ、常にマルチステートメントを強制しないように変更されたPRです。これにより、DBへのラウンドトリップ増加や ProxySQL でのコネクション多重化(multiplexing)無効化といった副作用を避けられるようになっています。

  1. 変更内容の詳細

背景

以前のリファクタリングで、begin_isolated_db_transaction(MySQL 抽象アダプタの「指定した隔離レベルでトランザクションを開始する」処理)は次のような方針になっていました:

  • SET TRANSACTION ISOLATION LEVEL ...; BEGIN を「マルチステートメント (multi statements)」として一括実行する
  • これにより、DB接続レベルの再試行(with_raw_connection などのリトライ機構)が入った場合でも、「SET だけ成功して BEGIN がリトライされる」/「BEGIN だけリトライされて、意図した分離レベルが失われる」といった不整合を防ぐ

ただし、この方法には以下の問題がありました:

  • マルチステートメントを有効/無効にするためのやり取りが増え、結果的に DB とのラウンドトリップ数が増える
  • ProxySQL を使っている場合、SET TRANSACTION ISOLATION とマルチステートメントの組み合わせによりコネクション multiplexing が無効化される(ProxySQL 側の仕様・制限)

今回の変更の狙い

  • SET TRANSACTION ISOLATION LEVELBEGIN を再試行単位として一体に扱う」=一貫性と安全性は維持したい
  • ただしその実現方法として、常にマルチステートメントを強制するのはやめる
  • 既にマルチステートメント利用を前提にしている設定では、互換性を保つため引き続き execute_batch(マルチステートメント)を利用

実装上のポイント

主な変更は abstract_mysql_adapter.rbbegin_isolated_db_transaction 周辺です。

1. マルチステートメント利用有無で挙動を分岐

擬似コード的には次のような形になります(実際のコードとは多少異なりますが概念的にはこういう分岐):

ruby
def begin_isolated_db_transaction(isolation)
  if supports_multi_statements?
    # 以前と同様、マルチステートメントで一体として実行
    execute_batch(<<~SQL)
      SET TRANSACTION ISOLATION LEVEL #{quote_isolation(isolation)};
      BEGIN
    SQL
  else
    # マルチステートメントを使わない場合:
    # with_raw_connection の allow_retry で「一体として再試行」する
    with_raw_connection(allow_retry: true) do
      execute("SET TRANSACTION ISOLATION LEVEL #{quote_isolation(isolation)}")
      execute("BEGIN")
    end
  end
end

ポイント:

  • マルチステートメントが有効な場合
    • 既存と同じく execute_batch を使い、SETBEGIN を一つの SQL バッチとして送る。
  • マルチステートメントが無効な場合
    • 2つの独立したクエリ (SET ... / BEGIN) として送るが、
    • with_raw_connection(allow_retry: true) ブロック内で実行することで、「途中でネットワークエラーや切断が発生した場合はブロック全体を再試行」できるようにしている。
    • これにより「BEGIN だけがリトライされて isolation level が落ちる」といった不整合を防ぐ。

重要な点:

  • マルチステートメントを使わないルートでは、SETBEGIN は別クエリだが、再試行単位としては「1ブロック」にまとまっている。
  • そのため、安全性(再試行時の一貫性)は維持しつつ、DB とのラウンドトリップ数は従来どおりで、余計な on/off 切り替えをしなくて良くなる。

2. Trilogy アダプタへの対応

trilogy/database_statements.rb にも小さな変更が加えられており、Trilogy ベースの MySQL アダプタでも上記ロジックが適切に動作するようになっています。

Trilogy は Shopify 発の MySQL クライアントライブラリで、Rails 7.1 以降で公式サポートされているものです。この PR では、抽象アダプタの挙動変更が Trilogy 経由の接続でも正しく機能するよう、必要なフック・メソッドを追加/調整しています。

3. テスト追加

abstract_mysql_adapter/transaction_test.rb にテストが大量に追加されています(+47行)。

想定されるテスト内容:

  • マルチステートメント有効時:
    • SET TRANSACTION ISOLATION LEVEL ...; BEGINexecute_batch でまとめて送信されること
  • マルチステートメント無効時:
    • SETBEGIN が別々のクエリとして実行されること
    • それらが with_raw_connection(allow_retry: true) の中で実行され、再試行単位が1つであること
  • 隔離レベル指定が正しく反映されること(:read_committed, :repeatable_read, :serializable など)

これにより、リグレッション(前のリファクタリングで得た「一体としての再試行」保証が壊れていないか)を防いでいます。


  1. 影響範囲・注意点

影響範囲

  • 対象: MySQL 系アダプタ(mysql2 / trilogy など)を利用しており、かつ
    • transaction(isolation: ...) のような「分離レベル指定付きトランザクション」を使っているアプリケーション
  • 特に恩恵が大きいケース:
    • ProxySQL 経由で MySQL に接続している構成
      • マルチステートメントと SET TRANSACTION ISOLATION の組み合わせで multiplexing が無効化されていた問題が軽減/解消される
    • ネットワークが不安定で、with_raw_connection の再試行に頼るケース
      • 分離レベルと BEGIN が一貫性を持って再試行される

互換性・挙動の変化

  • API レベルのインターフェース(transaction(isolation: ...) など)は変わりません。
  • 接続が「マルチステートメントをサポートしていない/利用しない」設定になっている場合:
    • 以前はマルチステートメントを強制する方向の変更が入っていたのに対し、今回の PR により、マルチステートメントを無理に有効化しなくても安全に動くようになります。
  • マルチステートメントを「明示的に利用している」アプリケーション:
    • その設定がオンであれば従来通り execute_batch を使うため、挙動面で大きな変化はありません(ただし、以前のPRで追加された「マルチステートメント on/off の切り替えラウンドトリップ」が削減される場合があります)。

注意点

  • ProxySQL利用時:
    • この修正により isolation 付きトランザクションでの multiplexing 無効化は緩和されますが、ProxySQL 側のバージョンや設定によって挙動が異なる可能性があります。
    • 重要なワークロードの場合は、本番適用前に「分離レベルを変更するトランザクションが期待通り multiplexing されるか」「接続数・レイテンシに変化がないか」を計測することを推奨します。
  • エラー再試行ロジック:
    • with_raw_connection(allow_retry: true) を利用することで、アダプタ内部の「再試行ポリシー」に依存する部分が増えます。
    • これによる実害は基本的にありませんが、独自のパッチや monkey patch で with_raw_connection 周りをいじっている場合は影響が出る可能性があるため、差分を確認してください。

  1. 参考情報 (あれば)
  • 関連コミット(前回のリファクタリング)
  • ProxySQL issue
  • 変更ファイル
    • activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
      • begin_isolated_db_transaction の本体ロジック変更
    • activerecord/lib/active_record/connection_adapters/trilogy/database_statements.rb
      • Trilogy 用の対応
    • activerecord/test/cases/adapters/abstract_mysql_adapter/transaction_test.rb
      • テストの追加(挙動の回帰検証)

#58115 Add async test coverage for contradiction/none short-circuits in calculations

マージ日: 2026/7/16 | 作成者: @55728

  1. 概要 (1-2文で)
    矛盾条件(contradiction)や @none による早期リターンが行われる計算系メソッドについて、非同期版(async_*)でも「DBクエリを発行しない」「期待値を返す」ことを検証するテストが追加・強化された PR です。従来は同期版のみで確認していた no-query パスを、Promise ベースの async パスでも同等にカバーするようにしています。

  1. 変更内容の詳細

背景・狙い

  • Active Record の計算系メソッド(count, sum, average, minimum, maximum、および pluck, ids, pick)には、「結果が明らかに空・矛盾する」場合に DB に行かずに即座に値を返すショートサーキットがあります。
    • 例: where("1 = 0").count のような明らかな矛盾条件。
    • @none が立っている Relation(Model.none 由来など)。
  • 非同期 API(async_count など)は、この早期リターンの結果を Promise::Complete でラップする実装になっていますが、テストは同期 API 経由しか通っていませんでした。
  • 本 PR では、この「非同期 + ショートサーキット」経路もテストで明示的にカバーし、Promise が期待通りの値を返し、かつクエリ発行が 0 であることを保証します。

追加されたテスト・修正内容

変更はすべて activerecord/test/cases/calculations_test.rb に集約されています(+16 / -3)。

1. 非グループ計算 (execute_simple_calculation の contradiction パス)

対象メソッド:

  • async_count
  • async_sum
  • async_average
  • async_minimum
  • async_maximum

やっていること:

  • 既存の同期計算テスト(矛盾条件で早期リターンするケース)に、対応する async_* 呼び出しのアサーションを追加。
  • テストでは以下を確認しているはずです:
    • assert_no_queries または assert_queries_count(0) ブロックの中で async_* を呼び、
    • 返り値が Promise であること、
    • その Promisevalue(または sync/相当のメソッド)で解決したとき、同期版と同じ結果(例: 0 や nil 等)になること。

疑似コード例:

ruby
assert_no_queries do
  promise = relation_with_contradiction.async_count
  assert_equal 0, promise.value
end

2. グループ計算 (execute_grouped_calculation の contradiction パス)

対象メソッド:

  • async_count
  • async_sum

やっていること:

  • グループ化された計算で矛盾条件により結果が空になるパスについても、async_* で同様に no-query + 期待値を検証。
  • ここでも assert_queries_count(0) 内で Promise を評価する形になっているはずです。

3. pluck (@none / contradiction パス)

  • 既に async_pluck のテスト自体は存在していましたが、no-query を保証するブロックの外でアサートしていたため、「async 版が DB を叩いていない」ことは明示的に保証されていませんでした。
  • これを修正し、既存の:
ruby
assert_no_queries do
  assert_equal [], relation.pluck(:id)
end

# (以前はこの辺に async_pluck のアサートがあった)

のような構造を、

ruby
assert_no_queries do
  assert_equal [], relation.pluck(:id)

  promise = relation.async_pluck(:id)
  assert_equal [], promise.value
end

のように async_pluckassert_no_queries / assert_queries_count(0) の内側に移動しています。

4. ids (contradiction パス)

  • pluck と同様の修正。
  • async_ids のアサーションを assert_queries_count(0) ブロック内に移し、非同期版でもクエリが 0 回であることをチェック。

5. pick (@none / contradiction / loaded パス)

  • test_pick_one に対して、同期 pick に加えて async_pick のアサーションを追加。
    • @none、矛盾条件、あるいは結果が明らかに一意となるケースなど、それぞれで期待値を検証。
  • 新規に test_async_pick_on_loaded_relation を追加。
    • Relation が既にロード済みの場合に async_pick を呼んだとき、
      • 追加のクエリが走らず(キャッシュされたレコードから値を得られ)、
      • 結果も同期版と一致することを確認しています。

  1. 影響範囲・注意点
  • 本 PR はテストコードのみの変更であり、実際の実装(async_* / Promise::Complete 等)は変更されていません。
  • ただし、これにより以下の仕様がテストで明示的にロックされます:
    • 矛盾条件 / @none / loaded な Relation での async_* 系メソッドはDB クエリを発行しないこと。
    • その結果は同期版と同じ値であり、Promise 経由で取得できること。
  • 将来、async_* の実装を変更する場合、
    • ショートサーキット時にクエリを発行するような変更、
    • あるいは Promise の解決値が変わる変更 は、これらのテストにより検出されるようになります。
  • Rails の async API を利用しているアプリケーションにとっては、
    • 「同期版と同じ no-query 最適化が async 版にも効いている」
    • 「矛盾条件や none のときにも無駄なクエリが走らない」 という前提がより堅く保証されたと言えます。

  1. 参考情報 (あれば)
  • ベースになっている PR: #58058(計算系メソッドの async 対応 / リファクタリングと推測される)
  • レビューコメント: https://github.com/rails/rails/pull/58058#pullrequestreview-4687080638
    → 「contradiction / @none の早期リターン経路でも async のテストを追加してほしい」という指摘へのフォローアップ。

#58085 Preserve sub-second precision when coercing a DateTime in Time.at

マージ日: 2026/7/16 | 作成者: @55728

  1. 概要 (1–2文で)
    Time.at(DateTime) したときに、サブ秒(マイクロ秒)精度が失われていた問題を修正し、DateTime から Time への変換でもマイクロ秒まで正確に保持されるようにしたPRです。ActiveSupport::TimeWithZone と同様に Rational ベースの変換を使うことで、浮動小数点の丸め誤差を排除しています。

  1. 変更内容の詳細

何が問題だったか

Time.at は ActiveSupport の拡張により、以下のように 1 引数で DateTimeActiveSupport::TimeWithZone を受け取れます。

ruby
Time.at(datetime)
Time.at(time_with_zone)
  • TimeWithZone の場合: to_r(Rational)を使って正確なタイムスタンプで変換していた
  • DateTime の場合: to_f(Float)を使って変換しており、ここでサブ秒精度が落ちていた

Float では 1 マイクロ秒単位をすべて正確に表現できないため、約半分の値で丸め誤差が出て Time#usec がずれる、という挙動になっていました。

PR 説明にある例:

ruby
dt = DateTime.civil(2000, 1, 1, 0, 0, Rational(1, 1_000_000)) # 2000-01-01 00:00:00.000001
Time.at(dt).usec
# 修正前 => 0
# 修正後 => 1

dt = DateTime.civil(2000, 1, 1, 0, 0, Rational(123_457, 1_000_000))
Time.at(dt).usec
# 修正前 => 123456
# 修正後 => 123457

どちらも「元の DateTime に含まれるサブ秒が、Time に変換したときに 1 マイクロ秒分誤差を持ってしまう」例です。

何をどう直したか

修正内容はシンプルで、ActiveSupport の Time.at 拡張部で DateTime を扱う箇所を Rational ベースに変更しています。

  • これまで: datetime.to_f を使っていた(Float)
  • これから: datetime.to_i + datetime.sec_fraction を使う(整数秒 + 有理数で表現された秒数の小数部分)

datetime.sec_fractionRational を返すため、全体として Rational のタイムスタンプになります。これにより、TimeWithZone と同じく「完全に正確な Rational 値から Time を生成」する形になり、丸め誤差がなくなります。

テストとしては、activesupport/test/core_ext/time_ext_test.rb に DateTime からの変換に関するテストケースが追加されており、マイクロ秒精度が期待通りに保たれることが確認されています。


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

    • Time.at(datetime) のように DateTime を直接 Time.at に渡しているコード
    • とくに、サブ秒(microseconds)を意識している箇所(ログタイムスタンプ、精密メトリクス、イベント順序制御など)
  • 挙動の変化の具体例

    • これまで「DateTime のサブ秒が、Time に変換したあとに 1µs 前後ズレていた」ケースが、正確に一致するようになります
    • Time#to_i(秒単位)や日付・時刻(年・月・日・時・分・秒)が変わるわけではありません。変化するのは usec / nsec 相当のサブ秒部分のみです。
  • 互換性・注意点

    • 既存コードで「ズレた値」を前提にテストしていた場合(たとえば Time.at(dt).usec の具体的な数値を fixture 的に固定していた場合)は、テストが落ちる可能性がありますが、それは元の挙動が不正確だったためで、このPRにより論理的に正しい値になります。
    • パフォーマンス面では、Float から Rational への切り替えによる影響はごく小さいと考えられます(通常アプリでは問題にならないレベル)。
  • タイムゾーンや DST, 負のタイムスタンプ

    • PR 内で以下のケースが検証されていると明記されています:
      • 整数秒(サブ秒なし)
      • タイムゾーンオフセット(DST を含む)
      • マイクロ秒境界(サブ秒あり)
      • Unix エポックより前(負のタイムスタンプ)
    • これらのケースでも、datetime.to_i + datetime.sec_fraction で表される瞬間と Time.at(datetime) の結果が厳密に一致し、かつ戻り値は引き続きローカルタイム(getlocal 済み)であることが保証されています。

  1. 参考情報 (あれば)
  • Ruby の DateTime#sec_fraction ドキュメント:
    サブ秒を Rational で返し、精度の高い時間計算に使えることが知られています。
  • 浮動小数点誤差に関する一般的な注意点:
    時刻を Float 秒で扱うとマイクロ秒単位での精度が保証されないため、
    • データベースやメッセージングのタイムスタンプ
    • イベントソーシングのイベント時間
      などでは、このPRのように Rational または整数 + サブ秒整数(秒・ナノ秒のペア)のような表現を用いるのが望ましいです。

このPRにより、「DateTimeTime 変換時もマイクロ秒が正確に保持される」という、直感的かつ一貫した挙動が得られるようになっています。


#58119 Allow to access Controller#controller_path in a Ractor

マージ日: 2026/7/16 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    AbstractController#controller_path が返す文字列を freeze することで、Ractor 内からも安全に controller_path を呼び出せるようにした変更です。Ractor セーフティを満たすための最小限の修正で、既存のメモ化ロジックを活かしつつ対応しています。

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

何をしたか

AbstractController::Base#controller_path が返す文字列を「凍結(freeze)」するように変更しています。

変更前(イメージ):

ruby
def controller_path
  @controller_path ||= self.name.delete_suffix("Controller").underscore
end

変更後:

ruby
def controller_path
  @controller_path ||= self.name.delete_suffix("Controller").underscore.freeze
end

実際の変更は actionpack/lib/abstract_controller/base.rb の 1 行のみで、controller_path の結果を freeze するのが本質です。

なぜ必要か (Ractor セーフティ)

Ruby の Ractor では、Ractor 間で共有されるオブジェクトは原則としてイミュータブル(変更不可)である必要があります。
これまで controller_path は毎回「非 frozen な String」を返していたため、その値を Ractor に渡そうとすると Ractor セーフティに違反し、例外が発生する可能性がありました。

例:

ruby
class PostsController < ApplicationController; end

Ractor.new(PostsController) do |controller|
  # 変更前はここで Ractor::IsolationError 等になる可能性がある
  controller.controller_path
end

この PR により、controller_path が返す文字列が frozen になるため、上記のようなコードが Ractor 内でも安全に動作します。

メモ化と Ractor セーフティ

controller_path はインスタンス変数などを使ってメモ化されますが、

  • 値が frozen である
  • 値が設定されるタイミングは既存の inherited フックを通じて行われる

という点から、Ractor の観点でも問題ないと判断されています。
つまり、「クラス定義時に一度だけ Ractor 外でセットされた、イミュータブルな値」を各 Ractor が読むだけ、という形になります。

テスト追加

actionpack/test/controller/base_test.rb に 14 行のテストが追加されており、少なくとも次のようなポイントを検証していると考えられます:

  • controller_path が frozen な文字列を返していること
  • 継承したコントローラでも same behavior であること
  • メモ化挙動が壊れていないこと(複数回呼び出しても同一オブジェクトが返る 等)

  1. 影響範囲・注意点

影響範囲

  • すべてのコントローラ (AbstractController::Base を継承するクラス) の controller_path が、今後は 常に frozen な String になります。
  • これにより、controller_path を Ractor に渡す・Ractor 内で使用することが可能になります。
  • Ractor を使わないアプリでも、「返ってくる String が frozen になる」という仕様変更の影響は受けます。

注意点

  1. controller_path を破壊的に変更しているコードがあると壊れる

たとえば、以下のようなコードがある場合は例外が発生します:

ruby
# NG になる例
path = SomeController.controller_path
path.gsub!("/", "_")  # frozen string に対する破壊的変更で例外が出る

このような場合は、非破壊メソッドを使用するか、複製を取る必要があります:

ruby
# 推奨
path = SomeController.controller_path.tr("/", "_")
# または
path = SomeController.controller_path.dup
path.gsub!("/", "_")
  1. テストや拡張コードでの monkey patch に注意
  • controller_path を前提に何かしら monkey patch や特殊なキャッシュロジックを書いている場合、
    それが「mutable string であること」に依存していないか確認が必要です。
  1. Ractor 利用時の前提
  • この変更はあくまで controller_path の返り値を Ractor セーフにするものです。
    コントローラ全体が Ractor セーフになったという意味ではないため、
    Ractor 内でコントローラ・インスタンスをどこまで扱うかは別途注意が必要です。

  1. 参考情報 (あれば)
  • 対象メソッド: AbstractController::Base#controller_path
    • 典型的には "Admin::PostsController""admin/posts" のようなパスを返す
  • 関連する Ruby の仕様:
    • Ractor: オブジェクト共有は frozen オブジェクトなどに制限される
    • frozen String への破壊的操作は FrozenError などを引き起こす

この PR は、Ractor 対応を進めるうえでの小さいが重要な一歩であり、
「framework が返す定型的な String を積極的に凍結していく」流れの一環といえます。


#58116 Guard tests for PostgreSQL 11, 12, and 15 changes

マージ日: 2026/7/15 | 作成者: @yahonda

  1. 概要 (1-2文で)
    このPRは、Active Record のテスト群とテスト用スキーマを「PostgreSQL 10〜18の全サポート範囲で通る」ように調整し、特定バージョン以降でしか存在しない PostgreSQL 機能を利用するテストにガード(条件分岐・スキップ)を入れたものです。ライブラリ本体の動作変更は行わず、あくまでテストとスキーマ定義のみを修正しています。

  1. 変更内容の詳細

背景と目的

  • Rails(Active Record)は PostgreSQL 10 以降をサポートしている一方で、CI は postgres:alpine(現状 PostgreSQL 18)でしか実行されていません。
  • 手元で postgres:17postgres:10 の Docker イメージを使って rake test:postgresql を実行すると、
    • 「最小サポートバージョン(10)より新しいサーバー機能」に依存したテストが、バージョン判定なしで実行されて失敗する
    • PostgreSQL 10 ではテストスキーマ自体が読み込めず、テストがそもそも走らない という問題が見つかりました。
  • このPRは、それらを「テスト側だけ」で吸収し、PostgreSQL 10〜18 すべてでテストが通るようにするものです。

PostgreSQL 15 向けのテストガード

対象ファイル:

  • activerecord/test/cases/schema_dumper_test.rb

問題となっていたテスト

ruby
SchemaDumperTest#test_schema_dumps_unique_constraints
  • PostgreSQL 14 での失敗例:
text
Expected /t\.unique_constraint\ \["position_4"\],\ nulls_not_distinct:\ true,\ name:\ "test_unique_constraints_position_nulls_not_distinct"/ to match ...
  • UNIQUE NULLS NOT DISTINCT 構文は PostgreSQL 15 以降でのみサポート。
  • 14 以前ではアダプタがこの句を付けずに制約を作成し、schema dump からも nulls_not_distinct: true が消えるため、正規表現マッチが失敗していました。

対応内容

  • すでに同ファイル内にある test_schema_dumps_nulls_not_distinct と同様に、supports_nulls_not_distinct? を利用してアサーションを分岐。
    • supports_nulls_not_distinct? == true(PG 15+): nulls_not_distinct: true を期待。
    • それ以外(PG 10〜14): そのフラグが無い出力を期待、あるいはその部分の検証をスキップ。

これにより、PostgreSQL 10〜14 でもテストが落ちず、15 以降では従来どおり機能確認が行われます。


PostgreSQL 12 向けのテストガード

対象ファイル:

  • activerecord/test/cases/persistence_test.rb
  • activerecord/test/cases/dirty_test.rb
  • activerecord/test/cases/adapters/postgresql/enum_test.rb

1) 生成列 (generated columns) 関連

エラー例(PostgreSQL 11.16):

text
NoMethodError: undefined method 'virtual_stored_number' for an instance of Default

text
Failure:
DirtyTest#test_virtual_column_loaded_change_on_update:
Expected: [1050, 1400]
  Actual: nil
  • virtual_stored_number カラムは「生成列 (generated stored column)」で、PostgreSQL 12 以降でのみサポートされている機能。
  • テストスキーマではすでに supports_virtual_columns? を条件に、このカラムの作成を制御していましたが、
    • カラム自体がない環境(PG 10, 11 等)でも、テスト test_fills_auto_populated_columns_on_updatetest_virtual_column_loaded_change_on_update が無条件で実行されていたため、NoMethodError / 期待値不一致が発生していました。

対応:

  • 上記 2 テストを supports_virtual_columns? を使ってガードし、生成列をサポートしない環境ではテストをスキップするように変更。

2) ENUM 型の ALTER TYPE ... ADD VALUE のトランザクション制限

エラー例(PostgreSQL 11.16):

text
ActiveRecord::StatementInvalid: PG::ActiveSqlTransaction: ERROR:  ALTER TYPE ... ADD cannot run inside a transaction block
  • PostgreSQL 12 より前は、ALTER TYPE ... ADD VALUE をトランザクションブロック内で実行できません。
  • 該当テスト: PostgresqlEnumTest#test_schema_dump_added_enum_value

対応:

  • database_version を参照し、12 未満(< 12_00_00)の場合はこのテストをスキップするように修正。
  • この機能には既存の supports_*? プレディケートが無いため、バージョン数値を直接比較しています。

PostgreSQL 11 向けのテストガードとスキーマ修正

対象ファイル:

  • activerecord/test/schema/postgresql_specific_schema.rb
  • activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
  • activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb

1) トリガー定義の構文変更

PostgreSQL 10.21 でのスキーマロードエラー:

text
PG::SyntaxError: ERROR:  syntax error at or near "FUNCTION"
... test/schema/postgresql_specific_schema.rb:213
  • エラーの要因: CREATE TRIGGER ... EXECUTE FUNCTION 構文は PostgreSQL 11 で導入された新しい書き方。
  • PostgreSQL 10 では、従来の EXECUTE PROCEDURE を使う必要があります。

対応:

  • 問題のトリガーを EXECUTE FUNCTION から EXECUTE PROCEDURE に変更。
  • 同ファイル内の partitioned_insert_trigger でもすでに EXECUTE PROCEDURE を使っていたため、それに合わせた形になります。
  • EXECUTE PROCEDURE は 10〜18 まで後方互換で通るため、すべてのサポートバージョンでスキーマがロード可能になります。

2) ドメイン型配列 (arrays over domains) とパーティションテーブル制約

スキーマがロードできるようになった結果、今度はテスト側で以下のエラーが発生:

text
PG::UndefinedObject: ERROR:  type "postgresql_domain_base[]" does not exist
PG::UndefinedObject: ERROR:  type "postgresql_domain_nested_after_bulk[]" does not exist

および:

text
PG::InFailedSqlTransaction: ERROR:  current transaction is aborted, commands ignored until end of transaction block

詳細:

  • ドメイン型の配列は PostgreSQL 11 からサポート。
    • これを利用しているのが PostgreSQLAdapterTest 内の
      • test_load_additional_types_cascades_dependency_lookups
      • test_load_additional_types_cascades_dependency_lookups_after_initial_bulk_load などのテスト。
  • PostgreSQL 10 ではドメイン配列型自体が存在せず、その型に対する操作で PG::UndefinedObject が発生していました。
  • また PostgreSQLReferentialIntegrityTest#test_all_foreign_keys_valid_having_foreign_keys_with_partitioned_table について:
    • パーティションテーブル上の primary / foreign key 制約は PostgreSQL 11 以降の機能。
    • PostgreSQL 10 では CREATE TABLE の時点で「primary key constraints are not supported on partitioned tables」といったエラーが発生し、以後の SQL が失敗 (InFailedSqlTransaction) していました。
    • このテストはパーティションテーブル“上に”外部キーを張るケースをテストしており、「パーティションテーブルを参照する外部キー」(PostgreSQL 12 以降)までは利用していません。

対応:

  • 上記 3 つのテストは、ensure ブロックでクリーンアップを行います。
  • Minitest/SkipEnsure のルール上、ensure 内で skip を呼べないため、「実行時スキップ」ではなく「定義時ガード」を採用。

具体的には:

ruby
if ActiveRecord::Base.lease_connection.database_version >= 11_00_00
  class PostgreSQLAdapterTest < ActiveRecord::TestCase
    def test_load_additional_types_cascades_dependency_lookups
      ...
    end

    def test_load_additional_types_cascades_dependency_lookups_after_initial_bulk_load
      ...
    end
  end
end

のように、テストメソッド自体を「PostgreSQL 11 以上のときだけ定義する」形に変更しています
(実際のコードはこのイメージに沿った記述になっている、という理解で十分です)。

同様に、パーティションテーブル関連のテストについても database_version >= 11_00_00 を条件に定義するアプローチが取られています。


  1. 影響範囲・注意点
  • 影響範囲は テストコードとテスト用 PostgreSQL スキーマのみ です。
    • activerecord/lib 以下の本体コードには変更がありません。
    • アプリケーションや gem 利用者に対する挙動・互換性への影響はありません。
  • これにより:
    • PostgreSQL 10〜18 (Docker: postgres:10postgres:18) の全バージョンで rake test:postgresql が通ることが確認済みです。
    • CI が現在使っている PostgreSQL 18.4 では、変更されたテストファイルについて「実行回数・アサーション数・skip 数」が main ブランチと同一であり、CI 上で新たにテストがスキップされることもありません。
  • 今後、古い PostgreSQL バージョンでテストを手元実行したい場合、この PR のようなバージョンガードがある前提で、追加のテストを実装することが必要になります。
  • database_version を直接比較するパターン(特に ALTER TYPE ... ADD VALUE のような細かい仕様差)は、将来的に supports_*? プレディケートとして切り出す余地がありますが、現時点ではテスト内ローカルな判断にとどまっています。

  1. 参考情報 (あれば)

PostgreSQL 側の機能追加コミット:

PR の要点をまとめると、「Active Record がサポートすると宣言している PostgreSQL 10〜18 すべてに対して、テストスイートが現実に成功するよう、バージョンごとの機能差をテスト側で吸収した」という位置づけになります。


#57702 Don't autovivify the PostgreSQL listener on shutdown

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

  1. 概要 (1-2文で)
    PostgreSQL 用の ActionCable サブスクリプションアダプタで、shutdown 時に未使用の listener を新規生成してしまう不具合を修正し、Redis アダプタと同様に「既に存在する場合のみ shutdown する」挙動に統一した PR です。これにより、未購読・送信専用プロセスの終了時に不要なスレッド・DB接続が作られなくなります。

  1. 変更内容の詳細

問題となっていた既存コードは以下のようになっていました:

ruby
def shutdown
  listener
end

def listener
  @listener || @mutex.synchronize { @listener ||= Listener.new(self, executor) }
end

shutdown が private メソッド listener を呼ぶことで、メモ化されている listener が未生成 (@listenernil) の場合でも Listener.new が走ります。

  • Listener.new
    • 即座にバックグラウンドスレッドを起動
    • そのスレッド内で with_subscriptions_connection を呼び、新しい DB コネクションを開いて LISTEN ループに入る
  • つまり、「購読を一度もしていない」「送信専用の」アダプタであっても、shutdown を呼んだ瞬間に
    • 不要な Listener インスタンス
    • 不要な DB コネクション
    • 不要なバックグラウンドスレッド が生成され、その直後に shutdown される挙動になっていました。
  • さらに、そのスレッドは abort_on_exception = true で動くため、接続失敗などが予期しないタイミング・場所で例外として表面化する可能性もありました。

Redis アダプタでは同様のメソッドが既に以下のように書かれており、問題が発生していませんでした:

ruby
def shutdown
  @listener.shutdown if @listener
end

両方のアダプタとも initialize 内で @listenernil 初期化しているため、インスタンス変数は常に定義済みです。このため、@listener に直接アクセスしてガードするのが安全であり、かつ「自動生成しない」挙動になります。

今回の修正で、PostgreSQL アダプタ側の shutdown は次のように変更されました:

ruby
def shutdown
  @listener.shutdown if @listener
end

つまり:

  • すでに listener が作られている場合のみ shutdown を呼ぶ
  • 存在しない (@listenernil) 場合は何もしない(no-op)

となり、Redis アダプタと挙動が揃いました。

テスト (actioncable/test/subscription_adapter/postgresql_shutdown_test.rb) では:

  • PostgreSQL アダプタを生成
  • 一度も subscribe などを呼ばずに shutdown を実行
  • 実行後も @listenernil のままであることをアサート

という形で「autovivify(shutdown 時の自動生成)が起きないこと」を検証しています。
テスト内では with_subscriptions_connection をスタブしており、バグありのコードでは autovivified なスレッドが実 DB に触らないようにしています。修正後コードではそもそもスレッドが生成されず、テストがグリーンになります。


  1. 影響範囲・注意点

影響範囲:

  • 対象: ActionCable::SubscriptionAdapter::PostgreSQL を使っているアプリケーション
  • 特に影響があるケース:
    • ActionCable で PostgreSQL アダプタを使っているが、プロセス側では購読を行わない(ブロードキャスト専用/送信専用)ワーカー・プロセス
    • あるいは購読開始前にプロセスが終了する可能性があるケース

この変更により:

  • これらのプロセスのシャットダウン時に、不要なスレッド・DB接続が生成されなくなる
  • 終了時に「たまたま LISTEN 用コネクション確立に失敗し、abort_on_exception でプロセスが異常終了」といった予期しない挙動が避けられます
  • 既存の正常な利用(購読を開始しているプロセス)では、listener はすでに生成済みであり、@listener.shutdown が呼ばれる点は従来と同じで、互換性に問題はほぼありません

注意点:

  • もし独自コードで @listener に直接触っている/ライフサイクルを上書きしているような非常に特殊なケースがあれば、その挙動が Redis アダプタと同じ形に揃ったことを前提に再確認するとよいです。
  • 一方で、shutdown を「呼ぶことで listener を初期化する」ような依存関係は、今回の変更で成立しなくなりますが、そのような使い方はもともと意図されたものではありません。

  1. 参考情報 (あれば)
  • 対応する Redis アダプタの実装(挙動を揃えた参照先)
    actioncable/lib/action_cable/subscription_adapter/redis.rbshutdown 実装
  • 関連クラス:
    • ActionCable::SubscriptionAdapter::PostgreSQL::Listener
    • with_subscriptions_connection(LISTEN 用 DB コネクションを管理)
  • この修正は 1 行の置き換えのみで、動作仕様は「未使用アダプタの shutdown は no-op」という形に明確化された、と捉えると理解しやすいです。

#58042 Add missing ActiveSupport core extension and feature requires

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

  1. 概要 (1-2文で) Rails の各コンポーネントが Active Support のコア拡張メソッドや定数を 明示的に require していなかった問題をまとめて修正し、「特定の順序で require したときだけ動く」状態を解消する PR です。挙動そのものは変えず、ロード順に依存した NoMethodError / NameError を潰すための require 追加のみが行われています。

  1. 変更内容の詳細

共通テーマ

  • これまで多くのファイルが、
    • index_by, years, symbolize_keys!, to_sentence, deep_symbolize_keys
    • ActiveSupport::TimeZone, ActiveSupport::Duration, ActiveSupport::TimeWithZone など Active Support が提供する拡張や定数に依存しているにもかかわらず、対応するファイルを require していなかった
  • そのため、
    • 「アプリ全体を require "rails" でまとめてロード」する場合は、たまたまどこかで読み込まれるので動く
    • しかし、コンポーネント単位の利用 / cherry-pick require / ゲームや CLI などの軽量構成では NoMethodErrorNameError が出る
  • この PR では、その穴を埋める形で 不足している require を各ファイルに追加しています(コードロジックの変更はなし)。

コンポーネント別の具体的な修正

Action Pack

  1. ActionController::Parameters#fetch_values

    • Array#index_by を呼んでいるが、index_by を定義するコア拡張の require がなかった。

    • actionpack 単体ロードで:

      ruby
      require "action_controller"
      ActionController::Parameters.new(name: "F", age: 22).fetch_values(:name, :age)
      # => NoMethodError: undefined method `index_by'

      という状態になっていたのを、適切な require 追加で解消。

  2. ActionController::MethodNotAllowed

    • エラーメッセージ構築に Array#to_sentence を使用しているが、to_sentence のコア拡張を require していなかった。
    • require "action_controller/metal/exceptions" だけでこの例外を発生させると NoMethodError になっていた問題を修正。
  3. ActionDispatch::Cookies::PermanentCookieJar

    • 期限計算に 20.years を使っているが、Numeric#years を定義する Active Support の time 拡張を require していなかった。
    • require "action_dispatch" ベースの Rack アプリで permanent cookie を使うと undefined method 'years' になる問題を修正。

Action View

  1. time_zone_options_for_select / time_zone_select

    • ActiveSupport::TimeZone を参照しているが、これは active_support/time を require しないと autoload されない。
    • Action View 単体 (require "action_view") で time zone 関連ヘルパーを呼ぶと NameError になっていたのを、ActiveSupport::TimeZone を提供するファイルを明示的に require する形で修正。
  2. ActionView::Rendering / ActionView::Layouts

    • モジュール定義時に attr_internal_reader / attr_internal_writer を呼んでいるが、それらを定義する core_ext を require していなかった。
    • require "action_view" 直後にこれらのモジュールを参照すると例外が起きうる状態だったため、他のファイル同様に対応する core_ext の require を追加。
  3. ContentExfiltrationPreventionHelper

    • html_safe な定数を定義しつつ、mattr_accessor も利用しているが、これらの機能を提供する
      • 文字列の output_safety 拡張
      • mattr_accessor を定義する Active Support のモジュール の require 順が崩れているとエラーになっていた。
    • form_tag_helper.rb がこの helper を、自前の string/output_safety require より先に読んでいたため、 require "action_view/helpers/form_tag_helper" のみで NoMethodError が発生するケースを修正。

Action Cable

  • 「ブロードキャスト専用プロセス」(Job などから ActionCable.server.broadcast だけ行う構成)が、以下の順に NoMethodError を起こしていた:
    1. ログ出力で truncate が未ロード
    2. PubSub アダプタ解決で camelize が未ロード
    3. Redis アダプタ設定で deep_symbolize_keys が未ロード
  • 原因は、コア拡張をロードするトリガーが “Channel クラスの定義” に依存していたこと。
  • 対策として、Action Cable の
    • configuration
    • broadcasting
    • subscription_adapter/redis などに、必要な Active Support core_ext の require を明示的に追加し、「ブロードキャストだけ使う」パターンでも問題なく動くようにしている。

Active Job

  • v8.1.0 以降、シリアライザのインデックス化が eager になった影響で、

    • ActiveJob::Base をロードする前に
    • ActiveJob::Serializers を触る(カスタムシリアライザ登録の推奨パターン)と、
  • ActiveSupport::Duration, DateTime, ActiveSupport::TimeWithZone が未ロードで NameError になる状態だった。

    ruby
    require "active_job"
    ActiveJob::Serializers.serializers
    # => NameError: uninitialized constant ActiveSupport::Duration
  • 対象の serializer ファイル(duration_serializer.rb など)から、それぞれが依存する Active Support のクラス・拡張を明示的に require するよう修正。


Active Storage

  • ActiveStorage::Service#service_nameString#remove を使っているが、remove を追加するコア拡張が require されていなかった。
  • ActiveStorage::Service.configure のような Active Storage 単体利用時に、インストルメンテーションを伴う操作で必ず NoMethodError になる状態を、該当する core_ext を require することで解消。

Active Support

  1. require "active_support/time" が壊れていた問題

    • time_formats.rbActiveSupport::Ractors を参照するようになったが、その提供元を require していなかった。
    • さらに、その裏で core_ext/time/conversions.rbcore_ext/date/conversions.rbDATE_FORMATS 非推奨 shim が
      • ActiveSupport::Deprecation
      • ActiveSupport.deprecator を require なしで参照していた。
    • 結果として、require "active_support/time" 単体が NameError で落ちるようになっていたのを、すべて依存元を require する形で修正。
  2. ActiveSupport.error_reporter.set_context(foo: 1)

    • symbolize_keys! を使っているが、Hash#symbolize_keys! を定義する core_ext を require しておらず、どの standalone プロセスでも必ず undefined method 'symbolize_keys!' になる状態を修正。
  3. MessageVerifier#generate(data, expires_in: 3600)

    • advance を使った時間計算を行っているが、Time#advance のコア拡張を require していなかった。
    • たまたま呼び出し側で 1.hour などを使うと、その require 副作用で問題が隠れる、という「ロード順依存バグ」だったものを是正。
  4. ActiveSupport::ActionableError, ActiveSupport::Testing::FileFixtures, StructuredEventSubscriber, EventReporter::LogSubscriber

    • これらが class_attribute を使うが、active_support/core_ext/class/attribute を require していなかった。
    • 特に rspec-rails がこれらを直接 require するため、テスト環境限定で undefined method 'class_attribute' が噴出するケースがあった。
    • ActiveSupport::LogSubscriber が既に行っているのと同様に、class_attribute の core_ext を各ファイルで require。
  5. Redis cache store の接続引数の非推奨 shim

    • squish を使用しているが、String#squish の提供元を require しておらず、警告を出す代わりに undefined method 'squish' が出てしまう状態を修正。
  6. ActiveSupport::Deprecation#behavior = :notify

    • underscore を使うが、String#underscore の core_ext を require していなかった。
    • standalone な gem が自前の deprecator を定義して behavior = :notify を設定すると NoMethodError になっていたのを修正。
  7. MessagePack シリアライズ (serializer: :message_pack)

    • json_create オブジェクトのシリアライズで as_json を呼ぶが、対応する Active Support 拡張がロードされていないと undefined method 'as_json' になる。
    • キャッシュ層などから MessagePack を使うケースでエラーが出ないよう、必要な拡張を require。
  8. XmlMini / Hash#to_xmlDate#to_fs

    • XmlMiniDate#to_fs を使うが、それを定義する date + Active Support のフォーマット拡張がロードされていない場合に NoMethodError
    • require "active_support"; require "active_support/core_ext/hash/conversions" のような 公式ドキュメント通りの cherry-pick ルートでも落ちていたので、XmlMini 側で Date フォーマット関連の拡張を require して修正。

  1. 影響範囲・注意点
  • 既存アプリへの挙動変更は基本的にありません。
    • 追加されたのはすべて require のみで、メソッドの実装や内部ロジックは変えていません。
    • これまで「たまたまどこかで require されていた」拡張を明示的に require するようになっただけです。
  • 影響が出る可能性があるとすれば:
    • ロードされるファイル数がわずかに増えるため、極限まで起動時間を最適化しているケースでは差分が出る可能性があります(とはいえ +35 行程度の require であり、現実的には誤差レベル)。
    • これまで「例外が出ていた」パスが正しく動作するようになるため、テストの挙動が良い方向に変わる(例外が出なくなる)ことがあります。
  • コンポーネント単体利用(Action Pack だけ、Action View だけ、Active Job だけ、など)や、
    • Job からの Action Cable broadcast
    • Active Storage の低レベル API
    • Active Support の一部機能だけ cherry-pick のような使い方をしている場合は、これまで sporadic に発生していた NoMethodError / NameError が解消されるはずです。

  1. 参考情報 (あれば)
  • この PR は、同様のクラスの修正である以下のコミットのフォローアップと明示されています:
    • 5fe517563e
    • 12422d5d0a
  • PR の趣旨:
    • ファイルは自分が使うものを自分で require すべき」という原則を Rails コードベース全体に徹底することで、
    • ロード順依存のバグを排除し、
    • コンポーネント単位利用や gem / ライブラリからの cherry-pick 利用を安全にする、というものです。

#58132 Fix rubocop.yml indentation

マージ日: 2026/7/15 | 作成者: @jeromedalbert

  1. 概要 (1-2文で)
    Rails が rails new などで生成する .rubocop.yml テンプレートのインデント幅を、他の YAML テンプレートと同じ「半角スペース 2 個」に揃える修正です。前回 PR(#57252) のフォローアップとして、4 スペースになっていた部分を 2 スペースに直しています。

  1. 変更内容の詳細

対象ファイル:
railties/lib/rails/generators/rails/app/templates/rubocop.yml.tt

このテンプレートは、Rails アプリケーション生成時に .rubocop.yml として出力される元ファイルです。今回の変更は、その中のインデントを 4 スペース → 2 スペースに揃えるだけの細かなフォーマット修正です。

イメージとしては、例えば以下のような差分です(実際のルール名や内容は例示):

yaml
# 変更前 (4スペースインデント)
AllCops:
    NewCops: enable
    TargetRubyVersion: 3.3

# 変更後 (2スペースインデント)
AllCops:
  NewCops: enable
  TargetRubyVersion: 3.3

Rails が生成する他の YAML (database.yml, storage.yml など) は 2 スペースインデントが標準になっているため、それと一貫させるための修正です。


  1. 影響範囲・注意点
  • 新規に生成される Rails アプリのみが対象
    PR マージ後に rails new で作成されるアプリの .rubocop.yml のインデントが 2 スペースになります。既存アプリのファイルは自動では変更されません。

  • 動作上の影響は基本的にない

    • YAML としては 4 スペースも 2 スペースもどちらも有効なため、Rubocop の挙動自体は変わりません。
    • ただし、「インデント 2 スペース」を前提にした YAML の自動整形ツールやエディタ設定を使っている場合に、見た目や差分がより一貫したものになります。
  • 既存プロジェクトとの一貫性を取りたい場合

    • 既に生成済みの .rubocop.yml をこのテンプレートに揃えたい場合は、インデントを 2 スペースに統一しておくと、将来的なテンプレート更新の取り込みや、チーム内のスタイル統一がしやすくなります。

  1. 参考情報 (あれば)

#58043 Respect a limit set on the relation in rails query

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

  1. 概要 (1-2文で)
    bin/rails query コマンドで、すでに limit が設定されている Active Record リレーションを評価した際に、その limit がページネーション用の件数 (per + 1) に上書きされてしまう問題を修正し、ユーザー指定の limit をそのまま尊重するようにした PR です。これにより Post.limit(5) のようなクエリで、期待どおり 5 件だけが取得されるようになります。

  1. 変更内容の詳細

何が問題だったか

bin/rails query は、結果をページング表示するために内部で「1ページあたりの件数 + 1 件」(per + 1)を使ってクエリに LIMIT を付与していました。
その際、すでに limit が指定されているリレーション(例: Post.limit(5))に対しても一律にページサイズの LIMIT を上書きしてしまっていたため、次のような挙動になっていました:

bash
bin/rails query "Post.limit(5)"
# 実際には LIMIT 101 相当が付与され、最大 101 件返ってきてしまう

これは、--sql オプションで生 SQL を投げた場合にユーザー指定の LIMIT が尊重される挙動 (execute_sql) とも不整合でした。

何を修正したか

railties/lib/rails/commands/query/query_command.rb にて、Relation にすでに limit が存在するかどうかを判定し、存在する場合はページネーション用の limit を付け足さないようにしました。

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

ruby
relation = eval(expression) # "Post.limit(5)" など

if relation.is_a?(ActiveRecord::Relation)
  if relation.limit_value # すでに limit が設定されている場合
    # そのまま実行。表示は別途 truncate などで制御。
    records = relation.to_a
  else
    # limit がない場合のみ、ページサイズ (per + 1) を付与
    records = relation.limit(per + 1).to_a
  end
end

実際には「クエリ実行時の件数制御」と「画面・出力上のページングのための表示件数トランケーション」が分離され、「クエリに埋め込まれた LIMIT は尊重しつつ、表示上は paginate/truncate する」という構造に寄せられています。

テストの追加

railties/test/commands/query_test.rb にテストが追加され、以下のようなケースがカバーされています:

  • bin/rails query "Post.limit(5)" 実行時に、クエリオブジェクト側の limit が変更されず、その件数だけが取得・表示されること
  • ページング表示は引き続き機能しつつも、ユーザー指定の limit を侵害しないこと

(実際のテストコードでは rails コマンドの出力をキャプチャして件数や SQL が期待どおりかどうかを検証していると考えられます。)


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

    • bin/rails query コマンドで、Active Record リレーション(Post.where(...).limit(5) など)を評価して使っているケース。
    • 特に、クエリ文字列の中に limit を含めて件数をコントロールしていた人にとって、今回からはその limit が正しく反映されるようになります。
  • Before / After の動き:

    bash
    bin/rails query "Post.limit(5)"
    
    # Before:
    #   内部で LIMIT がページサイズ (例: 101) に置き換えられ、
    #   最大 101 件まで返ってきていた。
    #
    # After:
    #   クエリに指定された LIMIT 5 がそのまま使われ、5 件のみ取得。
    #   表示上のトランケーションは別レイヤーで行われる。
  • 互換性上の注意:

    • これまで「ページサイズ分取れればいい」という前提で bin/rails query を使い、明示的な limit 指定をしていた場合、取得件数が減る可能性があります(意図は limit(5) なので、本来の挙動に修正されたといえる)。
    • もし「ページング件数に任せたい」場合は、クエリ内に limit を書かず、where, order などのみに留めるべきです。
  • --sql との一貫性:

    • bin/rails query --sql "SELECT * FROM posts LIMIT 5" では元々 LIMIT 5 が尊重されていましたが、今回の修正により Active Record パスでも同様に LIMIT が尊重されるようになり、両者の挙動が揃いました。

  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/58043
    • タイトル: Respect a limit set on the relation in rails query
  • 関連するコード:
    • railties/lib/rails/commands/query/query_command.rb
    • railties/test/commands/query_test.rb

運用上は、「bin/rails query で件数を制御したい場合は、今後は素直に limit を使えばよくなった」と捉えると分かりやすいです。


#58063 Test that reverse_merge takes the default from the other hash

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

  1. 概要 (1-2文で)
    Hash#reverse_merge / Hash#reverse_merge! が「レシーバではなく、引数側のハッシュの default / default_proc を引き継ぐ」という既存仕様をテストで明示的に保証する PR です。実装コードの変更はなく、テスト追加のみです。

  1. 変更内容の詳細

何をテストしたか

reverse_mergereverse_merge! の内部実装は、ざっくり言うと以下のようなイメージです。

ruby
def reverse_merge(other_hash)
  other_hash.merge(self)
end

この実装だと、「結果ハッシュ」が other_hash をベースとして構築されるため、結果の default / default_procother_hash のものが引き継がれます。

PR の説明にある挙動は次の通りです。

ruby
left  = Hash.new(:left)
right = Hash.new(:right)

left.reverse_merge(right)[:missing]   # => :right
left.reverse_merge!(right)
left[:missing]                        # => :right
  • left.reverse_merge(right) の戻り値は「右側 (right) を土台にしたハッシュ」
    ⇒ 未定義キー :missing の default は :right
  • 破壊的な reverse_merge! も同様に、結果として leftright の default を持つようになる

この挙動自体は元々そうなっていたものの、テストでカバーされていなかったため、今回の PR で以下を追加しています。

  • Hash#reverse_merge
    • 引数側ハッシュの default を引き継ぐこと
    • 引数側ハッシュの default_proc を引き継ぐこと
  • Hash#reverse_merge! でも同様の挙動になること

対象はあくまで「プレーンな Hash の core extension」であり、HashWithIndifferentAccess には手を加えていません(こちらは独自の reverse_merge! / replace 実装と default のセマンティクスを持つため)。

テストファイルの変更:

  • activesupport/test/core_ext/hash_ext_test.rb に 26 行のテストコードを追加

  1. 影響範囲・注意点
  • プロダクションコードの変更は一切なく「挙動を固定するためのテスト追加のみ」です。
  • 既に動いていた仕様をテストで明文化しただけなので、Rails をアップデートしてもこの PR 自体による挙動の変化はありません。
  • 逆に言うと、「reverse_merge はレシーバの default を保つだろう」と期待していた場合、その期待は間違いであり、この PR によって「今後もそのようにはならない」ことが契約としてより強く保証されます。
    • レシーバ側の default を維持したい場合は、merge 系を使う順序や実装を見直す必要があります。
  • HashWithIndifferentAccess はこの PR の対象外なので、その reverse_merge! の default の扱いは別物です。Hash 前提のテストと混同しないように注意が必要です。

  1. 参考情報 (あれば)
  • 該当 PR: #58063 “Test that reverse_merge takes the default from the other hash”
  • 関連議論: #58056(reverse_merge の default 挙動がテストされていないことが話題になったスレッド)
  • ActiveSupport の Hash#reverse_merge は、「レシーバよりも引数側を優先する merge」という API ですが、default の引き継ぎ元も「引数側」である点が直感とズレる可能性があるため、この PR によるテスト追加は仕様理解の上でも参考になります。

#58129 Fix Rubocop violation in config/puma.rb - Approach 1

マージ日: 2026/7/15 | 作成者: @jeromedalbert

  1. 概要 (1-2文で)
    Rails アプリ生成時の config/puma.rb テンプレートに含まれていた RuboCop (Layout/SpaceInsideArrayLiteralBrackets) 違反を修正し、rails new myapp --main で作成したアプリで bin/rubocopbin/ci が失敗しないようにした PR です。
    前の PR (#57424) による変更で入り込んだスタイル違反を、テンプレート側で1行だけ修正しています。

  1. 変更内容の詳細
  • 対象ファイル:
    railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt

  • 変更点は1行のみで、「配列リテラルのブラケット内のスペース」を削除し、Rubocop の Layout/SpaceInsideArrayLiteralBrackets に準拠するようにしています。

    具体的には、(イメージですが) 例えば:

    ruby
    # 修正前(Rubocop違反)
    workers [ 2, 4 ]

    のように [] の内側にスペースが入っていたものを:

    ruby
    # 修正後(Rubocop準拠)
    workers [2, 4]

    のようにブラケット直後/直前のスペースを削除する形に直した、という内容です。

    実際の行が何であれ、「ブラケット内の先頭/末尾の不要な空白をなくした」というのが本質的な変更です。

  • この PR は、別アプローチの PR (#58130) をクローズして採用された「Approach 1」の方であり、#57424 で導入された Rubocop 違反をピンポイントに修正するフォローアップになっています。


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

    • rails new myapp --main で生成される config/puma.rb の内容がわずかに変わります(スタイルのみ)。
    • これにより、新規アプリで実行する:
      • bin/rubocop
      • bin/ci が、デフォルト状態で Rubocop のスタイル違反によって落ちることがなくなります。
  • 既存アプリへの影響:

    • 既に生成済みアプリの config/puma.rb は自動では変更されないため、既存アプリで同じ Rubocop 違反が残っている場合は、手動で同様の修正(ブラケット内の不要な空白削除)を行う必要があります。
    • 実行時の挙動や Puma の設定値自体は一切変わらず、あくまでコードスタイルのみの変更です。
  • Rubocop 設定との関係:

    • デフォルトの Rails/Rubocop 設定に合わせた修正のため、独自に Layout/SpaceInsideArrayLiteralBrackets を無効化・緩和していない限り、PRマージ後のテンプレートの方が「Rails 標準設定に沿った書き方」となります。

  1. 参考情報 (あれば)

#55410 Introduce ActionView::Helper::NavigationHelper

マージ日: 2026/7/15 | 作成者: @seanpdoyle

  1. 概要 (1-2文で)
    button_to / link_to 系のヘルパー(link_to_unless_current, link_to_unless, link_to_if を含む)が、新設の ActionView::Helpers::NavigationHelper モジュールに切り出されました。挙動の変更はほぼなく、主に内部構造の整理・責務分離が目的のリファクタリングです。

  1. 変更内容の詳細

新モジュール ActionView::Helpers::NavigationHelper

  • これまで ActionView::Helpers::UrlHelper に定義されていた以下のメソッドが、ActionView::Helpers::NavigationHelper に移動しました:
    • link_to
    • button_to
    • link_to_unless_current
    • link_to_unless
    • link_to_if
  • それに付随する内部的な private メソッド群も、呼び出し元と同じモジュール(NavigationHelper)側に移動しています。

Rails 全体としては、ActionView::HelpersActionView::Base に mixin される構造は変わらないため、ビューからの呼び出し方は従来通りです。

erb
<%= link_to "Home", root_path %>
<%= button_to "Delete", post_path(@post), method: :delete %>

といったコードはそのまま動作します。

URL / ルーティング周りとの接続

  • action_dispatch/routing/url_for.rb などの関連ファイルで、url_for との結合を保つための微修正(1行レベルの変更)が入っています。
  • ルーティングガイド (guides/source/routing.md) とヘルパーガイド (guides/source/action_view_helpers.md) も、内部構造の変更に合わせて記述が微調整されていますが、API の使い方は変わっていません。

テスト構成の変更

  • NavigationHelper 用の新しいテストファイル:
    • actionview/test/template/navigation_helper_test.rb(約 1000 行)
  • 従来 UrlHelper に紐づいていた link_to / button_to 周りのテストは、ほぼそのまま navigation_helper_test 側に移されており、url_helper_test からは同等量が削除されています。
    → 挙動はテストでしっかり担保されており、「URL 生成」と「ナビゲーション(リンク/ボタン)」のテストが分離された、という整理です。

Railtie / 設定まわり

  • actionview/lib/action_view/helpers.rb および actionview/lib/action_view/railtie.rb で、新モジュールが読み込まれるように 1〜2 行レベルの追記・差し替えが行われています。
  • railties/test/application/configuration_test.rb も 6 行ずつの変更があり、NavigationHelper を前提にした設定テストへ更新されています。

  1. 影響範囲・注意点

アプリケーション開発者への影響

  • 通常のアプリ/ビューコードで link_to / button_to などを使っているだけなら、基本的に影響はありません
  • パブリック API としてのメソッドシグネチャや、link_to / button_to の仕様は変わっていません(PR 説明およびテスト量から見て、挙動互換を重視したリファクタリング)。

メタプログラミング・Monkey patch をしている場合の注意

以下に当てはまる場合は、影響が出る可能性があります。

  1. ActionView::Helpers::UrlHelper に対して prepend / include / alias_method_chain 的なパッチを当てて、link_to / button_to を上書きしている
  2. UrlHelper 内部の private メソッドに直接依存していた
  3. ActionView::Base への include 順序(どの Helper が先に読み込まれるか)に依存したコードを書いている

この PR では:

  • link_to / button_to の定義モジュールが UrlHelper から NavigationHelper に変わりました。
  • それに伴い、関連する private メソッドも NavigationHelper 側に移っています。

そのため、例えば次のようなコードは動かなくなる(または想定外の場所にパッチがあたる)可能性があります:

ruby
# 以前:UrlHelper にパッチを当てていた
module MyLinkPatch
  def link_to(name = nil, options = nil, html_options = nil, &block)
    super("[patched] #{name}", options, html_options, &block)
  end
end

ActionView::Helpers::UrlHelper.prepend(MyLinkPatch)

上記のような場合、今後は NavigationHelper に対してパッチを当てる必要があります:

ruby
ActionView::Helpers::NavigationHelper.prepend(MyLinkPatch)

private メソッド境界について

  • PR 本文にもある通り、ActionView::Helpers モジュールは最終的にすべて ActionView::Base に混ぜ込まれるため、Ruby の private スコープは実質あまり意味を持っていませんでした。
  • 今回、private メソッドが呼び出されるモジュール内にきちんと移されたことで、「どのモジュールが何を責務として持つか」が多少クリアになっています。
  • 内部実装に依存していた場合、メソッドの「属するモジュール」が変わる可能性がある点に注意が必要です。

  1. 参考情報 (あれば)

#58126 [RF-Docs] [ci-skip] Update the Rails Generators guide (#57371)

マージ日: 2026/7/15 | 作成者: @p8

  1. 概要 (1-2文で)
    Rails Generators ガイド(guides/source/generators.md)を大きく書き換え・再構成したドキュメント更新PRです。Rails Foundation のドキュメントプロジェクトの一環で、Generators の説明を最新かつ体系的な内容に整えることが目的です。

  1. 変更内容の詳細

※コード上の挙動変更ではなく、ガイド内容(文章・サンプルコード)の更新です。そのため、以下は「どういう知識が追加/整理されたか」という観点の解説になります。

  • ガイド全体の大幅な改訂

    • 追加 317 行 / 削除 153 行と差分が大きく、章立てや説明順序を含めて再構成されています。
    • Rails Generators の概要から、カスタムジェネレータの作り方、テンプレートやフックの仕組みまで、一連の流れで理解しやすい構成に整理されたと考えられます。
    • ドキュメント品質向上(表現の統一、例のアップデート、不要な重複の削除など)が主眼です。
  • Rails Foundation ドキュメントプロジェクト対応

    • PR タイトルにある通り、Rails Foundation によるドキュメント整備プロジェクトの一部です。
    • 既存ガイド(別の Generator 関連ガイド)を統合して一つのガイドにまとめる作業の一環であり、全体的に他ガイドとの整合性が取られた形になっているはずです。
  • 「Application Templates」「Rails Generator API」セクションの統合

    • 説明文にもある通り、以下の 2 セクションは他ガイドからマージされており、この PR では「レビュー対象外」と明記されています。
      • Application Templates
      • Rails Generator API
    • これらはすでに別途フルレビュー済みの内容であり、本 PR では「ジェネレータガイドに組み込んだ」ことが主な変更点です。
    • 実質的には、Rails アプリケーションの雛形を自動生成・カスタマイズするためのテンプレート機能や、Generator クラス/API の詳細な使い方が、Generators ガイドの中で一貫して読めるようになったと考えられます。
  • サンプルコード・実例の刷新(推測される内容)

    • 変更行数の多さとガイド性質から、以下のようなサンプルが整理・追加されている可能性が高いです:
      • rails generate コマンドの基本例
        bash
        bin/rails generate model Article title:string body:text
      • カスタムジェネレータの作成例
        ruby
        # lib/generators/report/report_generator.rb
        class ReportGenerator < Rails::Generators::NamedBase
          source_root File.expand_path("templates", __dir__)
        
          def create_report_file
            template "report.rb.erb", "app/reports/#{file_name}_report.rb"
          end
        end
      • source_root, template, copy_file, migration_template などの Thor::Actions を使ったファイル生成例
      • Hooks / invoke / revoke の使い方、Rails プロジェクト内でのジェネレータの拡張・上書き方法
    • これらの例が、最新の Rails のディレクトリ構成やコマンド (bin/rails) に合わせて整理されていると考えられます。
  • 文書スタイル・記述の更新

    • Rails ガイド全般のスタイルガイドに沿って、見出し構造、注意書き、補足の書き方などが統一された可能性があります。
    • 古くなった記述(古い Rails バージョン、script/rails のようなレガシーなコマンド例など)が最新の表現に置き換わっていると思われます。

  1. 影響範囲・注意点
  • ランタイム挙動への影響はなし

    • 変更ファイルはガイド (guides/source/generators.md) のみであり、Rails 本体のコードやテストは変更されていません。
    • そのため、既存アプリケーションやジェネレータの動作には影響しません。
  • 開発者への影響(知識面)

    • Generators 周りのドキュメントが整理・充実したことで、以下のような場面で参照価値が高まります:
      • Rails 標準ジェネレータの挙動を正しく理解したいとき
      • 自作ジェネレータや plugin のジェネレータを実装・メンテナンスしたいとき
      • Application Template を使って社内テンプレートやブートストラッププロジェクトを整備したいとき
    • 既存のジェネレータ関連記事・ブログと内容が食い違う場合は、本ガイドがより新しいソースとして参考になります。
  • レビュー対象外セクションへの注意

    • 「Application Templates」「Rails Generator API」はこの PR ではレビュー対象外とされていますが、すでに別ルートでのレビューを経ているため、そのまま利用して問題ない前提です。
    • もしこれらのセクションに関して疑問点や不整合を見つけた場合は、過去の関連 PR / issue を併せて確認すると経緯が追いやすいと思われます。

  1. 参考情報 (あれば)
  • 該当ファイル:
    • guides/source/generators.md
  • 関連トピック(Rails ガイド内想定):
    • Rails コマンドライン (rails / bin/rails)
    • Rails Engine / Plugin の作成
    • Thor / Rails::Generators API
    • Application Templates (rails new my_app -m template.rb のような利用方法)

実務的には、「カスタムジェネレータを書きたい」「アプリケーションひな型を自動生成したい」「既存ジェネレータをフック/上書きしたい」といった用途の際に、このガイドの最新版を一度通読しておくと設計・実装のベース知識として有用です。


#57371 [RF-Docs] [ci-skip] Update the Rails Generators guide

マージ日: 2026/7/15 | 作成者: @bhumi1102

  1. 概要 (1–2文で)
    Rails Foundation のドキュメント刷新プロジェクトの一環として、「Rails Generators ガイド(generators.md)」が大幅に書き直されました。既存内容の整理・拡張に加え、他ガイドから統合されたセクションを含めて、ジェネレータの使い方・カスタマイズ方法がより体系的に説明されています。

  1. 変更内容の詳細

※PR 本文から分かる範囲と、Rails の一般的なジェネレータ機構を前提にした解説です。実際の文面は guides/source/generators.md の最新版を確認してください。

全体構成の見直し・拡張

  • guides/source/generators.md が +317 / -153 行と大きく書き換えられており、章構成や説明順序が整理された可能性が高いです。
  • 典型的には次のような構成に整理されていると考えられます:
    • 基本的なジェネレータの使い方 (rails generate / rails g)
    • Rails に用意されている主要なジェネレータ一覧
    • 生成されるファイル・ディレクトリ構成の解説
    • 独自ジェネレータの作成方法
    • テンプレートのカスタマイズ
    • (別ガイドから統合された)Application Templates
    • (別ガイドから統合された)Rails Generator API

代表的な内容(想定されるアップデート点)

1) 基本的な rails generate の説明の拡充

  • 典型的なコマンド例:
    bash
    bin/rails generate model Article title:string body:text published:boolean
    bin/rails generate controller Articles index show new create
    bin/rails generate scaffold Post title:string body:text
  • これらが生成するファイルの一覧や、それぞれがどのレイヤ(モデル・コントローラ・ルーティング・ビュー・テスト・マイグレーション)に影響するか、といった説明がより明示的になっている可能性があります。

2) Rails 標準ジェネレータの整理

  • 代表的なジェネレータ(例):
    • model, controller, scaffold, migration
    • job, mailer, channel, task など
  • それぞれのオプションや、--skip-* 系オプション(--skip-routes, --skip-migration, --no-test-framework など)の使い方が、現行 Rails バージョンに合わせて更新されていると考えられます。

3) 独自ジェネレータ作成ガイドの改善

  • lib/generators 配下に独自ジェネレータを作るパターンが、より丁寧に解説されていると考えられます。

    例: lib/generators/admin_scaffold/admin_scaffold_generator.rb

    ruby
    # lib/generators/admin_scaffold/admin_scaffold_generator.rb
    class AdminScaffoldGenerator < Rails::Generators::NamedBase
      source_root File.expand_path("templates", __dir__)
    
      def create_controller
        template "controller.rb.tt", File.join("app/controllers/admin", class_path, "#{file_name.pluralize}_controller.rb")
      end
    
      def create_views
        available_views.each do |view|
          template "views/#{view}.html.erb.tt",
                   File.join("app/views/admin", file_name.pluralize, "#{view}.html.erb")
        end
      end
    
      private
    
      def available_views
        %w[index show new edit]
      end
    end
  • NamedBase, Base, Thor との関係、source_root の指定、template, copy_file, directory などの Thor::Actions の利用例が具体的に示されている可能性があります。

4) テンプレートファイル (*.tt) の扱い

  • ERB ベースのテンプレート (.rb.tt, .html.erb.tt など) によるコード生成例が整理され、@class_name, file_name, class_path など、ジェネレータから利用できる変数の説明がされていることが多いです。

    例: controller.rb.tt

    ruby
    class <%= class_name.pluralize %>Controller < ApplicationController
      def index
        @<%= file_name.pluralize %> = <%= class_name %>.all
      end
    end

5) Application Templates セクションの統合

  • PR 説明によると、「Application Templates」セクションは別ガイドから移植され、既にレビュー済みのため今回再レビュー対象外です。

  • 内容としては、rails new 実行時に -m オプションで Ruby スクリプトを指定し、アプリ生成プロセスをフックする機能の解説が含まれます。

    例:

    bash
    rails new blog -m https://example.com/rails_template.rb

    テンプレート内での generate, rails_command, gem, environment といった DSL の使い方が記載されていることが多いです。

6) Rails Generator API セクションの統合

  • こちらも別ガイドから統合され、既にレビュー済み。
  • Rails::Generators::BaseRails::Generators::NamedBaseRails::Generators::AppBase などのクラスと、利用可能なメソッドの一覧・意味がまとまっていると考えられます。
    • argument, class_option
    • invoke, invoke_all
    • no_color!, remove_file, empty_directory など
  • 「API」という名前から、単純な使い方だけでなく、内部的なフックポイントや、他のジェネレータを呼び出す方法 (invoke "model", [name] など) も説明されている可能性があります。

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

    • コード自体には一切変更がなく、ガイド(ドキュメント)のみの変更です。
    • 既存アプリやジェネレータの挙動には影響しません。
    • ただし、「正しい/推奨」とされる使い方やベストプラクティスの説明が最新状態に更新されているため、今後の学習・社内標準ドキュメント等はこの新ガイドに合わせるのが望ましいです。
  • 注意点:

    • 「Application Templates」と「Rails Generator API」は既に別ガイドとしてフルレビュー済みの内容が統合されているため、今回の PR では主に周辺テキスト・構成だけが調整されている可能性があります。
    • 以前に application_templates.mdgenerator_api.md のような別ガイドを参照していた場合、今後は generators.md 一つに統合されているかを確認してください。
    • 古いブログ記事や Qiita 記事等と今回のガイド内容が食い違う場合は、Rails Guides の方が現時点での公式・最新情報です。

  1. 参考情報 (あれば)

#58124 Fix Relation#one? doc example calling any?

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

  1. 概要 (1-2文で)
    ActiveRecord::Relation#one? のドキュメント中のサンプルコードが誤って any? を呼んでいた問題を修正し、説明文やコメントの文言・整形も合わせて調整したドキュメント専用のPRです。コードの挙動自体には一切変更はありません。

  1. 変更内容の詳細

以下は、activerecord/lib/active_record/relation.rb 内の Relation#one? / Relation#any? に関するドキュメントコメントのみの修正です。

(1) one? のサンプルが any? を呼んでいたのを修正

元のドキュメント例(問題のある状態・イメージ):

ruby
# Returns true if there is exactly one record.
#
#   posts.one?(Post)    # => true if exactly one record
#   posts.any?(Comment) # => false

one? の説明セクション内の2つ目の例が any? になっており、ドキュメントとして「one? の使い方」を示せていませんでした。

修正後はおそらく次のようになります(意図される形):

ruby
# Returns true if there is exactly one record.
#
#   posts.one?(Post)    # => true if exactly one record matches
#   posts.one?(Comment) # => false

つまり、2つ目の行も one? を使うように変更されています。

(2) 「records matches」の文言を「record matches」に統一

any? / one? 双方の説明文にあった以下のような文言:

  • "returns true if at least one records matches"
  • "returns true if exactly one records matches"

のような、英語として不自然な「records matches」が、単数形で文法的に正しい

  • "returns true if at least one record matches"
  • "returns true if exactly one record matches"

といった形に修正されています。

これにより、any? / one? の説明がどちらも英語として正しく読みやすくなります。

(3) # => コメントの整列

any? の使用例の # => の位置と、one? の例の # => の位置がずれていたのを揃えています。例:

ruby
# before (イメージ)
posts.any?(Post)      # => true
posts.any?(Comment)   # => false

# after (イメージ)
posts.any?(Post)      # => true
posts.any?(Comment)   # => false

※変更内容としてはスペース数の調整だけで、意味は変わりませんが、ドキュメントの可読性を上げています。

(4) any? ドキュメント内の重複した空コメント行を削除

any? のドキュメントコメント中に、# のみの空行が重複して存在していた箇所を1つ削除し、余計な空行をなくしています。これも振る舞いには影響せず、スタイル調整に留まります。


  1. 影響範囲・注意点
  • 実行時の挙動への影響:

    • Ruby / Rails のコードロジック (Relation#one?, Relation#any? の実装) は一切変更されていません。
    • 本PRは「ドキュメント専用」(Documentation-only) です。
  • 影響範囲:

    • Rails の API ドキュメントおよび relation.rb 内コメントを参照する開発者が対象です。
    • 既に one? / any? を利用しているアプリケーションコードに影響はありません。
  • 注意点(ドキュメントを読む側として):

    • これまでのドキュメントのまま読むと、one? の説明セクションに any? の例が混ざっており、one? の挙動に誤解を招く可能性がありました。
    • 今後のバージョンでは、one? のセクションは正しく one? だけを例示する形になります。

  1. 参考情報 (あれば)
  • 関連メソッドの概要:

    • Relation#any?
      • 条件にマッチするレコードが1件以上存在するかどうかを返します。
      • 例: posts.any? / posts.any?(Post) / posts.any? { |post| post.published? }
    • Relation#one?
      • 条件にマッチするレコードが「ちょうど1件」存在するかどうかを返します。
      • 例: posts.one? / posts.one?(Post) / posts.one? { |post| post.featured? }
  • PRメタ情報:

    • PR番号: #58124
    • マージ日時: 2026-07-15T12:05:58Z
    • 変更ファイル: activerecord/lib/active_record/relation.rb のみ(+4 / -5)

#58117 Raise for empty shards on connected_to_all_shards

マージ日: 2026/7/14 | 作成者: @eileencodes

  1. 概要 (1-2文で)
    connected_to_all_shards を、実際にはシャーディングされていないアプリ(shard 設定が空のケース)で呼び出した場合に、静かに動作するのではなくエラーを送出するように変更した PR です。
    「シャーディングされていない構成で connected_to_all_shards を使う」ことを明示的に不正利用として扱う方向に仕様が整理されています。

  1. 変更内容の詳細

2-1. コアの仕様変更

対象: ActiveRecord::ConnectionHandling#connected_to_all_shards まわりの処理

これまで:

  • connected_to_all_shards は、内部的に shard_keys(アプリケーションで定義されているシャードのキー一覧)を前提に動いていた。
  • しかしアプリ側が実際にはシャーディングされておらず、shard_keys が空配列のままでも、エラーにはならず「何も接続を切り替えない」状態でブロックを実行できてしまう余地があった。

変更後:

  • アプリがシャーディングされておらず shard_keys が空の場合に connected_to_all_shards を呼び出すと、例外を発生させるようになった。
  • 「デフォルト接続だけを対象に connected_to_all_shards が動く」ような仕様にはしない、という判断が明確にされた。

PR 説明文のポイント:

  • 「アプリが sharded である」と言うためには、少なくとも 2 つの異なるスキーマ(もしくは DB)を参照する接続設定が必要:
    • 1 つはルーター (router)
    • 1 つは shard(DB の 1x パーティションを表す)
  • それが満たされていない状態で connected_to_all_shards を使うのは誤用なので、黙って動かすのではなく「エラーで気付かせる」挙動にする。

疑似コードイメージ(簡略化・概念的なもの):

ruby
def connected_to_all_shards(role: ActiveRecord::Base.current_role, &block)
  shard_keys = self.shard_keys_for(role) # 実際には内部的な取得処理

  if shard_keys.empty?
    raise ArgumentError, "connected_to_all_shards cannot be used because no shards are configured"
  end

  shard_keys.each do |shard_key|
    connected_to(role: role, shard: shard_key) do
      yield
    end
  end
end

実際の PR では 4 行程度の軽微な変更ですが、ロジックとしてはおおよそ上記のようなチェックが追加されたと考えてよいです。

2-2. CHANGELOG の更新

activerecord/CHANGELOG.md に、以下のような内容が追記されています(意訳):

  • connected_to_all_shards を、シャードが 1 つも構成されていないアプリケーションで呼び出した場合にエラーを発生させるようにした。

これにより、将来バージョンにアップデートした際に挙動変更に気付きやすくなっています。

2-3. テストの追加・修正

activerecord/test/cases/shard_keys_test.rb にテストが追加・修正されています。

主なポイント:

  • シャードが定義されていない環境で connected_to_all_shards を呼んだときに、例外が発生することを検証するテストが追加。
  • 既存の「シャードがある」ケースについても、今回の仕様変更を踏まえて期待挙動が確認されるように微調整。

  1. 影響範囲・注意点

3-1. 影響が出る可能性があるケース

以下に当てはまるアプリケーションは挙動が変わります:

  • database.yml(もしくは DB 設定)上は実際にはシャーディングをしていない:
    • シャード用の接続設定がない、あるいは 1 つしかない。
  • それにもかかわらず、コード上で connected_to_all_shards を呼び出している。

このようなコードは、以前は「何も起きないけど一応動いていた」可能性がありますが、今後は 例外が発生してアプリケーションが落ちる ようになります。

典型的な問題例:

ruby
# 実際には shard 設定がないのに…
ActiveRecord::Base.connected_to_all_shards do
  User.some_maintenance_task
end

→ この PR 適用後は「シャードが設定されていない」旨のエラー(ArgumentError など)が発生するようになります。

3-2. どう直せばよいか

  • そもそもシャーディングしていないアプリなら:
    • connected_to_all_shards を使うのをやめる。
    • 代わりに通常の接続コンテキスト (connected_to なし、あるいは connected_to(role: ...)) を使う。
  • 将来的にシャーディングを導入する予定で先行して書いていたコードなら:
    • 実際に複数スキーマ/複数 DB のシャード設定を用意する。
    • あるいは、導入前は以下のようにガードを入れておく:
ruby
if ActiveRecord::Base.respond_to?(:shard_keys) && ActiveRecord::Base.shard_keys.present?
  ActiveRecord::Base.connected_to_all_shards do
    # shard 前提の処理
  end
else
  # 非 sharded 環境用のフォールバック処理
end

ただし、shard_keys の公開インターフェースはバージョンによって異なる可能性があるため、実際にはリリースノートや API ドキュメントを確認した上で、利用できるメソッドに合わせてガードを書く必要があります。

3-3. ライブラリ・エンジン開発者への影響

  • gem や Rails Engine で connected_to_all_shards を内部的に呼び出している場合:
    • 利用先アプリが non-sharded である可能性があるので、この変更によってエラーが出るようになります。
    • そのため、「シャードが存在すること」を前提にするのか、「シャードが無い環境では別コードパスにする」のか を明確化し、条件分岐を追加する必要があります。

  1. 参考情報 (あれば)
  • この PR は以下の issue をクローズしています:
    https://github.com/rails/rails/issues/58114
    → 「シャードがない場合は default だけを対象に動いてほしいのでは?」という意見に対し、「その場合そもそもアプリは sharded ではないので、connected_to_all_shards を成功させるのは不自然」という判断が示されています。
  • 関連する API:
    • ActiveRecord::Base.connected_to_all_shards
    • マルチ DB / シャーディング周辺の設定 (database.ymlshards: セクションなど)
  • 実運用上は、「シャーディングを使わないアプリでは connected_to_all_shards は使わない」という設計方針がより強くなった、と理解しておくとよいです。

#58055 Use NOT ENFORCED toggle in check_all_foreign_keys_valid! on PostgreSQL 18.4+

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

  1. 概要 (1-2文で)
    Rails の ActiveRecord::ConnectionAdapters::PostgreSQLAdapter#check_all_foreign_keys_valid! が、PostgreSQL 18.4 以降では NOT ENFORCED / ENFORCED トグルを用いて外部キーの再検証を行うようになり、スーパーユーザ権限が不要になりました。これにより、disable_referential_integrity と同様に、テーブル所有者権限だけでテスト用フィクスチャの外部キー検証を行えるようになります。

  1. 変更内容の詳細

2-1. 従来の実装

これまで check_all_foreign_keys_valid! は PostgreSQL のカタログテーブルを直接書き換えていました。

  • pg_catalog.pg_constraint.convalidatedfalse に更新
  • その後 VALIDATE CONSTRAINT を実行して再検証
    といった流れで、ざっくりいうと:
sql
UPDATE pg_catalog.pg_constraint
   SET convalidated = false
 WHERE contype = 'f' -- foreign key
   ...;

ALTER TABLE ... VALIDATE CONSTRAINT ...;

しかし pg_catalog の直接更新はスーパーユーザ権限が必須のため、#57378 で disable_referential_integrity からはスーパーユーザ依存を排除したものの、check_all_foreign_keys_valid! だけが依然として superuser を要求していました。

2-2. 新しい実装(PostgreSQL 18.4+)

PostgreSQL 18.4 以降に対しては、外部キーの再検証に ENFORCED / NOT ENFORCED のトグルを利用します。

実行されることのイメージ

  1. 対象となる外部キー制約に対して:

    sql
    ALTER TABLE schema_name.table_name
      ALTER CONSTRAINT constraint_name NOT ENFORCED;
  2. 直後に:

    sql
    ALTER TABLE schema_name.table_name
      ALTER CONSTRAINT constraint_name ENFORCED;
  3. ENFORCED へ戻すタイミングで、既存行に対する外部キー制約チェックがネイティブに実行されます。

    • 違反があれば PostgreSQL 側でエラーが発生し、それが ActiveRecord::InvalidForeignKey としてラップされます。

取得対象となる外部キーの絞り込み

新しい実装では、トグル対象とする外部キーを次の条件で絞り込みます:

  • contype = 'f'(外部キー)
  • すでに ENFORCED な制約のみ(NOT ENFORCED なものは触らない)
  • 対象テーブルに対して pg_has_role(relowner, 'USAGE')true となるもの
    • つまり、接続ユーザがテーブル所有者(もしくは所有ロールのメンバ)であるテーブルのみが対象
    • 単に DML 権限だけを持つテーブルは対象外

これにより、実際に ALTER TABLE ... ALTER CONSTRAINT を実行できるテーブルだけをクエリの時点で選んでいます。

スキーマ/テーブル周りの扱い

  • すべてのスキーマが対象(search_path によるフィルタはなし)
  • ALTER TABLE の対象はスキーマ修飾済みで実行されるため、search_path 依存性は解消されています
  • パーティションテーブルの場合は、子ではなく 親テーブルの制約 をトグルするようになっています
  • PostgreSQL 18.3 以前では、従来の
    • UPDATE pg_catalog.pg_constraint ...
    • VALIDATE CONSTRAINT のコードパスをそのまま維持(挙動変更なし)

2-3. ロック形態の変更

この PR によって、再検証処理で取得するロックが変わります。

  • 旧実装: VALIDATE CONSTRAINTSHARE UPDATE EXCLUSIVE ロックを取得
  • 新実装: ALTER TABLE ... ALTER CONSTRAINTACCESS EXCLUSIVE ロックを取得

ACCESS EXCLUSIVE はより強いロックで、DML/DDL をほぼブロックしますが:

  • 呼び出し元はテスト環境のフィクスチャ検証のみ
  • disable_referential_integrity も #57378 以降同程度のロックを取る

という前提から、運用上の影響は小さいと判断されています。


  1. 影響範囲・注意点

3-1. 実行権限と対象テーブル

PostgreSQL 18.4+ 環境で check_all_foreign_keys_valid! を呼ぶとき:

  • これまでは「スーパーユーザなら全テーブルの外部キーを検証」
  • 今後は:
    • 非スーパーユーザでも動作する
    • ただし 接続ユーザが所有していないテーブル の外部キーは検証されない

スーパーユーザで接続していれば、pg_has_role の条件をすべて満たすため、従来どおり全テーブルがカバーされます。

マルチスキーマ構成や、アプリ用ロールとスキーマ所有ロールが分離されている環境の場合:

  • 「テスト用 DB ユーザが対象テーブルの owner ではない」構成だと、18.4+ では外部キー検証の対象から外れる可能性があります
  • テストで全ての外部キーを検証したい場合:
    • テーブル所有者として接続する
    • もしくは所有ロールのメンバーになるようロール構成を見直す
      ことを検討してください(所有権自体は GRANT できない点に注意)。

3-2. PostgreSQL バージョン依存

  • PostgreSQL 18.4 以上:
    • 新しい NOT ENFORCED トグル方式が使われる
    • スーパーユーザ不要、テーブル所有者権限で十分
  • PostgreSQL 18.3 以下:
    • 実装は従来どおり
    • pg_catalog.pg_constraint への直接 UPDATE を伴うため、superuser が基本前提のまま

アプリケーション側から見える API (check_all_foreign_keys_valid! のメソッドシグネチャや例外種別) は変わりませんが、必要な DB 権限とロックの振る舞いがバージョンによって異なる点は意識しておく必要があります。

3-3. ロックによるテスト実行時間・ブロッキング

テスト環境であっても、DB を並列に共有しているようなケース(他プロセスが同じ DB にアクセスしている CI 構成など)では:

  • ACCESS EXCLUSIVE ロックにより、一時的に他のクエリがブロックされる可能性があります
  • ただし呼び出しはフィクスチャ検証時に限定されているため、通常の Rails テスト構成では大きな問題にはなりにくい想定です

  1. 参考情報 (あれば)
  • 元 PR:
    • #58055 "Use NOT ENFORCED toggle in check_all_foreign_keys_valid! on PostgreSQL 18.4+"
  • 関連 PR:
    • #57378: disable_referential_integrity から superuser 依存を排除した変更
    • #57428: conrelid::regclasssearch_path 依存性に関する議論(今回の 18.4+ 経路では ALTER TABLE がスキーマ修飾済みになったため、この問題は解消)
  • 関連テスト:
    • activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
    • activerecord/test/cases/fixtures_test.rb
      • PostgreSQL 18.4(非スーパーユーザ含む)と 17.10 で動作検証済み

#58111 Fix the broken #with reference

マージ日: 2026/7/13 | 作成者: @codergeek121

  1. 概要 (1–2文で)
    Rails の Active Record の #with_recursive メソッドのドキュメント内で、壊れていた #with への参照リンクが修正されました。コードの挙動には一切影響せず、ドキュメント上のリンク誤りのみを直す変更です。

  2. 変更内容の詳細

  • 対象ファイル: activerecord/lib/active_record/relation/query_methods.rb
  • 変更内容は 1 行の置換のみで、#with_recursive のドキュメントコメント中に書かれていた #with への参照形式(rdoc 用のリンク記法)が誤っていたため、正しい形に修正されています。

イメージとしては、以下のようなコメント行が:

ruby
# Similar to #with_recursive, see #with for non-recursive CTEs.

のように書かれていて、#with への参照が RDoc 上でリンク切れになっていたため、例えば:

ruby
# Similar to #with_recursive, see Relation#with for non-recursive CTEs.

のようにクラス名付きにする、あるいは RDoc が解釈可能な形(#withwith メソッドシグネチャ形式など)に書き換えられた、という種類の修正です。
※正確な文字列は PR 本文からは分かりませんが、「参照の形式」を 1 箇所修正しているだけです。

実装コード (def with_recursive ... end やクエリロジック) には手が入っていません。

  1. 影響範囲・注意点
  • 影響範囲:
    • Rails API ドキュメント (RDoc / api.rubyonrails.org 等) を閲覧した際に、#with_recursive の説明から #with へのリンクが正しく飛べるようになります。
    • ランタイムの挙動、パフォーマンス、互換性には一切影響しません。
  • 注意点:
    • 既存アプリケーション側で対応すべき変更はありません。
    • CI などでドキュメント生成をしている場合も、生成物のリンクが正しくなるだけでビルド手順の変更は不要です。
  1. 参考情報 (あれば)
  • ActiveRecord::QueryMethods#with / #with_recursive は CTE (Common Table Expression) を扱うためのメソッドで、例:

    ruby
    Post.with(
      popular_posts: Post.where("likes_count > 100")
    ).from("popular_posts AS posts")

    のように非再帰 CTE を with で定義したり、

    ruby
    Category.with_recursive(
      ancestors: Category.where(id: 10),
      union: Category.joins("JOIN ancestors ON categories.parent_id = ancestors.id")
    ).from("ancestors")

    のような再帰 CTE を with_recursive で扱うときに利用します。

この PR は、こうしたメソッドの使い方をドキュメント上で辿りやすくするための、リンク修正のみのメンテナンス変更です。


#58110 Remove unneeded parenthesis [ci-skip]

マージ日: 2026/7/13 | 作成者: @claudiob

  1. 概要 (1-2文で)
    Railsのガイド「product_reviews.md」に記載されていたコード例から、不要な括弧(parenthesis)を取り除くドキュメント修正PRです。アプリの挙動には影響せず、ガイドのコードスタイル・可読性を改善する目的の変更です。

  1. 変更内容の詳細
  • 対象ファイル: guides/source/product_reviews.md
  • 変更内容: 1行のコード例について、Ruby的に不要な括弧を削除して、より慣習的な書き方に修正。

実際の差分は以下のようなイメージです(擬似例):

diff
- User.create!(name: "Alice", email: "alice@example.com")
+ User.create! name: "Alice", email: "alice@example.com"

もしくは、メソッド呼び出しで余計な括弧を外す形:

diff
- deploy_to_production(app_name: "my_app")
+ deploy_to_production app_name: "my_app"

PRタイトルから読み取れる通り、ロジックや手順自体ではなく、「見た目とスタイル」だけの変更です。

説明文中のリンク:

See https://edgeguides.rubyonrails.org/product_reviews.html#deploying-to-production

から、この括弧は「Deploying to production」節内のコード例に含まれていたと考えられます。


  1. 影響範囲・注意点
  • 影響範囲:
    • Rails本体のコード・動作には一切影響しません。
    • product_reviews ガイドを見てコードを書く開発者に対して、よりRubyらしい記法を示すようになります。
  • 注意点:
    • 既にガイドを見ながら同じ括弧付きのコードを書いている場合でも、そのコードは通常そのまま動作します(Rubyでは多くの場合、括弧は任意)。
    • チームのスタイルガイドによっては「括弧を明示したい」場合もあるので、ガイドの記法と自分たちのプロジェクトのスタイルをどう合わせるかは別途検討が必要です。

  1. 参考情報 (あれば)

#57967 Change the order of Views and Controllers in the guides

マージ日: 2026/7/13 | 作成者: @p8

  1. 概要 (1-2文で)
    Rails Guides のナビゲーション順序を、従来の「MVCのV→C」から「リクエストライフサイクル (route → controller → view → response)」に合わせて整理し直した PR です。具体的には、コントローラ関連ガイドをビューより前に出し、ルーティングとコントローラをまとめて扱う形に変更しています。

  1. 変更内容の詳細

この PR で変更されているのは guides/source/documents.yaml 1ファイルのみで、Rails Guides の「どのガイドをどの順番で表示するか」を定義している YAML の順序・名称が調整されています。

主なポイントは以下です。

(1) Guides の章構成の順序変更

  • これまで:

    • 「Action View のガイド」が「Action Controller のガイド」より前に記載されていた
    • これは MVC のアルファベット順 (M→V→C) や、概念的な構成としては自然だったが、
      • 実際には一部の Action View ガイドでコントローラやルーティングの知識が前提になっていた
  • 変更後:

    • 「Controllers」に相当するセクションを、「Views」より前に移動
    • そのうえで、ルーティングのガイドを各種コントローラガイドよりさらに前に配置
      → 読む順番が「Routing → Controllers → Views」となるように整理

Xavier による提案どおり、

request lifecycle order: route -> controller -> view -> response

にガイド構成を揃える形になっています。

(2) セクション名の変更: 「Controllers」→「Routing and Controllers」

  • Guides のメニュー上での章タイトルが
    • 旧: Controllers
    • 新: Routing and Controllers
  • これに合わせて、同セクションの中に
    • Routing のガイド
    • Controllers 関連のガイド群 がまとめられるように順番・階層が調整されています。

(3) YAML 上の変更イメージ

PR では具体的な YAML が全文は示されていませんが、概ね以下のような差分が入っていると考えられます (擬似コード):

yaml
# 変更前(イメージ)
- name: Action View Overview
  ...
- name: Action Controller Overview
  ...
- name: Routing
  ...

# 変更後(イメージ)
- name: Routing and Controllers
  children:
    - name: Routing
      ...
    - name: Action Controller Overview
      ...
    - name: Other Controller Guides
      ...
- name: Action View Overview
  ...

実際の diff は「+26 / -23」と行数は小さく、構造と並び順、見出しのラベル変更が中心です。
アプリケーションコードや API には一切変更がありません。


  1. 影響範囲・注意点

影響範囲

  • 影響を受けるのは Rails Guides (公式ドキュメントサイト) のメニュー構成と章タイトル のみです。
  • Rails 本体の挙動、API、DSL、設定ファイル等には変更はありません。
  • これにより、
    • これから Rails Guides を読む人は、自然に
      1. ルーティング
      2. コントローラ
      3. ビュー の順で読むことになり、学習順序がリクエストの流れと揃うため、理解しやすくなります。
    • 一方で、「以前は View ガイドが先に出てきた」ことを前提にしたブログ記事などが、画面キャプチャや手順の説明と微妙に合わなくなる可能性があります(位置が変わるだけで内容は同じ)。

注意点

  • ドキュメント内リンク・外部リンク:
    • もし guides.rubyonrails.org の章名を直接文字列で参照している解説サイトがある場合、
      • 表示されるセクションのタイトルが「Routing and Controllers」に変わるため、文面の整合性が崩れることがあります。
    • ただし、URL パス自体 (/routing.html など) が変わるという情報はないため、リンク切れのリスクは低いと考えられます。
  • 社内資料等で「Rails Guides の『Controllers』セクションを参照」といった説明をしている場合は、
    • 「Routing and Controllers」に文言を更新しておくと混乱を防げます。

  1. 参考情報 (あれば)

この PR 自体はドキュメント構成のみの変更のため、アプリケーション側でのコード修正やバージョン対応は不要です。


#57966 Comment ellipsis in guides examples

マージ日: 2026/7/13 | 作成者: @p8

  1. 概要 (1-2文で)
    Rails Guides のコード例中に素の三点リーダ ... をそのまま書かないようにし、多くをコメントアウトされた # ... に統一した PR です。Ruby では ... が有効な構文であるため、ガイドのコードをコピペしたときに意図せず動作してしまう/構文エラーになる問題を避けることが目的です。

  1. 変更内容の詳細

背景となる問題

  • Rails Guides にはコード例で「中略」を表すために ... が使われている箇所がある

  • Ruby では ... は「3 点リーダレンジ演算子」として有効な構文であり、たとえば:

    ruby
    (1...5) # => 1,2,3,4 の範囲
  • そのため、ガイドのサンプルコードをそのままコピーして実行すると、

    • 「ここは適当に補ってね」という意味の ... が Ruby のコードとして解釈されてしまう
    • 望まない挙動や構文エラーにつながる可能性がある

この PR は、ガイド上の「中略表示」と、実際に実行されうる Ruby コードを明確に分離するための修正です。

具体的な修正パターン

主な方針は以下です:

  • コード例中の省略を表す ... はコメントアウトする

    • 例:

      ruby
      def some_method
        ...
      end

      のようなものを:

      ruby
      def some_method
        # ...
      end

      のように変更。

  • コマンド出力例など、「コピペしてもそもそも動かない」部分では従来通り ... を使うことを許容

    • rails g model ... のような「サンプル出力」「ターミナルの出力例」では ... を維持してよい、というガイドラインに整理。

変更ファイルごとの内容イメージ

PR の diff から読み取れる意図レベルの説明になります:

  • guides/source/active_storage_overview.md

    • Active Storage の使用例コード内にあった素の ...# ... へ変更
    • 例: モデル定義やコントローラ例で「省略」を示す行がコメント化されていると考えられる
  • guides/source/api_documentation_guidelines.md

    • API ドキュメント向けの例示コード内の ... の扱いを修正
    • ドキュメントに掲載するサンプルコードでも、... をコメントで示す、というスタイルに合わせた可能性が高い
  • guides/source/engines.md

    • Engine 関連のサンプルコード (routes.rbEngine クラス定義の例など) の省略部を # ...
  • guides/source/form_helpers.md

    • フォームヘルパーの利用例コード(フォームビルダ、ビューサンプルなど)での ... を同様にコメントアウト

    • ERB 例であれば:

      erb
      <%= form_with(model: @post) do |form| %>
        ...
      <% end %>

      erb
      <%= form_with(model: @post) do |form| %>
        <%# ... %>
      <% end %>

      のように「ERB コメント」に変更している可能性もあります(ERB 中では # ... ではなく <%# ... %> が適切)。実際の diff ではコンテキストに応じたコメント記法が選ばれているはずです。

  • guides/source/plugins.md

    • プラグイン作成例のコード中の省略を表現する ... をコメントに変更
  • guides/source/ruby_on_rails_guides_guidelines.md

    • Rails Guides 執筆者向けガイドライン自体に、「省略を表すときの書き方」のルールを追加
    • 追加された内容の意図:
      • Ruby コード例では、可読性のための省略はコメントとして記述すること
      • 実行可能なコードブロックに素の ... を書かないこと
      • Rails コマンドの出力例など、実行しないことが前提のブロックでは通常の ... を使ってもよい
      • これを示す具体例(ビフォー/アフターのサンプルコード)がガイドラインに追記されている

サンプルコードの一例(イメージ):

markdown
```ruby
class User < ApplicationRecord
  # ...
end

ガイドラインではこのような形の例を推奨していると考えられます。

---

3. 影響範囲・注意点  

- **影響範囲**
  - 変更対象は Rails Guides(ドキュメント)のみであり、Rails 本体のコードや挙動には影響しません。
  - ただし、今後ガイドを書く・更新する人にとってはスタイルガイドがアップデートされるため、執筆時に従うべきルールが明確になります。

- **注意点(ガイド執筆者・メンテナ向け)**
  - Ruby コードブロック、ERB テンプレート例など、**ユーザーがそのままコピーして実行しうるコード**には、素の `...` を書かないこと
    - Ruby: `# ...` などのコメントで表現
    - ERB: `<%# ... %>` のように ERB コメントを使う
  - `rails new`, `rails g`, `bin/rails` などの**コマンド出力例・ログ出力例**は、コピーしても実行できないことが明らかなので、従来通り `...` を使ってもよい
  - 既存のガイドのメンテ時にも、`...` を見つけたら「コードとして解釈されるか?」を確認し、必要に応じてコメント化することが望ましい

- **読者側の注意点**
  - この変更により、ガイドに載っているコードをコピー&ペーストしても、`...` による予期しない Ruby 構文解釈が起こりにくくなる
  - ただし、`# ...` となっている行は「ここに何らかのコードが入る」という意味であり、そのまま実行しても**本来意図している完全な例ではない**ことに注意する必要があります

---

4. 参考情報 (あれば)  

- Ruby における三点リーダレンジ (`...`) と二点レンジ (`..`) の違い:
  - `1..5` は 1〜5 を含む範囲
  - `1...5` は 1〜4 を含む範囲(終端を含まない)
- Rails Guides 執筆ガイドライン(`ruby_on_rails_guides_guidelines.md`)は、ガイド作成時のスタイル・コード例の書き方を決める基準となるため、ドキュメントに PR を出す場合はいちど目を通しておくとよいです。

---


## [#58098](https://github.com/rails/rails/pull/58098) Fix settings navigation test in authentication guide {#pr-58098}

**マージ日**: 2026/7/13 | **作成者**: [@Lemmah](https://github.com/Lemmah)

1. 概要 (1-2文で)  
認証ガイド内のシステムテスト例において、「Store Settings」がリンクではなく見出し(`h4`)としてレンダリングされている実際のマークアップに合わせて、テストのセレクタを修正したドキュメント変更です。これにより、一般ユーザーと管理者ユーザーのテスト例の整合性が取り戻され、意図した挙動を正しく検証できる内容になっています。

---

2. 変更内容の詳細  

対象: `guides/source/sign_up_and_settings.md` のテストコード例が 1 行修正されています。

元のガイドでは、「一般ユーザーは “Store Settings” を見られないこと」を検証する例として、おおよそ次のようなコードになっていました:

```ruby
# (修正前のイメージ)
assert_not_dom "a", "Store Settings"

しかしガイド中の UI マークアップでは、「Store Settings」はリンク (<a>) ではなく見出し (<h4>Store Settings</h4>) として表示されているため、このセレクタは実際の HTML と整合していませんでした。

PR では、これを次のように修正しています:

ruby
# (修正後)
assert_not_dom "h4", "Store Settings"

背景として:

  • ガイドのサンプル UI では、「Store Settings」は <h4> 見出しとして描画される。
  • 一般ユーザー用のテストでは「Store Settings が見えないこと」を検証したい。
  • 管理者ユーザー用のテストでは、すでに「h4 セレクタで Store Settings を確認する」ように書かれており、こちらはマークアップと一致していた。

にもかかわらず、一般ユーザー側だけ a セレクタを使っていたため、次のような問題がありました:

  • 実際の画面には <a>Store Settings</a> はそもそも存在しない(一般ユーザーでも管理者でも)。
  • そのため「一般ユーザーで assert_not_dom "a", "Store Settings" が通る」のは、UI が正しいからではなく、「そもそも <a> がないから」通ってしまっていた。
  • 本来検証したいのは「一般ユーザーには <h4>Store Settings</h4> が出ない」という点なので、テストが意図どおりの挙動を検証していなかった。

この PR により、一般ユーザーのテスト例も管理者ユーザーのテスト例も、どちらも h4 を対象にすることで、ガイドに示されているマークアップとテスト内容が一致するようになっています。


  1. 影響範囲・注意点
  • 影響範囲:
    • Rails 本体のコードには一切影響せず、「認証ガイド(sign_up_and_settings.md)」内のサンプルテストコードのみの変更です。
    • 実プロジェクトの動作やテストには直接影響しませんが、このガイドをコピペして使っている場合は、a ではなく h4 を対象にしているか確認したほうがよいです。
  • 注意点:
    • UI 実装側で「Store Settings をリンクに変更した」など、実際のマークアップが ガイドと異なる場合は、自分のアプリのマークアップに合わせてセレクタを調整する必要があります。
    • 「リンクテキストの有無を検証したい」のか、「見出しの有無を検証したい」のか、意図に応じて assert_dom, assert_not_dom のセレクタを選ぶ、というテスト設計上のポイントを示す変更でもあります。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58098
  • 対象ガイド: Authentication guide – sign_up_and_settings.md(「Store Settings」セクションのシステムテスト例)
  • ガイドのスクリーンショットによると、「Store Settings」は <h4> 要素として描画されていることが確認されています。

#58101 Downcase Cache-Control key in asset pipeline guide example

マージ日: 2026/7/13 | 作成者: @meganemura

  1. 概要 (1-2文で)
    Asset Pipeline ガイド内のサンプルコードにおける Cache-Control ヘッダー名を、Rack 3 の仕様に合わせて小文字の "cache-control" に修正したドキュメント更新です。実装コードではなく、ガイドの表記を現状のジェネレータ出力と整合させることが目的です。

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

  • 対象: guides/source/asset_pipeline.md の「CDNs and the Cache-Control Header」セクション内のコード例

  • 変更前の例(イメージ):

    ruby
    config.public_file_server.headers = {
      "Cache-Control" => "public, max-age=31536000"
    }
  • 変更後の例:

    ruby
    config.public_file_server.headers = {
      "cache-control" => "public, max-age=31536000"
    }
  • 文字としての HTTP ヘッダー名を説明する本文中では従来どおり Cache-Control と表記し、コード例のみ Rack 3 SPEC に準拠した小文字キーに揃えています。

  • 背景として、#52653 で config/environments/development.rb.tttest.rb.tt のテンプレートが "cache-control" に変更されており、新規アプリの実際の生成コードとの不整合を解消する修正です。

  1. 影響範囲・注意点
  • 影響範囲:
    • Rails ガイド(ドキュメント)のみであり、フレームワーク本体の動作や API には変更はありません。
    • ただし、ガイドを参考に環境設定を書いている開発者にとって、Rack 3 仕様に即した正しい例が示されるようになります。
  • 技術的なポイント・注意:
    • Rack 3 ではレスポンスヘッダーは小文字キーで扱うことが要求される(仕様)ため、Rails のジェネレートされる環境ファイルも小文字 "cache-control" を使用するようになっています。
    • HTTP ヘッダー名自体はプロトコル上は大文字・小文字を区別しませんが、Rack 3 のインターフェース要件に合わせる必要があります。
    • 既存アプリで "Cache-Control" をキーに使っていても、Rack 2 環境では通常問題になりませんが、Rack 3 への移行時には小文字化を検討するのが無難です(この PR はそのベストプラクティスの周知にもつながります)。
  1. 参考情報 (あれば)
  • 対応元 Issue/PR: #52653(Rack 3 SPEC に合わせて環境テンプレートのレスポンスヘッダーキーを小文字化した変更)
  • Rack 3 SPEC(概要):
    • レスポンスヘッダーは Hash&lt;String, String> で、キーは小文字の ASCII 文字列であることが求められる。
  • 実務的には、Rails 7 以降で Rack 3 を使う、または今後のアップグレードを見据える場合、config.public_file_server.headers やミドルウェアで設定するヘッダー名は小文字に揃えておくとよいです。

#58096 Read foreign key names from the schema on SQLite3

マージ日: 2026/7/12 | 作成者: @55728

  1. 概要 (1-2文で)
    SQLite3 アダプタで foreign_keys(table) が外部キー制約名を正しく取得するようになり、remove_foreign_key ..., name: ... が意図した制約だけを削除できるように修正されています。これにより、テーブル再構築時もカスタム外部キー名が維持され、スキーマダンプにも正しく反映されます。

  1. 変更内容の詳細

背景・従来の挙動

SQLite3 では、テーブル定義に以下のように名前付き外部キーを定義していても:

ruby
add_foreign_key :crews, :ships, column: "ship_id",        name: "fk_ship"
add_foreign_key :crews, :ships, column: "backup_ship_id", name: "fk_backup_ship"

foreign_keys("crews") から取得できる ActiveRecord::ConnectionAdapters::ForeignKeyDefinition は、name が常に nil になっていました。

その結果:

ruby
foreign_keys("crews").map(&:name)
# before: [nil, nil]
# after : ["fk_ship", "fk_backup_ship"]
  • remove_foreign_key(:crews, name: "fk_ship")
    → 名前で一致させられないため、最初に見つかった外部キーを削除してしまう
  • remove_foreign_key(:crews, name: "nope", if_exists: true)
    → 「見つからなかったので何もしない」ではなく、やはり任意の1件を削除してしまう
  • add_foreign_keyadd_column でテーブル再構築(ALTER TABLE → 一時テーブルへのコピー → 再作成)すると、foreign_keys の情報を元に再定義されるが、namenil のため、すべて自動生成名 fk_rails_... に置き換わる
  • スキーマダンプ (schema.rb) 時も、カスタム名が失われてしまう

という問題がありました。

今回の修正内容

1) SQLite3 adapter で制約名をスキーマから取得

SQLite3 のテーブル定義には、以下のような形で制約名が書かれます:

sql
CONSTRAINT "fk_ship" FOREIGN KEY ("ship_id") REFERENCES "ships"(...)
CONSTRAINT "fk_backup_ship" FOREIGN KEY ("backup_ship_id") REFERENCES "ships"(...)

この PR では、SQLite3 アダプタの foreign_keys(table_name) 実装を修正し、この CONSTRAINT "..." 部分から制約名をパースして ForeignKeyDefinition#name にセットするようになりました。

その結果:

ruby
foreign_keys("crews").map(&:name)
# before: [nil, nil]
# after : ["fk_ship", "fk_backup_ship"]

となり、他の DB アダプタ(MySQL/PostgreSQL)と同様に、foreign_keys の戻り値に正しい名前が入るようになります。

2) SQLite3 の remove_foreign_key:name を正しく使うように

これまで SQLite3 専用の remove_foreign_key 実装では、引数で渡された name: オプションが内部のマッチロジックに正しく伝搬しておらず、「名前指定しても最初の外部キーを削除する」挙動になっていました。

この PR では、抽象アダプタ(他 DB と共通の実装)と同様に、remove_foreign_key:name をきちんとマッチ条件として使用するように変更されています。

挙動の比較:

操作変更前変更後
remove_foreign_key(:crews, name: "fk_ship")最初の外部キーを削除(任意)"fk_ship" の外部キーだけを削除
remove_foreign_key(:crews, name: "nope", if_exists: true)いずれかの外部キーを削除してしまう対象がないので no-op(削除されない)

3) テーブル再構築時にカスタム名を維持

SQLite3 では ALTER TABLE の制約が厳しいため、カラム追加などの際に実質的にテーブルを「作り直す」処理(旧テーブルから一時テーブルへコピー → 新テーブルを作成 → データを戻す)を行います。
この際、外部キーも foreign_keys の戻り値を元に再作成されます。

今回の修正で foreign_keys に正しい name が入るようになったため:

  • 変更前:
    外部キー再作成時に namenil → Rails が自動生成名 fk_rails_... を付与 → カスタム名が失われる
  • 変更後:
    元のカスタム名(例: "fk_ship")がそのまま再作成時に使われる → 名前が維持される

4) スキーマダンプでの挙動

SchemaDumper には fk_ignore_pattern があり、自動生成の外部キー名(fk_rails_*)はスキーマダンプから意図的に省かれます。

今回の変更で:

  • 自動生成名 fk_rails_*
    → これまで通りダンプから除外(挙動変わらず)
  • カスタム名(例: "fk_ship"
    → これまで namenil だったため落ちていたが、今後は正しい名前付きで add_foreign_keyschema.rb に出力されるようになる

これにより、「明示的に付けた外部キー名」がスキーマダンプ経由で正しくラウンドトリップするようになります。


  1. 影響範囲・注意点
  • 対象:
    • SQLite3 を使用しているアプリケーションのみ
    • 明示的に name: を指定して add_foreign_key しているケース
    • remove_foreign_key ..., name: ... を利用しているケース
  • 期待される改善:
    • remove_foreign_key(name: ...) が誤った外部キーを削除してしまうバグが解消される
    • テーブル再構築(add_column など)によってカスタム外部キー名が fk_rails_* に変わってしまう問題が解消される
    • schema.rb へのスキーマダンプで、カスタム名の外部キーが正しく出力される
  • 互換性・注意点:
    • 「これまでバグに依存して動いていたコード」がある場合、挙動が変わる可能性があります
      • 例: name が存在しない前提で「最初の外部キーが削除される」ことに依存していた処理(通常はそういった依存は望ましくない)
    • スキーマダンプの内容が変わる可能性があります
      • 以前は無名扱いで出力されていなかった外部キーが、「カスタム名を持つ外部キー」として add_foreign_key に名前付きで記録されるようになります
  • テスト:
    • activerecord/test/cases/migration/foreign_key_test.rb にテストが追加・更新されており:
      • 外部キー名の取得
      • remove_foreign_key(name: ...) の挙動
      • スキーマダンプ・テーブル再構築時の名前保持
        などがカバーされています。

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

    • activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
      → 外部キーの読み取り・削除の挙動修正
    • activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
      → スキーマ関連の細かいロジック修正
    • activerecord/test/cases/migration/foreign_key_test.rb
      → 上記挙動を検証するテスト追加・更新
    • activerecord/CHANGELOG.md
      → 振る舞い変更が changelog に追記
  • 運用上のポイント:

    • SQLite3 + 外部キーを積極的に使う開発・テスト環境では、この修正により本番(MySQL/PostgreSQL)との挙動差がさらに減少します。
    • 既存アプリでは、アップデート後に schema:load やマイグレーションを走らせたときの schema.rb 差分や、remove_foreign_key の影響範囲を軽く確認するのがおすすめです。

#58064 Carry the form attribute onto a multiple select's hidden field

マージ日: 2026/7/12 | 作成者: @55728

  1. 概要 (1-2文で)
    multiple: trueselect ヘルパが生成する「値クリア用の hidden フィールド」にも、form: オプションで指定した form 属性を引き継ぐようにした変更です。これにより、<form> 要素の外にある multiple select でも、全ての選択を外したときに意図通り「空配列」が送信されるようになります。

  1. 変更内容の詳細

これまでの挙動

select ヘルパで multiple: true を指定すると、Rails は以下の 2 つの要素を出力します。

erb
<%= select :post, :category, [], {}, { multiple: true, form: "my_form" } %>

従来の出力(Before)は:

html
<input type="hidden" name="post[category][]" value="" autocomplete="off">
<select multiple="multiple" name="post[category][]" id="post_category" form="my_form"></select>

ポイント:

  • select には form="my_form" が付く
  • しかし、ペアの hidden フィールドには form 属性が付かない

HTML の仕様上、<form> 要素の外側に置いたコントロールは、form 属性で対象フォームを明示しないと、そのフォームの送信対象になりません。
そのため、select 自体は my_form に紐づく一方、hidden フィールドはどのフォームにも紐づかず、全ての選択を外したときに送られるはずの「空の値」が my_form に送信されない状況になっていました。

変更後の挙動

この PR では、multiple select に付与された form 属性を hidden フィールドにもコピーするように変更しています。

After:

html
<input type="hidden" name="post[category][]" value="" autocomplete="off" form="my_form">
<select multiple="multiple" name="post[category][]" id="post_category" form="my_form"></select>

結果として、以下が保証されます:

  • select<form id="my_form"> の外にあっても
  • form: "my_form" を指定していれば
  • select と hidden の両方が同じフォーム my_form に紐づく
  • 全てのオプションを非選択にすると、hidden が空の値 post[category][]= ""my_form に送信してくれる

実装的なポイント

  • 変更ファイル: actionview/lib/action_view/helpers/tags/select_renderer.rb
    • multiple select 用の hidden フィールド生成時に、form 属性も他の属性と同様に渡すよう 1 行修正
  • テスト: actionview/test/template/form_options_helper_test.rb
    • form: "my_form" 指定時に hidden フィールドにも form="my_form" が付与されることを検証するテストを追加 (+8 行)

他ヘルパとの整合性

この修正は、既存の他のヘルパと仕様を揃えるものです。

  • check_box
    • hidden フィールド生成時に "name", "disabled", "form" を引き継いでいる
  • collection_check_boxes / collection_radio_buttons
    • hidden フィールドに form: @html_options[:form] を指定している

multiple select だけが hidden 側に form を付けていなかったため、今回の変更で一貫性が取られました。


  1. 影響範囲・注意点
  • 影響があるケース:

    • multiple: trueselect
    • 実際の <form> 要素の外に配置し
    • form: "some_form_id" を使ってフォームに紐付けている場合

    このケースで、全ての選択を外したときにも確実にパラメータが空配列として送信されるようになります。
    具体的には、以前は何もパラメータが来なかったケースで、今後は post[category]=>[""] → コントローラ側でパラメータの正規化ロジックによって「選択なし」として扱われます(従来の multiple select + hidden の一般的な挙動)。

  • form オプションを指定していない通常のケース:

    • formnil の場合は、ヘルパが属性を出力しないため HTML 出力はこれまでと完全に同一
    • つまり、既存のフォームレイアウト/サーバサイド処理への影響はありません
  • 後方互換性:

    • 修正は「hidden に form 属性を足すだけ」の純粋に加算的な変更であり、既存挙動を壊すような変更は行っていません

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58064
  • 関連する内部実装:
    • actionview/lib/action_view/helpers/tags/select_renderer.rb
    • actionview/lib/action_view/helpers/tags/check_box.rb
    • actionview/lib/action_view/helpers/form_options_helper.rb(collection 系ヘルパ)
  • HTML 仕様: form 属性
    • フォームコントロール要素 (input, select, textarea など) は form 属性でフォームと紐付け可能
    • 要素が <form> の外にあっても、form 属性で指定されたフォームの送信時に値が送られる

#58099 Refactor assume_migrated_upto_version

マージ日: 2026/7/12 | 作成者: @fxn

  1. 概要 (1-2文で)
    ActiveRecord::Migration#assume_migrated_upto_version の内部実装を、主に変数名とテーブル名の扱いの整理によってリファクタリングしたPRです。挙動の変更はなく、読みやすさ・一貫性・将来の保守性を高めるための内部改善です。

  1. 変更内容の詳細

背景: migrated という変数名の問題意識

もともとこのメソッド内には以下のようなコードがありました:

ruby
migrated = migration_context.get_all_versions
  • get_all_versionsschema_migrations テーブルに記録されているバージョン一覧を返すメソッドです。
  • しかし、典型的な利用シナリオである「DBリセット直後」は、
    • db/schema.rb の内容は「どこまでマイグレーション済みか」を表している一方で、
    • schema_migrations テーブルは空 という状態になります。
  • このとき migrated は空配列ですが、変数名からは「すでにマイグレーションされたバージョン群」を意味しそうで誤解を招きます。

そのため、この PR では変数名を「schema_migrations テーブルに入っているバージョン」であることが明確な versions_in_schema_migrations に変更しています。
これにより、「実際に何が migrate 済みか」という論理概念と、「schema_migrations に記録されている値」という物理的な状態を別物として捉えやすくなっています。

作者が NOTE で触れている通り、db/schema.rb の :ruby フォーマットは「何が migrate 済みか」という履歴情報を完全には保持しておらず、structure.sql のような情報量はありません。この変数名変更は、その根本的な問題を意識しつつ、少なくともコード上の意味づけをクリアにする意図があります。


変更点1: migration context 経由で schema_migrations テーブル名を取得

これまでは、assume_migrated_upto_version の中で schema_migrations テーブルの名前を取得する際に、migration context とは別の経路を使っていました。

  • この PR では migration context が提供する API を一貫して使うように変更 しています。
  • 目的:
    • アクセス経路を統一し、コードの読みやすさ・整合性を向上。
    • 余分な文字列生成(allocation)も減るが、ここではパフォーマンスより一貫性が主目的。

結果として、「このメソッドは migration context を出入り口として動く」という構造がより明確になります。


変更点2: テーブル名の変数とクオート処理の整理

以前のコードでは:

  • sm_table という変数名が使われていたが、これは「テーブル名そのもの」ではなく「クオート済みのテーブル名」を保持していました。
  • 一方で SQL の中では
    • バージョン値(version カラム)にはクオートを施しているのに、
    • テーブル名はすでにクオート済みの文字列をそのまま使う という状態で、一見したときに何がどこでクオートされているかが分かりづらくなっていました。

この PR では:

  • 変数には「生のテーブル名」を保持するようにし、
  • SQL を組み立てる際に、
    • テーブル名もバージョン値も同じ場所・同じレイヤでクオートする
  • という形にリファクタリングしています。

これにより:

  • 変数名から「これはテーブル名か/クオート済み文字列か」が判断しやすくなります。
  • SQL 組み立ての責務が明確になり、「どこでクオートされるか」がコードリーディング時に追いやすくなります。

変更点3: versions_in_db_migrate など、より説明的な変数名

このメソッド内には複数の「バージョンの集合」が登場します。今回のリファクタでは、それぞれの意味を明確にするために名称を整理しています。

  • 旧: versions
  • 新: versions_in_db_migrate
    db/migrate 配下に存在するマイグレーションファイルから得られるバージョン集合であることを示す。

既に触れた versions_in_schema_migrations と対になる命名で、

  • 「ファイルシステム(db/migrate)上に存在するマイグレーション」
  • 「DBの schema_migrations テーブルに記録されているマイグレーション」

を区別しやすくしています。

さらに、

  • versions_to_insert という名前も導入・整理されており、
    • 「これから schema_migrations に INSERT される対象のバージョン集合」 であることが直感的に分かるようになっています。

この命名パターンにより、set 演算(差集合・和集合など)を読むときの意図が格段に把握しやすくなります。


  1. 影響範囲・注意点
  • 変更ファイルは activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb のみで、追加 13 行・削除 11 行の小規模なリファクタです。
  • 公開 API (assume_migrated_upto_version) のインターフェイスや挙動には変更がありません。
    • 既存アプリケーションがこのメソッドを利用している場合も、そのまま動作します。
  • 変更内容は変数名やクオートの扱いといった内部実装の整理が中心であり、互換性上の懸念はほぼありません。
  • 一方で、schema.rb (:ruby フォーマット) が「何が migrate 済みか」という履歴情報を明示的には保持しないという構造的な制約には手を付けていません。
    • すなわち、この PR は「その制約のもとでコードを読みやすくした」ものであり、「履歴情報をどこかに残す」ような機能追加ではありません。

  1. 参考情報 (あれば)
  • 対象メソッド: ActiveRecord::Migration#assume_migrated_upto_version
    • 主な用途:
      • 既存の DB に対して、過去のマイグレーションを「すでに適用済み」とみなして schema_migrations を埋める
      • DB リセット後に db/schema.rb ベースで状態を作り直しつつ、古いマイグレーションを再実行しないようにする など
  • 関連する話題:
    • db/schema.rb (:ruby) vs structure.sql
      • :ruby は DB に依存しない形でスキーマを表現するが、何がどの順序で migrate されたかまでは保持しない
      • structure.sql は DB の実際の構造や一部メタ情報も含めてダンプするため、「何が migrate 済みか」に近い情報をより忠実に残せる

この PR はそうした設計上の背景を意識しつつ、「少なくともコードから意味を誤読しないようにする」ためのリファクタと位置づけられます。


#58077 Strip only the commented line when detecting a LIMIT in rails query

マージ日: 2026/7/12 | 作成者: @55728

  1. 概要 (1-2文で)
    rails query --sql コマンドで SQL 内の LIMIT 有無を判定する際、行コメント -- ... の処理が誤ってその後ろの全体を巻き込んでしまい、本来存在する LIMIT を見落として二重に LIMIT を付けてしまう不具合を修正した PR です。コメント除去の正規表現を修正し、「コメント行だけ」を正しく取り除いてから LIMIT を検出するようにしています。

  1. 変更内容の詳細

問題となっていた挙動

rails query --sql は次のような流れで SQL を処理します:

  1. ユーザ入力の SQL からコメントを除去
  2. 残った SQL に LIMIT 句があるかを正規表現で検出
  3. LIMIT がなければデフォルトの LIMIT 101 を付加

このとき、コメント除去に使われていた「行コメント --」用の正規表現が以下のような状態でした(説明用・擬似コード):

ruby
sql_without_comments = sql.gsub(/--.*$/m, "")

ここで /m フラグが付いているため、. が改行も含めてマッチするようになっていました。その結果:

sql
-- a comment
SELECT * FROM posts ORDER BY id LIMIT 2

に対して --.*$

text
-- a comment\nSELECT * FROM posts ORDER BY id LIMIT 2

全体を貪欲にマッチしてしまい、クエリ本体ごと削除されていました。
そのため、内部的には「LIMIT を含まない空 SQL」とみなされ、LIMIT 101 が追加されて最終的に:

sql
SELECT * FROM posts ORDER BY id LIMIT 2 LIMIT 101

のような不正な SQL が生成されていました。

修正内容

PR ではこの行コメント用の正規表現を修正し、「コメント行だけ」を削除するようにしています。
要点は:

  • /m フラグ利用時でも、-- 行コメントが「その行の終端まで」にしかマッチしないようにパターンを変更
  • これにより、改行をまたいでクエリ本体まで巻き込まない

実際のコード差分は 1 行の置き換えのみですが、ロジックとしては:

  • -- からその行の終わりまで」を削除する
  • 改行文字をまたがないようにする([^\\n] などで制約する形)

という方向の修正です。

テストの追加

railties/test/commands/query_test.rb に 8 行のテストが追加されています。
テスト内容(要約):

ruby
def test_limit_is_not_duplicated_when_preceded_by_line_comment
  sql = <<~SQL
    -- a comment
    SELECT * FROM posts ORDER BY id LIMIT 2
  SQL

  # rails query --sql が内部的に構築する最終的な SQL を検査
  # 期待値: 既存の LIMIT 2 が尊重され、追加の LIMIT は付かない
end

といった形で、

bash
rails query --sql $'-- a comment\nSELECT * FROM posts ORDER BY id LIMIT 2'

のケースでも、LIMIT 2 のみで実行されることを検証しています。


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

    • 対象はあくまで rails query --sql CLI コマンドの内部処理です。
    • アプリケーションコードでの Active Record の limit や Arel には影響しません。
    • 「SQL 文字列に -- 行コメントが含まれていて、その後に LIMIT が続く」ようなケースでの動作が正しくなります。
  • 期待できる改善点

    • コメント付きの SQL を rails query --sql に食わせても、不要な二重 LIMIT による構文エラーが発生しなくなります。
    • 特に、開発時のデバッグ・チューニングなどで、生 SQL をコピペしながらコメントを入れて試すワークフローが安定します。
  • 注意点 / 想定される互換性

    • もともと不正な動作(本来存在する LIMIT を見落としていた)を正した変更のため、「正しく書かれた SQL」の挙動が変わることは基本的にありません。
    • 逆に、「今までバグに依存していた」ようなケース(例えば、コメントの後ろにあえて LIMIT を書いても無視されることを前提にしていた)では挙動が変わりますが、そのような使い方は通常想定されません。

  1. 参考情報 (あれば)
  • PR 本文に記載の再現コマンド:

    bash
    rails query --sql $'-- a comment\nSELECT * FROM posts ORDER BY id LIMIT 2'
    • 修正前: ... LIMIT 2 LIMIT 101 が生成され、DB が構文エラー
    • 修正後: 既存の LIMIT 2 が検出され、追加の LIMIT は付加されない
  • 関連する箇所の関心ごと:

    • コメント除去と LIMIT 検出は単純な正規表現ベースで行われており、SQL パーサではありません。
    • 今後他のコメント形式(/* ... */ など)との絡みや、サブクエリ中の LIMIT 等についても、正規表現の扱いには注意が必要です。

#58091 Honor if_not_exists: in SQLite3 add_check_constraint and add_foreign_key

マージ日: 2026/7/12 | 作成者: @55728

  1. 概要 (1-2文で)
    SQLite3 アダプタの add_check_constraint / add_foreign_key が、オプション if_not_exists: true を正しく解釈していなかった問題を修正し、既に存在する制約がある場合は何もせず(no-op)に済むようになりました。これにより、他のアダプタ(MySQL、Trilogy)や remove_* 系メソッドとの挙動の一貫性が取れ、ロールバック時の再実行も安全になります。

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

何が問題だったか

Rails のマイグレーションで以下のようなコードを書いたとします:

ruby
add_check_constraint :users, "age >= 0", name: "users_age_check", if_not_exists: true
add_foreign_key :comments, :posts, if_not_exists: true

抽象アダプタ(MySQL, Trilogy など)では、同じ名前・同じ定義の制約が既に存在する場合は no-op となり、エラーも重複定義も起きません。

しかし SQLite3 アダプタでは:

  • メソッドオーバーライドに if_not_exists: が引数として宣言されていなかった
  • そのため if_not_exists: は単に **options に埋もれて無視されていた
  • 結果として、同一制約が既に存在しても 重複して追加されてしまう 挙動になっていた

同時に、remove_check_constraint / remove_foreign_key の SQLite3 側オーバーライドは if_exists: をちゃんと解釈しており、ここだけ挙動が非対称でした。

今回の修正内容

activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb での変更が本体です。

ポイント:

  • add_check_constraintadd_foreign_key の SQLite3 実装に if_not_exists: を明示的な引数として追加
  • 実行前に「同じ制約が既に存在するか」をチェックし、存在する場合は 早期 return するように変更
  • 外部キーについては、抽象アダプタと同様に、foreign_key_options のロジックを使ってデフォルト列(通常は参照先テーブルの id カラム)を解決してから比較を行う
    • これにより、「同じテーブルだけど別カラムを参照する外部キー」は別物として扱われ、重複とみなさずに追加される

概念的なイメージ(※擬似コード):

ruby
def add_foreign_key(from_table, to_table, **options, if_not_exists: false)
  if if_not_exists && foreign_key_already_exists?(from_table, to_table, options)
    return # ここで no-op
  end

  # ここから従来通りの追加処理
  super
end

add_check_constraint も同様に、既存のチェック制約の存在チェックを行い、見つかれば no-op にしています。

ロールバックの再実行(rollback replay)の修正

Rails のマイグレーションレコーダは、以下のように逆操作を自動生成します:

  • remove_foreign_key(..., if_exists: true) の逆操作として
  • add_foreign_key(..., if_not_exists: true) を吐く

SQLite3 アダプタでは if_not_exists: が無視されていたため、ロールバックを繰り返し実行すると同じ外部キーが何度も追加されてしまいました。

修正後は if_not_exists: true が正しく働くため、ロールバックを何度繰り返しても 外部キーが重複しない・idempotent な挙動になります。

テストとドキュメント

  • check_constraint_test.rbforeign_key_test.rb に、それぞれ if_not_exists: を伴うケースのテストが追加されています
  • CHANGELOG.md に今回の挙動修正が追記されています

  1. 影響範囲・注意点
  • 対象:
    • SQLite3 を使っている Rails アプリ
    • マイグレーションで add_check_constraint / add_foreign_keyif_not_exists: true 付きで利用している場合
  • 期待される挙動の変化:
    • 以前:
      • SQLite3 だけ if_not_exists: true が効かず、同じ制約/外部キーが何度でも追加されていた
    • 今後:
      • 「本当の意味で同一」の制約/外部キーが既に存在する場合は no-op になる
      • 参照先テーブルは同じでも 参照カラムが違う外部キーは別物として追加される
  • マイグレーション互換性:
    • if_not_exists: を使っていない既存のマイグレーションの挙動は 一切変わりません
    • 既に DB 内に重複する制約・外部キーが作られてしまっている場合:
      • この PR 適用以降は、さらに重複が増えることはなくなりますが、
      • 既存の重複定義を整理したい場合は、必要に応じて手動で削除(または専用マイグレーション)する必要があります
  • 他アダプタとの一貫性:
    • MySQL や Trilogy の挙動、および SQLite3 の remove_* 系メソッドと揃ったため、
    • 「どの DB アダプタでも if_not_exists: / if_exists: を使えば idempotent なマイグレーションが書ける」という前提がより信頼できるようになりました。

  1. 参考情報 (あれば)
  • 該当 PR: Honor if_not_exists: in SQLite3 add_check_constraint and add_foreign_key (#58091)
  • 関連する ActiveRecord コード:
    • ActiveRecord::ConnectionAdapters::AbstractAdapter#add_foreign_key
    • ActiveRecord::ConnectionAdapters::SQLite3::SchemaStatements
  • マイグレーションにおける存在チェック系オプション:
    • add_* 系: if_not_exists: true
    • remove_* 系: if_exists: true
      これらを組み合わせることで、「同じマイグレーションを何度実行しても壊れない」スクリプトが書きやすくなります。

#58092 Accept Date and numeric inputs in relative_time_in_words

マージ日: 2026/7/12 | 作成者: @55728

  1. 概要 (1-2文で)
    relative_time_in_words ヘルパーが、ドキュメント上は受け付けることになっていた Date や数値(秒数)を実際には扱えず ArgumentError を出していた問題を修正した PR です。distance_of_time_in_words と同様に、引数を事前に Time に正規化してから過去/未来の判定を行うように変更されています。

  1. 変更内容の詳細

問題点の整理

relative_time_in_words は以下のような処理をしていました(説明文内の擬似コード):

ruby
def relative_time_in_words(from_time, options = {})
  now = Time.now
  time = distance_of_time_in_words(from_time, now, options.except(:scope))
  key = from_time > now ? :future : :past   # <- ここで raw 引数を比較している
  ...
end
  • distance_of_time_in_words 自体は、以下のような多様な型を受け付ける実装になっています。
    • Time
    • Date
    • DateTime
    • Integer(秒数)
  • しかし relative_time_in_words 内では、正規化前の引数 (from_time) を Time.now と直接比較して、:past / :future どちらの翻訳キーを使うかを決めていました。
  • その結果、Ruby の比較規則により、以下のようなエラーが出ていました:
ruby
relative_time_in_words(Date.tomorrow)
# => ArgumentError: comparison of Date with Time failed

relative_time_in_words(180)
# => ArgumentError: comparison of Integer with Time failed

ドキュメントでは time_ago_in_words と同様に Date や数値も受け付けると書かれているため、仕様と実装が食い違っている状態でした。

修正内容

この PR では、distance_of_time_in_words が既に使っているプライベートメソッド
normalize_distance_of_time_argument_to_time を利用して、relative_time_in_words の先頭で引数を Time に正規化するようにしています。

要するに、過去/未来の判定:

ruby
key = from_time > now ? :future : :past

に使われる値も、distance_of_time_in_words と同じ正規化ロジックを通った Time オブジェクトになるように変更されています。

これにより:

ruby
relative_time_in_words(Date.tomorrow) # OK
relative_time_in_words(180)           # OK (今から180秒後 / 前)
relative_time_in_words(Time.current)  # もともとOK
relative_time_in_words(DateTime.now)  # もともとOK

といった、ドキュメントに記載されているすべての入力型が、エラー無しで動作します。

テストとドキュメント

  • actionview/test/template/date_helper_test.rb に、Date と数値入力を含むテストが追加されています。
  • actionview/CHANGELOG.md に、この挙動修正についてのエントリが追加されています。

  1. 影響範囲・注意点
  • 対象: ActionView::Helpers::DateHelper#relative_time_in_words を利用しているコード。
  • 影響:
    • これまで DateInteger を渡すと ArgumentError になっていたケースが、期待通りに動くようになります
    • Time を渡していた既存コードの挙動は、distance_of_time_in_words と同じ正規化ルールに沿った比較になるため、基本的には後方互換です。
  • 注意点:
    • distance_of_time_in_words と同じ変換ルール(タイムゾーンの扱いや Date の 00:00 化など)に依存するため、境界時刻(例: 日付のみを渡した場合の日付変更線付近)での「過去/未来」判定が、「生の値」で比較していた場合と微妙に異なる可能性はあります。ただし、これまでそもそも ArgumentError で動作していなかった入力型に対する差分なので、実害はほぼありません。

  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/58092
  • 関連メソッド:
    • ActionView::Helpers::DateHelper#relative_time_in_words
    • ActionView::Helpers::DateHelper#time_ago_in_words
    • ActionView::Helpers::DateHelper#distance_of_time_in_words

#58093 Decouple schema loading from the versions formatter

マージ日: 2026/7/12 | 作成者: @fxn

  1. 概要 (1-2文で)
    Rails 8.1 で導入された「version formatter」の契約(インターフェース)から、schema.rb ローダー(:ruby ローダー)の要件が漏れ出していた問題を解消するため、スキーマ読み込み処理とバージョンフォーマッタを疎結合にする変更です。schema.rb ローダーと structure.sql ダンパーが同じ内部 SQL 生成ロジックを共有していたのをやめ、それぞれの責務を分離しています。

  1. 変更内容の詳細

背景

  • Rails には2種類のスキーマダンプ方式があります:

    • schema.rb:ruby ローダー / ダンパー)
    • structure.sql:sql ダンパー)
  • どちらの場合も、スキーママイグレーションテーブル(通常は schema_migrations)を「どのバージョンが適用済みか」で埋める必要があります。

  • これまでは、ActiveRecord の内部にある「マイグレーションバージョンの SQL を生成するプライベートメソッド」が

    • :ruby ローダー側: 生成した SQL を execute して実行
    • :sql ダンパー側: 生成した SQL を structure.sql に追記 という形で共用されていました
  • Rails 8.1 で「version formatter」が導入され、ユーザーが「どのような SQL で schema_migrations にバージョンを登録するか」をカスタマイズできるようになりましたが、もともとこれは structure.sql 生成を主眼にした設計です。

  • しかし実装上は、同じ SQL 生成ロジックを :ruby ローダーも使っていたため、version formatter の契約が「schema.rb ローダーからも呼ばれることを前提にしないと壊れる」状態になっていました(= 責務の漏れ・インターフェースの汚染)。

今回の変更の趣旨

schema.rb ローダー側はローダー専用の内部 SQL を使い、version formatter は structure.sql ダンパー専用の契約として扱う」

という形に変えて、スキーマ読み込み処理(ローダー)と version formatter を疎結合にすることが目的です。

実際のコード変更のイメージ

変更ファイルは1つだけです:

  • activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb (+3/-1)

差分は小さいですが、やっていることは概ね以下のようなことです(擬似コードイメージ):

ruby
# 以前: ローダー・ダンパー共通の SQL 生成ロジック
def sql_for_recording_schema_migration(versions)
  # version formatter を通して SQL を生成
end

def load_schema_from_ruby
  # ここからも上の共通 SQL 生成を呼んで execute していた
end

def dump_schema_information_to_sql
  # 同じメソッドを通して SQL を structure.sql に書き込んでいた
end

# 今回: ローダーとダンパーを分離
def load_schema_from_ruby
  # ローダー専用の、より直接的な SQL / 挿入処理を使う
  # (version formatter 非依存)
end

def dump_schema_information_to_sql
  # こちらは version formatter と連携して SQL を生成し、
  # structure.sql に書き込む
end

実際には3行の追加と1行の削除のみですが、

  • 「ローダー側で共通の SQL 生成メソッドを呼ばないようにする」
  • もしくは「ローダー専用の小さな SQL 片(またはメソッド)を持たせる」

といった形で、ローダーが version formatter 契約に依存しないようにしています


  1. 影響範囲・注意点

影響範囲

  • 一般的な Rails アプリケーション(デフォルト設定のみ使用)

    • 動作上の差異はほぼありません。
    • schema.rb / structure.sql の生成・ロードは、体感上これまで通りに動きます。
  • 独自の version formatter を実装しているプロジェクト

    • 一番影響を受ける可能性があるのはここです。
    • これまでは暗黙のうちに「schema.rb ローダーからも呼ばれることがある」前提になっていたかもしれませんが、今後は「structure.sql ダンパー専用」としての契約に整理されます
    • つまり:
      • formatter は「structure.sql に出力される SQL をどうフォーマットするか」だけを考えればよくなり、
      • schema.rb ロード時の挙動や、データベースに対する直接 execute の側面は考えなくてよい、という整理がされます。

注意点

  • この PR 自体は非常に小さな差分で、**現時点では破壊的変更というより「将来の契約の明確化のための内部整理」**に近いです。
  • 作成者コメントにもあるように、version formatter の正式な契約仕様は別パッチでドキュメント化される予定です。
  • version formatter を自作している場合は、そのドキュメントが公開されたタイミングで、
    • structure.sql 生成時だけを対象としているか」
    • 「ローダー向けの挙動に依存していないか」 を確認することをおすすめします。

  1. 参考情報 (あれば)

#58087 Fix MySQL POINT and MULTIPOINT columns being misreported as integers

マージ日: 2026/7/11 | 作成者: @kyuuri1791

  1. 概要 (1-2文で)
    MySQL の POINT / MULTIPOINT カラムが ActiveRecord 上で誤って整数型として扱われていた不具合を修正し、他の空間型と同様に「未知の型」として扱うようにした PR です。これにより、スキーマ情報の取得やスキーマ変換ツール利用時に不正な integer 定義が紛れ込む問題を防ぎます。

  1. 変更内容の詳細

問題の内容

MySQL で以下のようなテーブルを作成した場合:

ruby
connection = ActiveRecord::Base.lease_connection
connection.execute("CREATE TABLE spots (loc POINT, locs MULTIPOINT, shape POLYGON)")
columns = connection.columns("spots").index_by(&:name)

columns["loc"].type    # => :integer (sql_type: "point")      ← 誤り
columns["locs"].type   # => :integer (sql_type: "multipoint") ← 誤り
columns["shape"].type  # => nil      (sql_type: "polygon")    ← 未知の型として扱われている

POINT / MULTIPOINT という型名に "int" という文字列が含まれているため、AbstractAdapter 側の汎用ルール %r(int)i にマッチしてしまい、整数型として解釈されていました。
一方、POLYGON など他の空間型はこのルールにマッチせず、適切に「未知の型」(nilActiveRecord::Type::Value)として扱われています。

修正方針

  • 汎用ルール %r(int)i 自体は他のアダプタ(特に SQLite の type affinity 対応など)で意図的に使われているため、ここは変えない
  • 代わりに、MySQL 系アダプタ用の抽象クラス AbstractMysqlAdapter 内で、POINT / MULTIPOINT を明示的に「未知の型」として登録することで上書き(オーバーライド)する。
  • MySQL 用の 2 つのアダプタ(mysql2 / trilogy)で共通の挙動になるよう、抽象アダプタ (AbstractMysqlAdapter) に実装を追加。

実際の変更点

1) AbstractMysqlAdapter#initialize_type_map の拡張

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb にて、型マップ初期化処理に POINT / MULTIPOINT を登録する行が追加されています。イメージとしては以下のようなコードになります(実際のコードは多少文脈が付きますが、意味としてはこれと同じです):

ruby
def initialize_type_map(m = type_map)
  super

  # 既存の MySQL 固有型の登録に加えて……
  register_class_with_limit m, %r(char)i,      Type::String
  # などなど…

  # POINT / MULTIPOINT を未知の型 (Type::Value) として扱う
  m.register_type(/point/i,      ActiveRecord::Type::Value.new)
  m.register_type(/multipoint/i, ActiveRecord::Type::Value.new)
end

これにより、POINT / MULTIPOINT%r(int)i による整数型マッチよりも優先され、ActiveRecord::Type::Value として処理されます。

2) テスト追加

activerecord/test/cases/adapters/abstract_mysql_adapter/spatial_type_test.rb にテストが追加されています。
テストでは、POINTMULTIPOINT のカラムが:

  • sql_type として point / multipoint を持ち
  • ActiveRecord の columns(...).type などを通じて「integer ではない(= 数値系 type にマップされていない)」
  • 他の空間型同様に Type::Value として扱われる

ことを確認しています。

3) CHANGELOG 更新

activerecord/CHANGELOG.md にバグ修正としてエントリが追加され、今回の変更がアプリケーション挙動に影響しうることが明記されています。


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

    • MySQL を使っており、テーブルに POINT / MULTIPOINT 型カラムを定義しているアプリケーション。
    • これまで columns[name].type == :integer で判定していたようなコードがある場合、今回の修正により :integer ではなく「未知の型」扱いとなり、挙動が変わります。
      • 例: 「整数カラム一覧」を作るロジックに POINT / MULTIPOINT が紛れ込んでいた場合、今後はそこから除外されます(これは本来の正しい挙動)。
  • ActiveRecord 上での扱い

    • POINT / MULTIPOINT 向けの専用型(例えば PostGIS のようなジオメトリ型)が提供されるわけではなく、あくまで 「未知の型(Type::Value)」として扱うだけ です。
      • つまり、ActiveRecord 経由でこれらを扱う場合は、文字列やバイナリとして自前でパースする等の処理は引き続き必要です。
      • ただし少なくとも「暗黙に integer とみなされる」という誤った状態は解消されます。
  • スキーマ・マイグレーション系ツールへの影響

    • PR の動機にもある通り、MySQL → PostgreSQL のスキーマ変換ツールなどで、POINT / MULTIPOINT が integer と誤認識された結果、変換先スキーマが壊れる問題が解消されます。
    • ActiveRecord ベースで schema dumper / loader を実装しているツールでも、カラム型判定ロジックに依存している場合は挙動が変わり得ますので、一度確認するとよいです。

  1. 参考情報 (あれば)
  • PR 本文にあるツール:

    • MySQL → PostgreSQL スキーマ同期ツール: https://github.com/kyuuri1791/schema_ferry
      このツールの開発中に、POINT / MULTIPOINT が integer と誤検出される問題として発見されたものです。
  • 関連するコンセプト:

    • ActiveRecord の type map (initialize_type_map) は、DB の生の sql_type(例: "point", "varchar(255)")を Ruby 側の型クラス (ActiveRecord::Type::Integer, ActiveRecord::Type::String, ActiveRecord::Type::Value 等) にマッピングする仕組みであり、今回の修正はそのマッピングルールの一部を MySQL 専用に上書きしたものです。
    • %r(int)i のようにアンカーなし・大文字小文字無視の正規表現を使っているため、POINT / MULTIPOINT のような「int を部分的に含む型名」が誤マッチしうる、という背景知識があると理解しやすいです。

#58076 Interpolate a fixture label verbatim, without backreference expansion

マージ日: 2026/7/11 | 作成者: @55728

  1. 概要 (1-2文で)
    Rails の fixture で使える特殊変数 $LABEL を展開する際に、ラベル文字列中の \&\1 などが正規表現の後方参照として解釈されてしまう問題を修正し、ラベルを「そのままの文字列」として挿入するようにした PR です。これにより、バックスラッシュを含むラベルでも意図通りの値が fixture に設定されます。

  1. 変更内容の詳細

問題の背景

Rails の fixture では、以下のように値の中で $LABEL を使うと、その行のラベル名が展開されます:

yml
backref_parrot:
  name: $LABEL

ここで、ラベルが back\&ref といった、バックスラッシュ付きの文字列だった場合に問題が発生していました。

PR 説明から読み取れる「以前の実装」は、$LABEL の置き換えに Ruby の String#gsub とブロックを使わず直接の置換をしており、その中で「置換後文字列として」ラベルが渡されていたと考えられます。Ruby の gsub では、置換「後」文字列中の:

  • \& … マッチした全体文字列
  • \0\9 … キャプチャグループの後方参照
  • \k<name> … 名前付きキャプチャへの後方参照

などが特別扱いされます。そのため、

  • ラベルが back\&ref の場合、\& が「マッチ文字列(ここでは $LABEL)」に展開されてしまう
  • \1 などが含まれると、後方参照展開の結果としてその部分が消えたり不正な値になる

といった不具合が起きていました。

PR 内の「Before / After」の例:

yml
back\&ref:
  name: $LABEL
  • Before: name => back$LABELref
    • \&$LABEL に展開されてしまった(本来は「&」として扱いたい)
  • After: name => back\&ref
    • ラベル文字列そのものがそのまま入る

実際の変更点

activerecord/lib/active_record/fixture_set/table_row.rb$LABEL 展開ロジックが 1 行変更されています(+1 / -1)。

本体コードは PR からは全文がわかりませんが、挙動からすると例えば以下のような変更が行われています:

  • 以前 (イメージ):

    ruby
    value.gsub(LABEL_RE, label) # 第2引数に生文字列を渡していた

    これだと label 内の \&\1 が「置換後パターン」として解釈される。

  • 以後 (イメージ):

    ruby
    value.gsub(LABEL_RE) { label } # ブロックで返す

    ブロックから返した文字列はそのまま挿入されるため、\ を含んでいても特別扱いされない。

Ruby の gsub において、「置換後文字列を文字列リテラルで与える」場合だけ、\& などが特別扱いされますが、「ブロックの戻り値として与える」場合はそうした解釈はされません。そのため、今回のような「ラベルを verbatim(生の文字列)で入れたい」ケースではブロック形式が正しい実装になります。

テストの追加

2 つのテストファイルが更新されています。

  1. activerecord/test/cases/fixtures_test.rb (+4)

    $LABEL とバックスラッシュを含むラベルの組み合わせをカバーするテストが追加されています。例えば:

    ruby
    test "fixture label with backslash is interpolated verbatim" do
      parrot = parrots(:'back\&ref')
      assert_equal 'back\&ref', parrot.name
    end

    といった内容が追加されていると考えられます(ファイル追加行数・PR 説明からの推測を含みます)。

  2. activerecord/test/fixtures/parrots.yml (+5)

    上記テスト用の fixture が追加されています。例えば:

    yml
    back\&ref:
      name: $LABEL

    のようなデータが登録され、$LABEL 展開が期待どおりか確認しています。


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

    • ActiveRecord の fixtures 機能を利用し、値の中で $LABEL を使っているケース。
    • 特に「ラベル名にバックスラッシュを含めている」かつ「そのラベルを $LABEL で展開している」ケースに影響があります。
  • 互換性 / 破壊的変更の可能性

    • 「普通の識別子スタイル」のラベル(英数字・アンダースコアのみなど)はそもそもバックスラッシュを含まないため、この変更の影響を受けません。
    • 以前、ラベル中の \& が「マッチ文字列に展開される」という挙動に依存していたコードがあれば、挙動が変わります。ただしそれは Rails の意図した仕様ではなく、gsub の副作用に依存した偶然の挙動なので、修正は妥当といえます。
    • ラベル中の \1 などが「消えてしまう」不具合が解消されるため、そのようなラベルを使っていた場合は fixture の値が本来の意図通りに変わります。
  • 注意点

    • この修正により、今後 $LABEL 展開結果が「ラベル名そのもの」に必ず一致することが保証されます。
    • ラベル名に正規表現ライクな記号を含めても安全に $LABEL が使えるようになります。

  1. 参考情報 (あれば)
  • PR 本文の Summary にある Ruby 正規表現置換の特性:
    • 置換後文字列における \&, \0\9, \k<name> は特別な意味を持つが、gsub { ... } のブロック戻り値には適用されない。
  • 関連する Ruby ドキュメント:

この PR により、fixtures における $LABEL の仕様が「ラベル名を文字列としてそのまま挿入する」という直感的なものに揃えられ、特殊文字を含むラベルでも安全に利用できるようになっています。


#58080 Round-trip the null: false attribute modifier in GeneratedAttribute#to_s

マージ日: 2026/7/11 | 作成者: @55728

  1. 概要 (1-2文で)
    Rails のジェネレータで使う属性表現(GeneratedAttribute#to_s)において、string! のような ! 修飾子(= null: false)が文字列表現に正しく反映されずラウンドトリップできなかった問題を修正し、! を含む属性指定がパース前後で同じ表記に戻るようにした PR です。これにより、属性文字列表現のシリアライズ/デシリアライズの一貫性と正しさが改善されます。

  1. 変更内容の詳細

背景: ! 修飾子と null: false

Rails のジェネレータでは、モデル生成などの際に以下のような書き方ができます。

bash
rails g model User name:string! age:integer

ここで string!! は「NOT NULL 制約」を意味し、内部的には attr_optionsnull: false として保存されます。

問題は、この内部表現を文字列化する GeneratedAttribute#to_s(およびそこから呼ばれる print_attribute_options)において、

  • ! に対応する「専用の文字列表現」が定義されていなかった
  • そのため null: false が他のオプションと同列に { ... } の中に混ぜられてしまい、再パース時に意味が変わったり、パース不能になったりしていた

という点です。

Before の挙動

PR 説明にある「Before」の具体例:

text
name:string!          → name:string{null}          (no longer parses as that type)
name:integer{4}!      → name:integer{4}            (modifier silently dropped)
name:references{polymorphic}! → name:references{polymorphic,null}
                                                    (parses back as null: true)

それぞれ解説すると:

  1. name:string!

    • 入力: name:string!(NOT NULL を意味)
    • 出力: name:string{null}
    • 問題点:
      • {null} という形は、もともとの「! 付き型表現」として正しく再パースできない
      • そもそも {null} が何を意味するか曖昧で、実質壊れた表現
  2. name:integer{4}!

    • 入力: name:integer{4}!
    • 出力: name:integer{4}
    • 問題点:
      • ! 由来の null: false が文字列表現から完全に消えてしまう(制約がサイレントに失われる)
      • ラウンドトリップ時に NOT NULL が落ちる
  3. name:references{polymorphic}!

    • 入力: name:references{polymorphic}!
    • 出力: name:references{polymorphic,null}
    • 問題点:
      • {polymorphic,null} というオプション表現を再パースすると null: true 相当として解釈されてしまう
      • つまり null: false が逆転して null: true 相当の意味に変わる可能性がある

このように、「! 付きで指定した NOT NULL 制約」が、文字列表現化 → 再パースという往復(ラウンドトリップ)で:

  • 消える
  • 逆の意味になる
  • あるいはパース不能になる

という不具合がありました。

After の挙動

PR 説明では「After — all three round-trip to themselves.」とある通り、修正後は次のように「往復しても同じ表記に戻る」ようになっています。

text
name:string!                         → name:string!
name:integer{4}!                     → name:integer{4}!
name:references{polymorphic}!       → name:references{polymorphic}!

この修正のポイントは:

  • GeneratedAttribute#to_s(と print_attribute_options)で、
    • すでに attr_options にある null: false を、単に {null: false} のようなオプション表現に紛れ込ませるのではなく
    • 元の ! 修飾子という「構文レベルの情報」として復元できるようにした
  • つまり、「! というシンタックス」と「attr_options[:null] == false という内部表現」を正しくラウンドトリップ(二方向)させるようにした

コード上は:

  • railties/lib/rails/generators/generated_attribute.rb
    • to_sprint_attribute_options まわりで null: false を検出し、! として出力する処理が追加・調整されている
    • 他のオプション(例: {4}, {polymorphic})との組み合わせでも、! が落ちたり意味が変わらないように出力順序や表現が整理されている
  • railties/test/generators/generated_attribute_test.rb
    • 上記 3 パターン(および関連ケース)がテストとして追加され、ラウンドトリップの正しさを保証している

  1. 影響範囲・注意点
  • 対象範囲:
    • Rails ジェネレータで使用される GeneratedAttribute の文字列表現 (#to_s) に限定された変更です。
    • 特に、attribute! 形式で NOT NULL を指定している場合、および {...} オプションと ! を組み合わせているケースに影響します。
  • 影響内容:
    • これまで #to_s の出力を自前でパースして再利用していたツール・スクリプトがある場合、出力形式が変わる可能性があります。
      • 例: 以前は name:string{null} のような形式が出ていたところが、name:string! のように変わる。
      • ただし、これは本来意図されていた正しい表現への修正であり、ジェネレータ仕様としてはむしろ改善です。
    • 既存コードで {null}{polymorphic,null} のような「バグ起因の表現」を前提にパースしている場合は、パーサを見直す必要があります。
  • 実行時の挙動:
    • 実際に生成されるマイグレーションや DB スキーマの挙動(NOT NULL 制約の有無)そのものは、この PR によって変わらないはずです。
    • あくまで「属性の文字列表現」と「そのラウンドトリップ」が正しくなっただけで、DB 制約の解釈ロジックは従来どおりです。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58080
  • 関連クラス:
    • Rails::Generators::GeneratedAttribute
  • 関連する使い方の例(ジェネレータ):
    bash
    # NOT NULL な name カラムを持つ users テーブル
    rails g model User name:string!
    
    # 長さ指定や polymorphic オプションと ! の組み合わせ
    rails g model Product code:integer{4}! owner:references{polymorphic}!

#58086 Convert Symbol keys in a HashWithIndifferentAccess#transform_keys mapping

マージ日: 2026/7/11 | 作成者: @55728

  1. 概要 (1-2文で)
    HashWithIndifferentAccess(以下 HWIA)の transform_keys / transform_keys! に「変換マッピング用のハッシュ」を渡したとき、マッピング側のキーが Symbol の場合でも正しくキー変換されるように修正した PRです。これにより、Ruby 標準の Hash#transform_keys(hash) と同じ挙動になり、HWIA 内の他メソッド (slice, dig, fetch, []) との一貫性も取られました。

  1. 変更内容の詳細

これまでの問題点

with_indifferent_access したハッシュは、内部的には「文字列キー」で管理されていますが、アクセス時には Symbol / String を意識せずに使えるよう convert_key を通していました。

例:

ruby
h = { a: 1, b: 2 }.with_indifferent_access
h[:a]      # => 1
h["a"]     # => 1

しかし、transform_keys(hash) に「旧キー → 新キー」のマッピングハッシュを渡す場合、そのマッピングの“キー”には convert_key が適用されていなかったため、Symbol キーのマッピングが内部の String キーと一致せず、変換が起きませんでした。

ruby
{ a: 1, b: 2 }.with_indifferent_access.transform_keys(a: :x)
# 期待: { "x" => 1, "b" => 2 }
# 実際(修正前): { "a" => 1, "b" => 2 }  # 何も起こらない

一方、マッピング自体を文字列キーで書いた場合は動いていました:

ruby
{ a: 1, b: 2 }.with_indifferent_access.transform_keys("a" => "x")
# => { "x" => 1, "b" => 2 }

この挙動は:

  • Ruby 標準の Hash#transform_keys({ a: :x }) と比べて直感に反する
  • HWIA の他の API (slice, dig, fetch, []) がすべて引数キーを convert_key に通しているのと不整合

という問題がありました。

今回の修正内容

今回の PR では、HashWithIndifferentAccess#transform_keys が「マッピング用ハッシュ」を受け取った場合、そのマッピングのキーを convert_key に通してから比較・適用するようにしました。

ポイント:

  • マッピングハッシュのキーを convert_key するように変更(slice と同様の扱い)
  • これにより、Symbol キーのマッピングでも内部の String キーとマッチする
  • transform_keys!transform_keys に委譲しているため、同じ修正で不具合が解消

修正後の挙動:

ruby
h = { a: 1, b: 2 }.with_indifferent_access

h.transform_keys(a: :x)
# => { "x" => 1, "b" => 2 }

# 文字列キーのマッピングは従来どおり
h.transform_keys("a" => "x")
# => { "x" => 1, "b" => 2 }

ブロック形式や、マッピング + ブロックの複合形も挙動は変わらず、以下が確認されています:

  • 文字列キーのマッピングは従来通り動作
  • ブロック形式 (transform_keys { |k| ... }) は影響なし
  • ハッシュ + ブロック併用時も、マッピングにないキーはブロック処理される挙動を維持
  • 非破壊版 transform_keys はレシーバを変更しない
  • 非ハッシュ(nil, 42 など)をマッピングに渡した場合は、従来通り TypeError が発生

変更は本体 1 行・テスト 21 行追加と、ごく小さなピンポイント修正です。


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

    • HWIA に対して transform_keys(a: :x) のように「Symbol キーのマッピング」を渡していたコード
    • これまで「なぜか変換されていなかった」ケースが、期待どおり変換されるようになります。
  • 後方互換性の観点

    • これまで「Symbol キーのマッピングは常に no-op」として rely しているコードがあれば挙動が変わりますが、これはバグ依存であり、Ruby 本体や HWIA の他メソッドと比べても修正後の方が一貫しているため、通常は望ましい互換性変更と考えられます。
    • String キーで書いていたマッピング、ブロック形式、transform_keys! の破壊的挙動には変更なし。
  • テスト

    • HWIA 用のテストファイルにテストケースが追加されており、
      • Symbol キーのマッピングで期待どおり変換されること
      • 既存の動作が壊れていないこと が自動テストで担保されています。

  1. 参考情報 (あれば)

#58052 Combine equivalent owner-dependent preload scopes

マージ日: 2026/7/10 | 作成者: @rwalters44

  1. 概要 (1-2文で)
    owner(親レコード)依存のスコープ付き関連(いわゆるインスタンス依存スコープ)の eager load 時に、実質同じ条件のスコープをまとめて 1 つの Preloader に統合するようにした PRです。これにより、SQL の発行回数は変わらないまま、Preloader オブジェクトの数とレコードスキャン回数が大幅に減り、高負荷ケースで性能が大きく改善されます。

  1. 変更内容の詳細

問題の背景

ruby
class Project < ActiveRecord::Base
  has_many :tasks,
    ->(project) { where(account_id: project.account_id) }, # owner 依存スコープ
    inverse_of: :project
end

のように、ブロック(->(owner) { ... })を使った owner 依存スコープを持つ関連を preload/includes すると、各 owner ごとにスコープを評価します。

多くの owner が「同じ値」を持っている場合(例: 全ての Projectaccount_id が 1)、評価後に生成される Relation は実質同じになりますが、これまでの実装では

  • owner ごとに別々の reflection_scope を生成
  • それぞれに対して Preloader::Association を生成

しており、「同じクエリを発行する Preloader を無駄に多数作る」状態になっていました。

SQL 自体はその後のバッチングで 1 本にまとめられていたため、SQL カウントは増えない一方で、

  • Preloader オブジェクトが owner 数だけ増える
  • child レコードとのマッチング(n x m 問題)で無駄なループ・スキャンが増える

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

何をしたか

ActiveRecord::Associations::Preloader::Branch#preloaders_for_reflection の挙動を変更し、owner 依存スコープに限って、「結果として同じ Relation になるスコープ」をまとめて 1 つの Preloader に統合するようにしました。

ポイント:

  • 対象は owner 依存スコープのみ

    ruby
    if reflection.scope && reflection.scope.arity != 0
      # owner 依存スコープとして特別扱い
    end

    reflection.scope.arity != 0(引数ありのスコープブロック)の場合だけ新しい grouping ロジックを使います。非 owner 依存スコープは従来どおり。

  • 「等価なスコープ」の判定方法
    生成された Relation を次のキーでグルーピングします:

    • 関連クラス (klass)
    • テーブル名
    • connection specification name(接続設定)
    • values_for_queries
      → これは既存の「preloader の SQL バッチング」で使われている Relation 等価判定と同じ指標で、SQL クエリとして同じものになる Relation を同一グループと見なします。

    疑似コード的には次のようなイメージです(実際のコードはもう少し複雑ですが概念的にはこれ):

    ruby
    reflection_records
      .group_by { |owner|
        klass = owner.association(association).klass
        reflection_scope =
          reflection.join_scopes(klass.arel_table, klass.predicate_builder, klass, owner).inject(&:merge!)
    
        [
          klass,
          reflection_scope.table_name,
          klass.connection_specification_name,
          reflection_scope.values_for_queries
        ]
      }
      .map { |(_klass, _table, _conn, _values), owners_for_group|
        preloader_for(reflection).new(klass, owners_for_group, reflection, scope, reflection_scope, associate_by_default)
      }

    これにより、「owner は違うが、結果として同じ WHERE / JOIN 条件などを持つ Relation」を 1 つの Preloader にまとめます。

  • 非 owner 依存スコープは動作変更なし
    scope.arity == 0 の関連(λ の引数を取らない、もしくは scope 自体が無いもの)は、従来どおり 1 つの Preloader にまとまり、今回の grouping ロジックの影響を受けません。

  • テストの拡張
    既存の「インスタンス依存 preload」テストを拡張し、「等価な owner 依存スコープを持つ 2 つの owner(PR 説明では david が 2 レコード)」が 同じ Preloader を共有していることをアサートするようになりました。
    これにより、「同一スコープが複数の Preloader にばらけない」ことをテストで保証しています。

ベンチマーク結果の要点

  1. owner 依存スコープの micro benchmark

    • 1000 owner / owner 依存 scope
    ケースBeforeAfter変化
    全 owner が同じ scope(1 グループ)loaders: 1000 / SQL: 1 / 20.40msloaders: 1 / SQL: 1 / 8.81ms約 2.3x 高速、loader 1000x 減
    scope が 10 グループに分かれるloaders: 1000 / SQL: 10 / 21.80msloaders: 10 / SQL: 10 / 10.75ms約 2.0x 高速、loader 100x 減
    非 owner 依存 scopeloaders: 1 / SQL: 1 / 9.52msloaders: 1 / SQL: 1 / 9.11ms変化ほぼなし
    • SQL の数は元からバッチングされていたので「変わらない」
    • パフォーマンス改善は「Preloader 数削減 + n x m マッチングの削減」によるもの
  2. Generic preloader fanout benchmark

    条件:

    • 親レコード 1,000 件
    • 子レコード 2,000 件
    • 親は全て同じ account_id → owner 依存スコープの結果は 1 つの等価スコープになる
    • sqlite3 in-memory / 50 iteration
    ケースLoadersSQLRaw record scansAvg time
    通常の has_many :items112,00016.70ms
    旧 owner 依存スコープ1,00012,000,0001097.94ms
    新 owner 依存スコープ112,00025.19ms
    • 旧実装では 2,000 レコードを 1,000 Preloader がそれぞれ 2,000 件ずつ見るような形になり、2,000,000 回のスキャンが発生
    • 新実装では通常の has_many と同じく 1 Preloader で 2,000 件を見るだけになり、スキャン回数・時間ともに大幅削減

  1. 影響範囲・注意点
  • 対象となるのは owner 依存スコープ付き関連を preload / includes / eager_load するコードです。
    • 例: has_many :tasks, ->(project) { where(account_id: project.account_id) }
  • 非 owner 依存の関連、通常の has_many / has_one / belongs_to などには挙動変更はありません。
  • SQL の内容・件数は(元々バッチングされていたケースでは)基本的に変わりません。
    変わるのは「Preloader の個数」と「Ruby 側の matching コスト」であり、アプリ側ロジックから見える挙動は変わらない想定です。
  • Relation 等価判定には values_for_queries 等を用いているため、
    • 「同じ SQL になる Relation は同じ Preloader にまとめる」
    • 「異なる SQL になる Relation は別 Preloader のまま」 という、既存のバッチングロジックと整合的な振る舞いです。
  • owner ごとのスコープ評価順序や内側の実装に依存しているような非常にトリッキーなコードがあると、理論上は影響しうるものの、普通のアプリではまず問題にならない設計変更です。

期待されるメリット:

  • 大量の owner に対して同種の owner 依存スコープ付き関連を preload している処理で、メモリ使用量と CPU 時間が大幅に減る可能性があります。
  • 特に「マルチテナントで account_id で絞る」などの典型的なパターンで、全 owner の account_id が同じ/少数種類というケースでは効きやすいです。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58052

  • 関連クラス:

    • ActiveRecord::Associations::Preloader::Branch#preloaders_for_reflection
  • owner 依存スコープの典型例:

    ruby
    class Author < ApplicationRecord
      has_many :posts_mentioning_author,
        ->(author) { where("posts.body LIKE ?", "%#{author.name.downcase}%") }
    end

    このような形の関連を大量の Author に対して preload している場合、本 PR によって Preloader 数とマッチングコストが削減される可能性が高いです。


#57742 Introduce ActiveJob::DeserializationError::RecordNotFound

マージ日: 2026/7/10 | 作成者: @rosa

  1. 概要 (1-2文で)
    ActiveJob の引数デシリアライズ時に「レコードがない場合」と「それ以外のエラー」を区別するために、新しく ActiveJob::DeserializationError::RecordNotFound 例外が導入されました。これにより、レコード削除など「正当な」理由による失敗だけを discard し、ネットワーク障害など一時的なエラーは通常どおりリトライさせる運用が可能になります。

  1. 変更内容の詳細

背景にある問題

  • これまで ActiveJob では、ジョブ引数のデシリアライズ中に起きたあらゆるエラーを ActiveJob::DeserializationError でラップしていました。

  • 多くのアプリは雛形コメントに従って以下のように設定していることが多いです:

    ruby
    # Most jobs are safe to ignore if the underlying records are no longer available
    discard_on ActiveJob::DeserializationError
  • しかし、実際には「レコードが存在しない」だけでなく、DB との一時的なネットワーク障害 (例: TRILOGY_TRUNCATED_PACKET) などもこの例外に包まれます。

  • その結果、短時間のネットワーク障害が発生しただけで、本来はリトライすべき正常なジョブがすべて「静かに discard される」危険があり、かなりのフットガンになっていました。

新しい例外クラスの導入

この PR では以下の新例外が追加されています:

ruby
# activejob/lib/active_job/exceptions.rb
module ActiveJob
  class DeserializationError < StandardError
    class RecordNotFound < DeserializationError; end
  end
end

ポイント:

  • ActiveJob::DeserializationError::RecordNotFoundActiveJob::DeserializationError を継承するサブクラス。
  • 「デシリアライズ時に、対象のレコードが見つからない場合にのみ」 発生します。
  • それ以外の原因 (ネットワークエラーやクエリエラーなど) の場合は、従来どおり上位の ActiveJob::DeserializationError が使われます。

GlobalID の fetch を利用した実装

  • PR では GlobalID 側に導入された GlobalID::Locator.fetch (https://github.com/rails/globalid/pull/206) を利用するように変更されています。
  • fetch は「見つからない場合に例外を投げる」インターフェースで、ここで GlobalID::Locator#locate + nil チェックではなく、明示的な例外とその原因を使って RecordNotFound を判定しています。
  • activejob/lib/active_job/arguments.rb 内で、GlobalID からレコードを復元する処理が更新され、ActiveRecord::RecordNotFound 等を拾って ActiveJob::DeserializationError::RecordNotFound にラップする形になっています。

(概念イメージ)

ruby
def deserialize_global_id(gid)
  GlobalID::Locator.fetch(gid)
rescue ActiveRecord::RecordNotFound => e
  raise ActiveJob::DeserializationError::RecordNotFound.new(e.message)
rescue => e
  raise ActiveJob::DeserializationError.new("Error while trying to deserialize arguments: #{e.message}")
end

※ 実際のコードは多少異なりますが、ロジックとしては上記のように「RecordNotFound 系だけを特別扱い」しています。

使い方の変更例

これまで (問題があるパターン)

ruby
class ApplicationJob < ActiveJob::Base
  discard_on ActiveJob::DeserializationError
end

この場合:

  • レコードが消えていても discard
  • ネットワーク障害でデシリアライズに失敗しても discard
    → 望ましくない

今後推奨されるパターン

ruby
class ApplicationJob < ActiveJob::Base
  # 「レコードがもう存在しない」ケースだけ discard する
  discard_on ActiveJob::DeserializationError::RecordNotFound
end

こうすると:

  • レコード削除に起因するデシリアライズ失敗 → discard
  • DB ネットワーク障害やその他エラー → 通常のエラーハンドリング (リトライ等) が動作

ActiveRecord::RecordNotFound を使っているジョブへの影響

説明文にもある通り、以下のようにしていたジョブは注意が必要です:

ruby
# 旧来のパターン
discard_on ActiveRecord::RecordNotFound

これは「perform 内でレコードアクセスして RecordNotFound が起きた場合」にも、「デシリアライズ中にレコードが見つからない場合」にも同じ ActiveRecord::RecordNotFound が使われ得ました。

この PR 後は:

  • デシリアライズ中: ActiveJob::DeserializationError::RecordNotFound
  • perform 実行中: ActiveRecord::RecordNotFound

と分かれるので、「どちらのパスでも discard したい」場合は明示的に両方書く必要があります:

ruby
discard_on ActiveRecord::RecordNotFound,
           ActiveJob::DeserializationError::RecordNotFound

標準ジョブ / ガイド / ジェネレータの更新

  • Active Storage の各種ジョブ (AnalyzeJob, PurgeJob など) や Action Mailbox の IncinerationJobdiscard_on のターゲットを ActiveJob::DeserializationError から ActiveJob::DeserializationError::RecordNotFound に変更。
  • guides/source/active_job_basics.md の解説も更新され、「レコードが存在しない場合だけ discard したいなら ActiveJob::DeserializationError::RecordNotFound を使う」旨の説明が追加。
  • rails g job などで生成される ApplicationJob テンプレート (railties/lib/rails/generators/rails/app/templates/app/jobs/application_job.rb.tt) も同様にコメントが調整され、間違った使い方を誘発しないようになっています。

  1. 影響範囲・注意点

互換性

  • 新しい ActiveJob::DeserializationError::RecordNotFoundActiveJob::DeserializationError のサブクラスなので、
    • 既存で rescue_from ActiveJob::DeserializationErrordiscard_on ActiveJob::DeserializationError をしているコードは、そのままでも 動作は変わりません (RecordNotFound も引き続き捕捉されます)。
  • つまり、この PR は「後方互換性を保ちつつ、より細かい制御ができるようにした」変更です。

実運用上の推奨対応

  1. discard_on ActiveJob::DeserializationError をグローバルに設定している場合は、見直し推奨

    • ネットワーク事故時にジョブが黙って捨てられるリスクがあるため、

    • 可能なら ActiveJob::DeserializationError::RecordNotFound に絞るほうが安全です。

    • 用途に応じて、以下のように分けるとよいです:

      ruby
      # デシリアライズでレコードがない場合だけ discard
      discard_on ActiveJob::DeserializationError::RecordNotFound
      
      # それ以外の DeserializationError は通常の retry_on ハンドリングに任せる
      retry_on ActiveJob::DeserializationError, wait: 5.seconds, attempts: 3
  2. discard_on ActiveRecord::RecordNotFound を使っているジョブで「デシリアライズ時も含めて discard したい」場合

    • 明示的に両方書いてください:

      ruby
      discard_on ActiveRecord::RecordNotFound,
                 ActiveJob::DeserializationError::RecordNotFound
  3. モニタリング・アラート

    • これまで ActiveJob::DeserializationError をひとまとめでアラートしていた場合、RecordNotFound とそれ以外 (ネットワーク障害など) を分けて集計した方が、原因分析と運用ポリシーの設計がしやすくなります。

  1. 参考情報 (あれば)

#58061 Fix some FrozenErrors for ractorized application

マージ日: 2026/7/9 | 作成者: @hmcguire-shopify

  1. 概要 (1–2文で)
    Rails アプリケーションを Rails.application.ractorize! で共有可能(freeze)にした際に発生していた FrozenError を、キャッシュのローカルキャッシュキーとアプリケーションの revision を「後からではなく事前に」計算するようにすることで解消する PR です。Ractors 対応アプリでのエラー報告や Rails.cache 利用が、freeze 後でも安全に動作するようになります。

  1. 変更内容の詳細

背景となる問題

  • Rails.application.ractorize! を呼ぶと、Rails.application が shareable/frozen になり、その中に含まれる Rails.cache も再帰的に shareable/frozen になります。
  • Rails.cacheLocalCache ストラテジーを使っている場合、local_cache_key最初に LocalCache::Middleware がローカルキャッシュを作ろうとしたタイミングで初めて計算される、という遅延初期化になっていました。
    • しかし、その時点では Rails.cache が既に frozen になっており、local_cache_key の計算で内部状態を書き換えようとして FrozenError が発生していました。
  • 同様に、Rails.error レポーターがエラーを扱う際に、まだ設定されていない Rails.application.revision初回アクセス時に遅延初期化 していたため、Rails.application が frozen の状態だとここでも FrozenError が発生していました。

この PR は、これら「遅延初期化(lazy)」のタイミングを「インスタンス生成/初期化時(eager)」に移すことで、freeze 後の書き込みをなくしています。


変更1: LocalCache の local_cache_key をインスタンス生成時に計算

対象ファイル: activesupport/lib/active_support/cache/strategy/local_cache.rb

ポイント:

  • これまで:
    • LocalCache モジュール内で local_cache_key は最初に必要になったときに計算されていた(おそらくメモ化されたアクセサか、初アクセス時に代入)。
    • その結果、Rails.cache が freeze された後に local_cache_key を計算しようとして FrozenError 発生。
  • これから:
    • キャッシュストアに LocalCache ストラテジーがミックスインされる際(またはインスタンス生成時)に、local_cache_key先に計算しておくように変更。

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

ruby
# 以前(イメージ)
def local_cache_key
  @local_cache_key ||= :"local_cache_#{object_id}"
end

# 以後(イメージ)
def initialize(*)
  super
  @local_cache_key = :"local_cache_#{object_id}" # ここで eager に確定
end

attr_reader :local_cache_key

これにより:

  • Rails.application.ractorize! 実行前に local_cache_key が既に確定
  • freeze 後に local_cache_key を計算・代入しようとすることが無くなり、FrozenError が解消

変更2: Rails.application.revision をイニシャライザで eager 初期化

対象ファイル: railties/lib/rails/application.rb

ポイント:

  • これまで:
    • Rails.application.revision は lazily な計算で、初めて参照されたタイミング(例: Rails.error がエラーを報告する際など)で内部状態を書き換えてセットしていた。
    • しかし、Rails.error が実行されるときには Rails.application が ractorize によって frozen されている可能性があり、その場合に FrozenError
  • これから:
    • Rails.application の初期化プロセス(initializer)内で revision を前もって決定しておく。
    • 以後のアクセスはすべて既にセット済みの値を参照するだけなので、freeze 後でも安全。

イメージの例:

ruby
# どこかの initializer 内(イメージ)
initializer "rails.application.set_revision" do |app|
  app.revision # ここで一度アクセスして内部的に確定させる
end

正確なコードは PR に依存しますが、目的は「アプリケーションオブジェクトがまだ mutable なうちに revision を確定させる」ことです。


変更3: テストの追加・更新(ractors 対応)

対象ファイル: railties/test/application/ractors_test.rb

  • Ractors を利用したアプリケーション環境で、今回の修正により FrozenError が発生しないことを検証するテストを追加。
  • 具体的には:
    • Rails.application.ractorize! を呼んだ上で、
      • Rails.cacheLocalCache 使用時)の利用がエラーにならないこと
      • Rails.error によるエラー報告で Rails.application.revision に起因する FrozenError が出ないこと を確認するテストが含まれていると考えられます。

  1. 影響範囲・注意点
  • Rails の ractor 対応 (Rails.application.ractorize!) を有効にしていないアプリ:
    • 動作上の大きな変化はほぼありません。
    • 以前は「初アクセス時に計算されていた」local_cache_keyrevision が、「アプリ起動・インスタンス生成時に計算される」ようになるため、わずかに起動時の処理が増える可能性はありますが、実務上はほとんど無視できるレベルでしょう。
  • Ractors を積極的に使うアプリ:
    • Rails.cacheLocalCache ストラテジーを使っていても、Rails.application.ractorize! 実行後に FrozenError に遭遇しづらくなります。
    • エラー報告(Rails.error)が ractorized 環境下でもより安定して動くようになります。
  • 独自に LocalCache を拡張している場合:
    • local_cache_key の初期化タイミングに依存した特殊な実装をしていると、影響を受ける可能性があります。
    • 通常は問題ないはずですが、「オブジェクト ID に依存していて、後から変わることを期待している」ようなトリッキーなコードがあると挙動が変わる可能性があります。
  • Rails.application.revision について:
    • これまで「最初にアクセスされた時点で確定される」ことを前提とした処理(例えば、最初のアクセス時にのみ何かログを出す、など)を書いていた場合、そのタイミングが initializer 実行時に早まる点には留意してください。

  1. 参考情報 (あれば)
  • PR 本体: https://github.com/rails/rails/pull/58061
  • 関連機能:
    • Ractors 対応: Rails.application.ractorize!
    • キャッシュストアのローカルキャッシュ: ActiveSupport::Cache::Strategy::LocalCache
    • エラーレポート API: Rails.error
  • 類似の問題:
    • Ruby / Rails における「freeze 後に遅延初期化しようとして FrozenError」というパターンは、スレッドセーフ化・Ractors 対応の過程でよく発生するため、同様に「初期化は mutable なうちに済ませる」方針が複数箇所で採用されています。

#58068 Let dump_schema pass the given format down to schema_dump

マージ日: 2026/7/9 | 作成者: @fxn

  1. 概要 (1-2文で)
    ActiveRecord::DatabaseTasks.dump_schema が受け取った format 引数を内部の schema_dump 呼び出しに正しく引き渡すよう修正された PR です。これにより、dump_schema に任意のスキーマフォーマットを指定した場合でも、その指定が確実に反映されるようになりました。

  1. 変更内容の詳細

問題点

元の実装は以下のようになっており:

ruby
def dump_schema(db_config, format = db_config.schema_format) # :nodoc:
  return unless db_config.schema_dump
  ...
  schema_dump # ← 引数なしで呼び出していた
end

def schema_dump(format = schema_format)
  ...
end
  • dump_schema は呼び出し側から format を明示的に受け取れる(例: :ruby, :sql など)
  • しかし内部で schema_dump を呼び出す際に format を渡しておらず、schema_dump 側のデフォルト schema_format が常に使われていた

そのため、呼び出し側が dump_schema(db_config, :sql) のようにフォーマットを指定しても、それが無視されてしまうバグがありました。

修正内容

activerecord/lib/active_record/tasks/database_tasks.rb の該当箇所が以下のように修正されています。

変更前(概念的に):

ruby
def dump_schema(db_config, format = db_config.schema_format)
  return unless db_config.schema_dump

  # ... 省略 ...

  schema_dump # format を渡していない
end

変更後(概念的に):

ruby
def dump_schema(db_config, format = db_config.schema_format)
  return unless db_config.schema_dump

  # ... 省略 ...

  schema_dump(format) # ここで引数として渡す
end

これにより、dump_schema に渡された format がそのまま schema_dump に伝播し、意図したフォーマットでスキーマダンプされます。

テスト追加

activerecord/test/cases/tasks/database_tasks_test.rb に 23 行のテストが追加されています。テストの意図としては:

  • dump_schema に特定の format を引数として渡した場合に、
  • schema_dump がその format を受け取って呼ばれているか

を検証するものです。モックまたはスタブを使い、schema_dump に渡された引数を検査する形になっているはずです。


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

    • Rake タスクや独自スクリプト等で ActiveRecord::DatabaseTasks.dump_schema(config, :sql) など、デフォルトとは異なる format を明示的に指定している場合、これまで無視されていた指定が、今後は正しく反映されるようになります。
    • 以前の挙動に依存した(「指定しても実は効いていない」という)コードがあれば挙動が変わる可能性がありますが、通常それはバグ修正とみなせるはずです。
  • 注意点

    • db_config.schema_format を意図的にカスタマイズしていて、かつ dump_schema 呼び出しで別の format を渡している場合、今後は「引数で渡した format が優先」されます(より直感的な挙動)。
    • CI や本番でスキーマダンプを行うタスクをお持ちで、format を直接指定している場合は、一度生成物(schema.rb / structure.sql)が期待通りか確認しておくと安全です。

  1. 参考情報 (あれば)
  • 該当メソッド:
    • ActiveRecord::DatabaseTasks.dump_schema
    • ActiveRecord::DatabaseTasks.schema_dump
  • 想定利用箇所:
    • bin/rails db:schema:dump
    • bin/rails db:structure:dump
    • 独自のメンテナンススクリプトからの呼び出し等
  • PR: https://github.com/rails/rails/pull/58068

#58066 Add write_multi_entries method to batch writes for memcache store if dalli's set_multi is defined

マージ日: 2026/7/9 | 作成者: @ilianah

  1. 概要 (1-2文で)
    このPRは、ActiveSupport::Cache::MemCacheStorewrite_multi_entries メソッドを追加し、利用中の dalli に set_multi が定義されている場合はそれを使って複数キーを一括書き込みできるようにする変更です。これにより、memcached への書き込み時のラウンドトリップ数が削減され、パフォーマンス向上が見込まれます。

  1. 変更内容の詳細

追加されたもの

対象ファイル:

  • activesupport/lib/active_support/cache/mem_cache_store.rb (+17/-0)

MemCacheStore 内に、複数エントリを書き込むための内部メソッド write_multi_entries が追加されています。このメソッドは、dalli クライアントに set_multi が存在する場合はそれを利用し、存在しない場合は従来どおり個別に書くというフォールバック動作をします。

概ね以下のようなイメージの実装になっていると考えられます(擬似コード):

ruby
def write_multi_entries(entries, options)
  # entries: { "key1" => value1, "key2" => value2, ... }

  if @data.respond_to?(:set_multi)
    # dalli の set_multi を利用して一括書き込み
    # ここで memcached 用のオプションに変換する処理も挟まるはず
    @data.set_multi(entries, expires_in: options[:expires_in], raw: options[:raw])
  else
    # 旧バージョンの dalli など set_multi がない場合は従来どおりループで書き込み
    entries.each do |key, value|
      write_entry(key, value, options)
    end
  end
end

write_multi_entries 自体は Rails の内部 API で、write_multi(public API)などから呼ばれ、memcache ストアでの実際の書き込みを担当しているメソッドになります。

dalli の set_multi との連携

PRの説明にもある通り、dalli 側に以下のような変更が行われています:

今回の Rails 側の変更は、この set_multi が利用可能な場合のみ、既存コードを変えずに透過的にバッチ書き込みを有効化するものです。


  1. 影響範囲・注意点

影響範囲

  • 対象: ActiveSupport::Cache::MemCacheStore を利用し、かつキャッシュの複数キー書き込み (write_multi など) を行うアプリケーション
  • 条件: dalli のバージョンが set_multi を持っている場合
    • set_multi がある → memcached への書き込みが 1 ラウンドトリップで済む(可能な限り)
    • set_multi がない → 従来どおりキーごとに個別 set を実行

そのため、dalli を更新した場合にだけ、透過的にパフォーマンスが向上しうる変更です。既存コード側での明示的な変更は不要です。

互換性

  • 後方互換性は保たれています。
    • respond_to?(:set_multi) で有無をチェックしてから使う設計のため、古い dalli を使っていても NoMethodError にはなりません。
  • インターフェース(public API)としての MemCacheStore#write_multi の使い方は変わりません。

パフォーマンス面での注意

  • バッチ書き込みにより、1 回の write_multi で多くのキーを書き込む場合に特に効果が出ます。
  • ただし、一括で大量のデータを set_multi することで、単一リクエストあたりのペイロードが増える可能性はあります(ただし多くのケースでは往復回数削減のメリットの方が大きい)。

  1. 参考情報 (あれば)

#58058 Skip the query for a grouped calculation on a contradictory relation

マージ日: 2026/7/8 | 作成者: @55728

  1. 概要 (1-2文で)
    WHERE 句が論理的に矛盾していて絶対に行がヒットしない Relation に対して、グルーピング付き集計(group(...).count など)を行う場合、DBクエリを発行せずに即座に空ハッシュを返すように最適化した PR です。すでに非グループ集計で行っていた最適化を、グループ集計経路にも拡張した形です。

  1. 変更内容の詳細

何が問題だったか

例として、次のようなコードを考えます。

ruby
Post.where(id: []).group(:author_id).count

where(id: []) は SQL 上 WHERE 1=0 相当になり、必ず 0 件になる「矛盾した条件」です。
戻り値は Ruby 的には常に {}(空のハッシュ)ですが、これまでは次のような無駄なクエリが DB に投げられていました。

sql
SELECT COUNT(*) AS count_all, "posts"."author_id" AS author_id
FROM "posts"
WHERE 1=0
GROUP BY "posts"."author_id"

結果は空ですが、無意味なクエリ 1 回分のオーバーヘッドが発生していました。

非グループの集計メソッド(count / sum / average / minimum / maximum)については、すでに

  • Relation が「矛盾していて絶対に 0 行」だと分かる場合は
  • クエリを飛ばさずに、Ruby 側で結果を返す

というショートサーキットが入っていました。
今回の PR は 「グループ付き集計」の経路にも同じショートサーキットを追加 するものです。

実際の挙動の変化

Before

ruby
scope = Post.where(id: [])
scope.group(:author_id).count
# => {} を返すが、DB にクエリを 1 回投げる

After

ruby
scope = Post.where(id: [])
scope.group(:author_id).count
# => {} を返し、DB クエリは 0 回

つまり、「返り値は従来と完全に同じだが、DB への無駄な問い合わせがなくなる」という変更です。

実装のポイント(推定)

ファイル変更箇所から見ると:

  • activerecord/lib/active_record/relation/calculations.rb
    • グループ付きの計算を行うコードパスの先頭付近で
      • Relation が none? など、「矛盾した where で必ず 0 行」と判定できる場合に
      • {} を返す分岐を追加
  • activerecord/test/cases/calculations_test.rb
    • 矛盾した Relation に対する group(...).count
      • クエリを発行しないこと
      • {} を返すこと を保証するテストが追加

という形です。
非グループ集計側ですでに存在したロジック(@klass.none?contradiction? に相当するチェック)を、グループ計算側でも流用・適用していると考えられます。


  1. 影響範囲・注意点
  • 挙動(返り値)は従来と完全に同じ です
    • もともと {} だったものが、これからも {} のままです。
    • つまり、アプリケーションのロジック上の互換性は維持されます。
  • 影響はパフォーマンス面のみ(正の方向)
    • where(id: []) などのパターンは、フィルタリングの結果としてよく発生します(例:検索条件が一つもヒットしなかった ID 集合に対してさらに集計したい場合など)。
    • そのようなケースで、無駄な SELECT … GROUP BY クエリが 1 回減る ため、DB 負荷・レイテンシが僅かに改善します。
  • 適用されるのは「矛盾した Relation」でのみ
    • 通常の where(実際に行が返る可能性のある条件)に対しては従来通りクエリが実行されます。
    • あくまで Rails が「この Relation は絶対に 0 行」と静的に判断できるケースに限られます。
  • モニタリング/メトリクス上の違いに注意
    • DB クエリ数を監視している場合、group(...).count を多用するコードの一部でクエリ数が減る可能性があります。
    • これはバグではなく最適化によるものです。

  1. 参考情報 (あれば)
  • 類似 PR: #58004
    • none.ids がクエリなしで空配列を返すようにした変更で、本 PR と同じ「矛盾した Relation に対して DB を叩かない」クラスの最適化です。
  • 関連 API:
    • ActiveRecord::Relation#none
    • ActiveRecord::Calculations#count, #sum, #average, #minimum, #maximum
    • ActiveRecord::Relation#group

#58054 Remove some duplication in ColorField

マージ日: 2026/7/8 | 作成者: @p8

  1. 概要 (1-2文で)
    ColorFieldTextField で重複していた「値が渡されないとき/nil のときの value 属性の扱い」を共通化するリファクタリングです。TextFieldfallback_value メソッドを追加し、ColorField 側はそれをオーバーライドすることで重複コードを削除しつつ、属性の出力順序も他フィールドとより一貫したものにしています。

  1. 変更内容の詳細

背景

これまで ColorField#render は以下のような挙動を持っていました(PR 説明からの要約):

  • 値が渡されない場合(value キー自体がない / nil ではなく未指定)
    value 属性に "#000000" をセット
  • valuenil が明示的に渡された場合
    value 属性を出力しない
  • その後 superTextField#render)を呼ぶが、TextField も同様のロジックを持っており、fallback の値だけが違う
    • TextFieldvalue_before_type_cast を fallback に使う

つまり、「value がなければ fallback 値を使い、nil の場合は無視する」という処理が ColorFieldTextField の両方に重複していました。

変更1: TextField#fallback_value の追加

TextField に新たに fallback_value メソッドが追加されます。意図としては:

ruby
class ActionView::Helpers::Tags::TextField
  # 擬似コードイメージ
  def render
    options = @options.stringify_keys
    value   = options["value"]

    if options.key?("value")
      # 明示的に value が渡されている
      if value.nil?
        options.delete("value")     # nil は無視(value 属性を出さない)
      end
    else
      # value 未指定なら fallback_value を使う
      if (fallback = fallback_value)
        options["value"] = fallback
      end
    end

    # 通常の input 要素生成
    super
  end

  private

  def fallback_value
    value_before_type_cast
  end
end

※あくまで挙動を説明するためのイメージコードですが、

  • 「value が未指定なら fallback_value を使う」
  • 「value が指定されていても nil なら削除」
    というロジックを共通化し、fallback の中身だけを fallback_value で差し替え可能にした、という意図です。

TextField のデフォルト実装は value_before_type_cast を返し、従来の挙動を維持します。

変更2: ColorField から処理を削減し fallback_value をオーバーライド

ColorField 側では、これまで持っていた独自の render 内ロジック(#000000 を設定したり、キーを string 化したり)を削除し、代わりに fallback_value だけをオーバーライドする形になります。

イメージ:

ruby
class ActionView::Helpers::Tags::ColorField < TextField
  private

  def fallback_value
    "#000000"
  end
end

ポイント:

  • 「value が渡されなかったときに "#000000" を使う」という ColorField 特有の挙動は維持
  • 「value が明示的に nil のときは value 属性を出さない」という部分は TextField 側の共通実装に委譲
  • キーの string 化 (stringify_keys) も TextField 側の共通コードに任せるため、ColorField 内での重複処理が不要に

変更3: 属性の出力順序の変化

render 実装の共通化により、生成される <input type="color" ...> の属性の出力順が少し変わります。PR 説明では「他のフィールドとより一貫した順序になる」とされています。

テスト (form_helper_test) もこの新しい属性順序に合わせて修正されているため、挙動として「属性の順番が変わる」ことが仕様として固定されます(HTML 的には属性順は意味を持たないので互換性への影響は軽微と考えられます)。


  1. 影響範囲・注意点
  • ColorField の「デフォルト値 / nil の扱い」は実質的に従来通り
    • value 未指定 → #000000
    • value: nil → value 属性なし
    • これは TextField 側の汎用ロジック + fallback_value オーバーライドで表現されているだけで、意味的な挙動は変えていません。
  • フォームビルダーの出力 HTML を厳密に比較しているテスト・スナップショット等では、属性順の違いで差分が出る可能性があります。
    • Rails 本体内のテストはすでに更新済みですが、アプリ側で独自に HTML を string 比較している場合は注意が必要です。
  • TextField を継承して独自フィールドを実装している場合、fallback_value をオーバーライドすることで:
    • 「value がないときのデフォルト値の決め方」を簡潔にカスタマイズできるようになります。
    • 逆に render をゴリゴリ上書きして同様のことをしている場合は、将来的には fallback_value 利用へのリファクタ余地があります。

  1. 参考情報 (あれば)
  • この PR は直前の #58023 で追加された ColorField 関連コードを整理するための follow-up リファクタリングです。
  • 技術的には:
    • 値解決ロジックの共通化
    • サブクラスごとの差分を小さなメソッド (fallback_value) に閉じ込める という典型的なテンプレートメソッドパターン的なリファクタリングで、Form フィールドタグのコードベースの保守性を高める変更と言えます。

#58062 Ensure AV::Template::Sources::File is eager loaded

マージ日: 2026/7/8 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    ActionView::Template::Sources::FileActionView.eager_load! 実行時に確実に eager load されるように、Sources::File クラス定義を別ファイルから sources.rb 内にインライン化した変更です。これにより、最初のテンプレートレンダリングまで Sources::File が require されないという遅延ロード問題を解消します。

  1. 変更内容の詳細

背景

  • ActionView.eager_load!ActionView::Template.eager_load! が呼ばれます。
  • Template.eager_load!Sourceseager_autoload していますが、Sources::File までは自動的にたどり着いていませんでした。
  • Sources 側には
ruby
eager_autoload do
  autoload :File
end

のような指定があり、Sources::File は「eager load の対象として登録」されてはいるものの、Sources.eager_load! 自体がどこからも呼ばれていないため、eager load フェーズでは実際には読み込まれていない状態でした。

  • その結果、Sources::File は「最初のテンプレートレンダリング時」に初めて require される挙動になっていました。

この PR の変更点

  1. Sources::File のクラス定義を sources.rb にインライン化

    • もともと ActionView::Template::Sources::Fileactionview/lib/action_view/template/sources/file.rb に定義されていましたが、この PR でその中身を actionview/lib/action_view/template/sources.rb 内に移動しています。
    • file.rb 側の中身は削除され(-17行)、sources.rb 側に File クラス定義が直接書かれる形で +7 行追加されています。

    イメージ的には、これまで:

    ruby
    # sources.rb
    module ActionView
      class Template
        module Sources
          extend ActiveSupport::Autoload
    
          eager_autoload do
            autoload :File
          end
        end
      end
    end

    ruby
    # sources/file.rb
    module ActionView
      class Template
        module Sources
          class File
            # 実装...
          end
        end
      end
    end

    のように分かれていたものが、sources.rb

    ruby
    module ActionView
      class Template
        module Sources
          class File
            # 実装...
          end
        end
      end
    end

    のように直接含まれる構成になった、という変更です (実際のコードは若干異なる可能性がありますが、構造としてはこのイメージ)。

  2. eager_autoload チェインを増やさない方針

    • 別案としては、Template.eager_load!Sources.eager_load!Sources::File というように eager_load! チェインをつなぐことも可能でした。
    • しかし、この PR では「Sources の配下に他の定数が存在しない」「Sources を参照するならほぼ確実に File も欲しい」という前提から、構造を簡略化するためにインライン化を選択しています。

  1. 影響範囲・注意点
  • eager load の挙動が変わる

    • ActionView.eager_load! 実行時に、ActionView::Template::Sources::File が必ずロードされるようになります。
    • これにより本番環境(config.eager_load = true)などで、最初のリクエスト時ではなくブート時に Sources::File がロードされ、初回テンプレートレンダリングまでのレイテンシや「初回だけ遅い」現象が減る可能性があります。
  • オートロード階層に依存したコードへの影響

    • 以前は ActionView::Template::Sources::File が別ファイルにあり、autoload 機構により必要時にロードされていましたが、今後は sources.rb 読み込み時点でクラス定義が存在します。
    • 通常のアプリケーションコードではこの違いは意識しなくてよく、互換性問題はほぼ発生しないと考えられます。
  • Sources 配下に今後クラスを増やす場合の設計

    • 現時点では Sources の配下に File しか存在しないことを前提にインライン化されています。
    • 将来的に Sources::&lt;SomethingElse> を増やす場合、
      • それらも sources.rb にまとめるか、
      • 再び eager_autoload + eager_load! チェインで細かく制御するか
        の設計方針を検討する必要があります。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58062
  • 関連する Rails の設定:
    • config.eager_load = true の環境では、今回の修正により ActionView.eager_load! 時点で Sources::File が確実にロードされます。
  • eager_autoload / eager_load! に関する参考:
    • ActiveSupport::Autoload のドキュメント
    • Rails ガイド「Rails のオートローディングとリローディング」 (Zeitwerk まわり)

#57662 Add ActiveSupport::Ractors.on_main to proxy work to the main Ractor

マージ日: 2026/7/8 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    このPRは、非メイン Ractor からメイン Ractor に処理を委譲するための ActiveSupport::Ractors.on_main メソッドを追加し、クラスインスタンス変数のメモ化など「メイン Ractor でしか安全に行えない処理」をプロキシできるようにするものです。これにより、Ractors 対応のためにクラスレベルメモ化を完全に捨てずに、安全な形で保持できるようになります。

  1. 変更内容の詳細

追加された API: ActiveSupport::Ractors.on_main

  • 新規ファイル(または新規モジュール実装): activesupport/lib/active_support/ractors.rb
  • そこに ActiveSupport::Ractors.on_main が追加されています。
  • 実装には外部ライブラリ ractor-dispatch が利用されており、メイン Ractor に処理をディスパッチする仕組みをラップしています。

想定される利用方法は以下のとおりです。

従来のコード (Ractors 非対応で問題が出る可能性がある例)

rb
class Something
  def self.something
    @ivar ||= :a
  end
end
  • このパターンは「クラスインスタンス変数の遅延メモ化」ですが、Ractors 環境では
    • 非メイン Ractor からクラスインスタンス変数に書き込みが行われる
    • オブジェクト共有の制約との整合性 などの問題が出やすくなります。

Ractors 対応後のコード (この PR で意図している書き換え例)

rb
class Something
  def self.something
    @ivar || ActiveSupport::Ractors.on_main { @ivar ||= :a }
  end
end
  • 非メイン Ractor から Something.something が呼ばれた場合でも、
    • @ivar が未設定なら ActiveSupport::Ractors.on_main ブロックが「メイン Ractor 上で」実行される
    • メイン Ractor 上で安全に @ivar が設定/メモ化される
  • すでにメイン Ractor 上で @ivar がメモ化済みなら、ブロックを実行せずに @ivar をそのまま返す形になります(|| によるガード)。

Active Record 側での利用前提

  • activerecord/lib/active_record/core.rb にも小さな変更があり、クラスレベルのメモ化ロジックの一部を ActiveSupport::Ractors.on_main 経由で実行できるようにする準備が進められています。
  • 別 PR(https://github.com/rails/rails/pull/57642)で Active Record のクラスレベルメモ化を整理する作業と連携しており、「どうしてもメモ化が必要な箇所」だけを安全にメイン Ractor へ委譲する目的があります。

テストと依存関係

  • activesupport/test/ractors_test.rb にテストが追加され、on_main
    • メイン Ractor から呼び出された場合
    • 非メイン Ractor から呼び出された場合 の挙動をカバーしていると考えられます(+36/-1)。
  • activesupport/activesupport.gemspec および Gemfile.lock に変更が入り、ractor-dispatch を依存関係として追加しています。

  1. 影響範囲・注意点

影響範囲

  • Rails アプリケーションコードそのものにはすぐに大きな破壊的変更はありませんが、
    • ActiveSupport が新たに ractor-dispatch に依存する
    • 将来的に Active Record を含むフレームワーク内部が、この on_main を使って Ractors 対応のメモ化ロジックに書き換えられていく という形で影響が広がります。
  • Ractors を利用するアプリケーション・ライブラリは、この API を利用することでクラスインスタンス変数のメモ化などを安全に行いやすくなります。

注意点・設計上のトレードオフ

  1. メイン Ractor へ処理が集中する

    • on_main はあくまで「メイン Ractor に投げる」APIなので、呼び出しが多いとメイン Ractor がボトルネックになる可能性があります。
    • 高頻度で呼ばれる処理を全て on_main に載せると並列性が削がれるため、
      • 「本当にメイン Ractor でしか安全にできない初期化/メモ化」のみに絞る
      • 一度メモ化した後は他 Ractor で読み取りだけを行う といった運用が必要です。
  2. オートローダー連携案との比較

    • 代替案として、Autoloader にフックし
      rb
      def ractorize!
        some_memo_method
        other_memo_method
      end
      のような「Ractor 用の事前初期化メソッド」を呼ぶ方式も検討されました。
    • これは「Eager Load 的にすべてのメモ化を前もってやる」発想ですが、
      • Autoloader と密結合になる
      • Lazy Load との整合が複雑になる という理由で採用を見送られ、より汎用的で遅延評価とも両立しやすい on_main ベースの方式を取っています。
  3. API の安定性

    • この PR は ActiveSupport に新たな API と外部ライブラリを取り込むものであり、今後の Rails の Ractors サポートの基盤の一部になる可能性があります。
    • とはいえ、まだ比較的新しい領域(Ractors 対応)なので、将来的な API の微調整・拡張は想定しておくべきです。

  1. 参考情報 (あれば)

#58057 Raise on ignored if_exists/if_not_exists in change_table check constraints

マージ日: 2026/7/8 | 作成者: @55728

  1. 概要 (1-2文で)
    change_table ブロック内で t.check_constraint / t.remove_check_constraint に渡された if_exists / if_not_exists オプションが無視されていた問題を修正し、他のメソッド同様に ArgumentError を投げて誤用を検知できるようにした PR です。これにより、条件付きマイグレーションのつもりが実際には効いていない、というサイレントな失敗を防ぎます。

  1. 変更内容の詳細

背景

  • Rails のマイグレーションでは、トップレベルのメソッド(例: add_column, add_index, add_foreign_key など)に対して if_exists: / if_not_exists: オプションが使えます。
  • 一方、change_table :products do |t| ... end ブロック内の DSL (t.column, t.index, t.foreign_key など) では、これらのオプションはサポートされていないため、渡されると ArgumentError を投げ、「条件付きの形(トップレベルのメソッド)を使ってください」というガイダンスを返す仕様になっています。
  • しかし t.check_constraintt.remove_check_constraint だけはこのガードがなく、if_exists/if_not_exists を受け取ってそのままアダプタに渡してしまい、結果としてオプションが silently ignore(黙殺)される挙動になっていました。

この PR の変更点

対象ファイル:

  • activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
  • activerecord/test/cases/migration/change_table_test.rb

主な内容:

  1. change_table 内の check constraint メソッドにガードを追加

    • t.check_constraintt.remove_check_constraintif_exists / if_not_exists を受け取った場合、他のメソッドと同じように ArgumentError を投げるように変更。
    • エラーメッセージの内容は、既に column, index, foreign_key, remove_timestamps などで使われているものと同系統で、「change_table ブロック内ではなく、条件付きのトップレベルメソッドを使うように」といったガイダンスが含まれます。
  2. テストの追加

    • activerecord/test/cases/migration/change_table_test.rb に、次のようなコードが ArgumentError を投げることを確認するテストが追加されています:
    ruby
    change_table :products do |t|
      t.check_constraint "price > 0", name: "price_check", if_not_exists: true
    end
    
    change_table :products do |t|
      t.remove_check_constraint name: "price_check", if_exists: true
    end

    これにより、「以前はこのコードが通っていたがオプションは無視されていた」「今後は明示的に例外が出て誤用に気付ける」という振る舞いの違いがテストで保証されます。

  3. 既存のガード (remove_timestamps) のフォローアップ

    • 説明にもある通り、この PR は #57988(remove_timestamps に同種のガードを追加した変更)のフォローアップで、change_table DSL 全体で挙動を揃える意図があります。

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

    • 既存のマイグレーションで、次のようなコードを書いている場合:

      ruby
      change_table :products do |t|
        t.check_constraint "price > 0", name: "price_check", if_not_exists: true
        # あるいは
        t.remove_check_constraint name: "price_check", if_exists: true
      end
    • これまではマイグレーションが正常終了していたものの、if_not_exists / if_exists は実際には効いていませんでした。

    • この PR 適用後はマイグレーション時に ArgumentError が発生するようになります。

  • どう直すべきか(推奨の書き方)

    • 条件付きでチェック制約を追加・削除したい場合は、change_table ブロックを使わず、トップレベルのマイグレーションメソッドを用いる必要があります。

    例: チェック制約を「なければ作る」

    ruby
    def change
      add_check_constraint :products,
        "price > 0",
        name: "price_check",
        if_not_exists: true
    end

    例: チェック制約を「存在すれば削除」

    ruby
    def change
      remove_check_constraint :products,
        name: "price_check",
        if_exists: true
    end
    • もしくは change_table とは別に up/down を分けて書く、あるいは raw SQL を使うなど、これまでどおりの回避策も可能ですが、Rails が提供するトップレベル API を使うのが一貫性の観点で推奨されます。
  • 後方互換性の観点

    • 仕様としては、「元々 change_table 内の DSL で if_exists / if_not_exists をサポートしていなかった」という前提に合わせる修正なので「バグ修正」です。
    • ただし、これまで「たまたま通っていた」既存マイグレーションが落ちる可能性があるため、Rails 更新後にマイグレーションを再実行する CI を回しておくと安全です。

  1. 参考情報 (あれば)
  • 直接関連する PR:
    • #57988 — change_table 内の remove_timestamps に対して同様の if_exists/if_not_exists ガードを導入した PR。
  • 関連する API ドキュメント (Rails Guides / API):
    • Active Record Migrations — change_table, add_check_constraint, remove_check_constraint, if_exists / if_not_exists オプションの説明部分。
  • 設計上のポイント:
    • change_table DSL はあくまで「テーブル変更をまとめて書きやすくするための糖衣構文」に近く、高度なオプション(条件付き実行など)はトップレベルメソッド側で提供する、という整理がこの PR で強化されています。

#57956 Bump MySQL Image version to 9.7

マージ日: 2026/7/8 | 作成者: @akhilgkrishnan

  1. 概要 (1-2文で)
    Rails が内部で利用している MySQL のコンテナイメージバージョンを「9.7」に更新する PR です。Rails アプリ生成処理や関連テストで前提としている MySQL イメージタグを最新に追随させています。

  1. 変更内容の詳細

この PR が触っているのは、主に「Rails が自動生成する設定やテストコード内の MySQL イメージ指定」です。実装としては、旧バージョンの MySQL イメージタグを 9.7 に書き換えています。

具体的な箇所:

  • railties/lib/rails/generators/database.rb
    Rails プロジェクト生成時に、選択された DB に応じて設定を組み立てるジェネレータクラスです。
    MySQL を選択した場合のイメージ指定 (例: devcontainer / Docker 関連のデフォルト値) に使われているタグが 9.7 に更新されています。

  • railties/lib/rails/generators/rails/app/templates/config/deploy.yml.tt
    Rails アプリ生成時に config/deploy.yml を作るためのテンプレートです。
    ここでデプロイ・CI などに使う MySQL コンテナイメージ(例: image: mysql:9.7 のような形)を参照しており、そのタグが 9.7 に更新されています。

  • テストコード側の更新

    • railties/test/generators/app_generator_test.rb
    • railties/test/generators/db_system_change_generator_test.rb
    • railties/test/generators/devcontainer_generator_test.rb

    これらはいずれも「ジェネレータが生成する設定ファイルに、期待した MySQL イメージバージョンが書き込まれているか」を検証するテストです。
    旧バージョンの期待値から、9.7 を期待するようにアサーションが修正されています。

行数レベルの変更であり、処理ロジックの変更はなく、ほぼ文字列(イメージタグ)の置き換えのみです。


  1. 影響範囲・注意点
  • 新規に Rails アプリを生成するとき

    • MySQL を選択した場合、生成される config/deploy.yml や devcontainer 関連設定で、MySQL 9.7 イメージが前提になります。
    • これにより、開発・CI 環境で自動的に起動される MySQL のバージョンが上がる可能性があります。
  • 既存プロジェクトへの影響

    • 既存プロジェクトの config/deploy.yml や Docker 関連設定はこの PR では自動変更されません。
    • ただし、Rails のバージョンアップに伴って rails new 相当をやり直したり、サンプルやガイドに倣って設定を書き換えると MySQL 9.7 を前提とするようになるため、互換性確認が必要です。
  • 互換性・マイグレーション上の注意

    • 9.7 というバージョンの MySQL イメージが、従来使っていたバージョンと後方互換でない場合、
      • SQL モードのデフォルト変更
      • 予約語の追加・挙動変更
      • 文字コード・照合順序の変更
        などによって、スキーマ定義やアプリケーションコードが影響を受ける可能性があります。
    • バージョン間の Breaking change がないか、利用している MySQL イメージ(ベースディストリ/フォーク含む)のリリースノートを確認し、CI / ステージング環境でアップグレード検証を行うべきです。
  • テストへの影響

    • Rails 自身のテストは、期待値が 9.7 に追随しているため問題ありません。
    • 自前でジェネレータ出力を検証しているプロジェクト(テンプレート文字列をそのまま比較しているようなテスト)がある場合、MySQL イメージタグの期待値を更新する必要が生じる可能性があります。

  1. 参考情報 (あれば)
  • この PR は主に「公式テンプレートで利用する MySQL イメージの推奨バージョン更新」であり、Rails の Active Record アダプタの実装などには手を入れていません。
  • 実運用で MySQL 9.7 を利用する場合は、利用中の MySQL ディストリビューション(Oracle MySQL / MariaDB / Percona など)および該当イメージのドキュメント・リリースノートを確認のうえ、ローカル・CI・本番で同一メジャー/マイナーを揃えることが推奨されます。

#58050 Forward identifying options in remove_foreign_key's if_exists guard

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

  1. 概要 (1-2文で)
    remove_foreign_key ... if_exists: true が、:name:to_table で外部キーを特定した場合に例外を投げてしまう不具合を修正し、対象の外部キーが存在しなければ常に no-op になるよう統一されました。foreign_key_exists? の存在チェックに、後続の lookup と同じ識別オプションを正しく引き継ぐようにした変更です。

  1. 変更内容の詳細

問題の背景

remove_foreign_key には if_exists: true オプションがあり、「指定した外部キーがなければ何もしない(例外を出さない)」ことを保証するためのものです。

従来は以下のような位置引数形式では期待通りに動作していました。

ruby
remove_foreign_key :accounts, :branches, if_exists: true
# → 対象のFKがなければ何も起きない

しかし、外部キーを :name:to_table で指定したときに問題がありました。

ruby
remove_foreign_key :accounts, name: :special_fk, if_exists: true
remove_foreign_key :accounts, to_table: :owners, if_exists: true

この場合、内部の存在チェック (foreign_key_exists?) に :column しか渡しておらず:name:to_table を使った「この外部キー」を正しく特定できていませんでした。

その結果:

  1. 存在チェックが「テーブルに何かしら外部キーが付いていれば true」となってしまう
  2. if_exists ガードが ショートサーキットされず、実際の削除処理に進んでしまう
  3. 実際に指定された :name / :to_table の外部キーがなければ ArgumentError が発生

つまり、「存在しなければ何もしない」はずの if_exists: true を付けたのに、逆にエラーが出てしまうという、本来の意図と真逆の挙動になっていました。
これは特に「複数回実行されても安全なマイグレーション(idempotent migration)」を意図した場合に困る挙動です。

修正内容

この PR では、if_exists のための存在チェック部分に、後続の外部キー lookup と同じ識別オプションを渡すように修正しています。

具体的には:

  • foreign_key_exists? を呼ぶ際に、従来は column: のみを渡していたが、
  • :name, :to_table(および関連する識別情報)をそのまま転送するように変更

これにより、

ruby
remove_foreign_key :accounts, name: :special_fk, if_exists: true
remove_foreign_key :accounts, to_table: :owners, if_exists: true

のいずれも、該当する外部キーが存在しない場合は完全に no-opになり、ArgumentError は発生しません。

SQLite3 アダプタについて

SQLite3 アダプタ側でも同様の if_exists ガードがあり、こちらも :column のみを見ていたため同じ問題を抱えていました。
PR では SQLite3 の schema_statements も修正し、to_table によるスコープをきちんと尊重するようにしています。

  • SQLite は外部キーに名前を持てないため、:name は効きませんが
  • to_table でスコープされるようになったことで、こちらでも期待どおり「対象FKがなければ no-op」という挙動になります。

テスト

activerecord/test/cases/migration/foreign_key_test.rb にテストが追加され、次のようなケースをカバーしています(要約):

  • remove_foreign_keyname: 指定 + if_exists: true で呼び出し、対象外部キーが存在しない場合に例外が出ないこと
  • 同様に to_table: 指定 + if_exists: true でも例外が出ないこと
  • 既存の位置引数形式(テーブル名/参照テーブル指定)との挙動が一貫していること

  1. 影響範囲・注意点
  • マイグレーションの安全性向上

    • これまで、name:to_table: で外部キーを指定しつつ if_exists: true を付けていても、安全であるはずのマイグレーションが ArgumentError で落ちる可能性がありました。
    • 本修正により、if_exists: true の意図どおり、「外部キーがなくてもエラーにならない」ことが保証されます。
  • 挙動の変化(注意点)

    • もし既存コードが「if_exists: true を付けているが、実は例外が出ることを前提にしていた」ような特殊なケースがあると、今回の修正により例外が出なくなるため挙動変化になります。
    • ただし if_exists: true のドキュメント上の意図からすれば、今回の修正後の挙動が正であり、以前のほうがバグと言えます。
  • SQLite3 での違い

    • SQLite では外部キーに名前を付けられないため、:name での特定は実質的に効きませんが、:to_table のスコープは効くようになりました。
    • そのため、remove_foreign_key :accounts, to_table: :owners, if_exists: true といった形は、SQLite でも安全に no-op になります。
  • foreign_key_exists? の仕様に依存

    • 今回の修正は、foreign_key_exists? / ForeignKeyDefinition#defined_for? がすでに :name:to_table で一致判定を行う仕様に沿ったものです。この仕様が今後変わる場合は、この部分との整合性に注意が必要です。

  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/58050
  • 類似修正: remove_check_constraint に対する同様の修正コミット e27a878f4a
  • 関連メソッド:
    • ActiveRecord::ConnectionAdapters::SchemaStatements#remove_foreign_key
    • ActiveRecord::ConnectionAdapters::SchemaStatements#foreign_key_exists?
    • ActiveRecord::ConnectionAdapters::ForeignKeyDefinition#defined_for?

#58051 Freeze empty where clause predicates

マージ日: 2026/7/7 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    ActiveRecord の WhereClause::Empty オブジェクトを freeze することで、Ractor(Ruby の並行実行モデル)に対応した PR です。where 句が「空」の場合に使われる共有オブジェクトを不変にし、安全にスレッド/Ractor 間で扱えるようにしています。

  1. 変更内容の詳細

何をしているか

  • ActiveRecord::Relation::WhereClause::Empty に相当する空の where 句プレディケートを「freeze(凍結=不変化)」する変更が 1 行だけ入っています。
  • 具体的には、WhereClause 内で共有される「空集合」を表す定数 or オブジェクト定義に .freeze を追加した形です(差分は +1/-1 行なので、EMPTY_PREDICATES = [].freeze のような書き換えが入ったと推測できます)。

Ruby の Ractor では、Ractor 間で共有できる値は「イミュータブル(不変)」である必要があります。freeze することで WhereClause::Empty が Ractor の「共有可能オブジェクト」の条件を満たすようになります。

関連背景

  • 説明文中にある通り、Ractor 対応を進めた以前の PR(https://github.com/rails/rails/pull/57475)での漏れを補完するものです。
  • ActiveRecord::Relation::WhereClause::Empty は、「where 条件が何も無い Relation」を表現するためによく再利用されるオブジェクト(シングルトン的な役割)です。このような共有オブジェクトを Ractor 安全にすることが目的です。

簡単なイメージコード

※実際のコードとは多少異なる可能性がありますが、イメージとしては以下のような修正です。

ruby
# 修正前(イメージ)
EMPTY = WhereClause.new([])

# 修正後(イメージ)
EMPTY = WhereClause.new([]).freeze

もしくは配列ベースで:

ruby
# 修正前
EMPTY_PREDICATES = []

# 修正後
EMPTY_PREDICATES = [].freeze

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

    • Model.where(...) などで、結果として「条件なし」になる Relation を扱うコード全般。
    • ActiveRecord::Relation::WhereClause::Empty を直接触ることは通常ほぼ無いので、一般的なアプリコードへの影響は極めて小さいです。
    • Ractor を使用している / これから使おうとしている Rails アプリケーションにとっては、Active Record の WhereClause 周りが一段と安全になります。
  • 互換性・破壊的変更の可能性

    • もともと「空の where 句」オブジェクトはライブラリ内部で共有・再利用される想定のため、アプリケーションコード側でこれをミューテート(破壊的変更)することは非推奨かつ異常な利用です。
    • もし内部オブジェクトを直接いじっていた場合(例: relation.where_clause.send(:predicates) << ... のようなハック)には、FrozenError が発生する可能性があります。
    • ただし、そのようなケースはほとんど存在しないと考えられるため、通常の利用では後方互換性への影響は実質ゼロに近いです。
  • テスト・ドキュメント

    • PR のチェックリスト上、「テスト追加」「CHANGELOG 更新」は行われていません。
    • 挙動の変化は「内部オブジェクトが凍結されるようになった」という極小の実装詳細レベルであり、外部 API の仕様変更ではないため、テスト追加や CHANGELOG の更新は不要と判断された可能性が高いです。

  1. 参考情報 (あれば)
  • Ractor について:
    • Ruby 3 以降で導入された並行実行モデル。Ractor 間で共有できるのは「freeze 済みオブジェクト」や「コピーされたデータ」などに限られるため、ライブラリ側で共有の可能性があるオブジェクトを積極的に freeze する必要があります。
  • 関連 PR:

#58048 Fix ActionController::Live streams hanging on client disconnect

マージ日: 2026/7/7 | 作成者: @wpeterson

  1. 概要 (1-2文で)
    ActionController::Live でクライアント切断時にストリーム処理がハングし、スレッドが解放されない不具合を修正した PR です。Buffer#abort 時にも確実にコンシューマループが終了できるよう、nil 終端の enqueue とブロードキャスト処理を追加し、その挙動を確認する回帰テストを導入しています。

  1. 変更内容の詳細

問題の背景

ActionController::Live では、レスポンスボディをバックグラウンドスレッドでストリーミングするために ActionController::Live::Buffer が使われています。

  • プロデューサ側: Buffer#write 等で SizedQueue にチャンクを push
  • コンシューマ側: Buffer#each_chunkSizedQueue#pop をループしつつ読み出し
    • 終了条件は「popnil(もしくは falsy)を返したとき」
  • 正常終了: Buffer#close が queue に nil を enqueue(終端マーカー)→ each_chunk が抜ける

バグは「異常終了パス」である Buffer#abort の挙動にありました。

  • Buffer#abort
    • キューを clear する
    • @aborted フラグを立てる
    • しかし nil を enqueue しない
  • 一方 each_chunk は、
    • queue.pop が falsy なら break」というロジックで、@aborted は一切見ていない

このため、以下のようにハングが発生します。

  1. abort 発火時に、コンシューマが空キュー上で pop 待ちしているケース

    • #abort はキューを clear(元々空)して @aborted を設定するだけで、push しない
      SizedQueue#pop が永遠に戻らず、コンシューマスレッドがブロックし続ける
  2. #close#abort の競合レース

    • #close が終端の nil を enqueue
    • 直後に #abortclear してしまい、その nil を消してしまう
      → コンシューマが nil を受け取れず、やはり pop でブロックし続ける

このブロックはリクエスト処理スレッドを解放しないため、特に RAILS_MAX_THREADS=1 のような少スレッド構成だと、1 本のハングしたストリームでワーカーが永久に詰まるという致命的な問題になります。

修正内容

ActionController::Live::Buffer#abort の実装を、#close と同様に「確実に終端マーカーを通知する」形に変更しています。

ポイント:

  • #abort でやること
    1. キューを clear する
    2. @aborted を立てる(既存)
    3. nil を enqueue する(新規)
    4. ブロードキャストして待機中のスレッドを起こす(新規)

実装上の意図:

  • clear の後で nil を push
    • SizedQueue が満杯の状態で #abort すると、clear により中身が空になり、その後の push(nil) は必ずノンブロッキングで成功する
    • これにより #abort 自体が queue のサイズ制限に引きずられてブロックすることを防ぐ
  • #close#abort の両方が呼ばれても安全
    • closeabortnil を enqueue する可能性があるが、コンシューマ側は最初に読んだ nil でループを抜ける
      → 終端マーカーが重複しても問題ない

概念的な疑似コードイメージ:

ruby
class Buffer
  def abort
    @queue.clear
    @aborted = true
    @queue << nil   # 終端マーカー
    @cond.broadcast # 待機スレッドに終了を知らせる
  end

  def each_chunk
    while (chunk = @queue.pop)
      yield chunk
    end
    # nil を受け取るとループを抜ける
  end
end

※実際のコードはもう少し複雑ですが、ロジックの本質はこのイメージです。

テストの追加

actionpack/test/controller/live_stream_test.rb に 2 つの回帰テストが追加されています。

  1. each_chunk 中に #abort された場合に reader が解放される」テスト

    • 再現手順:
      • 別スレッドで buffer.each_chunk を呼んでブロックさせる
      • メインスレッドで buffer.abort を呼ぶ
    • 期待:
      • each_chunk のスレッドがハングせずに join で終了することを確認
  2. #close の直後に #abort が走るレースでも、reader が解放される」テスト

    • 再現手順:
      • buffer.close を呼んだ直後に buffer.abort を呼ぶような並行パターンを構成
    • 期待:
      • このレース条件下でも each_chunk を実行中のスレッドが終了できる

どちらのテストも、現行 main ブランチでは reader が join タイムアウトまでハングして失敗し、この修正を入れるとパスすることが確認されています。actionpack/test/controller/live_stream_test.rb 全体のテストもパスしています。

また、actionpack/CHANGELOG.md にはこの不具合修正に関するエントリが追加されています。


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

    • ActionController::Live を利用しているアプリケーション全般、とくに以下に該当するもの:
      • SSE(Server-Sent Events) やロングポーリングなど、長時間ストリーミングするエンドポイント
      • RAILS_MAX_THREADS が小さい構成(1〜数スレッド)で、スレッド枯渇に敏感な環境
    • クライアントがページ離脱やネットワーク切断などでコネクションを突然切るケースで、ワーカーがハングしにくくなります。
  • 注意点

    • アプリケーション側で独自に ActionController::Live::Buffer を直接いじっていない限り、互換性リスクは非常に低いです。
    • #abort が呼ばれた場合でも、コンシューマ側の each_chunk ループは 「nil が返ってきた」という通常終了パスと同じように抜ける ため、@aborted を見て特別扱いしているような独自コードが無い限り、挙動は「ハングしなくなる」以外は変わりません。
    • 複数回 abort / close が呼ばれるようなケースでも、余分な nil がキューに入るだけで、コンシューマは最初の 1 つで停止するため悪影響はありません。

  1. 参考情報 (あれば)
  • 対応 Issue: #58047
  • 対応 PR: #58048(本 PR)
  • 関連クラス/モジュール:
    • ActionController::Live
    • ActionController::Live::Buffer
    • SizedQueue(標準ライブラリ thread
  • テストファイル:
    • actionpack/test/controller/live_stream_test.rb
  • 実運用上の示唆:
    • Live ストリーミングを利用するアプリで、ワーカーが徐々に詰まっていく問題(特にクライアント切断が多い環境)に遭遇していた場合、この修正を含む Rails バージョンへのアップデートで改善が見込めます。

#58039 Support endless and beginless ranges in number_field_tag/range_field_tag

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

  1. 概要 (1-2文で)
    number_field_tag / range_field_tag に対して :in / :within オプションに「終端なし(endless)」や「始端なし(beginless)」の Range を渡したときに RangeError になっていた問題を解消し、適切な min / max 属性を出力するようにした PR です。既に対応済みだった number_field / range_field と挙動を統一する変更です。

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

変更前の挙動

:in / :within に endless / beginless range を渡すと、Range#max / Range#min を取得しようとして RangeError が発生していました。

ruby
number_field_tag("quantity", nil, in: 18..)
# RangeError: cannot get the maximum of endless range

range_field_tag("volume", nil, in: ..11)
# RangeError: cannot get the minimum of beginless range

内部実装としては、options[:in] などに Range が渡された場合に range.min, range.max によって min / max 属性を決定しており、endless / beginless range に対して不正なアクセスをしていた形です。

変更後の挙動

endless / beginless range を正しく扱い、HTML 属性に反映できるようにしました。

  • endless range (18..) → min 属性のみを付与、max は付けない
  • beginless range (..11) → max 属性のみを付与、min は付けない
ruby
number_field_tag("quantity", nil, in: 18..)
# => <input name="quantity" id="quantity" type="number" min="18" />

range_field_tag("volume", nil, in: ..11)
# => <input name="volume" max="11" id="volume" type="range" />

通常の「両端あり」の Range(1..101...10)の挙動は変わりません。特に排他的範囲(...)についてはこれまで通り「最大値 − 1」で max が設定されます。

ruby
# 変更後も従来通り
number_field_tag("quantity", nil, in: 1...10)
# => max="9"

実装レベルの補足

  • 修正は ActionView::Helpers::FormTagHelper 内の range オプション処理ロジックの 1 箇所のみ(+1/-1)で、endless / beginless range に対して max / min を無条件に取得しないように分岐・ロジックを調整しています。
  • form_tag_helper_test に endless / beginless range を渡したケースのテストが追加され、期待される min / max 属性の有無を検証しています。
  • CHANGELOG に挙動変更が追記されています。

  1. 影響範囲・注意点
  • 影響範囲は number_field_tag / range_field_tag:in / :within オプション付きで使っている箇所に限定されます。
  • 今まで endless / beginless range を渡していた場合は RangeError で落ちていたため、今回の変更によりそのようなコードが動作するようになります(=「今まで動いていた挙動の変更」ではなく「今まで例外だったパターンがサポートされた」)。
  • both-bounded range(通常の範囲)や排他的範囲(...)の既存挙動は維持されているため、既存の min / max の値が変わることはありません。
  • 既に number_field / range_field(form_for / form_with 系ヘルパ)では endless / beginless range がサポートされており、今回の変更で *_tag 系とも挙動が揃いました。form_forform_tag などへ書き換えているコードベースでは、endless / beginless range の扱いが一貫するようになります。

  1. 参考情報 (あれば)
  • この PR は、number_field / range_field で endless / beginless range をサポートしたコミット(530beb8550f140c21b790d6acdf990931e45e16b)との整合性をとる意図で行われています。
  • HTML 側の仕様的には min / max はともに任意属性であり、どちらか一方だけの指定も問題なく許容されるため、この変更は HTML 仕様に沿ったものです。

#58040 Treat an empty id array as an empty set for composite primary keys

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

  1. 概要 (1–2文で)
    複合主キー(Composite Primary Key)を持つモデルに対して destroy([])update([], []) を呼ぶと NoMethodError になっていた問題を修正し、空配列を「空のID集合」として扱うようにしたPRです。単一主キーのモデルと同様に、これらの呼び出しが安全な no-op として [] を返すようになります。

  1. 変更内容の詳細

問題の挙動

複合主キーを持つモデルで、以下のような呼び出しをすると:

ruby
CompositePkModel.destroy([])    # NoMethodError (undefined method `destroy' for []:Array)
CompositePkModel.update([], []) # NoMethodError (undefined method `update' for []:Array)

原因は、内部で「複数 ID を扱うコードパス」と「単一 ID を扱うコードパス」を分岐する条件が、空配列を正しく「複数 ID」とみなせていなかったためです。

  • destroy([]) / update([], []) で渡された配列が
    • 「複数の ID」か
    • 「単一 ID(複合主キーなら配列1つ)」か を判定するロジックが、first.is_a?(Array) などのチェックを行っていましたが、空配列の場合はこの判定に入らず、単一 ID 用のコードパスに落ちてしまっていました。

その結果:

  1. find([]) が呼ばれる
  2. find[] を返す
  3. その戻り値 ([]) に対して .destroy / .update を呼び、NoMethodError になる

一方、単一主キーのモデルでは、destroy([]) / update([], []) は既に「何もしないで [] を返す」挙動になっており、複合主キーだけが不整合な状態でした。

修正内容

2カ所の「複数IDかどうか」を判断するメソッドに、value.empty? のショートサーキットを追加しています。

対象のメソッド:

  1. ActiveRecord::Key::Composite#expects_multiple_ids?

    • Relation#destroy で使用される
    • 複合主キーに対して Model.destroy(ids) を呼ぶ際の挙動に影響
  2. ActiveRecord::Persistence::ClassMethods#update_multiple_ids?

    • Model.update / Model.update! で使用される
    • 複合主キーでの update([], []) の挙動に影響

どちらも以下のような形で修正されています(イメージ):

ruby
def expects_multiple_ids?(value)
  return false if value.empty? # ここが追加・変更点
  # 以降、first.is_a?(Array) などの既存ロジック
end

value.empty? の時点で「複数 ID でも単一 ID でもなく、空集合」とみなして即座に処理を分岐させることで、単一 ID ブランチに落ちてエラーを起こすのを防いでいます。

既存の find_with_ids はすでに空配列に対してショートサーキットしており:

ruby
return [] if first_item.empty?

というロジックを持っているため、find 自体の挙動は変更していません。

Before / After の挙動

ruby
CompositePkModel.destroy([])    # Before: NoMethodError / After: []
CompositePkModel.update([], []) # Before: NoMethodError / After: []

テストも追加されており:

  • activerecord/test/cases/key_test.rb
  • activerecord/test/cases/persistence_test.rb

で、空配列を渡したときに [] が返ることが確認されています。


  1. 影響範囲・注意点
  • 影響を受けるのは「複合主キーを持つ ActiveRecord モデル」に対するクラスメソッド:
    • Model.destroy([])Relation#destroy 経由も含む)
    • Model.update([], []) / Model.update!([], [])
  • これらが例外を投げず、単一主キーの場合と同様に「何もしないで [] を返す」ようになります。

運用・コード上の影響:

  • これまで destroy([]) / update([], [])NoMethodError になることに依存したコードがある場合、その挙動は変わりますが、通常は依存していない前提のはずで、実用上は「バグ修正」と考えてよい変更です。
  • 既に単一主キーでは同じ挙動であるため、「複合主キーだけ特別扱い」する必要がなくなります。汎用的なコード(単一主キー/複合主キーを意識しない共通処理)で扱いやすくなります。
  • ロジックの変更箇所が小さく、また空配列に対する振る舞いのみを変えているため、既存の非空配列や単一 ID に対する挙動への影響は極めて限定的です。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58040
  • 関連する内部API:
    • ActiveRecord::Key::Composite#expects_multiple_ids?
    • ActiveRecord::Persistence::ClassMethods#update_multiple_ids?
    • ActiveRecord::FinderMethods#find_with_ids (空配列のショートサーキットあり)

#58041 Apply DISTINCT to average

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

  1. 概要 (1-2文で)
    ActiveRecord::Relation#averagedistinct 指定を無視していた問題を修正し、distinct.average が SQL の AVG(DISTINCT column) を使うようになりました。これにより、distinct.count / distinct.sum と平均値の挙動が一貫します。

  1. 変更内容の詳細

何が問題だったか

distinct なリレーションに対して集計を行うとき、以下のように動作が揃っていませんでした:

ruby
# credit_limits: [50, 50, 60]
relation = firms.distinct # など、DISTINCT が効いている Relation を想定

relation.sum(:credit_limit)   # => 110   (DISTINCT が効いている)
relation.count(:credit_limit) # => 2     (DISTINCT が効いている)

# しかし…
relation.average(:credit_limit) # => 53.33... (AVG(credit_limit) になり DISTINCT 無視)

SQL レベルでも AVG(credit_limit) が発行されており、DISTINCT が付かないため、重複値を含めて平均してしまっていました。同様の問題は group(:firm_id).distinct.average(:credit_limit) のようなグループ集計時にも発生していました。

期待される動作は:

ruby
relation.distinct.average(:credit_limit)
# SQL: AVG(DISTINCT credit_limit)
# => 55.0 (重複 50 を 1 回として扱う)

今回の修正内容

activerecord/lib/active_record/relation/calculations.rbaverage 実装が修正され、以下の2パスで DISTINCT が平均計算に反映されるようになりました。

  1. グルーピングなしの average

    • relation.distinct.average(:credit_limit)AVG(DISTINCT credit_limit) を生成
  2. group を伴う average

    • relation.group(:firm_id).distinct.average(:credit_limit)
      AVG(DISTINCT credit_limit) を用いたグループ集計を生成

これにより:

ruby
# 例: firm_id ごとの credit_limit が [50, 50, 60] で 50 が重複している場合
Firm.group(:firm_id).distinct.average(:credit_limit)
# これまで: { 1 => 53.33... } (DISTINCT 無視)
# これから: { 1 => 55.0 }     (AVG(DISTINCT credit_limit))

minimum / maximum への影響

minimum / maximum には DISTINCT を付けても結果が本質的に変わらないため、今回もこれらには DISTINCT を付けません。
たとえば:

sql
MIN(column)      == MIN(DISTINCT column)
MAX(column)      == MAX(DISTINCT column)

となるため、クエリの意味的にもパフォーマンス的にも特にメリットがない、という判断です。

テスト

activerecord/test/cases/calculations_test.rb にテストが追加されており、

  • 非グループ average + distinct
  • group + distinct + average
    の両方で DISTINCT が平均値計算に効くことがカバーされています。

  1. 影響範囲・注意点
  • 互換性 (Breaking change の可能性)

    • 既存アプリで distinct.average を使っており、現在の「重複込みの平均値」を前提にしてロジックやテストを書いていた場合、結果が変わります。
    • 具体的には、いままで average だけが distinct を無視していた挙動が修正され、count / sum と一貫した扱いになるため、「バグ修正」ではあるものの互換性に影響しうる変更です。
  • 期待される利用方法

    • 重複レコードを除いた値集合の平均」を取りたい場合:
      ruby
      Model.distinct.average(:column)
    • 重複も含めてレコードごとの平均」を維持したい場合:
      • distinct を外すか、
      • 必要であればサブクエリやウィンドウ関数などを用いて、意図した集計を明示的に書く必要があります。
  • group との組み合わせ

    • group(:key).distinct.average(:value) を使っているコードも結果が変わる可能性があります。
    • 特に、joins + distinct + group で「重複JOINを消すためだけに distinct を付けている」ようなケースでは、平均値に DISTINCT が効いてしまう点に注意してください。

  1. 参考情報 (あれば)
  • 同様の挙動は過去に sum でも修正されており、そのときと同じポリシーに揃える変更です:
    • sum の ungrouped な distinct 対応: 566f1fd068 (fixes #16791)
    • sum の grouped な distinct 対応: 00e399d315 (#57896)
  • average についての長年の報告: issue #39857 (stale でクローズされていたが、今回の PR で解決された形)。

#58044 Resolve default_priority lazily so a configured value is honored

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

  1. 概要 (1-2文で)
    ActiveJob で default_priority をイニシャライザなどで後から設定してもジョブに反映されなかった問題を修正し、優先度を参照するタイミングで default_priority を遅延評価するように変更した PR です。これにより default_queue_name と同じ挙動・パターン(lambda での遅延デフォルト解決)に揃えられています。

  1. 変更内容の詳細

何が問題だったか

従来の挙動:

ruby
# config/initializers/active_job.rb
ActiveJob::Base.default_priority = 10

SomeJob.new.priority
# => nil (本来は 10 を期待する)

原因:

  • ActiveJob::QueuePriority concern が include されるタイミングで
    priority クラス属性に「その時点の default_priority の値」が固定的にコピーされてしまっていた。
  • Rails のロード順の関係で、initializer が走るより前に、このクラス属性の初期化が行われるケースがあり、 その後に ActiveJob::Base.default_priority = 10 としても、すでにクラス側には nil がセット済みで、上書きされない。

結果として、「起動後に default_priority を設定しても、一部のジョブクラスでは反映されない」というバグになっていました。

今回の変更内容

default_priority を「値として即時にコピー」するのではなく、「lambda 経由で遅延解決」するように変更しています。
default_queue_name ですでに使われているパターンに合わせた形です。

変更点の要旨:

  • activejob/lib/active_job/queue_priority.rb

    • priority クラス属性のデフォルト値を直接な値ではなく、-> { default_priority } のような lambda(Proc)に変更。
    • これにより、ジョブが priority を読むたびに「その時点の default_priority」が評価される。
  • activejob/test/cases/queue_priority_test.rb

    • 上記バグを再現するテストを追加し、修正後の期待挙動を検証。
    • 例: initializer 的なタイミング(ロード後)で ActiveJob::Base.default_priority = 10 をセットし、その後に生成したジョブインスタンスの priority が 10 になることを確認するテストが追加されている。

概念的なサンプル(イメージ):

ruby
# 変更前(イメージ)
class ActiveJob::Base
  class_attribute :priority, default: default_priority   # ここで値が固定的にコピーされる
end

# 変更後(イメージ)
class ActiveJob::Base
  class_attribute :priority, default: -> { default_priority } # 読み出し時に default_priority を評価
end

このおかげで、initializer で default_priority を設定・変更すると、その後に生成されるジョブインスタンスから参照される priority に反映されるようになります。

テストで確認される挙動(PR の説明通り):

ruby
ActiveJob::Base.default_priority = 10
SomeJob.new.priority
# Before: nil
# After: 10

  1. 影響範囲・注意点
  • 設定方法

    • ActiveJob::Base.default_priority = 10 を「initializer など、アプリケーション起動後のタイミング」で行っても、確実に反映されるようになります。
    • これまで「なぜか priority が反映されない」としてワークアラウンド(各ジョブで self.priority = 10 を書くなど)をしていた場合は、それらが不要になる可能性があります。
  • 既存コードへの影響

    • 一般的な use case では後方互換的な修正であり、既存アプリが壊れる可能性は低いです。
    • ただし「priority クラス属性に直接値を入れていた」ようなコードがある場合は、その挙動と default_priority の組み合わせを確認した方が安全です。
      • SomeJob.priority = 5 のような明示的な上書きは、これまで通り個別ジョブごとの設定として優先されるはずです(クラス属性の直接代入なので、lambda デフォルトの上にさらに値が乗る形)。
  • パフォーマンス影響

    • priority のデフォルトが lambda に変わることで「読み出し時に 1 回 Proc を評価する」オーバーヘッドが増えますが、非常に軽微で、通常のジョブ実行規模では問題にならないと考えられます。
  • 挙動の一貫性

    • default_queue_name と同じ「遅延評価パターン」になるため、ActiveJob の API の一貫性が向上しています。
    • 「キュー名は後から変えられるのに、優先度は変わらない」というアンバランスさが解消されます。

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/58044
  • 関連する既存の仕組み:
    • ActiveJob::Base.default_queue_name(すでに lambda ベースのデフォルト解決を採用)
    • class_attribute におけるデフォルト値の lambda 指定パターン(読取時評価)

#58034 Support log on ActiveSupport::ProxyLogger

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

  1. 概要 (1-2文で)
    ActiveSupport::ProxyLogger が標準ライブラリ Logger のインターフェイスと揃うように、log メソッドを add のエイリアスとしてサポートする変更です。これにより、ProxyLogger 経由で log を呼び出した際に NoMethodError にならず、適切なレベル判定を行ってログが出力されるようになります。

  1. 変更内容の詳細

背景

  • Ruby の標準ライブラリ Logger には add メソッドがあり、log はそのエイリアスとして定義されています。
  • Rails には複数のラッパーロガーがあり、例えば ActiveSupport::BroadcastLoggerlogadd に委譲しています。
  • 一方で ActiveSupport::ProxyLogger は「ほぼ完全な Logger 互換インターフェイス」を目指しているにもかかわらず、log が未定義だったため、log 呼び出しで NoMethodError が発生していました。
ruby
logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error)
logger.log(Logger::ERROR, "msg") # これが NoMethodError になっていた

このPRでの変更点

コード上の変更 (activesupport/lib/active_support/proxy_logger.rb)

  • ProxyLoggerlogadd のエイリアスとして追加。

擬似的には次のような変更イメージです(実際のコードは1行のみ):

ruby
class ActiveSupport::ProxyLogger
  # 既存: add は、指定の severity 以上かどうかを判定して元の logger に委譲する
  # def add(severity, message = nil, progname = nil, &block)
  #   ...
  # end

  alias_method :log, :add
end

これにより、ProxyLogger は標準 Logger と同様、log(severity, message = nil, progname = nil, &block) という形での呼び出しが可能になります。

テストの追加・修正 (activesupport/test/proxy_logger_test.rb)

  • log を呼び出したときに、従来の add 経由と同様の挙動をすることを確認するテストを追加。
  • 具体的には以下のようなケースがカバーされていると考えられます:
    • ProxyLogger に設定した severity 以上のログレベルで log を呼ぶと、内部の logger へメッセージが転送される。
    • 設定した severity 未満のレベルの場合は無視される(元の add と同じ判定ロジックの再利用)。

使用例

ruby
logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error)

# エラー以上だけログさせたい ProxyLogger

# これまでは:
# logger.add(Logger::ERROR, "error message")   # OK
# logger.log(Logger::ERROR, "error message")   # NoMethodError

# これからは:
logger.log(Logger::ERROR, "error message")     # 標準 Logger と同じように動く
logger.log(Logger::INFO,  "info message")      # ProxyLogger のレベルが :error なので無視される

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

    • ActiveSupport::ProxyLogger を直接利用しているコードで、Logger 互換インターフェイスを期待して log を呼び出していた場合に挙動が変わります。
      • 以前:NoMethodError が発生
      • 今後:add と同様にログ出力される(severity フィルタリング付き)
    • つまり、これまで意図せず落ちていたコードが「正しく動く」ようになるため、実運用上は改善方向の変更です。
    • BroadcastLogger は既に log をサポートしているため、ProxyLogger / BroadcastLogger を抽象的に扱うコードでインターフェイス差異を気にする必要がなくなります。
  • 注意点

    • log メソッドのシグネチャは Logger#log/Logger#add に準拠しているため、第一引数に severity を整数(Logger::DEBUG など)で渡すことを前提としています。
    • ProxyLogger 特有の severity 制御(ActiveSupport::ProxyLogger.new(underlying_logger, :error) のように指定するフィルタ)は、log 呼び出しでもそのまま有効です。
      そのため、「log なら常に素通しされる」といった特別扱いはなく、一貫して add と同じルールでフィルタされます。
    • NoMethodError を利用した分岐(例: respond_to?(:log)ProxyLogger かどうか判断していたようなコード)があれば、その挙動は変わる可能性がありますが、一般的にはそのような依存は推奨されません。

  1. 参考情報 (あれば)
  • Ruby 標準 Logger ドキュメント:
    • logadd のエイリアスとして定義されている
    • log(severity, message = nil, progname = nil) { ... }
  • 関連クラス:
    • ActiveSupport::ProxyLogger
      • 既存のロガーに対して、severity でフィルタしつつ委譲するラッパー
    • ActiveSupport::BroadcastLogger
      • 複数のロガーに同報するラッパーで、既に log をサポートしている

このPRにより、ProxyLogger も「標準 Logger ほぼフル互換」という設計意図に沿った実装になり、標準 Logger を想定したコードとの互換性が向上します。


#58023 Honor an explicit value: nil in color_field

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

  1. 概要 (1-2文で)
    color_field ヘルパーが、明示的に value: nil が指定された場合でもモデルの値で上書きしてしまう不具合を修正し、value: オプション(nil を含む)を正しく尊重するようにした PR です。これにより「入力欄をあえて空にしたい」というケースが期待通り動作します。

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

問題点(Before)

form_with / form_for などで color_field を使うとき、次のように value: nil を渡しても:

ruby
color_field("car", "color", value: nil)

本来は <input ...>value 属性が付かないことを期待しますが、実際にはモデルの値(例: #000fff)が入っていました。

html
<!-- Before の実際の出力例 -->
<input id="car_color"
       name="car[color]"
       type="color"
       value="#000fff" />

つまり、「あえて値を空にしたい」という明示的な指定が無視されていた状態です。

修正内容(After)

今回の PR により:

  • value: オプションが指定された場合は、それが nil であっても優先される
  • value: オプションが指定されていない場合のみ、従来どおりモデルの属性値が使われる

結果として、同じコード:

ruby
color_field("car", "color", value: nil)

は次のように value 属性なしでレンダリングされるようになります。

html
<!-- After の出力例 -->
<input id="car_color"
       name="car[color]"
       type="color" />

内部的な変更は color_field タグ生成ロジックの 1 行のみで、value: が明示されているかどうかの判定方法を修正したものと考えられます(options.key?(:value) を見る形など)。

テスト・ドキュメント

  • actionview/test/template/form_helper_test.rb にテストが追加され、
    • value: nil を指定したときに value 属性が出力されないこと
      が検証されるようになっています。
  • actionview/CHANGELOG.md に今回の変更点が追記されています。

  1. 影響範囲・注意点
  • 影響するのは ActionView::Helpers::FormHelper#color_field のみです。
  • これまで「color_fieldvalue: nil を渡しても結局モデル値が表示される」ことを前提に回避策を組んでいた場合、その挙動が変わります(ただし、そのような依存は通常あまりないと考えられます)。
  • 同様の仕様(「value が明示されていればそれを優先、なければモデル値」)は他のヘルパーでも一般的なため、今回の変更により color_field も他のフォームヘルパーと一貫した動きをする 形になります。
  • もし「常にモデルの値を出したい」のであれば、value: オプションを明示的に渡さないようにするのが正しい利用方法になります。

  1. 参考情報 (あれば)

#58035 Don't mutate the array passed to RedisCacheStore via :client

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

  1. 概要 (1-2文で)
    RedisCacheStore のコンストラクタに client: オプションとして渡した配列を内部で破壊的に変更しないようにし、FrozenError が発生しないように修正した PR です。redis-client ベースの未リリース実装にのみ影響する挙動で、本修正によりドキュメントどおりの使い方が安全に行えるようになります。

  1. 変更内容の詳細

何が問題だったか

ActiveSupport::Cache::RedisCacheStore.new(client: ...) に以下のような配列を渡すと:

ruby
configs = [RedisClient.config(url: url1), RedisClient.config(url: url2)]
ActiveSupport::Cache::RedisCacheStore.new(client: configs)

従来のコードでは、RedisCacheStore 内部処理で 呼び出し元が渡した configs 配列自体を破壊的に変更し、RedisClient::Config の配列を RedisClient::Pooled の配列に置き換えていました。

ruby
configs
# => [#&lt;RedisClient::Pooled ...>, #&lt;RedisClient::Pooled ...>]
# 本来は Config のままでいてほしいのに、呼び出し元の配列が書き換わってしまう

さらに、呼び出し元が防御的に configs.freeze して渡すと、内部で配列を変更しようとして FrozenError が発生していました。

ruby
ActiveSupport::Cache::RedisCacheStore.new(client: configs.freeze)
# => FrozenError: can't modify frozen Array

この使い方自体は、RedisCacheStore の RDoc に公式に記載されているパターンです:

ruby
config.cache_store = :redis_cache_store, client: [
  RedisClient.config(...),
  RedisClient.config(...)
]

つまり「公式に推奨している書き方が、内部実装の都合で配列を書き換えてしまう/freeze すると例外になる」という不整合がありました。

何を変えたか

修正後は以下のような動作になります。

  • RedisCacheStore 内部で 独自の配列を新たに作成して プール (RedisClient::Pooled) を保持する
  • 呼び出し元から渡された
    • client: の配列オブジェクト
    • その中身の RedisClient::Config オブジェクト を一切変更しない
  • 結果として、configsfreeze して渡しても問題なく動作する

イメージとしては、以前はこうだったものを:

ruby
# 擬似コードイメージ(before)
@clients = options[:client] # ここで呼び出し元の配列をそのまま使う
@clients.map! { |config| RedisClient::Pooled.new(config) } # 呼び出し元の配列を破壊的変更

このように書き換えた形です:

ruby
# 擬似コードイメージ(after)
configs = options[:client]
@clients = configs.map { |config| RedisClient::Pooled.new(config) } # 新しい配列を生成
# configs 自体も、その要素である config も変更しない

テスト (activesupport/test/cache/stores/redis_cache_store_test.rb) には、

  • 渡した client 配列とその要素が変化していないこと
  • client 配列を freeze しても例外にならないこと
    を確認するケースが追加されています。

なお、この挙動変更は redis-client ベースの未リリース実装 (コミット 5843a7b7f2) に対するもので、gem としてリリースされた Rails にはまだ載っていない段階の問題を修正しています。

また、今回の修正方針は、以前 Cache::Store#delete_multi に渡される names 配列をミューテートしないようにしたコミット (32d9437962) と同じ考え方に揃えたものです。


  1. 影響範囲・注意点
  • 対象:
    • ActiveSupport::Cache::RedisCacheStoreredis-client ベース実装で利用する場合のみ
    • まだ正式リリース前の実装向けの修正なので、現行の安定版 Rails には直接の影響はありません
  • 振る舞いの変更点:
    • client: に渡した配列・中身が これまで暗黙に書き換えられていたケースがあれば、その前提は壊れます
      • ただし、そのような依存は非意図的かつ望ましくないため、多くの場合は「壊れる」のではなく「正しくなる」方向です
  • freeze 対応:
    • client: configs.freeze のように、防御的に freeze をかけて渡しても動作するようになります
    • 設定オブジェクトをイミュータブルに扱いたい場合に安全です
  • 既存コードへの互換性:
    • RedisCacheStore に渡した結果として、呼び出し元の configsPooled の配列に変わることに依存している」ような特殊なコードがあれば挙動は変わりますが、そのような依存は避けるべきです
    • 通常の・ドキュメントどおりの使い方をしている限りは、実質的にはバグ修正であり互換性上の問題はほぼありません

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/58035

  • 関連コミット:

    • redis-client ベース実装導入: 5843a7b7f2
    • Cache::Store#delete_multi で引数配列をミューテートしないようにしたコミット: 32d9437962
  • ドキュメント上の利用例(問題になっていたパターン):

    ruby
    config.cache_store = :redis_cache_store, client: [
      RedisClient.config(url: url1),
      RedisClient.config(url: url2)
    ]

    この書き方が、そのまま安全に動くように整合性が取られています。


#58037 Require enumerable core ext for Range#sole

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

  1. 概要 (1-2文で)
    Range#sole が内部的に依存している Enumerable 拡張が読み込まれていないため、require "active_support/core_ext/range" だけだと常にクラッシュしていた問題を修正する PR です。Range#sole 定義ファイルで active_support/core_ext/enumerable を明示的に require することで、単体ロード時でも正しく動作するようにしています。

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

問題の原因

  • active_support/core_ext/range/sole.rbRange#sole を定義しているが、その実装は以下2つに依存している:
    • Enumerable#solesuper 経由で呼び出す)
    • 例外クラス ActiveSupport::EnumerableCoreExt::SoleItemExpectedError
  • これらはいずれも active_support/core_ext/enumerable に定義されている。
  • しかし、以下のどのファイルからも active_support/core_ext/enumerable が require されていなかった:
    • active_support/core_ext/range/sole.rb
    • active_support/core_ext/range.rb
    • range 関連の require 連鎖上のどこからも

その結果、ドキュメントにあるような「require "active_support/core_ext/range" だけを行うスタンドアロン利用」の場合、Range#sole が必ず異常終了していた。

具体的な不具合挙動

ruby -I lib などの素の Ruby 環境で:

ruby
require "active_support/core_ext/range"

(1..1).sole
# => NoMethodError: super: no superclass method `sole` for an instance of Range

(..1).sole
# => NameError: uninitialized constant ActiveSupport::EnumerableCoreExt
  • (1..1).sole は本来 Enumerable#sole に処理を委譲すべきだが、
    • そもそも Enumerable#sole が未定義のため super が失敗
  • (..1).sole は例外として ActiveSupport::EnumerableCoreExt::SoleItemExpectedError を参照しようとするが、
    • そのモジュール自体がロードされておらず NameError になる

この PR の修正内容

activesupport/lib/active_support/core_ext/range/sole.rb に以下のような require を追加(実質 +2 行):

ruby
require "active_support/core_ext/enumerable"

これにより:

ruby
require "active_support/core_ext/range"

(1..1).sole
# => 1

(2..1).sole
# => ActiveSupport::EnumerableCoreExt::SoleItemExpectedError: no item found

(..1).sole
# => ActiveSupport::EnumerableCoreExt::SoleItemExpectedError:
#    infinite range '..1' cannot represent a sole item

と、Enumerable#sole/関連例外クラスが正しく機能し、Range 拡張として期待通りの動作になる。

Rails フルロード時との関係

  • Rails 全体をロードした場合は、もともとどこかで active_support/core_ext/enumerable が require されており、この問題は表面化していなかった。
  • 今回の修正により、Rails フルロード時の挙動には変更なし(後方互換性維持)。

  1. 影響範囲・注意点
  • 影響範囲:
    • active_support/core_ext/range単体で require している環境でのみ挙動が変わる。
    • 具体的には、Gem やスクリプト内で軽量に Range 拡張だけを取り込みたい、という使い方をしているケース。
  • 挙動の変更点:
    • これまで異常終了(NoMethodError / NameError)していた Range#sole 呼び出しが、Enumerable 拡張と同じ意味論で正常に動作するようになる。
    • 例外も今後は ActiveSupport::EnumerableCoreExt::SoleItemExpectedError に揃うため、例外型に依存したコードを書いている場合は、もともと壊れていたコードが正しく動く(または正しい型に rescue される)ようになる。
  • パフォーマンス・ロードコスト:
    • active_support/core_ext/range/sole 読み込み時に active_support/core_ext/enumerable も読み込まれるため、ほんのわずかにロードコストが増える可能性はあるが、ActiveSupport 利用前提の環境では実質的な影響は軽微。

  1. 参考情報 (あれば)
  • Range#sole は 8.1.0 で導入(コミット: 044d2eac9fe3)。
  • この PR は、過去にも何度か行われている「missing require」の修正系列に属する:
    • e4de1ca9b5
    • 302fdb7677
    • 358ac36edf
    • a1c57cf69f
  • Enumerable#soleSoleItemExpectedError の挙動を理解しておくと、Range#sole の挙動も把握しやすいです(要素が 0 個・複数個・無限列の場合に例外を投げる、など)。

#58032 Fix event timestamp type in EventReporter rdoc

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

  1. 概要 (1-2文で)
    ActiveSupport::EventReporter のドキュメントで、イベントハッシュに含まれる :timestamp の型が誤って Float と記載されていたのを、実際の実装に合わせて Integer に修正した PR です。実際の値はナノ秒精度のエポックタイム(Process.clock_gettime(..., :nanosecond))であり、巨大整数である点が明示されました。

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

何を直したか

ActiveSupport::EventReporter の rdoc(コメントドキュメント)において、以下2箇所でイベントハッシュ中の :timestamp キーの型が誤っていました。

  • クラスレベルの subscriber 向けドキュメント
  • #subscribe メソッドのドキュメント

これらで

ruby
:timestamp # Float (The timestamp of the event, in nanoseconds)

のように書かれていたものを、

ruby
:timestamp # Integer (The timestamp of the event, in nanoseconds)

に修正しています(実際の diff は +2/-2 行の単純な型表記変更のみ)。

なぜ Integer なのか

ActiveSupport::EventReporter がイベントを報告するとき、タイムスタンプは以下のように取得されています(実際のコードは概念的にはこのイメージ):

ruby
Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
# => 1738964843208679035 のような整数
  • 返り値は「エポック(UNIX time)からのナノ秒」を表す Integer です。
  • 例として rdoc に載っている 1738964843208679035 のような値は、Float では正確に表現できないほど大きく、
    1738964843208679035.to_f.to_i == 1738964843208679035false になります。
    つまり Float にキャストすると情報が落ちるため、「Float 型の timestamp」というドキュメントは誤りでした。

この PR により、ドキュメント上の型表記が実装(Integer)と整合しました。


  1. 影響範囲・注意点

影響範囲

  • 実装コードには変更なし で、ドキュメントのみの修正です。
  • ただし、これまでドキュメントを信じて Float 前提で処理していた subscriber 実装には以下のような影響がありえます:
    • 型チェックで Float を期待していた場合に失敗する。
    • timestamp.to_f して小数として扱っているコードは、すでに精度喪失が発生していた可能性がある(この PR で振る舞いが変わるわけではない)。

実装側で意識すべきこと

  • 正しい型:
    • :timestamp は「エポックからのナノ秒」の Integer です。
  • もし秒単位やミリ秒・マイクロ秒など別の単位にしたい場合は、自前でスケール変換する必要があります:
ruby
# nanosecond -> second(Float)
seconds = timestamp / 1_000_000_000.0

# nanosecond -> millisecond(Integer, 切り捨て)
milliseconds = timestamp / 1_000_000
  • 精度を重視する場合は、内部表現は Integer のまま扱い、外部とのインターフェースで必要に応じて変換するのが安全です。

  1. 参考情報 (あれば)
  • 使用されている API:
    • Process.clock_gettime
      Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond) はエポックからのナノ秒を Integer で返します。
  • 関連するドキュメント修正:
    • この PR は、同じファイルのドキュメント調整を行ったコミット 27f4e99e51 の後続として位置づけられています。
  • 対象クラス:
    • ActiveSupport::EventReporter を使って独自 subscriber を実装している場合は、event[:timestamp] を Integer として扱うよう、実装と型注釈(Sorbet/RBS など)やコメントの整合を確認しておくと良いです。

#58033 Instrument the normalized key in Cache#fetch hit path

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

  1. 概要 (1-2文で)
    ActiveSupport::Cache::Store#fetch がキャッシュヒット時に発行する cache_fetch_hit.active_support 通知の payload[:key] を、他のキャッシュ関連イベントと同様に「正規化済み(namespace 付与後)のキー」に統一した変更です。これにより、ヒットとミスで同じ論理エントリに対して異なるキーが通知される不整合が解消されます。

  1. 変更内容の詳細

これまでの挙動

ActiveSupport::Cache::Store#fetch を、例えば以下のような namespace 付きで使っているケースを考えます:

ruby
cache = ActiveSupport::Cache::MemoryStore.new(namespace: "myapp")

cache.fetch("counter") { 0 }

通知周りの挙動は以下のようになっていました:

  • fetch ミス

    • キー "counter" に対し、内部では "myapp:counter" に正規化される
    • 発行されるイベント例:
      • cache_generate.active_support
      • cache_read.active_support
      • cache_write.active_support
    • 上記イベントの payload[:key] はすべて "myapp:counter"(正規化済みキー)
  • fetch ヒット

    • キャッシュから値が取れた場合に発行される
      • cache_fetch_hit.active_support
    • しかし、このイベントの payload[:key]"counter"(namespace 付与前の生キー)になっていた

つまり、同じ論理キー「counter」について:

  • ミス時に観測されるキー: "myapp:counter"
  • ヒット時に観測されるキー: "counter"

となっており、メトリクス集計やログ集約で「1つのキー」として扱うのが困難でした。

変更後の挙動

この PR により、cache_fetch_hit.active_support でも正規化済みキーを使うように修正されています。

  • fetch ミス

    • 今まで通り:
      • payload[:key] == "myapp:counter" (変化なし)
  • fetch ヒット

    • 変更前: payload[:key] == "counter"
    • 変更後: payload[:key] == "myapp:counter"

これで以下のイベントはすべて同じキー表現を使うようになります:

  • cache_read.active_support
  • cache_write.active_support
  • cache_delete.active_support
  • cache_exist?.active_support
  • cache_generate.active_support
  • cache_fetch_hit.active_support ← 今回統一された

コードレベルのイメージ

実際の diff はごく小さく、概念的には以下のような変更です(擬似コード):

ruby
# 変更前(イメージ)
def fetch(name, **options)
  # ...
  instrument(:cache_fetch_hit, key: name) do
    # ...
  end
end

# 変更後(イメージ)
def fetch(name, **options)
  # ...
  key = normalize_key(name, options) # namespace を含む正規化
  instrument(:cache_fetch_hit, key: key) do
    # ...
  end
end

テスト (cache_instrumentation_behavior) では、namespace ありのストアで #fetch ヒット時の cache_fetch_hit 通知に含まれる :key が、他のイベントと同じ正規化済みキーになっていることを検証するテストケースが追加されています。


  1. 影響範囲・注意点
  • 影響を受けるのは ActiveSupport のキャッシュ通知 (ActiveSupport::Notifications) を購読しているコード です。
  • とくに、"cache_fetch_hit.active_support" を購読している処理で、payload[:key]「生のキー」として扱っていた場合 に挙動が変わります。
    • 例: "counter" という文字列を前提にログのフィルタリングをしていた場合、今後は "myapp:counter" となるためマッチしなくなります。
  • 一方で、すでに他のイベント(cache_read / cache_write など)に合わせて 正規化済みキー前提で処理している場合 は、むしろ整合性が取れるようになり、集計や監視ロジックがシンプルになります。
  • namespace を使っていないストア (namespace: nil またはデフォルト) の場合は、正規化前後でキー文字列が同じになるため、実質的な変化はほぼありません。

互換性の観点

  • 破壊的変更になり得るケース:
    • cache_fetch_hitpayload[:key] を使って、
      • 独自メトリクス(例: Prometheus ラベル)を定義している
      • ログやトレースでキー名をパースしている
    • 上記の処理が「namespace なしのキー」を前提にしている場合は、コードの修正・確認が必要です。
  • 多くのアプリケーションでは、むしろ「ヒットとミスでキー表現が揃う」ことで挙動が分かりやすくなり、監視などが改善されるはずです。

  1. 参考情報 (あれば)
  • この変更は、他のキャッシュ関連の修正と一貫性を取るものです:
    • c013352ff2: MemoryStore#increment / #decrement の instrumentation 正規化
    • #57915: Cache#exist? イベントにおけるキー正規化の統一
  • 関連クラス・モジュール:
    • ActiveSupport::Cache::Store
    • ActiveSupport::Notifications
  • ドキュメント上も payload[:key] は「実際のキャッシュキー」として扱うことが想定されており、この PR によってその期待に沿う挙動になります。

#57830 Make ActionController Renderers ractor safe

マージ日: 2026/7/7 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    ActionController の Renderers(js, xml など)を Ractor セーフにするため、レンダラー名を保持するオブジェクトの扱いを見直し、ミューテーションの停止と freeze を行った PRです。併せて、内部利用想定の定数を非推奨化しつつ、ActiveSupport の deprecation プロキシに「初期化後に参照先を書き換えられる」仕組みを追加しています。

  1. 変更内容の詳細

背景と問題点

  • Ractor セーフにするには、Ractor 間で共有されるオブジェクトは基本的に不変(frozen)である必要がある。
  • ActionController::Renderers では、利用可能な renderer 名(js, xml など)を保持する Set を起動後に mutate(追加・削除)していたため、Ractor セーフではなかった。
  • 「ただ freeze する」だけでは、boot 時に renderer を追加・削除できなくなってしまう。

この PR では「外からは不変に見える構造」を維持しつつ、内部的には差し替えで対応するような形に変更しています。


1. 定数の非推奨化

  • ActionController::Renderers 周りで「ドキュメント化されていたが、本来は内部用としたい」定数を deprecate。
  • この定数は renderer 名リスト(Set)に直接アクセスできるもので、外部コードからのミューテーションを招きうるため、Ractor セーフ化の妨げになりやすい。

イメージとしては、例えば以下のようなコードが外部から書ける状態だったものを:

ruby
# 例: 以前なら(または現在でも)こう書けていた可能性がある
ActionController::Renderers::RENDERERS.add :foo

今後はこのような直接変更パターンを廃止方向に誘導するために deprecate しています。
(実際の定数名は PR で対象になっているものですが、ここではイメージとして記載しています)


2. Deprecated constant の参照先を書き換え可能に

activesupport/lib/active_support/deprecation/proxy_wrappers.rb に変更が加えられています。

  • Deprecation 用の proxy オブジェクトが、「初期化後に参照ターゲット(実体)を書き換えられる」API/仕組みを追加。
  • これにより、「古い定数 → 新しい定数 or 新しい構造体」へのラップを維持したまま、内部実装だけを差し替えることが可能になります。

技術的には、次のようなことができるようになります(擬似コード):

ruby
DeprecatedConstantProxy.new("OldConst", "NewConst")

# 後から NewConst の実体を差し替え
DeprecatedConstantProxy.for("OldConst").target = NewNewConst

これにより:

  • 外部コードは依然として旧定数経由でアクセスできる(ただし deprecation warning は出る)。
  • Rails 内部では Ractor セーフな新しいオブジェクトに差し替え可能。
  • Ractor セーフ化のために「Set を凍らせたいが、boot 時の追加は許可したい」といった要件も満たしやすくなる。

3. Renderers の Set をミューテーションしない & freeze する

actionpack/lib/action_controller/metal/renderers.rb のコア変更です。

目的:

  • レンダラー名を保持する Set を mutate せず、都度新しい Set を生成する形に変更。
  • 最終的に、その Set を freeze して Ractor セーフな共有ができるようにする。

典型的には、以前は次のようなイメージだったものが:

ruby
# 以前(イメージ)
RENDERERS = Set.new

def self.add_renderer(name, &block)
  RENDERERS.add(name)
  # その他の登録処理...
end

def self.remove_renderer(name)
  RENDERERS.delete(name)
  # その他の削除処理...
end

これを「不変な Set を差し替える」パターンへ変更します:

ruby
# イメージコード: 実際の実装とは異なる可能性があります
RENDERERS = Set.new.freeze

def self.add_renderer(name, &block)
  new_set = RENDERERS.dup
  new_set.add(name)
  RENDERERS = new_set.freeze
end

def self.remove_renderer(name)
  new_set = RENDERERS.dup
  new_set.delete(name)
  RENDERERS = new_set.freeze
end

実際には、直接定数再代入ではなく、内部的なオブジェクトや deprecation proxy などを挟みつつ、
「外から見えるインターフェースは保ちつつ、内部の Set は常に frozen」になるように実装されています。

この変更により:

  • Ractor 間で Renderers の情報を共有しても、共有されるオブジェクトは frozen な Set なので安全。
  • 追加・削除は「新しい frozen Set を作って差し替える」ため、Ractor セーフを維持しつつ動的変更を可能にしている。

4. テストと CHANGELOG

  • actionpack/test/controller/renderers_test.rb
    • renderer の追加・削除が期待どおり動くこと、および Ractor セーフ化に関連する挙動がテストされています。
  • activesupport/test/deprecation_test.rb
    • 新たに追加された「deprecated constant の target を変更できる」機能のテストが追加。
  • actionpack/CHANGELOG.md
    • Action Pack の変更点として「Renderers が Ractor セーフになった」こと、および関連する挙動の変更が明記されています。

  1. 影響範囲・注意点

  2. 内部定数への依存コード

    • 非推奨化された定数(レンダラー名の Set など)に直接アクセスしているアプリ/ライブラリは、deprecation warning が出るようになります。
    • 今後のバージョンで削除される可能性が高いので、代替 API(通常は ActionController::Renderers.add, remove, あるいは public に提供されているインターフェース)を利用するようにリファクタが必要です。
  3. カスタム renderer の追加・削除

    • 通常の追加・削除 (ActionController::Renderers.add / remove) を使っている分には挙動は変わらない想定です。
    • ただし、内部構造の変更に依存していたメタプログラミング的なコード(RENDERERS への直接アクセスや Set のミューテーションなど)は動作が変わる/壊れる可能性があります。
  4. Ractor 利用アプリ

    • この PR により、少なくとも Renderers 周りは Ractor セーフになります。
    • Rails を本格的に Ractor で並列実行する場合、他のコンポーネントの Ractor セーフ性も考慮する必要がありますが、ActionController 周辺の安全性は向上しています。

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/57830
  • 関連概念:
    • Ruby Ractor: 並列実行モデル。共有オブジェクトは基本的に frozen を要求。
    • ActiveSupport::Deprecation::DeprecatedConstantProxy 系の機構: 定数名は維持しつつ、内部実装や参照先の移行を段階的に行うために使われる仕組み。
  • 実運用での推奨:
    • カスタム renderer は公式にドキュメントされている API を通して登録/削除する。
    • 内部定数には依存せず、deprecation warning が出たら早めに対応する。

#57825 Introduce ractorize

マージ日: 2026/7/6 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    Rails アプリケーションを Ractor から共有可能にするための土台として、Rails::Application#ractorize! が導入され、基本的な Rails アプリを Ractor.make_shareable できるようにする PR です。あわせて、Ractor 非対応だったいくつかの内部実装(継承オプション、KeyGenerator、Mutex など)が Ractor セーフになるよう調整されています。

  1. 変更内容の詳細

新機能: Rails::Application#ractorize!

Rails::Application に以下のメソッドが追加されています。

ruby
class MyApp::Application < Rails::Application
  config.eager_load = true
  config.cache_store = :null_store

  # Ractor から共有可能にする
  config.after_initialize do
    Rails.application.ractorize!
  end
end

ractorize! が行うこと(PR 説明+差分から推測される内容):

  • アプリケーションを Ractor 共有可能な状態に「準備」する
    • Ractor セーフでないオブジェクト(Mutex など)を排除・解体
    • Engine / Initializer 周りの内部状態を Ractor 共有可能な形に再編
  • 最後に Ractor.make_shareable(app) を呼び出し、Rails.application 自体を Ractor から共有可能なオブジェクトにする

これにより、例えば「メイン Ractor でアプリをブート → サブ Ractor にアプリを共有 → 各 Ractor でリクエスト処理」みたいな構成の土台が整えられます。

制約事項

現時点(この PR 時点)で ractorize! を使うためには、次の条件が必要です:

  • アプリケーションコードとルーティングは eager load 必須
    • config.eager_load = true
    • 遅延ロードだと、Ractor 間でクラス定義のタイミングや状態が不整合になるため
  • Action Cable は非対応
    • 内部的にスレッド/コネクション管理など Ractor セーフでない構造が多数あるため
  • キャッシュストアは :null_store 必須
    • 他のキャッシュストア(メモリストアなど)が Ractor 共有できないデータ構造を使っているため

将来的にはこれらの制約を減らす予定とのことですが、この PR はあくまで最初の足場です。


継承オプション (Rails::Configuration / Rails::Engine / Rails::Initializable)

Rails::Configuration や Engine の継承オプション、initializer 系の設定は、これまで「通常の Ruby オブジェクト(しばしばミュータブル)」を前提にしていました。

この PR では:

  • 「継承オプション」の保持方法が Ractor セーフになるよう調整
    • Hashdefault_proc がミューテーションを行わないようにしたり
    • 初期化後に freeze 可能な形に揃えたり
  • Rails::Engine ごとにアプリケーションを eager に構築し、Ractor 非共有なオブジェクト(特に Mutex)を「初期化時に使って、共有前に破棄」するようなフローに変更
  • Rails::Initializable 周りで使われる Proc などを Ractor 共有可能な形に修正

これにより、Rails.application の内部構成(設定や初期化処理の一覧など)を Ractor.make_shareable に掛けられるようになります。


KeyGenerator を Ractor セーフに

ActiveSupport::KeyGenerator と、その利用箇所である ActiveSupport::Messages::RotationCoordinator に変更が入っています。

ポイント:

  • これまで KeyGenerator は内部で「Concurrent Map(あるいは似た構造)」を使っており、これは Ractor 共有不可能でした。
  • この PR では:
    • 「キー生成用のキャッシュ(Map)」を Ractor ローカル にする設計に変更
    • つまり「Ractor ごとに KeyGenerator のキャッシュを持つ」ようにして、Rails.application 自体は共有しつつも、共有不可能な Map は各 Ractor 側で持つ

影響として:

  • 同じ Rails.application を複数 Ractor から使っても、KeyGenerator 自体は共有されるが、その内部キャッシュは各 Ractor で独立して持たれる。
  • そのため、初回アクセス時のキー計算コストは Ractor ごとに一度ずつ発生するが、Ractor セーフネスとトレードオフにした pragmatic な対応になっている。

テスト (activesupport/test/key_generator_test.rb) も追加されており、Ractor 使用時の振る舞いを検証しています。


ActionDispatch Journey / Transition Table の Ractor 対応

actionpack/lib/action_dispatch/journey/gtg/transition_table.rb に追記:

  • ルーティングエンジン(Journey)の状態遷移テーブル構築処理で、Ractor 共有可能でない要素(例: デフォルト Proc 付き Hash)が含まれていた部分を修正
  • 遷移テーブル構造が Ractor.make_shareable できるようにするための小変更(ミュータブルな default_proc の削除・freeze など)が行われていると考えられます。

これにより、Rails.application.routes の内部表現も Ractor 共有しやすくなります。


Ractor 関連テストの追加

  • activesupport/lib/active_support/testing/ractors_assertions.rb
    • Ractor 関連のアサーションヘルパを定義
    • 例: 「このオブジェクトは Ractor から共有できるか」などをテストで確認する仕組み
  • railties/test/application/ractors_test.rb
    • 実際に Rails アプリをブートして ractorize! を呼び、別 Ractor から利用できるかなどを検証する統合テスト
    • eager load / null_store などの前提条件を満たしたアプリで、Ractor.make_shareable が成功することを確認していると推測できます。

  1. 影響範囲・注意点

影響範囲

  • Rails 内部:
    • アプリケーション設定 (Rails::Configuration)
    • Engine の構造 (Rails::Engine)
    • Initializer の登録・実行方法
    • KeyGenerator / メッセージ暗号化系
    • ルーティングエンジン(Journey)の内部構造
  • 外部 API レベル:
    • 新メソッド Rails::Application#ractorize!
    • Ractor ベースで Rails を動かしたい人向けの初期サポート

既存アプリへの影響

  • この PR 自体は、既存アプリが従来どおり(Ractor を使わずに)動く場合には、基本的に挙動変更を引き起こさないように配慮されています。
  • ただし以下の点は意識しておくと良いです:
    • internal な Hash の default_proc など、一部挙動が微妙に変わっている可能性はある(通常アプリではまず問題にならないレベルを目指しているはず)
    • KeyGenerator の実装変更に伴い、Ractor を明示的に使う場合は「Ractor ごとに KeyGenerator のキャッシュが別になる」ことを理解しておく必要がある

Ractor を使いたい場合の注意点

  • 必須条件:
    • config.eager_load = true
    • config.cache_store = :null_store
    • Action Cable を使わない
  • 実運用で使うには、まだ制約が大きく、かつスレッド・プロセス・Ractor の組み合わせの設計が難しいため、現時点では「実験的」な段階と見なすのが妥当です。
  • アプリケーションコード側も、自前で保持するグローバル状態やシングルトン、クラス変数などが Ractor セーフになっていないと、ractorize! 後の Ractor.make_shareable に失敗する可能性があります。

  1. 参考情報 (あれば)
  • この PR で触れられている関連 PR:
  • Ractor 自体の仕様:
  • Ractor 対応の設計上の方針:
    • 共有可能なオブジェクト (Ractor.shareable?) のみを共有し、Ractor ローカルで持つべきもの(キャッシュや Mutex など)は Ractor ごとに分離する、という設計が Rails 内部にも適用され始めているのがこの PR のポイントです。

#58018 Translate if_exists/if_not_exists guards when inverting remove_column and add_reference/remove_reference

マージ日: 2026/7/6 | 作成者: @55728

  1. 概要 (1-2文で)
    remove_column および add_reference / remove_reference(とそのエイリアス add_belongs_to / remove_belongs_to)について、マイグレーションの巻き戻し時に if_exists / if_not_exists オプションが正しく反転して適用されるように修正した PR です。これにより、これらのコマンドを使った冪等なマイグレーションの change / revert が、期待通りに動作するようになります。

  1. 変更内容の詳細

背景: command_recorder と idempotency guard

Rails の ActiveRecord::Migration::CommandRecorder は、change マイグレーションや revert ブロック実行時に発行されたコマンドを記録し、その逆操作(inverse)を自動生成します。

既に以下のコマンドでは、if_existsif_not_exists が「逆向きのガード」として正しく変換されていました:

  • add_column
  • add_index
  • add_foreign_key / remove_foreign_key
  • add_check_constraint / remove_check_constraint

しかし、以下のコマンドファミリーではこの変換が抜けていました:

  • remove_column
  • add_reference / remove_reference
  • add_belongs_to / remove_belongs_toadd_reference / remove_reference のエイリアス)

今回の PR は、この「抜けていた2ファミリー」に対してもガードの変換を適用するものです。


remove_column の変更

変更前の挙動

ruby
inverse_of :remove_column, [:users, :nickname, :string, if_exists: true]
# => [:add_column, [:users, :nickname, :string, { if_exists: true }], nil]
  • remove_column に渡した if_exists: true が、そのまま逆操作の add_column にも渡されていました。
  • しかし、add_columnif_exists オプションを認識しないため、ロールバック時にはガードが完全に無視され、「意図に反する形で」動いてしまう可能性がありました。
    • 元々の意図: 「カラムが存在する場合だけ remove する」→ ロールバックでは「カラムが存在しない場合だけ add する」としたい
    • 実際の挙動: ロールバック側でガードが消えるため、常に add_column を実行してしまう

変更後の挙動

ruby
inverse_of :remove_column, [:users, :nickname, :string, if_exists: true]
# => [:add_column, [:users, :nickname, :string, { if_not_exists: true }], nil]
  • remove_column(if_exists: true) の逆操作は add_column(if_not_exists: true) となり、
    • 削除側: 「あれば消す」
    • 逆操作: 「なければ作る」
  • というペアになり、冪等な前方・後方マイグレーションが成立するようになりました。

add_reference / remove_reference(およびエイリアス)の変更

変更前の挙動

ruby
inverse_of :add_reference, [:t, :u, if_not_exists: true]
# => [:remove_reference, [:t, :u, { if_not_exists: true }]]  # ガードが変換されず無視される

inverse_of :remove_reference, [:t, :u, if_exists: true]
# => [:add_reference, [:t, :u, { if_exists: true }]]          # 同上
  • コマンド名だけが addremove で入れ替わり、オプションハッシュはそのまま渡されていました。
  • その結果:
    • add_reference 側は if_not_exists をサポートしていても
    • remove_reference 側は if_not_exists / if_exists をサポートしていなかったり、逆に if_exists だけを見ていたり
  • という非対称な API のせいで、片方のコマンドで指定したオプションが逆操作側で「無視」されていました。

変更後の挙動

  • add_reference(if_not_exists: true) の逆操作は remove_reference(if_exists: true)
  • remove_reference(if_exists: true) の逆操作は add_reference(if_not_exists: true)
  • となるように、if_exists / if_not_exists を互いに反転して渡すように修正されました。
ruby
# 例: change マイグレーション
def change
  add_reference :comments, :post, if_not_exists: true
end

# ロールバック時に内部的に解釈されるイメージ
# => remove_reference :comments, :post, if_exists: true

add_belongs_to / remove_belongs_toadd_reference / remove_reference のエイリアスなので、この修正の恩恵をそのまま受けます。


  1. 影響範囲・注意点
  • 対象:
    • change マイグレーション(または revert ブロック)で
      • remove_column(..., if_exists: true)
      • add_reference(..., if_not_exists: true)
      • remove_reference(..., if_exists: true)
      • およびその *_belongs_to エイリアス を使っているケース。
  • 影響:
    • これまで:
      • 逆操作側でガードが「消える / 無視される」ことがあった。
      • そのため、本番環境などで「途中の状態からロールバック」するようなケースで、不要なエラーや意図しない再作成などが起こりうる。
    • これから:
      • if_exists / if_not_exists が逆操作側にも正しく反映され、前方・後方どちらの方向にも冪等性が担保されやすくなる。
  • 互換性について:
    • 挙動変更ではありますが、「ガードが無視されていた」状態から「意図通りガードされる」状態への修正なので、ほとんどの場合はバグ修正として望ましい変更です。
    • もし「逆操作側でガードが付かないこと」を前提にした非常に特殊なワークアラウンドをしている場合は、今回の修正で挙動が変わる可能性があります(そのような依存は避けるべきです)。

  1. 参考情報 (あれば)
  • 変更ファイル:
    • activerecord/lib/active_record/migration/command_recorder.rb
      • inverse_of のロジックに、remove_column および add_reference / remove_reference 系に対する if_existsif_not_exists の変換処理を追加。
    • activerecord/test/cases/migration/command_recorder_test.rb
      • 上記の変換が正しく行われることを保証するテストを追加。
  • 関連する既存仕様:
    • すでに add_column, add_index, add_foreign_key/remove_foreign_key, add_check_constraint/remove_check_constraint では、同様の guard 変換が行われており、本 PR はその不整合を解消するものです。

#58024 Derive the expected queue from the delivery job in assert_enqueued_email_with

マージ日: 2026/7/6 | 作成者: @55728

  1. 概要 (1-2文で)
    assert_enqueued_email_with が、メール送信時のキュー名を「メイラー側の設定」ではなく「delivery_job 側の設定」から導出するように修正されました。これにより、カスタム delivery_jobqueue_as を指定している場合でも、正しくテストが通るようになります。

  1. 変更内容の詳細

これまでの挙動(問題点)

ActionMailer::TestHelper#assert_enqueued_email_with は、期待されるキュー名を mailer_class.deliver_later_queue_name から導出していました。

しかし、次のようにカスタム delivery_job を使い、そのジョブに queue_as を設定しているケースでは問題が発生していました。

ruby
class RegistrationDeliveryJob < ActionMailer::MailDeliveryJob
  queue_as :notifications
end

class RegistrationMailer < ApplicationMailer
  self.delivery_job = RegistrationDeliveryJob
end

RegistrationMailer.welcome.deliver_later

assert_enqueued_email_with RegistrationMailer, :welcome
# => No enqueued job found with {job: RegistrationDeliveryJob, ..., queue: "mailers"}
#    Potential matches: {..., queue: "notifications", ...}
  • 実際には queue: "notifications" にジョブが積まれている
  • しかしアサーションは queue: "mailers" を期待してしまうため、失敗していました

これは deliver_later のドキュメント上「カスタム delivery_job を使う場合は、その job がキュー名を支配する」とある契約と食い違っていました。

今回の修正内容

変更ファイル:

  • actionmailer/lib/action_mailer/test_helper.rb (+1 / -1)
  • actionmailer/test/test_helper_test.rb (+18 / 0)

テストヘルパの実装を微調整し、「期待されるキュー名の決定ロジック」を以下のように変更しています。

  • 以前: mailer_class.deliver_later_queue_name からキュー名を導出
  • 今回: mailer_class.delivery_job 側からキュー名を導出
    delivery_jobqueue_as で指定したキューがある場合はそれを使う

結果として:

ruby
class RegistrationDeliveryJob < ActionMailer::MailDeliveryJob
  queue_as :notifications
end

class RegistrationMailer < ApplicationMailer
  self.delivery_job = RegistrationDeliveryJob
end

RegistrationMailer.welcome.deliver_later

assert_enqueued_email_with RegistrationMailer, :welcome
# 期待キュー名が "notifications" になり、正常にパスする

テスト (test_helper_test.rb) には、この振る舞いを保証するためのケースが追加されています(カスタム delivery_job + queue_as を使った場合のアサーション成功を検証)。

なお、デフォルトの MailDeliveryJob を使うケースや queue_name_prefix を利用しているケースでも、最終的に解決されるキュー名は従来と変わらないように設計されています。


  1. 影響範囲・注意点
  • テストでの影響

    • カスタム delivery_job を使っていて queue_as を指定している場合、これまで不当に落ちていた assert_enqueued_email_with が通るようになります。
    • 逆に言えば、「実運用ではカスタム delivery_job のキューに積まれている」のに、「テストで mailer 側のキュー名を前提にしていた」ような間違ったテストは、今回の修正で正しい挙動に修正されます。
  • 既存コードへの互換性

    • デフォルトの MailDeliveryJob を使っている場合、これまでとキュー名の解決結果は同じになるように配慮されているため、通常は影響はありません。
    • queue_name_prefix を利用している場合も、最終的なキュー名は従来と同じになるように実装されています。
  • テストコード側で明示的に queue を指定している場合の確認

    • assert_enqueued_email_withqueue: オプションで明示的にキューを指定している場合、その指定がカスタム delivery_job の設定と矛盾していないかは確認しておくと安全です。

  1. 参考情報 (あれば)
  • ActionMailer::TestHelper#assert_enqueued_email_with:
    • メイラーとメソッドを指定して、そのメールが正しくジョブとしてエンキューされているかを検証するためのテストヘルパ。
  • ActionMailer::Base#delivery_job:
    • メール送信に使用する Active Job クラスを指定するための設定。
    • deliver_later のドキュメント上、「カスタム delivery_job を設定した場合、その job がキュー名 (queue_as) を決定する」と規定されている。
  • 関連 PR: #58009(今回の修正はそのフォローアップとして位置付けられています)。

#58017 Preserve subsecond precision in the DateTime.now travel stub under with_usec

マージ日: 2026/7/6 | 作成者: @55728

  1. 概要 (1-2文で)
    travel_to(time, with_usec: true) 使用時に、DateTime.now でもサブ秒(マイクロ秒)精度が正しく保持されるように修正したPRです。これにより、同じ凍結時刻を参照する Time.nowDateTime.now の挙動が一致します。

  1. 変更内容の詳細

何が問題だったか

ActiveSupport::Testing::TimeHelpers#travel_to には with_usec: オプションがあります。

  • with_usec: true
    凍結する時刻のサブ秒(usec)まで含めて時間を固定する意図。
  • with_usec: false(デフォルト)
    秒単位で丸めて時間を固定する(usec は 0 になる)。

これまでは:

ruby
travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999), with_usec: true do
  Time.now.usec      # => 999999  # サブ秒が保持される
  DateTime.now.usec  # => 0       # サブ秒が失われる(バグ)
end

と、Time.nowwith_usec: true に従ってサブ秒を保持していた一方、DateTime.now のスタブは秒単位に切り捨ててしまっていました。

今回の修正内容

activesupport/lib/active_support/testing/time_helpers.rbDateTime.now のスタブ生成部分が 1行変更され、with_usec: true のときにサブ秒を保持するようになりました。

PRの説明に対応する挙動は次の通りです:

ruby
travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999), with_usec: true do
  Time.now.usec      # => 999999
  DateTime.now.usec  # => 999999   # Time.now と一致するように修正
end

また、テスト (activesupport/test/time_travel_test.rb) に DateTime.nowusec を検証するケースが 13行追加されています。
これにより、with_usec: trueDateTime.now がサブ秒を保持すること、with_usec: false では保持しないこと(usec == 0)が回 regresion テストとして担保されます。

with_usec: false の挙動は変わらない

デフォルトの with_usec: false の場合は、これまで通り DateTime.now.usec0 のままです:

ruby
travel_to Time.utc(2014, 10, 10, 10, 10, 50, 999999) do # with_usec: false がデフォルト
  Time.now.usec      # => 0
  DateTime.now.usec  # => 0
end

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

    • テストコードで ActiveSupport::Testing::TimeHelpers#travel_toDateTime.now を併用しており、かつ with_usec: true を指定しているケース。
    • そういったコードでは、これまで「秒単位に丸められていた」DateTime.now が「マイクロ秒まで含めて凍結される」ように変わります。
  • 挙動変化により起こりうること:

    • 以前の動作に依存して「DateTime.now.usec は常に 0 である」と決め打ちしていたテストがあると、with_usec: true の文脈では失敗する可能性があります。
    • ただし、with_usec: true を明示している場合は「サブ秒を保持したい」という意図が通常のため、この変更は意図により沿ったものといえます。
  • 注意点:

    • 秒単位で固定したいテストは、with_usec: を明示的に指定するか、with_usec: false を使うことを前提にするとよいです。
    • ライブラリやアプリ側で、DateTime.now ベースのロジックと Time.now ベースのロジックを混在させていた場合、これにより両者の挙動が揃うため、むしろ一貫性が向上します。

  1. 参考情報 (あれば)
  • 該当コード:
    • ActiveSupport::Testing::TimeHelpers#travel_totime_helpers.rb
  • 挙動の意図:
    • with_usec: true … テスト中の「凍結時刻」をサブ秒精度まで含めて再現したい場面向け。
    • with_usec: false … 秒単位で十分なテスト向け(以前からのデフォルト挙動)。

#58021 Honor an explicit :selected option in time_zone_select

マージ日: 2026/7/6 | 作成者: @55728

  1. 概要 (1-2文で)
    time_zone_select ヘルパーに selected: オプションを明示的に渡した場合、その値がきちんと選択状態として反映されるように修正したPRです。selected: を指定しない既存コードの挙動は変わりません。

  1. 変更内容の詳細

これまでの問題

time_zone_select は第4引数のオプションとして selected: を受け取れますが、実際には無視されており、常に「オブジェクトの属性値」もしくは「default: オプション」で選択タイムゾーンが決まっていました。

ruby
time_zone_select("firm", "time_zone", nil, selected: "B")
# 本来は "B" を選択させたいが、
# 実際には firm.time_zone(または default:)の値が選択されていた

フォームオブジェクト側の値を一時的に上書きしたいケース(例: 管理画面で一括編集、確認画面で一時的に別の値を見せたい等)で、selected: が効かないというバグ的挙動になっていました。

修正後の挙動

このPRでは time_zone_select の内部で、「どの値を選択状態にするか」の決定ロジックにおいて、:selected が明示的に渡されていればそれを最優先で使うように修正しています。

  • 優先順位(概念的には):
    1. options[:selected] があればそれを選択値とする
    2. なければ、従来通り「オブジェクトの属性値」または options[:default] を使う

使用イメージ:

ruby
# 明示的に "B" を選択したい
time_zone_select("firm", "time_zone", nil, selected: "B")
# => HTML上も "B" に対応する <option> が selected になる

# これまで通りオブジェクトの値を使いたい場合
time_zone_select("firm", "time_zone")
# => firm.time_zone が selected

# オブジェクトに値がなければ default: を使う(従来通り)
time_zone_select("firm", "time_zone", nil, default: "UTC")

コード上の変更点としては、ActionView::Helpers::Tags::TimeZoneSelect 内の選択値決定ロジックに 3行程度の追加があり、テスト (form_options_helper_test.rb) で「selected: を渡したときにそれが優先される」ことを検証するケースが追加されています。


  1. 影響範囲・注意点
  • 影響を受けるヘルパー

    • ActionView::Helpers::FormOptionsHelper#time_zone_select
    • form_for / form_with 等から呼ばれる同名ヘルパーも含む
  • 既存コードへの影響

    • selected: を渡していないコードは挙動が変わりません(オブジェクト値 + default: の従来ロジックそのまま)。
    • これまで「selected: を指定しても無視される」と思ってワークアラウンドをしていた場合は挙動が変わる可能性があります。
      例えば:
      ruby
      # selected: が無視される前提で、わざと「無関係な値」を渡していたような特殊なハック
      time_zone_select("user", "time_zone", nil, selected: "dummy")
      こうしたコードがあると、今回から "dummy"(に対応するタイムゾーン)が本当に選択されてしまいます。
  • 注意点 / ベストプラクティス

    • 「モデルの値をそのまま使いたい」場合は selected: を指定しない方が分かりやすく安全です。
    • 「モデルの値よりも、フォーム側で明示的に指定した選択状態を優先したい」ケース(例: 確認画面での上書き、独自の初期値制御)では selected: を積極的に使えるようになります。
    • default: はあくまで「オブジェクトの値がない時のフォールバック」として機能し、selected: を指定すれば default: よりも selected: が優先されます。

  1. 参考情報 (あれば)
  • 対象コード: actionview/lib/action_view/helpers/tags/time_zone_select.rb
  • テスト: actionview/test/template/form_options_helper_test.rb
  • 関連ヘルパー: select, time_select など他の select 系ヘルパー も似た API を持つため、今後 selected: の扱いがそれらと整合的になるかを見る際の参考になります。

#58012 Fix broken links in guides [ci skip]

マージ日: 2026/7/6 | 作成者: @fuentesjr

  1. 概要 (1-2文で)
    このPRは、Rails Guides 内および外部サイトへの「リンク切れ」を3箇所修正するドキュメント専用の変更です。動作コードには一切手を入れておらず、ガイドの参照先を正しいURL/アンカーに差し替えています。

  1. 変更内容の詳細

変更はいずれも1行ずつのリンク先修正です。

2-1. Active Storage ガイド: ImageMagick のリンク修正

対象: guides/source/active_storage_overview.md の Requirements セクション

変更前:

markdown
https://imagemagick.org/index.php

この URL は 404 になるため、サイトルートに変更。

変更後:

markdown
https://imagemagick.org/

Rails 自体の機能には影響しませんが、Active Storage をセットアップする際に参照する ImageMagick の公式サイトへのリンクが正常に開けるようになります。


2-2. Action Mailer ガイド: Active Job へのアンカー修正

対象: guides/source/action_mailer_basics.md の「コントローラの編集」付近

Action Mailer から Active Job を使ってメールを非同期送信する (deliver_later) 説明へのリンクが壊れていました。

変更前:

markdown
active_job_basics.html#action-mailer

Active Job ガイド側に #action-mailer というアンカーは存在せず、クリックしてもページトップ(もしくは何も起きない)状態でした。

変更後:

markdown
active_job_basics.html#example-sending-email

#example-sending-email セクションは、Active Job 経由でのメール送信(deliver_later など)を具体的に解説している箇所であり、文脈的にも正しいターゲットです。
Action Mailer ガイドから「メールをジョブ経由で送るサンプル」に直接ジャンプできるようになります。


2-3. Configuring ガイド: キャッシュストアへのアンカー修正

対象: guides/source/configuring.mdconfig.cache_store の説明部分

config.cache_store の説明中で、キャッシュストアの詳細として Caching ガイド中の特定セクションへリンクしていますが、そのアンカー名が存在しませんでした。

変更前:

markdown
caching_with_rails.html#cache-stores

Caching ガイドには #cache-stores セクションがなく、リンクはページトップ相当になっていました。

変更後:

markdown
caching_with_rails.html#other-cache-stores

#other-cache-stores セクションは、config.cache_store の説明で列挙されているストアと一致する内容を扱っています:

  • :memory_store
  • :file_store
  • :mem_cache_store
  • :null_store
  • :redis_cache_store

これにより、config.cache_store の説明から、そのまま各キャッシュストアの詳細説明へジャンプできるようになります。


  1. 影響範囲・注意点
  • 影響範囲は Rails Guides (ドキュメント) のみで、アプリケーションコードやフレームワークの挙動には一切影響しません。
  • 既存の URL あるいはアンカーから別の場所へ「リダイレクト」されるようになったのではなく、もともと機能していなかったリンクを、意図した場所に正しく張り直した変更です。
  • ドキュメント生成 (guides のビルド) が通るかどうかはほぼノーリスクですが、
    • プレーンな URL (https://imagemagick.org/)
    • 同一リポジトリ内の他のガイドへの相対リンク (active_job_basics.html#example-sending-email, caching_with_rails.html#other-cache-stores)
      の3つなので、ビルドエラー等の可能性も実質ありません。
  • リンクターゲットとなるガイド側のアンカー名 (#example-sending-email, #other-cache-stores) が今後変更されると再びリンク切れになるため、ドキュメント側でアンカーを変更する際は相互参照の確認が必要です。

  1. 参考情報 (あれば)

運用面では、こうした broken link は CI で自動検出しづらいことが多いため、今後リンクチェッカー(外部URL & 内部アンカー)の導入を検討すると、同種の不具合を早期に防ぎやすくなります。


#58016 Forward keyword arguments through deprecation proxies

マージ日: 2026/7/6 | 作成者: @55728

  1. 概要 (1-2文で)
    DeprecatedObjectProxy / DeprecatedInstanceVariableProxy 経由でキーワード引数必須メソッドを呼ぶと ArgumentError になっていた問題を修正し、正しくキーワード引数として転送されるようにした PR です。Ruby 3 以降の「キーワード引数の分離」による不具合を、ruby2_keywords を使って最小限の変更で解消しています。

  1. 変更内容の詳細

何が問題だったか

Ruby 3 以降では、以下のような「最後の Hash をキーワード引数として解釈する」挙動が変わり、*args で受けるとキーワードにならず「位置引数の Hash」として扱われます。

ruby
def target(a:)
  a
end

def caller(*args, &block)
  target(*args, &block)
end

caller({ a: 1 })
# Ruby 3 では ArgumentError: wrong number of arguments ...

ActiveSupport::Deprecation の以下のプロキシクラスが、まさにこのパターンでした。

  • ActiveSupport::Deprecation::DeprecatedObjectProxy
  • ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy

これらはベースクラス DeprecationProxy#method_missing を継承しており、その定義は

ruby
def method_missing(method, *args, &block)
  warn(method, caller)
  @object.__send__(method, *args, &block)
end

のような形でした。このため、プロキシ対象のメソッドが「必須キーワード引数」を持っていると、以下のようなエラーになっていました。

ruby
class Sample
  def foo(a:)
    a
  end
end

sample = Sample.new
proxy = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(sample, "sample", ActiveSupport::Deprecation.new)

proxy.foo(a: 1)
# Before:
# ArgumentError: wrong number of arguments (given 1, expected 0; required keyword: a)

Hash { a: 1 } が位置引数として転送されてしまい、キーワードとして解釈されなかったためです。

どう直したか

DeprecationProxy#method_missingruby2_keywords を付けました。

ruby
class DeprecationProxy
  ruby2_keywords def method_missing(method, *args, &block)
    warn(method, caller)
    @object.__send__(method, *args, &block)
  end
end

ruby2_keywords を付与すると、

  • 呼び出し元での「キーワードっぽい Hash」としての情報が *args の中にフラグ付きで保持される
  • 呼び出し先(ここでは @object.__send__)に転送したとき、再び「キーワード引数」として解釈される

という Ruby 2 互換の挙動が有効になります。

これにより、先ほどの例は次のように期待どおり動作します。

ruby
proxy.foo(a: 1)
# After:
# => 1
# かつ deprecation warning は従来どおり出る

なぜ *args, **kwargs にしなかったのか

DeprecationProxy#method_missingwarnargs を渡し、特に DeprecatedInstanceVariableProxy#warn では args.inspect をそのままメッセージに埋め込んでいます。

ruby
def warn(method, callstack)
  ActiveSupport::Deprecation.warn(
    "some message... Args: #{args.inspect}",
    callstack
  )
end

ここでメソッドシグネチャを

ruby
def method_missing(method, *args, **kwargs, &block)
  ...
end

のように分割してしまうと、args からキーワードが抜け落ち、警告メッセージ上では

  • 以前: Args: [1, {:a=>2}]
  • 変更後: Args: [1](キーワードは別の kwargs にいる)

となり、互換性が崩れます。

ruby2_keywords を使えば、

  • 呼び出し側・メッセージ上では「旧来どおり args に Hash が入っている」状態を維持
  • 転送先メソッドでは Ruby 3 スタイルのキーワードとして正しく解釈

という両方を満たせるため、この一行だけで済ませています。

他との一貫性

同じファイルにある DeprecatedConstantProxy#method_missing は、すでに

ruby
def method_missing(...)
  ...
  target.__send__(...)
end

という「...(dot-dot-dot)」構文でキーワードを正しく転送する形に直されていました。この PR は、その sibling 実装と挙動を揃える修正でもあります。

テスト追加

activesupport/test/deprecation_test.rb にテストが追加され、以下のようなケースがカバーされています(要約):

  • DeprecatedObjectProxy 経由で、必須キーワード引数を持つメソッドを呼べること
  • DeprecatedInstanceVariableProxy 経由でも同様に動作すること
  • その際に deprecation warning が出ること
  • 引数の表示(args.inspect)が従来どおりであること

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

    • ActiveSupport::Deprecation::DeprecatedObjectProxy
    • ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy
    • 特に、キーワード引数を持つメソッドをこれらのプロキシ越しに呼んでいるコードは挙動が変わります(以前は ArgumentError で落ちていたが、正常に呼べるようになる)。
  • 後方互換性

    • これまで例外が出ていた箇所が正しく動作するようになるため、基本的にはバグフィックスです。
    • warn に渡される args の内容・inspect 文字列は維持されるので、ログ監視・テストでメッセージを厳密比較している場合でも影響はほぼありません。
    • ruby2_keywords 自体は Ruby 3 系では「非推奨だがまだ使える互換機能」として残っていますが、Rails 内部実装に閉じており、利用者側のコードに直ちに影響はありません。
  • 注意点

    • 将来 Ruby 本体から ruby2_keywords が削除される可能性はあります。その場合、この周辺実装を *args, **kwargs などに書き換えつつ、警告メッセージの互換性をどう担保するか、という課題は残ります。
    • プロキシ経由でのメソッド呼び出しに頼ったテストが、「例外が出ること」を前提にしていた場合は、テストが通らなくなる可能性があります。

  1. 参考情報 (あれば)

#58015 Strip surrounding whitespace before parsing a duration in Hash.from_xml

マージ日: 2026/7/6 | 作成者: @55728

  1. 概要 (1-2文で)
    Hash.from_xmltype="duration" の要素をパースする際に、前後の空白文字(改行・インデント)を除去してから ActiveSupport::Duration に変換するように修正された PR です。これにより、「人間が整形した XML」に含まれる改行やインデントのせいで ParsingError が起きていた問題が解消されます。

  1. 変更内容の詳細

何が問題だったか

以下のように、整形された XML で type="duration" のノードに改行やインデントが含まれると:

ruby
Hash.from_xml(<<~XML)
  <event>
    <length type="duration">
      PT1H
    </length>
  </event>
XML

実際のテキストノードの値は " PT1H\n " のように前後に空白を含む形になり、
ActiveSupport::Duration::ISO8601Parser がこれを正しく解釈できず、ParsingError を送出していました。
同じような書き方でも、type="date", type="datetime", type="integer", type="boolean" などは内部で strip されている(または許容度が高い)ため問題なくパースできていましたが、duration だけが厳格だった状態です。

今回の修正内容

activesupport/lib/active_support/xml_mini.rb で、type="duration" の値をパースする前に strip をかけるように 1 行差し替えが行われました。

イメージとしては:

ruby
when "duration"
  # 変更前(概念的に):
  ActiveSupport::Duration.parse(value)

  # 変更後(概念的に):
  ActiveSupport::Duration.parse(value.to_s.strip)
end

※実際のコードは若干異なる可能性がありますが、意味としては「文字列に変換して前後の空白を削る」の 1 行の修正です。

テスト

activesupport/test/xml_mini_test.rb に 1 行のテストケースが追加されています。
内容は、インデント付きの type="duration" ノードを含む XML を Hash.from_xml に渡したときに、ActiveSupport::Duration(たとえば 1.hour)として正しくパースされることを検証するものです。

これにより、今回の回避策が将来のリグレッションで壊れないようにカバーされています。


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

    • ActiveSupport::XmlMini 経由で Hash.from_xml を用い、type="duration" のノードを扱う箇所すべてに影響があります。
    • インデントや改行を含む「整形された XML」や、人手で書かれた XML を扱うアプリケーションで特に恩恵があります。
    • これまで type="duration" だけ他の型と比べて扱いがシビアだった点が解消され、datedatetime などと同様にプレーンテキストに余分な空白があっても安全にパースされるようになります。
  • 後方互換性 / 互換性リスク

    • 前後の空白を strip するだけなので、通常の利用ではほぼリスクはありません。
    • ISO8601 の duration 文字列の意味的に、先頭・末尾の空白に意味を持たせるケースは現実的には想定されず、互換性問題はほぼ発生しないと考えられます。
    • もし「わざと先頭/末尾に空白を入れてパースエラーを起こさせる」といった挙動に依存しているコードがあれば、その挙動は変化します(エラーにならずパースに成功するようになる)が、そのような依存は一般的にはありえないはずです。
  • 運用・テスト上の注意

    • 既存の XML をそのまま Hash.from_xml に渡して duration 周りでエラーになっていた場合、今回の変更を含む Rails バージョンに上げることで自然に解消されます。
    • XML の仕様やフォーマッタの変更は不要です。既存の pretty-print やインデント付き XML をそのまま利用できます。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58015
    タイトル: Strip surrounding whitespace before parsing a duration in Hash.from_xml
    作者: @55728
  • 関連クラス:
    • ActiveSupport::XmlMini
    • ActiveSupport::Duration::ISO8601Parser
  • 類似仕様:
    • type="date", type="datetime", type="integer", type="boolean" など他の型では、既にプレーンテキストの前後空白に起因する問題は発生していませんでしたが、duration もその挙動に揃えられた形になります。

#58014 Remove stale payload-based filter example in EventReporter#subscribe rdoc

マージ日: 2026/7/6 | 作成者: @55728

  1. 概要 (1-2文で)
    EventReporter#subscribe の RDoc に載っていた「payload によるフィルタ」サンプルコードが、実際の挙動と矛盾しており誤解を招くため削除され、フィルタから参照できるのはイベント名のみであることを明示するドキュメント修正です。コードの挙動には一切変更はなく、ドキュメントのみの更新です。

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

変更の背景

Rails.event.subscribe の RDoc には、フィルタの例として次の2パターンが記載されていました(問題のある方だけ抜粋):

ruby
Rails.event.subscribe(subscriber) { |event| event[:payload].is_a?(UserEvent) }

しかし、実際にフィルタブロックに渡される event は「イベント名だけを持つハッシュ」であり、event[:payload] は常に nil になります。そのため、このサンプルをコピペして使うと、条件が常に false になり、イベントを一件も受け取れないという不具合を招きます(ただしコード上は例外も出ないため気づきにくい)。

モジュールレベルの「Filtered Subscriptions」の説明ではすでに:

Note that only the +:name+ key is available to filters, not the entire payload.

と明記されており、#subscribe の例だけがこれと矛盾していました。

実際の変更内容

変更ファイル: activesupport/lib/active_support/event_reporter.rb

行レベルでは以下が行われています(概要ベース):

  • #subscribe の RDoc から、event[:payload] を使ったフィルタ例を削除
  • 代わりに、「フィルタから使えるのは :name キーのみであり、payload 全体は参照できない」という注意書きを、クラス(モジュール)レベルの説明と同様に #subscribe のドキュメントにも追記

具体的な正しい利用イメージは、たとえば次のような形になります:

ruby
# OK: イベント名でフィルタする例
Rails.event.subscribe(->(event) { Rails.logger.info(event) }) do |event|
  event[:name] == "user.created"
end

event で使えるのは event[:name] だけで、payload はここには来ない、という点を強調する形に修正されています。


  1. 影響範囲・注意点
  • 影響範囲は「ドキュメントのみ」であり、ActiveSupport::EventReporter / Rails.event.subscribe の挙動そのものは以前から変わっていません。
  • すでに event[:payload] をフィルタ内で参照していたコードがある場合、それはこの PR の前から期待通りには動いておらず、「常に false になってイベントが一件も流れない」状態だった可能性が高いです。
    • もしそのようなコードが見つかった場合:
      • フィルタ部分では「イベント名のみ」を使ってフィルタする
      • payload ベースの条件分岐は「サブスクライバー側(購読ブロックの中)」で行う
        といった構成に変更する必要があります。
  • テストや実装を壊す類の変更ではないため、Rails を更新したことで挙動が変わることはありませんが、「今までたまたま気づかずに誤ったサンプルを真似していた」ケースを洗い出す良いトリガーにはなります。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58014
  • 関連ドキュメント: ActiveSupport::EventReporter の「Filtered Subscriptions」セクション
    • フィルタに渡されるハッシュは {:name => "event_name"} のように :name のみを保持し、payload は含まれないという仕様が明文化されています。

#58005 Make add_column(if_not_exists: true) reversible

マージ日: 2026/7/5 | 作成者: @55728

  1. 概要 (1-2文で)
    add_column(..., if_not_exists: true) を含むマイグレーションをロールバックした際に、逆操作として不正なオプションを付けた remove_column が生成されていた問題を修正し、正しく if_exists: true が付くようにした PR です。これにより、該当マイグレーションのロールバックが安全かつ他のコマンドと一貫した挙動になります。

  1. 変更内容の詳細

何が問題だったか

以下のようなマイグレーションを書いた場合:

ruby
add_column :users, :age, :integer, if_not_exists: true

rails db:rollback などで逆マイグレーションを生成・実行すると、これまでは CommandRecorder によって次のように解釈されていました:

ruby
# BEFORE (誤った逆操作)
remove_column :users, :age, :integer, if_not_exists: true

しかし remove_columnif_not_exists オプションを受け付けません。そのため:

  • 逆マイグレーションとして不正なメソッド呼び出しが生成される
  • 将来的にはエラーになったり、意図しない挙動を起こす可能性がある

という問題がありました。

今回の修正内容

ActiveRecord::Migration::CommandRecorder において、add_column の逆操作 (invert_add_column) を組み立てる際のオプション変換ロジックが追加されました。

挙動は次のように変わります:

ruby
# 正方向
add_column :users, :age, :integer, if_not_exists: true

# 逆方向 (今回の PR 以降)
remove_column :users, :age, :integer, if_exists: true

PR の説明にもある通り:

ruby
# inverse of add_column(:table, :column, :type, if_not_exists: true)
# before: remove_column(:table, :column, :type, if_not_exists: true)
# after:  remove_column(:table, :column, :type, if_exists: true)

つまり、if_not_existsif_exists に変換してから remove_column に渡すようになりました。

他のコマンドとの一貫性

add_index, add_foreign_key, add_check_constraint ではすでに同様の変換が実装されており:

  • add_*(..., if_not_exists: true)
    の逆操作として
  • remove_* ( ..., if_exists: true)

が生成されています。
今回の PR で add_column もその設計方針に揃えられました。

テスト

activerecord/test/cases/migration/command_recorder_test.rb にテストが追加され、add_column(..., if_not_exists: true) を記録して逆変換した際に if_exists: true が付いた remove_column が生成されることが検証されています。


  1. 影響範囲・注意点
  • 対象:
    • add_column :table, :column, :type, if_not_exists: true を使用しているマイグレーション
    • それらを rails db:rollbackrevert で巻き戻すケース
  • 影響:
    • 以前は「見かけ上」remove_column(..., if_not_exists: true) が生成されていましたが、これはそもそも正当な API ではありませんでした。
    • 今後は正しく remove_column(..., if_exists: true) になるため、「すでにカラムが削除されている場合」でもエラーにならない、より安全なロールバックになります。
  • 後方互換性:
    • 公開されていない・公式にサポートされていない挙動(remove_columnif_not_exists を渡す)を修正する変更であり、通常のアプリケーションコードには実害はほぼありません。
    • もしテストやアプリケーション側で CommandRecorder の生の出力を文字列ベースで厳密に検証している場合は、if_not_existsif_exists への変更によりテストが変わる可能性があります。
  • マイグレーションの書き方としての注意:
    • 今後も、冪等にしたいマイグレーションでは:
      • 追加系: add_column ..., if_not_exists: true
      • 削除系: remove_column ..., if_exists: true を明示的に書くのが推奨されますが、今回の修正により、逆操作生成の際にもこのペアが自動で維持されるようになります。

  1. 参考情報 (あれば)
  • 当該 PR: rails/rails #58005 “Make add_column(if_not_exists: true) reversible”
  • 関連する他の reversible な API 例:
    • add_index(..., if_not_exists: true)remove_index(..., if_exists: true)
    • add_foreign_key(..., if_not_exists: true)remove_foreign_key(..., if_exists: true)
    • add_check_constraint(..., if_not_exists: true)remove_check_constraint(..., if_exists: true)

これらと同様のパターンが add_column にも適用された、という位置づけの変更です。


#58004 Return no rows without a query for none.ids

マージ日: 2026/7/5 | 作成者: @55728

  1. 概要 (1-2文で)
    Relation#none に対して ids / async_ids を呼び出したとき、これまでは実際に SELECT ... WHERE (1=0) のクエリが発行されていたのを、pluck と同様にクエリを投げずに常に空配列(またはそれを返す Promise)を返すように修正した PR です。none が「DB に触れない null object」として一貫した動作をするようになります。

  1. 変更内容の詳細

背景

ActiveRecord::Relation#none は「必ず結果が 0 件で、かつ DB にクエリを投げない」ことを意図した null relation です。

  • 既に pluck など一部メソッドは @none フラグを見て、DB に触れずに結果を返していました。
  • 一方で idsnone を特別扱いしておらず、内部の条件が "1=0" という生の SQL だったため、矛盾判定(例: where(id: []))に引っかからず、実クエリが発行されていました。

その結果、以前は:

ruby
Post.none.ids        # => 実際には SELECT "posts"."id" FROM "posts" WHERE (1=0)
Post.none.pluck(:id) # => [] (クエリ無し)

という不整合な挙動になっていました。

何をしたか

calculations.rb 内の ids / async_ids に、pluck と同様の @none チェックを追加しています。

擬似コードレベルでは以下のようなガードが入っています(実装からの意訳):

ruby
def ids
  if @none
    []
  else
    # 従来どおりの ids のロジック(pluck(:id) 相当)
  end
end

def async_ids
  if @none
    # すでに完了した Promise(Future)を返し、その値は [] になる
  else
    # 従来どおり非同期で ids を取得
  end
end

結果として:

ruby
Post.none.ids        # => []  (クエリ発行なし)
Post.none.pluck(:id) # => []  (クエリ発行なし)
Post.none.async_ids  # => 完了済み Promise, value は [] (クエリ発行なし)

となり、none の定義(「DB に触れず常に空」)とテストの期待どおりの挙動になります。

テスト

activerecord/test/cases/null_relation_test.rb にテストが追加され、null_relation に対する ids の挙動(クエリを投げずに空配列を返すこと)がカバーされています。既存の pluck, delete_all, update_all, exists?, count などのテストと並ぶ形で「null relation は DB に触れない」という性質の一貫性を確認しています。


  1. 影響範囲・注意点
  • 対象メソッド

    • ActiveRecord::Relation#ids
    • ActiveRecord::Relation#async_ids
  • 挙動の変化

    • Model.none.ids が、これまでの「SELECT ... WHERE (1=0) を実行して空配列を返す」から、「クエリを打たずに即座に空配列を返す」に変わります。
    • async_ids も同様に、「クエリを打たない完了済み Promise(値は [])」を返します。
  • 後方互換性

    • 戻り値([])は変わらないため、普通にアプリ側で none.ids を使っている限りは機能的な互換性は保たれています
    • 影響し得るのは、以下のようなケースです:
      • none.ids でクエリが走ること」を前提にしてメトリクス・ログ収集・モンキーパッチなどをしていた場合(ほぼ無いはずですが)。
      • 「クエリが走るかどうか」を検査するテストを書いていた場合(assert_sql などで WHERE (1=0) を期待しているようなテストは失敗する可能性があります)。
  • パフォーマンス

    • none を通るパスについては、不必要なクエリが完全に削除されるため、ごくわずかですがパフォーマンス改善になります。
    • 特に大量に none が生成される複雑なスコープチェーンで ids を呼んでいるようなケースでは、微妙なオーバーヘッド削減になります。
  • 一貫性

    • none が関与する以下のメソッドの挙動が揃います:
      • pluck, ids, async_ids, delete_all, update_all, exists?, count, …
    • none からは絶対にクエリを出さない」という前提でコードや最適化・テストが書けるようになります。

  1. 参考情報 (あれば)
  • 修正対象: activerecord/lib/active_record/relation/calculations.rb
  • テスト: activerecord/test/cases/null_relation_test.rb
  • 関連知識:
    • Relation#none は「常に空」の relation を返すための標準 API で、default_scope, 条件付きスコープ, 認可ロジックなどで「このユーザーには 1 件も見せない」ことを表現するときに頻用されます。
    • ids は内部的に pluck(primary_key) に近い実装になっており、今回の修正で none に対する挙動が pluck と明示的に揃えられました。

#58007 Instrument the normalized key in MemoryStore#increment/#decrement

マージ日: 2026/7/5 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveSupport::Cache::MemoryStoreincrement / decrement が発行する計測イベント(cache_increment / cache_decrement)について、これまで「生のキー」を送っていたものを、他のストア実装と同様に「正規化済み(名前空間付き)のキー」を送るように揃えた変更です。これにより、メモリストア利用時の計測イベントのキー情報が一貫したものになります。

  1. 変更内容の詳細

何が問題だったか

MemoryStore#increment / #decrement が ActiveSupport のインストゥルメンテーション(ActiveSupport::Notifications)で発行するイベント:

  • cache_increment.active_support
  • cache_decrement.active_support

のペイロード :key に、キャッシュストア内部で実際に使われている「正規化後キー」ではなく、呼び出し側が渡した「生のキー」をそのまま入れていました。

ruby
cache = ActiveSupport::Cache::MemoryStore.new(namespace: "myapp")

cache.increment("counter")
# 以前:
#   payload[:key] #=> "counter"           (namespace が反映されていない)
#
# 期待される & 今回の修正後:
#   payload[:key] #=> "myapp:counter"

他のストア (FileStore, MemCacheStore, RedisCacheStore) や同じ MemoryStoreread / write イベントでは、すでに「正規化済みキー」を使っていたため、インクリメント/デクリメントだけ挙動がずれていました。

具体的な修正内容

変更ファイル:

  • activesupport/lib/active_support/cache/memory_store.rb
  • activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb

ポイント:

  1. MemoryStore#increment / #decrement のインストゥルメンテーション時に、payload[:key] として 正規化後のキー を渡すように修正

    • 正規化処理は normalize_key / normalize_key 相当の内部ヘルパを通す形(既存の read/write と同じパス)に変更されたと考えられます。
    • 実質的には namespace などが反映された「実際にストア内で利用しているキー」をイベントのキーとして報告します。
  2. テスト (cache_instrumentation_behavior) の修正

    • インクリメント/デクリメントの計測イベントにおける payload[:key] が名前空間付きであることを確認するアサーションが追加・修正。
    • これにより、将来的に再度「生のキー」を返すような退行があった場合に検出できるようになっています。

サンプル(概念的なイメージ):

ruby
cache = ActiveSupport::Cache::MemoryStore.new(namespace: "myapp")

ActiveSupport::Notifications.subscribe("cache_increment.active_support") do |*args|
  event = ActiveSupport::Notifications::Event.new(*args)
  puts event.payload[:key]
end

cache.increment("counter")
# 修正前: "counter"
# 修正後: "myapp:counter"

  1. 影響範囲・注意点
  • 影響対象
    • ActiveSupport::Cache::MemoryStore を使っていて
    • cache_increment.active_support / cache_decrement.active_support イベントを購読し
    • payload[:key] を直接参照しているコード

に影響があります。

  • 挙動の変化

    • namespace オプションを使っていない場合:
      正規化後キーも元キーと同じになるため、実質的な変化はほぼありません。
    • namespace を使っている場合:
      これまで payload[:key]"counter" だったものが、"myapp:counter" のように名前空間付きになります。
    • それ以外にも、normalize_key で行っている正規化(#to_param や encoding 調整など)があれば、その結果が反映されます。
  • 後方互換性の観点

    • 監視基盤やメトリクス集計で、「このイベントの key は必ず非 namespaced な生キー」という前提でロジックを書いていた場合、集計キーが変わる可能性があります。
    • 他ストア(File/MemCache/Redis)と挙動を合わせた変更のため、「複数ストアで共通の購読コード」を書いているプロジェクトでは、むしろ今回の変更で挙動が一貫し、扱いやすくなります。
  • 対応の指針

    • もし「アプリケーションレベルの論理名のキー」と「実際のストアキー(namespace 付き)」を区別してメトリクスを取りたい場合は、今後は以下のような設計を検討するとよいです:
      • イベントから受け取った payload[:key] を「実キー」とみなし、
      • 論理キーは別途 payload に入す(独自インストゥルメンテーション)か、キー生成側でロギングする。

  1. 参考情報 (あれば)
  • 該当 PR: Instrument the normalized key in MemoryStore#increment/#decrement (#58007)
  • 関連する他ストアの挙動:
    • ActiveSupport::Cache::FileStore
    • ActiveSupport::Cache::MemCacheStore
    • ActiveSupport::Cache::RedisCacheStore
      これらはいずれもインストゥルメンテーションで「正規化済みキー」を報告しており、今回の変更で MemoryStore もそれに揃った形です。
  • 関連テスト: activesupport/test/cache/behaviors/cache_instrumentation_behavior.rb
    ここを読むと、ActiveSupport が「計測イベントにどういう情報を載せることを期待しているか」のリファレンスとしても利用できます。

#58008 Clear a stale enqueue_error when perform_all_later re-enqueues a job

マージ日: 2026/7/5 | 作成者: @55728

  1. 概要 (1-2文で)
    perform_all_laterenqueue_all 未実装アダプタ向けに「1件ずつ enqueue」するフォールバック経路を使う際、過去の失敗時にセットされた enqueue_error がクリアされず、状態が矛盾する問題を修正したPRです。これにより、再enqueueに成功したジョブは successfully_enqueued? == true かつ enqueue_error == nil という一貫した状態になります。

  1. 変更内容の詳細

背景

  • Active Job には複数ジョブをまとめてキューに積む perform_all_later があり、アダプタが enqueue_all を実装していない場合は、内部的に各ジョブに対して個別に enqueue を呼ぶフォールバックが行われます。
  • 単一ジョブの enqueue 経路では、enqueue を試行する前に enqueue_error をクリアする仕様になっており、「前回の enqueue 失敗情報を引きずらない」ようになっています。
  • しかし、perform_all_later のフォールバック経路では successfully_enqueued フラグはリセットしていた一方で、enqueue_error をクリアしていませんでした。

問題となっていた挙動

  • あるジョブが何らかの理由で enqueue に失敗し、enqueue_error がセットされた状態になったとします。
  • そのジョブを後で perform_all_later のフォールバック経路(=enqueue_all 非対応アダプタ)経由で再enqueueし、今度は成功した場合でも、古い enqueue_error が残ってしまっていました。
  • この結果、同じジョブインスタンスに対して:
ruby
job.successfully_enqueued? # => true
job.enqueue_error          # => #&lt;SomeError ...> (古いエラーが残っている)

という矛盾した状態が起きていました。

このPRでの修正内容

  • フォールバック経路(各ジョブを個別に enqueue し直すロジック)の中で、再enqueueを試みる前に enqueue_error もクリアするように変更。
  • 単一ジョブの enqueue 経路と同様に、「enqueue を試行する際には過去のエラー情報をリセットする」挙動に揃えています。

擬似的には以下のようなイメージです(実際のコードは簡略化):

ruby
jobs.each do |job|
  job.successfully_enqueued = false
  job.enqueue_error = nil      # ← この行が追加されたイメージ
  queue_adapter.enqueue(job)
end

テストの追加

activejob/test/cases/queuing_test.rb にテストが追加され、以下のようなケースが検証されています:

  • 以前の enqueue 失敗で enqueue_error がセットされているジョブがある。
  • perform_all_later 実行時、アダプタが enqueue_all を実装していないため 1件ずつ enqueue される。
  • enqueue が成功したジョブは:
    • successfully_enqueued?true
    • enqueue_errornil であることを確認。

  1. 影響範囲・注意点
  • 対象となるのは「enqueue_all を実装していない Active Job アダプタ」を使用し、かつ「同じジョブインスタンスを再利用して perform_all_later で再enqueue する」ようなケースです。
  • これまで「ジョブは正常に enqueue されているのに enqueue_error が残っている」状態に依存したコード(例えば、enqueue_error の有無だけで前回の失敗を判定していたりする処理)があれば、挙動が変わります。ただし、本来は不整合な状態だったため、通常は修正として望ましい挙動です。
  • enqueue_all を実装しているアダプタにはこの変更は直接影響しません(そちらは元々 enqueue_all の実装側の責務になります)。

  1. 参考情報 (あれば)
  • 目的は「単一ジョブ enqueue とバッチ enqueue(perform_all_later のフォールバック)の状態管理を対称に保つ」ことです。
  • Active Job でカスタムアダプタや enqueue_all の実装を行う場合も、同様に「成功したジョブには enqueue_error を残さない」という一貫性を持たせるのが望ましい設計になります。

#58009 Account for queue_name_prefix in assert_enqueued_email_with

マージ日: 2026/7/5 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveJob::Base.queue_name_prefix を設定している場合でも、assert_enqueued_email_with が正しくジョブのキュー名を判定できるように修正した PR です。これにより、メール自体は正しく deliver_later されているのにテストだけが失敗する問題が解消されます。

  1. 変更内容の詳細

問題の背景

config.active_job.queue_name_prefix を設定すると、Active Job ではキュー名が以下のように変更されます:

ruby
config.active_job.queue_name_prefix = "prefix"

# 通常 "mailers" になるところが "prefix_mailers" になる
TestMailer.test.deliver_later

しかし、これまでの ActionMailer::TestHelper#assert_enqueued_email_with は、
「メール系ジョブは mailers キューに入るはず」という前提で期待キュー名を決めており、queue_name_prefix などの設定を考慮していませんでした。

そのため、以下のようなコードはジョブ自体は正しく prefix_mailers に enqueue されているにもかかわらず、テストが「mailers に入っていない」と誤判定して失敗していました。

ruby
config.active_job.queue_name_prefix = "prefix"

assert_enqueued_email_with(TestMailer, :test) do
  TestMailer.test.deliver_later
end
# before: 失敗 (期待 "mailers" / 実際 "prefix_mailers")
# after:  成功

具体的な修正点

変更ファイルは 2 つだけです。

  1. actionmailer/lib/action_mailer/test_helper.rb

    • assert_enqueued_email_with 内で「期待するキュー名」を決定する処理を、実際の配送ジョブ(ActionMailer::MailDeliveryJob 等)が enqueue 時にキュー名を決定するロジックと同じ方法で導出するように修正。
    • つまり、queue_name_prefixqueue_name_suffix が設定されていれば、それを含んだ完全なキュー名(例: "prefix_mailers", "prefix_mailers_suffix")を期待値として使うようになりました。
    • コード上は 1 行の差し替えで、直接 "mailers" を見るのではなく、ActiveJob が使うキュー名解決ロジックを経由する形になったと考えられます。
  2. actionmailer/test/test_helper_test.rb

    • この挙動を保証するテストが追加されています (+15 行)。
    • 内容としては、config.active_job.queue_name_prefix を設定した状態で TestMailerdeliver_later を呼び、assert_enqueued_email_with がパスすることを検証するテストです。
    • これにより、今後 queue_name_prefix の仕様変更や assert_enqueued_email_with の改修があっても、プレフィックス付きキュー名への対応が壊れないようにしています。

  1. 影響範囲・注意点
  • 影響を受けるのは「ActionMailer::TestHelper#assert_enqueued_email_with を使っており、かつ config.active_job.queue_name_prefix(または suffix)を使っているプロジェクト」です。

    • これまで、そのようなプロジェクトでは「メールは正常にキューに積まれているのにテストが落ちる」という症状が出ていたはずです。
    • この PR 適用後は、そのようなテストは通るようになります。
  • メール配送自体の挙動 (deliver_later 時の queue や実行タイミング) には変更がなく、「テスト用ヘルパーの期待値ロジックのみ」が修正対象です。そのため、本番環境の挙動変更リスクはほぼありません。

  • もし既にワークアラウンドとして

    • 自前で queue: "prefix_mailers" を指定して検査していたり、
    • assert_enqueued_email_with を使わずに低レベルな assert_enqueued_jobs などで検査していた といったケースがある場合、この修正によりワークアラウンドが不要になる可能性があります。
  • queue_name_suffix も「prefix と同様の仕組みで解決」されるため、この PR によって suffix を用いた構成でも期待どおりに動作するようになります(prefix/suffix を総合した「実際のキュー名」にテストが揃う)。


  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/58009
  • 関連する Rails ガイド:
    • Active Job Basics: Queue configuration
      • config.active_job.queue_adapter
      • config.active_job.queue_name_prefix
      • config.active_job.queue_name_delimiter
      • config.active_job.queue_name_suffix
  • 関連 API:
    • ActionMailer::TestHelper#assert_enqueued_email_with
    • ActiveJob::Base.queue_name_prefix / queue_name_suffix
    • ActionMailer::MailDeliveryJob(メール配送ジョブの実装)

#58010 Support endless and beginless ranges in number_field/range_field

マージ日: 2026/7/5 | 作成者: @55728

  1. 概要 (1-2文で)
    number_field / range_field ヘルパで、in: / within: オプションに無限範囲 (18.. など) や片側無限範囲 (..10 など) を渡したときに RangeError になっていた問題を修正し、適切に min / max 属性へ変換できるようにした PR です。Ruby の end/beginless Range を正式にサポートしつつ、従来の閉区間・排他的区間の挙動は維持しています。

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

変更の主眼

  • 対象ヘルパ:
    • ActionView::Helpers::FormOptionsHelper#number_field
    • ActionView::Helpers::FormOptionsHelper#range_field
  • オプション:
    • in: / within: に渡される Range の扱いを修正

以前は、以下のような範囲を渡すと RangeError が発生していました。

ruby
number_field("order", "quantity", in: 18..) # endless range
number_field("order", "quantity", in: ..10) # beginless range

この PR により、以下のように HTML 属性へマッピングされるようになります。

Endless range (18..)

ruby
number_field("order", "quantity", in: 18..)
# 変更前: RangeError
# 変更後: <input type="number" name="order[quantity]" min="18" />
#         (max 属性は付与されない)

Beginless range (..10)

ruby
number_field("order", "quantity", in: ..10)
# 変更前: RangeError
# 変更後: <input type="number" name="order[quantity]" max="10" />
#         (min 属性は付与されない)

既存の bounded range の挙動はそのまま

  • 通常の閉区間(両端あり、..):

    ruby
    number_field("order", "quantity", in: 1..10)
    # <input ... type="number" min="1" max="10" />
  • 排他的範囲 (...) も従来通り:

    ruby
    number_field("order", "quantity", in: 1...10)
    # => max は「上限 - 1」としてレンダリングされる
    # <input ... type="number" min="1" max="9" />

PR 説明によると、この既存の「exclusive range は max を 1 小さくする」ロジックもそのまま維持されています。

実装変更の概要

  • actionview/lib/action_view/helpers/tags/number_field.rbRange から min/max を決定する部分を、endless / beginless の Range を許容するように 1 行修正。
  • actionview/test/template/form_helper_test.rb にテストケースを追加 (+10 行):
    • endless range を渡したときに min のみ設定されること
    • beginless range を渡したときに max のみ設定されること
    • 例外が発生しないこと

テスト追加により、今後のリグレッションも検知できるようになっています。


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

    • number_field, range_fieldin: / within: オプション付きで利用しているフォームビューが対象。
    • 特に Ruby 2.6+ などで導入された end/beginless range 構文を既に利用しているコードで、以前は例外が発生していたケースが正常に動作するようになります。
    • フロントエンド的には、min / max 属性の有無だけが変わるため、HTML5 バリデーションロジックや、JavaScript 側で min/max の有無に依存している場合に影響し得ます。
  • 仕様面の注意点

    • in: / within: に Range を渡した場合の挙動は以下に整理されます:
      • 両端あり (a..b) → min=a, max=b
      • 排他的 (a...b) → min=a, max=b-1
      • endless (a..) → min=a, max なし
      • beginless (..b) → max=b, min なし
    • Ruby の「通常の Range オブジェクトを受け付ける」という Rails のドキュメント仕様と整合しており、RangeError を投げてしまうこれまでの挙動がよりどころのない状態だったため、それを是正する変更といえます。
    • これにより、サーバーサイドバリデーション (validates :field, inclusion: a..) とフォーム側の表示 (number_field ..., in: a..) を、end/beginless range を含めて一貫して書きやすくなります。
  • 後方互換性

    • これまで RangeError をトリガーする前提で rescue するようなコードを書いていた場合、その挙動は変わる可能性がありますが、通常のアプリケーションではそういう利用はまず想定されないため、実質的には後方互換性を損なわない変更と考えられます。
    • bounded range (特に 1...10 のような排他的 range) の既存挙動には一切変更がない点がポイントです。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58010
  • 関連する Rails ドキュメント:
    • Action View Form Helpers – number_field, range_field:in / :within オプション
  • Ruby の endless / beginless range 仕様:
    • a.. / ..b は Ruby 2.6 以降で導入された Range の構文であり、通常の Range オブジェクト ((18..) === 20 #=> true) として扱われます。

#58006 Return the current value for an unchanged store accessor's _was

マージ日: 2026/7/5 | 作成者: @55728

  1. 概要 (1-2文で)
    store アクセッサに対する <key>_was の挙動が、変更されていない場合にも nil を返してしまう不整合を修正し、ActiveModel::Dirty の契約どおり「未変更なら現在値を返す」ように揃えた PR です。これにより、store カラムを使う場合も通常の属性と同様の Dirty API の挙動が得られます。

  1. 変更内容の詳細

挙動の変更点

修正前(未変更時):

ruby
# 例: jsonb カラム `settings` に対して store_accessor で color を定義
# record.settings = { "color" => "black" }
record.color         # => "black"
record.color_was     # => nil  # ← 本来期待される挙動と異なる

修正後(未変更時):

ruby
record.color         # => "black"
record.color_was     # => "black"  # ← 通常の attribute と同じ挙動

値を変更した後の挙動は従来どおりで、過去の値を返します:

ruby
record.color         # => "black"
record.color = "white"

record.color_was     # => "black"      # 変更前の値
record.color_change  # => ["black", "white"]  # こちらは既に修正済みだった挙動と整合
record.color_changed? # => true

どのように修正されたか(概念レベル)

  • activerecord/lib/active_record/store.rb の store アクセッサ定義部分で、<key>_was が未変更時に nil を返さないように 1 行分のロジックが修正されています。
  • ActiveModel::Dirty#attribute_was と同様、「その属性が変更されていなければ、_was は現在値を返す」ように挙動を合わせています。
  • これまでに _change<key>_change)については未変更時の挙動が既に修正済みだったため、今回の変更で _was も含めて Dirty 系メソッド間で一貫した挙動になります。

テスト

activerecord/test/cases/store_test.rb に 9 行追加されています。

想定されるテスト内容(要約):

  • store アクセッサを持つモデルを用意し、レコードを保存した直後(未変更状態)に <key>_was を呼び出すと現在値を返すことを確認。
  • 値を更新した後は、変更前の値を返すことを確認。
  • _change や Dirty 判定との整合性を担保。

  1. 影響範囲・注意点

影響範囲

対象となるのは:

  • store / store_accessor を使っているモデル
  • かつ、<key>_was を明示的に参照しているコード

に限定されます。

これまで:

  • 「store アクセッサの <key>_was は、未変更時には nil を返す」と仮定したコード(ワークアラウンド)を書いていた場合、その前提が崩れます。

これから:

  • 通常の ActiveRecord 属性と同様に、
    • 未変更時: <key>_was は現在値
    • 変更後: <key>_was は変更前の値 となります。

移行・互換性上の注意点

  • もし次のようなコードを書いていた場合は挙動が変わります:

    ruby
    # 例: nil かどうかで「未設定かどうか」を判定しているケース
    if record.color_was.nil?
      # 以前は「未変更か、元々 nil」かを区別できないが、
      # 実際には「未変更 = nil」「変更済み = 過去値(non-nil)」のように使っていた可能性がある
    end

    このような用途には Dirty API を直接使う方が正確です:

    ruby
    if record.color_changed?
      # 変更された場合の処理
      before, after = record.color_change
    end
  • 「未変更かどうか」を知りたいなら _was ではなく

    • <key>_changed?
    • <key>_change
    • saved_change_to_<key>? / saved_change_to_<key>(永続化後の変更検知) を利用するのが筋となります。
  • nil 自体を「未設定」の意味で使っている store キーの場合、

    • 未変更時に <key>_wasnil を返す挙動は変わりません(値が nil だから)。
    • ただし、以前は「未変更だが nil」と「変更済みだが元値が nil」を _was.nil? では区別できなかった点は変わらず、Dirty API(_changed? 等)で区別する必要があります。

  1. 参考情報 (あれば)
  • 関連 API:
    • ActiveModel::Dirty#attribute_was
    • ActiveModel::Dirty#attribute_change
    • ActiveModel::Dirty#attribute_changed?
  • store / store_accessor の概要:
    • store :settings, accessors: %i[color size]
      • record.color / record.color=settings["color"] を透過的に扱う
  • この PR で _change_was の挙動が揃ったため、store アクセッサも「普通のカラム属性」と同じ感覚で Dirty 情報を扱えるようになっています。

#58011 Honor an explicit :selected option in weekday_select

マージ日: 2026/7/5 | 作成者: @55728

  1. 概要 (1-2文で)
    weekday_select ヘルパに selected: オプションを明示的に渡した場合でも、その値が正しく優先されるように挙動が修正されました。これにより、他の select 系ヘルパ (select, collection_select, grouped_collection_select, date_select) と一貫した動きになります。

  1. 変更内容の詳細

これまでの挙動

weekday_select は、フォームオブジェクト(例: @digest)側に既に曜日の値が入っていると、selected: オプションを無視してオブジェクトの値を選択状態にしていました。

ruby
# モデル側の値
@digest.send_day #=> "Monday"

# ビュー側
f.weekday_select(:send_day, selected: "Friday")

上記のように書いても、実際に選択されるのは "Monday" でした。

修正後の挙動

同じコードを実行した場合、selected: "Friday" が優先され、 "Friday" が選択状態になります。

ruby
# 修正後
f.weekday_select(:send_day, selected: "Friday")
# => セレクトボックス上では "Friday" が選択される

実装面の変更ポイント

  • actionview/lib/action_view/helpers/tags/weekday_select.rb のロジックが 1 行修正され、
    「オブジェクトの属性値」よりも「オプションで明示された :selected」を優先するようになりました。
  • actionview/test/template/form_options_helper_test.rb にテストが追加され、
    • オブジェクトに曜日がセットされている
    • かつ selected: オプションが指定されている
      というケースで、明示的 selected が優先されることが保証されています。

他の select 系ヘルパと同様に、「明示的に指定された selected があればそれを使う」という Rails の一貫したポリシーに合わせた変更です。


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

    • weekday_select を使用しており、かつモデルにデフォルトの曜日が格納されているフォームで selected: オプションも指定している場合、表示される初期選択が変わる可能性があります。
    • これまで「オブジェクトの値が優先される」前提で selected: を“ダミー”的に書いていたコードは、明示指定の方が効くようになるため挙動が変わります。
  • 互換性の観点

    • 他の select 系ヘルパと同じルールになるため、API 全体としては一貫性が向上します。
    • ただし、weekday_select だけを個別仕様として利用していた場合は挙動変更となるため、フォームの初期選択状態の変化に注意してください。
  • テスト

    • 対応するテストが追加されているため、selected: の優先挙動は今後も守られることが期待できます。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/58011
  • 類似の挙動を持つヘルパ:
    • ActionView::Helpers::FormOptionsHelper#select
    • ActionView::Helpers::FormOptionsHelper#collection_select
    • ActionView::Helpers::FormOptionsHelper#grouped_collection_select
    • ActionView::Helpers::DateHelper#date_select

これらと同じく、「オブジェクトの値 < 明示的な selected オプション」という優先順位になった、と理解しておくと良いです。


#58003 Fix belongs_to change tracking for composite foreign keys

マージ日: 2026/7/5 | 作成者: @anxkhn

  1. 概要 (1-2文で)
    belongs_to に複合外部キー(Composite Foreign Key)を使っている場合に、association_changed? / association_previously_changed? が常に false を返してしまうバグを修正したPRです。単一カラムの外部キーには影響せず、複合キーのときだけ正しく変更検知できるようにしています。

  1. 変更内容の詳細

問題点

belongs_to で複合外部キーを使うと、例えば以下のような関連づけになります:

ruby
class Cpk::Order < ApplicationRecord
  self.primary_keys = :shop_id, :id
end

class Cpk::Book < ApplicationRecord
  belongs_to :order,
    class_name: "Cpk::Order",
    foreign_key: [:shop_id, :order_id]
end

このとき Rails 内部では reflection.foreign_key が:

ruby
["shop_id", "order_id"]

という配列になっています。

ところが、BelongsToAssociation#target_changed? および #target_previously_changed? は、この reflection.foreign_key をそのまま「1つの名前」とみなして、スカラ用の dirty メソッドに渡していました:

ruby
attribute_changed?(reflection.foreign_key)            # 実際のイメージ
attribute_previously_changed?(reflection.foreign_key) # 実際のイメージ

スカラの dirty メソッド (attribute_changed?) は引数に対して to_s するので、配列は:

ruby
["shop_id", "order_id"].to_s
# => '["shop_id", "order_id"]'

となってしまい、「そんな属性名は存在しない」ため、常に false になっていました。その結果、belongs_to :order に対して自動生成される:

ruby
order_changed?
order_previously_changed?

などの公開APIが、複合キーのときだけ常に「変化なし」と報告してしまう状態でした。

単一カラムの外部キー ("shop_id" など) では to_s しても "shop_id" のままなので、この問題が表に出ず、これまで見逃されていました。

修正内容

BelongsToAssociation#target_changed?#target_previously_changed? を、「外部キーが配列のときは各カラムを個別にチェックする」ように変更しています。

擬似コードにすると、以下のようなイメージです:

ruby
def target_changed?
  Array(reflection.foreign_key).any? { |fk| owner.attribute_changed?(fk) }
end

def target_previously_changed?
  Array(reflection.foreign_key).any? { |fk| owner.attribute_previously_changed?(fk) }
end

ポイント:

  • Array(reflection.foreign_key) とすることで、
    • 複合キー: ["shop_id", "order_id"]["shop_id", "order_id"]
    • 単一キー: "shop_id"["shop_id"]
      のように正規化されます。
  • .any? で「どれか1つでも変わっていれば関連変更あり」と判断します。
  • これは同じファイル内で既に複合キー対応している他の処理 (replace_keys, foreign_key_present?) と同じパターンで、実装を揃えた形になっています。

ポリモーフィック belongs_to への影響

BelongsToPolymorphicAssociationsuper で上記ロジックを呼んだ上で、foreign_type もOR条件でチェックする実装になっています。

  • 複合キーの belongs_to でポリモーフィックなケースは、super の修正をそのまま継承。
  • ポリモーフィック外部キー自体はスカラなので、今回の修正で挙動が変わることはありません。

テスト

activerecord/test/cases/associations/belongs_to_associations_test.rb に回帰テストが追加されています。

テストのシナリオ(概念的な流れ):

  1. 複合主キーを持つ Cpk::Order を2つ作成(同じ shop_idorder_id だけ違う)。
  2. Cpk::Book を、最初の orderbelongs_to :order で紐づけて保存。
  3. その後、book.order = 別の order に付け替えて、shop_id は同じだが order_id を変更。
  4. ここで:
    • book.order_changed?true になることを確認。
  5. book.save 後に:
    • book.order_previously_changed?true になることを確認。

このテストは main ブランチでは両方 false になって失敗し、この修正を適用するとパスします。


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

    • 対象: belongs_to に複合外部キーを指定している関連。
    • 修正されるAPI:
      • association_changed?(例: order_changed?
      • association_previously_changed?(例: order_previously_changed?
    • 単一カラム外部キーの belongs_to の挙動は変わりません。
    • ポリモーフィック belongs_to の挙動も、実質的には従来通りです。
  • 意図的に変更していない箇所

    • saved_change_to_target? は今回は変更していません。
      • これはカウンタキャッシュや touch コールバック専用の内部APIで、ここだけ先に直すと:
        • after_update で新しい親のカウンタだけインクリメントされる
        • 一方で decrement_counters_before_last_save は古い親側のデクリメントをスキップしたまま
          という不整合が起き、カウンタキャッシュが膨らむ危険があります。
      • 複合キーに対するカウンタキャッシュ周りの扱いは #57608 で別途対応中であり、本PRでは「公開APIの association_changed? / association_previously_changed? のバグ修正」にスコープを限定しています。
  • 互換性面での注意

    • これまで「複合外部キーの belongs_to は常に *_changed?false になる」ことを前提にワークアラウンドを書いていたコードがあれば、今回の修正で挙動が変わる可能性があります。
      • 例: if !order_changed? を「常にここに入る」前提ロジックとして悪用(?)していた場合など。
    • 本来の意図した動作に修正されるものであり、一般的にはバグフィックスとして望ましい変更です。

  1. 参考情報 (あれば)
  • 該当PR: https://github.com/rails/rails/pull/58003
  • 関連する既知課題(複合キーとカウンタキャッシュまわり): https://github.com/rails/rails/pull/57608
  • 実際に影響を確認したい場合:
    • 複合外部キーの belongs_to を持つモデルで xxx_changed? / xxx_previously_changed? を呼び、外部キーの一部だけ変えたときに true になるかをテストすると、挙動差を確認できます。

#57991 Stop HTML-escaping JSON written by Coders::JSON

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveRecord::Coders::JSON が JSON を DB に書き込む際、意図されていた「HTML エスケープ無し (escape: false)」設定が実際には効いておらず、HTML エスケープされてしまっていた不具合を修正する PR です。これにより、JSON 型カラムや ActiveRecord::Type::Json との挙動が統一されます。

  1. 変更内容の詳細

問題点

ActiveRecord::Coders::JSON にはもともと、次のような方針がありました。

  • JSON を DB カラムに書き込むとき、HTML エスケープ(<\u003c など)をスキップするために、デフォルトオプションとして escape: false をマージする。

しかし実装上のバグにより:

  • エンコーダは「マージ済みの @options」ではなく、「呼び出し時に渡された生の options 引数」で初期化されていた。
  • その結果、escape: false が実際のエンコード処理に反映されておらず、HTML エスケープが有効のままだった。

具体例:

ruby
coder = ActiveRecord::Coders::JSON.new
coder.dump({ "k" => "<>&" })
# 修正前の実際の挙動:
# => "{\"k\":\"\\u003c\\u003e\\u0026\"}"

# 期待される挙動(ネイティブ JSON カラムと同じ):
# => "{\"k\":\"<>&\"}"

PR の説明で明示されている通り、修正前は:

  • {"k":"\u003c\u003e\u0026"}

が返されており、これは:

  • ネイティブ JSON カラム
  • ActiveRecord::Type::Json

によるシリアライズ結果と食い違っていました。

修正内容

ActiveRecord::Coders::JSON の内部で:

  • マージ後のオプション(escape: false を含む)を保持している @options
  • 元の引数として渡された options

のうち、エンコーダの初期化に @options を使うように 1 行だけ修正されています。

擬似コードで表すと、だいたい以下の違いです(実際のコードは若干異なりますが、意味としてはこういう修正):

ruby
# 修正前 (イメージ)
def initialize(options = {})
  @options = DEFAULT_OPTIONS.merge(options) # escape: false をマージしている
  @encoder = SomeJSONEncoder.new(options)   # ← 生 options を使っていた
end

# 修正後 (イメージ)
def initialize(options = {})
  @options = DEFAULT_OPTIONS.merge(options)
  @encoder = SomeJSONEncoder.new(@options)  # ← マージ後オプションを使う
end

これにより、ActiveRecord::Coders::JSON を使った場合も、デフォルトで escape: false がきちんと有効になります。

テストの追加

activerecord/test/cases/coders/json_test.rb に 5 行のテストが追加され、以下を確認しています:

  • Coders::JSON.new.dump({ "k" => "<>&" }) の結果が {"k":"<>&"} となること
  • つまり、HTML エスケープされないこと

  1. 影響範囲・注意点

影響範囲

  • 対象: ActiveRecord::Coders::JSON を使って、DB の文字列カラムなどに JSON を保存しているケース。
    • 例: serialize :settings, ActiveRecord::Coders::JSON のような実装
  • 変更内容: デフォルトで HTML エスケープされていた JSON 出力が、今後はエスケープされなくなります。

互換性の観点

  • ネイティブ JSON カラム(json / jsonb)や ActiveRecord::Type::Json の挙動と揃うため、「Rails 内での一貫性」という意味では正しい方向です。
  • 既存データ:
    • 既に DB に保存済みの "k":"\u003c\u003e\u0026" などの値はそのまま残り、新規保存分のみ "k":"<>&" 風になります。
    • 読み込み時はどちらも JSON として問題なく扱えるため、デシリアライズ時の挙動に違いはほぼありません。

注意点

  • HTML への直接出力時のセキュリティ:

    • この変更は「DB に保存する JSON の文字列の中身」がエスケープされなくなる、という変更です。
    • Rails の view レイヤ (<%= %>raw, html_safe など) での XSS 対策とは別物です。
    • JSON をそのまま HTML に埋め込んでいるようなコード(例えば <script>var data = <%= @model.settings.to_json %>;</script> のようなもの)は、元々も適切な HTML エスケープと j ヘルパーなどによる JS エスケープを自前で考慮すべき領域です。この PR によってそこが安全になったり危険になったりするわけではありませんが、「DB に保存される値が変わる」ことは理解しておく必要があります。
  • テストの更新:

    • 自前で Coders::JSON のシリアライズ結果文字列を検証しているテストがある場合、"\u003c" を期待していた部分は "<" になるよう修正が必要です。

  1. 参考情報 (あれば)
  • 関連するコンポーネント:

    • ActiveRecord::Coders::JSON
    • ActiveRecord::Type::Json
    • DB のネイティブ JSON 型(PostgreSQL の json / jsonb など)
  • この PR により、以下の 3 者の JSON シリアライズ結果が揃うことが期待されます:

    1. ネイティブ JSON カラム (json, jsonb)
    2. ActiveRecord::Type::Json
    3. ActiveRecord::Coders::JSON(本 PR の対象)

HTML エスケープに依存していたコードがないかだけ確認しつつ、基本的には「バグ修正 + 挙動の統一」として受け入れてよい変更です。


#57990 Match HTTP auth scheme case-insensitively for Token and Bearer

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    HTTP の Authorization ヘッダにおける Token / Bearer 認証スキームの判定が、大文字小文字を区別しない(case-insensitive)形に修正されました。これにより、bearer abc123TOKEN xyz といった非標準の大文字小文字でも正しくトークンが認識されるようになります。

  1. 変更内容の詳細

2-1. 変更の背景

  • RFC 9110 §11.1 によると、Authorization ヘッダ内の auth-schemeBasic, Bearer, Token などのスキーム名)は 大文字小文字を区別しない と規定されています。
  • 現実のクライアントやプロキシは Bearer だけでなく bearer, BEARER などさまざまな表記で送ってくることがあります。
  • しかし Rails では:
    • コントローラ側の Token 認証 (ActionController::HttpAuthentication::Token)
    • ActionDispatch::Request#bearer_token
      がいずれも 大文字小文字を区別する 実装になっており、
      • bearer abc123 → 無効として扱われ 401 を返す
      • request.bearer_tokenBearer 以外の表記では nil を返す
        といった問題がありました。
  • 一方で、同じファイルにある Basic 認証はすでに case-insensitive で比較しており、振る舞いが不統一でした。

本 PR はこの不整合を解消し、RFC 準拠かつ Basic と同様に、Token / Bearer も case-insensitive に扱うようにしています。


2-2. コントローラ側 (Token 認証) の変更

対象:

  • actionpack/lib/action_controller/metal/http_authentication.rb

ActionController::HttpAuthentication::Token 内の TOKEN_REGEX が修正されています。
元から存在する TOKEN_REGEXAuthorization ヘッダをパースして Token スキームかどうかを判定するための正規表現ですが、スキーム部分を 大文字小文字を区別せずにマッチ するように変わりました。

イメージとしては、以下のような変更です(実際のコードからの擬似例):

ruby
# Before(概念的なイメージ)
TOKEN_REGEX = /^Token\s+(.+)$/  # "Token" と完全一致でマッチ

# After(概念的なイメージ)
TOKEN_REGEX = /^Token\s+(.+)$/i # "Token" を大文字小文字を区別せずにマッチ

これにより、以下のようなヘッダがすべて同様にトークンとして認識されます:

http
Authorization: Token abc123
Authorization: token abc123
Authorization: TOKEN abc123
Authorization: ToKeN abc123

コントローラ側で authenticate_or_request_with_http_token などを使っている場合、これら全てが有効なトークンとして扱われます。


2-3. リクエスト側 (Request#bearer_token) の変更

対象:

  • actionpack/lib/action_dispatch/http/request.rb

ActionDispatch::Request#bearer_token は、Authorization ヘッダから Bearer トークンを取り出すヘルパーメソッドです。

元は Bearer というスキーム名を 大文字小文字を区別して マッチさせていたため、以下のようなケースは全て nil を返していました:

http
Authorization: bearer abc123
Authorization: BEARER abc123
Authorization: Bearer abc123
# → Bearer 以外はトークンが取れない

PR ではここも case-insensitive に変更され、Basic と同様の挙動になります。
擬似的なイメージ:

ruby
# Before(概念的なイメージ)
authorization&.match(/^Bearer\s+(.+)$/)

# After(概念的なイメージ)
authorization&.match(/^Bearer\s+(.+)$/i)

これにより、以下のようなコードがすべて期待通りに動作します:

ruby
# 例: Rack ミドルウェアやコントローラ内
token = request.bearer_token
# Authorization: bearer abc123
# Authorization: BEARER abc123
# Authorization: Bearer abc123
# いずれも token == "abc123" になる

2-4. テストの追加

対象:

  • actionpack/test/controller/http_token_authentication_test.rb (+9)
  • actionpack/test/dispatch/request_test.rb (+5)

上記の動作が保証されるよう、以下の観点のテストが追加されています:

  • Token 認証について、TOKEN, token, ToKeN などさまざまな表記の Authorization ヘッダを送っても正しく認証されること。
  • Request#bearer_token について、bearer, BEARER など任意の大文字小文字の組み合わせでもトークンを抽出できること。

これにより、今後のリファクタリング等で再び case-sensitive に戻ってしまうリグレッションを防止します。


  1. 影響範囲・注意点

3-1. 影響範囲

  • HTTP トークン認証 (ActionController::HttpAuthentication::Token) を利用しているアプリケーション
  • ActionDispatch::Request#bearer_token を直接、またはライブラリを通じて利用しているアプリケーション
  • 上記を利用しているライブラリ・ミドルウェア(OAuth2 実装、API 認証機構など)

基本的には後方互換な改善 であり、従来通り Token xxx / Bearer xxx もそのまま動作します。そのうえで、今まで 401 になっていた bearer xxx / TOKEN xxx などが通るようになります。

3-2. 注意点・想定外の影響になり得るケース

  • もしアプリケーション側で「Bearer という表記だけを許したい」「token は拒否したい」といった、あえて非 RFC 準拠な制約 を設けていた場合、その前提は崩れます。
    ただし、auth-scheme を case-sensitive に扱うこと自体が RFC 9110 に反するため、そのような前提がある場合は設計の見直しが推奨されます。
  • 同一クライアント・同一トークンで、これまでたまたま Bearer になっていたケースでは何も変わらず、混在環境(クライアントの一部が bearer を送るなど)でのみ挙動が変わる可能性があります。

  1. 参考情報 (あれば)
  • RFC 9110 §11.1: Authentication Scheme
    • auth-scheme は case-insensitive と定義されている。
  • 関連する Rails コード:
    • ActionController::HttpAuthentication::Token
      actionpack/lib/action_controller/metal/http_authentication.rb
    • ActionDispatch::Request#bearer_token
      actionpack/lib/action_dispatch/http/request.rb
  • 類似の既存挙動:
    • Basic 認証はすでにスキーム名を case-insensitive に扱っており、本 PR によって Token / Bearer もそれに揃えられた形になります。

#57993 Fix incorrect number_to_percentage default precision in docs [ci skip]

マージ日: 2026/7/4 | 作成者: @anxkhn

  1. 概要 (1-2文で)
    number_to_percentage ヘルパーのドキュメントに書かれていた :precision オプションのデフォルト値が誤っており、実際の挙動(デフォルト3桁)に合わせて「2」から「3」に修正した PR です。コードや挙動の変更はなく、ドキュメントのみの修正です。

  1. 変更内容の詳細

何が問題だったか

number_to_percentage の RDoc には、:precision オプションについて次のように書かれていました。

ruby
# [+:precision+]
#   The level of precision, or +nil+ to preserve +number+'s precision.
#   Defaults to 2.
#
#   number_to_percentage(12.3456789, precision: 4) # => "12.3457%"

しかし実際には、number_to_percentage(100) のデフォルト出力は "100.000%" であり、デフォルト精度は「3」です。

  • ベースのフォーマット設定: DEFAULTS[:format][:precision] = 3
  • :currency だけが precision を 2 にオーバーライド
  • :percentage:delimiter:format しかオーバーライドしていないため、precision=3 を継承

そのため、「Defaults to 2.」という説明が実際の挙動および直前のサンプル (number_to_percentage(100) # => "100.000%") やテストと矛盾していました。

具体的な修正内容

activesupport/lib/active_support/number_helper.rb の1行だけが変更されています。

diff
 # [+:precision+]
 #   The level of precision, or +nil+ to preserve +number+'s precision.
-#   Defaults to 2.
+#   Defaults to 3.
 #
 #   number_to_percentage(12.3456789, precision: 4) # => "12.3457%"

number_to_currency 側の「Defaults to 2.」は正しいためそのままです。


  1. 影響範囲・注意点
  • 実行時の挙動は一切変わりません
    もともと number_to_percentage のデフォルト precision は 3 であり、この PR はそれを説明するドキュメントを正しくしただけです。

  • 影響があるのは「ドキュメントを読んで実装していた人の認識」だけです。

    • これまで「precision のデフォルトは 2 だ」と思い込んでいた場合、実際の出力とのギャップに気づいていなかった可能性があります。
    • ただしコード側は以前から 3 桁出力で動いており、PR による挙動変更はありません。
  • もしアプリ側で「パーセンテージは必ず小数2桁にしたい」という要件がある場合は、明示的に precision を指定する必要があります。

    ruby
    number_to_percentage(12.3456)               # => "12.346%" (デフォルト precision: 3)
    number_to_percentage(12.3456, precision: 2) # => "12.35%"
  • 他のヘルパーとの一貫性:

    • number_to_rounded, number_to_human_size, number_to_human など、同じくベースの precision を継承するヘルパーはすでに「Defaults to 3.」と記載されており、number_to_percentage だけがドキュメント上ズレていました。本 PR により説明の一貫性が取れています。

  1. 参考情報 (あれば)

デフォルト precision が 3 であることを示すソース:

  • activesupport/lib/active_support/number_helper/number_converter.rb

    • DEFAULTS[:format][:precision] = 3
    • DEFAULTS[:currency][:format][:precision] = 2
    • DEFAULTS[:percentage]:delimiter:format だけ上書きし、precision は継承
  • activesupport/lib/active_support/locale/en.yml

    • number.format.precision: 3
    • number.currency.format.precision: 2
    • number.percentage.format は precision 未指定 → 3 を継承
  • テストコード

    • activesupport/test/number_helper_test.rb
    • activesupport/test/core_ext/numeric_ext_test.rb
      いずれも number_to_percentage(100) == "100.000%" を前提にしており、precision=3 を前提にしていることが分かります。

#57986 Fix Guides Index flyout menu overflowing its panel

マージ日: 2026/7/4 | 作成者: @iuhoay

  1. 概要 (1-2文で)
    Rails Guides のデスクトップ向け「Guides Index」フライアウトメニューで、3カラムレイアウトが横幅を超えて右にはみ出していた問題を、CSS のレイアウト指定を見直すことで解消した PR です。widthmargin の組み合わせで 100% を超えていたものを、column-gapcalc() を用いてちょうど 100% に収まるようにしています。

  1. 変更内容の詳細

対象: guides/assets/stylesrc/_main.scss

問題点:

  • .guides-section-container 内の .guides-section が以下のように指定されていました:
    • width: 33%;
    • margin: 0 2em 0.5em 0;
  • 3カラム × 33% = 99% に加え、各カラム右側に 2em マージンがあり、その分が合計で 100% を超えてしまい、右端のカラムがコンテナの外にはみ出していました。
  • はみ出した結果、右端カラム内の長いリンクテキスト(例: "Installing Rails Core Development Dependencies")がパネル外へ溢れて見えてしまう状態になっていました。

修正内容:

diff
.guides-section-container {
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
+ column-gap: 2em;
  max-height: 60em;
  width: 100%;

  .guides-section {
    flex: auto;
-   margin: 0 2em 0.5em 0;
+   margin: 0 0 0.5em 0;
    text-align: left;
-   width: 33%;
+   width: calc((100% - 4em) / 3);
  }
}

ポイント:

  • カラム間のスペースの扱いを、各要素の margin-right ではなく、コンテナ側の column-gap に移行:
    • 以前: .guides-sectionmargin-right: 2em; 相当の指定
    • 以後: .guides-section-containercolumn-gap: 2em;
  • .guides-section の幅を固定の 33% から、カラム間ギャップを考慮した calc((100% - 4em) / 3) へ変更:
    • 3カラムとその間に 2つのギャップ(2em × 2 = 4em)がある前提で、
      • コンテナの 100% 幅からギャップ合計 4em を引いた残りの幅を 3 等分。
    • これにより「カラムの幅 × 3 + ギャップ × 2 = 100%」となり、パネル幅を超えなくなります。
  • .guides-sectionmargin は下方向 (margin-bottom: 0.5em) のみ残し、左右の余白は 0 に変更。

結果として、3カラムとその間の余白がコンテナ幅にぴったり収まり、右端カラムや長いリンクテキストがパネル外へ溢れなくなります。


  1. 影響範囲・注意点
  • 影響範囲:
    • Rails Guides サイトのデスクトップビューにおける「Guides Index」フライアウトメニューのレイアウトに限定されます。
    • それ以外のページレイアウトや、Rails 本体の機能・挙動には影響しません(スタイルシートのみの変更)。
  • 技術的な注意点:
    • column-gap は比較的新しめの CSS プロパティですが、デスクトップ主要ブラウザでは概ねサポートされています。Rails Guides のターゲットブラウザを考えると問題になる可能性は低いと考えられます。
    • calc((100% - 4em) / 3) によって、コンテナ幅とフォントサイズ(em に依存)が変わるとカラム幅も自動的に再計算されるため、レイアウトはある程度柔軟に対応できますが、「3カラム+ギャップ 2つ」であることが前提になっています。将来的にカラム数を変える場合は、この式も合わせてメンテナンスする必要があります。
    • flex-direction: column; flex-wrap: wrap; という少し特殊な使い方をしているため、DOM 構造の変更やコンテナ高さの変更(max-height: 60em;)とあわせて動作を確認するのが望ましいです。

  1. 参考情報 (あれば)

#57997 Use Range#cover? for this_week?/this_month?/this_year?

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    Date/Time関連のヘルパー this_week? / this_month? / this_year? が、内部判定に Range#include? ではなく Range#cover? を使うように変更され、性能が大きく改善されました。振る舞いは変えずに、呼び出しごとの無駄な日付走査を避けるリファクタリングです。

  1. 変更内容の詳細

何をしているか

this_week?, this_month?, this_year? は、引数の日時オブジェクトが「今週 / 今月 / 今年」に属するかどうかを判定するメソッドです。
もともとはだいたい次のようなイメージで実装されていました:

ruby
def this_week?
  all_week.include?(self)
end

def this_month?
  all_month.include?(self)
end

def this_year?
  all_year.include?(self)
end

ここで all_week, all_month, all_yearDate の Range を返します。
Range#include?#succ で一つずつ値を進めながら探索する実装になるため、Date の場合:

  • this_week? → 約 7 回 succ(7日分)
  • this_month? → 最大 31 回 succ
  • this_year? → 約 365 回 succ

といった線形探索が毎回発生していました。

今回の変更では、これらを Range#cover? に差し替えています:

ruby
def this_week?
  all_week.cover?(self)
end

def this_month?
  all_month.cover?(self)
end

def this_year?
  all_year.cover?(self)
end

Range#cover? は「範囲の開始と終了の比較」だけで判定するため、内部で要素を1つずつたどることがなく、10〜100倍程度高速になることが PR 説明で言及されています。

背景

  • 先行 PR #57963 で this_quarter? はすでに cover? に変えられており、今回の PR はそのフォローアップとして、兄弟関数 (this_week? / this_month? / this_year?) を同じ実装方針に揃えたものです。
  • テストの変更はなく、既存の境界条件テストもそのまま通ることが確認されています。

  1. 影響範囲・注意点
  • 挙動面

    • Range#include?Range#cover? は、Date/Time のように比較が定義されたオブジェクトに対して、典型的な連続範囲を扱う場合は同じ結果を返します。
    • 既存テストが境界値(開始日・終了日を含むかどうか等)をカバーしており、そのままパスしているため、APIの論理的挙動は変わらないと見なして問題ありません。
  • パフォーマンス面

    • this_year? のように長い期間の Range を頻繁に判定しているコードでは、顕著な性能改善が見込めます。
    • 特に大量レコードに対して this_*? を呼ぶ場面(バリデーション、集計、バッチ処理など)で恩恵が大きくなります。
  • 後方互換性

    • 公開 API のメソッドシグネチャや返り値の型に変更はなく、内部実装の最適化のみです。
    • Range#cover? / Range#include? の差異に依存した特殊なケース(例えば、離散的でない独自オブジェクトを Range に入れている等)をこのメソッド群が利用しているわけではないので、互換性上のリスクは非常に低いと考えられます。

  1. 参考情報 (あれば)

#57963 Add this_quarter? to Date/Time

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    Date, Time, DateTime, ActiveSupport::TimeWithZone に対して「今期(今四半期)かどうか」を判定する this_quarter? メソッドが追加されました。これにより、week/month/year に揃う形で quarter にも this_*? プレディケートが用意され、日付処理の一貫性が向上しています。

  1. 変更内容の詳細

追加された機能

対象クラス:

  • Date
  • Time
  • DateTime
  • ActiveSupport::TimeWithZone

に、以下のプレディケートメソッドが追加されました:

ruby
date_or_time.this_quarter? # => true / false

判定ロジックの概要
this_month?this_year? と同様に、「現在の四半期」を基準にして判定します。

  • 基準となる“今”は Date.current(= Time.zone を考慮した現在日付)
  • その Date.current が属する四半期を「現在の四半期」とみなし、
  • 受け取った日付/時刻オブジェクトがその四半期の範囲内にあるかどうかを判定

概念的には以下と同等の判定です:

ruby
def this_quarter?
  Date.current.all_quarter.cover?(to_date)
end

※実装は activesupport/lib/active_support/core_ext/date_and_time/calculations.rb に追加されており、既存の this_month? / this_year? と同じスタイルで this_quarter? が定義されています。

使用例

ruby
# 前提: Date.current が 2000-02-15 (Q1: 1〜3月) の場合

Date.new(2000, 3, 31).this_quarter?   # => true  (同じ四半期 Q1)
Date.new(2000, 4, 1).this_quarter?    # => false (次の四半期 Q2)

Time.utc(2000, 1, 1).this_quarter?    # => true  (Q1 に含まれる)
DateTime.civil(2000, 4, 1).this_quarter? # => false (Q2)

既存機能との関係

Rails には既に以下のような四半期関連のメソッドがありました:

  • beginning_of_quarter
  • end_of_quarter
  • all_quarter
  • next_quarter
  • prev_quarter

一方で、週・月・年にはある this_*? 系が四半期にだけ存在していませんでした:

weekmonthquarteryear
beginning_of_*
end_of_*
all_*
next_*
prev_*
this_*?

今回の PR で、この this_quarter? により埋まり、API の一貫性が取れるようになりました。

テストとドキュメント

変更ファイル:

  • activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
    • this_quarter? 実装追加 (+5行)
  • activesupport/test/core_ext/date_time_ext_test.rb
    • test_this_quarter 追加 (+9行)
    • this_month? / this_year? と同じテストパターンで、四半期境界をカバー
  • activesupport/CHANGELOG.md
    • 新機能として this_quarter? 追加を明記 (+12行)

テストでは、四半期の開始日・終了日・直前直後の日付などを用い、境界条件を確認しています。


  1. 影響範囲・注意点

影響範囲

  • ActiveSupport の Date/Time 拡張を利用しているアプリケーション全般で、新たに this_quarter? が利用可能になります。
  • 既存メソッドの挙動変更や破壊的変更は含まれていません(追加のみ)。

実務上の利点

これまでよく見られたコード:

ruby
Date.current.all_quarter.cover?(record.created_at.to_date)
# または
Date.current.all_quarter.include?(record.created_at.to_date)

を、より簡潔に:

ruby
record.created_at.this_quarter?

と書けるようになります。
四半期ベースの:

  • 請求期間・課金期間の判定
  • OKR・目標管理の「今期」判定
  • 財務・レポート系でのフィルタリング

などが読みやすく実装できます。

Time.zone / タイムゾーンへの注意

this_quarter?Date.current を基準に判定するため、config.time_zoneTime.zone の設定に依存します。this_month? / this_year? と同じ挙動です。

例:

  • アプリの Time.zone"Tokyo" の場合: 「今期」は東京時間での日付を基準とする
  • DB が UTC でも、TimeWithZone で評価される場合は Time.zone を考慮

タイムゾーンを切り替えるテストや、マルチタイムゾーン対応アプリでは、Time.use_zone を用いた検証を行うと安全です。


  1. 参考情報 (あれば)

#57987 Make add_index(if_not_exists: true) reversible

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    add_index / remove_index のマイグレーションを逆変換(rollback 時の自動生成)する際に、if_not_exists / if_exists オプションが正しく対応付けられるようになりました。これにより、既存/非存在なインデックスに対して安全にロールバックでき、これまで発生していた不要な例外が解消されます。

  1. 変更内容の詳細

何が問題だったか

Rails の Migration には「コマンドインバータ(ActiveRecord::Migration::CommandRecorder)」があり、change メソッド内で書いた操作から rollback 用の逆操作を自動的に生成します。

しかし、これまで以下のような問題がありました:

  • remove_index(..., if_exists: true) を反転すると add_index が生成されるが、
    その際に if_exists: true失われる(:if_exists は add 側には存在しないオプションのため、単に削除されるだけだった)。

    • 結果: rollback 時に、すでにインデックスが存在するケースで例外が発生する。
  • add_index(..., if_not_exists: true) を反転すると remove_index が生成されるが、
    if_not_exists: true がそのまま remove_index に渡されてしまい、remove_indexif_not_exists理解しない ため、事実上オプションが無視される。

    • 結果: rollback 時に、インデックスが存在しないケースで例外が発生する。

つまり、「forward 方向では存在チェック付きで安全に動くように書いたマイグレーション」が、rollback 時にはその安全性が失われていた、という状態です。

今回の修正内容

CommandRecorderinvert_add_index / invert_remove_index において、if_not_exists / if_exists相互に変換するようになりました。

概念的には以下のような変換が行われます:

ruby
# 変更前(概念図)
invert_remove_index(columns, if_exists: true)
# => [:add_index, [columns], {}]  # :if_exists が落ちる

invert_add_index(columns, if_not_exists: true)
# => [:remove_index, [columns], { if_not_exists: true }]  # remove_index では無視される


# 変更後(概念図)
invert_remove_index(columns, if_exists: true)
# => [:add_index, [columns], { if_not_exists: true }]

invert_add_index(columns, if_not_exists: true)
# => [:remove_index, [columns], { if_exists: true }]

これにより:

  • 「存在していたら削除」を rollback すると「存在していなかったら作成しない(存在しなくてもエラーにしない)」という安全な対になる関係になる
  • 「なければ作らない」を rollback すると「すでになくてもエラーにしない削除」になる

サンプルコードイメージ

ruby
class AddIndexToUsersEmail < ActiveRecord::Migration[7.2]
  def change
    # インデックスがなければ作成
    add_index :users, :email, if_not_exists: true
  end
end

この change を rollback すると、従来は概ねこう解釈されていました:

ruby
remove_index :users, :email
# if_not_exists は無視されるので、インデックスがなければエラー

今回の修正後は、内部的には以下のような remove_index が生成されます:

ruby
remove_index :users, :email, if_exists: true
# インデックスがなくてもエラーにならない

逆に:

ruby
class RemoveIndexFromUsersEmail < ActiveRecord::Migration[7.2]
  def change
    remove_index :users, :email, if_exists: true
  end
end

を rollback すると、今までは(概念的に):

ruby
add_index :users, :email
# すでにインデックスがあるとエラー

となっていましたが、これからは:

ruby
add_index :users, :email, if_not_exists: true
# すでにインデックスがあってもエラーにならない

となります。

実装面の変更

  • activerecord/lib/active_record/migration/command_recorder.rb
    • add_index / remove_index の invert ロジックに、オプションハッシュの変換処理を追加
    • :if_not_exists:if_exists:if_exists:if_not_exists のマッピングを行うようにした
  • activerecord/test/cases/migration/command_recorder_test.rb
    • 上記の変換が正しく行われているかどうかを検証するテストを追加

この挙動は前にマージされた invert_add_foreign_key / invert_remove_foreign_key(PR #57972)や check_constraint 周りの invert 処理と揃えた形です。


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

    • change メソッド内で add_index ... if_not_exists: true または remove_index ... if_exists: true を利用しているマイグレーションの rollback 挙動が、より安全かつ期待に沿ったものになります。
    • 既存のマイグレーションファイルに対しても、Rails 本体を上げれば新しい CommandRecorder のロジックで rollback が行われます。
  • 期待されるメリット

    • すでに適用済みのマイグレーションを本番・ステージングで rollback する際に、「インデックスがもうある / もうない」状況でも例外が出づらくなります。
    • 並列デプロイや手動メンテなどでインデックス状態がずれがちな環境でも、マイグレーションの安全性が増します。
  • 注意点

    • 動作が「壊れる」方向ではなく、「これまで例外が出ていたケースで出なくなる」変更なので、互換性上の問題はほぼありません。
    • ただし、あえて rollback 時にインデックスの存在 / 非存在を厳密に検出したい、という特殊なケースでは挙動が変わる(例外が起きにくくなる)点は認識しておくとよいです。
    • Schema の整合性チェック等で、インデックスの存在を前提にした独自ツールを組んでいる場合は、「if_not_exists / if_exists 付きのマイグレーションでも rollback が失敗しにくくなった」ことを踏まえて運用を見直す余地があります。

  1. 参考情報 (あれば)
  • この PR:
    • Make add_index(if_not_exists: true) reversible (#57987)
  • 関連 PR:
    • foreign key inverter を同様に修正した PR: #57972
  • 該当クラス/メソッド:
    • ActiveRecord::Migration::CommandRecorder#invert_add_index
    • ActiveRecord::Migration::CommandRecorder#invert_remove_index

#57992 Require fileutils in Cache::FileStore

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveSupport::Cache::FileStore 内で FileUtils を利用しているにもかかわらず "fileutils" を require していなかった問題を修正し、スタンドアロンな ActiveSupport 利用時に発生していた NameError を解消する PR です。Rails フルスタックでは暗黙に読み込まれていた依存を、明示的に宣言するようになりました。

  1. 変更内容の詳細

変更は 1 行のみで、ActiveSupport::Cache::FileStore のファイル先頭付近に require "fileutils" が追加されました。

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

ruby
# activesupport/lib/active_support/cache/file_store.rb

# 旧:
# module ActiveSupport
#   module Cache
#     class FileStore < Store
#       ...
#       FileUtils.makedirs(...)
#       FileUtils.rm_r(...)
#       ...

# 新: fileutils を明示的に読み込む
require "fileutils"

module ActiveSupport
  module Cache
    class FileStore < Store
      # 内部で FileUtils.makedirs, FileUtils.rm_r を使用
    end
  end
end

背景として、このクラスはキャッシュディレクトリの作成や削除に FileUtils.makedirsFileUtils.rm_r を使っていますが、これまでは同じプロセス内のどこか別の場所で "fileutils" が require されている前提になっていました。Rails 全体をロードしている場合は、他のコンポーネントが fileutils を読み込むため問題は表面化していませんでしたが、ActiveSupport 単体で ActiveSupport::Cache::FileStore を使うと次のようなエラーが起きていました。

ruby
require "active_support"
require "active_support/cache"

store = ActiveSupport::Cache::FileStore.new("/tmp/cache")
store.write("foo", "bar")
# => NameError: uninitialized constant ActiveSupport::Cache::FileStore::FileUtils

この PR により、file_store.rb 自身が require "fileutils" するため、上記のような NameError は発生しなくなります。


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

    • ActiveSupport::Cache::FileStore を利用する全てのコードに対し、挙動は「例外が出なくなる」以外は実質的な変更はありません。
    • 既に Rails フルスタックを利用しているアプリケーションでは、もともとどこかで fileutils がロードされていたため、挙動上の違いはほぼありません。
    • ActiveSupport を単体で利用しているツール・スクリプト・ライブラリにおいて、FileStore を使った際の NameError (uninitialized constant FileUtils) が解消されます。
  • 注意点

    • require "fileutils" は Ruby 標準ライブラリの読み込みであり、互換性上の問題を引き起こす可能性は低いです。
    • もしアプリ側で FileUtils 定数を独自にモンキーパッチしていた場合でも、この PR は単に標準ライブラリを require するだけなので、通常は影響ありません(既にロード済みなら再 require も安全)。
    • 「他のファイルに依存して暗黙に定数をロードする」状態が解消され、今後は file_store.rb 単体で完結した依存関係になります。

  1. 参考情報 (あれば)
  • 対象クラスのドキュメント(キャッシュストアの概要など)
    • ActiveSupport::Cache::FileStore (現行 Rails ガイド / RDoc)
  • 類似のパターンとして、標準ライブラリに依存しているクラス・モジュールは、そのファイル自身で require するのが推奨されます。今回の修正は、そのベストプラクティスに沿ったものといえます。

#57984 Quote the index name in MySQL enable_index/disable_index

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    MySQL / MariaDB 用アダプタの enable_index / disable_index が、インデックス名をクォートせずに SQL を生成していた問題を修正し、予約語や特殊文字を含むインデックス名でも正常に動作するようにしました。rename_index など他のインデックス操作と一貫して、インデックス名に quote_column_name を適用するようになっています。

  1. 変更内容の詳細

何が問題だったか

enable_index / disable_index は、内部的に MySQL / MariaDB の以下のような SQL を発行します:

sql
ALTER TABLE `testings` ALTER INDEX order VISIBLE
ALTER TABLE `testings` ALTER INDEX order INVISIBLE

ここで order のように、

  • SQL の予約語
  • バッククォートすべき特殊文字を含む名前

をインデックス名に使っていると、インデックス名がクォートされていないため 構文エラー になります。

どのように修正されたか

abstract_mysql_adapter.rbenable_index / disable_index 実装で、インデックス名を quote_column_name で包むように変更されました。

概念的には以下のような変更です:

ruby
# 変更前 (イメージ)
def enable_index(table_name, index_name)
  execute("ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{index_name} VISIBLE")
end

# 変更後 (イメージ)
def enable_index(table_name, index_name)
  execute("ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{quote_column_name(index_name)} VISIBLE")
end

def disable_index(table_name, index_name)
  execute("ALTER TABLE #{quote_table_name(table_name)} ALTER INDEX #{quote_column_name(index_name)} INVISIBLE")
end

その結果、実際に発行される SQL は:

sql
-- Before
ALTER TABLE `testings` ALTER INDEX order VISIBLE

-- After
ALTER TABLE `testings` ALTER INDEX `order` VISIBLE

となり、インデックス名がバッククォートで囲まれます。

quote_column_name は、MySQL アダプタではカラム名・インデックス名など識別子を `name` のようにクォートするためのメソッドで、既に rename_index など他のインデックス操作では使われていました。今回の修正で、インデックス関連操作のクォート戦略が統一されました。

テストの追加

activerecord/test/cases/migration/index_test.rb にテストが追加され、

  • 予約語や特殊文字を含むインデックス名に対して
  • enable_index / disable_index がエラーなく動作し、正しい SQL が生成される

ことが検証されています。

(具体的なテストコードは PR 情報にはありませんが、order のようなケースをカバーしていると考えられます。)


  1. 影響範囲・注意点
  • 対象:
    • Active Record を使った MySQL / MariaDB アプリケーション
    • enable_index / disable_index を直接またはマイグレーション経由で利用している場合
  • 影響:
    • これまで「インデックス名が予約語等だと enable_index / disable_index で落ちる」ケースが解消されます。
    • 既存の正常なケースについては、単にインデックス名がクォートされるだけなので、後方互換性への影響はほぼありません(MySQL / MariaDB は識別子をクォートしても同じオブジェクトを指すため)。
  • 注意点:
    • もしアプリ側で生 SQL を組み立てて ALTER TABLE ... ALTER INDEX ... を発行している場合は、この修正は関係ありません。その場合も同様に、インデックス名はバッククォートでクォートするのが安全です。
    • マイグレーション上で index: { name: "order" } のような名前を指定しても、今回の修正により enable_index / disable_index 時のエラーは防げるようになりますが、アプリケーションコードや他の SQL からの参照でも予約語を使う場合は同様の注意が必要です。

  1. 参考情報 (あれば)
  • MySQL マニュアル:
    • 「Schema Object Names」(識別子とクォート)
    • 「Reserved Words」(予約語一覧)
  • Rails ソース周辺:
    • ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#quote_column_name
    • rename_index 実装 (同アダプタ内、インデックス名クォートの既存の扱いの参考になる)

#57988 Raise on ignored if_exists option in change_table's remove_timestamps

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    change_table ブロック内で t.remove_timestampsif_exists: / if_not_exists: オプションを渡した場合、これまでは黙って無視されていましたが、他のメソッドと同様に ArgumentError を投げて呼び出し側に誤用を知らせるように変更されました。これにより、サポートされないオプションの利用を早期に検知できるようになります。

  1. 変更内容の詳細

何が変わったか

  • 対象メソッド: ActiveRecord::ConnectionAdapters::Table#remove_timestamps
  • 変更点:
    • remove_timestamps が内部で raise_on_if_exist_options を呼ぶようになりました。
    • これにより、if_exists: / if_not_exists: オプションが渡された場合は例外を発生させます。

従来の挙動:

ruby
change_table :users do |t|
  t.remove_timestamps if_exists: true  # ← オプションは黙って無視される
end
# 例外は発生しないが、if_exists: は一切効いていない

今回の変更後:

ruby
change_table :users do |t|
  t.remove_timestamps if_exists: true
end
# => ArgumentError: options :if_exists, :if_not_exists are not supported ...

これは、既に以下のようなメソッドで行われている挙動と揃えたものです。

  • t.timestamps(if_exists: true)ArgumentError
  • t.remove(:column, if_exists: true)ArgumentError
  • t.remove_index(:column, if_exists: true)ArgumentError

つまり、change_table ブロック内の「if_exists / if_not_exists をサポートしないヘルパー」は、全て一貫して例外を投げるようになりました。

テスト追加

activerecord/test/cases/migration/change_table_test.rb にテストが追加され、例えば以下のようなケースがカバーされていると考えられます。

  • t.remove_timestamps(if_exists: true)ArgumentError を投げること
  • t.remove_timestamps(if_not_exists: true)ArgumentError を投げること

(実際のテストコードは数行で、この挙動を保証する内容になっています。)


  1. 影響範囲・注意点

影響を受けるケース

  • 既存のマイグレーションで、change_table ブロック内に以下のようなコードがある場合:
ruby
change_table :users do |t|
  t.remove_timestamps if_exists: true
  # または
  t.remove_timestamps if_not_exists: true
end

これらは これまでは正常終了していたが、オプションは効いていなかった のに対し、今後は ArgumentError によってマイグレーションが失敗します。

なぜ例外にするのか

  • サポートされていないオプションを黙って無視すると、開発者が「存在しない場合はスキップされるはず」と誤解したままになり、スキーマ不整合や本番環境でのバグの原因になります。
  • 他の change_table ヘルパー (timestamps, remove, remove_index など) はすでに同じポリシーで挙動しており、remove_timestamps だけ例外だったため、API 一貫性の観点からも修正されています。

対応方法

  • もし上記のようなコードが既に存在する場合は:

    1. if_exists: / if_not_exists: を削除する (最も単純):

      ruby
      change_table :users do |t|
        t.remove_timestamps
      end
    2. 「存在チェック込みで安全に消したい」という意図であれば、自前で条件分岐する:

      ruby
      def up
        if column_exists?(:users, :created_at) && column_exists?(:users, :updated_at)
          change_table :users do |t|
            t.remove_timestamps
          end
        end
      end
      
      def down
        change_table :users do |t|
          t.timestamps
        end
      end
  • なお、change_table ではなく remove_timestamps を直接呼び出す remove_timestamps(:users) のような DSL を使っている場合、この PR は change_table ブロック内の t.remove_timestamps に対する変更なので、その部分には直接は影響しません (この PR の差分範囲に依存しますが、説明文からは change_table ブロック内のヘルパーが対象と読み取れます)。


  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57988
  • 関連する API:
    • change_table ブロック内ヘルパー:
      • t.timestamps
      • t.remove
      • t.remove_index
      • t.remove_timestamps
    • オプション検証メソッド: raise_on_if_exist_options (ActiveRecord 内部ユーティリティ)

この変更は「サイレントに失敗するマイグレーション」を減らすための一貫性改善であり、バグ修正に近い性質の変更と考えられます。


#57989 Fix color_field emitting invalid color values for strings containing a hex substring

マージ日: 2026/7/4 | 作成者: @55728

  1. 概要 (1-2文で)
    color_field ヘルパーの色値バリデーションが不正確だったため、<input type="color"> にブラウザが解釈できない値がそのまま出力されていた問題を修正しています。正規表現を「文字列全体が #rrggbb 形式かどうか」を見るように変更し、それ以外は #000000 にフォールバックする挙動を統一しました。

  1. 変更内容の詳細

何が問題だったか

従来の color_field 内の validate_color_string は、値の中に「6桁の16進数」がどこかに含まれていれば OK と判定していました。

そのため、例えば以下のような値が「有効」とみなされていました。

  • "Not a color #123456 at all"
  • "#12345678"(8桁、実際には CSS の #rrggbbaa のような形式)
  • その他、文字列の一部に #abcdef のようなパターンが含まれるもの

これらはそのまま <input type="color">value 属性に出力されるため、ブラウザ側では無効なカラー値として扱われ、期待どおり動作しない(色が選択されていない状態になるなど)問題がありました。

具体的な修正内容

validate_color_string の正規表現を、

  • 「6桁の 16 進数の連続部分をどこかから拾ってくる」
    から
  • 「文字列全体が #rrggbb 形式と完全一致しているかを確認する」

ように変更しています。

Ruby 的には、例えばこんなイメージです(実際のコード断片のイメージ):

ruby
def validate_color_string(value)
  # 変更前(イメージ)
  # value[/#[0-9a-fA-F]{6}/] || "#000000"

  # 変更後(イメージ)
  value =~ /\A#[0-9a-fA-F]{6}\z/ ? value : "#000000"
end

/\A...\z/ でアンカーしているため、以下のような挙動になります:

  • "Not a color #123456 at all" → 不一致 → "#000000" にフォールバック
  • "#12345678" → 不一致 → "#000000" にフォールバック
  • "#1234TR" → 不一致 → "#000000"(従来からのフォールバック挙動を踏襲)
  • "#123456" → 一致 → そのまま "#123456" を利用

テストの追加

actionview/test/template/form_helper_test.rb に 6 行ほどテストが追加され、上記のようなケースで:

  • 部分一致する文字列や
  • 8桁のカラーや
  • その他の malformed な値

が渡されたときに、value="#000000" が出力されることが確認されています。


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

    • ActionView::Helpers::FormTagHelper#color_field_tag
    • ActionView::Helpers::FormHelper#color_field
      など、Rails 標準の color_field / color_field_tag を通じて <input type="color"> を生成している部分すべてに影響します。
  • 挙動の変化

    • これまでは「文字列の中に有効な #rrggbb パターンが一部でも含まれていれば、それをそのまま value に出力する」挙動でした。
    • 今後は「文字列全体が #rrggbb 形式である場合だけ受け入れ、それ以外は必ず #000000 にフォールバック」になります。
    • ブラウザ上の見た目として、今まで一見「謎の初期値」になっていたケースが、明示的に黒 (#000000) で表示されるようになります。
  • 後方互換性の観点

    • もしアプリケーション側で、意図的に "Some text #ff0000" のような値を color_field に渡し、ブラウザが無効値として扱うことに依存していた場合、その挙動は変わります(常に #000000 になります)。
    • 一般的には、HTML の仕様に沿ったより厳格なバリデーションとなるため、バグ修正としての互換性の破壊は最小です。
    • #rrggbbaa(8桁)のような色表現を将来的に扱いたい場合は、color_field ではなくカスタムの input / JS を利用する必要があります(HTML <input type="color"> 自体が現状 #rrggbb しか受けないため)。
  • 注意点

    • サーバー側から color_field に渡す値は、# + 16進6桁の文字列(/\A#[0-9a-fA-F]{6}\z/)に正規化してから渡すようにしておくと、今回の変更に依存せず安全です。
    • 不正な値をそのまま投げても自動で #000000 になるため、UI 的に「入力値がおかしい」ことがユーザーに分からない可能性があります。必要に応じてモデルバリデーションやクライアント側バリデーションで補完してください。

  1. 参考情報 (あれば)
  • HTML 仕様上、<input type="color">value#rrggbb 形式(# + 16 進 6 桁)以外は無効値として扱われます。
  • Rails 側でのフォールバックポリシー:
    • 有効な #rrggbb 形式 → そのまま出力
    • それ以外 → #000000(黒)に統一
      という仕様が、この修正でより一貫した形になりました。

#57985 Update PostgreSQL database.yml sample in Command Line guide [ci skip]

マージ日: 2026/7/4 | 作成者: @fuentesjr

  1. 概要 (1-2文で)
    Command Line ガイド内の PostgreSQL 用 database.yml サンプルを、現行の rails new ... --database=postgresql が生成する内容に合わせて更新した PRです。合わせて、非推奨になりつつある pool キーの例を、新しい max_connections キーの例に置き換えています。

  1. 変更内容の詳細

対象ファイル: guides/source/command_line.md の「Configure a Different Database」セクション内の PostgreSQL サンプル config/database.yml

主な変更は2点です。

(1) pg_config のパスの更新

Homebrew でインストールした pg gem に対して pg_config の場所を指定するコメントを、Intel Mac 前提のパスから Apple Silicon 前提のパスに変更しています。

diff
-#   gem install pg -- --with-pg-config=/usr/local/bin/pg_config
+#   gem install pg -- --with-pg-config=/opt/homebrew/bin/pg_config

Rails のジェネレータテンプレート(railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt)側が、すでに /opt/homebrew に切り替わっていたため、ガイドのサンプルもそれに追随させた形です。

(2) pool から max_connections への置き換え

database.yml 内の接続数関連のキー例を、旧来の pool から max_connections に変更しています。

diff
-  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 3 } %>
+  max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

ポイント:

  • キー名が poolmax_connections に変更
    • pool は、今後 max_connections / min_connections に置き換わっていく方向で、ドキュメント上も新しいキーを教えるように修正されています。
  • デフォルト値も 35 に変更
    • これはジェネレータテンプレートに合わせたもので、実際に rails new app --database=postgresql した結果と一致させています。

この変更により、ガイドに載っている YAML サンプルと、最新の Rails が自動生成する config/database.yml の内容が揃いました。


  1. 影響範囲・注意点
  • 実装コードへの影響はなく、ドキュメントのみの変更です。
  • ただし、これからガイドを読んで PostgreSQL 設定を行う開発者は、以下の点に注意が必要です:
    • 新規:
      • 新規アプリを Rails ジェネレータで作る場合、pool: ではなく max_connections: を使うのが推奨される形になります。
      • コネクションプールサイズを調整したい場合は max_connections / min_connections を前提に考えるとよいです。
    • 既存アプリ:
      • 既存アプリで pool: を使っていても、この PR 自体が挙動を変えるわけではありません。
      • ただし、今後のガイドや設定例は max_connections ベースになるため、タイミングを見て移行を検討すると、将来の非推奨・削除対応がしやすくなります。
  • Apple Silicon Mac(/opt/homebrew)以外の環境では、pg_config のパスは当然異なり得ます:
    • Intel Mac(Homebrew): /usr/local/bin/pg_config
    • Linux (例): /usr/bin/pg_config など
      → コメントはあくまで例示なので、自分の環境に合わせてパスは調整する必要があります。

  1. 参考情報 (あれば)

#57921 Expand Action Mailer delivery_method configuration docs [ci skip]

マージ日: 2026/7/4 | 作成者: @davidstosik

  1. 概要 (1-2文で)
    Action Mailer の config.action_mailer.delivery_method のドキュメントを拡充し、循環参照的で分かりにくかった記述を、実際に使えるレベルの説明に整理した PR です。あわせて smtp_settings / sendmail_settings / file_settings との依存関係が分かるように、記述の順番も入れ替えています。

  1. 変更内容の詳細

※コード変更ではなく、guides/source/configuring.md(Rails Guides の設定ガイド)内の Action Mailer セクションの文章修正です。
実際の差分は概ね以下のような内容を含むと考えてよいです(擬似的な例を含みます):

2.1 delivery_method の説明を具体化

以前のドキュメントでは、delivery_method の説明が他の箇所を参照するだけで、実際に何をどう設定できるのかが分かりにくく、参照が行ったり来たりする「循環」状態でした。
この PR では、以下のような情報が一か所にまとまるように整理されています:

  • delivery_method に指定できる主なシンボル例:
    • :smtp
    • :sendmail
    • :file
    • :test
    • (Action Mailer アダプタを追加していれば)独自の delivery method クラス
  • それぞれが「どのような形でメールを送る/保存する」のかという概要
    • :smtp → SMTP サーバ経由で送信
    • :sendmailsendmail コマンド経由で送信
    • :file → 実際には送信せず、メール内容をファイルに書き出す
    • :testActionMailer::Base.deliveries 配列に溜めるだけ(テスト用)

たとえば、設定例として以下のようなコードがドキュメント内で示されているはずです:

ruby
# config/environments/production.rb
Rails.application.configure do
  config.action_mailer.delivery_method = :smtp

  config.action_mailer.smtp_settings = {
    address: "smtp.example.com",
    port: 587,
    user_name: ENV["SMTP_USERNAME"],
    password:  ENV["SMTP_PASSWORD"],
    authentication: :login,
    enable_starttls_auto: true
  }
end

2.2 設定項目の順序を依存関係に沿って並べ替え

元のガイドでは、smtp_settings / sendmail_settings / file_settings が先に出てきて、その後に delivery_method が説明されていた、あるいは相互に行き来する形だったため、読み始めの人には依存関係が分かりにくくなっていました。

この PR では:

  1. まず config.action_mailer.delivery_method で「どの配信方法を使うか」を説明
  2. その後に、選択した配信方法ごとの設定 (smtp_settings, sendmail_settings, file_settings など) を説明

という順番に変えています。
これにより、「delivery_method:smtp を選んだ場合に、次に smtp_settings をこう書く」という流れが自然に理解できるようになります。

2.3 delivery_method と *_settings の関係を明記

config.action_mailer.smtp_settings などが「どの delivery method を選んだときに有効になるか」が明示されるように説明が補足されています。

例:

  • config.action_mailer.smtp_settingsdelivery_method = :smtp のときに意味を持つ
  • config.action_mailer.sendmail_settingsdelivery_method = :sendmail のときに使用される
  • config.action_mailer.file_settingsdelivery_method = :file 向け

この関係が文章としてはっきり書かれたことで、「設定を書いたのに効かない」という混乱を防げるようになっています。


  1. 影響範囲・注意点
  • 影響範囲はドキュメント(Rails Guides)のみであり、Action Mailer の動作や API には変更はありません。
  • アプリケーション側のコードを書き換える必要はありませんが、今後 delivery_method を設定する際に、
    • どの method を選ぶべきか
    • どの *_settings が効くか といった点が、今回のガイド更新を読むことで理解しやすくなります。
  • 既存プロジェクトで「メールが飛ばない」「設定が効いていない」などのトラブルがある場合、まずこの更新後のガイドを確認すると原因把握に役立ちます。

  1. 参考情報 (あれば)
  • 対応している Issue: #57876
    delivery_method のドキュメントが循環参照的で分かりにくいという報告への対応。
  • 具体的な差分は Rails 本体リポジトリの guides/source/configuring.md の変更箇所で確認できます。
  • Action Mailer の基本的な設定全体は Rails Guides の「Configuring Rails Applications」内の Action Mailer セクションにまとまっています。

#57972 Make remove_foreign_key(if_exists: true) reversible

マージ日: 2026/7/3 | 作成者: @55728

  1. 概要 (1-2文で)
    Rails のマイグレーションで remove_foreign_key if_exists: true を実行した際、その逆操作(ロールバック)として生成される add_foreign_key が誤って if_exists: true を含んでしまう問題を修正した PR です。これにより、ロールバック時に if_exists が正しく if_not_exists に変換され、操作がより安全かつ意図通りに動作するようになります。

  1. 変更内容の詳細

何が問題だったか

ActiveRecord::Migration::CommandRecorder は、マイグレーションのコマンドからロールバック用の逆コマンドを自動生成します。
従来は次のような挙動になっていました:

ruby
# forward
remove_foreign_key :orders, :users, if_exists: true

# generated inverse (before this PR)
add_foreign_key :orders, :users, if_exists: true

しかし add_foreign_keyif_exists: オプションをサポートしていません。
そのため:

  • 生成されたロールバックコードが不正なオプションを含む
  • ロールバックの「冪等性」(同じロールバックを何度実行しても問題にならないこと)が崩れる

という問題がありました。

修正内容

この PR では、invert_remove_foreign_key の処理を修正し、if_exists: true をロールバック側では if_not_exists: true に変換するようにしました。

要するに:

ruby
# forward
remove_foreign_key :orders, :users, if_exists: true

# generated inverse (after this PR)
add_foreign_key :orders, :users, if_not_exists: true

となります。

これにより:

  • すでに外部キーが存在しない場合でも remove_foreign_key が安全に実行できる (if_exists: true)
  • ロールバック時に外部キーが既に存在していても add_foreign_key 側で if_not_exists: true により二重作成を避けられる

という、前後両方向での冪等性が担保されます。

実装箇所

  • activerecord/lib/active_record/migration/command_recorder.rb
    • invert_remove_foreign_key(もしくはそれに相当するロジック)に 1 行追加し、オプション変換ロジックに if_existsif_not_exists のマッピングを追加
  • activerecord/test/cases/migration/command_recorder_test.rb
    • 上記変換が正しく行われることを確認するテストを 5 行追加

実装自体はごく小さい変更ですが、挙動の一貫性と安全性を高める修正です。


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

    • remove_foreign_key ... if_exists: true をマイグレーションで使用しているプロジェクト
    • 特に rails db:rollback / rails db:migrate:down などで自動生成される逆マイグレーションを信頼しているケース
  • 期待される挙動:

    • 既存マイグレーションの「前方」実行 (db:migrate) の挙動は変わりません。
    • 新しい Rails バージョンでロールバックすると、逆操作が add_foreign_key ... if_not_exists: true として扱われ、外部キーがすでに存在している環境でもエラーになりにくくなります。
  • 注意点:

    • add_foreign_keyif_not_exists: をサポートしていることが前提です。古い Rails から途中バージョンを飛ばしてアップグレードする場合は、if_not_exists 対応が入っているか(おおむね 7.1 以降)を確認してください。
    • もし、自前で CommandRecorder を拡張している場合は、remove_foreign_key / add_foreign_key まわりのオプション変換ロジックと競合しないか軽く確認すると安全です。

  1. 参考情報 (あれば)
  • 類似の変換ロジック:
    • invert_add_foreign_keyif_not_existsif_exists に変換しているコミット: 8d5c5bd2f5
    • invert_remove_check_constraint でも同様に if_existsif_not_exists の変換を行っており、それと整合する変更です。
  • 該当 PR:

#57970 Make ActiveSupport::ProxyLogger#silence suppress logs

マージ日: 2026/7/3 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveSupport::ProxyLogger#silence が期待どおりログを抑制するように修正された PR です。silence ブロック内で、指定した一時的なログレベル未満のメッセージが内部ロガーへ転送されないようになりました。

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

背景となる問題

ActiveSupport::ProxyLoggerLoggerSilence を include しているものの、これまで silence を呼んでも実際には何も抑制されていませんでした。

ruby
proxy = ActiveSupport::ProxyLogger.new(ActiveSupport::Logger.new($stdout))

proxy.silence do
  proxy.info("noise")
end

# 期待される動作(ActiveSupport::Logger と同じ):
#   何も出力されない
# 実際の動作(修正前):
#   "noise" が出力されていた

Rails 本体では、マイグレーション処理などの内部で logger.silence を使って冗長なログを抑制しており、
「標準 Logger インターフェースをほぼサポートする」ことをドキュメントで謳っている ProxyLoggersilence を無視してしまうのは契約違反になる、というのが問題意識です。

この PR の主な修正点

  1. ProxyLogger#silence が、ブロック内でのログレベルを一時的に引き上げて実際に抑制するようになった。

    • LoggerSilence の期待する挙動に合わせ、ブロック内では一時的な severity(例: Logger::ERROR)より低いログを出さない。
    • これにより ActiveSupport::Logger と同じ挙動になります。
  2. 抑制は「その ProxyLogger インスタンスのみに限定」されるようになった。

    • 同じ「内部ロガー(ActiveSupport::Logger など)」を共有している複数の ProxyLogger がある場合、 片方で silence しても、もう片方には影響しないように実装されています。
    • これは ActiveSupport::Logger#silence の挙動と整合的です(ロガー単位ではなく、プロキシごとにスコープされる)。
  3. テストの追加

    • activesupport/test/proxy_logger_test.rb に、次のような観点のテストが追加されています:
      • silence ブロック内で、閾値より低い severity のログが出力されないこと。
      • 同じ underlying logger を共有する別の ProxyLogger が、片方の silence の影響を受けないこと。

※ 実際のコードは 4 行追加・1 行削除と小さい変更ですが、要点は「ProxyLogger 経由でログ出力される前に、自身のサイレンス状態を考慮してフィルタする」ようにしたことです。


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

    • ActiveSupport::ProxyLogger を使い、かつ proxy_logger.silence { ... } しているコードに影響します。
    • これまで「silence を呼んでも実際には抑制されていなかった」挙動が修正されるため、ログ出力量が減る 可能性があります。
    • ただし、ProxyLogger 自体がまだリリース前のクラスであり、公開バージョンではこの挙動に依存しているアプリは基本的に存在しない想定です。
  • 注意点

    • 同じ underlying logger を共有する他の ProxyLogger に対しては、silence は効きません。
      例えば以下のケースで proxy1 を silence しても、proxy2 は通常どおりログを出します:

      ruby
      base_logger = ActiveSupport::Logger.new($stdout)
      
      proxy1 = ActiveSupport::ProxyLogger.new(base_logger)
      proxy2 = ActiveSupport::ProxyLogger.new(base_logger)
      
      proxy1.silence do
        proxy1.info("hidden")  # → 抑制される
        proxy2.info("visible") # → 出力される
      end

      複数のプロキシ間で一括してサイレンスしたい場合は、基底ロガー側で制御するか、別途ラッピングが必要です。

    • ログレベルに依存する処理(例: debug ログだけをフックするミドルウェアなど)を ProxyLogger 経由で行っている場合、silence ブロック内ではその処理も呼ばれなくなる可能性があります。


  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/57970
  • 関連クラス:
    • ActiveSupport::ProxyLogger
    • ActiveSupport::Logger
    • ActiveSupport::LoggerSilence / LoggerSilence
  • logger.silence は Rails において、マイグレーションや内部処理の一部で冗長なログを抑制する用途で使用されており、本 PR により ProxyLogger でも同じ期待値で利用できるようになります。

#57888 Make autosave association callbacks Ractor-safe

マージ日: 2026/7/3 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    Active Record の autosave な関連(autosave: true な association)で生成されるコールバックメソッドを、Ractor(並行実行環境)でも安全に動作するように実装を変更した PR です。define_method + ブロックで作っていたメソッドを class_eval + シンボル呼び出しに変えることで、非共有 Proc をキャプチャしないようにしています。

  1. 変更内容の詳細

背景・問題点

  • autosave な関連(例: has_many :comments, autosave: true)では、内部的に例えば以下のようなメソッドが自動生成されます:
    • autosave_associated_records_for_comments
  • これらは Active Record 内部の define_non_cyclic_method というヘルパで定義されており、以前はだいたい次のような形で実装されていました(イメージ):
ruby
define_non_cyclic_method(:comments) do |owner|
  owner.autosave_associated_records_for_comments
end
  • 実際には define_method とブロックを使ってメソッドが定義されており、そのブロックは Ruby の Proc オブジェクトとしてクラスにぶら下がります。
  • Ractor(Ruby 3+ の並行実行モデル)では、「共有不可能なオブジェクト(unshareable)」を他の Ractor にそのまま渡せません。ブロックから生成される Proc は通常共有不可能オブジェクトのため、それをキャプチャしたメソッドを非メイン Ractor から呼び出そうとすると制約に引っかかります。
  • その結果、非メイン Ractor 内で autosave 関連のコールバックが呼び出されるケースで問題が起きうるため、Ractor-safe な形に書き換えた、というのが PR のモチベーションです。

具体的な変更点

PR 説明によると:

define_non_cyclic_method implementation to use class_eval and symbols instead of define_method and blocks.

つまり:

  • 変更前: define_method + ブロック(Proc をキャプチャ)
  • 変更後: class_eval + メソッド名(シンボル)経由のディスパッチ

のような形になります。

中でやっていることのイメージを、かなり単純化したサンプルで表現すると:

変更前(イメージ)

ruby
def define_non_cyclic_method(name, &callback)
  define_method("autosave_associated_records_for_#{name}") do
    callback.call(self)
  end
end

ここで &callbackProc であり、define_method のブロックとしてクラスにぶら下がります。この Proc が Ractor 的には共有できないオブジェクトになりうる。

変更後(イメージ)

ruby
def define_non_cyclic_method(name, method_name)
  class_eval <<~RUBY, __FILE__, __LINE__ + 1
    def autosave_associated_records_for_#{name}
      #{method_name} # あるいは send(:#{method_name}) 等
    end
  RUBY
end
  • 文字列を class_eval してメソッドを定義するため、メソッド本体は単に既存のメソッド名を呼び出すだけになります。
  • ここでは共有不可能な Proc をクラスにキャプチャしないため、Ractor 間でも問題になりにくくなります。

実際のコードはもう少し複雑ですが、「ブロックをキャプチャして定義する」から「ソースコード文字列で定義し、実際の処理は別メソッド(シンボル)に委譲する」という方向に変わった、と理解しておくと良いです。

パフォーマンス

付属のベンチマークでは、autosave_associated_records_for_comments を大量に呼び出したときの性能を比較しています:

  • before: 約 4,491,233 i/s
  • after: 約 4,411,090 i/s

差は誤差範囲内("same-ish")とされており、パフォーマンス退行はほぼ無いとみなされています。


  1. 影響範囲・注意点

影響範囲

  • Active Record の autosave 関連コールバックの生成メカニズム(define_non_cyclic_method)が変更されています。
  • 対象となるのは主に:
    • has_many :foo, autosave: true
    • has_one :bar, autosave: true
    • その他 autosave オプション付きの関連に付随する内部的なコールバックメソッド
  • 表向きの API(autosave: true の指定方法や、関連オブジェクトの保存時挙動)は変わりません。

注意点 / 開発者視点でのチェックポイント

  • Ractor を使っていないアプリに対する互換性
    • 既存挙動を壊すような変更は意図されておらず、ベンチマーク上も挙動自体は変わらない前提です。
    • ただし、メソッド定義の仕方が define_method から class_eval に変わるため、メタプログラミングや method_source 系のツールでクラス定義を解析している場合に、挙動が微妙に変わる可能性はあります(メソッドの source_location など)。
  • Ractor を利用している / 利用予定のアプリ
    • この変更により、autosave な関連を持つモデルをサブ Ractor から扱ったときの安全性が向上します。
    • ただし Ractor そのものは他にも多数の制約があるため、「これで Active Record が完全に Ractor-safe になった」という保証ではなく、「autosave association callbacks に関する一つの問題を解消した」に留まると考えるべきです。
  • テストについて
    • チェックリストでは「Tests are added or updated」が未チェックになっています。
      • つまり、この PR 単体では新規テストやテスト更新が入っていない可能性があります。
      • 既存テストで十分にカバーされている前提か、あるいは別途 Ractor 関連のテストが追加されるフェーズがあるかもしれません。
    • Ractor 環境下の挙動についてはまだテストカバレッジが薄い可能性があるため、Ractor を本格利用する場合は手元での実験・検証を推奨します。

  1. 参考情報 (あれば)
  • Ractor と shareable / unshareable オブジェクトの制約については、Ruby 本体のドキュメントが参考になります:
  • Rails の autosave 機能自体の仕様:
    • autosave: true の関連は、親オブジェクトの save 時に関連先も自動的に save / destroy される機能で、バリデーションやトランザクションと組み合わせて複雑なオブジェクトグラフの整合性を保つ用途でよく利用されます。
  • 本 PR の趣旨:
    • 「挙動を変えずに実装を Ractor フレンドリーにする」リファクタリングの一種であり、今後の Ruby 並行実行サポートに向けた地ならしと言えます。

#57927 Make Active Record scope methods Ractor-shareable

マージ日: 2026/7/3 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    Active Record の scope が Ractor(並列実行機構)上で利用できるように、スコープ定義で使われる Proc を「Ractor-shareable」な形に変更した PR です。これにより、Ractor 内から AR モデルのスコープを安全に呼び出せるようになります。

  1. 変更内容の詳細

何をしたか

  • ActiveRecord::Scoping::Named 内の scope 定義処理を変更し、スコープ定義に使う Proc を Ractor で共有可能なオブジェクト(Ractor-shareable)として扱うようにした。
  • それに合わせてテスト (named_scoping_test.rb) を追加し、Ractor 上でもスコープが動作することを確認している。

Ruby 3 以降の Ractor では、スレッド安全性を保つために「共有できるオブジェクト」と「共有できないオブジェクト」が区別されており、通常の Proc やクロージャはしばしば「共有不可」とみなされます。この PR は、Active Record の scope が内部的に利用する Proc を、Ractor をまたいで共有できる形に組み立て直しています。

想定されるコードレベルの振る舞い

普段のスコープ定義:

rb
class Post < ApplicationRecord
  scope :published, -> { where(published: true) }
end

この PR 以前:

  • 上記の -> { ... } が作る Proc は「Ractor-shareable でない」ケースがあり、
    Ractor 内から Post.published を呼ぶとエラーになったり、利用に制限があった。

この PR 後:

  • scope :published, -> { ... } で内部的に生成される Proc が、Ractor で共有可能な形になるように処理される。
  • たとえば、以下のようなコードが動くことをテストしているはずです(イメージ):
rb
r = Ractor.new do
  # Ractor 内から Active Record のスコープを呼び出す
  Post.published.to_a
end

r.take

テストファイル (named_scoping_test.rb) では、

  • スコープが Ractor 内で問題なく呼び出せること
  • スコープチェーンや既存の機能との互換性
    といった点を確認するケースが追加されています。

  1. 影響範囲・注意点

影響範囲

  • 対象: ActiveRecord::Base.scope で定義された全てのスコープ
  • 利用場面:
    • Ractor を使った並列処理の中で Active Record のスコープを呼ぶ場合
    • Ractor をまたいでモデルクラス自体を共有するような構成

既存コードのインターフェース(scope :name, -> { ... } の書き方や返り値など)は変わりません。
スコープに関係するのは主に「内部で使われる Proc の性質」なので、通常のアプリケーションで Ractor を使っていなければ、挙動は従来とほぼ同じです。

注意点

  • Ractor 非対応なオブジェクトをキャプチャするスコープ
    スコープ定義内で、Ractor 非共有オブジェクト(ミューテーブルなオブジェクト、スレッドローカルな状態、IO など)をキャプチャしている場合、それが Ractor-shareable な Proc として扱えるかどうかは Ruby 本体/Active Record 側の実装に依存します。
    原則として:

    • スコープには「Ractor 間で共有できる情報」だけを使う
    • 外部の状態(特にミューテーブルなオブジェクト)を閉じ込めない
      という形にしておくと安全です。
  • CHANGELOG 未更新
    CHANGELOG はまだ更新されていませんが、挙動としては「新機能(Ractor 対応強化)」に近い変更です。将来的にドキュメントや変更履歴に追記される可能性があります。

  • Ruby / Rails のバージョン依存
    この PR は Ractor を前提としているため、Ruby 3 以降が前提です。Rails 側がサポートする Ruby バージョンとの組み合わせに依存するので、利用する際は公式のサポートマトリクスを確認してください。


  1. 参考情報 (あれば)

#57961 Add ActiveSupport::Ractors.try_make_shareable

マージ日: 2026/7/3 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    ActiveSupport::Ractors.try_make_shareable という新しいメソッドが追加され、任意のオブジェクトを「Ractor 共有可能(shareable)」にしようと試みる共通ユーティリティが ActiveSupport に入りました。unshareable_proc_action の設定に応じて、共有不可な Proc などにぶつかったときの挙動(例外・警告・無視)を切り替えられます。

  1. 変更内容の詳細

追加された API

activesupport/lib/active_support/ractors.rb に以下のようなメソッドが追加されています(概念的なイメージ):

ruby
module ActiveSupport
  module Ractors
    class << self
      # すでに存在するもの (イメージ)
      # def try_shareable_proc(proc, unshareable_proc_action: ...)
      #   ...
      # end

      # 今回追加されたもの
      def try_make_shareable(object, unshareable_proc_action: Rails.application.config.active_support.unshareable_proc_action)
        case unshareable_proc_action
        when :raise
          ::Ractor.make_shareable(object) # Ractor::IsolationError がそのまま飛ぶ
        when :warn
          begin
            ::Ractor.make_shareable(object)
          rescue Ractor::IsolationError => error
            ActiveSupport::Deprecation.warn(<<~MSG)
              Ractor isolation error while trying to make object shareable: #{error.message}
            MSG
            object # エラーは握りつぶして元のオブジェクトを返す/処理を続行
          end
        when nil
          begin
            ::Ractor.make_shareable(object)
          rescue Ractor::IsolationError
            object # 完全に無視(ログ・警告も出さない)
          end
        else
          # 想定外の設定値が来た場合のフォールバック(多くは :raise と等価)
          ::Ractor.make_shareable(object)
        end
      end
    end
  end
end

※上記は PR 説明とテストから読み取れる挙動イメージです。実際のコードとはメソッド名や細部が多少異なる可能性があります。

Rails にはすでに「Proc 専用」の try_shareable_proc が存在しており、この PR は「任意オブジェクト版」の try_make_shareable を追加したものです。#57852 など他の PR/機能から、この共通メソッドを呼び出す前提になっています。

unshareable_proc_action による挙動の違い

unshareable_proc_action は ActiveSupport の既存設定で、Proc が Ractor 的に共有できない場合どう扱うかを決めるフラグです。この PR では**「オブジェクト全般を Ractor 共有化しようとしたときに発生する isolation error」に対しても、その設定を流用する**ようになっています。

  • :raise(デフォルト想定)

    • Ractor.make_shareable(object) 実行時に Ractor::IsolationError が起これば、そのまま例外として呼び出し元に飛ぶ。
    • Ractor 安全性が最も厳密。
  • :warn

    • Ractor::IsolationError 発生時に、ActiveSupport の警告(Deprecation.warn など)としてログを出し、処理自体は継続。
    • 実際にはオブジェクトは共有可能にならない(/なれなかった)可能性があるが、アプリは止めずに挙動を観測できる。
  • nil

    • Ractor::IsolationError を完全に握りつぶし、何も出さない。
    • Ractor 共有化に失敗しても無視して続行したい場合のため。

テスト

activesupport/test/ractors_test.rb に 50 行超のテストが追加され、主に以下をカバーしていると考えられます:

  • try_make_shareable:raise, :warn, nil の各設定値で期待通りの挙動をすること
  • 共有可能なオブジェクト(例: freeze 済みの単純な Hash/Array など)が問題なく共有可能になること
  • 共有不可能なオブジェクト(Proc を含むオブジェクト)が、設定に応じて例外・警告・無視として扱われること

  1. 影響範囲・注意点
  • Ractor を利用している / 利用予定の Rails アプリ・ライブラリ

    • Ractor.make_shareable を直接呼ぶ代わりに、ActiveSupport::Ractors.try_make_shareable を使うことで、環境設定に応じてエラー処理を一元化できます。
    • Ractor 対応のコードを書くとき、例外をどう扱うかを都度実装する必要が減ります。
  • unshareable_proc_action 設定の意味合いが広がる

    • これまでは「Proc を共有可能にするときの挙動」を制御する設定でしたが、この PR によって「任意オブジェクトを Ractor 共有可能にしようとするときの挙動」にも影響します。
    • 既に unshareable_proc_action をカスタマイズしているアプリでは、オブジェクト共有化まわりの挙動にもそのポリシーが適用される点を意識する必要があります。
  • 例外を握りつぶす設定に注意

    • :warnnil を使うと、「本来は Ractor 共有されている前提でコードを書いたが、実際には共有化されていない」という状態になりうるため、マルチ Ractor 実行時のデータ競合や設計ミスに気付きにくくなります。
    • 本番運用ではまず :raise で問題の有無を洗い出し、問題箇所を修正した上で必要に応じて :warn/nil を検討するのが安全です。

  1. 参考情報 (あれば)

#57976 Make ActiveSupport::EventReport::LogSubscriber ractor safe

マージ日: 2026/7/3 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    ActiveSupport::EventReport::LogSubscriberevent_log_level API が Ractor セーフになるように、内部で使っている log_levels の扱いを「コピー&追記&freeze」に変更した PRです。共有ハッシュをそのまま書き換えないことで、Ractor 間でのスレッドセーフ/Ractor セーフな利用ができるようになっています。

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

※実際の差分は2行だけですが、説明のために典型的な変更前後イメージを示します(正確なメソッド名や変数名は若干異なる可能性があります)。

変更のポイント

  • log_levels(イベント名 => ログレベル のマッピング)を破壊的に更新しないようにした。
  • 新しいイベントログレベルを追加するときに
    1. 既存のハッシュを dup
    2. その dup したハッシュに新しいキー/値を追加
    3. そのハッシュを freeze
    4. それを元の参照に再代入
      というフローに変更。

このパターンは「イミュータブルなハッシュをバージョンアップしていく」やり方で、Ractor から見ても「freeze 済みのオブジェクト」を共有するだけになるため安全です。

イメージコード

変更前(イメージ):

ruby
def self.event_log_level(event_name, level)
  log_levels[event_name.to_sym] = level
end

変更後(イメージ):

ruby
def self.event_log_level(event_name, level)
  new_log_levels = log_levels.dup
  new_log_levels[event_name.to_sym] = level
  @log_levels = new_log_levels.freeze
end

ここでのポイント:

  • すでに @log_levels 自体が freeze されている想定で、その frozen なハッシュを直接書き換えなくなった
  • 毎回 dup してから freeze することで、「書き換え可能なオブジェクト」が Ractor 間で共有されなくなる。

テストの追加

activesupport/test/event_reporter/log_subscriber_test.rb に6行追加されており、主に以下の点を検証していると考えられます:

  • event_log_level を複数回呼んでも例外が出ないこと
  • log_levels が常に freeze されていること
  • (Ruby が Ractor をサポートする環境であれば)Ractor 内から event_log_level を使ってもエラーにならないこと、など

  1. 影響範囲・注意点
  • 対象: ActiveSupport::EventReport::LogSubscriber を直接または経由して利用しているコード。
    • 主に「イベントレポートのログレベルを動的にカスタマイズしている」ようなコードが影響範囲です。
  • 挙動面:
    • 公開APIとしては event_log_level の使い方・結果は変わらない想定です(同じイベントに対するログレベルが期待通り設定される)。
    • 内部実装のみがスレッド/Ractor セーフな書き方に変わっています。
  • パフォーマンス:
    • event_log_level 呼び出し毎に log_levels ハッシュの dupfreeze が入るため、わずかにオーバーヘッドは増えます。
    • ただし、この API は「起動時設定や初期化フェーズで数回呼ぶ」程度のことが多く、通常のリクエストパス上で高頻度に呼ぶことは想定されていないため、実務上は問題にならないケースがほとんどです。
  • マルチスレッド/Ractor:
    • 複数スレッドや Ractor から event_log_level を呼び出しても、共有の frozen ハッシュを直接書き換えないため、競合や Ractor 不正アクセスエラーを避けられます。
    • 既に Ractor を使っているアプリケーションでは、この変更により「イベントログレベル設定」に起因するエラーが起きにくくなります。

  1. 参考情報 (あれば)
  • Ractor セーフにする際の典型パターン:
    • 共有データ構造は freeze して不変にする。
    • 更新が必要な場合は「dup → 変更 → freeze → 参照を差し替え」のパターンを取る。
  • Rails / ActiveSupport における類似の対応:
    • キャッシュ設定やログ設定など、グローバルなコンフィグを扱う箇所では、同様の「イミュータブルなハッシュ/配列を差し替える」手法が徐々に導入されています。
  • 本 PR は CHANGELOG 更新は行われておらず、「外部API仕様は変えずに内部を Ractor フレンドリーにした小規模バグ修正/改善」という位置づけです。

#57978 Make Normalization.normalizes ractor safe

マージ日: 2026/7/3 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    ActiveModel::Normalization.normalizes が Ractor(マルチスレッド並列実行)環境でも安全に利用できるよう、正規化属性を追加する内部実装を「複製してから追記して凍結する」形に変更した PR です。挙動自体は変えずに、内部データ構造の扱い方だけをスレッド/Ractor安全寄りにしています。

  1. 変更内容の詳細

何をやったか

ActiveModel::Attributes::Normalization モジュール内で、normalizes 呼び出し時に使う内部配列(もしくは類似のコレクション)の扱いを以下のように変更しています。

  • これまで: 既存の配列に対してそのまま << などで追記(破壊的変更)
  • 変更後:
    1. 既存の配列を dup で複製
    2. 複製した配列に追記 (append)
    3. その配列を freeze して不変オブジェクトとして保持

これにより、ある Ractor で参照中の配列が、別の Ractor から破壊的に変更されることを防いでいます。

PR本文で書かれていた「Dup, append and freeze.」がまさにこのパターンです。

ざっくりしたイメージコード

実際のコードとは多少違う可能性がありますが、意図としては以下のような変化です:

ruby
# 変更前のイメージ
def normalizes(name, **options, &block)
  @normalizations ||= []
  @normalizations << build_normalization(name, options, &block)
end

# 変更後のイメージ
def normalizes(name, **options, &block)
  @normalizations ||= [].freeze
  new_normalizations = @normalizations.dup
  new_normalizations << build_normalization(name, options, &block)
  @normalizations = new_normalizations.freeze
end

ポイント:

  • 共有されるインスタンス変数(例: @normalizations)自体は置き換え(再代入)で更新し、
    「すでに公開済みのオブジェクト」は一切破壊的に書き換えない。
  • 新しく作る配列は最終的に freeze されるため、Ractor 間で共有しても書き換え不可能

テストの追加

  • activemodel/test/cases/attributes/normalization_test.rb に 7 行追加されています。
  • 目的は主に:
    • normalizes 呼び出し後も内部状態が適切であること
    • 複数回 normalizes を呼んだときに前の定義が壊れないこと
    • 凍結配列を前提にした挙動でも問題ないこと
      などを確認するものと考えられます(Ractor そのものを直接テストしているかはコード次第ですが、「破壊的変更しない実装」になっていることを保証するテストが入っているはずです)。

  1. 影響範囲・注意点

影響範囲

  • 対象: ActiveModel::Attributes::Normalization を利用しているコード、具体的には:
ruby
class User
  include ActiveModel::Model
  include ActiveModel::Attributes
  include ActiveModel::Attributes::Normalization

  attribute :email, :string

  normalizes :email, with: ->(value) { value.to_s.strip.downcase }
end

のように normalizes を使っている箇所。

  • 表面的な挙動(値がどう正規化されるか)は変わりません。
  • 主に内部の属性正規化情報テーブルの更新手順のみが、破壊的更新から「イミュータブルな再割り当て」方式に切り替わっています。

Ractor 安全性について

  • この PR によって:
    • 「normalized 属性を追加する処理」自体が Ractor セーフに近づいた
      (既存オブジェクトの破壊的変更を避けるため)
  • ただし PR 中でも明記されている通り:

Note, attribute registration that this relies on is still not ractor safe, its slightly more involved. Someone will do it next week probably.

  • つまり:
    • 「属性(attribute)自体の登録」に関わるコードは、まだ Ractor セーフではない。
    • normalizes 周辺だけが先行して Ractor セーフなスタイルにリファクタされている状態。
  • 結論として:
    • この PR だけで ActiveModel 属性周り全体が完全に Ractor セーフになるわけではない
    • とはいえ、将来的な完全 Ractor 対応に向けた一部ステップであり、
      少なくとも normalizes による内部配列の共有・書き換えに起因する Ractor 競合バグは出にくくなります。

パフォーマンス・互換性

  • dup + freeze により:
    • normalizes 呼び出しごとに配列のコピーが発生するため、属性数・normalizes 呼び出し回数が非常に多い場合には、わずかなオーバーヘッド増加があり得ます。
    • ただし normalizes 自体は通常アプリケーション起動時にクラス定義の中で呼ぶことが多く、リクエストごとに大量に呼ばれるものではないため、実務上は問題になりにくいと考えられます。
  • API 互換性:
    • 公開 API (normalizes の引数や動作) は変わっていないため、既存コードは基本的にそのまま動作します。

  1. 参考情報 (あれば)
  • Ruby Ractor とイミュータブルデータ構造:

    • Ractor 間で共有できるのは、基本的に「凍結されたオブジェクト」や Ractor セーフな型だけであり、可変オブジェクトを共有し破壊的変更するとエラーや未定義動作の原因になります。
    • この PR の dupappendfreeze パターンは、Ractor 対応で頻出する「内部状態をイミュータブルにする」ための典型的リファクタです。
  • 今後の想定:

    • PR 本文にある通り、属性登録周りの Ractor 対応が別 PR で行われる予定で、
      それと組み合わさることで ActiveModel 属性システム全体がより Ractor フレンドリーになっていきます。

#57979 Use latest ruby for GHA integration tests

マージ日: 2026/7/3 | 作成者: @skipkayhil

  1. 概要 (1-2文で)
    このPRは、GitHub Actions 上で動作する Rails の各種インテグレーションテスト(devcontainer、rails new docker、rail_inspector)で使用する Ruby のバージョンを「最新の Ruby」に切り替える変更です。テストフロー自体は変えず、実行環境としての Ruby の指定のみを更新しています。

  2. 変更内容の詳細(あればサンプルコードも含めて)
    対象ファイルはいずれも GitHub Actions のワークフローファイルです。

  • .github/workflows/devcontainer-smoke-test.yml
  • .github/workflows/rail_inspector.yml
  • .github/workflows/rails-new-docker.yml

各ワークフロー内で、Ruby のバージョンを指定している箇所が 1 行ずつ書き換えられています。具体的には、従来は固定バージョン(例: 3.3, 3.2 など)を指定していたものを、「最新の安定版 Ruby」を指す指定に変更した形が想定されます。

典型的には、以下のような変更になっている可能性が高いです(参考イメージ):

yaml
# 変更前(例)
- uses: ruby/setup-ruby@v1
  with:
    ruby-version: '3.3'

# 変更後(例)
- uses: ruby/setup-ruby@v1
  with:
    ruby-version: 'ruby'  # または '3.4' など、その時点の最新

もしくは、matrix で定義している ruby: [ '3.3' ] を最新バージョンに上げる、という形の可能性もあります。
いずれにせよ、各ワークフローの「使う Ruby のバージョンを 1 行だけ差し替えている」以外の変更は行われていません。

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

    • GitHub Actions 上で実行される以下のワークフローの Ruby 実行環境:
      • devcontainer-smoke-test
      • rail_inspector
      • rails-new-docker
    • これらが「常に最新の Ruby で Rails をテストする」状態になるため、Rails 本体が最新 Ruby に対して継続的に検証されるようになります。
  • メリット

    • Ruby の新バージョンがリリースされた際に、自動的にそのバージョンで検証が走るようになり、Ruby 側の変更による非互換や警告の早期検知が可能になります。
    • Rails の「最新 Ruby サポート状況」が CI 上で常にカバーされるため、メンテナンス性の向上に寄与します。
  • 注意点 / リスク

    • Ruby の新バージョンで非互換やバグが出た場合、Ruby リリース直後に CI が急に壊れる可能性があります(=Rails 側が即座には対応できていないケース)。
    • もし「特定の Ruby バージョンでの動作保証」を厳密にテストしたい場合は、別のジョブやマトリクスで LTS 的なバージョン(例: 3.1, 3.2 など)を並行して運用する必要があります。
    • 新しい Ruby でのみ発生する警告(deprecation など)が増え、ログがノイジーになる可能性があります。
  1. 参考情報 (あれば)

#57974 Require useragent when loading ActionController::AllowBrowser

マージ日: 2026/7/3 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    ActionController::AllowBrowser を読み込むタイミングで useragentrequire するように変更し、これまで「最初のリクエスト時」に行われていた useragent の読み込みを事前に行えるようにした PR です。主に eager load 環境や Ractor 対応時の初回読み込みタイミングの改善が目的です。

  1. 変更内容の詳細

※PR本文からはファイル単位の情報しかありませんが、内容的には以下のような極小変更です(+2/-2 行)。

対象ファイル:

  • actionpack/lib/action_controller/metal/allow_browser.rb

やっていることは要約すると:

  • これまで:
    • AllowBrowser が使われるタイミング、あるいは最初のリクエスト処理のどこかで useragentrequire していた(=eager load していても、実際の最初のリクエスト時に初回 require が走る)。
  • これから:
    • ActionController::AllowBrowser クラス/モジュールがロードされる段階で require "useragent" を実行するように変更。

イメージとしては、以下のような差分になっている可能性が高いです(擬似コード):

ruby
# 変更前 (例)
module ActionController
  class AllowBrowser
    # どこかのメソッド内や遅延ロード処理の中で:
    # require "useragent"
  end
end

# 変更後 (例)
require "useragent"  # ← ファイルロード時に実行

module ActionController
  class AllowBrowser
    # ここでは useragent はすでにロード済み
  end
end

これにより、AllowBrowser を使う環境では、そのファイルがロードされた時点で useragent もロードされます。


  1. 影響範囲・注意点
  • 影響範囲
    • ActionController::AllowBrowser を使用しているアプリ・ライブラリ。
    • 特に eager load を有効にしている本番環境や、Ractor を利用するような並行実行環境での挙動に影響します。
  • 具体的な挙動の変化
    • 以前: 最初のリクエスト処理中に useragentrequire が発生し、そのタイミングでロードコストやスレッド/Ractor との競合リスクがあった。
    • 以後: Rails が ActionController::AllowBrowser をロードした時点で useragent が確定的にロードされるため、最初のリクエスト時のオーバーヘッドが減り、Ractor 的にも安全になりやすい。
  • パフォーマンス・メモリ
    • AllowBrowser を利用している環境では、「どうせいつかは useragent を読む」ので、初回リクエスト前に読むか後に読むかの違いであり、全体的なメモリ使用増はほぼ変わりません。
    • ただし、AllowBrowser を定義だけ読み込んで実際には一切使わないような特殊な構成では、以前よりも早いタイミングで useragent がロードされる可能性があります。
  • 互換性
    • useragent 自体の API 変更はなく、あくまで require のタイミング変更のため、一般的なアプリでは後方互換性上の問題はほぼありません。
    • useragent gem を明示的に Gemfile に入れていない状況で(たまたま別の依存関係から入っていたなど)、ロード順に依存していたような非常にレアなケースでは、LoadError の発生タイミングが変わる可能性があります。

  1. 参考情報 (あれば)
  • Ractor との関連:
    • Ractor 環境では、スレッドセーフでない初回 require や定数定義が、並行実行と絡んで問題を起こし得るため、「最初のリクエスト中に require される」ようなパターンは避けたい。
    • 本PRにより、AllowBrowser のロードタイミングで useragent が確定的に読み込まれるため、Ractor 開始前に eager load を済ませる設計と相性が良くなります。
  • 実運用での確認ポイント:
    • AllowBrowser を利用しているアプリでは、本番起動時(Rails boot 時)に useragent が読み込まれることを前提に、必要ならば Gemfile に gem "useragent" が明示的に含まれているかを確認しておくと安全です。

#57977 Fix typographical errors from CHANGELOG files[ci skip]

マージ日: 2026/7/3 | 作成者: @nisusam

  1. 概要 (1-2文で)
    このPRは、Rails の CHANGELOG ファイル内に含まれていた英単語のタイポ(綴り誤り)を修正するものです。コードや挙動は一切変更せず、ドキュメントの品質向上のみを目的としています。

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

  • 対象ファイル

    • actionpack/CHANGELOG.md
    • actionview/CHANGELOG.md
    • railties/CHANGELOG.md
  • 変更内容

    • 各 CHANGELOG に記載されている文章中の typographical error(スペルミスや細かな表記揺れ)を、それぞれ正しい綴りに修正しています。
    • 追加行数 3 / 削除行数 3 から分かる通り、修正は行単位で 1 箇所ずつの軽微な置き換えであり、文面の意味や履歴内容自体は変えていません。
    • 例としては以下のような変更が想定されます(イメージであり、実際の差分そのものではありません):
      diff
      - * Fixes a behvaior where ...
      + * Fixes a behavior where ...
      
      - * Depreacted option ...
      + * Deprecated option ...

    Ruby コード・設定ファイル・テストコードなど、実行時に読み込まれる部分への変更は行われていません。

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

    • 影響するのはドキュメント(CHANGELOG)のみであり、Rails アプリケーションの実行時挙動、ビルド、テスト、API は一切影響を受けません。
    • 既存のバージョン互換性・マイグレーション・設定などにも影響なし。
  • 注意点

    • CI スキップ指定([ci skip])がタイトルに入っており、テストが走っていない点はログ上の事実としてありますが、そもそも実行可能コードに触れていないため問題にはなりません。
    • CHANGELOG の参照時に、以前のタイポに依存したリンクや検索(例: タイポした単語で全文検索していた)を行っていた場合は、今後は正しい綴りで検索する必要があります。
  1. 参考情報 (あれば)
  • PR タイトルから分かる通り、この変更は「CHANGELOG ファイル中の誤字修正」に限定されており、Rails の機能追加やバグ修正とは無関係です。
  • Rails では、コードと直接関係ないドキュメント変更についても PR ベースで管理し、CI をスキップして軽量にマージする運用が行われています。

#57959 Fix polymorphic belongs_to primary key for sharded targets

マージ日: 2026/7/3 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
  • シャーディング対応で「複合主キーを持つ has_many」と「単一外部キーを持つ polymorphic belongs_to」を組み合わせたときに、AutosaveAssociation 内で主キーと外部キーの対応付けがずれて ActiveModel::MissingAttributeError が起きる回 regress を修正した PRです。
  • BelongsToReflection#association_primary_key が、belongs_to 側が単一外部キーしか持たない場合でも、シャーディング用の複合主キー(query_constraints)をそのまま返してしまっていた挙動を是正しています。

  1. 変更内容の詳細

問題の背景

  • シャーディング対象モデルは、典型的に複合主キーのような「問い合わせ上の主キー」を持ちます:
    • 例: [:shard_id, :id]query_constraints として扱う
  • 一方で、対応する belongs_to 側は単一の外部キーしか持たないケースがあります:
    • 例: belongs_to :shipment, foreign_key: :shipment_id, polymorphic: true のような形
  • Rails 7.2 系での refactor により、AutosaveAssociation 内で使われていた compute_primary_key が削除され、代わりに BelongsToReflection#association_primary_key の戻り値がそのまま使われるようになりました(該当コミット)。
  • AutosaveAssociation は「関連先の primary_key」と「自分の foreign_key」を Array#zip して対応付ける実装になっているため、
    • primary_key 側が [:shard_id, :id](長さ2)
    • foreign_key 側が [:shipment_id](長さ1) のように長さが合わないと、
    • shard_id の対応 foreign_key が存在せず nil
    • それを読み出そうとしたときに ActiveModel::MissingAttributeError が発生してしまう、という不具合が生じていました。

原因になっていた association_primary_key の挙動

BelongsToReflection#association_primary_key は、関連の解決のために使う「関連先の主キー的なカラム」を求めるメソッドです。今回問題になったのは2つの分岐です:

  1. inverse_of がある場合の分岐:
    • inverse.options[:primary_key] をそのまま採用していた
    • 通常の「単一カラムの custom primary key(例: :uuid)」なら正しいが、
    • シャーディング対応で has_many 側に primary_key: [:shard_id, :id] などの「複合 primary_key もどき」が指定されている場合、belongs_to 側は単一外部キーでも composite をそのまま採用してしまう
  2. 上記以外のフォールスルー分岐:
    • ターゲットモデルが has_query_constraints? の場合、query_constraints(複合主キー的な配列)を展開して返す
    • その際、belongs_to が「明示的に単一 foreign_key を指定している」ことを考慮していなかった

その結果、belongs_to 側が単一 foreign_key にもかかわらず、関連先の「複合 primary key」配列が返ってきてしまい、AutosaveAssociation 側の zip で長さが合わずに例外を起こしていました。

修正内容のポイント

今回のコミットは、過去に compute_primary_key で行っていた「2つのガード」を association_primary_key に復元する形で問題を解消しています。

  1. inverse_of ブランチのガード復活

    • 目的: inverse.options[:primary_key] が「複合キー由来」のときに採用しないようにする
    • 実装の考え方:
      • has_many 側が「複合 foreign_key」を宣言しているとき、その情報は :query_constraints にも反映される
      • したがって、inverse.options:query_constraints が存在する場合、そこからは「複合キー前提」であることが分かる
    • 修正:
      • association_primary_key 内で inverse.options[:query_constraints] の有無を確認し、
        • ある場合: composite な inverse primary_key(≒query_constraints 由来)として扱うため、inverse.options[:primary_key] をそのまま採用しない
        • ない場合: 本当に単一の custom primary_key(例: uuid)を指定していると判断し、inverse.options[:primary_key] を採用する
  2. フォールスルーブランチのガード復活

    • 目的: belongs_to 側が明示的に単一 foreign_key を指定しているときには、関連先も単一 primary_key に解決するようにする
    • 修正:
      • has_query_constraints? なターゲットであっても、
        • belongs_to で「単一の foreign_key が明示されている」場合は、query_constraints を複合キーとしてそのまま返さず、
        • 単一の primary_key(primary_key ないし id)に解決するようなロジックを追加
      • これにより、zip の対象が
        • primary_key 側: [:id](または同等)
        • foreign_key 側: [:shipment_id] となり、要素数が一致して安定して動作する

テストの追加・変更

  • activerecord/test/cases/associations/belongs_to_associations_test.rb
    • polymorphic belongs_to + シャーディング対象モデル + inverse_of が絡むケースをカバーするテストを追加
  • activerecord/test/cases/autosave_association_test.rb
    • AutosaveAssociation 経由で保存されるシナリオ(accepts_nested_attributes_for 等)の中で、例外が発生しないことを確認する回帰テストを追加
  • activerecord/test/models/adjustment.rb, shipment.rb, schema.rb
    • 上記テスト用のモデル・スキーマ定義を拡張
    • シャーディング風の query_constraints/複合 primary_key もどき + polymorphic belongs_to の組み合わせを表現するためのフィクスチャモデルを追加

  1. 影響範囲・注意点
  • 影響を受ける可能性が高いケース:
    • シャーディングやマルチテナント構成で
      • モデルに query_constraints(例: [:shard_id, :id] 等)を設定している
      • そのモデルに対して polymorphic な belongs_to を張っている
      • inverse 側に has_many + primary_key: [:shard_id, :id] のような宣言をしている
      • かつ AutosaveAssociation 経由(accepts_nested_attributes_for など)で保存している
  • そのようなアプリでは、Rails のバージョン更新後に以下のような症状が出ていた場合、この PR によって解消される可能性があります:
    • ActiveModel::MissingAttributeErrorautosave のタイミングで突然出る
    • メッセージの原因が「関連先モデルの shard_id など、複合キーの一部が nil とみなされている」ように見える
  • 互換性の観点:
    • 通常の(非シャーディング・非複合キー)belongs_to / has_many には影響しない設計になっています。
    • 単一カラムの custom primary_key(例: primary_key: :uuid)を使っている場合も、inverse.options[:query_constraints] が無い限りはこれまでどおり inverse.options[:primary_key] が採用されるため、挙動は維持されます。
  • 注意点:
    • アプリ側で「belongs_to は単一外部キーだが、関連先は複合 query_constraints を前提にしている」という、かなりトリッキーなカスタマイズをしていた場合、今回の「単一 foreign_key 優先の解決ロジック」により挙動が変わる可能性があります。
    • 特に、独自に query_constraints をオーバーライドしているような高度なシャーディング実装を行っている場合は、この PR を含む Rails へのアップデート時に関連の保存・ロードの挙動をテストで確認することを推奨します。

  1. 参考情報 (あれば)
  • 回帰のきっかけとなったコミット:
  • 関連クラス・メソッド:
    • ActiveRecord::Reflection::BelongsToReflection#association_primary_key
    • ActiveRecord::AutosaveAssociation
    • ActiveRecord::Reflection#has_query_constraints?query_constraints
  • 実際のコードを確認したい場合は、activerecord/lib/active_record/reflection.rbassociation_primary_key 実装と、autosave_association_test.rb に追加されたテストケースを読むと、
    「inverse に複合 primary_key がある / belongs_to に単一 foreign_key がある」という具体的な差分が把握しやすくなります。

#57942 Return all parameters from deconstruct_keys when no keys are requested

マージ日: 2026/7/3 | 作成者: @55728

  1. 概要 (1-2文で)
    ActionController::Parameters に対するパターンマッチで、{ key:, **rest } のように 特定キーを指定せず rest バインディングだけを使った場合にマッチしなかった問題を修正し、Hash#deconstruct_keys と同じ挙動(キー指定なしなら全パラメータを返す)になるようにした変更です。

  1. 変更内容の詳細

これまでの問題点

Ruby 3 以降のパターンマッチ(case ... in)は、ハッシュライクなオブジェクトに対して deconstruct_keys を呼び出してパターンにマッチさせます。

ActionController::Parameters はこのインターフェイスを実装していますが、
パターン側で特定のキーをひとつも要求していない場合in { **rest } や、キーは指定するけど rest のみを実質使うようなケース)に、deconstruct_keys空ハッシュ {} を返してしまう挙動になっており、結果としてマッチ失敗になっていました。

PR で説明されている再現コード:

ruby
params = ActionController::Parameters.new(name: "Bob", age: 22).permit!

case params
in { name:, **rest }
  # Before: ここに来ない(マッチしない)
  # After:  name => "Bob", rest => { age: 22 }
end

原因は、deconstruct_keys(nil)(=「特定のキーは要求していない」という意味)に対して、ActionController::Parameters が空ハッシュを返していたためです。

Ruby の Hash#deconstruct_keys は、引数が nil のとき「全キー・全値」を返す仕様なので、ActionController::Parameters だけ挙動が異なり、パターンマッチが期待通り動かない不整合がありました。

今回の修正内容

actionpack/lib/action_controller/metal/strong_parameters.rbdeconstruct_keys 実装が修正されました:

  • 引数 keysnil の場合に
    全パラメータを返すように変更
  • これにより Hash#deconstruct_keys と同じ挙動になります。

テスト (actionpack/test/controller/parameters/equality_test.rb) では、おそらく次のような性質が確認されています(実際のコードは要約):

  • ActionController::Parameters.new(name: "Bob", age: 22).permit! に対し、
    • case params; in { name:, **rest }; ...; end がマッチすること
    • name"Bob" になること
    • rest{ age: 22 } 相当になること

動作イメージ(サンプル)

変更後の振る舞い:

ruby
params = ActionController::Parameters.new(
  name: "Bob",
  age: 22,
  admin: true,
).permit!

case params
in { name:, **rest }
  # deconstruct_keys(nil) が全パラメータを返すのでマッチする
  # name => "Bob"
  # rest => { "age" => 22, "admin" => true } あるいは Parameters 相当
end

case params
in { **rest }
  # これもマッチする(rest にすべてが入る)
end

  1. 影響範囲・注意点
  • 影響を受けるコード
    • ActionController::Parameters を直接 case ... in でマッチさせているコード
    • 特に { key:, **rest }{ **rest } のような rest バインディングを使うパターン を利用している場合に挙動が変わります。
  • 以前との違い
    • 以前は「マッチしない」ケースが、「マッチして全パラメータが渡される」ようになります。
    • つまり、以前は else 側に流れていた分岐が、in { ... } 側に入るようになる可能性があります。
  • 互換性
    • Ruby 標準の Hash と同じ deconstruct_keys 挙動になるため、設計上はむしろ一貫性が向上した変更です。
    • ただし、「ActionController::Parameters ではこのパターンはマッチしないはず」という前提で書かれていた奇妙なワークアラウンドがあれば、その前提は崩れます。
  • Strong Parameters / permit の扱い
    • 例では permit! 済みのオブジェクトになっており、通常は 許可されたパラメータの集合deconstruct_keys の対象です。
    • 未許可のパラメータを含む場合、もともとの ActionController::Parameters の振る舞い(フィルタリングの有無など)に依存するため、permit / require の位置づけは引き続き意識する必要があります。

  1. 参考情報 (あれば)
  • Ruby 本体の仕様: Hash#deconstruct_keys
    • 引数が nil のとき「ハッシュ自身を返す」という仕様で、今回の変更はこれに追従しています。
  • Rails側 PR: Return all parameters from deconstruct_keys when no keys are requested (Rails PR #57942)
    • ActionController::Parameters を Ruby のパターンマッチ機構とより自然に連携させるための小さな互換性改善です。

#57965 8 1 stable cp 1dbad939f8

マージ日: 2026/7/3 | 作成者: @p8

  1. 概要 (1-2文で)
    このPRは、Rails 8.1 ステーブルブランチに対して「product_reviews」という新しいガイドを大きく追加し、既存の「wishlists」ガイドをわずかに調整する、ドキュメント中心の変更です。コード本体には手が入っておらず、開発者向けの学習・サンプルアプリケーション的なガイドが充実した形になります。

  1. 変更内容の詳細

2-1. guides/source/product_reviews.md の新規追加 (+1350行)

1350行規模の大きなガイドが新規に追加されています。内容からすると、以下のような「Rails 8.1 を用いた Product Reviews(商品レビュー)機能」のチュートリアル/サンプルアプリ的ガイドである可能性が高いです(Wishlists ガイドと同系統のシリーズであることが多いです)。

想定される構成・内容:

  • アプリケーション概要

    • ECサイトや商品カタログに対して「レビュー」や「評価(rating)」を投稿できる機能を題材にしたガイド
    • Rails 8.1 の標準的なスタック(Zeitwerk, importmap / jsbundling, turbo, stimulus など)を前提とした作り
  • モデル設計例

    • Product / User / Review などの関連付け

    • 典型的には下記のような関連を解説していると考えられます:

      ruby
      class Product < ApplicationRecord
        has_many :reviews, dependent: :destroy
      end
      
      class Review < ApplicationRecord
        belongs_to :product
        belongs_to :user
      
        validates :rating, presence: true, inclusion: { in: 1..5 }
        validates :body, presence: true
      end
      
      class User < ApplicationRecord
        has_many :reviews, dependent: :nullify
      end
  • マイグレーションとスキーマ設計

    • reviews テーブルのカラム例:
      • product_id / user_id (外部キー)
      • rating (整数)
      • body (テキスト)
      • created_at, updated_at
    • インデックスや外部キー制約の付け方
  • コントローラとルーティング

    • resources :products do resources :reviews, only: %i[create update destroy] end のようなネストされたリソースルーティング

    • ReviewsController での create, update, destroy アクション実装例

      ruby
      class ReviewsController < ApplicationController
        before_action :set_product
      
        def create
          @review = @product.reviews.build(review_params.merge(user: current_user))
      
          if @review.save
            redirect_to @product, notice: "Review was successfully created."
          else
            render "products/show", status: :unprocessable_entity
          end
        end
      
        private
      
        def set_product
          @product = Product.find(params[:product_id])
        end
      
        def review_params
          params.require(:review).permit(:rating, :body)
        end
      end
  • ビュー・UI 実装の解説

    • 商品詳細ページにレビュー一覧と投稿フォームを組み込む例
    • form_with model: [@product, @product.reviews.build] などネスト資源のフォーム作成
    • rating をセレクトボックスやラジオボタンで扱う例
  • バリデーション・エラーハンドリング

    • バリデーションエラー時に同じページにレンダリングしなおすパターン
    • Turbo 対応時のエラーレスポンスやステータスコードの選び方 (422 Unprocessable Entity)
  • 認可・認証関連の話題(ある程度触れられる可能性)

    • ログイン済みユーザーのみレビュー可能
    • 自分のレビューしか編集・削除できない、といったポリシー例
  • テストのサンプル

    • Minitest / RSpec いずれか、または Rails 標準のテスティングガイドに沿った形でのモデル・コントローラテスト例
    • ルーティング・関連付け・バリデーションなどのテスト

要点として、このガイドは:

  • Rails 8.1 で「ある程度現実的なドメイン(product reviews)」を使って
  • モデル・ビュー・コントローラ・ルーティング・バリデーション・テスト
  • そして場合によっては Turbo/Stimulus まで

を一通りつなげて見せる「実践的なチュートリアル」のような位置づけと思われます。

2-2. guides/source/wishlists.md の変更 (+1 / -8)

wishlists.md に対してはごく小さい修正のみです。

典型的にありえる内容は:

  • Product Reviews ガイドとの整合性を取るためのリンク追加・章番号の調整
  • 「次に読むガイド」として product_reviews へのリンクを追加
  • 古いバージョン固有の記述や typo の修正・削除

行数的には 8 行削除 / 1 行追加なので、文章の差し替え・段落の統合・見出しレベルの変更など、小さな編集にとどまっています。機能仕様や API 解説のような本質的変更ではないと考えて問題ありません。


  1. 影響範囲・注意点
  • ランタイム挙動への影響:

    • 変更はすべて guides/source 配下であり、Rails 本体のコードは一切変更されていません。
    • そのため、アプリケーションの挙動・API・互換性への影響はありません。
  • 対象バージョン:

    • タイトルから、「8 1 stable」ブランチ向けの cherry-pick と思われます。
    • Rails 8.1 系の公式ガイドに Product Reviews チュートリアルが追加された、という位置づけです。
  • ドキュメント利用者への影響:

    • Rails 初学者や、サンプルアプリを通じて 8.1 の構成に慣れたい開発者に有用です。
    • Wishlists ガイドを既に読んでいる場合、その続き・姉妹編のような形で Product Reviews ガイドを読むことになる可能性があります。
    • 実案件で reviews 機能を実装する際のモデル・コントローラ・ビュー構成の「リファレンス例」としても利用できます。

注意点としては以下程度です:

  • ガイドはあくまでサンプルであり、そのまま本番環境に流用する場合は認可・バリデーション・攻撃耐性(スパムや大量投稿など)を自分の要件に合わせて補強する必要があります。
  • 8.1 以外のバージョン(特に 7 系以前)では、ガイド中で前提とされている API やフロントエンド構成が異なる場合があります。

  1. 参考情報 (あれば)

#57964 Fix casing in product reviews guide

マージ日: 2026/7/3 | 作成者: @p8

  1. 概要 (1-2文で)
    このPRは、Rails ガイド「product_reviews.md」内の英単語の大文字・小文字表記(casing)を修正し、用語やコードの表記ゆれを整えるドキュメント修正です。機能や挙動の変更はなく、アプリケーションコードやテストには影響しません。

  1. 変更内容の詳細
  • 対象ファイルは guides/source/product_reviews.md のみで、行単位で「+21 / -21」となっており、文章やコード例の一部表記が置き換えられています。
  • タイトルの通り "casing" の修正が中心で、主に以下のような変更が想定されます(実際の差分から一般的に起こるパターンを整理):
    • 英語の表記ゆれの統一
      • 例: "Product reviews" → "Product Reviews"(ガイドタイトルや見出しとしての表記統一)
      • 例: "api" → "API"、"json" → "JSON" などの一般的な頭字語の大文字化
      • 例: "Postgresql" → "PostgreSQL" など、固有名詞の正しい大文字小文字への修正
    • Rails 用語の表記統一
      • 例: "Active record" → "Active Record"
      • 例: "Action cable" → "Action Cable"
    • コード例・識別子の区別を明確にするための修正
      • 文中のクラス名・モジュール名を、実際の Ruby/Rails の定義と揃えた可能性があります
        • 例: productreviewProductReview
        • 例: productreviewscontrollerProductReviewsController
      • 文章とコードの混在箇所で、コードは小文字スネークケース、クラス名はパスカルケースといった区別を明確化

※差分から見ると行数は同じであるため、段落削除や追加ではなく、同じ行の中での文字置換(大文字小文字の変更)が中心です。


  1. 影響範囲・注意点
  • ランタイム挙動への影響:
    • ガイド文書のみの変更であり、Rails 本体のコード・API・挙動・テストには一切影響しません。
  • 既存アプリケーションへの影響:
    • 既存の Rails アプリやガイドに従って実装しているコードが壊れることはありません。
  • 読者への影響 / 注意点:
    • ガイドを参照している場合、用語やクラス名の表記が Rails 本体の実際の命名規則(クラス: CamelCase、ファイル・テーブル: snake_case など)とより一致するように改善されています。
    • もし過去に古いガイドの表記に合わせてクラス名などを誤って命名していた場合は、今後このガイドを参照するときには、改めて Rails の一般的な命名規則(例: ProductReview / product_reviews / product_reviews_controller.rb)に合わせることを推奨します。

  1. 参考情報 (あれば)
  • この PR には CHANGELOG への追記はなく、Rails 的には「ドキュメントの軽微な修正」と位置づけられています。
  • Rails ガイドと実際の API/クラス名を揃えることは、チュートリアルどおりに書いたのにエラーになる、といった学習者の混乱を防ぐ効果があります。
  • Rails の命名規則の詳細は以下を参照するとよいです(英語):

#57244 [RF-DOCS][ci-skip] Add Product Reviews Tutorial

マージ日: 2026/7/3 | 作成者: @excid3

  1. 概要 (1-2文で)
    このPRは、Rails公式ガイドの「ECサイトチュートリアル」の続きとして「商品レビュー(Product Reviews)」機能を実装する新しいチュートリアルを追加するドキュメント変更です。ユーザーによる星評価付きレビュー、画像アップロード、レビューに基づく商品の平均評価更新、フィルタリング・集計表示、管理画面でのレビュー編集などを一通りカバーしています。

  1. 変更内容の詳細

※コードベース本体ではなく「ガイド(Documentation)」のみの変更です。新規に product_reviews.md が追加され、既存の wishlists.md がわずかに修正されています。

2-1. 新チュートリアル: guides/source/product_reviews.md

このチュートリアルは、既存の「ECストア」チュートリアルに続けて「商品レビュー機能」を実装する流れを詳細に説明しています。主な要素は以下です。

モデル構成と関連付け

典型的には次のようなモデル構造を前提として解説されます:

  • Product
    • 多数の Review を持つ
    • 平均評価(例: ratingaverage_rating カラム)を持ち、レビュー投稿・更新のたびに再計算
  • Review
    • belongs_to :product
    • belongs_to :user (レビュアーを紐づけるケースが多い)
    • 属性例:
      • rating (1〜5の整数)
      • body (本文)
      • 画像添付用の has_many_attached :images など (Active Storage を使用した例が多い)

サンプルイメージ:

ruby
class Product < ApplicationRecord
  has_many :reviews, dependent: :destroy

  def update_rating!
    update!(
      rating: reviews.average(:rating).to_f.round(1) # 小数1桁など
    )
  end
end

class Review < ApplicationRecord
  belongs_to :product
  belongs_to :user

  has_many_attached :images

  validates :rating, presence: true, inclusion: { in: 1..5 }

  after_commit :update_product_rating

  private

  def update_product_rating
    product.update_rating!
  end
end

コントローラとルーティング

レビューは、商品にネストしたリソースとして扱う構成が説明されます。

ruby
# config/routes.rb
resources :products do
  resources :reviews, only: [:create, :edit, :update, :destroy]
end

ReviewsController では、以下のような操作をカバーします。

  • レビュー作成 (create)
  • レビュー編集・更新 (edit / update)
  • レビュー削除 (destroy)(場合によっては管理者のみ)
ruby
class ReviewsController < ApplicationController
  before_action :set_product
  before_action :set_review, only: [:edit, :update, :destroy]
  before_action :authenticate_user!

  def create
    @review = @product.reviews.new(review_params.merge(user: current_user))

    if @review.save
      redirect_to @product, notice: "レビューを投稿しました。"
    else
      render "products/show", status: :unprocessable_entity
    end
  end

  # ...

  private

  def set_product
    @product = Product.find(params[:product_id])
  end

  def set_review
    @review = @product.reviews.find(params[:id])
  end

  def review_params
    params.require(:review).permit(:rating, :body, images: [])
  end
end

星評価入力(UI)とCSS

説明の中で特徴的なのが「星評価をラジオボタンで実装し、CSSで直感的な星アイコンにする」という点です。これにより、以下の利点があります。

  • アクセシビリティを確保(スクリーンリーダーやキーボード操作に対応)
  • フォーム送信時には通常の rating パラメータとして送られる
  • CSSのみで視覚的にはリッチな星評価UIを実現

フォーム例:

erb
<%= form_with model: [@product, @review] do |f| %>
  <div class="rating-input">
    <% 5.downto(1) do |i| %>
      <%= f.radio_button :rating, i, id: "review_rating_#{i}" %>
      <%= f.label :"rating_#{i}" do %>

      <% end %>
    <% end %>
  </div>

  <%= f.text_area :body %>

  <%= f.file_field :images, multiple: true %>

  <%= f.submit "レビューを投稿" %>
<% end %>

CSSで label を星形にスタイリングし、input:checked ~ label などのセレクタを使って、選択状態に応じて星の色が変わるようにする、というパターンがチュートリアルで解説されています。

商品の平均評価の自動更新

レビュー作成・更新・削除のたびに、Product 側の評価を自動で再集計する仕組みを解説しています。

  • コールバック (after_commit など)で product.update_rating! を呼び出す
  • SQL の AVG(rating) を利用して平均値を計算
  • Product モデルの rating カラムに保存しておくことで、一覧画面などで N+1 クエリを避ける

計算例:

ruby
def update_rating!
  update!(
    rating: reviews.average(:rating) || 0
  )
end

レビューの絞り込みと集計表示

商品詳細ページなどで、レビューを以下のように表示・集計する例が解説されています。

  • 指定した星数のみを表示するフィルタ (例: 「星5のみ」「星4のみ」)
    • params[:rating] をもとに @product.reviews.where(rating: params[:rating]) などで絞り込み
  • 星ごとのレビュー数・割合をバーグラフで表示
    • 例: 5〜1まで各星の件数をカウントし、全体に対する割合を計算
ruby
# Product モデル例
def rating_breakdown
  total = reviews.count.to_f
  return {} if total.zero?

  (1..5).each_with_object({}) do |star, hash|
    count = reviews.where(rating: star).count
    hash[star] = {
      count: count,
      percent: ((count / total) * 100).round
    }
  end
end

ビュー側例 (擬似コード):

erb
<% @product.rating_breakdown.sort.reverse_each do |star, data| %>
  <div class="rating-row">
    <span><%= star %>★</span>
    <div class="bar">
      <div class="bar-fill" style="width: <%= data[:percent] %>%"></div>
    </div>
    <span><%= data[:count] %>件 (<%= data[:percent] %>%)</span>
  </div>
<% end %>

画像アップロード(任意)のサポート

  • Active Storage を使った画像添付例が含まれています
  • 1レビューに複数画像を添付できるように has_many_attached :images を使用
  • ビューで image_tag を使ってプレビュー表示する例が示されます
ruby
class Review < ApplicationRecord
  has_many_attached :images
end
erb
<% @review.images.each do |image| %>
  <%= image_tag image.variant(resize_to_limit: [400, 400]) %>
<% end %>

管理画面でのレビュー管理

  • 管理者がレビューを編集・削除できるようにするための UI / コントローラが説明されています
  • 主な用途: 誤字修正、不適切表現の修正、スパム的レビューの削除など
  • 実装例:
    • namespace :admin 配下に Admin::ReviewsController を作成
    • または既存の管理UI(例: Admin::ProductsController)にネストして操作する

ルート例:

ruby
namespace :admin do
  resources :products do
    resources :reviews, only: [:index, :edit, :update, :destroy]
  end
end

権限管理の考慮(認証・認可)が軽く触れられている可能性があります(before_action :require_admin! など)。


2-2. guides/source/wishlists.md の微修正

wishlists.md は既存のECストアチュートリアルの別パートで、ここに対して:

  • 他パート(今回追加された product_reviews.md など)へのリンクの修正・追加
  • あるいは構成の一貫性を保つための軽微な文章・見出しの変更

が行われています(+1/-8 行なので、ごく小さな調整に留まっています)。


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

    • Rails本体のコード・APIには影響せず、公式ガイド(ドキュメント)のみの変更です。
    • ただし「Rails Way」でのECサイトにおけるレビュー機能の実装パターンとして、今後このチュートリアルが事実上の「推奨例」になる可能性があります。
    • 特に以下の設計方針が、Railsコミュニティにおけるデファクトな例として参照されやすくなります:
      • 星評価をラジオボタン + CSS で実装するアプローチ
      • モデルのコールバック(orサービスオブジェクト)で平均評価を自動更新するアプローチ
      • Active Storage を利用したレビュー画像添付
  • 注意点 / 実装上のポイント

    • パフォーマンス:
      • 大量レビューがある商品の平均値再計算は、reviews.average(:rating) を毎回呼び出すと高コストになる可能性があります。
        • チュートリアルではシンプルな実装を重視しているため、実運用ではカウンタキャッシュ的な集計(reviews_count, rating_sum など)で最適化を検討するとよいです。
    • 整合性:
      • レビュー削除時にも平均値の更新が走るように、after_commit などのコールバックの種類やタイミングには注意が必要です。
    • セキュリティ:
      • 画像アップロードでは、ファイルサイズ・拡張子・Content-Typeの制限や、サムネイル生成時のImageMagick/ libvips 周りの安全性確保など、実運用では追加対策が必要になります。
    • 権限/認可:
      • 一般ユーザーが自分のレビューだけを編集・削除できるようにするのか、管理者のみ許可するのか、チュートリアルは1つの例に過ぎないため、自プロジェクトではポリシーを明確にする必要があります。

  1. 参考情報 (あれば)
  • Rails Guides (今後このチュートリアルが掲載されるであろう場所):
    https://guides.rubyonrails.org/
    Product Reviews もしくは E-Commerce 関連のチュートリアルセクションに追加される想定)

  • Active Storage ガイド(画像アップロードの詳細):
    https://edgeguides.rubyonrails.org/active_storage_overview.html

  • Rails公式チュートリアル内の他のEC機能 (カート、ウィッシュリストなど) と合わせて読むと、フル機能のECサイト構築フローとして参考になります。


#57826 Deprecate Mime::SET, Mime::LOOKUP and Mime::EXTENSION_LOOKUP

マージ日: 2026/7/2 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    Rails の MIME タイプ周りの内部レジストリを直接公開していた Mime::SET, Mime::LOOKUP, Mime::EXTENSION_LOOKUP が非推奨化され、代わりに既存の公開 API(Mime.symbols, Mime[...], Mime::Type.lookup, Mime::Type.lookup_by_extension など)と新設の Mime.extensions を使うように誘導する変更です。
    これにより、ミュータブルな内部状態への直接アクセスを抑制しつつ、これまで内部定数でしかできなかった操作も正式なパブリック API でカバーされるようになりました。

  1. 変更内容の詳細

非推奨になった定数

以下の 3 つの定数が非推奨になり、ActiveSupport::Deprecation::DeprecatedObjectProxy でラップされるようになりました。

  • Mime::SET
    • MIME タイプのレジストリ(シンボルなど)を保持していたセット。
  • Mime::LOOKUP
    • 文字列表現(例: "text/html")から MIME タイプを引くためのハッシュ。
  • Mime::EXTENSION_LOOKUP
    • 拡張子(例: "html")から MIME タイプを引くためのハッシュ。

これらはまだ参照可能ですが、使用すると deprecation warning が出るようになっています。

推奨される置き換え API

PR 説明にある通り、用途ごとに次のように書き換えが推奨されます。

1) Mime::SET

用途: 登録済みの MIME タイプを列挙・取得したい場合

置き換え:

  • 登録済みの MIME シンボル一覧が欲しいとき
    ruby
    # 旧
    Mime::SET # 非推奨
    
    # 新
    Mime.symbols
  • MIME タイプをキー(シンボルや文字列)から取得したいとき
    ruby
    # 旧
    type = Mime::SET[:html] # など
    
    # 新(推奨)
    type = Mime[:html]
    # または
    type = Mime::Type.lookup("text/html")

2) Mime::LOOKUP

用途: "text/html" のような MIME 文字列から Mime::Type を取得

置き換え:

ruby
# 旧
Mime::LOOKUP["text/html"] # 非推奨

# 新
Mime::Type.lookup("text/html")

3) Mime::EXTENSION_LOOKUP

用途: ファイル拡張子 "html" から MIME タイプを取得したり、全拡張子を列挙したり

置き換え:

  • 拡張子から MIME タイプを取得

    ruby
    # 旧
    Mime::EXTENSION_LOOKUP["html"] # 非推奨
    
    # 新
    Mime::Type.lookup_by_extension("html")
    # または
    Mime[:html] # 既存のショートカット
  • 登録済み拡張子を列挙したい(従来は Mime::EXTENSION_LOOKUP でしかできなかった)
    今回、新しく Mime.extensions が追加されています。

    ruby
    Mime.extensions
    # => [:html, :text, :json, ...] (全ての拡張子・シノニムを含む)

これにより、「レジストリ(Hash/Set)自体」に触るのではなく、メソッド経由で取得・列挙するスタイルに統一されます。

コア実装の変更点(概要)

actionpack/lib/action_dispatch/http/mime_type.rb の変更が中心で、以下のような対応がされています。

  • Mime::SET, Mime::LOOKUP, Mime::EXTENSION_LOOKUP を内部実体に対する DeprecatedObjectProxy に差し替え
    • 実行時には従来通り動作するが、アクセスごとに deprecation warning を発生させる。
  • Mime.extensions の追加
    • Mime.symbols 同様の公開メソッドとして、拡張子の一覧取得をサポート。
  • 既存内部コードで、これら定数を直接使っていた箇所を公開 API ベースの呼び出しにリファクタリング
    • action_mailer.rb, action_dispatch.rb, abstract_controller/collector.rb など。

テストの更新

  • actionpack/test/dispatch/mime_type_test.rb などで:
    • 新メソッド Mime.extensions の挙動テストが追加。
    • 旧定数使用時に deprecation が発生することの確認テストなどが追加または修正。
  • actionview/test/template/lookup_context_test.rb など内部で MIME レジストリを使うテストも、新 API に合わせて微修正。

  1. 影響範囲・注意点

影響を受けるコード

以下に該当するアプリケーション/ライブラリは deprecation warning の対象になります。

  • Mime::SET / Mime::LOOKUP / Mime::EXTENSION_LOOKUP を直接参照しているコード
    • 例:
      ruby
      Mime::SET.each do |type|
        # ...
      end
      
      if Mime::EXTENSION_LOOKUP["csv"]
        # ...
      end

これらは Rails の内部実装詳細(ミュータブルなレジストリそのもの)に依存しているため、今後のメジャーバージョンで削除される可能性が高いです。

マイグレーションの指針

  • 列挙用途
    • MIME シンボル一覧: Mime.symbols
    • 拡張子一覧: Mime.extensions
  • ルックアップ用途
    • MIME 文字列 "text/html"Mime::Type.lookup("text/html")
    • 拡張子 "html"Mime::Type.lookup_by_extension("html") または Mime[:html]
  • レジストリを直接変更している場合(かなりレアケース)
    • 例えば Mime::EXTENSION_LOOKUP["pdf"] = ... のように書いている場合は、元々非推奨なパターンであり、公式な登録 API(Mime::Type.register など)または提供されている拡張メソッド経由に置き換えるべきです。

注意点

  • 今はまだ動きますが、DeprecatedObjectProxy 経由の使用は将来の削除を前提とした一時措置です。
    deprecation メッセージが CI などで検出される場合は、早めの対応が必要です。
  • 特に gem やエンジンなどのライブラリは、利用者のアプリケーションで deprecation noise を出さないよう、できるだけ早期に新 API へ移行しておくとよいです。

  1. 参考情報 (あれば)
  • 置き換え API 一覧:
    • Mime::SET
      • Mime.symbols, Mime[...], Mime::Type.lookup
    • Mime::LOOKUP
      • Mime::Type.lookup
    • Mime::EXTENSION_LOOKUP
      • Mime.extensions, Mime::Type.lookup_by_extension, Mime[...]
  • 関連ファイル:
    • actionpack/lib/action_dispatch/http/mime_type.rb(MIME 周りの中核実装)
    • actionpack/CHANGELOG.md(非推奨化の公式なアナウンスが追記済み)

#57917 Attribute methods ractor safe

マージ日: 2026/7/2 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    Rails の ActiveModel::AttributeMethods 周りで使われる aliases_by_attribute_name / attribute_aliases / attribute_method_patterns を、Ractor 間で安全に共有できるよう「デフォルトで freeze される値オブジェクト&コレクション」に変更した PRです。これにより、Ractor 環境でも属性メソッド関連の情報を安心して参照できるようになり、並行実行時のデータ競合や Ractor 制限によるエラーを防ぎます。

  1. 変更内容の詳細

2-1. 目的となる3つのメソッドの Ractor-safe 化

Ractor 間で共有するには、「凍結されていて、かつ Ractor-shareable なオブジェクト」である必要があります。この PR では以下をその条件に合わせるための変更が行われています。

  • AttributeMethods.aliases_by_attribute_name
  • AttributeMethods.attribute_aliases
  • AttributeMethods.attribute_method_patterns

方針としては:

  • 値オブジェクトおよび配列・ハッシュを「生成時に freeze」する
  • 「必要になった時に書き換える(遅延・破壊的変更)」スタイルを避ける
  • 継承時に子クラスへ「凍結済みのコピー/設定」を行っておく

という構造になっています。

2-2. AttributeMethodPattern の値オブジェクトをデフォルトで frozen に

AttributeMethodPattern は、属性メソッド (before_type_cast, ?, = など) を表現するための内部的な値オブジェクトです。

この PR での変更点:

  • AttributeMethodPattern のインスタンスを生成時に freeze するように変更
  • これにより、attribute_method_patterns が保持するパターンの配列全体が Ractor 間で共有可能になる

イメージとしては:

ruby
pattern = AttributeMethodPattern.new(prefix: "", suffix: "=")
pattern.frozen? # => true

のように、パターン自体に後から変更を加えない前提をコードで保証するようにしています。

2-3. aliases_by_attribute_name を「継承時に」初期化するよう変更

従来は、aliases_by_attribute_name は「最初にアクセスされたときに default_proc を使って遅延生成・更新」していた箇所がありました。これは「ハッシュにアクセスした瞬間に中身を書き換える」ため、Ractor から見ると共有が難しくなります。

この PR では:

  • aliases_by_attribute_nameクラス継承時に確定させる 形に変更
    → サブクラス用のハッシュを、親クラスからコピー・設定するタイミングを明示的に
  • default_proc 内でハッシュを書き換えるのをやめ、「必要なキーが無い場合は、凍結された空配列などを返すだけ」に変更

イメージ:

ruby
# 旧: キー未定義時に default_proc 内でハッシュを更新していた
hash.default_proc = ->(h, k) { h[k] = [] }

# 新: ハッシュ自体は固定し、見つからないキーのときは
#     単に空の frozen 配列を返すだけにする
hash.default_proc = ->(_h, _k) { EMPTY_FROZEN_ARRAY }

こうすることで、aliases_by_attribute_name 自体は「それ以上変更されないハッシュ」となり、Ractor 間で共有しても安全になります。

2-4. 「欠けているキー」に対しては凍結された配列を返す

aliases_by_attribute_name[attribute_name] が存在しない場合:

  • 以前は default_proc 内で hash[attribute_name] = [] のように書き換えつつ空配列を返していた
  • 現在は、ハッシュを変更せず、単に「中身の無い・凍結された配列」を返す

これにより:

  • 「読み取りのみ」のパターンになる
  • 返される配列も frozen なので、Ractor 間で共有しても破壊的変更ができない設計になる

2-5. attribute_aliases / attribute_method_patterns の扱い

attribute_aliasesattribute_method_patterns も、本質的には「定義時に決まって、その後あまり変えたくない情報」です。

この PR により:

  • それぞれの戻り値となるハッシュや配列がデフォルトで freeze されるよう整理
  • AttributeMethodPattern 自体も frozen なので)attribute_method_patterns 全体が Ractor-safe になる

Rails アプリ側から見ると、これらに対して「返ってきた配列/ハッシュを破壊的に変更する」コードは元々非推奨に近い使い方ですが、この変更によって 実際に破壊的操作ができなくなる(=例外になる) ため、Ractor 対応と同時に API の利用スタイルもより明示的になります。


  1. 影響範囲・注意点

3-1. 影響を受ける主なコード

  • ActiveModel::AttributeMethods を継承して独自の attribute メタプログラミングをしているクラス
  • attribute_method_patterns, attribute_aliases, aliases_by_attribute_name を直接触っているメタプログラミング系コードや gem

特に注意が必要なのは次のようなコードです。

NG になりうるパターン例

ruby
# attribute_aliases に破壊的変更を加える
MyModel.attribute_aliases[:foo] = :bar

# or
MyModel.attribute_method_patterns << some_pattern

これらは返り値が frozen になることで FrozenError になる可能性があります。

3-2. 対処法の方針例

返ってきたオブジェクトを「そのまま破壊的に変更する」代わりに:

  • dup / to_h / to_a してから新しいオブジェクトを作成し、
  • 専用の設定メソッドや API を通じて反映する

といった形が推奨されます。

例:

ruby
# 旧: 直接破壊的に変更
MyModel.attribute_aliases[:foo] = :bar

# 新: 専用 API か、初期定義時にまとめて設定する
MyModel.attribute_alias(:foo, :bar)

attribute_method_patterns も基本的には「クラス定義時にパターンを追加する」ための public API 経由で触るべきで、内部の配列を直接いじるスタイルは避けるとよいです。

3-3. Ractor を使わない場合でも影響はあるか

  • Ractor を使わないアプリでも、「内部構造が freeze されるようになった」という意味で影響があります
  • ただし、通常の利用(public API 経由で attribute alias やメソッドパターンを定義する)であれば実害はなく、テストも追加されているため後方互換性は基本的に保たれる想定です

  1. 参考情報 (あれば)
  • PR 本体: https://github.com/rails/rails/pull/57917
  • Ractor の shareable オブジェクト要件(Ruby 3+)
    • freeze されていること
    • 参照グラフ全体が shareable であること
  • Rails 内の関連コード
    • ActiveModel::AttributeMethods(属性メソッド定義のベース)
    • ActiveRecord::Base はこれを継承しているため、全 AR モデルに波及する仕組み

この PR により、 attribute 周りの内部メタデータ構造が「読み取り専用」設計に近づき、Ractor による並行実行が現実的に扱いやすくなっています。


#57757 Make InheritableOptions ractor safe

マージ日: 2026/7/2 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    Rails の InheritableOptionsActiveSupport::OrderedOptions 系)の実装を見直し、freeze 済みで Ractor 共有可能(Ractor-safe)にできるようにした PRです。親ハッシュへのフォールバックを行うデフォルト Proc を凍結時に除去し、Ractor の shareable 制約に抵触しない仕組みに変更しています。

  1. 変更内容の詳細

背景: なぜ Ractor-safe ではなかったか

InheritableOptions は、親オプションを継承しつつ子側で上書きできる仕組みで、内部的には「キーが見つからない時に親ハッシュを見る」ためのデフォルト Proc を持っています。

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

ruby
class InheritableOptions < ActiveSupport::OrderedOptions
  def initialize(parent = nil)
    @parent = parent
    default_proc = ->(hash, key) { @parent[key] if @parent && @parent.key?(key) }
    super(&default_proc)
  end
end
  • この default_proc はインスタンス変数 @parent をキャプチャするため、
    • Proc 自体が Ractor 的に「shareable」になりにくい
    • かつ、その Proc を持つオプションオブジェクトも shareable にできない
  • Ractor.make_shareable は深くオブジェクトグラフを辿るので、「freeze したら勝手に @parent まで shareable 化する」のは副作用が大きく、freeze の責務としても不適切

そのため、「freeze 済み InheritableOptions を Ractor に渡す」ということが安全にできない状態でした。

方針: freeze 時に親参照を解消する

この PR では、

「凍結前は親を参照するが、凍結するときに親からのフォールバックを解消し、単純なハッシュとして完結させる」

という方針を取っています。

具体的には:

  1. InheritableOptions#freeze が呼ばれた時に
    • 親ハッシュの内容を「reverse merge」の形で自分自身に取り込む
      • =「自分に値がなければ親の値をコピーする」
  2. そのうえで default_proc を捨てる(親を参照しないようにする)
  3. 結果として、凍結後は「見た目は同じ値を返すが、親にアクセスしない純粋な hash-like オブジェクト」になる

擬似コードで表現すると次のようなイメージです(実装は ordered_options.rb 内):

ruby
def freeze
  unless frozen?
    if @parent
      # 親の値を自分自身に reverse_merge しておく
      @parent.each do |k, v|
        self[k] = v unless key?(k)
      end
    end

    # 親を参照する default_proc を除去
    self.default_proc = nil

    super
  end
end

※実際のコードでは OrderedOptions / InheritableOptions の構造に合わせた実装になっています。

overridden? の実装変更

InheritableOptions には概ね次のようなメソッドが存在します:

ruby
options.overridden?(:foo) # 親から継承したのではなく、子で明示的に設定されているか?

従来は「子にキーが存在するかどうか」で判定できていましたが、この PR 後は凍結時に親のキーも子へコピーされるため、そのままだと「すべて overridden 扱い」になってしまいます。

そこで、この PR では:

  • 「どのキーが子で明示的に設定されたか」を内部で追跡するように変更
    • 例: @overridden_keys のようなセットに、値を代入したタイミングでキーを記録
  • overridden?(key) は「@overridden_keys にそのキーが含まれているか」で判定するように変更

これにより、

  • 凍結前後で overridden? の意味・結果を変えずに維持
  • かつ凍結後は「親キーが子へコピーされても、それだけでは overridden と判定されない」

という動作にできます。

テスト追加

activesupport/test/ordered_options_test.rb にテストが追加されており、主に以下を検証しています:

  • Ractor-safe 化された InheritableOptions がこれまでどおりの値解決を行うこと
  • freeze 前後で overridden? の結果が変わらないこと
  • 親オプション・子オプションの関係が、凍結後も期待通りに維持されること(挙動レベルで)

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

    • ActiveSupport::InheritableOptions を直接/間接的に利用しているコード全般
      • 特にエンジンや Railtie の設定オプションなど、「親設定を継承して子で上書きする」パターン
    • overridden? を利用しているコード(Gem/アプリのメタプログラミング的な部分など)
  • 互換性

    • 通常ユースケースでは、凍結前後で返却される値は従来と一致するよう配慮されているため、挙動的な非互換は最小限
    • overridden? が「内部実装として、単に key? か否かに依存している」と仮定していたような非常に内部寄りのコードがあれば、影響を受ける可能性はありますが、そのようなコードは稀と考えられます。
  • Ractor を使う場合の注意

    • この PR により、「freeze 済み InheritableOptions を Ractor に渡す」パターンが実現しやすくなります。
    • ただし、親オプション自体や、親にぶら下がる値オブジェクトが Ractor-safe かどうかは別問題であり、アプリ側で引き続き注意が必要です。
    • freeze はあくまで「親からのフォールバックを解消し Proc を除去する」だけであり、Ractor.make_shareable を内包してはいません。そのため、凍結=完全 shareable という保証ではない点には留意してください。

  1. 参考情報 (あれば)
  • PR 本文の論点:
    • Ractor の shareable 要件と default_proc(Proc のクロージャ)が持つ参照の問題
    • freeze で勝手に Ractor.make_shareable を呼ぶことの是非(副作用の大きさと API としての責務不一致)
  • 関連するクラス/概念:
    • ActiveSupport::OrderedOptions
    • ActiveSupport::InheritableOptions
    • Ractor / Ractor.make_shareable
  • 実際に Rails アプリを Ractor-safe にする場合は、他のコンポーネント(例えば logger、キャッシュストア、クラス変数に保持している設定値など)も含めて「Proc やミューテーブルなオブジェクトがどこに残るか」を総合的に見ていく必要があります。

#57953 Mark a connection unverified when a non-StandardError interrupts a query

マージ日: 2026/7/2 | 作成者: @edaroit

  1. 概要 (1-2文で)
  • Active Record の DB コネクションプールで、StandardError 以外の例外(Timeout や fiber scheduler の Async::Stop / Async::Cancel など)でクエリが中断された場合にも、そのコネクションを「未検証状態」にマークするようにし、後続リクエストに壊れたソケットが再利用されないようにする修正です。
  • 中断されたクエリ自体の挙動(例外がそのまま飛ぶ)は変えず、「プールに戻されたコネクションの信頼性」だけを正しく扱うことで、EOFError などが後続の無関係なリクエストで発生するバグを防ぎます。

  1. 変更内容の詳細

背景となる問題

ConnectionAdapters::AbstractAdapter#with_raw_connection は内部で DB に対して「生の」クエリ実行を行い、ここで例外が起きたときにコネクション状態を調整しています。

従来の挙動:

ruby
def with_raw_connection(...)
  # 省略
rescue => e # == rescue StandardError
  downgrade_connection_after_error(e)
  raise
end
  • rescue => e は Ruby の仕様上 StandardError だけを捕捉します。
  • Timeout や、多くの async/fiber ランタイム (Async::Stop, Async::Cancel) は Exception の直下 or 途中のサブクラスであり、StandardError の外側です。
  • そのため、クエリ実行中にそうした例外で中断されると:
    • 例外は with_raw_connectionrescue をすり抜けて外側へ伝播
    • しかし、そのタイミングでコネクションオブジェクトは「正常・検証済み」としてプールに戻されてしまう
    • 次にプールからこのコネクションを checkout したリクエストが、破綻したソケットを使おうとして EOFErrorActiveRecord::ConnectionFailed を受ける
      → 一つのリクエストの中断が、後続の無関係なリクエストを壊す

特に Falcon など fiber scheduler ベースのサーバでは、キャンセルを fiber 内に Async::Stop として直接投げ込むため、このパスに頻繁に乗りやすくなります。

追加された処理

このギャップを塞ぐため、with_raw_connectionrescue Exception を追加し、「StandardError 外の例外でもコネクションを“ダウングレード”」するようにしました。

追加コード(概略):

ruby
def with_raw_connection(...)
  # 通常の処理

rescue => e # StandardError 系
  downgrade_connection_after_error(e)
  raise

rescue Exception
  @last_activity = nil
  @verified = false
  raise
end

ここで重要なのは:

  • @verified = false
  • @last_activity = nil

の2つの書き込みを行うことです。

なぜ @verified = false だけでは足りないのか

checkout 時の再検証ロジックはおおよそ次のようになっています:

ruby
elsif !@needs_reconnect && (last_activity = seconds_since_last_activity) && last_activity < verify_timeout
  # 直近で使われているなら、検証をスキップしてよし、とみなす

この分岐は「@needs_reconnect が false で、かつ最近使われた(last_activity が小さい)」場合に、検証(verify!)をスキップします。

  • @verified = false だけ変えても、
    • 「最近使われた」扱いだとこのショートカットに乗ってしまい、
    • 実際には壊れているソケットを再検証せずに再利用してしまう可能性があります。
  • そこで @last_activity = nil を一緒にクリアすることで、
    • 「最近使われた」ショートカットに入らないようにし、
    • 次の利用時に必ず検証経路に入り、そこで死んだソケットなら再接続されるようにしている。

なぜ @needs_reconnect は触らないのか

downgrade_connection_after_error は、エラーを「再試行可能な接続エラー」と判定できる場合にだけ @needs_reconnect = true を設定します。

  • 今回対象の Exception 系の割り込みは、
    • DB ドライバのエラー分類の範囲外であり、
    • 「接続エラーである」と決め打ちできる根拠がない。
  • ただし @verified / @last_activity をクリアすれば、次の利用時に verify が走り、そこで本当に再接続が必要なら @needs_reconnect が立つ or 実際に reconnect が行われます。

そのため、このパスでは「“とりあえずこのコネクションは信用しない” という最低限のフラグリセット」に留めています。

rescue Exception への懸念と既存の前例

rescue Exception は一般にアンチパターンとされますが、このファイルでは既に類似の用途に使われており、今回の変更もそれに倣っています。

  • attempt_configure_connection では Timeout::ExitExceptionException 派生)を補足するために rescue Exception を使用済み。
  • 今回も「アプリケーションコードの例外を飲み込む」のではなく、
    • 捕捉したら状態だけ直して raise し直す、という「透明な」挙動です。
    • 例外の型やスタックトレースは変えません。

テストの追加

activerecord/test/cases/adapter_test.rb に以下のようなテストが追加されています(概要):

  • あるアダプタに対して:
    1. まず普通のクエリを投げてコネクションを「使用済み・verified な状態」にする。
    2. 次に with_raw_connection の中で StandardError 外の例外を発生させる。
    3. その後のコネクション状態として:
      • @verified が false になっていること
      • @last_activity が nil になっていること をアサート。

このテストは、修正がないと失敗し(コネクションが verified のまま)、修正後は通ることが確認されています。sqlite3 と trilogy で検証済みです。


  1. 影響範囲・注意点
  • 対象:
    • Active Record 全般の DB 接続処理。
    • 特に fiber scheduler(Falcon, async-http 等)や、Timeout をクエリ実行中に使う環境で恩恵が大きいです。
  • 期待される改善:
    • StandardError の例外でクエリが中断された後に、別のリクエストで突然 EOFErrorActiveRecord::ConnectionFailed が起きる、といった「後続リクエストへの汚染」が起きにくくなります。
    • プール内のコネクションの「整合性」が高まり、障害の局所化がしやすくなります。
  • 互換性・副作用:
    • 中断されたリクエスト側の例外の種類・伝播のされ方は変わりません(あくまでコネクション状態のフラグを書き換えるだけ)。
    • StandardError を故意にクエリ中断に使っている場合、次回そのコネクション利用時に verify が必ず走るようになるため、ごく軽微ですが再接続や追加の round-trip が増える可能性があります。
    • rescue Exception を追加していますが、即座に例外を再送出しており swallowing はしていません。挙動面の変化は「コネクションが unverified になるかどうか」だけです。

  1. 参考情報 (あれば)

#57957 Bump Github Action cache version to 6

マージ日: 2026/7/1 | 作成者: @akhilgkrishnan

  1. 概要 (1-2文で)
    Rails が生成する GitHub Actions ワークフロー(CI 用)の「キャッシュバージョン」を 6 に更新した PRです。アプリケーション/プラグイン用 generator が出力する .github/workflows/ci.yml のテンプレートのみが対象です。

  1. 変更内容の詳細

対象ファイル:

  • railties/lib/rails/generators/rails/app/templates/github/ci.yml.tt
  • railties/lib/rails/generators/rails/plugin/templates/github/ci.yml.tt

これらは rails newrails plugin new 実行時に .github/workflows/ci.yml として生成されるテンプレートです。
今回の変更は、GitHub Actions のキャッシュキーに使っている「内部バージョン番号」を 5 → 6 に bump しただけの非常に小さな変更です。

イメージとしては、テンプレート中に以下のような行があるとします(実際の PR でもこれに近い形です):

yaml
- uses: actions/cache@v4
  with:
    path: vendor/bundle
    key: ${{ runner.os }}-gems-v6-${{ hashFiles('**/Gemfile.lock') }}
    restore-keys: |
      ${{ runner.os }}-gems-v6-

この v6 の部分が以前は v5 だったものを更新しています。

この「キャッシュバージョン」は、キャッシュの破棄や再生成のタイミングをコントロールするための任意のラベルで、

  • キャッシュスキーマを変えた
  • 中に入れるパス構成を変えた
  • bundle の扱いを変えた
    といったときに bump する、という運用がよくされています。

PR 説明にある followup: https://github.com/rails/rails/pull/56772 で行われた CI テンプレート関連変更(例: Bundler 周りや Ruby セットアップ周りの見直し)に続く整備として、キャッシュのバージョンだけを更新したものと考えられます。


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

    • これから rails new / rails plugin new で作成されるプロジェクト・プラグインのうち、--skip-github せず GitHub CI を生成するケースにのみ影響します。
    • 既存プロジェクトのワークフローは自動では書き換わらないため、この PR だけでは既存リポジトリには影響しません。
  • 効果

    • 新しく生成された .github/workflows/ci.yml は「バージョン 6」のキャッシュを使うので、過去に「バージョン 5」などのキーで作られていたキャッシュとは衝突せず、新しい条件でクリーンなキャッシュを作り直せるようになります。
    • Bundle パスや Ruby セットアップの変更に不整合がある古いキャッシュを誤って使い続けるリスクを減らします。
  • 注意点 / 既存プロジェクトで追従したい場合

    • Rails 7.2 以降の CI テンプレートに合わせたい場合は、既存の .github/workflows/ci.yml で、同様にキャッシュのキー中のバージョン(v5 など)を v6 に手動で変更することが推奨されます。
    • 変更後の最初のワークフロー実行では、新しいキーでキャッシュが作り直されるため、1 回目だけは依存関係のインストールにやや時間がかかる可能性があります。

  1. 参考情報 (あれば)
  • 対応する前 PR(テンプレートや CI 設定の調整などが含まれる可能性あり)
  • Rails の GitHub CI テンプレートは railties の generator で管理されており、ci.yml.ttrails new 時に展開されます。
    • app: railties/lib/rails/generators/rails/app/templates/github/ci.yml.tt
    • plugin: railties/lib/rails/generators/rails/plugin/templates/github/ci.yml.tt

#57939 Apply source mappings to dynamic permissions policy sources

マージ日: 2026/7/1 | 作成者: @55728

  1. 概要 (1-2文で)
    Permissions-Policy で「動的(callable)なソース指定」にキーワード(:self など)を返した場合も、静的指定と同様にマッピング('self' などへの変換)が適用されるようになりました。これにより、これまで動的ソースだけ不正なポリシー文字列(self のような裸の単語)が出力されていた問題が修正されています。

  1. 変更内容の詳細

何が問題だったか

Permissions-Policy(旧 Feature-Policy)の設定で、ブロック(→ Proc)などを使って「動的に」ソースを返すケースがあります。

ruby
policy.geolocation -> { :self }

このとき、:self のような「キーワードシンボル」を返しても、

  • 静的指定: policy.geolocation :self
    geolocation 'self'

  • 動的指定: policy.geolocation -> { :self }
    → 以前は geolocation self(クォートなし)になっていた

つまり、動的な場合だけ :self などのキーワードに対するマッピング処理が走らず、「素の self」が出力されてしまい、Permissions-Policy の文法上は不正なヘッダになっていました。

今回の修正内容

action_dispatch/http/permissions_policy.rb 内で、動的ソースの評価結果にも「ソースマッピング(:self'self' など)」を適用するように変更されています。

  • 静的ソースと同じ経路でマッピングされるようになり、CSP(Content Security Policy)の実装と挙動が揃えられました。
  • テスト (actionpack/test/dispatch/permissions_policy_test.rb) が追加され、動的ソースに対しても 'self' のように適切にクォートされた値が出力されることを検証しています。

挙動の具体例

修正前:

ruby
policy.geolocation :self
# => geolocation 'self'

policy.geolocation -> { :self }
# => geolocation self   # クォートなしで不正

修正後:

ruby
policy.geolocation :self
# => geolocation 'self'

policy.geolocation -> { :self }
# => geolocation 'self' # 静的と同じマッピングが適用される

他のキーワード(例: :none など)についても同様に、静的指定時と同じルールでマッピング・整形されます。


  1. 影響範囲・注意点
  • 影響を受けるのは、Permissions-Policy で ブロック / Proc などの動的ソース指定を使い、かつ :self などのキーワードシンボルを返している場合 です。
  • これまではそのようなコードが不正なヘッダ(self のようにクォートされていない単語を含む)を生成していた可能性があり、ブラウザによってはヘッダ全体が無視されていた可能性があります。
  • この修正によりヘッダが正しく解釈されるようになり、以前は実質無効化されていた Permissions-Policy が今後は有効に適用されるようになることがあります。その結果、既存アプリで以下のような挙動変化が起こり得ます:
    • geolocation, camera, microphone などの利用が、想定通りに制限され始める
    • 開発・テスト環境では問題なかったが、本番でのみ Permissions-Policy を緩く解釈していたブラウザが、修正後は厳格に解釈するように見える場合がある

確認ポイント:

  • config.action_dispatch.permissions_policy などで、Proc/ラムダを使ってソースを返している箇所を確認する:
    • -> { :self } / -> { :none } / -> { :src } などキーワードを返していないか
    • 返り値が従来から想定していた文字列表現('self', 'none', 'src' など)になることを前提に書かれていないか
  • ブラウザの DevTools で実際に送信されている Permissions-Policy / Permissions-Policy-Report-Only ヘッダを確認し、値が 'self' など適切にクォートされていること、およびポリシーが想定通りに働いていることをチェックする。

  1. 参考情報 (あれば)
  • この修正は、Content Security Policy における source mapping の挙動と整合を取る意図が明示されています。CSP での :self'self' のような扱いに慣れている場合、そのまま同じ感覚で Permissions-Policy の動的ソースも書けるようになります。
  • 実際のコード差分は小さく(+16/-1)、PermissionsPolicy クラス内のソース変換ロジックに対する一点修正と、それを保証するテスト追加が中心です。

#57938 Allow a trailing slash URL with a blank path to take query params

マージ日: 2026/7/1 | 作成者: @55728

  1. 概要 (1-2文で)
    url_fortrailing_slash: true かつ path: ""(空文字)を指定した場合に、クエリパラメータやアンカーを付与すると FrozenError が発生していた不具合を修正した PR です。
    これにより、"/" をベースに "/?a=1" のように正常にパラメータやアンカーを付与できるようになりました。

  1. 変更内容の詳細

問題の挙動

従来、以下のようなコードを実行すると:

ruby
url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 })
  • ベースのパスは "path: "" + trailing_slash: true" により "/" になる
  • その後、この "/" 文字列に対してクエリパラメータ "?a=1" を「破壊的に」連結しようとする
  • しかし、内部的に使っている文字列が freeze されたものだったため、破壊的変更ができず FrozenError: can't modify frozen String: "/" が発生

という状態になっていました。

修正内容

変更ファイル:

  • actionpack/lib/action_dispatch/http/url.rb (+1/-1)
  • actionpack/test/dispatch/request_test.rb (+6)

url_for 内部の URL 生成ロジック(ActionDispatch::Http::URL 内)で:

  • trailing_slash: true かつ path が空のケースで使われるパス文字列に対して
    クエリパラメータやアンカーを付与する際に凍結された文字列を直接破壊的変更しないように修正

実装上は、凍結された文字列をそのまま << などで変更するのではなく、
必要に応じて複製(dup)した上でクエリやアンカーを連結する形に変えたと考えられます(+1/-1 なので単純な修正)。

サンプルコード

修正前

ruby
url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 })
# => FrozenError: can't modify frozen String: "/"

修正後

ruby
url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 })
# => "/?a=1"

url_for(only_path: true, trailing_slash: true, path: "", anchor: "top")
# => "/#top"

url_for(only_path: true, trailing_slash: true, path: "", params: { a: 1 }, anchor: "top")
# => "/?a=1#top"

テスト (actionpack/test/dispatch/request_test.rb) では、
上記のようなケースが追加されているはずで、FrozenError が出ないことと、
期待する文字列が得られることを確認しています。


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

    • url_for(およびそれをラップしている *_url / *_path ヘルパなど)で
      • trailing_slash: true
      • path を空文字列("")または実質的にブランクと解釈されるケース
      • クエリパラメータ (params) またはアンカー (anchor) を付与
        という組み合わせを利用しているコードが対象になります。
    • これまではこの組み合わせが事実上「使用不能」だった(例外が出ていた)ため、
      既存アプリケーションがこの挙動に依存している可能性は極めて低く、
      変更は基本的に後方互換的で安全と考えられます。
  • 注意点

    • 今回の修正により、"/" をベースとするパスでもクエリ・アンカーを問題なく付与できるようになるので、
      「ルートパスに対してパラメータを付けたい」ケースで、path: "" + trailing_slash: true
      安心して使えるようになります。
    • 内部実装で文字列の dup などが行われている場合、微小なオブジェクト生成コストは増えますが、
      実用上は無視できるレベルと考えてよいです。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57938
  • 該当コード周辺: ActionDispatch::Http::URLactionpack/lib/action_dispatch/http/url.rb
  • 関連するヘルパー: url_for, ルーティングヘルパー(root_path, root_url など)

#57951 Improve usage of Ractor.shareable_lambda

マージ日: 2026/7/1 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    Action Mailbox 内で Ractor.shareable_lambda の使い方を、&-> を噛ませる形からブロックを直接渡す形に書き換えた、スタイル/可読性改善のための小さなリファクタリングです。挙動変更や機能追加はなく、既存の動作を保ったまま Ruby らしい書き方に揃えています。

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

PRのモチベーションとして、以下のような「悪い例」が既にコードベースに入り込んでしまっていたので、今後は推奨しない書き方に揃えたい、という説明がなされています。

ruby
# bad
shareable_lambda(&->(foo) { foo })

# good
shareable_lambda { |foo| foo }

Ractor.shareable_lambda は、Ractor(並列実行)間で共有可能なラムダを生成するためのメソッドで、actionmailbox のコールバック定義部分で使用されています。

この PR では actionmailbox/lib/action_mailbox/callbacks.rb 内の Ractor.shareable_lambda 呼び出しが、次のように変更されたと考えられます(実際のコードイメージ):

ruby
# 変更前(想定)
ActionMailbox::Callbacks.some_callback = Ractor.shareable_lambda(
  &->(inbound_email) { process_inbound_email(inbound_email) }
)

# 変更後(想定)
ActionMailbox::Callbacks.some_callback = Ractor.shareable_lambda do |inbound_email|
  process_inbound_email(inbound_email)
end

技術的な意味としては:

  • &->(foo) { foo } は「ラムダを作ってから & でブロックに変換して渡す」二段構え
  • shareable_lambda { |foo| foo } は「最初からブロックとして渡す」シンプルな形

であり、どちらも結果として shareable_lambda にブロックが渡され、そこから shareable な Proc(ラムダ)が生成される、という点では挙動は同じです。

この PR は、無駄な Proc 生成+ブロック変換をやめて、素直にブロックを渡すスタイルに統一する、というリファクタリングになります。


  1. 影響範囲・注意点
  • 機能面の影響

    • コールバックの挙動や Action Mailbox の外部 API は変わりません。
    • ラムダの引数・戻り値の扱いも変わらないため、利用側のコード修正は不要です。
  • パフォーマンス

    • &-> を経由しないため、わずかにオブジェクト生成が減る可能性はありますが、体感できるほどの差はほぼ期待できません。
    • 目的は最適化というより、スタイルと分かりやすさの改善です。
  • Ractor / スレッド安全性

    • Ractor.shareable_lambda の使い方自体は変わらないため、Ractor 間共有の安全性に関する挙動も変わりません。
  • 注意点

    • 今後同様のコードを書く際は、shareable_lambda(&-> { ... }) ではなく、ブロックを直接渡す形で統一すると、コードベースと一貫しやすくなります。
      • 例: Ractor.shareable_lambda { |mail| handle(mail) }

  1. 参考情報 (あれば)
  • Ractor.shareable_lambda について
    • CRuby の Ractor API の一つで、「Ractor 間で共有しても安全なラムダ(Proc)」を生成するためのメソッドです。
    • ブロック内で閉じ込めるオブジェクトは「共有可能 (shareable)」である必要があります(イミュータブルなオブジェクトや、.freeze 済みのオブジェクトなど)。
  • コーディングスタイル的なポイント
    • Ruby では通常のメソッド呼び出しに対し、&-> {} で Proc を渡すよりもブロックをそのまま渡す方が標準的です。
    • この PR は Rails コードベースのスタイルをそれに合わせる意図があります。

#57952 Make parameter parsers assignment ractor safe

マージ日: 2026/7/1 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    Rails のパラメータパーサー (parameter_parsers) を設定する処理が Ractor セーフになるように、代入時に渡されたハッシュを freeze するよう変更した PRです。あわせて、デフォルトパーサーの設定にパブリック API を経由せず、直接インスタンス変数に代入するようにして、不要なハッシュの複製を避けています。

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

2.1 parameters_parsers 代入時の Hashfreeze

ActionDispatch::Http::ParametersActionDispatch::Request を通して利用される)で、parameters_parsers= のようなメソッドを使ってパラメータパーサーを設定する際、渡されたハッシュをそのまま内部で変形していたため、Ractor セーフではありませんでした。

この PR では、「パラメータパーサーを代入するタイミングで、そのハッシュを freeze してからインスタンス変数に設定する」ようにしています。

イメージとしては、以下のような変更です(実際のコードは数行レベルですが、概念的なサンプル):

ruby
# 変更前(イメージ)
def parameter_parsers=(parsers)
  @parameter_parsers = parsers
  # @parameter_parsers を内部で mutate する可能性がある
end

# 変更後(イメージ)
def parameter_parsers=(parsers)
  @parameter_parsers = parsers.freeze
end

これにより、

ruby
ActionDispatch::Request.parameter_parsers = {
  Mime[:json].symbol => ->(raw) { ... }
}

のように設定すると、そのハッシュは freeze され、Rails 内部で意図せず書き換えられず、かつ Ractor から参照しても安全になります。

2.2 デフォルトパーサー設定でパブリック API を経由しない

以前のコミット(f2be45aaa8...)で、デフォルトのパラメータパーサー自体は Ractor セーフにしていたものの、そのデフォルト値を設定する際に、一度パブリックな setter を通していたため、同じハッシュの内容を二重に持つ(複製される)ような構造になっていました。

この PR では、「デフォルトパーサーを設定するときは、パブリック API (parameter_parsers= 等) を通さず、直接インスタンス変数(たとえば @parameter_parsers)に代入する」ように変更しています。
デフォルト値については Rails 側で形(キーの形式など)を保証できるため、わざわざ public API を通して整形/複製する必要がない、という判断です。

概念的には以下のイメージです:

ruby
# 変更前(イメージ)
DEFAULT_PARAMETER_PARSERS = { ... }.freeze

def initialize
  self.parameter_parsers = DEFAULT_PARAMETER_PARSERS
end

# 変更後(イメージ)
DEFAULT_PARAMETER_PARSERS = { ... }.freeze

def initialize
  @parameter_parsers = DEFAULT_PARAMETER_PARSERS
end

この変更により、

  • デフォルトハッシュの無駄な複製を避けられる
  • Ractor セーフ性を担保しやすくなる(共有オブジェクトの構造が単純になる)

という効果があります。

2.3 Proc の扱いについての考え方

PR の説明にもある通り、parameter_parsers の値として入るのは Proc-> で定義されるラムダなど)であり、これらは call されるものです。

  • Proc オブジェクトを Rails 側が勝手に Ractor.make_shareable すると、ユーザが定義したクロージャに対する制約や予期せぬエラーを招く可能性がある
  • そのため、Proc の「shareable 化」までは Rails は行わず、「ハッシュそのものを freeze する」ことで、少なくともハッシュの構造変形による非 Ractor セーフ化を防ぐ、という方針です。

Ractor 越しに parameter_parsers を共有したい場合は、ユーザ側が適切に shareable な Proc を用意する/Ractor 内で定義する必要があります。


  1. 影響範囲・注意点

  2. parameter_parsers= に渡したハッシュが freeze されるようになる

    • これまでは setter に渡したハッシュを後から変更しても動いていたコードが、今後は FrozenError になる可能性があります。

    • 例えば、以下のようなコードは動かなくなります:

      ruby
      parsers = { Mime[:json].symbol => ->(raw) { ... } }
      ActionDispatch::Request.parameter_parsers = parsers
      
      # 後から追加しようとする
      parsers[Mime[:xml].symbol] = ->(raw) { ... }  # => FrozenError
    • 対策としては、設定用のハッシュを使い回して外部から mutate しない、または、都度新しいハッシュを作って代入するようにする必要があります。

  3. Ractor を利用するアプリでの安全性向上

    • Ractor を使うマルチスレッド/マルチ Ractor 環境で parameter_parsers を共有する際に、ハッシュがミューテートされないことが保証されるため、競合状態や「shared object ではないものを Ractor 間で共有した」という類のエラーを避けやすくなります。
    • ただし前述の通り、「Proc 自体を shareable にする責任はユーザ側」にあるため、Ractor 間で共有したい場合はそこに留意が必要です。
  4. パブリック API を通さずにデフォルトを設定することによる外部 API への影響はほぼなし

    • デフォルトのパラメータパーサーは内部的な構造が少しシンプルになっただけで、挙動(JSON 等のパース結果)は変わりません。
    • ActionDispatch::Request.parameter_parsers の getter/setter といったパブリック API 自体のインターフェースは変わっていないため、通常の利用コードには影響しません。
  5. テスト・CHANGELOG について

    • この PR 単体ではテストや CHANGELOG の更新は行われていません(チェックリスト上も未チェック)。
    • 変更がかなり小さい(2行追加・2行削除)こと、および既存のテストでカバーされる範囲と判断されている可能性があります。

  1. 参考情報 (あれば)

Ractor を積極的に使う Rails アプリで、ActionDispatch::Request.parameter_parsers をカスタマイズしている場合は、設定用ハッシュが凍結されることと、Ractor 越しに共有する Proc の shareable 性を意識しておくと安全です。


#57955 Ractor safe counter cache configuration

マージ日: 2026/7/1 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    ActiveRecord のカウンタキャッシュ設定情報(_counter_cache_columnscounter_cached_association_names)をデフォルトで freeze し、Ractor(Ruby の並行オブジェクト実行モデル)でも安全に扱えるようにした PR です。これにより、Ractor 間で共有されるカウンタキャッシュ設定が不変オブジェクトとなり、スレッド/Ractor セーフティが向上します。

  1. 変更内容の詳細

何をしているか

  • ActiveRecord::CounterCache が内部的に保持している以下の情報を「初期化時に frozen にする」よう変更しています。
    • ActiveRecord::CounterCaches._counter_cache_columns
    • ActiveRecord::CounterCaches.counter_cached_association_names

これらは主に、belongs_to :foo, counter_cache: true のような関連付けに対して、「どのカラムがカウンタキャッシュとして使われているか」「どの関連がカウンタキャッシュを持つか」というメタデータを保持するための構造です。

コードレベルでのイメージ

実際の差分は小さいですが、イメージとしては次のような変更が入っています(擬似コード):

ruby
# 変更前(イメージ)
module ActiveRecord
  module CounterCaches
    def _counter_cache_columns
      @counter_cache_columns ||= {}
    end

    def counter_cached_association_names
      @counter_cached_association_names ||= Set.new
    end
  end
end

# 変更後(イメージ)
module ActiveRecord
  module CounterCaches
    def _counter_cache_columns
      @counter_cache_columns ||= {}.freeze
    end

    def counter_cached_association_names
      @counter_cached_association_names ||= Set.new.freeze
    end
  end
end

あるいは、belongs_to のビルダー側で設定を行うときに、

ruby
_counter_cache_columns[model_name] = something

のような「後から破壊的変更」をせず、
「新しい frozen ハッシュ/セットを作って差し替える」形になっている可能性があります。

いずれにせよポイントは:

  • 内部の設定オブジェクトを ミュータブルなまま共有しない
  • Ractor 間で共有できるように freeze 済みオブジェクト として扱う

という設計変更です。

テストの追加

activerecord/test/cases/counter_cache_test.rb で、主に次のようなことを検証するテストが追加されていると考えられます:

  • ActiveRecord::CounterCaches._counter_cache_columns が frozen であること
  • ActiveRecord::CounterCaches.counter_cached_association_names が frozen であること
  • カウンタキャッシュ機能自体が、freeze された設定オブジェクトのもとでも従来どおり動作すること

  1. 影響範囲・注意点

影響範囲

  • 影響を受けるのは、「ActiveRecord のカウンタキャッシュ設定情報を内部 API として直接触っているコード」です。
  • 通常の Rails アプリケーションで、以下のような典型的な使い方だけをしている場合は影響はほぼありません:
    ruby
    class Comment < ApplicationRecord
      belongs_to :post, counter_cache: true
    end
    このレベルであれば挙動は変わらず、カウンタキャッシュ機能はそのまま動作します。

注意点

  1. 内部構造への破壊的変更は不可になる

    もしアプリや gem で、内部 API に直接触れて次のような操作をしている場合は、FrozenError が発生する可能性があります:

    ruby
    # 悪い例(今後は FrozenError の可能性)
    ActiveRecord::CounterCaches._counter_cache_columns[:posts][:comments_count] = :some_value

    今後は、こうした「内部の Hash/Set を直接書き換える」前提のコードは動かなくなります。

  2. Ractor を使う場合の安全性向上

    • frozen 化により、これらの設定オブジェクトは Ractor 間で安全に共有できるようになります。
    • Ractor を利用した並列実行をしている/今後検討している Rails アプリやライブラリでは、カウンタキャッシュ周りでのデータ競合・オブジェクト共有エラーのリスクが軽減されます。
  3. カスタムパッチ・メタプログラミングに注意

    • カウンタキャッシュ周りに独自の Monkey Patch やメタプログラミングを入れている場合、
      • 「既存の Hash/Set を mutate するコード」
      • 「freeze されていないことを前提にした処理」 がないかを確認する必要があります。

  1. 参考情報 (あれば)
  • Ruby Ractor の仕様:
    • Ractor 間で共有されるオブジェクトは、基本的に immutable(freeze 済み)である必要があるため、ライブラリ側では「設定・メタデータオブジェクトを初期化時に freeze しておく」パターンが一般的になりつつあります。
  • この PR は CHANGELOG 更新が行われていないため、Rails 側では「挙動変更というより内部実装の安全性向上」として扱われていると考えられますが、内部 API を触っているコードがある場合はアップデート時に注意が必要です。

#57922 Freeze the inflection rules after the app boots

マージ日: 2026/7/1 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    このPRは、Railsアプリケーションのブート完了後に ActiveSupport::Inflector の「変換ルール(inflection rules)」を変更できないようにし、すべての言語ごとの inflections を freeze する変更です。これにより、起動後の不定な挙動を防ぎつつ、将来的な Ractor 対応も見据えたスレッドセーフ性の向上を図っています。

  1. 変更内容の詳細

2-1. 何が変わったか

  • 言語ごとに保持されている inflection インスタンス(単数・複数形変換、例外ルールなどを持つオブジェクト)を、アプリケーションブート完了時に freeze するようになりました。
  • freeze 後は、ActiveSupport::Inflector.inflections 経由でルールを追加・変更しようとすると、FrozenError が発生します。
  • Rails のブートシーケンス(ActiveSupport::Railtie)に、inflections を freeze するフックが追加されています。
  • これに対応するテストが railties/test/application/active_support_railtie_test.rb に追加されています。

2-2. 想定される処理の流れ

典型的な Rails アプリでは、config/initializers/inflections.rb などで起動時に inflection ルールを定義します。

ruby
# config/initializers/inflections.rb
ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.irregular 'person', 'people'
  inflect.acronym 'API'
end

この PR 適用後も、ブート前(初期化時)のこのコードは有効です。

アプリケーションがブートし終わると:

ruby
ActiveSupport::Inflector.inflections(:en).frozen? #=> true

となり、以降は inflections を変更しようとするとエラーになります:

ruby
ActiveSupport::Inflector.inflections(:en) do |inflect|
  inflect.irregular 'mouse', 'mice'
end
# => FrozenError (can't modify frozen ActiveSupport::Inflector::Inflections)

2-3. コードレベルのポイント (推測を含まない範囲で)

変更ファイルから読み取れる論点(PRの説明とdiffの規模から確実に言える範囲):

  • activesupport/lib/active_support/inflector/inflections.rb

    • Inflections インスタンスに対し、後から書き換えられないようにするための freeze ロジックを追加。
    • 「言語ごとに1つ存在する inflection インスタンス」をまとめて凍結する仕組みが導入された可能性が高い(説明文より)。
  • activesupport/lib/active_support/railtie.rb

    • Rails アプリケーションブート完了時(on_load または初期化プロセスの終盤)に、全 inflection インスタンスを freeze するフックが追加。
  • railties/test/application/active_support_railtie_test.rb

    • ブート後に inflections を変更できないことを確認するテストケースが追加。
    • おそらく「アプリ起動後に inflections を変更しようとして FrozenError が起きる」ことや、「ブート時に定義したルールは正しく適用される」ことを検証。

  1. 影響範囲・注意点

3-1. 影響を受けるケース

以下のようなコードを書いていると、この変更で動かなくなる/エラーが出る可能性があります。

  1. アプリケーションブート後に inflections を変更している場合

    例:

    • リクエストごとに inflection を変える
    • マルチテナントごとに inflection を動的に追加する
    • コンソール起動後に initializer 相当の処理を実行して inflection を追加している
    ruby
    # 例: Rails 起動後 / リクエスト中に
    ActiveSupport::Inflector.inflections(:en) do |inflect|
      inflect.acronym 'JSONAPI'
    end
    # => FrozenError 発生

    これらは、アプリケーションブート前(initializer)に移動する必要があります。

  2. gem やエンジンが遅いタイミングで inflections をいじる場合

    • エンジンやライブラリが to_prepare やミドルウェア・リクエスト処理の中で ActiveSupport::Inflector.inflections を変更している場合、これも FrozenError の対象となりえます。
    • gem 開発者は、inflection 変更を Rails の初期化プロセス中に移動し、ブート後に変更しない設計にする必要があります。

3-2. なぜこの仕様変更が妥当か

  • 不定なコードパスの排除
    起動後に inflection が変わると、例えば以下がリクエスト順序やタイミングによって変化する可能性がありました。

    • モデル名 → テーブル名 のマッピング
    • ルーティングのパス名
    • 他、inflector を用いる各種内部処理

    これは「あるタイミング以降でだけ挙動が変わる」ため、バグの再現や原因特定が非常に難しくなります。本PRはこれを防ぎます。

  • Ractor セーフティの向上
    Ruby 3 の Ractor を考えると、「起動後に共有オブジェクトを変更可能であること」は競合やメモリ可視性の問題を生みます。
    inflections を freeze してイミュータブルにすることで、「起動前に構築 → 起動後は読み取り専用」という Ractor 向きのモデルになります。

3-3. マイグレーション指針

  • アプリ開発者:

    • inflection の定義や変更は config/initializers/inflections.rb などのブート前処理に集約する。
    • 動的に inflection を変える設計は避け、必要なら自前ロジック(独自の変換テーブルやサービスクラス)を用意する。
  • gem / エンジン開発者:

    • Railtie / Engine の initializer ブロック内で inflections を設定し、to_prepare や実行時に inflections を再定義しない。
    • どうしても追加設定のタイミングがブート後になるなら、inflector を使わない別アプローチを検討する。

  1. 参考情報 (あれば)
  • 関連概念:

    • ActiveSupport::Inflector と inflections:
      ActiveSupport::Inflector.inflections(:en) で取得できるオブジェクトに対し、plural, singular, irregular, uncountable, acronym などでルールを定義する仕組み。
    • Ractor セーフティ:
      共有ミュータブル状態をなくし、起動前に構築したイミュータブルデータを並行に読む構造にすることで、マルチ Ractor 実行時の安全性を高める。
  • 実運用のベストプラクティス:

    • 以前からも「inflection の定義は initializer で行い、実行時に変更しない」ことが暗黙の前提/推奨だったため、その暗黙のルールを仕様として厳密に enforce した形の変更と捉えると理解しやすいです。

#57823 Initialize ActionView::LookupContext.view_context_class early

マージ日: 2026/7/1 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    ActionView::LookupContext.view_context_class をできるだけ早いタイミングで初期化することで、enable_reloading = false な環境ではミューテックスに触らずに済むようにし、Ractor からも追加セットアップなしで安全にビューコンテキストクラスを利用できるようにした変更です。

  1. 変更内容の詳細

※PR本文とファイル差分の要約ベースでの解説です(実際のコードは概念的なイメージ)。

2-1. LookupContext での view_context_class 初期化タイミングの変更

actionview/lib/action_view/lookup_context.rb において:

  • これまで:

    • view_context_class は必要になったタイミングで決定 / キャッシュされ、その際にミューテックスを用いた同期が行われていた。
    • そのため、スレッド/Ractor の利用状況によっては、view_context_class の決定時にロック処理が発生していた。
  • 変更後:

    • LookupContext の初期化時、もしくは「リロードを必要としない (enable_reloading = false)」ことが分かっている場合に、あらかじめ view_context_class を決定しておくようなロジックに変更。
    • enable_reloading = false の場合は、ビューコンテキストクラスが後から変化しない前提のため、一度決定してしまえば再度ミューテックスを取る必要がない。

その結果、enable_reloading = false の環境では:

  • view_context_class の取得がロックフリー(またはほぼロックフリー)になる。
  • Ractor からも「既に決まっている不変のクラス定義」として view_context_class をそのまま利用できる。

2-2. ActionView::Rendering 側の参照調整

actionview/lib/action_view/rendering.rb にも 1 行の変更があり、これは:

  • view_context_class の取得方法やタイミングを、LookupContext の新しい初期化タイミングに合わせて調整したものと考えられます。
  • 実質的には「ビューコンテキストを生成する際に、すでに初期化済みの view_context_class を使う」の一貫性を保つ変更です。

2-3. テスト追加

actionview/test/template/lookup_context_test.rb にテストが 16 行追加されています。

テストの狙いとしては:

  • enable_reloading = false のときに view_context_class が早期にセットされ、以降ミューテックスを介さずに参照できること。 -(間接的に)Ractor からも同じクラスを利用できる、もしくは並行実行時に問題が起きないこと。

といった挙動を担保していると考えられます。


  1. 影響範囲・注意点

3-1. パフォーマンスと並行性

  • パフォーマンス改善:
    • enable_reloading = false(本番環境が多い)で、view_context_class 取得時のミューテックス獲得が避けられるため、ビューのレンダリングにおけるロックコストが削減されます。
  • Ractor 対応の強化:
    • Ractor 内から ActionView を使う場合でも、追加のセットアップなしで view_context_class を共有しやすくなっています。
    • Ractor では「オブジェクトがイミュータブル or Ractor シェア可能」である必要がありますが、enable_reloading = false なら「クラス定義が途中で差し替わらない」ため、Ractor から扱いやすいモデルになります。

3-2. enable_reloading = true (開発環境など)の挙動

  • 開発環境では通常 enable_reloading = true のため:
    • 従来どおり、クラスの再定義やテンプレートの変更に対応する必要があり、必要な箇所ではミューテックスを使った同期が残ります。
    • この PR の主な恩恵(ロック回避・Ractor での利用容易化)は主に enable_reloading = false 側で現れます。

3-3. アプリ側コードへの影響

  • アプリコードで ActionView::LookupContext.view_context_class を直接触っていない限り、既存アプリケーションの動作は基本的に変わらない想定です。
  • もし以下のようなことをしている場合は注意が必要です:
    • view_context_class をモンキーパッチで動的にすげ替えている
    • 初回アクセス時に何か副作用を期待している
  • 今回は「初回決定タイミングがより早くなった」という性質の変更なので、view_context_class 決定時の副作用に依存しているコードがあると、タイミングずれの可能性があります(通常はそのような依存は推奨されません)。

  1. 参考情報 (あれば)

この変更は「本番運用でのロック削減」と「Rails + Ractor の相性改善」の両方に関連するため、Rails を Ruby 3 系で Ractor 活用したり、高負荷環境でのビューレンダリング性能に関心がある場合に重要なアップデートになります。


#57936 Deduplicate columns in default_order

マージ日: 2026/6/30 | 作成者: @55728

  1. 概要 (1-2文で)
    default_order に同じカラムを複数回渡した場合、ORDER BY 句に重複して出力されないよう重複排除される変更です。これにより、すでに同様の挙動をしていた orderregroup と一貫した動きになります。

  1. 変更内容の詳細

挙動の変更点

これまで:

ruby
User.default_order(:name, :name)
# ORDER BY "users"."name" ASC, "users"."name" ASC

この PR 適用後:

ruby
User.default_order(:name, :name)
# ORDER BY "users"."name" ASC

default_order に渡した引数リストから、同じカラム指定を重複して使わないように、内部的に「重複カラムの除去(デデュプリケーション)」を行うようにしています。

実装上のポイント

  • 変更ファイル: activerecord/lib/active_record/relation/query_methods.rb
    • default_order で使用するカラムリストに対して、orderregroup 同様に重複カラムをまとめる処理を追加(1行)。
  • テスト: activerecord/test/cases/relations_test.rb
    • default_order(:name, :name) のようなケースで、生成される SQL の ORDER BY 句に同じカラムが 1 回だけ現れることを確認するテストが追加(5行)。

実際には、既存の order / regroup の実装と同じような重複排除ロジック(例: uniq 的な処理)を default_order のカラム処理にも適用した形と考えられます。


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

    • default_order を使っており、同じカラムを意図的/偶発的に複数回指定しているコードで、生成される SQL が変わります。

    • 例えば、scope やモデルレベルで default_order を定義しているコード:

      ruby
      class User < ApplicationRecord
        default_scope { default_order(:name, :name, :created_at) }
      end

      これまでは name が複数回出ていたのが 1 回にまとまります。

  • パフォーマンス・動作への影響

    • ORDER BY 句の重複列が論理的な結果に違いを生むことは基本的にありません(同じ列・同じ方向での並び替えは冗長でしかないため)。
    • 従って、アプリケーションの機能的挙動が変わる可能性はほぼなく、SQL 文字列の見た目が変わる程度の影響にとどまります。
    • 一部、生成される SQL 文字列そのものを文字列比較でテストしている場合は、テストが落ちる可能性があります。その場合は、新しい形式(重複のない ORDER BY)に合わせて期待値を更新する必要があります。
  • 一貫性の向上

    • 既に orderregroup は同種の重複排除をしているため、default_order だけが異なる挙動をしていた状態が解消されます。
    • これにより、開発者は「ActiveRecord の並び替え関連メソッドは重複カラムを自動的にまとめる」という前提でコードを書けるようになります。

  1. 参考情報 (あれば)

もし既存コードで default_order を多用している場合は、テストスイートを走らせて SQL 文字列比較テストの影響有無を確認する程度で十分です。


#57856 Load ActionDispatch::Request::Session and Utils at boot time

マージ日: 2026/6/30 | 作成者: @andrewn617

  1. 概要 (1–2文で)
  • ActionDispatch::Request::Session とその内部で使われる Utils を「初回リクエスト時のオートロード」から「アプリ起動時のロード」に変更するためのリファクタリングです。
  • これにより ractor 利用時など並行環境でのオートロード問題や循環 require を避けつつ、SessionActionDispatch::Http::Session に移動して責務の位置付けを整理しています。

  1. 変更内容の詳細

背景

  • 以前の変更(rails/rails@8ce6b0c)で、ActionDispatch::Request::Session は循環 require を避けるために autoload されるようになっていました。
  • しかし autoload は「初回アクセス時に読み込む」ため、Ractor などスレッド/並行実行環境では「実行中にクラス定義が変わる」状態を招きやすく、問題になります。
  • この PR では、セッション関連クラスとユーティリティを「HTTP レイヤのクラス」として再配置し、ActionDispatch::Request 読み込み時点で一緒に require されるように変更しています。

主なリファクタリングポイント

1) ActionDispatch::Request::SessionActionDispatch::Http::Session への移動

  • 旧構成(イメージ):
ruby
# action_dispatch/http/request.rb
module ActionDispatch
  class Request
    class Session
      # セッション関連のクラス定義
    end
  end
end
  • 新構成(イメージ):
ruby
# action_dispatch/http/session.rb
module ActionDispatch
  module Http
    class Session
      # セッション関連のクラス定義
    end
  end
end
  • Request::Session はドキュメント化されていない内部 API という扱いであり、クラスの場所を変えても外部 API 互換性の問題は小さい、という判断になっています。
  • ActionDispatch::RequestActionDispatch::Http::Session に依存する側として整理され、循環参照を避ける構造になっています。

2) Utils を HTTP 層に移動し、起動時に require

  • UtilsRequest::Session と同様に HTTP レイヤで使われる内部ユーティリティとして扱い、action_dispatch/http/utils.rb に整理。
  • ActionDispatch::Http に関連するファイル (request.rb, parameters.rb, param_builder.rb など) から、Utils を明示的に require する形に変更し、起動時にロードされます。
ruby
# actionpack/lib/action_dispatch.rb
require "action_dispatch/http/session"
require "action_dispatch/http/utils"
# など Http 関連の require が追加・整理

3) autoload / require の整理

  • action_dispatch.rb, action_dispatch/http/request.rb, action_controller/metal.rb, action_dispatch/railtie.rb などで、SessionUtils の読み込み経路が調整されています。
    • これまで「最初のリクエスト時に autoload」されていたものが、
    • ActionDispatch/Http 読み込みタイミングで require されるように変更。
  • テスト (request_test.rb, session_test.rb 他) も、新しいクラス名・モジュール構成に合わせて参照先を更新しています。

4) 仕様追加・挙動変更はなし

  • 差分は主に「クラス/モジュールの配置 と ロードタイミング」であり、セッション機能自体の振る舞い(API やストアの挙動)は変えていません。
  • テストの追加・変更も挙動変更ではなく、クラスパス変更に追随するための修正が中心です。

  1. 影響範囲・注意点

影響範囲

  1. 内部 API (ActionDispatch::Request::Session) に依存しているコード

    • ドキュメント外 API ですが、もしアプリやライブラリが ActionDispatch::Request::Session を直接参照している場合、今回の変更により壊れる可能性があります。
    • 新しい正しい参照先は ActionDispatch::Http::Session になります。
  2. 初期化時のロードコスト

    • これまで「初回アクセス時にオートロード」されていた Session/Utils が「ブート時にロード」されるため、起動時間がわずかに増える可能性があります。
    • ただし、ActionDispatch 周りの他のクラスもすでにブート時にロードされており、相対的なインパクトは小さいと考えられます。
  3. Ractor / 並行実行環境での安定性向上

    • Rails プロセス起動後、アプリケーションコード実行中に Session クラスが新たに定義されることがなくなるため、Ractor やマルチスレッド環境でのオートロード競合・定義の途中読み取りなどのリスクが低減します。
    • ractor 対応や高並行処理を視野に入れている場合はプラスの変更です。

注意点・確認ポイント

  • 自前で autoload している箇所や const_get("ActionDispatch::Request::Session") のようなメタプログラミングをしている場合、クラスの場所変更により動作が変わるので確認が必要です。
  • ActionDispatch::Http::Session を直接使うつもりがない通常の Rails アプリ(コントローラレベルでしかセッションを扱わないケース)では、ほぼ影響はありません。
  • 将来的に Ractor を有効にする、あるいは Zeitwerk の strict モード・autoload 制限を強めたい場合には、この変更がその下準備の一つになっています。

  1. 参考情報 (あれば)
  • この PR の直接の動機: Ractor 対応作業中に「ActionDispatch::Request::Session が初回リクエストまでロードされない」=並行環境で問題になり得る autoload パターンであることが発覚したため。
  • 関連コミット: rails/rails@8ce6b0c
    • ここで ActionDispatch::Request::Session が autoload 化され、循環 require 回避の暫定措置が導入されていた。
  • 設計上の意味:
    • 「HTTP 関連の低レベルクラス (ActionDispatch::Http::Session, ActionDispatch::Http::Utils)」と
    • 「それを利用する高レベルな ActionDispatch::Request
    • という依存の向きが明確化され、Rails のロード順や並行実行時の安全性の面で一貫した構造になっています。

#57684 Add a Ractor-shareable ActiveSupport::TaggedLogging.shareable_logger

マージ日: 2026/6/30 | 作成者: @etiennebarrie

  1. 概要 (1–2文で)
    Ractor から安全に共有できるログ出力用 API ActiveSupport::TaggedLogging.shareable_logger が追加され、複数 Ractor / スレッドから同じ logger を使える仕組みが導入されました。内部的にはバックグラウンドスレッドに書き込み処理を委譲する ActiveSupport::ShareableLogger を tagged logging でラップする構成です。

  1. 変更内容の詳細

新しく追加された概念とクラス

2-1. ActiveSupport::TaggedLogging.shareable_logger

従来の:

ruby
logger = ActiveSupport::TaggedLogging.logger(STDOUT)

に対応する形で、Ractor 共有可能な logger を返すクラスメソッドが追加されています:

ruby
logger = ActiveSupport::TaggedLogging.shareable_logger(STDOUT)
# => ActiveSupport::TaggedLogging でラップされた ShareableLogger

特徴:

  • 返される logger は ActiveSupport::ShareableLogger::Logger のサブクラス)を内包
  • Ractor 間で共有可能になるよう設計されており、複数 Ractor から同じ logger に書ける
  • TaggedLogging によるタグ付きログ (tagged("request-id") { ... }) が通常通り使える

2-2. ActiveSupport::ShareableLogger の構成

コード的には以下のような構図になっています(ファイルは activesupport/lib/active_support/ractors/logger/*.rb):

  • Writer クラス

    • バックグラウンドで、実際の logdev(IO / ファイルなど)に書き込む専用スレッド
    • 他の Ractor/スレッドから送られてきたメッセージをキューから取り出して順次書き込む
    • 書き込み中に例外が起きた場合は:
      • 元の logger と同様に stderr にフォールバックして出力
      • 呼び出し側には例外を伝播させない(ログ出力失敗がアプリの例外にならない)
  • DeviceProxy クラス

    • Logger::LogDevice を直接触らず、その前段で「書き込み要求をキューに積む」役割
    • write, close, reopen などの操作を、Writer スレッドにメッセージとして渡す
    • これにより、アプリ側スレッド/Ractorは実ログデバイスに直接触らない

概念図:

text
[Ractor A] --\
[Ractor B] --- logger.info(...) -> DeviceProxy -> [queue] -> Writer thread -> logdev
[Ractor C] --/

2-3. エラーハンドリング

説明文にある通り、ログ書き込みエラー時の挙動は既存 Logger と同等になるように設計されています。

  • 書き込み中に IO エラーなどが発生
    • Writer 側でエラーを検知
    • stderr への書き出しにフォールバック
    • 呼び出し元 (logger.info したコード) には例外を返さない

ActiveSupport::ErrorReporter を使う案も検討されたが、それ自体が logger にフォールバックする設計のため、循環参照的になって断念、という経緯が説明されています。

2-4. Ractor 対応のためのインフラ

activesupport/lib/active_support/ractors.rb に Ractor 関連のサポートコードが追加され、それを使って:

  • Ractor 間で安全にやり取りできるオブジェクト/メッセージの制約
  • Writer スレッドとの連携

などを行う仕組みが提供されています。

2-5. テスト

  • activesupport/test/ractor_logger_test.rb(約 260 行)が追加
    • 複数 Ractor から同一 logger に書き込んでも正しくシリアライズされるか
    • flush の挙動
    • エラー時の挙動(stderr へのフォールバックなど)
  • railties/test/application/configuration_test.rb
    • Rails アプリケーション設定との統合(config 経由での利用など)が壊れていないことの確認

  1. 影響範囲・注意点

3-1. 利用シナリオ(想定)

  • Ractor ベースの並列化を行う Rails / ActiveSupport アプリケーションで、単一の logger を全 Ractor から安全に使いたいケース
  • 通常のスレッドベースの Rails アプリでも「非同期ログ書き込み」を使ってみたいケース
    • PR 説明では「通常の threaded Rails アプリでも使えるが、パフォーマンス向上があるかは不明」と明記

3-2. キューの無制限成長リスク

  • 書き込み用キューに上限がないため、ログが大量に出るエンドポイントで「Producer(アプリ側)が早く、Writer が追いつかない」状態になると、キューが肥大化しメモリを圧迫しうる
  • 緩和策:
    • 「リクエストの終わりで logger を flush する」という契約を前提としている
      • 例: Rack ミドルウェアや around_action で logger.flush 相当を呼ぶ設計を想定
  • 作者は別ブランチで「一定しきい値を超えたら同期書き込みに切り替える」backpressure 付き実装を用意しているが:
    • ractor_safe gem への依存が必要
    • そのため Rails 本体には入れず、オプション扱いに留めている

3-3. 互換性と今後の改善余地

  • 現段階では「ドキュメント未掲載の内部的/実験的 API」という位置付け
    • 公開メソッドだが、将来的な API 変更の可能性がある
  • 将来的な効果として期待されているもの:
    • Logger::LogDevice 内の mutex を無効化できる可能性(ログデバイスへの書き込みを Writer スレッドに集約するため)
    • これにより、マルチスレッド環境での logger 周りのロック競合を減らせるかもしれない

3-4. 導入時の実務的な注意点

  • Ractor を本格利用していない通常の Rails アプリで導入する場合:
    • 「本番での安定性・メモリ使用量」を慎重に観測しながらロールアウトするのが良さそう
    • ログ量が多いバッチ処理・エンドポイントでは特に、flush のタイミングとキューのサイズに注意
  • Ractor を使う場合:
    • logger を Ractor に渡す前に、Ractor-shareable であることを確認する設計にする
    • Ractor 内で logger の再構築を繰り返さず、極力共有して利用するほうがパフォーマンス上有利

  1. 参考情報 (あれば)
  • この PR の説明内で参照されている follow-up ブランチ(backpressure 付き実装案)
  • 関連するクラス/ファイル
    • ActiveSupport::TaggedLogging
      • shareable_logger が追加され、Ractor 対応 logger を生成
    • activesupport/lib/active_support/ractors/logger/writer.rb
      • バックグラウンド Writer スレッド本体
    • activesupport/lib/active_support/ractors/logger/device_proxy.rb
      • Ractor から Writer への非同期ブリッジ
    • activesupport/test/ractor_logger_test.rb
      • 具体的な利用・挙動を把握するのに有用なテストケース群

#57937 Load database config when a connection is absent from the shared section

マージ日: 2026/6/30 | 作成者: @55728

  1. 概要 (1-2文で)
    database.ymlshared セクションに存在しない接続名が環境ごとに定義されている場合、これまでは NoMethodError で落ちていたのを、「その接続は shared マージ対象外として素通りさせる」ように修正した PR です。これにより、3階層構造の database.yml をより柔軟に書けるようになり、安全にロードできるようになりました。

  1. 変更内容の詳細

何が問題だったか

3階層の database.yml を以下のように書いたケースを考えます:

yaml
shared:
  one:
    migrations_path: "db/one"

development:
  one:
    adapter: sqlite3
  two:            # `shared`にtwoが無い
    adapter: sqlite3

Rails は Rails.application.config.database_configuration を構築する際に、

  • shared セクションに同名のサブセクションがあれば、それを各環境の同名接続にマージする
  • 例: shared.onedevelopment.one にマージする

という処理を行っています。

しかし実装上、shared 側に 存在しない 接続名に対しても無条件に merge を呼んでおり、shared[:two]nil なのに merge しようとして、

ruby
NoMethodError: undefined method `merge` for nil

が発生していました。

このため、shared では一部の接続だけに共通設定を定義し、他の接続は shared 未定義のまま個別設定で使う、というごく自然な使い方ができませんでした。

どう直したか

railties/lib/rails/application/configuration.rb の shared マージ処理の中で、

  • shared 側に該当のキーがあるときだけ merge する
  • shared 側にキーが無い場合は、その接続設定は何もせず「そのまま残す」

ように条件分岐を追加しています(実質 +1/-1 の小さな修正)。

疑似コードで表すと:

ruby
# 変更前(イメージ)
merged = env_config.merge(shared_config[name]) # shared_config[name] が nil でも merge する

# 変更後(イメージ)
if shared_config[name]
  merged = env_config.merge(shared_config[name])
else
  merged = env_config # sharedに無ければそのまま
end

これにより、次のような挙動になります:

ruby
Rails.application.config.database_configuration
# Before: NoMethodError: undefined method 'merge' for nil
# After:  正常にロードされ、"two" は shared のマージを受けずにそのまま残る

テストの追加

railties/test/application/configuration_test.rb にテストが追加されており、

  • shared に存在する接続 (one) には共通設定がマージされる
  • shared に存在しない接続 (two) は例外を出さず、そのままの設定で残る

ことを確認しています。行数的に見ると、上記の YAML 構造に近い形のテストケースが1つ追加された形です。


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

    • config/database.yml で 3階層 (shared + 各環境 + 接続名) 構造を使っているアプリケーションに影響します。
    • 特に、shared が「一部の接続だけ」を定義しているプロジェクトで、これまで NoMethodError に遭遇していたケースは、この修正で解消されます。
    • shared にすべての接続名を定義していたプロジェクトでは挙動は変わりません。
  • 互換性 / 注意点

    • shared 側に無い接続名は、今後も shared の設定を一切マージされず「完全に独立した接続」として扱われます。
      • もし「本当は shared にも書くつもりだったが、書き忘れていた」ような設定ミスは、この変更では検出されず、静かに素通りします。
      • そのようなミスを検出したい場合は RuboCop などで独自ルールを追加するか、CI で database.yml の検証を自前で行う必要があります。
    • Rails 本体としては「shared に存在しない接続はマージ対象にしない」という仕様を明確化したと言えます。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57937
  • 関連しそうなキーワード:
    • 3-tier database.yml (shared / development / 接続名)
    • マルチ DB 構成 (primary, replica, readonly など)
    • Rails.application.config.database_configuration の解決ロジック

#57946 Allow the Evented LogSubscriber to be shareable

マージ日: 2026/6/30 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    Evented な LogSubscriber(および関連する StructuredEventSubscriber)の実装から「Proc を含む定数ハッシュ」を排除し、Ractor でも共有可能(shareable)になるようにリファクタリングした PR です。Ractor.shareable_proc を使わずに、設計側で Proc 依存を解消することで Ractor セーフティを高めています。

  1. 変更内容の詳細

※実際のコードを要約した概念的な説明です。

背景となる問題

  • もともと ActiveSupport::EventReporter::LogSubscriber(とそれに関連する ActiveSupport::StructuredEventSubscriber)には、
    • 「イベント名 → 処理内容」を表す 定数ハッシュ が存在
    • そのハッシュの値として Proc オブジェクト が格納されていた
  • 定数自体は freeze されていても、中に非 shareable なオブジェクト(Proc)があるため、Ractor 間で安全に共有できない状態だった。
  • Ractor.shareable_proc でラップすれば shareable にできるが、
    • そのたびにラップするのが煩雑
    • コードの可読性も下がる
      という理由で、設計レベルで Proc を定数ハッシュから排除する方向にリファクタリングしている。

主なリファクタリング内容

変更ファイルは次の2つです。

  • activesupport/lib/active_support/event_reporter/log_subscriber.rb (+10/-6)
  • activesupport/lib/active_support/structured_event_subscriber.rb (+2/-4)

おおまかな方向性は以下の通りです。

  1. 定数ハッシュから Proc を排除
    以前は、例えばこういった形だったものを(※イメージです):

    ruby
    EVENT_PROCESSORS = {
      "sql.active_record" => ->(event) { process_sql_event(event) },
      "render_template.action_view" => ->(event) { process_render_event(event) },
    }.freeze

    これを Proc を値に持たない形 に変更し、

    • シンボルや識別子だけを格納する
    • 実際の処理はメソッドディスパッチなどで行う
      という設計に変えています。例えば次のような形に近づけている可能性が高いです:
    ruby
    EVENT_PROCESSORS = {
      "sql.active_record" => :process_sql_event,
      "render_template.action_view" => :process_render_event,
    }.freeze
    
    def call(event)
      if (handler = EVENT_PROCESSORS[event.name])
        send(handler, event)
      end
    end

    こうすることで、EVENT_PROCESSORS

    • 文字列/シンボルのみで構成される
    • 完全に shareable なオブジェクト になり、Ractor 間で安全に共有できます。
  2. Evented LogSubscriber のコードパスを Ractor 安全に

    • Evented なログ購読ロジック(通知を受け取り、ログ出力や構造化イベントに変換する部分)が、
      • Ractor 内から利用される
      • かつグローバルに共有される可能性
        を考慮して、「共有しても安全なオブジェクト(Ractor.shareable)」だけを定数として持つようになっています。
    • StructuredEventSubscriber 側でも、同じ定数ハッシュや共通ロジックを参照している部分があるため、そこも Proc 非依存に合わせて微調整されています(+2/-4 程度の小修正)。
  3. Ractor.shareable_proc を使わない構成に

    • Ractor.shareable_proc は、ブロックや Proc を Ractor 間共有可能にするための仕組みですが、
      • ラップが散在しがち
      • 「どこまでが shareable でどこからが非 shareable か」が読み取りにくくなる
    • この PR では、それを使う代わりに
      • 「そもそも定数の中に Proc を入れない」
      • 「Proc 的な振る舞いはインスタンスメソッドやクラスメソッドのディスパッチに寄せる」 方針で整理しています。

  1. 影響範囲・注意点
  • 主な影響範囲
    • ActiveSupport::EventReporter::LogSubscriber
    • ActiveSupport::StructuredEventSubscriber
  • これらを直接継承・拡張しているコードが、
    • 定数ハッシュに Proc を追加・上書きしている
    • 内部の実装に強く依存している 場合は、挙動の差異や互換性に注意が必要です。
  • 公開 API としては、「イベント名に応じてログ/構造化イベントが処理される」という仕様自体は変わらない想定です。
  • この PR 自体では テストは追加・更新されていない ため、
    • Ractor を使う環境での Evented ログ出力
    • StructuredEventSubscriber 経由のイベント処理
      などを実際のアプリケーション・本番に近い環境で確認するのが無難です。
  • 今後、Ractor を前提とした並行実行(例: 複数 Ractor から ActiveSupport のイベントを購読・ログ出力する)を行う際の 前提条件 として、この変更が効いてきます。

  1. 参考情報 (あれば)
  • Ruby の Ractor と shareable オブジェクトの仕様:
  • Rails 内での関連コード:
    • ActiveSupport::LogSubscriber
    • ActiveSupport::Notifications これらと Evented LogSubscriber / StructuredEventSubscriber は密接に連携しているため、イベント処理まわりを拡張する場合はソースを一緒に読むと理解しやすいです。

#57816 Set browser binary when preloading system test driver

マージ日: 2026/6/30 | 作成者: @titusfortner

  1. 概要 (1-2文で)
    Rails の system test で Selenium ドライバをプリロードする際に、Chrome/Firefox のブラウザ実行ファイルの場所(options.binary)が設定されず「cannot find Chrome binary」で落ちる問題を修正する PR です。
    Selenium 側の仕様変更(DriverFinderoptions をミューテートしなくなった)に追従し、Rails 側で明示的にブラウザバイナリを設定するようにしています。

  1. 変更内容の詳細

背景

  • Rails の system test で Selenium ドライバを「プリロード」する際、action_dispatch/system_testing/browser.rbBrowser#preload が Selenium の Chrome::Service.driver_path / Firefox::Service.driver_path をグローバルに設定していました。
  • 以前は Selenium の DriverFinder
    • ドライバのパス解決(driver_path
    • ブラウザ実行ファイルパスの設定(options.binary) の両方を行っており、options.binary を書き換える副作用に依存していました。
  • Selenium 4.45 で DriverFinderoptions をミューテートしなくなり、その副作用が消滅。
    • Rails 側では Service.driver_path だけが設定される
    • その結果、各 worker がドライバを作るとき Selenium Manager がスキップされ、「ドライバパスだけ判明し、ブラウザパスは未設定」の状態になる
    • options.binary が空のままになり、「cannot find Chrome binary」で失敗する

この PR の対応

ポイントは「ドライバパスとブラウザパスを同じ DriverFinder から取得し、options.binary を Rails 側で明示的に設定する」ことです。

actionpack/lib/action_dispatch/system_testing/browser.rb の変更:

  • これまで:
    • Browser#preload 内で DriverFinder を呼び出して Service.driver_path を設定
    • DriverFinder が内部で勝手に options.binary も設定することに依存
  • これから:
    • 同じ DriverFinder 呼び出しで、ドライバパスとブラウザパスを両方取得
    • 取得したブラウザパスを options.binary = <resolved browser path> として明示的に代入

疑似コードイメージは以下のような形になります(実際の API 名は Selenium 実装に依存しますが、やっていることのイメージとして):

ruby
def preload
  # options は system test 用の Selenium::WebDriver::Chrome::Options など
  options = build_options

  # DriverFinder からドライバとブラウザの両方を解決
  driver_finder = Selenium::WebDriver::DriverFinder.new(browser: :chrome, options: options)
  driver_path   = driver_finder.driver_path
  browser_path  = driver_finder.browser_path # ここからブラウザパスも取得(新しく明示的に利用)

  # ドライバパスをグローバル Service に設定(従来どおり)
  Selenium::WebDriver::Chrome::Service.driver_path = driver_path

  # ブラウザバイナリを明示的に設定(新しく追加された部分)
  options.binary = browser_path

  # …以降、プリロード完了
end

※上記は PR の説明から再構成したイメージコードです。実際のメソッド名や引数は Selenium の実装に従います。

actionpack/test/dispatch/system_testing/driver_test.rb の変更:

  • テスト側で、「プリロード時に options.binary が正しく設定されること」を検証するテストが追加・更新されています。
  • 具体的には:
    • これまで「ドライバパスが解決されているか」を見ていた箇所に加えて
    • 「ブラウザバイナリパス(options.binary)も期待どおりに設定されているか」をチェックするようになっています。
  • 行数ベースでは +15/-11 程度の比較的小さな追加・修正で、既存ケースの調整+新アサーションの追加と考えられます。

  1. 影響範囲・注意点
  • 影響対象:
    • Rails の system test を Selenium(Chrome / Firefox)で動かしていて
    • config.system_testing で「ドライバのプリロード」を有効にしているプロジェクト
    • かつ Selenium 4.45 以降(DriverFinder 仕様変更後)を使っている環境
  • この PR により:
    • 「cannot find Chrome binary」などのエラーで system test が失敗していたケースが解消される見込みです。
    • ドライバとブラウザのパス解決が同じ DriverFinder によって一貫して行われるため、Selenium の >= 4.20.0 サポート範囲内で後方互換性が保たれています。
  • 注意点:
    • 独自に options.binary を上書きしている(例: 特定の Chrome/Firefox バイナリを明示指定している)場合は、Rails 側のプリロード処理との競合がないか確認するとよいです。
      • ただし、この PR は「DriverFinder が解決したブラウザパスを使う」だけなので、ほとんどのケースでは既存の期待どおりに動作します。
    • CI などで「Selenium Manager 無効化」「独自のドライバ/ブラウザ配置」をしている場合にも、DriverFinder ベースの解決に統一されるため、パス解決ロジックの挙動を一度確認しておくと安心です。

  1. 参考情報 (あれば)
  • 元の Selenium 側 Issue:
    https://github.com/SeleniumHQ/selenium/issues/17698
    DriverFinderoptions をミューテートしなくなった変更と、それにより options.binary が設定されなくなる問題の報告。
  • この PR は既存の Selenium サポート範囲(selenium-webdriver >= 4.20.0)と互換性を保つように実装されています。

#57918 Make default scopes ractor safe

マージ日: 2026/6/30 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    ActiveRecord::Base.default_scope の実装を見直し、デフォルトスコープを Ractor セーフ(スレッド/Ractor間で共有しても安全)にするために、default_scope の内部状態を freeze して不変オブジェクトとして扱うようにした PR です。加えて、この挙動を保証するテストが追加されています。

  1. 変更内容の詳細

何をしたか

  • ActiveRecord::Base.default_scope が内部的に保持している

    • 「default scope の値オブジェクト」
    • default_scopes 配列」 を、基本的に「凍結(freeze)」して不変オブジェクトとして扱うように変更。
  • default scope のブロック(proc)は instance_exec で評価されるため、ユーザーにとっても Ractor 共有可能な(Ractor.shareable? な)オブジェクトにしやすいと判断し、そこを前提に安全に共有できるようにしている。

  • default_scopes に新しい要素を追加する際は、

    1. 既存配列を dup
    2. 新しい scope を append(push)
    3. その結果の配列を freeze

    という手順で、「配列自体もイミュータブル」となるようにしている。

疑似コードイメージ(実際のコード構造を簡略化したイメージです):

ruby
# 変更前(イメージ)
def default_scope(scope = nil, &block)
  self.default_scopes ||= []
  new_scope = DefaultScope.new(scope || block)
  self.default_scopes << new_scope # 既存配列を破壊的に変更
end

# 変更後(イメージ)
def default_scope(scope = nil, &block)
  new_scope = DefaultScope.new(scope || block).freeze  # 値オブジェクトをfreeze

  # default_scopes 配列は、常に freeze された新しい配列オブジェクトに差し替える
  scopes = (self.default_scopes || []).dup
  scopes << new_scope
  self.default_scopes = scopes.freeze
end

テストの追加

activerecord/test/cases/scoping/default_scoping_test.rb にテストが追加され、主に以下のようなことが確認されていると考えられます(PR説明と統計から推測):

  • default_scope 呼び出し後に、内部で保持される default scope の値オブジェクトが frozen? == true であること。
  • default_scopes 配列自身も frozen? == true であること。
  • 複数回 default_scope を呼び出しても、配列の freeze が維持されつつ、新しい要素が正しく追加されること(dup + append + freeze の挙動が正しいこと)。

  1. 影響範囲・注意点

影響範囲

  • 対象は ActiveRecord::Base.default_scope の内部実装 およびそれに依存するクラスです。
  • 特に以下のようなケースに関わります:
    • default_scope を多用するモデル
    • Ractor を使った並列実行(Ruby 3 以降)を意識しているアプリ / ライブラリ
    • default_scope の内部状態(default_scopes 配列など)に直接アクセス・変更しているメタプログラミング的なコード

注意点

  • 破壊的変更の可能性:
    • default_scopes 配列が常に freeze されるようになるため、

      • Model.default_scopes << something
      • Model.default_scopes.push(...)
      • Model.default_scopes[0] = ...

      といった 配列を直接書き換えるコードは FrozenError で落ちる 可能性があります。

    • これらは本来 Rails が想定していない内部構造への依存なので「非推奨な使い方」ではありますが、メタプログラミングで触っている場合は注意が必要です。

  • default scope の「値オブジェクト」も freeze されるため、
    • もしライブラリ側で独自に default_scope の値オブジェクトを取り出して mutate(プロパティを書き換え)しているような場合も同様に FrozenError のリスクがあります。
  • 通常の使い方(default_scope { where(active: true) } など)をしているアプリケーションにとっては、挙動としてはほぼ影響なく、Ractor セーフになる分だけ安全性が向上します。

  1. 参考情報 (あれば)
  • Ruby の Ractor と共有可能オブジェクト:
    • Ractor.shareable?(obj)true になる条件として、「オブジェクトが再帰的にすべて frozen である」ことが重要です。今回の変更は default_scope 関連オブジェクトをその条件に近づけるものです。
  • Rails 内の類似の freeze 戦略:
    • 最近の Rails では、設定オブジェクトや Arel ノード、relation 関連オブジェクトで freeze を積極的に使うことで、Ractor セーフ / スレッドセーフな実装を進めており、その一環と見ることができます。

#57930 Only document the public ActionView::DependencyTracker API [ci skip]

マージ日: 2026/6/30 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    ActionView::DependencyTracker のうち、実際に外部から利用されている「公開API」のみを公式ドキュメントに載せるように整理した PR です。register_trackerERBTracker を正式にドキュメント対象とし、それ以外の内部実装は引き続き非公開(:nodoc:)とすることで、サポート対象のエントリポイントを明確にしています。

  1. 変更内容の詳細

2-1. 何が「公開API」として文書化されたか

この PR では以下が「公式にサポートされる public API」として RDoc/YARD などの生成ドキュメントに現れるようになります。

  • ActionView::DependencyTracker
  • ActionView::DependencyTracker.register_tracker
  • ActionView::DependencyTracker::ERBTracker

一方で、次は依然として内部用実装と位置付けられ、:nodoc: のままです。

  • find_dependencies
  • remove_tracker
  • 各種 tracker 用の正規表現(依存関係解析ロジックの詳細)
  • RubyTracker クラス

(実際のコード変更は主にこれらに対するコメント・:nodoc: の付け外しであり、仕様や挙動を変えるロジック変更ではありません。)

2-2. register_tracker の役割

register_tracker は「新しいテンプレート言語の依存関係を ActionView に教える」ための公式な拡張ポイントです。

概ね以下のような API だと考えられます(※PR 文脈と既存の coffee-rails / haml-rails からの推測であり、実際のシグネチャは Rails 本体を参照してください):

ruby
# 例: foo という独自テンプレート拡張子向けに tracker を登録する
ActionView::DependencyTracker.register_tracker(
  :foo, # テンプレートタイプ / 拡張子
  MyApp::ViewDependency::FooTracker # 依存関係解析ロジック
)

こうすることで、render の際に *.foo テンプレートから参照される partial / template などの依存関係を Rails が把握し、キャッシュの失効や再読み込みに反映できるようになります。

PR 説明でも触れられているように、すでに以下の gem がこの API を利用しています:

これらはすでに実運用されている事実上の public API でしたが、クラスごと :nodoc: になっていたため「非公開扱い」になっていました。今回それを公式に「ここが拡張ポイントです」と宣言する形です。

2-3. ERBTracker の役割

ActionView::DependencyTracker::ERBTracker

  • 「Ruby と同じような render 呼び出しを行うテンプレート言語」
    • 例: render "partial", render partial: "users/user", locals: { user: @user }
  • そのテンプレートから、どの partial / template が使われているかを解析して依存関係を抽出

するための再利用可能な tracker クラスです。

PR 説明によると、ERBTracker は「gem が自前のテンプレート言語を実装していても、その render 呼び出しが Ruby に似ているなら、そのまま使える共通の tracker」として想定されています。

例: 独自テンプレート *.foo が実質的に ERB と同じ記法で render を呼んでいるなら、

ruby
ActionView::DependencyTracker.register_tracker(:foo, ActionView::DependencyTracker::ERBTracker)

のように再利用すれば、独自に依存関係解析ロジックを書かなくてもよくなります。

今回の PR によって ERBTracker 自体の挙動は変わっていませんが、「ここを使ってよい・ここは拡張してよい」という意味での公式ドキュメントが追加され、サポート範囲が明文化されています。

2-4. 内部APIの非公開化の継続

find_dependenciesremove_trackerRubyTracker・内部の正規表現などは引き続き :nodoc: とされ、API ドキュメント生成の対象外になっています。これは

  • これらを「いつでも変更可能な内部実装」と位置付ける
  • gem 作者に対して「ここを直接触ると将来壊れる可能性が高い」とメッセージする

という意味があります。


  1. 影響範囲・注意点
  • 実行時の挙動への影響は基本的にありません
    変更はほぼドキュメントレベルであり、依存関係追跡のロジックやインターフェースのシグネチャ変更は含まれていません。

  • すでに register_tracker / ERBTracker を使っている gem / アプリは、そのまま利用継続可能です
    これまで「実質 public だがドキュメント上は private」という微妙な状態だったものが、正式に public API として認められた形です。将来的な後方互換性も、他の public API と同様のポリシーで扱われる期待が持てます。

  • 逆に、find_dependenciesRubyTracker 等の内部 API に依存している場合は要注意
    これらは今回も明確に非公開扱いとされました。

    • もし独自 gem 等で直接呼んでいる場合は、register_tracker と独自 Tracker クラス、もしくは ERBTracker の再利用に移行すべきです。
    • 将来的な Rails のマイナーアップデートで破壊的変更を受ける可能性があります。
  • ドキュメント生成時の見え方が変わります
    Rails の API ドキュメントサイト(api.rubyonrails.org 等)で、ActionView::DependencyTrackerregister_tracker / ERBTracker が見えるようになり、拡張方法がわかりやすくなります。


  1. 参考情報 (あれば)
ruby
# 独自テンプレートエンジン Foo の Tracker
class FooTracker
  # ActionView が呼び出す想定のインターフェース
  def self.call(name, template)
    # template.source を解析して render 呼び出しを拾い、
    # 依存しているテンプレートパスの配列を返す等
    # ここは Rails 本体の実装/Doc を参照して合わせる
  end
end

# 初期化時に tracker 登録
ActionView::DependencyTracker.register_tracker(:foo, FooTracker)
# もしくは render 構文が Ruby/ERB とほぼ同じなら:
ActionView::DependencyTracker.register_tracker(:foo, ActionView::DependencyTracker::ERBTracker)

独自エンジンを作る側は、今後はこのあたりが「公式なやり方」としてドキュメントで参照できるようになります。


#57932 Minor Active Support Cache refactorings

マージ日: 2026/6/30 | 作成者: @byroot

  1. 概要 (1-2文で)
    ActiveSupport::Cache 周りの内部実装を少し整理したリファクタリング PR です。instrument/ログ出力処理と、ローカルキャッシュ (LocalCache) の increment 挙動が単純化され、不要なヘルパーやメソッドが削除されています。

  1. 変更内容の詳細

2-1. ActiveSupport::Cache#instrument の整理

やったこと

  • 内部ヘルパー _instrument を削除し、instrument の処理を直接書く形に変更。
  • ログ出力において logger.level を手動で確認するのではなく、Logger#debug のブロック形式を利用するように変更。

目的・効果

  • _instrumentinstrument からしか呼ばれておらず、処理もほぼ同じだったため、メソッド分割による見通し改善よりも「追いにくさ」の方が勝っていたと判断して削除されています。
  • logger.debug { ... } 形式にすることで、デバッグログが無効なときにはブロックが評価されず、メッセージ構築コストを自動的に避けられます。
    これにより、手動で if logger.debug? のようなレベルチェックを書く必要がなくなります。

イメージコード(概念的な Before / After)

Before(イメージ):

ruby
def instrument(operation, key, options = nil)
  payload = { key: key }
  _instrument(operation, payload, options) do
    # 実処理
  end
end

def _instrument(operation, payload, options)
  if instrument_notifications?
    ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) do
      result = yield
      logger.debug("Cache #{operation}: #{payload[:key]}") if logger && logger.debug?
      result
    end
  else
    yield
  end
end

After(イメージ):

ruby
def instrument(operation, key, options = nil)
  payload = { key: key }

  if instrument_notifications?
    ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload) do
      result = yield
      logger&.debug { "Cache #{operation}: #{payload[:key]}" }
      result
    end
  else
    result = yield
    logger&.debug { "Cache #{operation}: #{payload[:key]}" }
    result
  end
end

※ 上記は概念を伝えるための擬似コードであり、実際のコードとは細部が異なります。


2-2. Local Cache (ActiveSupport::Cache::Strategy::LocalCache) の increment の簡略化

変更点

  • ローカルキャッシュ戦略における increment 実装がシンプルになった。
  • bypass_local_cache メソッドが削除された。

以前の挙動(概念)

LocalCache 戦略は、プロセス内で使う「スレッドローカルのキャッシュ」のようなもので、元のストア(例: Redis, Memcached, MemoryStore)をラップしています。

以前は、increment を行う際に、

  • 「ローカルキャッシュをバイパスして元のストアに直接 increment を送る」
    ような処理をしていたため、bypass_local_cache といったヘルパーが存在していました。

今回の簡略化後(概念)

  • increment を行うときに、わざわざローカルキャッシュをバイパスせず、よりストレートに本来のストア実装にフォワードする形に整理。
  • その結果、bypass_local_cache 自体が不要になり削除されています。

テスト側 (local_cache_behavior.rb) の変更から読み取れること:

  • increment 実行時のローカルキャッシュとの整合性や、複数回呼び出し時のカウンタ挙動などについてテストが拡充されているため、挙動は維持・明確化された上で内部構造だけがシンプルになった、という位置づけです。

2-3. その他の小さな変更

  • memory_store.rb の微修正 (+1/-1):
    ログ周り、あるいは increment 関連で統一的な呼び出しに合わせた小さなコード調整と思われます。
  • Redis ベースのキャッシュストアテスト
    • deprecated_redis_cache_store_test.rb
    • redis_cache_store_test.rb
      の両方で 1 行ずつ変更されています。主に increment の扱い、もしくはローカルキャッシュを経由した場合の振る舞い確認のための微修正です。

  1. 影響範囲・注意点
  • 外部 API の互換性

    • 公開 API (ActiveSupport::Cache::Store#increment, #fetch, etc.) の振る舞いは変えずに、内部実装を整理したリファクタリングです。
    • 直接 _instrumentbypass_local_cache に依存していなければ、通常のアプリケーションコードに影響はありません。
  • モンキーパッチ / 内部依存している場合

    • ActiveSupport::Cache に対して独自拡張・モンキーパッチをしている場合、以下を確認してください:
      • _instrument にフックしていたコードがあれば破綻します。
      • ActiveSupport::Cache::Strategy::LocalCache::LocalStore などの内部クラス/bypass_local_cache に依存していた場合も同様です。
    • 特にキャッシュミドルウェアや独自キャッシュストアを書く際に、内部メソッドに依存していると影響を受ける可能性があります。
  • ロギング周り

    • これまで logger.debug? 判定を前提にしていたようなモンキーパッチと組み合わせている場合、logger.debug { ... } 形式への移行によって挙動が微妙に変わる(ログメッセージ生成タイミングなど)可能性はありますが、通常は問題にならない範囲です。
  • テスト

    • Local Cache 挙動を検証するテストが増えているため、increment の意味的な挙動は今後もこの PR の形が前提になります。
    • redis_cache_store 関連テストも微修正されており、Redis + LocalCache 併用環境でも、想定どおりに increment が動くことが確認されています。

  1. 参考情報 (あれば)
  • Rails ガイド(キャッシュ)
    https://guides.rubyonrails.org/caching_with_rails.html
    ActiveSupport::Cache 全般の利用方法。今回の変更は主に内部実装であり、このガイドに書かれた使い方はそのまま有効です。

  • ActiveSupport::Notifications とログの連携
    多くのキャッシュ操作は cache_read.active_support などの通知イベントとして出るため、APM や独自メトリクス収集をしている場合は instrument 実装の読みやすさ向上がメンテナンス性に寄与します。


#57931 Implement ActiveSupport::ProxyLogger

マージ日: 2026/6/30 | 作成者: @byroot

  1. 概要 (1-2文で)
    RailsのActiveSupportに ActiveSupport::ProxyLogger が追加され、既存のロガーにログを委譲しつつ「別のログレベルで」フィルタリングできるラッパーロガーが使えるようになりました。これにより、アプリ全体と同じ出力先を使いつつ、特定ライブラリだけログを抑制・制御することが容易になります。

  1. 変更内容の詳細

2-1. ProxyLoggerとは何か

ActiveSupport::ProxyLogger は「あるロガー(親ロガー)」へログ出力を“丸ごと委譲”しつつ、自身が持つ独立したログレベルでログを間引くクラスです。

ruby
SomeLibrary.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error)

この例では:

  • 実際の出力先・フォーマットなどは Rails.logger をそのまま利用
  • しかし SomeLibrary から出るログは「error 以上」に制限される
    infodebug は、ProxyLogger 側で捨てられる

アプリ本体のログレベル(例: :info)と、ライブラリのログレベル(例: :error)を分けられるのが主な目的です。

2-2. 対応しているインターフェース

PR 説明によると、標準ライブラリ Logger のインターフェースのほとんどをサポートしています。典型的には以下のようなメソッドが委譲・対応されていると考えてよいです(実装ファイルは active_support/proxy_logger.rb):

  • レベル指定メソッド:
    • debug, info, warn, error, fatal, unknown
  • 汎用ログメソッド:
    • add(severity, message = nil, progname = nil, &block)
  • ログレベル関連:
    • level, level=, debug?, info?, warn?, error?, fatal?
  • その他 Logger 互換・ActiveSupport::Logger 互換のメソッドがほぼそのまま使える想定

挙動のイメージ:

ruby
# Rails.logger は通常通りアプリ全体のロガー
Rails.logger.level = :info

# noisy ライブラリはログが多いので、ライブラリ側だけ error 以上に絞る
NoisyLibrary.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error)

NoisyLibrary.logger.info("This will NOT be logged")  # ProxyLogger のレベル :error により破棄
NoisyLibrary.logger.error("Something bad")           # Rails.logger へ error として出力

親ロガー (Rails.logger) のログレベルや出力先には影響を与えず、ProxyLogger 側だけでフィルタリングしているのがポイントです。

2-3. 実装位置・その他の変更

  • 新クラス:
    • activesupport/lib/active_support/proxy_logger.rb に実装(176行追加)
  • 読み込み:
    • active_support.rbrequire が追加され、ActiveSupport をロードすれば利用可能
  • 既存 Logger 周りの微調整:
    • active_support/logger.rb が 1 行だけ削除されており、内部の読み込み順や依存関係の整理と思われます
  • テスト:
    • activesupport/test/proxy_logger_test.rb が追加され、主な挙動(ログレベル判定・委譲)がテストされています
  • ドキュメント:
    • activesupport/CHANGELOG.md にエントリ追加

  1. 影響範囲・注意点

3-1. 影響範囲

  • 新機能であり、既存コードは基本的に非互換変更の影響を受けません
  • 既存の Rails.logger / ActiveSupport::Logger の挙動も変わりません。
  • 外部ライブラリや自前コードで「ロガーを差し替えるポイント」があれば、そこで ProxyLogger を差し込むことができます。

3-2. 利用時の注意点・設計上の考慮

  1. ログレベルの二重管理

    • 親ロガーと ProxyLogger の両方にログレベルが存在します。
    • 実際に出力されるかどうかは「ProxyLogger のレベル判定を通過し」「かつ親ロガー側で弾かれない」場合のみです。
    • 例: 親が :error、ProxyLogger が :info だと、info ログは ProxyLogger 側で通っても親で捨てられます。
  2. 出力先・フォーマット変更は親ロガー側で行う

    • ProxyLogger は基本的に委譲しかしないため、
      • ログの出力先(ファイル・標準出力・外部サービスなど)
      • フォーマッタ、タグ付け、ログサブスクライバ
        などはすべて「親ロガー側」で設定・制御します。
  3. ライブラリのロガー差し替えが可能か確認

    • SomeLibrary.logger = ... のようにロガーを外から渡せる前提の仕組みを持つライブラリでのみ、この機能が直接活きます。
    • Rails の各種コンポーネント(ActiveRecord, ActionController など)や、Sidekiq など多くの gem がこのパターンを取っています。
  4. パフォーマンス観点

    • ProxyLogger で枝刈りすることにより、特に noisy なライブラリが debuginfo を大量に発行する場合に、出力やフォーマット処理の負荷を下げられます。
    • ただしログメッセージ構築自体(文字列連結など)が呼び出し元で行われている場合、そのコストは残るため、可能ならブロック形式のログ (logger.debug { expensive_computation }) を使うのが望ましいです。

  1. 参考情報 (あれば)
  • 追加されたクラス:
    activesupport/lib/active_support/proxy_logger.rb
    → 実装詳細・対応しているメソッド一覧を確認可能
  • 変更履歴:
    activesupport/CHANGELOG.md の ActiveSupport セクション
  • 利用イメージまとめ:
    ruby
    # アプリ本体
    Rails.logger.level = :info
    
    # ライブラリAはログがうるさいので error 以上だけ
    LibraryA.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :error)
    
    # ライブラリBは debug も見たい
    LibraryB.logger = ActiveSupport::ProxyLogger.new(Rails.logger, :debug)

このPRにより、「同じ出力先を共有しつつ、コンポーネントごとにログレベルを変える」というパターンが、Rails 標準の API だけでシンプルに書けるようになりました。


#57874 Make the Action View template handler registry Ractor-shareable

マージ日: 2026/6/30 | 作成者: @etiennebarrie

  1. 概要 (1-2文で)
    Action View のテンプレートハンドラのレジストリを Ractor で共有可能(Ractor-shareable)にするため、内部実装がクラス変数ベースからモジュールインスタンス変数+不変(frozen)な構造に再設計された PR です。これにより、テンプレート検索・描画パスでのハンドラ参照が Ractor セーフかつスレッド安全になり、読み取り時に書き込みが発生しないようになっています。

  1. 変更内容の詳細

2-1. レジストリの格納場所の変更

従来:

  • ActionView::Template::HandlersTemplateextend し、
  • ハンドラのレジストリ(extension → handler)は クラス変数 に保持されていました。
  • そのため、Ractor 的には共有しづらく、「読み取り時にレジストリを書き換える」ようなパターンも残っていました(遅延メモ化など)。

変更後:

  • レジストリは Template::Handlers モジュールの モジュールインスタンス変数 に保持されます。
  • Handlers モジュール自体が Ractor 間で共有され、非メイン Ractor からも「共有可能な値」として安全に読めるように設計されています。
  • Template 側の API (Template.handler_for_extension, Template::Handlers.extensions など) は、モジュール上の実装に委譲する薄いラッパーになりました。
    • Template は依然として Handlers を extend していますが、実体はモジュール側に寄せた形です。

2-2. ハンドラオブジェクトを Ractor-shareable に

Ractor 間で共有するには、保持されるオブジェクト(ハンドラ)自体も shareable である必要があります。この PR では以下のように処理しています。

  1. 組み込みハンドラ

    • 例: ERB ハンドラ、Builder ハンドラなど
    • これらのインスタンスを freeze して不変オブジェクトに。
    • 不変にすることで、Ractor 間で安全に共有可能となります。
  2. :ruby ハンドラ

    • :ruby テンプレート用のハンドラは ActiveSupport::Ractors.shareable_lambda を使って生成。
    • shareable_lambda は、Ractor 間で共有できる Proc/lambda を作るための ActiveSupport ヘルパです。
  3. アプリケーション側で登録される Proc ハンドラ

    • アプリケーションコードから ActionView::Template.register_template_handler などで登録される Proc/lambda については、
      • ActiveSupport::Ractors.try_shareable_proc を経由して登録します。
    • try_shareable_proc は、渡された Proc を shareable にできる場合はそうし、そうでなければ(たとえばクロージャで非共有な外部オブジェクトを閉じている場合など)適切に扱うためのヘルパです。
    • これにより、「アプリケーションが登録したハンドラが Ractor 的に問題を起こす」ケースを減らしつつ、できるだけ共有可能にする方針になっています。

2-3. レジストリ更新方式の変更(コピーオンライト & イager 計算)

以前の問題点:

  • Template::Handlers.extensions などが「遅延メモ化(memoization)」しており、読み取りのタイミングで内部状態を書き換える可能性がありました。
  • Ractor・並行性の観点からは、読み取りが決して書き込みを行わない構造が望ましいです。

変更後:

  • レジストリ(extension → handler の Hash)は 登録時に新しい frozen Hash を作るコピーオンライト方式に変更されました。

    • 例イメージ(実装イメージ・擬似コード):

      ruby
      def register_handler(ext, handler)
        @handlers = @handlers.merge(ext.to_s => handler).freeze
        @extensions = @handlers.keys.freeze
      end
  • Template::Handlers.extensions の内容(拡張子の一覧)も、

    • 登録時に毎回再計算して freeze
    • 読み取りは計算済みの frozen オブジェクトを返すだけ
  • これにより、「読み取り時には一切書き込みが発生しない」設計となり、Ractor/並行実行での安全性と予測可能性が向上しています。

2-4. 初期登録の場所変更 (self.extended フックを排除)

従来:

  • TemplateHandlers を extend する際の self.extended フック(モジュールの extend 時に呼ばれるフック)内で、組み込みハンドラの初期登録を行っていました。

変更後:

  • ハンドラのレジストリと登録ロジックはすべて Handlers モジュール自身に集約されました。
  • 組み込みハンドラは モジュール読み込み時に直接登録されます。
  • self.extended フックは不要となり削除されています。
    • テスト用・公開 API としての ActionView::Template.* インスタンスメソッドは、単に Handlers に委譲するだけです。

2-5. Builder ハンドラの require メモ化方式変更

ActionView::Template::Handlers::Builder では、

  • 以前は「require "builder" をしたかどうか」を インスタンス変数でメモ化していました。
  • しかし、この PR により Builder ハンドラも freeze されるため、インスタンス変数への書き込み(=メモ化)が不可能になります。

そこで、

  • require "builder" が既に実行されたかどうか」を defined? ガードで判定する方式に変えています。

    • 例イメージ(概念的なもの):

      ruby
      def call(...)
        require "builder" unless defined?(::Builder)
        ...
      end
  • これにより、ハンドラインスタンス自体は完全に不変のまま、require の多重実行を避けることができます。

2-6. テストとツールの更新

  • actionview/test/template/handlers_test.rb に、Ractor-shareable な動作や新たな登録ロジックを検証するテストが追加。
  • actionview/test/template/render_test.rb は、挙動変更に伴う小さな修正のみ。
  • tools/strict_warnings.rb でも、今回の実装変更で発生する警告に対応するための微修正が入っています。

  1. 影響範囲・注意点

3-1. Ractor/並行実行まわり

  • テンプレートハンドラのレジストリとハンドラオブジェクトが Ractor-shareable になったことで、
    • 将来の Action View の Ractor 対応
    • マルチ Ractor でのレンダリング・テンプレート解決
      への布石となります。
  • 「読み取り時に書き込みが発生しない」ようになったため、従来まれに起こり得た競合や予期しない挙動がさらに減ります。

3-2. カスタムテンプレートハンドラを定義しているアプリケーションへの影響

アプリケーション側で Proc/lambda ベースのカスタムテンプレートハンドラを登録している場合:

  • 登録処理は ActiveSupport::Ractors.try_shareable_proc を通るようになっています。
  • 一般的なハンドラ(トップレベル定義のメソッドや、外部の Ractor 非共有な状態を閉じていないラムダ)であれば、そのまま問題なく動作するはずです。
  • ただし、以下のようなケースでは Ractor-shareable にならない可能性があります:
    • クラスインスタンス変数やスレッドローカルなど、「Ractor 非共有なオブジェクト」を閉じた Proc
    • ミューテーブルな状態に依存するハンドラ
  • その場合でも動作自体は維持される想定ですが、将来の完全な Ractor 対応の観点からは、
    • ハンドラをできるだけ不変/再入可能に設計する、
    • Ractor 非共有な状態を閉じないようにする
      ことが推奨されます。

3-3. Builder ハンドラの挙動

  • Builder ハンドラは require "builder" のメモ化の仕方が変わりましたが、
    • Ruby の require 自体が多重呼び出しに耐性があること、
    • 実際には defined? によるガードで一度だけ読まれることから、
  • アプリケーション側から見た挙動に変化は基本的にありません。

  1. 参考情報 (あれば)
  • この PR で導入・利用されている Ractor 関連 API:
    • ActiveSupport::Ractors.shareable_lambda
    • ActiveSupport::Ractors.try_shareable_proc
  • 関連しそうなドキュメント/コード:
    • actionview/lib/action_view/template/handlers.rb
    • actionview/lib/action_view/template/handlers/builder.rb
    • actionview/test/template/handlers_test.rb
      → 新しいレジストリ実装の実例として読むと理解しやすいです。

#57916 Treat a Symbol stream name as a literal stream in channel test assertions

マージ日: 2026/6/30 | 作成者: @55728

  1. 概要 (1-2文で)
    Action Cable のチャンネル用テストヘルパ (ActionCable::Channel::TestCase) において、シンボルで指定したストリーム名を本番動作と同じく「文字列に対応するリテラルなストリーム名」として扱うように修正した PR です。これにより、:my_room"my_room" を使ったストリーム/アサーションの挙動が一貫し、誤検知・見逃しが起きないようになります。

  1. 変更内容の詳細

背景: 本番環境での挙動

本番では以下のようなコードは:

ruby
stream_from :my_room

内部的には "my_room" というリテラルなストリーム名に対して購読します。
つまり:

ruby
stream_from :my_room
stream_from "my_room"

は同じストリームを指します。

また ActionCable.server.broadcast も、シンボル・文字列を同等に扱うストリーム名としてサポートしています。

問題となっていたテストアサーションの挙動

ActionCable::Channel::TestCase の以下のアサーションだけが、この仕様とズレていました:

  • assert_broadcasts(:my_room)
  • assert_broadcast_on(:my_room)
  • assert_has_stream(:my_room)
  • assert_has_no_stream(:my_room)

1. assert_broadcasts / assert_broadcast_on の問題

シンボルで書いたときだけ、チャンネルスコープ付きの名前 に変換されていました。

ruby
assert_broadcasts(:my_room, 1) do
  ActionCable.server.broadcast("my_room", { text: "hi" })
end
# エラーメッセージ:
# 1 broadcasts to my_channel:my_room expected, but 0 were sent.
  • 実際に送っているのは "my_room" へのブロードキャスト
  • しかしアサーション側は my_channel:my_room という別のストリームを見てしまう
    → その結果、「0件しか送られていない」と誤判定。

文字列の場合は元々正しく動いており:

ruby
assert_broadcasts("my_room", 1) do
  ActionCable.server.broadcast("my_room", { text: "hi" })
end
# これは成功していた

2. assert_has_stream / assert_has_no_stream の問題

類似の問題がストリーム存在チェック系にもありました。

ruby
# 実際には stream_from :my_room が有効
assert_has_stream(:my_room)
# => "Stream my_room has not been started" で失敗

逆に:

ruby
# 実際には stream_from :my_room が有効
assert_has_no_stream(:my_room)
# => 何も検出できず成功してしまう(false green)

これは #57698 で指摘された assert_no_broadcasts と同じく、「テストが緑なのに本当は何も検証できていない」という危険な失敗パターンです。

具体的な修正内容

actioncable/lib/action_cable/channel/test_case.rb にて、以下のようなポリシーに統一:

シンボルで指定されたストリーム名は、テストアサーション内でも 等価な文字列として扱う

つまり:

  • :my_room"my_room" に変換し、チャンネルスコープを付けず にそのまま使う
  • これにより本番の stream_from / broadcast と同じストリーム名解釈となる

結果として、次のように書いたとき:

ruby
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from :my_room
  end
end

テストで:

ruby
assert_has_stream :my_room    # OK
assert_has_stream "my_room"   # OK

assert_broadcasts :my_room, 1 do
  ActionCable.server.broadcast(:my_room, text: "hi")
end

assert_broadcasts "my_room", 1 do
  ActionCable.server.broadcast("my_room", text: "hi")
end

といった書き方がいずれも期待どおり動きます。

テストの追加

actioncable/test/channel/test_case_test.rb に45行分のテストが追加され、主に以下をカバーしています:

  • シンボル指定の assert_broadcasts / assert_broadcast_on が、文字列指定と同じストリームを検証すること
  • シンボル指定の assert_has_stream / assert_has_no_stream も、実際のストリーム開始状況と一致して判定されること
  • 既存の文字列パスが壊れていないこと

  1. 影響範囲・注意点
  • 対象: Action Cable のチャンネルテスト (ActionCable::Channel::TestCase) を書いているコードベース

  • 本番挙動への影響: なし(本番とテストのズレを埋める変更)

  • 破壊的変更の可能性:

    • これまで「たまたま」緑になっていたテストが、本来の挙動に合わせてレッドになる 可能性があります。
    • 特に、以下のようなコードを書いていたプロジェクトは要注意です:
      ruby
      # 実際には my_channel:my_room にしかブロードキャストしていない
      stream_from "my_channel:my_room"
      
      # しかしテストで :my_room を想定していた
      assert_broadcasts :my_room, 1 do
        ActionCable.server.broadcast("my_channel:my_room", payload)
      end
      以前は「たまたま」 :my_roommy_channel:my_room にスコープされて通っていたものが、
      今後は "my_room" を指すようになるため失敗します。
    • その場合は、明示的に望むストリーム名を文字列で指定する か、
      テストと実装のストリーム名を揃える(:my_room / "my_room" いずれかに統一)必要があります。
  • ベストプラクティス:

    • 本番コードでシンボルを使っているなら、テストも同じシンボル/文字列に揃える
    • 「チャンネル名を含んだフルネーム」をあえて使いたいなら、必ず文字列で明示する:
      ruby
      stream_from "chat_channel:my_room"
      assert_broadcasts "chat_channel:my_room", 1 do
        # ...
      end

  1. 参考情報 (あれば)
  • PR本体: #57916 “Treat a Symbol stream name as a literal stream in channel test assertions”
  • 関連PR:
    • #57698: テストアダプタの assert_no_broadcasts におけるシンボル/文字列非対称問題の修正
    • #57703, #57704: 同じファイル内の周辺レイヤーでの、シンボル vs 文字列非対称を個別に修正した PR(本PRで統合)

#57912 Reference ActionCable::Configuration in the Action Cable guide

マージ日: 2026/6/30 | 作成者: @55728

  1. 概要 (1-2文で)
    Action Cable ガイド内の「Configuration」セクションで参照していたクラス名を、古い ActionCable::Server::Configuration から現在の正規名 ActionCable::Configuration に修正したドキュメント更新です。バックワードコンパチ目的のエイリアスは残っていますが、ガイド上は新しい正規クラス名を指すようになりました。

  1. 変更内容の詳細

対象ファイル:

  • guides/source/action_cable_overview.md

変更内容は 1 行のみで、Action Cable の設定項目一覧へ誘導する箇所のクラス名を修正しています。

従来 (概念的なイメージ):

md
See `ActionCable::Server::Configuration` for the full list of configuration options.

修正後:

md
See `ActionCable::Configuration` for the full list of configuration options.

つまり、ドキュメント中で「全設定オプションの一覧は ActionCable::Server::Configuration を見てください」と案内していたのを、「ActionCable::Configuration を見てください」という案内に更新しただけの変更です。

Rails 本体側では、ActionCable::Configuration がトップレベルの正規クラスであり、ActionCable::Server::Configuration は互換性維持用のエイリアスとして残っている状態になっている前提です。この PR は、その実装にドキュメントを追従させたものです。


  1. 影響範囲・注意点
  • 影響対象:
    • 公式ガイドを見て設定方法を確認する開発者
      ガイドに書かれているクラス名が、実際のコードベースが採用している正規の名前 (ActionCable::Configuration) と一致するようになります。
  • 実コードへの影響:
    • コードの挙動や API 仕様そのものは変更されていません。
    • ActionCable::Server::Configuration は引き続きエイリアスとして存在するため、これを参照している既存コードが壊れることはありません。
  • 注意点:
    • 新規実装やリファクタリング時には、今後は ActionCable::Configuration を前提にコードやドキュメントを書くのが推奨されます。
    • 既存の記事・ブログ・社内ドキュメントなどで ActionCable::Server::Configuration を紹介している場合、Rails の現行バージョンに合わせて表記だけ ActionCable::Configuration に更新するのが望ましいです。

  1. 参考情報 (あれば)
  • Action Cable ガイド (Configuration セクション):
    guides/source/action_cable_overview.md の設定セクション
  • 関連する実装 (推定箇所):
    • ActionCable::Configuration クラス定義
    • ActionCable::Server::Configuration = ActionCable::Configuration のようなエイリアス定義 (互換性維持用)

#57928 Eliminate a useless frame in AS::Notifications.instrument

マージ日: 2026/6/30 | 作成者: @byroot

  1. 概要 (1-2文で)
    ActiveSupport::Notifications.instrument 内の「無駄なスタックフレーム」を取り除き、スタックトレースやフレームグラフが少し読みやすくなるようにした変更です。性能への影響はほぼありませんが、デバッグやプロファイリング時の視認性が向上します。

  1. 変更内容の詳細

対象ファイル:

  • activesupport/lib/active_support/notifications.rb (+2/-2)

ActiveSupport::Notifications.instrument の実装から、実行上ほとんど意味を持たない「余計なメソッド呼び出し」や「余計なブロックラップ」を削り、スタックフレームを 1 段減らしています。

典型的には、以下のような構造になっていたものを:

ruby
def instrument(name, payload = {})
  if notifier.listening?(name)
    _instrument(name, payload) do
      yield
    end
  else
    yield
  end
end

def _instrument(name, payload)
  # 実際の計測・通知処理
end

これを、余計な中継レイヤをなくすような形に寄せています:

ruby
def instrument(name, payload = {})
  if notifier.listening?(name)
    # ここで直接計測処理を行う or 余計なラップを減らす
    notifier.start(name, payload)
    begin
      yield
    ensure
      notifier.finish(name, payload)
    end
  else
    yield
  end
end

実際の差分は +2/-2 行と非常に小さいもので、主な意図は「メソッドのネストを 1 段浅くする」「ブロック経由の無駄なコールを消す」ことで、結果的に:

  • 例外発生時のバックトレースに出てくるフレームが 1 つ減る
  • stackprof / rbspy / flamegraph などのプロファイリングツールでのコールスタック表示が少し簡潔になる

という効果があります。


  1. 影響範囲・注意点
  • 公開 API (ActiveSupport::Notifications.instrument) の挙動やシグネチャには変更なし
  • 通知されるイベント名・payload・タイミングも同一で、機能的な互換性は保たれています
  • 変更はごく小さく、かつ「中継用のフレーム削除」にとどまるため、通常のアプリケーションコードが影響を受ける可能性はほぼありません
  • backtrace を前提にした特殊な解析ツールや、スタックフレーム数に強く依存しているようなテストを組んでいる場合のみ、フレーム 1 つ分の違いが出る可能性があります

  1. 参考情報 (あれば)

#57919 Make LogSubscriber.log_levels ractor safe

マージ日: 2026/6/30 | 作成者: @andrewn617

  1. 概要 (1-2文で)
  • ActiveSupport::LogSubscribersubscribe_log_level が Ractor(マルチスレッド/マルチ Ractor)環境でも安全に動作するように、内部で持っている log_levels ハッシュをフリーズするようにした PR です。
  • これにより、Ractor 間で LogSubscriber を共有してもログレベル設定が原因でエラーやデータ競合が起きにくくなります。

  1. 変更内容の詳細

変更の主旨

元々、LogSubscriber には以下2点がありました:

  • ログレベル判定用の定数 LEVEL_CHECKS はすでに freeze 済み
  • しかし、各サブスクライバごとに保持している log_levels ハッシュはミューテーブルであり、Ractor セーフではない

この PR では、subscribe_log_level 呼び出し時に、この log_levels をフリーズするようにしています。すなわち、

  • ハッシュを更新してから freeze する
  • あるいは更新結果として新しい frozen ハッシュを持つようにする

という形を取ることで、Ractor が要求する「オブジェクトが不変であること」の条件を満たします。

具体的なイメージ

元のコード (イメージ):

ruby
def subscribe_log_level(*log_levels)
  # self.log_levels は Hash
  self.log_levels.merge!(derived_log_levels_from_args(log_levels))
end

Ractor では、Ractor 間で共有されるオブジェクトは基本的に frozen? である必要があるため、このままだと:

  • ある Ractor で log_levels を更新
  • 別の Ractor と共有しているときに Ractor セーフではなくなる

といった問題が起こりえます。

この PR では、以下のような形の変更がなされていると考えられます(正確なコードは PR 本文では省略されていますが、内容からの推測です):

ruby
def subscribe_log_level(*log_levels)
  self.log_levels = self.log_levels.merge(derived_log_levels_from_args(log_levels)).freeze
end

ポイント:

  • merge! のような破壊的更新ではなく、新しい Hash を merge で生成
  • 生成した Hash を freeze
  • log_levels には常に frozen Hash を持たせるようにする

テスト (activesupport/test/log_subscriber_test.rb) も追加されており:

  • subscribe_log_level 実行後に log_levelsfrozen? であること
  • 複数回呼び出しても正しいマージ結果と frozen 状態が保たれていること

といったことが検証されていると考えられます。


  1. 影響範囲・注意点
  • 影響を受けるのは ActiveSupport::LogSubscribersubscribe_log_level を利用しているコードのみです。

  • log_levels が常に frozen Hash になるため、公開 API から log_levels を直接破壊的に書き換えていた場合はエラーになります

    例(これまでなんとなく動いていたが、今後は壊れる可能性があるコード):

    ruby
    subscriber = MyLogSubscriber.new
    subscriber.subscribe_log_level(:info)
    
    # こういった破壊的更新は frozen になった Hash に対しては例外を投げる
    subscriber.log_levels[:debug] = true     # => FrozenError
    subscriber.log_levels.merge!(...)        # => FrozenError

    望ましい書き方としては、subscribe_log_level の API を通して設定するか、どうしても差し替えたい場合は新しい Hash を作って setter に渡す形です:

    ruby
    subscriber.log_levels = subscriber.log_levels.merge(debug: true).freeze
    # ただし、このような使い方自体も将来の仕様変更に脆いので、可能なら避ける
  • LogSubscriber を Ractor 間で共有するケース(あるいは Rails ロガー周りを Ractor 内で使うケース)では、今回の変更により Ractor セーフティが向上します。

  • CHANGELOG の更新は行われていないため、「挙動変更」というよりは「安全性向上のための内部的変更」という扱いになっています。


  1. 参考情報 (あれば)
  • Ruby の Ractor 仕様上、Ractor 間で共有されるオブジェクトは原則として immutable(freeze)である必要があります。
  • 以前から LEVEL_CHECKS 定数は freeze 済みでしたが、今回の PR によって log_levels も同様に不変化され、LogSubscriber 全体として Ractor フレンドリーな設計に近づいています。
  • マルチ Ractor 対応を見据えた Rails の内部改善の一環と位置付けられます。

#57920 Eagerly assign ExecutionWrapper's active key

マージ日: 2026/6/30 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    ExecutionWrapper のサブクラスにおける active_key の代入タイミングを「継承時」に早めることで、最初のリクエストが非メイン Ractor で処理された場合に発生する Ractor の隔離エラーを防ぐ修正です。Ractor 対応を強化するための内部的な初期化順序の見直しになります。

  1. 変更内容の詳細

背景

Ruby の Ractor では、「メイン Ractor 以外からクラスインスタンス変数を初期設定する」といった操作に制約があり、適切に扱わないと「ractor isolation error(Ractor の隔離違反)」が発生します。
ExecutionWrapper は Rails の実行コンテキスト(ミドルウェア的な周辺処理)を扱うためのクラスで、そのサブクラスごとに active_key というクラスインスタンス変数を持ちます。

従来は、この active_key が「必要になったタイミング(=リクエスト処理時など)」で初期化されていたため、最初のリクエストがメイン Ractor ではない Ractor で処理されるケースで Ractor の制約に引っかかる可能性がありました。

今回の変更

  • active_key の代入を 「サブクラス定義時 (継承フック inherited 内)」に移動 しました。
  • これにより、ExecutionWrapper を継承したクラスがロードされた瞬間、まだメイン Ractor 上にいる段階で active_key が確実にセットされます。

実装イメージ(簡略化した擬似コード):

ruby
class ActiveSupport::ExecutionWrapper
  class << self
    # もともとどこか別のタイミングで行っていた active_key 代入を
    # 継承時フックに移したイメージ
    def inherited(subclass)
      super
      subclass.instance_variable_set(:@active_key, :"active_#{subclass.name}")
    end
  end
end

※上記はあくまで概念的なサンプルです。実際のキー名や代入方法はコードに依存しますが、ポイントは「inherited フック内でサブクラスのクラスインスタンス変数をセットする」ことです。


  1. 影響範囲・注意点
  • 主な影響範囲
    • ActiveSupport::ExecutionWrapper を継承しているクラス(およびそれを利用するコード全般)
    • マルチ Ractor 環境で Rails を動かしているアプリケーション
  • 期待される効果
    • 最初のリクエストがメイン Ractor 以外で処理された場合でも、active_key に関する Ractor 隔離エラーを防げる。
    • ExecutionWrapper の初期化がより決定的(deterministic)になり、Ractor 利用時の不定動作の可能性が下がる。
  • 互換性・既存コードへの影響
    • active_key の値や外部 API が変わるわけではなく、「いつセットされるか」だけが変わるため、通常のアプリケーションコードへの影響はほぼありません。
    • ExecutionWrapper のサブクラス化や @active_key に対してメタプログラミングを行っているような高度なコードがある場合は、初期化タイミングの違いに注意してください(ただしそのようなケースは稀と思われます)。
  • パフォーマンス
    • 継承時にクラスインスタンス変数を一つセットするだけなので、事実上無視できるレベルのオーバーヘッドです。

  1. 参考情報 (あれば)
  • Ruby Ractor の制約として、「Ractor 間で共有されるオブジェクトや、Ractor 境界を跨いだミューテーション」に厳しい制約があり、クラスインスタンス変数の扱いにも注意が必要になります。
  • inherited フックは Ruby における標準的なメタプログラミングフックで、class Child < Parent; end のようにサブクラスが定義されたときに Parent.inherited(Child) が呼ばれます。
    今回はこの仕組みを利用して、「サブクラスロード時に Ractor セーフな形で内部状態 (active_key) を初期化する」ようにしています。