mongoid 7.3.2 → 7.3.5

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 (64) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/lib/config/locales/en.yml +13 -0
  4. data/lib/mongoid/association/embedded/batchable.rb +20 -1
  5. data/lib/mongoid/association/referenced/has_many/enumerable.rb +3 -7
  6. data/lib/mongoid/association/referenced/has_many/proxy.rb +2 -2
  7. data/lib/mongoid/association/relatable.rb +2 -0
  8. data/lib/mongoid/atomic/paths/embedded/many.rb +19 -0
  9. data/lib/mongoid/atomic.rb +26 -2
  10. data/lib/mongoid/config/environment.rb +9 -1
  11. data/lib/mongoid/contextual/atomic.rb +7 -2
  12. data/lib/mongoid/contextual/none.rb +3 -0
  13. data/lib/mongoid/criteria/queryable/selectable.rb +2 -2
  14. data/lib/mongoid/criteria/queryable/storable.rb +5 -5
  15. data/lib/mongoid/document.rb +3 -2
  16. data/lib/mongoid/errors/empty_config_file.rb +26 -0
  17. data/lib/mongoid/errors/invalid_config_file.rb +26 -0
  18. data/lib/mongoid/errors.rb +2 -0
  19. data/lib/mongoid/persistable/upsertable.rb +1 -1
  20. data/lib/mongoid/persistence_context.rb +3 -1
  21. data/lib/mongoid/query_cache.rb +11 -1
  22. data/lib/mongoid/tasks/database.rb +1 -1
  23. data/lib/mongoid/touchable.rb +10 -0
  24. data/lib/mongoid/version.rb +1 -1
  25. data/lib/rails/generators/mongoid/config/templates/mongoid.yml +7 -2
  26. data/spec/integration/associations/embeds_many_spec.rb +139 -0
  27. data/spec/integration/contextual/empty_spec.rb +142 -0
  28. data/spec/integration/stringified_symbol_field_spec.rb +2 -2
  29. data/spec/lite_spec_helper.rb +8 -1
  30. data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +21 -0
  31. data/spec/mongoid/association/embedded/embeds_many_models.rb +137 -0
  32. data/spec/mongoid/association/referenced/belongs_to_query_spec.rb +20 -0
  33. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +8 -0
  34. data/spec/mongoid/association/referenced/has_many/enumerable_spec.rb +244 -92
  35. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +30 -14
  36. data/spec/mongoid/association/referenced/has_many_models.rb +17 -0
  37. data/spec/mongoid/clients/factory_spec.rb +9 -3
  38. data/spec/mongoid/clients/options_spec.rb +11 -5
  39. data/spec/mongoid/config/environment_spec.rb +86 -8
  40. data/spec/mongoid/config_spec.rb +89 -16
  41. data/spec/mongoid/contextual/atomic_spec.rb +64 -25
  42. data/spec/mongoid/contextual/geo_near_spec.rb +1 -1
  43. data/spec/mongoid/copyable_spec.rb +1 -1
  44. data/spec/mongoid/criteria_spec.rb +32 -0
  45. data/spec/mongoid/document_spec.rb +21 -1
  46. data/spec/mongoid/errors/invalid_config_file_spec.rb +32 -0
  47. data/spec/mongoid/persistable/updatable_spec.rb +2 -0
  48. data/spec/mongoid/query_cache_spec.rb +26 -2
  49. data/spec/mongoid/scopable_spec.rb +11 -0
  50. data/spec/mongoid/touchable_spec.rb +18 -0
  51. data/spec/mongoid/touchable_spec_models.rb +2 -0
  52. data/spec/shared/lib/mrss/cluster_config.rb +6 -1
  53. data/spec/shared/lib/mrss/constraints.rb +21 -4
  54. data/spec/shared/lib/mrss/event_subscriber.rb +200 -0
  55. data/spec/shared/lib/mrss/server_version_registry.rb +17 -12
  56. data/spec/shared/lib/mrss/session_registry.rb +69 -0
  57. data/spec/shared/lib/mrss/session_registry_legacy.rb +60 -0
  58. data/spec/shared/share/Dockerfile.erb +8 -7
  59. data/spec/shared/shlib/server.sh +72 -22
  60. data/spec/support/models/audible_sound.rb +3 -0
  61. data.tar.gz.sig +0 -0
  62. metadata +627 -608
  63. metadata.gz.sig +0 -0
  64. data/spec/support/session_registry.rb +0 -50
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27ef7fb0ba8610ab6cd4cf814a5a47361fe909247a85535c807d90dbf70c1563
4
- data.tar.gz: fe110c6f5439f985325f3df374bfc87fa5c9133eb97cac501dd9166b8d45cf60
3
+ metadata.gz: efce0ba93d948e87b85aa1a6e1a0768ecb2bd17a052d5e34afb426c357391492
4
+ data.tar.gz: 39784e3b229e457971b79962c26e22f3f1e721c68d72268bdafafa821c245c80
5
5
  SHA512:
6
- metadata.gz: 3f963e5e7fb3a7705d36ce9a6d24fb09106ca2ccca52d1a2cf24ddbf9a98773206d315374d4ecc40ad6a208be75ad0de5e44a6d43285faa3304ec641d91de3fa
7
- data.tar.gz: 167e2188b8801cb0a4eb15ec98727e3f331eab9f0588923f80f5f3cf2e29eec8b9cf6a01ace242d6412ea578a9fa7e4de309278a309c0d84b5cc07091f74828f
6
+ metadata.gz: f690e5ca59dfdf1e658ac922c5e8c0e0de1bef099f005205cab26b4e27922ce82b64dae38e803673b32e73f9327746562548c3f9667a4d3fd275483729e6ff3d
7
+ data.tar.gz: b87aef76b5145507b5e48f8516f4b27b99f401d7afb2fa56441d1d47540eac6f3f7bf198fadfd995e9fc08b0563727b6a5c4f7ce5014456e0dd4a6d345cdf3bb
checksums.yaml.gz.sig CHANGED
Binary file
@@ -80,6 +80,12 @@ en:
80
80
  different collections so a simple id lookup is not sufficient enough."
81
81
  resolution: "Don't attempt to perform this action and have patience,
82
82
  maybe this will be supported in the future."
83
+ empty_config_file:
84
+ message: "Empty configuration file: %{path}."
85
+ summary: "Your mongoid.yml configuration file appears to be empty."
86
+ resolution: "Ensure your configuration file contains the correct contents.
87
+ Please consult the following page with respect to Mongoid's configuration:
88
+ https://docs.mongodb.com/mongoid/current/reference/configuration/"
83
89
  invalid_collection:
84
90
  message: "Access to the collection for %{klass} is not allowed."
85
91
  summary: "%{klass}.collection was called, and %{klass} is an embedded
@@ -94,6 +100,13 @@ en:
94
100
  A collation option is only supported if the query is executed on a MongoDB server
95
101
  with version >= 3.4."
96
102
  resolution: "Remove the collation option from the query."
103
+ invalid_config_file:
104
+ message: "Invalid configuration file: %{path}."
105
+ summary: "Your mongoid.yml configuration file does not contain the
106
+ correct file structure."
107
+ resolution: "Ensure your configuration file contains the correct contents.
108
+ Please consult the following page with respect to Mongoid's configuration:
109
+ https://docs.mongodb.com/mongoid/current/reference/configuration/"
97
110
  invalid_config_option:
98
111
  message: "Invalid configuration option: %{name}."
99
112
  summary: "A invalid configuration option was provided in your
@@ -82,6 +82,8 @@ module Mongoid
82
82
  def batch_replace(docs)
83
83
  if docs.blank?
84
84
  if _assigning? && !empty?
85
+ _base.delayed_atomic_sets.delete(path)
86
+ clear_atomic_path_cache
85
87
  _base.add_atomic_unset(first)
86
88
  target_duplicate = _target.dup
87
89
  pre_process_batch_remove(target_duplicate, :delete)
@@ -93,6 +95,8 @@ module Mongoid
93
95
  _base.delayed_atomic_sets.clear unless _assigning?
94
96
  docs = normalize_docs(docs).compact
95
97
  _target.clear and _unscoped.clear
98
+ _base.delayed_atomic_unsets.delete(path)
99
+ clear_atomic_path_cache
96
100
  inserts = execute_batch_set(docs)
97
101
  add_atomic_sets(inserts)
98
102
  end
@@ -250,7 +254,22 @@ module Mongoid
250
254
  #
251
255
  # @since 3.0.0
252
256
  def path
253
- @path ||= _unscoped.first.atomic_path
257
+ @path ||= if _unscoped.empty?
258
+ Mongoid::Atomic::Paths::Embedded::Many.position_without_document(_base, _association)
259
+ else
260
+ _unscoped.first.atomic_path
261
+ end
262
+ end
263
+
264
+ # Clear the cache for path and atomic_paths. This method is used when
265
+ # the path method is used, and the association has not been set on the
266
+ # document yet, which can cause path and atomic_paths to be calculated
267
+ # incorrectly later.
268
+ #
269
+ # @api private
270
+ def clear_atomic_path_cache
271
+ self.path = nil
272
+ _base.instance_variable_set("@atomic_paths", nil)
254
273
  end
255
274
 
256
275
  # Set the atomic path.
@@ -209,9 +209,9 @@ module Mongoid
209
209
  # @since 2.1.0
210
210
  def empty?
211
211
  if _loaded?
212
- in_memory.count == 0
212
+ in_memory.empty?
213
213
  else
214
- _unloaded.count + _added.count == 0
214
+ _added.empty? && !_unloaded.exists?
215
215
  end
216
216
  end
217
217
 
@@ -242,11 +242,7 @@ module Mongoid
242
242
  def any?(*args)
243
243
  return super if args.any? || block_given?
244
244
 
245
- if _loaded?
246
- in_memory.length > 0
247
- else
248
- _unloaded.exists? || _added.length > 0
249
- end
245
+ !empty?
250
246
  end
251
247
 
252
248
  # Get the first document in the enumerable. Will check the persisted
@@ -506,8 +506,8 @@ module Mongoid
506
506
  selector = conditions || {}
507
507
  removed = klass.send(method, selector.merge!(criteria.selector))
508
508
  _target.delete_if do |doc|
509
- if doc._matches?(selector)
510
- unbind_one(doc) and true
509
+ doc._matches?(selector).tap do |b|
510
+ unbind_one(doc) if b
511
511
  end
512
512
  end
513
513
  removed
@@ -122,6 +122,8 @@ module Mongoid
122
122
  # @since 7.0
123
123
  def inverses(other = nil)
124
124
  return [ inverse_of ] if inverse_of
125
+ return [] if @options.key?(:inverse_of) && !inverse_of
126
+
125
127
  if polymorphic?
126
128
  polymorphic_inverses(other)
127
129
  else
@@ -39,6 +39,25 @@ module Mongoid
39
39
  locator = document.new_record? ? "" : ".#{document._index}"
40
40
  "#{pos}#{"." unless pos.blank?}#{document._association.store_as}#{locator}"
41
41
  end
42
+
43
+ class << self
44
+
45
+ # Get the position of where the document would go for the given
46
+ # association. The use case for this function is when trying to
47
+ # persist an empty list for an embedded association. All of the
48
+ # existing functions for getting the position to store a document
49
+ # require passing in a document to store, which we don't have when
50
+ # trying to store the empty list.
51
+ #
52
+ # @param [ Document ] parent The parent document to store in.
53
+ # @param [ Association ] association The association.
54
+ #
55
+ # @return [ String ] The position string.
56
+ def position_without_document(parent, association)
57
+ pos = parent.atomic_position
58
+ "#{pos}#{"." unless pos.blank?}#{association.store_as}"
59
+ end
60
+ end
42
61
  end
43
62
  end
44
63
  end
@@ -385,11 +385,35 @@ module Mongoid
385
385
  updates = atomic_updates
386
386
  return {} unless atomic_updates.key?("$set")
387
387
  touches = {}
388
+ wanted_keys = %w(updated_at u_at)
389
+ # TODO this permits field to be passed as an empty string in which case
390
+ # it is ignored, get rid of this behavior.
391
+ if field.present?
392
+ wanted_keys << field.to_s
393
+ end
388
394
  updates["$set"].each_pair do |key, value|
389
- key_regex = /updated_at|u_at#{"|" + field if field.present?}/
390
- touches.merge!({ key => value }) if key =~ key_regex
395
+ if wanted_keys.include?(key.split('.').last)
396
+ touches.update(key => value)
397
+ end
391
398
  end
392
399
  { "$set" => touches }
393
400
  end
401
+
402
+ # Returns the $set atomic updates affecting the specified field.
403
+ #
404
+ # @param [ String ] field The field name.
405
+ #
406
+ # @api private
407
+ def set_field_atomic_updates(field)
408
+ updates = atomic_updates
409
+ return {} unless atomic_updates.key?("$set")
410
+ sets = {}
411
+ updates["$set"].each_pair do |key, value|
412
+ if key.split('.').last == field
413
+ sets.update(key => value)
414
+ end
415
+ end
416
+ { "$set" => sets }
417
+ end
394
418
  end
395
419
  end
@@ -52,7 +52,15 @@ module Mongoid
52
52
  # @api private
53
53
  def load_yaml(path, environment = nil)
54
54
  env = environment ? environment.to_s : env_name
55
- YAML.load(ERB.new(File.new(path).read).result)[env]
55
+ contents = File.new(path).read
56
+ if contents.empty?
57
+ raise Mongoid::Errors::EmptyConfigFile.new(path)
58
+ end
59
+ data = YAML.load(ERB.new(contents).result)
60
+ unless data.is_a?(Hash)
61
+ raise Mongoid::Errors::InvalidConfigFile.new(path)
62
+ end
63
+ data[env]
56
64
  end
57
65
  end
58
66
  end
@@ -173,13 +173,18 @@ module Mongoid
173
173
  # @example Unset the field on the matches.
174
174
  # context.unset(:name)
175
175
  #
176
- # @param [ String, Symbol, Array ] args The name of the fields.
176
+ # @param [ String | Symbol | Array<String|Symbol> | Hash ] args
177
+ # The name(s) of the field(s) to unset.
178
+ # If a Hash is specified, its keys will be used irrespective of what
179
+ # each key's value is, even if the value is nil or false.
177
180
  #
178
181
  # @return [ nil ] Nil.
179
182
  #
180
183
  # @since 3.0.0
181
184
  def unset(*args)
182
- fields = args.__find_args__.collect { |f| [database_field_name(f), true] }
185
+ fields = args.map { |a| a.is_a?(Hash) ? a.keys : a }
186
+ .__find_args__
187
+ .map { |f| [database_field_name(f), true] }
183
188
  view.update_many("$unset" => Hash[fields])
184
189
  end
185
190
 
@@ -112,6 +112,9 @@ module Mongoid
112
112
  entries.length
113
113
  end
114
114
  alias :size :length
115
+
116
+ alias :find_first :first
117
+ alias :one :first
115
118
  end
116
119
  end
117
120
  end
@@ -597,7 +597,7 @@ module Mongoid
597
597
  end
598
598
  _mongoid_expand_keys(new_s).each do |k, v|
599
599
  k = k.to_s
600
- if c.selector[k] || k[0] == ?$
600
+ if c.selector[k] || k.start_with?('$')
601
601
  c = c.send(:__multi__, [{'$nor' => [{k => v}]}], '$and')
602
602
  else
603
603
  if v.is_a?(Hash)
@@ -890,7 +890,7 @@ module Mongoid
890
890
  clone.tap do |query|
891
891
  normalized.each do |field, value|
892
892
  field_s = field.to_s
893
- if field_s[0] == ?$
893
+ if field_s.start_with?('$')
894
894
  # Query expression-level operator, like $and or $where
895
895
  query.add_operator_expression(field_s, value)
896
896
  else
@@ -38,7 +38,7 @@ module Mongoid
38
38
  raise ArgumentError, "Field must be a string: #{field}"
39
39
  end
40
40
 
41
- if field[0] == ?$
41
+ if field.start_with?('$')
42
42
  raise ArgumentError, "Field cannot be an operator (i.e. begin with $): #{field}"
43
43
  end
44
44
 
@@ -48,14 +48,14 @@ module Mongoid
48
48
  if value.is_a?(Hash) && selector[field].is_a?(Hash) &&
49
49
  value.keys.all? { |key|
50
50
  key_s = key.to_s
51
- key_s[0] == ?$ && !selector[field].key?(key_s)
51
+ key_s.start_with?('$') && !selector[field].key?(key_s)
52
52
  }
53
53
  then
54
54
  # Multiple operators can be combined on the same field by
55
55
  # adding them to the existing hash.
56
56
  new_value = selector[field].merge(value)
57
57
  selector.store(field, new_value)
58
- else
58
+ elsif selector[field] != value
59
59
  add_operator_expression('$and', [{field => value}])
60
60
  end
61
61
  else
@@ -185,7 +185,7 @@ module Mongoid
185
185
  raise ArgumentError, "Operator must be a string: #{operator}"
186
186
  end
187
187
 
188
- unless operator[0] == ?$
188
+ unless operator.start_with?('$')
189
189
  raise ArgumentError, "Operator must begin with $: #{operator}"
190
190
  end
191
191
 
@@ -220,7 +220,7 @@ module Mongoid
220
220
  raise ArgumentError, "Field must be a string: #{field}"
221
221
  end
222
222
 
223
- if field[0] == ?$
223
+ if field.start_with?('$')
224
224
  add_operator_expression(field, value)
225
225
  else
226
226
  add_field_expression(field, value)
@@ -194,8 +194,8 @@ module Mongoid
194
194
  #
195
195
  # @param [ Hash ] options The options.
196
196
  #
197
- # @option options [ true, false ] :compact Whether to include fields with
198
- # nil values in the json document.
197
+ # @option options [ true, false ] :compact (Deprecated) Whether to include fields
198
+ # with nil values in the json document.
199
199
  #
200
200
  # @return [ Hash ] The document as json.
201
201
  #
@@ -203,6 +203,7 @@ module Mongoid
203
203
  def as_json(options = nil)
204
204
  rv = super
205
205
  if options && options[:compact]
206
+ Mongoid.logger.warn('#as_json :compact option is deprecated. Please call #compact on the returned Hash object instead.')
206
207
  rv = rv.compact
207
208
  end
208
209
  rv
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ # encoding: utf-8
3
+
4
+ module Mongoid
5
+ module Errors
6
+
7
+ # This error is raised when an empty configuration file is attempted to be
8
+ # loaded.
9
+ class EmptyConfigFile < MongoidError
10
+
11
+ # Create the new error.
12
+ #
13
+ # @param [ String ] path The path of the config file used.
14
+ #
15
+ # @api private
16
+ def initialize(path)
17
+ super(
18
+ compose_message(
19
+ "empty_config_file",
20
+ { path: path }
21
+ )
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ # encoding: utf-8
3
+
4
+ module Mongoid
5
+ module Errors
6
+
7
+ # This error is raised when a bad configuration file is attempted to be
8
+ # loaded.
9
+ class InvalidConfigFile < MongoidError
10
+
11
+ # Create the new error.
12
+ #
13
+ # @param [ String ] path The path of the config file used.
14
+ #
15
+ # @api private
16
+ def initialize(path)
17
+ super(
18
+ compose_message(
19
+ "invalid_config_file",
20
+ { path: path }
21
+ )
22
+ )
23
+ end
24
+ end
25
+ end
26
+ end
@@ -8,8 +8,10 @@ require "mongoid/errors/criteria_argument_required"
8
8
  require "mongoid/errors/document_not_destroyed"
9
9
  require "mongoid/errors/document_not_found"
10
10
  require "mongoid/errors/eager_load"
11
+ require "mongoid/errors/empty_config_file"
11
12
  require "mongoid/errors/in_memory_collation_not_supported"
12
13
  require "mongoid/errors/invalid_collection"
14
+ require "mongoid/errors/invalid_config_file"
13
15
  require "mongoid/errors/invalid_config_option"
14
16
  require "mongoid/errors/invalid_dependent_strategy"
15
17
  require "mongoid/errors/invalid_field"
@@ -23,7 +23,7 @@ module Mongoid
23
23
  # @since 3.0.0
24
24
  def upsert(options = {})
25
25
  prepare_upsert(options) do
26
- collection.find(atomic_selector).update_one(
26
+ collection.find(atomic_selector).replace_one(
27
27
  as_attributes, upsert: true, session: _session)
28
28
  end
29
29
  end
@@ -237,7 +237,9 @@ module Mongoid
237
237
  # @since 6.0.0
238
238
  def clear(object, cluster = nil, original_context = nil)
239
239
  if context = get(object)
240
- context.client.close unless (context.cluster.equal?(cluster) || cluster.nil?)
240
+ unless cluster.nil? || context.cluster.equal?(cluster)
241
+ context.client.close
242
+ end
241
243
  end
242
244
  ensure
243
245
  Thread.current["[mongoid][#{object.object_id}]:context"] = original_context
@@ -7,8 +7,14 @@ module Mongoid
7
7
  #
8
8
  # @since 4.0.0
9
9
  module QueryCache
10
- class << self
10
+ # @api private
11
+ LEGACY_WARNING = <<~DOC
12
+ You are using the legacy Mongoid query cache which has known issues.
13
+ Please upgrade the `mongo' gem to at least 2.14.0 to use the improved driver query cache.
14
+ Refer to: https://docs.mongodb.com/mongoid/current/tutorials/mongoid-queries/#the-improved-driver-query-cache
15
+ DOC
11
16
 
17
+ class << self
12
18
  # Get the cached queries.
13
19
  #
14
20
  # @example Get the cached queries from the current thread.
@@ -86,6 +92,10 @@ module Mongoid
86
92
  if defined?(Mongo::QueryCache)
87
93
  Mongo::QueryCache.cache(&block)
88
94
  else
95
+ @legacy_query_cache_warned ||= begin
96
+ Mongoid.logger.warn(LEGACY_WARNING)
97
+ true
98
+ end
89
99
  enabled = QueryCache.enabled?
90
100
  QueryCache.enabled = true
91
101
  begin
@@ -123,7 +123,7 @@ module Mongoid
123
123
  next if model.shard_config.nil?
124
124
 
125
125
  if model.embedded? && !model.cyclic?
126
- logger.warn("MONGOID: #{model} has shard config but is emdedded")
126
+ logger.warn("MONGOID: #{model} has shard config but is embedded")
127
127
  next
128
128
  end
129
129
 
@@ -41,6 +41,16 @@ module Mongoid
41
41
  # _association.inverse_association.options but inverse_association
42
42
  # seems to not always/ever be set here. See MONGOID-5014.
43
43
  _parent.touch
44
+
45
+ if field
46
+ # If we are told to also touch a field, perform a separate write
47
+ # for that field. See MONGOID-5136.
48
+ # In theory we should combine the writes, which would require
49
+ # passing the fields to be updated to the parents - MONGOID-5142.
50
+ sets = set_field_atomic_updates(field)
51
+ selector = atomic_selector
52
+ _root.collection.find(selector).update_one(positionally(selector, sets), session: _session)
53
+ end
44
54
  else
45
55
  # If the current document is not embedded, it is composition root
46
56
  # and we need to persist the write here.
@@ -2,5 +2,5 @@
2
2
  # encoding: utf-8
3
3
 
4
4
  module Mongoid
5
- VERSION = "7.3.2"
5
+ VERSION = "7.3.5"
6
6
  end
@@ -80,8 +80,13 @@ development:
80
80
  # (default: 10)
81
81
  # connect_timeout: 10
82
82
 
83
- # The timeout to wait to execute operations on a socket before raising an error.
84
- # (default: 5)
83
+ # How long to wait for a response for each operation sent to the
84
+ # server. This timeout should be set to a value larger than the
85
+ # processing time for the longest operation that will be executed
86
+ # by the application. Note that this is a client-side timeout;
87
+ # the server may continue executing an operation after the client
88
+ # aborts it with the SocketTimeout exception.
89
+ # (default: nil, meaning no timeout)
85
90
  # socket_timeout: 5
86
91
 
87
92
  # The name of the replica set to connect to. Servers provided as seeds that do
@@ -65,4 +65,143 @@ describe 'embeds_many associations' do
65
65
  include_examples 'does not delete the target from the database'
66
66
  end
67
67
  end
68
+
69
+ context 'assigning attributes to the same association' do
70
+ context 'setting then clearing' do
71
+ let(:canvas) do
72
+ Canvas.create!(shapes: [Shape.new])
73
+ end
74
+
75
+ shared_examples 'persists correctly' do
76
+ it 'persists correctly' do
77
+ canvas.shapes.should be_empty
78
+ _canvas = Canvas.find(canvas.id)
79
+ _canvas.shapes.should be_empty
80
+ end
81
+ end
82
+
83
+ context 'via assignment operator' do
84
+ before do
85
+ canvas.shapes = [Shape.new, Shape.new]
86
+ canvas.shapes = []
87
+ canvas.save!
88
+ end
89
+
90
+ include_examples 'persists correctly'
91
+ end
92
+
93
+ context 'via attributes=' do
94
+ before do
95
+ canvas.attributes = {shapes: [Shape.new, Shape.new]}
96
+ canvas.attributes = {shapes: []}
97
+ canvas.save!
98
+ end
99
+
100
+ include_examples 'persists correctly'
101
+ end
102
+
103
+ context 'via assign_attributes' do
104
+ before do
105
+ canvas.assign_attributes(shapes: [Shape.new, Shape.new])
106
+ canvas.assign_attributes(shapes: [])
107
+ canvas.save!
108
+ end
109
+
110
+ include_examples 'persists correctly'
111
+ end
112
+ end
113
+
114
+ context 'clearing then setting' do
115
+ let(:canvas) do
116
+ Canvas.create!(shapes: [Shape.new])
117
+ end
118
+
119
+ shared_examples 'persists correctly' do
120
+ it 'persists correctly' do
121
+ canvas.shapes.length.should eq 2
122
+ _canvas = Canvas.find(canvas.id)
123
+ _canvas.shapes.length.should eq 2
124
+ end
125
+ end
126
+
127
+ context 'via assignment operator' do
128
+ before do
129
+ canvas.shapes = []
130
+ canvas.shapes = [Shape.new, Shape.new]
131
+ canvas.save!
132
+ end
133
+
134
+ include_examples 'persists correctly'
135
+ end
136
+
137
+ context 'via attributes=' do
138
+ before do
139
+ canvas.attributes = {shapes: []}
140
+ canvas.attributes = {shapes: [Shape.new, Shape.new]}
141
+ canvas.save!
142
+ end
143
+
144
+ include_examples 'persists correctly'
145
+ end
146
+
147
+ context 'via assign_attributes' do
148
+ before do
149
+ canvas.assign_attributes(shapes: [])
150
+ canvas.assign_attributes(shapes: [Shape.new, Shape.new])
151
+ canvas.save!
152
+ end
153
+
154
+ include_examples 'persists correctly'
155
+ end
156
+ end
157
+
158
+ context 'updating a child then clearing' do
159
+ let(:canvas) do
160
+ Canvas.create!(shapes: [Shape.new])
161
+ end
162
+
163
+ shared_examples 'persists correctly' do
164
+ it 'persists correctly' do
165
+ canvas.shapes.should be_empty
166
+ _canvas = Canvas.find(canvas.id)
167
+ _canvas.shapes.should be_empty
168
+ end
169
+ end
170
+
171
+ context 'via assignment operator' do
172
+ before do
173
+ # Mongoid uses the new value of `x` in the $pullAll query,
174
+ # which doesn't match the document that is in the database,
175
+ # resulting in the empty array assignment not taking effect.
176
+ pending 'MONGOID-5037'
177
+
178
+ canvas.shapes.first.x = 1
179
+ canvas.shapes = []
180
+ canvas.save!
181
+ end
182
+
183
+ include_examples 'persists correctly'
184
+ end
185
+
186
+ context 'via attributes=' do
187
+ before do
188
+ canvas.shapes.first.x = 1
189
+ canvas.attributes = {shapes: []}
190
+ canvas.save!
191
+ end
192
+
193
+ include_examples 'persists correctly'
194
+ end
195
+
196
+ context 'via assign_attributes' do
197
+ before do
198
+ canvas.shapes.first.x = 1
199
+ canvas.assign_attributes(shapes: [])
200
+ canvas.save!
201
+ end
202
+
203
+ include_examples 'persists correctly'
204
+ end
205
+ end
206
+ end
68
207
  end