dm-core 0.9.2 → 0.9.3

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -61,9 +61,9 @@ module DataMapper
61
61
  #
62
62
  # class Post
63
63
  # include DataMapper::Resource
64
- # property :title, String, :accessor => :private
64
+ # property :title, String, :accessor => :private
65
65
  # # Both reader and writer are private
66
- # property :body, Text, :accessor => :protected
66
+ # property :body, Text, :accessor => :protected
67
67
  # # Both reader and writer are protected
68
68
  # end
69
69
  #
@@ -243,7 +243,7 @@ module DataMapper
243
243
  # NOTE: PLEASE update PROPERTY_OPTIONS in DataMapper::Type when updating
244
244
  # them here
245
245
  PROPERTY_OPTIONS = [
246
- :public, :protected, :private, :accessor, :reader, :writer,
246
+ :accessor, :reader, :writer,
247
247
  :lazy, :default, :nullable, :key, :serial, :field, :size, :length,
248
248
  :format, :index, :unique_index, :check, :ordinal, :auto_validation,
249
249
  :validates, :unique, :track, :precision, :scale
@@ -264,7 +264,8 @@ module DataMapper
264
264
  Time,
265
265
  Object,
266
266
  Class,
267
- DataMapper::Types::Discriminator
267
+ DataMapper::Types::Discriminator,
268
+ DataMapper::Types::Serial
268
269
  ]
269
270
 
270
271
  IMMUTABLE_TYPES = [ TrueClass, Float, Integer, BigDecimal]
@@ -273,7 +274,8 @@ module DataMapper
273
274
 
274
275
  DEFAULT_LENGTH = 50
275
276
  DEFAULT_PRECISION = 10
276
- DEFAULT_SCALE = 0
277
+ DEFAULT_SCALE_BIGDECIMAL = 0
278
+ DEFAULT_SCALE_FLOAT = nil
277
279
 
278
280
  attr_reader :primitive, :model, :name, :instance_variable_name,
279
281
  :type, :reader_visibility, :writer_visibility, :getter, :options,
@@ -284,18 +286,14 @@ module DataMapper
284
286
  # @return <String> name of field in data-store
285
287
  # -
286
288
  # @api semi-public
287
- def field(*args)
288
- @options.fetch(:field, repository(*args).adapter.field_naming_convention.call(name))
289
+ def field(repository_name = nil)
290
+ @field || fields[repository_name || model.repository_name]
289
291
  end
290
292
 
291
293
  def unique
292
294
  @unique ||= @options.fetch(:unique, @serial || @key || false)
293
295
  end
294
296
 
295
- def repository(*args)
296
- @model.repository(*args)
297
- end
298
-
299
297
  def hash
300
298
  if @custom && !@bound
301
299
  @type.bind(self)
@@ -336,7 +334,6 @@ module DataMapper
336
334
  @lazy
337
335
  end
338
336
 
339
-
340
337
  # Returns whether or not the property is a key or a part of a key
341
338
  #
342
339
  # @return <TrueClass, FalseClass> whether the property is a key or a part of
@@ -377,17 +374,7 @@ module DataMapper
377
374
  def get(resource)
378
375
  new_record = resource.new_record?
379
376
 
380
- unless new_record || resource.attribute_loaded?(name)
381
- # TODO: refactor this section
382
- contexts = if lazy?
383
- name
384
- else
385
- model.properties(resource.repository.name).reject do |property|
386
- property.lazy? || resource.attribute_loaded?(property.name)
387
- end
388
- end
389
- resource.send(:lazy_load, contexts)
390
- end
377
+ lazy_load(resource) unless new_record || resource.attribute_loaded?(name)
391
378
 
392
379
  value = get!(resource)
393
380
 
@@ -416,6 +403,7 @@ module DataMapper
416
403
  #-
417
404
  # @api private
418
405
  def set(resource, value)
406
+ lazy_load(resource) unless resource.new_record? || resource.attribute_loaded?(name)
419
407
  new_value = typecast(value)
420
408
  old_value = get!(resource)
421
409
 
@@ -432,6 +420,21 @@ module DataMapper
432
420
  resource.instance_variable_set(instance_variable_name, value)
433
421
  end
434
422
 
423
+ # Loads lazy columns when get or set is called.
424
+ #-
425
+ # @api private
426
+ def lazy_load(resource)
427
+ # TODO: refactor this section
428
+ contexts = if lazy?
429
+ name
430
+ else
431
+ model.properties(resource.repository.name).reject do |property|
432
+ property.lazy? || resource.attribute_loaded?(property.name)
433
+ end
434
+ end
435
+ resource.send(:lazy_load, contexts)
436
+ end
437
+
435
438
  # typecasts values into a primitive
436
439
  #
437
440
  # @return <TrueClass, String, Float, Integer, BigDecimal, DateTime, Date, Time
@@ -439,17 +442,37 @@ module DataMapper
439
442
  #-
440
443
  # @api private
441
444
  def typecast(value)
442
- return value if value.kind_of?(type) || value.nil?
445
+ return type.typecast(value, self) if type.respond_to?(:typecast)
446
+ return value if value.kind_of?(primitive) || value.nil?
443
447
  begin
444
- if type == TrueClass then %w[ true 1 t ].include?(value.to_s.downcase)
445
- elsif type == String then value.to_s
446
- elsif type == Float then value.to_f
447
- elsif type == Integer then value.to_i
448
- elsif type == BigDecimal then BigDecimal(value.to_s)
449
- elsif type == DateTime then typecast_to_datetime(value)
450
- elsif type == Date then typecast_to_date(value)
451
- elsif type == Time then typecast_to_time(value)
452
- elsif type == Class then self.class.find_const(value)
448
+ if primitive == TrueClass then %w[ true 1 t ].include?(value.to_s.downcase)
449
+ elsif primitive == String then value.to_s
450
+ elsif primitive == Float then value.to_f
451
+ elsif primitive == Integer
452
+ # The simplest possible implementation, i.e. value.to_i, is not
453
+ # desirable because "junk".to_i gives "0". We want nil instead,
454
+ # because this makes it clear that the typecast failed.
455
+ #
456
+ # After benchmarking, we preferred the current implementation over
457
+ # these two alternatives:
458
+ # * Integer(value) rescue nil
459
+ # * Integer(value_to_s =~ /(\d+)/ ? $1 : value_to_s) rescue nil
460
+ value_to_i = value.to_i
461
+ if value_to_i == 0 && value != '0'
462
+ value_to_s = value.to_s
463
+ begin
464
+ Integer(value_to_s =~ /^(\d+)/ ? $1 : value_to_s)
465
+ rescue ArgumentError
466
+ nil
467
+ end
468
+ else
469
+ value_to_i
470
+ end
471
+ elsif primitive == BigDecimal then BigDecimal(value.to_s)
472
+ elsif primitive == DateTime then typecast_to_datetime(value)
473
+ elsif primitive == Date then typecast_to_date(value)
474
+ elsif primitive == Time then typecast_to_time(value)
475
+ elsif primitive == Class then self.class.find_const(value)
453
476
  else
454
477
  value
455
478
  end
@@ -503,6 +526,7 @@ module DataMapper
503
526
  @primitive = @options.fetch(:primitive, @type.respond_to?(:primitive) ? @type.primitive : @type)
504
527
 
505
528
  @getter = TrueClass == @primitive ? "#{@name}?".to_sym : @name
529
+ @field = @options.fetch(:field, nil)
506
530
  @serial = @options.fetch(:serial, false)
507
531
  @key = @options.fetch(:key, @serial || false)
508
532
  @default = @options.fetch(:default, nil)
@@ -524,18 +548,23 @@ module DataMapper
524
548
  @length = @options.fetch(:length, @options.fetch(:size, DEFAULT_LENGTH))
525
549
  elsif BigDecimal == @primitive || Float == @primitive
526
550
  @precision = @options.fetch(:precision, DEFAULT_PRECISION)
527
- @scale = @options.fetch(:scale, DEFAULT_SCALE)
551
+
552
+ default_scale = (Float == @primitive) ? DEFAULT_SCALE_FLOAT : DEFAULT_SCALE_BIGDECIMAL
553
+ @scale = @options.fetch(:scale, default_scale)
554
+ # @scale = @options.fetch(:scale, DEFAULT_SCALE_BIGDECIMAL)
528
555
 
529
556
  unless @precision > 0
530
557
  raise ArgumentError, "precision must be greater than 0, but was #{@precision.inspect}"
531
558
  end
532
559
 
533
- unless @scale >= 0
534
- raise ArgumentError, "scale must be equal to or greater than 0, but was #{@scale.inspect}"
535
- end
560
+ if (BigDecimal == @primitive) || (Float == @primitive && !@scale.nil?)
561
+ unless @scale >= 0
562
+ raise ArgumentError, "scale must be equal to or greater than 0, but was #{@scale.inspect}"
563
+ end
536
564
 
537
- unless @precision >= @scale
538
- raise ArgumentError, "precision must be equal to or greater than scale, but was #{@precision.inspect} and scale was #{@scale.inspect}"
565
+ unless @precision >= @scale
566
+ raise ArgumentError, "precision must be equal to or greater than scale, but was #{@precision.inspect} and scale was #{@scale.inspect}"
567
+ end
539
568
  end
540
569
  end
541
570
 
@@ -545,11 +574,13 @@ module DataMapper
545
574
  @model.property_serialization_setup(self) if @model.respond_to?(:property_serialization_setup)
546
575
  end
547
576
 
577
+ def fields
578
+ @fields ||= Hash.new { |h,k| h[k] = repository(k).adapter.field_naming_convention.call(self.name) }
579
+ end
580
+
548
581
  def determine_visibility # :nodoc:
549
582
  @reader_visibility = @options[:reader] || @options[:accessor] || :public
550
583
  @writer_visibility = @options[:writer] || @options[:accessor] || :public
551
- @writer_visibility = :protected if @options[:protected]
552
- @writer_visibility = :private if @options[:private]
553
584
 
554
585
  unless VISIBILITY_OPTIONS.include?(@reader_visibility) && VISIBILITY_OPTIONS.include?(@writer_visibility)
555
586
  raise ArgumentError, 'property visibility must be :public, :protected, or :private', caller(2)
@@ -4,10 +4,12 @@ module DataMapper
4
4
  include Enumerable
5
5
 
6
6
  def [](name)
7
- @property_for[name]
7
+ @property_for[name] || raise(ArgumentError, "Unknown property '#{name}'", caller)
8
8
  end
9
9
 
10
- alias has_property? []
10
+ def has_property?(name)
11
+ !!@property_for[name]
12
+ end
11
13
 
12
14
  def slice(*names)
13
15
  @property_for.values_at(*names)
@@ -44,13 +46,15 @@ module DataMapper
44
46
 
45
47
  def indexes
46
48
  index_hash = {}
47
- each { |property| parse_index(property.index, property.field.to_s, index_hash) }
49
+ repository_name = repository.name
50
+ each { |property| parse_index(property.index, property.field(repository_name), index_hash) }
48
51
  index_hash
49
52
  end
50
53
 
51
54
  def unique_indexes
52
55
  index_hash = {}
53
- each { |property| parse_index(property.unique_index, property.field.to_s, index_hash) }
56
+ repository_name = repository.name
57
+ each { |property| parse_index(property.unique_index, property.field(repository_name), index_hash) }
54
58
  index_hash
55
59
  end
56
60
 
@@ -108,16 +112,6 @@ module DataMapper
108
112
  '#<PropertySet:{' + map { |property| property.inspect }.join(',') + '}>'
109
113
  end
110
114
 
111
- def dup(target = nil)
112
- return super() unless target
113
-
114
- properties = map do |property|
115
- Property.new(target || property.model, property.name, property.type, property.options.dup)
116
- end
117
-
118
- self.class.new(properties)
119
- end
120
-
121
115
  private
122
116
 
123
117
  def initialize(properties = [])
data/lib/dm-core/query.rb CHANGED
@@ -3,10 +3,10 @@ module DataMapper
3
3
  include Assertions
4
4
 
5
5
  OPTIONS = [
6
- :reload, :offset, :limit, :order, :add_reversed, :fields, :links, :includes, :conditions
6
+ :reload, :offset, :limit, :order, :add_reversed, :fields, :links, :includes, :conditions, :unique
7
7
  ]
8
8
 
9
- attr_reader :repository, :model, *OPTIONS - [ :reload ]
9
+ attr_reader :repository, :model, *OPTIONS - [ :reload, :unique ]
10
10
  attr_writer :add_reversed
11
11
  alias add_reversed? add_reversed
12
12
 
@@ -14,6 +14,10 @@ module DataMapper
14
14
  @reload
15
15
  end
16
16
 
17
+ def unique?
18
+ @unique
19
+ end
20
+
17
21
  def reverse
18
22
  dup.reverse!
19
23
  end
@@ -42,6 +46,7 @@ module DataMapper
42
46
 
43
47
  # only overwrite the attributes with non-default values
44
48
  @reload = other.reload? unless other.reload? == false
49
+ @unique = other.unique? unless other.unique? == false
45
50
  @offset = other.offset unless other.offset == 0
46
51
  @limit = other.limit unless other.limit == nil
47
52
  @order = other.order unless other.order == model.default_order
@@ -67,6 +72,7 @@ module DataMapper
67
72
  # return hash == other.hash
68
73
  @model == other.model &&
69
74
  @reload == other.reload? &&
75
+ @unique == other.unique? &&
70
76
  @offset == other.offset &&
71
77
  @limit == other.limit &&
72
78
  @order == other.order && # order is significant, so do not sort this
@@ -136,6 +142,7 @@ module DataMapper
136
142
  [ :limit, limit ],
137
143
  [ :offset, offset ],
138
144
  [ :reload, reload? ],
145
+ [ :unique, unique? ],
139
146
  ]
140
147
 
141
148
  "#<#{self.class.name} #{attrs.map { |(k,v)| "@#{k}=#{v.inspect}" } * ' '}>"
@@ -157,6 +164,7 @@ module DataMapper
157
164
 
158
165
  @model = model # must be Class that includes DM::Resource
159
166
  @reload = options.fetch :reload, false # must be true or false
167
+ @unique = options.fetch :unique, false # must be true or false
160
168
  @offset = options.fetch :offset, 0 # must be an Integer greater than or equal to 0
161
169
  @limit = options.fetch :limit, nil # must be an Integer greater than or equal to 1
162
170
  @order = options.fetch :order, model.default_order # must be an Array of Symbol, DM::Query::Direction or DM::Property
@@ -214,18 +222,20 @@ module DataMapper
214
222
  options.each do |key, value|
215
223
  case key
216
224
  when DataMapper::Query::Operator
217
- options[key] = properties[key.target].type.dump(value, properties[key.target]) if !properties[key.target].nil? && properties[key.target].custom?
225
+ options[key] = properties[key.target].type.dump(value, properties[key.target]) if properties.has_property?(key.target) && properties[key.target].custom?
218
226
  when Symbol, String
219
- options[key] = properties[key].type.dump(value, properties[key]) if !properties[key].nil? && properties[key].custom?
227
+ options[key] = properties[key].type.dump(value, properties[key]) if properties.has_property?(key) && properties[key].custom?
220
228
  end
221
229
  end
222
230
  end
223
231
 
224
232
  # validate the options
225
233
  def assert_valid_options(options)
226
- # validate the reload option
227
- if options.has_key?(:reload) && options[:reload] != true && options[:reload] != false
228
- raise ArgumentError, "+options[:reload]+ must be true or false, but was #{options[:reload].inspect}", caller(2)
234
+ # validate the reload option and unique option
235
+ ([ :reload, :unique ] & options.keys).each do |attribute|
236
+ if options[attribute] != true && options[attribute] != false
237
+ raise ArgumentError, "+options[:#{attribute}]+ must be true or false, but was #{options[attribute].inspect}", caller(2)
238
+ end
229
239
  end
230
240
 
231
241
  # validate the offset and limit options
@@ -248,7 +258,17 @@ module DataMapper
248
258
  assert_kind_of "options[:#{attribute}]", value, Array
249
259
 
250
260
  if value.empty?
251
- raise ArgumentError, "+options[:#{attribute}]+ cannot be empty", caller(2)
261
+ if attribute == :fields
262
+ if options[:unique] == false
263
+ raise ArgumentError, '+options[:fields]+ cannot be empty if +options[:unique] is false', caller(2)
264
+ end
265
+ elsif attribute == :order
266
+ if options[:fields] && options[:fields].any? { |p| !p.kind_of?(Operator) }
267
+ raise ArgumentError, '+options[:order]+ cannot be empty if +options[:fields] contains a non-operator', caller(2)
268
+ end
269
+ else
270
+ raise ArgumentError, "+options[:#{attribute}]+ cannot be empty", caller(2)
271
+ end
252
272
  end
253
273
  end
254
274
 
@@ -318,9 +338,10 @@ module DataMapper
318
338
  # normalize fields to DM::Property
319
339
  def normalize_fields(fields)
320
340
  # TODO: return a PropertySet
341
+ # TODO: raise an exception if the property is not available in the repository
321
342
  fields.map do |field|
322
343
  case field
323
- when Property
344
+ when Property, Operator
324
345
  # TODO: if the Property's model doesn't match
325
346
  # self.model, append the property's model to @links
326
347
  # eg:
@@ -527,6 +548,8 @@ module DataMapper
527
548
  end
528
549
 
529
550
  def ==(other)
551
+ return true if super
552
+ return false unless other.kind_of?(self.class)
530
553
  @operator == other.operator && @target == other.target
531
554
  end
532
555
 
@@ -542,6 +565,8 @@ module DataMapper
542
565
 
543
566
  class Path
544
567
  include Assertions
568
+
569
+ %w[ id type ].each { |m| undef_method m }
545
570
 
546
571
  attr_reader :relationships, :model, :property, :operator
547
572
 
File without changes
@@ -2,6 +2,8 @@ require 'set'
2
2
 
3
3
  module DataMapper
4
4
  module Resource
5
+ include Assertions
6
+
5
7
  ##
6
8
  #
7
9
  # Appends a module for inclusion into the model class after
@@ -22,12 +24,10 @@ module DataMapper
22
24
  extra_inclusions.concat inclusions
23
25
  true
24
26
  end
25
-
27
+
26
28
  def self.extra_inclusions
27
29
  @extra_inclusions ||= []
28
30
  end
29
-
30
- include Assertions
31
31
 
32
32
  # When Resource is included in a class this method makes sure
33
33
  # it gets all the methods
@@ -334,11 +334,7 @@ module DataMapper
334
334
  # --
335
335
  # @api public
336
336
  def loaded_attributes
337
- names = []
338
- properties.each do |property|
339
- names << property.name if attribute_loaded?(property.name)
340
- end
341
- names
337
+ properties.map{|p| p.name if attribute_loaded?(p.name)}.compact
342
338
  end
343
339
 
344
340
  # set of original values of properties
@@ -471,7 +467,9 @@ module DataMapper
471
467
  # --
472
468
  # @api public
473
469
  def attributes
474
- properties.map{|p| [p.name,send(p.getter)] if p.reader_visibility == :public}.compact.to_hash
470
+ properties.map do |p|
471
+ [p.name, send(p.getter)] if p.reader_visibility == :public
472
+ end.compact.to_hash
475
473
  end
476
474
 
477
475
  # Mass assign of attributes
@@ -485,9 +483,11 @@ module DataMapper
485
483
  values_hash.each_pair do |k,v|
486
484
  setter = "#{k.to_s.sub(/\?\z/, '')}="
487
485
 
488
- # use the attribute mutator if it is public to set the value
489
- next unless respond_to?(setter, false)
490
- send(setter, v)
486
+ if respond_to?(setter)
487
+ send(setter, v)
488
+ else
489
+ raise NameError, "#{setter} is not a public property"
490
+ end
491
491
  end
492
492
  end
493
493
 
@@ -585,7 +585,7 @@ module DataMapper
585
585
  end
586
586
 
587
587
  def lazy_load(name)
588
- reload_attributes(*properties.lazy_load_context(name))
588
+ reload_attributes(*properties.lazy_load_context(name) - loaded_attributes)
589
589
  end
590
590
 
591
591
  def child_associations