mongoid 9.1.1 → 9.1.2

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +31 -11
  3. data/lib/mongoid/association/accessors.rb +18 -4
  4. data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +10 -10
  5. data/lib/mongoid/association/eager_load/embedded_distributor.rb +6 -6
  6. data/lib/mongoid/association/eager_load/lookup_pipeline.rb +1 -1
  7. data/lib/mongoid/association/eager_loadable.rb +8 -1
  8. data/lib/mongoid/association/embedded/batchable.rb +17 -1
  9. data/lib/mongoid/association/nested/many.rb +1 -1
  10. data/lib/mongoid/association/referenced/has_and_belongs_to_many/buildable.rb +6 -0
  11. data/lib/mongoid/association/relatable.rb +18 -0
  12. data/lib/mongoid/association.rb +8 -0
  13. data/lib/mongoid/atomic/modifiers.rb +100 -16
  14. data/lib/mongoid/clients/factory.rb +2 -0
  15. data/lib/mongoid/config/encryption.rb +148 -17
  16. data/lib/mongoid/document.rb +3 -0
  17. data/lib/mongoid/encryptable.rb +42 -15
  18. data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
  19. data/lib/mongoid/errors/no_encryption_schema.rb +5 -3
  20. data/lib/mongoid/errors.rb +1 -0
  21. data/lib/mongoid/fields.rb +7 -0
  22. data/lib/mongoid/persistable/creatable.rb +120 -14
  23. data/lib/mongoid/persistable.rb +15 -15
  24. data/lib/mongoid/persistence_context.rb +46 -4
  25. data/lib/mongoid/search_indexable.rb +5 -1
  26. data/lib/mongoid/tasks/database.rb +10 -7
  27. data/lib/mongoid/touchable.rb +28 -1
  28. data/lib/mongoid/version.rb +1 -1
  29. data/lib/mongoid/warnings.rb +4 -0
  30. data/spec/integration/associations/embedded_delayed_atomic_sets_spec.rb +216 -0
  31. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +14 -0
  32. data/spec/integration/atomic/modifiers_spec.rb +283 -0
  33. data/spec/integration/encryption_spec.rb +149 -0
  34. data/spec/mongoid/association/accessors_spec.rb +35 -0
  35. data/spec/mongoid/association/referenced/has_and_belongs_to_many/buildable_spec.rb +11 -0
  36. data/spec/mongoid/atomic/modifiers_spec.rb +347 -0
  37. data/spec/mongoid/attributes/nested_spec.rb +29 -0
  38. data/spec/mongoid/clients/factory_spec.rb +36 -0
  39. data/spec/mongoid/config/encryption_spec.rb +223 -0
  40. data/spec/mongoid/criteria/includable_spec.rb +346 -0
  41. data/spec/mongoid/encryptable_spec.rb +54 -0
  42. data/spec/mongoid/fields_spec.rb +8 -0
  43. data/spec/mongoid/search_indexable_spec.rb +26 -0
  44. data/spec/mongoid/tasks/database_spec.rb +13 -0
  45. data/spec/mongoid/touchable_spec.rb +217 -0
  46. data/spec/mongoid/touchable_spec_models.rb +37 -0
  47. data/spec/support/crypt/models.rb +96 -0
  48. data/spec/support/models/modifier_models.rb +65 -0
  49. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5bfb9894822f364261e1579143e0a4860a27dfe42437243938ecb2bc186a0819
4
- data.tar.gz: f66924e8684458b2318f82bc7614f01823938e8c7cb3f19bf5246dd1098dd5b5
3
+ metadata.gz: 0f94dec89da2023d081f5c08eed8a5ee4f61fd511b7f8eaea3dd8afde665829d
4
+ data.tar.gz: cb6e13ef1c7e708620d747a31f4cfff15088bd451c41259c2ade7b9bf4b26bdb
5
5
  SHA512:
6
- metadata.gz: 16d064d36b9a6a425d2727afdb879da7a28dd25a5889de7dd6242120d69441fb666db16dec584b0e00c77aed32fcd0a5d900c7fc3ed8ddfe0fe2006b0053dbc2
7
- data.tar.gz: 2d8c367c96724961cdd78354a8bf6d10cef2fa209cec4103741e26d43389286a173a8df301116121b2e9ad89495c7c5e268b4458dd8b22e6a2ddb4fbdd835eb0
6
+ metadata.gz: e4695df34902c08d6c0bb8f95f9e128f0a154b5a859b3e97181646cb6cd9f24a2d138be0721c2bba761e1db5327d7b207ce104c8d6edad1b9336f6dc62aa2e39
7
+ data.tar.gz: 99cb2aa72fcdf82ecd8f43d2a5bae06113a1af687d1e01d7a54f12e6b996c517c817b7b9b72c0418ddbd8e09af6e692159852cf8acafa3166166db5174acf34d
@@ -239,6 +239,13 @@ en:
239
239
  Mongoid does not support."
240
240
  resolution: "Valid field operators are: %{valid_operators}.
241
241
  Ensure you are using one of these operators."
242
+ invalid_encryption_schema:
243
+ message: "The encryption schema for %{namespace} is invalid."
244
+ summary: "Two models writing to %{namespace} describe %{path}
245
+ differently. The encryption schema has one entry per namespace,
246
+ so the two descriptions cannot both be used."
247
+ resolution: "Make the models agree on the encryption options of the
248
+ conflicting field, or store them in different collections."
242
249
  invalid_estimated_count_criteria:
243
250
  message: "Invalid criteria for estimated_count."
244
251
  summary: "Estimated count is strictly a collection-level operation and cannot be called
@@ -544,22 +551,35 @@ en:
544
551
  no_encryption_schema:
545
552
  message: "The encryption schema of client %{client} does not cover
546
553
  %{namespace}."
547
- summary: "%{klass} declares encrypted fields, but the namespace it
548
- resolved to, %{namespace}, is not part of the automatic encryption
549
- schema of client %{client}. The encryption schema is built once,
550
- when the client is created, and is keyed by namespace, so a
551
- database name that is only known later - a callable :database
552
- option, Model.with(database:), Mongoid.override_database - is not
553
- in it. Routing the model to a client that has no
554
- auto_encryption_options has the same effect. Mongoid refuses the
555
- operation because the driver would store the declared fields in
556
- plaintext without reporting an error."
554
+ summary: "%{klass} declares encrypted fields, directly or through
555
+ an embedded model, but the namespace it resolved to, %{namespace},
556
+ is not part of the automatic encryption schema of client
557
+ %{client}. The encryption schema is built once, when the client
558
+ is created, and is keyed by namespace, so a database name that is
559
+ only known later - a callable :database option,
560
+ Model.with(database:), Mongoid.override_database - is not in it.
561
+ The same happens when an embedded model with encrypted fields was
562
+ not yet loaded when the client was created. Routing the model to a
563
+ client that has no auto_encryption_options has the same effect.
564
+ Mongoid refuses the operation because the driver would store the
565
+ declared fields in plaintext without reporting an error."
557
566
  resolution: "Give the model a namespace that is known when the
558
567
  client is created, and a client configured with
559
- auto_encryption_options. If the model has to live in a database
568
+ auto_encryption_options. Load all models, including embedded
569
+ ones, before the first client is created, for example by
570
+ preloading them at boot. If the model has to live in a database
560
571
  chosen at runtime, configure one client per database, each with
561
572
  its own auto_encryption_options, and select between them with
562
573
  Model.with(client:) instead of switching the database."
574
+ no_encryption_schema_field:
575
+ message: "The encryption schema of client %{client} does not cover
576
+ the field %{field} of %{namespace}."
577
+ summary: "The namespace %{namespace} is in the automatic encryption
578
+ schema of client %{client}, but its entry does not encrypt
579
+ %{field}, which %{klass} declares as encrypted. Mongoid refuses
580
+ the operation rather than storing the field in plaintext."
581
+ resolution: "Regenerate the schema map of the client, or add
582
+ %{field} to the schema entry for %{namespace}."
563
583
  no_map_reduce_output:
564
584
  message: "No output location was specified for the map/reduce
565
585
  operation."
@@ -86,6 +86,24 @@ module Mongoid
86
86
  instance_variable_set("@_#{name}", relation)
87
87
  end
88
88
 
89
+ # Loads the documents the query preloaded for this one, kept apart from the
90
+ # attributes so they are not part of the document. A name this class
91
+ # doesn't define is dropped: a $lookup writes its field on every document
92
+ # it returns, including those of a sibling subclass that has no such
93
+ # association.
94
+ #
95
+ # @param [ Hash ] preloaded The preloaded documents by the field they
96
+ # travelled in.
97
+ #
98
+ # @api private
99
+ def load_preloaded_associations(preloaded)
100
+ preloaded.each do |field, documents|
101
+ name = field.delete_prefix(PRELOADED_PREFIX)
102
+ association = relations[name]
103
+ __build__(name, documents, association) if association
104
+ end
105
+ end
106
+
89
107
  private
90
108
 
91
109
  # Get the association. Extracted out from the getter method to avoid
@@ -127,10 +145,6 @@ module Mongoid
127
145
  _loading do
128
146
  if object && needs_no_database_query?(object, association)
129
147
  __build__(name, object, association)
130
- # Check if data was loaded via $lookup aggregation
131
- elsif !association.embedded? && attributes.key?(name.to_s)
132
- # Use the pre-loaded association data from $lookup
133
- __build__(name, attributes[name.to_s], association)
134
148
  else
135
149
  selected_fields = _mongoid_filter_selected_fields(association.key)
136
150
  __build__(name, attributes[association.key], association, selected_fields)
@@ -15,15 +15,15 @@ module Mongoid
15
15
  # For Machine.eager_load(:widgets), where Lathe#widgets => Cog and
16
16
  # Press#widgets => Belt, it emits:
17
17
  #
18
- # { '$lookup' => { 'from' => 'cogs', ..., 'as' => '__eager_load_widgets_Lathe' } },
19
- # { '$lookup' => { 'from' => 'belts', ..., 'as' => '__eager_load_widgets_Press' } },
18
+ # { '$lookup' => { 'from' => 'cogs', ..., 'as' => '__mongoid_eager_loaded_widgets_Lathe' } },
19
+ # { '$lookup' => { 'from' => 'belts', ..., 'as' => '__mongoid_eager_loaded_widgets_Press' } },
20
20
  # { '$set' => {
21
- # 'widgets' => { '$switch' => { 'branches' => [ # route each document to its
22
- # { 'case' => { '$eq' => [ '$_type', 'Lathe' ] }, 'then' => '$__eager_load_widgets_Lathe' }, # own type's matches
23
- # { 'case' => { '$eq' => [ '$_type', 'Press' ] }, 'then' => '$__eager_load_widgets_Press' }
21
+ # '__mongoid_eager_loaded_widgets' => { '$switch' => { 'branches' => [ # route each document to its
22
+ # { 'case' => { '$eq' => [ '$_type', 'Lathe' ] }, 'then' => '$__mongoid_eager_loaded_widgets_Lathe' }, # own type's matches
23
+ # { 'case' => { '$eq' => [ '$_type', 'Press' ] }, 'then' => '$__mongoid_eager_loaded_widgets_Press' }
24
24
  # ], 'default' => [] } }
25
25
  # } },
26
- # { '$unset' => [ '__eager_load_widgets_Lathe', '__eager_load_widgets_Press' ] }
26
+ # { '$unset' => [ '__mongoid_eager_loaded_widgets_Lathe', '__mongoid_eager_loaded_widgets_Press' ] }
27
27
  #
28
28
  # @api private
29
29
  class DiscriminatedInclusion < Inclusion
@@ -56,7 +56,7 @@ module Mongoid
56
56
  end
57
57
 
58
58
  def temporary_field(node)
59
- "__eager_load_#{node.association.name}_#{owner(node).discriminator_value}"
59
+ node.association.temporary_field(owner(node).discriminator_value)
60
60
  end
61
61
 
62
62
  # The $set that fills the association on each document from the temporary
@@ -68,15 +68,15 @@ module Mongoid
68
68
  'then' => "$#{field}"
69
69
  }
70
70
  end
71
- { '$set' => { name => { '$switch' => { 'branches' => branches, 'default' => [] } } } }
71
+ { '$set' => { preloaded_field => { '$switch' => { 'branches' => branches, 'default' => [] } } } }
72
72
  end
73
73
 
74
74
  def owner(node)
75
75
  node.association.inverse_class
76
76
  end
77
77
 
78
- def name
79
- @nodes.first.association.name.to_s
78
+ def preloaded_field
79
+ @nodes.first.association.preloaded_field
80
80
  end
81
81
 
82
82
  def discriminator_key
@@ -22,15 +22,15 @@ module Mongoid
22
22
  # 'from' => 'devices', # the embedded port, so they are
23
23
  # 'localField' => 'port.device_id', # collected in a temp top-level
24
24
  # 'foreignField' => '_id', # field instead
25
- # 'as' => '__eager_load_port_device'
25
+ # 'as' => '__mongoid_eager_loaded_device_port'
26
26
  # } },
27
27
  # { '$set' => {
28
- # 'port' => { '$mergeObjects' => [ # merge a 'device' key onto the port
28
+ # 'port' => { '$mergeObjects' => [ # merge the devices onto the port
29
29
  # '$port',
30
- # { 'device' => { '$filter' => { ... } } } # this port's matches
30
+ # { '__mongoid_eager_loaded_device' => { '$filter' => { ... } } } # this port's matches
31
31
  # ] }
32
32
  # } },
33
- # { '$unset' => '__eager_load_port_device' } # drop the temp field
33
+ # { '$unset' => '__mongoid_eager_loaded_device_port' } # drop the temp field
34
34
  #
35
35
  # @api private
36
36
  class EmbeddedDistributor
@@ -92,7 +92,7 @@ module Mongoid
92
92
  end
93
93
 
94
94
  def temporary_field
95
- "__eager_load_#{path.tr('.', '_')}_#{@association.name}"
95
+ @association.temporary_field(path.tr('.', '_'))
96
96
  end
97
97
 
98
98
  # An embedded collection (embeds_many) is rebuilt with $map so each element
@@ -104,7 +104,7 @@ module Mongoid
104
104
  element = many ? "$$#{head.store_as}" : node
105
105
  child =
106
106
  if rest.empty?
107
- { @association.name.to_s => correlated_matches(element) }
107
+ { @association.preloaded_field => correlated_matches(element) }
108
108
  else
109
109
  segment = rest.first.store_as
110
110
  { segment => distributed_value(rest, "#{element}.#{segment}") }
@@ -67,7 +67,7 @@ module Mongoid
67
67
  'from' => association.klass.collection.name,
68
68
  'localField' => local_field,
69
69
  'foreignField' => foreign_field,
70
- 'as' => association.name.to_s,
70
+ 'as' => association.preloaded_field,
71
71
  'pipeline' => sub_pipeline
72
72
  } }
73
73
  end
@@ -94,7 +94,14 @@ module Mongoid
94
94
  next unless as = assoc_map.delete(klass)
95
95
 
96
96
  as.each do |assoc|
97
- queue << assoc.class_name
97
+ # The queue is matched against the assoc_map keys, which are
98
+ # inverse_class_name values (resolved class names). Push the
99
+ # resolved class name so a class_name such as "::Photo" or a
100
+ # module-relative "Item" still matches its nested associations.
101
+ # A truly polymorphic association (a polymorphic belongs_to) has
102
+ # no single resolvable target class, so fall back to the raw name.
103
+ target = assoc.try_relation_class
104
+ queue << (target ? target.name : assoc.class_name)
98
105
 
99
106
  # If this class is nested in the inclusion tree, only load documents
100
107
  # for the association above it. If there is no parent association,
@@ -130,13 +130,29 @@ module Mongoid
130
130
  # batchable.add_atomic_sets([{ field: value }])
131
131
  #
132
132
  # @param [ Array<Hash> ] sets The atomic sets.
133
+ #
134
+ # @return [ Array<Hash> | nil ] The atomic sets, or nil if the base
135
+ # document is not persisted or is not being assigned.
133
136
  def add_atomic_sets(sets)
134
- return unless _assigning?
137
+ # New embedded documents are persisted whole by their parent with a
138
+ # $set or $push. A delayed update for one of their associations is
139
+ # redundant whether or not the document is attached to its parent.
140
+ # Before attachment, its path is relative to the document instead
141
+ # of the root.
142
+ return unless _assigning? && _base.persisted?
135
143
 
136
144
  _base.delayed_atomic_sets[path].try(:clear)
137
145
  _base.collect_children.each do |child|
138
146
  child.delayed_atomic_sets.clear
139
147
  end
148
+
149
+ # If the base document is itself inserted wholesale via a $set of
150
+ # its atomic path, that $set already contains these documents, and
151
+ # a nested $set under the same path would conflict with it. The
152
+ # cleanup above has already cleared the delayed sets the assigned
153
+ # children recorded, so skipping the recording here is safe.
154
+ return if _base.settable?
155
+
140
156
  _base.delayed_atomic_sets[path] = sets
141
157
  end
142
158
 
@@ -230,7 +230,7 @@ module Mongoid
230
230
  # the id is only ever resolved within the association itself.
231
231
  id
232
232
  else
233
- { _id: id, association.foreign_key => parent.id }
233
+ { _id: id, association.foreign_key => parent.send(association.primary_key) }
234
234
  end
235
235
  end
236
236
  end
@@ -20,6 +20,12 @@ module Mongoid
20
20
  # @return [ Array<Document> ] The documents.
21
21
  def build(_base, object, _type = nil, selected_fields = nil)
22
22
  if query?(object)
23
+ if object.is_a?(Array) && object.empty?
24
+ # An empty stored key array is not $lookup output. Build a none
25
+ # criteria so reset, empty?, and size do not query the database.
26
+ return query_criteria(nil)
27
+ end
28
+
23
29
  # Handle array of hashes from $lookup aggregation
24
30
  if object.is_a?(Array) && object.all? { |o| o.is_a?(Hash) }
25
31
  return object.map do |attrs|
@@ -217,6 +217,24 @@ module Mongoid
217
217
  stores_foreign_key? ? foreign_key : primary_key
218
218
  end
219
219
 
220
+ # The field a $lookup writes this association's preloaded documents into.
221
+ #
222
+ # @return [ String ] The field.
223
+ def preloaded_field
224
+ "#{PRELOADED_PREFIX}#{name}"
225
+ end
226
+
227
+ # A field where the aggregation collects this association's documents
228
+ # before moving them to where they belong. The qualifier tells it apart
229
+ # from the other temporary fields of the same association.
230
+ #
231
+ # @param [ String ] qualifier What tells this temporary field apart.
232
+ #
233
+ # @return [ String ] The field.
234
+ def temporary_field(qualifier)
235
+ "#{preloaded_field}_#{qualifier}"
236
+ end
237
+
220
238
  # The name of the setter on this object for assigning an associated object.
221
239
  #
222
240
  # @return [ String ] The setter name.
@@ -33,6 +33,14 @@ module Mongoid
33
33
  include Macros
34
34
  include Reflections
35
35
 
36
+ # Prefixes the field a $lookup writes an association's preloaded documents
37
+ # into. They travel apart from the document's own fields, so the hydration
38
+ # can keep them out of the attributes instead of leaving them on the
39
+ # document, where a later save would persist them.
40
+ #
41
+ # @api private
42
+ PRELOADED_PREFIX = '__mongoid_eager_loaded_'
43
+
36
44
  # Map the macros to their corresponding Association classes.
37
45
  #
38
46
  # @return [ Hash ] The mapping from macros to their Association class.
@@ -13,13 +13,9 @@ module Mongoid
13
13
  # @param [ Hash ] modifications The add to set modifiers.
14
14
  def add_to_set(modifications)
15
15
  modifications.each_pair do |field, value|
16
- if add_to_sets.has_key?(field)
17
- value.each do |val|
18
- add_to_sets[field]['$each'].push(val)
19
- end
20
- else
21
- add_to_sets[field] = { '$each' => value }
22
- end
16
+ mods = add_to_set_conflict?(field) ? conflicting_add_to_sets : add_to_sets
17
+ add_each_operation(mods, field, value)
18
+ add_to_set_fields[field.split('.', 2)[0]] = field
23
19
  end
24
20
  end
25
21
 
@@ -31,7 +27,8 @@ module Mongoid
31
27
  # @param [ Hash ] modifications The pull all modifiers.
32
28
  def pull_all(modifications)
33
29
  modifications.each_pair do |field, value|
34
- add_operation(pull_alls, field, value)
30
+ mods = pull_all_conflict?(field) ? conflicting_pull_alls : pull_alls
31
+ add_operation(mods, field, value)
35
32
  pull_fields[field.split('.', 2)[0]] = field
36
33
  end
37
34
  end
@@ -41,6 +38,14 @@ module Mongoid
41
38
  # @example Add pull all operations.
42
39
  # modifiers.pull({ "addresses" => { "_id" => { "$in" => [ 1, 2, 3 ]}}})
43
40
  #
41
+ # A $pull is never split out into a separate write. It shifts the
42
+ # positions of the remaining array elements, and the paths of the other
43
+ # operations on that array are built for the positions after the $pull.
44
+ # In a separate write the $pull could run after them, so they would hit
45
+ # a different element. Kept in the main update, an operation that
46
+ # conflicts with the $pull is rejected by MongoDB (code 40) without
47
+ # changing stored data.
48
+ #
44
49
  # @param [ Hash ] modifications The pull all modifiers.
45
50
  def pull(modifications)
46
51
  modifications.each_pair do |field, value|
@@ -99,6 +104,7 @@ module Mongoid
99
104
  else
100
105
  unsets.update(field => true)
101
106
  end
107
+ unset_fields[field.split('.', 2)[0]] = field
102
108
  end
103
109
  end
104
110
 
@@ -166,17 +172,28 @@ module Mongoid
166
172
  # @return [ true | false ] If this field is a conflict.
167
173
  def set_conflict?(field)
168
174
  name = field.split('.', 2)[0]
169
- pull_fields.has_key?(name) || push_fields.has_key?(name)
175
+ pull_fields.has_key?(name) || push_fields.has_key?(name) ||
176
+ add_to_set_fields.has_key?(name)
170
177
  end
171
178
 
172
179
  # Is the operation going to be a conflict for an $unset?
173
180
  #
181
+ # A $push or $addToSet does not shift existing array element positions,
182
+ # so a conflicting $unset can be safely applied in a separate write
183
+ # afterwards. A $pull does shift positions, so the $unset is not split
184
+ # out in that case; the operations stay in the same update and MongoDB
185
+ # rejects the update (code 40) without changing stored data, rather
186
+ # than risking the $unset hitting a different element.
187
+ #
174
188
  # @param [ String ] field The field.
175
189
  #
176
190
  # @return [ true | false ] If this field is a conflict.
177
191
  def unset_conflict?(field)
178
192
  key = field.split('.', 2)[0]
179
- set_fields.has_key?(key)
193
+ return true if set_fields.has_key?(key)
194
+ return false if pull_fields.has_key?(key)
195
+
196
+ push_fields.has_key?(key) || add_to_set_fields.has_key?(key)
180
197
  end
181
198
 
182
199
  # Is the operation going to be a conflict for a $push?
@@ -190,9 +207,36 @@ module Mongoid
190
207
  def push_conflict?(field)
191
208
  name = field.split('.', 2)[0]
192
209
  set_fields.has_key?(name) || pull_fields.has_key?(name) ||
210
+ add_to_set_fields.has_key?(name) || unset_fields.has_key?(name) ||
193
211
  (push_fields.keys.count { |item| item.split('.', 2).first == name } > 1)
194
212
  end
195
213
 
214
+ # Is the operation going to be a conflict for an $addToSet?
215
+ #
216
+ # As with $push, an $addToSet is only split out when the other
217
+ # operation does not shift array element positions. A conflicting
218
+ # $pull is left as-is so MongoDB rejects the update rather than the
219
+ # $addToSet possibly hitting a different element in a separate write.
220
+ #
221
+ # @param [ String ] field The field.
222
+ #
223
+ # @return [ true | false ] If this field is a conflict.
224
+ def add_to_set_conflict?(field)
225
+ name = field.split('.', 2)[0]
226
+ set_fields.has_key?(name) || push_fields.has_key?(name)
227
+ end
228
+
229
+ # Is the operation going to be a conflict for a $pullAll?
230
+ #
231
+ # @param [ String ] field The field.
232
+ #
233
+ # @return [ true | false ] If this field is a conflict.
234
+ def pull_all_conflict?(field)
235
+ name = field.split('.', 2)[0]
236
+ set_fields.has_key?(name) || push_fields.has_key?(name) ||
237
+ add_to_set_fields.has_key?(name) || pull_root?(name)
238
+ end
239
+
196
240
  # Returns true if the $unset is made redundant by a $set that covers
197
241
  # the entire root-level field. When $set "children" is issued, the
198
242
  # current (live) state of the array already includes all pending changes
@@ -202,16 +246,26 @@ module Mongoid
202
246
  # @param [ String ] field the field
203
247
  def unset_superseded_by_set?(field)
204
248
  key = field.split('.', 2).first
205
- sets(initialize: false).has_key?(key)
249
+ sets(initialize: false)&.has_key?(key)
250
+ end
251
+
252
+ # Get the conflicting $addToSet operations.
253
+ #
254
+ # @example Get the conflicting add to sets.
255
+ # modifiers.conflicting_add_to_sets
256
+ #
257
+ # @return [ Hash ] The conflicting add to set operations.
258
+ def conflicting_add_to_sets
259
+ conflicts['$addToSet'] ||= {}
206
260
  end
207
261
 
208
- # Get the conflicting pull modifications.
262
+ # Get the conflicting pull all modifications.
209
263
  #
210
- # @example Get the conflicting pulls.
211
- # modifiers.conflicting_pulls
264
+ # @example Get the conflicting pull alls.
265
+ # modifiers.conflicting_pull_alls
212
266
  #
213
- # @return [ Hash ] The conflicting pull operations.
214
- def conflicting_pulls
267
+ # @return [ Hash ] The conflicting pull all operations.
268
+ def conflicting_pull_alls
215
269
  conflicts['$pullAll'] ||= {}
216
270
  end
217
271
 
@@ -282,6 +336,26 @@ module Mongoid
282
336
  @set_fields ||= {}
283
337
  end
284
338
 
339
+ # Get the names of the fields that need to be added to sets.
340
+ #
341
+ # @example Get the add to set fields.
342
+ # modifiers.add_to_set_fields
343
+ #
344
+ # @return [ Array<String> ] The add to set fields.
345
+ def add_to_set_fields
346
+ @add_to_set_fields ||= {}
347
+ end
348
+
349
+ # Get the names of the fields that need to be unset.
350
+ #
351
+ # @example Get the unset fields.
352
+ # modifiers.unset_fields
353
+ #
354
+ # @return [ Array<String> ] The unset fields.
355
+ def unset_fields
356
+ @unset_fields ||= {}
357
+ end
358
+
285
359
  # Get the $pullAll operations or initialize a new one.
286
360
  #
287
361
  # @example Get the $pullAll operations.
@@ -333,6 +407,16 @@ module Mongoid
333
407
  def unsets
334
408
  self['$unset'] ||= {}
335
409
  end
410
+
411
+ # Is there a $pull on the given root-level field? Does not initialize
412
+ # the $pull operations.
413
+ #
414
+ # @param [ String ] name The root-level field name.
415
+ #
416
+ # @return [ true | false ] If a $pull touches this field.
417
+ def pull_root?(name)
418
+ (self['$pull'] || {}).keys.any? { |key| key.split('.', 2)[0] == name }
419
+ end
336
420
  end
337
421
  end
338
422
  end
@@ -90,6 +90,8 @@ module Mongoid
90
90
  return nil unless opts[:auto_encryption_options]
91
91
 
92
92
  opts[:auto_encryption_options].dup.tap do |auto_encryption_options|
93
+ Mongoid::Warnings.warn_bypass_auto_encryption if auto_encryption_options[:bypass_auto_encryption]
94
+
93
95
  if auto_encryption_options.key?(:schema_map)
94
96
  default_logger.warn(
95
97
  'The :schema_map is configured in the :auto_encryption_options for the client;' +