identity_cache 0.2.5 → 0.3.0

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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +2 -4
  3. data/CHANGELOG.md +22 -0
  4. data/Gemfile +2 -0
  5. data/Gemfile.rails40 +1 -0
  6. data/Gemfile.rails41 +1 -0
  7. data/Gemfile.rails42 +1 -0
  8. data/README.md +7 -0
  9. data/identity_cache.gemspec +1 -1
  10. data/lib/identity_cache.rb +5 -2
  11. data/lib/identity_cache/belongs_to_caching.rb +13 -5
  12. data/lib/identity_cache/cache_key_generation.rb +12 -19
  13. data/lib/identity_cache/configuration_dsl.rb +83 -84
  14. data/lib/identity_cache/parent_model_expiration.rb +4 -3
  15. data/lib/identity_cache/query_api.rb +93 -91
  16. data/lib/identity_cache/version.rb +2 -2
  17. data/test/attribute_cache_test.rb +42 -63
  18. data/test/deeply_nested_associated_record_test.rb +1 -0
  19. data/test/denormalized_has_many_test.rb +18 -0
  20. data/test/denormalized_has_one_test.rb +15 -5
  21. data/test/fetch_multi_test.rb +25 -3
  22. data/test/fetch_test.rb +20 -7
  23. data/test/fixtures/serialized_record.mysql2 +0 -0
  24. data/test/fixtures/serialized_record.postgresql +0 -0
  25. data/test/helpers/active_record_objects.rb +10 -0
  26. data/test/helpers/database_connection.rb +6 -1
  27. data/test/helpers/serialization_format.rb +1 -1
  28. data/test/index_cache_test.rb +50 -25
  29. data/test/normalized_belongs_to_test.rb +21 -6
  30. data/test/normalized_has_many_test.rb +44 -0
  31. data/test/{fetch_multi_with_batched_associations_test.rb → prefetch_normalized_associations_test.rb} +41 -3
  32. data/test/save_test.rb +14 -14
  33. data/test/schema_change_test.rb +2 -0
  34. data/test/test_helper.rb +4 -4
  35. metadata +11 -10
  36. data/Gemfile.rails32 +0 -5
  37. data/test/fixtures/serialized_record +0 -0
@@ -10,6 +10,7 @@ class DeeplyNestedAssociatedRecordHasOneTest < IdentityCache::TestCase
10
10
 
11
11
  def test_deeply_nested_models_can_cache_has_many_associations
12
12
  assert_nothing_raised do
13
+ PolymorphicRecord.include(IdentityCache)
13
14
  Deeply::Nested::AssociatedRecord.has_many :polymorphic_records, as: 'owner'
14
15
  Deeply::Nested::AssociatedRecord.cache_has_many :polymorphic_records, inverse_name: :owner
15
16
  end
@@ -82,10 +82,28 @@ class DenormalizedHasManyTest < IdentityCache::TestCase
82
82
  end
83
83
  end
84
84
 
85
+ def test_cache_uses_inverse_of_on_association
86
+ Item.has_many :invertable_association, :inverse_of => :owner, :class_name => 'PolymorphicRecord', :as => "owner"
87
+ Item.cache_has_many :invertable_association, :embed => true
88
+ end
89
+
85
90
  def test_saving_associated_records_should_expire_itself_and_the_parents_cache
86
91
  child = @record.associated_records.first
87
92
  IdentityCache.cache.expects(:delete).with(child.primary_cache_index_key).once
88
93
  IdentityCache.cache.expects(:delete).with(@record.primary_cache_index_key)
89
94
  child.save!
90
95
  end
96
+
97
+ def test_unsupported_through_assocation
98
+ assert_raises IdentityCache::UnsupportedAssociationError, "caching through associations isn't supported" do
99
+ Item.has_many :deeply_through_associated_records, :through => :associated_records, foreign_key: 'associated_record_id', inverse_of: :item, :class_name => 'DeeplyAssociatedRecord'
100
+ Item.cache_has_many :deeply_through_associated_records, :embed => true
101
+ end
102
+ end
103
+
104
+ def test_cache_has_many_on_derived_model_raises
105
+ assert_raises(IdentityCache::DerivedModelError) do
106
+ StiRecordTypeA.cache_has_many :polymorphic_records, :inverse_name => :owner, :embed => true
107
+ end
108
+ end
91
109
  end
@@ -20,7 +20,7 @@ class DenormalizedHasOneTest < IdentityCache::TestCase
20
20
  assert_equal @record, record_from_cache_miss
21
21
  assert_not_nil @record.fetch_associated
22
22
  assert_equal @record.associated, record_from_cache_miss.fetch_associated
23
- assert fetch.has_been_called_with?(@record.secondary_cache_index_key_for_current_values([:title]))
23
+ assert fetch.has_been_called_with?(@record.attribute_cache_key_for_attribute_and_current_values(:id, [:title], true))
24
24
  assert fetch.has_been_called_with?(@record.primary_cache_index_key)
25
25
  end
26
26
 
@@ -29,7 +29,6 @@ class DenormalizedHasOneTest < IdentityCache::TestCase
29
29
  @record.associated = nil
30
30
  @record.save!
31
31
  @record.reload
32
- @record.send(:populate_association_caches)
33
32
  Item.expects(:resolve_cache_miss).with(@record.id).once.returns(@record)
34
33
 
35
34
  fetch = Spy.on(IdentityCache.cache, :fetch).and_call_through
@@ -41,7 +40,7 @@ class DenormalizedHasOneTest < IdentityCache::TestCase
41
40
  5.times do
42
41
  assert_nil record_from_cache_miss.fetch_associated
43
42
  end
44
- assert fetch.has_been_called_with?(@record.secondary_cache_index_key_for_current_values([:title]))
43
+ assert fetch.has_been_called_with?(@record.attribute_cache_key_for_attribute_and_current_values(:id, [:title], true))
45
44
  assert fetch.has_been_called_with?(@record.primary_cache_index_key)
46
45
  end
47
46
 
@@ -53,7 +52,6 @@ class DenormalizedHasOneTest < IdentityCache::TestCase
53
52
  end
54
53
 
55
54
  def test_on_cache_hit_record_should_come_back_with_cached_association
56
- @record.send(:populate_association_caches)
57
55
  Item.expects(:resolve_cache_miss).with(1).once.returns(@record)
58
56
  Item.fetch_by_title('foo')
59
57
 
@@ -69,7 +67,6 @@ class DenormalizedHasOneTest < IdentityCache::TestCase
69
67
  @record.save!
70
68
  @record.reload
71
69
 
72
- @record.send(:populate_association_caches)
73
70
  Item.expects(:resolve_cache_miss).with(1).once.returns(@record)
74
71
  Item.fetch_by_title('foo')
75
72
 
@@ -110,4 +107,17 @@ class DenormalizedHasOneTest < IdentityCache::TestCase
110
107
  Item.cache_has_one :polymorphic_record, :inverse_name => :owner, :embed => true
111
108
  end
112
109
  end
110
+
111
+ def test_unsupported_through_assocation
112
+ assert_raises IdentityCache::UnsupportedAssociationError, "caching through associations isn't supported" do
113
+ Item.has_one :deeply_associated, :through => :associated, :class_name => 'DeeplyAssociatedRecord'
114
+ Item.cache_has_one :deeply_associated, :embed => true
115
+ end
116
+ end
117
+
118
+ def test_cache_has_one_on_derived_model_raises
119
+ assert_raises(IdentityCache::DerivedModelError) do
120
+ StiRecordTypeA.cache_has_one :polymorphic_record, :inverse_name => :owner, :embed => true
121
+ end
122
+ end
113
123
  end
@@ -121,6 +121,18 @@ class FetchMultiTest < IdentityCache::TestCase
121
121
  assert_equal [@joe, @bob, @joe], Item.fetch_multi(@joe.id, @bob.id, @joe.id)
122
122
  end
123
123
 
124
+ def test_fetch_multi_with_duplicate_ids_hits_backend_once_per_id
125
+ cache_response = {
126
+ @joe_blob_key => cache_response_for(@joe),
127
+ @bob_blob_key => cache_response_for(@bob),
128
+ }
129
+
130
+ fetcher.expects(:fetch_multi).with([@joe_blob_key, @bob_blob_key]).returns(cache_response)
131
+ result = Item.fetch_multi(@joe.id, @bob.id, @joe.id)
132
+
133
+ assert_equal [@joe, @bob, @joe], result
134
+ end
135
+
124
136
  def test_fetch_multi_with_open_transactions_hits_the_database
125
137
  Item.connection.expects(:open_transactions).at_least_once.returns(1)
126
138
  fetcher.expects(:fetch_multi).never
@@ -199,6 +211,18 @@ class FetchMultiTest < IdentityCache::TestCase
199
211
  assert_equal cache_response_for(Item.find(@bob.id)), backend.read(@bob.primary_cache_index_key)
200
212
  end
201
213
 
214
+ def test_fetch_multi_raises_when_called_on_a_scope
215
+ assert_raises(IdentityCache::UnsupportedScopeError) do
216
+ Item.where(updated_at: nil).fetch_multi(@bob.id, @joe.id, @fred.id)
217
+ end
218
+ end
219
+
220
+ def test_fetch_multi_on_derived_model_raises
221
+ assert_raises(IdentityCache::DerivedModelError) do
222
+ StiRecordTypeA.fetch_multi(1, 2)
223
+ end
224
+ end
225
+
202
226
  private
203
227
 
204
228
  def populate_only_fred
@@ -210,9 +234,7 @@ class FetchMultiTest < IdentityCache::TestCase
210
234
  end
211
235
 
212
236
  def cache_response_for(record)
213
- coder = {:class => record.class}
214
- record.encode_with(coder)
215
- coder
237
+ {class: record.class, attributes: record.attributes_before_type_cast}
216
238
  end
217
239
 
218
240
  def with_batch_size(size)
@@ -11,17 +11,12 @@ class FetchTest < IdentityCache::TestCase
11
11
  @record = Item.new
12
12
  @record.id = 1
13
13
  @record.title = 'bob'
14
- @cached_value = {:class => @record.class}
15
- @record.encode_with(@cached_value)
14
+ @cached_value = {class: @record.class, attributes: @record.attributes_before_type_cast}
16
15
  @blob_key = "#{NAMESPACE}blob:Item:#{cache_hash("created_at:datetime,id:integer,item_id:integer,title:string,updated_at:datetime")}:1"
17
- @index_key = "#{NAMESPACE}index:Item:title:#{cache_hash('bob')}"
16
+ @index_key = "#{NAMESPACE}attr:Item:id:title:#{cache_hash('bob')}"
18
17
  end
19
18
 
20
19
  def test_fetch_with_garbage_input
21
- Item.connection.expects(:exec_query)
22
- .with(Item.where(id: 0).limit(1).to_sql, any_parameters)
23
- .returns(ActiveRecord::Result.new([], []))
24
-
25
20
  assert_equal nil, Item.fetch_by_id('garbage')
26
21
  end
27
22
 
@@ -199,4 +194,22 @@ class FetchTest < IdentityCache::TestCase
199
194
 
200
195
  assert_equal false, ActiveRecord::Base.connection_handler.active_connections?
201
196
  end
197
+
198
+ def test_fetch_raises_when_called_on_a_scope
199
+ assert_raises(IdentityCache::UnsupportedScopeError) do
200
+ Item.where(updated_at: nil).fetch(1)
201
+ end
202
+ end
203
+
204
+ def test_fetch_by_raises_when_called_on_a_scope
205
+ assert_raises(IdentityCache::UnsupportedScopeError) do
206
+ Item.where(updated_at: nil).fetch_by_id(1)
207
+ end
208
+ end
209
+
210
+ def test_fetch_on_derived_model_raises
211
+ assert_raises(IdentityCache::DerivedModelError) do
212
+ StiRecordTypeA.fetch(1)
213
+ end
214
+ end
202
215
  end
@@ -76,6 +76,14 @@ module ActiveRecordObjects
76
76
  include IdentityCache
77
77
  self.primary_key = "hashed_key"
78
78
  }
79
+
80
+ Object.send :const_set, 'StiRecord', Class.new(base) {
81
+ include IdentityCache
82
+ has_many :polymorphic_records, :as => 'owner'
83
+ }
84
+
85
+ Object.send :const_set, 'StiRecordTypeA', Class.new(StiRecord) {
86
+ }
79
87
  end
80
88
 
81
89
  def teardown_models
@@ -89,6 +97,8 @@ module ActiveRecordObjects
89
97
  Object.send :remove_const, 'Item'
90
98
  Object.send :remove_const, 'ItemTwo'
91
99
  Object.send :remove_const, 'KeyedRecord'
100
+ Object.send :remove_const, 'StiRecord'
101
+ Object.send :remove_const, 'StiRecordTypeA'
92
102
  Deeply::Nested.send :remove_const, 'AssociatedRecord'
93
103
  Deeply.send :remove_const, 'Nested'
94
104
  Object.send :remove_const, 'Deeply'
@@ -1,6 +1,10 @@
1
1
  module DatabaseConnection
2
+ def self.db_name
3
+ ENV.fetch('DB', 'mysql2')
4
+ end
5
+
2
6
  def self.setup
3
- db_config = ENV['DATABASE_URL'] || DEFAULT_CONFIG[ENV.fetch('DB', 'mysql2')]
7
+ db_config = ENV['DATABASE_URL'] || DEFAULT_CONFIG[db_name]
4
8
  begin
5
9
  ActiveRecord::Base.establish_connection(db_config)
6
10
  ActiveRecord::Base.connection
@@ -39,6 +43,7 @@ module DatabaseConnection
39
43
  :items => [[:integer, :item_id], [:string, :title], [:timestamps, null: true]],
40
44
  :items2 => [[:integer, :item_id], [:string, :title], [:timestamps, null: true]],
41
45
  :keyed_records => [[:string, :value], :primary_key => "hashed_key"],
46
+ :sti_records => [[:string, :type], [:string, :name]],
42
47
  }
43
48
 
44
49
  DEFAULT_CONFIG = {
@@ -24,7 +24,7 @@ module SerializationFormat
24
24
  end
25
25
 
26
26
  def serialized_record_file
27
- File.expand_path("../../fixtures/serialized_record", __FILE__)
27
+ File.expand_path("../../fixtures/serialized_record.#{DatabaseConnection.db_name}", __FILE__)
28
28
  end
29
29
 
30
30
  def serialize(record, anIO = nil)
@@ -8,17 +8,14 @@ class IndexCacheTest < IdentityCache::TestCase
8
8
  @record = Item.new
9
9
  @record.id = 1
10
10
  @record.title = 'bob'
11
- @cache_key = "#{NAMESPACE}index:Item:title:#{cache_hash(@record.title)}"
12
11
  end
13
12
 
14
- def test_fetch_with_garbage_input_should_use_properly_typed_sql
13
+ def test_fetch_with_garbage_input
15
14
  Item.cache_index :title, :id
16
15
 
17
- Item.connection.expects(:exec_query)
18
- .with(Item.select(:id).where(title: 'garbage', id: 0).to_sql, any_parameters)
19
- .returns(ActiveRecord::Result.new([], []))
20
-
21
- assert_equal [], Item.fetch_by_title_and_id('garbage', 'garbage')
16
+ assert_queries(1) do
17
+ assert_equal [], Item.fetch_by_title_and_id('garbage', 'garbage')
18
+ end
22
19
  end
23
20
 
24
21
  def test_fetch_with_unique_adds_limit_clause
@@ -34,54 +31,55 @@ class IndexCacheTest < IdentityCache::TestCase
34
31
  def test_unique_index_caches_nil
35
32
  Item.cache_index :title, :unique => true
36
33
  assert_equal nil, Item.fetch_by_title('bob')
37
- assert_equal IdentityCache::CACHED_NIL, backend.read(@cache_key)
34
+ assert_equal IdentityCache::CACHED_NIL, backend.read(cache_key(unique: true))
38
35
  end
39
36
 
40
37
  def test_unique_index_expired_by_new_record
41
38
  Item.cache_index :title, :unique => true
42
- IdentityCache.cache.write(@cache_key, IdentityCache::CACHED_NIL)
39
+ IdentityCache.cache.write(cache_key(unique: true), IdentityCache::CACHED_NIL)
43
40
  @record.save!
44
- assert_equal IdentityCache::DELETED, backend.read(@cache_key)
41
+ assert_equal IdentityCache::DELETED, backend.read(cache_key(unique: true))
45
42
  end
46
43
 
47
44
  def test_unique_index_filled_on_fetch_by
48
45
  Item.cache_index :title, :unique => true
49
46
  @record.save!
50
47
  assert_equal @record, Item.fetch_by_title('bob')
51
- assert_equal @record.id, backend.read(@cache_key)
48
+ assert_equal @record.id, backend.read(cache_key(unique: true))
52
49
  end
53
50
 
54
51
  def test_unique_index_expired_by_updated_record
55
52
  Item.cache_index :title, :unique => true
56
53
  @record.save!
57
- IdentityCache.cache.write(@cache_key, @record.id)
54
+ old_cache_key = cache_key(unique: true)
55
+ IdentityCache.cache.write(old_cache_key, @record.id)
58
56
 
59
57
  @record.title = 'robert'
60
- new_cache_key = "#{NAMESPACE}index:Item:title:#{cache_hash(@record.title)}"
58
+ new_cache_key = cache_key(unique: true)
61
59
  IdentityCache.cache.write(new_cache_key, IdentityCache::CACHED_NIL)
62
60
  @record.save!
63
- assert_equal IdentityCache::DELETED, backend.read(@cache_key)
61
+ assert_equal IdentityCache::DELETED, backend.read(old_cache_key)
64
62
  assert_equal IdentityCache::DELETED, backend.read(new_cache_key)
65
63
  end
66
64
 
67
65
  def test_non_unique_index_caches_empty_result
68
66
  Item.cache_index :title
69
67
  assert_equal [], Item.fetch_by_title('bob')
70
- assert_equal [], backend.read(@cache_key)
68
+ assert_equal [], backend.read(cache_key)
71
69
  end
72
70
 
73
71
  def test_non_unique_index_expired_by_new_record
74
72
  Item.cache_index :title
75
- IdentityCache.cache.write(@cache_key, [])
73
+ IdentityCache.cache.write(cache_key, [])
76
74
  @record.save!
77
- assert_equal IdentityCache::DELETED, backend.read(@cache_key)
75
+ assert_equal IdentityCache::DELETED, backend.read(cache_key)
78
76
  end
79
77
 
80
78
  def test_non_unique_index_filled_on_fetch_by
81
79
  Item.cache_index :title
82
80
  @record.save!
83
81
  assert_equal [@record], Item.fetch_by_title('bob')
84
- assert_equal [@record.id], backend.read(@cache_key)
82
+ assert_equal [@record.id], backend.read(cache_key)
85
83
  end
86
84
 
87
85
  def test_non_unique_index_fetches_multiple_records
@@ -90,28 +88,29 @@ class IndexCacheTest < IdentityCache::TestCase
90
88
  record2 = Item.create(:title => 'bob') { |item| item.id = 2 }
91
89
 
92
90
  assert_equal [@record, record2], Item.fetch_by_title('bob')
93
- assert_equal [1, 2], backend.read(@cache_key)
91
+ assert_equal [1, 2], backend.read(cache_key)
94
92
  end
95
93
 
96
94
  def test_non_unique_index_expired_by_updating_record
97
95
  Item.cache_index :title
98
96
  @record.save!
99
- IdentityCache.cache.write(@cache_key, [@record.id])
97
+ old_cache_key = cache_key
98
+ IdentityCache.cache.write(old_cache_key, [@record.id])
100
99
 
101
100
  @record.title = 'robert'
102
- new_cache_key = "#{NAMESPACE}index:Item:title:#{cache_hash(@record.title)}"
101
+ new_cache_key = cache_key
103
102
  IdentityCache.cache.write(new_cache_key, [])
104
103
  @record.save!
105
- assert_equal IdentityCache::DELETED, backend.read(@cache_key)
104
+ assert_equal IdentityCache::DELETED, backend.read(old_cache_key)
106
105
  assert_equal IdentityCache::DELETED, backend.read(new_cache_key)
107
106
  end
108
107
 
109
108
  def test_non_unique_index_expired_by_destroying_record
110
109
  Item.cache_index :title
111
110
  @record.save!
112
- IdentityCache.cache.write(@cache_key, [@record.id])
111
+ IdentityCache.cache.write(cache_key, [@record.id])
113
112
  @record.destroy
114
- assert_equal IdentityCache::DELETED, backend.read(@cache_key)
113
+ assert_equal IdentityCache::DELETED, backend.read(cache_key)
115
114
  end
116
115
 
117
116
  def test_set_table_name_cache_fetch
@@ -119,6 +118,32 @@ class IndexCacheTest < IdentityCache::TestCase
119
118
  Item.table_name = 'items2'
120
119
  @record.save!
121
120
  assert_equal [@record], Item.fetch_by_title('bob')
122
- assert_equal [@record.id], backend.read(@cache_key)
121
+ assert_equal [@record.id], backend.read(cache_key)
122
+ end
123
+
124
+ def test_fetch_by_index_raises_when_called_on_a_scope
125
+ Item.cache_index :title
126
+ assert_raises(IdentityCache::UnsupportedScopeError) do
127
+ Item.where(updated_at: nil).fetch_by_title('bob')
128
+ end
129
+ end
130
+
131
+ def test_fetch_by_unique_index_raises_when_called_on_a_scope
132
+ Item.cache_index :title, :id, :unique => true
133
+ assert_raises(IdentityCache::UnsupportedScopeError) do
134
+ Item.where(updated_at: nil).fetch_by_title_and_id('bob', 2)
135
+ end
136
+ end
137
+
138
+ def test_cache_index_on_derived_model_raises
139
+ assert_raises(IdentityCache::DerivedModelError) do
140
+ StiRecordTypeA.cache_index :name, :id
141
+ end
142
+ end
143
+
144
+ private
145
+
146
+ def cache_key(unique: false)
147
+ "#{NAMESPACE}attr#{unique ? '' : 's'}:Item:id:title:#{cache_hash(@record.title)}"
123
148
  end
124
149
  end
@@ -34,15 +34,30 @@ class NormalizedBelongsToTest < IdentityCache::TestCase
34
34
  assert_equal @parent_record, @record.fetch_item
35
35
  end
36
36
 
37
- def test_fetching_the_association_should_assign_the_result_to_the_association_so_that_successive_accesses_are_cached
37
+ def test_fetching_the_association_should_assign_the_result_to_an_instance_variable_so_that_successive_accesses_are_cached
38
38
  Item.expects(:fetch_by_id).with(@parent_record.id).returns(@parent_record)
39
- @record.fetch_item
40
- assert @record.association(:item).loaded?
41
- assert_equal @parent_record, @record.item
39
+ assert_equal @parent_record, @record.fetch_item
40
+ assert_equal false, @record.association(:item).loaded?
41
+ assert_equal @parent_record, @record.fetch_item
42
42
  end
43
43
 
44
- def test_fetching_the_association_shouldnt_raise_if_the_record_cant_be_found
44
+ def test_fetching_the_association_should_cache_nil_and_not_raise_if_the_record_cant_be_found
45
45
  Item.expects(:fetch_by_id).with(@parent_record.id).returns(nil)
46
- assert_equal nil, @record.fetch_item
46
+ assert_equal nil, @record.fetch_item # miss
47
+ assert_equal nil, @record.fetch_item # hit
48
+ end
49
+
50
+ def test_cache_belongs_to_on_derived_model_raises
51
+ assert_raises(IdentityCache::DerivedModelError) do
52
+ StiRecordTypeA.cache_belongs_to :item, :embed => false
53
+ end
54
+ end
55
+
56
+ def test_fetching_polymorphic_belongs_to_association
57
+ PolymorphicRecord.include IdentityCache
58
+ PolymorphicRecord.cache_belongs_to :owner
59
+ PolymorphicRecord.create!(owner: @parent_record)
60
+
61
+ assert_equal @parent_record, PolymorphicRecord.first.fetch_owner
47
62
  end
48
63
  end
@@ -29,6 +29,50 @@ class NormalizedHasManyTest < IdentityCache::TestCase
29
29
  assert_equal false, fetched_record.associated_records.loaded?
30
30
  end
31
31
 
32
+ def test_batch_fetching_of_association_for_multiple_parent_records
33
+ record2 = Item.new(:title => 'two')
34
+ record2.associated_records << AssociatedRecord.new(:name => 'a')
35
+ record2.associated_records << AssociatedRecord.new(:name => 'b')
36
+ record2.save!
37
+
38
+ fetched_records = assert_queries(2) do
39
+ Item.fetch_multi(@record.id, record2.id)
40
+ end
41
+ assert_equal [[2, 1], [4, 3]], fetched_records.map(&:cached_associated_record_ids)
42
+ assert_equal false, fetched_records.any?{ |record| record.associated_records.loaded? }
43
+ end
44
+
45
+ def test_batch_fetching_of_deeply_associated_records
46
+ Item.has_many :denormalized_associated_records, class_name: 'AssociatedRecord'
47
+ Item.cache_has_many :denormalized_associated_records, embed: true
48
+ AssociatedRecord.cache_has_many :deeply_associated_records, embed: :ids
49
+ @record.associated_records[0].deeply_associated_records << DeeplyAssociatedRecord.new(:name => 'deep1')
50
+ @record.associated_records[1].deeply_associated_records << DeeplyAssociatedRecord.new(:name => 'deep2')
51
+ @record.associated_records.each(&:save!)
52
+
53
+ fetched_records = assert_queries(4) do
54
+ Item.fetch(@record.id)
55
+ end
56
+ assert_no_queries do
57
+ assert_equal [[1], [2]], fetched_records.fetch_denormalized_associated_records.map(&:cached_deeply_associated_record_ids)
58
+ assert_equal false, fetched_records.fetch_denormalized_associated_records.any?{ |record| record.deeply_associated_records.loaded? }
59
+ end
60
+ end
61
+
62
+ def test_batch_fetching_stops_with_nil_parent
63
+ Item.cache_has_one :associated, embed: true
64
+ AssociatedRecord.cache_has_many :deeply_associated_records, embed: :ids
65
+ AssociatedRecord.delete_all
66
+
67
+ fetched_records = assert_queries(3) do
68
+ Item.fetch(@record.id)
69
+ end
70
+ assert_no_queries do
71
+ assert_equal @record, fetched_records
72
+ assert_nil fetched_records.fetch_associated
73
+ end
74
+ end
75
+
32
76
  def test_fetching_associated_ids_will_populate_the_value_if_the_record_isnt_from_the_cache
33
77
  assert_equal [2, 1], @record.fetch_associated_record_ids
34
78
  end