Skip to content

MONGOID-5098 Standardize/improve Range queries -- .where(field: 1..3) #5025

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Sep 29, 2021
Merged
2 changes: 1 addition & 1 deletion lib/mongoid/criteria/queryable/extensions/object.rb
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ def __evolve__(object)
when ::Array
object.map{ |obj| evolve(obj) }
when ::Range
{ "$gte" => evolve(object.min), "$lte" => evolve(object.max) }
object.__evolve_range__
else
yield(object)
end
Expand Down
45 changes: 38 additions & 7 deletions lib/mongoid/criteria/queryable/extensions/range.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,60 @@ def __array__
to_a
end

# Convert the range to a min/max mongo friendly query for dates.
# Convert the range to a $gte/$lte mongo friendly query for dates.
#
# @example Evolve the range.
# (11231312..213123131).__evolve_date__
#
# @return [ Hash ] The min/max range query with times at midnight.
# @return [ Hash ] The $gte/$lte range query with times at UTC midnight.
def __evolve_date__
{ "$gte" => min.__evolve_date__, "$lte" => max.__evolve_date__ }
__evolve_range_naive__.transform_values! {|v| v&.__evolve_date__ }
end

# Convert the range to a min/max mongo friendly query for times.
# Convert the range to a $gte/$lte mongo friendly query for times.
#
# @example Evolve the range.
# (11231312..213123131).__evolve_date__
#
# @return [ Hash ] The min/max range query with times.
# @return [ Hash ] The $gte/$lte range query with times in UTC.
def __evolve_time__
{ "$gte" => min.__evolve_time__, "$lte" => max.__evolve_time__ }
__evolve_range_naive__.transform_values! {|v| v&.__evolve_time__ }
end

# Convert the range to a $gte/$lte mongo friendly query.
#
# @example Evolve the range.
# (11231312..213123131).__evolve_range__
#
# @return [ Hash ] The $gte/$lte range query.
def __evolve_range__
__evolve_range_naive__.transform_values! do |value|
case value
when Time, DateTime then value.__evolve_time__
when Date then value.__evolve_date__
else value
end
end
end

private

# @note This method's return value will be mutated by the __evolve_*__
# methods, therefore it must always return new objects.
#
# @api private
def __evolve_range_naive__
hash = {}
hash['$gte'] = self.begin if self.begin
hash[exclude_end? ? "$lt" : "$lte"] = self.end if self.end
hash
end

module ClassMethods

# Evolve the range. This will transform it into a $gte/$lte selection.
# Endless and beginning-less ranges will use only $gte or $lte respectively.
# End-excluded ranges (...) will use $lt selector instead of $lte.
#
# @example Evolve the range.
# Range.evolve(1..3)
Expand All @@ -50,7 +81,7 @@ module ClassMethods
# @return [ Hash ] The range as a gte/lte criteria.
def evolve(object)
return object unless object.is_a?(::Range)
{ "$gte" => object.min, "$lte" => object.max }
object.__evolve_range__
end
end
end
Expand Down
265 changes: 265 additions & 0 deletions spec/integration/criteria/range_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
# frozen_string_literal: true

require 'spec_helper'

describe 'Queries with Range criteria' do
let(:now_utc) { Time.utc(2020, 1, 1, 16, 0, 0, 0) }
let(:now_in_zone) { now_utc.in_time_zone('Asia/Tokyo') }
let(:today) { Date.new(2020, 1, 1) }

let!(:band1) { Band.create!(likes: 0, rating: 0.9, founded: today, updated_at: now_utc) }
let!(:band2) { Band.create!(likes: 1, rating: 1.0, founded: today + 1.day, updated_at: now_utc + 1.days) }
let!(:band3) { Band.create!(likes: 2, rating: 2.9, founded: today + 2.days, updated_at: now_utc + 2.days) }
let!(:band4) { Band.create!(likes: 3, rating: 3.0, founded: today + 3.days, updated_at: now_utc + 3.days) }
let!(:band5) { Band.create!(likes: 4, rating: 3.1, founded: today + 4.days, updated_at: now_utc + 4.days) }

context 'Range<Integer> criteria vs Integer field' do

it 'returns objects within the range' do
expect(Band.where(likes: 1..3).to_a).to eq [band2, band3, band4]
expect(Band.where(likes: 1...3).to_a).to eq [band2, band3]
end

context 'endless range' do
ruby_version_gte '2.6'

it 'returns all objects above the value' do
expect(Band.where(likes: eval('1..')).to_a).to eq [band2, band3, band4, band5]
end
end

context 'beginless range' do
ruby_version_gte '2.7'

it 'returns all objects under the value' do
expect(Band.where(likes: eval('..3')).to_a).to eq [band1, band2, band3, band4]
expect(Band.where(likes: eval('...3')).to_a).to eq [band1, band2, band3]
end
end
end

context 'Range<Integer> criteria vs Float field' do

it 'returns objects within the range' do
expect(Band.where(rating: 1..3).to_a).to eq [band2, band3, band4]
expect(Band.where(rating: 1...3).to_a).to eq [band2, band3]
end

context 'endless range' do
ruby_version_gte '2.6'

it 'returns all objects above the value' do
expect(Band.where(rating: eval('1..')).to_a).to eq [band2, band3, band4, band5]
end
end

context 'beginless range' do
ruby_version_gte '2.7'

it 'returns all objects under the value' do
expect(Band.where(rating: eval('..3')).to_a).to eq [band1, band2, band3, band4]
expect(Band.where(rating: eval('...3')).to_a).to eq [band1, band2, band3]
end
end
end

context 'Range<Float> criteria vs Integer field' do

it 'returns objects within the range' do
expect(Band.where(likes: 0.95..3.05).to_a).to eq [band2, band3, band4]
expect(Band.where(likes: 0.95...3.0).to_a).to eq [band2, band3]
end

context 'endless range' do
ruby_version_gte '2.6'

it 'returns all objects above the value' do
expect(Band.where(likes: eval('0.95..')).to_a).to eq [band2, band3, band4, band5]
end
end

context 'beginless range' do
ruby_version_gte '2.7'

it 'returns all objects under the value' do
expect(Band.where(likes: eval('..3.05')).to_a).to eq [band1, band2, band3, band4]
expect(Band.where(likes: eval('...3.0')).to_a).to eq [band1, band2, band3]
end
end
end

context 'Range<Float> criteria vs Float field' do

it 'returns objects within the range' do
expect(Band.where(rating: 0.95..3.05).to_a).to eq [band2, band3, band4]
expect(Band.where(rating: 0.95...3.0).to_a).to eq [band2, band3]
end

context 'endless range' do
ruby_version_gte '2.6'

it 'returns all objects above the value' do
expect(Band.where(rating: eval('0.95..')).to_a).to eq [band2, band3, band4, band5]
end
end

context 'beginless range' do
ruby_version_gte '2.7'

it 'returns all objects under the value' do
expect(Band.where(rating: eval('..3.05')).to_a).to eq [band1, band2, band3, band4]
expect(Band.where(rating: eval('...3.0')).to_a).to eq [band1, band2, band3]
end
end
end

context 'Range<Time> criteria vs Time field' do

it 'returns objects within the range' do
expect(Band.where(updated_at: (now_utc + 1.day)..(now_utc + 3.days)).to_a).to eq [band2, band3, band4]
expect(Band.where(updated_at: (now_utc + 1.day)...(now_utc + 3.days)).to_a).to eq [band2, band3]
end

context 'endless range' do
ruby_version_gte '2.6'

it 'returns all objects above the value' do
expect(Band.where(updated_at: eval('(now_utc + 1.day)..')).to_a).to eq [band2, band3, band4, band5]
end
end

context 'beginless range' do
ruby_version_gte '2.7'

it 'returns all objects under the value' do
expect(Band.where(updated_at: eval('..(now_utc + 3.days)')).to_a).to eq [band1, band2, band3, band4]
expect(Band.where(updated_at: eval('...(now_utc + 3.days)')).to_a).to eq [band1, band2, band3]
end
end
end

context 'Range<Time> criteria vs Date field' do

it 'returns objects within the range' do
expect(Band.where(founded: (now_utc + 1.day)..(now_utc + 3.days)).to_a).to eq [band2, band3, band4]
expect(Band.where(founded: (now_utc + 1.day)...(now_utc + 3.days)).to_a).to eq [band2, band3]
end

context 'endless range' do
ruby_version_gte '2.6'

it 'returns all objects above the value' do
expect(Band.where(founded: eval('(now_utc + 1.day)..')).to_a).to eq [band2, band3, band4, band5]
end
end

context 'beginless range' do
ruby_version_gte '2.7'

it 'returns all objects under the value' do
expect(Band.where(founded: eval('..(now_utc + 3.days)')).to_a).to eq [band1, band2, band3, band4]
expect(Band.where(founded: eval('...(now_utc + 3.days)')).to_a).to eq [band1, band2, band3]
end
end
end

context 'Range<ActiveSupport::TimeWithZone> criteria vs Time field' do

it 'returns objects within the range' do
expect(Band.where(updated_at: (now_in_zone + 1.day)..(now_in_zone + 3.days)).to_a).to eq [band2, band3, band4]
expect(Band.where(updated_at: (now_in_zone + 1.day)...(now_in_zone + 3.days)).to_a).to eq [band2, band3]
end

context 'endless range' do
ruby_version_gte '2.6'

it 'returns all objects above the value' do
expect(Band.where(updated_at: eval('(now_in_zone + 1.day)..')).to_a).to eq [band2, band3, band4, band5]
end
end

context 'beginless range' do
ruby_version_gte '2.7'

it 'returns all objects under the value' do
expect(Band.where(updated_at: eval('..(now_in_zone + 3.days)')).to_a).to eq [band1, band2, band3, band4]
expect(Band.where(updated_at: eval('...(now_in_zone + 3.days)')).to_a).to eq [band1, band2, band3]
end
end
end

context 'Range<ActiveSupport::TimeWithZone> criteria vs Date field' do

it 'returns objects within the range' do
expect(Band.where(founded: (now_in_zone + 1.day)..(now_in_zone + 3.days)).to_a).to eq [band3, band4, band5]
expect(Band.where(founded: (now_in_zone + 1.day)...(now_in_zone + 3.days)).to_a).to eq [band3, band4]
end

context 'endless range' do
ruby_version_gte '2.6'

it 'returns all objects above the value' do
expect(Band.where(founded: eval('(now_in_zone + 1.day)..')).to_a).to eq [band3, band4, band5]
end
end

context 'beginless range' do
ruby_version_gte '2.7'

it 'returns all objects under the value' do
expect(Band.where(founded: eval('..(now_in_zone + 3.days)')).to_a).to eq [band1, band2, band3, band4, band5]
expect(Band.where(founded: eval('...(now_in_zone + 3.days)')).to_a).to eq [band1, band2, band3, band4]
end
end
end

context 'Range<Date> criteria vs Date field' do

it 'returns objects within the range' do
expect(Band.where(founded: (today + 1.day)..(today + 3.days)).to_a).to eq [band2, band3, band4]
expect(Band.where(founded: (today + 1.day)...(today + 3.days)).to_a).to eq [band2, band3]
end

context 'endless range' do
ruby_version_gte '2.6'

it 'returns all objects above the value' do
expect(Band.where(founded: eval('(today + 1.day)..')).to_a).to eq [band2, band3, band4, band5]
end
end

context 'beginless range' do
ruby_version_gte '2.7'

it 'returns all objects under the value' do
expect(Band.where(founded: eval('..(today + 3.days)')).to_a).to eq [band1, band2, band3, band4]
expect(Band.where(founded: eval('...(today + 3.days)')).to_a).to eq [band1, band2, band3]
end
end
end

context 'Range<Date> criteria vs Time field' do

it 'returns objects within the range' do
expect(Band.where(updated_at: (today + 1.day)..(today + 3.days)).to_a).to eq [band2, band3]
expect(Band.where(updated_at: (today + 1.day)...(today + 3.days)).to_a).to eq [band2, band3]
end

context 'endless range' do
ruby_version_gte '2.6'

it 'returns all objects above the value' do
expect(Band.where(updated_at: eval('(today + 1.day)..')).to_a).to eq [band2, band3, band4, band5]
end
end

context 'beginless range' do
ruby_version_gte '2.7'

it 'returns all objects under the value' do
expect(Band.where(updated_at: eval('..(today + 3.days)')).to_a).to eq [band1, band2, band3]
expect(Band.where(updated_at: eval('...(today + 3.days)')).to_a).to eq [band1, band2, band3]
end
end
end
end
Loading