Skip to content

Ruby on Rails PR Digest - 2026年 3月

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

#57096 Ignore schemas when validating table name lengths for PostgreSQL

マージ日: 2026/3/30 | 作成者: @fatkodima

  1. 概要 (1-2文で)
    PostgreSQL でテーブル名の長さを検証する際に、schema_name.table_name のようなスキーマ部分を含めず、純粋なテーブル名だけで長さチェックするように修正した PRです。これにより、スキーマ名を含めたフル修飾名が 63 文字を超えていても、テーブル名自体が PostgreSQL の制限以内ならマイグレーションが通るようになります。

  1. 変更内容の詳細

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

PostgreSQL には「識別子は 63 文字まで」という制限がありますが、この PR が入る前は、ActiveRecord がテーブルを作成する際に、

ruby
"schema_name.table_name"

のようなスキーマ付きフルネーム全体を対象に長さチェックをしていました。

そのため例えば、

  • スキーマ名: very_long_schema_name
  • テーブル名: not_so_long_table

のように、テーブル名自体は短くても、very_long_schema_name.not_so_long_table というフルネームが 63 文字を超えると、ActiveRecord 側で「長すぎる」と弾かれてしまうケースがありました。
しかし PostgreSQL 自体は、識別子制限をテーブル名部分に対して適用する(スキーマ名とテーブル名はそれぞれ別の identifier)ので、これは Rails 側の過剰な制約でした。

2-2. 修正内容 (PostgreSQL アダプタ)

変更ファイル:
activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb (+5)

ここにある「テーブル名の長さを検証するロジック」が修正されています。

新しいロジックの要点:

  • schema.table 形式で渡される名前から、スキーマ名を切り離して、テーブル名部分だけを取り出す
  • PostgreSQL の識別子長制限(通常 63 文字)と比較する際には、このテーブル名だけを対象とする。
  • スキーマ部分は、長さチェックの対象から完全に除外。

擬似コードで表すと、だいたい次のようなイメージです(実際のコードとは多少異なりますが、意図を示すため):

ruby
def validate_table_length(name)
  schema, table = name.to_s.split(".", 2)
  table_name = table || schema # スキーマ指定なしならそのまま

  if table_name.length > max_identifier_length
    raise ArgumentError, "Table name '#{table_name}' is too long"
  end
end

2-3. テストの追加・変更

変更ファイル:
activerecord/test/cases/migration_test.rb (+6/-1)

テストでは、おそらく以下のようなケースがカバーされています:

  • スキーマ名が長く、テーブル名は制限以内のケースを用意
  • それを用いた create_table "long_schema.short_table" のようなマイグレーションがエラーにならず成功することを検証
  • 逆に、テーブル名自体が長すぎる場合にのみエラーになることを確認

イメージ例:

ruby
def test_long_schema_does_not_affect_table_name_length_validation
  long_schema = "a" * 40
  short_table = "b" * 30 # PostgreSQL の制限 63 文字以内

  # 以前は long_schema.short_table の全体長でエラーになっていたが、
  # 今は short_table 部分のみでチェックするため成功する
  assert_nothing_raised do
    create_table "#{long_schema}.#{short_table}" do |t|
      t.string :name
    end
  end
end

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

    • activerecord の PostgreSQL アダプタを使っているアプリケーション
    • 特に 複数スキーマを使っている場合や、スキーマ名が長くなりがちな設計で影響が出ます。
  • 期待できる改善:

    • これまで「テーブル名が長すぎる」として Rails に弾かれていたが、PostgreSQL 的には合法だったテーブル作成が問題なく通るようになります。
    • 既存のアプリで、長いスキーマ名を使ってマイグレーションが失敗していたケースが解消される可能性が高いです。
  • 注意点:

    • この変更は バリデーションの対象を PostgreSQL の実仕様に合わせたものであり、緩め過ぎているわけではありません。
    • テーブル名自体が 63 文字(DB の max_identifier_length)を超えていれば、これまで通りエラーになります。
    • もしアプリ側で「フルネーム(schema.table)の長さを独自ルールで制限したい」場合は、この PR とは別にアプリケーションレベルで検証を入れる必要があります。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57096
  • 関連 Issue: Fixes #57093(テーブル名長チェックでスキーマ名まで含めてしまう問題の報告)
  • PostgreSQL 公式ドキュメント(識別子長制限):
    • "Identifiers and Key Words" — NAMEDATALEN によりデフォルト 63 文字(コンパイル時定数)として制限される仕様が説明されています。

#57097 ActiveStorage: skip associated tests when a dependency is missing

マージ日: 2026/3/30 | 作成者: @byroot

  1. 概要 (1-2文で)
    Active Storage の一部テスト(動画アナライザ/プレビュー、MuPDF プレビュー)について、必要な外部依存ライブラリがインストールされていない環境ではテスト自体をスキップするようにした変更です。これにより、Active Storage の小変更を行う際に、全ての外部ツールをローカルに入れなくてもテストが実行しやすくなります。

  1. 変更内容の詳細

変更対象は以下の3ファイルです。

  • activestorage/test/analyzer/video_analyzer_test.rb
  • activestorage/test/previewer/mupdf_previewer_test.rb
  • activestorage/test/previewer/video_previewer_test.rb

いずれも「関連する外部コマンドやライブラリが無い場合はテストをスキップする」というガードを追加する修正です。
実際のコードは概ね次のようなパターンになっていると考えられます(あくまでイメージです):

ruby
# 例: video_analyzer_test.rb

require "test_helper"

return unless ActiveStorage::Previewer::VideoPreviewer.enabled?

class ActiveStorage::Analyzer::VideoAnalyzerTest < ActiveSupport::TestCase
  # テスト本体
end

あるいは、skip を用いる形式の可能性もあります:

ruby
class ActiveStorage::Analyzer::VideoAnalyzerTest < ActiveSupport::TestCase
  setup do
    skip "ffmpeg is not installed" unless ActiveStorage::Analyzer::VideoAnalyzer.accept?(fixture_file)
  end

  # テスト本体
end

MuPDF のプレビューや動画プレビュー用テストについても同様に、

  • MuPDF(mutool など)
  • 動画処理系ツール(ffmpeg など)
    といった外部依存ツールが無い場合はテストを実行せずスキップするようにしています。

ポイント:

  • 「依存がないと落ちるテスト」から「依存がないと実行されないテスト」へと振る舞いが変わっています。
  • 実装としては return unless ...skip unless ... のようなガードがそれぞれのテストファイル/テストクラスに数行追加された形で、既存のテストロジック自体には手が入っていません。

  1. 影響範囲・注意点
  • 開発ローカルでのテスト実行が楽になる
    Active Storage 本体や周辺コードを軽く触るだけの場合、MuPDF や ffmpeg 等をインストールしていなくても、テストスイート全体がエラーで止まらずに進むようになります。

  • CI 環境では依存を入れておかないとテストが実行されない
    これらのテストは「依存が満たされていないとスキップされる」ため、

    • 「動画プレビュー/MuPDF プレビュー機能をちゃんと継続的に検証したい」
      という場合には、CI ジョブで必要なバイナリ・ライブラリを必ずインストールするようにしておく必要があります。
      依存を入れていないと「グリーンだけど実はその機能はテストされていない」状態になり得ます。
  • 機能自体の挙動変更は無し
    変更はテストコードのみであり、Active Storage の本番コード(アナライザ、プレビューアの実装)そのものの挙動は変わっていません。そのため、既存アプリのランタイム挙動に直接の影響はありません。


  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57097
  • Active Storage のアナライザ/プレビューアがどの依存に基づいて有効化されるかは、Rails ガイドおよび該当クラス(ActiveStorage::Analyzer::VideoAnalyzer, ActiveStorage::Previewer::VideoPreviewer, ActiveStorage::Previewer::MuPDFPreviewer など)の accept? / enabled? ロジックを参照すると把握しやすいです。
  • CI でこれらのテストを有効にしたい場合は、Docker イメージやビルドスクリプトで ffmpeg, mutool などをインストールしておくのが一般的です。

#57094 Delay engine route building

マージ日: 2026/3/30 | 作成者: @byroot

  1. 概要 (1-2文で)
    Rails Engine が routes do ... end を評価するタイミングを遅延させ、make_routes_lazy 最適化が正しく効くようにした変更です。
    これにより、graphql など Engine 内で直接 routes do を呼んでいるライブラリでも、ルーティングの遅延構築によるパフォーマンス改善が適用されます。

  1. 変更内容の詳細

背景

以前の PR (#52353) で、Rails のルーティングを「遅延構築 (lazy)」する最適化が導入されました。
しかし Engine 側で以下のように直接 routes do を呼んでいる場合、そのブロックが make_routes_lazy 呼び出し前に評価されてしまい、結果としてルートが即時に構築されていました。

ruby
class SomeEngine < ::Rails::Engine
  routes.draw do
    # ここがすぐ評価されて RouteSet がフル構築されてしまう
    get "/status", to: "status#show"
  end
end

graphql のダッシュボードエンジンもまさにこのパターンです(PR 説明にリンクあり)。

今回の対応

今回の PR では「Engine が routes do ... end を宣言した時点では評価せずに蓄えておき、route_set_class が確定した後にまとめて評価する」ように変更しています。

ポイント:

  • Rails::Engine 内で:
    • routes(&block) が呼ばれたときに、すぐにブロックを実行して RouteSet を構築するのではなく、ブロックを保持しておくように変更。
    • route_set_class が設定され、make_routes_lazy などルーティングの最適化が完了した後に、その保持しておいたブロックを評価して RouteSet に反映するようにした。

疑似コードイメージとしては:

ruby
class Rails::Engine
  def routes(&block)
    if block_given?
      # 以前: ここですぐに draw / eval されていた
      # 今回: ここでは「登録」だけにする
      pending_route_blocks << block
    else
      # RouteSet の取得は従来どおり
      @routes ||= route_set_class.new
    end
  end

  def configure_routes!
    # route_set_class, make_routes_lazy などの設定が完了した後に呼ばれる
    pending_route_blocks.each do |blk|
      @routes ||= route_set_class.new
      @routes.draw(&blk)
    end
  end
end

実際の実装は Rails の内部構造に合わせた書き方になっていますが、概念的には「routes do に渡されたブロックを一旦キューに入れ、ルートセットのクラスや lazy 設定が整ってから処理する」変更です。


  1. 影響範囲・注意点

主な影響範囲

  • Rails::Engine を継承している Engine 全般
  • 特に、Engine 定義内で routes do ... end を直接呼んでいるコードgraphql-ruby のダッシュボードエンジンなど)
  • Rails ルーティングの lazy 構築機能 (make_routes_lazy) の挙動

期待される効果

  • これまで make_routes_lazy が効いていなかった Engine でも、ルート構築が遅延されるようになり、アプリ起動時のオーバーヘッドが軽減される可能性があります。
  • エンジン作者が特別な対応をしなくても、routes do を普通に書くだけで最適化の恩恵を受けられます。

互換性・注意点

  • routes do ブロックが「Engine 定義の評価タイミング」ではなく「Engine のルートセットが最終的に構成されるタイミング」で評価されるようになります。
  • 通常のルーティング宣言であれば問題ありませんが、ルート定義ブロック内で Engine のクラス状態やグローバル状態に依存した副作用的処理をしているようなコードがある場合、評価タイミングの違いによる影響が出る可能性があります。
    • 例: routes do ... end の中で SomeEngine.config を直接書き換える、といったイレギュラーなコードがある場合など。
  • ただし、今回の変更はあくまで「route_set_class の設定完了後に評価する」程度の遅延であり、一般的な用途の範囲では互換性に問題は出にくい設計です。

  1. 参考情報 (あれば)

#57090 Fix duplicated word in configuring guide

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

  1. 概要 (1-2文で)
    Railsガイド「Configuring Rails Applications」の config.action_on_early_load_hook に関する記述から、誤って重複していた "when" という単語を削除するドキュメント修正PRです。アプリケーションコードや挙動には一切影響せず、ガイド文言のみが整備されています。

  1. 変更内容の詳細
  • 対象ファイル: guides/source/configuring.md
  • 変更内容: 1行のみの軽微なテキスト修正(+1/-1)

config.action_on_early_load_hook に関する説明文に、英語の "when" が二重に書かれていた箇所を修正しています。
イメージとしては、以下のような変更です(実際の文面は英語ですが、ニュアンスとして):

diff
- This option controls what happens when when the early load hook is triggered.
+ This option controls what happens when the early load hook is triggered.

つまり、ガイドの文章中にあったタイポ(重複単語)のみが修正されています。
設定オプション config.action_on_early_load_hook 自体の意味・利用方法・値の種類などには変更はありません。


  1. 影響範囲・注意点
  • 影響範囲:
    • Railsの挙動・API・設定値には一切変更なし
    • 影響があるのはドキュメントの読みやすさのみ
  • 注意点:
    • 既存アプリケーションやライブラリで、コードや設定を修正する必要はありません。
    • config.action_on_early_load_hook を検索して挙動変更を期待しても、今回はタイポ修正のみである点に留意してください。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57090
  • 関連ガイド: Guides > Configuring Rails Applications (guides/source/configuring.md)
  • config.action_on_early_load_hook は、Railsの「early load hook」が呼ばれた際の動作を制御する設定ですが、本PRではその説明文のみが対象であり、仕様や推奨設定に変更はありません。

#57013 Predefine the well-known Postgres type OIDs

マージ日: 2026/3/28 | 作成者: @matthewd

  1. 概要 (1-2文で)
    PostgreSQL の組み込み型について、pg_type を毎回問い合わせて OID を取得するのをやめ、Rails 側に「よく知られた型 OID」の一覧を同梱して接続時に即座にロードできるようにした変更です。ユーザー定義型や拡張で初めて未知の型に遭遇したときにのみ、これまで通り pg_type をクエリして補完する遅延ロード方式へと切り替えています。

  1. 変更内容の詳細

背景と方針

  • これまで:
    • PostgreSQL 接続時に、常に pg_type をクエリして
      • 型名 → OID (および関連情報)
    • のマッピングを取得していた。
    • これはユーザー定義型や拡張 (例: hstore) を扱うためには必要だが、
      • コアの組み込み型まで毎回問い合わせているのは無駄。
  • PostgreSQL のコア型の多くは「静的に割り当てられた OID」を持ち、PostgreSQL のソースコードに固定値として含まれている。
  • この PR では、その「よく知られた (well-known) OID マップ」を Rails 側に生成・同梱し、
    • 接続直後はそれを使って型マッピングを初期化
    • 未知の型に出会った場合のみ、従来の pg_type クエリを発火
    • 必要であれば、今まで通り個別の one-shot クエリも行う という遅延ロードに変えている。

主なファイルごとの変更

1. well-known OID 情報の追加

  • activerecord/lib/active_record/connection_adapters/postgresql/oid/well_known_values.rb

    • PostgreSQL のコア型に対する「型名 → OID」などの静的テーブルを定義したファイル。
    • ここには生成済みの生データ (値の一覧) が格納される。
  • activerecord/lib/active_record/connection_adapters/postgresql/oid/well_known.rb

    • 上記 well_known_values をラップして、実際の型マッピング初期化で利用しやすい API を提供する層。
    • 例:
      • OID から型名 / カテゴリ / 入れ子関係 (配列型など) を引ける
      • type map initializer が使うための構造に整形する
    • 「コア型だけはこのテーブルから即座に分かる」ようにするためのユーティリティ的役割。
  • activerecord/lib/active_record/connection_adapters/postgresql/oid/well_known_generator.rb

    • 上記 well_known_values.rb を生成するためのジェネレータ。
    • 実際にはテスト環境や開発タスクとして利用し、PostgreSQL の実インスタンスや公式ソース情報から
      • OID
      • 型名
      • カテゴリ
      • 配列/範囲型の対応関係
    • などを収集し、静的 Ruby コードとして書き出す。
    • 実行用の Rake タスクと紐付いている (後述)。
  • activerecord/Rakefile

    • well_known_generator.rb を使って OID テーブルを再生成するための Rake タスクを追加。
    • 新しい PostgreSQL バージョンのサポートや OID 変更に伴い、メンテナが再生成できるようにする。

2. TypeMap 初期化フローの変更

  • activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb

    • 変更量が多いファイル。
    • これまでは:
      • 接続時に pg_type をクエリし、「必要そうな型定義」をまとめて取ってくる半 heuristics なロジックを実行。
    • これからは:
      • 接続直後はまず well_known の情報を使って type map を初期化(コア型はクエリ無しで準備完了)
      • 実際のクエリやスキーマ情報の解釈中に「未知の型 OID / 型名」に遭遇した場合:
        • 初回に限り、従来と同様の「まとめて pg_type をクエリして足りない型を取得する」ロジックを起動
        • それでも解決できない場合は、よりピンポイントな one-shot クエリ (特定 OID / 型名のみ取得) を行う
    • つまり「従来やっていたことはやめないが、発火タイミングを 未知の型検出時 まで遅延させる」という形のリファクタリング。
  • activerecord/lib/active_record/connection_adapters/postgresql/oid.rb

    • well_known 関連クラスの読み込みフックなど、OID 周りのエントリーポイントに最小限の変更・追加。

3. PostgreSQL アダプタ側の変更

  • activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

    • 接続時の初期化ロジックを変更。
    • これまで:
      • 接続確立後に pg_type を即座にクエリして type map を構築。
    • これから:
      • まず WellKnown の事前定義 OID マップを使って type map を構築。
      • ユーザー定義型や拡張型に遭遇したタイミングで、type_map_initializer が動的に pg_type を問い合わせる。
    • また、OID マップの初期化手順やコネクションごとのキャッシュの扱いにも調整が入っているはずで、テストコードから考えると「enum や extension 型を含めた動作が変わらないこと」を重視している。
  • activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb

    • クエリ実行フローのうち、型情報が関与する部分に「未知の OID に遭遇したときの遅延ロードフック」を組み込む変更が追加されている。
    • 実際には、クエリ結果のフィールドメタ情報から OID を見て、type map に無いものがあれば type_map_initializer に問い合わせて補完する、といった流れ。

4. テスト追加・更新

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

    • 接続直後に pg_type をクエリしないことを確認するテストや、
    • well-known OID が正しく利用されること、
    • 未知の型に遭遇したときに初めて DB クエリが走ること、
    • その後はキャッシュされて再クエリされないこと
    • など、振る舞いの変更に対応したテストが多数追加されている。
  • activerecord/test/cases/adapters/postgresql/enum_test.rb

    • enum 型 (ユーザー定義型) が、引き続き正しく解決されることを確認するテストを追加。
    • well-known には含まれないため、遅延クエリの経路が正常に動作するかの検証。
  • activerecord/test/cases/adapters/postgresql/well_known_generator_test.rb

    • well_known_generator が期待通りのデータを吐き出すかをテスト。
  • activerecord/test/cases/adapters/postgresql/well_known_test.rb

    • WellKnown のテーブルが妥当か、代表的な型の OID やメタ情報が正しいかをテスト。

  1. 影響範囲・注意点

性能面の影響

  • ポジティブな影響:

    • PostgreSQL への接続直後に行っていた pg_type へのクエリが、基本的には不要になる。
    • アプリケーションの起動時・コネクションプール拡張時のオーバーヘッドが減り、接続確立が速くなる可能性が高い。
    • 特に多数の DB 接続を張る環境や、短命プロセス (ジョブワーカーなど) でメリットが大きい。
  • 遅延クエリによる影響:

    • 初めてユーザー定義型や拡張型 (enum, composite, range, hstore, 他拡張) を利用するクエリを投げた瞬間に、
      • まとめて pg_type を問い合わせるクエリが一度走るため、そのクエリだけは若干のレイテンシ増が起こりうる。
    • ただし、それは従来「接続直後に常に発生していたコスト」を「必要になった時点まで先送りした」だけなので、トータルでは改善方向。

互換性・バージョン依存性

  • 組み込み型の OID は PostgreSQL ソースで静的に定義されている前提を利用しているため:
    • サポート対象 PostgreSQL バージョン間で OID が変わる場合は、well_known_values.rb を再生成する必要がある。
    • そのための Rake タスクと well_known_generator が同梱されており、メンテナが公式の OID 情報をソースから再取り込みできるようになっている。
  • ユーザー側としては:
    • 通常のアップグレードでは特に作業不要だが、
    • Rails が公式にサポートしていない PostgreSQL バージョンを使う場合、well-known OID と実 OID が乖離する可能性がある点に注意が必要。
      • その場合でも、未知の型として検出されれば pg_type クエリ経由で補完される可能性があるが、組み込み型が「既知」とみなされている分だけ、ずれが起きると厄介なので、サポート範囲外バージョンは避けるのが無難。

型周りの挙動

  • ユーザー定義型・拡張型:
    • これまで通り pg_type ベースで解決される。
    • 「初めて使われるタイミング」でのみクエリが発生する遅延ロードになるが、Rails アプリ側のコード変更は不要。
  • 型解決エラー:
    • 万が一 well-known OID が間違っていた場合、
      • 誤った型マッピングでクエリ結果を解釈するリスクがある。
    • そのため、テストで well-known テーブル自体の正しさを検証している。
  • connection_pool リセット / reconnect:
    • 再接続時も同じく well-known OID で即座に map が構築されるため、pg_type へのクエリは発生しない。
    • 再接続でパフォーマンス劣化するようなケースの改善が期待できる。

  1. 参考情報 (あれば)
  • この PR で導入された主な新規ファイル:
    • activerecord/lib/active_record/connection_adapters/postgresql/oid/well_known_values.rb
    • activerecord/lib/active_record/connection_adapters/postgresql/oid/well_known.rb
    • activerecord/lib/active_record/connection_adapters/postgresql/oid/well_known_generator.rb
  • 関連するテスト:
    • activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
    • activerecord/test/cases/adapters/postgresql/enum_test.rb
    • activerecord/test/cases/adapters/postgresql/well_known_generator_test.rb
    • activerecord/test/cases/adapters/postgresql/well_known_test.rb

開発者視点では、「PostgreSQL 接続時の型情報ロードが eager から lazy になり、かつコア型は静的 OID テーブルで即時解決されるようになった」という理解でおおむね十分です。


#57029 Document with_rich_text_* methods

マージ日: 2026/3/28 | 作成者: @p8

  1. 概要 (1-2文で)
    Action Text の with_rich_text_* 系メソッド(with_rich_text_body など)が公式にドキュメント化されるよう、ActionText::Attribute に Yard コメントが追加された PRです。機能追加や挙動変更ではなく、「どう使うか」が Rails API ドキュメントにきちんと出るように整理したドキュメント改善です。

  1. 変更内容の詳細

変更ファイルは 1 つだけです:

  • actiontext/lib/action_text/attribute.rb

ここに、主に以下のような Yard 形式のコメントが追加されています(概略):

with_rich_text_attribute_name(attribute_name)

with_rich_text_attribute_name という低レベルなヘルパーメソッドに説明が付き、その上で with_rich_text_* スコープがどう生成されるかがドキュメント化されています。

Rails の ActionText::Attribute モジュールは、モデルに has_rich_text :body のように宣言されたときに、以下のようなスコープを自動で生やします:

ruby
class Message < ApplicationRecord
  has_rich_text :body
end

# 自動的に定義される:
Message.with_rich_text_body       # => includes(:rich_text_body)
Message.with_rich_text_body_and_embeds

PR では、この挙動をコードコメントとして明示し、API ドキュメントに反映されるようにしています。例えば:

  • with_rich_text_#{attribute}
    • 対象のモデルに関連付いた ActionText::RichText レコードを eager load するスコープ
    • 実際には includes(rich_text_#{attribute}) 相当
  • with_rich_text_#{attribute}_and_embeds
    • 上記に加えて、リッチテキスト内で添付されている ActiveStorage::Attachment なども eager load するスコープ
    • N+1 を避けて表示するためのユーティリティであることを明示

ドキュメントには、利用例も含まれているはずです。典型的な利用イメージは以下のようなものです:

ruby
# N+1 を避けてリッチテキストを表示したい場合
@messages = Message.with_rich_text_body_and_embeds.limit(20)

@messages.each do |message|
  # ここで body を使っても、関連はすでに eager load 済み
  message.body.to_s
end

この PR 自体は Ruby コードのロジック変更はほぼなく、「こういうスコープが存在し、何をするか」を明快に説明するコメントを 25 行ほど追加しただけです。


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

    • 既存の with_rich_text_* スコープの実装や動作には変更なし。
    • 既存アプリのコードは一切影響を受けません。
  • ドキュメント/API 的な影響:

    • Rails API ドキュメントに with_rich_text_* メソッド群が正式に掲載され、補完や検索で見つけやすくなります。
    • これまで「暗黙に生えていて、ソースを読まないと分かりにくかった」スコープの存在と使い方が、公式に分かるようになります。
    • ガイドライン上、「N+1 を避けるには with_rich_text_xxx(_and_embeds) を使う」というベストプラクティスが明示されるため、チーム内での共通認識を作りやすくなります。
  • 注意点:

    • PR はあくまでドキュメント追加なので、将来的にスコープ名や挙動を変えるときは、このドキュメントとの整合性に注意が必要になります。
    • with_rich_text_* は「スコープ名が属性名から自動生成される」仕組みなので、has_rich_text :content_body など、長い/複雑な名前を付けると同名パターンのスコープが自動生成される点は引き続き意識が必要です。

  1. 参考情報 (あれば)
  • 関連する既存機能:
  • 利用のポイント:
    • 一覧表示などでリッチテキストを大量に読み出す場合は、Model.with_rich_text_bodywith_rich_text_body_and_embeds を使うことで、クエリ数削減とパフォーマンス改善が期待できます。

#57082 Document that :if and :unless options on validations accept arrays [ci skip]

マージ日: 2026/3/27 | 作成者: @cgunther

  1. 概要 (1-2文で)
    ActiveModel のバリデーションにおける :if / :unless オプションが「配列を受け付ける」ことを、公式ドキュメント側に明示的に追記したPRです。挙動自体はすでに存在していたもので、実装変更ではなくドキュメントの補強です。

  1. 変更内容の詳細

何が書かれたか

このPRは以下3つのファイルで、:if / :unless オプションの説明に「配列を渡せる」旨を追記しています。

  • activemodel/lib/active_model/validations.rb
  • activemodel/lib/active_model/validations/validates.rb
  • activemodel/lib/active_model/validations/with.rb

いずれも共通して、

  • :if / :unless には
    • Symbol(インスタンスメソッド名)
    • Proc / lambda
    • String(evalされる条件式)
    • そしてその配列(上記を複数指定するための Array)
  • を渡せる

という点を、文書としてはっきり記載しています。
もともとバリデーションは ActiveSupport::Callbacks.set_callback の上に構築されており、set_callback 側のドキュメントには配列サポートが書いてありましたが、バリデーションの説明側からそのつながりが分かりにくかったため、ここを補った形です。

イメージしやすいサンプルコード

たとえば、以下のようなバリデーション記述が「正当な使い方である」ことが、今回の変更でドキュメントに明記されます。

ruby
class User < ApplicationRecord
  # メソッド名とProcを組み合わせて複数条件
  validates :email, presence: true, if: [:activated?, -> { admin? }]

  # 複数の条件メソッドを :unless で
  validates :age, numericality: { greater_than: 18 }, unless: [:guest?, :under_review?]

  # validates_with / with_options などでも同様
  with_options if: [:activated?, :profile_completed?] do
    validates :phone_number, presence: true
  end

  validates_with SomeValidator, if: [-> { feature_enabled? }, :not_legacy_user?]
end

以前からこれらは動いていましたが、公式ドキュメントからは読み取りづらかったため、今回のPRで「サポートされている書き方」として明文化されています。


  1. 影響範囲・注意点
  • 挙動変更は一切なし
    • 既存アプリケーションの動作には影響しません(ドキュメントのみ変更)。
  • 既に配列を使っているコードはそのままでOK
    • 以前から set_callback によって動いていた挙動であり、今後も同じように動作します。
  • 新規コードでの記述がしやすくなる
    • 今後は「if:unless: に配列を渡して複数条件を管理する」ことが、安心して推奨しやすくなります。
  • 複数条件の評価順序や短絡評価
    • これは今回のPRで新規説明は入っていませんが、set_callback の仕組みに依存します。
    • 一般には、配列の要素ごとに順番に評価され、配列内のすべてが trueunless の場合は false)になるかどうかで実行可否が決まるイメージです。
    • 具体的な評価順・短絡の仕様に厳密に依存したい場合は、ActiveSupport::Callbacks のドキュメントやソースを確認するのが安全です。

  1. 参考情報 (あれば)
  • このPRで明示された背景:
    • バリデーションは ActiveSupport::Callbacks.set_callback ベースであり、:if / :unless の配列サポートはコールバックレイヤー由来。
    • しかし「バリデーションのドキュメントだけ読んでいると、そのことが伝わらない」ため、#55761, #57050 に続く形で説明を補強。
  • 関連PR/Issue:
    • #55761: コールバック / バリデーション周辺のドキュメント改善の流れ
    • #57050: 本PRの直前のフォローアップとなる変更(詳細は該当PR参照)

#57050 Combine per-validator and top-level :if/:unless/:on in validates

マージ日: 2026/3/27 | 作成者: @Mordorreal

  1. 概要 (1-2文で)
    validates でトップレベルとバリデータ単位の :if / :unless / :on オプションを同時に指定した場合に、これらを「上書き」ではなく「結合」するように挙動が変更されました。これにより、期待どおり「両方の条件を満たしたときだけバリデーションが走る」ようになります。

  1. 変更内容の詳細

これまでの問題

現状の Rails (この PR 前) では、以下のように validates を書くと:

ruby
validates :title, presence: { if: :local? }, if: :global?

開発者の期待としては:

  • local?global? も両方 true のときだけ presence バリデーションが走る

はずですが、実際は:

  • バリデータ単位の presence: { if: :local? } に含まれる :if
    トップレベルの if: :global? を Hash#merge によって「上書き」してしまい
  • 結果として「local? が true なら実行、global? は無視」という挙動になっていました。

これは #55761 で報告されていたバグで、この PR はその修正です。

新しい挙動: 条件オプションの「結合」

validates 内で、トップレベルとバリデータ単位の両方に指定された :if / :unless / :on について、Rails 側でマージ処理をカスタム実装し、配列として結合されるようになりました。

変更により、次のように解釈されます:

ruby
validates :title, presence: { if: :local? }, if: :global?
# 新しい挙動では、内部的にほぼこう扱われる:
validates_presence_of :title, if: [:global?, :local?]

つまり、

  • トップレベルの if: ...
  • バリデータ単位の presence: { if: ... }

が両方指定されている場合、それぞれが「条件リストの要素」として扱われ、
全ての条件が true のときに実行 という AND 条件になります。

同様に、:unless:on についても結合されます。

サンプル: :if 結合

ruby
validates :name,
  length: { minimum: 3, if: :short_name_check_enabled? },
  if: :user_active?

→ 実行条件は:

  • user_active? が true かつ
  • short_name_check_enabled? が true

のとき。

サンプル: :unless 結合

ruby
validates :email,
  presence: { unless: :guest? },
  unless: :email_optional?

→ 実行条件は:

  • guest? が false かつ
  • email_optional? が false

(「unless の AND」は「両方とも false である必要がある」という意味になります)

サンプル: :on 結合

ruby
validates :password,
  length: { minimum: 8, on: :create },
  on: :update

:on も配列として結合されるため、実質的には

ruby
on: [:create, :update]

となり、createupdate の両方で実行されます。
(※ :on はバリデーションの「コンテキスト名」なので、配列で複数指定すると「これらいずれかのコンテキストで実行」という OR 的な意味合いですが、「トップレベルに1つ」「バリデータに1つ」が結合されて配列化されるという意味で「結合」です)

スカラーオプションは従来どおり「上書き」

この結合ロジックが適用されるのは :if / :unless / :on のみで、
それ以外のスカラーなオプションは従来どおり「後勝ち (override)」です。

該当例:

  • :message
  • :allow_nil
  • :allow_blank
  • :strict

例:

ruby
validates :title,
  presence: { message: "local message" },
  message: "top-level message"

この場合:

  • 最終的な message は「top-level message」
  • トップレベルの指定が per-validator の指定を上書き

という挙動は変わりません。

実装上のポイント

  • activemodel/lib/active_model/validations/validates.rb_merge_validates_options を追加。
  • validates 内部で options をマージする際に:
    • :if, :unless, :on については、両方に存在する場合は配列化して結合
    • それ以外のキーは従来どおりの Hash#merge 相当の上書き
  • validates_test.rb に、結合挙動とスカラー上書きの両方を確認するテストが追加されています。

PR 説明にあった「コールバック条件評価の最適化 (ActiveSupport 側)」は最終版では削除され、この PR では ActiveModel 側の挙動変更にフォーカスしています。


  1. 影響範囲・注意点

影響範囲

  • 影響を受けるのは、「トップレベル」と「バリデータ個別」の両方に :if / :unless / :on を指定している既存コードです。
  • 今までは「per-validator 側が top-level 側を完全に上書き」していましたが、今後は「両方が結合 (AND)」されるため、条件が厳しくなり、バリデーションが呼ばれなくなるケース が発生する可能性があります。

具体的なケース

ruby
# 旧挙動(上書き)を前提に書かれていたコード

validates :title, presence: { if: :local? }, if: :global?
  • 以前: local? が true なら実行 (global? は無視)
  • 変更後: local? かつ global? が true のときのみ実行

もし、これまで「per-validator 側で top-level の条件を意図的に“消す”ため」にこの書き方をしていた場合、挙動が変わります。

同様に:

ruby
validates :title,
  presence: { on: :create },
  on: :update

を「更新時は presence だけ外すつもりで書いていた」ようなケースでは、変更後は on: [:update, :create] 的な扱いになり、`create / update 両方で実行されるようになります。

パフォーマンス

PR の説明に記載されたベンチ結果 (Ruby 4.0.1):

  • トップレベル :if のみ: +2.3%
  • per-validator :if のみ: +1.2%
  • 両方に :if がある結合ケース: -3.3%
  • 複数バリデータの結合ケース: -1.7%

ただし、最終版の PR では ActiveSupport 側のコールバック最適化が削除されているため、
これらの数値はそのままは当てはまりませんが、

  • 条件 1 個だけの一般的なケースではほぼ現状並み
  • 条件が 2 個以上結合されるケースでは、その分だけ条件評価のコストは増える (不可避)

という程度の影響と考えておけばよいです。
テストスイートは ActiveSupport/ActiveModel ともにパスしています。

マイグレーション上の注意

  • トップレベルと per-validator の両方で :if / :unless / :on を使っている箇所がないか検索し、
    「片方で片方を打ち消す」つもりの書き方をしていないか を確認した方が安全です。
  • もし「上書き」挙動をあえて使っていた場合は:
    • トップレベルの条件を削除する
    • コンテキスト (:on) 側で分ける
    • クラスメソッドなどで条件ロジックを一本化する といった対応が必要になります。

  1. 参考情報 (あれば)

#57080 Deprecate schema_order option in PostgreSQL database configurations

マージ日: 2026/3/27 | 作成者: @eileencodes

  1. 概要 (1-2文で)
    PostgreSQL 用のデータベース設定オプション schema_order が非推奨となり、今後は schema_search_path を使うように変更されました。schema_order は過去の名前の名残であり、現行の命名に統一するための整理です。

  1. 変更内容の詳細(あればサンプルコードも含めて)
  • schema_order オプションを 非推奨(deprecate) として扱うように変更
    • postgresql_adapter が接続設定を解釈する際に、schema_order が使われていたら警告を出しつつ、内部的には schema_search_path を使うような形に近い挙動になっています(実装としては、schema_order は古いエイリアスであり、正式名称の schema_search_path に統一するという方針)。
  • CHANGELOG に以下のような内容が追記
    • 「PostgreSQL データベース設定における schema_order オプションを非推奨にした。代わりに schema_search_path を使用してください」といった趣旨のエントリが追加されています。
  • テストの追加
    • PostgreSQL アダプタ用のテスト (postgresql_adapter_test) に、schema_order 使用時の挙動と非推奨警告を確認するテストケースが追加されています。

※PR の説明文どおり、schema_order は「現在の命名 (schema_search_path) より前から存在する古いエイリアス」であり、機能的には schema_search_path と同じ意味のオプションです。

サンプル(従来の書き方・新しい書き方の例):

yaml
# database.yml (従来: 非推奨になる書き方)
production:
  adapter: postgresql
  database: my_app_production
  schema_order: public,tenant_1

# database.yml (推奨: これから使うべき書き方)
production:
  adapter: postgresql
  database: my_app_production
  schema_search_path: public,tenant_1

  1. 影響範囲・注意点
  • 影響を受けるのは PostgreSQL を使っていて、database.yml (または接続設定) に schema_order を指定しているアプリケーション です。
  • 今回は「非推奨化」であり、ただちに動かなくなるわけではない ものの、Rails の将来のバージョンで schema_order が削除される可能性が高いです。
  • Rails をアップグレードした際に、起動時やテスト実行時などに deprecation warning が出る ようになると考えられます。そのため:
    • schema_order を使用中の設定は、早めに schema_search_path に置き換えることが推奨されます。
    • CI ログや本番ログに deprecation warning が大量に出るのを避ける意味でも、変更しておくとよいです。
  • アプリケーションコード中で schema_order を参照しているカスタム処理がある場合(例: 独自の接続ラッパーやマルチテナント周りの設定生成など)、そこも合わせて schema_search_path に名称変更する必要があります。

  1. 参考情報 (あれば)

#57077 Deprecate the strict option in MySQL database configurations

マージ日: 2026/3/27 | 作成者: @eileencodes

  1. 概要 (1-2文で)
    MySQL 用のデータベース設定オプション strict が非推奨になり、代わりに variables: { sql_mode: ... } を直接指定する形に整理された PR です。Rails が推奨しない「非 strict モード」のための特別なショートカットを廃止し、設定方法を一本化しています。

  1. 変更内容の詳細

非推奨になったオプション

database.yml などで使われていた以下の設定が非推奨になります。

yaml
# 旧: 非推奨
strict: true
strict: false
strict: :default

strict は元々、MySQL の sql_mode を簡易的に切り替えるために Rails 4.2 で導入されたオプションです。しかし現在は variables: { sql_mode: "..." } で同じことができるため、Rails 側で専用オプションを持つ必要がない、という整理です。

推奨される置き換え

PR で明示されているマイグレーションパスは次の 3 パターンです。

yaml
# 1. strict: true
#   → 削除する (これがデフォルト)
# 何も書かないか、既存の strict 行を消す
# strict: true  # ← 削除してよい

# 2. strict: false
#   → variables: { sql_mode: "" }
variables:
  sql_mode: ""

# 3. strict: :default
#   → variables: { sql_mode: :default }
variables:
  sql_mode: :default

つまり、strict を使わずに sql_mode をベタに指定してください、という変更です。
「strict モードを無効にしたい (strict: false)」場合にだけ、variables で明示的に sql_mode を空文字にする必要が出てきます。

コード・テストの変更の概要

  • activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
    • strict オプションを解釈していた箇所に deprecation を出す処理またはコメントが追加されています(strict を設定した際に警告が出るような対応)。
  • activerecord/CHANGELOG.md
    • strict が非推奨になったこと、推奨される代替手段(variables: { sql_mode: ... })が記載されています。
  • テスト (connection_test.rb, defaults_test.rb)
    • strict オプションに関する振る舞いとデフォルト値周りのテストが、廃止方針に合わせて修正されています。
    • sql_mode をどう設定するかが、strict 経由でなく variables 経由でテストされるようになっています。

  1. 影響範囲・注意点

影響を受けるのは誰か

  • database.yml などで MySQL アダプタを使い、かつ strict: オプションを指定しているアプリケーションが対象です。
  • PostgreSQL 等には無関係です。

どんな影響があるか

  • Rails の新バージョンに上げると、strict オプションを使っている場合に deprecation warning が表示されるようになります。
  • 将来のメジャーバージョンで strict が完全削除されると、設定に strict が残っていると起動時にエラーになる可能性があります。

実務上の対応

  1. strict: true を使っている場合

    • 設定行を 削除するだけでよい です。もともとこれがデフォルト挙動であり、今後もデフォルトが「strict モード相当」であることが前提になっています。
  2. strict: false を使っている場合

    • 明示的に non-strict を求めているため、今後も同じ挙動を維持したければ、必ず以下のように書き換える必要があります。
    yaml
    # 旧
    strict: false
    
    # 新
    variables:
      sql_mode: ""

    ※ ただし、PR 説明にある通り、2026 年時点では MySQL を非 strict モードで運用することは推奨されておらず、この設定自体を再検討した方がよいケースが多いです(サイレントな型変換・トランケーションなどの問題が起こりやすいため)。

  3. strict: :default を使っている場合

    • MySQL サーバー側のデフォルト sql_mode を使いたい、というニーズです。今後は以下のように書き換えます。
    yaml
    # 旧
    strict: :default
    
    # 新
    variables:
      sql_mode: :default

その他の注意点

  • すでに variables: を使って sql_mode を細かく設定している場合は、この PR による影響はありません。
  • 既存の sql_mode 設定を変える必要はなく、「strict というショートカットはやめて、variables.sql_mode に一本化します」というポリシー変更だと理解するとよいです。

  1. 参考情報 (あれば)

#57070 Consolidate configure_connection and allow skipping individual settings

マージ日: 2026/3/27 | 作成者: @eileencodes

  1. 概要 (1-2文で)
    PostgreSQL / MySQL 接続時のセッション設定処理が「1つの設定ハッシュを作って一括適用する」形にリファクタされ、サーバ側の現状値を確認して不要な SET クエリをスキップするようになりました。さらに、特定の設定を false にすることで「Rails からはその設定を一切触らない」ようにできるオプションが追加され、set_standard_conforming_strings は非推奨になりました。

  1. 変更内容の詳細

2-1. PostgreSQL: configure_connection の統合と最適化

これまでバラバラに行われていた PostgreSQL の接続設定 (standard_conforming_strings, schema_search_path, など) を、次のようなフローに統合しています。

  1. 接続時に「セッション設定」を表すハッシュを組み立てる
  2. そのハッシュをループしながら internal_set_configSET を実行
  3. internal_set_config 内で PostgreSQL の parameter_status を確認し、「すでに同じ値なら SET しない」

イメージとしては以下のような処理になっています(擬似コード):

ruby
settings = {
  "standard_conforming_strings" => ...,
  "intervalstyle"               => ...,
  "client_min_messages"         => ...,
  "search_path"                 => ...
}.compact # nil は除外

settings.each do |name, value|
  internal_set_config(name, value)
end

internal_set_config では connection.parameter_status(name) を参照し、サーバ側がすでに value になっていれば SET name = ... を発行しません。これにより、接続ごとに毎回 SET クエリを打たなくてよくなり、特にコネクションプーラー越しの環境での無駄なラウンドトリップが減ります。

2-2. 設定を「完全にスキップ」できるように

Rails の DB 設定で、特定のセッション設定項目に false を指定すると、その項目については「Rails はそもそも何も設定しない」挙動になります。これは、ロードバランサやプロキシ、接続プール側で既に適切な設定がされており、Rails に上書きしてほしくないケースを想定しています。

PostgreSQL の例

yaml
production:
  adapter: postgresql

  # Rails からは一切 SET しない(DB 側のデフォルト or プール側設定を使う)
  standard_conforming_strings: false
  intervalstyle: false
  min_messages: false
  schema_search_path: false

ここで false を指定すると、そのキーは設定ハッシュから除外され、internal_set_config も呼ばれません。
従来のように「値を変えたい」場合は、false ではなく文字列などの値を指定します。

MySQL の例

MySQL でも同様に、以下のように特定の設定をスキップできます:

yaml
production:
  adapter: mysql2

  # これまで Rails が接続時に wait_timeout を設定していたが、それをやめる
  wait_timeout: false

  # sql_mode は variables: 配下で false 指定(既存の :default 挙動と一貫性を持たせている)
  variables:
    sql_mode: false

これにより、MySQL サーバや RDS / Cloud SQL 側で定義した wait_timeoutsql_mode を Rails が上書きしないようにできます。

2-3. set_standard_conforming_strings の非推奨化

PostgreSQL アダプタにあった set_standard_conforming_strings が非推奨になりました。
理由:

  • いまや「統合された設定ハッシュ」の1項目として扱われるようになり、個別メソッドで制御する必要がなくなった
  • 将来的には設定ハッシュベースの API に一本化したい意図

今後は:

  • Rails の DB 設定(database.yml)で standard_conforming_strings を制御する
  • そもそも Rails に設定させたくない場合は standard_conforming_strings: false

という運用が推奨されます。

2-4. schema_search_path= にも parameter_status チェックを追加

PostgreSQL の schema_search_path= メソッドにも同様の最適化が入りました。

  • SET search_path = ... を発行する前に parameter_status("search_path") を確認
  • すでに同じ search_path であれば何もクエリを発行しない

これにより、アプリケーションコードやマイグレーションで schema_search_path= を多用している場合でも、不要な SET が減ります。

2-5. テストと CHANGELOG

  • PostgreSQL / MySQL それぞれのアダプタテストが大幅に追加され、
    • 設定ハッシュの生成内容
    • false 指定時に SET が発行されないこと
    • parameter_status によるスキップが行われること
      などが検証されています。
  • activerecord/CHANGELOG.md には:
    • 設定スキップ機能の追加
    • 接続設定処理のリファクタ
    • set_standard_conforming_strings の非推奨
      が明記されています。

  1. 影響範囲・注意点

影響範囲

  • ActiveRecord の PostgreSQL / MySQL アダプタ全般(特に接続確立時の処理)
  • DB 接続設定 (database.yml / 環境変数) のうち、以下あたりを使っているプロジェクト
    • PostgreSQL: standard_conforming_strings, intervalstyle, min_messages, schema_search_path
    • MySQL: wait_timeout, variables[:sql_mode]

注意点

  1. false と「値なし (nil)」の違いに注意

    • false …「Rails からはその設定に一切触らない」(SET しない)
    • nil / 未指定 …「Rails のデフォルトロジックに従って設定する or 何もしない(アダプタ実装依存)」
      既存アプリで false を設定し始めると、今まで DB に適用されていた Rails 側の設定が消える可能性があります。
  2. ロードバランサ・プロキシ利用時は明示的に false を検討
    pgbouncer / ProxySQL などを経由して接続する場合、

    • 接続プール側ですでにセッション変数を制御している
    • あるいはセッション変数をいじることが推奨されない
      ことが多いため、この false オプションで Rails 側をオフにするのが有用です。
  3. set_standard_conforming_strings の非推奨対応

    • アプリや gem が直接 set_standard_conforming_strings を呼んでいる場合、将来的な削除に備えて:
      • 呼び出しを削除する
      • あるいは database.ymlstandard_conforming_strings を使うように移行する
    • 非推奨警告のログ出力にも注意してください。
  4. パフォーマンス / トラフィックへのポジティブな影響

    • 接続確立時やスキーマ変更時に発行される SET クエリが減るため、レイテンシ・DB 負荷の軽減が期待できます。
    • 特にコネクションプーラー + 多数の短命コネクションな構成で効果が出やすい変更です。

  1. 参考情報 (あれば)
  • 関連 PR: https://github.com/rails/rails/pull/57013
    (今回のセッション設定リファクタと連動する変更が行われている可能性があります)
  • parameter_status は libpq / PostgreSQL が提供する「現在のセッションパラメータ値」問い合わせ機能で、これを利用することで「本当に変更が必要なときだけ SET する」挙動になっています。

#57053 Fix parallel test shutdown hang when workers die during Server#shutdown

マージ日: 2026/3/27 | 作成者: @markedmondson

  1. 概要 (1-2文で)
    Rails の並列テスト実行時に、Server#shutdown 中にワーカープロセスが死んだ場合にシャットダウン処理がハングする問題を修正した PR です。wait_for_active_workers ループ内で定期的に子プロセスの終了を回収することで、ゾンビプロセスによる無限待ちを防ぎます。

  1. 変更内容の詳細

背景となる既存実装

  • ActiveSupport::Testing::Parallelization では、並列テスト用のワーカープロセス群をフォークし、DRb 経由でテストジョブを配布しています。

  • シャットダウン時の流れは概ね以下です:

    1. Parallelization#shutdown で各ワーカーに停止指示を送りつつ、Process.waitpid(pid, WNOHANG) で「すでに死んでいる」ワーカーを掃除。
    2. その後 Server#shutdown が呼ばれ、wait_for_active_workers で「まだアクティブ(=Server 側が把握している)ワーカー」がいなくなるのを待機。
  • 問題は、Server#shutdown に入った後 にワーカーが異常終了した場合でした:

    • 例: parallelize_teardown フック内で例外発生、DRb 切断、OOM kill など。
    • 親プロセスは「そのワーカーはまだアクティブ」と思い続けるが、実際には子プロセスは終了している=ゾンビになっている。
    • wait_for_active_workerswhile active_workers? のようなループを回しているため、永遠に抜けられない → CI がタイムアウトまでハング。

過去 PR (#55794) では「Server#shutdown を呼ぶ前に 1 回だけ WNOHANG スイープする」対応をしていましたが、シャットダウン中に落ちるワーカー は拾えない、という抜けが残っていました。


今回の主な変更点

1. Server#reap_dead_workers の追加

activesupport/lib/active_support/testing/parallelization/server.rb に、新しく reap_dead_workers メソッドが追加されています。

概念的には以下のような処理です(実際のコードイメージ):

ruby
def reap_dead_workers
  @workers.each do |worker|
    pid = worker[:pid]
    next unless pid

    # すでに終了しているプロセスをノンブロッキングで回収
    begin
      dead_pid = Process.waitpid(pid, Process::WNOHANG)
      if dead_pid
        # ここで内部状態から「このワーカーはアクティブ」という情報を外す
        unregister_worker(dead_pid)
      end
    rescue Errno::ECHILD
      # すでに wait 済み、または存在しない場合はスキップ
    end
  end
end

ポイント:

  • Process.waitpid(pid, WNOHANG) により ブロックせずに 子プロセスの終了を検知・回収。
  • ワーカーが勝手に死んでいても、Server 側の「アクティブワーカー一覧」から削除できるようにしている。
  • Errno::ECHILD は「その PID の子プロセスはいない」ケースのため、無視して継続。

※実際の実装では、@workers の管理方法や unregister_worker 相当の処理は Server の既存ロジックに合わせた形になっています。

2. wait_for_active_workers ループ内での reap_dead_workers 呼び出し

wait_for_active_workers のループに reap_dead_workers 呼び出しが組み込まれました。

イメージ:

ruby
def wait_for_active_workers
  while active_workers?
    reap_dead_workers   # ← 新規
    sleep(SHORT_INTERVAL)
  end
end

これにより:

  • シャットダウン開始 にワーカーが異常終了した場合でも、
  • 次のループサイクルで reap_dead_workers によりその死を検知し、
  • active_workers? の対象から外れる → ループが正常に終了する、

という流れになります。

3. 回帰テストの追加

activesupport/test/parallelization_test.rb に、今回のバグを再現するテストが追加されています (約 36 行)。

テストの流れは概ね次のようなものです:

  1. 親プロセスが Server を起動。
  2. 子プロセスを fork してワーカーとして登録させる(DRb 経由で「生きているワーカー」と認識される状態を作る)。
  3. 親プロセス側で Server#shutdown を呼び、wait_for_active_workers に入ったことが確認できるタイミングで、
  4. 親が子プロセスに対して Process.kill などでシグナルを送り、ワーカーを強制終了
  5. その後 Server#shutdown が正常に終了し、テスト全体もハングせずに完了することを検証。
  • 修正前のコードだと、このテストは wait_for_active_workers が永遠に返ってこないためハング。
  • 修正後は reap_dead_workers によってワーカー死亡が検知され、テストが通ることを確認済み。

  1. 影響範囲・注意点

影響範囲

  • 対象: ActiveSupport::Testing::Parallelization を使った並列テスト実行(特に system test や CI での利用)。
  • 主な効果:
    • シャットダウンハングの解消: ワーカーが
      • OOM Kill、
      • ティアダウン処理中の例外、
      • DRb 切断 などで 事前通知なく死んだ場合でも、テストスイート全体が 90 秒前後ハングして watchdog に殺される、といった事象が起きにくくなります。
    • ゾンビプロセスの放置を減らし、親プロセスがきちんと wait するようになります。

パフォーマンス・副作用など

  • wait_for_active_workers ループ内で Process.waitpid(..., WNOHANG) を呼ぶので、ループごとに若干オーバーヘッドがありますが、

    • WNOHANG なのでブロッキングせず、
    • 対象となるワーカー数も通常はテスト並列数程度(数~数十プロセス)なため、 実用上のオーバーヘッドはごく小さいと考えられます。
  • Errno::ECHILD を握りつぶす実装であれば、「すでに何か別の箇所で wait されていた」パターンでも安全に動作します。

  • ワーカー異常終了のハンドリングは「静かに除外して shutdown を進める」方向なので:

    • 個々のワーカー側でのログやエラー検出は、これまで通り ワーカー側orテスト側で適切にログを出す 必要があります(この PR は主にハング防止のための変更)。

  1. 参考情報 (あれば)
  • 対応する Issue: #57052
    • 内容: 並列テストのシャットダウン時にハングする問題の報告。
  • 関連 PR:
    • #55794: Parallelization#shutdown 内での WNOHANG スイープを導入した以前の修正。
      • これは「Server#shutdown 前にすでに死んでいるワーカー」のみを対象としており、今回の PR はそのカバー範囲を「Server#shutdown 中に死ぬワーカー」にまで拡張する位置付けです。

並列テストが CI で時々 90 秒程度ハングして落ちる、といった現象がある場合、この修正が取り込まれた Rails バージョンに上げることで解消が期待できます。


#57047 Add test coverage for Error#initialize_dup, #hash, and #detail

マージ日: 2026/3/27 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    ActiveModel::Error のテスト不足だったメソッド(initialize_dup, hash, detail)に対して、専用のテストが追加されました。挙動の仕様をテストで明文化することで、error.rb のテストカバレッジが実質的に埋められています。

  1. 変更内容の詳細

このPRは実装コードには手を入れず、activemodel/test/cases/error_test.rb に29行のテストコードを追加しただけの変更です。主に以下3点を検証しています。

2-1. Error#initialize_dup のテスト

目的: dup した ActiveModel::Error オブジェクト同士で options が共有されず、片方の変更がもう片方に漏れないことを保証する。

想定されるテストイメージとしては:

ruby
error = ActiveModel::Error.new(record, :name, :too_short, count: 3)
duped  = error.dup

# dup 後に一方の options を変更しても
duped.options[:count] = 5

# 元の error の options に影響しないことを確認
assert_equal 3, error.options[:count]
assert_equal 5, duped.options[:count]

これにより、initialize_dupoptions をディープコピー(少なくとも別オブジェクトとしてコピー)していることをテストで保証しています。
その結果、バリデーションエラーを複製した上でメッセージやオプションを個別に変更するような処理において、副作用が紛れ込む可能性を防ぎます。

2-2. Error#hash#== の整合性テスト

目的: == で「等しい」と判定される ActiveModel::Error インスタンス同士が、同じ hash 値を返すことを保証する。

Ruby の Hash や Set の正しい動作には、以下の契約が必須です。

  • a == b ならば a.hash == b.hash でなければならない

テストのイメージ:

ruby
error1 = ActiveModel::Error.new(record, :name, :too_short, count: 3)
error2 = ActiveModel::Error.new(record, :name, :too_short, count: 3)

assert_equal error1, error2
assert_equal error1.hash, error2.hash

set = Set.new([error1])
assert_includes set, error2 # hash と == が整合していないと失敗しうる

このテストにより、ActiveModel::Error を Hash のキーや Set の要素として使うケース(エラーの一意化・重複排除など)で、期待どおりに動作することがテストレベルで保証されます。

2-3. Error#detail のエイリアス挙動テスト

目的: detaildetails の単なるエイリアスとして同じ結果を返すことを確認する。

イメージ:

ruby
error = ActiveModel::Error.new(record, :name, :too_short, count: 3)

assert_equal error.details, error.detail

これにより、detail メソッドを利用している既存コードが、details の挙動に追従することを前提にしても安全である、という契約をテストで固定しています。


  1. 影響範囲・注意点
  • 実装コードには一切変更がなく、あくまで「既存仕様のテストによる固定」が目的の PR です。そのため、このPR単体によるランタイム挙動の変化や互換性の問題はありません。
  • ただし、以下の仕様が「テストで固定」された意味は大きいです:
    • Error#dupoptions が共有されないこと
    • Error#hash#== の契約(等価なオブジェクトは同じ hash)
    • detaildetails が同一の結果を返すこと
      将来的にこれらを変えようとするとテストが落ちるため、互換性を意識した変更設計が必要になります。
  • ActiveModel::Error を拡張したり、これを前提としたライブラリ・アプリケーションを開発している場合、上記の挙動を仕様として安心して頼ることができます。

  1. 参考情報 (あれば)
  • 対象クラス: ActiveModel::Error
    • エラー1件を表すクラスで、record, attribute, type, options などを持つ
  • Ruby における hash== の契約(Hash/Set 利用時の前提)
    https://docs.ruby-lang.org/ja/latest/doc/spec=2fcomparison.html
  • 変更ファイル: activemodel/test/cases/error_test.rb のみ(+29行 / -0行)

#57044 Add test coverage for ActiveModel::Errors#import

マージ日: 2026/3/27 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    ActiveModel::Errors の公開メソッド import に対して、これまで存在しなかった専用のテストが追加されました。機能追加や挙動変更はなく、既存の挙動を明示的に保証するためのテストのみの変更です。

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

追加されたテストの目的

ActiveModel::Errors#import は、他オブジェクトのエラーを現在の Errors に取り込むための公開 API ですが、これまで merge! 経由で間接的にしかテストされていませんでした。本 PR では、この import を直接テストし、以下の点を確認しています。

  1. インポートされたエラーが NestedError としてラップされること
  2. NestedError が元のエラー (inner_error) を保持していること
  3. :attribute オプションで属性名を上書きできること(文字列を渡してもシンボルに変換されること)
  4. :type オプションでエラー種別を上書きできること(文字列を渡してもシンボルに変換されること)

想定されるテストコードのイメージ

実際のテストは activemodel/test/cases/errors_test.rb に 41 行追加されています。構造としては概ね以下のような内容になっていると考えられます(概念的なサンプル):

ruby
def test_import_wraps_error_as_nested_error
  source = ActiveModel::Errors.new(Object.new)
  source.add(:name, :blank)

  target = ActiveModel::Errors.new(Object.new)

  # ここで import を直接呼ぶ
  source.each do |error|
    target.import(error)
  end

  error = target.first

  # 1. NestedError であること
  assert_instance_of ActiveModel::Errors::NestedError, error

  # 2. inner_error を保持していること
  assert_equal source.first, error.inner_error
end

def test_import_with_attribute_override
  source = ActiveModel::Errors.new(Object.new)
  source.add(:name, :blank)

  target = ActiveModel::Errors.new(Object.new)

  source.each do |error|
    target.import(error, attribute: "title") # String で指定
  end

  error = target.first

  # attribute が上書きされ、シンボル化されている
  assert_equal :title, error.attribute
end

def test_import_with_type_override
  source = ActiveModel::Errors.new(Object.new)
  source.add(:name, :blank)

  target = ActiveModel::Errors.new(Object.new)

  source.each do |error|
    target.import(error, type: "too_short") # String で指定
  end

  error = target.first

  # type が上書きされ、シンボル化されている
  assert_equal :too_short, error.type
end

実際のテスト名や細部は異なる可能性がありますが、論点としては上記の 4 点がカバーされています。


  1. 影響範囲・注意点
  • 実装コードには一切変更がなく、テストファイル (activemodel/test/cases/errors_test.rb) のみが追加・修正されています。
  • そのため、ランタイムの振る舞い・API 仕様・パフォーマンスに関する変更や非互換はありません。
  • 既に ActiveModel::Errors#import を利用しているアプリケーションに影響はありませんが、以下の挙動が「テストで固定された仕様」として今後も維持される可能性が高くなりました:
    • import したエラーは必ず NestedError であること
    • 文字列の attribute / type を渡した場合にシンボルへ変換されること
    • 元エラーを inner_error として参照できること

これにより、将来 import の内部実装を変更する際にも、上記仕様を壊さないよう CI で検知できるようになります。


  1. 参考情報 (あれば)
  • 該当 PR: https://github.com/rails/rails/pull/57044
  • 関連クラス: ActiveModel::Errors, ActiveModel::Errors::NestedError
  • 関連メソッド(同じくエラー統合に関与するもの):
    • ActiveModel::Errors#merge!
    • ActiveModel::Errors#add
    • ActiveModel::Errors#each

#57048 Add test coverage for NestedError#inner_error, #raw_type, and #options

マージ日: 2026/3/27 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    ActiveModel::NestedError が持つ inner_error, raw_type, options の3つのアクセサに対して、これまで不足していた専用テストを追加するPRです。振る舞い自体の変更はなく、テストカバレッジを補強するだけの変更です。

  1. 変更内容の詳細

対象クラス: ActiveModel::NestedError
変更ファイル: activemodel/test/cases/nested_error_test.rb (+27行)

主な追加テストは以下の3点です。

2-1. inner_error のテスト

  • 目的: NestedError#inner_error が、元のエラーオブジェクトそのもの(同一オブジェクト)を返すことを保証する。
  • テストでは assert_same を使い、オブジェクトIDレベルで同一であることを検証しています。

イメージコード:

ruby
inner = ActiveModel::Error.new(record, :name, :blank)
nested = ActiveModel::NestedError.new(
  inner,
  attribute: :user_name,
  type: :invalid
)

assert_same inner, nested.inner_error

これにより、NestedError が内包するエラーをコピーや再生成するのではなく、同じエラーインスタンスをラップしているだけである、という契約がテストで明示されました。

2-2. raw_type の委譲のテスト

  • 目的: NestedError#raw_type が、NestedError 自身で独自に持つ値ではなく、内側のエラー(inner_error)から委譲されることを保証する。
  • NestedError 側で type を上書きしていても、raw_type はオリジナルのエラーの type を指す」という関係性を確認しています。

イメージコード:

ruby
inner = ActiveModel::Error.new(record, :name, :blank) # inner.raw_type == :blank
nested = ActiveModel::NestedError.new(
  inner,
  attribute: :user_name,
  type: :too_short # overrideされたtype
)

assert_equal :blank, nested.raw_type

これにより、次のような設計がテストで固定されます。

  • type … 表に見せる(上書き後の)エラー種別
  • raw_type … 内側のエラーが本来持っている種別(生の type)

2-3. options の委譲のテスト

  • 目的: NestedError#options が、override_options で attribute や type を上書きしても、オプションは内側のエラー由来のものを返すことを保証する。
  • つまり「オリジナルエラーのコンテキスト(count など)は保持しつつ、外側で attribute/type だけ差し替える」という設計を確認しています。

イメージコード:

ruby
inner = ActiveModel::Error.new(
  record,
  :name,
  :too_short,
  count: 3
)

nested = ActiveModel::NestedError.new(
  inner,
  attribute: :user_name,
  type: :invalid # typeだけ差し替え
)

assert_equal({ count: 3 }, nested.options)

これにより、NestedError は「元のエラー情報(options)を温存しつつ、属性名や種類だけ“外側”の事情で変えられるラッパー」であることがテストで明示されました。


  1. 影響範囲・注意点
  • 動作仕様の変更は一切なく、「現在の挙動をテストで固定した」だけの変更です。
  • 今後、NestedError の以下の挙動を変えるとテストが落ちるようになります:
    • inner_error が別インスタンスを返すように変える
    • raw_typeNestedError 独自に持つように変える
    • optionsNestedError 側で上書き/別管理するように変える
  • そのため、NestedError を拡張・修正する際は、
    • 「どこまでを inner_error から委譲し、どこからを NestedError 独自とするか」
    • 既存利用コードが raw_typeoptions に何を期待しているか を意識する必要があります。

  1. 参考情報 (あれば)
  • 対象クラス: ActiveModel::NestedError
    • ActiveModel の複雑なバリデーションエラー(ネストした属性、関連モデルなど)を扱う際のラッパークラス。
  • このPRはテストのみの変更のため、CHANGELOG 更新は不要と明示されています。

#57049 Add test coverage for Errors#to_hash with full_messages and #uniq!

マージ日: 2026/3/27 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    ActiveModel::Errors に対して、これまで直接はテストされていなかった to_hash(true)uniq! の挙動を確認するテストが追加されています。挙動変更はなく、テストカバレッジの補完のみです。

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

対象メソッド

  • ActiveModel::Errors#to_hash(full_messages = false)
  • ActiveModel::Errors#uniq!(内部的には委譲されたメソッド)

to_hash(true) のテスト追加

これまで to_hash はデフォルト引数(to_hashto_hash(false))についてはテストされていましたが、full_messages: true 相当となる to_hash(true) が直接はテストされていませんでした。

今回のPRでは、以下を確認するテストが追加されています:

  • to_hash(true) を呼び出したとき、
    • キー: 属性名
    • 値: 「属性名を含んだフルメッセージ」の配列
      で構成されるハッシュが返ってくること

イメージとしては、例えば以下のようなモデルにエラーが付いている場合:

ruby
user = User.new
user.errors.add(:name, :blank)         # "can't be blank"
user.errors.add(:email, "is invalid")  # "is invalid"

to_hash の違いは以下のようになります:

ruby
user.errors.to_hash
# => { name: ["can't be blank"], email: ["is invalid"] }

user.errors.to_hash(true)
# => { name: ["Name can't be blank"], email: ["Email is invalid"] }
#      ↑属性名が含まれる "full messages"

この「to_hash(true)full_messages 相当を返す」というコードパスを、明示的にテストでカバーしています。

uniq! のテスト追加

ActiveModel::Errors は、同じエラー内容を重複して持つことができますが、uniq! によって重複するエラーオブジェクトを削除できます。これまではこの委譲メソッド自体がテストされていませんでした。

今回のテストでは、例えば以下のようなケースを検証していると考えられます:

ruby
user = User.new
user.errors.add(:name, "is invalid")
user.errors.add(:name, "is invalid")  # 同じエラーを2つ追加

user.errors.size
# => 2

user.errors.uniq!

user.errors.size
# => 1  # 重複が取り除かれていることを確認

テストでは、同一属性・同一メッセージといった「同じエラーオブジェクト」に対して uniq! を呼ぶことで、エラー配列が縮約されることを確認しています。


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

    • コード変更はテストファイル (activemodel/test/cases/errors_test.rb) のみで、本体実装には変更がありません。
    • 現在の ActiveModel::Errors#to_hash および #uniq! の挙動はそのままで、仕様変更や破壊的変更はありません。
  • 注意点 / 認識しておくとよいこと

    • 今後これらのメソッドの挙動を変えると、今回追加されたテストが落ちるため、「to_hash(true) はフルメッセージを返す」「uniq! はエラーの重複を排除する」という挙動が事実上テストで固定されます。
    • as_json(full_messages: true) は内部的に to_hash(true) 相当の経路を通るため、to_hash(true) の仕様がテストで明文化されたことで、as_json の期待挙動もより堅く保証される形になります。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57049
  • 関連API(現行ドキュメントはRailsガイド/APIリファレンスを参照):
    • ActiveModel::Errors#to_hash(full_messages = false)
    • ActiveModel::Errors#full_messages
    • ActiveModel::Errors#uniq!

#57046 Add test coverage for Errors#delete with type and options

マージ日: 2026/3/27 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    ActiveModel::Errors#delete が本来サポートしている typeoptions を指定した削除挙動について、新たにテストが追加された PR です。これにより、既存機能のフィルタリングロジックに対して直接的なテストカバレッジが付きました。

  1. 変更内容の詳細

対象: ActiveModel::Errors#delete

既存仕様として delete は以下のインターフェイスを持ちます:

ruby
errors.delete(attribute, type = nil, **options)

今回の PR では、以下のケースに対するテストが activemodel/test/cases/errors_test.rb に追加されています。

2-1. 属性+type での削除

ruby
errors.add(:name, :blank)
errors.add(:name, :too_short, count: 3)

deleted = errors.delete(:name, :blank)

テストで確認している点:

  • delete(:name, :blank)name 属性かつ type=:blank のエラーだけを削除する
  • 同じ name に付いている :too_short など他 type のエラーは残る
  • delete の戻り値として削除されたメッセージが返る(type を指定したときの戻り値の挙動を確認)

2-2. 属性+type+options での削除(完全一致)

同じ属性・同じ type だが options が異なる複数エラーがあるケース:

ruby
errors.add(:name, :too_short, count: 3)
errors.add(:name, :too_short, count: 5)

deleted = errors.delete(:name, :too_short, count: 5)

テストで確認している点:

  • delete(:name, :too_short, count: 5)
    • 属性 :name
    • type :too_short
    • options { count: 5 }
      すべてが一致するエラーだけを削除する
  • count: 3 のエラーは残る(type が同じでも options が違えば削除されない)
  • 戻り値に削除されたメッセージが返る

2-3. 戻り値の確認

従来、delete(:attribute) だけのテストでは、削除結果の戻り値や type/options 指定時の戻り値挙動が十分にカバーされていませんでした。
今回は以下を明示的にテストしています:

  • type 指定ありの delete が「削除されたエラーメッセージ」を返すこと
  • options まで指定した場合も同様に、削除したものに対応するメッセージが返ること

  1. 影響範囲・注意点
  • 実装の挙動変更はなく、「テストの追加のみ」であるため、アプリケーションのランタイム挙動には影響しません。
  • ただし、次のような前提が公式テストとして明文化されたことで、今後の Rails バージョンでも以下の仕様が「期待される挙動」として固定化される傾向になります:
    • errors.delete(attribute, type) は「その attribute かつその type のエラーのみを削除する」
    • errors.delete(attribute, type, **options) は「attribute, type, options がすべて一致するエラーのみを削除する」(部分一致ではない)
    • 上記削除時に、削除したエラーのメッセージを戻り値として返す

このため、自前で Errors に似た API を実装している場合や、ActiveModel::Errors をモンキーパッチしている場合は、この仕様に合致しているかを改めて確認するとよいです。


  1. 参考情報 (あれば)

#57043 Add test coverage for ActiveModel::Error#strict_match?

マージ日: 2026/3/27 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    ActiveModel::Error#strict_match? に対して専用のテストが追加され、属性・タイプ・オプションが「完全一致」しているかを判定する挙動が明示的に保証されるようになりました。機能追加や仕様変更ではなく、既存のパブリック API のテスト補完のみです。

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

追加されたテストの主旨

ActiveModel::Error#match? は従来からテストが充実していましたが、
「すべてのオプションが一致するか」を見る strict_match? は未テストでした。
このPRでは以下を検証するテストが activemodel/test/cases/error_test.rb に追加されています。

  1. 属性・タイプ・オプションがすべて一致すると true
  2. 属性が異なれば false
  3. タイプが異なれば false
  4. オプション値が異なれば false
  5. チェック側のオプションが足りない場合も false
  6. コールバック系・メッセージ系のオプションは比較対象から除外される

strict_match? の仕様確認ポイント

strict_match? は以下を満たすときに真になります:

  • attribute が同じ
  • type が同じ
  • 比較対象に渡したオプションと、エラーが保持しているオプションが
    「コールバック系・メッセージ系を除いたものについて」完全一致している

「コールバック系・メッセージ系」として無視されるオプションは:

  • :if
  • :unless
  • :on
  • :allow_nil
  • :allow_blank
  • :strict
  • メッセージ関連オプション(message など)

これにより、バリデーションの実行条件やメッセージカスタマイズに関するオプションは、strict_match? の一致判定から除外されます。

想定されるテストイメージ(簡略例)

PR自体はテストファイルのみ +44 行ですが、挙動のイメージはだいたい以下のようになります(疑似コード):

ruby
error = ActiveModel::Error.new(record, :name, :too_short, count: 3, allow_nil: true)

# 属性・タイプ・オプション(allow_nil を除く)が完全一致 → true
error.strict_match?(:name, :too_short, count: 3) # => true

# 属性が違う → false
error.strict_match?(:email, :too_short, count: 3) # => false

# タイプが違う → false
error.strict_match?(:name, :invalid, count: 3) # => false

# オプション値が違う → false
error.strict_match?(:name, :too_short, count: 5) # => false

# 必要なオプションが足りない → false
error.strict_match?(:name, :too_short) # => false

# コールバック系・メッセージ系は無視して判定
error_with_callbacks = ActiveModel::Error.new(
  record,
  :name,
  :too_short,
  count: 3,
  if: :some_condition,
  message: "is too short"
)

# if/message は比較されないので、count が一致していれば true
error_with_callbacks.strict_match?(:name, :too_short, count: 3) # => true

  1. 影響範囲・注意点
  • ランタイムの挙動変更は一切なく、テスト追加のみのPRです。
  • これまで暗黙的だった strict_match? の仕様(特に「どのオプションが比較対象になるか」)がテストにより固定化されるため、今後仕様を変える場合はテスト変更を伴うことになります。
  • strict_match? を利用しているコードは、
    バリデーション条件やメッセージの違いは無視して、論理的なエラー内容(属性・タイプ・意味のあるオプション)の完全一致を見ている
    という前提が正しいことが改めて保証された形になります。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57043
  • 対象クラス: ActiveModel::Error
  • 既存メソッドとの違い:
    • match? … ゆるい一致(特定の条件を満たせば true)
    • strict_match? … コールバック/メッセージ系を除いたオプションの完全一致が必要

#57045 Add test coverage for EnvironmentInquirer predicates

マージ日: 2026/3/27 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    ActiveSupport::EnvironmentInquirer に用意されている development?, test?, production? の各述語メソッドに対して、これまで不足していたテストを追加する PR です。合わせて、カスタム環境(例: "staging")での振る舞いが StringInquirer にフォールバックすることや、local? が非ローカル環境で false を返すことも明示的にテストしています。

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

追加されたテストのポイント

対象クラスは ActiveSupport::EnvironmentInquirer で、以下の挙動がテストされています。

  1. development? の挙動

    • "development" のとき true
    • "test", "production" のとき false
  2. test? の挙動

    • "test" のとき true
    • "development", "production" のとき false
  3. production? の挙動

    • "production" のとき true
    • "development", "test" のとき false
  4. カスタム環境でのフォールバック動作

    • "staging" のようなカスタム環境では、development?, test?, production? はすべて false になることを確認
    • これは、組み込み 3 環境以外では EnvironmentInquirerStringInquirer の通常の挙動にフォールバックしていることを保証するテストです。
  5. local? の確認

    • "staging" のような非ローカル環境で local?false になることを確認
    • もともと local? についてはテストがありましたが、カスタム環境との組み合わせのケースを明示する形で追加されています。

イメージしやすいサンプルコード

PR で書かれているテスト内容はだいたい以下のようなイメージです(擬似コード):

ruby
env = ActiveSupport::EnvironmentInquirer.new("development")
assert env.development?
refute env.test?
refute env.production?

env = ActiveSupport::EnvironmentInquirer.new("test")
assert env.test?
refute env.development?
refute env.production?

env = ActiveSupport::EnvironmentInquirer.new("production")
assert env.production?
refute env.development?
refute env.test?

env = ActiveSupport::EnvironmentInquirer.new("staging")
refute env.development?
refute env.test?
refute env.production?
refute env.local?

実際には activesupport/test/environment_inquirer_test.rb に 30 行程度のテストコードが追加されているだけで、プロダクションコードの変更はありません。


  1. 影響範囲・注意点
  • 影響範囲は テストコードのみ であり、ランタイム挙動や API は一切変更されていません。
  • すでに実装されていた最適化済みメソッド development?, test?, production? の仕様をテストで「固定」した形になるため、今後これらの挙動を変えたい場合はテストの更新が必須になります。
  • カスタム環境(例: "staging", "review", "sandbox" など)で development?, test?, production?, local? がデフォルトでは false である、という前提が公式にテストされるようになりました。
    これは「Rails 本体としてその仕様を前提にしている」と読めるため、アプリ側でこれらのメソッドに依存したロジックを書く場合も、同じ前提でコードを書いて問題ないと言えます。
  • CHANGELOG は更新されておらず、「テストのみの変更」と明示されています。

  1. 参考情報 (あれば)
  • ActiveSupport::EnvironmentInquirer は、Rails.env が返すオブジェクトのクラスとして利用され、Rails.env.production? のような問い合わせを可能にするためのクラスです。
  • EnvironmentInquirerStringInquirer のサブクラスで、development?, test?, production? など一部の環境名については method_missing を通らない高速な専用メソッドを提供しています。本 PR は、その「最適化パス」が StringInquirer ベースの従来挙動と同じ結果になることを保証するテストを追加したものと位置付けられます。

#57071 Fix titleize to capitalize unicode lowercase letters

マージ日: 2026/3/27 | 作成者: @EldinGuzin

  1. 概要 (1-2文で)
    Rails の ActiveSupport::Inflector#titleize が、ASCII 以外の小文字(đ, é, ü, ñ, ć など)も正しく先頭大文字化できるように、正規表現を [a-z] から Unicode の小文字クラス \p{Ll} に変更した PR です。これにより、多言語テキストでのタイトル化処理が期待通りに動作します。

  1. 変更内容の詳細

何を直したか

従来の titleize は内部で次のような正規表現を用いていました(概念的な例):

ruby
# 変更前(イメージ)
string.gsub(/(?:^|\s|["'([{])([a-z])/) { $1.upcase }

[a-z] は ASCII の小文字 a〜z のみを対象にするため、以下のような Unicode の小文字はマッチせず、そのまま残っていました。

  • đ, é, ü, ñ, ć など

PR の説明にある挙動:

ruby
# 変更前
titleize("ćasim đipa") # => "ćasim đipa"  # どこも変わらない

# 変更後
titleize("ćasim đipa") # => "Ćasim Đipa"

修正内容

正規表現中で「小文字 1 文字」を表していた [a-z] を、Unicode の「小文字一般」を表す \p{Ll} に置き換えています。

ruby
# 実際の変更(概念的)
- /([a-z])/
+ /(\p{Ll})/

\p{Ll} は「Letter, lowercase(小文字の文字)」という Unicode プロパティクラスで、ラテン文字以外も含むあらゆる小文字のコードポイントが対象になります。

テスト追加

activesupport/test/inflector_test_cases.rb に、Unicode 小文字を含むケースが 1 行追加されています。
例として PR 説明にある:

ruby
assert_equal "Ćasim Đipa", ActiveSupport::Inflector.titleize("ćasim đipa")

といった形式のテストが追加されたと考えられます(実際の記述もほぼこれに相当)。


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

    • ActiveSupport::Inflector.titleize を直接/間接的に利用している箇所:
      • モデル名や属性名から画面表示用タイトルを生成しているヘルパー
      • I18n を使わず、Inflector ベースでラベルを生成している独自コード
      • ドキュメント生成やレポートタイトルなど、「文字列 → タイトルケース」変換を多言語で行うコード
  • 主な挙動の変化

    • これまで「先頭がユニコード小文字の単語」はそのまま小文字で残っていたのに対し、今後は正しく大文字化されます。
    • ASCII 文字のみ扱うアプリではほぼ影響ありませんが、多言語対応アプリでは表示結果が変わる可能性があります。
      • 例: 画面タイトル、ボタンラベルなどが「今までたまたま小文字だったが、本来の期待どおり大文字になる」といった見た目の差分が出ます。
  • 互換性・注意点

    • マッチ対象の拡大により、「あえて非 ASCII の先頭文字を小文字のまま残す」ことを前提にしていたロジックがあると、挙動が変わります(そのような前提はまず推奨されませんが)。
    • Ruby の正規表現が \p{Ll} を解釈するのは、通常の Rails がサポートする Ruby バージョンなら問題ありませんが、非常に古い Ruby を使っている環境では互換性に注意が必要です(2026 年時点の Rails を使うなら通常気にしなくてよい前提です)。

  1. 参考情報 (あれば)
  • Ruby の Unicode プロパティクラス:

    • \p{Ll}: Lowercase Letter(小文字の文字)
    • \p{Lu}: Uppercase Letter(大文字の文字)
    • 参考: Regexp と Unicode プロパティ(Ruby リファレンス)
  • Rails 側の関連メソッド:

    • ActiveSupport::Inflector.titleize
    • String#titleize(ActiveSupport による拡張。内部的には Inflector を利用)

この PR によって、titleize は英語以外の言語にもより自然に対応できるようになり、「国際化対応の基本的な文字列整形」としての信頼性が上がっています。


#57074 Update yarn install command to support immutable and frozen-lockfile in Dockerfile.tt

マージ日: 2026/3/27 | 作成者: @javier-menendez

  1. 概要 (1-2文で)
    Rails のアプリ生成時に使われる Dockerfile.tt の Yarn インストールコマンドが更新され、Yarn が Corepack 管理かどうかに応じて --immutable または --frozen-lockfile を使い分けるようになりました。これにより、Yarn 1 系/Yarn 2+(Berry)や Corepack 利用など、さまざまな環境でより安全かつ互換性の高いインストールが行えるようになります。

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

何が変わったか

対象ファイル:
railties/lib/rails/generators/rails/app/templates/Dockerfile.tt

アプリ生成 (rails new myapp --docker など) 時にテンプレートから生成される Dockerfile の中で、yarn install コマンドに渡すフラグが 1 行変更されています。

PR 説明から読み取れる挙動:

  • Yarn が Corepack によって管理されている場合
    yarn install --immutable
  • それ以外の場合 (従来の Yarn 1 など)
    yarn install --frozen-lockfile

実際のテンプレートでは、環境に応じて上記を切り替えるための記述に 1 行置き換えが入っています(擬似コードイメージ):

dockerfile
# 以前(イメージ)
RUN yarn install --frozen-lockfile

# 以後(イメージ)
# Corepack 管理の Yarn なら --immutable、それ以外は --frozen-lockfile を使用
RUN if yarn -v 2>/dev/null | grep -q 'berry'; then \
      yarn install --immutable; \
    else \
      yarn install --frozen-lockfile; \
    fi

※上記は動作イメージであり、実際のテンプレートの条件分岐ロジックや判定方法は PR 内の実装に依存します。

なぜこの変更なのか

  • Yarn 1 系では厳密な lockfile 検証に --frozen-lockfile を使用
  • Yarn 2+ / Berry では --immutable オプションが推奨であり、--frozen-lockfile は非推奨・無視される、あるいは望まない挙動になる可能性がある
  • Corepack 経由で Yarn を使うプロジェクトが増えており、その場合 Yarn 2+ を前提に --immutable を使うのが自然

Rails の Dockerfile テンプレートでこれを自動的に切り替えることで、開発者が自分で Dockerfile を調整しなくても「環境にあった正しいフラグ」が使われるようになります。


  1. 影響範囲・注意点

影響範囲

  • 新しく生成される Rails アプリの Dockerfile のみ影響を受けます。
    • 既存アプリの Dockerfile は自動では更新されません。
  • Yarn を使ったアセットビルド (Webpacker / jsbundling-rails / Vite など) を Docker 内で行っている場合、その yarn install フェーズに影響します。

実務的な影響

  • Corepack + Yarn 2+ (Berry) を使っている人にとっては改善:
    • 推奨される --immutable が使われることで、lockfile と node_modules の不整合が厳密に検出され、再現性の高いビルドが保証されます。
  • Yarn 1 を従来通り使っている人にとっても互換性維持:
    • これまで通り --frozen-lockfile が使われるため、挙動は本質的に変わりません。

注意点

  • lockfile (yarn.lock) を手で編集したり、package.json だけ変更して yarn.lock を更新していない場合、
    • --immutable / --frozen-lockfile いずれでも Docker ビルドがエラーで止まる ことがあります。
    • その場合はローカルで yarn install を正しく実行し、更新済みの yarn.lock をコミットしてからビルドする必要があります。
  • CI や本番ビルドで Dockerfile をベースにしている場合、Yarn のバージョンや Corepack の導入状況により yarn install のオプションが変わることで、今まで黙認されていた lockfile 不整合がエラーとして顕在化する可能性があります。

  1. 参考情報 (あれば)

#57063 [ci skip] Improve readability of note about redefining the id column

マージ日: 2026/3/26 | 作成者: @GyuhaWang

  1. 概要 (1-2文で)
    Active Record Basics ガイド内で、「id カラムを再定義した場合の挙動」を説明している注意書きのエラーメッセージ表記を、1行で見やすくなるように整形したドキュメント変更です。コードや挙動には一切手を加えていません。

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

  • 対象ファイル: guides/source/active_record_basics.md

  • id カラムを自前で定義したときに Active Record がどのようなエラーを出すか」を示す注意書き部分にある エラーメッセージの表記 が、複数行に分割されていて読みにくかったため、1行にまとめる変更が行われました。

  • 実際の diff は概ね以下のようなイメージです(内容は同じで、改行位置のみ変更):

    md
    **Before (イメージ)**
    
    Note: If you redefine `id` column, Rails will raise an error like:
    `ActiveRecord::UnknownPrimaryKey:
    Unknown primary key for table users in model User.`
    
    **After (イメージ)**
    
    Note: If you redefine `id` column, Rails will raise an error like:
    `ActiveRecord::UnknownPrimaryKey: Unknown primary key for table users in model User.`
  • これにより、ガイドを読むときにエラーメッセージ全体を一目で把握できるようになります。

  1. 影響範囲・注意点
  • 影響範囲は Rails ガイドの文章表現のみ で、フレームワーク本体の挙動・API・パブリックインターフェイスには変更ありません。
  • id カラム再定義時に ActiveRecord::UnknownPrimaryKey が発生するという仕様やそのメッセージ内容自体は従来どおりです。
  • コードや既存アプリケーションへの互換性の影響はありません。テストや CHANGELOG の更新も不要なレベルのドキュメント修正として扱われています。
  1. 参考情報 (あれば)
  • 該当ガイド: Active Record Basics(id カラムの扱いと主キーの説明セクション)
  • エラークラス: ActiveRecord::UnknownPrimaryKey
  • PR: #57063 “[ci skip] Improve readability of note about redefining the id column”

#57064 Bump Github Action upload-artifactversion to 7

マージ日: 2026/3/26 | 作成者: @neumayr

  1. 概要 (1-2文で)
    Rails が生成する GitHub Actions CI ワークフローテンプレートにおいて、actions/upload-artifact のバージョン指定を最新の v7 に更新する PR です。アプリケーションとプラグインの両方の GitHub CI テンプレートが対象です。

  1. 変更内容の詳細

対象ファイルは、Rails のジェネレータが作る GitHub Actions 設定テンプレートです。

  • railties/lib/rails/generators/rails/app/templates/github/ci.yml.tt
  • railties/lib/rails/generators/rails/plugin/templates/github/ci.yml.tt

これらの中で、GitHub Actions の upload-artifact アクションのバージョン指定が更新されています。

イメージとしては、以下のような変更です(実際の前バージョンは v4 や v3 などですが、概念的にはこのような差分):

yaml
- uses: actions/upload-artifact@v6
+ uses: actions/upload-artifact@v7
  with:
    name: test-results
    path: tmp/test-results

つまり、Rails new や Rails plugin new で生成される .github/workflows/ci.yml が、今後は actions/upload-artifact@v7 を使うようになります。

PR の Motivation から分かる点:

  • 既存のテンプレートが GitHub Actions 公式の最新リリースに追随していなかったため更新。
  • 先行する関連 PR (#56773) を踏まえたフォローアップ的な変更。

コードとしてのロジック追加や Ruby 側の仕様変更は一切なく、YAML テンプレートのバージョン指定のみです。


  1. 影響範囲・注意点

影響範囲:

  • この PR のマージ以降に rails new / rails plugin new で生成されるプロジェクトの GitHub Actions CI で、actions/upload-artifact@v7 が利用されます。
  • 既存プロジェクトの CI 設定は自動では変更されないため、影響を受けるのは「新しく生成される」ワークフローのみです。

注意点:

  • v7 は GitHub 公式アクションのメジャーバージョンアップなので、GitHub 側のリリースノートにある挙動変更や非推奨事項を確認した上で、既存プロジェクトも必要に応じて手動で追随するとよいです。
  • Rails 側では artifacts のパスやオプションには手を入れていないため、通常はそのまま問題なく動作する想定です。

  1. 参考情報 (あれば)

#57067 Classify mysql error 1046 (ER_NO_DB_ERROR) as ConnectionFailed

マージ日: 2026/3/26 | 作成者: @clayharmon

  1. 概要 (1-2文で)
    MySQL のエラーコード 1046「No database selected」を、ActiveRecord::ConnectionFailed として扱うようにした変更です。これにより、接続プール使用時などに TCP 接続がサイレントに張り替わった場合でも、Rails の自動リトライ機構が働くようになります。

  1. 変更内容の詳細

背景・問題点

  • MySQL 接続において、ロードバランサやプロキシなどの影響で TCP コネクションが「静かに」切断・再接続されるケースがあります。
  • mysql2 はこの場合、自動的に新しい TCP 接続を張り直しますが、再接続後に USE database_name を自動で実行しません。
  • その結果、次のクエリ実行時に MySQL 側でエラー 1046 (ER_NO_DB_ERROR: No database selected) が発生します。
  • 現状の Rails では、1046 に対する特別扱いがなく、translate_exception 内で ActiveRecord::StatementInvalid に分類されます。
  • retryable_connection_error?ActiveRecord::ConnectionFailed 等のみをリトライ対象とするため、1046 が原因の失敗では with_raw_connection の組込みリトライが動作しません。

この PR は、1046 を「接続周りの問題(実質的な接続リセット)」として扱い、自動リトライ対象に含めることで、アプリケーションコード側での個別対処を不要にすることを狙っています。

実際の変更点

  1. MySQL エラーコードの定数追加

    abstract_mysql_adapter.rb に以下を追加:

    ruby
    ER_NO_DB_ERROR = 1046
  2. 例外変換 (translate_exception) の分岐追加

    既に 2006 (CR_SERVER_GONE_ERROR), 2013 (CR_SERVER_LOST) を ConnectionFailed にマッピングしている分岐に、1046 も含めるよう変更:

    ruby
    case error.errno
    when CR_SERVER_GONE_ERROR, CR_SERVER_LOST, ER_NO_DB_ERROR
      ActiveRecord::ConnectionFailed.new(message, error)
    else
      # 既存の処理
    end

    ※正確な記述は PR のコードに依存しますが、趣旨としてはこのように 1046 を ConnectionFailed 扱いにしている、という変更です。

  3. テスト追加

    activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb に、1046 が投げられたときに ActiveRecord::ConnectionFailed に変換されることを検証するテストが追加されています。

    概ね以下のようなイメージのテストです:

    ruby
    def test_mysql_error_1046_is_treated_as_connection_failed
      error = Mysql2::Error.new("No database selected")
      error.errno = 1046
    
      assert_raises(ActiveRecord::ConnectionFailed) do
        @connection.translate_exception("SELECT 1", "SELECT 1", error)
      end
    end
  4. CHANGELOG 更新

    activerecord/CHANGELOG.md に、MySQL エラー 1046 を ConnectionFailed として扱うようになった旨が追記されています。


  1. 影響範囲・注意点

影響範囲

  • 対象: mysql2 アダプタを利用している Rails アプリケーション
  • 主に影響するケース:
    • コネクションプーリングを利用している
    • プロキシ/ロードバランサなどが TCP コネクションをサイレントに切り替える可能性がある環境 (例: MySQL Proxy, RDS Proxy, L4/L7 LB など)
  • この変更により:
    • 「No database selected (1046)」が発生した場合、単発のクエリエラーではなく「接続が壊れた」と判断され、with_raw_connection の内部リトライが走ります。
    • リトライ後の接続では USE database_name が正しく発行されるため、アプリケーションレベルでは一時的な 1046 の影響を受けにくくなります。

挙動上の注意点

  • これまで:
    • 1046 は ActiveRecord::StatementInvalid として扱われ、アプリケーションコードで rescue ActiveRecord::StatementInvalid している場合に捕捉されていた可能性があります。
  • 今後:
    • 同じケースが ActiveRecord::ConnectionFailed として扱われるようになります。
    • アプリコードでエラー種別に依存した rescue をしている場合は、そのロジックが変わる可能性があります。
    • 一方で、ConnectionFailed は内部で再接続・リトライを試みるため、表面上は例外がアプリ層に上がらないケースも増えます。

運用面では、「原因不明の No database selected が sporadic に出ていた」のが解消される(または頻度が減る)可能性が高く、典型的にはプラスの変更と考えられます。


  1. 参考情報 (あれば)
  • 類似として既に ConnectionFailed 扱いされている MySQL エラー:
    • 2006: CR_SERVER_GONE_ERROR (MySQL server has gone away)
    • 2013: CR_SERVER_LOST (Lost connection to MySQL server)
  • translate_exceptionretryable_connection_error? は、ActiveRecord のコネクション管理と自動リトライの中核ロジックであり、今回の変更もその一環です。
    これらをカスタマイズしているアプリ/ライブラリでは、1046 の扱いが変わる点を念頭に置くとよいです。

#57069 Make fast behavior the default for NumberToDelimitedConverter

マージ日: 2026/3/26 | 作成者: @willnet

  1. 概要 (1-2文で)
    number_to_delimited などの数値区切り系ヘルパーで、オプション未指定時にも常に高速化された経路(fast path)が使われるようにデフォルト動作が修正されました。これにより、従来は暗黙に実行されていた低速な処理が、明示的に指定した場合にのみ使われるようになります。

  1. 変更内容の詳細

背景

  • Rails では number_to_delimited(例: 10000 -> "10,000")といったヘルパーに対して、高速化(fast path)が既に導入されていました。
  • しかし、delimiter_pattern のデフォルト値に DEFAULT_DELIMITER_REGEX が設定されていたため、オプションを何も渡さない通常利用 (number_to_delimited(10000)) では、依然として古い低速な処理が実行されていたという問題がありました。
  • 高速経路を使うには、delimiter_pattern: nil を明示する必要があり、直感的でない上、意図した挙動とも考えにくい状態でした。

今回の変更の要点

  • NumberToDelimitedConverter 内の DEFAULT_DELIMITER_REGEX が削除されました。
  • これにより、delimiter_pattern が明示的に指定されていない場合は「パターンなし」とみなされ、高速な処理経路が使われるようになります。

概念的なイメージ:

従来(問題のある挙動):

ruby
# 実際には delimiter_pattern にデフォルトの正規表現が入る
# -> 正規表現ベースの低速な処理が使われる
number_to_delimited(10000)
# => 古い(遅い)経路

# 明示的に nil を渡したときのみ高速化される
number_to_delimited(10000, delimiter_pattern: nil)
# => 新しい(速い)経路

変更後:

ruby
# オプション未指定の場合は delimiter_pattern なし
# -> 常に高速な経路が使われる
number_to_delimited(10000)
# => 新しい(速い)経路

# delimiter_pattern を指定したときだけ、従来どおり正規表現ベースの処理
number_to_delimited(10000, delimiter_pattern: /.../)
# => 低速だが柔軟な経路

実際の差分は小さく、activesupport/lib/active_support/number_helper/number_to_delimited_converter.rb から DEFAULT_DELIMITER_REGEX の定義・利用が削除され、デフォルトで fast path が選ばれるように整理されています。


  1. 影響範囲・注意点

影響範囲

  • 影響を受けるメソッド
    主に ActiveSupport::NumberHelpernumber_to_delimited(およびそれを内部で利用するヘルパー)が高速になる影響を受けます。
  • デフォルト利用の場合の挙動
    多くのアプリケーションは number_to_delimited(1234567) のようにオプション無しで使っているため、
    • 表示フォーマットは変わらず
    • 内部実装のみが高速化
      される想定です。

注意点

  • delimiter_pattern を使っていない場合
    ほぼ影響は「パフォーマンス向上」のみで、フォーマット変更などの互換性問題は基本的にありません。
  • delimiter_pattern を積極的に使っている場合
    • 明示的に delimiter_pattern: /.../ を渡しているコードは、従来どおりそのパターンに基づく処理(低速)が行われます。
    • つまり、「delimiter_pattern を指定する=柔軟だが遅い経路を選択する」という意図がより明示的になりました。
  • パフォーマンステスト
    大量の数値フォーマットを行う箇所(例えば、レポート生成やダッシュボードのテンプレート)では、処理時間が変わる可能性があるため、ベンチマークを持っているプロジェクトでは差分計測すると効果が確認しやすいです。

  1. 参考情報 (あれば)

#57054 Cherry-pick security release commits onto main branch

マージ日: 2026/3/23 | 作成者: @jhawthorn

  1. 概要 (1–2文)
    このPRは、セキュリティリリースで入った修正を main ブランチにチェリーピックしたもので、主に Active Storage 周りのファイルアクセス/ダウンロードの安全性向上と、Action View のタグ生成・HTMLセーフティ関連の修正が含まれています。例外画面テンプレートや String#html_safe 周りも含めて、XSS/ディレクトリトラバーサル等の脆弱性を防ぐための変更と、それを担保するテスト追加が中心です。

  1. 変更内容の詳細

※実際の diff 全文は提示されていないため、ファイル構成と典型的な Rails セキュリティ修正パターンから読み取れる範囲での解説になります。

2-1. Action Pack: 例外画面テンプレート

  • 変更ファイル:
    • actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb (+1/-1)
    • actionpack/test/dispatch/debug_exceptions_test.rb (+12/-1)

想定される内容:

  • 例外ページ(エラーページ)のレイアウトで、例外メッセージやパスなどを出力する箇所のエスケープ方法を調整し、XSS の可能性をさらに下げた変更と思われます。
  • debug_exceptions_test.rb にテストが追加されているので、「特定のエラー内容を含むリクエストを投げても、レスキューページが不正な HTML/JS をそのまま実行してしまわない」ことを確認しているはずです。

ポイント:

  • 例外メッセージはしばしば外部入力を経由するため、rawhtml_safe を安易に使わず、h() / 自動エスケープを徹底する方向の修正がよく行われます。

2-2. Action View: Tag Helper と HTML セーフティ

  • 変更ファイル:
    • actionview/lib/action_view/helpers/tag_helper.rb (+5/-2)
    • actionview/test/template/tag_helper_test.rb (+29/-0)
    • activesupport/lib/active_support/core_ext/string/output_safety.rb (+11/-2)
    • actionview/CHANGELOG.md (+4/-0)

主な意図:

  • タグヘルパ(tag, content_tag, tag.input, tag.a など)の出力や属性値の扱いに関する安全性の強化。
  • String#html_safeActiveSupport::SafeBuffer 周りの仕様・挙動をより堅牢にし、「一度 html_safe が付いた文字列にユーザー入力を結合した結果が意図せずセーフ扱いになる」ような事態を防ぐ方向の変更が入っている可能性が高いです。

よくあるパターンの例:

ruby
unsafe = params[:q]                 # ユーザー入力
safe   = "<strong>Hello</strong>".html_safe

# 以前のバグパターン:
result = safe + unsafe              # -> result が html_safe のまま扱われ XSS となりうる

# 修正後:
# 結合時に unsafe が含まれると SafeBuffer が unsafe として扱われる or 適切にエスケープされる

tag_helper_test.rb の追加テストから推測されるポイント:

  • 属性値に html_safe な文字列を渡した場合と、プレーン文字列を渡した場合の扱いの違いが明示的にテストされている可能性があります。
  • デフォルトで自動エスケープが効き、html_safe を付けた文字列のみを信頼する挙動をより厳密にしているはずです。

CHANGELOG.md には:

  • 「タグヘルパが特定のケースで HTML セーフティを誤認していた問題を修正」
  • などの記述が追加されているはずです。

2-3. Active Storage: ディスク・ストリーミング・Blob 周りのセキュリティ強化

  • 変更ファイル(抜粋):
    • activestorage/app/controllers/active_storage/disk_controller.rb (+4/-0)
    • activestorage/app/controllers/concerns/active_storage/streaming.rb (+8/-1)
    • activestorage/app/models/active_storage/blob.rb (+21/-0)
    • activestorage/lib/active_storage/service/disk_service.rb (+46/-2)
    • activestorage/lib/active_storage/errors.rb (+4/-0)
    • activestorage/lib/active_storage.rb (+14/-0)
    • activestorage/lib/active_storage/engine.rb (+1/-0)
    • activestorage/test/... 一式 (controllers, models, services) 合計 ~200行以上のテスト追加
    • activestorage/CHANGELOG.md (+31/-0)

ここが今回の PR のメインと思われます。
典型的なセキュリティ修正内容は次のようなものです:

2-3-1. DiskService: パス検証・ディレクトリトラバーサル対策

disk_service.rb が +46/-2 と大きく変わっているため、ローカルディスクへの保存/読み出しパスの扱いを強化している可能性が高いです。

よくある修正内容:

  • join した結果が root パス配下から外れていないかの検証:
ruby
# イメージ (実際のコードは異なる可能性があります)
def path_for(key)
  path = File.join(@root, key)
  unless path.start_with?(File.expand_path(@root) + File::SEPARATOR)
    raise ActiveStorage::InvalidPathError, "Invalid key: #{key.inspect}"
  end
  path
end
  • ../ を含むキーや、絶対パス形式、不正なエンコーディングを持つキーなどを拒否。
  • 失敗時には新たに追加された ActiveStorage::Errors 内のカスタムエラー(例: ActiveStorage::InvariableError, ActiveStorage::IntegrityError, ActiveStorage::InvalidPathError 等)を投げるように変更。

テスト (activestorage/test/service/disk_service_test.rb +137行) では:

  • 「正常なキーは保存/読み出しできる」
  • 「ディレクトリトラバーサルっぽいキーはエラーになる」
  • 「存在しないファイルや壊れたパスでの動作」
    などが細かくチェックされているはずです。

2-3-2. DiskController / Streaming: レスポンスヘッダと認可の厳格化

active_storage/disk_controller.rbconcerns/active_storage/streaming.rb の修正から推測できる点:

  • 一時 URL、署名付き URL の検証をより厳格にしている可能性:

    • トークンのタイムスタンプ・署名・パラメータの整合性チェックの強化。
    • 不正パラメータ(例: 想定外の dispositionfilename)が入った場合に 4xx を返すように変更。
  • レスポンスヘッダの調整:

    • Content-Disposition を生成する際に、ファイル名を適切にエスケープ。
    • 任意のレスポンスヘッダインジェクションを防ぐため、改行やコントロール文字を除去。
    • Range リクエスト/ストリーミング処理での不正なオフセット・サイズ指定への防御。

サンプルイメージ:

ruby
# (イメージ)
def disposition_with_safe_filename(filename, disposition: :attachment)
  filename = ActiveStorage::Filename.new(filename).sanitized
  %(#{disposition}; filename="#{filename}"; filename*=UTF-8''#{ERB::Util.url_encode(filename)})
end

2-3-3. Blob モデル・エラークラスの追加

  • active_storage/blob.rb (+21)

    • 不正なメタデータ/コンテンツタイプの扱いを厳格化。
    • 署名付き ID から Blob を見つける際の find_signed の失敗ハンドリング強化。
    • 無効な状態の Blob を参照しようとした場合、新しいエラーを投げる等。
  • active_storage/errors.rb (+4)

    • 新規エラークラスの追加:
      • 例: ActiveStorage::FileNotFoundError, ActiveStorage::IntegrityError など(実際の名前は diff 要確認)。
    • これにより、アプリ側で特定の Active Storage エラーを rescue しやすくなる。
  • active_storage.rb / engine.rb

    • 新しい設定オプションや、セキュリティ関連のデフォルト挙動 (例: 「ディスクサービスで不正キー使用時は例外を投げる」等) をフックするためのコードが追加されている可能性があります。

  1. 影響範囲・注意点

3-1. セキュリティ観点での影響

  • この PR は「セキュリティリリースの cherry-pick」であるため、Rails アプリは基本的にこの変更を取り込むことが推奨されます。
  • 主な守備範囲:
    • XSS (エラーページ、ActionView のタグヘルパ、html_safe/SafeBuffer の扱い)
    • ディレクトリトラバーサル・任意ファイルアクセス (Active Storage Disk Service)
    • 不正な署名付き URL や不正ヘッダによるレスポンス分割などの防止

3-2. 既存アプリへの互換性・挙動変化

  1. Action View / SafeBuffer 周り

    • これまで動いていた「やや危ない書き方」がエラーになったり、エスケープされて表示されるようになる可能性があります。
    • 具体的には:
      • html_safe な文字列とユーザー入力を + 演算子などで結合していた箇所が、今後はエスケープされるため、見た目が微妙に変わることがあります。
      • テストで assert_equal "<tag>#{raw(user_input)}</tag>" のような書き方をしていた場合、期待値を見直す必要があるかもしれません。
  2. Active Storage Disk Service

    • カスタムで service: :disk を拡張している場合や、内部的に DiskService#path_for などを直接呼んでいる場合:
      • 不正なキーを渡していると例外が投げられるようになり、これまで「たまたま動いていた」ケースが壊れる可能性があります。
    • ローカルファイルシステムを多少「流用」していて、キーに /.. を含めていたような実装は、セキュリティ観点からも見直す必要があります。
  3. Active Storage コントローラのレスポンス変更

    • 一部の disk / blob / direct_uploads コントローラに追加テストが入っているため:
      • 不正パラメータを渡した際のステータスコードやレスポンスが変わっている可能性があります。
      • 正常系の挙動は維持されるはずですが、E2E テストでレスポンスヘッダやファイル名を厳密に比較している場合は差分が出るかもしれません。

3-3. マイグレーション・設定変更の必要性

  • 変更ファイルから見る限り、DB スキーマ変更 (migration) は含まれていません。
  • config/storage.yml などの設定形式も変わっているようには見えません。
  • ただし ActiveStorage モジュール (lib/active_storage.rb) に新しい config オプションが追加されている可能性はあるため、CHANGELOG で確認しておくとよいです。

  1. 参考情報 (あれば)
  • この PR 自体は「セキュリティリリースコミットの cherry-pick」という位置付けなので、詳細な説明は各セキュリティリリースのアナウンス(Rails の公式ブログ / セキュリティアドバイザリ)にまとまっている可能性が高いです。
  • 追って確認しておくとよい資料:
    • Rails 公式ブログのセキュリティリリース告知 (2026-03 頃の記事)
    • 該当する Rails バージョンの CHANGELOG.md (特に actionview / activestorage / activesupport)
    • Rails Guides:
  • アプリ側でやるべきこと:
    • html_saferaw を使っている箇所を改めて洗い出し、「ユーザー入力を混ぜていないか」「SafeBuffer の挙動に依存していないか」をチェックする。
    • Active Storage で Disk サービスを使っている場合、独自拡張やキーの扱い(ファイル名の生成など)を確認し、不正なキーを渡していないか検証する。

#56877 Fix FrozenError when deriving foreign key from inverse with composite foreign key

マージ日: 2026/3/23 | 作成者: @kirs

  1. 概要 (1-2文で)
    belongs_to 側が複合外部キー(配列の foreign_key)を持ち、その逆側の has_one / has_manyinverse_of から外部キーを導出する場合に FrozenError が発生していたバグを修正する PR です。
    凍結済みの配列に対して破壊的メソッド map! を呼び出していた実装を、非破壊的な map { ... }.freeze に置き換えることでエラーを回避しています。

  1. 変更内容の詳細

問題のシナリオ

以下のような構成を想定すると分かりやすいです。

ruby
class Comment < ApplicationRecord
  # 複合外部キー (例)
  belongs_to :blog_post,
    foreign_key: [:blog_id, :post_id]
end

class BlogPost < ApplicationRecord
  has_many :comments, inverse_of: :blog_post
end

BlogPost 側の has_many :commentsinverse_of: :blog_post により、
Commentbelongs_to :blog_post から外部キー情報を「導出 (derive)」します。

  • belongs_to 側の foreign_key配列(複合外部キー) の場合
  • Active Record の derive_foreign_key が、belongs_to 側の reflection が保持している メモ化済み・凍結済みの配列 を返す
  • その返ってきた配列に対して、従来コードが map!(破壊的変更)を行おうとして FrozenError が発生

というのが不具合の根本原因です。

コード上の変更ポイント

該当箇所は activerecord/lib/active_record/reflection.rb の外部キー導出ロジックです。

元のイメージ(概念的なもの):

ruby
def derive_foreign_key
  keys = inverse_of.foreign_key # ここで凍結済み配列が返るケースがある
  keys.map! { |key| key.to_s }  # ← FrozenError: can't modify frozen Array
  keys.freeze
end

これを、破壊的な map! をやめて、新しい配列を生成してから凍結する 形に変更しています:

ruby
def derive_foreign_key
  keys = inverse_of.foreign_key
  keys = keys.map { |key| key.to_s }.freeze
end

実際の PR では、map! + freeze というパターンを
map { ... }.freeze に置き換えただけの、最小限の修正です。

テストの追加

以下のファイルにテスト・テスト用モデルが追加されています:

  • activerecord/test/cases/reflection_test.rb
    • 逆側 (inverse_of) から複合外部キーを導出する際に FrozenError が出ないことをカバーするテストを追加。
  • activerecord/test/models/sharded/blog_post.rb
  • activerecord/test/models/sharded/comment.rb

sharded 名前空間のテスト用モデルを使って、実際に複合外部キーの関連を定義した状態で
reflection が正しく動作することを検証しています。


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

    • ActiveRecord::Reflection における「外部キー配列の導出・変換部分」に限定された変更です。
    • 主に以下の条件に当てはまるアプリケーションに影響します:
      • belongs_to で複合外部キー(配列の foreign_key)を使用している
      • その逆側の has_one / has_manyinverse_of を指定している
    • これらの条件を満たす関連で、以前は FrozenError によりアソシエーション定義・利用時にエラーになっていたケースが、正常に動くようになります。
  • 挙動の互換性:

    • 外部キー配列の中身(キー名の変換結果)は変わっておらず、「凍結済みの配列を返す」という契約も維持されています。
    • 変更点は「どの配列インスタンスを凍結するか(元の配列を壊さない)」だけなので、一般的な利用では挙動互換です。
  • パフォーマンス:

    • map! をやめて map で新しい配列を作るため、理論上は一時オブジェクトが増えますが、配列サイズは外部キー数(多くて数個)に限られるため、実務上のオーバーヘッドは無視できるレベルです。
  • 注意点:

    • 「reflection が返す foreign_key 配列は凍結されている」前提に依存したコード(破壊的変更をかけようとするなど)は、もともと誤りですが、この修正によっても引き続き凍結済みであるため、そのようなコードは引き続き FrozenError になります。
    • この PR は 「他 reflection の内部状態を壊さない」 方向の修正なので、ライブラリ作者などで ActiveRecord の reflection API を内部まで触っている場合には、むしろ安全性が増しています。

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/56877
  • 関連クラス:
    • ActiveRecord::Reflection::AssociationReflection
    • inverse_of / foreign_key の導出ロジック周り
  • 文脈:
    • Rails の複合外部キーサポートやシャーディング関連(sharded モデル)をテストする一環で、reflection の安全性・不変性の問題が顕在化したものと考えられます。

#57031 Add test coverage for ActiveModel::Errors#where

マージ日: 2026/3/23 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    ActiveModel::Errors#where に対する専用テストが追加され、属性・タイプ・オプションごとのフィルタリング挙動が網羅的に検証されるようになりました。これにより、既存の公開APIの振る舞いがテストで明示され、将来的なリグレッション防止に役立ちます。

  1. 変更内容の詳細

このPRでは activemodel/test/cases/errors_test.rbErrors#where 専用のテストが 5 件追加されています。実装コードの変更はなく、テストコードのみの追加です。

追加されたテストの内容は以下の通りです。

1) 属性によるフィルタリング

複数属性に対してエラーを追加し、where(:name)name 属性のエラーだけを返すことを検証しています。

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

ruby
person = Person.new
person.errors.add(:name, :blank)
person.errors.add(:email, :invalid)

errors = person.errors.where(:name)

assert_equal 1, errors.size
assert errors.all? { |error| error.attribute == :name }

ポイント:

  • 同一オブジェクトに複数属性のエラーを追加
  • where(:attribute) で特定属性だけを抽出できることを確認

2) 属性 + タイプによるフィルタリング

同じ属性に対して異なるタイプのエラーを追加し、属性とタイプの両方でフィルタできることを検証しています。

例:

ruby
person.errors.add(:name, :blank)
person.errors.add(:name, :too_short, count: 3)

errors = person.errors.where(:name, :too_short)

assert_equal 1, errors.size
assert errors.all? { |e| e.attribute == :name && e.type == :too_short }

ポイント:

  • where(attribute, type) の第二引数でエラータイプによる絞り込みができることを確認
  • 同一属性・別タイプのエラーが正しく除外されるかをテスト

3) 属性 + タイプ + オプションによるフィルタリング

タイプが同じでも、オプション(例: count: 2)が異なるエラーを区別できることを検証しています。

例:

ruby
person.errors.add(:name, :too_short, count: 2)
person.errors.add(:name, :too_short, count: 3)

errors = person.errors.where(:name, :too_short, count: 2)

assert_equal 1, errors.size
assert_equal 2, errors.first.options[:count]

ポイント:

  • where(attribute, type, **options) のオプションマッチングを確認
  • オプションが完全一致するものだけを返す挙動を担保

4) マッチしない場合は空配列を返す

存在しない属性、タイプ、オプションを指定した場合、where が空配列 [] を返すことを確認しています。

例:

ruby
person.errors.add(:name, :blank)

assert_equal [], person.errors.where(:email)
assert_equal [], person.errors.where(:name, :too_short)
assert_equal [], person.errors.where(:name, :blank, count: 2)

ポイント:

  • 「見つからないときに nil ではなく空配列を返す」という契約をテストで固定
  • 呼び出し側ロジックが each などを前提にしても安全

5) 戻り値の型検証

where の結果に含まれる要素がすべて ActiveModel::Error インスタンスであることを確認しています。

ruby
person.errors.add(:name, :blank)
errors = person.errors.where(:name)

assert errors.all? { |e| e.is_a?(ActiveModel::Error) }

ポイント:

  • where が内部配列やハッシュではなく、ActiveModel::Error オブジェクトの配列を返すことを明示
  • 公開APIの「戻り値の型」をテストで保証することで、将来の内部実装変更時の破壊的変更を防ぐ

  1. 影響範囲・注意点
  • 実装コードの挙動は変わっていない
    追加されたのはテストのみであり、ActiveModel::Errors#where の実装自体には変更がありません。

  • Errors#where の仕様がテストとして固定化された
    以下の点が挙動として「公式に」固定されたと考えられます。

    • フィルタ条件:
      • 第1引数: 属性 (attribute)
      • 第2引数: タイプ (type, 任意)
      • 第3引数以降: オプション (options, 任意・ハッシュマッチ)
    • 戻り値: ActiveModel::Error の配列
    • マッチしない場合: 空配列 []
  • 今後のリファクタ時の制約になる可能性
    テストが追加されたことで、たとえば

    • 戻り値を別オブジェクトに変える
    • オプションマッチングの仕様を変える といった変更は、これらのテストを更新しない限り許されなくなります。where に依存しているアプリケーションにとっては安全性向上になりますが、Rails本体の内部実装変更の自由度は相対的に下がります。

  1. 参考情報 (あれば)
  • 対象メソッド: ActiveModel::Errors#where
    Railsガイド(Active Model バリデーション)や API ドキュメント上で公開されているフィルタ用メソッドであり、errors.where(:name, :blank, count: 2) のように条件付きでエラーを取得する用途に使われます。
  • このPRによって、errors.where現状の振る舞いを前提にしたコードを書いても、将来の Rails バージョンで変わりにくくなったと考えられます。

#57032 Add test coverage for IsolatedExecutionState#key?, #delete, and #context

マージ日: 2026/3/23 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    ActiveSupport::IsolatedExecutionState の公開メソッド #key?, #delete, #context に対して、これまで不足していたテストが追加されました。機能追加や仕様変更ではなく、テストカバレッジを補完するためのPRです。

  1. 変更内容の詳細

追加されたテストの内容

activesupport/test/isolated_execution_state_test.rb に7つのテストが追加されています。

#key? に関するテスト (3件)

  1. 既存キーに対して true を返す

    • 事前に IsolatedExecutionState[:foo] = :bar のようにキーをセットし、
      IsolatedExecutionState.key?(:foo)true であることを検証。
  2. 存在しないキーに対して false を返す

    • セットしていないキーに対して key? を呼び、false を返すことを検証。
  3. 内部状態が nil の場合でも例外を出さず扱えること

    • 内部ストレージを「未初期化状態」(nil) にした上で key? を呼び出し、例外が起きないこと・適切に扱えることを確認。
    • これは IsolatedExecutionState の内部状態初期化ロジックが、key? 経由でも安全に動作することの保証になります。

#delete に関するテスト (2件)

  1. キーを削除し、その値を返す

    • 例:
      ruby
      IsolatedExecutionState[:foo] = :bar
      value = IsolatedExecutionState.delete(:foo)
      
      assert_equal :bar, value
      refute IsolatedExecutionState.key?(:foo)
    • delete の戻り値が削除した値であることと、実際にキーが削除されていることを検証。
  2. 存在しないキーに対して nil を返す

    • 未定義のキーで delete を呼び、nil が返ることを確認。
    • Ruby の Hash#delete と同様の直感的な挙動が保証されます。

#context に関するテスト (2件)

  1. :thread 隔離レベルでは現在のスレッドを返す

    • 隔離レベルを :thread に設定した状態で
      ruby
      assert_equal Thread.current, IsolatedExecutionState.context
      となることをテスト。
    • 「どの単位で状態を隔離しているか」を知るための context が、スレッド隔離時に正しくスレッドを指すことを確認。
  2. :fiber 隔離レベルでは現在のファイバーを返す

    • 同様に、隔離レベルを :fiber にした状態で
      ruby
      assert_equal Fiber.current, IsolatedExecutionState.context
      となることを確認。
    • Fiber ベースの並行処理(例: async ランタイム)利用時に、コンテキストが正しく Fiber 単位で分離されていることをテストで保証しています。

テスト全体の状態

  • isolated_execution_state_test.rb 内のテスト数は合計13件となり、27アサーション。
  • すべてグリーン (0 failures)。

  1. 影響範囲・注意点
  • 本PRはテストファイルのみ変更しており、実装コード (ActiveSupport::IsolatedExecutionState 本体) には手が入っていません。
  • したがって、ランタイムの挙動変更や互換性への影響はありません。既存アプリケーションの動作が変わることはありません。
  • ただし、今後このクラスの実装を変更する際に、
    • key? が「未初期化状態(nil)でも安全に動作する」こと
    • delete の戻り値や削除ロジック
    • context が隔離レベルに応じて Thread.current / Fiber.current を正しく返すこと
      がテストで強く縛られるため、実装変更時にはこれらの仕様を意識する必要があります。

  1. 参考情報 (あれば)
  • ActiveSupport::IsolatedExecutionState は、Rails内部で「スレッドやファイバーごとに独立した状態を持つ」ための仕組みで、ログ用コンテキストやトランザクションスコープなどの管理に関係するクラスです。
  • 今回のテスト追加により、並行処理環境での状態管理周りのリファクタリングや最適化を行う際に、key?, delete, context 周りで回帰バグが起きにくくなります。

#57033 [ci skip] Fix punctuation in Rails autoloading and reloading constants docs

マージ日: 2026/3/23 | 作成者: @written-fresh

  1. 概要 (1-2文で)
    このPRは、Railsガイド「autoloading and reloading constants」のドキュメント内にある句読点の誤りを修正するものです。コードや挙動の変更は一切なく、ドキュメント表現のみが微修正されています。

  2. 変更内容の詳細

  • 対象ファイル: guides/source/autoloading_and_reloading_constants.md
  • 差分: 1行の修正(1行追加・1行削除の実質置き換え)
  • 具体的な内容は英語ドキュメントの句読点(ピリオドやカンマなど)の誤りを正しい形に修正しただけであり、文意や説明内容自体は変わっていません。
  • 技術的なサンプルコードや設定例などは変更されていません。
  1. 影響範囲・注意点
  • 影響範囲:
    • Rails本体のオートローディング/リローディングの挙動には一切影響しません。
    • 生成されるガイド(HTML版など)の表示テキストのみが僅かに変わります。
  • 注意点:
    • アプリケーションのアップデート時に、このPRによってコードの修正が必要になるケースはありません。
    • CIを回していない([ci skip])ことからも分かる通り、あくまでドキュメント上の表記修正であると判断できます。
  1. 参考情報 (あれば)
  • PR番号: https://github.com/rails/rails/pull/57033
  • 対象ガイド: Rails Guides – “Autoloading and Reloading Constants”
    (Zeitwerk ベースの autoloading の説明があるセクションで、概念理解に影響しないレベルの句読点修正です。)

#57028 Require Ruby 3.3.1

マージ日: 2026/3/22 | 作成者: @byroot

  1. 概要 (1–2文で)
    Rails本体および関連コンポーネントの対応Rubyバージョンを「3.3.0 以上」から「3.3.1 以上」に引き上げるPRです。理由は、Ruby 3.3.0 に Rails の一部機能を壊すバグがあり、かつCIで複数パッチバージョンをサポートするコストを負えないためです。

  1. 変更内容の詳細

主な変更点
すべての .gemspec ファイルに記載されている required_ruby_version の下限バージョンが 3.3.03.3.1 に変更されています。対象は以下のコンポーネントです:

  • actioncable
  • actionmailbox
  • actionmailer
  • actionpack
  • actiontext
  • actionview
  • activejob
  • activemodel
  • activerecord
  • activestorage
  • activesupport
  • rails (メタgem)
  • railties
  • tools/rail_inspector
  • tools/releaser

変更イメージ(サンプル)
.gemspec はおおむね次のような差分になっています:

ruby
# 変更前
spec.required_ruby_version = ">= 3.3.0"

# 変更後
spec.required_ruby_version = ">= 3.3.1"

コードロジックやAPIには一切手を入れず、サポート対象Rubyバージョンの宣言のみを更新するPRです。

背景となる問題
PR本文にある通り:

  • Ruby 3.3.0 には Rails の一部を壊すバグが存在する
  • その問題に対して、Ruby 3.3.1 で修正が入っている
  • Rails側としては、3.3.0 向けのワークアラウンドを入れたり、CIで複数パッチバージョン(3.3.0/3.3.1)をテストし続けるコストを負わない方針

そのため、公式にサポートする最小バージョンを 3.3.1 に上げることで、3.3.0 特有の不具合切り捨てとテストマトリクス簡素化を図っています。


  1. 影響範囲・注意点

影響範囲

  • Bundlerによるインストール・更新時に、Ruby 3.3.0 以下の環境では Rails 7.2以降(想定)やmaster/main系のgemがインストールできなくなるか、バージョン解決が失敗します。

    • 例:
      bash
      $ ruby -v
      ruby 3.3.0
      $ bundle add rails
      # => Rails の該当バージョンは required_ruby_version >= 3.3.1 のため解決失敗
  • すでに Rails をインストール済みのプロジェクトで、今後 Rails のバージョンを上げる際に、Ruby も 3.3.1 以上に上げる必要が生じます。

Rails アプリ開発者側の対応

  • Ruby 3.3.0 を利用している場合は、Ruby 3.3.1 以上へのアップデートが必須になります。
    • rbenv/rvm/asdf 等のバージョンマネージャを使っている場合は、3.3.1をインストールしてアプリの .ruby-version やGemfileの ruby 宣言を更新します。
  • CI設定でも、3.3.0 で Rails のテストを回している場合は、3.3.1 以上に切り替える必要があります。

注意点

  • このPRは「Ruby 3.3.0 を完全に非推奨扱いにする」ものに近く、3.3.0 上での動作保証は行わないというRailsチームのメッセージと言えます。
  • 開発環境と本番環境のRubyバージョンがズレていると、デプロイ時にRailsのバージョンアップが失敗したり、不整合を生む可能性があるため、全環境で3.3.1以上に揃えることが望まれます。

  1. 参考情報 (あれば)
  • 問題の元PR: https://github.com/rails/rails/pull/57026
    上記でRuby 3.3.0 の不具合や、それに対するRails側の対応方針が議論されていると考えられます。
  • Ruby 3.3.1 リリースノート(Ruby側のバグ修正内容)
    • Ruby公式ニュース: https://www.ruby-lang.org/
    • そこから 3.3.1 のリリースアナウンスを参照することで、Railsに影響していたバグの詳細を確認できます。

#57022 Fix insert_all and upsert_all log messages for anonymous classes

マージ日: 2026/3/21 | 作成者: @byroot

  1. 概要 (1-2文で)
    insert_all / upsert_all 実行時のログ出力で、匿名クラス(anonymous class)の場合でも人間が読めるモデル名が出るように修正したPRです。これにより、例えば AnonymousBook Bulk Insert / AnonymousBook Bulk Upsert のような分かりやすいログメッセージになります。

  1. 変更内容の詳細

何が問題だったか

これまで insert_all / upsert_all のログメッセージで使われるクラス名は、通常 ModelName Bulk Insert / ModelName Bulk Upsert のようになりますが、クラスが匿名クラス(Class.new(ActiveRecord::Base) など)である場合に、ログ上の名前が読みにくい・適切でない形になっていました。

Rails では、テストや一部のメタプログラミング用途で「匿名クラスの ActiveRecord モデル」を使うことがあり、その際のログが判読しづらい状態になっていた、という背景です。

今回の修正内容

変更ファイルは以下の2つです。

  • activerecord/lib/active_record/insert_all.rb (+1/-1)
  • activerecord/test/cases/insert_all_test.rb (+15/-0)

実質的には、insert_all / upsert_all の内部でログメッセージを組み立てる際に使用する「モデル名の決め方」を 1 行修正し、それに対応するテストを追加しています。

ログメッセージ生成部分

元のコードは概ね、

ruby
instrument(:insert_all, ...) do
  # 例: "User Bulk Insert"
  ActiveRecord::LogSubscriber.log("Bulk Insert #{self.name}")
end

のように self.name(クラス名)をそのまま使っていたり、もしくは model.name ベースで文字列を組み立てていました。
今回のPRでは「匿名クラスでも、人間に読める ‘モデル名’ を返すメソッド(あるいはロジック)」を使うように1行差し替えられています。

それにより、匿名クラスにも AnonymousBook のような「匿名モデル用の名前」が割り当てられ、ログが以下のようにきれいになります。

text
AnonymousBook Bulk Insert (0.3ms)  ...
AnonymousBook Bulk Upsert (0.4ms)  ...

テストの追加

activerecord/test/cases/insert_all_test.rb に、15行分のテストが追加されています。
想定されるテストイメージは次のようなものです(実際のコードは多少異なる可能性がありますが、意図は同じです):

ruby
def test_insert_all_logs_with_anonymous_class_name
  klass = Class.new(ActiveRecord::Base) do
    self.table_name = "books"
  end

  assert_logged(/AnonymousBook Bulk Insert/) do
    klass.insert_all([{ title: "foo" }])
  end
end

def test_upsert_all_logs_with_anonymous_class_name
  klass = Class.new(ActiveRecord::Base) do
    self.table_name = "books"
  end

  assert_logged(/AnonymousBook Bulk Upsert/) do
    klass.upsert_all([{ id: 1, title: "bar" }])
  end
end

ポイントは、「匿名クラスの insert_all / upsert_all 実行時に、期待どおり AnonymousBook Bulk Insert/Upsert というパターンのログが出力されること」を検証している点です。


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

    • ActiveRecord の insert_all / upsert_all を利用しているアプリケーションで、ActiveRecord::Base を継承した匿名クラス(テスト用モデルや動的に生成したモデルクラスなど)を使っている場合に、ログに出るクラス名の表記が変わります。
    • 通常の(定義済みの)モデルクラス (User, Book など) を使っている場合のログは、基本的にこれまでと同じ形式のままです。
  • ログパース等への影響

    • ログのテキストを正規表現などでパースしている場合(特に匿名クラスを前提にしていた場合)、クラス名部分が変わることでマッチ条件に影響する可能性があります。
    • ただし、匿名クラスのログを機械的にパースしているケースは多くないと思われるため、実務上の影響は軽微と考えられます。
  • パフォーマンス

    • 変更はログメッセージに用いるクラス名解決だけで、insert_all / upsert_all 自体の性能への影響は事実上ありません。

  1. 参考情報 (あれば)

#57010 Fix invalid HTML in routing error template

マージ日: 2026/3/21 | 作成者: @pardeyke

  1. 概要 (1-2文で)
    Rails のルーティングエラー用レスキューテンプレート(routing_error.html.erb)の HTML 構造が W3C/WHATWG の仕様上不正だったため、<p> タグ内からブロック要素(<h2> / <ol>)を取り除き、正しい HTML 構造になるように修正した PR です。
    これにより、ブラウザやツールによっては崩れていた DOM 構造が、仕様に沿った形で安定して解釈されるようになります。

  1. 変更内容の詳細

何が問題だったか

HTML 仕様上、<p> 要素の中に書けるのは「phrasing content(インライン要素など)」のみであり、見出し(<h2>)やリスト(<ol>)といったブロックレベル要素を <p> の直下に書くことはできません。

元テンプレートでは、例えば以下のような構造になっていたと考えられます(イメージ):

erb
<p>
  このページは見つかりませんでした。
  <h2>Routes</h2>
  <ol>
    <li>...</li>
  </ol>
</p>

このような記述は HTML 的には不正で、ブラウザは <h2><ol> に到達した時点で暗黙的に <p> を閉じてしまい、結果として意図しない DOM 構造になります。

どう修正されたか

routing_error.html.erb から <p> タグの内側にあった <h2><ol> を取り除き、適切な場所でブロックレベル要素として独立させるように変更されています。

変更の方向性としては、次のような形になります(概念的なサンプル):

修正前(不正な HTML の例):

erb
<p>
  The page you were looking for doesn't exist.
  <h2>Possible routes</h2>
  <ol>
    <% @routes.each do |route| %>
      <li><%= route %></li>
    <% end %>
  </ol>
</p>

修正後(仕様に沿った HTML の例):

erb
<p>
  The page you were looking for doesn't exist.
</p>

<h2>Possible routes</h2>
<ol>
  <% @routes.each do |route| %>
    <li><%= route %></li>
  <% end %>
</ol>

実際の diff は +6 / -8 行と小規模で、主にタグの配置・入れ子の整理に留まります。
文言やロジック(Ruby コード)には手を入れず、ビューのマークアップ構造だけを修正しています。


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

    • 対象: Rails アプリケーションで発生する ActionController::RoutingError のデフォルトエラーページ(development/test 環境や consider_all_requests_local が true の場合などに表示されるテンプレート)。
    • 変更点は HTML 構造のみであり、レスポンスのステータスコードやヘッダ、エラー内容自体には影響しません。
  • 実見た目への影響

    • 多くのブラウザは元々暗黙的に <p> を閉じてレンダリングしていたため、見た目はほとんど、あるいは全く変わらない可能性が高いです。
    • ただし、HTML 検証ツールやアクセシビリティ関連ツール、DOM 解析ツール(クローラ、スクレイパー、E2E テストなど)にとっては、より予測可能で正しい DOM 構造になります。
  • カスタムテンプレートを使っている場合の注意点

    • actionpack/lib/action_dispatch/middleware/templates/rescues 以下のテンプレートをベースにして独自の rescue テンプレートを作っている場合、同様に <p> 内にブロックレベル要素を入れていないかを確認するとよいです。
    • 特に、独自のエラーページを HTML5 として厳格に検証したい場合や、アクセシビリティ・SEO を意識している場合、この変更方針に倣ってマークアップを整理することをおすすめします。

  1. 参考情報 (あれば)

この PR は、Rails のエラー画面テンプレートを HTML 仕様に準拠させるための小さなクリーンアップであり、将来的なブラウザ挙動の変化や検証の厳格化に対しても安全側に倒した変更と言えます。


#57017 LogSubscriber: Avoid repeated respond_to? calls

マージ日: 2026/3/21 | 作成者: @byroot

  1. 概要 (1-2文で)
    ActiveSupport::LogSubscriber が、イベントハンドラ呼び出し時に毎回 respond_to? を実行していたのをやめ、事前に「どのメソッドが存在するか」をキャッシュすることでオーバーヘッドを削減する変更です。結果としてログ購読処理のパフォーマンスがわずかに向上し、多数の通知が発生する環境での無駄なメソッド存在チェックが減ります。

  1. 変更内容の詳細

※PR本文に説明が無いため、LogSubscriber の既存実装とタイトルからの推測を含みますが、Rails の一般的なパターンに基づいた技術的に妥当な解説です。

背景

ActiveSupport::LogSubscriberActiveSupport::Notifications のイベントを受け取り、
"process_action.action_controller"def process_action(event)
のように、イベント名に対応するインスタンスメソッドを呼び出す仕組みになっています。

典型的には以下のようなコードが存在します(簡略化):

ruby
def call(subscriber, event)
  method = event.name.split('.').first
  if subscriber.respond_to?(method)
    subscriber.public_send(method, event)
  end
end

ここで、イベントが発生する度に respond_to? が呼ばれるため、高頻度イベントの場合はそこそこのオーバーヘッドになります。

今回の変更点

このPRのタイトル「Avoid repeated respond_to? calls」から読み取れるのは、
respond_to? の結果を都度計算するのではなく、メソッド存在情報をキャッシュして再利用する」ようになった、という点です。

イメージとしては次のような変更が行われています:

ruby
# 変更前(イメージ)
def call(subscriber, event)
  method = event_name_to_method(event.name)
  if subscriber.respond_to?(method)
    subscriber.public_send(method, event)
  end
end

# 変更後(イメージ)
def call(subscriber, event)
  method = event_name_to_method(event.name)
  if supported_method?(subscriber, method)
    subscriber.public_send(method, event)
  end
end

def supported_method?(subscriber, method)
  @supported_methods ||= {}
  @supported_methods[method] ||= subscriber.respond_to?(method)
end

もしくは、クラス単位で一度だけ「ハンドラーメソッドの一覧」を構築するような形になっている可能性もあります。
いずれのパターンでも、同じイベント種別に対しての respond_to? 実行回数を削減することが目的です。

変更差分が +6/-2 行と小さいため、

  • call 内部で respond_to? を直接呼んでいたのを、
  • 「メソッド一覧を事前に計算 (またはメモ化) するヘルパー」を介して判定するようにした

程度のスコープであると考えられます。


  1. 影響範囲・注意点

影響範囲

  • 対象: ActiveSupport::LogSubscriber を継承した全てのログサブスクライバ
    • 例: ActionController::LogSubscriber, ActiveRecord::LogSubscriber など
  • 動作仕様:
    • イベント名に対応するメソッドが存在すれば呼び出す、という仕様自体は変わらない想定です。
    • 変わるのは「メソッド存在チェックのやり方」であり、外から見える挙動は基本的に同一です。

注意点

キャッシュ・メモ化を導入すると、次のようなケースに影響しうる点に注意が必要です:

  1. 動的にメソッドを追加する場合

    • define_method, class_eval, method_missing + respond_to_missing? などを使って、 実行時にイベントハンドラメソッドを追加・変更しているようなかなりトリッキーな実装では、 「一度計算した respond_to? 結果がキャッシュされる」ことで、
      追加後のメソッドが認識されない・あるいは逆に存在しないと判断される、というリスクが理論上あります。
    • ただし LogSubscriber をそういった動的メタプロで使っているケースは一般的には稀です。
  2. method_missing ベースの実装

    • method_missing だけでイベントを処理し、respond_to_missing? をうまく実装している場合、 respond_to? の結果は正しく返るはずなので、挙動は変わらないのが通常です。
    • しかし、キャッシュが入ることで「一度 false と判断されたメソッドが、その後 true になっても反映されない」
      といったケースが発生しうるため、もしそのような高度に動的な LogSubscriber 実装がある場合は要確認です。
  3. スレッドセーフティ

    • メソッド存在情報のキャッシュに共有状態 (@supported_methods など) を使う場合、 スレッドセーフに扱われる実装であるかどうかが重要です。
    • Rails本体のコーディング規約や既存のメモ化パターンに従っていれば、
      実害のあるレベルの競合はほぼ起こらないように設計されているはずです。

通常の、クラス定義時にハンドラーメソッドを定義して以降は変化させない使い方をしている大多数のアプリケーションにおいては、
挙動が変わる心配はほぼなく、純粋なパフォーマンス改善とみなしてよい変更です。


  1. 参考情報 (あれば)

このPRは、特にログが大量に流れる本番環境や、トレース・計測を多用するシステムで、
わずかながら CPU オーバーヘッドを減らすためのマイクロオプティマイズと言えます。


#57020 Fix lazy ivars causing different model shapes

マージ日: 2026/3/21 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    ActiveRecord::Base のサブクラスで、インスタンス変数が「遅延で」定義されることによってクラスごとに異なるオブジェクトシェイプ(shape)が発生していた問題を解消し、すべてのモデルクラスで同一の shape になるようにした PR です。
    これにより、Lobsters ベンチマークにおいて ActiveRecord モデルの shape が 3 種類から 1 種類に減り、Ruby VM の最適化(JIT など)に有利になります。

  1. 変更内容の詳細

背景: なぜ shape が増えていたか

Ruby 3 以降(特に YJIT など)では、オブジェクトが持つインスタンス変数の構造(順序・有無)によって “shape” が決まり、それが JIT の最適化などに影響します。
この PR の説明から読み取れる問題点は以下の通りです。

  • @pending_attribute_modifications などのインスタンス変数が「必要になったタイミングで初期化される」実装だった
  • 一部のモデルクラスでは、クラス定義中の attribute 呼び出しなどにより「早い段階で」その ivar がセットされる
  • 他のモデルでは、attribute_types が初めて呼ばれるまで ivar が作られない
  • typed_store を使うモデル(@_store_accessors_module, @local_stored_attributes)も同様に、特定のモデルだけ別の ivar セットを持つ

結果として、同じ ActiveRecord::Base のサブクラスでも、どのタイミングで / どの ivar が初期化されるかにより、Ruby VM 上で複数の shape が発生していた、という状況です。

PR の Before/After の表では、Lobsters アプリのベンチにおいて:

  • Before:

    • shape 2169: 21 モデル(Category, Comment, ... Vote
      • 理由: @pending_attribute_modificationsattribute_types 呼び出しまで作られない
    • shape 2175: 2 モデル(Message, ReplyingComment
      • 理由: クラスボディ内の attribute 呼び出しにより @pending_attribute_modifications が早期にセット
    • shape 2172: 1 モデル(User
      • 理由: @pending_attribute_modifications に加え typed_store 由来の
        @_store_accessors_module, @local_stored_attributes を持つため
  • After:

    • shape 1831: 24 モデルすべて
      • 理由: すべてのインスタンス変数が inherited などのタイミングで統一的にセットされる

実際の変更点(ファイル単位)

1) activemodel/lib/active_model/attribute_registration.rb

ActiveModel 側で、属性登録まわりのインスタンス変数が「必ずサブクラス生成時に揃う」ように調整されています。

おそらく以下のようなパターンの変更が入っています(擬似コード):

ruby
def inherited(subclass)
  super

  # 以前は必要になったときに初期化していた ivar を
  # サブクラス生成時に必ず用意するようにする
  subclass.instance_variable_set(:@pending_attribute_modifications, {})
end

ポイント:

  • inherited(base) の引数名は Rails 内の他の inherited 実装に合わせている(PR 説明で言及されている「パラメータ名が違うのは変だけど、他と揃えた」という話)
  • これにより、attribute_types などが呼ばれる前でも、全サブクラスに同じ ivar セットが存在する

2) activerecord/lib/active_record/store.rb

typed_store / store 機能に関わるクラスインスタンス変数:

  • @_store_accessors_module
  • @local_stored_attributes

などを、これもまた継承タイミングで必ず初期化するように変更しています。

イメージとしては:

ruby
def inherited(subclass)
  super

  # store 用のモジュールや属性ハッシュを必ず用意する
  subclass.instance_variable_set(:@_store_accessors_module, Module.new)
  subclass.instance_variable_set(:@local_stored_attributes, {})
end

これにより、typed_store を使う/使わないによって shape が変わるのではなく、「どのモデルでもひとまず同じ ivar 構造を持つ」という状態になります。
(実データが空のハッシュやモジュールであっても、shape 的には「同じインスタンス変数が定義されている」ことが重要。)


  1. 影響範囲・注意点

パフォーマンス面

  • VM レベルでモデルクラスの shape が 1 つに統一されるため、YJIT などの最適化が効きやすくなり、Rails アプリの実行性能改善が見込まれます(特にモデルクラスのメソッド呼び出しが多いケース)。
  • Lobsters ベンチで具体的に shape 数が 3 → 1 になったことが計測されているため、少なくとも一例では改善が確認されています。

互換性 / バグリスク

  • 挙動としては、「これまで遅延でセットされていたインスタンス変数が、クラス継承時にあらかじめセットされる」ようになっただけで、表面的な API は変わっていません。
  • ただし、以下のような場合は影響の可能性があります:
    • クラスインスタンス変数の「未定義(instance_variable_defined? が false)」と「空の状態({} など)」を区別していたようなメタプログラミング
    • defined?(@pending_attribute_modifications) のようなチェックに依存していたコード
  • Rails の内部 API に近い部分なので、通常のアプリケーションコードがここに依存しているケースは多くないと思われますが、エッジなメタプログラミングをしている場合は注意が必要です。

バックポートについて

  • PR 内で作成者が @byroot に対して「8.1 にはバックポート可能だと思うが、それより前は難しそう」とコメントしています。
  • これは内部実装の差異や、8.1 以前との互換性リスクを考慮しての判断と考えられます。
    → Rails 8.1 以降に入る変更として見ておくのが安全そうです。

  1. 参考情報 (あれば)
  • 関連コンセプト:
    • Ruby の “object shape” / “hidden class” / “map” などの用語(特に YJIT や MJIT の文脈)
  • コードリーディングの際のヒント:
    • ActiveModel::AttributeRegistrationinherited 実装
    • ActiveRecord::Storestore / store_accessor / typed_store)の継承処理
  • ベンチマーク:
    • ruby-bench の Lobsters ベンチ マーカー
      (ActiveRecord モデルの利用頻度が高いため shape の違いの影響が出やすい)

#57008 Remove unused requires from ActiveSupport::JSON::Decoding

マージ日: 2026/3/19 | 作成者: @Saidbek

  1. 概要 (1-2文で)
    ActiveSupport::JSON::Decoding 内で、現在は一切使われていない require を2つ削除したPRです。機能追加や挙動変更は行われておらず、不要な依存を整理するリファクタリングです。

  1. 変更内容の詳細

対象ファイル:
activesupport/lib/active_support/json/decoding.rb

削除されたのは、以下のような「require系」の2行です(実際の行はPR本文からの推測を含みますが、意味としてはこの2種類):

  • active_support/core_ext/module/attribute_accessors に関する require
  • delegation(ActiveSupport::Delegationdelegate 関連)に関する require

PR説明によると:

  • attribute_accessors の require

    • もともとは mattr_accessor :parse_json_times を使うために必要だったが、コミット c562b54mattr_accessor
      ruby
      mattr_accessor :parse_json_times
      から
      ruby
      singleton_class.attr_accessor :parse_json_times
      のような「通常のクラス/モジュールの attr_accessor」を使う形に置き換えられた
    • これにより、mattr_accessor を提供する active_support/core_ext/module/attribute_accessors は不要になっていた
  • delegation の require

    • 2012年に「プラガブルな JSON backend(自前のバックエンドを差し替え可能にする仕組み)」が削除されたタイミングで、delegate を使った委譲も不要になっていた
    • しかし削除し忘れていた require が残り続けていた
    • 現在の decoding.rb 内では delegation 機能は参照されていない

このPRでやっていることは:

  • ファイル先頭付近などに書かれていた上記2つの require 行を削除する
  • それ以外のコード(JSONのデコード処理のロジック、API、挙動)は一切変更しない

実質的には「使われていない依存モジュールを削除しただけ」のクリーンアップです。


  1. 影響範囲・注意点
  • Rails本体の挙動

    • ActiveSupport::JSON::Decoding の公開API・内部ロジック・デコード結果には一切変更なし
    • JSONのパース挙動(parse_json_times の意味や挙動含む)も変わらない
  • 依存関係・ロード順

    • 対象 require は、すでにどこからも参照されていなかったため、削除しても NameError などは発生しない
    • もしアプリケーション側が「ActiveSupport::JSON::Decoding を require すると副作用で delegation や module attribute accessors が読み込まれる」ことに“たまたま”依存していた場合は影響がありますが、そのような依存は基本的に非推奨かつ想定外です
      • 例:
        ruby
        # こういう依存は元々好ましくない(今回のPRで動かなくなる可能性がある)
        require "active_support/json/decoding"
        # delegation を勝手に使える前提でいたコード
        class Foo
          delegate :bar, to: :baz
        end
      • delegation や mattr_accessor 系が必要であれば、各自で明示的に require すべきです:
        ruby
        require "active_support/core_ext/module/attribute_accessors"
        require "active_support/dependencies" # or appropriate delegation require
  • テスト・ドキュメント

    • 機能変更・挙動変更がないため、テストの追加や CHANGELOG 更新は行われていません(PRの種別的にも妥当)

  1. 参考情報 (あれば)
  • このPRが前提としている過去の変更:

    • mattr_accessor :parse_json_timessingleton_class.attr_accessor に置き換えたコミット: c562b54
    • プラガブル JSON backend 削除(2012年ごろ)の変更: JSON backend 差し替え機構や delegation ベースの実装が不要になった
  • 実務的な示唆:

    • ライブラリやgemでは、「副作用として何かを require することに暗黙に依存しない」設計をすべきである、という典型例
    • 使われなくなった require を定期的に掃除することで:
      • 不要なロード時間の削減
      • 依存関係グラフの単純化
      • 将来的なリファクタリングの容易化
        といったメリットがあります。

#57000 Batch SQL statements when creating tables.

マージ日: 2026/3/19 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    このPRは、create_table 時に発行する複数の SQL を一括送信(バッチ)することで、特に巨大なスキーマを持つアプリケーションで db:schema:load などのパフォーマンスを大幅に改善する変更です。PostgreSQL で 1000+ テーブルのケースでは、スキーマロードが約 120 秒から約 25 秒まで短縮された事例が示されています。

  1. 変更内容の詳細

全体方針

  • これまで create_table の中で、カラム追加・インデックス作成・コメント付与などを個別に execute していた処理を、
    • 一旦 Ruby 側で SQL 文字列として配列に貯める
    • 最後に execute_batch でまとめて送る
      という方式に変更しています。
  • drop_table やコメント付与など、これまで「その場で SQL 実行」していた部分についても、「SQL を生成して返すだけのヘルパーメソッド」を新設し、バッチ用の配列に積めるようにしています。

主な変更点

1) create_table の SQL バッチ化

ActiveRecord::ConnectionAdapters::SchemaStatements#create_table 付近が主な変更箇所です。

イメージとしては以下のような流れに変わります(擬似コード):

ruby
def create_table(table_name, **options)
  sqls = []

  # 1. CREATE TABLE 本体
  sqls << create_table_definition_sql(table_name, **options)

  # 2. カラムコメントやテーブルコメントを付ける SQL を追加
  sqls.concat(comment_sqls_for_table(table_name, **options))

  # 3. インデックスや制約などの追加 SQL を配列に追加
  sqls.concat(index_sqls_for_table(table_name, **options))

  # 4. 最後にまとめて実行
  execute_batch(sqls)
end

実際にはアダプタごとに実装が異なりますが、概念的には上記のように「SQL をためてから、最後に execute_batch」という構成になっています。

2) drop_table やコメント関連のヘルパー追加

  • これまで drop_table などで直接 execute("DROP TABLE ...") のように書かれていた箇所を、
    • 「DROP TABLE 用の SQL を生成して返すメソッド」
    • 「コメント作成用の SQL を生成して返すメソッド」 に分離し、create_table からはそれらを呼び出して SQL 文字列だけ取得し、バッチに積むようにしています。

例(イメージ):

ruby
def drop_table_sql(table_name, **options)
  # "DROP TABLE ..." の SQL 文字列だけを返す
end

def comment_sql_for_column(table_name, column_name, comment)
  # "COMMENT ON COLUMN ..." の SQL 文字列だけを返す
end

3) PostgreSQL / MySQL アダプタの対応

  • abstract_mysql_adapter.rb
    • MySQL 系アダプタにも execute_batch 対応や、バッチ実行を前提とした create_table 内部処理の整理が入っています。
    • MySQL 側が複数文を一度に投げるときの挙動が異なるため、それを考慮した実装になっている可能性があります(テストも追加)。
  • postgresql/schema_statements.rb
    • PostgreSQL アダプタ側で、テーブル作成時の付随処理(コメント、インデックスなど)を SQL 文字列として返すヘルパーを追加し、execute_batch に渡せるようにしています。

4) テストの追加・更新

  • activerecord/test/cases/adapters/abstract_mysql_adapter/active_schema_test.rb
  • activerecord/test/cases/migration/change_schema_test.rb

上記テストで、以下のような観点を検証していると考えられます。

  • バッチ実行でもこれまで通りテーブルが正しく作成されるか
  • コメントやインデックスなど、付随するメタデータも期待通りに適用されるか
  • MySQL / PostgreSQL など主要なアダプタで、バッチ処理が問題なく動作するか

5) CHANGELOG の更新

  • activerecord/CHANGELOG.md にこの動作変更(パフォーマンス改善)が記載されており、アプリケーション開発者に向けて「テーブル作成時の SQL 発行がバッチ化された」ことが明示されています。

  1. 影響範囲・注意点

影響する場面

  • 主に以下の操作に影響があります(内部的な SQL の出し方が変わる):
    • rails db:schema:load
    • マイグレーションでの create_table / drop_table
    • スキーマ定義でコメントやインデックスなど、テーブル作成に付随したオプションを使っている場合

アプリケーションコードから見える API (create_table の引数や DSL) は変わらないため、通常利用ではコード修正は不要です。

パフォーマンス面

  • 大量のテーブルや複雑なスキーマを持つアプリで、特にネットワークレイテンシがある環境(DB が別ホストなど)では、
    「DB とのラウンドトリップ回数削減」による高速化が期待できます。
  • PR では PostgreSQL 1000+ テーブルの環境で 120s → 25s まで短縮された、という具体的な数値が報告されています。

互換性・リスク

  • SQL を一括送信するようになったことで、以下のような点に注意が必要になる可能性があります:

    • DB ドライバや設定による制約
      • サーバ側で「一度に複数ステートメントを許可していない」設定がある場合、問題になる可能性があります(特に MySQL 系)。
      • ただし、Rails の公式アダプタで対応した形になっているため、通常の設定であれば動くようにテスト済みと思われます。
    • エラー時の扱い
      • バッチ内のどの SQL で失敗したかの特定が、ログ上わずかに分かりにくくなる可能性があります(1 回の execute_batch 内に複数文が含まれるため)。
      • とはいえトランザクション内で実行されていれば、失敗時のロールバック挙動自体はこれまで通りです。
  • gem やプラグインなどで、アダプタの create_table / drop_table をモンキーパッチしている場合、

    • 既存実装の前提(「その場で execute される」など)が崩れることで、不整合が起きる可能性があります。
    • アダプタの内部 API(SQL を返すヘルパーの追加など)に依存するコードがあれば、追従が必要になるかもしれません。

  1. 参考情報 (あれば)
  • PR で言及されている「autoresearch pattern」:

    • https://github.com/karpathy/autoresearch
    • 該当の実装例(pi-autoresearch): https://github.com/davebcn87/pi-autoresearch
      これらを使って「DB スキーマロードのボトルネック分析・最適化案の探索」を自動化し、その中から「create_table 時の SQL バッチ化」という現実的な最適化を抽出して手書きで実装した、という背景が説明されています。
  • 結論として:

    • 外部 API の互換性を保ったまま、ActiveRecord のスキーマ・マイグレーションの実行性能を改善する、比較的安全な内部構造のリファインと言えます。
    • 大規模スキーマを持つアプリでは、Rails をこのバージョンに上げるだけでスキーマロード時間が短縮される可能性があります。

#56998 Use flat_map in ContentSecurityPolicy

マージ日: 2026/3/18 | 作成者: @p8

  1. 概要 (1-2文で)
    Rails の ContentSecurityPolicy 実装において、validate 内で flat_map を使うことで配列のフラット化処理を整理し、build_directive の戻り値を validate の戻り値に依存しないようにする小さなリファクタリングが行われました。挙動は変えずに、コードの明確性と保守性を高める変更です。

  1. 変更内容の詳細

※ 実際の差分は概ね以下のようなイメージです(擬似コードレベル):

ruby
def validate(directive, sources)
  # 変更前(イメージ)
  resolved_sources = sources.map { |source| build_directive(directive, source) }.flatten

  # 変更後
  resolved_sources = sources.flat_map { |source| build_directive(directive, source) }

  # validate の戻り値に依存せずに build_directive が意味のある値を返すようにする
  resolved_sources
end

def build_directive(directive, source)
  # 変更前(イメージ)
  # result は配列や単一値を含むことがあり、そのまま flatten されていた
  result = some_resolution_logic(directive, source)
  Array(result).flatten

  # 変更後
  resolved_sources = some_resolution_logic(directive, source)
  # ここで flatten しない
  resolved_sources
end

実際に行われていることは次の2点です。

  1. validatemap + flattenflat_map に置き換え

    • 従来:
      ruby
      resolved_sources = sources.map { ... }.flatten
    • 変更後:
      ruby
      resolved_sources = sources.flat_map { ... }

    Array#flat_mapmap でブロック実行 → 結果を 1 レベル flatten までを一度に行うメソッドなので、意図が明確になり、不要な flatten 呼び出しも削除できます。

  2. build_directive の戻り値を validate に依存しない形に明示

    • これまで build_directive は内部で flatten されることを前提とした戻り値設計だったため、validate 側の実装(flatten するかどうか)に依存していました。
    • 今回の変更では build_directive 内で「解決済みソース (resolved_sources) を明示的に返す」ようにしており、
      • build_directive は「1 つのディレクティブと 1 つのソース」から「解決済みソース群」を返す
      • validate は「全ソースに対して flat_map で解決済みソースを 1 次元配列にまとめる」
        という責務分離がはっきりしました。

この結果、validatebuild_directive の役割がより明確になり、「どのメソッドが flatten を担当するか」がコード上で一目で分かるようになっています。


  1. 影響範囲・注意点
  • 外部 API としての ContentSecurityPolicy の挙動(生成される CSP ヘッダの内容)は、意図としては変わりません
  • 影響が出る可能性があるのは、以下のような「内部実装に依存した使い方」をしている場合です。
    • ContentSecurityPolicymonkey patch していて、build_directive が内部で flatten している前提でコードを書いている場合
    • build_directive の戻り値の型やネスト構造に依存した独自拡張をしている場合
  • Rails 内部の通常利用(config.content_security_policy などを通じた標準的な使い方)では、この変更による破壊的影響は想定されません。
  • flatten の責務が validate 側に一本化されたことで、今後 build_directive の実装を変更・拡張しやすくなります(テストも書きやすくなり、バグ混入のリスクが減る)。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/56998
  • 対象ファイル: actionpack/lib/action_dispatch/http/content_security_policy.rb
  • 変更は 1 ファイル・+4/-2 行のみで、純粋なリファクタリング (内部実装改善) に分類できる内容です。

#57003 Fix PostgreSQL column equality for generated types

マージ日: 2026/3/18 | 作成者: @Saidbek

  1. 概要 (1-2文で)
    PostgreSQL の generated column(生成列)の「格納型(stored/virtual)」が異なる場合に、PostgreSQL::Column==hash の挙動が食い違っていたバグを修正する PR です。==hash が同じ情報(generated 属性)に基づいて判定されるようにし、Hash/Set での不整合を解消しています。

  1. 変更内容の詳細

何が問題だったか

ActiveRecord::ConnectionAdapters::PostgreSQL::Column には ==hash が定義されていますが、PostgreSQL 18+ で導入された generated column の 2 種類のタイプ:

  • stored ("s")
  • virtual ("v")

に対して、以下のような不整合がありました。

  • #==
    virtual? という boolean(「仮想かどうか」だけ)で比較していた
    → 実質、generated"s" / "v" の違いを潰した形で比較

  • #hash
    内部 ivar の @generated"s" or "v")をそのままハッシュ計算に使用していた

結果として:

  • カラムの他の属性がすべて同じで、generated: "s"generated: "v" だけが違う 2 つの PostgreSQL::Column オブジェクトが:
    • col1 == col2truevirtual? ベースで比較)
    • col1.hash != col2.hash@generated ベース)

という状態になり、eql?/hash 契約(等価なオブジェクトは同じハッシュ値を持つべき)に違反していました。
この問題は Hash / Set をキーにしたキャッシュやコレクションで、同じと見なされるはずのカラムが重複したり、逆に取り出せなかったりといった「サイレントな不具合」を引き起こします。

同種の問題は SQLite3 用の Column でも発生しており、別 PR (#56817) ですでに修正済みで、今回は PostgreSQL 版の追従です。

具体的な修正内容

activerecord/lib/active_record/connection_adapters/postgresql/column.rb にて:

  • これまで #== 内で行っていた比較:
ruby
virtual? == other.virtual?

を削除し、代わりに generated 属性そのものを比較するよう変更:

ruby
generated == other.generated
  • そのために generated を参照可能にする protected attr_reader :generated を追加

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

ruby
class PostgreSQL::Column
  # 以前
  def ==(other)
    # ...
    virtual? == other.virtual? &&
      # ...
  end

  def hash
    [ # ...
      @generated,
      # ...
    ].hash
  end

  # virtual? は @generated が "v" かどうかを boolean で返すメソッド
end

が、次のように揃えられます:

ruby
class PostgreSQL::Column
  protected attr_reader :generated

  def ==(other)
    # ...
    generated == other.generated &&
      # ...
  end

  def hash
    [ # ...
      @generated, # == で比較する generated と同じ抽象レベル
      # ...
    ].hash
  end
end

つまり、==hash も「generated という同じ次元の情報」を見るようになり、stored/virtual の違いが適切に反映されるようになりました。

テスト

activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb にテストが追加されており、少なくとも以下を検証していると考えられます(要約):

  • generated type が異なる 2 つのカラムが == で異なると判定されること
  • あるいは(実装次第で)==/hash が一貫していることを検証する形で、contract の維持を確認

コード上、変更行数・ファイル数は少ないですが、挙動としては重要な修正です。


  1. 影響範囲・注意点
  • 対象: PostgreSQL 18 以降で、generated column(stored/virtual)を利用しているアプリケーション
  • 影響箇所:
    • ActiveRecord::ConnectionAdapters::PostgreSQL::Column をキーとして Hash / Set を作っているような場合
    • Active Record 内部のスキーマキャッシュ・カラムキャッシュなど、Column オブジェクトの == / hash に依存するロジック全般

この修正により:

  • 以前: stored/virtual の違いを無視して「同じカラム」とみなされてしまうケースがあった
  • 今後: stored と virtual は「異なる generated type を持つ別のカラム」として区別される

したがって、「stored/virtual の違いを無視して同一扱いしていた」ことに依存した既存挙動があれば変わる可能性がありますが、通常はむしろ正しい区別ができるようになるので望ましい変更です。

Hash/Set を使った独自のメタデータ管理・キャッシュ等をしている場合、Rails 更新後に:

  • カラムキーの重複・欠落がなくなっているか
  • 以前不自然だった動作(列キャッシュの取り違え等)が修正されているか

を確認するとよいです。


  1. 参考情報 (あれば)
  • 対応する SQLite3 側の修正:
    • PR #56817「Fix SQLite3 column equality for generated types」
  • バグの導入箇所:
    • コミット bdc7adce#hash を raw ivar ベースにリファクタリングした際、#== が追従していなかった)
  • 関連クラス:
    • ActiveRecord::ConnectionAdapters::PostgreSQL::Column
    • virtual?, generated(PostgreSQL の generated column の種別を表す属性)

#57001 Fix PostgresqlDbConsoleTest reports Environment leak detected

マージ日: 2026/3/17 | 作成者: @joaoGabriel55

  1. 概要 (1-2文で)
    PostgresqlDbConsoleTest で環境変数(ENV)のリークを検出する LeakChecker とのライフサイクルの噛み合わせ不良によりテストが失敗していた問題を、ENV のバックアップ/復元方法を変更することで修正した PRです。テストコードのみの変更で、本体の挙動には影響しません。

  1. 変更内容の詳細

問題の背景

  • Rails のテスト基盤には LeakChecker モジュールが ActiveSupport::TestCaseprepend されており、テスト間での環境変数の「リーク」(追加・変更されたまま次のテストに持ち越されること)を検出します。

  • LeakChecker はテストライフサイクルの以下のタイミングで ENV をチェックします:

    • before_setup: 現在の ENV をスナップショット
    • after_teardown: スナップショットと比較して差分があれば「Environment leak detected」として失敗させる
  • PostgresqlDbConsoleTest では、PostgreSQL 接続用の ENV(PGUSER, PGHOST, PGPORT など)をテスト内で書き換えるため、もともと「テストごとに元に戻す」ための仕組みを持っていました。

  • その仕組みは「#run をオーバーライドして、super を ENV の保存・復元でラップする」という実装でした。

疑似コード(問題のあったイメージ):

ruby
def run(*args, &block)
  preserve_pg_env do
    super  # ← この中で LeakChecker の before_setup / after_teardown が動く
  end
end

def preserve_pg_env
  backup_env
  yield
ensure
  restore_env  # ← after_teardown の後で実行されてしまう
end
  • この構造だと実際の実行順序は:

    1. LeakChecker#before_setup で ENV をスナップショット
    2. テスト本体で PGUSER, PGHOST, PGPORT を書き換え
    3. LeakChecker#after_teardown が「ENV が変わっている」と検出して失敗
    4. その後で ensure によって ENV が復元される(遅すぎる)
  • その結果、以下の 4 テストが LeakChecker によって落ちていました:

    • test_postgresql_full
    • test_postgresql_with_ssl
    • test_postgresql_include_password
    • test_postgresql_include_variables

    エラーメッセージ例:

    text
    Environment leak detected:
      - Added variables:
        - PGPORT
      - Changed variables:
        - PGUSER from "postgres" to "user"
        - PGHOST from "postgres" to "host"

対応方針

#run のオーバーライドをやめ、Minitest 標準の setup / teardown を使って ENV をバックアップ・復元するよう変更しました。
重要なポイントは、「LeakChecker の after_teardown が走る前に ENV を元に戻す」ことです。

Minitest ライフサイクル(LeakChecker を含む):

text
run
  before_setup (LeakChecker が ENV をスナップショット)
  setup        ← ここで PG* の ENV をバックアップ
  test body    ← PG* を変更
  teardown     ← ここで PG* の ENV を復元
  after_teardown (LeakChecker が ENV を比較)

これにより、LeakChecker が after_teardown で ENV を比較する時点では PostgresqlDbConsoleTest による ENV 変更はすでに元に戻っており、リークとして検出されなくなります。

実際の変更点

  • 変更ファイル: activerecord/test/cases/adapters/postgresql/dbconsole_test.rb
  • 主な変更内容:
    • #run のオーバーライドを削除
    • preserve_pg_env というプライベートメソッドを削除
    • 代わりに setup / teardownPG* ENV をバックアップ・復元するコードを追加

イメージ(擬似コードレベル):

ruby
class PostgresqlDbConsoleTest < ActiveRecord::TestCase
  def setup
    super
    @original_pg_env = {
      "PGUSER" => ENV["PGUSER"],
      "PGHOST" => ENV["PGHOST"],
      "PGPORT" => ENV["PGPORT"],
      # 必要なキーをここで保存
    }
  end

  def teardown
    @original_pg_env.each do |key, value|
      if value.nil?
        ENV.delete(key)
      else
        ENV[key] = value
      end
    end
    super
  end
end

※上は説明用の擬似コードであり、実際のコードと細部は異なる可能性がありますが、概ねこの方針の変更です。


  1. 影響範囲・注意点
  • 影響範囲:
    • 変更はテストコードのみであり、Rails / ActiveRecord の本番コードには影響しません。
    • PostgresqlDbConsoleTest 内で PostgreSQL 接続用 ENV をいじるテストが、LeakChecker によって「環境変数リーク」と誤検出されなくなります。
  • 注意点:
    • 今後、他のテストでも ENV を変更する場合は、#run のオーバーライドではなく、setup / teardown など Minitest のライフサイクルに沿ってバックアップ・復元を行う必要があります。
    • LeakChecker を前提とすると、「after_teardown よりも前に環境差分が元に戻っている」ことが重要です。独自フックでテスト実行をラップする場合は、この順序関係に注意する必要があります。
    • もし他のテストクラスでも同様に #run をオーバーライドして ENV を弄っている場合は、同種のリーク検出問題が起きうるため、同パターンに書き換えるのが望ましいです。

  1. 参考情報 (あれば)
  • 修正対象 Issue: Fixes: #56812
  • LeakChecker の実装:
    • tools/support/leak_checker.rb に定義
    • tools/test_common.rbActiveSupport::TestCaseprepend
  • Minitest ライフサイクルと Rails のテスト拡張:
    • ActiveSupport::TestCase は Minitest::Test をベースにしており、before_setup / after_teardown などを通じてこうしたフックを差し込んでいます。

#56994 Drop workaround for mail gem version < 2.8

マージ日: 2026/3/16 | 作成者: @yahonda

  1. 概要 (1-2文で)
    Action Mailbox が依存する mail gem の下限バージョンがすでに 2.8 以上になっているため、mail < 2.8 向けに入っていたワークアラウンド(互換コード)を削除した PR です。結果として、コードがシンプルになり、サポート対象が実質的に mail 2.8 以降に揃えられました。

  1. 変更内容の詳細
  • 対象ファイル: actionmailbox/lib/action_mailbox/mail_ext/addresses.rb
  • 行数ベースでは「+5 / -16」となっており、条件分岐や旧バージョン用の処理が削られています。

元々このファイルには、mail gem の古いバージョン (< 2.8) でアドレスパース周りに問題があったため、それを吸収するためのパッチ的な拡張(ワークアラウンド)が入っていました(#46643 で追加)。
しかし #50668 で「Action Mailbox は mail gem >= 2.8 を必須」としたため、mail 2.7 以前を考慮したコードは不要になり、今回それを削除しています。

イメージとしては、次のようなコードがなくなった形です(あくまでイメージであり実際のコード断片ではありません):

ruby
# 擬似コード: 古いバージョンを考慮した条件分岐
if Gem::Version.new(Mail::VERSION) < Gem::Version.new("2.8.0")
  # mail 2.8 未満用の独自パッチ・上書き
  class Mail::Address
    # バグを回避するためのメソッド上書き
  end
else
  # 2.8 以上では標準実装そのまま利用
end

このような「バージョン判定+古いバージョン向けの monkey patch / 拡張」の部分が削られ、2.8 以上前提のコードだけが残る形になっています。


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

    • Action Mailbox が行っている「メールアドレスのパース・処理」に関する内部実装がシンプルになります。
    • mail gem 2.8 以上の振る舞いを前提としているため、それ以前の挙動を想定した特別な処理はもう行われません。
  • 注意点

    • 実務的には、Rails / Action Mailbox を使うプロジェクトで mail gem を 2.8 未満に固定していると、依存関係の解決でそもそも弾かれる(または動かない)前提になっています。
      • もしプロジェクト側が mail (~> 2.7) のように古いバージョンを強制している場合は、Rails のバージョンアップ時に Gemfile の制約も見直す必要があります。
    • mail 2.8 未満でのみ再現していたバグを避けるためのワークアラウンドが削除されるため、「mail を 2.8 以上に上げること」自体がバグ回避条件になります(= サポート外のバージョンにおける動作は考慮されない)。

Rails アプリ開発者の視点では、「サポートされていない mail gem を無理に使わない限り、挙動が変わることはほぼない」と考えて差し支えありません。


  1. 参考情報 (あれば)
  • この PR: Drop workaround for mail gem version < 2.8 (#56994)
  • ワークアラウンドを最初に導入した PR: #46643
  • Action Mailbox が mail >= 2.8 を要求するようになった変更: #50668

#56980 Bump bundler from 4.0.6 to 4.0.8

マージ日: 2026/3/16 | 作成者: @larouxn

  1. 概要 (1-2文で)
    Rails リポジトリで使用している Bundler のバージョンを 4.0.6 から 4.0.8 に更新した PRです。これにより、bundle install などで利用される Bundler が最新のパッチバージョンになります。

  1. 変更内容の詳細
  • 変更ファイルは Gemfile.lock のみで、Bundler のバージョン表記を 4.0.6 → 4.0.8 に更新しています。
  • 実質的には、Rails の開発・動作環境で前提とされる Bundler のバージョンを最新の 4.0.8 に揃える変更です。
  • PR本文で 4.0.7 / 4.0.8 のリリースノートへのリンクが貼られているため、4.0.6 → 4.0.8 の間で入った修正・改善を取り込む意図があります(機能追加というよりパッチ・バグフィックスが主な想定)。

Gemfile.lock 中の変更イメージ(概念的な例):

diff
-  BUNDLED WITH
-     4.0.6
+  BUNDLED WITH
+     4.0.8

コードロジックやライブラリ依存関係そのものには手を加えておらず、あくまで Bundler のバージョン指定のみの変更です。


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

    • Rails の開発者・CI環境などで bundle installbundle exec を実行する際に、Bundler 4.0.8 を前提とした挙動になります。
    • Bundler 本体のバグ修正や互換性改善が取り込まれるため、依存関係の解決・インストール時の挙動が一部変わる可能性がありますが、同一メジャー・マイナー内のパッチアップデートのため、後方互換性が大きく崩れるリスクは低いと考えられます。
  • 注意点:

    • ローカル環境にインストールされている Bundler バージョンが 4.0.8 より古い場合、bundle install 実行時にバージョン差異による警告が出たり、うまく動作しない可能性があります。その場合は gem install bundler -v 4.0.8 などで更新する必要があります。
    • CI や開発用 Docker イメージで Bundler のバージョンを固定している場合は、それらの環境設定も 4.0.8 に合わせて更新する必要があります。
    • PR 作成者によるローカルテストは済んでいるものの、プロジェクト固有の環境差異で何かしら Bundler 依存の不具合が出る可能性はゼロではないため、CI の通過と実環境での確認が推奨されます。

  1. 参考情報 (あれば)

これらのリリースノートを見ることで、4.0.6 から 4.0.8 までに入ったバグ修正・改善点を確認できます。


#56926 ActionText: support block children in editor elements alongside value

マージ日: 2026/3/16 | 作成者: @jorgemanrubia

  1. 概要 (1-2文で)
    ActionText のエディタヘルパーで、value 引数とは別にブロックで渡した内容を「エディタ要素の子要素」として同時に扱えるようになりました。Trix エディタについては、従来どおり「value が無い場合はブロック内容を初期値として使う」という挙動も維持されています。

  1. 変更内容の詳細

これまでの挙動

  • ActionText のエディタヘルパー(例: form.rich_text_area 相当の低レベル API)でブロックを渡すと、そのブロックは「エディタの初期値」として解釈されていました。
  • そのため、value 引数とブロックは排他的 (OR) で、どちらか一方しか使えませんでした。
    • value を指定すると value が優先
    • value が無い場合のみ、ブロックをキャプチャしてエディタ初期コンテンツに充てる

これからの新しい挙動

  • ブロックは エディタ要素の DOM 子要素としてレンダリング されるように変更されました。
    • value はこれまでどおり「エディタの内容(コンテンツバインディング)」に流れ込みます。
    • ブロックは「エディタタグの内側に配置される追加要素」として扱われます。
  • つまり、value とブロックを同時に併用可能 になります。
    • value … リッチテキストそのもの
    • ブロック … プロンプトメニュー、ツールバー拡張、設定用の要素などをエディタ要素内にインジェクトする用途

イメージとなるサンプル(擬似コード)

erb
<%= action_text_editor_tag value: @post.body do %>
  <!-- ここはエディタ要素の「子要素」としてそのまま出力される -->
  <custom-toolbar slot="toolbar"></custom-toolbar>
  <prompt-menu data-target="editor.prompt"></prompt-menu>
<% end %>
  • 上記のように、value: @post.body で本文を設定しつつ、ブロック内には拡張 UI を自由に埋め込めるイメージです。
  • Lexxy のような他実装のエディタは、この「ブロック = 子要素」構文を設定・拡張用に利用しつつ、リッチテキストは value 属性から受け取ることができるようになります。

Trix エディタに対する互換対応

  • Trix 向けの TrixEditor::Tag#render_in では、value が存在しない場合のみ、ブロックをキャプチャして初期値にする」という従来挙動 を保持しています。
    • valuenil or 未指定 → ブロック内容を初期値として hidden input に設定(従来どおり)
    • value が指定されている → value がエディタ内容になり、ブロックは DOM 子要素としてレンダリング

これにより、既存の Trix ベースのコードで「ブロックに初期リッチテキストを書いている」ケースは、そのまま動作し続けます。


  1. 影響範囲・注意点
  • 新規実装 / 他エディタ実装(Lexxy など)

    • エディタヘルパーのブロックを「DOM 子要素挿入のためのフック」として使えるようになります。
    • value は常に「リッチテキスト値」、ブロックは「DOM 構造の拡張」として扱えるため、API の責務が明確になります。
    • エディタ側実装では、タグの子要素として渡されるノードを設定 UI やスロットコンテンツとして解釈することが可能です。
  • 既存 Trix ベースのアプリ

    • value なし + ブロックで初期コンテンツを書いている場合:
      • 今回の PR では Trix のみ特別に互換レイヤーが入り、従来通り「ブロックが初期値」として扱われるため、基本的に動作は変わりません
    • value とブロックを併用しているコードは、これまでは「ブロック側は初期値としてキャプチャされ、DOM 子要素としては存在しない」挙動でしたが、今後は「value が優先され、ブロックはエディタの子要素としてレンダリング」される形になります。
      • そのような使い方をしているケースは少ないと思われますが、もし独自に依存している場合は挙動の変化に注意が必要です。
  • テスト・内部仕様

    • actiontext/test/unit/editor_test.rb および form_helper_test.rb にテストが追加・更新され、
      • value とブロックの独立性
      • Trix における後方互換 がカバーされています。
    • CHANGELOG.md にもこの挙動変更が明記されています。

  1. 参考情報 (あれば)
  • 対応する既存 PR:
    • ブロック機能導入の元 PR: #55827
  • この PR:
    • #56926: ActionText: support block children in editor elements alongside value
  • 補足的な実装意図:
    • Lexxy など、Trix 以外のエディタ実装が ActionText のエディタタグを使う際に、
      • value … リッチテキストの値
      • ブロック … 設定用 / 拡張 UI 用の DOM 子要素
        という形で明確に使い分けられるようにするための設計変更です。

#56990 fix(guides): bin/jobs start was not copying

マージ日: 2026/3/16 | 作成者: @j01nb0k

  1. 概要 (1-2文で)
    Railsガイド「Active Job Basics」の中にある bin/jobs start のコマンド例が、シェル上で想定どおりコピー・実行できない状態になっていた問題を修正したPRです。原因は、ガイドのコードブロックから「$ プロンプト記号」が抜けていたことによる単純なドキュメント上のミスです。

  1. 変更内容の詳細
  • 対象ファイル: guides/source/active_job_basics.md
  • 変更内容は 1 行のみで、bin/jobs start の記述に $ を追加しています。

例(イメージ):

diff
- bin/jobs start
+ $ bin/jobs start

Railsガイドでは、シェルで実行するコマンド例は慣例として先頭に $ を付けて記述しています。
この $ がないと、ガイドの「右上のコピーアイコン(Copy code)」などでコマンドをコピーした際に、ほかの類似箇所と挙動が揃わない、あるいはレンダリング上の扱いが不統一になる場合があります。

PR説明文の "was not being copied" という表現から、ガイドをHTMLに変換した際に、この行だけコピー機能やスタイルの対象外になっていた(他の $ 付きコマンド例とは異なる扱いになっていた)ことが分かります。そのため、$ を付けて他のコマンド例と同じ形式に揃えた形です。


  1. 影響範囲・注意点
  • 影響範囲:
    • コード・ライブラリ本体には一切影響せず、Railsガイドのドキュメント表示・コピー挙動のみの変更です。
    • bin/jobs start 自体の動作や Active Job のAPI、Railsアプリの挙動には変更はありません。
  • 注意点:
    • 既存アプリケーション・ジョブキュー・本番運用への影響はゼロです。
    • これを機に、他のガイドにおいても $ の有無が不揃いになっていないか確認すると、ドキュメントの一貫性向上につながります。

  1. 参考情報 (あれば)
  • 該当ガイド:
  • 関連知識:
    • Railsガイドでは、シェルコマンド例に $ を付けることで「ユーザーが打つ部分」と「プロンプト」を明確にしつつ、コピー時には $ も含めるかどうかをコピー機能側で制御できるような前提で書かれていることが多いです。

#56987 Add tests for ActiveModel::Name#human count argument

マージ日: 2026/3/16 | 作成者: @nicolasva

  1. 概要 (1-2文で)
    ActiveModel::Name#human に渡せる count 引数が、I18n の複数形ルールに沿って正しく扱われることを確認するテストが追加されています。機能変更ではなく、既存仕様のテスト補完が目的のPRです。

  1. 変更内容の詳細
  • 追加ファイル: activemodel/test/cases/translation_test.rb に17行のテストコードが追加
  • テスト対象: ActiveModel::Name#human(count: ...)

ActiveModel::Name#human は以下のように呼び出されるメソッドです:

ruby
User.model_name.human           # => "User" など
User.model_name.human(count: 2) # => 複数形ルールに基づいた翻訳

今回のPRでは、この count: オプションが I18n の複数形 (pluralization) に使われていることをテストで保証しています。
具体的には:

  • ActiveModel::Name#humancount: 1count: 2 などを渡したとき、
  • 対応する I18n キー (例: activemodel.models.user.one / activemodel.models.user.other など) が正しく選択されているか
  • あるいは activerecord.models.* / activemodel.models.* の優先順位や fallback が期待どおりか

といった振る舞いを検証するテストが追加されています。

テストコード自体は、他の翻訳系テストと同様に:

  • テスト用のモデルクラス(例: Person など)を定義
  • テスト用の I18n ロケールを設定
  • model_name.human(count: X) を呼び出して返り値を検証

という形になっているはずです。


  1. 影響範囲・注意点
  • ランタイムの挙動変更はなく、テストの追加のみです。
  • 既に count に対応している実装を「壊さないための安全ネット」が増えたと捉えられます。
  • これにより、今後 ActiveModel::Name#human 実装を変更した際に:
    • count オプションが無視される
    • I18n の pluralization が崩れる
      などのリグレッションが CI で検出されるようになります。
  • 既存アプリで model_name.human(count: ...) を利用している場合、その挙動が今後の変更で壊れにくくなる、という意味での間接的なメリットがあります。

  1. 参考情報 (あれば)
  • 該当クラス: ActiveModel::Name
  • 関連する I18n の pluralization 仕様:
  • 関連 issue として PR 説明に挙がっている (取り消し線付き) #56984 は、count 引数周りの挙動確認やバグ報告が背景にあったと推測されますが、PR時点では issue としてはクローズまたは不要となり、テスト追加でカバーする形になった可能性があります。

#56989 Simplify activerecord tests with NotificationAssertions helpers

マージ日: 2026/3/16 | 作成者: @Saidbek

  1. 概要 (1-2文で)
    activerecord のテストで、これまでバラバラに書かれていた ActiveSupport::Notifications.subscribed ベースのコードを、共通ヘルパ NotificationAssertionsassert_notification / capture_notifications)に置き換えてシンプルにした PR です。テストの可読性と一貫性を高めるためのリファクタリングであり、挙動や API 仕様の変更はありません。

  1. 変更内容の詳細

背景

  • 以前の PR (#53822) で NotificationAssertions ヘルパが導入され、テストでの通知関連のアサーションを簡潔に書けるようになりました。
  • しかし activerecord 内にはまだ、従来の ActiveSupport::Notifications.subscribed を直接使ったテストが一部残っていたため、本 PR でそれらをすべてヘルパ利用に統一しています。

主な変更箇所

対象ファイルは 2 つです。

  • activerecord/test/cases/instrumentation_test.rb
  • activerecord/test/cases/relation/load_async_test.rb

いずれもテストコードのみの変更で、合計で 追加 14 行 / 削除 41 行 と、コード量が減っています。

1) load_async_test.rb の変更

これまでは「通知が発火したか」を判定するのに、ブールフラグと手動サブスクライバを使っていました。典型的な旧コードイメージは次のようなものです(擬似コード):

ruby
def test_async_load_instruments_notification
  notified = false

  subscriber = ActiveSupport::Notifications.subscribe("load_async.active_record") do |*args|
    notified = true
  end

  begin
    SomeModel.where(...).load_async
  ensure
    ActiveSupport::Notifications.unsubscribe(subscriber)
  end

  assert notified
end

これを NotificationAssertionsassert_notification による一行アサーションに置き換えています。イメージ:

ruby
def test_async_load_instruments_notification
  assert_notification "load_async.active_record" do
    SomeModel.where(...).load_async
  end
end

ポイント:

  • フラグ変数や subscribe/unsubscribe の手動管理が不要になり、テストが明快になっています。
  • テストの意図(「この処理で load_async.active_record が発火するべき」)がダイレクトに読めるようになります。

2) instrumentation_test.rb の変更

こちらでは、通知内容(複数レコードの発火や payload の中身)を検査するために、インラインで配列に push するようなコールバックが書かれていました。典型的な旧コードイメージ:

ruby
def test_queries_are_instrumented
  events = []

  subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
    event = ActiveSupport::Notifications::Event.new(*args)
    events << event
  end

  begin
    SomeModel.where(...).to_a
  ensure
    ActiveSupport::Notifications.unsubscribe(subscriber)
  end

  payloads = events.map(&:payload).reject { |p| p[:name] == "SCHEMA" }
  assert payloads.any? { |p| p[:sql].include?("SELECT") }
end

これを capture_notifications で置き換えています。イメージ:

ruby
def test_queries_are_instrumented
  events = capture_notifications("sql.active_record") do
    SomeModel.where(...).to_a
  end

  payloads = events.map(&:payload).reject { |p| p[:name] == "SCHEMA" }
  assert payloads.any? { |p| p[:sql].include?("SELECT") }
end

ここでの方針:

  • 「通知が出たかどうか」だけでよいケース → assert_notification
  • payload を詳細に触る必要があるケース → capture_notificationsEvent を取得してから map / reject などでフィルタリング

これにより:

  • ActiveSupport::Notifications.subscribe / unsubscribe の定型コードが消え、対象ロジックとアサーションだけが残る形になります。
  • どのイベント名を検査しているかが、テストメソッドの先頭で分かりやすくなります。

  1. 影響範囲・注意点
  • 対象は テストコードのみ で、activerecord 本体の機能・挙動には影響しません。
  • NotificationAssertions を利用する前提として、テスト環境で ActiveSupport::Testing::NotificationAssertions がミックスインされている必要があります(ActiveSupport::TestCase など)。本 PR は既存のセットアップを前提としており、新たに利用側で設定を変える必要はありません。
  • もし他のコンポーネントでまだ手動の Notifications.subscribed を使っているテストがある場合、本 PR の方針に倣って assert_notification / capture_notifications に置き換えることで、保守性が上がります。

  1. 参考情報 (あれば)

#56992 Deprecate require_dependency

マージ日: 2026/3/15 | 作成者: @fxn

  1. 概要 (1-2文で)
    require_dependency が「非推奨 (deprecate)」になり、将来的な削除に向けた準備が始まりました。クラシックオートローダからの移行や Rails < 7.0 サポートのために残されていた API を正式に廃止方向へ進める変更です。

  1. 変更内容の詳細

2-1. require_dependency の役割と今回の変更方針

  • もともと require_dependencyclassic autoloader(Zeitwerk 導入前の自前オートローダ)向けの仕組みで、
    • 依存関係クラス/モジュールの再読み込みを正しく行うため
    • 通常の require ではオートロード・再読み込みの追跡ができないケースを補うため
      に使われていました。
  • Zeitwerk ベースの autoloading が標準になって久しく、かつ Rails 7 以降を前提にした開発が一般的になってきたため、「互換性維持のために残していた require_dependency をそろそろ削除していく」という流れです。
  • この PR では「即削除」ではなく「非推奨化 (deprecation)」で、利用時に警告を出すような形に変更されています。

2-2. コードレベルの変更点

※PR本文から読み取れる範囲・Rails の一般的な流れからの推定を含みます。

activesupport/lib/active_support/dependencies/require_dependency.rb

  • require_dependency の実装ファイルに、非推奨警告を出すロジックが追加されています。

  • 典型的には、呼び出されたときに次のようなメッセージを出す形になります(実際の文言は PR/CHANGELOG に依存しますが、趣旨としては以下のような内容):

    ruby
    ActiveSupport::Deprecation.warn(
      "`require_dependency` is deprecated and will be removed in a future version of Rails. " \
      "If you rely on it for autoloading, please ensure your code is compatible with Zeitwerk " \
      "and use conventional autoloading instead."
    )
  • 既存の挙動(指定したファイルを require しつつ依存追跡する)は保たれていますが、「使うと警告が出る」状態になります。

activesupport/test/dependencies/*.rb, dependencies_test.rb

  • mutual_one.rb, mutual_two.rb, requires_nonexistent1.rb などのテスト用ファイルからコードが削除されており、require_dependency に依存したテストケースが整理・簡略化されています。
  • dependencies_test.rb では +24/-14 と比較的大きな変更があり、
    • require_dependency のテストが「非推奨 API 用テスト」的なものに書き換えられている
    • あるいは、「これまで必須とされていた挙動」から「将来削除可能なレガシー API」として位置づけるようなテスト方針に変更されている
      という方向性が考えられます。

activesupport/CHANGELOG.md

  • CHANGELOG に require_dependency 非推奨の項目が追加されています(+30 行)。
  • おそらく以下のような内容が含まれているはずです:
    • require_dependency が非推奨になったこと
    • 代替策として「Zeitwerk の自動読み込みに従うこと」「どうしても明示読み込みが必要な場合は通常の require を使うこと」などの記述
    • 削除予定バージョン(例: Rails 8.0 など)が示されている可能性あり

  1. 影響範囲・注意点

3-1. 影響を受けるコード

  • 次のようなコードを書いているアプリ/エンジンが警告対象になります:

    ruby
    class PostsController < ApplicationController
      require_dependency 'admin/post_policy' # これが非推奨に
    end
  • 特に注意が必要なのは、エンジンや gem の中で require_dependency を使っているケースです。

    • それらは「Rails < 7.0 もサポートするために残している」ことが多く、まさに PR 本文に書かれている対象です。
    • 今後、サポート対象バージョンを上げるタイミングで require_dependency の削除を検討すべきです。

3-2. 実務上どう対応すべきか

  1. Zeitwerk に沿ったオートロードを徹底する

    • app/models, app/controllers などの標準パスを使い、クラス名とファイルパスを Zeitwerk ルールに合わせる。
    • lib/ 以下をオートロードしたい場合は config.autoload_paths / eager_load_paths を適切に設定する。
  2. 明示 require_dependency をやめる

    • 多くの場合、正しいパスにファイルを置きさえすれば require_dependency 自体が不要になります。
    • 本当に「明示的に読み込みたいだけ」であれば、単純に requirerequire_relative に置き換えればよいです。
      • この場合、そのファイルは 再読み込み対象ではなくなる ので、開発環境でのコード変更が自動で反映されないことに注意してください。
  3. エンジン/gem でのマルチバージョンサポート

    • Rails 5/6 をまだサポートする gem では、いきなり require_dependency を消せない場合があります。
    • とはいえ、この PR により 最新版 Rails では deprecation 警告が出続けるようになるため、
      • 「Rails 7 未満サポートをいつまで続けるか」
      • 「次のメジャー gem バージョンで require_dependency と古い Rails サポートを一気に落とす」
        といった移行計画を立てることが推奨されます。

3-3. 将来的な削除リスク

  • この PR は「削除のための第一歩」としての非推奨化です。
  • 将来のメジャーバージョン(例: Rails 8 以降)で require_dependency が完全に削除される可能性が高いので、
    • 今のうちに全コードベースからの利用箇所を洗い出し、置き換え/設計見直しをしておくのが安全です。
  • CI などで deprecation をエラー扱いにしているプロジェクトは、すぐにビルドが赤くなるはずなので要対応です。

  1. 参考情報 (あれば)

#56986 Further reduce internal usage of cattr_accessor and mattr_accessor

マージ日: 2026/3/15 | 作成者: @byroot

  1. 概要 (1-2文で)
    Rails内部で cattr_accessor / mattr_accessor(クラス変数ベースのアクセサ)の利用をさらに減らし、主に「クラスインスタンス変数」や class_attribute への移行を進めたPRです。クラス変数の共有挙動による思わぬ副作用を避け、より直感的で安全な設定・状態管理に寄せるリファクタリングになります。

  1. 変更内容の詳細

背景: なぜ cattr_accessor / mattr_accessor を減らすのか

  • cattr_accessor / mattr_accessor は内部的に「クラス変数(@@var)」を使います。
  • クラス変数は「継承階層で共有」されるため、
    • サブクラス側の変更が親クラスや兄弟クラスにも影響する
    • ライブラリ・アプリが混在すると意図せぬ状態共有が起きやすい
      という問題があります。
  • 近年 Ruby 本体側でクラス変数の速度は改善されたものの、「共有スコープの広さ」という意味での問題は残ったままです。
  • そのため Rails コアでは、
    • ほとんどのケースで「クラスインスタンス変数」 (@varself に対して持つ) を使う
    • 必要に応じて class_attribute(継承時のコピー/オーバーライドが扱いやすい仕組み)を使う
      という指針に寄せており、本PRはそのフォローアップです。

※ 前提となるPR: https://github.com/rails/rails/pull/42442


主な変更ポイント

実際の差分は細かいですが、パターンとしては次の2種類です。

1) cattr_accessor / mattr_accessor -> class_attribute or クラスインスタンス変数

ファイルごとの変更はおおむね以下のような内容です。(疑似コードで表現)

  • 例: activesupport/lib/active_support.rb などでの変更

    ruby
    # 以前 (イメージ)
    module ActiveSupport
      mattr_accessor :some_config, instance_accessor: false, default: nil
    end
    
    # 以後 (イメージ)
    module ActiveSupport
      class_attribute :some_config, instance_accessor: false, default: nil
    end

    または、class_attribute も不要なシンプルなケースでは、

    ruby
    class SomeClass
      # 以前
      cattr_accessor :some_flag
    
      # 以後
      class << self
        attr_accessor :some_flag
      end
      @some_flag = false
    end

    のように、クラスインスタンス変数+クラスメソッドのアクセサに切り替えられています。

  • ActiveSupport::CacheActiveStorage::BlobActionMailbox::Routing などでも同様に、キャッシュ設定やルーティング設定といった「グローバル寄りの設定値」を、クラス変数による共有からより局所的な管理に移行しています。

2) 古い互換レイヤの削除・整理

  • activesupport/lib/active_support/core_ext/date_and_time/compatibility.rb
    Date/Time/DateTime まわりの互換レイヤに関するコードがまとめて削除されています(-23行、追加0)。
    Rails が過去バージョンとの互換性のために持っていた「古い挙動向けのフラグ」やクラス変数を整理し、現行挙動に一本化していると考えられます。
  • 同様に date_time/compatibility.rbtime/compatibility.rb もクリーンアップされています。

3) テストコードの更新

  • activerecord/test/models/developer.rbmembership.rb などのテスト用モデルや、
  • activesupport/test/cache/behaviors/cache_store_behavior.rbdependencies_test.rb など

で、もともと cattr_accessor / mattr_accessor 前提で書かれていたテスト設定が、class_attribute やクラスインスタンス変数前提のコードに書き換えられています。
これによりテストが新しいパターンを前提としたものになり、今後 cattr_accessor / mattr_accessor を減らしやすくなります。


  1. 影響範囲・注意点

Rails 内部への影響

  • あくまで「内部実装の状態保持方法の見直し」であり、パブリックAPIのインターフェース(メソッド名や返り値など)は基本的に変わっていません。
  • ただし、クラス変数からクラスインスタンス変数や class_attribute に変えたことで、「継承したサブクラス間での状態共有の仕方」が内部的に変わっている箇所があります。
    • これまで「たまたまクラス変数の共有挙動に依存してうまく動いていた」ような非公開API依存のコードは、挙動が変わる可能性があります(特に monkey patch や内部クラスを継承している場合)。

アプリケーション開発者への実務的な注意

  • 通常のアプリケーション開発では、このPR単体による破壊的変更の影響はほぼありません。
  • ただし、次のような場合は注意が必要です:
    • Rails 内部クラスを継承して、クラスレベルの設定を直接上書きしている
    • Rails の内部モジュール内のクラス変数(@@xxx)に直接アクセスしている
  • このPRは「Rails コアはクラス変数を避ける」という方向性をさらに明確にしたものなので、アプリ側でも同様の方針をとるのが望ましいです:
    • 新規コードでは cattr_accessor / mattr_accessor よりも、
      • 単純な「クラスごとの設定・状態」ならクラスインスタンス変数+class << self; attr_accessor ... end
      • 継承関係でオーバーライド・デフォルトの継承を扱いたいなら class_attribute を優先するようにすると、将来的なトラブルを避けやすくなります。

パフォーマンス面

  • Ruby 本体でクラス変数のアクセス速度は改善されているものの、クラスインスタンス変数/class_attribute によるシンプルな構造の方が、プロファイル上も扱いが素直になりやすく、並行実行環境でも予期しない状態共有を減らせます。
  • PRの規模は +48/-82 と比較的小さく、パフォーマンス上の大きな悪化は想定されません。むしろ設計上の明確化によるメンテナンス性向上が主眼です。

  1. 参考情報 (あれば)

#56983 [ci skip] Update CHANGELOG with transform_keys! overwrite precedence note

マージ日: 2026/3/15 | 作成者: @diaphragm

  1. 概要 (1-2文で)
    このPRは、Rails 8.0.3 で行われた ActiveSupport::HashWithIndifferentAccess#transform_keys! のバグ修正に伴い、「キー変換時にキーが衝突した場合の上書き優先順位」が変わったことを CHANGELOG に明記するドキュメント修正です。挙動は Ruby 標準の Hash#transform_keys! に合わせて「あと勝ち」になっており、その仕様変更をサンプル付きで説明しています。

  1. 変更内容の詳細

何をしたか

  • activesupport/CHANGELOG.md の Rails 8.0.3 の項目に、ActiveSupport::HashWithIndifferentAccess#transform_keys! の上書き優先順位変更に関する注記を追記。
  • 具体的な「Before / After」コード例を追加し、キー衝突時の挙動が旧バージョンからどのように変化したかを示しています。

挙動のポイント

ActiveSupport::HashWithIndifferentAccess#transform_keys! でキー変換を行ったとき、異なるキーが同じキーに変換されて衝突した場合の扱いが変わっています。

  • 以前 (8.0.3 以前):
    • 「先に存在していたキーの値」が優先される(先勝ち)
  • 現在 (8.0.3 以降):
    • 「あとから変換されたキーの値」が優先される(後勝ち)
    • 標準の Hash#transform_keys! と同じ挙動

イメージ例

※ CHANGELOG の例は camelCase / snake_case の衝突を使って説明されています(例のイメージ):

ruby
hash = ActiveSupport::HashWithIndifferentAccess.new(
  "userName" => "Alice",
  :user_name => "Bob"
)

hash.transform_keys! { |k| k.to_s.underscore }

# Rails 8.0.2 まで (旧挙動: 先勝ち)
# => { "user_name" => "Alice" }

# Rails 8.0.3 以降 (新挙動: 後勝ち, Ruby Hash と同じ)
# => { "user_name" => "Bob" }

このように、camelCase と snake_case が同じ user_name に変換されるケースで、どちらの値が最終的に残るかが逆転したことが明示されています。


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

    • ActiveSupport::HashWithIndifferentAccess#transform_keys! を利用しており、かつ
      • キー変換ロジックによって「複数の異なるキーが同一キーへマージされる」コード
      • 例: camelCasesnake_case 変換、prefix/suffix の削除、正規化処理 など
        を行っている箇所で、最終的に残る値が 以前と逆になる 可能性があります。
  • 注意点

    • 今回のPRはコード変更ではなく ドキュメントのみ の更新ですが、
      • Rails 8.0.2 以前 → 8.0.3 以降にアップグレードする場合、
      • すでに挙動は「後勝ち」に変わっており、その仕様に依存する/していたロジックは再確認が必要です。
    • 特に「キー変換後のハッシュをマージロジックとして利用している」「衝突時に先勝ちを前提にしていた」コードは、テストで期待値が変わっていないかを確認した方が安全です。
    • 逆に、「Ruby 標準 Hash と同じであってほしい」と期待していた場合は、今回の挙動が正しい(統一された)ものになります。

  1. 参考情報 (あれば)
  • 対応する issue: Fixes #56960
    • Rails 8.0.3 の HashWithIndifferentAccess#transform_keys! バグ修正により上書き優先順位が変わったが、ドキュメントに明記されておらず混乱を招く可能性がある、という報告に対する対応。
  • 関連機能:
    • Ruby: Hash#transform_keys!
    • Rails: ActiveSupport::HashWithIndifferentAccess#transform_keys!
      (今回の挙動は Ruby の Hash に合わせる形で統一されています。)

#56974 Simplify applicable activesupport tests with NotificationAssertions helpers

マージ日: 2026/3/13 | 作成者: @larouxn

  1. 概要 (1-2文で)
    ActiveSupport::Notifications を使ったキャッシュ関連テストを、ActiveSupport::Testing::NotificationAssertions ヘルパーで書き換えて簡潔・安全にした PR です。手動でのサブスクライバ管理やインスタンス変数ベースのイベント収集をやめ、共通ヘルパーによる記述に統一しています。

  1. 変更内容の詳細

どのテストが対象か

activesupport/test/cache/behaviors/cache_store_behavior.rb 内の「通知まわり」をテストしているヘルパーメソッド(モジュール的な共通テスト)が変更されています。
このファイルのテストは以下の各ストアテストから include されて実行されています。

  • file_store_test.rb
  • mem_cache_store_test.rb
  • memory_store_test.rb
  • redis_cache_store_test.rb

つまり、「各キャッシュストア共通の Notification テストの書き方」を差し替えた、という位置づけです。

具体的なリファクタリング内容

1) NotificationAssertions ヘルパーの利用

これまで:

  • ActiveSupport::Notifications.subscribe で手動購読
  • ensure ブロックで unsubscribe を手動で実施
  • ブロック内で @events << event のようにインスタンス変数にイベントを溜める
  • テスト本体で @events を検査

という流れだったものを、ActiveSupport::Testing::NotificationAssertions で用意されているヘルパーに書き換えています。

代表的には以下の2パターンが使われます:

  1. 「通知が送られたことをその場で検証」するタイプ

    • 例: assert_notification "cache_read.active_support" do ... end
    • ブロック内で対象コードを実行し、通知が飛んだか・payload がどうかを引数で受けて検証
  2. 「通知をキャプチャだけして後で自前で検証」するタイプ

    • 例: events = capture_notifications("cache_read.active_support") do ... end
    • ブロック実行中に飛んだイベントの配列を返し、あとで events に対して好きな assertion を書く

この PR の説明にある通り、「適しているところは assert_* 系で直接アサート」「それ以外は capture して自前アサート」という使い分けに変更されています。

2) @events → ローカル変数 events への整理

旧来はテストクラスのインスタンス変数 @events にイベントを溜め込んでいましたが、ヘルパーが events を戻り値として返すため、テストメソッド内で完結するローカル変数 events に切り替えています。

これにより:

  • テスト間で状態が残らない(インスタンス変数に依存しない)
  • メソッド毎に「どのイベントを見ているか」が分かりやすくなる

といった形でテストコードが局所的かつ明瞭になります。

3) ensure ブロックでの unsubscribe 削除

NotificationAssertions が購読の開始・終了をラップし、テスト終了時に自動でクリーンアップするため、

ruby
subscription = ActiveSupport::Notifications.subscribe("...") { ... }
begin
  ...
ensure
  ActiveSupport::Notifications.unsubscribe(subscription)
end

のようなパターンは不要になり、ブロックベースな API だけで完結するように書き換えられています。
テストが例外で落ちても leak しないクリーンアップが保証されるのは従来どおりですが、コードでそれを毎回書かなくて良くなっています。


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

    • プロダクションコードは一切変更なし。
    • activesupport のキャッシュストア共通テスト (cache_store_behavior) を include している以下のテストに限られます:
      • FileStore / MemCacheStore / MemoryStore / RedisCacheStore
    • 動作仕様としての挙動は変えておらず、「テストの書き方・実装スタイル」のみの変更です。
  • 注意点

    • 今後 ActiveSupport::Notifications を使ったテストを書く場合は、同様に ActiveSupport::Testing::NotificationAssertions を使うのが推奨される流れになります。
    • 既存のテストコードで手動 subscribe/unsubscribe している箇所は、今後この PR のように段階的に置き換えられていく可能性が高いです。
    • NotificationAssertions の挙動(イベントのキャプチャ粒度・スレッドとの関係など)に依存することになるため、独自ラッパーを重ねる場合は挙動が二重にならないよう注意が必要です。

  1. 参考情報 (あれば)

#56975 Simplify applicable activerecord tests with NotificationAssertions helpers

マージ日: 2026/3/13 | 作成者: @larouxn

  1. 概要 (1-2文で)
    activerecord の一部テストで、ActiveSupport::Notifications を直接扱っていた箇所を、ActiveSupport::Testing::NotificationAssertions ヘルパーに置き換えて簡潔にした PR です。これにより通知購読の登録/解除やアサーションが共通化され、テストコードが短く・安全になっています。

  1. 変更内容の詳細

全体方針

  • 対象: ActiveRecord のテストのうち、ActiveSupport::Notifications を使ってイベント発火を検証しているテスト。
  • 変更: 手書きで ActiveSupport::Notifications.subscribe / unsubscribe や配列に push してアサートしていたコードを、ActiveSupport::Testing::NotificationAssertions モジュールのヘルパーに置き換え。
  • 効果:
    • ensure での購読解除 (unsubscribe) が不要(ヘルパー側で自動クリーンアップ)。
    • よくあるパターン(特定イベントが発火したか・何件か・どんな payload か)のアサートを一行で書ける。
    • テストの意図が読み取りやすくなる。

NotificationAssertions に代表的には以下のような API があります(ドキュメント: https://api.rubyonrails.org/classes/ActiveSupport/Testing/NotificationAssertions.html):

ruby
# ブロックの中で指定イベントが 1 回だけ発火することを検証
assert_notification "sql.active_record" do
  User.first
end

# 発火した通知を配列で受け取ってカスタム検証
notifications = capture_notifications("sql.active_record") do
  User.first
end
assert_equal 1, notifications.size
assert_equal "User Load", notifications.first[:name]

PR ではこのようなヘルパーを使った形に書き換えています。


ファイルごとのポイント

※PR の diff から読み取れる典型パターンをまとめています(行数は +/- の規模感のみ)。

1) activerecord/test/cases/adapters/abstract_mysql_adapter/empty_all_tables_test.rb (+2/-8)

  • 目的: empty_all_tables 実行時に、想定した ActiveSupport::Notifications のイベント(例: sql.active_record や adapter 固有のイベント)が発火しているかを検証するテスト。
  • 従来:
    • notifications = []
    • subscriber = ActiveSupport::Notifications.subscribe("...") { |*args| notifications << args }
    • ensureActiveSupport::Notifications.unsubscribe(subscriber)
    • assert_equalnotifications の件数や内容を確認
  • 変更後:
    • 上記を capture_notifications あるいは類似ヘルパーに置き換え:
      ruby
      notifications = capture_notifications("sql.active_record") do
        ActiveRecord::Base.connection.empty_all_tables!
      end
      
      assert_equal expected_count, notifications.size
    • 手動での subscribe / ensure ブロック削除。

2) activerecord/test/cases/associations/belongs_to_associations_test.rb (+2/-6)

  • 目的: belongs_to に関連した操作(ロードや保存)で、所定の ActiveRecord 通知が発生するかの検証。
  • 変更内容:
    • 通知の購読・解除と配列への push を、capture_notifications などのヘルパーに置き換え。
    • テスト本体は維持しつつ、「どのイベントが何回発火したか」をシンプルにアサート。

例イメージ:

ruby
notifications = capture_notifications("sql.active_record") do
  post = Post.create!(...)
  post.author   # belongs_to のロードなど
end

assert_operator notifications.size, :>=, 1

3) activerecord/test/cases/associations/deprecation_test.rb (+6/-12)

  • 目的: 非推奨の関連付け API 利用時に、特定の deprecation 関連通知が発火することを検証。
  • 変更点:
    • ここは通知の発火そのものに加え、「イベント名・payload 内容を詳細に見る」テストである可能性が高く、assert_notification ではなく capture_notifications パターンが多いと考えられます。
    • それでも subscribe/unsubscribe はすべてヘルパーに一本化。
  • 結果:
    • 行数減少幅が比較的大きく、テストの意図(「この操作でこの deprecation 通知が出る」)がより直接的なコードになっている。

サンプルイメージ:

ruby
notifications = capture_notifications("deprecation.rails") do
  # 非推奨な has_and_belongs_to_many 等を呼ぶ
end

assert notifications.any? { |n| n[:message].include?("...") }

4) activerecord/test/cases/associations/has_many_associations_test.rb (+2/-7)

  • 目的: has_many 関連のロードや書き込みに対して、期待した通知が出ているかを検証。
  • 変更:
    • 他と同様に独自に定義していた購読ロジックをヘルパーへ移行。
    • イベント発火回数や、特定の関連に対する SQL 実行を間接的に確認するエテストをスリム化。

5) activerecord/test/cases/associations/has_one_associations_test.rb (+2/-6)

  • has_one 版の has_many と同様の整理。
  • 通知捕捉が NotificationAssertions ベースになり、冗長な後始末コードが削除。

6) activerecord/test/cases/transaction_isolation_test.rb (+10/-30)

  • 目的: トランザクション分離レベル関連の処理に伴うイベント(sql.active_record 等)を検証。
  • 従来:
    • 複数の subscribe / unsubscribeensure ブロックでトランザクション内外の通知を捕捉。
  • 変更後:
    • capture_notifications によるブロック単位の通知収集へ。
    • 分離レベル変更クエリや実際の SELECT/UPDATE クエリに対する通知を、イベントリストを元に明瞭にアサート。
    • 行数削減が大きいことから、ここでのテストがもっとも複雑な独自購読ロジックを持っていたと考えられます。

例イメージ:

ruby
notifications = capture_notifications("sql.active_record") do
  Person.transaction(isolation: :serializable) do
    Person.first
  end
end

# 分離レベル設定用の SQL と通常の SELECT の両方が発火しているか等を確認
assert notifications.any? { |n| n[:sql].match?(/SET TRANSACTION ISOLATION LEVEL SERIALIZABLE/i) }

  1. 影響範囲・注意点
  • 影響範囲:
    • プロダクションコードではなく、activerecord のテストコードのみの変更。
    • テストのロジック(「何を検証しているか」)は基本的に同じで、実装手段だけがヘルパー利用に変わっている。
  • 利用前提:
    • NotificationAssertions を利用するには、テストケースが ActiveSupport::Testing::NotificationAssertionsinclude している必要がありますが、対象テストスイートでは既に利用可能な前提(以前の PR で導入済)。
  • 注意点:
    • 今後 ActiveSupport::Notifications を使った新規テストを書く場合は、従来の手作り subscribe/unsubscribe パターンではなく、このヘルパーを使うのが推奨されます。
    • ヘルパーは自動でクリーンアップを行うため、複雑にネストした通知購読など独自のライフサイクル制御が必要な場合は、仕様(リンク先ドキュメント)を一度確認した方が安全です。
    • まだ他にも NotificationAssertions で書き換え可能なテストが残っていると明言されているため、今後も類似のクリーンアップ PR が出る可能性があります。

  1. 参考情報 (あれば)

#56972 Add MySQL lock option and extend algorithm to column DDL operations

マージ日: 2026/3/13 | 作成者: @dominikdarnel

  1. 概要 (1-2文で)
    このPRは、Rails の MySQL アダプタに lock: オプションを追加し、これまでインデックス操作にしか使えなかった algorithm: オプションをカラム操作(add_column など)にも拡張するものです。これにより、MySQL のオンラインDDL機能(ALGORITHM / LOCK)を Rails のマイグレーションDSLから直接制御できるようになります。

  1. 変更内容の詳細

対応したオプションと対象操作

MySQL アダプタに以下のオプションが追加・拡張されています。

  • lock: オプション(MySQL)

    • 対象:
      • add_index
      • remove_index
      • カラム操作:
        • add_column
        • change_column
        • remove_column
        • rename_column
    • 値:
      • :default, :none, :shared, :exclusive
      • MySQL の LOCK = {DEFAULT|NONE|SHARED|EXCLUSIVE} に対応
  • algorithm: オプション(MySQL)

    • 既存: インデックス操作 (add_index, remove_index) に対してサポート済
    • 今回拡張: 上記カラム操作にも利用可能
    • 値:
      • :default, :copy, :inplace, :instant
      • MySQL の ALGORITHM = {DEFAULT|COPY|INPLACE|INSTANT} に対応

具体的な利用例

これまで MySQL のオンラインDDLを使いたい場合、raw SQL が必要でした:

ruby
# 変更前: raw SQL が必要
execute "ALTER TABLE users ADD name VARCHAR(255), ALGORITHM = INSTANT, LOCK = NONE"

このPR後は、通常のマイグレーションDSLで指定できます:

ruby
# インデックス操作
add_index    :users, :email, algorithm: :inplace,  lock: :none
remove_index :users, :email, algorithm: :inplace,  lock: :none

# カラム追加
add_column    :users, :name, :string, algorithm: :instant, lock: :none

# カラム定義変更
change_column :users, :name, :string, null: false, algorithm: :inplace, lock: :none

# カラム削除
remove_column :users, :name, algorithm: :inplace, lock: :none

# カラム名変更
rename_column :users, :name, :full_name, algorithm: :inplace, lock: :none

内部的には、MySQLの ALTER TABLE 文末に以下のように付与されます:

sql
ALTER TABLE `users`
  ADD COLUMN `name` varchar(255)
  ALGORITHM = INSTANT
  LOCK = NONE

CommandRecorder の対応

ロールバック時にも同じオンラインDDLオプションが保持されるように、CommandRecorder#invert_rename_column が拡張されています。

ruby
# マイグレーション
rename_column :users, :name, :full_name, algorithm: :inplace, lock: :none

# ロールバック時も同じオプションが付いた rename に反転される
rename_column :users, :full_name, :name, algorithm: :inplace, lock: :none

これは、PostgreSQL における algorithm: :concurrently サポートと同様の実装パターンに従っています。

実装まわりに触れている主な変更点

高レベルの観点でいうと:

  • 抽象レイヤ (abstract/schema_statements.rb, abstract/schema_definitions.rb) に、カラム関連DDLでも algorithm / lock オプションを受け取れるようなインターフェイスを追加・調整
  • MySQL アダプタ (abstract_mysql_adapter.rb, mysql/schema_statements.rb, mysql/schema_creation.rb) で、渡された algorithm: / lock:ALTER TABLE ... ALGORITHM = ... LOCK = ... に組み立てるロジックを追加
  • migration/command_recorder.rb で、記録するコマンドに algorithm / lock を含め、特に rename_column の反転時にもオプションを維持
  • テスト:
    • MySQL 用 ActiveSchema テストに、各カラム操作+インデックス操作で algorithm / lock が正しくSQLに反映されるかのテストを追加
    • 無効なオプションの扱い(invalid_options_test)の拡充
    • CommandRecorder のオプション保持のテストを追加
  • ガイド (active_record_migrations.md) に、MySQLでの algorithm: / lock: 利用方法の説明を追加

  1. 影響範囲・注意点
  • 対象DB:
    • MySQL アダプタのみ(PostgreSQL の algorithm: :concurrently には影響なし)
  • 既存コードへの互換性:
    • algorithm: / lock: を指定していない既存マイグレーションの挙動は変わりません。
    • デフォルトはMySQLの ALGORITHM = DEFAULT / LOCK = DEFAULT 相当で、従来と同様にMySQL側のデフォルト動作に委ねられます。
  • オプション値の妥当性:
    • サポートされていない値を渡すと、テストを見る限り(invalid_options_test)従来同様に例外になるか、少なくともRails側で不正オプションとして扱われます。
    • 値のtypo(例: :instat)などには注意が必要です。
  • 実運用上の注意:
    • algorithm: / lock: は MySQL のバージョンやテーブルの状態によりサポート/不支持があります。
      • 例: ALGORITHM=INSTANT はMySQL 8系かつ特定のDDLにしか対応しないなど。
    • 不正な組み合わせ(例: そのDDLでは INSTANT がサポートされない)を指定した場合は、MySQL 側がエラーを返します。そのため、本番適用前に対象MySQLバージョンで検証することが重要です。
  • マイグレーション設計へのインパクト:
    • 大規模テーブルのオンラインスキーマ変更を、RailsのマイグレーションDSLだけで表現しやすくなります。
    • これまで execute "ALTER TABLE ..." で書いていた部分を、DSLベースにリファクタリング可能です。
    • ロールバック時にも同じオプションが使われるため、オンラインDDLを前提とした安全なマイグレーション/ロールバック戦略を組みやすくなります。

  1. 参考情報 (あれば)
  • PR本文のディスカッション:
    https://discuss.rubyonrails.org/t/add-mysql-lock-option-and-extend-algorithm-to-column-ddl-operations/90249
  • MySQLドキュメント: ALTER TABLE, ALGORITHM, LOCK
    • ALTER TABLE ... ALGORITHM / LOCK の詳細仕様やサポートされる組み合わせは、利用している MySQL バージョンの公式マニュアルを参照してください。
  • 類似機能:
    • PostgreSQL の algorithm: :concurrently サポートと同じ思想で、DDLのロック特性/実行方法をマイグレーションDSLから制御するための拡張です。

#56945 ActiveRecord: Support Postgres RESET on readonly queries

マージ日: 2026/3/13 | 作成者: @frodsan

  1. 概要 (1-2文で)
    PostgreSQL アダプタで prevent_writes: true(読み取り専用コンテキスト)でも RESET コマンドを実行できるようにした PR です。これにより、接続を「書き込み禁止」にしていても、セッションパラメータをデフォルトに戻すための RESET が ReadOnly エラーにならず通るようになります。

  1. 変更内容の詳細

やりたいこと / 背景

  • Rails の PostgreSQL アダプタは prevent_writes: true の場合に「書き込みっぽい」SQLを検出すると ActiveRecord::ReadOnlyError を投げます。
  • しかし PostgreSQL の RESETSET config_parameter TO DEFAULT のシンタックスシュガーであり、多くの場合は「状態を元に戻す」だけで、本質的には危険な書き込みではありません。
  • それにもかかわらず、現状の実装では RESET が「書き込み」と判定されて ReadOnlyError が発生していたため、それを許可するように修正しています。

実際のコード変更ポイント

1) PostgreSQL アダプタの prevent_writes 判定の緩和

activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb 内で、SQL が書き込みかどうかを判定するロジックに RESET を許可する変更が入っています。

イメージとしては、例えば次のような判定ロジックの一部が:

ruby
WRITE_QUERY = /\A\s*(UPDATE|INSERT|DELETE|ALTER|...)/i

のような形で書き込み系を検出していて、RESET が「ブロック対象」側に入っていた・もしくは包括的にブロックされていたところを、

  • RESET については prevent_writes の対象から除外
  • つまり RESETprevent_writes 中でも実行可能

という扱いに変えています。(実際のコードは 1 行差分ですが、このポリシー変更が本質です)

2) テスト追加

activerecord/test/cases/adapters/postgresql/postgresql_adapter_prevent_writes_test.rb にテストが追加されています。

ざっくりしたイメージ:

ruby
def test_reset_is_allowed_with_prevent_writes
  ActiveRecord::Base.connected_to(role: :reading, prevent_writes: true) do
    ActiveRecord::Base.connection.execute("RESET ALL")
    # ここで例外が出ないことを確認
  end
end

のように、「読み取り専用コンテキストで RESET を実行しても ActiveRecord::ReadOnlyError が起きない」ことを検証するテストが足されています。

3) CHANGELOG 追記

activerecord/CHANGELOG.md に以下のような記述が追加されています:

  • PostgreSQL アダプタが prevent_writes: true のときでも RESET を実行できるようになった旨
  • バージョンアップ時の利用者向け情報としての位置づけ

  1. 影響範囲・注意点

影響範囲

  • 対象: PostgreSQL を使っていて、ActiveRecord::Base.connected_to(... prevent_writes: true) などで読み取り専用ルーティング・保護をしているアプリケーション。
  • これまでは:
    • prevent_writes: true 下で RESET を実行すると ActiveRecord::ReadOnlyError が発生。
  • 今後は:
    • 同じ条件でも RESET は実行可能になり、エラーにならない。

セキュリティ / 一貫性上の注意点

  • RESET 自体はセッションパラメータをデフォルトに戻すだけで、データ行の変更は行いません。
  • ただし、RESET によって以下のような「間接的な影響」がありうる点には注意:
    • 以前に SET ROLESET search_path などを行っていた場合、それがデフォルト設定に戻る。
    • アプリ側が「セッションパラメータをある前提」で動いているコードがあるなら、RESET による前提崩れには気を付ける必要があります(これは prevent_writes の有無にかかわらず本質的な注意点)。

とはいえ、この PR の変更は「RESET を prevent_writes フィルタから除外するだけ」であり、既に手動で RESET を使っていたアプリケーションにとっては「ReadOnly 保護下でも同じことができるようになる」方向の互換性改善です。


  1. 参考情報 (あれば)

#56967 Optimize generated Dockerfile build performance

マージ日: 2026/3/13 | 作成者: @damln

  1. 概要 (1-2文で)
    Rails が rails new で生成する Dockerfile テンプレートと、そのテスト用 Dockerfile のビルドを高速化するための最適化 PRです。不要なレイヤーを減らし、COPY --chown を適切に使うことで、実測で 1 分前後のビルド時間短縮が見込まれます。

  1. 変更内容の詳細

2-1. node_modules 削除をアセットプリコンパイルと同一レイヤーに統合 (Dockerfile.tt)

従来は、ざっくり以下のように別レイヤーで node_modules を削除していたと考えられます:

dockerfile
RUN bundle exec rails assets:precompile
RUN rm -rf node_modules

これだと:

  • assets:precompile のレイヤー
  • rm -rf node_modules のレイヤー

という 2 つのレイヤーができ、特に後者では大量のファイル削除に伴うファイルシステム diff 計算が発生し、BuildKit のオーバーヘッドが大きくなります。

PR では、これを 1 つの RUN にまとめています:

dockerfile
RUN bundle exec rails assets:precompile \
  && rm -rf node_modules

あるいは実際のテンプレートもほぼこれと同等の形になっているはずです。

ポイント:

  • レイヤー数削減により、BuildKit が「削除だけを含む大きな diff レイヤー」を計算するコストをなくせる
  • 実ビルドで ~13 秒程度短縮と報告されている
  • アセットプリコンパイル完了後に node_modules を削除する、という意味的な流れは維持されるので挙動は変わらない

2-2. chown -RCOPY --chown に置き換え (Dockerfile.test)

テストフィクスチャの Dockerfile(railties/test/fixtures/Dockerfile.test)は、まだ古いパターンを使っていました:

dockerfile
COPY db log storage tmp /app/
RUN chown -R rails:rails db log storage tmp

このやり方だと:

  • COPY レイヤー
  • さらに chown -R レイヤー

と 2 レイヤーになり、特に chown -R は多量のファイル・ディレクトリに対する metadata 変更で、ビルドがかなり遅くなります。

メインの Dockerfile テンプレートはすでに COPY --chown を使っていたため、テスト側をそれに合わせる修正です:

dockerfile
COPY --chown=rails:rails db log storage tmp /app/

ポイント:

  • コピー時に所有者を設定するため、追加の RUN chown -R ... レイヤーが不要になる
  • 所有権変更のためのファイルシステム diff 計算が発生せず、ビルド時間が大幅に減る
  • 実測ではこの変更だけで ~50 秒程度の短縮と報告されている
  • メインテンプレートとテストフィクスチャの挙動が揃う(テンプレートの期待とテストが一貫する)

  1. 影響範囲・注意点

影響範囲

  • 影響対象:
    • rails new myapp --docker(などで)生成される Dockerfile テンプレート (Dockerfile.tt)
    • そのテンプレートを検証するテスト用 Dockerfile (Dockerfile.test)
  • 機能面:
    • Rails アプリの挙動そのものには変更なし
    • Docker イメージ内のファイル構成や権限は、従来と論理的には同等
  • 主な効果:
    • Docker ビルド時間の短縮
    • レイヤー数の削減と、BuildKit の diff 計算コスト削減

注意点・互換性

  • COPY --chown サポート:
    • Docker 17.09 以降でサポートされている一般的な機能
    • かなり古い Docker を使っていない限り問題にはならない
  • 既存プロジェクトへの影響:
    • 既に生成済みの Dockerfile には自動反映されないため、必要なら手動で同様の最適化を適用する必要がある
  • ビルドキャッシュへの影響:
    • レイヤー構成が変わるため、PRを取り込んだ直後の最初のビルドではキャッシュが効かずフルビルドになる可能性はあるが、その後の反復ビルドはより速くなる
  • rm -rf node_modules の位置:
    • assets:precompile と同一 RUN にまとめた結果、そのステップが失敗した場合は node_modules も削除されない(もともと precompile が成功しないと次の RUN に到達しないので、意味的にも従来とほぼ同じ)

  1. 参考情報 (あれば)
  • Docker ドキュメント:
  • レイヤー最適化の一般的なベストプラクティス:
    • 関連するコマンドを 1 つの RUN にまとめてレイヤー数を減らす
    • 大量ファイル削除(rm -rf)や大量権限変更(chown -R)を単独レイヤーにしない
  • この PR が示すベストプラクティス:
    • アセットプリコンパイル後に node_modules を消してイメージをスリム化するなら、RUN ... && rm -rf node_modules のように 1 レイヤーで行う
    • ユーザー権限を合わせるために chown -R を後から実行するのではなく、COPY --chownRUN --chown でコピー時・実行時に適切なユーザーを指定する

#56977 Fix SQLite virtual tables not ignored by ignore_tables

マージ日: 2026/3/13 | 作成者: @hschne

  1. 概要 (1-2文で)
    SQLite の仮想テーブル(virtual tables)が ActiveRecord::SchemaDumper.ignore_tables で無視されない問題を修正する PR です。これにより、スキーマダンプ(schema.rb の生成など)から、指定した SQLite 仮想テーブルも正しく除外できるようになりました。

  1. 変更内容の詳細

主な変更点

  • 対象ファイル:
    • activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb
    • activerecord/test/cases/adapters/sqlite3/virtual_table_test.rb

schema_dumper.rb の変更

SQLite 用の SchemaDumper が、テーブル一覧を取得する処理の中で「virtual table」も含めて処理していましたが、ignore_tables のフィルタリングがそれらに対して正しく機能していませんでした。
この PR では、そのフィルタリングロジックを修正し、通常のテーブルと同様に「無視対象テーブル」として扱えるようにしています。

実際のコードは 1 行の修正ですが、イメージとしては以下のような形の変更です(擬似コード):

ruby
# 変更前(イメージ)
tables.each do |table|
  # ここで virtual table が ignore_tables をすり抜けていた
  dump_table(table) unless ignored?(table)
end

# 変更後(イメージ)
tables.each do |table|
  next if ignored?(table) # ignore_tables を先に適用

  dump_table(table)
end

ポイントは「SQLite の仮想テーブルも ignore_tables の判定対象に含める」ことです。

テストの追加

activerecord/test/cases/adapters/sqlite3/virtual_table_test.rb に以下のようなテストが追加されています(内容イメージ):

  • SQLite の仮想テーブル(例: FTS テーブル)を作成
  • ActiveRecord::SchemaDumper.ignore_tables = ["virtual_table_name"] を設定
  • スキーマダンプを実行 (ActiveRecord::SchemaDumper.dump)
  • ダンプ結果に仮想テーブルの定義が含まれていないことを検証

これにより、今回の修正が回帰しないように自動テストで担保されています。


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

    • SQLite3 アダプタを使用している Rails アプリで、
      • SQLite の virtual table(例: FTS, R*Tree など)を利用しており
      • ActiveRecord::SchemaDumper.ignore_tables を使ってスキーマダンプから除外したい
        といったケースで、期待通りに無視されるようになります。
    • 他の DB アダプタ(PostgreSQL, MySQL など)には影響しません。
  • 互換性/挙動の変化

    • これまで「なぜか virtual table が schema.rb に出力されてしまう」状況だった場合、今回の修正により ignore_tables 設定どおり出力されなくなります。
    • 逆に「virtual table も含めてダンプされること」を前提にワークアラウンドしていた場合、その前提は崩れますが、基本的にはバグ修正として望ましい方向の変更です。
  • 注意点

    • ignore_tables は文字列/正規表現/Proc などを受け付けるため、virtual table 名に対しても適切にマッチするよう設定する必要があります。
    • structure.sql を使っている場合は、SchemaDumper ではなく DB ネイティブのダンプになるため、この修正の影響はありません(schema.rb を使っている場合のみ)。

  1. 参考情報 (あれば)
  • 該当 Issue: #56976 — SQLite virtual tables が ignore_tables で無視されない問題の報告
  • 対象メソッド:
    • ActiveRecord::SchemaDumper.ignore_tables
    • ActiveRecord::ConnectionAdapters::SQLite3Adapter 向けの SchemaDumper 実装
  • SQLite 仮想テーブルの代表例:
    • CREATE VIRTUAL TABLE ... USING fts5(...)
    • CREATE VIRTUAL TABLE ... USING rtree(...)

#56909 Validate URI scheme in Action Text markdown link conversion

マージ日: 2026/3/12 | 作成者: @flavorjones

  1. 概要 (1-2文で)
    Action Text がリッチテキストを Markdown に変換する際、リンクの URI スキームを検証していなかった問題を修正し、javascript:data:text/html など危険なスキームを Markdown に出力しないようにした PRです。HTML パイプラインと Markdown パイプラインの安全性・挙動の一貫性を高める修正です。

  1. 変更内容の詳細

問題の背景

  • HTML 出力では Rails::HTML::Sanitizer::SafeListSanitizer によって、
    <img src="javascript:..."><img src="data:text/html,..."> のような危険なスキームは除去されていた。
  • 一方で Markdown 出力では、RemoteImage#attachable_markdown_representationMarkdownConversion.markdown_link の経路で URI スキームの検証が行われておらず、
    <action-text-attachment url="data:text/html,PAYLOAD"> がそのまま
    ![Image](data:text/html,PAYLOAD) として Markdown に出力されてしまっていた。
  • 同じ Markdown パイプライン内でも、アンカーリンクを出力する箇所ではすでに allowed_uri? が呼ばれており、その点でも不整合があった。

主な修正点

  • Rails::HTML::Sanitizer.allowed_uri?(url)markdown_link 内で呼ぶように変更。
  • 許可されていない URI の場合は、リンクとして出力せず「エスケープされたテキストのみ」を出すように仕様変更。

挙動の例:

以前(問題あり):

html
<action-text-attachment url="data:text/html,PAYLOAD" caption="Image" />

Markdown 変換結果:

markdown
![Image](data:text/html,PAYLOAD)

修正後:

markdown
\[Image\]

ここで \[Image\] は、Markdown としては単なるリテラル文字列 [Image](リンクではない)として表示されます。

2) 「リンクを降格した場合」の表現の統一

  • 次の2パターンの場合、これまでは単に [caption] のように「普通のリンク記法に見えるが、実際には URL なし」の出力になっていたケースがあった模様:
    • URI が allowed_uri? により拒否された場合
    • attached_links: false が指定されており、添付の URL を出力しない設定の場合
  • 今回の修正により、これらの「リンク降格」ケースはすべて \[caption\](エスケープされた角括弧)で出力されるよう統一された。

つまり、以前:

markdown
[caption]

修正後:

markdown
\[caption\]

視覚的には HTML 表示時に [caption] と見えるが、Markdown としてはリンクではないことがよりはっきりし、
「元々はリンクになり得たが、安全上 or 設定上の理由でリンクにはしていない」ということが出力フォーマット上も明示されるようになっています。

3) Action Text 内部コードの変更点(概要)

  • actiontext/lib/action_text/markdown_conversion.rb
    • markdown_link の実装を拡張し、Rails::HTML::Sanitizer.allowed_uri? によるチェックを追加。
    • リンクとして出さない場合のフォールバック表現を \[title\] に統一。
  • actiontext/lib/action_text/attachment.rb
    • 添付の Markdown 変換ロジック(to_markdown 相当)でのリンク出力パスが、この新しい markdown_link の仕様に合わせて整理されたと推測される(+8/-3)。
  • actiontext/lib/action_text/attachables/remote_image.rb
    • attachable_markdown_representation が新しい挙動(URI 検証済みの markdown_link)を使うように微修正(+1/-1)。
  • actiontext/lib/action_text/engine.rb
    • Engine 初期化の中で Markdown 関連の設定や依存の読み込み順などが若干整理・削減された(+2/-6)。
  • actiontext/test/unit/markdown_conversion_test.rb
    • 危険なスキームの検証、リンク降格の挙動、attached_links: false のケースなどをカバーするテストが大幅に追加(+48/-10)。

※実際のメソッドシグネチャや細かいコードは PR 本文には載っていませんが、変更内容から上記のような構造になっていると考えられます。

4) 「防御の多層化 (defense-in-depth)」

  • ActiveStorage::Blob のパスでは、もともと url_for(self) によりアプリケーション内の安全な URL(ホストやパスがアプリ管理下)だけが生成されるようになっている。
  • それでも markdown_link 自体が allowed_uri? を持つことで、
    • 他の呼び出し元(将来的な利用拡張も含む)
    • アプリ開発者が独自に MarkdownConversion を使うケース などでも安全性が担保されるよう、より堅牢な設計にしている。

  1. 影響範囲・注意点

影響範囲

  1. Action Text で Markdown 出力を使っている箇所全般
  • Rich Text → Markdown の変換をライブラリや独自機能で利用している場合、
    危険なスキームを持つ URL が以前より厳しくフィルタされるようになります。
  1. 「リンク降格」の出力フォーマットが変わる
  • 以前:
    • 拒否された URL や、attached_links: false のときに [caption] のような Markdown リンクに見えるテキストが出力されていた。
  • 変更後:
    • \[caption\](エスケープされた角括弧)に統一。
  • これに依存してパースしている処理(独自の Markdown パーサ、正規表現マッチングなど)がある場合、
    動作が変わる可能性があります。

注意点 / マイグレーション上の考慮

  • Markdown 生データを後処理している場合:

    • \[.*\] 形式の文字列が増える可能性があるため、正規表現などを利用している場合は記述の見直しが必要かもしれません。
  • リンクが「突然消えた」ように見えるケース:

    • 実際には危険スキームのために正しくブロックされた結果です。
    • もしアプリ内で独自スキーム(例: myapp://user/123)などを使っている場合、それが Rails::HTML::Sanitizer.allowed_uri? によって拒否されている可能性があります。
      • その場合は、Rails::HTML::Sanitizer の設定を拡張してそのスキームを許可する必要があります(ただしセキュリティ影響をよく検討すること)。
  • 既存テキストの再変換:

    • すでに保存されている Action Text コンテンツを再度 Markdown 変換する処理(例: 一括エクスポート)を行うと、
      過去に作られたリンクの一部が \[title\] に変わる可能性があります。
    • トラストしていないコンテンツ(ユーザ投稿など)については望ましい挙動ですが、
      管理者のみが入力する安全なコンテンツを Markdown としてそのまま再利用しているケースでは違和感が出るかもしれません。

  1. 参考情報 (あれば)
  • この PR で利用されている Rails::HTML::Sanitizer.allowed_uri? は、Rails の HTML サニタイズ機能の一部で、
    http, https, mailto などの「安全とみなされるスキーム」だけを許可します。
  • HTML パイプライン (SafeListSanitizer) と Markdown パイプライン (MarkdownConversion) の両方で同じポリシーに基づいて URI をフィルタすることで、
    • XSS などの攻撃経路を Markdown 経由でも塞ぐ
    • 異なる出力フォーマット間での挙動を揃える という目的を達成しています。
  • Action Text でカスタムアタッチャブルや Markdown エクスポート機能を実装している場合、この PR による Markdown 出力の変化(リンク降格時の \[...\] など)を前提にロジックを見直す価値があります。

#56970 Fix parsing SQLite virtual tables without parenthesis

マージ日: 2026/3/12 | 作成者: @nicolasva

  1. 概要 (1-2文で)
    SQLite の仮想テーブル定義をパースするロジックが、USING 句に括弧がないケースを正しく扱えていなかった問題を修正した PR です。これにより、CREATE VIRTUAL TABLE ... USING module arg1, arg2 のような定義でも、Active Record が正しくメタ情報を取得できるようになります。

  1. 変更内容の詳細

何が問題だったか

ActiveRecord の SQLite3 アダプタは、schema 情報から仮想テーブルを認識・解析する際に、CREATE VIRTUAL TABLE の SQL を文字列パースしています。
以前の実装は、USING 句に「括弧付き」の形式だけを想定していた可能性があります:

sql
CREATE VIRTUAL TABLE posts USING fts5(title, body);

しかし SQLite では、括弧なしの以下のような構文も許可されています:

sql
CREATE VIRTUAL TABLE posts USING fts5 title, body;

この「括弧なし」のパターンをアダプタが正しく解釈できず、仮想テーブルのメタ情報取得(カラム情報など)が失敗・誤動作していました (#56969)。

実際のコード変更(推定的なイメージ)

変更は 1 行のみで、sqlite3_adapter.rb の仮想テーブル定義を解析する正規表現または split ロジックが修正されています。
イメージとしては以下のような変更が入ったと考えられます(実際のコードは PR を参照してください):

ruby
# 例: 以前は括弧が必須だった
definition.match(/CREATE VIRTUAL TABLE .* USING (\w+)\((.*)\)/)

# 例: 括弧があってもなくてもマッチするようにする
definition.match(/CREATE VIRTUAL TABLE .* USING (\w+)\s*(?:\((.*)\)|(.*))/)

もしくは、USING の後ろの文字列を「括弧付きなら中身を取り出し、括弧がなければそのまま引数列として扱う」ような分岐処理に変更された可能性が高いです。

テストの追加

activerecord/test/cases/adapters/sqlite3/virtual_table_test.rb に 9 行のテストが追加され、以下のようなケースがカバーされています:

  • 括弧なしで USING を記述した仮想テーブルを作成
  • そのテーブルに対して、columns などのメタ情報取得が期待通り動作するかを検証

例(イメージ):

ruby
def test_virtual_table_without_parenthesis
  @connection.execute <<-SQL
    CREATE VIRTUAL TABLE posts USING fts5 title, body;
  SQL

  columns = @connection.columns("posts").map(&:name)
  assert_includes columns, "title"
  assert_includes columns, "body"
end

  1. 影響範囲・注意点
  • 対象: SQLite3 アダプタを使い、かつ SQLite の仮想テーブル(特に FTS など)を利用している Rails アプリケーション。
  • 具体的な影響:
    • これまで CREATE VIRTUAL TABLE ... USING module arg1, arg2 のように括弧なしで定義していた場合、schema_dump, db:schema:dump, db:structure:dump などで不正確な情報が出ていた、あるいはエラーになっていた場合に改善が見込まれます。
    • schema 情報を元にしたメタプログラミング(カラム一覧取得など)が、括弧なし構文でも安定して動作するようになります。
  • 互換性:
    • 括弧ありの構文の挙動は変えない形での修正なので、既存の仮想テーブル定義(一般的な USING fts5(...) 形式)に対して後方互換性は維持されます。
    • 変更はパース処理の一部のみで、クエリ実行や接続管理など他の機能には影響しません。

  1. 参考情報 (あれば)

#56963 Restore previous instrumenter after execute_or_skip

マージ日: 2026/3/12 | 作成者: @rosa

  1. 概要 (1-2文で)
    async_load / async_pick などの非同期クエリ実行時に、ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] が元に戻らず、「そのスレッドで以後発生する sql.active_record 通知がロギングされなくなる」不具合を修正したPRです。特に async_query_executor = :global_thread_pool + fallback_policy = :caller_runs という構成で、スレッドプール飽和時にメインスレッドが汚染される問題を防ぎます。

  1. 変更内容の詳細

問題の発生条件・原因

  • 前提:

    • Railsの非同期クエリ (async_load, async_pick) は QueryIntent#execute_or_skip を経由して実行される。
    • 非同期クエリでは、クエリ中に発生する sql.active_record 通知を一度 EventBuffer に貯め、最後に flush してまとめて発火する、という形でインストルメンテーションしている。
    • そのために ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] に一時的に EventBuffer をセットしている。
  • ところが execute_or_skip は、

    • IsolatedExecutionState[:active_record_instrumenter] の「以前の値」を保存せず、
    • 実行後に「元のインストルメンター」に戻していなかった。
  • 通常想定されている動作:

    • 非同期クエリはグローバルスレッドプール上の「背景スレッド」で実行される。
    • そのスレッド上では IsolatedExecutionState[:active_record_instrumenter] はもともと nil であることが多く、EventBuffer をセットしてもスレッドのライフタイム的にはあまり問題になりにくい。
  • しかし、async_query_executor = :global_thread_pool かつ fallback_policy = :caller_runs の場合:

    • スレッドプール/キューが飽和すると、バックグラウンド実行ではなく「呼び出し元スレッド(多くはwebリクエスト処理スレッド)」で非同期タスクが実行される。
    • その際にも execute_or_skipIsolatedExecutionState[:active_record_instrumenter]EventBuffer を上書きするが、「元の値を復元しない」ため、そのリクエストスレッドのインストルメンターが永続的に EventBuffer のままになる。
    • EventBuffer への flush は非同期クエリ完了時に1回だけ呼ばれるが、その後もインストルメンターが EventBuffer のままなため、
      • 以後のすべての sql.active_record 通知が EventBuffer に飲み込まれたまま publish されない。
      • ActiveSupport::Notifications.subscribe("sql.active_record") しているログ・計測系がすべて無効化された状態になる。
    • さらにこの状態は、プロセス内でそのスレッドが生きている限り続くため、「時間の経過とともに汚染されたスレッドが増える」→「計測されないリクエストが時間とともに増加する」という挙動になる。
  • 実際に報告されている現象:

    • デプロイ直後は問題のあるリクエストはゼロ。
    • 時間が経つにつれて、インストルメンテーションされない(SQLログが出ない等)リクエストが徐々に増える。
    • 再デプロイするとプロセス・スレッドが再生成されるため、再びゼロに戻る。

修正内容

activerecord/lib/active_record/connection_adapters/query_intent.rbexecute_or_skip 周辺に、以下の修正が入っています(概念的な擬似コードで表現):

ruby
def execute_or_skip
  # 追加: 現在のインストルメンターを退避
  previous_instrumenter = ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]

  # 非同期クエリ用の EventBuffer をセット
  ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] =
    ActiveRecord::EventBuffer.new

  begin
    # ここで実際にクエリ実行 / スケジューリング
    # ...
  ensure
    # 追加: 終了時に元のインストルメンターを必ず復元
    ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] =
      previous_instrumenter
  end
end

※ 実際のコードでは行追加は +2 だけなので、上記はイメージです。実際には EventBuffer のセットと実行ブロックの前後に previous_instrumenter の保存・復元を2行程度加えている形です。

ポイント:

  • EventBuffer をセットする前に、既存のインストルメンターを保存」
  • EventBuffer を使い終わったら、必ず(例外の有無にかかわらず)保存しておいた元のインストルメンターを復元」

背景スレッドの場合

  • 多くの場合 previous_instrumenternil なので、
    • 保存: nil を保存
    • 復元: nil に戻す
      という no-op に近い挙動で、実質的な挙動は従来と変わりません。

caller_runs(呼び出し元スレッド)で実行された場合

  • previous_instrumenter には、「リクエスト処理用の本来のインストルメンター(通常の ActiveSupport::Notifications 連携等)」が入っている。
  • 一時的に EventBuffer に切り替えて非同期クエリ相当分を実行した後、最後に「もともとのインストルメンター」をきちんと戻すことで、
    • 以後の sql.active_record 通知は元通り publish される。
    • EventBuffer による「スレッドの汚染」が発生しない。

テスト追加

activerecord/test/cases/relation/load_async_test.rb に33行のテストコードが追加されています。

意図としては主に以下を検証していると考えられます(ファイル名・PRの説明からの推測を含む):

  • async_load / async_pick 実行中に EventBuffer が使われるにも関わらず、実行後はインストルメンターが元に戻っていること。
  • 特に「同じスレッドで複数回 async_load したり、非同期クエリの後に通常の同期クエリを実行したりしても、sql.active_record 通知が正しく購読者に届く」こと。

  1. 影響範囲・注意点

影響範囲

  • 対象となるのは主に以下の条件を満たすアプリケーションです:

    • Active Record の非同期クエリ (async_load, async_pick) を使用している。
    • config.active_record.async_query_executor = :global_thread_pool を使っている、または同様のスレッドプール構成を使っている。
    • スレッドプールが飽和して fallback_policy = :caller_runs 経由で呼び出し元スレッドがタスクを実行する可能性がある(global_thread_pool はデフォルトでこれ)。
  • 測定・ログ側の影響:

    • これまで「時間の経過とともに一部のリクエストだけ SQL ログ/計測が抜ける」ような謎の現象が出ていた場合、この修正で解消される可能性が高いです。
    • 逆に言うと、一部のログが silently 失われていたのが「ようやく本来あるべき状態で出るようになる」ので、モニタリング上は一時的に「SQLが増えた」「計測値が変わった」ように見えるかもしれません。

パフォーマンス・互換性

  • execute_or_skip での追加処理は
    • IsolatedExecutionState からの読み取り1回
    • 書き戻し1回 だけで、非常に軽量です。
  • 挙動は「インストルメンターを正しくスコープ内だけで切り替える」という素直な変更であり、互換性を壊すようなものではありません。
  • 既存の ActiveSupport::Notifications / sql.active_record サブスクライバのインターフェースには変更なし。

注意点 / 実運用で見るべきポイント

  • もしアプリ側で独自に ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] を直接いじっているような高度なメタプログラミングをしていると、この挙動変更と噛み合わない可能性がありますが、通常のアプリではまず該当しません。
  • 非同期クエリを多用し、かつ global_thread_pool が頻繁に飽和している場合:
    • 本PRは「インストルメンターの汚染」を防ぐものであって、「プール飽和そのもの」を解決するものではありません。
    • 必要に応じて
      • スレッドプールの concurrencymax_queue の調整
      • 非同期クエリの数/タイミングの見直し
      • 接続プールのサイズ調整 などのチューニングは依然として必要になります。

  1. 参考情報 (あれば)

#56964 Restore previous instrumenter after execute_or_skip

マージ日: 2026/3/12 | 作成者: @rosa

  1. 概要 (1-2文で)
    ActiveRecord::FutureResult#execute_or_skip 実行時に ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] を一時的に EventBuffer に差し替えたあと、元のインストゥルメンタを正しく復元するようにしたバグ修正です。これにより、スレッドプール飽和時にリクエストスレッド上でタスクが実行された場合でも、sql.active_record 通知が失われずに済むようになります。

  1. 変更内容の詳細

問題の背景

  • FutureResult#execute_or_skip の中で、SQL通知をバッファリングするために
    ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] = EventBuffer.new
    のようにインストゥルメンタを差し替えている。
  • 本来これは「バックグラウンドスレッド上で実行される」ことを前提としており、そのスレッドのライフサイクル内だけで完結する想定。
  • しかしグローバル非同期実行器(global async executor)が fallback_policy: :caller_runs を使っているため、スレッドプールが飽和すると、タスクが呼び出し元スレッド(=場合によってはWebリクエストスレッド)上で実行される
  • その場合、リクエストスレッドの IsolatedExecutionState[:active_record_instrumenter]EventBuffer に書き換えられたまま元に戻らない。
  • Rails はリクエスト毎にこのキーをクリアしていないため、そのスレッドで今後発生するすべての sql.active_record 通知が EventBuffer に飲み込まれ、flush もされず、サブスクライバに届かなくなるという永続的な汚染状態が起きる。

対応内容

FutureResult#execute_or_skip 内で、インストゥルメンタの元の値を保存し、処理終了後に必ず復元するように修正されています。

擬似コードで表すと、以下のような対応です(実際のコードはこれに近い構造):

ruby
def execute_or_skip
  previous_instrumenter =
    ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]

  ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] =
    ActiveRecord::EventBuffer.new

  begin
    # 非同期実行しつつ結果を待つ / スキップする
    ...
  ensure
    ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] =
      previous_instrumenter
  end
end
  • バックグラウンドスレッドで実行される場合
    • 元の値は nil であることが多く、nilEventBuffernil に戻すだけの no-op に近い挙動。
  • リクエストスレッド上で caller_runs により実行された場合
    • もともと入っていた「本物のインストゥルメンタ」を previous_instrumenter として保存し、ensure で確実に復元。
    • これにより、後続の SQL 実行は再び通常どおり sql.active_record 通知を発行し、サブスクライバ(ログ、APM、メトリクスなど)にも届く。

テストの追加

activerecord/test/cases/relation/load_async_test.rb にテストが追加されており、主に以下を確認しています:

  • load_async 実行後もインストゥルメンタが元に戻っていること。
  • 特に、caller_runs パスでリクエストスレッドが汚染されないことを検証する形になっている(PR の意図からするとそのようなケースをカバーしている)。

CHANGELOG (activerecord/CHANGELOG.md) には、8.0 向けのバグ修正としてこの挙動変更が追記されています。


  1. 影響範囲・注意点
  • 影響を受けるのは:
    • ActiveRecord::Relation#load_async など、FutureResult ベースの非同期ロード機構を利用しているコード
    • かつ、グローバル async executor のスレッドプールが飽和し、fallback_policy: :caller_runs によってリクエストスレッド上で実行されるケース
  • このバグにより発生し得た症状:
    • 一部のリクエストスレッドで sql.active_record 通知が一切来なくなる
    • ログ、ActiveSupport::Notifications ベースのメトリクス、APM/トレーシングなどが、そのスレッドでの SQL を観測できなくなる
    • 原因がわかりづらく、再現性もプールの状態依存で不安定
  • この修正による既存アプリへの影響:
    • 本来の正しい挙動への修正であり、互換性破壊ではなくバグフィックス。
    • これまで「たまたま通知が止まっていた」ケースで、通知が正常に飛ぶようになるため、メトリクスやログが増えたように見える可能性はある。
  • アプリケーション側で特別な対応は基本的に不要ですが、
    • load_async や custom な非同期クエリ実行を多用しており、
    • かつ「一部リクエストでだけ SQL ログ/メトリクスが出ない」といった謎の現象に遭遇していた場合、 この修正が問題を解決している可能性があります。

  1. 参考情報 (あれば)
  • 元 PR(main 向け): https://github.com/rails/rails/pull/56963
  • 本 PR はその Rails 8.0 向けバックポート。
  • 関連するコンポーネント:
    • ActiveRecord::FutureResult
    • ActiveSupport::IsolatedExecutionState
    • ActiveRecord の async loading (load_async)
    • ActiveSupport::Notifications / sql.active_record イベント

#56965 Restore previous instrumenter after execute_or_skip

マージ日: 2026/3/12 | 作成者: @rosa

  1. 概要 (1-2文で)
    ActiveRecord::FutureResult#execute_or_skip 実行時に ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] を一時的に EventBuffer に差し替えたあと、元のインストルメンターを復元していなかったバグを修正する PR です。スレッドプール飽和時に caller_runs でリクエストスレッド上で async ロードが実行されると、以降そのスレッドで発火する sql.active_record 通知が二度と購読者に届かなくなる問題を防ぎます。

  1. 変更内容の詳細

問題の背景

  • ActiveRecord::FutureResult#execute_or_skip は、非同期ロード結果を「実行 or スキップ」する処理で、内部でクエリ実行時の計測(instrumentation)をバッファリングするために EventBuffer を使っています。
  • その際に、スレッドローカルな ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]EventBuffer オブジェクトにセットしていましたが、処理後に元の値へ戻していませんでした。

通常想定:

  • 非同期実行はバックグラウンドスレッド(スレッドプール内)で行われる想定で、そのスレッドでは :active_record_instrumenternil のことが多く、「nilEventBuffer に変えて、そのまま EventBuffer のまま」になっていても実害がほぼない、という前提でした。

しかし実際には:

  • Rails のグローバル async executor が fallback_policy: :caller_runs を使っているため、スレッドプールが飽和すると、タスクが呼び出し元スレッド(しばしばリクエスト処理中のアプリケーションスレッド)上で実行されます。
  • リクエストスレッド上では、すでに「本来の」instrumenter(sql.active_record 通知を購読者に流すためのオブジェクト)が IsolatedExecutionState に入っています。
  • そこを EventBuffer で上書きし、かつ戻さないため、そのスレッドのライフタイム中ずっと :active_record_instrumenterEventBuffer のままになってしまいます。

その結果:

  • 以降そのスレッドで実行されるクエリの sql.active_record イベントは、「EventBuffer には貯まるが、flush は FutureResult の完了時に一度だけ呼ばれる」構造になっているため、FutureResult 実行以降のイベントは永遠に flush されず、購読者に届かない状態になります。
  • Rails はリクエスト間で ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] をクリアしていないため、この「汚染」はスレッドが破棄されるまで永続します。

修正内容

PR の主な修正は次の一点です:

  • FutureResult#execute_or_skip 内で EventBuffer:active_record_instrumenter にセットする前に、現在の値を退避し、EventBuffer の利用が終わったら必ず元のインストルメンターに戻すようにしました。

擬似コードイメージ:

ruby
def execute_or_skip
  previous_instrumenter = ActiveSupport::IsolatedExecutionState[:active_record_instrumenter]
  ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] = event_buffer

  begin
    # FutureResult の実行/スキップ処理
  ensure
    # 必ず元に戻す
    ActiveSupport::IsolatedExecutionState[:active_record_instrumenter] = previous_instrumenter
  end
end

これにより:

  • バックグラウンドスレッド(instrumenter が nil)では「nil を保存して nil に戻す」だけなので実質ノーオペ。
  • caller_runs でリクエストスレッド上で実行された場合でも、
    • 一時的に EventBuffer に差し替えてイベントをバッファ
    • 結果の flush 後、元のインストルメンターに確実に復元 という挙動になり、以降の sql.active_record 通知が正常に流れ続けます。

テストとドキュメント

  • activerecord/test/cases/relation/load_async_test.rb にテストが追加され、caller_runs シナリオ(呼び出し側スレッドで async ロードが実行されるケース)でも instrumenter が汚染されないことを確認しています。
  • activerecord/CHANGELOG.md に、この修正内容が 8.1 系向けのバグフィックスとして追記されています。
  • この PR は main (または 8.2 以降) での修正 PR #56963 の 8.1 バックポートです。

  1. 影響範囲・注意点
  • 影響を受けるのは、load_async など Active Record の非同期ロード機能と計測・監視機構(ActiveSupport::Notifications、ログ、APM 連携など)を併用しているプロジェクトです。
  • 特に以下の条件を満たす環境で、これまで「一部のリクエストで sql.active_record 通知が突然出なくなる」・「クエリログ/メトリクスが欠損する」といった現象が起きていた可能性があります:
    • Active Record の async クエリを利用している (load_async など)
    • グローバル async executor が fallback_policy: :caller_runs で動いている(Rails デフォルト)
    • スレッドプールが飽和しやすい(高負荷環境など)
  • この修正により、既存コード側で対応する必要は基本的にありませんが、
    • 以前、上記のような「時々発火しなくなる sql.active_record 通知」を感じて独自ワークアラウンドを入れている場合は、その挙動が変わる可能性があります。
  • パフォーマンスへの影響は極小です:
    • 追加された処理は、「スレッドローカルの値を一時退避・復元する」だけで、非同期クエリ実行時のみ実行されます。

  1. 参考情報 (あれば)
  • 元 PR(本流ブランチ向け): https://github.com/rails/rails/pull/56963
  • 本 PR(8.1 向けバックポート): https://github.com/rails/rails/pull/56965
  • 関連箇所:
    • ActiveRecord::FutureResult(非同期クエリ実行の結果ラッパ)
    • ActiveSupport::IsolatedExecutionState(スレッドローカルな Execution Context)
    • sql.active_record 通知(クエリごとの instrumentation イベント)

#56958 Remove create tests from passwords_controller_test.rb if ActionMailer::Railtie is not defined

マージ日: 2026/3/11 | 作成者: @ssjr

  1. 概要 (1-2文で)
    Rails アプリを --skip-action-mailer オプション付きで作成した場合に、rails g authentication が生成するパスワード関連テストで Action Mailer 依存のテストが落ちていた問題を修正する PR です。ActionMailer::Railtie が未定義のときは、PasswordsController#create に関するメール送信テストを生成しないようにしました。

  1. 変更内容の詳細

背景・問題点

再現手順の通り、

bash
rails new my-test-app --skip-action-mailer
cd my-test-app
sed -i 's/# root/root/g' config/routes.rb
bin/rails g authentication
bin/rails db:migrate
bin/rake test

とすると、以下のようなエラーが発生していました。

  • NameError: uninitialized constant PasswordsControllerTest::PasswordsMailer
    • PasswordsMailer 自体が存在しない(Action Mailer をスキップしているため)
  • NoMethodError: undefined method 'assert_enqueued_emails'
    • Action Mailer のテストヘルパがロードされていない

原因は、Action Mailer をスキップしても authentication generator がメール送信前提のテスト (create アクション用テスト) を生成していたことです。

実際の変更点

対象ファイル:

  • railties/lib/rails/generators/test_unit/authentication/templates/test/controllers/passwords_controller_test.rb.tt
  • railties/test/generators/authentication_generator_test.rb

1) テンプレート側: メール依存テストを条件付きで生成

passwords_controller_test.rb のテンプレート (.tt) に、ActionMailer::Railtie の有無でテスト生成を切り替える条件が追加されました。

イメージとしては、テンプレート内に以下のような条件分岐が入ります(あくまで概念的なサンプル):

erb
<% if defined?(ActionMailer::Railtie) %>
  test "create" do
    # ここに PasswordsMailer や assert_enqueued_emails を使ったテスト
  end

  test "create_for_an_unknown_user_redirects_but_sends_no_mail" do
    # 同上
  end
<% end %>

実際には ERB テンプレートとして、ActionMailer::Railtie が存在する環境だけで create 関連テストを展開するようになっています。
これにより、--skip-action-mailer でアプリを生成した場合:

  • PasswordsControllerTestcreate アクションのメール送信テストが含まれなくなる
  • ゆえに PasswordsMailer の未定義エラーや assert_enqueued_emails の未定義エラーが発生しない

2) Generator のテスト追加

railties/test/generators/authentication_generator_test.rb にテストが追加され、

  • --skip-action-mailer 付きでアプリを生成
  • rails g authentication 実行
  • 生成された passwords_controller_test.rb を検査し、
    • メール依存テストが生成されていないこと

を確認するアサーションが増えています。
このテストにより、今後の変更で再び Action Mailer 非依存環境にメールテストを生成してしまう退行を防ぎます。


  1. 影響範囲・注意点
  • 影響対象:
    • TestUnit + authentication generator を使うプロジェクト
    • かつ rails new ... --skip-action-mailer で Action Mailer を明示的に無効化しているケース
  • メリット:
    • 上記条件下で bin/rails test / bin/rake test が素の状態でグリーンになる
    • 不要な PasswordsMailer の作成や Action Mailer の設定を強制されなくなる
  • 注意点:
    • Action Mailer をスキップしている場合、当然ながら パスワードリセットメールの送信機能自体は提供されない ため、必要な場合は:
      • --skip-action-mailer を使わない
      • もしくは自前でメール機構とテストを用意する
    • 既に生成済みのアプリでは、この PR の前に生成した passwords_controller_test.rb には依然としてメールテストが残っている可能性があるため、
      • 新規に generator を再実行するか
      • 既存テストから create 関連のメールテストを削除する必要があります。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/56958
  • 関連機能:
    • Authentication generator (bin/rails g authentication)
    • --skip-action-mailer オプションで Action Mailer を無効化した Rails アプリの生成
  • 類似の設計パターン:
    • --skip-active-record など、Railtie をスキップした際には、その Railtie に依存するコード・テストを generator 側で条件付き生成する、という Rails の一般的な方針に沿った変更です。

#56956 Simplify applicable actionpack tests with NotificationAssertions helpers

マージ日: 2026/3/10 | 作成者: @larouxn

  1. 概要 (1-2文で)
    actionpackredirect_test.rb 内で ActiveSupport::Notifications を直接扱っていたテストを、ActiveSupport::Testing::NotificationAssertions ヘルパーを使う形に書き換え、テストコードを簡潔かつ安全にした PR です。通知の購読・解除やアサーションの記述がヘルパーに集約され、重複コードや ensure ブロックが削減されています。

  1. 変更内容の詳細

何をやったか

  • 対象ファイル: actionpack/test/controller/redirect_test.rb
  • 5つのテストケースが、手書きの ActiveSupport::Notifications.subscribe/unsubscribe ベースの実装から、NotificationAssertions モジュールのヘルパーメソッドを使う形に置き換えられています。
  • 行数ベースでは +19 / -45 と、テストコードがかなりスリムになっています。

具体的なリファクタリング内容

元のテストでは、例えば以下のようなパターンで通知をテストしていたと考えられます(イメージ):

ruby
def test_redirect_triggers_notification
  events = []

  callback = ->(*args) do
    events << ActiveSupport::Notifications::Event.new(*args)
  end

  ActiveSupport::Notifications.subscribe("redirect.action_controller", &callback)

  begin
    get :some_action
  ensure
    ActiveSupport::Notifications.unsubscribe(callback)
  end

  assert_equal 1, events.size
  assert_equal "/new_path", events.first.payload[:location]
end

これが NotificationAssertions を使うと、次のように簡略化されます(イメージ):

ruby
def test_redirect_triggers_notification
  events = capture_notifications("redirect.action_controller") do
    get :some_action
  end

  assert_equal 1, events.size
  assert_equal "/new_path", events.first.payload[:location]
end

もしくは、よりダイレクトにアサートするケースでは assert_notifications / assert_notification 系のヘルパーが使われているはずです:

ruby
assert_notification "redirect.action_controller" do |event|
  get :some_action
  # ブロックが終わるときに、"redirect.action_controller" が1回発火していること等を検証
end

NotificationAssertions の主なポイントは:

  • テスト毎に subscribe / unsubscribe を自動で行い、ensure ブロックが不要になる。
  • 通知の収集 (capture_notifications) と、通知の存在/回数などのアサーション (assert_notifications 等) を 統一されたインターフェースで扱える。
  • ActiveSupport::Notifications の生 API に直接触れるコードが減り、テスト意図(どのイベントがどう発生するべきか)が読み取りやすくなる

この PR では、それらのヘルパーを redirect_test.rb 内の「リダイレクト時にどの通知がいつ発火するか」を検証するテストに適用し、重複・儀式的コードを削除しています。


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

    • 変更はテストコード (actionpack/test/controller/redirect_test.rb) のみであり、実行時のアプリケーションコード (actionpack の本体) には一切手が入っていません。
    • したがって、Rails を利用するアプリケーションや、actionpack の挙動そのものには 機能的な変更や互換性の影響はありません
    • CI 上の actionpack テストの一部が NotificationAssertions に依存するようになりましたが、これはすでに Rails 本体に追加済みのテスト用モジュールです。
  • 注意点 / 開発者視点での示唆

    • 今後 ActiveSupport::Notifications を用いたテストを書く際は、素手で subscribe/unsubscribe せずに NotificationAssertions を使うのが推奨される流れになります。
    • 特に ensure でのクリーンアップ漏れや、複数テスト間での購読状態の汚染など、テストの不安定要因を減らせるため、同様のパターンのテストは追随してリファクタリングするとよいです。
    • もし独自のテストヘルパーやカスタム通知ロギングを持っている場合は、NotificationAssertions に置き換えられるかどうかを検討する価値があります。

  1. 参考情報 (あれば)

#56932 Fix dbconsole NotImplementedError message

マージ日: 2026/3/9 | 作成者: @eglitobias

  1. 概要 (1-2文で)
    ActiveRecord の抽象アダプタ AbstractAdapter.dbconsole が投げる NotImplementedError のエラーメッセージを修正し、「Class should define …」という曖昧な文言を、実際のアダプタクラス名が出るように改善した PR です。機能追加や挙動変更ではなく、開発者向けのエラーメッセージの改善のみが行われています。

  1. 変更内容の詳細

対象:
activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

ActiveRecord::ConnectionAdapters::AbstractAdapter のクラスメソッド dbconsole 内で、まだ実装していないアダプタがこのメソッドを呼び出した際に NotImplementedError を投げる部分があります。

元々 PR #54719 によって、クラスメソッド内での文言生成に self.class が使われるようになっていましたが、クラスメソッドでは self は「クラス」そのものを指す一方で、self.class は「クラスのクラス(= Class)」になってしまいます。その結果、例外メッセージに意図しない「Class」という文字列が表示されていました。

この PR では、その部分を self.class から self に戻すことで、正しくアダプタクラス名が表示されるようにしています。

イメージとしては、以下のような実装になります(実際のコードのイメージ):

ruby
def self.dbconsole(...)
  raise NotImplementedError, "#{self} should define `dbconsole` ..."
end

クラスメソッド内なので self は例えば ActiveRecord::ConnectionAdapters::AbstractAdapter 等になり、そのまま文字列展開されます。

動作例

修正前

ruby
ActiveRecord::ConnectionAdapters::AbstractAdapter.dbconsole(nil)
# => NotImplementedError: Class should define `dbconsole` ...

ここで self.classClass になるため、「Class should define …」という分かりにくいメッセージが出ていました。

修正後

ruby
ActiveRecord::ConnectionAdapters::AbstractAdapter.dbconsole(nil)
# => NotImplementedError: ActiveRecord::ConnectionAdapters::AbstractAdapter should define `dbconsole` ...

どのクラスが dbconsole を実装すべきかが明確に分かるメッセージになりました。


  1. 影響範囲・注意点
  • 影響範囲は エラー文言のみ で、実際の dbconsole の挙動や API 仕様は一切変わりません。
  • AbstractAdapter.dbconsole を直接呼び出すことは通常あまりないですが、独自アダプタを実装している場合や、ActiveRecord の接続アダプタ周りを拡張している場合には、NotImplementedError を読む機会があります。その際に、どのクラスで実装が必要なのかがより分かりやすくなります。
  • 既存テストやアプリケーションコード側で、例外メッセージ文字列を 厳密一致でテストしている場合(例: "Class should define dbconsole ..." を含むかどうかを見ているテスト)は、文言変更によりテストが落ちる可能性があります。このようなテストは "should define \dbconsole`"` のような部分一致に変更するのがよいです。

  1. 参考情報 (あれば)
  • 関連 PR:
    • #54719: AbstractAdapter.dbconsole 周りの変更を行い、今回の問題の原因になった PR
  • 関連クラス・メソッド:
    • ActiveRecord::ConnectionAdapters::AbstractAdapter.dbconsole
  • Ruby の文法的ポイント:
    • クラスメソッド内では self は「クラス」そのものを指す (AbstractAdapter)、
      self.class はそのクラスのクラスである Class を指すため、文字列展開に使うと "Class" になってしまう。

#56941 Remove foreign_key coercision in counter_cache

マージ日: 2026/3/9 | 作成者: @skipkayhil

  1. 概要 (1-2文で)
    counter_cache の内部実装から、Reflection#foreign_key に対する to_s / to_sym による型変換(強制変換)が削除されました。
    foreign_key は常に凍結済みの文字列またはその配列であることが前提とされ、その前提に合わせてコードが簡潔化されています。

  1. 変更内容の詳細

背景

Active Record の関連(has_many / belongs_to など)を表す Reflection オブジェクトは、foreign_key メソッドで外部キー名を返します。この PR では:

  • Reflection#foreign_key は常に以下のいずれかである
    • 凍結された interned String("user_id".freeze のようなもの)
    • そのような String の凍結済み配列(複合キーなどを想定)
  • したがって foreign_key.to_sforeign_key.to_sym といった「毎回の型変換」は不要

という前提を明示し、それに伴って counter_cache 周りのコードから変換処理を削っています。

実際の変更イメージ

activerecord/lib/active_record/counter_cache.rb で、概ね次のような変更が行われたと考えられます(擬似コード):

ruby
# 変更前(イメージ)
def foreign_key_name(reflection)
  reflection.foreign_key.to_s
end

def some_counter_cache_logic(reflection)
  fk = reflection.foreign_key.to_sym
  # ...
end

# 変更後(イメージ)
def foreign_key_name(reflection)
  reflection.foreign_key
end

def some_counter_cache_logic(reflection)
  fk = reflection.foreign_key
  # ...
end

ポイント:

  • to_s / to_sym の呼び出しが消え、reflection.foreign_key をそのまま使うように整理。
  • 配列の場合も「配列の中身はすでに正しい型・状態である」という前提で扱う。

行数レベルでは:

  • 追加 3 行 / 削除 7 行
  • 主に不要な型変換ロジックの削除によるコードの簡素化。

  1. 影響範囲・注意点

影響範囲

  • ActiveRecord の counter_cache 機能内部に限定される変更であり、アプリケーションコードから直接呼ぶ API のシグネチャは変わっていません。
  • Reflection#foreign_key の戻り値契約(常に凍結済み String か、凍結済み String の配列)が前提になるため、
    • Rails の内部実装同士の整合性が高まり、
    • 無駄なオブジェクト生成や文字列変換が減ることで、非常に小さいながらもパフォーマンス・メモリ効率の改善が期待できます。

注意点

通常のアプリ開発者:

  • belongs_to :post, counter_cache: true のような使い方には影響ありません。
  • マイグレーションやスキーマ定義を変える必要もありません。

Rails拡張やメタプログラミングをしている場合:

  • ActiveRecord::Reflection を自前でモンキーパッチしており、foreign_key が「常に String / Array<String> を返す」契約を破っている場合、
    • これまでは to_s / to_sym によって“なんとなく動いていた”ケースが、動かなくなる可能性があります。
  • 例えば、foreign_keynil やシンボルにしたり、独自オブジェクトを返したりしている場合は要確認です。
    • 現行 Rails の他の箇所でも foreign_key が文字列前提で利用されているため、この PR に限らず非互換な実装です。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/56941
  • 関連クラス:
    • ActiveRecord::Reflection::AssociationReflection#foreign_key
    • ActiveRecord::CounterCacheactiverecord/lib/active_record/counter_cache.rb
  • 背景知識:
    • Rails ではパフォーマンス・メモリ効率向上のため、頻用される文字列を「interned string」(String#freeze 等)で扱う方針があり、その一環としてのリファクタリングと考えられます。

#56951 Fix default value of raise_on_missing_required_finder_order_columns configuration.

マージ日: 2026/3/9 | 作成者: @r-plus

  1. 概要 (1-2文で)
    ActiveRecord.raise_on_missing_required_finder_order_columns のデフォルト値が、ガイドに書かれている false ではなく nil になっていた問題を修正し、明示的に false をデフォルトとする PR です。挙動としては nilfalse はどちらも falsy なので動作は変わりませんが、ドキュメントと実装を揃えるための修正です。

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

    • activerecord/lib/active_record.rb 内の raise_on_missing_required_finder_order_columns のデフォルト値を nilfalse に変更。
  • 背景

    • Rails Guides では config.active_record.raise_on_missing_required_finder_order_columns のデフォルトは false と明記されているが、load_defaults が 8.1 未満の環境では、実際には nil が設定されていた。
    • この差異は、過去の PR (#54608) の「コピペして書き換えを忘れた」ミスが原因と指摘されています。
  • 実際の現象 (修正前)

    ruby
    # load_defaults < "8.1" のアプリで
    ActiveRecord.raise_on_missing_required_finder_order_columns
    # => nil
  • 修正後の意図

    • 上記の値が常に false を返すようにし、Rails Guides の記載と整合させる。

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

    • 主に 設定値のデフォルト に関する変更であり、実際の挙動にはほぼ影響しません。
      • Ruby では nilfalse もいずれも falsy であるため、このフラグを単純に真偽値判定に使っている限り、nilfalse でコードの分岐結果は変わりません。
    • load_defaults < 8.1 で動いているアプリケーションに影響しうる変更ですが、通常の利用では実質的な動作は同じです。
  • 注意点

    • もしアプリ側で「nil かどうか」を明示的に判定しているようなコードがあれば、今回の変更で挙動が変わる可能性があります(例: if config.raise_on_missing_required_finder_order_columns.nil?)。
    • 正しい使い方としては、この設定を「真偽値のフラグ」として扱うことが想定されており、その意味では今回の修正はより一貫性のある状態に近づけるものといえます。
    • 古いバージョンからのアップグレード時に、raise_on_missing_required_finder_order_columns の値をログに出していたり、メタ的に検査しているコードがある場合は、期待値を false に合わせる必要があります。

  1. 参考情報 (あれば)

#56943 Address test_validates_comparison_of_incomparables failure with Ruby 4.1.0dev

マージ日: 2026/3/7 | 作成者: @yahonda

  1. 概要 (1-2文で)
    このPRは、Ruby 4.1.0dev で validates_comparison_of_incomparables のテストが落ちる問題に対応し、Ruby 本体側でエラー文言が変わったことを反映するためにテスト期待値を調整したものです。Rails 本体の挙動ではなく、比較バリデーションに関するテスト側のみの修正です。

  1. 変更内容の詳細

何が起きていたか

Ruby 4.1.0dev で以下のテストが失敗していました:

  • ComparisonValidationTest#test_validates_comparison_of_incomparables
  • ファイル: activemodel/test/cases/validations/comparison_validation_test.rb:294

比較不可能な型(IntegerString)を比較したときに発生するエラーメッセージが、Ruby 4.1.0dev で変更されたためです。

以前の Ruby(4.1.0dev 以前)では、例外メッセージは:

text
"comparison of Integer with String failed"

でしたが、Ruby 4.1.0dev では:

text
"comparison of Integer with String failed: coercion was not possible"

と末尾に : coercion was not possible が追加されるようになりました。

テストは 古い 文言 "comparison of Integer with String failed" を期待していたため、CI 上では diff が出て失敗していました。

何を変えたか

対象ファイル:

  • activemodel/test/cases/validations/comparison_validation_test.rb (+2/-2)

変更内容はこのテストケースの「期待されるエラーメッセージ」を、Ruby 4.1.0dev の新しいメッセージに合わせるというシンプルなものです。

具体的には、おそらく次のような変更が行われています(イメージ):

ruby
# 変更前(旧 Ruby 用の期待値)
assert_equal(
  "comparison of Integer with String failed",
  errors[:attribute].first
)

# 変更後(Ruby 4.1.0dev のメッセージに合わせる)
assert_equal(
  "comparison of Integer with String failed: coercion was not possible",
  errors[:attribute].first
)

実際には 2 行の追加・2 行の削除だけなので、ほぼこの期待メッセージの差し替えに限定されます。


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

    • Rails のランタイムコード(本番アプリに影響するコード)には変更がなく、Active Model のテストコードのみが対象です。
    • Ruby 4.1.0dev を用いた Rails の CI や、手元で Ruby 4.1.0dev + Rails master をテストする環境で、該当テストが正常に通るようになります。
    • Ruby のエラーメッセージ仕様変更に依存しているため、将来またメッセージが変われば再度テスト調整が必要になる可能性はあります。
  • 注意点

    • このテストは、「異種比較エラーが発生すること」 を検証するものですが、現在は エラーメッセージの完全一致 をチェックしています。そのため Ruby 側のメッセージ変更に非常に敏感です。
    • Ruby のバージョン互換性を広く持たせたい場合は、将来的に
      • メッセージの部分一致(include?)にする
      • もしくは例外クラスや挙動のみをテストする といった方針変更も検討の余地がありますが、本 PR ではそこまでは行っていません。
    • 現時点では Ruby 4.1.0dev の挙動に合わせる形で、Rails Nightly CI の安定性を優先した対応になっています。

  1. 参考情報 (あれば)

#56927 Fix stale Rack::Sendfile reference in DebugLocks documentation

マージ日: 2026/3/5 | 作成者: @ashwin47

  1. 概要 (1-2文で)
    DebugLocks ミドルウェアの挿入位置としてドキュメントに書かれていた Rack::Sendfile が、最近の変更によりミドルウェアスタックに存在しない場合があるため、常に存在する ActionDispatch::Executor を基準にするようにドキュメントとコメントを修正した PR です。機能変更ではなく、ドキュメントと inline コメントの不整合を直すメンテナンス的修正です。

  1. 変更内容の詳細

背景

  • PR #56915 により、x_sendfile_headernil(デフォルト)の場合は Rack::Sendfile をミドルウェアスタックに追加しないようになった。

  • その結果、従来ドキュメントに書かれていた以下のような設定:

    ruby
    config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocks

    を実行すると、Rack::Sendfile がそもそもスタックにないため、

    text
    No such middleware to insert before: Rack::Sendfile

    というエラーが発生するようになっていた。

修正内容

Rack::Sendfile を挿入ポイントとして推奨する記述を、ActionDispatch::Executor に変更しています。

  1. actionpack/lib/action_dispatch/middleware/debug_locks.rb の inline ドキュメント修正

    もともとのコメント例:

    ruby
    # config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocks

    を以下のように変更:

    ruby
    # config.middleware.insert_before ActionDispatch::Executor, ActionDispatch::DebugLocks

    これにより、DebugLocks を挿入する際の公式なサンプルコードが、常に存在するミドルウェアを基準にしたものになる。

  2. Guides (guides/source/threading_and_code_execution.md) の記述修正

    Threading ガイドにあるミドルウェア挿入例も同様に:

    ruby
    config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocks

    ruby
    config.middleware.insert_before ActionDispatch::Executor, ActionDispatch::DebugLocks

    に変更。

なぜ ActionDispatch::Executor なのか

  • ActionDispatch::Executor は、Rack ミドルウェアスタックの中で「無条件に最初に入る」ミドルウェアとして配置されている。
  • DebugLocks はスレッドロックの問題をデバッグするためのミドルウェアであり、「できるだけ上流(スタックの先頭付近)」に入れたいという意図がある。
  • Rack::Sendfile はもはや条件付き(x_sendfile_header 設定時のみ)になってしまったため、常に存在する ActionDispatch::Executor を基準にするのが合理的、という判断。

  1. 影響範囲・注意点
  • 対象:
    • ActionDispatch::DebugLocks を利用していて、ドキュメント通りに Rack::Sendfile を基準に insert_before しているアプリケーション。
  • 実運用上の影響:
    • この PR 自体は コードの動作は一切変えておらず、コメントとガイドの修正のみ。
    • ただし、既に Rack::Sendfile を基準に設定しているアプリでは、現状の Rails だと前述の No such middleware to insert before エラーが起きうる
  • 対応すべきこと:
    • config/application.rb や各環境設定で DebugLocks を使っている場合は、設定を以下のように変更する必要がある:

      ruby
      # 旧 (今後はエラーになる可能性が高い)
      config.middleware.insert_before Rack::Sendfile, ActionDispatch::DebugLocks
      
      # 新 (PR で推奨されている形)
      config.middleware.insert_before ActionDispatch::Executor, ActionDispatch::DebugLocks
    • x_sendfile_header を独自に設定していて Rack::Sendfile が有効な場合でも、この変更に合わせて ActionDispatch::Executor を基準にしておくと、設定が将来の変更に対しても頑健になる。


  1. 参考情報 (あれば)
  • この PR に依存する変更:
    • #56915: x_sendfile_headernil の場合に Rack::Sendfile を追加しないようにした変更。
  • 関連コンポーネント:
    • ActionDispatch::DebugLocks: スレッドロック・デッドロックの検出支援用ミドルウェア。
    • ActionDispatch::Executor: 各リクエスト毎にスレッド/コード実行コンテキストを管理するミドルウェア(スレッドセーフティやリロードに関わるコアミドルウェア)。

#56933 Fix missing comment marker in decrement_counter docs

マージ日: 2026/3/5 | 作成者: @eglitobias

  1. 概要 (1-2文で)
    ActiveRecord::CounterCachedecrement_counter のドキュメント中にあったサンプルコードのコメント行から # が抜けていた問題を修正した PR です。機能変更は一切なく、ドキュメント内のコードスニペットの体裁と一貫性を整えただけの変更です。

  2. 変更内容の詳細

対象: activerecord/lib/active_record/counter_cache.rb

decrement_counter の使用例にあるコメント付きサンプルコードのうち、1 行だけ Ruby のコメントマーカー # が抜けていたため、そこに # を追加しています。

イメージとしては、ドキュメント内の例が:

ruby
# Decrement the posts_count counter cache for the record with an id of 5
# in the authors table by one.
by a specific amount.

のように、最後の行だけコメントになっておらず、Ruby コードとしては文法的におかしい状態になっていました。
これを次のように修正しています:

ruby
# Decrement the posts_count counter cache for the record with an id of 5
# in the authors table by one.
# by a specific amount.

要するに、「by a specific amount.」という補足説明もちゃんとコメントとして扱われるようにしただけの修正です。

  1. 影響範囲・注意点
  • 実行コード (decrement_counter の挙動) には一切変更はありません。
  • 影響があるのは、counter_cache.rb 内に書かれた Yard/RDoc 用コメントや、そこから生成される API ドキュメントのみです。
  • 既存アプリケーション・Gem・テストコードなどへの影響はゼロと考えて問題ありません。
  • decrement_counter の使い方やシグネチャは変わっていないため、アップグレード時の互換性リスクもありません。
  1. 参考情報 (あれば)
  • 対象メソッド: ActiveRecord::CounterCache.decrement_counter
    一般的な利用例:
    ruby
    # 1 だけ減らす
    Author.decrement_counter(:posts_count, author_id)
    
    # 特定の値だけ減らす
    Author.decrement_counter(:posts_count, author_id, by: 3)
  • PR: https://github.com/rails/rails/pull/56933

#56911 array_position requires PostgreSQL 9.5

マージ日: 2026/3/4 | 作成者: @toy

  1. 概要 (1-2文で)
    このPRは、Active Record が内部で利用している PostgreSQL 関数 array_position が PostgreSQL 9.5 以降でしか使えないことを踏まえ、PostgreSQL アダプタのバージョンチェックとドキュメントを「9.4 まで」から「9.5 まで」に更新するものです。実質的には「Rails の PostgreSQL サポート下限が 9.5 である」ことをコードとガイドに明示した調整です。

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

背景

  • 以前のコミット(c93d1b09fcc0…, PR #55654)で、Active Record の PostgreSQL アダプタ内で array_position を使う実装が追加された。
  • array_position は PostgreSQL 9.5 で導入された関数のため、PostgreSQL 9.4 以下では存在せず、クエリ実行時にエラーになる可能性がある。
  • それにもかかわらず、Rails 側の「サポート対象 PostgreSQL バージョン」の記述やバージョンチェックの条件が古いまま(9.4 前提)になっていた部分があった。

このPRでは、それらの整合性を取るために、「最低サポートバージョンは 9.5」という前提に統一しています。

ファイルごとの変更ポイント

1) activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

  • PostgreSQL のバージョンチェックを行っている箇所で、しきい値を 9.4 → 9.5 に引き上げた変更が行われています。

  • 具体的には、内部で postgresql_version などを使って

    ruby
    if postgresql_version < 90400
      # 9.4 未満に対する警告やサポート外処理
    end

    のように書かれていたものを、

    ruby
    if postgresql_version < 90500
      # 9.5 未満に対する警告やサポート外処理
    end

    といった形に変更したイメージです(実際の定数値・条件はPRの差分そのものですが、役割としては「9.5 未満を非サポート扱いにする」変更です)。

  • これにより、Rails が PostgreSQL バージョンを判定するときに「9.4 まではOK」という扱いがなくなり、「9.5 未満はサポート外」となります。

2) guides/source/active_record_postgresql.md

  • Active Record と PostgreSQL の組み合わせについて説明しているガイド内の文言を、サポート対象バージョンを 9.4 → 9.5 に変更。

  • 例(イメージ):

    • 変更前: 「Rails は PostgreSQL 9.4 以降をサポートします」
    • 変更後: 「Rails は PostgreSQL 9.5 以降をサポートします」
  • 特に array_position のような 9.5 以降でしか使えない機能が利用されていることと整合する説明になっています。

3) guides/source/command_line.md

  • rails new コマンドなど、CLI 周りの説明の中で PostgreSQL を選択した場合の要件や説明に含まれている PostgreSQL バージョンの記述を 9.4 → 9.5 に更新。
  • ここもユーザー向けに「この Rails を使うときの PostgreSQL の最低バージョン」を明示する役割があります。

4) railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml.tt

  • rails new myapp -d postgresql などで生成される config/database.yml テンプレートの PostgreSQL 用雛形内のコメントや説明文に含まれるバージョン表記を 9.4 → 9.5 に修正。
  • アプリ新規作成時に生成される database.yml のコメントを読むと、「このアプリは PostgreSQL 9.5 以降を対象にしている」ことが分かるようになります。

  1. 影響範囲・注意点
  • PostgreSQL 9.4 以下を使っている Rails アプリケーションは、実質的にサポート外になります。
    • 既に array_position を利用するコードが入っていた時点で、実運用上は 9.4 以下ではクエリエラーが出うる状態でしたが、このPRで「Rails 公式として 9.5 以上前提」ということがより明確になります。
  • PostgreSQL 9.5 自体も EOL になっているため、実際の運用では 12 以降など、より新しいバージョンに上げることが推奨されますが、少なくとも Rails 的には 9.5 未満はサポートしないという位置づけになります。
  • 新規に Rails アプリを作る際(rails new)に生成される設定や、ガイドの要件を見て環境を整えるユーザーにとって、「9.4 で始めてしまう」ミスを防止できるようになります。
  • このPR自体はバグ修正・機能追加ではなく「バージョン要件/ドキュメントの明示・整合性取り」が主目的のため、アプリケーションコード側の変更は不要ですが、利用しているDBサーバーのバージョン確認は必須です。

  1. 参考情報 (あれば)
  • PostgreSQL 9.5 リリースノート(array_position 追加の記載あり)
    https://www.postgresql.org/docs/release/9.5.0/#AEN132063

  • array_position 関数ドキュメント(PostgreSQL 公式)
    https://www.postgresql.org/docs/current/functions-array.html
    (バージョンを切り替えて 9.4 と 9.5 のドキュメントを比較すると、9.5 から追加されたことが確認できます)

  • array_position を利用したクエリ例(Active Record ではArel/SQLフラグメント経由で使用される可能性があります):

    sql
    SELECT array_position(ARRAY[1, 2, 3], 2); -- => 2

このPRは、こうした関数を前提にした Active Record の実装と、Rails が公式に掲げる PostgreSQL のサポートバージョンを一致させるためのメンテナンス的な変更です。


#56879 Fixed guard around AS::TC.run_order

マージ日: 2026/3/3 | 作成者: @zenspider

  1. 概要 (1-2文で)
    ActiveSupport::TestCase の run_order 周りのガード条件が誤っていた/不十分だったのを 1行の修正で正した PR です。テストの実行順序設定に関する条件分岐が、想定通りのケースでのみ動作するようになります。

  1. 変更内容の詳細

※ PR 本文と統計から読み取れる範囲の解説であり、実際の 1行差分は要旨ベースです。

  • 対象ファイル: activesupport/lib/active_support/test_case.rb
  • 変更: 1行の条件式(ガード)を修正(+1 / -1)

ActiveSupport::TestCase には、Minitest との連携でテストの実行順を制御するための run_order 設定があります。
この PR は、ActiveSupport::TestCase.run_order を触る際の「条件付きで設定・上書きするための guard(if 条件など)」が誤っていたため、それを正しい条件に修正したものです。

Ruby のテスト周りでよくあるパターンとしては、例えば以下のようなガードです:

ruby
if defined?(Minitest::Test) && Minitest::Test.respond_to?(:run_one_method)
  # run_order を設定する / 書き換える
end

今回の PR は、

  • 「定義の有無のチェックが間違っている」
  • 「クラス/モジュール名を取り違えている」
  • respond_to? で見るべきメソッド名を誤っている」
  • 「truthy / falsy 判定の条件を逆にしている」

といったミスを、1行で修正したものと考えられます。

その結果、ActiveSupport::TestCase.run_order まわりのコードが、

  • 想定している環境でのみ有効になる
  • 想定していない環境では安全にスキップされる

という、本来意図していた振る舞いに揃えられています。


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

    • ActiveSupport::TestCase を使用しているテストスイートで、run_order の設定・挙動に関わる部分(例: テストの実行順序を :random:sorted などに設定している場合)。
    • 特に、Minitest のバージョンや API に依存した条件付きコードが存在する場合、その条件が「正しく効く」ようになるため、これまで偶然動いていたコードがより厳密な挙動になる可能性があります。
  • 想定される変化

    • ある特定の Minitest バージョン・環境で、以前は guard が効かずに run_order が設定されてしまっていた/逆に設定されていなかった、という状態が修正されます。
    • それに伴い、テストの実行順(ランダム化の有無など)が、Rails が意図しているデフォルトの挙動に揃えられる可能性があります。
  • 開発者が確認すべき点

    • Rails を更新した際に、テスト実行順が変わったと感じた場合、この PR の影響で guard が正しく機能し始めた可能性があります。
    • 独自に ActiveSupport::TestCase.run_order を変更している、あるいは Minitest との統合部分を monkey patch している場合は、そのコードが新しい guard 条件と矛盾していないか確認してください。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/56879
  • Minitest と ActiveSupport::TestCase の run_order 周り:
    • ActiveSupport::TestCase は内部的に Minitest のランナーと連携しており、run_order によってテスト順序の制御(例: ランダム化)が行われます。
  • 実際の差分(1行)の内容は PR 本体を参照すると、どの条件式がどう修正されたかが明確に確認できます。

#56906 Fix Encoding::CompatibilityError with non-ASCII strict locals defaults

マージ日: 2026/3/2 | 作成者: @kataokatsuki

  1. 概要 (1–2文で)
    strict_locals を使った ERB テンプレートで、ローカル変数のデフォルト値に非 ASCII 文字(例: "café")を含めると Encoding::CompatibilityError が発生する問題を修正した PRです。File.binread により ASCII-8BIT で読み込まれた locals 宣言のエンコーディングを、テンプレート本体と同じ外部エンコーディング(通常 UTF-8)に「タグ付けし直す」ことでエラーを防いでいます。

  1. 変更内容の詳細

問題の背景

  • テンプレートは File.binread で読み込まれており、初期状態のエンコーディングは ASCII-8BIT (バイナリ)。

  • Action View の strict_locals! は、テンプレート先頭のコメントに書かれた locals 宣言を正規表現で抜き出します。例:

    erb
    <%# locals: (label: "café") -%>
    Hello <%= label %>

    ここで $1 にマッチした部分 ("label: \"café\"" など) を @strict_locals に格納しますが、この時点ではまだ encode! が走っていないため、@strict_locals のエンコーディングは ASCII-8BIT のままです。

  • その後、encode! によってテンプレート本文は Encoding.default_external(通常は UTF-8)に変換・タグ付けされますが、locals 部分だけは ASCII-8BIT のまま残る

  • 最終的な compiled_source は「UTF-8 な ERB の出力」と「ASCII-8BIT な @strict_locals を含む文字列」を同じ Ruby 文字列の中で補間して組み立てます。

    • 両方とも 0x7F を超えるバイトを含んでいると、Ruby のエンコーディング交渉で Encoding::CompatibilityError が発生。
    • 偶然 ASCII の範囲しか含まない場合でも、compiled_source の encoding が不正に ASCII-8BIT になる可能性がある。

修正内容

actionview/lib/action_view/template.rbstrict_locals! メソッド内で、locals 宣言を抽出したあとに以下のような処理を追加しています(概念的なイメージ):

ruby
def strict_locals!
  # 正規表現で locals 宣言を取り出して @strict_locals に格納
  if @source.sub!(STRICT_LOCALS_PATTERN, "")
    @strict_locals = $1

    # ★ ここでエンコーディングの「ラベルだけ」を外部エンコーディングに揃える
    @strict_locals.force_encoding(Encoding.default_external)
  end
end

ポイント:

  • encode ではなく force_encoding を使っているため、「バイト列は一切変えず、エンコーディングタグだけを変える」挙動です。
  • これは、すでにテンプレート本体に対して encode! が行っている処理(外部エンコーディングへの re-tagging)と同種の対応で、locals の扱いもそれに揃えています。

テスト追加

actionview/test/template/template_test.rb にテストが追加されています。内容はおおよそ次のようなシナリオです(擬似コード):

ruby
def test_strict_locals_with_non_ascii_default_does_not_raise
  template = <<~ERB
    <%# locals: (label: "café") -%>
    <%= label %>
  ERB

  # strict_locals オプション付きでテンプレートをレンダリングしても
  # Encoding::CompatibilityError が発生しないこと
  assert_nothing_raised do
    render_with_strict_locals(template)
  end
end

これにより、非 ASCII 文字を含む strict locals のデフォルト値を使った場合でも、コンパイルおよびレンダリングが正常に動くことを確認しています。


  1. 影響範囲・注意点
  • 影響範囲:
    • ActionView::Template において、strict_locals 機能を利用し、かつ locals 宣言内に非 ASCII 文字(日本語、アクセント付きラテン文字など)を含むデフォルト値を記述しているテンプレート。
    • 上記のケースで発生していた Encoding::CompatibilityError が解消されます。
  • 互換性:
    • force_encoding による「ラベルのみ変更」であり、バイト列自体は変更していないため、動作上の後方互換性への影響は極めて小さいと考えられます。
    • これまで「たまたまエラーになっていなかったが compiled_source の encoding が ASCII-8BIT になっていた」ケースも、正しく UTF-8(等、default_external)として扱われるようになります。
  • 注意点:
    • Rails の Encoding.default_external が通常は UTF-8 を想定しているため、この挙動に依存している環境(たとえば default_external を変更している特殊な環境)では、locals 宣言もそのエンコーディングで解釈される点を理解しておく必要があります。
    • とはいえ、もともとテンプレート本文も同じ default_external に揃えられているため、locals 部分だけが異なるエンコーディングを持つよりは、今回の変更の方が一貫性があります。

  1. 参考情報 (あれば)
  • 関連 Issue: #56904
    「strict locals のデフォルト値に非 ASCII 文字を含めると Encoding::CompatibilityError が出る」ことを報告している Issue。
  • 関連コード:
    • ActionView::Template#strict_locals!
    • ActionView::Template#encode!
  • Ruby のエンコーディング交渉:
    • Ruby では文字列同士を結合・補間する際、両者の Encoding が互換でない場合に Encoding::CompatibilityError が発生します。
    • ASCII-8BIT と UTF-8 のように、「実際のバイト列に 0x7F を超える値を含む」組み合わせは典型的な衝突パターンです。

#56913 Set read-only permissions for GitHub Actions workflow generated by rails new

マージ日: 2026/3/2 | 作成者: @taketo1113

  1. 概要 (1-2文で)
    rails new(および plugin generator)が生成する GitHub Actions の CI ワークフローに、permissions を明示的に追加し、GITHUB_TOKEN の権限を「contents: read」の読み取り専用に固定する変更です。これにより、組織側のデフォルト設定に依存せず、最小限の権限で CI が動くようになります。

  1. 変更内容の詳細

何をしたか

Rails のジェネレータが吐き出す GitHub Actions ワークフローテンプレート(ci.yml.tt)に、以下のような permissions 設定が追加されました。

対象テンプレート:

  • railties/lib/rails/generators/rails/app/templates/github/ci.yml.tt
  • railties/lib/rails/generators/rails/plugin/templates/github/ci.yml.tt

追加された設定(イメージ・サンプル):

yaml
name: CI

on:
  push:
    branches:
      - main
  pull_request:

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      # ...
      # System test での artifact アップロードなど
      - uses: actions/upload-artifact@v4
        with:
          name: test-results
          path: tmp/test-results

ポイント:

  • permissions: contents: read をトップレベル(jobs: と同じ階層)に追加。
  • これにより GITHUB_TOKEN の権限がリポジトリコンテンツの読み取り専用に制限される。
  • PR 説明によると、actions/upload-artifact を使う System Test もこの権限で問題なく動作することが確認済み。

なぜやったか(背景)

  • GitHub Actions では permissions を明示しない場合、GitHub Organization レベルで設定されたデフォルト権限をそのまま継承します。
  • 組織のデフォルトが「書き込み可能」の場合、rails new が生成したワークフローも、不要に GITHUB_TOKEN が書き込み権限を持って実行されてしまう可能性があります。
  • Rails がデフォルトで生成する CI は基本的に「コードのチェック・テスト実行」が主目的であり、リポジトリへの変更などの書き込み権限は不要です。
  • 最小権限の原則(least privilege)の観点から、ワークフロー側で明示的に read-only に固定することで、安全なデフォルトを提供します。

また、この振る舞いの変更については railties/CHANGELOG.md にも追記されています。


  1. 影響範囲・注意点

影響範囲

  • 影響を受けるのは この PR マージ以降の Rails バージョンで rails new または rails plugin new を実行したときに自動生成される GitHub Actions CI ワークフロー です。
  • 既存のリポジトリに既に生成済みの ci.yml には自動では反映されないため、必要であれば手動で追随させる必要があります。

注意点・確認しておくとよいこと

  • 現状の Rails 標準 CI ワークフローが行うのは
    • コードチェック・テスト実行
    • actions/upload-artifact による成果物アップロード などであり、contents: read で問題ないことが確認されています。
  • もしプロジェクト側で、CI ワークフローから以下のような「書き込み」を行っている場合は注意してください:
    • GitHub Releases の作成
    • タグやブランチの push
    • Issue / PR への自動コメント、ラベル付け、マージ操作
    • ファイルの自動更新(例: actions/checkout + git commit + git push
  • その場合は、対象ワークフローで個別に permissions を上書きする必要があります。例:
yaml
permissions:
  contents: write
  pull-requests: write

もしくはジョブ単位で指定することも可能です。


  1. 参考情報 (あれば)

#56915 Don't add Rack::Sendfile to the stack if x_sendfile_header is nil

マージ日: 2026/3/2 | 作成者: @byroot

  1. 概要 (1-2文で)
    Rails のデフォルトミドルウェアスタックにおいて、config.action_dispatch.x_sendfile_headernil の場合は Rack::Sendfile を積まないように変更された PR です。x_sendfile_header が未設定なら Rack::Sendfile は実質 no-op なので、その無駄を省いています。

  1. 変更内容の詳細

2-1. 何をしているか

これまでの挙動:

  • config.action_dispatch.x_sendfile_headernil でも、Rack::Sendfile ミドルウェアは常にミドルウェアスタックに含まれていた。
  • ただし x_sendfile_headernil の場合、Rack::Sendfile はレスポンスを書き換えず「何もしない (noop)」挙動になる。

今回の変更:

  • x_sendfile_headernil のときは、そもそも Rack::Sendfile をスタックに追加しない
  • 結果的に、不要なミドルウェアを 1 つ減らし、スタックがわずかにシンプルかつ軽量になる。

2-2. コードレベルのイメージ

※ 実際のコードとは多少異なる可能性がありますが、意図としては以下のような変更です:

ruby
# 変更前のイメージ
if app.config.action_dispatch.x_sendfile_header
  # ヘッダ指定あり → Rack::Sendfile を追加
  middleware.use ::Rack::Sendfile, app.config.action_dispatch.x_sendfile_header
else
  # ヘッダ指定なしでも Rack::Sendfile は入っていた(no-op)
  middleware.use ::Rack::Sendfile
end
ruby
# 変更後のイメージ
if app.config.action_dispatch.x_sendfile_header
  # ヘッダ指定があるときだけ Rack::Sendfile を追加
  middleware.use ::Rack::Sendfile, app.config.action_dispatch.x_sendfile_header
end
# nil の場合は一切追加しない

2-3. テスト・CHANGELOG

  • railties/test/commands/middleware_test.rb では:
    • x_sendfile_header が設定されていない場合にミドルウェア一覧に Rack::Sendfile が含まれないことを確認するテストが追加・修正されている。
    • x_sendfile_header を設定した場合にのみ Rack::Sendfile が含まれることもテストされていると考えられます。
  • railties/CHANGELOG.md:
    • x_sendfile_headernil の場合は Rack::Sendfile をスタックに追加しない」という変更点が追記され、アップグレード時に気づけるようになっています。

  1. 影響範囲・注意点

3-1. 実務上の影響

  • デフォルト設定 (x_sendfile_header = nil) のアプリ

    • これまで: ミドルウェアスタックに Rack::Sendfile が含まれていたが、実質何もしていなかった。
    • 今後: そもそも Rack::Sendfile がスタックに現れなくなる。
    • 挙動としては元々 noop だったため、レスポンスや機能面の違いはほぼありません。
    • ただし、ミドルウェア一覧を検査しているテストやツールがある場合、Rack::Sendfile が消えることによる「見た目」の変化に注意が必要です。
  • x_sendfile_header を明示的に設定しているアプリ

    • 例: Apache + X-Sendfile, Nginx + X-Accel-Redirect などを使う場合
    • この場合は従来通り Rack::Sendfile がスタックに入り、挙動も変わりません。
    • つまり、静的ファイルをフロントサーバーにオフロードする既存の構成に影響はありません。

3-2. メトリクス・ミドルウェア依存コード

  • Rack ミドルウェアのリストをもとに何か処理しているコード(独自の introspection、ドキュメント生成、デバッグ用出力など)がある場合、Rack::Sendfile が「消えた」ことに依存関係がないかだけ確認すると安心です。
  • 性能面では、Rack::Sendfile 分の 1 レイヤーがなくなるため、非常にわずかに軽くなりますが、通常は体感できるレベルではありません。

  1. 参考情報 (あれば)
  • config.action_dispatch.x_sendfile_header の一般的な使い方:
    • Apache + mod_xsendfile など:
      ruby
      # config/environments/production.rb
      config.action_dispatch.x_sendfile_header = 'X-Sendfile'
    • Nginx (X-Accel-Redirect):
      ruby
      config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
  • Rack::Sendfile の役割:
    • レスポンスボディに大きなファイルがある場合、アプリケーションから直接送らず、X-Sendfile などのヘッダをフロントサーバに付与し、実際のファイル転送を Web サーバー側に任せるための Rack ミドルウェアです。
    • 今回の変更は、「そのヘッダをそもそも使わない設定なら Rack::Sendfile も不要だよね」という整理になります。

#56903 Fix flaky test: sql.active_record event count in transaction instrumentation tests

マージ日: 2026/3/1 | 作成者: @flavorjones

  1. 概要 (1-2文で)
    トランザクション計測用テストで、sql.active_record のイベント件数を厳密にカウントしている箇所が、スキーマキャッシュの状態に依存して CI でフレ flaky になる問題を修正した PRです。テスト開始前に Topic 関連のスキーマ情報をウォームアップしておくことで、想定外の SCHEMA クエリがイベント数に混入しないようにしています。

  1. 変更内容の詳細

対象テスト:

  • TransactionInstrumentationTest#test_sql_events_do_not_overlap_with_savepoints
  • TransactionInstrumentationTest#test_sql_events_do_not_overlap

これらのテストは、グローバルに sql.active_record を subscribe し、発行された SQL イベントの件数を「ぴったり N 件」とアサートしていました。ところが、直前の別テストが Topic.reset_column_information を呼び出して Topic のスキーマキャッシュをクリアしている場合、Topic.first の実行時にスキーマ関連のクエリが「遅延評価」で走り、その SQL イベントがカウントに含まれてしまい、期待件数 +1 となって失敗していました。

問題となる呼び出しパスは以下のようなものです:

text
Topic.first
  → instantiate
    → init_internals
      → define_attribute_methods
        → attribute_names
          → table_exists?
            → schema_cache.data_source_exists?

schema_cache.data_source_exists? が DB に対してスキーマ問い合わせを投げます(例:

  • MySQL: SHOW FULL FIELDS FROM "topics"
  • SQLite: PRAGMA table_xinfo('topics')
    など)。これが sql.active_record の「SCHEMA」イベントとして発火し、グローバルサブスクライバが拾ってしまう、という構図です。

修正内容:
各テストの「計測対象となるブロックの前」に、Topic 関連のキャッシュを温めるための Topic.first を追加しています。これにより、以下のような状態になります。

  • 事前に Topic.first を呼んでおく
    • カラム情報 / data source 存在確認 / attribute methods 定義 など、遅延ロードされるスキーマ関連処理がこのタイミングで済む
    • ここではまだ sql.active_record を subscribe していないので、SCHEMA クエリはカウント対象外
  • その後、sql.active_record を subscribe してテスト本体を実行
    • トランザクション・セーブポイント関連で発行される SQL のみがイベントとしてカウントされるようになる

イメージ的には以下のような形になっています(実際のコードは行数・メソッド名等が異なる可能性がありますが、意図としてはこれに近いです):

ruby
def test_sql_events_do_not_overlap_with_savepoints
  # 追加されたウォームアップ
  Topic.first

  events = []
  ActiveSupport::Notifications.subscribed(
    ->(*args) { events << args },
    "sql.active_record"
  ) do
    # テスト対象のトランザクション処理
    Topic.transaction do
      # ...
    end
  end

  assert_equal 6, events.size
end

この「テストの計測区間の外でキャッシュをウォームアップする」ことで、キャッシュ状態によるイベント数のぶれがなくなり、flakyさを解消しています。


  1. 影響範囲・注意点
  • 影響範囲
    • 変更はテストコード (activerecord/test/cases/transaction_instrumentation_test.rb) のみで、本番コードには影響ありません。
    • CI でたまに落ちていた TransactionInstrumentationTest の2つのテストの安定性が向上します。
  • 注意点 / 学び
    • sql.active_record のようなグローバルなイベントを「件数で厳密に」アサートするテストは、スキーマキャッシュや他のテストが発行するクエリの影響を受けやすく、flaky になりやすいです。
    • Rails では Model.reset_column_information が多数のテストで使われており(PR説明によると ~200 箇所)、これがスキーマキャッシュを cold にしてしまうため、「テスト実行順序に依存した不安定さ」が生じます。
    • 同様のテストを書く場合は:
      • 計測区間の前にキャッシュをウォームアップする
      • あるいは SCHEMA イベントをフィルタする / トランザクション関連の SQL だけを条件で絞る
      • そもそも「件数をぴったり固定値でアサートする」のではなく、下限・上限や特定のパターンのみを確認する といった工夫が必要になることが分かります。

  1. 参考情報 (あれば)
  • PRで例示されている再現手順(修正前):
    bash
    cd activerecord
    bin/test test/cases/persistence_test.rb:804 test/cases/transaction_instrumentation_test.rb:393
    persistence_test 側の ensureTopic.reset_column_information が走り、その直後に transaction_instrumentation_test が実行されることで、スキーマキャッシュがクリアされた「悪い順序」が再現されます。
  • スキーマキャッシュへの依存や lazy load の副作用が、Instrument/Notification ベースのテストで問題になり得る、という典型例としても参考になります。

#56902 Fix IsolatedExecutionState.share_with() call in AC::Live

マージ日: 2026/3/1 | 作成者: @tavianator

  1. 概要 (1-2文で)
    ActionController::Live が isolation_level = :fiber のときに誤ったコンテキストを IsolatedExecutionState.share_with に渡していたバグを修正し、正しい「元のコンテキスト」を共有するようにした PR です。これにより、Fiber ベースの並行実行時でも IsolatedExecutionState(スレッドローカルや CurrentAttributes 等を含む実行コンテキスト)が正しく引き継がれます。

  1. 変更内容の詳細

問題点

  • Rails では ActiveSupport::IsolatedExecutionState を使って、
    • CurrentAttributes
    • executor の state
    • その他の実行コンテキスト
      などを “isolated な状態” として管理しています。
  • その state は IsolatedExecutionState.share_with(context) で、ある実行単位(スレッド / Fiber など)から別の実行単位に「共有」できます。
  • isolation_level = :fiber の場合、本来は「元の IsolatedExecutionState コンテキスト」を share_with に渡す必要がありますが、ActionController::Live では誤って「元のスレッド」を渡していました。

結果として、Fiber 隔離モードの環境では:

  • Live streaming 用に起動したスレッド/Fiber 上で、期待どおりに
    • Current.xxx
    • request ローカルな状態
      が引き継がれない、もしくは誤った状態になる、というバグが発生しうる状況でした。

修正内容

ActionController::Live 内での Live レスポンススレッド起動部分の IsolatedExecutionState 連携コードが修正されています。

概念的には、以下のような修正です(擬似コード):

ruby
# 修正前イメージ(誤り)
original_thread = Thread.current

Thread.new do
  # isolation_level = :fiber のときに thread を share_with に渡していた
  ActiveSupport::IsolatedExecutionState.share_with(original_thread) do
    # live ストリーミングの処理
  end
end

# 修正後イメージ(正しい)
original_context = ActiveSupport::IsolatedExecutionState.context

Thread.new do
  # 保存しておいた “コンテキスト” を share_with に渡す
  ActiveSupport::IsolatedExecutionState.share_with(original_context) do
    # live ストリーミングの処理
  end
end

ポイント:

  • 修正前: 「元スレッド (Thread)」を share_with に渡していた
  • 修正後: 「元コンテキスト (IsolatedExecutionState.context)」を事前に取得して渡すようにした

isolation_level = :thread の場合は「スレッドごとに別の state」を持つのが自然ですが、:fiber の場合は「Fiber ごとに state を分離する」ので、Thread オブジェクトそのものを基準にするのは誤り、というのが本質です。

テスト追加

actionpack/test/controller/live_stream_test.rb にテストが追加されています (+20 行)。

おおまかに以下を検証していると考えられます(実際のコード構造ベースの推測):

  • isolation_level = :fiber 環境を設定
  • Live コントローラアクション内で、CurrentAttributes などの値をセット
  • streaming 用スレッド/Fiber 内でも同じ値が見えること
  • あるいは、「もとの context が正しく共有される」ことを明示的にテスト

これにより、今回の修正が回 regress しないように自動テストがカバーされています。


  1. 影響範囲・注意点
  • 影響を受けるのは:
    • ActionController::Live を利用しているアプリケーション
    • かつ ActiveSupport::IsolatedExecutionState の isolation_level = :fiber を利用している構成
  • この組み合わせで、以下のような症状が改善される可能性があります:
    • Live streaming 中に CurrentAttributes(例: Current.request_id など)が nil / 不正な値になる
    • executor / instrumentation の state が streaming 中だけおかしくなる
  • 修正はあくまで「正しいコンテキストを渡す」バグフィックスであり、API 仕様や表向きの挙動を変更するものではありません。そのため:
    • 基本的には後方互換な修正
    • 既に「誤った挙動」に依存してしまっていたコードがあれば、挙動が変わる可能性はありますが、そのような依存は通常想定されません
  • CHANGELOG は更新されていないため、「マイナーなバグ修正」と位置づけられていますが、Fiber 隔離モードと Live を組み合わせている環境では実害のある不具合修正になり得ます。

  1. 参考情報 (あれば)
  • 関連概念:

    • ActiveSupport::IsolatedExecutionState
      • Rails の “実行コンテキスト隔離” の仕組みで、スレッド / Fiber ごとに独立した state を管理する。
    • isolation_level = :fiber
      • マルチスレッド下でさらに Fiber 単位で state を分離するための設定。
    • ActionController::Live
      • Rack hijack を使ったストリーミングレスポンス向けのモジュール。レスポンス処理を別スレッドで実行するため、IsolatedExecutionState の共有が重要になる。
  • この PR により、「リクエストコンテキストを Live ストリーミング用スレッド / Fiber にも正しく引き継ぐ」という Rails の設計意図にコードが一致するようになっています。