dm-core 0.10.1 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (116) hide show
  1. data/.autotest +29 -0
  2. data/.document +5 -0
  3. data/.gitignore +27 -0
  4. data/LICENSE +20 -0
  5. data/{README.txt → README.rdoc} +14 -3
  6. data/Rakefile +23 -22
  7. data/VERSION +1 -0
  8. data/dm-core.gemspec +201 -10
  9. data/lib/dm-core.rb +32 -23
  10. data/lib/dm-core/adapters.rb +0 -1
  11. data/lib/dm-core/adapters/data_objects_adapter.rb +230 -151
  12. data/lib/dm-core/adapters/mysql_adapter.rb +7 -8
  13. data/lib/dm-core/adapters/oracle_adapter.rb +39 -59
  14. data/lib/dm-core/adapters/postgres_adapter.rb +0 -1
  15. data/lib/dm-core/adapters/sqlite3_adapter.rb +5 -0
  16. data/lib/dm-core/adapters/sqlserver_adapter.rb +114 -0
  17. data/lib/dm-core/adapters/yaml_adapter.rb +0 -5
  18. data/lib/dm-core/associations/many_to_many.rb +118 -56
  19. data/lib/dm-core/associations/many_to_one.rb +48 -21
  20. data/lib/dm-core/associations/one_to_many.rb +8 -30
  21. data/lib/dm-core/associations/one_to_one.rb +1 -5
  22. data/lib/dm-core/associations/relationship.rb +89 -97
  23. data/lib/dm-core/collection.rb +299 -184
  24. data/lib/dm-core/core_ext/enumerable.rb +28 -0
  25. data/lib/dm-core/core_ext/kernel.rb +0 -2
  26. data/lib/dm-core/migrations.rb +314 -170
  27. data/lib/dm-core/model.rb +97 -66
  28. data/lib/dm-core/model/descendant_set.rb +1 -1
  29. data/lib/dm-core/model/hook.rb +0 -3
  30. data/lib/dm-core/model/property.rb +7 -10
  31. data/lib/dm-core/model/relationship.rb +79 -26
  32. data/lib/dm-core/model/scope.rb +3 -4
  33. data/lib/dm-core/property.rb +152 -90
  34. data/lib/dm-core/property_set.rb +18 -37
  35. data/lib/dm-core/query.rb +452 -153
  36. data/lib/dm-core/query/conditions/comparison.rb +266 -173
  37. data/lib/dm-core/query/conditions/operation.rb +499 -57
  38. data/lib/dm-core/query/direction.rb +0 -3
  39. data/lib/dm-core/query/operator.rb +0 -4
  40. data/lib/dm-core/query/path.rb +10 -12
  41. data/lib/dm-core/query/sort.rb +4 -10
  42. data/lib/dm-core/repository.rb +10 -6
  43. data/lib/dm-core/resource.rb +343 -148
  44. data/lib/dm-core/spec/adapter_shared_spec.rb +17 -1
  45. data/lib/dm-core/spec/data_objects_adapter_shared_spec.rb +277 -17
  46. data/lib/dm-core/support/chainable.rb +0 -2
  47. data/lib/dm-core/support/equalizer.rb +27 -3
  48. data/lib/dm-core/transaction.rb +75 -75
  49. data/lib/dm-core/type.rb +19 -5
  50. data/lib/dm-core/types/discriminator.rb +4 -4
  51. data/lib/dm-core/types/object.rb +2 -7
  52. data/lib/dm-core/types/paranoid_boolean.rb +8 -2
  53. data/lib/dm-core/types/paranoid_datetime.rb +8 -2
  54. data/lib/dm-core/version.rb +1 -1
  55. data/script/performance.rb +7 -7
  56. data/script/profile.rb +6 -6
  57. data/spec/lib/collection_helpers.rb +2 -2
  58. data/spec/lib/pending_helpers.rb +22 -3
  59. data/spec/lib/rspec_immediate_feedback_formatter.rb +1 -0
  60. data/spec/public/associations/many_to_many_spec.rb +6 -4
  61. data/spec/public/associations/many_to_one_spec.rb +10 -1
  62. data/spec/public/associations/many_to_one_with_boolean_cpk_spec.rb +39 -0
  63. data/spec/public/associations/one_to_many_spec.rb +4 -3
  64. data/spec/public/associations/one_to_one_spec.rb +19 -1
  65. data/spec/public/associations/one_to_one_with_boolean_cpk_spec.rb +45 -0
  66. data/spec/public/collection_spec.rb +4 -3
  67. data/spec/public/migrations_spec.rb +144 -0
  68. data/spec/public/model/relationship_spec.rb +115 -55
  69. data/spec/public/model_spec.rb +13 -13
  70. data/spec/public/property/object_spec.rb +106 -0
  71. data/spec/public/property_spec.rb +18 -14
  72. data/spec/public/resource_spec.rb +10 -1
  73. data/spec/public/sel_spec.rb +16 -49
  74. data/spec/public/setup_spec.rb +1 -1
  75. data/spec/public/shared/association_collection_shared_spec.rb +6 -14
  76. data/spec/public/shared/collection_finder_shared_spec.rb +267 -0
  77. data/spec/public/shared/collection_shared_spec.rb +214 -217
  78. data/spec/public/shared/finder_shared_spec.rb +259 -365
  79. data/spec/public/shared/resource_shared_spec.rb +524 -248
  80. data/spec/public/transaction_spec.rb +27 -3
  81. data/spec/public/types/discriminator_spec.rb +1 -1
  82. data/spec/rcov.opts +6 -0
  83. data/spec/semipublic/adapters/sqlserver_adapter_spec.rb +17 -0
  84. data/spec/semipublic/associations/many_to_one_spec.rb +3 -20
  85. data/spec/semipublic/associations_spec.rb +2 -2
  86. data/spec/semipublic/collection_spec.rb +0 -32
  87. data/spec/semipublic/model_spec.rb +96 -0
  88. data/spec/semipublic/property_spec.rb +3 -3
  89. data/spec/semipublic/query/conditions/comparison_spec.rb +1719 -0
  90. data/spec/semipublic/query/conditions/operation_spec.rb +1292 -0
  91. data/spec/semipublic/query_spec.rb +1285 -144
  92. data/spec/semipublic/resource_spec.rb +0 -24
  93. data/spec/semipublic/shared/resource_shared_spec.rb +103 -38
  94. data/spec/spec.opts +1 -1
  95. data/spec/spec_helper.rb +15 -6
  96. data/tasks/ci.rake +1 -0
  97. data/tasks/metrics.rake +37 -0
  98. data/tasks/spec.rake +41 -0
  99. data/tasks/yard.rake +9 -0
  100. data/tasks/yardstick.rake +19 -0
  101. metadata +99 -29
  102. data/CONTRIBUTING +0 -51
  103. data/FAQ +0 -93
  104. data/History.txt +0 -27
  105. data/MIT-LICENSE +0 -22
  106. data/Manifest.txt +0 -121
  107. data/QUICKLINKS +0 -11
  108. data/SPECS +0 -35
  109. data/TODO +0 -1
  110. data/spec/semipublic/query/conditions_spec.rb +0 -528
  111. data/tasks/ci.rb +0 -24
  112. data/tasks/dm.rb +0 -58
  113. data/tasks/doc.rb +0 -17
  114. data/tasks/gemspec.rb +0 -23
  115. data/tasks/hoe.rb +0 -45
  116. data/tasks/install.rb +0 -18
@@ -18,14 +18,12 @@ module DataMapper
18
18
  deprecate :property, :target
19
19
  deprecate :direction, :operator
20
20
 
21
- # TODO: document
22
21
  # @api private
23
22
  def reverse!
24
23
  @operator = @operator == :asc ? :desc : :asc
25
24
  self
26
25
  end
27
26
 
28
- # TODO: document
29
27
  # @api private
30
28
  def get(resource)
31
29
  Sort.new(target.get(resource), @operator == :asc)
@@ -33,7 +31,6 @@ module DataMapper
33
31
 
34
32
  private
35
33
 
36
- # TODO: document
37
34
  # @api private
38
35
  def initialize(target, operator = :asc)
39
36
  super
@@ -13,15 +13,12 @@ module DataMapper
13
13
 
14
14
  equalize :target, :operator
15
15
 
16
- # TODO: document
17
16
  # @api private
18
17
  attr_reader :target
19
18
 
20
- # TODO: document
21
19
  # @api private
22
20
  attr_reader :operator
23
21
 
24
- # TODO: document
25
22
  # @api private
26
23
  def inspect
27
24
  "#<#{self.class.name} @target=#{target.inspect} @operator=#{operator.inspect}>"
@@ -29,7 +26,6 @@ module DataMapper
29
26
 
30
27
  private
31
28
 
32
- # TODO: document
33
29
  # @api private
34
30
  def initialize(target, operator)
35
31
  assert_kind_of 'operator', operator, Symbol
@@ -7,26 +7,27 @@
7
7
  module DataMapper
8
8
  class Query
9
9
  class Path
10
- instance_methods.each { |method| undef_method method unless %w[ __id__ __send__ send class dup object_id kind_of? instance_of? respond_to? equal? should should_not instance_variable_set instance_variable_get extend hash inspect ].include?(method.to_s) }
10
+ # TODO: replace this with BasicObject
11
+ instance_methods.each do |method|
12
+ next if method =~ /\A__/ ||
13
+ %w[ send class dup object_id kind_of? instance_of? respond_to? equal? should should_not instance_variable_set instance_variable_get instance_variable_defined? extend hash inspect copy_object ].include?(method.to_s)
14
+ undef_method method
15
+ end
11
16
 
12
17
  include Extlib::Assertions
13
18
  extend Equalizer
14
19
 
15
20
  equalize :relationships, :property
16
21
 
17
- # TODO: document
18
22
  # @api semipublic
19
23
  attr_reader :repository_name
20
24
 
21
- # TODO: document
22
25
  # @api semipublic
23
26
  attr_reader :relationships
24
27
 
25
- # TODO: document
26
28
  # @api semipublic
27
29
  attr_reader :model
28
30
 
29
- # TODO: document
30
31
  # @api semipublic
31
32
  attr_reader :property
32
33
 
@@ -39,19 +40,16 @@ module DataMapper
39
40
  RUBY
40
41
  end
41
42
 
42
- # TODO: document
43
43
  # @api public
44
44
  def kind_of?(klass)
45
45
  super || (defined?(@property) ? @property.kind_of?(klass) : false)
46
46
  end
47
47
 
48
- # TODO: document
49
48
  # @api public
50
49
  def instance_of?(klass)
51
50
  super || (defined?(@property) ? @property.instance_of?(klass) : false)
52
51
  end
53
52
 
54
- # TODO: document
55
53
  # @api semipublic
56
54
  def respond_to?(method, include_private = false)
57
55
  super ||
@@ -62,7 +60,6 @@ module DataMapper
62
60
 
63
61
  private
64
62
 
65
- # TODO: document
66
63
  # @api semipublic
67
64
  def initialize(relationships, property_name = nil)
68
65
  assert_kind_of 'relationships', relationships, Array
@@ -80,19 +77,20 @@ module DataMapper
80
77
  end
81
78
  end
82
79
 
83
- # TODO: document
84
80
  # @api semipublic
85
81
  def method_missing(method, *args)
86
82
  if @property
87
83
  return @property.send(method, *args)
88
84
  end
89
85
 
86
+ path_class = self.class
87
+
90
88
  if relationship = @model.relationships(@repository_name)[method]
91
- return self.class.new(@relationships.dup << relationship)
89
+ return path_class.new(@relationships.dup << relationship)
92
90
  end
93
91
 
94
92
  if @model.properties(@repository_name).named?(method)
95
- return self.class.new(@relationships, method)
93
+ return path_class.new(@relationships, method)
96
94
  end
97
95
 
98
96
  raise NoMethodError, "undefined property or relationship '#{method}' on #{@model}"
@@ -3,28 +3,23 @@
3
3
  module DataMapper
4
4
  class Query
5
5
  class Sort
6
- # TODO: document
7
6
  # @api semipublic
8
7
  attr_reader :value
9
8
 
10
- # TODO: document
11
9
  # @api semipublic
12
10
  def direction
13
11
  @ascending ? :ascending : :descending
14
12
  end
15
13
 
16
- # TODO: document
17
14
  # @api private
18
15
  def <=>(other)
19
16
  other_value = other.value
17
+ value_nil = @value.nil?
18
+ other_nil = other_value.nil?
20
19
 
21
20
  cmp = case
22
- when @value.nil? && other_value.nil?
23
- 0
24
- when @value.nil?
25
- 1
26
- when other_value.nil?
27
- -1
21
+ when value_nil then other_nil ? 0 : 1
22
+ when other_nil then -1
28
23
  else
29
24
  @value <=> other_value
30
25
  end
@@ -34,7 +29,6 @@ module DataMapper
34
29
 
35
30
  private
36
31
 
37
- # TODO: document
38
32
  # @api private
39
33
  def initialize(value, ascending = true)
40
34
  @value = value
@@ -42,7 +42,6 @@ module DataMapper
42
42
  :default
43
43
  end
44
44
 
45
- # TODO: document
46
45
  # @api semipublic
47
46
  attr_reader :name
48
47
 
@@ -64,10 +63,13 @@ module DataMapper
64
63
  # needed. Do not remove this code.
65
64
  @adapter ||=
66
65
  begin
67
- raise RepositoryNotSetupError, "Adapter not set: #{@name}. Did you forget to setup?" \
68
- unless self.class.adapters.key?(@name)
66
+ adapters = self.class.adapters
69
67
 
70
- self.class.adapters[@name]
68
+ unless adapters.key?(@name)
69
+ raise RepositoryNotSetupError, "Adapter not set: #{@name}. Did you forget to setup?"
70
+ end
71
+
72
+ adapters[@name]
71
73
  end
72
74
  end
73
75
 
@@ -101,12 +103,14 @@ module DataMapper
101
103
  #
102
104
  # @api private
103
105
  def scope
104
- Repository.context << self
106
+ context = Repository.context
107
+
108
+ context << self
105
109
 
106
110
  begin
107
111
  yield self
108
112
  ensure
109
- Repository.context.pop
113
+ context.pop
110
114
  end
111
115
  end
112
116
 
@@ -30,16 +30,18 @@ module DataMapper
30
30
  #
31
31
  # @deprecated
32
32
  def update_attributes(attributes = {}, *allowed)
33
- assert_update_clean_only(:update_attributes)
33
+ model = self.model
34
+ caller = caller[0]
34
35
 
35
- warn "#{model}#update_attributes is deprecated, use #{model}#update instead (#{caller[0]})"
36
+ warn "#{model}#update_attributes is deprecated, use #{model}#update instead (#{caller})"
36
37
 
37
38
  if allowed.any?
38
39
  warn "specifying allowed in #{model}#update_attributes is deprecated, " \
39
- "use Hash#only to filter the attributes in the caller (#{caller[0]})"
40
+ "use Hash#only to filter the attributes in the caller (#{caller})"
40
41
  attributes = attributes.only(*allowed)
41
42
  end
42
43
 
44
+ assert_update_clean_only(:update_attributes)
43
45
  update(attributes)
44
46
  end
45
47
 
@@ -50,13 +52,6 @@ module DataMapper
50
52
  model.extend Model
51
53
  end
52
54
 
53
- # Collection this resource associated with.
54
- # Used by SEL.
55
- #
56
- # @api private
57
- attr_writer :collection
58
-
59
- # TODO: document
60
55
  # @api public
61
56
  alias_method :model, :class
62
57
 
@@ -69,8 +64,8 @@ module DataMapper
69
64
  #
70
65
  # @api semipublic
71
66
  def repository
72
- # only set @repository explicitly when persisted
73
- defined?(@repository) ? @repository : model.repository
67
+ # only set @_repository explicitly when persisted
68
+ defined?(@_repository) ? @_repository : model.repository
74
69
  end
75
70
 
76
71
  # Retrieve the key(s) for this resource.
@@ -84,18 +79,16 @@ module DataMapper
84
79
  #
85
80
  # @api public
86
81
  def key
87
- return @key if defined?(@key)
82
+ return @_key if defined?(@_key)
83
+
84
+ model_key = model.key(repository_name)
88
85
 
89
- key = model.key(repository_name).map do |property|
86
+ key = model_key.map do |property|
90
87
  original_attributes[property] || (property.loaded?(self) ? property.get!(self) : nil)
91
88
  end
92
89
 
93
- return unless key.all?
94
-
95
- # memoize the key if the Resource is not frozen
96
- @key = key unless frozen?
97
-
98
- key
90
+ # only memoize a valid key
91
+ @_key = key if model_key.valid?(key)
99
92
  end
100
93
 
101
94
  # Checks if this Resource instance is new
@@ -115,7 +108,7 @@ module DataMapper
115
108
  #
116
109
  # @api public
117
110
  def saved?
118
- @saved == true
111
+ @_saved == true
119
112
  end
120
113
 
121
114
  # Checks if this Resource instance is destroyed
@@ -125,7 +118,7 @@ module DataMapper
125
118
  #
126
119
  # @api public
127
120
  def destroyed?
128
- @destroyed == true
121
+ @_destroyed == true
129
122
  end
130
123
 
131
124
  # Checks if the resource has no changes to save
@@ -145,15 +138,21 @@ module DataMapper
145
138
  #
146
139
  # @api public
147
140
  def dirty?
148
- if original_attributes.any?
149
- true
150
- elsif new?
151
- model.serial || properties.any? { |property| property.default? }
152
- else
153
- false
141
+ run_once(true) do
142
+ dirty_self? || dirty_parents? || dirty_children?
154
143
  end
155
144
  end
156
145
 
146
+ # Checks if this Resource instance is readonly
147
+ #
148
+ # @return [Boolean]
149
+ # true if the resource cannot be persisted
150
+ #
151
+ # @api public
152
+ def readonly?
153
+ @_readonly == true
154
+ end
155
+
157
156
  # Returns the value of the attribute.
158
157
  #
159
158
  # Do not read from instance variables directly, but use this method.
@@ -247,7 +246,9 @@ module DataMapper
247
246
  # @api public
248
247
  def attributes(key_on = :name)
249
248
  attributes = {}
250
- properties.each do |property|
249
+
250
+ lazy_load(properties)
251
+ fields.each do |property|
251
252
  if model.public_method_defined?(name = property.name)
252
253
  key = case key_on
253
254
  when :name then name
@@ -255,9 +256,10 @@ module DataMapper
255
256
  else property
256
257
  end
257
258
 
258
- attributes[key] = send(name)
259
+ attributes[key] = __send__(name)
259
260
  end
260
261
  end
262
+
261
263
  attributes
262
264
  end
263
265
 
@@ -271,11 +273,12 @@ module DataMapper
271
273
  #
272
274
  # @api public
273
275
  def attributes=(attributes)
276
+ model = self.model
274
277
  attributes.each do |name, value|
275
278
  case name
276
279
  when String, Symbol
277
280
  if model.public_method_defined?(setter = "#{name}=")
278
- send(setter, value)
281
+ __send__(setter, value)
279
282
  else
280
283
  raise ArgumentError, "The attribute '#{name}' is not accessible in #{model}"
281
284
  end
@@ -287,14 +290,21 @@ module DataMapper
287
290
 
288
291
  # Reloads association and all child association
289
292
  #
293
+ # This is accomplished by resetting the Resource key to it's
294
+ # original value, and then removing all the ivars for properties
295
+ # and relationships. On the next access of those ivars, the
296
+ # resource will eager load what it needs. While this is more of
297
+ # a lazy reload, it should result is more consistent behavior
298
+ # since no cached results will remain from the initial load.
299
+ #
290
300
  # @return [Resource]
291
301
  # the receiver, the current Resource instance
292
302
  #
293
303
  # @api public
294
304
  def reload
295
- if saved?
296
- eager_load(loaded_properties)
297
- child_relationships.each { |relationship| relationship.get!(self).reload }
305
+ if key
306
+ reset_key
307
+ clear_subjects
298
308
  end
299
309
 
300
310
  self
@@ -309,12 +319,10 @@ module DataMapper
309
319
  # true if resource and storage state match
310
320
  #
311
321
  # @api public
312
- chainable do
313
- def update(attributes = {})
314
- assert_update_clean_only(:update)
315
- self.attributes = attributes
316
- save
317
- end
322
+ def update(attributes = {})
323
+ assert_update_clean_only(:update)
324
+ self.attributes = attributes
325
+ save
318
326
  end
319
327
 
320
328
  # Updates attributes and saves this Resource instance, bypassing hooks
@@ -338,10 +346,9 @@ module DataMapper
338
346
  # true if Resource instance and all associations were saved
339
347
  #
340
348
  # @api public
341
- chainable do
342
- def save
343
- save_parents && save_self && save_children
344
- end
349
+ def save
350
+ assert_not_destroyed(:save)
351
+ _save(true)
345
352
  end
346
353
 
347
354
  # Save the instance and loaded, dirty associations to the data-store, bypassing hooks
@@ -351,7 +358,8 @@ module DataMapper
351
358
  #
352
359
  # @api public
353
360
  def save!
354
- save_parents(false) && save_self(false) && save_children(false)
361
+ assert_not_destroyed(:save!)
362
+ _save(false)
355
363
  end
356
364
 
357
365
  # Destroy the instance, remove it from the repository
@@ -360,10 +368,8 @@ module DataMapper
360
368
  # true if resource was destroyed
361
369
  #
362
370
  # @api public
363
- chainable do
364
- def destroy
365
- destroy!
366
- end
371
+ def destroy
372
+ destroy!
367
373
  end
368
374
 
369
375
  # Destroy the instance, remove it from the repository, bypassing hooks
@@ -373,21 +379,23 @@ module DataMapper
373
379
  #
374
380
  # @api public
375
381
  def destroy!
376
- if saved? && repository.delete(Collection.new(query, [ self ])) == 1
377
- @destroyed = true
378
- @collection.delete(self) if @collection
382
+ return true if destroyed?
383
+
384
+ if saved?
385
+ repository.delete(collection_for_self)
379
386
  reset
380
- freeze
387
+ @_readonly = true
388
+ @_destroyed = true
389
+ else
390
+ false
381
391
  end
382
-
383
- destroyed?
384
392
  end
385
393
 
386
394
  # Compares another Resource for equality
387
395
  #
388
- # Resource is equal to +other+ if they are the same object (identity)
389
- # or if they are both of the *same model* and all of their attributes
390
- # are equivalent
396
+ # Resource is equal to +other+ if they are the same object
397
+ # (identical object_id) or if they are both of the *same model* and
398
+ # all of their attributes are equivalent
391
399
  #
392
400
  # @param [Resource] other
393
401
  # the other Resource to compare with
@@ -403,9 +411,8 @@ module DataMapper
403
411
 
404
412
  # Compares another Resource for equivalency
405
413
  #
406
- # Resource is equal to +other+ if they are the same object (identity)
407
- # or if they are both of the *same base model* and all of their attributes
408
- # are equivalent
414
+ # Resource is equivalent to +other+ if they are the same object
415
+ # (identical object_id) or all of their attribute are equivalent
409
416
  #
410
417
  # @param [Resource] other
411
418
  # the other Resource to compare with
@@ -416,8 +423,9 @@ module DataMapper
416
423
  # @api public
417
424
  def ==(other)
418
425
  return true if equal?(other)
419
- other.respond_to?(:model) &&
420
- model.base_model.equal?(other.model.base_model) &&
426
+ other.respond_to?(:repository) &&
427
+ other.respond_to?(:key) &&
428
+ other.respond_to?(:clean?) &&
421
429
  cmp?(other, :==)
422
430
  end
423
431
 
@@ -433,6 +441,7 @@ module DataMapper
433
441
  #
434
442
  # @api public
435
443
  def <=>(other)
444
+ model = self.model
436
445
  unless other.kind_of?(model.base_model)
437
446
  raise ArgumentError, "Cannot compare a #{other.model} instance with a #{model} instance"
438
447
  end
@@ -488,7 +497,7 @@ module DataMapper
488
497
  #
489
498
  # @api semipublic
490
499
  def original_attributes
491
- @original_attributes ||= {}
500
+ @_original_attributes ||= {}
492
501
  end
493
502
 
494
503
  # Checks if an attribute has been loaded from the repository
@@ -543,70 +552,64 @@ module DataMapper
543
552
  dirty_attributes
544
553
  end
545
554
 
546
- # Saves the resource
547
- #
548
- # @return [Boolean]
549
- # true if the resource was successfully saved
555
+ # Reset the Resource to a similar state as a new record:
556
+ # removes it from identity map and clears original property
557
+ # values (thus making all properties non dirty)
550
558
  #
551
- # @api semipublic
552
- def save_self(safe = true)
553
- if safe
554
- new? ? create_hook : update_hook
555
- else
556
- new? ? _create : _update
557
- end
559
+ # @api private
560
+ def reset
561
+ @_saved = false
562
+ remove_from_identity_map
563
+ original_attributes.clear
564
+ self
558
565
  end
559
566
 
560
- # Saves the parent resources
567
+ # Returns the Collection the Resource is associated with
561
568
  #
562
- # @return [Boolean]
563
- # true if the parents were successfully saved
569
+ # @return [nil]
570
+ # nil if this is a new record
571
+ # @return [Collection]
572
+ # a Collection that self belongs to
564
573
  #
565
574
  # @api private
566
- def save_parents(safe = true)
567
- parent_relationships.all? do |relationship|
568
- parent = relationship.get!(self)
569
- if parent.dirty? ? parent.save_parents(safe) && parent.save_self(safe) : parent.saved?
570
- relationship.set(self, parent) # set the FK values
571
- end
572
- end
575
+ def collection
576
+ return @_collection if (@_collection && @_collection.query.conditions.matches?(self)) || new? || readonly?
577
+ collection_for_self
573
578
  end
574
579
 
575
- # Saves the children resources
580
+ # Associates a Resource to a Collection
576
581
  #
577
- # @return [Boolean]
578
- # true if the children were successfully saved
582
+ # @param [Collection, nil] collection
583
+ # the collection to associate the resource with
584
+ #
585
+ # @return [nil]
586
+ # nil if this is a new record
587
+ # @return [Collection]
588
+ # a Collection that self belongs to
579
589
  #
580
590
  # @api private
581
- def save_children(safe = true)
582
- child_relationships.all? do |relationship|
583
- association = relationship.get!(self)
584
- safe ? association.save : association.save!
585
- end
591
+ def collection=(collection)
592
+ @_collection = collection
586
593
  end
587
594
 
588
- # Reset the Resource to a similar state as a new record:
589
- # removes it from identity map and clears original property
590
- # values (thus making all properties non dirty)
595
+ # Return a collection including the current resource only
596
+ #
597
+ # @return [Collection]
598
+ # a collection containing self
591
599
  #
592
600
  # @api private
593
- def reset
594
- @saved = false
595
- identity_map.delete(key)
596
- original_attributes.clear
597
- self
601
+ def collection_for_self
602
+ Collection.new(query, [ self ])
598
603
  end
599
604
 
600
- # Gets a Collection with the current Resource instance as its only member
605
+ # Returns a Query that will match the resource
601
606
  #
602
- # @return [Collection, FalseClass]
603
- # nil if this is a new record,
604
- # otherwise a Collection with self as its only member
607
+ # @return [Query]
608
+ # Query that will match the resource
605
609
  #
606
- # @api private
607
- def collection
608
- return @collection if @collection || new? || frozen?
609
- @collection = Collection.new(query, [ self ])
610
+ # @api semipublic
611
+ def query
612
+ Query.new(repository, model, :fields => fields, :conditions => conditions)
610
613
  end
611
614
 
612
615
  protected
@@ -682,11 +685,21 @@ module DataMapper
682
685
  # @return [IdentityMap]
683
686
  # identity map of repository this object was loaded from
684
687
  #
685
- # @api semipublic
688
+ # @api private
686
689
  def identity_map
687
690
  repository.identity_map(model)
688
691
  end
689
692
 
693
+ # @api private
694
+ def add_to_identity_map
695
+ identity_map[key] = self
696
+ end
697
+
698
+ # @api private
699
+ def remove_from_identity_map
700
+ identity_map.delete(key)
701
+ end
702
+
690
703
  # Fetches all the names of the attributes that have been loaded,
691
704
  # even if they are lazy but have been called
692
705
  #
@@ -694,58 +707,94 @@ module DataMapper
694
707
  # names of attributes that have been loaded
695
708
  #
696
709
  # @api private
697
- def loaded_properties
698
- properties.select { |property| property.loaded?(self) }
710
+ def fields
711
+ properties.select do |property|
712
+ property.loaded?(self) || (new? && property.default?)
713
+ end
714
+ end
715
+
716
+ # Reset the key to the original value
717
+ #
718
+ # @return [undefined]
719
+ #
720
+ # @api private
721
+ def reset_key
722
+ properties.key.zip(key) do |property, value|
723
+ property.set!(self, value)
724
+ original_attributes.delete(property)
725
+ end
726
+ end
727
+
728
+ # Remove all the ivars for properties and relationships
729
+ #
730
+ # @return [undefined]
731
+ #
732
+ # @api private
733
+ def clear_subjects
734
+ model_properties = properties
735
+
736
+ (model_properties - model_properties.key | relationships.values).each do |subject|
737
+ next unless subject.loaded?(self)
738
+ remove_instance_variable(subject.instance_variable_name)
739
+ original_attributes.delete(subject)
740
+ end
699
741
  end
700
742
 
701
743
  # Lazy loads attributes not yet loaded
702
744
  #
703
- # @param [Array<Property>] fields
745
+ # @param [Array<Property>] properties
704
746
  # the properties to reload
705
747
  #
706
748
  # @return [self]
707
749
  #
708
750
  # @api private
709
- def lazy_load(fields)
710
- eager_load(fields - loaded_properties)
751
+ def lazy_load(properties)
752
+ eager_load(properties - fields)
711
753
  end
712
754
 
713
755
  # Reloads specified attributes
714
756
  #
715
- # @param [Array<Property>] fields
757
+ # @param [Array<Property>] properties
716
758
  # the properties to reload
717
759
  #
718
760
  # @return [Resource]
719
761
  # the receiver, the current Resource instance
720
762
  #
721
763
  # @api private
722
- def eager_load(fields)
723
- unless fields.empty? || new?
724
- collection.reload(:fields => fields)
764
+ def eager_load(properties)
765
+ unless properties.empty? || key.nil?
766
+ collection.reload(:fields => properties)
725
767
  end
726
768
 
727
769
  self
728
770
  end
729
771
 
730
- # Gets a Query that will return this Resource instance
772
+ # Return conditions to match the Resource
731
773
  #
732
- # @return [Query]
733
- # Query that will retrieve this Resource instance
774
+ # @return [Hash]
775
+ # query conditions
734
776
  #
735
777
  # @api private
736
- def query
737
- Query.new(repository, model, model.key_conditions(repository, key))
778
+ def conditions
779
+ key = self.key
780
+ if key
781
+ model.key_conditions(repository, key)
782
+ else
783
+ conditions = {}
784
+ properties.each do |property|
785
+ next unless property.loaded?(self)
786
+ conditions[property] = property.get!(self)
787
+ end
788
+ conditions
789
+ end
738
790
  end
739
791
 
740
- # TODO: document
741
792
  # @api private
742
793
  def parent_relationships
743
794
  parent_relationships = []
744
795
 
745
796
  relationships.each_value do |relationship|
746
- next unless relationship.respond_to?(:resource_for) && relationship.loaded?(self)
747
- next unless relationship.get(self)
748
-
797
+ next unless relationship.respond_to?(:resource_for) && relationship.loaded?(self) && relationship.get!(self)
749
798
  parent_relationships << relationship
750
799
  end
751
800
 
@@ -762,11 +811,7 @@ module DataMapper
762
811
  child_relationships = []
763
812
 
764
813
  relationships.each_value do |relationship|
765
- next unless relationship.respond_to?(:collection_for) && relationship.loaded?(self)
766
-
767
- association = relationship.get!(self)
768
- next unless association.loaded? || association.head.any? || association.tail.any?
769
-
814
+ next unless relationship.respond_to?(:collection_for) && relationship.loaded?(self) && relationship.get!(self)
770
815
  child_relationships << relationship
771
816
  end
772
817
 
@@ -777,6 +822,16 @@ module DataMapper
777
822
  many_to_many + other
778
823
  end
779
824
 
825
+ # @api private
826
+ def parent_resources
827
+ parent_relationships.map { |relationship| relationship.get!(self) }
828
+ end
829
+
830
+ # @api private
831
+ def child_collections
832
+ child_relationships.map { |relationship| relationship.get!(self) }
833
+ end
834
+
780
835
  # Creates the resource with default values
781
836
  #
782
837
  # If resource is not dirty or a new (not yet saved),
@@ -796,7 +851,7 @@ module DataMapper
796
851
  # @api private
797
852
  def _create
798
853
  # Can't create a resource that is not dirty and doesn't have serial keys
799
- return false if new? && !dirty?
854
+ return false if new? && clean?
800
855
 
801
856
  # set defaults for new resource
802
857
  properties.each do |property|
@@ -805,14 +860,14 @@ module DataMapper
805
860
  end
806
861
  end
807
862
 
808
- repository.create([ self ])
863
+ @_repository = repository
864
+ @_repository.create([ self ])
809
865
 
810
- @repository = repository
811
- @saved = true
866
+ @_saved = true
812
867
 
813
868
  original_attributes.clear
814
869
 
815
- identity_map[key] = self
870
+ add_to_identity_map
816
871
 
817
872
  true
818
873
  end
@@ -828,29 +883,124 @@ module DataMapper
828
883
  #
829
884
  # @api private
830
885
  def _update
831
- dirty_attributes = self.dirty_attributes
886
+ original_attributes = self.original_attributes
832
887
 
833
- if dirty_attributes.empty?
888
+ if original_attributes.empty?
834
889
  true
835
- elsif dirty_attributes.any? { |property, value| !property.nullable? && value.nil? }
890
+ elsif original_attributes.any? { |property, _value| !property.valid?(property.get!(self)) }
836
891
  false
837
892
  else
838
893
  # remove from the identity map
839
- identity_map.delete(key)
894
+ remove_from_identity_map
840
895
 
841
- return false unless repository.update(dirty_attributes, Collection.new(query, [ self ])) == 1
896
+ repository.update(dirty_attributes, collection_for_self)
897
+
898
+ original_attributes.clear
842
899
 
843
900
  # remove the cached key in case it is updated
844
- remove_instance_variable(:@key)
901
+ remove_instance_variable(:@_key)
845
902
 
846
- original_attributes.clear
903
+ add_to_identity_map
904
+
905
+ true
906
+ end
907
+ end
847
908
 
848
- identity_map[key] = self
909
+ # @api private
910
+ def _save(safe)
911
+ run_once(true) do
912
+ save_parents(safe) && save_self(safe) && save_children(safe)
913
+ end
914
+ end
915
+
916
+ # Saves the resource
917
+ #
918
+ # @return [Boolean]
919
+ # true if the resource was successfully saved
920
+ #
921
+ # @api semipublic
922
+ def save_self(safe = true)
923
+ new_resource = new?
924
+ if safe
925
+ new_resource ? create_hook : update_hook
926
+ else
927
+ new_resource ? _create : _update
928
+ end
929
+ end
930
+
931
+ # Saves the parent resources
932
+ #
933
+ # @return [Boolean]
934
+ # true if the parents were successfully saved
935
+ #
936
+ # @api private
937
+ def save_parents(safe)
938
+ run_once(true) do
939
+ parent_relationships.all? do |relationship|
940
+ parent = relationship.get!(self)
941
+
942
+ if parent.__send__(:save_parents, safe) && parent.__send__(:save_self, safe)
943
+ relationship.set(self, parent) # set the FK values
944
+ end
945
+ end
946
+ end
947
+ end
948
+
949
+ # Saves the children resources
950
+ #
951
+ # @return [Boolean]
952
+ # true if the children were successfully saved
953
+ #
954
+ # @api private
955
+ def save_children(safe)
956
+ child_collections.all? do |collection|
957
+ collection.send(safe ? :save : :save!)
958
+ end
959
+ end
849
960
 
961
+ # Checks if the resource has unsaved changes
962
+ #
963
+ # @return [Boolean]
964
+ # true if the resource has unsaged changes
965
+ #
966
+ # @api private
967
+ def dirty_self?
968
+ if original_attributes.any?
850
969
  true
970
+ elsif new?
971
+ !model.serial.nil? || properties.any? { |property| property.default? }
972
+ else
973
+ false
851
974
  end
852
975
  end
853
976
 
977
+ # Checks if the parents have unsaved changes
978
+ #
979
+ # @return [Boolean]
980
+ # true if the parents have unsaved changes
981
+ #
982
+ # @api private
983
+ def dirty_parents?
984
+ run_once(false) do
985
+ parent_resources.any? do |parent|
986
+ parent.__send__(:dirty_self?) || parent.__send__(:dirty_parents?)
987
+ end
988
+ end
989
+ end
990
+
991
+ # Checks if the children have unsaved changes
992
+ #
993
+ # @param [Hash] resources
994
+ # resources that have already been tested
995
+ #
996
+ # @return [Boolean]
997
+ # true if the children have unsaved changes
998
+ #
999
+ # @api private
1000
+ def dirty_children?
1001
+ child_collections.any? { |children| children.dirty? }
1002
+ end
1003
+
854
1004
  # Return true if +other+'s is equivalent or equal to +self+'s
855
1005
  #
856
1006
  # @param [Resource] other
@@ -864,7 +1014,7 @@ module DataMapper
864
1014
  # @api private
865
1015
  def cmp?(other, operator)
866
1016
  return false unless key.send(operator, other.key)
867
- return true if repository.send(operator, other.repository) && !dirty? && !other.dirty?
1017
+ return true if repository.send(operator, other.repository) && clean? && other.clean?
868
1018
 
869
1019
  # get all the loaded and non-loaded properties that are not keys,
870
1020
  # since the key comparison was performed earlier
@@ -888,9 +1038,54 @@ module DataMapper
888
1038
  #
889
1039
  # @api private
890
1040
  def assert_update_clean_only(method)
891
- if original_attributes.any?
1041
+ if dirty?
892
1042
  raise UpdateConflictError, "#{model}##{method} cannot be called on a dirty resource"
893
1043
  end
894
1044
  end
1045
+
1046
+ # Raises an exception if #save is performed on a destroyed resource
1047
+ #
1048
+ # @param [Symbol] method
1049
+ # the name of the method to use in the exception
1050
+ #
1051
+ # @return [undefined]
1052
+ #
1053
+ # @raise [PersistenceError]
1054
+ # raise if the resource is destroyed
1055
+ #
1056
+ # @api private
1057
+ def assert_not_destroyed(method)
1058
+ if destroyed?
1059
+ raise PersistenceError, "#{model}##{method} cannot be called on a destroyed resource"
1060
+ end
1061
+ end
1062
+
1063
+ # Prevent a method from being in the stack more than once
1064
+ #
1065
+ # The purpose of this method is to prevent SystemStackError from
1066
+ # being thrown from methods from encountering infinite recursion
1067
+ # when called on resources having circular dependencies.
1068
+ #
1069
+ # @param [Object] default
1070
+ # default return value
1071
+ #
1072
+ # @yield The block of code to run once
1073
+ #
1074
+ # @return [Object]
1075
+ # block return value
1076
+ #
1077
+ # @api private
1078
+ def run_once(default)
1079
+ caller_method = Kernel.caller(1).first[/`([^'?!]+)[?!]?'/, 1]
1080
+ sentinel = "@_#{caller_method}_sentinel"
1081
+ return instance_variable_get(sentinel) if instance_variable_defined?(sentinel)
1082
+
1083
+ begin
1084
+ instance_variable_set(sentinel, default)
1085
+ yield
1086
+ ensure
1087
+ remove_instance_variable(sentinel)
1088
+ end
1089
+ end
895
1090
  end # module Resource
896
1091
  end # module DataMapper