mongoid 9.1.0 → 9.1.1

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 (126) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +49 -0
  3. data/lib/mongoid/association/accessors.rb +9 -1
  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 +57 -100
  14. data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -1
  15. data/lib/mongoid/association/embedded/embeds_one.rb +1 -0
  16. data/lib/mongoid/association/macros.rb +13 -1
  17. data/lib/mongoid/association/nested/many.rb +34 -5
  18. data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
  19. data/lib/mongoid/association/nested/one.rb +6 -1
  20. data/lib/mongoid/association/options.rb +17 -0
  21. data/lib/mongoid/association/referenced/belongs_to/eager.rb +6 -1
  22. data/lib/mongoid/association/referenced/belongs_to.rb +1 -0
  23. data/lib/mongoid/association/referenced/counter_cache.rb +3 -3
  24. data/lib/mongoid/association/referenced/has_and_belongs_to_many.rb +5 -0
  25. data/lib/mongoid/association/referenced/has_many/enumerable.rb +3 -1
  26. data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
  27. data/lib/mongoid/association/referenced/has_many_through/eager.rb +99 -0
  28. data/lib/mongoid/association/referenced/has_many_through/proxy.rb +68 -0
  29. data/lib/mongoid/association/referenced/has_many_through.rb +201 -0
  30. data/lib/mongoid/association/referenced/has_one.rb +1 -0
  31. data/lib/mongoid/association/referenced/has_one_through/eager.rb +65 -0
  32. data/lib/mongoid/association/referenced/has_one_through/proxy.rb +30 -0
  33. data/lib/mongoid/association/referenced/has_one_through.rb +167 -0
  34. data/lib/mongoid/association/referenced.rb +2 -0
  35. data/lib/mongoid/association/relatable.rb +40 -0
  36. data/lib/mongoid/association.rb +7 -0
  37. data/lib/mongoid/clients/factory.rb +7 -13
  38. data/lib/mongoid/collection_configurable.rb +8 -0
  39. data/lib/mongoid/config/defaults.rb +0 -1
  40. data/lib/mongoid/config/encryption.rb +36 -19
  41. data/lib/mongoid/config.rb +56 -5
  42. data/lib/mongoid/contextual/aggregable/memory.rb +6 -2
  43. data/lib/mongoid/contextual/memory.rb +18 -7
  44. data/lib/mongoid/criteria/includable.rb +35 -11
  45. data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
  46. data/lib/mongoid/criteria/queryable/selectable.rb +123 -7
  47. data/lib/mongoid/deprecable.rb +45 -2
  48. data/lib/mongoid/encryptable.rb +46 -0
  49. data/lib/mongoid/errors/config_redactor.rb +43 -0
  50. data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
  51. data/lib/mongoid/errors/mixed_client_configuration.rb +1 -1
  52. data/lib/mongoid/errors/no_client_database.rb +1 -1
  53. data/lib/mongoid/errors/no_client_hosts.rb +1 -1
  54. data/lib/mongoid/errors/no_encryption_schema.rb +28 -0
  55. data/lib/mongoid/errors/readonly_association.rb +29 -0
  56. data/lib/mongoid/errors.rb +4 -0
  57. data/lib/mongoid/extensions/vector.rb +64 -0
  58. data/lib/mongoid/extensions.rb +1 -0
  59. data/lib/mongoid/field_readable.rb +69 -0
  60. data/lib/mongoid/indexable.rb +38 -26
  61. data/lib/mongoid/matchable.rb +6 -1
  62. data/lib/mongoid/matcher/eq_impl_with_regexp.rb +2 -4
  63. data/lib/mongoid/matcher/regex.rb +11 -12
  64. data/lib/mongoid/matcher/regexp_budget.rb +372 -0
  65. data/lib/mongoid/matcher.rb +1 -0
  66. data/lib/mongoid/persistable/creatable.rb +1 -1
  67. data/lib/mongoid/persistable/updatable.rb +9 -11
  68. data/lib/mongoid/persistence_context.rb +35 -0
  69. data/lib/mongoid/search_indexable.rb +64 -23
  70. data/lib/mongoid/serializable.rb +12 -4
  71. data/lib/mongoid/tasks/database.rb +23 -9
  72. data/lib/mongoid/threaded.rb +36 -0
  73. data/lib/mongoid/timestamps/timeless.rb +84 -10
  74. data/lib/mongoid/traversable.rb +2 -2
  75. data/lib/mongoid/validatable/associated.rb +18 -2
  76. data/lib/mongoid/version.rb +1 -1
  77. data/spec/integration/app_spec.rb +7 -1
  78. data/spec/integration/associations/belongs_to_spec.rb +13 -0
  79. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +17 -2
  80. data/spec/integration/dots_and_dollars_spec.rb +12 -2
  81. data/spec/integration/encryption_spec.rb +151 -0
  82. data/spec/integration/matcher_operator_data/regex.yml +21 -0
  83. data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
  84. data/spec/integration/query_operator_guard_spec.rb +89 -0
  85. data/spec/mongoid/association/eager_loadable_spec.rb +47 -0
  86. data/spec/mongoid/association/eager_spec.rb +2 -2
  87. data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +33 -0
  88. data/spec/mongoid/association/fallback_spec.rb +425 -0
  89. data/spec/mongoid/association/macros_spec.rb +26 -0
  90. data/spec/mongoid/association/referenced/belongs_to/eager_spec.rb +17 -0
  91. data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
  92. data/spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb +50 -0
  93. data/spec/mongoid/association/referenced/has_many/eager_spec.rb +33 -0
  94. data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
  95. data/spec/mongoid/association/referenced/has_many_through/eager_spec.rb +58 -0
  96. data/spec/mongoid/association/referenced/has_many_through/proxy_spec.rb +62 -0
  97. data/spec/mongoid/association/referenced/has_many_through_spec.rb +399 -0
  98. data/spec/mongoid/association/referenced/has_one_through/eager_spec.rb +58 -0
  99. data/spec/mongoid/association/referenced/has_one_through/proxy_spec.rb +49 -0
  100. data/spec/mongoid/association/referenced/has_one_through_spec.rb +211 -0
  101. data/spec/mongoid/attributes/nested_spec.rb +249 -8
  102. data/spec/mongoid/config/defaults_spec.rb +33 -0
  103. data/spec/mongoid/config/encryption_spec.rb +228 -0
  104. data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
  105. data/spec/mongoid/contextual/memory_spec.rb +177 -0
  106. data/spec/mongoid/criteria/includable_spec.rb +875 -0
  107. data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +15 -1
  108. data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +201 -0
  109. data/spec/mongoid/criteria_spec.rb +3 -0
  110. data/spec/mongoid/encryptable_spec.rb +61 -0
  111. data/spec/mongoid/errors/config_redactor_spec.rb +112 -0
  112. data/spec/mongoid/errors/mixed_client_configuration_spec.rb +31 -0
  113. data/spec/mongoid/errors/no_client_database_spec.rb +29 -0
  114. data/spec/mongoid/errors/no_client_hosts_spec.rb +26 -0
  115. data/spec/mongoid/errors/readonly_association_spec.rb +34 -0
  116. data/spec/mongoid/extensions/vector_spec.rb +175 -0
  117. data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
  118. data/spec/mongoid/search_indexable_spec.rb +295 -9
  119. data/spec/mongoid/tasks/database_spec.rb +18 -0
  120. data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
  121. data/spec/mongoid/touchable_spec.rb +25 -0
  122. data/spec/mongoid/touchable_spec_models.rb +10 -0
  123. data/spec/mongoid/validatable/associated_spec.rb +19 -0
  124. data/spec/spec_helper.rb +5 -1
  125. data/spec/support/crypt/models.rb +157 -0
  126. metadata +52 -2
@@ -0,0 +1,425 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'associations with the :fallback option' do
4
+ before(:all) do
5
+ class Anonymous
6
+ def attribution
7
+ 'Composer unknown'
8
+ end
9
+ end
10
+ end
11
+
12
+ after(:all) do
13
+ Object.send(:remove_const, :Anonymous)
14
+ end
15
+
16
+ context 'belongs_to' do
17
+ before(:all) do
18
+ class Composer
19
+ include Mongoid::Document
20
+
21
+ field :name
22
+
23
+ def attribution
24
+ "Composed by #{name}"
25
+ end
26
+ end
27
+
28
+ class Symphony
29
+ include Mongoid::Document
30
+
31
+ belongs_to :composer, fallback: -> { Anonymous.new }
32
+ end
33
+ end
34
+
35
+ after(:all) do
36
+ Object.send(:remove_const, :Symphony)
37
+ Object.send(:remove_const, :Composer)
38
+ end
39
+
40
+ it 'returns a fresh null object instance on every access when the association is nil' do
41
+ symphony = Symphony.create!
42
+
43
+ expect(symphony.composer.attribution).to eq('Composer unknown')
44
+ expect(symphony.composer).not_to equal(symphony.composer)
45
+ end
46
+
47
+ it 'returns the real associated document when the FK is set' do
48
+ symphony = Symphony.create!(composer: Composer.create!(name: 'Mahler'))
49
+
50
+ expect(Symphony.find(symphony.id).composer.attribution).to eq('Composed by Mahler')
51
+ end
52
+
53
+ it 'treats a direct null object assignment as nil' do
54
+ symphony = Symphony.create!
55
+
56
+ symphony.composer = Anonymous.new
57
+ symphony.save!
58
+
59
+ expect(Symphony.find(symphony.id).composer_id).to be_nil
60
+ end
61
+
62
+ it 'treats a null object passed via the constructor as nil' do
63
+ symphony = Symphony.create!(composer: Anonymous.new)
64
+
65
+ expect(Symphony.find(symphony.id).composer_id).to be_nil
66
+ end
67
+ end
68
+
69
+ context 'has_one' do
70
+ before(:all) do
71
+ class Composer
72
+ include Mongoid::Document
73
+
74
+ field :name
75
+
76
+ belongs_to :symphony
77
+
78
+ def attribution
79
+ "Composed by #{name}"
80
+ end
81
+ end
82
+
83
+ class Symphony
84
+ include Mongoid::Document
85
+
86
+ has_one :composer, fallback: -> { Anonymous.new }
87
+ end
88
+ end
89
+
90
+ after(:all) do
91
+ Object.send(:remove_const, :Symphony)
92
+ Object.send(:remove_const, :Composer)
93
+ end
94
+
95
+ it 'returns a fresh null object instance on every access when the association is nil' do
96
+ symphony = Symphony.create!
97
+
98
+ expect(symphony.composer.attribution).to eq('Composer unknown')
99
+ expect(symphony.composer).not_to equal(symphony.composer)
100
+ end
101
+
102
+ it 'returns the real associated document when one exists' do
103
+ symphony = Symphony.create!
104
+ Composer.create!(symphony: symphony, name: 'Mahler')
105
+
106
+ expect(Symphony.find(symphony.id).composer.attribution).to eq('Composed by Mahler')
107
+ end
108
+
109
+ it 'treats a direct null object assignment as nil' do
110
+ symphony = Symphony.create!
111
+
112
+ symphony.composer = Anonymous.new
113
+ symphony.save!
114
+
115
+ expect(Composer.where(symphony_id: symphony.id).first).to be_nil
116
+ end
117
+
118
+ it 'treats a null object passed via the constructor as nil' do
119
+ symphony = Symphony.create!(composer: Anonymous.new)
120
+
121
+ expect(Composer.where(symphony_id: symphony.id).first).to be_nil
122
+ end
123
+ end
124
+
125
+ context 'embeds_one' do
126
+ before(:all) do
127
+ class Composer
128
+ include Mongoid::Document
129
+
130
+ field :name
131
+
132
+ embedded_in :symphony
133
+
134
+ def attribution
135
+ "Composed by #{name}"
136
+ end
137
+ end
138
+
139
+ class Symphony
140
+ include Mongoid::Document
141
+
142
+ embeds_one :composer, fallback: -> { Anonymous.new }
143
+ end
144
+ end
145
+
146
+ after(:all) do
147
+ Object.send(:remove_const, :Symphony)
148
+ Object.send(:remove_const, :Composer)
149
+ end
150
+
151
+ it 'returns a fresh null object instance on every access when the association is nil' do
152
+ symphony = Symphony.create!
153
+
154
+ expect(symphony.composer.attribution).to eq('Composer unknown')
155
+ expect(symphony.composer).not_to equal(symphony.composer)
156
+ end
157
+
158
+ it 'returns the real embedded document when one exists' do
159
+ symphony = Symphony.create!(composer: Composer.new(name: 'Mahler'))
160
+
161
+ expect(Symphony.find(symphony.id).composer.attribution).to eq('Composed by Mahler')
162
+ end
163
+
164
+ it 'treats a direct null object assignment as nil' do
165
+ symphony = Symphony.create!
166
+
167
+ symphony.composer = Anonymous.new
168
+ symphony.save!
169
+
170
+ expect(Symphony.collection.find(_id: symphony.id).first['composer']).to be_nil
171
+ end
172
+
173
+ it 'treats a null object passed via the constructor as nil' do
174
+ symphony = Symphony.create!(composer: Anonymous.new)
175
+
176
+ expect(Symphony.collection.find(_id: symphony.id).first['composer']).to be_nil
177
+ end
178
+ end
179
+
180
+ context 'with a dependent strategy and no real associated document' do
181
+ before(:all) do
182
+ class Composer
183
+ include Mongoid::Document
184
+ end
185
+ end
186
+
187
+ after(:all) do
188
+ Object.send(:remove_const, :Composer)
189
+ end
190
+
191
+ context 'when the strategy is :destroy' do
192
+ before(:all) do
193
+ class Symphony
194
+ include Mongoid::Document
195
+
196
+ has_one :composer, dependent: :destroy, fallback: -> { Anonymous.new }
197
+ end
198
+ end
199
+
200
+ after(:all) do
201
+ Object.send(:remove_const, :Symphony)
202
+ end
203
+
204
+ it 'does not apply the strategy to the fallback when destroying the owner' do
205
+ symphony = Symphony.create!
206
+
207
+ expect { symphony.destroy }.not_to raise_error
208
+ end
209
+ end
210
+
211
+ context 'when the strategy is :nullify' do
212
+ before(:all) do
213
+ class Symphony
214
+ include Mongoid::Document
215
+
216
+ has_one :composer, dependent: :nullify, fallback: -> { Anonymous.new }
217
+ end
218
+ end
219
+
220
+ after(:all) do
221
+ Object.send(:remove_const, :Symphony)
222
+ end
223
+
224
+ it 'does not apply the strategy to the fallback when destroying the owner' do
225
+ symphony = Symphony.create!
226
+
227
+ expect { symphony.destroy }.not_to raise_error
228
+ end
229
+ end
230
+
231
+ context 'when the strategy is :delete_all' do
232
+ before(:all) do
233
+ class Symphony
234
+ include Mongoid::Document
235
+
236
+ has_one :composer, dependent: :delete_all, fallback: -> { Anonymous.new }
237
+ end
238
+ end
239
+
240
+ after(:all) do
241
+ Object.send(:remove_const, :Symphony)
242
+ end
243
+
244
+ it 'does not apply the strategy to the fallback when destroying the owner' do
245
+ symphony = Symphony.create!
246
+
247
+ expect { symphony.destroy }.not_to raise_error
248
+ end
249
+ end
250
+
251
+ context 'when the strategy is :restrict_with_exception' do
252
+ before(:all) do
253
+ class Symphony
254
+ include Mongoid::Document
255
+
256
+ has_one :composer, dependent: :restrict_with_exception, fallback: -> { Anonymous.new }
257
+ end
258
+ end
259
+
260
+ after(:all) do
261
+ Object.send(:remove_const, :Symphony)
262
+ end
263
+
264
+ it 'does not apply the strategy to the fallback when destroying the owner' do
265
+ symphony = Symphony.create!
266
+
267
+ expect { symphony.destroy }.not_to raise_error
268
+ end
269
+ end
270
+
271
+ context 'when the strategy is :restrict_with_error' do
272
+ before(:all) do
273
+ class Symphony
274
+ include Mongoid::Document
275
+
276
+ has_one :composer, dependent: :restrict_with_error, fallback: -> { Anonymous.new }
277
+ end
278
+ end
279
+
280
+ after(:all) do
281
+ Object.send(:remove_const, :Symphony)
282
+ end
283
+
284
+ it 'does not block destroying the owner because of the fallback' do
285
+ symphony = Symphony.create!
286
+
287
+ expect(symphony.destroy).to be_truthy
288
+ end
289
+ end
290
+ end
291
+
292
+ context 'on a polymorphic belongs_to' do
293
+ before(:all) do
294
+ class Composer
295
+ include Mongoid::Document
296
+ end
297
+
298
+ class Symphony
299
+ include Mongoid::Document
300
+
301
+ belongs_to :author, polymorphic: true, fallback: -> { Anonymous.new }
302
+ end
303
+ end
304
+
305
+ after(:all) do
306
+ Object.send(:remove_const, :Symphony)
307
+ Object.send(:remove_const, :Composer)
308
+ end
309
+
310
+ it 'does not raise when assigning a real document' do
311
+ symphony = Symphony.new
312
+
313
+ expect { symphony.author = Composer.new }.not_to raise_error
314
+ end
315
+
316
+ it 'treats a null object assignment as nil' do
317
+ symphony = Symphony.new
318
+
319
+ expect { symphony.author = Anonymous.new }.not_to raise_error
320
+ expect(symphony.author_id).to be_nil
321
+ end
322
+ end
323
+
324
+ context 'with nested attributes' do
325
+ before(:all) do
326
+ class Composer
327
+ include Mongoid::Document
328
+
329
+ field :name
330
+
331
+ embedded_in :symphony
332
+ end
333
+
334
+ class Symphony
335
+ include Mongoid::Document
336
+
337
+ embeds_one :composer, fallback: -> { Anonymous.new }
338
+ accepts_nested_attributes_for :composer
339
+ end
340
+ end
341
+
342
+ after(:all) do
343
+ Object.send(:remove_const, :Symphony)
344
+ Object.send(:remove_const, :Composer)
345
+ end
346
+
347
+ it 'builds the nested document instead of operating on the fallback' do
348
+ symphony = Symphony.new
349
+
350
+ expect { symphony.composer_attributes = { name: 'Mahler' } }.not_to raise_error
351
+ expect(symphony.composer.name).to eq('Mahler')
352
+ end
353
+ end
354
+
355
+ context 'when serialized with an included association' do
356
+ before(:all) do
357
+ class Composer
358
+ include Mongoid::Document
359
+ end
360
+
361
+ class Symphony
362
+ include Mongoid::Document
363
+
364
+ belongs_to :composer, fallback: -> { Anonymous.new }
365
+ end
366
+ end
367
+
368
+ after(:all) do
369
+ Object.send(:remove_const, :Symphony)
370
+ Object.send(:remove_const, :Composer)
371
+ end
372
+
373
+ it 'omits the fallback when the real association is nil' do
374
+ symphony = Symphony.new
375
+
376
+ expect { symphony.serializable_hash(include: :composer) }.not_to raise_error
377
+ expect(symphony.serializable_hash(include: :composer)).not_to have_key('composer')
378
+ end
379
+ end
380
+
381
+ context 'with a counter cache' do
382
+ before(:all) do
383
+ class Composer
384
+ include Mongoid::Document
385
+ end
386
+
387
+ class Symphony
388
+ include Mongoid::Document
389
+
390
+ belongs_to :composer, counter_cache: true, fallback: -> { Anonymous.new }
391
+ end
392
+ end
393
+
394
+ after(:all) do
395
+ Object.send(:remove_const, :Symphony)
396
+ Object.send(:remove_const, :Composer)
397
+ end
398
+
399
+ it 'does not update the counter on the fallback' do
400
+ expect { Symphony.create! }.not_to raise_error
401
+ end
402
+ end
403
+
404
+ context 'validation of the :fallback option' do
405
+ it 'rejects a declaration that combines :fallback with :autobuild' do
406
+ expect do
407
+ Class.new do
408
+ include Mongoid::Document
409
+
410
+ belongs_to :composer, fallback: -> { Object.new }, autobuild: true
411
+ end
412
+ end.to raise_error(ArgumentError, /cannot be combined with :autobuild/)
413
+ end
414
+
415
+ it 'rejects a non-callable :fallback value' do
416
+ expect do
417
+ Class.new do
418
+ include Mongoid::Document
419
+
420
+ belongs_to :composer, fallback: Object.new
421
+ end
422
+ end.to raise_error(ArgumentError, /must be a Proc or lambda/)
423
+ end
424
+ end
425
+ end
@@ -1027,4 +1027,30 @@ describe Mongoid::Association::Macros do
1027
1027
  end
1028
1028
  end
1029
1029
  end
1030
+
1031
+ context 'with :through option' do
1032
+ let(:klass) do
1033
+ Class.new do
1034
+ include Mongoid::Document
1035
+ end
1036
+ end
1037
+
1038
+ it 'creates a HasManyThrough for has_many with :through' do
1039
+ klass.has_many(:patients, through: :appointments)
1040
+ assoc = klass.relations['patients']
1041
+ expect(assoc).to be_a(Mongoid::Association::Referenced::HasManyThrough)
1042
+ end
1043
+
1044
+ it 'creates a HasOneThrough for has_one with :through' do
1045
+ klass.has_one(:store, through: :franchise)
1046
+ assoc = klass.relations['store']
1047
+ expect(assoc).to be_a(Mongoid::Association::Referenced::HasOneThrough)
1048
+ end
1049
+
1050
+ it 'raises when :through is used with belongs_to' do
1051
+ expect do
1052
+ klass.belongs_to(:something, through: :other)
1053
+ end.to raise_error(Mongoid::Errors::InvalidRelationOption)
1054
+ end
1055
+ end
1030
1056
  end
@@ -325,6 +325,23 @@ describe Mongoid::Association::Referenced::BelongsTo::Eager do
325
325
  eager.map(&:reviewer).should eq [ nil ]
326
326
  end
327
327
  end
328
+
329
+ context 'when the *_type field has not been registered with the resolver' do
330
+ before do
331
+ Publication::Review.collection.insert_one(
332
+ _id: BSON::ObjectId.new,
333
+ summary: 'tampered',
334
+ reviewer_id: BSON::ObjectId.new,
335
+ reviewer_type: 'Kernel'
336
+ )
337
+ end
338
+
339
+ it 'raises UnrecognizedModelAlias' do
340
+ expect do
341
+ Publication::Review.includes(:reviewer).entries
342
+ end.to raise_error(Mongoid::Errors::UnrecognizedModelAlias)
343
+ end
344
+ end
328
345
  end
329
346
 
330
347
  context 'when the association has scope' do
@@ -2161,6 +2161,13 @@ describe Mongoid::Association::Referenced::HasAndBelongsToMany::Proxy do
2161
2161
  end
2162
2162
  end
2163
2163
 
2164
+ # Whether the conditions carry a pattern decides which branch of remove_all
2165
+ # runs, so anything that has to behave the same on both is exercised twice.
2166
+ conditions_variants = [
2167
+ [ 'no regular expression', nil ],
2168
+ [ 'a regular expression', { name: /Test/ } ]
2169
+ ].freeze
2170
+
2164
2171
  %i[ delete_all destroy_all ].each do |method|
2165
2172
  describe "##{method}" do
2166
2173
  context 'when the relation is not polymorphic' do
@@ -2231,6 +2238,47 @@ describe Mongoid::Association::Referenced::HasAndBelongsToMany::Proxy do
2231
2238
  expect(person.preferences).to eq([])
2232
2239
  end
2233
2240
  end
2241
+
2242
+ context 'when the base holds the foreign keys' do
2243
+ # unbind_one pulls ids out of the base's foreign key array and marks
2244
+ # the base dirty, so which documents get unbound is visible here.
2245
+ # Only what the association already held is unbound, and that has to
2246
+ # hold whether or not the conditions carry a regular expression:
2247
+ # scanning under a regexp budget loads the association first, and
2248
+ # that load must not turn into an unbind of its own. See the note in
2249
+ # HasMany::Proxy#remove_all_bounded.
2250
+ let!(:person) do
2251
+ Person.create!.tap do |base|
2252
+ base.preferences.create!(name: 'Testing')
2253
+ base.preferences.create!(name: 'Test')
2254
+ end
2255
+ end
2256
+
2257
+ let(:ids) { person.preference_ids.dup }
2258
+
2259
+ conditions_variants.each do |description, conditions|
2260
+ context "when the conditions carry #{description}" do
2261
+ context 'when the association is already in memory' do
2262
+ it 'unbinds the deleted documents' do
2263
+ person.preferences.send(method, conditions)
2264
+ expect(person.preference_ids).to eq([])
2265
+ expect(person).to be_changed
2266
+ end
2267
+ end
2268
+
2269
+ context 'when the association has not been loaded' do
2270
+ let(:cold) { Person.find(person._id) }
2271
+
2272
+ it 'leaves the foreign keys alone' do
2273
+ expect(ids.length).to eq(2)
2274
+ cold.preferences.send(method, conditions)
2275
+ expect(cold.preference_ids).to eq(ids)
2276
+ expect(cold).not_to be_changed
2277
+ end
2278
+ end
2279
+ end
2280
+ end
2281
+ end
2234
2282
  end
2235
2283
  end
2236
2284
  end
@@ -984,4 +984,54 @@ describe Mongoid::Association::Referenced::HasAndBelongsToMany do
984
984
  expect(school.updated_at).to eq(update_time)
985
985
  end
986
986
  end
987
+
988
+ context 'when the stored foreign key array contains nil', :integration do
989
+ # Associating a target whose custom primary key is unset stores a literal
990
+ # nil in the foreign key array. A nil in an $in matches every document
991
+ # whose primary key field is null or absent, returning documents that were
992
+ # never associated.
993
+
994
+ before(:all) do
995
+ Object.const_set(:NkPost, Class.new do
996
+ include Mongoid::Document
997
+
998
+ store_in collection: 'nk_posts'
999
+ has_and_belongs_to_many :nk_tags, class_name: 'NkTag',
1000
+ primary_key: :slug, inverse_of: nil
1001
+ end)
1002
+ Object.const_set(:NkTag, Class.new do
1003
+ include Mongoid::Document
1004
+
1005
+ store_in collection: 'nk_tags'
1006
+ field :slug, type: String
1007
+ field :name, type: String
1008
+ end)
1009
+ end
1010
+
1011
+ after(:all) do
1012
+ %w[NkPost NkTag].each { |c| Object.send(:remove_const, c) }
1013
+ end
1014
+
1015
+ before { [ NkPost, NkTag ].each(&:delete_all) }
1016
+
1017
+ let!(:tag) { NkTag.create!(slug: 'ruby', name: 'tagged') }
1018
+ let!(:keyless_tag) { NkTag.create!(name: 'never associated') }
1019
+ let!(:post) { NkPost.create!(nk_tags: [ tag, NkTag.create!(name: 'no slug') ]) }
1020
+
1021
+ it 'does not return tags that were never associated' do
1022
+ expect(post.reload.nk_tags.to_a).to eq([ tag ])
1023
+ end
1024
+
1025
+ it 'does not put nil in the membership query' do
1026
+ expect(post.reload.nk_tags.criteria.selector['slug']['$in']).not_to include(nil)
1027
+ end
1028
+
1029
+ context 'when every stored key is nil' do
1030
+ let!(:post) { NkPost.create!(nk_tags: [ NkTag.create!(name: 'no slug') ]) }
1031
+
1032
+ it 'returns nothing rather than everything' do
1033
+ expect(post.reload.nk_tags.to_a).to eq([])
1034
+ end
1035
+ end
1036
+ end
987
1037
  end
@@ -323,6 +323,39 @@ describe Mongoid::Association::Referenced::HasMany::Eager do
323
323
  expect(eager.ordered_posts.max(:rating)).to eq(30)
324
324
  end
325
325
  end
326
+
327
+ context 'when the field name is a method name' do
328
+ it 'does not call the method for sum' do
329
+ eager.ordered_posts.each { |post| expect(post).not_to receive(:destroy) }
330
+ expect(eager.ordered_posts.sum(:destroy)).to eq(0)
331
+ end
332
+
333
+ it 'returns zero from sum' do
334
+ expect(eager.ordered_posts.sum(:object_id)).to eq(0)
335
+ end
336
+
337
+ it 'returns nil from avg' do
338
+ expect(eager.ordered_posts.avg(:object_id)).to be_nil
339
+ end
340
+
341
+ it 'returns nil from min' do
342
+ expect(eager.ordered_posts.min(:object_id)).to be_nil
343
+ end
344
+
345
+ it 'returns nil from max' do
346
+ expect(eager.ordered_posts.max(:object_id)).to be_nil
347
+ end
348
+ end
349
+
350
+ context 'when the field is not defined' do
351
+ it 'returns zero from sum' do
352
+ expect(eager.ordered_posts.sum(:not_a_field)).to eq(0)
353
+ end
354
+
355
+ it 'returns nil from avg' do
356
+ expect(eager.ordered_posts.avg(:not_a_field)).to be_nil
357
+ end
358
+ end
326
359
  end
327
360
  end
328
361
  end