Skip to content

Commit 14540a5

Browse files
authored
Merge pull request rails#42061 from kddnewton/ar-in-order-of
Add `ActiveRecord::QueryMethods#in_order_of`.
2 parents 6068639 + 459ca22 commit 14540a5

File tree

6 files changed

+88
-2
lines changed

6 files changed

+88
-2
lines changed

activerecord/CHANGELOG.md

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,29 @@
1+
* Add `ActiveRecord::QueryMethods#in_order_of`.
2+
3+
This allows you to specify an explicit order that you'd like records
4+
returned in based on a SQL expression. By default, this will be accomplished
5+
using a case statement, as in:
6+
7+
```ruby
8+
Post.in_order_of(:id, [3, 5, 1])
9+
```
10+
11+
will generate the SQL:
12+
13+
```sql
14+
SELECT "posts".* FROM "posts" ORDER BY CASE "posts"."id" WHEN 3 THEN 1 WHEN 5 THEN 2 WHEN 1 THEN 3 ELSE 4 END ASC
15+
```
16+
17+
However, because this functionality is built into MySQL in the form of the
18+
`FIELD` function, that connection adapter will generate the following SQL
19+
instead:
20+
21+
```sql
22+
SELECT "posts".* FROM "posts" ORDER BY FIELD("posts"."id", 1, 5, 3) DESC
23+
```
24+
25+
*Kevin Newton*
26+
127
* Fix `eager_loading?` when ordering with `Symbol`
228

329
`eager_loading?` is triggered correctly when using `order` with symbols.
@@ -34,7 +60,7 @@
3460

3561
*Luis Vasconcellos*, *Eileen M. Uchitelle*
3662

37-
* Fix `eager_loading?` when ordering with `Hash` syntax.
63+
* Fix `eager_loading?` when ordering with `Hash` syntax
3864

3965
`eager_loading?` is triggered correctly when using `order` with hash syntax
4066
on an outer table.

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,15 @@ def database_version # :nodoc:
632632
def check_version # :nodoc:
633633
end
634634

635+
def field_ordered_value(column, values)
636+
node = Arel::Nodes::Case.new(column)
637+
values.each.with_index(1) do |value, order|
638+
node.when(value).then(order)
639+
end
640+
641+
Arel::Nodes::Ascending.new(node.else(values.length + 1))
642+
end
643+
635644
class << self
636645
private
637646
def initialize_type_map(m)

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ def supports_insert_on_duplicate_update?
137137
true
138138
end
139139

140+
def field_ordered_value(column, values)
141+
field = Arel::Nodes::NamedFunction.new("FIELD", [column, values.reverse])
142+
Arel::Nodes::Descending.new(field)
143+
end
144+
140145
def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
141146
query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
142147
end

activerecord/lib/active_record/querying.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module Querying
1212
:create_or_find_by, :create_or_find_by!,
1313
:destroy_all, :delete_all, :update_all, :touch_all, :destroy_by, :delete_by,
1414
:find_each, :find_in_batches, :in_batches,
15-
:select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
15+
:select, :reselect, :order, :in_order_of, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
1616
:where, :rewhere, :invert_where, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly,
1717
:and, :or, :annotate, :optimizer_hints, :extending,
1818
:having, :create_with, :distinct, :references, :none, :unscope, :merge, :except, :only,

activerecord/lib/active_record/relation/query_methods.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,23 @@ def order!(*args) # :nodoc:
387387
self
388388
end
389389

390+
# Allows to specify an order by a specific set of values. Depending on your
391+
# adapter this will either use a CASE statement or a built-in function.
392+
#
393+
# User.in_order_of(:id, [1, 5, 3])
394+
# # SELECT "users".* FROM "users" ORDER BY FIELD("users"."id", 1, 5, 3)
395+
#
396+
def in_order_of(column, values)
397+
klass.disallow_raw_sql!([column], permit: connection.column_name_with_order_matcher)
398+
399+
references = column_references([column])
400+
self.references_values |= references unless references.empty?
401+
402+
column = order_column(column.to_s) if column.is_a?(Symbol)
403+
404+
spawn.order!(connection.field_ordered_value(column, values))
405+
end
406+
390407
# Replaces any existing order defined on the relation with the specified order.
391408
#
392409
# User.order('email DESC').reorder('id ASC') # generated SQL has 'ORDER BY id ASC'
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# frozen_string_literal: true
2+
3+
require "cases/helper"
4+
require "models/post"
5+
6+
class FieldOrderedValuesTest < ActiveRecord::TestCase
7+
fixtures :posts
8+
9+
def test_in_order_of
10+
order = [3, 4, 1]
11+
posts = Post.in_order_of(:id, order).limit(3)
12+
13+
assert_equal(order, posts.map(&:id))
14+
end
15+
16+
def test_in_order_of_expression
17+
order = [3, 4, 1]
18+
posts = Post.in_order_of(Arel.sql("id * 2"), order.map { |id| id * 2 }).limit(3)
19+
20+
assert_equal(order, posts.map(&:id))
21+
end
22+
23+
def test_in_order_of_after_regular_order
24+
order = [3, 4, 1]
25+
posts = Post.where(type: "Post").order(:type).in_order_of(:id, order).limit(3)
26+
27+
assert_equal(order, posts.map(&:id))
28+
end
29+
end

0 commit comments

Comments
 (0)