Skip to content

Ruby on Rails PR Digest - 2026年 4月

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

#57176 Make becomes preserve marked_for_destruction

マージ日: 2026/4/29 | 作成者: @genezys

  1. 概要 (1-2文で)
    ActiveRecord::Persistence#becomes を使用してオブジェクトを別クラスのインスタンスに変換した際に、
  • marked_for_destruction の状態
  • ロード済み関連(associations)上の変更
    が失われないようにするバグ修正・挙動改善の PR です。

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

何が問題だったか

becomes は、同じテーブルを共有する STI などの文脈で、あるモデルインスタンスを別クラスのインスタンスに「なり替える」ためのメソッドです。

従来の挙動では、以下のようなケースで問題がありました:

  • 親オブジェクトの has_many / has_one 関連で、子レコードを marked_for_destruction にしていた場合
  • 関連オブジェクトに対して変更を行っていた場合

becomes によって新しいクラスのインスタンスへ変換すると、その

  • marked_for_destruction フラグ
  • ロード済み関連オブジェクト上の変更
    が新インスタンスに正しく引き継がれないことがあった、というのが本 PR のモチベーションです。

具体的な修正点

ActiveRecord::Persistence#becomes に次の2点の「状態の引き継ぎ」が追加されました。

  1. marked_for_destruction の引き継ぎ
    すでに previously_new_record については引き継いでいましたが、それと同様の扱いで

    • 元のインスタンスで marked_for_destruction? == true であれば
    • becomes で生成された新インスタンスにも同じ状態をコピー
      するように修正されています。
  2. ロード済み関連(associations)の引き継ぎ

    • すでにロード済み(= association キャッシュに載っている)関連のみを対象に
    • 新インスタンスの association キャッシュにも同じ関連オブジェクトを設定
      するようになりました。
      これにより、becomes 前に関連に対して行っていた変更(破棄マークや属性変更など)が、新インスタンス側でも失われません。

    重要なのは「キャッシュ済みの関連のみ」を対象にしている点です。
    これによって:

    • 未使用の関連をわざわざロードしない(パフォーマンスと意図しない副作用を防ぐ)
    • テストにある「不正な関連定義」(ロードしたらエラーになるような association)に触れずに済む
      といった利点があります。

簡単なサンプルイメージ

(PR 本文には具体的コード例はありませんが、挙動イメージとして)

ruby
class Person < ApplicationRecord
  has_many :pets
end

class Admin < Person
end

person = Person.find(1)
person.pets.first.mark_for_destruction

admin = person.becomes(Admin)

# 修正前:
admin.pets.first.marked_for_destruction? # => false (フラグが失われる可能性)

# 修正後:
admin.pets.first.marked_for_destruction? # => true (元の状態が保持される)

このように becomes 後も、関連に対して行った「破棄予定」やその他の変更が保たれることを意図しています。


  1. 影響範囲・注意点
  • 対象:
    • ActiveRecord::Persistence#becomes を利用しているコード全般
    • 特に、STI や同一テーブルを共有するクラス間での「型の切り替え」と、mass-assignment を組み合わせて使っている箇所
  • 期待される影響:
    • これまで becomes の後に、子レコードの削除マークや関連オブジェクトの変更が「なぜか消えている/反映されない」といったバグが発生していた場合、その挙動が修正されます。
    • 逆に言えば、古い挙動(becomes したらフラグがリセットされることを前提としたコード)が存在していた場合は、その前提が崩れます。ただし、そのような前提は通常はバグ寄りの設計と考えられます。
  • パフォーマンス面:
    • 変更は「キャッシュ済みの関連」に対してのみ行うため、新たに関連をロードすることはなく、既存コードに対して大きなパフォーマンス悪化は想定されません。
  • テスト:
    • activerecord/test/cases/persistence_test.rb にテストが追加されており、
      • marked_for_destruction が保持されること
      • ロード済み関連の変更が保持されること
        が自動テストで担保されています。

  1. 参考情報 (あれば)
  • 対象メソッド: ActiveRecord::Persistence#becomes
  • 関連する状態:
    • marked_for_destruction(nested attributes などで使われる削除フラグ)
    • previously_new_record(すでに becomes で引き継がれていた状態)
  • ファイル変更:
    • activerecord/lib/active_record/persistence.rb
      becomes の実装に、状態・関連の引き継ぎロジックを 1 行追加
    • activerecord/test/cases/persistence_test.rb
      状態保持を検証するテストを 9 行追加

#57252 Enable frozen string literal by default

マージ日: 2026/4/27 | 作成者: @byroot

  1. 概要 (1-2文で)
    Rails が生成する新規アプリケーションで、「文字列リテラルの凍結(frozen_string_literal)」をデフォルト有効にする変更です。影響はアプリ自身のコードに限定され、Gem などの依存ライブラリにはデフォルトでは適用されません。

  1. 変更内容の詳細

※PR本文・差分から読み取れる範囲での要約です。

2-1. 新規アプリで frozen string literal がデフォルト有効に

この PR では、新しく rails new で生成されるアプリケーションについて、Ruby の「文字列リテラルをデフォルトで凍結する」設定を有効にしています。

実現方法としては、以下のような変更が組み合わさっています(実ファイル名ベースで整理):

config/bootsnap.rb.tt の変更

bootsnap の設定ファイルテンプレートに、アプリの自前コードを対象として frozen_string_literal を有効化する設定が追加されています。

イメージとしては、Bootsnap.setup のオプションに「自分のアプリのコードは frozen string literal モードで解釈する」旨の設定が入る形です。

ruby
# config/bootsnap.rb (生成イメージの一例)
Bootsnap.setup(
  # 中略
  load_path_cache: true,
  compile_cache_iseq: true,
  compile_cache_yaml: true,
  # アプリケーションコードに対して frozen_string_literal を有効化
  # (実際のキー名は bootsnap の実装側に依存)
  allow_frozen_string_literal: true
)

※実際のキー名や詳細は、参照 PR(bootsnap #535)側の API に沿ったものになります。

Rails 自体は Ruby のソースコードを直接パースして magic comment を書き換えるわけではなく、bootsnap の ISeq キャッシュ機能を利用して、アプリコードを frozen string モードで実行するようにしています。

Gemfile.tt の変更

Gemfile のテンプレートも 1 行だけ変更されています。
内容としては、bootsnap のバージョン / 設定を、上記の frozen string 対応を使えるものに合わせて更新している可能性が高いです(古い bootsnap ではこの機能がないため)。

ruby
# 例 (ニュアンスのみ)
gem "bootsnap", require: false
# → ここが frozen string 対応可能なバージョンを指すように変更

rubocop.yml.tt の変更

生成されるデフォルトの .rubocop.yml に、Style/FrozenStringLiteralComment など、frozen string 周りの設定が追記されています。
これにより、Rubocop も「frozen string が前提」のスタイルガイドで動くようになります。

例(イメージ):

yaml
Style/FrozenStringLiteralComment:
  Enabled: true
  EnforcedStyle: always # など(実際の値は差分に依存)

ただし、この PR は magic comment を各ファイルに自動で付与するのではなく、bootsnap による実行時の設定で凍結を有効化するため、「Rubocop は magic comment を期待するが、実際には bootsnap で有効化されている」という状態になる可能性があります。
そのため Rubocop 設定側で「コメントを要求しない」ようなコンフィグが入っている可能性もあります。

app_generator.rb / app_generator_test.rb の変更

  • app_generator.rb
    新規アプリ作成時に、上記 bootsnap / rubocop / Gemfile などのテンプレート変更が反映されるように 1 行追加されています(オプションの有効化やテンプレートコンテキストの調整など)。

  • app_generator_test.rb
    生成されたアプリに frozen string 設定が正しく反映されることを検証するテストが追加されています。
    例としては「config/bootsnap.rb などに特定の設定が書き込まれているか」などを確認しているはずです。

CHANGELOG.md の追記

Rails 7.x / 8.x(どのバージョンかは対象ブランチによる)向け CHANGELOG に、以下のような項目が追記されています:

  • 新規アプリケーションでは frozen string literal がデフォルトで有効になること
  • 影響はアプリケーションコードに限られること
  • Gems などの依存コードについては、デフォルトでは frozen string を強制しないこと

  1. 影響範囲・注意点

3-1. 影響範囲

  • 対象:この PR が取り込まれた Rails バージョン以降で rails new したアプリの「アプリケーションコード」
    • app/models, app/controllers, app/views, config/ など、いわゆる自前コード
  • 非対象:Gem やプラグインなどの外部依存コード
    • 既存の gem の中には、文字列リテラルが凍結されると壊れるものがまだあり得るため、デフォルトでは適用されない

3-2. 起きうる問題

自分のアプリコード内で、以下のような書き方をしていると、FrozenError が発生する可能性があります。

ruby
# NG 例(frozen string 下ではエラー)
foo = "hello"
foo << " world"    # "can't modify frozen String: \"hello\"" のようなエラー

# 改善例
foo = +"hello"     # dup して非 frozen にする
foo << " world"
# あるいは
foo = "hello world"

頻出しがちなパターン:

  • 定数内の文字列をミューテートする
  • メソッド引数で渡ってきた文字列に対して破壊的メソッド (<<, gsub!, upcase! など) を直接呼ぶ
  • String#freeze を前提にした一部の最適化コードの挙動が変わる

特に、古いチューニングコードや、曖昧な破壊的操作が多いコードベースでは、テストを回すとエラーがあぶり出される可能性があります。

3-3. 既存アプリへの影響

  • 既存アプリには「自動的には」適用されません。
  • ただし、この PR に合わせて自分で config/bootsnap.rb を編集し、同様の設定を追加すれば、既存アプリでも同じモードを有効化できます。
  • その場合は、テストをフルに回して FrozenError が出ないかを確認することが推奨されます。

3-4. gems への適用について

PR 説明にある通り:

It is also possible to enable it for gems, but some old ones may still not be ready.

bootsnap の設定次第で、Gem のコードにも frozen string を適用することは技術的には可能です。
しかし、古い Gem の中にはまだ非対応なものがあるため、Rails はデフォルトではそこまで踏み込んでいません。
もし gem にも適用したい場合は、段階的にオンにして、問題があれば Gem 側のアップデートや修正を検討する必要があります。


  1. 参考情報 (あれば)
  • 参照 PR: https://github.com/rails/bootsnap/pull/535
    • bootsnap 側で「frozen string literal を有効にして ISeq キャッシュを扱う」機能を追加している PR です。
  • Ruby 本体の機能: # frozen_string_literal: true
    • ファイル先頭のマジックコメントとしておなじみですが、この PR ではコメントベースではなく bootsnap ベースで一括制御しているのがポイントです。
  • 一般的な対応指針:
    • 文字列を変更したいときは dup あるいは +"literal" を使って非 frozen な文字列を生成する
    • グローバル/定数の文字列は基本的に不変オブジェクトとみなし、破壊的変更を避ける
    • Rubocop など静的解析ツールの設定を frozen string 前提(コメント必須 or コメント不要)に揃えることで、スタイルと挙動の齟齬を抑える

#56718 Fix innermost constraint reseting previous constraints

マージ日: 2026/4/27 | 作成者: @petrenkorf

  1. 概要 (1-2文で)
    ActionDispatch::Routing の「constraints」のマージ処理が修正され、ネストした複数の constraint を定義した際に「一番内側だけが有効になる」不具合が解消されました。これにより、ネストされた constraints ブロックがすべて評価され、期待通りに複合条件として動作するようになります。

  1. 変更内容の詳細

問題の背景

Rails のルーティングでは、以下のように constraints をネストして使うことができます:

ruby
constraints ConstraintA.new do
  constraints ConstraintB.new do
    get "/foo", to: "foo#index"
  end
end

本来であれば、ConstraintAConstraintB の両方が満たされたときだけ /foo にマッチしてほしいところですが、バグにより「一番内側 (ConstraintB) だけが評価される」状態になっていました。
つまり、外側の constraints が内側の constraints によって上書き(リセット)されてしまう挙動になっていました。

修正のポイント

この PR では、ActionDispatch 内部の「constraints のマージ処理」を変更し、単一の constraint を都度上書きするのではなく、constraint ブロックのリストとして蓄積し、順番に評価する 形に修正しています。

コード上では actionpack/lib/action_dispatch/routing/mapper.rb で以下のような変更が行われています(疑似イメージ):

これまでのイメージ(問題のある挙動)

ruby
# 擬似コードイメージ
def merge_constraints(existing, new_constraint)
  # new_constraint があれば既存を上書きしてしまう
  new_constraint || existing
end

このため、ネストしたときに一番内側の constraint が最終的な constraint として採用されてしまっていました。

修正後のイメージ

ruby
# 擬似コードイメージ
def merge_constraints(existing, new_constraint)
  # constraint を配列として扱い、どんどん蓄積する
  Array(existing) + Array(new_constraint)
end

# ルーティング評価時には、蓄積された全 constraint を順次評価
def matches?(request)
  constraints.all? { |c| c.matches?(request) }
end

実際にはもう少し複雑ですが、概ね以下のような方針です:

  • constraints オプション/ブロックを「単一のオブジェクト」ではなく「複数の constraint を表すコレクション」として扱うように変更
  • ネスト時の merge で「上書き」ではなく「配列への追加」になるよう変更
  • ルーティングのマッチ判定時に、その配列内のすべての constraint を評価して AND 条件を実現

テストの追加

actionpack/test/dispatch/routing_test.rb に新しいテストが追加されています(+13 行)。
内容としては、以下のようなケースをカバーしていると考えられます:

  • 外側と内側でそれぞれ別の constraint を定義
  • 両方が満たされる場合にのみルートがマッチすること
  • 一方だけ満たされない場合にマッチしないこと

これにより、以前報告されていた Issue (#56528) の再発を防ぐ形になっています。


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

    • constraints をネストしているルーティング定義に挙動の変化があります。
    • これまでバグに依存して「内側だけが効く」ことを前提にしていた場合、期待挙動が変わる可能性がありますが、本来の仕様に近づく方向の変更です。
    • 通常の(非ネスト)constraints の使い方にはほとんど影響ありません。
  • 既存アプリでの確認ポイント

    • routes.rb で以下のようなパターンを使っている場合は、テストで挙動確認をしておくとよいです:
      ruby
      constraints ConstraintA do
        constraints ConstraintB do
          # routes...
        end
      end
    • 特に、IP 制限やサブドメイン制御、認証状態など複数条件を constraint で表現している場合、全ての constraints が AND 条件で効くようになっていることを前提にテストを見直すと安心です。
  • パフォーマンス

    • constraints がリスト化されることで、ネストが深い場合に評価回数が増えますが、通常の数であれば無視できる程度です。
    • 非常に多数の constraints をネストしている場合は、ルーティングマッチのコストがわずかに増える可能性があります。

  1. 参考情報 (あれば)

#57238 Remove redundant libvips from Dockerfile build packages

マージ日: 2026/4/27 | 作成者: @rikki3

  1. 概要 (1-2文で)
    Rails が生成する Dockerfile から、ビルドステージ側に重複して記述されていた libvips のインストールを削除した PRです。ベースイメージ側ですでに libvips を含んでいるため、不要な二重インストールを解消しています。

  1. 変更内容の詳細

対象ファイル: railties/lib/rails/generators/app_base.rb
変更行数: 追加 0 / 削除 3

rails new でアプリケーションを生成する際に使われるテンプレートから、Dockerfile の「build 用パッケージ」リストに含まれていた libvips を削除しています。

背景として説明にある通り:

  • Rails の生成する Dockerfile には
    • ベースイメージ(runtime)用のパッケージ群
    • ビルドステージ(build)用のパッケージ群
      という 2 種類のインストールリストがあり、
  • build ステージは base ステージを継承しているため、base に入っているパッケージは build にもすでに入っている
  • にもかかわらず、両方に libvips が書かれていたため、Dockerfile 上で「同じパッケージを二度指定している」状態だった

この PR では「build packages 側に書かれていた libvips を削除」することで、Dockerfile 生成時に libvips のインストール行が重複しないようにしています。

疑似的なイメージ:

ruby
# 変更前(概念的なイメージ)
build_packages = %w[
  build-essential
  libvips      # <- base 側にも入っているのにここにもあった
  ...
]

# 変更後
build_packages = %w[
  build-essential
  # libvips は削除
  ...
]

実際の diff は 3 行削除のみで、新しいパッケージの追加やロジック変更はありません。


  1. 影響範囲・注意点
  • 対象:
    • この PR マージ後の Rails バージョンで rails new した際に生成される Dockerfile
    • とくに、libvips を利用している Active Storage + image_processing などの構成で Docker を使う場合
  • 動作面:
    • libvips 自体は引き続きベースイメージ側のパッケージとしてインストールされるため、ランタイムでの画像処理等の機能には影響しません。
    • build ステージも base を継承しているので、ビルド時に libvips が足りなくなることもありません。
  • 影響:
    • Dockerfile 上の冗長なインストール指定がなくなるだけであり、機能追加・削除はなし。
    • Docker のレイヤーキャッシュやログから見えるパッケージインストール行が少しシンプルになります。
  • 注意点:
    • 既存プロジェクトの Dockerfile を手書き・カスタマイズしている場合には自動的には変わりません。重複インストールが気になる場合は、同様に build 用パッケージから libvips を削ることを検討すると良いです。
    • ベースイメージを独自に差し替えていて、そちらに libvips を含めていない場合は、この PR の想定と異なる構成になるため、自前 Dockerfile では明示的にインストールする必要があります(ただしこの PR は Rails が生成する標準 Dockerfile テンプレートのみを変更します)。

  1. 参考情報 (あれば)
  • 対応 issue: #57237
  • libvips は Active Storage + image_processing gem などで用いられる高速な画像処理ライブラリです。
  • 該当テンプレートは Rails アプリ作成時のジェネレータ (Rails::Generators::AppBase) にあり、--docker などのオプションで Dockerfile を生成する際に利用されます。

#57246 Optimize ActiveRecord::Relation#extending! to skip work when called with empty modules

マージ日: 2026/4/26 | 作成者: @bogdan

  1. 概要 (1-2文で)
    ActiveRecord::Relation#extending! が、空のモジュール配列やブロックなしで呼ばれた場合に即座に何もせず return するよう最適化された PR です。これにより、merge_multi_values を含むあらゆる呼び出し元で、不要な処理を避けてパフォーマンスを改善しています。

  1. 変更内容の詳細

背景

ActiveRecord::Relation#extending! は、リレーションに対して拡張モジュールを付与するために使われるメソッドです:

ruby
Model.where(...).extending!(SomeExtensionModule)

このメソッドは、Relation#extensions 配列にモジュールを追加するなどの処理を行いますが、呼び出し側によっては「空のモジュール配列」を渡すケースもあり、その際にも内部処理が走ってしまうのが無駄でした。

前 PR (#57199) では、merge_multi_values 側にガードを置く対応がされていましたが、この PR ではそれを一歩進めて、「extending! 自体が早期リターンできるようにする」ことで、全ての呼び出し元で最適化の恩恵を受けられるようにしています。


extending! の変更

extending! に以下のようなガードが追加されたと考えられます(意訳コード):

ruby
def extending!(*modules, &block)
  # 新規: モジュールもブロックもない場合は何もしない
  return self if modules.empty? && !block_given?

  # もともとの処理(概要)
  self.extending_values |= modules  # `|=` で重複を除外しつつ追加
  extending!(Module.new(&block)) if block_given?
  self
end

ポイント:

  • モジュール引数が空 (modules.empty?) かつブロックもない (!block_given?) 場合は、即座に self を返す。
  • すでに |= を用いた配列マージで重複除去(deduplication)を行っていたため、呼び出し元側での手動の差集合計算は不要になった。

merge_multi_values の簡素化

merge_multi_values は、リレーション同士のマージ時に extensions もマージするメソッドです。以前は、other.extensionsrelation.extensions の差集合を手動で計算してから extending! に渡していました。

従来イメージ(意訳):

ruby
# 以前は手動で差集合を計算
extensions_to_add = other.extensions - relation.extensions
relation.extending!(*extensions_to_add) unless extensions_to_add.empty?

PR 後のイメージ:

ruby
# 差集合の計算をやめ、単に extensions を渡すだけ
relation.extending!(*other.extensions) unless other.extensions.empty?

変更点:

  • a - b による差集合計算を削除。
  • unless other.extensions.empty? というガードだけ残し、dedup は extending! 内部の |= に任せる設計に。
  • さらに、「空の extensions」の場合にメソッド呼び出し自体をスキップすることで、ホットパスでのオーバーヘッドを抑えている(extending! 側にもガードはあるが、「そもそも呼ばないほうが速い」ので二重ガード)。

  1. 影響範囲・注意点
  • 対象メソッド:
    • ActiveRecord::Relation#extending!
    • ActiveRecord::Relation::Merger#merge_multi_values
  • 機能的な挙動:
    • 機能面の変更はほぼなく、「空の extensions / ブロックなし」の場合に何もせず戻るようになっただけです。
    • extending! が内部で |= を使っているため、モジュールの重複は引き続き自動的に除外されます。
  • 互換性:
    • extending! の戻り値は従来通り self であり、メソッドチェーンの挙動は変わりません。
    • 空の引数・ブロックなしで extending! を呼んでいたコードがあっても、もともと何も有効な効果を持たない呼び出しなので、実質互換性の問題はありません(内部コストが減るだけ)。
  • パフォーマンス:
    • 空の extensions を頻繁に扱うようなコードや、merge によるクエリビルドを多用している箇所で、わずかなパフォーマンス改善が見込まれます。

  1. 参考情報 (あれば)

#54829 Allow using aliases for unions in from clause

マージ日: 2026/4/26 | 作成者: @fatkodima

  1. 概要 (1-2文で)
    Rails の ActiveRecord::Relation#from に対し、UNION を使ったサブクエリにテーブルエイリアスを付けて利用できるようにする修正です。これにより、from(union, :some_models) のような呼び出しで、PostgreSQL などで発生していた「missing FROM-clause entry」エラーが解消されます。

  1. 変更内容の詳細

これまでの問題点

以下のように、UNION を使ったクエリを from 経由で使いたいケースを想定します:

ruby
union = SomeModel.select(:id).where(...)
         .union(OtherModel.select(:id).where(...))

# UNION した結果を from で参照し、エイリアス some_models を付与したい
relation = SomeModel.from(union, :some_models)
  • Active Record は from(union) 自体は受け付けるものの、
  • 第2引数で渡された :some_models のエイリアスを無視して SQL を生成していました。
  • そのため、生成される SQL は例えば以下のようになっていたと考えられます:
sql
SELECT "some_models".* FROM (
  (SELECT "some_models"."id" FROM "some_models" WHERE ...)
  UNION
  (SELECT "other_models"."id" FROM "other_models" WHERE ...)
)
-- ※ ここに AS some_models が無い

その結果、SELECT "some_models".* ... で参照している "some_models" というテーブル名/エイリアスが FROM 句に存在せず、PostgreSQL で

missing FROM-clause entry for table "some_models"

というエラーが発生していました。

PR の対応内容

ActiveRecord::Relation#from が、引数として渡されたリレーション(特に UNION を含むもの)に対しても、明示的なエイリアスを正しく適用するように修正されています。

ポイント:

  • from(union_relation, :alias) といった呼び出し時、
    • これまでは、この :alias 情報が内部で無視されていた。
    • 修正後は、生成される SQL の FROM 句に AS alias が付与されます。

おおよそのイメージ:

ruby
# 修正後の期待挙動
SomeModel.from(union, :some_models)
# => SQL (イメージ)
# SELECT "some_models".* FROM (
#   SELECT ... FROM ...
#   UNION
#   SELECT ... FROM ...
# ) AS some_models

テストコード側 (activerecord/test/cases/relations_test.rb) では、

  • UNION を使って Relation#union で組み立てたサブクエリを from に渡す
  • その際に別名(エイリアス)を渡し、それが実際に SQL に反映される

といった内容のテストが追加されています。
これにより、今回の挙動が回帰しないよう担保されています。


  1. 影響範囲・注意点
  • 影響範囲:
    • ActiveRecord::Relation#fromUNION を含むリレーション(サブクエリ)と組み合わせて使っているコード全般。
    • 特に「第2引数でエイリアスを指定しているが、今までは動かなかった/偶然動いていた」ようなケースで挙動が変わる可能性があります。
  • 期待される変化:
    • これまでエイリアスが無視されていたケースで、正しく AS alias が付くようになります。
    • そのため、SELECT などでそのエイリアス名を前提としている複雑なクエリが正しく動作するようになります。
  • 注意点:
    • すでにこの不具合を回避するために、生 SQL や Arel.sql を使って無理やりエイリアスを付けていた場合、二重にエイリアスが付いてしまう等の影響が出る可能性があります。
      • 例: すでに "(#{union.to_sql}) some_models" のような文字列を渡している場合など。
    • DB ごとにサブクエリ + UNION + エイリアスのサポート状況・制約が異なる可能性はありますが、一般的な RDBMS(PostgreSQL, MySQL 等)では問題なく動作する構成です。

  1. 参考情報 (あれば)
ruby
union = User.select(:id, :name).where(active: true)
            .union(User.select(:id, :name).where(guest: true))

# UNION 結果を from に乗せ替えつつエイリアス user_union として扱う
User.from(union, :user_union)
    .where("user_union.name LIKE ?", "%foo%")

このような「複雑な条件を UNION でまとめてから ActiveRecord のクエリチェーンを続ける」パターンが、より安全かつ自然に書けるようになります。


#57227 Fix duplicate entries accumulating in aliases_by_attribute_name

マージ日: 2026/4/25 | 作成者: @njakobsen

  1. 概要 (1-2文で)
    alias_attribute が同じエイリアスを何度も登録した場合に内部配列に重複が蓄積し、その結果 define_attribute_methods が同じメソッドを何度も再生成してしまう問題を修正する PR です。Rails 7.1 以降で table_name を繰り返し変更するようなパターンで、クエリ時間やオブジェクト生成数がどんどん増大していた回帰バグを解消します。

  1. 変更内容の詳細

問題の背景

  • 対象メソッド: ActiveModel::AttributeMethods#alias_attribute
  • Rails 7.1 からの振る舞い:
    • alias_attributealiases_by_attribute_name[old_name]new_name を単純に << していた。
    • 重複チェックがないため、同じクラスで同じ alias_attribute を複数回呼ぶと、内部配列に同じ文字列が何度も入り続ける。
  • 同時に、Rails 7.1 では以下の変更が入っている:
    • ActiveRecord::Base#load_schema! が、id カラムごとに毎回 alias_attribute :id_value, :id を自動で呼ぶようになった。
    • aliases_by_attribute_name が永続的なストレージ構造として導入され、クラスに紐づいて蓄積されるようになった。

結果として、

  • 再利用可能な AR クラスに対して self.table_name = ... を繰り返し変更すると、その都度 load_schema!alias_attribute :id_value, :id が走る。
  • そのたびに aliases_by_attribute_name["id"]"id_value" が追記され、
    ["id_value"]["id_value", "id_value"]["id_value", "id_value", "id_value"] … と増えていく。
  • define_attribute_methodsaliases_by_attribute_name をもとに alias メソッドを生成するため、
    "id_value" が N 個あれば N 回同じパターンの処理が走る。
  • ベンチマーク例では、約 1000 回の table_name= 切り替えで、1 クエリあたりのコストが ~12ms → ~90ms、オブジェクト割り当てが ~41k → ~246k 個まで増加していた。

Rails 7.0 以前は:

  • alias_attribute がその場でメソッドを定義しており(attribute_method_matchers.each 内で)、aliases_by_attribute_name 自体が存在しなかった。
  • id_value の自動 alias もなかったため、同じパターンでも実質的に問題は顕在化していなかった。

修正内容

alias_attribute 内で aliases_by_attribute_name に値を追加する際に、重複を避けるように変更されています。

元のロジック(イメージ):

ruby
aliases_by_attribute_name[old_name] << new_name

修正後:

ruby
aliases = aliases_by_attribute_name[old_name]
aliases << new_name unless aliases.include?(new_name)

これにより、同じ (old_name, new_name) の組み合わせで alias_attribute を何度呼んでも、aliases_by_attribute_name[old_name] の中身は一意な new_name のみが残り、配列が肥大化しません。

その他の点:

  • attribute_aliases.merge(new_name => old_name) はもともと idempotent(同じ値で再度 merge しても結果が変わらない)なのでそのまま。
  • eagerly_generate_alias_attribute_methods は複数回呼ばれても安全:
    • 内部で呼ばれる define_attribute_method_pattern が、instance_method_already_implemented? を見て早期 return するため、既存メソッドを再定義しない。

追加されたテスト

  1. ActiveModel 単体テスト

    • 場所: activemodel/test/cases/attribute_methods_test.rb
    • 内容:
      • あるクラスで alias_attribute :bar, :foo を3回呼ぶ。
      • aliases_by_attribute_name["foo"]["bar"] だけであることを検証。
    • 未パッチ時:
      • 実際の値は ["bar", "bar", "bar"] となりテスト失敗。
    • パッチ適用後:
      • ["bar"] となりテスト成功。
  2. ActiveRecord 統合テスト

    • 場所: activerecord/test/cases/attribute_methods_test.rb
    • 内容:
      • 匿名の ActiveRecord::Base サブクラスを作成。
      • table_nametopicsposts の間で切り替えながら .first を実行する、という実際の再現パターンに近い流れをテスト。
      • 3回クエリ実行後の aliases_by_attribute_name["id"]["id_value"] だけであることを検証。
    • 未パッチ時:
      • ["id_value", "id_value"] など重複が残りテスト失敗。
    • パッチ適用後:
      • ["id_value"] となり成功。

全体として:

  • activemodel フルスイート (1168 tests)
  • activerecord sqlite3 フルスイート (9418 tests)

において、新たな失敗は発生していないことが確認されています。


  1. 影響範囲・注意点

影響を受ける主なケース:

  • Rails 7.1 以降を利用している。
  • 同じモデルクラスに対して:
    • alias_attribute を同じ引数で複数回呼んでいる、または
    • table_name を動的に切り替えるクラス(マルチテナント実装やシャーディングなど)で、内部的に load_schema!alias_attribute :id_value, :id が何度も呼ばれている。

この PR による挙動変化:

  • aliases_by_attribute_name[old_name] に格納される new_name の配列が「重複なし」になる。
  • すでに定義済みの alias メソッドの有無にかかわらず、API 上の表面挙動 (alias_attribute 呼び出しの結果としてアクセスできるメソッド) は変わらない。
  • 変わるのは内部状態(配列の中身)と、define_attribute_methods 実行時のパフォーマンスのみ。

後方互換性・リスク:

  • 既存アプリ側が aliases_by_attribute_name の中の重複を前提にしている可能性はほぼなく、通常は内部実装依存なので、互換性リスクは極小。
  • 外から見えるメソッド定義結果も変わらないため、挙動の意味的な変化はありません(バグ修正のみ)。
  • CHANGELOG は未更新で、「軽微なバグ修正」として扱われています。

この修正により期待できる効果:

  • 長時間動作するプロセスで table_name を動的に変更するようなコードの性能劣化・メモリ断続増加が抑制される。
  • モデルが多く、alias_attribute を多用しているアプリでも、同じ alias を再設定してしまった場合のオーバーヘッドがなくなる。

  1. 参考情報 (あれば)

この PR (#57227) 自体は、aliases_by_attribute_name の重複排除という一点に絞った小さい変更で、性能回復と内部状態の健全化を目的としたバグフィックスです。


#57245 Fix find_signed for models with a composite primary key

マージ日: 2026/4/25 | 作成者: @55728

  1. 概要 (1-2文で)
    ActiveRecord::SignedId#find_signed が複合主キー(CPK)を持つモデルで ArgumentError を投げていた不具合を修正し、find_signed! および通常の find と同等に動作するようにした PR です。これにより、サインインリンクやパスワードリセットなどで CPK モデルの signed_id を安全に利用できます。

  1. 変更内容の詳細

不具合の内容

対象: ActiveRecord::SignedId::ClassMethods#find_signed

前提となるモデル:

ruby
class Cpk::Order < ActiveRecord::Base
  self.primary_key = [:shop_id, :id]
end

order = Cpk::Order.first
token = order.signed_id

挙動:

ruby
Cpk::Order.find_signed!(token)
# => 正常にレコードを返す

Cpk::Order.find_signed(token)
# => ArgumentError: Expected corresponding value for ["shop_id", "id"] to be an Array

原因は find_signed 内部での検索条件の組み立て方です。

旧実装(問題箇所のイメージ)

ruby
if id = signed_id_verifier.verified(...)
  find_by(primary_key => id)
end
  • CPK の場合:
    • primary_key["shop_id", "id"]
    • id[37647470, 75294940] のような配列
  • find_by(["shop_id", "id"] => [37647470, 75294940]) の形になり、PredicateBuilder は
    • 「キーが配列 ⇒ 複数レコード指定」と解釈
    • 値も「配列の配列」を期待する([[shop_id, id], [shop_id, id], ...]
    • 実際は一次元配列なので ArgumentError を投げる

一方 find_signed!find(id) を使い、finder_methods.rb 内で

ruby
primary_key.zip(id).to_h
# => { shop_id: 37647470, id: 75294940 }

の形に変換してから検索するため問題が出ていませんでした。

今回の修正内容

find_signed 側でも CPK を意識した条件生成を行うよう変更されています(実際のコードは 1 行差分ですが、意図としては以下のようなロジック):

ruby
if id = signed_id_verifier.verified(...)
  if composite_primary_key?
    # CPK の場合: 各主キー列と値をペアにする
    find_by(primary_key.zip(id).to_h)
    # => { shop_id: 37647470, id: 75294940 } のような Hash になり、通常の where と同じ扱い
  else
    # 単一主キーの場合は従来どおり
    find_by(primary_key => id)
  end
end

ポイント:

  • CPK 時に primary_key(配列)と id(配列)を zip して Hash に変換することで、
    • PredicateBuilder から見て「普通の Hash 条件」になる
    • where(shop_id: ..., id: ...) と同等の条件で検索される
  • find ではなく find_by を使い続けている理由:
    • RelationMethods#find_signed でスコープ付きに使えることを維持するため
      (例: Cpk::Order.where(active: true).find_signed(token)

token_for との整合性

  • 過去に find_by_token_for に同種のバグがあり、primary_key => idprimary_key => [id] に修正済み
  • 今回は同じ「CPK + PredicateBuilder」の問題を、より明示的な zip による Hash 生成で解決している
    • どちらのアプローチも最終的な SQL は同じ
    • zip の方が「各キーと値を 1:1 に対応させている」と読み取りやすく、find_one の既存実装とも揃う

テスト

signed_id_test.rb に以下のテストが追加されています:

  1. CPK モデルでの find_signed ラウンドトリップ
    • record.signed_idfind_signed(token) で元のレコードが取得できること
  2. CPK + Relation での find_signed
    • スコープに合致する場合はレコードを返す
    • スコープに合致しない場合は nil を返す
  3. CPK での find_signed! ラウンドトリップ(リグレッション防止)

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

    • self.primary_key = [:col1, :col2, ...] のように複合主キーを使っているモデルで
    • signed_id / find_signed を利用しているアプリ
      • 例: マジックリンクサインイン、パスワードリセット、メールアドレス確認など
  • 以前との挙動差:

    • これまでは find_signedArgumentError で落ちていたパスが、正しくレコードを返すようになる
    • find_signed! の挙動・例外発生条件は従来どおり(該当レコードがなければ ActiveRecord::RecordNotFound
  • 互換性:

    • 単一主キーのモデルに対する挙動は変更なし
    • CPK モデルでもクエリの条件自体は「主キーの各カラムを等価条件で検索」という素直な形なので、意図しない結果に変わる可能性は低い
    • 既に find_signed を rescue するワークアラウンド(ArgumentError を拾うなど)を入れていた場合、その処理は不要になるか、挙動が変わる可能性がある
  • パフォーマンス:

    • 追加される処理は primary_key.zip(id).to_h のみで、レコード 1 件の検索に対してごく軽微
    • 実行される SQL は従来 find_signed! が発行していたものと同等

  1. 参考情報 (あれば)

#57220 Guides: remove outdated link to Action Cable examples

マージ日: 2026/4/22 | 作成者: @tjschuck

  1. 概要 (1-2文で)
    Railsガイド「Action Cable Overview」から、9年間更新されておらずアーカイブされた外部リポジトリ(actioncable-examples)へのリンクが削除されました。これにより、公式ガイドから古い・メンテされていない情報源への誘導がなくなります。

  1. 変更内容の詳細

対象ファイル:

  • guides/source/action_cable_overview.md

変更内容:

  • 「More complete examples(より完全なサンプル)」として案内されていた GitHub リポジトリ https://github.com/rails/actioncable-examples へのリンクが削除されました。
  • 実質的には、Action Cable ガイド内の「より大きな例/詳細なサンプルコードはこちら」といった説明行が5行分なくなっただけで、コードやAPIの説明文自体には手が入っていません。

イメージとしては、ガイド中の以下のような記述が削除された形です(擬似例):

md
## More complete examples

You can find more complete examples in the following repository:
https://github.com/rails/actioncable-examples

実際の差分は +0 / -5 行であり、新たなリンク先や代替コンテンツの追加は行われていません。


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

    • 公式ガイド(Action Cable Overview)を読んだ際に、外部のサンプル集への導線がなくなります。
    • Rails自体のコード、Action Cable のAPI、挙動には一切変更がありません。アプリケーションコードへの影響はゼロです。
    • すでに rails/actioncable-examples をブックマークしている人や、過去のガイドからリンクを踏めていた人には変化はありません(リポジトリ自体は存在し、アーカイブ済みという状態)。
  • 注意点 / 開発者が意識すべきこと

    • 公式ガイドから「推奨されるサンプル実装」としてはもはや扱われていない、というメッセージと受け取れます。
    • actioncable-examples は Rails の古いバージョンを前提にしている可能性が高く、現行バージョンのベストプラクティスとは乖離していると考えた方が安全です。
    • Action Cable のサンプルやベストプラクティスを探す場合は:
      • 現行の Rails ガイド(Action Cable Overview, Getting Started with Rails + Action Cable の章など)
      • Rails の公式リポジトリ内のテスト・サンプル
      • 信頼できるブログ・書籍・チュートリアル(Rails バージョンを明示しているもの) を優先するのがよいです。

  1. 参考情報 (あれば)

#57212 [ci skip] Fix a typo in the ActionMailbox basics docs

マージ日: 2026/4/22 | 作成者: @mgriffin

  1. 概要 (1-2文で)
    Action Mailbox の「Basics」ガイド内にあった英文のタイポ(andan)を修正するドキュメント専用のPRです。コードや挙動の変更は一切なく、テストガイドの文章を自然な英語に整えています。

  2. 変更内容の詳細

  • 対象ファイル: guides/source/action_mailbox_basics.md
  • 変更内容は1行のみで、テストヘルパーを使った受信メールテストの説明文から、文法上の誤りを修正しています。

修正前(イメージ):

md
Here is and example of testing an inbound email with Action Mailbox TestHelpers

修正後:

md
Here is an example of testing an inbound email with Action Mailbox TestHelpers

意味としては「Action Mailbox の TestHelpers を使って受信メールをテストする例は次のとおりです」という文で、and というタイプミスを正しい冠詞 an に直しただけです。

  1. 影響範囲・注意点
  • ランタイム動作への影響なし
    • アプリケーションコード、Action Mailbox の実装、テストコードには一切変更がありません。
    • バージョンアップに伴う互換性問題やマイグレーションは不要です。
  • 開発者ドキュメントの読みやすさ向上のみ
    • 英語ドキュメントが少し読みやすくなった、というレベルの変更です。
    • 既存のガイドの手順・サンプルコードの意味は変わりません。
  1. 参考情報 (あれば)
  • PR: https://github.com/rails/rails/pull/57212
  • 関連ドキュメント: Action Mailbox Basics ガイド
    • Action Mailbox のテストに関するセクションで、ActionMailbox::TestHelper などを使った受信メールテストの書き方を説明している部分の文言修正です。

#55282 Bump required PostgreSQL version to 10.0

マージ日: 2026/4/22 | 作成者: @yahonda

  1. 概要 (1-2文で)
    Rails が公式にサポートする PostgreSQL の最小バージョンを「9.3 以上」から「10 以上」に引き上げた PR です。これにより、今後リリースされる PostgreSQL 18 以降や pg gem 1.6.x との互換性を確保します。

  1. 変更内容の詳細

なぜ「10.0 以上」が必要になったか

  • PostgreSQL 18 で「キャンセルリクエストキー」のフォーマットが変更され、Rails のテスト(ActiveRecord::PostgresqlTransactionTest#test_raises_Interrupt_when_canceling_statement_via_interrupt)が失敗する問題が発生。
  • この問題自体は pg gem 側で修正済み(pg 1.6.0 に含まれる)。
  • しかし、pg 1.6 が PostgreSQL 10 以上を必須とする変更を入れたため(https://github.com/ged/ruby-pg/pull/606)、Rails が「PostgreSQL 9.3+」を名乗り続けると、Rails・pg・PostgreSQL 18 の組み合わせで整合性が取れなくなる。
  • そこで Rails 側もサポート表記とコードを更新し、最低要件を PostgreSQL 10 に引き上げた、という経緯です。

主なコード/設定の変更ポイント

  1. ActiveRecord の PostgreSQL アダプタ周り

    • postgresql_adapter.rb / schema_statements.rb などで、
      • 9.3〜9.6 など「古い PostgreSQL バージョンを考慮した分岐・ワークアラウンド」を削除・簡略化。
      • 「10 以降で常に有効」な仕様を前提にした実装に整理。
    • 例:
      • 旧バージョンを考慮していたバージョンチェック(postgresql_version < 100000 のような条件分岐)が消え、単純化されている。
      • 一部の型・機能(UUID, enum, geometric 型など)に対するテストやスキーマ定義から、古いバージョン専用パスが削除。
  2. テストコードの整理

    • activerecord/test/cases/adapters/postgresql/*.rb の複数ファイルで、
      • 「9.x 以下ではスキップ」「9.3 だとこう振る舞う」等の条件付きテストを削除。
      • PostgreSQL 10 以降を前提とした期待値に統一。
    • uuid_test.rb などで行数の増減が大きいのは、古いバージョン向けの分岐削除と、それに応じたテストケースの再編が主な理由です。
  3. スキーマ定義

    • activerecord/test/schema/postgresql_specific_schema.rb でも、古いバージョンに合わせたスキーマ定義や条件分岐を削除し、10 以降で前提となる仕様に合わせて整理。
  4. ドキュメントとテンプレート

    • guides/source/active_record_postgresql.md
    • guides/source/command_line.md
    • railties/.../config/databases/postgresql.yml.tt などで、サポートバージョン表記やサンプル設定を「PostgreSQL 10 以上」を前提とした内容に更新。
    • 新しく Rails アプリを rails new したときに生成される config/database.yml の PostgreSQL テンプレートも、PostgreSQL 10 を前提にした内容になります。
  5. CHANGELOG の更新

    • activerecord/CHANGELOG.md
      • 「Active Record がサポートする PostgreSQL の最小バージョンを 10 に引き上げた」
      • という旨のエントリが追加されています。
        → バージョンアップ時の破壊的変更(breaking change)として明示。

  1. 影響範囲・注意点

  2. アプリケーション側の必須要件の変更

    • Rails(main / 次期メジャー)+ PostgreSQL アダプタを利用する場合、本番/開発環境とも PostgreSQL 10 以上が必須になります。
    • 9.x 系(9.6, 9.5, 9.4, 9.3 など)を使い続けている場合:
      • このバージョンの Rails にはアップグレードできません。
      • もしくは PostgreSQL を 10 以上に上げる必要があります。
  3. pg gem のバージョンとの整合性

    • pg 1.6 以降を使う場合:
      • pg 自体が PostgreSQL 10 以上を要求するため、Rails 側のこの変更と整合します。
    • PostgreSQL 18(またはそれ以降)を使う予定なら、**Rails + pg 1.6+ + PostgreSQL 10+**の組み合わせが前提になります。
    • 逆に「PostgreSQL 9.x + Rails main + 最新 pg」を混ぜる構成は、サポート外かつ実質的に動作しません。
  4. テスト/開発環境の CI 設定

    • CI(GitHub Actions, CircleCI など)で PostgreSQL 9.x を使っている場合は、ジョブ定義を更新して 10 以上を使うように変更する必要があります。
    • テストコード側から「古いバージョン向けのワークアラウンド」が消えているため、CI を更新しないとテストがそもそも通らない or 動かない可能性があります。
  5. 古い PostgreSQL 向けの後方互換性の喪失

    • これまで非公式に「たまたま動いていた」ような 9.x 環境での挙動は、この PR 以降は保証されません。
    • 古いサーバへの接続、マイグレーション、特定の型(UUID, enum, geometric 型など)に絡む挙動についても、9.x で問題が出たとしてもバグとはみなされない前提になります。

  1. 参考情報 (あれば)

この PR を前提に Rails を使う場合は、「PostgreSQL 本体のバージョンポリシーを 10+ に揃える」「pg gem を 1.6+ に上げる」ことをセットで検討しておくとスムーズです。