praxis-mapper 3.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.
- checksums.yaml +7 -0
- data/.gitignore +26 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/CHANGELOG.md +83 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +102 -0
- data/Guardfile +11 -0
- data/LICENSE +22 -0
- data/README.md +19 -0
- data/Rakefile +14 -0
- data/lib/praxis-mapper/config_hash.rb +40 -0
- data/lib/praxis-mapper/connection_manager.rb +102 -0
- data/lib/praxis-mapper/finalizable.rb +38 -0
- data/lib/praxis-mapper/identity_map.rb +532 -0
- data/lib/praxis-mapper/logging.rb +22 -0
- data/lib/praxis-mapper/model.rb +430 -0
- data/lib/praxis-mapper/query/base.rb +213 -0
- data/lib/praxis-mapper/query/sql.rb +183 -0
- data/lib/praxis-mapper/query_statistics.rb +46 -0
- data/lib/praxis-mapper/resource.rb +226 -0
- data/lib/praxis-mapper/support/factory_girl.rb +104 -0
- data/lib/praxis-mapper/support/memory_query.rb +34 -0
- data/lib/praxis-mapper/support/memory_repository.rb +44 -0
- data/lib/praxis-mapper/support/schema_dumper.rb +66 -0
- data/lib/praxis-mapper/support/schema_loader.rb +56 -0
- data/lib/praxis-mapper/support.rb +2 -0
- data/lib/praxis-mapper/version.rb +5 -0
- data/lib/praxis-mapper.rb +60 -0
- data/praxis-mapper.gemspec +38 -0
- data/spec/praxis-mapper/connection_manager_spec.rb +117 -0
- data/spec/praxis-mapper/identity_map_spec.rb +905 -0
- data/spec/praxis-mapper/logging_spec.rb +9 -0
- data/spec/praxis-mapper/memory_repository_spec.rb +56 -0
- data/spec/praxis-mapper/model_spec.rb +389 -0
- data/spec/praxis-mapper/query/base_spec.rb +317 -0
- data/spec/praxis-mapper/query/sql_spec.rb +184 -0
- data/spec/praxis-mapper/resource_spec.rb +154 -0
- data/spec/praxis_mapper_spec.rb +21 -0
- data/spec/spec_fixtures.rb +12 -0
- data/spec/spec_helper.rb +63 -0
- data/spec/support/spec_models.rb +215 -0
- data/spec/support/spec_resources.rb +39 -0
- metadata +298 -0
@@ -0,0 +1,905 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
describe Praxis::Mapper::IdentityMap do
|
4
|
+
let(:scope) { {} }
|
5
|
+
let(:model) { SimpleModel }
|
6
|
+
|
7
|
+
subject(:identity_map) { Praxis::Mapper::IdentityMap.new(scope) }
|
8
|
+
|
9
|
+
let(:repository) { subject.connection(:default) }
|
10
|
+
|
11
|
+
let(:rows) {[
|
12
|
+
{:id => 1, :name => "george jr", :parent_id => 1, :description => "one"},
|
13
|
+
{:id => 2, :name => "george iii", :parent_id => 2, :description => "two"},
|
14
|
+
{:id => 3, :name => "george xvi", :parent_id => 2, :description => "three"}
|
15
|
+
|
16
|
+
]}
|
17
|
+
|
18
|
+
let(:person_rows) {[
|
19
|
+
{id: 1, email: "one@example.com", address_id: 1, prior_address_ids:JSON.dump([2,3])},
|
20
|
+
{id: 2, email: "two@example.com", address_id: 2, prior_address_ids:JSON.dump([2,3])},
|
21
|
+
{id: 3, email: "three@example.com", address_id: 2, prior_address_ids: nil},
|
22
|
+
{id: 4, email: "four@example.com", address_id: 3, prior_address_ids: nil},
|
23
|
+
{id: 5, email: "five@example.com", address_id: 3, prior_address_ids: nil}
|
24
|
+
|
25
|
+
]}
|
26
|
+
|
27
|
+
let(:address_rows) {[
|
28
|
+
{id: 1, owner_id: 1, state: 'OR'},
|
29
|
+
{id: 2, owner_id: 3, state: 'CA'},
|
30
|
+
{id: 3, owner_id: 1, state: 'OR'}
|
31
|
+
]}
|
32
|
+
|
33
|
+
before do
|
34
|
+
repository.clear!
|
35
|
+
repository.insert(SimpleModel, rows)
|
36
|
+
repository.insert(PersonModel, person_rows)
|
37
|
+
repository.insert(AddressModel, address_rows)
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
context ".setup!" do
|
42
|
+
|
43
|
+
context "with an identity map" do
|
44
|
+
before do
|
45
|
+
Praxis::Mapper::IdentityMap.current = Praxis::Mapper::IdentityMap.new(scope)
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'that has been cleared' do
|
49
|
+
before do
|
50
|
+
Praxis::Mapper::IdentityMap.current.clear!
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'returns the identity_map' do
|
54
|
+
Praxis::Mapper::IdentityMap.setup!(scope).should be_kind_of(Praxis::Mapper::IdentityMap)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'sets new scopes correctly' do
|
58
|
+
Praxis::Mapper::IdentityMap.setup!({:foo => :bar}).scope.should_not be scope
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
context ".current" do
|
68
|
+
it 'setting' do
|
69
|
+
Praxis::Mapper::IdentityMap.current = subject
|
70
|
+
Thread.current[:_praxis_mapper_identity_map].should be(subject)
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'getting' do
|
74
|
+
Thread.current[:_praxis_mapper_identity_map] = subject
|
75
|
+
Praxis::Mapper::IdentityMap.current.should be(subject)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
context "#connection" do
|
81
|
+
let(:connection) { double("connection") }
|
82
|
+
it 'proxies through to the ConnectionManager' do
|
83
|
+
Praxis::Mapper::ConnectionManager.any_instance.should_receive(:checkout).with(:default).and_return(connection)
|
84
|
+
|
85
|
+
subject.connection(:default).should == connection
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
context "#load" do
|
91
|
+
let(:query_proc) { Proc.new { } }
|
92
|
+
let(:query_mock) { double("query mock", track: Set.new, where: nil, load: Set.new) }
|
93
|
+
|
94
|
+
|
95
|
+
context 'with normal queries' do
|
96
|
+
let(:record_query) do
|
97
|
+
Praxis::Mapper::Support::MemoryQuery.new(identity_map, model) do
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
let(:records) { rows.collect { |row| m = model.new(row); m._query = record_query; m } }
|
102
|
+
|
103
|
+
it 'builds a query, executes, freezes it, and returns the query results' do
|
104
|
+
Praxis::Mapper::Support::MemoryQuery.any_instance.should_receive(:execute).and_return(records)
|
105
|
+
Praxis::Mapper::Support::MemoryQuery.any_instance.should_receive(:freeze)
|
106
|
+
identity_map.load(model, &query_proc).should === records
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'with a query with a subload' do
|
112
|
+
context 'for a many_to_one association' do
|
113
|
+
before do
|
114
|
+
identity_map.load PersonModel do
|
115
|
+
load :address
|
116
|
+
end
|
117
|
+
end
|
118
|
+
it 'loads the association records' do
|
119
|
+
identity_map.all(PersonModel).should have(5).items
|
120
|
+
identity_map.all(AddressModel).should have(3).items
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
context 'for a one_to_many association' do
|
126
|
+
before do
|
127
|
+
identity_map.load AddressModel do
|
128
|
+
load :residents
|
129
|
+
end
|
130
|
+
end
|
131
|
+
it 'loads the association records' do
|
132
|
+
identity_map.all(PersonModel).should have(5).items
|
133
|
+
identity_map.all(AddressModel).should have(3).items
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
context 'with nested loads and preexisting records loaded' do
|
138
|
+
before do
|
139
|
+
identity_map.load AddressModel do
|
140
|
+
where id: 2
|
141
|
+
end
|
142
|
+
|
143
|
+
identity_map.load PersonModel do
|
144
|
+
where id: 1
|
145
|
+
load :prior_addresses do
|
146
|
+
load :owner
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'works' do
|
152
|
+
expect { identity_map.get(PersonModel, id: 3) }.to_not raise_error
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
|
157
|
+
context 'with nested loads with a where clause' do
|
158
|
+
before do
|
159
|
+
identity_map.load PersonModel do
|
160
|
+
where id: 2
|
161
|
+
load :prior_addresses do
|
162
|
+
where state: 'CA'
|
163
|
+
load :owner
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'applies the where clause to the nested load' do
|
169
|
+
expect { identity_map.get(AddressModel, id: 3) }.to raise_error
|
170
|
+
end
|
171
|
+
|
172
|
+
it 'passes the resulting records a subsequent load' do
|
173
|
+
expect { identity_map.get(PersonModel, id: 3) }.to_not raise_error
|
174
|
+
expect { identity_map.get(PersonModel, id: 1) }.to raise_error
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
|
183
|
+
context "#get_staged" do
|
184
|
+
let(:stage) { {:id => [1,2], :names => ["george jr", "george XVI"]} }
|
185
|
+
before do
|
186
|
+
identity_map.get_staged(model,:id).should == Set.new
|
187
|
+
identity_map.stage(model, stage)
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'supports getting ids for a single identity' do
|
191
|
+
identity_map.get_staged(model, :id).should == Set.new(stage[:id])
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
|
196
|
+
|
197
|
+
context "#stage" do
|
198
|
+
|
199
|
+
before do
|
200
|
+
identity_map.stage(model, stage)
|
201
|
+
end
|
202
|
+
|
203
|
+
|
204
|
+
context "with one item for one identity" do
|
205
|
+
let(:stage) { {:id => 1} }
|
206
|
+
|
207
|
+
it 'stages the key' do
|
208
|
+
identity_map.get_staged(model,:id).should == Set.new([1])
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
context "with multiple items for one identity" do
|
213
|
+
let(:stage) { {:id => [1,2]} }
|
214
|
+
|
215
|
+
it 'stages the keys' do
|
216
|
+
identity_map.get_staged(model,:id).should == Set.new(stage[:id])
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
context 'with multiple items for multiple identities' do
|
221
|
+
let(:stage) { {:id => [1,2], :names => ["george jr", "george XVI"]} }
|
222
|
+
|
223
|
+
|
224
|
+
it 'stages the keys' do
|
225
|
+
stage.each do |identity,values|
|
226
|
+
identity_map.get_staged(model,identity).should == Set.new(values)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
end
|
231
|
+
|
232
|
+
end
|
233
|
+
|
234
|
+
|
235
|
+
context '#stage when staging something we already have loaded' do
|
236
|
+
|
237
|
+
before do
|
238
|
+
identity_map.load(model)
|
239
|
+
identity_map.get_staged(model,:id).should == Set.new
|
240
|
+
end
|
241
|
+
|
242
|
+
it 'is ignored' do
|
243
|
+
identity_map.stage(model, :id => [1] )
|
244
|
+
identity_map.get_staged(model,:id).should == Set.new
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
|
249
|
+
|
250
|
+
context "#finalize_model!" do
|
251
|
+
|
252
|
+
context 'with values staged for a single identity' do
|
253
|
+
let(:stage) { {:id => [1,2] } }
|
254
|
+
|
255
|
+
before do
|
256
|
+
identity_map.stage(model, stage)
|
257
|
+
end
|
258
|
+
|
259
|
+
it "does a multi-get for the unloaded ids and returns the query results" do
|
260
|
+
Praxis::Mapper::Support::MemoryQuery.any_instance.
|
261
|
+
should_receive(:multi_get).
|
262
|
+
with(:id, Set.new(stage[:id])).
|
263
|
+
and_return(rows[0..1])
|
264
|
+
Praxis::Mapper::Support::MemoryQuery.any_instance.should_receive(:freeze)
|
265
|
+
|
266
|
+
identity_map.finalize_model!(model).collect(&:id).should =~ rows[0..1].collect { |r| r[:id] }
|
267
|
+
end
|
268
|
+
|
269
|
+
context 'tracking associations' do
|
270
|
+
let(:track) { :parent }
|
271
|
+
|
272
|
+
it 'sets the track attribute on the generated query' do
|
273
|
+
Praxis::Mapper::Support::MemoryQuery.any_instance.should_receive(:multi_get).with(:id, Set.new(stage[:id])).and_return(rows[0..1])
|
274
|
+
Praxis::Mapper::Support::MemoryQuery.any_instance.should_receive(:track).with(track).at_least(:once).and_call_original
|
275
|
+
Praxis::Mapper::Support::MemoryQuery.any_instance.should_receive(:track).at_least(:once).and_call_original
|
276
|
+
Praxis::Mapper::Support::MemoryQuery.any_instance.should_receive(:freeze)
|
277
|
+
|
278
|
+
track_field = track
|
279
|
+
query = Praxis::Mapper::Support::MemoryQuery.new(identity_map,model) do
|
280
|
+
track track_field
|
281
|
+
end
|
282
|
+
|
283
|
+
identity_map.finalize_model!(model, query)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
end
|
288
|
+
|
289
|
+
|
290
|
+
context 'with values staged for multiple identities' do
|
291
|
+
let(:stage) { {:id => [1,2], :name => ["george jr", "george xvi"]} }
|
292
|
+
|
293
|
+
before do
|
294
|
+
identity_map.stage(model, stage)
|
295
|
+
end
|
296
|
+
|
297
|
+
it 'does a multi_get for ids, then one for remaining names, freezes the query, and consolidated query reults' do
|
298
|
+
query = Praxis::Mapper::Support::MemoryQuery.new(identity_map, model)
|
299
|
+
Praxis::Mapper::Support::MemoryQuery.stub(:new).and_return(query)
|
300
|
+
|
301
|
+
query.should_receive(:multi_get).with(:id, Set.new(stage[:id])).and_return(rows[0..1])
|
302
|
+
query.should_receive(:multi_get).with(:name, Set.new(['george xvi'])).and_return([rows[2]])
|
303
|
+
query.should_receive(:freeze).once
|
304
|
+
|
305
|
+
expected = rows.collect { |r| r[:id] }
|
306
|
+
result = identity_map.finalize_model!(model)
|
307
|
+
|
308
|
+
result.collect(&:id).should =~ expected
|
309
|
+
end
|
310
|
+
|
311
|
+
end
|
312
|
+
|
313
|
+
context 'with unfound values remaining at the end of finalizing' do
|
314
|
+
let(:stage) { {:id => [1,2,4] } }
|
315
|
+
|
316
|
+
before do
|
317
|
+
identity_map.stage(model, stage)
|
318
|
+
|
319
|
+
Praxis::Mapper::Support::MemoryQuery.any_instance.should_receive(:multi_get).with(:id, Set.new(stage[:id])).and_return(rows[0..1])
|
320
|
+
Praxis::Mapper::Support::MemoryQuery.any_instance.should_receive(:freeze)
|
321
|
+
end
|
322
|
+
|
323
|
+
it 'adds the missing keys to the identity map with nil values' do
|
324
|
+
identity_map.finalize_model!(model)
|
325
|
+
identity_map.get(model,:id => 4).should be_nil
|
326
|
+
end
|
327
|
+
|
328
|
+
|
329
|
+
end
|
330
|
+
|
331
|
+
context 'with values staged for a non-identity key' do
|
332
|
+
let(:model) { AddressModel }
|
333
|
+
let(:stage) {{
|
334
|
+
id: Set.new([1,2]),
|
335
|
+
owner_id: Set.new([1,2,3])
|
336
|
+
}}
|
337
|
+
|
338
|
+
before do
|
339
|
+
identity_map.stage(model, stage)
|
340
|
+
owner_id_response = [{id:1}, {id:2}]
|
341
|
+
|
342
|
+
query = Praxis::Mapper::Support::MemoryQuery.new(identity_map, model)
|
343
|
+
Praxis::Mapper::Support::MemoryQuery.stub(:new).and_return(query)
|
344
|
+
|
345
|
+
query.should_receive(:multi_get).
|
346
|
+
with(:owner_id, Set.new(stage[:owner_id]), select: [:id]).ordered.and_return(owner_id_response)
|
347
|
+
query.should_receive(:multi_get).
|
348
|
+
with(:id, Set.new(stage[:id])).ordered.and_return(person_rows)
|
349
|
+
|
350
|
+
query.should_receive(:freeze)
|
351
|
+
end
|
352
|
+
|
353
|
+
it 'first resolves staged non-identity keys to identities' do
|
354
|
+
identity_map.finalize_model!(model)
|
355
|
+
end
|
356
|
+
|
357
|
+
end
|
358
|
+
|
359
|
+
|
360
|
+
context 'for a model with _queries staged for it too....' do
|
361
|
+
|
362
|
+
before do
|
363
|
+
identity_map.clear!
|
364
|
+
identity_map.load(PersonModel) do
|
365
|
+
where id: 2
|
366
|
+
track :address do
|
367
|
+
context :default
|
368
|
+
end
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
|
373
|
+
it 'finalize_model!' do
|
374
|
+
identity_map.finalize_model!(AddressModel)
|
375
|
+
identity_map.get_staged(PersonModel, :id).should eq(Set[3])
|
376
|
+
end
|
377
|
+
|
378
|
+
it 'finalize! retrieves records for newly-staged keys.' do
|
379
|
+
identity_map.finalize!
|
380
|
+
identity_map.get(PersonModel, id: 3).id.should eq(3)
|
381
|
+
end
|
382
|
+
|
383
|
+
end
|
384
|
+
|
385
|
+
|
386
|
+
context 'with context that includes a track with a block' do
|
387
|
+
before do
|
388
|
+
identity_map.clear!
|
389
|
+
|
390
|
+
identity_map.load(AddressModel) do
|
391
|
+
context :current
|
392
|
+
where id: 3
|
393
|
+
end
|
394
|
+
|
395
|
+
identity_map.finalize!
|
396
|
+
end
|
397
|
+
|
398
|
+
it 'loads the owner and residents' do
|
399
|
+
identity_map.all(PersonModel).should have(3).items
|
400
|
+
identity_map.all(PersonModel).collect(&:id).should =~ [1,4,5]
|
401
|
+
end
|
402
|
+
|
403
|
+
it 'loads the owner with the :default and :tiny contexts' do
|
404
|
+
owner = identity_map.get(PersonModel, id: 1)
|
405
|
+
|
406
|
+
owner._query.contexts.should eq(Set[:default,:tiny])
|
407
|
+
owner._query.track.should eq(Set[:address])
|
408
|
+
end
|
409
|
+
|
410
|
+
it 'loads the residents with the :default and :tiny contexts' do
|
411
|
+
address = identity_map.get(AddressModel, id: 3)
|
412
|
+
|
413
|
+
address.residents.should have(2).items
|
414
|
+
address.residents.each do |resident|
|
415
|
+
resident._query.contexts.should eq(Set[:default, :tiny])
|
416
|
+
resident._query.track.should eq(Set[:address])
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
end
|
421
|
+
|
422
|
+
context 'and a where clause' do
|
423
|
+
|
424
|
+
|
425
|
+
context 'for a many_to_one association' do
|
426
|
+
before do
|
427
|
+
identity_map.load PersonModel do
|
428
|
+
track :address do
|
429
|
+
where state: 'OR'
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
it 'raises an error ' do
|
434
|
+
expect { identity_map.finalize!(AddressModel) }.to raise_error(/type :many_to_one is not supported/)
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
context 'for a array_to_many association' do
|
439
|
+
before do
|
440
|
+
identity_map.load PersonModel do
|
441
|
+
track :prior_addresses do
|
442
|
+
where state: 'OR'
|
443
|
+
end
|
444
|
+
end
|
445
|
+
end
|
446
|
+
it 'raises an error ' do
|
447
|
+
expect { identity_map.finalize!(AddressModel) }.to raise_error(/type :array_to_many is not supported/)
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
context 'for a one_to_many association' do
|
452
|
+
before do
|
453
|
+
identity_map.load PersonModel do
|
454
|
+
track :properties do
|
455
|
+
where state: 'OR'
|
456
|
+
end
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
460
|
+
it 'honors the where clause' do
|
461
|
+
identity_map.finalize!
|
462
|
+
identity_map.all(AddressModel).should_not be_empty
|
463
|
+
identity_map.all(AddressModel).all? { |address| address.state == 'OR' }.should be(true)
|
464
|
+
end
|
465
|
+
|
466
|
+
end
|
467
|
+
end
|
468
|
+
|
469
|
+
end
|
470
|
+
|
471
|
+
context 'adding and retrieving records' do
|
472
|
+
let(:record_query) do
|
473
|
+
Praxis::Mapper::Support::MemoryQuery.new(identity_map, model) do
|
474
|
+
track :parent
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
let(:records) { rows.collect { |row| m = model.new(row); m._query = record_query; m } }
|
479
|
+
|
480
|
+
before do
|
481
|
+
identity_map.add_records(records)
|
482
|
+
end
|
483
|
+
|
484
|
+
|
485
|
+
it 'sets record.identity_map' do
|
486
|
+
records.each { |record| record.identity_map.should be(identity_map) }
|
487
|
+
end
|
488
|
+
|
489
|
+
# FIXME: see similar test for unloaded keys above
|
490
|
+
it 'round-trips nicely...' do
|
491
|
+
identity_map.rows_for(model).should =~ records
|
492
|
+
end
|
493
|
+
|
494
|
+
it 'updates primary key indices' do
|
495
|
+
identity_map.row_by_key(model,:id,1).should == records.first
|
496
|
+
identity_map.row_by_key(model,:name,"george jr").should == records.first
|
497
|
+
end
|
498
|
+
|
499
|
+
it 'does not re-add existing rows' do
|
500
|
+
new_record = SimpleModel.new(
|
501
|
+
:id => records.first.id,
|
502
|
+
:name => records.first.name,
|
503
|
+
:parent_id => records.first.parent_id,
|
504
|
+
:description => "new description"
|
505
|
+
)
|
506
|
+
identity_map.add_records([new_record])
|
507
|
+
|
508
|
+
identity_map.rows_for(model).should =~ records
|
509
|
+
end
|
510
|
+
|
511
|
+
it 'stages tracked associations' do
|
512
|
+
identity_map.get_staged(ParentModel,:id).should == Set.new([1,2])
|
513
|
+
end
|
514
|
+
|
515
|
+
context 'with a tracked array association' do
|
516
|
+
let(:records) { [YamlArrayModel.new(:id => 1, :parent_ids => YAML.dump([1,2]) )].each { |m| m._query = records_query } }
|
517
|
+
let(:records_query) do
|
518
|
+
Praxis::Mapper::Support::MemoryQuery.new(identity_map, YamlArrayModel) do
|
519
|
+
track :parents
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
it 'stages the deserialized relationship ids' do
|
524
|
+
identity_map.get_staged(ParentModel,:id).should == Set.new([1,2])
|
525
|
+
end
|
526
|
+
|
527
|
+
context 'with a nil value for the association' do
|
528
|
+
let(:records) { [YamlArrayModel.new(:id => 1, :parent_ids => nil)] }
|
529
|
+
let(:opts) { {:track => [:parents]} }
|
530
|
+
|
531
|
+
it 'does not stage anything' do
|
532
|
+
identity_map.get_staged(ParentModel,:id).should == Set.new
|
533
|
+
end
|
534
|
+
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
|
540
|
+
context "#add_records" do
|
541
|
+
|
542
|
+
|
543
|
+
context "with a tracked many_to_one association " do
|
544
|
+
before do
|
545
|
+
identity_map.load(SimpleModel) do
|
546
|
+
track :parent
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
it 'sets loaded records #identity_map' do
|
551
|
+
identity_map.all(model).each { |record| record.identity_map.should be(identity_map) }
|
552
|
+
end
|
553
|
+
|
554
|
+
it 'adds loaded records to the identity map' do
|
555
|
+
rows.all? do |row|
|
556
|
+
identity_map.rows_for(model).any? { |record| record.id == row[:id] }
|
557
|
+
end.should be(true)
|
558
|
+
end
|
559
|
+
|
560
|
+
it 'adds ids for tracked associations to unloaded ids' do
|
561
|
+
identity_map.get_staged(ParentModel,:id).should == Set.new([1,2])
|
562
|
+
end
|
563
|
+
|
564
|
+
it 'does not add ids for untracked associations to unloaded ids' do
|
565
|
+
identity_map.get_staged(OtherModel,:id).should == Set.new
|
566
|
+
end
|
567
|
+
|
568
|
+
context "loading missing ParentModel records" do
|
569
|
+
let(:parent_rows) {[
|
570
|
+
{:id => 1, :name => "parent one"}
|
571
|
+
]}
|
572
|
+
|
573
|
+
before do
|
574
|
+
identity_map.get_staged(ParentModel,:id).should == Set.new([1,2])
|
575
|
+
|
576
|
+
repository.insert(ParentModel, parent_rows)
|
577
|
+
identity_map.load(ParentModel)
|
578
|
+
end
|
579
|
+
|
580
|
+
it 'removes only those ids from unloaded_ids that correspond to loaded rows' do
|
581
|
+
identity_map.get_staged(ParentModel,:id).should == Set.new([2])
|
582
|
+
end
|
583
|
+
|
584
|
+
end
|
585
|
+
end
|
586
|
+
|
587
|
+
context 'with a tracked one_to_many association' do
|
588
|
+
|
589
|
+
before do
|
590
|
+
identity_map.load(PersonModel) do
|
591
|
+
track :properties
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
it 'adds loaded records to the identity map' do
|
596
|
+
identity_map.rows_for(PersonModel).collect(&:id).should =~ person_rows.collect { |r| r[:id] }
|
597
|
+
|
598
|
+
end
|
599
|
+
|
600
|
+
it 'adds ids for tracked associations to unloaded ids' do
|
601
|
+
identity_map.get_staged(AddressModel,:owner_id).should == Set.new([1,2,3,4,5])
|
602
|
+
end
|
603
|
+
|
604
|
+
end
|
605
|
+
|
606
|
+
# TODO: track whether a model is finalized
|
607
|
+
#it 'tracks whether a model has been finalized'
|
608
|
+
|
609
|
+
context 'composite identity support' do
|
610
|
+
let(:composite_id_rows) {[
|
611
|
+
{:id => 1, :type => "foo", :state => "running"},
|
612
|
+
{:id => 2, :type => "bar", :state => "terminated"}
|
613
|
+
]}
|
614
|
+
|
615
|
+
context "loading records" do
|
616
|
+
|
617
|
+
context "for a model with a composite identity" do
|
618
|
+
before do
|
619
|
+
repository.insert(CompositeIdModel, composite_id_rows)
|
620
|
+
identity_map.load(CompositeIdModel)
|
621
|
+
end
|
622
|
+
|
623
|
+
it 'loads the rows normally' do
|
624
|
+
identity_map.rows_for(CompositeIdModel).collect(&:id).should =~ composite_id_rows.collect { |r| r[:id] }
|
625
|
+
end
|
626
|
+
|
627
|
+
it 'indexes the rows by the composite identity' do
|
628
|
+
identity_map.row_by_key(CompositeIdModel,[:id,:type],[1,"foo"]).state == composite_id_rows.first[:state]
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
context 'tracking composite associations' do
|
633
|
+
let(:child_rows) {[
|
634
|
+
{:id => 10, :composite_id => 1, :composite_type => "foo", :name => "something"},
|
635
|
+
{:id => 11, :composite_id => 2, :composite_type => "bar", :name => "nothing"}
|
636
|
+
]}
|
637
|
+
|
638
|
+
before do
|
639
|
+
repository.insert(:other_model, child_rows)
|
640
|
+
identity_map.load(OtherModel) do
|
641
|
+
track :composite_model
|
642
|
+
end
|
643
|
+
|
644
|
+
end
|
645
|
+
|
646
|
+
it 'stages the CompositeIdModel identity' do
|
647
|
+
identity_map.get_staged(CompositeIdModel,[:id,:type]).should == Set.new([[1,"foo"],[2,"bar"]])
|
648
|
+
end
|
649
|
+
context 'when one or more of the values in the composite key is nil' do
|
650
|
+
let(:child_rows) {[
|
651
|
+
{:id => 10, :composite_id => 1, :composite_type => "foo", :name => "something"},
|
652
|
+
{:id => 11, :composite_id => nil, :composite_type => "bar", :name => "nothing"},
|
653
|
+
{:id => 12, :composite_id => nil, :composite_type => nil, :name => "nothing"}
|
654
|
+
]}
|
655
|
+
it 'skips staging them' do
|
656
|
+
identity_map.get_staged(CompositeIdModel,[:id,:type]).should == Set.new([[1,"foo"]])
|
657
|
+
end
|
658
|
+
end
|
659
|
+
end
|
660
|
+
|
661
|
+
|
662
|
+
context 'tracking composite associations through arrays' do
|
663
|
+
let(:child_rows) {[
|
664
|
+
{id: 10,type: 'CompositeArrayModel',composite_array_keys: JSON.dump([[1,"foo"],[2,"bar"]]), name: "something"},
|
665
|
+
{id: 11,type: 'CompositeArrayModel',composite_array_keys: JSON.dump([[2,"bar"],[3,"baz"]]), name: "something"}
|
666
|
+
]}
|
667
|
+
|
668
|
+
before do
|
669
|
+
repository.insert(:composite_array_model, child_rows)
|
670
|
+
identity_map.load(CompositeArrayModel) do
|
671
|
+
track :composite_id_models
|
672
|
+
end
|
673
|
+
|
674
|
+
end
|
675
|
+
|
676
|
+
it 'stages the CompositeIdModel identity' do
|
677
|
+
identity_map.get_staged(CompositeIdModel,[:id,:type]).should == Set.new([[1,"foo"],[2,"bar"],[3,"baz"]])
|
678
|
+
end
|
679
|
+
|
680
|
+
context 'when one or more of the values in the composite key (for any composite values in the array) is nil' do
|
681
|
+
let(:child_rows) {[
|
682
|
+
{:id => 10, :type => 'CompositeArrayModel', :composite_array_keys => JSON.dump([[1,"foo"],[2,"bar"]]), :name => "something"},
|
683
|
+
{:id => 11, :type => 'CompositeArrayModel', :composite_array_keys => JSON.dump([[2,"bar"],[3,nil]]), :name => "something"},
|
684
|
+
{:id => 13, :type => 'CompositeArrayModel', :composite_array_keys => JSON.dump([nil, [4,nil]]), :name => "something"}
|
685
|
+
]}
|
686
|
+
it 'skips staging them' do
|
687
|
+
identity_map.get_staged(CompositeIdModel,[:id,:type]).should == Set.new([[1,"foo"],[2,"bar"]])
|
688
|
+
end
|
689
|
+
end
|
690
|
+
end
|
691
|
+
end
|
692
|
+
|
693
|
+
end
|
694
|
+
|
695
|
+
context "#clear!" do
|
696
|
+
let(:stage) { {:id => [3,4]} }
|
697
|
+
|
698
|
+
before do
|
699
|
+
identity_map.load(SimpleModel)
|
700
|
+
identity_map.stage(model, stage)
|
701
|
+
|
702
|
+
identity_map.rows_for(SimpleModel).should_not be_empty
|
703
|
+
identity_map.get_staged(SimpleModel,:id).should == Set.new(stage[:id])
|
704
|
+
identity_map.get_staged(SimpleModel,:name).should == Set.new
|
705
|
+
|
706
|
+
identity_map.queries[SimpleModel].add(double("query"))
|
707
|
+
|
708
|
+
identity_map.clear!
|
709
|
+
end
|
710
|
+
|
711
|
+
|
712
|
+
it 'resets the rows' do
|
713
|
+
identity_map.rows_for(SimpleModel).should be_empty
|
714
|
+
end
|
715
|
+
|
716
|
+
|
717
|
+
it 'clears the staged rows' do
|
718
|
+
identity_map.get_staged(SimpleModel,:id).should == Set.new
|
719
|
+
end
|
720
|
+
|
721
|
+
it 'clears the row keys' do
|
722
|
+
expect { identity_map.row_by_key(SimpleModel,:id,1) }.to raise_error(Praxis::Mapper::IdentityMap::UnloadedRecordException)
|
723
|
+
end
|
724
|
+
|
725
|
+
it 'clears the query history' do
|
726
|
+
identity_map.queries.should have(0).items
|
727
|
+
end
|
728
|
+
|
729
|
+
end
|
730
|
+
|
731
|
+
context "#all" do
|
732
|
+
|
733
|
+
before do
|
734
|
+
identity_map.load(SimpleModel)
|
735
|
+
identity_map.load(PersonModel)
|
736
|
+
|
737
|
+
# pretend we staged :id => 4, tried to properly load it, but it
|
738
|
+
# was not present in the database.
|
739
|
+
identity_map.instance_variable_get(:@row_keys)[model][:id][4] = nil
|
740
|
+
end
|
741
|
+
|
742
|
+
it 'returns all records' do
|
743
|
+
identity_map.all(SimpleModel).collect(&:id).should =~ rows.collect { |r| r[:id] }
|
744
|
+
end
|
745
|
+
|
746
|
+
it 'filters records by one id' do
|
747
|
+
identity_map.all(SimpleModel, :id =>[1]).collect(&:id).should =~ [rows.first].collect { |r| r[:id] }
|
748
|
+
end
|
749
|
+
|
750
|
+
it 'filters records by multiple ids' do
|
751
|
+
identity_map.all(SimpleModel, :id => [1,2]).collect(&:id).should =~ rows[0..1].collect { |r| r[:id] }
|
752
|
+
end
|
753
|
+
|
754
|
+
it 'returns an empty array if nothing was found matching a condition' do
|
755
|
+
identity_map.all(SimpleModel, :id => [4]).should =~ []
|
756
|
+
end
|
757
|
+
|
758
|
+
end
|
759
|
+
|
760
|
+
context '#row_by_key' do
|
761
|
+
let(:stage) { {:id => [1,2,3,4]} }
|
762
|
+
before do
|
763
|
+
Praxis::Mapper::Support::MemoryQuery.any_instance.should_receive(:multi_get).with(:id, Set.new(stage[:id])).and_return(rows)
|
764
|
+
Praxis::Mapper::Support::MemoryQuery.any_instance.should_receive(:freeze)
|
765
|
+
identity_map.stage(model,stage)
|
766
|
+
identity_map.finalize_model!(model)
|
767
|
+
end
|
768
|
+
|
769
|
+
it 'raises UnloadedRecordException for unknown records' do
|
770
|
+
expect { identity_map.row_by_key(model,:id, 5) }.to raise_error(Praxis::Mapper::IdentityMap::UnloadedRecordException)
|
771
|
+
end
|
772
|
+
end
|
773
|
+
|
774
|
+
context "#<<" do
|
775
|
+
let(:records) { 3.times.collect { SimpleModel.generate} }
|
776
|
+
|
777
|
+
before do
|
778
|
+
records.each { |record| identity_map << record }
|
779
|
+
end
|
780
|
+
|
781
|
+
it 'stores a single record at a time' do
|
782
|
+
identity_map.rows_for(SimpleModel).should =~ records
|
783
|
+
end
|
784
|
+
|
785
|
+
it 'sets loaded records #identity_map' do
|
786
|
+
records.each { |record| record.identity_map.should be(identity_map) }
|
787
|
+
end
|
788
|
+
|
789
|
+
|
790
|
+
end
|
791
|
+
|
792
|
+
context "#get" do
|
793
|
+
before do
|
794
|
+
identity_map.load(model)
|
795
|
+
|
796
|
+
# pretend we staged :id => 4, tried to properly load it, but it
|
797
|
+
# was not present in the database.
|
798
|
+
identity_map.instance_variable_get(:@row_keys)[model][:id][4] = nil
|
799
|
+
end
|
800
|
+
|
801
|
+
it 'returns a single records by id' do
|
802
|
+
identity_map.get(model, :id => 1).id.should == rows.first[:id]
|
803
|
+
end
|
804
|
+
|
805
|
+
it 'returns nil for a record that was not found' do
|
806
|
+
identity_map.get(model, :id => 4).should be_nil
|
807
|
+
end
|
808
|
+
|
809
|
+
end
|
810
|
+
|
811
|
+
|
812
|
+
|
813
|
+
|
814
|
+
context 'statistics tracking' do
|
815
|
+
|
816
|
+
context 'in a new identity map' do
|
817
|
+
its(:queries) { should have(0).items }
|
818
|
+
it 'initializes new keys with a Set' do
|
819
|
+
subject.queries[model].should == Set.new
|
820
|
+
end
|
821
|
+
end
|
822
|
+
|
823
|
+
context 'after loading queries' do
|
824
|
+
|
825
|
+
before do
|
826
|
+
subject.queries[PersonModel].should have(0).item
|
827
|
+
end
|
828
|
+
|
829
|
+
it 'tracks for #load' do
|
830
|
+
identity_map.load(PersonModel)
|
831
|
+
|
832
|
+
subject.queries[PersonModel].should have(1).item
|
833
|
+
end
|
834
|
+
|
835
|
+
it 'tracks for #finalize_model!' do
|
836
|
+
identity_map.stage(PersonModel, id: [1,2])
|
837
|
+
identity_map.finalize_model!(PersonModel)
|
838
|
+
|
839
|
+
subject.queries[PersonModel].should have(1).item
|
840
|
+
end
|
841
|
+
|
842
|
+
end
|
843
|
+
|
844
|
+
|
845
|
+
end
|
846
|
+
|
847
|
+
context 'secondary index support' do
|
848
|
+
let(:people) { identity_map.all PersonModel }
|
849
|
+
let(:secondary_indexes) { identity_map.instance_variable_get(:@secondary_indexes) }
|
850
|
+
|
851
|
+
before do
|
852
|
+
identity_map.load PersonModel
|
853
|
+
end
|
854
|
+
|
855
|
+
context '#index' do
|
856
|
+
it 'lazily calls #reindex! to build secondary indexes' do
|
857
|
+
identity_map.should_receive(:reindex!).with(PersonModel, :address_id).and_call_original
|
858
|
+
|
859
|
+
identity_map.index(PersonModel, :address_id, 1)
|
860
|
+
end
|
861
|
+
|
862
|
+
it 'does not call #reindex! if not necessary' do
|
863
|
+
identity_map.index(PersonModel, :address_id, 1)
|
864
|
+
identity_map.should_not_receive(:reindex!)
|
865
|
+
identity_map.index(PersonModel, :address_id, 2)
|
866
|
+
end
|
867
|
+
|
868
|
+
it 'supports composite keys' do
|
869
|
+
identity_map.should_receive(:reindex!).with(PersonModel, [:id, :email]).and_call_original
|
870
|
+
values = identity_map.index(PersonModel, [:id, :email], [1,"one@example.com"])
|
871
|
+
|
872
|
+
person, *rest = values
|
873
|
+
rest.should be_empty
|
874
|
+
|
875
|
+
person.id.should eq(1)
|
876
|
+
person.email.should eq('one@example.com')
|
877
|
+
end
|
878
|
+
end
|
879
|
+
|
880
|
+
context '#reindex!' do
|
881
|
+
it 'builds a secondary index for the given model and key' do
|
882
|
+
identity_map.reindex!(PersonModel, :address_id)
|
883
|
+
|
884
|
+
secondary_indexes.should have_key(PersonModel)
|
885
|
+
secondary_indexes[PersonModel].should have_key(:address_id)
|
886
|
+
secondary_indexes[PersonModel][:address_id].keys.should =~ person_rows.collect { |person| person[:address_id] }.uniq
|
887
|
+
|
888
|
+
people.each do |person|
|
889
|
+
identity_map.index(PersonModel, :address_id, person.address_id).should include(person)
|
890
|
+
end
|
891
|
+
end
|
892
|
+
end
|
893
|
+
|
894
|
+
context '#all' do
|
895
|
+
it 'uses the index internally' do
|
896
|
+
identity_map.should_receive(:reindex!).with(PersonModel, :address_id).and_call_original
|
897
|
+
|
898
|
+
people = identity_map.all(PersonModel, address_id: [2])
|
899
|
+
people.should have(2).items
|
900
|
+
people.each { |person| person.address_id.should eq(2) }
|
901
|
+
end
|
902
|
+
end
|
903
|
+
end
|
904
|
+
end
|
905
|
+
end
|