onyx-cache-money 0.2.5.8 → 0.2.6
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.
- data/config/environment.rb +7 -2
- data/lib/cash/accessor.rb +36 -10
- data/lib/cash/finders.rb +4 -4
- data/lib/cash/index.rb +5 -0
- data/lib/cash/query/abstract.rb +93 -19
- data/lib/cash/query/select.rb +3 -2
- data/spec/cash/accessor_spec.rb +16 -5
- data/spec/cash/active_record_spec.rb +8 -0
- data/spec/cash/finders_spec.rb +205 -2
- data/spec/cash/order_spec.rb +5 -0
- metadata +2 -2
data/config/environment.rb
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
require 'activerecord'
|
2
2
|
|
3
3
|
ActiveRecord::Base.establish_connection(
|
4
|
-
:adapter => 'sqlite3',
|
5
|
-
:dbfile => ':memory:'
|
4
|
+
# :adapter => 'sqlite3',
|
5
|
+
# :dbfile => ':memory:'
|
6
|
+
|
7
|
+
:adapter => 'mysql',
|
8
|
+
:socket => %w[/tmp/mysql.sock /var/lib/mysql/mysql.sock /var/run/mysqld/mysqld.sock /opt/local/var/run/mysql5/mysqld.sock].detect { |f| File.exists? f },
|
9
|
+
:database => 'cache_money_development',
|
10
|
+
:username => 'root'
|
6
11
|
)
|
data/lib/cash/accessor.rb
CHANGED
@@ -8,14 +8,20 @@ module Cash
|
|
8
8
|
end
|
9
9
|
|
10
10
|
module ClassMethods
|
11
|
+
NIL_CACHE_VALUE = 'NIL_CACHE_VALUE'
|
12
|
+
|
11
13
|
def fetch(keys, options = {}, &block)
|
12
14
|
case keys
|
13
15
|
when Array
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
16
|
+
cache_and_actual_keys = keys.inject({}) { |memo, key| memo[cache_key(key)] = key; memo }
|
17
|
+
cache_keys = keys.collect {|key| cache_key(key)}
|
18
|
+
|
19
|
+
hits = repository.get_multi(cache_keys)
|
20
|
+
if (missed_cache_keys = cache_keys - hits.keys).any?
|
21
|
+
actual_missed_keys = missed_cache_keys.collect {|missed_cache_key| cache_and_actual_keys[missed_cache_key]}
|
22
|
+
misses = block.call(actual_missed_keys)
|
23
|
+
|
24
|
+
hits.merge!(misses)
|
19
25
|
end
|
20
26
|
hits
|
21
27
|
else
|
@@ -26,17 +32,25 @@ module Cash
|
|
26
32
|
def get(keys, options = {}, &block)
|
27
33
|
case keys
|
28
34
|
when Array
|
29
|
-
fetch(keys, options
|
35
|
+
results = fetch(keys, options) do |missed_keys|
|
36
|
+
results = yield(missed_keys)
|
37
|
+
results.each {|key, value| add(key, wrap_nil(value), options)}
|
38
|
+
results
|
39
|
+
end
|
40
|
+
results.each { |key, result| results[key] = unwrap_nil(result) }
|
30
41
|
else
|
31
|
-
fetch(keys, options) do
|
42
|
+
result = fetch(keys, options) do
|
32
43
|
if block_given?
|
33
|
-
|
44
|
+
result = yield(keys)
|
45
|
+
value = result.is_a?(Hash) ? result[cache_key(keys)] : result
|
46
|
+
add(keys, wrap_nil(value), options)
|
34
47
|
result
|
35
48
|
end
|
36
49
|
end
|
50
|
+
unwrap_nil(result)
|
37
51
|
end
|
38
52
|
end
|
39
|
-
|
53
|
+
|
40
54
|
def add(key, value, options = {})
|
41
55
|
if repository.add(cache_key(key), value, options[:ttl] || 0, options[:raw]) == "NOT_STORED\r\n"
|
42
56
|
yield if block_given?
|
@@ -66,8 +80,20 @@ module Cash
|
|
66
80
|
end
|
67
81
|
|
68
82
|
def cache_key(key)
|
69
|
-
|
83
|
+
ready = key =~ /#{name}:#{cache_config.version}/
|
84
|
+
ready ? key : "#{name}:#{cache_config.version}/#{key.to_s.gsub(' ', '+')}"
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def wrap_nil(value)
|
90
|
+
value.nil? ? NIL_CACHE_VALUE : value
|
70
91
|
end
|
92
|
+
|
93
|
+
def unwrap_nil(value)
|
94
|
+
value == NIL_CACHE_VALUE ? nil : value
|
95
|
+
end
|
96
|
+
|
71
97
|
end
|
72
98
|
|
73
99
|
module InstanceMethods
|
data/lib/cash/finders.rb
CHANGED
@@ -10,7 +10,7 @@ module Cash
|
|
10
10
|
def self.extended(active_record_class)
|
11
11
|
class << active_record_class
|
12
12
|
alias_method_chain :find_every, :cache
|
13
|
-
alias_method_chain :find_from_ids, :cache
|
13
|
+
# alias_method_chain :find_from_ids, :cache
|
14
14
|
alias_method_chain :calculate, :cache
|
15
15
|
end
|
16
16
|
end
|
@@ -25,9 +25,9 @@ module Cash
|
|
25
25
|
end
|
26
26
|
|
27
27
|
# User.find(1), User.find(1, 2, 3), User.find([1, 2, 3]), User.find([])
|
28
|
-
def find_from_ids_with_cache(ids, options)
|
29
|
-
|
30
|
-
end
|
28
|
+
# def find_from_ids_with_cache(ids, options)
|
29
|
+
# Query::PrimaryKey.perform(self, ids, options, scope(:find))
|
30
|
+
# end
|
31
31
|
|
32
32
|
# User.count(:all), User.count, User.sum(...)
|
33
33
|
def calculate_with_cache(operation, column_name, options = {})
|
data/lib/cash/index.rb
CHANGED
@@ -67,6 +67,11 @@ module Cash
|
|
67
67
|
end
|
68
68
|
include Attributes
|
69
69
|
|
70
|
+
#not ideal, need to find a better solution
|
71
|
+
def serialize_objects(objects)
|
72
|
+
primary_key? ? objects.first : objects.map(&:id)
|
73
|
+
end
|
74
|
+
|
70
75
|
def serialize_object(object)
|
71
76
|
primary_key? ? object : object.id
|
72
77
|
end
|
data/lib/cash/query/abstract.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
module Cash
|
2
2
|
module Query
|
3
3
|
class Abstract
|
4
|
-
delegate :with_exclusive_scope, :get, :
|
4
|
+
delegate :with_exclusive_scope, :get, :quoted_table_name, :connection, :indices,
|
5
|
+
:find_every_without_cache, :cache_key, :columns_hash, :quote_value, :to => :@active_record
|
5
6
|
|
6
7
|
def self.perform(*args)
|
7
8
|
new(*args).perform
|
@@ -49,9 +50,12 @@ module Cash
|
|
49
50
|
|
50
51
|
private
|
51
52
|
def cacheable?(*optionss)
|
53
|
+
# Cache money used to cache queries with order option only when order was "id asc";
|
54
|
+
# For now, we just want all order queries to go to the database always.
|
55
|
+
return if optionss.find {|options| options[:order] }
|
52
56
|
optionss.each { |options| return unless safe_options_for_cache?(options) }
|
53
57
|
partial_indices = optionss.collect { |options| attribute_value_pairs_for_conditions(options[:conditions]) }
|
54
|
-
return if partial_indices.include?(nil)
|
58
|
+
return if partial_indices.flatten.include?(nil)
|
55
59
|
attribute_value_pairs = partial_indices.sum.sort { |x, y| x[0] <=> y[0] }
|
56
60
|
if index = indexed_on?(attribute_value_pairs.collect { |pair| pair[0] })
|
57
61
|
if index.matches?(self)
|
@@ -70,7 +74,20 @@ module Cash
|
|
70
74
|
end
|
71
75
|
|
72
76
|
def cache_keys(attribute_value_pairs)
|
73
|
-
attribute_value_pairs
|
77
|
+
cache_keys = collect_cache_keys(attribute_value_pairs)
|
78
|
+
cache_keys.size == 1 ? cache_keys.first : cache_keys
|
79
|
+
end
|
80
|
+
|
81
|
+
def collect_cache_keys(pairs)
|
82
|
+
return [] if pairs.empty?
|
83
|
+
key, values = pairs.shift
|
84
|
+
Array(values).inject([]) do |memo,value|
|
85
|
+
partial_keys = collect_cache_keys(pairs.clone)
|
86
|
+
|
87
|
+
memo << "#{key}/#{value}" if partial_keys.empty?
|
88
|
+
partial_keys.each { |partial_key| memo << "#{key}/#{value}/#{partial_key}" }
|
89
|
+
memo
|
90
|
+
end
|
74
91
|
end
|
75
92
|
|
76
93
|
def safe_options_for_cache?(options)
|
@@ -81,8 +98,6 @@ module Cash
|
|
81
98
|
def attribute_value_pairs_for_conditions(conditions)
|
82
99
|
case conditions
|
83
100
|
when Hash
|
84
|
-
# avoid key too long error when passing in array of ids
|
85
|
-
return nil if conditions.values.any? {|value| value.is_a?(Array)}
|
86
101
|
conditions.to_a.collect { |key, value| [key.to_s, value] }
|
87
102
|
when String
|
88
103
|
parse_indices_from_condition(conditions)
|
@@ -97,17 +112,20 @@ module Cash
|
|
97
112
|
|
98
113
|
AND = /\s+AND\s+/i
|
99
114
|
TABLE_AND_COLUMN = /(?:(?:`|")?(\w+)(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
|
100
|
-
VALUE = /'?(\d+|\?|(?:(?:[^']|'')
|
101
|
-
KEY_EQ_VALUE = /^\(?#{TABLE_AND_COLUMN}\s
|
102
|
-
ORDER = /^#{TABLE_AND_COLUMN}\s*(ASC|DESC)?$/i
|
115
|
+
VALUE = /'?(\d+|\?|(?:(?:[^']|'')*?))'?/ # Matches: 123, ?, '123', '12 ''3'
|
116
|
+
KEY_EQ_VALUE = /^\(?#{TABLE_AND_COLUMN}\s+(?:=|IN)\s+\(?(?:#{VALUE})\)?\)?$/ # Matches: KEY = VALUE, (KEY = VALUE), KEY IN (VALUE,VALUE,..)
|
117
|
+
ORDER = /^#{TABLE_AND_COLUMN}\s*(ASC|DESC)?$/i # Matches: COLUMN ASC, COLUMN DESC, COLUMN
|
103
118
|
|
104
119
|
def parse_indices_from_condition(conditions = '', *values)
|
105
120
|
values = values.dup
|
106
121
|
conditions.split(AND).inject([]) do |indices, condition|
|
107
|
-
matched, table_name, column_name,
|
122
|
+
matched, table_name, column_name, sql_values = *(KEY_EQ_VALUE.match(condition))
|
108
123
|
if matched
|
109
|
-
|
110
|
-
|
124
|
+
actual_values = sql_values.split(',').collect do |sql_value|
|
125
|
+
sql_value == '?' ? values.shift : columns_hash[column_name].type_cast(sql_value)
|
126
|
+
end
|
127
|
+
actual_values.flatten!
|
128
|
+
indices << [column_name, actual_values]
|
111
129
|
else
|
112
130
|
return nil
|
113
131
|
end
|
@@ -120,8 +138,7 @@ module Cash
|
|
120
138
|
alias_method :index_for, :indexed_on?
|
121
139
|
|
122
140
|
def format_results(cache_keys, objects)
|
123
|
-
return
|
124
|
-
|
141
|
+
return [] if objects.blank?
|
125
142
|
objects = convert_to_array(cache_keys, objects)
|
126
143
|
objects = apply_limits_and_offsets(objects, @options1)
|
127
144
|
deserialize_objects(objects)
|
@@ -131,8 +148,10 @@ module Cash
|
|
131
148
|
missed_keys == cache_keys ? misses : objects
|
132
149
|
end
|
133
150
|
|
134
|
-
def serialize_objects(index,
|
135
|
-
|
151
|
+
def serialize_objects(index, objects_hash)
|
152
|
+
objects_hash.each do |key, objects|
|
153
|
+
objects_hash[key] = index.serialize_objects(objects)
|
154
|
+
end
|
136
155
|
end
|
137
156
|
|
138
157
|
def convert_to_array(cache_keys, object)
|
@@ -152,15 +171,70 @@ module Cash
|
|
152
171
|
objects
|
153
172
|
else
|
154
173
|
cache_keys = objects.collect { |id| "id/#{id}" }
|
155
|
-
objects = get(cache_keys
|
174
|
+
objects = get(cache_keys) {|missed_keys| find_from_keys(missed_keys)}
|
156
175
|
convert_to_array(cache_keys, objects)
|
157
176
|
end
|
158
177
|
end
|
159
178
|
|
160
|
-
def find_from_keys(
|
161
|
-
|
162
|
-
|
179
|
+
def find_from_keys(missing_keys, options = {})
|
180
|
+
#[[id/1/title/foo], [id/1/title/foo]]
|
181
|
+
missing_keys_values_pairs = Array(missing_keys).flatten.collect { |key| key.split('/') }
|
182
|
+
#[[id,1,title,foo], [id,2,title,foo]]
|
183
|
+
|
184
|
+
conditions = conditions_from_missing_keys_values_pairs(missing_keys_values_pairs)
|
185
|
+
results = find_every_without_cache options.merge(:conditions => conditions)
|
186
|
+
# [<object1>, <object2>, <object3>]
|
187
|
+
|
188
|
+
collect_results_into_hash(missing_keys_values_pairs, Array(results))
|
189
|
+
#{ id/1/title/foo => [<object1>], id/2/title/foo => [<object2>,<object3>] }
|
190
|
+
end
|
191
|
+
|
192
|
+
def collect_results_into_hash(missing_keys_values_pairs, results)
|
193
|
+
missing_keys_values_pairs.inject({}) do |memo, missing_keys_values_pair|
|
194
|
+
match = results.select do |result|
|
195
|
+
found_match = false
|
196
|
+
missing_keys_values_pair.each_slice(2) do |key, value|
|
197
|
+
found_match = upcase_if_possible(result.send(key)) == convert_value(key, value)
|
198
|
+
break unless found_match
|
199
|
+
end
|
200
|
+
found_match
|
201
|
+
end
|
202
|
+
memo[cache_key(missing_keys_values_pair.join('/'))] = match
|
203
|
+
memo
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def conditions_from_missing_keys_values_pairs(missing_keys_values_pairs)
|
208
|
+
keys_values = missing_keys_values_pairs.inject({}) do |memo, missing_keys_values_pair|
|
209
|
+
missing_keys_values_pair.each_slice(2) do |key, value|
|
210
|
+
memo[key] ||= []
|
211
|
+
memo[key] << value
|
212
|
+
end
|
213
|
+
memo
|
214
|
+
end
|
215
|
+
# { :id => [1,2], :title => [foo] }
|
216
|
+
|
217
|
+
conditions = keys_values.collect do |key,values|
|
218
|
+
quoted_values = values.collect {|value| quote_value(value, columns_hash[key])}
|
219
|
+
quoted_table_and_column_name = "#{quoted_table_name}.#{connection.quote_column_name(key)}"
|
220
|
+
if quoted_values.size == 1
|
221
|
+
"#{quoted_table_and_column_name} = #{quoted_values}"
|
222
|
+
else
|
223
|
+
"#{quoted_table_and_column_name} IN (#{quoted_values.join(',')})"
|
224
|
+
end
|
225
|
+
end.join(' AND ')
|
226
|
+
end
|
227
|
+
|
228
|
+
protected
|
229
|
+
|
230
|
+
def upcase_if_possible(value)
|
231
|
+
value.respond_to?(:upcase) ? value.upcase : value
|
232
|
+
end
|
233
|
+
|
234
|
+
def convert_value(key, value)
|
235
|
+
columns_hash[key].type == :integer ? value.to_i : upcase_if_possible(value)
|
163
236
|
end
|
237
|
+
|
164
238
|
end
|
165
239
|
end
|
166
240
|
end
|
data/lib/cash/query/select.rb
CHANGED
@@ -4,8 +4,9 @@ module Cash
|
|
4
4
|
delegate :find_every_without_cache, :to => :@active_record
|
5
5
|
|
6
6
|
protected
|
7
|
-
def miss(
|
8
|
-
find_every_without_cache(miss_options)
|
7
|
+
def miss(missed_keys, miss_options)
|
8
|
+
# find_every_without_cache(miss_options)
|
9
|
+
misses = find_from_keys(missed_keys, miss_options)
|
9
10
|
end
|
10
11
|
|
11
12
|
def uncacheable
|
data/spec/cash/accessor_spec.rb
CHANGED
@@ -21,17 +21,28 @@ module Cash
|
|
21
21
|
describe '#fetch([...])', :shared => true do
|
22
22
|
describe 'when there is a total cache miss' do
|
23
23
|
it 'yields the keys to the block' do
|
24
|
-
Story.fetch(["yabba", "dabba"])
|
25
|
-
"Story:1/yabba" => "doo",
|
24
|
+
Story.fetch(["yabba", "dabba"]) do |*missing_ids|
|
25
|
+
{"Story:1/yabba" => "doo", "Story:1/dabba" => "doo"}
|
26
|
+
end.should == {
|
27
|
+
"Story:1/yabba" => "doo", "Story:1/dabba" => "doo"}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'when there is a partial cache miss' do
|
32
|
+
it 'yields just the missing keys and ids to the block' do
|
33
|
+
Story.set("yabba", "dabba")
|
34
|
+
Story.fetch(["yabba", "dabba"]) { |*missing_ids| {"Story:1/dabba" => "doo"} }.should == {
|
35
|
+
"Story:1/yabba" => "dabba",
|
26
36
|
"Story:1/dabba" => "doo"
|
27
37
|
}
|
28
38
|
end
|
29
39
|
end
|
30
40
|
|
31
|
-
describe 'when there is a
|
32
|
-
it 'yields
|
41
|
+
describe 'when there is a total cache hit' do
|
42
|
+
it 'yields empty hash to the block' do
|
33
43
|
Story.set("yabba", "dabba")
|
34
|
-
Story.
|
44
|
+
Story.set("dabba", "doo")
|
45
|
+
Story.fetch(["yabba", "dabba"]) { |*missing_ids| {} }.should == {
|
35
46
|
"Story:1/yabba" => "dabba",
|
36
47
|
"Story:1/dabba" => "doo"
|
37
48
|
}
|
@@ -41,6 +41,14 @@ module Cash
|
|
41
41
|
it 'raises an error' do
|
42
42
|
lambda { Story.find(1) }.should raise_error(ActiveRecord::RecordNotFound)
|
43
43
|
end
|
44
|
+
|
45
|
+
it 'raises an error the second time as well' do
|
46
|
+
begin
|
47
|
+
Story.find(1)
|
48
|
+
rescue ActiveRecord::RecordNotFound
|
49
|
+
lambda { Story.find(1) }.should raise_error(ActiveRecord::RecordNotFound)
|
50
|
+
end
|
51
|
+
end
|
44
52
|
end
|
45
53
|
|
46
54
|
describe 'when given multiple nonexistent ids' do
|
data/spec/cash/finders_spec.rb
CHANGED
@@ -5,6 +5,52 @@ module Cash
|
|
5
5
|
describe 'Cache Usage' do
|
6
6
|
describe 'when the cache is populated' do
|
7
7
|
describe '#find' do
|
8
|
+
|
9
|
+
describe 'storing nil values in cache' do
|
10
|
+
it 'populates the cache with special token when id does not exist in the database' do
|
11
|
+
Story.find_by_id(999_999)
|
12
|
+
Story.fetch("id/999999").should == 'NIL_CACHE_VALUE'
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'caches the non-existent record' do
|
16
|
+
Story.find_by_id(999_999)
|
17
|
+
mock(Story.connection).execute.never
|
18
|
+
Story.find_by_id(999_999)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'correctly retrieves nil when special token is stored in the cache' do
|
22
|
+
Story.find_by_id(999_999)
|
23
|
+
Story.find_by_id(999_999).should be_nil
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'populates the cache with multiple special tokens when none of the ids exist' do
|
27
|
+
Story.find(:all, :conditions => {:id => [999_997, 999_998]} )
|
28
|
+
Story.fetch("id/999997").should == 'NIL_CACHE_VALUE'
|
29
|
+
Story.fetch("id/999998").should == 'NIL_CACHE_VALUE'
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'caches the non-existent records when finding by multiple ids' do
|
33
|
+
Story.find(:all, :conditions => {:id => [999_997, 999_998]} )
|
34
|
+
mock(Story.connection).execute.never
|
35
|
+
Story.find(:all, :conditions => {:id => [999_997, 999_998]} )
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'correctly retrieves empty array when special tokens are stored in the cache' do
|
39
|
+
Story.find(:all, :conditions => {:id => [999_997, 999_998]} )
|
40
|
+
Story.find(:all, :conditions => {:id => [999_997, 999_998]} ).should == []
|
41
|
+
end
|
42
|
+
|
43
|
+
it "populates the cache correctly when passing in a nonexistent key" do
|
44
|
+
story1 = Story.create!
|
45
|
+
story2 = Story.create!
|
46
|
+
$memcache.flush_all
|
47
|
+
Story.find(:all, :conditions => ["id IN (?)", [story1.id, 999, story2.id]]).should == [story1, story2]
|
48
|
+
Story.fetch("id/#{story1.id}").should == story1
|
49
|
+
Story.fetch("id/#{story2.id}").should == story2
|
50
|
+
Story.fetch("id/999").should == 'NIL_CACHE_VALUE'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
8
54
|
describe '#find(1)' do
|
9
55
|
it 'does not use the database' do
|
10
56
|
story = Story.create!
|
@@ -170,8 +216,9 @@ module Cash
|
|
170
216
|
|
171
217
|
describe '#find(1, :conditions => ...)' do
|
172
218
|
it "does not use the database" do
|
219
|
+
Story.create!
|
173
220
|
story = Story.create!
|
174
|
-
character = Character.create!(:name => name = 'barbara', :story_id => story)
|
221
|
+
character = Character.create!(:name => name = 'barbara', :story_id => story.id)
|
175
222
|
mock(Character.connection).execute.never
|
176
223
|
Character.send :with_scope, :find => { :conditions => { :story_id => story.id } } do
|
177
224
|
Character.find(character.id, :conditions => { :name => name }).should == character
|
@@ -241,6 +288,17 @@ module Cash
|
|
241
288
|
end
|
242
289
|
end
|
243
290
|
|
291
|
+
it "populate cache with compound key" do
|
292
|
+
story = Story.create!
|
293
|
+
harry = story.characters.create! :name => "harry"
|
294
|
+
larry = story.characters.create! :name => "larry"
|
295
|
+
$memcache.flush_all
|
296
|
+
|
297
|
+
Character.find(:all, :conditions => {:story_id => story.id, :name => [larry.name, harry.name]})
|
298
|
+
Character.fetch("name/#{harry.name}/story_id/#{story.id}").should == [harry.id]
|
299
|
+
Character.fetch("name/#{larry.name}/story_id/#{story.id}").should == [larry.id]
|
300
|
+
end
|
301
|
+
|
244
302
|
it "should not create a key over 250 characters with hash for conditions" do
|
245
303
|
150.times do
|
246
304
|
Story.create!
|
@@ -354,6 +412,11 @@ module Cash
|
|
354
412
|
Story.find(:first, :conditions => { :title => @story.title })
|
355
413
|
Story.fetch("title/#{@story.title}").should == [@story.id]
|
356
414
|
end
|
415
|
+
|
416
|
+
it 'populates the cache when database is case insensitive - fails under sqlite (case sensitive)' do
|
417
|
+
Story.find(:first, :conditions => { :title => @story.title.upcase })
|
418
|
+
Story.fetch("title/#{@story.title.upcase}").should == [@story.id]
|
419
|
+
end
|
357
420
|
end
|
358
421
|
|
359
422
|
describe '#find_by_attr' do
|
@@ -361,6 +424,11 @@ module Cash
|
|
361
424
|
Story.find_by_title(@story.title)
|
362
425
|
Story.fetch("title/#{@story.title}").should == [@story.id]
|
363
426
|
end
|
427
|
+
|
428
|
+
it 'populates the cache with empty array for non-existent value' do
|
429
|
+
Story.find_by_title("blah")
|
430
|
+
Story.fetch("title/blah").should == []
|
431
|
+
end
|
364
432
|
end
|
365
433
|
|
366
434
|
describe '#find(:all, :conditions => ...)' do
|
@@ -383,13 +451,124 @@ module Cash
|
|
383
451
|
end
|
384
452
|
end
|
385
453
|
|
454
|
+
describe '#find(:conditions => ["... :attr", {:attr => 1}])' do
|
455
|
+
it 'retrieves story from database - no support yet for retrieving from cache' do
|
456
|
+
Story.find(:all, :conditions => ["id = :id", {:id => @story.id}]).should == [@story]
|
457
|
+
end
|
458
|
+
end
|
459
|
+
|
386
460
|
describe '#find(1)' do
|
387
461
|
it 'populates the cache' do
|
388
462
|
Story.find(@story.id)
|
389
|
-
Story.fetch("id/#{@story.id}").should ==
|
463
|
+
Story.fetch("id/#{@story.id}").should == @story
|
390
464
|
end
|
391
465
|
end
|
466
|
+
|
467
|
+
describe '#find(1,2)' do
|
468
|
+
|
469
|
+
it 'populates the cache' do
|
470
|
+
another_story = Story.create!
|
471
|
+
$memcache.flush_all
|
472
|
+
|
473
|
+
Story.find(@story.id, another_story.id).should == [@story, another_story]
|
474
|
+
Story.fetch("id/#{@story.id}").should == @story
|
475
|
+
Story.fetch("id/#{another_story.id}").should == another_story
|
476
|
+
end
|
477
|
+
|
478
|
+
it "populates the cache and retrieves from the cache" do
|
479
|
+
story1 = Story.create!
|
480
|
+
story2 = Story.create!
|
481
|
+
story3 = Story.create!
|
482
|
+
$memcache.flush_all
|
483
|
+
|
484
|
+
Story.find(story1.id, story2.id, story3.id).should == [story1, story2, story3]
|
485
|
+
mock(Story.connection).execute.never
|
486
|
+
Story.find(story1.id, story2.id).should == [story1, story2]
|
487
|
+
end
|
488
|
+
|
489
|
+
it "uses database for single missing key" do
|
490
|
+
story1 = Story.create!
|
491
|
+
story2 = Story.create!
|
492
|
+
story3 = Story.create!
|
493
|
+
$memcache.flush_all
|
494
|
+
|
495
|
+
Story.find(story1.id, story3.id).should == [story1, story3]
|
496
|
+
Story.fetch("id/#{story2.id}").should be_nil
|
497
|
+
|
498
|
+
quoted_table_name = Story.quoted_table_name
|
499
|
+
quoted_column_name = Story.connection.quote_column_name("id")
|
500
|
+
mock(Story).find_every_without_cache(:limit => nil,
|
501
|
+
:conditions => "#{quoted_table_name}.#{quoted_column_name} = #{story2.id}") do
|
502
|
+
story2
|
503
|
+
end
|
504
|
+
Story.find(story1.id, story2.id)
|
505
|
+
end
|
506
|
+
|
507
|
+
it "uses database for all missing keys" do
|
508
|
+
story1 = Story.create!
|
509
|
+
story2 = Story.create!
|
510
|
+
story3 = Story.create!
|
511
|
+
$memcache.flush_all
|
512
|
+
|
513
|
+
Story.find(story2.id).should == story2
|
514
|
+
Story.fetch("id/#{story1.id}").should be_nil
|
515
|
+
Story.fetch("id/#{story3.id}").should be_nil
|
516
|
+
|
517
|
+
quoted_table_name = Story.quoted_table_name
|
518
|
+
quoted_column_name = Story.connection.quote_column_name("id")
|
519
|
+
mock(Story).find_every_without_cache(:limit => nil,
|
520
|
+
:conditions => "#{quoted_table_name}.#{quoted_column_name} IN (#{story1.id},#{story3.id})") do
|
521
|
+
[story1, story3]
|
522
|
+
end
|
523
|
+
Story.find(story1.id, story2.id, story3.id)
|
524
|
+
end
|
525
|
+
|
526
|
+
it "populates and retrieves from cache when passing in a hash" do
|
527
|
+
story1 = Story.create!
|
528
|
+
story2 = Story.create!
|
529
|
+
$memcache.flush_all
|
530
|
+
|
531
|
+
Story.find(:all, :conditions => {:id => [story1.id, story2.id]})
|
532
|
+
mock(Story.connection).execute.never
|
533
|
+
Story.find(story1.id, story2.id)
|
534
|
+
end
|
535
|
+
|
536
|
+
it "populates and retrieves from cache when passing in a parametrized conditions" do
|
537
|
+
story1 = Story.create!
|
538
|
+
story2 = Story.create!
|
539
|
+
$memcache.flush_all
|
392
540
|
|
541
|
+
Story.find(:all, :conditions => ["id IN (?,?)", story1.id, story2.id])
|
542
|
+
mock(Story.connection).execute.never
|
543
|
+
Story.find(story1.id, story2.id)
|
544
|
+
end
|
545
|
+
|
546
|
+
it "populates and retrieves from cache when passing in a list of non-numeric keys" do
|
547
|
+
pending "need to fix regex to properly parse this"
|
548
|
+
story1 = Story.create! :title => 'one'
|
549
|
+
story2 = Story.create! :title => 'two'
|
550
|
+
$memcache.flush_all
|
551
|
+
|
552
|
+
Story.find(:all, :conditions => ["title IN ('one', 'two')"])
|
553
|
+
# We need to add this extra call as cache money will always go to the database
|
554
|
+
# one more time when finding by a non-id field first
|
555
|
+
Story.find_all_by_title(['one', 'two'])
|
556
|
+
mock(Story.connection).execute.never
|
557
|
+
Story.find(story1.id, story2.id)
|
558
|
+
end
|
559
|
+
|
560
|
+
it "should not create a key over 250 characters on a find_all_by_ids" do
|
561
|
+
75.times do
|
562
|
+
Story.create!
|
563
|
+
end
|
564
|
+
ids = Story.find(:all).map(&:id)
|
565
|
+
$memcache.flush_all
|
566
|
+
lambda do
|
567
|
+
Story.find(:all, :conditions => {:id => ids})
|
568
|
+
end.should_not raise_error(ArgumentError)
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
393
572
|
describe 'when there is a with_scope' do
|
394
573
|
it "uses the database, not the cache" do
|
395
574
|
Story.send :with_scope, :find => { :conditions => { :title => @story.title }} do
|
@@ -397,6 +576,30 @@ module Cash
|
|
397
576
|
end
|
398
577
|
end
|
399
578
|
end
|
579
|
+
|
580
|
+
it 'populates the cache correctly when arguments are unordered' do
|
581
|
+
another_story = Story.create!
|
582
|
+
$memcache.flush_all
|
583
|
+
|
584
|
+
Story.find(another_story.id, @story.id)
|
585
|
+
Story.fetch("id/#{@story.id}").should == @story
|
586
|
+
Story.fetch("id/#{another_story.id}").should == another_story
|
587
|
+
end
|
588
|
+
|
589
|
+
describe 'ordering' do
|
590
|
+
it 'returns data in correct order' do
|
591
|
+
story1 = Story.create!
|
592
|
+
story2 = Story.create!
|
593
|
+
story3 = Story.create!
|
594
|
+
story4 = Story.create!
|
595
|
+
$memcache.flush_all
|
596
|
+
|
597
|
+
Story.find(story1.id, story3.id)
|
598
|
+
unordered_fairy_tales = [story4.id, story3.id, story2.id, story1.id]
|
599
|
+
ordered_fairy_tales = Story.find(:all, :conditions => ["id IN (?)", unordered_fairy_tales], :order => 'id ASC')
|
600
|
+
ordered_fairy_tales.should == [story1, story2, story3, story4]
|
601
|
+
end
|
602
|
+
end
|
400
603
|
end
|
401
604
|
end
|
402
605
|
end
|
data/spec/cash/order_spec.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require File.join(File.dirname(__FILE__), '..', 'spec_helper')
|
2
2
|
|
3
3
|
module Cash
|
4
|
+
# for now, all order queries will go to database and bypass cache"
|
4
5
|
describe 'Ordering' do
|
5
6
|
before :suite do
|
6
7
|
FairyTale = Class.new(Story)
|
@@ -79,6 +80,7 @@ module Cash
|
|
79
80
|
describe "#find(..., :order => 'id ASC')" do
|
80
81
|
describe 'when the cache is populated' do
|
81
82
|
it 'does not use the database' do
|
83
|
+
pending "for now, all order queries will go to database and bypass cache"
|
82
84
|
mock(FairyTale.connection).execute.never
|
83
85
|
FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id ASC').should == @fairy_tales
|
84
86
|
FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id').should == @fairy_tales
|
@@ -91,6 +93,7 @@ module Cash
|
|
91
93
|
|
92
94
|
describe 'when the cache is not populated' do
|
93
95
|
it 'populates the cache' do
|
96
|
+
pending "for now, all order queries will go to database and bypass cache"
|
94
97
|
$memcache.flush_all
|
95
98
|
FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id ASC').should == @fairy_tales
|
96
99
|
FairyTale.get("title/#{@title}").should == @fairy_tales.collect(&:id)
|
@@ -124,6 +127,7 @@ module Cash
|
|
124
127
|
describe "#find(..., :order => 'id DESC')" do
|
125
128
|
describe 'when the cache is populated' do
|
126
129
|
it 'does not use the database' do
|
130
|
+
pending "for now, all order queries will go to database and bypass cache"
|
127
131
|
mock(FairyTale.connection).execute.never
|
128
132
|
FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id DESC').should == @fairy_tales.reverse
|
129
133
|
FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id DESC').should == @fairy_tales.reverse
|
@@ -136,6 +140,7 @@ module Cash
|
|
136
140
|
|
137
141
|
describe 'when the cache is not populated' do
|
138
142
|
it 'populates the cache' do
|
143
|
+
pending "for now, all order queries will go to database and bypass cache"
|
139
144
|
$memcache.flush_all
|
140
145
|
FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id DESC')
|
141
146
|
FairyTale.get("title/#{@title}").should == @fairy_tales.collect(&:id).reverse
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: onyx-cache-money
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nick Kallen
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-
|
12
|
+
date: 2009-06-18 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|