praxis-mapper 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +26 -0
  3. data/.rspec +3 -0
  4. data/.travis.yml +4 -0
  5. data/CHANGELOG.md +83 -0
  6. data/Gemfile +3 -0
  7. data/Gemfile.lock +102 -0
  8. data/Guardfile +11 -0
  9. data/LICENSE +22 -0
  10. data/README.md +19 -0
  11. data/Rakefile +14 -0
  12. data/lib/praxis-mapper/config_hash.rb +40 -0
  13. data/lib/praxis-mapper/connection_manager.rb +102 -0
  14. data/lib/praxis-mapper/finalizable.rb +38 -0
  15. data/lib/praxis-mapper/identity_map.rb +532 -0
  16. data/lib/praxis-mapper/logging.rb +22 -0
  17. data/lib/praxis-mapper/model.rb +430 -0
  18. data/lib/praxis-mapper/query/base.rb +213 -0
  19. data/lib/praxis-mapper/query/sql.rb +183 -0
  20. data/lib/praxis-mapper/query_statistics.rb +46 -0
  21. data/lib/praxis-mapper/resource.rb +226 -0
  22. data/lib/praxis-mapper/support/factory_girl.rb +104 -0
  23. data/lib/praxis-mapper/support/memory_query.rb +34 -0
  24. data/lib/praxis-mapper/support/memory_repository.rb +44 -0
  25. data/lib/praxis-mapper/support/schema_dumper.rb +66 -0
  26. data/lib/praxis-mapper/support/schema_loader.rb +56 -0
  27. data/lib/praxis-mapper/support.rb +2 -0
  28. data/lib/praxis-mapper/version.rb +5 -0
  29. data/lib/praxis-mapper.rb +60 -0
  30. data/praxis-mapper.gemspec +38 -0
  31. data/spec/praxis-mapper/connection_manager_spec.rb +117 -0
  32. data/spec/praxis-mapper/identity_map_spec.rb +905 -0
  33. data/spec/praxis-mapper/logging_spec.rb +9 -0
  34. data/spec/praxis-mapper/memory_repository_spec.rb +56 -0
  35. data/spec/praxis-mapper/model_spec.rb +389 -0
  36. data/spec/praxis-mapper/query/base_spec.rb +317 -0
  37. data/spec/praxis-mapper/query/sql_spec.rb +184 -0
  38. data/spec/praxis-mapper/resource_spec.rb +154 -0
  39. data/spec/praxis_mapper_spec.rb +21 -0
  40. data/spec/spec_fixtures.rb +12 -0
  41. data/spec/spec_helper.rb +63 -0
  42. data/spec/support/spec_models.rb +215 -0
  43. data/spec/support/spec_resources.rb +39 -0
  44. 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