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.
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