active_document 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +3 -0
- data/LICENSE +20 -0
- data/README.rdoc +111 -0
- data/Rakefile +43 -0
- data/VERSION.yml +4 -0
- data/active_document.gemspec +57 -0
- data/examples/foo.rb +10 -0
- data/examples/photo.rb +9 -0
- data/lib/active_document.rb +9 -0
- data/lib/active_document/base.rb +390 -0
- data/test/.gitignore +1 -0
- data/test/active_document_test.rb +417 -0
- data/test/deadlock_test.rb +127 -0
- data/test/test_helper.rb +8 -0
- metadata +73 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Justin Balthrop
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
= ActiveDocument
|
2
|
+
|
3
|
+
ActiveDocument is a persistent Model store built on Berkeley DB. It was inspired by
|
4
|
+
ActiveRecord, and in some cases, it can be used as a drop-in replacement. The performance
|
5
|
+
of ActiveDocument can exceed a traditional ORM for many applications, because the database
|
6
|
+
is stored locally and all lookups must use a predefined index. Also, attributes do not
|
7
|
+
have to be cast after they are read from the database like in ActiveRecord. Instead, Ruby
|
8
|
+
objects are stored directly in Berkeley DB and loaded using Marshal, which makes loading
|
9
|
+
objects much faster. For more information on the diffences between Berkeley DB and a
|
10
|
+
relational Database, see (http://www.oracle.com/database/docs/Berkeley-DB-v-Relational.pdf).
|
11
|
+
|
12
|
+
== Usage:
|
13
|
+
|
14
|
+
require 'rubygems'
|
15
|
+
require 'active_document'
|
16
|
+
|
17
|
+
class User < ActiveDocument::Base
|
18
|
+
path '/data/bdb'
|
19
|
+
accessor :first_name, :last_name, :username, :email_address, :tags
|
20
|
+
|
21
|
+
primary_key :username
|
22
|
+
index_by [:last_name, :first_name]
|
23
|
+
index_by :email_address, :unique => true
|
24
|
+
index_by :tags, :multi_key => true
|
25
|
+
end
|
26
|
+
|
27
|
+
User.create(
|
28
|
+
:first_name => 'John',
|
29
|
+
:last_name => 'Stewart',
|
30
|
+
:username => 'lefty',
|
31
|
+
:email_address => 'john@thedailyshow.com',
|
32
|
+
:tags => [:funny, :liberal]
|
33
|
+
)
|
34
|
+
|
35
|
+
User.create(
|
36
|
+
:first_name => 'Martha',
|
37
|
+
:last_name => 'Stewart',
|
38
|
+
:username => 'helen',
|
39
|
+
:email_address => 'martha@marthastewart.com',
|
40
|
+
:tags => [:conservative, :convict]
|
41
|
+
)
|
42
|
+
|
43
|
+
User.create(
|
44
|
+
:first_name => 'Stephen',
|
45
|
+
:last_name => 'Colbert',
|
46
|
+
:username => 'steve',
|
47
|
+
:email_address => 'steve@thereport.com',
|
48
|
+
:tags => [:conservative, :funny]
|
49
|
+
)
|
50
|
+
|
51
|
+
User.find('lefty').attributes
|
52
|
+
=> {"username"=>"lefty", "last_name"=>"Stewart", "email_address"=>"john@thedailyshow.com", "first_name"=>"John"}
|
53
|
+
|
54
|
+
User.find_by_email_address("john@thedailyshow.com").username
|
55
|
+
=> "lefty"
|
56
|
+
|
57
|
+
User.find_all_by_last_name("Stewart").collect {|u| u.first_name}
|
58
|
+
=> ["John", "Martha"]
|
59
|
+
|
60
|
+
User.find_all_by_tag(:funny).collect {|u| u.username}
|
61
|
+
=> ["lefty", "steve"]
|
62
|
+
|
63
|
+
=== Complex finds:
|
64
|
+
|
65
|
+
Any find can take multiple keys, a key range, or multiple key ranges.
|
66
|
+
|
67
|
+
User.create(
|
68
|
+
:first_name => 'Will',
|
69
|
+
:last_name => 'Smith',
|
70
|
+
:username => 'legend',
|
71
|
+
:email_address => 'will@smith.com',
|
72
|
+
:tags => [:actor, :rapper]
|
73
|
+
)
|
74
|
+
|
75
|
+
User.find_all_by_last_name("Stewart", "Smith").collect {|u| u.username}
|
76
|
+
=> ["lefty", "helen", "legend"]
|
77
|
+
|
78
|
+
User.find_all_by_last_name("Smith".."Stuart").collect {|u| u.username}
|
79
|
+
=> ["legend", "lefty", "helen"]
|
80
|
+
|
81
|
+
User.find_all_by_last_name("Smith".."Stuart", "Colbert").collect {|u| u.username}
|
82
|
+
=> ["legend", "lefty", "helen", "steve"]
|
83
|
+
|
84
|
+
User.find_all_by_last_name("Aardvark".."Daisy", "Smith".."Stuart").collect {|u| u.username}
|
85
|
+
=> ["steve", "legend", "lefty", "helen"]
|
86
|
+
|
87
|
+
=== Limit and Offset:
|
88
|
+
|
89
|
+
Any find can also take :limit, :offset, :page, and :per_page as options. These can be used for paginating large lists.
|
90
|
+
|
91
|
+
User.find_all_by_username(:limit => 2).collect {|u| u.username}
|
92
|
+
=> ["helen", "lefty"]
|
93
|
+
|
94
|
+
User.find_all_by_username(:limit => 2, :offset => 2).collect {|u| u.username}
|
95
|
+
=> ["legend", "steve"]
|
96
|
+
|
97
|
+
User.find_all_by_username(:per_page => 2, :page => 1).collect {|u| u.username}
|
98
|
+
=> ["helen", "lefty"]
|
99
|
+
|
100
|
+
User.find_all_by_username(:per_page => 2, :page => 2).collect {|u| u.username}
|
101
|
+
=> ["legend", "steve"]
|
102
|
+
|
103
|
+
== Install:
|
104
|
+
|
105
|
+
sudo gem install ninjudd-bdb -s http://gems.github.com
|
106
|
+
sudo gem install ninjudd-tuple -s http://gems.github.com
|
107
|
+
sudo gem install ninjudd-active_document -s http://gems.github.com
|
108
|
+
|
109
|
+
== License:
|
110
|
+
|
111
|
+
Copyright (c) 2009 Justin Balthrop, Geni.com; Published under The MIT License, see LICENSE
|
data/Rakefile
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/testtask'
|
3
|
+
require 'rake/rdoctask'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'jeweler'
|
7
|
+
Jeweler::Tasks.new do |s|
|
8
|
+
s.name = "active_document"
|
9
|
+
s.summary = %Q{Schemaless models in Berkeley DB}
|
10
|
+
s.email = "code@justinbalthrop.com"
|
11
|
+
s.homepage = "http://github.com/ninjudd/active_document"
|
12
|
+
s.description = "Schemaless models in Berkeley DB."
|
13
|
+
s.authors = ["Justin Balthrop"]
|
14
|
+
end
|
15
|
+
rescue LoadError
|
16
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
17
|
+
end
|
18
|
+
|
19
|
+
Rake::TestTask.new do |t|
|
20
|
+
t.libs << 'lib'
|
21
|
+
t.pattern = 'test/**/*_test.rb'
|
22
|
+
t.verbose = false
|
23
|
+
end
|
24
|
+
|
25
|
+
Rake::RDocTask.new do |rdoc|
|
26
|
+
rdoc.rdoc_dir = 'rdoc'
|
27
|
+
rdoc.title = 'active_document'
|
28
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
29
|
+
rdoc.rdoc_files.include('README*')
|
30
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
31
|
+
end
|
32
|
+
|
33
|
+
begin
|
34
|
+
require 'rcov/rcovtask'
|
35
|
+
Rcov::RcovTask.new do |t|
|
36
|
+
t.libs << 'test'
|
37
|
+
t.test_files = FileList['test/**/*_test.rb']
|
38
|
+
t.verbose = true
|
39
|
+
end
|
40
|
+
rescue LoadError
|
41
|
+
end
|
42
|
+
|
43
|
+
task :default => :test
|
data/VERSION.yml
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{active_document}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Justin Balthrop"]
|
12
|
+
s.date = %q{2009-11-19}
|
13
|
+
s.description = %q{Schemaless models in Berkeley DB.}
|
14
|
+
s.email = %q{code@justinbalthrop.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.rdoc"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".gitignore",
|
21
|
+
"LICENSE",
|
22
|
+
"README.rdoc",
|
23
|
+
"Rakefile",
|
24
|
+
"VERSION.yml",
|
25
|
+
"active_document.gemspec",
|
26
|
+
"examples/foo.rb",
|
27
|
+
"examples/photo.rb",
|
28
|
+
"lib/active_document.rb",
|
29
|
+
"lib/active_document/base.rb",
|
30
|
+
"test/.gitignore",
|
31
|
+
"test/active_document_test.rb",
|
32
|
+
"test/deadlock_test.rb",
|
33
|
+
"test/test_helper.rb"
|
34
|
+
]
|
35
|
+
s.homepage = %q{http://github.com/ninjudd/active_document}
|
36
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
37
|
+
s.require_paths = ["lib"]
|
38
|
+
s.rubygems_version = %q{1.3.5}
|
39
|
+
s.summary = %q{Schemaless models in Berkeley DB}
|
40
|
+
s.test_files = [
|
41
|
+
"test/active_document_test.rb",
|
42
|
+
"test/deadlock_test.rb",
|
43
|
+
"test/test_helper.rb",
|
44
|
+
"examples/foo.rb",
|
45
|
+
"examples/photo.rb"
|
46
|
+
]
|
47
|
+
|
48
|
+
if s.respond_to? :specification_version then
|
49
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
50
|
+
s.specification_version = 3
|
51
|
+
|
52
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
53
|
+
else
|
54
|
+
end
|
55
|
+
else
|
56
|
+
end
|
57
|
+
end
|
data/examples/foo.rb
ADDED
data/examples/photo.rb
ADDED
@@ -0,0 +1,390 @@
|
|
1
|
+
class ActiveDocument::Base
|
2
|
+
def self.path(path = nil)
|
3
|
+
@path = path if path
|
4
|
+
@path ||= (self == ActiveDocument::Base ? nil : ActiveDocument::Base.path)
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.path=(path)
|
8
|
+
@path = path
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.database_name(database_name = nil)
|
12
|
+
if database_name
|
13
|
+
raise 'cannot modify database_name after db has been initialized' if @database_name
|
14
|
+
@database_name = database_name
|
15
|
+
else
|
16
|
+
return if self == ActiveDocument::Base
|
17
|
+
@database_name ||= name.underscore.gsub('/', '-').pluralize
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.database
|
22
|
+
@database
|
23
|
+
end
|
24
|
+
|
25
|
+
def database
|
26
|
+
self.class.database
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.transaction(&block)
|
30
|
+
database.transaction(&block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def transaction(&block)
|
34
|
+
self.class.transaction(&block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.checkpoint(opts = {})
|
38
|
+
database.checkpoint(opts)
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.create(*args)
|
42
|
+
model = new(*args)
|
43
|
+
model.save
|
44
|
+
model
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.primary_key(field_or_fields, opts = {})
|
48
|
+
raise 'primary key already defined' if @database
|
49
|
+
|
50
|
+
if @partition_by = opts[:partition_by]
|
51
|
+
@database = Bdb::PartitionedDatabase.new(database_name, :path => path, :partition_by => @partition_by)
|
52
|
+
(class << self; self; end).instance_eval do
|
53
|
+
alias_method opts[:partition_by].to_s.pluralize, :partitions
|
54
|
+
alias_method "with_#{opts[:partition_by]}", :with_partition
|
55
|
+
alias_method "with_each_#{opts[:partition_by]}", :with_each_partition
|
56
|
+
end
|
57
|
+
else
|
58
|
+
@database = Bdb::Database.new(database_name, :path => path)
|
59
|
+
end
|
60
|
+
|
61
|
+
field = define_field_accessor(field_or_fields)
|
62
|
+
define_find_methods(field, :field => :primary_key) # find_by_field1_and_field2
|
63
|
+
|
64
|
+
define_field_accessor(field_or_fields, :primary_key)
|
65
|
+
define_find_methods(:primary_key) # find_by_primary_key
|
66
|
+
|
67
|
+
# Define shortcuts for partial keys.
|
68
|
+
define_partial_shortcuts(field_or_fields, :primary_key)
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.partitions
|
72
|
+
database.partitions
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.with_partition(partition, &block)
|
76
|
+
database.with_partition(partition, &block)
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.with_each_partition(&block)
|
80
|
+
database.partitions.each do |partition|
|
81
|
+
database.with_partition(partition, &block)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.partition_by
|
86
|
+
@partition_by
|
87
|
+
end
|
88
|
+
|
89
|
+
def partition_by
|
90
|
+
self.class.partition_by
|
91
|
+
end
|
92
|
+
|
93
|
+
def partition
|
94
|
+
send(partition_by) if partition_by
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.index_by(field_or_fields, opts = {})
|
98
|
+
raise "cannot have a multi_key index on an aggregate key" if opts[:multi_key] and field_or_fields.kind_of?(Array)
|
99
|
+
|
100
|
+
field = define_field_accessor(field_or_fields)
|
101
|
+
database.index_by(field, opts)
|
102
|
+
|
103
|
+
field_name = opts[:multi_key] ? field.to_s.singularize : field
|
104
|
+
define_find_methods(field_name, :field => field) # find_by_field1_and_field2
|
105
|
+
|
106
|
+
# Define shortcuts for partial keys.
|
107
|
+
define_partial_shortcuts(field_or_fields, field)
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.close_environment
|
111
|
+
# Will close all databases in the environment.
|
112
|
+
environment.close
|
113
|
+
end
|
114
|
+
|
115
|
+
def self.find_by(field, *args)
|
116
|
+
opts = extract_opts(args)
|
117
|
+
opts[:field] = field
|
118
|
+
args << :all if args.empty?
|
119
|
+
args << opts
|
120
|
+
database.get(*args)
|
121
|
+
end
|
122
|
+
|
123
|
+
def self.find(key, opts = {})
|
124
|
+
doc = database.get(key, opts).first
|
125
|
+
raise ActiveDocument::DocumentNotFound, "Couldn't find #{name} with id #{key.inspect}" unless doc
|
126
|
+
doc
|
127
|
+
end
|
128
|
+
|
129
|
+
def self.count(field, key)
|
130
|
+
database.count(field, key)
|
131
|
+
end
|
132
|
+
|
133
|
+
def self.define_field_accessor(field_or_fields, field = nil)
|
134
|
+
if field_or_fields.kind_of?(Array)
|
135
|
+
field ||= field_or_fields.join('_and_').to_sym
|
136
|
+
define_method(field) do
|
137
|
+
field_or_fields.collect {|f| self.send(f)}.flatten
|
138
|
+
end
|
139
|
+
elsif field
|
140
|
+
define_method(field) do
|
141
|
+
self.send(field_or_fields)
|
142
|
+
end
|
143
|
+
else
|
144
|
+
field = field_or_fields.to_sym
|
145
|
+
end
|
146
|
+
field
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.define_find_methods(name, config = {})
|
150
|
+
field = config[:field] || name
|
151
|
+
|
152
|
+
(class << self; self; end).instance_eval do
|
153
|
+
define_method("find_by_#{name}") do |*args|
|
154
|
+
modify_opts(args) do |opts|
|
155
|
+
opts[:limit] = 1
|
156
|
+
opts[:partial] ||= config[:partial]
|
157
|
+
end
|
158
|
+
find_by(field, *args).first
|
159
|
+
end
|
160
|
+
|
161
|
+
define_method("find_all_by_#{name}") do |*args|
|
162
|
+
modify_opts(args) do |opts|
|
163
|
+
opts[:partial] ||= config[:partial]
|
164
|
+
end
|
165
|
+
find_by(field, *args)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def self.define_partial_shortcuts(fields, primary_field)
|
171
|
+
return unless fields.kind_of?(Array)
|
172
|
+
|
173
|
+
(fields.size - 1).times do |i|
|
174
|
+
name = fields[0..i].join('_and_')
|
175
|
+
next if respond_to?("find_by_#{name}")
|
176
|
+
define_find_methods(name, :field => primary_field, :partial => true)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def self.timestamps
|
181
|
+
reader(:created_at, :updated_at, :deleted_at)
|
182
|
+
end
|
183
|
+
|
184
|
+
def self.defaults(defaults = {})
|
185
|
+
@defaults ||= {}
|
186
|
+
@defaults.merge!(defaults)
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.default(attr, default)
|
190
|
+
defaults[attr] = default
|
191
|
+
end
|
192
|
+
|
193
|
+
def self.reader(*attrs)
|
194
|
+
attrs.each do |attr|
|
195
|
+
define_method(attr) do
|
196
|
+
read_attribute(attr)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
def self.bool_reader(*attrs)
|
202
|
+
attrs.each do |attr|
|
203
|
+
define_method(attr) do
|
204
|
+
!!read_attribute(attr)
|
205
|
+
end
|
206
|
+
|
207
|
+
define_method("#{attr}?") do
|
208
|
+
!!read_attribute(attr)
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def self.writer(*attrs)
|
214
|
+
attrs.each do |attr|
|
215
|
+
define_method("#{attr}=") do |value|
|
216
|
+
attributes[attr] = value
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def self.accessor(*attrs)
|
222
|
+
reader(*attrs)
|
223
|
+
writer(*attrs)
|
224
|
+
end
|
225
|
+
|
226
|
+
def self.bool_accessor(*attrs)
|
227
|
+
bool_reader(*attrs)
|
228
|
+
writer(*attrs)
|
229
|
+
end
|
230
|
+
|
231
|
+
def self.save_method(method_name)
|
232
|
+
define_method("#{method_name}!") do |*args|
|
233
|
+
transaction do
|
234
|
+
value = send(method_name, *args)
|
235
|
+
save
|
236
|
+
value
|
237
|
+
end
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def initialize(attributes = {}, saved_attributes = nil)
|
242
|
+
@attributes = HashWithIndifferentAccess.new(attributes) if attributes
|
243
|
+
@saved_attributes = HashWithIndifferentAccess.new(saved_attributes) if saved_attributes
|
244
|
+
|
245
|
+
# Initialize defaults if this is a new record.
|
246
|
+
if @saved_attributes.nil?
|
247
|
+
self.class.defaults.each do |attr, default|
|
248
|
+
next if @attributes.has_key?(attr)
|
249
|
+
@attributes[attr] = default.is_a?(Proc) ? default.bind(self).call : default.dup
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Set the partition field in case we are in a with_partition block.
|
254
|
+
if partition_by and partition.nil?
|
255
|
+
set_method = "#{partition_by}="
|
256
|
+
self.send(set_method, database.partition) if respond_to?(set_method)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
attr_reader :saved_attributes
|
261
|
+
alias locator_key bdb_locator_key
|
262
|
+
|
263
|
+
def attributes
|
264
|
+
@attributes ||= Marshal.load(Marshal.dump(saved_attributes))
|
265
|
+
end
|
266
|
+
|
267
|
+
def read_attribute(attr)
|
268
|
+
if @attributes.nil?
|
269
|
+
saved_attributes[attr]
|
270
|
+
else
|
271
|
+
attributes[attr]
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
save_method :update_attributes
|
276
|
+
def update_attributes(attrs = {})
|
277
|
+
attrs.each do |field, value|
|
278
|
+
self.send("#{field}=", value)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
def to_json(*args)
|
283
|
+
attributes.to_json(*args)
|
284
|
+
end
|
285
|
+
|
286
|
+
def ==(other)
|
287
|
+
return false unless other.class == self.class
|
288
|
+
attributes == other.attributes
|
289
|
+
end
|
290
|
+
|
291
|
+
def new_record?
|
292
|
+
@saved_attributes.nil?
|
293
|
+
end
|
294
|
+
|
295
|
+
def changed?(field = nil)
|
296
|
+
return false unless @attributes and @saved_attributes
|
297
|
+
|
298
|
+
if field
|
299
|
+
send(field) != saved.send(field)
|
300
|
+
else
|
301
|
+
attributes != saved_attributes
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
def saved
|
306
|
+
raise 'no saved attributes for new record' if new_record?
|
307
|
+
@saved ||= self.class.new(saved_attributes)
|
308
|
+
end
|
309
|
+
|
310
|
+
def clone(changed_attributes = {})
|
311
|
+
cloned_attributes = Marshal.load(Marshal.dump(attributes))
|
312
|
+
uncloned_fields.each do |attr|
|
313
|
+
cloned_attributes.delete(attr)
|
314
|
+
end
|
315
|
+
cloned_attributes.merge!(changed_attributes)
|
316
|
+
self.class.new(cloned_attributes)
|
317
|
+
end
|
318
|
+
|
319
|
+
def self.uncloned_fields(*attrs)
|
320
|
+
if attrs.empty?
|
321
|
+
@uncloned_fields ||= [:created_at, :updated_at, :deleted_at]
|
322
|
+
else
|
323
|
+
uncloned_fields.concat(attrs)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def save(opts = {})
|
328
|
+
time = opts[:updated_at] || Time.now
|
329
|
+
attributes[:updated_at] = time if respond_to?(:updated_at)
|
330
|
+
attributes[:created_at] ||= time if respond_to?(:created_at) and new_record?
|
331
|
+
|
332
|
+
opts = {}
|
333
|
+
if changed?(:primary_key) or (partition_by and changed?(partition_by))
|
334
|
+
opts[:create] = true
|
335
|
+
saved.destroy
|
336
|
+
else
|
337
|
+
opts[:create] = new_record?
|
338
|
+
end
|
339
|
+
|
340
|
+
@saved_attributes = attributes
|
341
|
+
@attributes = nil
|
342
|
+
@saved = nil
|
343
|
+
database.set(primary_key, self, opts)
|
344
|
+
rescue Bdb::DbError => e
|
345
|
+
raise(ActiveDocument::DuplicatePrimaryKey, e) if e.code == Bdb::DB_KEYEXIST
|
346
|
+
raise(e)
|
347
|
+
end
|
348
|
+
|
349
|
+
def destroy
|
350
|
+
database.delete(primary_key)
|
351
|
+
end
|
352
|
+
|
353
|
+
save_method :delete
|
354
|
+
def delete
|
355
|
+
raise 'cannot delete a record without deleted_at attribute' unless respond_to?(:deleted_at)
|
356
|
+
saved_attributes[:deleted_at] = Time.now
|
357
|
+
end
|
358
|
+
|
359
|
+
save_method :undelete
|
360
|
+
def undelete
|
361
|
+
raise 'cannot undelete a record without deleted_at attribute' unless respond_to?(:deleted_at)
|
362
|
+
saved_attributes.delete(:deleted_at)
|
363
|
+
end
|
364
|
+
|
365
|
+
def deleted?
|
366
|
+
respond_to?(:deleted_at) and not deleted_at.nil?
|
367
|
+
end
|
368
|
+
|
369
|
+
def _dump(ignored)
|
370
|
+
attributes = @attributes.to_hash if @attributes
|
371
|
+
saved_attributes = @saved_attributes.to_hash if @saved_attributes
|
372
|
+
Marshal.dump([attributes, saved_attributes])
|
373
|
+
end
|
374
|
+
|
375
|
+
def self._load(data)
|
376
|
+
new(*Marshal.load(data))
|
377
|
+
end
|
378
|
+
|
379
|
+
private
|
380
|
+
|
381
|
+
def self.extract_opts(args)
|
382
|
+
args.last.kind_of?(Hash) ? args.pop : {}
|
383
|
+
end
|
384
|
+
|
385
|
+
def self.modify_opts(args)
|
386
|
+
opts = extract_opts(args)
|
387
|
+
yield(opts)
|
388
|
+
args << opts
|
389
|
+
end
|
390
|
+
end
|
data/test/.gitignore
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
tmp
|
@@ -0,0 +1,417 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
ActiveDocument::Base.path = File.dirname(__FILE__) + '/tmp'
|
4
|
+
FileUtils.rmtree ActiveDocument::Base.path
|
5
|
+
FileUtils.mkdir ActiveDocument::Base.path
|
6
|
+
|
7
|
+
class Foo < ActiveDocument::Base
|
8
|
+
accessor :foo, :bar, :id
|
9
|
+
|
10
|
+
primary_key :id
|
11
|
+
index_by :foo, :multi_key => true
|
12
|
+
index_by :bar, :unique => true
|
13
|
+
end
|
14
|
+
|
15
|
+
class Bar < ActiveDocument::Base
|
16
|
+
accessor :foo, :bar
|
17
|
+
|
18
|
+
primary_key [:foo, :bar]
|
19
|
+
index_by :bar
|
20
|
+
index_by :foo
|
21
|
+
end
|
22
|
+
|
23
|
+
class Baz < ActiveDocument::Base
|
24
|
+
accessor :foo, :bar, :baz
|
25
|
+
|
26
|
+
primary_key [:foo, :bar], :partition_by => :baz
|
27
|
+
index_by :bar
|
28
|
+
end
|
29
|
+
|
30
|
+
class User < ActiveDocument::Base
|
31
|
+
accessor :first_name, :last_name, :username, :email_address, :tags
|
32
|
+
timestamps
|
33
|
+
defaults :tags => []
|
34
|
+
|
35
|
+
primary_key :username
|
36
|
+
index_by [:last_name, :first_name]
|
37
|
+
index_by :email_address, :unique => true
|
38
|
+
index_by :tags, :multi_key => true
|
39
|
+
end
|
40
|
+
|
41
|
+
class View < ActiveDocument::Base
|
42
|
+
reader :profile_id, :user_id, :count
|
43
|
+
timestamps
|
44
|
+
|
45
|
+
primary_key [:profile_id, :user_id]
|
46
|
+
index_by [:user_id, :updated_at]
|
47
|
+
index_by [:profile_id, :updated_at]
|
48
|
+
|
49
|
+
save_method :increment
|
50
|
+
def increment
|
51
|
+
attributes[:count] += 1
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.increment!(profile_id, user_id)
|
55
|
+
transaction do
|
56
|
+
view = find_by_primary_key([profile_id, user_id]) #, :modify => true)
|
57
|
+
if view
|
58
|
+
view.increment!
|
59
|
+
else
|
60
|
+
view = create(:profile_id => profile_id, :user_id => user_id, :count => 1)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class ActiveDocumentTest < Test::Unit::TestCase
|
67
|
+
context 'with empty foo db' do
|
68
|
+
setup do
|
69
|
+
Foo.database.truncate!
|
70
|
+
end
|
71
|
+
|
72
|
+
should 'find in database after save' do
|
73
|
+
f = Foo.new(:foo => 'BAR', :id => 1)
|
74
|
+
f.save
|
75
|
+
|
76
|
+
assert_equal f, Foo.find(1)
|
77
|
+
end
|
78
|
+
|
79
|
+
should 'raise exception if not found' do
|
80
|
+
assert_raises(ActiveDocument::DocumentNotFound) do
|
81
|
+
Foo.find(7)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
should 'find_by_primary_key' do
|
86
|
+
f = Foo.new(:foo => 'BAR', :id => 1)
|
87
|
+
f.save
|
88
|
+
|
89
|
+
assert_equal f, Foo.find_by_primary_key(1)
|
90
|
+
assert_equal f, Foo.find_by_id(1)
|
91
|
+
end
|
92
|
+
|
93
|
+
should 'destroy' do
|
94
|
+
f = Foo.new(:foo => 'BAR', :id => 1)
|
95
|
+
f.save
|
96
|
+
|
97
|
+
assert_equal f, Foo.find_by_id(1)
|
98
|
+
|
99
|
+
f.destroy
|
100
|
+
|
101
|
+
assert_equal nil, Foo.find_by_id(1)
|
102
|
+
end
|
103
|
+
|
104
|
+
should 'change primary key' do
|
105
|
+
f = Foo.new(:foo => 'BAR', :id => 1)
|
106
|
+
f.save
|
107
|
+
|
108
|
+
assert_equal f, Foo.find_by_id(1)
|
109
|
+
|
110
|
+
f.id = 2
|
111
|
+
f.save
|
112
|
+
|
113
|
+
assert_equal nil, Foo.find_by_id(1)
|
114
|
+
assert_equal 2, Foo.find_by_id(2).id
|
115
|
+
end
|
116
|
+
|
117
|
+
should 'find by secondary indexes' do
|
118
|
+
f1 = Foo.new(:foo => ['BAR', 'BAZ'], :bar => 'FOO', :id => 1)
|
119
|
+
f1.save
|
120
|
+
|
121
|
+
f2 = Foo.new(:foo => 'BAR', :bar => 'FU', :id => 2)
|
122
|
+
f2.save
|
123
|
+
|
124
|
+
assert_equal f1, Foo.find_by_bar('FOO')
|
125
|
+
assert_equal f2, Foo.find_by_bar('FU')
|
126
|
+
assert_equal [f1,f2], Foo.find_all_by_foo('BAR')
|
127
|
+
assert_equal [f1], Foo.find_all_by_foo('BAZ')
|
128
|
+
end
|
129
|
+
|
130
|
+
should 'find by range' do
|
131
|
+
(1..20).each do |i|
|
132
|
+
Foo.new(:id => i, :foo => "foo-#{i}").save
|
133
|
+
end
|
134
|
+
|
135
|
+
assert_equal (5..17).to_a, Foo.find_all_by_id(5..17).collect {|f| f.id}
|
136
|
+
assert_equal (5..14).to_a, Foo.find_all_by_id(5..17, :limit => 10).collect {|f| f.id}
|
137
|
+
|
138
|
+
# Mixed keys and ranges.
|
139
|
+
assert_equal (1..4).to_a + (16..20).to_a, Foo.find_all_by_id(1..3, 4, 16..20).collect {|f| f.id}
|
140
|
+
end
|
141
|
+
|
142
|
+
should 'find all' do
|
143
|
+
(1..20).each do |i|
|
144
|
+
Foo.new(:id => i, :foo => "foo-#{i}").save
|
145
|
+
end
|
146
|
+
|
147
|
+
assert_equal (1..20).to_a, Foo.find_all_by_id.collect {|f| f.id}
|
148
|
+
assert_equal 1, Foo.find_by_id.id # First
|
149
|
+
end
|
150
|
+
|
151
|
+
should 'find with reverse' do
|
152
|
+
(1..20).each do |i|
|
153
|
+
Foo.new(:id => i, :foo => "foo-#{i}").save
|
154
|
+
end
|
155
|
+
|
156
|
+
assert_equal (1..20).to_a.reverse, Foo.find_all_by_id(:reverse => true).collect {|f| f.id}
|
157
|
+
assert_equal (5..17).to_a.reverse, Foo.find_all_by_id(5..17, :reverse => true).collect {|f| f.id}
|
158
|
+
assert_equal 20, Foo.find_by_id(:reverse => true).id # Last
|
159
|
+
end
|
160
|
+
|
161
|
+
should 'find with limit and offset' do
|
162
|
+
(1..100).each do |i|
|
163
|
+
Foo.new(:id => i, :bar => i + 42, :foo => i % 20).save
|
164
|
+
end
|
165
|
+
|
166
|
+
assert_equal [5, 5, 5, 5, 6, 6, 6],
|
167
|
+
Foo.find_all_by_foo(5..14, :limit => 7, :offset => 1).collect {|f| f.foo}
|
168
|
+
|
169
|
+
assert_equal [6, 6, 7, 7, 7, 7, 7, 8, 8, 8, 8],
|
170
|
+
Foo.find_all_by_foo(6..14, :limit => 11, :offset => 3).collect {|f| f.foo}
|
171
|
+
|
172
|
+
assert_equal [8, 9, 9, 9, 9, 9, 10, 10, 10, 10, 10, 11, 11, 11, 11, 11],
|
173
|
+
Foo.find_all_by_foo(8..14, :limit => 16, :offset => 4).collect {|f| f.foo}
|
174
|
+
|
175
|
+
assert_equal [12, 12, 12, 12, 12, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14],
|
176
|
+
Foo.find_all_by_foo(12..14, :offset => 0).collect {|f| f.foo}
|
177
|
+
end
|
178
|
+
|
179
|
+
should 'add locator_key to models' do
|
180
|
+
Foo.new(:id => 1, :foo => [1, 2, 3]).save
|
181
|
+
Foo.new(:id => 2, :foo => [4, 5, 6]).save
|
182
|
+
Foo.new(:id => 3, :foo => [6, 7, 8]).save
|
183
|
+
|
184
|
+
Foo.find_all_by_foo(2..4).each_with_index do |foo, i|
|
185
|
+
assert_equal [i + 2], foo.locator_key
|
186
|
+
end
|
187
|
+
|
188
|
+
Foo.find_all_by_foo(6).each do |foo|
|
189
|
+
assert_equal [6], foo.locator_key
|
190
|
+
end
|
191
|
+
|
192
|
+
i = 1
|
193
|
+
Foo.find_all_by_foo.each do |foo|
|
194
|
+
key = i > 6 ? [i - 1] : [i]
|
195
|
+
assert_equal key, foo.locator_key
|
196
|
+
i += 1
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
context 'with empty bar db' do
|
202
|
+
setup do
|
203
|
+
Bar.database.truncate!
|
204
|
+
end
|
205
|
+
|
206
|
+
should 'not overwrite existing model' do
|
207
|
+
b1 = Bar.new(:foo => 'foo', :bar => 'bar')
|
208
|
+
b1.save
|
209
|
+
|
210
|
+
assert_raises(ActiveDocument::DuplicatePrimaryKey) do
|
211
|
+
b2 = Bar.new(:foo => 'foo', :bar => 'bar')
|
212
|
+
b2.save
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
should 'find_by_primary_key and find by id fields' do
|
217
|
+
100.times do |i|
|
218
|
+
100.times do |j|
|
219
|
+
b = Bar.new(:foo => i, :bar => j)
|
220
|
+
b.save
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
assert_equal [5, 5], Bar.find_by_primary_key([5, 5]).primary_key
|
225
|
+
assert_equal [52, 52], Bar.find_by_foo_and_bar([52, 52]).foo_and_bar
|
226
|
+
assert_equal (0..99).collect {|i| [42, i]}, Bar.find_all_by_foo(42).collect {|b| b.primary_key}
|
227
|
+
assert_equal (0..99).collect {|i| [i, 52]}, Bar.find_all_by_bar(52).collect {|b| b.primary_key}
|
228
|
+
end
|
229
|
+
|
230
|
+
should 'count' do
|
231
|
+
(1..21).each do |i|
|
232
|
+
Bar.new(:foo => i % 7, :bar => i % 3).save
|
233
|
+
end
|
234
|
+
|
235
|
+
assert_equal 1, Bar.count(:primary_key, [6,2])
|
236
|
+
assert_equal 0, Bar.count(:primary_key, [2,6])
|
237
|
+
|
238
|
+
3.times {|i| assert_equal 7, Bar.count(:bar, i)}
|
239
|
+
assert_equal 0, Bar.count(:bar, 3)
|
240
|
+
|
241
|
+
7.times {|i| assert_equal 3, Bar.count(:foo, i)}
|
242
|
+
assert_equal 0, Bar.count(:foo, 7)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
context 'with empty baz db' do
|
247
|
+
setup do
|
248
|
+
Baz.database.truncate!
|
249
|
+
end
|
250
|
+
|
251
|
+
should 'partition_by baz' do
|
252
|
+
b1 = Baz.new(:foo => 'foo', :bar => 'bar', :baz => 1)
|
253
|
+
b1.save
|
254
|
+
|
255
|
+
b2 = Baz.new(:foo => 'foo', :bar => 'bar', :baz => 2)
|
256
|
+
b2.save
|
257
|
+
|
258
|
+
assert_equal b1, Baz.find(['foo','bar'], :baz => 1)
|
259
|
+
assert_equal b2, Baz.find(['foo','bar'], :baz => 2)
|
260
|
+
end
|
261
|
+
|
262
|
+
should 'find and save with partition' do
|
263
|
+
10.times do |i|
|
264
|
+
Baz.with_baz(i) do
|
265
|
+
assert_equal nil, Baz.find_by_primary_key(['foo','bar'])
|
266
|
+
|
267
|
+
b = Baz.new(:foo => 'foo', :bar => 'bar')
|
268
|
+
b.save
|
269
|
+
|
270
|
+
assert_equal i, b.baz
|
271
|
+
assert_equal b, Baz.find(['foo','bar'])
|
272
|
+
end
|
273
|
+
end
|
274
|
+
assert_equal (0...10).collect {|i| i.to_s}, Baz.partitions
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
context 'with empty user db' do
|
279
|
+
setup do
|
280
|
+
User.database.truncate!
|
281
|
+
|
282
|
+
@john = User.create(
|
283
|
+
:first_name => 'John',
|
284
|
+
:last_name => 'Stewart',
|
285
|
+
:username => 'lefty',
|
286
|
+
:email_address => 'john@thedailyshow.com',
|
287
|
+
:tags => [:funny, :liberal]
|
288
|
+
)
|
289
|
+
|
290
|
+
@martha = User.create(
|
291
|
+
:first_name => 'Martha',
|
292
|
+
:last_name => 'Stewart',
|
293
|
+
:username => 'helen',
|
294
|
+
:email_address => 'martha@marthastewart.com',
|
295
|
+
:tags => [:conservative, :convict]
|
296
|
+
)
|
297
|
+
|
298
|
+
@steve = User.create(
|
299
|
+
:first_name => 'Stephen',
|
300
|
+
:last_name => 'Colbert',
|
301
|
+
:username => 'steve',
|
302
|
+
:email_address => 'stephen@thereport.com',
|
303
|
+
:tags => [:conservative, :funny]
|
304
|
+
)
|
305
|
+
|
306
|
+
@will = User.create(
|
307
|
+
:first_name => 'Will',
|
308
|
+
:last_name => 'Smith',
|
309
|
+
:username => 'legend',
|
310
|
+
:email_address => 'will@smith.com',
|
311
|
+
:tags => [:actor, :rapper]
|
312
|
+
)
|
313
|
+
end
|
314
|
+
|
315
|
+
should 'initialize defaults' do
|
316
|
+
user = User.create
|
317
|
+
assert_equal [], user.tags
|
318
|
+
end
|
319
|
+
|
320
|
+
should 'find_all_by_username' do
|
321
|
+
assert_equal ['helen', 'lefty', 'legend', 'steve'], User.find_all_by_username.collect {|u| u.username}
|
322
|
+
end
|
323
|
+
|
324
|
+
should 'find_all_by_last_name_and_first_name' do
|
325
|
+
assert_equal ['steve', 'legend', 'lefty', 'helen'], User.find_all_by_last_name_and_first_name.collect {|u| u.username}
|
326
|
+
end
|
327
|
+
|
328
|
+
should 'find_all_by_last_name' do
|
329
|
+
assert_equal ['John', 'Martha'], User.find_all_by_last_name('Stewart').collect {|u| u.first_name}
|
330
|
+
assert_equal ['Stephen', 'Will', 'John', 'Martha'], User.find_all_by_last_name.collect {|u| u.first_name}
|
331
|
+
end
|
332
|
+
|
333
|
+
should 'find_all_by_tag' do
|
334
|
+
assert_equal ['lefty', 'steve'], User.find_all_by_tag(:funny).collect {|u| u.username}
|
335
|
+
end
|
336
|
+
|
337
|
+
should 'find with keys' do
|
338
|
+
assert_equal ['lefty', 'helen', 'legend'],
|
339
|
+
User.find_all_by_last_name("Stewart", "Smith").collect {|u| u.username}
|
340
|
+
end
|
341
|
+
|
342
|
+
should 'find with range' do
|
343
|
+
assert_equal ['legend', 'lefty', 'helen'],
|
344
|
+
User.find_all_by_last_name("Smith".."Stuart").collect {|u| u.username}
|
345
|
+
end
|
346
|
+
|
347
|
+
should 'find with range and key' do
|
348
|
+
assert_equal ['legend', 'lefty', 'helen', 'steve'],
|
349
|
+
User.find_all_by_last_name("Smith".."Stuart", "Colbert").collect {|u| u.username}
|
350
|
+
end
|
351
|
+
|
352
|
+
should 'find with ranges' do
|
353
|
+
assert_equal ['steve', 'legend', 'lefty', 'helen'],
|
354
|
+
User.find_all_by_last_name("Aardvark".."Daisy", "Smith".."Stuart").collect {|u| u.username}
|
355
|
+
end
|
356
|
+
|
357
|
+
should 'find with limit' do
|
358
|
+
assert_equal ["helen", "lefty"], User.find_all_by_username(:limit => 2).collect {|u| u.username}
|
359
|
+
end
|
360
|
+
|
361
|
+
should 'find with limit and offset' do
|
362
|
+
assert_equal ["legend", "steve"], User.find_all_by_username(:limit => 2, :offset => 2).collect {|u| u.username}
|
363
|
+
end
|
364
|
+
|
365
|
+
should 'find with group' do
|
366
|
+
expected = [[["Colbert", "Stephen"], 1],
|
367
|
+
[["Smith", "Will"], 1],
|
368
|
+
[["Stewart", "John"], 1],
|
369
|
+
[["Stewart", "Martha"], 1]]
|
370
|
+
|
371
|
+
assert_equal expected, User.find_all_by_last_name(:all, :group => true).collect {|k,v| [k,v.size]}
|
372
|
+
assert_equal expected, User.find_all_by_last_name(:all, :group => 2 ).collect {|k,v| [k,v.size]}
|
373
|
+
|
374
|
+
expected = [[["Colbert"], 1],
|
375
|
+
[["Smith"], 1],
|
376
|
+
[["Stewart"], 2]]
|
377
|
+
|
378
|
+
assert_equal expected, User.find_all_by_last_name(:all, :group => 1 ).collect {|k,v| [k,v.size]}
|
379
|
+
end
|
380
|
+
|
381
|
+
should 'find with page' do
|
382
|
+
assert_equal ["helen", "lefty"], User.find_all_by_username(:per_page => 2, :page => 1).collect {|u| u.username}
|
383
|
+
assert_equal ["legend", "steve"], User.find_all_by_username(:per_page => 2, :page => 2).collect {|u| u.username}
|
384
|
+
assert_equal ["helen", "lefty"], User.find_all_by_username(:limit => 2, :page => 1).collect {|u| u.username}
|
385
|
+
assert_equal ["legend", "steve"], User.find_all_by_username(:limit => 2, :page => 2).collect {|u| u.username}
|
386
|
+
end
|
387
|
+
|
388
|
+
should "mark deleted but don't destroy record" do
|
389
|
+
assert !@martha.deleted?
|
390
|
+
assert !User.find_by_username('helen').deleted?
|
391
|
+
|
392
|
+
@martha.delete!
|
393
|
+
|
394
|
+
assert @martha.deleted?
|
395
|
+
assert User.find_by_username('helen').deleted?
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
context 'with empty views db' do
|
400
|
+
setup do
|
401
|
+
View.database.truncate!
|
402
|
+
end
|
403
|
+
|
404
|
+
N = 10000
|
405
|
+
P = 1
|
406
|
+
U = 1
|
407
|
+
|
408
|
+
should 'increment views randomly without corrupting secondary index' do
|
409
|
+
N.times do
|
410
|
+
profile_id = rand(P)
|
411
|
+
user_id = rand(U)
|
412
|
+
View.increment!(profile_id, user_id)
|
413
|
+
end
|
414
|
+
assert true
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/test_helper'
|
2
|
+
|
3
|
+
ActiveDocument.default_path = File.dirname(__FILE__) + '/tmp'
|
4
|
+
ActiveDocument.env_config :cache_size => 1 * 1024 * 1024
|
5
|
+
ActiveDocument.db_config :page_size => 512
|
6
|
+
|
7
|
+
class Foo < ActiveDocument::Base
|
8
|
+
accessor :bar, :id
|
9
|
+
|
10
|
+
primary_key :id
|
11
|
+
end
|
12
|
+
|
13
|
+
class DeadlockTest < Test::Unit::TestCase
|
14
|
+
context 'with empty and closed environment' do
|
15
|
+
setup do
|
16
|
+
FileUtils.rmtree Foo.path
|
17
|
+
FileUtils.mkdir Foo.path
|
18
|
+
Foo.close_environment
|
19
|
+
end
|
20
|
+
|
21
|
+
N = 5000 # total number of records
|
22
|
+
R = 10 # number of readers
|
23
|
+
W = 10 # number of writers
|
24
|
+
T = 20 # reads per transaction
|
25
|
+
L = 100 # logging frequency
|
26
|
+
|
27
|
+
should 'detect deadlock' do
|
28
|
+
pids = []
|
29
|
+
|
30
|
+
W.times do |n|
|
31
|
+
pids << fork(&writer)
|
32
|
+
end
|
33
|
+
|
34
|
+
sleep(1)
|
35
|
+
|
36
|
+
R.times do
|
37
|
+
pids << fork(&reader)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Make sure that all processes finish with no errors.
|
41
|
+
pids.each do |pid|
|
42
|
+
Process.wait(pid)
|
43
|
+
assert_equal status, $?.exitstatus
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
C = 10
|
48
|
+
should 'detect unclosed resources' do
|
49
|
+
threads = []
|
50
|
+
|
51
|
+
threads << Thread.new do
|
52
|
+
C.times do
|
53
|
+
sleep(10)
|
54
|
+
|
55
|
+
pid = fork do
|
56
|
+
cursor = Foo.database.db.cursor(nil, 0)
|
57
|
+
cursor.get(nil, nil, Bdb::DB_FIRST)
|
58
|
+
exit!(1)
|
59
|
+
end
|
60
|
+
puts "\n====simulating exit with unclosed resources ===="
|
61
|
+
Process.wait(pid)
|
62
|
+
assert_equal 1, $?.exitstatus
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
threads << Thread.new do
|
67
|
+
C.times do
|
68
|
+
pid = fork(&writer(1000))
|
69
|
+
Process.wait(pid)
|
70
|
+
assert [0,9].include?($?.exitstatus)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
sleep(3)
|
75
|
+
|
76
|
+
threads << Thread.new do
|
77
|
+
C.times do
|
78
|
+
pid = fork(&reader(1000))
|
79
|
+
Process.wait(pid)
|
80
|
+
assert [0,9].include?($?.exitstatus)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
threads.each {|t| t.join}
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def reader(n = N)
|
89
|
+
lambda do
|
90
|
+
T.times do
|
91
|
+
(1...n).to_a.shuffle.each_slice(T) do |ids|
|
92
|
+
Foo.transaction do
|
93
|
+
ids.each {|id| Foo.find_by_id(id)}
|
94
|
+
end
|
95
|
+
log('r')
|
96
|
+
end
|
97
|
+
end
|
98
|
+
Foo.close_environment
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def writer(n = N)
|
103
|
+
lambda do
|
104
|
+
(1...n).to_a.shuffle.each_with_index do |id, i|
|
105
|
+
Foo.transaction do
|
106
|
+
begin
|
107
|
+
Foo.create(:id => id, :bar => "bar" * 1000 + "anne #{rand}")
|
108
|
+
rescue ActiveDocument::DuplicatePrimaryKey => e
|
109
|
+
Foo.find_by_id(id).destroy
|
110
|
+
retry
|
111
|
+
end
|
112
|
+
end
|
113
|
+
log('w')
|
114
|
+
end
|
115
|
+
Foo.close_environment
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def log(action)
|
120
|
+
@count ||= Hash.new(0)
|
121
|
+
if @count[action] % L == 0
|
122
|
+
print action.to_s
|
123
|
+
$stdout.flush
|
124
|
+
end
|
125
|
+
@count[action] += 1
|
126
|
+
end
|
127
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_document
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Justin Balthrop
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-11-19 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Schemaless models in Berkeley DB.
|
17
|
+
email: code@justinbalthrop.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.rdoc
|
25
|
+
files:
|
26
|
+
- .gitignore
|
27
|
+
- LICENSE
|
28
|
+
- README.rdoc
|
29
|
+
- Rakefile
|
30
|
+
- VERSION.yml
|
31
|
+
- active_document.gemspec
|
32
|
+
- examples/foo.rb
|
33
|
+
- examples/photo.rb
|
34
|
+
- lib/active_document.rb
|
35
|
+
- lib/active_document/base.rb
|
36
|
+
- test/.gitignore
|
37
|
+
- test/active_document_test.rb
|
38
|
+
- test/deadlock_test.rb
|
39
|
+
- test/test_helper.rb
|
40
|
+
has_rdoc: true
|
41
|
+
homepage: http://github.com/ninjudd/active_document
|
42
|
+
licenses: []
|
43
|
+
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options:
|
46
|
+
- --charset=UTF-8
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: "0"
|
60
|
+
version:
|
61
|
+
requirements: []
|
62
|
+
|
63
|
+
rubyforge_project:
|
64
|
+
rubygems_version: 1.3.5
|
65
|
+
signing_key:
|
66
|
+
specification_version: 3
|
67
|
+
summary: Schemaless models in Berkeley DB
|
68
|
+
test_files:
|
69
|
+
- test/active_document_test.rb
|
70
|
+
- test/deadlock_test.rb
|
71
|
+
- test/test_helper.rb
|
72
|
+
- examples/foo.rb
|
73
|
+
- examples/photo.rb
|