mongoid 7.4.3 → 7.5.0

Sign up to get free protection for your applications and to get access to all the features.
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