makoto-dm-tokyo-cabinet-adapter 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.
data/README ADDED
@@ -0,0 +1,48 @@
1
+ An experimentation to add ORM on top of Tokyo Cabinet
2
+
3
+ What Is Tokyo Cabinet?
4
+ - Modern implementation of DBM(key/value hash style(HDB), but supports fixed length hash(FDB), and b tree(BDB))
5
+ - High concurrency/ high scalability (developed by a developer at Mixi, Japanese equivalent of Facebook)
6
+ - More detail at http://tokyocabinet.sourceforge.net/index.html
7
+
8
+ How dm-tokyo-cabinet-adapter stores data into Tokyo Cabinet.
9
+
10
+ - Each object is stored as "ObjName.bdb" file: object id as key and entire data marshalled as value
11
+ - Each attribute is stored as "ObjNameAttribute.bdb" file: attribute name as key and reference object id as value
12
+ - The above architecture can also be considerd "ObjeName.bdb" as table and "ObjNameAttribute.bdb" as indexes for each attributes.
13
+ - Currently implements basic CRUD, association, and eql finder.
14
+
15
+ Motivation behind the development.
16
+
17
+ - To experiment what you can do with basic hash based database.
18
+ Interesting post at http://groups.google.com/group/merb/browse_thread/thread/a8c6b154576c6270
19
+ - To learn internal of DataMapper and how to implement ORM/adapter
20
+
21
+ How to install
22
+
23
+ 1. Install Tokyo Cabinet http://tokyocabinet.sourceforge.net
24
+ 2. Install Ruby Binding http://tokyocabinet.sourceforge.net/rubydoc/
25
+ 3. Install dm-tokyo-cabinet-adapter
26
+ 3.1 download dm-tokyo-cabinet-adapter
27
+ 3.2 cd to the dir
28
+ 3.3 gem build dm-tokyo-cabinet-adapter.gemspec
29
+ 3.4 gem install dm-tokyo-cabinet-adapter-0.0.2.gem
30
+ 4. Create data dir
31
+ 5. Setup database.yml like below
32
+
33
+ :development:
34
+ :adapter: tokyo_cabinet
35
+ :data_path: <%= Pathname(__FILE__).dirname.expand_path + 'data' %>
36
+
37
+ 6. The rest is usual way to setup datamapper on Merb.
38
+
39
+ Benchmarking results
40
+ http://gist.github.com/25946
41
+
42
+ My current implementation is a lot slower than MySQL and sqlite, but changing some data storage strategies speed up the performance dramatically, so there are lots of potential for optimization.
43
+
44
+ Further research topics/TODO
45
+ - Implement outstanding tasks such as multi conditions, other finder conditions (<, <=, like), Data Types
46
+ - At this moment, it's only uses B tree, as it covers the wide range of functionality (range search, duplicate values, transaction support, and so on). Consider replacing part to HDB or FDB for compact storage and speed.
47
+ - Are there any ways to retrieve first/last key on FDB/HDB?
48
+ - Can TC support "not equal" operation?
@@ -0,0 +1,17 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{dm-tokyo-cabinet-adapter}
3
+ s.version = "0.0.2"
4
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
5
+ s.authors = ["Makoto Inoue"]
6
+ s.date = %q{2008-11-15}
7
+ s.description = %q{A DataMapper adapter for Tokyo Cabinet}
8
+ s.email = %q{inouemak@googlemail.com}
9
+ s.files = ["lib/tokyo_cabinet_adapter.rb", "README", "dm-tokyo-cabinet-adapter.gemspec", "spec/tokyo_cabinet_adapter_spec.rb", "spec/spec_helper.rb"]
10
+ s.homepage = %q{http://github.com/makoto/dm-tokyo-cabinet-adapter}
11
+ s.require_paths = ["lib"]
12
+ s.rubygems_version = %q{1.2.0}
13
+ s.summary = %q{A DatMapper adapter for Tokyo Cabinet}
14
+ s.test_files = ["spec/tokyo_cabinet_adapter_spec.rb", "spec/spec_helper.rb"]
15
+ end
16
+
17
+
@@ -0,0 +1,225 @@
1
+ require 'rubygems'
2
+ require 'dm-core'
3
+ require 'tokyocabinet'
4
+ include TokyoCabinet
5
+
6
+ module DataMapper
7
+ module Adapters
8
+ class TokyoCabinetAdapter < AbstractAdapter
9
+
10
+ def create(resources)
11
+ resource = resources[0]
12
+ attributes = resource.attributes
13
+
14
+ item_id = access_data(resource.model) do |item|
15
+ #Getting the latest id
16
+ #TODO:Find out how to get last id using FDB, rather than BDB
17
+ cur = BDBCUR::new(item)
18
+ cur.last
19
+ attributes[:id] = cur.key.to_i + 1
20
+
21
+ item.put(attributes[:id], Marshal.dump(attributes))
22
+ attributes[:id]
23
+ end
24
+
25
+ resource.instance_variable_set(:@id, item_id)
26
+
27
+ add_index(attributes, resource, item_id)
28
+
29
+ # Seems required to return 1 to update @new_record instance variable at DataMapper::Resource.
30
+ # Not quite sure how it works.
31
+ 1
32
+ end
33
+
34
+ def read_many(query)
35
+ results = parse_query(query)
36
+
37
+ if results
38
+ results = results.sort_by do |result|
39
+ result[query.order.first.property.name] if result
40
+ end
41
+ results = results.reverse if query.order.first.direction == :desc
42
+ end
43
+
44
+ if results # to handle results == nil
45
+ Collection.new(query) do |collection|
46
+ results.each do |result|
47
+ data = map_into_query_field(query, result)
48
+ if data # to handle results == [nil]
49
+ collection.load(data)
50
+ end
51
+ end
52
+ end
53
+ else
54
+ []
55
+ end
56
+ end
57
+
58
+ def read_one(query)
59
+ results = parse_query(query)
60
+ data = (results.class == Array ? results.first : results)
61
+
62
+ if data
63
+ data = map_into_query_field(query, data)
64
+ query.model.load(data, query)
65
+ end
66
+ end
67
+
68
+ def update(attributes, query)
69
+ item_id = get_id(query)
70
+
71
+ old_attributes = get_items_from_id(query, item_id)
72
+ delete_index(old_attributes, query, item_id)
73
+
74
+ # Converting {#<Property:User:name>=>"peter", #<Property:User:age>=>22} to {:age=>22, :name=>"peter"}
75
+ new_attributes = attributes.inject({}){|total,current| total[current[0].name] = current[1]; total}
76
+ add_index(new_attributes, query, item_id)
77
+
78
+ access_data(query.model) do |item|
79
+ raw_data = item.get(item_id)
80
+ if raw_data
81
+ record = Marshal.load(raw_data)
82
+
83
+ attributes.each do |key, value|
84
+ record[key.name.to_sym] = value
85
+ end
86
+
87
+ item.put(item_id, Marshal.dump(record))
88
+ end
89
+ end
90
+ # Seems required to return 1 to update @new_record instance variable at DataMapper::Resource.
91
+ # Not quite sure how it works.
92
+ 1
93
+ end
94
+
95
+ def delete(query)
96
+ item_id = get_id(query)
97
+ attributes = get_items_from_id(query, item_id)
98
+
99
+ delete_index(attributes, query, item_id)
100
+
101
+ access_data(query.model) do |item|
102
+ item.out(item_id)
103
+ end
104
+
105
+ # Seems required to return 1 to update @new_record instance variable at DataMapper::Resource.
106
+ # Not quite sure how it works.
107
+ 1
108
+ end
109
+
110
+ private
111
+ # Access Index file if property is given. If not, access data file
112
+ def access_data(model, property = nil, &block)
113
+ item = BDB::new
114
+ attribute = property.to_s.capitalize if property
115
+ item.open(data_path + "#{model}#{attribute}.bdb", BDB::OWRITER | BDB::OCREAT)
116
+
117
+ result = yield(item)
118
+
119
+ item.close
120
+
121
+ result
122
+ end
123
+
124
+ def data_path
125
+ data_path = DataMapper.repository.adapter.uri[:data_path].to_s + "/"
126
+ end
127
+
128
+ def get_id(query)
129
+ unless query.conditions.empty?
130
+ query.conditions.first.last
131
+ end
132
+ end
133
+
134
+ def get_items_from_id(query, values)
135
+ result = []
136
+ values_in_array = (values.class == Array ? values : [values])
137
+ access_data(query.model) do |item|
138
+ result = values_in_array.map do |value|
139
+ raw_data = item.get(value)
140
+ if raw_data
141
+ Marshal.load(raw_data)
142
+ end
143
+ end
144
+ end
145
+
146
+ values.class == Array ? result : result.first
147
+ end
148
+
149
+ def parse_query(query)
150
+ results = []
151
+
152
+ unless query.conditions.empty?
153
+ operator, property, value = query.conditions.first
154
+
155
+ if property.name == :id # Model.get
156
+ results = get_items_from_id(query, value)
157
+ else # Model.first w argument
158
+ case operator
159
+ when :eql
160
+ then
161
+ item_ids = access_data(query.model, property.name) do |item|
162
+ value = value.first if value.class == Array
163
+ item.getlist(value)
164
+ end
165
+ when :not # TODO: Think about better way to extract, as this is going through data one by one
166
+ then NotImplementedError{"The below code is not working as order is not always correct"}
167
+ else
168
+ raise NotImplementedError("#{operator} is not implmented yet")
169
+ end
170
+ results = get_items_from_id(query, item_ids)
171
+ end
172
+ else # Model.all w/o argument
173
+ access_data(query.model) do |item|
174
+ #Getting the first id
175
+ #TODO:Find out how to get first id using FDB, rather than BDB
176
+ raw_data = BDBCUR::new(item)
177
+ if raw_data.first
178
+ while key = raw_data.key
179
+ results << Marshal.load(raw_data.val)
180
+ raw_data.next
181
+ end
182
+ end
183
+ end
184
+ end
185
+ results
186
+ end
187
+
188
+ def map_into_query_field(query, data)
189
+ if data
190
+ query.fields.map do |property|
191
+ data[property.field.to_sym]
192
+ end
193
+ end
194
+ end
195
+
196
+ def delete_index(attributes, query, item_id)
197
+ # Don't need id attribute and attribut with no data.
198
+ attributes.reject{|k,v| k == :id || v == nil}.each do | k, v|
199
+ access_data(query.model, k) do |item|
200
+ items = item.getlist(v)
201
+ items = items - [item_id]
202
+ item.out(v)
203
+ if items.size > 0
204
+ item.putlist(v, items)
205
+ end
206
+ end
207
+ end
208
+ end
209
+
210
+ def add_index(attributes, query, item_id)
211
+ attributes.each do |key, value|
212
+ # Creating index for each attributes except id
213
+ unless key == :id
214
+ access_data(query.model, key) do |item|
215
+ item.putlist(value, [item_id])
216
+ end
217
+ end
218
+ end
219
+ end
220
+
221
+ end # class AbstractAdapter
222
+ end # module Adapters
223
+ end # module DataMapper
224
+
225
+
@@ -0,0 +1,29 @@
1
+ require 'rubygems'
2
+ require 'dm-core'
3
+
4
+ require 'pathname'
5
+ require Pathname(__FILE__).dirname.parent.expand_path + 'lib/tokyo_cabinet_adapter'
6
+
7
+ DataMapper.setup(:default, {
8
+ :adapter => 'tokyo_cabinet',
9
+ :data_path => Pathname(__FILE__).dirname.parent.expand_path + 'data'
10
+ })
11
+
12
+ class Post
13
+ include DataMapper::Resource
14
+
15
+ property :id, Serial
16
+ property :title, String
17
+
18
+ belongs_to :user
19
+ end
20
+
21
+ class User
22
+ include DataMapper::Resource
23
+
24
+ property :id, Serial
25
+ property :name, String
26
+ property :age, Integer
27
+
28
+ has n, :posts
29
+ end
@@ -0,0 +1,231 @@
1
+ require 'pathname'
2
+ require Pathname(__FILE__).dirname.expand_path + 'spec_helper'
3
+
4
+ describe DataMapper::Adapters::TokyoCabinetAdapter do
5
+ before(:each) do
6
+ db_files = Dir.glob(DataMapper.repository.adapter.uri[:data_path].to_s + "/*.*db")
7
+ FileUtils.rm(db_files)
8
+ end
9
+
10
+ describe "Repository" do
11
+ it "should return adapter name" do
12
+ DataMapper.repository.adapter.uri[:adapter].should == 'tokyo_cabinet'
13
+ end
14
+
15
+ it "should return data path" do
16
+ DataMapper.repository.adapter.uri[:data_path].should == Pathname(__FILE__).dirname.parent.expand_path + 'data'
17
+ end
18
+ end
19
+
20
+ describe "CRUD" do
21
+ before(:each) do
22
+ @user = User.create(:name => 'tom')
23
+ end
24
+ describe "create" do
25
+ it "should assign id and attributes" do
26
+ user = User.create
27
+ user.should be_an_instance_of(User)
28
+ user.id.should_not == nil
29
+ end
30
+
31
+ it "should increment id" do
32
+ first_user = User.create
33
+ second_user = User.create
34
+ first_user.id.should == second_user.id - 1
35
+ end
36
+ end
37
+
38
+ describe "get" do
39
+ it "should get an item" do
40
+ User.get(@user.id).should == @user
41
+ end
42
+
43
+ it "should raise error if item does not exist" do
44
+ non_existance_number = 100
45
+ lambda{User.get!(non_existance_number)}.should raise_error(DataMapper::ObjectNotFoundError)
46
+ end
47
+ end
48
+
49
+ describe "update" do
50
+ before(:each) do
51
+ @user.name = 'peter'
52
+ @user.age = 22
53
+
54
+ @user.save
55
+ end
56
+ it "should update an item" do
57
+ User.get(@user.id) == @user
58
+ end
59
+
60
+ it "should reflect index" do
61
+ User.first(:name => @user.name).should == @user
62
+ end
63
+ end
64
+
65
+ describe "destroy" do
66
+ before(:each) do
67
+ @user.destroy
68
+ end
69
+ it "should destroy an item" do
70
+ lambda{User.get!(@user.id)}.should raise_error(DataMapper::ObjectNotFoundError)
71
+ end
72
+ it "should reflect index" do
73
+ User.first(:name => @user.name).should == nil
74
+ User.all(:name => @user.name).should == []
75
+ end
76
+ end
77
+ end
78
+
79
+ describe 'Finder' do
80
+ describe "when no data" do
81
+ it "first should return nil" do
82
+ User.first(:name => 'someone').should == nil
83
+ end
84
+
85
+ it "all should return []" do
86
+ User.all(:name => 'someone').should == []
87
+ end
88
+ end
89
+
90
+ describe "when data" do
91
+ before(:each) do
92
+ @tom = User.create(:name => 'tom')
93
+ @peter = User.create(:name => 'peter')
94
+ @post = Post.create
95
+ end
96
+
97
+ it 'should get one record per model' do
98
+ User.first.should == @tom
99
+ Post.first.should == @post
100
+ end
101
+
102
+ it 'should return collection of all records per model' do
103
+ Post.all.should have(1).post
104
+ User.all.should have(2).users
105
+ end
106
+ end
107
+
108
+ end
109
+
110
+ describe "Matcher" do
111
+ describe "first" do
112
+ describe "eql" do
113
+ before(:each) do
114
+ @tom = User.create(:name => 'tom', :age => 32)
115
+ @peter = User.create(:name => 'peter', :age => 32)
116
+ end
117
+
118
+ it "should return a record " do
119
+ User.first(:name => 'tom').should == @tom
120
+ end
121
+ it "should return first record when searched by an attribute which allows duplicate entry" do
122
+ User.first(:age => 32).should == @tom
123
+ end
124
+ end
125
+
126
+ describe "not" do
127
+ before(:each) do
128
+ @tom = User.create(:name => 'tom', :age => 2)
129
+ @peter = User.create(:name => 'peter', :age => 3)
130
+ @mark = User.create(:name => 'mark', :age => 5)
131
+ end
132
+ it "should return a record for string when non matching value comes first" do
133
+ pending()
134
+ User.first(:name.not => 'tom').should == @peter
135
+ end
136
+
137
+ it "should return a record for string when matching value comes first" do
138
+ pending()
139
+ User.first(:name.not => 'peter').should == @tom
140
+ end
141
+ it "should return a record for numeric" do
142
+ pending
143
+ User.first(:age.not => 2).should == @peter
144
+ end
145
+
146
+ end
147
+ it 'should get a record by not matcher'
148
+ it 'should get a record by gt matcher'
149
+ it 'should get a record by gte matcher'
150
+ it 'should get a record by lt matcher'
151
+ it 'should get a record by lte matcher'
152
+ it 'should get a record with multiple matchers'
153
+ end
154
+ describe "all" do
155
+ before(:each) do
156
+ @tom = User.create(:name => 'tom', :age => 2)
157
+ @peter = User.create(:name => 'peter', :age => 3)
158
+ @mark = User.create(:name => 'mark', :age => 5)
159
+ @andy = User.create(:name => 'andy', :age => 5)
160
+ end
161
+ it 'should get records by eql matcher' do
162
+ User.all(:age => 5).should == [@mark, @andy]
163
+ end
164
+ it 'should get records by not matcher'
165
+ it 'should get records by gt matcher'
166
+ it 'should get records by gte matcher'
167
+ it 'should get records by lt matcher'
168
+ it 'should get records by lte matcher'
169
+ it 'should get records with multiple matchers'
170
+ end
171
+ end
172
+
173
+ describe "DataType" do
174
+ before(:each) do
175
+ @dave = User.create(:name => 'dave', :age => 5)
176
+ @charles = User.create(:name => 'charles', :age => 15)
177
+ @bob = User.create(:name => 'bob', :age => 3)
178
+ @andy = User.create(:name => 'andy', :age => 4)
179
+ end
180
+
181
+ describe "sorting" do
182
+ # Tokyo Cabinet itself does not provide sorting, so done at ruby level.
183
+ it "should order by alphabet asc" do
184
+ User.all(:order => [:name]).should == [@andy, @bob, @charles, @dave]
185
+ end
186
+
187
+ it "should order by alphabet desc" do
188
+ User.all(:order => [:name.desc]).should == [@andy, @bob, @charles, @dave].reverse
189
+ end
190
+
191
+ it "should order by numeric asc" do
192
+ User.all(:order => [:age]).should == [@bob, @andy, @dave, @charles]
193
+ end
194
+
195
+ it "should order by numeric desc" do
196
+ User.all(:order => [:age.desc]).should == [@bob, @andy, @dave, @charles].reverse
197
+ end
198
+ end
199
+ end
200
+
201
+ describe 'associations' do
202
+ before(:each) do
203
+ @user = User.create(:name => 'tom')
204
+ @post = Post.create(:title => 'Good morning', :user => @user)
205
+ end
206
+
207
+ describe "Adding association" do
208
+ it 'should work with belongs_to associations' do
209
+ User.get(@user.id).posts.should include(@post)
210
+ end
211
+
212
+ it 'should work with has n associations' do
213
+ Post.get(@post.id).user.should == @user
214
+ end
215
+ end
216
+ describe "Appending association" do
217
+ before(:each) do
218
+ @post2 = Post.create(:title => 'Good morning', :user => @user)
219
+ @user.posts << @post2
220
+ end
221
+ it 'should work with belongs_to associations' do
222
+ User.get(@user.id).posts.should == [@post, @post2]
223
+ end
224
+
225
+ it 'should work with has n associations' do
226
+ Post.get(@post.id).user.should == @user
227
+ Post.get(@post2.id).user.should == @user
228
+ end
229
+ end
230
+ end
231
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: makoto-dm-tokyo-cabinet-adapter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Makoto Inoue
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2008-11-15 00:00:00 -08:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: A DataMapper adapter for Tokyo Cabinet
17
+ email: inouemak@googlemail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - lib/tokyo_cabinet_adapter.rb
26
+ - README
27
+ - dm-tokyo-cabinet-adapter.gemspec
28
+ - spec/tokyo_cabinet_adapter_spec.rb
29
+ - spec/spec_helper.rb
30
+ has_rdoc: false
31
+ homepage: http://github.com/makoto/dm-tokyo-cabinet-adapter
32
+ post_install_message:
33
+ rdoc_options: []
34
+
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: "0"
42
+ version:
43
+ required_rubygems_version: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: "0"
48
+ version:
49
+ requirements: []
50
+
51
+ rubyforge_project:
52
+ rubygems_version: 1.2.0
53
+ signing_key:
54
+ specification_version: 2
55
+ summary: A DatMapper adapter for Tokyo Cabinet
56
+ test_files:
57
+ - spec/tokyo_cabinet_adapter_spec.rb
58
+ - spec/spec_helper.rb