How to union two different Mongoid Criteria
Asked Answered
B

2

9

I have the following scopes defined in my model:

scope :upcoming, -> { where(:start_time.gt => Time.now).asc(:start_time) }
scope :in_progress, -> {
   now = Time.now
   where(:start_time.lte => now).where(:end_time.gte => now).asc(:start_time)
}

I want to create another scope that combines the results of both of those scopes called current. I tried something like this:

scope :current, -> { self.in_progress | self.upcoming }

But this just ends up treating them both like arrays and concatenating them. The problem with this is that when I try to call my scope with Model.current, I get the following error message:

NoMethodError: undefined method `as_conditions' for #<Array:0xaceb008>

This is because it converted the Mongoid Criteria object to an array, but I don't want that. I want the object to stay as a Mongoid Criteria object.

What I really want is the union of the in_progress set and the upcoming set.

Any ideas? Thanks.

Bombazine answered 21/4, 2012 at 3:51 Comment(1)
If you want the union of the two result sets then you're probably better off writing the third scope from scratch using an :$or query.Prau
P
7

You can try to compose your criteria using Mongoid's query methods and dereferencing into the criteria's selector, but I wouldn't necessarily recommend this -- see below for an example. I second the recommendation to craft your third scope. Remember that these scopes correspond to db queries that you want to be efficient, so it is probably worth your time to examine and understand the resulting and underlying MongoDB queries that are generated.

Model

class Episode
  include Mongoid::Document
  field :name, type: String
  field :start_time, type: Time
  field :end_time, type: Time

  scope :upcoming, -> { where(:start_time.gt => Time.now).asc(:start_time) }
  scope :in_progress, -> {
     now = Time.now
     where(:start_time.lte => now).where(:end_time.gte => now).asc(:start_time)
  }
  scope :current, -> { any_of([upcoming.selector, in_progress.selector]) }
  scope :current_simpler, -> { where(:end_time.gte => Time.now) }
end

Test

require 'test_helper'

class EpisodeTest < ActiveSupport::TestCase
  def setup
    Episode.delete_all
  end

  test "scope composition" do
    #p Episode.in_progress
    #p Episode.upcoming
    #p Episode.current
    #p Episode.current_simpler

    in_progress_name = 'In Progress'
    upcoming_name = 'Upcoming'
    Episode.create(:name => in_progress_name, :start_time => Time.now, :end_time => 1.hour.from_now)
    Episode.create(:name => upcoming_name, :start_time => 1.hour.from_now, :end_time => 2.hours.from_now)

    assert_equal([in_progress_name], Episode.in_progress.to_a.map(&:name))
    assert_equal([upcoming_name], Episode.upcoming.to_a.map(&:name))
    assert_equal([in_progress_name, upcoming_name], Episode.current.to_a.map(&:name))
    assert_equal([in_progress_name, upcoming_name], Episode.current_simpler.to_a.map(&:name))
  end
end
Pastore answered 23/4, 2012 at 21:7 Comment(0)
A
2

You have to map your Array back to a Mongoid::Criteria. Any array of yours can be translated to a criteria with any_in:

scope :has_data, -> { any_in(:_id => all.select{ |record| record.data.size > 0 }.map{ |r| r.id }) }

So, something like this should do the trick: (untested)

scope :current, -> { any_in(:_id => (self.in_progress + self.upcoming).map{ |r| r.id }) }

I hope there exists better solutions, but this solves the equation at least.

Alienee answered 5/5, 2012 at 16:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.