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.
- data/.gitignore +4 -0
- data/Gemfile +16 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +20 -0
- data/README.md +106 -0
- data/Rakefile +19 -0
- data/SPEC.md +109 -0
- data/ca_ching.gemspec +25 -0
- data/lib/ca_ching/adapters/active_record.rb +23 -0
- data/lib/ca_ching/adapters/redis.rb +127 -0
- data/lib/ca_ching/configuration.rb +23 -0
- data/lib/ca_ching/core_ext/array.rb +7 -0
- data/lib/ca_ching/errors.rb +3 -0
- data/lib/ca_ching/index.rb +28 -0
- data/lib/ca_ching/query/abstract.rb +106 -0
- data/lib/ca_ching/query/calculation.rb +6 -0
- data/lib/ca_ching/query/select.rb +6 -0
- data/lib/ca_ching/read_through.rb +64 -0
- data/lib/ca_ching/version.rb +13 -0
- data/lib/ca_ching/write_through.rb +23 -0
- data/lib/ca_ching.rb +21 -0
- data/spec/blueprints/articles.rb +5 -0
- data/spec/blueprints/comments.rb +5 -0
- data/spec/blueprints/people.rb +5 -0
- data/spec/blueprints/tags.rb +3 -0
- data/spec/ca_ching/adapters/active_record_spec.rb +5 -0
- data/spec/ca_ching/adapters/redis_spec.rb +176 -0
- data/spec/ca_ching/configuration_spec.rb +7 -0
- data/spec/ca_ching/index_spec.rb +22 -0
- data/spec/ca_ching/query/abstract_spec.rb +195 -0
- data/spec/ca_ching/query/calculation_spec.rb +9 -0
- data/spec/ca_ching/query/select_spec.rb +9 -0
- data/spec/ca_ching/read_through_spec.rb +214 -0
- data/spec/ca_ching/write_through_spec.rb +25 -0
- data/spec/console.rb +22 -0
- data/spec/helpers/ca_ching_helper.rb +2 -0
- data/spec/spec_helper.rb +37 -0
- data/spec/support/schema.rb +98 -0
- metadata +150 -0
@@ -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,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,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,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
|