identity_cache 0.2.5 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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