mongoid 7.5.1 → 7.5.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/Rakefile +25 -0
  4. data/lib/mongoid/association/embedded/batchable.rb +20 -3
  5. data/lib/mongoid/atomic/paths/embedded/many.rb +19 -0
  6. data/lib/mongoid/cacheable.rb +2 -2
  7. data/lib/mongoid/config.rb +6 -0
  8. data/lib/mongoid/contextual/mongo.rb +24 -7
  9. data/lib/mongoid/criteria/queryable/selector.rb +1 -1
  10. data/lib/mongoid/criteria/queryable/storable.rb +1 -1
  11. data/lib/mongoid/document.rb +8 -1
  12. data/lib/mongoid/persistence_context.rb +57 -6
  13. data/lib/mongoid/shardable.rb +35 -11
  14. data/lib/mongoid/version.rb +1 -1
  15. data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +21 -0
  16. data/spec/mongoid/association/embedded/embeds_many_models.rb +121 -0
  17. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +30 -0
  18. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +20 -0
  19. data/spec/mongoid/attributes_spec.rb +44 -0
  20. data/spec/mongoid/cacheable_spec.rb +3 -3
  21. data/spec/mongoid/clients_spec.rb +30 -0
  22. data/spec/mongoid/config_spec.rb +7 -0
  23. data/spec/mongoid/contextual/mongo_spec.rb +23 -3
  24. data/spec/mongoid/criteria/queryable/selector_spec.rb +75 -2
  25. data/spec/mongoid/criteria/queryable/storable_spec.rb +72 -0
  26. data/spec/mongoid/persistence_context_spec.rb +26 -1
  27. data/spec/mongoid/shardable_models.rb +14 -0
  28. data/spec/mongoid/shardable_spec.rb +157 -51
  29. data/spec/mongoid_spec.rb +7 -1
  30. data/spec/shared/lib/mrss/lite_constraints.rb +8 -0
  31. data/spec/shared/shlib/server.sh +5 -5
  32. data.tar.gz.sig +0 -0
  33. metadata +632 -628
  34. metadata.gz.sig +3 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 35ea6871ead43684bcaba29452a4d21a63d40a2703e5a954c60318bc4242759f
4
- data.tar.gz: bb819713b38b1639d9e0ccc4ac95bd171a054fc1025b330cbac492f980d93015
3
+ metadata.gz: ce7f228fdc033dc1142f46f4771395d36acafc97e0c144599cd063af537c7ae3
4
+ data.tar.gz: 15f8f0efbb076eb03f25f23898cbe27d044e7441e4a7ae4bc8d22f036838bb98
5
5
  SHA512:
6
- metadata.gz: 80981e2b9473bac38edf29ee73aa49f4f5932b577ee8095e1af9407f113e9a156f5860049300c8b486ec66ff267ec8fd9b91007697d3d6e953cc65580e29e4b9
7
- data.tar.gz: 45077ad45b7d833dddaee515736c3faf54b5472e684a1e0e4220c1bff1b6ba6b9284bea082b235eb7371540af95481e728ad09c082ae40f5cf3d6d77f45e2f11
6
+ metadata.gz: 91f40b7136ae729e6831c5033c60c0a1e2207cd98d80bf5caaef50844230d6830efe555748571d9bdef62a101f75ffd21d2a7942278f84b68e2a51fc6a8f20df
7
+ data.tar.gz: 3555b90da32865e1faadb9efccc6db0262adea620b4dd473d5dc1ee533b00451039dfaacd982ef5c8f6be4009ccff589119b2f512971a1b5ac509bb7ae4aa96e
checksums.yaml.gz.sig CHANGED
Binary file
data/Rakefile CHANGED
@@ -11,6 +11,15 @@ $: << File.join(ROOT, 'spec/shared/lib')
11
11
  require "rake"
12
12
  require "rspec/core/rake_task"
13
13
  require 'mrss/spec_organizer'
14
+ require 'rubygems/package'
15
+ require 'rubygems/security/policies'
16
+
17
+ def signed_gem?(path_to_gem)
18
+ Gem::Package.new(path_to_gem, Gem::Security::HighSecurity).verify
19
+ true
20
+ rescue Gem::Security::Exception => e
21
+ false
22
+ end
14
23
 
15
24
  $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
16
25
  require "mongoid/version"
@@ -103,3 +112,19 @@ namespace :release do
103
112
  end
104
113
  end
105
114
  end
115
+
116
+ desc 'Verifies that all built gems in pkg/ are valid'
117
+ task :verify do
118
+ gems = Dir['pkg/*.gem']
119
+ if gems.empty?
120
+ puts 'There are no gems in pkg/ to verify'
121
+ else
122
+ gems.each do |gem|
123
+ if signed_gem?(gem)
124
+ puts "#{gem} is signed"
125
+ else
126
+ abort "#{gem} is not signed"
127
+ end
128
+ end
129
+ end
130
+ end
@@ -80,7 +80,8 @@ module Mongoid
80
80
  def batch_replace(docs)
81
81
  if docs.blank?
82
82
  if _assigning? && !empty?
83
- _base.delayed_atomic_sets.clear
83
+ _base.delayed_atomic_sets.delete(path)
84
+ clear_atomic_path_cache
84
85
  _base.add_atomic_unset(first)
85
86
  target_duplicate = _target.dup
86
87
  pre_process_batch_remove(target_duplicate, :delete)
@@ -92,7 +93,8 @@ module Mongoid
92
93
  _base.delayed_atomic_sets.clear unless _assigning?
93
94
  docs = normalize_docs(docs).compact
94
95
  _target.clear and _unscoped.clear
95
- _base.delayed_atomic_unsets.clear
96
+ _base.delayed_atomic_unsets.delete(path)
97
+ clear_atomic_path_cache
96
98
  inserts = execute_batch_set(docs)
97
99
  add_atomic_sets(inserts)
98
100
  end
@@ -234,7 +236,22 @@ module Mongoid
234
236
  #
235
237
  # @return [ String ] The atomic path.
236
238
  def path
237
- @path ||= _unscoped.first.atomic_path
239
+ @path ||= if _unscoped.empty?
240
+ Mongoid::Atomic::Paths::Embedded::Many.position_without_document(_base, _association)
241
+ else
242
+ _unscoped.first.atomic_path
243
+ end
244
+ end
245
+
246
+ # Clear the cache for path and atomic_paths. This method is used when
247
+ # the path method is used, and the association has not been set on the
248
+ # document yet, which can cause path and atomic_paths to be calculated
249
+ # incorrectly later.
250
+ #
251
+ # @api private
252
+ def clear_atomic_path_cache
253
+ self.path = nil
254
+ _base.instance_variable_set("@atomic_paths", nil)
238
255
  end
239
256
 
240
257
  # Set the atomic path.
@@ -34,6 +34,25 @@ module Mongoid
34
34
  locator = document.new_record? ? "" : ".#{document._index}"
35
35
  "#{pos}#{"." unless pos.blank?}#{document._association.store_as}#{locator}"
36
36
  end
37
+
38
+ class << self
39
+
40
+ # Get the position of where the document would go for the given
41
+ # association. The use case for this function is when trying to
42
+ # persist an empty list for an embedded association. All of the
43
+ # existing functions for getting the position to store a document
44
+ # require passing in a document to store, which we don't have when
45
+ # trying to store the empty list.
46
+ #
47
+ # @param [ Document ] parent The parent document to store in.
48
+ # @param [ Association ] association The association.
49
+ #
50
+ # @return [ String ] The position string.
51
+ def position_without_document(parent, association)
52
+ pos = parent.atomic_position
53
+ "#{pos}#{"." unless pos.blank?}#{association.store_as}"
54
+ end
55
+ end
37
56
  end
38
57
  end
39
58
  end
@@ -15,7 +15,7 @@ module Mongoid
15
15
  # plural model name.
16
16
  #
17
17
  # If new_record? - will append /new
18
- # If not - will append /id-updated_at.to_s(cache_timestamp_format)
18
+ # If not - will append /id-updated_at.to_formatted_s(cache_timestamp_format)
19
19
  # Without updated_at - will append /id
20
20
  #
21
21
  # This is usually called inside a cache() block
@@ -26,7 +26,7 @@ module Mongoid
26
26
  # @return [ String ] the string with or without updated_at
27
27
  def cache_key
28
28
  return "#{model_key}/new" if new_record?
29
- return "#{model_key}/#{_id}-#{updated_at.utc.to_s(cache_timestamp_format)}" if do_or_do_not(:updated_at)
29
+ return "#{model_key}/#{_id}-#{updated_at.utc.to_formatted_s(cache_timestamp_format)}" if do_or_do_not(:updated_at)
30
30
  "#{model_key}/#{_id}"
31
31
  end
32
32
  end
@@ -116,6 +116,12 @@ module Mongoid
116
116
  # using and's instead of overwriting them.
117
117
  option :overwrite_chained_operators, default: true
118
118
 
119
+ # When this flag is true, the attributes method on a document will return
120
+ # a BSON::Document when that document is retrieved from the database, and
121
+ # a Hash otherwise. When this flag is false, the attributes method will
122
+ # always return a Hash.
123
+ option :legacy_attributes, default: true
124
+
119
125
  # Has Mongoid been configured? This is checking that at least a valid
120
126
  # client config exists.
121
127
  #
@@ -260,16 +260,16 @@ module Mongoid
260
260
  #
261
261
  # @return [ Document ] The first document.
262
262
  def first(limit_or_opts = nil)
263
- limit = limit_or_opts unless limit_or_opts.is_a?(Hash)
263
+ limit, opts = extract_limit_and_opts(limit_or_opts)
264
264
  if cached? && cache_loaded?
265
265
  return limit ? documents.first(limit) : documents.first
266
266
  end
267
267
  try_numbered_cache(:first, limit) do
268
- if limit_or_opts.try(:key?, :id_sort)
268
+ if opts.key?(:id_sort)
269
269
  Mongoid::Warnings.warn_id_sort_deprecated
270
270
  end
271
271
  sorted_view = view
272
- if sort = view.sort || ({ _id: 1 } unless limit_or_opts.try(:fetch, :id_sort) == :none)
272
+ if sort = view.sort || ({ _id: 1 } unless opts[:id_sort] == :none)
273
273
  sorted_view = view.sort(sort)
274
274
  end
275
275
  if raw_docs = sorted_view.limit(limit || 1).to_a
@@ -376,12 +376,12 @@ module Mongoid
376
376
  #
377
377
  # @return [ Document ] The last document.
378
378
  def last(limit_or_opts = nil)
379
- limit = limit_or_opts unless limit_or_opts.is_a?(Hash)
379
+ limit, opts = extract_limit_and_opts(limit_or_opts)
380
380
  if cached? && cache_loaded?
381
381
  return limit ? documents.last(limit) : documents.last
382
382
  end
383
383
  res = try_numbered_cache(:last, limit) do
384
- with_inverse_sorting(limit_or_opts) do
384
+ with_inverse_sorting(opts) do
385
385
  if raw_docs = view.limit(limit || 1).to_a
386
386
  process_raw_docs(raw_docs, limit)
387
387
  end
@@ -612,6 +612,23 @@ module Mongoid
612
612
  end
613
613
  end
614
614
 
615
+ # Extract the limit and opts from the given argument, so that code
616
+ # can operate without having to worry about the current type and
617
+ # state of the argument.
618
+ #
619
+ # @param [ nil | Integer | Hash ] limit_or_opts The value to pull the
620
+ # limit and option hash from.
621
+ #
622
+ # @return [ Array<nil | Integer, Hash> ] A 2-array of the limit and the
623
+ # option hash.
624
+ def extract_limit_and_opts(limit_or_opts)
625
+ case limit_or_opts
626
+ when nil, Integer then [ limit_or_opts, {} ]
627
+ when Hash then [ nil, limit_or_opts ]
628
+ else raise ArgumentError, "expected nil, Integer, or Hash"
629
+ end
630
+ end
631
+
615
632
  # Update the documents for the provided method.
616
633
  #
617
634
  # @api private
@@ -676,10 +693,10 @@ module Mongoid
676
693
  # @example Apply the inverse sorting params to the given block
677
694
  # context.with_inverse_sorting
678
695
  def with_inverse_sorting(opts = {})
679
- Mongoid::Warnings.warn_id_sort_deprecated if opts.try(:key?, :id_sort)
696
+ Mongoid::Warnings.warn_id_sort_deprecated if opts.key?(:id_sort)
680
697
 
681
698
  begin
682
- if sort = criteria.options[:sort] || ( { _id: 1 } unless opts.try(:fetch, :id_sort) == :none )
699
+ if sort = criteria.options[:sort] || ( { _id: 1 } unless opts[:id_sort] == :none )
683
700
  @view = view.sort(Hash[sort.map{|k, v| [k, -1*v]}])
684
701
  end
685
702
  yield
@@ -20,7 +20,7 @@ module Mongoid
20
20
  other.each_pair do |key, value|
21
21
  if value.is_a?(Hash) && self[key.to_s].is_a?(Hash)
22
22
  value = self[key.to_s].merge(value) do |_key, old_val, new_val|
23
- case _key
23
+ case _key.to_s
24
24
  when '$in'
25
25
  new_val & old_val
26
26
  when '$nin'
@@ -47,7 +47,7 @@ module Mongoid
47
47
  if value.is_a?(Hash) && selector[field].is_a?(Hash) &&
48
48
  value.keys.all? { |key|
49
49
  key_s = key.to_s
50
- key_s.start_with?('$') && !selector[field].key?(key_s)
50
+ key_s.start_with?('$') && !selector[field].keys.map(&:to_s).include?(key_s)
51
51
  }
52
52
  then
53
53
  # Multiple operators can be combined on the same field by
@@ -277,8 +277,15 @@ module Mongoid
277
277
  # criteria.
278
278
  #
279
279
  # @return [ Document ] A new document.
280
+ #
281
+ # @api private
280
282
  def instantiate(attrs = nil, selected_fields = nil)
281
- attributes = attrs || {}
283
+ attributes = if Mongoid.legacy_attributes
284
+ attrs
285
+ else
286
+ attrs&.to_h
287
+ end || {}
288
+
282
289
  doc = allocate
283
290
  doc.__selected_fields = selected_fields
284
291
  doc.instance_variable_set(:@attributes, attributes)
@@ -122,6 +122,21 @@ module Mongoid
122
122
  options == other.options
123
123
  end
124
124
 
125
+ # Whether the client of the context can be reused later, and therefore should
126
+ # not be closed.
127
+ #
128
+ # If the persistence context is requested with :client option only, it means
129
+ # that the context should use a client configured in mongoid.yml.
130
+ # Such clients should not be closed when the context is cleared since they
131
+ # will be reused later.
132
+ #
133
+ # @return [ true | false ] True if client can be reused, otherwise false.
134
+ #
135
+ # @api private
136
+ def reusable_client?
137
+ @options.keys == [:client]
138
+ end
139
+
125
140
  private
126
141
 
127
142
  def set_options!(opts)
@@ -172,8 +187,7 @@ module Mongoid
172
187
  #
173
188
  # @return [ Mongoid::PersistenceContext ] The persistence context for the object.
174
189
  def set(object, options_or_context)
175
- key = "[mongoid][#{object.object_id}]:context"
176
- existing_context = Thread.current[key]
190
+ existing_context = get_context(object)
177
191
  existing_options = if existing_context
178
192
  existing_context.options
179
193
  else
@@ -184,7 +198,7 @@ module Mongoid
184
198
  end
185
199
  new_options = existing_options.merge(options_or_context)
186
200
  context = PersistenceContext.new(object, new_options)
187
- Thread.current[key] = context
201
+ store_context(object, context)
188
202
  end
189
203
 
190
204
  # Get the persistence context for a particular class or model instance.
@@ -196,7 +210,7 @@ module Mongoid
196
210
  #
197
211
  # @return [ Mongoid::PersistenceContext ] The persistence context for the object.
198
212
  def get(object)
199
- Thread.current["[mongoid][#{object.object_id}]:context"]
213
+ get_context(object)
200
214
  end
201
215
 
202
216
  # Clear the persistence context for a particular class or model instance.
@@ -211,11 +225,48 @@ module Mongoid
211
225
  def clear(object, cluster = nil, original_context = nil)
212
226
  if context = get(object)
213
227
  unless cluster.nil? || context.cluster.equal?(cluster)
214
- context.client.close
228
+ context.client.close unless context.reusable_client?
215
229
  end
216
230
  end
217
231
  ensure
218
- Thread.current["[mongoid][#{object.object_id}]:context"] = original_context
232
+ store_context(object, original_context)
233
+ end
234
+
235
+ private
236
+
237
+ # Key to store persistence contexts in the thread local storage.
238
+ #
239
+ # @api private
240
+ PERSISTENCE_CONTEXT_KEY = :"[mongoid]:persistence_context"
241
+
242
+ # Get the persistence context for a given object from the thread local
243
+ # storage.
244
+ #
245
+ # @param [ Object ] object Object to get the persistance context for.
246
+ #
247
+ # @return [ Mongoid::PersistenceContext | nil ] The persistence context
248
+ # for the object if previously stored, otherwise nil.
249
+ #
250
+ # @api private
251
+ def get_context(object)
252
+ Thread.current[PERSISTENCE_CONTEXT_KEY] ||= {}
253
+ Thread.current[PERSISTENCE_CONTEXT_KEY][object.object_id]
254
+ end
255
+
256
+ # Store persistence context for a given object in the thread local
257
+ # storage.
258
+ #
259
+ # @param [ Object ] object Object to store the persistance context for.
260
+ # @param [ Mongoid::PersistenceContext ] context Context to store
261
+ #
262
+ # @api private
263
+ def store_context(object, context)
264
+ if context.nil?
265
+ Thread.current[PERSISTENCE_CONTEXT_KEY]&.delete(object.object_id)
266
+ else
267
+ Thread.current[PERSISTENCE_CONTEXT_KEY] ||= {}
268
+ Thread.current[PERSISTENCE_CONTEXT_KEY][object.object_id] = context
269
+ end
219
270
  end
220
271
  end
221
272
  end
@@ -47,18 +47,22 @@ module Mongoid
47
47
  self.class.shard_key_fields
48
48
  end
49
49
 
50
- # Returns the selector that would match the current version of this
51
- # document.
50
+ # Returns the selector that would match the defined shard keys. If
51
+ # `prefer_persisted` is false (the default), it uses the current values
52
+ # of the specified shard keys, otherwise, it will try to use whatever value
53
+ # was most recently persisted.
54
+ #
55
+ # @param [ true | false ] prefer_persisted Whether to use the current
56
+ # value of the shard key fields, or to use their most recently persisted
57
+ # values.
52
58
  #
53
59
  # @return [ Hash ] The shard key selector.
54
60
  #
55
61
  # @api private
56
- def shard_key_selector
57
- selector = {}
58
- shard_key_fields.each do |field|
59
- selector[field.to_s] = send(field)
62
+ def shard_key_selector(prefer_persisted: false)
63
+ shard_key_fields.each_with_object({}) do |field, selector|
64
+ selector[field.to_s] = shard_key_field_value(field.to_s, prefer_persisted: prefer_persisted)
60
65
  end
61
- selector
62
66
  end
63
67
 
64
68
  # Returns the selector that would match the existing version of this
@@ -72,11 +76,31 @@ module Mongoid
72
76
  #
73
77
  # @api private
74
78
  def shard_key_selector_in_db
75
- selector = {}
76
- shard_key_fields.each do |field|
77
- selector[field.to_s] = new_record? ? send(field) : attribute_was(field)
79
+ shard_key_selector(prefer_persisted: true)
80
+ end
81
+
82
+ # Returns the value for the named shard key. If the field identifies
83
+ # an embedded document, the key will be parsed and recursively evaluated.
84
+ # If `prefer_persisted` is true, the value last persisted to the database
85
+ # will be returned, regardless of what the current value of the attribute
86
+ # may be.
87
+ #
88
+ # @param [String] field The name of the field to evaluate
89
+ # @param [ true|false ] prefer_persisted Whether or not to prefer the
90
+ # persisted value over the current value.
91
+ #
92
+ # @return [ Object ] The value of the named field.
93
+ #
94
+ # @api private
95
+ def shard_key_field_value(field, prefer_persisted:)
96
+ if field.include?(".")
97
+ relation, remaining = field.split(".", 2)
98
+ send(relation)&.shard_key_field_value(remaining, prefer_persisted: prefer_persisted)
99
+ elsif prefer_persisted && !new_record?
100
+ attribute_was(field)
101
+ else
102
+ send(field)
78
103
  end
79
- selector
80
104
  end
81
105
 
82
106
  module ClassMethods
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mongoid
4
- VERSION = "7.5.1"
4
+ VERSION = "7.5.4"
5
5
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "spec_helper"
4
+ require_relative '../embeds_many_models.rb'
4
5
 
5
6
  describe Mongoid::Association::Embedded::EmbedsMany::Proxy do
6
7
 
@@ -4649,4 +4650,24 @@ describe Mongoid::Association::Embedded::EmbedsMany::Proxy do
4649
4650
  end
4650
4651
  end
4651
4652
  end
4653
+
4654
+ context "when using assign_attributes with an already populated array" do
4655
+ let(:post) { EmmPost.create! }
4656
+
4657
+ before do
4658
+ post.assign_attributes(company_tags: [{id: BSON::ObjectId.new, title: 'a'}],
4659
+ user_tags: [{id: BSON::ObjectId.new, title: 'b'}])
4660
+ post.save!
4661
+ post.reload
4662
+ post.assign_attributes(company_tags: [{id: BSON::ObjectId.new, title: 'c'}],
4663
+ user_tags: [])
4664
+ post.save!
4665
+ post.reload
4666
+ end
4667
+
4668
+ it "has the correct embedded documents" do
4669
+ expect(post.company_tags.length).to eq(1)
4670
+ expect(post.company_tags.first.title).to eq("c")
4671
+ end
4672
+ end
4652
4673
  end
@@ -67,3 +67,124 @@ class EmmOuter
67
67
 
68
68
  field :level, :type => Integer
69
69
  end
70
+
71
+ class EmmCustomerAddress
72
+ include Mongoid::Document
73
+
74
+ embedded_in :addressable, polymorphic: true, inverse_of: :work_address
75
+ end
76
+
77
+ class EmmFriend
78
+ include Mongoid::Document
79
+
80
+ embedded_in :befriendable, polymorphic: true
81
+ end
82
+
83
+ class EmmCustomer
84
+ include Mongoid::Document
85
+
86
+ embeds_one :home_address, class_name: 'EmmCustomerAddress', as: :addressable
87
+ embeds_one :work_address, class_name: 'EmmCustomerAddress', as: :addressable
88
+
89
+ embeds_many :close_friends, class_name: 'EmmFriend', as: :befriendable
90
+ embeds_many :acquaintances, class_name: 'EmmFriend', as: :befriendable
91
+ end
92
+
93
+ class EmmUser
94
+ include Mongoid::Document
95
+ include Mongoid::Timestamps
96
+
97
+ embeds_many :orders, class_name: 'EmmOrder'
98
+ end
99
+
100
+ class EmmOrder
101
+ include Mongoid::Document
102
+
103
+ field :amount, type: Integer
104
+
105
+ embedded_in :user, class_name: 'EmmUser'
106
+ end
107
+
108
+ module EmmSpec
109
+ # There is also a top-level Car class defined.
110
+ class Car
111
+ include Mongoid::Document
112
+
113
+ embeds_many :doors
114
+ end
115
+
116
+ class Door
117
+ include Mongoid::Document
118
+
119
+ embedded_in :car
120
+ end
121
+
122
+ class Tank
123
+ include Mongoid::Document
124
+
125
+ embeds_many :guns
126
+ embeds_many :emm_turrets
127
+ # This association references a model that is not in our module,
128
+ # and it does not define class_name hence Mongoid will not be able to
129
+ # figure out the inverse for this association.
130
+ embeds_many :emm_hatches
131
+
132
+ # class_name is intentionally unqualified, references a class in the
133
+ # same module. Rails permits class_name to be unqualified like this.
134
+ embeds_many :launchers, class_name: 'Launcher'
135
+ end
136
+
137
+ class Gun
138
+ include Mongoid::Document
139
+
140
+ embedded_in :tank
141
+ end
142
+
143
+ class Launcher
144
+ include Mongoid::Document
145
+
146
+ # class_name is intentionally unqualified.
147
+ embedded_in :tank, class_name: 'Tank'
148
+ end
149
+ end
150
+
151
+ # This is intentionally on top level.
152
+ class EmmTurret
153
+ include Mongoid::Document
154
+
155
+ embedded_in :tank, class_name: 'EmmSpec::Tank'
156
+ end
157
+
158
+ # This is intentionally on top level.
159
+ class EmmHatch
160
+ include Mongoid::Document
161
+
162
+ # No :class_name option on this association intentionally.
163
+ embedded_in :tank
164
+ end
165
+
166
+ class EmmPost
167
+ include Mongoid::Document
168
+
169
+ embeds_many :company_tags, class_name: "EmmCompanyTag"
170
+ embeds_many :user_tags, class_name: "EmmUserTag"
171
+ end
172
+
173
+
174
+ class EmmCompanyTag
175
+ include Mongoid::Document
176
+
177
+ field :title, type: String
178
+
179
+ embedded_in :post, class_name: "EmmPost"
180
+ end
181
+
182
+
183
+ class EmmUserTag
184
+ include Mongoid::Document
185
+
186
+ field :title, type: String
187
+
188
+ embedded_in :post, class_name: "EmmPost"
189
+ end
190
+
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "spec_helper"
4
+ require_relative "../has_and_belongs_to_many_models.rb"
4
5
 
5
6
  describe Mongoid::Association::Referenced::HasAndBelongsToMany::Proxy do
6
7
 
@@ -3770,4 +3771,33 @@ describe Mongoid::Association::Referenced::HasAndBelongsToMany::Proxy do
3770
3771
  expect(p2.d_ids).to match_array([d2.id])
3771
3772
  end
3772
3773
  end
3774
+
3775
+ # This test is for MONGOID-5344 which tests that the initial call to
3776
+ # signature_ids refers to the same array as subsequent calls to signature_ids.
3777
+ # Prior to the change in that ticket, this test broke because the array
3778
+ # returned from write_attribute (which is triggered the first time the
3779
+ # foreign key array is referenced, to set the default), refers to a different
3780
+ # array to the one stored in the attributes hash. This happened because,
3781
+ # when retrieving a document from the database, the attributes hash is actually
3782
+ # a BSON::Document, which applies a transformation to the array before
3783
+ # storing it.
3784
+ context "when executing concat on foreign key array from the db" do
3785
+ config_override :legacy_attributes, false
3786
+
3787
+ before do
3788
+ HabtmmContract.create!
3789
+ HabtmmSignature.create!
3790
+ end
3791
+
3792
+ let!(:contract) { HabtmmContract.first }
3793
+ let!(:signature) { HabtmmSignature.first }
3794
+
3795
+ before do
3796
+ contract.signature_ids.concat([signature.id])
3797
+ end
3798
+
3799
+ it "works on the first attempt" do
3800
+ expect(contract.signature_ids).to eq([signature.id])
3801
+ end
3802
+ end
3773
3803
  end
@@ -4089,4 +4089,24 @@ describe Mongoid::Association::Referenced::HasMany::Proxy do
4089
4089
  expect(band.same_name).to eq([agent])
4090
4090
  end
4091
4091
  end
4092
+
4093
+ context "when executing concat on foreign key array from the db" do
4094
+ config_override :legacy_attributes, false
4095
+
4096
+ before do
4097
+ Agent.create!
4098
+ Basic.create!
4099
+ end
4100
+
4101
+ let!(:agent) { Agent.first }
4102
+ let!(:basic) { Basic.first }
4103
+
4104
+ before do
4105
+ agent.basic_ids.concat([basic.id])
4106
+ end
4107
+
4108
+ it "works on the first attempt" do
4109
+ expect(agent.basic_ids).to eq([basic.id])
4110
+ end
4111
+ end
4092
4112
  end