ca_ching 0.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.
@@ -0,0 +1,64 @@
1
+ require 'ca_ching/query/abstract'
2
+ require 'ca_ching/query/calculation'
3
+ require 'ca_ching/query/select'
4
+
5
+ module CaChing
6
+ module ReadThrough
7
+ extend ActiveSupport::Concern
8
+
9
+ module ClassMethods
10
+ end
11
+
12
+ module InstanceMethods
13
+ # All queries, be they find(id), dynamic finders (find_by_foo, find_all_by_foo, etc.),
14
+ # where(:foo => :bar), order('foo DESC'), etc. go through to_a before being returned.
15
+ # Hook into that point to check the cache.
16
+ def to_a_with_cache
17
+ @_query = CaChing::Query::Select.new(self)
18
+
19
+ return to_a_without_cache if CaChing.cache.nil? || CaChing.disabled? || !cacheable?
20
+
21
+ result = CaChing.cache.find(@_query)
22
+ @from_cache = true
23
+
24
+ if result.nil?
25
+ result = to_a_without_cache
26
+ CaChing.cache.insert(result, :for => @_query)
27
+ @from_cache = false
28
+ end
29
+
30
+ result.from_cache = self.from_cache?
31
+ result.each { |item| item.from_cache = self.from_cache? }
32
+
33
+ return result
34
+ end
35
+
36
+ def from_cache?
37
+ @from_cache ||= false
38
+ end
39
+
40
+ def cacheable?
41
+ unsupported_methods = [:from_value,
42
+ :group_values,
43
+ :having_values,
44
+ :includes_values,
45
+ :joined_includes_values,
46
+ :joins_values,
47
+ :lock_value,
48
+ :select_values]
49
+ !where_values.empty? && find_on_indexed_fields? && unsupported_methods.inject(true) { |flag, method| self.send(method).send(method.to_s =~ /values/ ? :empty? : :nil?) && flag }
50
+ end
51
+
52
+ private
53
+ def find_on_indexed_fields?
54
+ (@_query.where.keys - indexed_fields.keys).empty?
55
+ end
56
+ end
57
+
58
+ included do |base|
59
+ base.class_eval do
60
+ alias_method_chain :to_a, :cache
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,13 @@
1
+ module CaChing
2
+ module Version
3
+ MAJOR = 0
4
+ MINOR = 1
5
+ PATCH = 0
6
+ PRE = nil
7
+
8
+
9
+ def self.to_s
10
+ [MAJOR, MINOR, PATCH, PRE].compact.join('.')
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ module CaChing
2
+ module WriteThrough
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ end
7
+
8
+ module InstanceMethods
9
+ def save_with_cache(*)
10
+ return save_without_cache if CaChing.cache.nil? || CaChing.disabled?
11
+
12
+ CaChing.cache.update(self)
13
+ save_without_cache
14
+ end
15
+ end
16
+
17
+ included do |base|
18
+ base.class_eval do
19
+ alias_method_chain :save, :cache
20
+ end
21
+ end
22
+ end
23
+ end
data/lib/ca_ching.rb ADDED
@@ -0,0 +1,21 @@
1
+ require "ca_ching/version"
2
+
3
+ require 'redis'
4
+ require 'redis/connection/hiredis'
5
+
6
+ require 'ca_ching/configuration'
7
+
8
+ require 'ca_ching/read_through'
9
+ require 'ca_ching/write_through'
10
+ require 'ca_ching/index'
11
+
12
+ require 'ca_ching/errors'
13
+
14
+ require 'ca_ching/adapters/redis'
15
+ require 'ca_ching/adapters/active_record'
16
+
17
+ require 'ca_ching/core_ext/array'
18
+
19
+ module CaChing
20
+ extend Configuration
21
+ end
@@ -0,0 +1,5 @@
1
+ Article.blueprint do
2
+ person
3
+ title
4
+ body
5
+ end
@@ -0,0 +1,5 @@
1
+ Comment.blueprint do
2
+ article
3
+ person
4
+ body
5
+ end
@@ -0,0 +1,5 @@
1
+ Person.blueprint do
2
+ name
3
+ salary
4
+ age { (rand(70) + 10).to_i }
5
+ end
@@ -0,0 +1,3 @@
1
+ Tag.blueprint do
2
+ name { Sham.tag_name }
3
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe ActiveRecord::Base do
4
+
5
+ end
@@ -0,0 +1,176 @@
1
+ require 'spec_helper'
2
+
3
+ module CaChing
4
+ module Adapters
5
+ describe Redis do
6
+ before :all do
7
+ @cache = CaChing::Adapters::Redis.new
8
+ end
9
+
10
+ before :each do
11
+ @cache.clear!
12
+ end
13
+
14
+ describe '#find' do
15
+ it 'finds the object(s) for the given query' do
16
+ object = [Article.where('1=1').limit(1).to_a_without_cache.first]
17
+ ar = Article.where(:title => object.first.title)
18
+ query = CaChing::Query::Abstract.new(ar)
19
+
20
+ @cache.insert(ar, :for => query)
21
+ @cache.find(query).should == object
22
+ end
23
+
24
+ it 'returns nil if the objects are not found' do
25
+ ar = Article.where(:title => 'Foo bar')
26
+ query = CaChing::Query::Abstract.new(ar)
27
+
28
+ @cache.find(query).should == nil
29
+ end
30
+
31
+ it 'does not return a dirty object' do
32
+ object = [Article.where('1=1').limit(1).to_a_without_cache.first]
33
+ ar = Article.where(:title => object.first.title)
34
+ query = CaChing::Query::Abstract.new(ar)
35
+
36
+ @cache.insert(ar, :for => query)
37
+ @cache.find(query).inject(true) { |flag, object| flag && object.changed? }.should == false
38
+ end
39
+ end
40
+
41
+ describe '#insert' do
42
+ it 'inserts the objects for a query with one where clause' do
43
+ object = [Article.where('1=1').limit(1).to_a_without_cache.first]
44
+ ar = Article.where(:title => object.first.title)
45
+ query = CaChing::Query::Abstract.new(ar)
46
+
47
+ @cache.insert(ar, :for => query)
48
+ @cache.find(query).should == object
49
+ end
50
+
51
+ it 'does not insert the objects for a query with multiple where clauses' do
52
+ ar = Article.where(:person_id => 1, :title => 'Foo bar')
53
+ query = CaChing::Query::Abstract.new(ar)
54
+
55
+ @cache.insert(ar, :for => query)
56
+ @cache.find(query).should == nil
57
+ end
58
+
59
+ it 'returns nil if the operation was unsuccessful' do
60
+ ar = Article.where(:person_id => 1, :title => 'Foo bar')
61
+ query = CaChing::Query::Abstract.new(ar)
62
+
63
+ @cache.insert(ar, :for => query).should == nil
64
+ end
65
+
66
+ it 'returns the objects inserted if the operation was successful' do
67
+ object = [Article.where('1=1').to_a_without_cache.first]
68
+ ar = Article.where(:title => object.first.title)
69
+ query = CaChing::Query::Abstract.new(ar)
70
+
71
+ @cache.insert(ar, :for => query).should == object
72
+ end
73
+ end
74
+
75
+ describe '#update' do
76
+ it 'updates the object at the given key' do
77
+ object = [Article.where('1=1').to_a_without_cache.first]
78
+ ar = Article.where(:person_id => object.first.person_id)
79
+ query = CaChing::Query::Abstract.new(ar)
80
+
81
+ @cache.insert(ar, :for => query)
82
+
83
+ record = ar.to_a_without_cache.first
84
+ record.title = record.title.reverse
85
+
86
+ @cache.update(record)
87
+ @cache.find(query).first.title.should == record.title
88
+ end
89
+
90
+ describe 'key change' do
91
+ it 'removes the object at the old key and inserts it at the new key if the key already exists' do
92
+ articles = Article.where('1=1').to_a_without_cache
93
+ ar = Article.where(:person_id => articles.first.person_id)
94
+ query = CaChing::Query::Abstract.new(ar)
95
+
96
+ @cache.insert(ar, :for => query)
97
+
98
+ ar = Article.where(:person_id => articles.last.person_id)
99
+ query2 = CaChing::Query::Abstract.new(ar)
100
+
101
+ @cache.insert(ar, :for => query)
102
+
103
+ record = ar.to_a_without_cache.first
104
+ record.person_id = articles.first.person_id
105
+
106
+ @cache.update(record)
107
+
108
+ ar = Article.where(:person_id => 1)
109
+ query = CaChing::Query::Abstract.new(ar)
110
+
111
+ @cache.find(query).last.title.should == record.title
112
+ @cache.find(query2).should == nil
113
+ end
114
+
115
+ it 'removes the object at the old key but does not insert it at the new if the key doesn\'t exist' do
116
+ articles = Article.where('1=1').to_a_without_cache
117
+ ar = Article.where(:title => articles.first.title)
118
+ query = CaChing::Query::Abstract.new(ar)
119
+
120
+ @cache.insert(ar, :for => query)
121
+
122
+ record = ar.to_a_without_cache.first
123
+ record.title = record.title.reverse
124
+
125
+ @cache.update(record)
126
+ @cache.find(query).should == nil
127
+ @cache.find(CaChing::Query::Abstract.new(Article.where(:title => record.title))).should == nil
128
+ end
129
+ end
130
+ end
131
+
132
+ describe '#destroy' do
133
+ it 'removes the object at the key' do
134
+ articles = Article.where('1=1').to_a_without_cache
135
+ ar = Article.where(:title => articles.last.title)
136
+ query = CaChing::Query::Abstract.new(ar)
137
+
138
+ @cache.insert(ar, :for => query)
139
+ @cache.destroy(ar.first, :at => "articles:title=#{articles.last.title}")
140
+ @cache.find(query).should == nil
141
+ end
142
+ end
143
+
144
+ describe '#clear!' do
145
+ it 'clears redis' do
146
+
147
+ end
148
+ end
149
+
150
+ describe '#inflate' do
151
+ it 'turns the stored objects back into AR objects' do
152
+
153
+ end
154
+ end
155
+
156
+ describe '#deflate' do
157
+
158
+ end
159
+
160
+ describe '#deflate_with_score' do
161
+ it 'turns the objects into JSON strings' do
162
+
163
+ end
164
+
165
+ it 'uses the score method if provided' do
166
+
167
+ end
168
+
169
+ it 'defaults to using id if no score method is provided' do
170
+
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ module CaChing
4
+ describe Configuration do
5
+
6
+ end
7
+ end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ module CaChing
4
+ describe Index do
5
+ it 'allows indexes to be defined' do
6
+ Person.index :name
7
+ Person.indexes?(:name).should == true
8
+ end
9
+
10
+ describe Index, 'index options' do
11
+ it 'allows indexes to be defined with options' do
12
+ Person.index :name, :order => { :age => :asc }, :ttl => 12.seconds
13
+ Person.indexes?(:name).should == true
14
+ Person.send(:indexed_fields)[:name].should == { :order => { :age => :asc }, :ttl => 12.seconds }
15
+ end
16
+
17
+ it 'rejects unsupported options' do
18
+ lambda { Person.index :name, :not => :valid }.should raise_error(InvalidOptionError)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,195 @@
1
+ require 'spec_helper'
2
+
3
+ module CaChing
4
+ module Query
5
+ describe Abstract do
6
+ describe 'SQL parsing' do
7
+ # The queries we are concerned with take the form:
8
+ # SELECT [*|field1,field2,...] FROM table_name WHERE field [=|<|>|<=...] value [AND|OR] ... LIMIT x OFFSET y... ORDER BY ...
9
+ # The tests are split up into parsing the various parts, namely:
10
+ # - table_name
11
+ # - conditions (the WHERE bit)
12
+ # - limit
13
+ # - offset
14
+ # - order
15
+ #
16
+ # The tests conclude with parsing various complete queries to test the combination of the various parsing logics.
17
+ describe '#order' do
18
+ it 'returns {} if no order is set' do
19
+ ar = Person.where(:name => 'Andrew')
20
+ query = Abstract.new(ar)
21
+ query.order.should == {}
22
+ end
23
+
24
+ it 'returns the key and desc for a singular order clause DESC' do
25
+ ar = Person.order('created_at DESC')
26
+ query = Abstract.new(ar)
27
+ query.order.should == { :created_at => :desc }
28
+ end
29
+
30
+ it 'returns the key and desc for a singular order clause ASC' do
31
+ ar = Person.order('created_at ASC')
32
+ query = Abstract.new(ar)
33
+ query.order.should == { :created_at => :asc }
34
+ end
35
+
36
+ it 'returns the keys and direction for multiple order clauses from different order calls' do
37
+ ar = Person.order('created_at DESC').order('name ASC')
38
+ query = Abstract.new(ar)
39
+ query.order.should == { :created_at => :desc, :name => :asc }
40
+ end
41
+
42
+ it 'returns the keys and direction for multiple order clauses from the same order call' do
43
+ ar = Person.order('created_at DESC, name ASC')
44
+ query = Abstract.new(ar)
45
+ query.order.should == { :created_at => :desc, :name => :asc }
46
+ end
47
+
48
+ it 'returns the keys and direction for multiple order clauses from different order calls and from the same order call' do
49
+ ar = Person.order('created_at DESC, name ASC').order('salary DESC')
50
+ query = Abstract.new(ar)
51
+ query.order.should == { :created_at => :desc, :name => :asc, :salary => :desc }
52
+ end
53
+ end
54
+
55
+ describe '#where' do
56
+ it 'returns {} if no where clause has been specified' do
57
+ ar = Person.order('created_at DESC')
58
+ query = Abstract.new(ar)
59
+ query.where.should == {}
60
+ end
61
+
62
+ it 'interpolates bind_values' do
63
+ pending 'needs a hook to get collection for Model.find(id), since that seems the only place that uses bind_values'
64
+ end
65
+
66
+ describe 'string queries' do
67
+ it 'handles a single condition' do
68
+ ar = Person.where('name = ?', 'Andrew')
69
+ query = Abstract.new(ar)
70
+ query.where.should == { :name => ['=', 'Andrew'] }
71
+ end
72
+
73
+ it 'splits on AND' do
74
+ ar = Person.where('name = ? AND age = ?', 'Andrew', 22)
75
+ query = Abstract.new(ar)
76
+ query.where.should == { :name => ['=', 'Andrew'], :age => ['=', '22'] }
77
+ end
78
+
79
+ it 'handles arbitrary comparators' do
80
+ ar = Person.where('age > ?', 18)
81
+ query = Abstract.new(ar)
82
+ query.where.should == { :age => ['>', '18'] }
83
+ end
84
+
85
+ it 'rejects OR conditions' do
86
+ ar = Person.where('name = ? OR age > ?', 'Andrew', 18)
87
+ query = Abstract.new(ar)
88
+ lambda { query.where }.should raise_error(UncacheableConditionError)
89
+ end
90
+ end
91
+
92
+ describe 'hash queries' do
93
+ it 'handles a single condition' do
94
+ ar = Person.where(:name => 'Andrew')
95
+ query = Abstract.new(ar)
96
+ query.where.should == { :name => ['=', 'Andrew'] }
97
+ end
98
+
99
+ it 'handles multiple clauses' do
100
+ ar = Person.where(:name => 'Andrew', :age => 22)
101
+ query = Abstract.new(ar)
102
+ query.where.should == { :name => ['=', 'Andrew'], :age => ['=', 22] }
103
+ end
104
+ end
105
+ end
106
+
107
+ describe '#limit' do
108
+ it 'returns the limit if one is set' do
109
+ ar = Person.limit(10)
110
+ query = Abstract.new(ar)
111
+ query.limit.should == 10
112
+ end
113
+
114
+ it 'returns nil if the limit is not set' do
115
+ ar = Person.where(:name => 'Andrew')
116
+ query = Abstract.new(ar)
117
+ query.limit.should == nil
118
+ end
119
+ end
120
+
121
+ describe '#offset' do
122
+ it 'returns the offset if one is set' do
123
+ ar = Person.offset(10)
124
+ query = Abstract.new(ar)
125
+ query.offset.should == 10
126
+ end
127
+
128
+ it 'returns nil if the offset is not set' do
129
+ ar = Person.where(:name => 'Andrew')
130
+ query = Abstract.new(ar)
131
+ query.offset.should == nil
132
+ end
133
+ end
134
+
135
+ describe '#table_name' do
136
+ it 'returns the table name' do
137
+ ar = Person.where(:name => 'Andrew')
138
+ query = Abstract.new(ar)
139
+ query.table_name.should == "people"
140
+ end
141
+ end
142
+
143
+ it 'handles complex queries' do
144
+ ar = Person.where(:name => 'Andrew').where('age >= ? AND salary < ?', 21, 10000).order('created_at DESC').order('name DESC, age ASC').limit(20).offset(20)
145
+ query = Abstract.new(ar)
146
+
147
+ query.where.should == { :name => ['=', 'Andrew'], :age => ['>=', '21'], :salary => ['<', '10000'] }
148
+ query.order.should == { :created_at => :desc, :name => :desc, :age => :asc }
149
+ query.limit.should == 20
150
+ query.offset.should == 20
151
+ end
152
+ end
153
+
154
+ describe '#to_key' do
155
+ it 'formats the key as table_name:field1="value1"&field2="value2"...' do
156
+ ar = Person.where(:name => 'Andrew', :age => 22)
157
+ query = Abstract.new(ar)
158
+ query.to_key.should == 'people:name="Andrew"&age="22"'
159
+ end
160
+
161
+ it 'escapes quotes in a string' do
162
+ ar = Person.where(:name => '"Howard"')
163
+ query = Abstract.new(ar)
164
+ query.to_key.should == 'people:name="\"Howard\""'
165
+ end
166
+
167
+ it 'separates multiple conditions by &' do
168
+ ar = Person.where(:name => 'Andrew', :age => 22)
169
+ query = Abstract.new(ar)
170
+ query.to_key.should == 'people:name="Andrew"&age="22"'
171
+ end
172
+ end
173
+
174
+ describe '#primary_key?' do
175
+ it 'returns true if the query is only on the primary key' do
176
+ ar = Person.where(:id => 1)
177
+ query = Abstract.new(ar)
178
+ query.primary_key?.should == true
179
+ end
180
+
181
+ it 'returns false if the query is not on the primary key' do
182
+ ar = Person.where(:name => 'Andrew')
183
+ query = Abstract.new(ar)
184
+ query.primary_key?.should == false
185
+ end
186
+
187
+ it 'returns false if the query is on the primary key and another field' do
188
+ ar = Person.where(:id => 1, :name => 'Andrew')
189
+ query = Abstract.new(ar)
190
+ query.primary_key?.should == false
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ module CaChing
4
+ module Query
5
+ describe Calculation do
6
+
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ module CaChing
4
+ module Query
5
+ describe Select do
6
+
7
+ end
8
+ end
9
+ end