From ba0ae5fbd888652acf04ac360b9386313fab4af5 Mon Sep 17 00:00:00 2001 From: johnnyshields Date: Fri, 21 Apr 2023 02:02:46 +0900 Subject: [PATCH 1/5] Fix exists on relations --- .../association/embedded/embeds_many/proxy.rb | 15 +++++++++++-- .../association/referenced/has_many/proxy.rb | 22 +------------------ lib/mongoid/contextual/memory.rb | 9 ++++++-- lib/mongoid/contextual/mongo.rb | 9 ++++++-- lib/mongoid/contextual/none.rb | 9 +++++--- lib/mongoid/findable.rb | 9 ++++++-- 6 files changed, 41 insertions(+), 32 deletions(-) diff --git a/lib/mongoid/association/embedded/embeds_many/proxy.rb b/lib/mongoid/association/embedded/embeds_many/proxy.rb index ee2c38286..f511b8741 100644 --- a/lib/mongoid/association/embedded/embeds_many/proxy.rb +++ b/lib/mongoid/association/embedded/embeds_many/proxy.rb @@ -217,8 +217,19 @@ def destroy_all(conditions = {}) # @example Are there persisted documents? # person.posts.exists? # - # @param [ Hash | Object | false ] id_or_conditions an _id to - # search for, a hash of conditions, nil or false. + # @example Is a document with the given id persisted? + # context.exists?(BSON::ObjectId(...)) + # + # @example Are there persisted documents with the given title? + # person.posts.exists?({ title: "50 Ways to Leave Your Lover" }) + # + # @example Always return false. + # person.posts.exists?(false) + # + # @param [ :none | Hash | BSON::ObjectId | nil | false ] id_or_conditions + # May optionally supply search conditions as a hash or an object id. + # Will always return false when given nil or false. The symbol :none is + # the default when argument is not supplied. # # @return [ true | false ] True is persisted documents exist, false if not. def exists?(id_or_conditions = :none) diff --git a/lib/mongoid/association/referenced/has_many/proxy.rb b/lib/mongoid/association/referenced/has_many/proxy.rb index 2be0615d8..021e609ea 100644 --- a/lib/mongoid/association/referenced/has_many/proxy.rb +++ b/lib/mongoid/association/referenced/has_many/proxy.rb @@ -14,7 +14,7 @@ class HasMany class Proxy < Association::Many extend Forwardable - def_delegator :criteria, :count + def_delegators :criteria, :count, :exists? def_delegators :_target, :first, :in_memory, :last, :reset, :uniq # Appends a document or array of documents to the association. Will set @@ -163,26 +163,6 @@ def each(&block) end end - # Determine if any documents in this association exist in the database. - # - # If the association contains documents but all of the documents - # exist only in the application, i.e. have not been persisted to the - # database, this method returns false. - # - # This method queries the database on each invocation even if the - # association is already loaded into memory. - # - # @example Are there persisted documents? - # person.posts.exists? - # - # @param [ Hash | Object | false ] id_or_conditions an _id to - # search for, a hash of conditions, nil or false. - # - # @return [ true | false ] True is persisted documents exist, false if not. - def exists?(id_or_conditions = :none) - criteria.exists?(id_or_conditions) - end - # Find the matching document on the association, either based on id or # conditions. # diff --git a/lib/mongoid/contextual/memory.rb b/lib/mongoid/contextual/memory.rb index 469851e12..2867a827e 100644 --- a/lib/mongoid/contextual/memory.rb +++ b/lib/mongoid/contextual/memory.rb @@ -116,8 +116,13 @@ def each(&block) # @example Do any documents exist for given conditions. # context.exists?(name: "...") # - # @param [ Hash | Object | false ] id_or_conditions an _id to - # search for, a hash of conditions, nil or false. + # @example Always return false. + # context.exists?(false) + # + # @param [ :none | Hash | BSON::ObjectId | nil | false ] id_or_conditions + # May optionally supply search conditions as a hash or an object id. + # Will always return false when given nil or false. The symbol :none is + # the default when argument is not supplied. # # @return [ true | false ] If the count is more than zero. # Always false if passed nil or false. diff --git a/lib/mongoid/contextual/mongo.rb b/lib/mongoid/contextual/mongo.rb index c881f9c8e..49381aa4e 100644 --- a/lib/mongoid/contextual/mongo.rb +++ b/lib/mongoid/contextual/mongo.rb @@ -159,11 +159,16 @@ def each(&block) # @example Do any documents exist for given conditions. # context.exists?(name: "...") # + # @example Always return false. + # context.exists?(false) + # # @note We don't use count here since Mongo does not use counted # b-tree indexes. # - # @param [ Hash | Object | false ] id_or_conditions an _id to - # search for, a hash of conditions, nil or false. + # @param [ :none | Hash | BSON::ObjectId | nil | false ] id_or_conditions + # May optionally supply search conditions as a hash or an object id. + # Will always return false when given nil or false. The symbol :none is + # the default when argument is not supplied. # # @return [ true | false ] If the count is more than zero. # Always false if passed nil or false. diff --git a/lib/mongoid/contextual/none.rb b/lib/mongoid/contextual/none.rb index 6660f4c4a..cd9bd8208 100644 --- a/lib/mongoid/contextual/none.rb +++ b/lib/mongoid/contextual/none.rb @@ -57,7 +57,7 @@ def each(&block) end end - # Do any documents exist for the context. + # Do any documents exist for the null context. # # @example Do any documents exist in the null context. # context.exists? @@ -68,8 +68,11 @@ def each(&block) # @example Do any documents exist for given conditions. # context.exists?(name: "...") # - # @param [ Hash | Object | false ] _id_or_conditions An _id to - # search for, a hash of conditions, nil or false. + # @example Always return false. + # context.exists?(false) + # + # @param [ :none | Hash | BSON::ObjectId | nil | false ] _id_or_conditions + # Not used in null context. # # @return [ false ] Always false. def exists?(_id_or_conditions = :none) diff --git a/lib/mongoid/findable.rb b/lib/mongoid/findable.rb index a29a47bc9..cf6c4fa3e 100644 --- a/lib/mongoid/findable.rb +++ b/lib/mongoid/findable.rb @@ -107,8 +107,13 @@ def empty? # @example Do any documents exist for given conditions. # Person.exists?(name: "...") # - # @param [ Hash | Object | false ] id_or_conditions an _id to - # search for, a hash of conditions, nil or false. + # @example Always return false. + # context.exists?(false) + # + # @param [ :none | Hash | BSON::ObjectId | nil | false ] id_or_conditions + # May optionally supply search conditions as a hash or an object id. + # Will always return false when given nil or false. The symbol :none is + # the default when argument is not supplied. # # @return [ true | false ] If any documents exist for the conditions. # Always false if passed nil or false. From 09aadde060910ee7aaa25ee83010b1dc77ac55a8 Mon Sep 17 00:00:00 2001 From: johnnyshields Date: Fri, 21 Apr 2023 16:25:40 +0900 Subject: [PATCH 2/5] Fix MONGOID-5610 --- docs/release-notes/mongoid-9.0.txt | 6 + .../association/embedded/embeds_many/proxy.rb | 28 +-- lib/mongoid/contextual/memory.rb | 2 +- .../embedded/embeds_many/proxy_spec.rb | 220 +++++++++++++++++- 4 files changed, 219 insertions(+), 37 deletions(-) diff --git a/docs/release-notes/mongoid-9.0.txt b/docs/release-notes/mongoid-9.0.txt index 5e52492af..b9c1c53f9 100644 --- a/docs/release-notes/mongoid-9.0.txt +++ b/docs/release-notes/mongoid-9.0.txt @@ -370,6 +370,12 @@ Bug Fixes and Improvements This section will be for smaller bug fixes and improvements: +- The ``exists?`` method on relations now accepts the same arguments + as ``Criteria#exists?``. + `MONGOID-5608 `_. +- Calling ``exists?`` on an embedded relation nil now return ``false`` + when the relation is not persisted in the database. + `MONGOID-5610 `_. - The ``.tally`` method can now take a keyword arg :unwind which tallies array values individually (using the ``$unwind`` operator.) `MONGOID-5556 `_. diff --git a/lib/mongoid/association/embedded/embeds_many/proxy.rb b/lib/mongoid/association/embedded/embeds_many/proxy.rb index f511b8741..2dd8c00ed 100644 --- a/lib/mongoid/association/embedded/embeds_many/proxy.rb +++ b/lib/mongoid/association/embedded/embeds_many/proxy.rb @@ -16,6 +16,8 @@ class EmbedsMany class Proxy < Association::Many include Batchable + def_delegator :criteria, :exists? + # Appends a document or array of documents to the association. Will set # the parent and update the index in the process. # @@ -212,32 +214,6 @@ def destroy_all(conditions = {}) remove_all(conditions, :destroy) end - # Determine if any documents in this association exist in the database. - # - # @example Are there persisted documents? - # person.posts.exists? - # - # @example Is a document with the given id persisted? - # context.exists?(BSON::ObjectId(...)) - # - # @example Are there persisted documents with the given title? - # person.posts.exists?({ title: "50 Ways to Leave Your Lover" }) - # - # @example Always return false. - # person.posts.exists?(false) - # - # @param [ :none | Hash | BSON::ObjectId | nil | false ] id_or_conditions - # May optionally supply search conditions as a hash or an object id. - # Will always return false when given nil or false. The symbol :none is - # the default when argument is not supplied. - # - # @return [ true | false ] True is persisted documents exist, false if not. - def exists?(id_or_conditions = :none) - return _target.any?(&:persisted?) if id_or_conditions == :none - - criteria.exists?(id_or_conditions) - end - # Finds a document in this association through several different # methods. # diff --git a/lib/mongoid/contextual/memory.rb b/lib/mongoid/contextual/memory.rb index 2867a827e..44d59ec14 100644 --- a/lib/mongoid/contextual/memory.rb +++ b/lib/mongoid/contextual/memory.rb @@ -128,7 +128,7 @@ def each(&block) # Always false if passed nil or false. def exists?(id_or_conditions = :none) case id_or_conditions - when :none then any? + when :none then any?(&:persisted?) when nil, false then false when Hash then Memory.new(criteria.where(id_or_conditions)).exists? else Memory.new(criteria.where(_id: id_or_conditions)).exists? diff --git a/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb b/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb index 2c62d13e9..2914d5ded 100644 --- a/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +++ b/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb @@ -2294,29 +2294,229 @@ class TrackingIdValidationHistory describe '#exists?' do - let!(:person) do + let!(:person1) do Person.create! end - context 'when documents exist in the database' do + let!(:person2) do + Person.create! + end - before do - person.addresses.create!(street: 'Bond St') + let!(:person3) do + Person.new + end + + let!(:address1_1) do + person1.addresses.create!(street: 'Bond St') + end + + let!(:address1_2) do + person1.addresses.build(street: 'Hyde Park Dr') + end + + let!(:address2) do + person2.addresses.build(street: 'Hyde Park Dr') + end + + let!(:address3) do + person3.addresses.build(street: 'Hyde Park Dr') + end + + context 'when not passing args' do + + context 'when documents exist in the database' do + + it 'returns true' do + expect_no_queries do + expect(person1.addresses.exists?).to be true + end + end end - it 'returns true' do - expect(person.addresses.exists?).to be true + context 'when no documents exist in the database' do + + it 'returns false' do + expect_no_queries do + expect(person2.addresses.exists?).to be false + end + end + end + + context 'when parent object not persisted' do + + it 'returns false' do + expect_no_queries do + expect(person3.addresses.exists?).to be false + end + end end end - context 'when no documents exist in the database' do + context 'when passing :none' do - before do - person.addresses.build(street: 'Hyde Park Dr') + context 'when documents exist in the database' do + + it 'returns true' do + expect_no_queries do + expect(person1.addresses.exists?(:none)).to be true + end + end + end + + context 'when no documents exist in the database' do + + it 'returns false' do + expect_no_queries do + expect(person2.addresses.exists?(:none)).to be false + end + end end + end + + context 'when passing an _id' do + + context 'when its of type BSON::ObjectId' do + + context 'when embedded object persisted' do + + it 'returns true' do + expect_no_queries do + expect(person1.addresses.exists?(address1_1._id)).to be true + end + end + end + + context 'when embedded object not persisted' do + + it 'returns false' do + expect_no_queries do + expect(person1.addresses.exists?(address1_2._id)).to be false + end + end + end + + context 'when parent object not persisted' do + + it 'returns false' do + expect_no_queries do + expect(person3.addresses.exists?(address3._id)).to be false + end + end + end + + context 'when the id does not exist' do + + it 'returns false' do + expect_no_queries do + expect(person1.addresses.exists?(BSON::ObjectId.new)).to be false + end + end + end + end + + context 'when its of type String' do + + context 'when embedded object persisted' do + + it 'returns true' do + expect_no_queries do + expect(person1.addresses.exists?(address1_1._id.to_s)).to be true + end + end + end + + context 'when embedded object not persisted' do + + it 'returns false' do + expect_no_queries do + expect(person1.addresses.exists?(address1_2._id.to_s)).to be false + end + end + end + + context 'when parent object not persisted' do + + it 'returns false' do + expect_no_queries do + expect(person3.addresses.exists?(address3._id.to_s)).to be false + end + end + end + + context 'when the id does not exist' do + + it 'returns false' do + expect_no_queries do + expect(person1.addresses.exists?(BSON::ObjectId.new.to_s)).to be false + end + end + end + end + end + + context 'when passing a hash' do + + context 'when embedded object persisted' do + + it 'returns true' do + expect_no_queries do + expect(person1.addresses.exists?(street: address1_1.street)).to be true + end + end + end + + context 'when embedded object not persisted' do + + it 'returns false' do + expect_no_queries do + expect(person1.addresses.exists?(street: address1_2.street)).to be false + end + end + end + + context 'when parent object not persisted' do + + it 'returns false' do + expect_no_queries do + expect(person3.addresses.exists?(street: address3.street)).to be false + end + end + end + + context 'when the value does not exist' do + + it 'returns false' do + expect_no_queries do + expect(person1.addresses.exists?(street: 'Foobar')).to be false + end + end + end + end + + context 'when passing false' do + + it 'returns false' do + expect_no_queries do + expect(person1.addresses.exists?(false)).to be false + end + end + end + + context 'when passing nil' do it 'returns false' do - expect(person.addresses.exists?).to be false + expect_no_queries do + expect(person1.addresses.exists?(nil)).to be false + end + end + end + + context 'when the limit is 0' do + + it 'returns false' do + expect_no_queries do + expect(person1.addresses.limit(0).exists?).to be false + end end end end From b1579e8255e731aeedbf256101163d90bd96857f Mon Sep 17 00:00:00 2001 From: johnnyshields Date: Fri, 21 Apr 2023 16:30:27 +0900 Subject: [PATCH 3/5] Fix rubocop --- .../embedded/embeds_many/proxy_spec.rb | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb b/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb index 2914d5ded..d795edd2b 100644 --- a/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +++ b/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb @@ -2306,11 +2306,11 @@ class TrackingIdValidationHistory Person.new end - let!(:address1_1) do + let!(:address1_persisted) do person1.addresses.create!(street: 'Bond St') end - let!(:address1_2) do + let!(:address1_not_persisted) do person1.addresses.build(street: 'Hyde Park Dr') end @@ -2381,7 +2381,7 @@ class TrackingIdValidationHistory it 'returns true' do expect_no_queries do - expect(person1.addresses.exists?(address1_1._id)).to be true + expect(person1.addresses.exists?(address1_persisted._id)).to be true end end end @@ -2390,7 +2390,7 @@ class TrackingIdValidationHistory it 'returns false' do expect_no_queries do - expect(person1.addresses.exists?(address1_2._id)).to be false + expect(person1.addresses.exists?(address1_not_persisted._id)).to be false end end end @@ -2420,7 +2420,7 @@ class TrackingIdValidationHistory it 'returns true' do expect_no_queries do - expect(person1.addresses.exists?(address1_1._id.to_s)).to be true + expect(person1.addresses.exists?(address1_persisted._id.to_s)).to be true end end end @@ -2429,7 +2429,7 @@ class TrackingIdValidationHistory it 'returns false' do expect_no_queries do - expect(person1.addresses.exists?(address1_2._id.to_s)).to be false + expect(person1.addresses.exists?(address1_not_persisted._id.to_s)).to be false end end end @@ -2460,7 +2460,7 @@ class TrackingIdValidationHistory it 'returns true' do expect_no_queries do - expect(person1.addresses.exists?(street: address1_1.street)).to be true + expect(person1.addresses.exists?(street: address1_persisted.street)).to be true end end end @@ -2469,7 +2469,7 @@ class TrackingIdValidationHistory it 'returns false' do expect_no_queries do - expect(person1.addresses.exists?(street: address1_2.street)).to be false + expect(person1.addresses.exists?(street: address1_not_persisted.street)).to be false end end end From 95a92ebc506b778b3391d8a2018fd208a4a1e809 Mon Sep 17 00:00:00 2001 From: johnnyshields Date: Fri, 21 Apr 2023 18:30:55 +0900 Subject: [PATCH 4/5] Fix exists? --- lib/mongoid/contextual/memory.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/mongoid/contextual/memory.rb b/lib/mongoid/contextual/memory.rb index 44d59ec14..24868e17d 100644 --- a/lib/mongoid/contextual/memory.rb +++ b/lib/mongoid/contextual/memory.rb @@ -105,6 +105,17 @@ def each(&block) end end + # Is the enumerable of matching documents empty? + # + # @example Is the context empty? + # context.blank? + # + # @return [ true | false ] If the context is empty. + def blank? + !any? + end + alias_method :empty?, :blank? + # Do any documents exist for the context. # # @example Do any documents exist for the context. From 069dc61ddc1d8ac6cf1a7fbf7892029e52dbbbcd Mon Sep 17 00:00:00 2001 From: johnnyshields Date: Fri, 21 Apr 2023 18:32:21 +0900 Subject: [PATCH 5/5] blank? should use any? --- lib/mongoid/contextual/memory.rb | 11 ----------- lib/mongoid/contextual/queryable.rb | 2 +- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/lib/mongoid/contextual/memory.rb b/lib/mongoid/contextual/memory.rb index 24868e17d..44d59ec14 100644 --- a/lib/mongoid/contextual/memory.rb +++ b/lib/mongoid/contextual/memory.rb @@ -105,17 +105,6 @@ def each(&block) end end - # Is the enumerable of matching documents empty? - # - # @example Is the context empty? - # context.blank? - # - # @return [ true | false ] If the context is empty. - def blank? - !any? - end - alias_method :empty?, :blank? - # Do any documents exist for the context. # # @example Do any documents exist for the context. diff --git a/lib/mongoid/contextual/queryable.rb b/lib/mongoid/contextual/queryable.rb index e97965d26..436828e49 100644 --- a/lib/mongoid/contextual/queryable.rb +++ b/lib/mongoid/contextual/queryable.rb @@ -19,7 +19,7 @@ module Queryable # # @return [ true | false ] If the context is empty. def blank? - !exists? + !any? end alias_method :empty?, :blank? end