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.
- checksums.yaml +4 -4
- data/lib/config/locales/en.yml +69 -0
- data/lib/mongoid/association/accessors.rb +27 -5
- data/lib/mongoid/association/depending.rb +14 -10
- data/lib/mongoid/association/eager.rb +9 -0
- data/lib/mongoid/association/eager_load/discriminated_inclusion.rb +88 -0
- data/lib/mongoid/association/eager_load/embedded_distributor.rb +151 -0
- data/lib/mongoid/association/eager_load/inclusion.rb +173 -0
- data/lib/mongoid/association/eager_load/inclusion_tree.rb +96 -0
- data/lib/mongoid/association/eager_load/lookup_pipeline.rb +121 -0
- data/lib/mongoid/association/eager_load/polymorphic_preloader.rb +61 -0
- data/lib/mongoid/association/eager_load/polymorphic_targets.rb +151 -0
- data/lib/mongoid/association/eager_loadable.rb +65 -101
- data/lib/mongoid/association/embedded/batchable.rb +17 -1
- data/lib/mongoid/association/embedded/embeds_many/proxy.rb +6 -1
- data/lib/mongoid/association/embedded/embeds_one.rb +1 -0
- data/lib/mongoid/association/macros.rb +13 -1
- data/lib/mongoid/association/nested/many.rb +34 -5
- data/lib/mongoid/association/nested/nested_buildable.rb +14 -0
- data/lib/mongoid/association/nested/one.rb +6 -1
- data/lib/mongoid/association/options.rb +17 -0
- data/lib/mongoid/association/referenced/belongs_to/eager.rb +6 -1
- data/lib/mongoid/association/referenced/belongs_to.rb +1 -0
- data/lib/mongoid/association/referenced/counter_cache.rb +3 -3
- data/lib/mongoid/association/referenced/has_and_belongs_to_many/buildable.rb +6 -0
- data/lib/mongoid/association/referenced/has_and_belongs_to_many.rb +5 -0
- data/lib/mongoid/association/referenced/has_many/enumerable.rb +3 -1
- data/lib/mongoid/association/referenced/has_many/proxy.rb +100 -1
- data/lib/mongoid/association/referenced/has_many_through/eager.rb +99 -0
- data/lib/mongoid/association/referenced/has_many_through/proxy.rb +68 -0
- data/lib/mongoid/association/referenced/has_many_through.rb +201 -0
- data/lib/mongoid/association/referenced/has_one.rb +1 -0
- data/lib/mongoid/association/referenced/has_one_through/eager.rb +65 -0
- data/lib/mongoid/association/referenced/has_one_through/proxy.rb +30 -0
- data/lib/mongoid/association/referenced/has_one_through.rb +167 -0
- data/lib/mongoid/association/referenced.rb +2 -0
- data/lib/mongoid/association/relatable.rb +58 -0
- data/lib/mongoid/association.rb +15 -0
- data/lib/mongoid/atomic/modifiers.rb +100 -16
- data/lib/mongoid/clients/factory.rb +9 -13
- data/lib/mongoid/collection_configurable.rb +8 -0
- data/lib/mongoid/config/defaults.rb +0 -1
- data/lib/mongoid/config/encryption.rb +170 -22
- data/lib/mongoid/config.rb +56 -5
- data/lib/mongoid/contextual/aggregable/memory.rb +6 -2
- data/lib/mongoid/contextual/memory.rb +18 -7
- data/lib/mongoid/criteria/includable.rb +35 -11
- data/lib/mongoid/criteria/queryable/mergeable.rb +4 -0
- data/lib/mongoid/criteria/queryable/selectable.rb +123 -7
- data/lib/mongoid/deprecable.rb +45 -2
- data/lib/mongoid/document.rb +3 -0
- data/lib/mongoid/encryptable.rb +73 -0
- data/lib/mongoid/errors/config_redactor.rb +43 -0
- data/lib/mongoid/errors/in_memory_regexp_timeout.rb +26 -0
- data/lib/mongoid/errors/invalid_encryption_schema.rb +27 -0
- data/lib/mongoid/errors/mixed_client_configuration.rb +1 -1
- data/lib/mongoid/errors/no_client_database.rb +1 -1
- data/lib/mongoid/errors/no_client_hosts.rb +1 -1
- data/lib/mongoid/errors/no_encryption_schema.rb +30 -0
- data/lib/mongoid/errors/readonly_association.rb +29 -0
- data/lib/mongoid/errors.rb +5 -0
- data/lib/mongoid/extensions/vector.rb +64 -0
- data/lib/mongoid/extensions.rb +1 -0
- data/lib/mongoid/field_readable.rb +69 -0
- data/lib/mongoid/fields.rb +7 -0
- data/lib/mongoid/indexable.rb +38 -26
- data/lib/mongoid/matchable.rb +6 -1
- data/lib/mongoid/matcher/eq_impl_with_regexp.rb +2 -4
- data/lib/mongoid/matcher/regex.rb +11 -12
- data/lib/mongoid/matcher/regexp_budget.rb +372 -0
- data/lib/mongoid/matcher.rb +1 -0
- data/lib/mongoid/persistable/creatable.rb +120 -14
- data/lib/mongoid/persistable/updatable.rb +9 -11
- data/lib/mongoid/persistable.rb +15 -15
- data/lib/mongoid/persistence_context.rb +77 -0
- data/lib/mongoid/search_indexable.rb +69 -24
- data/lib/mongoid/serializable.rb +12 -4
- data/lib/mongoid/tasks/database.rb +33 -16
- data/lib/mongoid/threaded.rb +36 -0
- data/lib/mongoid/timestamps/timeless.rb +84 -10
- data/lib/mongoid/touchable.rb +28 -1
- data/lib/mongoid/traversable.rb +2 -2
- data/lib/mongoid/validatable/associated.rb +18 -2
- data/lib/mongoid/version.rb +1 -1
- data/lib/mongoid/warnings.rb +4 -0
- data/spec/integration/app_spec.rb +7 -1
- data/spec/integration/associations/belongs_to_spec.rb +13 -0
- data/spec/integration/associations/embedded_delayed_atomic_sets_spec.rb +216 -0
- data/spec/integration/associations/has_and_belongs_to_many_spec.rb +31 -2
- data/spec/integration/atomic/modifiers_spec.rb +283 -0
- data/spec/integration/dots_and_dollars_spec.rb +12 -2
- data/spec/integration/encryption_spec.rb +300 -0
- data/spec/integration/matcher_operator_data/regex.yml +21 -0
- data/spec/integration/matcher_regexp_timeout_spec.rb +215 -0
- data/spec/integration/query_operator_guard_spec.rb +89 -0
- data/spec/mongoid/association/accessors_spec.rb +35 -0
- data/spec/mongoid/association/eager_loadable_spec.rb +47 -0
- data/spec/mongoid/association/eager_spec.rb +2 -2
- data/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +33 -0
- data/spec/mongoid/association/fallback_spec.rb +425 -0
- data/spec/mongoid/association/macros_spec.rb +26 -0
- data/spec/mongoid/association/referenced/belongs_to/eager_spec.rb +17 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/buildable_spec.rb +11 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +48 -0
- data/spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb +50 -0
- data/spec/mongoid/association/referenced/has_many/eager_spec.rb +33 -0
- data/spec/mongoid/association/referenced/has_many/proxy_spec.rb +145 -0
- data/spec/mongoid/association/referenced/has_many_through/eager_spec.rb +58 -0
- data/spec/mongoid/association/referenced/has_many_through/proxy_spec.rb +62 -0
- data/spec/mongoid/association/referenced/has_many_through_spec.rb +399 -0
- data/spec/mongoid/association/referenced/has_one_through/eager_spec.rb +58 -0
- data/spec/mongoid/association/referenced/has_one_through/proxy_spec.rb +49 -0
- data/spec/mongoid/association/referenced/has_one_through_spec.rb +211 -0
- data/spec/mongoid/atomic/modifiers_spec.rb +347 -0
- data/spec/mongoid/attributes/nested_spec.rb +278 -8
- data/spec/mongoid/clients/factory_spec.rb +36 -0
- data/spec/mongoid/config/defaults_spec.rb +33 -0
- data/spec/mongoid/config/encryption_spec.rb +451 -0
- data/spec/mongoid/contextual/aggregable/memory_spec.rb +91 -0
- data/spec/mongoid/contextual/memory_spec.rb +177 -0
- data/spec/mongoid/criteria/includable_spec.rb +1221 -0
- data/spec/mongoid/criteria/queryable/selectable_logical_spec.rb +15 -1
- data/spec/mongoid/criteria/queryable/selectable_where_spec.rb +201 -0
- data/spec/mongoid/criteria_spec.rb +3 -0
- data/spec/mongoid/encryptable_spec.rb +115 -0
- data/spec/mongoid/errors/config_redactor_spec.rb +112 -0
- data/spec/mongoid/errors/mixed_client_configuration_spec.rb +31 -0
- data/spec/mongoid/errors/no_client_database_spec.rb +29 -0
- data/spec/mongoid/errors/no_client_hosts_spec.rb +26 -0
- data/spec/mongoid/errors/readonly_association_spec.rb +34 -0
- data/spec/mongoid/extensions/vector_spec.rb +175 -0
- data/spec/mongoid/fields_spec.rb +8 -0
- data/spec/mongoid/matcher/regexp_budget_spec.rb +570 -0
- data/spec/mongoid/search_indexable_spec.rb +321 -9
- data/spec/mongoid/tasks/database_spec.rb +31 -0
- data/spec/mongoid/timestamps/timeless_spec.rb +197 -5
- data/spec/mongoid/touchable_spec.rb +242 -0
- data/spec/mongoid/touchable_spec_models.rb +47 -0
- data/spec/mongoid/validatable/associated_spec.rb +19 -0
- data/spec/spec_helper.rb +5 -1
- data/spec/support/crypt/models.rb +253 -0
- data/spec/support/models/modifier_models.rb +65 -0
- 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
|