mongoid 7.2.3 → 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 (152) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data.tar.gz.sig +0 -0
  4. data/Rakefile +16 -0
  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/nested_builder.rb +2 -2
  19. data/lib/mongoid/attributes.rb +24 -13
  20. data/lib/mongoid/attributes/projector.rb +120 -0
  21. data/lib/mongoid/cacheable.rb +2 -2
  22. data/lib/mongoid/clients.rb +1 -1
  23. data/lib/mongoid/clients/factory.rb +22 -8
  24. data/lib/mongoid/config.rb +19 -2
  25. data/lib/mongoid/contextual/aggregable/mongo.rb +10 -8
  26. data/lib/mongoid/copyable.rb +1 -1
  27. data/lib/mongoid/criteria.rb +4 -5
  28. data/lib/mongoid/criteria/findable.rb +1 -1
  29. data/lib/mongoid/criteria/queryable/expandable.rb +0 -24
  30. data/lib/mongoid/criteria/queryable/extensions.rb +0 -4
  31. data/lib/mongoid/criteria/queryable/extensions/boolean.rb +1 -1
  32. data/lib/mongoid/criteria/queryable/mergeable.rb +46 -20
  33. data/lib/mongoid/criteria/queryable/selectable.rb +8 -8
  34. data/lib/mongoid/document.rb +1 -15
  35. data/lib/mongoid/errors/delete_restriction.rb +8 -9
  36. data/lib/mongoid/evolvable.rb +1 -1
  37. data/lib/mongoid/extensions/boolean.rb +1 -2
  38. data/lib/mongoid/extensions/false_class.rb +1 -1
  39. data/lib/mongoid/extensions/hash.rb +2 -2
  40. data/lib/mongoid/extensions/true_class.rb +1 -1
  41. data/lib/mongoid/fields.rb +43 -5
  42. data/lib/mongoid/inspectable.rb +1 -1
  43. data/lib/mongoid/matcher.rb +7 -0
  44. data/lib/mongoid/matcher/bits.rb +41 -0
  45. data/lib/mongoid/matcher/bits_all_clear.rb +20 -0
  46. data/lib/mongoid/matcher/bits_all_set.rb +20 -0
  47. data/lib/mongoid/matcher/bits_any_clear.rb +20 -0
  48. data/lib/mongoid/matcher/bits_any_set.rb +20 -0
  49. data/lib/mongoid/matcher/expression.rb +4 -0
  50. data/lib/mongoid/matcher/field_operator.rb +6 -0
  51. data/lib/mongoid/matcher/mod.rb +17 -0
  52. data/lib/mongoid/matcher/type.rb +99 -0
  53. data/lib/mongoid/persistable/deletable.rb +1 -2
  54. data/lib/mongoid/persistable/destroyable.rb +8 -2
  55. data/lib/mongoid/persistable/updatable.rb +27 -2
  56. data/lib/mongoid/query_cache.rb +35 -29
  57. data/lib/mongoid/selectable.rb +5 -7
  58. data/lib/mongoid/shardable.rb +21 -5
  59. data/lib/mongoid/touchable.rb +23 -4
  60. data/lib/mongoid/version.rb +1 -1
  61. data/spec/integration/associations/embeds_many_spec.rb +44 -0
  62. data/spec/integration/associations/has_one_spec.rb +48 -0
  63. data/spec/integration/criteria/date_field_spec.rb +1 -1
  64. data/spec/integration/document_spec.rb +9 -0
  65. data/spec/integration/matcher_operator_data/bits_all_clear.yml +159 -0
  66. data/spec/integration/matcher_operator_data/bits_all_set.yml +159 -0
  67. data/spec/integration/matcher_operator_data/bits_any_clear.yml +159 -0
  68. data/spec/integration/matcher_operator_data/bits_any_set.yml +159 -0
  69. data/spec/integration/matcher_operator_data/comment.yml +22 -0
  70. data/spec/integration/matcher_operator_data/in.yml +16 -0
  71. data/spec/integration/matcher_operator_data/mod.yml +55 -0
  72. data/spec/integration/matcher_operator_data/type.yml +70 -0
  73. data/spec/integration/matcher_operator_data/type_array.yml +16 -0
  74. data/spec/integration/matcher_operator_data/type_binary.yml +18 -0
  75. data/spec/integration/matcher_operator_data/type_boolean.yml +39 -0
  76. data/spec/integration/matcher_operator_data/type_code.yml +26 -0
  77. data/spec/integration/matcher_operator_data/type_code_with_scope.yml +26 -0
  78. data/spec/integration/matcher_operator_data/type_date.yml +39 -0
  79. data/spec/integration/matcher_operator_data/type_db_pointer.yml +19 -0
  80. data/spec/integration/matcher_operator_data/type_decimal.yml +40 -0
  81. data/spec/integration/matcher_operator_data/type_double.yml +15 -0
  82. data/spec/integration/matcher_operator_data/type_int32.yml +33 -0
  83. data/spec/integration/matcher_operator_data/type_int64.yml +33 -0
  84. data/spec/integration/matcher_operator_data/type_max_key.yml +17 -0
  85. data/spec/integration/matcher_operator_data/type_min_key.yml +17 -0
  86. data/spec/integration/matcher_operator_data/type_null.yml +23 -0
  87. data/spec/integration/matcher_operator_data/type_object.yml +23 -0
  88. data/spec/integration/matcher_operator_data/type_object_id.yml +25 -0
  89. data/spec/integration/matcher_operator_data/type_regex.yml +44 -0
  90. data/spec/integration/matcher_operator_data/type_string.yml +15 -0
  91. data/spec/integration/matcher_operator_data/type_symbol.yml +32 -0
  92. data/spec/integration/matcher_operator_data/type_timestamp.yml +25 -0
  93. data/spec/integration/matcher_operator_data/type_undefined.yml +17 -0
  94. data/spec/lite_spec_helper.rb +2 -0
  95. data/spec/mongoid/association/depending_spec.rb +391 -352
  96. data/spec/mongoid/association/nested/one_spec.rb +18 -14
  97. data/spec/mongoid/association/referenced/belongs_to/proxy_spec.rb +25 -8
  98. data/spec/mongoid/association/referenced/has_and_belongs_to_many/binding_spec.rb +1 -1
  99. data/spec/mongoid/association/referenced/has_many/binding_spec.rb +1 -1
  100. data/spec/mongoid/association/referenced/has_many/enumerable_spec.rb +1 -1
  101. data/spec/mongoid/association/referenced/has_one_models.rb +8 -0
  102. data/spec/mongoid/atomic/paths_spec.rb +64 -12
  103. data/spec/mongoid/attributes/projector_data/embedded.yml +105 -0
  104. data/spec/mongoid/attributes/projector_data/fields.yml +93 -0
  105. data/spec/mongoid/attributes/projector_spec.rb +41 -0
  106. data/spec/mongoid/attributes_spec.rb +98 -6
  107. data/spec/mongoid/clients/factory_spec.rb +48 -0
  108. data/spec/mongoid/config_spec.rb +32 -0
  109. data/spec/mongoid/contextual/mongo_spec.rb +2 -2
  110. data/spec/mongoid/criteria/modifiable_spec.rb +1 -1
  111. data/spec/mongoid/criteria/queryable/expandable_spec.rb +0 -73
  112. data/spec/mongoid/criteria/queryable/extensions/boolean_spec.rb +1 -1
  113. data/spec/mongoid/criteria/queryable/mergeable_spec.rb +105 -7
  114. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +229 -24
  115. data/spec/mongoid/criteria/queryable/selectable_shared_examples.rb +39 -0
  116. data/spec/mongoid/criteria/queryable/selectable_spec.rb +1 -565
  117. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +590 -0
  118. data/spec/mongoid/criteria_projection_spec.rb +411 -0
  119. data/spec/mongoid/criteria_spec.rb +0 -275
  120. data/spec/mongoid/document_spec.rb +13 -13
  121. data/spec/mongoid/errors/delete_restriction_spec.rb +1 -1
  122. data/spec/mongoid/extensions/false_class_spec.rb +1 -1
  123. data/spec/mongoid/extensions/string_spec.rb +5 -5
  124. data/spec/mongoid/extensions/true_class_spec.rb +1 -1
  125. data/spec/mongoid/fields/localized_spec.rb +4 -4
  126. data/spec/mongoid/fields_spec.rb +4 -4
  127. data/spec/mongoid/inspectable_spec.rb +12 -4
  128. data/spec/mongoid/persistable/deletable_spec.rb +175 -1
  129. data/spec/mongoid/persistable/destroyable_spec.rb +191 -3
  130. data/spec/mongoid/persistable/savable_spec.rb +3 -5
  131. data/spec/mongoid/persistable/upsertable_spec.rb +1 -1
  132. data/spec/mongoid/query_cache_middleware_spec.rb +8 -0
  133. data/spec/mongoid/reloadable_spec.rb +18 -1
  134. data/spec/mongoid/shardable_spec.rb +44 -0
  135. data/spec/mongoid/touchable_spec.rb +104 -16
  136. data/spec/mongoid/touchable_spec_models.rb +52 -0
  137. data/spec/mongoid/validatable_spec.rb +1 -1
  138. data/spec/spec_helper.rb +6 -2
  139. data/spec/support/client_registry.rb +9 -0
  140. data/spec/support/models/bolt.rb +8 -0
  141. data/spec/support/models/hole.rb +13 -0
  142. data/spec/support/models/mop.rb +0 -1
  143. data/spec/support/models/nut.rb +8 -0
  144. data/spec/support/models/person.rb +6 -0
  145. data/spec/support/models/sealer.rb +8 -0
  146. data/spec/support/models/shirt.rb +12 -0
  147. data/spec/support/models/spacer.rb +8 -0
  148. data/spec/support/models/threadlocker.rb +8 -0
  149. data/spec/support/models/washer.rb +8 -0
  150. metadata +97 -3
  151. metadata.gz.sig +5 -3
  152. data/spec/support/cluster_config.rb +0 -158
@@ -0,0 +1,20 @@
1
+ module Mongoid
2
+ module Matcher
3
+
4
+ # @api private
5
+ module BitsAllSet
6
+ include Bits
7
+ extend self
8
+
9
+ def array_matches?(value, condition)
10
+ condition.all? do |c|
11
+ value & (1<<c) > 0
12
+ end
13
+ end
14
+
15
+ def int_matches?(value, condition)
16
+ value & condition == condition
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Mongoid
2
+ module Matcher
3
+
4
+ # @api private
5
+ module BitsAnyClear
6
+ include Bits
7
+ extend self
8
+
9
+ def array_matches?(value, condition)
10
+ condition.any? do |c|
11
+ value & (1<<c) == 0
12
+ end
13
+ end
14
+
15
+ def int_matches?(value, condition)
16
+ value & condition < condition
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ module Mongoid
2
+ module Matcher
3
+
4
+ # @api private
5
+ module BitsAnySet
6
+ include Bits
7
+ extend self
8
+
9
+ def array_matches?(value, condition)
10
+ condition.any? do |c|
11
+ value & (1<<c) > 0
12
+ end
13
+ end
14
+
15
+ def int_matches?(value, condition)
16
+ value & condition > 0
17
+ end
18
+ end
19
+ end
20
+ end
@@ -12,6 +12,10 @@ module Mongoid
12
12
  end
13
13
  expr.all? do |k, expr_v|
14
14
  k = k.to_s
15
+ if k == "$comment"
16
+ # Nothing
17
+ return true
18
+ end
15
19
  if k.start_with?('$')
16
20
  ExpressionOperator.get(k).matches?(document, expr_v)
17
21
  else
@@ -5,6 +5,10 @@ module Mongoid
5
5
  module FieldOperator
6
6
  MAP = {
7
7
  '$all' => All,
8
+ '$bitsAllClear' => BitsAllClear,
9
+ '$bitsAllSet' => BitsAllSet,
10
+ '$bitsAnyClear' => BitsAnyClear,
11
+ '$bitsAnySet' => BitsAnySet,
8
12
  '$elemMatch' => ElemMatch,
9
13
  '$eq' => Eq,
10
14
  '$exists' => Exists,
@@ -13,11 +17,13 @@ module Mongoid
13
17
  '$in' => In,
14
18
  '$lt' => Lt,
15
19
  '$lte' => Lte,
20
+ '$mod' => Mod,
16
21
  '$nin' => Nin,
17
22
  '$ne' => Ne,
18
23
  '$not' => Not,
19
24
  '$regex' => Regex,
20
25
  '$size' => Size,
26
+ '$type' => Type,
21
27
  }.freeze
22
28
 
23
29
  module_function def get(op)
@@ -0,0 +1,17 @@
1
+ module Mongoid
2
+ module Matcher
3
+
4
+ # @api private
5
+ module Mod
6
+ module_function def matches?(exists, value, condition)
7
+ unless Array === condition
8
+ raise Errors::InvalidQuery, "Unknown $mod argument #{condition}"
9
+ end
10
+ if condition.length != 2
11
+ raise Errors::InvalidQuery, "Malformed $mod argument #{condition}, should have 2 elements"
12
+ end
13
+ condition[1] == value%condition[0]
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,99 @@
1
+ module Mongoid
2
+ module Matcher
3
+
4
+ # @see https://docs.mongodb.com/manual/reference/operator/query/type/
5
+ #
6
+ # @api private
7
+ module Type
8
+ module_function def matches?(exists, value, condition)
9
+ conditions = case condition
10
+ when Array
11
+ condition
12
+ when Integer
13
+ [condition]
14
+ else
15
+ raise Errors::InvalidQuery, "Unknown $type argument: #{condition}"
16
+ end
17
+ conditions.each do |condition|
18
+ if one_matches?(exists, value, condition)
19
+ return true
20
+ end
21
+ end
22
+ false
23
+ end
24
+
25
+ module_function def one_matches?(exists, value, condition)
26
+ case condition
27
+ when 1
28
+ # Double
29
+ Float === value
30
+ when 2
31
+ # String
32
+ String === value
33
+ when 3
34
+ # Object
35
+ Hash === value
36
+ when 4
37
+ # Array
38
+ Array === value
39
+ when 5
40
+ # Binary data
41
+ BSON::Binary === value
42
+ when 6
43
+ # Undefined
44
+ BSON::Undefined === value
45
+ when 7
46
+ # ObjectId
47
+ BSON::ObjectId === value
48
+ when 8
49
+ # Boolean
50
+ TrueClass === value || FalseClass === value
51
+ when 9
52
+ # Date
53
+ Date === value || Time === value || DateTime === value
54
+ when 10
55
+ # Null
56
+ exists && NilClass === value
57
+ when 11
58
+ # Regex
59
+ Regexp::Raw === value || ::Regexp === value
60
+ when 12
61
+ # DBPointer deprecated
62
+ BSON::DbPointer === value
63
+ when 13
64
+ # JavaScript
65
+ BSON::Code === value
66
+ when 14
67
+ # Symbol deprecated
68
+ Symbol === value || BSON::Symbol::Raw === value
69
+ when 15
70
+ # Javascript with code deprecated
71
+ BSON::CodeWithScope === value
72
+ when 16
73
+ # 32-bit int
74
+ BSON::Int32 === value || Integer === value && (-2**32..2**32-1).include?(value)
75
+ when 17
76
+ # Timestamp
77
+ BSON::Timestamp === value
78
+ when 18
79
+ # Long
80
+ BSON::Int64 === value ||
81
+ Integer === value &&
82
+ (-2**64..2**64-1).include?(value) &&
83
+ !(-2**32..2**32-1).include?(value)
84
+ when 19
85
+ # Decimal
86
+ BSON::Decimal128 === value
87
+ when -1
88
+ # minKey
89
+ BSON::MinKey === value
90
+ when 127
91
+ # maxKey
92
+ BSON::MaxKey === value
93
+ else
94
+ raise Errors::InvalidQuery, "Unknown $type argument: #{condition}"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -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,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)
@@ -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