Skip to content

Commit

Permalink
Merge pull request #23377 from bogdan/last-with-sql
Browse files Browse the repository at this point in the history
Fix AR::Relation#last bugs instroduced in 7705fc
  • Loading branch information
eileencodes committed Feb 13, 2016
2 parents 4e80c12 + b679916 commit 7ee2002
Show file tree
Hide file tree
Showing 4 changed files with 78 additions and 17 deletions.
21 changes: 21 additions & 0 deletions activerecord/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
* Rework `ActiveRecord::Relation#last`

1. Never perform additional SQL on loaded relation
2. Use SQL reverse order instead of loading relation if relation doesn't have limit
3. Deprecated relation loading when SQL order can not be automatically reversed

Topic.order("title").load.last(3)
# before: SELECT ...
# after: No SQL

Topic.order("title").last
# before: SELECT * FROM `topics`
# after: SELECT * FROM `topics` ORDER BY `topics`.`title` DESC LIMIT 1

Topic.order("coalesce(author, title)").last
# before: SELECT * FROM `topics`
# after: Deprecation Warning for irreversible order

*Bogdan Gusiev*


* Allow `joins` to be unscoped.

Closes #13775.
Expand Down
28 changes: 19 additions & 9 deletions activerecord/lib/active_record/relation/finder_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,15 +145,21 @@ def first!
#
# [#<Person id:4>, #<Person id:3>, #<Person id:2>]
def last(limit = nil)
if limit
if order_values.empty? && primary_key
order(arel_attribute(primary_key).desc).limit(limit).reverse
else
to_a.last(limit)
end
else
find_last
end
return find_last(limit) if loaded? || limit_value

result = limit(limit || 1)
result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key
result = result.reverse_order!

limit ? result.reverse : result.first
rescue ActiveRecord::IrreversibleOrderError
ActiveSupport::Deprecation.warn(<<-WARNING.squish)
Finding a last element by loading the relation when SQL ORDER
can not be reversed is deprecated.
Rails 5.1 will raise ActiveRecord::IrreversibleOrderError in this case.
Please call `to_a.last` if you still want to load the relation.
WARNING
find_last(limit)
end

# Same as #last but raises ActiveRecord::RecordNotFound if no record
Expand Down Expand Up @@ -578,5 +584,9 @@ def find_nth_with_limit_and_offset(index, limit, offset:) # :nodoc:
find_nth_with_limit(index, limit)
end
end

def find_last(limit)
limit ? to_a.last(limit) : to_a.last
end
end
end
2 changes: 2 additions & 0 deletions activerecord/lib/active_record/relation/query_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1112,6 +1112,8 @@ def reverse_sql_order(order_query)

order_query.flat_map do |o|
case o
when Arel::Attribute
o.desc
when Arel::Nodes::Ordering
o.reverse
when String
Expand Down
44 changes: 36 additions & 8 deletions activerecord/test/cases/finder_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -516,16 +516,44 @@ def test_last_with_integer_and_order_should_keep_the_order
assert_equal Topic.order("title").to_a.last(2), Topic.order("title").last(2)
end

def test_last_with_integer_and_order_should_not_use_sql_limit
query = assert_sql { Topic.order("title").last(5).entries }
assert_equal 1, query.length
assert_no_match(/LIMIT/, query.first)
def test_last_with_integer_and_order_should_use_sql_limit
relation = Topic.order("title")
assert_queries(1) { relation.last(5) }
assert !relation.loaded?
end

def test_last_with_integer_and_reorder_should_not_use_sql_limit
query = assert_sql { Topic.reorder("title").last(5).entries }
assert_equal 1, query.length
assert_no_match(/LIMIT/, query.first)
def test_last_with_integer_and_reorder_should_use_sql_limit
relation = Topic.reorder("title")
assert_queries(1) { relation.last(5) }
assert !relation.loaded?
end

def test_last_on_loaded_relation_should_not_use_sql
relation = Topic.limit(10).load
assert_no_queries do
relation.last
relation.last(2)
end
end

def test_last_with_irreversible_order
assert_deprecated do
Topic.order("coalesce(author_name, title)").last
end
end

def test_last_on_relation_with_limit_and_offset
post = posts('sti_comments')

comments = post.comments.order(id: :asc)
assert_equal comments.limit(2).to_a.last, comments.limit(2).last
assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2)
assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3)

comments = comments.offset(1)
assert_equal comments.limit(2).to_a.last, comments.limit(2).last
assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2)
assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3)
end

def test_take_and_first_and_last_with_integer_should_return_an_array
Expand Down

0 comments on commit 7ee2002

Please sign in to comment.