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
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Mongoid::Association::Referenced::HasOneThrough::Eager do
6
+ before(:all) do
7
+ Object.const_set(:EgCustomer, Class.new do
8
+ include Mongoid::Document
9
+
10
+ store_in collection: 'eg_customers'
11
+ has_one :eg_franchise, class_name: 'EgFranchise', inverse_of: :eg_customer
12
+ has_one :eg_store, through: :eg_franchise, class_name: 'EgStore'
13
+ end)
14
+ Object.const_set(:EgFranchise, Class.new do
15
+ include Mongoid::Document
16
+
17
+ store_in collection: 'eg_franchises'
18
+ belongs_to :eg_customer, class_name: 'EgCustomer'
19
+ has_one :eg_store, class_name: 'EgStore', inverse_of: :eg_franchise
20
+ end)
21
+ Object.const_set(:EgStore, Class.new do
22
+ include Mongoid::Document
23
+
24
+ store_in collection: 'eg_stores'
25
+ belongs_to :eg_franchise, class_name: 'EgFranchise'
26
+ end)
27
+ end
28
+
29
+ after(:all) do
30
+ %w[EgCustomer EgFranchise EgStore].each { |c| Object.send(:remove_const, c) }
31
+ end
32
+
33
+ before { [ EgCustomer, EgFranchise, EgStore ].each(&:delete_all) }
34
+
35
+ it 'preloads the through association for multiple owners' do
36
+ c1 = EgCustomer.create!
37
+ c2 = EgCustomer.create!
38
+ f1 = EgFranchise.create!(eg_customer: c1)
39
+ f2 = EgFranchise.create!(eg_customer: c2)
40
+ s1 = EgStore.create!(eg_franchise: f1)
41
+ s2 = EgStore.create!(eg_franchise: f2)
42
+
43
+ customers = EgCustomer.includes(:eg_store).to_a
44
+ by_id = customers.index_by(&:id)
45
+
46
+ expect(by_id[c1.id].eg_store).to eq(s1)
47
+ expect(by_id[c2.id].eg_store).to eq(s2)
48
+ end
49
+
50
+ it 'sets nil on owners without a store' do
51
+ lone = EgCustomer.create!
52
+ EgFranchise.create!(eg_customer: lone) # franchise but no store
53
+
54
+ customers = EgCustomer.includes(:eg_store).to_a
55
+ loaded = customers.find { |c| c.id == lone.id }
56
+ expect(loaded.eg_store).to be_nil
57
+ end
58
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Mongoid::Association::Referenced::HasOneThrough::Proxy do
6
+ before(:all) do
7
+ Object.const_set(:PxHotStore, Class.new { include Mongoid::Document })
8
+ Object.const_set(:PxHotFranchise, Class.new do
9
+ include Mongoid::Document
10
+
11
+ has_one :px_hot_store, class_name: 'PxHotStore', inverse_of: :px_hot_franchise
12
+ end)
13
+ Object.const_set(:PxHotCustomer, Class.new do
14
+ include Mongoid::Document
15
+
16
+ has_one :px_hot_franchise, class_name: 'PxHotFranchise',
17
+ inverse_of: :px_hot_customer
18
+ has_one :px_hot_store, through: :px_hot_franchise, class_name: 'PxHotStore',
19
+ source: :px_hot_store
20
+ end)
21
+ end
22
+
23
+ after(:all) do
24
+ %w[PxHotCustomer PxHotFranchise PxHotStore].each { |c| Object.send(:remove_const, c) }
25
+ end
26
+
27
+ let(:customer) { PxHotCustomer.new }
28
+
29
+ describe 'setter' do
30
+ it 'raises ReadonlyAssociation' do
31
+ expect { customer.px_hot_store = PxHotStore.new }.to \
32
+ raise_error(Mongoid::Errors::ReadonlyAssociation)
33
+ end
34
+ end
35
+
36
+ describe '.eager_loader' do
37
+ it 'returns a HasOneThrough::Eager' do
38
+ assoc = PxHotCustomer.relations['px_hot_store']
39
+ result = described_class.eager_loader([ assoc ], [])
40
+ expect(result).to be_a(Mongoid::Association::Referenced::HasOneThrough::Eager)
41
+ end
42
+ end
43
+
44
+ describe '.embedded?' do
45
+ it 'returns false' do
46
+ expect(described_class.embedded?).to be false
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Mongoid::Association::Referenced::HasOneThrough do
6
+ # Model setup:
7
+ # HotCustomer has_one :hot_franchise
8
+ # HotCustomer has_one :hot_store, through: :hot_franchise, source: :depot
9
+ # HotFranchise has_one :depot (class HotStore)
10
+
11
+ before(:all) do
12
+ Object.const_set(:HotStore, Class.new do
13
+ include Mongoid::Document
14
+
15
+ field :hot_franchise_id, type: BSON::ObjectId
16
+ end)
17
+ Object.const_set(:HotFranchise, Class.new do
18
+ include Mongoid::Document
19
+
20
+ field :hot_customer_id, type: BSON::ObjectId
21
+ has_one :depot, class_name: 'HotStore', inverse_of: :hot_franchise
22
+ end)
23
+ Object.const_set(:HotCustomer, Class.new do
24
+ include Mongoid::Document
25
+
26
+ has_one :hot_franchise, class_name: 'HotFranchise', inverse_of: :hot_customer
27
+ has_one :hot_store, through: :hot_franchise,
28
+ class_name: 'HotStore', source: :depot
29
+ end)
30
+ end
31
+
32
+ after(:all) do
33
+ %w[HotCustomer HotFranchise HotStore].each { |c| Object.send(:remove_const, c) }
34
+ end
35
+
36
+ let(:assoc) { HotCustomer.relations['hot_store'] }
37
+
38
+ describe '#through_association' do
39
+ it 'returns the intermediate association metadata' do
40
+ expect(assoc.through_association).to eq(HotCustomer.relations['hot_franchise'])
41
+ end
42
+ end
43
+
44
+ describe '#source_association' do
45
+ it 'returns the :depot association on HotFranchise' do
46
+ expect(assoc.source_association).to eq(HotFranchise.relations['depot'])
47
+ end
48
+ end
49
+
50
+ describe '#embedded?' do
51
+ it 'returns false' do
52
+ expect(assoc.embedded?).to be false
53
+ end
54
+ end
55
+
56
+ describe 'VALID_OPTIONS' do
57
+ it 'accepts :through' do
58
+ expect { HotCustomer.has_one(:foo, through: :hot_franchise) }.not_to raise_error
59
+ end
60
+
61
+ it 'accepts :source' do
62
+ expect { HotCustomer.has_one(:bar, through: :hot_franchise, source: :depot) }.not_to raise_error
63
+ end
64
+
65
+ it 'rejects unknown options at definition time' do
66
+ expect do
67
+ HotCustomer.has_one(:baz, through: :hot_franchise, bogus_option: true)
68
+ end.to raise_error(Mongoid::Errors::InvalidRelationOption)
69
+ end
70
+ end
71
+
72
+ describe '#criteria' do
73
+ it 'returns nil when the intermediate is nil' do
74
+ customer = HotCustomer.new
75
+ allow(customer).to receive(:hot_franchise).and_return(nil)
76
+ expect(assoc.criteria(customer)).to be_nil
77
+ end
78
+
79
+ it 'delegates to the source association on the intermediate' do
80
+ franchise = HotFranchise.new
81
+ store = HotStore.new
82
+ customer = HotCustomer.new
83
+ allow(customer).to receive(:hot_franchise).and_return(franchise)
84
+ allow(franchise).to receive(:depot).and_return(store)
85
+ expect(assoc.criteria(customer)).to eq(store)
86
+ end
87
+ end
88
+
89
+ describe ':through on an embedded association' do
90
+ it 'raises InvalidRelationOption when through association is embedded' do
91
+ embedded_owner = Class.new do
92
+ include Mongoid::Document
93
+
94
+ embeds_one :address
95
+ end
96
+ embedded_owner.has_one(:city, through: :address)
97
+ expect do
98
+ embedded_owner.relations['city'].through_association # triggers lazy validation
99
+ end.to raise_error(Mongoid::Errors::InvalidRelationOption)
100
+ end
101
+ end
102
+
103
+ context 'integration', :integration do
104
+ before(:all) do
105
+ Object.const_set(:IntCustomer, Class.new do
106
+ include Mongoid::Document
107
+
108
+ store_in collection: 'int_customers'
109
+ has_one :int_franchise, class_name: 'IntFranchise', inverse_of: :int_customer
110
+ # source: :depot exercises the :source option end-to-end
111
+ has_one :int_store, through: :int_franchise, class_name: 'IntStore', source: :depot
112
+ end)
113
+
114
+ Object.const_set(:IntFranchise, Class.new do
115
+ include Mongoid::Document
116
+
117
+ store_in collection: 'int_franchises'
118
+ field :int_customer_id, type: BSON::ObjectId
119
+ belongs_to :int_customer, class_name: 'IntCustomer'
120
+ has_one :depot, class_name: 'IntStore', inverse_of: :int_franchise
121
+ end)
122
+
123
+ Object.const_set(:IntStore, Class.new do
124
+ include Mongoid::Document
125
+
126
+ store_in collection: 'int_stores'
127
+ field :int_franchise_id, type: BSON::ObjectId
128
+ belongs_to :int_franchise, class_name: 'IntFranchise'
129
+ end)
130
+ end
131
+
132
+ after(:all) do
133
+ %w[IntCustomer IntFranchise IntStore].each { |c| Object.send(:remove_const, c) }
134
+ end
135
+
136
+ before { [ IntCustomer, IntFranchise, IntStore ].each(&:delete_all) }
137
+
138
+ let!(:customer) { IntCustomer.create! }
139
+ let!(:franchise) { IntFranchise.create!(int_customer: customer) }
140
+ let!(:store) { IntStore.create!(int_franchise: franchise) }
141
+
142
+ describe 'getter' do
143
+ it 'returns the store via the franchise' do
144
+ expect(customer.int_store).to eq(store)
145
+ end
146
+
147
+ it 'returns nil when the franchise is absent' do
148
+ lone = IntCustomer.create!
149
+ expect(lone.int_store).to be_nil
150
+ end
151
+
152
+ it 'returns cached value before reload and fresh value after' do
153
+ first_store = customer.int_store # prime cache
154
+ expect(first_store).to eq(store)
155
+
156
+ new_store = IntStore.create!
157
+ franchise.depot = new_store
158
+ franchise.save!
159
+
160
+ expect(customer.int_store).to eq(store) # cached — still old
161
+ expect(customer.int_store(true)).to eq(new_store) # reloaded
162
+ end
163
+ end
164
+
165
+ describe 'setter' do
166
+ it 'raises ReadonlyAssociation' do
167
+ expect { customer.int_store = IntStore.new }.to \
168
+ raise_error(Mongoid::Errors::ReadonlyAssociation)
169
+ end
170
+ end
171
+ end
172
+
173
+ context 'with :source option resolving a differently-named association', :integration do
174
+ before(:all) do
175
+ Object.const_set(:SrcOrg, Class.new do
176
+ include Mongoid::Document
177
+
178
+ store_in collection: 'src_orgs'
179
+ has_one :src_location, class_name: 'SrcLocation', inverse_of: :src_org
180
+ has_one :src_hq, through: :src_location,
181
+ class_name: 'SrcBuilding', source: :src_main_building
182
+ end)
183
+ Object.const_set(:SrcLocation, Class.new do
184
+ include Mongoid::Document
185
+
186
+ store_in collection: 'src_locations'
187
+ belongs_to :src_org, class_name: 'SrcOrg'
188
+ has_one :src_main_building, class_name: 'SrcBuilding', inverse_of: :src_location
189
+ end)
190
+ Object.const_set(:SrcBuilding, Class.new do
191
+ include Mongoid::Document
192
+
193
+ store_in collection: 'src_buildings'
194
+ belongs_to :src_location, class_name: 'SrcLocation'
195
+ end)
196
+ end
197
+
198
+ after(:all) do
199
+ %w[SrcOrg SrcLocation SrcBuilding].each { |c| Object.send(:remove_const, c) }
200
+ end
201
+
202
+ before { [ SrcOrg, SrcLocation, SrcBuilding ].each(&:delete_all) }
203
+
204
+ it 'resolves the :source association on the intermediate' do
205
+ org = SrcOrg.create!
206
+ location = SrcLocation.create!(src_org: org)
207
+ building = SrcBuilding.create!(src_location: location)
208
+ expect(org.src_hq).to eq(building)
209
+ end
210
+ end
211
+ end
@@ -74,6 +74,78 @@ describe Mongoid::Atomic::Modifiers do
74
74
  end
75
75
  end
76
76
  end
77
+
78
+ context 'when a conflicting modification exists' do
79
+ context 'when the conflicting modification is a set' do
80
+ let(:sets) do
81
+ { 'addresses.0.street' => 'Bond' }
82
+ end
83
+
84
+ let(:adds) do
85
+ { 'addresses.0.tags' => %w[tag] }
86
+ end
87
+
88
+ before do
89
+ modifiers.set(sets)
90
+ modifiers.add_to_set(adds)
91
+ end
92
+
93
+ it 'adds the add to set modifiers to the conflicts hash' do
94
+ expect(modifiers).to eq(
95
+ { '$set' => { 'addresses.0.street' => 'Bond' },
96
+ conflicts: { '$addToSet' =>
97
+ { 'addresses.0.tags' => { '$each' => %w[tag] } } } }
98
+ )
99
+ end
100
+ end
101
+
102
+ context 'when the conflicting modification is a push' do
103
+ let(:pushes) do
104
+ { 'addresses' => { 'street' => 'Oxford St' } }
105
+ end
106
+
107
+ let(:adds) do
108
+ { 'addresses.0.tags' => %w[tag] }
109
+ end
110
+
111
+ before do
112
+ modifiers.push(pushes)
113
+ modifiers.add_to_set(adds)
114
+ end
115
+
116
+ it 'adds the add to set modifiers to the conflicts hash' do
117
+ expect(modifiers).to eq(
118
+ { '$push' => { 'addresses' => { '$each' => [ { 'street' => 'Oxford St' } ] } },
119
+ conflicts: { '$addToSet' =>
120
+ { 'addresses.0.tags' => { '$each' => %w[tag] } } } }
121
+ )
122
+ end
123
+ end
124
+
125
+ context 'when the conflicting modification is a pull' do
126
+ let(:pulls) do
127
+ { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } }
128
+ end
129
+
130
+ let(:adds) do
131
+ { 'addresses.0.tags' => %w[tag] }
132
+ end
133
+
134
+ before do
135
+ modifiers.pull(pulls)
136
+ modifiers.add_to_set(adds)
137
+ end
138
+
139
+ it 'does not split the operations, leaving MongoDB to reject the update' do
140
+ # A $pull shifts array elements, so applying the $addToSet in a
141
+ # separate write afterwards could target a different element.
142
+ expect(modifiers).to eq(
143
+ { '$pull' => { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } },
144
+ '$addToSet' => { 'addresses.0.tags' => { '$each' => %w[tag] } } }
145
+ )
146
+ end
147
+ end
148
+ end
77
149
  end
78
150
 
79
151
  describe '#pull' do
@@ -125,6 +197,98 @@ describe Mongoid::Atomic::Modifiers do
125
197
  end
126
198
  end
127
199
  end
200
+
201
+ context 'when a conflicting modification exists' do
202
+ # A $pull shifts the positions of the remaining elements, so it stays in
203
+ # the main update. MongoDB rejects the update if the paths overlap.
204
+ context 'when the conflicting modification is a set' do
205
+ let(:sets) do
206
+ { 'addresses.0.street' => 'Bond' }
207
+ end
208
+
209
+ let(:pulls) do
210
+ { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } }
211
+ end
212
+
213
+ before do
214
+ modifiers.set(sets)
215
+ modifiers.pull(pulls)
216
+ end
217
+
218
+ it 'keeps the pull modifiers in the main update' do
219
+ expect(modifiers).to eq(
220
+ { '$set' => { 'addresses.0.street' => 'Bond' },
221
+ '$pull' => { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } } }
222
+ )
223
+ end
224
+ end
225
+
226
+ context 'when the conflicting modification is a push' do
227
+ let(:pushes) do
228
+ { 'addresses' => { 'street' => 'Oxford St' } }
229
+ end
230
+
231
+ let(:pulls) do
232
+ { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } }
233
+ end
234
+
235
+ before do
236
+ modifiers.push(pushes)
237
+ modifiers.pull(pulls)
238
+ end
239
+
240
+ it 'keeps the pull modifiers in the main update' do
241
+ expect(modifiers).to eq(
242
+ { '$push' => { 'addresses' => { '$each' => [ { 'street' => 'Oxford St' } ] } },
243
+ '$pull' => { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } } }
244
+ )
245
+ end
246
+ end
247
+
248
+ context 'when the conflicting modification is an add to set' do
249
+ let(:adds) do
250
+ { 'addresses.0.tags' => %w[tag] }
251
+ end
252
+
253
+ let(:pulls) do
254
+ { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } }
255
+ end
256
+
257
+ before do
258
+ modifiers.add_to_set(adds)
259
+ modifiers.pull(pulls)
260
+ end
261
+
262
+ it 'keeps the pull modifiers in the main update' do
263
+ expect(modifiers).to eq(
264
+ { '$addToSet' => { 'addresses.0.tags' => { '$each' => %w[tag] } },
265
+ '$pull' => { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } } }
266
+ )
267
+ end
268
+ end
269
+
270
+ context 'when the conflicting modification is a pull all' do
271
+ let(:pull_alls) do
272
+ { 'addresses' => [ { '_id' => 'one' } ] }
273
+ end
274
+
275
+ let(:pulls) do
276
+ { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } }
277
+ end
278
+
279
+ before do
280
+ modifiers.pull_all(pull_alls)
281
+ modifiers.pull(pulls)
282
+ end
283
+
284
+ it 'keeps the pull modifiers in the main update' do
285
+ expect(modifiers).to eq(
286
+ { '$pullAll' => { 'addresses' => [ { '_id' => 'one' } ] },
287
+ '$pull' => { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } } }
288
+ )
289
+ end
290
+ end
291
+ end
128
292
  end
129
293
 
130
294
  describe '#pull_all' do
@@ -183,6 +347,54 @@ describe Mongoid::Atomic::Modifiers do
183
347
  end
184
348
  end
185
349
  end
350
+
351
+ context 'when a conflicting modification exists' do
352
+ context 'when the conflicting modification is a pull' do
353
+ let(:pulls) do
354
+ { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } }
355
+ end
356
+
357
+ let(:pull_alls) do
358
+ { 'addresses' => [ { '_id' => 'one' } ] }
359
+ end
360
+
361
+ before do
362
+ modifiers.pull(pulls)
363
+ modifiers.pull_all(pull_alls)
364
+ end
365
+
366
+ it 'adds the pull all modifiers to the conflicts hash' do
367
+ expect(modifiers).to eq(
368
+ { '$pull' => { 'addresses' => { '_id' => { '$in' => [ 'one' ] } } },
369
+ conflicts: { '$pullAll' =>
370
+ { 'addresses' => [ { '_id' => 'one' } ] } } }
371
+ )
372
+ end
373
+ end
374
+
375
+ context 'when the conflicting modification is a push' do
376
+ let(:pushes) do
377
+ { 'addresses' => { 'street' => 'Oxford St' } }
378
+ end
379
+
380
+ let(:pull_alls) do
381
+ { 'addresses' => [ { '_id' => 'one' } ] }
382
+ end
383
+
384
+ before do
385
+ modifiers.push(pushes)
386
+ modifiers.pull_all(pull_alls)
387
+ end
388
+
389
+ it 'adds the pull all modifiers to the conflicts hash' do
390
+ expect(modifiers).to eq(
391
+ { '$push' => { 'addresses' => { '$each' => [ { 'street' => 'Oxford St' } ] } },
392
+ conflicts: { '$pullAll' =>
393
+ { 'addresses' => [ { '_id' => 'one' } ] } } }
394
+ )
395
+ end
396
+ end
397
+ end
186
398
  end
187
399
 
188
400
  describe '#push' do
@@ -321,6 +533,56 @@ describe Mongoid::Atomic::Modifiers do
321
533
  )
322
534
  end
323
535
  end
536
+
537
+ context 'when the conflicting modification is an add to set' do
538
+ let(:adds) do
539
+ { 'addresses.0.tags' => %w[tag] }
540
+ end
541
+
542
+ let(:pushes) do
543
+ { 'addresses' => { 'street' => 'Oxford St' } }
544
+ end
545
+
546
+ before do
547
+ modifiers.add_to_set(adds)
548
+ modifiers.push(pushes)
549
+ end
550
+
551
+ it 'adds the push modifiers to the conflicts hash' do
552
+ expect(modifiers).to eq(
553
+ { '$addToSet' => { 'addresses.0.tags' => { '$each' => %w[tag] } },
554
+ conflicts: { '$push' =>
555
+ { 'addresses' => { '$each' => [
556
+ { 'street' => 'Oxford St' }
557
+ ] } } } }
558
+ )
559
+ end
560
+ end
561
+
562
+ context 'when the conflicting modification is an unset' do
563
+ let(:unsets) do
564
+ [ 'addresses.0.street' ]
565
+ end
566
+
567
+ let(:pushes) do
568
+ { 'addresses' => { 'street' => 'Oxford St' } }
569
+ end
570
+
571
+ before do
572
+ modifiers.unset(unsets)
573
+ modifiers.push(pushes)
574
+ end
575
+
576
+ it 'adds the push modifiers to the conflicts hash' do
577
+ expect(modifiers).to eq(
578
+ { '$unset' => { 'addresses.0.street' => true },
579
+ conflicts: { '$push' =>
580
+ { 'addresses' => { '$each' => [
581
+ { 'street' => 'Oxford St' }
582
+ ] } } } }
583
+ )
584
+ end
585
+ end
324
586
  end
325
587
 
326
588
  context 'when there is another operation on a sibling of a key which is nested' do
@@ -419,6 +681,30 @@ describe Mongoid::Atomic::Modifiers do
419
681
  )
420
682
  end
421
683
  end
684
+
685
+ context 'when the conflicting modification is an add to set' do
686
+ let(:adds) do
687
+ { 'addresses.0.tags' => %w[tag] }
688
+ end
689
+
690
+ let(:sets) do
691
+ { 'addresses.0.title' => 'Sir' }
692
+ end
693
+
694
+ before do
695
+ modifiers.add_to_set(adds)
696
+ modifiers.set(sets)
697
+ end
698
+
699
+ it 'adds the set modifiers to the conflicts hash' do
700
+ expect(modifiers).to eq(
701
+ { '$addToSet' =>
702
+ { 'addresses.0.tags' => { '$each' => %w[tag] } },
703
+ conflicts:
704
+ { '$set' => { 'addresses.0.title' => 'Sir' } } }
705
+ )
706
+ end
707
+ end
422
708
  end
423
709
  end
424
710
 
@@ -488,6 +774,67 @@ describe Mongoid::Atomic::Modifiers do
488
774
  end
489
775
  end
490
776
  end
777
+
778
+ context 'when the unsets conflict with a prior push' do
779
+ # A $push appends to the array and does not shift existing element
780
+ # positions, so the $unset can be applied safely in a separate write.
781
+ let(:pushes) { { 'books' => { 'title' => '1984' } } }
782
+
783
+ let(:unsets) { [ 'books.0.title' ] }
784
+
785
+ before do
786
+ modifiers.push(pushes)
787
+ modifiers.unset(unsets)
788
+ end
789
+
790
+ it 'adds the unset modifiers to the conflicts hash' do
791
+ expect(modifiers).to eq(
792
+ { '$push' => { 'books' => { '$each' => [ { 'title' => '1984' } ] } },
793
+ conflicts: { '$unset' => { 'books.0.title' => true } } }
794
+ )
795
+ end
796
+ end
797
+
798
+ context 'when the unsets conflict with a prior add to set' do
799
+ let(:adds) { { 'books.0.tags' => %w[tag] } }
800
+
801
+ let(:unsets) { [ 'books.0.title' ] }
802
+
803
+ before do
804
+ modifiers.add_to_set(adds)
805
+ modifiers.unset(unsets)
806
+ end
807
+
808
+ it 'adds the unset modifiers to the conflicts hash' do
809
+ expect(modifiers).to eq(
810
+ { '$addToSet' => { 'books.0.tags' => { '$each' => %w[tag] } },
811
+ conflicts: { '$unset' => { 'books.0.title' => true } } }
812
+ )
813
+ end
814
+ end
815
+
816
+ context 'when there is a pending pull on the same field' do
817
+ # A $pull shifts array elements, so routing the $unset into a separate
818
+ # write could make it target a different element. The operations are
819
+ # left together so MongoDB rejects the update (code 40) atomically.
820
+ let(:pulls) do
821
+ { 'books' => { '_id' => { '$in' => [ 'one' ] } } }
822
+ end
823
+
824
+ let(:unsets) { [ 'books.0.title' ] }
825
+
826
+ before do
827
+ modifiers.pull(pulls)
828
+ modifiers.unset(unsets)
829
+ end
830
+
831
+ it 'does not split the operations' do
832
+ expect(modifiers).to eq(
833
+ { '$pull' => { 'books' => { '_id' => { '$in' => [ 'one' ] } } },
834
+ '$unset' => { 'books.0.title' => true } }
835
+ )
836
+ end
837
+ end
491
838
  end
492
839
  end
493
840
  end