require 'active_record' require 'memcache/migration' require 'pg' class PGconn def self.quote_ident(name) %("#{name}") end end class Memcache class PGServer < Base attr_reader :db, :table def initialize(opts) @table = opts[:table] @db = opts[:db] || ActiveRecord::Base.connection.raw_connection end def name @name ||= begin db_config = db.instance_variable_get(:@config) "#{db_config[:host]}:#{db_config[:database]}:#{table}" end end def flush_all(delay = nil) db.exec("TRUNCATE #{table}") end def get(keys) return get([keys])[keys.to_s] unless keys.kind_of?(Array) return {} if keys.empty? keys = keys.collect {|key| quote(key.to_s)}.join(',') sql = %{ SELECT key, value FROM #{table} WHERE key IN (#{keys}) AND #{prefix_clause} AND #{expiry_clause} } results = {} db.query(sql).each do |row| results[row['key']] = row['value'] end results end def incr(key, amount = 1) transaction do value = get(key) return unless value return unless value =~ /^\d+$/ value = value.to_i + amount value = 0 if value < 0 db.exec %{ UPDATE #{table} SET value = #{quote(value)}, updated_at = NOW() WHERE key = #{quote(key)} AND #{prefix_clause} } value end end def decr(key, amount = 1) incr(key, -amount) end def delete(key) result = db.exec %{ DELETE FROM #{table} WHERE key = #{quote(key)} AND #{prefix_clause} } result.cmdtuples == 1 end def set(key, value, expiry = 0) transaction do delete(key) insert(key, value, expiry) end value end def add(key, value, expiry = 0) delete_expired(key) insert(key, value, expiry) value rescue PGError => e nil end def replace(key, value, expiry = 0) delete_expired(key) result = update(key, value, expiry) result.cmdtuples == 1 ? value : nil end def append(key, value) delete_expired(key) result = db.exec %{ UPDATE #{table} SET value = value || #{quote(value)}, updated_at = NOW() WHERE key = #{quote(key)} AND #{prefix_clause} } result.cmdtuples == 1 end def prepend(key, value) delete_expired(key) result = db.exec %{ UPDATE #{table} SET value = #{quote(value)} || value, updated_at = NOW() WHERE key = #{quote(key)} AND #{prefix_clause} } result.cmdtuples == 1 end private def insert(key, value, expiry = 0) db.exec %{ INSERT INTO #{table} (prefix, key, value, updated_at, expires_at) VALUES (#{quoted_prefix}, #{quote(key)}, #{quote(value)}, NOW(), #{expiry_sql(expiry)}) } end def update(key, value, expiry = 0) db.exec %{ UPDATE #{table} SET value = #{quote(value)}, updated_at = NOW(), expires_at = #{expiry_sql(expiry)} WHERE key = #{quote(key)} AND #{prefix_clause} } end def transaction return yield if @in_transaction begin @in_transaction = true db.exec('BEGIN') value = yield db.exec('COMMIT') value rescue Exception => e db.exec('ROLLBACK') raise e ensure @in_transaction = false end end def quote(string) string.to_s.gsub(/'/,"\'") "'#{string}'" end def delete_expired(key) db.exec "DELETE FROM #{table} WHERE key = #{quote(key)} AND #{prefix_clause} AND NOT (#{expiry_clause})" end def expiry_clause "expires_at IS NULL OR expires_at > NOW()" end def expiry_sql(expiry) expiry = Time.at(expiry) if expiry > 60*60*24*30 if expiry.kind_of?(Time) quote(expiry.to_s(:db)) else expiry == 0 ? 'NULL' : "NOW() + interval '#{expiry} seconds'" end end def quoted_prefix quote(prefix || '') end def prefix_clause "prefix = #{quoted_prefix}" end end end