mongoid 8.0.13 → 8.1.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 (225) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/CHANGELOG.md +3 -3
  4. data/README.md +3 -3
  5. data/Rakefile +17 -66
  6. data/lib/config/locales/en.yml +46 -40
  7. data/lib/mongoid/association/accessors.rb +3 -7
  8. data/lib/mongoid/association/builders.rb +1 -1
  9. data/lib/mongoid/association/depending.rb +4 -8
  10. data/lib/mongoid/association/eager_loadable.rb +0 -3
  11. data/lib/mongoid/association/embedded/batchable.rb +2 -2
  12. data/lib/mongoid/association/embedded/embedded_in/buildable.rb +2 -2
  13. data/lib/mongoid/association/embedded/embedded_in/proxy.rb +2 -1
  14. data/lib/mongoid/association/embedded/embeds_many/buildable.rb +3 -2
  15. data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -6
  16. data/lib/mongoid/association/embedded/embeds_one/buildable.rb +1 -1
  17. data/lib/mongoid/association/embedded/embeds_one/proxy.rb +2 -2
  18. data/lib/mongoid/association/macros.rb +0 -6
  19. data/lib/mongoid/association/nested/many.rb +3 -38
  20. data/lib/mongoid/association/nested/nested_buildable.rb +0 -14
  21. data/lib/mongoid/association/nested/one.rb +40 -2
  22. data/lib/mongoid/association/proxy.rb +1 -1
  23. data/lib/mongoid/association/referenced/counter_cache.rb +2 -2
  24. data/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb +1 -1
  25. data/lib/mongoid/association/referenced/has_many/enumerable.rb +6 -23
  26. data/lib/mongoid/association/referenced/has_many/proxy.rb +4 -103
  27. data/lib/mongoid/association/reflections.rb +2 -2
  28. data/lib/mongoid/atomic.rb +7 -16
  29. data/lib/mongoid/attributes/dynamic.rb +1 -1
  30. data/lib/mongoid/attributes/nested.rb +2 -2
  31. data/lib/mongoid/attributes/processing.rb +5 -29
  32. data/lib/mongoid/attributes/projector.rb +1 -1
  33. data/lib/mongoid/attributes/readonly.rb +1 -1
  34. data/lib/mongoid/attributes.rb +8 -2
  35. data/lib/mongoid/changeable.rb +107 -5
  36. data/lib/mongoid/clients/factory.rb +0 -4
  37. data/lib/mongoid/clients/storage_options.rb +2 -5
  38. data/lib/mongoid/clients/validators/storage.rb +1 -13
  39. data/lib/mongoid/collection_configurable.rb +58 -0
  40. data/lib/mongoid/composable.rb +2 -0
  41. data/lib/mongoid/config/defaults.rb +60 -0
  42. data/lib/mongoid/config/options.rb +0 -3
  43. data/lib/mongoid/config/validators/async_query_executor.rb +24 -0
  44. data/lib/mongoid/config/validators.rb +1 -0
  45. data/lib/mongoid/config.rb +88 -89
  46. data/lib/mongoid/contextual/aggregable/memory.rb +2 -5
  47. data/lib/mongoid/contextual/atomic.rb +1 -1
  48. data/lib/mongoid/contextual/memory.rb +240 -51
  49. data/lib/mongoid/contextual/mongo/documents_loader.rb +177 -0
  50. data/lib/mongoid/contextual/mongo.rb +370 -133
  51. data/lib/mongoid/contextual/none.rb +162 -7
  52. data/lib/mongoid/contextual.rb +12 -0
  53. data/lib/mongoid/criteria/findable.rb +2 -2
  54. data/lib/mongoid/criteria/includable.rb +4 -3
  55. data/lib/mongoid/criteria/queryable/extensions/numeric.rb +1 -15
  56. data/lib/mongoid/criteria/queryable/key.rb +1 -1
  57. data/lib/mongoid/criteria/queryable/mergeable.rb +1 -13
  58. data/lib/mongoid/criteria/queryable/optional.rb +8 -8
  59. data/lib/mongoid/criteria/queryable/selectable.rb +43 -121
  60. data/lib/mongoid/criteria/queryable/selector.rb +1 -1
  61. data/lib/mongoid/criteria/queryable/storable.rb +1 -1
  62. data/lib/mongoid/criteria.rb +6 -5
  63. data/lib/mongoid/deprecable.rb +1 -2
  64. data/lib/mongoid/deprecation.rb +3 -3
  65. data/lib/mongoid/document.rb +1 -8
  66. data/lib/mongoid/errors/create_collection_failure.rb +33 -0
  67. data/lib/mongoid/errors/drop_collection_failure.rb +27 -0
  68. data/lib/mongoid/errors/immutable_attribute.rb +26 -0
  69. data/lib/mongoid/errors/invalid_async_query_executor.rb +25 -0
  70. data/lib/mongoid/errors/invalid_global_executor_concurrency.rb +22 -0
  71. data/lib/mongoid/errors/invalid_storage_parent.rb +2 -0
  72. data/lib/mongoid/errors.rb +4 -2
  73. data/lib/mongoid/extensions/hash.rb +3 -51
  74. data/lib/mongoid/extensions/object.rb +2 -2
  75. data/lib/mongoid/extensions/time.rb +2 -0
  76. data/lib/mongoid/fields/localized.rb +10 -0
  77. data/lib/mongoid/fields/standard.rb +10 -0
  78. data/lib/mongoid/fields.rb +59 -35
  79. data/lib/mongoid/findable.rb +27 -3
  80. data/lib/mongoid/interceptable.rb +6 -116
  81. data/lib/mongoid/matchable.rb +1 -6
  82. data/lib/mongoid/matcher/eq_impl.rb +1 -1
  83. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +5 -3
  84. data/lib/mongoid/matcher/regex.rb +9 -8
  85. data/lib/mongoid/matcher/type.rb +1 -1
  86. data/lib/mongoid/matcher.rb +0 -1
  87. data/lib/mongoid/persistable/creatable.rb +1 -0
  88. data/lib/mongoid/persistable/deletable.rb +1 -1
  89. data/lib/mongoid/persistable/savable.rb +13 -1
  90. data/lib/mongoid/persistable/unsettable.rb +2 -2
  91. data/lib/mongoid/persistable/updatable.rb +51 -1
  92. data/lib/mongoid/persistable/upsertable.rb +20 -1
  93. data/lib/mongoid/persistable.rb +3 -0
  94. data/lib/mongoid/query_cache.rb +5 -1
  95. data/lib/mongoid/railties/database.rake +7 -2
  96. data/lib/mongoid/reloadable.rb +5 -3
  97. data/lib/mongoid/stateful.rb +22 -1
  98. data/lib/mongoid/tasks/database.rake +12 -0
  99. data/lib/mongoid/tasks/database.rb +20 -0
  100. data/lib/mongoid/threaded.rb +0 -3
  101. data/lib/mongoid/timestamps/created.rb +1 -8
  102. data/lib/mongoid/traversable.rb +0 -12
  103. data/lib/mongoid/utils.rb +22 -0
  104. data/lib/mongoid/validatable/associated.rb +17 -98
  105. data/lib/mongoid/validatable/macros.rb +5 -20
  106. data/lib/mongoid/validatable.rb +4 -10
  107. data/lib/mongoid/version.rb +1 -5
  108. data/lib/mongoid/warnings.rb +17 -2
  109. data/lib/mongoid.rb +16 -3
  110. data/spec/integration/app_spec.rb +2 -20
  111. data/spec/integration/associations/embeds_one_spec.rb +5 -25
  112. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +0 -55
  113. data/spec/integration/callbacks_spec.rb +99 -12
  114. data/spec/integration/discriminator_key_spec.rb +4 -5
  115. data/spec/integration/dots_and_dollars_spec.rb +2 -12
  116. data/spec/integration/i18n_fallbacks_spec.rb +3 -2
  117. data/spec/integration/matcher_operator_data/regex.yml +0 -21
  118. data/spec/mongoid/association/eager_spec.rb +2 -24
  119. data/spec/mongoid/association/embedded/embedded_in/proxy_spec.rb +27 -0
  120. data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +20 -25
  121. data/spec/mongoid/association/embedded/embeds_many_models.rb +1 -0
  122. data/spec/mongoid/association/embedded/embeds_many_query_spec.rb +0 -4
  123. data/spec/mongoid/association/embedded/embeds_one/proxy_spec.rb +15 -2
  124. data/spec/mongoid/association/referenced/belongs_to_spec.rb +2 -18
  125. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +42 -103
  126. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +9 -195
  127. data/spec/mongoid/association/syncable_spec.rb +1 -1
  128. data/spec/mongoid/attributes/nested_spec.rb +10 -204
  129. data/spec/mongoid/attributes_spec.rb +3 -33
  130. data/spec/mongoid/changeable_spec.rb +299 -24
  131. data/spec/mongoid/clients/factory_spec.rb +0 -30
  132. data/spec/mongoid/clients_spec.rb +122 -13
  133. data/spec/mongoid/collection_configurable_spec.rb +158 -0
  134. data/spec/mongoid/config/defaults_spec.rb +160 -0
  135. data/spec/mongoid/config_spec.rb +154 -27
  136. data/spec/mongoid/contextual/aggregable/memory_spec.rb +0 -91
  137. data/spec/mongoid/contextual/memory_spec.rb +332 -253
  138. data/spec/mongoid/contextual/mongo/documents_loader_spec.rb +187 -0
  139. data/spec/mongoid/contextual/mongo_spec.rb +1009 -125
  140. data/spec/mongoid/contextual/none_spec.rb +49 -2
  141. data/spec/mongoid/copyable_spec.rb +2 -10
  142. data/spec/mongoid/criteria/queryable/extensions/string_spec.rb +4 -10
  143. data/spec/mongoid/criteria/queryable/options_spec.rb +1 -1
  144. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +419 -2
  145. data/spec/mongoid/criteria/queryable/selectable_spec.rb +1 -1
  146. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +0 -235
  147. data/spec/mongoid/criteria/queryable/selector_spec.rb +3 -76
  148. data/spec/mongoid/criteria/queryable/storable_spec.rb +0 -72
  149. data/spec/mongoid/criteria_projection_spec.rb +1 -4
  150. data/spec/mongoid/criteria_spec.rb +5 -11
  151. data/spec/mongoid/document_spec.rb +0 -27
  152. data/spec/mongoid/errors/readonly_document_spec.rb +2 -2
  153. data/spec/mongoid/extensions/hash_spec.rb +3 -239
  154. data/spec/mongoid/extensions/time_spec.rb +8 -43
  155. data/spec/mongoid/extensions/time_with_zone_spec.rb +7 -52
  156. data/spec/mongoid/fields/localized_spec.rb +46 -28
  157. data/spec/mongoid/fields_spec.rb +136 -77
  158. data/spec/mongoid/findable_spec.rb +391 -34
  159. data/spec/mongoid/indexable_spec.rb +16 -10
  160. data/spec/mongoid/interceptable_spec.rb +153 -442
  161. data/spec/mongoid/interceptable_spec_models.rb +111 -51
  162. data/spec/mongoid/persistable/deletable_spec.rb +26 -6
  163. data/spec/mongoid/persistable/destroyable_spec.rb +26 -6
  164. data/spec/mongoid/persistable/incrementable_spec.rb +37 -0
  165. data/spec/mongoid/persistable/logical_spec.rb +37 -0
  166. data/spec/mongoid/persistable/poppable_spec.rb +36 -0
  167. data/spec/mongoid/persistable/pullable_spec.rb +72 -0
  168. data/spec/mongoid/persistable/pushable_spec.rb +72 -0
  169. data/spec/mongoid/persistable/renamable_spec.rb +36 -0
  170. data/spec/mongoid/persistable/savable_spec.rb +96 -0
  171. data/spec/mongoid/persistable/settable_spec.rb +37 -0
  172. data/spec/mongoid/persistable/unsettable_spec.rb +36 -0
  173. data/spec/mongoid/persistable/updatable_spec.rb +20 -28
  174. data/spec/mongoid/persistable/upsertable_spec.rb +80 -6
  175. data/spec/mongoid/persistence_context_spec.rb +7 -57
  176. data/spec/mongoid/query_cache_spec.rb +56 -61
  177. data/spec/mongoid/reloadable_spec.rb +24 -28
  178. data/spec/mongoid/scopable_spec.rb +70 -0
  179. data/spec/mongoid/serializable_spec.rb +23 -44
  180. data/spec/mongoid/stateful_spec.rb +122 -8
  181. data/spec/mongoid/tasks/database_rake_spec.rb +74 -0
  182. data/spec/mongoid/tasks/database_spec.rb +127 -0
  183. data/spec/mongoid/timestamps/created_spec.rb +0 -23
  184. data/spec/mongoid/timestamps_spec.rb +9 -11
  185. data/spec/mongoid/touchable_spec.rb +277 -5
  186. data/spec/mongoid/touchable_spec_models.rb +3 -1
  187. data/spec/mongoid/traversable_spec.rb +9 -24
  188. data/spec/mongoid/validatable/associated_spec.rb +34 -27
  189. data/spec/mongoid/validatable/numericality_spec.rb +0 -16
  190. data/spec/mongoid/validatable/uniqueness_spec.rb +2 -3
  191. data/spec/mongoid_spec.rb +36 -10
  192. data/spec/shared/lib/mrss/docker_runner.rb +1 -1
  193. data/spec/shared/lib/mrss/server_version_registry.rb +1 -1
  194. data/spec/shared/lib/mrss/spec_organizer.rb +3 -32
  195. data/spec/shared/share/Dockerfile.erb +85 -15
  196. data/spec/shared/shlib/distro.sh +0 -10
  197. data/spec/shared/shlib/server.sh +27 -34
  198. data/spec/shared/shlib/set_env.sh +68 -9
  199. data/spec/spec_helper.rb +5 -0
  200. data/spec/support/expectations.rb +17 -20
  201. data/spec/support/immutable_ids.rb +118 -0
  202. data/spec/support/macros.rb +47 -15
  203. data/spec/support/models/artist.rb +0 -1
  204. data/spec/support/models/band.rb +1 -0
  205. data/spec/support/models/building.rb +2 -0
  206. data/spec/support/models/name.rb +0 -10
  207. data/spec/support/models/person.rb +0 -1
  208. data/spec/support/models/product.rb +1 -0
  209. data.tar.gz.sig +0 -0
  210. metadata +715 -671
  211. metadata.gz.sig +2 -0
  212. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +0 -26
  213. data/lib/mongoid/field_readable.rb +0 -70
  214. data/lib/mongoid/matcher/regexp_budget.rb +0 -389
  215. data/lib/mongoid/validatable/numericality.rb +0 -19
  216. data/spec/integration/matcher_regexp_timeout_spec.rb +0 -215
  217. data/spec/integration/query_operator_guard_spec.rb +0 -89
  218. data/spec/mongoid/criteria/queryable/extensions/bignum_spec.rb +0 -61
  219. data/spec/mongoid/criteria/queryable/extensions/fixnum_spec.rb +0 -61
  220. data/spec/mongoid/matcher/regexp_budget_spec.rb +0 -570
  221. data/spec/shared/CANDIDATE.md +0 -28
  222. data/spec/shared/lib/mrss/release/candidate.rb +0 -281
  223. data/spec/shared/lib/mrss/release/product_data.rb +0 -144
  224. data/spec/shared/lib/tasks/candidate.rake +0 -64
  225. data/spec/support/models/purse.rb +0 -9
@@ -53,26 +53,12 @@ module Mongoid
53
53
  # @example Convert the id.
54
54
  # builder.convert_id(Person, "4d371b444835d98b8b000010")
55
55
  #
56
- # Ids arriving from a form are always scalars. A Hash or an Array
57
- # here means the parameters were crafted, and letting one through
58
- # would turn the id into a query operator, so they are rejected.
59
- #
60
56
  # @param [ Class ] klass The class we're trying to convert for.
61
57
  # @param [ String ] id The id, usually coming from the form.
62
58
  #
63
59
  # @return [ BSON::ObjectId | String | Object ] The converted id.
64
- #
65
- # @raise [ Errors::DocumentNotFound ] if the id is not a scalar, or
66
- # cannot be converted to the type the class uses for its ids.
67
- # The BSON::Error rescue is defensive: a value that reaches
68
- # BSON::ObjectId.mongoize and raises there must not surface as an
69
- # unhandled BSON error in the caller.
70
60
  def convert_id(klass, id)
71
- raise Errors::DocumentNotFound.new(klass, id) if id.is_a?(::Hash) || id.is_a?(::Array)
72
-
73
61
  klass.using_object_ids? ? BSON::ObjectId.mongoize(id) : id
74
- rescue BSON::Error
75
- raise Errors::DocumentNotFound.new(klass, id)
76
62
  end
77
63
  end
78
64
  end
@@ -32,6 +32,8 @@ module Mongoid
32
32
  parent.send(association.setter, Factory.build(@class_name, attributes))
33
33
  elsif delete?
34
34
  parent.send(association.setter, nil)
35
+ else
36
+ check_for_id_violation!
35
37
  end
36
38
  end
37
39
 
@@ -54,6 +56,17 @@ module Mongoid
54
56
 
55
57
  private
56
58
 
59
+ # Extracts and converts the id to the expected type.
60
+ #
61
+ # @return [ BSON::ObjectId | String | Object | nil ] The converted id,
62
+ # or nil if no id is present in the attributes hash.
63
+ def extracted_id
64
+ @extracted_id ||= begin
65
+ id = association.klass.extract_id_field(attributes)
66
+ convert_id(existing.class, id)
67
+ end
68
+ end
69
+
57
70
  # Is the id in the attributes acceptable for allowing an update to
58
71
  # the existing association?
59
72
  #
@@ -64,8 +77,7 @@ module Mongoid
64
77
  #
65
78
  # @return [ true | false ] If the id part of the logic will allow an update.
66
79
  def acceptable_id?
67
- id = association.klass.extract_id_field(attributes)
68
- id = convert_id(existing.class, id)
80
+ id = extracted_id
69
81
  existing._id == id || id.nil? || (existing._id != id && update_only?)
70
82
  end
71
83
 
@@ -110,6 +122,32 @@ module Mongoid
110
122
  def update?
111
123
  existing && !destroyable? && acceptable_id?
112
124
  end
125
+
126
+ # Checks to see if the _id attribute (which is supposed to be
127
+ # immutable) is being asked to change. If so, raise an exception.
128
+ #
129
+ # If Mongoid::Config.immutable_ids is false, this will do nothing,
130
+ # and the update operation will fail silently.
131
+ #
132
+ # @raise [ Errors::ImmutableAttribute ] if _id has changed, and
133
+ # the document has been persisted.
134
+ def check_for_id_violation!
135
+ # look for the basic criteria of an update (see #update?)
136
+ return unless existing&.persisted? && !destroyable?
137
+
138
+ # if the id is either absent, or if it equals the existing record's
139
+ # id, there is no immutability violation.
140
+ id = extracted_id
141
+ return if existing._id == id || id.nil?
142
+
143
+ # otherwise, an attempt has been made to set the _id of an existing,
144
+ # persisted document.
145
+ if Mongoid::Config.immutable_ids
146
+ raise Errors::ImmutableAttribute.new(:_id, id)
147
+ else
148
+ Mongoid::Warnings.warn_mutable_ids
149
+ end
150
+ end
113
151
  end
114
152
  end
115
153
  end
@@ -118,7 +118,7 @@ module Mongoid
118
118
  # to the target of the proxy. This can be overridden in special cases.
119
119
  #
120
120
  # @param [ String | Symbol ] name The name of the method.
121
- # @param [ Array ] args The arguments passed to the method.
121
+ # @param [ Object... ] *args The arguments passed to the method.
122
122
  ruby2_keywords def method_missing(name, *args, &block)
123
123
  _target.send(name, *args, &block)
124
124
  end
@@ -13,7 +13,7 @@ module Mongoid
13
13
  # @example Reset the given counter cache
14
14
  # post.reset_counters(:comments)
15
15
  #
16
- # @param [ Symbol | Array ] counters One or more counter caches to reset
16
+ # @param [ Symbol... ] *counters One or more counter caches to reset.
17
17
  def reset_counters(*counters)
18
18
  self.class.with(persistence_context) do |_class|
19
19
  _class.reset_counters(self, *counters)
@@ -30,7 +30,7 @@ module Mongoid
30
30
  # Post.reset_counters('50e0edd97c71c17ea9000001', :comments)
31
31
  #
32
32
  # @param [ String ] id The id of the object that will be reset.
33
- # @param [ Symbol | Array ] counters One or more counter caches to reset
33
+ # @param [ Symbol... ] *counters One or more counter caches to reset.
34
34
  def reset_counters(id, *counters)
35
35
  document = id.is_a?(Document) ? id : find(id)
36
36
  counters.each do |name|
@@ -21,7 +21,7 @@ module Mongoid
21
21
  # @example Concat with other documents.
22
22
  # person.posts.concat([ post_one, post_two ])
23
23
  #
24
- # @param [ Document | Array<Document> ] args Any number of documents.
24
+ # @param [ Document... ] *args Any number of documents.
25
25
  #
26
26
  # @return [ Array<Document> ] The loaded docs.
27
27
  def <<(*args)
@@ -215,13 +215,13 @@ module Mongoid
215
215
  # completely depending on whether it is iterated to completion.
216
216
  #
217
217
  # This method can take a parameter and a block. The behavior with
218
- # either the paramater or the block is delegated to the standard
218
+ # either the parameter or the block is delegated to the standard
219
219
  # library Enumerable module.
220
220
  #
221
221
  # Note that when Enumerable's any? method is invoked with both
222
222
  # a block and a pattern, it only uses the pattern.
223
223
  #
224
- # @param [ Object ] condition The condition that documents
224
+ # @param [ Object... ] *args The condition that documents
225
225
  # must satisfy. See Enumerable documentation for details.
226
226
  #
227
227
  # @return [ true | false ] If the association has any documents.
@@ -422,28 +422,11 @@ module Mongoid
422
422
  #
423
423
  # @return [ Integer ] The size of the enumerable.
424
424
  def size
425
- # If _unloaded is present, then it will match the set of documents
426
- # that belong to this association, which have already been persisted
427
- # to the database. This set of documents must be considered when
428
- # computing the size of the association, along with anything that has
429
- # since been added.
430
- if _unloaded
431
- if _added.any?
432
- # Note that _added may include records that _unloaded already
433
- # matches. This is the case if the association is assigned an array
434
- # of items and some of them were already elements of the association.
435
- #
436
- # we need to thus make sure _unloaded.count excludes any elements
437
- # that already exist in _added.
438
-
439
- count = _unloaded.not(:_id.in => _added.values.map(&:id)).count
440
- count + _added.values.count
441
- else
442
- _unloaded.count
443
- end
444
-
425
+ count = (_unloaded ? _unloaded.count : _loaded.count)
426
+ if count.zero?
427
+ count + _added.count
445
428
  else
446
- _loaded.count + _added.count
429
+ count + _added.values.count { |d| d.new_record? }
447
430
  end
448
431
  end
449
432
 
@@ -25,7 +25,7 @@ module Mongoid
25
25
  # @example Concat with other documents.
26
26
  # person.posts.concat([ post_one, post_two ])
27
27
  #
28
- # @param [ Document | Array<Document> ] args Any number of documents.
28
+ # @param [ Document... ] *args Any number of documents.
29
29
  #
30
30
  # @return [ Array<Document> ] The loaded docs.
31
31
  def <<(*args)
@@ -203,7 +203,7 @@ module Mongoid
203
203
  # @note This will keep matching documents in memory for iteration
204
204
  # later.
205
205
  #
206
- # @param [ Object | Array<Object> ] *args The ids.
206
+ # @param [ [ Object | Array<Object> ]... ] *args The ids.
207
207
  # @param [ Proc ] block Optional block to pass.
208
208
  #
209
209
  # @return [ Document | Array<Document> | nil ] A document or matching documents.
@@ -418,7 +418,7 @@ module Mongoid
418
418
  # If the method exists on the array, use the default proxy behavior.
419
419
  #
420
420
  # @param [ Symbol | String ] name The name of the method.
421
- # @param [ Array ] args The method args
421
+ # @param [ Object... ] *args The method args
422
422
  # @param [ Proc ] block Optional block to pass.
423
423
  #
424
424
  # @return [ Criteria | Object ] A Criteria or return value from the target.
@@ -477,111 +477,12 @@ module Mongoid
477
477
  # @return [ Integer ] The number of documents deleted.
478
478
  def remove_all(conditions = nil, method = :delete_all)
479
479
  selector = conditions || {}
480
- selector.merge!(criteria.selector)
481
-
482
- # Scanning before the delete means scanning only what is already in
483
- # memory, so it is only done where there is a pattern whose cost has
484
- # to be bounded. Everywhere else the delete comes first and the scan
485
- # sees the survivors, which is what this association has always
486
- # done.
487
- #
488
- # The limit is read once and carried into whichever branch is taken.
489
- # Letting the branch below ask again would read it a second time,
490
- # across a server round trip, and a limit that had become positive
491
- # in the meantime would put a deadline on the delete_if -- a query,
492
- # and an unbind of every match -- which is the arrangement this
493
- # branch exists to avoid.
494
- limit = Matcher::RegexpBudget.limit_for(selector)
495
- return remove_all_bounded(selector, method, limit) if limit
496
-
497
- removed = klass.send(method, selector)
498
-
499
- # No scope around this. The scan runs after the delete, so the
500
- # association this loads comes back holding only what the delete did
501
- # not match, and there is no pattern here to bound in any case.
502
- # Opening a scope would save _matches? from deciding once per
503
- # document -- 0.48us against 1.74us for the match itself -- but a
504
- # scope with nothing to bound suppresses that decision for
505
- # everything nested inside it, and loading the association runs
506
- # find callbacks. A pattern in a selector one of those evaluates
507
- # would go unguarded, which costs more than the saving is worth on a
508
- # path that has just made two round trips.
480
+ removed = klass.send(method, selector.merge!(criteria.selector))
509
481
  _target.delete_if do |doc|
510
482
  doc._matches?(selector).tap do |b|
511
483
  unbind_one(doc) if b
512
484
  end
513
485
  end
514
-
515
- removed
516
- end
517
-
518
- # Deletes all related documents matching a selector that carries a
519
- # regular expression, with the whole scan under one regexp budget.
520
- #
521
- # The scan runs before anything is deleted, so a budget that runs out
522
- # leaves both the database and the association untouched rather than
523
- # reporting a failure for a delete that has already happened. It also
524
- # means the budget is closed by the time the association is mutated,
525
- # so where the budget is enforced with Timeout there is no window for
526
- # the exception to land in the middle of an unbind.
527
- #
528
- # @param [ Hash ] selector The selector to delete with.
529
- # @param [ Symbol ] method The deletion method to call.
530
- # @param [ Float ] limit The seconds the scan may spend, as read by
531
- # the caller when it chose this path.
532
- #
533
- # @return [ Integer ] The number of documents deleted.
534
- def remove_all_bounded(selector, method, limit)
535
- # Only what the association already holds. Iterating it instead
536
- # would load the whole association before the delete, and for a
537
- # broad pattern that is every document the association has: /.*/ is
538
- # both the cheapest pattern an attacker can supply and the one that
539
- # matches everything, so bounding the cost of the matching would
540
- # have been paid for with an unbounded amount of memory. Nothing is
541
- # missed by not loading, because the server evaluates the same
542
- # selector for the delete itself.
543
- #
544
- # It also means the scan runs no query, so no deadline of ours can
545
- # land on one. Where the budget is enforced with Timeout, covering
546
- # a query would report a slow network as a regexp timeout and could
547
- # deliver the asynchronous exception inside the driver's socket
548
- # read.
549
- documents = _target.in_memory
550
-
551
- matching = Matcher::RegexpBudget.open_with(limit) do
552
- documents.select { |doc| doc._matches?(selector) }
553
- end
554
-
555
- removed = klass.send(method, selector)
556
-
557
- # The ids are already known to be in memory, so the loaded and added
558
- # documents can be dropped directly. Enumerable#delete would look
559
- # each one up by id and stop at the first hash that has it, which
560
- # removes the wrong instance when both hashes hold one for the same
561
- # id. Enumerable#delete_if drops it from both, as this does, but
562
- # only after load_all!, which is the load this scan exists to avoid.
563
- #
564
- # Which documents get unbound has always depended on which ones
565
- # happened to be in memory, and scanning only what is in memory
566
- # keeps that. It matters for has_and_belongs_to_many, whose
567
- # unbind_one pulls the id out of the foreign key array on _base and
568
- # marks it dirty; the scan used to run after the delete, against an
569
- # association that a cold proxy had just reloaded as holding only
570
- # the survivors, so nothing matched and nothing was unbound.
571
- #
572
- # TODO: unbinding should not depend on what was in memory. Every
573
- # matched document leaves the association, so every one of them
574
- # should be unbound, which would also stop _base's foreign key
575
- # array from keeping ids that no longer resolve. That is a
576
- # behaviour change for has_and_belongs_to_many and belongs in a
577
- # ticket of its own, with a release note; it should not ride along
578
- # on this one.
579
- matching.each do |doc|
580
- unbind_one(doc)
581
- _target._loaded.delete(doc._id)
582
- _target._added.delete(doc._id)
583
- end
584
-
585
486
  removed
586
487
  end
587
488
 
@@ -25,7 +25,7 @@ module Mongoid
25
25
  # @example Find multiple association metadata by macro.
26
26
  # person.reflect_on_all_associations(:embeds_many)
27
27
  #
28
- # @param [ Array<Symbol> ] macros The association macros.
28
+ # @param [ Symbol... ] *macros The association macros.
29
29
  #
30
30
  # @return [ Array<Association> ] The matching association metadata.
31
31
  def reflect_on_all_association(*macros)
@@ -51,7 +51,7 @@ module Mongoid
51
51
  # @example Find multiple association metadata by macro.
52
52
  # Person.reflect_on_all_associations(:embeds_many)
53
53
  #
54
- # @param [ Array<Symbol> ] macros The association macros.
54
+ # @param [ Symbol... ] *macros The association macros.
55
55
  #
56
56
  # @return [ Array<Association> ] The matching association metadata.
57
57
  def reflect_on_all_associations(*macros)
@@ -178,15 +178,13 @@ module Mongoid
178
178
  #
179
179
  # @return [ Object ] The associated path.
180
180
  def atomic_paths
181
- return @atomic_paths if @atomic_paths
182
-
183
- paths = if _association
184
- _association.path(self)
185
- else
186
- Atomic::Paths::Root.new(self)
187
- end
188
-
189
- paths.tap { @atomic_paths = paths unless new_record? }
181
+ @atomic_paths ||= begin
182
+ if _association
183
+ _association.path(self)
184
+ else
185
+ Atomic::Paths::Root.new(self)
186
+ end
187
+ end
190
188
  end
191
189
 
192
190
  # Get all the attributes that need to be pulled.
@@ -313,13 +311,6 @@ module Mongoid
313
311
 
314
312
  private
315
313
 
316
- # Clears all pending atomic updates.
317
- def reset_atomic_updates!
318
- Atomic::UPDATES.each do |update|
319
- send(update).clear
320
- end
321
- end
322
-
323
314
  # Generates the atomic updates in the correct order.
324
315
  #
325
316
  # @example Generate the updates.
@@ -115,7 +115,7 @@ module Mongoid
115
115
  # document.method_missing(:test)
116
116
  #
117
117
  # @param [ String | Symbol ] name The name of the method.
118
- # @param [ Array ] args The arguments to the method.
118
+ # @param [ Object... ] *args The arguments to the method.
119
119
  #
120
120
  # @return [ Object ] The result of the method call.
121
121
  def method_missing(name, *args)
@@ -33,8 +33,8 @@ module Mongoid
33
33
  # accepts_nested_attributes_for :addresses, :game, :posts
34
34
  # end
35
35
  #
36
- # @param [ Array<Symbol> | Hash ] args A list of association names, followed
37
- # by a hash of options.
36
+ # @param [ Symbol..., Hash ] *args A list of association names, followed
37
+ # by an optional hash of options.
38
38
  #
39
39
  # @option *args [ true | false ] :allow_destroy Can deletion occur?
40
40
  # @option *args [ Proc | Symbol ] :reject_if Block or symbol pointing
@@ -43,46 +43,22 @@ module Mongoid
43
43
  # @return [ true | false ] True if pending, false if not.
44
44
  def pending_attribute?(key, value)
45
45
  name = key.to_s
46
+
46
47
  aliased = if aliased_associations.key?(name)
47
48
  aliased_associations[name]
48
49
  else
49
50
  name
50
51
  end
52
+
51
53
  if relations.has_key?(aliased)
52
- set_pending_relation(name, aliased, value)
54
+ pending_relations[name] = value
53
55
  return true
54
56
  end
55
57
  if nested_attributes.has_key?(aliased)
56
- set_pending_nested(name, aliased, value)
57
- return true
58
- end
59
- false
60
- end
61
-
62
- # Set value of the pending relation.
63
- #
64
- # @param [ Symbol ] name The name of the relation.
65
- # @param [ Symbol ] aliased The aliased name of the relation.
66
- # @param [ Object ] value The value of the relation.
67
- def set_pending_relation(name, aliased, value)
68
- if stored_as_associations.include?(name)
69
- pending_relations[aliased] = value
70
- else
71
- pending_relations[name] = value
72
- end
73
- end
74
-
75
- # Set value of the pending nested attribute.
76
- #
77
- # @param [ Symbol ] name The name of the nested attribute.
78
- # @param [ Symbol ] aliased The aliased name of the nested attribute.
79
- # @param [ Object ] value The value of the nested attribute.
80
- def set_pending_nested(name, aliased, value)
81
- if stored_as_associations.include?(name)
82
- pending_nested[aliased] = value
83
- else
84
58
  pending_nested[name] = value
59
+ return true
85
60
  end
61
+ return false
86
62
  end
87
63
 
88
64
  # Get all the pending associations that need to be set.
@@ -6,7 +6,7 @@ module Mongoid
6
6
  # This module defines projection helpers.
7
7
  #
8
8
  # Projection rules are rather non-trivial. See
9
- # https://docs.mongodb.com/manual/reference/method/db.collection.find/#find-projection
9
+ # https://www.mongodb.com/docs/manual/reference/method/db.collection.find/#find-projection
10
10
  # for server documentation.
11
11
  # 4.4 server (and presumably all older ones) requires that a projection
12
12
  # for content fields is either exclusionary or inclusionary, i.e. one
@@ -61,7 +61,7 @@ module Mongoid
61
61
  # attr_readonly :name, :genre
62
62
  # end
63
63
  #
64
- # @param [ Array<Symbol> ] names The names of the fields.
64
+ # @param [ Symbol... ] *names The names of the fields.
65
65
  def attr_readonly(*names)
66
66
  names.each do |name|
67
67
  readonly_attributes << database_field_name(name)
@@ -177,8 +177,14 @@ module Mongoid
177
177
  attribute_will_change!(field_name)
178
178
  end
179
179
  if localized
180
- attributes[field_name] ||= {}
181
- attributes[field_name].merge!(typed_value)
180
+ present = fields[field_name].try(:localize_present?)
181
+ loc_key, loc_val = typed_value.first
182
+ if present && loc_val.blank?
183
+ attributes[field_name]&.delete(loc_key)
184
+ else
185
+ attributes[field_name] ||= {}
186
+ attributes[field_name].merge!(typed_value)
187
+ end
182
188
  else
183
189
  attributes[field_name] = typed_value
184
190
  end
@@ -68,9 +68,13 @@ module Mongoid
68
68
  # @example Move the changes to previous.
69
69
  # person.move_changes
70
70
  def move_changes
71
+ @changes_before_last_save = @previous_changes
71
72
  @previous_changes = changes
73
+ @attributes_before_last_save = @previous_attributes
72
74
  @previous_attributes = attributes.dup
73
- reset_atomic_updates!
75
+ Atomic::UPDATES.each do |update|
76
+ send(update).clear
77
+ end
74
78
  changed_attributes.clear
75
79
  end
76
80
 
@@ -131,6 +135,72 @@ module Mongoid
131
135
  mods
132
136
  end
133
137
 
138
+ # Returns the original value of an attribute before the last save.
139
+ #
140
+ # This method is useful in after callbacks to get the original value of
141
+ # an attribute before the save that triggered the callbacks to run.
142
+ #
143
+ # @param [ Symbol | String ] attr The name of the attribute.
144
+ #
145
+ # @return [ Object ] Value of the attribute before the last save.
146
+ def attribute_before_last_save(attr)
147
+ attr = database_field_name(attr)
148
+ attributes_before_last_save[attr]
149
+ end
150
+
151
+ # Returns the change to an attribute during the last save.
152
+ #
153
+ # @param [ Symbol | String ] attr The name of the attribute.
154
+ #
155
+ # @return [ Array<Object> | nil ] If the attribute was changed, returns
156
+ # an array containing the original value and the saved value, otherwise nil.
157
+ def saved_change_to_attribute(attr)
158
+ attr = database_field_name(attr)
159
+ previous_changes[attr]
160
+ end
161
+
162
+ # Returns whether this attribute changed during the last save.
163
+ #
164
+ # This method is useful in after callbacks, to see the change
165
+ # in an attribute during the save that triggered the callbacks to run.
166
+ #
167
+ # @param [ String ] attr The name of the attribute.
168
+ # @param **kwargs The optional keyword arguments.
169
+ #
170
+ # @option **kwargs [ Object ] :from The object the attribute was changed from.
171
+ # @option **kwargs [ Object ] :to The object the attribute was changed to.
172
+ #
173
+ # @return [ true | false ] Whether the attribute has changed during the last save.
174
+ def saved_change_to_attribute?(attr, **kwargs)
175
+ changes = saved_change_to_attribute(attr)
176
+ return false unless changes.is_a?(Array)
177
+ if kwargs.key?(:from) && kwargs.key?(:to)
178
+ changes.first == kwargs[:from] && changes.last == kwargs[:to]
179
+ elsif kwargs.key?(:from)
180
+ changes.first == kwargs[:from]
181
+ elsif kwargs.key?(:to)
182
+ changes.last == kwargs[:to]
183
+ else
184
+ true
185
+ end
186
+ end
187
+
188
+ # Returns whether this attribute change the next time we save.
189
+ #
190
+ # This method is useful in validations and before callbacks to determine
191
+ # if the next call to save will change a particular attribute.
192
+ #
193
+ # @param [ String ] attr The name of the attribute.
194
+ # @param **kwargs The optional keyword arguments.
195
+ #
196
+ # @option **kwargs [ Object ] :from The object the attribute was changed from.
197
+ # @option **kwargs [ Object ] :to The object the attribute was changed to.
198
+ #
199
+ # @return [ true | false ] Whether the attribute change the next time we save.
200
+ def will_save_change_to_attribute?(attr, **kwargs)
201
+ attribute_changed?(attr, **kwargs)
202
+ end
203
+
134
204
  private
135
205
 
136
206
  # Get attributes of the document before the document was saved.
@@ -140,6 +210,14 @@ module Mongoid
140
210
  @previous_attributes ||= {}
141
211
  end
142
212
 
213
+ def changes_before_last_save
214
+ @changes_before_last_save ||= {}
215
+ end
216
+
217
+ def attributes_before_last_save
218
+ @attributes_before_last_save ||= {}
219
+ end
220
+
143
221
  # Get the old and new value for the provided attribute.
144
222
  #
145
223
  # @example Get the attribute change.
@@ -159,12 +237,24 @@ module Mongoid
159
237
  # model.attribute_changed?("name")
160
238
  #
161
239
  # @param [ String ] attr The name of the attribute.
240
+ # @param **kwargs The optional keyword arguments.
241
+ #
242
+ # @option **kwargs [ Object ] :from The object the attribute was changed from.
243
+ # @option **kwargs [ Object ] :to The object the attribute was changed to.
162
244
  #
163
245
  # @return [ true | false ] Whether the attribute has changed.
164
- def attribute_changed?(attr)
246
+ def attribute_changed?(attr, **kwargs)
165
247
  attr = database_field_name(attr)
166
248
  return false unless changed_attributes.key?(attr)
167
- changed_attributes[attr] != attributes[attr]
249
+ return false if changed_attributes[attr] == attributes[attr]
250
+ if kwargs.key?(:from)
251
+ return false if changed_attributes[attr] != kwargs[:from]
252
+ end
253
+ if kwargs.key?(:to)
254
+ return false if attributes[attr] != kwargs[:to]
255
+ end
256
+
257
+ true
168
258
  end
169
259
 
170
260
  # Get whether or not the field has a different value from the default.
@@ -300,8 +390,11 @@ module Mongoid
300
390
  # @param [ String ] meth The name of the accessor.
301
391
  def create_dirty_change_check(name, meth)
302
392
  generated_methods.module_eval do
303
- re_define_method("#{meth}_changed?") do
304
- attribute_changed?(name)
393
+ re_define_method("#{meth}_changed?") do |**kwargs|
394
+ attribute_changed?(name, **kwargs)
395
+ end
396
+ re_define_method("will_save_change_to_#{meth}?") do |**kwargs|
397
+ will_save_change_to_attribute?(name, **kwargs)
305
398
  end
306
399
  end
307
400
  end
@@ -336,6 +429,15 @@ module Mongoid
336
429
  re_define_method("#{meth}_previously_was") do
337
430
  attribute_previously_was(name)
338
431
  end
432
+ re_define_method("#{meth}_before_last_save") do
433
+ attribute_before_last_save(name)
434
+ end
435
+ re_define_method("saved_change_to_#{meth}") do
436
+ saved_change_to_attribute(name)
437
+ end
438
+ re_define_method("saved_change_to_#{meth}?") do |**kwargs|
439
+ saved_change_to_attribute?(name, **kwargs)
440
+ end
339
441
  end
340
442
  end
341
443
 
@@ -95,10 +95,6 @@ module Mongoid
95
95
  [MONGOID_WRAPPING_LIBRARY] + options[:wrapping_libraries]
96
96
  else
97
97
  [MONGOID_WRAPPING_LIBRARY]
98
- end.tap do |wrap|
99
- if defined?(::Rails) && ::Rails.respond_to?(:version)
100
- wrap << { name: 'Rails', version: ::Rails.version }
101
- end
102
98
  end
103
99
  options[:wrapping_libraries] = wrap_lib
104
100
  end