kasket 1.0.4 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +1 -1
- data/lib/kasket.rb +21 -6
- data/lib/kasket/configuration_mixin.rb +1 -5
- data/lib/kasket/query_parser.rb +15 -4
- data/lib/kasket/read_mixin.rb +28 -23
- data/lib/kasket/relation_mixin.rb +9 -0
- data/lib/kasket/reload_association_mixin.rb +3 -3
- data/lib/kasket/select_manager_mixin.rb +26 -0
- data/lib/kasket/version.rb +5 -3
- data/lib/kasket/visitor.rb +140 -0
- data/test/configuration_mixin_test.rb +2 -2
- data/test/database.yml +1 -3
- data/test/fixtures/authors.yml +5 -0
- data/test/fixtures/comments.yml +5 -3
- data/test/fixtures/posts.yml +9 -1
- data/test/helper.rb +15 -4
- data/test/parser_test.rb +64 -35
- data/test/read_mixin_test.rb +14 -7
- data/test/reload_test.rb +85 -0
- data/test/schema.rb +30 -0
- data/test/test_models.rb +20 -24
- data/test/visitor_test.rb +17 -0
- metadata +41 -22
- data/lib/kasket/active_record_patches.rb +0 -57
data/README.rdoc
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
= Kasket {<img src="https://secure.travis-ci.org/staugaard/kasket.png" />}[http://travis-ci.org/staugaard/kasket]
|
2
2
|
=== Puts a cap on your queries
|
3
|
-
A caching layer for ActiveRecord
|
3
|
+
A caching layer for ActiveRecord (2.3.x and 3.1.x)
|
4
4
|
|
5
5
|
Developed and used on http://zendesk.com.
|
6
6
|
|
data/lib/kasket.rb
CHANGED
@@ -2,26 +2,42 @@
|
|
2
2
|
require 'active_record'
|
3
3
|
require 'active_support'
|
4
4
|
|
5
|
-
require 'kasket/active_record_patches'
|
6
5
|
require 'kasket/version'
|
7
6
|
|
8
7
|
module Kasket
|
9
|
-
autoload :
|
8
|
+
autoload :ReadMixin, 'kasket/read_mixin'
|
9
|
+
autoload :WriteMixin, 'kasket/write_mixin'
|
10
|
+
autoload :DirtyMixin, 'kasket/dirty_mixin'
|
11
|
+
autoload :QueryParser, 'kasket/query_parser'
|
12
|
+
autoload :ConfigurationMixin, 'kasket/configuration_mixin'
|
10
13
|
autoload :ReloadAssociationMixin, 'kasket/reload_association_mixin'
|
11
|
-
autoload :Query,
|
14
|
+
autoload :Query, 'kasket/query'
|
15
|
+
autoload :Visitor, 'kasket/visitor'
|
16
|
+
autoload :SelectManagerMixin, 'kasket/select_manager_mixin'
|
17
|
+
autoload :RelationMixin, 'kasket/relation_mixin'
|
18
|
+
|
19
|
+
AR30 = (ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 0)
|
12
20
|
|
13
21
|
CONFIGURATION = {:max_collection_size => 100}
|
14
22
|
|
15
23
|
module_function
|
16
24
|
|
17
25
|
def setup(options = {})
|
18
|
-
return if ActiveRecord::Base.
|
26
|
+
return if ActiveRecord::Base.respond_to?(:has_kasket)
|
19
27
|
|
20
28
|
CONFIGURATION[:max_collection_size] = options[:max_collection_size] if options[:max_collection_size]
|
21
29
|
|
22
30
|
ActiveRecord::Base.extend(Kasket::ConfigurationMixin)
|
31
|
+
|
32
|
+
if defined?(ActiveRecord::Relation)
|
33
|
+
ActiveRecord::Relation.send(:include, Kasket::RelationMixin)
|
34
|
+
Arel::SelectManager.send(:include, Kasket::SelectManagerMixin)
|
35
|
+
end
|
36
|
+
|
23
37
|
ActiveRecord::Associations::BelongsToAssociation.send(:include, Kasket::ReloadAssociationMixin)
|
24
|
-
ActiveRecord::
|
38
|
+
if ActiveRecord::VERSION::MAJOR == 2 || AR30
|
39
|
+
ActiveRecord::Associations::BelongsToPolymorphicAssociation.send(:include, Kasket::ReloadAssociationMixin)
|
40
|
+
end
|
25
41
|
ActiveRecord::Associations::HasOneThroughAssociation.send(:include, Kasket::ReloadAssociationMixin)
|
26
42
|
end
|
27
43
|
|
@@ -39,4 +55,3 @@ module Kasket
|
|
39
55
|
end
|
40
56
|
end
|
41
57
|
end
|
42
|
-
|
@@ -3,10 +3,6 @@ require 'active_support'
|
|
3
3
|
require "digest/md5"
|
4
4
|
|
5
5
|
module Kasket
|
6
|
-
autoload :ReadMixin, 'kasket/read_mixin'
|
7
|
-
autoload :WriteMixin, 'kasket/write_mixin'
|
8
|
-
autoload :DirtyMixin, 'kasket/dirty_mixin'
|
9
|
-
autoload :QueryParser, 'kasket/query_parser'
|
10
6
|
|
11
7
|
module ConfigurationMixin
|
12
8
|
|
@@ -27,7 +23,7 @@ module Kasket
|
|
27
23
|
end
|
28
24
|
|
29
25
|
def kasket_key_prefix
|
30
|
-
@kasket_key_prefix ||= "kasket-#{Kasket::Version::
|
26
|
+
@kasket_key_prefix ||= "kasket-#{Kasket::Version::PROTOCOL}/#{table_name}/version=#{column_names.join.sum}/"
|
31
27
|
end
|
32
28
|
|
33
29
|
def kasket_key_for(attribute_value_pairs)
|
data/lib/kasket/query_parser.rb
CHANGED
@@ -11,15 +11,26 @@ module Kasket
|
|
11
11
|
|
12
12
|
def initialize(model_class)
|
13
13
|
@model_class = model_class
|
14
|
-
@supported_query_pattern =
|
15
|
-
|
14
|
+
@supported_query_pattern = if AR30
|
15
|
+
/^select\s+(?:`#{@model_class.table_name}`.)?\* from (?:`|")#{@model_class.table_name}(?:`|") where (.*?)\s*$/i
|
16
|
+
else
|
17
|
+
/^select \* from (?:`|")#{@model_class.table_name}(?:`|") where \((.*)\)(|\s+limit 1)\s*$/i
|
18
|
+
end
|
19
|
+
@table_and_column_pattern = /(?:(?:`|")?#{@model_class.table_name}(?:`|")?\.)?(?:`|")?([a-zA-Z]\w*)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
|
16
20
|
@key_eq_value_pattern = /^[\(\s]*#{@table_and_column_pattern}\s+(=|IN)\s+#{VALUE}[\)\s]*$/ # Matches: KEY = VALUE, (KEY = VALUE), ()(KEY = VALUE))
|
17
21
|
end
|
18
22
|
|
19
23
|
def parse(sql)
|
20
24
|
if match = @supported_query_pattern.match(sql)
|
25
|
+
where, limit = match[1], match[2]
|
26
|
+
if AR30 && where =~ /limit \d+\s*$/i
|
27
|
+
# limit is harder to find in rails 3.0 since where does not use surrounding braces
|
28
|
+
return unless where =~ /(.*?)(\s+limit 1)\s*$/i
|
29
|
+
where, limit = $1, $2
|
30
|
+
end
|
31
|
+
|
21
32
|
query = Hash.new
|
22
|
-
query[:attributes] = sorted_attribute_value_pairs(
|
33
|
+
query[:attributes] = sorted_attribute_value_pairs(where)
|
23
34
|
return nil if query[:attributes].nil?
|
24
35
|
|
25
36
|
if query[:attributes].size > 1 && query[:attributes].map(&:last).any? {|a| a.is_a?(Array)}
|
@@ -28,7 +39,7 @@ module Kasket
|
|
28
39
|
end
|
29
40
|
|
30
41
|
query[:index] = query[:attributes].map(&:first)
|
31
|
-
query[:limit] =
|
42
|
+
query[:limit] = limit.blank? ? nil : 1
|
32
43
|
query[:key] = @model_class.kasket_key_for(query[:attributes])
|
33
44
|
query[:key] << '/first' if query[:limit] == 1 && !query[:index].include?(:id)
|
34
45
|
query
|
data/lib/kasket/read_mixin.rb
CHANGED
@@ -8,8 +8,17 @@ module Kasket
|
|
8
8
|
end
|
9
9
|
end
|
10
10
|
|
11
|
-
def find_by_sql_with_kasket(
|
12
|
-
|
11
|
+
def find_by_sql_with_kasket(*args)
|
12
|
+
sql = args[0]
|
13
|
+
|
14
|
+
if use_kasket?
|
15
|
+
if sql.respond_to?(:to_kasket_query)
|
16
|
+
query = sql.to_kasket_query(self, args[1])
|
17
|
+
else
|
18
|
+
query = kasket_parser.parse(sanitize_sql(sql))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
13
22
|
if query && has_kasket_index_on?(query[:index])
|
14
23
|
if query[:key].is_a?(Array)
|
15
24
|
find_by_sql_with_kasket_on_id_array(query[:key])
|
@@ -21,40 +30,36 @@ module Kasket
|
|
21
30
|
Array.wrap(value).collect { |record| instantiate(record.dup) }
|
22
31
|
end
|
23
32
|
else
|
24
|
-
store_in_kasket(query[:key], find_by_sql_without_kasket(
|
33
|
+
store_in_kasket(query[:key], find_by_sql_without_kasket(*args))
|
25
34
|
end
|
26
35
|
end
|
27
36
|
else
|
28
|
-
find_by_sql_without_kasket(
|
37
|
+
find_by_sql_without_kasket(*args)
|
29
38
|
end
|
30
39
|
end
|
31
40
|
|
32
41
|
def find_by_sql_with_kasket_on_id_array(keys)
|
33
|
-
|
34
|
-
missing_ids = []
|
35
|
-
|
36
|
-
keys.each do |key|
|
37
|
-
if value = key_value_map[key]
|
38
|
-
key_value_map[key] = instantiate(value.dup)
|
39
|
-
else
|
40
|
-
missing_ids << key.split('=').last.to_i
|
41
|
-
end
|
42
|
-
end
|
42
|
+
key_attributes_map = Kasket.cache.read_multi(*keys)
|
43
43
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
instance.store_in_kasket
|
48
|
-
key_value_map[instance.kasket_key] = instance
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
44
|
+
found_keys, missing_keys = keys.partition{|k| key_attributes_map[k] }
|
45
|
+
found_keys.each{|k| key_attributes_map[k] = instantiate(key_attributes_map[k].dup) }
|
46
|
+
key_attributes_map.merge!(missing_records_from_db(missing_keys))
|
52
47
|
|
53
|
-
|
48
|
+
key_attributes_map.values.compact
|
54
49
|
end
|
55
50
|
|
56
51
|
protected
|
57
52
|
|
53
|
+
def missing_records_from_db(missing_keys)
|
54
|
+
return {} if missing_keys.empty?
|
55
|
+
|
56
|
+
id_key_map = Hash[missing_keys.map{|key| [key.split('=').last.to_i, key] }]
|
57
|
+
|
58
|
+
found = without_kasket { find_all_by_id(id_key_map.keys) }
|
59
|
+
found.each(&:store_in_kasket)
|
60
|
+
Hash[found.map{|record| [id_key_map[record.id], record] }]
|
61
|
+
end
|
62
|
+
|
58
63
|
def store_in_kasket(key, records)
|
59
64
|
if records.size == 1
|
60
65
|
if records.first.kasket_cacheable?
|
@@ -1,13 +1,13 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
module Kasket
|
3
3
|
module ReloadAssociationMixin
|
4
|
-
# TODO write tests for this
|
5
4
|
def reload_with_kasket_clearing(*args)
|
6
5
|
if loaded?
|
7
6
|
Kasket.clear_local if target.class.include?(WriteMixin)
|
8
7
|
else
|
9
|
-
|
10
|
-
|
8
|
+
refl = respond_to?(:reflection) ? reflection : proxy_reflection
|
9
|
+
target_class = (refl.options[:polymorphic] ? (respond_to?(:klass) ? klass : association_class) : refl.klass)
|
10
|
+
Kasket.clear_local if target_class && target_class.include?(WriteMixin)
|
11
11
|
end
|
12
12
|
|
13
13
|
reload_without_kasket_clearing(*args)
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Kasket
|
2
|
+
module SelectManagerMixin
|
3
|
+
def to_kasket_query(klass, binds = [])
|
4
|
+
query = Kasket::Visitor.new(klass, binds).accept(ast)
|
5
|
+
|
6
|
+
return nil if query.nil? || query == :unsupported
|
7
|
+
return nil if query[:attributes].blank?
|
8
|
+
|
9
|
+
query[:index] = query[:attributes].map(&:first)
|
10
|
+
|
11
|
+
if query[:limit]
|
12
|
+
return nil if query[:limit] > 1
|
13
|
+
# return nil if !query[:index].include?(:id)
|
14
|
+
end
|
15
|
+
|
16
|
+
if query[:index].size > 1 && query[:attributes].any? { |attribute, value| value.is_a?(Array) }
|
17
|
+
return nil
|
18
|
+
end
|
19
|
+
|
20
|
+
query[:key] = klass.kasket_key_for(query[:attributes])
|
21
|
+
query[:key] << '/first' if query[:limit] == 1 && query[:index] != [:id]
|
22
|
+
|
23
|
+
query
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/kasket/version.rb
CHANGED
@@ -0,0 +1,140 @@
|
|
1
|
+
require 'arel'
|
2
|
+
|
3
|
+
module Kasket
|
4
|
+
class Visitor < Arel::Visitors::Visitor
|
5
|
+
def initialize(model_class, binds)
|
6
|
+
@model_class = model_class
|
7
|
+
@binds = binds.dup
|
8
|
+
end
|
9
|
+
|
10
|
+
def accept(node)
|
11
|
+
self.last_column = nil
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def last_column=(col)
|
16
|
+
Thread.current[:arel_visitors_to_sql_last_column] = col
|
17
|
+
end
|
18
|
+
|
19
|
+
def last_column
|
20
|
+
Thread.current[:arel_visitors_to_sql_last_column]
|
21
|
+
end
|
22
|
+
|
23
|
+
def column_for(name)
|
24
|
+
@model_class.columns_hash[name.to_s]
|
25
|
+
end
|
26
|
+
|
27
|
+
def visit_Arel_Nodes_SelectStatement(node)
|
28
|
+
return :unsupported if !AR30 && node.with
|
29
|
+
return :unsupported if node.offset
|
30
|
+
return :unsupported if node.lock
|
31
|
+
return :unsupported if node.orders.any?
|
32
|
+
return :unsupported if node.cores.size != 1
|
33
|
+
|
34
|
+
query = visit_Arel_Nodes_SelectCore(node.cores[0])
|
35
|
+
return query if query == :unsupported
|
36
|
+
|
37
|
+
query = query.inject({}) do |memo, item|
|
38
|
+
memo.merge(item)
|
39
|
+
end
|
40
|
+
|
41
|
+
query.merge!(visit(node.limit)) if node.limit
|
42
|
+
query
|
43
|
+
end
|
44
|
+
|
45
|
+
def visit_Arel_Nodes_SelectCore(node)
|
46
|
+
return :unsupported if node.groups.any?
|
47
|
+
return :unsupported if node.having
|
48
|
+
return :unsupported if !AR30 && node.set_quantifier
|
49
|
+
return :unsupported if !AR30 && (!node.source || node.source.empty?)
|
50
|
+
return :unsupported if node.projections.size != 1
|
51
|
+
|
52
|
+
select = node.projections[0]
|
53
|
+
select = select.name if select.respond_to?(:name)
|
54
|
+
return :unsupported if select != '*'
|
55
|
+
|
56
|
+
parts = [visit(node.source)]
|
57
|
+
|
58
|
+
parts += node.wheres.map {|where| visit(where) }
|
59
|
+
|
60
|
+
parts.include?(:unsupported) ? :unsupported : parts
|
61
|
+
end
|
62
|
+
|
63
|
+
def visit_Arel_Nodes_Limit(node)
|
64
|
+
{:limit => node.value.to_i}
|
65
|
+
end
|
66
|
+
|
67
|
+
def visit_Arel_Nodes_JoinSource(node)
|
68
|
+
return :unsupported if !node.left || node.right.any?
|
69
|
+
return :unsupported if !node.left.is_a?(Arel::Table)
|
70
|
+
visit(node.left)
|
71
|
+
end
|
72
|
+
|
73
|
+
def visit_Arel_Table(node)
|
74
|
+
{:from => node.name}
|
75
|
+
end
|
76
|
+
|
77
|
+
def visit_Arel_Nodes_And(node)
|
78
|
+
attributes = node.children.map { |child| visit(child) }
|
79
|
+
return :unsupported if attributes.include?(:unsupported)
|
80
|
+
attributes.sort! { |pair1, pair2| pair1[0].to_s <=> pair2[0].to_s }
|
81
|
+
{ :attributes => attributes }
|
82
|
+
end
|
83
|
+
|
84
|
+
def visit_Arel_Nodes_In(node)
|
85
|
+
left = visit(node.left)
|
86
|
+
return :unsupported if left != :id
|
87
|
+
|
88
|
+
[left, visit(node.right)]
|
89
|
+
end
|
90
|
+
|
91
|
+
def visit_Arel_Nodes_Equality(node)
|
92
|
+
right = node.right
|
93
|
+
[visit(node.left), right ? visit(right) : nil]
|
94
|
+
end
|
95
|
+
|
96
|
+
def visit_Arel_Attributes_Attribute(node)
|
97
|
+
self.last_column = column_for(node.name)
|
98
|
+
node.name.to_sym
|
99
|
+
end
|
100
|
+
|
101
|
+
def literal(node)
|
102
|
+
if node == '?'
|
103
|
+
column, value = @binds.shift
|
104
|
+
value.to_s
|
105
|
+
else
|
106
|
+
node.to_s
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# only gets used on 1.8.7
|
111
|
+
def visit_Arel_Nodes_BindParam(x)
|
112
|
+
@binds.shift[1]
|
113
|
+
end
|
114
|
+
|
115
|
+
def visit_Array(node)
|
116
|
+
node.map {|value| quoted(value) }
|
117
|
+
end
|
118
|
+
|
119
|
+
#TODO: We are actually not using this?
|
120
|
+
def quoted(node)
|
121
|
+
@model_class.connection.quote(node, self.last_column)
|
122
|
+
end
|
123
|
+
|
124
|
+
alias :visit_String :literal
|
125
|
+
alias :visit_Fixnum :literal
|
126
|
+
alias :visit_TrueClass :literal
|
127
|
+
alias :visit_FalseClass :literal
|
128
|
+
alias :visit_Arel_Nodes_SqlLiteral :literal
|
129
|
+
|
130
|
+
def method_missing(name, *args, &block)
|
131
|
+
return :unsupported if name.to_s.start_with?('visit_')
|
132
|
+
super
|
133
|
+
end
|
134
|
+
|
135
|
+
def respond_to?(name, include_private = false)
|
136
|
+
return super || name.to_s.start_with?('visit_')
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
end
|
@@ -6,7 +6,7 @@ class ConfigurationMixinTest < ActiveSupport::TestCase
|
|
6
6
|
context "Generating cache keys" do
|
7
7
|
|
8
8
|
should "not choke on empty numeric attributes" do
|
9
|
-
expected_cache_key = "kasket-#{Kasket::Version::
|
9
|
+
expected_cache_key = "kasket-#{Kasket::Version::PROTOCOL}/posts/version=#{POST_VERSION}/blog_id=null"
|
10
10
|
query_attributes = [ [:blog_id, ''] ]
|
11
11
|
|
12
12
|
assert_equal expected_cache_key, Post.kasket_key_for(query_attributes)
|
@@ -27,7 +27,7 @@ class ConfigurationMixinTest < ActiveSupport::TestCase
|
|
27
27
|
|
28
28
|
should "downcase string attributes" do
|
29
29
|
query_attributes = [ [:title, 'ThIs'] ]
|
30
|
-
expected_cache_key = "kasket-#{Kasket::Version::
|
30
|
+
expected_cache_key = "kasket-#{Kasket::Version::PROTOCOL}/posts/version=#{POST_VERSION}/title='this'"
|
31
31
|
|
32
32
|
assert_equal expected_cache_key, Post.kasket_key_for(query_attributes)
|
33
33
|
end
|
data/test/database.yml
CHANGED
data/test/fixtures/comments.yml
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
few_comments_1:
|
2
|
-
|
2
|
+
id: 1
|
3
|
+
post_id: 2
|
3
4
|
body: what ever body 1
|
4
5
|
|
5
6
|
few_comments_2:
|
6
|
-
|
7
|
+
id: 2
|
8
|
+
post_id: 2
|
7
9
|
body: what ever body 2
|
8
10
|
|
9
|
-
<% (1..
|
11
|
+
<% (1..10).each do |i| %>
|
10
12
|
many_comments_<%= i %>:
|
11
13
|
post: has_many_comments
|
12
14
|
body: what ever body <%= i %>
|
data/test/fixtures/posts.yml
CHANGED
@@ -1,15 +1,23 @@
|
|
1
1
|
no_comments:
|
2
|
+
id: 1
|
2
3
|
blog: a_blog
|
3
4
|
title: no_comments
|
5
|
+
author: mick
|
4
6
|
|
5
7
|
has_two_comments:
|
8
|
+
id: 2
|
6
9
|
blog: a_blog
|
7
10
|
title: few_comments
|
11
|
+
author: mick
|
8
12
|
|
9
13
|
on_other_blog:
|
14
|
+
id: 3
|
10
15
|
blog: other_blog
|
11
16
|
title: no_comments
|
17
|
+
author: eric
|
12
18
|
|
13
19
|
has_many_comments:
|
20
|
+
id: 4
|
14
21
|
blog: a_blog
|
15
|
-
title: many_comments
|
22
|
+
title: many_comments
|
23
|
+
author: eric
|
data/test/helper.rb
CHANGED
@@ -10,6 +10,7 @@ if defined?(Debugger)
|
|
10
10
|
end
|
11
11
|
|
12
12
|
require 'test/unit'
|
13
|
+
require 'active_record'
|
13
14
|
require 'active_record/fixtures'
|
14
15
|
|
15
16
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
@@ -18,13 +19,9 @@ require 'kasket'
|
|
18
19
|
|
19
20
|
Kasket.setup
|
20
21
|
|
21
|
-
require 'shoulda'
|
22
|
-
|
23
22
|
class ActiveSupport::TestCase
|
24
23
|
include ActiveRecord::TestFixtures
|
25
24
|
|
26
|
-
fixtures :all
|
27
|
-
|
28
25
|
def create_fixtures(*table_names)
|
29
26
|
if block_given?
|
30
27
|
Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
|
@@ -41,11 +38,23 @@ class ActiveSupport::TestCase
|
|
41
38
|
def clear_cache
|
42
39
|
Kasket.cache.clear
|
43
40
|
end
|
41
|
+
|
42
|
+
def arel?
|
43
|
+
self.class.arel?
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.arel?
|
47
|
+
ActiveRecord::VERSION::MAJOR >= 3 && ActiveRecord::VERSION::MINOR >= 1
|
48
|
+
end
|
44
49
|
end
|
45
50
|
|
46
51
|
ActiveSupport::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
|
47
52
|
$LOAD_PATH.unshift(ActiveSupport::TestCase.fixture_path)
|
48
53
|
|
54
|
+
class ActiveSupport::TestCase
|
55
|
+
fixtures :all
|
56
|
+
end
|
57
|
+
|
49
58
|
module Rails
|
50
59
|
module_function
|
51
60
|
CACHE = ActiveSupport::Cache::MemoryStore.new
|
@@ -61,3 +70,5 @@ module Rails
|
|
61
70
|
end
|
62
71
|
|
63
72
|
require 'test_models'
|
73
|
+
POST_VERSION = Post.column_names.join.sum
|
74
|
+
COMMENT_VERSION = Comment.column_names.join.sum
|
data/test/parser_test.rb
CHANGED
@@ -2,6 +2,24 @@ require File.expand_path("helper", File.dirname(__FILE__))
|
|
2
2
|
require 'kasket/query_parser'
|
3
3
|
|
4
4
|
class ParserTest < ActiveSupport::TestCase
|
5
|
+
def parse(options)
|
6
|
+
scope = Post
|
7
|
+
if arel?
|
8
|
+
options.each do |k,v|
|
9
|
+
scope = case k
|
10
|
+
when :conditions then scope.where(v)
|
11
|
+
else
|
12
|
+
scope.send(k, v)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
scope.to_kasket_query
|
16
|
+
elsif ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 0
|
17
|
+
@parser.parse(scope.scoped(options).to_sql)
|
18
|
+
else
|
19
|
+
sql = scope.send(:construct_finder_sql, options)
|
20
|
+
@parser.parse(sql)
|
21
|
+
end
|
22
|
+
end
|
5
23
|
|
6
24
|
context "Parsing" do
|
7
25
|
setup do
|
@@ -9,40 +27,50 @@ class ParserTest < ActiveSupport::TestCase
|
|
9
27
|
end
|
10
28
|
|
11
29
|
should "not support conditions with number as column (e.g. 0 = 1)" do
|
12
|
-
|
13
|
-
|
30
|
+
assert !parse(:conditions => "0 = 1")
|
31
|
+
end
|
32
|
+
|
33
|
+
should "not support conditions with number as column and parans (e.g. 0 = 1)" do
|
34
|
+
assert !parse(:conditions => "(0 = 1)")
|
14
35
|
end
|
15
36
|
|
16
37
|
should 'not support IN queries in combination with other conditions' do
|
17
|
-
|
18
|
-
assert(!parsed_query)
|
38
|
+
assert !parse(:conditions => {:id => [1,2,3], :is_active => true})
|
19
39
|
end
|
20
40
|
|
21
41
|
should "extract conditions" do
|
22
|
-
|
42
|
+
kasket_query = parse(:conditions => {:title => 'red', :blog_id => 1})
|
43
|
+
assert_equal [[:blog_id, "1"], [:title, "red"]], kasket_query[:attributes]
|
44
|
+
end
|
45
|
+
|
46
|
+
should "extract conditions with parans that do not surround" do
|
47
|
+
kasket_query = parse(:conditions => "(title = 'red') AND (blog_id = 1)")
|
48
|
+
if ActiveRecord::VERSION::STRING > "3.1.0"
|
49
|
+
assert !kasket_query
|
50
|
+
else
|
51
|
+
assert_equal [[:blog_id, "1"], [:title, "red"]], kasket_query[:attributes]
|
52
|
+
end
|
23
53
|
end
|
24
54
|
|
25
55
|
should "extract required index" do
|
26
|
-
assert_equal [:blog_id, :title],
|
56
|
+
assert_equal [:blog_id, :title], parse(:conditions => {:title => 'red', :blog_id => 1})[:index]
|
27
57
|
end
|
28
58
|
|
29
59
|
should "only support queries against its model's table" do
|
30
|
-
assert
|
60
|
+
assert !parse(:conditions => {'users.id' => 2}, :from => 'apples')
|
31
61
|
end
|
32
62
|
|
33
63
|
should "support cachable queries" do
|
34
|
-
assert
|
35
|
-
assert
|
64
|
+
assert parse(:conditions => {:id => 1})
|
65
|
+
assert parse(:conditions => {:id => 1}, :limit => 1)
|
36
66
|
end
|
37
67
|
|
38
68
|
should "support IN queries on id" do
|
39
|
-
|
40
|
-
assert(parsed_query)
|
41
|
-
assert_equal([[:id, ['1', '2', '3']]], parsed_query[:attributes])
|
69
|
+
assert_equal [[:id, ['1', '2', '3']]], parse(:conditions => {:id => [1,2,3]})[:attributes]
|
42
70
|
end
|
43
71
|
|
44
72
|
should "not support IN queries on other attributes" do
|
45
|
-
assert
|
73
|
+
assert !parse(:conditions => {:hest => [1,2,3]})
|
46
74
|
end
|
47
75
|
|
48
76
|
should "support vaguely formatted queries" do
|
@@ -50,57 +78,58 @@ class ParserTest < ActiveSupport::TestCase
|
|
50
78
|
end
|
51
79
|
|
52
80
|
context "extract options" do
|
53
|
-
|
54
81
|
should "provide the limit" do
|
55
|
-
|
56
|
-
assert_equal
|
57
|
-
|
58
|
-
sql << ' LIMIT 1'
|
59
|
-
assert_equal 1, @parser.parse(sql)[:limit]
|
82
|
+
assert_equal nil, parse(:conditions => {:id => 2})[:limit]
|
83
|
+
assert_equal 1, parse(:conditions => {:id => 2}, :limit => 1)[:limit]
|
60
84
|
end
|
61
|
-
|
62
85
|
end
|
63
86
|
|
64
87
|
context "unsupported queries" do
|
65
|
-
|
66
88
|
should "include advanced limits" do
|
67
|
-
assert
|
89
|
+
assert !parse(:conditions => {:title => 'red', :blog_id => 1}, :limit => 2)
|
68
90
|
end
|
69
91
|
|
70
92
|
should "include joins" do
|
71
|
-
assert
|
93
|
+
assert !parse(:conditions => {:title => 'test', 'apple.tree_id' => 'posts.id'}, :from => ['posts', 'apple'])
|
94
|
+
assert !parse(:conditions => {:title => 'test'}, :joins => :comments)
|
72
95
|
end
|
73
96
|
|
74
97
|
should "include specific selects" do
|
75
|
-
assert
|
98
|
+
assert !parse(:conditions => {:title => 'red'}, :select => :id)
|
76
99
|
end
|
77
100
|
|
78
101
|
should "include offset" do
|
79
|
-
assert
|
102
|
+
assert !parse(:conditions => {:title => 'red'}, :limit => 1, :offset => 2)
|
80
103
|
end
|
81
104
|
|
82
105
|
should "include order" do
|
83
|
-
assert
|
106
|
+
assert !parse(:conditions => {:title => 'red'}, :order => :title)
|
84
107
|
end
|
85
108
|
|
86
109
|
should "include the OR operator" do
|
87
|
-
assert
|
110
|
+
assert !parse(:conditions => "title = 'red' OR blog_id = 1")
|
88
111
|
end
|
89
112
|
end
|
90
113
|
|
91
114
|
context "key generation" do
|
92
115
|
should "include the table name and version" do
|
93
|
-
|
116
|
+
kasket_query = parse(:conditions => {:id => 1})
|
117
|
+
assert_match(/^kasket-#{Kasket::Version::PROTOCOL}\/posts\/version=#{POST_VERSION}\//, kasket_query[:key])
|
94
118
|
end
|
95
119
|
|
96
120
|
should "include all indexed attributes" do
|
97
|
-
|
98
|
-
assert_match(/
|
99
|
-
|
121
|
+
kasket_query = parse(:conditions => {:id => 1})
|
122
|
+
assert_match(/id=1$/, kasket_query[:key])
|
123
|
+
|
124
|
+
kasket_query = parse(:conditions => {:id => 1, :blog_id => 2})
|
125
|
+
assert_match(/blog_id=2\/id=1$/, kasket_query[:key])
|
126
|
+
|
127
|
+
kasket_query = parse(:conditions => {:id => 1, :title => 'title'})
|
128
|
+
assert_match(/id=1\/title='title'$/, kasket_query[:key])
|
100
129
|
end
|
101
130
|
|
102
131
|
should "generate multiple keys on IN queries" do
|
103
|
-
keys =
|
132
|
+
keys = parse(:conditions => {:id => [1,2]})[:key]
|
104
133
|
assert_instance_of(Array, keys)
|
105
134
|
assert_match(/id=1$/, keys[0])
|
106
135
|
assert_match(/id=2$/, keys[1])
|
@@ -108,13 +137,13 @@ class ParserTest < ActiveSupport::TestCase
|
|
108
137
|
|
109
138
|
context "when limit 1" do
|
110
139
|
should "add /first to the key if the index does not include id" do
|
111
|
-
assert_match(/title='a'\/first$/,
|
140
|
+
assert_match(/title='a'\/first$/, parse(:conditions => {:title => 'a'}, :limit => 1)[:key])
|
112
141
|
end
|
142
|
+
|
113
143
|
should "not add /first to the key when the index includes id" do
|
114
|
-
assert_match(/id=1$/,
|
144
|
+
assert_match(/id=1$/, parse(:conditions => {:id => 1}, :limit => 1)[:key])
|
115
145
|
end
|
116
146
|
end
|
117
147
|
end
|
118
148
|
end
|
119
|
-
|
120
149
|
end
|
data/test/read_mixin_test.rb
CHANGED
@@ -20,20 +20,25 @@ class ReadMixinTest < ActiveSupport::TestCase
|
|
20
20
|
end
|
21
21
|
|
22
22
|
should "read results" do
|
23
|
-
Kasket.cache.write("kasket-#{Kasket::Version::
|
23
|
+
Kasket.cache.write("kasket-#{Kasket::Version::PROTOCOL}/posts/version=#{POST_VERSION}/id=1", @post_database_result)
|
24
24
|
assert_equal @post_records, Post.find_by_sql('SELECT * FROM `posts` WHERE (id = 1)')
|
25
25
|
end
|
26
26
|
|
27
|
+
should "support sql with ?" do
|
28
|
+
Kasket.cache.write("kasket-#{Kasket::Version::PROTOCOL}/posts/version=#{POST_VERSION}/id=1", @post_database_result)
|
29
|
+
assert_equal @post_records, Post.find_by_sql(['SELECT * FROM `posts` WHERE (id = ?)', 1])
|
30
|
+
end
|
31
|
+
|
27
32
|
should "store results in kasket" do
|
28
33
|
Post.find_by_sql('SELECT * FROM `posts` WHERE (id = 1)')
|
29
34
|
|
30
|
-
assert_equal @post_database_result, Kasket.cache.read("kasket-#{Kasket::Version::
|
35
|
+
assert_equal @post_database_result, Kasket.cache.read("kasket-#{Kasket::Version::PROTOCOL}/posts/version=#{POST_VERSION}/id=1")
|
31
36
|
end
|
32
37
|
|
33
38
|
should "store multiple records in cache" do
|
34
39
|
Comment.find_by_sql('SELECT * FROM `comments` WHERE (post_id = 1)')
|
35
|
-
stored_value = Kasket.cache.read("kasket-#{Kasket::Version::
|
36
|
-
assert_equal(["kasket-#{Kasket::Version::
|
40
|
+
stored_value = Kasket.cache.read("kasket-#{Kasket::Version::PROTOCOL}/comments/version=#{COMMENT_VERSION}/post_id=1")
|
41
|
+
assert_equal(["kasket-#{Kasket::Version::PROTOCOL}/comments/version=#{COMMENT_VERSION}/id=1", "kasket-#{Kasket::Version::PROTOCOL}/comments/version=#{COMMENT_VERSION}/id=2"], stored_value)
|
37
42
|
assert_equal(@comment_database_result, stored_value.map {|key| Kasket.cache.read(key)})
|
38
43
|
|
39
44
|
Comment.expects(:find_by_sql_without_kasket).never
|
@@ -43,13 +48,15 @@ class ReadMixinTest < ActiveSupport::TestCase
|
|
43
48
|
|
44
49
|
context "modifying results" do
|
45
50
|
setup do
|
46
|
-
Kasket.cache.write("kasket-#{Kasket::Version::
|
47
|
-
@
|
51
|
+
Kasket.cache.write("kasket-#{Kasket::Version::PROTOCOL}/posts/version=#{POST_VERSION}/id=1", {'id' => 1, 'title' => "asd"})
|
52
|
+
@sql = 'SELECT * FROM `posts` WHERE (id = 1)'
|
53
|
+
@record = Post.find_by_sql(@sql).first
|
54
|
+
assert_equal "asd", @record.title # read from cache ?
|
48
55
|
@record.instance_variable_get(:@attributes)['id'] = 3
|
49
56
|
end
|
50
57
|
|
51
58
|
should "not impact other queries" do
|
52
|
-
same_record = Post.find_by_sql(
|
59
|
+
same_record = Post.find_by_sql(@sql).first
|
53
60
|
|
54
61
|
assert_not_equal @record, same_record
|
55
62
|
end
|
data/test/reload_test.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
require File.expand_path("helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class ReloadTest < ActiveSupport::TestCase
|
4
|
+
context "Loading a polymorphic belongs_to" do
|
5
|
+
should "not clear cache when loading nil" do
|
6
|
+
@post = Post.first
|
7
|
+
@post.poly = nil
|
8
|
+
@post.save!
|
9
|
+
Kasket.expects(:clear_local).never
|
10
|
+
assert_nil @post.poly
|
11
|
+
end
|
12
|
+
|
13
|
+
context "that is uncached" do
|
14
|
+
setup do
|
15
|
+
@post = Post.first
|
16
|
+
@post.poly = Blog.first
|
17
|
+
@post.save!
|
18
|
+
assert @post.poly
|
19
|
+
end
|
20
|
+
|
21
|
+
should "not clear local when it is unloaded" do
|
22
|
+
Kasket.expects(:clear_local).never
|
23
|
+
assert Post.first.poly
|
24
|
+
end
|
25
|
+
|
26
|
+
should "not clear local when it is loaded" do
|
27
|
+
Kasket.expects(:clear_local).never
|
28
|
+
assert @post.poly.reload
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "that is cached" do
|
33
|
+
setup do
|
34
|
+
@post = Post.first
|
35
|
+
@post.poly = Comment.first
|
36
|
+
@post.save!
|
37
|
+
assert @post.poly
|
38
|
+
end
|
39
|
+
|
40
|
+
should "clear local when it is loaded" do
|
41
|
+
Kasket.expects(:clear_local)
|
42
|
+
@post.poly.reload
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context "Reloading a model" do
|
48
|
+
setup do
|
49
|
+
@post = Post.first
|
50
|
+
assert @post
|
51
|
+
assert @post.title
|
52
|
+
end
|
53
|
+
|
54
|
+
should "clear local cache" do
|
55
|
+
Kasket.expects(:clear_local)
|
56
|
+
@post.reload
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context "Reloading a belongs_to association" do
|
61
|
+
setup do
|
62
|
+
@post = Comment.first.post
|
63
|
+
assert @post
|
64
|
+
assert @post.title
|
65
|
+
end
|
66
|
+
|
67
|
+
should "clear local cache" do
|
68
|
+
Kasket.expects(:clear_local)
|
69
|
+
@post.reload
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "Reloading a has_one_through association" do
|
74
|
+
setup do
|
75
|
+
@author = Comment.first.author
|
76
|
+
assert @author
|
77
|
+
assert @author.name
|
78
|
+
end
|
79
|
+
|
80
|
+
should "clear local cache" do
|
81
|
+
Kasket.expects(:clear_local)
|
82
|
+
@author.reload
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
data/test/schema.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 1) do
|
2
|
+
suppress_messages do
|
3
|
+
create_table 'comments', :force => true do |t|
|
4
|
+
t.text 'body'
|
5
|
+
t.integer 'post_id'
|
6
|
+
t.datetime 'created_at'
|
7
|
+
t.datetime 'updated_at'
|
8
|
+
end
|
9
|
+
|
10
|
+
create_table 'authors', :force => true do |t|
|
11
|
+
t.string 'name'
|
12
|
+
end
|
13
|
+
|
14
|
+
create_table 'posts', :force => true do |t|
|
15
|
+
t.string 'title'
|
16
|
+
t.integer 'author_id'
|
17
|
+
t.integer 'blog_id'
|
18
|
+
t.integer 'poly_id'
|
19
|
+
t.string 'poly_type'
|
20
|
+
t.datetime 'created_at'
|
21
|
+
t.datetime 'updated_at'
|
22
|
+
end
|
23
|
+
|
24
|
+
create_table 'blogs', :force => true do |t|
|
25
|
+
t.string 'name'
|
26
|
+
t.datetime 'created_at'
|
27
|
+
t.datetime 'updated_at'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/test/test_models.rb
CHANGED
@@ -1,29 +1,29 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
t.datetime "created_at"
|
9
|
-
t.datetime "updated_at"
|
10
|
-
end
|
1
|
+
ActiveRecord::Base.configurations = YAML::load(IO.read(File.expand_path("database.yml", File.dirname(__FILE__))))
|
2
|
+
|
3
|
+
conf = ActiveRecord::Base.configurations['test']
|
4
|
+
`echo "drop DATABASE if exists #{conf['database']}" | mysql --user=#{conf['username']}`
|
5
|
+
`echo "create DATABASE #{conf['database']}" | mysql --user=#{conf['username']}`
|
6
|
+
ActiveRecord::Base.establish_connection('test')
|
7
|
+
load(File.dirname(__FILE__) + "/schema.rb")
|
11
8
|
|
9
|
+
class Comment < ActiveRecord::Base
|
12
10
|
belongs_to :post
|
11
|
+
has_one :author, :through => :post
|
13
12
|
|
14
13
|
has_kasket_on :post_id
|
15
14
|
end
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
t.datetime "updated_at"
|
23
|
-
end
|
16
|
+
class Author < ActiveRecord::Base
|
17
|
+
has_many :posts
|
18
|
+
|
19
|
+
has_kasket
|
20
|
+
end
|
24
21
|
|
22
|
+
class Post < ActiveRecord::Base
|
25
23
|
belongs_to :blog
|
24
|
+
belongs_to :author
|
26
25
|
has_many :comments
|
26
|
+
belongs_to :poly, :polymorphic => true
|
27
27
|
|
28
28
|
has_kasket
|
29
29
|
has_kasket_on :title
|
@@ -33,15 +33,11 @@ create_model :post do
|
|
33
33
|
self.updated_at = Time.now
|
34
34
|
self.connection.execute("UPDATE posts SET updated_at = '#{updated_at.utc.to_s(:db)}' WHERE id = #{id}")
|
35
35
|
end
|
36
|
+
|
36
37
|
kasket_dirty_methods :make_dirty!
|
37
38
|
end
|
38
39
|
|
39
|
-
|
40
|
-
with_columns do |t|
|
41
|
-
t.string "name"
|
42
|
-
t.datetime "created_at"
|
43
|
-
t.datetime "updated_at"
|
44
|
-
end
|
45
|
-
|
40
|
+
class Blog < ActiveRecord::Base
|
46
41
|
has_many :posts
|
42
|
+
has_many :comments, :through => :posts
|
47
43
|
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require File.expand_path("helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class VisitorTest < ActiveSupport::TestCase
|
4
|
+
if arel?
|
5
|
+
context Kasket::Visitor do
|
6
|
+
should "build select id" do
|
7
|
+
expected = {
|
8
|
+
:attributes=>[[:id, "1"]],
|
9
|
+
:from=>"posts",
|
10
|
+
:index=>[:id],
|
11
|
+
:key=>"kasket-#{Kasket::Version::PROTOCOL}/posts/version=#{POST_VERSION}/id=1"
|
12
|
+
}
|
13
|
+
assert_equal expected, Post.where(:id => 1).to_kasket_query
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kasket
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 2.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,24 +10,30 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2012-11-
|
13
|
+
date: 2012-11-20 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
17
17
|
requirement: !ruby/object:Gem::Requirement
|
18
18
|
none: false
|
19
19
|
requirements:
|
20
|
-
- -
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 2.3.6
|
23
|
+
- - <
|
21
24
|
- !ruby/object:Gem::Version
|
22
|
-
version:
|
25
|
+
version: '3.3'
|
23
26
|
type: :runtime
|
24
27
|
prerelease: false
|
25
28
|
version_requirements: !ruby/object:Gem::Requirement
|
26
29
|
none: false
|
27
30
|
requirements:
|
28
|
-
- -
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.3.6
|
34
|
+
- - <
|
29
35
|
- !ruby/object:Gem::Version
|
30
|
-
version:
|
36
|
+
version: '3.3'
|
31
37
|
- !ruby/object:Gem::Dependency
|
32
38
|
name: rake
|
33
39
|
requirement: !ruby/object:Gem::Requirement
|
@@ -61,23 +67,23 @@ dependencies:
|
|
61
67
|
- !ruby/object:Gem::Version
|
62
68
|
version: '0'
|
63
69
|
- !ruby/object:Gem::Dependency
|
64
|
-
name:
|
70
|
+
name: appraisal
|
65
71
|
requirement: !ruby/object:Gem::Requirement
|
66
72
|
none: false
|
67
73
|
requirements:
|
68
|
-
- -
|
74
|
+
- - ~>
|
69
75
|
- !ruby/object:Gem::Version
|
70
|
-
version: '0'
|
76
|
+
version: '0.5'
|
71
77
|
type: :development
|
72
78
|
prerelease: false
|
73
79
|
version_requirements: !ruby/object:Gem::Requirement
|
74
80
|
none: false
|
75
81
|
requirements:
|
76
|
-
- -
|
82
|
+
- - ~>
|
77
83
|
- !ruby/object:Gem::Version
|
78
|
-
version: '0'
|
84
|
+
version: '0.5'
|
79
85
|
- !ruby/object:Gem::Dependency
|
80
|
-
name:
|
86
|
+
name: shoulda
|
81
87
|
requirement: !ruby/object:Gem::Requirement
|
82
88
|
none: false
|
83
89
|
requirements:
|
@@ -93,13 +99,13 @@ dependencies:
|
|
93
99
|
- !ruby/object:Gem::Version
|
94
100
|
version: '0'
|
95
101
|
- !ruby/object:Gem::Dependency
|
96
|
-
name:
|
102
|
+
name: mocha
|
97
103
|
requirement: !ruby/object:Gem::Requirement
|
98
104
|
none: false
|
99
105
|
requirements:
|
100
106
|
- - ~>
|
101
107
|
- !ruby/object:Gem::Version
|
102
|
-
version:
|
108
|
+
version: 0.10.5
|
103
109
|
type: :development
|
104
110
|
prerelease: false
|
105
111
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -107,23 +113,23 @@ dependencies:
|
|
107
113
|
requirements:
|
108
114
|
- - ~>
|
109
115
|
- !ruby/object:Gem::Version
|
110
|
-
version:
|
116
|
+
version: 0.10.5
|
111
117
|
- !ruby/object:Gem::Dependency
|
112
|
-
name:
|
118
|
+
name: test-unit
|
113
119
|
requirement: !ruby/object:Gem::Requirement
|
114
120
|
none: false
|
115
121
|
requirements:
|
116
|
-
- -
|
122
|
+
- - ~>
|
117
123
|
- !ruby/object:Gem::Version
|
118
|
-
version:
|
124
|
+
version: 2.5.1
|
119
125
|
type: :development
|
120
126
|
prerelease: false
|
121
127
|
version_requirements: !ruby/object:Gem::Requirement
|
122
128
|
none: false
|
123
129
|
requirements:
|
124
|
-
- -
|
130
|
+
- - ~>
|
125
131
|
- !ruby/object:Gem::Version
|
126
|
-
version:
|
132
|
+
version: 2.5.1
|
127
133
|
description: puts a cap on your queries
|
128
134
|
email:
|
129
135
|
- mick@zendesk.com
|
@@ -132,13 +138,15 @@ executables: []
|
|
132
138
|
extensions: []
|
133
139
|
extra_rdoc_files: []
|
134
140
|
files:
|
135
|
-
- lib/kasket/active_record_patches.rb
|
136
141
|
- lib/kasket/configuration_mixin.rb
|
137
142
|
- lib/kasket/dirty_mixin.rb
|
138
143
|
- lib/kasket/query_parser.rb
|
139
144
|
- lib/kasket/read_mixin.rb
|
145
|
+
- lib/kasket/relation_mixin.rb
|
140
146
|
- lib/kasket/reload_association_mixin.rb
|
147
|
+
- lib/kasket/select_manager_mixin.rb
|
141
148
|
- lib/kasket/version.rb
|
149
|
+
- lib/kasket/visitor.rb
|
142
150
|
- lib/kasket/write_mixin.rb
|
143
151
|
- lib/kasket.rb
|
144
152
|
- README.rdoc
|
@@ -149,14 +157,18 @@ files:
|
|
149
157
|
- test/dirty_test.rb
|
150
158
|
- test/find_one_test.rb
|
151
159
|
- test/find_some_test.rb
|
160
|
+
- test/fixtures/authors.yml
|
152
161
|
- test/fixtures/blogs.yml
|
153
162
|
- test/fixtures/comments.yml
|
154
163
|
- test/fixtures/posts.yml
|
155
164
|
- test/helper.rb
|
156
165
|
- test/parser_test.rb
|
157
166
|
- test/read_mixin_test.rb
|
167
|
+
- test/reload_test.rb
|
168
|
+
- test/schema.rb
|
158
169
|
- test/test_models.rb
|
159
170
|
- test/transaction_test.rb
|
171
|
+
- test/visitor_test.rb
|
160
172
|
homepage: http://github.com/staugaard/kasket
|
161
173
|
licenses: []
|
162
174
|
post_install_message:
|
@@ -171,13 +183,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
171
183
|
version: '0'
|
172
184
|
segments:
|
173
185
|
- 0
|
174
|
-
hash:
|
186
|
+
hash: 2863500110980526189
|
175
187
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
176
188
|
none: false
|
177
189
|
requirements:
|
178
190
|
- - ! '>='
|
179
191
|
- !ruby/object:Gem::Version
|
180
192
|
version: '0'
|
193
|
+
segments:
|
194
|
+
- 0
|
195
|
+
hash: 2863500110980526189
|
181
196
|
requirements: []
|
182
197
|
rubyforge_project:
|
183
198
|
rubygems_version: 1.8.24
|
@@ -192,11 +207,15 @@ test_files:
|
|
192
207
|
- test/dirty_test.rb
|
193
208
|
- test/find_one_test.rb
|
194
209
|
- test/find_some_test.rb
|
210
|
+
- test/fixtures/authors.yml
|
195
211
|
- test/fixtures/blogs.yml
|
196
212
|
- test/fixtures/comments.yml
|
197
213
|
- test/fixtures/posts.yml
|
198
214
|
- test/helper.rb
|
199
215
|
- test/parser_test.rb
|
200
216
|
- test/read_mixin_test.rb
|
217
|
+
- test/reload_test.rb
|
218
|
+
- test/schema.rb
|
201
219
|
- test/test_models.rb
|
202
220
|
- test/transaction_test.rb
|
221
|
+
- test/visitor_test.rb
|
@@ -1,57 +0,0 @@
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
2
|
-
module Kasket
|
3
|
-
module FixForAssociationAccessorMethods
|
4
|
-
def association_accessor_methods(reflection, association_proxy_class)
|
5
|
-
define_method(reflection.name) do |*params|
|
6
|
-
force_reload = params.first unless params.empty?
|
7
|
-
association = association_instance_get(reflection.name)
|
8
|
-
|
9
|
-
if association.nil? || force_reload
|
10
|
-
association = association_proxy_class.new(self, reflection)
|
11
|
-
retval = force_reload ? association.reload : association.__send__(:load_target)
|
12
|
-
if retval.nil? and association_proxy_class == ActiveRecord::Associations::BelongsToAssociation
|
13
|
-
association_instance_set(reflection.name, nil)
|
14
|
-
return nil
|
15
|
-
end
|
16
|
-
association_instance_set(reflection.name, association)
|
17
|
-
end
|
18
|
-
|
19
|
-
association.target.nil? ? nil : association
|
20
|
-
end
|
21
|
-
|
22
|
-
define_method("loaded_#{reflection.name}?") do
|
23
|
-
association = association_instance_get(reflection.name)
|
24
|
-
association && association.loaded?
|
25
|
-
end
|
26
|
-
|
27
|
-
define_method("#{reflection.name}=") do |new_value|
|
28
|
-
association = association_instance_get(reflection.name)
|
29
|
-
|
30
|
-
if association.nil? || association.target != new_value
|
31
|
-
association = association_proxy_class.new(self, reflection)
|
32
|
-
end
|
33
|
-
|
34
|
-
if association_proxy_class == ActiveRecord::Associations::HasOneThroughAssociation
|
35
|
-
association.create_through_record(new_value)
|
36
|
-
if new_record?
|
37
|
-
association_instance_set(reflection.name, new_value.nil? ? nil : association)
|
38
|
-
else
|
39
|
-
self.send(reflection.name, new_value)
|
40
|
-
end
|
41
|
-
else
|
42
|
-
association.replace(new_value)
|
43
|
-
association_instance_set(reflection.name, new_value.nil? ? nil : association)
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
define_method("set_#{reflection.name}_target") do |target|
|
48
|
-
return if target.nil? and association_proxy_class == ActiveRecord::Associations::BelongsToAssociation
|
49
|
-
association = association_proxy_class.new(self, reflection)
|
50
|
-
association.target = target
|
51
|
-
association_instance_set(reflection.name, association)
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
ActiveRecord::Base.extend Kasket::FixForAssociationAccessorMethods
|