sequel-query-cache 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a295f7dea37913880f5a662798aa375f17f80314
4
+ data.tar.gz: b953a768fd1a58750cf68b5b99b3e1403fc82ed0
5
+ SHA512:
6
+ metadata.gz: 569ecbb3f9433d70e5c39dece54a2f68efb615a6a323fe145c5d46ccc87664fc7d17cced2d764e934dc02b77a032894eac87dc441c824cf44bfc1275fa97f65d
7
+ data.tar.gz: 19e2ff8e34d236c8e5a724fca56b1cf8392416ff1e6251dd332b18d8b665e4dee18679416ae04491454aa07f1076c588203a9cdf90f8b3fc694774f4a11a4507
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
4
+
5
+ group :development do
6
+ gem 'pry'
7
+ end
File without changes
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2012-2013 Sho Kusano <rosylilly>
2
+ Copyright (c) 2013 Joshua Hansen <binarypaladin>
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining
5
+ a copy of this software and associated documentation files (the
6
+ "Software"), to deal in the Software without restriction, including
7
+ without limitation the rights to use, copy, modify, merge, publish,
8
+ distribute, sublicense, and/or sell copies of the Software, and to
9
+ permit persons to whom the Software is furnished to do so, subject to
10
+ the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,112 @@
1
+ Sequel Query Cache
2
+ ==================
3
+
4
+ Sequel Query Cache is a Sequel model plugin that allows the results of Sequel datasets to be cached in a key-value store like Memcached or Redis. This plugin is flexible and can easily be adapted to other key-value stores.
5
+
6
+ Results are serialized for storage by default using JSON or [MessagePack](http://msgpack.org/) if the [MessagePack gem](https://github.com/msgpack/msgpack-ruby) is available. This is also flexible. Any object with a very simple interface can be used to serialize and unserialize data.
7
+
8
+ Sequel Query Cache was forked from Sho Kusano's [sequel-cacheable gem](https://github.com/rosylilly/sequel-cacheable) and while it has seen substantial internal architectural changes, it maintains the original spirit of being easy to adapt to multiple cache stores and serialization options.
9
+
10
+ Installation
11
+ ------------
12
+
13
+ Sequel Query Cache requires [Sequel](https://github.com/jeremyevans/sequel) and one of the following gems for accessing cache store:
14
+
15
+ * [Redis](https://rubygems.org/gems/redis)
16
+ * [Hiredis](https://rubygems.org/gems/hiredis)
17
+ * [Memcached](https://rubygems.org/gems/memcache)
18
+ * [Dalli](https://rubygems.org/gems/dalli)
19
+
20
+ Additionally, using [MessagePack](https://github.com/msgpack/msgpack-ruby) is strongly encouraged over the JSON default and is necessary when caching binary data.
21
+
22
+ Configuration
23
+ -------------
24
+
25
+ First, initialize a cache client.
26
+
27
+ Using Dalli:
28
+
29
+ ```ruby
30
+ CACHE_CLIENT = Dalli::Client.new('localhost:11211')
31
+ ```
32
+
33
+ Using Redis:
34
+
35
+ ```ruby
36
+ CACHE_CLIENT = Redis.new(host: 'localhost', port: 6379)
37
+ ```
38
+
39
+ Then, apply the plugin to all models:
40
+
41
+ ```ruby
42
+ Sequel::Model.plugin :query_cache, CACHE_CLIENT
43
+ ```
44
+
45
+ Or just a select few:
46
+
47
+ ```ruby
48
+ MyModel.plugin :query_cache, CACHE_CLIENT
49
+ ```
50
+
51
+ Configuration Options
52
+ ---------------------
53
+
54
+ The plugin method also accepts a hash of options as a final argument, like so:
55
+
56
+ ```ruby
57
+ Sequel::Model.plugin :query_cache, CACHE_CLIENT, ttl: 3600
58
+ ```
59
+
60
+ The current options are:
61
+
62
+ * **ttl**: Time to live. The amount of time before a given cache expires automatically.
63
+ * **serializer**: An object that responds to serialize and unserialize methods for converting Sequel result hashs and models to serialized strings.
64
+ * **cache_by_default**: See below.
65
+
66
+ Cached Datasets
67
+ ---------------
68
+
69
+ When a dataset is set to have its results cached, if a method that would return results is called (e.g. #first or #all) a check will be made to see if there are any cached results. If there are, the cached results will be used. If there are not, the results will be pulled from the database, cached in the store and then passed to the user.
70
+
71
+ Whether or not dataset results are cached is determined in one of two ways. The first is to explicitly set a dataset to be cached. For example:
72
+
73
+ ```ruby
74
+ MyModel.cached.all
75
+ ```
76
+
77
+ This will check the cache for results and set them if they do not exist. Caching be be explicitly ignored as well:
78
+
79
+ ```ruby
80
+ MyModel.uncached.all
81
+ ```
82
+
83
+ The second way a determination to cache a dataset is made is by the options set by cache_by_default. By default, these are set to cache the results of any query which has a LIMIT clause set to 1.
84
+
85
+ For more details on how cache_by_default, see the documentation for Sequel::Plugins::QueryCache and Sequel::Plugins::QueryCache::DatasetMethods.
86
+
87
+ Caches are automatically deleted when a dataset is updated or deleted.
88
+
89
+ Cached Models
90
+ ---------------
91
+
92
+ Models have a few additional features on top of their respective dataset. Models can have their caches explicitly set or deleted by calling #cache! or #uncache! on a model instance.
93
+
94
+ Additionally, models have an #after_save hook that updates caches associated with model instances.
95
+
96
+ TODO
97
+ ----
98
+
99
+ * The testing suite is completely broken and needs to be updated.
100
+ * Additional documentation and commenting needs to be added to the source, particularly for the functionality of cache_by_default.
101
+ * There is basically no need for this plugin to require the use of models and could be used purely as a dataset extension with a tiny model plugin built on top. This should be implemented at some point.
102
+
103
+ Thanks
104
+ ------
105
+
106
+ * [Sho Kusano](https://github.com/rosylilly)
107
+ * [Jeremy Watkins](https://github.com/vegasje)
108
+
109
+ Copyright
110
+ ---------
111
+
112
+ Copyright (c) 2013 Joshua Hansen. See LICENSE for further details.
@@ -0,0 +1,2 @@
1
+ # encoding: utf-8
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ require 'sequel'
3
+ require 'sequel-query-cache/version'
4
+ require 'sequel-query-cache/driver'
5
+ require 'sequel-query-cache/class_methods'
6
+ require 'sequel-query-cache/instance_methods'
7
+ require 'sequel-query-cache/dataset_methods'
8
+
9
+ module Sequel::Plugins
10
+ module QueryCache
11
+ def self.configure(model, store, opts={})
12
+ model.instance_eval do
13
+ @cache_options = {
14
+ :ttl => 3600,
15
+ :cache_by_default => {
16
+ :always => false,
17
+ :if_limit => 1
18
+ }
19
+ }.merge(opts)
20
+
21
+ @cache_driver = Driver.from_store(
22
+ store,
23
+ :serializer => @cache_options.delete(:serializer)
24
+ )
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ # coding: utf-8
2
+ module Sequel::Plugins
3
+ module QueryCache
4
+ module ClassMethods
5
+ attr_reader :cache_driver, :cache_options
6
+
7
+ def cached(opts={})
8
+ dataset.cached(opts)
9
+ end
10
+
11
+ def not_cached
12
+ dataset.not_cached
13
+ end
14
+
15
+ def default_cached
16
+ dataset.default_cached
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,254 @@
1
+ # coding: utf-8
2
+ require 'digest/md5'
3
+
4
+ module Sequel::Plugins
5
+ module QueryCache
6
+ module DatasetMethods
7
+ CACHE_BY_DEFAULT_PROC = lambda do |ds, opts|
8
+ if ds.opts[:limit] && opts[:if_limit]
9
+ return true if
10
+ (opts[:if_limit] == true) ||
11
+ (opts[:if_limit] >= ds.opts[:limit])
12
+ end
13
+
14
+ false
15
+ end
16
+
17
+ # Returns the model's cache driver.
18
+ #--
19
+ # TODO: Caching should be modified to be a database/dataset extension with
20
+ # a tiny plugin for the models. However, this works just fine for now.
21
+ #++
22
+ def cache_driver
23
+ model.cache_driver
24
+ end
25
+
26
+ # Copies the model's +cache_options+ and merges them with options provided
27
+ # by +opts+ if any are provided. Returns the current cache_options.
28
+ #
29
+ # <tt>@cache_options</tt> is cloned when the dataset is cloned.
30
+ def cache_options(opts=nil)
31
+ @cache_options ||= model.cache_options
32
+ @cache_options = @cache_options.merge(opts) if opts
33
+ @cache_options
34
+ end
35
+
36
+ # Determines whether or not to cache a dataset based on the configuration
37
+ # settings of the plugin.
38
+ #--
39
+ # TODO: Specify a place to find those settings. However, where those are
40
+ # applied is currently in flux. Also, further document how this process
41
+ # actually works.
42
+ #++
43
+ def is_cacheable_by_default?
44
+ cache_by_default = cache_options[:cache_by_default]
45
+ return false unless cache_by_default
46
+ return true if cache_by_default[:always]
47
+ proc = cache_by_default[:proc] || CACHE_BY_DEFAULT_PROC
48
+ proc.call(self, cache_by_default)
49
+ end
50
+
51
+ # Determines whether or not a dataset should be cached. If
52
+ # <tt>@is_cacheable</tt> is set that value will be returned, otherwise the
53
+ # default value will be returned by #is_cacheable_by_default?
54
+ def is_cacheable?
55
+ defined?(@is_cacheable) ? @is_cacheable : is_cacheable_by_default?
56
+ end
57
+
58
+ # Sets the value for <tt>@is_cacheable</tt> which is used as the return
59
+ # value from #is_cacheable?. <tt>@is_cacheable</tt> is cloned when the
60
+ # dataset is cloned.
61
+ #
62
+ # *Note:* In general, #cached and #not_cached should be used to set this
63
+ # value. This method exists primarily for their use.
64
+ def is_cacheable=(is_cacheable)
65
+ @is_cacheable = !!is_cacheable
66
+ end
67
+
68
+ # Clones the current dataset, sets it to be cached and returns the new
69
+ # dataset. This is useful for chaining purposes:
70
+ #
71
+ # dataset.where(column1: true).order(:column2).cached.all
72
+ #
73
+ # In the above example, the data would always be pulled from the cache or
74
+ # cached if it wasn't already. The value of <tt>@is_cacheable</tt> is
75
+ # cloned when a dataset is cloned, so the following example would also
76
+ # have the same result:
77
+ #
78
+ # dataset.cached.where(column1: true).order(:column2).all
79
+ #
80
+ # +opts+ is passed to #cache_options on the new dataset.
81
+ def cached(opts=nil)
82
+ c = clone
83
+ c.cache_options(opts)
84
+ c.is_cacheable = true
85
+ c
86
+ end
87
+
88
+ # Clones the current dataset, sets it to not be cached and returns the new
89
+ # dataset. See #cached for further details and examples.
90
+ def not_cached
91
+ c = clone
92
+ c.is_cacheable = false
93
+ c
94
+ end
95
+
96
+ # Clones the current dataset, returns the caching state to whatever
97
+ # would be default for that dataset and returns the new dataset. See
98
+ # #cached for further details and examples.
99
+ #
100
+ # *Note:* This is the "proper" way to clear <tt>@is_cacheable</tt> once
101
+ # it's been set.
102
+ def default_cached
103
+ if defined? @is_cacheable
104
+ c = clone
105
+ c.remove_instance_variable(:@is_cacheable)
106
+ c
107
+ else
108
+ self
109
+ end
110
+ end
111
+
112
+ # Creates a default cache key, which is an MD5 base64 digest of the
113
+ # the literal select SQL with +Sequel:+ added as a prefix. This value is
114
+ # memoized because assembling the SQL string and hashing it every time
115
+ # this method gets called is obnoxious.
116
+ def default_cache_key
117
+ @default_cache_key ||= "Sequel:#{Digest::MD5.base64digest(sql)}"
118
+ end
119
+
120
+ # Returns the default cache key if a manual cache key has not been set.
121
+ # The cache key is used by the storage engines to retrieve cached data.
122
+ # The default will suffice in almost all instances.
123
+ def cache_key
124
+ @cache_key || default_cache_key
125
+ end
126
+
127
+ # Sets a manual cache key for a dataset that overrides the default MD5
128
+ # hash. This key has no +Sequel:+ prefix, so if that's important, remember
129
+ # to add it manually.
130
+ #
131
+ # *Note:* Setting the cache key manually is *NOT* inherited by cloned
132
+ # datasets since keys are presumed to be for the current dataset and any
133
+ # changes, such as where clauses or limits, should result in a new key. In
134
+ # general, you shouldn't change the cache key unless you have a really
135
+ # good reason for doing so.
136
+ def cache_key=(cache_key)
137
+ @cache_key = cache_key ? cache_key.to_s : nil
138
+ end
139
+
140
+ def clear_cache_keys!
141
+ remove_instance_variable(:@default_cache_key) if defined? @default_cache_key
142
+ remove_instance_variable(:@cache_key) if defined? @cache_key
143
+ end
144
+
145
+ # Gets the cache value using the current dataset's key and logs the
146
+ # action. The underlying driver should return +nil+ in the event that
147
+ # there is no cached data. Also logs whether there was a hit or miss on
148
+ # the cache.
149
+ def cache_get
150
+ db.log_info("CACHE GET: #{cache_key}")
151
+ cached_rows = cache_driver.get(cache_key)
152
+ db.log_info("CACHE #{cached_rows ? 'HIT' : 'MISS'}: #{cache_key}")
153
+ cached_rows
154
+ end
155
+
156
+ # Sets the cache value using the current dataset's key and logs the
157
+ # action. In general, this method should not be called directly. It's
158
+ # exposed because model instances need access to it.
159
+ #
160
+ # An +opts+ hash can be passed to override any default options being sent
161
+ # to the driver. The most common use for this would be to modify the +ttl+
162
+ # for a cache. However, this should probably be done using #cached rather
163
+ # than doing anything directly via this method.
164
+ def cache_set(value, opts={})
165
+ db.log_info("CACHE SET: #{cache_key}")
166
+ cache_driver.set(cache_key, value, opts.merge(cache_options))
167
+ end
168
+
169
+ # Deletes the cache value using the current dataset's key and logs the
170
+ # action.
171
+ def cache_del
172
+ db.log_info("CACHE DEL: #{cache_key}")
173
+ cache_driver.del(cache_key)
174
+ end
175
+
176
+ def cache_clear_on_update
177
+ @cache_clear_on_update.nil? ? true : @cache_clear_on_update
178
+ end
179
+
180
+ def cache_clear_on_update=(v)
181
+ @cache_clear_on_update = !!v
182
+ end
183
+
184
+ # Overrides the dataset's existing +update+ method. Deletes an existing
185
+ # cache after a successful update.
186
+ def update(values={}, &block)
187
+ result = super
188
+ cache_del if is_cacheable? && cache_clear_on_update
189
+ result
190
+ end
191
+
192
+ # Overrides the dataset's existing +delete+ method. Deletes an existing
193
+ # cache after a successful delete.
194
+ def delete(&block)
195
+ result = super
196
+ cache_del if is_cacheable?
197
+ result
198
+ end
199
+
200
+ # Overrides the dataset's existing +fetch_rows+ method. If the dataset is
201
+ # cacheable it will do one of two things:
202
+ #
203
+ # If a cache exists it will yield the cached rows rather query the
204
+ # database.
205
+ #
206
+ # If a cache does not exist it will query the database, store the results
207
+ # in an array, cache those and then yield the results like the original
208
+ # method would have.
209
+ #
210
+ # *Note:* If you're using PostgreSQL, or another database where +each+
211
+ # iterates with the cursor rather over the dataset results, you'll lose
212
+ # that functionality when caching is enabled for a query since the entire
213
+ # result is iterated first before it is yielded. If that behavior is
214
+ # important, remember to disable caching for that particular query.
215
+ def fetch_rows(sql)
216
+ if is_cacheable?
217
+ if cached_rows = cache_get
218
+ # Symbolize the row keys before yielding as they're often strings
219
+ # when the data is deserialized. Sequel doesn't play nice with
220
+ # string keys.
221
+ cached_rows.each{|r| yield r.reduce({}){|h,v| h[v[0].to_sym]=v[1]; h}}
222
+ else
223
+ cached_rows = []
224
+ super(sql){|r| cached_rows << r}
225
+ cache_set(cached_rows)
226
+ cached_rows.each{|r| yield r}
227
+ end
228
+ else
229
+ super
230
+ end
231
+ end
232
+
233
+ # Sets self as the source_dataset on a result if that result supports
234
+ # source datasets. While it can almost certainly be presumed the result
235
+ # will, if the dataset's row_proc has been modified for some reason the
236
+ # result might be different than expected.
237
+ def each
238
+ super do |r|
239
+ r.source_dataset = self if r.respond_to? :source_dataset=
240
+ yield r
241
+ end
242
+ end
243
+
244
+ # Overrides the dataset's existing clone method. Clones the existing
245
+ # dataset but clears any manually set cache key and the memoized default
246
+ # cache key to ensure it's regenerated by the new dataset.
247
+ def clone(opts=nil)
248
+ c = super(opts)
249
+ c.clear_cache_keys!
250
+ c
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,58 @@
1
+ # coding: utf-8
2
+ module Sequel::Plugins
3
+ module QueryCache
4
+ class Driver
5
+ def self.from_store(store, opts={})
6
+ case store.class.name
7
+ when 'Memcache'
8
+ require 'sequel-query-cache/driver/memcache'
9
+ MemcacheDriver.new(store, opts)
10
+ when 'Dalli::Client'
11
+ require 'sequel-query-cache/driver/dalli'
12
+ DalliDriver.new(store, opts)
13
+ else
14
+ Driver.new(store, opts)
15
+ end
16
+ end
17
+
18
+ attr_reader :store, :serializer
19
+
20
+ def initialize(store, opts={})
21
+ @store = store
22
+ @serializer = opts[:serializer] || _default_serializer
23
+ end
24
+
25
+ def get(key)
26
+ val = store.get(key)
27
+ val ? serializer.deserialize(val) : nil
28
+ end
29
+
30
+ def set(key, val, opts={})
31
+ store.set(key, serializer.serialize(val))
32
+ expire(key, opts[:ttl]) unless opts[:ttl].nil?
33
+ val
34
+ end
35
+
36
+ def del(key)
37
+ store.del(key)
38
+ nil
39
+ end
40
+
41
+ def expire(key, time)
42
+ store.expire(key, time)
43
+ end
44
+
45
+ private
46
+
47
+ def _default_serializer
48
+ if defined? MessagePack
49
+ require 'sequel-query-cache/serializer/message_pack'
50
+ Serializer::MessagePack
51
+ else
52
+ require 'sequel-query-cache/serializer/json'
53
+ Serializer::JSON
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,18 @@
1
+ # coding: utf-8
2
+ module Sequel::Plugins
3
+ module QueryCache
4
+ class DalliDriver < Driver
5
+ def del(key)
6
+ store.delete(key)
7
+ end
8
+
9
+ def expire(key, time)
10
+ if time > 0
11
+ store.touch(key, time)
12
+ else
13
+ store.delete(key)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ # coding: utf-8
2
+ module Sequel::Plugins
3
+ module QueryCache
4
+ class MemcacheDriver < Driver
5
+ def del(key)
6
+ store.delete(key)
7
+ end
8
+
9
+ def expire(key, time)
10
+ store.set(key, store.get(key), time)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,72 @@
1
+ # coding: utf-8
2
+ module Sequel::Plugins
3
+ module QueryCache
4
+ module InstanceMethods
5
+ # For the purpose of caching, it's helpful to have the dataset that
6
+ # is actually responsible for creating the model instance since it's
7
+ # likely that if the instance is updated you'll want the dataset related
8
+ # to it to be cleaned up. See #recache_source_dataset! for further
9
+ # information.
10
+ attr_accessor :source_dataset
11
+
12
+ def before_save
13
+ # Since the cache will be updated after the save is complete there's no
14
+ # reason to have it deleted by the update process.
15
+ this.cache_clear_on_update = false
16
+ super
17
+ end
18
+
19
+ def after_save
20
+ super
21
+ recache_source_dataset!
22
+ cache!
23
+ end
24
+
25
+ def cache_key
26
+ this.cache_key
27
+ end
28
+
29
+ def cache!(opts={})
30
+ this.cache_set([self], opts) if this.is_cacheable?
31
+ self
32
+ end
33
+
34
+ def uncache!
35
+ this.cache_del if this.is_cacheable?
36
+ source_dataset.cache_del if source_dataset_cache?
37
+ self
38
+ end
39
+
40
+ def to_msgpack(io=nil)
41
+ values.to_msgpack(io)
42
+ end
43
+
44
+ private
45
+ # There are many instances where the dataset that creates a model instance
46
+ # is not equal to #this. The two most common instances are when a model
47
+ # has a unique column that is used on a regular basis to fetch records
48
+ # (e.g. an email address in a User model) or a foreign key.
49
+ #
50
+ # In the event that the source dataset is guaranteed to return only one
51
+ # result (has a limit statement of 1) it will be cached. If it is not, the
52
+ # related cached will be cleared in an attempt to clean up potentially
53
+ # stale queries.
54
+ def recache_source_dataset!
55
+ if source_dataset_cache?
56
+ if source_dataset.opts[:limit] == 1
57
+ source_dataset.cache_set([self])
58
+ else
59
+ source_dataset.cache_del
60
+ end
61
+ end
62
+ end
63
+
64
+ def source_dataset_cache?
65
+ source_dataset &&
66
+ (source_dataset != this) &&
67
+ source_dataset.respond_to?(:is_cacheable?) &&
68
+ source_dataset.is_cacheable?
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,24 @@
1
+ # coding: utf-8
2
+ require 'json'
3
+
4
+ module Sequel::Plugins
5
+ module QueryCache
6
+ module Serializer
7
+ # While this works, if you're using binary data at all, it's not a great
8
+ # idea and MessagePack is faster.
9
+ module JSON
10
+ def self.serialize(obj)
11
+ binding.pry
12
+ obj.to_json
13
+ end
14
+
15
+ # Keys are specifically *NOT* symbolized here. This is done by
16
+ # Sequel::Plugins::Cacheable::DatasetMethods#fetch_rows since only the
17
+ # top level needs to have symbolized keys for Sequel's purposes.
18
+ def self.deserialize(string)
19
+ ::JSON.parse(string)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ require 'msgpack'
3
+
4
+ module Sequel::Plugins
5
+ module QueryCache
6
+ module Serializer
7
+ module MessagePack
8
+ def self.serialize(obj)
9
+ obj.to_msgpack
10
+ end
11
+
12
+ def self.deserialize(string)
13
+ ::MessagePack.unpack(string)
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ # Adds #to_msgpack to some common classes for packing purposes.
21
+ [BigDecimal, Bignum, Date, Time, Sequel::SQLTime].each do |klass|
22
+ unless klass.instance_methods.include? :to_msgpack
23
+ klass.send(:define_method, :to_msgpack) {|io=nil| to_s.to_msgpack(io)}
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ # coding: utf-8
2
+ module Sequel
3
+ module Plugins
4
+ module QueryCache
5
+ MAJOR_VERSION = 0
6
+ MINOR_VERSION = 2
7
+ TINY_VERSION = 1
8
+ VERSION = [MAJOR_VERSION, MINOR_VERSION, TINY_VERSION].join('.')
9
+
10
+ def self.version; VERSION; end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,28 @@
1
+ require './lib/sequel-query-cache/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'sequel-query-cache'
5
+ s.version = Sequel::Plugins::QueryCache::VERSION
6
+ s.license = 'MIT'
7
+
8
+ s.authors = ['Joshua Hansen']
9
+ s.email = ['joshua@amicus-tech.com']
10
+ s.homepage = 'https://github.com/binarypaladin/sequel-query-cache'
11
+
12
+ s.summary = 'A plugin for Sequel that allows dataset results to be cached in Memcached or Redis.'
13
+ s.description = s.summary
14
+
15
+ s.files = Dir.glob('lib/**/*') + [
16
+ 'Gemfile',
17
+ 'History.md',
18
+ 'LICENSE',
19
+ 'Rakefile',
20
+ 'README.md',
21
+ 'sequel-query-cache.gemspec',
22
+ ]
23
+
24
+ s.test_files = Dir.glob('spec/**/*')
25
+ s.require_paths = ['lib']
26
+
27
+ s.add_dependency 'sequel', ['>= 3.42', '< 5.0']
28
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sequel::Plugins::QueryCache::DalliDriver do
4
+ let(:store) { DalliCli }
5
+
6
+ include_examples :driver
7
+ end
8
+
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sequel::Plugins::QueryCache::MemcacheDriver do
4
+ let(:store) { MemcacheCli }
5
+
6
+ include_examples :driver
7
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sequel::Plugins::QueryCache::RedisDriver do
4
+ let(:store) { RedisCli }
5
+
6
+ include_examples :driver
7
+ end
8
+
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sequel::Plugins::QueryCache::Driver do
4
+ let(:store) { RedisCli }
5
+
6
+ include_examples :driver
7
+
8
+ describe '.factory' do
9
+ subject { described_class.factory(store) }
10
+
11
+ context 'when Memcache' do
12
+ let(:store) { MemcacheCli }
13
+
14
+ it { should be_a(Sequel::Plugins::QueryCache::MemcacheDriver) }
15
+ end
16
+
17
+ context 'when Dalli' do
18
+ let(:store) { DalliCli }
19
+
20
+ it { should be_a(Sequel::Plugins::QueryCache::DalliDriver) }
21
+ end
22
+
23
+ context 'when Redis' do
24
+ let(:store) { RedisCli }
25
+
26
+ it { should be_a(Sequel::Plugins::QueryCache::RedisDriver) }
27
+ end
28
+
29
+ context 'when Unkown Store' do
30
+ let(:store) { mock }
31
+
32
+ it { should be_a(Sequel::Plugins::QueryCache::Driver) }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe Sequel::Plugins::QueryCache do
4
+ it { should be_const_defined(:VERSION) }
5
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe DalliModel do
4
+ it_should_behave_like :cacheable
5
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe MemcacheModel do
4
+ it_should_behave_like :cacheable
5
+ end
@@ -0,0 +1,5 @@
1
+ require 'spec_helper'
2
+
3
+ describe RedisModel do
4
+ it_should_behave_like :cacheable
5
+ end
@@ -0,0 +1,102 @@
1
+ shared_examples :driver do
2
+ let(:pack_lib) { MessagePack }
3
+ subject(:driver) { described_class.new(store, pack_lib) }
4
+
5
+ let(:key) { 'cache_key' }
6
+ let(:val) { 100 }
7
+
8
+ describe '#set' do
9
+ subject { driver.set(key, val) }
10
+
11
+ it { should == val }
12
+
13
+ it 'should be stored in cache' do
14
+ driver.set(key, val)
15
+ store.get(key).should_not be_nil
16
+ end
17
+
18
+ context 'with expire' do
19
+ subject { driver.set(key, val, -1) }
20
+
21
+ it 'should be expired cache' do
22
+ store.get(key).should be_nil
23
+ end
24
+ end
25
+ end
26
+
27
+ describe '#get' do
28
+ before do
29
+ driver.set(key, val)
30
+ end
31
+
32
+ let(:get_key) { key }
33
+
34
+ subject { driver.get(get_key) }
35
+
36
+ context 'be found key' do
37
+ it { should == val }
38
+ end
39
+
40
+ context 'be not found key' do
41
+ let(:get_key) { 'not_found' }
42
+
43
+ it { should be_nil }
44
+ end
45
+ end
46
+
47
+ describe '#del' do
48
+ before do
49
+ driver.set(key, val)
50
+ end
51
+
52
+ subject(:del_method) { driver.del(key) }
53
+
54
+ it { should be_nil }
55
+
56
+ it 'should be deleted cache' do
57
+ store.get(key).should_not be_nil
58
+ del_method
59
+ store.get(key).should be_nil
60
+ end
61
+ end
62
+
63
+ describe '#expire' do
64
+ before do
65
+ driver.set(key, val)
66
+ end
67
+
68
+ it 'should be expired cache' do
69
+ store.get(key).should_not be_nil
70
+ driver.expire(key, -1)
71
+ store.get(key).should be_nil
72
+ end
73
+ end
74
+
75
+ describe '#fetch' do
76
+ subject(:fetch) do
77
+ driver.fetch(key) { val }
78
+ end
79
+
80
+ context 'be found key' do
81
+ before do
82
+ driver.set(key, val)
83
+ end
84
+
85
+ it { should == val }
86
+
87
+ it 'should not call #set' do
88
+ store.should_not_receive(:set)
89
+ fetch
90
+ end
91
+ end
92
+
93
+ context 'be not found key' do
94
+ it { should == val }
95
+
96
+ it 'should call #set' do
97
+ store.should_receive(:set).and_call_original
98
+ fetch
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,146 @@
1
+ require 'spec_helper'
2
+
3
+ shared_examples :cacheable do
4
+ let(:model) { described_class }
5
+
6
+ before do
7
+ 3.times do
8
+ model.create(
9
+ :string => Forgery::Basic.text,
10
+ :integer => rand(255),
11
+ :float => rand(3000).to_f / 10.0,
12
+ :bignum => (2 + rand(10)) ** 100,
13
+ :numeric => BigDecimal(rand(100).to_s),
14
+ :date => Date.today,
15
+ :datetime => DateTime.now,
16
+ :time => Time.now,
17
+ :bool => rand(2).odd?
18
+ )
19
+ end
20
+ end
21
+
22
+ describe 'ClassMethods' do
23
+ let(:key) { 'cache_key' }
24
+ let(:value) { model.first }
25
+
26
+ describe '#cache_set' do
27
+ subject(:cache_set) { model.cache_set(key, value) }
28
+
29
+ it { should == value }
30
+
31
+ it 'should be stored cache' do
32
+ cache_set
33
+ model.cache_driver.get("#{model.name}:#{key}").should == value
34
+ end
35
+ end
36
+
37
+ describe '#cache_get' do
38
+ before do
39
+ model.cache_set(key, value)
40
+ end
41
+
42
+ subject { model.cache_get(key) }
43
+
44
+ it { should == value }
45
+ end
46
+
47
+ describe '#cache_del' do
48
+ before do
49
+ model.cache_set(key, value)
50
+ end
51
+
52
+ subject(:cache_del) { model.cache_del(key) }
53
+
54
+ it { should be_nil }
55
+
56
+ it 'should be deleted cache' do
57
+ cache_del
58
+ model.cache_get("#{model.name}:#{key}").should be_nil
59
+ end
60
+ end
61
+
62
+ describe '#cache_fetch' do
63
+ it 'should call driver#fetch' do
64
+ model.cache_driver.should_receive(:fetch).at_least(1).times.and_call_original
65
+
66
+ model.cache_fetch('test') do
67
+ value
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ describe 'DatasetMethods' do
74
+ let(:dataset) { model.dataset }
75
+
76
+ describe '#all' do
77
+ subject(:fetch_all) { dataset.all }
78
+
79
+ it { should have(3).records }
80
+
81
+ it { should == model.all }
82
+ end
83
+
84
+ describe '#first' do
85
+ subject(:fetch_first) { dataset.first }
86
+
87
+ it { should be_kind_of(model) }
88
+ end
89
+ end
90
+
91
+ describe 'InstanceMethods' do
92
+ let(:instance) { model.first }
93
+
94
+ describe '#after_initialize' do
95
+ it 'should call #cache!' do
96
+ model.any_instance.should_receive(:cache!)
97
+ model.first
98
+ end
99
+ end
100
+
101
+ describe '#after_update' do
102
+ it 'should call #recache!' do
103
+ instance.string = 'hoge'
104
+ instance.should_receive(:recache!)
105
+ instance.save
106
+ end
107
+ end
108
+
109
+ describe '#destroy' do
110
+ it 'should call #uncache!' do
111
+ instance.should_receive(:uncache!)
112
+ instance.destroy
113
+ end
114
+ end
115
+
116
+ describe '#delete' do
117
+ it 'should call #uncache!' do
118
+ instance.should_receive(:uncache!)
119
+ instance.delete
120
+ end
121
+ end
122
+
123
+ describe '#cache!' do
124
+ it 'should call .cache_set' do
125
+ instance = model.first
126
+ model.should_receive(:cache_set).with(instance.pk.to_s, instance)
127
+ instance.cache!
128
+ end
129
+ end
130
+
131
+ describe '#uncache!' do
132
+ it 'should call .cache_del' do
133
+ model.should_receive(:cache_del).at_least(1).times
134
+ instance.uncache!
135
+ end
136
+ end
137
+
138
+ describe '#recache!' do
139
+ it 'should call #uncache! and #cache!' do
140
+ instance.should_receive(:uncache!)
141
+ instance.should_receive(:cache!)
142
+ instance.recache!
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,35 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.require(:default, :test)
4
+
5
+ DB = Sequel.sqlite
6
+ DB.create_table(:spec) do
7
+ primary_key :id, :auto_increment => true
8
+ String :string
9
+ Integer :integer
10
+ Float :float
11
+ Bignum :bignum
12
+ BigDecimal :numeric
13
+ Date :date
14
+ DateTime :datetime
15
+ Time :time, :only_time=>true
16
+ TrueClass :bool
17
+ end
18
+
19
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
20
+ Dir["#{File.dirname(__FILE__)}/shared/**/*.rb"].each {|f| require f}
21
+
22
+ RSpec.configure do |config|
23
+ config.after(:each) do
24
+ RedisCli.flushall
25
+ MemcacheCli.flush_all
26
+ DalliCli.flush_all
27
+ end
28
+
29
+ config.around(:each) do |e|
30
+ DB.transaction do
31
+ e.run
32
+ raise Sequel::Rollback
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ DalliCli = Dalli::Client.new('localhost:11211')
2
+
3
+ class DalliModel < Sequel::Model(:spec)
4
+ plugin :cacheable, DalliCli, :query_cache => true
5
+ end
@@ -0,0 +1,5 @@
1
+ MemcacheCli = Memcache.new(:server => 'localhost:11211')
2
+
3
+ class MemcacheModel < Sequel::Model(:spec)
4
+ plugin :cacheable, MemcacheCli, :query_cache => true
5
+ end
@@ -0,0 +1,5 @@
1
+ RedisCli = Redis.new(:server => 'localhost:6379')
2
+
3
+ class RedisModel < Sequel::Model(:spec)
4
+ plugin :cacheable, RedisCli, :query_cache => true
5
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sequel-query-cache
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Joshua Hansen
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: sequel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.42'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '5.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '3.42'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ description: A plugin for Sequel that allows dataset results to be cached in Memcached
34
+ or Redis.
35
+ email:
36
+ - joshua@amicus-tech.com
37
+ executables: []
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - Gemfile
42
+ - History.md
43
+ - LICENSE
44
+ - README.md
45
+ - Rakefile
46
+ - lib/sequel-query-cache.rb
47
+ - lib/sequel-query-cache/class_methods.rb
48
+ - lib/sequel-query-cache/dataset_methods.rb
49
+ - lib/sequel-query-cache/driver.rb
50
+ - lib/sequel-query-cache/driver/dalli.rb
51
+ - lib/sequel-query-cache/driver/memcache.rb
52
+ - lib/sequel-query-cache/instance_methods.rb
53
+ - lib/sequel-query-cache/serializer/json.rb
54
+ - lib/sequel-query-cache/serializer/message_pack.rb
55
+ - lib/sequel-query-cache/version.rb
56
+ - sequel-query-cache.gemspec
57
+ - spec/lib/sequel-query-cache/driver/dalli_driver_spec.rb
58
+ - spec/lib/sequel-query-cache/driver/memcache_driver_spec.rb
59
+ - spec/lib/sequel-query-cache/driver/redis_driver_spec.rb
60
+ - spec/lib/sequel-query-cache/driver_spec.rb
61
+ - spec/lib/sequel-query-cache_spec.rb
62
+ - spec/models/dalli_spec.rb
63
+ - spec/models/memcache_spec.rb
64
+ - spec/models/redis_spec.rb
65
+ - spec/shared/driver.rb
66
+ - spec/shared/query-cache.rb
67
+ - spec/spec_helper.rb
68
+ - spec/support/models/dalli.rb
69
+ - spec/support/models/memcache.rb
70
+ - spec/support/models/redis.rb
71
+ homepage: https://github.com/binarypaladin/sequel-query-cache
72
+ licenses:
73
+ - MIT
74
+ metadata: {}
75
+ post_install_message:
76
+ rdoc_options: []
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - ">="
82
+ - !ruby/object:Gem::Version
83
+ version: '0'
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ requirements: []
90
+ rubyforge_project:
91
+ rubygems_version: 2.2.2
92
+ signing_key:
93
+ specification_version: 4
94
+ summary: A plugin for Sequel that allows dataset results to be cached in Memcached
95
+ or Redis.
96
+ test_files:
97
+ - spec/lib/sequel-query-cache/driver/dalli_driver_spec.rb
98
+ - spec/lib/sequel-query-cache/driver/memcache_driver_spec.rb
99
+ - spec/lib/sequel-query-cache/driver/redis_driver_spec.rb
100
+ - spec/lib/sequel-query-cache/driver_spec.rb
101
+ - spec/lib/sequel-query-cache_spec.rb
102
+ - spec/models/dalli_spec.rb
103
+ - spec/models/memcache_spec.rb
104
+ - spec/models/redis_spec.rb
105
+ - spec/shared/driver.rb
106
+ - spec/shared/query-cache.rb
107
+ - spec/spec_helper.rb
108
+ - spec/support/models/dalli.rb
109
+ - spec/support/models/memcache.rb
110
+ - spec/support/models/redis.rb
111
+ has_rdoc: