dm-core 0.9.2 → 0.9.3

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.
Files changed (84) hide show
  1. data/.autotest +26 -0
  2. data/{CHANGELOG → History.txt} +78 -77
  3. data/Manifest.txt +123 -0
  4. data/{README → README.txt} +0 -0
  5. data/Rakefile +29 -0
  6. data/SPECS +63 -0
  7. data/TODO +1 -0
  8. data/lib/dm-core.rb +6 -1
  9. data/lib/dm-core/adapters/data_objects_adapter.rb +29 -32
  10. data/lib/dm-core/adapters/mysql_adapter.rb +1 -1
  11. data/lib/dm-core/adapters/postgres_adapter.rb +1 -1
  12. data/lib/dm-core/adapters/sqlite3_adapter.rb +2 -2
  13. data/lib/dm-core/associations.rb +26 -0
  14. data/lib/dm-core/associations/many_to_many.rb +34 -25
  15. data/lib/dm-core/associations/many_to_one.rb +4 -4
  16. data/lib/dm-core/associations/one_to_many.rb +48 -13
  17. data/lib/dm-core/associations/one_to_one.rb +4 -4
  18. data/lib/dm-core/associations/relationship.rb +144 -42
  19. data/lib/dm-core/associations/relationship_chain.rb +31 -24
  20. data/lib/dm-core/auto_migrations.rb +0 -4
  21. data/lib/dm-core/collection.rb +40 -7
  22. data/lib/dm-core/dependency_queue.rb +31 -0
  23. data/lib/dm-core/hook.rb +2 -2
  24. data/lib/dm-core/is.rb +2 -2
  25. data/lib/dm-core/logger.rb +10 -10
  26. data/lib/dm-core/model.rb +94 -41
  27. data/lib/dm-core/property.rb +72 -41
  28. data/lib/dm-core/property_set.rb +8 -14
  29. data/lib/dm-core/query.rb +34 -9
  30. data/lib/dm-core/repository.rb +0 -0
  31. data/lib/dm-core/resource.rb +13 -13
  32. data/lib/dm-core/scope.rb +25 -2
  33. data/lib/dm-core/type.rb +3 -3
  34. data/lib/dm-core/types/discriminator.rb +10 -8
  35. data/lib/dm-core/types/object.rb +4 -0
  36. data/lib/dm-core/types/paranoid_boolean.rb +15 -4
  37. data/lib/dm-core/types/paranoid_datetime.rb +15 -4
  38. data/lib/dm-core/version.rb +3 -0
  39. data/script/all +5 -0
  40. data/script/performance.rb +191 -0
  41. data/script/profile.rb +86 -0
  42. data/spec/integration/association_spec.rb +288 -204
  43. data/spec/integration/association_through_spec.rb +9 -3
  44. data/spec/integration/associations/many_to_many_spec.rb +97 -31
  45. data/spec/integration/associations/many_to_one_spec.rb +41 -6
  46. data/spec/integration/associations/one_to_many_spec.rb +18 -2
  47. data/spec/integration/auto_migrations_spec.rb +0 -0
  48. data/spec/integration/collection_spec.rb +89 -42
  49. data/spec/integration/dependency_queue_spec.rb +58 -0
  50. data/spec/integration/model_spec.rb +67 -8
  51. data/spec/integration/postgres_adapter_spec.rb +19 -20
  52. data/spec/integration/property_spec.rb +17 -8
  53. data/spec/integration/query_spec.rb +273 -191
  54. data/spec/integration/resource_spec.rb +108 -10
  55. data/spec/integration/strategic_eager_loading_spec.rb +138 -0
  56. data/spec/integration/transaction_spec.rb +3 -3
  57. data/spec/integration/type_spec.rb +121 -0
  58. data/spec/lib/logging_helper.rb +18 -0
  59. data/spec/lib/model_loader.rb +91 -0
  60. data/spec/lib/publicize_methods.rb +28 -0
  61. data/spec/models/vehicles.rb +34 -0
  62. data/spec/models/zoo.rb +48 -0
  63. data/spec/spec.opts +3 -0
  64. data/spec/spec_helper.rb +25 -62
  65. data/spec/unit/adapters/data_objects_adapter_spec.rb +1 -0
  66. data/spec/unit/associations/many_to_many_spec.rb +3 -0
  67. data/spec/unit/associations/many_to_one_spec.rb +9 -1
  68. data/spec/unit/associations/one_to_many_spec.rb +12 -4
  69. data/spec/unit/associations/relationship_spec.rb +19 -15
  70. data/spec/unit/associations_spec.rb +37 -0
  71. data/spec/unit/collection_spec.rb +8 -0
  72. data/spec/unit/data_mapper_spec.rb +14 -0
  73. data/spec/unit/model_spec.rb +2 -2
  74. data/spec/unit/property_set_spec.rb +0 -13
  75. data/spec/unit/property_spec.rb +92 -21
  76. data/spec/unit/query_spec.rb +49 -4
  77. data/spec/unit/resource_spec.rb +122 -60
  78. data/spec/unit/scope_spec.rb +11 -0
  79. data/tasks/ci.rb +68 -0
  80. data/tasks/dm.rb +63 -0
  81. data/tasks/doc.rb +20 -0
  82. data/tasks/hoe.rb +38 -0
  83. data/tasks/install.rb +20 -0
  84. metadata +63 -22
data/lib/dm-core/scope.rb CHANGED
@@ -1,16 +1,28 @@
1
1
  module DataMapper
2
2
  module Scope
3
+ Model.append_extensions self
4
+
5
+ # @api private
6
+ def default_scope(repository_name = nil)
7
+ repository_name = self.default_repository_name if repository_name == :default || repository_name.nil?
8
+ @default_scope ||= Hash.new{|h,k| h[k] = {}}
9
+ @default_scope[repository_name]
10
+ end
11
+
12
+ # @api private
3
13
  def query
4
14
  scope_stack.last
5
15
  end
6
16
 
7
17
  protected
8
18
 
19
+ # @api semipublic
9
20
  def with_scope(query, &block)
10
21
  # merge the current scope with the passed in query
11
22
  with_exclusive_scope(self.query ? self.query.merge(query) : query, &block)
12
23
  end
13
24
 
25
+ # @api semipublic
14
26
  def with_exclusive_scope(query, &block)
15
27
  query = DataMapper::Query.new(repository, self, query) if query.kind_of?(Hash)
16
28
 
@@ -25,11 +37,22 @@ module DataMapper
25
37
 
26
38
  private
27
39
 
40
+ # @api private
41
+ def merge_with_default_scope(query)
42
+ DataMapper::Query.new(query.repository, query.model, default_scope_for_query(query)).update(query)
43
+ end
44
+
45
+ # @api private
28
46
  def scope_stack
29
- scope_stack_for = Thread.current[:dm_scope_stack] ||= Hash.new { |h,k| h[k] = [] }
47
+ scope_stack_for = Thread.current[:dm_scope_stack] ||= Hash.new { |h,model| h[model] = [] }
30
48
  scope_stack_for[self]
31
49
  end
32
50
 
33
- Model.send(:include, self)
51
+ # @api private
52
+ def default_scope_for_query(query)
53
+ repository_name = query.repository.name
54
+ default_repository_name = query.model.default_repository_name
55
+ self.default_scope(default_repository_name).merge(self.default_scope(repository_name))
56
+ end
34
57
  end # module Scope
35
58
  end # module DataMapper
data/lib/dm-core/type.rb CHANGED
@@ -35,7 +35,7 @@ module DataMapper
35
35
  # end
36
36
  class Type
37
37
  PROPERTY_OPTIONS = [
38
- :public, :protected, :private, :accessor, :reader, :writer,
38
+ :accessor, :reader, :writer,
39
39
  :lazy, :default, :nullable, :key, :serial, :field, :size, :length,
40
40
  :format, :index, :unique_index, :check, :ordinal, :auto_validation,
41
41
  :validates, :unique, :track, :precision, :scale
@@ -86,7 +86,7 @@ module DataMapper
86
86
  @primitive = primitive
87
87
  end
88
88
 
89
- #load DataMapper::Property options
89
+ # Load DataMapper::Property options
90
90
  PROPERTY_OPTIONS.each do |property_option|
91
91
  self.class_eval <<-EOS, __FILE__, __LINE__
92
92
  def #{property_option}(arg = nil)
@@ -97,7 +97,7 @@ module DataMapper
97
97
  EOS
98
98
  end
99
99
 
100
- #create property aliases
100
+ # Create property aliases
101
101
  PROPERTY_OPTION_ALIASES.each do |property_option, aliases|
102
102
  aliases.each do |ali|
103
103
  self.class_eval <<-EOS, __FILE__, __LINE__
@@ -10,20 +10,22 @@ module DataMapper
10
10
  model = property.model
11
11
 
12
12
  model.class_eval <<-EOS, __FILE__, __LINE__
13
- def self.child_classes
14
- @child_classes ||= []
13
+ def self.descendants
14
+ (@descendants ||= []).uniq!
15
+ @descendants
15
16
  end
16
17
 
17
18
  after_class_method :inherited, :add_scope_for_discriminator
18
19
 
19
- def self.add_scope_for_discriminator(target)
20
- target.send(:scope_stack) << DataMapper::Query.new(target.repository, target, :#{property.name} => target.child_classes << target)
21
- propagate_child_classes(target)
20
+ def self.add_scope_for_discriminator(retval, target)
21
+ target.descendants << target
22
+ target.default_scope.update(#{property.name.inspect} => target.descendants)
23
+ propagate_descendants(target)
22
24
  end
23
25
 
24
- def self.propagate_child_classes(target)
25
- child_classes << target
26
- superclass.send(:propagate_child_classes,target) if superclass.respond_to?(:propagate_child_classes)
26
+ def self.propagate_descendants(target)
27
+ descendants << target
28
+ superclass.propagate_descendants(target) if superclass.respond_to?(:propagate_descendants)
27
29
  end
28
30
  EOS
29
31
  end
@@ -8,6 +8,10 @@ module DataMapper
8
8
  lazy true
9
9
  track :hash
10
10
 
11
+ def self.typecast(value, property)
12
+ value
13
+ end
14
+
11
15
  def self.dump(value, property)
12
16
  Base64.encode64(Marshal.dump(value))
13
17
  end
@@ -3,20 +3,31 @@ module DataMapper
3
3
  class ParanoidBoolean < DataMapper::Type(Boolean)
4
4
  primitive TrueClass
5
5
  default false
6
+ lazy true
6
7
 
7
8
  def self.bind(property)
8
9
  model = property.model
9
10
  repository = property.repository
10
11
 
11
- model.class_eval <<-EOS
12
+ model.send(:set_paranoid_property, property.name){true}
13
+
14
+ model.class_eval <<-EOS, __FILE__, __LINE__
15
+
16
+ def self.with_deleted
17
+ with_exclusive_scope(#{property.name.inspect} => true) do
18
+ yield
19
+ end
20
+ end
21
+
12
22
  def destroy
13
- attribute_set(#{property.name.inspect}, true)
23
+ self.class.paranoid_properties.each do |name, blk|
24
+ attribute_set(name, blk.call(self))
25
+ end
14
26
  save
15
27
  end
16
28
  EOS
17
29
 
18
- model.send(:scope_stack) << DataMapper::Query.new(repository, model, property.name => nil)
19
-
30
+ model.default_scope(repository.name).update(property.name => false)
20
31
  end
21
32
  end # class ParanoidBoolean
22
33
  end # module Types
@@ -2,20 +2,31 @@ module DataMapper
2
2
  module Types
3
3
  class ParanoidDateTime < DataMapper::Type(DateTime)
4
4
  primitive DateTime
5
+ lazy true
5
6
 
6
7
  def self.bind(property)
7
8
  model = property.model
8
9
  repository = property.repository
9
10
 
10
- model.class_eval <<-EOS
11
+ model.send(:set_paranoid_property, property.name){DateTime.now}
12
+
13
+ model.class_eval <<-EOS, __FILE__, __LINE__
14
+
15
+ def self.with_deleted
16
+ with_exclusive_scope(#{property.name.inspect}.not => nil) do
17
+ yield
18
+ end
19
+ end
20
+
11
21
  def destroy
12
- attribute_set(#{property.name.inspect}, DateTime.now)
22
+ self.class.paranoid_properties.each do |name, blk|
23
+ attribute_set(name, blk.call(self))
24
+ end
13
25
  save
14
26
  end
15
27
  EOS
16
28
 
17
- model.send(:scope_stack) << DataMapper::Query.new(repository, model, property.name => nil)
18
-
29
+ model.default_scope(repository.name).update(property.name => nil)
19
30
  end
20
31
  end # class ParanoidDateTime
21
32
  end # module Types
@@ -0,0 +1,3 @@
1
+ module DataMapper
2
+ VERSION = '0.9.3' unless defined?(DataMapper::VERSION)
3
+ end
data/script/all ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env sh
2
+ rake spec:unit
3
+ ADAPTER=sqlite3 rake spec:integration
4
+ ADAPTER=mysql rake spec:integration
5
+ ADAPTER=postgres rake spec:integration
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'dm-core')
4
+
5
+ require 'rubygems'
6
+ require 'ftools'
7
+
8
+ # sudo gem install rbench
9
+ # OR git clone git://github.com/somebee/rbench.git , rake install
10
+ gem 'rbench', '>=0.2.2'
11
+ require 'rbench'
12
+
13
+ gem 'faker', '>=0.3.1'
14
+ require 'faker'
15
+
16
+ gem 'activerecord', '>=2.1.0'
17
+ require 'active_record'
18
+
19
+ socket_file = Pathname.glob(%w[
20
+ /opt/local/var/run/mysql5/mysqld.sock
21
+ tmp/mysqld.sock
22
+ /tmp/mysqld.sock
23
+ tmp/mysql.sock
24
+ /tmp/mysql.sock
25
+ /var/mysql/mysql.sock
26
+ ]).find { |path| path.socket? }
27
+
28
+ configuration_options = {
29
+ :adapter => 'mysql',
30
+ :username => 'root',
31
+ :password => '',
32
+ :database => 'data_mapper_1',
33
+ }
34
+
35
+ configuration_options[:socket] = socket_file unless socket_file.nil?
36
+
37
+ log_dir = DataMapper.root / 'log'
38
+ log_dir.mkdir unless log_dir.directory?
39
+
40
+ DataMapper::Logger.new(log_dir / 'dm.log', :debug)
41
+ adapter = DataMapper.setup(:default, "mysql://root@localhost/data_mapper_1?socket=#{socket_file}")
42
+
43
+ if configuration_options[:adapter]
44
+ sqlfile = File.join(File.dirname(__FILE__),'..','tmp','perf.sql')
45
+ mysql_bin = %w[mysql mysql5].select{|bin| `which #{bin}`.length > 0 }
46
+ mysqldump_bin = %w[mysqldump mysqldump5].select{|bin| `which #{bin}`.length > 0 }
47
+ end
48
+
49
+ ActiveRecord::Base.logger = Logger.new(log_dir / 'ar.log')
50
+ ActiveRecord::Base.logger.level = 0
51
+
52
+ ActiveRecord::Base.establish_connection(configuration_options)
53
+
54
+ class ARExhibit < ActiveRecord::Base #:nodoc:
55
+ set_table_name 'exhibits'
56
+ end
57
+
58
+ ARExhibit.find_by_sql('SELECT 1')
59
+
60
+ class Exhibit
61
+ include DataMapper::Resource
62
+
63
+ property :id, Serial
64
+ property :name, String
65
+ property :zoo_id, Integer
66
+ property :notes, Text, :lazy => true
67
+ property :created_on, Date
68
+ # property :updated_at, DateTime
69
+ end
70
+
71
+ touch_attributes = lambda do |exhibits|
72
+ [*exhibits].each do |exhibit|
73
+ exhibit.id
74
+ exhibit.name
75
+ exhibit.created_on
76
+ # exhibit.updated_at
77
+ end
78
+ end
79
+
80
+
81
+ c = configuration_options
82
+
83
+ if sqlfile && File.exists?(sqlfile)
84
+ puts "Found data-file. Importing from #{sqlfile}"
85
+ #adapter.execute("LOAD DATA LOCAL INFILE '#{sqlfile}' INTO TABLE exhibits")
86
+ `#{mysql_bin} -u #{c[:username]} #{"-p#{c[:password]}" unless c[:password].blank?} #{c[:database]} < #{sqlfile}`
87
+ else
88
+
89
+ Exhibit.auto_migrate!
90
+
91
+ exhibits = []
92
+ # pre-compute the insert statements and fake data compilation,
93
+ # so the benchmarks below show the actual runtime for the execute
94
+ # method, minus the setup steps
95
+ 10_000.times do
96
+ exhibits << [
97
+ 'INSERT INTO `exhibits` (`name`, `zoo_id`, `notes`, `created_on`) VALUES (?, ?, ?, ?)',
98
+ Faker::Company.name,
99
+ rand(10).ceil,
100
+ Faker::Lorem.paragraphs.join($/),
101
+ Date.today
102
+ ]
103
+ end
104
+ 10_000.times { |i| adapter.execute(*exhibits.at(i)) }
105
+
106
+ if sqlfile
107
+ answer = nil
108
+ until answer && answer[/^$|y|yes|n|no/]
109
+ print("Would you like to dump data into tmp/perf.sql (for faster setup)? [Yn]");
110
+ STDOUT.flush
111
+ answer = gets
112
+ end
113
+
114
+ if answer[/^$|y|yes/]
115
+ File.makedirs(File.dirname(sqlfile))
116
+ #adapter.execute("SELECT * INTO OUTFILE '#{sqlfile}' FROM exhibits;")
117
+ `#{mysqldump_bin} -u #{c[:username]} #{"-p#{c[:password]}" unless c[:password].blank?} #{c[:database]} exhibits > #{sqlfile}`
118
+ puts "File saved\n"
119
+ end
120
+ end
121
+
122
+ end
123
+
124
+ TIMES = ENV['x'] ? ENV['x'].to_i : 10_000
125
+
126
+ puts "You can specify how many times you want to run the benchmarks with rake:perf x=(number)"
127
+ puts "Some tasks will be run 10 and 1000 times less than (number)"
128
+ puts "Benchmarks will now run #{TIMES} times"
129
+
130
+ RBench.run(TIMES) do
131
+
132
+ column :times
133
+ column :dm, :title => "DM 0.9.3"
134
+ column :ar, :title => "AR 2.1"
135
+ column :diff, :compare => [:dm,:ar]
136
+
137
+ report "Model.get specific (not cached)" do
138
+ dm { touch_attributes[Exhibit.get(1)] }
139
+ ActiveRecord::Base.uncached { ar { touch_attributes[ARExhibit.find(1)] } }
140
+ end
141
+
142
+ report "Model.get specific (cached)" do
143
+ Exhibit.repository(:default) { dm { touch_attributes[Exhibit.get(1)] } }
144
+ ActiveRecord::Base.cache { ar { touch_attributes[ARExhibit.find(1)] } }
145
+ end
146
+
147
+ report "Model.first" do
148
+ dm { touch_attributes[Exhibit.first] }
149
+ ar { touch_attributes[ARExhibit.first] }
150
+ end
151
+
152
+ report "Model.all limit(100)", TIMES / 10 do
153
+ dm { touch_attributes[Exhibit.all(:limit => 100)] }
154
+ ar { touch_attributes[ARExhibit.find(:all, :limit => 100)] }
155
+ end
156
+
157
+ report "Model.all limit(10,000)", TIMES / 1000 do
158
+ dm { touch_attributes[Exhibit.all(:limit => 10_000)] }
159
+ ar { touch_attributes[ARExhibit.find(:all, :limit => 10_000)] }
160
+ end
161
+
162
+ create_exhibit = {
163
+ :name => Faker::Company.name,
164
+ :zoo_id => rand(10).ceil,
165
+ :notes => Faker::Lorem.paragraphs.join($/),
166
+ :created_on => Date.today
167
+ }
168
+
169
+ report "Model.create" do
170
+ dm { Exhibit.create(create_exhibit) }
171
+ ar { ARExhibit.create(create_exhibit) }
172
+ end
173
+
174
+ report "Resource#update" do
175
+ dm { e = Exhibit.get(1); e.name = 'bob'; e.save }
176
+ ar { e = ARExhibit.find(1); e.name = 'bob'; e.save }
177
+ end
178
+
179
+ report "Resource#destroy" do
180
+ dm { Exhibit.first.destroy }
181
+ ar { ARExhibit.first.destroy }
182
+ end
183
+
184
+ summary "Total"
185
+
186
+ end
187
+
188
+ connection = adapter.send(:create_connection)
189
+ command = connection.create_command("DROP TABLE exhibits")
190
+ command.execute_non_query rescue nil
191
+ connection.close
data/script/profile.rb ADDED
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'dm-core')
4
+
5
+ require 'rubygems'
6
+
7
+ gem 'ruby-prof', '>=0.6.0'
8
+ require 'ruby-prof'
9
+
10
+ gem 'faker', '>=0.3.1'
11
+ require 'faker'
12
+
13
+ OUTPUT = DataMapper.root / 'profile_results.txt'
14
+ #OUTPUT = DataMapper.root / 'profile_results.html'
15
+
16
+ SOCKET_FILE = Pathname.glob(%w[
17
+ /opt/local/var/run/mysql5/mysqld.sock
18
+ /tmp/mysqld.sock
19
+ /tmp/mysql.sock
20
+ /var/mysql/mysql.sock
21
+ ]).find { |path| path.socket? }
22
+
23
+ DataMapper::Logger.new(DataMapper.root / 'log' / 'dm.log', :debug)
24
+ DataMapper.setup(:default, "mysql://root@localhost/data_mapper_1?socket=#{SOCKET_FILE}")
25
+
26
+ class Exhibit
27
+ include DataMapper::Resource
28
+
29
+ property :id, Serial
30
+ property :name, String
31
+ property :zoo_id, Integer
32
+ property :notes, Text, :lazy => true
33
+ property :created_on, Date
34
+ # property :updated_at, DateTime
35
+
36
+ auto_migrate!
37
+ create # create one row for testing
38
+ end
39
+
40
+ touch_attributes = lambda do |exhibits|
41
+ [*exhibits].each do |exhibit|
42
+ exhibit.id
43
+ exhibit.name
44
+ exhibit.created_on
45
+ exhibit.updated_at
46
+ end
47
+ end
48
+
49
+ # RubyProf, making profiling Ruby pretty since 1899!
50
+ def profile(&b)
51
+ result = RubyProf.profile &b
52
+ printer = RubyProf::FlatPrinter.new(result)
53
+ #printer = RubyProf::GraphHtmlPrinter.new(result)
54
+ printer.print(OUTPUT.open('w+'))
55
+ end
56
+
57
+ profile do
58
+ # 10_000.times { touch_attributes[Exhibit.get(1)] }
59
+ #
60
+ # repository(:default) do
61
+ # 10_000.times { touch_attributes[Exhibit.get(1)] }
62
+ # end
63
+ #
64
+ # 1000.times { touch_attributes[Exhibit.all(:limit => 100)] }
65
+ #
66
+ # repository(:default) do
67
+ # 1000.times { touch_attributes[Exhibit.all(:limit => 100)] }
68
+ # end
69
+ #
70
+ # 10.times { touch_attributes[Exhibit.all(:limit => 10_000)] }
71
+ #
72
+ # repository(:default) do
73
+ # 10.times { touch_attributes[Exhibit.all(:limit => 10_000)] }
74
+ # end
75
+
76
+ create_exhibit = {
77
+ :name => Faker::Company.name,
78
+ :zoo_id => rand(10).ceil,
79
+ :notes => Faker::Lorem.paragraphs.join($/),
80
+ :created_on => Date.today
81
+ }
82
+
83
+ 1000.times { Exhibit.create(create_exhibit) }
84
+ end
85
+
86
+ puts "Done!"