memcache-client-activerecord 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/.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