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 ADDED
@@ -0,0 +1,3 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
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,4 @@
1
+ ---
2
+ :minor: 1
3
+ :patch: 0
4
+ :major: 0
@@ -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
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'active_document'
3
+
4
+ class Foo < ActiveDocument::Base
5
+ path '/tmp/data'
6
+
7
+ reader :roo
8
+ writer :woo
9
+ accessor :foo, :bar, :key
10
+ end
data/examples/photo.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'active_document'
2
+
3
+ class Photo < ActiveDocument::Base
4
+ path '/var/data'
5
+
6
+ accessor :id, :tagged_ids, :user_id, :description
7
+
8
+ index_by :tagged_ids
9
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveDocument
2
+ class DocumentNotFound < StandardError; end
3
+ class DuplicatePrimaryKey < StandardError; end
4
+ end
5
+
6
+ require 'bdb/database'
7
+ require 'bdb/partitioned_database'
8
+ require 'active_support'
9
+ require 'active_document/base'
@@ -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
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+ require 'mocha'
5
+ require 'pp'
6
+
7
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../lib')
8
+ require 'active_document'
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