ActiveRecord::Relation#order supports hash like ActiveRecord::Relation#where (#50000)

* relation#order supports hash like relation#where

This allows for an ActiveRecord::Relation to take a hash such as
`Topic.includes(:posts).order(posts: { created_at: :desc })`

* use is_a? to support subclasses of each

Co-authored-by: Rafael Mendonça França <rafael@rubyonrails.org>
This commit is contained in:
mylesboone 2024-02-21 12:43:15 -05:00 committed by GitHub
parent db4c6db59d
commit 278d6574cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 110 additions and 15 deletions

@ -505,4 +505,12 @@
*Tristan Fellows*
* Allow for more complex hash arguments for `order` which mimics `where` in `ActiveRecord::Relation`.
```ruby
Topic.includes(:posts).order(posts: { created_at: :desc })
```
*Myles Boone*
Please check [7-1-stable](https://github.com/rails/rails/blob/7-1-stable/activerecord/CHANGELOG.md) for previous changes.

@ -1918,7 +1918,9 @@ def validate_order_args(args)
args.each do |arg|
next unless arg.is_a?(Hash)
arg.each do |_key, value|
unless VALID_DIRECTIONS.include?(value)
if value.is_a?(Hash)
validate_order_args([value])
elsif VALID_DIRECTIONS.exclude?(value)
raise ArgumentError,
"Direction \"#{value}\" is invalid. Valid directions are: #{VALID_DIRECTIONS.to_a.inspect}"
end
@ -1926,9 +1928,13 @@ def validate_order_args(args)
end
end
def flattened_args(order_args)
order_args.flat_map { |e| (e.is_a?(Hash) || e.is_a?(Array)) ? flattened_args(e.to_a) : e }
end
def preprocess_order_args(order_args)
@klass.disallow_raw_sql!(
order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a },
flattened_args(order_args),
permit: connection.column_name_with_order_matcher
)
@ -1943,14 +1949,20 @@ def preprocess_order_args(order_args)
when Symbol
order_column(arg.to_s).asc
when Hash
arg.map { |field, dir|
case field
when Arel::Nodes::SqlLiteral, Arel::Nodes::Node, Arel::Attribute
field.public_send(dir.downcase)
arg.map do |key, value|
if value.is_a?(Hash)
value.map do |field, dir|
order_column([key.to_s, field.to_s].join(".")).public_send(dir.downcase)
end
else
order_column(field.to_s).public_send(dir.downcase)
case key
when Arel::Nodes::SqlLiteral, Arel::Nodes::Node, Arel::Attribute
key.public_send(value.downcase)
else
order_column(key.to_s).public_send(value.downcase)
end
end
}
end
else
arg
end
@ -1964,18 +1976,20 @@ def sanitize_order_arguments(order_args)
end
def column_references(order_args)
references = order_args.flat_map do |arg|
order_args.flat_map do |arg|
case arg
when String, Symbol
arg
when Hash
arg.keys.map do |key|
key if key.is_a?(String) || key.is_a?(Symbol)
end
arg.keys.select { |e| e.is_a?(String) || e.is_a?(Symbol) }
end
end
references.map! { |arg| arg =~ /^\W?(\w+)\W?\./ && $1 }.compact!
references
end.filter_map do |arg|
arg =~ /^\W?(\w+)\W?\./ && $1
end +
order_args
.select { |e| e.is_a?(Hash) }
.flat_map { |e| e.map { |k, v| k if v.is_a?(Hash) } }
.compact
end
def order_column(field)

@ -0,0 +1,65 @@
# frozen_string_literal: true
require "cases/helper"
require "models/author"
require "models/book"
class OrderTest < ActiveRecord::TestCase
fixtures :authors
def test_order_asc
Book.destroy_all
z = Book.create!(name: "Zulu", author: authors(:david))
y = Book.create!(name: "Yankee", author: authors(:mary))
x = Book.create!(name: "X-Ray", author: authors(:david))
alphabetical = [x, y, z]
assert_equal(alphabetical, Book.order(name: :asc))
assert_equal(alphabetical, Book.order(name: :ASC))
assert_equal(alphabetical, Book.order(name: "asc"))
assert_equal(alphabetical, Book.order(:name))
assert_equal(alphabetical, Book.order("name"))
assert_equal(alphabetical, Book.order("books.name"))
assert_equal(alphabetical, Book.order(Book.arel_table["name"]))
assert_equal(alphabetical, Book.order(books: { name: :asc }))
end
def test_order_desc
Book.destroy_all
z = Book.create!(name: "Zulu", author: authors(:david))
y = Book.create!(name: "Yankee", author: authors(:mary))
x = Book.create!(name: "X-Ray", author: authors(:david))
reverse_alphabetical = [z, y, x]
assert_equal(reverse_alphabetical, Book.order(name: :desc))
assert_equal(reverse_alphabetical, Book.order(name: :DESC))
assert_equal(reverse_alphabetical, Book.order(name: "desc"))
assert_equal(reverse_alphabetical, Book.order(:name).reverse_order)
assert_equal(reverse_alphabetical, Book.order("name desc"))
assert_equal(reverse_alphabetical, Book.order("books.name desc"))
assert_equal(reverse_alphabetical, Book.order(Book.arel_table["name"].desc))
assert_equal(reverse_alphabetical, Book.order(books: { name: :desc }))
end
def test_order_with_association
Book.destroy_all
z = Book.create!(name: "Zulu", author: authors(:david))
y = Book.create!(name: "Yankee", author: authors(:mary))
x = Book.create!(name: "X-Ray", author: authors(:david))
author_then_book_name = [x, z, y]
assert_equal(author_then_book_name, Book.includes(:author).order(authors: { name: :asc }, books: { name: :asc }))
assert_equal(author_then_book_name, Book.includes(:author).order("authors.name", books: { name: :asc }))
assert_equal(author_then_book_name, Book.includes(:author).order("authors.name", "books.name"))
assert_equal(author_then_book_name, Book.includes(:author).order({ authors: { name: :asc } }, Book.arel_table[:name]))
author_desc_then_book_name = [y, x, z]
assert_equal(author_desc_then_book_name, Book.includes(:author).order(authors: { name: :desc }, books: { name: :asc }))
assert_equal(author_desc_then_book_name, Book.includes(:author).order("authors.name desc", books: { name: :asc }))
assert_equal(author_desc_then_book_name, Book.includes(:author).order({ authors: { name: :desc } }, :name))
end
end

@ -912,6 +912,14 @@ irb> Book.order("title ASC").order("created_at DESC")
SELECT * FROM books ORDER BY title ASC, created_at DESC
```
You can also order from a joined table
```ruby
Book.includes(:author).order(books: { print_year: :desc }, authors: { name: :asc })
# OR
Book.includes(:author).order('books.print_year desc', 'authors.name asc')
```
WARNING: In most database systems, on selecting fields with `distinct` from a result set using methods like `select`, `pluck` and `ids`; the `order` method will raise an `ActiveRecord::StatementInvalid` exception unless the field(s) used in `order` clause are included in the select list. See the next section for selecting fields from the result set.
Selecting Specific Fields