whoahbot-dm-redis 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2009 Dan Herrera
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ 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
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,18 @@
1
+ h1. DM-Redis
2
+
3
+ This is an experimental datamapper adapter for the <a href="http://code.google.com/p/redis/wiki/README">Redis</a> key-value database.
4
+
5
+ Please be aware that this is very alpha quality software! It is not recommended for production use yet. If you find a bug, or are using dm-redis, please let me know.
6
+
7
+ h1. TODO
8
+
9
+ Refactoring +records_for+ to filter down the set of records returned.
10
+
11
+ h1. Install
12
+
13
+ Prerequisites:
14
+ * Redis, git version:
15
+ ** <a href="http://github.com/antirez/redis/">Redis, git version</a>
16
+ * Gems:
17
+ ** <a href="http://github.com/datamapper/extlib">extlib</a>, dependency for dm-core
18
+ ** <a href="http://github.com/datamapper/dm-core/tree/next">dm-core</a> next branch
@@ -0,0 +1,55 @@
1
+ require 'rubygems'
2
+ require 'rake/gempackagetask'
3
+ require 'rubygems/specification'
4
+ require 'date'
5
+ require 'spec/rake/spectask'
6
+
7
+ GEM = 'dm-redis'
8
+ GEM_NAME = 'dm-redis'
9
+ GEM_VERSION = '0.0.2'
10
+ AUTHORS = ['Dan Herrera']
11
+ EMAIL = "whoahbot@gmail.com"
12
+ HOMEPAGE = "http://github.com/whoahbot/dm-redis"
13
+ SUMMARY = "DataMapper adapter for the Redis key-value database"
14
+
15
+ spec = Gem::Specification.new do |s|
16
+ s.name = GEM
17
+ s.version = GEM_VERSION
18
+ s.platform = Gem::Platform::RUBY
19
+ s.has_rdoc = true
20
+ s.extra_rdoc_files = ["MIT-LICENSE"]
21
+ s.summary = SUMMARY
22
+ s.description = s.summary
23
+ s.authors = AUTHORS
24
+ s.email = EMAIL
25
+ s.homepage = HOMEPAGE
26
+ s.add_dependency "rspec"
27
+ s.add_dependency "dm-core", "0.10.0"
28
+ s.require_path = 'lib'
29
+ s.autorequire = GEM
30
+ s.files = %w(MIT-LICENSE README.textile Rakefile) + Dir.glob("{lib,spec}/**/*")
31
+ end
32
+
33
+ task :default => :spec
34
+
35
+ desc "Run specs"
36
+ Spec::Rake::SpecTask.new do |t|
37
+ t.spec_files = FileList['spec/**/*_spec.rb']
38
+ t.spec_opts = %w(-fs --color)
39
+ end
40
+
41
+ Rake::GemPackageTask.new(spec) do |pkg|
42
+ pkg.gem_spec = spec
43
+ end
44
+
45
+ desc "install the gem locally"
46
+ task :install => [:package] do
47
+ sh %{sudo gem install pkg/#{GEM}-#{GEM_VERSION}}
48
+ end
49
+
50
+ desc "create a gemspec file"
51
+ task :make_spec do
52
+ File.open("#{GEM}.gemspec", "w") do |file|
53
+ file.puts spec.to_ruby
54
+ end
55
+ end
@@ -0,0 +1,170 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'rubyredis'))
2
+
3
+ module DataMapper
4
+ module Adapters
5
+ Extlib::Inflection.word 'redis'
6
+
7
+ class RedisAdapter < AbstractAdapter
8
+ ##
9
+ # Used by DataMapper to put records into the redis data-store: "INSERT" in SQL-speak.
10
+ # It takes an array of the resources (model instances) to be saved. Resources
11
+ # each have a key that can be used to quickly look them up later without
12
+ # searching.
13
+ #
14
+ # @param [Enumerable(Resource)] resources
15
+ # The set of resources (model instances)
16
+ #
17
+ # @api semipublic
18
+ def create(resources)
19
+ resources.each do |resource|
20
+ initialize_identity_field(resource, @redis.incr("#{resource.model}:#{redis_key_for(resource.model)}:serial"))
21
+ @redis.set_add("#{resource.model}:#{redis_key_for(resource.model)}:all", resource.key)
22
+ end
23
+
24
+ update_attributes(resources)
25
+ end
26
+
27
+ ##
28
+ # Looks up one record or a collection of records from the data-store:
29
+ # "SELECT" in SQL.
30
+ #
31
+ # @param [Query] query
32
+ # The query to be used to seach for the resources
33
+ #
34
+ # @return [Array]
35
+ # An Array of Hashes containing the key-value pairs for
36
+ # each record
37
+ #
38
+ # @api semipublic
39
+ def read(query)
40
+ records = records_for(query).each do |record|
41
+ query.fields.each do |property|
42
+ next if query.model.key.include?(property.name)
43
+ record[property.name.to_s] = property.typecast(@redis["#{query.model}:#{record[redis_key_for(query.model)]}:#{property.name}"])
44
+ end
45
+ end
46
+
47
+ records = query.match_records(records)
48
+ records = query.sort_records(records)
49
+ records = query.limit_records(records)
50
+ records
51
+ end
52
+
53
+ ##
54
+ # Used by DataMapper to update the attributes on existing records in the redis
55
+ # data-store: "UPDATE" in SQL-speak. It takes a hash of the attributes
56
+ # to update with, as well as a collection object that specifies which resources
57
+ # should be updated.
58
+ #
59
+ # @param [Hash] attributes
60
+ # A set of key-value pairs of the attributes to update the resources with.
61
+ # @param [DataMapper::Collection] collection
62
+ # The query that should be used to find the resource(s) to update.
63
+ #
64
+ # @api semipublic
65
+ def update(attributes, collection)
66
+ attributes = attributes_as_fields(attributes)
67
+ read(collection.query).each { |r| r.update(attributes) }
68
+ end
69
+
70
+ ##
71
+ # Destroys all the records matching the given query. "DELETE" in SQL.
72
+ #
73
+ # @param [DataMapper::Collection] collection
74
+ # The query used to locate the resources to be deleted.
75
+ #
76
+ # @return [Array]
77
+ # An Array of Hashes containing the key-value pairs for
78
+ # each record
79
+ #
80
+ # @api semipublic
81
+ def delete(collection)
82
+ collection.query.filter_records(records_for(collection.query)).each do |record|
83
+ collection.query.model.properties.each do |p|
84
+ @redis.delete("#{collection.query.model}:#{record[redis_key_for(collection.query.model)]}:#{p.name}")
85
+ end
86
+ @redis.set_delete("#{collection.query.model}:#{redis_key_for(collection.query.model)}:all", record[redis_key_for(collection.query.model)])
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ ##
93
+ # Creates a string representation for the keys in a given model
94
+ #
95
+ # @param [DataMapper::Model] model
96
+ # The query used to locate the resources to be deleted.
97
+ #
98
+ # @return [Array]
99
+ # An Array of Hashes containing the key-value pairs for
100
+ # each record
101
+ #
102
+ # @api private
103
+ def redis_key_for(model)
104
+ model.key.collect {|k| k.name}.join(":")
105
+ end
106
+
107
+ ##
108
+ # Saves each key value pair to the redis data store
109
+ #
110
+ # @param [Array] resources
111
+ # An array of resources to save
112
+ #
113
+ # @api private
114
+ def update_attributes(resources)
115
+ resources.each do |resource|
116
+ resource.attributes.each do |property, value|
117
+ @redis["#{resource.model}:#{resource.key}:#{property}"] = value unless value.nil?
118
+ end
119
+ end
120
+ end
121
+
122
+ ##
123
+ # Retrieves records for a particular model.
124
+ #
125
+ # @param [DataMapper::Query] query
126
+ # The query used to locate the resources
127
+ #
128
+ # @return [Array]
129
+ # An array of hashes of all of the records for a particular model
130
+ #
131
+ # @api private
132
+ def records_for(query)
133
+ keys = []
134
+ query.conditions.operands.select {|o| o.is_a?(Conditions::EqualToComparison) && query.model.key.include?(o.property)}.each do |o|
135
+ if @redis.set_member?("#{query.model}:#{redis_key_for(query.model)}:all", o.value)
136
+ keys << {"#{redis_key_for(query.model)}" => o.value}
137
+ end
138
+ end
139
+
140
+ # TODO: Implement other conditions to filter down the records retrieved
141
+ # Keys are empty, fall back and load all the values for this model
142
+ if keys.empty?
143
+ @redis.set_members("#{query.model}:#{redis_key_for(query.model)}:all").each do |val|
144
+ keys << {"#{redis_key_for(query.model)}" => val.to_i}
145
+ end
146
+ end
147
+
148
+ keys
149
+ end
150
+
151
+ ##
152
+ # Make a new instance of the adapter. The @redis ivar is the 'data-store'
153
+ # for this adapter.
154
+ #
155
+ # @param [String, Symbol] name
156
+ # The name of the Repository using this adapter.
157
+ # @param [String, Hash] uri_or_options
158
+ # The connection uri string, or a hash of options to set up
159
+ # the adapter
160
+ #
161
+ # @api semipublic
162
+ def initialize(name, uri_or_options)
163
+ super
164
+ @redis = RedisClient.new(@options)
165
+ end
166
+ end # class RedisAdapter
167
+
168
+ const_added(:RedisAdapter)
169
+ end # module Adapters
170
+ end # module DataMapper
@@ -0,0 +1,237 @@
1
+ # RubyRedis is an alternative implementatin of Ruby client library written
2
+ # by Salvatore Sanfilippo.
3
+ #
4
+ # The aim of this library is to create an alternative client library that is
5
+ # much simpler and does not implement every command explicitly but uses
6
+ # method_missing instead.
7
+
8
+ require 'socket'
9
+ require 'set'
10
+
11
+ begin
12
+ if (RUBY_VERSION >= '1.9')
13
+ require 'timeout'
14
+ RedisTimer = Timeout
15
+ else
16
+ require 'system_timer'
17
+ RedisTimer = SystemTimer
18
+ end
19
+ rescue LoadError
20
+ RedisTimer = nil
21
+ end
22
+
23
+ class RedisClient
24
+ BulkCommands = {
25
+ "set"=>true, "setnx"=>true, "rpush"=>true, "lpush"=>true, "lset"=>true,
26
+ "lrem"=>true, "sadd"=>true, "srem"=>true, "sismember"=>true,
27
+ "echo"=>true, "getset"=>true, "smove"=>true
28
+ }
29
+
30
+ ConvertToBool = lambda{|r| r == 0 ? false : r}
31
+
32
+ ReplyProcessor = {
33
+ "exists" => ConvertToBool,
34
+ "sismember"=> ConvertToBool,
35
+ "sadd"=> ConvertToBool,
36
+ "srem"=> ConvertToBool,
37
+ "smove"=> ConvertToBool,
38
+ "move"=> ConvertToBool,
39
+ "setnx"=> ConvertToBool,
40
+ "del"=> ConvertToBool,
41
+ "renamenx"=> ConvertToBool,
42
+ "expire"=> ConvertToBool,
43
+ "keys" => lambda{|r| r.split(" ")},
44
+ "info" => lambda{|r|
45
+ info = {}
46
+ r.each_line {|kv|
47
+ k,v = kv.split(":",2).map{|x| x.chomp}
48
+ info[k.to_sym] = v
49
+ }
50
+ info
51
+ }
52
+ }
53
+
54
+ Aliases = {
55
+ "flush_db" => "flushdb",
56
+ "flush_all" => "flushall",
57
+ "last_save" => "lastsave",
58
+ "key?" => "exists",
59
+ "delete" => "del",
60
+ "randkey" => "randomkey",
61
+ "list_length" => "llen",
62
+ "push_tail" => "rpush",
63
+ "push_head" => "lpush",
64
+ "pop_tail" => "rpop",
65
+ "pop_head" => "lpop",
66
+ "list_set" => "lset",
67
+ "list_range" => "lrange",
68
+ "list_trim" => "ltrim",
69
+ "list_index" => "lindex",
70
+ "list_rm" => "lrem",
71
+ "set_add" => "sadd",
72
+ "set_delete" => "srem",
73
+ "set_count" => "scard",
74
+ "set_member?" => "sismember",
75
+ "set_members" => "smembers",
76
+ "set_intersect" => "sinter",
77
+ "set_intersect_store" => "sinterstore",
78
+ "set_inter_store" => "sinterstore",
79
+ "set_union" => "sunion",
80
+ "set_union_store" => "sunionstore",
81
+ "set_diff" => "sdiff",
82
+ "set_diff_store" => "sdiffstore",
83
+ "set_move" => "smove",
84
+ "set_unless_exists" => "setnx",
85
+ "rename_unless_exists" => "renamenx"
86
+ }
87
+
88
+ def initialize(opts={})
89
+ @host = opts[:host] || '127.0.0.1'
90
+ @port = opts[:port] || 6379
91
+ @db = opts[:db] || 0
92
+ @timeout = opts[:timeout] || 0
93
+ connect_to_server
94
+ end
95
+
96
+ def to_s
97
+ "Redis Client connected to #{@host}:#{@port} against DB #{@db}"
98
+ end
99
+
100
+ def connect_to_server
101
+ @sock = connect_to(@host,@port,@timeout == 0 ? nil : @timeout)
102
+ call_command(["select",@db]) if @db != 0
103
+ end
104
+
105
+ def connect_to(host, port, timeout=nil)
106
+ # We support connect() timeout only if system_timer is availabe
107
+ # or if we are running against Ruby >= 1.9
108
+ # Timeout reading from the socket instead will be supported anyway.
109
+ if @timeout != 0 and RedisTimer
110
+ begin
111
+ sock = TCPSocket.new(host, port, 0)
112
+ rescue Timeout::Error
113
+ @sock = nil
114
+ raise Timeout::Error, "Timeout connecting to the server"
115
+ end
116
+ else
117
+ sock = TCPSocket.new(host, port, 0)
118
+ end
119
+
120
+ # If the timeout is set we set the low level socket options in order
121
+ # to make sure a blocking read will return after the specified number
122
+ # of seconds. This hack is from memcached ruby client.
123
+ if timeout
124
+ secs = Integer(timeout)
125
+ usecs = Integer((timeout - secs) * 1_000_000)
126
+ optval = [secs, usecs].pack("l_2")
127
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, optval
128
+ sock.setsockopt Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, optval
129
+ end
130
+ sock
131
+ end
132
+
133
+ def method_missing(*argv)
134
+ call_command(argv)
135
+ end
136
+
137
+ def call_command(argv)
138
+ # this wrapper to raw_call_command handle reconnection on socket
139
+ # error. We try to reconnect just one time, otherwise let the error
140
+ # araise.
141
+ connect_to_server if !@sock
142
+ begin
143
+ raw_call_command(argv)
144
+ rescue Errno::ECONNRESET
145
+ @sock.close
146
+ connect_to_server
147
+ raw_call_command(argv)
148
+ end
149
+ end
150
+
151
+ def raw_call_command(argv)
152
+ bulk = nil
153
+ argv[0] = argv[0].to_s.downcase
154
+ argv[0] = Aliases[argv[0]] if Aliases[argv[0]]
155
+ if BulkCommands[argv[0]]
156
+ bulk = argv[-1].to_s
157
+ argv[-1] = bulk.length
158
+ end
159
+ @sock.write(argv.join(" ")+"\r\n")
160
+ @sock.write(bulk+"\r\n") if bulk
161
+
162
+ # Post process the reply if needed
163
+ processor = ReplyProcessor[argv[0]]
164
+ processor ? processor.call(read_reply) : read_reply
165
+ end
166
+
167
+ def select(*args)
168
+ raise "SELECT not allowed, use the :db option when creating the object"
169
+ end
170
+
171
+ def [](key)
172
+ get(key)
173
+ end
174
+
175
+ def []=(key,value)
176
+ set(key,value)
177
+ end
178
+
179
+ def sort(key, opts={})
180
+ cmd = []
181
+ cmd << "SORT #{key}"
182
+ cmd << "BY #{opts[:by]}" if opts[:by]
183
+ cmd << "GET #{[opts[:get]].flatten * ' GET '}" if opts[:get]
184
+ cmd << "#{opts[:order]}" if opts[:order]
185
+ cmd << "LIMIT #{opts[:limit].join(' ')}" if opts[:limit]
186
+ call_command(cmd)
187
+ end
188
+
189
+ def incr(key,increment=nil)
190
+ call_command(increment ? ["incrby",key,increment] : ["incr",key])
191
+ end
192
+
193
+ def decr(key,decrement=nil)
194
+ call_command(decrement ? ["decrby",key,decrement] : ["decr",key])
195
+ end
196
+
197
+ def read_reply
198
+ # We read the first byte using read() mainly because gets() is
199
+ # immune to raw socket timeouts.
200
+ begin
201
+ rtype = @sock.read(1)
202
+ rescue Errno::EAGAIN
203
+ # We want to make sure it reconnects on the next command after the
204
+ # timeout. Otherwise the server may reply in the meantime leaving
205
+ # the protocol in a desync status.
206
+ @sock = nil
207
+ raise Errno::EAGAIN, "Timeout reading from the socket"
208
+ end
209
+
210
+ raise Errno::ECONNRESET,"Connection lost" if !rtype
211
+ line = @sock.gets
212
+ case rtype
213
+ when "-"
214
+ raise "-"+line.strip
215
+ when "+"
216
+ line.strip
217
+ when ":"
218
+ line.to_i
219
+ when "$"
220
+ bulklen = line.to_i
221
+ return nil if bulklen == -1
222
+ data = @sock.read(bulklen)
223
+ @sock.read(2) # CRLF
224
+ data
225
+ when "*"
226
+ objects = line.to_i
227
+ return nil if bulklen == -1
228
+ res = []
229
+ objects.times {
230
+ res << read_reply
231
+ }
232
+ res
233
+ else
234
+ raise "Protocol error, got '#{rtype}' as initial reply byte"
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,21 @@
1
+ require File.dirname(__FILE__) + '/spec_helper'
2
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib/redis_adapter.rb'))
3
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib/rubyredis'))
4
+
5
+ require 'dm-core/spec/adapter_shared_spec'
6
+
7
+ describe DataMapper::Adapters::RedisAdapter do
8
+ before(:all) do
9
+ @adapter = DataMapper.setup(:default, {
10
+ :adapter => "redis",
11
+ :db => 15
12
+ })
13
+ end
14
+
15
+ after(:all) do
16
+ redis = RedisClient.new(:db => 15)
17
+ redis.flushdb
18
+ end
19
+
20
+ it_should_behave_like 'An Adapter'
21
+ end
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'dm-core'
3
+
4
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib/redis_adapter'))
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: whoahbot-dm-redis
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Dan Herrera
8
+ autorequire: dm-redis
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-24 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: dm-core
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "="
32
+ - !ruby/object:Gem::Version
33
+ version: 0.10.0
34
+ version:
35
+ description: DataMapper adapter for the Redis key-value database
36
+ email: whoahbot@gmail.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - MIT-LICENSE
43
+ files:
44
+ - MIT-LICENSE
45
+ - README.textile
46
+ - Rakefile
47
+ - lib/redis_adapter.rb
48
+ - lib/rubyredis.rb
49
+ - spec/dm_redis_adapter_spec.rb
50
+ - spec/spec_helper.rb
51
+ has_rdoc: false
52
+ homepage: http://github.com/whoahbot/dm-redis
53
+ post_install_message:
54
+ rdoc_options: []
55
+
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ version:
70
+ requirements: []
71
+
72
+ rubyforge_project:
73
+ rubygems_version: 1.2.0
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: DataMapper adapter for the Redis key-value database
77
+ test_files: []
78
+