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 +48 -0
- data/dm-tokyo-cabinet-adapter.gemspec +17 -0
- data/lib/tokyo_cabinet_adapter.rb +225 -0
- data/spec/spec_helper.rb +29 -0
- data/spec/tokyo_cabinet_adapter_spec.rb +231 -0
- metadata +58 -0
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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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
|