ca_ching 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ # Development gems
6
+ gem 'ruby-debug19'
7
+ gem 'rake'
8
+
9
+ gem 'sqlite3'
10
+
11
+ # Testing
12
+ gem 'rspec'
13
+ gem 'machinist'
14
+ gem 'faker'
15
+ gem 'guard-rspec'
16
+ gem 'growl_notify'
data/Guardfile ADDED
@@ -0,0 +1,9 @@
1
+ guard 'rspec', :version => 2, :cli => "--color --format documentation" do
2
+ watch(%r{^spec/ca_ching/.+_spec\.rb$})
3
+ watch(%r{^lib/ca_ching/(.+)\.rb$}) { |m| "spec/ca_ching/#{m[1]}_spec.rb" }
4
+ watch(%r{^spec/ca_ching/adapters/.+_spec\.rb$})
5
+ watch(%r{^lib/ca_ching/adapters/(.+)\.rb$}) { |m| "spec/ca_ching/adapters/#{m[1]}_spec.rb" }
6
+ watch(%r{^spec/ca_ching/query/.+_spec\.rb$})
7
+ watch(%r{^lib/ca_ching/query/(.+)\.rb$}) { |m| "spec/ca_ching/query/#{m[1]}_spec.rb" }
8
+ watch('spec/spec_helper.rb') { "spec" }
9
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Andrew Latimer
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # CaChing
2
+
3
+ CaChing is a write-through and read-through caching library for ActiveRecord 3.1+.
4
+
5
+ That means that when you read from the database, your results are stored in the cache (read-through). When you write to
6
+ the database, whatever is written to the database is also written to the cache (write-through). If the results are already
7
+ in the cache, great, they're read straight from there on a read, and updated on a write.
8
+
9
+ Take a look at `SPEC.md` for what's planned for v1.
10
+
11
+ ## Getting started
12
+
13
+ In your Gemfile:
14
+
15
+ gem 'ca_ching'
16
+ # gem 'ca_ching', :git => 'git://github.com/ahlatimer/ca_ching.git' # Track git repo
17
+
18
+ In an initializer:
19
+
20
+ CaChing.configure do |config|
21
+ config.cache = CaChing::Adapters::Redis.new
22
+ config.enabled = true # defaults to true
23
+ end
24
+
25
+ ## Defining what to cache
26
+
27
+ The simplest case is to add `index :field_name` to your model. From then on, any query that does a find based on
28
+ that field will get cached.
29
+
30
+ class Person < ActiveRecord::Base
31
+ index :id
32
+ index :first_name
33
+ index :last_name
34
+
35
+ has_many :addresses
36
+ end
37
+
38
+ class Address < ActiveRecord::Base
39
+ # Let's you find by associations (person.addresses.all)
40
+ index :person_id
41
+
42
+ belongs_to :person
43
+ end
44
+
45
+ ## Using the cache
46
+
47
+ If you've defined your indices, everything should work without any additional effort. Doing
48
+ `Person.where(:first_name => 'Andrew')` should hit the cache and return what is there, or
49
+ miss and pull the data into the cache.
50
+
51
+ ## Queries supported
52
+
53
+ Generally anything `eqality` is supported by CaChing. Currently, only single fields can be found (e.g., `Person.where(:name => 'Andrew')` will be cached,
54
+ `Person.where(:name => 'Andrew', :age => 22)` is not). There is some plumbing for adding support for the latter, and support will be
55
+ introduced as soon as possible.
56
+
57
+ Anything including joins, includes, `OR`, and inequality (!=) are not supported, nor do I have any plans for adding support.
58
+
59
+ For example, these queries are supported:
60
+
61
+ Person.where(:first_name => 'Andrew')
62
+ Person.find_by_first_name('Andrew')
63
+ Person.find_all_by_first_name('Andrew')
64
+
65
+ These queries are not:
66
+
67
+ Person.where('first_name = ? OR first_name = ?', 'Andrew', 'David')
68
+ Person.where('first_name != ?', 'Andrew')
69
+ Person.where(:first_name => 'Andrew').order('created_at DESC') # not supported because first_name isn't sorted by anything
70
+ Person.where('id >= 200')
71
+ Person.where('created_at >= ?', Date.today - 1).where(:first_name => 'Andrew')
72
+ Person.where('created_at <= ?', Date.today - 1).where(:first_name => 'Andrew').limit(10).order('created_at desc')
73
+
74
+ ## Caveats
75
+
76
+ ### Order
77
+
78
+ Order is not currently supported, although there will be some order semantics in the next version.
79
+
80
+ ### Composite keys and queries against multiple fields
81
+
82
+ Because of the awesomeness from Redis, composite keys aren't necessary for queries with multiple fields.
83
+ If all of the fields are indexed, CaChing will try to use the cache to build the results.
84
+
85
+ Redis requires that sorted set intersections be stored in a resulting set. If the composite key is not specified
86
+ for a query against those fields, that resulting set will be destroyed as soon as the results are read. If the
87
+ composite key *is* specified, CaChing will keep the resulting set.
88
+
89
+ While there is support for this in the Redis adapter, there isn't any support in the ActiveRecord ties as I haven't
90
+ decided how, exactly, I'd like to add this.
91
+
92
+ ## Ruby/Rails versions supported
93
+
94
+ Ruby 1.9.2 and Rails 3.1+ are officially supported. I try to stick to Ruby 1.8.7 syntax, so it may be supported,
95
+ but use it with the understanding that you are doing so at your own risk.
96
+
97
+ ## Patches and Issues
98
+
99
+ I'd love the help! If you find an issue, please report it on the [Github issues page](http://github.com/ahlatimer/ca_ching/issues).
100
+ If you fix an issue, please include a spec that illustrates the issue. If you submit a feature, include thorough specs.
101
+ Don't bump up the version in your pull request. If you want to keep different versions in your fork, feel free, but
102
+ please do not include them in your pull request.
103
+
104
+ ## Acknowledgments
105
+
106
+ Inspired by (and a bit of code copied from) [Cache Money](http://github.com/ngmoco/cache-money).
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ RSpec::Core::RakeTask.new(:spec) do |rspec|
7
+ rspec.rspec_opts = ['--debugger', '--color', '--format documentation']
8
+ end
9
+
10
+ task :default => :spec
11
+
12
+ desc "Open an irb session with CaChing and the sample data used in specs"
13
+ task :console do
14
+ require 'irb'
15
+ require 'irb/completion'
16
+ require 'console'
17
+ ARGV.clear
18
+ IRB.start
19
+ end
data/SPEC.md ADDED
@@ -0,0 +1,109 @@
1
+ # CaChing
2
+
3
+ CaChing is a write-through and read-through caching library for ActiveRecord 3.1+.
4
+
5
+ That means that when you read from the database, your results are stored in the cache (read-through). When you write to
6
+ the database, whatever is written to the database is also written to the cache (write-through). If the results are already
7
+ in the cache, great, they're read straight from there on a read, and updated on a write.
8
+
9
+ ## Getting started
10
+
11
+ In your Gemfile:
12
+
13
+ gem 'ca_ching'
14
+ # gem 'ca_ching', :git => 'git://github.com/ahlatimer/ca_ching.git' # Track git repo
15
+
16
+ In an initializer:
17
+
18
+ CaChing.configure do |config|
19
+ # some configuration options go here, likely just for redis...
20
+ end
21
+
22
+ ## Defining what to cache
23
+
24
+ The simplest case is to add `index :field_name` to your model. From then on, any query that does a find based on
25
+ that field will get cached. You can also specify the maximum number of elements stored, the TTL (time to live),
26
+ and the order.
27
+
28
+ class Person < ActiveRecord::Base
29
+ index :id
30
+ index :first_name, :limit => 1000
31
+ index :last_name, :limit => 1000, :order => { :created_at => :desc }
32
+
33
+ has_many :addresses
34
+ end
35
+
36
+ class Address < ActiveRecord::Base
37
+ # You can have associations, so person.addresses.find(1) will hit the cache if a composite index is specified
38
+ index [:id, :person_id], :limit => 100, :ttl => 10.minutes
39
+ # Or just find all of them based on person_id (person.addresses.all)
40
+ index :person_id
41
+
42
+ belongs_to :person
43
+ end
44
+
45
+ ## Using the cache
46
+
47
+ If you've defined your indices, everything should work without any additional effort. Doing
48
+ `Person.where(:first_name => 'Andrew')` should hit the cache and return what is there, or
49
+ miss and pull the data into the cache.
50
+
51
+ ## Queries supported
52
+
53
+ Generally anything with an `AND` and/or `eqality` is supported by CaChing. Anything including
54
+ joins, includes, `OR`, and inequality (!=) are not supported at this time. Queries involving
55
+ comparators (>, <, >=, <=) are supported ONLY if the order is specified. The caveats for order
56
+ (covered below) apply here as well.
57
+
58
+ For example, these queries are supported:
59
+
60
+ Person.where(:first_name => 'Andrew')
61
+ Person.find_by_first_name('Andrew')
62
+ Person.where('created_at >= ?', Date.today - 1).where(:first_name => 'Andrew')
63
+ Person.where('created_at <= ?', Date.today - 1).where(:first_name => 'Andrew').limit(10).order('created_at desc')
64
+
65
+ These queries are not:
66
+
67
+ Person.where('first_name = ? OR first_name = ?', 'Andrew', 'David')
68
+ Person.where('first_name != ?', 'Andrew')
69
+ Person.where(:first_name => 'Andrew').order('created_at DESC') # not supported because first_name isn't sorted by anything
70
+ Person.where('id >= 200')
71
+
72
+ ## Caveats
73
+
74
+ ### Order
75
+
76
+ If the order in the query is not the same as the order in the index directive, the cache will be skipped.
77
+ It's likely faster to take the DB hit than to try to sort in Ruby.
78
+
79
+ If multiple fields are specified, at least one of them must have the order of the query.
80
+
81
+ If the field is sorted, it must respond to `to_f` and return a reasonable response (e.g., even though a
82
+ string will respond to `to_f`, it will return `0.0` if it is not a number).
83
+
84
+ By default, indexes are ordered by the primary key.
85
+
86
+ ### Composite keys and queries against multiple fields
87
+
88
+ Because of the awesomeness from Redis, composite keys aren't necessary for queries with multiple fields.
89
+ If all of the fields are indexed, CaChing will try to use the cache to build the results.
90
+
91
+ Redis requires that sorted set intersections be stored in a resulting set. If the composite key is not specified
92
+ for a query against those fields, that resulting set will be destroyed as soon as the results are read. If the
93
+ composite key *is* specified, CaChing will keep the resulting set.
94
+
95
+ ## Ruby/Rails versions supported
96
+
97
+ Ruby 1.9.2 and Rails 3.1+ are officially supported. I try to stick to Ruby 1.8.7 syntax, so it may be supported,
98
+ but use it with the understanding that you are doing so at your own risk.
99
+
100
+ ## Patches and Issues
101
+
102
+ I'd love the help! If you find an issue, please report it on the [Github issues page](http://github.com/ahlatimer/ca_ching/issues).
103
+ If you fix an issue, please include a spec that illustrates the issue. If you submit a feature, include thorough specs.
104
+ Don't bump up the version in your pull request. If you want to keep different versions in your fork, feel free, but
105
+ please do not include them in your pull request.
106
+
107
+ ## Acknowledgments
108
+
109
+ Inspired by (and a bit of code copied from) [Cache Money](http://github.com/ngmoco/cache-money).
data/ca_ching.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "ca_ching/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "ca_ching"
7
+ s.version = CaChing::Version.to_s
8
+ s.authors = ["Andrew Latimer"]
9
+ s.email = ["andrew@elpasoera.com"]
10
+ s.homepage = "http://github.com/ahlatimer/ca_ching"
11
+ s.summary = %q{Write-through ActiveRecord model caching that's right on the money}
12
+ s.description = %q{Write-through ActiveRecord model caching that's right on the money}
13
+
14
+ s.rubyforge_project = "ca_ching"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_dependency 'hiredis'
22
+ s.add_dependency 'redis', '2.2.2'
23
+ s.add_dependency 'activesupport', '>= 3.1.0'
24
+ s.add_dependency 'activerecord', '>= 3.1.0'
25
+ end
@@ -0,0 +1,23 @@
1
+
2
+ ActiveRecord::Base.send :include, CaChing::Index
3
+ ActiveRecord::Base.send :include, CaChing::WriteThrough
4
+ ActiveRecord::Relation.send :include, CaChing::ReadThrough
5
+
6
+ class ActiveRecord::Base
7
+ attr_accessor :from_cache
8
+
9
+ def from_cache?
10
+ @from_cache ||= false
11
+ end
12
+
13
+ def to_keys(options={})
14
+ was = options[:old_values] ? "_was" : ""
15
+ indexed_fields.map do |index, options|
16
+ if index.is_a?(Array)
17
+ self.class.table_name.to_s + index.map { |index| "#{index}=#{self.send("#{index}#{was}")}" }.join("&")
18
+ else
19
+ "#{self.class.table_name}:#{index}=#{self.send("#{index}#{was}")}"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,127 @@
1
+ module CaChing
2
+ module Adapters
3
+ class Redis
4
+ def initialize(options={})
5
+ @cache = ::Redis.new(options)
6
+ end
7
+
8
+ def find(query, options={})
9
+ results = nil
10
+ # where_values in the form { :field => ['=', 'value'] }
11
+ where_values = query.where
12
+ keys = where_values.map { |where_value| "#{where_value[0]}#{where_value[1][0]}#{where_value[1][1]}" }
13
+ return nil if keys.empty?
14
+
15
+ offset = query.offset || 0
16
+ limit = (query.limit || 0) + offset - 1
17
+
18
+ if keys.length <= 1
19
+ key = "#{query.table_name}:#{keys[0]}"
20
+ return nil unless @cache.exists key # the key has never been added to the cache, so it's a miss
21
+
22
+ results = @cache.zrange(key, offset, limit)
23
+ else # needs an intersection
24
+ intersection_key = "#{query.table_name}:#{keys.join "&"}"
25
+
26
+ if @cache.exists intersection_key
27
+ results = @cache.zrange(intersection_key, offset, limit)
28
+ else
29
+ return nil unless keys.inject(true) { |memo, key| @cache.exists("#{query.table_name}:#{key}") && memo }
30
+ @cache.zinterstore intersection_key, *keys.map { |key| "#{query.table_name}:#{key}" }
31
+ @cache.zrange(intersection_key, offset, limit)
32
+ @cache.del(intersection_key) unless options[:keep_intersection]
33
+ end
34
+ end
35
+
36
+ inflate(results, :for => query)
37
+ end
38
+
39
+ def insert(objects, options={})
40
+ key = nil
41
+ if options[:for]
42
+ query = options[:for]
43
+ if (where_values = query.where).length == 1
44
+ where_value = query.where.first
45
+ key = "#{query.table_name}:#{where_value[0]}#{where_value[1][0]}#{where_value[1][1]}"
46
+ else
47
+ nil
48
+ end
49
+ elsif options[:at]
50
+ key = options[:at]
51
+ end
52
+
53
+ return nil if key.nil?
54
+
55
+ deflated = deflate_with_score(objects)
56
+ deflated.each do |object_and_score|
57
+ @cache.zadd key, *object_and_score
58
+ end
59
+
60
+ objects
61
+ end
62
+
63
+ def update(object, options={})
64
+ old_keys = object.to_keys(:old_values => true).select { |key| @cache.exists(key) }
65
+ new_keys = object.to_keys(:old_values => false).select { |key| @cache.exists(key) }
66
+
67
+ @cache.multi do
68
+ old_keys.each do |key|
69
+ destroy(object, :at => key, :old_values => true)
70
+ end
71
+
72
+ new_keys.each do |key|
73
+ insert([object], :at => key)
74
+ end
75
+ end
76
+ end
77
+
78
+ def destroy(object, options={})
79
+ @cache.zrem(options[:at], deflate(object, :old_values => options[:old_values]))
80
+ end
81
+
82
+ def clear!
83
+ @cache.flushdb
84
+ end
85
+
86
+ def inflate(objects, options={})
87
+ return nil if objects.nil?
88
+
89
+ unless options[:for]
90
+ return objects
91
+ else
92
+ objects.map do |object|
93
+ obj = options[:for].klass.new
94
+ attributes = ActiveSupport::JSON.decode(object)
95
+ attributes.each do |attr, value|
96
+ obj.send(:"#{attr}=", value)
97
+ end
98
+ obj.changed_attributes.clear
99
+ obj.send(:instance_variable_set, "@new_record", false)
100
+ obj
101
+ end
102
+ end
103
+ end
104
+
105
+ def deflate(object, options={})
106
+ attributes = if options[:old_values]
107
+ object.attributes.keys.inject({}) do |attributes, key|
108
+ attributes[key] = object.send(:"#{key}_was")
109
+ attributes
110
+ end
111
+ else
112
+ object.attributes
113
+ end
114
+
115
+ ActiveSupport::JSON.encode(attributes)
116
+ end
117
+
118
+ def deflate_with_score(objects, options={})
119
+ return nil if objects.nil?
120
+
121
+ score_method = options[:sorted_by] || :id
122
+
123
+ objects.map { |object| [object.send(score_method).to_i, deflate(object)] }
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,23 @@
1
+ module CaChing
2
+ module Configuration
3
+ def configure
4
+ yield self
5
+ end
6
+
7
+ def cache=(cache)
8
+ @cache = cache
9
+ end
10
+
11
+ def cache
12
+ @cache
13
+ end
14
+
15
+ def disabled?
16
+ @disabled
17
+ end
18
+
19
+ def disabled=(d)
20
+ @disabled = d
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ class Array
2
+ attr_accessor :from_cache
3
+
4
+ def from_cache?
5
+ @from_cache ||= false
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module CaChing
2
+ class InvalidOptionError < StandardError; end
3
+ end
@@ -0,0 +1,28 @@
1
+ module CaChing
2
+ module Index
3
+ extend ActiveSupport::Concern
4
+
5
+ VALID_OPTIONS = [:order, :ttl, :limit]
6
+
7
+ module ClassMethods
8
+ def index(field_name, options={})
9
+ raise CaChing::InvalidOptionError unless options.keys.inject(true) { |flag, key| flag && CaChing::Index::VALID_OPTIONS.include?(key) }
10
+ indexed_fields[field_name] = options
11
+ end
12
+
13
+ def indexes?(field_name)
14
+ indexed_fields.has_key?(field_name)
15
+ end
16
+
17
+ def indexed_fields
18
+ @_indexed_fields ||= {}
19
+ end
20
+ end
21
+
22
+ module InstanceMethods
23
+ def indexed_fields
24
+ self.class.indexed_fields
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,106 @@
1
+ module CaChing
2
+ module Query
3
+ class Abstract
4
+ # Unceremoniously taken from cache-money
5
+ AND = /\s+AND\s+/i
6
+ OR = /\s+OR\s+/i
7
+ TABLE_AND_COLUMN = /(?:(?:`|")?(\w+)(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
8
+ VALUE = /'?(\d+|\?|(?:(?:[^']|'')*))'?/ # Matches: 123, ?, '123', '12''3'
9
+ KEY_CMP_VALUE = /^\(?#{TABLE_AND_COLUMN}\s+(=|<|<=|>|>=)\s+#{VALUE}\)?$/ # Matches: KEY = VALUE, (KEY = VALUE)
10
+ ORDER = /^#{TABLE_AND_COLUMN}\s*(ASC|DESC)?$/i # Matches: COLUMN ASC, COLUMN DESC, COLUMN
11
+
12
+ attr_accessor :klass
13
+
14
+ def initialize(active_record_collection, options={})
15
+ self.tap do
16
+ @collection = active_record_collection
17
+ @sql = active_record_collection.to_sql
18
+ @klass = active_record_collection.klass
19
+ end
20
+ end
21
+
22
+ def table_name
23
+ @collection.table_name
24
+ end
25
+
26
+ def where
27
+ raise UncacheableConditionError unless cacheable?
28
+
29
+ @collection.where_values.inject({}) do |hash, value|
30
+ if value.respond_to? :left
31
+ left = value.left.name.to_sym
32
+ right = value.right
33
+ if right == '?'
34
+ right = @collection.bind_values.find { |value| value.first.name == left.to_s }[1]
35
+ end
36
+
37
+ hash[left] = ['=', right]
38
+ else
39
+ value.split(AND).each do |value|
40
+ match = KEY_CMP_VALUE.match(value).captures
41
+ left, comparator, right = match[1].to_sym, match[2], match[3]
42
+
43
+ hash[left] = [comparator, right]
44
+ end
45
+ end
46
+
47
+ @where = hash
48
+ end
49
+ end
50
+
51
+ def order
52
+ order_values = @collection.order_values.map { |v| v.split(',').map { |v| v.strip } }.flatten
53
+
54
+ order_values.inject({}) do |hash, value|
55
+ match = ORDER.match(value).captures
56
+ hash[match[1].to_sym] = match[2] == "ASC" ? :asc : :desc
57
+
58
+ hash
59
+ end
60
+ end
61
+
62
+ def limit
63
+ @collection.limit_value
64
+ end
65
+
66
+ def offset
67
+ @collection.offset_value
68
+ end
69
+
70
+ def calculation?
71
+ false
72
+ end
73
+
74
+ # Formats the query to a key for the value store. Takes the form
75
+ # table_name:encode(field1, [operator1, value1])&encode(field2, [operator2, value2])...
76
+ def to_key
77
+ "#{table_name}:#{where.map { |field, operator_and_value| encode(field, operator_and_value) } * "&" }"
78
+ end
79
+
80
+ def primary_key?
81
+ @where ||= where
82
+ @where.keys.length == 1 && @where.keys.include?(@collection.primary_key.to_sym)
83
+ end
84
+
85
+ private
86
+ # Encodes string to the form "field_name=value" where '=' can be any arbitrary
87
+ # operator.
88
+ #
89
+ # @example
90
+ # encode('name', ['=', 'Andrew']) # => "name='Andrew'"
91
+ #
92
+ # @example
93
+ # encode('age', ['>=', 21]) # => "age>=21"
94
+ def encode(field, operator_and_value)
95
+ operator, value = operator_and_value
96
+ "#{field}#{operator}\"#{value.to_s.gsub('"', '\"')}\""
97
+ end
98
+
99
+ def cacheable?
100
+ !(@collection.to_sql =~ OR)
101
+ end
102
+ end
103
+
104
+ class UncacheableConditionError < StandardError; end
105
+ end
106
+ end
@@ -0,0 +1,6 @@
1
+ module CaChing
2
+ module Query
3
+ class Calculation < Abstract
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ module CaChing
2
+ module Query
3
+ class Select < Abstract
4
+ end
5
+ end
6
+ end