ExportXMLWordPrintableJSON

    • 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 =

      { uri: ENV.fetch('MONGODB_URI', 'mongodb://localhost:27017/mongoid_repro') }

      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 #

      {i + 1}

      ") }

      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.

            Assignee:
            Unassigned
            Reporter:
            Henrique Marques (EXT)
            Votes:
            0 Vote for this issue
            Watchers:
            2 Start watching this issue

              Created:
              Updated: