seamusabshere-cache-money 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/README +205 -0
- data/TODO +17 -0
- data/UNSUPPORTED_FEATURES +14 -0
- data/config/environment.rb +6 -0
- data/config/memcache.yml +6 -0
- data/db/schema.rb +12 -0
- data/lib/cache_money.rb +54 -0
- data/lib/cash/accessor.rb +79 -0
- data/lib/cash/buffered.rb +126 -0
- data/lib/cash/config.rb +71 -0
- data/lib/cash/finders.rb +38 -0
- data/lib/cash/index.rb +207 -0
- data/lib/cash/local.rb +59 -0
- data/lib/cash/lock.rb +52 -0
- data/lib/cash/mock.rb +86 -0
- data/lib/cash/query/abstract.rb +162 -0
- data/lib/cash/query/calculation.rb +45 -0
- data/lib/cash/query/primary_key.rb +51 -0
- data/lib/cash/query/select.rb +16 -0
- data/lib/cash/request.rb +3 -0
- data/lib/cash/transactional.rb +42 -0
- data/lib/cash/util/array.rb +9 -0
- data/lib/cash/write_through.rb +72 -0
- data/spec/cash/accessor_spec.rb +159 -0
- data/spec/cash/active_record_spec.rb +199 -0
- data/spec/cash/calculations_spec.rb +67 -0
- data/spec/cash/finders_spec.rb +355 -0
- data/spec/cash/lock_spec.rb +87 -0
- data/spec/cash/order_spec.rb +166 -0
- data/spec/cash/transactional_spec.rb +574 -0
- data/spec/cash/window_spec.rb +195 -0
- data/spec/cash/write_through_spec.rb +223 -0
- data/spec/spec_helper.rb +56 -0
- metadata +113 -0
data/lib/cash/index.rb
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
module Cash
|
2
|
+
class Index
|
3
|
+
attr_reader :attributes, :options
|
4
|
+
delegate :each, :hash, :to => :@attributes
|
5
|
+
delegate :get, :set, :expire, :find_every_without_cache, :calculate_without_cache, :calculate_with_cache, :incr, :decr, :primary_key, :to => :@active_record
|
6
|
+
|
7
|
+
DEFAULT_OPTIONS = { :ttl => 1.day }
|
8
|
+
|
9
|
+
def initialize(config, active_record, attributes, options = {})
|
10
|
+
@config, @active_record, @attributes, @options = config, active_record, Array(attributes).collect(&:to_s).sort, DEFAULT_OPTIONS.merge(options)
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
case other
|
15
|
+
when Index
|
16
|
+
attributes == other.attributes
|
17
|
+
else
|
18
|
+
attributes == Array(other)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
alias_method :eql?, :==
|
22
|
+
|
23
|
+
module Commands
|
24
|
+
def add(object)
|
25
|
+
clone = object.shallow_clone
|
26
|
+
_, new_attribute_value_pairs = old_and_new_attribute_value_pairs(object)
|
27
|
+
add_to_index_with_minimal_network_operations(new_attribute_value_pairs, clone)
|
28
|
+
end
|
29
|
+
|
30
|
+
def update(object)
|
31
|
+
clone = object.shallow_clone
|
32
|
+
old_attribute_value_pairs, new_attribute_value_pairs = old_and_new_attribute_value_pairs(object)
|
33
|
+
update_index_with_minimal_network_operations(old_attribute_value_pairs, new_attribute_value_pairs, clone)
|
34
|
+
end
|
35
|
+
|
36
|
+
def remove(object)
|
37
|
+
old_attribute_value_pairs, _ = old_and_new_attribute_value_pairs(object)
|
38
|
+
remove_from_index_with_minimal_network_operations(old_attribute_value_pairs, object)
|
39
|
+
end
|
40
|
+
|
41
|
+
def delete(object)
|
42
|
+
old_attribute_value_pairs, _ = old_and_new_attribute_value_pairs(object)
|
43
|
+
key = cache_key(old_attribute_value_pairs)
|
44
|
+
expire(key)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
include Commands
|
48
|
+
|
49
|
+
module Attributes
|
50
|
+
def ttl
|
51
|
+
@ttl ||= options[:ttl] || config.ttl
|
52
|
+
end
|
53
|
+
|
54
|
+
def order
|
55
|
+
@order ||= options[:order] || :asc
|
56
|
+
end
|
57
|
+
|
58
|
+
def limit
|
59
|
+
options[:limit]
|
60
|
+
end
|
61
|
+
|
62
|
+
def buffer
|
63
|
+
options[:buffer]
|
64
|
+
end
|
65
|
+
|
66
|
+
def window
|
67
|
+
limit && limit + buffer
|
68
|
+
end
|
69
|
+
end
|
70
|
+
include Attributes
|
71
|
+
|
72
|
+
def serialize_object(object)
|
73
|
+
primary_key? ? object : object.id
|
74
|
+
end
|
75
|
+
|
76
|
+
def matches?(query)
|
77
|
+
query.calculation? ||
|
78
|
+
(query.order == ['id', order] &&
|
79
|
+
(!limit || (query.limit && query.limit + query.offset <= limit)))
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
def old_and_new_attribute_value_pairs(object)
|
84
|
+
old_attribute_value_pairs = []
|
85
|
+
new_attribute_value_pairs = []
|
86
|
+
@attributes.each do |name|
|
87
|
+
new_value = object.attributes[name]
|
88
|
+
original_value = object.send("#{name}_was")
|
89
|
+
old_attribute_value_pairs << [name, original_value]
|
90
|
+
new_attribute_value_pairs << [name, new_value]
|
91
|
+
end
|
92
|
+
[old_attribute_value_pairs, new_attribute_value_pairs]
|
93
|
+
end
|
94
|
+
|
95
|
+
def add_to_index_with_minimal_network_operations(attribute_value_pairs, object)
|
96
|
+
if primary_key?
|
97
|
+
add_object_to_primary_key_cache(attribute_value_pairs, object)
|
98
|
+
else
|
99
|
+
add_object_to_cache(attribute_value_pairs, object)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def primary_key?
|
104
|
+
@attributes.size == 1 && @attributes.first == primary_key
|
105
|
+
end
|
106
|
+
|
107
|
+
def add_object_to_primary_key_cache(attribute_value_pairs, object)
|
108
|
+
set(cache_key(attribute_value_pairs), [object], :ttl => ttl)
|
109
|
+
end
|
110
|
+
|
111
|
+
def cache_key(attribute_value_pairs)
|
112
|
+
attribute_value_pairs.flatten.join('/')
|
113
|
+
end
|
114
|
+
|
115
|
+
def add_object_to_cache(attribute_value_pairs, object, overwrite = true)
|
116
|
+
return if invalid_cache_key?(attribute_value_pairs)
|
117
|
+
|
118
|
+
key, cache_value, cache_hit = get_key_and_value_at_index(attribute_value_pairs)
|
119
|
+
if !cache_hit || overwrite
|
120
|
+
object_to_add = serialize_object(object)
|
121
|
+
objects = (cache_value + [object_to_add]).sort do |a, b|
|
122
|
+
(a <=> b) * (order == :asc ? 1 : -1)
|
123
|
+
end.uniq
|
124
|
+
objects = truncate_if_necessary(objects)
|
125
|
+
set(key, objects, :ttl => ttl)
|
126
|
+
incr("#{key}/count") { calculate_at_index(:count, attribute_value_pairs) }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def invalid_cache_key?(attribute_value_pairs)
|
131
|
+
attribute_value_pairs.collect { |_,value| value }.any? { |x| x.nil? }
|
132
|
+
end
|
133
|
+
|
134
|
+
def get_key_and_value_at_index(attribute_value_pairs)
|
135
|
+
key = cache_key(attribute_value_pairs)
|
136
|
+
cache_hit = true
|
137
|
+
cache_value = get(key) do
|
138
|
+
cache_hit = false
|
139
|
+
conditions = attribute_value_pairs.to_hash
|
140
|
+
find_every_without_cache(:select => primary_key, :conditions => conditions, :limit => window).collect do |object|
|
141
|
+
serialize_object(object)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
[key, cache_value, cache_hit]
|
145
|
+
end
|
146
|
+
|
147
|
+
def truncate_if_necessary(objects)
|
148
|
+
objects.slice(0, window || objects.size)
|
149
|
+
end
|
150
|
+
|
151
|
+
def calculate_at_index(operation, attribute_value_pairs)
|
152
|
+
conditions = attribute_value_pairs.to_hash
|
153
|
+
calculate_without_cache(operation, :all, :conditions => conditions)
|
154
|
+
end
|
155
|
+
|
156
|
+
def update_index_with_minimal_network_operations(old_attribute_value_pairs, new_attribute_value_pairs, object)
|
157
|
+
if index_is_stale?(old_attribute_value_pairs, new_attribute_value_pairs)
|
158
|
+
remove_object_from_cache(old_attribute_value_pairs, object)
|
159
|
+
add_object_to_cache(new_attribute_value_pairs, object)
|
160
|
+
elsif primary_key?
|
161
|
+
add_object_to_primary_key_cache(new_attribute_value_pairs, object)
|
162
|
+
else
|
163
|
+
add_object_to_cache(new_attribute_value_pairs, object, false)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def index_is_stale?(old_attribute_value_pairs, new_attribute_value_pairs)
|
168
|
+
old_attribute_value_pairs != new_attribute_value_pairs
|
169
|
+
end
|
170
|
+
|
171
|
+
def remove_from_index_with_minimal_network_operations(attribute_value_pairs, object)
|
172
|
+
if primary_key?
|
173
|
+
remove_object_from_primary_key_cache(attribute_value_pairs, object)
|
174
|
+
else
|
175
|
+
remove_object_from_cache(attribute_value_pairs, object)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def remove_object_from_primary_key_cache(attribute_value_pairs, object)
|
180
|
+
set(cache_key(attribute_value_pairs), [], :ttl => ttl)
|
181
|
+
end
|
182
|
+
|
183
|
+
def remove_object_from_cache(attribute_value_pairs, object)
|
184
|
+
return if invalid_cache_key?(attribute_value_pairs)
|
185
|
+
|
186
|
+
key, cache_value, _ = get_key_and_value_at_index(attribute_value_pairs)
|
187
|
+
object_to_remove = serialize_object(object)
|
188
|
+
objects = cache_value - [object_to_remove]
|
189
|
+
objects = resize_if_necessary(attribute_value_pairs, objects)
|
190
|
+
set(key, objects, :ttl => ttl)
|
191
|
+
end
|
192
|
+
|
193
|
+
def resize_if_necessary(attribute_value_pairs, objects)
|
194
|
+
conditions = attribute_value_pairs.to_hash
|
195
|
+
key = cache_key(attribute_value_pairs)
|
196
|
+
count = decr("#{key}/count") { calculate_at_index(:count, attribute_value_pairs) }
|
197
|
+
|
198
|
+
if limit && objects.size < limit && objects.size < count
|
199
|
+
find_every_without_cache(:select => :id, :conditions => conditions).collect do |object|
|
200
|
+
serialize_object(object)
|
201
|
+
end
|
202
|
+
else
|
203
|
+
objects
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
data/lib/cash/local.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
module Cash
|
2
|
+
class Local
|
3
|
+
delegate :respond_to?, :to => :@remote_cache
|
4
|
+
|
5
|
+
def initialize(remote_cache)
|
6
|
+
@remote_cache = remote_cache
|
7
|
+
end
|
8
|
+
|
9
|
+
def cache_locally
|
10
|
+
@remote_cache = LocalBuffer.new(original_cache = @remote_cache)
|
11
|
+
yield
|
12
|
+
ensure
|
13
|
+
@remote_cache = original_cache
|
14
|
+
end
|
15
|
+
|
16
|
+
def method_missing(method, *args, &block)
|
17
|
+
@remote_cache.send(method, *args, &block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class LocalBuffer
|
22
|
+
delegate :respond_to?, :to => :@remote_cache
|
23
|
+
|
24
|
+
def initialize(remote_cache)
|
25
|
+
@local_cache = {}
|
26
|
+
@remote_cache = remote_cache
|
27
|
+
end
|
28
|
+
|
29
|
+
def get(key, *options)
|
30
|
+
if @local_cache.has_key?(key)
|
31
|
+
@local_cache[key]
|
32
|
+
else
|
33
|
+
@local_cache[key] = @remote_cache.get(key, *options)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def set(key, value, *options)
|
38
|
+
@remote_cache.set(key, value, *options)
|
39
|
+
@local_cache[key] = value
|
40
|
+
end
|
41
|
+
|
42
|
+
def add(key, value, *options)
|
43
|
+
result = @remote_cache.add(key, value, *options)
|
44
|
+
if result == "STORED\r\n"
|
45
|
+
@local_cache[key] = value
|
46
|
+
end
|
47
|
+
result
|
48
|
+
end
|
49
|
+
|
50
|
+
def delete(key, *options)
|
51
|
+
@remote_cache.delete(key, *options)
|
52
|
+
@local_cache.delete(key)
|
53
|
+
end
|
54
|
+
|
55
|
+
def method_missing(method, *args, &block)
|
56
|
+
@remote_cache.send(method, *args, &block)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/cash/lock.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Cash
|
2
|
+
class Lock
|
3
|
+
class Error < RuntimeError; end
|
4
|
+
|
5
|
+
DEFAULT_RETRY = 5
|
6
|
+
DEFAULT_EXPIRY = 30
|
7
|
+
|
8
|
+
def initialize(cache)
|
9
|
+
@cache = cache
|
10
|
+
end
|
11
|
+
|
12
|
+
def synchronize(key, lock_expiry = DEFAULT_EXPIRY, retries = DEFAULT_RETRY)
|
13
|
+
if recursive_lock?(key)
|
14
|
+
yield
|
15
|
+
else
|
16
|
+
acquire_lock(key, lock_expiry, retries)
|
17
|
+
begin
|
18
|
+
yield
|
19
|
+
ensure
|
20
|
+
release_lock(key)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def acquire_lock(key, lock_expiry = DEFAULT_EXPIRY, retries = DEFAULT_RETRY)
|
26
|
+
retries.times do |count|
|
27
|
+
begin
|
28
|
+
response = @cache.add("lock/#{key}", Process.pid, lock_expiry)
|
29
|
+
return if response == "STORED\r\n"
|
30
|
+
raise Error if count == retries - 1
|
31
|
+
end
|
32
|
+
exponential_sleep(count) unless count == retries - 1
|
33
|
+
end
|
34
|
+
raise Error, "Couldn't acquire memcache lock for: #{key}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def release_lock(key)
|
38
|
+
@cache.delete("lock/#{key}")
|
39
|
+
end
|
40
|
+
|
41
|
+
def exponential_sleep(count)
|
42
|
+
@runtime += Benchmark::measure { sleep((2**count) / 2.0) }
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def recursive_lock?(key)
|
48
|
+
@cache.get("lock/#{key}") == Process.pid
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
data/lib/cash/mock.rb
ADDED
@@ -0,0 +1,86 @@
|
|
1
|
+
module Cash
|
2
|
+
class Mock < HashWithIndifferentAccess
|
3
|
+
attr_accessor :servers
|
4
|
+
|
5
|
+
def get_multi(keys)
|
6
|
+
slice(*keys).collect { |k,v| [k, Marshal.load(v)] }.to_hash
|
7
|
+
end
|
8
|
+
|
9
|
+
def set(key, value, ttl = 0, raw = false)
|
10
|
+
self[key] = marshal(value, raw)
|
11
|
+
end
|
12
|
+
|
13
|
+
def get(key, raw = false)
|
14
|
+
if raw
|
15
|
+
self[key]
|
16
|
+
else
|
17
|
+
if self.has_key?(key)
|
18
|
+
Marshal.load(self[key])
|
19
|
+
else
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def incr(key, amount = 1)
|
26
|
+
if self.has_key?(key)
|
27
|
+
self[key] = (self[key].to_i + amount).to_s
|
28
|
+
self[key].to_i
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def decr(key, amount = 1)
|
33
|
+
if self.has_key?(key)
|
34
|
+
self[key] = (self[key].to_i - amount).to_s
|
35
|
+
self[key].to_i
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def add(key, value, ttl = 0, raw = false)
|
40
|
+
if self.has_key?(key)
|
41
|
+
"NOT_STORED\r\n"
|
42
|
+
else
|
43
|
+
self[key] = marshal(value, raw)
|
44
|
+
"STORED\r\n"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def append(key, value)
|
49
|
+
set(key, get(key, true).to_s + value.to_s, nil, true)
|
50
|
+
end
|
51
|
+
|
52
|
+
def namespace
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def flush_all
|
57
|
+
clear
|
58
|
+
end
|
59
|
+
|
60
|
+
def stats
|
61
|
+
{}
|
62
|
+
end
|
63
|
+
|
64
|
+
def reset_runtime
|
65
|
+
[0, Hash.new(0)]
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def marshal(value, raw)
|
71
|
+
if raw
|
72
|
+
value.to_s
|
73
|
+
else
|
74
|
+
Marshal.dump(value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def unmarshal(marshaled_obj)
|
79
|
+
Marshal.load(marshaled_obj)
|
80
|
+
end
|
81
|
+
|
82
|
+
def deep_clone(obj)
|
83
|
+
unmarshal(marshal(obj))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class Abstract
|
4
|
+
delegate :with_exclusive_scope, :get, :table_name, :indices, :find_from_ids_without_cache, :cache_key, :columns_hash, :to => :@active_record
|
5
|
+
|
6
|
+
def self.perform(*args)
|
7
|
+
new(*args).perform
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(active_record, options1, options2)
|
11
|
+
@active_record, @options1, @options2 = active_record, options1, options2 || {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def perform(find_options = {}, get_options = {})
|
15
|
+
if cache_config = cacheable?(@options1, @options2, find_options)
|
16
|
+
cache_keys, index = cache_keys(cache_config[0]), cache_config[1]
|
17
|
+
|
18
|
+
misses, missed_keys, objects = hit_or_miss(cache_keys, index, get_options)
|
19
|
+
format_results(cache_keys, choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects))
|
20
|
+
else
|
21
|
+
uncacheable
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
DESC = /DESC/i
|
26
|
+
|
27
|
+
def order
|
28
|
+
@order ||= begin
|
29
|
+
if order_sql = @options1[:order] || @options2[:order]
|
30
|
+
matched, table_name, column_name, direction = *(ORDER.match(order_sql))
|
31
|
+
[column_name, direction =~ DESC ? :desc : :asc]
|
32
|
+
else
|
33
|
+
['id', :asc]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def limit
|
39
|
+
@limit ||= @options1[:limit] || @options2[:limit]
|
40
|
+
end
|
41
|
+
|
42
|
+
def offset
|
43
|
+
@offset ||= @options1[:offset] || @options2[:offset] || 0
|
44
|
+
end
|
45
|
+
|
46
|
+
def calculation?
|
47
|
+
false
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def cacheable?(*optionss)
|
52
|
+
optionss.each { |options| return unless safe_options_for_cache?(options) }
|
53
|
+
partial_indices = optionss.collect { |options| attribute_value_pairs_for_conditions(options[:conditions]) }
|
54
|
+
return if partial_indices.include?(nil)
|
55
|
+
attribute_value_pairs = partial_indices.sum.sort { |x, y| x[0] <=> y[0] }
|
56
|
+
if index = indexed_on?(attribute_value_pairs.collect { |pair| pair[0] })
|
57
|
+
if index.matches?(self)
|
58
|
+
[attribute_value_pairs, index]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def hit_or_miss(cache_keys, index, options)
|
64
|
+
misses, missed_keys = nil, nil
|
65
|
+
objects = @active_record.get(cache_keys, options.merge(:ttl => index.ttl)) do |missed_keys|
|
66
|
+
misses = miss(missed_keys, @options1.merge(:limit => index.window))
|
67
|
+
serialize_objects(index, misses)
|
68
|
+
end
|
69
|
+
[misses, missed_keys, objects]
|
70
|
+
end
|
71
|
+
|
72
|
+
def cache_keys(attribute_value_pairs)
|
73
|
+
attribute_value_pairs.flatten.join('/')
|
74
|
+
end
|
75
|
+
|
76
|
+
def safe_options_for_cache?(options)
|
77
|
+
return false unless options.kind_of?(Hash)
|
78
|
+
options.except(:conditions, :readonly, :limit, :offset, :order).values.compact.empty? && !options[:readonly]
|
79
|
+
end
|
80
|
+
|
81
|
+
def attribute_value_pairs_for_conditions(conditions)
|
82
|
+
case conditions
|
83
|
+
when Hash
|
84
|
+
conditions.to_a.collect { |key, value| [key.to_s, value] }
|
85
|
+
when String
|
86
|
+
parse_indices_from_condition(conditions)
|
87
|
+
when Array
|
88
|
+
parse_indices_from_condition(*conditions)
|
89
|
+
when NilClass
|
90
|
+
[]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
AND = /\s+AND\s+/i
|
95
|
+
TABLE_AND_COLUMN = /(?:(?:`|")?(\w+)(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
|
96
|
+
VALUE = /'?(\d+|\?|(?:(?:[^']|'')*))'?/ # Matches: 123, ?, '123', '12''3'
|
97
|
+
KEY_EQ_VALUE = /^\(?#{TABLE_AND_COLUMN}\s+=\s+#{VALUE}\)?$/ # Matches: KEY = VALUE, (KEY = VALUE)
|
98
|
+
ORDER = /^#{TABLE_AND_COLUMN}\s*(ASC|DESC)?$/i # Matches: COLUMN ASC, COLUMN DESC, COLUMN
|
99
|
+
|
100
|
+
def parse_indices_from_condition(conditions = '', *values)
|
101
|
+
values = values.dup
|
102
|
+
conditions.split(AND).inject([]) do |indices, condition|
|
103
|
+
matched, table_name, column_name, sql_value = *(KEY_EQ_VALUE.match(condition))
|
104
|
+
if matched
|
105
|
+
value = sql_value == '?' ? values.shift : columns_hash[column_name].type_cast(sql_value)
|
106
|
+
indices << [column_name, value]
|
107
|
+
else
|
108
|
+
return nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def indexed_on?(attributes)
|
114
|
+
indices.detect { |index| index == attributes }
|
115
|
+
end
|
116
|
+
alias_method :index_for, :indexed_on?
|
117
|
+
|
118
|
+
def format_results(cache_keys, objects)
|
119
|
+
return objects if objects.blank?
|
120
|
+
|
121
|
+
objects = convert_to_array(cache_keys, objects)
|
122
|
+
objects = apply_limits_and_offsets(objects, @options1)
|
123
|
+
deserialize_objects(objects)
|
124
|
+
end
|
125
|
+
|
126
|
+
def choose_deserialized_objects_if_possible(missed_keys, cache_keys, misses, objects)
|
127
|
+
missed_keys == cache_keys ? misses : objects
|
128
|
+
end
|
129
|
+
|
130
|
+
def serialize_objects(index, objects)
|
131
|
+
Array(objects).collect { |missed| index.serialize_object(missed) }
|
132
|
+
end
|
133
|
+
|
134
|
+
def convert_to_array(cache_keys, object)
|
135
|
+
if object.kind_of?(Hash)
|
136
|
+
cache_keys.collect { |key| object[cache_key(key)] }.flatten.compact
|
137
|
+
else
|
138
|
+
Array(object)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def apply_limits_and_offsets(results, options)
|
143
|
+
results.slice((options[:offset] || 0), (options[:limit] || results.length))
|
144
|
+
end
|
145
|
+
|
146
|
+
def deserialize_objects(objects)
|
147
|
+
if objects.first.kind_of?(ActiveRecord::Base)
|
148
|
+
objects
|
149
|
+
else
|
150
|
+
cache_keys = objects.collect { |id| "id/#{id}" }
|
151
|
+
objects = get(cache_keys, &method(:find_from_keys))
|
152
|
+
convert_to_array(cache_keys, objects)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def find_from_keys(*missing_keys)
|
157
|
+
missing_ids = Array(missing_keys).flatten.collect { |key| key.split('/')[2].to_i }
|
158
|
+
find_from_ids_without_cache(missing_ids, {})
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class Calculation < Abstract
|
4
|
+
delegate :calculate_without_cache, :incr, :to => :@active_record
|
5
|
+
|
6
|
+
def initialize(active_record, operation, column, options1, options2)
|
7
|
+
super(active_record, options1, options2)
|
8
|
+
@operation, @column = operation, column
|
9
|
+
end
|
10
|
+
|
11
|
+
def perform
|
12
|
+
super({}, :raw => true)
|
13
|
+
end
|
14
|
+
|
15
|
+
def calculation?
|
16
|
+
true
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
def miss(_, __)
|
21
|
+
calculate_without_cache(@operation, @column, @options1)
|
22
|
+
end
|
23
|
+
|
24
|
+
def uncacheable
|
25
|
+
calculate_without_cache(@operation, @column, @options1)
|
26
|
+
end
|
27
|
+
|
28
|
+
def format_results(_, objects)
|
29
|
+
objects.to_i
|
30
|
+
end
|
31
|
+
|
32
|
+
def serialize_objects(_, objects)
|
33
|
+
objects.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def cacheable?(*optionss)
|
37
|
+
@column == :all && super(*optionss)
|
38
|
+
end
|
39
|
+
|
40
|
+
def cache_keys(attribute_value_pairs)
|
41
|
+
"#{super(attribute_value_pairs)}/#{@operation}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class PrimaryKey < Abstract
|
4
|
+
def initialize(active_record, ids, options1, options2)
|
5
|
+
super(active_record, options1, options2)
|
6
|
+
@expects_array = ids.first.kind_of?(Array)
|
7
|
+
@original_ids = ids
|
8
|
+
@ids = ids.flatten.compact.uniq.collect do |object|
|
9
|
+
object.respond_to?(:quoted_id) ? object.quoted_id : object.to_i
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def perform
|
14
|
+
return [] if @expects_array && @ids.empty?
|
15
|
+
raise ActiveRecord::RecordNotFound if @ids.empty?
|
16
|
+
|
17
|
+
super(:conditions => { :id => @ids.first })
|
18
|
+
end
|
19
|
+
|
20
|
+
protected
|
21
|
+
def deserialize_objects(objects)
|
22
|
+
convert_to_active_record_collection(super(objects))
|
23
|
+
end
|
24
|
+
|
25
|
+
def cache_keys(attribute_value_pairs)
|
26
|
+
@ids.collect { |id| "id/#{id}" }
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def miss(missing_keys, options)
|
31
|
+
find_from_keys(*missing_keys)
|
32
|
+
end
|
33
|
+
|
34
|
+
def uncacheable
|
35
|
+
find_from_ids_without_cache(@original_ids, @options1)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def convert_to_active_record_collection(objects)
|
40
|
+
case objects.size
|
41
|
+
when 0
|
42
|
+
raise ActiveRecord::RecordNotFound
|
43
|
+
when 1
|
44
|
+
@expects_array ? objects : objects.first
|
45
|
+
else
|
46
|
+
objects
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Cash
|
2
|
+
module Query
|
3
|
+
class Select < Abstract
|
4
|
+
delegate :find_every_without_cache, :to => :@active_record
|
5
|
+
|
6
|
+
protected
|
7
|
+
def miss(_, miss_options)
|
8
|
+
find_every_without_cache(miss_options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def uncacheable
|
12
|
+
find_every_without_cache(@options1)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/cash/request.rb
ADDED