mongoid 9.1.1 → 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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/config/locales/en.yml +31 -11
  3. data/lib/mongoid/association/accessors.rb +18 -4
  4. data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +10 -10
  5. data/lib/mongoid/association/eager_load/embedded_distributor.rb +6 -6
  6. data/lib/mongoid/association/eager_load/lookup_pipeline.rb +1 -1
  7. data/lib/mongoid/association/eager_loadable.rb +8 -1
  8. data/lib/mongoid/association/embedded/batchable.rb +17 -1
  9. data/lib/mongoid/association/nested/many.rb +1 -1
  10. data/lib/mongoid/association/referenced/has_and_belongs_to_many/buildable.rb +6 -0
  11. data/lib/mongoid/association/relatable.rb +18 -0
  12. data/lib/mongoid/association.rb +8 -0
  13. data/lib/mongoid/atomic/modifiers.rb +100 -16
  14. data/lib/mongoid/clients/factory.rb +2 -0
  15. data/lib/mongoid/config/encryption.rb +148 -17
  16. data/lib/mongoid/document.rb +3 -0
  17. data/lib/mongoid/encryptable.rb +42 -15
  18. data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
  19. data/lib/mongoid/errors/no_encryption_schema.rb +5 -3
  20. data/lib/mongoid/errors.rb +1 -0
  21. data/lib/mongoid/fields.rb +7 -0
  22. data/lib/mongoid/persistable/creatable.rb +120 -14
  23. data/lib/mongoid/persistable.rb +15 -15
  24. data/lib/mongoid/persistence_context.rb +46 -4
  25. data/lib/mongoid/search_indexable.rb +5 -1
  26. data/lib/mongoid/tasks/database.rb +10 -7
  27. data/lib/mongoid/touchable.rb +28 -1
  28. data/lib/mongoid/version.rb +1 -1
  29. data/lib/mongoid/warnings.rb +4 -0
  30. data/spec/integration/associations/embedded_delayed_atomic_sets_spec.rb +216 -0
  31. data/spec/integration/associations/has_and_belongs_to_many_spec.rb +14 -0
  32. data/spec/integration/atomic/modifiers_spec.rb +283 -0
  33. data/spec/integration/encryption_spec.rb +149 -0
  34. data/spec/mongoid/association/accessors_spec.rb +35 -0
  35. data/spec/mongoid/association/referenced/has_and_belongs_to_many/buildable_spec.rb +11 -0
  36. data/spec/mongoid/atomic/modifiers_spec.rb +347 -0
  37. data/spec/mongoid/attributes/nested_spec.rb +29 -0
  38. data/spec/mongoid/clients/factory_spec.rb +36 -0
  39. data/spec/mongoid/config/encryption_spec.rb +223 -0
  40. data/spec/mongoid/criteria/includable_spec.rb +346 -0
  41. data/spec/mongoid/encryptable_spec.rb +54 -0
  42. data/spec/mongoid/fields_spec.rb +8 -0
  43. data/spec/mongoid/search_indexable_spec.rb +26 -0
  44. data/spec/mongoid/tasks/database_spec.rb +13 -0
  45. data/spec/mongoid/touchable_spec.rb +217 -0
  46. data/spec/mongoid/touchable_spec_models.rb +37 -0
  47. data/spec/support/crypt/models.rb +96 -0
  48. data/spec/support/models/modifier_models.rb +65 -0
  49. metadata +5 -2
@@ -254,6 +254,19 @@ describe Mongoid::Tasks::Database do
254
254
  described_class.create_search_indexes([ searchable_model_spy ], wait: false)
255
255
  end
256
256
  end
257
+
258
+ context 'when waiting for the indexes' do
259
+ it 'runs the wait inside collection management' do
260
+ managing = nil
261
+ allow(described_class).to receive(:wait_for_search_indexes) do
262
+ managing = Mongoid::Threaded.managing_collection?
263
+ end
264
+
265
+ described_class.create_search_indexes([ searchable_model_spy ], wait: true)
266
+
267
+ expect(managing).to be true
268
+ end
269
+ end
257
270
  end
258
271
 
259
272
  describe '.remove_search_indexes' do
@@ -1400,6 +1400,46 @@ describe Mongoid::Touchable do
1400
1400
  end
1401
1401
  end
1402
1402
 
1403
+ describe 'the parent chain touch paths' do
1404
+ context 'on a root document' do
1405
+ it 'contains the root updated_at path' do
1406
+ expect(TouchableParent.new._touchable_chain_paths).to eq(%w[updated_at])
1407
+ end
1408
+ end
1409
+
1410
+ context 'on an embedded document with a touchable parent' do
1411
+ let(:parent) do
1412
+ parent = TouchableParent.create!
1413
+ parent.child = TouchableChild.create!(parent: parent)
1414
+ parent
1415
+ end
1416
+
1417
+ it 'contains the paths of each node in the chain' do
1418
+ expect(parent.child._touchable_chain_paths)
1419
+ .to eq(%w[child.updated_at updated_at])
1420
+ end
1421
+ end
1422
+
1423
+ context 'with a custom touch field' do
1424
+ it 'contains the custom field path of the anchor document' do
1425
+ building = TouchableSpec::Embedded::Building.create!
1426
+
1427
+ expect(building._touchable_chain_paths(:last_used_at))
1428
+ .to eq(%w[updated_at last_used_at])
1429
+ end
1430
+ end
1431
+
1432
+ context 'when the models use Timestamps::Short' do
1433
+ it 'resolves the aliased timestamp fields' do
1434
+ building = TouchableSpec::Short::Building.create!
1435
+ building.floors.create!
1436
+
1437
+ expect(building.reload.floors.first._touchable_chain_paths)
1438
+ .to eq(%w[floors.0.u_at u_at])
1439
+ end
1440
+ end
1441
+ end
1442
+
1403
1443
  describe 'touch merged with embedded insert' do
1404
1444
  context 'when pushing an embedded document with touch: true' do
1405
1445
  let(:building) do
@@ -1424,6 +1464,21 @@ describe Mongoid::Touchable do
1424
1464
  end
1425
1465
  end
1426
1466
 
1467
+ it 'sends the merged touch updates rewritten to the positional operator' do
1468
+ subscriber = Mrss::EventSubscriber.new
1469
+ Mongoid.client(:default).subscribe(Mongo::Monitoring::COMMAND, subscriber)
1470
+ begin
1471
+ floor.sofas.push(TouchableSpec::Embedded::Sofa.new)
1472
+ ensure
1473
+ Mongoid.client(:default).unsubscribe(Mongo::Monitoring::COMMAND, subscriber)
1474
+ end
1475
+
1476
+ command = subscriber.single_command_started_event('update').command
1477
+ sets = command['updates'].first['u']['$set']
1478
+
1479
+ expect(sets.keys).to contain_exactly('floors.$.updated_at', 'updated_at')
1480
+ end
1481
+
1427
1482
  it 'updates updated_at on the parent after push' do
1428
1483
  original_floor_updated_at = floor.updated_at
1429
1484
  original_building_updated_at = building.updated_at
@@ -1536,5 +1591,167 @@ describe Mongoid::Touchable do
1536
1591
  expect(building.updated_at).to be > original_updated_at
1537
1592
  end
1538
1593
  end
1594
+
1595
+ context 'when an existing child of the same array has a pending touch' do
1596
+ let(:building) do
1597
+ TouchableSpec::Embedded::Building.create!(title: 'Tower')
1598
+ end
1599
+
1600
+ before do
1601
+ floor = building.floors.create!(level: 1)
1602
+ # Leave a pending touch on the existing sibling so merging it into
1603
+ # the insert would target a path inside the pushed array.
1604
+ floor.updated_at = Time.now + 60
1605
+ end
1606
+
1607
+ it 'does not raise a conflict error when creating a sibling' do
1608
+ expect { building.floors.create!(level: 2) }.not_to raise_error
1609
+ end
1610
+
1611
+ it 'persists the new sibling' do
1612
+ building.floors.create!(level: 2)
1613
+ expect(building.reload.floors.length).to eq(2)
1614
+ end
1615
+
1616
+ it 'touches the parent' do
1617
+ building.floors.create!(level: 2)
1618
+ building.reload
1619
+ expect(building.updated_at).to be_within(5).of(Time.now)
1620
+ end
1621
+ end
1622
+
1623
+ context 'when a sibling in a parent array has a pending touch' do
1624
+ let(:building) do
1625
+ TouchableSpec::Embedded::Building.create!(title: 'Tower')
1626
+ end
1627
+
1628
+ let(:first_floor) do
1629
+ building.floors.create!(level: 1)
1630
+ end
1631
+
1632
+ let(:second_floor) do
1633
+ building.floors.create!(level: 2)
1634
+ end
1635
+
1636
+ let(:pending_touch_time) do
1637
+ Time.now + 60
1638
+ end
1639
+
1640
+ before do
1641
+ first_floor
1642
+ # Leave a pending touch on the other floor, at a different index of
1643
+ # the same array. Rewriting both that touch and the insert's own
1644
+ # touch to the positional operator collapses them onto one path and
1645
+ # touches the wrong floor.
1646
+ second_floor.updated_at = pending_touch_time
1647
+ end
1648
+
1649
+ it 'does not raise when creating a grandchild under the other floor' do
1650
+ expect { first_floor.sofas.create! }.not_to raise_error
1651
+ end
1652
+
1653
+ it 'touches the floor the grandchild was inserted under' do
1654
+ first_floor.sofas.create!
1655
+ building.reload
1656
+ expect(building.floors[0].updated_at).to be_within(5).of(Time.now)
1657
+ end
1658
+
1659
+ it 'preserves the pending touch on the sibling floor' do
1660
+ first_floor.sofas.create!
1661
+ building.reload
1662
+ expect(building.floors[1].updated_at.to_f)
1663
+ .to be_within(1).of(pending_touch_time.to_f)
1664
+ end
1665
+ end
1666
+
1667
+ context 'when the parent touch option names a custom field' do
1668
+ let(:building) do
1669
+ TouchableSpec::Embedded::Building.create!(title: 'Tower')
1670
+ end
1671
+
1672
+ it 'writes the custom touch field when creating an embedded child' do
1673
+ # Materialize the parent before the freeze so its updated_at differs
1674
+ # from the touch time, forcing the touch updates to merge into the
1675
+ # child's insert.
1676
+ building
1677
+ Timecop.freeze(Time.utc(2026, 1, 2, 3, 4, 5)) do
1678
+ building.waiting_rooms.create!
1679
+ building.reload
1680
+ expect(building.last_used_at).not_to be_nil
1681
+ expect(building.last_used_at.to_f)
1682
+ .to be_within(1).of(Time.utc(2026, 1, 2, 3, 4, 5).to_f)
1683
+ end
1684
+ end
1685
+ end
1686
+
1687
+ context 'when the models use Timestamps::Short' do
1688
+ let(:building) do
1689
+ TouchableSpec::Short::Building.create!
1690
+ end
1691
+
1692
+ let(:floor) do
1693
+ building.floors.create!
1694
+ end
1695
+
1696
+ before do
1697
+ building
1698
+ floor
1699
+ end
1700
+
1701
+ it 'merges the touch updates into the insert round-trip' do
1702
+ expect_query(1) do
1703
+ floor.gadgets.push(TouchableSpec::Short::Gadget.new)
1704
+ end
1705
+ end
1706
+
1707
+ it 'touches the parent chain through the aliased timestamp fields' do
1708
+ original_building_updated_at = building.u_at
1709
+ original_floor_updated_at = floor.u_at
1710
+
1711
+ Timecop.travel(Time.now + 10) do
1712
+ floor.gadgets.push(TouchableSpec::Short::Gadget.new)
1713
+ end
1714
+
1715
+ building.reload
1716
+ expect(building.u_at).to be > original_building_updated_at
1717
+ expect(building.floors.first.u_at).to be > original_floor_updated_at
1718
+ end
1719
+ end
1720
+
1721
+ context 'when a sibling has a pending touch and the touch callback does not run' do
1722
+ let(:building) do
1723
+ TouchableSpec::Embedded::Building.create!(title: 'Tower')
1724
+ end
1725
+
1726
+ let(:first_floor) do
1727
+ building.floors.create!(level: 1)
1728
+ end
1729
+
1730
+ let(:second_floor) do
1731
+ building.floors.create!(level: 2)
1732
+ end
1733
+
1734
+ let(:pending_touch_time) do
1735
+ Time.now + 60
1736
+ end
1737
+
1738
+ before do
1739
+ first_floor
1740
+ # Leave a pending touch on the other floor: rewriting it and this
1741
+ # chain's own touch would collapse both onto the same positional
1742
+ # path, so the touch updates cannot be merged into the insert.
1743
+ second_floor.updated_at = pending_touch_time
1744
+ end
1745
+
1746
+ it 'persists the deferred touches at insert time' do
1747
+ Mongoid::Touchable.suppress_touch_callbacks(TouchableSpec::Embedded::Sofa.name) do
1748
+ first_floor.sofas.push(TouchableSpec::Embedded::Sofa.new)
1749
+ end
1750
+
1751
+ building.reload
1752
+ expect(building.floors[1].updated_at.to_f)
1753
+ .to be_within(1).of(pending_touch_time.to_f)
1754
+ end
1755
+ end
1539
1756
  end
1540
1757
  end
@@ -7,10 +7,12 @@ module TouchableSpec
7
7
  include Mongoid::Timestamps
8
8
 
9
9
  field :title, type: String
10
+ field :last_used_at, type: Time
10
11
 
11
12
  embeds_many :entrances, class_name: 'TouchableSpec::Embedded::Entrance'
12
13
  embeds_many :floors, class_name: 'TouchableSpec::Embedded::Floor'
13
14
  embeds_one :lobby, class_name: 'TouchableSpec::Embedded::Lobby'
15
+ embeds_many :waiting_rooms, class_name: 'TouchableSpec::Embedded::WaitingRoom'
14
16
  end
15
17
 
16
18
  class Lobby
@@ -75,6 +77,41 @@ module TouchableSpec
75
77
 
76
78
  embedded_in :floor, touch: true, class_name: 'TouchableSpec::Embedded::Floor'
77
79
  end
80
+
81
+ class WaitingRoom
82
+ include Mongoid::Document
83
+ include Mongoid::Timestamps
84
+
85
+ field :name, type: String
86
+
87
+ embedded_in :building, touch: :last_used_at,
88
+ class_name: 'TouchableSpec::Embedded::Building'
89
+ end
90
+ end
91
+
92
+ module Short
93
+ class Building
94
+ include Mongoid::Document
95
+ include Mongoid::Timestamps::Short
96
+
97
+ embeds_many :floors, class_name: 'TouchableSpec::Short::Floor'
98
+ end
99
+
100
+ class Floor
101
+ include Mongoid::Document
102
+ include Mongoid::Timestamps::Short
103
+
104
+ embedded_in :building, touch: true, class_name: 'TouchableSpec::Short::Building'
105
+
106
+ embeds_many :gadgets, class_name: 'TouchableSpec::Short::Gadget'
107
+ end
108
+
109
+ class Gadget
110
+ include Mongoid::Document
111
+ include Mongoid::Timestamps::Short
112
+
113
+ embedded_in :floor, touch: true, class_name: 'TouchableSpec::Short::Floor'
114
+ end
78
115
  end
79
116
 
80
117
  module Referenced
@@ -203,4 +203,100 @@ module Crypt
203
203
 
204
204
  embeds_one :missing_note, class_name: 'Crypt::MissingNote'
205
205
  end
206
+
207
+ # Two unrelated models stored in one collection, each with its own encrypted
208
+ # field.
209
+ class Badge
210
+ include Mongoid::Document
211
+
212
+ store_in collection: 'crypt_badges'
213
+
214
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
215
+
216
+ field :serial, type: String, encrypt: true
217
+ end
218
+
219
+ class BadgeScan
220
+ include Mongoid::Document
221
+
222
+ store_in collection: 'crypt_badges'
223
+
224
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
225
+
226
+ field :holder, type: String, encrypt: true
227
+ end
228
+
229
+ # Single collection inheritance. Only Member is registered in
230
+ # Mongoid.models; the subclasses are not.
231
+ class Member
232
+ include Mongoid::Document
233
+
234
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
235
+
236
+ field :ssn, type: String, encrypt: true
237
+ end
238
+
239
+ class Employee < Member
240
+ field :salary, type: String, encrypt: true
241
+ end
242
+
243
+ class Contractor < Member
244
+ field :rate, type: String, encrypt: true
245
+ end
246
+
247
+ # Single collection inheritance where only the subclass is encrypted.
248
+ class Listing
249
+ include Mongoid::Document
250
+
251
+ field :title, type: String
252
+ end
253
+
254
+ class PrivateListing < Listing
255
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
256
+
257
+ field :owner_ssn, type: String, encrypt: true
258
+ end
259
+
260
+ # An embeds_one target whose subclass adds an encrypted field.
261
+ class Box
262
+ include Mongoid::Document
263
+
264
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
265
+
266
+ embeds_one :item, class_name: 'Crypt::Item'
267
+ end
268
+
269
+ class Item
270
+ include Mongoid::Document
271
+
272
+ field :label, type: String, encrypt: true
273
+
274
+ embedded_in :box, class_name: 'Crypt::Box'
275
+ end
276
+
277
+ class SealedItem < Item
278
+ field :seal, type: String, encrypt: true
279
+ end
280
+
281
+ # An embeds_one target with no encrypted field, whose subclass has one.
282
+ # Nothing on Locker or Content declares encryption.
283
+ class Locker
284
+ include Mongoid::Document
285
+
286
+ embeds_one :content, class_name: 'Crypt::Content'
287
+ end
288
+
289
+ class Content
290
+ include Mongoid::Document
291
+
292
+ field :label, type: String
293
+
294
+ embedded_in :locker, class_name: 'Crypt::Locker'
295
+ end
296
+
297
+ class SecretContent < Content
298
+ encrypt_with key_id: 'grolrnFVSSW9Gq04Q87R9Q==', deterministic: true
299
+
300
+ field :code, type: String, encrypt: true
301
+ end
206
302
  end
@@ -22,4 +22,69 @@ module ModifierModels
22
22
  embedded_in :book, class_name: 'ModifierModels::Book'
23
23
  field :text, type: String
24
24
  end
25
+
26
+ class Roster
27
+ include Mongoid::Document
28
+
29
+ embeds_many :players, class_name: 'ModifierModels::Player'
30
+ accepts_nested_attributes_for :players, allow_destroy: true
31
+ end
32
+
33
+ class Player
34
+ include Mongoid::Document
35
+
36
+ embedded_in :roster, class_name: 'ModifierModels::Roster'
37
+ embeds_one :medal, class_name: 'ModifierModels::Medal'
38
+ has_and_belongs_to_many :badges, inverse_of: nil, class_name: 'ModifierModels::Badge'
39
+ embeds_many :awards, class_name: 'ModifierModels::Award'
40
+ accepts_nested_attributes_for :awards, allow_destroy: true
41
+ field :nickname, type: String
42
+ end
43
+
44
+ class Award
45
+ include Mongoid::Document
46
+
47
+ embedded_in :player, class_name: 'ModifierModels::Player'
48
+ has_and_belongs_to_many :badges, inverse_of: nil, class_name: 'ModifierModels::Badge'
49
+ field :name, type: String
50
+ end
51
+
52
+ class Medal
53
+ include Mongoid::Document
54
+
55
+ embedded_in :player, class_name: 'ModifierModels::Player'
56
+ end
57
+
58
+ class Badge
59
+ include Mongoid::Document
60
+ end
61
+
62
+ class Cabinet
63
+ include Mongoid::Document
64
+
65
+ embeds_one :drawer, class_name: 'ModifierModels::Drawer', autobuild: true
66
+ end
67
+
68
+ class Drawer
69
+ include Mongoid::Document
70
+
71
+ field :matcher, type: String
72
+ embedded_in :cabinet, class_name: 'ModifierModels::Cabinet'
73
+ embeds_many :items, class_name: 'ModifierModels::Item'
74
+ end
75
+
76
+ class Item
77
+ include Mongoid::Document
78
+
79
+ field :name, type: String
80
+ embedded_in :drawer, class_name: 'ModifierModels::Drawer'
81
+ embeds_many :widgets, class_name: 'ModifierModels::Widget'
82
+ end
83
+
84
+ class Widget
85
+ include Mongoid::Document
86
+
87
+ field :label, type: String
88
+ embedded_in :item, class_name: 'ModifierModels::Item'
89
+ end
25
90
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.1.1
4
+ version: 9.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - The MongoDB Ruby Team
@@ -317,6 +317,7 @@ files:
317
317
  - lib/mongoid/errors/invalid_discriminator_key_target.rb
318
318
  - lib/mongoid/errors/invalid_dot_dollar_assignment.rb
319
319
  - lib/mongoid/errors/invalid_elem_match_operator.rb
320
+ - lib/mongoid/errors/invalid_encryption_schema.rb
320
321
  - lib/mongoid/errors/invalid_estimated_count_criteria.rb
321
322
  - lib/mongoid/errors/invalid_estimated_count_scoping.rb
322
323
  - lib/mongoid/errors/invalid_expression_operator.rb
@@ -529,6 +530,7 @@ files:
529
530
  - spec/integration/active_job_spec.rb
530
531
  - spec/integration/app_spec.rb
531
532
  - spec/integration/associations/belongs_to_spec.rb
533
+ - spec/integration/associations/embedded_delayed_atomic_sets_spec.rb
532
534
  - spec/integration/associations/embedded_dirty_spec.rb
533
535
  - spec/integration/associations/embedded_spec.rb
534
536
  - spec/integration/associations/embeds_many_spec.rb
@@ -1287,7 +1289,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1287
1289
  - !ruby/object:Gem::Version
1288
1290
  version: 1.3.6
1289
1291
  requirements: []
1290
- rubygems_version: 4.0.21
1292
+ rubygems_version: 4.0.24
1291
1293
  specification_version: 4
1292
1294
  summary: Elegant Persistence in Ruby for MongoDB.
1293
1295
  test_files:
@@ -1297,6 +1299,7 @@ test_files:
1297
1299
  - spec/integration/active_job_spec.rb
1298
1300
  - spec/integration/app_spec.rb
1299
1301
  - spec/integration/associations/belongs_to_spec.rb
1302
+ - spec/integration/associations/embedded_delayed_atomic_sets_spec.rb
1300
1303
  - spec/integration/associations/embedded_dirty_spec.rb
1301
1304
  - spec/integration/associations/embedded_spec.rb
1302
1305
  - spec/integration/associations/embeds_many_spec.rb