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
@@ -32,6 +32,9 @@ module Mongoid
32
32
  hash[key] = "#{key}-stack"
33
33
  end
34
34
 
35
+ # The name of the stack tracking collection and index management.
36
+ COLLECTION_MANAGEMENT = :collection_management
37
+
35
38
  # The key for the current thread's sessions.
36
39
  SESSIONS_KEY = 'sessions'
37
40
 
@@ -42,6 +45,9 @@ module Mongoid
42
45
  # executed on documents.
43
46
  EXECUTE_CALLBACKS = 'execute-callbacks'
44
47
 
48
+ # The key for the time left in the current in-memory regexp budget.
49
+ REGEXP_BUDGET_KEY = 'regexp-budget'
50
+
45
51
  extend self
46
52
 
47
53
  # Resets the current thread- or fiber-local storage to its initial state.
@@ -165,6 +171,36 @@ module Mongoid
165
171
  !stack(name).empty?
166
172
  end
167
173
 
174
+ # Execute the block as collection or index management.
175
+ #
176
+ # Creating, dropping and inspecting collections and indexes sends no
177
+ # document data, so these operations are exempt from the encryption schema
178
+ # check that PersistenceContext applies to reads and writes. Without the
179
+ # exemption, tasks such as db:mongoid:create_collections would need an
180
+ # encryption-capable client to run.
181
+ #
182
+ # @example Create a collection.
183
+ # Threaded.with_collection_management { model.create_collection }
184
+ #
185
+ # @return [ Object ] The result of the block.
186
+ def with_collection_management
187
+ begin_execution(COLLECTION_MANAGEMENT)
188
+ yield
189
+ ensure
190
+ exit_execution(COLLECTION_MANAGEMENT)
191
+ end
192
+
193
+ # Is collection or index management being executed?
194
+ #
195
+ # @example Is a collection being managed?
196
+ # Threaded.managing_collection?
197
+ #
198
+ # @return [ true | false ] Whether collection or index management is in
199
+ # progress on the current thread.
200
+ def managing_collection?
201
+ executing?(COLLECTION_MANAGEMENT)
202
+ end
203
+
168
204
  # Exit from a named thread local stack.
169
205
  #
170
206
  # @example Exit from the stack.
@@ -22,13 +22,20 @@ module Mongoid
22
22
  true
23
23
  end
24
24
 
25
- # Begin an execution that should skip timestamping.
25
+ # Skip timestamping for the duration of the given block, or (in the
26
+ # deprecated, block-less form) for the next persistence operation.
26
27
  #
27
- # @example Save a document but don't timestamp.
28
+ # @example Save a document but don't timestamp (block form).
29
+ # person.timeless { person.save }
30
+ #
31
+ # @example Save a document but don't timestamp (deprecated chained form).
28
32
  # person.timeless.save
29
33
  #
30
- # @return [ Document ] The document this was called on.
31
- def timeless
34
+ # @return [ Object | Document ] The return value of the block, or (in the
35
+ # block-less form) the document this was called on.
36
+ def timeless(&block)
37
+ return Timeless.with_timeless(&block) if block
38
+
32
39
  self.class.timeless
33
40
  self
34
41
  end
@@ -47,6 +54,9 @@ module Mongoid
47
54
  # The key to use to store the timeless table
48
55
  TIMELESS_TABLE_KEY = '[mongoid]:timeless'
49
56
 
57
+ # The key to use to store the block-based timeless flag.
58
+ TIMELESS_FLAG_KEY = '[mongoid]:timeless-flag'
59
+
50
60
  # Returns the in-memory thread cache of classes
51
61
  # for which to skip timestamping.
52
62
  #
@@ -58,16 +68,78 @@ module Mongoid
58
68
  end
59
69
 
60
70
  def_delegators :timeless_table, :[]=, :[]
71
+
72
+ # Skip timestamping for the duration of the given block, on the
73
+ # current thread or fiber. This applies to every document persisted
74
+ # while the block is executing, regardless of class, including
75
+ # cascaded embedded children at any nesting depth.
76
+ #
77
+ # @example Skip timestamping for a block.
78
+ # Mongoid::Timestamps::Timeless.with_timeless do
79
+ # person.save
80
+ # end
81
+ #
82
+ # @return [ Object ] The return value of the block.
83
+ def with_timeless
84
+ # Only the outermost block owns the flag: if we are already inside a
85
+ # timeless scope, we leave the suppression in place when this block
86
+ # ends. This avoids tracking a nesting depth that could drift out of
87
+ # sync.
88
+ already_timeless = suppressing_timestamps?
89
+ set_suppressing_timestamps(true) unless already_timeless
90
+ yield
91
+ ensure
92
+ set_suppressing_timestamps(false) unless already_timeless
93
+ end
94
+
95
+ # Whether a block-based timeless scope is currently active on this
96
+ # thread/fiber.
97
+ #
98
+ # @return [ true | false ] Whether timestamps are being suppressed.
99
+ #
100
+ # @api private
101
+ def suppressing_timestamps?
102
+ !!Threaded.get(TIMELESS_FLAG_KEY) { false }
103
+ end
104
+
105
+ # Set whether a block-based timeless scope is active on this
106
+ # thread/fiber.
107
+ #
108
+ # @param [ true | false ] value Whether to suppress timestamps.
109
+ #
110
+ # @api private
111
+ def set_suppressing_timestamps(value)
112
+ Threaded.set(TIMELESS_FLAG_KEY, value)
113
+ end
61
114
  end
62
115
 
63
116
  module ClassMethods
64
- # Begin an execution that should skip timestamping.
117
+ # Skip timestamping for the duration of the given block, or (in the
118
+ # deprecated, block-less form) for the next persistence operation.
119
+ #
120
+ # @example Create a document but don't timestamp (block form).
121
+ # Person.timeless { Person.create(title: "Sir") }
65
122
  #
66
- # @example Create a document but don't timestamp.
123
+ # @example Create a document but don't timestamp (deprecated form).
67
124
  # Person.timeless.create(:title => "Sir")
68
125
  #
69
- # @return [ Class ] The class this was called on.
70
- def timeless
126
+ # @return [ Object | Class ] The return value of the block, or (in the
127
+ # block-less form) the class this was called on.
128
+ def timeless(&block)
129
+ return Timeless.with_timeless(&block) if block
130
+
131
+ locations = caller_locations
132
+
133
+ # if this is called from the #timeless instance method, look up one
134
+ # level higher for the call site
135
+ locations.shift if locations[0].path == __FILE__
136
+
137
+ Mongoid.deprecation_warning(
138
+ :timeless_sans_block,
139
+ 'Calling #timeless without a block is deprecated; pass a block ' \
140
+ 'instead, e.g. `record.timeless { record.save }`.',
141
+ locations
142
+ )
71
143
  counter = 0
72
144
  counter += 1 if self < Mongoid::Timestamps::Created
73
145
  counter += 1 if self < Mongoid::Timestamps::Updated
@@ -109,12 +181,14 @@ module Mongoid
109
181
  Timeless[name] = (counter == 0) ? nil : counter
110
182
  end
111
183
 
112
- # Returns whether the current class should skip timestamping.
184
+ # Returns whether the current class should skip timestamping. This is
185
+ # true when either a block-based timeless scope is active on the
186
+ # current thread/fiber, or the deprecated per-class counter is set.
113
187
  #
114
188
  # @return [ true | false ] Whether the current class should
115
189
  # skip timestamping.
116
190
  def timeless?
117
- !!Timeless[name]
191
+ Timeless.suppressing_timestamps? || !!Timeless[name]
118
192
  end
119
193
  end
120
194
  end
@@ -49,7 +49,7 @@ module Mongoid
49
49
 
50
50
  begin
51
51
  touches = _gather_touch_updates(Time.current, field)
52
- _root.send(:persist_atomic_operations, '$set' => touches) if touches.present?
52
+ _root.persist_atomic_operations('$set' => touches) if touches.present?
53
53
  _run_touch_callbacks_from_root
54
54
  ensure
55
55
  _clear_touch_updates(field)
@@ -104,6 +104,33 @@ module Mongoid
104
104
  run_callbacks(:touch)
105
105
  end
106
106
 
107
+ # The root-relative paths that a touch of this document's parent chain
108
+ # writes: the (possibly aliased) +updated_at+ field of each touchable
109
+ # node in the chain, plus this document's custom touch field, if any.
110
+ #
111
+ # @api private
112
+ #
113
+ # @param [ Symbol, String, nil ] field The custom touch field.
114
+ #
115
+ # @return [ Array<String> ] The touchable paths.
116
+ def _touchable_chain_paths(field = nil)
117
+ field = database_field_name(field) if field
118
+
119
+ node = self
120
+ paths = []
121
+ loop do
122
+ if node.respond_to?(:updated_at=)
123
+ paths << node.atomic_attribute_name(node.database_field_name(:updated_at))
124
+ end
125
+ paths << node.atomic_attribute_name(field) if node.equal?(self) && field
126
+
127
+ break unless node._touchable_parent?
128
+
129
+ node = node._parent
130
+ end
131
+ paths
132
+ end
133
+
107
134
  # Indicates whether the parent exists and is touchable.
108
135
  #
109
136
  # @api private
@@ -211,7 +211,7 @@ module Mongoid
211
211
  # @api private
212
212
  def self.add_discriminator_mapping(value, klass = self)
213
213
  self.discriminator_mapping ||= {}
214
- self.discriminator_mapping[value] = klass
214
+ discriminator_mapping[value] = klass
215
215
  superclass.add_discriminator_mapping(value, klass) if hereditary?
216
216
  end
217
217
 
@@ -225,7 +225,7 @@ module Mongoid
225
225
  #
226
226
  # @api private
227
227
  def self.get_discriminator_mapping(value)
228
- self.discriminator_mapping[value] if self.discriminator_mapping
228
+ discriminator_mapping[value] if discriminator_mapping
229
229
  end
230
230
  end
231
231
 
@@ -67,12 +67,13 @@ module Mongoid
67
67
  valid = document.validating do
68
68
  # Now, treating the target as an array, look at each element
69
69
  # and see if it is valid, but only if it has already been
70
- # persisted, or changed, and hasn't been flagged for destroy.
70
+ # persisted, or changed, and hasn't been flagged for destroy
71
+ # or already destroyed.
71
72
  #
72
73
  # use map.all? instead of just all?, because all? will do short-circuit
73
74
  # evaluation and terminate on the first failed validation.
74
75
  list.map do |value|
75
- if value && !value.flagged_for_destroy? && (!value.persisted? || value.changed?)
76
+ if needs_validation?(value)
76
77
  value.validated? || value.valid?
77
78
  else
78
79
  true
@@ -120,6 +121,21 @@ module Mongoid
120
121
  def get_target_documents_for_other(target)
121
122
  Array.wrap(target)
122
123
  end
124
+
125
+ # Returns true if the given value should be validated as part of
126
+ # an associated validation. Destroyed and flagged-for-destroy
127
+ # documents are skipped, as are persisted documents that haven't
128
+ # changed.
129
+ #
130
+ # @param [ Mongoid::Document | nil ] value The value to check.
131
+ #
132
+ # @return [ true | false ] Whether the value needs validation.
133
+ def needs_validation?(value)
134
+ value &&
135
+ !value.flagged_for_destroy? &&
136
+ !value.destroyed? &&
137
+ (!value.persisted? || value.changed?)
138
+ end
123
139
  end
124
140
  end
125
141
  end
@@ -5,5 +5,5 @@ module Mongoid
5
5
  #
6
6
  # Note that this file is automatically updated via `rake candidate:create`.
7
7
  # Manual changes to this file will be overwritten by that rake task.
8
- VERSION = '9.1.0'
8
+ VERSION = '9.1.2'
9
9
  end
@@ -36,5 +36,9 @@ module Mongoid
36
36
  warning :autosave_saves_unchanged_documents, "Autosave associations are currently configured to save documents even if they haven't changed. " \
37
37
  'This legacy behavior is deprecated. Set Mongoid.autosave_saves_unchanged_documents to false to ' \
38
38
  'skip saving unchanged documents in autosave associations.'
39
+ warning :bypass_auto_encryption,
40
+ 'auto_encryption_options includes bypass_auto_encryption: true. The driver ' \
41
+ 'will not encrypt anything for this client: fields declared with encrypt: true ' \
42
+ 'are written to the database unencrypted.'
39
43
  end
40
44
  end
@@ -435,7 +435,13 @@ describe 'Mongoid application tests' do
435
435
  end
436
436
  gemfile_lines << "gem 'mongoid', path: '#{File.expand_path(BASE)}'\n"
437
437
 
438
- # Rails 6.0 and 6.1 need logger gem on Ruby 2.7+ due to stdlib changes
438
+ # json 3.0 removed the quirks_mode and max_nesting keywords that
439
+ # ActiveSupport::JSON (every 7.x) still passes to JSON.generate and
440
+ # JSON.parse, so a freshly resolved bundle picks a 3.x json and the app
441
+ # fails to encode any response with ArgumentError: unknown keyword:
442
+ # quirks_mode. Cap json at 2.x, which still accepts those keywords.
443
+ gemfile_lines << "gem 'json', '< 3'\n" # Rails 6.0 and 6.1 need logger gem on Ruby 2.7+ due to stdlib changes
444
+
439
445
  gemfile_lines << "gem 'logger'\n" if rails_version && rails_version.to_f < 7.0 && RUBY_VERSION >= '2.7'
440
446
 
441
447
  if rails_version
@@ -99,6 +99,19 @@ describe 'belongs_to associations' do
99
99
  expect(team_manager.reload.unit_type).to eq 'group'
100
100
  expect(team_manager.unit).to eq team
101
101
  end
102
+
103
+ it 'eager-loads the corresponding unit when unit_type is a different alias' do
104
+ dept_manager.update unit_type: 'sandbox_dept'
105
+ team_manager.update unit_type: 'group'
106
+
107
+ managers = SandboxManager.where(:_id.in => [ dept_manager.id, team_manager.id ])
108
+ .includes(:unit)
109
+ .to_a
110
+ units_by_id = managers.to_h { |m| [ m.id, m.ivar(:unit) ] }
111
+
112
+ expect(units_by_id[dept_manager.id]).to eq department
113
+ expect(units_by_id[team_manager.id]).to eq team
114
+ end
102
115
  end
103
116
 
104
117
  context 'when the association uses the default resolver' do
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ module EmbeddedDelayedAtomicSetsSpec
6
+ class Section
7
+ include Mongoid::Document
8
+
9
+ field :content
10
+
11
+ embedded_in :parent, polymorphic: true
12
+ end
13
+
14
+ class Group
15
+ include Mongoid::Document
16
+
17
+ embedded_in :root
18
+ embeds_many :sections, class_name: 'EmbeddedDelayedAtomicSetsSpec::Section'
19
+ accepts_nested_attributes_for :sections
20
+ end
21
+
22
+ class Root
23
+ include Mongoid::Document
24
+
25
+ embeds_many :sections, class_name: 'EmbeddedDelayedAtomicSetsSpec::Section'
26
+ embeds_one :group, class_name: 'EmbeddedDelayedAtomicSetsSpec::Group'
27
+ accepts_nested_attributes_for :sections
28
+ accepts_nested_attributes_for :group
29
+ end
30
+ end
31
+
32
+ module NewEmbeddedChildSpec
33
+ class Member
34
+ include Mongoid::Document
35
+
36
+ field :name
37
+
38
+ embedded_in :team
39
+ end
40
+
41
+ class Team
42
+ include Mongoid::Document
43
+
44
+ embedded_in :org
45
+ embeds_many :members, class_name: 'NewEmbeddedChildSpec::Member'
46
+ accepts_nested_attributes_for :members
47
+ end
48
+
49
+ class Org
50
+ include Mongoid::Document
51
+
52
+ field :title
53
+
54
+ embeds_one :team, class_name: 'NewEmbeddedChildSpec::Team'
55
+ accepts_nested_attributes_for :team
56
+ end
57
+ end
58
+
59
+ RSpec.describe 'embedded delayed atomic sets' do
60
+ context 'with the same stored name' do
61
+ let!(:root) do
62
+ root = EmbeddedDelayedAtomicSetsSpec::Root.new
63
+ root.sections = [ EmbeddedDelayedAtomicSetsSpec::Section.new(content: 'root-original') ]
64
+ root.save!
65
+ root
66
+ end
67
+
68
+ def stored_document
69
+ EmbeddedDelayedAtomicSetsSpec::Root.collection.find(_id: root.id).first
70
+ end
71
+
72
+ def stored_contents(sections)
73
+ sections.to_a.map { |section| section['content'] }
74
+ end
75
+
76
+ def document_contents(sections)
77
+ sections.map(&:content)
78
+ end
79
+
80
+ # The persisted root has no group, so attributes= builds the new nested group
81
+ # while the root sections update is still pending.
82
+ it 'does not merge a new child into the parent association' do
83
+ reloaded = EmbeddedDelayedAtomicSetsSpec::Root.find(root.id)
84
+ reloaded.attributes = {
85
+ sections: [ { _id: reloaded.sections.first.id, content: 'root-updated' } ],
86
+ group: { sections: [ { content: 'group-new' } ] }
87
+ }
88
+ reloaded.save!
89
+
90
+ stored = stored_document
91
+ expect(stored_contents(stored['sections'])).to eq [ 'root-updated' ]
92
+ expect(stored_contents(stored.dig('group', 'sections'))).to eq [ 'group-new' ]
93
+ expect(document_contents(reloaded.sections)).to eq [ 'root-updated' ]
94
+ expect(document_contents(reloaded.group.sections)).to eq [ 'group-new' ]
95
+ end
96
+
97
+ it 'does not copy a new child into an empty parent association' do
98
+ reloaded = EmbeddedDelayedAtomicSetsSpec::Root.find(root.id)
99
+ reloaded.attributes = {
100
+ sections: [],
101
+ group: { sections: [ { content: 'group-new' } ] }
102
+ }
103
+ reloaded.save!
104
+
105
+ stored = stored_document
106
+ expect(stored).not_to have_key('sections')
107
+ expect(stored_contents(stored.dig('group', 'sections'))).to eq [ 'group-new' ]
108
+ expect(reloaded.sections).to be_empty
109
+ expect(document_contents(reloaded.group.sections)).to eq [ 'group-new' ]
110
+ end
111
+
112
+ it 'does not merge a new child when the parent association is unchanged' do
113
+ reloaded = EmbeddedDelayedAtomicSetsSpec::Root.find(root.id)
114
+ reloaded.attributes = { group: { sections: [ { content: 'group-new' } ] } }
115
+ reloaded.save!
116
+
117
+ stored = stored_document
118
+ expect(stored_contents(stored['sections'])).to eq [ 'root-original' ]
119
+ expect(stored_contents(stored.dig('group', 'sections'))).to eq [ 'group-new' ]
120
+ expect(document_contents(reloaded.sections)).to eq [ 'root-original' ]
121
+ expect(document_contents(reloaded.group.sections)).to eq [ 'group-new' ]
122
+ end
123
+
124
+ it 'updates an existing group and isolates a subsequently added group' do
125
+ root.group = EmbeddedDelayedAtomicSetsSpec::Group.new(
126
+ sections: [ EmbeddedDelayedAtomicSetsSpec::Section.new(content: 'group-original') ]
127
+ )
128
+ root.save!
129
+
130
+ reloaded = EmbeddedDelayedAtomicSetsSpec::Root.find(root.id)
131
+ reloaded.attributes = {
132
+ group: {
133
+ _id: reloaded.group.id,
134
+ sections: [ { _id: reloaded.group.sections.first.id, content: 'group-updated' } ]
135
+ }
136
+ }
137
+ reloaded.save!
138
+
139
+ stored = stored_document
140
+ expect(stored_contents(stored['sections'])).to eq [ 'root-original' ]
141
+ expect(stored_contents(stored.dig('group', 'sections'))).to eq [ 'group-updated' ]
142
+ expect(document_contents(reloaded.sections)).to eq [ 'root-original' ]
143
+ expect(document_contents(reloaded.group.sections)).to eq [ 'group-updated' ]
144
+
145
+ reloaded.group = nil
146
+ reloaded.save!
147
+ reloaded = EmbeddedDelayedAtomicSetsSpec::Root.find(root.id)
148
+ reloaded.attributes = {
149
+ sections: [ { _id: reloaded.sections.first.id, content: 'root-updated' } ],
150
+ group: { sections: [ { content: 'group-new' } ] }
151
+ }
152
+ reloaded.save!
153
+
154
+ stored = stored_document
155
+ expect(stored_contents(stored['sections'])).to eq [ 'root-updated' ]
156
+ expect(stored_contents(stored.dig('group', 'sections'))).to eq [ 'group-new' ]
157
+ expect(document_contents(reloaded.sections)).to eq [ 'root-updated' ]
158
+ expect(document_contents(reloaded.group.sections)).to eq [ 'group-new' ]
159
+ end
160
+
161
+ it 'does not merge a new child when updating the parent through nested attributes' do
162
+ reloaded = EmbeddedDelayedAtomicSetsSpec::Root.find(root.id)
163
+ reloaded.attributes = {
164
+ sections_attributes: {
165
+ '0' => { _id: reloaded.sections.first.id, content: 'root-updated' }
166
+ },
167
+ group: { sections: [ { content: 'group-new' } ] }
168
+ }
169
+ reloaded.save!
170
+
171
+ stored = stored_document
172
+ expect(stored_contents(stored['sections'])).to eq [ 'root-updated' ]
173
+ expect(stored_contents(stored.dig('group', 'sections'))).to eq [ 'group-new' ]
174
+ expect(document_contents(reloaded.sections)).to eq [ 'root-updated' ]
175
+ expect(document_contents(reloaded.group.sections)).to eq [ 'group-new' ]
176
+ end
177
+ end
178
+
179
+ context 'when assigning a new embeds_one child with nested embeds_many' do
180
+ let!(:org) { NewEmbeddedChildSpec::Org.create!(title: 't0') }
181
+
182
+ it 'does not write the nested array as a root-level field' do
183
+ reloaded = NewEmbeddedChildSpec::Org.find(org.id)
184
+ reloaded.attributes = { title: 't1', team: { members: [ { name: 'm1' } ] } }
185
+ reloaded.save!
186
+
187
+ stored = NewEmbeddedChildSpec::Org.collection.find(_id: org.id).first
188
+ expect(stored.keys).to match_array(%w[_id title team])
189
+ expect(stored.dig('team', 'members').map { |member| member['name'] }).to eq [ 'm1' ]
190
+ end
191
+
192
+ it 'does not write nested attributes at the root level' do
193
+ reloaded = NewEmbeddedChildSpec::Org.find(org.id)
194
+ reloaded.attributes = {
195
+ team_attributes: { members: [ { name: 'm1' } ] }
196
+ }
197
+ reloaded.save!
198
+
199
+ stored = NewEmbeddedChildSpec::Org.collection.find(_id: org.id).first
200
+ expect(stored.keys).to match_array(%w[_id title team])
201
+ expect(stored.dig('team', 'members').map { |member| member['name'] }).to eq [ 'm1' ]
202
+ expect(reloaded.team.members.map(&:name)).to eq [ 'm1' ]
203
+ end
204
+
205
+ it 'does not conflict when assigning to a newly built child' do
206
+ reloaded = NewEmbeddedChildSpec::Org.find(org.id)
207
+ reloaded.build_team
208
+ reloaded.team.attributes = { members: [ { name: 'm1' } ] }
209
+ reloaded.save!
210
+
211
+ stored = NewEmbeddedChildSpec::Org.collection.find(_id: org.id).first
212
+ expect(stored.dig('team', 'members').map { |member| member['name'] }).to eq [ 'm1' ]
213
+ expect(reloaded.team.members.map(&:name)).to eq [ 'm1' ]
214
+ end
215
+ end
216
+ end
@@ -60,6 +60,20 @@ module HabtmSpec
60
60
  end
61
61
 
62
62
  describe 'has_and_belongs_to_many associations' do
63
+ context 'when the stored foreign-key array is empty' do
64
+ it 'can reset and enumerate the association proxy' do
65
+ item = HabtmSpec::Item.create!(color_ids: [])
66
+ colors = item.reload.colors
67
+
68
+ expect_query(0) do
69
+ colors.reset
70
+ expect(colors.empty?).to be true
71
+ expect(colors.size).to be 0
72
+ expect(colors.to_a).to eq []
73
+ end
74
+ end
75
+ end
76
+
63
77
  context 'when an anonymous class defines a has_and_belongs_to_many association' do
64
78
  let(:klass) do
65
79
  Class.new do
@@ -90,8 +104,23 @@ describe 'has_and_belongs_to_many associations' do
90
104
  })
91
105
  end
92
106
 
93
- it 'does not raise on save' do
94
- expect { image_block.save! }.not_to raise_error
107
+ # The nested attributes are processed before the attachment_ids
108
+ # assignment is applied, so at that point the id is not yet in the
109
+ # association and resolving it requires a collection-wide lookup.
110
+ context 'when allow_reparenting_via_nested_attributes is false' do
111
+ config_override :allow_reparenting_via_nested_attributes, false
112
+
113
+ it 'raises a document not found error' do
114
+ expect { image_block.save! }.to raise_error(Mongoid::Errors::DocumentNotFound)
115
+ end
116
+ end
117
+
118
+ context 'when allow_reparenting_via_nested_attributes is true' do
119
+ config_override :allow_reparenting_via_nested_attributes, true
120
+
121
+ it 'does not raise on save' do
122
+ expect { image_block.save! }.not_to raise_error
123
+ end
95
124
  end
96
125
  end
97
126