mongoid 9.1.0 → 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 (143) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +69 -0
  3. data/lib/mongoid/association/accessors.rb +27 -5
  4. data/lib/mongoid/association/depending.rb +14 -10
  5. data/lib/mongoid/association/eager.rb +9 -0
  6. data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +88 -0
  7. data/lib/mongoid/association/eager_load/embedded_distributor.rb +151 -0
  8. data/lib/mongoid/association/eager_load/inclusion.rb +173 -0
  9. data/lib/mongoid/association/eager_load/inclusion_tree.rb +96 -0
  10. data/lib/mongoid/association/eager_load/lookup_pipeline.rb +121 -0
  11. data/lib/mongoid/association/eager_load/polymorphic_preloader.rb +61 -0
  12. data/lib/mongoid/association/eager_load/polymorphic_targets.rb +151 -0
  13. data/lib/mongoid/association/eager_loadable.rb +65 -101
  14. data/lib/mongoid/association/embedded/batchable.rb +17 -1
  15. data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -1
  16. data/lib/mongoid/association/embedded/embeds_one.rb +1 -0
  17. data/lib/mongoid/association/macros.rb +13 -1
  18. data/lib/mongoid/association/nested/many.rb +34 -5
  19. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  20. data/lib/mongoid/association/nested/one.rb +6 -1
  21. data/lib/mongoid/association/options.rb +17 -0
  22. data/lib/mongoid/association/referenced/belongs_to/eager.rb +6 -1
  23. data/lib/mongoid/association/referenced/belongs_to.rb +1 -0
  24. data/lib/mongoid/association/referenced/counter_cache.rb +3 -3
  25. data/lib/mongoid/association/referenced/has_and_belongs_to_many/buildable.rb +6 -0
  26. data/lib/mongoid/association/referenced/has_and_belongs_to_many.rb +5 -0
  27. data/lib/mongoid/association/referenced/has_many/enumerable.rb +3 -1
  28. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  29. data/lib/mongoid/association/referenced/has_many_through/eager.rb +99 -0
  30. data/lib/mongoid/association/referenced/has_many_through/proxy.rb +68 -0
  31. data/lib/mongoid/association/referenced/has_many_through.rb +201 -0
  32. data/lib/mongoid/association/referenced/has_one.rb +1 -0
  33. data/lib/mongoid/association/referenced/has_one_through/eager.rb +65 -0
  34. data/lib/mongoid/association/referenced/has_one_through/proxy.rb +30 -0
  35. data/lib/mongoid/association/referenced/has_one_through.rb +167 -0
  36. data/lib/mongoid/association/referenced.rb +2 -0
  37. data/lib/mongoid/association/relatable.rb +58 -0
  38. data/lib/mongoid/association.rb +15 -0
  39. data/lib/mongoid/atomic/modifiers.rb +100 -16
  40. data/lib/mongoid/clients/factory.rb +9 -13
  41. data/lib/mongoid/collection_configurable.rb +8 -0
  42. data/lib/mongoid/config/defaults.rb +0 -1
  43. data/lib/mongoid/config/encryption.rb +170 -22
  44. data/lib/mongoid/config.rb +56 -5
  45. data/lib/mongoid/contextual/aggregable/memory.rb +6 -2
  46. data/lib/mongoid/contextual/memory.rb +18 -7
  47. data/lib/mongoid/criteria/includable.rb +35 -11
  48. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  49. data/lib/mongoid/criteria/queryable/selectable.rb +123 -7
  50. data/lib/mongoid/deprecable.rb +45 -2
  51. data/lib/mongoid/document.rb +3 -0
  52. data/lib/mongoid/encryptable.rb +73 -0
  53. data/lib/mongoid/errors/config_redactor.rb +43 -0
  54. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  55. data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
  56. data/lib/mongoid/errors/mixed_client_configuration.rb +1 -1
  57. data/lib/mongoid/errors/no_client_database.rb +1 -1
  58. data/lib/mongoid/errors/no_client_hosts.rb +1 -1
  59. data/lib/mongoid/errors/no_encryption_schema.rb +30 -0
  60. data/lib/mongoid/errors/readonly_association.rb +29 -0
  61. data/lib/mongoid/errors.rb +5 -0
  62. data/lib/mongoid/extensions/vector.rb +64 -0
  63. data/lib/mongoid/extensions.rb +1 -0
  64. data/lib/mongoid/field_readable.rb +69 -0
  65. data/lib/mongoid/fields.rb +7 -0
  66. data/lib/mongoid/indexable.rb +38 -26
  67. data/lib/mongoid/matchable.rb +6 -1
  68. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +2 -4
  69. data/lib/mongoid/matcher/regex.rb +11 -12
  70. data/lib/mongoid/matcher/regexp_budget.rb +372 -0
  71. data/lib/mongoid/matcher.rb +1 -0
  72. data/lib/mongoid/persistable/creatable.rb +120 -14
  73. data/lib/mongoid/persistable/updatable.rb +9 -11
  74. data/lib/mongoid/persistable.rb +15 -15
  75. data/lib/mongoid/persistence_context.rb +77 -0
  76. data/lib/mongoid/search_indexable.rb +69 -24
  77. data/lib/mongoid/serializable.rb +12 -4
  78. data/lib/mongoid/tasks/database.rb +33 -16
  79. data/lib/mongoid/threaded.rb +36 -0
  80. data/lib/mongoid/timestamps/timeless.rb +84 -10
  81. data/lib/mongoid/touchable.rb +28 -1
  82. data/lib/mongoid/traversable.rb +2 -2
  83. data/lib/mongoid/validatable/associated.rb +18 -2
  84. data/lib/mongoid/version.rb +1 -1
  85. data/lib/mongoid/warnings.rb +4 -0
  86. data/spec/integration/app_spec.rb +7 -1
  87. data/spec/integration/associations/belongs_to_spec.rb +13 -0
  88. data/spec/integration/associations/embedded_delayed_atomic_sets_spec.rb +216 -0
  89. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +31 -2
  90. data/spec/integration/atomic/modifiers_spec.rb +283 -0
  91. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  92. data/spec/integration/encryption_spec.rb +300 -0
  93. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  94. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  95. data/spec/integration/query_operator_guard_spec.rb +89 -0
  96. data/spec/mongoid/association/accessors_spec.rb +35 -0
  97. data/spec/mongoid/association/eager_loadable_spec.rb +47 -0
  98. data/spec/mongoid/association/eager_spec.rb +2 -2
  99. data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +33 -0
  100. data/spec/mongoid/association/fallback_spec.rb +425 -0
  101. data/spec/mongoid/association/macros_spec.rb +26 -0
  102. data/spec/mongoid/association/referenced/belongs_to/eager_spec.rb +17 -0
  103. data/spec/mongoid/association/referenced/has_and_belongs_to_many/buildable_spec.rb +11 -0
  104. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  105. data/spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb +50 -0
  106. data/spec/mongoid/association/referenced/has_many/eager_spec.rb +33 -0
  107. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  108. data/spec/mongoid/association/referenced/has_many_through/eager_spec.rb +58 -0
  109. data/spec/mongoid/association/referenced/has_many_through/proxy_spec.rb +62 -0
  110. data/spec/mongoid/association/referenced/has_many_through_spec.rb +399 -0
  111. data/spec/mongoid/association/referenced/has_one_through/eager_spec.rb +58 -0
  112. data/spec/mongoid/association/referenced/has_one_through/proxy_spec.rb +49 -0
  113. data/spec/mongoid/association/referenced/has_one_through_spec.rb +211 -0
  114. data/spec/mongoid/atomic/modifiers_spec.rb +347 -0
  115. data/spec/mongoid/attributes/nested_spec.rb +278 -8
  116. data/spec/mongoid/clients/factory_spec.rb +36 -0
  117. data/spec/mongoid/config/defaults_spec.rb +33 -0
  118. data/spec/mongoid/config/encryption_spec.rb +451 -0
  119. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  120. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  121. data/spec/mongoid/criteria/includable_spec.rb +1221 -0
  122. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +15 -1
  123. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +201 -0
  124. data/spec/mongoid/criteria_spec.rb +3 -0
  125. data/spec/mongoid/encryptable_spec.rb +115 -0
  126. data/spec/mongoid/errors/config_redactor_spec.rb +112 -0
  127. data/spec/mongoid/errors/mixed_client_configuration_spec.rb +31 -0
  128. data/spec/mongoid/errors/no_client_database_spec.rb +29 -0
  129. data/spec/mongoid/errors/no_client_hosts_spec.rb +26 -0
  130. data/spec/mongoid/errors/readonly_association_spec.rb +34 -0
  131. data/spec/mongoid/extensions/vector_spec.rb +175 -0
  132. data/spec/mongoid/fields_spec.rb +8 -0
  133. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  134. data/spec/mongoid/search_indexable_spec.rb +321 -9
  135. data/spec/mongoid/tasks/database_spec.rb +31 -0
  136. data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
  137. data/spec/mongoid/touchable_spec.rb +242 -0
  138. data/spec/mongoid/touchable_spec_models.rb +47 -0
  139. data/spec/mongoid/validatable/associated_spec.rb +19 -0
  140. data/spec/spec_helper.rb +5 -1
  141. data/spec/support/crypt/models.rb +253 -0
  142. data/spec/support/models/modifier_models.rb +65 -0
  143. metadata +55 -2
@@ -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;' +
@@ -111,10 +113,6 @@ module Mongoid
111
113
  version: VERSION
112
114
  }.freeze
113
115
 
114
- def driver_version
115
- Mongo::VERSION.split('.')[0...2].map(&:to_i)
116
- end
117
-
118
116
  # Prepare options for Mongo::Client based on Mongoid client configuration.
119
117
  #
120
118
  # @param [ Hash ] opts Parameters from options section of Mongoid client configuration.
@@ -125,16 +123,14 @@ module Mongoid
125
123
  options = opts.dup
126
124
  options[:platform] = PLATFORM_DETAILS
127
125
  options[:app_name] = Mongoid::Config.app_name if Mongoid::Config.app_name
128
- if (driver_version <=> [ 2, 13 ]) >= 0
129
- wrap_lib = if options[:wrapping_libraries]
130
- [ MONGOID_WRAPPING_LIBRARY ] + options[:wrapping_libraries]
131
- else
132
- [ MONGOID_WRAPPING_LIBRARY ]
133
- end.tap do |wrap|
134
- wrap << { name: 'Rails', version: ::Rails.version } if defined?(::Rails) && ::Rails.respond_to?(:version)
135
- end
136
- options[:wrapping_libraries] = wrap_lib
126
+ wrap_lib = if options[:wrapping_libraries]
127
+ [ MONGOID_WRAPPING_LIBRARY ] + options[:wrapping_libraries]
128
+ else
129
+ [ MONGOID_WRAPPING_LIBRARY ]
130
+ end.tap do |wrap|
131
+ wrap << { name: 'Rails', version: ::Rails.version } if defined?(::Rails) && ::Rails.respond_to?(:version)
137
132
  end
133
+ options[:wrapping_libraries] = wrap_lib
138
134
  options.reject { |k, _v| k == :hosts }.to_hash.symbolize_keys!
139
135
  end
140
136
  end
@@ -20,6 +20,14 @@ module Mongoid
20
20
  # @raise [ Errors::CreateCollectionFailure ] If collection creation failed.
21
21
  # @raise [ Errors::DropCollectionFailure ] If an attempt to drop collection failed.
22
22
  def create_collection(force: false)
23
+ Threaded.with_collection_management do
24
+ perform_create_collection(force: force)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def perform_create_collection(force:)
23
31
  if collection_name.empty?
24
32
  # This is most probably an anonymous class, we ignore them.
25
33
  return
@@ -30,7 +30,6 @@ module Mongoid
30
30
  load_defaults '9.0'
31
31
 
32
32
  when '9.0'
33
- self.allow_reparenting_via_nested_attributes = true
34
33
  self.autosave_saves_unchanged_documents = true
35
34
 
36
35
  load_defaults '9.1'
@@ -20,21 +20,51 @@ module Mongoid
20
20
  #
21
21
  # @return [ Hash ] The encryption schema map.
22
22
  def encryption_schema_map(default_database, models = ::Mongoid.models)
23
- visited = Set.new
24
- models.each_with_object({}) do |model, map|
25
- next if visited.include?(model)
23
+ # Subclasses share their parent's collection but are not registered in
24
+ # Mongoid.models, so they have to be added to the walk explicitly.
25
+ classes = models.flat_map { |model| [ model ] + model.descendants }.uniq
26
26
 
27
- visited << model
27
+ classes.each_with_object({}) do |model, map|
28
28
  next if model.embedded?
29
- next unless model.encrypted?
29
+ next unless model.requires_encryption_schema?
30
30
 
31
31
  database = model.storage_options.fetch(:database) { default_database }
32
+ # A callable database name cannot be resolved here: the documented
33
+ # multi-tenant idiom has no correct value while the client is being
34
+ # built. Interpolating the callable would produce a key that never
35
+ # matches any namespace, so leave the model out of the map. Writes
36
+ # are refused later, by PersistenceContext, rather than silently
37
+ # going out unencrypted.
38
+ next if database.respond_to?(:call)
39
+
32
40
  key = "#{database}.#{model.collection_name}"
33
- props = metadata_for(model).merge(properties_for(model, visited))
34
- map[key] = props unless props.empty?
41
+ props = metadata_for(model).merge(properties_for(model, [ model ], key))
42
+ next if props.empty?
43
+
44
+ # The root of a collection schema describes the document, so it is
45
+ # always an object. Saying so matters when a nested schema carries
46
+ # encryptMetadata: mongocryptd rejects the schema otherwise.
47
+ entry = { 'bsonType' => 'object' }.merge(props)
48
+ map[key] = map.key?(key) ? merge_schema_entries(map[key], entry, key) : entry
35
49
  end
36
50
  end
37
51
 
52
+ # Collect the paths of the fields encrypted by the schema generated for
53
+ # a single model.
54
+ #
55
+ # @param [ Mongoid::Document ] model The model.
56
+ #
57
+ # @return [ Array<Array<String>> ] The property paths, outermost first.
58
+ #
59
+ # @api private
60
+ def encrypted_paths_for(model)
61
+ return model.instance_variable_get(:@encrypted_paths_for) if
62
+ model.instance_variable_defined?(:@encrypted_paths_for)
63
+
64
+ properties = properties_for(model, [ model ])['properties'] || {}
65
+ model.instance_variable_set(:@encrypted_paths_for, collect_encrypted_paths(properties, []))
66
+ end
67
+
38
68
  private
39
69
 
40
70
  # The algorithm to use for the deterministic encryption.
@@ -100,11 +130,14 @@ module Mongoid
100
130
  # are marked as encrypted.
101
131
  #
102
132
  # @param [ Mongoid::Document ] model The model to generate the properties for.
103
- # @param [ Set<Mongoid::Document> ] visited The set of models that have already been visited.
133
+ # @param [ Array<Mongoid::Document> ] path The models the walk is already
134
+ # inside of, outermost first.
135
+ # @param [ String | nil ] namespace The namespace the schema is built
136
+ # for, used for error reporting.
104
137
  #
105
138
  # @return [ Hash ] The encryption properties.
106
- def properties_for(model, visited)
107
- result = properties_for_fields(model).merge(properties_for_relations(model, visited))
139
+ def properties_for(model, path, namespace = nil)
140
+ result = properties_for_fields(model).merge(properties_for_relations(model, path, namespace))
108
141
  if result.empty?
109
142
  {}
110
143
  else
@@ -141,26 +174,141 @@ module Mongoid
141
174
  # are configured to be encrypted.
142
175
  #
143
176
  # @param [ Mongoid::Document ] model The model to generate the properties for.
144
- # @param [ Set<Mongoid::Document> ] visited The set of models that have already been visited.
177
+ # @param [ Array<Mongoid::Document> ] path The models the walk is already
178
+ # inside of, outermost first.
179
+ # @param [ String | nil ] namespace The namespace the schema is built
180
+ # for, used for error reporting.
145
181
  #
146
182
  # @return [ Hash ] The encryption properties.
147
- def properties_for_relations(model, visited)
183
+ def properties_for_relations(model, path, namespace = nil)
148
184
  model.relations.each_with_object({}) do |(name, relation), props|
149
- next if visited.include?(relation.relation_class)
185
+ # relation_class constantizes, and a polymorphic embedded_in has no
186
+ # class to resolve, so the relation type has to be checked first.
150
187
  next unless relation.is_a?(Association::Embedded::EmbedsOne)
151
- next unless relation.relation_class.encrypted?
152
-
153
- visited << relation.relation_class
154
- metadata_for(
155
- relation.relation_class
156
- ).merge(
157
- properties_for(relation.relation_class, visited)
158
- ).tap do |properties|
159
- props[name] = { 'bsonType' => 'object' }.merge(properties) unless properties.empty?
188
+
189
+ klass = relation.try_relation_class
190
+ # An association target does not have to be a Mongoid document, and
191
+ # the class it names does not have to exist.
192
+ next unless klass.respond_to?(:requires_encryption_schema?)
193
+
194
+ # Any subclass of the declared class can be assigned to the
195
+ # association, so its encrypted fields have to be mapped here too.
196
+ properties = ([ klass ] + klass.descendants).uniq.inject({}) do |merged, candidate|
197
+ # Stop at a model the walk is already inside of, or a self-embedding
198
+ # model never terminates. The path covers the current branch only:
199
+ # a model embedded by two parents, or twice by one parent, has to be
200
+ # emitted at every place it appears.
201
+ next merged if path.include?(candidate)
202
+ next merged unless candidate.respond_to?(:requires_encryption_schema?)
203
+ next merged unless candidate.requires_encryption_schema?
204
+
205
+ merge_schema_entries(
206
+ merged,
207
+ metadata_for(candidate).merge(
208
+ properties_for(candidate, path + [ candidate ], namespace)
209
+ ),
210
+ namespace
211
+ )
212
+ end
213
+ props[name] = { 'bsonType' => 'object' }.merge(properties) unless properties.empty?
214
+ end
215
+ end
216
+
217
+ # Walk the schema properties and collect the path of every encrypted
218
+ # leaf.
219
+ #
220
+ # @param [ Hash ] properties The properties to walk.
221
+ # @param [ Array<String> ] path The properties the walk is already
222
+ # inside of.
223
+ #
224
+ # @return [ Array<Array<String>> ] The property paths.
225
+ def collect_encrypted_paths(properties, path)
226
+ properties.each_with_object([]) do |(name, schema), paths|
227
+ if schema.key?('encrypt')
228
+ paths << (path + [ name ])
229
+ elsif schema['properties']
230
+ paths.concat(collect_encrypted_paths(schema['properties'], path + [ name ]))
231
+ end
232
+ end
233
+ end
234
+
235
+ # Merge two schema objects covering the same model or namespace.
236
+ #
237
+ # Every model writing to one namespace contributes an entry to the
238
+ # schema map. Those entries have to agree on everything they have in
239
+ # common, or the schema would silently encrypt a field differently for
240
+ # one model than for another.
241
+ #
242
+ # @param [ Hash ] left The first schema.
243
+ # @param [ Hash ] right The second schema.
244
+ # @param [ String | nil ] namespace The namespace the schema belongs to.
245
+ #
246
+ # @return [ Hash ] The merged schema.
247
+ def merge_schema_entries(left, right, namespace = nil)
248
+ left.merge(right) do |key, left_value, right_value|
249
+ case key
250
+ when 'bsonType'
251
+ left_value
252
+ when 'encryptMetadata'
253
+ merge_encrypt_metadata(left_value, right_value, namespace)
254
+ when 'properties'
255
+ merge_properties(left_value, right_value, namespace)
256
+ else
257
+ raise_invalid_encryption_schema(namespace, key)
160
258
  end
161
259
  end
162
260
  end
163
261
 
262
+ # Merge two encryptMetadata objects.
263
+ #
264
+ # @param [ Hash ] left The first encryptMetadata object.
265
+ # @param [ Hash ] right The second encryptMetadata object.
266
+ # @param [ String | nil ] namespace The namespace the schema belongs to.
267
+ #
268
+ # @return [ Hash ] The merged encryptMetadata object.
269
+ def merge_encrypt_metadata(left, right, namespace)
270
+ left.merge(right) do |key, left_value, right_value|
271
+ unless left_value == right_value
272
+ raise_invalid_encryption_schema(namespace, "encryptMetadata.#{key}")
273
+ end
274
+
275
+ left_value
276
+ end
277
+ end
278
+
279
+ # Merge two properties objects.
280
+ #
281
+ # @param [ Hash ] left The first properties object.
282
+ # @param [ Hash ] right The second properties object.
283
+ # @param [ String | nil ] namespace The namespace the schema belongs to.
284
+ #
285
+ # @return [ Hash ] The merged properties object.
286
+ def merge_properties(left, right, namespace)
287
+ left.merge(right) do |name, left_value, right_value|
288
+ if left_value.key?('encrypt') && right_value.key?('encrypt')
289
+ unless left_value['encrypt'] == right_value['encrypt']
290
+ raise_invalid_encryption_schema(namespace, "properties.#{name}")
291
+ end
292
+
293
+ left_value
294
+ elsif left_value.key?('encrypt') || right_value.key?('encrypt')
295
+ raise_invalid_encryption_schema(namespace, "properties.#{name}")
296
+ else
297
+ merge_schema_entries(left_value, right_value, namespace)
298
+ end
299
+ end
300
+ end
301
+
302
+ # Raise an error for a schema conflicting with another one.
303
+ #
304
+ # @param [ String | nil ] namespace The namespace the schema belongs to.
305
+ # @param [ String ] path The conflicting path in the schema.
306
+ #
307
+ # @raise [ Errors::InvalidEncryptionSchema ] Always raised.
308
+ def raise_invalid_encryption_schema(namespace, path)
309
+ raise Errors::InvalidEncryptionSchema.new(namespace, path)
310
+ end
311
+
164
312
  # Get the BSON type identifier for the provided field according to the
165
313
  # https://www.mongodb.com/docs/manual/reference/bson-types/#std-label-bson-types
166
314
  #
@@ -130,12 +130,14 @@ module Mongoid
130
130
  validate_isolation_level!(level)
131
131
  }
132
132
 
133
- # When this flag is true, it will be possible to change the parent of a
134
- # record in a "has_many" association by passing the child record's id in the
135
- # nested attributes for another parent record.
133
+ # When this flag is true, it will be possible to add a record to a
134
+ # "has_many" or "has_and_belongs_to_many" association by passing that
135
+ # record's id in the nested attributes for another parent record, even
136
+ # when the record does not already belong to that association. For a
137
+ # "has_many" association this moves the record to the new parent.
136
138
  #
137
- # When this flag is false, attempting to change the parent of a record in a
138
- # "has-many" association via nested attributes will raise an error.
139
+ # When this flag is false, an id in nested attributes is only resolved
140
+ # within the association itself, and anything else raises an error.
139
141
  #
140
142
  # The default is `false`. Note that allowing reparenting via nested attributes
141
143
  # is a potential security risk, since it could allow a malicious user to move
@@ -294,6 +296,51 @@ module Mongoid
294
296
  # See https://jira.mongodb.org/browse/MONGOID-5892 for more details.
295
297
  option :serializable_hash_with_legacy_only, default: true
296
298
 
299
+ # When false (default), query operators are restricted when building a
300
+ # selector, so that user-supplied input reaching the query builder cannot
301
+ # make MongoDB execute arbitrary JavaScript. Two rules are enforced:
302
+ #
303
+ # - An operator at the top level of an expression must appear in
304
+ # +Criteria::Queryable::Selectable::ALLOWED_QUERY_OPERATORS+.
305
+ # - +$where+, +$function+, and +$accumulator+ are rejected at any depth,
306
+ # including inside +$expr+ and the logical operators.
307
+ #
308
+ # This applies to every query method that accepts an expression, including
309
+ # +where+, +find_by+, +and+, +or+, +nor+, +not+, +any_of+, and +none_of+.
310
+ # It also governs the string form of +where+, e.g.
311
+ # +where("this.name == 'admin'")+, which the server evaluates as +$where+.
312
+ # Applications relying on that form must set this option to true.
313
+ #
314
+ # The APIs that request JavaScript explicitly, +Criteria#for_js+ and
315
+ # +js_query+, are unaffected: there the developer has asked for it.
316
+ #
317
+ # Set to true to restore the unrestricted pass-through behavior.
318
+ #
319
+ # Note that this option is deliberately not tied to +load_defaults+: an
320
+ # application that has opted into older defaults still gets the guard, and
321
+ # must set this option explicitly to turn it off.
322
+ #
323
+ # See https://jira.mongodb.org/browse/MONGOID-5939,
324
+ # https://jira.mongodb.org/browse/MONGOID-5993,
325
+ # https://jira.mongodb.org/browse/MONGOID-5994 for details.
326
+ option :allow_unsafe_query_operators, default: false
327
+
328
+ # The maximum number of seconds that evaluating a single query in memory
329
+ # may spend executing regular expressions. Queries against an embedded
330
+ # association are evaluated in the calling thread, so a pattern built from
331
+ # user input runs locally and can otherwise consume unbounded CPU. The
332
+ # limit is cumulative over the whole query, since cost grows with the
333
+ # number of documents and conditions as well as with the pattern.
334
+ #
335
+ # Set to nil to remove the limit. On Ruby 3.2 and later the remaining
336
+ # budget is compiled into the pattern, so the limit counts only the time
337
+ # spent matching. Earlier Rubies have no per-Regexp timeout, so the query
338
+ # is bounded with Timeout instead and the limit is wall clock over the
339
+ # whole in-memory evaluation.
340
+ #
341
+ # See https://jira.mongodb.org/browse/MONGOID-5981 for details.
342
+ option :in_memory_regexp_time_limit, default: 5.0
343
+
297
344
  # Returns the Config singleton, for use in the configure DSL.
298
345
  #
299
346
  # @return [ self ] The Config singleton.
@@ -539,6 +586,10 @@ module Mongoid
539
586
  end
540
587
  end
541
588
 
589
+ # Wraps configuration options that have been deprecated so that assigning
590
+ # them emits a deprecation warning. OPTIONS is intentionally empty when no
591
+ # options are currently deprecated; it is populated as options are retired,
592
+ # at which point this module rewrites their setters to warn.
542
593
  module DeprecatedOptions
543
594
  OPTIONS = %i[]
544
595
 
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'mongoid/field_readable'
4
+
3
5
  module Mongoid
4
6
  module Contextual
5
7
  module Aggregable
6
8
  # Contains behavior for aggregating values in memory.
7
9
  module Memory
10
+ include FieldReadable
11
+
8
12
  # Get all the aggregate values for the provided field.
9
13
  # Provided for interface consistency with Aggregable::Mongo.
10
14
  #
@@ -28,7 +32,7 @@ module Mongoid
28
32
  #
29
33
  # @return [ Numeric ] The average.
30
34
  def avg(field)
31
- total = count { |doc| !doc.send(field).nil? }
35
+ total = count { |doc| !read_field_value(doc, field).nil? }
32
36
  return nil unless total > 0
33
37
 
34
38
  total = total.to_f if total.is_a?(Integer)
@@ -114,7 +118,7 @@ module Mongoid
114
118
  def aggregate_by(field, method)
115
119
  return nil unless any?
116
120
 
117
- map { |doc| doc.public_send(field) }.compact.public_send(method)
121
+ map { |doc| read_field_value(doc, field) }.compact.public_send(method)
118
122
  end
119
123
  end
120
124
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'mongoid/contextual/aggregable/memory'
4
4
  require 'mongoid/association/eager_loadable'
5
+ require 'mongoid/field_readable'
5
6
 
6
7
  module Mongoid
7
8
  module Contextual
@@ -15,6 +16,7 @@ module Mongoid
15
16
  include Association::EagerLoadable
16
17
  include Queryable
17
18
  include Positional
19
+ include FieldReadable
18
20
 
19
21
  # @attribute [r] root The root document.
20
22
  # @attribute [r] path The atomic path.
@@ -174,10 +176,20 @@ module Mongoid
174
176
  # @param [ Criteria ] criteria The criteria.
175
177
  def initialize(criteria)
176
178
  @criteria, @klass = criteria, criteria.klass
177
- @documents = criteria.documents.select do |doc|
178
- @root ||= doc._root
179
- @collection ||= root.collection
180
- doc._matches?(criteria.selector)
179
+
180
+ # Resolving the collection can build a Mongo::Client the first time it
181
+ # runs, so it happens before the budget opens. Where the budget is
182
+ # enforced with Timeout the exception is asynchronous, and landing in
183
+ # the middle of that would leave a half-built client behind.
184
+ if (first = criteria.documents.first)
185
+ @root = first._root
186
+ @collection = @root.collection
187
+ end
188
+
189
+ # One regexp budget covers the whole scan, so that the limit bounds the
190
+ # query rather than each document individually.
191
+ @documents = Matcher::RegexpBudget.open(criteria.selector) do
192
+ criteria.documents.select { |doc| doc._matches?(criteria.selector) }
181
193
  end
182
194
  apply_sorting
183
195
  apply_options
@@ -739,10 +751,9 @@ module Mongoid
739
751
  # If this is a localized field, and there are remaining, get the
740
752
  # _translations hash so that we can get the specified translation in
741
753
  # the remaining
742
- document.send("#{segment}_translations") if field&.localized?
754
+ document.public_send("#{segment}_translations") if field&.localized?
743
755
  end
744
- meth = klass.aliased_associations[segment] || segment
745
- res.nil? ? document.try(meth) : res
756
+ res.nil? ? read_field_value(document, segment) : res
746
757
  elsif document.is_a?(Hash)
747
758
  # TODO: Remove the indifferent access when implementing MONGOID-5410.
748
759
  if document.key?(segment.to_s)