memcache-client-activerecord 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,24 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ /memcache-client-activerecord.gemspec
23
+ /spec/database.sqlite3
24
+ /spec/database.yml
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 ISHIHARA Masaki
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.rdoc ADDED
@@ -0,0 +1,34 @@
1
+ = memcache-client-activerecord
2
+
3
+ memcache-client-activerecord has the same interface as memcache-client,
4
+ and provides the functionality of saving to ActiveRecord instead of Memcached.
5
+
6
+ == INSTALL
7
+
8
+ $ [sudo] gem install memcache-client-activerecord
9
+
10
+ == Usage for Rails
11
+
12
+ config/environment.rb
13
+ config.gem 'memcache-client-activerecord'
14
+
15
+ Generate a model and a migration
16
+ $ script/generate cache_model Cache
17
+ $ rake db:migrate
18
+
19
+ Instead of MemCache.new
20
+ cache = MemCache::ActiveRecord.new(Cache)
21
+
22
+ == Note on Patches/Pull Requests
23
+
24
+ * Fork the project.
25
+ * Make your feature addition or bug fix.
26
+ * Add tests for it. This is important so I don't break it in a
27
+ future version unintentionally.
28
+ * Commit, do not mess with rakefile, version, or history.
29
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
30
+ * Send me a pull request. Bonus points for topic branches.
31
+
32
+ == Copyright
33
+
34
+ Copyright (c) 2009 ISHIHARA Masaki. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "memcache-client-activerecord"
8
+ gem.summary = %Q{memcache-client with ActiveRecord backend}
9
+ gem.description = %Q{memcache-client-activerecord has the same interface as memcache-client, and provides the functionality of saving to ActiveRecord instead of Memcached.}
10
+ gem.email = "m.ishihara@gmail.com"
11
+ gem.homepage = "http://github.com/m4i/memcache-client-activerecord"
12
+ gem.authors = ["ISHIHARA Masaki"]
13
+ gem.add_runtime_dependency "activerecord", ">= 2.1"
14
+ gem.add_development_dependency "rspec", ">= 1.2.9"
15
+ gem.add_development_dependency "memcache-client", ">= 1.7.7"
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ Jeweler::GemcutterTasks.new
19
+ rescue LoadError
20
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
21
+ end
22
+
23
+ require 'spec/rake/spectask'
24
+ Spec::Rake::SpecTask.new(:spec) do |spec|
25
+ spec.libs << 'lib' << 'spec'
26
+ spec.spec_files = FileList['spec/**/*_spec.rb']
27
+ end
28
+
29
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
30
+ spec.libs << 'lib' << 'spec'
31
+ spec.pattern = 'spec/**/*_spec.rb'
32
+ spec.rcov = true
33
+ end
34
+
35
+ task :spec => :check_dependencies
36
+
37
+ task :default => :spec
38
+
39
+ require 'rake/rdoctask'
40
+ Rake::RDocTask.new do |rdoc|
41
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
42
+
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "memcache-client-activerecord #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,41 @@
1
+ # based on railties/lib/rails_generator/generators/components/model/model_generator.rb
2
+ class CacheModelGenerator < Rails::Generator::NamedBase
3
+ default_options :skip_migration => false
4
+
5
+ def manifest
6
+ record do |m|
7
+ # Check for class naming collisions.
8
+ m.class_collisions class_name
9
+
10
+ # Model directory.
11
+ m.directory File.join('app/models', class_path)
12
+
13
+ # Model class.
14
+ m.template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb")
15
+
16
+ # Migration.
17
+ migration_file_path = file_path.gsub(/\//, '_')
18
+ migration_name = class_name
19
+ if ActiveRecord::Base.pluralize_table_names
20
+ migration_name = migration_name.pluralize
21
+ migration_file_path = migration_file_path.pluralize
22
+ end
23
+
24
+ unless options[:skip_migration]
25
+ m.migration_template 'migration.rb', 'db/migrate', :assigns => {
26
+ :migration_name => "Create#{migration_name.gsub(/::/, '')}"
27
+ }, :migration_file_name => "create_#{migration_file_path}"
28
+ end
29
+ end
30
+ end
31
+
32
+ protected
33
+ def add_options!(opt)
34
+ opt.separator ''
35
+ opt.separator 'Options:'
36
+ opt.on('--skip-migration',
37
+ "Don't generate a migration file for this model") do |value|
38
+ options[:skip_migration] = value
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,28 @@
1
+ class <%= migration_name %> < ActiveRecord::Migration
2
+ def self.up
3
+ case connection.adapter_name
4
+ when 'MySQL'
5
+ execute(<<-SQL)
6
+ CREATE TABLE `<%= table_name %>` (
7
+ `key` VARBINARY(250) NOT NULL PRIMARY KEY,
8
+ `value` MEDIUMBLOB NOT NULL,
9
+ `cas` INT UNSIGNED NOT NULL,
10
+ `expire_at` DATETIME
11
+ ) ENGINE=InnoDB
12
+ SQL
13
+
14
+ else
15
+ create_table :<%= table_name %>, :id => false do |t|
16
+ t.string :key, :null => false, :limit => 250
17
+ t.binary :value, :null => false
18
+ t.integer :cas, :null => false
19
+ t.datetime :expire_at
20
+ end
21
+ add_index :<%= table_name %>, :key, :unique => true
22
+ end
23
+ end
24
+
25
+ def self.down
26
+ drop_table :<%= table_name %>
27
+ end
28
+ end
@@ -0,0 +1,3 @@
1
+ class <%= class_name %> < ActiveRecord::Base
2
+ establish_connection
3
+ end
@@ -0,0 +1 @@
1
+ require 'memcache/activerecord'
@@ -0,0 +1,501 @@
1
+ require 'digest/sha1'
2
+
3
+ class MemCache
4
+ class ActiveRecord
5
+ DEFAULT_OPTIONS = {
6
+ :autofix_keys => false,
7
+ :check_size => true,
8
+ :failover => false,
9
+ :logger => nil,
10
+ :multithread => true,
11
+ :namespace => nil,
12
+ :namespace_separator => ':',
13
+ :no_reply => false,
14
+ :readonly => false,
15
+ :timeout => nil,
16
+ }
17
+
18
+ MAX_KEY_SIZE = 250
19
+ MAX_VALUE_SIZE = 2 ** 20
20
+
21
+ COLUMN_NAMES = {
22
+ :key => 'key',
23
+ :value => 'value',
24
+ :cas => 'cas',
25
+ :expire_at => 'expire_at',
26
+ }
27
+
28
+ STORED = "STORED\r\n"
29
+ NOT_STORED = "NOT_STORED\r\n"
30
+ EXISTS = "EXISTS\r\n"
31
+ DELETED = "DELETED\r\n"
32
+ NOT_FOUND = "NOT_FOUND\r\n"
33
+
34
+ attr_reader :autofix_keys
35
+ attr_reader :failover
36
+ attr_reader :logger
37
+ attr_reader :multithread
38
+ attr_reader :namespace
39
+ attr_reader :no_reply
40
+ attr_reader :timeout
41
+
42
+ def initialize(active_record, options = {})
43
+ @ar = active_record
44
+
45
+ [
46
+ :check_size,
47
+ :failover,
48
+ :logger,
49
+ :multithread,
50
+ :timeout,
51
+ ].each do |name|
52
+ if options.key?(name) && options[name] != DEFAULT_OPTIONS[name]
53
+ raise ArgumentError, "#{name} isn't changeable"
54
+ end
55
+ end
56
+
57
+ options = DEFAULT_OPTIONS.merge(options)
58
+ @autofix_keys = options[:autofix_keys]
59
+ @check_size = options[:check_size]
60
+ @failover = options[:failover]
61
+ @logger = options[:logger]
62
+ @multithread = options[:multithread]
63
+ @namespace = options[:namespace]
64
+ @namespace_separator = options[:namespace_separator]
65
+ @no_reply = options[:no_reply]
66
+ @readonly = options[:readonly]
67
+ @timeout = options[:timeout]
68
+ end
69
+
70
+ def inspect
71
+ '<%s: %s, ns: %p, ro: %p>' %
72
+ [self.class, @ar, @namespace, @readonly]
73
+ end
74
+
75
+ def active?
76
+ true
77
+ end
78
+
79
+ def readonly?
80
+ @readonly
81
+ end
82
+
83
+ def get(key, raw = false)
84
+ cache_key = make_cache_key(key)
85
+ if value = find(cache_key, :value, true, __method__)
86
+ raw ? value : Marshal.load(value)
87
+ end
88
+ end
89
+
90
+ def fetch(key, expiry = 0, raw = false, &block)
91
+ value = get(key, raw)
92
+
93
+ if value.nil? && block_given?
94
+ value = yield
95
+ add(key, value, expiry, raw)
96
+ end
97
+
98
+ value
99
+ end
100
+
101
+ def get_multi(*keys)
102
+ cache_keys = keys.inject({}) do |cache_keys, key|
103
+ cache_keys[make_cache_key(key)] = key
104
+ cache_keys
105
+ end
106
+ rows = find_all(cache_keys.keys, [:key, :value], true, __method__)
107
+ rows.inject({}) do |hash, (key, value)|
108
+ hash[cache_keys[key]] = Marshal.load(value)
109
+ hash
110
+ end
111
+ end
112
+
113
+ def set(key, value, expiry = 0, raw = false)
114
+ check_readonly!
115
+
116
+ cache_key = make_cache_key(key)
117
+ value = value_to_storable(value, raw)
118
+
119
+ unless update(cache_key, value, expiry, __method__)
120
+ # rescue duplicate key error
121
+ insert(cache_key, value, expiry, __method__) rescue nil
122
+ end
123
+
124
+ STORED unless @no_reply
125
+ end
126
+
127
+ def cas(key, expiry = 0, raw = false, &block)
128
+ check_readonly!
129
+ raise MemCacheError, 'A block is required' unless block_given?
130
+
131
+ result = cas_with_reply(key, expiry, raw, __method__, &block)
132
+ result unless @no_reply
133
+ end
134
+
135
+ def add(key, value, expiry = 0, raw = false)
136
+ check_readonly!
137
+
138
+ cache_key = make_cache_key(key)
139
+ value = value_to_storable(value, raw)
140
+
141
+ old_value, expire_at =
142
+ find(cache_key, [:value, :expire_at], false, __method__)
143
+
144
+ if old_value && available?(expire_at)
145
+ NOT_STORED unless @no_reply
146
+
147
+ else
148
+ if old_value
149
+ update(cache_key, value, expiry, __method__)
150
+ else
151
+ # rescue duplicate key error
152
+ insert(cache_key, value, expiry, __method__) rescue nil
153
+ end
154
+
155
+ STORED unless @no_reply
156
+ end
157
+ end
158
+
159
+ def replace(key, value, expiry = 0, raw = false)
160
+ check_readonly!
161
+
162
+ cache_key = make_cache_key(key)
163
+ value = value_to_storable(value, raw)
164
+
165
+ if update(cache_key, value, expiry, __method__, true)
166
+ STORED unless @no_reply
167
+ else
168
+ NOT_STORED unless @no_reply
169
+ end
170
+ end
171
+
172
+ def append(key, value)
173
+ append_or_prepend(__method__, key, value)
174
+ end
175
+
176
+ def prepend(key, value)
177
+ append_or_prepend(__method__, key, value)
178
+ end
179
+
180
+ def incr(key, amount = 1)
181
+ incr_or_decl(__method__, key, amount)
182
+ end
183
+
184
+ def decr(key, amount = 1)
185
+ incr_or_decl(__method__, key, amount)
186
+ end
187
+
188
+ def delete(key)
189
+ check_readonly!
190
+
191
+ cache_key = make_cache_key(key)
192
+ conditions = { COLUMN_NAMES[:key] => cache_key }
193
+
194
+ if @no_reply
195
+ _delete(conditions, __method__)
196
+ nil
197
+ else
198
+ exists = !!find(cache_key, :key, true, __method__)
199
+ _delete(conditions, __method__)
200
+ exists ? DELETED : NOT_FOUND
201
+ end
202
+ end
203
+
204
+ def flush_all
205
+ check_readonly!
206
+ truncate(__method__)
207
+ end
208
+
209
+ alias_method :[], :get
210
+
211
+ def []=(key, value)
212
+ set(key, value)
213
+ end
214
+
215
+ def garbage_collection!
216
+ _delete(["#{quote_column_name(:expire_at)} <= ?", now], __method__)
217
+ end
218
+
219
+ private
220
+ def check_readonly!
221
+ raise MemCacheError, 'Update of readonly cache' if @readonly
222
+ end
223
+
224
+ def make_cache_key(key)
225
+ if @autofix_keys && (key =~ /\s/ ||
226
+ (key.length + (namespace.nil? ? 0 : namespace.length)) > MAX_KEY_SIZE)
227
+ key = "#{Digest::SHA1.hexdigest(key)}-autofixed"
228
+ end
229
+
230
+ key = namespace.nil? ?
231
+ key :
232
+ "#{namespace}#{@namespace_separator}#{key}"
233
+
234
+ if key =~ /\s/
235
+ raise ArgumentError, "illegal character in key #{key.inspect}"
236
+ end
237
+ if key.length > MAX_KEY_SIZE
238
+ raise ArgumentError, "key too long #{key.inspect}"
239
+ end
240
+
241
+ key
242
+ end
243
+
244
+ def value_to_storable(value, raw)
245
+ value = raw ? value.to_s : Marshal.dump(value)
246
+ check_value_size!(value)
247
+ value
248
+ end
249
+
250
+ def check_value_size!(value)
251
+ if @check_size && value.size > MAX_VALUE_SIZE
252
+ raise MemCacheError,
253
+ "Value too large, memcached can only store 1MB of data per key"
254
+ end
255
+ end
256
+
257
+ def gets(key, raw, method)
258
+ cache_key = make_cache_key(key)
259
+ value, cas = find(cache_key, [:value, :cas], true, method)
260
+ if cas
261
+ [raw ? value : Marshal.load(value), cas]
262
+ end
263
+ end
264
+
265
+ def cas_with_reply(key, expiry, raw, method, &block)
266
+ value, cas = gets(key, raw, method)
267
+ if cas
268
+ cache_key = make_cache_key(key)
269
+ value = value_to_storable(yield(value), raw)
270
+
271
+ update(cache_key, value, expiry, method, true, cas) ?
272
+ STORED : EXISTS
273
+ end
274
+ end
275
+
276
+ # TODO: check value size
277
+ def append_or_prepend(method, key, value)
278
+ check_readonly!
279
+
280
+ cache_key = make_cache_key(key)
281
+ value = value.to_s
282
+
283
+ old = quote_column_name(:value)
284
+ new = quote_value(:value, value)
285
+ pairs = {
286
+ :value => concat_sql(*(method == :append ? [old, new] : [new, old]))
287
+ }
288
+
289
+ affected_rows = @ar.connection.update(
290
+ update_sql(cache_key, pairs, true, nil),
291
+ sql_name(method)
292
+ )
293
+
294
+ affected_rows > 0 ? STORED : NOT_STORED unless @no_reply
295
+ end
296
+
297
+ def incr_or_decl(method, key, amount)
298
+ check_readonly!
299
+
300
+ unless /\A\s*\d+\s*\z/ =~ amount.to_s
301
+ raise MemCacheError, 'invalid numeric delta argument'
302
+ end
303
+ amount = method == :incr ? amount.to_i : - amount.to_i
304
+
305
+ value = nil
306
+
307
+ count = 0
308
+ begin
309
+ count += 1
310
+ raise MemCacheError, "cannot #{method}" if count > 10
311
+
312
+ result = cas_with_reply(key, nil, true, method) do |old_value|
313
+ unless /\A\s*\d+\s*\z/ =~old_value
314
+ raise MemCacheError,
315
+ 'cannot increment or decrement non-numeric value'
316
+ end
317
+ value = [old_value.to_i + amount, 0].max
318
+ end
319
+ end while result == EXISTS
320
+
321
+ value unless @no_reply
322
+
323
+ rescue MemCacheError
324
+ raise unless @no_reply
325
+ end
326
+
327
+ def find(cache_key, column_keys, only_available, method)
328
+ result = @ar.connection.send(
329
+ column_keys.is_a?(Array) ? :select_one : :select_value,
330
+ select_sql(
331
+ cache_key,
332
+ quote_column_name(*Array(column_keys)),
333
+ only_available
334
+ ),
335
+ sql_name(method)
336
+ )
337
+
338
+ (result && column_keys.is_a?(Array)) ?
339
+ column_keys.map {|k| result[COLUMN_NAMES[k]] } :
340
+ result
341
+ end
342
+
343
+ def find_all(cache_keys, column_keys, only_available, method)
344
+ return [] if cache_keys.empty?
345
+
346
+ result = @ar.connection.send(
347
+ column_keys.is_a?(Array) ? :select_all : :select_values,
348
+ select_sql(
349
+ cache_keys,
350
+ quote_column_name(*Array(column_keys)),
351
+ only_available
352
+ ),
353
+ sql_name(method)
354
+ )
355
+
356
+ column_keys.is_a?(Array) ?
357
+ result.map {|r| column_keys.map {|k| r[COLUMN_NAMES[k]] }} :
358
+ result
359
+ end
360
+
361
+ def insert(cache_key, value, expiry, method)
362
+ attributes = attributes_for_update(value, expiry).merge(
363
+ :key => cache_key,
364
+ :cas => 0
365
+ )
366
+
367
+ column_keys = attributes.keys
368
+
369
+ quoted_values = column_keys.map do |column_key|
370
+ quote_value(column_key, attributes[column_key])
371
+ end
372
+
373
+ @ar.connection.execute(
374
+ "INSERT INTO #{@ar.quoted_table_name}" +
375
+ " (#{quote_column_name(*column_keys)})" +
376
+ " VALUES(#{quoted_values.join(', ')})",
377
+ sql_name(method)
378
+ )
379
+ end
380
+
381
+ def update(cache_key, value, expiry, method, only_available = false, cas = nil)
382
+ attributes = attributes_for_update(value, expiry)
383
+
384
+ pairs = attributes.keys.inject({}) do |pairs, column_key|
385
+ pairs[column_key] = quote_value(column_key, attributes[column_key])
386
+ pairs
387
+ end
388
+
389
+ @ar.connection.update(
390
+ update_sql(cache_key, pairs, only_available, cas),
391
+ sql_name(method)
392
+ ) > 0
393
+ end
394
+
395
+ def _delete(conditions, method)
396
+ @ar.connection.execute(
397
+ "DELETE FROM #{@ar.quoted_table_name}" +
398
+ " WHERE #{@ar.send(:sanitize_sql, conditions)}",
399
+ sql_name(method)
400
+ )
401
+ end
402
+
403
+ def truncate(method)
404
+ sql = case @ar.connection.adapter_name
405
+ when 'SQLite'
406
+ "DELETE FROM #{@ar.quoted_table_name}"
407
+ else
408
+ "TRUNCATE TABLE #{@ar.quoted_table_name}"
409
+ end
410
+ @ar.connection.execute(sql, sql_name(method))
411
+ end
412
+
413
+ def attributes_for_update(value, expiry)
414
+ attributes = { :value => value }
415
+ unless expiry.nil?
416
+ attributes.update(:expire_at => expiry.zero? ? nil : now(expiry))
417
+ end
418
+ attributes
419
+ end
420
+
421
+ def select_sql(cache_key, select, only_available)
422
+ conditions = build_conditions(cache_key, nil, only_available)
423
+
424
+ "SELECT #{select}" +
425
+ " FROM #{@ar.quoted_table_name}" +
426
+ " WHERE #{@ar.send(:sanitize_sql, conditions)}"
427
+ end
428
+
429
+ def update_sql(cache_key, pairs, only_available, cas)
430
+ pairs[:cas] = "#{quote_column_name(:cas)} + 1"
431
+ pairs = pairs.map {|n, v| "#{quote_column_name(n)} = #{v}" }
432
+ conditions = build_conditions(cache_key, cas, only_available)
433
+
434
+ "UPDATE #{@ar.quoted_table_name}" +
435
+ " SET #{pairs.join(', ')}" +
436
+ " WHERE #{@ar.send(:sanitize_sql, conditions)}"
437
+ end
438
+
439
+ def build_conditions(cache_key, cas, only_available)
440
+ conditions = [
441
+ @ar.send(:attribute_condition, quote_column_name(:key), cache_key),
442
+ cache_key
443
+ ]
444
+
445
+ if cas
446
+ conditions.first << " AND #{quote_column_name(:cas)} = ?"
447
+ conditions << cas
448
+ end
449
+
450
+ if only_available
451
+ conditions.first << ' AND (' +
452
+ "#{quote_column_name(:expire_at)} IS NULL" +
453
+ " OR #{quote_column_name(:expire_at)} > ?" +
454
+ ')'
455
+ conditions << now
456
+ end
457
+
458
+ conditions
459
+ end
460
+
461
+ def concat_sql(a, b)
462
+ case @ar.connection.adapter_name
463
+ when 'MySQL'
464
+ "CONCAT(#{a}, #{b})"
465
+ else
466
+ "#{a} || #{b}"
467
+ end
468
+ end
469
+
470
+ def sql_name(method)
471
+ "#{self.class.name}##{method}"
472
+ end
473
+
474
+ def now(delay = 0)
475
+ Time.now + delay
476
+ end
477
+
478
+ def quote_column_name(*column_keys)
479
+ column_keys.map do |column_key|
480
+ @ar.connection.quote_column_name(
481
+ column_key.is_a?(Symbol) ? COLUMN_NAMES[column_key] : column_key)
482
+ end.join(', ')
483
+ end
484
+
485
+ def quote_value(column_key, value)
486
+ @ar.connection.quote(value, @ar.columns_hash[COLUMN_NAMES[column_key]])
487
+ end
488
+
489
+ def available?(expire_at)
490
+ expire_at.nil? || now < to_time(expire_at)
491
+ end
492
+
493
+ def to_time(expire_at)
494
+ @ar.columns_hash[COLUMN_NAMES[:expire_at]].type_cast(expire_at)
495
+ end
496
+ end
497
+
498
+ unless const_defined?(:MemCacheError)
499
+ class MemCacheError < RuntimeError; end
500
+ end
501
+ end