mongoid 7.4.3 → 7.5.0

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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/lib/config/locales/en.yml +7 -0
  4. data/lib/mongoid/association/embedded/batchable.rb +3 -20
  5. data/lib/mongoid/association/macros.rb +20 -0
  6. data/lib/mongoid/association/referenced/has_many/enumerable.rb +12 -8
  7. data/lib/mongoid/association/referenced/has_many/proxy.rb +2 -2
  8. data/lib/mongoid/atomic/paths/embedded/many.rb +0 -19
  9. data/lib/mongoid/config.rb +6 -1
  10. data/lib/mongoid/contextual/memory.rb +144 -12
  11. data/lib/mongoid/contextual/mongo.rb +118 -26
  12. data/lib/mongoid/contextual/none.rb +45 -1
  13. data/lib/mongoid/criteria/queryable/extensions/array.rb +2 -0
  14. data/lib/mongoid/criteria/queryable/extensions/hash.rb +2 -0
  15. data/lib/mongoid/criteria/queryable/mergeable.rb +21 -0
  16. data/lib/mongoid/criteria/queryable/selectable.rb +26 -10
  17. data/lib/mongoid/criteria.rb +2 -0
  18. data/lib/mongoid/document.rb +2 -0
  19. data/lib/mongoid/equality.rb +4 -4
  20. data/lib/mongoid/errors/document_not_found.rb +23 -6
  21. data/lib/mongoid/fields.rb +145 -21
  22. data/lib/mongoid/findable.rb +20 -5
  23. data/lib/mongoid/version.rb +1 -1
  24. data/lib/mongoid/warnings.rb +29 -0
  25. data/lib/mongoid.rb +1 -0
  26. data/lib/rails/generators/mongoid/config/templates/mongoid.yml +4 -3
  27. data/spec/integration/i18n_fallbacks_spec.rb +15 -1
  28. data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +0 -21
  29. data/spec/mongoid/association/embedded/embeds_many_models.rb +0 -121
  30. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +0 -8
  31. data/spec/mongoid/association/referenced/has_many/enumerable_spec.rb +54 -0
  32. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +8 -24
  33. data/spec/mongoid/clients/options_spec.rb +1 -0
  34. data/spec/mongoid/config_spec.rb +10 -4
  35. data/spec/mongoid/contextual/memory_spec.rb +826 -65
  36. data/spec/mongoid/contextual/mongo_spec.rb +781 -18
  37. data/spec/mongoid/contextual/none_spec.rb +46 -0
  38. data/spec/mongoid/criteria/queryable/selectable_spec.rb +212 -39
  39. data/spec/mongoid/criteria_spec.rb +8 -0
  40. data/spec/mongoid/equality_spec.rb +12 -12
  41. data/spec/mongoid/errors/document_not_found_spec.rb +49 -0
  42. data/spec/mongoid/findable_spec.rb +30 -0
  43. data/spec/support/models/code.rb +2 -0
  44. data.tar.gz.sig +0 -0
  45. metadata +3 -2
  46. metadata.gz.sig +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b9706abdd1325c02319659ad86963770019a8559220f411ec6a98207d41e6388
4
- data.tar.gz: 99026d1acf68f1a4347607fad2374462b7bf1ea5b42c268180a2ede163c707a0
3
+ metadata.gz: 32597c6d6b536f273850be1989db0d876026d2bffda067d7923f173c3966faa6
4
+ data.tar.gz: 97fffa62c90ae5c20e7dcf00eb151b557c26ab5ae676c45b5651efa5143cf34c
5
5
  SHA512:
6
- metadata.gz: 42224a7effb0349e39934d79191af29dd1f77802cb6a273c44312edba01aba0a46c07061577660f223d0b09211141c843a68827ef9cb5453ed3bfb1d9764ca8c
7
- data.tar.gz: be7fff7159b76d9827ee3b5e28c78313098895ecd63047e478992709c975e05579fb6a0b7a9444bbc8f4451284948af585f76b987975c3ad16dbd0aabafe0e48
6
+ metadata.gz: dbe0c003a7e058f5ab6ac5592170ef1a26e02bf9be729b889c55a3a70af4e0c8b6e52f7157cc9b40879545689972769bae5deabf6814ce1ebeb97fdf1a128212
7
+ data.tar.gz: d696cd95560d51b34318e7f25467450dde8461383dd17830f1177363cbf006c863b1268a2b79616e563343a3fd0e388c9d3bde67821b698b4cf0035bbfc9b719
checksums.yaml.gz.sig CHANGED
Binary file
@@ -404,6 +404,13 @@ en:
404
404
  \_\_\_\_\_\_default:\n
405
405
  \_\_\_\_\_\_\_\_hosts:\n
406
406
  \_\_\_\_\_\_\_\_\_\_- localhost:27017\n\n"
407
+ no_documents_found:
408
+ message: "Could not find a document of class %{klass}."
409
+ summary: "Mongoid attempted to find a document of the class %{klass}
410
+ but none exist."
411
+ resolution: "Create a document of class %{klass} or use a finder
412
+ method that returns nil when no documents are found instead of
413
+ raising an exception."
407
414
  no_environment:
408
415
  message: "Could not load the configuration since no environment
409
416
  was defined."
@@ -80,8 +80,7 @@ module Mongoid
80
80
  def batch_replace(docs)
81
81
  if docs.blank?
82
82
  if _assigning? && !empty?
83
- _base.delayed_atomic_sets.delete(path)
84
- clear_atomic_path_cache
83
+ _base.delayed_atomic_sets.clear
85
84
  _base.add_atomic_unset(first)
86
85
  target_duplicate = _target.dup
87
86
  pre_process_batch_remove(target_duplicate, :delete)
@@ -93,8 +92,7 @@ module Mongoid
93
92
  _base.delayed_atomic_sets.clear unless _assigning?
94
93
  docs = normalize_docs(docs).compact
95
94
  _target.clear and _unscoped.clear
96
- _base.delayed_atomic_unsets.delete(path)
97
- clear_atomic_path_cache
95
+ _base.delayed_atomic_unsets.clear
98
96
  inserts = execute_batch_set(docs)
99
97
  add_atomic_sets(inserts)
100
98
  end
@@ -236,22 +234,7 @@ module Mongoid
236
234
  #
237
235
  # @return [ String ] The atomic path.
238
236
  def path
239
- @path ||= if _unscoped.empty?
240
- Mongoid::Atomic::Paths::Embedded::Many.position_without_document(_base, _association)
241
- else
242
- _unscoped.first.atomic_path
243
- end
244
- end
245
-
246
- # Clear the cache for path and atomic_paths. This method is used when
247
- # the path method is used, and the association has not been set on the
248
- # document yet, which can cause path and atomic_paths to be calculated
249
- # incorrectly later.
250
- #
251
- # @api private
252
- def clear_atomic_path_cache
253
- self.path = nil
254
- _base.instance_variable_set("@atomic_paths", nil)
237
+ @path ||= _unscoped.first.atomic_path
255
238
  end
256
239
 
257
240
  # Set the atomic path.
@@ -12,6 +12,26 @@ module Mongoid
12
12
  class_attribute :embedded, instance_reader: false
13
13
  class_attribute :embedded_relations
14
14
  class_attribute :relations
15
+
16
+ # A hash that maps aliases to their associations. This hash maps the
17
+ # associations "in database name" to its "in code" name. This is used when
18
+ # associations specify the `store_as` option, or on a referenced association.
19
+ # On a referenced association, this is used to map the foreign key to
20
+ # the association's name. For example, if we had the following
21
+ # relationship:
22
+ #
23
+ # User has_many Accounts
24
+ #
25
+ # User will have an entry in the aliased associations hash:
26
+ #
27
+ # account_ids => accounts
28
+ #
29
+ # Note that on the belongs_to associations, the mapping from
30
+ # foreign key => name is not in the aliased_associations hash, but a
31
+ # mapping from name => foreign key is in the aliased_fields hash.
32
+ #
33
+ # @return [ Hash<String, String> ] The aliased associations hash.
34
+ #
15
35
  # @api private
16
36
  class_attribute :aliased_associations
17
37
  self.embedded = false
@@ -243,14 +243,16 @@ module Mongoid
243
243
  # use the option { id_sort: :none }.
244
244
  # Be aware that #first/#last won't guarantee order in this case.
245
245
  #
246
- # @param [ Hash ] opts The options for the query returning the first document.
246
+ # @param [ Integer | Hash ] limit_or_opts The number of documents to
247
+ # return, or a hash of options.
247
248
  #
248
- # @option opts [ :none ] :id_sort Don't apply a sort on _id.
249
+ # @option limit_or_opts [ :none ] :id_sort This option is deprecated.
250
+ # Don't apply a sort on _id if no other sort is defined on the criteria.
249
251
  #
250
252
  # @return [ Document ] The first document found.
251
- def first(opts = {})
253
+ def first(limit_or_opts = nil)
252
254
  _loaded.try(:values).try(:first) ||
253
- _added[(ul = _unloaded.try(:first, opts)).try(:_id)] ||
255
+ _added[(ul = _unloaded.try(:first, limit_or_opts)).try(:_id)] ||
254
256
  ul ||
255
257
  _added.values.try(:first)
256
258
  end
@@ -330,15 +332,17 @@ module Mongoid
330
332
  # use the option { id_sort: :none }.
331
333
  # Be aware that #first/#last won't guarantee order in this case.
332
334
  #
333
- # @param [ Hash ] opts The options for the query returning the first document.
335
+ # @param [ Integer | Hash ] limit_or_opts The number of documents to
336
+ # return, or a hash of options.
334
337
  #
335
- # @option opts [ :none ] :id_sort Don't apply a sort on _id.
338
+ # @option limit_or_opts [ :none ] :id_sort This option is deprecated.
339
+ # Don't apply a sort on _id if no other sort is defined on the criteria.
336
340
  #
337
341
  # @return [ Document ] The last document found.
338
- def last(opts = {})
342
+ def last(limit_or_opts = nil)
339
343
  _added.values.try(:last) ||
340
344
  _loaded.try(:values).try(:last) ||
341
- _added[(ul = _unloaded.try(:last, opts)).try(:_id)] ||
345
+ _added[(ul = _unloaded.try(:last, limit_or_opts)).try(:_id)] ||
342
346
  ul
343
347
  end
344
348
 
@@ -469,8 +469,8 @@ module Mongoid
469
469
  selector = conditions || {}
470
470
  removed = klass.send(method, selector.merge!(criteria.selector))
471
471
  _target.delete_if do |doc|
472
- doc._matches?(selector).tap do |b|
473
- unbind_one(doc) if b
472
+ if doc._matches?(selector)
473
+ unbind_one(doc) and true
474
474
  end
475
475
  end
476
476
  removed
@@ -34,25 +34,6 @@ module Mongoid
34
34
  locator = document.new_record? ? "" : ".#{document._index}"
35
35
  "#{pos}#{"." unless pos.blank?}#{document._association.store_as}#{locator}"
36
36
  end
37
-
38
- class << self
39
-
40
- # Get the position of where the document would go for the given
41
- # association. The use case for this function is when trying to
42
- # persist an empty list for an embedded association. All of the
43
- # existing functions for getting the position to store a document
44
- # require passing in a document to store, which we don't have when
45
- # trying to store the empty list.
46
- #
47
- # @param [ Document ] parent The parent document to store in.
48
- # @param [ Association ] association The association.
49
- #
50
- # @return [ String ] The position string.
51
- def position_without_document(parent, association)
52
- pos = parent.atomic_position
53
- "#{pos}#{"." unless pos.blank?}#{association.store_as}"
54
- end
55
- end
56
37
  end
57
38
  end
58
39
  end
@@ -23,7 +23,8 @@ module Mongoid
23
23
  # database name is not explicitly defined.
24
24
  option :app_name, default: nil
25
25
 
26
- # Create indexes in background by default.
26
+ # (Deprecated) In MongoDB 4.0 and earlier, set whether to create
27
+ # indexes in the background by default. (default: false)
27
28
  option :background_indexing, default: false
28
29
 
29
30
  # Mark belongs_to associations as required by default, so that saving a
@@ -111,6 +112,10 @@ module Mongoid
111
112
  # demongoize the values on returning them.
112
113
  option :legacy_pluck_distinct, default: true
113
114
 
115
+ # Combine chained operators, which use the same field and operator,
116
+ # using and's instead of overwriting them.
117
+ option :overwrite_chained_operators, default: true
118
+
114
119
  # Has Mongoid been configured? This is checking that at least a valid
115
120
  # client config exists.
116
121
  #
@@ -78,7 +78,11 @@ module Mongoid
78
78
  #
79
79
  # @return [ Array<Object> ] The distinct values for the field.
80
80
  def distinct(field)
81
- documents.map{ |doc| doc.send(field) }.uniq
81
+ if Mongoid.legacy_pluck_distinct
82
+ documents.map{ |doc| doc.send(field) }.uniq
83
+ else
84
+ pluck(field).uniq
85
+ end
82
86
  end
83
87
 
84
88
  # Iterate over the context. If provided a block, yield to a Mongoid
@@ -116,9 +120,16 @@ module Mongoid
116
120
  # @example Get the first document.
117
121
  # context.first
118
122
  #
123
+ # @param [ Integer | Hash ] limit_or_opts The number of documents to
124
+ # return, or a hash of options.
125
+ #
119
126
  # @return [ Document ] The first document.
120
- def first(*args)
121
- eager_load([documents.first]).first
127
+ def first(limit_or_opts = nil)
128
+ if limit_or_opts.nil? || limit_or_opts.is_a?(Hash)
129
+ eager_load([documents.first]).first
130
+ else
131
+ eager_load(documents.first(limit_or_opts))
132
+ end
122
133
  end
123
134
  alias :one :first
124
135
  alias :find_first :first
@@ -159,9 +170,52 @@ module Mongoid
159
170
  # @example Get the last document.
160
171
  # context.last
161
172
  #
173
+ # @param [ Integer | Hash ] limit_or_opts The number of documents to
174
+ # return, or a hash of options.
175
+ #
176
+ # @option limit_or_opts [ :none ] :id_sort This option is deprecated.
177
+ # Don't apply a sort on _id if no other sort is defined on the criteria.
178
+ #
162
179
  # @return [ Document ] The last document.
163
- def last
164
- eager_load([documents.last]).first
180
+ def last(limit_or_opts = nil)
181
+ if limit_or_opts.nil? || limit_or_opts.is_a?(Hash)
182
+ eager_load([documents.last]).first
183
+ else
184
+ eager_load(documents.last(limit_or_opts))
185
+ end
186
+ end
187
+
188
+ # Take the given number of documents from the database.
189
+ #
190
+ # @example Take a document.
191
+ # context.take
192
+ #
193
+ # @param [ Integer | nil ] limit The number of documents to take or nil.
194
+ #
195
+ # @return [ Document ] The document.
196
+ def take(limit = nil)
197
+ if limit
198
+ eager_load(documents.take(limit))
199
+ else
200
+ eager_load([documents.first]).first
201
+ end
202
+ end
203
+
204
+ # Take the given number of documents from the database.
205
+ #
206
+ # @example Take a document.
207
+ # context.take
208
+ #
209
+ # @return [ Document ] The document.
210
+ #
211
+ # @raises [ Mongoid::Errors::DocumentNotFound ] raises when there are no
212
+ # documents to take.
213
+ def take!
214
+ if documents.empty?
215
+ raise Errors::DocumentNotFound.new(klass, nil, nil)
216
+ else
217
+ eager_load([documents.first]).first
218
+ end
165
219
  end
166
220
 
167
221
  # Get the length of matching documents in the context.
@@ -189,14 +243,35 @@ module Mongoid
189
243
  end
190
244
 
191
245
  def pluck(*fields)
192
- fields = Array.wrap(fields)
193
- documents.map do |doc|
194
- if fields.size == 1
195
- doc[fields.first]
196
- else
197
- fields.map { |n| doc[n] }.compact
246
+ if Mongoid.legacy_pluck_distinct
247
+ documents.pluck(*fields)
248
+ else
249
+ documents.map do |d|
250
+ if fields.length == 1
251
+ retrieve_value_at_path(d, fields.first)
252
+ else
253
+ fields.map do |field|
254
+ retrieve_value_at_path(d, field)
255
+ end
256
+ end
198
257
  end
199
- end.compact
258
+ end
259
+ end
260
+
261
+ # Tally the field values in memory.
262
+ #
263
+ # @example Get the counts of values in memory.
264
+ # context.tally(:name)
265
+ #
266
+ # @param [ String | Symbol ] field Field to tally.
267
+ #
268
+ # @return [ Hash ] The hash of counts.
269
+ def tally(field)
270
+ return documents.each_with_object({}) do |d, acc|
271
+ v = retrieve_value_at_path(d, field)
272
+ acc[v] ||= 0
273
+ acc[v] += 1
274
+ end
200
275
  end
201
276
 
202
277
  # Skips the provided number of documents.
@@ -414,6 +489,63 @@ module Mongoid
414
489
  def _session
415
490
  @criteria.send(:_session)
416
491
  end
492
+
493
+ # Retrieve the value for the current document at the given field path.
494
+ #
495
+ # For example, if I have the following models:
496
+ #
497
+ # User has_many Accounts
498
+ # address is a hash on Account
499
+ #
500
+ # u = User.new(accounts: [ Account.new(address: { street: "W 50th" }) ])
501
+ # retrieve_value_at_path(u, "user.accounts.address.street")
502
+ # # => [ "W 50th" ]
503
+ #
504
+ # Note that the result is in an array since accounts is an array. If it
505
+ # was nested in two arrays the result would be in a 2D array.
506
+ #
507
+ # @param [ Object ] document The object to traverse the field path.
508
+ # @param [ String ] field_path The dotted string that represents the path
509
+ # to the value.
510
+ #
511
+ # @return [ Object | nil ] The value at the given field path or nil if it
512
+ # doesn't exist.
513
+ def retrieve_value_at_path(document, field_path)
514
+ return if field_path.blank? || !document
515
+ segment, remaining = field_path.to_s.split('.', 2)
516
+
517
+ curr = if document.is_a?(Document)
518
+ # Retrieves field for segment to check localization. Only does one
519
+ # iteration since there's no dots
520
+ res = if remaining
521
+ field = document.class.traverse_association_tree(segment)
522
+ # If this is a localized field, and there are remaining, get the
523
+ # _translations hash so that we can get the specified translation in
524
+ # the remaining
525
+ if field&.localized?
526
+ document.send("#{segment}_translations")
527
+ end
528
+ end
529
+ meth = klass.aliased_associations[segment] || segment
530
+ res.nil? ? document.try(meth) : res
531
+ elsif document.is_a?(Hash)
532
+ # TODO: Remove the indifferent access when implementing MONGOID-5410.
533
+ document.key?(segment.to_s) ?
534
+ document[segment.to_s] :
535
+ document[segment.to_sym]
536
+ else
537
+ nil
538
+ end
539
+
540
+ return curr unless remaining
541
+
542
+ if curr.is_a?(Array)
543
+ # compact is used for consistency with server behavior.
544
+ curr.map { |d| retrieve_value_at_path(d, remaining) }.compact
545
+ else
546
+ retrieve_value_at_path(curr, remaining)
547
+ end
548
+ end
417
549
  end
418
550
  end
419
551
  end
@@ -42,6 +42,7 @@ module Mongoid
42
42
  #
43
43
  # @return [ true, false ] If the context is cached.
44
44
  def cached?
45
+ Mongoid::Warnings.warn_criteria_cache_deprecated
45
46
  !!@cache
46
47
  end
47
48
 
@@ -251,25 +252,28 @@ module Mongoid
251
252
  # and have no sort defined on the criteria, use the option { id_sort: :none }.
252
253
  # Be aware that #first/#last won't guarantee order in this case.
253
254
  #
254
- # @param [ Hash ] opts The options for the query returning the first document.
255
+ # @param [ Integer | Hash ] limit_or_opts The number of documents to
256
+ # return, or a hash of options.
255
257
  #
256
- # @option opts [ :none ] :id_sort Don't apply a sort on _id if no other sort
257
- # is defined on the criteria.
258
+ # @option limit_or_opts [ :none ] :id_sort This option is deprecated.
259
+ # Don't apply a sort on _id if no other sort is defined on the criteria.
258
260
  #
259
261
  # @return [ Document ] The first document.
260
- def first(opts = {})
261
- return documents.first if cached? && cache_loaded?
262
- try_cache(:first) do
263
- if sort = view.sort || ({ _id: 1 } unless opts[:id_sort] == :none)
264
- if raw_doc = view.sort(sort).limit(1).first
265
- doc = Factory.from_db(klass, raw_doc, criteria)
266
- eager_load([doc]).first
267
- end
268
- else
269
- if raw_doc = view.limit(1).first
270
- doc = Factory.from_db(klass, raw_doc, criteria)
271
- eager_load([doc]).first
272
- end
262
+ def first(limit_or_opts = nil)
263
+ limit = limit_or_opts unless limit_or_opts.is_a?(Hash)
264
+ if cached? && cache_loaded?
265
+ return limit ? documents.first(limit) : documents.first
266
+ end
267
+ try_numbered_cache(:first, limit) do
268
+ if limit_or_opts.try(:key?, :id_sort)
269
+ Mongoid::Warnings.warn_id_sort_deprecated
270
+ end
271
+ sorted_view = view
272
+ if sort = view.sort || ({ _id: 1 } unless limit_or_opts.try(:fetch, :id_sort) == :none)
273
+ sorted_view = view.sort(sort)
274
+ end
275
+ if raw_docs = sorted_view.limit(limit || 1).to_a
276
+ process_raw_docs(raw_docs, limit)
273
277
  end
274
278
  end
275
279
  end
@@ -325,6 +329,10 @@ module Mongoid
325
329
  #
326
330
  # @return [ Array ] The result of mapping.
327
331
  def map(field = nil, &block)
332
+ if !field.nil?
333
+ Mongoid::Warnings.warn_map_field_deprecated
334
+ end
335
+
328
336
  if block_given?
329
337
  super(&block)
330
338
  else
@@ -360,19 +368,26 @@ module Mongoid
360
368
  # and have no sort defined on the criteria, use the option { id_sort: :none }.
361
369
  # Be aware that #first/#last won't guarantee order in this case.
362
370
  #
363
- # @param [ Hash ] opts The options for the query returning the first document.
371
+ # @param [ Integer | Hash ] limit_or_opts The number of documents to
372
+ # return, or a hash of options.
364
373
  #
365
- # @option opts [ :none ] :id_sort Don't apply a sort on _id if no other sort
366
- # is defined on the criteria.
367
- def last(opts = {})
368
- try_cache(:last) do
369
- with_inverse_sorting(opts) do
370
- if raw_doc = view.limit(1).first
371
- doc = Factory.from_db(klass, raw_doc, criteria)
372
- eager_load([doc]).first
374
+ # @option limit_or_opts [ :none ] :id_sort This option is deprecated.
375
+ # Don't apply a sort on _id if no other sort is defined on the criteria.
376
+ #
377
+ # @return [ Document ] The last document.
378
+ def last(limit_or_opts = nil)
379
+ limit = limit_or_opts unless limit_or_opts.is_a?(Hash)
380
+ if cached? && cache_loaded?
381
+ return limit ? documents.last(limit) : documents.last
382
+ end
383
+ res = try_numbered_cache(:last, limit) do
384
+ with_inverse_sorting(limit_or_opts) do
385
+ if raw_docs = view.limit(limit || 1).to_a
386
+ process_raw_docs(raw_docs, limit)
373
387
  end
374
388
  end
375
389
  end
390
+ res.is_a?(Array) ? res.reverse : res
376
391
  end
377
392
 
378
393
  # Get's the number of documents matching the query selector.
@@ -398,6 +413,44 @@ module Mongoid
398
413
  @view = view.limit(value) and self
399
414
  end
400
415
 
416
+ # Take the given number of documents from the database.
417
+ #
418
+ # @example Take 10 documents
419
+ # context.take(10)
420
+ #
421
+ # @param [ Integer | nil ] limit The number of documents to return or nil.
422
+ #
423
+ # @return [ Document | Array<Document> ] The list of documents, or one
424
+ # document if no value was given.
425
+ def take(limit = nil)
426
+ if limit
427
+ limit(limit).to_a
428
+ else
429
+ # Do to_a first so that the Mongo#first method is not used and the
430
+ # result is not sorted.
431
+ limit(1).to_a.first
432
+ end
433
+ end
434
+
435
+ # Take one document from the database and raise an error if there are none.
436
+ #
437
+ # @example Take a document
438
+ # context.take!
439
+ #
440
+ # @return [ Document ] The document.
441
+ #
442
+ # @raises [ Mongoid::Errors::DocumentNotFound ] raises when there are no
443
+ # documents to take.
444
+ def take!
445
+ # Do to_a first so that the Mongo#first method is not used and the
446
+ # result is not sorted.
447
+ if fst = limit(1).to_a.first
448
+ fst
449
+ else
450
+ raise Errors::DocumentNotFound.new(klass, nil, nil)
451
+ end
452
+ end
453
+
401
454
  # Initiate a map/reduce operation from the context.
402
455
  #
403
456
  # @example Initiate a map/reduce.
@@ -534,6 +587,31 @@ module Mongoid
534
587
  end
535
588
  end
536
589
 
590
+ # yield the block given or return the cached value
591
+ #
592
+ # @param [ String, Symbol ] key The instance variable name
593
+ # @param [ Integer | nil ] n The number of documents requested or nil
594
+ # if none is requested.
595
+ #
596
+ # @return [ Object ] The result of the block.
597
+ def try_numbered_cache(key, n, &block)
598
+ unless cached?
599
+ yield if block_given?
600
+ else
601
+ len = n || 1
602
+ ret = instance_variable_get("@#{key}")
603
+ if !ret || ret.length < len
604
+ instance_variable_set("@#{key}", ret = Array.wrap(yield))
605
+ elsif !n
606
+ ret.is_a?(Array) ? ret.first : ret
607
+ elsif ret.length > len
608
+ ret.first(n)
609
+ else
610
+ ret
611
+ end
612
+ end
613
+ end
614
+
537
615
  # Update the documents for the provided method.
538
616
  #
539
617
  # @api private
@@ -598,8 +676,10 @@ module Mongoid
598
676
  # @example Apply the inverse sorting params to the given block
599
677
  # context.with_inverse_sorting
600
678
  def with_inverse_sorting(opts = {})
679
+ Mongoid::Warnings.warn_id_sort_deprecated if opts.try(:key?, :id_sort)
680
+
601
681
  begin
602
- if sort = criteria.options[:sort] || ( { _id: 1 } unless opts[:id_sort] == :none )
682
+ if sort = criteria.options[:sort] || ( { _id: 1 } unless opts.try(:fetch, :id_sort) == :none )
603
683
  @view = view.sort(Hash[sort.map{|k, v| [k, -1*v]}])
604
684
  end
605
685
  yield
@@ -793,6 +873,18 @@ module Mongoid
793
873
  end
794
874
  end
795
875
  end
876
+
877
+ # Process the raw documents retrieved for #first/#last.
878
+ #
879
+ # @return [ Array<Document> | Document ] The list of documents or a
880
+ # single document.
881
+ def process_raw_docs(raw_docs, limit)
882
+ docs = raw_docs.map do |d|
883
+ Factory.from_db(klass, d, criteria)
884
+ end
885
+ docs = eager_load(docs)
886
+ limit ? docs : docs.first
887
+ end
796
888
  end
797
889
  end
798
890
  end
@@ -105,13 +105,57 @@ module Mongoid
105
105
  @criteria, @klass = criteria, criteria.klass
106
106
  end
107
107
 
108
+ # Always returns nil.
109
+ #
110
+ # @example Get the first document in null context.
111
+ # context.first
112
+ #
113
+ # @param [ Integer | Hash ] limit_or_opts The number of documents to
114
+ # return, or a hash of options.
115
+ #
116
+ # @return [ nil ] Always nil.
117
+ def first(limit_or_opts = nil)
118
+ if !limit_or_opts.nil? && !limit_or_opts.is_a?(Hash)
119
+ []
120
+ end
121
+ end
122
+
108
123
  # Always returns nil.
109
124
  #
110
125
  # @example Get the last document in null context.
111
126
  # context.last
112
127
  #
128
+ # @param [ Integer | Hash ] limit_or_opts The number of documents to
129
+ # return, or a hash of options.
130
+ #
113
131
  # @return [ nil ] Always nil.
114
- def last; nil; end
132
+ def last(limit_or_opts = nil)
133
+ if !limit_or_opts.nil? && !limit_or_opts.is_a?(Hash)
134
+ []
135
+ end
136
+ end
137
+
138
+ # Returns nil or empty array.
139
+ #
140
+ # @example Take a document in null context.
141
+ # context.take
142
+ #
143
+ # @param [ Integer | nil ] limit The number of documents to take or nil.
144
+ #
145
+ # @return [ [] | nil ] Empty array or nil.
146
+ def take(limit = nil)
147
+ limit ? [] : nil
148
+ end
149
+
150
+ # Always raises an error.
151
+ #
152
+ # @example Take a document in null context.
153
+ # context.take!
154
+ #
155
+ # @raises [ Mongoid::Errors::DocumentNotFound ] always raises.
156
+ def take!
157
+ raise Errors::DocumentNotFound.new(klass, nil, nil)
158
+ end
115
159
 
116
160
  # Always returns zero.
117
161
  #
@@ -116,6 +116,8 @@ module Mongoid
116
116
  # @param [ Proc ] block The block to execute on each value.
117
117
  #
118
118
  # @return [ Array ] the array.
119
+ #
120
+ # @deprecated
119
121
  def update_values(&block)
120
122
  replace(map(&block))
121
123
  end