Skip to content

Ruby on Rails PR Digest - 2026年 5月

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

#57438 Preserve IPv6 prefix when dumping PostgreSQL cidr/inet defaults to schema.rb

マージ日: 2026/5/30 | 作成者: @55728

  1. 概要 (1-2文で)
    PostgreSQL の cidr / inet 型のデフォルト値を schema.rb にダンプする際、IPv6 アドレスのプレフィックスが /32 のときに失われてしまうバグを修正する PR です。IPv4 と IPv6 で「フルマスク」とみなすプレフィックスを正しく判定することで、IPv6 のネットワーク情報がスキーマの往復で壊れないようにしています。

  1. 変更内容の詳細

問題のポイント

ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Cidr#type_cast_for_schema は、schema.rb に書き出す Ruby 文字列リテラルを生成するメソッドです。既存実装では次のようになっていました:

ruby
def type_cast_for_schema(value)
  # If the subnet mask is equal to /32, don't output it
  if value.prefix == 32
    "\"#{value}\""
  else
    "\"#{value}/#{value.prefix}\""
  end
end
  • /32 を「フルマスク」とみなし、その場合はサフィックス /32 を省略して "192.168.1.0" のように出力する、という前提でした。
  • これは IPv4 では正しいものの、IPv6 では「フルマスク」は /128 であり、/32 は普通に使われるネットワークプレフィックスです。

そのため、たとえば:

ruby
create_table :networks do |t|
  t.cidr :address, default: "::/32"
end

というマイグレーションを作ると:

  1. db:schema:dump::/32"::" として schema.rb に書き出される
  2. その後 db:schema:load で再読込すると、"::"IPAddr.new("::") により ::/128 として解釈される

結果として、マイグレーションで意図したデフォルト (::/32) と、スキーマから再生成したデフォルト (::/128) が食い違う、というサイレントなデータ破壊が起きていました。

修正内容

フルマスクの条件を「IPv4 なら /32、IPv6 なら /128」とするように変更しました:

ruby
def type_cast_for_schema(value)
  # If the subnet mask covers the full address, don't output it
  if value.prefix == (value.ipv6? ? 128 : 32)
    "\"#{value}\""
  else
    "\"#{value}/#{value.prefix}\""
  end
end

これにより:

  • IPv4:
    • 192.168.1.0/32"192.168.1.0" (従来通り)
    • 192.168.1.0/24"192.168.1.0/24" (従来通り)
  • IPv6:
    • ::/32"::/32"(これまでは "::" になっていたのが修正される)
    • fe80::/10"fe80::/10"
    • ::/0"::/0"
    • ::1/128"::1"(新たに /128 を省略するようになるが、IPAddr.new("::1") するともともと /128 なので意味的には同じ)

OID::InetOID::Cidr を継承しており type_cast_for_schema をオーバーライドしていないため、この修正は cidrinet 両方の型に効きます。

追加テスト

activerecord/test/cases/adapters/postgresql/cidr_test.rb に、これまでなかった type_cast_for_schema 用のテストが追加されています。

  • IPv4 の既存挙動(/32 省略・その他は保持)が変わっていないことを確認
  • IPv6 で:
    • /32/10, /0 のような部分プレフィックスがそのまま保持されること
    • /128 のときだけ省略されること

を検証しています。


  1. 影響範囲・注意点
  • 影響を受けるケース:
    • PostgreSQL の cidr または inet カラムに IPv6 のネットワーク (/128 以外) をデフォルト値として設定している アプリケーション。
    • これまで db:schema:dumpdb:schema:load の往復で、元のデフォルト値から異なる値が設定されていた可能性があります。
  • 具体的な問題例:
    • 以前の挙動: default: "::/32"schema.rb には "::" → 再ロードすると ::/128
    • この PR 適用後: schema.rb には "::/32" と出力され、db:schema:load による再現で ::/32 が維持されます。
  • 既存 IPv4 の挙動には変更なし:
    • 192.168.1.0/24 などの既存テストケースはそのまま通っており、互換性に問題はありません。
  • 仕様上の“変化”:
    • IPv6 の /128 フルホストをデフォルトにしている場合、schema.rb 上の見た目が "::1/128" から "::1" に変わることがあります。
    • これは IPAddr の解釈が変わるわけではなく、あくまで表示上・スキーマ上の表記ゆれが解消されるだけです(IPv4 の挙動との対称性がよくなります)。

アップグレード時に、もし schema.rb に IPv6 のデフォルトがある場合は、ダンプ結果の差分を一度確認すると安心です。「これまで silent に壊れていたものが正しく直る」という修正なので、通常は望ましい差分だけのはずです。


  1. 参考情報 (あれば)
  • 変更ファイル:
    • activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb
      • type_cast_for_schema のフルマスク判定ロジック変更
    • activerecord/test/cases/adapters/postgresql/cidr_test.rb
      • 新規テストの追加
    • activerecord/CHANGELOG.md
      • IPv6 cidr / inet デフォルト値のスキーマダンプに関するバグ修正として追記
  • 背景:
    • 元の /32 前提の実装は 2014 年 (commit: 4321cd09a5) から存在しており、長年 IPv6 に対して不正確な挙動をしていたことになります。
    • ただし影響を受けるのは「IPv6 ネットワークをデフォルトとして持つ」比較的ニッチなケースに限られます。

#57506 Fix NullPool#server_version deadlock

マージ日: 2026/5/29 | 作成者: @hmcguire-shopify

  1. 概要 (1-2文で)
    Rails の Active Record で「単体接続(NullPool)」利用時に server_version を最初に呼ぶとデッドロックする不具合があり、NullPool が内部で使うロックを Mutex から Monitor に変更することで解消した PR です。通常の ConnectionPool と同じ再入可能なロック方式に揃えた、比較的小さなバグ修正です。

  1. 変更内容の詳細

問題の背景

  • 新規に確立した DB 接続で、最初に server_version を取得するフロー:

    1. #connect!
    2. #configure_connection
    3. #check_version
      ⇨ この中で再度プールの #server_version が呼ばれる
  • プール側の #server_version は同期化されたメソッド:

    • 通常の ConnectionPool:
      • ロックに Monitor を使用(再入可能なので、同じスレッドがネストしてロックを取り直しても OK)
    • 単体接続用の NullPool:
      • ロックに Mutex を使用(再入不可のため、同じスレッドで server_version を再入するとデッドロック)
  • 結果として、

    • 「コネクションプールを使わずに単体接続を扱うパス(NullPool)」で
    • 「接続直後に server_version を取りにいく」ケース
      でデッドロックが発生していた。

修正内容

  • NullPool が利用する同期プリミティブを MutexMonitor に変更:

    • これにより、server_version 呼び出し中に connect!configure_connectioncheck_version → 再度 server_version という再入が起きても、同一スレッドであればロックが再取得可能になりデッドロックが発生しなくなる。
    • 実装レベルでは ConnectionPool 側と同じロック戦略に統一。
  • テスト追加:

    • activerecord/test/cases/connection_adapters/standalone_connection_test.rb に、スタンドアロン接続(NullPool 経由)でのこの挙動を検証するテストが追加されている。
    • 狙いは「最初の問い合わせで server_version を取得するようなコードパスでもハングしないこと」の担保。
  • CHANGELOG 更新:

    • Active Record の CHANGELOG.md に、このバグ修正が追記されている(挙動に影響するバグ修正に該当するため)。

  1. 影響範囲・注意点
  • 影響を受ける可能性があるケース:

    • Active Record で「コネクションプールを使わずに単一接続を扱うパス(NullPool)」を利用している場合
    • かつ、その接続で最初に実行する処理が server_version の問い合わせ、または check_version を内部で呼ぶような接続設定処理である場合
    • このような環境では、これまでデッドロックやハングに見える現象が起きていた可能性がある
  • 変更による挙動:

    • デッドロックが解消される以外の挙動変更は基本的になし
    • 並行実行に関する同期の性質も、ConnectionPool に合わせた形になるため、プール利用時と単体接続時の一貫性が増す
  • パフォーマンス/互換性:

    • MutexMonitor への変更は Ruby の標準的な同期プリミティブの置き換えであり、一般的なユースケースで性能面の顕著な悪化は想定しにくい
    • インターフェースや設定項目には変更がないため、アプリケーションコード側での対応は不要

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57506
  • 関連クラス:
    • ActiveRecord::ConnectionAdapters::NullPool
    • ActiveRecord::ConnectionAdapters::ConnectionPool
  • Ruby の MonitorMutex の違い:
    • Mutex: 非再入ロック。同じスレッドが再度 lock するとデッドロック。
    • Monitor: 再入可能ロック。同じスレッドであればネストしたロック取得が可能。
      今回はこの「再入可能性」が必要なパスだったため、Monitor への切り替えとなっている。

#57475 Active Record Ractor-safe freezing

マージ日: 2026/5/29 | 作成者: @gmcgibbon

  1. 概要 (1-2文で)
    Active Record 内のいくつかのモジュール/クラスで、オブジェクトの freeze や定数化(constantize)を進めることで、Ractor(Ruby 3 の並列実行モデル)でも安全に利用できるようにするための変更です。機能追加というより、Ractor 対応のための内部実装の堅牢化に近い位置づけです。

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

※PR本文からは抽象的な説明しかありませんが、ファイル一覧と目的から推測できる範囲での技術的な整理です。

共通の方向性

  • 「Ractor セーフ」にするために行っているのは主に次の 2 つです:
    • オブジェクトを freeze する
      Ractor 間で共有されるオブジェクトは、原則として「イミュータブル(変更不可)」である必要があるため。
    • 可能な限り定数として定義する
      実行時に書き換えられない形(凍結された定数など)で持つことで、Ractor 間共有が安全になる。

ファイルごとの変更イメージ

実際の差分が小さい(+8 / -3 行)ことから、各ファイルで行われているのは「既存のクラス変数・定数・ハッシュ等に freeze を付与する」「動的に構築していたものを、定数化+凍結する」といったものと考えられます。

1) activerecord/lib/active_record/readonly_attributes.rb (+1/-1)

  • 読み取り専用属性(attr_readonly など)の管理ロジックのどこかで、集合(配列・set など)やハッシュの freeze を追加した可能性が高いです。
  • 例としては、次のような変更が典型です(あくまでイメージ):
ruby
# 変更前のイメージ
READONLY_ATTRIBUTES = %i[created_at updated_at]

# 変更後のイメージ
READONLY_ATTRIBUTES = %i[created_at updated_at].freeze
  • 目的は「読み取り専用属性の定義そのものをイミュータブルにして、Ractor 間で安全に共有できるようにする」ことです。

2) activerecord/lib/active_record/relation/delegation.rb (+1/-0)

  • ActiveRecord::Relation が Array や Enumerable に委譲しているメソッド定義のテーブルなどに freeze が入った可能性があります。
  • よくあるパターン:
ruby
SINGLE_VALUE_METHODS = [:limit, :offset, :order]
# → これに .freeze を付けた、あるいは Hash/Array の中身を凍結
SINGLE_VALUE_METHODS = [:limit, :offset, :order].freeze
  • Relation のデリゲーション設定自体は実行時に変化しない前提なので、凍結しても通常のアプリ開発者には影響が出ない想定です。

3) activerecord/lib/active_record/relation/where_clause.rb (+3/-1)

  • WhereClause は SQL の WHERE 句を表現する内部クラスで、述語(predicates)の配列などを持っています。
  • Ractor セーフティの観点では、以下のようなことが行われた可能性があります:
    • 空の WhereClause を表すオブジェクトを「共有定数+freeze」にする
    • 条件リストなどを freeze してイミュータブルに扱う

例(イメージ):

ruby
# 変更前
EMPTY_WHERE = new([])

# 変更後
EMPTY_WHERE = new([].freeze).freeze
  • こうしておくと、Ractor 間で EMPTY_WHERE を共有しても、後から変更される心配がなくなります。

4) activerecord/lib/active_record/type.rb (+3/-1)

  • Active Record の型システム(ActiveRecord::Type)に関連する定数やレジストリを凍結したと考えられます。
  • 典型的な変更パターン:
ruby
# 変更前
TYPES = {
  integer: Type::Integer.new,
  string:  Type::String.new,
}

# 変更後
TYPES = {
  integer: Type::Integer.new,
  string:  Type::String.new,
}.freeze
  • もしくは、型のリスト/マップを定数化して freeze することで、Ractor 間で型情報を安全に共有する狙いです。

  1. 影響範囲・注意点

影響範囲

  • 対象はほぼ内部実装であり、一般的な Rails アプリで直接触れる API(Model.where など)の表面的な挙動は変わらない想定です。
  • ActiveRecord::Relation、WhereClause、型レジストリ、readonly attributes まわりの内部オブジェクト構造がやや「よりイミュータブル寄り」になっています。

注意点

  1. 内部定数を書き換えていたコードは壊れる可能性

    • ActiveRecord::Relation::Delegation::SOMETHING << :foo のように、Rails の内部定数を直接変更していた場合、freeze により FrozenError が発生し得ます。
    • そもそも非推奨な使い方なので、これを機にやめるべきです。
  2. Ractor を使う場合の前提整備

    • この PR 単体で「Active Record が完全に Ractor セーフになる」わけではなく、「Ractor セーフに近づけるための一歩」です。
    • Ractor を本格的に使う場合は、他の PR(#57323 など)も含めて Ruby / Rails のバージョンと互換性を確認する必要があります。
  3. freeze によるパフォーマンス面の懸念はほぼ小さい

    • 変更されているのは多くが定数や内部の再利用オブジェクトで、一度生成して共有する性質のものです。
    • 通常の CRUD 操作で目に見えるパフォーマンス劣化はまず起きないと考えられます。

  1. 参考情報 (あれば)
  • 類似 PR:
  • Ractor の制約(公式ドキュメント):
  • 実務的には:
    • Rails で Ractor を使いたい場合は、Active Record を含む各コンポーネントがどこまで Ractor セーフ対応されているかを、PR/CHANGELOG ベースで追いながら検証する必要があります。

#57478 Acquire reaper_lock in preconnect to avoid racing with the reaper

マージ日: 2026/5/29 | 作成者: @yahonda

  1. 概要 (1-2文で)
    ActiveRecord の接続プールで preconnect 実行時に reaper_lock を取得するようにし、リーパー(メンテナンス用スレッド)との競合状態(race condition)で CI の test_preconnect が落ちる問題を解消する PR です。disconnectdiscard! と同じ「プール全体に影響する操作は reaper_lock で囲む」という設計に揃えています。

  1. 変更内容の詳細

問題の背景

  • ActiveRecord の接続プールには、バックグラウンドで定期的にメンテナンスを行う「reaper」スレッドがあります。
  • reaper はメンテナンスサイクルの中で preconnect を呼びますが、このときすでに reaper_lock を取得した状態で動きます。
  • テスト ActiveRecord::ConnectionAdapters::ConnectionPoolThreadTest#test_preconnect では、アプリケーション側からも同時に pool.preconnect を呼ぶケースがあり、ここで reaper とユーザースレッドが並行に preconnect を叩くことがあります。
  • reaper スレッド側は checkout_for_maintenance により一時的にコネクションをチェックアウトし、その間は接続が「遷移中」の状態になります:
    • allow_preconnect = false
    • @raw_connection = nil
  • この「遷移中」状態で、別スレッドの preconnect が実行されると、接続が connected? にならないまま終わり、テストが失敗することがありました(CI での intermittent failure)。

変更点

ConnectionPool#preconnectreaper_lock で囲むように変更しています。

イメージとしては、以下のような変更です(概念的なサンプル):

ruby
def preconnect
  reaper_lock.synchronize do
    # 既存の preconnect ロジック
    with_exclusively_acquired_all_connections do |conn|
      conn.verify!
    end
  end
end

実際には:

  • 既存の preconnect メソッドの本体を、そのまま reaper_lock.synchronize の中に移動
  • すでに disconnect, discard!, with_exclusively_acquired_all_connections が同様に reaper_lock で囲まれており、そのパターンに揃えた形

なぜ reaper 自身はブロックされないのか

  • reaper_lock は再入可能 (reentrant) な Monitor を使っています。
  • reaper スレッドがメンテナンスサイクル内で reaper_lock を取得済みの状態で preconnect を呼んでも、同じスレッドからの再取得なのでブロックされません。
  • 逆に、ユーザースレッドからの preconnectreaper_lock を取りに行くため、reaper がメンテナンス中であれば待機し、コネクションが中途半端な状態のまま処理されることを防ぎます。

再現と効果

  • Docker 上の MariaDB 11.4 (--cpus=0.02 --memory=128m でかなりリソースを絞った状態) かつ並行負荷をかけることでローカル再現。
  • 修正前: test_preconnect が 7, 16, 36, 65 回目など複数回 fail。
  • 修正後: 100 回以上の繰り返し実行でも失敗せず、そのうち 86 回以上は実際に reaper_lock の待ちが発生していることをログで確認。

  1. 影響範囲・注意点
  • 影響範囲
    • ActiveRecord の ConnectionPool#preconnect を明示的に使っているコード(アプリ起動時やジョブワーカー起動時などで「事前接続」しているケース)が対象です。
    • 内部的には、reaper が動いている環境すべてでロック順序がより一貫したものになります。
  • パフォーマンス
    • preconnect 自体が「全体に影響する重めの操作」であり、頻繁に呼ぶべきではないことから、reaper_lock で囲むことによるオーバーヘッドは実質的に無視できる範囲と考えられます。
    • むしろ、メンテナンススレッドとの競合で接続が壊れたりテストが flaky になるリスクを減らすメリットが大きいです。
  • デッドロックの懸念
    • reaper_lock はすでに disconnectdiscard! 等で使われており、そこに preconnect が加わるだけなので、ロック設計としては既存方針の拡張です。
    • Monitor ベースで再入可能な点と、reaper 側がもともと reaper_lockpreconnect の順で呼んでいる点から、新たなロック順序逆転は発生していません。

  1. 参考情報 (あれば)
  • この PR は次の変更のフォローアップです:
    • e45d8acc746「Lock the pool while performing global operations」
      → 接続プール全体に影響する操作を reaper_lock で保護する方針を導入したコミット
  • デバッグ用ブランチ(問題再現とログ出力付き):
  • 対象テスト:
    • ActiveRecord::ConnectionAdapters::ConnectionPoolThreadTest#test_preconnect

#57501 Return a completed promise for async_count with limit(0)

マージ日: 2026/5/28 | 作成者: @fallintoplace

  1. 概要 (1-2文で)
    Relation#async_countlimit(0) のときに整数 0 を直接返してしまう不整合が修正され、常に ActiveRecord::Promise を返すようになりました。これにより、async_count の戻り値で value, then, pending? などの Promise API を一貫して利用できます。

  1. 変更内容の詳細

何が問題だったか

async_count はドキュメント上「ActiveRecord::Promise を返す」とされていますが、次のようなケースでは Promise ではなく Integer が返っていました。

ruby
result = Post.limit(0).async_count
result.class
# => Integer (修正前)

原因は execute_simple_calculation にある「limit(0) のショートカット」です。
limit(0) の場合は「レコード数は必ず 0 になる」と判断して DB クエリを実行せず、早期 return していましたが、その早期 return が「async 用のラップ処理」よりも先に実行されていたため、async_count 経由でも生の 0 が返ってしまっていました。

その結果:

  • 呼び出し側は Promise を期待しているのに Integer を受け取ることがある
  • value, then, pending? など Promise 向けメソッドが使えない
  • 「常に Promise を返す」という API 契約が破られていた

何を直したか

ポイントは「limit(0) の高速パス(ショートカット)は維持しつつ、async のときは完成済み Promise を返す」ようにしたことです。

  • 同期パス:
    • Post.limit(0).count → これまで通りクエリ無しで 0 を返す
  • 非同期パス (async_count):
    • 以前: Post.limit(0).async_count → クエリ無しだが 生の 0 が返る
    • 以後: Post.limit(0).async_count → クエリ無しで 値が 0 の完了済み ActiveRecord::Promise を返す

修正後の挙動:

ruby
result = Post.limit(0).async_count
result.class
# => ActiveRecord::Promise

result.value
# => 0

result.pending?
# => false

内部的には、execute_simple_calculationlimit(0) での早期 return の取り扱いを調整し、「async の場合は早期 return も Promise で包まれる」ように揃えています。
クエリを飛ばさない最適化自体(「0件確定ならクエリしない」)はそのまま残っています。

テスト

既存の「limit(0) で count はクエリ無しで 0 を返す」テストを拡張し、async_count でも同じくクエリ無しで動作しつつ、返り値が Promise になっていることを検証するようになっています。

実行されたテスト(一部):

  • test_count_should_shortcut_with_limit_zero
  • test_count_with_empty_in

  1. 影響範囲・注意点
  • async_count 利用コードへの影響

    • これまで limit(0).async_count の戻り値を「Integer だと仮定して」扱っていたコードがある場合、型が ActiveRecord::Promise に変わるため注意が必要です。
      • 例: Post.limit(0).async_count == 0 → 修正後は promise == 0 になるので常に false
      • 正しくは: Post.limit(0).async_count.value == 0
    • 一方で、本来の API 契約(常に Promise を返す)に沿ったコードは今回の変更でむしろ安全になります。
  • パフォーマンス

    • limit(0) のショートカットによる「クエリを飛ばさない」最適化は維持されているため、パフォーマンス的な後退はありません。
    • async でも DB にアクセスせず、即時完了済み Promise が得られます。
  • API の一貫性向上

    • async_count, async_sum, async_average など、非同期集計 API が「条件によっては生の値を返す」ようなバラつきをなくす方向の一貫した修正の一つです(PR 説明にも、以前修正された「矛盾する条件での async aggregation の早期 return」と同種の問題とあります)。

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

    • activerecord/lib/active_record/relation/calculations.rb
      • execute_simple_calculation 内の limit(0) 周りのロジック
    • activerecord/test/cases/calculations_test.rb
      • test_count_should_shortcut_with_limit_zeroasync_count 分の検証が追加
  • 実務でのポイント:

    • 非同期集計を使う場合、戻り値は常に Promise 前提で書く.value か、then でつなぐ)ようにしておくと、今回のような修正に強いコードになります。
    • limit(0) を絡めたクエリで「結果が常に 0 になる」という前提に依存していても、今回の変更でその前提自体は変わっていません(0であることとクエリを打たないことは維持されています)。

#50979 Action Cable server adapterization

マージ日: 2026/5/28 | 作成者: @palkan

  1. 概要 (1-2文で)
    Action Cable の「WebSocket + スレッド実装」に強く結びついていた部分を分離し、「サーバーアダプタ化」するリファクタリングです。既存の Rails 標準 Action Cable の挙動はほぼ変えずに、代替サーバー・Fiber 実行モデル・SSE などに乗せ替えやすくするための基盤整備が行われています。

  1. 変更内容の詳細

2-1. 非同期実行基盤(Executor)の整理・抽象化

これまで

  • Action Cable には主に2種類のスレッドプールが存在:
    • Worker: アプリケーションコード(チャネルの perform など)実行用
    • Pub/Sub 用 Executor: Redis などの pub/sub に絡む非同期処理用
  • 後者は StreamEventLoop の中に埋め込まれており、I/O ループと Executor が密結合でした。
  • また、Concurrent Ruby の Executor をそのまま使っていたため、実行モデルの差し替えがしづらい状態でした。

今回の変更

  • pub/sub 用 Executor を StreamEventLoop から切り離し、ActionCable::Server の属性として独立。

  • Executor を小さなインターフェイスでラップ:

    ruby
    class AsyncExecutor
      # 非同期にブロックを実行
      def post(&block); end
    
      # interval 秒ごと(or 後)にブロックを実行するタイマー
      # 戻り値はキャンセル用ハンドルなど
      def timer(interval, &block); end
    
      # シャットダウン処理
      def shutdown; end
    end
  • 内部実装としては Concurrent Ruby のスレッドプールを使うが、このラッパを挟むことで将来的に Fiber ベースの Executor などに差し替え可能。

  • この PR 自体には config.action_cable.async_executor = :fiber のような設定追加は含まず、まずは内部構造だけ抽象化。

ポイント:
「Action Cable の非同期実行モデル」を 1 箇所で差し替えられるようにするための第一歩。WebSocket に限らず、ほかの I/O モデルでも同じ Connection/Channel コードを再利用しやすくなります。


2-2. 非同期処理の責務を pub/sub レイヤーに集約

これまで

  • Channel の内部ロジックから event_loop(=WebSocket 用 I/O ループ)へ直接アクセスして、非同期に stream_from をセットするなど、層をまたいだ依存があった。
  • pub/sub アダプタ側でも非同期実行していたため、stream_from 一回につき二重に非同期呼び出しが走る可能性があった。

今回の変更

  • pub/sub 周りの非同期処理を SubscriberMap::Async サブクラスに集約。
  • Channel レイヤーからは「非同期かどうか」を意識せず、pub/sub アダプタ側が必要なら内部で async 実行する形に統一。

これにより:

  • Channel 抽象は「ストリームを張る・外す」だけに集中し、「どう非同期化するか」はアダプタに委譲。
  • WebSocket 実装に依存していた箇所から event_loop の直参照が消える。
  • stream_from の実行パスがシンプルになり、別サーバー実装でも Channel コードをそのまま使いやすくなる。

2-3. Connection クラスの分割(アプリ層とサーバ層)

これまで

ActionCable::Connection::Base が以下を全部抱えていました:

  • アプリケーションレベルの責務
    • 接続の認証 (connect)、拒否 (reject_unauthorized_connection)
    • 接続状態に紐づくロジック、Channel 管理など
  • サーバー実装レベルの責務
    • WebSocket イベントの購読・ハンドリング (onopen, onmessage, onclose の設定など)
    • Rails executor のラップ
    • ソケット I/O、バッファリング

このため:

  • WebSocket 以外のトランスポート(SSE など)で Connection ロジックだけ再利用するのが難しい。
  • テスト時にも EM などの I/O ループを必要としてしまい、重く・複雑になりがち。

今回の変更

Connection レイヤーを 2 つに分割:

  1. ActionCable::Connection::Base

    • アプリケーションレベルの Connection クラス(従来利用していたクラス)。
    • ユーザーが定義する ApplicationCable::Connection は引き続きこれを継承。
    • 「ソケットがどう届くか」「どのサーバーか」を意識せず、抽象的な Socket/Server を相手にする。
  2. ActionCable::Server::Connection

    • Web サーバー側から見た、低レベル Connection 実装。
    • WebSocket や HTTP SSE などトランスポートに近い部分と、Connection::Base の橋渡しを担う。
    • Rails executor ラップや各種ハンドラ登録など、「サーバー実装としての責務」を持つ。

両者の間は以下の抽象インターフェイスでやり取りします(PR 説明の UML より):

  • _Server
    • pubsub, executor, config を提供。
  • _Socket
    • logger, env, request, protocol
    • transmit(payload) / close() / perform_work(receiver, method_name, *args)
  • _Connection
    • handle_open, handle_incoming(payload), handle_close, beat, statistics

概念的な流れ:

txt
Webサーバー (Puma/Iodine/etc.)

 ActionCable::Server::Socket::* (WebSocket, SSE, etc.)
      ↓ (抽象 _Socket として)
 ActionCable::Server::Connection

 ActionCable::Connection::Base (ApplicationCable::Connection)

 各 Channel

メリット:

  • WebSocket 以外のトランスポート(SSE など)でも、Server::Socket レイヤーを差し替えるだけで Connection::Base / Channel のコードはそのまま使える。
  • テストでは「純 Ruby のクラス」として Connection/Channel を扱えるようになり、I/O ループ依存が軽減。
  • Rails executor のラップや pub/sub との連携など、サーバー固有の事情を Server::Connection 側に閉じ込められる。

2-4. Server::Socket レイヤーの新設

削除されたファイル(旧構造):

  • action_cable/connection/client_socket.rb
  • action_cable/connection/web_socket.rb
  • action_cable/connection/message_buffer.rb
  • action_cable/connection/stream.rb

新たに追加されたファイル(新構造):

  • action_cable/server/socket.rb
  • action_cable/server/socket/client_socket.rb
  • action_cable/server/socket/web_socket.rb
  • action_cable/server/socket/stream.rb
  • action_cable/server/socket/message_buffer.rb

役割の整理:

  • ActionCable::Server::Socket およびそのサブクラス群は「実際のトランスポート実装」を担当。
    • ClientSocket:クライアントとのエンドポイント抽象
    • WebSocket:WebSocket プロトコルの扱い
    • StreamMessageBuffer:ソケット I/O のためのバッファリング・ストリーム処理
  • Connection::Base 側からは、これらは _Socket 抽象として見える。

これにより:

  • Iodine など独自の WebSocket 実装・pub/sub を持つサーバでも、「Action Cable アダプタ」として実装しやすくなる。
  • 同じインターフェイスで、WebSocket 以外のトランスポートを差し込める。

2-5. テストサポートの改善 (ActionCable::Connection::TestCase, Channel::TestCase)

主なポイント:

  • Connection / Channel のテストで、より実際に近いクラスを使えるようにリファクタ。
    • 以前:EventMachine / WebSocket 実装に近いテストヘルパが必要になるケースがあった。
    • 今回:純粋な Ruby オブジェクトとして Connection / Channel を扱える構成になり、テストがシンプルに。
  • ActionCable::Connection::TestCase に多くの変更(+117 / -34)
    • 実際のサーバアダプタに依存しない形で、接続確立・メッセージ送信・切断などをテスト。
  • ActionCable::Channel::TestCase も内部ロジック刷新(+58 / -73)
    • 実運用に近い形で stream/subscription 周りをテスト可能。

また、periodic timers のテスト用に新ヘルパーが追加:

ruby
# テスト内でタイマーを進める
advance_time(5.seconds)

これにより、Channel の periodic :foo, every: 5.seconds のような処理を、実時間を待たずにテストできます。


2-6. 購読エラーの取り扱い改善

  • 以前は、以下のような不正な状態に対して:
    • AlreadySubscribed
    • ChannelNotFound などを、単にログに出すだけ / 無視するだけのことが多かった。
  • 今回は、Subscriptions レイヤーが「どう扱うか」を決めず、例外として必ず raise する設計に変更。
  • その代わり、例外をどこでキャッチしてどう応答するかは、より上位(サーバ/接続)レイヤーで制御。

目的:

  • エラー処理ポリシーを一箇所に集約しやすくし、実サーバ実装ごとの振る舞い(ログを出す、クライアントにエラーを返す等)を明確に切り替えられるようにする。

2-7. 既存 Action Cable API への影響

  • PR 説明にもある通り、「目に見える大きな新機能 or 破壊的変更」は導入していません。
  • ApplicationCable::Connection, ApplicationCable::Channel, クライアント JavaScript から見た Action Cable の API は基本的に変わりません。
  • あくまで内部構造のリファクタリングと抽象化が中心で、現時点では Rails ユーザーが設定を変えなくてもそのまま動作します。

  1. 影響範囲・注意点

3-1. 影響範囲

  • Action Cable の内部構造:
    • Connection / Socket / Executor / PubSub の責務分担がかなり変わっており、Action Cable 内部に依存したハックや monkey patch を書いている場合は影響大。
    • ActionCable::Connection::Base の中にあったソケット関係のクラス(ClientSocket, WebSocket, Stream, MessageBuffer)は ActionCable::Server::Socket::* 側へ移動しているため、クラス名・モジュールパスの変更に注意。
  • テストヘルパー:
    • ActionCable::Connection::TestCase, ActionCable::Channel::TestCase を独自に拡張している場合は挙動・呼び出し順序の変化に注意。
    • 新しい構造に対応した rspec-rails 側の変更も別リポジトリで進行中(リンクあり)。

3-2. 注意点 / マイグレーション観点

  • 通常の Rails アプリが公式にサポートされている使い方(ApplicationCable::Connection / Channel を素直に継承しているだけ)であれば、基本的にコード変更は不要と想定されています。
  • ただし、以下に該当する場合はコードの見直しが必要になる可能性があります:
    • ActionCable::Connection::Base に対する monkey patch / private メソッドへの依存。
    • ActionCable::Connection::* 配下のクラス(旧 ClientSocket など)を直接参照している。
    • Action Cable の pub/sub 実装をカスタムしていて、event_loop に直接触っている。
  • actioncable-next gem(Rails 7+ 向けの先行実装)と同等の構造に近いため、すでにそれを試している場合は互換性の確認がしやすいです。

  1. 参考情報

この PR は「Action Cable を 1 つのサーバ・実行モデルに固定しないための抽象化」が主目的なので、Action Cable を他サーバで動かしたい・Fiber 対応を進めたい・SSE など複数トランスポートを併用したいといったニーズがある場合に、今後の基盤になります。


#57268 Fix: registers ERBTracker instead of RubyTracker when eager_load = true

マージ日: 2026/5/28 | 作成者: @joaoGabriel55

  1. 概要 (1-2文で)
    Rails 8.1 デフォルト設定かつ eager_load: true などの条件下で、ERB テンプレートの依存関係トラッキングが正しく動かない不具合を修正した PR です。ActionView.render_tracker の設定タイミングを after_initialize から専用 initializer に移し、ERB が適切に RubyTracker へ紐づくようにしています。

  1. 変更内容の詳細

不具合の背景

元々のコードでは、ActionView.render_tracker の設定が「ネストした config.after_initialize」内で行われていました:

ruby
config.after_initialize do |app|
  config.after_initialize do
    ActionView.render_tracker = config.action_view.render_tracker
  end
  # ...
end

after_initialize は Rails のブートシーケンスのかなり後ろで呼ばれますが、ここでさらにネストした after_initialize を呼んでいたため、

  • 外側の after_initialize が完了した
  • すでに eager load 等の処理が終わった

ActionView.render_tracker が設定されていました。

その結果:

  • eager_load: true
  • enable_reloading: false
  • config.load_defaults 8.1

といった構成だと、ERB ハンドラが ActionView::DependencyTracker::RubyTracker に紐づく前に、依存関係トラッカーのセットアップ処理が終わってしまい、ERB の dependency tracking が機能しない、というバグが出ていました(Issue #57265)。

修正内容:initializer 化

この PR では、ActionView.render_tracker の設定を専用 initializer に切り出し、ブートシーケンス上の適切なタイミングで確実に実行されるように変更しています。

修正後のコード:

ruby
initializer "action_view.set_render_tracker" do |app|
  ActionView.render_tracker = app.config.action_view.render_tracker
end

ポイント:

  • config.after_initialize ではなく、Railtie の initializer を使うことで、
    • 他の initializer 処理
    • eager_load!
    • after_initialize コールバック より前に ActionView.render_tracker が設定されるようになる。
  • app.config.action_view.render_tracker の値(設定されたトラッカー)が、ActionView 全体の render_tracker として適切に反映される。

テストの追加

以下のような条件で、ERB ハンドラが正しく RubyTracker に紐づくことを検証する回帰テストが追加されています。

  • load_defaults 8.1
  • eager_load = true
  • config.enable_reloading = false

ActionView::DependencyTracker に対して、ERB (:erb) に対応するトラッカーとして ActionView::DependencyTracker::RubyTracker が登録されていることを確認するテストです。
また、actionview/test/template/dependency_tracker_test.rb 側にもテストが追加され、依存トラッキングの期待挙動が守られていることをより直接的に保証しています。


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

    • Action View の dependency tracking(特に ERB テンプレート)の初期化タイミングに依存しているコード。
    • eager_load: true かつ enable_reloading: false のような、本番相当の設定での挙動に特に影響します。
    • Rails 8.1 以降のデフォルト設定(load_defaults 8.1)で恩恵を受けるケースが中心です。
  • 実務上の影響

    • これまで、ERB テンプレート変更に対する依存関係検出やキャッシュ無効化等が正しく行われていなかった環境では、今回の修正により期待通りに動作するようになります。
    • ActionView.render_tracker をアプリ側で独自に上書きしていた場合でも、initializer の実行順序によっては今回の変更の影響を受ける可能性があります。
      • Railtie/Engine 自作時に initializer の順序を細かく制御している場合は、"action_view.set_render_tracker" より前後どちらで動くべきかを意識しておくと安全です。
  • 注意点

    • 挙動が「後ろにずれる」のではなく「前にくる」方向の変更なので、after_initialize の中で ActionView.render_tracker に依存していたコードがある場合、今回の修正でむしろ安定する方向の変更です。
    • CHANGELOG の更新は行われていないため、マイナーなバグフィックス扱いとなっていますが、本番環境で依存関係トラッキングに依存している場合は挙動確認した方がよい変更です。

  1. 参考情報 (あれば)
  • 対応 Issue: #57265
  • PR 本体: #57268 「Fix: registers ERBTracker instead of RubyTracker when eager_load = true」
  • 該当箇所の主なファイル:
    • actionview/lib/action_view.rb
    • actionview/lib/action_view/railtie.rb
    • actionview/test/template/dependency_tracker_test.rb
    • railties/test/application/configuration_test.rb

#39525 Add support for default order

マージ日: 2026/5/28 | 作成者: @udan11

  1. 概要 (1-2文で)
    ActiveRecord::Relation に「デフォルトの並び順」を定義できる #default_order が追加され、関連(has_many など)にも default_order: オプション/スコープで同等の機能を指定できるようになりました。これにより、アプリ側で「常にこの順で取りたいが、明示的に order を書いたときはそちらを優先したい」というパターンを Rails レベルで表現できます。

  1. 変更内容の詳細

2-1. Relation#default_order の追加

ActiveRecord::Relation に新たに default_order が追加されました(query_methods.rb)。

挙動イメージ:

ruby
# モデル側でスコープとして定義
class Comment < ApplicationRecord
  scope :popular_first, -> { default_order(likes: :desc) }
end

# 使用例
Comment.popular_first        # likes DESC で並び替え
Comment.popular_first.to_sql # SELECT ... ORDER BY "comments"."likes" DESC

# ただし、明示的に order を指定した場合はそちらが優先
Comment.popular_first.order(created_at: :asc)
# => ORDER BY "comments"."created_at" ASC

ポイント:

  • default_order(...) は「他に order が何も指定されていないときだけ有効になる order 条件」を relation に付与する。
  • 通常の order と違い、あとから order を呼ぶと「マージ」ではなく、明示的な order のほうが優先される設計(=デフォルトは上書きされることを前提とした order)。

implicit_order_column との違い:

  • implicit_order_column は主に last, first, find_each などの暗黙の order を必要とする場面用(モデル単位、1カラム前提のことが多い)。
  • default_order は relation / scope / association 単位で任意の order 式を指定可能。

2-2. アソシエーション(特に has_many)での default_order サポート

has_many 定義に default_order オプション、あるいはラムダスコープ中で default_order(...) を指定できるようになりました。

PR の例:

ruby
class Post < ApplicationRecord
  has_many :comments, default_order: :likes
  # .. または ..
  has_many :comments, -> { default_order(:likes) }
end

post = Post.first
post.comments                 # likes でソートされて返る
post.comments.order(:created_at) # ここでは created_at の order が優先される

実装的には:

  • associations/builder/has_many.rb などで default_order: オプションを許容。
  • association_scope.rb / collection_association.rb 側で、関連のスコープに default_order を反映させる。
  • アソシエーションの default_order: オプションと、スコープ内の default_order を組み合わせられる(ただし、やはり明示的な order のほうが優先される、というルールは同じ)。

2-3. Finder 系メソッドとの連携

finder_methods.rb / relation.rb への変更により、default_orderfirst, last, take, find_by などの finder メソッドを呼び出した際にも適切に考慮されるようになっています。

  • すでにある implicit_order_column との兼ね合いで、
    • 明示的な order
    • default_order
    • implicit_order_column
      の優先順位や適用タイミングが整理されています(テストが追加されている)。

2-4. テスト・ドキュメント

  • has_many_associations_test.rb, relations_test.rb, relation/mutation_test.rb, test/models/post.rb にテスト追加。
  • CHANGELOG.md に機能追加として記載。

  1. 影響範囲・注意点

影響範囲:

  • ActiveRecord 全般(特に has_many を多用するコード)で、default_order を利用できるようになる。
  • default_order を導入しても、既存の order 呼び出しはそのまま優先されるため、既存コードへの破壊的な影響は基本的にない。
  • implicit_order_column をすでに使っているアプリでは、default_order との組み合わせで order の振る舞いが変わるケースがありうるため、重要なクエリは挙動確認が推奨される。

注意点:

  1. default_order は「隠れた order」になりうる

    • relation をチェインしている場所で、意図せずソートされている(パフォーマンス・結果順序)ことに気づきにくくなる可能性があります。
    • チーム内コーディング規約として、どこに default_order を使うかを明確にするのが良さそうです(モデル全体のポリシー vs 特定アソシエーション限定など)。
  2. 明示的な order の振る舞い

    • default_order は「明示的 order が存在しない場合のみ有効」なので、後続の order があると無視されます。
    • デフォルトに加えてさらに order を積み上げたい場合は、order(:likes).order(:created_at) のような従来どおりの書き方を使うほうが明確です(default_order はそうした「積み上げ」用途にはあまり向かない)。
  3. 複合キー・複雑な order 式

    • default_order(likes: :desc, created_at: :asc) のように複数列も指定可能ですが、DB インデックスと整合しているか・パフォーマンスに問題が出ないかは各アプリで要確認です。

  1. 参考情報 (あれば)

この機能を使うと、例えば「コメントはデフォルトで最新順」「いいね数順」などをモデル側に閉じ込めつつ、必要な画面だけ明示的な order で挙動を変える、という設計がやりやすくなります。


#53101 [Fix #52229] Respect #reject in Action Cable before_subscribe callbacks

マージ日: 2026/5/28 | 作成者: @joshuay03

  1. 概要 (1-2文で)
    Action Cable の before_subscribe コールバック内で reject を呼んだ場合に、これまで実行されてしまっていた subscribed を呼ばないようにするバグ修正です。before_subscribe による購読拒否が、期待どおりチャンネル購読開始処理全体を止めるようになります。

  1. 変更内容の詳細

2-1. 問題の背景

  • Action Cable では、チャンネルに接続する際に before_subscribe コールバックを定義できます。
  • 期待される挙動としては、before_subscribe 内で reject(または reject_subscription)を呼ぶと、そのクライアントのチャンネル購読は拒否され、subscribed は呼ばれないはずです。
  • しかし、バグにより、before_subscribe 内で reject を呼んでも、その後に subscribed が実行されてしまうケースがありました。

この PR は、その不整合を解消して Issue #52229 をクローズしています。

2-2. 主なコード変更

a. ActionCable::Channel::Base#subscribe_to_channel の修正

subscribe_to_channel は、WebSocket 接続からチャンネル購読を開始する際のメインエントリです。このメソッドの中で:

  1. before_subscribe コールバックの実行
  2. 実際の購読処理(subscribed の呼び出し、ストリームの開始など)

が順に行われています。

今回の修正では、「1. before_subscribe 実行後にチャンネルが reject 済みであれば、2. 以降を実行しない」という条件分岐が追加されました。

擬似コードで表すと、次のような変化です(あくまで概念的なイメージ):

ruby
def subscribe_to_channel
  run_before_subscribe_callbacks

  # 変更前: 常に subscribed を呼んでいた
  # subscribed

  # 変更後: reject 済みの場合は subscribed を呼ばない
  unless rejected?
    subscribed
  end
end

ここで rejected? は、チャンネルが購読拒否されたかどうかを示すフラグ的な状態を確認するメソッド(もしくはそれに準ずる判定)で、reject 実行時にセットされるようになっています。

b. ActionCable::Channel::Callbacks の拡張

actioncable/lib/action_cable/channel/callbacks.rb に、before_subscribe コールバックからの reject を正しく検知できるような処理が追加されています。

イメージとしては:

  • before_subscribe 実行中に reject が呼ばれたら、そのチャンネルインスタンスに「reject 済み」を記録する
  • その記録を subscribe_to_channel 側で参照して、後続処理(subscribed)をスキップする

という連携です。

c. テストの追加

actioncable/test/channel/base_test.rb にテストが追加・修正され、以下を検証しています:

  • before_subscribe コールバック内で reject を呼んだ場合に、subscribed が呼ばれないこと
  • (必要なら)ソケット側の状態・メッセージ送信が期待通りになっていること

テストソケット (actioncable/test/stubs/test_socket.rb) にも 1 行追加されており、テストで reject 状態を検証するための最小限の変更が入っています。

d. CHANGELOG の更新

actioncable/CHANGELOG.md にバグ修正として追記されており、外部公開 API 挙動として修正されたことが明記されています。


  1. 影響範囲・注意点
  • 影響を受けるのは、Action Cable のチャンネルで before_subscribe を使い、その中で reject を呼んでいる場合です。
  • これまで:
    • before_subscribereject しても subscribed が呼ばれてしまっていたため、
      • subscribed 内で「購読成功前提」の処理(DB 更新・ログ・他チャンネルへのブロードキャストなど)を書いていると、実は reject されているのに動いてしまう、というバグがありました。
  • 修正後:
    • before_subscribereject すると、その後の subscribed は確実に呼ばれません。
    • 「旧バグ前提」でワークアラウンドを書いていた場合(例: subscribed 側で rejected? をチェックして早期 return していた等)、そのコードは不要になるか、挙動が変わる可能性があります。

サンプルイメージ:

ruby
class ChatChannel < ApplicationCable::Channel
  before_subscribe :ensure_allowed

  def ensure_allowed
    reject unless current_user&.allowed_in_chat?
  end

  def subscribed
    # 修正前: allowed_in_chat? == false でも呼ばれてしまうことがあった
    # 修正後: allowed_in_chat? == false ならここは呼ばれない
    stream_from "chat_#{params[:room_id]}"
  end
end

このようなコードは、修正後にようやく「意図した通りの安全な挙動」になります。


  1. 参考情報 (あれば)

#57500 Fix Array#to_query producing double && when array contains empty hash

マージ日: 2026/5/28 | 作成者: @bogdan

  1. 概要 (1-2文で)
    Array に空 Hash ({}) が含まれている場合に Array#to_query&& を連続させた不正なクエリ文字列を生成してしまうバグを修正した PR です。空 Hash 由来の空文字列を結合前に捨てることで、仕様に沿った application/x-www-form-urlencoded 形式になるようにしています。

  1. 変更内容の詳細

問題点

従来の挙動:

ruby
{ a: [1, {}, 2] }.to_query
# => "a%5B%5D=1&&a%5B%5D=2"

ここで a%5B%5Da[] を URL エンコードした文字列です。
&& の間には何もないため、application/x-www-form-urlencoded としてパースすると「名前も値も空のペア」が 1 つ挿入されたのと同じ扱いになります(WHATWG URL Standard 上は & 区切りで 3 要素と解釈される)。

原因:

  • Hash#to_query は「空 Hash の場合は ""(空文字)を返す」仕様になっている。
  • Array#to_query は、要素ごとに to_query を呼んだ結果を & で join している。
  • 配列に空 Hash {} が含まれると、その要素が "" になり、join の結果 value1 && value2 のような文字列ができてしまう。

修正内容

Array#to_query 内で、各要素を to_query した結果の中から空文字列 ("") を除外してから & で連結するように変更しました。

疑似コードイメージ:

ruby
# 変更前 (概念的に)
array.map { |v| v.to_query(key) }.join("&")

# 変更後 (概念的に)
array.
  map { |v| v.to_query(key) }.
  reject { |s| s.empty? }.
  join("&")

これにより、空 Hash が途中に含まれていてもクエリ文字列は次のようになります。

ruby
{ a: [1, {}, 2] }.to_query
# 変更後 => "a%5B%5D=1&a%5B%5D=2"

テスト

activesupport/test/core_ext/object/to_query_test.rb に、空 Hash を含む配列を使ったテストが追加されています。
期待値として && ではなく単一の & で繋がることを確認しています。


  1. 影響範囲・注意点
  • 影響範囲:
    • ActiveSupport の Array#to_query を利用しているコード全般。
    • 特に、クライアントサイド・サーバサイド問わず、Rails 側でクエリ文字列を組み立てて外部 API / 内部エンドポイントにリクエストしている箇所で、配列に空 Hash が紛れ込む可能性がある場合に挙動が変わります。
  • 後方互換性:
    • 以前から出力されていた && 付きのクエリは仕様的には誤りに近く、多くの実装では「空のパラメータ 1 個を余計に含む」か「無視される」だけなので、バグ修正として見なせます。
    • 逆に「&& が出ること」自体に依存したコード(文字列比較テストなど)がある場合はテストが落ちます。クエリ文字列の期待値を書いているテストは見直しが必要な場合があります。
  • 注意点:
    • Hash#to_query はもともとトップレベルでは空 Hash をスキップする実装になっているため、今回の変更はその振る舞いと Array のケースを揃える目的があります。
    • 「空の要素をどう扱うか」に関する仕様がより一貫したので、今後は Array#to_query に空 Hash を混ぜても安全に扱えると考えてよいです。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57500
  • WHATWG URL Standard: https://url.spec.whatwg.org/#concept-urlencoded-parser
    • application/x-www-form-urlencoded では & 区切りで name/value ペアに分解するため、&& は「空のペア」を 1 つ追加したのと同じことになる。
  • 該当コード: activesupport/lib/active_support/core_ext/object/to_query.rb
    • Array#to_query 実装部付近の変更で、要素ごとの to_param/to_query 結果から空文字を弾いている。

#57467 Add shims for Ractor shareability methods

マージ日: 2026/5/28 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    Rails(Active Support)に、Rubyの Ractor 関連メソッドをラップする「シム(shim)」が追加され、Rubyバージョンごとの Ractor 有無を意識せずに「Ractor共有可能化」を呼び出せる仕組みが導入されました。今後のRactor対応を見据え、Kernel 拡張として統一インターフェースを提供する変更です。

  1. 変更内容の詳細

追加されたメソッド (Kernel拡張)

ActiveSupport に以下の4つのメソッドが Kernel のコア拡張として追加されています。

  • ractor_shareable?(obj)
  • ractor_make_shareable(obj)
  • ractor_shareable_proc(&block)
  • ractor_shareable_lambda(&block) (typo ではなく PR 説明どおり)

これらは、実行環境が Ractor をサポートしているかどうかで挙動が変わります。

1. Ractor が利用可能な場合

Ruby 3.0 以降などで Ractor が存在し、対応メソッドが定義されている場合は、そのまま Ractor のメソッドを呼び出す「薄いラッパー」になります。

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

ruby
module Kernel
  if defined?(Ractor) && Ractor.respond_to?(:make_shareable)
    def ractor_shareable?(obj)
      Ractor.shareable?(obj)
    end

    def ractor_make_shareable(obj)
      Ractor.make_shareable(obj)
    end

    def ractor_shareable_proc(&block)
      Ractor.shareable_proc(&block)
    end

    def ractor_shareable_lambda(&block)
      Ractor.shareable_lambda(&block)
    end
  else
    # Ractor がない環境向け (no-op 実装。後述)
  end
end

これにより、Rails側のコードは Ruby のバージョンごとに以下のような条件分岐を書かずに済みます。

ruby
# 変更前イメージ
if defined?(Ractor) && Ractor.respond_to?(:make_shareable)
  Ractor.make_shareable(obj)
end

# 変更後
ractor_make_shareable(obj)

2. Ractor が利用できない場合 (古い Ruby など)

Ractor そのものが存在しない、または該当メソッドがない環境では「no-op(何もしない)」実装になります。

  • ractor_shareable?(obj)
    → 例えば常に false を返す、または「常に shareable とみなす」といった実装が想定される(実装詳細は PR diff 参照)。
  • ractor_make_shareable(obj)
    → そのまま obj を返すだけなど、副作用を持たない。
  • ractor_shareable_proc, ractor_shareable_lambda
    → 通常の proc / lambda をそのまま返すか、単にブロックをラップするだけで特別な共有化処理は行わない。

いずれにしても「Ractor対応コードを書いても、Ractor非対応環境では安全にスルーできる」設計になっています。

実装場所

  • activesupport/lib/active_support/core_ext/kernel/ractor_shareability.rb
    上記4メソッドを定義するコア拡張ファイル。
  • activesupport/lib/active_support/core_ext/kernel.rb
    ここから ractor_shareability 拡張が読み込まれるよう 1 行追加。

テストとドキュメント

  • activesupport/test/core_ext/kernel/ractor_shareability_test.rb
    上記メソッド群が期待どおり Ractor 環境/非Ractor環境で動くことを確認するテストが追加。
  • guides/source/active_support_core_extensions.md
    Active Support のコア拡張ガイドに、新しい Kernel 拡張としてこれらのメソッドの使い方が追記。
  • activesupport/CHANGELOG.md
    Active Support に新しい Ractor-related Kernel 拡張が入ったことが記載。

  1. 影響範囲・注意点

影響範囲

  • Rails 内部コード
    今後、フレームワーク内のさまざまな箇所で Ractor 対応が進む際に、この4メソッドを通じて Ractor shareability を扱うようになります。Ruby バージョンチェックを個別に書く必要がなくなります。
  • アプリケーションコード
    require "active_support/core_ext/kernel" または Rails 環境下であれば、自前のコードからも同じインターフェースを利用可能です。
    例:
    ruby
    # Ractor 対応コードを書きたい場合
    config = { foo: "bar" }
    
    ractor_make_shareable(config)
    
    Ractor.new(config) do |cfg|
      # cfg は共有可能なオブジェクトとして扱える
    end
    Ractor がない環境では ractor_make_shareable は no-op なので、このコード自体はエラーにならずに動作します(ただし当然ながら Ractor 自体は使えません)。

注意点

  1. Kernel 拡張であること
    Kernel にメソッドを増やすのは Rails でも慎重に扱われる部分で、名前衝突リスクがあります。

    • 既に ractor_shareable? などの名前をアプリが独自定義していると衝突する可能性があります。
    • ただし Ractor 自体がかなりニッチかつ新しめの機能であり、かつプレフィックス付きの名前 (ractor_...) なので、衝突リスクは比較的低いと考えられています。
  2. no-op 実装であることの意味
    Ractor非対応環境では、

    • 「Ractor 用に設計した安全性」が実際には担保されない
    • それでもコードは「動いてしまう」
      ため、Ractor あり/なしで挙動(特に並行性まわり)が変わる可能性があります。
      そのため、Ractor を本気で使うコードでは、テストを Ractor 対応 Ruby で実行することが重要です。
  3. 挙動の違いを前提にしたロジックに注意
    例えば以下のような分岐は、Ractor非対応環境では意味が変わる可能性があります。

    ruby
    if ractor_shareable?(obj)
      # Ractor-safe な処理…
    else
      # 非Ractor-safe として扱う処理…
    end

    no-op 実装で何を返すか(true/false)が重要になるため、実装を確認しておくとよいです。


  1. 参考情報 (あれば)
  • Rails PR: 「Add shims for Ractor shareability methods」 (#57467)
    Ractor 対応を進めるための基盤的変更として、まずは「Ruby バージョンに依存しないインターフェース」を提供する目的で導入されています。
  • Ruby 公式ドキュメント(参考)
    • Ractor.make_shareable
    • Ractor.shareable?
    • Ractor.shareable_proc
    • Ractor.shareable_lambda

これら Ruby のメソッドの仕様は、今回追加された Kernel メソッド群の「Ractorあり環境における挙動」とほぼ1対1に対応すると考えてよいです。


#57493 [ci skip] Add step to create path for redirection before launching the application

マージ日: 2026/5/28 | 作成者: @maurobusso

  1. 概要 (1-2文で)
    Railsガイド「Sign Up and Settings」チュートリアルの手順に、パスワード更新後にリダイレクトするためのルート(settings_profile_path)を事前に作成するステップが追加されました。これにより、まだ定義されていないパスヘルパーをチュートリアル中で先に使ってしまう問題が解消されています。

  1. 変更内容の詳細
  • 対象ファイル: guides/source/sign_up_and_settings.md
  • 変更内容はドキュメントのみで、Rails本体のコードや挙動は変わっていません。
  • 具体的には、チュートリアルの中で「パスワード更新後に settings_profile_path へリダイレクトする」コードを示す前に、そのパスが存在するようにルート定義を追加する手順が挿入されています。

おおよそ、以下のような内容がガイドに明示的に加わったと考えられます(イメージ例):

ruby
# config/routes.rb

namespace :settings do
  resource :profile, only: [:show, :update]
end

もしくは:

ruby
get "settings/profile", to: "settings/profiles#show", as: :settings_profile

といった形で、settings_profile_path が利用できるようになるルーティングを定義するステップが説明として追加されています。

背景として、もともとのチュートリアルでは、コントローラなどで以下のようなコードを先に紹介していました:

ruby
redirect_to settings_profile_path, notice: "Your password has been updated."

しかし、その時点ではガイド中に settings_profile_path を生成するルート定義を行う記述がなく、読み進める人にとって「このパスヘルパーどこから来たの?」という混乱を生む状態でした。このPRはそのギャップを埋めています。


  1. 影響範囲・注意点
  • 影響範囲は「ガイドの内容・手順」に限定されており、RailsフレームワークのAPIや挙動に変更はありません。
  • これからチュートリアルに沿ってアプリを作る人にとって:
    • settings_profile_path 未定義によるエラー(undefined method 'settings_profile_path' など)が起きにくくなります。
    • ルーティング定義 → コントローラ実装 → リダイレクトの流れがより自然に理解できるようになります。
  • 既存アプリケーションへのコード変更は不要です。あくまでドキュメント改善です。

  1. 参考情報 (あれば)
  • 関連Issue: #57471
    「チュートリアル内で settings_profile_path が定義前に利用されている」という指摘に対する修正PRです。
  • PR番号: #57493
    CIスキップ指定([ci skip])が付いていることからも分かる通り、コードベースではなくドキュメントのみの変更です。

#57483 Make exception wrapper ractor safe

マージ日: 2026/5/28 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    Rails の ActionDispatch::ExceptionWrapper を Ractor セーフにするため、内部で保持している例外関連の設定ハッシュを凍結・再生成する形に変更し、あわせて wrapper_exceptionssilent_exceptions に対する公式な設定インターフェースが追加されました。これによりマルチ Ractor 実行時のスレッドセーフ性を高めつつ、例外ハンドリングをより柔軟に設定できるようになっています。

  1. 変更内容の詳細

2-1. 例外ラッパーの Ractor セーフ化

対象クラス: ActionDispatch::ExceptionWrapper

もともと以下のようなハッシュをクラスレベルで持っており、Rails 起動後に変更され得る実装になっていました:

  • rescue_responses: 例外クラス → HTTP ステータスコード / シンボル
  • rescue_templates: 例外クラス → 使用するテンプレート名
  • wrapper_exceptions: ラップ対象の例外 (ActionController::RoutingError など)
  • silent_exceptions: ログなど出力を抑制する例外

Ractor セーフにするため、PR では:

  • これらの「ベースになるハッシュ」を freeze する
  • 設定で値を追加・上書きする際は「元ハッシュを複製してマージし、それを凍結した新しいハッシュ」を使う

という方針になっています。
これにより「共有されているミュータブルな状態」を排除し、Ractor 間で同じオブジェクトを安全に共有できるようにしています。

2-2. engine 設定を経由したハッシュの構成

ActionDispatch::ExceptionWrapper に関して、既に以下はエンジン設定として存在していました:

  • config.action_dispatch.rescue_responses
  • config.action_dispatch.rescue_templates

PR では:

  • これらの設定値を読み込む際に、元のデフォルトハッシュを freeze した上で
  • 「デフォルト + 設定で上書き/追加した値」の新しいハッシュを作成し、それを freeze

という処理に統一しています。

イメージとしては以下のような動きです (実際のコードは多少異なりますが概念的にはこのような感じ):

ruby
DEFAULT_RESCUE_RESPONSES = {
  "ActionController::RoutingError" => :not_found,
  # ...
}.freeze

def rescue_responses
  @rescue_responses ||= begin
    merged = DEFAULT_RESCUE_RESPONSES.merge(configured_rescue_responses)
    merged.freeze
  end
end

2-3. wrapper_exceptionssilent_exceptions の公開設定追加

これまで wrapper_exceptionssilent_exceptions は内部定義のみで、config からカスタマイズするための公式な API が存在しませんでした。

PR で:

  • config.action_dispatch.wrapper_exceptions
  • config.action_dispatch.silent_exceptions

が新たに導入され、config/application.rb や各環境ファイルで設定可能になります。

ガイド (guides/source/configuring.md) にも設定方法が追記されています。
例としては以下のように使えます:

ruby
# config/application.rb

module MyApp
  class Application < Rails::Application
    # ラップ対象の例外を追加/変更
    config.action_dispatch.wrapper_exceptions.merge!(
      'MyApp::BadRequestError' => :bad_request
    )

    # ログを出したくない例外を追加
    config.action_dispatch.silent_exceptions += [
      'MyApp::IgnoredError'
    ]
  end
end

内部的には、これらの設定も rescue_responses と同様に:

  • デフォルト値は frozen なハッシュ/配列
  • 設定で変更が入ると「新しい frozen オブジェクト」を生成

という形で扱われます。

2-4. Railtie 側の変更

actionpack/lib/action_dispatch/railtie.rb では、上記の設定項目を扱うための初期化処理が拡張されています:

  • config.action_dispatch.wrapper_exceptions / silent_exceptions の初期値をデフォルト定義から設定
  • アプリケーション設定を読むタイミングで、ExceptionWrapper に反映し直す処理を追加

これにより:

  • 既存のデフォルト挙動は維持しつつ
  • config による上書きが Rails 標準の設定フローの中で安全に適用

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


  1. 影響範囲・注意点

  2. Ractor を使うアプリケーションでは安全性が向上

    • 例外ハンドリング関連のクラス変数/クラスインスタンス変数が不変オブジェクトとして扱われるようになり、Ractor 間でのデータ競合やエラー発生リスクが減ります。
  3. ランタイムでの直接ミューテーションは非推奨 (事実上困難に)

    • 以前: ActionDispatch::ExceptionWrapper.rescue_responses['FooError'] = :not_found のような書き換えが技術的には可能だった
    • 今回: 内部ハッシュが freeze されるため、こうした直接変更は FrozenError を発生させる可能性があります
    • 推奨パターン: すべて config.action_dispatch.* を通した設定に切り替えること
  4. 既存アプリとの互換性

    • config を通して設定していたアプリは、基本的に互換性あり
    • アプリコードや initializer で「クラスの内部ハッシュを直接 mutate していた」場合はエラーになる可能性があるため、そのような箇所がないか確認が必要です。
    • その場合は以下のように書き換える必要があります:
    ruby
    # NG (これからは FrozenError の可能性)
    ActionDispatch::ExceptionWrapper.rescue_responses['MyError'] = :not_found
    
    # OK: config を通す
    Rails.application.config.action_dispatch.rescue_responses.merge!(
      'MyError' => :not_found
    )
  5. テスト・ドキュメント面

    • この PR 自体では新しいテストは追加されていませんが、CHANGELOG とガイドは更新されています。
    • ドキュメント更新により、例外ハンドリングの変更方法として config.action_dispatch.wrapper_exceptions / silent_exceptions の利用が正式に案内されるようになります。

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

    • actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
      • 例外ラップ処理と、rescue_responses などの保持方法が変更
    • actionpack/lib/action_dispatch/railtie.rb
      • config.action_dispatch.* の初期化と ExceptionWrapper への反映処理を更新
    • guides/source/configuring.md
      • wrapper_exceptions / silent_exceptions の新しい設定方法が追記
    • actionpack/CHANGELOG.md
      • Action Pack の挙動変更として記録
  • 関連する設計上のポイント:

    • Ractor セーフ化の基本戦略: 共有オブジェクトは「イミュータブル」にする
    • Rails では、クラスレベルの可変な設定を config 経由で受け取り、最終的に frozen したオブジェクトとして共有する方向に少しずつ移行している流れの一環と見ることができます。

#57344 Update Active Job Attributes documentation

マージ日: 2026/5/28 | 作成者: @bdewater-thatch

  1. 概要 (1-2文で)
    Active Job の「Attributes」機能に関するドキュメントを補強し、Railsガイドや Continuation API からも参照できるようにしたうえで、サンプルコードが実際に属性を「使う」例になるよう修正した PR です。機能追加ではなく、主にドキュメントとサンプルの改善によって意図を分かりやすくしています。

  1. 変更内容の詳細

2-1. Active Job Attributes の説明改善

activejob/lib/active_job/attributes.rb

  • Active Job の Attributes 機能の説明コメントおよびサンプルコードが修正されています。
  • 以前は「属性に値を保存する」ことは示されていたものの、その値を実際にどう使うかが分かりづらかったため、例を「保存して終わり」ではなく、ジョブ内で実際に payment_token を利用する形に変更しています。

イメージとしては、例えば以下のような流れになります(疑似コードのイメージ):

ruby
class ChargeCustomerJob < ApplicationJob
  # Active Job Attributes の宣言
  attribute :payment_token

  def perform
    # 以前: payment_token をどこかに保存するだけ、あるいはそもそも使っていなかった
    # 今回: 実際に payment_token を使って決済処理をするような例に修正
    PaymentGateway.charge(token: payment_token)
  end
end

# ジョブのエンキュー例
ChargeCustomerJob.set(attributes: { payment_token: "tok_xyz" }).perform_later

ポイント:

  • attribute :payment_token のように「ジョブの属性」を宣言。
  • perform メソッドの中で、単に保持するだけでなく、ビジネスロジックに利用する例に変更され、「Attributes を使うと何が嬉しいのか」が伝わりやすくなっています。

2-2. Continuation API からのリンク追加

activejob/lib/active_job/continuation.rb

  • Continuation API(ジョブのチェーンや再開処理に関係する API)のドキュメントコメントから、Active Job Attributes へのリンクが追加されています。
  • 「Continuation と一緒に Attributes をどう使うか / 使えるのか」を探していた人が、直接該当ドキュメントへたどり着きやすくなっています。

2-3. Rails ガイド (Active Job Basics) の更新

guides/source/active_job_basics.md

  • Active Job ガイドに新しく「Attributes」についてのセクション/説明が追加されています。
    • Active Job の標準的な使い方の流れ(キュー指定、引数、コールバックなど)の中に、「Attributes」という項目が組み込まれた形になっているはずです。
  • ガイドから Active Job Attributes のリファレンスへリンクされており、
    「まずガイドで概要をつかんでから、詳細は docs(RDoc/YARDコメント)を読む」という自然な導線ができています。
  • 具体的には:
    • Attributes の定義方法(attribute :foo, :bar など)
    • ジョブのインスタンス変数との違い/使いどころ
    • Attributes 付きでジョブを enqueue するパターン
      といった内容が、ガイドの中で触れられるようになっています。

  1. 影響範囲・注意点
  • この PR はコードの挙動にはほぼ影響しないドキュメント/サンプル修正です。
    • 既存の Active Job の実行パスや API には変更がなく、互換性への影響は基本的にありません。
  • ただし、今後 Active Job Attributes/Continuation を使おうとする人は、
    「Railsガイドおよびコメントの意図した使い方」に徐々に寄せていくことになるので、
    サンプルに引きずられる形で設計方針が変わる可能性はあります。
  • 既に Attributes を独自解釈で使っていた場合でも、この PR による破壊的変更はなく、
    ただ「公式推奨の使い方」がより明文化された、と捉えて問題ありません。

  1. 参考情報 (あれば)

#57496 Fix docs for ActiveSupport::SecurityUtils.fixed_length_secure_compare

マージ日: 2026/5/28 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    ActiveSupport::SecurityUtils.fixed_length_secure_compare の RDoc コメントが API ドキュメントに正しく表示されていなかった問題を、コメント位置を修正することで解消したPRです。機能や挙動は一切変えず、ドキュメント生成のためのメタ情報のみを調整しています。

  1. 変更内容の詳細(あればサンプルコードも含めて)
  • 対象: activesupport/lib/active_support/security_utils.rb
  • 目的:
    fixed_length_secure_compare の説明コメントが、Rails APIドキュメント上に表示されるようにすること。

Rails の RDoc/YARD では、「直前に書かれたコメント」がそのメソッドのドキュメントとして紐づきます。このPRでは:

擬似的なイメージ(実際のコードから簡略化):

ruby
module ActiveSupport
  module SecurityUtils
    # 固定長の2つの文字列を、タイミング攻撃耐性のある方法で比較します。
    # 例:
    #   ActiveSupport::SecurityUtils.fixed_length_secure_compare(a, b)
    #
    # 引数の長さが異なる場合は false を返します。
    # (コメント位置をこの定義の直前に移動)
    def self.fixed_length_secure_compare(a, b)
      # 実装は既存のまま
    end
  end
end

実際の差分は、コメントがどの def の直前にあるかを調整しただけで、ロジックの変更はありません。


  1. 影響範囲・注意点
  • 実行時の挙動・APIシグネチャ
    • メソッドの処理内容・引数・戻り値は一切変更されていません。
    • したがって、既存コードへの影響はゼロです。
  • 影響範囲
    • 影響するのは 生成されるドキュメント(Rails APIサイトやRDoc出力) のみです。
    • ドキュメント利用者にとっては、このメソッドの説明が読めるようになり、fixed_length_secure_compare の意図や使い方が分かりやすくなります。
  • 注意点
    • テストは不要と判断されており追加されていません(挙動変更がないため)。
    • CHANGELOG も更新されていません(ドキュメント修正のみのため、Rails の運用ポリシーに沿った対応)。

  1. 参考情報 (あれば)

#57489 Improve documentation/testing of abort in Action Mailer `before_action

マージ日: 2026/5/28 | 作成者: @bensheldon

  1. 概要 (1-2文で)
    Action Mailer の before_action コールバックで「送信を中断(abort)する」挙動について、ドキュメントが実装とずれていた部分を修正し、その挙動を保証するテストが追加されています。古い body メソッドに依存した誤った説明をやめ、現状の API に即した正しい書き方に更新した PRです。

  1. 変更内容の詳細

ドキュメント修正 (guides/source/action_mailer_basics.md)

  • Action Mailer のコールバック (before_action など) で「メールを中断する」方法の説明が更新されています。
  • 以前のドキュメントでは、すでに 2009 年に非推奨になっている body メソッドを前提とした説明が含まれており、現在の実装と齟齬がありました。
  • この PR で、以下の点が是正されています。
    • 「何をすれば送信処理が abort されるのか」を、現行のインターフェイスに沿って説明し直し。
    • 読み手が「body をいじる/body に依存する」ようなコードを書かないように、表現を整理。

PR テキストから推測される典型的な書き方イメージ:

ruby
class UserMailer < ApplicationMailer
  before_action :ensure_can_be_emailed

  def welcome_email(user)
    @user = user
    mail to: @user.email, subject: "Welcome"
  end

  private

  def ensure_can_be_emailed
    # 条件を満たさない場合にメール送信を中断
    throw :abort if @user.unsubscribed?
  end
end

上記のように、コールバック内で throw :abort することで、そのメールの送信処理全体が中止される、という形で説明が明確化されていると考えられます。

テスト追加

1) actionmailer/test/mailers/callback_mailer.rb

  • テスト用の CallbackMailer に、before_action で abort する振る舞いを持つメソッド/コールバックが追加されています。
  • 例: (実際のコードは多少異なる可能性がありますが、概ね以下のようなイメージです)
ruby
class CallbackMailer < ActionMailer::Base
  before_action :abort_sending, only: :aborted_mail

  def aborted_mail
    mail to: "test@example.com", subject: "Should not be delivered"
  end

  private

  def abort_sending
    throw :abort
  end
end

2) actionmailer/test/callbacks_test.rb

  • 上記 CallbackMailer の動きを検証するテストが追加されています。
  • 主な意図:
    • before_action 内で throw :abort が呼ばれた場合、メールが「生成・送信されない」こと。
    • ActionMailer::Base の既存のコールバックチェーン実装が、throw :abort を正しく扱えていること。
  • 具体的には:
    • CallbackMailer.aborted_mail.deliver_now / deliver_later 等を呼び出した結果として、ActionMailer::Base.deliveries にメールが追加されないこと、あるいは mail オブジェクトが nil もしくは送信されない状態であること等を assertion しています。

  1. 影響範囲・注意点
  • 機能追加や挙動変更ではなく、「ドキュメントの修正」と「テストの追加」が中心です。
  • すでに before_action 内で throw :abort を使って送信中断しているアプリケーションは、そのままの書き方で問題ありません。PR の目的は「以前から存在していた挙動を、ドキュメントとテストで正確にカバーする」ことです。
  • 逆に、古いドキュメントを見て body などの古い API に依存した中断ロジックを書いていた場合:
    • 今回のドキュメント更新を機に、「throw :abort による中断」に書き換えるのが推奨されます。
  • コールバックで throw :abort した場合:
    • 後続のフィルタやメール本体の処理が実行されないことに留意してください。
    • コールバック内で副作用(ログ出力、フラグ切り替え等)が必要な場合、throw :abort を呼ぶ位置・条件を明示的に設計しておくと安全です。

  1. 参考情報 (あれば)
  • 関連 PR: Action Mailer ドキュメントに以前手を入れていた PR: #47630
  • body 非推奨化の関連コミット:
    • 418c3f801c / 4964d3b02c (2009 年頃に Action Mailer の body が deprecated)
  • ガイド: Action Mailer Basics (edgeguides)
    • 本 PR マージ後、このガイドの「コールバック」「before_action / abort」に関する節が最新の内容になります。

#57495 Reject malformed Action Mailbox raw email params

マージ日: 2026/5/28 | 作成者: @afurm

  1. 概要 (1–2文で)
    Mailgun / Postmark / SendGrid の Action Mailbox ingress において、「生メール(raw email)」パラメータの型を検証し、文字列以外(例: 配列)が渡された場合に 422 Unprocessable Content を返すように修正した PR です。これにより、従来 TypeError になっていたケースが、他の不正入力と同様に適切にハンドリングされます。

  1. 変更内容の詳細

背景となる問題

  • 対象: Action Mailbox の以下の Ingress Controller
    • Mailgun: ActionMailbox::Ingresses::Mailgun::InboundEmailsController
    • Postmark: ActionMailbox::Ingresses::Postmark::InboundEmailsController
    • SendGrid: ActionMailbox::Ingresses::Sendgrid::InboundEmailsController
  • これらは、各サービスから Webhook で飛んでくる「生メール」を以下のパラメータで受け取ります:
    • Mailgun: body-mime
    • Postmark: RawEmail
    • SendGrid: email
  • もともと「値が必須」であることは前提にしていたものの、「型」が正しいか(文字列かどうか)はチェックしていませんでした。
  • その結果、何らかの理由で Array など「配列型」の値がここに入り込むと、後続の処理 (ActionMailbox::InboundEmail.create_and_extract_message_id!) 内でチェックサム抽出時に TypeError が発生してしまい、本来想定している「不正入力としての 422 応答」ではなく例外クラッシュのような扱いになっていました。

今回の修正ポイント

各 ingress controller で raw email パラメータが String であることを検証 し、そうでない場合は 422 を返すようにしました。

擬似コードで表すと、以下のようなイメージです(実際のコードは各コントローラごとに書かれていますが、ロジックは概ね共通):

ruby
def create
  raw_email = params["body-mime"] # Mailgun の場合の例

  # ここが今回追加されたロジックに相当
  unless raw_email.is_a?(String)
    head :unprocessable_entity
    return
  end

  # 正常な場合のみ ActionMailbox に処理を渡す
  ActionMailbox::InboundEmail.create_and_extract_message_id!(raw_email)
  head :ok
end

Postmark / SendGrid も同様で、それぞれ:

  • Postmark: params["RawEmail"] が String かチェック
  • SendGrid: params["email"] が String かチェック

テストの追加

各 ingress ごとに「raw email が不正(配列など)な場合に 422 を返す」ことを確認するテストが追加されています。

たとえば Mailgun 側テストのイメージ:

ruby
test "returns 422 for malformed raw email" do
  post ingress_mailgun_inbound_emails_url,
    params: { "body-mime" => ["not-a-string"] }

  assert_response :unprocessable_entity
end

Postmark / SendGrid も同様のテストが追加され、「配列型などの不正な raw email パラメータ → 422」という振る舞いが明示的に保証されるようになりました。

CHANGELOG の更新

actionmailbox/CHANGELOG.md に、この不具合修正が記載され、リリースノートから追跡できるよう更新されています。


  1. 影響範囲・注意点
  • 影響するのは Action Mailbox を Mailgun / Postmark / SendGrid ingress で利用しているアプリケーションです。
  • これまでは:
    • 不正な raw email(配列など)が来ると TypeError が発生し、スタックトレースが出たり、500 系エラーとして扱われる可能性がありました。
  • 今後は:
    • 同じケースで 422 Unprocessable Content を返す安定した挙動になります。
    • 他の「不正な受信者情報や envelope メタデータ」と同じコードパスで処理されるため、エラー挙動が一貫します。

アプリ側での注意点:

  • Webhook のレスポンスコードに応じた リトライ制御やアラート連携を行っている場合、
    • 以前: サーバ内部エラー(500 系)として扱っていたかもしれない
    • 今回以降: 422 でクライアント側(送信側)起因の不正データとみなされる
      ため、アラート閾値やリトライポリシーを 422 向けに見直す必要があるかもしれません。
  • raw email パラメータに対して独自ミドルウェア等で加工を行っている場合、誤って配列・ハッシュを代入してしまうと、今後は 422 が返るようになります。
    通常は外部サービス(Mailgun/Postmark/SendGrid)が正しく送ってくるため問題ありませんが、中継処理を挟んでいる場合はご注意ください。

  1. 参考情報 (あれば)
  • 対応 Issue: #57494
  • 対応 PR: #57495 「Reject malformed Action Mailbox raw email params」
  • 対象ファイル(主なもの):
    • actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb
    • actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb
    • actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb
    • 各種テスト:
      • actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
      • actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb
      • actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb

#57110 Reduce number of queries during MariaDB column introspection

マージ日: 2026/5/28 | 作成者: @ilianah

  1. 概要 (1-2文で)
    MariaDB 接続時のカラム情報取得処理(カラム introspection)において、無駄な SHOW CREATE TABLE クエリが大量に発行されていた問題を、クエリ回数を最小限に抑える形で修正した PR です。MariaDB の「関数っぽいデフォルト値」を検出するロジックを集約し、非 MariaDB 環境では追加クエリが発生しないようになりました。

  1. 変更内容の詳細

背景となる問題

  • #44654 で「MariaDB における関数デフォルトの検出」が修正された際、AbstractMysqlAdapter#new_column_from_field 内に MariaDB 向けのチェックが追加された。
  • その結果、カラムごとに SHOW CREATE TABLE を実行する実装になっており、
    • スキーマのカラム数が多いアプリケーションでは、columns 呼び出し 1 回につき大量の SHOW CREATE TABLE クエリが発行される
    • MariaDB ではない(MySQL 等の)接続でも不要なフックが走る
      というパフォーマンス上の問題が出ていた。

今回の方針

  • MariaDB 固有の「関数デフォルト検出」を new_column_from_field から切り離し、より高いレイヤである column_definitions に移動。

  • フローとしては次のように変更:

    1. まず SHOW FULL FIELDS を実行して、テーブルの各カラムの情報を取得。
    2. 接続先が MariaDB の場合のみ、取得したフィールドの Default を走査し、関数呼び出しのように見える値があるかを確認。
      • 例: CURRENT_TIMESTAMP, uuid(), now() など、xxx(...) の形や、MariaDB で関数扱いされる既知の値。
    3. 少なくとも 1 つでも「関数っぽい Default」が見つかった場合に限り、テーブルごとに 1 回だけ SHOW CREATE TABLE を発行。
    4. その SHOW CREATE TABLE の結果を解析し、該当するカラムを DEFAULT_GENERATED としてメタデータにマークする。
    5. こうしてマークされたカラムは、既に MySQL 側で使われている new_column_from_fieldDEFAULT_GENERATED 分岐に乗るようになり、MariaDB でも正しく「関数デフォルト」として扱われる。

コード上の主な変更点

1) abstract_mysql_adapter.rb

AbstractMysqlAdapter に、MariaDB 専用のデフォルト値判定ロジックと、それを踏まえたカラム定義処理が追加されています。おおまかなイメージ:

ruby
def column_definitions(table_name)
  fields = exec_query("SHOW FULL FIELDS FROM #{quote_table_name(table_name)}")
  if mariadb?
    # 1. Default が関数っぽいフィールドがあるか確認
    if fields.any? { |f| function_like_default?(f["Default"]) }
      # 2. 必要な場合だけ SHOW CREATE TABLE を 1 回だけ実行
      table_ddl = exec_query("SHOW CREATE TABLE #{quote_table_name(table_name)}").first["Create Table"]
      # 3. DDL から DEFAULT_GENERATED なカラムを特定し、fields にメタデータを付与
      mark_generated_defaults_from_ddl!(fields, table_ddl)
    end
  end

  fields.map { |field| new_column_from_field(field) }
end

ポイント:

  • MariaDB でない場合 (mariadb? が false) には、この判定はそもそも実行されない。
  • 「関数っぽい Default」を持つカラムが 0 件であれば SHOW CREATE TABLE 自体をスキップする。

2) mysql/schema_statements.rb

元々ここにあった MariaDB 向けの関数デフォルト検出ロジックが整理・削減され、AbstractMysqlAdapter 側に集約されています。
これにより、MariaDB と MySQL で「DEFAULT_GENERATED を用いた関数デフォルトの扱い」を共通化しつつ、MariaDB 特有のクエリは最小限に抑える構成になりました。

3) テスト (migration_test.rb)

  • 前述の挙動変更に合わせて、マイグレーション関連テストの期待値が微調整されています。
  • 主に「カラムのデフォルトが関数の場合に、スキーマ上どう解釈されるか」に関する部分が、今回の新しい経路(DEFAULT_GENERATED マーク → new_column_from_field の MySQL 共通ルート)に沿うように更新されています。

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

    • MariaDB 接続時columns / スキーマ introspection 処理が主対象。
    • MySQL / 他 DB アダプタには影響しない設計(MariaDB 判定を条件分岐している)になっている。
  • 性能面の影響:

    • 以前 (#44654 後) は「カラムごとに SHOW CREATE TABLE」だったケースが、「テーブルごとに最大 1 回」に減少。
    • カラム数が多いテーブルを多数持つアプリでは、スキーマロード・マイグレーション・コンソール起動時などに発行されるクエリ数が大幅に削減される可能性が高い。
  • 機能面の影響:

    • MariaDB 上で「関数をデフォルト値に持つカラム」を introspect したとき、
      • 以前と同様、関数デフォルトとして正しく扱われる(#44654 の修正は維持)。
      • 実装上の経路は変わったが、最終的な ActiveRecord::ConnectionAdapters::Column の属性は互換になるよう配慮されている。
  • 注意点:

    • MariaDB の SHOW CREATE TABLE 出力フォーマットに依存したパース・マッピング処理を行うため、
      • 将来 MariaDB 側で DDL 出力フォーマットが大きく変わると、DEFAULT_GENERATED マーク付けがずれる可能性はある。
    • ただし、従来も同様に MariaDB 固有の挙動に依存していたため、リスクは増えてはいない。

  1. 参考情報 (あれば)
  • 関連PR:
    • #44654 — MariaDB の関数デフォルト検出を最初に導入した PR。
      本 PR は、その実装によるクエリ数増大の副作用を解消する位置づけ。
  • 実装上のキーワード:
    • SHOW FULL FIELDS FROM <table>
    • SHOW CREATE TABLE <table>
    • DEFAULT_GENERATED メタデータ
    • AbstractMysqlAdapter#column_definitions
    • MariaDB 判定 (mariadb? など)

MariaDB を使っていてスキーマが大きいアプリケーションでは、アップデートにより introspection 時のクエリ数削減・レスポンス改善が期待できます。


#57470 Fix number_to_delimited mangling non-finite floats

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

  1. 概要 (1-2文で)
    ActiveSupport::NumberHelper.number_to_delimitedFloat::INFINITY などの非有限浮動小数点数を不正に区切ってしまうリグレッションを修正し、非有限値はそのまま文字列表現を返すようにした PR です。これにより "Infinity""In,fin,ity" になる問題が解消されます。

  1. 変更内容の詳細

問題点

number_to_delimited は数値を3桁ごとに区切るヘルパですが、最近入ったパフォーマンス改善の影響で、Float::INFINITY 等に対して以下のような誤った結果を返していました。

ruby
ActiveSupport::NumberHelper.number_to_delimited(Float::INFINITY)
# => "In,fin,ity"   💀

ActiveSupport::NumberHelper.number_to_delimited(-Float::INFINITY)
# => "-In,fin,ity"

ActiveSupport::NumberHelper.number_to_delimited(Float::NAN)
# => "NaN"          # たまたま3文字なので崩れなかった

原因は、NumberToDelimitedConverter に導入された「手動スライスによる高速パス」が Float::INFINITY.to_s # => "Infinity" を「数字列」と見なして右から3文字ずつ区切ってしまったためです。
以前は正規表現ベースの実装で、Infinity は数字を含まないためマッチせず、そのまま返されていました。

修正内容

NumberToDelimitedConverter#parts の最初で、「有限でない数値」を特別扱いし、区切り処理に入らないようにしました。

差分(概略):

diff
def parts
+  return [number.to_s] if number.respond_to?(:finite?) && !number.finite?
+
  left, right = number.to_s.split(".")
  if delimiter_pattern
    # 既存の区切り処理
  else
    # 高速パス (手動スライス)
  end
end

ポイント:

  • number.respond_to?(:finite?) && !number.finite?
    • Float など finite? を持つオブジェクトで、かつ有限でないもの (Infinity, -Infinity, NaN) を検出。
  • その場合は number.to_s をそのまま 1 要素の配列で返し、最終的な parts.join で区切りなしの文字列が得られる。

修正後の挙動:

ruby
ActiveSupport::NumberHelper.number_to_delimited(Float::INFINITY)
# => "Infinity"

ActiveSupport::NumberHelper.number_to_delimited(-Float::INFINITY)
# => "-Infinity"

ActiveSupport::NumberHelper.number_to_delimited(Float::NAN)
# => "NaN"

これは既に number_to_phone(Float::INFINITY) # => "Infinity" が行っている挙動と整合的です。

テスト・ドキュメント

  • activesupport/test/number_helper_test.rb に、Float::INFINITY / -Float::INFINITY / Float::NAN に対する期待値を確認するテストを追加。
  • activesupport/CHANGELOG.md に本バグと修正内容を追記。

  1. 影響範囲・注意点
  • 影響範囲
    • ActiveSupport::NumberHelper.number_to_delimited(およびそれを内部的に利用しているヘルパ)で、Float::INFINITY / -Float::INFINITY / Float::NAN を扱うケースに影響します。
    • 以前(リグレッション前)の挙動に戻る形で、Infinity / -Infinity / NaN をそのまま返すようになります。
  • 互換性
    • 直近数リリース(高速化導入後)でだけ "In,fin,ity" のような壊れた文字列が返っていたため、それに依存しているコードがある可能性はほぼありません。
    • 数値フォーマット周りで「非有限値をどう扱うか」は API デザイン上の論点ですが、この PR は既存の number_to_phone に合わせただけで、新たな仕様変更ではありません。
  • 他 API との整合性
    • number_to_human / number_to_human_sizeFloat::INFINITY に対して FloatDomainError を投げ続けており、この PR では変更されていません(追って検討する候補として #57451 で言及)。

  1. 参考情報 (あれば)
  • このバグを引き起こしたパフォーマンス関連コミット:
    • 2d485aecf5 (2026-03-04, CVE-2026-33169): 3文字ごとの手動スライスによる高速パス導入。
    • 33fbedb1b1 (2026-03-26): delimiter_pattern のデフォルトを nil に変更し、高速パスをデフォルト経路に。
  • 関連 PR:
    • #57451: 別のコンバータにおける FloatDomainError 対応 (number_to_human 系) に関する議論。

#57451 Format non-finite numbers consistently in NumberHelper significant mode

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

  1. 概要 (1-2文で)
    ActiveSupport::NumberHelpernumber_to_rounded などで significant: true を指定した際、Infinity / -Infinity / NaN を渡すと FloatDomainError で落ちていた不具合を修正し、非 significant モードと同様に "Inf" / "-Inf" / "NaN" として一貫してフォーマットされるようにした PR です。
    これにより、同じヘルパー内で significant オプションの有無による挙動の不整合が解消されています。

  1. 変更内容の詳細

問題のあった挙動

もともと significant: false(デフォルト)では、無限大や NaN をよしなに文字列化していました:

ruby
ActiveSupport::NumberHelper.number_to_rounded(Float::INFINITY, precision: 3)
# => "Inf"

ActiveSupport::NumberHelper.number_to_rounded(Float::NAN, precision: 3)
# => "NaN"

ところが significant: true を加えると同じ値で例外が発生していました:

ruby
ActiveSupport::NumberHelper.number_to_rounded(Float::INFINITY, precision: 3, significant: true)
# => FloatDomainError: Infinity

この例外は RoundingHelper を経由する以下のヘルパーにも波及します:

  • number_to_rounded
  • number_to_percentage
  • number_to_currency
  • number_to_delimited

典型例:

ruby
total   = 100.0
seconds = 0
rate    = total / seconds               # => Infinity

number_to_rounded(rate, precision: 3, significant: true)
# 期待: "Inf"
# 実際: FloatDomainError

原因

例外は RoundingHelper#digit_count 内の Math.log10 呼び出しで発生していました。

ruby
def digit_count(number)
  return 1 if number.zero?
  (Math.log10(number.abs) + 1).floor
  # Math.log10(Infinity) #=> Infinity
  # Infinity.floor       #=> FloatDomainError
end

digit_count は以下の場面で使われます:

  • significant: true && precision > 0 のときの absolute_precision 計算
  • NumberToRoundedConverter#convert 内で有効桁数に応じて precision を調整する処理

つまり、Infinity / NaN を significant モードで扱うと必ずここを通り、Math.log10 が例外の原因となっていました。

修正内容

digit_count に「有限数かどうか」のチェックを追加し、非有限値(Infinity, -Infinity, NaN)の場合は 1 桁として扱うようにしました。

差分イメージ(説明用・概念的なコード):

ruby
def digit_count(number)
  return 1 if number.zero?
  return 1 unless number.respond_to?(:finite?) && number.finite?
  (Math.log10(number.abs) + 1).floor
end

ここで 1 を返す理由・妥当性:

  • absolute_precision はおおむね precision - digit_count(number) のように使われるため、digit_count == 1 なら precision - 1 として自然な形で処理が継続できる

  • BigDecimal("Infinity").round(n) はどんな n でも Infinity のまま変化しない

  • その後の NumberToRoundedConverter#convert にはすでに

    ruby
    if rounded_number.finite?
      # 通常のフォーマット
    else
      "%f" % rounded_number
    end

    という有限値チェックがあり、ここで "Inf", "-Inf", "NaN" にフォーマットされる設計になっている

  • もともと 0 の場合も digit_count1 を返しており、その分岐は今回の変更より前に評価されるため、以前正常に動いていたケースはそのまま維持される

結果として、「非有限値をうまくやり過ごして既存の分岐まで処理を届ける」ための、最小限で局所的な防御を追加した形になっています。

テスト・検証

activesupport/test/number_helper_test.rb に以下のようなテストが追加されています(内容の要旨):

  • number_to_roundedInfinity, -Infinity, NaN を渡し、significant: true でそれぞれ "Inf", "-Inf", "NaN" になること
  • 同じく number_to_percentage, number_to_currency でも "Inf%", "$Inf" など期待される文字列になること

全体として:

  • 23 テスト, 843 アサーション, 失敗・エラーなし

activesupport/CHANGELOG.md も更新され、挙動変更(バグ修正)が明記されています。


  1. 影響範囲・注意点

影響を受けるヘルパー(挙動が改善されるもの)

以下で significant: true を指定した際に、Infinity / -Infinity / NaN を渡しても例外が出ず、非 significant モードと同じ文字列を返すようになります。

  • number_to_rounded
  • number_to_percentage
  • number_to_currency
  • number_to_delimited

期待される新しい挙動の例:

ruby
number_to_rounded(Float::INFINITY, precision: 3, significant: true)
# => "Inf"

number_to_percentage(Float::INFINITY, precision: 3, significant: true)
# => "Inf%"

number_to_currency(Float::INFINITY, precision: 3, significant: true)
# => "$Inf"

number_to_rounded(Float::NAN, precision: 3, significant: true)
# => "NaN"

影響を受けないヘルパー(依然として例外を出しうるもの)

  • number_to_human
  • number_to_human_size

これらは RoundingHelper 経由ではなく、独自の calculate_exponentexponent の中で Math.log10 / Math.log を直接呼び出しているため、Infinity を渡すと引き続き FloatDomainError になる可能性があります。
この PR はあくまで RoundingHelper 経路の修正に限定されており、number_to_human 系の扱い("Inf" / "NaN" が人間にとって読みやすいかどうか等)は別 PR で検討する前提になっています。

後方互換性

  • 有限値に対するフォーマットロジック(桁数の計算や rounding 動作)は変えていないため、通常の数値入力での出力形式は変わりません。
  • 無限大や NaN については、これまで「例外が出ていた」ケースが「文字列を返す」ようになるだけなので、既存コードが rescue していた場合でも後方互換性上の問題はほぼありません(ログに出る内容等だけ注意)。

  1. 参考情報 (あれば)
  • 本 PR で参考にされている旧 PR:
    • 2016 年の試み #25946
      • number_to_rounded / number_to_percentage / number_to_human を一気に直そうとして、number_to_human"Inf" / "NaN" 表現が人間向けでないとの指摘でクローズ
      • 今回は RoundingHelper 経路(= number_to_rounded / _percentage / _currency / _delimited)に限定することで、その批判ポイントを避け、同一ヘルパー内の整合性回復に scope を絞っています。
  • 実装設計上のポイント:
    • digit_count は初期実装時から Math.log10(number.abs).floor ベースだったが、NumberToRoundedConverter 側にはすでに finite? チェックがあり、設計的には非有限値もサポートする意図があったと推測されるため、その意図に合わせてガードを追加した形になっている。

#57492 Reject malformed Action Mailbox original recipients

マージ日: 2026/5/28 | 作成者: @afurm

  1. 概要 (1-2文で)
  • Action Mailbox の Mailgun/Postmark インバウンド受信処理で、オプションの「元の宛先」パラメータが不正フォーマットだった場合に TypeError で落ちていた問題を修正し、422 Unprocessable Content を返して安全に拒否するようにした PR です。
  • これにより、Mailgun の recipient と Postmark の OriginalRecipient が壊れていてもアプリ側が例外で落ちず、クライアントに正しいエラーを返せるようになります。

  1. 変更内容の詳細

背景

  • Mailgun / Postmark は、受信メールを Webhook で Rails に渡す際に「元々の宛先」を表す値を送ることがあります:
    • Mailgun: recipient パラメータ
    • Postmark: OriginalRecipient パラメータ
  • Action Mailbox ではこれらを使って、RAW メールに X-Original-To ヘッダを付ける処理をしていました。
  • しかし、これらの値が「文字列ではない」「文字列として扱えない」などで不正な場合、そのまま String#prepend に渡されて TypeError が発生していました。

この PR の主な対応

1) Mailgun 受信コントローラのバリデーション追加

対象ファイル:
actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb

  • もともとやっていたこと (概念的には):
ruby
raw_email = params.require(:body_mime)
if (original_recipient = params["recipient"]).present?
  raw_email.prepend("X-Original-To: #{original_recipient}\n")
end
  • PR で追加された点:
    • recipient が「to_s した結果をメールアドレスとして扱える / ヘッダとして妥当」かを事前チェック。
    • 不正な場合は 422 Unprocessable Content を返し、String#prepend を呼ばない。

イメージとしては以下のような流れになります (実装の雰囲気):

ruby
def create
  original_recipient = params[:recipient]

  if original_recipient.present? && !valid_original_recipient?(original_recipient)
    head :unprocessable_content # 422
    return
  end

  raw_email = params.require(:body_mime)

  if original_recipient.present?
    raw_email.prepend("X-Original-To: #{original_recipient}\n")
  end

  # 既存のメール処理へ…
end

private

def valid_original_recipient?(value)
  value.is_a?(String) && value.present?
  # 実際にはもっと厳密 or 既存の Action Mailbox のバリデーションに沿ったチェック
end

※ 実際のコードは Rails 内部のヘルパや共通メソッドを使っている可能性がありますが、概ねこのように「事前チェック → NG なら 422」という構造になっています。

2) Postmark 受信コントローラのバリデーション追加

対象ファイル:
actionmailbox/app/controllers/action_mailbox/ingresses/postmark/inbound_emails_controller.rb

  • もともとの処理イメージ:
ruby
raw_email = params.require(:raw_email)
if (original_recipient = params["OriginalRecipient"]).present?
  raw_email.prepend("X-Original-To: #{original_recipient}\n")
end
  • Mailgun と同様、OriginalRecipient が不正な場合に 422 を返して処理を中断するチェックが追加されています。
  • バリデーションの方針は Mailgun と同様で、「String#prepend に安全に渡せるか」を最低限保証するものです。

3) テストの追加

対象ファイル:

  • actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
  • actionmailbox/test/controllers/ingresses/postmark/inbound_emails_controller_test.rb

主なテスト内容:

  • 「malformed recipient / malformed original recipient」という名前のテストが追加されており、以下を検証:
    • 不正な recipient/OriginalRecipient を含むリクエストを送る。
    • レスポンスが 422 Unprocessable Content になること。
    • サーバ側で TypeError などの例外が発生せず、エラーとして正しく終了すること。

4) CHANGELOG の更新

対象ファイル:
actionmailbox/CHANGELOG.md

  • Action Mailbox の変更点として「Malformed な original recipient を拒否するようになった」旨が追記されています。
  • 将来的な Rails バージョンアップ時に、この挙動変更がリリースノートとして分かるようになります。

  1. 影響範囲・注意点
  • 対象: Action Mailbox を使っており、Mailgun または Postmark の Ingress を利用しているアプリケーション
  • 今まで:
    • Mailgun/Postmark 側のバグや悪意ある入力により recipient / OriginalRecipient が壊れていた場合、
    • Rails 側で TypeError が発生 → 500 エラーになる、もしくはジョブが落ちる、ログが汚れる、などの不安定な挙動となる可能性がありました。
  • これから:
    • そうした不正な値は検出され、422 Unprocessable Content として明示的に拒否されます。
    • アプリ側は「そのメールは受理されなかった」ことを前提に扱う必要がありますが、これはもともと正常に処理できなかったケースなので仕様として自然です。
  • ログ・監視上の注意:
    • この変更後、Mailgun/Postmark 経由の Webhook で 422 応答が出る可能性があります。
    • ログやエラーレポートで 500 が減って 422 が増える可能性があるため、監視閾値やアラート設定をしている場合は挙動の変化に注意してください。
  • 互換性:
    • 正常な recipient / OriginalRecipient を送っている限り、挙動は従来通りで互換性があります。
    • エラーコードが「500 から 422 に変わる」程度なので、Webhook 側で応答コードに依存した独自リトライやハンドリングをしている場合は確認するとよいです。

  1. 参考情報 (あれば)
  • 関連 Issue: #57491
    「Mailgun / Postmark の original recipient が壊れていると Action Mailbox で TypeError が出る」問題報告。
  • PR タイトル: Reject malformed Action Mailbox original recipients (#57492)
  • 影響するプロバイダ:
    • Mailgun Ingress: ActionMailbox::Ingresses::Mailgun::InboundEmailsController
    • Postmark Ingress: ActionMailbox::Ingresses::Postmark::InboundEmailsController
  • 追加のテストコマンド例 (PR 説明より):
    • Mailgun の malformed recipient テストのみ:
      • cd actionmailbox && bin/test test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb -i "/malformed recipient/"
    • Postmark の malformed original recipient テストのみ:
      • cd actionmailbox && bin/test test/controllers/ingresses/postmark/inbound_emails_controller_test.rb -i "/malformed original recipient/"

#57477 Refactor ActiveJob::TestHelper to no longer walk descendants

マージ日: 2026/5/28 | 作成者: @byroot

  1. 概要 (1-2文で)
    ActiveJob::TestHelper の内部実装がリファクタされ、テスト時の設定リセットで ActiveJob::Base の子クラスを総なめ(descendants walk)しなくてよくなりました。これにより、テスト teardown が高速かつ安定になり、関連する不具合 (#57440, #57441) が修正されています。

  1. 変更内容の詳細

何をやめたのか

これまで ActiveJob::TestHelper は、class_attribute を使って ActiveJob::Base 直下のクラスに設定を伝播していました。
class_attribute の値をテストごとに上書きすると、teardown 時に「どのクラスに変更が入ったか」を把握するために ActiveJob::Base.descendants を走査する必要がありました。

  • 問題点:
    • descendants の走査はクラス数に比例して O(n) でコストがかかる
    • 再読み込みや autoload のタイミングによって不安定になることがある
    • 一部ケースで設定がきちんとリセットされず、バグ報告 (#57440, #57441) の原因になっていた

何に置き換えたのか

この PR では「全子孫クラスを探しに行く」というアプローチをやめて、「どの値をどう上書きしたか」を内部のデータ構造に直接記録する方式に変えています。

  • ポイント:
    • ActiveJob::TestHelper が使う設定のオーバーライド情報を、専用の内部ハッシュやセットに保持
    • teardown 時には、class_attribute を巻き戻す代わりに、これらの内部データ構造を O(1) でクリアするだけで済む
    • つまり「グローバルな一括リセット」→「テストヘルパー内部の局所状態のクリア」に変わったイメージ

具体的な API の振る舞いや public インターフェイス(例: perform_enqueued_jobs, perform_enqueued_at_jobs, assert_enqueued_jobs など)が変わったわけではなく、内部的な状態管理の方法のみが変更されています。

Active Storage のテストの追加

変更ファイルに railties/test/application/active_storage/analyzers_integration_test.rb が含まれており、ここにテストが追加されています (+9 行)。

  • 追加されたのは、ActiveJob::TestHelper の新しい実装により、Active Storage のアナライザーがジョブ実行まわりで期待通り動くかを確認するためのテスト
  • ActiveStorage の integration test で
    • ActiveJob のテストヘルパーによるキュー実行/管理
    • その teardown 時の状態リセット などが正常に機能することを保証する意図があります。

  1. 影響範囲・注意点
  • 影響するのは主にテスト実行時のみ
    ActiveJob::TestHelper の内部実装変更なので、本番コード(ジョブの enqueue や perform)には直接の影響はありません。

  • パブリック API は原則そのまま
    既存の次のようなコードは、基本的にそのまま動作します。

    ruby
    class MyJobTest < ActiveJob::TestCase
      include ActiveJob::TestHelper
    
      def teardown
        clear_enqueued_jobs
        clear_performed_jobs
      end
    end

    もし内部実装に依存するメタプログラミングをしていなければ、互換性問題はほぼありません。

  • パフォーマンスと安定性の向上

    • 大量の Job クラスを定義しているプロジェクトで、テストの teardown が速くなる可能性があります。
    • クラス再読み込みや並行テスト環境で起こりうる、descendants 走査に起因する妙な不具合(設定が残留する、特定ジョブのテストだけ落ちる等)が減ることが期待されます。
  • ActiveJob::Base.descendants への依存が減る

    • ActiveJob::TestHelper 内部で descendants に依存しなくなったため、Rails の autoload / Zeitwerk reload 周りと競合しにくくなります。
    • もし自分のテストヘルパーで「ActiveJob::TestHelper の teardown を前提に descendants をいじる」ような特殊なことをしている場合は、挙動が変わる可能性があります(かなりニッチなケースです)。

  1. 参考情報 (あれば)

#57481 Simplify more activerecord tests with NotificationAssertions helpers

マージ日: 2026/5/28 | 作成者: @Edilbek

  1. 概要 (1-2文で)
    Active Record のテストコードで、ActiveSupport::Notifications.subscribe を直接使っていた箇所の多くを、NotificationAssertionscapture_notifications などのヘルパーに置き換えたリファクタリングです。挙動の変更はなく、テストの可読性・メンテナンス性の向上のみを目的としています。

  1. 変更内容の詳細

全体方針

  • 既存テストでは、通知の検証に以下のような「手書き subscribe パターン」が多く使われていました:
    • ActiveSupport::Notifications.subscribe("sql.active_record") で購読
    • ブロック内でフラグを立てたり (asserted = true)、直接 assert_equal 等を実行
    • ensureunsubscribe する、または subscribed を使う
  • これを、NotificationAssertions モジュールのヘルパー (特に capture_notifications) に置き換えています。
  • 例外的に、非同期スレッドから発火する通知を検証するテストでは、従来の subscribe を維持しています (subscribed がスレッドローカルであり、クロススレッドの通知を確実に拾えないため)。

asynchronous_queries_test.rb の変更

対象: Active Record の非同期クエリ (load_async など) に関するテスト。

  • 置き換え前(イメージ):

    ruby
    asserted = false
    subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
      event = ActiveSupport::Notifications::Event.new(*args)
      if event.payload[:name] == "User Load"
        asserted = true
        assert_in_delta 0.1, event.duration
      end
    end
    
    begin
      User.load_async.to_a
    ensure
      ActiveSupport::Notifications.unsubscribe(subscriber)
    end
    
    assert asserted
  • 置き換え後(イメージ):

    ruby
    events = capture_notifications("sql.active_record") do
      User.load_async.to_a
    end
    
    user_events = events.select { _1.payload[:name] == "User Load" }
    assert_not_empty user_events
    assert_in_delta 0.1, user_events.first.duration

ポイント

  • asserted フラグや ensure での unsubscribe が不要になり、テストが「何を検証しているのか」が読みやすくなっています。
  • capture_notifications は、指定したイベント名の通知を配列として返すため、後からフィルタして通常のオブジェクトに対するアサーションと同じ感覚で書けます。

connection_pool_test.rb の変更

対象: コネクションプール関連のテスト。

  • ActiveSupport::Notifications で発火される、例えば sql.active_record やプール関連のイベントに対するテストを capture_notifications ベースに移行。

  • 典型的なパターン:

    置き換え前:

    ruby
    asserted = false
    ActiveSupport::Notifications.subscribed(->(*args) {
      event = ActiveSupport::Notifications::Event.new(*args)
      asserted = true if event.payload[:connection_id] == connection.object_id
    }, "sql.active_record") do
      # テスト対象処理
    end
    
    assert asserted

    置き換え後:

    ruby
    events = capture_notifications("sql.active_record") do
      # テスト対象処理
    end
    
    assert events.any? { _1.payload[:connection_id] == connection.object_id }

ポイント

  • ActiveSupport::Notifications.subscribed の入れ子構造がなくなり、テストの main path に集中しやすくなっています。
  • 失敗時も、events の中身をデバッグしやすくなります(どの通知も飛んでいないのか、条件に合わないのかが切り分けやすい)。

relation/load_async_test.rb の変更

対象: Relation#load_async 関連のテスト。

  • PR 説明に明記されている通り、非同期スレッド側で発火する通知を対象にしているテストについては、引き続き 直接 ActiveSupport::Notifications.subscribe を使用 しています。

    理由:

    • capture_notifications の内部実装は ActiveSupport::Notifications.subscribed を使っており、これはスレッドローカル。
    • そのため、テストスレッド以外(非同期クエリスレッド)で出る sql.active_record イベントを確実に取れない。
  • 一方で、テストスレッド上で通知が発火するケース(例: wait_for_async_query 後に to_a することでロード完了時に通知が出るようなケース)は capture_notifications に移行。

    イメージ:

    ruby
    events = capture_notifications("sql.active_record") do
      relation = User.all.load_async
      relation.wait_for_async_query
      relation.to_a # このあたりでテストスレッド上で通知が出る
    end
    
    assert events.any? { _1.payload[:name] == "User Load" }

ポイント

  • スレッド境界をまたぐ通知テストは subscribe を維持し、そうでない箇所だけ capture_notifications に寄せており、ヘルパー導入に伴うテスト信頼性の低下を避けています。
  • 「どこから発火する通知なのか(どのスレッドか)」を意識してヘルパー適用範囲を調整しているのがポイントです。

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

    • 変更は activerecord/test/cases 以下の 3 ファイルのみで、本番コードには一切変更なし。
    • テストの書き方が変わっただけなので、仕様・パブリック API・パフォーマンスへの影響はありません。
  • 注意点 (テストを書く側向け):

    • capture_notificationsActiveSupport::Notifications.subscribed ベースであり、スレッドローカル であることを意識する必要があります。
      • 非同期処理で別スレッドから発火する通知をテストしたい場合は、従来どおり ActiveSupport::Notifications.subscribe を使うべきです。
    • 通知テストを新たに書く際は、以下を目安に選ぶとよいです:
      • 同期処理 or 同一スレッドの通知 → capture_notifications 推奨
      • 別スレッド、バックグラウンドジョブなど → 手動 subscribe/unsubscribe or 専用ラッパー

  1. 参考情報 (あれば)
  • 前提 PR: #57320
    NotificationAssertions ヘルパーの導入・適用を始めた PR。今回の PR はそのフォローアップで、残りの activerecord テストへの適用を進めたものです。

  • NotificationAssertions の代表的な使い方(概念的な例):

    ruby
    events = capture_notifications("sql.active_record") do
      User.first
    end
    
    sqls = events.map { _1.payload[:sql] }
    assert_includes sqls, "SELECT \"users\".* FROM \"users\" ORDER BY \"users\".\"id\" ASC LIMIT ?"

この PR を踏まえると、新規の通知検証テストはまず NotificationAssertions を使うことを検討し、非同期・マルチスレッドなケースのみ旧来の subscribe パターンを残す、という方針が推奨されます。


#57485 Reject malformed Mailgun signatures

マージ日: 2026/5/28 | 作成者: @afurm

  1. 概要 (1-2文で)
    Mailgun からの Webhook 受信(Action Mailbox Mailgun ingress)で、署名パラメータ signature の形が不正な場合に、secure_compare 実行前に認証エラーとして 401 を返すようにした修正です。これにより、配列などの想定外な形で signature が送られてきたときに例外で落ちるのではなく、きちんと認証失敗として扱われるようになります。

  1. 変更内容の詳細

何を直したか

既存コードは「Mailgun の署名パラメータ signature は必ず文字列」という前提で、次のような流れで処理していました:

ruby
# イメージ(実際のコードから要約)
if ActiveSupport::SecurityUtils.secure_compare(provided_signature, expected_signature)
  # 認証成功
else
  head :unauthorized
end

ところが、リクエストのパラメータ解釈によって params[:signature] が配列(Array)など、想定外の型になるケースがあり、その場合 secure_compare が内部で型エラーを起こして例外が発生していました。本来は 401 Unauthorized を返したいのに、アプリケーションエラーになってしまうのが問題です。

具体的な修正内容

  1. signature が文字列かを明示的にチェック

actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb にて、secure_compare を呼ぶ前に、signature が String であることを確認する条件が追加されています。

イメージとしては次のような形になります(実際のコードから簡略化):

ruby
provided_signature = params[:signature]

unless provided_signature.is_a?(String)
  # 文字列でなければ認証失敗として扱う
  head :unauthorized and return
end

if ActiveSupport::SecurityUtils.secure_compare(provided_signature, expected_signature)
  # 認証成功の処理
else
  head :unauthorized
end

元々は is_a?(String) のような型チェックがなく、配列が来たときに secure_compare 内部でエラーになっていましたが、それを防いでいます。

  1. 回帰テストの追加

actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb に、署名パラメータが配列のケースを再現するテストが追加されています。

ざっくりとしたテストのイメージ:

ruby
test "rejects requests with array-shaped signature" do
  post action_mailbox_ingress_mailgun_url,
    params: { signature: ["not-a-string"], # ← わざと配列にする
              ...他の必須パラメータ... }

  assert_response :unauthorized
end

これにより、今後の変更で同様の不具合が再発してもテストで検知できるようになります。

  1. CHANGELOG の更新

actionmailbox/CHANGELOG.md に、Mailgun ingress の署名検証まわりが修正されたことが追記されています。
アプリケーションの観点からは挙動の「安定性」が増した変更であり、外から見える仕様としては「不正な signature 形状でも 401 が返る」という改善になります。


  1. 影響範囲・注意点
  • 対象機能
    • Action Mailbox の Mailgun ingress (ActionMailbox::Ingresses::Mailgun::InboundEmailsController) の認証処理のみが対象です。
  • 本番挙動への影響
    • 今まで「例外になって 500 系エラーやアプリケーションエラーとして扱われていたケース」が、「401 Unauthorized として正常に拒否される」挙動になります。
    • Mailgun 側の正しい実装(signature が常に文字列)で運用している限り、通常系の挙動は変わりません。
  • セキュリティ面
    • 予期しないパラメータ形状でアプリケーションが落ちる穴を塞いでおり、堅牢性が向上しています。
    • 型を確認してから secure_compare するのは一般的なベストプラクティス(タイミング攻撃対策関数に不正な型を渡さない)にも沿っています。
  • 移行・対応の必要性
    • 既存のアプリケーションコードに変更を加える必要は基本的にありません。
    • 独自に Mailgun ingress をラップしていたり、params[:signature] を上書きしているような特殊な実装がある場合は、一応ログを見て 401 が増えていないか確認すると安心です。

  1. 参考情報 (あれば)
  • 対応している Issue: #57484 — 「Mailgun ingress が配列型の signature で例外を投げる」旨の報告に対する修正。
  • 関連クラス:
    • ActionMailbox::Ingresses::Mailgun::InboundEmailsController
    • ActiveSupport::SecurityUtils.secure_compare
  • テストと静的解析:
    • cd actionmailbox && bin/test test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
    • bundle exec rubocop actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb

#57087 Only set Postgres timezone when needed

マージ日: 2026/5/27 | 作成者: @matthewd

  1. 概要 (1-2文で)
    PostgreSQL アダプタがサーバ側の TimeZone 設定を「常に SET する」のではなく、「必要なときだけ変更する」ように調整された PR です。
    あわせて、PostgreSQL の parameter_status から取得した TimeZonevariables を、大文字・小文字の違いに影響されない(case-insensitive)形で扱うようになっています。

  1. 変更内容の詳細

※ PR #57070 のフォローアップであり、「サーバの現在設定(parameter status)を見て、Active Record が必要とする設定と差分がある場合だけ SET する」という方針を TimeZone 周りにも適用した変更です。実際のコード断片は概念的に再構成しています(正確な行番号や細部はリポジトリを参照してください)。

2-1. PostgreSQL の TimeZone 設定ロジックの見直し

これまでの挙動(推定):

  • 接続時/リセット時などに SET time zone 'UTC' のように、Active Record 側の設定に合わせて無条件に SET を発行していた。

変更後の挙動:

  • PostgreSQL が接続ごとに返す parameter_status(例: SHOW ALL 相当の情報)から、サーバ側の現在の TimeZone を取得。
  • Active Record 側が期待するタイムゾーン(例: ActiveRecord::Base.default_timezoneconfig.time_zone 等から導かれる値)と比較。
  • 異なっている場合だけ SET TIME ZONE ... を実行する。

このために、PostgreSQL アダプタ内で、

  • 接続時に得られる variables / parameter_status を Ruby のハッシュとして扱う際、
  • キー(例: "TimeZone", "timezone", "TIMEZONE")を 大文字小文字を区別せずに扱えるように正規化 する

といった処理が追加・変更されています。

疑似コードイメージ:

ruby
# 接続確立時に parameter_status をハッシュとして取得している想定
@parameter_status = normalize_keys(connection.parameter_status)

def normalize_keys(hash)
  hash.transform_keys { |k| k.to_s.downcase } # 例: "TimeZone" -> "timezone"
end

def configure_connection
  super
  configure_time_zone
end

def configure_time_zone
  desired_tz = lookup_desired_time_zone # Rails の設定から決める
  current_tz = @parameter_status["timezone"]

  # サーバの timezone が既に期待通りなら何もしない
  return if current_tz && current_tz == desired_tz

  execute("SET TIME ZONE #{quote(desired_tz)}", "SCHEMA")
end

上記はあくまでイメージですが、PR 説明にある通り:

  • TimeZone 用の特別な処理が追加されたこと
  • variables / parameter_status のキーを case-insensitive に扱うようにしたこと

がポイントです。

2-2. variables キーの大文字小文字を無視する扱い

PostgreSQL 側の実装やクライアントライブラリ(libpq)によっては parameter_status のキー名が "TimeZone" だったり "timezone" だったりする可能性があります。

これに対応するため、

  • アダプタ内部ではキーを一律で小文字に変換して扱う
  • 以降の参照は "timezone" のような一定のキー名に統一

といった実装になっていると考えられます。

これにより、環境差やドライバの仕様差で "TimeZone" が取れたり取れなかったりする問題が解消されます。

2-3. テストの強化

activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb に大きな変更があり、主に以下がテストされていると考えられます。

  • PostgreSQL サーバ側の TimeZone が Active Record の期待値と同じときは SET TIME ZONE を発行しないこと
  • 異なるときだけ SET TIME ZONE を発行すること
  • parameter_status(あるいは variables)のキーが "TimeZone", "timezone", "TIMEZONE" など、さまざまな表記でも正しく検出できること
  • TimeZone 関連の挙動が既存の Rails 設定(config.time_zone, default_timezone など)と整合していること

テスト行数が +146/-76 と大きく変わっているので、従来よりも「TimeZone と parameter_status の組み合わせ」について細かいケースがカバーされるようになったと見てよいです。


  1. 影響範囲・注意点

3-1. 影響範囲

  • 対象: PostgreSQL を使っている Rails アプリケーション(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
  • 主な影響:
    • 接続時・リセット時などに発行される SET TIME ZONE の回数が減る(不要な変更が抑制される)
    • サーバ側でデフォルト TimeZone を適切に設定している場合、Rails からは「何もしない」ケースが増える
    • パフォーマンス的には微小ながらクエリ数が減る可能性がある
    • DB ログ上で SET TIME ZONE が頻発していた場合、その回数が減る/場合によっては消える

3-2. 注意点・挙動変化の可能性

  • サーバのデフォルト TimeZone に依存する度合いが明確になる
    以前は Rails が毎回 SET TIME ZONE していたため、サーバ設定が何であっても Rails 設定が優先されるように見えていたケースがあります。
    今回の変更により、「Rails が期待する TimeZone とサーバの TimeZone が既に一致している場合は、あえて変えない」という挙動になります。

  • TimeZone 表記の正規化方法によってはマッチングの仕方が変わる可能性
    例: PostgreSQL 側は Etc/UTC、Rails 側は UTC のように、意味的には同じだが文字列としては異なる TimeZone を指定している環境では、
    「異なる」と判定されて毎回 SET TIME ZONE が行われるか、もしくは逆に「同じ」と判定されてセットされないか、
    実装次第で挙動が変わります。
    そのため、DB ログやアプリの期待挙動を確認しておくと安心です。

  • 独自に PostgreSQL の TimeZone を操作しているアプリ・ライブラリとの相互作用

    • アプリケーションコードやミドルウェアが独自に SET TIME ZONE を発行している場合、Rails が「現在の TimeZone 値」を見て SET TIME ZONE の要・不要を判断します。
    • 接続プーリング(pgbouncer など)を利用しており、接続元ごとに TimeZone がいろいろ変化するような構成では、挙動が複雑になる可能性があります。
    • そのような構成の場合、接続のライフサイクルごとに、どのタイミングで誰が TimeZone を設定しているのかを意識した方がよいです。

  1. 参考情報 (あれば)
  • この PR:

    • タイトル: Only set Postgres timezone when needed
    • 番号: #57087
    • 作者: matthewd
    • マージ日時: 2026-05-27T11:33:57Z
  • 関連 PR:

    • #57070: 「parameter_status を使い、必要なときのみ PostgreSQL の設定(おそらく encoding / locale / time zone など)を変更する」という方針を導入した元 PR。
      本 PR はその follow-up で、特に TimeZone と case-insensitive な variables 取り扱いにフォーカスした修正。
  • 実運用でのチェックポイント:

    • 本番環境で PostgreSQL の SHOW ALL; / SHOW TIME ZONE; の結果と Rails の config.time_zone / Time.zone.name を比べておく
    • DB ログで SET TIME ZONE の頻度の変化を確認する
    • 接続プールや DB ミドルウェア(pgbouncer など)を挟んでいる場合、そのレイヤでの TimeZone 設定ポリシーも確認する

#56340 [RF-Docs] [ci-skip] Active Storage Guide

マージ日: 2026/5/27 | 作成者: @bhumi1102

  1. 概要 (1-2文で)
    Active Storage ガイド(active_storage_overview.md)が全面的に再構成され、概念説明・セットアップ手順・表示/配信方法などが大幅に整理・追記されました。あわせてテストガイド(testing.md)に Active Storage 向けのテスト方法が追記されています。

  1. 変更内容の詳細

2.1 ガイド全体の再構成・トーン調整

  • 章構成を入れ替え・統合し、「何ができるか → どうセットアップするか → どう使うか → 配信モード・パフォーマンス → 高度なトピック」という流れに近づけています。
  • 「〜しろ」という命令口調だった部分が、「なぜそれをやるのか」の背景説明を含む記述に変更されています。
  • Active Storage 固有の用語・概念(サービス、アタッチメント、blob、representations など)に、より明確な説明がつきました。

2.2 「What is Active Storage?」と「Setup」の整理

  • Requirements(必要条件)の説明を「What is Active Storage?」から「Setup」セクションに移動。
  • 初めから S3/GCS を出すのではなく、「Disk サービスを使う最小構成」を先に示し、その後にクラウドストレージ(S3, GCS など)の詳細設定を別セクションで説明する構成になっています。

例: Disk サービスの設定(イメージ)

yml
# config/storage.yml
local:
  service: Disk
  root: <%= Rails.root.join("storage") %>
ruby
# config/environments/development.rb
config.active_storage.service = :local

「まずは Disk でローカル動作させ、その後必要に応じて S3/GCS に進む」という導線が明示されているのがポイントです。

2.3 サービスアダプタと「バケット」の説明の強化

  • 「サービスアダプタ」が「Active Storage がファイルシステムや S3/GCS などのストレージとやり取りするための抽象レイヤー」であることを説明し、Disk や S3 がその具体例であることを明示。
  • S3/GCS 設定の前に「bucket = ストレージコンテナ」という用語説明を追加し、「バケット名」とは何かが初心者にもわかるようになっています。
  • S3/GCS の CacheControl 設定について、「何のために必要か(ブラウザキャッシュに関する振る舞い)」を明示的に説明。
    • 例: 画像のレスポンスに Cache-Control ヘッダを付けることで、ブラウザ側でキャッシュが効き、同じ画像を何度もダウンロードしないようにする、など。

2.4 has_many_attached.attach の挙動の訂正・明確化

  • 既存ガイドでは .attach が「既存の添付を置き換える」ように読める部分がありましたが、実際には:
    • has_one_attached: .attach は添付を置き換える(1つだけだから)
    • has_many_attached: .attach は「追加」する(既存の添付は保持される)
  • この挙動が正しく説明されるよう文面を修正。

例: has_many_attached.attach

ruby
class User < ApplicationRecord
  has_many_attached :photos
end

user = User.find(1)
user.photos.attach(io: File.open("a.jpg"), filename: "a.jpg")
user.photos.attach(io: File.open("b.jpg"), filename: "b.jpg")

# => photos には a.jpg と b.jpg の2つが添付される(a.jpg が消えるわけではない)

2.5 processed / preprocessed オプションの記述追加

  • has_many_attached に関して、preprocessed オプションに触れられていなかったため、新たに説明が追加されています(API ドキュメント未記載なのも課題として言及)。
  • variant / preview を事前に処理しておくオプション(※実装詳細の正確なコードは PR から直接は読めませんが、少なくとも「そういうオプションの存在と役割」がガイドに反映された)。

2.6 表示関連(画像・動画・PDF)の章の整理

  • 8章「Displaying Images, Videos, and PDFs」の構成を見直し:
    • 「variant」→「preview」→「representable」のように、利用頻度の高いものから説明。
    • これまで見出しがなかった representable の説明に独立した小節(または明確な説明)が追加。
  • 3種類の「プレビュー的なもの」について概念を整理:
    • variant: 画像用の変形(リサイズ、トリミングなど)
    • preview: 動画や PDF からサムネイルを生成する処理
    • representable: 添付ファイルを一覧などに表示するときに、代替表示可能なもの
  • 「Lazy vs Immediate Loading(遅延 vs 即時ロード)」の節を、上記3種類の説明のあとに移動し、「全タイプに共通する話」として整理。

2.7 Proxy モード vs Redirect モードの明確化

  • 「5.2 Proxy Mode」などの配信モード説明が初心者にはわかりづらかったため、概念説明と例を増補:
    • Redirect モード:
      • Rails が署名付き URL を返し、ブラウザがストレージ(S3 など)へ直接アクセス。
      • Rails サーバの負荷が低いが、ファイルの実体 URL がクライアントに知られる。
    • Proxy モード:
      • クライアントからのリクエストは Rails に届き、Rails がストレージから読み出した内容を「プロキシ」として返す。
      • 認可ロジックをアプリ側できめ細かく制御しやすく、ストレージの URL を隠せるが、Rails サーバの帯域負荷は増える。
  • それぞれの使いどころやセキュリティ・パフォーマンス上のトレードオフを、より平易な言葉で説明するようになっています。

2.8 N+1 問題・Eager Loading の文脈追加

  • 8.1 の中で N+1 クエリが唐突に出ていたのを修正し、「N+1 問題とは何か」「Active Storage でどのように発生しやすいか(添付を 1 件ごとに個別クエリで取りにいくパターン)」の背景を補足。
  • includes(:attached) 的な使い方で eager loading して N+1 を防ぐ、といった形の流れがわかるような説明になっています。

2.9 直アップロード(Direct Upload)と CORS の説明拡充

  • 9.1 Usage にある「サードパーティストレージサービスに CORS を設定する必要がある」という記述に対し、
    • 「具体的な設定方法は、このあとすぐの節で解説している」という誘導リンク・文言を追加。
  • この PR により、「ガイドの途中だけ読んでいる人」でも CORS の詳細がどこにあるか迷いにくくなります。

2.10 未使用アップロード(Unattached Upload)の削除セクションの整理

  • 12章「Purging Unattached Uploads」は内容が短く、9.7 のヘルプボックスと分断されていたため、
    • 9.7 にすでに触れられている内容と統合される、または相互に明確な参照関係が作られています(PR 説明からは統合の方向性が示唆されている)。
  • 結果として、未使用 blob/purge の扱いについて、散在していた説明が整理されています。

2.11 ポリモーフィック関連へのリンク追加

  • セットアップのテーブル内で「polymorphic associations(ポリモーフィック関連)」という用語が出てくる箇所に、Rails ガイド内の該当説明へのリンクが追加されました。
  • Active Storage を初めて触る初学者が、ポリモーフィック関連という用語でつまずきにくくなります。

2.12 Testing ガイド(guides/source/testing.md)の追加

  • Active Storage のテスト方法に関する節が追加されています(+188 行)。
    • 典型的には以下のような観点が含まれていると推測されます:
      • テスト環境用の disk サービス設定
      • 添付ファイル/ダウンロードのテスト
      • 直アップロードやバックグラウンド処理に絡むテスト(ジョブの実行)
  • これにより、Active Storage を使うアプリでの統合テスト・システムテストのベストプラクティスが公式ガイド上で参照しやすくなっています。

  1. 影響範囲・注意点
  • **主な影響は「ドキュメントのみ」**であり、Active Storage の API や既存の挙動が変わる PR ではありません。
    • ただし、これまで誤解されやすかったポイント(has_many_attached.attach の挙動、proxy/redirect の違いなど)が明確化されているため、「今まで思い込みで使っていた」部分が修正される可能性はあります。
  • 新しいオプションの把握
    • processed / preprocessed の説明が追加されたことで、これらのオプションを知らずに都度オンデマンドで処理していたケースについて、改善の余地があることに気づけるようになります。
  • パフォーマンス/セキュリティ設計の再確認
    • Proxy/Redirect の違いや Cache-Control の説明が増えたことで、自身のアプリの設定が要件(セキュリティレベル・帯域コスト・キャッシュ戦略)に合っているかを見直すきっかけになります。
  • テスト戦略の改善
    • testing.md の更新により、これまで「なんとなく」書いていた Active Storage を含むテストを、公式推奨の形に合わせて整理できる可能性があります。

  1. 参考情報 (あれば)

#57243 Fix NoMethodError for malformed multiparameter attribute keys

マージ日: 2026/5/26 | 作成者: @55728

  1. 概要 (1-2文で)
  • multiparameter 属性のキーが壊れている(written_on( のように ( だけで ) が無い)場合に、素の NoMethodError で落ちていたのを、想定どおり ActiveRecord::MultiparameterAssignmentErrors として扱うように修正した PR です。
  • これにより、既存コードが rescue している MultiparameterAssignmentErrors で一貫してハンドリングできるようになります。

  1. 変更内容の詳細

何が問題だったか

_assign_attributes は、属性キーに ( が含まれていると「multiparameter 属性」とみなして、multiparameter 用の処理系に流します。
ここで、multiparameter のインデックス((1i)1 など)をパースするメソッド find_parameter_position が以下のようになっていました:

ruby
multiparameter_name.scan(/\(([0-9]*).*\)/).first.first.to_i
  • 期待されるキー例: "written_on(1i)", "written_on(2i)" など
  • 壊れたキー例: "written_on("( だけで ) が無い)

"written_on(" のようなキーでは scan の結果が [] になり、.firstnil を返し、.first をさらに呼んで NoMethodError: undefined method 'first' for nil が発生していました。

この例外は、multiparameter 用のラッパである execute_callstack_for_multiparameter_attributes が期待している「想定済みのエラー」ではなく、ただの NoMethodError なので、呼び出し側が

ruby
rescue ActiveRecord::MultiparameterAssignmentErrors

としていても捕捉できませんでした。

今回の修正

1. find_parameter_position の挙動変更

  • 正規表現がマッチしない(つまり ( はあるが ) を含まない、等)場合に、NoMethodError ではなく 明示的に ArgumentError を raise するように変更。

擬似コードイメージ:

ruby
def find_parameter_position(multiparameter_name)
  match = multiparameter_name.scan(/\(([0-9]*).*\)/).first
  raise ArgumentError, "could not parse parameter position from #{multiparameter_name.inspect}" unless match

  match.first.to_i
end

これにより「インデックスをパースできない壊れた multiparameter キー」であることを意図的に表現する例外になります。

2. その ArgumentError を multiparameter エラーとしてラップ

  • multiparameter の callstack を組み立てる extract_callstack_for_multiparameter_attributes 側で、find_parameter_position から飛んでくる ArgumentError を捕捉。
  • それを AttributeAssignmentError に包み、既存の仕組みどおり ActiveRecord::MultiparameterAssignmentErrors にまとめて raise するようにしました。

結果として、壊れた multiparameter 属性も他の不正な multiparameter 入力(例: 型変換に失敗した値など)と同じ経路・同じ例外クラスで扱われるようになります。

実行例

修正前

ruby
Topic.new("written_on(" => "2024")
# => NoMethodError: undefined method 'first' for nil
#    activerecord/lib/active_record/attribute_assignment.rb:79:in `find_parameter_position'

修正後

ruby
Topic.new("written_on(" => "2024")
# => ActiveRecord::MultiparameterAssignmentErrors:
#      1 error(s) on assignment of multiparameter attributes
#      [invalid multiparameter attribute name "written_on("
#       (could not parse parameter position from "written_on(")]

つまり、「壊れたキーだが multiparameter として扱おうとしている」状況を、ちゃんと multiparameter 用のエラーとして通知してくれるようになりました。

テスト追加

activerecord/test/cases/multiparameter_attributes_test.rb に 2 ケース追加されています。

  1. ( を含むが ) を含まないキー
    例: "written_on("
    ActiveRecord::MultiparameterAssignmentErrors が発生することを確認。

  2. 壊れたキー + 正常な multiparameter キー混在パターン

    ruby
    Topic.new(
      "written_on(1i)" => "2024",
      "written_on(2i)" => "05",
      "written_on(3i)" => "01",
      "written_on("    => "oops"
    )
    • 正常な "written_on(1i)", (2i), (3i) は問題なく扱われる
    • 壊れた "written_on(" のみが 1 件のエラーとして MultiparameterAssignmentErrors に含まれる

という挙動を確認しています。


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

    • multiparameter 属性のキーが壊れている(( だけや、正規表現にマッチしない形式のキー)場合に、これまで NoMethodError で落ちていたところが、ActiveRecord::MultiparameterAssignmentErrors に変わります。
    • これにより「今まで気づかず rescue されていなかったエラー」が、アプリ側の rescue ActiveRecord::MultiparameterAssignmentErrors によってハンドリングされるようになる可能性があります。
  • 既存の正常系には影響なし

    • 正常な multiparameter キー(written_on(1i) のように (...) が正しく閉じていてインデックスがパース可能なもの)の挙動は変わりません。
    • _assign_attributes の「( を含んでいれば multiparameter 扱いにする」というガード自体も変更されていません。
      つまり、「どのキーが multiparameter 処理に乗るか」という判定ロジックは従来どおりです。
  • 例外クラスの変化に依存しているコードへの注意

    • もしアプリ側で「壊れた multiparameter キーが来るのはバグだから、NoMethodError をそのまま落とすべき」といった前提でいた場合は、今回から MultiparameterAssignmentErrors になる点に注意が必要です。
    • 一般的には、multiparameter の入力バリデーションをしているアプリケーションにとっては、むしろ期待どおりの一貫した挙動になったと言えます。

  1. 参考情報 (あれば)
  • 対象コミットは ActiveRecord (activerecord) のみで、変更ファイルは次の 3 つです。

    • activerecord/lib/active_record/attribute_assignment.rb
      • multiparameter のパース (find_parameter_position) と callstack 抽出 (extract_callstack_for_multiparameter_attributes) の挙動変更
    • activerecord/test/cases/multiparameter_attributes_test.rb
      • 壊れた multiparameter キーのテスト追加
    • activerecord/CHANGELOG.md
      • 本修正のエントリ追加
  • multiparameter 属性は、Rails フォームヘルパ(date_select, datetime_select など)が生成する model[attr(1i)], model[attr(2i)] のようなパラメータを 1 つの属性に組み立てるための仕組みです。この PR は、それらの組み立て時のエラーをより一貫して扱えるようにするものです。


#57472 Active Job: pass job to stopping? for fine-grained control over interruption

マージ日: 2026/5/26 | 作成者: @bdewater-thatch

  1. 概要 (1-2文で)
    Active Job の継続ジョブ (continuations) が「中断可能かどうか」を判定する際に、キューアダプタ側で stopping?job オブジェクトを受け取れるようにした変更です。これにより、Solid Queue / Good Job / Sidekiq Pro などがジョブの属性(キュー名など)に応じて長時間実行ジョブをきめ細かく中断できるようになります。

  1. 変更内容の詳細

2-1. 背景と問題点

  • Active Job には、長時間実行処理を複数ステップに分割して再実行できるようにする「continuation(継続)」機能があります (ActiveJob::Continuable / ActiveJob::Continuation)。
  • 一部のバックエンド(Solid Queue, Good Job, Sidekiq Pro など)は、キューごと・タグごとなど「ジョブの属性ベース」で処理を一時停止できる機能を持っています。
  • しかし、一度実行を開始した「長時間ジョブ(継続ジョブ)」は、バックエンド側がそのキューを pause しても最後まで走り続けてしまい、途中で中断されないという課題がありました。
  • これまでも Active Job 側に「ジョブ実行を中断すべきか?」を問い合わせる stopping? の仕組みはありましたが、その判定に job オブジェクトが渡されなかったため、バックエンド側は「どのジョブか」を知らずに中断条件を決めざるを得なかった、という制約がありました。

この PR は、stopping?job を渡すことで、「特定キューだけ中断」「特定のジョブクラスだけ中断」といった粒度の中断制御を可能にします。


2-2. 具体的なコードレベルの変更点

※PR本文には差分の中身までは載っていませんが、ファイル名と変更行数、および機能説明から推測できるレベルで解説しています。

2-2-1. ActiveJob::QueueAdapters::AbstractAdapter#stopping?

従来(推定):

ruby
module ActiveJob
  module QueueAdapters
    class AbstractAdapter
      # 継続処理内で「いま中断すべきか?」を問い合わせるためのフック
      def stopping?
        false
      end
    end
  end
end

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

ruby
module ActiveJob
  module QueueAdapters
    class AbstractAdapter
      # job を引数に受け取れるように
      def stopping?(job = nil)
        false
      end
    end
  end
end

ポイント:

  • シグネチャが stopping?()stopping?(job) に変更され、ジョブインスタンスを引数として受け取れるようになりました。
  • 既存アダプタとの互換性確保のためにデフォルト引数が付いているか、もしくは引数があっても使わない実装になっていると考えられます。

2-2-2. 継続ジョブ側からの stopping? 呼び出し

ActiveJob::Continuable / ActiveJob::Continuation から stopping? を呼び出す箇所で、今実行中のジョブオブジェクトを引数として渡すように変更されています。

イメージ:

ruby
module ActiveJob
  module Continuable
    private

      def should_stop_continuation?
        # 以前: queue_adapter.stopping?
        queue_adapter.stopping?(self)
      end
  end
end

これにより、継続処理の各ステップの途中で「このジョブに対して、いま中断すべきか?」をアダプタに問い合わせることができます。

2-2-3. テストアダプタ・テストヘルパの更新

  • activejob/lib/active_job/queue_adapters/test_adapter.rb

    • stopping? のインターフェース変更に追従し、job 引数を受け取る形に修正。
    • テスト内で「特定条件で stopping? が true を返す」シナリオを作りやすくなります。
  • activejob/lib/active_job/continuation/test_helper.rb

    • 継続ジョブのテストをしやすくするヘルパが stopping?(job) を想定した形に修正。
    • たとえば「このジョブの queue_namelow_priority なら stopping? は true にする」など、より実際のアダプタに近いテストが書けるようになっていると考えられます。
  • activejob/test/cases/continuation_test.rb

    • 新しい挙動(stopping? に job が渡されること、およびその結果として継続処理が中断されること)をカバーするテストが追加。
    • シナリオ例(推定):
      • 特定の条件にマッチする job に対してだけ stopping? が true を返し、継続が途中で止まっていることを検証。

2-2-4. CHANGELOG 更新

  • activejob/CHANGELOG.md に、Active Job における新しい stopping?(job) フックの追加(あるいは既存フックの拡張)が記載されています。
  • これにより、Rails アップグレード時に「継続ジョブの中断フックが job を受け取れるようになった」ということが分かるようになっています。

2-3. バックエンドアダプタ側で想定される実装例

Solid Queue や Good Job のようなアダプタが、次のように stopping? を実装できるイメージです。

ruby
class MyCoolAdapter < ActiveJob::QueueAdapters::AbstractAdapter
  def stopping?(job)
    # 例: 一時停止リストに入っている queue のジョブなら中断
    paused_queues.include?(job.queue_name)
  end
end

あるいは、クラス単位やタグ単位など:

ruby
def stopping?(job)
  return true  if paused_queues.include?(job.queue_name)
  return true  if paused_job_classes.include?(job.class.name)
  return true  if job.arguments.first == :emergency_stop
  false
end

  1. 影響範囲・注意点

3-1. 影響範囲

  • 影響を受けるのは Active Job のキューアダプタ実装(特に独自アダプタ)と、継続ジョブ(continuations)を利用しているアプリケーションです。
  • Rails が標準で提供するアダプタ(Async, Que, Delayed Job, Sidekiq, Solid Queue など)は、順次この変更に追随していくはずですが、サードパーティ製 / 自前実装のアダプタは注意が必要です。

3-2. 後方互換性

  • AbstractAdapter#stopping? に引数が追加されたので、自作アダプタが def stopping? をオーバーライドしている場合、シグネチャ不一致で警告やエラーになる可能性があります
    • Ruby のバージョンや書き方によっては問題にならない場合もありますが、将来的な互換性のためにも def stopping?(job = nil) のように修正しておくのが安全です。
  • これまで stopping? を使っていなかったアダプタは、そのままでも機能的には問題ありません(常に false 扱い)。ただし、「ジョブ単位での中断制御」を利用したい場合は、新しい引数を利用するように実装を追加する必要があります。

3-3. アプリケーション開発者視点での注意点

  • アプリケーション側の Active Job コード(MyJob < ApplicationJob)は、基本的にこの変更で壊れませんが、長時間実行する継続ジョブでは「途中で中断されうる」ことを前提に設計する必要があります
    • たとえば:
      • 中断ポイントごとに状態を保存しておく(idempotent な処理にする)。
      • 途中で ActiveRecord::ConnectionNotEstablished などの例外が出てもリトライ可能なように書く。
  • バックエンド側が pause したときに「ジョブがいつ中断されるか」は、継続ジョブ側の中断チェック頻度に依存します(stopping? をどのくらいの粒度で呼び出しているか)。
    → 非常に大きなループなどの中で継続を回している場合は、適切な頻度で stopping? を確認するようにするのが望ましいです。

  1. 参考情報 (あれば)

#54542 Duplicate context hash in ActiveSupport::ErrorHandler#report

マージ日: 2026/5/26 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    ActiveSupport::ErrorHandler#report が複数サブスクライバに同じ context ハッシュオブジェクトを共有して渡していた問題を修正し、サブスクライバごとに deep_dup したハッシュを渡すようにしたPRです。これにより、あるサブスクライバが context を破壊的変更しても、他のサブスクライバへの影響が出ないようになります。

  1. 変更内容の詳細

何が問題だったか

ActiveSupport::ErrorReporter / ActiveSupport::ErrorHandler#report は、例外報告時に以下のような「コンテキスト情報」をハッシュで受け取り、複数のサブスクライバ(エラーレポーティングサービスなど)に渡せる仕組みになっています。

ruby
error_reporter.report(exception, handled: true, context: { user_id: 1, request_id: "abc" })

しかし、内部実装では「同じ context ハッシュインスタンス」がそのまま全サブスクライバに渡されていました。そのため、あるサブスクライバで:

ruby
subscriber.call(error, context) do
  Foo.bar(baz: context.delete(:baz))
end

のような「破壊的操作 (delete, merge!, []=, etc.)」をすると、その変更が後続のサブスクライバにも反映されてしまう、というバグ/フットガンになっていました。

何を変えたか

PRでは、サブスクライバへ渡す直前に contextdeep_dup してから渡すように変更しています。

イメージとしては、これまで:

ruby
subscribers.each do |subscriber|
  subscriber.call(error, context)
end

だったものが、以下のようになる変更です(実際のコードは若干異なる可能性がありますが、概念的にはこういう変更):

ruby
subscribers.each do |subscriber|
  subscriber.call(error, context&.deep_dup)
end

ポイント:

  • deep_dup を使用することで、トップレベルのハッシュのみならず、ネストしたハッシュや配列なども含めて複製します。
  • 各サブスクライバは「論理的に同じ内容を持つが別オブジェクトの context」を受け取るため、サブスクライバ内での破壊的変更は他サブスクライバに波及しません。

テストの追加

activesupport/test/error_reporter_test.rb にテストが追加され、たとえば以下のような振る舞いが検証されていると考えられます:

  • 複数のサブスクライバを登録する
  • context にネストしたハッシュ/配列を含めた状態で report を呼ぶ
  • 1つ目のサブスクライバで context を破壊的変更する(delete[]= など)
  • 2つ目のサブスクライバに渡る context にはその変更が反映されていないことを確認する

また、この挙動変更が activesupport/CHANGELOG.md にも追記されています。


  1. 影響範囲・注意点

影響範囲

  • 対象: ActiveSupport::ErrorReporter / ActiveSupport::ErrorHandler#report を使っており、context を渡しているコード、およびそれを受け取るサブスクライバ。
  • 特に影響するのは「複数のサブスクライバを登録しており、かつそのうち1つ以上が context を破壊的に変更している」ケースです。

これまでと変わる挙動

  • 以前は「サブスクライバAが context を変更すると、サブスクライバBにも変更済みの context が渡される」という暗黙の共有状態がありましたが、今後は「サブスクライバごとに独立した context のコピー」が渡されます。
  • もし「意図的に」この共有状態に依存したコードを書いていた場合は、その前提が崩れます。ただし、そのような依存はほぼバグと見なされるため、この変更は後方互換性を高める方向の修正といえます。

パフォーマンス面の注意

  • deep_dupcontext のサイズやネストの深さに応じてコストがかかるため、大きなコンテキストや非常にネストの深いオブジェクトを頻繁に渡している場合はオーバーヘッドが増える可能性があります。
  • 一方で、エラーレポートは通常それほど高頻度ではないことが多く、かつバグ回避のメリットが大きいので、ほとんどのアプリケーションでは妥当なトレードオフです。
  • パフォーマンスを気にする場合は、context に巨大なオブジェクトを渡さないように設計する(IDなど必要最小限の情報だけを渡す)ことが推奨されます。

これからの実装上の指針

  • サブスクライバ側では、context を破壊的に変更しても他サブスクライバや呼び出し元には影響しない、と考えてよくなりました。
  • 「サブスクライバ間で情報を共有したい」場合は、context の変更に頼らず、別の共有メカニズム(スレッドローカル、専用のストレージ、イベントごとのID経由で参照するストアなど)を考えるべきです。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/54542
  • ActiveSupport::ErrorReporter ドキュメント(参考として現行Railsガイドなど)
  • Object#deep_dup の挙動: Active Support による深いコピー機能で、ハッシュ/配列/文字列などを再帰的に複製する

このPRは、エラーハンドリング用のコンテキスト情報を安全に扱えるようにするための、「細かいが実務上ありがちなバグを防ぐ」タイプの改善です。


#55638 [ci skip] Wording correction for url_from documentation

マージ日: 2026/5/26 | 作成者: @dajabe

  1. 概要 (1-2文で)
    ActionController::Redirecting#url_from のドキュメント内の文言を修正し、「サブドメインは安全扱いされない(UnsafeRedirectError になり得る)」という実際の挙動がより明示的に分かるようにしたドキュメント専用 PR です。コードの挙動自体には一切変更はありません。

  1. 変更内容の詳細
  • 対象ファイル: actionpack/lib/action_controller/metal/redirecting.rb
  • 変更内容: url_from(リダイレクト時に使われる内部ヘルパ)の 挙動説明コメント(ドキュメント)に 2 語追加 し、「サブドメインの扱い」に関する説明をより正確にした。

元 PR の Motivation にある通り、従来の説明だと:

  • 「サブドメインは安全とみなされる」ような印象を与える書き方になっていた
  • 実際には、サブドメイン宛てのリダイレクトでも UnsafeRedirectError が発生し得る
  • そのギャップにより、挙動がバグに見え、デバッグに時間を取られた

という問題があったため、

  • 実例として挙がっている UnsafeRedirectError の動作と ドキュメントが一致するよう
  • 「ホスト名の検証/サブドメインの扱い」に関する記述を明示的に修正

した、という内容です。

実装コードは変わっていないので、url_from の実際の挙動は従来通りです。
(例: redirect_to "https://foo.example.com" のような外部 URL / サブドメインへのリダイレクトは、config.hostsallow_other_host などの設定を満たさなければ UnsafeRedirectError になり得る、という既存仕様のままです。)


  1. 影響範囲・注意点
  • 挙動の変更なし

    • ランタイム挙動・API 仕様・互換性に影響はありません。
    • テストや CHANGELOG の更新も不要と判断されているドキュメント-only の修正です。
  • 開発者目線での注意点

    • url_from / redirect_to まわりで「サブドメインなら自動で安全とみなされる」と期待していた場合、その理解は誤りであり、この PR によりドキュメントもそのような誤解を与えないようになります。
    • サブドメイン宛てリダイレクトで UnsafeRedirectError が起きる場合は、Rails のホスト検証(config.hostsallow_other_host: true、もしくは自前検証)が必要です。
    • 既存アプリ側でドキュメントを参照しつつ実装している場合でも、今回の修正で 「今までの挙動が変わった」わけではない ことに注意してください。あくまでドキュメントが実装に追いついただけです。

  1. 参考情報 (あれば)

#57463 Reject malformed Mandrill inbound events without raw messages

マージ日: 2026/5/26 | 作成者: @afurm

  1. 概要 (1-2文で)
    Mandrill のインバウンドイベントで msg.raw_msg(生 MIME メッセージ)が欠落または文字列でない場合、それを不正なイベントとして 422 を返して明示的に拒否するようにした変更です。これにより、従来 500 (TypeError) を返していたケースが正常にハンドリングされ、他の不正ペイロードと一貫した振る舞いになります。

  1. 変更内容の詳細

背景となる問題

  • Mandrill の inbound webhook は、イベントの配列を JSON で送ってくる。
  • その各イベントは event["msg"]["raw_msg"] に、受信メールの「生 MIME メッセージ」を含む想定。
  • しかし、実際には以下のような「パース自体は成功するが中身が足りない」ケースがある:
    • msg 自体がない
    • msg["raw_msg"] が存在しない
    • msg["raw_msg"]nil や文字列以外の値

Rails 側では ActionMailbox::InboundEmail.create_and_extract_message_id! にこの raw_msg を渡しているため、nil を渡すと TypeError が発生し、結果的に 500 エラーになっていた。

今回の対応方針

  • ActionMailbox::Ingresses::Mandrill::InboundEmailsController で、各イベントごとに msg.raw_msg が「存在していて、かつ String であること」を検証する。
  • 検証に失敗したイベント(raw message 不正)は、既にある「不正なイベント」扱いのエラーを発生させるようにし、HTTP レスポンスとして 422 Unprocessable Content を返す。
  • これは元々存在していた「不正ペイロード」ハンドリングと同じ振る舞いに合わせたもの。

コード面でのイメージ

実際のコードは PR 内にありますが、概念的には以下のようなチェックが追加されています(擬似コード):

ruby
events.each do |event|
  raw_message = event.dig("msg", "raw_msg")

  unless raw_message.is_a?(String)
    # 既存の「不正イベント」扱いの例外を投げる
    raise ActionMailbox::Ingresses::Mandrill::InboundEmailsController::MalformedEventError
  end

  ActionMailbox::InboundEmail.create_and_extract_message_id!(raw_message)
end

ポイント:

  • nil だけでなく、IntegerHash など 「文字列以外の型」も NG として扱われる。
  • 検証自体は Ingress コントローラ(Mandrill 用)に閉じており、ActionMailbox::InboundEmail 側には修正は入っていない。

テストの追加

テストファイル:
actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb

主な追加:

  • 「raw message がない場合」のテストケースが追加され、以下を検証している:
    • リクエストが 422 を返すこと
    • InboundEmail が作成されないこと
  • Mandrill ingress 全体のテストスイートが更新されている (bin/test test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb などが通ることを確認済み)。

また、actionmailbox/CHANGELOG.md にこの挙動変更が追記されており、Action Mailbox の挙動として公式にドキュメント化されています。


  1. 影響範囲・注意点

影響範囲

  • 対象: Mandrill 経由で Action Mailbox を使っているアプリケーション。
  • 変更前:
    • msg.raw_msg が欠落/非文字列 → TypeError → Rails が 500 Internal Server Error を返す。
  • 変更後:
    • 同じ条件 → 「不正イベント」扱い → 422 Unprocessable Content を返す。
  • アプリケーションコード側の ActionMailbox::InboundEmail 利用方法は変わりませんが、外部から見える HTTP ステータスコードが 500 → 422 に変わる点が挙動変更になります。

注意点

  • Mandrill 側・クライアント側で 5xx をリトライ条件にしている場合:
    • 今後は 422 が返るため、自動リトライされなくなる可能性があります(Mandrill の実装依存)。その場合、「イベントが不正である」という前提で運用・監視を行う必要があります。
  • エラーの性質が「サーバ側バグ(500)」から「クライアント側/Webhook ペイロードの問題(422)」として整理されるため、
    • アプリのエラーログにおける 500 の件数は減り、
    • 代わりに 422 として検知されるようになります。
  • raw_msg に依存した独自処理(例えば、Mandrill イベント JSON を直接処理しているコード)がある場合、
    • 今回は Rails 内部の Ingress レイヤーでのみチェックしているので直接な互換性問題はありませんが、
    • ペイロード生成側(Mandrill 側の統合)で raw_msg が必ず含まれるようチェックしておくと安全です。

  1. 参考情報 (あれば)
  • 関連 Issue: #57462
    msg.raw_msg 欠落時に TypeError で 500 が返る問題の報告・追跡 Issue。
  • 関連ファイル:
    • actionmailbox/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb
    • actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
    • actionmailbox/CHANGELOG.md
  • テストコマンド(PR 基準):
    • cd actionmailbox && bin/test test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
    • cd actionmailbox && bin/test test/controllers/ingresses
    • cd actionmailbox && bundle exec rubocop app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb

#57459 Fix typos in README, CONTRIBUTING, RELEASING_RAILS, and component docs

マージ日: 2026/5/26 | 作成者: @sagarjunnarkar

  1. 概要 (1-2文で)
    Rails 本体リポジトリ内の README やドキュメント類に含まれていた英語の誤字・文法ミス・記号抜けなどをまとめて修正した PR です。コードの挙動や API には一切影響せず、ドキュメントの品質向上のみを目的としています。

  1. 変更内容の詳細

各ファイルごとの修正ポイントは以下の通りです。

CONTRIBUTING.md

英語の不自然な表現・文法ミスを修正:

  • "instead to refer""instead refer"
    • 「〜の代わりに参照してください」という文脈で、不要な "to" が入っていたのを削除。
  • "Those change should""Those changes should"
    • 単数形 "change" を複数形 "changes" に修正。

これにより、コントリビュートガイドラインの英語として自然な文章になっています。


RELEASING_RAILS.md

リリース手順ドキュメントの文法・スタイル修正:

  • "releases. Publish""releases, publish"
    • 文が途中で不自然に切れていた(文断片)ので、カンマ区切りで一文につなげて読みやすく修正。
  • "To release, Run""To release, run"
    • 文中で不要に大文字になっていた "Run" を小文字に修正し、通常の英文スタイルに統一。

このファイルはリリース作業手順のベースですが、意味内容は変えず、読みやすさと一貫性だけを整えています。


actiontext/README.md

動詞の形を修正:

  • "checkin""check in"
    • "check in" は本来スペースを含む句動詞であり、名詞 "checkin" では意味が異なるため(または不自然なため)、正しい句動詞表記に修正。

Action Text の利用手順説明文の自然さを改善しています。


activemodel/README.rdoc

サンプルコードと文中の記号抜けを修正:

  1. Ruby コード例のクォート抜け:

    • 'bob → 'bob'

    • 本来は以下のような形を想定しているコード例です(例):

      ruby
      user = User.new(name: 'bob')
    • この 'bob' の閉じクォートが落ちていて、RDoc としても Ruby コードとしても不正な形になっていたものを修正。

  2. テキスト中のコロン追加:

    • "on GitHub" の後にコロンを追加。
    • 例: 「ソースコードは GitHub にあります: https://github.com/rails/rails」
    • リンクの前にコロンをつけるスタイルに他のドキュメントとあわせて統一。

これにより、README 内のサンプルコードがコピー&ペースト可能な正しい Ruby コードになり、記述スタイルも一貫します。


actionview/README.rdoc

  • "API documentation is at" の後にコロン : を追加。
    • 例: API documentation is at: https://api.rubyonrails.org/... といった形に揃えたものと思われます。
    • 表記ゆれの修正であり、意味的な変更はありません。

actionmailer/README.rdoc

  • こちらも API documentation is at の後にコロン : を追加。
    • Action View と同様に API ドキュメントリンクの記法を統一。

railties/README.rdoc

  • "on GitHub" の後にコロン : を追加。
  • "API documentation is at" の後にもコロン : を追加。

これにより、Rails 本体 (railties) に関する情報のリンク表記が他コンポーネントと統一されます。


guides/README.md

  • "wholistically""holistically"
    • "wholistically" は誤綴りまたはかなり稀な変種で、一般的な英語としては "holistically" が正しいため修正。
    • 「ガイドを全体的に(holistically)改善する」といったニュアンスの文脈で用いられている部分と思われます。

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

    • 変更対象はすべてドキュメント (Markdown / RDoc / README 類) のみです。
    • Ruby コード、API、挙動、テスト、設定ファイルには一切手が入っていません。
    • そのため、アプリケーションのビルド・実行・テスト結果・互換性に影響はありません。
  • 注意点

    • 外部から README をスクレイピングして自動処理しているツール(パーサやリンクチェッカーなど)がある場合、文言の変更でごくわずかに差分が出る可能性はありますが、「タイポ修正」レベルであり、URL 等は変わっていません。
    • サンプルコード(activemodel/README.rdoc)は、今回の修正で正しい Ruby 構文になっているので、古いバージョンの README をもとにしていた場合は最新のドキュメントのほうが正確です。

  1. 参考情報 (あれば)

ドキュメントコントリビューションのガイドラインに沿った、タイポ・文法・表記統一に関するクリーンアップ PR として位置づけられます。


#55156 Fix crash when trying to attach a Blob to a Record:

マージ日: 2026/5/26 | 作成者: @Edouard-chin

  1. 概要 (1-2文で)
    Active Storage で ActiveStorage.touch_attachment_records = false のとき、既存の Blob をレコードにアタッチするとクラッシュしていた不具合を修正した PR です。Blob が自動で Attachment を保存しなくなった仕様変更により、未保存の Attachment を touch しようとして例外が出ていた箇所にガードを入れています。

  1. 変更内容の詳細

問題の背景

  • 対象機能: Active Storage の direct upload(事前に Blob を作成し、それを後からレコードに attach する流れ)
  • 設定: ActiveStorage.touch_attachment_records = false にした場合のみクラッシュ
  • 状況:
    • 既存 Blob を Record に attach するとき、Active Storage は関連する Attachmenttouch しようとする。
    • しかし、その Attachment はまだ DB に保存(persisted?)されておらず、touch すると例外が発生していた。

以前 (#53623 以前) は:

  1. Blob を作成
  2. Blob が自動的に自分の Attachment を autosave
  3. 保存された Attachment を touch

という流れで、Attachment はすでに保存済みだったため例外は発生しませんでした(ただし 3) の SQL は無駄でした)。

#53623 によって「Blob が自分の Attachment を autosave しない」ように変わったため、Attachment が未保存のまま touch を呼ぶケースが生じ、その結果クラッシュするようになっていました。

この問題は ActiveStorage.touch_attachment_records = true(デフォルト)では発生しません。理由は、Blob 側の attachments_to_touch 的な処理で、レコード未関連の Attachment に対しては空の Relation を返すようになっており、そのパスを通るため未保存の Attachment に touch しに行かないからです。

具体的な修正内容

修正は activestorage/app/models/active_storage/blob.rb の 1 行のみのロジック変更と、それをカバーするテスト追加です。

概念的には以下のような変更です(実際のコードは Rails 本体を参照):

ruby
# 変更前(イメージ)
attachments.each do |attachment|
  attachment.touch # attachment が未保存でも呼んでしまう
end

# 変更後(イメージ)
attachments.each do |attachment|
  next unless attachment.persisted?
  attachment.touch
end

つまり:

  • Blob から関連する Attachment に対して touch する際、
    • その Attachment が persisted? である場合にのみ touch を呼ぶ
    • 未保存 (new_record?) の Attachment には何もしない

テスト (activestorage/test/models/attachment_test.rb) では、以下のような条件を確認するケースが追加されています:

  • ActiveStorage.touch_attachment_records = false に設定
  • 既存 Blob をレコードに attach する
  • この一連の操作で例外が発生しないことを検証

加えて activestorage/CHANGELOG.md に、このバグ修正が明記されています。


  1. 影響範囲・注意点
  • 影響対象:
    • Active Storage を利用しており
    • ActiveStorage.touch_attachment_records = false を明示的に設定している
    • direct upload などで「既存 Blob を後からレコードに attach」するフローを使っているアプリケーション
  • これらのアプリケーションでは、アップグレード後に:
    • これまでクラッシュしていたケースが正常に動作するようになります。
  • 変更内容は「未保存 Attachment への touch 呼び出しをスキップする」だけなので、既存の永続化済み Attachment への touch の挙動は変わりません。
  • touch が呼ばれなくなるのは「そもそもまだ DB に保存されていない Attachment」に対してのみなので、監査用の updated_at やキャッシュ更新といった用途に影響はありません(保存されていない行はそもそも存在しないため)。

運用上の注意:

  • Rails をバージョンアップした際、この挙動変更に伴うマイグレーションや設定変更は不要です。
  • もし独自で Active Storage 周りを monkey patch して touch のタイミングをいじっている場合は、同様に persisted? チェックを入れているかを確認した方が安全です。

  1. 参考情報 (あれば)
  • 関連 issue: #55144(この PR で修正されたバグレポート)
  • 関連 PR: #53623(Blob が Attachment を autosave しなくなった挙動変更の PR。今回の不具合の原因となった仕様変更)
  • 設定項目: ActiveStorage.touch_attachment_records
    • true(デフォルト): アタッチされたレコードを更新したときに Attachment を touch して、Blob の updated_at 連鎖などを制御
    • false: そのような touch を行わない(今回のバグは、この設定時の一部パスで未保存 Attachment に touch しようとしていた)

#57458 Drop test databases when use_postgresql / use_mysql2 is given a block

マージ日: 2026/5/26 | 作成者: @yahonda

  1. 概要 (1-2文で)
    Rails のテスト用ヘルパ use_postgresql / use_mysql2 にブロックを渡した場合、そのブロック内で作成された railties_${Process.pid}_% 形式のテストDBを自動で削除するようにしたPRです。これにより、per-PID / per-worker で作られる一時テストDBがテスト実行後に残り続ける問題が解消されます。

  1. 変更内容の詳細

2-1. 何が追加されたか

TestHelpers::Generation にある use_postgresql / use_mysql2 が「ブロックを受け取る」形で使われた場合、そのブロック実行前後で DB のスナップショットを取り、差分として新規作成された railties_${Process.pid}_% DB を後始末する仕組みが入りました。

内部的には新しいヘルパ with_test_database_cleanup(adapter) が導入され、以下のような動きをします:

  1. ブロックに入る前に、指定アダプタで存在する railties_${Process.pid}_% な DB 一覧を取得してスナップショット
  2. ブロックを実行
  3. ensure で再度 railties_${Process.pid}_% 一覧を取得し、差分(=ブロック中に新規作成されたDB)を DROP

これにより、テストコードが ActiveRecord::TestDatabases.create_and_load_schema などを通じて per-PID/per-worker のテストDBを作成していても、config/database.yml に記述がなくても確実に削除されます。

2-2. 既存テストコードの更新

今回の PR では、すでに use_postgresql / use_mysql2 を使っていて、かつ DB を実際に作っていたテストたちが「ブロック形式」に書き換えられています。

例(イメージ):

変更前(イメージ)

ruby
use_postgresql
# ここで DB 作成系のタスクや create_and_load_schema を実行
rails "db:drop" # ensure 句などで手動クリーンアップ

変更後(イメージ)

ruby
use_postgresql do
  # ここで DB 作成系のタスクや create_and_load_schema を実行
  # 明示的な rails "db:drop" は不要
end

PR 説明文でも述べられている通り、以下のようなテストが対象です(ファイル単位):

  • railties/test/application/rake/dbs_postgresql_test.rb
  • railties/test/application/rake/multi_dbs_test.rb
  • railties/test/application/runner_test.rb
  • railties/test/application/test_runner_test.rb
  • railties/test/application/test_test.rb
  • railties/test/isolation/abstract_unit.rb

これらでは、従来 ensure rails "db:drop" のように明示的に drop していた部分が削除され、新しいヘルパによる自動クリーンアップに任せる形になっています。

2-3. Devcontainer のテストは対象外

Rails::Command::DevcontainerTestuse_mysql2 呼び出しは config/database.yml への書き込みのみを行い、実際の DB 作成はしていないため、今回の自動クリーンアップの影響は受けません。このテストはコード的にも変更なしです。

2-4. 動作確認

PostgreSQL では以下のように、テスト前後で pg_database をスナップショットして railties_* が残らないことを確認済みです:

bash
psql -d postgres -At -c 'SELECT datname FROM pg_database'

上記をテスト前後で比較し、以下のコマンドで走らせた 199 テストケース実行後に railties_* DB が残っていないことを確認:

bash
cd railties && bin/test \
  test/application/bin_setup_test.rb \
  test/application/rake/dbs_postgresql_test.rb \
  test/application/rake/multi_dbs_test.rb \
  test/application/runner_test.rb \
  test/application/test_runner_test.rb \
  test/application/test_test.rb \
  test/isolation/abstract_unit.rb

  1. 影響範囲・注意点
  • テストヘルパ API の実質的拡張

    • use_postgresql / use_mysql2 を「ブロック付き」で呼び出すことが推奨パターンになり、そのブロック内で作成された一時テストDBはヘルパが責任を持って削除します。
    • 既存の「ブロックなし呼び出し」も動作は維持されますが、ブロック外で作られた railties_${Process.pid}_% DB は自動削除されません。
  • 独自テストヘルパを持っている場合の注意

    • Rails 本体に倣って use_postgresql / use_mysql2 をラップした社内ヘルパなどがある場合、それらも「ブロックを取る」ように書き換えることで、今回のクリーンアップロジックの恩恵を受けられます。
    • テスト終了後に手動で rails db:drop を叩いているようなパターンは、ブロック化とこのヘルパに任せる形に移行すると安全かつシンプルになります。
  • 落とされる DB の条件

    • 名前が railties_${Process.pid}_% にマッチするもののみが対象です。それ以外の DB(例: app_development, app_test)は別 PR (#57457) で扱われており、この PR のクリーンアップ対象ではありません。
    • 「差分のみ」を drop するため、ブロック実行前から存在していた同名 DB は削除されません。
  • テスト失敗時・例外時の挙動

    • クリーンアップは ensure で行われるため、ブロック内で例外が発生しても一時テストDBは drop されます。テストが失敗してもDBリークしにくい構造になっています。

  1. 参考情報 (あれば)
  • 対応する sibling PR: #57457
    • app_development / app_test のリーク対策を別途行っている PR。今回の PR は railties_${Process.pid}_% 系の per-PID/per-worker DB にフォーカスしています。
  • 関連箇所:
    • ActiveRecord::TestDatabases.create_and_load_schema
      • per-PID/per-worker テストDBを作成し、それが今回のクリーンアップ対象となる。
    • TestHelpers::Generation#use_postgresql, #use_mysql2
      • ブロック対応 & with_test_database_cleanup を通じた自動 drop を行う。

#57465 Clean up rails console subprocesses spawned by FullStackConsoleTest

マージ日: 2026/5/26 | 作成者: @yahonda

  1. 概要 (1-2文で)
    FullStackConsoleTest が rails console をサブプロセスとして起動したまま放置していた問題を修正し、テスト終了時に必ずサブプロセスを kill/wait してクリーンアップするようにした PR です。これによりテスト実行時に zombie プロセスや不要な rails console プロセスが残らなくなります。

  1. 変更内容の詳細

対象: railties/test/application/console_test.rb に 8 行追加。

問題点

  • FullStackConsoleTest#spawn_consoleProcess.spawn により rails console を起動しているが、

    • その PID をどこかに記録して後で kill / wait する処理がなかった
    • そのためテストファイルを 1 プロセスで実行すると、呼び出しごとにサブプロセスが増え続け、以下のように zombie と生きたままの console が大量に残る:
    text
    === ruby processes (PPid = test runner ruby) ===
    112454 Z(zombie)      <- earlier spawn, exited, never reaped
    112608 Z(zombie)
    ...
    113373 S(sleeping)    <- still-running rails console, 4 threads
    114296 S(sleeping)
    ...
  • console_test.rb 内の 29 個のテストメソッドがそれぞれ 1 回以上 (中には 2 回) spawn_console を呼び出すため、リークが積み上がる構造。

対応内容

テストクラス内に「起動した console サブプロセスを記録し、テスト後に確実に終了させる」処理が追加されています。具体的なイメージは以下のようなコードです(実際のコードの構造を反映した擬似例):

ruby
class FullStackConsoleTest < ActiveSupport::TestCase
  def setup
    super
    @console_pids = []
  end

  def spawn_console(...)
    pid = Process.spawn(...)
    @console_pids << pid
    # 既存の spawn_console の戻り値や PTY 設定などは維持
    pid
  end

  def teardown
    @console_pids&.each do |pid|
      begin
        # Rails の「正常終了」は不要なので SIGKILL で即時終了
        Process.kill("KILL", pid)
      rescue Errno::ESRCH
        # 既に終了している場合は無視
      end

      begin
        Process.wait(pid)
      rescue Errno::ECHILD
        # 他で wait 済み or 既に reaped 済みの場合は無視
      end
    end

    super
  end
end

ポイント:

  • spawn_console 呼び出し時に PID を配列などに溜める。
  • teardown(テストごと)あるいは teardown 相当のフックで:
    • Process.kill("KILL", pid)SIGKILL を送信し、即時終了させる。
      • 動機: テストは Rails の「きれいなシャットダウン」を検証したいわけではなく、PTY を通じてコンソールの動作を確認するだけなので、TERM での優雅な終了処理は不要。
    • Process.wait(pid) でプロセスの終了を回収し、zombie を出さない。
      • 既にどこかで wait 済み/内部的に回収済みの可能性があるため、Errno::ECHILD を rescue して安全に無視。

  1. 影響範囲・注意点
  • 影響範囲:
    • Rails 本体の「実行時の挙動」や API ではなく、テストコード (FullStackConsoleTest) のみ に限定された変更です。
    • rails console の動作やアプリケーションコードには一切影響しません。
  • 開発者・CI 観点でのメリット:
    • テスト実行後に zombie プロセスや不要な rails console が残らなくなるため、
      • CI コンテナやローカル環境でのリソースリークが防げる
      • 長時間テストを回す環境でのメモリ・プロセス数の肥大を抑えられる
    • ps / top などで Ruby プロセスが大量発生する現象が解消される。
  • 注意点:
    • SIGKILL 使用のため、コンソール側で「終了時フック」や「後処理」を行うようなテストには不向きだが、このテストではそうしたシナリオを扱っていないため問題なし。
    • Process.waitErrno::ECHILD を rescue しているので、二重 wait 等によるテスト失敗は発生しない設計になっています。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57465
  • 修正対象ファイル: railties/test/application/console_test.rb
  • 関連トピック:
    • Ruby のプロセス管理: Process.spawn, Process.kill, Process.wait
    • テストでのサブプロセス管理のベストプラクティス(PID の記録と、ensure/teardown での kill + wait)

#57466 Dump enum types with fully qualified names for PostgreSQL

マージ日: 2026/5/25 | 作成者: @fatkodima

  1. 概要 (1-2文で)
    PostgreSQL の enum 型を schema.rb にダンプするときに、「スキーマ付きの完全修飾名(schema.enum_name)」で出力するように変更した PR です。複数スキーマや同名 enum がある環境で、schema.rb の再ロード時に正しく enum を復元できるようになります。

  1. 変更内容の詳細

何をしているか

対象ファイル:

  • activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
  • activerecord/test/cases/adapters/postgresql/enum_test.rb

主な変更点:

  • PostgreSQL の enum 型ダンプ時に、型名を「単純名」ではなく「完全修飾名(schema_name.enum_name)」として扱うようにした。
  • これに伴い、テスト (enum_test.rb) にスキーマ付き enum 名のダンプを検証するケースを追加。

元 PR #56278 をリベース・再調整したものとしてマージされています。

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

実際の内部実装の 1 行差し替えなので概念的なサンプルですが、イメージとしては:

ruby
# 変更前(イメージ)
create_enum "status", ["draft", "published"]

# 変更後(イメージ)
create_enum "public.status", ["draft", "published"]
# あるいは別スキーマ
create_enum "accounting.status", ["draft", "closed"]

のように、schema.rb にダンプされる enum 型名が「スキーマ付き」になる方向の修正です。

テストでは、例えば以下のような観点を検証していると考えられます(概念的な例):

ruby
assert_schema_dump(expected_schema) do
  connection.create_enum "custom.status", %w[draft published]
end

ここで expected_schema 側に create_enum "custom.status" が含まれることを確認するなど。


  1. 影響範囲・注意点

影響範囲

  • 対象:

    • DB に PostgreSQL を使用しており
    • enum 型を使用していて
    • schema.rb(ActiveRecord::Schema)形式でスキーマを管理しているプロジェクト
  • 影響内容:

    • 今後 rails db:schema:dump などで生成される schema.rbcreate_enum 呼び出しが、スキーマ付きの名前で出力されるようになります。
    • 複数スキーマに同名 enum が存在する場合でも、schema.rb をロードした際に正しいスキーマ・型が再現されるようになります。

互換性・マイグレーション時の注意点

  • 既存の schema.rb との差分:
    • 以前に生成済みの schema.rb を、今回の修正が入った Rails で再生成すると、enum 型の名前部分にスキーマが付与されるため、git diff で差分が多く出る可能性があります。
  • スキーマを前提とした enum 名の扱い:
    • すでに手作業で schema.rb を編集していた場合、スキーマ名の有無や名前解決ロジックに依存しているコードがあれば影響が出ないか確認が必要です。
  • 良い影響:
    • これまで、search_path や複数スキーマ構成に依存して enum 型を曖昧に解決していたケースで、ダンプ・ロードの再現性が向上し、より安全になります。

  1. 参考情報 (あれば)

#56979 PostgreSQL::SchemaDumper drops index opclass when using persistent schemas alongside dump_schemas #56966

マージ日: 2026/5/25 | 作成者: @nicolasva

  1. 概要 (1-2文で)
    dump_schemas と永続スキーマ(別スキーマに拡張を置く構成)を併用したときに、PostgreSQL のインデックス opclass が schema.rb から黙って落ちてしまう不具合を修正した PRです。
    スキーマ修飾された opclass 名(shared_extensions.gin_trgm_ops など)を正しくパースして、schema.rb に反映できるようにしています。

  1. 変更内容の詳細

問題の背景

  • within_each_schema を使ってスキーマごとにダンプする際、schema_search_path が「ダンプ対象のスキーマ」だけになります(例: "public")。
  • 一方、拡張機能(例: pg_trgm)を別スキーマ(例: shared_extensions)にインストールしているケースがあります。
  • このとき、PostgreSQL が pg_get_indexdef で返すインデックス定義には、opclass がスキーマ付きで出てきます:
    • 例: ... USING gin (name shared_extensions.gin_trgm_ops)
  • 既存の実装では opclass を抽出する正規表現が \w+_ops で、ドットを含む shared_extensions.gin_trgm_ops にマッチせず、opclass 情報が捨てられていました。
  • 結果として、schema.rb から opclass 指定が消え、db:schema:load などでインデックス定義が変わってしまう可能性がありました。

具体的な修正点

activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb の opclass 抽出処理が変更されています。

1) 正規表現の拡張

以前:

ruby
/opclass:\s*(\w+_ops)/

修正後(イメージ):

ruby
/opclass:\s*((?:\w+\.)?\w+_ops)/
  • (?:\w+\.)? を追加 → 「schema_name. があってもなくてもよい」という形に拡張
  • これにより、以下どちらもマッチ可能になります:
    • gin_trgm_ops
    • shared_extensions.gin_trgm_ops

2) スキーマ修飾部分の除去

マッチした opclass 名に対して:

ruby
opclass_name = match[1].split(".").last.to_sym

という形で、shared_extensions.gin_trgm_ops"gin_trgm_ops" に変換したうえで Symbol 化します。

  • これにより、スキーマ修飾を気にせず、従来どおり using: :gin, opclass: :gin_trgm_ops のような形で schema.rb に書き出せます。

3) テストの追加

activerecord/test/cases/adapters/postgresql/schema_test.rbSchemaOpclassParsingTest が追加されています。

2ケース:

  1. スキーマ修飾なしの opclass

    • 例: USING gin (name gin_trgm_ops)
    • 期待: 正しく :gin_trgm_ops としてパースされること
  2. スキーマ修飾ありの opclass

    • 例: USING gin (name shared_extensions.gin_trgm_ops)
    • 期待: スキーマ部分を無視して :gin_trgm_ops としてパースされること

これにより、今回のバグが再発しないように回帰テストが整備されています。


  1. 影響範囲・注意点
  • 対象:
    • PostgreSQL を使用し、
    • pg_trgm などの拡張を「別スキーマ」にインストールしており、
    • dump_schemas と永続スキーマ(複数スキーマ運用)を組み合わせているプロジェクト
  • 影響:
    • これまで schema.rb から黙って落ちていた opclass 指定が正しく保持されるようになります。
    • インデックスの再作成時(db:schema:load など)に、期待どおりの opclass でインデックスが張られるようになります。
  • 互換性:
    • スキーマ修飾のない opclass(従来ケース)も従来どおりパースされ、影響はありません。
    • 正規表現は「(schema_name.)?name_ops」というよくあるパターンのみを許容しているため、意図せず別の文字列を誤認識するリスクも小さいです。

実運用上は、この修正を含む Rails にアップデートした後、db:schema:dump をやり直すことで、schema.rb に正しい opclass 情報が反映されます。


  1. 参考情報 (あれば)
  • 元 issue: #56966
    「PostgreSQL: opclass silently dropped from schema.rb when using dump_schemas」
  • この PR: #56979
    「PostgreSQL::SchemaDumper drops index opclass when using persistent schemas alongside dump_schemas」
  • 関連コンポーネント:
    • ActiveRecord::ConnectionAdapters::PostgreSQL::SchemaStatements
    • within_each_schema / dump_schemas
    • 拡張例: pg_trgmgin_trgm_ops opclass)

#57457 Drop generated app databases after test_bin_setup_output

マージ日: 2026/5/25 | 作成者: @yahonda

  1. 概要 (1-2文で)
    Rails のテスト ApplicationTests::BinSetupTest#test_bin_setup_output が PostgreSQL 上に作成していた app_development / app_test データベースを、テスト終了時に確実に削除するようにした PR です。テスト実行後に不要なデータベースが残り続ける問題を解消し、クリーンなテスト環境を保つことが目的です。

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

何が問題だったか

  • 対象テスト: railties/test/application/bin_setup_test.rbApplicationTests::BinSetupTest#test_bin_setup_output
  • このテストでは以下を行っていました:
    • 生成したダミーアプリの DB アダプタを PostgreSQL に切り替える
    • そのアプリに対して bin/setup を実行する
  • bin/setup により app_developmentapp_test という PostgreSQL データベースが作成される
  • しかし、テスト終了時に実行される teardown_app は「生成したアプリのディレクトリを削除する」だけで、PostgreSQL のデータベースそのものは削除していなかった
  • その結果、テストを実行するとローカルの PostgreSQL に app_development / app_test が残り続けてしまう

今回の修正内容

  • テスト内で Dir.chdir(app_path) しているブロックの中に ensure 節を追加し、そこで生成アプリ用の DB を削除するようにしました。
  • 既に同じテストメソッド内で使われている rails "db:drop" の呼び出しに合わせて、allow_failure: true オプションを付けています(DB が存在しないなどで失敗してもテストを落とさない)。

変更イメージは以下のようなものです(簡略化した擬似コード):

ruby
Dir.chdir(app_path) do
  begin
    # ...(既存の検証処理: bin/setup の実行や出力チェックなど)
  ensure
    # 生成アプリの PostgreSQL DB を削除
    rails "db:drop", allow_failure: true
  end
end

実際の差分は +2 行のみで、ensure 内で db:drop を実行するコードが足された形です。

動作確認

  • PR 説明によると、以下の手順で検証済み:
    1. psql -d postgres -At -c 'SELECT datname FROM pg_database' で DB 一覧を取得しスナップショット
    2. cd railties && bin/test test/application/bin_setup_test.rb を実行
    3. 再度同じ psql コマンドで一覧を取得
    4. 実行後に app_development / app_test が存在しないことを確認

  1. 影響範囲・注意点
  • 対象は Rails コアのテストコードのみであり、アプリケーション本体の挙動や API 仕様には一切影響しません。
  • PostgreSQL を使って Rails 本体のテスト(railties)を回している開発者にとってのみ影響があり、「テスト実行後に app_development / app_test が残らなくなる」という改善になります。
  • allow_failure: true により、万一 DB が既に削除済み / 作成されなかったなどで db:drop が失敗してもテストが落ちないため、テストの安定性や既存の挙動を壊さないよう配慮されています。
  • 実運用のアプリケーション DB には一切触れない(生成アプリのコンテキスト内だけで db:drop を実行)ため、誤削除のリスクはありません。

  1. 参考情報 (あれば)
  • 対象テストファイル: railties/test/application/bin_setup_test.rb
  • 目的: 「生成アプリを使ったテストが DB を汚さないようにする」というテストクリーンアップの一環
  • 類似のパターン:
    • 生成アプリを使うテストでは、ディレクトリ削除だけでなく「外部リソース(DB、キュー、ストレージなど)のクリーンアップ」を ensure などで行うのが望ましい、という設計指針を示している変更と言えます。

#57158 Fix touch_record for composite foreign keys

マージ日: 2026/5/24 | 作成者: @romulostorel

  1. 概要 (1–2文で)
    複合外部キーを持つ belongs_totouch: true を指定した場合、関連先が変更されたときに「古い側」のレコードが touch されない不具合を修正するPRです。touch_record の実装を、単一キー/複合キーの両方で正しく旧レコードを特定できるように見直しています。

  1. 変更内容の詳細

問題の状況

例えば次のようなモデル構成:

ruby
class Order < ActiveRecord::Base
  self.primary_key = [:shop_id, :order_number]
end

class Book < ActiveRecord::Base
  belongs_to :order,
    foreign_key: [:shop_id, :order_number],
    primary_key: [:shop_id, :order_number],
    touch: true
end

このとき:

ruby
order1 = Order.create!(shop_id: 1, order_number: 100, name: "Order 1")
order2 = Order.create!(shop_id: 1, order_number: 200, name: "Order 2")
book   = Book.create!(title: "My Book", shop_id: 1, order_number: 100)

sleep 1
book.update!(order_number: 200)

期待される挙動は:

  • order2updated_at が更新される(Book が紐づいた側)
  • order1 … 旧関連先として updated_at が touch される(関連解消時の touch)

しかし現状(修正前)は:

  • order2updated_at のみ更新され、
  • order1updated_at は更新されない(= サイレントに無視される)

原因は、belongs_totouch_record 実装が「複合外部キー」を想定しておらず、saved_changes(または changes)の扱いが単一カラム前提になっていたことです。

根本原因の詳細

  • touch_record 内で、外部キーの変更を changes[foreign_key] で取得しようとしていた。
  • 複合外部キーの場合、foreign_key[:shop_id, :order_number] のような配列。
  • 一方で saved_changes{"shop_id" => [...], "order_number" => [...]} のように「カラム名ごと」にキーを持つ。
  • そのため changes[[:shop_id, :order_number]] というハッシュ参照は常に nil を返し、「外部キーが変わっていない」と誤認して旧レコード側を touch しない、というバグになっていた。

修正内容

activerecord/lib/active_record/associations/builder/belongs_to.rbtouch_record を以下の方針で修正しています:

  1. 外部キーを常に配列として扱う

    • foreign_keyreflection.foreign_key)を Array(foreign_key) で正規化し、
    • 単一キーと複合キーの処理経路を統一。
  2. 各外部キーカラムごとに変更有無をチェック

    • saved_changes(もしくは changes)を参照し、
    • 例えば [:shop_id, :order_number] であれば、
      • saved_changes["shop_id"] / saved_changes["order_number"]
    • のように「カラム名ごと」に以前の値・新しい値を取り出す。
  3. 「旧側」の複合キー値を組み立てるロジックの追加

    • 各カラムごとに:
      • そのカラムが変更されていれば saved_changes[column].first(old value)を採用
      • 変更されていなければ read_attribute(column)(現在の値 = old/new共通)を採用
    • こうして、old_foreign_id = [old_shop_id, old_order_number] のように「旧側の複合外部キー値」を組み立てる。
  4. 複合主キーにも対応した find_by の組み立て

    • 関連先モデルの主キー(primary_key)も Array(primary_key) で配列化。
    • Array(primary_key).zip(old_foreign_id).to_h で検索条件を生成し、
      • 単一キーなら { "id" => old_id }
      • 複合キーなら { "shop_id" => old_shop_id, "order_number" => old_order_number }
    • これを find_by に渡して旧レコードを特定し、touch を実行。

この結果、外部キーが単一の場合も複合の場合も、同一ロジックで「以前紐づいていた関連レコード」を正しく見つけて touch できるようになります。

テストの追加・変更

  • activerecord/test/cases/timestamp_test.rb
    • 複合主キー+複合外部キー環境で、
      • 関連先変更により「新しい Order が touch されること」
      • 「古い Order も touch されること」
    • を確認するテストが追加。
  • activerecord/test/models/cpk/book.rb / activerecord/test/schema/schema.rb
    • 上記シナリオを実現するためのモデル・スキーマが微調整されています(複合キー前提のbook/orderモデルなど)。

  1. 影響範囲・注意点
  • 影響を受けるケース
    • belongs_to において:
      • foreign_key を配列で指定(複合外部キー)
      • primary_key も単一または複合キーで明示
      • touch: true を指定
    • 上記条件で「関連先を別のレコードに付け替える」ケース。
  • 期待される挙動の変化
    • これまで「古い関連先が touch されていなかった」ケースで、
      • 古い関連先レコードの updated_at(等のタイムスタンプ)が更新されるようになる。
    • ログや監査用途で updated_at を見ている場合、アップグレード後に「以前は変わっていなかったレコードのタイムスタンプが動く」ことがありますが、これはもともとの仕様意図に沿った振る舞いです。
  • 互換性
    • 単一外部キーの belongs_to ... touch: true の挙動は仕様上変わりません(内部実装の共通化のみ)。
    • primary_key が単一か複数かにかかわらず対応するようになったため、カスタム主キーや複合主キーを活用しているアプリでもより一貫した挙動になります。

  1. 参考情報 (あれば)
  • 関連するAPI/リファレンス:
  • 概念的には、dependent: :destroy 等と同様、「関連付けの変更に伴い、旧関連先にも副作用(ここでは touch)を及ぼす」系の機能の一部であり、複合キー利用時のサポート強化/バグフィックスと言えます。

#57163 Document that value_method and text_method arguments of options_from_collection_for_select accept Proc [ci skip]

マージ日: 2026/5/24 | 作成者: @cgunther

  1. 概要 (1-2文で)
    options_from_collection_for_selectoption_groups_from_collection_for_selectvalue_method / text_method / group_method / group_label_method に、従来から存在していた Proc のサポートを公式ドキュメントに明記した PR です。コードの挙動は変わらず、ドキュメントのみの更新です。

  1. 変更内容の詳細

対象ヘルパー:

  • ActionView::Helpers::FormOptionsHelper#options_from_collection_for_select
  • ActionView::Helpers::FormOptionsHelper#option_groups_from_collection_for_select

これらのメソッドの以下の引数について、「シンボル/文字列によるメソッド名」だけでなく「Proc も渡せる」ことがコメント(ドキュメント)として追記されました。

  • options_from_collection_for_select
    • value_method
    • text_method
  • option_groups_from_collection_for_select
    • group_method
    • group_label_method
    • (内部的には各グループの value_method / text_method も同様に Proc を取れる)

コード上はもともと、object.public_send(value_method) するような単純な実装ではなく、渡された引数が呼び出し可能であれば call するようなロジックで、Proc を受け付ける設計になっていました。今回の PR では、その既存仕様が Yard ドキュメントコメントに反映されただけです。

想定される利用イメージは以下のような形です。

options_from_collection_for_select での Proc 利用例

ruby
# コレクション
users = User.all

# value を任意のロジックで組み立てる
value_proc = ->(user) { "#{user.id}-#{user.account_code}" }

# text も任意のフォーマットで組み立てる
text_proc  = ->(user) { "#{user.name} (#{user.email})" }

options_from_collection_for_select(users, value_proc, text_proc)

従来ドキュメントには :id, :name のようにメソッド名を渡す例しか書かれていませんでしたが、今回の変更で上記のような Proc 利用が公式にサポートされていることが明示されます。

option_groups_from_collection_for_select での Proc 利用例

ruby
# groups: 例として Team モデルの配列
teams = Team.includes(:users)

group_method       = ->(team) { team.users }                    # 各グループに属する items
group_label_method = ->(team) { "#{team.name} (#{team.users.count} users)" }

value_proc = ->(user) { user.id }
text_proc  = ->(user) { "#{user.name} <#{user.email}>" }

option_groups_from_collection_for_select(
  teams,
  group_method,
  group_label_method,
  value_proc,
  text_proc
)

上記のように、グループの展開 (group_method) や表示ラベル (group_label_method) も Proc で柔軟に制御できることがドキュメントに追記されています。


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

    • ライブラリの挙動変更はなく、ドキュメントの更新のみです。
    • すでに Proc を渡して利用していたコードに影響はありませんが、「非公式っぽいテクニック」という心理的ハードルが下がり、今後は積極的に利用してよいと判断できます。
  • 注意点:

    • ドキュメントが公式に Proc を許容したことで、将来のバージョンでも Proc サポートを維持すべき API 契約として扱われる可能性が高くなりました。
    • Proc 内で例外を投げると、select のレンダリング時に例外が発生します。ビューロジックとして適切な範囲の処理に留めることが望ましいです。
    • Proc は1引数(各要素)を受け取る形で設計されているので、->(record) { ... } という形にする必要があります。

  1. 参考情報 (あれば)
  • 該当ファイル:
    actionview/lib/action_view/helpers/form_options_helper.rb
    (メソッドコメント内に Proc を受け付ける旨の記述が追加されている)

  • 関連ヘルパーの RDoc / API ドキュメント:

    • options_from_collection_for_select(collection, value_method, text_method, selected = nil)
    • option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)

今回の PR によって、これらのメソッドの *_method 引数で「メソッド名(Symbol/String)または Proc」を選べることが公式仕様として明確になります。


#56973 [ci skip] Update ActiveRecord Migrations documentation concerning dat…

マージ日: 2026/5/24 | 作成者: @joshmfrankel

  1. 概要 (1-2文で)
    Active Record Migrations ガイドの「データマイグレーション」節に、script/ ディレクトリを使った一度きりのスクリプトやデータ移行の配置場所についての公式な説明が追記された PR です。既存の maintenance_tasks gem の紹介に加えて、Rails 本体に用意されているスクリプト実行の仕組みとの関係を明示しています。

  1. 変更内容の詳細

※PR本文から読み取れる範囲での要約です(実際の差分は guides/source/active_record_migrations.md で 11 行追加・5 行削除)。

追加された主な内容

  • Active Record Migrations ガイド内の「Data Migrations(データマイグレーション)」に、一度きりのスクリプト/データ移行の置き場所として script/ ディレクトリを挙げる文書が追加された。
  • すでに紹介されていた maintenance_tasks gem を使う方法 に並べて、「公式に推奨される整理パターン」として script/ ディレクトリを解説する形になっている。

典型的なイメージ:

  • スキーマ変更(テーブル追加・カラム追加など)
    → 通常どおり db/migrate のマイグレーションファイル
  • 一時的・一度きりのデータ処理(既存データの修正、バックフィル、データ移行など)
    script/ ディレクトリ内の Ruby スクリプト
    または maintenance_tasks gem を用いたタスクとして管理

想定されるサンプルコード(イメージ)

ドキュメント中では、例えば以下のような形が説明されている可能性があります(Rails コミュニティの一般的なパターンに基づく例):

bash
# 一時的なデータ移行スクリプト例
script/
  backfill_user_timezone.rb
ruby
# script/backfill_user_timezone.rb
User.find_each do |user|
  next if user.timezone.present?

  user.update!(timezone: "UTC")
end

そして、Railties が用意するスクリプト実行の仕組みや bin/rails runner などを使って実行する使い方が説明されている可能性があります:

bash
bin/rails runner script/backfill_user_timezone.rb

また、同じ用途を maintenance_tasks を使って行う場合の対比(バックフィルタスクを class として定義し、UI やジョブキューと連携させる等)にも触れていると考えられます。


  1. 影響範囲・注意点
  • 実装・挙動への影響はない
    この PR はドキュメントのみの変更であり、Rails 本体のコードや挙動、API に変更はありません。既存アプリケーションへの互換性問題もありません。
  • データマイグレーションの「置き場所」の公式な指針が明確に
    これまで議論されがちだった「一度きりのスクリプトやデータ移行ロジックをどこに置くか」について、Rails ガイドとして
    • db/migrate はスキーマ変更にフォーカスする
    • データ移行・一時スクリプトは script/(または maintenance_tasks)に置く という分類が文章として明示されるため、チーム内のベストプラクティスを決める際の根拠にしやすくなります。
  • 新規プロジェクトでの構成方針に影響
    • 「一度きりのデータ修正をマイグレーションに書くか?」という議論の際に、「公式ガイドでは script/maintenance_tasks を推奨している」という形で合意形成しやすくなります。
    • 既存プロジェクトで script/ ディレクトリをあまり使っていない場合でも、今後のデータ移行タスクの配置先として採用していくきっかけになります。

  1. 参考情報 (あれば)

#57455 Fix typo in metaprogramming comment

マージ日: 2026/5/24 | 作成者: @nertzy

  1. 概要 (1-2文で)
    ActiveSupportString#output_safety 周りのコメント中のタイポ(** が誤って && と記載されていた)を修正した PR です。実装自体の挙動は変わらず、コメントだけが正しい内容に整合するように修正されています。

  1. 変更内容の詳細 (サンプルコード例を含む)
  • 対象ファイル: activesupport/lib/active_support/core_ext/string/output_safety.rb
  • 変更内容: コメント中の演算子表記の誤りを修正
    • 誤: &&
    • 正: **

このファイルは、ActiveSupport::SafeBuffer / String#html_safe など、HTML セーフティ関連の拡張が定義されている部分で、メタプログラミングを用いて文字列操作メソッドをラップ・委譲する処理に関するコメントが書かれています。
そのコメント中で「どのような演算子・メソッドをラップするか」を説明している箇所に、実装に合わせるとべき記号演算子 **(べき乗演算子)が、本来意図していない &&(論理 AND 演算子)と書かれていたのを修正したものです。

実コード(Ruby)のイメージとしては、例えば以下のように、演算子メソッドを列挙して動的に定義しているコメントに対応していると考えられます:

ruby
# 例: 実装イメージ(実コードとは異なる可能性があります)
# %i[+ << concat prepend gsub gsub! sub sub! * **].each do |method|
#   # これらのメソッドは SafeBuffer に対して〜〜
# end

ここの説明コメントに「**」と書くべきところが、以前のコミットで誤って「&&」になっていたため、今回の PR で修正されています。


  1. 影響範囲・注意点
  • コードロジック・API・挙動の変更は一切ありません。
  • String/SafeBuffer のメソッド呼び出し動作には全く影響しません。
  • 影響はドキュメンテーションレベル(コメント)だけであり、既存アプリケーションやライブラリへの互換性上の懸念は不要です。
  • コメントを参照して実装を読む開発者に対する誤解のリスクが減るという、読みやすさ・保守性の向上のみです。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57455
  • 問題の導入コミット: 47cec2bb17b5a65df66385325b7997bd32b409fc
    • このコミットでコメント修正時に **&& の誤置換が発生したと説明されています。

#56582 Fix PostgreSQL COMMENT ON INDEX for schema-qualified tables

マージ日: 2026/5/23 | 作成者: @eglitobias

  1. 概要 (1-2文で)
    PostgreSQL でスキーマ付きテーブル(例: "other_schema.widgets")に対して add_index をコメント付きで作成した際、COMMENT ON INDEX がスキーマ無しのインデックス名で発行されてエラーになる問題を修正する PR です。これにより、インデックスコメントもインデックス作成時と同様にスキーマ修飾された名前を使うようになります。

  1. 変更内容の詳細

問題の内容

add_indexcomment: オプションを付けて、スキーマ修飾済みのテーブル名を渡すと次のようなコードになります。

ruby
add_index "other_schema.widgets", :id, name: "index_widgets_on_id", comment: "some comment"

現状(修正前)、Active Record はインデックス自体は正しいスキーマに作りますが、インデックスにコメントを付ける SQL を次のように生成していました。

sql
COMMENT ON INDEX "index_widgets_on_id" IS 'some comment'

PostgreSQL では、インデックスに対するコメントは スキーマ付きで指定する必要があり、正しくは:

sql
COMMENT ON INDEX "other_schema"."index_widgets_on_id" IS 'some comment'

でなければなりません。そのため、other_schema にテーブル・インデックスが存在していても、スキーマを付けない COMMENT ON INDEX により PG::UndefinedTable が発生していました。

修正内容のポイント

この PR では、以下のような対応が行われています。

  1. スキーマ付きテーブル名からスキーマ名を考慮してインデックス名を構築

    • add_index 呼び出し時に、テーブル名が "schema.table" の形式で与えられていた場合、
      • 生成するインデックスコメント用の識別子に、そのスキーマ名を含めるように変更。
    • Rails 内部ではおおよそ以下のような挙動に近づく形になります(擬似コード):
    ruby
    # 例: table_name = "other_schema.widgets"
    schema, plain_table_name = extract_schema_and_table(table_name)
    
    index_name = options[:name] || index_name_for(plain_table_name, column_names)
    index_identifier = schema ? %("#{schema}"."#{index_name}") : %("#{index_name}")
    
    execute <<~SQL
      COMMENT ON INDEX #{index_identifier} IS #{quote(comment)}
    SQL
  2. PostgreSQL アダプタ側の schema_statements の修正

    • PostgreSQL 固有の schema_statements 実装でコメント付与ロジックを修正し、テーブル名にスキーマが含まれる場合はインデックス名にも同じスキーマを付けるようにしています。
    • インデックス自体の作成時のスキーマ解決ロジックと、コメント生成時のロジックの整合性がとられました。
  3. テストの追加

    • activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb にテストが追加され、スキーマ付きテーブルに対して add_index + comment: を行った場合に、例外が出ず正しくコメントが適用されることを確認しています。
    • これにより、回帰テストとしても機能します。

  1. 影響範囲・注意点
  • 影響を受けるのは PostgreSQL を使っていて、かつスキーマ付きテーブル名を文字列で渡しているケースのみです。
    例: add_index "other_schema.widgets", :id, comment: "..."

  • 既存のマイグレーションについて:

    • これまで「マイグレーション実行時に PG::UndefinedTable で落ちていたもの」は、この修正以降は正常に通るようになります。
    • すでに「コメントなしで回避」「別の方法でコメントを付けていた」場合でも挙動が壊れることはありません。
  • インデックス名を明示指定していない場合でも、Rails が自動生成するインデックス名にスキーマを正しく付けて COMMENT ON INDEX を実行するため、特別な対応は不要です。

  • 他の DB アダプタ(MySQL, SQLite など)には影響しません。PostgreSQL 固有の挙動修正です。


  1. 参考情報 (あれば)
  • 対応 Issue: #56581
  • 対応 PR: #56582 「Fix PostgreSQL COMMENT ON INDEX for schema-qualified tables」
  • PostgreSQL ドキュメント (COMMENT):
    https://www.postgresql.org/docs/current/sql-comment.html
    • COMMENT ON INDEX の構文として、[schema_name.]index_name が必要であることに起因する問題です。

#57335 Handle invalid SendGrid envelope input in Action Mailbox

マージ日: 2026/5/23 | 作成者: @afurm

  1. 概要 (1-2文で)
    Action Mailbox の SendGrid 受信エンドポイントにおいて、envelope パラメータが「JSONとしては正しいが、想定した構造ではない」場合に、422 Unprocessable Content を返してメールを作成しないようにする防御的なバリデーションが追加されました。これにより、既存の「JSONとして不正なenvelope」と同様の扱いになり、挙動の一貫性と堅牢性が向上しています。

  1. 変更内容の詳細

背景

  • 既存実装:
    • SendGrid ingress(ActionMailbox::Ingresses::Sendgrid::InboundEmailsController)は、envelope パラメータが「無効なJSON」の場合、422 Unprocessable Content を返す。
    • しかし、「JSONとしてはパースできるが、期待する形(スキーマ)ではない」envelope については明確な防御がなく、コントローラ内部で想定外の構造を前提に処理する可能性があった。
  • このPRでは、JSONパース後に envelope["to"] の型や存在を検証し、想定されない構造の場合は 422 を返すよう統一しています。

具体的な挙動の変更

  1. envelope のバリデーション追加

    • これまで:
      • envelope が JSON としてパースできれば、そのまま envelope["to"] を使おうとする実装。
    • 変更後:
      • パース後の envelope に対して、以下を満たすかをチェック:
        • envelope["to"] キーが存在すること
        • envelope["to"] が「文字列の配列(Array<String>)」であること
      • これらを満たさない場合は、既存の「unprocessable」なリクエストとして扱い、422を返し、InboundEmail を作成しない

    疑似コードイメージ:

    ruby
    envelope = JSON.parse(params[:envelope]) if params[:envelope]
    
    unless valid_envelope?(envelope)
      # 422 Unprocessable Content を返し、処理を打ち切る
      render_unprocessable_entity
      return
    end
    
    recipients = envelope["to"] # Array&lt;String> であることが保証される

    valid_envelope? の中で、「to が存在し、配列であり、各要素が String」であるかをチェックするイメージです。

  2. 「オプションパラメータとしての envelope」の扱いの明確化

    PR文中で「optional envelope parameter」とあるように、envelope 自体は必須ではありませんが、送られてきた場合は形を厳格にチェックするようになりました。

    • envelope が送られてこない場合の挙動は従来通り。
    • 送られてきていても:
      • JSONとして不正 → 422
      • JSONとしては正しいが、to が欠けている / to の型が不正 → 422
      • 正常な形 → 通常処理(InboundEmail作成処理)に進む
  3. テストの追加・更新

    変更されたテストファイル:

    • actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb

    主に次のようなケースがテストされていると考えられます:

    • envelopeto が存在しない場合 → 422 が返却され、メールが作成されないこと
    • envelope["to"] が配列でない場合(例: 文字列やオブジェクト) → 422
    • envelope["to"] が配列だが、要素が文字列以外を含む場合 → 422
    • 正常な envelopeto が文字列配列) → 200/202相当で、InboundEmail が作成されること
  4. CHANGELOG の更新

    • actionmailbox/CHANGELOG.md に 4 行の追記があり、
      「SendGrid ingressにおけるenvelopeバリデーション追加」に関する記述(バグ修正/振る舞い変更としてのメモ)が追加されています。

  1. 影響範囲・注意点
  • SendGrid 連携を利用しているアプリケーションへの影響:

    • 正常な SendGrid 本番トラフィック(公式仕様に従ったペイロード)であれば、この変更による影響は基本的にありません(PR説明にも「not a known SendGrid production payload」と明記されています)。
    • ただし、独自の実装やテスト等で、envelope のフォーマットが厳密でないリクエストを送っている場合、
      • これまではたまたま受け入れられていたケースが、
      • 今後は 422 Unprocessable Content で拒否されるようになります。
  • 防御的プログラミングの一環としての変更:

    • 不正・異常な入力に対して Rails 側が早期にエラーを返すことで、
      • 内部処理(メール作成ロジック)側の前提が崩れにくくなる
      • 予期せぬ例外やデータ破壊のリスクを減らす
    • HTTP レベルでの一貫した応答(「解釈不可能なコンテンツ」は 422)を保つ目的があります。
  • 挙動の確認ポイント(既存アプリの開発者向け):

    • 自前で SendGrid からの Webhook をモックしているテストやツールがあれば、
      • その envelope JSON が以下を満たしているか確認すると安全です:
        • envelope がJSONオブジェクト
        • envelope["to"] が存在する
        • envelope["to"]["user@example.com", ...] のような配列
        • 各要素がメールアドレス文字列(少なくとも String 型)である

  1. 参考情報 (あれば)
  • 関連 Issue: #57334
    • envelope が期待する形でない場合の扱い」に関する報告・議論がなされていると考えられます。
  • 変更対象コントローラ:
    • actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb
    • SendGrid 受信 Webhook エンドポイントを扱う主要なクラスであり、ここに envelope 構造チェックが追加されています。
  • 実行された検証コマンド:
    • 個別テスト:
      • cd actionmailbox && bin/test test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb
    • Action Mailbox 全体テスト:
      • cd actionmailbox && bin/test
    • 静的解析 (Rubocop):
      • bundle exec rubocop actionmailbox/app/controllers/action_mailbox/ingresses/sendgrid/inbound_emails_controller.rb actionmailbox/test/controllers/ingresses/sendgrid/inbound_emails_controller_test.rb

このPRは、「挙動の仕様を変える」というよりは、「これまで暗黙的だった前提(envelope["to"] は配列の文字列である)をコード上で明示し、異常系を 422 で早期に弾く」ための安全性・一貫性向上の修正と捉えると理解しやすいです。


#57452 Handle malformed Mandrill events payloads in Action Mailbox

マージ日: 2026/5/23 | 作成者: @Messier81

  1. 概要 (1-2文で)
    Mandrill の Action Mailbox ingress が、JSON の「形」が不正な payload を受け取った際に 500 を返していた問題を修正し、422 Unprocessable Content を返すようにした PRです。mandrill_events が「配列の Hash であること」を検証し、そうでない場合は新設の MalformedEventsError を投げて既存ハンドリングに乗せています。

  1. 変更内容の詳細

背景の問題点

従来の Mandrill ingress の挙動:

  • mandrill_events パラメータを JSON としてパース
  • パースに失敗 (JSON::ParserError) した場合
    → 422 Unprocessable Content を返す (OK な挙動)
  • しかし、JSON としては妥当だが、想定している構造と違う場合:
    • 例: "null", "1", "{}", "[1,2,3]", "[null]" など
    • コントローラ内で events.select { ... } のように配列前提でメソッドを呼ぶため、
      • nil.selectNoMethodError
      • 配列中に Hash 以外が入り、それを .[] などして TypeError
    • これにより 500 Internal Server Error が発生していた

本 PR での方針は、「JSON 文字列として妥当か」だけでなく、「イベント配列として妥当か」も検証し、不正なら 422 にする、というものです。

具体的な実装変更

1. 形の検証と MalformedEventsError

Mandrill ingress コントローラ (actionmailbox/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb) にて:

  • mandrill_events をパースした結果に対して、以下を満たすかをチェック:
    • オブジェクト全体が Array である
    • かつ、その要素がすべて Hash (Mandrill イベントオブジェクト) である

擬似コードイメージ:

ruby
def mandrill_events
  events = JSON.parse(params[:mandrill_events])

  unless events.is_a?(Array) && events.all? { |event| event.is_a?(Hash) }
    raise MalformedEventsError
  end

  events
rescue JSON::ParserError, MalformedEventsError
  # 422 を返し、InboundEmail は作らない
end

※実際のコードは Rails コーディングスタイルに合わせてもう少し分割されているはずですが、ロジックとしてはこのイメージです。

  • MalformedEventsError は、この「形が不正」ケース専用の例外として新設され、
  • 既存の rescue JSON::ParserError とまとめて扱われることで、レスポンスを 422 に統一します。

2. Mailgun テストの調整

actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb も変更されているのは、
テストヘルパーやエラー検証ロジックを共通化・調整したためです。

考えられる変更点:

  • 「不正 payload を送ったとき 422 になること」を汎用的にチェックするヘルパーメソッドの追加
  • Mandrill 用に追加した MalformedEventsError や、ステータスコードの検証に合わせて Mailgun 側の期待値・書き方を揃えた

これにより、Mandrill / Mailgun ingress 双方で「不正入力に対して 500 を出さない」ことをテストで保証できます。

3. テストの追加・拡充

actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb に多くの行が追加されています (+51/-2)。

主な内容:

  • 形が不正な JSON payload についてのテストケース
    • null, スカラー値 (数値や文字列), Hash ルート, 配列だが要素が Hash でない場合など
    • これらを mandrill_events として送信し、
      • レスポンスが 422 Unprocessable Content であること
      • ActionMailbox::InboundEmail が作成されないこと
  • 正常な payload (配列の Hash) に対する既存テストの維持

actionmailbox/test/test_helper.rb も +9/-1 行の変更があり、
テスト共通ヘルパーの追加 (例: 不正 payload を送るヘルパー、エラーレスポンス検証など) が行われています。

4. CHANGELOG の更新

actionmailbox/CHANGELOG.md に以下のようなエントリが追加:

  • Mandrill ingress で malformed な Mandrill events payload に対して 500 ではなく 422 を返すようになったことを明記。
  • 将来アップグレードする利用者が、この挙動の修正を把握できるようにしている。

  1. 影響範囲・注意点
  • 対象:
    • Mandrill を Action Mailbox ingress として利用しているアプリケーション
  • 挙動の変化:
    • これまで 500 が出ていた「JSON だが形が不正」な mandrill_events に対して 422 が返るようになります。
    • 正常な payload (イベント配列) に対する処理や、既に 422 を返していた「純粋な JSON パースエラー」の挙動は変わりません。
  • 互換性:
    • アプリ側で「Mandrill ingress が 500 を返したときだけ特別な処理をする」といったロジックを組んでいない限り、後方互換性の問題はほぼありません。
    • 監視周りでは、「Mandrill inbound でたまに 500 が出ていた」のが「422 に減る/なくなる」ため、エラーレートが改善する可能性があります。
  • セキュリティ・堅牢性:
    • 不正な入力で内部例外 (NoMethodError, TypeError) を発生させず、安全に 422 を返すようになったため、
      ingress エンドポイントの堅牢性が向上しています。
    • エラーレスポンスとして 500 スタックトレースなどが外に漏れる可能性も下がります (本番設定では元々抑制されているはずですが、開発環境などでもより明確な 422 になります)。

  1. 参考情報 (あれば)
bash
cd actionmailbox && bin/test test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
cd actionmailbox && bin/test
bundle exec rubocop \
  actionmailbox/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb \
  actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
git diff --check

Mandrill を使っている場合は、アップグレード後に「意図せず 500 が発生していないか」「422 のレスポンスを受けた際の再試行戦略」があるかを確認しておくと安心です。


#57453 Fix HTML-aware ERB compiler errors in bundled templates so they apss strict validations

マージ日: 2026/5/23 | 作成者: @thomaswitt

  1. 概要 (1-2文で)
    Herb 0.10+ の「HTML 構文・属性位置を検証する ERB コンパイラ」で Rails 同梱のテンプレートがコンパイルエラーになる問題を解消し、すべての同梱 .erb テンプレートが Herb の厳密検証を通るようにした PR です。ついでに 2013 年から潜在していたレスキュー画面のテキストレスポンス内の余計な文字と、<option> タグ内の余計なスペースも解消されています。

  1. 変更内容の詳細

2-1. routing_error.text.erb の不正な閉じタグ削除

対象ファイル:
actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb

問題点:

  • テキストレスキュー用テンプレート内に、対応する <code> 開始タグのない「余計な </code>」がそのまま生テキストとして残っていた。
  • HTML としても不正な構造であり、Herb の HTML-aware パーサが「開きタグなしの閉じタグ」として検出しコンパイルエラーを出す。

対応:

  • 該当の </code> を削除。
  • 実行時の差分としては、XHR/Text で返している routing error レスポンス内から、今までこっそり出力されていた </code> という文字列が消えるだけ。

2-2. mailer preview テンプレート内 <option> の ERB 記法修正

対象ファイル:
railties/lib/rails/templates/rails/mailers/email.html.erb

問題点:

  • <option> タグの selected 属性を、属性名スロットに ERB 出力を書き込む形で実装していた。

典型的な(問題のある)例は以下のイメージです:

erb
<option <%= condition ? 'selected' : '' %> value="foo">
  Foo
</option>

HTML 的に見ると、<option value="foo"> のように、

  • 「空文字が出力されることで二重スペースが入る」
  • Herb 的には「属性名の位置に動的な ERB 出力がある」ため検証が難しく、セキュリティ/妥当性チェックに引っかかる

という問題があります。

対応:

  • 制御構文 ERB (<% %>) による条件分岐に変更し、属性名とテキストは純粋に静的な HTML として出すように変更。

イメージ:

erb
<option<% if condition %> selected<% end %> value="foo">
  Foo
</option>

ポイント:

  • selected 属性自体は静的文字列としてレンダリングされる(ERB 出力で属性名を書かない)。
  • condition が false の場合は <option value="foo"> となり、
    以前のような <option value="foo">(二重スペース)が解消される。
  • selected が true の場合の出力(例: <option selected value="foo">)は従来とバイト列レベルで同等。

2-3. mailer preview のテスト更新

対象ファイル:
railties/test/application/mailer_previews_test.rb

問題点:

  • 既存テストが、「二重スペースを含む <option value="...">」文字列を期待していた。
    • これは、前述の <%= '' %> による空文字 + スペースが原因。

対応:

  • 期待値を以下のように変更:
    • '<option value="..."''<option value="..."'(二重スペース → 単一スペース)
  • '<option selected value="..."' を期待している箇所は出力が変わらないためそのまま。

2-4. Guides レイアウトのバージョンスイッチャー修正

対象ファイル:
guides/source/layout.html.erb

問題点:

  • Guides のバージョン切替用 <select> にある <option> で、
    selected の付与に <%= " selected" if cond %> という 属性位置での ERB 出力を使っていた。

例(問題のあるパターン):

erb
<option value="<%= version %>"<%= " selected" if current_version?(version) %>>
  <%= version %>
</option>

これは Herb から見ると、「属性リストの中に動的な文字列が挿入されている」構造で、セキュリティ/妥当性チェックが難しい。

対応:

  • メーラーテンプレート同様、制御構文に変換。

例(イメージ):

erb
<option value="<%= version %>"<% if current_version?(version) %> selected<% end %>>
  <%= version %>
</option>
  • true/false の両パターンで、レンダリング結果は従来とバイトレベルで同一となるように調整されている。

  1. 影響範囲・注意点
  • Herb 利用アプリのブート障害解消

    • Herb 0.10+ を ERB エンジンとして使っているアプリで、Rails の同梱テンプレート読込時に発生していた
      • Herb::Engine::InvalidNestingError
      • Herb::Engine::SecurityError
        などのコンパイルエラーが解消されます。
    • herb compile(0.10.1)で全 66 ファイルの .erb テンプレートを検証した結果:
      • 変更前: 3 件失敗(1 MissingOpeningTag, 2 SecurityError)
      • 変更後: 0 件
  • 実行時の見える差分

    • routing error の text レスポンス:
      • 余計な文字列 </code> が出力されなくなる。
    • メーラープレビュー/Guides の <option>:
      • 「未選択の <option> に入っていた二重スペース」が単一スペースになる。
      • それ以外の HTML 構造・属性順序は、selected/unselected ともに既存動作と同等。
  • Rails 8.1.x 系への影響

    • 本 PR は main への修正で、Herb 利用者が Rails 8.1.x でブートできない問題への根本対処として、Rails 側の不正マークアップを正しています。
    • rescues/routing_error.html.erb 側の修正は既に別コミット(84023dfd1f)で main に入っており、本 PR は残りのテンプレートをすべてクリーンにする「締め」の位置づけです。
    • 8.1-stable へは、この PR と 84023dfd1f のバックポートが Herb 利用者にとって重要になります。
  • 互換性上のリスク

    • HTML 出力の差分はごく小さく、仕様的にはより正しい HTML に近づいているため、互換性リスクは低いと考えられます。
    • ただし「生 HTML を文字列で比較している極端に厳しいテスト・スクレイパ」が option 内の空白数まで固定で見ている場合には、テストの更新が必要になる可能性があります(今回の mailer preview テスト更新と同様)。

  1. 参考情報 (あれば)
  • Herb 側の関連 Issue:
    https://github.com/marcoroth/herb/issues/1744
    → Herb に「パスごとのバリデータスコープ」を導入する downstream 対策も議論されているが、この PR では Rails 側のマークアップを正して根本的にエラーを消しています。

  • 既存の関連コミット:

    • 84023dfd1frescues/routing_error.html.erb<p><h2/><ol/></p> という不正な入れ子を修正したコミット(今回と同じ文脈での修正)。

#57450 Deep-freeze NumberConverter::DEFAULTS

マージ日: 2026/5/23 | 作成者: @paracycle

  1. 概要 (1-2文で)
    ActiveSupport::NumberHelper::NumberConverter::DEFAULTS 定数を「深い凍結(deep-freeze)」することで、Ractor で共有可能な完全イミュータブルなオブジェクトにした PR です。外側の Hash だけでなく、ネストされた Hash もすべて freeze されています。

  1. 変更内容の詳細

何をしているか

  • 対象: activesupport/lib/active_support/number_helper/number_converter.rb 内の NumberConverter::DEFAULTS
  • 外側の Hash は既に .freeze 済みだったが、値として入っているネストされた Hash がミュータブルのままだった。
  • そのため、DEFAULTS は「見かけ上は」定数だが、内部は変更可能であり、Ractor 間での共有ができない状態だった。
  • 各ネストした Hash リテラルに .freeze を追加し、DEFAULTS 配下のオブジェクトツリー全体を凍結した。

イメージとしては、もともと以下のような構造だったものが:

ruby
DEFAULTS = {
  format: {
    delimiter: ",",
    separator: ".",
  },
  currency: {
    unit: "$",
    format: {
      # ...
    },
  },
}.freeze

この PR により、ネストした Hash もすべて freeze されるように変更されています:

ruby
DEFAULTS = {
  format: {
    delimiter: ",",
    separator: ".",
  }.freeze,
  currency: {
    unit: "$",
    format: {
      # ...
    }.freeze,
  }.freeze,
}.freeze

(具体的なキー名・構造は例示ですが、実際にはこのような形で各レベルの Hash に .freeze が付いている変更です)

背景

  • 目的は「Ractor 対応の一環として、Rails 内部で“不変であるべき定数”を実際に不変(deep-frozen)にする」こと。
  • 既に同様の対応を行った PR #57323 の続きで、ActiveSupport のナンバーヘルパー部分に対象を広げたものです。
  • RuboCop の Style/MutableConstant Cop によって外側の .freeze は付いていたが、それでは「浅い freeze」にとどまり、内部要素は依然として書き換え可能だったため、Ractor が要求する「shareable?」なオブジェクト要件を満たしていませんでした。

  1. 影響範囲・注意点

影響範囲

  • ActiveSupport::NumberHelper::NumberConverter を通じて数値フォーマット・通貨フォーマットなどを行っているコード全般に影響しますが、「正しく使っている限り」挙動は変わりません。
  • 影響が出るのは、NumberConverter::DEFAULTS の内部 Hash を直接書き換えていたコード です。

たとえば、以下のようなコードは、この PR 以降は FrozenError を起こします:

ruby
# 以前はできてしまっていた(推奨されない)書き換え
ActiveSupport::NumberHelper::NumberConverter::DEFAULTS[:format][:delimiter] = '.'
# => この PR 以降は FrozenError

注意点 / マイグレーション

  • もしアプリやライブラリで上記のように DEFAULTS を直接変更して独自設定を注入していた場合:
    • 今後は DEFAULTS をベースに dup / deep_dup してから変更 するか、
    • Rails が提供している公式な設定インターフェース(ヘルパーのオプション指定や I18n 経由の設定など)を利用する必要があります。

例:

ruby
# 悪い例(今後 FrozenError)
defaults = ActiveSupport::NumberHelper::NumberConverter::DEFAULTS
defaults[:format][:delimiter] = '.'

# よい例(自前でコピーしてから変更)
defaults = Marshal.load(Marshal.dump(
  ActiveSupport::NumberHelper::NumberConverter::DEFAULTS
))
defaults[:format][:delimiter] = '.'

あるいは、単にヘルパーの呼び出し時にオプションを渡す方法が推奨されます:

ruby
number_to_delimited(1000, delimiter: '.')

  1. 参考情報 (あれば)
  • 関連 PR: #57323 – Rails を Ractor で使うための定数 freeze 対応の一部。
  • Ruby Ractor と shareable オブジェクト: Ractor 間でオブジェクトを共有するには、そのオブジェクトが凍結され、内部もミュータブルでない必要がある(deep-freeze が必要)ため、本 PR のような対応が行われています。

#48549 Qualify association unscope values with table names

マージ日: 2026/5/23 | 作成者: @philip-maina

  1. 概要 (1-2文で)
    Rails の unscope が関連(association)経由のクエリで使われたときに、どのテーブルのカラムを外すのかが曖昧になりうる問題を、テーブル名付きで条件を解釈するように修正した PR です。これにより、特に has_many :through など複数テーブルが絡むクエリでの unscope の挙動が、より正確かつ予測可能になります。

  1. 変更内容の詳細

※実際のコード断片は概念レベルの説明です(PR 本文には詳細がないため、Rails の既存実装からの推測を含みます)。

背景となる問題

ActiveRecord::Relation#unscope は、スコープやチェインされたクエリから特定の条件(where, order, select など)を取り除くための API です。

ruby
Post.where(published: true).order(:created_at).unscope(:order)
# => ORDER BY が取り除かれる

ところが、関連を辿ったクエリ(特に has_many :throughincludes / joins で複数テーブルが絡む場合)で unscope を使うと、以下のような曖昧さが出ます。

  • postscomments が両方 id, created_at といった共通カラムを持っている
  • unscope(where: :id) のようにカラム名だけを指定すると、「どのテーブルの id を外すのか」がはっきりしない
  • 内部的に生成される AssociationRelation / association scope 側で unscope の指定が正しくテーブルに紐づかないと、
    • 意図と違うテーブルの条件が外れる
    • あるいは何も外れない
      といったバグが起きる

Issue #48548 で報告されているのは、この種の「関連経由クエリ + unscope + カラム名の曖昧さ」による誤動作です。

変更点の概要

この PR の要点は以下です。

  • unscope が受け取る「どの値(カラム・条件)を外すか」の指定を、テーブル名付きで解釈・適用するようにした
  • Association 経由 (has_many, has_many :through など) のクエリに unscope が適用される際に、関連先のテーブルに正しくスコープされるように修正した
  • これに合わせて
    • activerecord/lib/active_record/relation/query_methods.rbunscope 用のロジックを追加(+24)
    • activerecord/lib/active_record/associations/association_scope.rb の関連スコープ構築時の挙動を 1 行修正
  • 挙動を保証するためのテストをいくつか追加
    • has_many_through_associations_test.rb に、has_many :through + unscope のテストを追加(+22)
    • relation/select_test.rbselectunscope の組み合わせに関するテストの修正(+1/-1)
    • author.rb, comment.rb, post.rb のテスト用モデルにフィールドや関連定義を追加
    • schema.rb にそれに対応するカラムを追加

イメージしやすい挙動例

(実テストコードとは異なる可能性がありますが、変更内容のイメージ用です)

ruby
class Author < ApplicationRecord
  has_many :posts
  has_many :comments, through: :posts
end

class Post < ApplicationRecord
  belongs_to :author
  has_many :comments
end

class Comment < ApplicationRecord
  belongs_to :post
end

# 例: Author#comments 経由の has_many :through
author = Author.first

# 関連にデフォルトスコープや where 条件が含まれているとして
relation = author.comments.where(approved: true).where(post_id: 1)

# ここで unscope を使う
relation = relation.unscope(:where)              # すべての where を外す
relation = relation.unscope(where: :approved)    # approved に対する where だけを外す

この PR 以前は、内部的に生成される SQL / relation ツリーの中で、approvedpost_id がどのテーブルに属すのかが曖昧なケースがあり、unscope(where: :approved)comments ではなく posts 側の同名カラムにマッチしようとしたり、逆に何も外せなかったりするケースがありました。

この PR では、association scope 構築時に「どのテーブルのスコープか」を明確に持たせ、unscope に渡された値を内部的に「テーブル名付きのカラム」として扱うことで、期待どおりのテーブルから条件を外すようにしています。


  1. 影響範囲・注意点
  • 影響を受けるのは主に以下のケースです
    • 関連経由(has_many, has_many :through, belongs_to …)で構築された Relation に対して
    • unscope を呼び出し
    • かつ複数テーブルにまたがる同名カラムや条件が存在する場合
  • 期待される変化
    • これまで「動くには動いていたが、どの条件が外れているか分かりづらかった」「バグっぽい挙動だった」unscope が、テーブル名レベルで正確に動くようになります
    • Issue #48548 で報告されているような誤動作は解消されます
  • 互換性・注意点
    • SQL レベルで生成されるクエリが多少変わる可能性があります(対象テーブルが明確になるため)
    • もし既存コードが「たまたま」曖昧な挙動に依存していた場合(例: 本来外してほしくない方が外れていた/その逆)、この PR 適用後は挙動が変わる可能性があります
    • 複数テーブルにまたがる複雑なクエリで unscope(where: :column_name) のような指定をしている場合は、テストを一度走らせて SQL の差分を確認しておくと安全です

  1. 参考情報 (あれば)

#57434 Run LoadAsync*ThreadPoolExecutorTest on the replaced pool to prevent connection leak

マージ日: 2026/5/22 | 作成者: @yahonda

  1. 概要 (1-2文で)
    LoadAsyncMultiThreadPoolExecutorTestLoadAsyncMixedThreadPoolExecutorTest で、接続プールの差し替えタイミングを変更し、テスト実行時に PostgreSQL コネクションがリークして too many clients になる問題を防ぐ修正です。テスト専用の変更であり、本番コードには影響しません。

  1. 変更内容の詳細

問題の原因

対象テストでは、「非同期ロードをマルチスレッド用プールで動かす」ことを検証するために、ActiveRecord::Base / ARUnit2Model の接続プールを :multi_thread_pool に差し替えています。

以前は以下のような流れになっていました:

  1. Rails のテストフレームワークが Transactional fixtures により
    • テスト開始時 (setup より前) に arunit / arunit2 の「元の接続プール」でトランザクションを開始する。
  2. 各テストの setup 内で establish_connection を呼び出し、
    • ActiveRecord::Base / ARUnit2Model の接続プールを :multi_thread_pool に差し替える。
  3. 元の接続プール上には、すでに fixtures 用トランザクションを張ったままのコネクションが 1 本いる。
  4. テスト終了時にプールを切り替え直す/切り離す (disconnect!) 処理を行うが、
    • ConnectionPool#disconnect! が「そのトランザクション保持中のコネクションを排他的に取る」ことができずタイムアウトする。
    • この結果、PG のソケットが閉じられずにリークし、テストスイート全体としては FATAL: sorry, too many clients already を引き起こす。

以前は setupensure で接続をすぐ戻していたため、そもそも :multi_thread_pool が十分に行使されておらず、リークも目立っていませんでした。ensure 削除によってテスト自体は正しくなった一方で、上記のリークが表面化しています。

今回の修正方針

トランザクション開始より前に接続プールを差し替え
トランザクション終了後に元に戻すよう、フックポイントを調整しています。

  • これまで:
    • プール差し替え: setup
    • プール復元: teardown 相当 (以前は ensure で即時復元、その後の変更でそこがなくなった)
  • これから:
    • プール差し替え: before_setup で行い、その後に super を呼ぶ
      → Transactional fixtures が開始する時点で、すでに :multi_thread_pool 側のプールが使われる
    • プール復元: after_teardown で行い、その前に super を呼ぶ
      → テストと fixtures トランザクションが完全に終わってから、元のプールに戻す

これにより、

  • フィクスチャのトランザクションは「差し替え後のプール」で開始される。
  • テスト終了時には、トランザクションも含めてすべてが差し替え後プール上で完結しているため、disconnect! がブロックされる状況が発生しない。
  • 結果として PG ソケットのリークが解消される。

コードイメージ(擬似的な例)

実際のパッチは 10 行追加・4 行削除程度ですが、イメージとしては以下のような構造になります:

ruby
class LoadAsyncMultiThreadPoolExecutorTest < ActiveRecord::TestCase
  def before_setup
    # ここで既存プールの保存 & :multi_thread_pool への差し替え
    swap_to_multi_thread_pool
    super  # ← ここで transactional fixtures などが実行される
  end

  def after_teardown
    super  # ← すべてのテスト処理・トランザクション終了後
    restore_original_pool
  end

  # テスト本体は従来どおり
end

ポイントは、

  • before_setup先に プールを差し替え、その 後に super を呼ぶこと
  • after_teardown先に super でテスト/トランザクションを完全に終わらせ、その 後に プールを元に戻すこと

という「フックの順序」を明確に変えている点です。


  1. 影響範囲・注意点
  • 影響範囲:
    • 対象は activerecord のテストコード (特に load_async_test.rb 内の LoadAsyncMultiThreadPoolExecutorTest / LoadAsyncMixedThreadPoolExecutorTest) のみです。
    • ランタイムの Active Record や接続プール実装には一切変更はなく、Rails アプリケーションの本番挙動には影響しません。
  • 効果:
    • PostgreSQL のコネクションリークが、計測上ほぼ「フィクスチャが最低限使う 2 コネクション」にまで削減されています。
    • これにより、CI の activerecord postgresql ジョブで発生していた too many clients に起因する失敗が軽減/解消されると期待できます。
  • 注意点:
    • この PR は「テストフレームワークと connection pool 置き換えを併用する場合の落とし穴」の具体例であり、同様に establish_connection をテスト中で動的に切り替えている他のテストがないか注意するとよいです。
    • 特に、Transactional fixtures 利用時は「トランザクション開始前に接続プールをセットアップする」ことが安全なパターンとして再確認できます。

  1. 参考情報 (あれば)
  • この PR の経緯・計測方法:
  • テスト実行条件 (リーク確認に使用したもの):
    • Docker: postgres:alpine (PostgreSQL 18)
    • 設定: max_connections=100
    • 対象テスト: -n /LoadAsync(Multi|Mixed)ThreadPoolExecutorTest/
    • シード: --seed=15223 (ベースラインで最悪ケース)
    • N=5 回実行、毎回コンテナ再起動
  • 計測結果:
    • 修正前ピーク接続数: 25, 23, 23, 26, 28
    • 修正後ピーク接続数: 2, 2, 2, 2, 2(fixtures 基本分のみ)

#57432 Rename reset_connection to reset_pool; have reset_connection reset just the existing connection

マージ日: 2026/5/22 | 作成者: @yahonda

  1. 概要 (1-2文で)
    このPRは、テストで使われている reset_connection ヘルパーの意味と実装を整理し、コネクションプールごと作り直す挙動を新しい reset_pool に切り出すことで、PostgreSQL テスト群で発生していたコネクションリーク(too many clients)を解消するものです。既存のテストはメソッド名そのままで、より安全な「既存コネクションのリセット」だけを行うようになります。

  1. 変更内容の詳細

背景・問題点

  • PostgresqlRangeTest, PostgresqlEnumTest, PostgresqlCompositeTest, PostgresqlDomainTest などが teardown で reset_connection を呼んでいた。

  • reset_connection の実装は実質的に:

    ruby
    # 旧: reset_connection のイメージ
    def reset_connection
      ActiveRecord::Base.remove_connection
      ActiveRecord::Base.establish_connection
    end

    のように「接続プールを破棄して作り直す(プールの差し替え)」動作をしていた。

  • しかし Rails のテストは「トランザクション・フィクスチャ」を使っており、テスト用トランザクションがすでにあるコネクションを掴みっぱなしにしている。

  • その状態でプールを差し替えようとすると ConnectionPool#disconnect! がタイムアウトしてしまい、古い PG ソケットが閉じられずリークする。その結果、CI で FATAL: sorry, too many clients already が発生。

新しい方針

テストの目的は「キャッシュされたクエリプランやセッション状態を捨てたい」のであって、「プールを完全に作り直したい」わけではないテストがほとんど。

そこで今回のPRでは、ヘルパーを次のように整理しています。

  1. reset_connection の意味を変更

    • 「プールを作り直す」のではなく、「今使っているコネクションの状態だけリセットする」ヘルパーに変更。
    • 実装としては、アダプタの #reset! を呼び出す形になる(PostgreSQL アダプタでは DISCARD ALL を発行してセッション状態をリセットする)。

    イメージ:

    ruby
    # 新: reset_connection
    def reset_connection
      @connection&.reset!
    end

    ※実際には @connection の取得方法などは connection_helper.rb 内で適切に行われている。

  2. 旧来の「プール差し替え」挙動を reset_pool にリネーム

    • これまでの reset_connection (= remove + establish)の実装は reset_pool という別メソッド名に切り出される。
    • 「アダプタそのものを入れ替えたい」テストだけが reset_pool を明示的に呼ぶように修正される。

    たとえば:

    ruby
    # 新: 接続プール毎リセットしたいとき
    reset_pool

実際の変更箇所のポイント

変更は次のファイルに限定されています。

  • activerecord/test/support/connection_helper.rb

    • reset_connection の実装を「現在の接続を reset! する」に変更。
    • 旧来の「プール差し替え」ロジックを reset_pool として定義。
  • activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb

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

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

  • activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb

    • これらのテストのうち、本当に「新しいアダプタインスタンス/プールを要求する」ケースだけが reset_connection から reset_pool に呼び出しを変更。
    • 特に PostgreSQLReferentialIntegrityTest はアダプタにモジュールを extend して挙動を変えるため、「その変更を取り除いた新しいアダプタ」が必要 → reset_pool を使うべき対象。

一方で、リークの原因だった4テスト (PostgresqlRangeTest, PostgresqlEnumTest, PostgresqlCompositeTest, PostgresqlDomainTest) は PR の diff には出てきません。
これらはメソッド名としては引き続き reset_connection を呼び続けますが、中身の実装が変わったことで、コネクションプールを交換せず、既存コネクションのリセットだけを行うようになり、リークが解消されるという構造になっています。

効果測定

  • テスト対象: 4つのリーク元テストファイルのみ

  • 環境:

    • postgres:alpine (PostgreSQL 18・max_connections=100)
    • --seed=22021(リーク量が最大になったシード)
    • N=5 回実行(毎回コンテナを再起動して計測)
  • 結果(ピーク接続数っぽいメトリクス):

    状態peak (5 runs)
    BEFORE9, 12, 7, 7, 8
    AFTER2, 2, 2, 2, 2 (= フィクスチャのベースライン)

→ テストが max_connections に近づくことなく、リークが抑制されている。


  1. 影響範囲・注意点
  • 影響範囲は基本的に「テストコードのみ」

    • 変更は activerecord/test/...activerecord/test/support/... に限られており、ランタイムの ActiveRecord コードや本番アプリケーションの挙動には影響しません。
    • PR 作者も「CHANGELOG は不要、test-only fix」と明記。
  • reset_connection の意味が変わる点に注意

    • これまで「プールごと作り直す」意味合いで reset_connection を使っていた独自テストヘルパーが Rails 本体にあった場合は、このPRを取り込むと挙動が変わります。
    • Rails 本体側では、プールを再構築したい箇所はすべて reset_pool に移行しているので、同様のパターンを自分のプロジェクトで真似しているなら、呼び出し先の名前・意味が変わっていないか要確認です。
  • トランザクションフィクスチャ + コネクションプール操作の組み合わせは要注意

    • このPRが示している通り、トランザクションフィクスチャがアクティブな状態で remove_connection などを行うと、コネクションがプールからうまく返却されずにソケットリークを引き起こす可能性があります。
    • テストで「セッション状態だけリセットしたい」場合は、プールを再構築するよりもアダプタの #reset! を呼ぶアプローチの方が安全です。

  1. 参考情報 (あれば)

このPRは「reset_connection = 既存接続の DISCARD ALL」「reset_pool = remove + establish でプール再構築」という明確な役割分担を導入し、テストでのコネクション管理をより安全にした修正と捉えると分かりやすいです。


#57446 Include valid values in ActiveRecord::Enum invalid-value error

マージ日: 2026/5/22 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    ActiveRecord の enum に無効な値を代入したときに発生する ArgumentError のメッセージに、「どの値が有効か」を列挙して表示するようにした変更です。例外クラスや発生タイミングは変わらず、エラーメッセージの内容だけが改善されています。

  1. 変更内容の詳細

これまでの挙動

ruby
class Book < ApplicationRecord
  enum status: {
    proposed: 0,
    written:  1,
    published: 2,
  }
end

Book.new(status: "bogus")
# ArgumentError: 'bogus' is not a valid status

無効な値(enum に定義されていない値)を渡すと ArgumentError になりますが、メッセージには「何が有効なのか」が含まれていませんでした。

変更後の挙動

この PR では、同じ ArgumentError に、有効な値のリストを含めるようにしています:

ruby
Book.new(status: "bogus")
# ArgumentError: 'bogus' is not a valid status. Valid values are: "proposed", "written", "published"
  • 例外クラス: 変更なし (ArgumentError)
  • 例外の発生条件: 変更なし(enum に未定義の値を渡したとき)
  • メッセージ内容:
    • 先頭の " 'bogus' is not a valid status" 部分は据え置き
    • 後ろに Valid values are: ... と有効な enum のキー一覧を追記

実装のポイント

  • 変更は activerecord/lib/active_record/enum.rb の 1 行のみ。
  • enum の定義に使われたキー(シンボル/文字列)の「キー側」(例えば { proposed: 0 }:proposed)をそのまま列挙。
  • 出力には Symbol#inspect を使っているため、以下のように常に引用符付きで表現され、曖昧さを避けています:
    • :proposed"proposed"
    • "proposed""proposed"
      いずれの場合も "proposed" として表示されるので、開発者にとって読みやすくなっています。

テストの変更

activerecord/test/cases/enum_test.rb にある、従来のエラーメッセージ文字列を厳密に比較していたテストを修正しています。

  • 以前: 全メッセージ文字列を assert_equal 的に厳密比較していた。
  • 変更後:
    • 先頭部分(従来からある "is not a valid" まで)を assert_match で確認。
    • さらに "proposed" など既知の有効値が含まれているかを assert_match で確認。
      → 将来的に enum のマッピング順序が変わっても、テストが壊れにくい構成になっています。

CHANGELOG

  • activerecord/CHANGELOG.md に今回の挙動変更を追記。
  • 変更前後の例(エラーメッセージの Before / After)が記載されており、利用者が挙動変更をすぐに把握できるようになっています。

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

    • ActiveRecord の enum を利用しているすべてのアプリケーションで、「無効な enum 値を代入したときのエラーメッセージ」が変わります。
    • 実行時の挙動としては「メッセージ文字列だけ」が変わるため、アプリケーションロジックへの直接的な影響はほぼありません。
  • 注意点:

    • 例外メッセージの文字列をテストで厳密に比較している場合(assert_equal / == などで全文比較している場合)、今回の変更でテストが失敗する可能性があります。その場合は assert_matchinclude? などに修正した方がよいです。
    • エラーメッセージをパースして何らかの処理をしている(ログ収集の正規表現マッチなど)場合は、新しい末尾部分(Valid values are: ...)が追加されることを考慮する必要があります。

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/57446
  • テストコマンド(PR での検証内容):
    • ARCONN=sqlite3_mem bundle exec ruby -Itest test/cases/enum_test.rb
    • bundle exec rubocop activerecord/lib/active_record/enum.rb activerecord/test/cases/enum_test.rb
  • この変更は軽量(+13/-2 行)かつ後方互換性が高い UX 改善であり、enum 利用時のデバッグ体験が向上します。

#57437 Match HTML attribute quotes to actual output in ActionView docs [ci skip]

マージ日: 2026/5/22 | 作成者: @55728

  1. 概要 (1-2文で)
    ActionView のヘルパー (simple_format, debug, button など) が実際に出力する HTML と、RDoc のサンプルコード内の HTML 属性のクォート表記(シングル/ダブル)が食い違っていたため、ドキュメント側を実際の出力(ダブルクォート)に揃えた PR です。コードの挙動は一切変えず、ドキュメントのみの修正です。

  1. 変更内容の詳細

背景

  • Rails の ActionView ヘルパーは HTML 属性を ダブルクォートclass="description")で出力する。
  • 一方で、RDoc のサンプルには シングルクォートclass='description')で書かれている例がいくつか存在しており、これをそのままテストの期待値などにコピペすると、実際のヘルパー出力と一致しない状況になっていた。
    • 例: simple_format / button / debug のサンプル。

具体的な修正箇所

対象は RDoc のコメント中のサンプルコードのみで、Ruby コード自体は変更されていません。

  1. actionview/lib/action_view/helpers/text_helper.rb

    • simple_format の例で以下のような不一致があったものを修正:

      修正前(ドキュメントの例):

      ruby
      # => "<p class='description'>Look ma! A class!</p>"

      修正後:

      ruby
      # => "<p class=\"description\">Look ma! A class!</p>"

      Ruby の文字列リテラル内に HTML を書いているため、内側のダブルクォートは \" でエスケープされています。他の simple_format の例と同じスタイルに揃えています。

  2. actionview/lib/action_view/helpers/debug_helper.rb

    • debug ヘルパーの出力例を、実際の出力に合わせて修正:

      修正前(イメージ):

      html
      <pre class='debug_dump'>

      修正後:

      html
      <pre class="debug_dump">
  3. actionview/lib/action_view/helpers/form_helper.rb

    • button ヘルパーの 3 つの例を修正(いずれも素の HTML 例):

      修正前(例):

      html
      <button name='button' type='submit'>Create article</button>

      修正後:

      html
      <button name="button" type="submit">Create article</button>

あえて変更していない箇所

  • form_tag_helper.rb:177select_tag のドキュメントに出てくる selected='selected' は、その文字列を raw(...) で「そのまま流し込む」例であり、Rails が生成した属性ではないため、意図的にシングルクォートのまま残しています。

検証方法

  • ローカルで各ヘルパーを実行し、ダブルクォートで出力されることを確認:

    ruby
    simple_format("Look ma! A class!", class: "description")
    # => "<p class=\"description\">Look ma! A class!</p>"
    
    debug(some_user)
    # => "<pre class=\"debug_dump\">…</pre>"
    
    button("Create article")
    # => "<button name=\"button\" type=\"submit\">Create article</button>"
  • grep -rnE "# =>.*<[a-z]+ [a-z]+='[^']+'" actionview/lib actionpack/lib で、シングルクォート属性を含む RDoc サンプルを洗い出し、本 PR で修正した箇所以外に不整合がないことを確認。残っているヒットは上記の意図的な select_tag のケースのみ。


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

    • 実行時の挙動には一切変更なし。
    • 影響があるのは RDoc ベースのドキュメントおよびそれを参照している読者のみ。
  • 注意点・開発者目線でのポイント

    • これまでドキュメント例をそのままテストの期待値などに使っていた場合、今後生成されるドキュメントを見てコピペすると、実際のヘルパー出力と正しく一致するようになる。
    • 既存のコードやテストで、ドキュメント例(シングルクォート)に合わせた期待値を書いている場合は、元々実際の出力とズレている可能性があるので、一度確認するとよい。
    • HTML としてはシングル/ダブルどちらも構文的には正しいが、Rails のヘルパーはダブルクォートを一貫して使っているため、今後もドキュメントはダブルクォートを前提に読むとよい。
  • CI・互換性

    • [ci skip] が付いており、テストも不要なドキュメント専用変更。
    • 互換性やマイグレーションは不要。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57437
  • 対象モジュール:
    • ActionView::Helpers::TextHelper#simple_format
    • ActionView::Helpers::DebugHelper#debug
    • ActionView::Helpers::FormHelper#button
  • Rails のヘルパーが生成する HTML の属性クォートは「ダブルクォート」がデファクトである点は、他のヘルパー出力や既存テストからも裏付けられている。

#57445 Fix incorrect SQL examples in ActiveRecord::QueryMethods docs

マージ日: 2026/5/22 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    ActiveRecord::QueryMethods のドキュメント中に記載されていた SQL のサンプルが、実際に Rails が生成する SQL と食い違っていた問題を修正した PRです。コードの挙動には一切影響せず、コメント(ドキュメント)のみの修正です。

  1. 変更内容の詳細

対象ファイル: activerecord/lib/active_record/relation/query_methods.rb
変更はいずれも「コメント中に記載された SQL 例」のみです。

(1) where.associated の JOIN 例で ON が抜けていた

元のドキュメント例では、JOIN 句に ON がなく、SQL として不正な形式になっていました。

修正前:

ruby
#    # LEFT OUTER JOIN "authors" "authors"."id" = "posts"."author_id"

修正後:

ruby
#    # LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id"

Rails 実装が実際に生成する SQL には ON が入るため、それに合わせた形です。

(2) where.associated のテーブル名が単数形 "author" になっていた

同じく where.associated の例で、WHERE 句のテーブル名が実際とは異なっていました。

修正前:

ruby
#   #  WHERE "author"."id" IS NOT NULL

問題点:

  • "author" と単数形になっており、他の例や実際の SQL と不整合
  • コメントのインデントも他行とずれている(# # ...

修正後:

ruby
#    # WHERE "authors"."id" IS NOT NULL
  • テーブル名を "authors" に変更
  • インデントを他の行と揃え (# # ...)、ブロック内の整形を統一

(3) regroup の MySQL サンプルで * をバッククォートで囲っていた

MySQL のバックスラッシュ付き識別子引用(`...`)はカラム名やテーブル名などの「識別子」に使うべきですが、* はワイルドカードであり識別子ではありません。
従来の例は次のようになっており、実際には SQL エラーになる形式でした。

修正前:

ruby
#   # SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`title`, `posts`.`body`
#   # SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`title`

修正後:

ruby
#   # SELECT `posts`.* FROM `posts` GROUP BY `posts`.`title`, `posts`.`body`
#   # SELECT `posts`.* FROM `posts` GROUP BY `posts`.`title`

この形式は、ファイル内の他の regroup / group の MySQL 例とも整合しており、かつ実際に有効な SQL です。


  1. 影響範囲・注意点
  • 実行コードは一切変更されておらず、挙動・パフォーマンス・互換性への影響はゼロです。
  • ドキュメントの SQL サンプルが正しくなったことで、
    • ドキュメントの SQL をそのままコピー&ペーストして利用してもエラーになりにくくなります。
    • Rails が実際に生成する SQL とドキュメントの表記が一致するため、デバッグ・学習時の混乱が減ります。
  • CHANGELOG は Rails の慣例に従い、ドキュメントのみの修正のため更新されていません。
  • CI も [ci skip] 付きコミットでスキップされており、テストランはありません (必要でもない変更内容です)。

  1. 参考情報 (あれば)
  • 対象クラス: ActiveRecord::Relation::QueryMethods
    • where.associated / regroup 等のクエリビルダ API のドキュメントコメント
  • 本 PR: rails/rails リポジトリの #57445
    • タイトル: “Fix incorrect SQL examples in ActiveRecord::QueryMethods docs”
    • マージ日時: 2026-05-22T21:55:13Z

#57444 Freeze LookupContext default proc internals

マージ日: 2026/5/22 | 作成者: @paracycle

  1. 概要 (1-2文で)
    ActionView の ActionView::LookupContext::Details::LookupContext::Accessors において、テンプレート詳細情報の DEFAULT_PROCS の扱いを見直し、「実質的に不変」な内部状態を freeze できるようにした PR です。Ractor 対応の一環として、変更可能な定数をやめ、毎回凍結されたクラスインスタンス変数を差し替える方式にしています。

  2. 変更内容の詳細

背景・目的

  • Rails を Ractor 対応するためには、「本来不変であるべき定数」やグローバル状態を凍結する必要があります。
  • しかし LookupContext::Accessors::DEFAULT_PROCS は、実行時に register_detail などで更新され得るため、そのまま freeze すると壊れてしまいます。
  • この PR では、「定数として公開されているオブジェクト」は凍結しつつ、「更新が必要なときには新しい凍結済みオブジェクトに差し替える」パターンに移行しています。

具体的な変更点

※ 実際の diff は 9 行追加 / 7 行削除のみの小改修ですが、概念的には以下のような変更です。

1. DEFAULT_PROCS 定数の実体を「クラスインスタンス変数」に移動

従来(イメージ):

ruby
module ActionView
  class LookupContext
    module Details
      module Accessors
        DEFAULT_PROCS = {
          formats: -> { ... },
          locale:  -> { ... },
          # ...
        }

        class << self
          attr_reader :registered_details
          # ...
        end
      end
    end
  end
end

DEFAULT_PROCS は定数としつつも、内部的には DEFAULT_PROCS[:something] = proc のように変更されていたため、「不変」とは言えない状態でした。

今回(イメージ):

ruby
module ActionView
  class LookupContext
    module Details
      module Accessors
        class << self
          # 凍結された Hash を保持するクラスインスタンス変数
          def default_procs
            @default_procs
          end

          def register_detail(name, &block)
            # 既存の @default_procs から dup して書き換え、
            # 最後に freeze した上で @default_procs に再代入
            new_default_procs = @default_procs.dup
            new_default_procs[name] = block
            @default_procs = new_default_procs.freeze

            # registered_details もここで更新 …
          end
        end

        # 旧 DEFAULT_PROCS 定数は「初期値の参照」程度 or 廃止方向
      end
    end
  end
end

ポイント:

  • DEFAULT_PROCS という「(見かけ上)不変であってほしい定数」は、実際には中身を書き換えない。
  • 更新が必要なときは、
    1. 現在の @default_procsdup
    2. 新しいエントリを追加
    3. freeze して @default_procs に置き換え
  • これにより「常に凍結済みの Hash を参照している」状態が保たれます。

2. registered_detailsDEFAULT_PROCS のキーを一本化

説明文にある通り:

we can use the keys of the same list as the registered_details list, thus removing a class instance variable in the process.

  • これまで:
    • registered_details 用の配列/セット
    • DEFAULT_PROCS のキー が「ほぼ同じ情報を別々に持っていた」状態。
  • これを整理し、
    • default_procs のキー情報をそのまま registered_details として利用
    • もしくはその逆、といった形で「二重管理」していたクラスインスタンス変数を削減。

これにより、状態管理がシンプルになり、Ractor 対応上の「共有状態」の表面積も減ります。

  1. 影響範囲・注意点

影響範囲

  • 対象クラス/モジュール:
    • ActionView::LookupContext::Details::Accessors およびその周辺のみ。
  • 主に影響を受けるのは以下のようなケースです:
    • ActionView の LookupContext をカスタマイズし、register_detail 相当を直接呼び出しているコード
    • DEFAULT_PROCS 定数を直接読み書きしている独自拡張

挙動上の変化

  • 「標準的な使い方」(ビューの formats, locale, variants 等の通常の解決処理)における挙動は、原則として変わりません。
  • ただし、
    • DEFAULT_PROCS の中身を書き換える(DEFAULT_PROCS[:formats] = -> { ... } など) といったハック的なコードを書いていた場合:
    • 今後そのような直接書き換えはできなくなる(もしくは非推奨)で、register_detail などの正式 API を通す必要が出てきます。
  • マルチスレッド / Ractor 環境下では、default_procs が常に凍結されているため、読み取り専用のデータ構造として安全に共有できることが期待されます。

パフォーマンス面

  • register_detail 実行時に Hash#dup + freeze を行うオーバーヘッドが増えますが、register_detail 自体は通常アプリケーション起動時やフレームワーク初期化時にのみ呼ばれることがほとんどのため、実行時のリクエスト処理に対する影響は極小と考えられます。
  1. 参考情報 (あれば)
  • この PR は以下の PR のフォローアップです:
  • Ractor 対応の文脈:
    • Ruby 3 以降の Ractor では、「ミュータブルオブジェクト」を Ractor 間で共有できないため、「本来不変なはずの設定/定義オブジェクト」は積極的に freeze する方向に設計が進んでいます。
    • Rails では、ActionView を含む多くのコンポーネントで、こうした「不変オブジェクトの明確化 / 凍結」が進められています。

#57427 Fix #57401: Stop DRb service when shutting down parallel test workers

マージ日: 2026/5/22 | 作成者: @mugitti9

  1. 概要 (1-2文で)
    Rails の並列テスト(プロセス並列)実行時に、親プロセス側の DRb サービスが終了せずテストプロセスが終了しないことがある不具合が修正されました。shutdown 時に DRb.stop_service を呼ぶようにし、DRb の受け付けスレッドが残らないことをテストで保証しています。

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

背景

  • ActiveSupport::Testing::Parallelization は、プロセス並列実行 (parallelize / PARALLEL_WORKERS) のときに DRb を使って親子プロセス間のキューイングを行っています。
  • 親プロセスは Parallelization#initialize 内で DRb.start_service を呼んで DRb サービスを起動します。
  • しかし、これまでの #shutdown 実装では
    • キューサーバのシャットダウン
    • ワーカープロセスの Process.waitpid による回収 までは行うものの、親プロセス側で DRb.stop_service を呼んでいなかったため、DRb の accept-loop スレッドが生き残ってしまっていました。
  • 環境によってはこのスレッドが原因でプロセスが終了せず、CI などで「テストは全部終わって 0 failures が出ているのにプロセスが落ちずタイムアウトする」という症状が発生していました。
  • 子プロセス(ワーカー)は fork 後に DRb.stop_service を呼んでおり、親だけが後始末をしていない状態でした。

コード上の変更

親プロセスのシャットダウンで DRb を止める

ActiveSupport::Testing::Parallelization#shutdown の末尾に、DRb.stop_service 呼び出しが追加されています。

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

ruby
def shutdown
  # キューサーバ停止
  @queue.close

  # 子プロセスの wait
  @workers.each_value do |pid|
    Process.waitpid(pid)
  end

  # ★ 親プロセス側の DRb サービスを終了
  DRb.stop_service
end

これにより、親プロセスで立ち上げた DRb の accept-loop スレッドが確実に終了するようになります。

回帰テストの追加/拡充

activesupport/test/parallelization_test.rb に、DRb のスレッドリークを検出するテストが追加されています(112 行追加・80 行削除と、テストコードは比較的大きく整理/追加されています)。

ポイント:

  • shutdown 実行後に、Ruby VM 内のスレッド一覧を確認し、「DRb の accept スレッド」が残っていないことをアサートするテストが追加されています。
  • このテストは、修正ありの場合はパスし、DRb.stop_service をコメントアウトすると DRb スレッドがリークして失敗することが手元で検証されています。

テスト内容のイメージ:

ruby
def test_shutdown_stops_the_drb_service
  parallel = ActiveSupport::Testing::Parallelization.new(workers: 2)

  # 何らかの形で parallel を動かして…

  parallel.shutdown

  # shutdown 後に DRb の accept-loop スレッドが残っていないことを確認
  drb_threads = Thread.list.select { |t| t.name&.include?("drb") || t.backtrace&.any? { |l| l.include?("drb") } }
  assert_empty drb_threads
end

(実際の実装は名前や backtrace などでもう少し堅牢に判定している可能性がありますが、狙いは「DRb の受け付けスレッドが残っていないことを検証する」点です。)


  1. 影響範囲・注意点
  • 対象となるのは、プロセスベースの並列テストを使っているケースです:
    • parallelize(workers: N) を使っている
    • もしくは環境変数 PARALLEL_WORKERS を設定している
  • この変更によって、テスト完了後にプロセスが終了しない/CI がタイムアウトする、といった問題が 解消されることが期待されます。
  • 動作の変更点は「テスト終了時に DRb を正しく止める」だけであり、通常のテスト実行フローには影響しません。
  • 子プロセス側ではすでに DRb.stop_service を呼んでおり、今回の変更は親プロセスの後始末を揃えるものなので、後方互換性への影響は極めて小さいです。
  • もし DRb に依存した独自拡張をしており、親プロセスの DRb サービスをテスト後も使い続けるような特殊なコードがある場合は、その前提が崩れる可能性がありますが、Rails 標準の利用方法では問題にならないはずです。

  1. 参考情報 (あれば)
  • 対応 Issue: #57401
    「プロセス並列テストで DRb スレッドが残りプロセスが終了しない」系の報告がここで議論されていたと考えられます。
  • 対応 PR: #57427
    タイトル: “Fix #57401: Stop DRb service when shutting down parallel test workers”
  • 変更ファイル:
    • activesupport/lib/active_support/testing/parallelization.rb
      → シャットダウン処理に DRb.stop_service を追加
    • activesupport/test/parallelization_test.rb
      → DRb スレッドリークを検証する回帰テストを追加・整理

#57447 Improve insert_all error message to show key mismatch

マージ日: 2026/5/22 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    insert_all / upsert_all で、挿入するレコード同士のキーが揃っていない場合に発生する ArgumentError のメッセージが改善され、どのキーが「余分」または「不足」しているかがエラー文言に含まれるようになりました。これにより、大量データやカラム数が多いテーブルでのデバッグがしやすくなっています。

  1. 変更内容の詳細

これまでの挙動

insert_all / upsert_all は、渡された配列の各ハッシュが同じキー集合を持っていないと ArgumentError を投げますが、そのメッセージは以下のように非常にざっくりしたものでした。

ruby
Book.insert_all [
  { name: "Rework", author_id: 1 },
  { name: "Remote", author_id: 1, isbn: "1974522598" }
]
# ArgumentError: All objects being inserted must have the same keys

どのキーが問題なのか分からないため、広いテーブルや大量レコードのバッチ挿入では原因特定が大変でした。

変更後の挙動

エラーメッセージに「どのキーが余分か / 足りないか」が付加されます。

  • あるレコードに「余分なキー」が含まれているケース:
ruby
Book.insert_all [
  { name: "Rework", author_id: 1 },
  { name: "Remote", author_id: 1, isbn: "1974522598" }
]
# ArgumentError: All objects being inserted must have the same keys (extra: ["isbn"])
  • 逆に、「他のレコードにはあるキーが欠けている」ケース:
ruby
Book.insert_all [
  { name: "Remote", author_id: 1, isbn: "1974522598" },
  { name: "Rework", author_id: 1 }
]
# ArgumentError: All objects being inserted must have the same keys (missing: ["isbn"])

extra / missing それぞれについて、キーの配列がメッセージに含まれます。

実装のポイント

  • 対象ファイル: activerecord/lib/active_record/insert_all.rb
  • verify_attributes 内で、キー集合の差分(対称差分)をその場で計算し、エラー送出直前にメッセージへ追記しています。
  • ハッピーパス(キーが揃っている場合)では差分計算は行われないため、通常ケースのパフォーマンスへの影響は最小限です。
  • 新たなヘルパーメソッド追加などは行わず、既存ロジックの近くに小さく閉じた変更として実装されています(KISS/YAGNI の方針)。

テスト

  • 対象ファイル: activerecord/test/cases/insert_all_test.rb
  • 以下 2 パターンのテストが追加されています。
    • extra キーがある場合のエラーメッセージ検証
    • missing キーがある場合のエラーメッセージ検証
  • 既存テストで古いメッセージ文字列を前提としたものは存在しないことを grep で確認済み。

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

    • ActiveRecord::Persistence#insert_all および #upsert_all を利用しているコードで、挿入データのキー不一致により ArgumentError が発生する場合、そのメッセージ内容が変わります。
    • エラークラスや発生タイミングは変わらないため、rescue ArgumentError などでの制御フローには影響ありません。
  • 注意点

    • もしアプリ側で「エラーメッセージの文字列を厳密一致で判定」しているようなコード(例: ログフィルタや i18n を絡めた独自ラッパなど)がある場合は、メッセージ末尾に (extra: [...]) / (missing: [...]) が付与されることで期待とズレる可能性があります。
    • 実運用の観点では、エラー文言の部分一致やパターンマッチ(正規表現)などにしておく方が、今後の Rails 側の文言変更にも強くなります。

  1. 参考情報 (あれば)
  • 変更ファイル:
    • activerecord/lib/active_record/insert_all.rb
      • verify_attributes 内のキー検証とエラーメッセージ組み立てロジックが変更。
    • activerecord/test/cases/insert_all_test.rb
      • extra / missing パターン用のテストが追加。
    • activerecord/CHANGELOG.md
      • insert_all / upsert_all のエラーメッセージ改善が追記。

この変更はあくまで「エラーメッセージ改善」に留まるため、既存コードの動作仕様(挿入時にキー集合が一致している必要がある点)は変わっていません。


#57448 Fix grammar in ActiveRecord errors and integration doc comments

マージ日: 2026/5/22 | 作成者: @hammadxcm

  1. 概要 (1-2文で)
    ActiveRecord のエラーメッセージおよび to_param のドキュメントコメントにおける英語の文法ミスを修正した PR です。コードの挙動やテストには一切影響せず、コメントのみの変更です。

  1. 変更内容の詳細

activerecord/lib/active_record/errors.rb

ActiveRecord::Errors 内の ValueTooLong 例外の説明文が文法的に不完全だったため修正されています。

Before:

ruby
# Raised when a record cannot be inserted or updated because a value too long for a column type.

「because 以下」に動詞がなく、文として不自然でした。

After:

ruby
# Raised when a record cannot be inserted or updated because a value is too long for a column type.

sibling entries と同様に、「because 以下」が完全な節(節内に主語+動詞)になるように修正されています。
このコメントは、例外クラスの RDoc/yard などの API ドキュメントに反映される部分です。


activerecord/lib/active_record/integration.rb

to_param のドキュメントコメントの文法が誤解を招く表現だったため修正されています。

Before:

ruby
# or +nil+ if this record's unsaved.

record's が「所有格(record の unsaved という属性)」として読めてしまい、
意図している「record is unsaved(レコードが未保存である場合)」という意味が伝わりにくい表現でした。

After:

ruby
# or +nil+ if this record is unsaved.

これにより、record is unsaved という状態を表す、明確な英文になっています。
to_param の説明を読むユーザーにとって、「未保存レコードのときに nil を返しうる」という意味がより正確に伝わります。


  1. 影響範囲・注意点
  • 実行時挙動への影響なし
    変更はコメントのみであり、クラス定義やメソッド本体には一切手が入っていません。
    そのため、アプリケーションコードの挙動・既存テスト・マイグレーション等に影響はありません。

  • 対象バージョン
    この PR がマージされたブランチ(通常は main / master)以降の Rails で、
    API ドキュメントやエディタのホバー表示などに反映されます。

  • ドキュメント生成・参照時の改善

    • RDoc / YARD / Rails API サイトなどで、より自然で誤解のない英語説明が表示されます。
    • 英語コメントをリファレンスにしている開発者にとって、理解しやすくなります。
  • CI や CHANGELOG

    • コメントのみの修正のため、Rails の慣例として CHANGELOG エントリは追加されていません。
    • コミットメッセージに [ci skip] が付いており、CI はスキップされています。

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

    • activerecord/lib/active_record/errors.rb
      • ValueTooLong 例外クラス(または関連定義)のコメント
    • activerecord/lib/active_record/integration.rb
      • ActiveRecord::Integration#to_param のドキュメントコメント
  • 仕様・挙動に関する追加の読みどころ:

    • ValueTooLong を含む ActiveRecord のデータベース例外クラス一覧
    • to_param のオーバーライド例(resources ルーティングでの URL 生成をカスタマイズする場合など)

#57431 fix: validate subcommand in rails plugin command

マージ日: 2026/5/22 | 作成者: @ruyrocha

  1. 概要 (1-2文で)
    rails plugin コマンドで new 以外のサブコマンドが指定された場合、これまでは無視されて rails plugin new として実行されていた挙動を修正し、不正なサブコマンドであればエラーを表示して終了するようにした PR です。これにより、タイプミスや誤ったコマンド指定による意図しないプラグイン生成を防ぎます。

  1. 変更内容の詳細

これまでの挙動

bash
# 本来は Rails プラグインのサブコマンドは "new" のみ
rails plugin foo bar

本来であれば "foo" は不正なサブコマンドですが、従来は:

  • foo が黙って無視される
  • bar がプラグイン名として扱われる
  • 実質的に rails plugin new bar が実行される

という挙動になっていました。
つまり、サブコマンドをタイプミスしても検知されず、意図せずプラグインが作られてしまう可能性がありました。

修正後の挙動

PluginCommand#perform でサブコマンドの検証を行うように変更されています。

  • サブコマンドが new 以外の場合:
    • エラーメッセージを表示
    • プロセスをステータスコード 1 で終了

おおよそ以下のようなフローになっていると考えられます(擬似コード):

ruby
def perform(*args)
  subcommand = args.shift

  if subcommand != "new"
    $stderr.puts "Unknown command #{subcommand}. Only `rails plugin new` is supported."
    exit 1
  end

  # ここから先は従来通り `new` サブコマンドの処理
  # ...
end

※実際の文言は PR のコードに依存しますが、趣旨としては「new 以外はエラーにする」というものです。

テストの追加

railties/test/commands/plugin_test.rb にテストが追加されており、主に次を検証しています:

  • 不正なサブコマンド (foo など) を指定した場合に:
    • 適切なエラーメッセージが出力されること
    • プロセスがエラー終了すること
  • 正常なサブコマンド (new) については従来の挙動が保たれていること

CHANGELOG (railties/CHANGELOG.md) にも、この挙動変更がバグ修正として追記されています。


  1. 影響範囲・注意点
  • 影響を受けるコマンド:

    • rails plugin サブコマンド全般(現時点では事実上 rails plugin new のみが有効)
  • これまで「たまたま」動いていた以下のような書き方は、今後エラーになります:

    bash
    # これまでは "foo" を無視して plugin 名 "bar" として扱われていた
    rails plugin foo bar    # => 今後はエラー
  • CI やスクリプトで rails plugin を呼び出している場合:

    • サブコマンド指定に誤りがあると、以前は成功していた処理が失敗するようになります
    • ただし、正しい形式 rails plugin new my_plugin で呼んでいれば影響はありません
  • 新たに rails plugin にサブコマンドを追加しようとする場合:

    • PluginCommand#perform でのバリデーションロジックを更新しないと、新サブコマンドはエラー扱いになります
    • 将来的にサブコマンドを増やしたい場合は、このバリデーションを拡張する必要があります

  1. 参考情報 (あれば)
  • 対応 Issue: #57430
  • 修正対象クラス: railties/lib/rails/commands/plugin/plugin_command.rb
  • 変更の種類: バグ修正(挙動の明示的なエラー化・安全側への変更)

#56195 Allow create_join_table to accept a primary key

マージ日: 2026/5/22 | 作成者: @gsamokovarov

  1. 概要 (1-2文で)
    create_join_table:primary_key オプションを受け付けるようになり、従来は不可能だった結合テーブルへの主キー(特に複合主キー)の定義が可能になりました。デフォルトの挙動はこれまで通り主キー無し(id: false)のまま維持され、後方互換性も確保されています。

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

これまでの問題点

create_join_table は内部的に create_table を呼び出す際に常に id: false を指定していました。

ruby
# イメージ(従来)
create_table(name, id: false, **options) do |t|
  ...
end

このため、マイグレーション側で primary_key: を指定しても、id: false が優先されて主キーが一切作られませんでした。

  • Postgres の論理レプリケーションでは「全テーブルに主キーが必要」という制約があるため
  • join テーブルにも主キーを付けたいユースケース(特に複合主キー)に対応できていませんでした

その結果、create_join_table が使えず、

  • 代わりに create_table を直接使う
  • あるいはカスタムメソッド+手動で reversible なマイグレーションを書く

といった回避策が必要でした。

今回の変更点

  1. id: false をハードコードしないように変更

    内部で create_table を呼び出す際に、これまで固定で渡していた id: false をやめ、呼び出し元からのオプションを尊重するようにしました。

  2. デフォルトは引き続き id: false

    • 何も指定しない場合は、従来通り「id カラムなし・主キーなしの join テーブル」が作られます。
    • つまり、既存アプリのマイグレーションは挙動が変わりません。
  3. :primary_key を指定すると :id が自動的に有効化される

    オプションに primary_key: が指定された場合、内部的に id: オプションが自動で設定され、主キーありのテーブル定義になります。

    想定される UX は以下のようなものです:

    ruby
    # 新しい推奨の書き方(複合主キー)
    create_join_table :assemblies, :parts, primary_key: [:assembly_id, :part_id]

    これによって、基になる create_table 呼び出しでは:

    ruby
    create_table :assemblies_parts,
                 id: :primary_key,           # 内部的に自動設定されるイメージ
                 primary_key: [:assembly_id, :part_id] do |t|
      t.references :assembly
      t.references :part
    end

    のような扱いになり、複合主キーがちゃんと生成されるようになります。

    作者のコメントにもある通り、従来のインタフェースとしては:

    ruby
    # 冗長だが理屈としてはこう書ける形だった
    create_join_table :assemblies, :parts,
                      id: :primary_key,
                      primary_key: [:assembly_id, :part_id]

    という形もありえましたが、利用者にわかりやすくするために、

    • 利用者は primary_key: だけ指定すればよい
    • id: はライブラリ側でよしなに設定する

    というインタフェースに整理されています。

  4. テスト・ドキュメントの更新

    • activerecord/CHANGELOG.md に、この変更がリリースノートとして追記されています。
    • activerecord/test/cases/migration/create_join_table_test.rb に、primary_key: を使った join テーブル生成のテストが追加されています。
    • コード中の build_create_join_table_definition という :nodoc: なテスト専用メソッドに関して、「使われていなそうなので削除してよいか?」という問いかけが PR 説明に記載されています(この PR では削除されていません)。

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

    • create_join_table のデフォルトは依然として id: false であり、既存のマイグレーションは動作・生成スキーマともに変わりません。
    • 新たに primary_key: を指定した場合にのみ挙動が変わります。
  • Postgres 論理レプリケーションとの相性

    • これにより、論理レプリケーションを利用するアプリケーションでも create_join_table を安全に利用しやすくなります。
    • 特に、has_and_belongs_to_many 形式の join テーブルに複合主キーを付与したい場合に有用です。
  • 設計上の注意点

    • 複合主キーを採用すると、Active Record モデルの扱いが複雑になりがちです(Rails の標準 API は単一カラム主キーを前提にしている箇所が多いため)。
    • join テーブルをモデルとして直接扱わず、あくまで中間テーブルに留める(has_and_belongs_to_many など)ケースでは問題になりにくいですが、「中間モデルとして ActiveRecord モデルを定義する」場合は挙動をよく確認した方がよいです。
    • 今回の機能は「スキーマ定義として複合主キーを張れるようにする」ことが主目的であり、Active Record レベルでの複合主キーの完全サポートを追加したわけではありません。
  • 既存プロジェクトへの導入

    • 既に create_table で join テーブルを作成している場合、同じスキーマを create_join_table + primary_key: で再現できるようになりますが、マイグレーションの差分・ロールバック可能性をよく確認のうえで移行する必要があります。(既存テーブルを削除・再作成するようなマイグレーションは本番環境では慎重に検討すべきです。)

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/56195

  • 追加された主な API イメージ:

    ruby
    # 主キーなし(従来どおり)
    create_join_table :users, :roles
    
    # 単一主キーを指定したい場合(例: join テーブル用の surrogate key を持たせる)
    create_join_table :users, :roles, primary_key: :id
    
    # 複合主キー(今回の主なユースケース)
    create_join_table :assemblies, :parts, primary_key: [:assembly_id, :part_id]
  • Postgres 論理レプリケーション要件(全テーブルに主キー必須)との整合性確保のための変更であることが、PR 本文で明示されています。


#56193 Updates diagrams for dark mode compatibility and add EPUB fallback for SVG images [Guides]

マージ日: 2026/5/22 | 作成者: @shivabhusal

  1. 概要 (1-2文で)
    Rails Guides の関連ガイドに使われている図版を、ライト/ダークモードに自動対応する SVG に差し替えるとともに、EPUB 生成時に SVG を他形式へフォールバックする仕組みが追加されています。これにより、Web 版ではダークモードでも読みやすくなり、EPUB(Kindle/Apple Books など)では「画像が表示されない」問題が軽減されます。

  1. 変更内容の詳細

図版(アソシエーションの図)のアップデート

対象は association_basics ガイド内の以下の関連図です。

  • belongs_to
  • has_one
  • has_many
  • has_many :through
  • has_and_belongs_to_many (habtm)

それぞれについて:

  • 新たにダークモード対応の SVG 版ファイルが追加
    • 例:
      • guides/assets/images/association_basics/belongs_to.svg
      • .../has_many.svg など
  • 既存の PNG も *-light.png / *-dark.png といった形で整理されており、テーマに応じて適切に表示できるようになっています。

SVG 図版のポイント:

  • ライト/ダークモードに自動で追従するようなデザイン・色指定になっている(CSS の prefers-color-scheme などを前提とした配色)。
  • ベクタ画像なので、拡大しても劣化せず、ガイドの図がより鮮明に表示される。

※ PR 本文では「アニメーションやロスレス品質のために SVG を選択」と言及されていますが、実際にアニメーションしているかはコードを見ないとわかりません。少なくとも「将来的にアニメ可能で高品質」という意図を持った形式選定です。

EPUB 生成処理での SVG フォールバック

EPUB 向けには、以下の課題がありました。

  • 多くの EPUB リーダー(特にネイティブアプリ)が SVG を完全にはサポートしていない
  • Apple Books で Rails Guides の EPUB を開いたとき、一部の図版がレンダリング失敗する
  • ダークモード対応も EPUB 側では問題になっていた

この PR では、rake guides:generate:kindle(EPUB 生成)実行時に:

  • epub.rb 内で SVG ファイルを別形式に差し替える処理 を追加
    • SVG がそのまま EPUB に入らないようにし、PNG 等の互換性の高い形式に置き換えて埋め込む
    • これにより、EPUB 出力で画像が欠落する/表示されない問題を改善

疑似的なイメージコード(実際のコードとは異なる可能性がありますが、やっていることは概ねこういう処理です):

ruby
# guides/epub.rb のどこかで
def image_path_for_epub(path)
  if File.extname(path) == ".svg"
    # 同名 PNG を探して差し替える、など
    fallback = path.sub(/\.svg\z/, ".png")
    return fallback if File.exist?(fallback)
  end

  path
end

実際には、HTML 生成時に埋め込まれる <img src="..."> を走査し、拡張子 .svg のものをフォールバックパスに書き換える処理などが入っているはずです。

テスト手順(PR 記載内容)

  • Web 版:
    • PR 作成者の提供するプレビュー URL(例: association_basics#belongs-to)にアクセス
    • OS やブラウザのテーマをライト/ダークで切り替え、図版が見やすく変化するかを確認
  • EPUB 版:
    • 該当ブランチ auto-color-scheme-svg を checkout
    • cd guides && rake guides:generate:kindle
    • output/epub/ruby_on_rails_guides_[hash].epub を Apple Books などの非 Web ベースリーダーで開く
    • 公開中の公式 EPUB (v8.1.1) と比較し、図版が欠けずに表示されているかを確認

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

    • Rails Guides のうち「Active Record Associations(association_basics)」に掲載されている図版全般
    • epub.rb を通じて生成される EPUB/Kindle 用ガイドすべて(他のガイドで SVG を使った場合もフォールバック処理の影響を受ける)
  • 既存コンテンツとの互換性:

    • Web 版:
      • 既存の PNG 図版から SVG に差し替わるため、古いブラウザでの SVG サポートが極端に弱い環境では表示に問題が出る可能性がありますが、現代的なブラウザならほぼ問題ありません。
    • EPUB 版:
      • これまで「画像が表示されない」状況だった環境では改善が見込まれます。
      • フォールバック用の画像ファイル(PNG 等)がきちんと存在することが前提になるため、将来別の SVG を追加する際は「EPUB 用の代替画像も用意されているか」「epub.rb の変換ロジックで拾える配置・命名か」を注意する必要があります。
  • ダークモード対応:

    • SVG のスタイルが prefers-color-scheme に依存している場合、ブラウザや EPUB リーダー側がこれをサポートしていないとライトモード前提の配色で見える可能性があります。
    • EPUB 側はフォールバック画像がどちらのテーマ前提になっているか(ライト/ダークまたは中庸な配色か)が実際の表示結果に影響します。
  • ビルド関連:

    • rake guides:generate:kindle のフローが変わるため、自前でガイドの EPUB を生成している人は、一度生成物を確認して差分・レイアウト崩れがないかを見ることを推奨します。

  1. 参考情報 (あれば)

#57435 Add missing docs for Rails.autoloaders

マージ日: 2026/5/22 | 作成者: @fxn

  1. 概要 (1-2文で)
    Rails 7系(?) で導入されている Rails.autoloaders の公開インターフェイスについて、ガイドには書かれていたものの、APIドキュメント(RDoc/YARD)側に欠けていた説明を追加するPRです。コードの挙動には手を入れず、railties/lib/rails.rb にドキュメントコメントを追加するだけの変更です。

  1. 変更内容の詳細
  • 対象ファイル: railties/lib/rails.rb
  • 変更内容: Rails.autoloaders 関連の公開APIに対し、RDoc/YARD形式のドキュメントコメントを約 53 行分追加

主なポイント:

  • Rails.autoloaders メソッドおよびそこからアクセスできるインターフェイスについて、

    • どのようなオブジェクトを返すのか
    • どのような用途で使うのか(例: main / once autoloader へのアクセス、設定の変更など)
    • 既存のガイド(autoloading_and_reloading_constants)に書かれている内容と整合を取った説明
      を API ドキュメント形式で明示。
  • 動作やシグネチャの変更はなく、「ガイドにしか書かれていなかった説明」を Rails.autoloaders のメソッド定義の直上にコメントとして移植/再構成したイメージです。

サンプル的な利用イメージ(PRそのものには含まれていないが、今回ドキュメントで説明される想定の使い方):

ruby
# メインのオートローダー (再読み込みあり)
Rails.autoloaders.main

# 一度だけロードするオートローダー (再読み込みなし)
Rails.autoloaders.once

# 設定例: あるパスをオートロードの対象から除外
Rails.autoloaders.main.ignore(Rails.root.join("lib/tasks"))

このようなパターンが、APIリファレンスからもたどれるようになります。


  1. 影響範囲・注意点
  • 実行時挙動・パブリックAPIの変更は一切なし

    • 既存コードへの互換性に影響はありません。
    • テストの追加や削除もなく、ロジック部分の差分はゼロです。
  • 影響範囲は「ドキュメント生成時」に限定

    • rails doc:rails などで生成される API ドキュメントに Rails.autoloaders の説明が追加されます。
    • エディタ/IDE が RDoc/YARD コメントを参照する場合、補完時のヘルプ情報が充実する可能性があります。
  • 注意点

    • 内容的には既にガイドに存在していた情報なので、新しい機能の追加や非推奨化の宣言ではありません。
    • 「Rails.autoloaders が正式に文書化された」という意味で、今後この API がより「パブリック」な位置づけとして扱われやすくなる可能性はありますが、このPR単体で API のステータスが変わったわけではありません。

  1. 参考情報 (あれば)
  • 元ガイド:
    Autoloading and Reloading Constants (Zeitwerk モード)
    https://guides.rubyonrails.org/autoloading_and_reloading_constants.html

    このPRは上記ガイド内で既に説明されていた Rails.autoloaders の内容を、APIドキュメント側に反映したものです。

  • 関連クラス/概念(補足)

    • Rails.autoloaders.main
      • 通常のアプリコード(app/models, app/controllers など)用。開発環境でのリロード対象。
    • Rails.autoloaders.once
      • 一度だけ読み込んで変更を前提としないコード(初期化処理・一部ライブラリなど)用。

これらの使い分けやメソッドの詳細が、今後は API ドキュメントからも参照しやすくなります。


#57406 [#57402] Fix Active Record Pool Reaper thread leak after Parallelization#shutdown

マージ日: 2026/5/22 | 作成者: @ruyrocha

  1. 概要 (1-2文で)
    Rails のテスト並列実行後に Active Record の Pool Reaper スレッドが親プロセス側にリークし、接続プールやファイルディスクリプタが解放されない問題を修正する PR です。Parallelization#shutdown 時にクリーンアップフックを実行し、Active Record が自前でプール破棄用フックを登録し、かつ Reaper スレッドを正しく停止・回収するようにしています。

  1. 変更内容の詳細

問題の構造

PR 説明によると、以下 3 層のバグが組み合わさって、並列テスト終了後も「Active Record Pool Reaper」というスレッドが親プロセスに残り続けていました:

  1. Parallelization#shutdown がクリーンアップフック (run_cleanup_hooks) を呼ばない

    • 並列テスト実行でフォーク/スレッドを使い終わった後、本来は親プロセス側で「テストが終わった後片付け」を行うためのフックがあるが、これが呼び出されていなかった。
  2. Active Record がクリーンアップフックに自分の後始末(接続プール破棄)を登録していない

    • フック自体が呼ばれていなかった上に、AR も「テスト並列実行が終わったら接続プールを破棄する」処理をフックに登録していなかった。
  3. clear_all_connections!disconnect! を呼んでいて Reaper スレッドを止めない

    • たとえクリーンアップフックが呼ばれても、disconnect! は「接続を切る」だけで、プールに紐づく Reaper スレッドを停止させないため、スレッドが生き残る。

この結果、長寿命な CI プロセスなどでテストスイートを順に何度も実行すると、親プロセスに Pool Reaper スレッドと DB コネクションがどんどん溜まっていく、という問題になっていました。


具体的な修正点

1. Parallelization#shutdown でクリーンアップフックを実行

対象ファイル: activesupport/lib/active_support/testing/parallelization.rb

  • shutdown メソッドの最後で、ワーカー終了後に run_cleanup_hooks を呼ぶように変更。
  • これにより、「テスト並列実行が終わったタイミング」で各種ライブラリ(Active Record など)が登録したクリーンアップ処理が確実に実行される。

イメージとしては:

ruby
def shutdown
  # 既存: workers を止める処理など
  # ...
  run_cleanup_hooks  # ← 新規追加
end

2. Active Record がクリーンアップフックを登録し、discard! でプールを破棄

対象ファイル:

  • activerecord/lib/active_record/test_databases.rb
  • activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb

主な変更点:

  • Active Record が テスト用セットアップのどこかで「クリーンアップフック」を登録し、その中で 全ての接続プールに対して discard! を呼ぶようにした。
    (従来の clear_all_connections! / disconnect! ではなく、プール自体を破棄する方向に寄せている)

  • ConnectionPool#discard! 内で、後述の Reaper 側の処理が呼ばれるようになっている。

discard! の役割は、単にコネクションを切るだけでなく「このプールはもう使わない」と宣言し、関連するバックグラウンド処理(ここでは Reaper)も止める、という意味合いになります。

3. ConnectionPool#discard! が Reaper 登録からも確実に外す

対象ファイル:

  • activerecord/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb

メインの追加ロジック:

  • Reaper.discard_pool(pool) を実装し、ConnectionPool#discard! からこれを呼ぶようにした。
  • Reaper.discard_pool の中で:
    • 対象のプールを Reaper の管理対象リストから削除。
    • その「reaping_frequency(監視間隔)」に紐づいたプールが 1 つも残っていない場合は:
      • 対応する Reaper スレッドを 即座に kill & join する(スレッドを停止し、リソースとして確実に回収)。

結果として、「もう使わない接続プールを破棄するときに、そのプールを監視していた Reaper スレッドも同時に正しく終了」するようになります。


4. テストの追加

対象ファイル:

  • activerecord/test/cases/reaper_test.rb
  • activesupport/test/parallelization_test.rb

主な内容:

  • Active Record 側:

    • 特定の reaping_frequency を持つ複数のプールを登録し、順に discard! していったときに:
      • 対応する Reaper スレッドがプール 0 件になったタイミングで停止・解放されることを確認するテスト。
  • Active Support 側:

    • Parallelization#shutdown を呼び出した後に、クリーンアップフック経由で Active Record の Reaper スレッドが生き残っていないことを確認するテスト。
    • PR 説明にある再現コードに近い形で、「shutdown 後に Thread.listActive Record Pool Reaper がいないこと」をアサートしている。

  1. 影響範囲・注意点
  • 対象シナリオ

    • ActiveSupport::Testing::Parallelization を用いた 並列テスト実行環境(Minitest ベースの Rails テスト)が主な対象です。
    • CI 上で 1 プロセスの中で複数のテストスイートを順次実行するような構成だと、この修正によりスレッド・接続リークの蓄積が止まります。
  • 影響範囲

    • ActiveRecord の接続プール管理 (ConnectionPool#discard!, Reaper) に手が入っているため、テスト以外でも明示的に discard! を呼んでいるコードがあれば、そのタイミングで Reaper スレッドも終了するようになります。
    • ただし、通常のアプリケーション運用(establish_connection して自動的に Reaper を使うだけ)では動作は従来と互換性があり、破棄しない限り Reaper は動き続けます。
  • 破壊的変更の可能性

    • disconnect! ではなく discard! でプールを破棄する流れになったことで、「テスト終了後にプールが完全に捨てられる」ようになりました。そのため:
      • テストプロセスを生かしっぱなしで、同じ AR::Base の接続プールをそのまま再利用するようなカスタムコードがある場合は、後続処理で「既存プールがなくなっている」ことを前提に見直しが必要になる可能性があります(一般的な Rails テスト運用では問題にならない想定)。
  • パフォーマンス / リソース

    • リークしていた Reaper スレッドと DB コネクションが確実に解放されるため、長時間動作する CI プロセスのメモリ消費や DB 接続数の安定性向上が期待できます。

  1. 参考情報 (あれば)
  • 対応 Issue: Fixes #57402

  • PR に記載された再現スクリプト(要点):

    ruby
    ActiveRecord::Base.establish_connection(
      adapter: "sqlite3",
      database: ":memory:",
      reaping_frequency: 5
    )
    ActiveRecord::Base.connection_pool.with_connection { |c| c.execute("SELECT 1") }
    sleep 0.1
    
    class ARPoolReaperLeakTest < Minitest::Test
      def test_ar_pool_reaper_is_cleaned_up_after_parallelization_shutdown
        assert Thread.list.find { |t| t.name == "Active Record Pool Reaper" }
    
        parallelization = ActiveSupport::Testing::Parallelization.new(1)
        parallelization.start
        parallelization.shutdown
    
        assert_empty Thread.list.select { |t| t.alive? && t.name == "Active Record Pool Reaper" }
      end
    end
  • 変更ファイル概要:

    • ActiveRecord:
      • connection_pool.rb / reaper.rb: 接続プール破棄と Reaper スレッド停止の連携強化
      • test_databases.rb: テスト用クリーンアップフック登録
      • reaper_test.rb: Reaper の挙動テスト追加
    • ActiveSupport:
      • parallelization.rb: shutdown 時に run_cleanup_hooks を呼ぶ
      • parallelization_test.rb: フック実行を検証するテスト追加

#57433 Disconnect test-local PostgreSQL adapters to prevent connection leak

マージ日: 2026/5/22 | 作成者: @yahonda

  1. 概要 (1-2文で)
    PostgreSQLAdapterTest 内で一時的に生成していた PostgreSQL アダプタ接続を明示的に切断することで、テスト実行時の PostgreSQL 接続リークを防ぐ修正です。PostgreSQLAdapter.new を直接呼び出していた箇所を、必ず disconnect! されるヘルパーメソッド経由に統一しています。

  1. 変更内容の詳細

背景・問題点

  • activerecord postgresql のナイトリー job が FATAL: sorry, too many clients already で落ちることがあり、その一因としてテストコード側の接続リークが特定されました。

  • PostgreSQLAdapterTest では、多くのテストが以下のように「使い捨てのアダプタインスタンス」を直接生成していました:

    ruby
    def test_something
      connection = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.new( ... )
      # connection を使って何らかのテスト
      # ここでメソッド終了 ⇒ 参照がなくなるが、明示的なdisconnectはしない
    end
  • この場合、ソケットのクローズは pg gem の Cレベルの finalizer に任されており、Ruby の GC タイミング次第となるため、max_connections=100 環境では GC されるまで接続が溜まり続けます。

新しいヘルパーメソッド: with_postgresql_adapter

  • 上記問題を解決するため、テストクラスにプライベートヘルパー with_postgresql_adapter(config_hash) { |connection| ... } が追加されました。

  • 役割:

    • 渡された設定ハッシュから PostgreSQLAdapter を生成
    • ブロックに接続オブジェクトを渡して処理を実行
    • ensure で必ず disconnect! を呼び出し、テスト終了時に確実に接続を閉じる
  • イメージとしては次のような構造です(実際のコードイメージ):

    ruby
    private
      def with_postgresql_adapter(config)
        connection = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.new(**config)
        yield connection
      ensure
        connection&.disconnect!
      end
  • これに伴い、今まで PostgreSQLAdapter.new を直接使っていたテストは、原則として以下のように書き換えられています:

    ruby
    # 修正前
    def test_xxx
      conn = ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.new(config)
      assert_equal "UTF8", conn.encoding
    end
    
    # 修正後
    def test_xxx
      with_postgresql_adapter(config) do |conn|
        assert_equal "UTF8", conn.encoding
      end
    end

connection_without_insert_returning のリファクタ

  • 既存のヘルパー connection_without_insert_returning も、内部で PostgreSQLAdapter.new を使っていましたが、ここも接続リークの温床になりうるため、ブロックを取る形に変更されています。

  • 新構造では、このメソッドも内部的に with_postgresql_adapter を利用し、必ず disconnect! されるようになりました:

    ruby
    # イメージ
    def connection_without_insert_returning
      config = @connection_db_config.configuration_hash.merge(insert_returning: false)
    
      with_postgresql_adapter(config) do |conn|
        yield conn
      end
    end
  • これにより、

    • 呼び出し側は connection_without_insert_returning にブロックを渡す
    • そのブロック内だけ一時的な接続を利用し、処理後は自動的に切断 という形に整理されています。

計測結果

  • テスト: postgresql_adapter_test.rb のみ、postgres:alpine (PostgreSQL 18), max_connections=100, --seed=37604(ベースラインで最悪ケース)、各5回計測。

  • ピーク同時接続数(最大値)の推移:

    状態peak (5 runs)
    BEFORE13, 15, 17, 16, 13
    AFTER4, 5, 5, 5, 5
  • ベースライン(テスト実行に最低限必要なセッション数)より 2–3 多い分は、プール管理されている @connection を経由した verify! / reconnect! 由来であり、本PRのスコープ外と明示されています。


  1. 影響範囲・注意点
  • 対象は テストコードのみactiverecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb)であり、本番コード・API の挙動には影響しません。
  • 今後、PostgreSQLAdapterTest 内で一時的なアダプタ接続を新たに追加する場合は、
    • PostgreSQLAdapter.new を直接呼ぶのではなく、
      with_postgresql_adapter または connection_without_insert_returning(ブロック形式)を使うべき、というコーディング規約上の示唆になります。
  • pg gem など GC に依存したクローズに任せない、というテスト設計上のベストプラクティスが反映されており、同様のパターンが他のアダプタテストにも存在する場合、同じアプローチでの修正が必要になる可能性があります。
  • 接続プール管理の @connection を通じた verify! / reconnect! 周りの残りのリーク要因は未対応のため、ナイトリーの too many clients が完全には解消しない可能性はありますが、このPRにより一時接続由来のリークは大きく抑制されています。

  1. 参考情報 (あれば)

#57421 Document endless range inconsistency for where [ci-skip]

マージ日: 2026/5/22 | 作成者: @p8

  1. 概要 (1-2文で)
    where で「終端のない Range(endless range)」を使ったとき、start..(閉区間)と start...(開区間)がどちらも同じ SQL(>= start)になるという挙動がガイドに明記されました。
    挙動自体の変更ではなく、「一見すると直感に反するが、そういう仕様である」ことを公式ドキュメントに追記した PR です。

  1. 変更内容の詳細 (サンプルコード例あり)

対象ファイル:

  • guides/source/active_record_querying.md に説明文を 5 行追加

内容としては、Active Record のクエリインターフェイスで、次のようなコードを書いたとき:

ruby
# endless range(終端なし Range)を使った where
Model.where(created_at: start_time..)   # 例: 2024-01-01 00:00:00..
Model.where(created_at: start_time...)  # 例: 2024-01-01 00:00:00...

Ruby 的には .. は「終端を含む」、... は「終端を含まない」Range ですが、
「終端がない」Range(endless range)の場合、どちらも同じ意味になり、生成される SQL は同じになる ことが明文化されました。

生成される SQL イメージ:

sql
SELECT "models".*
FROM   "models"
WHERE  "models"."created_at" >= '2024-01-01 00:00:00'
  • Model.where(created_at: start_time..)
  • Model.where(created_at: start_time...)

のどちらも、>= start_time という条件になります(> にはならない)。

PR の説明にもある通り、これは以前から存在する挙動で、以下のような GitHub Issue で何度か議論されていましたが、
これまで正式なドキュメントに記載されていなかったため、今回ガイドに追記された、という位置付けです。


  1. 影響範囲・注意点
  • 挙動変更はなし(ドキュメントのみの更新)
    • アプリケーションコードやクエリの生成結果が変わることはありません。
  • endless range では ..... の違いは無視される
    • start..end / start...end(終端あり Range)は従来通り明確に挙動が異なりますが、
      start.. / start...(endless range)ではどちらも >= start となります。
    • そのため、「厳密に > にしたいから ... を使う」といった意図は、endless range では反映されません。
  • コードレビュー・スタイル上の注意
    • チームメンバーが where(start...) を「> 条件になる」と誤解して書いている可能性があるので、
      コードレビューで意図を確認しておくと安全です。
    • >= 条件」であることを明確にしたいなら、素直に where("created_at >= ?", start_time) のように書く、
      またはコメントで意図を補足するのも一案です。

  1. 参考情報 (あれば)

#57209 Fix label for not matching input id when collection value is nil

マージ日: 2026/5/21 | 作成者: @55728

  1. 概要 (1–2文で)
    collection_radio_buttons / collection_check_boxes のコレクション内に nil 値がある場合に、labelfor 属性と対応する inputid が食い違ってしまう不具合を修正する PR です。nil のときに for 側だけ末尾に不要なアンダースコア(_)が付いていた問題を解消し、正しくクリックで選択できるようにしています。

  1. 変更内容の詳細

問題の具体例

以下のようなコード:

ruby
collection_radio_buttons(:user, :active, [["Yes", true], ["Undefined", nil]], :last, :first)

現状(修正前)は次のような HTML が生成されていました:

html
<!-- ✅ true のケース: for と id が一致 -->
<input id="user_active_true" type="radio" value="true" />
<label for="user_active_true">Yes</label>

<!-- ❌ nil のケース: 末尾のアンダースコアで不一致 -->
<input id="user_active" type="radio" value="" />
<label for="user_active_">Undefined</label>

labelfor="user_active_"inputid="user_active" が一致しないため、「Undefined」のラベルをクリックしてもラジオボタンが選択されませんでした。

不具合の原因

inputidlabelfor は別々のメソッドで計算されており、nil の扱いが揃っていませんでした。

1. Tags::Base#add_default_name_and_field_for_valueid を生成)

ruby
def add_default_name_and_field_for_value(tag_value, options, field = "id")
  if tag_value.nil?
    add_default_name_and_field(options, field)  # suffix を付けない → id="user_active"
  else
    # ...
    options[field] += "_#{sanitized_value(tag_value)}"
  end
end

tag_valuenil の場合は「値によるサフィックス(_true など)」を付けず、単に user_active のような ID になります。

2. CollectionHelpers#sanitize_attribute_namelabel for を生成)

ruby
def sanitize_attribute_name(value)
  "#{sanitized_method_name}_#{sanitized_value(value)}"
  # nil → sanitized_value(nil) = "" → "active_" → for="user_active_"
end

こちらは valuenil でも必ず "#{メソッド名}_#{値}" という形で結合するため、サニタイズ後の値が空文字でも _ が残ってしまい、"active_"for="user_active_" という不整合が発生していました。

修正内容

sanitize_attribute_name を修正し、「サニタイズ後の値が空文字のときはアンダースコアを付けない」ように変更しています。

ruby
def sanitize_attribute_name(value)
  sanitized = sanitized_value(value)

  if sanitized.empty?
    sanitized_method_name.dup
  else
    "#{sanitized_method_name}_#{sanitized}"
  end
end

これにより、valuenilsanitized_value(value)"" の場合は:

  • これまで: "active_"for="user_active_"
  • 修正後: "active"for="user_active"

となり、input 側の id="user_active" と一致するようになります。

テスト

actionview/test/template/form_collections_helper_test.rb に、以下をカバーする回帰テストが追加されています。

  • collection_radio_buttons でコレクションに nil を含むケース
  • collection_check_boxes で同様のケース

どちらも、生成される labelfor と対応する inputid が一致することを検証しています。


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

    • ActionViewform_collections_helper を通じて呼ばれる collection_radio_buttons / collection_check_boxes において、値が nil(またはサニタイズ結果が空文字になる値)のときの ID / for の生成ロジックが変わります。
    • labelfor の値が、これまで "user_active_" のような形だったケースは "user_active" に変更されます。
  • 期待されるポジティブな影響:

    • label をクリックしても対応する input が選択されない / フォーカスされないといったアクセシビリティ上の不具合が解消されます。
    • HTML 的にも forid の整合性が取れます。
  • 注意点:

    • もし既存コードや CSS / JavaScript が、forid の末尾のアンダースコア(_)を前提に依存している場合、今回の変更でセレクタなどがマッチしなくなる可能性があります。
      • 例: #user_active_label[for="user_active_"] といったセレクタを使っている場合。
    • ただし、nil 値を持つラジオボタン/チェックボックスに対して、こうした依存をしているケースは比較的レアと思われますし、本来不正な紐づけだったため、修正後の挙動が妥当と考えられます。

  1. 参考情報 (あれば)

#57400 Reset lock_version after a nested savepoint rollback

マージ日: 2026/5/21 | 作成者: @55728

  1. 概要 (1-2文で)
    requires_new: true なネストしたトランザクション(セーブポイント)がロールバックされたときに、メモリ上の lock_version をDBの状態に戻すようにして、次回の save / update!ActiveRecord::StaleObjectError が発生する不具合を修正するPRです。前回の #57363 で対応できていなかった「ネストしたセーブポイントのロールバック」ケースを補完します。

  1. 変更内容の詳細

問題の背景

Railsの楽観的ロック(lock_version)は更新時に WHERE lock_version = ? を付与することで、他トランザクションによる更新競合を検出します。

しかし、次のようなケースで不整合が発生していました。

ruby
widget = Widget.create!(name: "a")              # lock_version: 0

Widget.transaction do
  Widget.transaction(requires_new: true) do
    widget.update!(name: "b")                   # lock_version: 0 -> 1
    raise ActiveRecord::Rollback                # セーブポイントのロールバック: DB上は lock_version -> 0 に戻る
  end

  widget.lock_version                           # => 1 (メモリ上は 1 のまま / DBは 0)
  widget.update!(name: "c")                     # ActiveRecord::StaleObjectError 発生
end

セーブポイント内の更新がロールバックされてDB上の lock_version は元に戻る一方で、Rubyオブジェクト側はインクリメント済み (1) のまま保持しているため、次の update! 時の WHERE 句は lock_version = 1 となり、行がヒットせず StaleObjectError になります。

外側のトランザクションがコミットされても同じ問題が出ます:

ruby
Widget.transaction do
  Widget.transaction(requires_new: true) do
    widget.update!(name: "b")
    raise ActiveRecord::Rollback
  end
end

widget.update!(name: "c")                       # 同様に StaleObjectError

前PR (#57363) では「最も外側のトランザクションがロールバックされたとき」に lock_version をリセットする対応のみで、ネストされた requires_new: true セーブポイントのロールバックには対応できていませんでした。


原因となっていた実装の挙動

トランザクションロールバック時には restore_transaction_record_state が呼ばれますが、以下の条件でショートサーキットしていました:

  • force_restore_statefalse
  • restore_state[:level] > 1 (= ネストしたセーブポイントレベル)
  • このパスは SavepointTransaction#full_rollback? = false のときに通る

その結果、「ネストしたセーブポイントのロールバック時」にはレコードの属性が何も復元されず、lock_version もインクリメントされたまま残ってしまっていました。

メソッド上のコメントは本来やりたいことを正しく表していました:

ruby
# Call the #after_rollback callbacks. The +force_restore_state+ argument indicates if the record
# state should be rolled back to the beginning or just to the last savepoint.

つまり「トランザクション開始時点に戻す(full rollback)」と「最後のセーブポイント時点に戻す(savepoint rollback)」の両方をサポートするつもりだったものの、ロック用カラムについては後者が実装されていなかった、という状態です。


このPRの実装内容

activerecord/lib/active_record/transactions.rbrestore_transaction_record_state まわりに以下のロジックを追加しています(概念的な説明):

  1. 既存: force_restore_statetrue(完全ロールバック時)は、スナップショットを元に全体の状態を復元するブランチがある。
  2. 追加: force_restore_statefalse だが locking_enabled? なモデルについては、
    • トランザクションスナップショットに保存している original_value を使い、
    • write_from_database によりロックカラム(lock_version 等)だけをDB由来の値に戻す
    • この復元処理は #57363 で最外レベル用に導入されたものと同じアプローチ

これにより、セーブポイントロールバック時に「そのセーブポイント開始時点の lock_version」相当の値にメモリ上のオブジェクトも戻すことができます。結果として、次回の save / update!

  • WHERE 句の lock_version がDBと一致する
  • Dirty tracking(変更検知)の状態も整合した形になる

ため、正しく更新が通り StaleObjectError が起きなくなります。


追加されたテスト

activerecord/test/cases/locking_test.rbOptimisticLockingRollbackTest で次の2テストが追加されています(このテストクラスは self.use_transactional_tests = false):

  1. test_lock_version_restored_after_savepoint_rollback

    • パターン: 「外側Tx内でネストした requires_new: true のセーブポイントがロールバックし、その同じ外側Txの中で再度 save
    • 期待: 2回目の save が成功する(StaleObjectError にならない)
  2. test_lock_version_restored_after_savepoint_rollback_when_outer_commits

    • パターン: 「内側セーブポイントロールバック → 外側Txはコミット → ブロックの外で再度 save
    • 期待: ブロックの外での save が成功する

どちらも main では ActiveRecord::StaleObjectError で落ち、パッチ適用後にグリーンになることが確認されています。


  1. 影響範囲・注意点

影響範囲

  • 対象となるのは「楽観的ロック(locking_enabled? = true)を有効にしているモデル」で、
  • 「トランザクション内で requires_new: true なネストしたトランザクション(セーブポイント)を使い、その中でロック対象レコードをUPDATEし、そのセーブポイントをロールバックする」ケースです。
  • 上記条件を満たすアプリケーションでは、
    • これまで ActiveRecord::StaleObjectError が発生していたシナリオが正しく成功するようになります。
    • 既存の挙動を前提に(例: ロールバック後にStaleObjectErrorをトリガーとして何かしていた等)奇妙なワークアラウンドを書いていた場合、それらの挙動が変わる可能性はあります。

通常は「バグ修正」として期待通りの方向の変更であり、互換性上のリスクは小さいと考えられます。

なお、未対応のコーナーケース

PR説明にもある通り、次のような「さらに一段複雑なケース」はまだ未解決です:

ruby
Widget.transaction do
  widget.update!(name: "outer")   # ここで lock_version が 0 -> 1 に

  Widget.transaction(requires_new: true) do
    widget.update!(name: "inner") # ここで lock_version が 1 -> 2 に
    raise ActiveRecord::Rollback  # セーブポイントロールバック: DB上は lock_version 1 に戻るはず
  end

  # スナップショットに残っている original_value が
  # 「outer の前」の値 (0) だったりすると、
  # どの値に戻すべきか分からない問題が残る
end
  • 現状のスナップショットは「トランザクション開始時点」の値のみ持っており、「各セーブポイント前の値」を持っているわけではありません。
  • そのため、「外側トランザクションの中でも同じレコードを更新している」場合に、
    「セーブポイント直前の正しい lock_version」を復元するための情報が足りません。
  • このPRでは、そのようなケースの症状は変わらず StaleObjectError の発生が続きます(ただし値の不一致内容は変化し得る)。
    → 挙動が悪化するのではなく、「よくあるパターンだけ直し、レアで複雑なパターンは現状維持」という位置づけです。

根本的にこのコーナーケースを解決するには、remember_transaction_record_state でセーブポイントごとにスナップショットを持つような、より大きな設計変更が必要になります。


  1. 参考情報 (あれば)
  • 対応済みの前PR:
    • #57363 — 「最外トランザクションのロールバック時に lock_version を復元する」修正
  • このPRでの検証:
    • SQLiteを使った自己完結型の再現スクリプト /tmp/lock_savepoint_rollback.rbmain では StaleObjectError、本PR後は成功)
    • Active Record test suite(sqlite3)での通過:
      • locking_test.rb
      • transactions_test.rb
      • autosave_association_test.rb
  • 変更ファイル:
    • activerecord/lib/active_record/transactions.rb
    • activerecord/test/cases/locking_test.rb
    • activerecord/CHANGELOG.md(バグ修正として追記)

#57429 Preserve SafeBuffer encoding in ActiveSupport::MessagePack

マージ日: 2026/5/21 | 作成者: @Copilot

  1. 概要 (1-2文で)
    ActiveSupport::MessagePack で ActiveSupport::SafeBuffer をシリアライズ/デシリアライズした際に、元の文字エンコーディング(特に UTF-8)が失われないようにする修正です。これにより、キャッシュなどから復元した SafeBuffer を UTF-8 文字列と安全に連結できるようになります。

  1. 変更内容の詳細

問題の背景

従来の実装では、ActiveSupport::SafeBuffer を MessagePack によって round-trip(dump → load)すると、復元後のオブジェクトのエンコーディングが ASCII-8BIT(バイナリ)になっていました。その結果、例えば以下のようなケースでエラーや不具合が発生し得ます。

ruby
loaded = ActiveSupport::MessagePack.load(
  ActiveSupport::MessagePack.dump(ActiveSupport::SafeBuffer.new("Mäßig"))
)

loaded.encoding
# 旧実装: #&lt;Encoding:ASCII-8BIT> などになり得る

"prefix " + loaded
# Encoding::CompatibilityError などを引き起こす可能性

特に、キャッシュレイヤ(例: Rails.cache)で SafeBuffer を保存し、後から取り出してビューやヘルパーで UTF-8 文字列と連結するようなパターンで問題になっていました。

主な変更点

1. シリアライズ方法の変更

対象ファイル: activesupport/lib/active_support/message_pack/extensions.rb

  • これまでは SafeBuffer を MessagePack の「生の拡張バイト列(raw extension bytes)」として扱っていたため、デコード時にエンコーディング情報が失われやすい状態でした。
  • 本PRでは、SafeBuffer を「内部に通常の MessagePack 文字列ペイロードをネストして持つ」形でエンコードするように変更しています。

イメージとしては:

ruby
# 変更前 (概念的に)
# SafeBuffer -> EXT(binary) -> load で ASCII-8BIT な文字列になりがち

# 変更後 (概念的に)
# SafeBuffer -> "中身の String を MessagePack 文字列としてエンコードしたデータ" を持つ拡張
# load 時にその文字列を decode してから SafeBuffer を再構築

2. デシリアライズ時の SafeBuffer 再構築

  • load 時には、ネストされた MessagePack 文字列を decode し、その結果の Ruby 文字列(この時点で元のエンコーディング情報を保持している)から ActiveSupport::SafeBuffer を再生成します。
  • これにより、UTF-8 のまま round-trip されるようになります。

動作例(PR 説明より):

ruby
loaded = ActiveSupport::MessagePack.load(
  ActiveSupport::MessagePack.dump(ActiveSupport::SafeBuffer.new("Mäßig"))
)

loaded.encoding
# => #&lt;Encoding:UTF-8>

"prefix " + loaded
# => "prefix Mäßig"

ここで確認できる保証:

  • loadedEncoding::UTF_8
  • "prefix " + loaded が例外なく動作
  • loadedActiveSupport::SafeBuffer かつ html_safe? == true のまま

3. 回帰テスト(回帰防止)の追加

対象ファイル: activesupport/test/message_pack/shared_serializer_tests.rb

以下の点をカバーするテストが追加されています。

  • MessagePack シリアライザで SafeBuffer を dump → load した結果:
    • オブジェクトクラスが ActiveSupport::SafeBuffer のまま
    • html_safe?true のまま
    • encodingEncoding::UTF_8 のまま
    • UTF-8 の通常文字列との連結が例外なく可能であること

  1. 影響範囲・注意点

影響範囲

  • 対象: ActiveSupport::MessagePack を使って ActiveSupport::SafeBuffer をシリアライズ/デシリアライズしているコード(特にキャッシュやバックグラウンドジョブ間の引き渡しなど)。
  • 具体的な影響:
    • これまで ASCII-8BIT になってしまっていたケースが UTF-8 のまま復元されます。
    • その結果、ビューやヘルパーで UTF-8 文字列との連結時に発生していた Encoding::CompatibilityError 等が解消されます。
    • HTML セーフ属性 (html_safe?) も保持されるため、既存の XSS 対策の挙動は変わりません。

互換性・注意点

  • API の表面的なインターフェース(ActiveSupport::MessagePack.dump/load のシグネチャ)は変わっていません。
  • 変更点はシリアライズフォーマット(内部表現)であり、MessagePack のバイナリ互換性については:
    • 旧形式でシリアライズ済みの SafeBuffer データを新しい実装で load しようとするケースが理論上あり得ます(長期保存されたキャッシュ等)。
    • PR 説明上、後方互換フォーマットへの配慮については明示されていませんが、通常 Rails の MessagePack 拡張は同じアプリケーションプロセス内で完結する用途が多く、実運用で問題になる可能性は低めです。
    • もし長期保管された MessagePack バイナリを別バージョンの Rails で読むケースがある場合は、切り替え時にキャッシュクリアや再生成を検討した方が安全です。

  1. 参考情報 (あれば)
  • PR タイトル: Preserve SafeBuffer encoding in ActiveSupport::MessagePack
  • PR 番号: #57429
  • 変更ファイル:
    • activesupport/lib/active_support/message_pack/extensions.rb
    • activesupport/test/message_pack/shared_serializer_tests.rb
  • 対象クラス:
    • ActiveSupport::SafeBuffer
    • ActiveSupport::MessagePack

この変更は「SafeBuffer をキャッシュして取り出したらエンコーディングがおかしくなり、ビュー側で文字列連結時に落ちる」といった、よくあるけれど厄介なバグに対するピンポイントな修正と捉えると理解しやすいです。


#57404 Update permissions policy list

マージ日: 2026/5/21 | 作成者: @rubenarakelyan

  1. 概要 (1–2文で)
    Rails の ActionDispatch::PermissionsPolicy が、ブラウザ側で新たに定義・拡充された Permissions Policy(旧 Feature Policy)のディレクティブ一覧に追随して更新されました。これにより、config.permissions_policy などから最新のポリシー項目を公式に扱えるようになります。

  1. 変更内容の詳細

2-1. 何が更新されたか

ActionDispatch::PermissionsPolicy 内で定義されている「許可されるディレクティブ名のリスト」が、ブラウザ仕様の最新状態に合わせて拡張されています。追加された主なディレクティブは以下です(PR 本文の列挙と同じ):

  • attribution-reporting
  • battery
  • bluetooth
  • ch-ua
  • ch-ua-arch
  • ch-ua-bitness
  • ch-ua-full-version
  • ch-ua-full-version-list
  • ch-ua-high-entropy-values
  • ch-ua-mobile
  • ch-ua-model
  • ch-ua-platform
  • ch-ua-platform-version
  • ch-ua-wow64
  • compute-pressure
  • cross-origin-isolated
  • direct-sockets
  • execution-while-not-rendered
  • execution-while-out-of-viewport
  • identity-credentials-get
  • mediasession
  • navigation-override
  • otp-credentials
  • publickey-credentials-get
  • storage-access
  • window-management
  • xr-spatial-tracking

これらが ActionDispatch::PermissionsPolicy の「サポートされるディレクティブ名」として登録されたため、Rails の DSL から安全に扱えるようになっています。

2-2. 実際のコードでの利用例

config/application.rb もしくは各環境ごとの config/environments/*.rb での設定例:

ruby
# config/application.rb
module YourApp
  class Application < Rails::Application
    config.permissions_policy do |policy|
      # Attribution Reporting API を同一オリジンのみ許可
      policy.attribution_reporting :self

      # Bluetooth API を完全に禁止
      policy.bluetooth :none

      # User-Agent Client Hints 関連
      policy.ch_ua :self
      policy.ch_ua_platform :self
      policy.ch_ua_full_version_list :self

      # WebAuthn / Credential Management 関連
      policy.publickey_credentials_get :self
      policy.identity_credentials_get :none
      policy.otp_credentials :self

      # XR / WebXR 関連
      policy.xr_spatial_tracking :self

      # ウィンドウ管理やストレージアクセス
      policy.window_management :self
      policy.storage_access :self
    end
  end
end

ActionDispatch::PermissionsPolicy は、上記のような設定から HTTP レスポンスヘッダ Permissions-Policy を生成します。
例: 上記設定の一部に対応するヘッダ断片は次のようになります。

http
Permissions-Policy:
  attribution-reporting=(self),
  bluetooth=(),
  ch-ua=(self),
  ch-ua-platform=(self),
  publickey-credentials-get=(self),
  ...

※ 実際のヘッダフォーマットは Rails / Rack の実装とブラウザ仕様に応じて変わりますが、概ねこのような形で送出されます。


  1. 影響範囲・注意点
  • 新しいディレクティブを使えるようになった
    これまで Rails 側でサポートされていなかった最新の Permissions Policy ディレクティブを、アプリケーション設定から明示的に制御できます。
    例: policy.bluetooth, policy.compute_pressure, policy.direct_sockets など。

  • 既存アプリへの互換性
    既存のアプリケーションで、これら新ディレクティブを明示的に使っていなければ、挙動は変わりません。
    逆に、「手動でヘッダ文字列を組み立てていた」「ミドルウェアを自作していた」場合は、Rails 標準の config.permissions_policy に移行することでコードの簡素化と将来の追随性向上が見込めます。

  • ブラウザ側のサポート状況に依存
    Rails にディレクティブが追加されたことで、ヘッダ上は設定可能になりますが、
    実際に機能が制御されるかどうかは、各ブラウザが該当ディレクティブをサポートしているかに依存します。
    新しめの API(attribution-reporting, direct-sockets, compute-pressure など)は、ブラウザ実装状況を確認した上で利用してください。

  • ポリシーのデフォルト値に注意
    新しいディレクティブは、設定しなければブラウザのデフォルト挙動に従います。
    たとえばセキュリティ・プライバシー要件が高い環境では、未使用の強力な API(Bluetooth, Direct Sockets, Window Management など)は :none で明示的に閉じる、といったポリシー設計が推奨されます。


  1. 参考情報 (あれば)

この PR によって、Rails アプリから最新のブラウザ機能制御ポリシーを公式に扱えるようになった点が主な価値です。


#57171 Parallelize exist? checks and uploads in MirrorService#mirror

マージ日: 2026/5/21 | 作成者: @Mordorreal

  1. 概要 (1-2文で)
    Active Storage の MirrorService#mirror において、複数ミラーへの exist? チェックとアップロード処理をスレッドプール上で並列化し、特にネットワークレイテンシが支配的な環境で大幅な速度向上を図る変更です。既存の @executor(スレッドプール)を mirror でも活用するようにした改善 PR です。

  1. 変更内容の詳細

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

ActiveStorage::Service::MirrorService#mirror は、複数のミラーサービスに対して:

  1. それぞれに exist?(key) を順番に実行
  2. exist?false のミラーに対して、順番に upload 実行

という「ミラーの数 N に比例して処理時間が伸びる O(N) の逐次処理」になっていました。
一方で、同じクラス内の deletedelete_prefixed は、すでに @executorConcurrent::ThreadPoolExecutor 相当)を使って並列実行されていますが、mirror だけはその恩恵を受けていませんでした。

ネットワーク(S3/GCSなど)のレイテンシが支配的な場面では、「順番に待つ」よりも「まとめて投げて並列に待つ」方が圧倒的に速くなるため、mirror もスレッドプールを使って並列化すべき、というのがモチベーションです。


2-2. 変更の要点

(1) exist? チェックとアップロードの両方を並列化

Concurrent::Promise と既存の @executor を用いて、

  • フェーズ1: 各ミラーに対する exist?(key) チェックを並列で実行
  • フェーズ2: 「存在しなかったミラー」に対する upload を並列で実行

するように変更されました。

概念的な流れは以下のようになります(擬似コードイメージ):

ruby
def mirror(key, io, checksum: nil, **options)
  # io を安全に複数スレッドで扱うため、後述のように一度内容を読みきる

  # 1. 各ミラーに対して exist? を並列チェック
  exist_promises = mirrors.map do |service|
    Concurrent::Promise.execute(executor: @executor) do
      service.exist?(key)
    end
  end

  # 結果を待ち、存在しないミラーだけを抽出
  missing_services = mirrors.zip(exist_promises).reject { |_, p| p.value }.map(&:first)

  # 2. 存在しなかったミラーに対して upload を並列実行
  upload_promises = missing_services.map do |service|
    Concurrent::Promise.execute(executor: @executor) do
      # 各ミラーに専用の IO を渡して upload
      service.upload(key, build_io_for(service), checksum: checksum, **options)
    end
  end

  # すべてのアップロード完了を待つ(エラーは従来通り伝播)
  upload_promises.each(&:value!)
end

実際のコードはもう少し凝っていますが、流れとしてはこういう2フェーズ構造を、どちらも Concurrent::Promise + @executor で並列化した形です。

(2) IO の扱いをスレッドセーフにする工夫

複数スレッドから同じ IO オブジェクトを読み書きすると競合するため、次の工夫が入っています:

  1. ファイル内容を一度だけ読み出し、凍結された文字列に保存

    • 元の io から String を1回だけ read し、その文字列を freeze して共有データにします。
    • これにより、並列処理の中で「共有のミュータブル IO を読む」ことを避けます。
  2. 各ミラー用に専用の StringIO を生成

    • 上記の「凍結された文字列」をもとに、各ミラーごとに StringIO.new(frozen_string) を生成して upload に渡します。
    • こうすることで、各スレッドは自分専用の IO を読むだけなのでスレッドセーフになります。
  3. io.rewind を呼んで EOF 問題を回避

    • mirror が呼ばれたとき、渡された io がすでに EOF(末尾)にある可能性があります。
    • 最初に io.rewind を実行してから読み出すことで、「もともとの処理系が io をどこまで読んだか」に依存せず、mirror の中でファイル全体を正しく読めるようにしています。

このあたりは、Active Storage が Tempfile やストリームなど様々な IO を扱うことを前提に、慎重に対応している部分です。

(3) 既存スレッドプール (@executor) の再利用

MirrorService はコンストラクタで、ミラー数に応じたスレッドプールをすでに持っています:

ruby
@executor = Concurrent::ThreadPoolExecutor.new(
  min_threads: 1,
  max_threads: mirrors.size,
  # ... 略 ...
)

delete / delete_prefixed ではすでにこのプールを使っていましたが、mirror だけは同期処理だったため、ここでも @executor を利用するように統一されました。
これにより、並列度は「ミラーの数」に上限があり、無制限にスレッドが増えることはありません。


2-3. ベンチマーク結果

説明中のベンチマークでは、1ミラーあたり 50ms のクラウドレイテンシ(S3/GCS の P50 想定)を負荷として与えたときの測定結果が示されています:

MirrorsSequentialParallelSpeedup
2100ms50ms~2x
3150ms50ms~3x
5250ms50ms~5x

各ミラーでの処理がほぼ「待ち時間だけ」で占められるため、並列実行すると「最大レイテンシ ≒ 1ミラー分のレイテンシ」で済み、ミラー数に比例したスピードアップが得られている、という結果です。

また、ファイルサイズ(50KB–20MB)は性能にほとんど影響しないことも確認されており、I/O のスループットよりもネットワークのラウンドトリップがボトルネックであることが示唆されています。


  1. 影響範囲・注意点

影響範囲

  • 対象: Active Storage を使っているアプリケーションのうち、ActiveStorage::Service::MirrorService を利用している環境
    • 特に S3 + GCS など複数クラウドストレージにミラーしているケース
  • 変更点:
    • mirror 動作の外から見たインターフェイス (mirror(key, io, ...)) は従来通り
    • 内部実装が並列実行になることで、全体の処理時間が短縮される可能性が高い

注意点

  1. 並列実行に伴う例外伝播

    • Concurrent::Promise を使っているため、並列タスク内で例外が発生した場合の伝播は、実装の value! 呼び出しなどを通して行われます。
    • 従来と同様に、いずれかのミラーで upload が失敗したときは例外が上がる想定ですが、「どのミラーで失敗したか」のデバッグにはスタックトレースやログを確認する必要があります(ここは元々の挙動と本質的に大きくは変わらないはずです)。
  2. IO の扱いが変わることによる副作用

    • mirror 内で io.rewindio.read を行い、その内容から StringIO を生成する実装になっているため、
      • mirror 直後に同じ io を外側で再利用しているようなコードがある場合、rewind の影響を受ける可能性があります。
      • 一般的には Active Storage 内部が所有する IO を渡す形なので問題になりにくいですが、mirror メソッドを直接呼び出して独自に拡張している場合は念のため挙動を確認すると安心です。
  3. スレッドプール負荷

    • スレッドプールの max_threadsmirrors.size に合わせているため、極端にスレッドが増えることはありませんが、ミラー数が多い環境ではスレッド数が相応に増えます。
    • CPU が細い環境でミラー数を増やしすぎると、スレッドスケジューリングのオーバーヘッドが相対的に増える可能性はあります(ただしネットワーク待ち主体なので通常は問題になりにくい)。
  4. テスト追加による挙動保証

    • activestorage/test/service/mirror_service_test.rb にテストが多数追加されており、mirror が並列実行になっても既存仕様が保たれていることを確認しています。
    • 特に「存在チェックの後にアップロードされる」「一度読み込んだ IO 内容を各ミラーで正しくアップロードできる」といった振る舞いが検証されていると考えられます。

  1. 参考情報 (あれば)
  • 関連 PR: #57170
    • MirrorService や Active Storage のミラーリング周りで、今回の並列化と合わせて改善・リファクタリングが進んでいる可能性があります。
  • 変更ファイル:
    • activestorage/CHANGELOG.md
      • この変更が挙動面の改善として CHANGELOG に追記されているため、今後のリリースノートでも確認できます。
    • activestorage/lib/active_storage/service/mirror_service.rb
      • 実際の並列化実装(Concurrent::Promise@executor の使用、IO の扱いの変更)がここに含まれます。
    • activestorage/test/service/mirror_service_test.rb
      • 並列化に伴う挙動検証テストが追加されており、具体的な利用シナリオや期待される振る舞いを確認するのに参考になります。

この変更により、複数ストレージへのミラーリングを使っている環境では、特にレイテンシの高いクラウドストレージを組み合わせている場合に、アップロード系のレスポンス改善が見込めます。


#57423 Fix ShareLock ownership under :fiber isolation

マージ日: 2026/5/21 | 作成者: @joeljunstrom

  1. 概要 (1-2文で)
    ActiveSupport::Concurrency::ShareLock の「ロック所有者」の識別方法を、Thread.current ベースから ActiveSupport::IsolatedExecutionState.context ベースに変更し、config.active_support.isolation_level = :fiber のときでも正しく排他制御できるようにした PR です。これにより、Falcon などの Fiber スケジューラ使用時に、リロード後のランダムな NoMethodError が発生する不具合を修正しています。

  1. 変更内容の詳細

背景と問題点

従来の ShareLock は、以下のように「今ロックを持っている主体」を Thread.current で識別していました:

  • スレッド隔離 (isolation_level = :thread, デフォルト) では、
    • 1 リクエスト = 1 スレッド が一般的
      → リクエストごとに別スレッドなので、Thread.current で「所有者」を区別できる
  • しかし Fiber 隔離 (isolation_level = :fiber) では、
    • 複数リクエスト Fiber が 1 つのスレッド上で実行される
      → どの Fiber も同じ Thread.current になるため、ShareLock から見ると「全部同じ所有者」に見えてしまう

この結果:

  • リロード用 Fiber が :unload (排他) ロックを取得しようとしたとき、
    「同じ所有者が既に共有ロック中」と誤認して、排他ロック取得が通ってしまうケースがある
  • 実際には別のリクエスト Fiber がまだ共有ロックを保持しているのに、
    • リロード Fiber が :unload 排他ロックを取得
    • 定数をクリア
    • 進行中リクエストから見ると、途中で定数が消える
      → ビュー描画などでランダムな NoMethodError が発生
      → 特に「起動後最初のファイル変更時」に顕在化

解決策: 所有者を IsolatedExecutionState に紐づける

ShareLock の「所有者」を ActiveSupport::IsolatedExecutionState.context ベースで判定するように変更しています。

  • ActiveSupport::IsolatedExecutionState は、Rails が
    • CurrentAttributes
    • その他「現在の実行コンテキスト」を意識する仕組み
      で既に使っているコンテキスト管理
  • isolation_level = :thread のとき:
    • コンテキストはスレッドごと
      → これまで通りスレッド単位で所有者が区別される
  • isolation_level = :fiber のとき:
    • コンテキストは Fiber ごと
      → Fiber ごとに別の所有者として扱われる

結果として:

  • 「ロックの所有者」の定義が Rails 全体で一貫 (CurrentAttributes などと同じ概念)
  • Fiber スケジューラ環境でも、
    • 各リクエスト Fiber が別オーナーとして ShareLock を取得
    • ある Fiber が共有ロックを持っている間、別 Fiber が排他ロックを誤って取得することを防止

実装上の主な変更点

  1. ShareLock の内部フィールド名の見直し

    以前は:

    • @exclusive_thread
    • raw_state 内の :thread キー

    という名前で、「スレッド」を前提にした命名でした。

    これを以下のように変更:

    • @exclusive_owner
      • 所有者はコンテキスト (スレッド or Fiber) によって変わるので、中立的な名前に変更
    • raw_state:thread:owner
      • 実際には Fiber を保持し得るので、thread と名乗らないようにした

    :thread isolation 時の挙動自体は変わらず、「名前だけ」より正確になっています。

  2. ActionDispatch::DebugLocks の更新

    ActionDispatch::DebugLocks/rails/locks からロック状態をデバッグ表示するためのミドルウェアで、ShareLock#raw_state の唯一の利用者です。

    ここで以下の対応がなされています:

    • raw_state:thread:owner に追従
    • 所有者が Fiber の場合を考慮し、
      • Fiber#status が存在しないため、素直に呼ぶと例外になる
        → ここを安全に扱うよう修正 (Thread とは別の型の場合でも落ちないように)

    これにより、Fiber 環境でも /rails/locks を安全に表示できます。

  3. テストの追加・強化

    • 既存の ShareLockTest に加え、ShareLockFiberTest を新設
    • Fiber isolation (isolation_level = :fiber) における所有権の挙動をテスト
      • start_exclusive(no_wait: true) と手動 Fiber.resume を利用し、
        スケジューラ不要で自己完結したテストにしている
    • さらに、「isolation_level = :thread の場合は複数 Fiber を同一所有者として扱う」回帰テストも追加
      → デフォルト構成での互換性が保たれていることを確認

    なお、wait_for 内のスケジューラ連携 (実際のブロッキング挙動) まではカバーしていません。
    これは別途 Fiber スケジューラ用のテストハーネスが必要になるため、この PR ではスコープ外としています。


  1. 影響範囲・注意点
  • Rails アプリケーション側の挙動

    • config.active_support.isolation_level = :fiber を有効にし、Falcon などの Fiber 対応サーバを使っている場合:
      • リクエスト中にコードを変更 → リロード → ランダムな NoMethodError が出る
        といった問題が解消される可能性が高いです。
    • デフォルト (:thread) の場合:
      • 所有者判定はスレッド単位のままで、挙動変更は意図的に最小限に抑えられています。
      • 既存アプリへの影響はほぼありません (互換性テストも追加済み)。
  • ミドルウェア / 拡張を書いている場合

    • ActiveSupport::Concurrency::ShareLock#raw_state を直接触っている独自コードがもしあれば:
      • :thread キー → :owner キーに変更された点に注意してください。
      • 値として Thread だけでなく Fiber が入る可能性があります。
        型チェックや status 呼び出しなどをしている場合は要見直しです。
    • 通常のアプリケーションコードでは raw_state に触れることはほぼないため、一般的な利用者への影響は限定的です。
  • デバッグ用途 (/rails/locks)

    • Fiber 環境でもクラッシュせずロック情報を確認できるようになりました。
    • ロックの所有者表示が「スレッド ID 前提」ではなくなるため、
      Fiber 環境での見え方が以前と変わる可能性があります。

  1. 参考情報 (あれば)
  • 対応する設定:
    • config.active_support.isolation_level = :thread (デフォルト)
    • config.active_support.isolation_level = :fiber
  • 関連しそうなコンポーネント:
    • ActiveSupport::IsolatedExecutionState
    • ActiveSupport::CurrentAttributes
    • ActiveSupport::Concurrency::ShareLock
    • ActionDispatch::DebugLocks (/rails/locks)
  • 想定サーバ:
    • Falcon などの Fiber スケジューラ対応 Rack サーバ環境で特に効果がある修正です。

#57425 Release reloader share on rack hijack in ActionDispatch::Executor

マージ日: 2026/5/21 | 作成者: @joeljunstrom

  1. 概要 (1-2文で)
    ActionDispatch::Executor が WebSocket や Rack hijack(rack.hijack_io)による長寿命コネクションでもリクエスト完了処理(executor state の完了・reloader share の解放)を適切なタイミングで行うように変更されました。これにより、開発環境でのリロード待ちのデッドロックや、クリーンアップ処理がソケットクローズまで遅延する問題が解消されます。

  1. 変更内容の詳細

背景 / 問題点

従来の ActionDispatch::Executor の挙動:

  • Executor の「完了」(state.complete!)は以下のタイミングで行われていた:
    • Rack レスポンスボディの close コールバック
    • もしくはサーバが提供する rack.response_finished コールバック
  • WebSocket アップグレード (HTTP 101) や Rack hijack (rack.hijack_io) を使ったストリーミングでは:
    • レスポンスボディの close はソケットクローズ時まで呼ばれない
    • つまり Executor の完了もコネクション終了まで遅延する

これに起因する問題:

  • 開発環境では reloader が before_class_unloadShareLock:unload 用の排他ロックを取りに行くが、
    • あるリクエストが reloader share を保持したまま長時間生き続けると(WebSocket など)
    • :unload ロックが取れず、コードリロードがブロックされる
  • 特に fiber スケジューラなサーバ(Falcon + async-cable など)の場合:
    • リクエスト用 fiber がそのまま hijacked ソケットの read loop を続けるため
    • その fiber 上で reloader share がコネクション存続中ずっと保持される
  • Puma では:
    • Action Cable の hijack がワーカープールスレッドに処理を委譲するため
    • 元のリクエストスレッドは解放され、この問題が表面化しにくい

本番環境では reloader が動いていないためデッドロックは起こらないものの:

  • Executor のライフサイクルが「リクエスト〜レスポンス」ではなく「コネクション存続期間全体」に引き伸ばされており、
    • クエリキャッシュのクリーンアップ
    • CurrentAttributes のリセット
    • アプリ側で登録した to_complete コールバック
      などが「ソケットが閉じた時点」でしか実行されない、という設計上望ましくない状態になっていた。

今回の変更の要点

1) hijack / HTTP 101 の検出と早期 finalize

ActionDispatch::Executor#call 内で、レスポンスが hijack されたかどうかを検知し、レスポンスボディの close を待たずに executor state を完了させるようになりました。

新たに導入されたプライベートメソッド:

ruby
def hijacked?(status, headers, env)
  status == 101 || env["rack.hijack_io"]
end
  • env["rack.hijack_io"] の存在 … 実際に hijack が行われたことを示す
  • HTTP ステータス 101 (Switching Protocols) … WebSocket などのプロトコルアップグレード

注意点として:

  • rack.hijack? は「サーバが hijack をサポートしているか」を示すだけで、「実際に hijack されたか」は分からないため検出条件には使っていません。

Executor#call の中では、以下のような流れになります(疑似コードイメージ):

ruby
def call(env)
  state, to_complete = run_callbacks # ここで executor state を開始

  finalize = -> do
    # idempotent(複数回呼ばれても一度しか効かない)にしている
    state.complete!
    to_complete.each(&:call)
  end

  status, headers, body = @app.call(env)

  if hijacked?(status, headers, env)
    # hijack / 101 の場合はここで即座に executor を完了
    finalize.call
  else
    # rack.response_finished / body.close 経由の完了などに finalize をフック
    attach_finalize_to_response_lifecycle(finalize, status, headers, body, env)
  end

  [status, headers, body]
rescue => e
  finalize.call
  raise
end

※実際の実装は多少異なる可能性がありますが概念的にはこのような構造です。

2) finalize クロージャによる完了処理の一元化

これまで state.complete! が複数箇所から直接呼ばれていましたが、今回:

  • finalize というクロージャを 1 つ作り
    • state.complete!
    • to_complete コールバック群の実行
  • をひとまとめにして、idempotent(何度呼んでも一度しか効かない) にしています。

これにより:

  • hijack 検出時の「早期 finalize」
  • rack.response_finished
  • レスポンスボディの close
  • 例外発生時の ensure 的なパス

など、複数の経路から finalize が呼ばれても二重に complete! されないようになります。

3) Action Cable などの長寿命接続との整合性

  • Action Cable などの長寿命 hijack consumer は、既に before_class_unload フックで「クラスアンロード前に接続を閉じる」ことによってリロード安全性を確保しています。
  • 今回の変更で「hijack タイミングで reloader share を解放」しても、
    • 彼らはアンロード前にクリーンに切断されるため
    • 定数が消える/差し替わることによる不整合は生じない、と判断されています。

4) 関連 PR #57423 との関係

  • #57423 では ActiveSupport::Concurrency::ShareLockisolation_level を正しく考慮するよう修正されています。
  • これがないと、fiber スケジューラなサーバで「全 request fiber を同一オーナーとみなす」バグがあり、今回の PR が解決するはずのデッドロックが別の形で隠れてしまうとのことです。
  • 両者はロジック上は独立ですが、開発環境で fiber ベースサーバを使う場合はどちらも取り込まれていることが望ましいです。

  1. 影響範囲・注意点

影響範囲

  • 対象:
    • ActionDispatch::Executor を利用する Rack アプリ(ほぼ全ての Rails アプリ)
    • WebSocket / Action Cable / 任意の Rack hijack (rack.hijack_io) を用いる機能
  • 振る舞いの変化:
    • これまで「ソケットクローズ時」だった以下の処理が、「HTTP 101 / hijack 応答を返した時点」で実行されるようになります:
      • Active Record query cache のクリーンアップ
      • CurrentAttributes のリセット
      • config.action_dispatch.executor_to_complete などで登録した to_complete コールバック

期待されるメリット

  • 開発環境:
    • 長時間開いた WebSocket / hijacked 接続があっても、リロードのための ShareLock:unload ロック取得がブロックされにくくなる
    • Falcon + async-cable のような fiber スケジューラ環境でのデッドロックが解消される
  • 本番環境:
    • Executor のライフサイクルが「リクエスト単位」に戻り、クエリキャッシュ等のリソース管理が従来想定により近い形になる
    • CurrentAttributes などスレッド/リクエストローカルな状態が、長寿命接続に引きずられずリクエスト完了時にリセットされる

注意点

  • WebSocket / hijack コネクション内で「Executor の存在を前提としたリクエストスコープの状態」に依存していた場合:
    • 例えば、Executor のライフタイムにのみ依存して Current を使い回していたりすると、
    • 今回からは アップグレード後すぐに Executor が完了するため、その前提は崩れます。
    • 通常の Action Cable の利用パターンでは問題ない設計ですが、自前で複雑な hijack をしている場合は確認が必要です。
  • fiber ベースのサーバで ShareLock を正しく使いたい場合:
    • #57423 の ShareLock 修正も合わせて取り込まれていることを前提に挙動検証を行うとよいです。

  1. 参考情報 (あれば)

#57397 [ci skip] Clarify Active Job queue backends

マージ日: 2026/5/21 | 作成者: @sandstrom

  1. 概要 (1-2文で)
    Active Job で利用できるキューイングバックエンドに関するガイド文書を整理し、「どのバックエンドを・なぜ挙げているのか」をより明確にしたドキュメント変更です。組み込みアダプタの今後の扱い(外部抽出の方針)とも整合するように説明とリンク構成が調整されています。

  1. 変更内容の詳細

※このPRは guides/source/active_job_basics.md のみを編集するドキュメント変更です。コードや挙動の変更はありません。

主なポイントは以下です。

  • Active Job の「キュー backend 一覧」の説明を明確化

    • 旧来の文章だと「なぜこのバックエンドが載っていて、他は載っていないのか」が分かりづらかったため、
      • 「このリストはあくまで代表的な実装例である」
      • 「Rails本体に同梱される・されない、とは必ずしもイコールではない」
        といったニュアンスが分かるように文言が整理されたと思われます。
    • 「将来的にビルトインアダプタは別gemに切り出す計画」であることがガイドに書かれているため、その方針と矛盾しないような書き方に調整されています。
  • バックエンド一覧の並び順を GitHub スター数ベースに変更

    • Sidekiq, Resque, Delayed Job, Que, GoodJob など、代表的なバックエンドの列挙順が「GitHub スター数を元にした人気順」に再ソートされています。
    • Rails 公式として「特定ベンダー推し」に見えないよう、かつ利用者が直感的に「よく使われている順」に見えるような並びにした意図が説明されています。
  • ドキュメント中のリンクを更新

    • 各バックエンドの公式ドキュメントや GitHub リポジトリへのリンクが最新のものに更新されています。
    • 古くなっていたURLやリダイレクトされるリンクなどが、より適切なものに差し替えられたと考えられます。
  • 位置づけや選定基準に関する説明のトーン調整

    • 「Rails が公式にサポートするのはこのバックエンドだけ」と誤解されないような書き方に寄せられている可能性が高いです。
    • 「他にも多くのバックエンドが存在する」「このリストはあくまで代表的なもの」という趣旨が明示的になっているはずです。

(このPRにはコード例の追加・変更はほぼ含まれていないと思われるため、サンプルコードは割愛します。)


  1. 影響範囲・注意点
  • ランタイム挙動への影響はなし

    • 実装コードには一切手が入っていないため、Active Job の挙動・API・対応アダプタに即時の変更はありません。
    • テストやCHANGELOGの更新も不要と見なされている純粋なドキュメント修正です。
  • 今後の「ビルトインアダプタ外出し」への布石

    • ガイド文言が「ビルトインアダプタは別gemとして抽出する計画」と整合するよう調整されているため、将来的に:
      • activejob が最小限のインタフェースのみを持ち
      • Sidekiq などのアダプタは外部gemとして提供
        という構成になっても、ガイドの意味が破綻しないようになっています。
    • すでにActive Jobを使っているプロジェクトは、このPR自体で直ちに何かを変更する必要はありませんが、
      「アダプタは今後も独立したgemとして管理されていく」ことを意識しておくとよいです。
  • バックエンド選定時の参考情報としての読みやすさ向上

    • 「どれがよく使われているのか」「どのリンクを見れば良いか」が整理されたので、
      新しくバックエンドを選ぶ人や、既存バックエンドからの移行を検討している人にとってはガイドが使いやすくなっています。
    • ただし、並び順がスター数ベースになったことで「Rails公式の推奨度」とは無関係である点には注意が必要です。
      あくまで「人気の目安」であり、自身のユースケース(信頼性、性能、ライセンス、キューの特性など)に合わせて選ぶ必要があります。

  1. 参考情報 (あれば)
  • 該当ファイル:
    guides/source/active_job_basics.md
    → Active Job の概要・使い方・バックエンド一覧が記載される公式ガイド章

  • 関連する将来方針(ガイド上で触れられているもの)

    • Active Job の「ビルトイン」アダプタを別gemとして切り出す計画
    • そのため、Active Job は「統一インタフェース」としての役割がより明確になり、
      各バックエンドとは疎結合な関係になる方向性が示唆されています。

このPRは「実装ではなくガイドを読んだときの理解を改善する」ことにフォーカスした変更なので、
開発者としては「Active Jobバックエンド選定や構成を見直す際に、最新ガイドを読み直す」といった対応があれば十分です。


#57170 Fix MirrorService#mirror IntegrityError with nil checksum

マージ日: 2026/5/21 | 作成者: @Mordorreal

  1. 概要 (1-2文で)
    Active Storage の MirrorService#mirror が、チェックサムが nil の場合でも整合性チェックを実行してしまい ActiveStorage::IntegrityError を投げていた不具合を修正した PRです。チェックサムが存在する場合にのみ整合性検証を行うようにし、ドキュメントの仕様と実装を一致させています。

  1. 変更内容の詳細

問題の背景

  • ActiveStorage::Service::MirrorService は、複数のストレージサービスへ同時に書き込むためのサービスです。
  • MirrorJob から mirror(key, checksum: ...) が呼ばれる際、track_variants: false な場合などに checksum: nil で呼ばれるケースがあります。
  • これまでの実装では、primary.open(key, checksum: checksum) の中で verify_integrity_of が呼ばれ、実ファイルの MD5 と引数の checksum を比較していました。
  • checksumnil の場合でも比較が行われるため、常に不一致となり ActiveStorage::IntegrityError が発生していました(#57164)。

修正内容

MirrorService#mirror 内の primary.open 呼び出しに verify オプションを追加し、チェックサムが存在する場合のみ検証するように変更しています。

変更前(イメージ):

ruby
primary.open(key, checksum: checksum) do |io|
  # ...
end

変更後:

ruby
primary.open(key, checksum: checksum, verify: checksum.present?) do |io|
  # ...
end

ポイント:

  • checksum.present?true のときだけ verify が有効になり、MD5 の整合性チェックが走ります。
  • checksumnil または空の場合は verify: false となり、整合性チェック自体をスキップします。
  • これは「チェックサムが存在する場合のみ整合性チェックを行う」という Active Storage のドキュメントに記載された仕様と一致します。

テストと CHANGELOG

  • activestorage/test/service/mirror_service_test.rb に、checksum: nilmirror しても IntegrityError が発生しないことを確認するテストが追加されています(+15行)。
  • activestorage/CHANGELOG.md に今回のバグ修正についての項目が追加されています(+5行)。

  1. 影響範囲・注意点
  • 対象: Active Storage を利用しており、MirrorService を使っている環境、特に track_variants: false 等の設定でチェックサムが保存されない・渡されないケース。
  • 挙動の変化:
    • 以前: checksum: nil でも実データとの MD5 比較を行おうとして、必ず ActiveStorage::IntegrityError が発生。
    • 以後: checksum: nil の場合は整合性チェックをスキップし、通常通りミラーリングを続行。
  • 互換性:
    • 「チェックサムがない場合は整合性チェックしない」というもともとの設計・ドキュメントに沿った修正のため、意図せぬ互換性破壊は基本的にありません。
    • ただし、「チェックサムを渡さないことであえて IntegrityError を発生させる」といった特殊な前提で依存しているコードがあれば、その前提は崩れます(通常はない想定)。
  • セキュリティ・信頼性:
    • そもそもチェックサムが保存・渡されていないケースでは、これまでも「正しい検証」はできていなかった(常に失敗していた)ため、今回の修正は「ドキュメントに沿ってチェック自体を行わない」方向です。
    • データの整合性を確実に検証したい場合は、アップロード時にチェックサムを正しく保存し、MirrorService にも渡るような設定を維持する必要があります。

  1. 参考情報 (あれば)
  • 対応 issue: #57164
  • 関連 PR: #57171
  • 修正対象クラス: ActiveStorage::Service::MirrorService
  • 仕様上の前提: 「チェックサムが存在する場合にのみ整合性チェックを行う」(Active Storage ドキュメント記載の挙動)

#57223 Fix duplicate where conditions in create_or_find_by

マージ日: 2026/5/21 | 作成者: @y-dashev

  1. 概要 (1–2文で)
    create_or_find_by がユニーク制約違反 (RecordNotUnique) 後のリトライ時に重複した where 条件を生成してしまい、結果として RecordNotFound が起き得るバグを修正した PR です。where(...).find_by!(...)rewhere(...).take! に置き換えることで、競合時の再検索ロジックを正しく動作させています。

  1. 変更内容の詳細

バグの背景

create_or_find_by は、概ね次のような流れで動きます:

  1. 渡された attributes で INSERT を試みる
  2. DB のユニーク制約に引っかかって RecordNotUnique が起きたら
  3. 既に存在しているレコードを SELECT で取り直す

元の実装では、そのリトライ用の SELECT が以下のような形でした(イメージ):

ruby
relation.where(attributes).find_by!(attributes)

relation 側にすでに何らかの where が付いていたり、create_or_find_by 自身の前半の処理で scope が汚れていた場合に、where(attributes)find_by!(attributes) の両方で同じ条件が積み上がり、結果として「条件が二重についた不正な Relation」が生成されることがありました。

その結果、本当は存在するはずのレコードを見つけられずに RecordNotFound が発生する、というバグになっていました。

修正内容

1. whererewhere への変更

where(attributes)rewhere(attributes) に変更:

ruby
# 変更前(概念的)
relation.where(attributes).find_by!(attributes)

# 変更後(概念的)
relation.rewhere(attributes).take!

rewhere は、同じキーを持つ既存の where 条件を置き換えるメソッドです。
これにより、リトライ時には「その attributes に関する条件はすべて上書きされる」ため、以前に積み上がってしまった衝突する条件(重複 where)をクリーンにできます。

結果:

  • 同じカラムに対して複数の矛盾する where が積み上がることを防止
  • 競合後の再検索クエリが、意図した attributes に対してシンプルな条件になる

2. find_by!take! への変更

find_by!take! に変更しています。

  • find_by!(attributes) は与えた attributes を再度条件に含めてクエリを組み直します。
  • すでに rewhere(attributes) 済みの Relation からレコードを1件取るだけで良いので、単に take! で十分です。

つまり:

  • 条件の指定は rewhere(attributes) に一本化
  • レコード取得は take! で「その Relation から1件必ず取る」ことに限定

これにより「条件指定の責務」と「レコード取得の責務」が分離され、スコープの衝突が起きにくくなっています。

3. テスト修正

test_find_or_create_by_race_condition のスタブを :find_by! から :take! に変更:

  • 実装が find_by! を呼ばなくなったため、テストのモック・スタブも合わせて変更
  • レースコンディション(ユニーク制約競合)の再現テストが、新実装に対しても妥当な形で動くように調整

4. CHANGELOG 更新

activerecord/CHANGELOG.md に、このバグ修正についての記述が追加されています。
今後のリリースノートに、この挙動変更(バグ修正)が載る想定です。


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

    • ActiveRecordcreate_or_find_by を使っていて
    • かつ DB のユニーク制約競合が実際に発生し得るケース
    • かつ Relation にすでにスコープや where が積まれているケース(例: User.active.create_or_find_by(email: ...) のようなもの)
  • ポジティブな影響:

    • 競合発生時に本来は存在しているレコードを見つけられず、RecordNotFound が不定期に飛ぶ、という不安定な挙動が解消されます。
    • スコープが複雑な Relation 上でも、create_or_find_by のリトライ動作がより堅牢になります。
  • 挙動の変化の有無:

    • 意図としては「バグ修正」であり API の表面上の仕様変更ではありません。
    • ただし、これまでバグに依存した挙動(たとえば「競合時にたまたま RecordNotFound が出て rescue していた」など)に依存している場合は挙動が変わる可能性があります。
    • 正常系・期待される使い方においては、より直感的な動作になります。
  • パフォーマンス:

    • クエリの本質的な回数は変わらず、形が変わるだけなので目立った性能劣化・向上はありません。
    • 重複条件を減らすという意味では、極端なケースでクエリプランがむしろシンプルになる可能性はあります。

  1. 参考情報 (あれば)
  • 対応 issue: Fix #57192
  • 関連メソッド:
    • Relation#rewhere: 既存の条件をキー単位で置き換える where
    • Relation#take!: レコードを1件取得し、なければ ActiveRecord::RecordNotFound を発生させる
  • 修正箇所:
    • activerecord/lib/active_record/relation.rb
    • activerecord/test/cases/relations_test.rb
    • activerecord/CHANGELOG.md

#57424 Update SOLID_QUEUE_IN_PUMA handling in puma.rb template

マージ日: 2026/5/21 | 作成者: @jordan-brough

  1. 概要 (1-2文で)
    Rails が生成する config/puma.rb のテンプレートにおいて、ENV["SOLID_QUEUE_IN_PUMA"] を真偽解釈するロジックが変更され、nil / 空文字列 "" / "0" / "false" を「false 相当」と扱うようになりました。これにより、環境変数に "false""0" を設定したときに、直感に反して機能が有効になってしまう問題を避けられます。

  1. 変更内容の詳細

何が変わったか

rails new で生成される、または rails app:update で更新される config/puma.rb テンプレート (railties/lib/rails/generators/rails/app/templates/config/puma.rb.tt) 内で、ENV["SOLID_QUEUE_IN_PUMA"] の扱いが以下のような「独自の真偽値判定」に変更されています。

おおまかには、以下のようなロジックになります(疑似コード):

ruby
solid_queue_in_puma_env = ENV["SOLID_QUEUE_IN_PUMA"]

SOLID_QUEUE_IN_PUMA = ![nil, "", "0", "false"].include?(solid_queue_in_puma_env)

もしくはほぼ等価な条件分岐で、

  • 以下の値は 「無効(false)」として扱う
    • nil(環境変数が未設定)
    • ""(空文字)
    • "0"
    • "false"
  • それ以外の値(例: "1", "true", "yes", "on", "anything" など)は 「有効(true)」として扱う

という挙動になります。

なぜこの変更が必要か

config/deploy.yml にはデフォルトで以下のような設定コメントがあります:

yaml
# Run the Solid Queue Supervisor inside the web server's Puma process to do jobs.
# When you start using multiple servers, you should split out job processing to a dedicated machine.
SOLID_QUEUE_IN_PUMA: true

これを見た開発者が、「無効化したいから false と書けばいいだろう」と考えやすい一方で、Rails 側のコードが「文字列としての ENV["SOLID_QUEUE_IN_PUMA"]存在すれば true」と解釈していると、"false" という文字列でも true 扱いになり、意図に反して Solid Queue Supervisor が Puma 内で動作し続けてしまうという落とし穴がありました。

今回の変更で、"false""0" を「false」として扱うようになり、環境変数の直感的な運用がしやすくなります。


  1. 影響範囲・注意点

影響対象

  • 新規に rails new で作成するアプリ
    • 生成される config/puma.rb は、最初からこの新しい真偽判定ロジックを持ちます。
  • 既存アプリで rails app:update を実行した場合
    • config/puma.rb が更新候補として出てきて、差分を取り込むかどうかを選ぶ形になります。

挙動が変わるケース

これまで:
ENV["SOLID_QUEUE_IN_PUMA"] が以下のような値の場合でも、単に「設定されている」という理由だけで true 扱いされていた可能性があります(以前の実装次第ですが、多くの「簡易な実装」ではこの挙動になりがちです)。

  • ENV["SOLID_QUEUE_IN_PUMA"] = ""
  • ENV["SOLID_QUEUE_IN_PUMA"] = "0"
  • ENV["SOLID_QUEUE_IN_PUMA"] = "false"

これから:
上記の値は false 扱いになります。その結果:

  • "" / "0" / "false" を入れて あえて有効にしていた」ようなケースがもしあれば、
    → この PR に相当する変更を取り込んだ後は無効になってしまいます。
    ただし PR 作成者も述べている通り、そのような運用をしているケースは極めて稀と想定されています。

今後、どう設定すべきか

  • 有効にしたい場合
    • 推奨: "true" もしくは "1" など、true として扱われることが明らかな値を設定する
    • 例:
      bash
      SOLID_QUEUE_IN_PUMA=true
      # または
      SOLID_QUEUE_IN_PUMA=1
  • 無効にしたい場合
    • "false""0"、空文字を設定するか、そもそも環境変数を設定しない
    • 例:
      bash
      # 無効にするいずれかの方法
      SOLID_QUEUE_IN_PUMA=false
      # または
      SOLID_QUEUE_IN_PUMA=0
      # または
      unset SOLID_QUEUE_IN_PUMA

マージ後の実務的な注意点

  • rails app:update を実行した際に config/puma.rb に差分が出る場合があるため、
    • Git diff を確認し、SOLID_QUEUE_IN_PUMA 周りの処理をどうするかを明示的に判断する必要があります。
  • CI / 本番環境で SOLID_QUEUE_IN_PUMA を設定している場合、
    • 現在どんな値を設定しているか(特に "false" / "0" / 空文字)を確認し、
    • この PR 相当の変更を取り込むタイミングで挙動が変わらないかをチェックするべきです。

  1. 参考情報 (あれば)

これらの PR は、Solid Queue と Puma の連携や、Rails におけるジョブ処理の標準的な構成(Web サーバ + ジョブ実行プロセスの分離/統合)に関する改善の流れの一部です。


#57284 Don't bump lock_version on records during blob analysis

マージ日: 2026/5/21 | 作成者: @g-pavlik

  1. 概要 (1-2文で)
    Active Storage の非同期分析(ActiveStorage::AnalyzeJob)が lock_version を意図せず更新してしまい、楽観ロック中のレコードで ActiveRecord::StaleObjectError が起きる問題を修正する PR です。
    Blob 分析時の関連レコード touch では updated_at は更新しつつ、lock_version の更新だけを抑制する仕組みが Active Record 側に追加されています。

  1. 変更内容の詳細

背景問題の整理

  • Active Storage の ActiveStorage::AnalyzeJob は Blob のメタデータ (Blob#metadata) だけを書き換える想定。
  • しかし、Blob には touch_attachments による「親レコードへの cascade touch」があり、以下のような流れになっていた:
    1. Blob 分析 → Blob#metadata 更新
    2. Blob#touch_attachments により、添付元モデル (および belongs_to ..., touch: true な親) を touch
    3. その touch が楽観ロック (lock_version カラム) を持つレコードに対して lock_version をインクリメント
  • その結果:
    • ユーザーがフォーム編集中にファイルを添付
    • その直後に分析ジョブが走って親レコードの lock_version が上がる
    • ユーザーが古い lock_version 付きで更新を送る → ActiveRecord::StaleObjectError
    • 実際にはアプリ的な意味での「競合した更新」は発生していないのにエラーになる

config.active_storage.touch_attachment_records = false で cascade 自体を止めることはできるが、それをすると #45567 で問題になった「キャッシュが更新されない」バグが再発するため、不十分な回避策だった。

新しい仕組み: preserve_lock_version_on_touch

ActiveRecord の楽観ロックモジュールに以下が追加されました:

ruby
ActiveRecord::Locking::Optimistic.preserve_lock_version_on_touch do
  record.touch
end

このブロック内で発生した touch については:

  • lock_version更新しない(インクリメントしない)
  • UPDATEWHERE 句に lock_version = ?含めない
  • ただし updated_at など通常の touch 対象カラムは 更新される

これにより:

  • 親レコードのキャッシュキーや updated_at ベースのキャッシュはちゃんと更新される
  • 楽観ロックの競合検出には影響を与えない(=「内容が変わっていないのに lock_version だけ上がる」ケースを防ぐ)

実装のポイント

  1. _touch_row の変更

    • 通常、touch 時は lock_version も「変更された属性」に含めて dirty tracking する。
    • preserve_lock_version_on_touch が有効な場合、_touch_rowlock_version@_touch_attr_names に追加しないようにしている。
    • これにより、「touch の一環として lock_version を変更する」という動作自体を抑制。
  2. _update_row の変更

    • Locking::Optimistic#_update_row 内で、attempted_action == "touch" かつ「preserve_lock_version フラグが立っている」場合は、楽観ロック用のロジックをスキップして、Persistence#_update_row にフォールバックする。
    • フォールバック先では lock_version をインクリメントせず、WHERE lock_version = ? も付かない、通常の更新として実行される。
    • attempted_action == "touch" でのみ有効にしているため、通常の saveupdate ではこれまで通りロックが効く
  3. Deferred touch (touch_later) への対応

    • belongs_to ..., touch: true などは「コミット前コールバック (before_committed!) で親を touch_later する」という非同期タッチになる。
    • preserve_lock_version_on_touch のブロックは touch を呼び出したスレッドでのみ有効なので、そのままだとコミット時点ではブロックのスコープを外れてしまう。
    • そこで、新たに prepended された DeferredTouch モジュールで:
      • touch_later 呼び出し時に「lock_version を preserve したいかどうか」の意図をレコードインスタンス上のフラグとして保存
      • 実際に defer された touch が実行されるタイミングで、そのフラグを参照してロックバージョンの preserve を適用
    • このフラグは一度使ったら消費される(使い回しされない)よう考慮されている。

Active Storage Blob 側の変更

ActiveStorage::Blob#touch_attachments が以下のように変更されています(概念的な例):

ruby
def touch_attachments
  ActiveRecord::Locking::Optimistic.preserve_lock_version_on_touch do
    attachments.includes(:record).find_each do |attachment|
      attachment.record&.touch if attachment.record&.respond_to?(:touch)
    end
  end
end

(実際のコードは多少異なる可能性がありますが、意図としてはこのように「Blob からの cascade touch 全体を preserve ブロックで囲む」形です。)

これにより:

  • Blob の分析 → Blob 更新 → 親レコード touch → 親レコードの updated_at / キャッシュキーは更新
  • しかし親レコードの lock_version は変わらない

という挙動になるため、フォーム編集中ユーザーの更新が不必要に StaleObjectError にならなくなります。

テストとドキュメント

  • activerecord/test/cases/locking_test.rbpreserve_lock_version_on_touch の挙動を検証するテストが追加。
  • activestorage/test/models/blob_test.rb に、Blob 分析時に lock_version が変わらないことを確認するテストが追加。
  • activestorage/CHANGELOG.md に今回の変更が追記されています。

  1. 影響範囲・注意点

影響範囲

  • ActiveRecord

    • 楽観ロック (locking/optimistic) の内部実装に新しいコードパスが追加。
    • 通常の save / update / destroy など、attempted_action != "touch" な更新はこれまで通りの挙動。
    • preserve_lock_version_on_touch を明示的に使うコードのみ新挙動の影響を受ける。
  • Active Storage

    • 標準の Blob 分析ジョブ (ActiveStorage::AnalyzeJob) 経由で発生する親レコード touch は、今後 lock_version を変えない。
    • config.active_storage.analyze の利用がある通常のアプリでは、この変更によりフォームの「謎の StaleObjectError」が解消される可能性が高い。

注意点・開発者視点でのポイント

  • touch が lock_version を上げない」という挙動は preserve_lock_version_on_touch ブロックの中だけ で有効。
  • 独自に以下のようなコードを書いている場合、今回の機能を流用できる可能性があります:
    • 「キャッシュ無効化のためだけに touch しているが、実際の競合検出には使いたくない」箇所
    • 「自動ジョブが touch するだけでユーザー更新と競合扱いされる」のを避けたい箇所
  • 一方で、「touch をトリガーに実質的な更新だと見なしてほしい」ワークフロー(例: lock_version の変化を監査に使っている等)がある場合は、このブロックを安易に使わないほうがよいです。
  • 既存アプリで特別な対応をしなくても、Rails をこのコミット以降に上げれば Active Storage 周りの問題は自動的に改善されます(config.active_storage.touch_attachment_records を false にしていなければ)。

  1. 参考情報 (あれば)
  • 修正対象の issue:
    • #55764 – Active Storage の非同期分析に起因する ActiveRecord::StaleObjectError
  • 挙動ドキュメント PR:
    • #55799 – 現象のドキュメント化(この PR が入れば不要になる見込み)
  • cascade touch の導入経緯:
    • #45567 / commit 294c271062 – Blob 分析後もキャッシュを更新するための cascade 追加
  • 既存設定オプション:
    • #49723 – config.active_storage.touch_attachment_records 追加(今回はこれをオフにせずに問題を解消するアプローチ)

#57422 Fixes to query command

マージ日: 2026/5/21 | 作成者: @andyjeffries

  1. 概要 (1-2文で)
    rails query コマンドのバグ修正 PR で、
  • 読み取り専用接続をバイパスして書き込みできてしまう危険な挙動
  • 実用上「使えない」レベルの挙動不良
    を修正し、安全かつ実用的に使えるようにしたものです。

  1. 変更内容の詳細

※PR本文から分かるのは主に「何を直したか(意図)」であり、「どう直したか(具体的実装)」は diff がないと細部までは追えません。以下は PR 説明・変更ファイルから推測できるレベルでまとめています。

2-1. 対応しているバグ

PR は以下 2 件の Issue を同時に解決しています。

  • #57419: rails query の「実用上の致命的な不具合」
    • 作成者が「LLM から rails query を使いたいが、これがあると現実的に無理」と表現しているため、
      • 入出力の扱い(プロンプトや結果表示)
      • コマンドの引数解釈や終了コード
      • あるいはインタラクティブな利用方法
        のどれかが壊れていた可能性が高いです。
  • #57420: 読み取り専用 DB 接続がバイパスされる危険な挙動
    • rails query 実行時、本来 readonly な接続に対しても書き込み系クエリが実行できてしまう問題を修正しています。
    • これは「本番のレプリカに対して誤って更新をかける」といった事故につながるので、セキュリティ・運用上重大です。

PR の説明文から:

one DANGEROUS bypassing of the database connection being readonly #57420
one real deal breaker of an aspect of usage that is broken #57419

と明記されており、意図としては

  • rails query は DB の接続モード(特に read-only)を正しく尊重する
  • CLI として実際に日常的に使える UX / 挙動にする

という 2 点を達成する修正です。

2-2. 変更されたファイル

railties/lib/rails/commands/query/query_command.rb

  • +14 / -5 行の変更が入っています。

  • このファイルは rails query サブコマンドの本体ロジックを持つクラス (例: Rails::Command::QueryCommand) が定義されている場所です。

  • ここで行われているであろう主な修正は次のようなものが考えられます:

    1. DB 接続の扱いの修正

      • ActiveRecord::Base.connection ではなく、
        Rails が「現在のコマンド・環境に対応する接続」を適切に解決する仕組みを用いている可能性があります。
      • あるいは、接続に対して
        ruby
        connection.readonly? # あるいは role / type を見る
        のようなチェックを行い、書き込みクエリの実行を拒否する、または例外を投げる形に変えた可能性があります。
    2. クエリ実行方法・例外処理の調整

      • 失敗時のメッセージや終了コードがツール連携(LLM など)で扱いやすいように整理されている可能性があります。
      • これにより、「使い物にならない」問題 (#57419) が解消されていると考えられます。
    3. 入出力の改善

      • 標準入力・標準出力の扱いを、非対話的な利用(例: echo "SELECT ..." | rails query)でも安定して動くように調整している可能性があります。
      • LLM 連携を意識しているため、出力のフォーマット・エラー時の出力なども見直されている可能性があります。

具体的なコード例は diff がないと正確に書けませんが、イメージとしては以下のようなロジックが追加・修正されている可能性があります:

ruby
# イメージ: 書き込みクエリを read-only 接続で実行しないようチェック
def execute(query)
  connection = ActiveRecord::Base.connection

  if connection.respond_to?(:read_only?) && connection.read_only? && write_query?(query)
    raise "Cannot execute write query on a read-only connection"
  end

  connection.exec_query(query)
end

railties/test/commands/query_test.rb

  • +52 行でテストが大幅に追加されています。

  • 追加されたテストの性質から、この PR の意図と仕様がある程度推測できます:

    1. バグ再現テスト + 修正確認

      • #57419 で報告されていた「使えない挙動」が再現されるケースをテスト化。
      • rails query 実行時の入出力や終了コード、例外の出方などを検証しているはずです。
    2. read-only 接続の尊重テスト

      • ActiveRecord の接続を read-only に設定した状態で rails query を実行し、
        • 書き込みクエリ(INSERT, UPDATE, DELETE など)がブロックされる/失敗すること
        • 読み取り専用クエリ(SELECT)は通ること
          を確認するテストが追加されていると考えられます。
      • これにより、#57420 に対する回 regress しない保証を提供しています。

  1. 影響範囲・注意点

3-1. 影響範囲

  • 影響を受けるのは rails query コマンドのみ です。
  • アプリケーションコード(コントローラやモデルなど)や他の Rails コマンド (rails console など) には影響しません。

ただし、rails query を使っている運用・ツール連携では次の挙動が変わります:

  1. read-only 接続での書き込みクエリが通らなくなる

    • これまで「たまたま通っていた」ケースは、今後は失敗あるいはエラーになり得ます。
    • 本来は望ましい挙動ですが、既存のスクリプトやツールがこれに依存していると挙動が変わります。
  2. エラー処理や終了コードの改善

    • #57419 の修正により、失敗時の終了コードやメッセージが変わっている可能性があります。
    • CI や自動化ツールから rails query を呼び出している場合、終了コードや出力を前提にしている部分があれば確認した方が安全です。

3-2. 注意点

  • PR 作者も書いている通り、「バグ修正ではあるが、挙動の変更はそれなりに大きい」可能性があります。
  • 特に以下のような運用をしているチームは影響確認がおすすめです:
    • 本番で rails query を使って ad-hoc クエリを投げている
    • read replica / primary を役割分担していて、readonly 接続の上で rails query を実行している
    • LLM や bot が rails query を経由して DB にアクセスしている

  1. 参考情報 (あれば)

この PR により、rails query

  • DB 接続の read-only 設定をきちんと守り
  • CLI ツールや LLM 連携からも実用的に扱える
    コマンドとして使えるようになる、という位置付けの変更です。

#54930 Add referential integrity note in AR::FixtureSet docs [ci skip]

マージ日: 2026/5/21 | 作成者: @danimashu

  1. 概要 (1-2文で)
    ActiveRecord::FixtureSet のAPIドキュメントに、「フィクスチャ読み込み時にRailsがリレーショナル整合性(外部キー制約など)を一時的に無効化する」ことを説明する注意書きが追加されました。これにより、特にDBトリガーや外部キー検証に依存しているテストで、意図せず「非整合」なテストデータが生じうる点への注意喚起が強化されています。

  1. 変更内容の詳細

追加された内容の趣旨

修正は activerecord/lib/active_record/fixtures.rb に対する ドキュメントコメントの2行追加のみ です。機能的な挙動は一切変わっていません。

追加されたドキュメントの要点は次のとおりです:

  • ActiveRecord::FixtureSet がテストごとにフィクスチャを削除・挿入するとき、
    • RDBMSのリレーショナル整合性機構(外部キー制約、PostgreSQLのトリガーなど)を一時的に無効化する
    • そのため、「外部キーの依存関係どおりの順序でINSERTする必要がない」形でデータを投入している
  • PostgreSQL:
    • ALTER TABLE ... DISABLE TRIGGER ALL を実行し、外部キー制約だけでなく ユーザー定義トリガーもすべて無効化 してフィクスチャをロードしている
    • config.active_record.verify_foreign_keys_for_fixtures が有効な場合、後から外部キー制約のチェックを実行し、違反があれば検出する
    • ただし「その他のトリガーに基づくビジネスルール」は 検証されないまま になりうる
  • MySQL:
    • 外部キー再検証に相当する config.active_record.verify_foreign_keys_for_fixtures の機構は まだ未実装
    • したがってテストDBに外部キー不整合が紛れ込んでも、自動的には検出されない可能性がある

イメージしやすいサンプルシナリオ

PostgreSQL + トリガーを使っているケース:

sql
CREATE FUNCTION check_business_rule() RETURNS trigger AS $$
BEGIN
  -- 何らかのビジネスルール検証
  IF NEW.status = 'paid' AND NEW.paid_at IS NULL THEN
    RAISE EXCEPTION 'paid_at must be set if status is paid';
  END IF;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER orders_business_rule
  BEFORE INSERT OR UPDATE ON orders
  FOR EACH ROW
  EXECUTE FUNCTION check_business_rule();

このようなトリガーに依存したビジネスルールがあっても、フィクスチャ読み込み時には

sql
ALTER TABLE orders DISABLE TRIGGER ALL;
-- fixture の INSERT / DELETE
ALTER TABLE orders ENABLE TRIGGER ALL;

という流れになるため、トリガーが発火せず、ビジネスルールを満たしていないテストデータが入ってしまう可能性があります。

外部キーについては、config.active_record.verify_foreign_keys_for_fixtures = true で後検証できますが、上記のような 任意トリガーに基づくルールは検証されない、という点が今回のドキュメントで明示的に注意喚起されるようになりました。


  1. 影響範囲・注意点

コードの動作は一切変わっていないため、既存アプリへのランタイム影響はありません。
ただし、開発者目線での「理解すべきポイント」「テスト戦略上の注意」は次のとおりです。

  1. フィクスチャがDBの整合性検証をバイパスすることがある

    • テスト環境のDBには、本番環境では許されないようなデータ(外部キー不整合やトリガー違反)が入り込む可能性があります。
    • その結果、「本番では失敗するはずの操作がテストでは通ってしまう」といった齟齬が起こりえます。
  2. PostgreSQLでの外部キー検証オプション

    • config.active_record.verify_foreign_keys_for_fixtures = trueconfig/environments/test.rb に設定すると、フィクスチャ読み込み後に外部キー制約の違反チェックが走ります。
    • ただし、前述の通り「ユーザー定義トリガーのロジック」までは検証されません。
    • トリガーに重めのロジックを載せている場合、テストでそのロジックを信頼しすぎないほうが安全です(モデルレイヤーやサービス層側での検証をテストする、など)。
  3. MySQLでは外部キー再検証が未サポート

    • config.active_record.verify_foreign_keys_for_fixtures を指定しても、MySQLアダプタ側はまだ対応しておらず、外部キー不整合が残る可能性があります。
    • MySQLを使っている場合、フィクスチャの使い方にはより注意が必要です:
      • テストデータ投入順序や、関連オブジェクトの存在をアプリ側ロジックやモデルバリデーションで担保する
      • 必要なら、フィクスチャではなく FactoryBot などでアプリケーション層経由でデータを作る
  4. トリガーに依存したビジネスルールのテスト

    • トリガーによるチェックを前提にすると、「テスト中だけそのチェックが無効化されている」というギャップが発生します。
    • そのルールをアプリケーションコード(モデルバリデーションやサービスオブジェクト)でも再現し、そちらをテストするか、
    • あるいはトリガーを前提としたテストだけはフィクスチャではなく直接SQLを叩く/別の仕組みで準備する、といった戦略も検討が必要です。

  1. 参考情報 (あれば)

Rails本体の関連箇所やガイド:

このPR自体はドキュメントのみの微修正ですが、フィクスチャとDB制約の関係を正しく理解しておくと、「テストは通るのに本番で落ちる」系の問題を減らすのに役立ちます。


#52871 Allow to pass array values to .in_order_of

マージ日: 2026/5/21 | 作成者: @doits

  1. 概要 (1–2文で)
    .in_order_of にネストを含む配列を渡して、「値のグルーピング + グループの順序付け」ができるようになりました。これにより、ENUM などを使った状態カラムを「いくつかの状態をまとめて 1 グループとして扱いつつ、そのグループ自体の順序も制御する」といったクエリが書きやすくなります。

  1. 変更内容の詳細

これまでの .in_order_of

従来の .in_order_of は、基本的に「単純な値の並び順」を指定するためのものでした。

rb
Post.in_order_of(:state, [:published, :archived, :canceled])
# => state が :published → :archived → :canceled の順に並ぶ

このとき SQL は概ね次のような CASE 式になります。

sql
ORDER BY CASE
  WHEN "posts"."state" = 1 THEN 1
  WHEN "posts"."state" = 2 THEN 2
  WHEN "posts"."state" = 3 THEN 3
END ASC

(ENUM の内部値に応じて 1, 2, 3 などに type cast される)

今回追加された機能: 値のグルーピング

この PR では、.in_order_of配列をネストして渡す ことで「同じ順位のグループ」を作れるようになりました。

PR の例:

rb
Posts.in_order_of(:state, [[:published, :canceled], :archived])
     .order(created_at: :desc)

生成される SQL(概略)は以下のようになります:

sql
SELECT "posts".*
FROM "posts"
WHERE "posts"."state" IN (1, 2, 3)
ORDER BY CASE
  WHEN "posts"."state" IN (1, 2) THEN 1   -- :published, :canceled がグループ1
  WHEN "posts"."state" = 3 THEN 2        -- :archived がグループ2
END ASC,
"posts"."created_at" DESC

ポイント:

  • 第一引数は従来通り「カラム名」 (:state)。
  • 第二引数で「順序」を指定するが、ネストした配列を渡せる:
    • [:published, :canceled] → 1つのグループとして扱われる(IN (1, 2)
    • :archived → 別グループ(= 3
  • その後に .order(created_at: :desc) など通常の order を追加することで
    「まずグループ順、次にグループ内では created_at DESC」という並びになる。

実用パターンの例

  • 「公開中 or キャンセル済み」を上にまとめて表示したい:

    rb
    Post.in_order_of(:state, [[:published, :canceled], :archived, :draft])
  • 「active 系の状態をまとめて、inactive を最後にまわす」など:

    rb
    User.in_order_of(:status, [[:active, :trial, :pending], :inactive])

実装的なポイント (コード上での変更点)

ファイル単位では次の 3 つが更新されています。

  • activerecord/lib/active_record/relation/query_methods.rb
    • .in_order_of の実装を拡張し、値として配列が渡された場合には IN (...) 句を使った CASE を生成するように変更。
    • 具体的には、ORDER 用の条件式を組み立てる箇所で、
      • 単一値: WHEN column = value THEN n
      • 配列値: WHEN column IN (value1, value2, ...) THEN n の2パターンを扱うようになっている。
    • また、これに伴いtype cast の単位が「配列全体」ではなく「配列の各要素」になっている
  • activerecord/test/cases/relation/field_ordered_values_test.rb
    • 新機能向けのテストが追加。
    • ネストした配列を渡したときに期待通りの SQL/並び順になることを検証。
  • activerecord/CHANGELOG.md
    • Active Record に「.in_order_of で配列をグルーピング指定に使えるようになった」旨を記載。

  1. 影響範囲・注意点

主な影響範囲

  • Active Record の .in_order_of を利用しているコード全般が対象ですが、
    • 既存の「単純な配列」を渡すケースはそのまま動作します(後方互換性あり)。
    • 新たに「配列の中に配列を含める」ケースがサポートされた形です。

注意点・懸念点(PR 内で述べられているもの)

  1. type cast の方式変更

    • 以前は「配列を 1 つの値として type cast」していた部分が、 今回の対応により「配列の各要素を個別に type cast」する挙動になります。
    • 通常の整数/文字列/ENUM などでは問題になりにくいですが、
      • DB カラム自体が「配列型」の場合(PostgreSQL の integer[] など)
      • あるいはアダプタが「配列値を 1 単位で扱う」ことを仮定している場合 には、従来と挙動が変わる可能性があります。
    • そのようなケースを使っている場合は、.in_order_of の利用有無を確認した方が安全です。
  2. パフォーマンス面

    • PR 作成者は「Array.wrap を使った方がコードはすっきりするが、パフォーマンスへの影響があり得る」と言及。
    • 現行実装は主に values.is_a?(Array) で分岐しており、あえて Array.wrap は使っていません。
    • .in_order_of で多数の値・多数のグループを指定すると CASE が大きくなるため、DB 側の最適化・インデックス利用に注意が必要です。
  3. 複雑なネストは非想定

    • 想定されているのは「1段階のネスト」([[:a, :b], :c, [:d, :e]] のような形)です。
    • それ以上の多重ネスト([[:a, [:b, :c]]] など)は可読性も悪く、将来の互換性も保証しにくいため、避けるのが無難です。

  1. 参考情報 (あれば)
  • PR 本文の主なモチベーション:
    • .in_order_of を使って「レコードをグルーピングし、そのグループをさらにソート」したいニーズから実装された。
    • 既存の Rails 機能で同じことを簡潔に表現する方法が見当たらなかったため、コアへの機能追加となった。
  • 関連しそうな既存 API:
    • order(Arel.sql(...)) で自前の CASE WHEN を書けば類似のことは可能だが、Enum などの type cast を自分で面倒見る必要がある。
    • .in_order_of は Active Record の type system を使って value をキャストした上で CASE を組み立ててくれる点が利点。

#57416 [RF-Docs][ci-skip] Caching with Rails (#57107)

マージ日: 2026/5/21 | 作成者: @p8

  1. 概要 (1-2文で)
    Rails ガイド「Caching with Rails」を現行のRailsの挙動・用語に合わせて大幅に書き直し、構成も整理しつつ、低レベルキャッシュ・フラグメントキャッシュ・Conditional GET・SQL/クエリキャッシュ・Solid Cache・非リクエストコンテキストでのキャッシュなどをより実践的かつ詳細に解説するドキュメント改訂PRです。機能追加や挙動変更ではなく、既存の仕組みを正しく理解してもらうためのドキュメント改善です。

  1. 変更内容の詳細

全体構成・導入の見直し

  • ガイド冒頭(イントロ)と「このガイドを読み終えるとできるようになること」リストを、実際の内容とずれないように書き換え。
  • 「Types of Caching」セクションの並びを整理し、Rails の主なキャッシュ手法を理解しやすい順序に変更:
    1. 低レベルキャッシュ(Rails.cache
    2. フラグメントキャッシュ
    3. Conditional GET(HTTP キャッシュ)
    4. SQL キャッシュ(= Active Record クエリキャッシュ)

低レベルキャッシュ (Rails.cache) の拡充

次のトピックについて詳しい説明・サンプルが追加/拡充されています。

  • Rails.cache.fetch の挙動

    • キャッシュヒット/ミスとは何か、fetch ブロックがいつ実行されるかを丁寧に説明。

    • 典型的な利用例:

      ruby
      result = Rails.cache.fetch("expensive_api:#{user.id}", expires_in: 10.minutes) do
        ExternalApi.call(user)
      end
  • キャッシュヒット/ミスの概念と運用上の考え方

    • ミス時に高コストな処理が走ること、そのためのキー設計や TTL 設計の重要性など。
  • キャッシュのクリア(Rails.cache.clear

    • アプリ全体のキャッシュをクリアする操作であること、ローカル/開発環境やメンテナンス時の使い方などに触れている模様。
  • キャッシュキー生成と cache_key_with_version

    • Active Record オブジェクト由来のキーは #cache_key_with_version を使うのが基本であることを強調。

      ruby
      Rails.cache.fetch(article.cache_key_with_version) do
        render_to_string partial: "articles/article", locals: { article: article }
      end
  • Active Record オブジェクトを「そのまま」キャッシュするのがよくない理由

    • モデルのスキーマ変更時の整合性問題
    • キャッシュに古いオブジェクトが長期に残ることによるバグ
    • シリアライズ/デシリアライズコストやMarshal互換性など
      → 代わりに、IDやcache_keyをキーにして「レンダリング結果」や「計算済み値」をキャッシュする流儀を推奨。
  • fetch の高度なオプション(例:race_condition_ttl

    • 「ドッグパイル(キャッシュスタンピード)」対策として、期限切れ直後の一時的な猶予時間を設けるパターンを説明。

      ruby
      Rails.cache.fetch("slow-report", expires_in: 10.minutes, race_condition_ttl: 30.seconds) do
        HeavyReport.generate
      end

フラグメントキャッシングの改善

  • テンプレートツリーダイジェストの説明

    • ERB/partial の構造に応じて自動生成されるダイジェストがキャッシュキーに含まれること、
    • 部分テンプレートを編集するとキーが自動的に変わり、フラグメントが正しく無効化されるメカニズムを明示。
  • コレクションキャッシュの挙動

    • render partial: "article", collection: @articles のような場合のキャッシュキー生成ルールや、
    • 各要素ごとのフラグメントキャッシュ + コレクションをまとめるラッパの動き、
    • expires_in 等のオプションがコレクションレンダリングにどう適用されるかが整理されている。
  • キャッシュキーと有効期限オプションの扱い

    • フラグメントキャッシュ (cache ビュー helper など) における expires_in, namespace, skip_digest 等がどのように効くかを明解化していると考えられます。

依存関係管理のセクションをフラグメントキャッシュ配下へ移動・再構成

  • 「暗黙的な依存」「明示的な依存」「外部依存」の3種類を整理して説明。
  • view helper 経由でレンダリングされる場合など、依存関係が見えづらいパターンでも、どの変更でどのフラグメントが無効になるかを理解しやすくした構成になっています。

ロシアン・ドール・キャッシング(Russian doll caching)の明確化

  • ネストしたフラグメントキャッシュがどのように連携して更新されるのかを整理。

  • 親モデル側で touch: true を指定することで、子モデル更新時に親の updated_at が変わり、それに紐づく親フラグメントも無効化される流れを強調。

    ruby
    class Comment < ApplicationRecord
      belongs_to :post, touch: true
    end

共有パーシャルキャッシュの再説明

  • MIME タイプごとのキャッシュ分離(HTML, JSON, etc.)について、どのようにキーに反映されるかを説明。
  • ビューパス上のパーシャル解決ルールとキャッシュキーの関係が、より追いかけやすい形で書き直されている。

Conditional GET (HTTP キャッシュ) の拡充

  • stale?fresh_when の説明強化

    • If-None-Match / If-Modified-Since と ETag / Last-Modified の関係を踏まえ、
    • それぞれをどう使い分けるか、返るレスポンス (200 vs 304) の違いを、より丁寧に説明。
  • strict_freshness の現在の挙動

    • 条件付きリクエストヘッダがある場合でも、どの条件でコンテンツを再送するか/しないかの基準を明確化。
  • http_cache_forever をいつ使うべきか

    • コンテンツが実質的に不変な場合に限るべき理由、将来の変更をどう扱うか(パスやクエリにバージョンを含める等)などを説明。
  • 強いETag / 弱いETag の新セクション

    • W/ プレフィックスの意味
    • バイナリ完全一致が必要な場面 vs 論理的な同一性でよい場面の違いなど。

SQL キャッシュ(Active Record クエリキャッシュ)の更新

  • SQL キャッシュが「Active Record query caching」であり、リクエスト/ジョブなどの「実行コンテキスト」単位で自動的に効く機能であることを明示。
  • 同一コネクション・同一コンテキスト内で同じSQLが繰り返されるとメモリ上から返ること、config.action_controller.perform_caching とは別の仕組みであることを強調している可能性が高いです。
  • クエリキャッシュのスコープ(たとえばミドルウェアや ActiveRecord::Base.cache ブロック)についても現行仕様に即して説明。

Solid Cache セクションの更新

  • Rails での現在の正式名称・用語に合わせて文章を更新。
  • 新規 Rails アプリ生成時のデフォルト設定について追記。
  • DB バックエンドとして Solid Cache を使う設定例(database.yml/config/environments/*.rb)を最新の形で提示。
  • 開発環境でのセットアップ手順を現行の recommended flow に合わせて案内。
  • トランザクションとキャッシュストア選択の説明をより厳密に:
    • DBトランザクションとキャッシュ書き込みの整合性、
    • Solid Cache を他のストア(Memcached, Redis等)とどう使い分けるかといった観点を明確化。

Advanced Caching Patterns セクションの新設

新しく「高度なキャッシュパターン」セクションが追加され、特に以下をカバー:

  • バックグラウンドジョブや非リクエストコンテキストでのキャッシュ

    • Active Job / Sidekiq等から Rails.cache を使う際の考慮点(名前空間、キー設計、キャッシュのライフサイクルなど)。
    • 「リクエスト単位ではなく、プロセス全体・複数ワーカーで共有される」ことを意識した設計の必要性。
  • ローカルキャッシュ (Rails.cache.with_local_cache) の挙動

    • 単一リクエスト/ジョブ内でだけ有効なインメモリキャッシュレイヤーを追加し、同じキーへの多重アクセスを高速化するパターン。

      ruby
      Rails.cache.with_local_cache do
        # このブロック内では同一キーの読み出しがより高速化される
        user = Rails.cache.fetch("user:#{id}") { User.find(id) }
        # 以降の同一キーfetchはローカルキャッシュから返る
      end

メタ的な変更

  • ETag の見出し/リンクなど、ガイド内のアンカーや内部参照を修正し、broken link を解消。
  • guides/source/documents.yaml を更新し、ガイド一覧・ナビゲーション上での位置づけやタイトル等を最新化。

  1. 影響範囲・注意点
  • これは ドキュメントのみの変更 であり、Rails 本体のキャッシュ機能の挙動変更は行っていません。

  • ただし、説明がかなり現行仕様に寄せて具体的になったため、以下のような「認識のアップデート」が必要になる可能性があります。

    • 「Active Record オブジェクトを丸ごとキャッシュしてもいい」という古い習慣は非推奨であること。
    • フラグメントキャッシュのキー/ダイジェスト/依存関係の理解が、以前よりも厳密に整理されているため、既存実装を見直すきっかけになり得る。
    • Conditional GET / ETag 周りのベストプラクティスが明文化された結果、既存アプリで「とりあえず fresh_when」しているコードを改善したくなるかもしれない。
    • Solid Cache の推奨設定が最新版に合わされているので、古い記事・ブログ等と食い違う場合は、このガイドを優先すべき。
  • ガイド内のサンプルコードや推奨パターンが刷新されているため、新しくキャッシュを導入する際は、このPR後のドキュメントを前提に設計するのがよいです。


  1. 参考情報 (あれば)

#57417 Update the Rails on Rack guide (#56918) [ci-skip]

マージ日: 2026/5/21 | 作成者: @p8

  1. 概要 (1-2文で)
  • Rails公式ガイド「Rails on Rack」を大幅に書き直し・構成見直ししたドキュメント改善PRです。
  • Rackミドルウェアスタックの最新情報反映、初期化処理へのリンク追加、カスタムミドルウェアやコントローラからRack APIを使う高度な利用方法などが追記されています。

  1. 変更内容の詳細

2-1. 全体的な文言修正・構成改善

  • 説明文の表現を整理し、他のガイド(特に Configuration / Instrumentation ガイド)とスタイルを揃えるように再構成。
  • 古くなっていた説明や用語を現行Railsに即した形にアップデート。
  • 末尾の「Resources」セクションを削除し、外部リンクや参照は本文中の関連箇所に直接埋め込む形に変更。

これにより、ガイド全体が「今のRailsのRack周りの実装と運用」を前提に読みやすくなっています。


2-2. bin/rails server と初期化ガイドの連携

  • Rails::Server の起動フローをこのガイド内に詳細に書かず、 Initialization ガイドの「Rails server start」セクションへのリンクを明示的に追加。
    • 例: 「bin/rails server が内部で何をしているか」については Initialization ガイドに委譲。

意図:

  • 起動処理の情報源を1か所に集約し、将来の変更時にドキュメントの同期ズレを起こしにくくするため。
  • Rackガイドでは「Rackとの関係性・ミドルウェア」を主題にし、起動ロジックの詳細は別ガイドへ分離。

2-3. デフォルトミドルウェアスタックの更新

  • Railsアプリがデフォルトで読み込む Rack ミドルウェア一覧と、その説明を最新の構成に合わせて更新。
  • 各ミドルウェア名から、対応する API ドキュメント (api.rubyonrails.org など) へのリンクを追加。
    • 例: ActionDispatch::Static, ActionDispatch::Cookies, Rack::Runtime などに対して、それぞれのクラス API ドキュメントへのリンク。

ポイント:

  • 以前のガイドでは古いミドルウェア名や構成が残っていたり、Sprockets 前提の説明が含まれていた可能性があるのを整理。
  • Propshaft 利用時のミドルウェア Propshaft::QuietAsset に対応(後述)。

2-4. 内部ミドルウェアスタックセクションの再構成

  • 「Internal Middleware Stack」のセクション構成を全面的に見直し、Configuration ガイドと似たフォーマットに変更。
    • 各ミドルウェアを箇条書き、もしくは小見出し + 説明 + リンクという形式で整理。
    • ミドルウェアごとに「何をしているのか」「いつ役に立つのか」が分かりやすく説明されるように。
  • 中には「Railsが自動的に挿入するもの」と「環境や設定により条件付きで挿入されるもの」が区別されて書かれている可能性が高いです。

開発者視点での価値:

  • “なぜこのミドルウェアがスタックにいるのか”“これを削ると何が壊れそうか”を判断しやすくなる。
  • カスタムスタックを組む際に、標準スタックを参考にしやすい。

2-5. Sprockets から Propshaft への更新

  • 監査メモにもある通り、Sprockets::Rails::QuietAssets は現行では Propshaft::QuietAsset に置き換えられているため、ガイドの記述を Propshaft ベースに更新。
  • 特に development 環境における asset リクエストのログ出力抑制など、「静的アセットに対するQuietモード」の説明がPropshaft前提に修正されていると考えられます。

意味合い:

  • Rails 7.1 以降のデフォルトに沿った形で、asset パイプライン周りの説明が一貫する。
  • Sprockets 前提の古い情報を参照してしまうことを防げる。

2-6. カスタムミドルウェアをスタックに追加する方法のセクション追加

ガイドに「カスタムミドルウェアを追加する」ための新しい専用セクションが追加されています。

典型的には、以下のような内容・サンプルが含まれていると考えられます(あくまで代表的な例):

ruby
# app/middleware/my_custom_middleware.rb
class MyCustomMiddleware
  def initialize(app)
    @app = app
  end

    # Rack API: call(env) -> [status, headers, body]
  def call(env)
    # 前処理 (logging, 計測など)
    status, headers, response = @app.call(env)
    # 後処理 (header 追加など)
    [status, headers, response]
  end
end
ruby
# config/application.rb など
config.middleware.use MyCustomMiddleware
# あるいは順序指定
config.middleware.insert_before Rack::Runtime, MyCustomMiddleware
config.middleware.insert_after  ActionDispatch::Static, MyCustomMiddleware

ガイドには、おそらく以下のような点も説明されています:

  • config.middleware.use / insert_before / insert_after / delete の使い分け。
  • env Hash に入ってくる rack.* キー、action_dispatch.* キーの例。
  • ミドルウェアをproductionだけ・developmentだけで読み込む Rails.env.production? などのパターン。

メリット:

  • Rackレベルでログ、セキュリティヘッダ、A/Bテスト用cookie処理などを挟みたいときの、Railsとしての公式的なやり方が明文化された。

2-7. コントローラ内でRack APIを使う高度な利用方法のセクション追加

新しく「高度な使い方」的なセクションが加わり、「RailsコントローラからRack APIを直接利用する方法」が説明されています。

典型的には次のようなユースケースの説明が含まれることが多いです:

  • Rack準拠レスポンス([status, headers, body])を自前で返す:

    ruby
    class RawRackController < ApplicationController
      def show
        body = ['Hello from Rack!']
        headers = { 'Content-Type' => 'text/plain', 'Content-Length' => body.sum(&:bytesize).to_s }
        status = 200
    
        # Rack風に直接返すこともできるが、Rails的には render / send_data を使うことが多いはず、という比較説明など
        [status, headers, body]
      end
    end
  • request.env を通じて Rack レベルの情報(rack.session, rack.url_scheme, HTTP_* ヘッダなど)を扱う例:

    ruby
    class EnvController < ApplicationController
      def index
        logger.info request.env['HTTP_USER_AGENT']
        head :ok
      end
    end
  • ActionDispatch::Response と純 Rack レスポンスとの対応関係や、RackアプリをRailsルーティングでマウントする例:

    ruby
    # config/routes.rb
    mount SomeRackApp, at: '/rack_app'

狙い:

  • 「RailsはRack上で動いている」ことを実感しつつ、必要に応じて素のRack APIに近い制御もできることを示す。
  • 既存のRackアプリを Rails に統合するときの参考になる。

  1. 影響範囲・注意点
  • コードへの直接的な変更はなし
    変更対象は guides/source/rails_on_rack.md のみで、アプリケーションやフレームワーク本体の挙動には影響しません。

  • ただし「正しい理解」に影響する

    • これまで古いガイドに基づいてミドルウェア構成や起動フローを理解していた場合、その前提が古くなっている可能性があります。
    • 特に asset 関連(Sprockets vs Propshaft)や、デフォルトミドルウェアの一覧・順序の理解を、この新ガイドをもとにアップデートした方がよいです。
  • ドキュメント参照パスが変わる

    • Rails起動処理については、Rails on Rack ガイドだけでなく Initialization ガイドも必ず参照する前提になります。
    • カスタムミドルウェアの追加・調整を行う際は、この新しいセクションの方が現行ベストプラクティスに近いため、既存のブログ記事よりも公式ガイドを優先して読むのがおすすめです。

  1. 参考情報 (あれば)

#57415 [RF Docs] Active Record Query Interface (#56341)

マージ日: 2026/5/21 | 作成者: @p8

  1. 概要 (1–2文)
    Active Record Query Interface ガイド(active_record_querying.md)を大幅に書き直し・再構成し、関連する API コメントおよび Composite Primary Keys ガイドとのリンクも整理した PRです。SQL や複合主キー、スコープ、ロック、パフォーマンス関連への導線を明確にしつつ、例示・用語の整合性や安全性(SQL インジェクション等)の説明を強化しています。

  1. 変更内容の詳細

2-1. Active Record Query Interface ガイド全体の再構成

対象: guides/source/active_record_querying.md
変更量が最も大きく、実質的に「全面的なリファイン」です。

主なポイント:

セクション構成・見出しの整理

  • 「Conditions」をより意味が明確なタイトル(例: 「Where 条件」「Filtering Results」系)に変更し、where によるフィルタリングであることを明示。
  • 「Ordering」を「Ordering Results」のような、結果セットに対する並び替えであることが直感的に分かる名称に変更。
  • 「Retrieving Objects from the Database」配下の構成を見直し:
    • 単一オブジェクト取得 (find, find_by, Dynamic Finders) と
    • 複数オブジェクト取得 (all, where, バッチ処理) をより分かりやすく整理。
    • Customer.all のような基本的な複数取得の例を明示的に示す、あるいはその旨を本文で説明。
  • 「Dynamic Finders」セクションを find_by の説明後、「Retrieving a Single Object」系の場所に移動し、関連する finder 群として連続して理解できるように変更。
  • 「Selecting Specific Fields」セクションから distinct を切り離し、limit/offset 周辺(セクション名を「Limiting Results」等に変更)と合わせて、結果セットの「件数・一意性の制御」という観点で再配置。
  • 「Overriding Conditions」は where だけでなく order など他の SQL 句の上書きも扱っているため、「Overriding Clauses」等に名称を見直し、カバー範囲に即したセクション名に。

API・関連ガイドへのリンク強化

  • 文中で触れているが別のガイドで詳説されるトピックへリンクを追加:
    • Composite Primary Keys ガイド(新規にリンク追加、詳細は 2-2 を参照)
    • トランザクションやロックに関する別ガイド(将来の「Active Model Transactions & Locking」)への導線を意識した記述。
  • order, default_scope, lock, with_lock, references など、説明されていない(または補足が必要な)メソッドへの API ドキュメントリンクを追加。

セキュリティと SQL リテラシーの補強

  • 「条件(string 条件)」での SQL インジェクション注意喚起の部分を整理し、Rails Security Guide の関連節へのリンクを追加。
  • 配列条件 (where(["name = ?", value]) など) の説明部でも、セキュリティの観点(プレースホルダを使う/使わないの差)を簡潔に整理し、同じく Security Guide を参照。
  • Active Record Basics ガイドにある「SQL の知識があると理解しやすい」という旨の注意書き・学習用リンクと同様のノートを、この Query Interface ガイドにも追加し、SQL 初学者がどこから学べばよいかを明示。

サンプルコード・表記の整合性と改善

  • ブール値の例で 0 / 1 を使っていた箇所を false / true に修正し、DB 実装に依存しない直感的な例に統一。
  • created_at を「日付」として扱っていた例のうち、GROUP BY を含むものは実際には timestamp 単位でグループされるため不正確になりうることから、statuscustomer_id など、日付変換ロジックに依存しないグルーピング例へ変更。
  • has_and_belongs_to_many の例に含まれる join_table オプションは、本来必須ではないが、実運用に近い「包括的な例」として現状維持(コメントにもその意図を明記)。
  • 日付レンジ条件(hash range 条件)で Date.yesterday.all_day などを使って簡潔に書く提案はあったが、
    • Date.yesterday.all_day がデフォルトで UTC 評価され、
    • config.time_zone がローカルと異なる場合に subtle なズレを生む ことを避けるため、Time.now.midnight あるいは Time.zone.now.midnight 系の例に留める判断を明文化(Timezone 混乱を避けるため、例はより明示的に)。

スコープ・メソッドチェーン・Fluent Interface の説明強化

  • out_of_print_and_expensive のような複合スコープ例について:
    • out_of_print.costs_more_than(500) のように既存スコープを再利用して組み立てる形も可能である点を踏まえつつ、
    • チェーン可能なスコープのデモとして利用していることを明示し、初期のシンプルな例からは外す整理。
  • 「scopes とクラスメソッドは似ているが、1つ重要な違いがある」という既存の説明に加え:
    • クラスメソッド内で self を返すと、スコープとほぼ同様にメソッドチェーンが行える例を追加。
  • 「The Active Record pattern implements Method Chaining」という文言の誤解を解消:
    • Active Record パターン自体と Method Chaining は無関係であり、ここで説明したいのはむしろ「Fluent Interface」であることを明確化する方向で表現を修正。
  • 「Understanding Method Chaining」節に NOTE を追加し、
    • Rails Console では inspect が暗黙に呼ばれるため、
    • メソッドチェーン結果が意図せずクエリ実行をトリガすることがある という挙動を明記(開発者が「いつ SQL が投げられるのか」を誤解しないようにする)。

Find / Create 系メソッドの拡充と注意点

  • 「Find or Build a New Object」セクションを拡充し、以下を含める:
    • find_or_create_by / find_or_create_by! の基本的な使い方・例。
    • 新しい create_or_find_by / create_or_find_by! の紹介と使いどころ。
    • find_or_create_by 系は DB レベルでアトミックではないため、競合状態(race condition)により同一レコードが二重作成される可能性がある Caveat を明示。
  • 上記に伴い、「Retrieving Objects from the Database」セクションから「finder」と呼べないもの(create_with など)はリストから除外し、概念上の一貫性を確保。

SQL 由来メソッドの説明整理

  • 「Finding by SQL」セクション内に混在していた内容を整理:
    • find_by_sql は独立した見出しを付けて説明し、戻り値や用途(任意の SQL 実行)を明確化。
    • 一方で、pluck, pick, ids など「カラムを直接配列で取得する」系は、クエリインターフェイスの中でも「Retrieving Columns」といったサブセクションにまとめ、find_by_sql とは別の振る舞い(Relation ではなく Ruby オブジェクト配列を返す等)を強調。

Eager Loading・EXPLAIN・パフォーマンス関連

  • includes セクション内で触れられている references に API リンクを付与。
  • eager_load のクエリ例が実挙動と乖離していた点を修正(別 PR #51940 で補完する旨のコメントあり)。
  • 一方で:
    • Eager Loading
    • EXPLAIN の実行 など「パフォーマンス」の文脈に強く依存するトピックは、将来的に「Active Record Performance ガイド」へ切り出す構想を明文化。
      ただしこの PR では実際の切り出しは行わず、別タスクで検討する前提に留めています。

ロッキング・トランザクション・Read-only

  • ロッキング (lock, with_lock) に API リンクを追加。
  • ロックやトランザクション、readonly 指定などはクエリインターフェースとはやや別軸の話題であるため、
    • 「Active Model Transactions & Locking」的な新ガイドへの切り出しを行う方針を記載。
    • ただし、実際の分離は今後の別タスクとして defer(この PR では現行ガイド内に残したまま)。

2-2. Composite Primary Keys ガイドの補強

対象: guides/source/active_record_composite_primary_keys.md

  • Query Interface ガイドから「複合主キー」の具体的な説明をなるべくこちらに集約するように再整理し、「クエリインターフェースガイドはこれ以上重くしない」方針。
  • Query Interface ガイドの中では、複合主キー関連の例はなるべく控えめにし、代わりにこのガイドへのリンクを設置。
  • 「composite」や「transaction」を検索したときに、必ず少なくともどこかにガイドがヒットするよう、用語とリンクを意識して追加。

2-3. QueryMethods のコメント/ドキュメント微修正

対象: activerecord/lib/active_record/relation/query_methods.rb (+8/-8)

変更は実装というよりドキュメント寄り:

  • only, except, reorder, lock などクエリビルダ的メソッドの Yard ドキュメントやコメント表現を、ガイドの説明と整合するよう修正。
    • only に対する except(逆操作)を説明に追加。
    • reorder は「default_scope を上書きする」だけでなく、「これまでに積み上げられた order 句全般を上書きする」ことを明示。
  • メソッドの役割がガイド内のサンプルと噛み合うよう、文言を統一。

  1. 影響範囲・注意点
  • 実装コードへの影響は query_methods.rb のコメントレベルで、挙動変更はありません。既存アプリの動作には影響しません。
  • 影響が大きいのは「ドキュメントの読み方・導線」です:
    • Query Interface ガイドの見出しやサンプル位置が変わっているため、古いバージョンのガイドを前提に「◯◯セクション参照」としていた社内資料・ブログ等は、リンク切れ・参照箇所ずれの可能性があります。
    • find_or_create_bycreate_or_find_by の関係、レースコンディションの caveat が明示されたことで、今後は「ユニーク制約付き + create_or_find_by の利用」など、より安全なパターンへ誘導されます。既存コードを見直す際の指針としても参照できる反面、今まで暗黙に許容していた非アトミックなパターンについて、チーム内の認識差が表面化する可能性があります。
    • Timezone と日付条件に関する例がより厳密になったため、これをきっかけに Time.zone.now 系を採用するかどうか、プロジェクトポリシーを確認するとよいです。
  • ロッキング・トランザクション・パフォーマンス(Eager Loading / EXPLAIN / async_*)は「別ガイドに分離する構想」の段階であり:
    • 近い将来、さらにドキュメントの構成変更(ガイドの分割)が発生する見込みがある点に注意してください。
    • 社内 wiki や学習資料を書き直す際は、「今後別ガイドに移りそうなトピック」としてモジュール化しておくと追従しやすくなります。

  1. 参考情報

#57414 Cache fixtures parsing

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

  1. 概要 (1-2文で)
    このPRは、テスト実行時のフィクスチャ(YAML)の「パース処理」をキャッシュすることで、use_transactional_tests = false なテストが混在するケースでの大幅なパフォーマンス低下を緩和する変更です。DBのリセットは従来どおり行いつつ、YAML ファイルの再解析を避けてフィクスチャ読み込みのオーバーヘッドを削減しています。

  1. 変更内容の詳細

背景と課題

  • use_transactional_tests = false のテストが走ると、
    「どのテーブル・レコードが変更されたかわからない」という前提から、
    全フィクスチャのリセットが必要になります。
  • 特に Megatest のように「テストメソッド単位」で実行・並列化される環境では、 非トランザクションテストとトランザクションテストが入り混じることで、 「フィクスチャキャッシュの破棄 → 再ロード」が頻発し、
    その際の YAML パースコストが全体の約半分を占めるほどのボトルネックになっていた、という説明です。

このPRは「DBのリセットは必要だが、YAML の再パースまでは不要」という点に着目して、
パース結果をメモ化して使い回せるようにする、という小規模な最適化を行っています。


主なコード上の変更点

(実際の差分は概略レベルでの説明になります)

1) フィクスチャファイル読み込み周り (fixture_set/file.rb)

  • ActiveRecord::FixtureSet::File あたりにある「YAML ファイルを読み込んでパースする処理」に対して、 パース済みデータをキャッシュする仕組みが導入されています。

イメージとしては:

ruby
# ざっくりしたイメージ(実装は異なる可能性あり)
class ActiveRecord::FixtureSet::File
  def rows
    @rows ||= parse_yaml_file
  end

  private

  def parse_yaml_file
    # 実際の YAML 読み込み・パース処理
  end
end

のように、一度パースした結果をインスタンス変数に保存しておき、
同じフィクスチャファイルに対しては何度も YAML パースしないようにしています。

2) フィクスチャ読み込みロジック (fixtures.rb)

  • ActiveRecord::FixtureSet がフィクスチャをロード/リセットする際の処理経路が少し整理され、

    • DB をリセットする部分
    • YAML をパースして Ruby オブジェクトにする部分

    が論理的に分けやすい形になっています。

  • これにより「DB を全部リセットしたいタイミングでも、YAML の再パースはスキップし、
    既にメモ化されたパース結果を使い回す」ことが可能になります。

内部的には、reset_cache 相当の処理から「YAML パース結果の破棄」が外され、
DB 側だけをクリアするような設計に寄せている形です。

3) テスト (fixtures_test.rb)

  • 新しいキャッシュ挙動を担保するためのテストが追加・修正されています。
    • フィクスチャのリセットを何度か繰り返しても、
      YAML のパース回数が増えない/期待どおりキャッシュが効いていること
    • 逆に、フィクスチャファイルそのものが変化したとき(ファイル更新など)は、 適切にキャッシュが無効化されること(あれば)

などを検証する内容になっていると考えられます。


  1. 影響範囲・注意点

影響範囲

  • 主に影響を受けるのは テスト実行時のフィクスチャ周りのパフォーマンスです。

  • 特に、

    • use_transactional_tests = false なテストを混在させているテストスイート
    • Megatest 等でテストメソッド単位で並列化・分割実行している環境

    では、フィクスチャリセットの回数が多くなるため、
    この変更による テスト時間の短縮が比較的大きくなることが期待できます。

挙動の互換性/注意点

  • アプリケーションの外部から見た API は基本的に変わらず、
    挙動としても「DB の状態」は従来どおりリセットされるため、 テスト結果の意味論的な互換性は維持されています。
  • 変更点は「YAML パースを毎回やり直さない」だけなので、 次のようなケースにだけ注意が必要です:
    • 「テスト実行中にフィクスチャの YAML ファイルを書き換える」ような変則的なワークフロー
      (通常はほぼ無いと思われます)
  • もしそのようなワークフローを取っている場合、
    同一プロセス内の後続テストから 古いパース結果が使われ続ける可能性があるため、 テストプロセスの再起動などで回避する必要があります。

将来的なリファクタリングの示唆

PR 説明にあるとおり、現状のフィクスチャコードは:

  • 「テーブルのリセット」という単純な目的のために
  • 「YAML パース → オブジェクト構築 → 挿入ロジック …」といった、 かなり複雑な処理を毎回行っている

という状態になっており、
理想的には「事前に計算した SQL のリストを流すだけ」にできるはずだ、という問題意識が示されています。

今回の変更はその一歩として「YAML パースの結果だけでもキャッシュする」もので、
今後さらに大きなリファクタリングが行われる可能性があります。


  1. 参考情報 (あれば)
  • 元 PR(今回の抜き出し元):
    https://github.com/rails/rails/pull/57326
  • 対象ファイル:
    • activerecord/lib/active_record/fixture_set/file.rb
    • activerecord/lib/active_record/fixtures.rb
    • activerecord/test/cases/fixtures_test.rb
  • 関連する設定:
    • ActiveSupport::TestCase.use_transactional_tests
    • Minitest / Megatest によるテスト実行戦略の違い(クラス単位 vs メソッド単位)

#57413 Optimize fixtures lookup pattern

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

  1. 概要 (1-2文で)
    Rails の ActiveRecord fixtures で、フィクスチャ名の検索(lookup)に使うパターンが最適化され、約 50% 高速かつコードも簡潔になるように変更されています。変更は 1 行のみですが、フィクスチャ読み込み周りのパフォーマンスに影響します。

  1. 変更内容の詳細

※ この PR は #57326 から切り出された一部で、activerecord/lib/active_record/fixtures.rb 内の「フィクスチャの検索用パターン」を 1 行差し替えています。

実際の差分は「旧パターン → 新パターン」の置き換えで、主に以下のような意図を持っています:

  • fixtures 内でレコードを参照するときに使う「名前 → レコード ID」の解決に使用されるパターン(おそらく正規表現か文字列検索ロジック)が、
    • マッチングの仕方を見直して不要な分岐・バックトラッキングや重いメソッド呼び出しを減らす
    • Ruby VM / Regexp エンジンにとって処理しやすい形に単純化する
      ことで高速化されている。
  • byroot 氏の PR でよくあるパターンですが、「動作仕様を変えずに実装をシンプルにして速くする」タイプの変更です。

典型的には次のような変更が行われている可能性があります(イメージ):

ruby
# 例: 以前
LOOKUP_PATTERN = /\A(.+)\((.+)\)\z/

# 例: 以後(アンカーやキャプチャの仕方を整理)
LOOKUP_PATTERN = /\A([^()]+)\(([^()]+)\)\z/

あるいは、複雑な gsub などを介したパターン構築から、よりストレートな正規表現・マッチングに簡略化しているケースです。
PR の説明からも、「約 50% 高速」「かつシンプル」 という点から、フィクスチャ名をパースする部分の正規表現やパターン構築ロジックが整理されただけで、外から見える API や挙動は変わっていないと考えられます。


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

    • ActiveRecord の fixtures を使っているテストコード全般。
    • 特に、フィクスチャのラベル参照・関連参照(posts(:one), comments(:one_for_post_one) など)に関わる「ラベル名の解釈・検索」部分の内部実装が対象です。
    • RSpec / Minitest など、Rails 標準 fixtures を利用しているテスト環境で恩恵があります。
  • 互換性

    • 作者コメント的にも「最適化」としか述べておらず、かつ 1 行差分のみであることから、既存のフィクスチャ名の書き方・解決ルールは変わらない前提です。
    • ただし、元のパターンが「ややゆるくマッチしていた」場合、新パターンで厳密になりすぎてしまうと、
      • かなり変な名前(特殊文字を多用したり、想定外のフォーマット)を使ったフィクスチャ
      • 非公式に依存していた挙動(バグ寄りの仕様) があれば、テストで解決できなくなる可能性はあります。
    • そのため、Rails をアップデートしたあとに「一部のフィクスチャだけ見つからない」「関連の解決に失敗する」といった問題があれば、フィクスチャ名の形式や、<table>(<label>) のような書き方をしていないかを確認するとよいです。
  • パフォーマンス

    • Rails 本体のテスト・ベンチマークにおいて、フィクスチャ lookup が約 50% 高速化しているということなので、大量のフィクスチャを使う大規模テストスイートでは実測でのテスト時間短縮が期待できます。

  1. 参考情報 (あれば)

#57405 Improve Date::DATE_FORMATS and Time::DATE_FORMAT deprecation

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

  1. 概要 (1-2文で)
    Rails 7.2系(?)で進んでいる Date::DATE_FORMATS / Time::DATE_FORMATS 廃止方針に関するフォローアップで、非推奨警告の出し方を整理し、新しい推奨APIを警告メッセージに含めるようにしつつ、コア拡張とフォーマット定義モジュールの関係を整理したPRです。実質的には「どう警告を出すか」「どこにフォーマットが定義されるか」の内部構造の変更であり、既存アプリへの影響は主に非推奨警告の出方にとどまります。

  1. 変更内容の詳細

2-1. ActiveSupport::Deprecation を使った警告出力

以前のPR (#57345) で Date::DATE_FORMATS / Time::DATE_FORMATS の利用が非推奨になりましたが、このPRでは:

  • ActiveSupport 独自の ActiveSupport::Deprecation(=いわゆる ActiveSupport deprecator)を使って警告を出すように変更
  • これにより:
    • Railsの標準的なやり方で deprecation が管理される
    • config.active_support.report_deprecations など既存の設定でまとめて制御できる
    • Backtrace フィルタなども一貫して扱える

以前は独自に warn を使っていた、あるいは ActiveSupport deprecator を十分に活用していなかった部分をリファクタして、標準のdeprecation仕組みに寄せた形です。

2-2. 警告メッセージに「新しいAPI」を明示

非推奨警告の文言が改善され、「代わりに何を使えばよいか」が明示されるようになりました。
おおよそ以下のようなメッセージになるイメージです(正確な文言はPR本文参照):

  • Date::DATE_FORMATS[:short] を使ったとき:
    → 「Date::DATE_FORMATS は非推奨です。代わりに ActiveSupport::DateFormats (または ActiveSupport::TimeWithZone のフォーマッタ) を使ってください」
  • 時刻系 (Time::DATE_FORMATS) も同様に、ActiveSupport::TimeFormats など新API(あるいは I18n.l / to_fs 系)の利用を促す内容に変更

これにより、deprecation を見た開発者が「何をすればいいか」まで即座に分かりやすくなります。

2-3. コア拡張とフォーマットモジュールの関係を逆転

ファイル構成上の意味で:

  • 以前は:
    • core_ext/date/conversions.rb / core_ext/time/conversions.rb でフォーマット定義もかなり握っていた
    • date_formats.rb / time_formats.rb は「追加的な定義」あるいは補助的な関係になっていた
  • 今回の変更後:
    • activesupport/lib/active_support/date_formats.rb
    • activesupport/lib/active_support/time_formats.rb
      のほうがフォーマット定義の「本体」となり
    • core_ext/date/conversions.rb / core_ext/time/conversions.rb はそれを利用する側(拡張を提供する側)として、依存関係が「逆転」した

つまり:

  • 「日付/時刻のフォーマット定義」そのものは ActiveSupport の専用モジュール (ActiveSupport::DateFormats / ActiveSupport::TimeFormats 相当) に集約
  • Date / Time のコア拡張はそのモジュールを利用して to_s / to_formatted_s などを提供

という、より分離された構造になったと考えられます。

2-4. 実装上の変更点イメージ

正確なコードは一部省略しますが、概念的には以下のような変化です。

旧: コア拡張が直接定数を管理

ruby
# activesupport/lib/active_support/core_ext/date/conversions.rb

class Date
  DATE_FORMATS = {
    short: "%e %b",
    # ...
  }

  def to_s(format = :default)
    # DATE_FORMATS を直接参照
  end
end

新: フォーマットモジュールに集約し、そこ経由で利用

ruby
# activesupport/lib/active_support/date_formats.rb

module ActiveSupport
  module DateFormats
    FORMATS = {
      short: "%e %b",
      # ...
    }

    # ここで deprecation 経由で Date::DATE_FORMATS をラップする等
  end
end

# activesupport/lib/active_support/core_ext/date/conversions.rb

class Date
  # include か委譲などの形で ActiveSupport::DateFormats を利用
  # Date::DATE_FORMATS を触るときはActiveSupport::Deprecation経由で警告
end

またテスト (date_ext_test.rb / time_ext_test.rb) も更新され、

  • 非推奨警告が ActiveSupport deprecator から出ること
  • 新しい構造で format が期待どおりに解決されること
    などが確認されるようになっています。

  1. 影響範囲・注意点
  • すでに #57345 の時点で Date::DATE_FORMATS / Time::DATE_FORMATS は非推奨になっているため、「初めて非推奨になった」わけではありません。
  • このPRによる主な実務的影響は:
    • deprecation メッセージが変わる(より具体的になる)
    • deprecation が ActiveSupport の仕組みに統一されるため、ActiveSupport::Deprecation を使ったサプレッションやログ出力制御がしやすくなる

アプリケーションコード側での注意点

  • まだ Date::DATE_FORMATS / Time::DATE_FORMATS に直接フォーマットを追加・参照している場合は、deprecation 警告がよりはっきりした形で出るようになります。
  • 推奨される置き換えはアプリの使い方次第ですが、典型的には:
    • 独自フォーマットは I18n.l と locale の date/time フォーマットで管理
    • もしくは to_fs(:your_format) と ActiveSupport のフォーマット API を利用
    • Rails が提供する標準フォーマットに依存しているだけなら、特に自前で DATE_FORMATS を触らないようにする

既存の to_s(:short) などは、内部構造が変わっても同じ結果になるよう維持されているため、即時に壊れる可能性は低いですが、Date::DATE_FORMATS / Time::DATE_FORMATS の直接操作は確実に廃止方向なので、警告を見つけたら早めに移行するのが無難です。


  1. 参考情報 (あれば)

#57345 Introduce ActiveSupport::TimeFormats and ActiveSupport::DateFormats for registering custom date formats

マージ日: 2026/5/20 | 作成者: @paracycle

  1. 概要 (1-2文で)
    Time#to_fs / Date#to_fs で使うカスタム日付フォーマットを、グローバル定数 Time::DATE_FORMATS / Date::DATE_FORMATS を直接いじらずに登録できる ActiveSupport::TimeFormats / ActiveSupport::DateFormats が導入されました。従来の定数ベースのカスタマイズは非推奨となり、次期Railsで削除予定です。

  1. 変更内容の詳細

新しいAPI: ActiveSupport::TimeFormats / ActiveSupport::DateFormats

to_fs 用のフォーマット登録方法が次のように変わります。

これから推奨される書き方

ruby
# Time 用
ActiveSupport::TimeFormats.register(:month_and_year, '%B %Y')

# Date 用(Proc も使える)
ActiveSupport::DateFormats.register(
  :short_ordinal,
  ->(date) { date.strftime("%B #{date.day.ordinalize}") }
)

Time.current.to_fs(:month_and_year) # => "February 2024"
Date.current.to_fs(:short_ordinal)  # => "February 21st"
  • register(name, format) でフォーマットを登録
    • formatstrftime 形式の文字列、または (time_or_date) -> string な Proc を想定
  • Time#to_fs(:format_name) / Date#to_fs(:format_name) から利用可能
  • フォーマットの実体は内部的に ActiveSupport::TimeFormats.list / ActiveSupport::DateFormats.list に保持される

これまでの書き方(非推奨)

ruby
Time::DATE_FORMATS[:month_and_year] = '%B %Y'
Date::DATE_FORMATS[:short_ordinal]  = ->(date) { ... }

Time.current.to_fs(:month_and_year)
Date.current.to_fs(:short_ordinal)
  • これらの定数は引き続き解決に使われますが「deprecated」と明示され、次の Rails メジャーバージョンで削除予定です。

互換性のための挙動

  • 初期状態では:
    • ActiveSupport::TimeFormats.listTime::DATE_FORMATS の内容から初期化
    • ActiveSupport::DateFormats.listDate::DATE_FORMATS の内容から初期化
    • 両方とも 凍結されておらず、従来どおり Time::DATE_FORMATS[:foo] = ... などで追加したものもリストに反映される
  • ただし 一度でも register が呼ばれたら:
    • そのタイミングで list 側を書き換え&freeze される
    • 以後は「新APIを使う前提」とみなされ、グローバル定数をいじることによる副作用の拡散を抑える設計

これによって、既存アプリはそのまま動かしつつ、新APIに opt-in したプロジェクトでは日付フォーマットのスコープと一貫性を強化できます。

関連する内部コードの変更

  • activesupport/lib/active_support/core_ext/{date,time,date_time}/conversions.rb
    • to_fs / to_formatted_s がフォーマットを解決する際に、
      1. ActiveSupport::TimeFormats.list / ActiveSupport::DateFormats.list
      2. (互換性のため)Time::DATE_FORMATS / Date::DATE_FORMATS
        を見るような形に調整されています(実装詳細はファイル差分に依存)。
  • ActiveRecord::Integration など、to_fs / 日付フォーマットに依存する部分のテストが新APIに沿うよう修正されています。
  • activesupport/CHANGELOG.md に変更点、非推奨事項として追記。

  1. 影響範囲・注意点

実装上の影響

  • 今すぐ壊れる変更ではありませんが、次のメジャーバージョンで Time::DATE_FORMATS / Date::DATE_FORMATS が削除予定である点が最重要です。
  • ライブラリやアプリケーションで以下のようなコードを書いている場合は、将来の互換性のために移行が必要です。
    • Time::DATE_FORMATS[:foo] = ...
    • Date::DATE_FORMATS[:bar] = ...
  • 新APIは ActiveSupport 依存なので、純粋な Ruby オブジェクトで Time / Date に手を入れていた gem は、Rails 環境ではできるだけこの API 経由に寄せる方が安全です。

マイグレーションの方針

アプリ側での推奨対応:

ruby
# config/initializers/time_formats.rb などで

# 旧:
# Time::DATE_FORMATS[:ymd_hms] = '%Y-%m-%d %H:%M:%S'
# Date::DATE_FORMATS[:ymd]     = '%Y-%m-%d'

# 新:
ActiveSupport::TimeFormats.register(:ymd_hms, '%Y-%m-%d %H:%M:%S')
ActiveSupport::DateFormats.register(:ymd, '%Y-%m-%d')

gem / engine の場合:

  • Time::DATE_FORMATS / Date::DATE_FORMATS を直接触るのはやめ、
    ActiveSupport::TimeFormats.register / ActiveSupport::DateFormats.register を使うように変更
  • Rails の該当バージョン未満をサポートする必要がある場合は、
    respond_to?(:register) なら新 API、そうでなければ従来の定数」を使う二段構えも検討できます。
ruby
if defined?(ActiveSupport::TimeFormats) && ActiveSupport::TimeFormats.respond_to?(:register)
  ActiveSupport::TimeFormats.register(:foo, '%H:%M')
else
  Time::DATE_FORMATS[:foo] = '%H:%M'
end

ライブラリ間の衝突回避

  • グローバルな Time::DATE_FORMATS / Date::DATE_FORMATS に勝手に追加する設計は、複数の gem が同じキーを使ったときに衝突しやすい問題がありました。
  • 今回の変更で「フォーマット管理を ActiveSupport 名義に閉じる」方向に寄せることで、衝突の起点を Rails 側に集約しつつ、将来的にはより厳密な管理(例えばキー競合時の警告など)もしやすくなります。

  1. 参考情報 (あれば)

#53636 fix referential_integrity for postgres partitioned tables

マージ日: 2026/5/20 | 作成者: @aandrieu

  1. 概要 (1-2文で)
    PostgreSQL のパーティショニングされたテーブルに対して ActiveRecord::FixtureSet.create_fixtures などを利用した際、外部キー制約が意図せず NOT VALID になる不具合を修正する PR です。referential_integrity 実装内で行っていた制約の無効化 SQL を、対象テーブルにスコープするように変更しています。

  1. 変更内容の詳細

問題の背景

ActiveRecord::FixtureSet.create_fixtures は、テストデータ投入時などに一時的に外部キー制約を無効化・再検証するため、内部的に ActiveRecord::Base.connection.disable_referential_integrity を利用します。

PostgreSQL アダプタでは、この処理の一部として ActiveRecord::ConnectionAdapters::PostgreSQL::ReferentialIntegrity#all_foreign_keys_valid! が呼ばれ、pg_catalog.pg_constraint を直接更新して以下のようなことを行っていました:

  1. convalidated = false にして制約を「未検証 (NOT VALID)」状態にする
  2. ALTER TABLE ... VALIDATE CONSTRAINT ... で再検証する

しかし、PostgreSQL の パーティショニングされたテーブル では、次のような状況が起こります。

  • 親テーブルとそのパーティションに、同名の外部キー制約 が存在しうる
  • これまでの実装では UPDATE pg_catalog.pg_constraint ... がテーブルにスコープされておらず、同名制約を持つ全パーティションの制約をまとめて NOT VALID にしてしまう
  • その後の VALIDATE CONSTRAINT は「1テーブル分」しか行わないため、その他のパーティションに対する制約が NOT VALID のまま残る

その結果、rails db:migrate やスキーマのダンプ (structure.sql) を実行すると、ほとんどすべてのパーティションテーブル上の外部キー制約に NOT VALID が付与された差分が出続ける、という問題が発生していました。

具体的な変更

変更ファイル:

  • activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb (+1/-1)
  • activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb (+33/-0)

本質的な変更は 1 行で、

ruby
# 変更前(イメージ)
UPDATE pg_catalog.pg_constraint
  SET convalidated = false
WHERE conname = #{quote(constraint_name)}

# 変更後(イメージ)
UPDATE pg_catalog.pg_constraint
  SET convalidated = false
WHERE conname = #{quote(constraint_name)}
  AND conrelid = #{quote(table_oid)}

のように、UPDATE pg_catalog.pg_constraint対象テーブルの OID (conrelid) でスコープする 形に修正しています。

これにより:

  • あるテーブル parent_table の外部キー制約 fk_parent_foo を再検証したい場合
    conname = 'fk_parent_foo' かつ conrelid = 'parent_table' の OID の行だけが NOT VALID になる
  • 同名の制約 fk_parent_foo を持つ別パーティション parent_table_2024_01 などは影響を受けない

という挙動になり、パーティション全体へ「巻き込み」無効化される問題を防ぎます。

テスト

activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb に、PostgreSQL のパーティションテーブルと外部キー制約を用いたテストが追加されています。内容としては概ね:

  • パーティション構造を持つテーブルと外部キー制約を作成
  • disable_referential_integrityall_foreign_keys_valid! 相当の処理を実行
  • パーティション側の制約が NOT VALID のまま残らないことを検証

というシナリオで、今回のバグ再現と修正確認を行っています。


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

    • PostgreSQL を使用している Rails アプリケーション
    • 特に、パーティションテーブル を使用しつつ
    • fixtures / ActiveRecord::FixtureSet.create_fixtures / disable_referential_integrity といった機能を利用しているプロジェクト
  • 期待される改善:

    • rails db:migrate 後に structure.sql を見ると、パーティションテーブル上の外部キーに NOT VALID が大量に付加される差分が出続ける問題が解消される
    • 実運用中の DB でも、テストやバッチ処理で disable_referential_integrity を利用しても、関係ないパーティションの外部キー制約が勝手に NOT VALID にならない
  • 注意点:

    • 既に「壊れた状態」で NOT VALID になっている制約は、この PR で自動的には直りません。必要に応じて、手動で ALTER TABLE ... VALIDATE CONSTRAINT ...; を実行する必要があります。
    • 変更は pg_constraintUPDATE にフィルタ条件を足しているだけなので、パーティションを使っていない環境に対して挙動の後退はほぼありません(むしろ限定的になった)。

  1. 参考情報 (あれば)
  • 対応クラス:
    ActiveRecord::ConnectionAdapters::PostgreSQL::ReferentialIntegrity
    disable_referential_integrity / all_foreign_keys_valid! の Postgres 実装部分

  • 関連挙動:

    • PostgreSQL ドキュメント: 外部キー制約と NOT VALID / VALIDATE CONSTRAINT
      https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-VALIDATE-CONSTRAINT
    • パーティションテーブルと制約の扱い
      親テーブルと各パーティションは、それぞれ pg_constraint 上では独立したエントリを持ち、conrelid によってどのテーブルの制約かが区別される。
      今回はこの conrelid を利用して「どのテーブルの制約を無効化するか」を限定する形に修正している。

#57367 Prevent development welcome route from duplicating on route reload

マージ日: 2026/5/20 | 作成者: @curi

  1. 概要 (1-2文で)
    開発環境で reload_routes! などによりルーティングを再読み込みすると、Rails が内部的に用意している「開発用ウェルカムページ」の GET / ルートが毎回追加され、重複してしまう問題を解消する PR です。アプリ側で定義した root ルートの優先順位はこれまでどおり維持しつつ、内部ウェルカムルートの登録を「アプリ起動時の一度だけ」に制限しています。

  1. 変更内容の詳細

問題の背景

  • Rails は開発環境で config/routes.rb に root が定義されていない場合、GET / に内部の「Welcome」ページを表示するルートを自動的に追加しています。
  • この内部ルートは routes_reloader.run_after_load_paths 内で app.routes.append にブロックを渡す形で登録されていました。
  • RouteSet#append は「渡されたブロックを永続的に保持する」一方で、「ルートの再読み込み時には実体化されたルートだけがクリアされる」という挙動を持ちます。
  • そのため、reload_routes! が呼ばれるたびに append が積み増しされ、結果として内部の GET / ルートが何度も追加される、というバグが発生していました。

対応方針

  • 「内部ウェルカムルートを登録する append ブロック」をアプリのブート時に一度だけ登録するように変更。
  • ルートの並び順は従来どおりに維持し、アプリ側が定義した root ルートが内部ウェルカムルートよりも優先される挙動はそのまま残す。

実際の変更ポイント

※ 実際のコード全文は省略し、要点のみ説明します。

1. routes_reloader に「一度だけ追加する」仕組みを導入

railties/lib/rails/application/routes_reloader.rb

  • 内部的に「開発用ウェルカムルート append ブロックを既に登録したかどうか」を判定するフラグ/条件を追加。
  • run_after_load_paths が呼ばれるたびに無条件で app.routes.append していた処理を、「まだ登録していない場合のみ append する」ように変更。

イメージとしては:

ruby
# 疑似コードイメージ
unless @welcome_route_registered
  app.routes.append do
    # 内部ウェルカムルートの定義
  end
  @welcome_route_registered = true
end

のような形で、一度だけ append されるようになっています。

2. finisher の微調整

railties/lib/rails/application/finisher.rb

  • run_after_load_paths の呼び出しやそのフックの扱いに合わせて 1 行レベルの修正が入っています。
  • 目的はあくまで「内部ルートの append ブロックが多重に登録されない」ようにすることで、通常のルート再読み込みフロー自体は保持されています。

3. テスト追加

railties/test/application/routing_test.rb

  • 以下のような観点のテストが追加されています:
    • 「開発用ウェルカムルートがルート再読み込みによって重複しないこと」
    • 「アプリケーション側で root を定義した場合、そのルートが内部ウェルカムルートより優先されること(append による順序性が保たれていること)」

テスト名の一部:

  • /not duplicated/ にマッチするテスト: 開発用 root の重複が起きないことを検証。
  • /appended root/ にマッチするテスト: アプリ側の root が append 後のルート順に沿って優先されることを検証。

4. CHANGELOG の追記

railties/CHANGELOG.md

  • この挙動修正についての短いエントリが追加され、開発環境でのルーティング動作の変更点が明示されています。

  1. 影響範囲・注意点
  • 対象:
    • 主に development 環境reload_routes! やコードリロード後のルート再読み込みに関わる挙動。
    • config/routes.rb に root が定義されていない状態で開発用ウェルカムページが使われるケース。
  • 期待される挙動の変化:
    • 複数回のルート再読み込みをしても、GET / の内部ウェルカムルートが 1 つだけに保たれるようになります。
    • もし以前、rails routes 等で root が複数行出ていた場合、それが解消されます。
  • 従来から変わらない点:
    • アプリケーション側で root "some#controller" を定義すれば、それが 最優先 され、内部ウェルカムルートはマッチしません。
    • ルート定義の順序性(append で追加されるルートが後ろに来る)自体は維持されています。
  • 互換性:
    • 重複していた root ルートに依存しているアプリは通常想定されないため、実質的にはバグ修正であり、互換性上のリスクは低いと考えられます。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57367
  • 関連するクラス・モジュール:
    • Rails::Application::RoutesReloader
    • ActionDispatch::Routing::RouteSet#append
  • テスト実行例(PR 内で言及されているもの):
    • cd railties && bin/test test/application/routing_test.rb -i '/not duplicated|appended root/'
    • cd railties && bin/test test/application/routing_test.rb

#57340 Add ActionController::Parameters#deep_transform_values

マージ日: 2026/5/20 | 作成者: @Edilbek

  1. 概要 (1-2文で)
    ActionController::Parametersdeep_transform_values / deep_transform_values! が追加され、Strong Parameters の「許可状態(permitted?)」を保ったままネストした値を再帰的に変換できるようになりました。これにより、これまで必要だった to_unsafe_h.deep_transform_values によるガードレールの迂回が不要になります。

  1. 変更内容の詳細

追加されたメソッド

ActionController::Parameters に以下が追加されています。

  • #deep_transform_values(&block)
    自身をコピーして、すべてのネストした値に対してブロックを再帰的に適用し、新しい ActionController::Parameters を返します。
  • #deep_transform_values!(&block)
    自身を破壊的に書き換えます。戻り値は変換後の selfActionController::Parameters)。

いずれも Active Support の Hash#deep_transform_values とインターフェース・挙動を揃えつつ、戻り値を ActionController::Parameters のままに保ち、permitted? の状態も維持します。

動作イメージ

PR本文の例を少し噛み砕くと:

ruby
params = ActionController::Parameters.new(
  user: {
    email: "  ALICE@EXAMPLE.COM  ",
    profile: { bio: "  Hello world  " }
  }
)

# 非破壊版: 新しい Parameters を返す
normalized = params.deep_transform_values { |v|
  v.is_a?(String) ? v.strip.downcase : v
}
# => ActionController::Parameters オブジェクトを維持
#    user, profile もすべて Parameters としてネストされたまま

# 破壊版: 自身を書き換える
params.deep_transform_values! { |v|
  v.is_a?(String) ? v.strip : v
}

# permitted? 状態の引き継ぎ
params.permit!
params.permitted?                               # => true
params.deep_transform_values { |v| v }.permitted? # => true

ここで重要なのは以下の点です:

  • ネストしたハッシュ部分も ActionController::Parameters のまま再構築される
  • permit! を呼んだ後に deep_transform_values しても、結果の Parameterspermitted? == true のまま
  • Hash#deep_transform_values と同様、「値」に対してのみブロックが適用され、キーは変更されない

これにより、典型的なユースケースである:

  • 入力値の前後空白除去
  • 文字列の正規化(小文字化、全角/半角変換など)
  • boolean/number へのキャスト
  • nil/空文字の統一処理

といった「バリデーション前の一括整形」を、Strong Parameters を崩さずに行えます。

実装のポイント(推測含む)

コード上の詳細は以下のような方針になっています(テスト/慣例から読み取れる範囲):

  • 既存の Parameters#deep_transform_keys 実装パターンに倣っている
  • 再帰的に Parameters / Hash / Array を辿り、最終的なスカラー値にブロックを適用
  • 返却時に ActionController::Parameters.new(hash_like) でラップしつつ、元の permitted? フラグをコピー
  • bang 版は @parameters の中身を書き換える形で実装されている

  1. 影響範囲・注意点

影響範囲

  • Rails コントローラで Strong Parameters を使っているアプリケーション全般が、より安全に値変換ロジックを組み込みやすくなります。
  • これまで以下のようなコードを書いていたケースを、deep_transform_values に置き換え可能です:
ruby
# 旧: Strong Parameters をいったん Hash にしてしまう
normalized = params.to_unsafe_h.deep_transform_values { ... }

# 新: Strong Parameters を維持したまま変換
normalized = params.deep_transform_values { ... }
  • Strong Parameters の「許可/非許可」判定ロジックには変更はなく、その状態を保ったまま値変換を追加で行えるだけなので、既存コードに対して互換性上の破壊的変更は基本的にありません。

注意点 / 使い方上の留意

  • 変換タイミング:
    • 多くの場合は、permit の前に正規化を行うと、キー・構造に依存しない変換処理を簡潔に書けます。
    • 一方で、「許可された値だけを変換したい」場合は permit 後に呼びます。
  • 破壊的メソッドの利用:
    • deep_transform_values! はインスタンスを書き換えるので、後続で元の値が必要な場合は非破壊版を使うか dup してから呼び出してください。
  • Strong Parameters の性質は変わらない:
    • deep_transform_values 自体は「何を permit するか」を決めるわけではなく、あくまで「値の変形」だけを行います。
    • 変換後も require / permit の書き方は従来どおりで、キー名はそのままです(キー変換は deep_transform_keys を使う)。
  • ブロックの責務:
    • 文字列・配列・数値など、すべての「値」にブロックが渡されるため、型チェック (is_a?) をきちんと行わないと意図しない変換が走る可能性があります。
    • 特に boolean や nil を扱う際は、String のみを対象にするなどの条件分岐が推奨されます。

  1. 参考情報 (あれば)
  • 類似 API:
    • Hash#deep_transform_values (Active Support)
    • ActionController::Parameters#deep_transform_keys / #deep_transform_keys!
  • 想定ユースケース:
    • 入力フォーム全体のホワイトスペース除去、メールアドレスの downcase
    • API 受信パラメータの型・形式の一括正規化(ただしビジネスロジック的な変換はモデル/サービス層に寄せるのが望ましい)
  • 実際に導入する際のパターン例:
ruby
class UsersController < ApplicationController
  def create
    normalized = user_params.deep_transform_values { |v|
      v.is_a?(String) ? v.strip : v
    }

    @user = User.new(normalized)
    ...
  end

  private

  def user_params
    params.require(:user).permit(:email, :name, profile: [:bio])
  end
end

このように、Strong Parameters の枠組みを崩さずに「入力前処理」を行えるようになった、というのがこの PR の本質的な改善点です。


#57399 doc: escape "API" for strong_parameters doc

マージ日: 2026/5/20 | 作成者: @okuramasafumi

  1. 概要 (1-2文で)
    strong_parameters のドキュメント中で、「API」という単語が誤ってリンク扱いになっていた問題を、Markdown エスケープによって修正した PR です。コードの挙動や API 仕様には一切変更はなく、純粋なドキュメント修正です。

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

  • 対象ファイル: actionpack/lib/action_controller/metal/strong_parameters.rb
  • strong_parameters のクラスコメント(YARD/rdoc用のドキュメントコメント)内にある「API」という単語が、そのまま書かれていることで自動的にリンクとして解釈されていました。
  • この PR では、その「API」をバックスラッシュ(\)でエスケープして、ドキュメント生成時にリンクにならないようにしています。

イメージとしては、ドキュメントコメント中の:

rdoc
API

のような箇所が、以下のように変更された形です:

rdoc
\API

これにより、rdoc/Markdown のパーサが「API」をリンク候補として扱わず、単なるテキストとして表示します。

  1. 影響範囲・注意点
  • 影響範囲:
    • Rails の公式ドキュメント(strong_parameters の API ドキュメント)の見た目のみが変わります。
    • 生成される HTML ドキュメント上で、「API」という文字列がリンクではなくプレーンテキストとして表示されるようになります。
  • 注意点:
    • 実行時の挙動、パブリック API、型、メソッドのシグネチャ等には一切変更がありません。
    • テスト追加のチェックボックスにはチェックが入っていますが、この変更自体はドキュメントのみのため、特にテストは本質的には不要な種類の修正です。
    • CHANGELOG の更新も不要な範囲(ドキュメントのみの微修正)として扱われています。
  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57399
  • strong_parameters ドキュメントは「どのパラメータを許可するか」の説明で “API” という語を頻繁に使うため、余計な自動リンクを防ぐのは可読性向上・誤解回避のために有効です。

#57368 Limit the size of strings we call to_i on in ActiveRecord

マージ日: 2026/5/20 | 作成者: @tenderlove

  1. 概要 (1-2文で)
    このPRは、ActiveRecord/ActiveModel が文字列を自動的に整数へ変換するときに、to_i をかける文字列長を制限し、超長い文字列による DoS の可能性を下げる変更です。カラムのバイトサイズに基づき、先頭 _limit * 4 バイトだけを整数化の対象にします。

  1. 変更内容の詳細

何をしているか

ActiveModel の整数型キャスト(ActiveModel::Type::Integer)で、文字列から整数への変換時に以下の制約を導入しました。

  • 対象が文字列のとき、
    「文字列の先頭 _limit * 4 バイト」だけを切り出して to_i する
  • ここで _limit は「カラムが DB 上で使うバイト数」
    • 4バイト integer_limit = 4 → 先頭 16バイトを使用
    • 8バイト bigint_limit = 8 → 先頭 32バイトを使用
    • limit: 1 の tinyint 的カラム → _limit = 1 → 先頭 4バイトを使用

Ruby の String#to_i は文字列長に比例して時間が伸びるため、極端に長い文字列を渡されると DoS の温床になります。この変更で、内部で勝手にやる「自動整数化」のコストを上限付きにしています。

なぜ _limit * 4 なのか

  • 表現可能な桁数に対して十分な余裕がある:
    • 4バイト int: 最大 10 桁程度
    • 8バイト bigint: 最大 19 桁程度
  • _limit * 4 だと:
    • 4バイト int → 16バイト → 10桁 + 符号 + ちょっとした suffix を入れても足りる
    • 8バイト bigint → 32バイト → 19桁 + 符号 + slug っぽい suffix も許容

当初は「DB カラムの幅」ベースで厳密に制限しようとしたが、以下のような「slug の自動整数化」ケースが壊れるため断念し、4倍のマージンを取った形です。

ruby
# URL: /posts/123-hello-world
Post.where(id: params[:id]).first
# params[:id] == "123-hello-world" だが、id カラムは integer/bigint
# ActiveRecord は先頭の整数部分だけを見て、id: 123 として検索する

slug 全体を入れるとカラムの桁幅は超えるが、"123-hello-world" のような短い slug は _limit * 4 の範囲に収まりやすく、これまで通り動作します。

コードイメージ

※ 実際のコードはもう少し細かいですが、概念的にはこんな処理が ActiveModel::Type::Integer に入ります:

ruby
def cast_value(value)
  case value
  when String
    limit_bytes = @limit&.*(4) # カラムの limit(バイト) * 4
    if limit_bytes && value.bytesize > limit_bytes
      value = value.byteslice(0, limit_bytes)
    end
    value.to_i
  else
    value.to_i
  end
end

説明文にあった example:

ruby
ActiveRecord::Base.connection.create_table :users do |t|
  t.integer :age, limit: 1
  t.timestamps
end

class User < ActiveRecord::Base; end

User.create(age: '9' * 5_000_000)
  • 変更前: 500万文字すべてを to_i が走査
  • 変更後: limit: 1_limit = 1_limit * 4 = 4 バイトだけを見て to_i
    age キャストに要する時間が、一定上限までに抑えられる

テスト・ドキュメント

  • activemodel/CHANGELOG.md
    • この振る舞い変更が追加されている旨を記載
  • activemodel/test/cases/type/integer_test.rb
    • 長大な文字列をキャストした場合に、先頭部分のみ考慮されることをテスト

  1. 影響範囲・注意点

影響を受けるケース

  • ActiveRecord / ActiveModel が内部で行う「文字列 → integer 変換」全般
    • モデルの integer/bigint カラムへの代入
    • コントローラで where(id: params[:id]) 等をした時の ID キャスト
    • フォーム送信で数値カラムに長い文字列を送ってきた場合

破壊的になりうる挙動の変化

  • 極端に長い数値文字列を渡したとき、末尾部分は無視されるようになります。
    • 例: id: "1234567890" * 100000 のような入力
      → 先頭 _limit * 4 バイトだけを見て to_i
  • ただし、実際には DB 側の整数カラムに収まらない値は、これまでもオーバーフロー・バリデーションエラー・DB エラーなどが発生していたため、「正しく扱えていた巨大入力」が壊れるケースは実用上ほぼないと考えられます。

セキュリティ・パフォーマンス上の意味

  • 非常に長い文字列を to_i に渡すことで CPU 時間を消費させる DoS のリスクが軽減されます。
  • Rails アプリ側で:
    • Strong Parameters やフォームオブジェクトでの length validation
    • Rack ミドルウェアでのボディサイズやパラメータ長の制限
      などと組み合わせることで、より広範な防御が可能です。
      ただし、この変更だけで「任意の長さの入力による全 DoS」を防げるわけではありません。

注意点 / 既存コードで意識すべきこと

  • 仕様として「長過ぎる文字列を渡した場合、Rails は先頭 _limit * 4 バイトだけで to_i する」ことを知っておく必要があります。
  • 「文字列の末尾まで含めて意味を持つ形式(特殊なエンコード値など)を integer カラムにそのまま突っ込んで、to_i に頼っている」ような設計がある場合、誤変換に気づきにくくなる可能性があるため、そもそもスキーマ設計やキャスト方法を見直すべきです。
  • DoS 対策としては有効ですが、ユーザ入力に対する明示的な length validation は依然として推奨されます。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57368
  • 関連クラス: ActiveModel::Type::Integeractivemodel/lib/active_model/type/integer.rb
  • 参考: Ruby String#to_i の計算量は文字列長に比例するため、無制限な長さの入力はパフォーマンスリスクとなる。

#57376 Fix non-working time-only example in ActiveModel::Type::DateTime doc [ci skip]

マージ日: 2026/5/20 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveModel の :datetime 型のドキュメントに「時刻だけの文字列も受け付ける」という誤った記述とサンプルがあったため、それを削除し、時刻のみを扱いたい場合は :time 型を使うよう誘導する修正です。実装には変更がなく、ドキュメントのみの修正です。

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

何が問題だったか

ActiveModel::Type::DateTime の RDoc に、次のような趣旨の記述がありました(要約):

  • :datetime 型は「部分的な時刻のみのフォーマット」も受け付ける
  • 例: event.start = "06:07:08+09:00"Time オブジェクトが返る

しかし実際の挙動は異なり、"06:07:08+09:00" のような 日付を含まない時刻のみの文字列 を与えると nil が返ります。
内部的には Date._parse で文字列を解析していますが、時刻のみだと :year が含まれないパース結果になり、new_timenil を返すためです。

ruby
# 実際の挙動イメージ
type = ActiveModel::Type::DateTime.new

type.cast("2024-01-01 06:07:08+09:00")
# => 2024-01-01 06:07:08 +0900 (Time/ActiveSupport::TimeWithZone など)

type.cast("06:07:08+09:00")
# => nil  # ドキュメントの記述と食い違っていた

この説明と例は、ActiveModel::Type::Time のドキュメントからほぼコピーされたもので、Time 型のほうでは「時刻のみのパース」が正しくサポートされています。

具体的な修正内容

  • ActiveModel::Type::DateTime の RDoc から:
    • 「部分的な time-only フォーマットを受け付ける」といった説明文を削除
    • event.start = "06:07:08+09:00" のサンプルを削除
  • 代わりに:
    • 時刻のみの値を扱いたい場合は :time 型 (ActiveModel::Type::Time) を使うように誘導する文言を追加

コード上のロジックや挙動には一切変更がなく、activemodel/lib/active_model/type/date_time.rb のコメント(RDoc)のみが 2 行追加・5 行削除で修正されています。


  1. 影響範囲・注意点
  • 挙動の変更は一切なし
    既存の :datetime 型のキャスト挙動はこれまでどおりで、時刻だけの文字列は引き続き nil になります。

  • ドキュメントを信じてコードを書いていた場合の実害が可視化された
    以前のドキュメントを見て次のように書いていた場合:

    ruby
    attribute :start_at, :datetime
    
    # 「時刻だけもいけるはず」と思っている例
    model.start_at = "06:07:08+09:00"
    # 期待: その日の 06:07:08+09:00 の Time
    # 実際: nil

    この違いはドキュメントの誤りが原因で、コードのバグではありません。
    「日付を省略した時刻のみの文字列」を扱いたい場合は、次のように :time 型を使う必要があります。

    ruby
    attribute :start_at, :time
    
    model.start_at = "06:07:08+09:00"
    # => 有効な Time オブジェクトとしてパースされる(Time 型)
  • 今後の設計の指針

    • 「日付+時刻」の値 → :datetime
    • 「時刻のみ」の値 → :time
      と役割を明確に分けるべきであり、:datetime に「日付なしの時刻文字列」を渡しても補完はされない、という点を前提にコードを書く必要があります。

  1. 参考情報 (あれば)
  • 該当ファイル:
    activemodel/lib/active_model/type/date_time.rb(RDoc のみ変更)
  • 関連する型:
    • ActiveModel::Type::DateTime
    • ActiveModel::Type::Time(time-only パースがサポートされている)
  • 内部的なパース挙動の背景:
    • Ruby 標準の Date._parse は時刻のみの文字列を {:hour, :min, :sec} などで返すが :year を含まない
    • ActiveModel::Type::DateTime 側の new_time:year がないケースをカバーしておらず nil を返すため、結果として time-only 文字列は nil になる

#50862 Accept Tempfile as ActiveStorage attachable

マージ日: 2026/5/20 | 作成者: @shouichi

  1. 概要 (1-2文で)
    Active Storage の「添付可能オブジェクト(attachable)」として Tempfile が正式にサポートされました。これにより、File と同様に Tempfile をそのまま has_one_attached / has_many_attached に渡して添付できるようになります。

  1. 変更内容の詳細

2-1. 変更の主旨

  • もともと Active Storage では File オブジェクトを attachable として扱えましたが、同様の用途でよく使われる Tempfile はサポートされていませんでした。
  • この PR によって、TempfileFile と同様に扱えるようになり、例えば一時ファイルを生成してからそのまま添付する、といったワークフローが自然に書けるようになります。

2-2. コード上の変更点

activestorage/lib/active_storage/attached/changes/create_one.rb

CreateOne クラスは has_one_attached の変更を扱う内部クラスで、「渡されたオブジェクトを attachable としてどう扱うか」を判定する役割があります。

この PR では、ここでの型判定が修正され、「File だけでなく Tempfile も attachable として扱う」ように変更されています。

イメージとしては、以下のようなロジックになっていると考えられます(実際のコードを簡略化した擬似例):

ruby
def change
  case attachable
  when ActionDispatch::Http::UploadedFile, Rack::Test::UploadedFile
    # 既存: アップロード済みファイル
  when File, Tempfile
    # 今回: File に加えて Tempfile もここで扱う
  when Hash
    # 既存: IO として扱うなど
  else
    # 既存: ActiveStorage::Blob や Signed ID など
  end
end

具体的な差分としては、File 単体で判定していた箇所が FileTempfile の両方を許容するような条件に書き換えられています(is_a?(File) から is_a?(File) || is_a?(Tempfile)、または配列での when File, Tempfile など)。

テスト追加: activestorage/test/models/attached/one_test.rb

has_one_attached に対して Tempfile を渡した時に、問題なく Blob が作られ添付されることを確認するテストが追加されています。イメージとしては以下のようなテストです:

ruby
test "attaching Tempfile" do
  record = User.create! # has_one_attached :avatar など

  Tempfile.create(["avatar", ".png"]) do |tempfile|
    tempfile.write("dummy")
    tempfile.rewind

    record.avatar.attach(tempfile)
  end

  assert record.avatar.attached?
end

CHANGELOG 追記: activestorage/CHANGELOG.md

  • Active Storage の変更履歴に、Tempfile が attachable としてサポートされたことが追記されています。
  • 次のリリースに含まれる機能としてドキュメント化された形です。

  1. 影響範囲・注意点

3-1. 想定される利用パターン

これまで、Tempfile を Active Storage に添付するために以下のようなワークアラウンドを書いていた場合:

ruby
file = Tempfile.new(["avatar", ".png"])
# ... 書き込み処理 ...
file.rewind

# 以前は明示的に IO として扱ったり、UploadedFile に包むなどしていたケース
user.avatar.attach(
  io: file,
  filename: "avatar.png",
  content_type: "image/png"
)

今後は次のように、File と同様にそのまま渡せるようになります:

ruby
Tempfile.create(["avatar", ".png"]) do |file|
  file.write("dummy")
  file.rewind

  user.avatar.attach(file)
end

あるいは、Web API クライアントやバッチ処理で一時ファイルを作って添付する、といったケースでもシンプルになります。

3-2. 互換性・破壊的変更の有無

  • 既存の File attachable の挙動は変えていないため、後方互換性の上での破壊的変更はほぼありません。
  • TempfileFile を継承している環境がほとんどですが、実装によっては異なる型として扱われている可能性もあるため、それを Active Storage 側で明示的に許可した形になります。
  • attachable の型判定分岐に Tempfile が追加されたのみで、既存のインターフェイス (attach, attachables) に変更はありません。

3-3. 運用上の注意 (Tempfile のライフサイクル)

  • Tempfile は Ruby の GC によって自動的にクローズ・削除されるとはいえ、ブロック形式で使うことが推奨されます:
    ruby
    Tempfile.create(["data", ".csv"]) do |tempfile|
      # Active Storage への attach はこのブロック内で完結させる
      model.file.attach(tempfile)
    end
  • Active Storage は添付時にファイル内容をサービス(S3 など)へアップロードするため、attach 完了後に Tempfile が削除されても問題ありません。ただし、非同期ジョブ側でファイルシステムに依存しているような独自処理がある場合は挙動を確認した方が安全です。

  1. 参考情報 (あれば)

#57393 Load image processing backend upfront

マージ日: 2026/5/20 | 作成者: @janko

  1. 概要 (1-2文で)
    このPRは、Active Storage で初回の画像バリアント生成時に発生していた「画像処理バックエンドのロード遅延」をなくすため、ImageMagick / vips のバックエンドをあらかじめ require しておくようにした変更です。これにより、初回アクセスのレイテンシ低減と、prefork 型アプリサーバにおけるメモリ共有効率の向上が期待できます。

  1. 変更内容の詳細

背景

  • Active Storage の画像変換は image_processing gem を通して ImageMagick もしくは vips を利用します。
  • image_processing は使用されるまでバックエンド(image_processing/mini_magickimage_processing/vips)を autoload する仕組みですが、
    • デプロイ直後の「最初のバリアント生成時」にバックエンド読み込みコスト(約 50–100ms + 追加メモリ)が発生する
    • prefork 型サーバ(Puma clustered / Unicorn, Passenger など)では、フォーク後にロードするとコピーオンライトが効きにくくなり、各ワーカーでメモリが二重・三重に確保される
  • 特に image_processing/vips のロードは、image_processing 単体に比べて 約 19MB の追加メモリ を消費するとの測定結果が示されています。

実際の変更点

このPRでは、Active Storage の各トランスフォーマークラスが初期化時にバックエンドを明示的に require するようにしています。

対象ファイルは次の2つです。

  • activestorage/lib/active_storage/transformers/image_magick.rb
  • activestorage/lib/active_storage/transformers/vips.rb

具体的には、各トランスフォーマーファイルの先頭付近で、次のような形のコードが追加されているイメージです(疑似コード/要約):

ruby
# image_magick.rb
require "image_processing/mini_magick"

module ActiveStorage
  module Transformers
    class ImageMagick
      # 既存の変換ロジック
    end
  end
end
ruby
# vips.rb
require "image_processing/vips"

module ActiveStorage
  module Transformers
    class Vips
      # 既存の変換ロジック
    end
  end
end

これにより、アプリケーションが Active Storage のトランスフォーマークラスをロードした段階で、対応する image_processing バックエンドも同時にロードされます。

CHANGELOG

activestorage/CHANGELOG.md にエントリが追加されており、「画像処理バックエンドを事前に読み込む」挙動変更が明示されています。
これは、挙動に軽微ながらもパフォーマンス上のインパクトがあるため、利用者がアップグレード時に気付きやすいようにする目的と考えられます。


  1. 影響範囲・注意点

性能・メモリ面の影響

  • 初回リクエスト遅延の減少
    デプロイ直後に最初のバリアント生成を行うリクエストからは、image_processing バックエンドのロード時間(50–100ms 程度、環境による)が除去されます。
  • prefork 型サーバでのメモリ削減
    • マスタープロセスが image_processing/vips をロードした状態で forkされるため、追加の約 19MB がワーカー間でコピーオンライトにより共有されやすくなります。
    • 結果として、ワーカー数が多い構成ほど総メモリ使用量の削減効果が見込まれます。

起動時のオーバーヘッド

  • アプリケーションプロセスの起動時(マスタープロセス起動時)に、image_processing バックエンドを読み込む分の時間・メモリコストが前倒しで発生します。
  • ただし、これは「いつか必ず発生するコスト」を「最初のリクエスト」ではなく「起動時」に移動しただけとも言えます。
  • Bootsnap 利用環境や Linux の本番サーバでの具体的な数値は作者も未計測とのことですが、一般的には「初回リクエストのレスポンス改善」「メモリ共有効率の向上」のメリットが勝るケースが多いと考えられます。

アプリケーション側での対応

  • 通常は特別な変更は不要で、Rails をアップグレードすればこの挙動が自動的に有効になります。
  • もしアプリ側で独自に image_processing を lazy load していたとしても、Active Storage が require 済みであれば二重 require による問題は基本的に発生しません(Ruby の require は同一ファイルを2回ロードしない)。
  • メモリが極端にシビアな環境で「画像変換機能を一切使っていないのにバックエンドを読み込みたくない」といったニッチな要件がある場合は、
    • Active Storage 自体を使わない構成にする
    • または独自に autoload/require のタイミングを制御する などの検討が必要になるかもしれませんが、一般的な利用では問題にならないはずです。

  1. 参考情報 (あれば)

#57396 Invalidate query cache when UPDATE goes through update_with_result

マージ日: 2026/5/20 | 作成者: @kylekeesling

  1. 概要 (1-2文で)
    PostgreSQL の GENERATED STORED カラムを持つテーブルで UPDATE が行われた際、update_with_result 経由の更新でもクエリキャッシュが正しく無効化されるようにした修正です。これにより、同一キャッシュスコープ内で UPDATE 後に古いレコードが返ってしまう不具合が解消されます。

  1. 変更内容の詳細

何が問題だったか

Rails edge では、PostgreSQL の GENERATED ... STORED カラムが存在するテーブルに対しては、UPDATE 時に RETURNING を使うルート(update_with_result)が使われるようになっています(関連 PR: #48628)。

しかしクエリキャッシュの実装では、キャッシュを「汚す」(invalidate) 対象として update は登録されていた一方で、update_with_result がリストに含まれていませんでした。

その結果、次のような状況が発生していました:

ruby
ActiveRecord::Base.connection.cache do
  p = Post.create!(counter1: 1)
  Post.find(p.id)              # この SELECT がクエリキャッシュに載る
  p.update!(counter1: 99)      # GENERATED STORED カラムがあり update_with_result ルートを通る
  Post.find(p.id).counter1     # => 1 (本当は 99 であるべきなのに、キャッシュから古い値が返る)
end

update_with_result がキャッシュ無効化の対象になっていないため、2 回目の Post.find が UPDATE 前のキャッシュをそのまま使ってしまう、というバグです。

具体的な修正内容

  1. dirties_query_cache:update_with_result を追加

    対象ファイル:
    activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb

    ここに定義されている「この種の SQL が実行されたらクエリキャッシュを無効化する」というメソッド名のリストに、update_with_result を追加しています。

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

    ruby
    # 以前
    dirties_query_cache :insert, :update, :delete
    
    # 変更後
    dirties_query_cache :insert, :update, :update_with_result, :delete

    これにより、update_with_result が呼ばれたタイミングでも、現在の接続のクエリキャッシュがフラッシュされ、後続の SELECT は DB から最新状態を再取得するようになります。

  2. 回帰テストの追加

    対象ファイル:
    activerecord/test/cases/query_cache_test.rb

    すでに test_update というクエリキャッシュと UPDATE の連携を検証するテストがあり、それに隣接して、新たに update_with_result 経由の更新でもキャッシュが無効化されることを確認するテストが追加されています。

    主なポイント:

    • アダプタが PostgreSQL であること
    • supports_virtual_columns? が true であること
      GENERATED ... STORED カラムがサポートされていること)

    という条件でテストを実行します。

    既存の Default モデルにはすでに virtual_stored_number という GENERATED STORED カラムがあるため、それを利用して:

    • クエリキャッシュ有効範囲 (connection.cache do ... end) を張る
    • 1 度 SELECT してキャッシュに載せる
    • update_with_result ルートを通る UPDATE を実行
    • 再度同じレコードを SELECT し、更新結果が反映されていることを期待

    という流れのテストになっています。


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

    • DB アダプタ: PostgreSQL
    • テーブルに GENERATED ... STORED カラム(Rails 上では virtual :..., stored: true 等)が存在する
    • save! / update! 等で UPDATE が発生する
    • その前後の SELECT が同じ ActiveRecord::Base.connection.cache ブロック(または同等のクエリキャッシュスコープ)の中で行われている

    この条件が揃っている場合、これまでは UPDATE 後も古い行が SELECT で返る可能性がありましたが、この PR によって解消されます。

  • 既存アプリへの影響

    • これまでは「たまたま」キャッシュが無効化されずに古いデータが返っていたところが、正しく最新データを返すようになります。
    • クエリキャッシュが無効化されるタイミングが 1 パターン増えるため、極めて細かいレベルで見ると、「UPDATE 直後の SELECT がキャッシュを参照して高速になっていた」ケースはなくなります。ただしそれは本来バグであり、正しい挙動は今回の修正後です。
    • インターフェイスの変更や API の互換性に関わる変更はなく、あくまで内部のキャッシュ・インバリデーションの修正です。
  • 注意点

    • この PR は「GENERATED STORED カラムがある場合の UPDATE ルート」に限定された修正で、他の DB(MySQL、SQLite など)や、GENERATED カラムを持たないテーブルの挙動は従来どおりです。
    • もしアプリ側で独自に update_with_result をフックしている・モンキーパッチしている場合は、クエリキャッシュとの連携周りで挙動の変化がないかを確認しておくと安心です。

  1. 参考情報 (あれば)
  • この PR が修正している経路の追加元:
    • #48628 – PostgreSQL での UPDATE RETURNING パス (update_with_result) 追加の PR
  • 関連クラス:
    • ActiveRecord::ConnectionAdapters::QueryCache
    • UPDATE 経路:
      • update
      • update_with_result(RETURNING を伴う UPDATE、GENERATED STORED カラムがある場合に利用)

#57378 Use NOT ENFORCED in disable_referential_integrity on PostgreSQL 18.4+

マージ日: 2026/5/20 | 作成者: @yahonda

  1. 概要 (1-2文で)
    PostgreSQL 18.4 以降を対象に、disable_referential_integrity が内部で外部キー制約を「NOT ENFORCED / ENFORCED」に切り替える方式に変更され、DISABLE TRIGGER ALL を使わなくなりました。これにより superuser 権限が不要になりつつ、トランザクションで安全に元の制約状態を復元できるようになっています。

  1. 変更内容の詳細

背景・目的

  • これまで PostgreSQL アダプタの disable_referential_integrityDISABLE TRIGGER ALL / ENABLE TRIGGER ALL を使って外部キー制約を一時的に無効化していました。
  • この方法は多くの場合 スーパーユーザー権限 が必要であり、マネージド DB や権限が制限された環境では使いづらいという問題がありました。
  • PostgreSQL 18.4+ では、FK 制約を NOT ENFORCED / ENFORCED で切り替えることで、テーブル所有者権限だけで 実質的に制約の有効/無効を切り替えられるようになります。
  • さらに NOT ENFORCED では ON DELETE CASCADE / SET NULL / SET DEFAULT などの参照アクションも抑止 されるため、古い実装で目指していた「一時的に参照整合性に伴う副作用を止める」ことも実現できます(revert された #27636 が意図していた挙動に相当)。

主な変更点

1) PostgreSQL 18.4+ での disable_referential_integrity の実装切り替え

PostgreSQL 18.4 以上の場合、disable_referential_integrity は以下のようなイメージのクエリを実行するようになります(擬似コード):

ruby
def disable_referential_integrity
  if postgresql_version >= 18.4
    transaction do
      # 対象テーブルの外部キー制約を NOT ENFORCED に
      foreign_keys(table_name).each do |fk|
        execute("ALTER TABLE #{quote_table_name(fk.from_table)} ALTER CONSTRAINT #{quote_column_name(fk.name)} NOT ENFORCED")
      end

      yield

      # 実行後、元々 ENFORCED だった制約について ENFORCED に戻す
      foreign_keys(table_name).each do |fk|
        execute("ALTER TABLE #{quote_table_name(fk.from_table)} ALTER CONSTRAINT #{quote_column_name(fk.name)} ENFORCED")
      end
    end
  else
    # 既存実装: DISABLE TRIGGER ALL / ENABLE TRIGGER ALL
  end
end

実際には、Rails 側で「もともと ENFORCED だったか / もともと NOT ENFORCED だったか」を区別できるように管理しており、元の状態を正しく復元 します。

2) トランザクションでのラップを強化

  • disable_referential_integrity の内部で、「NOT ENFORCED にする」「ブロックを実行する」「元に戻す」 という一連の処理を 単一のトランザクションでラップ するように変更されています。

  • これにより、ユーザーが外側で明示的にトランザクションを張っていなくても、以下のような状況を防げます:

      1. ALTER CONSTRAINT ... NOT ENFORCED がコミット済み
      1. ブロック内で制約違反のデータを入れてしまい、ENFORCED に戻す際にエラー
      1. その結果、もともと ENFORCED だった制約が NOT ENFORCED のまま DB に残る
  • トランザクションでラップすることで、途中でエラーが出た場合はトランザクションがロールバックされ、制約の状態は呼び出し前に完全に戻る ことが保証されます。

Rails 内部の利用箇所:

  • insert_fixtures_set
    • 既にトランザクションでラップされているが、disable_referential_integrity 自体も内部トランザクションで保護される。
  • truncate_tables
    • 同様に、内部で disable_referential_integrity を利用。

3) check_all_foreign_keys_valid! の挙動変更

  • PostgreSQL の NOT ENFORCED 制約には VALIDATE CONSTRAINT を適用できないため、check_all_foreign_keys_valid!NOT ENFORCED な制約を検査対象から除外 するようになっています。
  • これにより、アプリケーション上で意図的に「NOT ENFORCED のまま運用している制約」がある場合でも、check_all_foreign_keys_valid! の呼び出しが失敗したりしません。

4) テストの追加・更新

  • activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb に大幅なテスト追加 (+208 行) が行われており、主に以下をカバーしていると考えられます:
    • PostgreSQL 18.4+ で NOT ENFORCED / ENFORCED が使われること
    • 元々 ENFORCED だった制約の状態がブロック実行後に正しく復元されること
    • もともと NOT ENFORCED だった制約は、実行前後で状態が変わらないこと
    • 制約違反が発生した場合でも、トランザクションのロールバックで状態が壊れないこと
  • fixtures_test.rb なども更新され、フィクスチャ投入まわりでの挙動がカバーされています。

  1. 影響範囲・注意点

影響範囲

  • 対象: PostgreSQL 18.4 以上を使っている Rails アプリケーションで、以下を利用しているケース
    • ActiveRecord::Base.connection.disable_referential_integrity
    • フィクスチャ (insert_fixtures_set) 利用
    • truncate_tables 利用
  • これらの場面では、従来よりも低い権限(テーブル所有者権限)で外部キー制約の一時無効化が行われます。

開発者視点でのメリット

  • DB 管理者が superuser 権限をアプリケーションユーザーに渡さなくても、テストやデータロード処理が動かしやすくなる。
  • NOT ENFORCED によって ON DELETE CASCADE なども抑止されるため、データ移行・テストデータロード時に「予期せぬカスケード削除」が起こりにくい。

注意点

  1. NOT ENFORCED 制約の存在に対する前提の見直し

    • アプリケーション側で「すべての外部キーは常に ENFORCED である」という前提を置いているコードがある場合、disable_referential_integrity 実行中はその前提が崩れます。
    • ただし、この PR では Rails 側がコール前後で状態を復元するため、「呼び出し前後での恒常的な状態」は保たれます。
  2. check_all_foreign_keys_valid! のカバレッジ

    • NOT ENFORCED の制約は check_all_foreign_keys_valid! の対象外になります。
    • 「DB 全体で参照整合性が有効であることを完全に保証したい」場合は、
      • 「意図的に NOT ENFORCED にしている制約」がないか
      • 必要に応じて運用側で個別チェックを追加する といった設計上の考慮が必要です。
  3. PostgreSQL バージョン依存

    • 18.4 未満の PostgreSQL では、従来通り DISABLE TRIGGER ALL / ENABLE TRIGGER ALL が利用されます。
    • 同じアプリを複数バージョンの PostgreSQL で運用する場合、環境ごとに内部動作が変わることになるため、 -権限要件(superuser が必要か) -性能特性・ロック挙動 に若干の違いが出る可能性があります。

  1. 参考情報 (あれば)
  • 元 PR: https://github.com/rails/rails/pull/57378
  • NOT ENFORCED / ENFORCED の詳細な挙動説明 PR: https://github.com/rails/rails/pull/57377
  • 関連する過去の変更・議論:
    • rails/rails#27636 (ON DELETE CASCADE などを抑止する狙いがあったが、一度 revert されている)
  • PostgreSQL ドキュメント(NOT ENFORCED まわりはバージョン 18 系のリリースノート・リファレンスを参照)

#57394 Stabilize say_with_time regression tests

マージ日: 2026/5/20 | 作成者: @yahonda

  1. 概要 (1-2文で)
    say_with_time の回帰テストが CI 環境でタイミングぶれにより不安定になっていた問題を、期待値を「固定文字列」から「フォーマットのみを保証する正規表現マッチ」に変更することで安定化した PR です。ログ出力の形式は維持したまま、経過時間の微小な違いでテストが落ちないようにしています。

  1. 変更内容の詳細

対象は activerecord/test/cases/migration_test.rb 内の以下の 2 つのテストです。

  • MigrationTest#test_migration_say_with_time_with_integer_returning_in_block
  • MigrationTest#test_migration_say_with_time_with_non_integer_returning_in_block

元々のテストは、say_with_time が出力するログ全文を「完全一致」で比較していました:

ruby
expected = <<~MSG
  -- Bar
     -> 0.0000s
     -> 123 rows
MSG

assert_equal expected, output

このように "-> 0.0000s" という文字列に固定していたため、ブロックの実行時間が 0.0001s0.0002s になると、CI で以下のように失敗していました:

diff
"-- Bar
-   -> 0.0000s
+   -> 0.0001s
   -> 123 rows
"

今回の修正では、経過時間の具体的な値ではなく、フォーマットだけを検証するように変更しています。説明文によると、次の条件を満たす正規表現を使って assert_match する形になっています:

  • 先頭の "-- Bar" 行 (ヘッダ行) があること
  • 次の行にサブ項目がインデント付きで表示されること (スペース + ->)
  • 経過時間は %.4f(小数点以下 4 桁)の形式であること
    • 例: 0.0000s, 0.0001s, 0.1234s, etc.
  • 単位の s が末尾につくこと
  • (該当テストでは) 行数出力 "-> 123 rows" が続くパターンと、行数なしのパターンの両方でフォーマットを保証
  • 改行の有無・数もある程度固定 (アンカー付き \A ... \z など) で検証

イメージとしては、次のような形に近いテストになっていると考えられます(実際のコードを概念的に表現):

ruby
assert_match(
  /\A-- Bar\n   -> \d+\.\d{4}s\n   -> 123 rows\n\z/,
  output
)

および:

ruby
assert_match(
  /\A-- Bar\n   -> \d+\.\d{4}s\n\z/,
  output
)

これにより、0.0000s 〜 0.0002s などの微小な揺らぎは許容しつつ、出力レイアウトとフォーマット仕様は担保できるようになっています。


  1. 影響範囲・注意点
  • 影響範囲は Active Record のテストコードのみ であり、フレームワーク本体の挙動 (say_with_time の実装) には変更はありません。
  • Rails を利用するアプリケーションの本番挙動やログフォーマットには影響しません。
  • 目的は「CI の flakiness(ときどき落ちる不安定テスト)の解消」であり、
    • 経過時間の数値自体に意味を持たせたいテストではなく
    • say_with_time の出力フォーマット回帰テスト」であることから、この変更は妥当です。
  • 同様に、時間・タイミングに依存する他のテストでも、厳密値ではなくフォーマットと範囲をテストする方が CI を安定化しやすい、という実例として参考にできます。

  1. 参考情報 (あれば)

#50431 Add exclusion_constraint_exists? and unique_constraint_exists? helpers

マージ日: 2026/5/19 | 作成者: @fatkodima

  1. 概要 (1-2文で)
    PostgreSQL アダプタに、排他制約(exclusion constraint)とユニーク制約(unique constraint)が既に存在するかをチェックするためのヘルパー exclusion_constraint_exists?unique_constraint_exists? が追加されました。これにより、制約を「存在しなければ追加する」といった形で、より安全かつ冪等的にマイグレーションを書くことができます。

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

2-1. 追加されたヘルパーメソッド

PostgreSQL 用の ActiveRecord::ConnectionAdapters::PostgreSQLAdapter に以下が追加されています。

  • exclusion_constraint_exists?
  • unique_constraint_exists?

既存の check_constraint_exists?foreign_key_exists?, index_exists? と同じ思想の API です。

想定されるインターフェイス

PRの説明・差分構成から、以下のような呼び出しが可能と推測されます(実際の引数名は他の *_constraint_exists? メソッドとほぼ同様になる想定):

ruby
# 排他制約の存在確認
exclusion_constraint_exists?(
  :bookings,
  name: "bookings_no_overlap"
)

# 条件付きユニーク制約の存在確認
unique_constraint_exists?(
  :users,
  name: "index_users_on_email_where_active"
)

あるいは、change_table ブロック内の DSL からも利用できるようにテストが追加されているため、例えば次のようなスタイルがサポートされています:

ruby
change_table :bookings do |t|
  unless t.exclusion_constraint_exists?(name: "bookings_no_overlap")
    t.exclusion_constraint :room_id,
      using: :gist,
      with: "&&",
      where: "cancelled_at IS NULL",
      name: "bookings_no_overlap"
  end
end
ruby
change_table :users do |t|
  unless t.unique_constraint_exists?(name: "unique_active_email")
    t.unique_constraint :email, where: "deleted_at IS NULL", name: "unique_active_email"
  end
end

※ 正確な DSL シグネチャは、既存の check_constraint / check_constraint_exists? とほぼ同じパターンになるよう実装されています。

2-2. 実装箇所

  • schema_definitions.rb

    • ChangeTableDefinition / TableDefinition などに、exclusion_constraint_exists? および unique_constraint_exists? を委譲するメソッドが追加されています。
    • これにより、change_table ブロック内部からも直感的に存在確認が可能になります。
  • schema_statements.rb

    • PostgreSQL アダプタに exclusion_constraint_exists? / unique_constraint_exists? の実装が追加。
    • 実装としては PostgreSQL の pg_constraintpg_index などのシステムカタログ/情報スキーマを用いたクエリになっており、指定された table_nameconstraint name(あるいは対象カラム等)に一致する制約があるかどうかをチェックしています。
    • 既存の check_constraint_exists? と同様のコードパス・クエリスタイルで整合性が保たれています。
  • テスト (exclusion_constraint_test.rb, unique_constraint_test.rb, change_table_test.rb)

    • 「制約を追加 → *_constraint_exists?true を返す」パス。
    • 「制約が存在しない場合に false を返す」パス。
    • change_table ブロック内からの利用をカバーするテスト。
    • これにより、API が今後も後方互換性を保ちながら動作することが保証されています。

  1. 影響範囲・注意点

3-1. 影響範囲

  • 対象は PostgreSQL アダプタのみ(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)。
  • すでに存在していた
    • 排他制約サポート (add_exclusion_constraint など)
    • ユニーク制約サポート (add_unique_constraint など) を補完する「存在確認」機能の追加であり、既存の API の挙動を変えるものではありません。
  • 追加メソッドを利用しない既存コードには影響しません。

3-2. 開発者が意識するとよい点

  • マイグレーションの冪等性確保:

    • 今後は排他制約・ユニーク制約も以下のようなパターンで安全に記述できます。

      ruby
      def up
        unless exclusion_constraint_exists?(:bookings, name: "bookings_no_overlap")
          add_exclusion_constraint :bookings,
            "tsrange(start_at, end_at)",
            using: :gist,
            name: "bookings_no_overlap"
        end
      end
      
      def down
        remove_exclusion_constraint :bookings, name: "bookings_no_overlap"
      end
      ruby
      def up
        unless unique_constraint_exists?(:users, name: "unique_active_email")
          add_unique_constraint :users,
            :email,
            name: "unique_active_email",
            where: "deleted_at IS NULL"
        end
      end
  • スキーマ管理ツールやプラグイン:

    • 「実 DB と schema.rb / structure.sql の差分を調べて同期する」といったツールから、*_constraint_exists? を通じて排他制約・ユニーク制約も安全に扱えるようになります。
  • RDBMS 依存性:

    • これらのメソッドは PostgreSQL 固有機能(排他制約・複雑なユニーク制約)に依存しているため、MySQL / SQLite など他アダプタでは利用できません。
    • アプリケーションが DB 抽象化を重視する場合は、呼び出し前に connection.adapter_name で PostgreSQL かどうかを確認するなどの考慮が必要です。

  1. 参考情報 (あれば)
  • 排他制約の追加 PR: https://github.com/rails/rails/pull/40224
  • ユニーク制約の追加 PR: https://github.com/rails/rails/pull/46192
  • 本 PR: Add exclusion_constraint_exists? and unique_constraint_exists? helpers (#50431)
  • 類似 API:
    • check_constraint_exists?
    • foreign_key_exists?
    • index_exists? と同じパターンで利用できるため、これらの既存ドキュメントを参照すると使用感をつかみやすいです。

#57377 Add enforced: option for foreign keys on PostgreSQL 18.4+

マージ日: 2026/5/19 | 作成者: @yahonda

  1. 概要 (1-2文で)
    PostgreSQL 18.4 以降を対象に、Active Record の外部キーに enforced: オプションが追加され、「参照整合性チェックを一時的に無効化して後から再検証する」ことを Rails の API 経由で安全に行えるようになりました。これにより、これまで superuser 権限やトリガ無効化に頼っていたバルクロードなどのユースケースを、テーブル所有者権限のみで実現できます。

  1. 変更内容の詳細

新しくできることの概要

  • add_foreign_keyenforced: false を指定可能(PostgreSQL 18.4+ のみ)
    • 対応する外部キー制約は NOT ENFORCED で作成される
    • DML(INSERT/UPDATE/DELETE)時の参照整合性チェックがスキップされる
  • change_foreign_key で既存外部キーの enforced 状態をトグル可能
    • 一時的に NOT ENFORCED にしてバルクロード → 再度 ENFORCED に戻す、といった運用が Rails のマイグレーションだけで完結
  • スキーマダンプ(schema.rb / structure.sql)に enforced:validate: が正しく反映される

PostgreSQL 側の仕様との対応

  • PostgreSQL 18.0 で NOT ENFORCED な外部キー制約が導入されたが、18.3 までは DEFERRABLE 属性が失われる不具合があった
  • PostgreSQL 18.4 で「外部キーの deferrability が失われるバグ」が修正されたため、Rails は「18.4 以降のみ対応」というポリシーにしている
  • NOT ENFORCED な外部キーは内部的に NOT VALID として扱われる
    • VALIDATE CONSTRAINT は適用できない
    • そのため schema dumper は enforced: falsevalidate: false の両方を出力する

具体的な API 変更と使い方

1) add_foreign_keyenforced: オプション追加

ruby
# 例: posts.user_id -> users.id の外部キーを「一時的にチェックなし」で作る
add_foreign_key :posts, :users, enforced: false

PostgreSQL 18.4+ では、これが内部的におおよそ以下のような SQL に相当します(実際の SQL は Rails が組み立てます):

sql
ALTER TABLE posts
  ADD CONSTRAINT fk_posts_users
  FOREIGN KEY (user_id)
  REFERENCES users(id)
  NOT ENFORCED;

この状態では:

  • 外部キー制約は存在するが、DML 時に参照整合性チェックは行われない
  • ただし制約オブジェクト自体はあるため、後から ENFORCED に戻すことができる

2) change_foreign_key による enforced 状態のトグル

新メソッド change_foreign_key が追加され、既存制約の enforced/NOT ENFORCED を切り替え可能です。

ruby
# 既存外部キーを NOT ENFORCED にする
change_foreign_key :posts, :users, enforced: false

# 再度 ENFORCED に戻す(このとき PostgreSQL が全行を再検証する)
change_foreign_key :posts, :users, enforced: true
  • enforced: true に戻す際、PostgreSQL はテーブル全行をチェックして不正データがないか検証します
    • 不正な行があればエラーになり、ENFORCED への切り替えは失敗します
  • これは「トリガ再有効化では既存行は再検証されない」という従来の問題を解消するポイントです

3) 抽象層・アダプタ周りの変更

主な変更ポイント:

  • ActiveRecord::ConnectionAdapters::AbstractAdapter
    • supports_enforced_constraints?(名称は概念的; 実際は POSTGRES 18.4+ チェック用の capability フラグ)を導入している形
    • PostgreSQL アダプタが 18.4 以上かどうかを判定するロジックを追加
  • schema_statements.rb / postgresql/schema_statements.rb
    • add_foreign_key / change_foreign_keyenforced: を考慮して SQL を組み立てる実装を追加
  • schema_creation.rb
    • constraint 生成時に NOT ENFORCED を付与するロジックを追加
  • schema_dumper.rb
    • 外部キー出力時に enforced: falsevalidate: false の両方を出すよう変更
  • schema_definitions.rb
    • 外部キー定義オブジェクトに enforced 属性を追加

4) テストの追加

activerecord/test/cases/migration/foreign_key_test.rb に大量のテストが追加されています:

  • enforced: false で作成された外部キーが期待通り NOT ENFORCED になること
  • change_foreign_key で ENFORCED/NOT ENFORCED を正しくトグルできること
  • schema dump / load(マイグレーション → schema.rb → 再ロード)の往復で enforced: の状態が保持されること

  1. 影響範囲・注意点

対応バージョン制約

  • PostgreSQL 18.4 未満では NOT ENFORCED 外部キーの仕様が不完全なため、この機能は PostgreSQL 18.4+ 専用 機能です
    • Rails はバージョンをチェックし、サポートしないバージョンで enforced: を使った場合は例外を投げる/無視する(実装に依存)可能性があります
    • 実際に利用する際は DB バージョンを厳密に確認してください

NOT ENFORCED の使いどころとリスク

  • 主な用途は「バルクロード・大量データ移行時の一時的な制約無効化」
    • 例えば:
      1. 参照先テーブル・参照元テーブルを任意順でロード
      2. ロード中は enforced: false にしておく
      3. ロード完了後に change_foreign_key enforced: true で再検証しつつ有効化
  • ただし NOT ENFORCED の間は不整合なデータが普通に入り得る ため、
    • ENFORCED に戻すときにエラーになる可能性がある
    • ロードスクリプトやマイグレーションのロールバック戦略をきちんと設計する必要がある

既存コードへの影響

  • 既存の add_foreign_key / remove_foreign_key / add_reference などは、enforced: を指定しない限り動作は変わりません
  • schema.rb / structure.sql の出力形式に、外部キー周りで enforced: false および validate: false が追加される可能性があります
    • スキーマ差分を CI でチェックしている場合、PostgreSQL 18.4+ に上げたタイミングで差分が発生することがあります

将来の変更との関係

  • PR 説明にもある通り、「次の PR で disable_referential_integrityNOT ENFORCED ベースに書き換える」予定になっています
    • 現状(PostgreSQL 17 以前)は DISABLE TRIGGER ALL に依存しており、superuser 権限が必要
    • これが NOT ENFORCED ベースに置き換わると、superuser 不要でより安全に一時的な外部キー無効化が行えるようになる見込みです

  1. 参考情報 (あれば)

#57388 http_cache_forever now accept an optional last_modified: keyword parameter.

マージ日: 2026/5/19 | 作成者: @byroot

  1. 概要 (1-2文で)
    http_cache_forever がオプション引数 last_modified: を受け取れるようになり、デフォルトの日付(2011-01-01)の代わりに実際の更新時刻を使った長期キャッシュ制御が可能になりました。あわせて Active Storage の ProxyController で、この新オプションを使うように変更されています。

  1. 変更内容の詳細

2-1. http_cache_foreverlast_modified: キーワード引数が追加

これまで:

ruby
http_cache_forever do
  # ...
end
  • 内部的に Last-Modified が固定日付(2011-01-01)として扱われる挙動だった。
  • 実際には「永遠に変わらない静的なリソース」を想定したヘルパだったが、Last-Modified を明示的に指定することはできなかった。

今回の変更後:

ruby
http_cache_forever(last_modified: some_time) do
  # ...
end
  • last_modified: に任意の Time/ActiveSupport::TimeWithZone 等を渡せるようになった。
  • 引数を渡さなかった場合は、引き続きデフォルトの 2011-01-01 が使われる(後方互換性維持)。

イメージ例:

ruby
class AssetsController < ApplicationController
  def show
    asset = Asset.find(params[:id])

    http_cache_forever(last_modified: asset.updated_at) do
      send_data asset.data, type: asset.content_type, disposition: "inline"
    end
  end
end

これにより、

  • Cache-Control: public, max-age=...(「実質 forever」な長期キャッシュ)
  • Last-Modified: asset.updated_at

のようなヘッダを組み合わせて返せるため、クライアントは If-Modified-Since を使った条件付きリクエストを行えます。

2-2. Active Storage ProxyController での利用

activestorage/app/controllers/active_storage/blobs/proxy_controller.rb が 1 行変更され、http_cache_forever 呼び出し時に last_modified: を渡すようになりました。

ざっくりしたイメージ:

ruby
# 変更前
http_cache_forever do
  # blob をプロキシして返す
end

# 変更後
http_cache_forever(last_modified: @blob.created_at) do
  # あるいは updated_at / service_metadata 由来の時刻等
end

実際には Active Storage の blob モデルが持つタイムスタンプ(例: @blob.created_at@blob.updated_at)が Last-Modified に反映されるようになっています。

これにより、Active Storage のプロキシエンドポイント(例: /rails/active_storage/blobs/proxy/...)でも、実際の BLOB の最終更新時刻に基づく HTTP キャッシュ制御が行われます。

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

  • actionpack/test/controller/render_test.rb に、last_modified: を指定した場合の挙動を検証するテストが追加。
    • 指定した last_modified がレスポンスヘッダに反映されるか
    • If-Modified-Since を含む条件付きリクエストに対して 304 が返るか
      などが確認されていると考えられます。
  • actionpack/CHANGELOG.mdactivestorage/CHANGELOG.md に、この機能追加についての記述が追加。

  1. 影響範囲・注意点
  • 互換性
    • http_cache_forever の既存呼び出しは、引数なしでこれまで通り利用可能です。デフォルトの日付も変わっていないため、既存コードの挙動は基本的に変わりません。
  • last_modified の指定に関する注意
    • 実際に内容が変わらない限りタイムスタンプを更新しない値(例: モデルの updated_at やファイルの実更新日時)を渡す必要があります。
    • 内容が変わっていないのに last_modified が頻繁に変わる値(現在時刻など)を渡すと、クライアント側キャッシュが無効化されて毎回 200 で返ってしまい、「forever キャッシュ」のメリットが失われます。
  • Active Storage を使っている場合
    • Active Storage の ProxyController 経由でアクセスしているクライアントは、BLOB の更新に応じて Last-Modified が変わるようになります。
    • これにより、ブラウザや CDN がより正確にキャッシュの再検証(304 Not Modified)を行えるようになり、帯域やレスポンス時間の改善が期待できます。
  • ETag との組み合わせ
    • コード側で ETag を併用している場合、Last-Modified と整合的な運用を行うべきです(内容が変わった時に両方が変わる状態が望ましい)。

  1. 参考情報 (あれば)
  • Rails Guides: Caching with Rails
    • 特に「HTTP Caching」の章(expires_in, fresh_when, stale?, etag, last_modified など)
  • Rails ソース:
    • actionpack/lib/action_controller/metal/conditional_get.rb
      • http_cache_forever, fresh_when, stale? の実装周辺
    • activestorage/app/controllers/active_storage/blobs/proxy_controller.rb
      • Active Storage の BLOB プロキシ配信の流れと、キャッシュヘッダの付与部分

#46262 Support proc and symbol for NumericalityValidator's :in option

マージ日: 2026/5/19 | 作成者: @ThomasSevestre

  1. 概要 (1-2文で)
    NumericalityValidator:in オプションで、固定の Range だけでなく「Proc(ラムダ)」と「シンボル(インスタンスメソッド参照)」も指定できるようになりました。これにより、モデルインスタンスの状態に応じて動的に数値の許容範囲を変えられます。

  1. 変更内容の詳細

何ができるようになったか

これまで:

ruby
validates_numericality_of :price, in: 0..100

のように、:in には固定の Range しか渡せませんでした。
このPRにより、以下のように Proc および シンボル も使えるようになります。

Proc を使うパターン

ruby
validates_numericality_of :price, in: ->(record) { 0..record.max_price }
  • ブロックは (record)(検証対象のモデルインスタンス)を引数に取り、Range を返す。
  • レコードごとに max_price が違う場合でも、その値を元に許容範囲を計算可能。

シンボルを使うパターン

ruby
class Product < ApplicationRecord
  validates_numericality_of :price, in: :price_range

  def price_range
    0..max_price
  end
end
  • :in に渡したシンボル名のインスタンスメソッドが呼ばれ、その戻り値を Range として扱う。
  • #price_rangeRange を返す必要がある。

コード上の変更ポイント(概要)

activemodel/lib/active_model/validations/numericality.rb にて:

  • :in オプションを解決する際に、
    • Range → そのまま利用
    • Proccall(record) して得た値を利用
    • Symbolrecord.public_send(symbol) して得た値を利用 という分岐が追加された形になっています(実装としてはこれに準じるロジック)。

それに応じたテストが numericality_validation_test.rb に追加され、

  • Proc 指定で正しく Range が評価されること
  • シンボル指定でインスタンスメソッドが呼び出されること
  • 期待どおりにバリデーションが通る/落ちること
    がカバーされています。

また、activemodel/CHANGELOG.md にこの新機能が記載され、外部API変更として明示されています。


  1. 影響範囲・注意点
  • 戻り値は必ず Range を返す必要がある
    Proc/メソッド(シンボル指定)ともに、戻り値は Range オブジェクトを想定しています。
    nil や他の型を返すと例外発生や意図しない挙動となる可能性が高いです。

  • 毎回評価される
    Proc やメソッドはバリデーション実行時に都度呼ばれるため、

    • 副作用のある処理
    • 重い計算・外部API呼び出し
      などは避けるべきです。
  • 既存コードへの互換性
    既存の Range を直接指定しているコードはそのまま動作し、挙動変更はありません。
    追加で Proc / シンボルが許可されただけなので、互換性の観点でのリスクは低いです。

  • マルチスレッド環境での安全性
    評価対象はレコードインスタンスに紐づく Proc / メソッド呼び出しなので、
    クラス変数やグローバル状態を使った Range 生成を行う場合は、スレッドセーフかどうか注意してください。


  1. 参考情報 (あれば)
  • 該当PR: https://github.com/rails/rails/pull/46262
  • 近い機能として、NumericalityValidator 以外のバリデータ(e.g. inclusion, exclusion)でも、:in に Proc / シンボルを渡して動的なコレクションを指定するパターンが既に存在します。このPRはそれらとインターフェイスを揃える形の拡張と言えます。

#57386 Use ... and anonymous splats when possible

マージ日: 2026/5/19 | 作成者: @byroot

  1. 概要 (1-2文で)
    このPRは、activesupportString 拡張(output_safety関連)で、可変長引数の扱いを「*args から ... / 匿名スプラット」へ書き換えるリファクタリングです。機能仕様は変えずに、わずかなオブジェクト生成削減と、よりモダンで読みやすいRubyコードに整理しています。

  1. 変更内容の詳細

対象ファイル:

  • activesupport/lib/active_support/core_ext/string/output_safety.rb

やっていることは一貫していて、「明示的な引数配列 (*args)」→「...(引数フォワーディング)と匿名スプラット」を使うスタイルへの移行です。

典型的な変更パターンは次のようなものです(概念的なイメージ):

ruby
# Before
def concat(*args)
  safe_concat(*args)
end

# After (Ruby 2.7+ の引数フォワーディング)
def concat(...)
  safe_concat(...)
end

あるいは、ブロック付きでメソッドに引数をそのまま渡すケースで:

ruby
# Before
def some_method(*args, &block)
  super(*args, &block)
end

# After
def some_method(...)
  super(...)
end

「anonymous splats」という表現は、def foo(*) のように引数を受け取るが名前を付けない(参照しない)パターンや、(... ) のように「配列として受け取らずにそのままフォワードする」ことを指しています。
このPRでは、output_safety.rb 内で、html_safe を扱うためのラッパーメソッド・デリゲーションメソッドなどに対して、上記のような書き換えが行われています。

結果として:

  • メソッド内で引数の配列(args)が不要な箇所は匿名化し、
  • ただ他メソッドへそのまま委譲するだけのメソッドは ... によるフォワーディングに変更

という統一的なスタイルになっています。


  1. 影響範囲・注意点
  • 公開APIの挙動変更はありません。
    • 引数の受け取り方や返り値、html_safe 系メソッドの意味的な振る舞いは変わりません。
  • 内部実装として、*args による配列生成が減るため、わずかにメモリアロケーションとオーバーヘッドが減ります(PR本文の "minor allocation reduction")。
  • Ruby 2.7 以降で導入された ... 構文を前提としているため、ランタイムのサポートバージョンと合致していることが前提です(Rails本体のサポート範囲内なので問題はありません)。
  • output_safety 周り (String#html_safeActiveSupport::SafeBuffer 系) をモンキーパッチしている場合:
    • 引数の配列名(例: args)に依存していたり、メソッドシグネチャを厳密に反射しているようなコードがあれば影響する可能性がありますが、一般的には稀なケースです。

  1. 参考情報 (あれば)

#57385 fix start_with_delimiter? has a false-positive when the number coincidentally begins with the delimiter string in number_to_phone

マージ日: 2026/5/19 | 作成者: @tahsin352

  1. 概要 (1-2文で)
    Rails の number_to_phone で、電話番号の数字列がたまたま区切り文字(delimiter)と同じ並びで始まる場合に、先頭の数字が誤って削られてしまうバグを修正した PR です。先頭の区切り文字を事後的に削るロジックをやめ、gsub の置換ブロック内で「最初のグループが空かどうか」を見て直接出力を制御するように変更しています。

  1. 変更内容の詳細

問題の背景

number_to_phone は内部で正規表現と gsub を使って、数字列を「市外局番・市内局番・加入者番号」のように 3 パートに分割し、delimiter で連結しています。

元の実装では、先頭グループ (\d{0,3}) が「0〜3桁」の可変長であり、空文字列になるケース(たとえば桁数が少ないなど)を考慮するために、以下のようなパターンを取っていました。

  1. まず gsub で「常に part1 + delimiter + part2 + delimiter + part3」という形の文字列にする
  2. その後、start_with_delimiter? で「先頭が delimiter で始まっているか」をチェックし、始まっている場合は slice! で先頭の delimiter 分を削る

ここで問題なのは、start_with_delimiter?

ruby
number.start_with?(delimiter)

という単純な文字列判定で、**「先頭の delimiter が、本当に“空の第1グループ”由来なのか」**を区別できないことです。

PR の説明にある具体例:

ruby
number_to_phone(9001234, delimiter: "900")
# gsub の結果: "900900-1234"
#  - \1 = "900"(空ではない)
#  - しかし文字列は "900" で始まるので:
start_with_delimiter?("900900-1234") # => true
# slice!(0, 3) により:
# => "900-1234" となり、先頭の「900」が消えてしまう(不正)

ここでは「最初の 3 桁 900」が本物の番号の一部なのに、「delimiter と同じ値だから」という理由だけで削られてしまっています。

具体的な修正内容

この PR では、上記の「置換後に先頭の delimiter を見て削る」という構造を改め、gsub の置換ブロックの中で $1(最初のグループ)の内容を見て、その場で出力を決定するように変えています。

変更のポイント:

  • start_with_delimiter? メソッドを 完全に削除

  • gsub の置換ロジックを以下のように変更(擬似コード):

    ruby
    formatted = number.to_s.gsub(phone_pattern) do
      part1, part2, part3 = $1, $2, $3
    
      if part1.empty?
        # 最初のグループが空の場合は delimiter を出さない
        "#{part2}#{delimiter}#{part3}"
      else
        # 最初のグループがある場合のみ、先頭に part1 + delimiter
        "#{part1}#{delimiter}#{part2}#{delimiter}#{part3}"
      end
    end
  • これにより、「最初の delimiter を出すかどうか」は「$1 が空かどうか」だけで決まるようになり、
    「数字列の先頭がたまたま delimiter と同じ文字列だった」というだけでは削除されなくなります。

  • テスト (activesupport/test/number_helper_test.rb) に、マルチバイト/複数文字の delimiter と今回のバグケースをカバーする 回帰テストを 2 つ追加
    これにより、将来的に同種のロジック変更で再発しないようにしています。


  1. 影響範囲・注意点
  • 対象メソッドは ActiveSupport::NumberHelper#number_to_phone のみです。
  • 影響するのは主に以下の条件を満たすケースです:
    • number_to_phone を使っている
    • delimiter に複数文字の文字列を指定している(例: "900", "---" など)
    • かつ「電話番号の先頭の数字列」が、その delimiter の文字列と一致する場合
  • これまでそのようなケースでは、先頭の数桁が誤って削られていた可能性があります。今回の修正によって、本来の数字が保持されるようになるため、挙動が変わりますが、これはバグ修正として正しい方向の互換性変更です。
  • start_with_delimiter? が内部ロジックとして削除されただけで、パブリック API には影響ありません(number_to_phone の引数・戻り値の形は変わらない)。

  1. 参考情報 (あれば)
  • この PR は直前の修正 PR https://github.com/rails/rails/pull/57375 をフォローアップする位置づけで、
    そこではマルチキャラ delimiter 周りのバグを扱っており、その副作用として今回の false positive 問題が顕在化したものと思われます。
  • 実装上の設計として、「正規表現のキャプチャ結果($1〜)に依存するなら、その情報が生きている gsub ブロック内でロジックを完結させる」方針になっており、後処理で文字列だけを見て判断するより安全です。

#57384 Accept and discard unused block in ReflectionProxy#all_includes

マージ日: 2026/5/19 | 作成者: @joshuay03

  1. 概要 (1-2文で)
    Ruby 3.4 の strict_unused_block 警告に対応するため、Active Record の ReflectionProxy#all_includes が渡されたブロックを明示的に受け取りつつ破棄するように変更されました。挙動の変更はなく、警告を抑制するだけのメンテナンス的修正です。

  1. 変更内容の詳細

対象:
activerecord/lib/active_record/associations/association_scope.rb 内の ReflectionProxy#all_includes

背景:

  • Ruby 3.4 で strict_unused_block という警告カテゴリが導入され、
    「ブロックを受け取っている呼び出し元」→「ブロックを宣言していない呼び出し先」
    というパターンで警告が出るようになりました。
  • ReflectionProxy#all_includes は、add_constraints からブロック付きで呼ばれるものの、「何もしない(No-op、ブロックを意図的に無視する)」実装になっており、そのため strict_unused_block に引っかかっていました。

変更内容:
ReflectionProxy#all_includes がブロックを匿名転送で受け取り、即座に捨てるように修正されています。

イメージとしては、以下のような変更です(実際のコードは行数的に +1/-1 なので細部は多少異なる可能性がありますが、概念的にはこれ):

ruby
# 変更前(イメージ)
def all_includes
  reflection.all_includes
end

# 変更後(イメージ)
def all_includes(&)
  reflection.all_includes(&)
end

あるいは、ReflectionProxy 側で「ブロックを受け取るが使わない」ことをはっきりさせるため、次のような書き方をしている可能性もあります:

ruby
def all_includes(&)
  # ブロックは受け取るが、ReflectionProxy では意図的に何もしない
  reflection.all_includes
end

いずれにせよ、「ブロックを受け取ることをメソッドシグネチャで明示」することで、Ruby が「ブロックが意図的に無視されている」と判断できるようにし、strict_unused_block 警告が出ないようにしています。

ポイント:

  • ReflectionProxy#all_includes は、本来 add_constraints が組み立てる includes 情報を拾うためのフックですが、ReflectionProxy はそのチェーン上で「特に何もしない」役割のため、元々ブロックを利用していません。
  • その「何もしない」こと自体は変えずに、「ブロックを受け取っている」という事実だけを宣言的に追加した変更です。

  1. 影響範囲・注意点
  • 実行時の挙動・SQL・関連のロードロジックなど、Active Record のユーザが体感する動作は変わりません。
  • 影響は主に以下のケースでの「警告メッセージの有無」のみです:
    • Ruby 3.4 以降
    • strict_unused_block 警告を有効 (Warning[:strict_unused_block] = true など) にしている環境
  • 既存アプリケーションのコード変更は不要です。
  • ReflectionProxy を直接拡張している、あるいはモンキーパッチしているような高度なメタプログラミングをしていない限り、互換性上の問題は発生しないと考えられます。

  1. 参考情報 (あれば)
  • Ruby 3.4 の strict_unused_block 警告カテゴリは、「ブロックが呼び出し元から渡されているのに、呼び出し先で一切宣言されていない・使われていない」ケースを検出するためのものです。
  • この PR は Active Record を Ruby 3.4 の警告ポリシーに合わせるための、事実上の互換性・静的品質改善 PR です。
  • 類似の変更は、他の gem やフレームワークでも Ruby 3.4 対応として行われることが予想されます。

#46486 Preserve attachment changes when converting record to another class using STI

マージ日: 2026/5/18 | 作成者: @fatkodima

  1. 概要 (1-2文で)
    STI(Single Table Inheritance)でレコードの type を変更して別クラスに「キャスト」した際に、Active Storage の添付ファイル変更(添付・削除)が失われてしまう不具合を修正したPRです。becomes / becomes! を使ってクラスを変換した場合でも、添付ファイルの変更が正しく引き継がれるようになりました。

  1. 変更内容の詳細

問題の背景

  • Active Record では、becomes / becomes! を使って STI のサブクラス間でオブジェクトを変換できます。
    ruby
    user = User.find(1)               # type: "User"
    admin = user.becomes(AdminUser)   # type: "AdminUser" (オブジェクトのクラスが変わる)
  • しかし、Active Storage の添付(has_one_attached / has_many_attached)に対して
    ruby
    record.avatar.attach(...)
    record = record.becomes(OtherType)
    record.save
    のようにクラス変換を挟むと、「attach/delete の pending な変更」が新しいオブジェクト側にコピーされず、添付の変更が失われるケースがありました。
  • Issue #45778 でこの挙動が報告され、このPRで修正されています。

主なコード変更ポイント

1) Active Storage モデルモジュールへの対応追加

activestorage/lib/active_storage/attached/model.rb に、STI 変換時に添付の変更状態を引き継ぐためのフックが追加されています。

イメージとしては、Active Record が becomes / becomes! 時に内部的にクローンを作るタイミングで、Active Storage 側が「添付の変更情報(change オブジェクト)」を新しいインスタンスへコピーする仕組みを加えた、という形です。

具体的には以下のような処理が入っていると考えられます(実際のコードは多少異なりますが、概念的にはこういうことをしています):

ruby
module ActiveStorage::Attached::Model
  # ...
  def init_internals_for_dup
    super
    copy_active_storage_attachment_changes
  end

  private

  def copy_active_storage_attachment_changes
    # 各 attachment にぶら下がっている変更オブジェクトを
    # 新しいレコードインスタンスに紐付け直す
  end
end

ポイントは「Active Record が内部的に dup/clone するフローにフックし、Active Storage の変更状態も一緒に複製する」ことです。

2) 変更オブジェクト (CreateOne/CreateMany/DeleteOne/DeleteMany) の微調整

以下の4つのクラスに1行ずつ変更が入っています。

  • activestorage/lib/active_storage/attached/changes/create_one.rb
  • activestorage/lib/active_storage/attached/changes/create_many.rb
  • activestorage/lib/active_storage/attached/changes/delete_one.rb
  • activestorage/lib/active_storage/attached/changes/delete_many.rb

record を内部で持っているこれらの「変更オブジェクト」が、STI 変換後の新しいレコードにも正しく紐づくように、dup / clone 時の挙動や record の参照の持ち方が調整されています。

概念的には、以下のような方向の修正です:

  • 変更オブジェクトが特定のレコードインスタンスに強く結びついており、becomes 後のインスタンスとは無関係になってしまう → 新しいインスタンスに紐付け直せるようにする・あるいはレコードの差し替えができるようにする。

3) テストの追加

  • activestorage/test/models/attached/one_test.rb
  • activestorage/test/models/attached/many_test.rb

にテストが追加され、次のようなシナリオがカバーされています:

  • has_one_attached / has_many_attached を持つ STI モデルで
    1. 添付を追加 or 削除する
    2. becomes で別のサブクラスに変換する
    3. 保存する
  • このとき、添付の追加・削除が期待通り DB に反映されることを確認

また、test_helper や migration (create_users_migration.rb) に1行ずつ追加があり、テスト用に STI の型カラムや対象モデル定義が準備されています。

4) CHANGELOG の更新

  • activestorage/CHANGELOG.md に、上記バグ修正がリリースノートとして追記されています。

  1. 影響範囲・注意点
  • 対象:
    • Active Storage を利用しており、かつ Active Record の STI を使っているアプリケーション
    • 特に、becomes / becomes! / type を書き換えるロジックの中で添付ファイルの変更を行うケース
  • 振る舞いの変更点:
    • これまでは、STI クラス変換の途中で行った attach / purge などが保存時に失われることがあったが、今後は保持されるようになります。
    • すでにこのバグを前提としたワークアラウンド(例えば「becomes する前に先に save しておく」「添付変更はクラス変換とは別トランザクションにする」など)を組んでいた場合、その必要がなくなります。
  • 互換性:
    • 正しい動作に近づく方向の修正であり、通常は望ましい互換性改善です。
    • もし「STI 変換中の添付変更をあえて捨てる」ような特殊な依存をしている場合は挙動が変わる可能性があります。
  • パフォーマンス:
    • 添付の「変更オブジェクト」を複製・引き継ぐ処理が増えましたが、通常は微小なオーバーヘッドに留まると考えられます。

  1. 参考情報 (あれば)

STI モデルで Active Storage を利用している場合は、この修正が含まれる Rails バージョンにアップデートすることで、becomes 前後で添付の変更が安全に扱えるようになります。


#57375 Fix number_to_phone dropping only one char of a multi-char delimiter

マージ日: 2026/5/18 | 作成者: @55728

  1. 概要 (1-2文で)
    number_to_phone に複数文字の :delimiter を指定し、かつ市外局番なし(7桁程度)の番号を渡した場合に、先頭に区切り文字の“欠片”が残ってしまうバグを修正したPRです。slice!(0, 1) で1文字だけ削っていた処理を、指定された区切り文字列全体の長さぶん削るように変更しています。

  1. 変更内容の詳細

問題の挙動

number_to_phone:delimiter に複数文字の文字列を渡すと、番号に市外局番がないケースで先頭に中途半端な区切りが残っていました:

ruby
number_to_phone(5551234, delimiter: " - ")
# 期待: "555 - 1234"
# 実際: "- 555 - 1234"

number_to_phone(5551234, delimiter: ", ")
# 期待: "555, 1234"
# 実際: " 555, 1234"

背景として、number_to_phone 内部の convert_without_area_code では、電話番号を正規表現で3桁 + 4桁のように分解する際、先頭グループ(\d{0,3})が空になる場合があります(≒ 市外局番なしの7桁)。
このとき、内部表現として組み立てた文字列が「区切り文字 + 残り番号」のような形になり、余計な先頭区切りを slice!(0, 1) で削っていました。

しかし slice!(0, 1) は「先頭1文字だけ削る」ため、区切り文字が " - "", " のように複数文字の場合は、一文字分だけが削られて残りが先頭に残ってしまう、というのが今回のバグです。
このバグは、

  • 7桁以下(市外局番なし)
  • :delimiter が2文字以上
    の組合せでのみ発生し、かつテストは「1文字デリミタ」か「10桁番号」のケースしかカバーしていなかったため長年検知されていませんでした。

修正内容

該当処理を以下のように修正しています:

ruby
# 変更前(概念的なコード)
number.slice!(0, 1) if start_with_delimiter?(number)

# 変更後
number.slice!(0, delimiter.length) if start_with_delimiter?(number)

ポイント:

  • start_with_delimiter? の実装上、delimiter が空文字列では呼ばれないため delimiter.length >= 1 は保証されている。
  • String#slice! は文字(コードポイント)単位の削除なので、マルチバイト文字列のデリミタでも正しく動作する。

これにより、例えば:

ruby
number_to_phone(5551234, delimiter: " - ")
# => "555 - 1234"

number_to_phone(5551234, delimiter: "—")
# => "555—1234"

number_to_phone(5551234, delimiter: "・")
# => "555・1234"

のように、ASCIIでもマルチバイトでも期待どおりのフォーマットになります。

テスト追加

activesupport/test/number_helper_test.rb に以下のようなケースが追加されています(概念的な例):

  • ASCIIの複数文字デリミタ: delimiter: " - "
  • マルチバイトデリミタ: delimiter: "—" など

これにより、今回のバグパターン(7桁 + 複数文字/マルチバイトデリミタ)が今後退行しないようになっています。


  1. 影響範囲・注意点
  • 影響を受けるのは以下の条件の number_to_phone 呼び出しのみ:
    • 引数の数字が7桁程度で、市外局番(:area_code)を付けていない
    • :delimiter に2文字以上の文字列、もしくはマルチバイト文字列を指定している
  • 以下は動作が変わりません:
    • :delimiter が1文字(従来も正しく slice されていた)
    • 10桁以上の番号(米国形式の典型的な10桁番号など)
    • :area_code, :country_code, :extension を指定しているパターン
    • :delimiter が空文字の場合
  • 変更内容はユーザーに見えるフォーマットの修正であるため、CHANGELOG にも追記済みです。
  • これまで「バグを前提にしたワークアラウンド」(例: わざと先頭に空白が出る前提で文字列を post-process している等)がある場合は挙動が変わりますが、通常の利用では「期待どおりになった」だけであり互換性リスクはきわめて小さいと考えられます。

  1. 参考情報 (あれば)
  • 該当コードは ActiveSupport::NumberHelper::NumberToPhoneConverter (activesupport/lib/active_support/number_helper/number_to_phone_converter.rb) 内の、エリアコードなし用の変換ロジック。
  • 問題のもとになった slice!(0, 1) は 2011年の #3314 で導入されて以降、テストがカバーしていなかったため、今回まで放置されていました。
  • テストスイート (number_helper_test.rb) はこの修正後も全てグリーン (22 runs, 819 assertions, 0 failures)。

#56918 [RF-Docs] [ci-skip] Update the Rails on Rack guide

マージ日: 2026/5/18 | 作成者: @ayushn21

  1. 概要 (1-2文で)
    Railsガイドの「Rails on Rack」章が大幅に書き直され、現行のRailsの挙動やミドルウェア構成に即した内容へアップデートされました。Railsサーバ起動プロセス、ミドルウェアスタック、Rack APIの高度な使い方、カスタムミドルウェアの組み込み方法などが整理・拡充されています。

  1. 変更内容の詳細

2-1. 全体的な書き換え・コピー改善

  • ガイド全体の文章表現が見直され、最新のガイドラインや他のガイド(Configuration, Instrumentation など)とトーン・構成を揃えるようにリライト。
  • 古い情報や現状とズレた記述を削除・更新し、現在のRailsバージョンに合わせた内容に調整。

2-2. bin/rails server と初期化プロセスへのリンク

  • bin/rails server が内部的にどのように起動し、Rails::Server がどのように構成されるかについて、詳細説明をここに書きすぎず、代わりに Initialization ガイドの該当セクションへリンクする形に整理。

    例:

    • 「Railsサーバ起動の詳細なライフサイクル説明」は Initialization ガイド(Rails::Server start-up の節)を参照するよう誘導。
    • 本ガイドでは「RackアプリケーションとしてのRails」という観点にフォーカスさせる構成になっていると考えられます。

2-3. デフォルトミドルウェアスタックの更新

  • Rails が起動時に自動でロードする「デフォルトのRackミドルウェアスタック」の説明を最新状態に更新。
  • 各ミドルウェアについて、簡単な説明に加えて API ドキュメントへのリンクを付与:
    • 例: ActionDispatch::Static, Rack::MethodOverride, ActionDispatch::ShowExceptions, Rack::Runtime など、Rails::Application の config.middleware によって構成される標準スタックの各要素を API リファレンスに誘導。
  • Sprockets 時代の記述を修正:
    • Sprockets::Rails::QuietAssetsPropshaft::QuietAsset に更新。
      → 新しいアセットパイプライン(Propshaft)を前提にした構成へアップデートされています。

2-4. 「Internal Middleware Stack」セクションの再構成

  • 「内部ミドルウェアスタック」の章構成を、Configuration ガイドや Instrumentation ガイドと同じような見出し・表形式等に合わせて再フォーマット。
  • 開発者視点で参照しやすくするため、
    • 各ミドルウェア名
    • 役割(例:例外処理、静的ファイル配信、セッション管理、ロギングなど)
    • 設定方法(config.middleware.useconfig.action_dispatch.* など)
    • APIドキュメントへのリンク
      というスタイルに近い形に整理されたと考えられます。

2-5. カスタムミドルウェア追加方法のセクション追加

  • 独自のRackミドルウェアをRailsアプリのスタックに差し込む方法を説明するセクションが新設。

  • おそらく概ね以下のような内容・コード例が含まれています:

    ruby
    # シンプルなRackミドルウェア例
    class RequestTimer
      def initialize(app)
        @app = app
      end
    
      def call(env)
        started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
        status, headers, body = @app.call(env)
        elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
        Rails.logger.info "Request took #{elapsed} seconds"
        [status, headers, body]
      end
    end
    ruby
    # config/application.rb または特定環境の config/environments/*.rb
    class Application < Rails::Application
      # スタック末尾に追加
      config.middleware.use RequestTimer
    
      # 既存ミドルウェアの前後に挿入
      config.middleware.insert_before Rack::Runtime, RequestTimer
      config.middleware.insert_after  ActionDispatch::Executor, RequestTimer
    end
  • どのタイミング・どのクラスの前後に挿入するべきかについて、Rack の仕様と Rails のミドルウェア順序を踏まえた説明が加えられているはずです。

2-6. 「高度な使い方: コントローラ内でのRack API利用」セクション追加

  • Railsコントローラ内から低レベルのRack APIを利用する「高度な使い方」を説明するセクションが追加。

  • 主に以下のような内容がカバーされていると考えられます:

    • request.env への直接アクセス(生の Rack env ハッシュ)
    • Rack::ResponseRack::Request を使ったレスポンス構築
    • render を使わずに Rack レスポンス三要素 [status, headers, body] を返すパターンなど

    例イメージ:

    ruby
    class RawRackController < ApplicationController
      def index
        # Rack レベルの情報に直接触る
        rack_env = request.env
    
        status  = 200
        headers = { "Content-Type" => "text/plain" }
        body    = ["Hello from Rack layer"]
    
        # Rails でも Rack 互換レスポンス三要素をそのまま返せる
        [status, headers, body]
      end
    end
  • ただしこのような使い方は「普通のRailsアプリでは推奨されない、特殊用途(プロキシ、ストリーミング、低レイテンシAPIなど)」向けである旨の注意も含まれている可能性が高いです。

2-7. リソースセクション削除とリンクの本文組み込み

  • ガイド末尾の「Resources(参考リンク集)」のようなセクションを削除し、代わりに本文中の適切な場所で関連ドキュメントへ直接リンクするスタイルに変更。
  • これにより、読み進めながら自然に関連リソースに移動しやすくなっています。

  1. 影響範囲・注意点
  • コード変更はなく、ドキュメントのみの更新です。
    → 既存アプリの挙動には影響ありません。
  • ただし、以下の点は開発者が情報源として参照する際に注意が必要です:
    • デフォルトミドルウェアの情報が刷新されているため、古いガイドやブログ記事を参照している場合は、この新しいガイドを優先した方が正確です。
    • Sprockets ベースの説明から Propshaft への移行が反映されているため、Sprockets を使い続けているアプリでは一部のミドルウェア名・挙動説明が自分の環境と異なる可能性があります。
    • 「高度な使い方(Rack APIをコントローラで直叩き)」は、フレームワークの抽象レイヤをバイパスするため、保守性やテスト容易性に影響しうる点に留意が必要です。

  1. 参考情報 (あれば)

#45705 Add explicit regression tests for ActiveRecord::Migration's #say & #say_with_time

マージ日: 2026/5/18 | 作成者: @nfedyashev

  1. 概要 (1-2文で)
    ActiveRecord::Migration の #say#say_with_time に対して、明示的なリグレッションテストが追加されています。加えて、say_with_time をブロックなしで呼び出した場合に明示的なエラーになる挙動をテストで保証するようになっています。

  1. 変更内容の詳細

2-1. どこに何が追加されたか

  • 変更ファイル: activerecord/test/cases/migration_test.rb
  • 追加行数: 48行(テストコードのみ)

この PR は実装コードではなくテストコードのみを変更しており、Migration の出力周りの挙動をより厳密に検証するためのテストが追加されています。

2-2. Migration#say のテスト

元々 activerecord/test/cases/migrator_test.rb には「何回 puts が呼ばれたか」程度のテストはありましたが、この PR では以下のような点をより直接的に確認するテストが追加されていると考えられます:

  • say "message" を呼んだときに、期待どおりの文字列がログ(あるいは出力バッファ)に記録される
  • インデント(subitem: true オプションなど)がある場合、適切に字下げされたメッセージが出力される
  • verbose が無効な場合は出力されない(もしくは、出力が抑制される)こと

イメージ的には以下のようなテストが追加されているはずです(擬似コード):

ruby
def test_say_outputs_message
  migration = Class.new(ActiveRecord::Migration[7.1]) do
    def change
      say "Hello"
    end
  end.new

  output = capture(:stdout) { migration.migrate(:up) }

  assert_includes output, "==  Hello\n"
end

実際には ActiveRecord::Migration::CommandRecorder やテスト用ヘルパに合わせた書き方になっているはずですが、趣旨としては「say がどう表示されるかを直接アサートする」テストです。

2-3. Migration#say_with_time のテスト

#say_with_time は以下のようなインターフェイスを持つメソッドです:

ruby
say_with_time "Doing something" do
  # 測定対象の処理
end
  • ブロック実行前に "-- Doing something" のようなメッセージを出力
  • ブロックの実行にかかった時間を計測し、(0.1234s) のような形で表示
  • ブロックの戻り値を必要に応じて "-> 123 rows" のように後置表示

PR 説明の「after this update it fails with an explicit error in case #say_with_time was called without a block」から推測されるテスト内容:

  • say_with_time をブロックなしで呼び出した場合に、ArgumentError もしくは独自メッセージつきの例外が発生することを確認するテスト
  • 正常系として、ブロック付きで呼んだときに
    • メッセージ部分が期待どおりに表示される
    • 実行時間の表示フォーマットが期待どおりである
    • ブロックの戻り値に応じて後続情報(例: "rows")が付与される
      といった点を検証していると考えられます。

擬似コード例:

ruby
def test_say_with_time_requires_block
  migration = Class.new(ActiveRecord::Migration[7.1]) do
    def change
      say_with_time "Doing something"
    end
  end.new

  error = assert_raises(ArgumentError) do
    migration.migrate(:up)
  end

  assert_match "must be called with a block", error.message
end

実際にどの例外クラス・メッセージになっているかは実装依存ですが、「ブロックなし呼び出しを明示的に禁止する」という仕様をテストで固定しています。


  1. 影響範囲・注意点
  • 影響範囲(実装)
    この PR 自体はテストファイルのみ変更しており、Rails 本体のロジックは変更されていません。したがって、現時点ではランタイム挙動に直接の変更はありません。

  • 仕様固定による将来影響
    ただし、テストが「say_with_time をブロックなしで呼ぶと明示的エラーになる」ことを前提としているため、将来的に対応する実装変更(例: raise ArgumentError, "..." unless block_given?)が導入されることが前提に近い状態になっています。
    その結果として、

    • もし既存アプリケーションで say_with_time "msg" をブロックなしで呼んでいるコードがあれば、将来の Rails バージョンアップ時に例外で落ちる可能性があります。
    • say_with_time は「ブロックつきで使うもの」という前提で書かれており、今後はそれが明示的に強制されると考えておくとよいです。
  • 回帰テストとしての役割
    say / say_with_time 周りのフォーマット・動作(メッセージ、インデント、時間計測、エラー条件など)が、今後のリファクタリングや内部実装変更で壊れないようにする「安全網」として機能します。
    ログフォーマットやマイグレーション出力に依存したツール・スクリプトなどがある場合、仕様のズレをより早く検知できるようになります。


  1. 参考情報 (あれば)
  • 元々の類似テスト: activerecord/test/cases/migrator_test.rb
    ここでは主に puts の呼び出し回数など簡易的な検証のみ行われていた。
  • 関連 API:
    • ActiveRecord::Migration#say(message, subitem = false)
    • ActiveRecord::Migration#say_with_time(message) { ... }
      いずれもマイグレーション実行時のコンソール出力用ヘルパで、開発者が進捗表示や計測ログを簡単に出すために使うメソッド。
  • 実装側で今後入りそうな変更(推測):
    • def say_with_time(message) の先頭で raise ArgumentError, "say_with_time must be called with a block" unless block_given? のようなチェックが追加されることが予想される。

#57370 Prevent false leaked-connection reports during reaper maintenance

マージ日: 2026/5/18 | 作成者: @curi

  1. 概要 (1-2文で)
    ActiveRecord のテスト用リーク検知 (ActiveRecord::TestCase#check_connection_leaks) が、コネクションプールの Reaper 実行中に誤って「リーク」と判定してしまう問題を修正する PRです。プールの reaper_lock と同期してリークチェックを行うことで、Reaper が正当な理由で保持しているコネクションを誤検知しないようにしています。

  1. 変更内容の詳細

問題の背景

  • テストの teardown 時に ActiveRecord::TestCase#check_connection_leaks が、各 ConnectionPool をスキャンして「解放されずに残っている(スレッドがオーナーのままの)コネクション」がないかを検査しています。
  • 一方、ConnectionPool::Reaper はバックグラウンドスレッド(典型的にはスレッド名 AR Pool Reaper)として定期的にプールのメンテナンスを行い、その間 reaper_lock を取りつつコネクションを保持することがあります。
  • これまでの実装では、check_connection_leakspool.connections を読む際に pool.reaper_lock を取得しておらず、
    • Reaper がちょうどメンテナンス中にコネクションを握っている
    • そのタイミングで teardown が走ってリークチェックする
      というレースが発生すると、
      ownerAR Pool Reaper のコネクション = テストがリークさせた」と誤判定し、リークとして報告するとともに Reaper スレッドを kill してしまう、という問題が起きていました。

主な修正点

1) リークチェック処理を reaper_lock と同期

activerecord/test/cases/test_case.rb 内の check_connection_leaks 相当の処理が修正され、

  • pool.connections の走査
  • 「どのスレッドがコネクションを握っているか」の検査
    を行う直前に、pool.reaper_lock を取得してから処理するようになりました。

これにより:

  • Reaper がメンテナンス中にコネクションを保持している場合、teardown 側は reaper_lock の取得で待たされるので、Reaper がメンテを終えてロックを解放するまでリークチェックが実行されません。
  • 結果として、Reaper による「正当な一時的占有」をリークとしてカウントしなくなります。

コードイメージ(擬似コード):

ruby
def check_connection_leaks
  ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
    pool.with_reaper_lock do
      pool.connections.each do |conn|
        next unless conn.owner

        # ここで owner (スレッド名など) を見てリーク判定
        # Reaper が maintenance 中に持っているだけのケースは
        # ロック同期によりここに入ってこなくなる
      end
    end
  end
end

実際の修正は +11/-8 行程度で、既存のリークチェックロジックに reaper_lock を組み合わせる形での最小限の変更になっています。

2) 回帰テストの追加

activerecord/test/cases/test_case_test.rb に新たなテストが追加されています(+69 行)。

このテストは以下を再現するためのものです:

  • スレッド名が "AR Pool Reaper" のスレッドを立ち上げる。
  • そのスレッドが ConnectionPool::Reaper と同様にコネクションを保持してメンテナンス中の状態を模倣する。
  • その状態で ActiveRecord::TestCase のリークチェックを実行する。
  • 旧実装(origin/main)では、
    • この「メンテ中に保持しているコネクション」がリークと誤判定され、テストが失敗することを確認。
  • 新実装では、
    • reaper_lock による同期のおかげで、このケースではリーク扱いされず、テストが通る。

これにより、今回の PR が解決しようとしている「Reaper がオーナーのコネクションを誤検知する」問題の再発を防ぐことができます。


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

    • 主に ActiveRecord を使った Rails のテストコード(ActiveRecord::TestCase を継承しているテストクラス)。
    • 特に DB コネクションの Reaper(ConnectionPool::Reaper)が有効な環境で、CI などでまれに「リーク接続あり」と報告されていたケース。
  • 期待される改善:

    • CI などでときどき出ていた「AR Pool Reaper がコネクションをリークしている」というような偽陽性の failure が解消されます。
    • テストが Reaper スレッドを誤って kill することがなくなり、テスト後の状態がより安定します。
  • パフォーマンス/デッドロック懸念:

    • reaper_lock の取得は、リークチェック(テストの teardown 時)のみで、かつ短時間の読み取り処理のため、通常のアプリ実行時パフォーマンスへの影響は限定的です。
    • reaper_lock はもともと Reaper のメンテナンス処理で利用されているロックであり、今回の変更はそれに対する正しい同期を追加しただけなので、適切に設計されていればデッドロックリスクも増えていません。
  • アプリコード側での変更の必要性:

    • アプリケーション側のコードや設定を変更する必要はありません。
    • Rails の ActiveRecord テスト基盤の挙動が安全側に改善されるだけです。

  1. 参考情報 (あれば)
  • 関連 PR: #57367 の CI 実行中に、今回のリーク誤検知問題が発覚したと説明されています。
  • 実行されたテスト:
    • cd activerecord && ARCONN=postgresql bundle exec ruby -Itest test/cases/test_case_test.rb -v
    • cd activerecord && ARCONN=postgresql bundle exec ruby -Itest test/cases/reaper_test.rb
    • cd activerecord && ARCONN=postgresql bundle exec ruby -Itest test/cases/associations/has_and_belongs_to_many_associations_test.rb
    • bundle exec rubocop activerecord/test/cases/test_case.rb activerecord/test/cases/test_case_test.rb
      → ActiveRecord テスト基盤および Reaper 挙動まわりに限定した変更で、既存テストに対しても後方互換性が確認されています。

#57380 Fix pre-serialized String values skipping cast in insert_all hot path

マージ日: 2026/5/18 | 作成者: @tahsin352

  1. 概要 (1-2文で)
    insert_all の高速化パスで「シリアライズ済みの文字列」を受け取った場合に、値が二重エンコードされてしまう不具合を修正し、create と同じキャスト挙動になるようにした PR です。Hash/Array などの通常ケースでのパフォーマンス改善は維持しつつ、String の場合だけ cast をスキップしないようにしています。

  1. 変更内容の詳細

背景

  • 以前の PR #56855 で、InsertAll::Builder#values_list に「シリアライズされたカラムでは type.cast(value) をスキップする」高速パスが導入されました。
  • 目的は、serialize なカラムに対して Hash や Array を渡したときに行われる以下のような「無駄なラウンドトリップ」を避けることでした:
ruby
# Mutable#cast は実質的に:
cast(value) == deserialize(serialize(value))
# serialize: coder.dump(value)
# deserialize: coder.load(...)
  • ところが「value がすでにエンコード済みの String」の場合、この最適化により cast が完全にスキップされ、結果として DB に二重エンコードされたゴミが入るケースが発生していました。

問題の具体例

insert_all の(問題のあった)経路:

ruby
# 以前の insert_all 内部(シリアライズされたカラム + 高速パス)
SerializeCastValue.serialize(type, "already_encoded")
# => type.serialize("already_encoded")
# => coder.dump("already_encoded")       # ここでさらに dump がかかる
# => "encoded:encoded:..." のような二重エンコード

create の経路:

ruby
type.cast("already_encoded")
# => coder.load(coder.dump("already_encoded"))  # 一度 round-trip して正規化
# その後 create 側の serialize 処理
# => coder.dump(normalised_value)              # 結果として正しい一回分のエンコード

つまり:

  • create は「入力が String の場合も一度 cast する」ため、結果は常に「一回だけエンコードされた値」になる。
  • insert_all は「シリアライズカラムでは無条件に cast をスキップ」していたため、すでにエンコード済みの String にさらに dump をかけてしまい、二重エンコードが起きていた、という不整合がありました。

修正内容

修正は最小限で、以下のポリシーになりました:

シリアライズされたカラムに対して insert_all をする場合、

  • 値が String 以外(Hash, Array, Custom Object など)なら cast をスキップしてよい
  • 値が String の場合は cast をスキップせず、create と同じ処理を通す

コード上では insert_all の hot path に

ruby
unless value.is_a?(String)
  # cast をスキップして SerializeCastValue.serialize に直行
end

に相当する条件が追加され、String のときだけ type.cast(value) を経由するようになっています。

これにより:

  • Hash/Array/custom object:
    • もともと coder.dump/coder.load のラウンドトリップは「同じ Ruby オブジェクトを返すだけ」で冗長なので、引き続きスキップしてパフォーマンスを維持。
  • String:
    • 「これはすでに DB 表現なのか? ただの Ruby 文字列なのか?」という曖昧さがあるため、一度 cast を通して create と同じ規則で正規化してから serialize する。

テスト

activerecord/test/cases/insert_all_test.rb に4つのテストが追加されています。

ポイント:

  • topics.contenttext カラム + serialize)を対象にテスト。
  • ReverseCoder という「非冪等な coder」を使っている:
    • dump("foo") # => "encoded:foo"
    • もう一度 dump すると "encoded:encoded:foo" となるため、二重エンコードが起きるとすぐに検出できる。
  • これにより:
    • createinsert_all の結果が一致すること
    • 特に String を渡したときに二重エンコードにならないこと
      をアサートしています。

  1. 影響範囲・注意点
  • 対象:
    • ActiveRecord::Persistence#insert_all 系を使っていて、
    • serialize(あるいは Type::Serialized)されたカラムを持ち、
    • そのカラムに「String を直接渡している」ケース。
  • 行動差分:
    • 以前: その String がすでにエンコード済みでも、そのまま serialize されて二重エンコードになる可能性があった。
    • 以後: create と同様に必ず cast 経由になるため、一回ぶんの正規化+エンコードに収束する。
  • パフォーマンス:
    • String については、これまでスキップしていた cast を呼ぶようになるため、わずかに遅くなりますが、
      • そもそも「すでにエンコード済みの String を直接渡す」というのは一般的なユースケースではなく、
      • 非 String(Hash/Array など)については引き続き最適化が効く
        ため、全体としてはほぼ PR #56855 の性能メリットを維持します。
  • 後方互換性:
    • 「二重エンコードされた値をあえて DB に保存していた」ような極めて特殊なケースがない限り、期待される挙動は「バグ修正」として自然に改善されます。
    • createinsert_all の間で動作が揃うようになったため、API 間での一貫性は向上しています。

  1. 参考情報 (あれば)

#57382 Remove support for Rails 6.1 Active Record Marshal format

マージ日: 2026/5/18 | 作成者: @byroot

  1. 概要 (1-2文で)
    Rails 7.2 以降で、Rails 6.1 時代の Active Record オブジェクトの Marshal フォーマット(シリアライズ形式)を**「正式にはサポート対象外」とする**変更です。これにより Active Record 内部表現を将来的に大きく変更しやすくします。

  1. 変更内容の詳細

2-1. 何が「サポート終了」になったのか

  • 対象: 「Rails 6.1 の Active Record オブジェクトを Marshal.dump / Marshal.load したときのバイナリ形式」を安定仕様として維持することがやめられました。
  • 実装的には、Rails の設定オプションやガイド上の「6.1 形式をサポートします」という記述が削除・緩和されています。

ポイント:

  • いまはまだ「たまたま動いている」状態
    PR 説明にもあるとおり、現時点では「Rails 6.1 で生成された Marshal データをロードすること自体」はまだテストも残されており、動作します。
  • ただし、Active Record の内部 API (Attribute / AttributeSet など) を将来変更した際に互換性が壊れてもバグではない扱いになります。

2-2. コードレベルの変更概要

変更ファイル:

  1. activerecord/lib/active_record/marshalling.rb

    • Marshal 対応周りのサポート表現を微調整。
    • 6.1 フォーマットを安定的にサポートする、という前提を捨てる方向の変更(+2/-3)。
  2. guides/source/configuring.md

    • アプリケーション設定ガイドから、6.1 Marshal フォーマットサポートに関する記述が削除・簡略化(+2/-7)。
    • 例えば、config.active_record.use_legacy_marshalling 的な説明や、6.1 互換モードが永続的にあるかのような記述があれば、それが削られている可能性が高いです。
  3. guides/source/upgrading_ruby_on_rails.md

    • Rails バージョンアップガイドに、**「6.1 Marshal フォーマットが今後壊れる可能性がある」**という趣旨の説明・注意喚起が追加(+7)。
    • どういうケースで影響を受けるか、移行にどう備えるかといった情報が含まれていると考えられます。
  4. railties/lib/rails/application/configuration.rb

    • アプリケーション設定から 6.1 形式サポートに関わるオプションか関連コードが 1 行削除されている(+0/-1)。
    • 例: 6.1 互換モードを明示的に有効にするような設定の公式サポートがなくなった、など。

2-3. deprecation warning が出ない理由

PR 説明より:

Unfortunately I don't see any realistic solution for emitting deprecation warnings if the format is still in use by an application.

  • Marshal のバイナリフォーマットが使われているかどうかを、Rails 側から安全に・確実に検知する手段がほぼないため、
    • 「まだ 6.1 フォーマットを使っていますよ」という deprecation warning をアプリケーション実行中に出すことが現実的でないと判断されています。
  • そのため、明示的な非推奨メッセージやログなしで、あるタイミングで突然壊れる可能性がある、という性質の変更になります。

  1. 影響範囲・注意点

3-1. 影響を受ける可能性が高いケース

Rails アプリケーションで、以下のような用途で Active Record オブジェクトをそのまま Marshal.dump / Marshal.load している場合です。

  • キャッシュ層に Marshal を使っているケース
    • 例: Rails.cache.write("user:#{id}", user_record) (かつバックエンドが Redis, Memcached など)
    • これ自体は Rails が内部でやることもありますが、6.1 時代に生成した Marshal バイナリを長く保持していると、将来ロード時に壊れる可能性があります。
  • ジョブキューやバックグラウンドジョブの引数に Marshal を利用
    • 独自実装のキューや一部のライブラリで AR オブジェクトを丸ごとシリアライズしているケース。
  • 独自の永続化層に Marshal を使用
    • ファイル、Key-Value ストア、その他ストレージに AR オブジェクトを Marshal で保存している場合。

3-2. いま何が起きるか / これから何が起こりうるか

現在(この PR マージ直後):

  • Rails 6.1 で生成された Marshal バイナリを、最新版 Rails の Active Record で Marshal.load しても、まだロードは成功するように保たれています。
  • ただし「6.1 互換フォーマットを意図的に維持する」という約束がなくなったため、
    • 将来の Rails アップデートで内部表現が変わると、
    • 6.1 時代のデータが突然ロードできなくなるTypeError / ArgumentError / NoMethodError など)可能性があります。

3-3. 開発者が取るべき対策・推奨パターン

  1. AR オブジェクトを Marshal で長期保存しない

    • 長寿命のキャッシュや永続ストレージには、AR オブジェクト丸ごとではなく、必要フィールドのみ or ID のみを保存するのが安全です。
    • 例:
      ruby
      # 悪手(将来壊れやすい)
      Rails.cache.write("user:#{id}", user_record)
      
      # 推奨: ID だけキャッシュして、必要時に取り直す
      Rails.cache.write("user_id_for_something:#{id}", user_record.id)
      
      # または属性ハッシュに落として保存(自前で互換性管理できるなら)
      Rails.cache.write("user_snapshot:#{id}", user_record.attributes)
  2. どうしてもシリアライズが必要なら JSON / MessagePack 等に寄せる

    • as_json / to_json、あるいは ActiveModel::Serializers::JSON などを利用して、アプリ側でスキーマ互換性を管理しやすいフォーマットにしておくほうが将来のアップグレードで安全です。
  3. 既存の Marshal データの「ガーベジ期間」を設ける

    • 6.1 時代から残っている可能性のある古いキャッシュ・キュー・ストレージを、
      • TTL を短くする
      • ロールアウト前に一度クリアする
    • などで「古いフォーマットが環境内に残り続けない」ようにしておくと、将来の互換性問題を避けやすくなります。
  4. 独自シリアライズ層をラップする

    • どうしてもバイナリシリアライズが必要な場合、
      • バージョン番号付きで自前のラッパー形式({version: 1, payload: ...})を用意する
      • Active Record オブジェクトを、そのラッパーが理解できるプレーンデータ構造に変換する
    • など、Rails の内部表現に依存しないシリアライズ方式に移行するのが望ましいです。

  1. 参考情報 (あれば)
  • この PR が修正として言及している PR:
    https://github.com/rails/rails/pull/53362
    (Active Record の Marshal 関連の挙動・後方互換性に関する問題が背景にあると考えられます)

  • 対応ファイルの役割:

    • activerecord/lib/active_record/marshalling.rb
      Active Record オブジェクトの Marshal 対応(_dump/_load 周り)のコア実装。
    • guides/source/configuring.md
      Rails アプリの設定方法ガイド。6.1 互換モードに関する説明の削除・簡略化。
    • guides/source/upgrading_ruby_on_rails.md
      Rails のバージョンアップ時に確認すべき項目(今回の Marshal フォーマット非サポート化の注意が追加)。
    • railties/lib/rails/application/configuration.rb
      config.* で設定できる項目の定義。6.1 Marshal 関連の設定が削除されている可能性。

この PR の実質的な意味は「6.1 Marshal フォーマットを、今後の Rails バージョンアップにおける互換性保証の対象から外した」という宣言なので、AR オブジェクトの Marshal 利用をしているかどうかを改めて棚卸ししておくのが安全です。


#56565 Revert "Sort schema cache columns and indexes per table when dumping"

マージ日: 2026/5/18 | 作成者: @byroot

  1. 概要 (1-2文で)
    schema_cache.yml 生成時にテーブルごとにカラム・インデックスをソートしていた変更 (#54960) を元に戻し、スキーマキャッシュ内では定義順を維持するようにした PR です。
    理由は、スキーマキャッシュのソートが pluck("*") などカラム順に依存する挙動を変えてしまい、実際にバグを引き起こし得るためです。

  1. 変更内容の詳細

元に戻したもの(revert の対象)

  • 以前の PR #54960 では、以下を行っていました:
    • schema.rb の出力時に、テーブル内のカラムやインデックスをソート
    • schema_cache.yml(スキーマキャッシュ)にも同様のソートロジックを適用

今回の PR はこのうち「スキーマキャッシュ側のソート」をやめています(schema.rb 側はこの PR では触っていない)。

コードレベルでの変更点(概念的な説明)

対象ファイル:

  • activerecord/lib/active_record/connection_adapters/schema_cache.rb
  • activerecord/test/cases/connection_adapters/schema_cache_test.rb

差分は少ないですが、意味合いとしては:

以前(revert 前)のイメージ

ruby
# 疑似コードイメージ
def columns(table_name)
  @columns[table_name].sort_by!(&:name) if @columns[table_name]
end

def indexes(table_name)
  @indexes[table_name].sort_by!(&:name) if @indexes[table_name]
end

今回の PR 後(revert 後)のイメージ

ruby
# ソートは行わず、DB / schema 定義順をそのまま保持
def columns(table_name)
  @columns[table_name]
end

def indexes(table_name)
  @indexes[table_name]
end

テスト側 (schema_cache_test.rb) では、

  • カラムやインデックスが「ソートされていること」を前提としていたテストを削除・修正し、
  • 「定義順が維持されること」を前提とした挙動に戻しています。

  1. 影響範囲・注意点

影響するもの

  • schema_cache.yml の内容の順序

    • カラム・インデックスの順番が、以前の(Rails 7 などの)挙動と同じく「定義順」に戻ります。
    • すでに schema cache をコミットしている場合、再生成すると diff に「順序の変更」が出る可能性があります(ただし、実行時挙動を安定させるための変更)。
  • カラム順に依存するコード

    • 代表例として PR 説明に挙がっているのが pluck("*") です。
    • pluck("*") は「DB から返ってくるカラム順」を前提としているケースがあり、スキーマキャッシュ内でカラムをソートしてしまうと、
      • 期待する順序と異なる順で値が返る
      • 位置ベースで配列を解釈しているコードが壊れる
        といった不具合が発生し得ます。
    • 今回の revert により、そのような順序依存コードは「以前の Rails の挙動」に戻るため、潜在バグを回避する方向です。

なぜ schema.rb と扱いを分けるのか

  • schema.rb:
    • Git コンフリクト削減のために「ソートしたい」というニーズがある。
    • かつ、順序を保持したい場合の「エスケープハッチ(設定・オプション)」が用意されている。
  • schema_cache.yml:
    • 主にランタイム最適化用の内部キャッシュであり、Git コンフリクトの問題は相対的に小さい。
    • 一方、ここでのソートは挙動そのもの(カラム順依存のコード)を変えてしまう。
    • かつ、順序を保持するためのエスケープハッチがない。
      → そのため「スキーマキャッシュをソートする変更」はリスクが高く、今回 revert された。

注意点

  • Rails のアップデート時に:
    • schema_cache.yml を再生成するタイミングで、ファイル上の順序が変わる可能性があります。
    • もし、独自に schema cache を解析するツールやメタプログラミングをしている場合、順序に依存していないかを確認する価値があります(依存しているなら、今回の変更はむしろ安定方向)。

  1. 参考情報 (あれば)
  • 元のソート導入 PR:
    • rails/rails#54960 — 「Sort schema cache columns and indexes per table when dumping」
  • 本 PR:
    • rails/rails#56565 — 「Revert "Sort schema cache columns and indexes per table when dumping"」
  • スキーマキャッシュ関連のドキュメント(英語):
    • Rails Guides: Active Record Migrations -> Schema dumping and loading
    • ActiveRecord::ConnectionAdapters::SchemaCache クラスの RDoc / API ドキュメント

#57267 Fix bulk job and email enqueueing methods with no arguments

マージ日: 2026/5/17 | 作成者: @fatkodima

  1. 概要 (1-2文で)
    Action Mailer と Active Job の「引数なし」での一括エンキュー (*_all, *_later など) が正しく動作しない問題 (#57264) を、内部ロジックの配置を整理することで修正した PR です。実質的な仕様変更や挙動追加はなく、「コードの場所を変えてバグを解消した」リファクタリング寄りの修正です。

  1. 変更内容の詳細

何が問題だったか (Issue #57264 の概要)

  • Active Job / Action Mailer には
    • 複数ジョブをまとめてキューに積む「bulk enqueue」
    • メール送信を一括で deliver_later する
      ための API があり、引数なしで呼び出した場合の挙動に不具合があった。
  • 例として、ジョブ/メールの enqueuing をラップするメソッドが「可変長引数」や「ブロック引数」を前提にしており、引数なし時のパスが正しくカバーされていない/再利用しづらい場所にあった、という類の問題が Issue 側で議論されている。

この PR では、そのロジックをより適切な場所に「移動」することで、引数なしのケースも自然に面倒を見られるようにしている、という形です。


実際の変更点

1) Active Job 側 (activejob/lib/active_job.rb, activejob/lib/active_job/enqueuing.rb)

  • ActiveJob::Enqueuing モジュールにあった一部の enqueuing ロジックが ActiveJob 本体 (active_job.rb) に移動。

  • 具体的には、以下のような「クラスメソッド系の enqueue ヘルパ」が ActiveJob 側に集約されたと考えられます(擬似コードイメージ):

    ruby
    # 以前: active_job/enqueuing.rb にあった
    module ActiveJob::Enqueuing
      module ClassMethods
        def perform_all_later(jobs)
          # ...
        end
      end
    end
    
    # 以後: active_job.rb 側に移動
    class ActiveJob::Base
      class << self
        def perform_all_later(jobs)
          # 同じ実装をこちらに移動
        end
      end
    end
  • これにより:

    • ActiveJob::Base クラスメソッドとしての一括エンキュー API の見通しが良くなる
    • 引数なしや単一ジョブなどのバリエーションへの対応が「クラス本体側」で完結し、モジュール経由の呼び出しとの齟齬が減る

enqueuing.rb の該当コードは削除されている(-30行)ので、API 自体はそのままに、定義位置だけが変わった形です。

2) Action Mailer 側 (actionmailer/lib/action_mailer.rb, actionmailer/lib/action_mailer/message_delivery.rb)

  • これまで ActionMailer::MessageDelivery(個々のメールオブジェクト)側にあった一部の「enqueue 関連コード」が、ActionMailer 本体 (action_mailer.rb) に移動。

  • 典型的には deliver_later, deliver_later!, あるいは「メールを Active Job に乗せる」ための共通処理と、その bulk 版をまとめている箇所です。

  • 擬似コードイメージ:

    ruby
    # 以前: message_delivery.rb
    class ActionMailer::MessageDelivery
      def enqueue_delivery
        # ...
      end
    end
    
    # 以後: action_mailer.rb
    module ActionMailer
      class Base
        def self.enqueue_delivery(message)
          # 同じ処理をこちらに移動
        end
      end
    end
  • メールを一括で deliver_later するようなヘルパも、ActionMailer 側に寄せられたことで、引数なし(あるいはデフォルト値のみ)での呼び出しを含めた全パターンが正しく処理されるようになっています。


  1. 影響範囲・注意点

影響範囲

  • 対象:
    • Active Job の一括エンキュー API(perform_all_later など、クラスメソッド経由の enqueue helper)
    • Action Mailer の一括 deliver_later 系 API(内部で Active Job に乗せる部分)
  • これらを引数なし/引数オプション少なめで呼び出していた場合、以前はエラーや不正な挙動になっていたケースが、正常に動作するようになります。

下位互換性

  • PR 説明の通り「コードの移動のみ」で、メソッドシグネチャや公開 API は変えていないと明言されています。
  • ただし以下のようなケースでは影響の可能性があります:
    • Rails 内部の ActiveJob::Enqueuing / ActionMailer::MessageDeliveryprivate メソッドや内部構造に依存していたメタプログラミング をしている
    • これらのクラス/モジュールを reopen して、内部メソッドを monkey patch していた

このような「内部実装に強く依存した拡張」をしている場合は、Rails のアップデート後に一度動作確認を行うことを推奨します。

対応すべきこと

  • 通常のアプリケーションコードであれば、特別な対応は不要です。
  • 以下に心当たりがあれば、アップデート後にテストを重点的に確認:
    • 独自 wrapper で Active Job の bulk enqueue を叩いている
    • Mailer まわりで deliver_later をラップして独自ログやメトリクスを仕込んでいる
    • job/mailer の enqueue execution chain を monkey patch している

  1. 参考情報 (あれば)

Issue 側に、「どのメソッドがどのような引数で失敗したか」「どのようなユースケースで顕在化したか」が記載されているので、自分のコードに影響がありそうか判断したい場合は Issue をあわせて読むと把握しやすいです。


#54587 Expose all BatchEnumerator attributes

マージ日: 2026/5/17 | 作成者: @fatkodima

  1. 概要 (1-2文で)
    ActiveRecord::Batches::BatchEnumerator が内部的に持っていた cursor, order, use_ranges を、外部から読み取れる公開属性として追加し、バッチ処理の挙動に応じた高度なカスタマイズができるようになりました。これにより、バッチイテレーションの状態や戦略を外部のライブラリ/アプリケーションから参照可能になります。

  1. 変更内容の詳細

2-1. 何が変わったか

BatchEnumerator に以下の3つの属性リーダーが追加されています。

  • cursor
  • order
  • use_ranges

もともと一部の属性は attr_reader で公開されていましたが、今回のPRで「すべての属性を公開したい」というニーズに応え、残りの内部属性も外から参照できるように統一されています。

対象クラスは以下です:

ruby
# activerecord/lib/active_record/relation/batches/batch_enumerator.rb
class ActiveRecord::Batches::BatchEnumerator
  # ここに attr_reader :cursor, :order, :use_ranges が追加されたイメージ
end

加えて、activerecord/test/cases/batches_test.rb にテストが追加され、BatchEnumerator のインスタンスからこれらの属性が参照できることが検証されています。

2-2. どう使えるようになるか (サンプルコード)

例えば、in_batches を使った処理の中で、Enumerator 自体にアクセスし、その設定を元に処理を分岐させることができます。

ruby
# 例: in_batches から BatchEnumerator を取得
enumerator = User.in_batches(of: 1000, order: :id, use_ranges: true)

# 新しく公開された属性にアクセス
puts enumerator.cursor      # => 現在のカーソル情報(id など)
puts enumerator.order       # => :id など、バッチ処理に使われている order
puts enumerator.use_ranges  # => true / false(範囲ベースのバッチ処理かどうか)

enumerator.each_record do |user|
  # enumerator の状態によって処理を変えたい場合
  if enumerator.use_ranges
    # 範囲ベースのバッチに対するロジック
  else
    # オフセット/IDベースなど別のロジック
  end
end

あるいは、バッチ処理をラップする gem や社内ライブラリ側で、BatchEnumerator を受け取って、その設定内容をログに出したり、再実行用のメタデータとして保存したりといった利用も想定されています。


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

    • 既存の挙動は変わっておらず、「新しい public な読み取り用属性が増えた」だけなので、通常のアプリケーションコードに対する破壊的変更はありません。
    • これまで内部実装に直接依存していた(インスタンス変数に直接触れる、send で無理やり読みに行く等)コードがあった場合は、これら新しい属性リーダーを使うことで、より安全で将来の変更に強い実装に置き換えられます。
  • APIの安定化傾向

    • 元々内部実装寄りだった情報を公式に公開した形になるため、cursor, order, use_ranges は今後も ActiveRecord のパブリック API としてある程度の安定性が期待できます。
    • これらに依存したライブラリ/gem(PR説明にあるように、作者の gem など)も、より安心して BatchEnumerator を利用できるようになります。
  • 利用時の前提

    • cursor の具体的な中身や意味は、利用しているバッチ戦略(ID カーソル、レンジなど)に依存します。実体のクラスや構造に過度に依存せず、「カーソルとして使える値」程度の抽象度で扱うのが安全です。
    • use_ranges が true の場合、find_each / in_batches が ID の範囲やカスタムレンジを使った処理になっていることを前提にしたロジックが書けますが、実装詳細には依存しすぎないように注意してください。

  1. 参考情報 (あれば)
  • このPRは #42312 のフォローアップとして行われており、そこで BatchEnumerator の一部属性がすでに公開されていた流れがあります。
  • 対象PR: https://github.com/rails/rails/pull/54587
  • BatchEnumerator の基本的な使い方は ActiveRecord::Batches (find_each, in_batches) のドキュメント・ガイドも合わせて確認すると理解しやすいです。

#45201 Adds ability to get the combined bytes_size of all attached blobs

マージ日: 2026/5/17 | 作成者: @sandip-mane

  1. 概要 (1-2文で)
    Active Storage の has_many_attached に対して、紐づいている複数ファイルの総サイズ(バイト数)を一括で取得できる byte_size メソッドが追加されました。これにより、Gallery.new.photos.byte_size のようにして、複数添付ファイルの合計容量を簡単に取得できます。

  1. 変更内容の詳細

追加された機能

ActiveStorage::Attached::Manybyte_size メソッドが追加され、複数添付(has_many_attached)に対して以下のような呼び出しが可能になります。

ruby
class Gallery < ApplicationRecord
  has_many_attached :photos
end

gallery = Gallery.find(1)
gallery.photos.byte_size # => photos に紐づく全 Blob の byte_size 合計 (Integer)

単一添付(has_one_attached)に対しては以前から record.attachment.byte_size が使えていましたが、今回の変更で「複数添付にも同じインターフェースで合計サイズを取得できる」ようになった形です。

実装イメージ

PR 本文からは詳細なコードは示されていませんが、ActiveStorage::Attached::Many に、内部で attachments あるいは blobs を辿って合計を取るような実装が追加されています。概ね下記のような処理が行われていると考えられます。

ruby
def byte_size
  attachments.sum { |attachment| attachment.blob.byte_size }
end

テスト (activestorage/test/models/attached/many_test.rb) によって、以下が確認されているはずです。

  • 複数ファイルを添付した場合、各 Blob の byte_size の合計が返ること
  • 添付が無い場合に期待される挙動(0 を返すか、nil を返すか)が明確になっていること
    (Active Storage の一貫性からすると 0 を返す実装である可能性が高い)

また、activestorage/CHANGELOG.md にもこの機能追加が記載され、次バージョンの変更点として明示されています。


  1. 影響範囲・注意点
  • 対象:
    • Rails の Active Storage を利用しており、has_many_attached を使っているすべてのアプリケーションで利用可能な新 API です。
  • 互換性:
    • 既存の API を壊す変更ではなく、メソッド追加のみの後方互換性のある変更です。
  • パフォーマンス面の注意:
    • byte_size は内部で関連する Blob 情報を参照するため、
      • 添付ファイル数が非常に多い
      • N+1 クエリが発生する状況で呼び出す といった場合には、クエリ数やメモリアクセスが増える可能性があります。
    • 大量データで頻繁に呼ぶ場合は、includes(:blob) などで事前ロードしておく、キャッシュする、といった対策を検討してください。
  • 空の場合の挙動:
    • 添付が1つもないときの返り値が仕様としてどうなるか(0 か nil か)を、実際のコード/ドキュメントで確認したうえで利用すると安心です。
      通常は「サイズ合計」という意味合いから 0 が自然ですが、アプリのロジック上、nil チェックが必要なケースもあり得ます。

  1. 参考情報 (あれば)
  • Rails (Active Storage) の CHANGELOG:
    • activestorage/CHANGELOG.md に今回の変更が追記されており、利用可能になる Rails バージョンが確認できます。
  • 類似 API との関係:
    • 単一添付: user.avatar.byte_size
    • 複数添付: gallery.photos.byte_size(今回新たに追加)
  • 合計サイズからの派生利用例:
    • MB/GB 単位に変換して UI 表示する:
      ruby
      size_in_mb = gallery.photos.byte_size.to_f / (1024 * 1024)
    • 上限チェックに使う:
      ruby
      if gallery.photos.byte_size > 100.megabytes
        errors.add(:photos, "の合計サイズが 100MB を超えています")
      end

#44746 Fix ActiveRecord::QueryMethods#in_order_of when passing an out-of-range Integer.

マージ日: 2026/5/17 | 作成者: @tejanium

  1. 概要 (1–2文で)
    ActiveRecord::QueryMethods#in_order_of に、整数型カラムに対して範囲外の整数を渡したときに ActiveModel::RangeError が発生する不具合があり、それを修正する PR です。修正後は、範囲外など「シリアライズ不可能な値」は無視され、エラーを出さずに ORDER BY 句が構築されます。

  1. 変更内容の詳細

問題となっていた挙動

in_order_of は、指定したフィールドに対して与えられた値の配列順にレコードを並べるためのクエリメソッドです:

ruby
Post.in_order_of(:id, [3, 1, 2])
# => id が 3, 1, 2 の順になるように ORDER BY を組み立てる

しかし、整数カラム (integer, bigint など) に対して、カラム型の範囲を超える大きな整数を渡すと、ActiveModel::Type::Integer によるキャストで ActiveModel::RangeError が発生していました。

ruby
Post.in_order_of(:id, [1, 9999999999999])
# ActiveModel::RangeError: 9999999999999 is out of range for ActiveModel::Type::Integer with limit 4 bytes

["1", "9999999999999"] のように文字列で渡しても、内部でキャストされるため同様にエラーになります。

この挙動は、以前の PR (#43322) によって in_order_of が渡された全ての値を一律に type_cast_for_database でキャストするようになったことが原因です。
その結果、DB 上で表現できない値(int4 に対する 9999999999999 など)も無理にキャストしようとして例外が飛ぶようになっていました。

修正方針

この PR では、次のような変更が行われています。

  • これまで:
    • type_cast_for_database を使って、in_order_of に渡された全ての値を DB 向けにキャストしていた
    • → 範囲外の整数などもキャスト対象になり、ActiveModel::RangeError が発生
  • これから:
    • type_cast_for_database の代わりに、caster.serialize(value) if caster.serializable?(value) という条件付きシリアライズを使用
    • serializable?(value)false を返す値(型・範囲が不適切な値)は「無効な値」とみなし、シリアライズせずに無視する

具体的には、in_order_of が内部で Arel の CASE 式相当の ORDER BY を構築する際、
「この値は現在のカラム型に対してシリアライズ可能か?」を serializable? でチェックし、
シリアライズ不可能なら CASE 式からその値を除外するようにしています。

このアプローチは、Arel 内の他の実装と揃えるためのものでもあります:

これらと同様に、「キャストできない値は ORDER 条件から外す」という方針に統一されています。

動作イメージ(修正後の挙動)

※テストコード自体は PR からは全文わかりませんが、挙動はおおよそ以下のようになります。

ruby
# id は 4byte integer (PostgreSQL で integer / MySQL で int など) とする

Post.in_order_of(:id, [1, 9999999999999])
# これまでは RangeError を raise していたが、
# 修正後は 9999999999999 が無効値として無視される。
# 実質的に [1] だけが ORDER の対象になる。
#
# ORDER BY CASE
#   WHEN "posts"."id" = 1 THEN 0
# END ASC, "posts"."id" ASC  -- のようなイメージ

Post.in_order_of(:id, ["1", "9999999999999"])
# "1" はキャスト可能なので含まれるが、
# "9999999999999" はキャスト不可のため無視される。

  1. 影響範囲・注意点
  • 影響範囲:
    • ActiveRecord::Relation#in_order_of を整数カラムなどに対して使用しており、かつクエリ引数にカラム型の範囲外の整数や、型にマッチしない値が混ざる可能性がある場合に挙動が変わります。
  • 以前との違い:
    • 以前: 範囲外や不正な値が 1 つでも含まれると ActiveModel::RangeError が発生してクエリ自体が失敗
    • 今回: そのような値は「無効値」として ORDER 句から無視され、残りの有効値だけを元に順序づけが行われる
  • 注意点:
    • 「不正な値を渡したらエラーで気づきたい」という用途には向かなくなります。
      in_order_of を入力検証の代わりに使うのではなく、コントローラやフォームオブジェクト側で別途バリデーションするのがよいです。
    • 無効値がすべて除外された結果、in_order_of の指定リストが空になった場合は、実質的に通常の order(デフォルトのスコープ内 order)だけが適用されるような挙動になります。
    • 「ユーザー入力をそのまま in_order_of に流す」ようなケースでは、これまでエラーになっていたケースが「静かに無視される」ようになる点を理解しておく必要があります。

  1. 参考情報 (あれば)

#57107 [RF-Docs][ci-skip] Caching with Rails

マージ日: 2026/5/17 | 作成者: @Ridhwana

  1. 概要 (1-2文で)
    Rails公式ガイド「Caching with Rails」の内容を、現行のRailsの挙動に合わせて大幅に書き直し・再構成したドキュメント改善PRです。既存の構成やトーンは維持しつつ、Rails.cache、フラグメントキャッシュ、Conditional GET、SQLキャッシュ、Solid Cache、ETagなどの解説がかなり具体的・実践的になりました。

  1. 変更内容の詳細

ガイド全体の構成・イントロの見直し

  • 冒頭の導入文と「このガイドを読んだ後にわかること」を、実際の内容に即して書き直し。
  • 「Types of Caching」セクションの並びを整理し、学びやすい順に再構成:
    1. ローレベルキャッシュ (Rails.cache)
    2. フラグメントキャッシュ
    3. Conditional GET
    4. SQL(Active Recordクエリ)キャッシュ

ローレベルキャッシュ (Rails.cache) の拡充

Rails.cache.fetch とヒット/ミス

  • cache.fetch の基本的な挙動(ミス時にブロック実行して保存、以後は保存済み値を返す)を丁寧に説明。
  • 「キャッシュヒット」「キャッシュミス」の概念が明示的に説明されるようになりました。
ruby
Rails.cache.fetch("articles/#{article.id}") do
  # キャッシュミス時にだけ実行される
  expensive_query_for_article(article.id)
end

キャッシュクリア

  • 全体をクリアする Rails.cache.clear の説明が追加。
    • どのような状況で使うか、開発・本番での扱いに触れている可能性が高いです。

キャッシュキーと cache_key_with_version

  • Active Recordオブジェクトの cache_key_with_version を用いたキー生成ロジックを詳しく説明。
    • レコードのid + 更新時刻などから一意なキーを生成し、「内容が変わったらキーが変わる」というRails流の依存関係管理の基礎を整理。
  • これに基づき、「どのようにキー設計をすると、オブジェクトの更新に追従しつつキャッシュの整合性を保ちやすいか」を解説。

Active Recordオブジェクトをそのままキャッシュしない理由

  • 「ARオブジェクトを丸ごとキャッシュするのは通常よくない」という点を明示的に解説。
    • 例: 接続状態・lazy loadされた関連・環境差(プロセス間など)で壊れやすい。
    • 一般にはシリアライズしやすいプレーンなデータ構造(Hash / JSONなど)をキャッシュする方が安全、という指針が強調されていると考えられます。

fetch の高度なオプション(race_condition_ttl など)

  • race_condition_ttl 等のオプションの使い方を追記し、「スラッシング(多数のプロセスが同時にミスして高負荷になる)」を避けるためのパターンを説明。
ruby
Rails.cache.fetch("stats", expires_in: 10.minutes, race_condition_ttl: 30.seconds) do
  heavy_stats_calculation
end

フラグメントキャッシュ周りの改善

テンプレートツリーダイジェスト

  • ビューの「ツリー」単位でのダイジェスト(テンプレート+部分テンプレート)について説明が強化。
    • テンプレートの内容変更に応じて自動的にフラグメントキーが変わる仕組みを、より明示的に記述。

コレクションキャッシュ

  • render @articlesrender partial: "article", collection: @articles のようなコレクションレンダリング時のキャッシュ挙動を詳細に説明。
    • 各要素ごとのキャッシュキー
    • コレクション全体のキー/expires_in などオプションがどう解釈されるか
    • 個々の要素の更新がコレクション表示にどう影響するか
erb
<%= render partial: "articles/article", collection: @articles, cached: true %>

などのパターンで、キー・有効期限がどう構成されるかの解説が整理されているはずです。


依存関係管理の整理(フラグメントキャッシュ配下へ移動)

  • 依存関係管理に関する説明をフラグメントキャッシュの章の中へ移動し、内容も書き直し。
  • 次の3種類を区別して説明:
    • 暗黙の依存(テンプレートのツリーダイジェストに含まれるもの)
    • 明示的な依存(cache_key_with_version などで直接キーに組み込む)
    • 外部依存(ヘルパー経由で値が決まるなど、テンプレート外の要素に依存するもの)
  • ヘルパー経由のレンダリング時に、どのようにキャッシュキーを構成すると安全か、などが明確化。

ロシアンドールキャッシュと touch: true

  • 「ロシアンドールキャッシュ」(入れ子構造のフラグメント)の説明が整理され、touch: true の役割が明確に。
    • 親モデルが has_many :comments, touch: true のように設定されていると、子の更新時に親のupdated_atが変わり、親フラグメントのキーが変わる→キャッシュ更新される、という流れを丁寧に説明。

共有パーシャルのキャッシュ(MIMEタイプや解決ルール)

  • 複数フォーマット(HTML, JSON など)で共有されるパーシャルをキャッシュする際の挙動を再説明。
    • MIMEタイプごとに別キャッシュになるのか
    • パーシャル解決(どのパーシャルファイルが選ばれるか)がキャッシュキーへどう反映されるか
  • これにより、APIとHTMLで同じパーシャルを使う場合などの混乱を減らす意図があります。

Conditional GET(HTTPキャッシュ)セクションの拡充

stale?fresh_when

  • 両メソッドの役割の違いと使い分けがより具体的に説明されました。
    • stale? : 条件付きリクエストヘッダとサーバ側のETag/Last-Modifiedを比較し、「レスポンス生成が必要か?」を判定するインタフェース。
    • fresh_when : ETag/Last-Modifiedの設定と条件付きレスポンスの判定を包括的に行う高レベルメソッド。
ruby
def show
  @article = Article.find(params[:id])

  if stale?(@article)
    # ここに来たときのみ、テンプレートをレンダリング
    render
  end
end
ruby
def show
  @article = Article.find(params[:id])

  fresh_when(@article)
end

strict_freshness

  • 現在の strict_freshness の挙動について説明を強化。
    • ETagやLast-Modifiedの扱いをより厳格にするオプションや設定がある場合、その意味が明文化されています。

http_cache_forever

  • http_cache_forever が適切となるケース(ほぼ不変のアセットや静的コンテンツなど)と、その注意点を明示。

強いETagと弱いETag

  • 新しく「strong vs weak ETags」のサブセクションを追加。
    • 強いETag: レスポンスボディが完全に同一であることを前提に比較される。
    • 弱いETag: 意味的に同じであれば良いが、バイトレベルでは変わっていてもよい、という比較。
  • ブラウザ・HTTPキャッシュとの相互運用や、どちらを使うべきかの指針を示しています。

SQLキャッシュ(Active Recordクエリキャッシュ)の明確化

  • SQLキャッシュが「Active Record query caching」であることを明確にし、リクエストや実行コンテキストの中で自動的に働く仕組みを説明。
    • 同一リクエスト中で同じSQLが複数回実行される場合、二度目以降はキャッシュされた結果が使われる。
    • どの境界(Rackミドルウェア・ジョブ・コンソール等)でクエリキャッシュが有効かをより理解しやすくなっています。

Solid Cache セクションの更新

  • Railsの最新デフォルト構成と用語に合わせて説明を更新。
    • rails new で生成されるアプリがどうSolid Cacheを設定しているかを反映。
  • データベース設定例を更新し、実際のconfig/database.ymlconfig.cache_storeの例を提示。
  • 開発環境でのセットアップガイドを追記。
  • トランザクションとキャッシュストア選択に関する表現をより正確にし、どのパターンでSolid Cacheを選択すべきかがわかりやすくなりました。

Advanced Caching Patterns(新セクション)

バックグラウンドジョブや非リクエストコンテキストでのキャッシュ

  • Active Job / Sidekiq等のジョブ内や、Rakeタスク、コンソール、サービスオブジェクトなど「リクエスト以外のコンテキスト」でのキャッシュの使い方を整理。
    • リクエスト境界に依存するクエリキャッシュやローカルキャッシュとの違い。
    • そのコンテキストで Rails.cache をどう扱うべきか。

ローカルキャッシュと Rails.cache.with_local_cache

  • ローカルキャッシュの挙動 (ActiveSupport::Cache::Strategy::LocalCache) を説明。
  • Rails.cache.with_local_cache do ... end を使うことで、そのブロック中だけ同プロセス内での超高速メモリキャッシュを利用できることを解説。
ruby
Rails.cache.with_local_cache do
  100.times do
    Rails.cache.fetch("expensive") { expensive_operation }
  end
end
  • 上記のようなケースで、リモートストアへの往復を抑えて性能を向上できる点などを説明していると考えられます。

その他ドキュメント修正

  • guides/source/documents.yaml を更新して、キャッシングガイドのタイトル・アンカーが最新構成に合うよう修正。
  • ガイド内部の参照・アンカー(特にETag周りの見出し/リンク)を修正して、リンク切れや古い見出し名を解消。

  1. 影響範囲・注意点
  • コード挙動の変更はなく、あくまで公式ドキュメントの更新です。

    • 本PRのマージ自体がアプリケーションの動作を変えることはありません。
  • ただし以下の観点で「実務的な影響」はあります:

    • 既に「古いガイド」に基づいてパターンを決めているチームは、Active Recordオブジェクトの直接キャッシュやロシアンドールキャッシュの設計・ETagの扱いなどを見直すきっかけになる。
    • Solid Cacheやwith_local_cacherace_condition_ttlなど、これまであまり使っていなかった高度なオプションを導入しやすくなる。
    • Conditional GET / ETagの理解がアップデートされることで、APIやSPA向けのHTTPキャッシュ設計が改善できる可能性が高い。
  • 注意点として:

    • ガイドに書かれているベストプラクティスは「最新のRailsを前提」としているため、古いRailsバージョン (5系, 6.0など) では一部のオプション・デフォルト挙動が異なる可能性があります。
    • 特に strict_freshnesshttp_cache_forever、Solid Cache周りはバージョン依存の差分を意識して読む必要があります。

  1. 参考情報 (あれば)
  • 対象ガイド: guides/source/caching_with_rails.md
    → 最新のRails公式ガイド(英語)を読むと、本PRで説明された内容がすべて反映されています。
  • 関連トピック:
    • ActiveSupport::Cache の各ストア実装(MemCacheStore, RedisCacheStore, SolidCacheStoreなど)
    • HTTP仕様のETag / Last-Modified / Conditional GET (If-None-Match, If-Modified-Since)
    • Active Recordクエリキャッシュ (config.active_record.cache_versioning 等の設定)

#56341 [RF Docs] Active Record Query Interface

マージ日: 2026/5/17 | 作成者: @Ridhwana

  1. 概要 (1–2文で)
  • Active Record Query Interface ガイド全体を大幅に書き直し・整理し、用語・構成・サンプルコード・相互リンクを現状の Rails に合わせてアップデートしたドキュメント改善 PRです。
  • 併せて ActiveRecord::Relation#only / #except まわりなど、クエリメソッドのドキュメントコメントも微調整され、複合主キーガイドとの連携も強化されています。

  1. 変更内容の詳細

2.1 Active Record Query Interface ガイド全体の再構成

guides/source/active_record_querying.md が約 2,200 行追加・1,400 行削除されており、実質的に「全面改訂」に近い変更です。主なポイント:

構成・見出しの整理

  • 冒頭:
    • 「What is the Active Record Query Interface?」直後にあった書店サンプルを別の適切なセクションへ移動。
    • コード例の前にあった小ネタ的な「Tip」の位置を移動し、「モデルの一覧を示す導入文 → サンプルコード」という流れを阻害しないように整理。
  • セクション名の明確化:
    • Conditions → 「Where 条件 / Filtering Results with where」のような、クエリの意味が一目でわかる名称に変更。
    • Ordering → 「Ordering Results」的な名前へ変更し、「結果のソート」であることを明確化。
    • Overriding Conditionswhere 以外の句も対象であることを踏まえ、「Overriding SQL Clauses」等、より包括的な名前に変更。
  • 「Retrieving Objects from the Database」:
    • これまで手薄だった「複数レコード取得」の説明を補強。
    • Customer.all のような基本例を追加し、その後に where, order, limit, group などの各節につながるようにガイド全体のストーリーを再構成。
    • Dynamic Finders 部分を、find_by の説明の直後に移動し、「単一レコード取得」の文脈にまとめて配置。
  • 「Selecting Specific Fields」セクション:
    • distinct の説明がここにあるのは不自然という指摘に基づき、LIMIT/OFFSET 周辺、もしくは「Limiting Results」的なセクションへ再配置して整合性を改善。

ガイド内リンク・他ガイドへのリンク強化

  • SQL 知識への言及:
    • Active Record Basics 同様、「SQL をある程度知っているとこのガイドが読みやすい」という旨の注意書きを追加。
    • SQL 学習用の外部リソースリンクを追加。
  • 他の Rails ガイドとのクロスリンク:
    • 複合主キーに言及している箇所から active_record_composite_primary_keys.md へのリンクを追加。
    • transaction, locking など、他ガイドのトピックについては、少なくとも検索でヒットして他ガイドに辿れるよう、導入部にまとめてリンクを配置する方向に。
  • セキュリティガイドへのリンク:
    • "conditions with strings" セクションで SQL インジェクションを警告している箇所から Security Guide の該当セクションへリンク。
    • where に配列引数を渡すセクションと合わせて記述を少し簡潔にしつつ、インジェクション回避のベストプラクティスを明確化。

サンプルコードの見直し

  • boolean の扱い:
    • WHERE active = 0/1 のような例を false/true に統一し、DB 実装に依存しない・Rails 的に読みやすい形に修正。
  • 日付・時刻の例:
    • created_at を日付扱いして group する例が実際には「timestamp で group している」問題を是正。
      • DB 依存の DATE(created_at) などを多用せずに済むよう、statusstatus, customer_id など、日付変換不要なカラムでのグルーピングに置き換え。
  • 時間範囲 (Hash range conditions) の例:
    • Date.yesterday.all_day などのシンタックスは簡潔だが、all_day が基本的に UTC で評価されることに起因するタイムゾーン混乱を避けるため、採用しない判断。
    • 代わりに Time.now.midnight / 実運用を意識するなら Time.zone.now.midnight を使った例にして、「Time.zone を前提にしたタイムゾーン意識」のメッセージを強化。
  • join の例:
    • has_and_belongs_to_manyjoin_table を書くかどうかは意見が割れたが、「現実的で包括的な例を見せたい」という意図から現状維持を選択。
  • eager_load の例:
    • 「2 クエリ実行されるように見えるが実際にはそうならない」不正確な例を修正するための別 PR (#51940) が前提の形になっていた箇所を調整。
  • lock, with_lock, references:
    • これらのメソッドを紹介している箇所から、それぞれの API ドキュメントへのリンクを追加。
  • スコープの例:
    • out_of_print_and_expensiveout_of_print.costs_more_than(500) で再定義できることを踏まえ、スコープのチェーンを理解しやすい示し方に整理。
    • スコープとクラスメソッドの類似性を説明する節で、「self を返すクラスメソッドはスコープとほぼ同様に振る舞う」ことを明示した例を追加。

メソッド・機能のカバレッジ拡張

  • only / except:
    • ガイド内で only に触れていた部分に加え、except も存在することを説明。
    • only は指定した句だけを残す」「except は指定した句だけを取り除く」という SpawnMethods の役割を理解しやすいように説明を補強。
  • reorder:
    • 「default_scope で指定された order を上書きする」だけでなく、「それまでにチェーンされたすべての order を上書きする」ことを明確に記述。
  • create_with:
    • Finder メソッドの一覧から create_with を削除。create_with は検索ではなく「後続の create 用のデフォルト値を指定する」メソッドであり、分類上誤解を招くため。
  • create_or_find_by / create_or_find_by!:
    • 既存の find_or_create_by / find_or_create_by! に加え、新しい create_or_find_by / create_or_find_by! をガイドに追加。
    • find_or_create_by がトランザクションを跨いだときにレースコンディションで二重作成し得る非原子的な性質を明記。
    • それに対し create_or_find_by は DB の一意制約エラーを利用して、原子性を保ちつつ「作成 or 取得」を実現するテクニックであること、およびユニークインデックス前提であることを説明。
  • pluck, pick, ids, find_by_sql:
    • それぞれを一括して「Finding by SQL」という名前に置くのは誤解を招くため、find_by_sql に専用の見出しを用意。
    • pluck/pick/ids は「特定カラムの値を配列で取得する」系のユーティリティとして、「Retrieving Objects from the Database」内に「Retrieving Columns」的な節を新設して整理。
  • メソッドチェーンの注意点:
    • 「Method Chaining」の節に、Rails コンソールで inspectto_a が自動的に呼ばれ、想定よりも早くクエリが実行されることがあるという注意書きを追加。
    • Active Record パターンと Method Chaining の関係について、「Active Record パターン自体が Method Chaining を意味するわけではない」ことを明確化し、代わりに Martin Fowler の「Fluent Interface」に言及。

ロッキング・トランザクション・パフォーマンス関連

  • ロッキング・トランザクション:
    • lock/with_lock などの基本的な説明は残しつつ、「本来はトランザクションやロッキングをまとめた新しいガイド(Active Model Transactions & Locking)で扱うべき」という方向性を明言し、この PR では移動せず据え置き。
    • 別タスクとして Basecamp 上に「Active Model Transactions & Locking - Documentation Updates」を登録済み。
  • パフォーマンス:
    • EXPLAIN 実行や eager loading など、パフォーマンスに強く紐づくトピックを「Active Record Performance」ガイドへ移す構想を記述。
    • この PR ではまだ分離せず、今後のフォローアップで切り出す方針。

2.2 Active Record Composite Primary Keys ガイドの更新

guides/source/active_record_composite_primary_keys.md にも 30 行程度の追記があります。ポイント:

  • Query Interface ガイドからのリンク先としての役割を明確化。
  • クエリ生成・where 条件・order など、複合主キーを用いる際に典型的にハマりやすい点を補足するテキストを追加(主にドキュメント相互参照のための微調整)。

2.3 クエリメソッド (query_methods.rb) のコメント調整

activerecord/lib/active_record/relation/query_methods.rb に +8/-8 の変更が入っていますが、実装ロジックではなくドキュメントレベルの変更です。

  • only, except, reorder など、この PR でガイド側の説明を強化したメソッドの doc コメントを合わせて調整。
  • 意味合いをガイドと揃えるための文言修正(例: reorder が「default_scope だけでなく既存の order すべてを置き換える」ことをより明確に表現)。

  1. 影響範囲・注意点
  • 実装コードへの影響:
    • query_methods.rb の変更はコメントのみであり、挙動変更や API 変更はありません。アプリケーションコードが壊れる可能性は実質ゼロです。
  • ドキュメントを参照している二次資料への影響:
    • ガイドの見出し名やセクション構造がかなり変わっているため、特定セクションへのリンク(アンカー)を持つブログ記事・社内 Wiki・スライド等は、リンク切れや内容不一致が発生する可能性があります。
    • 特に「Conditions」「Ordering」「Finding by SQL」など、名前や位置が変わった箇所は要確認です。
  • ベストプラクティスの変化:
    • find_or_create_by の非原子的な性質と、create_or_find_by 推奨の流れが明示されたため、古いコードサンプルをそのまま教科書にしている場合は見直しを検討したほうが良いです。
    • boolean を 0/1 ではなく true/false で扱うサンプルが標準になったので、ガイドに合わせるなら新規コードは論理値ベースで書くのが望ましいです。
  • 時刻・タイムゾーン関連:
    • all_day/beginning_of_day をあえて避けている説明があるため、「Rails の Time.zone / config.time_zone 設定と all_day の挙動」をしっかり把握していないと、安易に真似るとサンプルと違う結果を得ることがあります。
    • ガイドは慎重な立場を取っているので、運用では Time.zone.now を前提に range を構成する習慣を持つことが推奨されます。

  1. 参考情報 (あれば)

#43736 Introduce **options argument in the DestroyAssociationAsyncJob#perform method

マージ日: 2026/5/16 | 作成者: @smt116

  1. 概要 (1-2文で)
    ActiveRecord::DestroyAssociationAsyncJob#perform の引数に **options が追加され、将来的にオプションを拡張しても既存アプリを壊さないようにするための後方互換性改善が行われました。実質的な挙動は現状変わらず、ごく小さなシグネチャ変更のみです。

  1. 変更内容の詳細

activerecord/lib/active_record/destroy_association_async_job.rbperform メソッドの引数リストが、キーワード引数を余分に受け取れるように変更されました。

想定される変更イメージは以下のようなものです(実際のコードはほぼこれと同等です):

ruby
# 変更前(イメージ)
def perform(owner_model_name, owner_id, association_name)
  # ...
end

# 変更後(イメージ)
def perform(owner_model_name, owner_id, association_name, **options)
  # options は現状は使われないが、引数として受け取れる
  # ...
end

これにより、将来的に以下のようにオプション付きでジョブがキューイングされても:

ruby
DestroyAssociationAsyncJob.perform_later(
  "User",
  1,
  "posts",
  some_flag: true,
  timeout: 30
)

既存の perform 実装を壊さずに、追加のオプションを受け取れる余地ができます。
現段階では **options は内部で利用されていない(あるいは少なくともこのPRでは使われていない)ため、動作は従来と同じです。

背景として、関連PR/コメント(#42452 のレビューなど)で「後からキーワード引数を追加すると、既存のメソッドシグネチャと衝突して互換性問題が起きる」ことが指摘されており、その対策として最初から **options を受ける形にしておく、という方針になっています。


  1. 影響範囲・注意点
  • 既存コードへの影響

    • 既存の DestroyAssociationAsyncJob を直接使っているアプリは、そのままでも挙動は変わりません。
    • すでに perform をオーバーライドしている場合は、Rails 側の定義とシグネチャの差異に注意が必要です。
      • 例: アプリ側で

        ruby
        def perform(owner_model_name, owner_id, association_name)
        end

        と定義していても Ruby 的には問題なく動きますが、将来オプションを利用する形に移行したい場合は:

        ruby
        def perform(owner_model_name, owner_id, association_name, **options)
        end

        と揃えておくと安全です。

  • 将来の拡張性

    • Rails 本体がこのジョブに新しいキーワード引数オプションを追加しても、**options が受け止めるため、メソッド呼び出し側との互換性が保ちやすくなります。
    • gem やアプリが独自にこのジョブをラップしてオプションを付加するようなユースケースにも対応しやすくなります。
  • 非互換リスクの軽減

    • キーワード引数周りは Ruby 2.7〜3.0 で挙動が変わっており、将来的に Rails が新しいキーワード引数を追加した際に、「位置引数/キーワード引数の不一致」によるエラーを避ける狙いがあります。

  1. 参考情報 (あれば)

#56842 Revert: Sort table columns by name when dumping schema

マージ日: 2026/5/15 | 作成者: @bert-mccutchen

  1. 概要 (1-2文で)
    スキーマダンプ時にテーブルのカラムを名前順にソートする挙動を導入した変更(#53281)を、丸ごと取り消すPRです。これにより、schema.rb などでのカラムの出力順は、従来どおり「DBが返す順序(定義順)」に戻ります。

  1. 変更内容の詳細

2-1. 何をリバートしたか

PR本文より:

  • 「Reverted all code changes made in #53281.」
  • 「Updated the ActiveRecord CHANGELOG.」

つまり、#53281 で導入された「カラムを名前でソートする」ロジックと、それに紐づくテスト変更を削除し、挙動を元に戻した上で、Active Record の CHANGELOG にその旨を追記しています。

実際の差分(要約):

  • activerecord/lib/active_record/schema_dumper.rb
    • カラム出力時に columns.sort_by(&:name) のような処理をしていた部分を削除し、ソートしない元の実装に戻している(+1/-1 行なので、ほぼ 1 箇所の挙動差)。
  • activerecord/test/cases/schema_dumper_test.rb
    • 「カラムが名前順に並んでいること」を前提にしたテストを削除(-8 行)。
  • activerecord/CHANGELOG.md
    • このリバートに関する変更点を記載(+6 行)。

2-2. 元の変更 (#53281) の文脈

元の PR (#53281) は、「スキーマダンプ時にテーブルカラムを名前順で並べる」ことで、「マイグレーションの順番や定義の揺れによる schema.rb の差分ノイズを減らしたい/安定化したい」という動機で導入されたものと考えられます。

しかし、本 PR (#56842) では:

Based on my findings, the original approach of sorting schema columns is not the appropriate solution to the problem the original developer was facing.

とあり、当初の問題の解決策として「ダンプ時にカラムをソートする」のは不適切である、との結論になっています。


  1. 影響範囲・注意点

3-1. schema.rb / structure.sql の出力順が元に戻る

  • Rails 7.2 以降のどこかのタイミング(#53281 マージ以降)で一度「カラムが名前順で出力される」状態を経験している場合、この PR が入ったバージョンでは、再び「DBの定義順(追加した順など)」で出力される挙動に戻ります。
  • そのため、Rails のバージョンアップ時に db:schema:dump などを実行すると、カラム順の変化による大量の差分が出る可能性があります
    • 例: users テーブルの email, name, created_at の順にダンプされていたものが、DB 定義順に戻ることで name, email, created_at のように並びが変わる、など。

3-2. 差分ノイズ対策をカラムソート前提で組んでいた場合

  • 独自ツールや CI 等で「schema.rb の差分がないか」を見ている場合、以前「名前順ソート」を前提にしてロジックを書いていたなら、今回のリバートによって前提が崩れます。
  • カラム順の安定性をどう担保するかは、アプリケーション側/運用側のポリシーに委ねられる形に戻ります:
    • 例: マイグレーションの定義順を厳格に管理する
    • 例: 比較時に「行の並びを無視」するような diff ツールを用いる など

3-3. テストが壊れる可能性

  • アプリ側テストで、schema.rb などを読み取り「カラムがアルファベット順で書かれていること」を前提にしたチェックを書いていた場合、それらは失敗するようになります。
  • 同様に、生成されたスキーマのテキストを丸ごと snapshot テストしている場合も、カラム順の変化で snapshot の更新が必要になる可能性があります。

  1. 参考情報 (あれば)

#57374 Revert "Add default #render_in implementation to ActiveModel::Conversion"

マージ日: 2026/5/15 | 作成者: @seanpdoyle

  1. 概要 (1-2文で)
    ActiveModel::Conversion に追加されていたデフォルト実装の #render_in を、一旦まるごと巻き戻した PR です。まだ未リリースの機能だったため、コントローラ起点のパーシャル名前空間解決に関する問題を踏まえて、仕様を見直した上で再度出し直す方針になっています。

  1. 変更内容の詳細

なにを Revert したか

元 PR(#57349)で導入されていた以下の変更がすべて取り消されています。

  • ActiveModel::Conversion#render_in のデフォルト実装の追加
  • それに伴うテスト (conversion_test など) の追加
  • ActiveModel::Lint によるインターフェースチェックの変更
  • ガイド (active_model_basics.md) における render_in 関連の記述
  • CHANGELOG のエントリ

コード的には、以下のようなイメージで追加されていたものが、まるごと削除された状態になります(擬似コード):

ruby
module ActiveModel
  module Conversion
    # これがまるごと無かったことになる
    def render_in(view_context, &block)
      view_context.render(partial: to_partial_path, object: self, &block)
    end
  end
end

これにより、ActiveModel::Conversion を include しただけのオブジェクトは、render_in を持たない元の状態に戻ります(ViewComponent などの「render_in を前提とする仕組み」に、そのまま ActiveModel を乗せる想定だった部分が撤回された形)。

Revert の理由

元 PR に対するコメントで、

render 呼び出しが、レンダーコンテキストにもとづく「コントローラ単位でのパーシャル名前空間付け(暗黙のディレクトリ解決)」を考慮していない

という指摘がありました。

Rails では、コントローラから render :foo / render "foo" / render partial: "foo" などを呼んだ場合、暗黙に以下のような名前空間解決が行われます:

  • PostsControllerindex から render :postapp/views/posts/_post.html.* が優先される
  • ネストされたコントローラ・エンジンなどのコンテキストを含めた lookup path が使われる

今回の ActiveModel::Conversion#render_in の実装では、この「呼び出し元コントローラ・テンプレートに応じたパーシャル名前空間の解決」が適切に反映されていなかったため、一旦リリース前に取り消し、問題を織り込んだ形で再設計することになっています。


  1. 影響範囲・注意点
  • リリース済みのバージョンには影響なし
    ActiveModel::Conversion#render_in はまだどのリリースにも含まれていない状態だったため、公開済みの Rails を使っているアプリケーションには直接の破壊的変更はありません。

  • render_in を ActiveModel に期待する設計は現状できない
    今回の revert により、以下のような使い方は引き続き非サポートです:

    ruby
    class User
      include ActiveModel::Model
      include ActiveModel::Conversion
    
      # to_partial_path は従来通りだが…
      def to_partial_path
        "users/user"
      end
    end
    
    user = User.new
    # NG: まだ user.render_in(view_context) は定義されない
    user.render_in(view_context)

    「ActiveModel を include していれば render_in も使える」という API にはなっていない点に注意が必要です。
    render_in を使いたい場合は、従来通り ViewComponent など、render_in を実装している専用のクラス/gem を利用する必要があります。

  • ドキュメントも元に戻っている
    guides/source/active_model_basics.md から render_in に関する記述が削除されているため、最新ガイドを参照している限り、この機能を前提とした実装例は登場しません。

  • 将来的には改めて導入される可能性が高い
    PR の本文で「subsequent re-submission(後続の再投稿)」と明言されているため、

    • コントローラコンテキストに応じたパーシャル名前空間解決
    • 既存の to_partial_path / render の挙動との整合性 を考慮した上で、改めて ActiveModel::Conversion#render_in が提案される見込みです。

  1. 参考情報 (あれば)
  • Reverted 元 PR:
  • 問題指摘コメント:
  • 現状の ActiveModel::Conversion の役割:
    • #to_model / #to_key / #to_param / #to_partial_path などを提供し、「Rails の view / routing と相互運用しやすい plain Ruby object」を定義するためのモジュールであり、render_in はまだ正式な一部ではない状態に戻りました。

#57364 Enumerate columns in SELECT when only_columns is set

マージ日: 2026/5/15 | 作成者: @sarub0b0

  1. 概要 (1-2文で)
    only_columns を設定したときに、SQL の SELECT 句が table.* のままになってしまう不具合を修正し、ドキュメントどおり「指定したカラムだけを取得する」挙動になるようにした PR です。ignored_columns 使用時と同様に、only_columns でも SELECT 句でカラムを列挙するようにしています。

  1. 変更内容の詳細

何が問題だったか

only_columnsignored_columns の逆の機能として導入されていますが、挙動に不整合がありました。

  • only_columns 設定時:
    • load_schema! 内で columns_hash は正しくフィルタされる
      model.column_names などでは期待どおりのカラムだけが見える
    • しかしクエリは SELECT "table".* FROM ... のまま
      → DB からは全カラムが返り、結果的にレコードには全カラムが埋まる

例:

ruby
class User < ApplicationRecord
  self.only_columns = %w[id name]
end

User.all.to_sql
# 期待: SELECT "users"."id", "users"."name" FROM "users"
# 実際: SELECT "users".* FROM "users"

User.first.attributes.keys
# 期待: ["id", "name"]
# 実際: ["id", "name", "email", "created_at", "updated_at"]

原因は ActiveRecord::QueryMethods#build_select

  • ignored_columns がある場合
  • enumerate_columns_in_select_statements が有効な場合

のみ SELECT 句を明示的に列挙する分岐に入っており、only_columns はその判定に含まれていなかったためです。


実際の変更内容

activerecord/lib/active_record/relation/query_methods.rbbuild_select 内の条件を 1 箇所だけ変更:

ruby
- elsif model.ignored_columns.any? || model.enumerate_columns_in_select_statements
+ elsif model.ignored_columns.any? || model.only_columns.any? || model.enumerate_columns_in_select_statements
  arel.project(*model.column_names.map { |field| table[field] })

ポイント:

  • model.column_namescolumns_hash を元にしており、load_schema! 時点で only_columns によりすでにフィルタ済み。
  • そのため、only_columns 用に特別な処理を追加する必要はなく、「明示的に列挙するかどうか」の条件に model.only_columns.any? を足すだけでよい。

この変更により:

ruby
class User < ApplicationRecord
  self.only_columns = %w[id name]
end

User.all.to_sql
# => SELECT "users"."id", "users"."name" FROM "users"

User.first.attributes.keys
# => ["id", "name"]

という、ドキュメントどおりの挙動になります。


テスト追加

activerecord/test/cases/base_test.rb にテストを追加:

  • 既存の ignored_columns_not_included_in_SELECT テストのすぐそばに

  • 既存の OnlyColumnsDeveloper フィクスチャモデルを利用

  • テスト内容のイメージ:

    • モデルに self.only_columns = ... を設定
    • to_sql を評価して SELECT 句に指定カラムだけが列挙されていることを確認

新テストは:

  • この修正あり → パス
  • この修正なし → 失敗

となることを SQLite3 / PostgreSQL / MySQL で確認済みです。


  1. 影響範囲・注意点
  • 対象: ActiveRecordonly_columns を利用しているモデル
  • 実際の挙動変化:
    • これまで: only_columns を設定しても DB レベルでは SELECT * が発行されていた
    • これから: SELECT 句でカラムが列挙され、DB からも指定カラムのみ取得される

主な影響:

  • 期待される挙動に修正されるため、基本的には「バグ修正」です。
  • ただし、これまで「only_columns を設定しているが、実は裏で SELECT * が効いていた」ことで成り立っていたコードがある場合は挙動が変わります:
    • 例: only_columns = %w[id name] にもかかわらず、record.email など「含めていないはずのカラム」にアクセスしていた場合
      → 今後はそのカラムは SELECT されず、nil になるか、場合によっては attribute 未定義に近い扱いになります(アプリのコード・バージョンによる)。
  • パフォーマンス面では:
    • カラム数が多いテーブルで only_columns を使っている場合、今回の修正により本来の目的どおり「不要なカラムを取得しない」形になり、ネットワーク転送量・デシリアライズの削減につながります。

注意点:

  • only_columns は「取得するカラムをホワイトリストする」ものであり、「それ以外のカラムにアプリケーション側から普通にアクセスできる」ことは今後保証されません。
  • ignored_columns と同様に、「スキーマ的には存在しているがアプリケーションからは見せたくない/使いたくない」カラムの制御に利用する想定です。

  1. 参考情報 (あれば)
  • 対応する既存機能: ignored_columns, enumerate_columns_in_select_statements
  • 関連 PR: only_columns の導入 PR #55121
  • 今回の変更は「ドキュメントどおりの挙動に合わせる」ものであり、新しい公開 API の追加や互換性のない仕様変更ではなく、バグフィックス扱いです。

#57372 Remove caller supplied :object option in render_in ActiveModel helper

マージ日: 2026/5/15 | 作成者: @tahsin352

  1. 概要 (1-2文で)
    ActiveModel の render_in ヘルパーで、呼び出し元が :object オプションを渡しても無視し、常に self を描画対象オブジェクトとして使うようにした PR です。これにより、キーワード引数の重複エラーや、意図せず self が上書きされる挙動を防ぎます。

  1. 変更内容の詳細

背景となる問題

ActiveModel の Conversion モジュールには、ViewComponent などで使われる render_in(view_context, **options) 的なヘルパー(to_partial_path と組み合わせるレンダリングサポート)があり、内部で次のような呼び出しをしていると想定できます:

ruby
view_context.render(object: self, **options)

ここで、呼び出し側が options の中に object: を含めてしまうと、Ruby のキーワード引数ルール上、2つの問題が起きます。

  • render(object: self, **{ object: other })
    • Ruby レベルでは ArgumentError: duplicated keyword argument :object が発生する
  • もしくは、内部で **options をそのまま受け取るようなラッパーがいると、
    • 呼び出し元の object:self を上書きしてしまう(本来「self が優先」という設計意図と逆)

この PR は「render_in 系のヘルパーでは必ず selfobject: として使い、呼び出し元は object: をいじれないようにする」ことで、この2つの問題をまとめて解消します。

実際の変更点

activemodel/lib/active_model/conversion.rb に 1 行だけ変更が入り、render_in 相当のメソッドで options から :object キーを取り除く処理が追加されています(options = options.except(:object) のようなイメージ)。

擬似コードで表すと、以前は:

ruby
def render_in(view_context, **options)
  view_context.render(object: self, **options)
end

だったものが、今回の PR により次のような動きになります:

ruby
def render_in(view_context, **options)
  # 呼び出し元が渡した :object は必ず削除
  options = options.except(:object)

  # 常に self が object として使われる
  view_context.render(object: self, **options)
end

これにより、次のような呼び出しは:

ruby
person_component.render_in(view_context, object: other_person, locals: { foo: :bar })

内部的には:

ruby
view_context.render(object: person_component, locals: { foo: :bar })

として扱われ、object: other_person は完全に無視されます。

テストの追加

activemodel/test/cases/conversion_test.rb に 18 行のテストが追加され、以下のような点が検証されています:

  • render_inobject: を渡しても、レンダリング対象は常に self であること
  • 呼び出し元が object: を含めても例外にならず、かつ self が上書きされないこと
  • その他のオプション(locals, partial, layout など)は従来通り素通しされること

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

    • ActiveModel の render_in(およびそれに依存する機能)を使っているコード
    • 特に、render_in 呼び出し時に object: をオプションとして渡していたコード
  • 互換性:

    • これまで「たまたま」render_in(..., object: something_else) が通っていて、self ではなく別オブジェクトを object: として使えていたケースは、今回の PR により必ず self が使われるようになります。
    • 本来の設計意図(「render_in は自分自身をレンダリングする」)に沿った締め付けなので、仕様としては妥当ですが、そうした“裏技”的な利用は壊れます。
  • 注意点:

    • render_in 経由でレンダリング対象を差し替えたいユースケースがある場合は、render を直接呼ぶ、あるいは別のヘルパー/APIを用意すべきです。
    • もし既存コードで render_in(..., object: ...) としている箇所があれば、それはもはや意味を持たず無視されることになるので、リファクタリングの対象になります。

  1. 参考情報 (あれば)
  • フォローアップ元の PR:
    https://github.com/rails/rails/pull/57349
    render_in 周りの挙動・インターフェイスを整理している一連の変更の一部)

  • Ruby のキーワード引数と **options の仕様:

    • foo(bar: 1, **{ bar: 2 }) のような呼び方は ArgumentError: duplicated keyword argument :bar になる
    • これを避けるためには、**options 展開前に options.delete(:bar) などで重複を取り除く必要がある

#48628 PostgreSQL reload virtual columns on update via RETURNING clause

マージ日: 2026/5/14 | 作成者: @abaldwin88

  1. 概要 (1-2文で)
    PostgreSQL を使う場合、Active Record が UPDATE クエリに RETURNING 句を付けて実行し、DB 側で計算・更新される「仮想カラム(virtual columns, generated columns)」を自動的に再ロードするようになりました。これにより、更新直後のモデル上で仮想カラムの値を正しく参照するために reload を呼ぶ必要がなくなります。

  1. 変更内容の詳細

2-1. 何ができるようになったか

PostgreSQL で「stored な virtual column(generated column)」を使っている場合:

ruby
create_table :posts do |t|
  t.integer :upvotes_count
  t.integer :downvotes_count
  t.virtual :total_votes_count, type: :integer,
                                as: "upvotes_count + downvotes_count",
                                stored: true
end

今後は、以下のようなコードで:

ruby
post = Post.find(1)
post.update(upvotes_count: 2, downvotes_count: 2)

# ここで `post.reload` は不要
post.total_votes_count # => 4 になる

update 成功直後の post オブジェクトに、DB が計算した total_votes_count が反映されるようになります。

2-2. 実装の概要

主なポイントは「UPDATE + RETURNING」で更新後の値を一度に取得して、その結果でレコードの属性を更新するようにしたことです。

a. 抽象アダプタ側の拡張

  • ActiveRecord::ConnectionAdapters::DatabaseStatements / AbstractAdapter に「UPDATE 結果を返すためのフック」が追加されています。
  • PostgreSQL アダプタはこのフックを実装し、UPDATE 文に RETURNING を付けた SQL を組み立て・実行し、返ってきた行を ActiveRecord に返すようになっています。
  • MySQL / SQLite アダプタ側にもメソッドシグネチャの変更など最小限の対応が入っていますが、RETURNING による自動リロードは「未サポート」のままです。

b. カラム情報に「virtual かどうか」を持たせる

  • ActiveRecord::ConnectionAdapters::Column に「virtual カラム」であるかどうかを示すフラグが追加されています。
  • スキーマ情報(model_schema.rb など)から、t.virtual で定義されたカラムがわかるようになり、PostgreSQL アダプタでもそれを認識できるようになっています。

c. UPDATE 時に返してもらうカラムの選択

  • ActiveRecord::Persistence の更新ロジックが拡張され、「どのカラムを RETURNING で返してもらうか」を決定する処理が入っています。
    • 基本的には「仮想カラム(stored virtual / generated columns)」を RETURNING 対象に含めます。
    • それ以外にも、必要に応じてデフォルト値のあるカラムや、DB 側で書き換わる可能性のあるカラムを RETURNING させる準備が入っています(関連 PR を踏まえた一連の流れ)。

UPDATE 実行後、返ってきた行の値で該当属性を上書きするため、Ruby 側のオブジェクトが DB 上の最新の状態になります。

d. Arel 側の対応

  • arel/visitors/postgresql.rb に変更が入り、PostgreSQL 用の Arel Visitor が RETURNING 句付き UPDATE に対応しました。
  • 対応に合わせて activerecord/test/cases/arel/visitors/postgres_test.rb が追加され、UPDATE + RETURNING の SQL 生成がテストされています。

e. テスト・その他

  • 仮想カラム (Column#virtual? 的な仕様) に対するテストが追加。
  • dirty_test.rb / persistence_test.rb に、「更新後に virtual column の値が正しく反映されるか」「余計な reload をしなくても良いか」などのテストが追加。
  • SQLite 用スキーマ定義(sqlite_specific_schema.rb)などが、virtual カラムまわりの振る舞いに合わせて微調整されています。
  • activerecord/CHANGELOG.md に、この新挙動が追記されています。

  1. 影響範囲・注意点

  2. 対象は PostgreSQL のみ

    • MySQL / SQLite アダプタには現時点でこの挙動は実装されていません。
    • マルチ DB で、「PostgreSQL のときだけ update 後に virtual column が自動更新される」という差異が生じます。
  3. 対象は virtual / generated columns(特に stored)

    • Rails の t.virtual ... stored: true で定義されているカラムが主対象です。
    • DB 側で値が計算される(トリガーや DEFAULT 式など)カラムについても、今後/周辺 PR で RETURNING を利用する設計が進んでいるため、仮想カラム以外でも挙動が変わる可能性がありますが、本 PR は主に virtual columns 向けです。
  4. reload が不要になるが、既存コードはそのままでも安全

    • これまで post.update(...); post.reload と書いていたコードは、そのままでも動作しますが、単純にDB往復が増えるだけになります。
    • パフォーマンスを気にする場合は、PostgreSQL + virtual column 使用箇所では reload を削れるか検討する余地があります。
  5. SQL の形が変わる可能性

    • PostgreSQL での UPDATE 文に RETURNING 句が付くようになります。
    • カスタムログ集計や、SQL パターンに依存した処理(プロキシ、監査、FWのシグネチャなど)を書いている場合は、SQL 形状の変化に注意してください。
  6. dirty tracking との整合性が改善

    • DB 側で書き換わる virtual column について、更新直後にモデルオブジェクトが最新状態を持つようになるため、changes, saved_changes などの結果がより直感的になります。
    • とはいえ、virtual column 自体は「保存対象の属性」というより「結果属性」のため、どこまで dirty tracking に期待するかは仕様理解が必要です。

  1. 参考情報 (あれば)
  • この PR は以下の PR / Issue と関連しています:
    • Follow-up: PR #48241
    • Related: PR #48434
    • Closes: Issue #48423(PostgreSQL 使用時)
  • 機能としては「PostgreSQL の UPDATE ... RETURNING による楽観的な再読み込み」を ActiveRecord レベルで一般化していく流れの一部です。
  • PostgreSQL の GENERATED ALWAYS AS (...) STORED カラムや、Rails の t.virtual ... stored: true を多用しているアプリケーションでは、更新後の属性不整合・reload 忘れによるバグを減らしつつ、DB 往復を減らせる変更になります。

#56855 fix: skip coder round-trip for Hash/Array values during insert_all

マージ日: 2026/5/14 | 作成者: @elasticspoon

  1. 概要 (1-2文で)
    insert_all / insert_all! がシリアライズ属性(YAML/JSON など)を扱う際に、不要な cast(YAML.dump → YAML.load の往復)をスキップするホットパスを入れて、性能劣化を解消した PR です。通常の属性代入など他の挙動に影響を与えず、insert_all のみを高速化します。

  1. 変更内容の詳細

2-1. 問題の背景

  • 対象: シリアライズ属性(serialize, typed_store, JSON/YAML カラムなど)を持つテーブルに対する insert_all / insert_all!

  • 回帰(regression)の発生元: PR #48139

    • そこでは、insert/insert_all が通常の create と同じ DB 表現になるように、型の正規化(cast)を追加した。
    • 例:
      ruby
      # YAML シリアライズを想定
      Book.create!(name: ["Array"])
      # => DB には "---\n- Array\n" が保存される
      
      Book.insert!({ name: ["Array"] })
      # => 以前は '["Array"]' など、別フォーマットで保存されていた
    • この不整合を直すため、insert! でも cast を通すようになった。その insert! が内部で insert_all! を使うため、insert_all にも cast が入るようになった。
  • 問題: シリアライズ型は ActiveModel::Type::Helpers::Mutable を include しており、cast(value) が次のように定義されている:

    ruby
    def cast(value)
      deserialize(serialize(value))
    end
    • つまり、毎回 serialize(例: YAML.dump)→ deserialize(例: YAML.load)を行う
    • insert_all に渡す値はすでに Hash / Array であることが多く、本来であれば DB へのシリアライズだけで済むところを、余計な往復変換を挟んでいた。
    • その結果、YAML/JSON カラムを含む大規模 insert_all が、raw SQL に比べて約 1.8 倍遅くなっていた。

2-2. 検討された別案(却下されたアプローチ)

  • Serialized 型の cast をオーバーライドし、値がすでに Hash/Array の場合はショートサーキットして deserialize(serialize()) をスキップする案を試した。
  • しかしこれは「属性代入」全体に影響し、次のような問題が起きる:
    ruby
    # JSON coder を想定
    Book.content = { a: :b }
    # cast をサボると、メモリ上では { a: :b } のままになる
    Book.save!
    # DB 保存時には {"a" => "b"} になる
    • Rails のシリアライズ属性は「メモリ上の値も正規化(キーを String 化など)」されることを前提としており、ここを変えると dirty tracking やアプリロジックに影響するため不適切。

2-3. 採用された解決策

  • グローバルな cast の挙動はいじらず、insert_all の内部だけでホットパスを追加する。

  • insert_all は「DB に直接書き込むだけ」で、メモリ上の属性オブジェクト状態は気にしなくてよい場面。
    そのため、既に「シリアライズ済み(serialized?)」と判断できる型については、deserialize(serialize()) を省略してよい

  • 実装イメージ(挙動レベルの説明):

    • これまでの流れ(問題のあるパス):
      1. 型情報を元に type = types[key] を取得
      2. value = type.cast(value) を呼ぶ
      3. SerializeCastValue.serialize(type, value) で DB 向けに最終シリアライズ
    • 新しい流れ(ホットパス適用時):
      1. type.serialized? をチェック
      2. serialized? == true の場合は type.cast を呼ばず、直接 SerializeCastValue.serialize(type, value) に渡す
      3. serialized? == false の通常型は従来どおり cast を通す
  • 要点:

    • 「値が Hash/Array だからスキップする」のではなく「型がシリアライズ型かどうか (serialized?) で分岐」しているため、ロジックが insert_all のみ・シリアライズ属性のみの最小範囲に閉じる。
    • insert_all では「メモリ上のオブジェクトの正規化」は不要で、「DB に正しく保存できること」が唯一の関心事なので、deserialize(serialize()) による正規化処理はカットしても安全。

2-4. パフォーマンス

ベンチマーク(10000 行・37 カラム・YAML シリアライズ 1 カラム)の結果:

  • insert_all! (with cast):約 1.0 i/s
  • insert_all! (array / hash hot path):約 1.82 i/s
  • raw_sql:約 2.07 i/s

array / hash hot path(今回の変更のイメージに近いパス)は:

  • cast を入れる以前 (no cast) とほぼ同じ速度
  • with cast に比べて約 2 倍高速
  • raw SQL に対して約 1.13〜1.14 倍遅い程度にまで近づく

  1. 影響範囲・注意点
  • 影響範囲:
    • ActiveRecord::InsertAllinsert_all / insert_all! / upsert_all 等)でシリアライズ属性を含むレコードを挿入するケースが対象。
    • 通常の create / save / update / 属性代入 (model.attr = ...) のシリアライズ挙動は変更されない。
  • 挙動面:
    • 以前の PR #48139 による修正(insertcreate のシリアライズ結果の整合性を取る)は維持されている。
    • 今回は「すでにシリアライズ型として認識できる値について insert_all 内部で cast の往復を省略する」だけなので、DB に保存される最終的な値のフォーマットは従来と同じになる想定。
  • 注意点:
    • 自前で独自のシリアライザや型を実装している場合で、serialized? の実装に依存している・もしくは上書きしているようなケースでは、理論上このホットパスに乗るかどうかが変わる可能性がある。
    • ただし、この PR 自体は非常に局所的な変更(insert_all 内の型分岐)であり、互換性への影響は小さいと考えられる。
  • テスト:
    • activerecord/test/cases/serialized_attribute_test.rb にテスト追加済み。
    • シリアライズ属性を含む insert_all の結果が想定どおりになること、および回帰修正 (#48139) で期待された動作が維持されていることを検証している。

  1. 参考情報 (あれば)
  • 関連 Issue: #56163
  • 回帰の原因となった PR: #48139(insert 系でのシリアライズ結果の正規化)
  • この PR が主に対処しているポイント:
    • シリアライズ属性での Mutable#cast のコスト(deserialize(serialize()))を、insert_all に限ってショートカットする。
    • シリアライズ属性を多用した大規模バルクインサートのパフォーマンスを、raw SQL に比較的近い水準まで戻す。

#57363 Reset lock_version after transaction rollback

マージ日: 2026/5/14 | 作成者: @55728

  1. 概要 (1-2文で)
    このPRは、楽観的ロック (lock_version) を使ったレコードをトランザクション内で更新した後にロールバックした際、メモリ上の lock_version とDB上の値が不整合になり、次回の保存で ActiveRecord::StaleObjectError が発生するバグを修正します。ロールバック時にトランザクション前の lock_version を正しく復元するように、トランザクション状態の復元処理を調整しています。

  1. 変更内容の詳細

問題の挙動

ruby
widget = Widget.create!(name: "a")          # lock_version: 0

Widget.transaction do
  widget.update!(name: "b")                 # lock_version: 0 -> 1 (DBも1になる)
  raise ActiveRecord::Rollback              # トランザクション全体がロールバック
end

widget.lock_version                         # => 1 (メモリ上は1, DBは再び0に戻っている)
widget.update!(name: "c")                   # => ActiveRecord::StaleObjectError
  • DB側はロールバックにより lock_version: 0 に戻る
  • しかしRubyオブジェクト widgetlock_version は 1 のまま
  • その状態で再度 update! すると、WHERE lock_version = 1 で更新を試みるためヒットせず StaleObjectError になる

従来は「ロールバック後に record.reload してね」という運用回避策のみで、失敗が次の保存時まで顕在化しないためデバッグしづらく、また「トランザクション内で更新 → ロールバック → リトライ」というパターンが一般的であることから、ロールバック時点でオブジェクト状態をDBに揃えるべきと判断されています。

根本原因

トランザクション開始時にとる「状態スナップショット」の持ち方と、lock_version のインクリメントの仕方の相性が悪かったことにあります。

  1. remember_transaction_record_state が記録するスナップショットは @attributes を参照渡ししている:

    ruby
    @_start_transaction_state ||= {
      ...
      attributes: @attributes,  # deep_dup ではなく参照
      ...
    }
  2. 楽観的ロックの更新 (Locking::Optimistic#_update_row) では self[locking_column] += 1 と、同じ AttributeSet をインプレースに更新している

  3. 保存が成功した後、Dirty#forget_attribute_assignments によってレコード本体の @attributes は新しい AttributeSet に置き換えられるが、スナップショット側は古い(かつすでに書き換わった)AttributeSet を保持し続ける

    • 結果として、スナップショット内では lock_version 属性が「value = N+1, original = N」という “dirty 状態” のまま残る
  4. ロールバック時の restore_transaction_record_state では、スナップショットと現在値が同じなら復元処理をスキップするロジックがある:

    ruby
    # 擬似コードイメージ
    snapshot_attr = snapshot[attribute_name]
    current_attr  = @attributes[attribute_name]
    
    # value が同じなら何もしない
    next if snapshot_attr.value == current_attr.value

    しかし lock_version の場合は

    • snapshot_attr.value = N+1
    • current_attr.value = N+1
      となっていて「同じ」なので復元をスキップしてしまう
  5. その結果、ロールバック後もレコードの lock_version 属性は

    • value: N+1
    • original_value: N
      のまま残り、「dirty な lock_version」として扱われる。その状態で次の保存を行うと WHERE lock_version = N+1 を使ってしまい、DB側の N と不整合になって StaleObjectError になる。

修正内容

このPRでは、restore_transaction_record_state 内でロックカラムだけを特別扱いして、スナップショットに記録されている original_value から属性を再構築 するようにしています。

  • 対象: 楽観的ロックに使用されるカラム (locking_column、通常は lock_version)
  • ロールバック時の処理:
    • スナップショット中の lock_versionoriginal_value (保存前の値、N) を取り出す
    • with_value_from_database を使って Attribute(value=N, original=N) という「DBと同期が取れたクリーンな属性」を生成
    • それを現在の @attributes にセットし直す

これにより:

  • メモリ上の lock_version がDBと同じ値 (N) に戻る
  • dirty tracking 上も lock_version は変更されていないと判断される
  • 次の update / saveWHERE 句では N を使うため、StaleObjectError は発生しない

テスト

  • 新しいテストクラス OptimisticLockingRollbackTest を追加
    • self.use_transactional_tests = false に設定
      → Railsのテストフレームワークが外側をトランザクションでラップしてしまうと、内部で requires_new: true なセーブポイントが使われ、「外側のトランザクションレベル=1でのロールバック」にならず、今回の修正箇所 (force_restore_state: true ブランチ) が実行されないため、それを避けるための設定
  • 追加テストは、main ブランチ (修正前) だとロールバック後の lock_version が 1 のままで失敗し、このPRのパッチ適用後は 0 に戻ってテストが通ることが確認されている

  1. 影響範囲・注意点

影響範囲

  • 対象:

    • Active Record の楽観的ロック (lock_version カラム) を利用しているアプリケーション
    • かつ、最外層のトランザクション がロールバックされるパターンで、ロールバック後も同じレコードインスタンスを使って再度更新するケース
  • 期待できる改善:

    • ロールバック後に record.reload を明示的に呼ばなくても、メモリ上の lock_version がDBと同期した状態に戻る

    • よくあるパターン:

      ruby
      Widget.transaction do
        widget.update!(...)
        raise ActiveRecord::Rollback
      end
      
      # 以前はここで reload が必要だった
      widget.update!(...)  # ← これがそのまま正常動作するようになる

まだ解決していないケース

PR説明にもある通り、セーブポイントレベルでのロールバックrequires_new: true など)には今回の修正は効きません。

例:

ruby
Widget.transaction do
  Widget.transaction(requires_new: true) do
    widget.update!(...)
    raise ActiveRecord::Rollback  # ← セーブポイントのロールバック
  end

  # ここで widget を使って再び update すると、
  # lock_version の不整合が起こる可能性は依然としてある
end
  • 現状、深いレベルのセーブポイントロールバックでは restore_transaction_record_state 自体をスキップしているため、今回のロジックも実行されません。
  • これを完全に解決するには「各レベルごとのスナップショット管理」や「すべてのレベルで復元処理を走らせる」など、より大きな変更が必要であり、このPRのスコープ外とされています。

互換性・副作用の可能性

  • 通常のユースケースでは、「ロールバック後も lock_version がロールバック前の値のまま残っている」こと自体がバグなので、このPRはバグ修正として安全側に働きます。
  • もしアプリケーションコード側が、この不整合な挙動に依存したワークアラウンド(例: ロールバック後の lock_version の値をそのまま参照して何かする)をしていた場合は挙動が変わる可能性がありますが、そうしたコードは一般的ではないと思われます。

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

    • activerecord/lib/active_record/transactions.rb
      • restore_transaction_record_state に楽観的ロック列の特別扱いロジックを追加
    • activerecord/test/cases/locking_test.rb
      • OptimisticLockingRollbackTest 追加
    • activerecord/CHANGELOG.md
      • バグ修正としてエントリ追加
  • PR内で言及されている追加リソース:

    • /tmp/lock_rollback_deep.rb
      自己完結なSQLite用再現スクリプト(このPRの前後で StaleObjectError の有無が変わることを検証済み)
    • 関連テストスイート:
      • locking_test.rb
      • transactions_test.rb
      • transaction_callbacks_test.rb
      • dirty_test.rb
      • autosave_association_test.rb
      • nested_attributes_test.rb

#53812 Action Text: Dispatch Active Storage events with id and file

マージ日: 2026/5/14 | 作成者: @seanpdoyle

  1. 概要 (1-2文で)
    Action Text が発火する Active Storage 直アップロード用の JavaScript イベントについて、event.detailidfile を含め、さらに 2 種類のイベントを追加しつつ、ドキュメントとテストを実装に揃えた変更です。あわせて、イベントターゲットが <input> ではなく <trix-editor> であることや、attachment プロパティの存在もガイドに明記しています。

  1. 変更内容の詳細

2-1. 直アップロードイベントの event.detailidfile を追加

Action Text が添付ファイルの direct upload を行う際に発火する Active Storage 由来のイベントについて、Active Storage 本体の DirectUploadController と同じスタイルで event.detail を構築するように変更されています。

従来はドキュメント上は id, file があるとされていたものの、Action Text から dispatch されるイベントでは実装が追従しておらず不整合がありました。この PR で実装がドキュメントに揃えられています。

イベント例(Active Storage の direct upload と同等になる想定):

js
document.querySelector("trix-editor").addEventListener("direct-upload:initialize", (event) => {
  const { id, file, attachment } = event.detail
  // id: アップロードごとの一意な ID (例: "direct-upload-123")
  // file: File オブジェクト
  // attachment: Trix.Attachment インスタンス
})

Action Text 側の attachment_upload.js で、Active Storage 側 (DirectUploadController) と同じ形で detail を組み立て、各フェーズで同じキー (id, file, xhr, など) を含めて dispatch するようにしています。

2-2. 2つのイベントを新たにサポート

Action Text のガイドには載っていなかったものの、Active Storage 本体では存在している 2 つのイベントが Action Text でも明示的にサポートされるようになりました。

追加されたイベントと event.detail:

  1. direct-upload:before-blob-request

    • タイミング: アプリケーションに direct upload 用メタデータ(blob の作成)を問い合わせる直前
    • event.detail の内容: { id, file, xhr, attachment }
    • 想定用途: メタデータリクエストに対してカスタムヘッダの追加や、リクエスト前の検証など
  2. direct-upload:before-storage-request

    • タイミング: 実際にストレージサービス(S3 など)へ PUT/POST する直前
    • event.detail の内容: { id, file, xhr, attachment }
    • 想定用途: ストレージリクエストに対して署名ヘッダの差し替え、進捗トラッキングの準備、キャンセル制御など

サンプルコード:

js
const editor = document.querySelector("trix-editor")

editor.addEventListener("direct-upload:before-blob-request", (event) => {
  const { id, file, xhr } = event.detail
  xhr.setRequestHeader("X-Upload-ID", id)
})

editor.addEventListener("direct-upload:before-storage-request", (event) => {
  const { file, xhr } = event.detail
  console.log("About to upload to storage:", file.name)
})

2-3. イベントターゲットと attachment プロパティをドキュメントに反映

  • これまでのドキュメントでは、direct upload イベントのターゲットが <input> 要素であるかのように書かれていましたが、実際には Action Text の AttachmentUpload クラスは <trix-editor> 要素をターゲットとしてイベントを dispatch しています。
  • この PR ではガイド (guides/source/action_text_overview.md) を修正し、「イベントは <trix-editor> に対して発火する」ことを明記しています。
  • さらに、event.detail.attachment として Trix の Attachment インスタンスが渡されることをドキュメントに追記しています。これにより、イベントハンドラ内で Trix の API を利用した高度な制御が可能になります。

例:

js
editor.addEventListener("direct-upload:initialize", (event) => {
  const { attachment } = event.detail
  // 例: プログレス用の属性を Trix 側に持たせる
  attachment.setAttributes({ "data-upload-state": "initializing" })
})

2-4. ESM / Sprockets 両方のビルドを更新

変更は次のファイルで行われており、ESM ビルド (actiontext.esm.js) と Sprockets 用ビルド (actiontext.js) の両方に反映されています。

  • actiontext/app/assets/javascripts/actiontext.esm.js
  • actiontext/app/assets/javascripts/actiontext.js
  • actiontext/app/javascript/actiontext/attachment_upload.js

これにより、どのビルドパスを使っていても同じイベント仕様になります。

2-5. システムテストの拡充

actiontext/test/system/rich_text_editor_test.rb に、直アップロードイベントが想定どおりの detail を持って <trix-editor> から dispatch されることを確認するシステムテストが追加されています。

  • 追加テストでは、ブラウザ上で実際にファイルを添付し、イベントリスナで event.detail.id, file, xhr, attachment などが正しく渡っているかを検証しています。
  • 新規イベント (before-blob-request, before-storage-request) もテストの対象になっているため、将来的なリグレッションを防ぐ意図があります。

  1. 影響範囲・注意点
  • 対象バージョン
    Rails 8 系の Action Text / Active Storage の挙動に合わせた変更であり、Rails 8 をターゲットにしたアプリや gem が影響を受けます。

  • 既存コードへの影響

    • これまで Action Text 経由の direct upload イベントをフックしていたコードは、以下の点を確認するとよいです:
      • イベントターゲットとして <trix-editor> を前提にしているか(input を前提にしていた場合は修正推奨)
      • event.detailid, file, attachment が存在することを前提にしてカスタマイズできるようになる(破壊的ではないが、よりリッチに書き換える余地があります)
    • 新しい 2 つのイベント (before-blob-request, before-storage-request) は後方互換性を壊さない追加なので、既存コードはそのまま動作しつつ、必要であればこれらのイベントを利用できます。
  • Active Storage 本体との一貫性
    Action Text が dispatch するイベント仕様が Active Storage の DirectUploadController と揃ったため、

    • 素の <input type="file" data-direct-upload-url="..."> での direct upload と
    • <trix-editor> + Action Text での direct upload
      で、ほぼ同じイベントハンドラを共有できるようになります。
  • テスト/デバッグ上の注意

    • テストにおいて直アップロードイベントを検証している場合、ターゲット要素や event.detail の期待値を新仕様に合わせておくとよいです。
    • イベントを利用して XHR にヘッダ等を差し込んでいる場合、新しく利用可能になった before-blob-request / before-storage-request に処理を移した方が、より正確なタイミングを取れる可能性があります。

  1. 参考情報 (あれば)

#47161 Add support for RETURNING to INSERT/UPDATE/DELETE statements

マージ日: 2026/5/14 | 作成者: @benedikt

  1. 概要 (1-2文で)
    Arel の INSERT / UPDATE / DELETERETURNING 句を付けてクエリを組み立てられるようにした PR です。PostgreSQL と SQLite で、データを変更しつつ同じクエリでその結果(変更後の行や集計値など)を取得する用途を Arel レベルでサポートします。

  1. 変更内容の詳細

2-1. 何ができるようになったか

Arel の各マネージャ・ステートメントクラスに returning が追加され、以下のようなクエリを組み立てられます。

  • Arel::InsertManager#returning
  • Arel::UpdateManager#returning
  • Arel::DeleteManager#returning
  • 対応するステートメントクラス(InsertStatement, UpdateStatement, DeleteStatement)に returning ノードが追加

これにより、次のような SQL を Arel で生成可能になります。

sql
INSERT ... RETURNING ...
UPDATE ... RETURNING ...
DELETE ... RETURNING ...

2-2. 使用イメージ(サンプル)

2-2-1. DELETE + RETURNING を使った集計例(PR の例)

ruby
counter_queue = Arel::Table.new(:counter_queue)
flush = Arel::Table.new(:flush)

delete_manager = Arel::DeleteManager.new
  .from(counter_queue)
  .returning([counter_queue[:post_id], counter_queue[:delta]])

select_manager = Arel::SelectManager.new
  .with(
    Arel::Nodes::TableAlias.new(
      Arel::Nodes::Grouping.new(delete_manager.ast),
      flush.name,
    )
  )
  .from(flush)
  .project(flush[:post_id], flush[:delta].sum)
  .group(flush[:post_id])

生成される SQL:

sql
WITH "flush" AS (
  DELETE FROM
    "counter_queue" 
  RETURNING 
    "counter_queue"."post_id",
    "counter_queue"."delta"
)
SELECT
  "flush"."post_id",
  SUM("flush"."delta")
FROM
  "flush"
GROUP BY
  "flush"."post_id"

DELETE と同時に削除対象行の post_id / delta を返し、それを CTE として集計しています。

2-2-2. INSERT + RETURNING のシンプルな例

(テストや PR から想定される使い方)

ruby
users = Arel::Table.new(:users)

manager = Arel::InsertManager.new
  .into(users)
  .insert([[users[:name], "Alice"]])
  .returning(users[:id])

sql = manager.to_sql
# => INSERT INTO "users" ("name") VALUES ('Alice') RETURNING "users"."id"

2-2-3. UPDATE + RETURNING の例

ruby
users = Arel::Table.new(:users)

manager = Arel::UpdateManager.new
  .table(users)
  .set([[users[:name], "Bob"]])
  .where(users[:id].eq(1))
  .returning([users[:id], users[:name]])

sql = manager.to_sql
# => UPDATE "users" SET "users"."name" = 'Bob'
#    WHERE "users"."id" = 1
#    RETURNING "users"."id", "users"."name"

2-3. 実装面での主な変更点

  • InsertStatement, UpdateStatement, DeleteStatement
    • returning 属性(Arel::Nodes::Returning を格納する想定)が追加され、AST ノードとして RETURNING を保持できるようになった。
    • 既存の InsertStatement / UpdateStatement の初期化・dup まわりも returning を考慮するよう修正。
  • InsertManager, UpdateManager, DeleteManager
    • def returning(expr) 形式のメソッドを追加。引数には Arel のカラムノード・配列などを渡す。
    • 呼び出すと内部ステートメント (@ast) の returning にノードを設定する。
  • Arel::Visitors::ToSql
    • visit_Arel_Nodes_InsertStatement 等で、returning がセットされていれば末尾に RETURNING ... を付けるロジックを追加。
  • Arel::Visitors::Dot
    • DOT 出力にも returning ノードを含めるようにテスト・実装を追加(可視化の一貫性確保)。

テスト (*_test.rb) もそれぞれのマネージャ・ステートメント・ビジターに対して RETURNING を含むケースが追加されています。


  1. 影響範囲・注意点
  • 対象は Arel レイヤーのみ

    • この PR では ActiveRecord のパブリック API(Model.create, update_all など)には RETURNING サポートは追加されていません。
    • あくまで Arel を直接使う(または ActiveRecord 内部で Arel を駆使する)場面向けの機能拡張です。
  • 対応 DB

    • RETURNING がサポートされるのは PostgreSQL と SQLite3。
      • PostgreSQL: INSERT / UPDATE / DELETE すべてでサポート
      • SQLite3: 同上(最近のバージョンで対応)
    • MySQL は RETURNING をサポートしていないため、この機能を使って生成した SQL は MySQL ではエラーになります。
      • アプリケーション側で DB ごとの分岐やアダプタごとの挙動に注意する必要があります。
  • 既存コードへの影響

    • 既存の INSERT / UPDATE / DELETE の SQL 生成ロジックに後方互換性のある形で RETURNING 句をオプションとして追加しているため、returning を呼んでいない既存コードの SQL は変わりません。
    • ToSql / Dot visitor の挙動は returningnil(または空)であれば従来通り。
  • パフォーマンス/トランザクション的な観点

    • これまで Ruby から複数クエリで行っていた「変更 + 取得」を 1 クエリで行えるため、往復回数削減やロック時間短縮などのメリットが期待できます。
    • 一方で、返ってくる行数が多いケースでは結果セットのサイズが増える点に注意が必要です(設計時に考慮が必要)。

  1. 参考情報 (あれば)

この PR によって、PostgreSQL / SQLite を使うプロジェクトでは、Arel を直接用いることで「データ変更とその結果取得を 1 クエリで行う」高度なクエリ構築がしやすくなります。


#57215 Fix ParameterFilter hash-lookup optimization for line-anchored regexps

マージ日: 2026/5/14 | 作成者: @alexcwatt

  1. 概要 (1-2文で)
    ActiveSupport::ParameterFilter の正規表現最適化で、/^token$//\Atoken\z/ を同一視していたことによる不正なフィルタリング挙動を修正し、行アンカー (^/$) と文字列アンカー (\A/\z) を正しく区別するようにした PR です。これにより、複数行を含むパラメータキーでも本来の正規表現 semantics に従って安全にフィルタリングできるようになります。

  1. 変更内容の詳細

背景となる最適化

前の PR (#57166) で、パフォーマンス向上のために以下のような「完全一致」正規表現をハッシュルックアップに変換する最適化が入りました。

ruby
# 例: フィルタ対象キーを正規表現で指定
filter = ActiveSupport::ParameterFilter.new([/\Atoken\z/, /^secret$/])

このとき、/\Atoken\z//^token$/

  • token に完全一致するキー」とみなして
  • @exact_keys のようなハッシュに "token" を登録し
  • 実行時は正規表現マッチではなく、ハッシュのキー一致で判定する

という扱いになっていました。

しかし Ruby の正規表現では:

  • \A / \z: 文字列全体の先頭・末尾アンカー(multi-line を意識しない)
  • ^ / $: 行頭・行末アンカー("token\nfoo" のような複数行文字列では 2 行目以降にもマッチしうる)

という違いがあり、/^token$//\Atoken\z/同じ意味ではありません

問題となっていたケース

/^token$/ の場合、本来は以下のようなキーもマッチし得ます:

ruby
key = "token\nfoo"
key.match?(/^token$/)  # => true(1行目 "token" にマッチ)

ところが前回の最適化では "token\nfoo" というキーに対して

  • 「完全一致の "token\nfoo" としてのみ」ハッシュ検索を行う
  • "token" という行に対するマッチ可能性を考慮していない

ため、本来 ^/$ の semantics ではマッチすべきケースを見逃す可能性がありました。

今回の修正内容

今回の PR では、この最適化を次のように細分化・修正しています。

  1. \A...\z は「本当の完全一致」として @exact_keys に保持
    例: /\Atoken\z/

    • キーが "token" のときのみマッチ
    • "token\nfoo" はマッチしない
      → ハッシュでの完全一致判定と意味が一致するため、従来どおり最適化対象にしてよい。
  2. ^...$@exact_line_keys のような「行単位完全一致」用の別ハッシュに保持
    例: /^token$/

    • まずキー全体 ("token\nfoo" など) についてハッシュを引く
    • さらにキーに "\n" が含まれている場合は、key.split("\n") の各行に対してもハッシュを参照
      → 「行単位で token に一致するか」をハッシュで疑似的に再現する。

    イメージコード:

    ruby
    if exact_line_keys[key]
      # マッチしたとみなす
    elsif key.include?("\n")
      key.each_line do |line|
        if exact_line_keys[line.chomp]
          # マッチしたとみなす
        end
      end
    end
  3. 混在アンカー /\Atoken$/, /^token\z/ は最適化しないで素直に正規表現マッチへフォールバック

    • /\Atoken$/: 文字列全体の先頭 + 行末
    • /^token\z/: 行頭 + 文字列全体の末尾
      いずれも、ハッシュで表現するには意味が非対称で複雑であり、簡単には正しく再現できないため、「危ない最適化」は捨てて元の regexp マッチのままとしています。

テスト

activesupport/test/parameter_filter_test.rb に新規テストが追加され、次のような点がカバーされていると考えられます(テスト名から推測される範囲):

  • /\A...\z/ が完全一致として正しく扱われること
  • /^...\$/ が複数行キーに対して行単位で正しくマッチすること
  • 混在アンカーの regexp はハッシュ最適化されず、正しい挙動を保つこと

  1. 影響範囲・注意点
  • 対象: ActiveSupport::ParameterFilter を使用し、フィルタパターンにアンカー付きの正規表現(特に ^/$)を使っているコード
  • 行アンカー (^/$) を使ったフィルタ:
    • これまで誤って「完全一致」と解釈されていたケースが、本来の Ruby 正規表現どおりの挙動に戻るため、複数行キーを持つ(ややレアな)環境ではフィルタ結果が変わる可能性があります。
  • パフォーマンス:
    • /\A...\z/ のパターンは引き続き高速なハッシュルックアップで処理されます。
    • /^...\$/ も別ハッシュ + 行分割で依然として最適化されており、フル regexp スキャンよりは有利と考えられます。
    • /\A...\$//^...\z/ のような混在アンカーは最適化されなくなるため、そのようなパターンを大量に使うと、ごくわずかに性能が下がる可能性がありますが、正しさ優先の妥当なトレードオフです。

注意点として、もしアプリ側で

ruby
[/^token$/]

のようなフィルタを「本当に文字列全体の完全一致」として意図していた場合、Ruby の意味論とはもともとずれていたので、意図どおりなら /\Atoken\z/ に変えておく方が安全で明示的です。


  1. 参考情報 (あれば)
  • 対象コード:
    activesupport/lib/active_support/parameter_filter.rb
  • 関連 PR:
    • #57166: ハッシュルックアップによる ParameterFilter 正規表現マッチの初期最適化
  • Ruby のアンカー仕様:
    • \A / \z: 文字列全体の先頭・末尾
    • ^ / $: 行頭・行末(/m フラグや複数行文字列で挙動が変わる)

#57313 Remove framework_defaults if load_defaults matches

マージ日: 2026/5/14 | 作成者: @runephilosof

  1. 概要 (1-2文で)
    config.load_defaults で指定したバージョンと実際の Rails バージョンが一致している場合は、config/initializers/new_framework_defaults_*.rb を生成・維持しないようにして、「使っていない new_framework_defaults が散らかる」問題を解消する変更です。rails new だけでなく rails app:update 実行時にも、不要な new_framework_defaults が勝手に再作成されなくなります。

  1. 変更内容の詳細

背景となる挙動

従来の挙動:

  • rails new:
    • 一番新しい new_framework_defaults_x_y_z.rb は生成されない(削除される)
  • rails app:update:
    • new_framework_defaults_x_y_z.rb は常に生成される

一方で、アプリ開発者の一般的な運用としては:

  1. Rails をアップグレードしたら、まず config/initializers/new_framework_defaults_x_y_z.rb を使って個別に新デフォルトを有効/無効にしつつ動作確認
  2. 安定したら config.load_defaults x.y を新バージョンに上げる
  3. そのタイミングで new_framework_defaults_x_y_z.rb は不要になるので削除する

ところが、(3) のあとに rails app:update を再度実行すると、不要になった new_framework_defaults がまた作られてしまい、初期化ファイルがノイズになる問題がありました。

今回の変更のコアアイデア

「アプリの load_defaults バージョンと Rails 本体のデフォルトバージョンが一致しているなら、そのアプリは“すでに新デフォルトに追随済み”とみなせるので、new_framework_defaults は不要」とみなして、生成しないようにします。

app_generator.rb の変更

railties/lib/rails/generators/rails/app/app_generator.rb に、new_framework_defaults 関連ファイルを生成するかどうかを判定するロジックが追加されています。概ね次のような条件分岐が入ったと考えられます(疑似コード):

ruby
if load_defaults_version == Rails::VERSION::STRING # または適切なバージョン比較
  # すでに最新デフォルトに揃っているので new_framework_defaults は不要
  # → 生成しない
else
  # 移行途中のアプリなので new_framework_defaults を生成する
end

これにより:

  • rails new 実行時:
    • config.load_defaults が最新バージョンを指す新規アプリでは、基本的に new_framework_defaults_*.rb は生成されない
  • rails app:update 実行時:
    • すでに config.load_defaults を最新に上げているアプリでは、削除済みの new_framework_defaults_*.rb が再作成されない

テストの追加

railties/test/generators/app_generator_test.rb にテストが22行追加されています。内容としては:

  • 特定の load_defaults バージョン設定のもとで rails new / rails app:update を動かし
  • config/initializers/new_framework_defaults_*.rb が生成される/されないことを確認する

といったケースが追加されていると考えられます。

CHANGELOG の更新

railties/CHANGELOG.md に、上記の挙動変更が明記されました。これにより、Rails アップデート時に「new_framework_defaults が自動で作られなくなった」理由が追えるようになっています。


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

    • rails new
    • rails app:update で生成される config/initializers/new_framework_defaults_*.rb 周りの挙動
  • 実用上の効果:

    • すでに config.load_defaults を最新バージョンに上げているアプリでは:
      • rails app:update を何度再実行しても、new_framework_defaults が勝手に復活しない
        → 初期化ファイルディレクトリがスッキリし、マイグレーション完了後のノイズが減る
    • まだ load_defaults を古いバージョンにしているアプリでは:
      • これまで通り new_framework_defaults_x_y_z.rb が作られ、個別フラグでデフォルト挙動を切り替えながら移行できる
  • 注意点:

    • new_framework_defaults を「将来的なメモ」として意図的に残しておきたい運用をしている場合、config.load_defaults を最新に上げた瞬間から、新しく rails app:update を実行してもそのファイルは再生成されません。
      → その場合は、load_defaults を上げる前に内容を別の場所へ移すか、自前の initializer に整理しておく必要があります。
    • 古い Rails からの移行ドキュメントで「app:update を走らせると new_framework_defaults が生成される」と書かれているケースがありますが、この PR 以降のバージョンでは「load_defaults が最新でない場合のみ生成」という前提に読み替える必要があります。

  1. 参考情報 (あれば)
  • 対象 PR: https://github.com/rails/rails/pull/57313
  • 関連概念:
    • config.load_defaults x.y
      → 「Rails x.y 時点のフレームワークデフォルトを一括適用」するメソッド
    • config/initializers/new_framework_defaults_x_y_z.rb
      → 新バージョンで変わる挙動を個別フラグでオン/オフしながら段階的に移行するためのテンプレートファイル

#55100 Fix #55099 allowing array syntax for expression indexes in add_index

マージ日: 2026/5/14 | 作成者: @AlexandreCamillo

  1. 概要 (1-2文で)
    add_index / t.index で「式インデックス(expression index)」を定義する際、これまで文字列でしか書けなかったものを、通常のマルチカラムインデックスと同様に配列シンタックスでも書けるようにした修正です。これにより、["lower(email)", :status] のように、式とカラムを一貫した形で指定できます。

  1. 変更内容の詳細

これまでの問題点

Rails のマイグレーションで式インデックスを張る場合:

ruby
create_table :users do |t|
  t.string :email
  t.string :status
end

add_index :users, 'lower(email), status'
# または
t.index 'lower(email), status'

というように、式インデックスは 生 SQL 文字列 で書く必要がありました。
これだと:

  • 通常のインデックスが [:email, :status] のような配列指定なのと書き方が揃わない
  • 式と通常カラムを混在させるときに見通しが悪い
  • Ruby 側での処理(例: Array 前提の処理)と噛み合わない

といった不便がありました。

本 PR の変更点

add_index / t.index式インデックスを配列シンタックスでも指定可能 にしました。

新たに許可される書き方

ruby
# 式 + カラム
t.index ["lower(email)", :status]

# 式だけ
t.index ["lower(email)"]

# もちろん従来通りのカラムのみ指定も可能
t.index [:email, :status]

これにより:

  • 配列の中に「文字列で表現した式」と「シンボル(または文字列)で表した通常カラム」を混在させて指定できる
  • 従来の "lower(email), status" のような カンマ区切りの 1 つの文字列 ではなく、要素ごとに分割された配列 として指定できる

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

実装レベルのポイント(推測を含む)

activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rbadd_index 内部で:

  • これまで「引数が文字列ならそのまま SQL 断片として扱う」という処理が主だったところに、
  • 「配列が渡された場合、その要素がシンボルならカラム名としてクオート、文字列なら式として扱う」ロジックが追加

されている形です。
index_test.rb 側では、以下のようなケースがテストされているはずです:

  • t.index ["lower(email)"] でインデックスが作れること
  • t.index ["lower(email)", :status] で複合インデックスが作れること
  • 生成されるインデックス名・SQL が期待通りであること

CHANGELOG.md にも「配列シンタックスで expression index を書けるようになった」というエントリが追加されています。


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

    • 既存の文字列指定('lower(email), status')はそのまま動作し、破壊的変更にはなっていません。
    • 既存マイグレーションを書き換える必要はありませんが、今後は配列シンタックスを使うことで可読性を上げられます。
  • 書き方の指針

    • 「SQL の式として扱いたい部分」は 文字列 で書く必要があります:
      ruby
      t.index ["lower(email)", :status]  # OK
      t.index [:lower_email, :status]    # これは「:lower_email カラム」と解釈される
    • 通常のカラムは従来どおりシンボル/文字列で指定可能です。
  • データベース固有の式

    • "lower(email)" のような式自体は DB 依存(PostgreSQL での LOWER 関数など)なので、DB を跨ぐマイグレーションでは従来同様、式の可搬性には注意が必要です。
    • 本 PR はあくまで「指定方法」の改善であり、式そのものの互換性までは保証しません。
  • コードの一貫性とリファクタリング

    • 既存のマイグレーションをリファクタする場合は、下記のような置き換えが可能です:
      ruby
      # Before
      t.index 'lower(email), status'
      
      # After
      t.index ["lower(email)", :status]
    • RuboCop ルールなどで「配列シンタックスを推奨する」ようなガイドラインを設けると、コードスタイルを統一しやすくなります。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/55100
  • 関連 Issue: #55099
  • 関連ドキュメント(現行 Rails Guides — Active Record マイグレーション / Indexes)も、今後この配列シンタックスについて追記される可能性があります。

#54416 ActiveStorage: implement attach!

マージ日: 2026/5/14 | 作成者: @quentindemetz

  1. 概要 (1-2文で)
    ActiveStorage の attach に対応する「バン付き」メソッド attach! が、一つ/複数添付の両方(has_one_attached, has_many_attached)に実装されました。attach! は保存に失敗した場合に例外を投げることで、添付がサイレントに失敗することを防ぎます。

  1. 変更内容の詳細

何が追加されたか

ActiveStorage の以下2クラスに attach! が追加されています。

  • ActiveStorage::Attached::One#attach!
  • ActiveStorage::Attached::Many#attach!

挙動は ActiveRecord の save / save! と同様の関係になります。

  • attach
    • 内部で record.save を呼ぶ
    • バリデーション等で保存に失敗した場合でも 例外は投げずに false を返す/添付されない
  • attach!
    • 内部で record.save! 相当の処理を呼ぶ
    • 保存に失敗した場合 ActiveRecord::RecordInvalid などの例外を発生させる

これにより、「必ず添付できていること」を前提にしたコードを書きやすくなります。

想定される実装イメージ

実際のコードは PR 参照ですが、ざっくりしたイメージは以下のようなものです(擬似コード):

ruby
# activestorage/lib/active_storage/attached/one.rb
def attach(*args)
  change = build_change(*args)
  change.save # => 失敗しても例外は上げない
end

def attach!(*args)
  change = build_change(*args)
  change.save! # => 失敗時に ActiveRecord::RecordInvalid などの例外
end

Many 版も同様のパターンで実装されています。

テスト

次のファイルに attach! 用のテストが追加されています。

  • activestorage/test/models/attached/one_test.rb
  • activestorage/test/models/attached/many_test.rb

テスト内容の方向性:

  • attach! が正常系で添付を行えること
  • バリデーション失敗など「保存できない条件」の場合に例外を投げること

また、activestorage/CHANGELOG.md にもこの新機能が追記されています。


  1. 影響範囲・注意点

影響範囲

  • 既存の attach の挙動は 一切変更されていない ため、既存コードへの互換性は基本的に保たれています。
  • 新たに attach! が利用可能になり、以下のような書き方ができるようになります。
ruby
class User < ApplicationRecord
  has_one_attached :avatar
  has_many_attached :photos
end

user = User.find(1)

# 1件添付(失敗したら例外)
user.avatar.attach!(io: File.open("/path/to/file"), filename: "avatar.png")

# 複数添付(失敗したら例外)
user.photos.attach!([
  { io: File.open("/path/to/1.png"), filename: "1.png" },
  { io: File.open("/path/to/2.png"), filename: "2.png" }
])

注意点

  • attach! はバリデーションや DB 制約などで添付が保存できなかった場合に例外を投げます。
    • 例: ActiveRecord::RecordInvalid, DB のユニーク制約違反由来の例外など
  • 例外ハンドリングを明示的に行う場合、以下のようなコードが必要になります。
ruby
begin
  user.avatar.attach!(...) # or user.photos.attach!(...)
rescue ActiveRecord::RecordInvalid => e
  # エラーメッセージ: e.record.errors.full_messages など
end
  • 「失敗しても無視して進めたい」ケースでは、従来どおり attach を使う方が適切です。
  • モデル側で validate :xxx_on_attachments のように添付に絡むバリデーションを行っている場合、attach! 利用時に例外発生の可能性がある点に注意してください。

  1. 参考情報 (あれば)

#57360 Fix renderable arity checks when method is overridden

マージ日: 2026/5/14 | 作成者: @chaadow

  1. 概要 (1-2文で)
    Rails の render が「レンダリング可能オブジェクト」の render_in の引数個数(arity)を判定する際に、自前で method メソッドを定義しているオブジェクトだと壊れていた問題を修正した PRです。Kernel#method を明示的に呼ぶことで、render_in の arity 判定が常に正しく行われるようになりました。

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

問題の背景

Rails では、以下のような「レンダリング可能オブジェクト」(view component 的なもの)を render で扱うことができます:

ruby
class MyRenderable
  def render_in(view_context, &block)
    # ...
  end
end

render MyRenderable.new

このとき Rails 側では、「このオブジェクトは render_in を持っているか」「render_in はどんな arity (引数の個数) か」をチェックして挙動を切り替えたりします。

以前の実装では、おそらく以下のようなコードに相当する処理をしていました:

ruby
renderable.method(:render_in).arity

ところが、レンダリング対象のオブジェクトが method を独自実装している場合に問題が起こります:

ruby
class MyRenderable
  # 独自の method メソッドを定義している
  def method(name)
    # 何か全然違うことをする or 対応していない
  end

  def render_in(view_context)
    # 実際のレンダリング処理
  end
end

この場合、renderable.method(:render_in) は Ruby 組み込みの Kernel#method ではなく、このクラスの method が呼ばれてしまい、

  • NoMethodError
  • 不正なオブジェクトが返る
    などによりレンダリングが壊れてしまいます。

PRでの修正内容

actionview/lib/action_view/template/renderable.rb での arity チェック処理を、次のように「必ず Kernel 実装を使う」ように修正しています (擬似コード):

ruby
# 修正前 (イメージ)
renderable.method(:render_in).arity

# 修正後 (イメージ)
Kernel.instance_method(:method).bind(renderable).call(:render_in).arity
# あるいは内部的には `Kernel#method` を明示的に呼び出している

要点としては:

  • Object#method (= Kernel#method) を明示的に呼び出すことで、
  • オブジェクトが method をオーバーライドしていても、
  • render_in のメソッドオブジェクトと arity を正しく取得できるようにした。

テストの追加

actionview/test/template/render_test.rb に回帰テストが追加されています。内容としては:

  • method を独自定義したレンダリング可能オブジェクトを用意
  • そのオブジェクトを render しようとしたときに、例外にならず正常に動作すること
  • かつ、render_in が正しく呼ばれていること

を確認するテストです。
今回のバグが将来の変更で再発しないようにするための回帰テストになっています。


  1. 影響範囲・注意点
  • 影響を受けるのは「render 可能なオブジェクト」のうち、method を独自に定義しているクラスです。
    • 例: コンポーネント/デコレーターなどでメタプログラミングをしており、method をフックしているようなケース。
  • これらのクラスが render_in を実装していても、以前は render 時の arity チェックで壊れる可能性がありましたが、この PR によって正しく動くようになります。
  • 変更は internal な arity 判定ロジックのみであり、パブリック API のシグネチャや一般的な使い方には影響しません。
  • method_missing をカスタマイズしているだけで method を直接オーバーライドしていない場合は、もともと大きな影響はなく、この PR による挙動変化もほぼありません。

運用上の注意という観点では:

  • 自作コンポーネントや view object で method をオーバーライドしている場合、Rails のバージョンアップで「今まで壊れていたレンダリングが直る」可能性があります。
  • 逆に、「壊れていた前提でワークアラウンドを書いていた場合」は、そのワークアラウンドが不要になるか確認した方が良いかもしれません。

  1. 参考情報 (あれば)

このあたりを読むと、「なぜ renderable の arity 判定が必要なのか」「どのように API 互換性を保ちながら実装しているのか」が補足的に理解できます。


#57366 Freeze more immutable constants

マージ日: 2026/5/14 | 作成者: @andrewn617

  1. 概要 (1-2文で)
    Rails内部で「本来不変であるべき定数」に .freeze を追加し、特に配列・ハッシュ・Set などのミュータブルなオブジェクトを不変化することで、Ractor 対応(並列実行の安全性向上)を進めるPRです。前回PR (#57323) でカバーしきれなかった箇所を追加で凍結しています。

  1. 変更内容の詳細

全体方針

  • 対象:
    • Hash / Array / Set / オブジェクトインスタンスを値に持つ定数
    • RuboCop の Style/MutableConstant では検出されにくいパターン(例: Set[] で定義された定数、Object.new を代入している定数など)
  • 対応:
    • それらの定数定義に .freeze を付与
    • 既存の定数の意味や構造は変えず、「あとから変更できない」ようにするだけの変更

コードレベルではほぼすべて、次のような形の修正です(ファイルごとに具体的なキー/値は異なるがパターンは同じ):

ruby
# 変更前
SOME_CONSTANT = {
  foo: :bar,
  baz: :qux,
}

# 変更後
SOME_CONSTANT = {
  foo: :bar,
  baz: :qux,
}.freeze
ruby
# 変更前
SOME_SET = Set[:foo, :bar]

# 変更後
SOME_SET = Set[:foo, :bar].freeze
ruby
# 変更前
SOME_ARRAY = [1, 2, 3]

# 変更後
SOME_ARRAY = [1, 2, 3].freeze

主な変更箇所の役割イメージは以下の通りです(実際のキー名などはファイルごとに異なります):

  • actionpack/lib/action_dispatch/http/headers.rb
    • HTTPヘッダの扱いに関する定数のハッシュ/配列を .freeze
  • actionpack/lib/action_dispatch/routing/mapper.rb
    • ルーティング定義で使われる内部定数(例えばHTTPメソッドやオプション関連)を .freeze
  • actionview/lib/action_view/helpers/javascript_helper.rb
    • JavaScriptヘルパ内のオプション・テンプレート関連の定数(配列/ハッシュ)を .freeze
  • actionview/lib/action_view/helpers/tag_helper.rb
    • HTMLタグ描画ヘルパの定数(属性名リスト等)を .freeze
  • actionview/lib/action_view/template.rb
    • テンプレートに関する内部オプション・ハンドラの定数を .freeze
  • activejob/lib/active_job/exceptions.rb
    • リトライ戦略などで利用される例外関連の定数(リスト/マッピング)を .freeze
  • activemodel/lib/active_model/attribute.rb
    • 属性ハンドリングに使用する定数(型・フラグなど)を .freeze
  • activerecord/* (primary_key, attributes, relation, relation/merger, relation/query_methods)
    • クエリメソッドで使う予約語のSet/配列
    • マージ戦略、primary key関連で使う内部ハッシュ
      .freeze
  • activesupport/lib/active_support/cache.rb
    • キャッシュオプションやプリセット設定の定数を .freeze
  • activesupport/lib/active_support/hash_with_indifferent_access.rb
    • 内部変換/初期値に関する定数を .freeze
  • activesupport/lib/active_support/testing/assertions.rb
    • テスト用アサーションで使うメッセージやオプションの定数を .freeze
  • activesupport/lib/active_support/xml_mini.rb
    • XML パーサ関連のアダプタ名やオプション定数を .freeze

純粋に「可変なオブジェクトを定数に置いている箇所を、不変にした」という修正であり、ロジック変更や新機能の追加はありません。


  1. 影響範囲・注意点
  • 外部API/動作仕様の変化:
    • 公開APIの挙動としては変化はほぼありません。
    • ただし「Rails内部の定数を実行時に書き換えていた」ようなコードがある場合は、FrozenError が発生する可能性があります。
      • 例: ActiveRecord::Relation::SOME_INTERNAL_CONSTANT << :new_value のようなコードは動かなくなります。
  • Ractor 対応・スレッドセーフティへの影響:
    • Ractor では、共有されるオブジェクトが原則不変である必要があり、このPRはその前提を満たすための一歩です。
    • 定数にぶら下がるミュータブルなオブジェクトを凍結することで、マルチRactor環境での安全性と予測可能性が向上します。
  • パフォーマンス:
    • 参照時のオーバーヘッドはありません。
    • 初期化時に .freeze が1回走るだけなので、実質的なパフォーマンス影響は無視できるレベルと考えられます。
  • テスト:
    • 新しいテストの追加は明示されていませんが、「挙動が変わらないリファクタリング」扱いで既存テストがパスすることを前提とした変更です。

  1. 参考情報 (あれば)
  • このPRの背景となる前回PR:
  • Ractor と不変オブジェクトの関係(Ruby本体のドキュメント):
    • Ractorはスレッドセーフな並列実行モデルであり、Ractor間で共有できるのは「不変のオブジェクト」または「Ractor間共有を許可されたオブジェクト」に限られます。
    • Rails内部の定数を凍結することは、RailsをRactor対応させる上での前提条件の1つです。

#51627 [Fix #51524] Fix strict_loading violations ignored when using pluck

マージ日: 2026/5/14 | 作成者: @shes50103

  1. 概要 (1-2文で)
    strict_loading モードが有効な関連に対して pluck を呼んだ場合でも、他の場合と同様に strict loading 違反としてエラー or ログ出力されるように修正した PRです。
    あわせて、「事前に preload されている関連」は strict loading 違反とみなされないように判定ロジックが調整されています。

  1. 変更内容の詳細

2-1. pluck で strict_loading 違反を検出するように

対象: ActiveRecord::Calculations#pluck(とそれを使う CollectionProxy#pluck

元々の問題:
strict_loading を有効にしていても、関連オブジェクトに対して pluck を呼ぶと strict loading のチェックをすり抜けてしまい、N+1 などを防ぐ目的が達成されないケースがありました。

例(イメージ):

ruby
class Post < ApplicationRecord
  has_many :comments
end

post = Post.strict_loading.first
# post.comments のアクセスが strict loading 対象

# 本来は strict loading 違反としたいケース
post.comments.pluck(:content)

この PR により、pluck が内部で関連をロードする際にも strict loading チェックが走り、設定に応じて:

  • strict_loading_by_default = true かつ違反 → 例外を raise
  • ログモードの場合 → 違反としてログ出力

といった、他の関連ロードと同等の挙動になります。

PR本文では ActiveRecord::Calculations#pluck の更新と書かれていますが、差分としては CollectionProxy 周りにも pluck の strict loading 対応コードが追加されており、関連コレクション経由の pluck も対象になっています。

2-2. preload 済み関連は strict_loading 違反にしないように

対象: ActiveRecord::Associations::Association#violates_strict_loading?

これまでの挙動だと、「preload で事前にロードしておいた関連」にアクセスしても strict loading 違反として扱われるケースがありました。

この PR では、violates_strict_loading? の判定ロジックを変更し:

  • strict_loading が有効
  • まだロードされていない関連に初回アクセス → 違反
  • preload・eager_load・includes 等で事前ロード済み → 違反ではない

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

具体的には、Association 内部で「preloaded な状態」を考慮した条件分岐に変更されており、preload を活用しつつ strict_loading を使うユースケースがサポートされます。

2-3. テスト・CHANGELOG

  • activerecord/test/cases/strict_loading_test.rb に、pluck と preload に絡む strict loading のテストケースが追加。
  • activerecord/CHANGELOG.md に挙動変更として追記。

  1. 影響範囲・注意点
  • strict_loading を使っているアプリでの挙動変化

    • これまで問題なく動いていた association.pluck(:column) が、strict_loading 違反として例外を投げたりログ出力されるようになる可能性があります。
    • strict_loading を「N+1 検出用のガード」として期待していた場合は、より一貫した挙動になりますが、既存コードが壊れることもあり得るので確認が必要です。
  • preload を正しく使っていればエラーは出ない

    • includes / preload / eager_load などで事前ロードしておけば、strict_loading 違反にはなりません。

    • strict_loading + pluck を使う場合は、以下のような書き方を推奨できます:

      ruby
      posts = Post.strict_loading.includes(:comments).to_a
      
      # preload 済みなので OK
      posts.first.comments.pluck(:content)
  • 関連 PR とのコンフリクト

    • PR本文にある通り、似た問題を別のシナリオで扱う PR (#50389) とコンフリクトしうるため、strict_loading 周りをいじっている場合は upstream のマージ状況に注意が必要です。

  1. 参考情報 (あれば)

#57358 Document ActiveStorage::Blob#custom_metadata [ci-skip]

マージ日: 2026/5/14 | 作成者: @p8

  1. 概要 (1-2文で)
    Active Storage の ActiveStorage::Blob#custom_metadata / #custom_metadata= メソッドについて、既に存在していたものの公式に説明されていなかったため、コメントでドキュメントを追加した PR です。挙動や API 仕様の変更はなく、コード上のドキュメント整備のみです。

  1. 変更内容の詳細(あればサンプルコードも含めて)
  • 対象: activestorage/app/models/active_storage/blob.rb
  • 追加されたのは 8 行程度のコメント(YARD / RDoc 用のドキュメント)で、ActiveStorage::Blob に定義されている custom_metadata 系メソッドが何をするのかを説明しています。
  • これらのメソッド自体は、過去のコミット e106a4a1d29 ですでに導入済みで、今回は「ドキュメントがなかったものに説明を足しただけ」という位置づけです。

custom_metadata は、Active Storage が内部で使う標準の metadata とは別枠で、アプリケーション側が自由にキー・バリューを保存するための領域として使う想定の機能です。典型的には、以下のように利用します。

ruby
blob = ActiveStorage::Blob.create_and_upload!(
  io: File.open("/path/to/file.jpg"),
  filename: "file.jpg",
  content_type: "image/jpeg",
)

# 任意のメタデータを設定
blob.custom_metadata = {
  "source" => "user_upload",
  "license" => "CC-BY-SA",
  "tag"    => "profile_image"
}
blob.save!

# 読み出し
blob.custom_metadata
# => { "source" => "user_upload", "license" => "CC-BY-SA", "tag" => "profile_image" }

ドキュメントの追加により、例えば「metadatacustom_metadata の違い」や「ユーザー定義の属性をどこに持たせるべきか」といった点が、公式リファレンスから読み取りやすくなります。


  1. 影響範囲・注意点
  • 影響範囲:
    • 実行時の挙動・API のシグネチャは一切変わらず、既存コードへの影響はありません。
    • Rails ガイドや API ドキュメント生成時に、custom_metadata メソッドが正式に説明されるようになります。
  • 注意点:
    • ドキュメントが追加されたことで、今後 custom_metadata が「公式にサポートされた API」として認識されやすくなります。既に内部的に使っていたプロジェクトは、その使い方がドキュメントと合っているか、改めて確認するとよいです。
    • PR タイトルに [ci-skip] とある通り、テスト実行を伴わないドキュメント専用変更であるため、実装側のテストカバレッジや品質に関する変化はありません。

  1. 参考情報 (あれば)

#57362 [ci skip] Update the debugging guide to link the actively maintained Exception Notifier repo

マージ日: 2026/5/14 | 作成者: @KDunc11

  1. 概要 (1-2文で)
    Railsガイド「debugging_rails_applications.md」に記載されていた Exception Notifier へのリンクが、メンテナンスされていないリポジトリを指していたため、現在アクティブにメンテナンスされているリポジトリへのリンクに差し替えたドキュメント修正PRです。コードや挙動の変更はなく、あくまでガイド文書の更新のみです。

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

  • 対象ファイル: guides/source/debugging_rails_applications.md
  • 変更内容:
    • Exception Notifier の GitHub リポジトリ URL を、非メンテナンスのものから「現在アクティブにメンテされている Exception Notifier リポジトリ」に差し替え。
    • 具体的には 1 行のみの変更で、古いリンクを新しいリンクに置き換えています(ガイド上の説明文やサンプルコード等は変更なし)。

(実際の diff イメージ)※URLは例示

diff
- * Exception Notifier: https://github.com/old-org/exception_notifier
+ * Exception Notifier: https://github.com/railsware/exception_notification

※ 実際の組織名・リポジトリ名は PR の差分に依存しますが、趣旨としては「正しい現行リポジトリに差し替え」という変更です。

  1. 影響範囲・注意点
  • 影響範囲:
    • Rails ガイドを読みながら例外通知の設定を行う開発者が、メンテされていない古い repo ではなく、現行でメンテされている Exception Notifier の実装・README にアクセスできるようになります。
    • ランタイムの挙動・Rails 本体の API・テストなどには一切影響しません(ドキュメントのみ、CI スキップ指定 [ci skip] あり)。
  • 注意点:
    • すでに旧リポジトリを参照して設定していた場合は、ガイド更新をきっかけに「どの repo を使うべきか」を見直したほうがよい可能性があります(依存 gem の更新状況やセキュリティ修正などの観点)。
    • 本 PR 自体にはテストや CHANGELOG 更新は不要と判断されており、実質的には「参照先 URL の正規化」のみです。
  1. 参考情報 (あれば)
  • Rails ガイド(英語版): guides/source/debugging_rails_applications.md
  • Exception Notifier(例外通知)の代表的な利用例(概念的な例):
    ruby
    # Gemfile
    gem 'exception_notification'
    
    # config/environments/production.rb
    Rails.application.configure do
      config.middleware.use ExceptionNotification::Rack,
        email: {
          email_prefix: "[MyApp ERROR] ",
          sender_address: %{"notifier" <notifier@example.com>},
          exception_recipients: %w[dev-team@example.com]
        }
    end
    このような設定方法を確認する際に、今回の PR によって開発が続いている最新版の README / Wiki を参照しやすくなります。

#57359 Fix formatting in Active Storage controller docs

マージ日: 2026/5/13 | 作成者: @p8

  1. 概要 (1-2文で)
    Active Storage のコントローラに書かれているドキュメントコメント内のインラインコード表記を、+foo+ から <tt>foo</tt> に変更して、Rails API ドキュメント上での表示を整えた PRです。機能や挙動の変更は一切なく、ドキュメント表示の体裁を整えるだけの修正です。

  1. 変更内容の詳細

対象ファイルは以下の 4 つのコントローラです。

  • activestorage/app/controllers/active_storage/blobs/proxy_controller.rb
  • activestorage/app/controllers/active_storage/blobs/redirect_controller.rb
  • activestorage/app/controllers/active_storage/representations/proxy_controller.rb
  • activestorage/app/controllers/active_storage/representations/redirect_controller.rb

これらのファイル内の「クラスコメント (YARD/RDoc形式のドキュメントコメント)」に含まれる、インラインコードの書き方が修正されています。

以前は例えばこんな書き方になっていたものが:

ruby
# Returns the +Content-Type+ of the blob.

これが <tt> を使った書き方に変更されています:

ruby
# Returns the <tt>Content-Type</tt> of the blob.

Rails の API ドキュメントは RDoc ベースで生成されていますが、+...+ 表記だと文脈によっては意図したスタイルにならなかったり、フォント幅や高さが崩れて読みにくくなります。この PR では、スクリーンショットにあるように、インラインコード部分が明示的に <tt>...</tt> でマークアップされることで、フォントと高さが統一され、API ドキュメント上の見た目が改善されています。

コードロジックへの変更 (メソッド定義やルーティング、レスポンス挙動等) はなく、差分は各ファイルともコメント行で +...+<tt>...</tt> の 1 行置換のみです。


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

    • コントローラの処理内容やレスポンスには一切影響しません。
    • テストやアプリケーションコードの変更は不要です。
  • 対象:

    • Rails API ドキュメント (特に Active Storage の blob/representation 用プロキシ/リダイレクトコントローラのクラスリファレンス) の見た目のみが変わります。
    • ci-skip が付いている通り、ビルドやテスト観点では無視してよい純粋なドキュメント整形変更です。
  • 注意点 (Rails にコントリビュートする側向け):

    • Active Storage に限らず、Rails 本体のコメントで「インラインコード」を書くときは、+...+ ではなく <tt>...</tt> を使うのが今後の推奨スタイルであることが読み取れます。
    • ドキュメントのスタイルを合わせたい場合、この PR と同じ方針で <tt> への統一を検討するとよいです。

  1. 参考情報 (あれば)

#57357 Remove reference to removed QueAdapter

マージ日: 2026/5/13 | 作成者: @p8

  1. 概要 (1-2文で)
    QueAdapter 本体がすでに削除されているにもかかわらず、ActiveJob 内に残っていた QueAdapter への参照を整理・削除する PR です。
    これにより、存在しないアダプタへの不整合な参照がなくなり、Active Job のキューアダプタ定義が実装と一致するようになりました。

  1. 変更内容の詳細

変更ファイル:

  • activejob/lib/active_job/queue_adapters.rb (+0 / -2)

このファイルは、Active Job で利用可能なキューアダプタをまとめて管理している場所です。
ここから QueAdapter に関する2行が削除されています。

おおまかなイメージとしては、以下のようなコードが:

ruby
module ActiveJob
  module QueueAdapters
    # 例: 他のアダプタ
    class AsyncAdapter; end
    class InlineAdapter; end
    # ...
    class QueAdapter; end  # ← こういった参照が残っていた
  end
end

このうち、QueAdapter に関する参照だけが取り除かれた、という形になります(実際のコード行は多少異なりますが、意味としては「QueAdapter という名前をもう queue_adapters.rb では扱わない」変更です)。

PR の説明にあるとおり、QueAdapter 自体は以前のコミット cb22eb2b3628de428171175063371155d70a136c ですでに削除されており、今回の PR はその「後始末」として、残っていた参照だけを消しています。


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

    • Active Job のアダプタ一覧から、実装のない QueAdapter 参照が完全に消えるだけであり、既に QueAdapter は使えない状態だったため、実質的な挙動の変化はほとんどありません。
    • 「参照だけ残っていたものを消した」整理 PR なので、既に QueAdapter なしでテストが通っていた環境では影響はありません。
  • アプリケーション側への影響

    • すでに前のコミット(cb22eb2b3...)の時点で QueAdapter は削除されているため、その時点から以下のような設定はそもそも動きません:
      ruby
      config.active_job.queue_adapter = :que
    • この PR によって新しく壊れるものが増えるわけではなく、「まだどこかに QueAdapter の名残りがあって、それに依存していた」ようなメタプログラミングをしていない限り、追加の影響はありません。
  • ライブラリ/ジェネレータへの影響

    • ActiveJob::QueueAdapters モジュールを列挙してアダプタ一覧を動的に取得しているツールがあった場合、そこから QueAdapter が消えます。
    • ただし、QueAdapter 実装が既に存在しないため、そのようなツールはすでに壊れていたか、もしくは QueAdapter を除外するように対応済みであるべきです。

  1. 参考情報 (あれば)
  • 本PRで言及されている削除コミット:
    • cb22eb2b3628de428171175063371155d70a136c
      • このコミットで QueAdapter 本体が Rails から取り除かれた(詳細はそのコミットログを参照)。
  • Que を使いたい場合の一般的な方針:
    • Rails 標準から QueAdapter が消えたため、Que を利用する場合は外部 gem(例: community 製の ActiveJob 用 Que アダプタ)を使う、もしくは自前でアダプタを実装する形になります。
    • 自作アダプタを使う場合は、例えば以下のような形で設定します:
      ruby
      # config/initializers/que_adapter.rb
      class QueAdapter < ActiveJob::QueueAdapters::Base
        # perform_later されたジョブを Que に投げる処理を実装
      end
      
      # config/application.rb
      config.active_job.queue_adapter = QueAdapter.new
    • ただし、具体的な推奨方法や公式サポート状況は、使用する Rails のバージョンと Que 側のドキュメントを確認する必要があります。

#57351 Document ActiveStorage::Blob#content_type and Blob#metadata

マージ日: 2026/5/13 | 作成者: @p8

  1. 概要 (1–2文で)
    Active Storage の ActiveStorage::Blob#content_type#metadata について、「パブリックにサポートされているメソッドである」ことをコード上で明示するドキュメントコメントが追加されました。実行時の挙動やシグネチャは変わらず、仕様の位置づけが明確化されたドキュメント改善のPRです。

  1. 変更内容の詳細

主な変更点

  • activestorage/app/models/active_storage/blob.rb において、以下の2つの属性メソッドのドキュメントが追加:
    • ActiveStorage::Blob#content_type
    • ActiveStorage::Blob#metadata

これらは ActiveRecord のカラムに対応する通常のアクセサですが、「public API として想定されている」ことがコメントベースのドキュメントとして明示されました。

おおよそ次のような説明がコードコメントとして追記されているイメージです(疑似例):

ruby
# Returns the content type of the file associated with this blob.
# This is a public accessor and can be relied upon in application code.
#
# Examples:
#
#   blob.content_type # => "image/png"
#
class ActiveStorage::Blob < ActiveRecord::Base
  # ...
end
ruby
# Returns a Hash with metadata extracted from the file (e.g. width, height, etc.).
# This is a public accessor and part of the supported API.
#
# Examples:
#
#   blob.metadata # => { "identified" => true, "width" => 800, "height" => 600 }
#
class ActiveStorage::Blob < ActiveRecord::Base
  # ...
end

※実際のコメント文言は多少異なる可能性がありますが、意図としては「よく使われる ActiveRecord アクセサであり、パブリック API としてドキュメント化した」というものです。

サンプル利用コード

すでに一般的に行われている使い方が、公式に「OK」と明文化された形です。

ruby
blob = ActiveStorage::Blob.last

# コンテンツタイプの取得
if blob.content_type.start_with?("image/")
  # 画像向けの処理
end

# メタデータの参照(幅・高さなど)
width  = blob.metadata["width"]
height = blob.metadata["height"]

  1. 影響範囲・注意点
  • 実装上の挙動変更はなし
    追加・削除ともにドキュメントコメントのみであり、メソッドの実装や DB スキーマ、シリアライズ形式などには一切変更がありません。

  • API の位置づけがより公式に
    以前から利用可能だった content_type / metadata が、「内部実装詳細」ではなく「公にサポートされるアクセサ」であることが明文化されたため、ライブラリ作者やアプリケーション開発者がこれらに依存しやすくなります。

  • 将来の後方互換性の期待値向上
    ドキュメントされた public メソッドは、Rails が後方互換性をより強く意識する対象になるため、今後のアップグレード時にも安心して使い続けられる可能性が高くなります。

  • 注意点 (すでに使っている場合)

    • 今まで通りそのまま使って問題ありません。
    • 逆に言うと、「今まで非推奨だったものが急に推奨に変わった」わけではなく、慣例的に使われていたものがドキュメントに追いついた形です。

  1. 参考情報 (あれば)

このPR自体はドキュメントの補強のみですが、今後 Gem や社内ライブラリで ActiveStorage::Blob を拡張する際に、content_type / metadata へ依存することの妥当性を説明しやすくなる変更と言えます。


#57355 Fix docs formatting for ActionController::Live [ci skip]

マージ日: 2026/5/13 | 作成者: @drjayvee

  1. 概要 (1-2文で)
    ActionController::Live のクラスドキュメントの「書式崩れ」「読みづらさ」を修正するために、コメントのフォーマット(RDoc/YARD向け)を整理した PR です。コードの挙動や API は一切変更されておらず、生成ドキュメントの見た目と読みやすさだけが改善されています。

  1. 変更内容の詳細

※PR は actionpack/lib/action_controller/metal/live.rb 内のコメントのみを変更しています。実装コードには差分がありません。

主な変更ポイント(推定含む):

  • ドキュメントの段落・インデント調整

    • サンプルコードや説明文が、RDoc 生成時や IDE(RubyMine 等)で正しく整形されるように、空行やインデントを整理。
    • 行頭のスペース・# コメントの並びを調整することで、コードブロックとして認識されやすくしています。
  • コードブロックのマークアップ改善

    • RDoc では、コメント中のインデントや # code のようなパターンによってコードブロック扱いされます。このルールに合わせて、ActionController::Live のサンプル(ストリーミングレスポンスの例など)のフォーマットを修正しています。
    • Web ドキュメントで「1 行として横に伸びてしまう」「改行が効いていない」などの問題を解消するためのレイアウト調整が主眼です。
  • 箇条書き・説明文の構造化

    • 文章の途中で改行位置が悪く、RDoc で繋がって表示されていたものを段落として分離するなど、読みやすい構造になるように変更。
    • メソッドや注意事項の説明が、IDE 上でもまとまったブロックとして読めるようにしています。

サンプルイメージ(あくまで典型的な修正例):

ruby
# Before:
#   def stream
# response.headers['Content-Type'] = 'text/event-stream'
#   100.times { |i|
#     response.stream.write "data: #{i}\n\n"
#   }
# ensure
#   response.stream.close
# end

# After:
#   def stream
#     response.headers["Content-Type"] = "text/event-stream"
#
#     100.times do |i|
#       response.stream.write "data: #{i}\n\n"
#     end
#   ensure
#     response.stream.close
#   end

上記はあくまでイメージですが、このようにインデントや空行を整えることで、RDoc で「コードブロック」としてきれいに表示されるようにする変更です。


  1. 影響範囲・注意点
  • ランタイム挙動への影響なし

    • コメントのみの変更であり、ActionController::Live の API・挙動・パフォーマンスには一切影響しません。
    • テストや本番環境で何かを確認・対応する必要はありません。
  • 影響するのは主に以下:

    • Rails API ドキュメントサイト(https://api.rubyonrails.org)の ActionController::Live ページの表示。
    • RubyMine などの IDE でホバー表示されるドキュメント、あるいは「定義へジャンプ」時に表示されるコメント。
  • ドキュメントを参考に実装している場合のメリット

    • ストリーミングレスポンスや SSE を ActionController::Live で実装する際のサンプルコードが見やすくなり、誤読やコピペミスのリスクが軽減されます。
    • 「どこからどこまでがサンプルコードなのか」「どこから説明文なのか」が視覚的に明確になります。

  1. 参考情報 (あれば)
  • 対象クラス: ActionController::Live
    • Rack レスポンスを逐次書き込みできるようにし、HTTP ストリーミング / SSE などを実現するためのモジュール。
  • 公開 API ドキュメント(変更反映後がそのうち閲覧可能に)
  • ドキュメント生成:
    • Rails のドキュメントは主に RDoc ベースで生成されており、コメント内のインデントや空行がレイアウトに大きく影響するため、今回のようなフォーマット修正は定期的に行われています。

#56872 Add start_day argument to this_week? for consistency with all_week

マージ日: 2026/5/12 | 作成者: @55728

  1. 概要 (1-2文で)
    Date/Time#this_week?start_day 引数が追加され、all_week / beginning_of_week / end_of_week と同様に呼び出しごとに週の開始曜日を指定できるようになりました。これにより、グローバルな Date.beginning_of_week を書き換えずに、ロケールや用途ごとに柔軟に「今週かどうか」を判定できます。

  1. 変更内容の詳細

追加されたインターフェース

これまで:

ruby
date.this_week?          # 常に Date.beginning_of_week に依存

このPR後:

ruby
date.this_week?           # 従来どおり、Date.beginning_of_week を使用
date.this_week?(:sunday)  # 週の開始を日曜とみなして「今週か?」を判定
date.this_week?(:monday)  # 週の開始を月曜とみなして「今週か?」を判定
  • this_week?(start_day = Date.beginning_of_week) という形で、start_day がオプション引数として追加されています。
  • デフォルト値は従来どおり Date.beginning_of_week なので、既存コードは挙動変更なしで動作します。

他メソッドとの一貫性

既存の関連メソッドはすでに start_day を取っています:

ruby
date.all_week(start_day = Date.beginning_of_week)
date.beginning_of_week(start_day = Date.beginning_of_week)
date.end_of_week(start_day = Date.beginning_of_week)

今回、this_week? にも同じシグネチャが導入されたことで、以下のように一貫したAPIで扱えるようになります:

ruby
range = date.all_week(:monday)
# ...
date.this_week?(:monday) # 同じ基準(Mon–Sun)の「今週か?」判定

想定ユースケース

  • マルチテナント / 多言語アプリ

    • USテナント: 週の開始は :sunday
    • EUテナント: 週の開始は :monday

    グローバル設定を書き換えずに、テナントごと:

    ruby
    start_day = current_tenant.week_start_day # :sunday or :monday
    
    date.this_week?(start_day)
  • レポート・ダッシュボード用途
    1つの画面で異なる週の定義を並列表示:

    ruby
    # 「今週 (Mon–Sun)」か?
    date.this_week?(:monday)
    
    # 「今週 (Sun–Sat)」か?
    date.this_week?(:sunday)

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

    • 引数なし呼び出し (this_week?) の挙動は変わりません。
    • 既存コードに対して破壊的な変更はありません。
  • 並行実行環境での安全性向上

    • これまで「リクエストごとに Date.beginning_of_week= を変更する」ような実装は、マルチスレッド環境で競合するリスクがありました。
    • 今後は this_week?(start_day) を使うことで、グローバル状態を書き換えずに要件を満たせます。
  • 設計上の一貫性

    • 日付計算系のメソッド群で start_day を引数で受けられるようになり、API利用時の混乱が減ります。
    • 新しくコードを書く場合は、all_week / beginning_of_week / end_of_week / this_week? を同じ基準で揃えて使うことが推奨されます。

  1. 参考情報 (あれば)

#57349 Add default #render_in implementation to ActiveModel::Conversion

マージ日: 2026/5/12 | 作成者: @seanpdoyle

  1. 概要 (1-2文で)
    ActiveModel::Conversion にデフォルトの #render_in 実装が追加され、Active Model / Active Record オブジェクトを Action View がどのように HTML / JSON にレンダリングするかをモデル側でカスタマイズしやすくなりました。
    既存の「partial + object」ベースのレンダリング挙動は保ったまま、render model / render renderable: model をフックできる統一的な入口が用意されています。

  1. 変更内容の詳細

2-1. ActiveModel::Conversion#render_in のデフォルト実装追加

ActiveModel::Conversion モジュールに、デフォルトの #render_in が追加されました。
(実際のコードは概ね以下のようなイメージです)

ruby
module ActiveModel
  module Conversion
    # ...

    # Action View から:
    #   render user
    #   render renderable: user
    # などと呼ばれたときに使われる
    def render_in(view_context, &block)
      view_context.render(partial: to_partial_path, object: self, &block)
    end

    # ...
  end
end

ポイント:

  • render_in(view_context) が呼ばれると、
    • partial: to_partial_path
    • object: self を指定して、従来どおりの「パーシャル推論 + オブジェクト割り当て」でレンダリングします。
  • つまり、今まで render @person / render person で使われていた to_partial_path ベースの仕組みを、render_in 経由に抽象化した形です。
  • ActiveModel::Conversion を include しているクラス(Active Record モデルなど)は、 そのままこのデフォルト実装を利用できます。

2-2. ビュー側での利用例

PR説明にある通り、以下のようなパーシャルがあったとします:

erb
<%# app/views/people/_person.html.erb %>

<%= local_assigns[:shout] ? person.name.upcase : person.name %>

Person モデルが ActiveModel::Conversion を含んでいれば、従来どおり次のようにレンダリングできます:

ruby
person = Person.new(name: "Ralph")

render person                                       # => "Ralph"
render person, shout: true                          # => "RALPH"
render renderable: person                           # => "Ralph"
render renderable: person, locals: { shout: true }  # => "RALPH"

ここで、render personrender renderable: person が最終的に person.render_in(view_context) を呼び出し、
その中で partial: person.to_partial_path, object: person を指定した view_context.render が行われます。

2-3. カスタマイズ例(ViewComponent との連携など)

この PR の狙いは、「モデル側が render_in をオーバーライドすることで、ビュー側での描画方法を差し替えられるようにする」ことです。

ViewComponent を例にすると、モデルにこんな実装が可能になります:

ruby
class Person < ApplicationRecord
  include ActiveModel::Conversion

  def render_in(view_context, &block)
    # Person を表示するためのコンポーネントに委譲する
    PersonComponent.new(person: self).render_in(view_context, &block)
  end
end

これで、コントローラやビューからは単に:

ruby
render @person
# または
render renderable: @person

と呼ぶだけで、内部的には ViewComponent によるレンダリングが行われるようになります。
HTML だけでなく、JSON などの別フォーマットに対するレンダリングロジックも、この render_in を起点に制御しやすくなります。

2-4. Lint / ガイド / テストの更新

  • activemodel/lib/active_model/lint.rb
    • Lint モジュールに #render_in の存在チェックが追加されています。
    • Active Model 準拠オブジェクトが render_in を実装していることが、API 契約の一部として明確になりました。
  • guides/source/active_model_basics.md
    • ActiveModel Basics ガイドに render_in 追加と、その使い方の説明が追記されました。
  • actionview / activemodel のテスト
    • render の挙動が render_in と連携すること、
    • ログ出力 / 部分テンプレートレンダリングとの互換性が保たれていること がテストでカバーされています。

  1. 影響範囲・注意点
  • 互換性:
    • デフォルトの render_in は従来の render partial: to_partial_path, object: self と同じ挙動を行うため、既存の render @model / render model: の挙動は基本的に変わりません。
  • モデル側で render_in をオーバーライドする場合:
    • そのモデルを引数にした render の挙動は、すべてその render_in 実装に従います。
    • パーシャルレンダリングに依存しているコードがある場合、render_in の実装でそれ相当の処理(view_context.render partial: ..., object: ...)を維持するか、明示的に互換性を取る必要があります。
  • Lint チェック:
    • ActiveModel::Lint を使用している場合、新たに render_in を実装しているかどうかがチェックされるようになります。
    • ActiveModel::Conversion を include していればデフォルト実装が付与されるため、通常は追加作業不要ですが、 独自に ActiveModel インターフェイスを定義しているクラスや Proxy オブジェクトでは要確認です。
  • ライブラリ側の活用:
    • ViewComponent のように、オブジェクトレンダリングを差し替えたいライブラリは、 モデルやラッパークラスに render_in を実装させることで、Action View の render API と自然に統合できるようになります。

  1. 参考情報 (あれば)
  • この PR のフォローアップ元:
    • #50623 — render における renderable: オプションなど、オブジェクトレンダリングの拡張
    • #46202 — モデルレンダリングのインターフェイス設計に関する議論
  • 関連ドキュメント:
    • Rails Guides: Active Model Basics(render_in に関する新セクションが追加)
  • 関連ライブラリ例:

#57356 Fix ActionText::Editor::Tag#render_in to accept kwargs

マージ日: 2026/5/12 | 作成者: @flavorjones

  1. 概要 (1-2文で)
    Action Text の ActionText::Editor::Tag#render_in が、Action View 側の新しい呼び出し規約(kwargs 付き)に対応していなかったバグを修正した PRです。これにより、独自エディタアダプタのサブクラスが super を呼んだときに ArgumentError が発生する問題が解消されます。

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

背景: Action View の render_in 呼び出し仕様の変更

コミット d24d0b71(PR #50623)以降、Action View は「#render_in を持つオブジェクト」を以下のように呼び出すようになりました:

ruby
renderable.render_in(context, locals: locals, &@block)
  • render_in の arity が 1 でない場合、この形で呼び出される
  • つまり、render_inrender_in(view_context, locals: ..., &block) のようにキーワード引数を受け取る形にしておく必要がある

問題点: ActionText::Editor::Tag#render_in のシグネチャが古い

既存の ActionText::Editor::Tag#render_in は以下のように「第1引数だけ」を取る形になっていました(概念的に):

ruby
def render_in(view_context)
  # ...
end

Action View 側は

ruby
tag.render_in(view_context, locals: locals)

のように第2引数以降を渡すため、サブクラスがドキュメント通りに

ruby
def render_in(view_context, ...)
  # カスタム処理
  super
end

と書いていると、super 呼び出し時に

text
ArgumentError: wrong number of arguments (given 2, expected 1)

が発生していました。
Rails に同梱されている TrixEditor::Tagsuper を呼ばずに本体を再実装しているため、この問題に当たっていなかった一方、Lexxy など外部のアダプタで問題が顕在化しました。

この PR の修正内容

ActionText::Editor::Tag#render_in のメソッドシグネチャを「可変長引数を受け取る形」に広げました:

ruby
# 変更前(イメージ)
def render_in(view_context)
  # ...
end

# 変更後(イメージ)
def render_in(view_context, ...)
  # 既存の実装は基本的にそのまま
end

ポイント:

  • ...(Ruby 2.7+ の引数フォワーディング構文)を使うことで、

    • 位置引数
    • キーワード引数(locals: など)
    • ブロック引数
      をそのまま受け取れる
  • これにより、サブクラス側はこれまで通り

    ruby
    def render_in(view_context, ...)
      # 何か前処理
      super
    end

    のように書いても、Action View から渡された locals: などを壊さずに super に引き渡せる

テストの追加

actiontext/test/unit/editor/tag_test.rb にテストが追加されています(+19 行)。

テストで確認していることのイメージ:

  • ActionText::Editor::Tag を継承したクラスで render_in(view_context, ...) を実装し、super を呼ぶ
  • そのサブクラスを Action View 経由で render したときに
    • 例外(ArgumentError)が発生しない
    • 期待する HTML/出力が得られる

  1. 影響範囲・注意点
  • 影響を受けるのは Action Text のエディタアダプタ (ActionText::Editor::Tag のサブクラス) のみ
    • 特に、Rails のガイドや既存のパターンに従って render_in(view_context, ...) を定義し super を呼んでいる実装
  • この PR による挙動の後方互換性
    • 既存の render_in(view_context) 実装は、そのままでも動作します
    • ベースクラス側が ... を受け取れるようになっただけなので、既存アダプタのシグネチャを変更する必要はありません
  • Action View との整合性が取れたことで、今後のバージョンでも安全
    • Action View が render_in(context, locals: locals, &block) で呼ぶ前提と整合したため、今後このインターフェイス前提でアダプタを実装しても安全です

運用面での注意:

  • Rails をアップデートした際に、Action Text のカスタムエディタアダプタで ArgumentError: wrong number of arguments が出ていた場合、この PR の入ったバージョンに上げることで解消する可能性が高いです
  • 逆に、render_in のシグネチャを独自にいじっている場合(例: 明示的に locals: を定義しているなど)は、super 呼び出しで引数を正しくフォワードしているか確認する価値があります

  1. 参考情報 (あれば)

#57342 Disable freezing of NATIVE_DATABASE_TYPES for SQLite3 adapter

マージ日: 2026/5/12 | 作成者: @tahsin352

  1. 概要 (1-2文で)
    SQLite3 アダプタの NATIVE_DATABASE_TYPESfreeze しないようにし、外部の gem などがこのハッシュを書き換えてカスタム型を登録できるように戻した PR です。
    MySQL アダプタで行われた同種の対応 (#57333) を SQLite3 版でも行うフォローアップです。

  1. 変更内容の詳細

何をしたか

  • activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb 内で
    NATIVE_DATABASE_TYPES.freeze されていたのをやめた(もしくは freeze していた行を削除/変更)。

    • これにより、実行時に他のライブラリが NATIVE_DATABASE_TYPES ハッシュを変更できるようになります。
  • その挙動を保証するテストを追加: activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb に「NATIVE_DATABASE_TYPES を変更できること」を確認するテストが追加されています。

背景・モチベーション

  • NATIVE_DATABASE_TYPES は Active Record アダプタにおける「型定義の拡張ポイント」として昔から使われており、
    • 例: neighboractiverecord-enhancedsqlite3-adapter のような gem が、vector 型など独自型を登録するために、このハッシュを直接変更しています。
  • 一方で、最近の変更でこのハッシュに .freeze が入った結果、
    ruby
    ActiveRecord::ConnectionAdapters::SQLite3Adapter::NATIVE_DATABASE_TYPES[:vector] = { name: "vector" }
    # => FrozenError: can't modify frozen Hash
    といったエラーが「ロード時(load time)」に発生し、これらの gem がクラッシュするようになりました。
  • PostgreSQL アダプタのドキュメントにも「この定数を編集してカスタム型を追加できる」とあり、 この振る舞いは公式に想定された拡張ポイントである、という前提に立っています:
  • すでに MySQL アダプタでは同様の理由で .freeze を外す対応(#57333)が入っており、 それに合わせて SQLite3 アダプタでも同じ方針に揃えた、という位置づけです。

(想定されるコード例)

この PR によって、以下のようなコードが再び正常に動くようになります:

ruby
# config/initializers/sqlite_types.rb など
ActiveRecord::ConnectionAdapters::SQLite3Adapter::NATIVE_DATABASE_TYPES[:vector] = {
  name:  "vector",
  limit: 1536,
}

freeze されているとこの書き換えが FrozenError になりますが、PR 後は問題なく通ります。


  1. 影響範囲・注意点
  • 影響を受けるのは SQLite3 アダプタを使っているアプリケーションと、その上で動く拡張 gem です。
    • NATIVE_DATABASE_TYPES を書き換える系の gem(neighbor, activerecord-enhancedsqlite3-adapter など)は、これによりエラーが解消されるはずです。
  • セマンティクスの変更点:
    • NATIVE_DATABASE_TYPES は immutable な定数ハッシュ」という前提は成り立たなくなり、
      他のコードがこれを変更しうる という前提で扱う必要があります。
    • これは以前の Rails の挙動(freeze 前)への“ロールバック”に近いため、
      「古くからの慣習に戻る」という意味では後方互換的な変更です。
  • パフォーマンス面:
    • freeze による微小な最適化(変更検知や GC の観点)は失われますが、通常のアプリケーションで体感できるレベルの影響はほぼないと考えられます。
  • セキュリティ/予期せぬ変更のリスク:
    • 任意の初期化コードや gem が NATIVE_DATABASE_TYPES を上書きできるため、 誤った変更により型マッピングが壊れる可能性はあります。
    • ただし、これは freeze 前の世界と同様のリスクであり、新たに増えたものではありません。

  1. 参考情報 (あれば)

#55978 Enable config.asset_host to read from environment by default

マージ日: 2026/5/12 | 作成者: @stevepolitodesign

  1. 概要 (1-2文で)
    Rails の config.asset_host がデフォルトで ENV["CDN_HOST"] を参照するようになり、環境変数を設定するだけで CDN 経由で静的アセットを配信できるようになりました。ENV["CDN_HOST"] が未設定の場合はこれまで通り asset_host = nil のままで、挙動は変わりません。

  1. 変更内容の詳細

何が変わったか

config/environments/production.rb のデフォルト設定(およびテスト用ダミーアプリ)が、以下のような形になりました。

ruby
# 旧来のイメージ(例)
# config.asset_host = "https://assets.example.com"

# 今回の変更後(テンプレート)
config.asset_host = ENV["CDN_HOST"]
  • railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
    • 新規に生成される Rails アプリの production.rb テンプレートが config.asset_host = ENV["CDN_HOST"] という形に変更。
  • actionmailbox/test/dummy/.../production.rb
  • actiontext/test/dummy/.../production.rb
  • activestorage/test/dummy/.../production.rb
    • これらテスト用ダミーアプリの production 設定も同様に ENV["CDN_HOST"] を参照するよう更新。
  • railties/CHANGELOG.md
    • 上記の仕様追加が CHANGELOG に追記されています。

挙動の詳細

  • ENV["CDN_HOST"] が「設定されていない」場合

    • Ruby では ENV["CDN_HOST"] #=> nil となるため、config.asset_host = nil と同等になります。
    • asset_host 無しの従来挙動と変わらない(=相対パスでアセットを配信)。
  • ENV["CDN_HOST"] が「設定されている」場合

    • 例: CDN_HOST=https://cdn.example.com
      → Rails 側で config.asset_host = "https://cdn.example.com" が自動的にセットされる。
    • 明示的に config.asset_host を書き換えなくても、CDN からアセットを配信する設定になる。

サンプル: 実際の利用イメージ

1) 本番環境の設定(Rails 側)

config/environments/production.rb は、生成直後から次のようになっている想定です:

ruby
Rails.application.configure do
  # ...
  config.asset_host = ENV["CDN_HOST"]
  # ...
end

開発者はここを編集せず、そのまま使えます。

2) デプロイ先での環境変数設定

  • Heroku / Render / ECS / Kubernetes などで以下を設定:
bash
export CDN_HOST="https://cdn.example.com"

もしくは YAML など:

yaml
env:
  - name: CDN_HOST
    value: "https://cdn.example.com"

これだけで、Rails のアセット URL が https://cdn.example.com/assets/... のように生成されるようになります。


  1. 影響範囲・注意点

影響範囲

  • 新たに生成される Rails アプリの production 環境設定
  • Rails リポジトリ内のダミーアプリ(Action Mailbox / Action Text / Active Storage)の production 設定
  • 既に production.rb をカスタマイズしている既存アプリには即時影響はありません(既存アプリのファイルは勝手に書き換えられないため)。

注意点

  1. CDN_HOST が誤設定されていると、全アセット URL が壊れる

    • 例: CDN_HOST=cdn.example.com(スキーム無し)
      → ブラウザ側の解釈によっては意図しない URL になる可能性があります。
    • 一般には https://cdn.example.com のようにスキームを含めて設定すべきです。
  2. config.asset_host を手動で設定している場合の優先度

    • 既存アプリで config.asset_host = "https://old-cdn.example.com" のように明示設定している場合、その値が使われます。
    • 今回の PR は新規生成テンプレートのデフォルトを変えたものであり、手動設定を上書きするものではありません。
  3. 環境変数ベースの設定ポリシーへの移行

    • Rails ガイド(Asset Pipeline ガイド)の例と整合する形で CDN_HOST が採用されています。
    • 組織で ASSET_HOST など別名を使っている場合は、今まで通り production.rb を編集するか、ENV["ASSET_HOST"] || ENV["CDN_HOST"] のように独自に拡張する選択肢もあります。

  1. 参考情報 (あれば)
  • Rails Guides: Asset Pipeline – Set up a CDN to serve static assets
    https://guides.rubyonrails.org/asset_pipeline.html#set-up-a-cdn-to-serve-static-assets
    (今回の ENV["CDN_HOST"] はこのガイド中のサンプルと名前を合わせています)

  • config.asset_host の公式ドキュメント(Action Pack / Rails Guides)

    • アセット URL の生成、メールテンプレートでの URL 生成などにも影響するため、CDN 利用時はここを理解しておくと運用しやすくなります。

#57352 Properly reset fixtures cache after non-transactional tests

マージ日: 2026/5/12 | 作成者: @byroot

  1. 概要 (1-2文で)
    このPRは、トランザクションを使わないテスト実行後に、fixtures のキャッシュが正しくリセットされない問題を修正し、ActiveRecord のテストフィクスチャ機構の一貫性を高めたものです。あわせて、この挙動を検証するテストと、リークチェッカー側のサポートを追加しています。

  1. 変更内容の詳細

※実際の差分は要約ベースですが、挙動としては以下のような変更が行われています。

2-1. ActiveRecord::TestFixtures におけるキャッシュリセットの修正

activerecord/lib/active_record/test_fixtures.rb において、

  • トランザクションを使うテスト (use_transactional_tests = true)
    • これまでもテストごとにトランザクションのロールバックでDB状態は戻っていたが、
    • フィクスチャの内部キャッシュ(ロード済みフィクスチャなど)については、
      「トランザクション前提」でしかリセットされていない箇所があった。
  • トランザクションを使わないテスト (use_transactional_tests = false、あるいは self.use_transactional_tests = false なテストケース)
    • DBはtruncateや手動cleanupなど別手段でリセットされる一方、
    • ActiveRecord のフィクスチャキャッシュ自体は前のテストの状態が残りうる、という不整合があり得た。

このPRでは、上記の不整合を解消するために、
**「非トランザクションテストが終わるときにも、fixtures のキャッシュを確実にリセットする」**ようロジックが修正されています。

イメージとしては、以下のような処理が追加・修正されています(擬似コード):

ruby
def after_teardown
  super
  # 以前は transactional な場合にしか呼ばれていなかった、あるいは条件が不十分だった
  ActiveRecord::FixtureSet.reset_cache # ← これを非トランザクションテストでも確実に実行
end

実際には FixtureSet まわりや fixtures_cache のクリア処理が適切なフックから呼ばれるように整理されているイメージです。

2-2. regression テストの追加

activerecord/test/cases/fixtures_test.rb にテストが追加・拡張されています(+6/-2)。

テスト内容としては概ね:

  • 非トランザクションなテストケースをセットアップし、
  • 複数のテストでフィクスチャを読み込ませる、
  • テスト間でのフィクスチャキャッシュの状態を確認し、
  • PRで導入したリセット処理がないと失敗する(= バグを再現できる)テストを追加

といった「退行テスト (regression test)」になっているはずです。

これにより、今後同様の変更が行われた場合にも、
非トランザクションテストにおけるフィクスチャキャッシュのクリア漏れが CI 上で検知できるようになります。

2-3. tools/support/leak_checker.rb の拡張

tools/support/leak_checker.rb に +2 行の変更が入っています。

これは Rails の internal テストツールで、
オブジェクトやグローバル状態の「リーク」を検知するためのサポートコードです。

ここでは、

  • フィクスチャのキャッシュ(おそらく ActiveRecord::FixtureSet 周辺)に関するリーク、
  • もしくはテスト後にクリーンアップされるべき内部状態

をリークチェッカーの監視対象に追加するための1〜2行の設定が加えられています。

これによって、fixtures キャッシュがリセットされないような退行が起きた場合、
リークチェッカーが失敗することで問題を早期に検知できるようになります。


  1. 影響範囲・注意点

3-1. 影響範囲

  • 対象: ActiveRecord::TestFixtures を利用している Rails のテスト環境全般
    • 特に use_transactional_tests = false を利用しているテストスイート
    • RSpec などから ActiveSupport::TestCase ベースのフィクスチャを利用している場合も間接的に影響
  • 主な影響:
    • テスト間でフィクスチャキャッシュが持ち越されないようになるため、
    • これまで「たまたま」前のテストのキャッシュに依存していたケースがあれば、挙動が変わる可能性はあります。
    • ただし本来の仕様からすると、今回の修正が正しい挙動であり、依存していたコードの方が脆いと考えられます。

3-2. 開発者が意識すべき点

  • もしプロジェクトで use_transactional_tests = false を明示的に使っていて、
    • フィクスチャの読み込みが「一回だけ」であることに暗黙依存している処理があれば挙動が変わるかもしれません。
  • しかし、通常の Rails の使い方では、
    • 「テストケースごとにフィクスチャ状態はクリーンである」ことが期待されるため、
    • このPRによってより予測可能で一貫性のあるテスト挙動になります。
  • また、CI や開発環境で:
    • リークチェッカーが有効な場合、フィクスチャキャッシュのクリア漏れによる失敗が検出されやすくなります。
    • これは基本的にはメリットであり、特別な対応は不要です。

  1. 参考情報 (あれば)
  • 抽出元PR: https://github.com/rails/rails/pull/57326
    • このPRは、より大きな改善の一部を切り出したものと思われます。
  • 関連コード:
    • activerecord/lib/active_record/test_fixtures.rb
      • ActiveRecord::TestFixtures モジュール: fixtures のロードとキャッシュ、トランザクションテストまわりのロジック
    • ActiveRecord::FixtureSet (内部的に利用)
      • reset_cache / all_cached_fixtures 等のメソッドがキャッシュ管理を担当
  • 類似の文脈:
    • DB Cleaner や並列テストなど、トランザクションに依存しないテスト戦略と fixtures を併用しているプロジェクトでは、この種のキャッシュ挙動がバグの温床になりやすく、今回の修正はそうした問題の一つを潰す意図があります。

#36433 Fix unexpected behavior for dependent: :purge

マージ日: 2026/5/11 | 作成者: @moveson

  1. 概要 (1-2文で)
    has_one_attached / has_many_attacheddependent: :purgedependent: :purge_later を指定した場合、親レコード削除時に添付ファイルが「detach(関連のみ削除)」されてしまっていた不具合を修正し、「実際に purge(blob も含め削除)」されるようにした PR です。これにより dependent: :detach / :purge / :purge_later がそれぞれ意図どおりに動作するようになります。

  1. 変更内容の詳細

何が問題だったか

Active Storage の関連定義:

ruby
class User < ApplicationRecord
  has_one_attached :photo, dependent: :purge
end

このように dependent: :purge を指定していても、user.destroy を呼んだときの挙動は「purge ではなく detach」になっていました。

  • 期待される挙動:

    • dependent: :detach
      → attachment レコードと親の紐付けのみ削除(blob 本体は残る)
    • dependent: :purge
      → attachment レコード+blob レコード+ストレージ上のファイルをまとめて削除
    • dependent: :purge_later
      → 上記 purge を非同期ジョブで実行
  • 実際の挙動(修正前):

    • dependent: :purge / :purge_later でも、親 destroy 時は detach 相当の処理になっていた

つまり dependent: オプションで指定した値が destroy 連鎖時にうまく反映されていない、というバグです。

PR での主な変更点

1) ActiveStorage::Attachment の dependent 動作の修正

activestorage/app/models/active_storage/attachment.rb が修正され、dependent の指定に従って正しくメソッドが呼ばれるようになっています。

ざっくりいうと、以下のようなマッピングがきちんと効くようになった形です(実際のコードはもう少し抽象化されています):

ruby
# 擬似的なイメージ
case dependent
when :detach
  attachment.destroy
when :purge
  attachment.purge
when :purge_later
  attachment.purge_later
end

従来は destroy のフック内で destroy 相当しか呼ばれておらず、dependent が無視される経路があったため、ここが修正対象になっています。

2) モデル側の attached 定義の修正

activestorage/lib/active_storage/attached/model.rb もわずかに変更されています。
ここは has_one_attached / has_many_attached 定義時に dependent: オプションを受け取り、ActiveStorage::Attachment の関連に適切な dependent 動作を委譲する部分です。

今回の修正によって:

ruby
class User < ApplicationRecord
  has_one_attached :photo, dependent: :purge
  has_many_attached :images, dependent: :purge_later
end

のような設定が、destroy 連鎖時にちゃんと purge / purge_later で処理されるようになっています。

3) テストの追加・強化

以下のテストが追加・拡張されています。

  • activestorage/test/models/attached/one_test.rb
    • has_one_attached + dependent: :purge / :purge_later が destroy 時に正しく動くかのテスト追加
  • activestorage/test/models/attached/many_test.rb
    • has_many_attached 版の同様のテスト追加
  • activestorage/test/models/reflection_test.rb
    • Active Storage の関連リフレクションが dependent: を正しく解釈しているかのテストを大幅追加
  • activestorage/test/test_helper.rb
    • テストヘルパーに purge / purge_later 検証のための補助が足された可能性が高いです

これにより、dependent: :detach / :purge / :purge_later のそれぞれがリグレッションテストでカバーされるようになっています。

4) CHANGELOG の更新

  • activestorage/CHANGELOG.md に「dependent: :purge の挙動修正」が明記され、挙動の変更がドキュメント化されています。

  1. 影響範囲・注意点

挙動が「仕様どおり」に変わる点に注意

これまで誤って「detach 相当」になっていた部分が、今後は「purge / purge_later 相当」で動作するようになります。

具体的には:

ruby
class User < ApplicationRecord
  has_one_attached :photo, dependent: :purge
  has_many_attached :images, dependent: :purge_later
end

user.destroy を呼んだとき:

  • 修正前:
    • attachment レコードと User の紐付けだけが消え、blob や実ファイルは残る(detach 相当)
  • 修正後:
    • photo は即時削除(purge)
    • images は Active Job 経由で非同期に削除(purge_later)

そのため、「これまで誤った挙動に依存していたコード」があると、以下のような影響が出ます。

  • 親レコード削除後もファイルを残して再利用していたケース
  • 「消えないと思っていた」ファイルが実際には削除されるようになる
  • 削除ジョブ(purge_later)が増えることによるジョブキューの負荷増大

既存アプリでの確認ポイント

  • has_one_attached / has_many_attacheddependent: :purge / :purge_later を既に使っている場合:
    • その添付ファイルは「親 destroy 時に完全削除される」想定で OK かを再確認する
    • もし「detach したいだけ」なら dependent: :detach に変更する
  • Storage(S3, GCS, ローカルなど)からの物理削除が増えるので、誤削除がないかステージング環境で挙動確認する
  • purge_later を多用している場合:
    • ActiveJob / キューワーカーのスループットとジョブ量をモニタリングする

  1. 参考情報 (あれば)

#50623 Pass render options and block to calls to #render_in

マージ日: 2026/5/11 | 作成者: @seanpdoyle

  1. 概要 (1-2文で)
    render にオブジェクトを渡したときに呼ばれる #render_in に対して、呼び出し元のオプション(locals など)とブロックを正しく引き渡せるようにした変更です。既存の render_in(view_context) というシグネチャは非推奨となり、今後はオプションとブロックを受け取る形への移行が求められます。

  1. 変更内容の詳細

何が問題だったか

  • Rails では、render some_object のように「#render_in を持つオブジェクト」を描画できる仕組みが既にありました(ViewComponent などが利用)。
  • しかし従来の実装は、呼び出し側で指定したオプション(例: locals: { ... }name: "...")やブロックを #render_in に渡していませんでした。
  • そのため、render obj, name: "Local"render(obj) { ... } と書いても、obj#render_in 側からはそれらにアクセスできず、「自分自身のコンテキストだけで完結して描画する」前提になっていました。

今回の変更のポイント

  1. render#render_in を呼ぶ際に、キーワード引数・ブロックを渡すように拡張

    • これにより、render 呼び出し時の オプション(locals 相当)とブロックを、#render_in 側で受け取って活用 できるようになりました。

    例(PR に記載のサンプル):

    ruby
    class Greeting
      def render_in(view_context, **options, &block)
        if block
          view_context.render html: block.call
        else
          view_context.render inline: <<~ERB.strip, **options
            Hello, <%= name %>
          ERB
        end
      end
    end
    
    render(Greeting.new)                    # => "Hello, World"
    render(Greeting.new, name: "Local")     # => "Hello, Local"
    render(Greeting.new) { "Hello, Block" } # => "Hello, Block"
    • render(Greeting.new, name: "Local")name: "Local"render_in に渡され、inline テンプレートで name として参照可能になります。
    • ブロック付き render(Greeting.new) { ... } のブロックも &block として render_in に届きます。
  2. render_in(view_context) シグネチャの非推奨化

    • 既存の多くのライブラリ(例: ViewComponent など)は以下のようなシグネチャで実装していました:

      ruby
      def render_in(view_context)
        # ...
      end
    • 今回の変更では、オプションとブロックを受け取れるシグネチャへの移行を推奨し、それに伴い従来の「引数は view_context のみ」の形を deprecate しました。

    • 推奨される形は、少なくとも可変キーワード引数とブロックを受け取れるようにすることです:

      ruby
      def render_in(view_context, **options, &block)
        # options と block を使う/無視するのは自由
      end
    • これにより、将来的に render から渡される情報を失わずに受けられるようになります。

  3. Rails 内部のレンダリング処理全体の対応

    • actionpack / actionview 両方のレンダラ関連クラスが修正され、renderable オブジェクトを扱う箇所で
      • オプション引き回し
      • ブロックの受け渡し を行うように整理されています。
    • コントローラ側 (ActionController::Metal#render など) とビュー側 (ActionView::Renderer など) の両レイヤでテストが追加され、「コントローラからの render / ビュー内からの render」の両方で挙動が保証されています。
    • ガイド (layouts_and_rendering.md) と 8.1 のリリースノートにも、render_in を持つオブジェクトの使い方とこの挙動が追記されています。

  1. 影響範囲・注意点

影響があるコード

  • #render_in(view_context) を実装しているクラス全般
    例:
    • 独自の「レンダラブル」オブジェクト
    • ViewComponent や類似のコンポーネントライブラリ
    • テンプレート以外のオブジェクトを render している gem / アプリのコード

注意点・移行ガイド

  1. render_in のシグネチャを広げる

    既存:

    ruby
    def render_in(view_context)
      # ...
    end

    推奨:

    ruby
    def render_in(view_context, **options, &block)
      # options や block を使わない場合でも、
      # 受け取れるようにはしておくと将来互換性が高い。
    end
    • こうしておくことで、render(obj, foo: :bar)render(obj) { ... }foo: やブロックが消えずに届きます。
    • シグネチャを広げるだけなら後方互換は保たれる(既存の呼び出しでも動く)ため、安全な変更です。
  2. options / block を使いたい場合の実装例

    • locals 的に扱う:

      ruby
      def render_in(view_context, **options, &block)
        view_context.render inline: <<~ERB, locals: options
          Hello, <%= name %>
        ERB
      end
      
      # 呼び出し側
      render(Greeting.new, name: "Rails") # => "Hello, Rails"
    • ブロックを部分テンプレートの中で評価したい:

      ruby
      def render_in(view_context, **options, &block)
        if block
          # capture 等でラップしてもよい
          content = view_context.capture(&block)
          view_context.render inline: "<div>#{content}</div>"
        else
          view_context.render inline: "<div>No block</div>"
        end
      end
  3. ディプリケーション警告への対応

    • Rails 8.1 以降で、render_in(view_context) だけを実装しているクラスは 非推奨警告が出る可能性があります(実際の警告メッセージ内容はガイド・CHANGELOG を参照)。
    • ライブラリ作者は、この変更に追随しておかないと将来的な Rails のメジャーアップデート時に動かなくなるリスクがあります。
  4. テストの更新

    • 自作の render_in 実装を持つコンポーネントをテストしている場合、
      • render(component, foo: :bar) のようなテスト
      • render(component) { "block content" } のようなテスト を追加すると、今回の新仕様にきちんと乗れているかを検証できます。

  1. 参考情報 (あれば)
  • 元 issue / PR:
  • ドキュメント:
    • guides/source/layouts_and_rendering.md
      render にオブジェクトを渡すパターンと render_in の説明が更新
    • guides/source/8_1_release_notes.md
      → 本変更のハイライトが簡潔に記載
  • 実装の詳細を追いたい場合:
    • actionview/lib/action_view/template/renderable.rb
    • actionview/lib/action_view/renderer/renderer.rb
    • actionpack/lib/action_controller/metal/rendering.rb
      あたりを見ると、renderable 経由の処理フローとオプション/ブロックの引き回しが確認できます。

#57346 Rewrite Arel tests using ActiveSupport::TestCase instead of Minitest::Spec

マージ日: 2026/5/11 | 作成者: @rafaelfranca

  1. 概要 (1-2文で)
    Rails の Arel 周辺テストを、Minitest::Spec スタイルから ActiveSupport::TestCase ベースのクラシックな xUnit スタイル (test "..." do) に全面書き換えした PRです。これにより megatest などで spec 構文をサポートする必要がなくなり、テスト基盤の統一とシンプル化が図られています。

  1. 変更内容の詳細

主眼: Minitest::Spec → ActiveSupport::TestCase への書き換え

この PR のほぼすべては「テスト記述スタイルの移行」です。Arel の各種テストが、以下のような spec スタイルから:

ruby
# 旧: Minitest::Spec スタイルの例
describe Arel::Nodes::And do
  it "joins two predicates with AND" do
    node = Arel::Nodes::And.new([left, right])
    node.to_sql.must_equal "(foo = 1 AND bar = 2)"
  end
end

ActiveSupport::TestCase を継承するクラシックなテストクラス・スタイルへ変換されています:

ruby
# 新: ActiveSupport::TestCase スタイルの例
class ArelNodesAndTest < ActiveSupport::TestCase
  def test_joins_two_predicates_with_and
    node = Arel::Nodes::And.new([left, right])
    assert_equal "(foo = 1 AND bar = 2)", node.to_sql
  end
end

代表的な変換パターン:

  • describe Foo doclass FooTest < ActiveSupport::TestCase
  • it "does something"def test_does_something
  • before dodef setup
  • after dodef teardown
  • 期待値:
    • must_equalassert_equal
    • wont_equalrefute_equal
    • must_be_nilassert_nil
    • must_be :>, 1assert_operator obj, :>, 1
    • など、Minitest::Spec 用マッチャを xUnit 形式のアサーションに置き換え

ファイル単位では、以下のような Arel テストが対象になっています (抜粋):

  • activerecord/test/cases/arel/attributes/attribute_test.rb
  • activerecord/test/cases/arel/attributes/math_test.rb
  • activerecord/test/cases/arel/collectors/*_test.rb
  • activerecord/test/cases/arel/crud_test.rb
  • activerecord/test/cases/arel/delete_manager_test.rb
  • activerecord/test/cases/arel/insert_manager_test.rb
  • activerecord/test/cases/arel/nodes/*_test.rb
  • 他 Arel 関連 50 ファイル超

+4195 / -4705 という大きな差分ですが、ほぼ「構文変換」「アサーション API の置き換え」であり、テストの意図やカバレッジそのものは極力維持されています。

Arel テストヘルパの調整

activerecord/test/cases/arel/helper.rb も修正されています。ここでは:

  • Minitest::Spec 前提だった require / include / DSL の削除・整理
  • ActiveSupport::TestCase から継承するためのヘルパーメソッドの位置・モジュール化調整

などが行われていると考えられます。これにより、Arel 用のテストクラスがすべて同じテスト基盤 (ActiveSupport::TestCase) 上に乗るように統一されています。

本体コード (プロダクションコード) への軽微な変更

activerecord/lib/arel/nodes/casted.rb にも +2 行の追加があり、ここだけは本体コードが触られています。ただしテストリライトに付随する「より正確なテストのための public API 補助」や「警告回避」「互換性向上」のような、小さな調整である可能性が高いです。

例としてありそうな変更イメージ:

  • inspect / to_s の振る舞いをテストしやすくする
  • 属性のデフォルト値や型情報を一貫させる
  • Ruby のバージョン差異対応

いずれにせよ、PR の主眼はテスト移行であり、このファイルの変更はそれを支える補助的なものです。


  1. 影響範囲・注意点

影響範囲

  • 外部 API / 振る舞い:
    • Rails / Active Record / Arel の公開 API やクエリ生成ロジックには原則として変更なし。
    • ユーザーコードへの直接的な破壊的変更は想定されません。
  • テスト基盤:
    • Rails コアの Arel テストは Minitest::Spec に依存しなくなり、ActiveSupport::TestCase に統一されます。
    • megatest (巨大テストスイートの統合ランナー的なもの) は spec DSL をサポートしなくてよくなり、メンテナンスが簡素化されます。

注意点 (Rails コントリビュータ / メンテナ向け)

  • Arel 周辺で新たにテストを書く/直す時は:
    • describe / it / must_* などの spec 記法は使用せず、
    • class SomeFeatureTest < ActiveSupport::TestCase + def test_... + assert_* を使う必要があります。
  • 既存の Arel テストを参照する際は、「クラス名ベース」「メソッド名ベース」の xUnit スタイルになります。
    • テストのグルーピングを describe のネストに頼っていた場合、その構造がクラス/メソッドへフラットに実装されているので読み替えが必要です。
  • 外部で Rails テストスタイルを真似ている社内コード / gem 等がある場合:
    • 「Rails 本体が spec スタイルを使っているから自分たちも」という前提は崩れるので、今後は ActiveSupport::TestCase を基準にするのが無難です。

リグレッションリスク

  • 「テストスタイルのみの変更」とはいえ、大規模な置き換えでテストのロジックが変わっている可能性には注意が必要です。
    • アサーションの引数順 (expected / actual) の入れ替えミス
    • beforesetup 変換時の shared state の取り扱いミス
    • spec DSL 特有のコンテキスト共有パターンがうまく xUnit に写せていない箇所
  • Rails 本体の CI が通ってからマージされているため、大きな不具合は出尽くしていると考えられますが、Arel の挙動を細かく利用しているような高度なユースケースでは、問題切り分けの際に「テストの意図」を再確認する場面が出るかもしれません。

  1. 参考情報 (あれば)
  • この PR は Related to #57326 とされており、#57326 側で:
    • megatest やテスト実行環境の整理
    • Minitest::Spec 依存の削減・削除 といった、より広いテスト基盤の方針が議論されていると推測されます。
  • Rails の推奨テストスタイル:
    • コアは一貫して ActiveSupport::TestCase ベースに寄せる方向にあり、RSpec や Minitest::Spec スタイルは「外部で自由に使うもの」という整理になりつつあります。
  • 実務上、
    • Arel の挙動をカスタムで拡張している場合でも、この PR が直接 API を壊すことはほぼなく、通常はアップデート時の追加作業は不要です。
    • コントリビュートする際だけ、新しいテストスタイルに合わせれば十分です。

#57347 Document Active Job continuation instrumentation events

マージ日: 2026/5/11 | 作成者: @bdewater-thatch

  1. 概要 (1-2文で)
    Active Job の「ジョブ継続 (continuation)」機能で発火する Active Support Instrumentation のイベントについて、公式ガイド(active_support_instrumentation.md)にドキュメントを追加した PR です。機能自体の実装変更はなく、「どんなイベントがいつ、どういうペイロードで通知されるか」を明文化しています。

  1. 変更内容の詳細

※ このPRは guides/source/active_support_instrumentation.md のドキュメント追記のみです。コードの挙動は変わっていません。

追加された内容のポイント

  • Active Job の「Continuation」関連で発火するイベントが、新たなセクションとして Active Support Instrumentation ガイドに追加されています。
  • 具体的には、以下のようなイベントが対象になっていると考えられます(#55127のフォローアップであり、「Continuation」によるチェーン実行をトレースするためのもの):
    • enqueue_continuation.active_job
    • perform_start_continuation.active_job / perform_continuation.active_job 相当のイベント
    • それぞれのイベントで受け取れる payload の key(例: job, adapter, queue, enqueued_at, continuation_token など)がガイド内で説明されている構成です。

ガイドでは一般的に、Instrumentation イベントごとに以下の形式で説明されますが、今回の追加もこれに合わせています:

  • イベント名 (例: enqueue_continuation.active_job)
  • 発火タイミング
    • Continuation を伴うジョブがキューに積まれるとき
    • 前のジョブ完了後に次のジョブとして実行がスケジュールされるとき など
  • payload の内容
    • job : 実行されるジョブオブジェクト
    • job_id : ジョブID
    • queue : キュー名
    • adapter : 使用されるキューアダプタ
    • Continuation 特有の識別情報(トークンや元となるジョブ情報) など
  • 購読例(サンプルコード)

サンプルコード(イメージ・ガイドに沿った形):

ruby
ActiveSupport::Notifications.subscribe("enqueue_continuation.active_job") do |event_name, started, finished, unique_id, payload|
  Rails.logger.info(
    "[#{event_name}] continuation for job=#{payload[:job].class} " \
    "job_id=#{payload[:job_id]} queue=#{payload[:queue]}"
  )
end

あるいは、ActiveSupport::Notifications と組み合わせてメトリクス用ミドルウェアから利用できる形で説明されています。


  1. 影響範囲・注意点
  • アプリの動作には影響なし
    コードベースの変更はなく、ドキュメントのみの追加なので、既存アプリケーションの挙動に影響はありません。

  • Observability / Monitoring の精度向上

    • Continuation を利用してジョブをチェーン実行しているアプリケーションで、
      • 「どのジョブからどのジョブに引き継がれたか」
      • 「どのタイミングで enqueue / perform されたか」
        を ActiveSupport::Notifications で正しくトレースしやすくなります。
    • メトリクス収集ツール(StatsD, Prometheus, OpenTelemetry など)と連携させる際に、正しいイベント名・payload キーを参照できるようになります。
  • バージョンに注意
    PR説明にある通り、「ガイドに表示させるために backport が必要」とコメントされています。

    • Continuation 機能およびそのイベントが導入された Rails バージョンよりも古いガイドには、この記述が載らない可能性があります。
    • 実際に自分の使っている Rails バージョンでこれらのイベントが存在するかどうかは、ActiveSupport::Notifications で簡単な subscribe を書いて確認するか、該当バージョンの CHANGELOG / API ドキュメントを確認するのが安全です。

  1. 参考情報 (あれば)

#57341 Add tests for constants that are expected to be mutated

マージ日: 2026/5/11 | 作成者: @paracycle

  1. 概要 (1-2文で)
    Rails本体で「本来ミューテート(変更)されることが前提の定数」に対して、その期待をテストで明示するPRです。
    mutable な定数を一律に freeze した対応 (#57323) のあとに見つかった「実は変更されることが仕様」というケースをテストで保証し、今後のリグレッションを防ぐ目的があります。

  1. 変更内容の詳細

背景

  • 先行PR #57323 で「ミュータブルな定数を freeze する」対応が入りました。
  • その結果、以下2件のPRで「freeze してはいけない(利用者が変更する前提の)定数」が判明し、修正されています。
    • #57332
    • #57333
  • しかし、これらの「定数がミューテートされることを許容する」という仕様がテストに明文化されておらず、将来的にまた誤って freeze されるリスクがありました。

このPR #57341 では、その2つのケースについて「ミューテート可能であること」を確認するテストを追加しています。

具体的な変更点

変更ファイルは2つで、いずれもテストの追加のみです(削除はなし、合計16行追加)。

  1. activerecord/test/cases/adapters/abstract_mysql_adapter/sql_types_test.rb (+8)
  2. activesupport/test/core_ext/date_ext_test.rb (+8)

※実際のコードはPR本文には載っていませんが、文脈から以下のような内容になっていると考えられます。

1) MySQL アダプタの SQL 型関連の定数に対するテスト

対象: ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter まわりの「SQL型マッピング」を表す定数(例: NATIVE_DATABASE_TYPES など)

テストの意図:

  • 「ユーザー(アプリケーション側)がこの定数をミューテートしてカスタマイズする」ことを前提としている箇所に対し、
    • テスト内で定数を変更(あるいは <<, []=, merge! などの破壊的操作)してもエラー(FrozenError)にならない
    • かつ、その変更が実際にアダプタの振る舞いに反映される
      ことを確認しているはずです。

サンプルイメージ(概念的な例):

ruby
def test_mysql_native_types_are_mutable
  types = ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::NATIVE_DATABASE_TYPES

  assert_nothing_raised do
    types[:foo] = { name: "foo" }
  end

  assert_equal "foo", types[:foo][:name]
end

※あくまでイメージですが、実際には「#57332 で修正した定数」に対する操作がテストされています。

2) Active Support の Date 拡張の定数に対するテスト

対象: ActiveSupportDate 拡張 (core_ext/date) 内で定義されている、フォーマットや曜日名などを表す定数(例: DATE_FORMATS 相当のもの)

テストの意図:

  • Rails アプリが Date 系のフォーマット用定数を、初期化コードなどで上書き・追加することを正式なユースケースとして想定している。
  • そのため、freeze してはいけない定数であることをテストで保証する。

サンプルイメージ(概念的な例):

ruby
def test_date_formats_are_mutable
  formats = Date::DATE_FORMATS

  assert_nothing_raised do
    formats[:short] = "%Y-%m"
  end

  assert_equal "%Y-%m", formats[:short]
end

実際には #57333 で修正された対象の定数を操作するテストになっているはずです。


  1. 影響範囲・注意点
  • 実装コードへの変更はなく、テストが増えただけ なので、このPR単体では挙動の変更や互換性への影響はありません。
  • ただし、テストが追加されたことで次のような「仕様」が明文化されたと言えます:
    • 対象となっている定数は、今後もアプリケーション側からミューテートできることが Rails の前提 である。
    • これらの定数を将来 freeze したり、イミュータブルなオブジェクトに置き換えるような変更は、テストが落ちることで検知される。
  • アプリケーション開発者の視点では:
    • すでに #57332 / #57333 で「誤って freeze されていた問題」は解消済みであり、このPRはその動作をテストでロックしたものです。
    • 上記のような定数(MySQL アダプタの型マッピングや Date 拡張のフォーマット設定など)を「カスタマイズポイント」として利用している場合、その利用パターンが今後も正式にサポートされやすくなった と捉えてよいです。

  1. 参考情報 (あれば)

このPRは、上記2つのPRで復旧・修正された「mutable な定数」をテストで守るためのフォローアップPRという位置付けです。


#57336 Fix broken link in changelog

マージ日: 2026/5/11 | 作成者: @RDIL

  1. 概要 (1-2文で)
    Rails 4.0 のリリースノート内に記載されていたリンク切れを修正した PR です。コードの挙動には一切影響せず、ドキュメント(ガイド)の品質向上のみを目的としています。

  2. 変更内容の詳細

  • 対象ファイル: guides/source/4_0_release_notes.md
  • 変更内容: リリースノート中に含まれていた URL を、現在も有効にアクセスできるリンクへ差し替えています(1行の更新: 1行削除 + 1行追加)。
  • 背景:
    • 2014年頃に書かれた Rails 4.0 リリースノート中のあるリンクが、その後のサイト構成変更などにより 404 になるなど「リンク切れ」状態になっていた。
    • PR 作成者は古いアプリのアップグレード作業中に該当リンク切れに気づき、同様に古いバージョンからのアップグレード情報を参照する開発者が困らないよう、リンクを修正した。

※変更対象は「文章中の URL のみ」であり、サンプルコードや設定例などの技術内容には変更はありません。

  1. 影響範囲・注意点
  • 影響範囲:
    • Rails ガイド(特に Rails 4.0 Release Notes)を参照する開発者が対象です。
    • アプリケーションコード、API、挙動、互換性には一切影響しません。
  • 注意点:
    • Rails 4.0 という既に古いバージョンのリリースノートの変更のため、現行バージョンでの挙動確認に直接関係するものではありませんが、レガシーアプリからのアップグレード調査時には、正しいリンク先に飛べるようになります。
    • ドキュメント生成(guides のビルド)でのみ反映される変更です。
  1. 参考情報 (あれば)

#57333 Disable freezing of NATIVE_DATABASE_TYPES for MySQL adapter

マージ日: 2026/5/10 | 作成者: @chaadow

  1. 概要 (1-2文で)
    MySQL アダプタの NATIVE_DATABASE_TYPES を凍結(freeze)しないように変更し、これを前提に上書き・拡張しているコード(例: neighbor gem)が FrozenError で落ちないようにした PR です。PostgreSQL アダプタと同様の挙動に揃えるフォローアップ変更でもあります。

  1. 変更内容の詳細

何をしたか

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb 内で、MySQL アダプタの NATIVE_DATABASE_TYPES を凍結していたコードをやめました。

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

ruby
# 変更前(イメージ)
NATIVE_DATABASE_TYPES = {
  primary_key: "bigint auto_increment PRIMARY KEY",
  string:      { name: "varchar", limit: 255 },
  # ...
}.freeze

# 変更後(イメージ)
NATIVE_DATABASE_TYPES = {
  primary_key: "bigint auto_increment PRIMARY KEY",
  string:      { name: "varchar", limit: 255 },
  # ...
}
# freeze しない

関連 PR (#57323) で行った変更の MySQL 版であり、PostgreSQL アダプタ側の定義と挙動を揃えるための修正です。

なぜ必要か

neighbor gem では、MySQL 用に NATIVE_DATABASE_TYPES を拡張する実装を行っています:

ruby
# https://github.com/ankane/neighbor/blob/2978d482d48340ab16477f76406c7f1522427d68/lib/neighbor/mysql.rb#L9

ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::NATIVE_DATABASE_TYPES[:vector] = { name: "vector" }
# などのような拡張をしているイメージ

Rails 側で NATIVE_DATABASE_TYPES.freeze してしまうと、このようなコードが実行されたときに FrozenError が発生します:

can't modify frozen Hash: { primary_key: "bigint auto_increment PRIMARY KEY", ... } (FrozenError)

この PR により、MySQL アダプタでも PostgreSQL と同様に NATIVE_DATABASE_TYPES を後から拡張可能な状態に戻し、既存の拡張コード(neighbor ほか)がエラーなく動作するようになります。


  1. 影響範囲・注意点
  • 影響を受けるのは主に MySQL / MariaDB を使うアプリケーション
    ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter(およびそれを継承する Mysql2Adapter など)で NATIVE_DATABASE_TYPES を直接参照・変更しているコードに影響します。

  • メリット

    • neighbor などの gem が NATIVE_DATABASE_TYPES に対して []=, merge! などで型定義を追加・変更できるようになります。
    • PostgreSQL アダプタと同じパターンに揃うため、アダプタ間でカスタマイズ方法が一貫します。
  • デメリット / 注意点

    • NATIVE_DATABASE_TYPES はミュータブルな Hash のままになるため、アプリケーション or gem がこれを破壊的に変更すると、プロセス内の他の接続やコードにも影響が及びます。
      • 例: NATIVE_DATABASE_TYPES.clear や意味のある既存キーの上書きは危険。
    • スレッドセーフではない変更(マルチスレッド環境での同時書き換え)にも注意が必要です。
    • フレームワーク側の型定義を直接いじるのではなく、できるだけ公式 API(拡張機構)があればそちらを使うのが望ましいです。
  • 現行アプリへの実務的な影響

    • 変更前に FrozenError が出ていたようなコード(特に gem 側)は、そのままで正常に動くようになります。
    • これまで freeze されていたことを「前提」にしていたコード(たとえば NATIVE_DATABASE_TYPES.dup して安全を確保していたようなコード)は、そのままでも動きますが、今後は「元のもの自体が書き換えられる可能性」があることを考慮すべきです。

  1. 参考情報 (あれば)

#57332 fix: unfreeze Date::DATE_FORMATS

マージ日: 2026/5/10 | 作成者: @luizkowalski

  1. 概要 (1–2文で)
    Date::DATE_FORMATS が凍結(freeze)されていたことで、アプリ側から日付フォーマットを追加できなくなっていた不具合を解消し、従来どおりフォーマットを後から登録できるようにした PR です。Time 周りの実装(Time::DATE_FORMATS)と整合を取る形で、意図しない freeze を取り消しています。

  1. 変更内容の詳細

対象ファイル:

  • activesupport/lib/active_support/core_ext/date/conversions.rb

このファイル内で定義されている Date::DATE_FORMATS の定義から freeze が外されました。
(実際の差分はおおよそ以下のようなイメージです)

ruby
# 変更前(イメージ)
Date::DATE_FORMATS = {
  # いくつかのデフォルトフォーマット定義
}.freeze

# 変更後
Date::DATE_FORMATS = {
  # いくつかのデフォルトフォーマット定義
}

これにより、アプリケーションコードから以下のように自由にフォーマットを追加・変更できる状態に戻ります。

ruby
# 例: アプリケーション初期化時など
Date::DATE_FORMATS[:short_ymd] = '%Y/%m/%d'
Date::DATE_FORMATS[:ja_long]   = '%Y年%m月%d日'

Date.today.to_s(:short_ymd) # => "2026/05/11"
Date.today.to_s(:ja_long)   # => "2026年05月11日"

PR 説明にもある通り、Time の拡張 (activesupport/lib/active_support/core_ext/time/conversions.rb) ではすでに Time::DATE_FORMATS を freeze していないため、それと同じ挙動に揃えた形です。


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

    • Date::DATE_FORMATS にアプリケーション側からフォーマットを追加・上書きしているコードが、再び正常に動作します。
    • 直前のバージョン(DATE_FORMATS が誤って freeze されていた期間)で発生していた FrozenError(もしくは類するエラー)が解消される可能性があります。
  • 注意点

    • 定数が「ミュータブルなハッシュ」である状態に戻るため、ライブラリやアプリケーションが意図せず Date::DATE_FORMATS を書き換えると、グローバルな影響が出ます。
      • 特に gem やエンジンがフォーマットを変更する場合は、名前衝突しないキー名を使う、もしくは局所的に I18n.l など別の手段でフォーマットすることを検討してください。
    • 既に「freeze されている前提」でワークアラウンド(例: dup して使うなど)を書いていた場合、そのコードは依然として動きますが、不要になっている可能性があります。

  1. 参考情報 (あれば)
  • Rails ガイドや API ドキュメントで紹介されているフォーマット追加パターンは、今回の PR により再びそのまま利用可能です。
    例:
    ruby
    # config/initializers/date_formats.rb
    Date::DATE_FORMATS[:default] = '%Y-%m-%d'
    Date::DATE_FORMATS[:db]      = '%Y-%m-%d'
  • 関連する実装:
    • activesupport/lib/active_support/core_ext/time/conversions.rbTime::DATE_FORMATS 定義(freeze していないのと同じ方針に揃えた変更)。

#57330 Handle missing Mandrill ingress signatures

マージ日: 2026/5/10 | 作成者: @afurm

  1. 概要 (1-2文で)
    Action Mailbox の Mandrill ingress で、署名ヘッダ X-Mandrill-Signature が欠落している場合に NoMethodError が発生していた問題を修正し、欠落時も正しく 401 Unauthorized を返すようにした PR です。これにより、署名改ざんと同様に署名欠落も認証失敗として安全に扱われます。

  1. 変更内容の詳細

問題点

従来の実装では、Mandrill ingress の認証ロジックが以下のような形で動いていたと考えられます:

ruby
# 擬似コードイメージ
def authenticate
  signature = request.headers["X-Mandrill-Signature"]
  expected  = generate_expected_hmac

  # ここで signature が nil の場合でも secure_compare を呼んでしまう
  ActiveSupport::SecurityUtils.secure_compare(signature, expected)
end

ActiveSupport::SecurityUtils.secure_compare は両方の引数に #bytesize を要求するため、signaturenil の場合に以下の例外が発生していました:

text
NoMethodError: undefined method `bytesize' for nil
    activesupport/lib/active_support/security_utils.rb:34:in `secure_compare'

本来であれば「署名がない=認証失敗」として 401 を返すべきところが、例外で処理が中断していました。

修正内容

PR では Mandrill 用の認証処理を次のように変更しています:

  • X-Mandrill-Signature存在していることを前提条件 とし、空・nil の場合は secure_compare を呼び出さず即座に認証失敗とする。
  • 署名が欠落しているリクエストは、偽造(HMAC 不一致)の場合と同じ扱いで 401 Unauthorized を返す。

コード上は 1 行の変更ですが、実質的には下記のようなロジックになります(実際のコードを簡略化したイメージです):

ruby
def authenticated?
  signature = request.headers["X-Mandrill-Signature"]
  return false if signature.blank? # 新たに「署名が必須」であることを明示

  expected = generated_mandrill_hmac

  ActiveSupport::SecurityUtils.secure_compare(signature, expected)
end

テストの追加

actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb に以下のようなテストが追加されています:

  • X-Mandrill-Signature ヘッダがない Mandrill リクエスト」を送る
  • レスポンスが 401 Unauthorized であること
  • ActionMailbox::InboundEmail が作成されていないこと

これにより、以前は NoMethodError で落ちていたケースが、意図どおり「認証失敗」として扱われることが検証されています。


  1. 影響範囲・注意点
  • 影響対象: Mandrill を経由して Action Mailbox にメールを取り込んでいるアプリケーション。
  • これまで:
    • X-Mandrill-Signature が欠落していると、コントローラ内部で NoMethodError が発生し、アプリによっては 500 系エラーとして扱われる可能性があった。
  • これから:
    • X-Mandrill-Signature 欠落時は、偽造署名時と同じく 401 Unauthorized を返す。
    • 署名ヘッダがなければ絶対にメールは取り込まれない(これはセキュリティ上望ましい挙動)。
  • ログやモニタリングの観点:
    • 以前は 500 系エラー(例外)で検知されていたものが、401 応答(認証失敗)として観測されるようになる可能性があるため、モニタリングしているステータスコードの分布が変わることがあります。
  • API 互換性:
    • 正常な Mandrill 連携(正しい署名ヘッダを送っている場合)には影響がありません。
    • Mandrill 側設定ミスなどで署名が送られていない環境では、今後 inbound email が一切作成されなくなるので、Mandrill の webhook 設定とシークレットの設定を確認すべきです。

  1. 参考情報 (あれば)
  • 対応 Issue: #57329
  • 修正対象コンポーネント: Action Mailbox / Mandrill ingress
  • 追加で実行された検証コマンド:
    • 個別テスト:
      cd actionmailbox && bin/test test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb -i "/without a signature/"
    • ファイル全体テスト:
      cd actionmailbox && bin/test test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
    • RuboCop:
      bundle exec rubocop actionmailbox/app/controllers/action_mailbox/ingresses/mandrill/inbound_emails_controller.rb actionmailbox/test/controllers/ingresses/mandrill/inbound_emails_controller_test.rb
    • git diff --check による差分・スタイルチェック

この PR は、本番で意図しない 500 エラーを 401 Unauthorized に正規化し、Mandrill ingress の認証フローをより安全かつ一貫したものにするバグフィックスです。


#57331 Remove isolated test tasks

マージ日: 2026/5/10 | 作成者: @byroot

  1. 概要 (1-2文で)
    Rails 本体に定義されていた「テストを isolated(プロセス分離)して実行する系の Rake タスク」が一括で削除されました。
    ビルド時間のわりに得られるメリットが小さく、CI 構成も見直されたため、メンテナンス対象から外した形です。

  1. 変更内容の詳細

2-1. 削除されたものの概要

各コンポーネントごと(actionpack, activerecord, activesupport など)の Rakefile から、以下のような「isolated テスト」タスク群が削除されています。

  • test:*:isolated あるいはそれに類するタスク
  • Active Record まわりの複雑な test/DB 組み合わせタスク(特に activerecord/Rakefile から大幅削除)
  • activejob でも複数アダプタ・バックエンドを isolated にテストするようなタスクが整理・削除

PR 本文で触れられている通り:

These are much slower to run and don't catch much. Not worth the complexity.

という判断で、「時間だけかかって、得られる追加の検知能力が薄いテスト実行モード」をやめています。

2-2. Rakefile 周りの変更

Rakefile(ルート)と各コンポーネントの Rakefile の共通的な変更はおおむね以下のようなものです。

  • test:*:isolated 系のタスク定義・依存関係を削除
  • AGENTS.mdguides/source/contributing_to_ruby_on_rails.md から、それら isolated テストに言及していた記述を削除
  • 代わりに 通常のテストタスク (e.g. bundle exec rake test, test:units, test:integration など) に一本化する方向に整理

activerecord/Rakefile は -101 行と差分が大きく、ここでは特に以下のような処理が削られたと考えられます(実際のコードは PR 本体参照):

  • 複数データベースアダプタ(SQLite / PostgreSQL / MySQL など)や構成を、個別プロセスで走らせるためのタスク群
  • CI 専用の複雑なテストマトリクスを作る Rake タスク

Rails の CI 構成は rails/buildkite-config リポジトリで管理されており、今回の PR はその follow-up として、本体側からも不要なタスクを削ぎ落としたものです。

2-3. ドキュメントの修正

guides/source/contributing_to_ruby_on_rails.mdAGENTS.md から:

  • 「Rails にコントリビュートする際は、この isolated テストも実行してね」のような記述が削られ、
  • 単純なテストコマンド(通常の rake test など)に関する説明に整理されています。

これにより、コントリビューターが読む公式なガイドラインからも isolated テストの存在が消え、今後は「通常のテストを回せばよい」という分かりやすい形になります。


  1. 影響範囲・注意点
  • Rails 開発者(コントリビューター)への影響

    • これまでドキュメントに従って rake test:XXX:isolated のようなタスクを手動で叩いていた場合、それらは今後存在しない可能性が高いです。
    • 代わりに、通常の bundle exec rake test や、コンポーネントごとの基本的なテストタスクのみを実行すればよい形になります。
  • CI / ビルド環境への影響

    • Rails 本家での CI 構成(Buildkite)が rails/buildkite-config 側で既に見直されており、その follow-up として本体のタスクが消されています。
    • 自前で Rails 本体をフォークしており、独自にこれらの isolated タスクを CI から呼び出していた場合は、ブランチ更新後に CI が失敗する可能性があります。
      • 対応としては、CI 設定から該当の Rake タスク呼び出しを削除し、通常のテストタスクに切り替える必要があります。
  • アプリケーション開発者への影響

    • Rails「本体リポジトリのビルドタスク」に対する変更であり、通常の Rails アプリケーションで一般的に触るタスク (db:migrate, test, assets:precompile など) には基本的に影響しません。
    • 独自に Rails 本体の Rakefile を参照してタスクを上書きしたりしていない限り、ほぼ無視してよい変更です。
  • 挙動の変化に関する注意

    • isolated テストは「別プロセス実行」や「環境一式を毎回クリアして実行する」などの特性を持つことが多く、環境汚染・グローバル状態のリーク等を検出しやすい一面がありました。
    • PR 説明にある通り、それでも得られるバグ検知効果が限定的で、パフォーマンスやメンテナンスコストの方が重いという判断がなされています。
    • 仮に isolated 実行でしか再現しないようなバグを狙ったテストを書いていた場合は、自分たちのリポジトリ側で rake タスクを定義し直すなど、独自対応が必要です。

  1. 参考情報 (あれば)

#57323 Enable Style/MutableConstant cop with literals style

マージ日: 2026/5/10 | 作成者: @paracycle

  1. 概要 (1-2文で)
    Style/MutableConstant Cop を RuboCop 設定で有効化し、「リテラルで定義される定数は基本的に凍結して不変にする」という方針を Rails 全体に適用した PRです。これにより、Ractor でクラス/モジュールを安全に共有しやすくし、Ractor-safe なフレームワーク化を進めています。

  1. 変更内容の詳細

RuboCop 設定の変更

.rubocop.yml にて Style/MutableConstant Cop が literals スタイルで有効化されています。

  • literals スタイル:
    「リテラルで定義された定数 (配列、ハッシュ、文字列など) は変更されない前提なので、凍結されるべき」というポリシーを強制するモードです。
    例:

    ruby
    # NG (MutableConstant)
    NAMES = ["Alice", "Bob"]
    
    # OK
    NAMES = ["Alice", "Bob"].freeze

Rails コードベース全体でこのルールが有効になり、リテラル定数のほぼ全てに .freeze が付与されるか、あるいは mutable に使いたい箇所では Cop が明示的に無効化されています。

各ファイルでの主なパターン

変更された 100 ファイルの多くが、以下いずれかのパターンに該当します。

1. リテラル定数の凍結

配列・ハッシュ・文字列などで定義される定数に .freeze を付ける、または既に freeze されている書き方に統一する変更です。

(簡略例・実際と似たイメージ)

ruby
# 変更前
DEFAULT_FORMATS = { html: Mime[:html], json: Mime[:json] }

# 変更後
DEFAULT_FORMATS = { html: Mime[:html], json: Mime[:json] }.freeze
ruby
# 変更前
ACCEPTABLE_TYPES = %w[foo bar baz]

# 変更後
ACCEPTABLE_TYPES = %w[foo bar baz].freeze

これにより、フレームワーク側が「一度定義した定数の内容を書き換えない」ことを静的に保証しやすくなります。

2. 「意図的にミュータブル」な定数の除外

一部の定数は、キャッシュ用の Hash や、利用者が拡張する前提のマップ・配列などとして「中身を書き換える」設計になっています。
そうした箇所は、rubocop:disable Style/MutableConstant コメントで明示的に Cop を無効化したうえで、これまで通り mutable なままにしています。

イメージ:

ruby
# rubocop:disable Style/MutableConstant
MIME_TYPES = {} # ランタイムで追加される設計
# rubocop:enable Style/MutableConstant

これにより、「ここは mutable である」という意図がコード上にも明示され、将来的に改善(例えば Ractor 用の別経路を設ける、Thread/Ractor 局所キャッシュに分離する等)を行うべき箇所が可視化されています。

3. Ractor safety を意識した定数設計

PR の説明にもある通り、Ractor では「共有可能 (shareable)」であるオブジェクトだけが Ractor 間で共有できます。
Ruby 3 系の Ractor.make_shareable などの文脈では、freeze されていて再帰的に不変なオブジェクト が shareable の候補になります。

  • クラス/モジュールを Ractor 間で共有する場合、その中の定数も shareable である必要がある
  • よって「単なる設定値・メタデータ」としての定数は極力 freeze して不変にしておく必要がある

この PR は、その前提を RuboCop による静的チェックとして Rails コア全体に導入し、「Ractor-safe な設計かどうか」を壊れにくくしています。


  1. 影響範囲・注意点

ランタイム挙動への影響

  • ほとんどの定数は元々「書き換えない前提」で使われていたはずなので、多くのケースでは挙動は変わりません。

  • しかし、アプリやプラグイン側が Rails の定数を直接書き換えていた場合は破壊的変更になり得ます。

    例:

    ruby
    # 以前は(たまたま)動いていたかもしれないコード
    ActionController::Base::DEFAULT_PROTECTED_PARAMS << :foo

    この PR でその配列が freeze されると、上記は FrozenError を起こします。

Rails の定数を拡張したい場合

  • この PR のコメントにもある通り、「利用者が拡張する前提」の定数は mutable のままにしてありますが、その数はかなり限定的です。

  • 既存アプリで Rails の定数を上書き・破壊的変更して依存している場合は、以下のような形へのリファクタリングを検討すべきです。

    • 自分側の設定オブジェクトを定義して、Rails の定数は「読み取り専用」として参照だけ行う
    • モンキーパッチが必要な場合でも、定数そのものを mutate せず、クラスメソッド・インスタンスメソッドの override で対応する

Ractor を使う場合の安心感

  • Rails のコアにある「単なる設定・メタデータ的な定数」は、原則として Ractor 間で共有しても安全な状態に近づきました。
  • まだ mutable な定数も残っていますが、それらは rubocop:disable Style/MutableConstant によって明示されており、将来的な Ractor 対応の検討対象が明確になっています。

  1. 参考情報 (あれば)

この PR は、Rails 本体のスタイルと静的保証のレベルを一段上げつつ、Ractor 対応への足場を整えるためのインフラ的な変更と捉えると理解しやすいです。


#57328 Fix some Active Record flakes

マージ日: 2026/5/9 | 作成者: @byroot

  1. 概要 (1-2文で)
    Active Recordのテストがときどき落ちる「フレークテスト(flaky tests)」を減らすため、ActiveRecord::RuntimeRegistry と一部関連テストを微調整したPRです。主にスレッドローカルな状態管理やテスト用モデル定義の見直しによる安定性向上が目的で、アプリケーション側の挙動変更はほぼありません。

  1. 変更内容の詳細

※ 実際の差分を要約した内容であり、行数ベース・ファイル名ベースでの説明です。

2-1. activerecord/lib/active_record/runtime_registry.rb (+2/-1)

ActiveRecord::RuntimeRegistry は、接続情報やスコープなど、スレッドローカルに紐づくActive Record内部の状態を管理するクラスです。
このPRでは、テストがフレークになる原因となっていた状態の残り方・初期化タイミングを修正する1行+αの変更が入っています。

典型的には以下のようなパターンの修正が考えられます:

  • スレッドローカル変数のデフォルト値を明示する
  • nil チェックを追加し、安全にリセットする
  • テスト時にのみ状態をクリーンアップするためのガードを入れる など

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

ruby
module ActiveRecord
  class RuntimeRegistry
    # before
    # def connection_handler
    #   ActiveSupport::IsolatedExecutionState[:active_record_connection_handler]
    # end

    # after (例)
    def connection_handler
      ActiveSupport::IsolatedExecutionState[:active_record_connection_handler] ||= default_connection_handler
    end
  end
end

この種の修正により、テストケース間で状態が持ち越されてしまうことを防ぎ、ランダム順序でテストを走らせたときにも安定するようになります。

2-2. activerecord/test/cases/associations/join_model_test.rb (+1/-1)

join_model_test.rb は、多対多関連(has_many :through など)で使用される中間モデルの挙動をテストするファイルです。
ここでは1行のみの修正で、以下のような調整が入っている可能性があります。

  • 期待値の微修正(たとえば順序依存をなくす)
  • 一時的に作るレコードや関連の作り方を修正
  • モデル名の変更や読み込みタイミングの調整

例としては、以下のような「フレークになりやすいテスト」の修正が典型です(イメージ例):

ruby
# before
assert_equal [car1, car2], owner.cars

# after (順序に依存しないように)
assert_equal [car1, car2].sort_by(&:id), owner.cars.sort_by(&:id)

このように、DBの並び順や実行タイミングに依存してしまう部分を排除し、どの環境でも安定して通るようにしています。

2-3. activerecord/test/models/car.rb (+2/-0)

テスト用モデル Car に2行の追加が行われています。
join_model_test.rb の安定化のために、以下のような属性や関連の定義が明示的に追加された可能性があります。

イメージ例:

ruby
class Car < ActiveRecord::Base
  # PRで追加された可能性が高いもの
  has_many :ownerships
  has_many :owners, through: :ownerships
end

あるいは、バリデーションやデフォルトスコープを追加/削除して、テストで期待される状態と実際のモデル定義を一致させるような調整が行われています。
これにより、テストが暗黙の前提(「この関連があるはず」「この順で返るはず」など)に依存しないようにしています。


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

    • 主な対象はRails本体のActive Recordテストスイートです。
    • RuntimeRegistry の挙動は一部アプリケーション側から直接参照される可能性があるものの、今回の変更は内部状態の初期化・クリーンアップに関するごく小さな修正で、通常のアプリケーションコードには影響しない設計になっています。
    • Car モデルや join_model_test.rb はテスト専用のため、Railsを利用する側のアプリケーションには影響しません。
  • 注意点

    • 自前で ActiveRecord::RuntimeRegistry の内部実装に依存したメタプログラミングをしている場合には、挙動の差異が出る可能性がありますが、そのようなケースはかなりレアです。
    • フレーク修正PRのため、主眼は「テストの安定化」であり、「機能仕様の変更」ではありません。アップグレード時にこのPR単体でアプリケーションの仕様が変わることは基本的にないと考えて差し支えありません。

  1. 参考情報 (あれば)
  • このPRは以下のPRから抽出されています:
  • ActiveRecord::RuntimeRegistry はスレッドローカルな状態管理に関するクラスであり、マルチスレッド環境(Puma など)や ActiveSupport::IsolatedExecutionState に関心がある場合の参考実装としても読む価値があります。

#57324 Use wait_for helper in reaper and async query tests

マージ日: 2026/5/9 | 作成者: @bogdan

  1. 概要 (1-2文で)
    Active Record のテストコード内に残っていた独自実装のポーリングループを、テスト用ヘルパー wait_for に置き換えた PR です。これにより、テストがハングしたり、タイムアウト時に誤解を生むアサーションへ進んでしまう問題を防ぎ、失敗箇所が明確になります。

  1. 変更内容の詳細

背景

  • #57309 に続くフォローアップで、Active Record のテストにまだ 3 つのポーリングループが残っていた。
  • そのうち:
    • 1つは上限のないループで、条件が満たされないと永遠にハングする可能性がある。
    • 残り 2つは、一定時間ポーリングした後に条件未達でもそのまま後続のアサーションに進んでしまい、「本当の原因は条件未達なのに、別のアサーション失敗として見えてしまう」という誤解を招く挙動だった。

具体的な修正内容

対象ファイル:

  • activerecord/test/cases/asynchronous_queries_test.rb
  • activerecord/test/cases/reaper_test.rb

上記ファイル内の「自前のポーリングループ」を、テストヘルパー wait_for の呼び出しに統一しました。

wait_for は典型的に以下のような形で使われます (イメージ):

ruby
wait_for do
  # ここで期待する条件が true になるまで待つ
  some_async_state.ready?
end

あるいはタイムアウトを指定して:

ruby
wait_for(timeout: 5.seconds) do
  connection_pool.active_connection_count.zero?
end

この PR では、元々次のようなパターンになっていたコードを:

ruby
# 擬似例: 独自ポーリング
timeout_at = Time.now + 5
until something_happened? || Time.now > timeout_at
  sleep(0.1)
end

# ここで「起きているはず」という前提でアサートするが
# 実際には起きていない場合もある
assert something_happened?

wait_for による明示的な待機に置き換えています:

ruby
wait_for do
  assert something_happened?
end

あるいは:

ruby
wait_for do
  something_happened?
end

wait_for は、指定時間内にブロックが成功しない場合に Timeout::Error を発生させるため、

  • 「どの条件待ちが失敗したか」がスタックトレース上で明確になる
  • 無限ループによるハングを避けられる

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

テスト対象の文脈としては:

  • asynchronous_queries_test.rb: 非同期クエリの完了・結果取得を待つためのポーリングを wait_for に置き換え
  • reaper_test.rb: コネクションプールの reaper(アイドル接続のクリーンアップを行うスレッド)の動作確認で、reaper が実際に動作するのを待つ部分を wait_for に置き換え

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

    • 本 PR は「テストコードのみ」の変更であり、本体のライブラリ挙動やパブリック API には影響しません。
    • Active Record の非同期クエリとコネクションプール reaper に関するテストの信頼性・デバッグ容易性が向上します。
  • 注意点

    • 今後、同様に非同期処理やバックグラウンドスレッドの状態を待つテストを書く場合は、独自のループ構築ではなく wait_for の利用が推奨されます。
    • wait_for のデフォルトタイムアウト値や挙動は test helper に依存するため、タイムアウトがシビアなケースでは timeout: オプション指定を検討するとよいです。
    • 従来は「原因が曖昧なハング」や「別のアサーションでの失敗」として現れていた問題が、Timeout::Error としてはっきり表面化するようになるため、既存 CI 上でまれに「新しく落ちるように見える」テストが出る可能性がありますが、それは潜在的なテスト不備やタイミング依存バグの顕在化と考えられます。

  1. 参考情報 (あれば)
  • この PR: https://github.com/rails/rails/pull/57324
  • 前提となる PR (#57309): wait_for の導入や既存テストの置き換えが実施された PR (詳細は該当 PR 参照)
  • wait_for は Rails テストスイートで非同期挙動を扱う際の標準的ヘルパーであり、Active Job・Action Cable・非同期クエリ・バックグラウンドスレッドなど、状態が安定するまでポーリングする必要がある箇所での利用が推奨されます。

#57327 Active Record connection pool tests avoid always skipped tests

マージ日: 2026/5/9 | 作成者: @byroot

  1. 概要 (1-2文で)
    Active Record の connection pool に関するテストのうち、実行環境的に「常に skip されてしまうテスト」がそもそも定義されないように整理された PR です。これにより、テストスイートのノイズが減り、実際に実行されるテストだけが定義される構成になりました。

  1. 変更内容の詳細

※この PR は activerecord/test/cases/connection_pool_test.rb を大量にリファクタリングしており、コード上の挙動を変えるというより「テストの構造・定義条件」を大きく整理しています。

主なポイント:

  • 「常に skip されるテスト」の削除/条件付き定義

    • 以前は、例えば特定のアダプタや機能が有効になっていないときに skip されるテストが、多数クラス定義されていました。
    • しかし「どの CI/開発環境でも前提が満たされず、永遠に実行されないテスト」が存在しており、それらがテストスイートに残り続けていました。
    • この PR では、そうしたテストを
      • そもそも if 条件 を満たさない場合は test 自体を定義しない
      • もしくはテストクラス自体を定義しない という形に変更しています。
  • テスト定義条件の見直し

    • 例としてイメージしやすいパターン:
      • 変更前(イメージ):
        ruby
        class ConnectionPoolFooTest < ActiveRecord::TestCase
          def setup
            skip "Adapter X is required" unless ActiveRecord::Base.connection.adapter_name == "X"
          end
        
          def test_foo_behavior
            # ...
          end
        end
      • 変更後(イメージ):
        ruby
        if ActiveRecord::Base.connection.adapter_name == "X"
          class ConnectionPoolFooTest < ActiveRecord::TestCase
            def test_foo_behavior
              # ...
            end
          end
        end
    • このように、実行対象になり得るケースでのみテストを定義するようにしています。
    • よくある SKIP_MESSAGE = "..." を毎回使うのではなく、条件に応じてクラス単位・ブロック単位でテストを囲む構造になっているはずです。
  • テストの整理・グルーピング

    • connection pool のテストが、アダプタ種別や機能有無(例: reaping, async, multiple databases など)ごとに整理され、条件つきで定義される形にまとまっている可能性が高いです。
    • その結果、行数としては+437/-456 で、ほぼ全体が書き換えに近いリファクタリングになっていますが、挙動変化ではなく「どのケースでどのテストが存在するか」の整理が中心です。
  • スキップの可視性から「実行されるテストの明確化」へ

    • 以前は rake test や CI 上で「たくさん skip が出るが、実際には二度と動かないテスト」というノイズが混ざっていた可能性があります。
    • 今回はそれを根本的に解消し、「意味のある skip」だけが残り、「どうやっても実行されないテスト」は定義しない、という方針に寄せています。

  1. 影響範囲・注意点
  • Rails 本体の挙動への直接的な影響は原則なし

    • 変更は activerecord/test/cases/connection_pool_test.rb のみで、本番コード(lib/active_record/...)には触れていません。
    • そのため、アプリケーションの connection pool の挙動自体が変わることはありません。
  • テストスイートの見え方が変わる

    • Rails を fork してテストを流している場合や、Rails 本体にコントリビュートしている場合、
      • これまで「skip」として出ていたテストが丸ごと表示されなくなる
      • テスト総数 / skip 数のカウントが変わる といった違いが出ます。
    • 「昔は connection pool 周りで X 個 skip があったのに今は無い」といった変化は、この PR によるものです。
  • 環境依存テストの追加・変更時の注意

    • 今後 connection pool 関連でテストを追加する場合、
      • 「条件を満たさないときは skip する」のではなく
      • 「条件を満たす場合にだけテスト/クラスを定義する」 というこの PR のスタイルに揃えるほうが整合的です。
    • 例えばカスタムアダプタ向けテストや、特定の設定オプションに依存するテストを書く場合は、テスト定義そのものを条件でラップする設計が推奨されます。
  • 古い/未サポート機能向けテストの棚卸し

    • 今回のように「どの環境でも実行されないテスト」がある場合、それは
      • もはや使われていない/削除済み機能
      • サポートを終了したアダプタ 向けのテストであることが多いです。
    • 自分のプロジェクトでも connection pool 周りのテストを長く維持している場合は、「常に skip されているテスト」はそもそも削除 or 条件付き定義にし直す、という観点で棚卸しするとよいです。

  1. 参考情報 (あれば)
  • この PR:

    • タイトル: Active Record connection pool tests avoid always skipped tests
    • 番号: #57327
    • 作成者: byroot
    • マージ日時: 2026-05-09T12:43:55Z
  • 関連する観点:

    • Rails コアでは、CI の安定性・実行時間短縮のため、「実行されないテストを減らす」「意味のない skip を無くす」流れが継続しており、本 PR もその一環と考えられます。
    • connection pool 周りの仕様・挙動自体を確認したい場合は、activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb 付近の実装と、周辺テスト (connection_pool_test.rb のうち実行されているテスト) をあわせて読むと理解が深まります。

#57325 Fix BigDecimal rendering failure in Arel tests

マージ日: 2026/5/9 | 作成者: @byroot

  1. 概要 (1-2文で)
    Arel の ToSQL ビジターが BigDecimal を文字列化する際、指数表記 (0.214e1) になってしまい、期待される通常の小数表記 (2.14) とズレてテストが落ちていた問題を修正する PR です。
    テストコードと依存バージョンの微調整により、BigDecimal の SQL 文字列表現が期待どおり安定して比較できるようにしています。

  1. 変更内容の詳細

問題の内容

テスト失敗メッセージ:

text
  1) Failure:
Arel::Visitors::ToSQLTest::the to_sql visitor#test_0032_should visit_BigDecimal [test/cases/arel/visitors/to_sql_test.rb:350]:
Expected: "2.14"
  Actual: "0.214e1"

Arel::Visitors::ToSQLBigDecimal("2.14") 相当の値を SQL に変換した結果が、Ruby / BigDecimal / 依存ライブラリのバージョン差などにより "2.14" ではなく "0.214e1" という指数表記になってしまい、テストが失敗していました。

実際の変更点

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

1) activerecord/test/cases/arel/visitors/to_sql_test.rb (+1/-1)

BigDecimal を扱うテスト (should visit_BigDecimal) の期待値、あるいは比較方法が調整されています。

PR 説明から読み取れる状況:

  • 元の期待値: "2.14"
  • 実際の出力: "0.214e1"

この差分を解消するために、

  • 期待値側を、実際の ToSQL 出力のフォーマットに合わせる
    もしくは
  • 出力を正規化した上で比較する(例: to_d して数値として比較、指数表記を通常表記に変換して比較)

といった形でテストが修正されています。

(PR情報的に、実装コードは触られておらずテスト側だけ変更されているので、「SQL 生成ロジック自体は問題なく数値としては正しいが、文字列表現が環境依存で揺れる」ことを前提に、テストを安定させる方向の修正と考えられます。)

サンプルイメージ:

ruby
# 例: 期待値を単純な文字列ではなく BigDecimal として比較する形に寄せるなど
assert_equal BigDecimal("2.14"), BigDecimal(sql_value_from_visitor)

実際の1行差分は、例えば以下のようなイメージです(あくまで擬似コード):

diff
- assert_equal "2.14", visit(BigDecimal("2.14"))
+ assert_equal "0.214e1", visit(BigDecimal("2.14"))
# または
+ assert_equal BigDecimal("2.14"), BigDecimal(visit(BigDecimal("2.14")))

2) Gemfile.lock (+2/-2)

BigDecimal の文字列表現に影響する可能性がある依存ライブラリや Ruby / gem バージョンの更新・固定が行われています。
目的は、開発・CI 環境で BigDecimal の出力フォーマットがブレないようにすることです。

具体的には、例えば以下のような変更が想定されます(実際の gem 名は PR 本体参照):

  • Ruby パッチバージョン / bigdecimal gem / アダプタ関連 gem のバージョンアップ・ダウン
  • バージョンの固定(~> X.Y= X.Y.Z など)によるフォーマットの安定化

  1. 影響範囲・注意点
  • 影響範囲は 基本的にテストコードと依存のバージョン管理に限定 されており、Rails / Arel を利用するアプリケーションの動作や公開 API には変更はありません。
  • ただし以下のようなケースでは注意が必要です:
    • アプリケーション側で、Arel による BigDecimal の SQL 出力を「文字列として」厳密比較している(例: "2.14" であることを前提に正規表現マッチや文字列置換を行っている)場合
      • 環境によって "0.214e1" のような表記になる可能性自体は以前から存在しており、本 PR はその事実をテスト側に反映させた形です。
      • 今後も「数値的には同じだが文字列表現が違う」出力があり得る前提でコードを書く方が安全です。
  • Rails コアの方針としても、BigDecimal の SQL 文字列表現は環境差が出得る ため、アプリ側では数値として扱うか、フォーマットを自前で正規化するのが望ましいです。

  1. 参考情報 (あれば)
  • BigDecimal の文字列表現の違いの例(素の Ruby でも起こりうる):

    ruby
    require "bigdecimal"
    
    BigDecimal("2.14").to_s          # => "0.214E1"
    BigDecimal("2.14").to_s("F")     # => "2.14"   # 通常の小数表記

    Rails / Arel の実装が to_s なのか to_s("F") なのか、または他のフォーマッタを使うかによって、出力が指数表記になるかが変わります。

  • 関連しそうなコード:

    • Arel::Visitors::ToSQLvisit_Arel_Nodes_BindParam / visit_BigDecimal 付近の実装
    • 以前から BigDecimal のフォーマット周りは Ruby / bigdecimal gem のバージョンによる差異でテストが不安定になりやすく、今回の PR もその一環での安定化対応と考えられます。

#57136 Reject trailing dash in PostgreSQL adapter

マージ日: 2026/5/8 | 作成者: @yoLotus

  1. 概要 (1-2文で)
    PostgreSQL アダプタが UUID の末尾に余分なハイフン (-) を含む文字列を誤って有効とみなしていたバグを修正し、正しく ActiveRecord::RecordNotFound(説明文では NotFound と書いていますが実際の例外クラスは通常これ)を発生させるようにした PR です。UUID のバリデーションに使う正規表現を修正し、Trailing dash を含む UUID を拒否するように変更されています。

  1. 変更内容の詳細

問題の挙動

PostgreSQL 自体はドキュメントどおりの UUID 形式のみを受け付けますが、Rails の PostgreSQL アダプタ側で UUID 判定に使っている正規表現に不備があり、以下のような「末尾にダッシュが 1 つある」値を UUID として扱ってしまっていました。

ruby
User.find('a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11-') # 末尾にハイフン

この場合、本来であれば「不正な UUID なのでレコードが見つからない」= ActiveRecord::RecordNotFound が発生してほしいところですが、アダプタ側の処理で「末尾の - を取り除いたうえで」UUID として扱ってしまい、結果として既存レコードがあれば普通に返ってきてしまう、という問題がありました。
つまり、Rails が「不正な UUID を暗黙に修正してしまう」ことで、アプリケーション側のバグや入力不正を検知しにくくなっていました。

修正内容

変更ファイル:

  • activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
  • activerecord/test/cases/adapters/postgresql/uuid_test.rb

主な修正は UUID のパースに使われる正規表現の 1 行だけです。

旧来の正規表現は「UUID の標準形式 8-4-4-4-12 のパターン」に加えて、末尾の 1 文字分を曖昧に許容してしまうような形になっており、その結果「最後に余計な - が 1 つ付いた文字列」もマッチしていました。

この PR では、PostgreSQL のドキュメントに基づいた UUID 形式(xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx[0-9a-fA-F] の 8-4-4-4-12 桁、ダッシュ位置固定)にきっちり一致する場合だけ有効とみなすように正規表現を修正しています。
これにより、以下のような値はすべて不正な UUIDとして扱われます。

ruby
# 不正として拒否される例
User.find('a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11-') # 末尾の余計なダッシュ
User.find('a0ee-bc99-9c0b-4ef8-bb6d-6bb9bd38-0a11')   # ダッシュ位置がずれている
User.find('a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a1')   # 桁数不足
User.find('g0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11')  # 16進数以外の文字を含む

テスト (uuid_test.rb) も更新されており、「trailing dash を含む UUID が拒否される」ことを確認するケースが追加されています。


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

    • PostgreSQL を使用しており、UUID 型の主キーやカラムを使っているアプリケーション。
    • 特に Model.find(...) / find_by / where などに「末尾に - がついた UUID 文字列」やその他フォーマット不正な UUID を渡していた場合、以前は通っていたものが「不正」として扱われます。
  • 具体的な挙動変化

    • 以前: User.find('uuid-with-trailing-dash-') で、内部的に末尾の - が削られ、存在すればレコードが返る(サイレント修正)。
    • 今後: その値は「正しい UUID 形式ではない」と判定され、PostgreSQL アダプタ側で UUID として扱われなくなり、最終的に ActiveRecord::RecordNotFound など、期待されるエラーが発生するようになります(呼び出しパスによる)。
  • 既存コードへの影響

    • クライアントや外部 API から渡ってくる UUID 文字列が厳密な形式になっていない場合、今後はエラーが表面化します。
      • 今まで「なぜか動いていた」ケースが、形式不正として落ちる可能性があります。
    • 一方で、セキュリティや不正入力検知の観点では正しい動作に近づくため、意図しない ID 改変を早期に検出しやすくなります。
  • 対応のポイント

    • 外部入力(URL パラメータ、JSON、ヘッダなど)で UUID を受け取る箇所では、クライアント側 / サーバ側双方で UUID フォーマットを厳密にバリデートしておくとよいです。
    • もし「末尾にスペースやダッシュが付いてしまう」といった前処理ミスがある場合は、この PR 適用後に 400 / 404 が増える可能性があるため、入力前処理を修正する必要があります。

  1. 参考情報 (あれば)
  • PostgreSQL UUID ドキュメント
    https://www.postgresql.org/docs/18/datatype-uuid.html

  • 関連して挙動変化を導入した可能性がある PR として言及されているもの:

    • #49934 (この PR で導入された正規表現の変更が今回のバグの原因の一つかもしれないとコメントされています)
  • 対象コード:

    • activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb
      • PostgreSQL アダプタで UUID 型を扱うための OID マッピングとキャストロジックが定義されているファイルで、ここにある正規表現が今回修正された箇所です。

#56575 Support Multiple Arguments in ActionController::Parameters#merge in Consistent with Ruby's Hash#merge

マージ日: 2026/5/8 | 作成者: @berniechiu

  1. 概要 (1-2文で)
    ActionController::Parameters#merge / #merge! が Ruby 3.2 以降の Hash#merge と同様に「複数引数」を受け取れるように拡張された PR です。Rails の Strong Parameters の API と Ruby 標準 Hash API の挙動を揃えることで、一貫性と使い勝手を向上させています。

  1. 変更内容の詳細

何が変わったか

これまで:

ruby
params = ActionController::Parameters.new(a: 1)

# OK
params.merge(b: 2)

# NG(ArgumentError になる)
params.merge({ b: 2 }, { c: 3 })

この PR 以降:

ruby
params = ActionController::Parameters.new(a: 1)

# 1つの引数
params.merge(b: 2)                     # => #&lt;ActionController::Parameters {"a"=>1, "b"=>2} ...>

# 複数の引数 (Hash / Parameters など)
params.merge({ b: 2 }, { c: 3 })       # => {"a"=>1, "b"=>2, "c"=>3}

# bang メソッドも同様に複数引数対応
params.merge!({ d: 4 }, { e: 5 })      # params 自身が {"a"=>1,"b"=>2,"c"=>3,"d"=>4,"e"=>5} に更新

Ruby 3.2 の Hash#merge が以下のような形をサポートしているのに合わせたものです:

ruby
h = { a: 1 }
h.merge({ b: 2 }, { c: 3 })  # => { a: 1, b: 2, c: 3 }

この PR では、ActionController::Parameters#merge / #merge! の内部実装を変更し、可変長引数 (*others) を受け取り、それらを順番にマージするようにしています。これにより、Ruby の Hash#merge とほぼ同じ感覚で Strong Parameters を扱えます。

テスト

actionpack/test/controller/parameters/parameters_permit_test.rb にテストが追加されており、

  • Parameters#merge が複数の ParametersHash を引数に取れること
  • Strong Parameters の性質(permit/unpermit の扱い)が保たれていること

などが確認されています。


  1. 影響範囲・注意点
  • 対象クラス
    ActionController::Parameters#merge / #merge! のみが対象です。他の merge 実装には影響しません。

  • 既存コードへの影響
    単一引数で使っている既存コードはそのまま動作します。インターフェースが拡張されただけで、1引数時の挙動互換性は維持されています。

  • 複数引数のマージ順序
    Hash#merge と同様に「左から右へ」順番にマージされ、後ろの引数のキーが前のものを上書きします。

    ruby
    params.merge({ a: 1 }, { a: 2 })  # => a は 2 になる
  • Strong Parameters と permit の扱い
    Parameters#merge は元々、permit 状態や strong parameters の性質に注意が必要なメソッドです。

    • マージ相手が単なる Hash の場合、その値は「未許可パラメータ」として扱われうるため、コントローラ内での permit の呼び方・位置には従来どおり注意が必要です。
    • この PR はあくまで「複数引数を受け取れるようにした」だけで、Strong Parameters のポリシー自体は変えていません。
  • Ruby バージョン依存性との一貫性
    Rails 側の API が Ruby 3.2+ の Hash#merge と一貫するようになったため、Ruby の Hash で書いていたコードパターンをそのまま Strong Parameters に適用しやすくなります。


  1. 参考情報 (あれば)

#54189 Allow configuring Active Storage base controller parent class

マージ日: 2026/5/8 | 作成者: @zzak

  1. 概要 (1-2文で)
    Active Storage のコントローラ (ActiveStorage::BaseController) が継承する親クラスをアプリ側から設定できるようになり、ActionController::API を親にして API-only 構成でも利用しやすくなりました。これに伴い、新しい設定フラグとテストが追加されています。

  1. 変更内容の詳細

2-1. 何ができるようになったか

  • これまで Active Storage のコントローラは Rails 側で固定の親クラスを継承していましたが、
  • この PR により「Active Storage のベースコントローラの親クラス」をアプリ側で構成可能になりました。
  • 特に、ActionController::API を継承させることで、API-only アプリケーションでも Active Storage を素直に使えるようにするのが主目的です。

具体的には、次のような構成が可能になります(例:config/application.rb など):

ruby
# 例: API-only アプリで ActiveStorage のコントローラを API ベースにする
config.active_storage.draw_routes = true
config.active_storage.resolve_model_to_route = :rails_storage_proxy

# 新しく追加されたであろう設定(名称は概念的な例)
config.active_storage.routes_api_only = true
# または
config.active_storage.use_api_only_controller = true

※実際の「フラグ名」は PR 本文では flag as suggested としか書かれていないため、ここでは概念的に表現しています。最終的な設定名は Rails の最新ドキュメント / activestorage/lib/active_storage/engine.rb を確認して利用してください。

2-2. コードレベルの変更ポイント

主な変更ファイルと役割は次の通りです。

active_storage/base_controller.rb

  • これまで固定だった親クラス継承を、Active Storage の設定値に応じて決定する形に変更。
  • 内容イメージ(疑似コード):
ruby
module ActiveStorage
  class BaseController < ActiveStorage.config.base_controller # のような形
    # もしくは Engine 側で Module#prepend などを使って決めている可能性もあり
  end
end

実際には、ActiveStorage::Engine 内の設定やフラグに基づいて、

  • 通常: ActionController::Base を継承
  • API-only 設定時: ActionController::API を継承
    といった切り替えをしています。

active_storage/disk_controller.rb

  • DiskControllerBaseController を継承しているため、BaseController の親クラス変更の恩恵を受けます。
  • ここでは inherit するクラスの参照の仕方や、BaseController 側のインターフェース変更に追従する軽微な修正が入っています。

active_storage/lib/active_storage.rb

  • Active Storage の設定項目に「親コントローラクラス関連の設定/フラグ」が追加。
  • 例として、下記のような設定アクセサが増えているイメージです(あくまで概念的な例):
ruby
module ActiveStorage
  class << self
    attr_accessor :api_only # あるいは :routes_api_only など
  end
end

active_storage/lib/active_storage/engine.rb

  • Engine 初期化時に、上記設定フラグを読み取り、BaseController の親クラスとして何を使うか決めるロジックを追加。
  • Rails の設定 (config.active_storage.xxx) を ActiveStorage モジュール配下の設定値にブリッジする箇所もここに含まれます。

railties/test/application/active_storage/direct_uploads_integration_test.rb

  • 変更が正しく動作することを保証するため、API-only アプリを想定した統合テストが大幅に追加されています(+192 行)。
  • 想定されるテスト内容:
    • API-only アプリの config.api_only = true + 新フラグを有効にした状態で、Active Storage の direct upload エンドポイントに対するリクエストが通るか。
    • レスポンス形式や CSRF 周りの振る舞いが期待通りか。
    • ルーティング・マウントが正しく行われているか。

activestorage/CHANGELOG.md

  • 今回の機能追加が CHANGELOG に追記されています(新規設定・API-only サポートが明記されているはずです)。

  1. 影響範囲・注意点

3-1. 既存アプリへの影響

  • 既存アプリで特に設定を追加しなければ、従来通りの親クラス(ActionController::Base)が使われることが想定されます。
  • したがって、アップデート直後に Active Storage が壊れるリスクは低い設計です。

3-2. API-only アプリでの利用時の注意

  • API-only モード (config.api_only = true) では、ActionController::API を継承することで自動的に HTML 関連の機能(view レンダリング、cookie ベースのセッション、CSRF 対策など)が無効/簡略化されます。
  • そのため:
    • Active Storage のルーティングや direct upload を 完全に API として使う 場合にはメリットが大きいです。
    • 一方で、ブラウザフォーム + direct upload + authenticity_token など、従来の HTML フローを期待している場合は、ActionController::API に切り替えると一部機能が使えなくなる可能性があります。
  • どの親クラスにするかを決める前に、「アプリ内で Active Storage をどう使っているか(純 API / ブラウザ UI / admin 画面など)」を確認した方が安全です。

3-3. カスタムコントローラを持っている場合

  • もしアプリ側で ActiveStorage::BaseControllerDiskController を継承して独自拡張している場合:
    • 親クラスが変わることで利用できるヘルパーメソッドやフィルタが変わる可能性があります。
    • 特に before_action :authenticate_user! のような Devise / 認証系を組み込んでいると、ActionController::API だと挙動が変わるケースがあります(セッション利用など)。
  • そのような場合は、親クラスを変えた後に自分のカスタムコントローラも含めて一通り動作確認する必要があります。

  1. 参考情報 (あれば)

実際に利用する際は、Rails の該当バージョンのガイド・API ドキュメント、activestorage/lib/active_storage/engine.rb 内の config.active_storage 関連設定を確認して、導入するフラグ名・設定方法を正確に把握してください。


#55134 Move prevent_writes defaulting to params of role switching methods

マージ日: 2026/5/8 | 作成者: @joshuay03

  1. 概要 (1-2文で)
    このPRは、Active Record の接続ロール切り替え系メソッドにおける prevent_writes のデフォルト挙動を「メソッド本体で上書き」する形から「メソッド引数レベルで明示的に定義・検証」する形に変更し、:reading ロールで prevent_writes: false を指定した場合は ArgumentError を発生させるようにしたものです。あわせてドキュメントとテストが、この新しい仕様に合わせて整理・強化されています。

  1. 変更内容の詳細

対象となるメソッド

ActiveRecord のパブリックな「ロール切り替え」関連メソッド(例)に影響します。

  • connected_to
  • connected_to_many
  • with_role
  • with_shard
  • with_role_and_shard(内部実装用メソッドも含む)

これらのメソッドは、role: :reading のときに自動的に prevent_writestrue にする挙動を持っていましたが、その実装位置と仕様表現が変わっています。

これまでの挙動

従来は、メソッド本体の中で「もし role == :reading なら prevent_writes を強制的に true に上書き」していました。
そのため、次のようなコードを書いても:

ruby
ActiveRecord::Base.connected_to(role: :reading, prevent_writes: false) do
  # ...
end

実際には prevent_writes は内部で true に上書きされ、書き込みは抑止されていました。ただしこの仕様は:

  • メソッドシグネチャ(引数定義)からは読み取りにくい
  • prevent_writes: false を渡しても無視されるためバグを見落としやすい
  • 一部メソッドではドキュメントに明示されていない

という問題がありました。

新しい仕様・実装

このPRでは、次のように振る舞いが整理されています。

  1. メソッド引数レベルでのデフォルト指定

    • ロール切り替えメソッドのパラメータ定義に、「role: :reading のときは prevent_writes はデフォルトで true」であることを反映。
    • 具体的には、メソッドの引数・キーワード引数の宣言や、内部でのパラメータ処理において、このデフォルトが明示されるよう変更されています(PR説明より)。
  2. :reading + prevent_writes: falseArgumentError

    • これまでは「与えられた値を上書き」していましたが、
    • これからは次のような呼び出しはエラーになります。
    ruby
    ActiveRecord::Base.connected_to(role: :reading, prevent_writes: false) do
      # => ArgumentError を発生させるべきケースに変更
    end

    理由:

    • 読み取り専用の接続ロールで「書き込みを防がない (prevent_writes: false)」という指定は論理的に破綻している
    • かつ、実装上もその指定は既に無視されていたため、明示的にエラーとした方がバグに気付きやすい
  3. with_role_and_shard を含む一貫した挙動とドキュメント整備

    • これまで一部のメソッドは内部でロール判定と prevent_writes 上書きをしていましたが、ロジックの位置を「パラメータ処理」に寄せることで挙動が一貫するようになっています。
    • すべてのロール切り替え系メソッドで、「role: :reading の場合 prevent_writes がデフォルトで true になる」ことがドキュメントに明示されました。
  4. 役割 (role) とシャード (shard) の引数仕様のドキュメント修正

    ドキュメント中で次が整理されています:

    • 「role 引数が必須で、shard 引数はオプション」のメソッド
    • 「role または shard、あるいはその両方を受け取る(and/or)」メソッド

    これにより、各メソッドの使い方がより正確に伝わるようになっています。

  5. テスト・CHANGELOG

    • activerecord/test/cases/base_test.rb
    • activerecord/test/cases/shard_keys_test.rb

    にテストが追加・更新され、新しい prevent_writes の扱いやエラー条件がカバーされています。
    activerecord/CHANGELOG.md にも、この挙動変更が記載されました。


  1. 影響範囲・注意点
  • APIの挙動変更(潜在的な破壊的変更)

    • これまで「黙って prevent_writestrue に上書き」していたケースで、今後は ArgumentError になり得ます。

    • 特に、既存コードで以下のようなパターンがあるとエラーになる可能性が高いです。

      ruby
      # NG になるようになった例
      ActiveRecord::Base.connected_to(role: :reading, prevent_writes: false) do
        # ...
      end
      
      # role を動的に渡しているコードも要注意
      role = compute_role # :reading になることがある
      ActiveRecord::Base.connected_to(role: role, prevent_writes: some_flag) do
        # some_flag が false の場合にエラー
      end
    • 以前は「実質無視されていた」「意図しない指定」に対して、明示的なエラーが出るようになるので、テスト時にこの種のバグがあれば早期に発見できます。

  • 読み取りロールで「書き込み許可」はできない

    • このPRにより、「role: :reading だが一時的に書き込みを許可したい」というような設計は、少なくとも公式APIとしてはサポートしない方向性がより明確になりました。
    • 書き込みが必要な処理は、明示的に書き込み可能なロール (:writing など) に切り替える必要があります。
  • ドキュメントに沿った引数使用が重要

    • 「role が必須か」「shard はオプションか」「role/shard のどちらか/両方を渡せるのか」といった仕様がドキュメントで整理されたので、それに沿って引数を指定することが重要です。
    • 既存コードで role/shard の指定の仕方が曖昧だった場合、今後はより厳密にチェック・把握しておくと安全です。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/55134
    タイトル: Move prevent_writes defaulting to params of role switching methods
  • 関連コンテキスト:
    • Rails 6 以降のマルチDB・ロール/シャード対応 (connected_to, with_role, with_shard など) に関する設計の一環として、「読み取りロールは常に書き込み禁止」という前提が明確化された変更と捉えられます。
    • prevent_writes は、読み取り用レプリカなどに対して誤って書き込みを行わないためのガードとして使われるフラグですが、その仕様がより宣言的かつ安全になっています。

#57091 Fix bug with escaped characters when "Copy as text" is clicked

マージ日: 2026/5/8 | 作成者: @bogdan

  1. 概要 (1-2文で)
    Rails の例外画面にある「Copy as text」ボタンでコピーされるテキストに、HTML エスケープされた文字(&#39; など)が混ざってしまう不具合を修正した PR です。テキストの格納先を <script> 要素の body から <textarea> 要素の value に変更することで、自動エスケープを回避しています。

  1. 変更内容の詳細

何が問題だったか

  • Rails のエラーページ(ActionDispatch::DebugExceptions が描画する rescue テンプレート)には、「Copy as text」ボタンがあり、スタックトレースなどをそのままコピーできるようになっています。
  • 以前は、このコピー用テキストを HTML テンプレート内の <script> タグの中に埋め込み、それを JS 側で読み出してクリップボードにコピーしていました。
  • しかし <script> タグ内のテキストは、テンプレート描画時に HTML エスケープされるため、例外メッセージ中のシングルクォート '&#39; になるなど、コピー結果が本来の文字列と異なってしまう問題がありました。

PR 説明にある例:

text
app/controllers/admin/assets_uploads_controller.rb:73:in &#39;Admin::AssetsUploadsController#resource_params&#39;

本来は:

text
app/controllers/admin/assets_uploads_controller.rb:73:in 'Admin::AssetsUploadsController#resource_params'

とコピーされるべきところがエスケープされてしまっていた、という状況です。

修正方針

  • コピー対象のテキストを <script> タグではなく <textarea> 要素の value 属性として保持するように変更しています。
  • <textarea>value はフォームの値として扱われ、<' などがそのまま保持され、不要な HTML エスケープが入りません。
  • JS からは textarea.value を参照してクリップボードへコピーすることで、画面表示と同じ生のテキストがコピーされます。

コードレベルの変更イメージ

actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb の中で:

  • 以前: エラーテキストを <script> 要素内のテキストとして埋め込んでいた。
  • 以後: 同じテキストを <textarea id="...">value として配置し、JS 側で document.getElementById("...").value を参照する形へ変更。

テスト (actionpack/test/dispatch/debug_exceptions_test.rb) も更新され、「Copy as text」で取得される文字列に HTML エスケープされた文字が含まれないことを検証するようになっています。


  1. 影響範囲・注意点
  • 影響範囲は Rails の開発・デバッグ時に表示されるエラーページ(rescue テンプレート)のみで、本番環境の挙動やアプリケーションコードには影響しません。
  • 既存の「Copy as text」ボタンを利用している開発者にとっては、コピーされる内容がより正確(表示されているものと一致) になります。
  • エスケープが解消されることにより、例外メッセージ内のクォートを含むコードフラグメントを、そのままエディタや issue tracker に貼り付けやすくなります。
  • 内部的な実装が <script><textarea> に変わっているため、もし独自に rescue テンプレートを上書きして「Copy as text」機能に依存している場合は、テンプレートの差分を確認するとよいです(ただし一般的な Rails アプリではこのカスタマイズは稀です)。

  1. 参考情報 (あれば)
  • 変更ファイル:
    • actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
    • actionpack/test/dispatch/debug_exceptions_test.rb
  • 機能としては「例外ページの UX 改善・バグ修正」であり、アプリ側のコード変更は不要です。
  • エラーページや ActionDispatch::DebugExceptions のカスタマイズをしている場合は、今後のバージョンアップ時にこの変更とコンフリクトしないか確認してください。

#57309 Prevent test_checkout_fairness_by_group from hanging indefinitely

マージ日: 2026/5/8 | 作成者: @bogdan

  1. 概要 (1-2文で)
    activerecord の一部テストが CI 上で無限にハングする可能性があった問題に対して、該当テストを含む複数のテストで待機処理をタイムアウト付きにするなどのリファクタリングを行い、「落ちるときはきちんと失敗として終了する」ようにした PR です。アプリケーションコードには影響せず、テスト専用の変更です。

  1. 変更内容の詳細

ハングしていたテストの問題点

対象となったのは activerecord/test/cases/connection_pool_test.rb 内の test_checkout_fairness_by_group で、スレッドの完了を次のような形でポーリングしていました(イメージ):

ruby
loop do
  sleep 0.1
  break if success || error
end

ここで、何らかの理由で success / error のいずれにも到達しない場合、このループが永遠に回り続け、CI ジョブ全体がエージェント側のジョブタイムアウトまでハングしてしまう、という問題がありました。

対応内容: Timeout でループに上限を付与

このポーリングループを Timeout.timeout(5) でラップし、最大 5 秒でタイムアウトさせるようにしています。

概念的には以下のような形です:

ruby
Timeout.timeout(5) do
  loop do
    sleep 0.1
    break if success || error
  end
end

こうすることで、もしテストロジックのバグや環境要因でスレッドが期待どおり完了しない場合でも、

  • テストは 5 秒で Timeout::Error となり
  • 通常のテスト失敗としてレポートされる
    ようになり、CI のビルド全体が延々とハングする状況を避けられます。

テストヘルパの共通化・微修正

activerecord/test/cases/helper.rb が +14/-6 行と最も変更多くなっていることから、以下のような共通化/ユーティリティ追加が行われています:

  • 複数のテストで使われている「スレッド・非同期動作を待ち合わせるためのユーティリティ」を helper に寄せる、または
  • Timeout 付きの待機処理をヘルパーメソッドとして追加し、個々のテストからそれを利用するように変更

この結果として、以下のテストファイルで +1/-1 程度の小さな変更が入っています(主にヘルパの呼び出し名の変更・統一などが想定されます):

  • associations/belongs_to_associations_test.rb
  • associations/has_many_associations_test.rb
  • associations/has_one_associations_test.rb
  • relation/load_async_test.rb

変更規模から、個々のテスト内のインライン実装を「helper に切り出した共通メソッド呼び出しに置き換えた」程度の修正と考えて差し支えありません。

将来的な改善案への言及

PR 説明にある通り、「ポーリング + sleep」は本質的に不安定で遅いので、将来的には同じファイル内で既に使われている Concurrent::CountDownLatch への置き換えを検討している、というコメントがあります。
この PR ではそこまでは行わず、まずは CI のハングを解消するための最小限の Timeout 導入に留めています。


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

    • Rails 本体のプロダクションコードには一切変更がなく、影響は Rails 自身のテストスイートに限定されます。
    • これまで稀に「CI が止まったように見える」原因になっていた activerecord 関連テスト(特に接続プールの公平性テスト)の挙動が、ハングではなく 5 秒程度でのテスト失敗として顕在化するようになります。
    • 他の association / async load 関連テストも、非同期処理の待ち方が helper を介する形に揃えられており、将来的な保守性や flakiness 改善に寄与します。
  • 注意点

    • 5 秒というタイムアウト値は CI を止めないための安全弁としては妥当ですが、環境によっては「本当に重いが正しく進行しているテスト」が稀に Timeout::Error になる可能性もゼロではありません。ただし、それ自体が「テストの設計が遅すぎる/脆い」サインとも言えるため、問題が顕在化したらテストの作りを見直す良いきっかけになります。
    • Rails を利用するアプリ側のテストには何も影響しませんが、Rails 本体のテストを自前で回している開発者は、今後このテストがタイムアウトで落ちるようなら、接続プールまわりの挙動や環境依存の問題を疑う必要があります。

  1. 参考情報 (あれば)
  • ハングが観測されたビルドログ:
    https://buildkite.com/rails/rails/builds/128041
  • 類似のテスト flakiness に関する過去の issue:
    https://github.com/rails/rails/pull/53591
  • 非同期テストでの sleep ポーリングの代替として言及されている Concurrent::CountDownLatchconcurrent-ruby の同期プリミティブで、特定数のイベント発生まで待機する用途に向いています。Rails テスト内でも既に別箇所で利用されています。

#57320 Simplify more activerecord tests with NotificationAssertions helpers

マージ日: 2026/5/8 | 作成者: @Saidbek

  1. 概要 (1-2文で)
    activerecord の一部テストで、手書きしていた ActiveSupport::Notifications の subscribe/unsubscribe ロジックを、共通の NotificationAssertions ヘルパー(capture_notifications など)に置き換えて簡潔にした PR です。テストの重複コードを減らし、通知の検証ロジックを統一するリファクタリングであり、挙動やパブリック API の仕様変更はありません。

  1. 変更内容の詳細

全体方針

  • 対象ファイル:
    • trilogy_adapter_test.rb
    • instrumentation_test.rb
    • query_cache_test.rb
  • いずれも、これまで以下のようなパターンで通知を検証していました:
    • ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| ... end
    • テスト内で直接 subscribe し、ブロック内でアサーション (assert_equal, refute, フラグのセットなど) を行う
    • 明示的に unsubscribe する、または ActiveSupport::Notifications.subscribed を使う
  • これを NotificationAssertions モジュールが提供するヘルパー(主に capture_notifications)に統一し、
    • テストの本体では「どのイベントが発火したか」「ペイロード内容」をシンプルに検証するだけにする
    • subscribe/unsubscribe の煩雑さやフラグ管理を隠蔽する という形に整理しています。

以下は変更の代表的なイメージです(疑似コードレベルです。実コードのメソッド名やペイロードキーはテスト内容に依存します)。

変更前の典型パターン(手動 subscribe)

ruby
def test_something_is_instrumented
  asserted = false

  callback = lambda do |*args|
    event = ActiveSupport::Notifications::Event.new(*args)
    if event.name == "sql.active_record"
      asserted = true
      assert_match(/SELECT/, event.payload[:sql])
    end
  end

  ActiveSupport::Notifications.subscribe("sql.active_record", &callback)

  begin
    # 実際の処理
    Model.where(id: 1).load
  ensure
    ActiveSupport::Notifications.unsubscribe(callback)
  end

  assert asserted, "sql.active_record が発火していない"
end

変更後の典型パターン(capture_notifications 利用)

ruby
def test_something_is_instrumented
  events = capture_notifications("sql.active_record") do
    # 実際の処理
    Model.where(id: 1).load
  end

  sql_events = events.select { |e| e.payload[:sql].match?(/SELECT/) }

  assert_not_empty sql_events
  # あるいは最初のイベントだけを検査
  assert_match(/SELECT/, sql_events.first.payload[:sql])
end

ポイント:

  • イベントを配列として受け取る
    capture_notifications("sql.active_record") { ... } が、ブロック実行中に発火した該当イベントを ActiveSupport::Notifications::Event オブジェクトの配列として返すため、
    • 手動で Event.new(*args) を呼ぶ処理が不要
    • assert 用のフラグ (asserted = true) も不要
  • フィルタリングとアサーションが明確
    select / find / count 等でイベント配列をフィルタリングし、その結果に対して普通のアサーションを書くスタイルになっており、テストの意図(どのイベントを何件期待するか)が読み取りやすくなっています。

trilogy_adapter_test.rb の変更

  • Trilogy アダプタ(MySQL 向けアダプタ)のテストで、SQL 実行や接続に関する instrumentation が期待通り発火しているかを検証する箇所を capture_notifications ベースに変更。
  • 代表的な変更パターン:
    • 以前は ActiveSupport::Notifications.subscribe("sql.active_record") 内で SQL 文・アダプタ名・バインドなどを直接検証していた。
    • 今回は、テスト本体で:
      1. events = capture_notifications("sql.active_record") { 実際のクエリ実行 }
      2. trilogy_events = events.select { |e| e.payload[:adapter] == "Trilogy" }
      3. assert_equal 1, trilogy_events.size などで検証
    • このスタイルに統一されています。

instrumentation_test.rb の変更

  • ActiveRecord の各種 instrumentation(例: クエリ実行、キャッシュヒット/ミス、トランザクション開始終了など)のテストに NotificationAssertions を使用。
  • 以前はテストごとに subscribe ブロックを用意して、if name == "sql.active_record" のような条件分岐を入れていましたが、今回:
    • capture_notifications で対象のイベント名だけをキャプチャし、
    • 期待される回数やペイロード内容(payload[:name], payload[:connection_id] など)を配列経由で検証する形に変更されています。
  • subscribed パターン(ActiveSupport::Notifications.subscribed(callback, "sql.active_record") { ... })も同様に capture_notifications にまとめられています。

query_cache_test.rb の変更

  • クエリキャッシュに関する通知(キャッシュヒット・ミス・書き込み等)を検証するテストを NotificationAssertions に移行。
  • 代表的な検証例:
    • 以前: subscribe して、payload[:name]payload[:sql] でキャッシュヒットかどうかを判定し、即座に assert
    • 現在:
      1. events = capture_notifications("sql.active_record") { 同じクエリを2回実行 }
      2. 1回目のイベントでは payload[:cached]false(ミス)、2回目では true(ヒット)であることを events[0].payload[:cached] のように検証。
  • これにより、キャッシュまわりの挙動が「どのタイミングでどんなイベントが出るか」という観点で読みやすくなっています。

  1. 影響範囲・注意点
  • 機能・挙動への影響

    • 変更はすべて activerecord/test/... 配下のテストコードのみであり、本体のライブラリコードやパブリック API の挙動には影響しません。
    • したがって Rails を利用するアプリケーション側への互換性影響はありません。
  • テストコードを書く側への示唆

    • 今後 ActiveSupport::Notifications を使ったテストを書く際は、基本的に NotificationAssertions のヘルパーを使うのが推奨される流れになります。
    • 既存の手書き subscribe パターンは、同様に capture_notifications にリファクタリングできる余地がある場合、同じスタイルに寄せるとテストの一貫性が上がります。
    • capture_notifications が返すのは ActiveSupport::Notifications::Event の配列なので、ペイロードを見たい場合は event.payload[:key]、時間情報を見たい場合は event.time, event.end, event.duration を使えます。
  • 注意点

    • capture_notifications は指定したイベント名(もしくは正規表現など)を対象とするため、複数のイベント名をまとめて扱いたい場合は、テスト側でフィルタリングを行う必要があります。
    • テストの意図として「イベントが1つも来なかったこと」を検証したい場合でも、これまでの asserted フラグではなく events.empty? / assert_empty events / assert_equal 0, events.size の形で表現されるようになります。

  1. 参考情報 (あれば)
  • フォローアップ元 PR: #56989
    • こちらで NotificationAssertions ヘルパーが導入され、すでに一部テストが移行済み。
  • 関連モジュール:
    • ActiveSupport::Notifications
    • ActiveSupport::Notifications::Event
    • NotificationAssertions(Rails テスト用ヘルパー。capture_notifications などを提供)
  • 今回の PR はテストリファクタリングのみのため、CHANGELOG などは更新されていません。

#51244 [Fix #50118] :prepend option not working with run_after_transaction_callbacks_in_order_defined config

マージ日: 2026/5/8 | 作成者: @joshuay03

  1. 概要 (1-2文で)
    config.active_record.run_after_transaction_callbacks_in_order_defined = true を有効にしている場合でも、after_commit / after_rollback に渡した prepend: true オプションが正しく効くようにしたバグ修正です。トランザクション後コールバックの実行順に関する仕様どおりの挙動が保証されるようになりました。

  1. 変更内容の詳細

何が問題だったか

Active Record には以下の2つの要素があります:

  • モデルに定義した順に after_commit / after_rollback を実行したい、という設定
    ruby
    config.active_record.run_after_transaction_callbacks_in_order_defined = true
  • 個別のコールバックを「一番最初に実行させたい」ためのオプション
    ruby
    after_commit :do_something, prepend: true

本来は、「基本は定義順だが、prepend: true を付けたものは先頭に来る」という挙動になるべきですが、この設定を有効にすると prepend: true が無視されるケースがありました(Issue #50118)。
そのため、既存アプリが prepend: true を前提としていると、Rails 7.1 以降などで実行順が変わるというバグが起きていました。

このPRでの修正内容

主に以下のポイントで修正が入っています。

1. コールバック登録ロジックの見直し

activerecord/lib/active_record/transactions.rb 内で、トランザクション後コールバック(after_commit / after_rollback)を登録・実行順序を決める処理が見直されています。

  • run_after_transaction_callbacks_in_order_defined = true の場合でも、
    • prepend: true が指定されたコールバックは「同じ kind(commit/rollback)・同じタイミングの中で先頭」になる
    • それ以外のコールバックは、従来どおりモデルへ定義された順番で実行される
  • つまり、「定義順」(config)と「prepend」(個別オプション)の両方を満たすように順序制御が調整されています。

実装上は、トランザクションコールバックの内部配列への挿入位置や、グルーピング/ソートの方法が変更されています(+19 / -16 行程度の変更)。

2. テスト追加・修正

activerecord/test/cases/transaction_callbacks_test.rb に以下のようなテストが追加・修正されています。

  • run_after_transaction_callbacks_in_order_defined = true を有効にした状態で、
    • 通常の after_commit / after_rollback
    • prepend: true を付けた after_commit / after_rollback
      を混在させ、実際の実行順が期待と一致することを検証。

イメージとしては、次のようなケースがテストされています(簡略化した疑似コード):

ruby
class Post < ActiveRecord::Base
  after_commit :callback_a
  after_commit :callback_b
  after_commit :callback_c, prepend: true
end

期待される実行順:

  1. callback_c(prepend 指定)
  2. callback_a(定義順)
  3. callback_b(定義順)

after_rollback についても同様のテストが追加されています。

3. CHANGELOG の更新

activerecord/CHANGELOG.md に今回のバグ修正が追記され、
run_after_transaction_callbacks_in_order_defined 設定と prepend オプションの組み合わせで起きていた不具合を修正した」旨が明示されました。


  1. 影響範囲・注意点

影響を受けるケース

次の両方を満たすアプリケーションは、トランザクション後コールバックの実行順が変わる可能性があります

  1. config.active_record.run_after_transaction_callbacks_in_order_defined = true を設定している
  2. after_commit または after_rollbackprepend: true を使っている

この PR 前までは「バグによって prepend: true が効いていなかった」ため、その壊れた挙動に依存しているコードがあれば、順序が変わり挙動が変化します。

特に注意が必要なのは:

  • コールバック内で外部サービスへの通知や非同期ジョブ投入を行っていて、「どの処理が先に走るか」に依存している場合
  • 親子モデルや複数レコード間で after_commit の順序が前提になっている場合

マイグレーション/設定変更の必要性

  • 設定キーやメソッドの API 変更はなく、コードの書き換えは不要です。
  • ただし、「prepend: true を付けたのに、なぜか定義順に扱われている」ようなコードがあった場合は、今回の修正により本来意図した順番(=先頭実行)に戻るため、
    必要に応じてテストで実行順を確認することが推奨されます。

  1. 参考情報 (あれば)

#57316 Reject malformed Mailgun ingress timestamps

マージ日: 2026/5/8 | 作成者: @afurm

  1. 概要 (1-2文で)
    Mailgun 用 Action Mailbox ingress が、壊れた形式の timestamp を受け取ったときに 500 エラーになっていた問題を修正し、認証失敗(401/403 想定)として処理するようにした PR です。
    HMAC 計算には従来どおり生の文字列を使い、別途パースしたタイムスタンプで時刻の妥当性チェックを行うことで、パースエラーを例外ではなく「認証失敗」として扱うようにしています。

  1. 変更内容の詳細

問題点

既存実装では、Mailgun からのリクエストに含まれる timestamp を以下のように扱っていました:

  • HMAC 検証用の入力として timestamp(文字列)を使用
  • 同時に Integer(timestamp) で数値に変換し、リクエストの「古さ(recency)」をチェック

このとき timestamp が不正な文字列(例: "abc", 空文字, フォーマット不良など)の場合、Integer(timestamp)ArgumentError を送出し、認証ロジックの「結果として false を返す」のではなく、サーバ内部エラー(500)として落ちる挙動になっていました。

本来は「不正な署名 or 不正な timestamp」として 401/403 を返したいケースです。

修正内容

PR の要点は次の 2 点です。

  1. HMAC 用の timestamp と、時刻チェック用の timestamp を分離

    • HMAC 検証には 生の timestamp 文字列をそのまま使う(従来どおり)。
    • 時刻の妥当性チェック用には、別変数にパースした timestamp(整数)を使う。
  2. timestamp のパース時に例外を投げず、認証失敗として扱う

    • Integer(timestamp, exception: false) を用いてパース。
    • パースに失敗した場合は nil が返るため、これを検知して 「認証失敗」として扱う
    • その結果、401/403(Action Mailbox の既存挙動)として「拒否」され、500 にはならない。

概念的なイメージコードは以下のようになります(実際のコードの簡略版):

ruby
# 変更前(イメージ)
def authenticate_request
  raw_timestamp = params[:timestamp]
  token         = params[:token]
  signature     = params[:signature]

  # ここで壊れた timestamp だと ArgumentError が発生して 500 エラーとなる
  timestamp = Integer(raw_timestamp)

  hmac_valid = valid_signature?(raw_timestamp, token, signature)
  recent     = recent_enough?(timestamp)

  hmac_valid && recent
end

# 変更後(イメージ)
def authenticate_request
  raw_timestamp = params[:timestamp]
  token         = params[:token]
  signature     = params[:signature]

  # HMAC 入力には raw_timestamp をそのまま利用(Mailgun 仕様どおり)
  hmac_valid = valid_signature?(raw_timestamp, token, signature)

  # recency チェック用に別途 timestamp をパース
  parsed_timestamp = Integer(raw_timestamp, exception: false)

  # パースに失敗(nil)した場合は recency チェック NG として扱う
  recent = parsed_timestamp && recent_enough?(parsed_timestamp)

  hmac_valid && recent
end

テスト追加

actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb にテストが追加されています(+18行)。

追加されたテストでは:

  • timestamp に不正な値(数値化できない文字列など)を渡した場合に
    • サーバが 500 を返さないこと
    • リクエストが正しく拒否されること(ステータスコード・レスポンス内容) を検証していると考えられます。

また、PR 説明にある通り、以下で検証済みです:

bash
cd actionmailbox && bin/test test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
cd actionmailbox && bin/test
bundle exec rubocop actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb
git diff --check

  1. 影響範囲・注意点
  • 対象は Mailgun ingress を使っている Rails アプリ です(ActionMailbox::Ingresses::Mailgun::InboundEmailsController)。
  • これまで:
    • timestamp が壊れているリクエスト → 500 エラーになっていた可能性がある。
  • 変更後:
    • 同じリクエストは 認証失敗として扱われ、401/403(または 4xx)で拒否される挙動に変わる。
    • これにより、攻撃的・誤ったリクエストでアプリが 500 を返すことを防げる。

注意点・補足:

  • HMAC 計算時の入力(timestamp の生文字列)は従来の仕様のままなので、正しくフォーマットされた Mailgun リクエストへの互換性は保たれる想定です。
  • Mailgun 側が公式に指定する署名検証手順は「生の timestamp 文字列+token+署名」で HMAC を取る方式なので、この PR はその仕様に忠実です。
  • もしアプリ側で「Mailgun ingress への 500 エラー増加」を監視していた場合、本修正導入後は 4xx に変わるため、メトリクスやアラート設定に影響する可能性があります。

  1. 参考情報 (あれば)
  • 対応する Issue: #57315
    → 「Mailgun ingress が壊れた timestamp で 500 を返してしまう」旨の報告と考えられます。
  • Mailgun 署名検証の仕様(公式ドキュメント)
    • 概要: HMAC-SHA256(api_key, timestamp + token) を計算し、signature と比較。
    • ここでの timestamp は文字列(Mailgun が送ってくるまま)で扱うのが前提。
  • 対象ファイル:
    • actionmailbox/app/controllers/action_mailbox/ingresses/mailgun/inbound_emails_controller.rb
    • actionmailbox/test/controllers/ingresses/mailgun/inbound_emails_controller_test.rb

#57174 Update EnvConfiguration class doc examples and drop stale commented require

マージ日: 2026/5/8 | 作成者: @Saidbek

  1. 概要 (1-2文で)
    EnvConfiguration クラスの RDoc (ドキュメントコメント) に書かれている option / envify のサンプルコードが、実際の挙動に即した正しい ENV キー・fetch の使い方になるよう更新されました。あわせて、不要になっていたコメントアウトされた require 行が削除されています。

  1. 変更内容の詳細

※ この PR は実装ロジックではなく「ドキュメント (RDoc) のサンプルコード」の修正です。

  • 対象ファイル: activesupport/lib/active_support/env_configuration.rb
  • 主な変更点:
    • EnvConfiguration クラスの doc コメント内にある optionenvify の使用例が修正された。
      • ENV から値を取る際のキー名が、実際の envify が生成するキーと一致するように修正。
      • fetch の呼び方(引数の形、デフォルト値の渡し方など)が、実際に使われているインターフェイスに沿う形に更新。
    • コメントアウトされた古い require 行(既に不要だが残骸として残っていたもの)が削除された(+2/-4 行という小さな差分のうち、これが一部を占める)。

EnvConfiguration はだいたい次のような役割のクラスです:

  • option :foo, default: ... といった形で設定項目を定義する
  • envify によって、ENV から値を取ってきて Config.foo などに反映するための仕組みを提供する

この PR では、その説明用の RDoc で:

  • ENV["SOME_KEY"] のキー名が間違っている/不自然
  • ENV.fetch の引数の与え方が、実際の envify のインターフェイスとズレている

といった「サンプルコードとして見たときに誤解を招く」部分が修正されています。

そのため、ドキュメントを見ながら EnvConfigurationenvify を使う開発者が、正しい ENV キーおよび fetch の使い方をそのままコピペできるようになっています。


  1. 影響範囲・注意点
  • ランタイム挙動への影響: なし
    • 変更は doc コメントとコメントアウトコードの削除のみで、クラスの実装は一切変わっていません。
  • 既存アプリケーションが壊れる可能性: なし
  • 注意点:
    • これまで古い RDoc の例をそのままコピペして使っていた場合、今回の PR によって「どこが本来の正しい使い方なのか」が明確になります。
    • envify 利用時に ENV キー名や fetch の使い方で迷っていた場合は、最新のドキュメントを見直す価値があります。

  1. 参考情報 (あれば)

#57321 Fix running AbstractMySQL tests when testing Trilogy and Mysql2

マージ日: 2026/5/8 | 作成者: @byroot

  1. 概要 (1-2文で)
    Active Record の MySQL 抽象テスト (AbstractMySQL テスト群) を、Trilogy アダプタと Mysql2 アダプタの両方で正しく実行できるように、テスト用のサポートコード (activerecord/test/support/tools.rb) が修正されています。主にテスト実行時のアダプタ切り替えロジック(またはアダプタ判定・ロード周り)の不備を解消する小さな変更です。

  1. 変更内容の詳細

※このPRは activerecord/test/support/tools.rb に+7/-2行の変更のみで、説明文もないため、Rails の既存構造とPRタイトルから推測を含めて解説します。

背景

Active Record のテストスイートには、MySQL 向けの共通テスト(AbstractMySQL)があり、

  • Mysql2 アダプタ
  • Trilogy アダプタ(Shopify 発の MySQL 互換アダプタ)

など複数のアダプタに対して同じテストを流すための仕組みがあります。

しかし、現状では:

  • 特定のアダプタでしか動かない前提でツールが書かれていた
  • もしくは、アダプタを切り替えてテストを実行するための指定・検出ロジックに不整合があった

といった理由で、「AbstractMySQL テストを Trilogy と Mysql2 の両方で回す」ことがうまくいっていなかったと考えられます。

activerecord/test/support/tools.rb の修正内容の方向性

このファイルには、テストスイートを実行するための補助メソッドや、アダプタ・DB設定の切り替えロジックなどが定義されています。
PR タイトルから推定すると、以下のような変更が行われた可能性が高いです(代表的なパターンを列挙):

  1. アダプタ名・ENV 変数の扱い修正

    • 例: ENV["ARCONN"] や独自の環境変数経由でどのアダプタでテストを走らせるかを指定しているが、

      • "trilogy"
      • "mysql2"
        など両者を正しく識別できていなかった、あるいは AbstractMySQL 用の共通パスに乗らなかった問題を修正。
    • 典型的にはこんなロジックの修正が入りがちです:

      ruby
      # 修正前(例)
      if ENV["ARCONN"] == "mysql2"
        # MySQL 用テストを有効化
      end
      
      # 修正後(例)
      if %w[mysql2 trilogy].include?(ENV["ARCONN"])
        # MySQL 抽象テストを両アダプタで有効化
      end
  2. AbstractMySQL テストのスキップ条件を整理

    • 例えば「MySQL 系アダプタ以外ならスキップ」という判定が mysql2 だけを想定しており、Trilogy だと誤ってスキップされていた可能性があります。
    • それを「MySQL 互換アダプタのグループ」として扱うように変更した、など。
  3. テストランナーのユーティリティに、アダプタ毎の実行パターンを追加

    • 例: run_abstract_mysql_tests_for(adapter) のようなヘルパがあり、Trilogy 用にも呼び出せるようになった、あるいはマルチアダプタ対応の DSL が整備された、など。

実際の差分は数行レベルのため、概ね「アダプタの判定条件を拡張した」「テスト実行時に AbstractMySQL テストが意図したアダプタの設定で走るようにした」といった小規模なロジック修正と考えるのが妥当です。


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

    • Rails 自体のランタイム動作・アプリケーションコードには影響せず、Active Record のテストスイートのみに影響する変更です。
    • 特に、MySQL 関連の共通テスト(AbstractMySQL)を、複数アダプタ(Trilogy / Mysql2)で回す際のテスト環境構築・実行方法に影響します。
    • Rails コアのCIや、自前で Rails 本体をテストしている開発者(フレームワークコントリビュータ)が主な対象です。
  • 注意点

    • 自前で Rails 本体のテストスイートを回していて、ARCONN=mysql2ARCONN=trilogy などでアダプタを切り替えている場合、
      • 今後は 同じ AbstractMySQL テストが両方のアダプタに対して適切に実行される ようになります。
    • もし、これまで「mysql2 のときだけテストが動く」前提のスクリプトやCI設定を組んでいた場合は、Trilogy に切り替えたときの挙動も合わせて確認しておくと安全です。
    • 一部のアダプタ特有の制約や未対応機能が AbstractMySQL テストで表面化する可能性があるため、Trilogy アダプタの開発・検証をしている場合は、新たに失敗するテストが出ていないかに注意してください。

  1. 参考情報 (あれば)
  • 対象PR: https://github.com/rails/rails/pull/57321
  • 関連しうるもの:
    • Active Record のアダプタ毎テストについては activerecord/test 以下の各アダプタ用テスト (mysql2, trilogy など) や abstract_mysql 系テストクラスを参照すると、どのテストが共通化されているか確認できます。
    • Trilogy アダプタ自体の仕様・互換性は、Trilogy のリポジトリや関連PR(Trilogy サポート追加系の PR)を見ると理解しやすいです。

#57304 [ci skip] Fix getting started guide: remove autocomplete="off" from authenticity_token example

マージ日: 2026/5/7 | 作成者: @ryu

  1. 概要 (1-2文で)
    Rails の Getting Started ガイド内にあるフォームのサンプルコードから、authenticity_token 用の hidden フィールドに付いていた autocomplete="off" を削除し、実際の Rails 8.1.0 以降の出力に合わせてドキュメントを修正した PRです。機能追加や挙動変更ではなく、ドキュメントの追随・整合性を取るための変更です。

  1. 変更内容の詳細

対象ファイル:

  • guides/source/getting_started.md

変更前(ガイド内の HTML サンプル):

html
<input type="hidden" name="authenticity_token" value="..." autocomplete="off">

変更後:

html
<input type="hidden" name="authenticity_token" value="...">

背景となる仕様/挙動:

  • Rails 8.1.0 から、token_tag などが生成する hidden フィールドには autocomplete="off" が自動的には付与されなくなりました。
  • これは #53512 の変更により、HTML 仕様上 input type="hidden"autocomplete 属性は適用されないため削除されたものです。
  • 今回の PR は、その実装変更(#53512)に合わせて Getting Started ガイドのサンプルコードを修正し、現行の Rails が実際に生成する HTML と一致させるものです。
  • タイトルに [ci skip] が付いており、CI を走らせないドキュメントのみの微修正として扱われています。

  1. 影響範囲・注意点
  • 実装コードやランタイムの挙動は一切変更されておらず、「ガイドのサンプルが最新の出力に追いついた」だけです。
  • 既存アプリで、hidden フィールド(特に authenticity_token)に autocomplete="off" を付けている場合:
    • HTML 仕様上意味のない属性であり、ブラウザの挙動には基本的に影響しませんが、不要な属性として削除しても問題ないことが改めて確認できます。
  • ドキュメントを見ながらフォームを手書きしている場合:
    • 旧ガイドを参照していると autocomplete="off" を付けてしまうかもしれませんが、付ける必要はありません。
    • Rails ヘルパ(form_with など)を使っていれば、Rails 8.1.0 以降は最初から付かないため、ガイドと実際の出力が食い違うことがこの PR で解消されています。

  1. 参考情報 (あれば)
  • 関連 PR: #53512 – Remove autocomplete="off" from hidden inputs
  • 対応する Rails バージョン: 8.1.0 以降
  • HTML 仕様: input type="hidden" はユーザーの入力対象ではないため、autocomplete 属性は意味を持たず、仕様上も対象外とされています。

#57266 Replace Europe/Kiev with Europe/Kyiv and Asia/Rangoon with Asia/Yangon

マージ日: 2026/5/7 | 作成者: @tsymbalenkovlad

  1. 概要 (1-2文で)
    Rails のタイムゾーンマッピングから廃止された IANA タイムゾーン名「Europe/Kiev」「Asia/Rangoon」を使うのをやめ、「Europe/Kyiv」「Asia/Yangon」に置き換える PR です。既存の古い名称では ActiveSupport::TimeZone が解決できない問題へのフォローアップ対応です。

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

変更対象ファイル

  • activesupport/lib/active_support/values/time_zone.rb

ActiveSupport::TimeZone::MAPPING(Rails 独自のタイムゾーン名と IANA タイムゾーン名の対応表)の中で、以下のような変更が行われています(概念的なイメージ):

ruby
# 変更前(イメージ)
MAPPING = {
  # ...
  "Kyiv"   => "Europe/Kiev",
  "Yangon" => "Asia/Rangoon",
  # ...
}

# 変更後(イメージ)
MAPPING = {
  # ...
  "Kyiv"   => "Europe/Kyiv",
  "Yangon" => "Asia/Yangon",
  # ...
}

ポイント:

  • 参照している IANA タイムゾーン名を「非推奨・廃止された別名」から「現在の正式名称」に置き換え。
  • 実際の差分は +2/-2 行で、純粋に文字列を置き換えているだけです。

背景

PR の Motivation にある通り、この PR は以前の PR(https://github.com/rails/rails/pull/55494)で行われたタイムゾーンまわりの整理のフォローアップです。

作者が提示しているチェック方法:

rb
ActiveSupport::TimeZone::MAPPING.keys.select { ActiveSupport::TimeZone.new(it).nil? }

このコードで、

  • MAPPING に登録されているキーから ActiveSupport::TimeZone.new で解決できないもの(= 実際には有効でない/廃止された IANA 名を参照しているもの) を洗い出し、そのうち「Europe/Kiev」「Asia/Rangoon」を修正した、という位置づけです。

  1. 影響範囲・注意点

影響範囲

  • ActiveSupport::TimeZone を通じて「Kyiv」「Yangon」タイムゾーンを利用しているアプリケーション。
    • 例: Time.zone = "Kyiv"Time.zone = "Yangon"config.time_zone = "Kyiv" など。
  • 内部的に対応する IANA 名が Europe/KievEurope/KyivAsia/RangoonAsia/Yangon に切り替わります。

通常、IANA 側では古い名前はエイリアス(リンク)として扱われることが多いため、動作上の違いはほぼありませんが、「新しい正式名に追随しておく」ことで以下のようなメリットがあります。

  • 将来、OS / tzdata の更新で古いエイリアスが削除・変更されても安全になりやすい。
  • ログや設定画面に表示されるタイムゾーン名を、現在の公式名称に揃えられる。

注意点

  • アプリケーションコード上で「Europe/Kiev」「Asia/Rangoon」という IANA 名を“直接”ハードコードしている場合、この PR 自体はそれらを変更しません(あくまで ActiveSupport::TimeZone::MAPPING の更新だけ)。
    • そのため、Rails アップグレードと合わせて、必要に応じてアプリ側でも IANA 名を正式名称へリネームしていくことが推奨されます。
  • テストなどで "Europe/Kiev" を前提とした文字列比較をしている場合は、正式名称に合わせる方向で見直した方がよいです。

  1. 参考情報 (あれば)

#57297 Document Active Storage routes_prefix scope options

マージ日: 2026/5/7 | 作成者: @Ivanov-Anton

  1. 概要 (1-2文で)
    Active Storage の config.active_storage.routes_prefix が、単なるパス文字列だけでなく scope が受け付けるあらゆるオプション(ハッシュなど)を受け取れることを公式ドキュメントに明記した PR です。これにより、サブドメイン制約などを含む柔軟なルーティング設定が「サポートされていること」がハッキリしました。

  1. 変更内容の詳細

何がドキュメント化されたか

Active Storage のルート定義は内部的に以下のようになっています:

ruby
scope ActiveStorage.routes_prefix do
  # Active Storage のエンジンルートがここにマウントされる
end

ここで ActiveStorage.routes_prefix は、これまで主に「パスプレフィックスを表す文字列」として紹介されていましたが、実際には scope が受け取れる任意の値(文字列またはハッシュ)を渡せます。

この PR では:

  • activestorage/lib/active_storage.rb
    • ActiveStorage.routes_prefix の API コメントを更新し、「文字列だけでなく scope が受け付けるルーティングオプション全般を渡せる」ことを説明
  • ガイドの更新
    • guides/source/configuring.md
      • config.active_storage.routes_prefix の説明に「文字列 or ハッシュを渡せる」ことを追記
      • 単なる /files のようなパス指定だけでなく、scope のオプションをそのまま渡せるという説明を追加
    • guides/source/active_storage_overview.md
      • Active Storage のルーティングについての節に、上記と整合する説明と具体例を追加

想定されるサンプルコード

PR の本文には具体例コードは書かれていませんが、scope の仕様と今回の説明から、以下のような設定が正式に「OK」と明示された形になります。

1) 従来通りの単純なパスプレフィックス

ruby
# config/application.rb または各環境の config
config.active_storage.routes_prefix = '/files'

内部的には:

ruby
scope '/files' do
  # Active Storage ルート
end

2) サブドメイン制約つき + パスプレフィックス

ruby
config.active_storage.routes_prefix = {
  path: '/files',
  constraints: { subdomain: 'assets' }
}

内部的に:

ruby
scope path: '/files', constraints: { subdomain: 'assets' } do
  # Active Storage ルート
end

3) サブドメイン制約のみ(パスはデフォルト)

ruby
config.active_storage.routes_prefix = {
  constraints: { subdomain: 'assets' }
}

など、scope にそのまま渡せるオプション構造であればよい、ということがドキュメント上も明示されました。


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

    • コード(挙動)の変更はなく、ドキュメントと API コメントのみの更新です。
    • すでに config.active_storage.routes_prefix にハッシュを渡して運用していたアプリは、今回の PR によってその使い方が「公式にドキュメント化された」形になります。
    • まだハッシュ指定を利用していなかったアプリでも、Active Storage のエンドポイントをサブドメインや追加の constraints 付きで切り出す設計を検討しやすくなります。
  • 注意点

    • 実際に利用するオプションは scope の仕様に従う必要があります(例: path:, constraints:, defaults:, module: など)。
    • routes_prefix にハッシュを渡した場合、そのまま scope に渡されるため、他のルーティングとの競合・優先度には通常の scope と同じ注意が必要です。
    • 動作自体は以前から可能だったものなので、Rails をこの PR を含むバージョンに上げることで急に挙動が変わることはありません。

  1. 参考情報 (あれば)
  • 対応 Issue: #48915
  • マージされた PR: #57297 – Document Active Storage routes_prefix scope options
  • 関連ドキュメント(更新対象):
    • Configuring Rails Applications ガイドの config.active_storage.routes_prefix
    • Active Storage Overview ガイドのルーティング解説節
  • scope のオプション一覧は ActionDispatch::Routing::Mapper#scope の API ドキュメントを参照すると具体的な指定方法を確認できます。

#57122 railties: fix misleading production cache store comment

マージ日: 2026/5/7 | 作成者: @wwenrr

  1. 概要 (1-2文で)
    Rails が生成する config/environments/production.rb のコメント文が、実際のデフォルトキャッシュストア設定と食い違っていた問題を修正する PR です。実装は変えず、コメントだけを「メモリストア前提」から「ファイルストア前提」に合わせています。

  1. 変更内容の詳細

何が問題だったか

  • Rails::Application::Configuration のデフォルト:

    ruby
    @cache_store = [ :file_store, "#{root}/tmp/cache/" ]

    → デフォルトはファイルベースのキャッシュストア(:file_store)。

  • 一方、アプリ生成時テンプレートの config/environments/production.rb.tt に書かれていたコメント:

    ruby
    # Replace the default in-process memory cache store with a durable alternative.

    → あたかも「デフォルトはプロセス内メモリキャッシュ」であるかのような書き方で、実際の挙動と矛盾していた。

どう直したか

対象ファイル:

  • railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt(1ファイルのみの変更)

コメントを以下のように修正:

Before:

ruby
# Replace the default in-process memory cache store with a durable alternative.

After:

ruby
# Replace the default file-based cache store with a more robust alternative.

コードそのもの(@cache_store のデフォルト値など)は一切変更されておらず、説明文のみが実態に揃えられています。


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

    • 新しく rails new などでアプリを生成する際の config/environments/production.rb に埋め込まれるコメントが変わるだけです。
    • 実際のデフォルトキャッシュストア設定(:file_store)やランタイム挙動には影響ありません。
    • 既存アプリの設定ファイルはこの PR では自動変更されないため、既に生成済みの production.rb のコメントはそのままです。
  • 注意点 / 開発者視点での読み替え

    • これまで「production ではデフォルトがメモリキャッシュ」と誤解してコメントを読んでいた場合、実際には:
      • デフォルト: :file_storetmp/cache 配下)
      • メモリ常駐ではなくファイルシステム依存
    • 本番環境でスケールアウトする場合やパフォーマンス・信頼性を求める場合、コメントの通り、次のようなストアへの移行が推奨されます:
      ruby
      config.cache_store = :mem_cache_store, "cache.example.com"
      # もしくは
      config.cache_store = :redis_cache_store, { url: ENV.fetch("REDIS_URL") }
    • 「default が何か」に依存したドキュメントや教育資料を書いている場合、この PR で示された事実(ファイルストアがデフォルト)と整合しているか注意するとよいです。

  1. 参考情報 (あれば)
  • この PR がクローズした Issue: #56089
  • 関連コード:
    • デフォルト設定: railties/lib/rails/application/configuration.rb@cache_store 初期化部
    • テンプレート: railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
  • デフォルトキャッシュストアと選択肢に関しては公式ガイド「Caching with Rails: An Overview」も参照すると、file_store, mem_cache_store, redis_cache_store などの使い分けを確認できます。

#57317 Ractor-safe class attribute implementation

マージ日: 2026/5/7 | 作成者: @jhawthorn

  1. 概要 (1-2文で)
    Rails の class_attribute 実装が Ractor セーフになるように内部実装が変更され、Ractor でクラス属性を安全に扱えるようになりました。性能劣化は 10% 未満に抑えられつつ、singleton class に対して instance_reader が誤って定義される既存バグも修正されています。

  1. 変更内容の詳細

2-1. 従来の仕組み (問題点)

従来の class_attribute は、継承チェーンに応じた値解決を以下のような形で実装していました。

  • __class_attr__name のようなメソッドを define_method で定義し、
    • そのメソッド内の「クロージャに閉じ込めた値」を返す
    • クラス階層で値が変わるたびに、この define_method でメソッド自体を再定義していた
  • この「クロージャに値を閉じ込める」実装は Ractor セーフではありません
    (Ractor 間で共有する Proc/メソッドに、Ractor セーフでないオブジェクトを閉じ込める可能性があるため)

そのため、Ractor から安全に class_attribute を利用できない、あるいは将来的な Ractor 対応の妨げになっていました。


2-2. 新しい仕組みのポイント

この PR では、値そのものではなく「オーナークラス」を返すメソッドに役割を分離することで、Ractor セーフを達成しています。

変更前

  • __class_attr__name
    • クロージャに値を持ち、呼び出すと値を返す
    • クラスごとにこのメソッドを再定義していた

変更後

  • __class_attr__name_owner
    • 「どのクラスが現在の値のオーナーか」を返すメソッド
    • 値そのものではなくクラスオブジェクトを返す
    • クラス階層でオーナーが変わる都度、このメソッドだけを再定義
  • attr_reader :__class_attr__name
    • 実際の値はインスタンス変数 + attr_reader で管理
    • こちらは再定義不要

Ruby のクラスオブジェクトは Ractor-shareable なので、

  • __class_attr__name_owner を返す Proc / メソッドは Ractor で共有可能
  • 実際の値へは、そのクラスのインスタンス変数を通してアクセスすることで、
    • 値が shareable なら全 Ractor から参照可能
    • shareable でない場合は Ruby の通常の Ractor ivar セマンティクス (基本的にはメイン Ractor) に従う

という、Ruby 本体の Ractor セマンティクスに則った挙動になります。

イメージコード

概念的には、class_attribute :setting があると以下のような構造になります(擬似コード):

ruby
class Base
  # オーナークラスを返すメソッド(継承関係で変化)
  def self.__class_attr__setting_owner
    Base
  end

  # 実際の値を持つインスタンス変数への reader(再定義不要)
  class << self
    attr_reader :__class_attr__setting
  end

  # class_attribute :setting の public API
  def self.setting
    __class_attr__setting_owner.__class_attr__setting
  end

  def self.setting=(value)
    @__class_attr__setting = value
    # owner の付け替えなどは内部でやる
  end
end
  • 継承先クラスで Sub.setting = :world のようにオーバーライドしたときは、
    • Sub.__class_attr__setting_owner のみ Sub 用に再定義
    • 実際の値は @__class_attr__setting を Sub クラス側に保持

この分割により、Ractor 間で共有されるのは「どのクラスがオーナーか」という情報だけになり、値自体を Proc/クロージャに閉じ込める必要がなくなっています。


2-3. instance_reader のバグ修正

PR 説明にある通り、以下のようなケースでバグがありました:

  • singleton class(class << obj; self; end のようなクラス)に対して class_attribute を定義すると、
    • instance_reader が常に定義されてしまっていた

想定としては:

ruby
class << some_object
  class_attribute :foo, instance_accessor: false
end

のようなケースではインスタンスリーダーは定義されるべきではありませんが、従来は 無条件に定義されていた 模様です。
この PR で、singleton class に対しても instance_reader の有無がオプションどおりに振る舞うように修正されています。


2-4. パフォーマンス

ベンチマークの結果:

  • クラスメソッド読み出し (Base.setting, Sub.setting, SubSub.setting)
    • おおむね 2~3% 程度の低下 (約 50M i/s → 約 49M i/s)
  • インスタンスメソッド経由での読み出し (obj_base.setting, obj_sub.setting)
    • むしろわずかに向上 (約 42.4M i/s → 約 42.8M i/s)

全体として 10% 未満の差に収まっており、実務的にはほぼ気にならないレベルのオーバーヘッドです。


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

    • ActiveSupport::ClassAttribute および core_ext/class/attribute を利用する全てのコードが対象
    • class_attribute, mattr_accessor 等を通じてクラス属性を使っているコードは、内部実装がこの新方式に切り替わります
  • 挙動の互換性

    • API レベルでは互換性が保たれており、通常の使い方で挙動が変わるケースは想定されていません
    • ただし、__class_attr__* 系の内部メソッドやインスタンス変数に直接触れている場合は挙動が変わっています
      • そのようなコードは本来非推奨な内部依存なので、今回の変更を機に見直した方が良いです
  • Ractor 利用時の注意

    • class attribute の値が Ractor-shareable でないオブジェクト(例: 通常の Array, Hash, String)の場合、
      • その値を別 Ractor から直接参照することはできません
      • これは Ruby の Ractor 仕様そのものであり、今回の変更でより明示的にその制約に沿うようになった形です
    • Ractor 間で読み取りたいクラス属性には、freeze されたオブジェクトや純データ構造(Ractor-shareable なオブジェクト)を使うのが安全です
      • 例: default: [1, 2, 3].freeze
  • singleton class + class_attribute を使っている場合

    • 従来「たまたま」インスタンスリーダーが定義されていて依存していた場合、今回のバグ修正でそのメソッドがなくなる可能性があります
    • そのようなコードがないかを確認し、必要であれば instance_accessor: true などを明示することを推奨します

  1. 参考情報 (あれば)

#57318 Add datalist to FormBuilder

マージ日: 2026/5/7 | 作成者: @tahsin352

  1. 概要 (1-2文で)
    FormBuilder に HTML の <datalist> 要素を生成する datalist メソッドが追加され、form_with / form_for からオートコンプリート付きテキスト入力を素直な記述で扱えるようになりました。text_field との連携用に id が自動生成され、list: オプションで簡単に紐付けられます。

  1. 変更内容の詳細

新しく追加された機能

ActionView::Helpers::FormOptionsHelperdatalist メソッドが追加され、FormBuilder から以下のように使えるようになりました。

erb
<%= form_with model: @post do |f| %>
  <%# text_field 側で list: に datalist の id を指定 %>
  <%= f.text_field :country, list: f.field_id(:country, :datalist) %>
  <%= f.datalist :country, ["Argentina", "Brazil", "Chile"] %>
<% end %>

上記は、次のような HTML を生成します。

html
<input list="post_country_datalist" type="text"
       name="post[country]" id="post_country" />

<datalist id="post_country_datalist">
  <option value="Argentina">Argentina</option>
  <option value="Brazil">Brazil</option>
  <option value="Chile">Chile</option>
</datalist>

ID の自動生成仕様

  • FormBuilder のオブジェクト名と属性名から、<datalist> 用の id が自動生成されます。
    • 例: form_with model: @post かつ :country の場合、id="post_country_datalist"
  • text_field 側は、list: オプションでこの id を参照する必要があります。
    • 推奨パターンは list: f.field_id(:country, :datalist) のように field_id を使う形です
      :datalist サフィックスを付けた ID を生成)。

実装面のポイント(推測される仕様)

PR で触れられている範囲から読み取れる点:

  • FormOptionsHelper に新メソッド datalist(method, options_array, **html_options) 相当が追加。
    • method から id を決定
    • 第二引数の配列から <option> 要素を列挙
  • テスト (form_options_helper_test.rb) が追加され、少なくとも以下を確認していると考えられます:
    • 自動生成される id のフォーマット
    • 渡した配列が <option>value と本文両方に反映される
    • フォームビルダを通した出力が期待どおりの HTML になる

CHANGELOG には「FormBuilder に datalist を追加した」旨のエントリが追記されています。


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

    • ActionView の FormBuilder API が拡張される「追加機能」なので、既存コードは基本的に非互換影響を受けません。
    • form_options_helper 周辺に手を入れているため、カスタム FormBuilder を作っている場合は、同名メソッドの競合にだけ注意が必要です。
  • 注意点 / 使い方上の落とし穴

    • <datalist> はあくまで「ブラウザの補完候補」であり、バリデーションや制約にはならない点は HTML の仕様どおりです。サーバ側での検証は別途必要です。
    • オートコンプリートを有効にするには 必ず text_field 側の list:<datalist>id を指定 する必要があります。
      • IDs の手入力はヒューマンエラーを生むので、PR で示されているように f.field_id(:country, :datalist) で生成するのが安全です。
    • モデル・属性名に応じた ID 生成ルール(object_name_attribute_datalist)に依存するため、field_id を使わずに自前で list: を書く場合は、このフォーマットを守る必要があります。

  1. 参考情報 (あれば)

#56807 Rails.cache.read: add support to new option delete: true

マージ日: 2026/5/7 | 作成者: @glaucocustodio

  1. 概要 (1-2文で)
    RedisCacheStoreRails.cache.readdelete: true オプションが追加され、Redis の GETDEL コマンドを使って「読み出しと削除」をアトミックに行えるようになりました。これにより「一度だけ読めればよい」ワンタイム読み取りパターンをレースコンディションなしで実装できます。

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

新オプション delete: true

対象: RedisCacheStore を使っている場合の Rails.cache.read(内部的には read_serialized_entry

ruby
# 従来
value = Rails.cache.read("one_time_token")
Rails.cache.delete("one_time_token") # ここにレースコンディションの可能性

# 今回の変更後
value = Rails.cache.read("one_time_token", delete: true)
# Redis の GETDEL で value を返しつつ key を同時に削除
  • read_serialized_entry(key, **options)delete: キーワード引数を受け取るようになった。
  • delete: true のとき:
    • Redis クライアント呼び出しが c.get(key)c.getdel(key) に切り替わる。
    • 1回の Redis コマンドで「値の取得」と「キー削除」を実行。
  • delete: false(デフォルト)もしくは未指定の場合の挙動は従来どおり GET

ローカルキャッシュ層 (local_cache strategy) の変更

RedisCacheStorelocal_cache strategy と組み合わせて使うことができます(Rails.cache.fetch 時のプロセスメモリ内キャッシュなど)。

今回の変更では delete: true 指定時のローカルキャッシュとの整合性を担保するために、以下のような挙動になります。

  • delete: true の場合:
    1. ローカルキャッシュをバイパスして、必ず Redis に対して GETDEL を実行する
      → ローカルに古い値が残っていても、それを返さない。
    2. 対象キーのローカルキャッシュエントリを削除
      → Redis 側で削除された状態と揃える。

これにより、「プロセス内キャッシュにだけ値が残り続ける」「GETDEL で消えたはずなのにローカルから読める」といった不整合を防いでいます。

テストでカバーされているケース

activesupport/test/cache/stores/redis_cache_store_test.rb で以下が確認されています。

  • 基本的な「読み取り + 削除」動作
    • read(..., delete: true) で値が返り、その後同じキーを読み取ると nil になる。
  • 存在しないキー
    • delete: true でも単に nil が返る(余計なエラーは出ない)。
  • raw: true な値の取り扱い
  • 期限切れ(expired)エントリの挙動
  • Redis 側でキーが実際に削除されていることの確認
  • ローカルキャッシュとの連携
    • ローカルキャッシュが登録されていても delete: true 時は Redis を必ず見ること
    • 読み取り + 削除後にローカルキャッシュ側のエントリも消えていること

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

    • delete: true の実質的な意味を持つのは RedisCacheStore のみ
    • 他のストアはこのオプションを受け取っても 無視 される(**options 経由で素通りする設計)。
  • Redis バージョン要件:

    • 内部で使用する GETDEL は Redis 6.2+ で導入されたコマンド。
    • それより古い Redis を使っている場合:
      • 実運用では GETDEL が存在せずエラーになる可能性があるため、Redis バージョンに注意が必要。
      • PR本文でも「古いバージョンをケアすべきか?」という問いがあり、少なくともドキュメント/運用面での周知が前提になりそう。
  • 互換性(BC):

    • delete オプションは新規追加で、デフォルトは false
    • 既存コード(delete: true を指定していないもの)の挙動は変わらない。
    • local_cache strategy の挙動のうち、delete: true のケースのみが新たに定義されているため、既存の read / fetch には影響しない。
  • ユースケース:

    • ワンタイムトークン・一度きりのジョブペイロード・一回だけ表示するメッセージなど、 「確実に一度しか読まれない」ことが重要なデータの取り扱いに向く。
    • これまで readdelete の2ステップで起きていたレースコンディション(他プロセスが同じ値を二重に読む可能性)を防げる。

  1. 参考情報 (あれば)
  • Redis GETDEL コマンド仕様:
    https://redis.io/docs/latest/commands/getdel/
  • 追加されたオプションは RedisCacheStore 固有機能のため、マルチストア構成や抽象レイヤとして Rails.cache を利用している場合は、裏側のストアによって挙動が変わることに留意してください。

#57310 fix: has_json accessors after requiring active_model

マージ日: 2026/5/7 | 作成者: @tellodaniel

  1. 概要 (1-2文で)
    ActiveModel::SchematizedJson が内部で String#remove を使っているにもかかわらず、対応する Active Support の拡張を require していなかった問題を修正した PRです。has_json アクセサを使うと NoMethodError が出るケースを、依存関係を明示的に require することで解消しています。

  1. 変更内容の詳細

問題点

  • ActiveModel::SchematizedJson 内部で String#remove を呼び出している。
  • String#removeactive_support/core_ext/string/filters で定義される Active Support のコア拡張メソッド。
  • しかし active_model/schematized_json からこのファイルを require しておらず、
    「他のどこかでたまたま Active Support の String 拡張が読み込まれている」ことを前提に動いていた。
  • そのため、純粋に Active Model だけを読み込んで has_json を使うと、String#remove が定義されておらず NoMethodError が発生する。

再現コード(PR説明より)

bash
bundle exec ruby -Iactivesupport/lib -Iactivemodel/lib -e '
  require "active_model";

  account = Class.new {
    include ActiveModel::Attributes
    include ActiveModel::SchematizedJson

    attribute :settings
    has_json :settings, restricts_access: true
  }.new

  p account.settings.restricts_access
'

上記のように、Active Record などをロードせず、ActiveModel だけで動かそうとしたときに落ちる状況です。

修正内容
変更ファイル:

diff
activemodel/lib/active_model/schematized_json.rb
+ require "active_support/core_ext/string/filters"

実質的にはこの 1 行だけの変更で、ActiveModel::SchematizedJson が自前で String#remove を提供する Active Support の拡張を明示的に読み込むようになりました。

これにより:

  • ActiveModel::SchematizedJson を単体で使う
  • has_json アクセサを呼ぶ

という最小構成でも、NoMethodError が発生せずに正常に動作するようになります。


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

    • 直接の影響: ActiveModel::SchematizedJsonhas_json を使っているコード。
    • 特に、Active Record や Rails 全体をロードせず、Active Model 単体で使っているスクリプト/ライブラリで恩恵が大きいです。
    • 既に Rails フルスタックで動かしているアプリでは、もともと Active Support の String 拡張が読み込まれていることが多いため、挙動上の変化はほぼありません(バグが顕在化していなかったケース)。
  • 後方互換性

    • require が 1 行増えただけで、既存の API 変更や挙動変更はありません。
    • 既存コードが壊れるリスクは極めて低く、実質バグ修正のみです。
  • テスト

    • PR 説明では「スタンドアロンの Active Model スクリプト」と「Active Model のテストスイート」で動作確認済みとあります。
    • 追加テストは書かれていませんが、挙動としては依存の明示化のみなので、リスクは小さいと考えられます。

  1. 参考情報 (あれば)
  • 関連クラス/モジュール:
    • ActiveModel::SchematizedJson
    • ActiveModel::Attributes
    • active_support/core_ext/string/filtersString#remove など String のフィルタ系拡張を提供)
  • この PR は「暗黙的な Active Support 依存を顕在化して、Active Model を単体利用しやすくする」という性質の修正であり、今後も同様の暗黙依存を減らしていく方向性が示唆されます。

#57312 Fix changelog url

マージ日: 2026/5/7 | 作成者: @morgoth

  1. 概要 (1-2文で)
    このPRは、Action View の CHANGELOG に記載されていた URL の誤った変更を元に戻し、正しいリンク先に修正するものです。コード本体の挙動には影響せず、ドキュメント上のリンク整備のみを行っています。

  2. 変更内容の詳細

  • 対象ファイル: actionview/CHANGELOG.md
  • 変更点: 1行分の URL を「意図せず変更されてしまった状態」から元の正しい URL に差し戻しています。
  • 具体的には、以前の PR(#52137)で誤って変更された changelog 内のリンクが、今回の PR で正しい URL に修正されています。

サンプル(イメージ):

diff
- See the changelog at https://example.com/wrong/path
+ See the changelog at https://example.com/correct/path

※ 実際の URL は PR 本体を参照してください。

  1. 影響範囲・注意点
  • ランタイムの挙動・ビルド結果・API 仕様などには一切影響しません。
  • 影響範囲はドキュメント(actionview/CHANGELOG.md)内のリンク参照のみです。
  • CHANGELOG から関連情報や diff を辿る際に、正しい URL にアクセスできるようになります。
  • アプリケーション側で特別な対応は不要です。
  1. 参考情報 (あれば)

#57311 Accept Active Record Encryption credentials as ENV

マージ日: 2026/5/7 | 作成者: @claudiob

  1. 概要 (1-2文で)
    Active Record Encryption で必要な primary_key, deterministic_key, key_derivation_salt を、Rails.apps.creds に加えて環境変数からも設定できるようになりました。これにより、Rails の資格情報ファイルを使わずに、12-factor 的な運用(環境変数ベース)で暗号化キーを扱いやすくなります。

  1. 変更内容の詳細

新たにサポートされた環境変数

ActiveRecord::Encryption が参照するキーを、以下の環境変数で与えられるようになりました:

  • ACTIVE_RECORD_ENCRYPTION__PRIMARY_KEY
  • ACTIVE_RECORD_ENCRYPTION__DETERMINISTIC_KEY
  • ACTIVE_RECORD_ENCRYPTION__KEY_DERIVATION_SALT

これらは、もともと config.active_record.encryption.primary_key などで設定していた値と同等の役割を持ちますが、Rails 資格情報 (config/credentials.yml.enc など) や Rails.apps.creds に加えて、ENV 経由でも指定できるようになった、という位置づけです。

Rails.apps.creds との連携

この PR は、先行して導入された Rails.apps.creds (#56455) を前提にしています。
Rails.apps.creds は、アプリケーションの「資格情報」的な値を一元的に扱う API で、この PR ではそこから Active Record Encryption のキーを解決できるようにしたうえで、「同じ値を ENV からも渡せる」ようにした、という流れです。

内部的には、ActiveRecord::Railtie 内で暗号化設定を初期化する処理が変更されており、設定値の解決順序に「ENV による上書き」が組み込まれています(差分は +3/-3 行程度の軽微な変更)。

ガイドの更新

guides/source/active_record_encryption.md では、暗号化キーの設定方法の説明が更新され、Rails.apps.creds と環境変数の利用パターンが反映されています。
以前は「credentials 経由でこう書く」といった説明が中心だったものが、Rails.apps.creds + ENV という新しい標準的な書き方を前提に整理されています(行数としては削除 9 行 / 追加 3 行程度の軽微なドキュメント修正)。

テストの追加

  • railties/test/application/creds_test.rb
  • railties/test/application/envs_test.rb

にそれぞれ 1〜2 行ほどテストが追加され、Rails アプリケーション起動時に ENV 経由で Active Record Encryption のキーが正しく認識されることが検証されています。


  1. 影響範囲・注意点
  • 環境変数でキーを渡せるようになった
    Docker / Kubernetes / Heroku など、環境変数ベースでシークレットを注入する運用にフィットします。これまで credentials でしか管理していなかったキーを、必要に応じてシークレットマネージャ + ENV という構成に移行しやすくなります。

  • 優先順位・競合への注意
    実装詳細は PR 本体を確認する必要がありますが、一般的には「明示的設定 > creds > ENV」といった優先順位になることが多く、

    • config.active_record.encryption.primary_key を直接設定
    • Rails.apps.creds で設定
    • 上記に加えて ENV も設定
      といった状況では、どれが最終的に使われるかを意識する必要があります。特に移行期間中「credentials にも ENV にも同じキーを書いている」状態だと、将来の変更時にどちらが効いているか分かりにくくなるので、できるだけ片方に寄せるのが無難です。
  • キーの取り扱いは依然として慎重に
    暗号化キーを ENV に出せるようになったとはいえ、

    • シェル履歴に残さない
    • CI ログ/アプリログに出さない
    • 本番と検証環境でのキーの分離
      といったセキュリティ上の配慮は引き続き必要です。特に PRIMARY_KEY などを単一キーで複数環境に使い回すと、漏洩時の影響範囲が広くなります。

  1. 参考情報 (あれば)

#52137 Add datalist_tag to create datalist form elements

マージ日: 2026/5/6 | 作成者: @willianveiga

  1. 概要 (1-2文で)
    Rails の ActionView::Helpers::FormTagHelper に、HTML の <datalist> 要素を簡潔に生成するための datalist_tag ヘルパーが追加されました。これにより <option> の配列から datalist を組み立てる処理を、素の content_tag 連打なしで記述できます。

  1. 変更内容の詳細

新ヘルパー: datalist_tag

ActionView::Helpers::FormTagHelperdatalist_tag が追加されています。
基本的な呼び出しイメージは PR に記載の通りです。

ruby
datalist_tag(
  'countries_datalist',
  [
    'Argentina',
    ['Brazil', { class: 'brazilian_option' }],
    ['Chile', 'CL', { disabled: true }]
  ],
  { class: 'sa-countries-sample' }
)

# => <datalist id="countries_datalist" class="sa-countries-sample">
#      <option value="Argentina">Argentina</option>
#      <option value="Brazil" class="brazilian_option">Brazil</option>
#      <option value="CL" disabled="disabled">Chile</option>
#    </datalist>

引数の構造(推定仕様)

※テストコードから読み取れる典型的な Rails のヘルパー設計に基づく説明です。

  • 第1引数: id(または全体の HTML オプションに流し込まれる識別子)

    • 上記例では "countries_datalist"<datalist id="countries_datalist"> となる。
  • 第2引数: <option> を表す配列

    • 各要素は以下のいずれかの形式:
      1. "Argentina" 形式(文字列のみ)
        • value と表示テキストが同じになる:
          html
          <option value="Argentina">Argentina</option>
      2. ['Brazil', { class: 'brazilian_option' }] 形式
        • 第1要素 … value 兼 表示テキスト
        • 第2要素 … HTML オプション(class, data-*, disabled など)
      3. ['Chile', 'CL', { disabled: true }] 形式
        • 第1要素 … 表示テキスト
        • 第2要素 … value 属性の値
        • 第3要素 … HTML オプション
  • 第3引数: <datalist> 自体に付与する HTML オプション

    • 例: { class: 'sa-countries-sample' }<datalist class="sa-countries-sample">

従来の記述との比較

これまで <datalist> を Rails で生成するには、以下のように content_tag をネストする必要がありました:

ruby
<%= content_tag(:datalist, nil, id: :payees_datalist, 'data-autocomplete-input-target' => :datalist) do %>
  <% @payees.each do |payee| %>
    <%= content_tag(
      :option,
      nil,
      value: payee.name,
      data: { id: payee.id },
      'data-autocomplete-input-target' => 'datalistOption'
    ) %>
  <% end %>
<% end %>

datalist_tag を使うと、概ね次のようにシンプルな記述で済ませられます(イメージ):

ruby
<%= datalist_tag(
      :payees_datalist,
      @payees.map { |p| [p.name, { data: { id: p.id }, 'data-autocomplete-input-target': 'datalistOption' }] },
      'data-autocomplete-input-target': :datalist
    )
%>

実装まわり

  • 追加ファイル・変更:
    • actionview/lib/action_view/helpers/form_tag_helper.rb
      • datalist_tag メソッドが追加。
      • 内部で content_tag(:datalist, ...)content_tag(:option, ...) を組み合わせて出力していると考えられます。
    • actionview/test/template/form_tag_helper_test.rb
      • datalist_tag に対するテストが追加され、引数バリエーション・HTML オプション付与などがカバーされている。
    • actionview/CHANGELOG.md
      • 新機能として datalist_tag が追記。

  1. 影響範囲・注意点
  • 互換性:
    • 既存アプリへの破壊的変更はなく、新規ヘルパーの追加のみ。
    • 既存のフォームヘルパーには影響しない。
  • 利用上の注意:
    • options 配列の各要素の書き方(1要素/2要素/3要素)で、 「text と value のどちらを何にするか」が変わる点に注意。
      • "foo"<option value="foo">foo</option>
      • ["foo", "bar"]<option value="bar">foo</option>(である可能性が高い)
      • ["foo", { ... }]<option value="foo" ...>foo</option>
    • 既存の options_for_select などと似たインターフェイスになるはずなので、混同しないように(どちらも <option> を作るが、datalist_tag<datalist> 向けに最適化されている)。
  • ブラウザ側仕様:
    • <datalist><input list="..."> と組み合わせて使う HTML5 機能。
    • 対応ブラウザでのみ補完候補として機能するため、レガシーブラウザを対象にする場合は別途フォールバック(JS オートコンプリートなど)が必要。

  1. 参考情報 (あれば)

datalist を多用するオートコンプリート UI を Rails で作る場合、標準ヘルパーとして datalist_tag を使えるようになったことで、ビューコードの可読性と一貫性が向上します。


#49368 Action View: Document TagBuilder as part of the public interface

マージ日: 2026/5/6 | 作成者: @seanpdoyle

  1. 概要 (1-2文で)
    ActionView の tag ヘルパで内部的に使われている ActionView::Helpers::TagHelper::TagBuilder を「公式な公開API」として位置づけ直し、APIドキュメント検索からも辿れるようにした PR です。あわせて TagBuilder の責務を整理し、一部のメソッドを @view_context に委譲するようにして公開インターフェイスを絞り込んでいます。

  1. 変更内容の詳細

2-1. TagBuilder を公開APIとして明示

  • これまで TagBuilder クラス自体に :nodoc: が付いており、「内部実装」扱いで公式ドキュメントに出てきませんでした。
  • しかし実際には tag ヘルパの説明の中で
    • tag.attributes
    • tag.div, tag.h1 などの動的メソッド
      が言及されており、利用が事実上想定されているクラスでした。
  • そこで今回:
    • クラス自体は「Public API」としてドキュメント上可視にする
    • ただし、全メソッドをベタに公開するとインターフェイスが広すぎるため、メソッドには :nodoc: を付けてドキュメント上の露出を抑える という折衷案が取られています。

2-2. RubyDoc コメントを追加して検索可能に

目的は「ユーザが API ドキュメント上で tag.attributes などを検索した時に、きちんとヒットさせること」です。

そのために:

  • #attributes メソッドに RubyDoc コメントを追加
  • tag.div, tag.a など、method_missing 経由で生成されるタグビルダーメソッドについても、RubyDoc 上で説明できるようなコメントを追加

これにより:

  • tag.attributes で属性ハッシュを HTML 属性文字列へ変換する方法
  • tag.div, tag.span, tag.a などの使い方

が、Rails API ドキュメント上で検索しやすくなります。

想定される利用イメージ

ruby
# attributes の利用例
tag_builder = view_context.tag
tag_builder.attributes(class: "btn", data: { controller: "modal" })
# => "class=\"btn\" data-controller=\"modal\""

# 動的タグメソッドの利用例
tag.div("Hello", class: "greeting")
# => "<div class=\"greeting\">Hello</div>"

tag.a("リンク", href: "/path", data: { turbo: false })
# => "<a href=\"/path\" data-turbo=\"false\">リンク</a>"

tag オブジェクトの実態が TagBuilder であり、そのメソッドや挙動がドキュメントから追いやすくなります。

2-3. CaptureHelper / OutputSafetyHelper をクラスから切り離し委譲

これまで TagBuilder は以下のモジュールを include していました:

  • ActionView::Helpers::CaptureHelper
  • ActionView::Helpers::OutputSafetyHelper

これにより TagBuilder 自身が capture 系・HTML セーフティ系のメソッドを多数持っている状態でしたが、これらは本来「ビューコンテキストの責務」であり、TagBuilder が直接の公開インターフェイスとして持つべきかは微妙でした。

今回の変更では:

  • TagBuilder から上記モジュールの include を削除
  • その代わりに、これらに相当する機能が必要な箇所は内部の @view_context に委譲するよう実装を変更

という整理が行われています。

イメージ

ruby
# 変更前(イメージ)
class TagBuilder
  include CaptureHelper
  include OutputSafetyHelper

  def some_helper(...)
    # self の capture, safe_join などを直接呼ぶ
  end
end

# 変更後(イメージ)
class TagBuilder
  def initialize(view_context)
    @view_context = view_context
  end

  def some_helper(...)
    # @view_context.capture, @view_context.safe_join などに委譲
  end
end

これにより:

  • TagBuilder の「表向きの」メソッド一覧は減り、API としての表面積が小さくなる
  • 実際の振る舞い(出力のキャプチャや HTML セーフティ)は、今までどおり view_context によって提供されるので、既存の動作は維持される

  1. 影響範囲・注意点

3-1. 公開APIとしての安定性向上

  • TagBuilder が公的に認められた API になることで、tag ヘルパとそのメソッドの仕様変更がより慎重になります。
  • アプリや gem で tag.attributestag.div などを明示的に使っている場合も、「内部実装に依存している」という不安はやや軽減されます。

3-2. TagBuilder に対するメタプログラミング依存は要注意

  • クラス自体は公開されますが、多くのメソッドが :nodoc: で非推奨レベルに近い扱い(少なくとも「積極的にはドキュメント化しない」)となっています。
  • もしアプリ側で
    • TagBuilder に対する include/prepend
    • TagBuilder の private/protected メソッドの利用
    • TagBuilderCaptureHelper/OutputSafetyHelper を直接 include していることへの依存 などをしている場合、今回のようなリファクタリングで壊れる可能性があります。
  • 公開APIとして期待できるのは主に:
    • tag オブジェクトとしての一般的なタグ生成メソッド (tag.div, tag.a, …)
    • tag.attributes などであり、それ以外への踏み込みは今後も破壊的変更リスクがあります。

3-3. 互換性

  • 外部から見える典型的な使い方(tag.div, tag.span, tag.attributes など)は挙動維持が前提でテストも追加・更新されています。
  • TagBuilder のインクルードモジュール構成に依存したコードがある場合は挙動確認が必要です。

  1. 参考情報 (あれば)

#56384 Add ActionController::Parameters#fetch_values for multi-key fetching

マージ日: 2026/5/6 | 作成者: @Saidbek

  1. 概要 (1-2文で)
    ActionController::Parameters に、複数キーを一度に必須パラメータとして取得できる #fetch_values が追加されました。Ruby の Hash#fetch_values に近いインターフェイスで、複数パラメータ取得時のコードを簡潔に書けるようになります。

  1. 変更内容の詳細

新メソッド: ActionController::Parameters#fetch_values

params.fetch_values(:key1, :key2, ...) という形で、複数のキーを一度に取得できます。

基本的な挙動は Ruby の Hash#fetch_values に倣いつつ、Rails のストロングパラメータ仕様 (fetch と同じく ActionController::ParameterMissing を出す) に準拠しています。

基本的な使い方

ruby
params = ActionController::Parameters.new(name: "Francesco", age: 22)

params.fetch_values(:name, :age)
# => ["Francesco", 22]

戻り値は配列で、指定したキー順に値が並びます。

パラメータが欠けている場合

指定したキーのいずれかが存在しない・空・無効な場合には、既存の fetch と同様に ActionController::ParameterMissing が発生します。

ruby
params = ActionController::Parameters.new(name: "Francesco", age: 22)

params.fetch_values(:name, :none)
# => ActionController::ParameterMissing:
#    param is missing or the value is empty or invalid: none

エラーメッセージも既存の params.fetch(:none) と同じ形式です。

ブロック付き呼び出し

Ruby の Hash#fetch_values 同様、ブロックを渡すと キーが見つからなかった場合のフォールバック値 を指定できます。
見つかったキーは通常どおりの値、見つからないキーはブロックの戻り値が使われます。

ruby
params = ActionController::Parameters.new(name: "Francesco", age: 22)

params.fetch_values(:name, :none) { |key| key }
# => ["Francesco", :none]

この例では :none キーが無いため、ブロックが呼ばれ、key シンボル自体が値として使われています。

実装上のポイント(推測される仕様)

  • 内部的には #fetch 相当のロジックをキーごとに適用し、結果を配列に積む形。
  • ブロックが渡されていない場合、どれか 1 つでも欠けていれば即 ParameterMissing を raise。
  • ブロックが渡されている場合は、欠けたキーごとにブロックを呼び、例外は出さない。

代表的な利用イメージ

コントローラ内での「複数の必須パラメータ」の取得が簡潔になります。

ruby
class UsersController < ApplicationController
  def create
    name, email, age = user_params.fetch_values(:name, :email, :age)
    # name, email, age を安全に利用できる
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, :age)
  end
end

フォールバックを与えたい場合:

ruby
name, nickname = user_params.fetch_values(:name, :nickname) { |key|
  key == :nickname ? "anonymous" : nil
}

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

    • ActionController::Parameters の public API が 1 つ増えただけで、既存メソッドの挙動変更はありません。
    • 既存コードには互換性の影響はほぼありません(名前の衝突がない限り後方互換)。
  • 注意点

    • 単一キーなら params.fetch(:key) との機能差はありませんが、複数キーを取る場合は #fetch_values の方が簡潔で、例外メッセージやバリデーション挙動も fetch と同等になるよう設計されています。
    • ブロックを渡した場合は 原則として ParameterMissing が出ない 想定なので、「存在していなければ例外で落としたい」というケースではブロックを渡さないこと。
    • permit 前のパラメータに対して fetch_values を使うと、Strong Parameters のフィルタリングが効かない点は従来どおりです。必須パラメータチェックとストロングパラメータの適用順序には注意してください。

  1. 参考情報 (あれば)
  • 元になった Ruby コアのメソッド:
  • 既存の類似 API:
    • ActionController::Parameters#fetch
    • ActionController::Parameters#require
    • ActionController::Parameters#permit

#57290 Fix image analyzer reporting wrong dimensions for mirrored EXIF orientations

マージ日: 2026/5/6 | 作成者: @bogdan

  1. 概要 (1-2文で)
    Active Storage の画像アナライザ(Vips / ImageMagick)が、EXIF のミラー(反転)系オリエンテーションを持つ画像に対して width/height を誤って入れ替えて記録していたバグを修正する PR です。90°/270° 回転を含む 4 種類の EXIF orientation(5, 6, 7, 8)の場合だけ幅・高さを入れ替えるようにロジックを修正しています。

  1. 変更内容の詳細

問題の背景・バグの内容

Active Storage では、画像のメタデータ解析時に rotated_image? というメソッドを使い、EXIF orientation に応じて「縦横を入れ替える必要があるか」を判定しています。この判定は Vips / ImageMagick 両方のアナライザで使われています。

本来、EXIF orientation のうち「90°または270°の回転を含むもの」だけが、見かけ上の向き(portrait/landscape)が入れ替わるため、widthheight をスワップする必要があります。
しかし従来コードでは、以下の2つの誤りがありました:

  • 誤ってスワップしていた:

    • 2: TopRight(水平反転のみ)
    • 4: BottomLeft(垂直反転のみ)
      → これらは回転なしの「反転のみ」なので、見かけ上の縦横は変わらず、本来はスワップしてはいけない。
  • 本来スワップすべきなのにスワップしていなかった:

    • 5: LeftTop(反転 + 270°回転)
    • 7: RightBottom(反転 + 90°回転)
      → これらは 90°/270° 回転を含むため、portrait/landscape が入れ替わるにもかかわらず、従来ロジックではスワップ対象に入っていなかった。

この結果、ミラー系の EXIF orientation を持つ画像について、blob.metadata 上の width/height が逆転して記録されるケースがありました。たとえば、実際は横長画像なのに width < height となって portrait だと誤認される、などです。

なお、このバグは メタデータ上の width/height のみに影響し、サムネイルや variant の生成(実際の画像の回転処理自体)は vips の auto_rot により正しく行われていたため、出力画像の見た目には影響しません。


具体的な修正内容

方針
rotated_image? が「スワップすべき orientation だけを true にする」ように整理し、90°/270°回転を含む 4 種類に限定しました:

  • 5 (Left-top) : Flip + rotate 270° CW
  • 6 (Right-top): Rotate 90° CW
  • 7 (Right-bottom): Flip + rotate 90° CW
  • 8 (Left-bottom): Rotate 270° CW

結果として、EXIF orientation ごとの挙動はこうなります:

EXIF変換内容width/height スワップ変更前変更後
2水平反転しない○(誤)×(正)
4垂直反転しない○(誤)×(正)
5反転 + 270°回転する×(誤)○(正)
690°回転する○(正)○(正)
7反転 + 90°回転する×(誤)○(正)
8270°回転する○(正)○(正)

コード上の変更 (概念的なイメージ)

Vips と ImageMagick 双方のアナライザで、rotated_image? の条件が修正されています。イメージとしては:

ruby
# 変更前(例:一部の mirrored も含めていた)
def rotated_image?(orientation)
  [2, 4, 6, 8].include?(orientation)
end

# 変更後(90°/270°回転を含む4つに限定)
def rotated_image?(orientation)
  [5, 6, 7, 8].include?(orientation)
end

※実際の実装とはメソッド名・定数名などが異なる場合がありますが、ロジックの意図は上記の通りです。

テスト・フィクスチャの追加

  • 新たに 2 種類のテスト用 JPEG を追加:
    • racecar_mirrored.jpg → EXIF orientation 2
    • racecar_mirrored_rotated.jpg → EXIF orientation 5
  • これらを使い、Vips / ImageMagick の両方のアナライザテストを追加:
    • orientation 2: 以前のコードだと width/height が誤って入れ替わる → 修正後は入れ替わらないことを検証
    • orientation 5: 以前のコードだと入れ替わらない → 修正後は入れ替わることを検証
  • これにより、未修正コードではテストが fail し、修正後に pass することが確認されます。

CHANGELOG の更新

  • activestorage/CHANGELOG.md に、このバグ修正についてのエントリが追加されています。
    → ライブラリ利用者がリリースノートから挙動変更を把握できるようになっています。

  1. 影響範囲・注意点
  • 影響するのは Active Storage の blob.metadata[:width] / [:height] の値のみです。
    • サムネイル/variant のピクセル自体はもともと正しく回転されており、この PR での変更はありません。
  • 過去にアップロード済みの blob については、既に保存されているメタデータは自動では修正されません
    • orientation 2, 4, 5, 7 を使った画像が大量にあり、そのメタデータを正したい場合は、任意で再解析処理を走らせる必要があります(例: ActiveStorage::Blob.find_each { |blob| blob.analyze if blob.image? } など、環境に応じて)。
  • これに依存するアプリ側のロジック(例: width > height かどうかでレイアウトを変えるなど)は、
    • 以前は誤った判定をしていた可能性があり、
    • この修正を含む Rails へのアップデート後は正しい判定になるため、挙動が変わる可能性があります
  • この変更はあくまでバグ修正であり、新しい API 追加や破壊的なインターフェース変更はありません。
    • ただし「以前のバグ依存の挙動」を前提にしていたコードがある場合は注意が必要です。

  1. 参考情報 (あれば)
  • 関連 Issue/PR:
    • #41940: EXIF orientation 周りの以前の議論/不具合
    • #42005: mirrored orientation を扱うようにしようとして今回のバグを導入してしまった PR
  • EXIF orientation 一覧(一般的な資料):
    • 1: Normal
    • 2: Mirror horizontal
    • 3: Rotate 180°
    • 4: Mirror vertical
    • 5: Mirror horizontal + rotate 270° CW
    • 6: Rotate 90° CW
    • 7: Mirror horizontal + rotate 90° CW
    • 8: Rotate 270° CW
      → このうち、画面上で縦横が入れ替わるのは 5, 6, 7, 8 の 4 種類であり、今回の修正はこの定義に合わせたものです。

#57271 Gracefully handle nil during multi-parameter assignment

マージ日: 2026/5/5 | 作成者: @seanpdoyle

  1. 概要 (1-2文で)
    マルチパラメータ属性(例: Date カラム)の代入時に、これまで "" のときだけ正常に「値なし」として扱われていた挙動を、nil に対しても例外なく同様に扱えるようにした修正です。nil を渡した際に発生していた NoMethodError: undefined method 'empty?' for nil を解消します。

  1. 変更内容の詳細

何が問題だったか

Rails のマルチパラメータ属性("last_read(1i)", "last_read(2i)", "last_read(3i)" のような形式のキー)に対して、値として空文字列 "" を渡すと「値なし」として扱われていました:

ruby
topic = Topic.where.not(last_read: nil).first
topic.attributes = {
  "last_read(1i)" => "",
  "last_read(2i)" => "",
  "last_read(3i)" => "",
}
topic.last_read # => nil

一方で、同じ状況で ""nil に置き換えると、内部実装で value.empty? のようなチェックをしていたため、NilClassempty? が実装されておらず NoMethodError が発生していました:

ruby
topic = Topic.where.not(last_read: nil).first
topic.attributes = {
  "last_read(1i)" => nil,
  "last_read(2i)" => nil,
  "last_read(3i)" => nil,
}
# => NoMethodError: undefined method `empty?' for nil:NilClass

何をどう直したか

activerecord/lib/active_record/attribute_assignment.rb のマルチパラメータ処理部分で、

  • これまで: value.empty? で「空」を判定していた
  • これから: value.blank? で判定する

という変更が行われました(実質 1 行の差し替え)。

blank? は Rails 拡張であり、

  • String#blank? は内部で empty? などを使って空文字・空白文字列を判定
  • NilClass#blank? も定義されていて true を返す

ため、"" だけでなく nil に対しても同じように「値なし」という扱いができます。

この修正後は次のように動作します:

ruby
topic = Topic.where.not(last_read: nil).first
topic.attributes = {
  "last_read(1i)" => nil,
  "last_read(2i)" => nil,
  "last_read(3i)" => nil,
}
topic.last_read # => nil (例外は発生しない)

テストとドキュメント

  • activerecord/test/cases/multiparameter_attributes_test.rb に、nil を渡したケースが正しく nil に設定され、例外が出ないことを確認するテストが追加されています。
  • activerecord/CHANGELOG.md に、この挙動変更(バグフィックス)が追記されています。

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

    • Active Record のマルチパラメータ属性("column(1i)", "column(2i)", "column(3i)" など)を使った代入ロジック全般が対象です。典型的にはフォームからの Date / Time / DateTime 入力のパラメータ処理で利用されます。
  • アプリケーションへの実質的な影響

    • これまで「nil を渡すと例外が出る」ために、ワークアラウンドとして nil"" に変換していた箇所があれば、その必要がなくなります。
    • "" を「値なし」として扱っていた挙動は維持されつつ、nil も同様に「値なし」として安全に扱われるようになります。
    • blank? を使うことで " "(空白のみの文字列)なども「空」とみなされる点は、従来の empty? との違いになり得ますが、フォーム入力的には通常期待される挙動です(※既存コードでも多くの場合 blank? を利用しているので、違和感は少ないはずです)。
  • 後方互換性の観点

    • これまで nil によって意図的に例外を発生させていた、というような特殊な利用をしていない限り、挙動は「エラーにならなくなる」という改善として受け取れます。
    • もしマルチパラメータ用の値に、" "(スペースのみ)を意図的に「非空」として扱いたいような非常にニッチなケースがあれば、今回の blank? への変更で「空」と見なされるようになる可能性があります。

  1. 参考情報 (あれば)

#50889 Introduce ActiveSupport::TestCase.around

マージ日: 2026/5/5 | 作成者: @seanpdoyle

  1. 概要 (1-2文で)
    Rails の ActiveSupport::TestCasearound コールバック(フック)が追加され、各テストの setupteardown を1つの「囲い込み」処理として扱えるようになりました。これにより、RSpec などで馴染みのある「around フック」を Rails 標準のテストでもシンプルに使えるようになります。

  1. 変更内容の詳細

2-1. ActiveSupport::TestCase.around が追加

ActiveSupport::TestCasearound コールバックが導入され、以下の順序で実行される処理全体を 1 つのフックで囲めます。

  • setup
  • 実際のテストメソッド (test "..." do ... end)
  • teardown

PR の説明にある通り、ブロックには テストクラスのインスタンス (test_case) と実行ブロック (&block) が渡されます。

ruby
class MyTest < ActiveSupport::TestCase
  around do |test_case, &block|
    # 例: あるクライアントをスタブした状態でテストを実行
    Client.with(stubbed: true, &block)
  end

  def setup
    # 通常の setup
  end

  test "something" do
    # この中身が Client.with(stubbed: true) の中で実行される
  end

  def teardown
    # 通常の teardown
  end
end

上記の around ブロックは、内部的には

ruby
around do |test_case, &block|
  # setup → test → teardown を表す「ひとかたまりの block」が渡される
  Client.with(stubbed: true) do
    block.call
  end
end

というイメージで動作します。

2-2. 実装位置

実装は activesupport/lib/active_support/testing/setup_and_teardown.rb に追加されています。
ActiveSupport::TestCase が持つ既存の setup / teardown に続く、テスト用コールバック機構の一部として around が組み込まれました。

Rails には既に Controller / Job / Mailer などで利用されている Active Support のコールバックシステムがあるため、それと同じ仕組み・記法で around を利用できます。

2-3. テストとファイル構成の変更

  • 新規テスト: activesupport/test/testing/around_callback_test.rb
    around コールバックの挙動(順序・ネスト・複数定義など)がテストされています。
  • 既存の teardown 周りのテストファイル名を整理するため、after_teardown_test.rbcallbacks_test.rb にリネームされています(PR 説明にあるが、この diff ではリネームというより around 用テスト追加がメイン)。

2-4. 典型的な利用例

  1. 一時的なコンテキストや接続の差し替え:
ruby
class ApiClientTest < ActiveSupport::TestCase
  around do |test_case, &block|
    ApiClient.with_base_url("https://stubbed.example.com") do
      block.call
    end
  end
end
  1. グローバル設定の一時変更:
ruby
class FeatureFlagTest < ActiveSupport::TestCase
  around do |test_case, &block|
    old_value = Feature.enabled?(:new_ui)

    Feature.enable!(:new_ui)
    block.call
  ensure
    Feature.set!(:new_ui, old_value)
  end
end
  1. トランザクション・外部リソースのラップ(※DB は Rails が標準でトランザクションフィクスチャを持つので、別リソース向けなどに向いている):
ruby
class ExternalServiceTest < ActiveSupport::TestCase
  around do |test_case, &block|
    ExternalService.transaction do
      block.call
      raise ActiveRecord::Rollback
    end
  end
end

  1. 影響範囲・注意点
  • 対象: ActiveSupport::TestCase を継承する Rails 標準のテスト(モデルテスト、コントローラテスト、システムテストなど)。
    ただし、Minitest::Test を直接継承しているテストには影響しません。
  • 既存の setup / teardown との関係:
    • around は「setup + test本体 + teardown の外側」を囲むイメージです。
    • したがって、around 内で block.call を実行しないと、setup / テスト本体 / teardown が一切実行されません。
  • 複数の around の組み合わせ:
    • 複数の around を定義した場合、コールバック順序(LIFO / FIFO)やネストのされ方は Active Support の一般的なコールバックと同様です。
    • 内外の順番を意識しないと、想定と違う順序で環境のセットアップ・クリーンアップが行われる可能性があります。
  • minitest-hooks との競合:
    既に minitest-hooks を使って around を導入している場合、ActiveSupport::TestCase 側の around と競合する可能性があります。
    Rails 標準の around に移行する場合は、二重適用・メソッド名衝突などに注意してください。
  • 実行コスト・複雑さ:
    around はテスト全体をラップできるため便利な一方で、ロジックが重いものを入れるとテスト全体のパフォーマンス悪化や理解コスト上昇につながります。
    簡潔なスコープ制御・設定/復元処理に留めるのが望ましいです。

  1. 参考情報 (あれば)

#50320 Tag Builders: render keywords as dasherized HTML attributes

マージ日: 2026/5/5 | 作成者: @seanpdoyle

  1. 概要 (1-2文で)
    Rails の tag ヘルパー等で、属性にネストした Hash を渡したときに、キーを「ダッシュ区切り(dasherize)した HTML 属性名」として展開して出力できるようになりました。これにより、Stimulus/Turbo の data-* だけでなく、Alpine.js の x-* や htmx の hx-* 形式の属性も、Ruby 側で自然なハッシュ記法のまま生成しやすくなっています。

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

2-1. ネストした Hash を HTML 属性に展開する挙動の追加

これまでの Rails では、data: については data: { controller: "..." }data-controller="..." のような展開がありましたが、hx:x: など任意のプレフィックスに対して、かつ「任意にネストした Hash」を属性に落とし込むような標準サポートはありませんでした。

この PR により、tag ビルダー (例: tag.div, tag.button) で、次のようなコードが書けます:

ruby
tag.button "POST to /clicked",
  hx: { post: "/clicked", swap: :outerHTML, data: { json: true } }

出力は以下のようになります:

html
<button
  hx-post="/clicked"
  hx-swap="outerHTML"
  hx-data="{&quot;json&quot;:true}"
>
  POST to /clicked
</button>

ポイント:

  • トップレベルの hx: キーが「プレフィックス」として扱われる
  • その中の Hash のキーは dasherize されて、hx-post, hx-swap, hx-data という属性名になる
    • posthx-post
    • swaphx-swap
    • datahx-data
  • 値がさらにネストした Hash (data: { json: true }) の場合、適切に JSON へシリアライズされ、属性値としてエスケープされて格納される
    hx-data="{&quot;json&quot;:true}"

Stimulus/Turbo でおなじみの:

ruby
tag.div data: { controller: "users", action: "click->users#save" }
# => <div data-controller="users" data-action="click->users#save"></div>

というスタイルが、hx:x: にも自然に拡張できるイメージです。

2-2. 実装箇所

  • actionview/lib/action_view/helpers/tag_helper.rb
    • Tag ビルダーで、属性ハッシュを処理するロジックに、任意のネスト Hash を「ダッシュ区切り属性名」に展開する処理が追加。
    • 具体的には、hx: { foo_bar: ... } のような場合に hx-foo-bar="..." という属性に変換するような仕組み。

2-3. テストの追加

以下にテストが追加されています。

  • actionview/test/template/tag_helper_test.rb
    • tag ヘルパー単体で、ネスト Hash → 属性展開の挙動を確認するテストが追加。
  • actionview/test/template/form_helper_test.rb
  • actionview/test/template/form_helper/form_with_test.rb
    • form_tagform_with 経由でも同様の属性構築が期待通り動くことを確認するテストが追加。

2-4. CHANGELOG の更新

  • actionview/CHANGELOG.md
    • Action View に「タグビルダーがネストした Hash から dasherized な HTML 属性を生成できるようになった」という新機能として記載。

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

    • ActionView::Helpers::TagHelper を利用しているすべてのコード
      • tag.div, tag.button, form_with, form_for, form_tag 内で生成される各種タグの属性処理に影響します。
  • 後方互換性

    • 既存の data: { ... } の挙動はそのまま維持されつつ、より一般的なプレフィックス (hx:, x: 等) にも同様の変換ロジックが適用される形です。
    • 通常の単層 Hash (class: "btn", hx_post: "/..." など) は従来通り動作します。
      ネストさせたときにのみ、新しい展開ロジックが効きます。
  • 注意点・設計上のポイント

    • 「トップレベルのキー」(例: hx:) と「その値の Hash のキー」(例: post, swap, data) を組み合わせて属性名を構築する前提になっています。
    • ネストがさらに深くなる場合 (hx: { data: { json: { foo: :bar } } }) については、基本的に JSON シリアライズされるため、「プレフィックス + さらにダッシュでつなぐ」形ではなく「1 属性の JSON 値」として表現されます。この点は data: { foo: { bar: :baz } } と同じ考え方に沿うと考えられます。
    • Hash のキーは dasherize されるため、foo_barfoo-bar になります。JS 側の属性名と揃えるために、キー名・キャメルケース/スネークケースの扱いには注意してください。

  1. 参考情報 (あれば)
ruby
tag.div x: { data: { foo: :bar }, on: { click: "doSomething()" } }
# => <div x-data="{&quot;foo&quot;:&quot;bar&quot;}" x-on-click="doSomething()"></div>

このように、サーバーサイドで Ruby の Hash を自然に書きつつ、各種フロントエンドフレームワーク用の属性を柔軟に生成できるようになった、というのが本 PR の主旨です。


#57240 Move generated kamal gem into development group

マージ日: 2026/5/5 | 作成者: @jaykava

  1. 概要 (1-2文で)
    Rails のアプリケーション生成時に自動で追加される kamal gem を、トップレベルではなく group :development 内に移動する変更です。これにより、Kamal が本番環境の Bundle からデフォルトで外れ、開発時専用のデプロイツールとして扱われます。

  1. 変更内容の詳細

背景・動機

  • 現状の rails new では、生成される Gemfile に以下のような行がトップレベルに入ります:
    ruby
    gem "kamal", require: false
  • その結果、「デプロイ用ツールである Kamal」が、本番環境用の bundle install --without development test などをしていても、トップレベル依存として本番バンドルに含まれてしまう問題がありました。
  • この PR は「Kamal は開発時のデプロイツールであり、本番コードの実行に不要」という前提に沿って、Gemfile 上の位置づけを見直しています。

Gemfile テンプレートの変更

変更対象: railties/lib/rails/generators/rails/app/templates/Gemfile.tt

  • これまで:
    ruby
    # 例: トップレベルに kamal
    gem "kamal", require: false
  • 変更後: group :development 内に移動
    ruby
    group :development do
      gem "kamal", require: false
    end

実際には他の development 向け gem(web-console, listen など)と同じグループに Kamal が含まれる形になります。
これにより、RAILS_ENV=production bundle install などで development グループを除外した際、Kamal はインストール対象から外れます。

ジェネレータの挙動とテスト

変更対象テストファイル:

  • railties/test/generators/app_generator_test.rb
  • railties/test/generators/api_app_generator_test.rb

テストで担保しているポイント:

  1. 通常のアプリ生成 (rails new my_app)
    • 生成された Gemfile において、Kamal が group :development 内に含まれていることを確認。
  2. API モードのアプリ生成 (rails new my_app --api)
    • API アプリでも同様に Kamal が development グループに入っていることを確認。
    • 既存の API 用 Gemfile 形状(特に assets 周りを含む Kamal の設定)との整合性をテスト。
  3. --skip-dev-gems オプション
    • rails new my_app --skip-dev-gems では、development グループ用の gem が Gemfile に含まれないため、Kamal も Gemfile に現れないことを確認。
  4. --skip-kamal オプション
    • 既存オプションの挙動維持: rails new my_app --skip-kamal を指定した場合は、Kamal 自体が Gemfile に出現しないことを確認。

開発者向けに想定される生成結果イメージ:

ruby
# Gemfile (抜粋)

# 通常の gem 群
gem "rails", "~> 7.2.0"
gem "pg"
# ...

group :development do
  gem "kamal", require: false
  # その他 development 用 gem
end

  1. 影響範囲・注意点

影響範囲

  • 新しく rails new で生成するアプリケーションのみが対象です。
  • 既存アプリの Gemfile は自動で書き換えられませんが、同じ問題(Kamal がトップレベルにある)が気になる場合、自分で group :development に移動しても良いです。
  • 通常/ API モードいずれのアプリ生成でも、Kamal は「開発グループの gem」として扱われます。

注意点

  1. 本番環境で Kamal を実行したい場合
    • サーバ上でデプロイ作業を行う際に bundle exec kamal ... を使う運用を想定している場合、production 環境で bundle install --without development のようにしていると Kamal がインストールされません。
    • その場合:
      • デプロイ用の環境(ローカルCIサーバなど)だけは development グループも含めて bundle install する
      • もしくは「アプリ実行環境」と「Kamal 実行環境」を分ける(ローカル or CI で Kamal からリモートにデプロイする) などの運用設計が必要です。
  2. --skip-dev-gems / --skip-kamal の組み合わせ
    • --skip-dev-gems を付けると Kamal を含む全ての development グループ gem が除外されます。
    • --skip-kamal は Kamal 単体の除外用であり、development グループの他の gem には影響しません。
    • これらの挙動は引き続き既存通りであることがテストで担保されています。

  1. 参考情報 (あれば)
  • 対応 issue: #57205 — 「Kamal が本番 bundle に含まれてしまう」問題への対応 PR。
  • 実行されたテストコマンド:
    • 一般アプリのジェネレータテスト:
      bash
      bundle exec ruby -Irailties/test \
        railties/test/generators/app_generator_test.rb \
        -n "/kamal|skip_dev_gems/"
    • API アプリのジェネレータテスト:
      bash
      bundle exec ruby -Irailties/test \
        railties/test/generators/api_app_generator_test.rb \
        -n "/test_api_modified_files|test_kamal_deploy_yml_excludes_asset_path_for_api_apps/"
  • 変更ファイル:
    • Gemfile テンプレート: railties/lib/rails/generators/rails/app/templates/Gemfile.tt
    • ジェネレータテスト:
      • railties/test/generators/app_generator_test.rb
      • railties/test/generators/api_app_generator_test.rb

#57301 Document _came_from_user? and _for_database attribute method suffixes [ci skip]

マージ日: 2026/5/5 | 作成者: @joaomarcos96

  1. 概要 (1-2文で)
    Rails の ActiveRecord::AttributeMethods::BeforeTypeCast が内部的に提供している *_for_database*_came_from_user? という属性メソッドサフィックスについて、公式ドキュメントに追記した PR です。挙動の変更は一切なく、ドキュメントとコメントの整備のみです。

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

対象となるモジュール

ActiveRecord::AttributeMethods::BeforeTypeCast では、以下のように3つのサフィックス付き属性メソッドを定義しています:

ruby
attribute_method_suffix "_before_type_cast", "_for_database", parameters: false
attribute_method_suffix "_came_from_user?", parameters: false

従来は _before_type_cast だけが rdoc に説明されており、_for_database / _came_from_user? は、コードを読まない限りほぼ知られない状態でした。

追加されたドキュメントの要点

この PR では、モジュールレベルの rdoc に下記 2 つのサフィックスの説明と、Book モデル+enum を用いた簡単な例が追加されています。

*_for_database

  • 対象: book.status_for_database のように、属性名 + _for_database というメソッド
  • 役割:
    「その属性が実際に DB に保存される際の値」(=type キャスト・enum 変換などを適用した後の値)を取得するためのディスパッチ先
  • 元々存在していた API:
    • read_attribute_for_database(:status)
    • attributes_for_database からも利用できる機能だが、*_for_database というメソッド形式はドキュメント化されていなかった、という位置づけです。

イメージ例(enum を利用した Book モデル):

ruby
class Book < ApplicationRecord
  enum status: { draft: 0, published: 1 }
end

book = Book.new(status: :published)

book.status               # => "published" (enum のキー)
book.status_before_type_cast  # => "published" (元入力)
book.status_for_database      # => 1 (DB に保存される実際の整数値)

*_came_from_user?

  • 対象: book.status_came_from_user? のように、属性名 + _came_from_user? というメソッド
  • 役割:
    「その属性の現在の値が、ユーザー入力(属性代入)由来かどうか」を判定するためのディスパッチ先
  • これも内部的には以前から存在していましたが、コメントや rdoc に説明がなかったため、今回説明を追加しています。

イメージ例(フォーム入力などを想定):

ruby
book = Book.new

book.status_came_from_user?   # => false (まだ何も代入されていない)

book.status = 'published'

book.status_came_from_user?   # => true (ユーザー入力によってセットされた値)

※ 実際の判定ロジックは ActiveRecord の内部実装に依存しますが、概念としては「DB 読み込みやデフォルト値ではなく、アプリ側からの代入でセットされた値か」を知るためのフラグです。

コメントの追加

内部メソッドに、他の _before_type_cast と同様のコメントが追加されました。

  • attribute_for_database に:
ruby
# Dispatch target for *_for_database attribute methods.
  • attribute_came_from_user? に:
ruby
# Dispatch target for *_came_from_user? attribute methods.

これにより、*_for_database / *_came_from_user? がどのメソッドにディスパッチされるのか、コードリーディング時にも分かりやすくなっています。


  1. 影響範囲・注意点
  • 挙動変更なし
    既存の動作・API に一切変更はありません。
    *_for_database / *_came_from_user? メソッドは以前から利用可能であり、今回の PR はそれを公式に文書化しただけです。

  • ドキュメント依存のユーザーにとっての影響

    • これまで「知られざる内部 API」っぽかった status_for_database / status_came_from_user? などが、公式にサポートされている振る舞いとして分かりやすくなります。
    • enum・独自 type・暗号化 attribute などを扱うときに、
      • 「DB に実際に保存される値」を直接確認したい
      • 「これはユーザー入力起点の値かどうか」を判別したい
        といった場面で、これらのメソッドを安心して利用しやすくなります。
  • 将来の互換性の観点

    • ドキュメントに明示されたことで、「半ば内部実装」に見えていたこれらの方法が、よりパブリックな契約に近づいたと解釈できます。
    • gem やアプリケーションがこれらに依存しても、将来的に破壊的変更になりにくくなる、という意味でプラスです。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57301
  • 関連する API:
    • ActiveRecord::AttributeMethods::BeforeTypeCast
    • Model#attribute_before_type_cast
    • Model#read_attribute_for_database(name)
    • Model#attributes_for_database

#57129 Use Hash#each_key instead of Hash#keys.each

マージ日: 2026/5/5 | 作成者: @ashwin47

  1. 概要 (1-2文で)
    Rails内部で、Hash#keys.each を使っていた箇所の一部を Hash#each_key に置き換え、余計な配列生成を避けることでパフォーマンスとメモリ効率をわずかに改善するリファクタリングです。ハッシュを反復中に変更しない箇所のみを安全に置き換えており、挙動の変更はほぼありません。

  1. 変更内容の詳細

背景・意図

  • Hash#keys.each は以下のようなコードです:

    ruby
    hash.keys.each do |key|
      # ...
    end

    これは

    1. hash.keys で「全キーを配列にコピー」
    2. その配列に対して each でループ

    という2段階処理になり、中間配列の生成コスト(メモリ・GC)が発生します。

  • 一方、Hash#each_key は:

    ruby
    hash.each_key do |key|
      # ...
    end

    のように、ハッシュ内部構造を直接たどってキーを反復するため、中間配列を生成せずに済みます。

  • PR #17099 でも同種のクリーンアップが行われており、その続きとして今回のPRが入っています。

実際の変更点

PR本文では以下2ファイルと書かれていますが、今回の diff では3ファイルが変更対象として挙がっています(実際のコード差分は1行ずつの軽微な修正):

  • activemodel/lib/active_model/schematized_json.rb
  • activerecord/test/cases/connection_adapters/connection_handler_test.rb
  • activesupport/test/env_configuration_test.rb

いずれも本質的には同じパターンの置き換えです:

変更前(イメージ):

ruby
hash.keys.each do |key|
  # ハッシュはこのループ中に変更されない
end

変更後:

ruby
hash.each_key do |key|
  # ハッシュはこのループ中に変更されない
end

重要なのは、「ループ中に delete などでハッシュを破壊的に変更しない」箇所だけが置き換え対象になっている点です。

置き換え対象に していない 箇所

PR説明中では、以下の3箇所はそのまま keys.each が残されていると明記されています:

  • hash/keys.rb
  • strong_parameters.rb
  • routing/mapper.rb

これらではループ内で delete などによりハッシュを変更しているため、先に keys でスナップショット(配列コピー)を取ってから反復する必要があるケースです。このような場合に each_key に変えると、繰り返し中に対象ハッシュが変化してバグや予期せぬ動作を招く可能性があるため、意図的に残されています。


  1. 影響範囲・注意点
  • 影響範囲は内部実装のパフォーマンスに限られ、外部APIの仕様変更や互換性破壊はありません。

  • 変更対象となった箇所ではループ中にハッシュを変更しておらず、挙動は keys.each と論理的に等価です。

  • メモリ使用量がごくわずかに減り、GC負荷もわずかに軽くなる可能性がありますが、多くのアプリケーションでは体感できない程度の微小な改善です。

  • 自分のアプリ・ライブラリ側でも、以下のようなループは each_key / each_value / each_pair に置き換え可能か検討できます:

    ruby
    # 変更前
    hash.keys.each do |key|
      # ループ中に hash を変更しない
    end
    
    # 推奨パターン
    hash.each_key do |key|
      # ...
    end

    ただし、ループ中に hash.delete(key)hash[some_key] = ... 等で構造を変える場合は、従来どおり hash.keys.each でスナップショットを取る方が安全です。


  1. 参考情報 (あれば)

#55826 Add conflicting record ID to validates_uniqueness_of error details

マージ日: 2026/5/5 | 作成者: @bvicenzo

  1. 概要 (1-2文で)
    validates_uniqueness_of / validates :field, uniqueness: true の検証に失敗した場合、errors.details に「衝突している既存レコードの主キーID(existing_id)」が含まれるようになりました。これにより、どのレコードと一意制制約がぶつかったかをアプリ側で特定しやすくなります。

  1. 変更内容の詳細

2-1. 何が変わったか

ActiveRecord の ActiveRecord::Validations::UniquenessValidator の重複チェック実装が以下のように変更されています。

  • 以前:
    検証対象と同じ値を持つレコードが存在するかを .exists? で確認していた
    → 存在するかどうか (true/false) しか分からない

  • 変更後:
    .pick(klass.primary_key) を使って、衝突している既存レコードの主キー値を取得する
    → 存在する場合は、その ID を errors.details:existing_id として格納

具体的には、バリデーションが失敗したときのエラー詳細が次のように変わります。

変更前

ruby
class Topic < ApplicationRecord
  validates :title, uniqueness: true
end

topic = Topic.new(title: "Existing Topic")
topic.valid? # => false

topic.errors.details[:title]
# => [{ error: :taken, value: "Existing Topic" }]

変更後

ruby
topic.errors.details[:title]
# => [{ error: :taken, value: "Existing Topic", existing_id: 1 }]

existing_id には、衝突しているレコードの primary_key(通常は id)が入ります。
スコープ付き一意性 (validates :title, uniqueness: { scope: :user_id } など) に対しても同様に適用されます。

2-2. 実装上のポイント

  • DB問い合わせ部分で .exists?.pick(klass.primary_key) に変更。
    • どちらも LIMIT 1 で 1 行だけ取得するクエリになるため、パフォーマンス特性はほぼ同等。
    • .pick は該当行があれば主キーIDを返し、なければ nil を返す。
  • 衝突レコードが見つかった場合のみ、errors.details の要素ハッシュに existing_id: <主キー値> を追加。
  • I18n 関連・ユニークネス関連のテストがこの新しい振る舞いに合わせて更新されています。
  • activerecord/CHANGELOG.md に仕様変更として追記済み(公式にサポートされる振る舞い)。

2-3. 利用イメージ

JSON API で 409/422 エラーに「どのレコードと衝突したか」を載せたいケースなどで、そのまま使いやすくなります。

ruby
# 例: API レスポンス整形
record.valid?
if record.errors.added?(:email, :taken)
  existing_id = record.errors.details[:email].first[:existing_id]

  render json: {
    error: "email_taken",
    message: "Email is already taken by another user",
    conflict_user_id: existing_id,
    conflict_user_url: user_url(existing_id)
  }, status: :unprocessable_entity
end

  1. 影響範囲・注意点
  • errors.details のスキーマが変わる
    一意性バリデーションエラーの details 要素に :existing_id キーが増えます。
    既存コードで errors.details[:attr].first をそのままシリアライズしている場合、追加フィールドがクライアントに見えるようになります。

    • もし既存クライアントが「キーを厳密にチェックしている」場合は、API 仕様として許容するか、サーバ側でフィルタリングが必要な可能性があります。
  • セキュリティ / 情報漏えい面の検討

    • existing_id によって、既存レコードの ID 情報がクライアントに渡せるようになります。
    • 自分以外のリソースIDをユーザーに見せたくないケース(マルチテナントでID自体を秘匿したい、など)の場合は、レスポンス整形で existing_id を露出しないようにする必要があります。
    • 逆に「ID をキーにしたリンクを返したい」など、積極的に ID を利用したい API では便利になります。
  • パフォーマンス
    ベンチマーク(SQLite, Ruby 3.4.6, AR 8.1.0.beta1)では:

    • .exists? : 約 9.6k i/s
    • .pick(:id): 約 9.7k i/s
      と誤差範囲内の差で、実質的なパフォーマンス劣化はほぼ無しとみなせます。
  • 主キーがカスタムな場合

    • klass.primary_key を使用しているため、id 以外を主キーにしているモデルでも、その主キー値が existing_id として格納されます。
    • existing_primary_key ではなく existing_id というキー名である点は注意(意味的には「existing recordの主キー」)。

  1. 参考情報 (あれば)

#57247 Use ** in .dockerignore template for recursive directory exclusions

マージ日: 2026/5/5 | 作成者: @bogdan

  1. 概要 (1-2文で)
    Rails が rails new --docker などで生成する .dockerignore テンプレートを修正し、/log/*/tmp/* といった「1階層だけマッチするパターン」を /log/** のような「再帰的にすべてのサブディレクトリを含めてマッチするパターン」に変更した PR です。これにより、ログ・tmp・storage などの配下にある深い階層のファイルも Docker build context から確実に除外されるようになります。

  1. 変更内容の詳細

具体的な変更

.dockerignore テンプレート (railties/lib/rails/generators/rails/app/templates/dockerignore.tt) が以下のように変更されています。

パターンの変更

単一階層マッチ * → 再帰マッチ ** に変更:

diff
- /log/*
- /tmp/*
- /storage/*
- /app/assets/builds/*
+ /log/**
+ /tmp/**
+ /storage/**
+ /app/assets/builds/**

Docker の .dockerignore では:

  • dir/*dir 直下のファイル/ディレクトリのみマッチ
  • dir/**dir 配下のあらゆる階層のファイル/ディレクトリを再帰的にマッチ

となるため、** にすることで、深い階層のファイルも確実に build context から除外されます。

冗長なエントリの削除

もともと .dockerignore には、/tmp 配下を個別に除外する行がありましたが、/tmp/** を導入したことで冗長になったため削除されています。

diff
- /tmp/pids/**
- /tmp/storage/**

/tmp/**/tmp/pids/tmp/storage の中身もすべてカバーするためです。

.keep の例外指定を整理

tmp 以下に置かれた .keep ファイルを context に含めるための否定パターン(除外ルールの例外)をまとめ直しています:

dockerignore
/tmp/**
!/tmp/.keep
!/tmp/pids/.keep
!/tmp/storage/.keep
  • /tmp/** ですべてを除外
  • その後 ! プレフィックスで .keep ファイルだけを再度含める(.dockerignore の挙動: 後勝ち)

という形で、tmp 配下のディレクトリ構造を保ちつつ、中身の実データは送らないようにしています。


  1. 影響範囲・注意点

どこに影響するか

  • Rails が生成する .dockerignore が変わるため、今後 rails new で Docker 対応のアプリを作成したときのデフォルト挙動が変わります。
  • 既存プロジェクトには自動で適用されないので、恩恵を受けたい場合は自分で .dockerignore を更新する必要があります。

具体的に改善される点

旧パターンのままだと除外されていなかった代表例:

  • tmp/cache/assets/sprockets/v4.0.0/ab/abcdef1234...
    → Sprockets のアセットキャッシュが深くネストされており、/tmp/* では除外しきれなかったが、/tmp/** により除外される。
  • tmp/pids/server.pid
    → サーバ PID ファイルも tmp/pids/ 以下のため、旧パターンでは build context に含まれうる。
  • storage/ab/cd/abcdefghijklmn
    → Active Storage の Disk サービスはキーをプレフィックスで2階層に分けて保存するため、ユーザのアップロードファイルが Docker イメージの build context に含まれてしまう可能性があった。

これらがすべて .dockerignore により除外されるようになります。

結果として:

  • Docker build context が小さくなり、ビルド時間・アップロード時間(リモートビルドの場合)が短縮される。
  • 誤ってユーザーのアップロードファイルやキャッシュ・PID などを Docker イメージに含めてしまうリスクが下がる。

注意点

  • .dockerignore の挙動は .gitignore と完全に同じではありません。特に ** の再帰マッチは Docker 固有の挙動です。
    カスタマイズしている場合は、Docker ドキュメントを前提にパターンを確認してください。
  • 既存プロジェクトで手動マージする際:
    • すでに /tmp/** などを独自に書いている場合は重複しないように統合する。
    • /tmp 配下であえて含めたいファイル(例: 独自に何か生成しているもの)がある場合は、!/tmp/your_file のように例外指定が必要です。

  1. 参考情報 (あれば)

#57208 Add ActiveJob::Attributes to persist data between steps

マージ日: 2026/5/5 | 作成者: @bdewater-thatch

  1. 概要 (1-2文で)
    Rails 8.1で導入された Active Job Continuation(マルチステップジョブ)に対して、ステップ間で一時的なデータを安全に保持できる ActiveJob::Attributes が追加されました。Active Model Attributes をベースに、ジョブのシリアライズ/デシリアライズを意識せずに型付き属性を使って値を永続化できます。

  1. 変更内容の詳細

ActiveJob::Attributes の追加

  • 新モジュール ActiveJob::Attributes が追加されました。
  • 中身は基本的に ActiveModel::Attributes の API をそのまま使えるようにしたものです。
    • attribute DSL による宣言
    • 型付き属性 (:string, :integer, :datetime, 独自 type など)
    • デフォルト値、lazy default(Proc)、cast、serialize など Active Model Attributes の機能を利用可能

イメージとしては「Active Job 版 Active Model Attributes」で、ジョブ引数とは別に「ジョブの状態を持つための属性レイヤー」を提供するものです。

Continuable へのデフォルト組み込み

  • ActiveJob::Continuableinclude ActiveJob::Attributes が追加されています。
  • つまり、Continuation を使ったマルチステップジョブでは、特別な設定なしに attribute が使えます。
rb
class EnrollmentJob < ApplicationJob
  include ActiveJob::Continuable

  # ステップ間で保持したいデータ
  attribute :payment_token, :string
  attribute :user_metadata, :json

  step :tokenize_payment
  step :charge_payment

  def tokenize_payment(step)
    self.payment_token = PaymentGateway.tokenize(enrollment.user.payment_instrument)
    # この時点で payment_token はジョブのペイロードに埋め込まれる
    step.advance!
  end

  def charge_payment(step)
    # 再キューイング後でも payment_token が復元されている
    PaymentGateway.charge(token: payment_token)
    step.advance!
  end
end

上記のように、step の間で payment_token をインスタンス変数や外部テーブルに自前保存することなく、安全に保持できます。

既存案(step の戻り値を自動保存)を採用しなかった理由

PR本文で触れられているが、以下のような API は採用されていません:

rb
payment_token = step(:tokenize_payment_instrument, store: true) do
  PaymentGateway.tokenize(enrollment.user.payment_instrument)
end

この案を見送った理由:

  • attribute ベースの方がシンプルかつ明示的である
  • Continuable がカーソルを step.cursor / set! / advance! で明示的に扱っている現状の API と整合的
    (フレームワークが「勝手に戻り値を保存する」より、「属性として明示的に状態を持つ」方針に揃えている)

Active Model 側の微調整

  • activemodel/lib/active_model/attributes.rb にも 1 行変更があり、ActiveJob::Attributes からの利用に必要な小さな調整が入っています(主に include まわりの都合付けで、既存利用への影響は極小と考えられます)。

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

    • 新機能なので既存ジョブはそのまま動作します。
    • ActiveJob::Continuable を使うジョブでは、デフォルトで ActiveJob::Attributes が使えるようになります。
    • Continuable を使っていない通常の Active Job でも、include ActiveJob::Attributes すれば利用可能です。
  • 使いどころ

    • マルチステップ処理の「中間状態」を一時的に保持したいが、DB のカラム追加や専用テーブル作成まではしたくないケース。
    • 例:
      • 外部 API のトークン、セッション情報
      • ステップ 1 で計算した一時的な集計値
      • 後続ステップでのみ必要な、生存期間の短いデータ
  • ジョブ引数との違い

    • ジョブ引数は「ジョブ作成時に渡すもの」であり、通常は immutable な前提で扱われます。
    • ActiveJob::Attributes は「ジョブのライフサイクルの中で変化する“状態”」を持つための仕組みです。
    • ジョブを再キューイング・再開しても、これらの属性はシリアライズ/デシリアライズされて維持されます。
  • シリアライズサイズへの配慮

    • 属性はジョブのペイロードに載るため、扱うデータはできるだけコンパクトなものに限定するべきです。
    • 大きなオブジェクト・バイナリなどを保存すると、キューシステム(Sidekiq, Resque など)のメッセージサイズ制限に引っかかる可能性があります。
    • 「必要な識別子だけ属性に保存し、実データは再度取得する」ような設計が推奨されます。
  • 型・変換の注意

    • Active Model Attributes の型システムに依存するため、
      • カスタム type を使うなら、Active Model 側で type を定義してから利用する
      • JSON や Array、Hash などもサポートされるが、シリアライズ表現(JSON / String 化)に注意する
        といった点は既存の Active Model Attributes と同様です。

  1. 参考情報 (あれば)

この PR により、Active Job のマルチステップ処理で「一時的だが、再開時にも必要な状態」を持つための公式なパターンが用意され、従来の serialize/deserialize のオーバーライドや独自ストレージ実装が不要になることが期待されます。


#56450 Fix ActionMailer::Base.mail dispatch in 8.1+

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

  1. 概要 (1-2文で)
    Rails 8.1 で ActionMailer::Base.mail をクラスメソッドとして直接呼び出した際に ActionNotFound が発生するリグレッションを修正し、8.0 と同じ挙動(クラスレベル配送)を復元する PR です。mail が ActionMailer の「アクション」として常に認識されるようにし、その挙動を担保する回帰テストが追加されています。

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

問題の背景

Rails 8.1 以降で、以下のようなコードが動かなくなっていました:

ruby
ActionMailer::Base.mail(
  to: "user@example.com",
  from: "no-reply@example.com",
  subject: "Hello",
  body: "Hi"
).deliver_now

従来(〜Rails 8.0)はこれが許可されていましたが、8.1 では mail が「有効なメールアクション(action_methods)」として扱われなくなり、Action Mailer の内部ディスパッチで ActionNotFound が発生するようになっていました。

修正内容

1) ActionMailer::Base の action_methods に常に mail を含める

actionmailer/lib/action_mailer/base.rb に変更が入り、ベースクラス (ActionMailer::Base) を直接使う場合、mail が常にアクションとしてみなされるようになりました。

イメージとしては、Action Mailer が action_methods を決定するロジックに「ActionMailer::Base に関しては mail を特別扱いして常に含める」という処理が入った形です。

その結果、ActionMailer::Base.mail 呼び出し時も内部的には通常の Action Mailer のアクションディスパッチフローに乗り、ActionNotFound が起きなくなります。

2) 回帰テストの追加

actionmailer/test/base_test.rb に、以下のようなテストが追加されています:

  • ActionMailer::Base.mail を直接呼び出して deliver_now / deliver_later できること
  • その呼び出しが Action Mailer のアクションディスパッチを通って成功すること(=ActionNotFound にならないこと)

テストは PR 説明にある通り、以下で実行確認されています。

bash
bundle exec ruby -Iactionmailer/test actionmailer/test/base_test.rb -n "/ActionMailer::Base.mail/"

利用イメージ

この PR 適用後は、Rails 8.1+ でも以下のような「簡易ワンショットメール送信」が再び安全に使えます。

ruby
# メイラークラスを定義しなくても、ベースクラスだけで送信可能
ActionMailer::Base.mail(
  to: "user@example.com",
  from: "no-reply@example.com",
  subject: "Quick mail",
  body: "This is a quick one-off email."
).deliver_now

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

    • ActionMailer::Base.mail(...) をクラスメソッドとして直接呼んでいる既存コードに対して、Rails 8.1 で発生していた ActionNotFound が解消され、Rails 8.0 と同じ挙動になります。
    • 独自のメイラークラス(UserMailer < ApplicationMailer など)で、通常のアクション(def welcome_email ... end)を使っているケースには基本的に影響はありません。
  • 互換性

    • 明確に「8.0 の挙動を復元する」ことが目的のため、既存の 8.0 向けコードとの後方互換性を回復する変更です。
    • ActionMailer::Base を特殊な形で継承し、action_methods をカスタマイズしているような高度なメタプログラミングをしていない限り、副作用の可能性は低いと考えられます。
  • 注意点

    • PR 時点では CHANGELOG は更新されていないため、この挙動変更は一見分かりづらい可能性があります。Rails を 8.1 系に上げた際に ActionMailer::Base.mail 周辺が壊れた場合、この修正が入っているバージョンかを確認するとよいです。
    • ActionMailer::Base を直接使ったメール送信はあくまで「簡易用途」向けであり、大量のメール送信や複雑なロジックには従来通り独自のメイラークラスの定義を推奨します。

  1. 参考情報 (あれば)

#57288 Define as_json on ActiveStorage::Attached::One and Attached::Many

マージ日: 2026/5/5 | 作成者: @onyxblade

  1. 概要 (1-2文で)
    has_one_attached / has_many_attached の名前が実カラム(あるいは ignored_columns に入ったカラム)と衝突しているときに、record.to_jsonSystemStackError(無限再帰)が発生するバグを修正するPRです。ActiveStorage::Attached::One / Many に明示的な as_json を定義し、レコードの JSON シリアライズ時に安全に変換されるようにしました。

  1. 変更内容の詳細

問題の背景

以下のようなモデルがあるとします:

ruby
class Product < ApplicationRecord
  self.ignored_columns = [:photo]
  has_one_attached :photo
end

Product.select('*').first.to_json
# => SystemStackError: stack level too deep

ポイント:

  • DB 上には photo カラムが存在する。
  • Rails 側では self.ignored_columns = [:photo] で無視しつつ、同名で has_one_attached :photo を定義。
  • Product.select('*') により、無視されるはずの photo カラムも結果セットには含まれる。
  • その状態で record.to_json を呼ぶと、ActiveModel の JSON シリアライズ処理の途中で、Attached::One / Attached::Many のインスタンスが Object#as_json にフォールバック。
  • Object#as_jsoninstance_values(インスタンス変数をすべてハッシュ化)を返すため、その中に含まれる @record からまた as_json が呼ばれ…というサイクルが発生し、スタックオーバーフローになる。

スタックトレース上では、Attached::One / ManyObject#as_json を通ってしまうことが原因で、record へのバックリファレンスがシリアライズ対象になり無限再帰が発生していました。

対応内容

ActiveStorage::Attached::OneActiveStorage::Attached::Manyas_json を定義し、Object#as_json に落ちないようにする、というローカルな修正です。

Attached::One#as_json

  • 振る舞い:
    • 添付済みなら、そのアタッチメント(ActiveStorage::Attachment レコード)の as_json を返す。
    • 未添付なら nil を返す。

擬似コードイメージ:

ruby
# activestorage/lib/active_storage/attached/one.rb

def as_json(*)
  if attached?
    attachment.as_json
  else
    nil
  end
end

結果として、Product.first.to_json の中で photo が JSON 化されるときは、以下のような JSON に展開されます(概念的な例):

json
{
  "id": 1,
  "name": "Sample product",
  "photo": {
    "id": 10,
    "blob_id": 99,
    "record_id": 1,
    "record_type": "Product",
    "name": "photo",
    "created_at": "2026-05-05T12:34:56Z"
  }
}

Attached::Many#as_json

  • 振る舞い:
    • アタッチされている全ての ActiveStorage::Attachmentas_json を配列で返す。

擬似コードイメージ:

ruby
# activestorage/lib/active_storage/attached/many.rb

def as_json(*)
  attachments.map(&:as_json)
end

JSON は以下のような形になります(概念的な例):

json
{
  "id": 1,
  "name": "Sample product",
  "photos": [
    {
      "id": 10,
      "blob_id": 99,
      "record_id": 1,
      "record_type": "Product",
      "name": "photos",
      "created_at": "2026-05-05T12:34:56Z"
    },
    {
      "id": 11,
      "blob_id": 100,
      "record_id": 1,
      "record_type": "Product",
      "name": "photos",
      "created_at": "2026-05-05T12:35:56Z"
    }
  ]
}

なぜこれで安全なのか

  • ActiveStorage::Attachment は以下のようなスカラーカラムのみを持つシンプルなモデルです:
    • id, blob_id, name, record_id, record_type, created_at
  • これらのカラムには ActiveStorage::Attached::One / Many のような「レコード自身へのバックリファレンス」は含まれていないため、Attachment#as_json を呼び出しても再帰ループは発生しません。
  • そのため Attached::* インスタンスの JSON 化は、Attachment レコードの配列(または単一オブジェクト)に収束し、安全にシリアライズが完了します。

代替案として検討されたもの

PR説明で触れられている代替案:

  • ActiveRecord::Persistence#instantiate_instance_of の中で、ignored_columns に入っているカラムを row hash から取り除く。
    • これにより、select('*') を書いていても、ignored_columns に入っているカラムは attribute_names に現れないようにできる。
  • しかし、この方法は:
    • ユーザーが明示的に SQL で取得したカラムが「サイレントにドロップされる」挙動変更であり、影響範囲が広い。
    • ActiveStorage 以外にも「レコードをバックリファレンスする任意の Object サブクラス」には効かない。

そのため、より局所的で副作用の少ない「Attached::* プロキシに as_json を生やす」方針が選ばれています。

テスト・ドキュメント

  • activestorage/test/models/attached/one_test.rb
  • activestorage/test/models/attached/many_test.rb

にテストが追加され、今回の as_json の挙動が保証されています。

  • activestorage/CHANGELOG.md にも変更が記載され、挙動変更として明示されています。

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

    • Active Storage を利用しているアプリケーションのうち、
      • has_one_attached / has_many_attached の名前が実カラム名と衝突している、
      • または ignored_columns と組み合わせてカラムを無視しつつ、同名で attachment を定義している、
      • さらに select('*') や特定の select で無視したはずのカラムを結果に含めてしまう、 といったケースで record.to_json を呼んでいるプロジェクト。
  • この PR により、これらのケースで SystemStackError が発生しなくなります。

  • JSON 形式の変化:

    • これまで to_json を呼んでもエラーで落ちていたようなケースでは、今回初めて JSON が正常に返ることになります。
    • has_one_attached のフィールドは「単一の Attachment オブジェクト or nil」、has_many_attached は「Attachment オブジェクトの配列」として JSON に含まれるようになります。
    • もし to_json / as_json をオーバーライドして独自形式を返したい場合は、アプリ側で as_json を追加実装する必要があります。
  • Rails の他部分への影響:

    • 修正箇所は Active Storage の Attached::One / Many に限定されており、ActiveRecord の汎用的なインスタンス生成や ignored_columns の扱いには手を触れていません。
    • そのため、ActiveRecord 全体の挙動変更のリスクは小さいです。

  1. 参考情報 (あれば)
  • 対応する issue: #57287
    has_one_attached / has_many_attached が属性名をシャドウするときの SystemStackError」を扱うものです。
  • 対象クラス:
    • ActiveStorage::Attached::One
    • ActiveStorage::Attached::Many
    • ActiveStorage::Attachment
  • 類似の問題を避けるための指針:
    • カラム名と ActiveStorage の attachment 名を可能な限り衝突させない。
    • どうしても同名にする必要がある場合でも、今回の修正により少なくとも to_json 時の無限再帰は解消されます。

#57162 Skip CreateUsers migration when User model already exists

マージ日: 2026/5/5 | 作成者: @johntopley

  1. 概要 (1-2文で)
    Rails の rails generate authentication で、すでに User モデルが存在する場合に CreateUsers マイグレーションを自動生成しないようにし、「table already exists」エラーを避ける修正です。既存アプリに後から認証機能を追加しやすくなる変更です。

  1. 変更内容の詳細

2-1. 挙動の変更点

これまで:

  • rails generate authentication User などを実行すると、
    • 常に db/migrate/xxxxxx_create_users.rb のような CreateUsers マイグレーションを生成
    • 既に users テーブルがある(=既存の User モデル+マイグレーションがある)状態で db:migrate を実行すると、
      • 「table already exists」エラーで失敗

今回の変更後:

  • ジェネレータ実行前に app/models/user.rb が存在するかをチェック
  • 存在する場合は CreateUsers マイグレーションの生成をスキップ
  • そのため、既存の users テーブルがあるアプリに後から authentication generator を適用しても、テーブル作成マイグレーションが重複せずに動く

2-2. コードレベルでの変更点 (概要)

railties/lib/rails/generators/rails/authentication/authentication_generator.rb にて:

  • User モデルファイルの存在チェックロジックを追加
    • 例: File.exist?(Rails.root.join("app/models/user.rb")) のような条件を導入し、真の場合は CreateUsers マイグレーションのテンプレート生成を行わないようにしている(正確なメソッド名・条件はPR本文からの推測だが、実装方針はこれ)
  • 既存のマイグレーション生成処理の前後に条件分岐を挿入し、「Userモデルがないときだけ create_users マイグレーションを生成」するように変更

railties/test/generators/authentication_generator_test.rb では:

  • 新しい挙動に対するテストが追加
    • app/models/user.rb がある状態でジェネレータを実行したときに CreateUsers マイグレーションが生成されないことを検証
    • User モデルがない状態では従来どおりマイグレーションが生成されることも担保

railties/CHANGELOG.md:

  • 上記の仕様変更が CHANGELOG に追記され、振る舞いの変更として明文化されています。

  1. 影響範囲・注意点
  • 影響対象:
    • Rails 付属の authentication generator を利用しているプロジェクト
    • 既存アプリに後から rails generate authentication User を適用するケースで特に恩恵がある
  • 注意点:
    • 今回の判定は「app/models/user.rb が存在するかどうか」で行われているため、
      • テーブル名は users だがモデルファイルが別名/別ディレクトリにある、
      • あるいは User モデルがあってもファイル名が慣例どおりでない
        といった特殊な構成をしている場合は、期待どおりに判定されない可能性があります。
    • 逆に、app/models/user.rb だけ存在していて users テーブルがまだない場合は、マイグレーションが生成されずテーブルが作成されないので、そのような構成をとる場合は手動でマイグレーションを用意する必要があります。
  • バックワード互換性:
    • 一般的な Rails アプリ(User モデル=app/models/user.rb、テーブル=users)では、既存の挙動が「壊れる」ことは基本的になく、むしろ二重マイグレーションのエラーが防止される方向の変更です。

  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57162
  • 閉じられた Issue(動機となったバグ報告): #57149
    • 「既に User モデルと users テーブルが存在するアプリで authentication generator を使うと、db:migrate で ‘table already exists’ が発生する」という報告に対応した変更です。

#57298 Fix eager_load for CPK models with explicit select

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

  1. 概要 (1-2文で)
    複合主キー(CPK: Composite Primary Key)を持つモデルに対して、select を明示したクエリに eager_load を使うと正しく動かない不具合が修正されました。eager_load 時の主キー列の扱いを見直し、CPK モデルでも関連を正しくプリロードできるようになっています。

  1. 変更内容の詳細

※ パッチ自体は非常に小さく、join_dependency.rb の条件分岐を 1 行変更し、それに対応するテストを eager_test.rb に追加したものです。実際のコード断片はリポジトリを直接参照する必要がありますが、変更の意図と動きは次のように整理できます。

背景

  • ActiveRecord の eager_load は、関連を 1 クエリで取得するために LEFT OUTER JOIN を使います。
  • その際、関連付けの整合性をとるために、主キー列(id など)を必ず SELECT 句に含めるロジックがあります。
  • しかし、複合主キーを持つモデル(内部的には primary_key が複数列)で、かつ select を明示的に指定している場合、
    • 必要な複合主キーのカラムが SELECT に含まれない/含め方が不適切
    • → eager_load の JOIN 結果を元にレコードを組み立てる処理で不整合・エラーが生じる
      という問題 (#57296) がありました。

join_dependency.rb の修正

ActiveRecord::Associations::JoinDependency には、eager load 用クエリに含めるカラムを決定するロジックがあります。
今回の修正は:

  • 「CPK モデル + explicit select」というケースで
    • そのモデルの すべての主キーカラムを必ず SELECT に含める
  • という条件付けが正しく働くよう、条件式を微調整したものです。

イメージとしては、次のような動きをするようになったと考えてください(擬似コード):

ruby
# 擬似コードレベルのイメージ
if using_eager_load?
  # 以前は単一主キー前提 or 条件が甘く、CPK + select 時に不足
  columns_to_select += Array(klass.primary_key)  # CPK の場合は ["key1", "key2"] など
end

これにより、たとえば以下のようなケースが正しく動くようになります。

ruby
# モデル Post が複合主キー (year, code) を持っているとする
Post.select(:title)            # 明示的 select
    .eager_load(:comments)     # 関連を eager_load
    .to_a

以前は yearcode が SELECT 句に含まれず、eager_load が期待通りのレコード構築を行えないことがありましたが、修正後は内部的に主キー列も自動で SELECT に追加されます。

テストの追加

activerecord/test/cases/associations/eager_test.rb に 11 行のテストが追加されています。内容の趣旨は次の通りです。

  • 複合主キーを持つモデルを用意
  • select で一部カラムのみを指定しつつ eager_load を使う
  • 結果がエラーにならず、関連が正しく読み込まれることを検証

このテストが、回 regress しないことを保証する役割を持ちます。


  1. 影響範囲・注意点

影響範囲

  • 対象:
    • ActiveRecord で複合主キーを扱っているアプリケーション
    • かつ、そのモデルに対して select を明示しつつ eager_load を使っているコード
  • 具体的な影響:
    • 以前は
      • エラーが起きる
      • もしくは、関連が正しくロードされない/結果が不完全になる
    • といった挙動が出ていた箇所が、正常に動作するようになります。
    • 内部的には SELECT されるカラムに主キー列(複合主キーの全列)が追加されます。

注意点

  • 明示的に select を使っていて、「本当にそのカラムしか SELECT したくない」という前提でコードを書いていた場合でも、
    • eager_load を使う限り、内部的には主キー列が追加で SELECT されます。
    • これは単一主キーのときも同様の設計思想であり、CPK でもそれに揃えた形です。
  • そのため、
    • 「SELECT 対象カラムを厳密に制御してパフォーマンスを最適化したい」
    • 「SELECT されるカラム数を完全にコントロールしたい」 といったユースケースでは、eager_load ではなく includes + references / preload を使う設計を検討したほうがよい場合があります。

  1. 参考情報 (あれば)
  • 関連 Issue: #57296
  • 変更ファイル:
    • activerecord/lib/active_record/associations/join_dependency.rb
    • activerecord/test/cases/associations/eager_test.rb
  • キーワード:
    • Composite Primary Key (CPK)
    • eager_load
    • 明示的 select
    • ActiveRecord::Associations::JoinDependency

#57255 Update AGENTS.md to remove PR reference

マージ日: 2026/5/5 | 作成者: @nateberkopec

  1. 概要 (1-2文で)
    このPRは、AGENTS.md に含まれていた特定のプルリクエスト(PR)への言及を削除する、ドキュメントのみの微修正です。Rails本体の機能や挙動には一切変更がありません。

  1. 変更内容の詳細
  • 対象ファイル: AGENTS.md
  • 差分: 1行削除(追加行なし)

AGENTS.md は、Rails 内で利用される「エージェント」に関するドキュメント(例: User-Agent や各種クライアント識別、もしくは内部的な“agent”コンポーネントの解説)をまとめたファイルと考えられます。
今回削除されたのは、その中にあった「特定の PR 番号(あるいは PR への直接リンク)を参照する一文」です。

説明文にある:

Maybe I'm missing something, but this seems way too specific to be intentional.

というコメントから、この PR 参照は意図せず紛れ込んだか、あるいは履歴上のごく一時的な情報をドキュメントに固定してしまっていたため、「ドキュメントとしては不自然なほど具体的すぎる」ので削除した、という判断です。

擬似的なイメージとしては、以下のような行が削除されたと考えられます(実際の文面は異なる可能性があります):

diff
- See PR #12345 for more details about this behavior.

あるいは、もっと具体的なケース:

diff
- This behavior was introduced in PR #56789 and should only affect FooBar agents.

Rails のガイドライン的にも、ユーザ向けドキュメントに「開発時の個別 PR の履歴」を直接書き込むことはあまり望まれず、安定した概念・API・設定方法に焦点を当てるべきなので、その整理の一環と見てよいです。


  1. 影響範囲・注意点
  • 機能影響: なし
    • ソースコード・設定・API などへの変更は一切なく、あくまでドキュメント行削除のみです。
  • 互換性への影響: なし
    • アプリケーションコードを修正する必要も、Rails のバージョンアップにおける互換性問題も発生しません。
  • 開発者視点の注意点:
    • AGENTS.md を直接参照していた場合、その中にあった「特定の PR に飛んで詳細を見る」という導線がなくなります。
    • ただし、その情報は主に内部開発者・コントリビュータ向けの履歴情報であり、通常のアプリケーション開発者にとってはほぼ影響ありません。
    • 今後、ドキュメントに履歴的な情報を書く場合は「PR番号を直接書く」よりも、「その機能が何をするのか」「いつから利用可能か(バージョン)」といった抽象度の高い情報にとどめるのが望ましい、という方向性が改めて示されたとも言えます。

  1. 参考情報 (あれば)

この変更は「クリーンアップ系のドキュメント整理」と捉えてよく、Rails を利用する側としては特別な対応は不要です。


#57280 Fix ActionCable Overview JS example: localStorage.get → getItem

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

  1. 概要 (1–2文で)
    Action Cableガイド内の JavaScript サンプルコードで、localStorage の誤った呼び出し方 (get) を正しい getItem に修正したドキュメント変更です。ブラウザでコピペして動く形になるように、動的 WebSocket URL の例を実用的なコードに直しています。

  2. 変更内容の詳細

対象ファイル: guides/source/action_cable_overview.md の「Connect Consumer」セクション内 getWebSocketURL の例。

もともとのガイドでは (概念的には) こんなコードになっていました:

js
function getWebSocketURL() {
  const token = localStorage.get('auth-token') // ← これが誤り
  return `wss://example.com/cable?token=${token}`
}

この PR で以下のように修正されています:

js
function getWebSocketURL() {
  const token = localStorage.getItem('auth-token') // ← 正しい Web Storage API
  return `wss://example.com/cable?token=${token}`
}

技術的背景:

  • ブラウザの Web Storage API (Storage インターフェイス) は getItem(key: string): string | null を提供します。
  • localStorage.get というメソッドは存在しないため、サンプルをそのまま実行すると
    TypeError: localStorage.get is not a function が発生します。
  • これを getItem に直すことで、Action Cable の接続 URL を localStorage のトークンで動的に組み立てる例が、実際にブラウザで動くコードになります。
  1. 影響範囲・注意点
  • 影響範囲:

    • Rails 本体のランタイム動作には一切影響なし (ガイド文書のみの変更)。
    • Action Cable の「概要」ガイドを読みながら実装する開発者が、ブラウザ上でエラーに遭遇しなくなる。
  • 注意点:

    • すでにドキュメントを参考にして localStorage.get(...) を書いてしまっているコードがある場合は、手元の実装を getItem(...) に修正する必要があります。
    • getItem はキーが存在しない場合 null を返すため、本番コードでは null チェックや未ログイン時のハンドリングを追加することが推奨されます (例: トークンがなければ接続しない、ログイン画面へ誘導するなど)。
  1. 参考情報 (あれば)

#56209 Update Configurable deprecaton warning

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

  1. 概要 (1-2文で)
    Rails の ActiveSupport::Configurable を非推奨にする際に表示される警告文が、「class_attribute を使え」と単一の代替手段だけを示していたのをやめ、複数の代替案を提案する、より中立的な文言に変更した PR です。機能的な変更はなく、あくまで deprecation warning のメッセージ内容のみが更新されています。

  1. 変更内容の詳細

変更ファイルは 1 行のみで、ActiveSupport::Configurable 使用時に表示される非推奨警告文の文言が修正されています。

背景:

  • 以前の PR (ref: https://github.com/rails/rails/pull/53970) で ActiveSupport::Configurable が非推奨になり、その際の警告文で「class_attribute を使うように」と強く推奨していました。
  • その結果、実際には単純な attr_accessor や別の仕組みで十分なケースでも、ライブラリ作者やアプリ開発者が「とりあえず class_attribute」を選びがちになる、という誤誘導が発生していた、という問題意識があります。

今回の PR の意図:

  • Configurableclass_attribute に置き換えるべき」という “一択” のガイダンスをやめる。
  • 用途によっては attr_accessor 等のよりシンプルな代替の方が適切であることを反映し、複数の選択肢を警告文に列挙する。
  • これにより、「自分の使い方なら何を使うべきか」を、開発者自身が検討しやすくする。

実際のコード変更のイメージ(擬似例):

ruby
# 変更前(イメージ)
ActiveSupport::Deprecation.warn(
  "ActiveSupport::Configurable is deprecated. " \
  "Use `class_attribute` instead."
)

# 変更後(イメージ)
ActiveSupport::Deprecation.warn(
  "ActiveSupport::Configurable is deprecated. " \
  "Use an alternative such as `attr_accessor`, `mattr_accessor`, " \
  "`class_attribute`, or another configuration pattern as appropriate."
)

※上記は説明用の擬似コードです。実際の文言は英語・1 行差分のみですが、趣旨としては「class_attribute 一択 → 複数案提示」に変わっています。


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

    • ActiveSupport::Configurable の機能自体や挙動にはいっさい変更ありません。
    • 影響があるのは 警告メッセージの内容のみ です。
  • 移行ガイドとしての影響:

    • これまで警告に従って機械的に class_attribute へ置き換えていた場合、それが必ずしもベストプラクティスではないことを再検討すべきケースがあります。
    • 特に以下のような場合は、よりシンプルな手段が適している可能性があります:
      • 単に「設定値を保持するだけ」のクラスで、クラスインスタンス変数 + attr_accessor で事足りる。
      • クラス階層間での設定の継承やスレッドローカルな設定を必要としていない。
    • 一方で、以下のような要件がある場合は class_attributemattr_accessor などを含めた検討が必要です:
      • サブクラスごとに別々の設定値を持たせたい。
      • 設定値をクラスレベルで参照・変更したいが、一定の継承挙動も欲しい。
  • gem・ライブラリ作者への注意:

    • 既に「Configurable 非推奨 → class_attribute に一律置換」という対応をしている gem は、その選択が妥当かを再確認してもよいです。
    • 今後 Configurable の非推奨警告を見た開発者には、「いくつか選択肢があるので、自分のユースケースに合うものを選ぶ」ことが推奨されます。

  1. 参考情報

この PR 自体にはコード上の API 変更や破壊的変更はなく、「移行を促すメッセージの質の改善」が目的です。


#57285 Deactivate ViewReloader hooks when reloaders are cleared

マージ日: 2026/5/4 | 作成者: @ariens

  1. 概要 (1-2文で)
    ViewReloader が登録する「ビューの監視用フック」が app.reloaders.clear 実行後も残り続けて無駄なファイルスキャンを行っていた問題を修正し、リローダー削除時にフックも確実に解除されるようにした PR です。これにより、特に Spring のフォークワーカーなどで、不要な Dir[] / File.mtime 呼び出しが発生しなくなります。

  1. 変更内容の詳細

背景

  • ViewReloader は、レイルタイ初期化時に PathRegistry.file_system_resolver_hooksrebuild_watcher フックを登録します(#57269 で導入)。
  • app.reloaders には ViewReloader などのリローダーオブジェクトが格納されていて、例えば Spring のテストワーカーなどのフォーク後プロセスでは app.reloaders.clear が呼ばれます。
  • しかしこれまでは:
    • app.reloaders の配列から ViewReloader 自体は削除される
    • 登録済みの file_system_resolver_hooks 内のフックは生き残る
  • 結果として、prepend_view_path などが呼ばれるたびに、もはや必要のない Dir.[] + File.mtime を全ビューディレクトリに対して実行してしまう、という無駄な処理が行われていました。

この PR は「リローダーが Rails から取り除かれるときは、その副作用で登録していたフックも無効化する」という責務を明示化しています。


ViewReloader に #deactivate を追加

actionview/lib/action_view/cache_expiry.rb にある ViewReloader に新しく deactivate メソッドが追加されました。

イメージとしては以下のような処理です(疑似コード):

ruby
class ActionView::CacheExpiry::ViewReloader
  def initialize(...)
    # railtie 側で hook 登録するようになった (#57269)
  end

  def deactivate
    # PathRegistry.file_system_resolver_hooks から
    # 自分が登録した hook を取り除く処理
  end
end

ポイント:

  • ViewReloader が登録した file_system_resolver_hooks 内のエントリを削除するロジックをカプセル化。
  • これにより、「リローダーのライフサイクル」と「フックのライフサイクル」が同期される。

app.reloaders の Array を ReloadersCollection に差し替え

railties/lib/rails/application.rb で、これまで単なる Array だった app.reloaders の実体が、ReloadersCollection クラスに差し替えられました。

新クラスは railties/lib/rails/application/reloaders_collection.rb に定義され、主な役割は:

  • Array 互換のインターフェイスを提供しつつ、
  • clear / delete などで要素が削除されるときに、その要素に #deactivate があれば呼び出すこと。

ざっくりしたイメージ:

ruby
class Rails::Application::ReloadersCollection
  include Enumerable

  def initialize
    @reloaders = []
  end

  def <<(reloader)
    @reloaders << reloader
  end

  def clear
    @reloaders.each { |r| r.deactivate if r.respond_to?(:deactivate) }
    @reloaders.clear
  end

  def delete(reloader)
    if @reloaders.delete(reloader)
      reloader.deactivate if reloader.respond_to?(:deactivate)
    end
  end

  # 他にも each / [] / size など Array っぽい API を実装
end

これにより:

  • 既存コードは基本的に app.reloaders を Array として扱い続けられる(<<, each, clear など)。
  • しかし内部的には、cleardelete のタイミングで自動的に deactivate が呼ばれ、ViewReloader のフックが確実に外される。
  • respond_to?(:deactivate) チェックをしているため、ViewReloader 以外のリローダー(deactivate を定義していない)には影響しない。

Railtie 側の変更

actionview/lib/action_view/railtie.rb では、#57269 の変更を前提にしたわずかな修正が入っていますが、今回の PR の中心は以下の2点です:

  • フック登録の実装の位置は前 PR (#57269) で railtie に移っているため、今回の変更は「登録したフックをどう解除するか」にフォーカス。
  • ViewReloader がアプリケーションのリローダー一覧に登録されている前提のもと、その削除時に deactivate が呼ばれるようになった。

テスト追加

以下のテストが追加されています。

  • actionview/test/template/cache_expiry_test.rb

    • ViewReloaderdeactivate が実際に file_system_resolver_hooks からフックを取り除くかを確認するテスト。
    • prepend_view_path 時にフックが呼ばれなくなることを検証していると推測されます。
  • railties/test/application/reloaders_collection_test.rb

    • ReloadersCollection の挙動テスト:
      • clear が各リローダーの deactivate を呼ぶか。
      • delete が対象リローダーの deactivate を呼ぶか。
      • Enumerable 的な動作を保っているか。

  1. 影響範囲・注意点

主に影響を受けるケース

  • Spring など、Rails アプリケーションをフォークしてワーカーを立ち上げる環境で、
    • フォーク後に app.reloaders.clear を呼んでいる場合。
  • これまではフォーク後のワーカーで:
    • ViewReloader 自体は無効化されたつもりでも、
    • PathRegistry.file_system_resolver_hooks のフックだけは生き残り、
    • prepend_view_path(たとえばテストでパスを切り替えるなど)のたびに余計なファイルシステムスキャンが走っていた。
  • 今回の変更により:
    • その無駄な Dir[] / File.mtime 呼び出しがなくなり、フォークワーカー上でのオーバーヘッドが減る。
    • フォーク後のプロセスでファイル監視が不要な典型ケース(テストワーカーなど)で特に有効。

注意点・後方互換性

  • app.reloaders は依然として Array ライクなオブジェクトとして振る舞うため、普通に <<, each, clear, delete をしている分にはコード変更は不要。
  • ただし、リフレクションや is_a?(Array) に依存したコードを書いている場合は注意が必要です。
    • そのようなコードがあれば ReloadersCollection を許容する形に直す必要があります。
  • 独自リローダーを app.reloaders に追加していて「削除時に何かクリーンアップしたい」場合:
    • 任意で #deactivate を定義しておけば、app.reloaders.clear / delete 時に自動で呼ばれるようになります。
    • 逆に、そうしたクリーンアップを意図していなければ、特に何もしなくてよい(respond_to?(:deactivate) で守られている)。

パフォーマンス・動作上の効果

  • フォークワーカー上で、不要な view ディレクトリの走査がなくなるため、テスト起動・実行のパフォーマンス向上が見込めます。
  • ビューのリロード自体の挙動(開発環境などでの通常のオートリロード)は、今回の PR によって変化しないはずです。
    • 影響が出るのは「リローダーを明示的に clear / delete したとき」のみ。

  1. 参考情報 (あれば)
  • この PR が前提にしている変更:
    • #57269: ViewReloader のフック登録を initialize から railtie へ移動し、watcher の生成を遅延させることでブート時の無駄な再構築を防いだ PR。
  • Spring 側の関連 PR:
    • https://github.com/rails/spring/pull/754
      • Spring.after_environment_load によるアプリケーションレベルのプリロード制御。
      • 本 PR は Rails 内部のクリーンアップ(app.reloaders.clear が自動的にフックも無効化する)を担当し、Spring 側のフックとは役割分担されています。
  • 変更ログ:
    • actionview/CHANGELOG.md, railties/CHANGELOG.md に今回の修正内容の要約が追記されています。

#57289 Raise ArgumentError when MemCacheStore is created with Dalli::Client

マージ日: 2026/5/4 | 作成者: @p8

  1. 概要 (1-2文で)
    Rails の ActiveSupport::Cache::MemCacheStore に、Dalli::Client インスタンスを渡して初期化しようとした場合に ArgumentError を明示的に発生させる変更が入りました。これにより、すでに非推奨・削除されていた Dalli::Client ベースの初期化方法が、エラーメッセージどおりに完全に禁止されます。

  1. 変更内容の詳細

対象:
activesupport/lib/active_support/cache/mem_cache_store.rb の初期化ロジックが 1 行だけ変更されています。

背景:

  • 以前のコミット 78b74e9 で「Dalli::ClientMemCacheStore に渡す」方法は非推奨となっていました。
  • さらに c33e2d2e49d でそのサポートが削除されています。
  • しかし、コード上・エラーメッセージ上は「空配列・アドレス文字列・アドレス配列のみ許可」となっているにもかかわらず、Dalli::Client を渡したときに明示的な ArgumentError を投げていませんでした。

今回のPRでは、この不整合を解消し、Dalli::Client が渡された場合は必ず ArgumentError を投げるようにしています。

イメージとしては以下のようなチェックが入っていると考えてください(実際のコードは 1 行変更のみですが、概念的にはこのような条件分岐です):

ruby
# 例: MemCacheStore のコンストラクタ内のイメージ
def initialize(addresses = nil, **options)
  # 許可されるのは:
  # - nil または空配列
  # - "localhost:11211" のような address 文字列
  # - ["host1:11211", "host2:11211"] のようなアドレス配列
  #
  # Dalli::Client は明示的にエラーにする
  if addresses.is_a?(Dalli::Client)
    raise ArgumentError,
      "MemCacheStore: addresses must be empty array, address string, or array of addresses (Dalli::Client is no longer supported)"
  end

  # ... 残りの初期化処理 ...
end

PR 説明にある通り、エラーメッセージの仕様

only "empty array, address, or array of addresses" are allowed

と実際の挙動を揃えるのが主目的です。


  1. 影響範囲・注意点

影響があるケース:

  • アプリケーションやライブラリで、以下のように Dalli::Client オブジェクトを直接渡して MemCacheStore を作っている場合:
ruby
# 以前(非推奨だが技術的には動いていたパターン)
client = Dalli::Client.new("localhost:11211")
Rails.application.config.cache_store = :mem_cache_store, client

この PR 適用後は、上記のようなコードは ArgumentError で落ちます。

正しい移行方法(従来から推奨されていた書き方):

  • MemCacheStore には アドレス文字列またはアドレス配列 を渡してください。
ruby
# 推奨パターン
Rails.application.config.cache_store = :mem_cache_store, "localhost:11211"

# 複数サーバー
Rails.application.config.cache_store = :mem_cache_store,
  ["cache1.example.com:11211", "cache2.example.com:11211"]
  • Dalli を直接使いたい場合は、MemCacheStore 経由ではなく、自前で Dalli をラップする、あるいは Dalli 専用の設定パス(gem・ミドルウェアなど)を利用する必要があります。

注意点:

  • この変更は後方互換性を壊す可能性がありますが、
    • そもそも過去のコミットで非推奨になっていた
    • 対応するエラーメッセージも既に存在していた
      という経緯から、「非推奨状態からの明示的なエラー化」という整理にあたります。
  • テストと CHANGELOG が更新されているため、この挙動は Rails の正式な仕様変更として扱われます

  1. 参考情報 (あれば)
  • このPR: Raise ArgumentError when MemCacheStore is created with Dalli::Client (#57289)
  • 関連コミット(PR本文で言及されているもの)
    • 78b74e9: Dalli::Client を渡す方法の非推奨化
    • c33e2d2e49d: Dalli::Client サポートの削除
  • MemCacheStore のサポートする引数は、公式ドキュメントおよびエラーメッセージ上、
    • 空配列
    • 単一のアドレス文字列
    • アドレス文字列の配列
      に限定される仕様になっています。

#57291 [ci skip] Fix typos in test descriptions

マージ日: 2026/5/2 | 作成者: @jackbowlin

  1. 概要 (1-2文で)
    このPRは、テストコード内の説明文字列(テスト名)のスペルミスを修正したものです。テストの挙動やアプリケーションコードには一切変更はなく、影響は極めて限定的です。

  1. 変更内容の詳細

対象は以下2つのテストファイルです。

  • activesupport/test/env_configuration_test.rb
  • activestorage/test/models/named_variant_test.rb

修正されたのは「テストの説明文字列(テスト名)」だけで、テスト内容(アサーションや実行ロジック)は変更されていません。

ActiveSupport::EnvConfigurationTest

誤字:

  • reqiure → 正: require

例(イメージ):

ruby
# 変更前(イメージ)
test "env configuration reqiure something" do
  # ...
end

# 変更後(イメージ)
test "env configuration require something" do
  # ...
end

実際には "reqiure" という文字列が "require" に修正されています。

ActiveStorage::NamedVariantTest

誤字:

  • explicity → 正: explicitly

例(イメージ):

ruby
# 変更前(イメージ)
test "named variants are applied explicity" do
  # ...
end

# 変更後(イメージ)
test "named variants are applied explicitly" do
  # ...
end

こちらも "explicity""explicitly" に修正されたのみです。

テスト実行について

PRの説明どおり、「テストの名前(説明)」しか変えていないため、テストは実行されていません。Ruby/Railsのテストランナーは通常、test "..." do の文字列を人間向けの説明として扱うだけで、テストの実行ロジックには影響しません。


  1. 影響範囲・注意点
  • テスト・本番動作への影響

    • テストの実行結果や挙動、本番コードの動作には影響ありません。
    • スナップショットツールや外部のテストレポートに「テスト名で紐づけている」場合、名称の変更として認識される可能性があります(例: テストレポートビューアでのテストケース名が少し変わる程度)。
  • テスト選択・フィルタリングへの影響

    • Railsのruby -Itest test/path/to/file_test.rb -n "..."のように、テスト説明文字列を指定してフィルタリングしている場合、旧スペルで指定しているスクリプトはマッチしなくなる可能性があります。
    • そのような用途で使っている場合は、新しいテスト名(正しいスペル)に更新する必要があります。
  • リファクタリング観点

    • 本PRは技術的負債の解消というよりは、テストのメタデータの整備に近い変更です。
    • このような誤字修正は、ドキュメント生成・テストレポート・検索性(grepなど)におけるノイズ削減に貢献します。

  1. 参考情報 (あれば)

いずれも文字列の修正のみで、テストケースの構造やアサーションロジックの変更はありません。


#57269 Defer ViewReloader build when no view paths are registered

マージ日: 2026/5/1 | 作成者: @Korri

  1. 概要 (1-2文で)
    View 用のファイル更新監視(ViewReloader / FileUpdateChecker)の生成タイミングをさらに遅延させ、「まだ view path が1つも登録されていない状態」で updated? が呼ばれた場合でも不要な watcher を作らないようにした PRです。これにより、特に Spring 利用時のテスト実行など、開発環境での起動〜初回リクエスト前の無駄なディレクトリ走査が大幅に削減されます。

  1. 変更内容の詳細

背景と問題点

前回の変更 (#51308) で、「最初の updated? チェックまでは View watcher を作らない(lazy build)」という仕組みが導入されていました。しかし、以下のケースで問題が残っていました。

  • dirs_to_watch(監視対象ディレクトリの配列)が まだ空の状態updated? が呼ばれる
  • その時点で #build_watcher が呼ばれ、中身が空の FileUpdateChecker でもとりあえず作成して @watcher にセットしてしまう
  • 以降は @watcher が non-nil なので #rebuild_watcherreturn unless @watcher ガードが常に通り、
    後から各エンジンが prepend_view_path するたびに「全ディレクトリ集合に対する Dir[*globs] + File.mtime のフルスキャン」が走る

Spring を使ったテスト環境ではこれが顕在化します。

  • Spring の preloader は Rails.application.reloaders を直接見て reload! が必要か判断します(Reloader.check! は通さない)
  • 多くのエンジンの prepend_view_pathActiveSupport.on_load(:action_mailer) / :action_controller_base のフック内で行われるため、preload フェーズではまだ呼ばれていない
  • そのため「view path が1つも登録されていない状態」で updated? が呼ばれ、空 watcher が固定されてしまう
  • 結果として、その後の各 prepend_view_path ごとにフルスキャンが走り、フォークごとに高コストなディスク I/O が発生

PR の説明中にある挙動比較は以下の通りです。

修正前

  • 早期ブート時の updated? 呼び出し
    • → watcher が空のまま作られる(軽いが「作ってしまう」)
  • その後の prepend_view_path ×10
    • 10回ともフル再構築(ディレクトリ再走査)
  • 最初のリクエスト時の updated?
    • → すでにウォッチャーがあるので再構築なし

修正後

  • 早期ブート時の updated? 呼び出し
    • view path が空なので watcher を作らない
  • その後の prepend_view_path ×10
    • @watcher がまだ nil のため 0回再構築
  • 最初のリクエスト時の updated?
    • → ここで初めて watcher を build(1回だけディレクトリ走査

実際のコード上の変更点(概念的な説明)

※実際の diff からの要約であり、擬似コードを含みます。

1) ActionView::CacheExpiry 内の watcher ビルドロジック

actionview/lib/action_view/cache_expiry.rb の主な変更点は:

  • build_watcher / rebuild_watcher 周りの条件分岐が見直され、
  • 「監視対象ディレクトリが空のときは watcher を作らない」 というガードが追加された

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

ruby
def build_watcher
  return if dirs_to_watch.empty?  # 追加されたガード: 何も監視対象がなければ何もしない

  @watcher = ActiveSupport::FileUpdateChecker.new(dirs_to_watch, &callback)
end

def rebuild_watcher
  return unless @watcher         # 既存のガード: watcher がないなら何もしない
  return if dirs_to_watch.empty? # 追加(もしくは強化)された条件: 空なら再構築もしない

  @watcher = @watcher.rebuild(dirs_to_watch)
end

ポイント:

  • 初回 updated?
    • @watcher が nil かつ dirs_to_watch が空 → build_watcher しても何も作らない → @watcher は nil のまま
  • view path が登録されたあと
    • 初めて dirs_to_watch が非空の状態で updated? が走ったタイミングで watcher を生成
  • その後のパス追加
    • すでに @watcher がある状態で dirs_to_watch が変化したときにだけ rebuild_watcher が動く

これにより「空の watcher が早期に固定される」パターンが潰され、
prepend_view_path のたびにフルスキャンが走るのを防いでいます。

2) Railtie 側の微調整

actionview/lib/action_view/railtie.rb では 1 行だけの追加ですが、
ViewReloader の登録や初期化タイミングに関する配慮が行われています。

  • ねらいとしては、「reloader の初期化そのものは行うが、実際の FileUpdateChecker の生成は view path が揃うまで遅延させる」という設計に沿うようになっています。

3) テストの追加

actionview/test/template/cache_expiry_test.rb に約 94 行のテストが追加されています。
主に以下を確認する内容と考えられます。

  • view path 未登録時に updated? を呼んでも watcher が作られない こと
  • その後に prepend_view_path を複数回呼んでも、不必要に watcher を再構築しないこと
  • 最初に「非空の view path セット」で updated? を呼んだときにのみ watcher が1回だけ作られ、その後の挙動が正しいこと

これにより、今回の遅延ロジックが将来的に regress しないように自動テストでカバーしています。

4) CHANGELOG

actionview/CHANGELOG.md に今回の修正の概要が追記され、Action View の挙動変更として明示されました。


  1. 影響範囲・注意点

影響範囲

  • 対象:
    • ActionView の ViewReloader / CacheExpiry 周り
    • 主に 開発環境・Spring 利用時 に影響
  • 影響しないケース:
    • production 等で cache_classes = true の場合
      → ViewReloader 自体がスキップされるため、挙動は従来通り
    • 「最初の reloader チェック(updated?)よりも先に、全ての view path が登録される」ような小規模アプリ
      → もともと問題が顕在化しないケースなので挙動上の差はほぼない

性能面のインパクト

  • Shopify の Rails モノレポでの計測では、Spring を使ったテストループ(小さめのワークロード)で
    • 5.16 秒 → 3.66 秒(約 29% の短縮)
  • 実際には、prepend_view_path を多用している大規模アプリケーションほど恩恵が大きくなります。

互換性・注意点

  • 監視対象ディレクトリが「存在しない or 空」である場合には watcher が作られない挙動になりますが、
    • 「view path が 1 つも登録されていないのに view キャッシュの更新検知をしようとする」というケースはそもそも意味がないため、実質的には妥当な制約です。
  • 独自に ActionView::CacheExpiry を拡張しているような特殊なコードベースがある場合は、
    • @watcher が nil であり続けるパターンが増えることを前提にしているかどうかを確認した方が安全です。
  • view path の追加タイミングに依存して「最初の updated? 時に必ず watcher がある」と仮定しているコードがあると、
    その前提が崩れる可能性はありますが、通常の Rails アプリではまず問題にならない想定です。

  1. 参考情報 (あれば)
  • 本 PR:
    • Defer ViewReloader build when no view paths are registered (#57269)
  • 直前の関連 PR:
    • Do not build View watcher until the first updated? check (#51308)
  • コード参照:
    • actionview/lib/action_view/cache_expiry.rb
      • View の更新検知周り(build_watcher, rebuild_watcher, updated? など)
    • actionview/lib/action_view/railtie.rb
      • ViewReloader の登録・初期化
    • actionview/test/template/cache_expiry_test.rb
      • 遅延ビルドと再構築ロジックのテスト
  • Spring 側の関連コード(参考):
    • spring/lib/spring/application.rb:197
      • Rails.application.reloaders を直接見て reload が必要か判定するロジック

#57283 Avoid assert_nil for Mime::NullType request format

マージ日: 2026/5/1 | 作成者: @mugitti9

  1. 概要 (1-2文で)
    Railsのテストコードで、未知のフォーマット要求時の request.formatassert_nil で検証していた箇所を、Mime::NullType を前提とした検証に変更し、最新の Minitest の挙動変更に追従したテスト専用の修正です。実行時の挙動や公開APIには影響せず、テストの互換性・堅牢性を高めるための変更です。

  1. 変更内容の詳細

背景となる問題

  • Rails では、未知のフォーマットがリクエストされた場合、request.formatnil ではなく Mime::NullType のインスタンスを返します。
  • もともとテストでは、このケースを assert_nil request.format のように「nil であること」で検証していました。
  • しかし Minitest 側の変更(assert_nilobj.nil? ではなく nil == obj で比較するようになった)により、
    • 以前: obj.nil? を呼ぶ → Mime::NullType#nil?true を返せばテストが通る
    • 現在: nil == obj を評価 → どんなに obj.nil?true でも、obj が実際に nil でないため失敗
      という不整合が発生していました。

このPRでの主な修正ポイント

1) request.format のテストを Mime::NullType 前提に変更

actionpack/test/dispatch/request_test.rb のテストが、以下のような形に修正されています(擬似コード):

ruby
format = request.format

# 以前:
# assert_nil format

# 以後: 「NullType であること」と「nil? が true であること」を明示的に検証
assert_instance_of Mime::NullType, format
assert_predicate format, :nil?
# 既存の html?/xml?/json? などの否定チェックも維持
refute_predicate format, :html?
refute_predicate format, :xml?
refute_predicate format, :json?

ポイント:

  • 単に「nil と等しいか」ではなく、「Mime::NullType というクラスのオブジェクトであり、その #nil?true を返す」という Rails 独自の仕様を明示的にテスト。
  • すでに存在していた html?, xml?, json? などのフォーマット判定メソッドのテストは維持され、Unknown フォーマット時にはどれも false であることを引き続き確認しています。

2) actiontext 側のテストも同様に修正

actiontext/test/unit/model_test.rb にも同種の assert_nil を使ったテストがあり、同じ問題が起きる可能性があるため、こちらも同じ方針で Mime::NullType + nil? の検証に書き換えられています。

3) Gemfile.lock の微調整

Gemfile.lock に数行の変更 (+3/-2) が入っており、Minitest を含むテスト関連依存のバージョンが更新されています(今回の assert_nil の仕様変更を取り込んだバージョンに追従するためのものと考えられます)。
本体コードではなくテスト環境に関連する変更です。


  1. 影響範囲・注意点
  • 実行時の Rails コード (Mime::Type, Mime::NullType, ActionDispatch::Request など) の挙動は変わりません。あくまでテストコードのみの変更です。

  • ただし、以下のようなプロジェクトには実務的な示唆があります:

    1. Minitest を新しいバージョンに上げた Rails アプリ

      • 自前のテストで assert_nil some_request.format のように「nil? に依存したテスト」を書いている場合、今回と同じ理由で壊れる可能性があります。
      • 未知フォーマットで Mime::NullType を返す箇所に対しては、
        • assert_instance_of Mime::NullType, obj
        • assert_predicate obj, :nil?
          など、Rails の実際の戻り値仕様に沿ったテストに修正するのが望ましいです。
    2. nil? をオーバーライドしているクラスに対する assert_nil

      • Minitest の仕様変更により、「nil? をオーバーライドした擬似 Nil オブジェクト」を assert_nil でテストする手法は通用しなくなります。
      • そのようなオブジェクトについては、このPRと同様に
        • 型 (assert_instance_of / assert_kind_of)
        • ふるまい (assert_predicate obj, :nil?) で個別に検証するスタイルに切り替える必要があります。
  • 本PRはテストの期待値を Minitest 実装依存から切り離し、「Rails 側が本来保証したい仕様(NullType + nil?)」を直接テストする形に整理しているため、長期的にはテストの安定性が向上します。


  1. 参考情報 (あれば)
  • 関連する Minitest の変更:
    • assert_nil(obj) が内部で obj.nil? を呼ぶ実装から、nil == obj で比較する実装に変更されたコミット:
      • minitest/minitest@c471347
  • Rails の Mime::NullType 仕様:
    • 未知の MIME タイプに対するフォーマットオブジェクトであり、nil?true にすることで「nil に準ずる」扱いを可能にしているが、オブジェクトとしては nil とは異なる、という設計になっています。