active_document 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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