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 CHANGED
@@ -19,3 +19,6 @@ rdoc
19
19
  pkg
20
20
 
21
21
  ## PROJECT::SPECIFIC
22
+
23
+ #Tokyocabinet test DB
24
+ test.tcb
@@ -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
- client = Rkv::Store.new(:cassandra, { :servers => ['127.0.0.1:9160'], :keyspace => "Blog" })
16
- store = client[:Users]
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
- store["5"] = { :name => "Dev Random", :role => "admin" }
19
- puts store["3"].inspect
28
+ user = users["5"]
29
+ user["name"] = "Dev Random"
30
+ user["role"] = "admin"
31
+
32
+ puts users["3"].inspect
20
33
 
21
- store.batch do
22
- store[id1] = { :name => "John Doe" }
23
- store[id2] = { :name => "Jane Doe" }
34
+ users.batch do
35
+ users[id1]["name"] = "John Doe"
36
+ users[id2]["name"] = "Jane Doe"
24
37
  end
25
38
 
26
- store = client[:UserRelationships]
27
- store[id1, "posts"] = post_id1
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
- * Create a keystore feature support matrix
71
+ * Store support
72
+ * Performance testing
73
+ * Keystore feature support matrix
74
+ * Object mapper?
50
75
 
data/Rakefile CHANGED
@@ -25,6 +25,7 @@ end
25
25
  Spec::Rake::SpecTask.new(:rcov) do |spec|
26
26
  spec.libs << 'lib' << 'spec'
27
27
  spec.pattern = 'spec/**/*_spec.rb'
28
+ spec.rcov_opts = ['-x.gem/gems,spec']
28
29
  spec.rcov = true
29
30
  end
30
31
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.2.0
@@ -0,0 +1 @@
1
+ require 'rkv/rkv'
@@ -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
+
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{rkv}
8
- s.version = "0.0.1"
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-09}
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/spec_helper.rb"
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.1
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-09 00:00:00 -08:00
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