whoahbot-dm-redis 0.0.2

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.
@@ -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
+