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.
- checksums.yaml +4 -4
- data/lib/config/locales/en.yml +31 -11
- data/lib/mongoid/association/accessors.rb +18 -4
- data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +10 -10
- data/lib/mongoid/association/eager_load/embedded_distributor.rb +6 -6
- data/lib/mongoid/association/eager_load/lookup_pipeline.rb +1 -1
- data/lib/mongoid/association/eager_loadable.rb +8 -1
- data/lib/mongoid/association/embedded/batchable.rb +17 -1
- data/lib/mongoid/association/nested/many.rb +1 -1
- data/lib/mongoid/association/referenced/has_and_belongs_to_many/buildable.rb +6 -0
- data/lib/mongoid/association/relatable.rb +18 -0
- data/lib/mongoid/association.rb +8 -0
- data/lib/mongoid/atomic/modifiers.rb +100 -16
- data/lib/mongoid/clients/factory.rb +2 -0
- data/lib/mongoid/config/encryption.rb +148 -17
- data/lib/mongoid/document.rb +3 -0
- data/lib/mongoid/encryptable.rb +42 -15
- data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
- data/lib/mongoid/errors/no_encryption_schema.rb +5 -3
- data/lib/mongoid/errors.rb +1 -0
- data/lib/mongoid/fields.rb +7 -0
- data/lib/mongoid/persistable/creatable.rb +120 -14
- data/lib/mongoid/persistable.rb +15 -15
- data/lib/mongoid/persistence_context.rb +46 -4
- data/lib/mongoid/search_indexable.rb +5 -1
- data/lib/mongoid/tasks/database.rb +10 -7
- data/lib/mongoid/touchable.rb +28 -1
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +4 -0
- data/spec/integration/associations/embedded_delayed_atomic_sets_spec.rb +216 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +14 -0
- data/spec/integration/atomic/modifiers_spec.rb +283 -0
- data/spec/integration/encryption_spec.rb +149 -0
- data/spec/mongoid/association/accessors_spec.rb +35 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/buildable_spec.rb +11 -0
- data/spec/mongoid/atomic/modifiers_spec.rb +347 -0
- data/spec/mongoid/attributes/nested_spec.rb +29 -0
- data/spec/mongoid/clients/factory_spec.rb +36 -0
- data/spec/mongoid/config/encryption_spec.rb +223 -0
- data/spec/mongoid/criteria/includable_spec.rb +346 -0
- data/spec/mongoid/encryptable_spec.rb +54 -0
- data/spec/mongoid/fields_spec.rb +8 -0
- data/spec/mongoid/search_indexable_spec.rb +26 -0
- data/spec/mongoid/tasks/database_spec.rb +13 -0
- data/spec/mongoid/touchable_spec.rb +217 -0
- data/spec/mongoid/touchable_spec_models.rb +37 -0
- data/spec/support/crypt/models.rb +96 -0
- data/spec/support/models/modifier_models.rb +65 -0
- metadata +5 -2
|
@@ -64,15 +64,18 @@ module Mongoid
|
|
|
64
64
|
# @param [ true | false ] wait whether to wait for the indexes to be
|
|
65
65
|
# built.
|
|
66
66
|
def create_search_indexes(models = ::Mongoid.models, wait: true)
|
|
67
|
-
|
|
67
|
+
Threaded.with_collection_management do
|
|
68
|
+
searchable = models.select { |m| m.search_index_specs.any? }
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
# queue up the search index creation requests
|
|
71
|
+
index_names_by_model = searchable.each_with_object({}) do |model, obj|
|
|
72
|
+
logger.info("MONGOID: Creating search indexes on #{model}...")
|
|
73
|
+
obj[model] = model.create_search_indexes
|
|
74
|
+
end
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
wait_for_search_indexes(index_names_by_model) if wait
|
|
77
|
+
index_names_by_model
|
|
78
|
+
end
|
|
76
79
|
end
|
|
77
80
|
|
|
78
81
|
# Return the list of indexes by model that exist in the database but aren't
|
data/lib/mongoid/touchable.rb
CHANGED
|
@@ -49,7 +49,7 @@ module Mongoid
|
|
|
49
49
|
|
|
50
50
|
begin
|
|
51
51
|
touches = _gather_touch_updates(Time.current, field)
|
|
52
|
-
_root.
|
|
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
|
data/lib/mongoid/version.rb
CHANGED
data/lib/mongoid/warnings.rb
CHANGED
|
@@ -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
|
|
@@ -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
|
|
@@ -134,4 +134,287 @@ describe Mongoid::Atomic::Modifiers do
|
|
|
134
134
|
expect(library.books[2].foreword).not_to be_nil
|
|
135
135
|
end
|
|
136
136
|
end
|
|
137
|
+
|
|
138
|
+
context 'when adding to a set on an existing embedded document and pushing a new one' do
|
|
139
|
+
let(:badges) { Array.new(2) { ModifierModels::Badge.create! } }
|
|
140
|
+
let(:roster) do
|
|
141
|
+
ModifierModels::Roster.create!(players: [
|
|
142
|
+
ModifierModels::Player.new(badges: [ badges[0] ]),
|
|
143
|
+
ModifierModels::Player.new(badges: [ badges[0] ])
|
|
144
|
+
])
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
before do
|
|
148
|
+
roster.reload
|
|
149
|
+
roster.players.build
|
|
150
|
+
player = roster.players[0]
|
|
151
|
+
player.badge_ids = player.badge_ids + [ badges[1].id ]
|
|
152
|
+
roster.save!
|
|
153
|
+
roster.reload
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it 'applies both operations' do
|
|
157
|
+
expect(roster.players.size).to eq(3)
|
|
158
|
+
expect(roster.players[0].badge_ids).to eq([ badges[0].id, badges[1].id ])
|
|
159
|
+
expect(roster.players[1].badge_ids).to eq([ badges[0].id ])
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
context 'when pulling all from an existing embedded document and pushing a new one' do
|
|
164
|
+
let(:badges) { Array.new(2) { ModifierModels::Badge.create! } }
|
|
165
|
+
let(:roster) do
|
|
166
|
+
ModifierModels::Roster.create!(players: [
|
|
167
|
+
ModifierModels::Player.new(badges: badges),
|
|
168
|
+
ModifierModels::Player.new
|
|
169
|
+
])
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
before do
|
|
173
|
+
roster.reload
|
|
174
|
+
roster.players.build
|
|
175
|
+
player = roster.players[0]
|
|
176
|
+
player.badge_ids = player.badge_ids.drop(1)
|
|
177
|
+
roster.save!
|
|
178
|
+
roster.reload
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it 'applies both operations' do
|
|
182
|
+
expect(roster.players.size).to eq(3)
|
|
183
|
+
expect(roster.players[0].badge_ids).to eq([ badges[1].id ])
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
context 'when unsetting a field on an existing embedded document and pushing a new one' do
|
|
188
|
+
let(:roster) do
|
|
189
|
+
ModifierModels::Roster.create!(players: [
|
|
190
|
+
ModifierModels::Player.new(medal: ModifierModels::Medal.new),
|
|
191
|
+
ModifierModels::Player.new
|
|
192
|
+
])
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
before do
|
|
196
|
+
roster.reload
|
|
197
|
+
roster.players.build
|
|
198
|
+
roster.players[0].assign_attributes(medal: nil)
|
|
199
|
+
roster.save!
|
|
200
|
+
roster.reload
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
it 'applies both operations' do
|
|
204
|
+
expect(roster.players.size).to eq(3)
|
|
205
|
+
expect(roster.players[0].medal).to be_nil
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
context 'when unsetting an embedded field and pulling a sibling' do
|
|
210
|
+
let(:roster) do
|
|
211
|
+
ModifierModels::Roster.create!(players: [
|
|
212
|
+
ModifierModels::Player.new(medal: ModifierModels::Medal.new),
|
|
213
|
+
ModifierModels::Player.new(medal: ModifierModels::Medal.new)
|
|
214
|
+
])
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
before do
|
|
218
|
+
roster.reload
|
|
219
|
+
roster.add_atomic_pull(roster.players[0])
|
|
220
|
+
roster.players[1].assign_attributes(medal: nil)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
it 'fails without changing stored data' do
|
|
224
|
+
# $pull shifts array elements, so these operations cannot be safely
|
|
225
|
+
# applied in separate writes; MongoDB rejects them in a single write.
|
|
226
|
+
expect { roster.save! }.to raise_error(Mongo::Error::OperationFailure)
|
|
227
|
+
|
|
228
|
+
roster.reload
|
|
229
|
+
expect(roster.players.size).to eq(2)
|
|
230
|
+
expect(roster.players[0].medal).not_to be_nil
|
|
231
|
+
expect(roster.players[1].medal).not_to be_nil
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
context 'when changing foreign keys of a remaining nested document after pulling its sibling' do
|
|
236
|
+
let(:badges) { Array.new(2) { ModifierModels::Badge.create! } }
|
|
237
|
+
let(:roster) do
|
|
238
|
+
ModifierModels::Roster.create!(players: [
|
|
239
|
+
ModifierModels::Player.new(badges: [ badges[0] ]),
|
|
240
|
+
ModifierModels::Player.new(
|
|
241
|
+
badges: [ badges[0] ],
|
|
242
|
+
awards: [
|
|
243
|
+
ModifierModels::Award.new(name: 'first', badges: [ badges[0] ]),
|
|
244
|
+
ModifierModels::Award.new(name: 'second', badges: [ badges[0] ])
|
|
245
|
+
]
|
|
246
|
+
)
|
|
247
|
+
])
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
before do
|
|
251
|
+
roster.reload
|
|
252
|
+
first_player, second_player = roster.players.to_a
|
|
253
|
+
first_award, second_award = second_player.awards.to_a
|
|
254
|
+
# The change on the first player comes before the nested $pull on the
|
|
255
|
+
# same root. The change on the second award uses its position after
|
|
256
|
+
# the $pull.
|
|
257
|
+
change.call(first_player)
|
|
258
|
+
roster.players_attributes = [
|
|
259
|
+
{ id: second_player.id, awards_attributes: [ { id: first_award.id, _destroy: true } ] }
|
|
260
|
+
]
|
|
261
|
+
change.call(second_award)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
context 'when adding to the foreign keys' do
|
|
265
|
+
let(:change) do
|
|
266
|
+
->(doc) { doc.badge_ids = doc.badge_ids + [ badges[1].id ] }
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
it 'fails without changing stored data' do
|
|
270
|
+
# The $addToSet for the second award would hit the first award if it
|
|
271
|
+
# were applied before the $pull, so it stays in the same update.
|
|
272
|
+
expect { roster.save! }.to raise_error(Mongo::Error::OperationFailure)
|
|
273
|
+
|
|
274
|
+
roster.reload
|
|
275
|
+
expect(roster.players[0].badge_ids).to eq([ badges[0].id ])
|
|
276
|
+
expect(roster.players[1].awards.map(&:name)).to eq(%w[first second])
|
|
277
|
+
expect(roster.players[1].awards.map(&:badge_ids)).to eq([ [ badges[0].id ], [ badges[0].id ] ])
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
context 'when removing from the foreign keys' do
|
|
282
|
+
let(:change) do
|
|
283
|
+
->(doc) { doc.badge_ids = [] }
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
it 'applies the change to the remaining nested document' do
|
|
287
|
+
roster.save!
|
|
288
|
+
|
|
289
|
+
roster.reload
|
|
290
|
+
expect(roster.players[0].badge_ids).to eq([])
|
|
291
|
+
expect(roster.players[1].awards.map(&:name)).to eq([ 'second' ])
|
|
292
|
+
expect(roster.players[1].awards[0].badge_ids).to eq([])
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
context 'when renaming a remaining nested document after pulling a preceding sibling and its sibling' do
|
|
298
|
+
let(:roster) do
|
|
299
|
+
ModifierModels::Roster.create!(players: [
|
|
300
|
+
ModifierModels::Player.new(nickname: 'a'),
|
|
301
|
+
ModifierModels::Player.new(nickname: 'b'),
|
|
302
|
+
ModifierModels::Player.new(
|
|
303
|
+
nickname: 'c',
|
|
304
|
+
awards: [
|
|
305
|
+
ModifierModels::Award.new(name: 'first'),
|
|
306
|
+
ModifierModels::Award.new(name: 'second')
|
|
307
|
+
]
|
|
308
|
+
)
|
|
309
|
+
])
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
before do
|
|
313
|
+
roster.reload
|
|
314
|
+
first_player, second_player, third_player = roster.players.to_a
|
|
315
|
+
first_award, second_award = third_player.awards.to_a
|
|
316
|
+
roster.players_attributes = [
|
|
317
|
+
{ id: first_player.id, _destroy: true },
|
|
318
|
+
{ id: second_player.id, nickname: 'B' },
|
|
319
|
+
{ id: third_player.id, awards_attributes: [
|
|
320
|
+
{ id: first_award.id, _destroy: true },
|
|
321
|
+
{ id: second_award.id, name: 'SECOND' }
|
|
322
|
+
] }
|
|
323
|
+
]
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
it 'fails without changing stored data' do
|
|
327
|
+
# Both $pulls stay in the main update. The rename of the second award
|
|
328
|
+
# would hit the first award if it were applied before the nested $pull.
|
|
329
|
+
expect { roster.save! }.to raise_error(Mongo::Error::OperationFailure)
|
|
330
|
+
|
|
331
|
+
roster.reload
|
|
332
|
+
expect(roster.players.map(&:nickname)).to eq(%w[a b c])
|
|
333
|
+
expect(roster.players[2].awards.map(&:name)).to eq(%w[first second])
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
context 'when adding a nested document and pulling a sibling' do
|
|
338
|
+
let(:roster) do
|
|
339
|
+
ModifierModels::Roster.create!(players: [
|
|
340
|
+
ModifierModels::Player.new(nickname: 'a'),
|
|
341
|
+
ModifierModels::Player.new(
|
|
342
|
+
nickname: 'b',
|
|
343
|
+
awards: [ ModifierModels::Award.new(name: 'first') ]
|
|
344
|
+
)
|
|
345
|
+
])
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
before do
|
|
349
|
+
roster.reload
|
|
350
|
+
first_player, second_player = roster.players.to_a
|
|
351
|
+
second_player.awards.build(name: 'second')
|
|
352
|
+
roster.players_attributes = [ { id: first_player.id, _destroy: true } ]
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
it 'applies both operations' do
|
|
356
|
+
roster.save!
|
|
357
|
+
|
|
358
|
+
roster.reload
|
|
359
|
+
expect(roster.players.map(&:nickname)).to eq([ 'b' ])
|
|
360
|
+
expect(roster.players[0].awards.map(&:name)).to eq(%w[first second])
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
context 'when assigning embeds_many attributes on a new embedded document' do
|
|
365
|
+
let(:cabinet) do
|
|
366
|
+
ModifierModels::Cabinet.create!
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
before do
|
|
370
|
+
cabinet.reload
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
context 'when the nested documents are assigned on the built document' do
|
|
374
|
+
before do
|
|
375
|
+
cabinet.build_drawer(matcher: 'all')
|
|
376
|
+
cabinet.drawer.assign_attributes(items: [ { name: 'a' } ])
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
it 'saves without conflicting $set paths' do
|
|
380
|
+
expect { cabinet.save! }.not_to raise_error
|
|
381
|
+
|
|
382
|
+
cabinet.reload
|
|
383
|
+
expect(cabinet.drawer.matcher).to eq('all')
|
|
384
|
+
expect(cabinet.drawer.items.map(&:name)).to eq([ 'a' ])
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
context 'when the nested documents are assigned via the parent' do
|
|
389
|
+
before do
|
|
390
|
+
cabinet.assign_attributes(drawer: { matcher: 'all', items: [ { name: 'a' } ] })
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
it 'saves without conflicting $set paths' do
|
|
394
|
+
expect { cabinet.save! }.not_to raise_error
|
|
395
|
+
|
|
396
|
+
cabinet.reload
|
|
397
|
+
expect(cabinet.drawer.matcher).to eq('all')
|
|
398
|
+
expect(cabinet.drawer.items.map(&:name)).to eq([ 'a' ])
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
context 'when an assigned child has its own embedded collection' do
|
|
403
|
+
let(:item) { ModifierModels::Item.new(name: 'a') }
|
|
404
|
+
|
|
405
|
+
before do
|
|
406
|
+
item.assign_attributes(widgets: [ { label: 'w' } ])
|
|
407
|
+
cabinet.build_drawer(matcher: 'all')
|
|
408
|
+
cabinet.drawer.assign_attributes(items: [ item ])
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
it 'does not write the child collection to the root document' do
|
|
412
|
+
expect { cabinet.save! }.not_to raise_error
|
|
413
|
+
|
|
414
|
+
stored = ModifierModels::Cabinet.collection.find(_id: cabinet.id).first
|
|
415
|
+
expect(stored.keys).to contain_exactly('_id', 'drawer')
|
|
416
|
+
expect(stored['drawer']['items'].first['widgets'].first['label']).to eq('w')
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
end
|
|
137
420
|
end
|