kasket 0.5.5 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +2 -2
- data/VERSION +1 -1
- data/lib/kasket.rb +8 -7
- data/lib/kasket/active_record_patches.rb +1 -1
- data/lib/kasket/cache.rb +15 -20
- data/lib/kasket/configuration_mixin.rb +39 -3
- data/lib/kasket/dirty_mixin.rb +1 -1
- data/lib/kasket/query_parser.rb +53 -0
- data/lib/kasket/rack_middleware.rb +1 -1
- data/lib/kasket/read_mixin.rb +23 -75
- data/lib/kasket/reload_association_mixin.rb +1 -1
- data/lib/kasket/write_mixin.rb +6 -4
- data/test/cache_expiry_test.rb +10 -16
- data/test/cache_test.rb +102 -0
- data/test/database.yml +2 -0
- data/test/dirty_test.rb +1 -4
- data/test/find_one_test.rb +10 -3
- data/test/find_some_test.rb +6 -10
- data/test/helper.rb +5 -0
- data/test/parser_test.rb +98 -0
- data/test/read_mixin_test.rb +45 -0
- data/test/schema.rb +1 -1
- metadata +11 -6
- data/lib/kasket/conditions_parser.rb +0 -79
- data/test/serialization_test.rb +0 -21
data/Rakefile
CHANGED
@@ -6,10 +6,10 @@ begin
|
|
6
6
|
Jeweler::Tasks.new do |gem|
|
7
7
|
gem.name = "kasket"
|
8
8
|
gem.summary = %Q{A write back caching layer on active record}
|
9
|
-
gem.description = %Q{
|
9
|
+
gem.description = %Q{put's a cap on your queries}
|
10
10
|
gem.email = "mick@staugaard.com"
|
11
11
|
gem.homepage = "http://github.com/staugaard/kasket"
|
12
|
-
gem.authors = ["Mick Staugaard"]
|
12
|
+
gem.authors = ["Mick Staugaard", "Eric Chapweske"]
|
13
13
|
gem.add_dependency('activerecord', '>= 2.3.4')
|
14
14
|
gem.add_dependency('activesupport', '>= 2.3.4')
|
15
15
|
gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.6.0
|
data/lib/kasket.rb
CHANGED
@@ -9,6 +9,7 @@ module Kasket
|
|
9
9
|
autoload :ConfigurationMixin, 'kasket/configuration_mixin'
|
10
10
|
autoload :ReloadAssociationMixin, 'kasket/reload_association_mixin'
|
11
11
|
autoload :RackMiddleware, 'kasket/rack_middleware'
|
12
|
+
autoload :Query, 'kasket/query'
|
12
13
|
|
13
14
|
CONFIGURATION = {:max_collection_size => 100}
|
14
15
|
|
@@ -26,13 +27,6 @@ module Kasket
|
|
26
27
|
ActiveRecord::Associations::BelongsToPolymorphicAssociation.send(:include, Kasket::ReloadAssociationMixin)
|
27
28
|
ActiveRecord::Associations::HasOneThroughAssociation.send(:include, Kasket::ReloadAssociationMixin)
|
28
29
|
|
29
|
-
#sets up local cache clearing on rack
|
30
|
-
begin
|
31
|
-
ActionController::Dispatcher.middleware.use(Kasket::RackMiddleware)
|
32
|
-
rescue NameError => e
|
33
|
-
puts('WARNING: The kasket rack middleware is not in your rack stack')
|
34
|
-
end
|
35
|
-
|
36
30
|
#sets up local cache clearing before each request.
|
37
31
|
#this is done to make it work for non rack rails and for functional tests
|
38
32
|
begin
|
@@ -42,6 +36,13 @@ module Kasket
|
|
42
36
|
rescue NameError => e
|
43
37
|
end
|
44
38
|
|
39
|
+
#sets up local cache clearing on rack
|
40
|
+
begin
|
41
|
+
ActionController::Dispatcher.middleware.use(Kasket::RackMiddleware)
|
42
|
+
rescue NameError => e
|
43
|
+
puts('WARNING: The kasket rack middleware is not in your rack stack')
|
44
|
+
end
|
45
|
+
|
45
46
|
#sets up local cache clearing after each test case
|
46
47
|
begin
|
47
48
|
ActiveSupport::TestCase.class_eval do
|
data/lib/kasket/cache.rb
CHANGED
@@ -6,11 +6,9 @@ module Kasket
|
|
6
6
|
|
7
7
|
def read(*args)
|
8
8
|
result = @local_cache[args[0]] || Rails.cache.read(*args)
|
9
|
-
if result.is_a?(
|
10
|
-
result = result.instanciate_model
|
11
|
-
elsif result.is_a?(Array)
|
9
|
+
if result.is_a?(Array) && result.first.is_a?(String)
|
12
10
|
models = get_multi(result)
|
13
|
-
result = result.map { |key| models[key]}
|
11
|
+
result = result.map { |key| models[key] }
|
14
12
|
end
|
15
13
|
|
16
14
|
@local_cache[args[0]] = result if result
|
@@ -25,7 +23,6 @@ module Kasket
|
|
25
23
|
if Rails.cache.respond_to?(:read_multi)
|
26
24
|
missing_map = Rails.cache.read_multi(missing_keys)
|
27
25
|
missing_map.each do |key, value|
|
28
|
-
value = value.instanciate_model if value.is_a?(CachedModel)
|
29
26
|
missing_map[key] = @local_cache[key] = value
|
30
27
|
end
|
31
28
|
map.merge!(missing_map)
|
@@ -39,14 +36,12 @@ module Kasket
|
|
39
36
|
map
|
40
37
|
end
|
41
38
|
|
42
|
-
def write(
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
args[1] = CachedModel.new(args[1])
|
39
|
+
def write(key, value)
|
40
|
+
if storable?(value)
|
41
|
+
@local_cache[key] = value.duplicable? ? value.dup : value
|
42
|
+
Rails.cache.write(key, value.duplicable? ? value.dup : value) # Fix due to Rails.cache freezing values in 2.3.4
|
47
43
|
end
|
48
|
-
|
49
|
-
Rails.cache.write(*args)
|
44
|
+
value
|
50
45
|
end
|
51
46
|
|
52
47
|
def delete(*args)
|
@@ -68,15 +63,15 @@ module Kasket
|
|
68
63
|
@local_cache = {}
|
69
64
|
end
|
70
65
|
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
@attributes = model.instance_variable_get(:@attributes)
|
75
|
-
end
|
66
|
+
def local
|
67
|
+
@local_cache
|
68
|
+
end
|
76
69
|
|
77
|
-
|
78
|
-
|
70
|
+
protected
|
71
|
+
|
72
|
+
def storable?(value)
|
73
|
+
!value.is_a?(Array) || value.size <= Kasket::CONFIGURATION[:max_collection_size]
|
79
74
|
end
|
80
|
-
|
75
|
+
|
81
76
|
end
|
82
77
|
end
|
@@ -4,14 +4,50 @@ module Kasket
|
|
4
4
|
autoload :ReadMixin, 'kasket/read_mixin'
|
5
5
|
autoload :WriteMixin, 'kasket/write_mixin'
|
6
6
|
autoload :DirtyMixin, 'kasket/dirty_mixin'
|
7
|
+
autoload :QueryParser, 'kasket/query_parser'
|
7
8
|
|
8
9
|
module ConfigurationMixin
|
10
|
+
|
11
|
+
def without_kasket(&block)
|
12
|
+
old_value = @use_kasket || true
|
13
|
+
@use_kasket = false
|
14
|
+
yield
|
15
|
+
ensure
|
16
|
+
@use_kasket = old_value
|
17
|
+
end
|
18
|
+
|
19
|
+
def use_kasket?
|
20
|
+
@use_kasket != false
|
21
|
+
end
|
22
|
+
|
23
|
+
def kasket_parser
|
24
|
+
@kasket_parser ||= QueryParser.new(self)
|
25
|
+
end
|
26
|
+
|
9
27
|
def kasket_key_prefix
|
10
28
|
@kasket_key_prefix ||= "kasket/#{table_name}/version=#{column_names.join.sum}/"
|
11
29
|
end
|
12
30
|
|
13
31
|
def kasket_key_for(attribute_value_pairs)
|
14
|
-
kasket_key_prefix + attribute_value_pairs.map
|
32
|
+
kasket_key_prefix + attribute_value_pairs.map do |attribute, value|
|
33
|
+
if (column = columns_hash[attribute.to_s]) && column.number?
|
34
|
+
attribute.to_s + '=' + convert_number_column_value(value.to_s)
|
35
|
+
else
|
36
|
+
attribute.to_s + '=' + connection.quote(value.to_s)
|
37
|
+
end
|
38
|
+
end.join('/')
|
39
|
+
end
|
40
|
+
|
41
|
+
def convert_number_column_value(value)
|
42
|
+
if value == false
|
43
|
+
0
|
44
|
+
elsif value == true
|
45
|
+
1
|
46
|
+
elsif value.is_a?(String) && value.blank?
|
47
|
+
nil
|
48
|
+
else
|
49
|
+
value
|
50
|
+
end
|
15
51
|
end
|
16
52
|
|
17
53
|
def kasket_key_for_id(id)
|
@@ -42,8 +78,8 @@ module Kasket
|
|
42
78
|
@kasket_indices << attributes unless @kasket_indices.include?(attributes)
|
43
79
|
|
44
80
|
include WriteMixin unless instance_methods.include?('store_in_kasket')
|
45
|
-
extend ReadMixin unless methods.include?('without_kasket')
|
46
81
|
extend DirtyMixin unless methods.include?('kasket_dirty_methods')
|
82
|
+
extend ReadMixin unless methods.include?('find_by_sql_with_kasket')
|
47
83
|
end
|
48
84
|
end
|
49
|
-
end
|
85
|
+
end
|
data/lib/kasket/dirty_mixin.rb
CHANGED
@@ -0,0 +1,53 @@
|
|
1
|
+
module Kasket
|
2
|
+
class QueryParser
|
3
|
+
# Examples:
|
4
|
+
# SELECT * FROM `users` WHERE (`users`.`id` = 2)
|
5
|
+
# SELECT * FROM `users` WHERE (`users`.`id` = 2) LIMIT 1
|
6
|
+
# 'SELECT * FROM \'posts\' WHERE (\'posts\'.\'id\' = 574019247) '
|
7
|
+
|
8
|
+
AND = /\s+AND\s+/i
|
9
|
+
VALUE = /'?(\d+|\?|(?:(?:[^']|''|\\')*))'?/ # Matches: 123, ?, '123', '12''3'
|
10
|
+
|
11
|
+
def initialize(model_class)
|
12
|
+
@model_class = model_class
|
13
|
+
@supported_query_pattern = /^select \* from (?:`|")#{@model_class.table_name}(?:`|") where \((.*)\)(|\s+limit 1)\s*$/i
|
14
|
+
@table_and_column_pattern = /(?:(?:`|")?#{@model_class.table_name}(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
|
15
|
+
@key_eq_value_pattern = /^[\(\s]*#{@table_and_column_pattern}\s+=\s+#{VALUE}[\)\s]*$/ # Matches: KEY = VALUE, (KEY = VALUE), ()(KEY = VALUE))
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse(sql)
|
19
|
+
if match = @supported_query_pattern.match(sql)
|
20
|
+
query = Hash.new
|
21
|
+
query[:attributes] = sorted_attribute_value_pairs(match[1])
|
22
|
+
return nil if query[:attributes].nil?
|
23
|
+
query[:index] = query[:attributes].map(&:first)
|
24
|
+
query[:limit] = match[2].blank? ? nil : 1
|
25
|
+
query[:key] = @model_class.kasket_key_for(query[:attributes])
|
26
|
+
query[:key] << '/first' if query[:limit] == 1 && !query[:index].include?(:id)
|
27
|
+
query
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def sorted_attribute_value_pairs(conditions)
|
34
|
+
if attributes = parse_condition(conditions)
|
35
|
+
attributes.sort { |pair1, pair2| pair1[0].to_s <=> pair2[0].to_s }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def parse_condition(conditions = '', *values)
|
40
|
+
values = values.dup
|
41
|
+
conditions.split(AND).inject([]) do |pairs, condition|
|
42
|
+
matched, column_name, sql_value = *(@key_eq_value_pattern.match(condition))
|
43
|
+
if matched
|
44
|
+
value = sql_value == '?' ? values.shift : sql_value
|
45
|
+
pairs << [column_name.to_sym, value.gsub(/''|\\'/, "'")]
|
46
|
+
else
|
47
|
+
return nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
end
|
data/lib/kasket/read_mixin.rb
CHANGED
@@ -1,92 +1,40 @@
|
|
1
|
-
require 'kasket/conditions_parser'
|
2
|
-
|
3
1
|
module Kasket
|
4
2
|
module ReadMixin
|
5
3
|
|
6
|
-
def self.extended(
|
7
|
-
class <<
|
8
|
-
alias_method_chain :
|
9
|
-
alias_method_chain :find_every, :kasket unless methods.include?('find_every_without_kasket')
|
4
|
+
def self.extended(base)
|
5
|
+
class << base
|
6
|
+
alias_method_chain :find_by_sql, :kasket
|
10
7
|
end
|
11
8
|
end
|
12
9
|
|
13
|
-
def
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def find_every_with_kasket(options)
|
22
|
-
attribute_value_pairs = kasket_conditions_parser.attribute_value_pairs(options) if cache_safe?(options)
|
23
|
-
|
24
|
-
limit = (options[:limit] || (scope(:find) || {})[:limit])
|
25
|
-
|
26
|
-
if (limit.nil? || limit == 1) && attribute_value_pairs && has_kasket_index_on?(attribute_value_pairs.map(&:first))
|
27
|
-
collection_key = kasket_key_for(attribute_value_pairs)
|
28
|
-
collection_key << '/first' if limit == 1
|
29
|
-
unless records = Kasket.cache.read(collection_key)
|
30
|
-
records = without_kasket do
|
31
|
-
find_every_without_kasket(options)
|
32
|
-
end
|
33
|
-
|
34
|
-
if records.size == 1
|
35
|
-
Kasket.cache.write(collection_key, records[0])
|
36
|
-
elsif records.size <= Kasket::CONFIGURATION[:max_collection_size]
|
37
|
-
records.each { |record| record.store_in_kasket if record }
|
38
|
-
Kasket.cache.write(collection_key, records.map(&:kasket_key))
|
39
|
-
end
|
10
|
+
def find_by_sql_with_kasket(sql)
|
11
|
+
query = kasket_parser.parse(sql) if use_kasket?
|
12
|
+
if query && has_kasket_index_on?(query[:index])
|
13
|
+
if value = Kasket.cache.read(query[:key])
|
14
|
+
Array.wrap(value).collect! { |record| instantiate(record.clone) }
|
15
|
+
else
|
16
|
+
store_in_kasket(query[:key], find_by_sql_without_kasket(sql))
|
40
17
|
end
|
41
|
-
|
42
|
-
Array(records)
|
43
18
|
else
|
44
|
-
|
45
|
-
find_every_without_kasket(options)
|
46
|
-
end
|
19
|
+
find_by_sql_without_kasket(sql)
|
47
20
|
end
|
48
21
|
end
|
49
22
|
|
50
|
-
|
51
|
-
attribute_value_pairs = kasket_conditions_parser.attribute_value_pairs(options) if cache_safe?(options)
|
52
|
-
attribute_value_pairs << [:id, ids] if attribute_value_pairs
|
53
|
-
|
54
|
-
limit = (options[:limit] || (scope(:find) || {})[:limit])
|
55
|
-
|
56
|
-
if limit.nil? && attribute_value_pairs && has_kasket_index_on?(attribute_value_pairs.map(&:first))
|
57
|
-
id_to_key_map = Hash[*ids.uniq.map { |id| [id, kasket_key_for_id(id)] }.flatten]
|
58
|
-
cached_record_map = Kasket.cache.get_multi(id_to_key_map.values)
|
59
|
-
|
60
|
-
missing_keys = cached_record_map.select { |key, record| record.nil? }.map(&:first)
|
23
|
+
protected
|
61
24
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
else
|
73
|
-
without_kasket do
|
74
|
-
find_some_without_kasket(ids, options)
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
private
|
80
|
-
|
81
|
-
def cache_safe?(options)
|
82
|
-
@use_kasket != false && [options, scope(:find) || {}].all? do |hash|
|
83
|
-
result = hash[:select].nil? && hash[:joins].nil? && hash[:order].nil? && hash[:offset].nil?
|
84
|
-
result && (options[:limit].nil? || options[:limit] == 1)
|
25
|
+
def store_in_kasket(key, records)
|
26
|
+
if records.size == 1
|
27
|
+
Kasket.cache.write(key, records.first.instance_variable_get(:@attributes))
|
28
|
+
else
|
29
|
+
keys = records.map do |record|
|
30
|
+
key = kasket_key_for_id(record.id)
|
31
|
+
Kasket.cache.write(key, record.instance_variable_get(:@attributes))
|
32
|
+
key
|
33
|
+
end
|
34
|
+
Kasket.cache.write(key, keys)
|
85
35
|
end
|
36
|
+
records
|
86
37
|
end
|
87
38
|
|
88
|
-
def kasket_conditions_parser
|
89
|
-
@kasket_conditions_parser ||= Kasket::ConditionsParser.new(self)
|
90
|
-
end
|
91
39
|
end
|
92
40
|
end
|
data/lib/kasket/write_mixin.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
module Kasket
|
2
2
|
module WriteMixin
|
3
|
+
|
3
4
|
module ClassMethods
|
4
5
|
def remove_from_kasket(ids)
|
5
6
|
Array(ids).each do |id|
|
@@ -20,7 +21,7 @@ module Kasket
|
|
20
21
|
|
21
22
|
def store_in_kasket
|
22
23
|
if !readonly? && kasket_key
|
23
|
-
Kasket.cache.write(kasket_key,
|
24
|
+
Kasket.cache.write(kasket_key, @attributes)
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
@@ -35,13 +36,14 @@ module Kasket
|
|
35
36
|
keys = []
|
36
37
|
self.class.kasket_indices.each do |index|
|
37
38
|
keys += attribute_sets.map do |attribute_set|
|
38
|
-
self.class.kasket_key_for(index.map { |attribute| [attribute, attribute_set[attribute]]})
|
39
|
+
key = self.class.kasket_key_for(index.map { |attribute| [attribute, attribute_set[attribute]]})
|
40
|
+
index.include?(:id) ? key : [key, key + '/first']
|
39
41
|
end
|
40
42
|
end
|
41
43
|
|
42
|
-
keys.uniq!
|
43
|
-
keys.map! {|key| [key, "#{key}/first"]}
|
44
44
|
keys.flatten!
|
45
|
+
keys.uniq!
|
46
|
+
keys
|
45
47
|
end
|
46
48
|
|
47
49
|
def clear_kasket_indices
|
data/test/cache_expiry_test.rb
CHANGED
@@ -3,14 +3,11 @@ require File.dirname(__FILE__) + '/helper'
|
|
3
3
|
class CacheExpiryTest < ActiveSupport::TestCase
|
4
4
|
fixtures :blogs, :posts
|
5
5
|
|
6
|
-
Post.has_kasket
|
7
|
-
Post.has_kasket_on :title
|
8
|
-
Post.has_kasket_on :blog_id
|
9
|
-
|
10
6
|
context "a cached object" do
|
11
7
|
setup do
|
12
8
|
post = Post.first
|
13
9
|
@post = Post.find(post.id)
|
10
|
+
|
14
11
|
assert(Rails.cache.read(@post.kasket_key))
|
15
12
|
end
|
16
13
|
|
@@ -21,11 +18,9 @@ class CacheExpiryTest < ActiveSupport::TestCase
|
|
21
18
|
|
22
19
|
should "clear all indices for instance when deleted" do
|
23
20
|
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "id=#{@post.id}")
|
24
|
-
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "
|
25
|
-
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title
|
26
|
-
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "
|
27
|
-
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "blog_id=#{@post.blog_id}")
|
28
|
-
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "blog_id=#{@post.blog_id}/first")
|
21
|
+
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title='#{@post.title}'")
|
22
|
+
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title='#{@post.title}'/first")
|
23
|
+
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "blog_id=#{@post.blog_id}/id=#{@post.id}")
|
29
24
|
Kasket.cache.expects(:delete).never
|
30
25
|
|
31
26
|
@post.destroy
|
@@ -39,17 +34,16 @@ class CacheExpiryTest < ActiveSupport::TestCase
|
|
39
34
|
|
40
35
|
should "clear all indices for instance when updated" do
|
41
36
|
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "id=#{@post.id}")
|
42
|
-
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "
|
43
|
-
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title
|
44
|
-
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title
|
45
|
-
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title=new title")
|
46
|
-
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "
|
47
|
-
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "blog_id=#{@post.blog_id}")
|
48
|
-
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "blog_id=#{@post.blog_id}/first")
|
37
|
+
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title='#{@post.title}'")
|
38
|
+
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title='#{@post.title}'/first")
|
39
|
+
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title='new title'")
|
40
|
+
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "title='new title'/first")
|
41
|
+
Kasket.cache.expects(:delete).with(Post.kasket_key_prefix + "blog_id=#{@post.blog_id}/id=#{@post.id}")
|
49
42
|
Kasket.cache.expects(:delete).never
|
50
43
|
|
51
44
|
@post.title = "new title"
|
52
45
|
@post.save
|
53
46
|
end
|
47
|
+
|
54
48
|
end
|
55
49
|
end
|
data/test/cache_test.rb
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/helper'
|
2
|
+
|
3
|
+
class CacheTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
context "Cache" do
|
6
|
+
setup do
|
7
|
+
@cache = Kasket::Cache.new
|
8
|
+
end
|
9
|
+
|
10
|
+
context "reading" do
|
11
|
+
|
12
|
+
should "work with non collection values" do
|
13
|
+
@cache.write('key', 'value')
|
14
|
+
assert_equal 'value', @cache.read('key')
|
15
|
+
end
|
16
|
+
|
17
|
+
should "fetch original results of stored collections" do
|
18
|
+
@cache.write('key1', 'value1')
|
19
|
+
@cache.write('key2', 'value2')
|
20
|
+
@cache.write('key3', 'value3')
|
21
|
+
@cache.write('collection_key', [ 'key1', 'key2', 'key3'])
|
22
|
+
|
23
|
+
assert_equal [ 'value1', 'value2', 'value3'], @cache.read('collection_key')
|
24
|
+
end
|
25
|
+
|
26
|
+
should "not impact original object" do
|
27
|
+
record = { 'id' => 1, 'color' => 'red' }
|
28
|
+
@cache.write('key', record)
|
29
|
+
record['id'] = 2
|
30
|
+
|
31
|
+
assert_not_equal record, @cache.read('key')
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
context "writing" do
|
37
|
+
setup do
|
38
|
+
@cache.write('key', 'value')
|
39
|
+
end
|
40
|
+
|
41
|
+
should "store the object locally" do
|
42
|
+
assert_equal 'value', @cache.local['key']
|
43
|
+
end
|
44
|
+
|
45
|
+
should "persist the object" do
|
46
|
+
@cache.clear_local
|
47
|
+
assert_equal 'value', @cache.read('key')
|
48
|
+
end
|
49
|
+
|
50
|
+
should "respect max collection size" do
|
51
|
+
original_max = Kasket::CONFIGURATION[:max_collection_size]
|
52
|
+
Kasket::CONFIGURATION[:max_collection_size] = 2
|
53
|
+
|
54
|
+
@cache.write('key', [ 'a', 'b'])
|
55
|
+
assert_equal 2, @cache.read('key').size
|
56
|
+
|
57
|
+
@cache.write('key2', ['a', 'b', 'c'])
|
58
|
+
assert_equal nil, @cache.read('key2')
|
59
|
+
|
60
|
+
Kasket::CONFIGURATION[:max_collection_size] = original_max
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
should "delete" do
|
66
|
+
@cache.write('key', 'value')
|
67
|
+
@cache.delete('key')
|
68
|
+
|
69
|
+
assert_equal nil, @cache.local['key']
|
70
|
+
assert_equal nil, @cache.read('key')
|
71
|
+
end
|
72
|
+
|
73
|
+
should "delete matched local" do
|
74
|
+
@cache.write('key1', 'value1')
|
75
|
+
@cache.write('key2', 'value2')
|
76
|
+
@cache.delete_matched_local(/2/)
|
77
|
+
|
78
|
+
assert_equal nil, @cache.local['key2']
|
79
|
+
assert_equal 'value1', @cache.local['key1']
|
80
|
+
assert_equal 'value2', @cache.read('key2')
|
81
|
+
end
|
82
|
+
|
83
|
+
should "delete local" do
|
84
|
+
@cache.write('key1', 'value1')
|
85
|
+
@cache.write('key2', 'value2')
|
86
|
+
@cache.delete_local('key1', 'key2')
|
87
|
+
|
88
|
+
assert_equal nil, @cache.local['key1']
|
89
|
+
assert_equal nil, @cache.local['key2']
|
90
|
+
assert_equal 'value1', @cache.read('key1')
|
91
|
+
assert_equal 'value2', @cache.read('key2')
|
92
|
+
end
|
93
|
+
|
94
|
+
should "clear local" do
|
95
|
+
@cache.write('key1', 'value1')
|
96
|
+
@cache.clear_local
|
97
|
+
|
98
|
+
assert @cache.local.blank?
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
data/test/database.yml
CHANGED
data/test/dirty_test.rb
CHANGED
@@ -3,13 +3,10 @@ require File.dirname(__FILE__) + '/helper'
|
|
3
3
|
class DirtyTest < ActiveSupport::TestCase
|
4
4
|
fixtures :blogs, :posts
|
5
5
|
|
6
|
-
Post.has_kasket
|
7
|
-
Post.kasket_dirty_methods :make_dirty!
|
8
|
-
|
9
6
|
should "clear the indices when a dirty method is called" do
|
10
7
|
post = Post.first
|
11
8
|
|
12
|
-
pots = Post.find(post.id)
|
9
|
+
Post.cache { pots = Post.find(post.id) }
|
13
10
|
assert(Rails.cache.read(post.kasket_key))
|
14
11
|
|
15
12
|
post.make_dirty!
|
data/test/find_one_test.rb
CHANGED
@@ -3,17 +3,24 @@ require File.dirname(__FILE__) + '/helper'
|
|
3
3
|
class FindOneTest < ActiveSupport::TestCase
|
4
4
|
fixtures :blogs, :posts
|
5
5
|
|
6
|
-
Post.has_kasket
|
7
|
-
|
8
6
|
should "cache find(id) calls" do
|
9
7
|
post = Post.first
|
10
|
-
|
8
|
+
Rails.cache.write(post.kasket_key, nil)
|
11
9
|
assert_equal(post, Post.find(post.id))
|
12
10
|
assert(Rails.cache.read(post.kasket_key))
|
13
11
|
Post.connection.expects(:select_all).never
|
14
12
|
assert_equal(post, Post.find(post.id))
|
15
13
|
end
|
16
14
|
|
15
|
+
should "only cache on indexed attributes" do
|
16
|
+
Kasket.cache.expects(:read).twice
|
17
|
+
Post.find_by_id(1)
|
18
|
+
Post.find_by_id(1, :conditions => {:blog_id => 2})
|
19
|
+
|
20
|
+
Kasket.cache.expects(:read).never
|
21
|
+
Post.first :conditions => {:blog_id => 2}
|
22
|
+
end
|
23
|
+
|
17
24
|
should "not use cache when using the :select option" do
|
18
25
|
post = Post.first
|
19
26
|
assert_nil(Rails.cache.read(post.kasket_key))
|
data/test/find_some_test.rb
CHANGED
@@ -3,10 +3,7 @@ require File.dirname(__FILE__) + '/helper'
|
|
3
3
|
class FindSomeTest < ActiveSupport::TestCase
|
4
4
|
fixtures :blogs, :posts
|
5
5
|
|
6
|
-
|
7
|
-
Post.has_kasket_on :blog_id
|
8
|
-
|
9
|
-
should "cache find(id, id) calls" do
|
6
|
+
should_eventually "cache find(id, id) calls" do
|
10
7
|
post1 = Post.first
|
11
8
|
post2 = Post.last
|
12
9
|
|
@@ -17,32 +14,31 @@ class FindSomeTest < ActiveSupport::TestCase
|
|
17
14
|
|
18
15
|
assert(Rails.cache.read(post1.kasket_key))
|
19
16
|
assert(Rails.cache.read(post2.kasket_key))
|
20
|
-
|
21
17
|
Post.connection.expects(:select_all).never
|
22
18
|
Post.find(post1.id, post2.id)
|
23
19
|
end
|
24
20
|
|
25
|
-
|
21
|
+
should_eventually "only lookup the records that are not in the cache" do
|
26
22
|
post1 = Post.first
|
27
23
|
post2 = Post.last
|
28
24
|
assert_equal(post1, Post.find(post1.id))
|
29
25
|
assert(Rails.cache.read(post1.kasket_key))
|
30
26
|
assert_nil(Rails.cache.read(post2.kasket_key))
|
31
27
|
|
32
|
-
Post.expects(:
|
28
|
+
Post.expects(:find_by_sql_without_kasket).with("SELECT * FROM \'posts\' WHERE (\'posts\'.\'id\' IN (#{post1.id},#{post2.id})) ").returns([post2])
|
33
29
|
found_posts = Post.find(post1.id, post2.id)
|
34
30
|
assert_equal([post1, post2].map(&:id).sort, found_posts.map(&:id).sort)
|
35
31
|
|
36
|
-
Post.expects(:
|
32
|
+
Post.expects(:find_by_sql_without_kasket).never
|
37
33
|
found_posts = Post.find(post1.id, post2.id)
|
38
34
|
assert_equal([post1, post2].map(&:id).sort, found_posts.map(&:id).sort)
|
39
35
|
end
|
40
36
|
|
41
|
-
|
37
|
+
should_eventually "cache on index other than primary key" do
|
42
38
|
blog = blogs(:a_blog)
|
43
39
|
posts = Post.find_all_by_blog_id(blog.id)
|
44
40
|
|
45
|
-
Post.expects(:
|
41
|
+
Post.expects(:find_by_sql_without_kasket).never
|
46
42
|
|
47
43
|
assert_equal(posts, Post.find_all_by_blog_id(blog.id))
|
48
44
|
end
|
data/test/helper.rb
CHANGED
@@ -56,10 +56,15 @@ class Post < ActiveRecord::Base
|
|
56
56
|
belongs_to :blog
|
57
57
|
has_many :comments
|
58
58
|
|
59
|
+
has_kasket
|
60
|
+
has_kasket_on :title
|
61
|
+
has_kasket_on :blog_id, :id
|
62
|
+
|
59
63
|
def make_dirty!
|
60
64
|
self.updated_at = Time.now
|
61
65
|
self.connection.execute("UPDATE posts SET updated_at = '#{updated_at.utc.to_s(:db)}' WHERE id = #{id}")
|
62
66
|
end
|
67
|
+
kasket_dirty_methods :make_dirty!
|
63
68
|
end
|
64
69
|
|
65
70
|
class Blog < ActiveRecord::Base
|
data/test/parser_test.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/helper'
|
2
|
+
require 'kasket/query_parser'
|
3
|
+
|
4
|
+
class ParserTest < ActiveSupport::TestCase
|
5
|
+
|
6
|
+
context "Parsing" do
|
7
|
+
setup do
|
8
|
+
@parser = Kasket::QueryParser.new(Post)
|
9
|
+
end
|
10
|
+
|
11
|
+
should "extract conditions" do
|
12
|
+
assert_equal [[:color, "red"], [:size, "big"]], @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`color` = red AND `posts`.`size` = big)')[:attributes]
|
13
|
+
end
|
14
|
+
|
15
|
+
should "extract required index" do
|
16
|
+
assert_equal [:color, :size], @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`color` = red AND `posts`.`size` = big)')[:index]
|
17
|
+
end
|
18
|
+
|
19
|
+
should "only support queries against its model's table" do
|
20
|
+
assert !@parser.parse('SELECT * FROM `apples` WHERE (`users`.`id` = 2) ')
|
21
|
+
end
|
22
|
+
|
23
|
+
should "support cachable queries" do
|
24
|
+
assert @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`id` = 2) ')
|
25
|
+
assert @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`id` = 2) LIMIT 1')
|
26
|
+
end
|
27
|
+
|
28
|
+
should "support vaguely formatted queries" do
|
29
|
+
assert @parser.parse('SELECT * FROM "posts" WHERE (color = red AND size = big)')
|
30
|
+
end
|
31
|
+
|
32
|
+
context "extract options" do
|
33
|
+
|
34
|
+
should "provide the limit" do
|
35
|
+
sql = 'SELECT * FROM `posts` WHERE (`posts`.`id` = 2)'
|
36
|
+
assert_equal nil, @parser.parse(sql)[:limit]
|
37
|
+
|
38
|
+
sql << ' LIMIT 1'
|
39
|
+
assert_equal 1, @parser.parse(sql)[:limit]
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
context "unsupported queries" do
|
45
|
+
|
46
|
+
should "include advanced limits" do
|
47
|
+
assert !@parser.parse('SELECT * FROM `posts` WHERE (color = red AND size = big) LIMIT 2')
|
48
|
+
end
|
49
|
+
|
50
|
+
should "include joins" do
|
51
|
+
assert !@parser.parse('SELECT * FROM `posts`, `trees` JOIN ON apple.tree_id = tree.id WHERE (color = red)')
|
52
|
+
end
|
53
|
+
|
54
|
+
should "include specific selects" do
|
55
|
+
assert !@parser.parse('SELECT id FROM `posts` WHERE (color = red)')
|
56
|
+
end
|
57
|
+
|
58
|
+
should "include offset" do
|
59
|
+
assert !@parser.parse('SELECT * FROM `posts` WHERE (color = red) LIMIT 1 OFFSET 2')
|
60
|
+
end
|
61
|
+
|
62
|
+
should "include order" do
|
63
|
+
assert !@parser.parse('SELECT * FROM `posts` WHERE (color = red) ORDER DESC')
|
64
|
+
end
|
65
|
+
|
66
|
+
should "include the OR operator" do
|
67
|
+
assert !@parser.parse('SELECT * FROM `posts` WHERE (color = red OR size = big) LIMIT 2')
|
68
|
+
end
|
69
|
+
|
70
|
+
should "include the IN operator" do
|
71
|
+
assert !@parser.parse('SELECT * FROM `posts` WHERE (id IN (1,2,3))')
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
|
76
|
+
context "key generation" do
|
77
|
+
should "include the table name and version" do
|
78
|
+
assert_match(/^kasket\/posts\/version=3558\//, @parser.parse('SELECT * FROM `posts` WHERE (id = 1)')[:key])
|
79
|
+
end
|
80
|
+
|
81
|
+
should "include all indexed attributes" do
|
82
|
+
assert_match(/id=1$/, @parser.parse('SELECT * FROM `posts` WHERE (id = 1)')[:key])
|
83
|
+
assert_match(/blog_id=2\/id=1$/, @parser.parse('SELECT * FROM `posts` WHERE (id = 1 AND blog_id = 2)')[:key])
|
84
|
+
assert_match(/id=1\/title='world\\'s best title'$/, @parser.parse("SELECT * FROM `posts` WHERE (id = 1 AND title = 'world\\'s best title')")[:key])
|
85
|
+
end
|
86
|
+
|
87
|
+
context "when limit 1" do
|
88
|
+
should "add /first to the key if the index does not include id" do
|
89
|
+
assert_match(/title='a'\/first$/, @parser.parse("SELECT * FROM `posts` WHERE (title = 'a') LIMIT 1")[:key])
|
90
|
+
end
|
91
|
+
should "not add /first to the key when the index includes id" do
|
92
|
+
assert_match(/id=1$/, @parser.parse("SELECT * FROM `posts` WHERE (id = 1) LIMIT 1")[:key])
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/helper'
|
2
|
+
|
3
|
+
class ReadMixinTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
context "find by sql with kasket" do
|
6
|
+
setup do
|
7
|
+
@database_results = [ { 'id' => 1, 'title' => 'Hello' }, { 'id' => 2, 'title' => 'World' } ]
|
8
|
+
@records = @database_results.map { |r| Post.send(:instantiate, r) }
|
9
|
+
Post.stubs(:find_by_sql_without_kasket).returns(@records)
|
10
|
+
end
|
11
|
+
|
12
|
+
should "handle unsupported sql" do
|
13
|
+
assert_equal @records, Post.find_by_sql_with_kasket('select unsupported sql statement')
|
14
|
+
assert Kasket.cache.local.empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
should "read results" do
|
18
|
+
Kasket.cache.write('kasket/posts/version=3558/id=1', @database_results.first)
|
19
|
+
assert_equal [ @records.first ], Post.find_by_sql('SELECT * FROM `posts` WHERE (id = 1)'), Kasket.cache.inspect
|
20
|
+
end
|
21
|
+
|
22
|
+
should "store results in kasket" do
|
23
|
+
Post.find_by_sql('SELECT * FROM `posts` WHERE (id = 1)')
|
24
|
+
|
25
|
+
assert_equal @database_results.first, Kasket.cache.read('kasket/posts/version=3558/id=1'), Kasket.cache.inspect
|
26
|
+
end
|
27
|
+
|
28
|
+
context "modifying results" do
|
29
|
+
setup do
|
30
|
+
Kasket.cache.write('kasket/posts/version=3558/id=1', @database_results.first)
|
31
|
+
@record = Post.find_by_sql('SELECT * FROM `posts` WHERE (id = 1)').first
|
32
|
+
@record.instance_variable_get(:@attributes)['id'] = 3
|
33
|
+
end
|
34
|
+
|
35
|
+
should "not impact other queries" do
|
36
|
+
same_record = Post.find_by_sql('SELECT * FROM `posts` WHERE (id = 1)').first
|
37
|
+
|
38
|
+
assert_not_equal @record, same_record
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
data/test/schema.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# This file is auto-generated from the current state of the database. Instead of editing this file,
|
1
|
+
# This file is auto-generated from the current state of the database. Instead of editing this file,
|
2
2
|
# please use the migrations feature of Active Record to incrementally modify your database, and
|
3
3
|
# then regenerate this schema definition.
|
4
4
|
#
|
metadata
CHANGED
@@ -1,15 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kasket
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Mick Staugaard
|
8
|
+
- Eric Chapweske
|
8
9
|
autorequire:
|
9
10
|
bindir: bin
|
10
11
|
cert_chain: []
|
11
12
|
|
12
|
-
date: 2009-12-
|
13
|
+
date: 2009-12-05 00:00:00 -08:00
|
13
14
|
default_executable:
|
14
15
|
dependencies:
|
15
16
|
- !ruby/object:Gem::Dependency
|
@@ -52,7 +53,7 @@ dependencies:
|
|
52
53
|
- !ruby/object:Gem::Version
|
53
54
|
version: "0"
|
54
55
|
version:
|
55
|
-
description:
|
56
|
+
description: put's a cap on your queries
|
56
57
|
email: mick@staugaard.com
|
57
58
|
executables: []
|
58
59
|
|
@@ -71,14 +72,15 @@ files:
|
|
71
72
|
- lib/kasket.rb
|
72
73
|
- lib/kasket/active_record_patches.rb
|
73
74
|
- lib/kasket/cache.rb
|
74
|
-
- lib/kasket/conditions_parser.rb
|
75
75
|
- lib/kasket/configuration_mixin.rb
|
76
76
|
- lib/kasket/dirty_mixin.rb
|
77
|
+
- lib/kasket/query_parser.rb
|
77
78
|
- lib/kasket/rack_middleware.rb
|
78
79
|
- lib/kasket/read_mixin.rb
|
79
80
|
- lib/kasket/reload_association_mixin.rb
|
80
81
|
- lib/kasket/write_mixin.rb
|
81
82
|
- test/cache_expiry_test.rb
|
83
|
+
- test/cache_test.rb
|
82
84
|
- test/database.yml
|
83
85
|
- test/dirty_test.rb
|
84
86
|
- test/find_one_test.rb
|
@@ -87,8 +89,9 @@ files:
|
|
87
89
|
- test/fixtures/comments.yml
|
88
90
|
- test/fixtures/posts.yml
|
89
91
|
- test/helper.rb
|
92
|
+
- test/parser_test.rb
|
93
|
+
- test/read_mixin_test.rb
|
90
94
|
- test/schema.rb
|
91
|
-
- test/serialization_test.rb
|
92
95
|
has_rdoc: true
|
93
96
|
homepage: http://github.com/staugaard/kasket
|
94
97
|
licenses: []
|
@@ -119,9 +122,11 @@ specification_version: 3
|
|
119
122
|
summary: A write back caching layer on active record
|
120
123
|
test_files:
|
121
124
|
- test/cache_expiry_test.rb
|
125
|
+
- test/cache_test.rb
|
122
126
|
- test/dirty_test.rb
|
123
127
|
- test/find_one_test.rb
|
124
128
|
- test/find_some_test.rb
|
125
129
|
- test/helper.rb
|
130
|
+
- test/parser_test.rb
|
131
|
+
- test/read_mixin_test.rb
|
126
132
|
- test/schema.rb
|
127
|
-
- test/serialization_test.rb
|
@@ -1,79 +0,0 @@
|
|
1
|
-
module Kasket
|
2
|
-
class ConditionsParser
|
3
|
-
def initialize(model_class)
|
4
|
-
@model_class = model_class
|
5
|
-
end
|
6
|
-
|
7
|
-
def attribute_value_pairs(options)
|
8
|
-
#pulls out the conditions from each hash
|
9
|
-
condition_fragments = [options[:conditions]]
|
10
|
-
|
11
|
-
#add the scope to the mix
|
12
|
-
if scope = @model_class.send(:scope, :find)
|
13
|
-
condition_fragments << scope[:conditions]
|
14
|
-
end
|
15
|
-
|
16
|
-
#add the type if we are on STI
|
17
|
-
condition_fragments << {:type => @model_class.name} if @model_class.finder_needs_type_condition?
|
18
|
-
|
19
|
-
condition_fragments.compact!
|
20
|
-
|
21
|
-
#parses each conditions fragment but bails if one of the did not parse
|
22
|
-
attributes_fragments = condition_fragments.map do |condition_fragment|
|
23
|
-
attributes_fragment = attributes_for_conditions(condition_fragment)
|
24
|
-
return nil unless attributes_fragment
|
25
|
-
attributes_fragment
|
26
|
-
end
|
27
|
-
|
28
|
-
#merges the hashes but bails if there is an overlap
|
29
|
-
attributes = attributes_fragments.inject({}) do |memo, attributes_fragment|
|
30
|
-
attributes_fragment.each do |attribute, value|
|
31
|
-
return nil if memo.has_key?(attribute)
|
32
|
-
memo[attribute] = value
|
33
|
-
end
|
34
|
-
memo
|
35
|
-
end
|
36
|
-
|
37
|
-
attributes.keys.sort.map { |attribute| [attribute.to_sym, attributes[attribute]] }
|
38
|
-
end
|
39
|
-
|
40
|
-
def attributes_for_conditions(conditions)
|
41
|
-
pairs = case conditions
|
42
|
-
when Hash
|
43
|
-
return conditions.stringify_keys
|
44
|
-
when String
|
45
|
-
parse_indices_from_condition(conditions)
|
46
|
-
when Array
|
47
|
-
parse_indices_from_condition(*conditions)
|
48
|
-
when NilClass
|
49
|
-
[]
|
50
|
-
end
|
51
|
-
|
52
|
-
return nil unless pairs
|
53
|
-
|
54
|
-
pairs.inject({}) do |memo, pair|
|
55
|
-
return nil if memo.has_key?(pair[0])
|
56
|
-
memo[pair[0]] = pair[1]
|
57
|
-
memo
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
AND = /\s+AND\s+/i
|
62
|
-
TABLE_AND_COLUMN = /(?:(?:`|")?(\w+)(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
|
63
|
-
VALUE = /'?(\d+|\?|(?:(?:[^']|'')*))'?/ # Matches: 123, ?, '123', '12''3'
|
64
|
-
KEY_EQ_VALUE = /^[\(\s]*#{TABLE_AND_COLUMN}\s+=\s+#{VALUE}[\)\s]*$/ # Matches: KEY = VALUE, (KEY = VALUE), ()(KEY = VALUE))
|
65
|
-
|
66
|
-
def parse_indices_from_condition(conditions = '', *values)
|
67
|
-
values = values.dup
|
68
|
-
conditions.split(AND).inject([]) do |indices, condition|
|
69
|
-
matched, table_name, column_name, sql_value = *(KEY_EQ_VALUE.match(condition))
|
70
|
-
if matched
|
71
|
-
value = sql_value == '?' ? values.shift : @model_class.columns_hash[column_name].type_cast(sql_value)
|
72
|
-
indices << [column_name, value]
|
73
|
-
else
|
74
|
-
return nil
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
data/test/serialization_test.rb
DELETED
@@ -1,21 +0,0 @@
|
|
1
|
-
require File.dirname(__FILE__) + '/helper'
|
2
|
-
|
3
|
-
class SerializationTest < ActiveSupport::TestCase
|
4
|
-
fixtures :blogs, :posts
|
5
|
-
|
6
|
-
Post.has_kasket
|
7
|
-
|
8
|
-
should "store a CachedModel" do
|
9
|
-
post = Post.first
|
10
|
-
post.store_in_kasket
|
11
|
-
assert_instance_of(Kasket::Cache::CachedModel, Rails.cache.read(post.kasket_key))
|
12
|
-
end
|
13
|
-
|
14
|
-
should "bring convert CachedModel to model instances" do
|
15
|
-
post = Post.first
|
16
|
-
post.store_in_kasket
|
17
|
-
|
18
|
-
post = Kasket.cache.read(post.kasket_key)
|
19
|
-
assert_instance_of(Post, post)
|
20
|
-
end
|
21
|
-
end
|