mongoid 7.3.2 → 7.3.5

Sign up to get free protection for your applications and to get access to all the features.
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