kasket 1.0.4 → 2.1.0
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.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
|