From 2bcd50b2067c4e0eda94fc71547b229ea3a4aceb Mon Sep 17 00:00:00 2001 From: johnnyshields <27655+johnnyshields@users.noreply.github.com> Date: Sun, 3 Sep 2023 15:44:57 +0900 Subject: [PATCH 1/5] Remove Object#__find_args__. This primarily used in Criteria::Findable where it is now a private method. 2 points to note: - __find_args__ was mistakenly called in Atomic#unset method. Although technically __find_args__ does a similar thing as what #unset requires for its argument preparation, this is pure coincidence and its a kludge to use find_args here. So I've inlined the code and made a new private method. - __find_args__ for Range previously called to_a. This is actually a bug / DOS risk, because ranges of strings tend to explode when you use to_a. As an example, ('64e0'..'64ff').to_a.size => 9320, and it gets much worse for 24-char BSON ids! Interestingly however, MongoDB itself can handle ranges of ids gracefully, so I am now just passing them into the DB as-is and it magically works--I've added tests. --- lib/mongoid/contextual/atomic.rb | 34 +++++-- lib/mongoid/criteria/findable.rb | 14 ++- lib/mongoid/extensions/array.rb | 10 -- lib/mongoid/extensions/object.rb | 10 -- lib/mongoid/extensions/range.rb | 10 -- spec/mongoid/criteria/findable_spec.rb | 134 +++++++++++++++++++++++++ spec/mongoid/extensions/object_spec.rb | 7 -- spec/mongoid/extensions/range_spec.rb | 11 -- 8 files changed, 172 insertions(+), 58 deletions(-) diff --git a/lib/mongoid/contextual/atomic.rb b/lib/mongoid/contextual/atomic.rb index 1587c4aca7..8ab4911504 100644 --- a/lib/mongoid/contextual/atomic.rb +++ b/lib/mongoid/contextual/atomic.rb @@ -169,17 +169,14 @@ def set(sets) # @example Unset the field on the matches. # context.unset(:name) # - # @param [ [ String | Symbol | Array | Hash ]... ] *args - # The name(s) of the field(s) to unset. - # If a Hash is specified, its keys will be used irrespective of what - # each key's value is, even if the value is nil or false. + # @param [ [ String | Symbol | Array | Hash ]... ] *unsets + # The name(s) of the field(s) to unset. If a Hash is specified, + # its keys will be used irrespective of value, even if the value + # is nil or false. # # @return [ nil ] Nil. - def unset(*args) - fields = args.map { |a| a.is_a?(Hash) ? a.keys : a } - .__find_args__ - .map { |f| [database_field_name(f), true] } - view.update_many("$unset" => Hash[fields]) + def unset(*unsets) + view.update_many('$unset' => collect_unset_operations(unsets)) end # Performs an atomic $min update operation on the given field or fields. @@ -247,6 +244,25 @@ def collect_each_operations(ops) operations[database_field_name(field)] = { "$each" => Array.wrap(value).mongoize } end end + + # Builds the selector an atomic $unset operation from arguments. + # + # @example Prepare selector from array. + # context.collect_unset_operations([:name, :age]) + # #=> { "name" => true, "age" => true } + # + # @example Prepare selector from hash. + # context.collect_unset_operations({ name: 1 }, { age: 1 }) + # #=> { "name" => true, "age" => true } + # + # @param [ String | Symbol | Array | Hash ] ops + # The name(s) of the field(s) to unset. + # + # @return [ Hash ] The selector for the atomic $unset operation. + def collect_unset_operations(ops) + ops.map { |op| op.is_a?(Hash) ? op.keys : op }.flatten + .map { |field| [database_field_name(field), true] }.to_h + end end end end diff --git a/lib/mongoid/criteria/findable.rb b/lib/mongoid/criteria/findable.rb index 09d557b44c..06c3b29991 100644 --- a/lib/mongoid/criteria/findable.rb +++ b/lib/mongoid/criteria/findable.rb @@ -40,7 +40,7 @@ def execute_or_raise(ids, multi) # # @return [ Document | Array ] The matching document(s). def find(*args) - ids = args.__find_args__ + ids = prepare_ids_for_find(args) raise_invalid if ids.any?(&:nil?) for_ids(ids).execute_or_raise(ids, args.multi_arged?) end @@ -133,6 +133,18 @@ def mongoize_ids(ids) end end + # Convert args to the +#find+ method into a flat array of ids. + # + # @example Get the ids. + # find_args([ 1, [ 2, 3 ] ]) + # + # @param [ Array ] args The arguments. + # + # @return [ Array ] The array of ids. + def prepare_ids_for_find(args) + args.flat_map {|a| a.is_a?(Set) ? a.to_a : a }.uniq(&:to_s) + end + # Convenience method of raising an invalid options error. # # @example Raise the error. diff --git a/lib/mongoid/extensions/array.rb b/lib/mongoid/extensions/array.rb index 812ac620da..968155b801 100644 --- a/lib/mongoid/extensions/array.rb +++ b/lib/mongoid/extensions/array.rb @@ -18,16 +18,6 @@ def __evolve_object_id__ self end - # Get the array of args as arguments for a find query. - # - # @example Get the array as find args. - # [ 1, 2, 3 ].__find_args__ - # - # @return [ Array ] The array of args. - def __find_args__ - flat_map{ |a| a.__find_args__ }.uniq{ |a| a.to_s } - end - # Mongoize the array into an array of object ids. # # @example Evolve the array to object ids. diff --git a/lib/mongoid/extensions/object.rb b/lib/mongoid/extensions/object.rb index ec0cd89f91..71da3b5374 100644 --- a/lib/mongoid/extensions/object.rb +++ b/lib/mongoid/extensions/object.rb @@ -18,16 +18,6 @@ def __evolve_object_id__ end alias :__mongoize_object_id__ :__evolve_object_id__ - # Convert the object to args for a find query. - # - # @example Convert the object to args. - # object.__find_args__ - # - # @return [ Object ] self. - def __find_args__ - self - end - # Mongoize a plain object into a time. # # @note This method should not be used, because it does not diff --git a/lib/mongoid/extensions/range.rb b/lib/mongoid/extensions/range.rb index a804dc2da6..41937447b4 100644 --- a/lib/mongoid/extensions/range.rb +++ b/lib/mongoid/extensions/range.rb @@ -7,16 +7,6 @@ module Extensions # Adds type-casting behavior to Range class. module Range - # Get the range as arguments for a find. - # - # @example Get the range as find args. - # range.__find_args__ - # - # @return [ Array ] The range as an array. - def __find_args__ - to_a - end - # Turn the object from the ruby type we deal with to a Mongo friendly # type. # diff --git a/spec/mongoid/criteria/findable_spec.rb b/spec/mongoid/criteria/findable_spec.rb index 9715fe1ae3..d27998af8b 100644 --- a/spec/mongoid/criteria/findable_spec.rb +++ b/spec/mongoid/criteria/findable_spec.rb @@ -260,6 +260,140 @@ end end + context "when providing a Set of ids" do + + let!(:band_two) do + Band.create!(name: "Tool") + end + + context "when all ids match" do + let(:found) do + Band.find(Set[ band.id, band_two.id ]) + end + + it "contains the first match" do + pending 'https://github.com/mongodb/mongoid/pull/5702 - removal of multi_arged? monkey patch' + + expect(found).to include(band) + end + + it "contains the second match" do + pending 'https://github.com/mongodb/mongoid/pull/5702 - removal of multi_arged? monkey patch' + + expect(found).to include(band_two) + end + end + + context "when any id does not match" do + + context "when raising a not found error" do + config_override :raise_not_found_error, true + + let(:found) do + Band.find(Set[ band.id, BSON::ObjectId.new ]) + end + + it "raises an error" do + expect { + found + }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) + end + end + + context "when raising no error" do + config_override :raise_not_found_error, false + + let(:found) do + Band.find(Set[ band.id, BSON::ObjectId.new ]) + end + + it "returns only the matching documents" do + pending 'https://github.com/mongodb/mongoid/pull/5702 - removal of multi_arged? monkey patch' + + expect(found).to eq([ band ]) + end + end + end + end + + context "when providing a Range of ids" do + + let!(:band_two) do + Band.create!(name: "Tool") + end + + context "when all ids match" do + let(:found) do + Band.find(band.id.to_s..band_two.id.to_s) + end + + it "contains the first match" do + expect(found).to include(band) + end + + it "contains the second match" do + expect(found).to include(band_two) + end + + + context "when any id does not match" do + + context "when raising a not found error" do + config_override :raise_not_found_error, true + + let(:found) do + Band.find(band_two.id.to_s..BSON::ObjectId.new) + end + + it "does not raise error and returns only the matching documents" do + expect(found).to eq([ band_two ]) + end + end + + context "when raising no error" do + config_override :raise_not_found_error, false + + let(:found) do + Band.find(band_two.id.to_s..BSON::ObjectId.new) + end + + it "returns only the matching documents" do + expect(found).to eq([ band_two ]) + end + end + end + end + + context "when all ids do not match" do + + context "when raising a not found error" do + config_override :raise_not_found_error, true + + let(:found) do + Band.find(BSON::ObjectId.new..BSON::ObjectId.new) + end + + it "raises an error" do + expect { + found + }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) + end + end + + context "when raising no error" do + config_override :raise_not_found_error, false + + let(:found) do + Band.find(BSON::ObjectId.new..BSON::ObjectId.new) + end + + it "returns only the matching documents" do + expect(found).to eq([]) + end + end + end + end + context "when providing a single id as extended json" do context "when the id matches" do diff --git a/spec/mongoid/extensions/object_spec.rb b/spec/mongoid/extensions/object_spec.rb index 80d313a3af..87a69a8226 100644 --- a/spec/mongoid/extensions/object_spec.rb +++ b/spec/mongoid/extensions/object_spec.rb @@ -16,13 +16,6 @@ end end - describe "#__find_args__" do - - it "returns self" do - expect(object.__find_args__).to eq(object) - end - end - describe "#__mongoize_object_id__" do it "returns self" do diff --git a/spec/mongoid/extensions/range_spec.rb b/spec/mongoid/extensions/range_spec.rb index 572f076fb2..58ea85c9ff 100644 --- a/spec/mongoid/extensions/range_spec.rb +++ b/spec/mongoid/extensions/range_spec.rb @@ -5,17 +5,6 @@ describe Mongoid::Extensions::Range do - describe "#__find_args__" do - - let(:range) do - 1..3 - end - - it "returns the range as an array" do - expect(range.__find_args__).to eq([ 1, 2, 3 ]) - end - end - describe ".demongoize" do subject { Range.demongoize(hash) } From 71217c33e7861f776c76c410e856541e4a52671a Mon Sep 17 00:00:00 2001 From: johnnyshields <27655+johnnyshields@users.noreply.github.com> Date: Sun, 3 Sep 2023 16:23:01 +0900 Subject: [PATCH 2/5] Fix specs --- lib/mongoid/criteria/findable.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/mongoid/criteria/findable.rb b/lib/mongoid/criteria/findable.rb index 06c3b29991..4ad9ef4d83 100644 --- a/lib/mongoid/criteria/findable.rb +++ b/lib/mongoid/criteria/findable.rb @@ -142,7 +142,16 @@ def mongoize_ids(ids) # # @return [ Array ] The array of ids. def prepare_ids_for_find(args) - args.flat_map {|a| a.is_a?(Set) ? a.to_a : a }.uniq(&:to_s) + args.flat_map do |arg| + case arg + when Array, Set + prepare_ids_for_find(arg) + when Range + arg.begin&.numeric? && arg.end&.numeric? ? arg.to_a : arg + else + arg + end + end.uniq(&:to_s) end # Convenience method of raising an invalid options error. From 690f2cdf8a50cef839acc9ddc83fc33275ed6401 Mon Sep 17 00:00:00 2001 From: johnnyshields <27655+johnnyshields@users.noreply.github.com> Date: Sun, 3 Sep 2023 16:28:55 +0900 Subject: [PATCH 3/5] Add test to cover #find for nested arrays, which was pre-existing behavior. --- spec/mongoid/criteria/findable_spec.rb | 62 ++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/spec/mongoid/criteria/findable_spec.rb b/spec/mongoid/criteria/findable_spec.rb index d27998af8b..f00aa04fc9 100644 --- a/spec/mongoid/criteria/findable_spec.rb +++ b/spec/mongoid/criteria/findable_spec.rb @@ -260,6 +260,68 @@ end end + context "when providing nested arrays of ids" do + + let!(:band_two) do + Band.create!(name: "Tool") + end + + context "when all ids match" do + + let(:found) do + Band.find([ [ band.id ], [ [ band_two.id ] ] ]) + end + + it "contains the first match" do + expect(found).to include(band) + end + + it "contains the second match" do + expect(found).to include(band_two) + end + + context "when ids are duplicates" do + + let(:found) do + Band.find([ [ band.id ], [ [ band.id ] ] ]) + end + + it "contains only the first match" do + expect(found).to eq([band]) + end + end + end + + context "when any id does not match" do + + context "when raising a not found error" do + config_override :raise_not_found_error, true + + let(:found) do + Band.find([ [ band.id ], [ [ BSON::ObjectId.new ] ] ]) + end + + it "raises an error" do + expect { + found + }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) + end + end + + context "when raising no error" do + config_override :raise_not_found_error, false + + let(:found) do + Band.find([ [ band.id ], [ [ BSON::ObjectId.new ] ] ]) + end + + it "returns only the matching documents" do + expect(found).to eq([ band ]) + end + end + end + end + context "when providing a Set of ids" do let!(:band_two) do From 6b733017afe40e94e0fded55f2426487feacc8cd Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Mon, 6 Nov 2023 16:05:13 -0700 Subject: [PATCH 4/5] deprecate __find_args__ instead of removing it --- lib/mongoid/extensions/array.rb | 12 ++++++++++++ lib/mongoid/extensions/object.rb | 12 ++++++++++++ lib/mongoid/extensions/range.rb | 20 ++++++++++++++++---- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/lib/mongoid/extensions/array.rb b/lib/mongoid/extensions/array.rb index 968155b801..e80a46ed2d 100644 --- a/lib/mongoid/extensions/array.rb +++ b/lib/mongoid/extensions/array.rb @@ -18,6 +18,18 @@ def __evolve_object_id__ self end + # Get the array of args as arguments for a find query. + # + # @example Get the array as find args. + # [ 1, 2, 3 ].__find_args__ + # + # @return [ Array ] The array of args. + # @deprecated + def __find_args__ + flat_map{ |a| a.__find_args__ }.uniq{ |a| a.to_s } + end + Mongoid.deprecate(self, :__find_args__) + # Mongoize the array into an array of object ids. # # @example Evolve the array to object ids. diff --git a/lib/mongoid/extensions/object.rb b/lib/mongoid/extensions/object.rb index 71da3b5374..0359a2328e 100644 --- a/lib/mongoid/extensions/object.rb +++ b/lib/mongoid/extensions/object.rb @@ -18,6 +18,18 @@ def __evolve_object_id__ end alias :__mongoize_object_id__ :__evolve_object_id__ + # Convert the object to args for a find query. + # + # @example Convert the object to args. + # object.__find_args__ + # + # @return [ Object ] self. + # @deprecated + def __find_args__ + self + end + Mongoid.deprecate(self, :__find_args__) + # Mongoize a plain object into a time. # # @note This method should not be used, because it does not diff --git a/lib/mongoid/extensions/range.rb b/lib/mongoid/extensions/range.rb index 41937447b4..e5dc8e89c3 100644 --- a/lib/mongoid/extensions/range.rb +++ b/lib/mongoid/extensions/range.rb @@ -3,9 +3,23 @@ module Mongoid module Extensions - # Adds type-casting behavior to Range class. module Range + def self.included(base) + base.extend(ClassMethods) + end + + # Get the range as arguments for a find. + # + # @example Get the range as find args. + # range.__find_args__ + # + # @return [ Array ] The range as an array. + # @deprecated + def __find_args__ + to_a + end + Mongoid.deprecate(self, :__find_args__) # Turn the object from the ruby type we deal with to a Mongo friendly # type. @@ -29,7 +43,6 @@ def resizable? end module ClassMethods - # Convert the object from its mongo friendly ruby type to this type. # # @example Demongoize the object. @@ -97,5 +110,4 @@ def __mongoize_range__(object) end end -::Range.__send__(:include, Mongoid::Extensions::Range) -::Range.extend(Mongoid::Extensions::Range::ClassMethods) +Range.include(Mongoid::Extensions::Range) From 06ab8ba5cb9c4826a978317fb1a42ea21543d5e8 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Mon, 6 Nov 2023 16:18:32 -0700 Subject: [PATCH 5/5] Set#resizable? has to be true for this to work --- lib/mongoid/criteria/findable.rb | 2 +- lib/mongoid/extensions/set.rb | 12 ++++++++++-- spec/mongoid/criteria/findable_spec.rb | 6 ------ 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/mongoid/criteria/findable.rb b/lib/mongoid/criteria/findable.rb index a8c59292f6..527821a631 100644 --- a/lib/mongoid/criteria/findable.rb +++ b/lib/mongoid/criteria/findable.rb @@ -137,7 +137,7 @@ def mongoize_ids(ids) # Convert args to the +#find+ method into a flat array of ids. # # @example Get the ids. - # find_args([ 1, [ 2, 3 ] ]) + # prepare_ids_for_find([ 1, [ 2, 3 ] ]) # # @param [ Array ] args The arguments. # diff --git a/lib/mongoid/extensions/set.rb b/lib/mongoid/extensions/set.rb index 21a887a5f6..574723f845 100644 --- a/lib/mongoid/extensions/set.rb +++ b/lib/mongoid/extensions/set.rb @@ -6,7 +6,6 @@ module Extensions # Adds type-casting behavior to Set class. module Set - # Turn the object from the ruby type we deal with to a Mongo friendly # type. # @@ -18,8 +17,17 @@ def mongoize ::Set.mongoize(self) end - module ClassMethods + # Returns whether the object's size can be changed. + # + # @example Is the object resizable? + # object.resizable? + # + # @return [ true ] true. + def resizable? + true + end + module ClassMethods # Convert the object from its mongo friendly ruby type to this type. # # @example Demongoize the object. diff --git a/spec/mongoid/criteria/findable_spec.rb b/spec/mongoid/criteria/findable_spec.rb index f00aa04fc9..1468fc5f80 100644 --- a/spec/mongoid/criteria/findable_spec.rb +++ b/spec/mongoid/criteria/findable_spec.rb @@ -334,14 +334,10 @@ end it "contains the first match" do - pending 'https://github.com/mongodb/mongoid/pull/5702 - removal of multi_arged? monkey patch' - expect(found).to include(band) end it "contains the second match" do - pending 'https://github.com/mongodb/mongoid/pull/5702 - removal of multi_arged? monkey patch' - expect(found).to include(band_two) end end @@ -370,8 +366,6 @@ end it "returns only the matching documents" do - pending 'https://github.com/mongodb/mongoid/pull/5702 - removal of multi_arged? monkey patch' - expect(found).to eq([ band ]) end end