dm-core 0.9.5 → 0.9.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/Manifest.txt +3 -0
  2. data/lib/dm-core.rb +14 -20
  3. data/lib/dm-core/adapters.rb +18 -0
  4. data/lib/dm-core/adapters/abstract_adapter.rb +17 -10
  5. data/lib/dm-core/adapters/data_objects_adapter.rb +17 -22
  6. data/lib/dm-core/adapters/in_memory_adapter.rb +87 -0
  7. data/lib/dm-core/adapters/mysql_adapter.rb +1 -1
  8. data/lib/dm-core/adapters/postgres_adapter.rb +2 -2
  9. data/lib/dm-core/adapters/sqlite3_adapter.rb +1 -1
  10. data/lib/dm-core/associations.rb +3 -2
  11. data/lib/dm-core/associations/many_to_many.rb +3 -3
  12. data/lib/dm-core/associations/one_to_many.rb +10 -2
  13. data/lib/dm-core/associations/relationship.rb +20 -16
  14. data/lib/dm-core/auto_migrations.rb +5 -4
  15. data/lib/dm-core/collection.rb +10 -6
  16. data/lib/dm-core/dependency_queue.rb +2 -1
  17. data/lib/dm-core/identity_map.rb +3 -6
  18. data/lib/dm-core/model.rb +48 -27
  19. data/lib/dm-core/property.rb +57 -37
  20. data/lib/dm-core/property_set.rb +29 -22
  21. data/lib/dm-core/query.rb +57 -49
  22. data/lib/dm-core/repository.rb +3 -3
  23. data/lib/dm-core/resource.rb +17 -15
  24. data/lib/dm-core/scope.rb +7 -7
  25. data/lib/dm-core/support/kernel.rb +6 -2
  26. data/lib/dm-core/transaction.rb +7 -7
  27. data/lib/dm-core/version.rb +1 -1
  28. data/script/performance.rb +114 -22
  29. data/spec/integration/association_spec.rb +31 -2
  30. data/spec/integration/association_through_spec.rb +2 -0
  31. data/spec/integration/associations/many_to_many_spec.rb +152 -0
  32. data/spec/integration/associations/one_to_many_spec.rb +40 -3
  33. data/spec/integration/dependency_queue_spec.rb +0 -12
  34. data/spec/integration/postgres_adapter_spec.rb +1 -1
  35. data/spec/integration/property_spec.rb +3 -3
  36. data/spec/integration/query_spec.rb +39 -8
  37. data/spec/integration/resource_spec.rb +10 -6
  38. data/spec/integration/sti_spec.rb +22 -0
  39. data/spec/integration/strategic_eager_loading_spec.rb +21 -6
  40. data/spec/integration/type_spec.rb +1 -0
  41. data/spec/lib/model_loader.rb +10 -1
  42. data/spec/models/content.rb +16 -0
  43. data/spec/spec_helper.rb +4 -1
  44. data/spec/unit/adapters/data_objects_adapter_spec.rb +11 -11
  45. data/spec/unit/adapters/in_memory_adapter_spec.rb +98 -0
  46. data/spec/unit/associations/many_to_many_spec.rb +16 -1
  47. data/spec/unit/model_spec.rb +0 -16
  48. data/spec/unit/property_set_spec.rb +8 -1
  49. data/spec/unit/property_spec.rb +476 -240
  50. data/spec/unit/query_spec.rb +41 -0
  51. data/spec/unit/resource_spec.rb +75 -56
  52. data/tasks/ci.rb +4 -36
  53. data/tasks/dm.rb +3 -3
  54. metadata +5 -2
@@ -4,10 +4,11 @@ module DataMapper
4
4
  include Enumerable
5
5
 
6
6
  def [](name)
7
- @property_for[name] || raise(ArgumentError, "Unknown property '#{name}'", caller)
7
+ property_for(name) || raise(ArgumentError, "Unknown property '#{name}'", caller)
8
8
  end
9
9
 
10
10
  def []=(name, property)
11
+ @key, @defaults = nil
11
12
  if existing_property = detect { |p| p.name == name }
12
13
  property.hash
13
14
  @entries[@entries.index(existing_property)] = property
@@ -18,14 +19,23 @@ module DataMapper
18
19
  end
19
20
 
20
21
  def has_property?(name)
21
- !!@property_for[name]
22
+ !!property_for(name)
22
23
  end
23
24
 
24
25
  def slice(*names)
25
- @property_for.values_at(*names)
26
+ @key, @defaults = nil
27
+ names.map do |name|
28
+ property_for(name)
29
+ end
30
+ end
31
+
32
+ def clear
33
+ @key, @defaults = nil
34
+ @entries.clear
26
35
  end
27
36
 
28
37
  def add(*properties)
38
+ @key, @defaults = nil
29
39
  @entries.push(*properties)
30
40
  properties.each { |property| property.hash }
31
41
  self
@@ -47,11 +57,11 @@ module DataMapper
47
57
  end
48
58
 
49
59
  def defaults
50
- reject { |property| property.lazy? }
60
+ @defaults ||= reject { |property| property.lazy? }
51
61
  end
52
62
 
53
63
  def key
54
- select { |property| property.key? }
64
+ @key ||= select { |property| property.key? }
55
65
  end
56
66
 
57
67
  def indexes
@@ -68,10 +78,6 @@ module DataMapper
68
78
  index_hash
69
79
  end
70
80
 
71
- def inheritance_property
72
- detect { |property| property.type == DataMapper::Types::Discriminator }
73
- end
74
-
75
81
  def get(resource)
76
82
  map { |property| property.get(resource) }
77
83
  end
@@ -93,7 +99,7 @@ module DataMapper
93
99
  end
94
100
 
95
101
  def lazy_context(name)
96
- lazy_contexts[name]
102
+ lazy_contexts[name] ||= []
97
103
  end
98
104
 
99
105
  def lazy_load_context(names)
@@ -128,25 +134,17 @@ module DataMapper
128
134
  assert_kind_of 'properties', properties, Enumerable
129
135
 
130
136
  @entries = properties
131
- @property_for = hash_for_property_for
137
+ @property_for = {}
132
138
  end
133
139
 
134
140
  def initialize_copy(orig)
141
+ @key, @defaults = nil
135
142
  @entries = orig.entries.dup
136
- @property_for = hash_for_property_for
137
- end
138
-
139
- def hash_for_property_for
140
- Hash.new do |h,k|
141
- ksym = k.to_sym
142
- if property = detect { |property| property.name == ksym }
143
- h[ksym] = h[k.to_s] = property
144
- end
145
- end
143
+ @property_for = {}
146
144
  end
147
145
 
148
146
  def lazy_contexts
149
- @lazy_contexts ||= Hash.new { |h,context| h[context] = [] }
147
+ @lazy_contexts ||= {}
150
148
  end
151
149
 
152
150
  def parse_index(index, property, index_hash)
@@ -158,5 +156,14 @@ module DataMapper
158
156
  when Enumerable then index.each { |idx| parse_index(idx, property, index_hash) }
159
157
  end
160
158
  end
159
+
160
+ def property_for(name)
161
+ unless @property_for[name]
162
+ property = detect { |property| property.name == name.to_sym }
163
+ @property_for[name.to_s] = @property_for[name.to_sym] = property if property
164
+ end
165
+ @property_for[name]
166
+ end
167
+
161
168
  end # class PropertySet
162
169
  end # module DataMapper
data/lib/dm-core/query.rb CHANGED
@@ -99,9 +99,12 @@ module DataMapper
99
99
  bind_values
100
100
  end
101
101
 
102
- # TODO: spec this
103
- def inheritance_property_index(repository)
104
- fields.index(model.inheritance_property(repository.name))
102
+ def inheritance_property
103
+ fields.detect { |property| property.type == DataMapper::Types::Discriminator }
104
+ end
105
+
106
+ def inheritance_property_index
107
+ fields.index(inheritance_property)
105
108
  end
106
109
 
107
110
  # TODO: spec this
@@ -167,7 +170,7 @@ module DataMapper
167
170
  @unique = options.fetch :unique, false # must be true or false
168
171
  @offset = options.fetch :offset, 0 # must be an Integer greater than or equal to 0
169
172
  @limit = options.fetch :limit, nil # must be an Integer greater than or equal to 1
170
- @order = options.fetch :order, model.default_order # must be an Array of Symbol, DM::Query::Direction or DM::Property
173
+ @order = options.fetch :order, model.default_order(@repository.name) # must be an Array of Symbol, DM::Query::Direction or DM::Property
171
174
  @add_reversed = options.fetch :add_reversed, false # must be true or false
172
175
  @fields = options.fetch :fields, @properties.defaults # must be an Array of Symbol, String or DM::Property
173
176
  @links = options.fetch :links, [] # must be an Array of Tuples - Tuple [DM::Query,DM::Assoc::Relationship]
@@ -190,7 +193,7 @@ module DataMapper
190
193
  @includes = normalize_includes(@includes)
191
194
 
192
195
  # treat all non-options as conditions
193
- (options.keys - OPTIONS - OPTIONS.map { |option| option.to_s }).each do |k|
196
+ (options.keys - OPTIONS).each do |k|
194
197
  append_condition(k, options[k])
195
198
  end
196
199
 
@@ -218,53 +221,50 @@ module DataMapper
218
221
 
219
222
  # validate the options
220
223
  def assert_valid_options(options)
221
- # validate the reload option and unique option
222
- ([ :reload, :unique ] & options.keys).each do |attribute|
223
- if options[attribute] != true && options[attribute] != false
224
- raise ArgumentError, "+options[:#{attribute}]+ must be true or false, but was #{options[attribute].inspect}", caller(2)
225
- end
226
- end
227
-
228
- # validate the offset and limit options
229
- ([ :offset, :limit ] & options.keys).each do |attribute|
230
- value = options[attribute]
231
- assert_kind_of "options[:#{attribute}]", value, Integer
232
- end
233
-
234
- if options.has_key?(:offset) && options[:offset] < 0
235
- raise ArgumentError, "+options[:offset]+ must be greater than or equal to 0, but was #{options[:offset].inspect}", caller(2)
236
- end
237
-
238
- if options.has_key?(:limit) && options[:limit] < 1
239
- raise ArgumentError, "+options[:limit]+ must be greater than or equal to 1, but was #{options[:limit].inspect}", caller(2)
240
- end
224
+ # [DB] This might look more ugly now, but it's 2x as fast as the old code
225
+ # [DB] This is one of the heavy spots for Query.new I found during profiling.
226
+ options.each_pair do |attribute, value|
227
+
228
+ # validate the reload option and unique option
229
+ if [:reload, :unique].include? attribute
230
+ if value != true && value != false
231
+ raise ArgumentError, "+options[:#{attribute}]+ must be true or false, but was #{value.inspect}", caller(2)
232
+ end
241
233
 
242
- # validate the order, fields, links, includes and conditions options
243
- ([ :order, :fields, :links, :includes ] & options.keys).each do |attribute|
244
- value = options[attribute]
245
- assert_kind_of "options[:#{attribute}]", value, Array
234
+ # validate the offset and limit options
235
+ elsif [:offset, :limit].include? attribute
236
+ assert_kind_of "options[:#{attribute}]", value, Integer
237
+ if attribute == :offset && value < 0
238
+ raise ArgumentError, "+options[:offset]+ must be greater than or equal to 0, but was #{value.inspect}", caller(2)
239
+ elsif attribute == :limit && value < 1
240
+ raise ArgumentError, "+options[:limit]+ must be greater than or equal to 1, but was #{options[:limit].inspect}", caller(2)
241
+ end
246
242
 
247
- if value.empty?
248
- if attribute == :fields
249
- if options[:unique] == false
250
- raise ArgumentError, '+options[:fields]+ cannot be empty if +options[:unique] is false', caller(2)
251
- end
252
- elsif attribute == :order
253
- if options[:fields] && options[:fields].any? { |p| !p.kind_of?(Operator) }
254
- raise ArgumentError, '+options[:order]+ cannot be empty if +options[:fields] contains a non-operator', caller(2)
243
+ # validate the :order, :fields, :links and :includes options
244
+ elsif [ :order, :fields, :links, :includes ].include? attribute
245
+ assert_kind_of "options[:#{attribute}]", value, Array
246
+
247
+ if value.empty?
248
+ if attribute == :fields
249
+ if options[:unique] == false
250
+ raise ArgumentError, '+options[:fields]+ cannot be empty if +options[:unique] is false', caller(2)
251
+ end
252
+ elsif attribute == :order
253
+ if options[:fields] && options[:fields].any? { |p| !p.kind_of?(Operator) }
254
+ raise ArgumentError, '+options[:order]+ cannot be empty if +options[:fields] contains a non-operator', caller(2)
255
+ end
256
+ else
257
+ raise ArgumentError, "+options[:#{attribute}]+ cannot be empty", caller(2)
255
258
  end
256
- else
257
- raise ArgumentError, "+options[:#{attribute}]+ cannot be empty", caller(2)
258
259
  end
259
- end
260
- end
261
260
 
262
- if options.has_key?(:conditions)
263
- value = options[:conditions]
264
- assert_kind_of 'options[:conditions]', value, Hash, Array
261
+ # validates the :conditions option
262
+ elsif :conditions == attribute
263
+ assert_kind_of 'options[:conditions]', value, Hash, Array
265
264
 
266
- if value.empty?
267
- raise ArgumentError, '+options[:conditions]+ cannot be empty', caller(2)
265
+ if value.empty?
266
+ raise ArgumentError, '+options[:conditions]+ cannot be empty', caller(2)
267
+ end
268
268
  end
269
269
  end
270
270
  end
@@ -402,6 +402,7 @@ module DataMapper
402
402
  clause
403
403
  when Operator
404
404
  operator = clause.operator
405
+ return if operator == :not && bind_value == []
405
406
  if clause.target.is_a?(Symbol)
406
407
  @properties[clause.target]
407
408
  elsif clause.target.is_a?(Query::Path)
@@ -438,7 +439,7 @@ module DataMapper
438
439
  dump_custom_value(property_or_path.property, bind_value)
439
440
  when Property
440
441
  if property_or_path.custom?
441
- property_or_path.type.dump(bind_value, property_or_path)
442
+ property_or_path.type.dump(bind_value, property_or_path)
442
443
  else
443
444
  bind_value
444
445
  end
@@ -463,10 +464,11 @@ module DataMapper
463
464
 
464
465
  # build an index of conditions by the property and operator to
465
466
  # avoid nested looping
466
- conditions_index = Hash.new { |h,k| h[k] = {} }
467
+ conditions_index = {}
467
468
  @conditions.each do |condition|
468
469
  operator, property = *condition
469
470
  next if :raw == operator
471
+ conditions_index[property] ||= {}
470
472
  conditions_index[property][operator] = condition
471
473
  end
472
474
 
@@ -476,6 +478,7 @@ module DataMapper
476
478
  other_operator, other_property, other_bind_value = *other_condition
477
479
 
478
480
  unless :raw == other_operator
481
+ conditions_index[other_property] ||= {}
479
482
  if condition = conditions_index[other_property][other_operator]
480
483
  operator, property, bind_value = *condition
481
484
 
@@ -587,6 +590,11 @@ module DataMapper
587
590
  @property ? @property.field(*args) : nil
588
591
  end
589
592
 
593
+ # more duck typing
594
+ def to_sym
595
+ @property ? @property.name.to_sym : @model.storage_name(@repository).to_sym
596
+ end
597
+
590
598
  private
591
599
 
592
600
  def initialize(repository, relationships, model, property_name = nil)
@@ -608,7 +616,7 @@ module DataMapper
608
616
  end
609
617
 
610
618
  if @model.properties(@repository.name)[method]
611
- @property = @model.properties(@repository.name)[method]
619
+ @property = @model.properties(@repository.name)[method] unless @property
612
620
  return self
613
621
  end
614
622
 
@@ -33,11 +33,11 @@ module DataMapper
33
33
  end
34
34
 
35
35
  def identity_map(model)
36
- @identity_maps[model]
36
+ @identity_maps[model] ||= IdentityMap.new
37
37
  end
38
38
 
39
39
  # TODO: spec this
40
- def scope(&block)
40
+ def scope
41
41
  Repository.context << self
42
42
 
43
43
  begin
@@ -97,7 +97,7 @@ module DataMapper
97
97
  assert_kind_of 'name', name, Symbol
98
98
 
99
99
  @name = name
100
- @identity_maps = Hash.new { |h,model| h[model] = IdentityMap.new }
100
+ @identity_maps = {}
101
101
  end
102
102
 
103
103
  # TODO: move to dm-more/dm-migrations
@@ -40,6 +40,10 @@ module DataMapper
40
40
  model.const_set('Resource', self) unless model.const_defined?('Resource')
41
41
  extra_inclusions.each { |inclusion| model.send(:include, inclusion) }
42
42
  descendants << model
43
+ class << model
44
+ @_valid_model = false
45
+ attr_reader :_valid_model
46
+ end
43
47
  end
44
48
 
45
49
  # Return all classes that include the DataMapper::Resource module
@@ -53,7 +57,7 @@ module DataMapper
53
57
  # include DataMapper::Resource
54
58
  # end
55
59
  #
56
- # DataMapper.Resource.decendents[1].type == Foo
60
+ # DataMapper::Resource.descendants.to_a.first == Foo
57
61
  #
58
62
  # -
59
63
  # @api semipublic
@@ -273,9 +277,7 @@ module DataMapper
273
277
  associations_saved = false
274
278
  child_associations.each { |a| associations_saved |= a.save }
275
279
 
276
- saved = if dirty? || (new_record? && key_properties.any? { |p| p.serial? })
277
- new_record? ? create : update
278
- end
280
+ saved = new_record? ? create : update
279
281
 
280
282
  if saved
281
283
  original_values.clear
@@ -377,20 +379,15 @@ module DataMapper
377
379
  property = properties[name]
378
380
  new_value = property.get!(self)
379
381
 
380
- if property.custom?
381
- new_value = property.type.dump(new_value, property)
382
- old_value = property.type.dump(old_value, property)
383
- end
384
-
385
382
  dirty = case property.track
386
- when :hash then old_value != new_value.hash
387
- else
388
- old_value != new_value
383
+ when :hash then old_value != new_value.hash
384
+ else
385
+ property.value(old_value) != property.value(new_value)
389
386
  end
390
387
 
391
388
  if dirty
392
389
  property.hash
393
- dirty_attributes[property] = new_value
390
+ dirty_attributes[property] = property.value(new_value)
394
391
  end
395
392
  end
396
393
 
@@ -547,6 +544,8 @@ module DataMapper
547
544
 
548
545
  # Needs to be a protected method so that it is hookable
549
546
  def create
547
+ # Can't create a resource that is not dirty and doesn't have serial keys
548
+ return false if new_record? && !dirty? && !model.key.any? { |p| p.serial? }
550
549
  # set defaults for new resource
551
550
  properties.each do |property|
552
551
  next if attribute_loaded?(property.name)
@@ -578,6 +577,7 @@ module DataMapper
578
577
  end
579
578
 
580
579
  def assert_valid_model # :nodoc:
580
+ return if self.class._valid_model
581
581
  properties = self.properties
582
582
 
583
583
  if properties.empty? && relationships.empty?
@@ -587,6 +587,8 @@ module DataMapper
587
587
  if properties.key.empty?
588
588
  raise IncompleteResourceError, "#{model.name} must have a key."
589
589
  end
590
+
591
+ self.class.instance_variable_set("@_valid_model", true)
590
592
  end
591
593
 
592
594
  # TODO document
@@ -625,8 +627,8 @@ module DataMapper
625
627
  # @api public
626
628
  #
627
629
  # TODO: move to dm-more/dm-transactions
628
- def transaction(&block)
629
- model.transaction(&block)
630
+ def transaction
631
+ model.transaction { |*block_args| yield(*block_args) }
630
632
  end
631
633
  end # module Transaction
632
634
 
data/lib/dm-core/scope.rb CHANGED
@@ -5,8 +5,8 @@ module DataMapper
5
5
  # @api private
6
6
  def default_scope(repository_name = nil)
7
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]
8
+ @default_scope ||= {}
9
+ @default_scope[repository_name] ||= {}
10
10
  end
11
11
 
12
12
  # @api private
@@ -17,13 +17,13 @@ module DataMapper
17
17
  protected
18
18
 
19
19
  # @api semipublic
20
- def with_scope(query, &block)
20
+ def with_scope(query)
21
21
  # merge the current scope with the passed in query
22
- with_exclusive_scope(self.query ? self.query.merge(query) : query, &block)
22
+ with_exclusive_scope(self.query ? self.query.merge(query) : query) {|*block_args| yield(*block_args) }
23
23
  end
24
24
 
25
25
  # @api semipublic
26
- def with_exclusive_scope(query, &block)
26
+ def with_exclusive_scope(query)
27
27
  query = DataMapper::Query.new(repository, self, query) if query.kind_of?(Hash)
28
28
 
29
29
  scope_stack << query
@@ -44,8 +44,8 @@ module DataMapper
44
44
 
45
45
  # @api private
46
46
  def scope_stack
47
- scope_stack_for = Thread.current[:dm_scope_stack] ||= Hash.new { |h,model| h[model] = [] }
48
- scope_stack_for[self]
47
+ scope_stack_for = Thread.current[:dm_scope_stack] ||= {}
48
+ scope_stack_for[self] ||= []
49
49
  end
50
50
 
51
51
  # @api private
@@ -1,7 +1,11 @@
1
1
  module Kernel
2
2
  # Delegates to DataMapper::repository.
3
3
  # Will not overwrite if a method of the same name is pre-defined.
4
- def repository(*args, &block)
5
- DataMapper.repository(*args, &block)
4
+ def repository(*args)
5
+ if block_given?
6
+ DataMapper.repository(*args) { |*block_args| yield(*block_args) }
7
+ else
8
+ DataMapper.repository(*args)
9
+ end
6
10
  end
7
11
  end # module Kernel