active_document 0.1.0
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/.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
|