-
Type:
Bug
-
Resolution: Unresolved
-
Priority:
Unknown
-
None
-
Affects Version/s: 9.0.12
-
Component/s: None
-
None
-
None
-
Ruby Drivers
-
None
-
None
-
None
-
None
-
None
-
None
`first(limit)` and `last(limit)` on a has_many association return a single document instead of `limit` documents once the association is loaded. When it is not loaded, the same call returns the expected array. The result then depends on whether something loaded the association before, such as iterating over it or inspecting it in a console.
Steps to reproduce
```ruby
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'mongoid', '9.0.12'
end
require 'mongoid'
Mongoid.configure do |config|
config.clients.default =
end
class Author
include Mongoid::Document
has_many :posts
end
class Post
include Mongoid::Document
belongs_to :author
field :title, type: String
end
Author.delete_all
Post.delete_all
author = Author.create!
3.times { |i| author.posts.create!(title: "post #
") }
unloaded = Author.find(author.id)
p unloaded.posts.last(2).map(&:title) # => ["post 2", "post 3"]
loaded = Author.find(author.id)
loaded.posts.to_a
p loaded.posts.last(2) # => #<Post title: "post 3">
p loaded.posts.first(2) # => #<Post title: "post 1">
```
Tested with Mongoid 9.0.12, Ruby 3.3 and MongoDB 8.0.
Expected behavior
`last(2)` returns the last two documents as an array, and `first(2)` the first two, whether or not the association is loaded. This is what `Criteria#first(limit)` and `Criteria#last(limit)` do, and what the `@param limit` documentation of `HasMany::Enumerable#first` and `#last` describes.
Actual behavior
When the association is loaded, both methods return a single document and ignore the limit.
Cause
In `lib/mongoid/association/referenced/has_many/enumerable.rb`, the limit is only passed to the unloaded criteria:
```ruby
def last(limit = nil)
_added.values.try(:last) ||
_loaded.try(:values).try(:last) ||
_added[(ul = _unloaded.try(:last, limit)).try(:_id)] ||
ul
end
```
When `_loaded` or `_added` has documents, the method returns `.values.last` before the limit is used. `first` has the same issue.
Suggested fix
Handle the limit in a separate path and keep the current behavior when no limit is given:
```ruby
def last(limit = nil)
return with_limit(:last, limit) if limit
# current implementation
end
private
def with_limit(method, limit)
persisted = _loaded? ? _loaded.values : _unloaded.public_send(method, limit).to_a
persisted.to_h
{ [_1._id, _1] }.merge(_added).values.public_send(method, limit)
end
```
This uses the in-memory documents when the association is loaded, still queries only `limit` documents when it is not, prefers the in-memory instance of a persisted document (as `#each` does), and keeps new documents at the end. With a limit, both methods always return an array.