dm-core 0.9.5 → 0.9.6

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 (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