rkv 0.0.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/README.rdoc +38 -13
- data/Rakefile +1 -0
- data/VERSION +1 -1
- data/lib/rkv.rb +1 -0
- data/lib/rkv/rkv.rb +52 -0
- data/lib/rkv/store/cassandra_adapter.rb +191 -0
- data/lib/rkv/store/memory_adapter.rb +63 -0
- data/lib/rkv/store/tokyocabinet_adapter.rb +124 -0
- data/rkv.gemspec +12 -3
- data/spec/integration/stores_spec.rb +62 -0
- data/spec/lib/rkv_store_spec.rb +26 -0
- metadata +11 -2
data/.gitignore
CHANGED
data/README.rdoc
CHANGED
@@ -1,4 +1,12 @@
|
|
1
|
-
rkv - Ruby Key Value - an adapter on top of various key-value stores
|
1
|
+
rkv - Ruby Key Value - an adapter on top of various key-value and column stores
|
2
|
+
|
3
|
+
== Philosophy
|
4
|
+
|
5
|
+
The API is a nested (multidimensional) Hash. This allows effective mapping to both column stores and flat KV stores.
|
6
|
+
|
7
|
+
For example, if the Cassandra adapter is used, the first dimension is the column family, the second is the row within the CF and the third is the column within the row.
|
8
|
+
|
9
|
+
For flat KV stores, a key hierarchy is used for efficient mapping.
|
2
10
|
|
3
11
|
== Install
|
4
12
|
|
@@ -6,25 +14,30 @@ Installing the gem:
|
|
6
14
|
|
7
15
|
gem install rkv
|
8
16
|
|
9
|
-
|
10
17
|
== Usage
|
11
18
|
|
12
19
|
require 'rubygems'
|
13
20
|
require 'rkv'
|
14
21
|
|
15
|
-
|
16
|
-
store =
|
22
|
+
store = Rkv.open(:cassandra, { :servers => ['127.0.0.1:9160'], :keyspace => "Blog" })
|
23
|
+
# store = Rkv.open(:memory, :recurse => {"Users" => "*"})
|
24
|
+
# store = Rkv.open(:tokyocabinet, :file => "test.tcb", :recurse => {"Users" => "*"})
|
25
|
+
|
26
|
+
users = store["Users"]
|
17
27
|
|
18
|
-
|
19
|
-
|
28
|
+
user = users["5"]
|
29
|
+
user["name"] = "Dev Random"
|
30
|
+
user["role"] = "admin"
|
31
|
+
|
32
|
+
puts users["3"].inspect
|
20
33
|
|
21
|
-
|
22
|
-
|
23
|
-
|
34
|
+
users.batch do
|
35
|
+
users[id1]["name"] = "John Doe"
|
36
|
+
users[id2]["name"] = "Jane Doe"
|
24
37
|
end
|
25
38
|
|
26
|
-
|
27
|
-
|
39
|
+
user_rels = client[:UserRelationships]
|
40
|
+
user_rels[id1]["posts"][Cassandra::UUID.new] = post_id1
|
28
41
|
|
29
42
|
== Notes
|
30
43
|
|
@@ -34,17 +47,29 @@ Keystores have different features. Operations are mapped to the best of the key
|
|
34
47
|
|
35
48
|
RDoc: http://rdoc.info/projects/devrandom/rkv
|
36
49
|
|
50
|
+
See spec/integration/stores_spec.rb for an example program that runs identically on all stores.
|
51
|
+
|
52
|
+
Git:
|
53
|
+
* http://gitorious.org/cluster-storage/rkv
|
54
|
+
* http://github.com/devrandom/rkv
|
55
|
+
|
37
56
|
== Supported KV stores
|
38
57
|
|
39
58
|
* Cassandra
|
59
|
+
* Memory
|
60
|
+
* TokyoCabinet
|
40
61
|
|
41
62
|
== Planned KV stores
|
42
63
|
|
43
|
-
* Keyspace
|
44
64
|
* Voldemort
|
45
65
|
* Redis
|
66
|
+
* Keyspace
|
67
|
+
* SQL
|
46
68
|
|
47
69
|
== Roadmap
|
48
70
|
|
49
|
-
*
|
71
|
+
* Store support
|
72
|
+
* Performance testing
|
73
|
+
* Keystore feature support matrix
|
74
|
+
* Object mapper?
|
50
75
|
|
data/Rakefile
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0
|
1
|
+
0.2.0
|
data/lib/rkv.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'rkv/rkv'
|
data/lib/rkv/rkv.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Rkv
|
2
|
+
module Store
|
3
|
+
class BaseAdapter
|
4
|
+
def batch
|
5
|
+
# default batch just passes control to block
|
6
|
+
yield
|
7
|
+
end
|
8
|
+
|
9
|
+
def close
|
10
|
+
end
|
11
|
+
|
12
|
+
def ==(other)
|
13
|
+
my_keys = keys.sort
|
14
|
+
o_keys = keys.sort
|
15
|
+
return false unless my_keys == o_keys
|
16
|
+
my_keys.each do |key|
|
17
|
+
puts "#{key}" unless self[key] == other[key]
|
18
|
+
return false unless self[key] == other[key]
|
19
|
+
end
|
20
|
+
return true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
@@backends = {}
|
26
|
+
def self.open(backend_name, opts)
|
27
|
+
get_backend(backend_name).open(opts)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def self.get_backend(backend_name)
|
33
|
+
backend = @@backends[backend_name]
|
34
|
+
return backend if backend
|
35
|
+
return load_backend(backend_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.load_backend(backend_name)
|
39
|
+
my_require "rkv/store/#{backend_name}_adapter"
|
40
|
+
backend = Rkv::Store.const_get(camelize(backend_name) + "Adapter")
|
41
|
+
@@backends[backend_name] = backend
|
42
|
+
return backend
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.my_require(path)
|
46
|
+
require path
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.camelize(str)
|
50
|
+
str.to_s.split(/[^a-z0-9]/i).map{|word| word.capitalize}.join
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,191 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'cassandra'
|
3
|
+
|
4
|
+
module Rkv
|
5
|
+
module Store
|
6
|
+
class CassandraAdapter < BaseAdapter
|
7
|
+
|
8
|
+
attr_accessor :consistency
|
9
|
+
|
10
|
+
attr_accessor :opts
|
11
|
+
|
12
|
+
attr_accessor :recurse
|
13
|
+
|
14
|
+
def self.open(opts)
|
15
|
+
self.new(opts)
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(opts)
|
19
|
+
@format = opts[:format] || :pass
|
20
|
+
@consistency = opts[:consistency] || :safe
|
21
|
+
@backend = Cassandra.new(opts[:keyspace], opts[:servers])
|
22
|
+
@columns = {}
|
23
|
+
@recurse = opts[:recurse] || :default
|
24
|
+
@opts = {
|
25
|
+
:consistency => (consistency == :safe) ? Cassandra::Consistency::QUORUM : Cassandra::Consistency::ONE
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def [](key)
|
30
|
+
_must_recurse(key)
|
31
|
+
return @columns[key] if @columns[key]
|
32
|
+
@columns[key] = ColumnAdapter.new(self, key)
|
33
|
+
return @columns[key]
|
34
|
+
end
|
35
|
+
|
36
|
+
def []=(key, value)
|
37
|
+
_must_recurse(key)
|
38
|
+
raise ArgumentError.new("this store does not allow storing at top level - subscript me")
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def _must_recurse(key)
|
44
|
+
raise ArgumentError.new("at this depth - key must end with slash, or :recurse option must be specified") unless _recurse?(key)
|
45
|
+
end
|
46
|
+
|
47
|
+
def _recurse?(key)
|
48
|
+
return false if @recurse.nil?
|
49
|
+
return (@recurse == :default || @recurse == "*" || @recurse[key] || key.end_with?("/"))
|
50
|
+
end
|
51
|
+
|
52
|
+
public
|
53
|
+
|
54
|
+
def backend
|
55
|
+
@backend
|
56
|
+
end
|
57
|
+
|
58
|
+
class ColumnAdapter < BaseAdapter
|
59
|
+
attr_reader :store
|
60
|
+
|
61
|
+
attr_reader :id
|
62
|
+
|
63
|
+
attr_reader :recurse
|
64
|
+
|
65
|
+
def initialize(store, column_key)
|
66
|
+
@store = store
|
67
|
+
@id = column_key.to_sym
|
68
|
+
@recurse = @store.recurse
|
69
|
+
if Hash === @recurse
|
70
|
+
@recurse = @recurse[column_key]
|
71
|
+
elsif "*" == @recurse
|
72
|
+
@recurse = {}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def [](key)
|
77
|
+
_must_recurse(key)
|
78
|
+
RowAdapter.new(self, key, nil)
|
79
|
+
end
|
80
|
+
|
81
|
+
def delete(key)
|
82
|
+
if _recurse?(key)
|
83
|
+
@store.backend.remove(@id, key, @store.opts)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def []=(key, value)
|
88
|
+
_must_recurse(key)
|
89
|
+
# TODO allow storing of hashes here
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
def _must_recurse(key)
|
95
|
+
raise ArgumentError.new("at this depth - key must end with slash, or :recurse option must be specified") unless _recurse?(key)
|
96
|
+
end
|
97
|
+
|
98
|
+
def _recurse?(key)
|
99
|
+
return false if @recurse.nil?
|
100
|
+
return (@recurse == :default || @recurse == "*" || @recurse[key] || key.end_with?("/"))
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
class RowAdapter < BaseAdapter
|
106
|
+
def initialize(column, id, prefix)
|
107
|
+
@column = column
|
108
|
+
@store = column.store
|
109
|
+
@prefix = prefix
|
110
|
+
@id = id
|
111
|
+
|
112
|
+
@recurse = @column.recurse
|
113
|
+
if Hash === @recurse
|
114
|
+
@recurse = @recurse[id]
|
115
|
+
elsif "*" == @recurse
|
116
|
+
@recurse = {}
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def [](key)
|
121
|
+
if _recurse?(key)
|
122
|
+
RowAdapter.new(@column, @id, (@prefix || "") + key)
|
123
|
+
else
|
124
|
+
_get[key]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def []=(key, val)
|
129
|
+
key = @prefix + key if @prefix
|
130
|
+
# TODO recurse
|
131
|
+
@store.backend.insert(@column.id, @id, { key => val } , @store.opts)
|
132
|
+
end
|
133
|
+
|
134
|
+
def delete(key)
|
135
|
+
if _recurse?(key)
|
136
|
+
key = @prefix + key if @prefix
|
137
|
+
key = key + "/" unless key.end_with?("/")
|
138
|
+
ohash = @store.backend.get(@column.id, @id, @store.opts)
|
139
|
+
ohash.each_key do |okey|
|
140
|
+
next unless okey == key || okey.start_with?(key)
|
141
|
+
@store.backend.remove(@column.id, @id, okey, @store.opts)
|
142
|
+
end
|
143
|
+
else
|
144
|
+
key = @prefix + key if @prefix
|
145
|
+
@store.backend.remove(@column.id, @id, key, @store.opts)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def method_missing(meth, *args, &block)
|
150
|
+
_get.send(meth, *args, &block)
|
151
|
+
end
|
152
|
+
|
153
|
+
def inspect
|
154
|
+
_get.inspect
|
155
|
+
end
|
156
|
+
|
157
|
+
def to_s
|
158
|
+
_get.to_s
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
def _recurse?(key)
|
163
|
+
return false if @recurse.nil?
|
164
|
+
return (@recurse == :default || @recurse == "*" || @recurse[key] || key.end_with?("/"))
|
165
|
+
end
|
166
|
+
|
167
|
+
def _get
|
168
|
+
# TODO cache
|
169
|
+
ohash = @store.backend.get(@column.id, @id, @store.opts)
|
170
|
+
res = {}
|
171
|
+
ohash.each_pair do |okey, value|
|
172
|
+
if @prefix
|
173
|
+
next unless okey.start_with?(@prefix)
|
174
|
+
okey[@prefix] = ""
|
175
|
+
end
|
176
|
+
keys = okey.split("/")
|
177
|
+
last_key = keys.pop
|
178
|
+
ptr = res
|
179
|
+
keys.each do |key|
|
180
|
+
ptr[key] ||= {}
|
181
|
+
ptr = ptr[key]
|
182
|
+
end
|
183
|
+
ptr[last_key] = value
|
184
|
+
end
|
185
|
+
res
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
module Rkv
|
4
|
+
module Store
|
5
|
+
class MemoryAdapter < BaseAdapter
|
6
|
+
attr_accessor :recurse
|
7
|
+
|
8
|
+
def self.open(opts)
|
9
|
+
self.new(opts)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(opts)
|
13
|
+
@recurse = opts[:recurse] || :default
|
14
|
+
@content = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def [](key)
|
18
|
+
if _recurse?(key)
|
19
|
+
key = key[0..-2] if key.end_with?("/")
|
20
|
+
res = @content[key]
|
21
|
+
return res if res
|
22
|
+
recurse = @recurse
|
23
|
+
if Hash === @recurse
|
24
|
+
recurse = recurse[key]
|
25
|
+
elsif "*" == @recurse
|
26
|
+
recurse = nil
|
27
|
+
end
|
28
|
+
res = MemoryAdapter.new(:recurse => recurse)
|
29
|
+
@content[key] = res
|
30
|
+
return res
|
31
|
+
else
|
32
|
+
return @content[key]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def delete(key)
|
37
|
+
key = key[0..-2] if key.end_with?("/")
|
38
|
+
@content.delete(key)
|
39
|
+
end
|
40
|
+
|
41
|
+
def []=(key, value)
|
42
|
+
# TODO hash value
|
43
|
+
@content[key] = value
|
44
|
+
end
|
45
|
+
|
46
|
+
def inspect
|
47
|
+
@content.inspect
|
48
|
+
end
|
49
|
+
|
50
|
+
def method_missing(meth, *args, &block)
|
51
|
+
@content.send(meth, *args, &block)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def _recurse?(key)
|
57
|
+
return false if @recurse.nil?
|
58
|
+
return (@recurse == :default || @recurse == "*" || @recurse[key] || key.end_with?("/"))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'tokyocabinet'
|
3
|
+
|
4
|
+
module Rkv
|
5
|
+
module Store
|
6
|
+
class TokyocabinetAdapter < BaseAdapter
|
7
|
+
attr_accessor :recurse
|
8
|
+
|
9
|
+
def self.open(opts)
|
10
|
+
self.new(opts)
|
11
|
+
end
|
12
|
+
|
13
|
+
def tc_raise
|
14
|
+
ecode = @bdb.ecode
|
15
|
+
raise "TokyoCabinet - #{@bdb.errmsg(ecode)}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.new(opts)
|
19
|
+
@recurse = opts[:recurse] || :default
|
20
|
+
@bdb = TokyoCabinet::BDB::new
|
21
|
+
|
22
|
+
# open the database
|
23
|
+
unless @bdb.open(opts[:file], TokyoCabinet::BDB::OWRITER | TokyoCabinet::BDB::OCREAT)
|
24
|
+
tc_raise
|
25
|
+
end
|
26
|
+
|
27
|
+
TCHash.new(@bdb, @recurse, "")
|
28
|
+
end
|
29
|
+
|
30
|
+
class TCHash < BaseAdapter
|
31
|
+
def initialize(bdb, recurse, prefix)
|
32
|
+
@bdb = bdb
|
33
|
+
@recurse = recurse
|
34
|
+
@prefix = prefix
|
35
|
+
end
|
36
|
+
|
37
|
+
def close
|
38
|
+
bdb.close
|
39
|
+
end
|
40
|
+
|
41
|
+
def [](key)
|
42
|
+
if _recurse?(key)
|
43
|
+
key = key[0..-2] if key.end_with?("/")
|
44
|
+
new_prefix = "#{@prefix}#{key}/"
|
45
|
+
recurse = @recurse
|
46
|
+
if Hash === @recurse
|
47
|
+
recurse = recurse[key]
|
48
|
+
elsif "*" == @recurse
|
49
|
+
recurse = nil
|
50
|
+
end
|
51
|
+
return TCHash.new(@bdb, recurse, new_prefix)
|
52
|
+
else
|
53
|
+
return _get[key]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete(key)
|
58
|
+
full_key = "#{@prefix}#{key}"
|
59
|
+
if _recurse?(key)
|
60
|
+
full_key = full_key[0..-2] if full_key.end_with?("/")
|
61
|
+
cur = TokyoCabinet::BDBCUR.new(@bdb)
|
62
|
+
return unless cur.jump(full_key)
|
63
|
+
while true
|
64
|
+
ckey = cur.key
|
65
|
+
return unless ckey.start_with?(full_key)
|
66
|
+
cur.out
|
67
|
+
return unless cur.next
|
68
|
+
end
|
69
|
+
else
|
70
|
+
@bdb.delete(full_key)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def []=(key, value)
|
75
|
+
# TODO hash value
|
76
|
+
full_key = "#{@prefix}#{key}"
|
77
|
+
@bdb[full_key] = value
|
78
|
+
end
|
79
|
+
|
80
|
+
def inspect
|
81
|
+
_get().inspect
|
82
|
+
end
|
83
|
+
|
84
|
+
def method_missing(meth, *args, &block)
|
85
|
+
_get().send(meth, *args, &block)
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def _recurse?(key)
|
91
|
+
return true if key.end_with?("/")
|
92
|
+
return false if @recurse.nil?
|
93
|
+
return (@recurse == :default || @recurse == "*" || @recurse[key])
|
94
|
+
end
|
95
|
+
|
96
|
+
def _get
|
97
|
+
cur = TokyoCabinet::BDBCUR.new(@bdb)
|
98
|
+
|
99
|
+
res = {}
|
100
|
+
|
101
|
+
return res unless cur.jump(@prefix)
|
102
|
+
|
103
|
+
while true
|
104
|
+
ckey = cur.key
|
105
|
+
break unless ckey.start_with?(@prefix)
|
106
|
+
value = cur.val
|
107
|
+
ckey[@prefix] = ""
|
108
|
+
keys = ckey.split("/")
|
109
|
+
last_key = keys.pop
|
110
|
+
ptr = res
|
111
|
+
keys.each do |key|
|
112
|
+
ptr[key] ||= {}
|
113
|
+
ptr = ptr[key]
|
114
|
+
end
|
115
|
+
ptr[last_key] = value
|
116
|
+
break unless cur.next
|
117
|
+
end
|
118
|
+
res
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
data/rkv.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{rkv}
|
8
|
-
s.version = "0.0
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Miron Cuperman"]
|
12
|
-
s.date = %q{2010-01-
|
12
|
+
s.date = %q{2010-01-11}
|
13
13
|
s.description = %q{Ruby Key Value - an adapter on top of various key-value stores, supporting Cassandra and others}
|
14
14
|
s.email = %q{c1.github@niftybox.net}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -20,7 +20,14 @@ Gem::Specification.new do |s|
|
|
20
20
|
"README.rdoc",
|
21
21
|
"Rakefile",
|
22
22
|
"VERSION",
|
23
|
+
"lib/rkv.rb",
|
24
|
+
"lib/rkv/rkv.rb",
|
25
|
+
"lib/rkv/store/cassandra_adapter.rb",
|
26
|
+
"lib/rkv/store/memory_adapter.rb",
|
27
|
+
"lib/rkv/store/tokyocabinet_adapter.rb",
|
23
28
|
"rkv.gemspec",
|
29
|
+
"spec/integration/stores_spec.rb",
|
30
|
+
"spec/lib/rkv_store_spec.rb",
|
24
31
|
"spec/spec.opts",
|
25
32
|
"spec/spec_helper.rb"
|
26
33
|
]
|
@@ -30,7 +37,9 @@ Gem::Specification.new do |s|
|
|
30
37
|
s.rubygems_version = %q{1.3.5}
|
31
38
|
s.summary = %q{Ruby Key Value - an adapter on top of various key-value stores}
|
32
39
|
s.test_files = [
|
33
|
-
"spec/
|
40
|
+
"spec/lib/rkv_store_spec.rb",
|
41
|
+
"spec/spec_helper.rb",
|
42
|
+
"spec/integration/stores_spec.rb"
|
34
43
|
]
|
35
44
|
|
36
45
|
if s.respond_to? :specification_version then
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
def test_store(store)
|
4
|
+
userid = "__test1"
|
5
|
+
users = store["Users"]
|
6
|
+
users.delete(userid)
|
7
|
+
user = users[userid]
|
8
|
+
user.batch do
|
9
|
+
user["x1"] = "3"
|
10
|
+
user["x2"] = "4"
|
11
|
+
end
|
12
|
+
users[userid].should == {"x1" => "3", "x2" => "4"}
|
13
|
+
test = []
|
14
|
+
user.each_pair { |k,v| test << [k,v] }
|
15
|
+
test.should == [["x1", "3"], ["x2", "4"]]
|
16
|
+
user.delete("x1")
|
17
|
+
user["abc/"]["def"] = "xyz"
|
18
|
+
user["abc/"]["lmn"] = "xyz"
|
19
|
+
|
20
|
+
users[userid].should == {"abc"=>{"lmn"=>"xyz", "def"=>"xyz"}, "x2"=>"4"}
|
21
|
+
users[userid]["abc/"].should == {"lmn"=>"xyz", "def"=>"xyz"}
|
22
|
+
user["abc/"].delete("def")
|
23
|
+
users[userid].should == {"abc"=>{"lmn"=>"xyz"}, "x2"=>"4"}
|
24
|
+
user.delete("abc/")
|
25
|
+
users[userid].should == {"x2"=>"4"}
|
26
|
+
users.delete(userid)
|
27
|
+
users[userid].should == {}
|
28
|
+
end
|
29
|
+
|
30
|
+
def open_store(store_name)
|
31
|
+
store = nil
|
32
|
+
case store_name
|
33
|
+
when :memory then
|
34
|
+
store = Rkv.open(:memory, :recurse => {"Users" => "*"})
|
35
|
+
when :cassandra then
|
36
|
+
store = Rkv.open(:cassandra, :servers => "127.0.0.1:9160", :keyspace => 'Twitter', :recurse => {"Users" => "*"})
|
37
|
+
when :tokyocabinet then
|
38
|
+
File.unlink "test.tcb"
|
39
|
+
store = Rkv.open(:tokyocabinet, :file => "test.tcb", :recurse => {"Users" => "*"})
|
40
|
+
else
|
41
|
+
raise "Unknown store #{store_name}"
|
42
|
+
end
|
43
|
+
store
|
44
|
+
end
|
45
|
+
|
46
|
+
describe Rkv do
|
47
|
+
[:memory, :cassandra, :tokyocabinet].each do |store_name|
|
48
|
+
it "should work with #{store_name}" do
|
49
|
+
store = nil
|
50
|
+
begin
|
51
|
+
store = open_store(store_name)
|
52
|
+
rescue Exception => e
|
53
|
+
puts "#{e.message}\n#{e.backtrace}"
|
54
|
+
end
|
55
|
+
if store
|
56
|
+
test_store(store)
|
57
|
+
else
|
58
|
+
fail "failed to load store - is it installed?"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
module Rkv
|
4
|
+
module Store
|
5
|
+
class TestAdapter
|
6
|
+
def self.open(opts)
|
7
|
+
return self.new
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe Rkv::Store do
|
14
|
+
before :each do
|
15
|
+
@opts = {:servers => ['dummy1:8888']}
|
16
|
+
@store = Rkv::Store::TestAdapter.new
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should load backend" do
|
20
|
+
Rkv.should_receive(:my_require).with("rkv/store/test_adapter").and_return(true)
|
21
|
+
Rkv::Store::TestAdapter.should_receive(:open).with(@opts).and_return(@store)
|
22
|
+
store = Rkv.open(:test, @opts)
|
23
|
+
store.should == @store
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rkv
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Miron Cuperman
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-01-
|
12
|
+
date: 2010-01-11 00:00:00 -08:00
|
13
13
|
default_executable:
|
14
14
|
dependencies: []
|
15
15
|
|
@@ -26,7 +26,14 @@ files:
|
|
26
26
|
- README.rdoc
|
27
27
|
- Rakefile
|
28
28
|
- VERSION
|
29
|
+
- lib/rkv.rb
|
30
|
+
- lib/rkv/rkv.rb
|
31
|
+
- lib/rkv/store/cassandra_adapter.rb
|
32
|
+
- lib/rkv/store/memory_adapter.rb
|
33
|
+
- lib/rkv/store/tokyocabinet_adapter.rb
|
29
34
|
- rkv.gemspec
|
35
|
+
- spec/integration/stores_spec.rb
|
36
|
+
- spec/lib/rkv_store_spec.rb
|
30
37
|
- spec/spec.opts
|
31
38
|
- spec/spec_helper.rb
|
32
39
|
has_rdoc: true
|
@@ -58,4 +65,6 @@ signing_key:
|
|
58
65
|
specification_version: 3
|
59
66
|
summary: Ruby Key Value - an adapter on top of various key-value stores
|
60
67
|
test_files:
|
68
|
+
- spec/lib/rkv_store_spec.rb
|
61
69
|
- spec/spec_helper.rb
|
70
|
+
- spec/integration/stores_spec.rb
|