default_orderの逆順が正しく機能するように reverse_order! を改善
Rails の default_order が適用された Relation に対して reverse_order を呼び出すと、暗黙的に主キーの降順が適用され、期待した逆順が得られませんでした。
背景
default_order のみでソートされた Relation に reverse_order を実行すると、order_values が空になるため内部的にプライマリキーの DESC が使用され、SQL は "models"."id" DESC となります。これは default_order を無視した結果 であり、暗黙のバグとしてユーザーに誤った順序を返していました。実際の挙動は次の通りです。
Model.default_order("name ASC").to_sql
# => "... ORDER BY name ASC"
Model.default_order("name ASC").reverse_order.to_sql
# => "... ORDER BY \"models\".\"id\" DESC" # ❌
reverse_order! が order_values のみを逆転させ、default_order_values を参照しない 実装であったことが根本原因です。build_order は空の order_values の場合に default_order_values へフォールバックしますが、reverse_order! はそのロジックを継承していませんでした。
技術的な変更
activerecord/lib/active_record/relation/query_methods.rb の reverse_order! 実装に条件分岐を追加し、order_values が空でかつ default_order_values が存在する場合に default_order_values を逆転 させるようにしました。変更前後のコードは次の通りです。
変更前:
def reverse_order! # :nodoc:
orders = order_values.compact_blank
self.order_values = reverse_sql_order(orders)
self
end
変更後:
def reverse_order! # :nodoc:
orders = order_values.compact_blank
default_orders = default_order_values.compact_blank
if orders.empty? && default_orders.any?
self.default_order_values = reverse_sql_order(default_orders)
else
self.order_values = reverse_sql_order(orders)
end
self
end
このロジックは、order_values が無いケースで default_order_values をそのまま逆順に変換 し、default_order_values に再設定します。結果として build_order のフォールバックパスと同様の挙動が保証され、明示的な order が存在する場合は従来通り order_values が逆転されます。また、既存の API 互換性は保たれ、default_order だけを使用しているコードが破壊されることはありません。
テストは activerecord/test/cases/relations_test.rb に追加され、default_order を逆転した Relation の SQL が ORDER BY title DESC を含むこと、そして ids が期待通り一致することが検証されています。
設計判断
本修正は 既存の ordering 基盤に最小限の分岐を加える という方針で実装されました。reverse_order! が default_order_values を捨てていた点は、build_order と非対称であり、コードベース全体の一貫性を欠いていました。そこで、default_order_values を意識したフォールバックロジック を取り入れ、他の ordering メソッドと同じ判定基準を共有させました。この変更により
-
reverse_orderが暗黙の主キー順序に退行しなくなる - 後続の
order呼び出しでdefault_orderが上書き可能なまま残る - 既存の API 互換性が維持され、ユーザーコードへの影響がゼロになる
というトレードオフが実現されています。実装規模は 7 行の追加で済み、リレーション層の挙動を統一するという設計上の利点が得られました。
まとめ
reverse_order! が default_order を正しく逆転 しないバグを、default_order_values を考慮した分岐で修正しました。これにより、デフォルトソートのみの Relation でも期待通りの逆順が生成され、SQL の予測可能性が向上します。変更は裏側の ordering ロジックと整合性を取るだけで、既存コードへの影響はありません。