record-cache 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/lib/record-cache.rb +1 -0
  2. data/lib/record_cache/active_record.rb +318 -0
  3. data/lib/record_cache/base.rb +136 -0
  4. data/lib/record_cache/dispatcher.rb +90 -0
  5. data/lib/record_cache/multi_read.rb +51 -0
  6. data/lib/record_cache/query.rb +85 -0
  7. data/lib/record_cache/statistics.rb +82 -0
  8. data/lib/record_cache/strategy/base.rb +154 -0
  9. data/lib/record_cache/strategy/id_cache.rb +93 -0
  10. data/lib/record_cache/strategy/index_cache.rb +122 -0
  11. data/lib/record_cache/strategy/request_cache.rb +49 -0
  12. data/lib/record_cache/test/resettable_version_store.rb +49 -0
  13. data/lib/record_cache/version.rb +5 -0
  14. data/lib/record_cache/version_store.rb +54 -0
  15. data/lib/record_cache.rb +11 -0
  16. data/spec/db/database.yml +6 -0
  17. data/spec/db/schema.rb +42 -0
  18. data/spec/db/seeds.rb +40 -0
  19. data/spec/initializers/record_cache.rb +14 -0
  20. data/spec/lib/dispatcher_spec.rb +86 -0
  21. data/spec/lib/multi_read_spec.rb +51 -0
  22. data/spec/lib/query_spec.rb +148 -0
  23. data/spec/lib/statistics_spec.rb +140 -0
  24. data/spec/lib/strategy/base_spec.rb +241 -0
  25. data/spec/lib/strategy/id_cache_spec.rb +168 -0
  26. data/spec/lib/strategy/index_cache_spec.rb +223 -0
  27. data/spec/lib/strategy/request_cache_spec.rb +85 -0
  28. data/spec/lib/version_store_spec.rb +104 -0
  29. data/spec/models/apple.rb +8 -0
  30. data/spec/models/banana.rb +8 -0
  31. data/spec/models/pear.rb +6 -0
  32. data/spec/models/person.rb +11 -0
  33. data/spec/models/store.rb +13 -0
  34. data/spec/spec_helper.rb +44 -0
  35. data/spec/support/after_commit.rb +71 -0
  36. data/spec/support/matchers/hit_cache_matcher.rb +53 -0
  37. data/spec/support/matchers/miss_cache_matcher.rb +53 -0
  38. data/spec/support/matchers/use_cache_matcher.rb +53 -0
  39. metadata +253 -0
@@ -0,0 +1,241 @@
1
+ $KCODE = 'UTF8'
2
+ require 'spec_helper'
3
+
4
+ describe RecordCache::Strategy::Base do
5
+
6
+ it "should provide easy access to the Version Store" do
7
+ Apple.record_cache[:id].send(:version_store).should == RecordCache::Base.version_store
8
+ end
9
+
10
+ it "should provide easy access to the Record Store" do
11
+ Apple.record_cache[:id].send(:record_store).should == RecordCache::Base.stores[:shared]
12
+ Banana.record_cache[:id].send(:record_store).should == RecordCache::Base.stores[:local]
13
+ end
14
+
15
+ it "should provide easy access to the statistics" do
16
+ Apple.record_cache[:person_id].send(:statistics).should == RecordCache::Statistics.find(Apple, :person_id)
17
+ Banana.record_cache[:id].send(:statistics).should == RecordCache::Statistics.find(Banana, :id)
18
+ end
19
+
20
+ it "should retrieve the cache key based on the :key option" do
21
+ Apple.record_cache[:id].send(:cache_key, 1).should == "rc/apl/1"
22
+ end
23
+
24
+ it "should retrieve the cache key based on the model name" do
25
+ Banana.record_cache[:id].send(:cache_key, 1).should == "rc/Banana/1"
26
+ end
27
+
28
+ it "should define the versioned key" do
29
+ Banana.record_cache[:id].send(:versioned_key, "rc/Banana/1", 2312423).should == "rc/Banana/1v2312423"
30
+ end
31
+
32
+ it "should serialize a record (currently Active Record only)" do
33
+ Banana.record_cache[:id].send(:serialize, Banana.find(1)).should == {:a=>{"name"=>"Blue Banana 1", "id"=>1, "store_id"=>2, "person_id"=>4}, :c=>"Banana"}
34
+ end
35
+
36
+ it "should deserialize a record (currently Active Record only)" do
37
+ Banana.record_cache[:id].send(:deserialize, {:a=>{"name"=>"Blue Banana 1", "id"=>1, "store_id"=>2, "person_id"=>4}, :c=>"Banana"}).should == Banana.find(1)
38
+ end
39
+
40
+ context "filter" do
41
+ it "should apply filter on :id cache hits" do
42
+ lambda{ @apples = Apple.where(:id => [1,2]).where(:name => "Adams Apple 1").all }.should use_cache(Apple).on(:id)
43
+ @apples.should == [Apple.find_by_name("Adams Apple 1")]
44
+ end
45
+
46
+ it "should apply filter on index cache hits" do
47
+ lambda{ @apples = Apple.where(:store_id => 1).where(:name => "Adams Apple 1").all }.should use_cache(Apple).on(:store_id)
48
+ @apples.should == [Apple.find_by_name("Adams Apple 1")]
49
+ end
50
+
51
+ it "should return empty array when filter does not match any record" do
52
+ lambda{ @apples = Apple.where(:store_id => 1).where(:name => "Adams Apple Pie").all }.should use_cache(Apple).on(:store_id)
53
+ @apples.should == []
54
+ end
55
+
56
+ it "should filter on text" do
57
+ lambda{ @apples = Apple.where(:id => [1,2]).where(:name => "Adams Apple 1").all }.should use_cache(Apple).on(:id)
58
+ @apples.should == [Apple.find_by_name("Adams Apple 1")]
59
+ end
60
+
61
+ it "should filter on integers" do
62
+ lambda{ @apples = Apple.where(:id => [1,2,8,9]).where(:store_id => 2).all }.should use_cache(Apple).on(:id)
63
+ @apples.map(&:id).sort.should == [8,9]
64
+ end
65
+
66
+ it "should filter on dates" do
67
+ lambda{ @people = Person.where(:id => [1,2,3]).where(:birthday => Date.civil(1953,11,11)).all }.should use_cache(Person).on(:id)
68
+ @people.size.should == 1
69
+ @people.first.name.should == "Blue"
70
+ end
71
+
72
+ it "should filter on floats" do
73
+ lambda{ @people = Person.where(:id => [1,2,3]).where(:height => 1.75).all }.should use_cache(Person).on(:id)
74
+ @people.size.should == 2
75
+ @people.map(&:name).sort.should == ["Blue", "Cris"]
76
+ end
77
+
78
+ it "should filter on arrays" do
79
+ lambda{ @apples = Apple.where(:id => [1,2,8,9]).where(:store_id => [2, 4]).all }.should use_cache(Apple).on(:id)
80
+ @apples.map(&:id).sort.should == [8,9]
81
+ end
82
+
83
+ it "should filter on multiple fields" do
84
+ # make sure two apples exist with the same name
85
+ @apple = Apple.find(8)
86
+ @apple.name = Apple.find(9).name
87
+ @apple.save!
88
+
89
+ lambda{ @apples = Apple.where(:id => [1,2,3,8,9,10]).where(:store_id => 2).where(:name => @apple.name).all }.should use_cache(Apple).on(:id)
90
+ @apples.size.should == 2
91
+ @apples.map(&:name).should == [@apple.name, @apple.name]
92
+ @apples.map(&:id).sort.should == [8,9]
93
+ end
94
+
95
+ end
96
+
97
+ context "sort" do
98
+ it "should apply sort on :id cache hits" do
99
+ lambda{ @people = Person.where(:id => [1,2,3]).order("name DESC").all }.should use_cache(Person).on(:id)
100
+ @people.map(&:name).should == ["Cris", "Blue", "Adam"]
101
+ end
102
+
103
+ it "should apply sort on index cache hits" do
104
+ lambda{ @apples = Apple.where(:store_id => 1).order("person_id ASC").all }.should use_cache(Apple).on(:store_id)
105
+ @apples.map(&:person_id).should == [nil, nil, 4, 4, 5]
106
+ end
107
+
108
+ it "should default to ASC" do
109
+ lambda{ @apples = Apple.where(:store_id => 1).order("person_id").all }.should use_cache(Apple).on(:store_id)
110
+ @apples.map(&:person_id).should == [nil, nil, 4, 4, 5]
111
+ end
112
+
113
+ it "should apply sort nil first for ASC" do
114
+ lambda{ @apples = Apple.where(:store_id => 1).order("person_id ASC").all }.should use_cache(Apple).on(:store_id)
115
+ @apples.map(&:person_id).should == [nil, nil, 4, 4, 5]
116
+ end
117
+
118
+ it "should apply sort nil last for DESC" do
119
+ lambda{ @apples = Apple.where(:store_id => 1).order("person_id DESC").all }.should use_cache(Apple).on(:store_id)
120
+ @apples.map(&:person_id).should == [5, 4, 4, nil, nil]
121
+ end
122
+
123
+ it "should sort ascending on text" do
124
+ lambda{ @people = Person.where(:id => [1,2,3,4]).order("name ASC").all }.should use_cache(Person).on(:id)
125
+ @people.map(&:name).should == ["Adam", "Blue", "Cris", "Fry"]
126
+ end
127
+
128
+ it "should sort descending on text" do
129
+ lambda{ @people = Person.where(:id => [1,2,3,4]).order("name DESC").all }.should use_cache(Person).on(:id)
130
+ @people.map(&:name).should == ["Fry", "Cris", "Blue", "Adam"]
131
+ end
132
+
133
+ it "should sort ascending on integers" do
134
+ lambda{ @people = Person.where(:id => [1,2,3,4]).order("id ASC").all }.should use_cache(Person).on(:id)
135
+ @people.map(&:id).should == [1,2,3,4]
136
+ end
137
+
138
+ it "should sort descending on integers" do
139
+ lambda{ @people = Person.where(:id => [1,2,3,4]).order("id DESC").all }.should use_cache(Person).on(:id)
140
+ @people.map(&:id).should == [4,3,2,1]
141
+ end
142
+
143
+ it "should sort ascending on dates" do
144
+ lambda{ @people = Person.where(:id => [1,2,3,4]).order("birthday ASC").all }.should use_cache(Person).on(:id)
145
+ @people.map(&:birthday).should == [Date.civil(1953,11,11), Date.civil(1975,03,20), Date.civil(1975,03,20), Date.civil(1985,01,20)]
146
+ end
147
+
148
+ it "should sort descending on dates" do
149
+ lambda{ @people = Person.where(:id => [1,2,3,4]).order("birthday DESC").all }.should use_cache(Person).on(:id)
150
+ @people.map(&:birthday).should == [Date.civil(1985,01,20), Date.civil(1975,03,20), Date.civil(1975,03,20), Date.civil(1953,11,11)]
151
+ end
152
+
153
+ it "should sort ascending on float" do
154
+ lambda{ @people = Person.where(:id => [1,2,3,4]).order("height ASC").all }.should use_cache(Person).on(:id)
155
+ @people.map(&:height).should == [1.69, 1.75, 1.75, 1.83]
156
+ end
157
+
158
+ it "should sort descending on float" do
159
+ lambda{ @people = Person.where(:id => [1,2,3,4]).order("height DESC").all }.should use_cache(Person).on(:id)
160
+ @people.map(&:height).should == [1.83, 1.75, 1.75, 1.69]
161
+ end
162
+
163
+ it "should sort on multiple fields (ASC + ASC)" do
164
+ lambda{ @people = Person.where(:id => [2,3,4,5]).order("height ASC, id ASC").all }.should use_cache(Person).on(:id)
165
+ @people.map(&:height).should == [1.69, 1.75, 1.75, 1.91]
166
+ @people.map(&:id).should == [4, 2, 3, 5]
167
+ end
168
+
169
+ it "should sort on multiple fields (ASC + DESC)" do
170
+ lambda{ @people = Person.where(:id => [2,3,4,5]).order("height ASC, id DESC").all }.should use_cache(Person).on(:id)
171
+ @people.map(&:height).should == [1.69, 1.75, 1.75, 1.91]
172
+ @people.map(&:id).should == [4, 3, 2, 5]
173
+ end
174
+
175
+ it "should sort on multiple fields (DESC + ASC)" do
176
+ lambda{ @people = Person.where(:id => [2,3,4,5]).order("height DESC, id ASC").all }.should use_cache(Person).on(:id)
177
+ @people.map(&:height).should == [1.91, 1.75, 1.75, 1.69]
178
+ @people.map(&:id).should == [5, 2, 3, 4]
179
+ end
180
+
181
+ it "should sort on multiple fields (DESC + DESC)" do
182
+ lambda{ @people = Person.where(:id => [2,3,4,5]).order("height DESC, id DESC").all }.should use_cache(Person).on(:id)
183
+ @people.map(&:height).should == [1.91, 1.75, 1.75, 1.69]
184
+ @people.map(&:id).should == [5, 3, 2, 4]
185
+ end
186
+
187
+ it "should use mysql style collation" do
188
+ ids = []
189
+ ids << Person.create!(:name => "ċedriĉ 3").id # latin other special
190
+ ids << Person.create!(:name => "a cedric").id # first in ascending order
191
+ ids << Person.create!(:name => "čedriĉ 4").id # latin another special
192
+ ids << Person.create!(:name => "ćedriĉ Last").id # latin special lowercase
193
+ ids << Person.create!(:name => "sedric 1").id # second to last latin in ascending order
194
+ ids << Person.create!(:name => "Cedric 2").id # ascii uppercase
195
+ ids << Person.create!(:name => "čedriĉ คฉ Almost last cedric").id # latin special, with non-latin
196
+ ids << Person.create!(:name => "Sedric 2").id # last latin in ascending order
197
+ ids << Person.create!(:name => "1 cedric").id # numbers before characters
198
+ ids << Person.create!(:name => "cedric 1").id # ascii lowercase
199
+ ids << Person.create!(:name => "คฉ Really last").id # non-latin characters last in ascending order
200
+ ids << Person.create!(:name => "čedriĉ ꜩ Last").id # latin special, with latin non-collateable
201
+
202
+ names_asc = ["1 cedric", "a cedric", "cedric 1", "Cedric 2", "ċedriĉ 3", "čedriĉ 4", "ćedriĉ Last", "čedriĉ คฉ Almost last cedric", "čedriĉ ꜩ Last", "sedric 1", "Sedric 2", "คฉ Really last"]
203
+ lambda{ @people = Person.where(:id => ids).order("name ASC").all }.should hit_cache(Person).on(:id).times(ids.size)
204
+ @people.map(&:name).should == names_asc
205
+
206
+ lambda{ @people = Person.where(:id => ids).order("name DESC").all }.should hit_cache(Person).on(:id).times(ids.size)
207
+ @people.map(&:name).should == names_asc.reverse
208
+ end
209
+ end
210
+
211
+ it "should combine filter and sort" do
212
+ lambda{ @people = Person.where(:id => [1,2,3]).where(:height => 1.75).order("name DESC").all }.should use_cache(Person).on(:id)
213
+ @people.size.should == 2
214
+ @people.map(&:name).should == ["Cris", "Blue"]
215
+
216
+ lambda{ @people = Person.where(:id => [1,2,3]).where(:height => 1.75).order("name").all }.should hit_cache(Person).on(:id).times(3)
217
+ @people.map(&:name).should == ["Blue", "Cris"]
218
+ end
219
+
220
+ context "NotImplementedError" do
221
+ before(:each) do
222
+ @invalid_strategy = RecordCache::Strategy::Base.new(Object, nil, nil, "key")
223
+ end
224
+
225
+ it "should require record_change to be implemented" do
226
+ lambda { @invalid_strategy.record_change(Object.new, 1) }.should raise_error(NotImplementedError)
227
+ end
228
+
229
+ it "should require cacheable? to be implemented" do
230
+ lambda { @invalid_strategy.cacheable?(RecordCache::Query.new) }.should raise_error(NotImplementedError)
231
+ end
232
+
233
+ it "should require invalidate to be implemented" do
234
+ lambda { @invalid_strategy.invalidate(1) }.should raise_error(NotImplementedError)
235
+ end
236
+
237
+ it "should fetch_records to be implemented" do
238
+ lambda { @invalid_strategy.fetch(RecordCache::Query.new) }.should raise_error(NotImplementedError)
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,168 @@
1
+ require 'spec_helper'
2
+
3
+ describe RecordCache::Strategy::IdCache do
4
+
5
+ it "should retrieve an Apple from the cache" do
6
+ lambda{ Apple.find(1) }.should miss_cache(Apple).on(:id).times(1)
7
+ lambda{ Apple.find(1) }.should hit_cache(Apple).on(:id).times(1)
8
+ end
9
+
10
+ it "should retrieve cloned records" do
11
+ @apple_1a = Apple.find(1)
12
+ @apple_1b = Apple.find(1)
13
+ @apple_1a.should == @apple_1b
14
+ @apple_1a.object_id.should_not == @apple_1b.object_id
15
+ end
16
+
17
+ context "logging" do
18
+ before(:each) do
19
+ Apple.find(1)
20
+ end
21
+
22
+ it "should write full hits to the debug log" do
23
+ mock(RecordCache::Base.logger).debug(/IdCache hit for ids 1|^(?!IdCache)/).times(any_times)
24
+ Apple.find(1)
25
+ end
26
+
27
+ it "should write full miss to the debug log" do
28
+ mock(RecordCache::Base.logger).debug(/IdCache miss for ids 2|^(?!IdCache)/).times(any_times)
29
+ Apple.find(2)
30
+ end
31
+
32
+ it "should write partial hits to the debug log" do
33
+ mock(RecordCache::Base.logger).debug(/IdCache partial hit for ids \[1, 2\]: missing \[2\]|^(?!IdCache)/).times(any_times)
34
+ Apple.where(:id => [1,2]).all
35
+ end
36
+ end
37
+
38
+ context "cacheable?" do
39
+ before(:each) do
40
+ # fill cache
41
+ @apple1 = Apple.find(1)
42
+ @apple2 = Apple.find(2)
43
+ end
44
+
45
+ it "should use the cache when a single id is requested" do
46
+ lambda{ Apple.where(:id => 1).all }.should hit_cache(Apple).on(:id).times(1)
47
+ end
48
+
49
+ it "should use the cache when a multiple ids are requested" do
50
+ lambda{ Apple.where(:id => [1, 2]).all }.should hit_cache(Apple).on(:id).times(2)
51
+ end
52
+
53
+ it "should use the cache when a single id is requested and the limit is 1" do
54
+ lambda{ Apple.where(:id => 1).limit(1).all }.should hit_cache(Apple).on(:id).times(1)
55
+ end
56
+
57
+ it "should not use the cache when a single id is requested and the limit is > 1" do
58
+ lambda{ Apple.where(:id => 1).limit(2).all }.should_not use_cache(Apple).on(:id)
59
+ end
60
+
61
+ it "should not use the cache when multiple ids are requested and the limit is 1" do
62
+ lambda{ Apple.where(:id => [1, 2]).limit(1).all }.should_not use_cache(Apple).on(:id)
63
+ end
64
+
65
+ it "should use the cache when a single id is requested together with other where clauses" do
66
+ lambda{ Apple.where(:id => 1).where(:name => "Adams Apple x").all }.should hit_cache(Apple).on(:id).times(1)
67
+ end
68
+
69
+ it "should use the cache when a multiple ids are requested together with other where clauses" do
70
+ lambda{ Apple.where(:id => [1,2]).where(:name => "Adams Apple x").all }.should hit_cache(Apple).on(:id).times(2)
71
+ end
72
+
73
+ it "should use the cache when a single id is requested together with (simple) sort clauses" do
74
+ lambda{ Apple.where(:id => 1).order("name ASC").all }.should hit_cache(Apple).on(:id).times(1)
75
+ end
76
+
77
+ it "should use the cache when a multiple ids are requested together with (simple) sort clauses" do
78
+ lambda{ Apple.where(:id => [1,2]).order("name ASC").all }.should hit_cache(Apple).on(:id).times(2)
79
+ end
80
+ end
81
+
82
+ context "record_change" do
83
+ before(:each) do
84
+ # fill cache
85
+ @apple1 = Apple.find(1)
86
+ @apple2 = Apple.find(2)
87
+ end
88
+
89
+ it "should invalidate destroyed records" do
90
+ lambda{ Apple.where(:id => 1).all }.should hit_cache(Apple).on(:id).times(1)
91
+ @apple1.destroy
92
+ lambda{ @apples = Apple.where(:id => 1).all }.should miss_cache(Apple).on(:id).times(1)
93
+ @apples.should == []
94
+ # try again, to make sure the "missing record" is not cached
95
+ lambda{ Apple.where(:id => 1).all }.should miss_cache(Apple).on(:id).times(1)
96
+ end
97
+
98
+ it "should add updated records directly to the cache" do
99
+ @apple1.name = "Applejuice"
100
+ @apple1.save!
101
+ lambda{ @apple = Apple.find(1) }.should hit_cache(Apple).on(:id).times(1)
102
+ @apple.name.should == "Applejuice"
103
+ end
104
+
105
+ it "should add created records directly to the cache" do
106
+ @new_apple = Apple.create!(:name => "Fresh Apple", :store_id => 1)
107
+ lambda{ @apple = Apple.find(@new_apple.id) }.should hit_cache(Apple).on(:id).times(1)
108
+ @apple.name.should == "Fresh Apple"
109
+ end
110
+
111
+ it "should add updated records to the cache, also when multiple ids are queried" do
112
+ @apple1.name = "Applejuice"
113
+ @apple1.save!
114
+ lambda{ @apples = Apple.where(:id => [1, 2]).order('id ASC').all }.should hit_cache(Apple).on(:id).times(2)
115
+ @apples.map(&:name).should == ["Applejuice", "Adams Apple 2"]
116
+ end
117
+
118
+ end
119
+
120
+ context "invalidate" do
121
+ before(:each) do
122
+ @apple1 = Apple.find(1)
123
+ @apple2 = Apple.find(2)
124
+ end
125
+
126
+ it "should invalidate single records" do
127
+ Apple.record_cache[:id].invalidate(1)
128
+ lambda{ Apple.find(1) }.should miss_cache(Apple).on(:id).times(1)
129
+ end
130
+
131
+ it "should only miss the cache for the invalidated record when multiple ids are queried" do
132
+ # miss on 1
133
+ Apple.record_cache[:id].invalidate(1)
134
+ lambda{ Apple.where(:id => [1, 2]).all }.should miss_cache(Apple).on(:id).times(1)
135
+ # hit on 2
136
+ Apple.record_cache[:id].invalidate(1)
137
+ lambda{ Apple.where(:id => [1, 2]).all }.should hit_cache(Apple).on(:id).times(1)
138
+ # nothing invalidated, both hit
139
+ lambda{ Apple.where(:id => [1, 2]).all }.should hit_cache(Apple).on(:id).times(2)
140
+ end
141
+
142
+ it "should invalidate records when using update_all" do
143
+ Apple.where(:id => [3,4,5]).all # fill id cache on all Adam Store apples
144
+ lambda{ @apples = Apple.where(:id => [1, 2, 3, 4, 5]).order('id ASC').all }.should hit_cache(Apple).on(:id).times(5)
145
+ @apples.map(&:name).should == ["Adams Apple 1", "Adams Apple 2", "Adams Apple 3", "Adams Apple 4", "Adams Apple 5"]
146
+ # update 3 of the 5 apples in the Adam Store
147
+ Apple.where(:id => [1,2,3]).update_all(:name => "Uniform Apple")
148
+ lambda{ @apples = Apple.where(:id => [1, 2, 3, 4, 5]).order('id ASC').all }.should hit_cache(Apple).on(:id).times(2)
149
+ @apples.map(&:name).should == ["Uniform Apple", "Uniform Apple", "Uniform Apple", "Adams Apple 4", "Adams Apple 5"]
150
+ end
151
+
152
+ it "should invalidate reflection indexes when a has_many relation is updated" do
153
+ # assign different apples to store 2
154
+ lambda{ Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:id).times(2)
155
+ store2_apple_ids = Apple.where(:store_id => 2).map(&:id)
156
+ store1 = Store.find(1)
157
+ store1.apple_ids = store2_apple_ids
158
+ store1.save!
159
+ # the apples that used to belong to store 2 are now in store 1 (incremental update)
160
+ lambda{ @apple1 = Apple.find(store2_apple_ids.first) }.should hit_cache(Apple).on(:id).times(1)
161
+ @apple1.store_id.should == 1
162
+ # the apples that used to belong to store 1 are now homeless (cache invalidated)
163
+ lambda{ @homeless_apple = Apple.find(1) }.should miss_cache(Apple).on(:id).times(1)
164
+ @homeless_apple.store_id.should == nil
165
+ end
166
+ end
167
+
168
+ end
@@ -0,0 +1,223 @@
1
+ require 'spec_helper'
2
+
3
+ describe RecordCache::Strategy::IndexCache do
4
+
5
+ context "initialize" do
6
+ it "should only accept index cache on DB columns" do
7
+ lambda { Apple.send(:cache_records, :index => :unknown_column) }.should raise_error("No column found for index 'unknown_column' on Apple.")
8
+ end
9
+
10
+ it "should only accept index cache on integer columns" do
11
+ lambda { Apple.send(:cache_records, :index => :name) }.should raise_error("Incorrect type (expected integer, found string) for index 'name' on Apple.")
12
+ end
13
+ end
14
+
15
+ it "should use the id cache to retrieve the actual records" do
16
+ lambda { @apples = Apple.where(:store_id => 1).all }.should miss_cache(Apple).on(:store_id).times(1)
17
+ lambda { Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:store_id).times(1)
18
+ lambda { Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:id).times(@apples.size)
19
+ end
20
+
21
+ context "logging" do
22
+ before(:each) do
23
+ Apple.where(:store_id => 1).all
24
+ end
25
+
26
+ it "should write hit to the debug log" do
27
+ mock(RecordCache::Base.logger).debug(/IndexCache hit for rc\/apl\/store_id=1v\d+: found 5 ids|^(?!IndexCache)/).times(any_times)
28
+ Apple.where(:store_id => 1).all
29
+ end
30
+
31
+ it "should write miss to the debug log" do
32
+ mock(RecordCache::Base.logger).debug(/IndexCache miss for rc\/apl\/store_id=2v\d+: found no ids|^(?!IndexCache)/).times(any_times)
33
+ Apple.where(:store_id => 2).all
34
+ end
35
+ end
36
+
37
+ context "cacheable?" do
38
+ before(:each) do
39
+ @store1_apples = Apple.where(:store_id => 1).all
40
+ @store2_apples = Apple.where(:store_id => 2).all
41
+ end
42
+
43
+ it "should hit the cache for a single index id" do
44
+ lambda { Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:store_id).times(1)
45
+ end
46
+
47
+ it "should hit the cache for a single index id with other where clauses" do
48
+ lambda { Apple.where(:store_id => 1).where(:name => "applegate").all }.should hit_cache(Apple).on(:store_id).times(1)
49
+ end
50
+
51
+ it "should hit the cache for a single index id with (simple) sort clauses" do
52
+ lambda { Apple.where(:store_id => 1).order("name ASC").all }.should hit_cache(Apple).on(:store_id).times(1)
53
+ end
54
+
55
+ it "should not hit the cache for a single index id with limit" do
56
+ lambda { Apple.where(:store_id => 1).limit(1).all }.should_not hit_cache(Apple).on(:store_id)
57
+ end
58
+
59
+ it "should not hit the cache when an :id where clause is defined" do
60
+ # this query should make use of the :id cache, which is faster
61
+ lambda { Apple.where(:store_id => 1).where(:id => 1).all }.should_not hit_cache(Apple).on(:store_id)
62
+ end
63
+ end
64
+
65
+ context "record_change" do
66
+ before(:each) do
67
+ @store1_apples = Apple.where(:store_id => 1).order('id ASC').all
68
+ @store2_apples = Apple.where(:store_id => 2).order('id ASC').all
69
+ end
70
+
71
+ [false, true].each do |fresh|
72
+ it "should #{fresh ? 'update' : 'invalidate'} the index when a record in the index is destroyed and the current index is #{fresh ? '' : 'not '}fresh" do
73
+ # make sure the index is no longer fresh
74
+ Apple.record_cache.invalidate(:store_id, 1) unless fresh
75
+ # destroy an apple
76
+ @destroyed = @store1_apples.last
77
+ @destroyed.destroy
78
+ # check the cache hit/miss on the index that contained that apple
79
+ if fresh
80
+ lambda { @apples = Apple.where(:store_id => 1).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
81
+ else
82
+ lambda { @apples = Apple.where(:store_id => 1).order('id ASC').all }.should miss_cache(Apple).on(:store_id).times(1)
83
+ end
84
+ @apples.size.should == @store1_apples.size - 1
85
+ @apples.map(&:id).should == @store1_apples.map(&:id) - [@destroyed.id]
86
+ # and the index should be cached again
87
+ lambda { Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:store_id).times(1)
88
+ end
89
+
90
+ it "should #{fresh ? 'update' : 'invalidate'} the index when a record in the index is created and the current index is #{fresh ? '' : 'not '}fresh" do
91
+ # make sure the index is no longer fresh
92
+ Apple.record_cache.invalidate(:store_id, 1) unless fresh
93
+ # create an apple
94
+ @new_apple_in_store1 = Apple.create!(:name => "Fresh Apple", :store_id => 1)
95
+ # check the cache hit/miss on the index that contains that apple
96
+ if fresh
97
+ lambda { @apples = Apple.where(:store_id => 1).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
98
+ else
99
+ lambda { @apples = Apple.where(:store_id => 1).order('id ASC').all }.should miss_cache(Apple).on(:store_id).times(1)
100
+ end
101
+ @apples.size.should == @store1_apples.size + 1
102
+ @apples.map(&:id).should == @store1_apples.map(&:id) + [@new_apple_in_store1.id]
103
+ # and the index should be cached again
104
+ lambda { Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:store_id).times(1)
105
+ end
106
+
107
+ it "should #{fresh ? 'update' : 'invalidate'} two indexes when the indexed value is updated and the current index is #{fresh ? '' : 'not '}fresh" do
108
+ # make sure both indexes are no longer fresh
109
+ Apple.record_cache.invalidate(:store_id, 1) unless fresh
110
+ Apple.record_cache.invalidate(:store_id, 2) unless fresh
111
+ # move one apple from store 1 to store 2
112
+ @apple_moved_from_store1_to_store2 = @store1_apples.last
113
+ @apple_moved_from_store1_to_store2.store_id = 2
114
+ @apple_moved_from_store1_to_store2.save!
115
+ # check the cache hit/miss on the indexes that contained/contains that apple
116
+ if fresh
117
+ lambda { @apples1 = Apple.where(:store_id => 1).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
118
+ lambda { @apples2 = Apple.where(:store_id => 2).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
119
+ else
120
+ lambda { @apples1 = Apple.where(:store_id => 1).order('id ASC').all }.should miss_cache(Apple).on(:store_id).times(1)
121
+ lambda { @apples2 = Apple.where(:store_id => 2).order('id ASC').all }.should miss_cache(Apple).on(:store_id).times(1)
122
+ end
123
+ @apples1.size.should == @store1_apples.size - 1
124
+ @apples2.size.should == @store2_apples.size + 1
125
+ @apples1.map(&:id).should == @store1_apples.map(&:id) - [@apple_moved_from_store1_to_store2.id]
126
+ @apples2.map(&:id).should == [@apple_moved_from_store1_to_store2.id] + @store2_apples.map(&:id)
127
+ # and the index should be cached again
128
+ lambda { Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:store_id).times(1)
129
+ lambda { Apple.where(:store_id => 2).all }.should hit_cache(Apple).on(:store_id).times(1)
130
+ end
131
+
132
+ it "should #{fresh ? 'update' : 'invalidate'} multiple indexes when several values on different indexed attributes are updated at once and one of the indexes is #{fresh ? '' : 'not '}fresh" do
133
+ # find the apples for person 1 and 5 (Chase)
134
+ @person4_apples = Apple.where(:person_id => 4).all # Fry's Apples
135
+ @person5_apples = Apple.where(:person_id => 5).all # Chases' Apples
136
+ # make sure person indexes are no longer fresh
137
+ Apple.record_cache.invalidate(:person_id, 4) unless fresh
138
+ Apple.record_cache.invalidate(:person_id, 5) unless fresh
139
+ # move one apple from store 1 to store 2
140
+ @apple_moved_from_s1to2_p5to4 = @store1_apples.last # the last apple belongs to person Chase (id 5)
141
+ @apple_moved_from_s1to2_p5to4.store_id = 2
142
+ @apple_moved_from_s1to2_p5to4.person_id = 4
143
+ @apple_moved_from_s1to2_p5to4.save!
144
+ # check the cache hit/miss on the indexes that contained/contains that apple
145
+ lambda { @apples_s1 = Apple.where(:store_id => 1).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
146
+ lambda { @apples_s2 = Apple.where(:store_id => 2).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
147
+ if fresh
148
+ lambda { @apples_p1 = Apple.where(:person_id => 4).order('id ASC').all }.should hit_cache(Apple).on(:person_id).times(1)
149
+ lambda { @apples_p2 = Apple.where(:person_id => 5).order('id ASC').all }.should hit_cache(Apple).on(:person_id).times(1)
150
+ else
151
+ lambda { @apples_p1 = Apple.where(:person_id => 4).order('id ASC').all }.should miss_cache(Apple).on(:person_id).times(1)
152
+ lambda { @apples_p2 = Apple.where(:person_id => 5).order('id ASC').all }.should miss_cache(Apple).on(:person_id).times(1)
153
+ end
154
+ @apples_s1.size.should == @store1_apples.size - 1
155
+ @apples_s2.size.should == @store2_apples.size + 1
156
+ @apples_p1.size.should == @person4_apples.size + 1
157
+ @apples_p2.size.should == @person5_apples.size - 1
158
+ @apples_s1.map(&:id).should == @store1_apples.map(&:id) - [@apple_moved_from_s1to2_p5to4.id]
159
+ @apples_s2.map(&:id).should == [@apple_moved_from_s1to2_p5to4.id] + @store2_apples.map(&:id)
160
+ @apples_p1.map(&:id).should == ([@apple_moved_from_s1to2_p5to4.id] + @person4_apples.map(&:id)).sort
161
+ @apples_p2.map(&:id).should == (@person5_apples.map(&:id) - [@apple_moved_from_s1to2_p5to4.id]).sort
162
+ # and the index should be cached again
163
+ lambda { Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:store_id).times(1)
164
+ lambda { Apple.where(:store_id => 2).all }.should hit_cache(Apple).on(:store_id).times(1)
165
+ lambda { Apple.where(:person_id => 4).all }.should hit_cache(Apple).on(:person_id).times(1)
166
+ lambda { Apple.where(:person_id => 5).all }.should hit_cache(Apple).on(:person_id).times(1)
167
+ end
168
+ end
169
+
170
+ it "should leave the index alone when a record outside the index is destroyed" do
171
+ # destroy an apple of store 2
172
+ @store2_apples.first.destroy
173
+ # index of apples of store 1 are not affected
174
+ lambda { @apples = Apple.where(:store_id => 1).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
175
+ end
176
+
177
+ it "should leave the index alone when a record outside the index is created" do
178
+ # create an apple for store 2
179
+ Apple.create!(:name => "Fresh Apple", :store_id => 2)
180
+ # index of apples of store 1 are not affected
181
+ lambda { @apples = Apple.where(:store_id => 1).order('id ASC').all }.should hit_cache(Apple).on(:store_id).times(1)
182
+ end
183
+ end
184
+
185
+ context "invalidate" do
186
+ before(:each) do
187
+ @store1_apples = Apple.where(:store_id => 1).all
188
+ @store2_apples = Apple.where(:store_id => 2).all
189
+ end
190
+
191
+ it "should invalidate single index" do
192
+ Apple.record_cache[:store_id].invalidate(1)
193
+ lambda{ Apple.where(:store_id => 1).all }.should miss_cache(Apple).on(:store_id).times(1)
194
+ end
195
+
196
+ it "should invalidate indexes when using update_all" do
197
+ pending "is there a performant way to invalidate index caches within update_all? only the new value is available, so we should query the old values..." do
198
+ # update 2 apples for index values store 1 and store 2
199
+ Apple.where(:id => [@store1_apples.first.id, @store2_apples.first.id]).update_all(:store_id => 3)
200
+ lambda{ @apples_1 = Apple.where(:store_id => 1).all }.should miss_cache(Apple).on(:store_id).times(1)
201
+ lambda{ @apples_2 = Apple.where(:store_id => 2).all }.should miss_cache(Apple).on(:store_id).times(1)
202
+ @apples_1.map(&:id).sort.should == @store1_apples[1..-1].sort
203
+ @apples_2.map(&:id).sort.should == @store2_apples[1..-1].sort
204
+ end
205
+ end
206
+
207
+ it "should invalidate reflection indexes when a has_many relation is updated" do
208
+ # assign different apples to store 2
209
+ lambda{ Apple.where(:store_id => 1).all }.should hit_cache(Apple).on(:store_id).times(1)
210
+ store2_apple_ids = @store2_apples.map(&:id).sort
211
+ store1 = Store.find(1)
212
+ store1.apple_ids = store2_apple_ids
213
+ store1.save!
214
+ # apples in Store 1 should be all (only) the apples that were in Store 2 (cache invalidated)
215
+ lambda{ @apples_1 = Apple.where(:store_id => 1).all }.should miss_cache(Apple).on(:store_id).times(1)
216
+ @apples_1.map(&:id).sort.should == store2_apple_ids
217
+ # there are no apples in Store 2 anymore (incremental cache update, as each apples in store 2 was saved separately)
218
+ lambda{ @apples_2 = Apple.where(:store_id => 2).all }.should hit_cache(Apple).on(:store_id).times(1)
219
+ @apples_2.should == []
220
+ end
221
+ end
222
+
223
+ end