mongoid 7.2.0.rc1 → 7.3.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 (214) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/Rakefile +45 -10
  5. data/lib/config/locales/en.yml +2 -2
  6. data/lib/mongoid/association/accessors.rb +1 -1
  7. data/lib/mongoid/association/constrainable.rb +1 -1
  8. data/lib/mongoid/association/depending.rb +4 -4
  9. data/lib/mongoid/association/embedded/batchable.rb +1 -1
  10. data/lib/mongoid/association/embedded/embedded_in.rb +1 -1
  11. data/lib/mongoid/association/embedded/embeds_many/proxy.rb +10 -3
  12. data/lib/mongoid/association/nested/many.rb +1 -1
  13. data/lib/mongoid/association/nested/one.rb +4 -2
  14. data/lib/mongoid/association/proxy.rb +6 -1
  15. data/lib/mongoid/association/referenced/auto_save.rb +2 -2
  16. data/lib/mongoid/association/referenced/has_many/enumerable.rb +493 -495
  17. data/lib/mongoid/association/referenced/has_many/proxy.rb +2 -2
  18. data/lib/mongoid/association/referenced/has_one/buildable.rb +8 -0
  19. data/lib/mongoid/association/referenced/has_one/nested_builder.rb +2 -2
  20. data/lib/mongoid/association/referenced/has_one/proxy.rb +6 -1
  21. data/lib/mongoid/attributes.rb +32 -14
  22. data/lib/mongoid/attributes/projector.rb +120 -0
  23. data/lib/mongoid/cacheable.rb +2 -2
  24. data/lib/mongoid/clients.rb +1 -1
  25. data/lib/mongoid/clients/factory.rb +22 -8
  26. data/lib/mongoid/config.rb +19 -2
  27. data/lib/mongoid/contextual/aggregable/mongo.rb +10 -8
  28. data/lib/mongoid/copyable.rb +6 -2
  29. data/lib/mongoid/criteria.rb +4 -5
  30. data/lib/mongoid/criteria/findable.rb +1 -1
  31. data/lib/mongoid/criteria/queryable/expandable.rb +0 -24
  32. data/lib/mongoid/criteria/queryable/extensions.rb +0 -4
  33. data/lib/mongoid/criteria/queryable/extensions/boolean.rb +1 -1
  34. data/lib/mongoid/criteria/queryable/mergeable.rb +46 -20
  35. data/lib/mongoid/criteria/queryable/selectable.rb +8 -8
  36. data/lib/mongoid/criteria/queryable/selector.rb +0 -4
  37. data/lib/mongoid/document.rb +4 -17
  38. data/lib/mongoid/errors/delete_restriction.rb +8 -9
  39. data/lib/mongoid/evolvable.rb +1 -1
  40. data/lib/mongoid/extensions.rb +1 -0
  41. data/lib/mongoid/extensions/boolean.rb +1 -2
  42. data/lib/mongoid/extensions/false_class.rb +1 -1
  43. data/lib/mongoid/extensions/hash.rb +2 -2
  44. data/lib/mongoid/extensions/true_class.rb +1 -1
  45. data/lib/mongoid/fields.rb +46 -5
  46. data/lib/mongoid/inspectable.rb +1 -1
  47. data/lib/mongoid/interceptable.rb +3 -1
  48. data/lib/mongoid/matcher.rb +26 -43
  49. data/lib/mongoid/matcher/bits.rb +41 -0
  50. data/lib/mongoid/matcher/bits_all_clear.rb +20 -0
  51. data/lib/mongoid/matcher/bits_all_set.rb +20 -0
  52. data/lib/mongoid/matcher/bits_any_clear.rb +20 -0
  53. data/lib/mongoid/matcher/bits_any_set.rb +20 -0
  54. data/lib/mongoid/matcher/elem_match.rb +2 -1
  55. data/lib/mongoid/matcher/expression.rb +9 -14
  56. data/lib/mongoid/matcher/field_expression.rb +4 -5
  57. data/lib/mongoid/matcher/field_operator.rb +13 -11
  58. data/lib/mongoid/matcher/mod.rb +17 -0
  59. data/lib/mongoid/matcher/type.rb +99 -0
  60. data/lib/mongoid/persistable/deletable.rb +1 -2
  61. data/lib/mongoid/persistable/destroyable.rb +8 -2
  62. data/lib/mongoid/persistable/updatable.rb +27 -2
  63. data/lib/mongoid/query_cache.rb +35 -29
  64. data/lib/mongoid/reloadable.rb +5 -0
  65. data/lib/mongoid/selectable.rb +5 -7
  66. data/lib/mongoid/shardable.rb +21 -5
  67. data/lib/mongoid/stringified_symbol.rb +53 -0
  68. data/lib/mongoid/touchable.rb +23 -4
  69. data/lib/mongoid/version.rb +1 -1
  70. data/lib/rails/generators/mongoid/config/config_generator.rb +8 -1
  71. data/spec/README.md +19 -4
  72. data/spec/integration/app_spec.rb +175 -88
  73. data/spec/integration/associations/embeds_many_spec.rb +68 -0
  74. data/spec/integration/associations/embeds_one_spec.rb +24 -0
  75. data/spec/integration/associations/has_many_spec.rb +60 -0
  76. data/spec/integration/associations/has_one_spec.rb +108 -0
  77. data/spec/integration/callbacks_models.rb +49 -0
  78. data/spec/integration/callbacks_spec.rb +216 -0
  79. data/spec/integration/criteria/date_field_spec.rb +1 -1
  80. data/spec/integration/document_spec.rb +30 -0
  81. data/spec/integration/matcher_operator_data/bits_all_clear.yml +159 -0
  82. data/spec/integration/matcher_operator_data/bits_all_set.yml +159 -0
  83. data/spec/integration/matcher_operator_data/bits_any_clear.yml +159 -0
  84. data/spec/integration/matcher_operator_data/bits_any_set.yml +159 -0
  85. data/spec/integration/matcher_operator_data/comment.yml +22 -0
  86. data/spec/integration/matcher_operator_data/elem_match.yml +46 -0
  87. data/spec/integration/matcher_operator_data/gt_types.yml +63 -0
  88. data/spec/integration/matcher_operator_data/gte_types.yml +15 -0
  89. data/spec/integration/matcher_operator_data/implicit_traversal.yml +96 -0
  90. data/spec/integration/matcher_operator_data/in.yml +16 -0
  91. data/spec/integration/matcher_operator_data/lt_types.yml +15 -0
  92. data/spec/integration/matcher_operator_data/lte_types.yml +15 -0
  93. data/spec/integration/matcher_operator_data/mod.yml +55 -0
  94. data/spec/integration/matcher_operator_data/ne_types.yml +15 -0
  95. data/spec/integration/matcher_operator_data/type.yml +70 -0
  96. data/spec/integration/matcher_operator_data/type_array.yml +16 -0
  97. data/spec/integration/matcher_operator_data/type_binary.yml +18 -0
  98. data/spec/integration/matcher_operator_data/type_boolean.yml +39 -0
  99. data/spec/integration/matcher_operator_data/type_code.yml +26 -0
  100. data/spec/integration/matcher_operator_data/type_code_with_scope.yml +26 -0
  101. data/spec/integration/matcher_operator_data/type_date.yml +39 -0
  102. data/spec/integration/matcher_operator_data/type_db_pointer.yml +19 -0
  103. data/spec/integration/matcher_operator_data/type_decimal.yml +40 -0
  104. data/spec/integration/matcher_operator_data/type_double.yml +15 -0
  105. data/spec/integration/matcher_operator_data/type_int32.yml +33 -0
  106. data/spec/integration/matcher_operator_data/type_int64.yml +33 -0
  107. data/spec/integration/matcher_operator_data/type_max_key.yml +17 -0
  108. data/spec/integration/matcher_operator_data/type_min_key.yml +17 -0
  109. data/spec/integration/matcher_operator_data/type_null.yml +23 -0
  110. data/spec/integration/matcher_operator_data/type_object.yml +23 -0
  111. data/spec/integration/matcher_operator_data/type_object_id.yml +25 -0
  112. data/spec/integration/matcher_operator_data/type_regex.yml +44 -0
  113. data/spec/integration/matcher_operator_data/type_string.yml +15 -0
  114. data/spec/integration/matcher_operator_data/type_symbol.yml +32 -0
  115. data/spec/integration/matcher_operator_data/type_timestamp.yml +25 -0
  116. data/spec/integration/matcher_operator_data/type_undefined.yml +17 -0
  117. data/spec/integration/stringified_symbol_field_spec.rb +190 -0
  118. data/spec/lite_spec_helper.rb +9 -7
  119. data/spec/mongoid/association/depending_spec.rb +391 -352
  120. data/spec/mongoid/association/embedded/embedded_in/proxy_spec.rb +50 -0
  121. data/spec/mongoid/association/nested/one_spec.rb +18 -14
  122. data/spec/mongoid/association/referenced/belongs_to/proxy_spec.rb +25 -8
  123. data/spec/mongoid/association/referenced/has_and_belongs_to_many/binding_spec.rb +1 -1
  124. data/spec/mongoid/association/referenced/has_many/binding_spec.rb +1 -1
  125. data/spec/mongoid/association/referenced/has_many/enumerable_spec.rb +1 -1
  126. data/spec/mongoid/association/referenced/has_many_models.rb +12 -0
  127. data/spec/mongoid/association/referenced/has_one_models.rb +20 -0
  128. data/spec/mongoid/association/referenced/has_one_spec.rb +1 -1
  129. data/spec/mongoid/atomic/paths_spec.rb +105 -12
  130. data/spec/mongoid/attributes/projector_data/embedded.yml +105 -0
  131. data/spec/mongoid/attributes/projector_data/fields.yml +93 -0
  132. data/spec/mongoid/attributes/projector_spec.rb +41 -0
  133. data/spec/mongoid/attributes_spec.rb +333 -0
  134. data/spec/mongoid/clients/factory_spec.rb +48 -0
  135. data/spec/mongoid/config_spec.rb +32 -0
  136. data/spec/mongoid/contextual/atomic_spec.rb +17 -4
  137. data/spec/mongoid/contextual/mongo_spec.rb +2 -2
  138. data/spec/mongoid/copyable_spec.rb +44 -17
  139. data/spec/mongoid/copyable_spec_models.rb +14 -0
  140. data/spec/mongoid/criteria/modifiable_spec.rb +1 -1
  141. data/spec/mongoid/criteria/queryable/expandable_spec.rb +0 -73
  142. data/spec/mongoid/criteria/queryable/extensions/boolean_spec.rb +1 -1
  143. data/spec/mongoid/criteria/queryable/mergeable_spec.rb +105 -7
  144. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +265 -24
  145. data/spec/mongoid/criteria/queryable/selectable_shared_examples.rb +39 -0
  146. data/spec/mongoid/criteria/queryable/selectable_spec.rb +1 -565
  147. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +590 -0
  148. data/spec/mongoid/criteria_projection_spec.rb +411 -0
  149. data/spec/mongoid/criteria_spec.rb +0 -275
  150. data/spec/mongoid/document_fields_spec.rb +26 -0
  151. data/spec/mongoid/document_spec.rb +13 -13
  152. data/spec/mongoid/equality_spec.rb +0 -1
  153. data/spec/mongoid/errors/delete_restriction_spec.rb +1 -1
  154. data/spec/mongoid/extensions/false_class_spec.rb +1 -1
  155. data/spec/mongoid/extensions/string_spec.rb +5 -5
  156. data/spec/mongoid/extensions/stringified_symbol_spec.rb +85 -0
  157. data/spec/mongoid/extensions/true_class_spec.rb +1 -1
  158. data/spec/mongoid/fields/localized_spec.rb +4 -4
  159. data/spec/mongoid/fields_spec.rb +4 -4
  160. data/spec/mongoid/inspectable_spec.rb +12 -4
  161. data/spec/mongoid/matcher/extract_attribute_data/numeric_keys.yml +104 -0
  162. data/spec/mongoid/matcher/extract_attribute_data/traversal.yml +68 -88
  163. data/spec/mongoid/matcher/extract_attribute_spec.rb +3 -13
  164. data/spec/mongoid/persistable/deletable_spec.rb +175 -1
  165. data/spec/mongoid/persistable/destroyable_spec.rb +191 -3
  166. data/spec/mongoid/persistable/savable_spec.rb +3 -5
  167. data/spec/mongoid/persistable/settable_spec.rb +30 -0
  168. data/spec/mongoid/persistable/upsertable_spec.rb +1 -1
  169. data/spec/mongoid/query_cache_middleware_spec.rb +8 -0
  170. data/spec/mongoid/reloadable_spec.rb +18 -1
  171. data/spec/mongoid/shardable_spec.rb +44 -0
  172. data/spec/mongoid/touchable_spec.rb +104 -16
  173. data/spec/mongoid/touchable_spec_models.rb +52 -0
  174. data/spec/mongoid/validatable_spec.rb +1 -1
  175. data/spec/shared/LICENSE +20 -0
  176. data/spec/shared/bin/get-mongodb-download-url +17 -0
  177. data/spec/shared/lib/mrss/child_process_helper.rb +80 -0
  178. data/spec/shared/lib/mrss/cluster_config.rb +221 -0
  179. data/spec/shared/lib/mrss/constraints.rb +354 -0
  180. data/spec/shared/lib/mrss/docker_runner.rb +265 -0
  181. data/spec/shared/lib/mrss/lite_constraints.rb +191 -0
  182. data/spec/shared/lib/mrss/server_version_registry.rb +115 -0
  183. data/spec/shared/lib/mrss/spec_organizer.rb +162 -0
  184. data/spec/shared/lib/mrss/utils.rb +15 -0
  185. data/spec/shared/share/Dockerfile.erb +231 -0
  186. data/spec/shared/shlib/distro.sh +73 -0
  187. data/spec/shared/shlib/server.sh +290 -0
  188. data/spec/shared/shlib/set_env.sh +128 -0
  189. data/spec/spec_helper.rb +7 -1
  190. data/spec/support/client_registry.rb +9 -0
  191. data/spec/support/constraints.rb +0 -226
  192. data/spec/support/models/bolt.rb +8 -0
  193. data/spec/support/models/customer.rb +11 -0
  194. data/spec/support/models/customer_address.rb +12 -0
  195. data/spec/support/models/dictionary.rb +6 -0
  196. data/spec/support/models/hole.rb +13 -0
  197. data/spec/support/models/mop.rb +9 -0
  198. data/spec/support/models/nut.rb +8 -0
  199. data/spec/support/models/order.rb +11 -0
  200. data/spec/support/models/person.rb +8 -0
  201. data/spec/support/models/sealer.rb +8 -0
  202. data/spec/support/models/series.rb +1 -0
  203. data/spec/support/models/shirt.rb +12 -0
  204. data/spec/support/models/spacer.rb +8 -0
  205. data/spec/support/models/threadlocker.rb +8 -0
  206. data/spec/support/models/washer.rb +8 -0
  207. data/spec/support/models/wiki_page.rb +1 -0
  208. data/spec/support/spec_config.rb +8 -0
  209. metadata +655 -507
  210. metadata.gz.sig +0 -0
  211. data/spec/support/child_process_helper.rb +0 -79
  212. data/spec/support/cluster_config.rb +0 -158
  213. data/spec/support/lite_constraints.rb +0 -22
  214. data/spec/support/spec_organizer.rb +0 -130
@@ -47,7 +47,7 @@ module Mongoid
47
47
  #
48
48
  # @since 4.0.0
49
49
  def atomic_deletes
50
- { atomic_delete_modifier => { atomic_path => _index ? { "_id" => id } : true }}
50
+ { atomic_delete_modifier => { atomic_path => _index ? { "_id" => _id } : true }}
51
51
  end
52
52
 
53
53
  # Delete the embedded document.
@@ -117,7 +117,6 @@ module Mongoid
117
117
  #
118
118
  # @since 4.0.0
119
119
  def prepare_delete
120
- return false unless catch(:abort) { apply_delete_dependencies! }
121
120
  yield(self)
122
121
  freeze
123
122
  self.destroyed = true
@@ -23,13 +23,19 @@ module Mongoid
23
23
  def destroy(options = nil)
24
24
  raise Errors::ReadonlyDocument.new(self.class) if readonly?
25
25
  self.flagged_for_destroy = true
26
- result = run_callbacks(:destroy) { delete(options || {}) }
26
+ result = run_callbacks(:destroy) do
27
+ if catch(:abort) { apply_destroy_dependencies! }
28
+ delete(options || {})
29
+ else
30
+ false
31
+ end
32
+ end
27
33
  self.flagged_for_destroy = false
28
34
  result
29
35
  end
30
36
 
31
37
  def destroy!(options = {})
32
- destroy || raise(Errors::DocumentNotDestroyed.new(id, self.class))
38
+ destroy || raise(Errors::DocumentNotDestroyed.new(_id, self.class))
33
39
  end
34
40
 
35
41
  module ClassMethods
@@ -137,8 +137,33 @@ module Mongoid
137
137
  coll = collection(_root)
138
138
  selector = atomic_selector
139
139
  coll.find(selector).update_one(positionally(selector, updates), session: _session)
140
- conflicts.each_pair do |key, value|
141
- coll.find(selector).update_one(positionally(selector, { key => value }), session: _session)
140
+
141
+ # The following code applies updates which would cause
142
+ # path conflicts in MongoDB, for example when changing attributes
143
+ # of foo.0.bars while adding another foo. Each conflicting update
144
+ # is applied using its own write.
145
+ #
146
+ # TODO: MONGOID-5026: reduce the number of writes performed by
147
+ # more intelligently combining the writes such that there are
148
+ # fewer conflicts.
149
+ conflicts.each_pair do |modifier, changes|
150
+
151
+ # Group the changes according to their root key which is
152
+ # the top-level association name.
153
+ # This handles at least the cases described in MONGOID-4982.
154
+ conflicting_change_groups = changes.group_by do |key, _|
155
+ key.split(".", 2).first
156
+ end.values
157
+
158
+ # Apply changes in batches. Pop one change from each
159
+ # field-conflict group round-robin until all changes
160
+ # have been applied.
161
+ while batched_changes = conflicting_change_groups.map(&:pop).compact.to_h.presence
162
+ coll.find(selector).update_one(
163
+ positionally(selector, modifier => batched_changes),
164
+ session: _session,
165
+ )
166
+ end
142
167
  end
143
168
  end
144
169
  end
@@ -117,38 +117,44 @@ module Mongoid
117
117
  end
118
118
  end
119
119
 
120
- # The middleware to be added to a rack application in order to activate the
121
- # query cache.
122
- #
123
- # @since 4.0.0
124
- class Middleware
125
-
126
- # Instantiate the middleware.
127
- #
128
- # @example Create the new middleware.
129
- # Middleware.new(app)
130
- #
131
- # @param [ Object ] app The rack applciation stack.
120
+ if defined?(Mongo::QueryCache::Middleware)
121
+ Middleware = Mongo::QueryCache::Middleware
122
+ else
123
+ # The middleware to be added to a rack application in order to activate the
124
+ # query cache.
132
125
  #
133
126
  # @since 4.0.0
134
- def initialize(app)
135
- @app = app
136
- end
127
+ class Middleware
137
128
 
138
- # Execute the request, wrapping in a query cache.
139
- #
140
- # @example Execute the request.
141
- # middleware.call(env)
142
- #
143
- # @param [ Object ] env The environment.
144
- #
145
- # @return [ Object ] The result of the call.
146
- #
147
- # @since 4.0.0
148
- def call(env)
149
- QueryCache.cache { @app.call(env) }
150
- ensure
151
- QueryCache.clear_cache
129
+ # Instantiate the middleware.
130
+ #
131
+ # @example Create the new middleware.
132
+ # Middleware.new(app)
133
+ #
134
+ # @param [ Object ] app The rack application stack.
135
+ #
136
+ # @since 4.0.0
137
+ def initialize(app)
138
+ @app = app
139
+ end
140
+
141
+ # Execute the request, wrapping in a query cache.
142
+ #
143
+ # @example Execute the request.
144
+ # middleware.call(env)
145
+ #
146
+ # @param [ Object ] env The environment.
147
+ #
148
+ # @return [ Object ] The result of the call.
149
+ #
150
+ # @since 4.0.0
151
+ def call(env)
152
+ QueryCache.cache do
153
+ @app.call(env)
154
+ end
155
+ ensure
156
+ QueryCache.clear_cache
157
+ end
152
158
  end
153
159
  end
154
160
 
@@ -21,6 +21,11 @@ module Mongoid
21
21
  #
22
22
  # @since 1.0.0
23
23
  def reload
24
+ if @atomic_selector
25
+ # Clear atomic_selector cache for sharded clusters. MONGOID-5076
26
+ remove_instance_variable('@atomic_selector')
27
+ end
28
+
24
29
  reloaded = _reload
25
30
  if Mongoid.raise_not_found_error && reloaded.empty?
26
31
  raise Errors::DocumentNotFound.new(self.class, _id, _id)
@@ -21,7 +21,7 @@ module Mongoid
21
21
  # @since 1.0.0
22
22
  def atomic_selector
23
23
  @atomic_selector ||=
24
- (embedded? ? embedded_atomic_selector : root_atomic_selector)
24
+ (embedded? ? embedded_atomic_selector : root_atomic_selector_in_db)
25
25
  end
26
26
 
27
27
  private
@@ -44,18 +44,16 @@ module Mongoid
44
44
  end
45
45
  end
46
46
 
47
- # Get the atomic selector for a root document.
47
+ # Get the atomic selector that would match the existing version of the
48
+ # root document.
48
49
  #
49
50
  # @api private
50
51
  #
51
- # @example Get the root atomic selector.
52
- # document.root_atomic_selector
53
- #
54
52
  # @return [ Hash ] The root document selector.
55
53
  #
56
54
  # @since 4.0.0
57
- def root_atomic_selector
58
- { "_id" => _id }.merge!(shard_key_selector)
55
+ def root_atomic_selector_in_db
56
+ { "_id" => _id }.merge!(shard_key_selector_in_db)
59
57
  end
60
58
  end
61
59
  end
@@ -52,15 +52,31 @@ module Mongoid
52
52
  self.class.shard_key_fields
53
53
  end
54
54
 
55
- # Get the document selector with the defined shard keys.
56
- #
57
- # @example Get the selector for the shard keys.
58
- # person.shard_key_selector
55
+ # Returns the selector that would match the current version of this
56
+ # document.
59
57
  #
60
58
  # @return [ Hash ] The shard key selector.
61
59
  #
62
- # @since 2.0.0
60
+ # @api private
63
61
  def shard_key_selector
62
+ selector = {}
63
+ shard_key_fields.each do |field|
64
+ selector[field.to_s] = send(field)
65
+ end
66
+ selector
67
+ end
68
+
69
+ # Returns the selector that would match the existing version of this
70
+ # document in the database.
71
+ #
72
+ # If the document is not persisted, this method uses the current values
73
+ # of the shard key fields. If the document is persisted, this method
74
+ # uses the values retrieved from the database.
75
+ #
76
+ # @return [ Hash ] The shard key selector.
77
+ #
78
+ # @api private
79
+ def shard_key_selector_in_db
64
80
  selector = {}
65
81
  shard_key_fields.each do |field|
66
82
  selector[field.to_s] = new_record? ? send(field) : attribute_was(field)
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ # encoding: utf-8
3
+
4
+ # A class which sends values to the database as Strings but returns them to the user as Symbols.
5
+ module Mongoid
6
+ class StringifiedSymbol
7
+
8
+ class << self
9
+
10
+ # Convert the object from its mongo friendly ruby type to this type.
11
+ #
12
+ # @example Demongoize the object.
13
+ # Symbol.demongoize(object)
14
+ #
15
+ # @param [ Object ] object The object to demongoize.
16
+ #
17
+ # @return [ Symbol ] The object.
18
+ #
19
+ # @api private
20
+ def demongoize(object)
21
+ if object.nil?
22
+ object
23
+ else
24
+ object.to_s.to_sym
25
+ end
26
+ end
27
+
28
+ # Turn the object from the ruby type we deal with to a Mongo friendly
29
+ # type.
30
+ #
31
+ # @example Mongoize the object.
32
+ # Symbol.mongoize("123.11")
33
+ #
34
+ # @param [ Object ] object The object to mongoize.
35
+ #
36
+ # @return [ Symbol ] The object mongoized.
37
+ #
38
+ # @api private
39
+ def mongoize(object)
40
+ if object.nil?
41
+ object
42
+ else
43
+ object.to_s
44
+ end
45
+ end
46
+
47
+ # @api private
48
+ def evolve(object)
49
+ mongoize(object)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -30,11 +30,30 @@ module Mongoid
30
30
  write_attribute(:updated_at, current) if respond_to?("updated_at=")
31
31
  write_attribute(field, current) if field
32
32
 
33
- touches = touch_atomic_updates(field)
34
- unless touches["$set"].blank?
35
- selector = atomic_selector
36
- _root.collection.find(selector).update_one(positionally(selector, touches), session: _session)
33
+ # If the document being touched is embedded, touch its parents
34
+ # all the way through the composition hierarchy to the root object,
35
+ # because when an embedded document is changed the write is actually
36
+ # performed by the composition root. See MONGOID-3468.
37
+ if _parent
38
+ # This will persist updated_at on this document as well as parents.
39
+ # TODO support passing the field name to the parent's touch method;
40
+ # I believe it should be read out of
41
+ # _association.inverse_association.options but inverse_association
42
+ # seems to not always/ever be set here. See MONGOID-5014.
43
+ _parent.touch
44
+ else
45
+ # If the current document is not embedded, it is composition root
46
+ # and we need to persist the write here.
47
+ touches = touch_atomic_updates(field)
48
+ unless touches["$set"].blank?
49
+ selector = atomic_selector
50
+ _root.collection.find(selector).update_one(positionally(selector, touches), session: _session)
51
+ end
37
52
  end
53
+
54
+ # Callbacks are invoked on the composition root first and on the
55
+ # leaf-most embedded document last.
56
+ # TODO add tests, see MONGOID-5015.
38
57
  run_callbacks(:touch)
39
58
  true
40
59
  end
@@ -2,5 +2,5 @@
2
2
  # encoding: utf-8
3
3
 
4
4
  module Mongoid
5
- VERSION = "7.2.0.rc1"
5
+ VERSION = "7.3.0"
6
6
  end
@@ -15,7 +15,14 @@ module Mongoid
15
15
  end
16
16
 
17
17
  def app_name
18
- Rails::Application.subclasses.first.parent.to_s.underscore
18
+ app_cls = Rails.application.class
19
+ parent = begin
20
+ # Rails 6.1+
21
+ app_cls.module_parent_name
22
+ rescue NoMethodError
23
+ app_cls.parent.to_s
24
+ end
25
+ parent.underscore
19
26
  end
20
27
 
21
28
  def create_config_file
data/spec/README.md CHANGED
@@ -1,8 +1,17 @@
1
1
  # Running Mongoid Tests
2
2
 
3
- ## Overview
4
- ### Quick Start
5
- Spin up a MongoDB deployment against which to run the Mongoid specs. Mongoid specs support a variety of MongoDB topologies, but the simplest is a single MongoDB instance:
3
+ ## Quick Start
4
+
5
+ The test suite requires shared tooling that is stored in a separate repository
6
+ and is referenced as a submodule. After checking out the desired driver
7
+ branch, check out the matching submodules:
8
+
9
+ git submodule init
10
+ git submodule update
11
+
12
+ Spin up a MongoDB deployment against which to run the Mongoid specs.
13
+ Mongoid specs support a variety of MongoDB topologies, but the simplest is
14
+ a single MongoDB instance:
6
15
 
7
16
  # Launch mongod in one terminal
8
17
  mkdir /tmp/mdb
@@ -14,5 +23,11 @@ Run the test suite in a separate terminal:
14
23
 
15
24
 
16
25
  ## Caveats
26
+
17
27
  ### "Too many open files" error
18
- On MacOS, you may encounter a "Too many open files" error on the MongoDB server when running the tests. If this happens, stop the server, run the command `ulimit -n 10000` in the same terminal session as the server, and restart the server. This will increase the number of files that can be opened. Then, re-run the tests.
28
+
29
+ On MacOS, you may encounter a "Too many open files" error on the MongoDB server
30
+ when running the tests. If this happens, stop the server, run the command
31
+ `ulimit -n 10000` in the same terminal session as the server, and restart
32
+ the server. This will increase the number of files that can be opened.
33
+ Then, re-run the tests.
@@ -13,109 +13,131 @@ describe 'Mongoid application tests' do
13
13
  end
14
14
 
15
15
  require 'fileutils'
16
- require 'support/child_process_helper'
16
+ require 'mrss/child_process_helper'
17
17
  require 'open-uri'
18
18
 
19
19
  FileUtils.mkdir_p(TMP_BASE)
20
20
  end
21
21
 
22
- context 'demo application - sinatra' do
23
- it 'runs' do
24
- clone_application(
25
- 'https://github.com/mongoid/mongoid-demo',
26
- subdir: 'sinatra-minimal',
27
- ) do
22
+ context 'demo application' do
23
+ context 'sinatra' do
24
+ it 'runs' do
25
+ clone_application(
26
+ 'https://github.com/mongoid/mongoid-demo',
27
+ subdir: 'sinatra-minimal',
28
+ ) do
28
29
 
29
- process = ChildProcess.build(*%w(bundle exec ruby app.rb))
30
- process.environment.update(clean_env)
31
- process.io.inherit!
32
- process.start
33
-
34
- begin
35
30
  # JRuby needs a long timeout
36
- wait_for_port(4567, 20)
37
- sleep 1
38
-
39
- uri = URI.parse('http://localhost:4567/posts')
40
- resp = JSON.parse(uri.open.read)
41
- ensure
42
- Process.kill('TERM', process.pid)
43
- status = process.wait
44
- end
31
+ start_app(%w(bundle exec ruby app.rb), 4567, 40) do |port|
32
+ uri = URI.parse('http://localhost:4567/posts')
33
+ resp = JSON.parse(uri.open.read)
45
34
 
46
- resp.should == []
35
+ resp.should == []
47
36
 
48
- status.should == 0
37
+ end
38
+ end
49
39
  end
50
40
  end
51
- end
52
41
 
53
- context 'demo application - rails-api' do
54
- ['~> 6.0.0'].each do |rails_version|
55
- context "with rails #{rails_version}" do
56
- it 'runs' do
57
- clone_application(
58
- 'https://github.com/mongoid/mongoid-demo',
59
- subdir: 'rails-api',
60
- rails_version: rails_version,
61
- ) do
62
-
63
- process = ChildProcess.build(*%w(bundle exec rails s))
64
- process.environment.update(clean_env)
65
- process.io.inherit!
66
- process.start
67
-
68
- begin
69
- # JRuby needs a long timeout
70
- wait_for_port(3000, 30)
71
- sleep 1
72
-
73
- uri = URI.parse('http://localhost:3000/posts')
74
- resp = JSON.parse(uri.open.read)
75
- ensure
76
- Process.kill('TERM', process.pid)
77
- status = process.wait
78
- end
42
+ context 'rails-api' do
43
+ it 'runs' do
44
+ clone_application(
45
+ 'https://github.com/mongoid/mongoid-demo',
46
+ subdir: 'rails-api',
47
+ ) do
79
48
 
80
- resp.should == []
49
+ # JRuby needs a long timeout
50
+ start_app(%w(bundle exec rails s), 3000, 50) do |port|
51
+ uri = URI.parse('http://localhost:3000/posts')
52
+ resp = JSON.parse(uri.open.read)
81
53
 
82
- # 143 = 128 + 15
83
- [0, 15, 143].should include(status)
54
+ resp.should == []
84
55
  end
85
56
  end
86
57
  end
87
58
  end
88
59
  end
89
60
 
61
+ def start_app(cmd, port, timeout)
62
+ process = ChildProcess.build(*cmd)
63
+ process.environment.update(clean_env)
64
+ process.io.inherit!
65
+ process.start
66
+
67
+ begin
68
+ wait_for_port(port, timeout, process)
69
+ sleep 1
70
+
71
+ rv = yield port
72
+ ensure
73
+ # The process may have already died (due to an error exit) -
74
+ # in this case killing it will raise an exception.
75
+ Process.kill('TERM', process.pid) rescue nil
76
+ status = process.wait
77
+ end
78
+
79
+ # Exit should be either success or SIGTERM
80
+ [0, 15, 128 + 15].should include(status)
81
+
82
+ rv
83
+ end
84
+
90
85
  context 'new application - rails' do
91
- ['~> 5.1.0', '~> 5.2.0', '~> 6.0.0'].each do |rails_version|
92
- context "with rails #{rails_version}" do
93
- it 'creates' do
94
- ChildProcessHelper.check_call(%w(gem uni rails -a))
95
- ChildProcessHelper.check_call(%w(gem install rails --no-document -v) + [rails_version])
96
-
97
- Dir.chdir(TMP_BASE) do
98
- FileUtils.rm_rf('mongoid-test')
99
- ChildProcessHelper.check_call(%w(rails new mongoid-test --skip-spring --skip-active-record), env: clean_env)
100
-
101
- Dir.chdir('mongoid-test') do
102
- adjust_app_gemfile
103
- ChildProcessHelper.check_call(%w(bundle install), env: clean_env)
104
-
105
- ChildProcessHelper.check_call(%w(rails g model post), env: clean_env)
106
- ChildProcessHelper.check_call(%w(rails g model comment post:belongs_to), env: clean_env)
107
-
108
- # https://jira.mongodb.org/browse/MONGOID-4885
109
- comment_text = File.read('app/models/comment.rb')
110
- comment_text.should =~ /belongs_to :post/
111
- comment_text.should_not =~ /embedded_in :post/
112
- end
113
- end
86
+ it 'creates' do
87
+ install_rails
88
+
89
+ Dir.chdir(TMP_BASE) do
90
+ FileUtils.rm_rf('mongoid-test')
91
+ Mrss::ChildProcessHelper.check_call(%w(rails new mongoid-test --skip-spring --skip-active-record), env: clean_env)
92
+
93
+ Dir.chdir('mongoid-test') do
94
+ adjust_app_gemfile
95
+ Mrss::ChildProcessHelper.check_call(%w(bundle install), env: clean_env)
96
+
97
+ Mrss::ChildProcessHelper.check_call(%w(rails g model post), env: clean_env)
98
+ Mrss::ChildProcessHelper.check_call(%w(rails g model comment post:belongs_to), env: clean_env)
99
+
100
+ # https://jira.mongodb.org/browse/MONGOID-4885
101
+ comment_text = File.read('app/models/comment.rb')
102
+ comment_text.should =~ /belongs_to :post/
103
+ comment_text.should_not =~ /embedded_in :post/
104
+ end
105
+ end
106
+ end
107
+
108
+ it 'generates Mongoid config' do
109
+ install_rails
110
+
111
+ Dir.chdir(TMP_BASE) do
112
+ FileUtils.rm_rf('mongoid-test-config')
113
+ Mrss::ChildProcessHelper.check_call(%w(rails new mongoid-test-config --skip-spring --skip-active-record), env: clean_env)
114
+
115
+ Dir.chdir('mongoid-test-config') do
116
+ adjust_app_gemfile
117
+ Mrss::ChildProcessHelper.check_call(%w(bundle install), env: clean_env)
118
+
119
+ mongoid_config_file = File.join(TMP_BASE,'mongoid-test-config/config/mongoid.yml')
120
+
121
+ File.exist?(mongoid_config_file).should be false
122
+ Mrss::ChildProcessHelper.check_call(%w(rails g mongoid:config), env: clean_env)
123
+ File.exist?(mongoid_config_file).should be true
124
+
125
+ config_text = File.read(mongoid_config_file)
126
+ config_text.should =~ /mongoid_test_config_development/
127
+ config_text.should =~ /mongoid_test_config_test/
114
128
  end
115
129
  end
116
130
  end
117
131
  end
118
132
 
133
+ def install_rails
134
+ Mrss::ChildProcessHelper.check_call(%w(gem uni rails -a))
135
+ if (rails_version = SpecConfig.instance.rails_version) == 'master'
136
+ else
137
+ Mrss::ChildProcessHelper.check_call(%w(gem install rails --no-document -v) + [rails_version])
138
+ end
139
+ end
140
+
119
141
  context 'local test applications' do
120
142
  let(:client) { Mongoid.default_client }
121
143
 
@@ -136,7 +158,7 @@ describe 'Mongoid application tests' do
136
158
  before do
137
159
  Dir.chdir(APP_PATH) do
138
160
  remove_bundler_req
139
- ChildProcessHelper.check_call(%w(bundle install), env: env)
161
+ Mrss::ChildProcessHelper.check_call(%w(bundle install), env: env)
140
162
  write_mongoid_yml
141
163
  end
142
164
 
@@ -150,7 +172,7 @@ describe 'Mongoid application tests' do
150
172
  end
151
173
  index.should be nil
152
174
 
153
- ChildProcessHelper.check_call(%w(rake db:mongoid:create_indexes),
175
+ Mrss::ChildProcessHelper.check_call(%w(bundle exec rake db:mongoid:create_indexes),
154
176
  cwd: APP_PATH, env: env)
155
177
 
156
178
  index = client['posts'].indexes.detect do |index|
@@ -165,13 +187,14 @@ describe 'Mongoid application tests' do
165
187
  end
166
188
  end
167
189
 
168
- def clone_application(repo_url, subdir: nil, rails_version: nil)
190
+ def clone_application(repo_url, subdir: nil)
169
191
  Dir.chdir(TMP_BASE) do
170
192
  FileUtils.rm_rf(File.basename(repo_url))
171
- ChildProcessHelper.check_call(%w(git clone) + [repo_url])
193
+ Mrss::ChildProcessHelper.check_call(%w(git clone) + [repo_url])
172
194
  Dir.chdir(File.join(*[File.basename(repo_url), subdir].compact)) do
173
- adjust_app_gemfile(rails_version: rails_version)
174
- ChildProcessHelper.check_call(%w(bundle install), env: clean_env)
195
+ adjust_app_gemfile
196
+ adjust_rails_defaults
197
+ Mrss::ChildProcessHelper.check_call(%w(bundle install), env: clean_env)
175
198
  puts `git diff`
176
199
 
177
200
  write_mongoid_yml
@@ -181,11 +204,44 @@ describe 'Mongoid application tests' do
181
204
  end
182
205
  end
183
206
 
207
+ def parse_mongodb_uri(uri)
208
+ pre, query = uri.split('?', 2)
209
+ if pre =~ %r,\A(mongodb(?:.*?))://([^/]+)(?:/(.*))?\z,
210
+ protocol = $1
211
+ hosts = $2
212
+ database = $3
213
+ if database == ''
214
+ database = nil
215
+ end
216
+ else
217
+ raise ArgumentError, "Invalid MongoDB URI: #{uri}"
218
+ end
219
+ if query == ''
220
+ query = nil
221
+ end
222
+ {
223
+ protocol: protocol,
224
+ hosts: hosts,
225
+ database: database,
226
+ query: query,
227
+ }
228
+ end
229
+
230
+ def build_mongodb_uri(parts)
231
+ "#{parts.fetch(:protocol)}://#{parts.fetch(:hosts)}/#{parts[:database]}?#{parts[:query]}"
232
+ end
233
+
184
234
  def write_mongoid_yml
235
+ # HACK: the driver does not provide a MongoDB URI parser and assembler,
236
+ # and the Ruby standard library URI module doesn't handle multiple hosts.
237
+ parts = parse_mongodb_uri(SpecConfig.instance.uri_str)
238
+ parts[:database] = 'mongoid_test'
239
+ uri = build_mongodb_uri(parts)
240
+ p uri
185
241
  env_config = {'clients' => {'default' => {
186
242
  # TODO massive hack, will fail if uri specifies a database name or
187
243
  # any uri options
188
- 'uri' => "#{SpecConfig.instance.uri_str}/mongoid_test",
244
+ 'uri' => uri,
189
245
  }}}
190
246
  config = {'development' => env_config, 'production' => env_config}
191
247
  File.open('config/mongoid.yml', 'w') do |f|
@@ -193,7 +249,7 @@ describe 'Mongoid application tests' do
193
249
  end
194
250
  end
195
251
 
196
- def adjust_app_gemfile(rails_version: nil)
252
+ def adjust_app_gemfile(rails_version: SpecConfig.instance.rails_version)
197
253
  remove_bundler_req
198
254
 
199
255
  gemfile_lines = IO.readlines('Gemfile')
@@ -205,13 +261,41 @@ describe 'Mongoid application tests' do
205
261
  gemfile_lines.delete_if do |line|
206
262
  line =~ /rails/
207
263
  end
208
- gemfile_lines << "gem 'rails', '#{rails_version}'\n"
264
+ if rails_version == 'master'
265
+ gemfile_lines << "gem 'rails', git: 'https://github.com/rails/rails'\n"
266
+ else
267
+ gemfile_lines << "gem 'rails', '~> #{rails_version}.0'\n"
268
+ end
209
269
  end
210
270
  File.open('Gemfile', 'w') do |f|
211
271
  f << gemfile_lines.join
212
272
  end
213
273
  end
214
274
 
275
+ def adjust_rails_defaults(rails_version: SpecConfig.instance.rails_version)
276
+ if File.exist?('config/application.rb')
277
+ lines = IO.readlines('config/application.rb')
278
+ lines.each do |line|
279
+ line.gsub!(/config.load_defaults \d\.\d/, "config.load_defaults #{rails_version}")
280
+ end
281
+ File.open('config/application.rb', 'w') do |f|
282
+ f << lines.join
283
+ end
284
+ end
285
+
286
+ if rails_version == '5.1'
287
+ secrets = {
288
+ 'development' => {
289
+ 'secret_key_base' => 'abracadabra',
290
+ 'my_secret_token' => 'very_secret',
291
+ },
292
+ }
293
+ File.open('config/secrets.yml', 'w') do |f|
294
+ f << YAML.dump(secrets)
295
+ end
296
+ end
297
+ end
298
+
215
299
  def remove_bundler_req
216
300
  lock_lines = IO.readlines('Gemfile.lock')
217
301
  # Get rid of the bundled with line so that whatever bundler is installed
@@ -230,14 +314,14 @@ describe 'Mongoid application tests' do
230
314
  # in `initialize': too long unix socket path (126bytes given but 108bytes max) (ArgumentError)
231
315
  # Is it trying to create unix sockets in current directory?
232
316
  # https://stackoverflow.com/questions/30302021/rails-runner-without-spring
233
- ChildProcessHelper.check_call(%w(bin/spring binstub --remove --all), env: clean_env)
317
+ Mrss::ChildProcessHelper.check_call(%w(bin/spring binstub --remove --all), env: clean_env)
234
318
  end
235
319
 
236
320
  def clean_env
237
321
  @clean_env ||= Hash[ENV.keys.grep(/BUNDLE|RUBYOPT/).map { |k| [k, nil ] }]
238
322
  end
239
323
 
240
- def wait_for_port(port, timeout)
324
+ def wait_for_port(port, timeout, process)
241
325
  deadline = Time.now + timeout
242
326
  loop do
243
327
  begin
@@ -245,6 +329,9 @@ describe 'Mongoid application tests' do
245
329
  return
246
330
  end
247
331
  rescue IOError, SystemCallError
332
+ unless process.alive?
333
+ raise "Process #{process} died while waiting for port #{port}"
334
+ end
248
335
  if Time.now > deadline
249
336
  raise
250
337
  end