mongoid 8.1.2 → 8.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2100bfc3ff5a707914028bbcca16fe61db68cfc271f2603b79a9f420baccdd3a
4
- data.tar.gz: c1147f2941f122d45a5678ed2c9d95ab840a65e1f8883970384f846f979bfb9f
3
+ metadata.gz: '059cd4652349083d2098577aa1c6f1f90233ab91474ef0a3f97db4bef4843f9f'
4
+ data.tar.gz: ca14035b9902221fe082862ea061f83beb2abc41a9a3eeb10dd0f781a34e697e
5
5
  SHA512:
6
- metadata.gz: 4710b315e359d87807dc3b882b39d929eb52bf53612bf2d37e6bbf409309b3116773eab6e810b8b7dbbbfc33f24e135758c757b3de43dca4c591881b39d1e2df
7
- data.tar.gz: 758af68d937986c4a01963ff9e8c6f106276fc40805c1133966d88461eabd19ca7b41e412b50566fe962b180525a925bae529ff5b58846fa8433336d8880a6ad
6
+ metadata.gz: 358e801040678c844022e9e41bcdd3a5d482cbafbe608b623340c834d4b72545df547afa83d861d64431703d198c4347f6d9bada41c2960d38b46fdc1a243855
7
+ data.tar.gz: 8a2981f0a6d4bb4b2c24b6a636079ad3e3cbd1586791104deca7c118d0a08f8753a9a2ce564b416293ce337154c965f26a9ce34f44f087dd0fa8c981d58684da
checksums.yaml.gz.sig CHANGED
Binary file
@@ -227,9 +227,24 @@ module Mongoid
227
227
  # @example Are there persisted documents?
228
228
  # person.posts.exists?
229
229
  #
230
- # @return [ true | false ] True is persisted documents exist, false if not.
231
- def exists?
232
- _target.any? { |doc| doc.persisted? }
230
+ # @param [ :none | nil | false | Hash | Object ] id_or_conditions
231
+ # When :none (the default), returns true if any persisted
232
+ # documents exist in the association. When nil or false, this
233
+ # will always return false. When a Hash is given, this queries
234
+ # the documents in the association for those that match the given
235
+ # conditions, and returns true if any match which have been
236
+ # persisted. Any other argument is interpreted as an id, and
237
+ # queries for the existence of persisted documents in the
238
+ # association with a matching _id.
239
+ #
240
+ # @return [ true | false ] True if persisted documents exist, false if not.
241
+ def exists?(id_or_conditions = :none)
242
+ case id_or_conditions
243
+ when :none then _target.any?(&:persisted?)
244
+ when nil, false then false
245
+ when Hash then where(id_or_conditions).any?(&:persisted?)
246
+ else where(_id: id_or_conditions).any?(&:persisted?)
247
+ end
233
248
  end
234
249
 
235
250
  # Finds a document in this association through several different
@@ -172,9 +172,18 @@ module Mongoid
172
172
  # @example Are there persisted documents?
173
173
  # person.posts.exists?
174
174
  #
175
+ # @param [ :none | nil | false | Hash | Object ] id_or_conditions
176
+ # When :none (the default), returns true if any persisted
177
+ # documents exist in the association. When nil or false, this
178
+ # will always return false. When a Hash is given, this queries
179
+ # the documents in the association for those that match the given
180
+ # conditions, and returns true if any match. Any other argument is
181
+ # interpreted as an id, and queries for the existence of documents
182
+ # in the association with a matching _id.
183
+ #
175
184
  # @return [ true | false ] True is persisted documents exist, false if not.
176
- def exists?
177
- criteria.exists?
185
+ def exists?(id_or_conditions = :none)
186
+ criteria.exists?(id_or_conditions)
178
187
  end
179
188
 
180
189
  # Find the matching document on the association, either based on id or
@@ -185,6 +185,19 @@ module Mongoid
185
185
  # See https://jira.mongodb.org/browse/MONGOID-5542
186
186
  option :prevent_multiple_calls_of_embedded_callbacks, default: false
187
187
 
188
+ # When this flag is true, callbacks for embedded documents will not be
189
+ # called. This is the default in 8.x, but will be changed to false in 9.0.
190
+ #
191
+ # Setting this flag to true (as it is in 8.x) may lead to stack
192
+ # overflow errors if there are more than cicrca 1000 embedded
193
+ # documents in the root document's dependencies graph.
194
+ #
195
+ # It is strongly recommended to set this flag to false in 8.x, if you
196
+ # are not using around callbacks for embedded documents.
197
+ #
198
+ # See https://jira.mongodb.org/browse/MONGOID-5658 for more details.
199
+ option :around_callbacks_for_embeds, default: true
200
+
188
201
  # Returns the Config singleton, for use in the configure DSL.
189
202
  #
190
203
  # @return [ self ] The Config singleton.
@@ -69,7 +69,12 @@ module Mongoid
69
69
  # @return [ Integer ] The number of matches.
70
70
  def count(options = {}, &block)
71
71
  return super(&block) if block_given?
72
- view.count_documents(options)
72
+
73
+ if valid_for_count_documents?
74
+ view.count_documents(options)
75
+ else
76
+ view.count(options)
77
+ end
73
78
  end
74
79
 
75
80
  # Get the estimated number of documents matching the query.
@@ -1046,6 +1051,24 @@ module Mongoid
1046
1051
  limit ? docs : docs.first
1047
1052
  end
1048
1053
 
1054
+ # Queries whether the current context is valid for use with
1055
+ # the #count_documents? predicate. A context is valid if it
1056
+ # does not include a `$where` operator.
1057
+ #
1058
+ # @return [ true | false ] whether or not the current context
1059
+ # excludes a `$where` operator.
1060
+ def valid_for_count_documents?(hash = view.filter)
1061
+ # Note that `view.filter` is a BSON::Document, and all keys in a
1062
+ # BSON::Document are strings; we don't need to worry about symbol
1063
+ # representations of `$where`.
1064
+ hash.keys.each do |key|
1065
+ return false if key == '$where'
1066
+ return false if hash[key].is_a?(Hash) && !valid_for_count_documents?(hash[key])
1067
+ end
1068
+
1069
+ true
1070
+ end
1071
+
1049
1072
  def raise_document_not_found_error
1050
1073
  raise Errors::DocumentNotFound.new(klass, nil, nil)
1051
1074
  end
@@ -27,7 +27,8 @@ module Mongoid
27
27
  # @param [ [ Symbol | Hash<Symbol, [ Symbol | String ]> ]... ] *method_descriptors
28
28
  # The methods to deprecate, with optional replacement instructions.
29
29
  def deprecate(target_module, *method_descriptors)
30
- Mongoid::Deprecation.deprecate_methods(target_module, *method_descriptors)
30
+ @_deprecator ||= Mongoid::Deprecation.new
31
+ @_deprecator.deprecate_methods(target_module, *method_descriptors)
31
32
  end
32
33
  end
33
34
  end
@@ -15,10 +15,10 @@ module Mongoid
15
15
  #
16
16
  # @return Array<Proc> The deprecation behavior.
17
17
  def behavior
18
- @behavior ||= Array(->(message, callstack, _deprecation_horizon, _gem_name) {
18
+ @behavior ||= Array(->(*args) {
19
19
  logger = Mongoid.logger
20
- logger.warn(message)
21
- logger.debug(callstack.join("\n ")) if debug
20
+ logger.warn(args[0])
21
+ logger.debug(args[1].join("\n ")) if debug
22
22
  })
23
23
  end
24
24
  end
@@ -43,7 +43,7 @@ module Mongoid
43
43
  real_key = klass.database_field_name(key2)
44
44
 
45
45
  value.delete(key2) if real_key != key2
46
- value[real_key] = (key == "$rename") ? value2.to_s : mongoize_for(key, klass, real_key, value2)
46
+ value[real_key] = value_for(key, klass, real_key, value2)
47
47
  end
48
48
  consolidated[key] ||= {}
49
49
  consolidated[key].update(value)
@@ -185,6 +185,24 @@ module Mongoid
185
185
 
186
186
  private
187
187
 
188
+ # Get the value for the provided operator, klass, key and value.
189
+ #
190
+ # This is necessary for special cases like $rename, $addToSet and $push.
191
+ #
192
+ # @param [ String ] operator The operator.
193
+ # @param [ Class ] klass The model class.
194
+ # @param [ String | Symbol ] key The field key.
195
+ # @param [ Object ] value The original value.
196
+ #
197
+ # @return [ Object ] Value prepared for the provided operator.
198
+ def value_for(operator, klass, key, value)
199
+ case operator
200
+ when "$rename" then value.to_s
201
+ when "$addToSet", "$push" then value.mongoize
202
+ else mongoize_for(operator, klass, operator, value)
203
+ end
204
+ end
205
+
188
206
  # Mongoize for the klass, key and value.
189
207
  #
190
208
  # @api private
@@ -140,6 +140,28 @@ module Mongoid
140
140
  #
141
141
  # @api private
142
142
  def _mongoid_run_child_callbacks(kind, children: nil, &block)
143
+ if Mongoid::Config.around_callbacks_for_embeds
144
+ _mongoid_run_child_callbacks_with_around(kind, children: children, &block)
145
+ else
146
+ _mongoid_run_child_callbacks_without_around(kind, children: children, &block)
147
+ end
148
+ end
149
+
150
+ # Execute the callbacks of given kind for embedded documents including
151
+ # around callbacks.
152
+ #
153
+ # @note This method is prone to stack overflow errors if the document
154
+ # has a large number of embedded documents. It is recommended to avoid
155
+ # using around callbacks for embedded documents until a proper solution
156
+ # is implemented.
157
+ #
158
+ # @param [ Symbol ] kind The type of callback to execute.
159
+ # @param [ Array<Document> ] children Children to execute callbacks on. If
160
+ # nil, callbacks will be executed on all cascadable children of
161
+ # the document.
162
+ #
163
+ # @api private
164
+ def _mongoid_run_child_callbacks_with_around(kind, children: nil, &block)
143
165
  child, *tail = (children || cascadable_children(kind))
144
166
  with_children = !Mongoid::Config.prevent_multiple_calls_of_embedded_callbacks
145
167
  if child.nil?
@@ -148,23 +170,91 @@ module Mongoid
148
170
  child.run_callbacks(child_callback_type(kind, child), with_children: with_children, &block)
149
171
  else
150
172
  child.run_callbacks(child_callback_type(kind, child), with_children: with_children) do
151
- _mongoid_run_child_callbacks(kind, children: tail, &block)
173
+ _mongoid_run_child_callbacks_with_around(kind, children: tail, &block)
152
174
  end
153
175
  end
154
176
  end
155
177
 
156
- # This is used to store callbacks to be executed later. A good use case for
157
- # this is delaying the after_find and after_initialize callbacks until the
158
- # associations are set on the document. This can also be used to delay
159
- # applying the defaults on a document.
178
+ # Execute the callbacks of given kind for embedded documents without
179
+ # around callbacks.
180
+ #
181
+ # @param [ Symbol ] kind The type of callback to execute.
182
+ # @param [ Array<Document> ] children Children to execute callbacks on. If
183
+ # nil, callbacks will be executed on all cascadable children of
184
+ # the document.
185
+ #
186
+ # @api private
187
+ def _mongoid_run_child_callbacks_without_around(kind, children: nil, &block)
188
+ children = (children || cascadable_children(kind))
189
+ callback_list = _mongoid_run_child_before_callbacks(kind, children: children)
190
+ return false if callback_list == false
191
+ value = block&.call
192
+ callback_list.each do |_next_sequence, env|
193
+ env.value &&= value
194
+ end
195
+ return false if _mongoid_run_child_after_callbacks(callback_list: callback_list) == false
196
+
197
+ value
198
+ end
199
+
200
+ # Execute the before callbacks of given kind for embedded documents.
201
+ #
202
+ # @param [ Symbol ] kind The type of callback to execute.
203
+ # @param [ Array<Document> ] children Children to execute callbacks on.
204
+ # @param [ Array<ActiveSupport::Callbacks::CallbackSequence, ActiveSupport::Callbacks::Filters::Environment> ] callback_list List of
205
+ # pairs of callback sequence and environment. This list will be later used
206
+ # to execute after callbacks in reverse order.
207
+ #
208
+ # @api private
209
+ def _mongoid_run_child_before_callbacks(kind, children: [], callback_list: [])
210
+ children.each do |child|
211
+ chain = child.__callbacks[child_callback_type(kind, child)]
212
+ env = ActiveSupport::Callbacks::Filters::Environment.new(child, false, nil)
213
+ next_sequence = compile_callbacks(chain)
214
+ unless next_sequence.final?
215
+ Mongoid.logger.warn("Around callbacks are disabled for embedded documents. Skipping around callbacks for #{child.class.name}.")
216
+ Mongoid.logger.warn("To enable around callbacks for embedded documents, set Mongoid::Config.around_callbacks_for_embeds to true.")
217
+ end
218
+ next_sequence.invoke_before(env)
219
+ return false if env.halted
220
+ env.value = !env.halted
221
+ callback_list << [next_sequence, env]
222
+ if (grandchildren = child.send(:cascadable_children, kind))
223
+ _mongoid_run_child_before_callbacks(kind, children: grandchildren, callback_list: callback_list)
224
+ end
225
+ end
226
+ callback_list
227
+ end
228
+
229
+ # Execute the after callbacks.
230
+ #
231
+ # @param [ Array<ActiveSupport::Callbacks::CallbackSequence, ActiveSupport::Callbacks::Filters::Environment> ] callback_list List of
232
+ # pairs of callback sequence and environment.
233
+ def _mongoid_run_child_after_callbacks(callback_list: [])
234
+ callback_list.reverse_each do |next_sequence, env|
235
+ next_sequence.invoke_after(env)
236
+ return false if env.halted
237
+ end
238
+ end
239
+
240
+ # Returns the stored callbacks to be executed later.
160
241
  #
161
- # @return [ Array<Symbol> ] an array of symbols that represent the pending callbacks.
242
+ # @return [ Array<Symbol> ] Method symbols of the stored pending callbacks.
162
243
  #
163
244
  # @api private
164
245
  def pending_callbacks
165
246
  @pending_callbacks ||= [].to_set
166
247
  end
167
248
 
249
+ # Stores callbacks to be executed later. A good use case for
250
+ # this is delaying the after_find and after_initialize callbacks until the
251
+ # associations are set on the document. This can also be used to delay
252
+ # applying the defaults on a document.
253
+ #
254
+ # @param [ Array<Symbol> ] value Method symbols of the pending callbacks to store.
255
+ #
256
+ # @return [ Array<Symbol> ] Method symbols of the stored pending callbacks.
257
+ #
168
258
  # @api private
169
259
  def pending_callbacks=(value)
170
260
  @pending_callbacks = value
@@ -299,7 +389,7 @@ module Mongoid
299
389
  end
300
390
  self.class.send :define_method, name do
301
391
  env = ActiveSupport::Callbacks::Filters::Environment.new(self, false, nil)
302
- sequence = chain.compile
392
+ sequence = compile_callbacks(chain)
303
393
  sequence.invoke_before(env)
304
394
  env.value = !env.halted
305
395
  sequence.invoke_after(env)
@@ -309,5 +399,24 @@ module Mongoid
309
399
  end
310
400
  send(name)
311
401
  end
402
+
403
+ # Compile the callback chain.
404
+ #
405
+ # This method hides the differences between ActiveSupport implementations
406
+ # before and after 7.1.
407
+ #
408
+ # @param [ ActiveSupport::Callbacks::CallbackChain ] chain The callback chain.
409
+ # @param [ Symbol | nil ] type The type of callback chain to compile.
410
+ #
411
+ # @return [ ActiveSupport::Callbacks::CallbackSequence ] The compiled callback sequence.
412
+ def compile_callbacks(chain, type = nil)
413
+ if chain.method(:compile).arity == 0
414
+ # ActiveSupport < 7.1
415
+ chain.compile
416
+ else
417
+ # ActiveSupport >= 7.1
418
+ chain.compile(type)
419
+ end
420
+ end
312
421
  end
313
422
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mongoid
4
- VERSION = "8.1.2"
4
+ VERSION = "8.1.4"
5
5
  end
@@ -557,6 +557,7 @@ describe 'callbacks integration tests' do
557
557
 
558
558
  context 'nested embedded documents' do
559
559
  config_override :prevent_multiple_calls_of_embedded_callbacks, true
560
+ config_override :around_callbacks_for_embeds, true
560
561
 
561
562
  let(:logger) { Array.new }
562
563
 
@@ -581,4 +582,24 @@ describe 'callbacks integration tests' do
581
582
  expect(logger).to eq(%i[embedded_twice embedded_once root])
582
583
  end
583
584
  end
585
+
586
+ context 'cascade callbacks' do
587
+ ruby_version_gte '3.0'
588
+ config_override :around_callbacks_for_embeds, false
589
+
590
+ let(:book) do
591
+ Book.new
592
+ end
593
+
594
+ before do
595
+ 1500.times do
596
+ book.pages.build
597
+ end
598
+ end
599
+
600
+ # https://jira.mongodb.org/browse/MONGOID-5658
601
+ it 'does not raise SystemStackError' do
602
+ expect { book.save! }.not_to raise_error(SystemStackError)
603
+ end
604
+ end
584
605
  end
@@ -2310,9 +2310,37 @@ describe Mongoid::Association::Embedded::EmbedsMany::Proxy do
2310
2310
  person.addresses.create!(street: "Bond St")
2311
2311
  end
2312
2312
 
2313
+ let(:address) { person.addresses.first }
2314
+
2313
2315
  it "returns true" do
2314
2316
  expect(person.addresses.exists?).to be true
2315
2317
  end
2318
+
2319
+ context 'when given specifying conditions' do
2320
+ context 'when the record exists in the association' do
2321
+ it 'returns true by condition' do
2322
+ expect(person.addresses.exists?(street: 'Bond St')).to be true
2323
+ end
2324
+
2325
+ it 'returns true by id' do
2326
+ expect(person.addresses.exists?(address._id)).to be true
2327
+ end
2328
+
2329
+ it 'returns false when given false' do
2330
+ expect(person.addresses.exists?(false)).to be false
2331
+ end
2332
+
2333
+ it 'returns false when given nil' do
2334
+ expect(person.addresses.exists?(nil)).to be false
2335
+ end
2336
+ end
2337
+
2338
+ context 'when the record does not exist in the association' do
2339
+ it 'returns false' do
2340
+ expect(person.addresses.exists?(street: 'Garfield Ave')).to be false
2341
+ end
2342
+ end
2343
+ end
2316
2344
  end
2317
2345
 
2318
2346
  context "when no documents exist in the database" do
@@ -2324,6 +2352,13 @@ describe Mongoid::Association::Embedded::EmbedsMany::Proxy do
2324
2352
  it "returns false" do
2325
2353
  expect(person.addresses.exists?).to be false
2326
2354
  end
2355
+
2356
+ context 'when given specifying conditions' do
2357
+ it 'returns false' do
2358
+ expect(person.addresses.exists?(street: 'Hyde Park Dr')).to be false
2359
+ expect(person.addresses.exists?(street: 'Garfield Ave')).to be false
2360
+ end
2361
+ end
2327
2362
  end
2328
2363
  end
2329
2364
 
@@ -2395,6 +2395,42 @@ describe Mongoid::Association::Referenced::HasMany::Proxy do
2395
2395
  end
2396
2396
  end
2397
2397
  end
2398
+
2399
+ context 'when invoked with specifying conditions' do
2400
+ let(:other_person) { Person.create! }
2401
+ let(:post) { person.posts.first }
2402
+
2403
+ before do
2404
+ person.posts.create title: 'bumfuzzle'
2405
+ other_person.posts.create title: 'bumbershoot'
2406
+ end
2407
+
2408
+ context 'when the conditions match an associated record' do
2409
+ it 'detects its existence by condition' do
2410
+ expect(person.posts.exists?(title: 'bumfuzzle')).to be true
2411
+ expect(other_person.posts.exists?(title: 'bumbershoot')).to be true
2412
+ end
2413
+
2414
+ it 'detects its existence by id' do
2415
+ expect(person.posts.exists?(post._id)).to be true
2416
+ end
2417
+
2418
+ it 'returns false when given false' do
2419
+ expect(person.posts.exists?(false)).to be false
2420
+ end
2421
+
2422
+ it 'returns false when given nil' do
2423
+ expect(person.posts.exists?(nil)).to be false
2424
+ end
2425
+ end
2426
+
2427
+ context 'when the conditions match an unassociated record' do
2428
+ it 'does not detect its existence' do
2429
+ expect(person.posts.exists?(title: 'bumbershoot')).to be false
2430
+ expect(other_person.posts.exists?(title: 'bumfuzzle')).to be false
2431
+ end
2432
+ end
2433
+ end
2398
2434
  end
2399
2435
 
2400
2436
  context "when documents exist in application but not in database" do
@@ -2465,6 +2501,12 @@ describe Mongoid::Association::Referenced::HasMany::Proxy do
2465
2501
  end
2466
2502
  end
2467
2503
  end
2504
+
2505
+ context 'when invoked with specifying conditions' do
2506
+ it 'returns false' do
2507
+ expect(person.posts.exists?(title: 'hullaballoo')).to be false
2508
+ end
2509
+ end
2468
2510
  end
2469
2511
  end
2470
2512
 
@@ -158,6 +158,16 @@ describe Mongoid::Contextual::Mongo do
158
158
  end
159
159
  end
160
160
  end
161
+
162
+ context 'when for_js is present' do
163
+ let(:context) do
164
+ Band.for_js('this.name == "Depeche Mode"')
165
+ end
166
+
167
+ it 'counts the expected records' do
168
+ expect(context.count).to eq(1)
169
+ end
170
+ end
161
171
  end
162
172
 
163
173
  describe "#estimated_count" do
@@ -3690,16 +3700,51 @@ describe Mongoid::Contextual::Mongo do
3690
3700
 
3691
3701
  context "when the attributes are in the correct type" do
3692
3702
 
3693
- before do
3694
- context.update_all("$set" => { name: "Smiths" })
3703
+ context "when operation is $set" do
3704
+
3705
+ before do
3706
+ context.update_all("$set" => { name: "Smiths" })
3707
+ end
3708
+
3709
+ it "updates the first matching document" do
3710
+ expect(depeche_mode.reload.name).to eq("Smiths")
3711
+ end
3712
+
3713
+ it "updates the last matching document" do
3714
+ expect(new_order.reload.name).to eq("Smiths")
3715
+ end
3695
3716
  end
3696
3717
 
3697
- it "updates the first matching document" do
3698
- expect(depeche_mode.reload.name).to eq("Smiths")
3718
+ context "when operation is $push" do
3719
+
3720
+ before do
3721
+ depeche_mode.update_attribute(:genres, ["electronic"])
3722
+ new_order.update_attribute(:genres, ["electronic"])
3723
+ context.update_all("$push" => { genres: "pop" })
3724
+ end
3725
+
3726
+ it "updates the first matching document" do
3727
+ expect(depeche_mode.reload.genres).to eq(["electronic", "pop"])
3728
+ end
3729
+
3730
+ it "updates the last matching document" do
3731
+ expect(new_order.reload.genres).to eq(["electronic", "pop"])
3732
+ end
3699
3733
  end
3700
3734
 
3701
- it "updates the last matching document" do
3702
- expect(new_order.reload.name).to eq("Smiths")
3735
+ context "when operation is $addToSet" do
3736
+
3737
+ before do
3738
+ context.update_all("$addToSet" => { genres: "electronic" })
3739
+ end
3740
+
3741
+ it "updates the first matching document" do
3742
+ expect(depeche_mode.reload.genres).to eq(["electronic"])
3743
+ end
3744
+
3745
+ it "updates the last matching document" do
3746
+ expect(new_order.reload.genres).to eq(["electronic"])
3747
+ end
3703
3748
  end
3704
3749
  end
3705
3750